通过前几篇文章,我们了解到在我们修改 data
后会依次执行setter -> Dep -> Watcher -> patch -> 视图
的过程。
先看个例子
<template>
<div>
<div>{{number}}</div>
<div @click="handleClick">click</div>
</div>
</template>
export default {
data () {
return {
number: 0
};
},
methods: {
handleClick () {
for(let i = 0; i < 1000; i++) {
this.number++;
}
}
}
}
当我们点击click时,number
会被增加1000次,那么按照我们之前的理解,会执行1000次setter -> Dep -> Watcher -> patch -> 视图
的过程。显然这种方式是不可取的,那么vue中关于这种情况是怎么处理的呢?
vue在默认情况下,每次触发某个数据的 setter
方法后,对应的 Watcher
对象其实会被 push
进一个队列 queue
中,在下一个 tick
的时候将这个队列 queue
全部拿出来 run
( Watcher
对象的一个方法,用来触发 patch
操作) 一遍
我们再看一下watcher
的update
方法
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
这里有个queueWatcher
方法,通过这个方法就把当前的watcher对象 push
进一个队列 queue
中,在下一个 tick
的时候将这个队列 queue
全部拿出来 run
。
那么,什么是nextTick呢?
nextTick
众所周知,Event Loop分为宏任务(macro task)以及微任务( micro task),不管执行宏任务还是微任务,完成后都会进入下一个tick,并在两个tick之间执行UI渲染。
vue.js在 microtask(或是 macro task)中创建一个事件,目的是在当前调用栈执行完毕以后(不一定立即),即UI渲染完毕后,才会去执行这个事件.
Vue.js 实现了一个 nextTick
函数,传入一个 cb
,这个 cb
会被存储到一个队列中,在下一个 tick
时触发队列中的所有 cb
事件。
let callbacks = []; // 用来存放回调函数的队列,全局对象
let pending = false; // 异步锁,默认开启
function flushCallbacks () {
// 重置异步锁(开启,以便再次调用nextTick执行)
pending = false
// 复制一份callbacks出来,为了防止nextTick里调用另一个nextTick
const copies = callbacks.slice(0)
callbacks.length = 0 // 清空callbacks,以便下一次nextTick的cb
for (let i = 0; i < copies.length; i++) {
// 执行每一项回调
copies[i]()
}
}
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
/**异步方法,会根据不同的浏览器来选择不同的方法,这里理解为一个类似于setTimeout的异步方
法即可,当所有同步方法执行完成后会执行这个方法,这个方法内部会执行flushCallbacks方法**/
timerFunc()
}
}
简单说一下几个变量或方法
callbacks
:数组用来存储回调函数cb
。pending
: 我在这里称作异步锁。默认情况下,这个锁是开启状态(pending=false
),表示异步方法还未开始执行或者刚刚执行完毕
,当pending=true
时,表示正在等待所有同步方法执行完毕马上执行异步方法
timerFunc
: 异步方法,会根据不同的浏览器来选择不同的方法,这里理解为一个类似于setTimeout的异步方法即可,当所有同步方法执行完成后会执行这个方法,这个方法内部会执行flushCallbacks方法
当我们执行nextTick
时,这时候会把传入的这个cb
,push
到callbacks
的队列中并将pending
设为true
,等待所有的同步方法后,执行timerFunc
方法,这个方法内部会执行flushCallbacks
方法,将callbacks里的所有cb依次执行,并重置异步锁,以便下次调用nextTick
我们注意到在flushCallbacks
方法里,有这么一句话
const copies = callbacks.slice(0)
为什么?这是为了防止nextTick里调用另一个nextTick,假设不copy一份callbacks出来,那么嵌套两层执行nextTick时,会一次性将两次nextTick
的cb
全部执行完,这显然不符合我们的要求。
ok,到现在为止,我们已经了解了nextTick的实现原理,那么,我们再返回来看看,我们的update()
方法。
异步更新策略
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
我们来看看,这个queueWatcher
发生了什么?
let has: { [key: number]: ?true } = {}
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 如果queue里不存在当前的watcher
if (has[id] == null) {
has[id] = true // 表示该watcher已存在
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.
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)
}
}
}
// 省略部分源码,只保留核心部分
function flushSchedulerQueue () {
let watcher, id
for (index = 0; index < queue.length; index++) {
watcher = queue[index] //取出当前watcher
id = watcher.id // watcher的id
has[id] = null // 将has[id]置为空
watcher.run() // 执行run方法
}
waiting = false;
}
简单介绍几个变量和方法
has
:一种{ [key: number]: ?true }
的map
结构的数据,用来表示当前的watcher是否已经推入到当前队列中。waiting
: 是一个标记位,标记是否已经向nextTick
传递了flushSchedulerQueue
方法flushSchedulerQueue
:用来执行队列里的每一个watcher的run()方法。
整个update的过程就是:
首先判断当前watcher是否存在于queue里,如果不存在,则push
到队列中,否则则什么都不执行。
之后再将flushSchedulerQueue
方法,作为回调传入nextTick()
方法,再所有同步方法执行完成后,再将queue
中所有watcher的run方法依次执行。这个过程就是我们说的异步更新策略。
ok,再来看看我们开始的例子
<template>
<div>
<div>{{number}}</div>
<div @click="handleClick">click</div>
</div>
</template>
export default {
data () {
return {
number: 0
};
},
methods: {
handleClick () {
for(let i = 0; i < 1000; i++) {
this.number++;
}
}
}
}
当我们执行this.number++
时,触发过程就变为setter -> Dep -> Watcher -> update -> queueWatcher -> nextTick(flushSchedulerQueue) -> run -> patch -> 视图
当我们执行run
的时候,这时候number
已经变为1000
,这时候视图只更新1次。