Vue异步更新和nextTick实现原理

94 阅读2分钟
<body>
    <div id="app" ref="app">
        <div>{{ a }}</div>
        <div>{{ b }}</div>
        <div><button @click="test">修改</button></div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script>
        const app = new Vue({
            el: "#app",
            data(){
                return {
                    a: 1,
                    b: 2
                }
            },
            methods: {
                test(){
                    let _this = this
                    this.a = 11
                    this.$nextTick(function get(){
                       console.log(_this.$refs.app.innerHTML)
                    })
                    this.b = 22
                }
            },
            
        })
    </script>
</body>

data中的数据a绑定了一个render watcher image.png

data中的数据b也绑定了一个render watcher image.png 经过分析对比,a和b绑定的其实是同一个render watcher,对应的就是当前组件的render函数。

a和b的Watcher的id都是1。

上面的test方法执行过程:

this.a = 11
  1. 调用通过Object.defineProperty为a添加的set方法
  2. set函数会调用a绑定的Dep的notify方法
  3. Dep的notify方法中会遍历Dep下的所有Watcher,执行Watcher的update方法
  4. 调用nextTick方法将Watcher推入到微任务队列中
this.$nextTick(function get(){
     console.log(_this.$refs.app.innerHTML)
  })
  1. 调用nextTick方法将get方法推入到微任务队列中
this.b = 22

此段代码的执行过程同this.a = 11,但由于他绑定的Watcher和a绑定的Watcher是同一个render Watcher。所以b对应的Watcher不会被推入到微任务队列中。

基于JS的事件循环机制,此轮事件循环的主代码执行完成后,会执行当前主代码中产生的所有微任务。

由上面的分析可以得到微任务队列中有两个微任务,第一个是当前组件的render Watcher,第二个是通过nextTick传入的get方法。

render Watcher对应的当前组件的render方法,render方法执行完成后生成新的vnode,新旧vnode进行diff对比。将差异vnode渲染到真实dom中。

接下来执行第二个微任务,即get方法,get方法获取当前组件的innerHTML。由于get方法是在render Watcher 之后执行,所以此时获取到的innerHTML是最新的,如下

<div>11</div> <div>22</div> <div><button>修改</button></div>

考虑到兼容性问题,nextTick的实现异步的逻辑为:

优先使用promise.then,再MutationObserver,再setImmediate,最后使用setTimeout;

源码实现如下:

var timerFunc;

  // The nextTick behavior leverages the microtask queue, which can be accessed
  // via either native Promise.then or MutationObserver.
  // MutationObserver has wider support, however it is seriously bugged in
  // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
  // completely stops working after triggering a few times... so, if native
  // Promise is available, we will use it:
  /* istanbul ignore next, $flow-disable-line */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function () {
      p.then(flushCallbacks);
      // In problematic UIWebViews, Promise.then doesn't completely break, but
      // it can get stuck in a weird state where callbacks are pushed into the
      // microtask queue but the queue isn't being flushed, until the browser
      // needs to do some other work, e.g. handle a timer. Therefore we can
      // "force" the microtask queue to be flushed by adding an empty timer.
      if (isIOS) { setTimeout(noop); }
    };
    isUsingMicroTask = true;
  } else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // Use MutationObserver where native Promise is not available,
    // e.g. PhantomJS, iOS7, Android 4.4
    // (#6466 MutationObserver is unreliable in IE11)
    var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true
    });
    timerFunc = function () {
      counter = (counter + 1) % 2;
      textNode.data = String(counter);
    };
    isUsingMicroTask = true;
  } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    // Fallback to setImmediate.
    // Technically it leverages the (macro) task queue,
    // but it is still a better choice than setTimeout.
    timerFunc = function () {
      setImmediate(flushCallbacks);
    };
  } else {
    // Fallback to setTimeout.
    timerFunc = function () {
      setTimeout(flushCallbacks, 0);
    };
  }