深入理解NextTick实现原理 | 青训营笔记

101 阅读2分钟

深入理解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的实现原理所涉及的前置知识

  1. Vue响应式原理(理解设计意图)
  2. 浏览器事件循环机制(理解原理)

响应式原理

核心是数据劫持和依赖收集,主要利用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创建微任务实现异步更新。