w字总结《深入浅出Vue.js》之变化侦测篇

601 阅读4分钟

变化侦测

什么是变化侦测?

Vue.js自动通过状态生成DOM,并将其输出到页面,此为渲染。Vue.js的渲染过程是声明式的,我们通过模版来描述状态与DOM之间的映射关系。而当应用内部状态变化时需要重新渲染,而如何确定状态中什么变化了就是变化侦测要解决的问题。

object的变化侦测

如何追踪变化?

两种方式可以追踪变化:Object.defineProperty和ES6的Proxy

由于ES6在浏览器中的支持度不理想,所以Vue2采用了Object.defineProperty实现,而Vue3则采用了Proxy

利用Object.defineProperty侦测对象变化可以写出这样的代码:

function defineReactiv (data, key, val) {
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            return val
        },
        set: function (newVal) {
            if(val === newVal) {
                return
            }
            val = newVal
        }
    })
}

defineReactive是对Object.defineProperty的封装,其作用为定义一个响应式数据,在这个函数中进行变化追踪,封装后只需要传递data,key,value即可。封装好后,每当从data的key中读取数据,get函数被触发;往data的key中设置数据时,set函数被触发。

如何收集依赖?收集在哪里?

在Vue2中,模板使用数据等同于组件使用数据,所以数据变化时,会将通知发送到组件,组件内部再通过虚拟DOM重新渲染。

我们观察依赖的目的是当数据发生变化的时候,通知那些使用了该数据的地方。所以我们需要收集依赖,把用到数据的地方收集起来,等属性发生变化时,将之前收集好的依赖循环触发一遍,也就是在getter中收集依赖,setter中触发依赖。

现在已经知道了要在哪里收集触发依赖,那么要把它收集到哪里?我们创建一个专门帮助管理依赖的类Dep,使用这个类,我们可以收集、删除依赖或向依赖发送通知。

export default class Dep {
    constructor() {
        this.subs = []
    }
    
    addSub(sub) {
        this.subs.push(sub)
    }
    
    removeSub(sub) {
        remove(this.subs, sub)
    }
    depend() {
        if(window.target) {
            this.addSub(windwo.target)
        }
    }
    
    notify() {
        const subs = this.subs.slice()
        for(let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}

function remove (arr, item) {
    if (arr.length) {
        const index = arr.indexOf(item)
        if(index > -1) {
            return arr.splice(index, 1)
        }
    }
}

再改造一下defineReactive

function defineReactive (data, key, val) {
    let dep = new Dep()
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.depend()
            return val
        },
        set: function () {
            if(val === newVal) {
               return 
            }
            val = newVal
            dep.notify()
        }
    })
}

什么是Watcher

上边的代码中,我们收集的依赖是window.target,那我们究竟要收集谁呢。收集说的通俗易懂点就是当属性变化时,我们应该通知谁。

我们需要通知用到数据的地方,而用到它的地方可能会很多,有可能是模版,也有可能是watch等等,所以我们需要抽象出一个能集中处理这些情况的类。我们在收集依赖阶段只收集这个封装好的类实例,同样也只通知它自己,由它负责通知其他地方,这就是Watcher。

Watcher是一个中介的角色,数据变化时通知它,他负责通知其他地方。

首先看一下Watcher的经典使用方式:当a.b.c变化时会触发第二个参数中的函数

vm.$watch('a.b.c', function (newVal, oldVal) {
    // 操作
})

根据使用方法我们可以首先实现Watcher:

export default class Watcher {
    constructor(vm, expOrFn, cb) {
        this.vm = vm
        this.getter = parsePath(expOrFn)
        this.cb = cb
        this.value = this.get()
    }
    
    get() {
        window.target = this
        let value = this.getter.call(this.vm, this.vm)
        windwo.target = undefined
        return value
    }
    
    update() {
        const oldValue = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
    }
}

这段胆码可以把自己主动添加到data.a.b.c的Dep中。在get方法中先把window.target设置为this即当前的watcher实例,然后读data.a.b.c的值,触发getter。

触发了getter,就会触发收集依赖的逻辑,从window.target中读取一个依赖添加到Dep中。这样只要在window.target赋一个this,再读一下值,触发getter,就可以把this主动添加到keypath的Dep中。

依赖注入到Dep中后,每次值变化就会让依赖列表中所有依赖循环触发update方法,执行参数中的回调函数,将value和oldValue传到参数中。

递归侦测所有key

现在已经可以实现变化侦测的功能,但是我们希望把数据中所有属性(含子属性)都侦测到,需要一个Observer类。它的作用是将一个数据内所有属性都转换成getter/setter形式,再去追踪他们的变化

export class Observer {
    constructor(value) {
        this.value = value
        
        if(!Array.isArray(value)) {
            this.walk(value)
        }
    }
    
    walk() {
        const keys = Object.keys(obj)
        for(let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }
}

function defineReactive(data, key, val) {
    if(typeof val === 'object') {
        new Observer(val)
    }
    
    let dep = new Dep()
    Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
        dep.depend()
        return val
    },
    set: function () {
        if(val === newVal) {
           return 
        }
        val = newVal
        dep.notify()
        }
    })
}

我们定义Observer类,将正常对象转换成被侦测的对象。然后判断数据类型,只有Object才会调用walk进行转换,最后在defineReactive中新增new Observer(val)来递归子属性。

关于Object的问题

上边我们介绍了Object类型的变化侦测原理,正是因为数据变化是通过getter/setter进行追踪,所以有些语法中即使数据变化,Vue也追踪不到。

比如,向object添加或删除属性:

var vm = new Vue({
    el:'#app',
    template:'#demo-template',
    methods:{
        action() {
            this.obj.age = '23'
        },
        dAction() {
            delete this.obj.name
        }
    },
    data: {
        obj:{
            name:'abc'
        }
    }
})

Object.defineProperty将对象的key转化为getter/setter来追踪变化,但它只能追踪一个数据是否被修改,无法侦测新增和删除。

所以Vue提供两个API:vm.setvm.set和vm.delete

总结

Object可以通过Object.defineProperty将属性转换成getter/setter形式来追踪变化,读取触发getter,修改触发setter。

在getter中对使用了数据的依赖进行收集,当setter触发时通知getter中收集的依赖数据发生变化。

收集依赖需要为依赖找一个储存的地方,所以有了Dep,它用来收集、删除依赖并向依赖发送消息。

依赖就是Watcher,只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter就把它收集到Dep中,数据发生变化时会循环依赖列表通知所有Watcher。

Watcher的原理是先把自己设置到全局唯一的位置,然后读取数据触发getter,接着getter中就会从唯一的位置读取当前正在读取数据的Watcher,并把它收集到Dep中,这样Watcher可以主动去订阅任一数据的变化。

此外,还创建了Observer类,它是用来将一个object中的所有数据(包括子数据)都转换成响应式的。

Array的变化侦测

Array的侦测为什么与Object不同,因为Object使用getter/setter侦测变化,而数组通常使用push等方法来改变,不会触发getter/setter。

如何追踪变化

这里不讨论ES6的情况,在ES6以前,我们没有能够拦截原型方法(push等)的能力,但我们可以用自定义方法覆盖原生的原型方法。

我们可以用一个拦截器覆盖Array.prototype。之后每当使用原型上的方法操作数组的时候,执行的其实是拦截器中的方法,之后在拦截器内使用原生的原型方法去操作数组。

拦截器

数组原型上的方法有七个,分别是push,pop,shift,unshift,splice,sort和reverse。我们可以实现一个和原型样的Object,里面的属性一模一样,只不过其中改变数组自身内容的方法是重写的。

const arrayProto = Array.prototype

export const arrayMethods = Object.create(arrayProto)

const methods = ['push','pop','shift','unshift','splice','sort','reverse']

methods.forEach((method) => {
    const original = arrayProto[method]
    
    Object.defineProperty(arrayMethods , method , {
        value: function mutator(...args) {
            return original.apply(this, args)
        },
        enumerable:false,
        writable: true,
        configurable:true
    })
})

创建了arrayMethods继承自Array.prototype,具备所有功能,接下来使用Object.defineProperty对改变数组的方法进行封装。所以当我们调用push的时候,实际调用的是arrayMethods.push,而它实际上是mutator函数,在mutator函数中执行original(原生方法)来做应该做的事,我们可以在mutator中做其他的事比如发送变化通知。

使用拦截器覆盖Array原型

我们需要用它覆盖Array.prototype,但又不能直接覆盖,因为这样会污染全局的Array。我们希望拦截只针对那些被侦测了变化的数据生效,也就是希望拦截器只覆盖那些响应式数组的原型。

而数据转成响应式的,需要通过Observer,所以我们只需要在Observer中用拦截器覆盖那些Array类型数据的原型。

export class Observer {
    constructor(value) {
        this.value = value
        
        if(Array.isArray(value)) {
            value._proto_ = arrayMethods
        } else {
            this.walk(value)
        }
    }
}

它的作用是将拦截器赋给value.proto,覆盖value原型的功能。

拦截器挂载到数组属性上

部分浏览器可能不支持_proto_,而Vue的做法是如果不能使用_proto_,则直接将arrayMethods身上的方法设置到被侦测的数组上:

const hasProto = '_proto_' in {}

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

export class Observer {
    constructor(value) {
        this.value = value
        
        if(Array.isArray(value)) {
            const augment = hasProto ? protoAugment : copyAugment
            
            augment(value, arrayMethods, arrayKeys)
        } else {
            ...
        }
    }
    
    ...
}

function protoAugment(target, src, keys) {
    target._proto_ = src
}

function copyAugment(target, src, keys) {
    for(let i = 0,l = keys.length;i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])
    }
}

如何收集依赖

我们已完成了拦截器,本质上是为了当数组内容变化时能够得到通知Dep中的依赖的能力。而数组也是在getter中收集依赖的。想要读取数组,首先肯定会触发这个数组的名对应属性的getter。而Array的依赖和Object一样,也会在defineReactive中收集:

function defineReactive(data, key, val) {
    if(typeof val === 'object') new Observer(val)
    
    let dep = new Dep()
    
    Object.deintProperty(data,key, {
        enumerable:true,
        configurable:true,
        get:function() {
            dep.depend()
            // 收集数组的依赖
            
            return val
        },
        set:function(newVal) {
            if(val === newVal) return
            
            dep.notify()
            val = newVal
        }
    })
}

Array在getter中收集依赖,在拦截器中触发依赖。

依赖列表存在哪

Array的依赖被存放在Observer中:

export class Observer {
    constructor(value) {
        this.value = value
        this.dep = new Dep() // 新增dep
        
        if(Array.isArray(value)) {
            const augment = hasProto ? protoAugment : copyAugment
            
            augment(value, arrayMethods, arrayKeys)
        } else {
            ...
        }
    }
    
    ...
}

我们有个疑问,为什么数组的dep要保存在Observer实例上呢。因为数组在getter中收集依赖,在拦截器中触发依赖,所以依赖保存的位置很关键,需要在getter和拦截器中都能访问到。将Dep实例保存到Observer的属性上以后,我们能够在getter中访问并收集。

function defineReactive(data, key, val) {
    let childOb = observer(val)
    let dep = new Dep()
    Object.defineProerty(data, key, {
        enumerable:true,
        configurable:true,
        get:function() {
            dep.depend()
            if(childOb) {
                childOb.dep.depend()
            }
            return val
        },
        set:function(newVal) {
            if(val === newVal) {
                return
            }
            
            dep.notify()
            val = newVal
        }
    })
}

在defineReactive中调用observe,将val当做参数传进去拿到返回值,就是observer实例。

在拦截器中获取Observer实例

因为Array拦截器是对原型的一种封装,所以在拦截器中可以访问到this。而dep保存在Observer中,所以需要在this上读到Observer实例。

function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key,  {
        value:val,
        enumerable:!!enumerable,
        writable:true,
        configruable:true
    })
}

export class Observer {
    constructor(value) {
        this.value = value
        this.dep = new Dep()
        
        def(value , '__ob__',this)
        
        if(Array.isArray(value)) {
            ...
        } else {
            ...
        }
    }
    ...
}

上述代码在Observer中新增了一段代码,它在value上新增了一个不可枚举的__ob__属性即当前Observer实例。它还可以用来标记当前value是否被Observer转换成响应式数据。

也就是说所有被侦测了的数据身上都会有一个__ob__来表示它们是响应式的,可以通过value.__ob__来访问Observer实例。如果是Array拦截器,因为它是原型方法,所以可以直接通过this.__ob__来访问Observer实例。


['push','pop',……].forEach(function(method) {
    const original = arrayProto[method]
    Object.defineProperty(arrayMethods,method, {
        value:function mutator(...args) {
            const ob = this.__ob__
            return original.apply(this,args)
        },
        ...
    })
})

向数组依赖发送通知

我们只需要在拦截器中访问Observer实例,拿到dep属性,直接发送通知即可。


['push','pop',……].forEach(function(method) {
    const original = arrayProto[method]
    Object.defineProperty(arrayMethods,method, {
        value:function mutator(...args) {
            const ob = this.__ob__
            ob.dep.notify()
            return original.apply(this,args)
        },
        ...
    })
})

侦测数组元素变化及新增变化

元素变化

介绍Observer时说过,作用是将object的属性变为getter/setter形式,现在Observer类不仅处理Object类型,还要处理Array类型。所以我们要在Observer中新增一些处理,让它能把Array也变成响应式:

export class Observer {
    constructor(value) {
        this.value = value
        def(value,'__ob__',this)
        if(Array.isArray(value)) {
            this.observeArray(value)
        } else {
            this.walk(value)
        }
    }
    
    ...
    
    observeArray(items) {
        for(let i = 0,l = items.length;i < l; i++) {
            observe(items[i])
        }
    }
}

新增元素的变化

  1. 获取新增元素

我们需要在拦截器中队数组方法的类型进行判断,如果是push、unshift和splice(添加元素方法),需要把参数中新增的元素拿过来,用Observer侦测。

['push','pop',……].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
        }
        ob.dep.notify()
        return result
    })
})

关于数组的问题

对Array的变化侦测是通过拦截原型的方法实现,所以有些操作Vue无法拦截,比如:

this.list[0] = 2
this.list.length = 0

无法侦测数组变化,不会触发re-render或者watch。

总结

Array的追踪方式和Object不同,因为它是通过方法来改变内容,所以通过创建拦截器去覆盖数组的原型方法来追踪变化。

而为了不污染全局的原型方法,我们在Observer中只针对需要侦测变化的数组用__proto__来覆盖原型方法,而ES6之前并不是所有浏览器都支持,所以针对不支持的浏览器,我们循环拦截器,将方法设置到数组身上来拦截Array.prototype上的原型方法。

Array同Object收集依赖的方式相同,均在getter中,但是使用依赖的位置不同,数组要在拦截器中向依赖发送消息,所以依赖不能像Object一样保存在defineReactive中,而是保存在Observer实例上。

在Observer中队每个侦测了变化的数据都标记__ob__,并把this保存在__ob__上。主要有两个作用,一方面为了标记数据是否被侦测变化(保证同一数据只侦测一次),另一方面可以很方便的通过数据取到__ob__,从而拿到Observer上保存的依赖,发送通知。

除了侦测数组自身变化,数组中元素的变化也要侦测,在Observer中判断如果当前被侦测数据是数组,则调用observerArray将数组中每个元素转换成响应式。

除了已有数据的侦测,当使用push等方法新增数据时,新增的数据也要侦测,我们使用当前操作数组的方法判断,如果是push、unshift和splice,则将参数中的新增数据提取出来,对其转换。

变化侦测相关的api实现原理

vm.$watch

用法

vm.$watch(expOfFn, callback, [options])

用于观察一个表达式或computed函数在vue实例上的变化。回调函数调用时会从参数得到新数据和旧数据,表达式只接受以点分隔的路径,例如a.b.c

options包括deep和immediate,deep为了发现对象内部值的变化,immediate将立即以表达式的当前值触发回调。

vm.$watch('someObject',callback, {
    deep:true, // someObject.a修改时触发。
    immediate:true // 立即以someObject的当前值触发回调。
})

内部原理

vm.watch是对Watcher的一种封装,通过Watcher完全可以实现vm.watch是对Watcher的一种封装,通过Watcher完全可以实现vm.watch的功能,但它的deep和immediate是Watcher没有的。

Vue.property.$watch = function(expOrFn, cb, options) {
    const vm = this
    options = options || {}
    const watcher = new Watcher(vm, expOfFn, cb, options)
    if(options.immediate) {
        cb.call(vm, watcher)
    }
    return function unwatchFn() {
        watcher.teardown()
    }
}

expOrFn是支持函数的,而之前介绍Watcher时没有添加这部分,需要对Watcher简单修改。

export default class Watcher {
    constructor(vm, expOrFn, cb) {
        this.vm = vm
        
        //修改
        if(typeof expOrFn === 'function') {
            this.getter = expOrFn
        } else {
            this.getter = parsePath(expOrFn)
        }
        
        this.cb = cb
        this.value = this.get()
    }
}

如果expOrFn是函数,将它直接赋给getter,如果不是,则用parsePath读取keypath中的数据,keypath是属性路径,例如a.b.c.d就代表从vm.a.b.c.d中读取数据。

而expOrFn是函数时,不止可以动态返回数据,其中读取的数据也都会被Watcher观察。当expOrFn是字符串类型的keypath时,Watcher会读取它指向的数据并观察数据的变化。而expOrFn是函数时,Watcher会同时观察expOrFn函数中读取的所有Vue实例上的响应式数据。

执行new Watcher后,代码会判断用户是否用了immediate参数,使用了则立即执行一次cb。

最后返回一个函数unwatcheFn,它的作用是取消观察数据。用户执行它时,实际上是执行了watcher.teardown()来取消观察数据,其本质是把watcher实例从当前正在观察的状态的依赖列表中移除。

现在需要实现watcher中的teardown方法,来实现unwatch功能。

首先需要在Watcher中记录自己都订阅了谁,当Watcher不想继续订阅时,循环自己记录的列表来通知他们将自己从他们的依赖列表中移除。

export default class Watcher {
    constructor(vm, expOrFn, cb) {
        this.vm  = vm
        this.deps = []
        this.depsIds= new Set()
        ……
    }
    
    ……
    
    addDep(dep) {
        const id = dep.id
        if(!this.depIds.has(id)) {
            this.depIds.add(id)
            this.deps.push(dep)
            dep.addSub(this)
        }
    }
}

上述代码中,用depIds判断当前Watcher是否订阅了Dep,不会发生重复订阅。

接着执行this.depIds.add来记录当前Watcher已经订阅了这个Dep。然后执行this.deps.push(dep)记录自己订阅了那些Dep。最后触发dep.addSub(this)将自己订阅到Dep中。

Watcher中新增了addDep方法后,Dep中收集依赖的逻辑也需要改变:

let uid = 0

export default class Dep {
    constructor() {
        this.id = uid++
        this.subs = []
    }
    
    ……
    
    depend() {
        if(window.target) {
            window.target.addDep(this)
        }
    }
}

Dep会记录数据发生变化的时候,需要通知哪些Watcher,而Watcher中也记录了自己会被哪些Dep通知,是多对多的关系。

在Watcher中记录了自己都订阅了那些Dep后,可以在Watcher中增加teardown方法来通知订阅的Dep,让他们把自己从依赖中移除:

teardown() {
    let i = this.deps.length
    while(i--) {
        this.deps[i].removeSub(this)
    }
}

export default class Dep {
    ……
    
    removeSub(sub) {
        const index = this.subs.indexOf(sub)
        if(index > -1) {
            return this.subs.splice(index, 1)
        }
    }
}

deep参数实现原理

deep的 作用是监听它及其子对象的数据,其实就是除了要触发当前被监听数据的收集依赖逻辑外,还要把当前监听值在内的所有子值都触发一遍收集依赖逻辑。

export default class Watcher {
    constructor(vm, expOrFn, cb, options) {
        this.vm = vm
        // 新增
        if(options) {
            this.deep = !!options.deep
        } else {
            this.deep = false
        }
        
        ……
    }
    
    get() {
        window.target = this
        let value = this.getter.call(vm, vm)
        
        //新增
        if(this.deep) {
            traverse(value)
        }
        windwo.target = undefined
        return value
    }
}

如果使用了deep参数,则在target = undefined前调用traverse来处理deep的逻辑。

const seenObjects = new Set()
export function traverse(value) {
    _traverse(val, seenObjects)
    seenObjects.clear()
}

function _traverse(val, seen) {
    let i , keys
    const isA = Array.isArray(val)
    if( (!isA && !isObject(val)) || (Object.isFrozen(val)) ) {
        return
    }
    
    if(val.__ob__) {
        const depId = val.__ob__.dep.id
        if(seen.has(depId)) {
            return
        }
        seen.add(depId)
    }
    if(isA) {
        i = val.length
        while(i--) _traverse(val[i], seen)
    } else {
        keys =Object.keys(val)
        i = keys.length
        while(i--) _traverse(val[keys[i]], seen)
    }
}

利用递归来判断子值的类型,数组则直接循环递归调用_traverse,而对象则利用key读取并递归子值,而val[keys[i]]会触发getter,所以要在target = undefined之前触发收集依赖的原因。

vm.$set

用法

vm.$set(target, key, value)

在object上设置一个属性,如果object是响应式的,Vue保证属性在创建后也是响应式的,并能够触发视图更新。此方法为了避开Vue不能侦测属性被添加的限制。

target 不能是Vue实例或Vue实例的根数据对象。

举个例子来看看

var vm = new Vue({
    el:'#el',
    template:'#demo-template',
    data:{
        obj
    },
    methods: {
        action() {
            this.obj.name = 'abc'
        }
    }
})

调用action方法时,会为obj新增一个name属性,但Vue不会得到任何通知,新增的属性也不是响应式的,Vue不知道这个obj新增了属性等同于不知道我们使用了array.length = 0来清空数组一样。而vm.$set就用来解决这类问题。

Array的处理

首先创建set方法,并规定接收与$set规定的参数一致的三个参数,并对数组进行处理。

export function set(target, key, val) {
    if(Array.isArray(target) && isValidArrayIndex(key) {
        target.length = Math.max(target.length, key)
        target.splice(key, 1, val)
        return val
    }
}

上述代码中,如果target是数组并且key是有效的索引值,就先设置length属性,这样如果我们传的索引值大于length,就需要让target的lenth等于索引。接下来通过splice方法把val设置到target的指定位置,当我们使用splice方法时,数组拦截器会侦测到target变化,自动把新增的val转换成响应式的,最后返回val。

key已经存在target中

因为key已存在target中,所以它已经被侦测了变化,此时修改数据直接用key和val就好,修改的动作会被侦测到。

export function set(target, key, val) {
    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
    }
}

处理新增的属性

现在处理在target上新增的key:

export function set(target, key, val) {
    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.__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
}

上述代码,首先获取了target的__ob__属性,然后处理边界条件即“target不能是Vue实例或Vue实例的跟数据对象”和target不是响应式的情况。若果它身上没有__ob__,则不是响应式的,直接用key和val设置即可。如果前边所有条件都不满足那么说明这是新增的属性,使用defineReactive将新增的属性转成getter/setter即可。

vm.$delete

vm.$delete的作用是删除数据中的某个属性,因为Vue2采用Object.defineProperty实现监听,delete关键字删除无法侦测到。

用法

vm.$delete(target , key)

删除对象的属性,如果对象是响应式的,需要确保删除能更新视图。同样的,目标不能是Vue实例或Vue实例的跟数据对象。

实现原理

vm.$delete的实现原理和上述代码类似,删除属性后向依赖发消息。

export function del(target, key) {
    if(Array.isArray(target) && isValidArrayIndex(key)) {
        target.splice(key , l)
        return
    }
    
    const ob = (target).__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()
}