Vue源码系列——$nexttick

737 阅读1分钟

使用场景

我们在修改一个数据后,立刻获取绑定该数据的dom,会发现dom上的数据并没有变。 而放在this.$nextTick中就可以打印出变化之后的数据。

<div ref="name">{{name}}</div>

data() {
    return {
        activeName: [],
        name: 'mashaodong'
    };
},
mounted() {
    this.name = '222222';
    console.log(this.$refs.name.innerHTML) // mashaodong
    this.$nextTick(() => {
       console.log(this.$refs.name.innerHTML) // 222222
    })
}

看源码前戏必备

要理解dom修改和页面渲染是两件事:

  • dom修改是微任务,所以改完数据立刻获取dom,获取的是之前的数据。
  • 页面渲染是微任务执行完和下一次宏任务执行前。

Promise

Promise是微任务,修改数据在Promise之前执行,所以微任务列表中dom改变先执行,Promise后执行。就能获取到dom改变后的数据。

MutationObserver

MutationObserver用于对一个dom进行监听,设置一个回调,dom改变后就回执行回调。关于它的使用很巧妙,文章最后我们分析一下。

setImmediate

目前只有最新版本的 Internet Explorer 和Node.js 0.10+实现了该方法。我们可以先不管它。

setTimeout

下一次的宏任务执行,在渲染之后,肯定也就在dom更新之后。是一个向后兼容的方案。

👀源码去喽

nextTick

  1. 下面重点部分:把函数执行放在一个数组callbacks内,然后看callbacks在哪执行的。目的是连续执行$nextTick时,进行整合,优化程序。
Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
};
function nextTick (cb, ctx) {
  var _resolve;
  callbacks.push(function () {
    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(function (resolve) {
      _resolve = resolve;
    })
  }
}

callbacks在哪执行

  1. flushCallback函数把callbacks的方法全执行一边。然后看flushCallback什么时候执行
const callbacks = [];
var pending = false;
function flushCallbacks () {
  pending = false;
  var copies = callbacks.slice(0);
  callbacks.length = 0;
  for (var i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

flushCallbacks在哪执行

四个地方分别用了执行了它,把flushCallback放在了timerFunc中执行,四种方法了做兼容,顺序是:

  1. Promise
  2. MutationObserver
  3. setImmediate
  4. setTimeout
var timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  var p = Promise.resolve();
  timerFunc = function () {
    // 标记1
    p.then(flushCallbacks);
    if (isIOS) { setTimeout(noop); }
  };
  isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 标记2
  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 () {
    // 标记3
    setImmediate(flushCallbacks);
  };
} else {
  timerFunc = function () {
    // 标记4
    setTimeout(flushCallbacks, 0);
  };
}

整体分析(重点)执行顺序

  1. 标题:callbacks在哪执行:声明callbacks数组,声明flushCallbacks遍历执行callbacks中的函数。声明一个pending,保证下面的timerFunc只执行一次(因为是异步,所以会滞后执行)。
  2. 标题:flushCallbacks在哪执行:判断环境,决定flushCallbacks采用哪种方式执行,声明timerFunc函数,把flushCallbacks()放在里面,等待执行,timerFunc的执行环境都是异步的
  3. 标题:nextTick:两件事 a:每次执行把fn放在callbacks中; b:利用pedding保证timerFunc只执行一次;

MutationObserver的使用

MutationObserver并没有监听改变的dom,而是利用每次$nextTick都会执行一次timerFunc,声明一个文本元素textNode,监听textNode,timerFunc执行改变文本元素,从而执行flushCallbacks

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);
};