前言
本文是vue2.x源码分析的第九篇,主要看响应式设计的处理过程!
实例代码
Vue
{{message}}
1、关键断点
initData(vm)
proxy(vm,"_data",'message')
observe(data,true/asRootdata/)
vm.$mount(vm.options.el)
mount.call(this,el,hydrating)
mountComponent(this,el,hydrating)
vm._watcher=new Watcher(vm,updateComponent,noop)
2、详细分析
从Watcher开始分析
Watcher = function Watcher (vm,expOrFn,cb,options) {
this.vm = vm;
vm._watchers.push(this);
this.deep = this.user = this.lazy = this.sync = false;
this.cb = cb;
this.id = ++uid$2; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
...
}
this.value = this.lazy
? undefined
: this.get();
};
Watcher.prototype.get = function get () {
pushTarget(this); //这个this即是刚创建的watcher实例,称之为render watcher,本质上执行Dep.target = this;
var value;
var vm = this.vm;
if (this.user) {
...
} else {
value = this.getter.call(vm, vm); //this.getter得到执行,即是updateComponent函数被执行
}
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
return value
};
接下来执行updateComponent
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
先执行vm._render
Vue.prototype._render = function () {
var vm = this;
var ref = vm.$options;
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
var _parentVnode = ref._parentVnode;
...
var vnode;
try {
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
...
}
vnode.parent = _parentVnode;
return vnode
};
以上主要执行的是render函数,该函数是通过AST得到的匿名函数
function() {
with(this){ //this是vm._renderProxy,而不是vm
return _c('div',{attrs:{"id":"app"}},[_v(_s(message))])
}
}
注意_s(message),它在执行时会执行vm.message取值操作,从而触发vm.message的get函数,进而触发vm._data.message的get函数,看下该函数:
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) { // Dep.target在上面被设为了render watcher
dep.depend(); //data的每个属性都有一个dep实例对应,让render watcher收集该属性的dep
...
}
return value
},
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
//addDep如下:
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this); //dep的subs收集该watcher
}
}
};
//addSub如下:
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);//dep的subs收集该watcher
};
//render watcher收集了所有dep,同时每个dep又都收集了render watcher,这时this.\_render执行完毕,返回了Vnode
接下来执行vm._update函数
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
if (vm._isMounted) {
callHook(vm, 'beforeUpdate');
}
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var prevActiveInstance = activeInstance;
activeInstance = vm;
vm._vnode = vnode;
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
);
} else {
...
}
...
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
};
主要执行vm.__patch__函数,该函数内部主要执行在上节提到的createElm函数,该函数的四步执行完后就能创建真实DOM结构了,于是页面首次渲染就完成了。
下面分析当vm.message的值发生变化,vue是如何追踪到变化并更新页面的:
首先message的值发生变化会触发vm.message的set函数,进而触发vm._data.message的set函数,看下该函数:
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if ("development" !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal; //旧值被新值替换
}
childOb = observe(newVal);//观测新值
dep.notify(); //这个dep与get函数中的是同一个,在get中dep.subs中已经订阅了render watcher
}
看下notify函数:
Dep.prototype.notify = function notify () {
// stabilize(使稳固) the subscriber list first
var subs = this.subs.slice(); //只有一个render watcher
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update(); //执行render watcher的update函数
}
};
看下update函数:
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);//执行该函数
}
};
看下queueWatcher函数:
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) { //has是个对象,存放watcher的id
has[id] = true;
if (!flushing) { //flushing可看成全局变量,默认false
queue.push(watcher); //render watcher被推入queue数组
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
var i = queue.length - 1;
while (i >= 0 && queue[i].id > watcher.id) {
i--;
}
queue.splice(Math.max(i, index) + 1, 0, watcher);
}
// queue the flush
if (!waiting) {//waiting可看成全局变量,默认false
waiting = true;
nextTick(flushSchedulerQueue);
}
}
}
看下flushSchedulerQueue函数:
function flushSchedulerQueue () {
debugger;
flushing = true;
var watcher, id, vm;
queue.sort(function (a, b) { return a.id - b.id; });
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
id = watcher.id;
has[id] = null;
watcher.run();
...
}
// reset scheduler before updated hook called
var oldQueue = queue.slice();
resetSchedulerState();
// call updated hooks
index = oldQueue.length;
while (index--) {
watcher = oldQueue[index];
vm = watcher.vm;
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated');
}
}
...
}
看下watcher.run函数:
Watcher.prototype.run = function run () {
debugger;
if (this.active) {
var value = this.get();
...
}
};
这个this.get就是初次渲染时调用过一次的this.get
Watcher.prototype.get = function get () {
pushTarget(this); //这个this即是刚创建的watcher实例,称之为render watcher,本质上执行Dep.target = this;
var value;
var vm = this.vm;
if (this.user) {
...
} else {
value = this.getter.call(vm, vm); //this.getter得到执行,即是updateComponent函数被执行
}
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
return value
};
然后updateComponent函数又一次被执行,从而this._render,this._update都得到执行,DOM结构得以创建,唯一不同的是message值已经被修改成新值了,从而页面实现了更新。
3、总结:
第一次渲染:
- observe(data) 为下次页面更新做准备
- updateComponent 只要该函数得到执行,就会生成真实DOM
- new Watcher(vm,updateComponent,noop),在内部updateComponent得到执行
第二次渲染(更新):
- 触发set函数
- dep.notify
- watcher.update
- queueWatcher
- flushSchedulerQueue
- watcher.run
- updateComponent再次执行,生成新DOM