vue2和vue3中nextTick的底层原理详解

9 篇文章 1 订阅


前言

如果你理解nextTick和MutationObserver的基本使用,为节约时间请直接跳到第三步浏览”vue中nextTick的底层原理详解“即可。


一、nextTick是什么?

1:由来

尽管vue所使用的MVVM框架并不推荐访问DOM,但实际开发中免不了要进行DOM操作。而nextTick就提供了一个桥梁,确保我们操作的是更新后的DOM。

2:语法

this.nextTick(()=>{})

3:使用

import { nextTick } from 'vue'
export default {
  data() {
    return {
      message: '张三'
    }
  },
  methods: {
    async increment() {
      // DOM 还未更新
      this.message = '李四'
      // DOM 此时已经更新
      await  nextTick(()=>{
        // 更新后要进行的操作
      )}
  }

4:作用

等待下一次 DOM 更新刷新的工具方法。(在数据更新后调用里面的回调函数)

二、MutationObserver

1:由来

MutationObserver是HTML5新增的属性。

2:作用

MutationObserver用于监听DOM修改事件,能够监听到节点的属性、文本内容、子节点等的改动。

3:使用

官网示例如下

// Select the node that will be observed for mutations
const targetNode = document.getElementById("some-id");

// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };

// Callback function to execute when mutations are observed
const callback = (mutationList, observer) => {
  for (const mutation of mutationList) {
    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.`);
    }
  }
};

// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);

// Start observing the target node for configured mutations
observer.observe(targetNode, config);

// Later, you can stop observing
observer.disconnect();

示例引用链接:https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver


三、vue中nextTick的底层原理详解

1:简单概括事件循环

这里简单概括一下,详细的可以看事件循环详解(event loop)

JavaScript是单线程的,执行异步(如setTimeout,axios请求等)会造成事件堵塞,为了解决类似问题,事件循环应运而生。它会在一个队列中执行诸如点击按钮、发送请求等操作的时候,添加对应的事件,结束后就会移除这个事件,这就是一个简单的事件循环。每次结束事件后,都会重新渲染dom。
在这里插入图片描述

2:思考

for(let i=0; i<100; i++){
  dom.style.left = i + 'px';
}

执行上述代码浏览器会进行100次DOM更新吗?
显然不会的,这样太耗性能了,所以这个事件只会执行一次重新渲染dom。

3:微任务(microtask)

事件循环中的 微任务(microtask),在这里,你只需要知道任务队列中它的优先级最高就好了,即在消息队列中中途加入微任务,这个微任务不会等到下一个消息队列中执行,而是执行完这个微任务后才会执行下一个消息队列。

4:vue中nextTick源码

到了这一步,你可能有所猜测,使用MutationObserver来监听所有的数据,然后只要让nextTick里的代码放在页面渲染步骤后面执行,就能访问到更新后的DOM了。更新DOM的那个 微任务(microtask)后追加了我们自己的回调函数,从而确保我们的代码在DOM更新后执行,同时也避免了setTimeout可能存在的多次执行问题。

(1)vue@2.2.5关于nextTick的源码
//node_modules\vue\src\core\util\env.js
if (typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')) {
  var counter = 1
  var observer = new MutationObserver(nextTickHandler)
  var textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
      characterData: true
  })

  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
}
(2)vue@2.6.14关于nextTick的源码
// node_modules\vue\src\core\util\next-tick.ts
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e: any) {
        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
    })
  }
}
(3)vue@3.2.13关于nextTick的源码
// node_modules\vue\dist\vue.esm-browser.js
const resolvedPromise = /*#__PURE__*/ Promise.resolve();
let currentFlushPromise = null;
const RECURSION_LIMIT = 100;
function nextTick(fn) {
    const p = currentFlushPromise || resolvedPromise;
    return fn ? p.then(this ? fn.bind(this) : fn) : p;
}

vue3中的nextTick就更为简单了,如果p变量是resolvedPromise,那么只会立即执行then回调并加入到微任务队列中。

(4)vue2和vue3 nextTick的区别

Vue2的nextTick是维护了一个callbacks数组,每次更新过程中只插入一个 微任务(microtask),执行放在callbacks数组中的回调。
Vue3的nextTick和Promise基本没什么区别,set过程的更新看似也不再依赖nextTick进行。仅仅只是将创建一个resovled状态的Promise,将传入的函数放入回调中罢了。

5:vue2中的补充(vue的降级策略)

上面我们讲到了,队列控制的最佳选择是微任务(microtask),而微任务(microtask)的最佳选择是Promise.但如果当前环境不支持Promise,vue就不得不降级为宏任务(macrotask)来做队列控制了。

在vue2.5的源码中,宏任务(macrotask)降级的方案依次是:setImmediate、MessageChannel、setTimeout.
setImmediate是最理想的方案了,可惜的是只有IE和nodejs支持。
MessageChannel的onmessage回调也是微任务(microtask),但是个新API,面临兼容性的尴尬。
所以最后的兜底方案就是setTimeout了,尽管它有因执行的最小时间间隔是约4ms的延迟,可能造成多次渲染,算是没有办法的办法了。

总结

vue用异步队列的方式来控制DOM更新和nextTick回调先后执行(dom更新先执行,nextTick后执行)。
微任务(microtask)因为其最高优先级的特性,能确保队列中的微任务在下一次事件循环前被执行完毕。
因为兼容性问题,vue@2.5及之前版本不得不做了微任务(microtask)向 宏任务(macrotask)的降级方案。vue@2.5之后版本,你可以理解为promise的使用即可。

参考文章
https://juejin.cn/post/6844903590293684231
vue2.5的nextTick更改记录:https://github.com/vuejs/vue/commit/6e41679a96582da3e0a60bdbf123c33ba0e86b31

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值