深入理解NextTick实现原理 | 青训营笔记
这是我参与「第四届青训营 」笔记创作活动的第3天
在下次DOM更新循环结束后执行延迟回调,在修改数据之后使用这个方法,获取更新后的DOM;即通过nextTick方法,可以在传入nextTick的回调函数中获取到变化后的DOM
举例:
<template>
<div>
<button @click='update'>更新数据</button>
<span id='content>{{message}}</span>
</div>
</template>
<script>
export default {
data: {
message: 'hello world'
},
methods: {
update() {
this.message='hello d'
console.log(document.getElementById('content').textContent);
this.$nextTick(()=>{
console.log(document.getElementById('content').textContent)
})
}
}
}
</script>
// 输出: hello world Hello d
由实际结果可知在update中对message的更新并不会马上同步到span中,dom树的更新发生在nextTick的回调函数当中;可以看出DOM更新是异步执行的,并且会在更新之后默认调用nextTick的回调函数进行更新
nextTick的实现原理所涉及的前置知识
- Vue响应式原理(理解设计意图)
- 浏览器事件循环机制(理解原理)
响应式原理
核心是数据劫持和依赖收集,主要利用Object.defineProperty()实现对数据存取操作的拦截,把该实现称为数据代理;同时通过数据get方法的拦截,获取到数据的依赖,并将所有依赖收集到一个集合中。
defineProperty()方法实现的数据劫持
Observe类的实现
class Observe{
constructor(data) {
//若传入的数据是object
if(typeof data=='object') {
this.walk(data);
}
}
//该方法遍历对象中的属性,并依次对其进行响应式处理
walk(obj) {
//获取所有属性
const key = Object.keys(obj)
for(let i=0; i<keys.length; i++) {
this.defineReactive(obj, keys[i])
}
}
defineReactive(obj, key) {
if(typeof obj[key]=='object') {
//如果属性是对象,递归调用walk方法
this.walk(obj[key]);
}
const dep = new Dep(); //用于收集依赖的集合
const val = obj[key];
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
//get代理将Dep.target即watcher对象添加到依赖集合中
get() {
//创建Watcher对象时会给Dep.target赋值
if(Dep.target) {
dep.addSubs(Dep.target);
}
return val;
},
set(newVal) {
val = newVal;
//依赖的变更响应
dep.notify(newVal)
}
}
}
}
Dep类的实现
class Dep {
static target=null
constructor() {
this.subs=[];
}
addSubs(watcher) {
this.subs.push(watcher)
}
notify(newVal) {
for(let i=0; i<this.subs.length; i++) {
this.subs[i].update(newVal);
}
}
}
Watcher类:管擦和数据的变更,调用data中对应属性的get方法触发依赖收集,并在数据变更后执行相应的更新
let uid = 0
class Watcher{
//vm即Vue对象,key要观察的属性,cb时观测到数据变化后需要做的操作,通常是指DOM变更
constructor(vm, key, cb) {
this.vm = vm;
this.uid = uid++;
this.cb = cb;
//调用get触发依赖收集之前,把自身赋值给Dep.target静态变量
Dep.target = this;
//触发对象上代理的get方法,执行get添加依赖
this.value = vm.$data[key];
//用完即情况
Dep.target = null;
}
//调用set触发Dep的notify时要执行的update函数,用于响应数据变化执行run函数即dom变更
update(newVal) {
//值发生变化才变更
if(this.value !== newValue) {
this.value = newVal;
this.run();
}
}
//执行DOM更新操作
run() {
this.cb(this.value);
}
}
为什么要使用nextTIck
按照上述响应式原理的实现,如果对某项数据进行频繁的更新时会有很严重的性能问题,每次data数据的变化都会带哦用watcher的update去更新dom
dom更新的性能成本是很昂贵的,在开发中应该尽量减少DOM操作;因此引入nextTick去优化该问题,在每次数据变化之后不会立即去执行DOM更新,而是把数据变化的动作缓存起来,在合适的时机只执行一次的dom更新操作。通过设置合适的事件间隔,与下面的事件循环机制有关
事件循环机制
通过事件循环机制能知道每次事件循环之间都有一个视图render,只需在render之前完成对dom的更新即可,因此为避免无效的DOM操作,需要将数据变更缓存起来,只保存最后一次数据最终的变更结果
使用Promise创建的是微任务,微任务会在本次事件循环同步代码执行结束后执行,使用setTimeout创建的是宏任务,同样会在此次同步代码执行完成后执行,区别是在setTimeout代码执行之前会穿插一次无效的视图渲染,因此我们尽量使用Promise创建微任务实现异步更新。