活着,最有意义的事情,就是不遗余力地提升自己的认知,拓展自己的认知边界。
引言
关于Vue中的数据更新,是通过watcher来完成的,那么:
- 不同的数据更新顺序是什么样的?
- 不同类型的watcher之间的执行顺序又是怎样的?
- watcher的调度原理是怎样的?
- Vue是如何基于Promise实现异步更新的? 读完下面的内容,希望能对你有所帮助!
测试案例
一个简单的案例
<!DOCTYPE html>
<html>
<head>
<title>Vue源码剖析</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="demo">
<h1>行装</h1>
<div>背心:{{vest}}</div>
<div>裤子:{{trousers}}</div>
</div>
<script>
// 创建实例
const app = new Vue({
el: '#demo',
data: {
vest: 'broad',
trousers: 'broad'
},
beforeUpdate(){
console.log('keep in shape')
},
mounted() {
setTimeout(() => {
this.vest = 'thin'
}, 1000)
}
});
</script>
</body>
</html>
宏任务与微任务
事件循环(Event Loop):浏览器为了协调事件处理、脚本执行、网络请求和渲染等任务而制定的工作机制。
宏任务Task:代表一个个离散的、独立的工作单元。浏览器完成一个宏任务,在下一个宏任务执行开始前,会对页面进行重新渲染。主要包括创建文档对象、解析HTML、执行主线JS代码以及各种事件如页面重载、输入、网络事件和定时器等。
微任务:微任务是更小的任务,是在当前宏任务执行结束后立即执行的任务。如果存在微任务,浏览器会清空微任务之后再重新渲染。微任务的例子有Promise回调函数、DOM变化等。
源码解析
切入点
从上述简单案例中入手,在beforeUpdate钩子中打印日志,并在打印日志的地方打一个断点,刷新页面,下面看执行到断点处时的调用堆栈:
按照执行顺序依次为:
-
mounted: 页面完成初始渲染和挂载后,执行mounted钩子函数; -
(匿名):用户传给setTimeout的匿名函数; -
proxySetter:在匿名函数中,通过this访问了data选项中的属性,因此触发了代理修改器(在初始化过程中,对data选项中的属性进行了代理处理,详细过程可参考Vue2源码解析☞ 2 ☞ 初始化) -
reactiveGetter:在proxyGetter函数中,修改了vm._data中的属性,因此触发了reactiveGetter函数(关于data选项中属性定义数据劫持,可参考Vue2源码解析☞ 3 ☞ 响应式机制) -
notify:在reactiveGetter函数中,通过dep调用notify函数,在notify函数中,分别执行dep.subs中watcher实例的update方法(其中,watcher可能是三种watcher(详细介绍可参考:Vue2源码解析☞ 4 ☞ 搞懂Vue中的三种Watcher),此处是render watcher) -
update:在此函数中,Vue提供了三种处理策略:- 1、
watcher实例的lazy属性为true时,将dirty属性置为true,适用于computed watcher,既没有调用watcher的run方法,也没有放入异步更新队列(关于计算属性的值是何时更新的,computedGetter是如何被触发的,可参考:Vue2源码解析☞ 4 ☞ 搞懂Vue中的三种Watcher); - 2、
watcher实例的sync属性为true时,会立即执行watcher实例的run方法;(只有watch watcher的sync属性适合设置为true,个人观点仅供参考); - 3、其余场景,执行
queueWatcher函数
- 1、
-
至于
queueWatcher、nextTick、timerFunc、flushCallbacks、flushSchedulerQueue等函数具体做了什么,下面将通过源码来分析; -
before:此函数是创建render watcher实例时,以options参数形式传给Watcher构造器的,并挂载到watcher实例上的。(before函数本质上就是调用beforeUpdate钩子);
queueWatcher的逻辑
/**
* 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) {//防止重复执行watcher更新
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.
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)
}
}
}
- 如果
flushing为false,则直接将watcher追加到queue中(此时并没有进行排序)。 - 如果
flushing为true,则执行以下逻辑:- 获取
watcher队列的最大索引,并赋值给i index是全局变量,是watcher队列中当前正在处理的watcher实例的索引。在flushSchedulerQueue函数中,会根据watcher的id属性进行升序排列,并依次处理每一个watcher,并实时更新index的值。- 如果
i > index && queue[i].id > watcher.id,表明queue[i]还没有处理,且可以将当前的watcher插入到queue[i]前边。 - 如果
i > index && queue[i].id < watcher.id,表明虽然queue[i]还没有处理,但当前watcher的id属性比queue[i]的id属性值大,所以会排在第i+1项。 - 如果
i < index,表明queue[i]已经处理了,只能将当前watcher放在第i+1项。
- 获取
- 如果
waiting为false,则将flushSchedulerQueue传递给nextTick,在下一个时钟执行,waiting的值是通过resetSchedulerState函数来恢复。
那么,nextTick到底做了什么,继续往下看。
nextTick
export function nextTick (cb?: Function, ctx?: Object) {//注意:两个参数都是可选参数
let _resolve
callbacks.push(() => {//callbacks用来收集需要异步执行的代码,异步更新队列
if (cb) {
try {
cb.call(ctx) //允许我们给回调函数绑定上下文,也就是this,通常情况下是vm,特殊情况下可以考虑
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx) //当没有传入回调函数时,
}
})
if (!pending) {//执行flushCallbacks时,在开始执行异步队列中的代码前,将pending置为false
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
下面我们从实际的发生场景来解读上面的代码:
-
场景一:既有回调函数,又有上下文 将回调函数封装到箭头函数代码块中,追加到异步更新队列中,并给回调函数绑定传入的上下文。 如果
pending为false,则执行timerFunc。 -
场景二:只有回调函数,没有上下文 与场景一的唯一区别,就是没有修改回调函数的上下文。
-
场景三:没有回调函数 此时,压入异步更新队列的本质上就是resolve函数。
例如:
this.$nextTick(null, obj).then((data) => {
//略
)
其中,this.$nextTick()返回的是Promise实例,当执行flushCallbacks时,callbacks中某一项的resolve函数执行后,此时会触发then函数的回调,将obj赋值给data。
timerFunc
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
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)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
//略
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
//略
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
-
Vue采用的降级策略:
Promise——>MutationObserver——>setImmediate——>setTimeout。由于目前主流浏览器都已支持Promise API,所以着重研究Promise的场景即可。 -
对于
IOS平台,需要添加一个空的计时器,来强制清空微任务队列。
flushCallbacks
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)//备份异步更新队列
callbacks.length = 0 //清空异步更新队列
for (let i = 0; i < copies.length; i++) {
copies[i]() //执行异步回调
}
}
- 上述代码,保证在一个微任务中,将
pending置为false,同时备份异步更新队列,并置空异步更新队列。
flushSchedulerQueue
在queueWatcher函数中,将flushSchedulerQueue传给了nextTick函数,因此flushSchedulerQueue是异步更新队列的一个回调函数。
callbacks中,除了可能是flushSchedulerQueue外,还可能是用户编写的回调。
flushSchedulerQueue函数中,也会涉及到一个队列,不同的是调度队列中都是watcher实例,
Vue是如何调度这些watcher实例的呢?
继续看源码:
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)
// 这里的user watcher其实就是我们所说的watch watcher,因为初始化watch选项是在initData阶段,
// render watcher是在beforeCreate之后。
// 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() //这里的watcher可以是watch watcher和render watcher
}
id = watcher.id
has[id] = null
watcher.run()
}
// keep copies of post queues before resetting state
// 在重置状态前,保存缓存组件队列的备份和watcher队列的备份
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue) //本质上是执行updated钩子
}
- 对
watcher队列进行排序,为了保证先创建的watcher先更新。 - 依次执行
watcher实例的run方法(对于watch watcher就是执行其handler,对于render watcher就是重新渲染挂载)。 - 重置watcher调度相关的状态。
- 执行缓存组件的相应钩子(缓存组件后续有专题介绍),执行
updated钩子。
总结
1、watcher的执行顺序
在Vue中,三种watcher中只有render watcher和watch watcher会异步执行,并且通过watcher实例的id值的大小来排序。
在一个Vue实例中,先执行initWatch,然后创建render watcher,也就是说在vue实例内部,watch watcher的执行顺序优先与render watcher。
- 对于有父子关系的两个
vue实例,他们的钩子执行顺序如下: - 父组件的
beforeCreate - 父组件的
created - 父组件的
beforeMount- 子组件的
beforeCreate - 子组件的
created - 子组件的
beforeMount - 子组件的
mounted
- 子组件的
- 父组件的
mounted
那么这两个vue实例的render watcher的创建顺序是怎样的呢?
先看一个调用堆栈图:
从上图可以知道,先创建vue实例的render watcher实例,然后执行组件的渲染挂载,如果存在子组件,则会执行createComponent,进一步执行子组件的初始化。
由此可知,父组件render watcher的执行顺序优先与子组件。
2、异步更新机制
当data的属性值变化后,会触发代理修改器proxySetter,进而触发reactiveSetter函数,通过dep发送通知,执行subs中的所有watcher实例的update方法。
如果需要异步执行,则会执行queueWatcher,watcher实例被压入queue队列。
watcher的调度函数flushSchedulerQueue,会被当做回调函数压入callbacks队列中,通过Promise异步完成各回调函数的执行。
结束语
千山万水何惧怕,拨开云雾见红霞
—— 祝君好梦 ——