美文网首页
vue异步更新流程梳理

vue异步更新流程梳理

作者: 0月 | 来源:发表于2021-07-09 21:21 被阅读0次

    前言

    vue实例创建后,当我们重新赋值data中的数据时,视图就会更新,那么具体干了啥呢?本文用demo + 调试断点,一步步来研究一下具体流程。

    demo代码

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="app">{{value}}</div>
      <script src="../dist/vue.js"></script>
      <script>
        new Vue({
          el: '#app',
          data() {
            return {
              value: 1
            }
          },
          mounted () {
            this.value = 2
          }
        })
      </script>
    </body>
    </html>
    

    众所周知,vue在实例化的时候会对data的数据进行响应式设置,当我们赋值的时候就会触发对应的setter,所以把断点打到setter看一下,如下图:


    image.png

    我们传入的newVal 是 2,老的val 是1,后面直接val = newVal ,那么此时val就更新为2了。更新之后,最后还有一句代码

    dep.notify();
    

    这里就是对观察者们进行通知了。当我们的值变化时,dep就负责去通知watcher们,每一个watcher实例就会调用自己内部的update方法。咱门进入notify方法看看它是不是这样子:


    image.png

    看果然是这样子,subs这个数组就是专门存放watcher实例的,循环遍历watcher,调用每个watcher自己的update方法。
    这里只存放了一个watcher实例,看看这个watcher是啥。这里先解释一下watcher都有哪些分类:

    1. 渲染watcher, 负责更新视图变化的,即一个vue实例对应一个渲染watcher
    2. 用户自定义watcher,用户通过watch:{value(val, oldVal){}}选项定义的,或者this.$watch()方法生成的。
    3. computed选项里面的计算属性也是watcher, 和第2点中的watcher的区别是它的watcher实例有dirty属性控制着watcher.value值的变化

    打开右边的scope栏,找到Local下的subs[0]


    image.png

    再去查看watcher.vm._wather是否有值,有就代表是渲染watcher。同时找到源码Watcher的声明:


    image.png

    由此可见我们现在正在执行update的这个watcher就是一个渲染watcher。
    继续找看update方法干了啥:

    image.png
    update就是执行queueWatcher(this),再看这里干了啥
    image.png
    其实猜也能猜到就维护一个queue:[watcher, ...]的队列,里面的watcher不重复;最后执行nextTick(flushSchedulerQueue); 看看flushSchedulerQueue干了啥,其实就是执行watcher.run方法。
    image.png

    来到这里我们大概就清晰了这个流程:
    赋值操作this.value = 2就是把渲染watcher放到一个queue的队列中,并且通过nextTick在将来某个时刻把queue队列的watcher拿出来一个个去执行watcher.run()方法。

    这里的nextTick就是利用事件循环,在未来某个时刻会执行flushSchedulerQueue方法 , flushSchedulerQueue 又是循环执行 watcher.run();继续回到watcher看run方法干了啥:


    image.png
    image.png

    注意观察上面两张图, 在run方法中执行this.get(),视图就改变了值,从1变成2,一切的谜团就在这一行代码中,我们继续研究get()方法!

    image.png

    get里面调用了this.getter.call(vm, vm),找到this.getter,发现这是一个updateComponent的方法:


    image.png

    那么这个方法是怎么来的呢?


    image.png

    如上图,是watcher实例化的第二个参数: expOrFn,那么就打断点在watcher实例化过程中,看看这个updateComponent怎么来的。刷新页面:


    image.png
    根据上图中1、2步骤,在call stack执行栈中往下点,一个个方法进去看,很幸运,在下面的mountComponent方法的执行栈就看到了updateComponent的声明,在下图2处,就是updateComponent这个方法做的事情,从方法中我们可以知道,就是这行代码起了作用
     vm._update(vm._render(), hydrating);
    
    image.png

    总结

    那么,至此我们再总结一下流程:
    this.value = 2 触发 setter,
    同步执行过程:
    setter => dep.notify() => watcher.update() =>queueWatcher(this) =>nextTick(flushSchedulerQueue);

    由于把flushSchedulerQueue放到了nextTick里面,那么接下来未来的某个时刻会执行flushSchedulerQueue,然后从queque队列中提取watcher出来循环执行watcher.run

    watcher.run() => watcher.get() => watcher.getter() => updateComponent() => vm._update(vm._render())

    到此为止我们就知道大概就是这么一个流程。
    最后再看看_update()方法与_render()方法,
    _render()方法很纯粹,就是返回一个虚拟dom: vnode对象。而_update()方法就是把虚拟dom: vnode去进行patch的过程,得到一个新的真实dom。

    一些问题:

    • 为什么watcher放在queue队列中不直接去执行watcher.run呢,而要放到nextTick里面,等待未来某个时刻统一执行呢?
      答:其实还是为了性能,高效,如果你这么写代码:
    this.value = 2
    this.value = 3
    

    没有nextTick就会走两次 vm._update(vm._render())了,而这里面的patch过程的diff就是一个比较复杂消耗性能的过程。

    • 为什么只有一个渲染watcher?
      答: 因为vue1.x就是因为采用了一个绑定值一个watcher的方式,虽然变化可以精确到绑定值的位置,但是这样子在大一点的项目就很多watcher,会消耗大量内存造成性能瓶颈,vue2采用了虚拟dom更新的方案,以组件为单位进行更新,一个组件实例对应一个渲染watcher。组件内的一个或者多个响应式属性更新 --> 触发渲染watcher.unpdate() --> 同一个渲染watcher只被送入一次queueWatcher队列 -->nextTick之后的回调里面触发watcher.run()。

    相关文章

      网友评论

          本文标题:vue异步更新流程梳理

          本文链接:https://www.haomeiwen.com/subject/smmtpltx.html