前置说明
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 函数就是就是输出虚拟节点的。