前言
距离上一篇过去了一个多月,抱歉咕咕咕了这么久。上个月在公司疯狂加班加点赶版本进度(领导说这个很重要拍),在测试进度都快80%了,领导一句话先别上了,有个更重要的页面重构让我先去整,直接把我整麻了,合着我加班了一个月等于做了个寂寞。。然后又要去铲前人的屎💩山代码(其实就是我自己拉一坨好看点的💩山上去,顺便写点注释)。无论如何,最难熬的时刻快结束了,终于有时间可以写文章了!
Vue的批量、异步更新队列
大家以前或多或少都可能对 Vue
的更新,还有 nextTick
有些疑问吧,就比如最经典的通过 v-if
将某个组件从 false
改为 true
,然后立刻获取焦点,你会发现写成同步的方式是行不通的,至于为什么可能也没太过去在意,所以我们就一起去了解下吧。
前置知识
大家都知道这涉及到了event loop
的知识,我们稍微快速的过一下。
任务的话有两类,一个是同步任务,一个是异步任务,而异步任务又可以分为宏任务和微任务。在每个宏任务执行之后会清空掉微任务队列,再执行下一个宏任务。
举个简单的例子
console.log("Start");
setTimeout(() => {console.log("setTimeout")}, 0);
new Promise((resolve)=>{
console.log("Promise0")
resolve();
})
.then(() => {console.log("Promise1")})
.then(()=>{console.log("Promise2")})
console.log("End")
结果
Start
Promise0
End
Promise1
Promise2
setTimeout
分析
宏任务队列: [run script, setTimeout]
同步任务: [Start, Promise0, End]
微任务队列: [Promise1, Promise2]
调用顺序 [run script [Start, Promise0, End [Promise1, Promise2]],setTimeout]
调用过程(没兴趣的同学可直接看小总结)
回想一下,我们每个属性都做了数据拦截,每次 set
的时候都会去触发 Dep
然后告诉 Watcher
更新,那么我们只要追踪一下这个过程就知道大概的一个顺序。
src/core/observer/index.js
- dep.notify();
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创建key和dep一一对应的关系
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,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
...
return value;
},
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();
}
// #7981: for accessor properties without setter
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
// 变更通知
dep.notify();
},
});
}
src/core/observer/dep.js
- subs[i].update() => Watcher.update()
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
// 翻译: 可能没有开启有异步的更新,这时候就要排列一下,用正确的顺序去更新
subs.sort((a, b) => a.id - b.id)
}
// 循环管理内部的watcher实例,执行他们的update方法
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
src/core/observer/watcher.js
- queueWatcher()
update() {
/* istanbul ignore else */
// lazy指computed
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
// sync 是 watcher 的时候可以传入配置{sync: true}
this.run();
} else {
// 正常情况下是这步
// watcher入队
queueWatcher(this);
}
}
src/core/observer/scheduler.js
- nextTick()
- flushSchedulerQueue
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.
// 翻译: 如果已经在冲刷,会根据这个watcher的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
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
// 异步启动任务队列冲刷任务
// 此处的nextTick就是我们用的那个
// 启动一个异步任务,在未来的某个时刻执行flushSchedulerQueue
nextTick(flushSchedulerQueue)
}
}
}
src/core/util/next-tick.js
- nextTick()
- timerFunc()
- flushCallbacks() // 作用: 遍历执行
[flushSchedulerQueue]
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
// 这里的callbacks存放的是下面包装过的flushSchedulerQueue
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 声明
let timerFunc
// 一堆的降级处理,
// 可以简单的理解为有Promise用Promise, 没有就用MutationObserver,setImmediate,setTimeout
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else {...}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 封装一个能够处理错误的高阶函数
// 并将它存入callbacks的数组中
callbacks.push(() => {
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(resolve => {
_resolve = resolve
})
}
}
src/core/observer/scheduler.js
- flushSchedulerQueue()
- watcher.run()
// 遍历执行所有的watchers,执行他们的run函数
function flushSchedulerQueue() {
currentFlushTimestamp = getNow();
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.
queue.sort((a, b) => 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];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
// 真正的更新函数
watcher.run();
...
}
...
}
src/core/observer/watcher.js
- this.get()
- this.getter.call(vm, vm) -> 调用mountComponent
run() {
if (this.active) {
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;
if (this.user) {
const info = `callback for watcher "${this.expression}"`;
invokeWithErrorHandling(
this.cb,
this.vm,
[value, oldValue],
this.vm,
info
);
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
pushTarget(this);
let value;
const 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 {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
}
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
...
// parse expression for getter
// 回想之前的$mount -> mountComponent -> updateComponent + new Watcher(vm, updateComponent)
// 如果参数2是函数,表示它是组件的更新函数
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
...
}
this.value = this.lazy ? undefined : this.get();
}
src/core/instance/lifecycle.js
- updateComponent()
- render()
- update(vnode)
- patch(oldVnode, vnode)
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el;
if (!vm.$options.render) {
...
}
callHook(vm, "beforeMount");
// 组件更新函数声明
let updateComponent;
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
...
} else {
updateComponent = () => {
// 首先执行render -> vdom
// 然后_update将vdom转为dom
vm._update(vm._render(), hydrating);
};
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 重点,updateComponent 就是 watcher里的第二个参数 expOrFn
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, "beforeUpdate");
}
},
},
true /* isRenderWatcher */
);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, "mounted");
}
return vm;
}
小总结
- obj.set()
- dep.notify() // 遍历调用watcher的update
- watcher.update()
- queueWatcher(this) // watcher入队
queue: Watcher[]
,并且会对watcher进行去重 - nextTick(flushSchedulerQueue)
- timerFunc() //
callbacks: flushSchedulerQueue[]
---async--- - flushCallbacks() // 遍历调用callbacks的flushSchedulerQueue
- flushSchedulerQueue() // 遍历调用queue的watcher.run
- watcher.run() // 真正的更新函数
- watcher.getter() -> updateComponent
- render()
- update(vnode)
- patch(oldvnode, vnode)
关于nextTick
下面通过几个例子来解释一下 nextTick
的一些原理,以及 和 Promise
的一些区别。
思考1: 为什么在nextTick才能拿到dom的最新值
<div id="app">
<h2>初始化</h2>
<div>
<p class="ppp">count --- {{count}}</p>
</div>
</div>
const app = new Vue({
el: "#app",
data: {
count: "asd",
},
mounted() {
let el = document.querySelector(".ppp");
console.log("start0", el.textContent, this.count);
this.count = 1;
console.log("start1", el.textContent, this.count);
this.count = 2;
console.log("start2", el.textContent, this.count);
this.$nextTick(() => {
console.log("next--", el.textContent, this.count);
});
},
});
大家不妨先猜一下结果,再来康康对不对。
可以看到,我们每次修改完count之后直接去访问el的真实值会发现没有变化,而是需要在nextTick中才能拿到真实值。到这里大家可能会觉得nextTick是异步的嘛,肯定在这些set函数之后执行,如果你有这样的一个想法,那么请你再看下面的例子再下判断。
思考2:如果nextTick放到前面有用吗
const app = new Vue({
el: "#app",
data: {
count: "asd",
},
mounted() {
let el = document.querySelector(".ppp");
this.$nextTick(() => {
console.log("next1--", el.textContent, this.count);
});
console.log("start0", el.textContent, this.count);
this.count = 1;
console.log("start1", el.textContent, this.count);
this.$nextTick(() => {
console.log("next2--", el.textContent, this.count);
});
this.count = 2;
console.log("start2", el.textContent, this.count);
},
});
注意到了吗,next1打印的el.textContent的值没有变化,穿插在start1和start2之间的next2能拿到最新值。我们结合前面的异步更新过程来看下这里有什么秘密。
解释起来很简单,我们手动调用的nextTick会往callbacks里塞进一个回调函数,我们的this.count = 1
也会往里面塞入一个回调函数。那么我们只要在flushSchedulerQueue后面的回调函数中访问,就能获得dom的最新修改值。
思考3:Promise和nextTick哪个先执行
大家想一想,Promise和nextTick都是微任务,那么按照event loops来理解的话,是不是谁先写在前面谁就先执行?我们一起来验证下。
const app = new Vue({
el: "#app",
data: {
count: "asd",
},
mounted() {
let el = document.querySelector(".ppp");
console.log("start0", el.textContent, this.count);
this.count = 1;
console.log("start1", el.textContent, this.count);
this.count = 2;
console.log("start2", el.textContent, this.count);
Promise.resolve().then(() => {
console.log("Promise--", el.textContent, this.count);
});
this.$nextTick(() => {
console.log("next--", el.textContent, this.count);
});
},
});
嗯?是不是跟你想的有点不一样。但是如果你看了上面的调用过程,有一个特别重要的东西如果你留意到了就知道为什么会这样。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc;
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
再看一下小总结的图
这下清楚了吧,Promise 从设计上来讲优先级没有nextTick高,所以它不会nextTick先执行。
小总结
nextTick的理解
- 概念: vue批量异步更新策略的实际执行者,组件更新的时候他不会立即执行,而是通过nextTick异步起动
- 作用:nextTick(cb),当数据变化需要访问dom最新的值时候
- 如何工作:数据变化的时候,会触发dep.notify,让watcher入队,然后nextTick会异步的冲刷队列,最后调用watcher.run()更新dom
总结
- 异步:只要监听到数据变化,
Vue
就会开启一个队列,并缓冲在同一个事件循环中发生的所有数据变更。 - 批量:如果同一个
watcher
被多次触发,只会被推入到队列中一次。去重对于避免不必要的计算和dom操作是十分必要的。然后在下一个事件循环的tick
中,Vue
会刷新队列实际工作。 - 异步策略:
Vue
在内部对异步队列会尝试使用Promise.then
进行异步处理,如果浏览器不支持就会尝试MutationObserver
、setImmediate
、setTimeout
进行异步操作。