Vue系列---理解Vue.nextTick使用及源码分析(五)

阅读目录javascript

一. 什么是Vue.nextTick()?html

官方文档解释为:在下次DOM更新循环结束以后执行的延迟回调。在修改数据以后当即使用该方法,获取更新后的DOM。vue

咱们也能够简单的理解为:当页面中的数据发生改变了,就会把该任务放到一个异步队列中,只有在当前任务空闲时才会进行DOM渲染,当DOM渲染完成之后,该函数就会自动执行。java

2.1 更改数据后,进行节点DOM操做。api

好比修改数据、修改节点样式、等操做。好比说我修改data中的一个属性数据后,若是我这个时候直接获取该html内容的话,它仍是老数据的,那么此时此刻,咱们可使用 Vue.nextTick(), 在该函数内部获取该数据便可: 以下代码:
<!DOCTYPE html>
<html>
<head>
  <title>vue.nextTick()方法的使用</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <template>
      <div ref="list">{{name}}</div>
    </template>
  </div>
  <script type="text/javascript">
    new Vue({
      el: '#app',
      data: {
        name: 'kongzhi111'
      },
      mounted() {
        this.updateData();
      },
      methods: {
        updateData() {
          this.name = 'kongzhi222';
          console.log(this.$refs.list.textContent); // 打印 kongzhi111
          this.$nextTick(() => {
            console.log('-------');
            console.log(this.$refs.list.textContent); // 打印 kongzhi222
          });
        }
      }
    })
  </script>
</body>
</html>

如上代码,页面初始化时候,页面显示的是 "kongzhi111"; 当页面中的全部的DOM更新完成后,我在mounted()生命周期中调用 updateData()方法,而后在该方法内部修改 this.name 这个数据,再打印 this.$refs.list.textContent, 能够看到打印的数据 仍是 'kongzhi111'; 为何会是这样呢?那是由于修改name数据后,咱们的DOM尚未被渲染完成,因此咱们这个时候获取的值仍是以前的值,可是咱们放在nextTick函数里面的时候,代码会在DOM更新完成后 会自动执行 nextTick()函数,所以这个时候咱们再去使用 this.$refs.list.textContent 获取该值的时候,就能够获取到最新值了。
理解DOM更新:在VUE中,当咱们修改了data中的某一个值后,并不会马上去渲染html页面,而是将vue更改的数据放到watcher的一个异步队列中,只有在当前任务空闲时才会执行watcher中的队列任务,所以这就会有一个延迟时间,所以咱们把代码放到nextTick函数后就能够获取到该 html 页面的最新值了。数组

2.2 在created生命周期中进行DOM操做。promise

在Vue生命周期中,只有在mounted生命周期中咱们的HTML才渲染完成,所以在该生命周期中,咱们就能够获取到页面中的html DOM节点,可是若是咱们在 created生命周期中是访问不到DOM节点的。
在该生命周期中咱们想要获取DOM节点的话,咱们须要使用 this.$nextTick() 函数。浏览器

好比以下代码进行演示:缓存

<!DOCTYPE html>
<html>
<head>
  <title>vue.nextTick()方法的使用</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <template>
      <div ref="list">{{name}}</div>
    </template>
  </div>
  <script type="text/javascript">
    new Vue({
      el: '#app',
      data: {
        name: 'kongzhi111'
      },
      created() {
        console.log(this.$refs.list); // 打印undefined
        this.$nextTick(() => {
          console.log(this.$refs.list); // 打印出 "<div>kongzhi111</div>"
        });
      },
      methods: {
        
      }
    })
  </script>
</body>
</html>

如上代码,在created生命周期内,咱们打印 this.$refs.list 值为undefined,那是由于在created生命周期内页面的html没有被渲染完成,所以打印出为undefined; 可是咱们把它放入 this.$nextTick函数内便可 打印出值出来,这也印证了 nextTick 是在下次DOM更新循环结束以后执行的延迟回调。所以只有DOM渲染完成后才会自动执行的延迟回调函数。app

Vue的特色之一就是能实现响应式,但数据更新时,DOM不会当即更新,而是放入一个异步队列中,所以若是在咱们的业务场景中,须要在DOM更新以后执行一段代码时,这个时候咱们可使用 this.$nextTick() 函数来实现。

三. Vue.nextTick的调用方式以下:

Vue.nextTick([callback, context]) 和 vm.$nextTick([callback]);

Vue.nextTick([callback, context]); 该方法是全局方法,该方法可接收2个参数,分别为回调函数 和 执行回调函数的上下文环境。

vm.$nextTick([callback]): 该方法是实列方法,执行时自动绑定this到当前的实列上。

四:vm.$nextTick 与 setTimeout 的区别是什么?

在区别他们俩以前,咱们先来看一个简单的demo以下:
<!DOCTYPE html>
<html>
<head>
  <title>vue.nextTick()方法的使用</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <template>
      <div ref="list">{{name}}</div>
    </template>
  </div>
  <script type="text/javascript">
    new Vue({
      el: '#app',
      data: {
        name: 'kongzhi111'
      },
      created() {
        console.log(this.$refs.list); // 打印undefined
        setTimeout(() => {
          console.log(this.$refs.list); // 打印出 "<div>kongzhi111</div>"
        }, 0);
      }
    })
  </script>
</body>
</html>

如上代码,咱们不使用 nextTick, 咱们使用setTimeout延迟也同样能够获取页面中的HTML元素的,那么他们俩之间到底有什么区别呢?

经过看vue源码咱们知道,nextTick 源码在 src/core/util/next-tick.js 里面。在vue中使用了三种状况来延迟调用该函数,首先咱们会判断咱们的设备是否支持Promise对象,若是支持的话,会使用 Promise.then 来作延迟调用函数。若是设备不支持Promise对象,再判断是否支持 MutationObserver 对象,若是支持该对象,就使用MutationObserver来作延迟,最后若是上面两种都不支持的话,咱们会使用 setTimeout(() => {}, 0); setTimeout 来作延迟操做。

在比较 nextTick 与 setTimeout 的区别,其实咱们能够比较 promise 或 MutationObserver 对象 与 setTimeout的区别的了,由于nextTick会先判断设备是否支持promise及MutationObserver 对象的,只要咱们弄懂 promise 和 setTimeout的区别,也就弄明白 nextTick 与 setTimeout的区别了。

在比较promise与setTimeout以前,咱们先来看以下demo。

<!DOCTYPE html>
<html>
<head>
  <title></title>
  <meta charset="utf-8">
</head>
<body>
  <script type="text/javascript">
    console.log(1);
    setTimeout(function(){
      console.log(2);
    }, 0);
    new Promise(function(resolve) {
      console.log(3);
      for (var i = 0; i < 100; i++) {
        i === 99 && resolve();
      }
      console.log(4);
    }).then(function() {
      console.log(5);
    });
    console.log(6);
  </script>
</body>
</html>

如上代码输出的结果是:1, 3, 4, 6, 5, 2; 首先打印1,这个咱们能理解的,其实为何打印3,在promise内部也属于同步的,只有在then内是异步的,所以打印 1, 3, 4 , 而后执行then函数是异步的,所以打印6. 那么结果为何是 1, 3, 4, 6, 5, 2 呢? 为何不是 1, 3, 4, 6, 2, 5呢?

咱们都知道 Promise.then 和 setTimeout 都是异步的,那么在事件队列中Promise.then的事件应该是在setTimeout的后面的,那么为何Promise.then比setTimeout函数先执行呢?

理解Event Loop 的概念

咱们都明白,javascript是单线程的,全部的任务都会在主线程中执行的,当主线程中的任务都执行完成以后,系统会 "依次" 读取任务队列里面的事件,所以对应的异步任务进入主线程,开始执行。

可是异步任务队列又分为: macrotasks(宏任务) 和 microtasks(微任务)。 他们二者分别有以下API:

macrotasks(宏任务): setTimeout、setInterval、setImmediate、I/O、UI rendering 等。

microtasks(微任务): Promise、process.nextTick、MutationObserver 等。

如上咱们的promise的then方法的函数会被推入到 microtasks(微任务) 队列中,而setTimeout函数会被推入到 macrotasks(宏任务) 任务队列中,在每一次事件循环中 macrotasks(宏任务) 只会提取一个执行,而 microtasks(微任务) 会一直提取,直到 microtasks(微任务)队列为空为止。

也就是说,若是某个 microtasks(微任务) 被推入到执行中,那么当主线程任务执行完成后,会循环调用该队列任务中的下一个任务来执行,直到该任务队列到最后一个任务为止。而事件循环每次只会入栈一个 macrotasks(宏任务), 主线程执行完成该任务后又会循环检查 microtasks(微任务) 队列是否还有未执行的,直到全部的执行完成后,再执行 macrotasks(宏任务)。 依次循环,直到全部的异步任务完成为止。

有了上面 macrotasks(宏任务) 和 microtasks(微任务) 概念后,咱们再来理解上面的代码,上面全部的代码都写在script标签中,那么读取script标签中的全部代码,它就是第一个宏任务,所以咱们就开始执行第一个宏任务。所以首先打印 1, 而后代码往下读取,咱们遇到setTimeout, 它就是第二个宏任务,会将它推入到 macrotasks(宏任务) 事件队列里面排队。
下面咱们继续往下读取,
遇到Promise对象,在Promise内部执行它是同步的,所以会打印3, 4。 而后继续遇到 Promise.then 回调函数,他是一个 microtasks(微任务)的,所以将他 推入到 microtasks(微任务) 事件队列中,最后代码执行 console.log(6); 所以打印6. 第一个macrotasks(宏任务)执行完成后,而后咱们会依次循环执行 microtasks(微任务), 直到最后一个为止,所以咱们就执行 promise.then() 异步回调中的代码,所以打印5,那么此时此刻第一个 macrotasks(宏任务) 执行完毕,会执行下一个 macrotasks(宏任务)任务。所以就执行到 setTimeout函数了,最后就打印2。到此,全部的任务都执行完毕。所以咱们最后的结果为:1, 3, 4, 6, 5, 2;

咱们能够继续多添加几个setTimeout函数和多加几个Promise对象来验证下,以下代码:

<script type="text/javascript">
  console.log(1);
  setTimeout(function(){
    console.log(2);
  }, 10);
  new Promise(function(resolve) {
    console.log(3);
    for (var i = 0; i < 10000; i++) {
      i === 9999 && resolve();
    }
    console.log(4);
  }).then(function() {
    console.log(5);
  });
  setTimeout(function(){
    console.log(7);
  },1);
  new Promise(function(resolve) {
    console.log(8);
    resolve();
  }).then(function(){
    console.log(9);
  });
  console.log(6);
</script>

如上打印的结果为: 1, 3, 4, 8, 6, 5, 9, 7, 2;

首先打印1,这是没有任何争议的哦,promise内部也是同步代码,所以打印 3, 4, 而后就是第二个promise内部代码,所以打印8,再打印外面的代码,就是6。所以主线程执行完成后,打印的结果分别为:

1, 3, 4, 8, 6。 而后再执行 promise.then() 回调的 microtasks(微任务)。所以打印 5, 9。所以microtasks(微任务)执行完成后,就执行第二个宏任务setTimeout,因为第一个setTimeout是10毫秒后执行,第二个setTimeout是1毫秒后执行,所以1毫秒的优先级大于10毫秒的优先级,所以最后分别打印 7, 2 了。所以打印的结果是: 1, 3, 4, 8, 6, 5, 9, 7, 2;
总结: 如上咱们也看到 microtasks(微任务) 包括 Promise 和 MutationObserver, 所以 咱们能够知道在Vue中的nextTick 的执行速度上是快于setTimeout的。

咱们从以下demo也能够获得验证:

<!DOCTYPE html>
<html>
<head>
  <title>vue.nextTick()方法的使用</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <template>
      <div ref="list">{{name}}</div>
    </template>
  </div>
  <script type="text/javascript">
    new Vue({
      el: '#app',
      data: {
        name: 'kongzhi111'
      },
      created() {
        console.log(this.$refs.list); // 打印undefined
        setTimeout(() => {
          console.log(this.$refs.list); // 打印出 "<div>kongzhi111</div>"
        }, 0);
        this.$nextTick(function(){
          console.log('nextTick比setTimeout先执行');
        });
      }
    })
  </script>
</body>
</html>

如上代码,先打印的是 undefiend, 其次是打印 "nextTick比setTimeout先执行" 信息, 最后打印出 "<div>kongzhi111</div>" 信息。

五:理解 MutationObserver

在Vue中的nextTick的源码中,使用了3种状况来作延迟操做,首先会判断咱们的设备是否支持Promsie对象,若是支持Promise对象,就使用Promise.then()异步函数来延迟,若是不支持,咱们会继续判断咱们的设备是否支持 MutationObserver, 若是支持,咱们就使用 MutationObserver 来监听。最后若是上面两种都不支持的话,咱们会使用 setTimeout 来处理,那么咱们如今要理解的是 MutationObserver 是什么?
5.1 MutationObserver是什么?

MutationObserver 中文含义能够理解为 "变更观察器"。它是监听DOM变更的接口,DOM发生任何变更,MutationObserver会获得通知。在Vue中是经过该属性来监听DOM更新完毕的。

它和事件相似,但有所不一样,事件是同步的,当DOM发生变更时,事件会马上处理,可是 MutationObserver 则是异步的,它不会当即处理,而是等页面上全部的DOM完成后,会执行一次,若是页面上要操做100次DOM的话,若是是事件的话会监听100次DOM,可是咱们的 MutationObserver 只会执行一次,它是等待全部的DOM操做完成后,再执行。

它的特色是:

1. 等待全部脚本任务完成后,才会执行,即采用异步方式。
2. DOM的变更记录会封装成一个数组进行处理。
3. 还能够观测发生在DOM的全部类型变更,也能够观测某一类变更。

固然 MutationObserver 也是有浏览器兼容的,咱们可使用以下代码来检测浏览器是否支持该属性,以下代码:

var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;
// 监测浏览器是否支持
var observeMutationSupport = !!MutationObserver;

MutationObserver 构造函数

首先咱们要使用 MutationObserver 构造函数的话,咱们先要实列化 MutationObserver 构造函数,同时咱们要指定该实列的回调函数,以下代码:

var observer = new MutationObserver(callback);

观察器callback回调函数会在每次DOM发生变更后调用,它接收2个参数,第一个是变更的数组,第二个是观察器的实列。

MutationObserver 实列的方法

observe() 该方法是要观察DOM节点的变更的。该方法接收2个参数,第一个参数是要观察的DOM元素,第二个是要观察的变更类型。

调用方式为:observer.observe(dom, options);

options 类型有以下:

childList: 子节点的变更。
attributes: 属性的变更。
characterData: 节点内容或节点文本的变更。
subtree: 全部后代节点的变更。

须要观察哪种变更类型,须要在options对象中指定为true便可; 可是若是设置subtree的变更,必须同时指定childList, attributes, 和 characterData 中的一种或多种。

1. 监听childList的变更

以下测试代码:

<!DOCTYPE html>
<html>
<head>
  <title>MutationObserver</title>
  <meta charset="utf-8">
</head>
<body>
  <div id="app">
    <ul>
      <li>kongzhi111</li>
    </ul>
  </div>
  <script type="text/javascript">
    var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;
    var list = document.querySelector('ul');
    var Observer = new MutationObserver(function(mutations, instance) {
      console.log(mutations);  // 打印mutations 以下图对应的
      console.log(instance);   // 打印instance 以下图对于的
      mutations.forEach(function(mutation){
        console.log(mutation); // 打印mutation
      });
    });
    Observer.observe(list, {
      childList: true, // 子节点的变更
      subtree: true // 全部后代节点的变更
    });
    var li = document.createElement('li');
    var textNode = document.createTextNode('kongzhi');
    li.appendChild(textNode);
    list.appendChild(li);
  </script>
</body>
</html>

如上代码,咱们使用了 observe() 方法来观察list节点的变化,只要list节点的子节点或后代的节点有任何变化都会触发 MutationObserver 构造函数的回调函数。所以就会打印该构造函数里面的数据。
打印以下图所示:

2. 监听characterData的变更

以下测试代码:

<!DOCTYPE html>
  <html>
    <head>
      <title>MutationObserver</title>
      <meta charset="utf-8">
    </head>
    <body>
      <div id="app">
        <ul>
          <li>kongzhi111</li>
        </ul>
      </div>
      <script type="text/javascript">
        var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;
        var list = document.querySelector('ul');
        var Observer = new MutationObserver(function(mutations, instance) {
          mutations.forEach(function(mutation){
            console.log(mutation);
          });
        });
        Observer.observe(list, {
          childList: true, // 子节点的变更
          characterData: true, // 节点内容或节点文本变更
          subtree: true // 全部后代节点的变更
        });
        // 改变节点中的子节点中的数据
        list.childNodes[0].data = "kongzhi222";
      </script>
    </body>
  </html>

打印以下效果:

3. 监听属性的变更

<!DOCTYPE html>
<html>
<head>
  <title>MutationObserver</title>
  <meta charset="utf-8">
</head>
<body>
  <div id="app">
    <ul>
      <li>kongzhi111</li>
    </ul>
  </div>
  <script type="text/javascript">
    var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;
    var list = document.querySelector('ul');
    var Observer = new MutationObserver(function(mutations, instance) {
      mutations.forEach(function(mutation){
        console.log(mutation);
      });
    });
    Observer.observe(list, {
      attributes: true
    });
    // 设置节点的属性,会触发回调函数
    list.setAttribute('data-value', 'tugenhua111');

    // 从新设置属性,会触发回调函数
    list.setAttribute('data-value', 'tugenhua222');

    // 删除属性,也会触发回调函数
    list.removeAttribute('data-value');
  </script>
</body>
</html>

如上就是MutationObserver的基本使用,它能监听 子节点的变更、属性的变更、节点内容或节点文本的变更 及 全部后代节点的变更。 下面咱们来看下咱们的 nextTick.js 中的源码是如何实现的。

六:nextTick源码分析

vue源码在 vue/src/core/util/next-tick.js 中。源码以下:
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = []
let pending = false
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
let timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    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)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

如上代码,咱们从上往下看,首先定义变量 callbacks = []; 该变量的做用是: 用来存储全部须要执行的回调函数。let pending = false; 该变量的做用是表示状态,判断是否有正在执行的回调函数。
也能够理解为,若是代码中 timerFunc 函数被推送到任务队列中去则不须要重复推送。

flushCallbacks() 函数,该函数的做用是用来执行callbacks里面存储的全部回调函数。以下代码:

function flushCallbacks () {
  /*
   设置 pending 为 false, 说明该 函数已经被推入到任务队列或主线程中。须要等待当前
   栈执行完毕后再执行。
  */
  pending = false;
  // 拷贝一个callbacks函数数组的副本
  const copies = callbacks.slice(0)
  // 把函数数组清空
  callbacks.length = 0
  // 循环该函数数组,依次执行。
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

timerFunc: 保存须要被执行的函数。

继续看接下来的代码,咱们上面讲解过,在Vue中使用了几种状况来延迟调用该函数。

1. promise.then 延迟调用, 基本代码以下: 

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
}

如上代码的含义是: 若是咱们的设备(或叫浏览器)支持Promise, 那么咱们就使用 Promise.then的方式来延迟函数的调用。Promise.then会将函数延迟到调用栈的最末端,从而会作到延迟。

2. MutationObserver 监听, 基本代码以下:

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)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
}

如上代码,首先也是判断咱们的设备是否支持 MutationObserver 对象, 若是支持的话,咱们就会建立一个MutationObserver构造函数, 而且把flushCallbacks函数当作callback的回调, 而后咱们会建立一个文本节点, 以后会使用MutationObserver对象的observe来监听该文本节点, 若是文本节点的内容有任何变更的话,它就会触发 flushCallbacks 回调函数。那么要怎么样触发呢? 在该代码内有一个 timerFunc 函数, 若是咱们触发该函数, 会致使文本节点的数据发生改变,进而触发MutationObserver构造函数。

3. setImmediate 监听, 基本代码以下:

else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
}

若是上面的 Promise 和 MutationObserver 都不支持的话, 咱们继续会判断设备是否支持 setImmediate, 咱们上面分析过, 他属于 macrotasks(宏任务)的。该任务会在一个宏任务里执行回调队列。

4. 使用setTimeout 作降级处理

若是咱们上面三种状况, 设备都不支持的话, 咱们会使用 setTimeout 来作降级处理, 实现延迟效果。以下基本代码:

else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

如今咱们的源码继续往下看, 会看到咱们的nextTick函数被export了,以下基本代码:

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

如上代码, nextTick 函数接收2个参数,cb 是一个回调函数, ctx 是一个上下文。 首先会把它存入callbacks函数数组里面去, 在函数内部会判断cb是不是一个函数,若是是一个函数,就调用执行该函数,固然它会在callbacks函数数组遍历的时候才会被执行。其次 若是cb不是一个函数的话, 那么会判断是否有_resolve值, 有该值就使用Promise.then() 这样的方式来调用。好比: this.$nextTick().then(cb) 这样的使用方式。所以在下面的if语句内会判断赋值给_resolve:

if (!cb && typeof Promise !== 'undefined') {
  return new Promise(resolve => {
    _resolve = resolve
  })
}

使用Promise返回了一个 fulfilled 的Promise。赋值给 _resolve; 而后在callbacks.push 中会执行以下:

_resolve(ctx);

全局方法Vue.nextTick在 /src/core/global-api/index.js 中声明,是对函数nextTick的引用,因此使用时能够显式指定执行上下文。代码初始化以下:

Vue.nextTick = nextTick;

咱们可使用以下的一个简单的demo来简化上面的代码。以下demo:

<script type="text/javascript">
  var callbacks = [];
  var pending = false;
  function timerFunc() {
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (var i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }
  function nextTick(cb, ctx) {
    var _resolve;
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx)
        } catch (e) {
          handleError(e, ctx, 'nextTick')
        }
      } else if (_resolve) {
        _resolve(ctx)
      }
    });
    if (!pending) {
      pending = true
      timerFunc()
    }
    // $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(resolve => {
        _resolve = resolve
      })
    }
  }

  // 调用方式以下:
  nextTick(function() {
    console.log('打印出来了'); // 会被执行打印
  });
</script>

如上咱们已经知道了 nextTick 是Vue中的一个全局函数, 在Vue里面会有一个Watcher, 它用于观察数据的变化, 而后更新DOM, 可是在Vue中并非每次数据改变都会触发更新DOM的, 而是将这些操做都缓存到一个队列中, 在一个事件循环结束后, 会刷新队列, 会统一执行DOM的更新操做。

在Vue中使用的是Object.defineProperty来监听每一个对象属性数据变化的, 当监听到数据发生变化的时候, 咱们须要把该消息通知到全部的订阅者, 也就是Dep, 那么Dep则会调用它管理的全部的Watch对象,所以会调用Watch对象中的update方法, 咱们能够看下源码中的update的实现。源码在 vue/src/core/observer/watcher.js 中以下代码:

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    // 同步执行渲染视图
    this.run()
  } else {
    // 异步推送到观察者队列中
    queueWatcher(this)
  }
}

如上代码咱们能够看到, 在Vue中它默认是使用异步执行DOM更新的。当异步执行update的时候,它默认会调用 queueWatcher 函数。

咱们下面再来看下该 queueWatcher 函数代码以下: (源码在: vue/src/core/observer/scheduler.js) 中。

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

如上源码, 咱们从第一句代码执行过来, 首先获取该 id = watcher.id; 而后判断该id是否存在 if (has[id] == null) {} , 若是已经存在则直接跳过,不存在则执行if
语句内部代码, 而且标记哈希表has[id] = true; 用于下次检验。若是 flushing 为false的话, 则把该watcher对象push到队列中, 考虑到一些状况, 好比正在更新队列中
的watcher时, 又有事件塞入进来怎么处理? 所以这边加了一个flushing来表示队列的更新状态。

若是加入队列到更新状态时,又分为两种状况:

1. 这个watcher尚未处理, 就找到这个watcher在队列中的位置, 而且把新的放在后面, 好比以下代码:

if (!flushing) {
  queue.push(watcher)
}

2. 若是watcher已经更新过了, 就把这个watcher再放到当前执行的下一位, 当前的watcher处理完成后, 当即会处理这个最新的。以下代码:

else {
  // if already flushing, splice the watcher based on its id
  // if already past its id, it will be run next immediately.
  let i = queue.length - 1
  while (i > index && queue[i].id > watcher.id) {
    i--
  }
  queue.splice(i + 1, 0, watcher)
}

接着以下代码:

if (!waiting) {
  waiting = true

  if (process.env.NODE_ENV !== 'production' && !config.async) {
    flushSchedulerQueue()
    return
  }
  nextTick(flushSchedulerQueue)
}

waiting 为false, 等待下一个tick时, 会执行刷新队列。 若是不是正式环境的话, 会直接 调用该函数 flushSchedulerQueue; (源码在: vue/src/core/observer/scheduler.js) 中。不然的话,  把该函数放入 nextTick 函数延迟处理。

相关文章
相关标签/搜索