前端积累 | Vue响应式的基本原理

370 阅读3分钟

众所周知,Vue是一个响应式的 JavaScript框架。响应式作为前端开发中一个重要的设计方法,其原理也是需要掌握的。

数据驱动视图

Vue的响应式原理遵循的是一种称作“数据驱动视图”的设计思维。通常情况下的函数,都是传入数据作为参数,然后返回数据作为结果。而前端页面中最多的不是数据而是视图,而视图的变化又与数据息息相关。因此数据驱动视图就是让函数通过数据的变化来使视图发生变化。当数据一旦有变化,那么便会重新渲染视图。

基本实现

Vue2和Vue3的实现方式略有区别,Vue2使用的是Object.defineProperty实现,而Vue3则使用的是Proxy

Vue2中的实现

我们正常给对象添加属性的话是不能自定义属性的特征的(比如是否能够被修改,以及gettersetter等),而如果我们使用Object.defineProperty的话,就可以设置这些特征。当我们通过某个函数获取属性的时候,这个函数会作为该对象的一个依赖而塞进一个集合中。当数据改变时,会调用set,然后会改变所有的依赖项。

let obj ={
    val: null,
};
Object.defineProperty(obj,'key',{
    get(){
        // 依赖收集
        console.log('get');
        return obj.val;
    },
    set(val){
        // 更新分发
        console.log('set');
        obj.val = val;
    },
})
obj.key =1;// set
obj.key;// get 1

Vue3中的实现

Vue3中使用的则是ES6中的新特性Proxy。它的特性在于可以高效地对对一个对象的操作进行拦截。gettersetter只有在修改本身值的时候才会触发,而Proxy可以设置自己的自定义条件。

let obj ={};
let nObj=new Proxy(obj,{
    //拦截get,当我们访问nObj.key时会被这个方法拦截到
    get: function (target, propKey, receiver) {
        console.log('get');
        return Reflect.get(target, propKey, receiver);
    },
    //拦截set,当我们为nObj.key赋值时会被这个方法拦截到
    set: function (target, propKey, value, receiver) {
        console.log('set');
        return Reflect.set(target, propKey, value, receiver);
    }
})
nObj.key=1;// set
nObj.key// get 1

当一个对象载入的时候,Vue会遍历这个对象的全部属性,然后给所有的属性添加对应的gettersetter,然后把代理对象挂载到VM上。

监听对象属性

Vue需要一个Observer类来实现监听,也就是上面说的给属性添加对应的gettersetterObserver类的实例会绑定为对象的ob属性,然后遍历walk方法,给每个属性加上对应的getter/setter,使用的是defineReactive方法,也就是我们上文中提到的基本实现,后面使用了一个递归函数,是为了实现深度监听,如果需要监听的对象有嵌套的话,那么就需要使用深度监听。

class Observer {
    constructor(value) {
        this.value = value;
        if (!value || (typeof value !== 'object')) {
            return;
        } else {
            this.walk(value);
        }
    }
    walk(obj) {
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key])
        })
    }
}
function defineReactive(obj, key, val) {
    Object.defineProperty(obj,key,{
        get(){
            // 依赖收集
            console.log('get');
            return val;
        },
        set(newVal){
            if(newVal === val){
                return;
            }
            new Observer(val)
            updateView();
        },
    })
}
function updateView() {
    console.log('view updated')
}
const data = {
        val:1,
}
new Observer(data)
data.val = 2;  // view updated

依赖收集

我们已经可以做到初级的更新分发了,下面介绍依赖收集的原理。

当有地方获取数据的时候,Dep类的一个对象会接收到依赖,也就是一个Watcher类的对象。当数据发生变动的时候,Dep会通知Watcher实例,然后通过回调进行视图更新。

class Observer {
    constructor(value) {
        this.value = value;
        if (!value || (typeof value !== 'object')) {
            return;
        } else {
            this.walk(value);
        }
    }
    walk(obj) {
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key])
        })
    }
}
class Dep {
    constructor() {
        this.subs = [];
    }
    /*添加一个观察者对象*/
    addSub (sub) {
        this.subs.push(sub)
    }
    /*依赖收集,当存在Dep.target的时候添加观察者对象*/
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }
    // 通知所有watcher对象更新视图
    notify () {
        this.subs.forEach((sub) => {
            sub.update()
        })
    }
}
class Watcher {
    constructor() {
    /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
        Dep.target = this;
    }
    update () {
        console.log('view updated')
    }
    /*添加一个依赖关系到Deps集合中*/
    addDep (dep) {
        dep.addSub(this)
    }
}
function defineReactive(obj, key, val) {
    const dep = new Dep();
    Object.defineProperty(obj,key,{
        get(){
            dep.depend();
            console.log('get');
            return val;
        },
        set(newVal){
            if(newVal === val){
                return;
            }
            new Observer(val)
            dep.notify();
        },
    })
}
class Vue {
    constructor (options) {
        this._data = options.data
        new Observer(this._data) // 所有data变成可观察的
        new Watcher() // 创建一个观察者实例
        console.log('render', this._data.val)
    }
}
let o = new Vue({
    data: {
        val:1,
    }
})
o._data.val = 2;
Dep.target = null;
//get render 1 view updated