vue 無疑是一個很是棒的前端MVVM庫,懷着好奇的心情開始看VUE源碼,固然遇到了不少的疑問,也查了不少的資料看了一些文章。可是這些資料不少都忽略了很重要的部分或者是一些重要的細節,亦或是一些很重要的部分沒有指出,特別是在computed的實現上。因此纔打算寫這篇文章,記錄一下本身的學習過程,固然也但願能給其餘想了解VUE源碼的童鞋一點參考。若是筆者在某些地方理解有誤,也歡迎批評指正出來,一塊兒學習。html
爲了加深理解,我按着源碼的思路造了一個簡易的輪子,基本核心的實現是與VUE源代碼一致。測試 demo。倉庫的地址:eltonchan/rollup-ts前端
VUE的源碼採用rollup和 flow至於爲何不採用typescript,主要考慮工程上成本和收益的考量, 這一點尤大在知乎也有說過。(3.0+版本肯定改用typesript)vue
Vue 2.0 爲何選用 Flow 進行靜態代碼檢查而不是直接使用TypeScriptnode
不懂rollup 與typescript 也不要緊,本項目已經配置好了, 只須要先執行npm i (或者cnpm i)安裝相應依賴,而後 npm start 啓動就能夠。 npm run build 構建,默認是輸出umd格式,若是須要cmd或者amd 能夠在rollup.config.js配置文件修改。git
output: {
file: 'dist/bundle.js',
format: 'umd',
name: 'myBundle',
sourcemap: true
}
複製代碼
questions ? 帶着問題去了解一個事物每每能帶來更好的收益,那咱們就從下面幾個問題開始github
vue實現雙向綁定原理,主要是利用Object.defineProperty getter/setter(事實上,大多數響應式編程的庫都是利用這個實現的,好比很是棒的mobx.js)和發佈訂閱模式(定義了對象間的一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都將得到通知),而在vue中,watcher 就是訂閱者,而一對多的依賴關係 就是指data上的屬性與watcher,而data上的屬性如何與watcher 關聯起來, dep 就是橋樑, 因此搞懂 dep, watcher, observe三者的關係,天然就搞懂了vue實現雙向綁定的原理了。typescript
1、 Proxy 回到第一個問題, 答案實際上是:對於每個 data 上的key,都在 vm 上作一個代理,實際操做的是 this._data、 實現的代碼以下:express
export function proxy (target: IVue, sourceKey: string, key: string) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
return this[sourceKey][key];
},
set(val: any) {
this[sourceKey][key] = val;
}
});
}
複製代碼
能夠看出獲取和修改this.xx 都是在獲取或者修改this.data.xx;npm
2、Observer 用於把data上的屬性封裝成可觀察的屬性 用Object.defineProperty來攔截對象的讀寫gettet/setter操做, 在獲取的時候收集依賴, 在修改的時候通知相關的依賴。編程
walk(data): void {
if (!data || typeof data !== 'object') return;
Object.keys(data).forEach(key => {
this.defineReactive({
data,
key,
value: data[key]
});
});
}
defineReactive({ data, key, value }: IReactive): void {
const dep = new Dep();
this.walk(value);
const self = this;
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
Dep.target.addDep(dep);
}
return value;
},
set(newVal: any): void {
if (value === newVal) return;
self.walk(value);
value = newVal;
dep.notify();
}
});
}
複製代碼
能夠看出, 在get的時候收集依賴,而Dep.target 其實就是watcher, 等下講到watcher的時候再回過來, 這裏要關注dep 其實dep在這裏是一個閉包環境,在執行get 或者set的時候 還能夠訪問到建立的dep. 好比 this.name當在獲取this.name的值的時候 會建立一個Dep的實例, 把watcher 添加到這個dep中。
爲何對象上新增屬性不會監聽,而修改整個對象爲何能檢測到子屬性的變化 ?
因爲 JavaScript 的限制,Vue 不能檢測對象屬性的添加或刪除(固然mobx也不例外的)。因此可觀察的對象屬性的添加或者刪除沒法觸發set 方法,而直接修改對象則能夠,而在set 方法中則會判斷新值是不是對象數組類型,若是是 則子屬性封裝成可觀察的屬性,這也是set中self.walk(value);的做用。
3、Watcher 剛纔提到了watcher,從上圖中也能夠看到了watcher的做用 事實上,每個computed屬性和watch 都會new 一個 Watcher。接下來會講到。先來看watcher的實現。
constructor(
vm: IVue,
expression: Function | string,
cb: Function,
) {
this.vm = vm;
vm._watchers.push(this);
this.cb = cb || noop;
this.id = ++uid;
// 處理watch 的狀況
if (typeof expression === 'function') {
this.getter = expression;
} else {
this.getter = () => vm[expression];
}
this.expression = expression.toString();
this.depIds = new Set();
this.newDepIds = new Set();
this.deps = [];
this.newDeps = [];
this.value = this.get();
}
複製代碼
這裏的expression,對於初始化用來渲染視圖的watcher來講,就是render方法,對於computed來講就是表達式,對於watch纔是key,因此這邊須要判斷是字符串仍是函數,而getter方法是用來取value的。這邊有個depIds,deps,可是又有個newDepIds,newDeps,爲何這樣設計,接下去再講,先看this.value = this.get();能夠看出在這裏給watcher的value賦值,再來看get方法。
get() :void {
Dep.target = this;
const value = this.getter.call(this.vm); // 執行一次get 收集依賴
Dep.target = null;
this.cleanupDeps(); // 清除依賴
return value;
}
複製代碼
能夠看到getter是用來取值的,當執行這一行代碼的時候,以render的那個watcher爲例,會執行VNode render 當遇到{{ msg }}的表達式的時候會取值,這個時候會觸發msg的get方法,而此時的Dep.target 就是這個watcher, 因此咱們會把這個render的watcher和msg這個屬性關聯起,也就是msg的dep已經有render的這個watcher了。這個就是Watcher,Dep,Observer的關係。咱們再來看Dep:
export default class Dep implements IDep {
static target: any = null;
subs:any = [];
id;
constructor () {
this.id = uid++;
this.subs = [];
}
addSub(sub: IWatcher): void {
if (this.subs.find(o => o.id === sub.id)) return;
this.subs.push(sub);
}
removeSub (sub: IWatcher) {
const idx = this.subs.findIndex(o => o.id === sub.id);
if (idx >= 0) this.subs.splice(idx, 1);
}
notify():void {
this.subs.forEach((sub: Isub) => {
sub.update();
})
}
depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
}
}
複製代碼
Dep的實現很簡單,這邊看notify的方法,咱們知道在修改data上的屬性的時候回觸發set,而後觸發notify方法,而後咱們知道sub就是watcher,因此watcher.update方法就是修改屬性所執行的方法,回到watcher看這個update的實現。
update() {
// 推送到觀察者隊列中,下一個tick時調用。*/
queueWatcher(this);
}
run(cb) {
const value = this.get();
if (value !== this.value) {
const oldValue = this.value;
this.value = value;
cb.call(this.vm, value, oldValue);
}
}
複製代碼
update方法並無直接render vNode。而是把watcher推到一個隊列中,事實上vue是的更新dom是異步的,爲何要異步更新隊列,這邊摘抄了一下官網的描述:Vue 異步執行 DOM 更新。只要觀察到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據改變。若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做上很是重要。而後,在下一個的事件循環「tick」中,Vue 刷新隊列並執行實際 (已去重的) 工做。Vue 在內部嘗試對異步隊列使用原生的 Promise.then 和 MessageChannel,若是執行環境不支持,會採用 setTimeout(fn, 0) 代替。其實這是一種很是好的性能優化方案,咱們設想一下若是在mounted中循環賦值,若是不採用異步更新策略,每個賦值都更新,徹底是一種浪費。
4、nextTick 關於nextTick其實不少文章寫的都不錯,這邊就不詳細介紹了。涉及到的概念能夠點擊下面連接查看:
JavaScript 運行機制詳解:再談Event Loop
5、Computed 計算屬性是基於它們的依賴進行緩存的。只在相關依賴發生改變時它們纔會從新求值 回到開始的那個問題,如何實現依賴緩存? name的更新如何讓info也更新,若是name不變,info如何取值?
剛纔在講watcher的時候,提到過每一個computed會實例化一個Watcher,從下面代碼中也能夠看出來,每個computed屬性都有一個訂閱者watcher。
initComputed(computed) {
if (!computed || typeof computed !== 'object') return;
const keys = Object.keys(computed);
const watchers = this._computedWatchers;
let i = keys.length;
while(i--) {
const key = keys[i];
const func = computed[key];
watchers[key] = new Watcher(
this,
func || noop,
noop,
);
defineComputed(this, key);
}
}
複製代碼
看這個例子:
computed: {
info() {
console.info('computed update');
return this.name + 'hello';
}
},
複製代碼
watcher 的getter方法就是computed屬性的表達式,而在執行this.value = this.get();這個value就會是表達式的運行結果,因此其實Vue是把info的值存儲在它的watcher的value裏面的,而後又知道在取name的值的時候,會觸發name的get方法,此時的Dep.target 就是這個info的watcher,而dep是一個閉包,仍是以前收集name的那個dep, 因此name的dep就會有兩個watcher,[renderWatcher, computedWatcher], 當name更新的時候,這兩個訂閱者watcher都會收到通知,這也就是name的更新讓info也更新。
那info的值是watcher的value, 因此這邊要作一個代理,把computed屬性的取值代理到對應watcher的value,實現起來也很簡單。
export default function defineComputed(vm: IVue, key: string) {
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get() {
const watcher = vm._computedWatchers && vm._computedWatchers[key];
return watcher.value;
},
});
}
複製代碼
6、依賴更新
<p v-if="switch">{{ name }}</p>
複製代碼
假設switch由true切換成false時候,是須要把name上面的renderWatcher刪除掉的,因此須要用depIds和deps的屬性來記錄dep。
addDep(dep: Dep) {
const id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
}
cleanupDeps() {
let i = this.deps.length;
while (i--) {
const dep = this.deps[i];
if (!this.depIds.has(dep.id)) {
dep.removeSub(this);
}
}
const tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
const deps = this.deps;
this.deps = this.newDeps;
this.newDeps = deps;
this.newDeps.length = 0;
}
複製代碼
這裏把newDepIds賦值給了depIds, 而後newDepIds再清空,deps也是這樣的操做,這是一種效率很高的操做,避免使用了深拷貝。添加依賴的時候都是用newDepIds,newDeps來記錄,刪除的時候會去deps裏面遍歷查找,等刪除了再把newDepIds賦值給depIds,這樣能保證在更新依賴的時候,沒有使用的依賴會從這個watcher中移除。
7、watch 爲何watch 一個對象的時候 oldValue == value ?
watch的屬性也是一個實例化的Watcher,只是這個時候的expression是key,value 是vm[key],而cb就是回調函數,因此這個時候對應屬性的dep中天然就有這個watcher。
initWatch(watch) {
if (!watch || typeof watch !== 'object') return;
const keys = Object.keys(watch);
let i = keys.length;
while(i--) {
const key = keys[i];
const cb = watch[key];
new Watcher(this, key, cb);
}
}
複製代碼
當屬性更新的時候,會執行到這個run方法, 當watch一個對象的時候,watcher的value實際上是一個引用,修改這個屬性的時候,this.value也同步修改了,因此也就是爲何oldValue == value了, 至於做者爲何這麼設計,我想確定是有他緣由的。
run(cb) {
const value = this.get();
if (value !== this.value) {
const oldValue = this.value;
this.value = value;
cb.call(this.vm, value, oldValue);
}
}
複製代碼
8、Compile vue 2+ 已經使用VNode了,這部分尚未細緻研究過,因此我這邊本身寫了個簡易的Compile,這部分已經和源碼沒有關係了。主要用到了DocumentFragment和閉包而已,有興趣的童鞋能夠到這個倉庫查看。
components vnode 待補充...