一、Vue 的 DOM 更新策略
Vue 的响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。那么 DOM 更新的策略是什么样的呢?
Vue 官网说明:
Vue 在更新 DOM 时是 异步执行 的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
也就是说 Vue 在数据变化之后,视图不会立刻更新,而是等同一事件循环中所有的数据变化完成之后再进行统一是数据更新。
这样就会产生一些问题:
<template>
<div class="test">
<div>
<button @click="test" ref="btn">{{msg}}</button>
</div>
</div>
</template>
<script>
export default {
data () {
return {
msg:"1",
}
},
methods:{
test(){
this.msg="2";
console.log(this.$refs.btn.innerText);
}
}
}
</script>
这样打印出来的值是 ‘1’ ,而不是我们已经需改过的 ‘2’,原因就是我在赋值之后立刻获取 DOM 的值,但是这个时候 DOM 还没有完成更新,所以取到的是修改之前的值。
二、$nextTick() 的使用
官方定义: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
解决上面的问题:
<template>
<div class="test">
<div>
<button @click="test" ref="btn">{{msg}}</button>
</div>
</div>
</template>
<script>
export default {
data () {
return {
msg:"1",
}
},
methods:{
test(){
this.msg="2";
this.$nextTick(function(){
console.log(this.$refs.btn.innerText);
})
}
}
}
</script>
使用 $nextTick 之后,获取 DOM 值的代码会在 DOM 重新渲染结束之后执行,这样得到的值就是手动修改之后的值。
看看这段代码是如何运行的:
//1
this.msg="2";
//2
console.log(this.$refs.btn.innerText);
//3
this.$nextTick(function(){
console.log(this.$refs.btn.innerText);
})
1、修改数据,这是同步任务,然后 Vue 监听到数据变化之后开启异步队列,缓冲事件循环数据的变化。
2、打印 btn 的值,这个也是同步任务,会在异步执行前完成,此时 DOM 还没有重新渲染。
3、$nextTick 在 DOM 异步任务执行结束之后执行,此时 DOM 已经重新渲染。
注意:
1、$nextTick 写在赋值前面,同样能拿到修改后的值。
2、也可以使用定时器来代替 $nextTick.。
三、补充
1、为什么 Vue 更新 DOM 要用异步操作?
在某些情况下,如果在页面加载的时候对对象 test 进行循环100次的赋值操作;如果不是异步更新,那么每一次循环赋值时都会根据响应式触发 setter => Def => Watcher => update => run 这样的操作,每一次都会更新视图,这样是非常消耗性能的。
2、异步执行机制
1、js 是单线程的。
2、所有任务都在主线程从上到下依次执行。
3、遇到异步任务时会将其放到任务队列中。
4、当主线程里面的同步代码执行完成之后,系统会读取任务队列,将可运行的异步任务塞入主线程依次执行。
3、为什么 js 是单线程的?
如果在DOM操作中,有两个线程一个添加节点,一个删除节点,浏览器并不知道以哪个为准,所以只能选择一个主线程来执行代码,以防止冲突。
4、事件循环的理解
1、所有任务开始执行时可以理解成一个事件循环,这个循环执行的都是一些同步的任务。
2、同步任务执行完成之后,开始执行异步任务,这个可以理解是下一个事件循环。