Vue双向数据绑定原理分析 -- Observer(观察者,数据劫持)

Vue双向数据绑定概述

Vue采用数据劫持 + 发布者-订阅者模式实现双向数据绑定,实现逻辑图如下所示:

vue双向数据绑定实现逻辑图


数据劫持

Vue 借助Object.defineProperty()来劫持各个属性,这样一来属性存取过程都会被监听到


发布者-订阅者模式

主要实现三个对象:Observer(观察者),Watcher(订阅者,观察者),Dep(发布者,订阅收集器)。

1、Observer: 数据的观察者,让数据对象的读写操作(数据劫持)都处于自己的监管之下

2、Watcher: 数据的订阅者,数据的变化会通知到Watcher,然后由Watcher进行相应的操作,例如更新视图

3、Dep: Observer与Watcher的纽带,当数据变化时,会被Observer观察到,然后由Dep通知到Watcher


Observer(观察者)

// src/core/util/lang.js

// 这个方法就是对Object.defineProperty的封装,同时加入一些默认配置

export function def (obj: Object,  key: string,  val: any,  enumerable?: boolean) {

    Object.defineProperty(obj, key, {

        value: val,

        enumerable: !!enumerable,  // 设置是否可枚举

        writable: true,

        configurable: true

    })

}


// src/core/observer/index.js

export class Observer {

    value:any;  // 读写需要被监听的数据对象

    dep: Dep;

    vmCount: number;     // number of vms that has this object as root $data

    constructor (value:any) {

        this.value = value

        this.dep =new Dep()  // 关联一个订阅收集器实例对象

        this.vmCount =0

        // def是defineProperty方法的封装

        // 为数据对象设置一个__ob__属性,并赋值为当前Observer实例

        def(value, '__ob__', this)

        if (Array.isArray(value)) {   

            // hasProto是一个判断对象的__proto__属性是否可用的函数

            // protoAugment是一个利用__proto__属性为数组或者对象扩充原型链的方法

            // copyAugment是一个实现属性拷贝的方法

            const augment = hasProto ? protoAugment : copyAugment

            // arrayMethods是继承自数组原型对象(Array.prototype)的对象, arrayKeys是arrayMethods所有属性名的集合

            augment(value, arrayMethods, arrayKeys)

            this.observeArray(value)

        }else {   // value是对象

            this.walk(value)

        }

    }

    /**

        * Walk through each property and convert them into

        * getter/setters. This method should only be called when

        * value type is Object.

        * 简单来说就是对对象建立观察的方法

        */

    walk (obj: Object) {

        const keys = Object.keys(obj)

        for (let i =0; i < keys.length; i++) {

            defineReactive(obj, keys[i])

        }

    }

    /**

        * Observe a list of Array items.

         * 对数组建立观察的方法

        */

    observeArray (items: Array) {

        for (let i =0, l = items.length; i < l; i++) {

            observe(items[i])

        }

    }

}

这类定义了三个实例属性:

value:需要被观察的数据对象;

dep:关联的依赖收集器对象(Dep类的实例对象);

vmCount:关联的vue实例对象个数。

接下来我们看一下构造函数constructor,初始化以上三个属性的代码就不多说了,我们简单说一下 def(value, '__ob__', this),这是在需要被观察的数据对象(value)上,增加__ob__属性,作为数据已经被Observer观察的标志。针对不同类型的value,vue做不同的处理。

实现对象的数据监听(value是对象)

value是对象处理过程比较简单,直接调用Observer的walk方法(Observer类的实例方法),而walk方法的内部其实是调用了defineReactive方法,那么我们来看一下walk方法和defineReactive方法:

// src/core/observer/index.js

walk (obj: Object) {

    const keys = Object.keys(obj)

    for (let i =0; i < keys.length; i++) {

        defineReactive(obj, keys[i])

    }

}

// src/core/observer/index.js

export function defineReactive (

    obj: Object,   // 被观察的对象

    key: string,  // 被观察的属性

    val:any,  // 该属性的值

    customSetter?: ?Function,  // 自定义的setter

    shallow?: boolean  // 是否只浅层次观察,类似于浅拷贝

) {

    const dep =new Dep()

    // 获取属性的配置对象

    const property = Object.getOwnPropertyDescriptor(obj, key)

    if (property && property.configurable ===false) {

        return

    }

    const getter = property && property.get

    const setter = property && property.set

    let childOb = !shallow && observe(val)

    // 实现数据劫持

    Object.defineProperty(obj, key, {

        enumerable:true,

        configurable:true,

        get: function reactiveGetter () {

            // 监听数据获取操作

        },

        set: function reactiveSetter (newVal) {

            // 监听数据赋值操作

        }

    })

}

walk方法实现非常简单,在这里不再赘述。而defineReactive 方法的功能是把要观察的 data 对象的每个属性都赋予 getter 和 setter 方法。这样一旦属性被访问或者更新,我们就可以追踪到这些变化。我们来详细说说defineReactive方法的实现过程:

1、定一个Dep类型的对象,用来作为依赖收集器 --- const dep =new Dep()


2、获取key属性的配置对象,如果配置项configurable为false,表示该属性不可配置,直接返回

const property = Object.getOwnPropertyDescriptor(obj, key)

if (property && property.configurable ===false) {

    return

}


3、缓存该key属性的get/set函数

const getter = property && property.get

const setter = property && property.set


4、进行shallow参数判断,要不要进行深层次观察(默认是进行深层次观察的),什么叫深层次观察呢?说直白点,就是当value是一个对象或者一个数组时,我们可以继续观察value对象的】每一个属性。而实现这个过程的是observer方法:

/**

* @param value: 任意类型的值

* @param asRootData: 判断是不是根数据

* @returns {Observer|void}  返回一个Observer实例对象或者无返回

*/

export function observe (value:any, asRootData: ?boolean): Observer | void {

    // value必须是一个对象或者数组,且不能是vnode

    if (!isObject(value) || value instanceof VNode) {

        return

    }

    let ob: Observer | void       // ob可以是Observer类型的对象或者undefined

    // 当value的__ob__属性存在,说明该value已经存在Obsever,直接赋值给ob变量

    if (hasOwn(value, '__ob__')  && value.__ob__instanceof Observer) {

        ob = value.__ob__

    }else if (

        shouldObserve && 

        !isServerRendering() &&   // 判断是不是服务器渲染

        (Array.isArray(value) || isPlainObject(value)) && 

        Object.isExtensible(value) &&   // 是个可扩展的对象

        !value._isVue// 不是Vue实例

    ) {

        ob =new Observer(value)

    }

    // asRootData 为真,ob表示最外层的Observer实例,用vmCount记录vm实例数量,这里暂时还不知道是做什么用的

    if (asRootData && ob) {

        ob.vmCount++ 

    }

    return ob

}

针对observer函数的实现流程,这里附上一张图方便小伙伴们理解整体的流程:

observer函数实现逻辑图

这里要说明的是这个图并非在下原创,来源于另一篇技术文章:https://segmentfault.com/a/1190000008377887?utm_source=tag-newest


实现数组的数据监听(value是数组)

我们再简单贴一下数组处理的相关代码:

// hasProto是一个判断对象的__proto__属性是否可用的函数           

// protoAugment是一个利用__proto__属性为数组或者对象扩充原型链的方法           

// copyAugment是一个实现属性拷贝的方法

// arrayMethods是继承自数组原型对象(Array.prototype)的对象,arrayKeys是arrayMethods所有属性名的集合

const augment = hasProto ? protoAugment : copyAugment            // 1

augment(value, arrayMethods, arrayKeys)            // 2

this.observeArray(value)


// observeArray方法源码

observeArray (items: Array) {    

    for (let i =0, l = items.length; i < l; i++) {        

        observe(items[i])    

    }

}

简单来讲,1和2的实现的东西是当对象存在__proto__属性时,直接将__proto__属性指向一个继承于数组原型的对象;否则就将数组原型里面定义的方法全部赋值到target上,并进行监听。最后调用this.observeArray(value),observeArray方法实际上就是遍历元素,然后依次调用前面提到的observer方法。

⚠️注意:当属性key的值value是数组时,并没有调用defineReactive方法对该属性进行劫持!为什么这么做呢?因为数组的属性其实就是索引,Object.defineProperty 本身做不到对这种属性变化的监听!!!所以我们在开发中有时候会遇到以下情况:即使修改了数据,视图并没有更新

vm.todos[0] = {

    name: 'New name',

    description: 'New description'

}

// 正确的数据更新方式,当数组元素是个对象时,vue还是会进入对象内部建立监听

vm.todos[0].name = 'New name';

vm.todos[0].description = 'New description';

⚠️注意:当数组调用'push', 'pop','shift','unshift','splice', 'sort','reverse'这些会改变数组自身的方法时,vue才能监听到数组的变化。


到目前为止,我们已经把Observer类和数据劫持过程讲解清楚了。接下来我们将继续分析 Dep 和 Watcher。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容