浅曦 Vue 源码 - 43-patch 阶段 - 异步队列更新 & 性能优化

一、背景

Vue 如何组织队列更新,主要依托于下面几个方法:

1.Watcher.prototype.update,当响应式数据发生变化,其对应的 dep.notify 执行,watcher.update 会调用 queueWatcher;
2.queueWatcher 负责把 watcher 实例加入到待求值的 watcher 队列 queue 中,添加到队列需要根据当前队列是否处于刷新状态做不同的处理;
3.queueWatcher 还会调用 nextTick 方法,传入消耗 queue 队列的 flushSchedulerQueue 方法;
4.nextTick 会把 flushSchedulerQueue 包装然后放到 callbacks 队列,nextTick 另一个重要任务就是把消耗 callbacks 队列的 flushCallback 放入到下一个事件循环(或者下一个事件循环的开头,即微任务);

总结起来就两件事:

响应式数据发生变化,将依赖它的 watcher 放到 queue 队列;
nextTick 把消耗 queue 的 flushSchedulerQueue 放到 callbacks 队列,同时把消耗 callbacks 队列的 flushCallbacks 方法放到下个事件循环(或事件环的开头)
听完这些感觉已经很明白了,但是现在有两个具体的问题需要分析一番:

如果在一个用户 watcher 中修改某一个 渲染 watcher 依赖的响应式数据,这个渲染 watcher 会被多次添加到 queue 吗?

在一个 tick 中多次修改同一个被渲染 watcher 依赖的响应式数据(或者修改多个不同的响应式数据)那么渲染 watcher 会被多次添加到 queue 队列中吗?

很多人在看 Vue 面试题的时候都看到过一句话:Vue 会合并当前事件循环中的所有更新,只触发一次依赖它的 watcher;

所以答案很显然:是不会多次添加的,今天我们就来掰扯掰扯为什么不会?

二、用户 watcher 修改响应式数据

先来看一段示例代码:

这个示例代码是想表达:

渲染 watcher 依赖了 forProp.a 以及条件渲染的 imgFlag,即

{ {froProp.a}}

当点击 button 按钮时,更新响应式数据 forProp.a 属性,使之 ++;
forProp.a 的变化就会触发用户 watcher 即 forProp.a(nv, ov) {…},用户 watcher 会在触发时更新 imgFlag;
首先 forProp.a 变化,渲染 watcher 肯定会被 push 到 queue 队列,那么用户 watcher 执行时会不会再次把渲染 watcher push 到 queue 队列,即 queue 中有两个渲染 watcher ?

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Vue</title>
</head>
<body>
<div id="app">
 <button @click="goForPatch">使 forProp.a++</button>
 <div v-if="imgFlag"> ===> {
    
    {
    
    forProp.a}}</div>
 <img v-else
    :src="imgSrc"
    onload="console.log('img onload')">
</div>
<script src="./dist1/vue.js"></script>
<script>
 const src = 'https://gift-static.hongyibo.com.cn/static/ad_oss/image-1004-294/6139ce3ed297e/16311783028626.jpg'
 debugger
 new Vue({
    
    
    el: '#app',
    forProp: {
    
    
      a: 100
    },
   imgFlag: false,
   imgSrc: src
  },  
  methods: {
    
    
    goForPatch () {
    
    
      this.forProp.a++
    }
  },
  
  watch: {
    
    
    // 这个 watch 选项就是 用户 watcher,
    // 有别于 Vue 自己创建的渲染 watcher、计算属性对应的 lazy watcher
    'forProp.a' (nv, ov) {
    
    
      this.imgFlag = !this.imgFlag
     }
   }
 })
</script>
</body>
</html>
复制代码

2.1 queueWatcher 的 has [id]、waiting

2.1.1 watch.id

每个 watcher 被创建时,都会获取一个唯一自增的 id,这个值是唯一的,无论是用户 watcher 还是 渲染 watcher 都有;

2.1.2 has[id]

前面的 forProp.a++ 使得 forProp.a 的 setter 被触发,最终调用 dep.notity -> watcher.update -> queueWatcher(this);

queueWatcher 把 this(watcher 实例)添加到 queue,在添加之前会判断缓存对象 has 中是否已经存在该 watcher.id,如果判断出 has[id] 不存在,再 push 到 queue,并且 has[id] = watcher.id;

export function queueWatcher (watcher: Watcher) {
    
    
  const id = watcher.id

  // 如果 watcher 已经存在,则跳过,不会重复进入 queue
  if (has[id] == null) {
    
    
    // 缓存 watcher.id,用于判断 watcher 是否已经进入队列
    has[id] = true
    if (!flushing) {
    
    
      queue.push(watcher)
    }
  }
}
复制代码

2.2 用户 watcher 和 渲染 watcher 的顺序

从上一篇讲述 消耗 queue 队列的 flushSchedulerQueue 方法中的得知,在触发 watcher 重新求值前会有一个给 queue 中的 watcher 按照 id 进行升序排序,所以 id 小的 watcher 将会被先执行;

所以现在问题变成了 用户 watcher 和 渲染 watcher 的 id 谁更小的问题。这个问题答案很显然,是用户 watcher id 更小。

在 Vue watcher 的 id 是个自增的值,先被创建的 watcher 的 id 会更小; 用户 watcher 是在初始化时初期进行响应式数据初始化的过程中创建的,而渲染 watcher 是在挂载阶段创建的,所以用户 watcher id 更小;

这里我们假设用户 watcher id 为 4,渲染 watcher 的 id 为 5;

此时缓存 watcher id 的 has 对象:{ 4: true, 5: true };

2.3 消耗 queue 队列

综上,当 flushSchedulerQueue 方法执行时,开始遍历排序后的 queue 队列执行 queue 中每一项 watcher.run() 方法,因为用户 watcher id 较小,所以就会先执行用户 watcher 的回调: forProp.a(nv, ov) { this.imgFlag = !this.imgFlag }。

imgFlag 被重新赋值,就会触发 imgFlag 这个响应式数据的 setter,进而触发 dep.notify(),notify() 执行会触发 watcher.update(),调用流程如下:

this.imgFlag = !this.imgFlag;
  -> imgFlag setter () 
    -> dep.notify()
      -> watcher.update()
        -> queueWatcher(this) this 是渲染 watcher,其 id  5
          -> if (has[id] == null) 不成立,因为 has = {
    
     4: true, 5: true }
复制代码
export function queueWatcher (watcher: Watcher) {
    
    
  // 此时 watcher 是渲染 watcher,id  5
  const id = watcher.id

  // 因为 has = {
    
     4: true, 5: true },
  // 由于 imgFlag 变更时,渲染 watcher 已经在 queue 了,
  // 所以不会重复将渲染 watcher 放入 queue
  if (has[id] == null) {
    
    
    has[id] = true
    if (!flushing) {
    
    
      queue.push(watcher)
    }
  }
}
复制代码

2.4 总结

因为 watcher 被放入到 queue 前经过了判重处理,同时因为用户 watcher 的执行时机早于渲染 watcher,所以在用户 watcher 中修改渲染 watcher 依赖的数据时,不会多次将渲染 watcher 放入到 queue;

这么做的好处显而易见了,这就能够避免用户 watcher 中修改响应式数据导致页面刷新多次,这就减少了非常大的性能开销。

这里还有一个隐藏条件:当渲染 watcher 执行时,就能拿到用户 watcher 更新后的响应式数据最新值,这是为啥?因为用户 watcher 和 渲染 watcher 是同步串行的。

三、合并一个 tick 多次修改

3.1 一个 tick 多次修改同一个数据

先看一个例子:

这个例子很简单,当点击 button 按钮时,对 this.forProp++ 两次,此时分析一下会不会向 queue 中添加两次同一个渲染 watcher,同样我们假设渲染 watcher 的 watcher.id 为 5;

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Vue</title>
</head>
<body>
<div id="app">
 <button @click="goForPatch">使 forProp.a++</button>
 <div> ===> {
    
    {
    
    forProp.a}}</div>
</div>
<script src="./dist1/vue.js"></script>
<script>
  debugger
  new Vue({
    
    
    el: '#app',
    data: {
    
    
      forProp: {
    
    
        a: 100
      },
    },

    methods: {
    
    
      goForPatch () {
    
    
        this.forProp.a++
        this.forProp.a++
      }
    }
  })
</script>
</body>
</html>
复制代码

点击事件触发时,this.forProp.a 第一次被 ++ 时,

this.forProp.a++
  -> forProp.a  setter()
   -> dep.notify()
    -> 渲染 watcher.update()
     -> queueWatcher(this)
      -> if (has[id] == null) 成立
      ->  has[5] = true
复制代码
第二次 this.forProp.a  ++ 时,还会走一变和上面类似的步骤,但是 has[5] == null 不成立了:

this.forProp.a++
  -> forProp.a  setter()
   -> dep.notify()
    -> 渲染 watcher.update()
     -> queueWatcher(this)
      -> if (has[id] == null) 不成立,has[5] = true
复制代码

虽然 this.forProps.a 在同一个 tick 中被 ++ 两次,但是最终 queue 中只有一个渲染 watcher;这个也就是常说的 Vue 性能优化的一个重要手段:合并同一个 tick 中对同一个响应式数据的多次更新。

为啥称之为合并呢?当渲染 watcher 真正触发重新求值的时候,已经是在多次更新响应式数据的 tick 之后的下一个 tick 了,此时渲染 watcher 重新求值,获取到的就是上一个 tick 中响应式数据的最新值,至于在最新值之前的值通通被渲染 watcher 忽略掉了,因为渲染 watcher 从来就不知道这个响应式数据有这么多的前任。

3.2 一个 tick 修改多个不同数据

这个原理同样被应用到在一个 tick 中一次性修改多个响应式数据,比如 this.forProp.a++ 然后 this.imgFlag = !this.imgFlag,这两个步骤都触发了各自的 setter,但是因为渲染 watcher 已经存在 queue 的原因,不会被重复添加,渲染 watcher 最后还是只有一个;

四、总结

深入理解 nextTick 精妙设计所在的过程。另外,也解答了何为合并多次修改的性能优化,其核心实现如下:

watcher 被 push 到 queue 之前有一步判断重复处理,即 has[id] == null;
watcher 被成功推入 queue 之后 has[id] = true,可保证下次 push 这个 watcher 时不会通过上一步的判重,因而不会重复加入 queue;
当 queue 中的 watcher 被重新求值前,会按 id 进行升序,用户 wacher 的 id 小于 渲染 watcher 的 id,所以用户 watcher 放心大胆的改响应式数据,同样是由于 has[渲染watcher id] = true 故而不会将渲染 watcher 多次加入 queue;
最后,每个 watcher 从 queue 取出并重新求值后 has[被取出 watcher id] 被置为 null,这就保证下个 tick 中修改响应式数据时,这些 watcher 又可以被重新添加到 queue;
源码附件已经打包好上传到百度云了,大家自行下载即可~

链接: https://pan.baidu.com/s/14G-bpVthImHD4eosZUNSFA?pwd=yu27
提取码: yu27
百度云链接不稳定,随时可能会失效,大家抓紧保存哈。

如果百度云链接失效了的话,请留言告诉我,我看到后会及时更新~

开源地址

码云地址:
http://github.crmeb.net/u/defu

Github 地址:
http://github.crmeb.net/u/defu

开源不易,Star 以表尊重,感兴趣的朋友欢迎 Star,提交 PR,一起维护开源项目,造福更多人!

猜你喜欢

转载自blog.csdn.net/qq_39221436/article/details/125003821
今日推荐