理解Vue.nextTick使用及源码分析

理解Vue.nextTick使用及源码分析

  • 什么是Vue.nextTick()?
  • Vue.nextTick()方法的应用场景有哪些?
    • 更改数据后,进行节点DOM操作。
    • 在created生命周期中进行DOM操作
  • Vue.nextTick的调用方式如下
  • vm.$nextTick 与 setTimeout 的区别是什么?
  • 理解Event Loop 的概念
  • 理解 MutationObserver
    • MutationObserver是什么?
    • MutationObserver 构造函数
      • MutationObserver 实列的方法
        • 监听childList的变动
        • 监听characterData的变动
        • 监听属性的变动
  • nextTick源码分析

什么是Vue.nextTick()?

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

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

Vue.nextTick()方法的应用场景有哪些?

更改数据后,进行节点DOM操作。


<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';
          // 此时data数据变了,但是还没有更新dom节点
          console.log(this.$refs.list.textContent); // 打印 kongzhi111
          this.$nextTick(() => {
      
            // 数据改变,dom节点更新后,才会触发
            console.log(this.$refs.list.textContent); // 打印 kongzhi222
          });
        }
      }
    })
  script>
body>
html>

理解DOM更新:在VUE中,当我们修改了data中的某一个值后,并不会立刻去渲染html页面,而是将vue更改的数据放到watcher的一个异步队列中,只有在当前任务空闲时才会执行watcher中的队列任务

在created生命周期中进行DOM操作

在Vue生命周期中,created触发时,实例已完成以下的配置:数据观测 (data observer)property 和方法的运算watch/event 事件回调。然而,挂载阶段还没开始,$el property目前尚不可用(通过$refs获取dom元素也获取不到).


<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); // 打印出 "
kongzhi111
"
}); }, methods: { } })
script> body> html>

Vue.nextTick的调用方式如下

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

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

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

在区别他们俩之前,我们先来看一个简单的demo如下:


<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); // 打印出 "
kongzhi111
"
}, 0); } })
script> body> html>

如上代码,我们不使用 nextTick, 我们使用setTimeout延迟也一样可以通过$refs获取页面中的HTML元素的,那么他们俩之间到底有什么区别呢?

vue源码中nextTicksrc/core/util/next-tick.js。在vue中使用了三种情况来延迟调用该函数

  • 首先我们会判断我们的设备是否支持Promise对象,如果支持的话,会使用 Promise.then来做延迟调用函数。
  • 如果设备不支持Promise对象,再判断是否支持 MutationObserver对象,如果支持该对象,就使用MutationObserver来做延迟,
  • 最后如果上面两种都不支持的话,我们会使用setTimeout(() => {}, 0); setTimeout 来做延迟操作。

比较 nextTick 与 setTimeout 的区别,实际上是比较 promise 或 MutationObserver 对象 与 setTimeout的区别的了

在比较promise与setTimeout之前,我们先来看如下demo。


<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.thensetTimeout都是异步的,那么在事件队列中Promise.then的事件应该是在setTimeout的后面的,那么为什么Promise.then比setTimeout函数先执行呢?

理解Event Loop 的概念

这一次,彻底弄懂 JavaScript 执行机制(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(微任务)队列为空为止。

有了上面 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(微任务)包括 PromiseMutationObserver, 因此 我们可以知道在Vue中的nextTick 的执行速度上快于setTimeout

我们从如下demo也可以得到验证:


<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); // 打印出 "
kongzhi111
"
}, 0); this.$nextTick(function(){ console.log('nextTick比setTimeout先执行'); }); } })
script> body> html>

如上代码,先打印的是 undefiend, 其次是打印 “nextTick比setTimeout先执行” 信息, 最后打印出 “

kongzhi111” 信息。

理解 MutationObserver

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

MutationObserver是什么?

MutationObserver–MDN
MutationObserver 中文含义可以理解为 “变动观察器”。它是监听DOM变动的接口,DOM发生任何变动,MutationObserver会得到通知。在Vue中是通过该属性来监听DOM更新完毕的。

它和事件类似,但有所不同,事件是同步的,当DOM发生变动时,事件会立刻处理,但是MutationObserver则是异步(微任务)的,它不会立即处理,而是等页面上所有的DOM完成后,会执行一次,如果页面上要操作100次DOM的话,如果是事件的话会监听100次DOM,但是我们的 MutationObserver 只会执行一次,它是等待所有的DOM操作完成后,再执行。

它的特点是:

  • 等待所有脚本任务完成后,才会执行,即采用异步方式。
  • DOM的变动记录会封装成一个数组进行处理。
  • 还可以观测发生在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 中的一种或多种。

监听childList的变动


<html>
<head>
  <title>MutationObservertitle>
  <meta charset="utf-8">
head>
<body>
  <div id="app">
    <ul>
      <li>kongzhi111li>
    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 构造函数的回调函数。因此就会打印该构造函数里面的数据
打印如下图所示:
理解Vue.nextTick使用及源码分析_第1张图片

监听characterData的变动


  <html>
    <head>
      <title>MutationObservertitle>
      <meta charset="utf-8">
    head>
    <body>
      <div id="app">
        <ul>
          <li>kongzhi111li>
        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>

打印如下
理解Vue.nextTick使用及源码分析_第2张图片

监听属性的变动


<html>
<head>
  <title>MutationObservertitle>
  <meta charset="utf-8">
head>
<body>
  <div id="app">
    <ul>
      <li>kongzhi111li>
    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
    })
  }
}

Vue你不得不知道的异步更新机制和nextTick原理
Vue系列—理解Vue.nextTick使用及源码分析(五)

你可能感兴趣的:(Vue,vue,nextTick)