【Vue】$nextTick 应用及源码解析

Vue $nextTick

1.前言

在实际开发的很多业务场景中,我们都需要用到$nextTick,简单理解的话,它就是一个setTimeOut函数,可以将函数放到异步后去处理。但实际上,它的底层不仅仅是setTimeout那么简单,让我们深入了解一下

2.$nextTick想要解决的问题

看这样一个需求:根据文字的行数来显示一个展开更多的按钮,因此,我们在程序中为文字数据赋值后,需要拿到文字的高度。例如下面这段代码:

<div id="app">
	<div class="msg">
        {{ msg }}
    </div>
</div>

<script>
export default {
    data(){
        return {
            msg: '',
        }
    },
    mounted(){
        this.msg = '测试!'
        console.log(document.quertSelector('.msg').offsetHeight)
    }
}
</script>

但此时会发现,无论怎么执行,打印出来的文字div高度均为0。但是从控制台中直接获取时,却可以拿到值:
在这里插入图片描述
类似的情况还有:父子组件通过props传参、获取v-if控制的DOM元素中的内容等。

这些问题实际上都是由于DOM元素尚未更新导致的,而这也正是$nextTick要解决的问题。

2.问题原因:DOM的异步更新

对于上面的问题,都是在更新数据后立即查看数据导致的,而查看数据是一个同步操作,但其都是在赋值之后,按道理讲不会出现无法获取的问题。除非给数据赋值的操作中出现了异步操作,没有马上执行,导致我们读取数据时,无法取到。对此,Vue官网是这样描述的:

可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。

只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。

然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0)代替。

即,在上面的例子中,我们在设置了this.msg = ‘xxx’之后,Vue并没有立即拿这个数据去更新DOM,而是将这个操作放入一个缓冲队列中。同时,如果我们在一段时间内,多次更新这个数据,更新操作也只会被push到队列中一次(去重)。等待同一事件循环中的所有数据变化完成后,才会将这个队列中的事件拿出来执行。

那么为什么要这样做?主要是为了提升性能,因为如果我们在主线程中更新DOM,那么每循环一次就要更新一次DOM,循环100次就要更新100次DOM,这样会大大降低性能。但是如果等待事件循环完成之后再进行更新,则一段时间内只需要更新一次DOM!

而对于上面的问题,即在数据更新之后去操作DOM时,要拿到最新的DOM元素,就可以使用$nextTick(callback),保证其中的回调函数在DOM更新完成后被调用,从而拿到最新的DOM元素。

因此对于上面的例子,我们可以修改代码如下:

mounted(){
    this.msg = '测试!'
    this.$nextTick(() => {
    	console.log(document.quertSelector('.msg').offsetHeight)
    })
}

3.Vue3中nextTick的使用

在Vue3的 setup 中,不再有this,所以不能像Vue2中那样使用nextTick,而是要用下面的形式:

mounted(() => {
    this.msg = '测试!'
    nextTick(() => {
    	console.log(document.quertSelector('.msg').offsetHeight)
    })
})

4.nextTick源码分析

Vue把nextTick的源码单独抽到了文件/src/core/util/next-tick.js中,让我们逐段来分析一下。

首先是nextTick函数的定义:
在这里插入图片描述
其首先在外层定义了三个变量:

  • callbacks - 一个数组,实际上就是我们上面提到的缓冲队列
  • pending - Boolean,用于标识同一时间只能执行一次
  • timerFunc - 一个函数

可以看到,我们每一次调用nextTick()的过程其实就是在向callbacks新增回调函数的过程!

而当pendingfalse时,会转而去执行timerFunc函数。

其定义如下:
在这里插入图片描述
这里出现了好几个isNative函数,这是用来判断所传参数是否在当前环境原生就支持;例如某些浏览器不支持Promise,虽然我们使用了垫片(polify),但是isNative(Promise)还是会返回false

可以看出这边代码其实是做了四个判断,对当前环境进行不断的降级处理,尝试使用原生的Promise.then、MutationObserversetImmediate,上述三个都不支持的话则使用setTimeout

而降级处理的最终目前是将flushCallbacks函数放入微任务(判断1和判断2)或者宏任务(判断3和判断4),从而等待下一次事件循环时执行。

那么这个flushCallbacks是什么呢?
在这里插入图片描述
它所做的事非常简单,实际上就是把原本的callbacks数组复制一份,然后将其置空,最后,将复制出来的数组中的每一个函数依次执行一遍。所以,总的来说,就是用来执行callbacks中的回调函数。

总结

整体nextTick的代码都分析完毕了,总结一下它的流程就是:

  1. 把回调函数放入callbacks等待执行
  2. 通过pending判断是不是已经执行过;
  3. 将执行函数放到微任务或者宏任务
  4. 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调

再回到我们开头说的setTimeout,可以看出来nextTick是进行了多种兼容性的处理,宽泛的也可以理解为将回调函数放入setTimeout中执行;不过nextTick是优先采用原生的Promise.thenMutationObserver(微任务)和setImmediate(宏任务),如果都不支持,则选择采用setTimeout(宏任务)。

文章参考:面试题:Vue中$nextTick原理

补充:MutationObserver

MutationObserverHTML5新增的属性,用于监听DOM修改事件,能够监听到节点的属性、文本内容、子节点等的改动,是一个功能强大的利器,基本用法如下:

//MO基本用法
var observer = new MutationObserver(function(){
  //这里是回调函数
  console.log('DOM被修改了!');
});
var article = document.querySelector('article');
observer.observer(article);

MutationObserver属于微任务(microtask)

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue.js的$nextTick函数是用于在DOM更新之后执行异步操作的方法。该方法的实现使用了微任务队列,即将异步操作推入到微任务队列中,在DOM更新后执行异步操作。$nextTick方法是Vue.js响应式系统的重要部分,它确保了Vue.js组件的异步行为和数据响应式。 具体的实现过程如下: 1. 首先检查是否支持原生的Promise对象,如果支持,则直接返回Promise.resolve()。 2. 如果不支持原生的Promise对象,则创建一个新的Promise对象。 3. 将一个空函数推入微任务队列中。 4. 在新创建的Promise对象的resolve回调中,再次推入一个空函数到微任务队列中。 5. 当浏览器执行到微任务队列中的空函数时,DOM更新已经完成,可以执行异步操作了。 下面是$nextTick方法的源代码: ```javascript Vue.prototype.$nextTick = function(fn: Function) { return nextTick(fn, this) } ``` 其中,nextTick是一个工具函数,它实现了具体的异步操作逻辑: ```javascript 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 if (useMacroTask) { macroTimerFunc() } else { microTimerFunc() } } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } } ``` 该函数定义了一个callbacks数组用于存储异步操作,当调用nextTick函数时,将回调函数push进callbacks数组中,然后判断是否有待执行的异步操作,如果没有,则通过macroTimerFunc或microTimerFunc函数执行异步操作。 最后,如果调用$nextTick方法时没有传入回调函数,则会返回一个新的Promise对象,用于异步操作的等待和处理。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值