源码笔记(八):update 阶段(上)
前面七章分析了 vue demo
的整个初始化过程,本文开始分析当数据(model
)发生变化时,Vue
的处理过程。
我们以点击 dom
触发 plus
执行 this.info.age++
为例,分析 Vue
的 update
阶段。
proxyGetter && proxySetter
此时读取 this.info
,则触发在 ininState->initData->proxy(vm, "_data", key)
监听的 proxyGetter
:
function proxy(target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
其中,this
为 Vue
实例,sourceKey
为 _data
,即返回 _data
对象里的 key
。在读取 key
时,又触发了在 ininState->initData->observe->walk->defineReactive$$1
里的 reactiveGetter
:
reactiveGetter && reactiveSetter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
var value = getter ? getter.call(obj) : val;
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
if (true && customSetter) {
customSetter();
}
if (getter && !setter) {
return;
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
},
});
因为此时并非在执行 watcher
的 update
操作, 所以 Dep.target
即 watcher
不存在,直接返回 value
。
然后读取 this.info.age
,直接触发 reactiveGetter
返回 value
。
读取阶段结束后,触发 reactiveSetter
来设置 this.info.age
的新值,设置后执行:
childOb = !shallow && observe(newVal);
dep.notify();
对新值进行了监听后,然后通知该变量对应的订阅中心 Dep
。
Dep.prototype.notify
Dep.prototype.notify = function notify() {
var subs = this.subs.slice();
if (true && !config.async) {
subs.sort(function (a, b) {
return a.id - b.id;
});
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
其中,subs
为变量的订阅列表。在本例中,变量 this.info.age
有 3 个订阅者 watcher
,分别来自 watch watcher
,渲染 watcher
,计算 watcher
。
在 notify
里,如果配置了同步,则对订阅列表 subs
按 id
大小进行排序。然后依次触发每项(watcher
)的 update
方法。
Watcher.prototype.update
Watcher.prototype.update = function update() {
if (this.lazy) {
this.dirty = true; //设置 计算 watcher 的标识位
} else if (this.sync) {
this.run(); // 如果设置同步,则立即执行
} else {
queueWatcher(this); //以当前实例为参数
}
};
Watcher.prototype.update
主要针对配置的一些处理,然后将当前 watcher
传入 queueWatcher
。
其中 计算 watcher
为懒加载(即 this.lazy
为 true
),所以不会执行 queueWatcher
,但会将标识位 this.dirty
置为 true
。这样计算属性在后面 computedGetter
里取值时,就会通过 Watcher.prototype.evaluate
重新计算而不是直接取缓存值 value
,并且还会将标识位 this.dirty
置为 false
,这样在后续如果没有计算属性对应的表达式里的被监听的其他值更新触发计算属性 watcher
更新的情况下,就可以直接取 value
值了。
queueWatcher
function queueWatcher(watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true; // 防止同一时刻同一 watcher 的多次 push
if (!flushing) {
queue.push(watcher); //是否在执行 flushSchedulerQueue
} 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 > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
} // queue the flush
if (!waiting) {
waiting = true; // 防止多次 nextTick,即防止多次注册新的微任务队列
if (true && !config.async) {
flushSchedulerQueue();
return;
}
nextTick(flushSchedulerQueue); //将 flushSchedulerQueue 函数传入 nextTick
}
}
}
has
为一个对象,用于过滤同一个watcher
的同时间多次调用update
queue
为一个数组,用于存放等待更新的watcher
队列flushing
为一个布尔值,用于标识队列是否正在执行更新,直到queue
执行完后才重置为false
flushSchedulerQueue
为更新队列的执行函数waiting
为一个布尔值,用于标识已经把flushSchedulerQueue
注册到下一次微循环的任务队列中,直到queue
执行完后才重置为false
queueWatcher
里主要将当前实例 Watcher
push
到 queue
即 watcher
队列里,然后将 flushSchedulerQueue
传入 nextTick
方法。
nextTick
function nextTick(cb, ctx) {
var _resolve;
// callbacks 保存异步执行的任务队列
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
} // $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
});
}
}
nextTick
里对异步执行的任务队列 cb
统一收集到 callbacks
数组( flushSchedulerQueue
为其中一项),然后执行 timerFunc
。
timerFunc
里通过异步执行 flushCallbacks
。异步方法的选用优先级:Promise.resolve().then
>MutationObserver
>setImmediate
>setTimeout
,前两个属于 microTask
微任务队列,后两个属于 macroTask
宏任务队列。
执行 nextTick(flushSchedulerQueue)
后,进入异步等待,此时回到 Dep.prototype.notify
里执行下一个 watcher
的 update
把 watcher
push
到 queue
里。
待到异步调用后,执行 flushCallbacks
,该方法里遍历 callbacks
并执行。
为什么要这么做
如果没有异步队列,则每一次的数据变化时,就会直接触发 update->patch
去比较 vnode
进行更新 dom
,如果在同一时间有大量修改数据(watcher
被多次触发),则会反复频繁更新视图。而有了这个队列,在当前栈可以先判断 watcher
的重复性,过滤掉重复更新。保证更新视图操作 DOM
的动作是在当前栈执行完以后的下一个事件循环 tick
的时候调用,大大优化了性能。
flushSchedulerQueue
function flushSchedulerQueue() {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
queue.sort(function (a, b) {
return a.id - b.id;
});
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run(); // in dev build, check and stop circular updates.
if (true && has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn('You may have an infinite update loop ' + (watcher.user ? 'in watcher with expression "' + watcher.expression + '"' : 'in a component render function.'), watcher.vm);
break;
}
}
} // keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
resetSchedulerState(); // call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue); // devtool hook
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
对于本例,queue
里目前有两个 watcher
: watch watcher
,渲染 watcher
。flushSchedulerQueue
里先对 queue
排序:
- 组件的更新是由父到子的(因为父组件的创建在子组件之前),所以
watcher
的创建和执顺序行也应该是先父后子 - 用户自定义
watcher
应该在渲染 watcher
之前执行(因为用户自定义watcher
的创建在渲染 watcher
之前) - 如果一个组件在父组件的
watcher
执行期间被销毁,那么这个子组件的watcher
都可以被跳过(this.active
标识)。
然后执行 queue
队列中的每一个 watcher
的 watcher.before
触发生命周期 beforeUpdate
钩子。然后执行 watcher.run()
,下文分析。
执行结束后,调用 resetSchedulerState
重置状态,调用 callActivatedHooks
改变组件为 activated
状态并触发生命周期 activated
钩子,调用 callUpdatedHooks
触发生命周期 update
钩子,最后触发工具的 flush
事件,整个流程执行结束。
Watcher.prototype.run
Watcher.prototype.run = function run() {
if (this.active) {
var value = this.get();
if (value !== this.value || isObject(value) || this.deep) {
// set new value
var oldValue = this.value;
this.value = value;
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, 'callback for watcher "' + this.expression + '"');
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};
this.active
用于标识该 watcher
是否已经被卸载。在 Watcher.prototype.teardown
里设置为 false
(已卸载)。
Watcher.prototype.run
执行 this.get
得到 value
赋给 this.value
。然后更新 watcher
的 value
值。如果 this.user
为真这表示为用户定义的 watch
,则执行 cb
即用户定义的回调方法,watch watcher
就会触发该回调。
Watcher.prototype.get
Watcher.prototype.get = function get() {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, 'getter for watcher "' + this.expression + '"');
} else {
throw e;
}
} finally {
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
};
Watcher.prototype.get
里主要是通过 this.getter
得到 value
。
先将本 watcher
推入全局变量 Dep.target
下,在添加订阅结束后,移除在 Dep.target
下的 watcher
。
如果设置了 deep
为 true
,那么执行 traverse
里递归执行 _traverse
读取对象里的每一项子属性给他们添加本 watcher
订阅。
其中对于
computed watcher
的getter
为:js//... return function computedGetter() { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value; } }; //...
如果
dirty
为true
,则触发重新计算computed
,否则直接取value
。其中对于
watch watcher
的getter
为:js//... return function (obj) { for (var i = 0; i < segments.length; i++) { if (!obj) { return; } obj = obj[segments[i]]; } return obj; }; //...
在读取
obj[segments[i]]
时,会触发属性info
的proxyGetter -> reactiveGetter
,在得到最新值的同时,将该watch watcher
添加到对应的订阅列表,并且因为info
是对象,所以还添加到info.__ob__
上。其中对于
渲染 watcher
的getter
为:jsupdateComponent = function () { vm._update(vm._render(), hydrating); };
vm._render
会根据render
函数得到vnode
。在执行render
的过程中,读取到的每一个变量都会触发其对应的proxyGetter->reactiveGetter
取得最新值和该渲染 watcher
订阅到对应变量的订阅列表里。得到vnode
后,然后执行vm._update
更新视图,下一章分析。
最后执行 cleanupDeps
方法通过对比新旧 depIds
来删除无效的订阅 sub
,最后返回 value
,回到 flushSchedulerQueue
方法里。
本章小结
- 本章以一个数据更新为始,分析了对数据的添加订阅和触发订阅的相关逻辑。
- 一共涉及到 3 个
watcher
:分别是watch watcher
,渲染 watcher
,计算 watcher
,对不同watcher
进行了不同的处理。 - 触发订阅涉及到了异步队列优化,优化了不必要的视图更新,大大提升了性能。