vue双向数据绑定原理和实现

172 阅读3分钟

vue2(vue3没来得及理解)的双向数据绑定是通过Object.defineProperty()来劫持对象属性的set和get事件

关于Object.defineProperty()这个函数的语法

Object.defineProperty(obj, prop, descriptor)
obj:要定义属性的对象。
prop:要定义或修改的属性的名称或 Symbol 。
descriptor:要定义或修改的属性描述符。

const object1 = {};

Object.defineProperty(object1, 'property1', {
  value: 42,
  writable: true
});

object1.property1 = 77;
console.log(object1.property1); //  77

 Object.defineProperty()的更多用法

developer.mozilla.org/zhCN/docs/W…

var obj  = {};
Object.defineProperty(obj, 'name', {
        get: function() {
            console.log('获取值')
            return val;
        },
        set: function (newVal) {
            console.log('设置值')
        }
})
obj.name = '数据绑定';//在给obj设置name属性的时候,触发了set这个方法
var val = obj.name; //数据绑定

通过Object.defineProperty( )这个方法给obj对象设置了name属性,对其get和set方法进行重写操作,set方法在设置name属性时被触发,get方法在获得name属性时被调用并返回对应的值。

这是双向数据绑定的核心原理:通过Object.defineProperty()实现对属性的劫持,然后同get和set事件达到监听数据变动的目的。

vue双向数据绑定的实现


observer对每个vue中data定义的属性用Object.defineProperty()实现数据劫持,以便利用其中的set和get,然后通知订阅者,订阅者会触发它的update方法,对视图进行更新。

observer的实现

定义一个 defineReactive ,这个方法通过 Object.defineProperty 来实现对对象的响应式化,入参是一个 obj(需要绑定的对象)、key(obj的某一个属性),val(具体的值)。

function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,       /* 属性可枚举 */
        configurable: true,     /* 属性可被修改或删除 */
        get: function reactiveGetter () {
            return val;         /* 实际上会依赖收集 */
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            console.log(newval);
        }
    });
}

经过 defineReactive 处理以后,我们的 obj 的 key 属性在「读」的时候会触发reactiveGetter 方法,而在该属性被「写」的时候则会触发 reactiveSetter 方法。


然后封装一层 observer 。这个函数传入一个 value(需要响应式化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理。(实际上 observer 会进行递归调用,为了便于理解去掉了递归的过程)

function observer (value) {
    if (!value || (typeof value !== 'object')) {
        return;
    }
    
    Object.keys(value).forEach((key) => {
        defineReactive(value, key, value[key]);
    });
}

Dep的实现

Dep为每个属性添加订阅者,主要作用是用来存放 Watcher 观察者对象

class Dep {
    constructor () {
        /* 用来存放Watcher对象的数组 */
        this.subs = [];
    }

    /* 在subs中添加一个Watcher对象 */
    addSub (sub) {
        this.subs.push(sub);
    }

    /* 通知所有Watcher对象更新视图 */
    notify () {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

  1. addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;

  2. notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。

Watcher观察者

class Watcher {
    constructor () {
        /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
        Dep.target = this;
    }

    /* 更新视图的方法 */
    update () {
        console.log("视图更新啦~");
    }
}

Dep.target = null;

总结

observer 的过程中会注册 get 方法,该方法用来进行依赖收集。在它的闭包中会有一个 Dep 对象,这个对象用来存放 Watcher 对象的实例。依赖收集的过程就是把 Watcher 实例存放到对应的 Dep 对象中去。get 方法可以让当前的 Watcher 对象(Dep.target)存放到它的 subs 中(addSub)方法,在数据变化时,set 会调用 Dep 对象的 notify 方法通知它内部所有的 Watcher 对象进行视图更新。

这是 Object.definePropertyset/get 方法处理的事情,那么「依赖收集」的前提条件还有两个:

  1. 触发 get 方法;

  2. 新建一个 Watcher 对象。

这个在 Vue 的构造类中处理。新建一个 Watcher 对象只需要 new 出来,这时候 Dep.target 已经指向了这个 new 出来的 Watcher 对象来。而触发 get 方法也很简单,实际上只要把 render function 进行渲染,那么其中的依赖的对象都会被「读取」,这里我们通过打印来模拟这个过程,读取 test 来触发 get 进行「依赖收集」。

总的来说就是 get 进行「依赖收集」,set 通过观察者来更新视图。