响应式系统(三)

前言

这章节承上章节漏掉的数组观测、新增属性观测

正文

观测数组

我们回到Observer的这段代码

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

上章节我们走的是else,这次我们走if,即value是数组的情况
我们先判断当前环境是否支持__proto__,看情况分别使用protoAugment、copyAugment,将其赋值给augment

augment(value, arrayMethods, arrayKeys)

我们先搞清楚arrayMethods、arrayKeys分别是什么

export const arrayMethods = Object.create(arrayProto)
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

溯源可知arrayMethods就是数组的原型对象,所以我们再看protoAugment、copyAugment

function protoAugment(target, src: Object, keys: any) {
    target.__proto__ = src
}
function copyAugment(target: Object, src: Object, keys: Array<string>) {
    for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])
    }
}

可见protoAugment就是将arrayMEthods赋值给value.__proto__,也就是将处理过的数组原型上的方法赋值给数组原型,也就是劫持下数组原型对象,这样子我们就可以在调用[].splice之类的方法时在不破坏原生的操作之后加上自己的一些操作
copyAugment就是在数组不支持__proto__时,那我们就需要遍历arrayKeys,然后使用defObject.defineProperty)逐项设置,这样子也可以达到类似的效果
这样子就能做到在调用.splice之类的方法时可以执行注入到原型上的逻辑

这只是修改了数组对象的原型对象指向,将其指向修改过的arrayMethods。也就是并不是所有的数组对象都会被劫持,只有被观测的数组对象才会被劫持

最后this.observeArray(value)

observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
        observe(items[i])
    }
}

就是遍历数组,然后逐项观测即可

arrayMethods

接下来看看arrayMethodsArray.prototype做了什么改动

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]
methodsToPatch.forEach(function (method) {
    const original = arrayProto[method]
    def(arrayMethods, method, function mutator(...args) {
        const result = original.apply(this, args)
        const ob = this.__ob__
        let inserted
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
                break
        }
        if (inserted) ob.observeArray(inserted)
        ob.dep.notify()
        return result
    })
})

首先我们取到数组原型对象,然后通过Object.create复制一份副本。因为劫持必然会对原本的做出改动,使用副本的话不会影响原本的
然后定义methodsToPatch变量存储会对数组做出改动的方法,因为若不会对数组做出改动就没有什么劫持的价值
以上是准备工作,接下来遍历methodsToPatch。首先取得该项原方法赋值给original,然后使用def覆盖在原型上的此方法,既然是劫持,就不好做些影响原本结果的情况,就比如push的结果,劫持完了的push也该和原本的一致

const result = original.apply(this, args)
// ...省略
return result

这个就是调用原方法得到操作结果,最后返回
接下来看我们注入的逻辑

const ob = this.__ob__
let inserted
switch (method) {
    case 'push':
    case 'unshift':
        inserted = args
        break
    case 'splice':
        inserted = args.slice(2)
        break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify()

首先取得这个数组对象对应的__ob__赋值给ob,然后我们试想下,这么些个方法里有几个可是会增加新元素的,新的值自然也是需要观测的,所以我们得拿到这部分新值。对于push、unshiftargs就是新增的元素,splice可新增也可删除,新增的话就是参数的第三项,所以取args.slice(2)。然后简单了,判断inserted存在的话就调用ob.observeArray(inserted),最后调用ob.dep.notify(),触发该数组对象上收集到的依赖

观测数组和观测对象为何要区分

我们可以看到数组和纯对象观测是不一样的,纯对象的话每个键值都Object.defineProperty处理过,而数组的话索引是没有被处理过的,这也就导致了数组的索引是非响应式的
这个在官网有提到

注意事项

其实这里很多人看到会有误区,也就是是不是Object.defineProperty监测不到索引变动什么的,其实不是。看这个issue8562
也就是其实完全可以当做纯对象处理,不过终究是

性能代价和获得的用户体验收益不成正比

Vue.set

上章节我们简单说了下新增属性原理,也就是Vue.set,即:

  • 将新属性值转为响应式
  • 触发新属性宿主对象收集到的依赖(__ob__)

新增我们根据这个思路来看看Vue.set源码

export function set(target: Array<any> | Object, key: any, val: any): any {
    if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
    ) {
        warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
    }
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        target.splice(key, 1, val)
        return val
    }
    if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
    }
    const ob = (target: any).__ob__
    if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
            'Avoid adding reactive properties to a Vue instance or its root $data ' +
            'at runtime - declare it upfront in the data option.'
        )
        return val
    }
    if (!ob) {
        target[key] = val
        return val
    }
    defineReactive(ob.value, key, val)
    ob.dep.notify()
    return val
}

首先判断下该宿主对象情况,不能是undefined、null、原始类型
然后判断下若是数组,而且key是有效地索引,那么直接用splice就行了
接下来这段有点门道,所以深究下

if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
}

这个if有俩条件,即keytarget或者其原型链上且不能在Object.prototype上,那么就直接修改值就行了。其实原本并非如此,详情看这issues/6845。原本仅仅是if (hasOwn(target, key))

class Model {
    constructor() {
        this.foo = '123'
        this._bar = null
    }
    get bar() {
        return this._bar;
    }
    set bar(newvalue) {
        this._bar = newvalue;
    }
}
data = new Model()

试想若是target、key分别是data、'bar'那么hasOwn(data, 'bar') === false'bar' in data && !('bar' in Object.prototype) === true
可见前者会当做新增属性,后者直接当做已有属性,直接修改即可,即触发set bar
最后代码到这了就必然是新增属性
首先就是简单的取下ob对象,然后就是揭示一个规矩:

  • 不能给Vue实例设置新属性
    这个就是可能出现覆盖情况
  • 不能给根data设置新属性
    这个有点讲究,其实呢是可以的,如demo4。它为什么不可以呢,我们知道initData里有对data实现了代理访问即proxy(vm, '_data', key)。也就是vm.a === vm._data.a。我们新增的自然也就没有这层代理,那么根数据新增属性自然也就不能vm.nVal这样子访问了。所以如例子所示,自行做了这个代理就可以啦
    然后要是ob不存在的话就说明这个target非响应式,简单设置即可
    最后就是defineReactive转化成响应式,并且ob.dep.notify()触发依赖更新
Vue.del
export function del(target: Array<any> | Object, key: any) {
    if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
    ) {
        warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
    }
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.splice(key, 1)
        return
    }
    const ob = (target: any).__ob__
    if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
            'Avoid deleting properties on a Vue instance or its root $data ' +
            '- just set it to null.'
        )
        return
    }
    if (!hasOwn(target, key)) {
        return
    }
    delete target[key]
    if (!ob) {
        return
    }
    ob.dep.notify()
}

首先就是和Vue.set一样的判定以及数组情况下调用劫持过的数组方法处理还有Vue实例对象以及根data不能操作的限定
然后就是if (!hasOwn(target, key)) {,这个就是判定该对象上有没有该属性,没有的话自然就return。这里为什么不用和Vue.set里一样呢,这是因为delete操作只会在自身的属性上起作用,要删除原型链上的属性就得传入那个原型对象
最后就是删除该属性,判断下ob不在的话就return,因为不是响应式的自然不用触发更新,是的话就ob.dep.notify()触发依赖更新

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

推荐阅读更多精彩内容