浅析$nextTick和$forceUpdate

vue中提供了数十种api供我们开发者日常使用,而常用的其实也就十多种,比如​​setup​​​, ​​mount​​​, ​forceupdate​​, ​​nextTick​​​, ​​compute​​​, ​​ref​​​等,这些参数有的是在生命周期中进行管理,有的是在页面执行过程中,更新参数,有些是可以用来检测页面数据,这些随着项目的推进或多或少都是会使用到。其中​​nextTick​​​和​​forceUpdate​​​都是用来更新参数的,那这两个参数有什么差异呢?这还真值得仔细琢磨。

浅析$nextTick和$forceUpdate

在开发过程中,经常出现的场景比如当你气势汹汹地使用Vue大展宏图的时候,突然发现,咦,我明明对这个数据进行更改了,但是当我获取它的时候怎么是上一次的值……
此时,Vue就会说:“小样,这你就不懂了吧,我的DOM是异步更新的呀!!!”
简单的说,Vue的响应式并不是只数据发生变化之后,DOM就立刻发生变化,而是按照一定的策略进行DOM的更新。这样的好处是可以避免一些对DOM不必要的操作,提高渲染性能。
在Vue官方文档中是这样说明的:

可能你还没有注意到,Vue异步执行DOM更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作上非常重要。然后,在下一个的事件循环“tick”中,Vue刷新队列并执行实际 (已去重的) 工作。

白话一点就是说,其实这是和JS当中的事件循环是息息相关的,就是Vue不可能对每一个数据变化都做一次渲染,它会把这些变化先放在一个异步的队列当中,同时它还会对这个队列里面的操作进行去重,比如你修改了这个数据三次,它只会保留最后一次。这些变化是都可以通过队列的形式保存起来,那现在的问题就来到了,那vue是在事件循环的哪个时机来对DOM进行修改呢?
Vue有两种选择,一个是在本次事件循环的最后进行一次DOM更新,另一种是把DOM更新放在下一轮的事件循环当中。这时,尤雨溪拍了拍胸脯说:“这两种方法,我都有!” 但是因为本轮事件循环最后执行会比放在下一轮事件循环要快很多,所以Vue优先选择第一种,只有当环境不支持的时候才触发第二种机制。
虽然性能上提高了很多,但这个时候问题就出现了,我们都知道在一轮事件循环中,同步执行栈中代码执行完成之后,才会执行异步队列当中的内容,那我们获取DOM的操作是一个同步的呀!!那岂不是虽然我已经把数据改掉了,但是它的更新异步的,而我在获取的时候,它还没有来得及改,所以会出现文章开头的那个问题。
这。。。我确实需要进行这样操作,那这么办呢??
没关系啦,尤大很贴心的为我们提供了​​Vue.$nextTick()​

$nextTick

官方解释:将回调延迟到下次 DOM 更新循环之后执行。

理解

首先要了解一下vue的​​异步更新队列​​,Vue 异步执行 DOM 更新。只要观察到数据变化,不会立即更新DOM,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个数据被的多次改变,只会被推入到队列中一次。例如,当你设置​​ vm.someData = 'new value'​​,对应的DOM更新会被推到一个队列里,该组件不会立即重新渲染,会在当前tick完毕后,在下一个tick中渲染DOM。在事件循环中,每进行一次循环操作称为tick。而​​nextTick​​函数就是vue提供的一个实例方法,数据更新后等待下一个tick里Dom更新完后执行回调,回调的 this 自动绑定到调用它的实例上。
既然提到上述两点,那么又好奇了,Vue是如何渲染DOM节点的?

Vue渲染DOM——浏览器渲染流程

浏览器接收到 HTML 文件并转换为 DOM 树,将 CSS 文件转换为 CSSOM

在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器需要递归​​CSSOM ​​树,然后确定具体的元素到底是什么样式。

生成渲染树

当我们生成​​DOM​​树和​​CSSOM​​树以后,就需要将这两棵树组合为渲染树。

在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是​​display: none​​的,那么就不会在渲染树中显示。
当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流),然后调用 GPU 绘制,合成图层,显示在屏幕上。对于这一部分的内容因为过于底层,还涉及到了硬件相关的知识,这里就不再继续展开内容了。

为什么操作 DOM 慢

想必大家都听过操作​​DOM​​性能很差,但是这其中的原因是什么呢?

因为​​DOM​​属于渲染引擎中的东西,而​​JS​​又是​​JS​​引擎中的东西。当我们通过​​JS​​操作​​DOM​​的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作​​DOM​​次数一多,也就等同于一直在进行线程之间的通信,并且操作​​DOM​
而且可能还会带来重绘回流的情况,所以也就导致了性能上的问题。

经典面试题:插入几万个 DOM,如何实现页面不卡顿?

首先我们肯定不能一次性把几万个​​DOM​​全部插入,这样肯定会造成卡顿,所以解决问题的重点应该是如何分批次部分渲染​​DOM​​。大部分人应该可以想到通过​​requestAnimationFrame​​的方式去循环的插入​​DOM​​,其实还有种方式去解决这个问题:虚拟滚动​​virtualized scroller​​。
这种技术的原理就是只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容。

什么情况阻塞渲染

首先渲染的前提是生成渲染树,所以​​HTML​​和​​CSS​​肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。

然后当浏览器在解析到​​script​​标签时,会暂停构建​​DOM​​,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载​​JS​​文件,这也是都建议将​​script​​标签放在​​body​​标签底部的原因。

当然在当下,并不是说​​script​​标签必须放在底部,因为你可以给​​script​​标签添加​​defer​​或者​​async​​属性。

当​​script​​标签加上​​defer​​属性以后,表示该​​JS​​文件会并行下载,但是会放到​​HTML​​解析完成后顺序执行,所以对于这种情况你可以把​​script​​标签放在任意位置。

对于没有任何依赖的​​JS​​文件可以加上​​async​​属性,表示​​JS​​文件下载和解析不会阻塞渲染。

重绘(Repaint)和回流(Reflow)

重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。

  • 重绘是当节点需要更改外观而不会影响布局的,比如改变​​color​​就叫称为重绘
  • 回流是布局或者几何属性需要改变就称为回流。

回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。

用法:

在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。

参数

{Function} [callback]

示例:

不妨比较一下使用nextTick和不使用的区别

<template>
<span ref="test">{{ egData }}</span>
<el-button @click="changeData">改变</el-button>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
setup() {
return {}
},
data() {
return {
egData: 'old Message'
}
},
methods: {
changeData() {
console.log(this.$refs.test, '1-----')
console.log('-----------')
this.$nextTick(function () {
console.log(this.$refs.test, '2-----')
})
this.egData = 'new Message'
}
}
})
</script>

<style scoped></style>

浅析$nextTick和$forceUpdate

总结

在上面提到很多理论知识,看看this.$nextTick具体是如何使用的?
​this.$nextTick​​这个方法作用是当数据被修改后使用这个方法会回调获取更新后的dom再渲染出来。

在Vue生命周期的​​created()​​钩子函数进行的​​DOM​​操作一定要放在​​Vue.nextTick()​​的回调函数中。原因是在​​created()​​钩子函数执行的时候​​DOM​​其实并未进行任何渲染,而此时进行​​DOM​​操作无异于徒劳,所以此处一定要将​​DOM​​操作的​​js​​代码放进​​Vue.nextTick()​​的回调函数中。与之对应的就是​​mounted​​钩子函数,因为该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题。

在数据变化后要执行的某个操作,比如案例中​​vm.egData = 'new message'​​,​​DOM​​并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的​​DOM​​更新。如果此时你想要根据更新的​​DOM​​状态去做某些事情,就会出现问题。。为了在数据变化之后等待​​Vue​​完成更新​​DOM​​,可以在数据变化之后立即使用​​Vue.nextTick(callback)​​。这样回调函数在​​DOM​​更新完成后就会调用。

​mounted​​不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用​​vm.$nextTick​​替换掉​​mounted​

$forceUpdate

归属于实例方法,迫使Vue实例重新(rander)渲染虚拟DOM,注意并不是重新加载组件。结合vue的生命周期,调用$forceUpdate后只会触发beforeUpdate和updated这两个钩子函数,不会触发其他的钩子函数。它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。对于forceUpdate的分析,不妨见这篇文章​​forceUpdate的解析​

用法:

当在data里没有显示的声明一个对象的属性,而是之后给该对象添加属性,这种情况vue是检测不到数据变化的,可以使用$forceUpdate()

<template>
<span ref="test">{{ egData }}</span>
<el-button @click="changeData">改变</el-button>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
setup() {
return {}
},
data() {
return {
egData: 'old Message'
}
},
methods: {
changeData() {
this.egData = 'new Message'
this.$forceUpdate()
}
}
})
</script>

<style scoped></style>

不过这种用法并不是很推荐,官方说如果你现在的场景需要用forceUpdate方法 ,那么99%是你的操作有问题,如上data里不显示声明对象的属性,之后添加属性时正确的做法时用 vm.$set() 方法,所以forceUpdate请慎用:)

参考资料

  1. https://vuejs.org/api/general.html#nexttick
  2. https://cn.vuejs.org/v2/api/
  3. https://vue3js.cn/vue-composition-api
  4. https://juejin.cn/post/6844903812872798216
  5. https://juejin.cn/post/7030991442520571917
  6. https://juejin.cn/post/7007328894621581349

文章版权声明

 1 原创文章作者:图大地,如若转载,请注明出处: https://www.52hwl.com/36108.html

 2 温馨提示:软件侵权请联系469472785#qq.com(三天内删除相关链接)资源失效请留言反馈

 3 下载提示:如遇蓝奏云无法访问,请修改lanzous(把s修改成x)

 免责声明:本站为个人博客,所有软件信息均来自网络 修改版软件,加群广告提示为修改者自留,非本站信息,注意鉴别

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023年7月15日 下午4:22
下一篇 2023年7月15日 下午4:23