大家好,我是若川。持續組織了8個月源碼共讀活動,感興趣的可以點此加我微信 ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列。另外:目前建有江西|湖南|湖北?籍 前端群,可加我微信進群。
如果打算學vue2源碼,可以看看這個系列https://vue.windliang.wang
目前工作中大概有 40%
的需求是在用 Vue2
的技術棧,所謂知其然更要知其所以然,為了更好的使用 Vue
、更快的排查問題,最近學習了源碼相關的一些知識,雖然網上總結 Vue
的很多很多了,不少自己一個,但也不多自己一個,歡迎一起討論學習,發現問題歡迎指出。
響應式系統要干什么
回到最簡單的代碼:
data?=?{text:?'hello,?world'
}const?updateComponent?=?()?=>?{console.log('收到',?data.text);
}updateComponent()data.text?=?'hello,?liang'
//?運行結果
//?收到?hello,?world
響應式系統要做的事情:某個依賴了 data
數據的函數,當所依賴的 data
數據改變的時候,該函數要重新執行。
我們期望的效果:當上邊 data.text
修改的時候, updateComponent
函數再執行一次。
為了實現響應式系統,我們需要做兩件事情:
知道
data
中的數據被哪些函數依賴data
中的數據改變的時候去調用依賴它的函數們
為了實現第 1
點,我們需要在執行函數的時候,將當前函數保存起來,然后在讀取數據的時候將該函數保存到當前數據中。
第 2
點就迎刃而解了,當修改數據的時候將保存起來的函數執行一次即可。
在讀取數據和修改數據的時候需要做額外的事情,我們可以通過 Object.defineProperty()
?重寫對象屬性的 get
和 set
函數。
響應式數據
我們來寫一個函數,重寫屬性的 get
和 set
函數。
/***?Define?a?reactive?property?on?an?Object.*/
export?function?defineReactive(obj,?key,?val)?{const?property?=?Object.getOwnPropertyDescriptor(obj,?key);//?讀取用戶可能自己定義了的?get、setconst?getter?=?property?&&?property.get;const?setter?=?property?&&?property.set;//?val?沒有傳進來話進行手動賦值if?((!getter?||?setter)?&&?arguments.length?===?2)?{val?=?obj[key];}Object.defineProperty(obj,?key,?{enumerable:?true,configurable:?true,get:?function?reactiveGetter()?{const?value?=?getter???getter.call(obj)?:?val;/*********************************************///?1.這里需要去保存當前在執行的函數/*********************************************/return?value;},set:?function?reactiveSetter(newVal)?{const?value?=?getter???getter.call(obj)?:?val;if?(setter)?{setter.call(obj,?newVal);}?else?{val?=?newVal;}/*********************************************///?2.將依賴當前數據依賴的函數執行/*********************************************/},});
}
為了調用更方便,我們把第 1
步和第 2
步的操作封裝一個 Dep
?類。
export?default?class?Dep?{static?target;?//當前在執行的函數subs;?//?依賴的函數constructor()?{this.subs?=?[];?//?保存所有需要執行的函數}addSub(sub)?{this.subs.push(sub);}depend()?{//?觸發?get?的時候走到這里if?(Dep.target)?{//?委托給?Dep.target?去調用?addSubDep.target.addDep(this);}}notify()?{for?(let?i?=?0,?l?=?this.subs.length;?i?<?l;?i++)?{this.subs[i].update();}}
}Dep.target?=?null;?//?靜態變量,全局唯一
我們將當前執行的函數保存到 Dep
類的 target
變量上。
保存當前正在執行的函數
為了保存當前的函數,我們還需要寫一個 Watcher
類,將需要執行的函數傳入,保存到 Watcher
類中的 getter
屬性中,然后交由 Watcher
類負責執行。
這樣在 Dep
類中, subs
中保存的就不是當前函數了,而是持有當前函數的 Watcher
對象。
import?Dep?from?"./dep";
export?default?class?Watcher?{constructor(Fn)?{this.getter?=?Fn;this.get();}/***?Evaluate?the?getter,?and?re-collect?dependencies.*/get()?{Dep.target?=?this;?//?保存包裝了當前正在執行的函數的?Watcherlet?value;try?{//?調用當前傳進來的函數,觸發對象屬性的?getvalue?=?this.getter.call();}?catch?(e)?{throw?e;}return?value;}/***?Add?a?dependency?to?this?directive.*/addDep(dep)?{//?觸發?get?后會走到這里,收集當前依賴//?當前正在執行的函數的?Watcher?保存到?dep?中的?subs?中dep.addSub(this);}/***?Subscriber?interface.*?Will?be?called?when?a?dependency?changes.*///?修改對象屬性值的時候觸發?set,走到這里update()?{this.run();}/***?Scheduler?job?interface.*?Will?be?called?by?the?scheduler.*/run()?{this.get();}
}
Watcher
的作用就是將正在執行的函數通過 Watcher
包裝后保存到 Dep.target
中,然后調用傳進來的函數,此時觸發對象屬性的 get
函數,會收集當前 Watcher
。
如果未來修改對象屬性的值,會觸發對象屬性的 set
,接著就會調用之前收集到的 Watcher
對象,通過 Watcher
對象的 uptate
方法,來調用最初執行的函數。
響應式數據
回到我們之前沒寫完的 defineReactive
函數,按照上邊的思路,我們來補全一下。
import?Dep?from?"./dep";
/***?Define?a?reactive?property?on?an?Object.*/
export?function?defineReactive(obj,?key,?val)?{const?property?=?Object.getOwnPropertyDescriptor(obj,?key);//?讀取用戶可能自己定義了的?get、setconst?getter?=?property?&&?property.get;const?setter?=?property?&&?property.set;//?val?沒有傳進來話進行手動賦值if?((!getter?||?setter)?&&?arguments.length?===?2)?{val?=?obj[key];}/*********************************************/const?dep?=?new?Dep();?//?持有一個?Dep?對象,用來保存所有依賴于該變量的?Watcher/*********************************************/Object.defineProperty(obj,?key,?{enumerable:?true,configurable:?true,get:?function?reactiveGetter()?{const?value?=?getter???getter.call(obj)?:?val;/*********************************************///?1.這里需要去保存當前在執行的函數if?(Dep.target)?{dep.depend();}/*********************************************/return?value;},set:?function?reactiveSetter(newVal)?{const?value?=?getter???getter.call(obj)?:?val;if?(setter)?{setter.call(obj,?newVal);}?else?{val?=?newVal;}/*********************************************///?2.將依賴當前數據依賴的函數執行dep.notify();/*********************************************/},});
}
Observer 對象
我們再寫一個 Observer
方法,把對象的全部屬性都變成響應式的。
export?class?Observer?{constructor(value)?{this.walk(value);}/***?遍歷對象所有的屬性,調用?defineReactive*?攔截對象屬性的?get?和?set?方法*/walk(obj)?{const?keys?=?Object.keys(obj);for?(let?i?=?0;?i?<?keys.length;?i++)?{defineReactive(obj,?keys[i]);}}
}
我們提供一個 observe
方法來負責創建 Observer
對象。
export?function?observe(value)?{let?ob?=?new?Observer(value);return?ob;
}
測試
將上邊的方法引入到文章最開頭的例子,來執行一下:
import?{?observe?}?from?"./reactive";
import?Watcher?from?"./watcher";
const?data?=?{text:?"hello,?world",
};
//?將數據變成響應式的
observe(data);const?updateComponent?=?()?=>?{console.log("收到",?data.text);
};//?當前函數由?Watcher?進行執行
new?Watcher(updateComponent);data.text?=?"hello,?liang";
此時就會輸出兩次了~
收到?hello,?world
收到?hello,?liang
說明我們的響應式系統成功了。
總

先從整體理解了響應式系統的整個流程:
每個屬性有一個 subs
數組,Watcher
會持有當前執行的函數,當讀取屬性的時候觸發 get
,將當前 Watcher
保存到 subs
數組中,當屬性值修改的時候,再通過 subs
數組中的 Watcher
對象執行之前保存的函數。
當然還有億點點細節需要完善,后邊的文章會繼續。vue2源碼系列文章,作者現在寫了12篇了。?https://vue.windliang.wang
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經堅持寫了8年,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助3000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
掃碼加我微信 ruochuan02、拉你進源碼共讀群
今日話題
目前建有江西|湖南|湖北?籍 前端群,想進群的可以加我微信 ruochuan12?進群。分享、收藏、點贊、在看我的文章就是對我最大的支持~