$nextTick的7道练习题-我猜你不能全对

584 阅读4分钟

摘要

本文介绍了$nexTick的工作原理,以及this.数据项=新值 背后做了什么,nextTick与promise的关系。阅读本文需要你对vue的基本运作过程,微任务以及promise有一定的了解。

内容

  • 7道测试题
  • $nextTick的工作原理,它与Promise的关系
  • this.数据项=新值 背后做了什么
  • 7道测试题的答案及讲解

测试题

下面是一份基础代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
      <div id="div">{{a}}</div>
      <button @click="btn">btn</button>
  </div>
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.9/vue.js"></script>
  <script>
    var vm = new Vue({
      el:"#app",
      data:{ a :1 },
      methods: {
        btn() {
          this.a = 2
          const div = document.getElementById('div')
          this.$nextTick(() => {
            console.log('nextTick', div.innerHTML)
          })
          console.log(div.innerHTML)
        }
      }
    })
</script>
</body>
</html>

问题是: 点击了按钮之后,输出的顺序和结果是什么?答案是:1;nextTick 2;  这应该难不到你吧。

好了,下面类似的题有7道,为了节省代码,我只给出了btn()中的部分。

题目1

图片

题目2

图片

题目3

图片

题目4

图片

题目5

图片

题目6

图片

题目7

图片

如果你能很清楚的答出来,就请直接拉到底部,留言给我,让我膜拜一下。要是你有遗憾,或者不太会,就请你一定坚持读完哈~

this.$nextTick到底做了什么

阅读源码是最好的方式,nextTick的源码如下(有删减):


function nextTick (cb, ctx) {
    var _resolve;
    callbacks.push(function () {
      // 把回调添加到callbacks数组中
      cb.call(ctx);
    });
    
    // 确保当前微任务执行完成之后,
    // 再把callbacks的内容推入下一微任务
    if (!pending) {
      pending = true;
      timerFunc(); //调用timerFunc
    }
  }
  var timerFunc;
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function () {
      p.then(flushCallbacks);
    };
    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)) {
    timerFunc = function () {
      setImmediate(flushCallbacks);
    };
  } else {
    timerFunc = function () {
      setTimeout(flushCallbacks, 0);
    };
  }
  function flushCallbacks () {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }
  

请你关注两个内容:callbacks和timerFunc。

callbacks是一个数组,它将用来收集回调。

timerFunc是一个异步函数,它是动态确定的。优先级如下:Promise.then > MutationObserver > setImmediate > setTimeout。 那在标准浏览器中(支持promise的环境),我们可以认为它就是Promise.then。所以可以用下图标识nextTick的功能。

它做两件事:

  1. 将回调函数push到其内部维护的callbacks数组中
  2. 用Promise.resolve().then 注册微任务,执行callbacks中的所有回调。

图片

这是图1。下边的讲解中还会有一个图2。你掌握两张图之后,前面的7道题就没有问题了。

this.a=2 做了什么

假设原来的数据是a=1,现在this.a=2做了什么呢?

我也准备了图给你 图片

上图中有很多中间过程,简化这个过程,如下图示:

图片 这是图2。总结一下就是:

  1. 数据的改变会导致nextTick执行,进而将watcher.run添加到callbacks这个内部维护的数组中,然后进入微任务序列。
  2. nextTick才是异步更新的关键点。

上边的讲解中有一个图1,现在有张图2。目前,你已经掌握两张图,可以直接进入练习题的答案及讲解部分了。

如果你想进一步加深印象就可以看下面具体过程:

数据修改后,激活属性的setter,

图片

调用dep的notify来通知订阅者 vue2源代码目录/src/core/observer/dep.js

图片

每个subs[i] 就是一个watcher(一个组件一个watcher),它的update方法如下

vue2源代码目录/src/core/observer/watcher.js

图片

注意,这里的update并不会立即执行,而是会进入 queueWatcher函数,这个函数是在scheduler.js中定义的,它用来对watcher进行统一排队管理。

vue2源代码目录/src/core/observer/scheduler.js , 它定义了一个flushSchedulerQueue函数,在这个函数中去调用watch,但是,它并不是直接执行的,而是交给了nextTick。这里才是异步的开始。注意下面的代码中第165,166行的:根据watcher的id,去掉重复的watcher

图片 最重要的,还是到了187行中的nextTick中来。

上面的flushSchedulerQueue如下(有删减):


function flushSchedulerQueue () {
    queue.sort((a, b) => a.id - b.id) 
    // watcher的id是有序的,父组件的created在子组件之前,所以它的watcher的id肯定要小些
    // 就会在前面执行
    for (index = 0; index < queue.length; index++) {
      watcher = queue[index]
      watcher.run()// 包括新的虚拟dom生成,diff算法,更新视图等等
      
    }
  }

而nextTick在前面已经分析过了。

相当于在微任务中去执行: flushCallbacks=>callbacks=>flushSchedulerQueue  =>watcher.run。

你可以再次回去看看图2。

测试题答案及讲解

题目1

图片

答案:1;  nextTick 2;

分析过程:

第3行:this.a=3 修改了数据,将watcher.run添加到callbacks,记callbacks=[watcher.run],并推到微任务队列:[  ...callbacks ]

第4行:this.a=2 修改了数据,本来也要将render()函数进入微任务队列,但前面介绍的queueWatcher()有对watcher的去重功能,所以,本句代码不会让render再次进入微任务队列。

第5行:this.$nextTick,将回调添加到了微任务队列的末尾。微任务队列中有: [watcher.run, 第5行的回调]

第8行:打印输出1,此时,同步任务结束。

开始依次从微任务队列中取出任务执行。

watcher.run:会更新视图,它执行完成后,视图中div的内容是2

第5行的回调:输出nextTick 2

题目2

图片

答案:1; nextTick  1;

分析过程:

第3行:this.$nextTick,将回调添加到callbacks, 记callbacks=[第5行的回调] 。微任务队列中有:[ ... callbacks ]

第6行:this.a=2 修改了数据,将watcher.run添加到callbacks,记callbacks=[第5行的回调, watcher.run],微任务队列中有:[第5行的回调,watcher.run ]

第7行:打印输出1,此时,同步任务结束。

开始依次从微任务队列中取出任务执行。

第5行的回调:输出nextTick 1。注意,它在watcher.run的前边执行。所以视图并没有更新。

watcher.run:会更新视图,它执行完成后,视图中div的内容是2。

题目3

图片

答案:1 ; nextTick 2;

题目4

图片

答案:1 ; nextTick 2;  resolve 2;

分析过程:

第1行:this.a=2 修改了数据,将watcher.run函数 添加到callbacks,记callbacks=[watcher.run],并将callback 进入微任务队列。微任务队列中有:[ ...callbacks ]

第4行:注册一个微任务。微任务队列中有:[ ...callbacks, console.log('resovle') ]

第7行:this.$nextTick将回调添加到了callbacks的末尾。callbacks=[watcher.run,nextTick的回调] ,微任务队列中有:[ ...callbacks, console.log('resovle') ]

第10行:打印输出1,此时,同步任务结束。

开始依次从微任务队列中取出任务执行。所以答案是:

watcher.run -> nextTick的回调 -->console.log('resovle')

题目5

图片

答案:1;resolve 1;  nextTick 1;

题目6

图片

答案:1; nextTick 1;  resolve 2;

题目7

图片

答案:1 ; nextTick 2; resolve 2

小结

$nextTick是异步函数,在标准浏览器中(支持原生Promise),它采用Promise.resolve().then()来实现异步功能。

$nextTick做两件事:1是将回调收集到内部闭包维护的callbacks中,2. 在本轮事件循环中的微任务中,依次执行callbacks中的回调。

数据改变时,仍旧会调用$nextTick,传入的回调是经过去重排序之后的water.run。

转载自 mp.weixin.qq.com/s/Gaa7tDLR-…