前端(vue)入門到精通課程:進入學習
vue 是一個易上手的框架,許多便捷功能都在其內部做了整合,其中最有區別性的功能就是其潛藏於底層的響應式系統。元件狀態都是響應式的 JavaScript 物件。當更改它們時,檢視會隨即更新,這讓狀態管理更加簡單直觀。那麼,Vue 響應性系統是如何實現的呢?本文也是在閱讀了 Vue 原始碼後的理解以及模仿實現,所以跟隨作者的思路,我們一起由淺入深的探索一下vue吧!【相關推薦:】
本文 Vue 原始碼版本:2.6.14,為了便於理解,程式碼都最簡化。
當你把一個普通的 JavaScript 物件傳入 Vue 範例作為 data 選項,Vue 將遍歷此物件所有的 property,並使用 Object.defineProperty 把這些 property 全部轉為 getter/setter,然後圍繞 getter/setter來執行。
一句話概括Vue 的響應式系統就是: 觀察者模式 + Object.defineProperty 攔截getter/setter
Object.defineProperty()
方法會直接在一個物件上定義一個新屬性,或者修改一個物件的現有屬性,並返回此物件。
簡單的說,就是通過此方式定義的 property,執行 obj.xxx
時會觸發 get,執行 obj.xxx = xxx
會觸發 set,這便是響應式的關鍵。
Object.defineProperty 是 ES5 中一個無法 shim(無法通過polyfill實現) 的特性,這也就是 Vue 不支援 IE8 以及更低版本瀏覽器的原因。
現在,我們來基於Object.defineProperty
實現一個簡易的響應式更新系統作為「開胃菜」
let data = {}; // 使用一箇中間變數儲存 value let value = "hello"; // 用一個集合儲存資料的響應更新函數 let fnSet = new Set(); // 在 data 上定義 text 屬性 Object.defineProperty(data, "text", { enumerable: true, configurable: true, set(newValue) { value = newValue; // 資料變化 fnSet.forEach((fn) => fn()); }, get() { fnSet.add(fn); return value; }, }); // 將 data.text 渲染到頁面上 function fn() { document.body.innerText = data.text; } // 執行函數,觸發讀取 get fn(); // 一秒後改變資料,觸發 set 更新 setTimeout(() => { data.text = "world"; }, 1000);
接下來我們在瀏覽器中執行這段程式碼,會得到期望的效果
通過上面的程式碼,我想你對響應式系統的工作原理已經有了一定的理解。為了讓這個「開胃菜」易於消化,這個簡易的響應式系統還有很多缺點,例如:資料和響應更新函數是通過寫死強耦合在一起的、只實現了一對一的情況、不夠模組化等等……所以接下來,我們來一一完善。
要設計一個完善的響應式系統,我們需要先了解一個前置知識,什麼是觀察者模式?
什麼是觀察者模式?
它就是一種行為設計模式, 允許你定義一種訂閱機制, 可在物件事件發生時通知多個 「觀察」 該物件的其他物件。
擁有一些值得關注狀態的物件通常被稱為目標,由於它自身狀態發生改變時需要通知其他物件,我們也將其成為釋出者(publisher) 。所有希望關注釋出者狀態變化的其他物件被稱為訂閱者(subscribers) 。此外,釋出者與所有訂閱者直接僅通過介面互動,都必須具有同樣的介面。
舉個例子?:
你(即應用中的訂閱者)對某個書店的週刊感興趣,你給老闆(即應用中的釋出者)留了電話,讓老闆一有新週刊就給你打電話,其他對這本週刊感興趣的人,也給老闆留了電話。新週刊到貨時,老闆就挨個打電話,通知讀者來取。
假如某個讀者一不小心留的是 qq 號,不是電話號碼,老版打電話時就會打不通,該讀者就收不到通知了。這就是我們上面說的,必須具有相同的介面。
瞭解了觀察者模式後,我們就開始著手設計響應式系統。
抽象觀察者(訂閱者)類Watcher
在上面的例子中,資料和響應更新函數是通過寫死強耦合在一起的。而實際開發過程中,更新函數不一定叫fn
,更有可能是一個匿名函數。所以我們需要抽像一個觀察者(訂閱者)類Watcher
來儲存並執行更新函數,同時向外提供一個update
更新介面。
// Watcher 觀察者可能有 n 個,我們為了區分它們,保證唯一性,增加一個 uid let watcherId = 0; // 當前活躍的 Watcher let activeWatcher = null; class Watcher { constructor(cb) { this.uid = watcherId++; // 更新函數 this.cb = cb; // 儲存 watcher 訂閱的所有資料 this.deps = []; // 初始化時執行更新函數 this.get(); } // 求值函數 get() { // 呼叫更新函數時,將 activeWatcher 指向當前 watcher activeWatcher = this; this.cb(); // 呼叫完重置 activeWatcher = null; } // 資料更新時,呼叫該函數重新求值 update() { this.get(); } }
抽象被觀察者(釋出者)類Dep
我們再想一想,實際開發過程中,data 中肯定不止一個資料,而且每個資料,都有不同的訂閱者,所以說我們還需要抽象一個被觀察者(釋出者)Dep
類來儲存資料對應的觀察者(Watcher
),以及資料變化時通知觀察者更新。
class Dep { constructor() { // 儲存所有該依賴項的訂閱者 this.subs = []; } addSubs() { // 將 activeWatcher 作為訂閱者,放到 subs 中 // 防止重複訂閱 if(this.subs.indexOf(activeWatcher) === -1){ this.subs.push(activeWatcher); } } notify() { // 先儲存舊的依賴,便於下面遍歷通知更新 const deps = this.subs.slice() // 每次更新前,清除上一次收集的依賴,下次執行時,重新收集 this.subs.length = 0; deps.forEach((watcher) => { watcher.update(); }); } }
抽象 Observer
現在,Watcher
和Dep
只是兩個獨立的模組,我們怎麼把它們關聯起來呢?
答案就是Object.defineProperty
,在資料被讀取,觸發get
方法,Dep 將當前觸發 get 的 Watcher 當做訂閱者放到 subs中,Watcher
就與 Dep
建立關係;在資料被修改,觸發set
方法,Dep
就遍歷 subs 中的訂閱者,通知Watcher
更新。
下面我們就來完善將資料轉換為getter/setter的處理。
上面基礎的響應式系統實現中,我們只定義了一個響應式資料,當 data 中有其他property時我們就處理不了了。所以,我們需要抽象一個 Observer
類來完成對 data資料的遍歷,並呼叫defineReactive
轉換為 getter/setter,最終完成響應式繫結。
為了簡化,我們只處理data中單層資料。
class Observer { constructor(value) { this.value = value; this.walk(value); } // 遍歷 keys,轉換為 getter/setter walk(obj) { const keys = Object.keys(obj); for (let i = 0; i < keys.length; i++) { const key = keys[i] defineReactive(obj, key, obj[key]); } } }
這裡我們通過引數 value 的閉包,來儲存最新的資料,避免新增其他變數
function defineReactive(target, key, value) { // 每一個資料都是一個被觀察者 const dep = new Dep(); Object.defineProperty(target, key, { enumerable: true, configurable: true, // 執行 data.xxx 時 get 觸發,進行依賴收集,watcher 訂閱 dep get() { if (activeWatcher) { // 訂閱 dep.addSubs(activeWatcher); } return value; }, // 執行 data.xxx = xxx 時 set 觸發,遍歷訂閱了該 dep 的 watchers, // 呼叫 watcher.updata 更新 set(newValue) { // 如果前後值相等,沒必要跟新 if (value === newVal) { return; } value = newValue; // 派發更新 dep.notify(); }, }); }
至此,響應式系統就大功告成了!!
測試
我們通過下面程式碼測試一下:
let data = { name: "張三", age: 18, address: "成都", }; // 模擬 render const render1 = () => { console.warn("-------------watcher1--------------"); console.log("The name value is", data.name); console.log("The age value is", data.age); console.log("The address value is", data.address); }; const render2 = () => { console.warn("-------------watcher2--------------"); console.log("The name value is", data.name); console.log("The age value is", data.age); }; // 先將 data 轉換成響應式 new Observer(data); // 範例觀察者 new Watcher(render1); new Watcher(render2);
在瀏覽器中執行這段程式碼,和我們期望的一樣,兩個render
都執行了,並且在控制檯上列印了結果。
我們嘗試修改 data.name = '李四 23333333'
,測試兩個 render
都會重新執行:
我們只修改 data.address = '北京'
,測試一下是否只有render 1
回撥都會重新執行:
都完美通過測試!!?
總結
Vue
響應式原理的核心就是Observer
、Dep
、Watcher
,三者共同構成 MVVM 中的 VM
Observer
中進行資料響應式處理以及最終的Watcher
和Dep
關係繫結,在資料被讀的時候,觸發get
方法,將 Watcher
收集到 Dep
中作為依賴;在資料被修改的時候,觸發set
方法,Dep
就遍歷 subs 中的訂閱者,通知Watcher
更新。
本篇文章屬於入門篇,並非原始碼實現,在原始碼的基礎上簡化了很多內容,能夠便於理解Observer
、Dep
、Watcher
三者的作用和關係。
本文的原始碼,以及作者學習 Vue 原始碼完整的逐行註釋原始碼地址:github.com/yue1123/vue…
(學習視訊分享:、)
以上就是一文聊聊Vue響應式實現原理的詳細內容,更多請關注TW511.COM其它相關文章!