翼度科技»论坛 编程开发 JavaScript 查看内容

Vue之关于异步更新细节

7

主题

7

帖子

21

积分

新手上路

Rank: 1

积分
21
前言

Vue官网对于异步更新的介绍如下:

  • Vue 在更新 DOM 时是异步执行的。
  • 只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
  • 如果同一个 watcher 被多次触发,只会被推入到队列中一次。
  • 这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的
Vue使用Object.defineProperty对数据劫持后,当对对象进行set操作,就会触发视图更新。

更新逻辑

以下面实例来分析视图更新处理逻辑:
  1. <div>{{ message }}</div>
  2. <button @click="handleClick">更新</button>

  3. new Vue({
  4.         data: {
  5.                 message: ''
  6.         },
  7.         methods: {
  8.                 handleClick() {
  9.                         this.message = Date.now();
  10.                 }
  11.         }
  12. })
复制代码
当点击更新按钮后会对已劫持的属性message做赋值操作,此时会触发Object.defineProperty的set操作。

Object.defineProperty set操作

Object.defineProperty的set函数的设置,实际上最核心的逻辑就是触发视图更新,具体代码逻辑如下:
  1. set: function reactiveSetter (newVal) {
  2.         // 其他逻辑
  3.        
  4.     // 触发视图更新
  5.     dep.notify();
  6. }
复制代码
每个属性都会对应一个Dep对象,当对属性进行赋值时就会调用Dep的notify实例方法,该实例方法的功能就是是通知视图需要更新。

Dep notify实例方法

notify实例方法的代码逻辑如下:
  1. Dep.prototype.notify = function notify () {
  2.   // stabilize the subscriber list first
  3.   var subs = this.subs.slice();
  4.   for (var i = 0, l = subs.length; i < l; i++) {
  5.     subs[i].update();
  6.   }
  7. };
复制代码
subs中存储是watcher对象,每个Vue实例都存在一个与视图更新关联的watcher对象,该对象的创建是在$mount阶段,具体看查看之前的文章Vue实例创建整体流程
  1. 代表属性的Dep对象与watcher对象的关联是在render函数调用阶段具体属性获取时建立的即依赖收集
复制代码
notify方法会执行与当前属性关联的所有watcher对象的update方法,必然会存在一个视图更新相关的watcher。
watcher对象的按照分类实际上分为两类:

  • 视图更新相关的,每一个Vue实例都存在一个此类的watcher对象
  • 逻辑计算相关的,计算属性和watch监听所创建的watcher对象

Watcher update实例方法

update实例方法的代码逻辑具体如下:
  1. Watcher.prototype.update = function update () {
  2.   /* istanbul ignore else */
  3.   if (this.lazy) {
  4.     this.dirty = true;
  5.   } else if (this.sync) {
  6.     this.run();
  7.   } else {
  8.     queueWatcher(this);
  9.   }
  10. };
复制代码
lazy、sync都是Watcher的属性,分别表示:

  • lazy:表示懒处理,即延迟相关处理,用于处理计算属性
  • computedsync:表示同步执行,即触发属性更新就立即更新视图
从上面逻辑中可知,默认是queueWatcher处理即开启一个队列,并缓冲在同一事件循环中发生的所有数据变更,即视图是异步更新的。
这里需要注意的一点是:
queueWatcher中必然存在视图更新的watcher对象,不会存在计算属性computed对应的watcher(computed对应的watcher对象lazy属性默认为true),可能存在watch API对应的用户性质的watcher对象

queueWatcher执行逻辑
  1. function queueWatcher (watcher) {
  2.   var id = watcher.id;
  3.   if (has[id] == null) {
  4.     has[id] = true;
  5.     if (!flushing) {
  6.       queue.push(watcher);
  7.     } else {
  8.       // if already flushing, splice the watcher based on its id
  9.       // if already past its id, it will be run next immediately.
  10.       var i = queue.length - 1;
  11.       while (i > index && queue[i].id > watcher.id) {
  12.         i--;
  13.       }
  14.       queue.splice(i + 1, 0, watcher);
  15.     }
  16.     // queue the flush
  17.     if (!waiting) {
  18.       waiting = true;
  19.       nextTick(flushSchedulerQueue);
  20.     }
  21.   }
  22. }
复制代码
实际上面逻辑主要分成3点:

  • 对于同一个watcher对象,使用has对象结构+id为key来判断队列中是否已存在对应watcher对象,如果存在就不会将其添加到queue中
  • 通过flushing标识区分当在清空队列过程中和正常情况下,如何向queue中添加watcher
  • 通过waiting标识区分是否要执行nextTick即清空queue的动作
  1. 因为queue是全局变量,在此步骤之前就将watcher对象添加到queue,如果waiting为true就标识已经调用nextTick实现异步处理queue了,就不要再次调用nextTick
复制代码
从上面整体逻辑可知,queueWacther的逻辑主要就两点:

  • 判断是否重复watcher,对于不重复的watcher将其添加到queue中
  • 调用nextTick开启异步处理queue操作即flushSchedulerQueue函数执行

nextTick + flushSchedulerQueue

nextTick函数实际上跟$nextTick是相同的逻辑,主要的区别就是上下文的不同,即函数的this绑定值的不同。
  1. 使用macroTask API还是microTask API来执行flushSchedulerQueue
复制代码
而flushSchedulerQueue函数就是queue的具体处理逻辑,主要逻辑如下:
  1. function flushSchedulerQueue () {
  2.   flushing = true;
  3.   var watcher, id;

  4.   // Sort queue before flush.
  5.   // This ensures that:
  6.   // 1. Components are updated from parent to child. (because parent is always
  7.   //    created before the child)
  8.   // 2. A component's user watchers are run before its render watcher (because
  9.   //    user watchers are created before the render watcher)
  10.   // 3. If a component is destroyed during a parent component's watcher run,
  11.   //    its watchers can be skipped.
  12.   queue.sort(function (a, b) { return a.id - b.id; });

  13.   for (index = 0; index < queue.length; index++) {
  14.     watcher = queue[index];
  15.     id = watcher.id;
  16.     has[id] = null;
  17.     watcher.run();
  18.   }

  19.   var activatedQueue = activatedChildren.slice();
  20.   var updatedQueue = queue.slice();

  21.   resetSchedulerState();

  22.   // call component updated and activated hooks
  23.   callActivatedHooks(activatedQueue);
  24.   callUpdatedHooks(updatedQueue);
  25. }
复制代码
flushSchedulerQueue函数的主要逻辑可以总结成如下几点:

  • 对队列queue中watcher对象进行排序
  • 遍历queue执行每个watcher对象的run方法
  • 重置控制queue的相关状态,用于下一轮更新
  • 执行组件的updated和activated生命周期
这里就不展开了,需要注意的是activated是针对于keep-alive下组件的特殊处理,updated生命周期是先子组件再父组件的,队列queue的watcher对象是按照父组件子组件顺序排列的,所以在源码中updated生命周期的触发是倒序遍历queue触发的。
首先说说watcher对象的run实例方法,该方法的主要逻辑就是执行watcher对象的getter属性和cb属性对应的函数。
上面说过watcher对象的按照分类实际上分为两类:

  • 视图更新相关的,每一个Vue实例都存在一个此类的watcher对象
  • 逻辑计算相关的,计算属性和watch监听所创建的watcher对象
watcher对象的getter属性和cb属性就是对应着上面各类watcher的实际处理逻辑,例如watch API对应的getter属性就是监听项,cb属性才是具体的处理逻辑。

为什么需要对queue中watcher对象进行排序?

实际上Vue源码中有相关说明,这主要涉及到嵌套组件Vue实例创建、render watch和用户watch创建的时机。
每个组件都是一个Vue实例,嵌套组件创建总是从父组件Vue实例开始创建的,在父组件patch阶段才创建子组件的Vue实例。
而这个顺序决定了watcher对象的id值大小问题:
父组件的所有watcher对象id < 子组件的所有watcher对象id
render watch实际上就是与视图更新相关的watcher对象,该对象是其对应的Vue实例创建的末期即挂载阶段才创建的,是晚于用户watch即计算属性computed和watch API创建的watcher对象,所以:
render watch的id < 所有用户watch的id的
子组件可能是更新触发源,如果父组件也需要更新视图,这样queue队列中子组件的watcher对象位置会在父组件的watcher对象之前,对queue中watcher对象进行排序就保证了:
视图更新时 父组件 总是先于 子组件开始更新操作,而每个组件对应的视图渲染的watcher最后再执行(即用户watcher对象对应的逻辑先执行)

总结

Vue异步更新的过程还是非常清晰的:

  • 对属性赋值触发Dep对象notify方法执行
  • 继而执行Watcher对象的update方法将对象保存到队列queue中
  • 继而调用mircoTask API或macroTask API执行queue中任务
  • 对队列中watcher进行排序,保证顺序执行的正确性,调用其对应run方法来实现视图更新和相关逻辑更新操作
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

来源:https://www.jb51.net/javascript/322395ip9.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具