前置說明
vue 版本 2.6.2,測試用的代碼
<!DOCTYPE html>
<html>
<head>
<title>vue test</title>
</head>
<body>
<div id="app">
{{message}}
<button-counter :title="tt"></button-counter>
</div>
<!-- Vue.js v2.6.11 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('button-counter', {
props: ['title'],
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">{{title}}: You clicked me {{ count }} times.</button>'
});
var app = new Vue({
el: '#app',
data: {
message: 'TEST',
tt: 'on'
},
mounted() {
window.addEventListener('test', (e) => {
this.message = e.detail;
}, false);
},
})
console.log(app);
// var event = new CustomEvent('test', { 'detail': 5 }); window.dispatchEvent(event);
</script>
</body>
</html>
簡要概括
在攔截器(Object.defineProperty)里,在它的閉包中會有一個觀察者(Dep)對象,這個對象用來存放被觀察者(watcher)的實例。
並且攔截器注冊 get 方法,該方法用來進行「依賴收集」。其實「依賴收集」的過程就是把 Watcher 實例存放到對應的 Dep 對象中去。
get 方法可以讓當前的 Watcher 對象(Dep.target)存放到它的 subs 中(addSub)方法,在數據變化時,set 會調用 Dep 對象的 notify 方法通知它內部所有的 Wathcer 對象進行視圖更新。
function defineReactive$$1(obj, key, val, customSetter, shallow) {
var dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
//... 省略部分代碼
}
return value;
},
set: function reactiveSetter(newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
//... 省略部分代碼
val = newVal;
dep.notify();
}
});
}
分析
在初始化過程中(beforeCreate 和 created 之間) Object.defineProperty 劫持了數據
劫持的過程中定義了觀察者 dep,其結構非常簡單:
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
然后在掛載過程中(beforeMount 和 mounted 之間) ,攔截器(Object.defineProperty) 觸發了 get ,get 函數里 dep.depend();
就做了觀察者 dep 關聯被觀察者 watcher 的動作。
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
watcher 的結構如下
var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
// ... 省略部分
this.cb = cb;
this.id = ++uid$2; // uid for batching
// ... 省略部分
this.expression = expOrFn.toString();
};
所以最后結構觀察者和被觀察者就是這樣的結構,完成了依賴收集。最終就是我們熟知的觸發流程,點擊上圖代碼的按鈕時,攔截器的 set 觸發了 dep.notify() 通知了所有被觀察者 Wacher,而一番排隊操作后需而觸發 watcher 里的表達式,就去重新渲染這個組件。
Dep {
id: 9,
subs: [
0: Watcher {
...
expression: "function () { vm._update(vm._render(), hydrating); }"
...
}
]
}
以上有個關鍵的一步,為什么 Dep.target 為什么會指向這個 Watcher 對象?
Dep.target 為什么會指向這個 Watcher 對象?
在 callHook(vm, 'beforeMount') 后,進入 mount 階段,此時初始化 Watcher
function noop (a, b, c) {}
var updateComponent;
// 省略if (config.performance && mark)判斷
updateComponent = function() {
vm._update(vm._render(), hydrating);
};
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
我們知道 computed 屬性會被標記為 lazy 直到取值時才觸發 this.cb,那么一般情況下就調用 this.get。
var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
// ... 省略部分
vm._watchers.push(this);
if (options) {
this.lazy = !!options.lazy;
// ... 省略部分
}
this.cb = cb;
this.id = ++uid$2; // uid for batching
// ... 省略部分
this.expression = expOrFn.toString();
this.value = this.lazy
? undefined
: this.get();
};
而就是在 Watcher.prototype.get,注意 pushTarget,此時就和 Dep 發布者產生了聯系,Dep 的 target 被設置為了這個 wacher,並且在每次監測對象被 get 時,就會往自身的 Dep 里推入這個 wacher。
Watcher.prototype.get = function get() {
pushTarget(this);
var value;
var vm = this.vm;
//...
value = this.getter.call(vm, vm);
//...
popTarget();
this.cleanupDeps();
return value;
};
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget () {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
到此就完成了依賴收集。
那么再來闡述下被觀察者怎么開始更新視圖的
多數情況下,被觀察者 Watcher 的結構里都有表達式 expression 屬性,它的內容是 vm._update(vm._render(), hydrating)
,渲染的過程就是觸發了此函數。
那么首先需要調用 vm._render() 方法,此方法要返回一個 VNode
Vue.prototype._render = function () {
// ...
var vm = this;
var ref = vm.$options;
var render = ref.render;
vnode = render.call(vm._renderProxy, vm.$createElement);
// ...
return vnode
}
// 而render方法其實就是用於輸出一個虛擬節點
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[(message + 1 > 1)?_c('div',[_v(_s(message + 1))]):_e(),_v(" "),_c('button',{on:{"click":function($event){message += 1}}},[_v("阿道夫")])])}
})
然后結果交給 vm._update
Vue.prototype._update = function(vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
// ...
vm._vnode = vnode;
// ...
vm.$el = vm.__patch__(prevVnode, vnode);
// ...
};
結論是 mount 階段 初始化 Watcher,然后在 wathcer初始化后調用 get,get里 pushTarget(this),並且執行自身的getter也就是表達式,表達式的內容就是 vm._update(vm._render(), hydrating)
故而就開始執行 render函數,render 函數就是就是輸出虛擬節點的。