Vue中的nextTick作用原理

1.Vue的异步更新策略

Vue采用了一种称为异步更新策略的机制。这意味着当数据发生变化时,Vue不会立即更新DOM,而是将更新任务放入一个队列中,等待下一个事件循环迭代时再进行更新。也就是说:当数据发生变化,Vue会开启异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新。

下面看一个例子:

<div id="root">{{message}}</div>
    <script type="text/javascript">
      const vm = new Vue({
        el: "#root",
        data() {
          return {
            message: "修改前的值",
          };
        },
        mounted() {
          this.message = "修改后的值";
          let root = document.querySelector("#root");
          console.log(root.textContent); //这时候打印的还是“修改前的值”
        },
        methods: {},
      });
    </script>

如上,页面一开始进去,在mounted钩子中修改root元素的内容,立马打印页面中root内部的值,这时候数据更新了但页面是没更新的。所以打印出来的还是修改前的值
在这里插入图片描述
界面上的修改后的值是在这轮时间事件循环结束(肯定在console.log(root.textContent)之后)才更新到视图的。

我们可以验证一下上述说法----------------视图肯定是在console.log(root.textContent)之后才完成,至于什么时候执行,那就和事件循环内部情况有关了。因为视图的更新必须等待事件循环迭代时再进行更新。

mounted() {
    this.message = "修改后的值";
    let root = document.querySelector("#root");
    console.log(root.textContent); //这时候打印的还是“修改前的值”
    debugger;
},

如上,添加了一个debugger,它可以在此处终止程序的运行。我们看看刚初始的界面:
在这里插入图片描述
这说明在进行debugger时候页面的视图还没有更新。

如果改为以下写法,在setTimeout(这一块和事件循环有关,后续我会讲到)中去进行debugger,结果就不一样了。

 mounted() {
    this.message = "修改后的值";
    let root = document.querySelector("#root");
    console.log(root.textContent); //这时候打印的还是“修改前的值”
    setInterval(() => {
       debugger;
    }, 0);
},

初始界面如下,会发现这时候页面的视图已经更新:
在这里插入图片描述

2.Vue为什么会有异步更新这种机制

如果是同步更新dom元素,每次数据一更新就要更新视图,而更新视图需要经过很多步骤,会有回流重绘操作,会造成性能的浪费。

3.nextTick

3.1 nextTick的作用

由于 Vue 的异步更新策略,当我们在数据发生变化后立即访问或修改DOM 元素时,可能会遇到数据已经改变但 DOM 尚未更新的情况。这时,我们可以使用nextTick方法来延迟执行后面代码,确保在 DOM 更新后执行。

nextTick方法接受一个回调函数作为参数,这个回调函数会在DOM 更新完成后被调用。这样,我们可以在回调函数中安全地访问和修改 DOM 元素。

3.2 nextTick的用法

还是以1.1为例,我们可以把代码改成下面这样子:

mounted() {
    this.message = "修改后的值";
    let root = document.querySelector("#root");
    console.log(root.textContent); //这时候打印的还是“修改前的值”
    this.$nextTick(() => {
       console.log(root.textContent); //这时候打印的是“修改后的值”
    });
},

这时候会发现nextTick内部执行的操作是建立在页面视图已经更新好的基础上了:
在这里插入图片描述

3.3 nextTick的原理

源码位置:/src/core/util/next-tick.js
如下callbacks表示异步操作队列。我们会往callbacks中新增回调函数,新增回调函数之后会执行timerFunc函数,而pending用于标识同一时间只能执行一次。

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

下面分析一下timerFunc函数。它是根据当前环境支持什么方法才决定调用哪个,分别有Promise.thenMutationObserversetImmediatesetTimeout。通过上面的任意一种方法,进行优雅降级操作。代码如下:

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

无论是微任务还是宏任务,都会放到flushCallbacks使用。这里将callbacks里面的函数复制一份,同时将callbacks置空,依次执行callbacks中的函数:

function flushCallbacks () {
 pending = false
 const copies = callbacks.slice(0)
 callbacks.length = 0
 for (let i = 0; i < copies.length; i++) {
    copies[i]()
 }
}

总结一下,大致过程就是

  • 将回调函数放入到callbacks等待执行
  • 将执行任务放到微任务和宏任务队列中
  • 当事件循环到了微任务和宏任务队列,执行函数一次执行callbacks中的回调
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

太阳与星辰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值