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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容