响应式系统(二)

前言

接上一章节,我们知道响应式系统大致就这三步:

  1. 观测数据,将其转化为响应式对象
  2. 暴露$watch方法,接收exp、fn参数,确定要监听的属性以及接收数据变化的回调函数
  3. get收集依赖,set触发依赖,也就是get里收集fnset里触发fn

现在我们看看Vue里是怎么实现响应式的

function initData(vm: Component) {
    // ...
    // observe data
    observe(data, true /* asRootData */)
}

我们就从initData为入口看看是如何观测数据的

正文

observe函数

export function observe(value: any, asRootData: ?boolean): Observer | void {
    if (!isObject(value) || value instanceof VNode) {
        return
    }
    let ob: Observer | void
    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
    ) {
        ob = new Observer(value)
    }
    if (asRootData && ob) {
        ob.vmCount++
    }
    return ob
}

可见initData调用observe方法观测数据。它接收俩个参数value(要观测的数据)asRootData(是否是根数据)

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

首先判断value若不是对象或者是VNode实例对象就return,很容易理解,不是对象自然是不能观测,若是VNode实例对象也是不需要观测的,因为VNode实例对象用于DOM比对更新,这不需要响应式。即使自己new VNode也没什么实际意义。demo

let ob: Observer | void
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
) {
    ob = new Observer(value)
}

接下来是if...else分支我们先看if分支,它先判断value上有没有__ob__属性且是Observer实例,是的话就将其赋值给ob

其实__ob__属性是new Observer(value)之后添加上的,我们暂且不管它其他作用,在此它可以判断是否已经观测,也就是防止重复观测。它是Observer实例对象

然后是else if分支,它有五个条件:

  • shouldObserve
export let shouldObserve: boolean = true
export function toggleObserving(value: boolean) {
    shouldObserve = value
}

可见该变量初始值为true,且暴露toggleObserving方法用于修改其值。其实从这if语句可知shouldObservefalse的话就不会观测,那么很显然这个值是用于控制转换响应式数据的。比如props就调用了此方法用于禁止观测。盖其数据来源一般来自于data,其本身已经是响应式得了,就不需要观测了

  • !isServerRendering()
    这个就是必须非服务端渲染。服务端渲染的话不观测。原因呢官网讲的很清楚,服务器上的数据响应
  • (Array.isArray(value) || isPlainObject(value))
    必须是数组或者纯对象,无需解释
  • Object.isExtensible(value)
    必须是可扩展,Object.preventExtensions()、Object.freeze()、Object.seal()可禁用对象扩展
  • !value._isVue
    必须不是Vue实例,这个也是因为Vue实例诸如_isVue之类的属性不可能去修改,而data之类的已经观测了。https://nymlc.github.io/vue2.x-analysis/demo/observer/4.html#2

五个条件为真才调用Observer观测且返回一个对象赋值给ob

if (asRootData && ob) {
    ob.vmCount++
}
return ob

最后判断下是否是datavm._data)且ob也存在,那么就给ob.vmCount++,最后返回ob
vmCount稍微讲下,比如一个Hello组件(data引用的同一个对象),每用一次组件就会vmCount++https://nymlc.github.io/vue2.x-analysis/demo/observer/4.html#3

Observer 类

上文可知,其实observe只是做了些边界情况处理,真正转换响应式对象是在Observer

export class Observer {
    value: any;
    dep: Dep;
    constructor(value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
            const augment = hasProto
                ? protoAugment
                : copyAugment
            augment(value, arrayMethods, arrayKeys)
            this.observeArray(value)
        } else {
            this.walk(value)
        }
    }
    walk(obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i])
        }
    }
    observeArray(items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
            observe(items[i])
        }
    }
}

看一个类首先看整体,也就是看它的属性和方法,然后从constructor开始看起

vmCount

初始化vmCount0,这个在observe里有重新赋值,其实就是用于判断是否是data。比如可以在Vue.set里防止给data添加新属性

我们知道Vue.set可以添加新属性,但是并不能给data添加新属性。但是我们稍加修改是可以的,在Vue.set详解,在此不再赘述。https://nymlc.github.io/vue2.x-analysis/demo/observer/4.html#4

ob

这个是个很关键的属性,之后会看见它的诸多用处。这里只是简单说下它的构造和基本用途
首先为什么使用def呢,是因为使用了def,这个__ob__属性就不可枚举,这样子遍历观测就不会观测此属性。它可以用于判断当前对象是否已观测,其值是Observer实例对象,更是诸多用处
其构造如下

const data = {
    a: 1,
    // 不可枚举
    __ob__: {
        value: value, // 其实就是data
        dep: new Dep(), // Dep实例
        vmCount: 0 // 根data被观测使用次数(其实就相当于组件被实例化次数)
    }
}
dep

这个其实就是上章节说的那个收集依赖的容器,只是比我们例子复杂很多而已,之后详述。值得注意的是,这里的dep容器是对应于对象的,下面还有个dep是对应于属性的

defineReactive

初始化完了几个实例属性之后就开始观测数据

if (Array.isArray(value)) {
    const augment = hasProto
        ? protoAugment
        : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
} else {
    this.walk(value)
}

可见是区分数组、对象的。我们先从对象看起,即else分支,调用walk方法处理

walk(obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i])
    }
}

此方法很简单,就是遍历每个属性然后调用defineReactive处理即可

其实早先并非如此设计,是defineReactive(obj, keys[i], obj[keys[i]]),为何如此下文到了再说


export function defineReactive(
    obj: Object,
    key: string,
    val: any,
    customSetter?: ?Function,
    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
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }
    let childOb = !shallow && observe(val)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    childOb.dep.depend()
                    if (Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            if (process.env.NODE_ENV !== 'production' && customSetter) {
                customSetter()
            }
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            childOb = !shallow && observe(newVal)
            dep.notify()
        }
    })
}

首先定义dep变量,且初始化,这就是上文说的对应属性(key)的容器,通过闭包机制被setter、getter调用
然后获取当前属性值得属性描述符,若是不可配置,那么就return

const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
}
let childOb = !shallow && observe(val)

这段还是比较难理解的,说道说道。首先这个是控制val值情况,因为若是没有执行到if里,那么valundefined,也就意味着之后的深度观测失效
这个if有俩个判断条件:demo

  • arguments.length === 2
    首先参数至少有俩,若是假,说明参数传了val,那么自然无需求值
  • !getter || setter
    其实最先并非这么设计的除了上文的defineReactive调用还有如这个issue
if (!getter && arguments.length <= 2)

我们先不管setter,那么这里看来若是有getter的话自然就不会深度观测。可见demodata.a属性
若是有getter、setter的话,按此逻辑就不会深度观测,重新赋值的话可见新值被深度观测了。这样子本未深度观测的被深度观测了,前后不一致,所以就改成现在样子,即setter在的话就取值,以达到深度观测效果,代码如下,在线demo

if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
}
  1. gettersetter不在的话,不深度观测,因为getter是用户自定义的,可能有未知的问题,所以规避
  2. setter在的话,深度观测。这是因为要是不深度观测,重新赋值之后又被深度观测,前后违背了定义响应式数据行为不一致,简单处理就是只要有setter就深度观测
    观测之后,无论之前有没有getter、setter现在都会有

除了上文说的val = undefined影响深度观测,很明显shallow也是用于控制深度观测的,前者隐控制后者显控制

let childOb = !shallow && observe(val)

很明显,只有显示传参为true才能非深度观测。返回的Observer实例对象赋值给childOb,搜索可知只有以下俩者是非深度观测

defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)

控制深度观测手段有俩者:

  • val = undefined深度观测失效
  • shallow = true不会深度观测
收集依赖

接下来就是设置getter、setter访问设置器,我们先看getter,也就是收集依赖

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
        const value = getter ? getter.call(obj) : val
        if (Dep.target) {
            dep.depend()
            if (childOb) {
                childOb.dep.depend()
                if (Array.isArray(value)) {
                    dependArray(value)
                }
            }
        }
        return value
    }
})

get需要干俩件事:

  • 返回属性值
    我们先判断此属性是否有getter,它保存的是该属性原本的getter函数。若存在就直接执行获取值,否则就返回之前的val赋值给value,最后返回即可
  • 收集依赖
    我们先假设这就是我们已经观测完的数据,然后我们收集data.a依赖,那么俩个依赖容器分别是:
    • dep就是a属性对应的依赖容器,通过闭包引用的
    • a属性对象对应的依赖容器,也就是如下的ob2.dep
const data = {
    a: {
        b: 1,
        __ob__: ob2
    },
    __ob__: ob1
}

这个我们先回想下上章节的小栗子,我们先判断Dep.target存不存在,这个其实就是我们上章节的Target也就是我们要收集的依赖,有的话就进入收集逻辑

  1. 首先是dep.depend(),我们姑且先不往里看,看字面意思就是调用dep.depend方法将依赖收集到dep这个容器里,也就是容器1,这是用于setterdep.notify()触发依赖更新
  2. 然后判断childOb是否存在,由上可知这个就是observe(data.a)的返回值,因为data.a是个对象,所以就有值,而childOb === ob2,所以childOb.dep.depend()就是把当前依赖还收集到容器2
    这个是因为没有Proxyjs不能监听到属性的新增,那么我们得想个法子把这个依赖给收集了
window.app = new Vue({
    data: {
        a: {
            b: 1
        }
    },
    methods: {
        change() {
            this.$set(this.a, 'c', 6)
        }
    },
    watch: {
        'a.c': function (nVal, oVal) {
            console.log(nVal, oVal)
        },
        'a.b': function (nVal, oVal) {
            console.log(nVal, oVal)
        }
    },
    template: `<div>
        <p class="title">Vue.set</p>
        <button @click="change">Change a.c</button>
        </div>`
}).$mount('#app')

如此例所示,初始化完了之后$data如下

{
    a: { // dep是[{ expression: "a.c" }, { expression: "a.b" }]
        b: 1,  // dep是{ expression: "a.b" }
        __ob__: {
            value: {
                b: 1
            },
            dep: [{
                expression: "a.c"
            }, {
                expression: "a.b"
            }],
            vmCount: 0
        }
    },
    __ob__: {
        value: {
            a: {
                b: 1
            }
        },
        dep: [],
        vmCount: 1
    }
}

假设我们this.a.c = 2,这里必然不能触发a.c的getter,因为它根本就没有。那么我们就得调用defineReactive()a.c转成响应式数据
然后我们就得想办法触发依赖,为什么不是收集依赖呢?因为this.a.c = 2这句语句就得触发依赖,其实触发依赖就会重新计算值,自然也会收集依赖
既然触发依赖,那么就得收集依赖,这也是childOb.dep.depend()的存在意义,所以我们只需要调用ob.dep.notify()即可,这个功能也就是Vue.set如下

function set(target, key, val) {
    const ob = target.__ob__
    defineReactive(ob.value, key, val)
    ob.dep.notify()
}
  1. 最后判断这个值是否是数组,若是数组的话就dependArray(value)
function dependArray(value: Array<any>) {
    for (let e, i = 0, l = value.length; i < l; i++) {
        e = value[i]
        e && e.__ob__ && e.__ob__.dep.depend()
        if (Array.isArray(e)) {
            dependArray(e)
        }
    }
}

可见其只是简单的遍历这个数组,然后判断子项是否有.__ob__属性。我们知道只有这个子项是对象,那么才有这个属性,所以同第2条,对象需要把依赖收集到对应的dep(依赖容器)。判断子项是否是数组,是的话递归即可

触发依赖

然后就是setter,也就是触发依赖

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    set: function reactiveSetter(newVal) {
        const value = getter ? getter.call(obj) : val
        if (newVal === value || (newVal !== newVal && value !== value)) {
            return
        }
        if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
        }
        if (setter) {
            setter.call(obj, newVal)
        } else {
            val = newVal
        }
        childOb = !shallow && observe(newVal)
        dep.notify()
    }
})

set也做了俩件事:

  • 设置属性值
    首先计算当前值,因为能拿到将设置的新值(newVal),那么我就需要判断下新旧值有没有变化,没变化的话自然就return就是了。值得注意的是(newVal !== newVal && value !== value),这个就是NaN,也就是新旧值都是NaN,那么自然也是无变化
    然后就是customSetter,这个就是一个设置值得时候的提示,我们可以看$listeners
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
        !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
    
    可见设置时就会触发customSetter函数,也就是提示$listeners is readonly.
    接下来我们判断是否有setter,它存有该属性原有的setter,存在的话就调用setter设置新值,这样子就保证其原有的设置操作不被影响,否则的话简单val = newVal即可
  • 触发依赖
    因为新值可能是对象也可能是非对象基本数据类型,所以调用observe观测,且赋值给childOb,这是因为旧值被覆盖了,那么childOb自然也无效了,得重新计算
    然后dep.notify()触发yila即可

后言

这章节主要讲的是如何将数据转化为响应式数据,但是关于数组的观测、新增属性观测(提到一点)都没有讲,下章节继续

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

推荐阅读更多精彩内容