入口
在实现响应式时,我们给每个属性重写的set方法里面的dep.notify()来遍历每个watcher实现节点更新。而入口就是在notify方法。
notify
notify方法主要为遍历当前属性的dep收集的所有wacther,然后调用每个watcher的update方法。
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() //通知所有依赖来调用自己的update方法
}
}
}
watcher的update方法
update方法主要执行updateWater(),this为watcher实例,sync以及lazy都为该watcher实例的属性。update方面有三个分支
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
- 懒执行:例如computed就会进行该分支,该分支主要为将dirty设置为true,在组件更新之后,当响应式数据再次被更新时,执行computed的getter,重新执行computed回调函数,计算新值,然后缓存到watcher.value
- 同步执行(this.$watch()或者watch选项):传递一个sync配置,比如{sync:true}
- 将当前watcher放入watcher队列,一般走这个分支
queueWatcher
queueWatcher()主要做了把当前watcher根据id大小然后找到一个watcher的id比当前watcher的id小的插入,然后把watcher实例添加到queue数组中,然后调用nextTick()把flushSchedulerQueue()方法添加进去。
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) { // watcher只有第一次加入队列,后面会被忽略
has[id] = true//缓存一下,表示已经入队
if (!flushing) { // flushing为false说明当前watcher队列没有再被刷新,watcher直接进队
queue.push(watcher)
} else {//flushing为true说明队列已经被刷新了,这时候watcher需要进行排序操作,从而保证新入队的watcher后,刷新队列依然有序
// 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
// 跳过执行过的watcher和所有比我大的
while (i > index && queue[i].id > watcher.id) {
i--
}
// 最后放在队列中,一个比我老的 watcher 后面
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) { //为false,表示当前浏览器的异步队列任务中没有flushSchedulerQueue函数的时候
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
//如果当前为开发环境,配置传入了async值 为true的时候
//直接进行同步执行,直接去刷新wathcer队列,性能大打折扣
flushSchedulerQueue()
return
}
//nextTick,this.$nextTick调用的方法
nextTick(flushSchedulerQueue) // 注册宏微任务
}
}
}
- nextTick nextTick的原理:try/catch包裹方法添加进callbacks数组->timerFun(使用微任务),执行flushCallbacks->flushCallbacks为遍历callback数组执行每个方法
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
//callbacks为一个全局数组,把我们传入的nextTick的回调函数做了一层try/catch包装,然后将包装后的函数放在callbacks数组中,所以callbacks数组存储就是我们传入的nextTick的回调函数
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {//锁,防止重复执行timerFunc
pending = true
timerFunc()//把flushCllbacks放在new Promise.resolve().then(flushCllbacks)中
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
- flushCallbacks函数是:
- 将pending再次设置为false,表示下一个flushCallbacks函数可以进入浏览器的微任务队列
- 清空callbacks数组
- 执行callbacks数组中的所有函数
- flushSchedulerQueue
- 用户自己调用this.$nextTick传递的回调函数
function flushCallbacks () {
pending = false//确保同一时刻在微任务任务队列中只有一个flushCallbacks
const copies = callbacks.slice(0) //将callbacks中的数据复制给copies,并将callbacks清空
callbacks.length = 0
for (let i = 0; i < copies.length; i++) { // 执行回调函数
copies[i]()
}
}
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true//flushing为true表示watcher队列正在被刷新,不会进行新watcher添加
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.
// watcher.id 越大,表示这个 watcher 越年轻,实例是越后面生成的
// 本着先更新父组件再更新子组件的原则
// 当 父组件传给子组件的数据变化的时候,父组件需要把 变化后的数据 传给 子组件,子组件才能知道数据变了那么 子组件才能更新组件内使用 props 的地方所以,父组件必须先更新,把最新数据传给 子组件,子组件再更新,此时才能获取最新的数据不然你子组件更新了,父组件再传数据过来,那就不会子组件就不会显示最新的数据了
// 一个组件在父组件更新期间被销毁了,它及它的子组件将会被跳过
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
//循环遍历wathcer队列,一次执行watcher的run方法
for (index = 0; index < queue.length; index++) {
//拿出当前索引的watcher
watcher = queue[index]
//首先执行before钩子----渲染的时候会有一个beforeUpate钩子
if (watcher.before) {
watcher.before()
}
//清空缓存,表示当前watcher已经被执行,当该watcher再次入队时就可以进来了
id = watcher.id
has[id] = null
//执行watcher的run方法
watcher.run()//执行了updateComponent方法,该方法主要为把对应的虚拟节点机型patch操作从而更新dom
// 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()
// reset state
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
- 为什么需要排队呢?要先更新父组件再更新子组件呢? 因为父组件会给子组件通过props传递一些数据,而这些props也是一个watcher,而在flushSchedulerQueue该调度队列方法里面,会对watcher的run方法调用从而更新节点。因此先更新那个watcher的数据要有先后顺序,否则子组件先父组件更新,拿到的props的数据就会是旧的数据,从而不能达到更新的目的。
- run方法
-
执行实例化watcher传递的第二个参数,updateComponnet或者获取this.xx的一个函数(parsepath返回的函数)
-
更新旧值为新值
-
执行实例化watcher时传递的第三个参数,比如用户watcher的回调函数
-
更新过程图示
总结
-
异步更新: Vue的异步更新机制的核心是利用了浏览器的异步任务队列实现的,首选微任务队列,宏任务队列次之。
当响应式数据更新之后,会调用dep.notify方法,通知dep中收集的watcher去执行update方法。
然后通过nextTick方法将一个刷新watcher队列的方法(flushScheduleQueue)放入一个全局的callbacks数组中。
如果此时浏览器的异步队列中没有一个叫flushCallbacks的函数,则执行timeFunc函数,将flushCallbacks函数放入异步任务队列中。如果异步任务队列中已经存在flushCallbacks函数,等待其执行完成后再放入下一个flushCallbacks函数。
flushCallbacks函数负责执行callbacks数组中的所有flushSchedulerQueue函数。
flushSchedulerQueue函数负责刷新watcher队列,即执行queue数组每一个watcher的run方法,从而进行更新阶段,比如执行组件更新函数或者执行用户watch的回调函数。 -
nexxTick如何实现 Vue.nextTick实现原理:
-
将 传递的回调函数用try/catch包裹放入callbacks数组
-
执行timeFunc函数,在浏览器的异步任务队列放入一个刷新callbacks数组的函数flushCallbacks(执行存储的为用户使用nextTick回调的callbacks数组中的每个函数)。因为flushCallbacks在执行callbacks数组的时候,callbacks数组第一个方法就是flushSchedulerQueue,而flushSchedulerQueue这个调度器主要是执行每个watcher进行大小排序,本着先更新父组件后更新子组件的原则,然后执行排序好的watcher的run方法从而更新节点。执行完这个flushSchedulerQueue方法之后,节点已经实现更新了。然后再执行callbacks后的方法,这些方法就是我们自己使用nextTick可以获取更新后的节点。 例如:flushSchedulerQueue方法主要为排序watcher数组,并且执行watcher的run方法,进行调用pacth方法来更新真实dom。
mounted:{
this.counter = 1
this.counter = 2
this.counter = 3
this.nextTick(() => {
console.log('111')
})
}
//callbacks数组第一个就是flushSchedulerQueue
//后面跟的就是用户nextTick的回调方法
此时callbcks的数组值为:[flushSchedulerQueue, () => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
}]
nextTick更多理解:
因为vue是异步更新DOM的,一旦观察到数据变化,Vue会开启一个队列,然后把同一个事件循环当中观察到的数据变化的watcher推送到这个队列中。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲罐行为可以有效的去掉重复数据造成的不必要的计算和DOM操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。
宏任务:setTimeout ,setInterval, setImmediate,requestAnimationFrame, I/O ,UI渲染
微任务:Promise, process.nextTick, Object.observe, MutationObserver