Vue异步更新过程及$nextTick原理详解

源码调试地址

https://github.com/KingComedy/vue-debugger

什么是异步更新

在本轮宏任务内,组件内多个属性更新,或者一个属性更新多次,最终这个组件只会重新渲染一次,即组件里的dom只会做一次重新渲染 如:以下代码只会触发组件的一次更新渲染

this.count = 1
this.key = 'key'
this.count = 2

Vue为什么是异步更新

避免组件多次patch,引起dom多次重新渲染

nextTick(cb, ctx)方法源码详解(在src\core\util\next-tick.js中定义)

  • 使用:$nextTick(cb)
  • 在next-tick.js里 定义了callbacks回调函数数组,定义pending任务执行状态
  • nextTick(cb), 将传入的cb回调函数push到callbacks,并且判断pending任务执行状态是否为false,即当前没有执行任务
  • 设置pending=true,执行timeFunc()
  • timeFunc函数的任务主要是异步执行 flushCallbacks函数,即Promise.resolve().then(flushCallbacks)
  • flushCallbacks主要是 遍历执行callbacks里的每个回调
  • timeFunc函数这边会做浏览器兼容的降级处理, 即浏览器不支持Promise, 依次降级为 MutationObserver(前提不是IE浏览器) => setImmediate => setTimeout
  • 总结:nextTick会把传入的回调函数存入callbacks,flushCallbacks函数会遍历执行callbacks里的每个回调,nextTick会将flushCallbacks当做一个任务推入微任务队列。

异步更新过程

  • 当触发属性更新时,属性的dep(在数据做响应式处理时的依赖收集),会执行dep.notify通知属性的观察者watcher更新,执行所有watcher的update方法
  • watcher的update默认异步更新,执行queueWatcher(this), 将watcher推入watcher列表queue。通过watcher.id判断queue里是否已存在watcher,没有则加入到queue数组里
  • 判断!waiting === true, 即目前是否属于空闲状态,是则执行nextTick(flushSchedulerQueue)
  • flushSchedulerQueue: 对queue列表根据watcher.id进行排序,遍历执行执行queue列表里的所有watcher,先执行每一个watcher.before(即watcher所对应的组件的beforeUpdate生命周期函数) => 再执行watcher.run。遍历完成后,在重新遍历queue列表执行每个watcher所对应的组件的updated生命周期
  • 总结:属性更新 => 将渲染watcher推入queue列表(如果当前watcher已经存在就不入列) => 将遍历执行watcher的函数 flushSchedulerQueue 推入微任务队列, nextTick(flushSchedulerQueue)

 

通过例子加深理解

例子一

countClick() {
  this.count = 1 // 触发更新,watcher进入queue列表,queue.push(watcher), 此时更新任务已经进入微任务队列
  this.count = 2 // 触发更新,当前组件的渲染watcher已经在queue列表里,所以不会进入列表
  console.log('count:', this.$refs.count.innerHTML) // count: 0,微任务队列还未执行,dom还未更新
  this.$nextTick(() => {
    console.log('count:', this.$refs.count.innerHTML) // count: 2 推入微任务队列callbacks
  })
}

流程解析:

  • 打印的顺序是:count:0 => count:2
  • count 第一次设置值时,触发更新,watcher进入queue列表,即queue.push(watcher), 此时更新任务已经进入微任务队列,即微任务队列为[flushCallbacks],且callbacks = [flushSchedulerQueue],只有flushSchedulerQueue执行完后,dom才会更新
  • count 第二次更新时,因为当前组件的渲染watcher已经在queue列表里,所以不会进入queue列表
  • 因为本轮宏任务还未执行完成,所以微任务队列也还没有执行,即更新任务还未执行,所以此时dom还没更新,dom上的count值仍旧为0
  • 执行nextTick时,会把回调函数(cb1)推入callbacks列表,所以此时微任务队列为[flushCallbacks], 即[[...callbacks]], 且 callbacks = [flushSchedulerQueue, cb1]
  • 当宏任务执行完成,开始遍历执行微任务,此时微任务就一个flushCallbacks,遍历执行callbacks
  • flushSchedulerQueue先执行,执行完成后,此时dom已经更新
  • cb1执行时,dom已经改变,所以此时dom的count值为2

 例子二

countClick() {
  this.count = 1 // 触发更新,watcher进入queue列表,queue.push(watcher), 此时更新任务已经进入微任务队列
  this.count = 2 // 触发更新,当前组件的渲染watcher已经在queue列表里,所以不会进入queue列表
  Promise.resolve().then(() => {
    console.log('p1 count:', this.$refs.count.innerHTML) // count: 2
  })
  console.log('count:', this.$refs.count.innerHTML) // count: 0 属于宏任务,先执行,dom还未更新,所以count值还未改变为 0
  this.$nextTick(() => {
    console.log('cb1 count:', this.$refs.count.innerHTML) // count: 2
  })
}

流程解析:

  • 打印的顺序是:count:0 => cb1 count:2 => p1 count:2
  • count 第一次设置值时,触发更新,watcher进入queue列表,即queue.push(watcher), 此时更新任务已经进入微任务队列,即微任务队列为[flushCallbacks],且callbacks = [flushSchedulerQueue],只有flushSchedulerQueue执行完后,dom才会更新
  • count 第二次更新时,因为当前组件的渲染watcher已经在queue列表里,所以不会进入queue列表
  • Promise会把回调函数p1推入微任务队列,所以此时微任务队列为:[flushSchedulerQueue, p1]
  • 因为本轮宏任务还未执行完成,所以微任务队列也还没有执行,即更新任务还未执行,所以此时dom还没更新,dom上的count值仍旧为0
  • 执行nextTick时,会把回调函数(cb1)推入callbacks列表,所以此时微任务队列为[flushCallbacks, p1],即[[...callbacks], p1] ,且 callbacks = [flushSchedulerQueue, cb1]
  • 当宏任务执行完成,开始遍历执行微任务,此时微任务为[flushCallbacks, p1],会先执行flushCallbacks,即遍历执行callbacks = [flushSchedulerQueue, cb1]
  • flushSchedulerQueue先执行,执行完成后,此时dom已经更新
  • cb1执行时,dom已经改变,所以此时dom的count值为2,所以打印 cb1 count:2
  • 然后执行第二个微任务p1,即 p1 count:2

 例子三

countClick() {
  Promise.resolve().then(() => {
    console.log("p1 count:", this.$refs.count.innerHTML); // count: 0
  });
  this.count = 1; // 触发更新, flushSchedulerQueue 存入callbacks, flushCallbacks才进入微任务队列
  this.count = 2;
  
  console.log("count:", this.$refs.count.innerHTML); // count: 0 属于宏任务,先执行,dom还未更新,所以count值还未改变为 0
  this.$nextTick(() => {
    console.log("cb2 count:", this.$refs.count.innerHTML); // count: 2
  });
}

流程解析:

  • 打印顺序 count:0 => p1 count:0 => cb1 count:2
  • 因为Promise在 count设值之前,已经进入了微任务队列,count设值时,flushSchedulerQueue 存入callbacks, flushCallbacks才进入微任务队列
  • 所以最终的微任务队列为 [p1, flushCallbacks],所以p1 打印在dom更新前

 例子四

countClick() {
  this.$nextTick(() => {
    this.count = 3 // 触发一次更新,但是没有进入queue队列
    console.log("cb1 count:", this.$refs.count.innerHTML); // count: 2
  });
  Promise.resolve().then(() => {
    console.log("p1 count:", this.$refs.count.innerHTML); // 进入微任务队列
  });
  this.count = 1;
  this.count = 2;
  this.$nextTick(() => {
    console.log("cb2 count:", this.$refs.count.innerHTML); // count: 2
  });
}

流程解析:

  • 打印顺序 cb1 count:0 => cb2 count:3 => p1 count: 3
  • 微任务队列为:[flushCallbacks, p1], 且callbacks = [cb1, flushSchedulerQueue, cb2], cb1在执行的时候,flushSchedulerQueue还未执行,所以dom还未更新,所以打印时dom的count的值还是 0。
  • flushSchedulerQueue执行时,count的值已经在cb1更新为3,所以最终count的值为3
  • 所以flushCallbacks执行后打印顺序为: cb1 count:0 => cb2 count:3
  • 最后执行p1,打印p1 count:3

 

总结

  • 每个组件都有自己对应的渲染watcher,用于执行dom的更新
  • 组件内,在一个宏任务内,多个属性的更新或者同个属性的多次更新,watcher只会渲染一次
  • 异步更新流程:属性更新 => 通知watcher更新 => queueWatcher(this) => nextTick(flushSchedulerQueue)
  • 属性更新 只会将flushSchedulerQueue推入微任务队列,只有在flushSchedulerQueue执行完后,dom才会更新完成

相关文章

Vue源码解析——组件更新过程:https://blog.csdn.net/comedyking/article/details/115670343

Vue-Watcher观察者源码详解:https://blog.csdn.net/comedyking/article/details/117695761

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值