关于nextTick及其它的原理

上一篇有提到事件循环,及完整的事件循环(Event loop)过程解析,趁热打铁,理解一下vue中的nextTick()。

function nextTick(callback?: () => void): Promise<void>

官方文档中的解释是:“在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用nextTick(),获取更新后的 DOM。”

简单来说,nextTick(),是将回调函数延迟执行,在下一次DOM更新数据后调用,当数据更新并在DOM中渲染后,自动执行该函数。

看下官方示例:

<script>
import { nextTick } from 'vue'

export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    async increment() {
      this.count++

      // DOM 还未更新
      console.log(document.getElementById('counter').textContent) // 0

      await nextTick()
      // DOM 此时已经更新
      console.log(document.getElementById('counter').textContent) // 1
    }
  }
}
</script>

<template>
  <button id="counter" @click="increment">{
   
   { count }}</button>
</template>

nextTick源码理解

简单理解一下之后,我们看下源码吧,在src/core/instance/render.js中 将nextTick定义到vue原型链上,这里的 this是指当前组件的this。

Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
}

在src/core/util/next-tick.js中,通过export暴露出了 nextTick 函数。

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接收两个参数,第一个是函数cb(即 我们要延迟执行的函数),第二个是this上下文。

在函数体内,callbacks是一个数组,用来存储所有需要执行的回调函数。判断cb存在,就把cb存到callbacks数组中,同时把cb的上下文指向组件的this。cb不存在,就把_resolve函数放到callbacks数组中。

然后判断pending的值,pending用来控制状态,判断是否有正在执行的回调函数。当pending为false,表示没有回调函数在执行;此时将pending设为true,然后执行timerFunc函数;

判断当cb不存在并且浏览器支持Promise时,返回一个Promise。也就是说当没有回调函数时,可以通过this.$nextTick().then(cb)的方式进行调用。

理解下来,这里的 timerFunc 应该是 nextTick 实现的关键函数了,我们看下 timerFunc 的源码:

let timerFunc

// nextTick行为利用了微任务队列,可以通过本机Promise访问该队列。
// 使MutationObserver有更广泛的支持,但它被严重窃听,
// 当在触摸事件中触发时,iOS中的UIWebView>=9.3.3。触发几次后完全停止工作…

// tips:源码中是一大段英文,方便理解,我直接翻译成中文,很明显,这里是交代了一下使用场景;

/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // 在有问题的UIWebViews中,Promise.then()不会完全断裂,但是它可能会陷入一种奇怪的状态, 
    // 回调被推入微任务队列,但队列不会被刷新,直到浏览器需要做一些其他工作,例如处理计时器。
    // 因此,我们可以通过添加空计时器“强制”刷新微任务队列
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 在原生Promise不可用的情况下使用MutationObserver,
  // 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)) {
  // 回退到setImmediate.
  // 技术上,它利用(宏)任务队列,但它仍然是比setTimeout更好的选择
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 回退到setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

简单理解一下:if-else-if是对nextTicket 做兼容处理:判断系统是否支持promise,支持,用promise做延时处理;不支持,判断是否支持MutationObserver,setImmediate 和 setTimeout。

区别在于:promise 和 MutationObserver是微任务,setImmediate和setTimeout是宏任务,执行的顺序有差别,微任务的执行顺序早于宏任务;

我们看到,以上所有的延时中都执行了flushCallbacks函数,那我们看看flushCallbacks的源码:

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]()
  }
}

执行flushCallbacks(),会把pending设为false,把callbacks数组复制一份到copies数组中,并将callbacks数组清空,然后对copies数组循环,并以此执行数组中每一项( 即执行在nextTick() 时存到callbacks数组中的cb())。

MutationObserver

我们看到,当不支持promise时,会用MutationObserver,字面意思来看是“变动观察器”,那我们来看看这是个啥吧。

MDN上的解释:MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。

简单理解为:MutationObserver 监听DOM变动,DOM 发生任何变动,MutationObserver 就会收到通知,所以vue可以用MutationObserver 来监听DOM 是否更新完毕。

引用MDN上的示例,来看一下MutationObserver的使用:

 // 选择需要观察变动的节点
const targetNode = document.getElementById('some-id');

// 观察器的配置(需要观察什么变动)
// attributes: 属性的变动。
// childList: 子节点的变动。
// characterData: 节点内容或节点文本的变动。
// subtree: 所有后代节点的变动。
const config = { attributes: true, childList: true, characterData: true, subtree: true };

// 当观察到变动时执行的回调函数
const callback = function(mutationsList, observer) {
    // Use traditional 'for loops' for IE 11
    for(let mutation of mutationsList) {
        if (mutation.type === 'childList') {
            console.log('A child node has been added or removed.');
        }
        else if (mutation.type === 'attributes') {
            console.log('The ' + mutation.attributeName + ' attribute was modified.');
        }
    }
};

// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);

// 以上述配置开始观察目标节点
observer.observe(targetNode, config);

// 之后,可停止观察
observer.disconnect();

observe()用来观察DOM节点变化,通过其回调函数接收通知。接收2个参数,第一个参数是要观察的DOM元素,第二个是要观察的变动类型。调用方式为observer.observe(dom, options)

我们看一个简单的例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="http://cdn.bootcss.com/jquery/3.1.0/jquery.js"></script>
</head>
<body>
    <div id='target'>
        <p></p>
    </div>
</body>
<script>
    let targetNode = document.getElementById('target');
    let pNode = targetNode.getElementsByTagName('p')[0];
    let config = { attributes: true, childList: true, subtree: true }
    let i = 0
    let observe = new MutationObserver((mutations, observe) => {
        i++
        console.log('pNode', pNode, i)
    });
    observe.observe(target, config);
    pNode.appendChild(document.createTextNode('哈哈哈'));
    pNode.appendChild(document.createTextNode('1'));
    pNode.appendChild(document.createTextNode('2'));
    pNode.appendChild(document.createTextNode('3'));
    pNode.setAttribute('class', 'test')
</script>
</html>

执行结果如下:

我们可以看到,我们总共加了四个文本节点到dom上,控制台上只打印了一次,也就是说MutationObserver只执行了一次,也就是说MutationObserver是等页面上所有dom完成后,再执行;

这样的话,我们就能理解nextTicket中MutationObserver的用法了,即通过对文本节点的操作来触发MutationObserver从而使flushCallbacks执行;

总结一下:

1. nextTick是Vue提供的⼀个全局API,由于vue的异步更新策略导致我们对数据的修改不会⽴刻体现在dom 变化上,如果想要⽴即获取更新后的dom状态,就需要使⽤这个⽅法;

2. Vue 在更新 DOM 时是 异步 执⾏的。只要侦听到数据变化, Vue 将开启⼀个队列,并缓存同⼀事件循环中发⽣的所有数据变更。如果同⼀个 watcher 被多次触发,只会被推⼊到队列中⼀次。在缓存时去除重复数据对于避免不必要的计算和 DOM 操作是⾮常重要的。 nextTick ⽅法会在队列 中加⼊⼀个回调函数,确保该函数在前⾯的dom 操作完成后才调⽤。
3. 当我们想在修改数据后⽴即看到 dom 执⾏结果就需要⽤到 nextTick ⽅法, 传⼀个回调函数进去,在⾥⾯执⾏ dom 操作即可。
4. 通过源码我们了解了 nextTick的 实现,它会在 callbacks ⾥⾯加⼊我们传⼊的回调函数cb,然后⽤ timerFunc异步调⽤它们,⾸选的异步⽅式会是Promise,所以我们可以在 nextTick 中看到 dom
作结果。

猜你喜欢

转载自blog.csdn.net/srj15110129498/article/details/127794512