响应式原理-派发更新
在上一节 15.响应式原理-依赖收集 中我们了解了依赖收集的过程,收集的目的就是当我们修改数据时可以对相关依赖派发更新,本节将了解派发更新的过程。
先回顾一下 setter
部分逻辑:
/**
* Define a reactive property on an Object.
*/
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep();
const property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return;
}
// cater for pre-defined getter/setters
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// ...
set: function reactiveSetter(newVal) {
const 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 (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
},
});
}
复制代码
setter
主要看两个点:
childOb = !shallow && observe(newVal)
,如果shallow
为false
的情况,会对新设置的值变成一个响应式对象- 在
set
中通过执行dep.notify()
进行派发更新,也就是调用Dep.notify
过程分析
Dep.notify
是 Dep
的一个实例方法,定义在 src/core/observer/dep.js
中:
class Dep {
// ...
notify() {
// stabilize the subscriber list first
const subs = this.subs.slice();
// 遍历 dep 中存储的 watcher,执行 watcher.update()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
复制代码
这里主要是遍历 subs
,也就是 Watcher
的实例数组,然后调用 watcher
的 update
方法:
class Watcher {
// ...
update() {
/* istanbul ignore else */
if (this.computed) {
// 懒执行时走这里,比如 computed
// 将 dirty 置为 true,就可以让 computedGetter 执行时重新计算 computed 回调函数的执行结果
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true;
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify();
});
}
} else if (this.sync) {
// 在使用 $watch 或者 watch 选项时,可以传入一个 sync 选项,标识 watcher 需要同步更新
this.run();
} else {
// 一般的 watcher 更新都是异步队列,将 watcher 放入到更新对象队列
queueWatcher(this);
}
}
}
复制代码
这里对于 Watcher
的不同状态执行不同逻辑,computed
和 sync
稍后再看,在一般的组件数据更新的场景中,会执行 queueWatcher(this)
。
queueWatcher
定义在 src/core/observer/scheduler.js
中:
const queue: Array<Watcher> = [];
let has: { [key: number]: ?true } = {};
let waiting = false;
let flushing = false;
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher(watcher: Watcher) {
const id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
// 如果已经刷新,则根据它的id进行拼接;
// 如果已经超过了它的id,则它将立即运行。
let 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(flushSchedulerQueue);
}
}
}
复制代码
在这里引入了队列的概念 ,在 Vue
中并不会每次数据变化都会触发 Watcher
回调,而是把这些 Watcher
先添加到队列里面,然后在 nextTick
后执行 flushSchedulerQueue
。
queueWatcher
的逻辑主要注意这几点:
- 首先用
has
保证只添加一次Watcher
- 满足
!flushing
条件,把watcher
推入到队列中,否则说明正在执行队列,就会从后往前找,找到第一个待插入watcher
的id
比当前队列中watcher
的id
大的位置。把watcher
按照id
的插入到队列中,因此queue
的长度发生了变化 【标记点 1】 - 通过
waiting
来保证nextTick(flushSchedulerQueue)
逻辑只执行一次,nextTick
暂时不管,可以理解为是在异步的去执行flushSchedulerQueue
flushSchedulerQueue
定义在 src/core/observer/scheduler.js
中:
let flushing = false;
let index = 0;
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue() {
flushing = true;
let watcher, id;
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
// 1.组件的更新由父到子:因为父组件的创建过程是先于子组件的。所以 watcher 的创建也是先子后父,执行顺序也是先子后父
// 2.用户自定义的 watcher 要优于 渲染 watcher 执行:因为用户自定义的 watcher 是在渲染 watcher 之前创建的
// 3.如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行可以被跳过
queue.sort((a, b) => a.id - b.id);
// do not cache length because more watchers might be pushed
// as we run existing watchers
// 每次遍历都会求一次 queue 的长度,因为在 watcher.run() 时,有可能用户会添加新的 watcher
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 (process.env.NODE_ENV !== "production" && 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
const activatedQueue = activatedChildren.slice();
const updatedQueue = queue.slice();
// 状态恢复
resetSchedulerState();
// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit("flush");
}
}
复制代码
flushSchedulerQueue
主要做了下面几件事:
- 队列排序:
queue.sort((a, b) => a.id - b.id)
- 组件的更新由父到子:因为父组件的创建过程是先于子组件的。所以
watcher
的创建也是先子后父,执行顺序也是先子后父 - 用户自定义的
watcher
要优于 渲染watcher
执行:因为用户自定义的watcher
是在渲染watcher
之前创建的 - 如果一个组件在父组件的
watcher
执行期间被销毁,那么它对应的watcher
执行可以被跳过
- 队列遍历
for (index = 0; index < queue.length; index++) {
// ...
watcher.run();
// ...
}
复制代码
拿到 watcher
之后执行它的 run
方法。在这里要注意每次遍历都会求一次 queue
的长度,因为在 watcher.run()
时,有可能用户会添加新的 watcher
,这时就会走到我们讲到的 queueWatcher
方法内,执行 【标记点 1】 的逻辑。
- 状态恢复:
resetSchedulerState()
const queue: Array<Watcher> = [];
let has: { [key: number]: ?true } = {};
let circular: { [key: number]: number } = {};
let waiting = false;
let flushing = false;
let index = 0;
/**
* Reset the scheduler's state.
*/
function resetSchedulerState() {
index = queue.length = activatedChildren.length = 0;
has = {};
if (process.env.NODE_ENV !== "production") {
circular = {};
}
waiting = flushing = false;
}
复制代码
目的就是把控制流程状态的一些变量恢复到初始值,把 watcher
队列清空。
接下来分析 watcher.run()
方法,定义在 src/core/observer/watcher.js
中:
class Watcher {
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run() {
if (this.active) {
this.getAndInvoke(this.cb);
}
}
getAndInvoke(cb: Function) {
const value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value;
this.value = value;
this.dirty = false;
if (this.user) {
try {
// 传入的两个参数就是 `value` 和 `oldValue` ,这就是我们使用自定义 `watcher` 时能在回调函数的参数中拿到 `newVal` 、 `oldVal` 的原因
cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`);
}
} else {
cb.call(this.vm, value, oldValue);
}
}
}
}
复制代码
run
方法就是执行 getAndInvoke
方法,并传入 watcher
的回调函数,主要做了下面两件事:
const value = this.get()
- 满足新旧值不相等、新值为对象类型、
deep
模式其一,就执行watcher
的回调。在回调函数执行的时候传入的两个参数就是value
和oldValue
,这就是我们使用自定义watcher
时能在回调函数的参数中拿到newVal
、oldVal
的原因。
对于渲染 watcher
而言,它在执行 this.get()
方法求值的时候,会执行 getter
方法:
updateComponent = () => {
vm._update(vm._render(), hydrating);
};
复制代码
以上就是当我们去修改数据时,会触发组件重新渲染的原因。
总结
依赖派发的实质就是当数据变化时,触发 setter
逻辑,把在依赖收集过程中订阅的所有观察者也就是 watcher,都触发她们的 update
的过程,并且这个过程利用了队列做进一步优化,在 nextTick 后执行所有 watcher
的 run,最后执行它们的回调函数。
参考:Vue.js 技术揭秘