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

变化侦测

什么是变化侦测?

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

object的变化侦测

如何追踪变化?

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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,使用这个类,我们可以收集、删除依赖或向依赖发送通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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变化时会触发第二个参数中的函数

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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形式,再去追踪他们的变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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添加或删除属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.$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,里面的属性一模一样,只不过其中改变数组自身内容的方法是重写的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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类型数据的原型。

1
2
3
4
5
6
7
8
9
10
11
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身上的方法设置到被侦测的数组上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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中收集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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中访问并收集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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实例。

1
2
3
4
5
6
7
8
9
10
11
12

['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属性,直接发送通知即可。

1
2
3
4
5
6
7
8
9
10
11
12
13

['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也变成响应式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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侦测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
['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无法拦截,比如:

1
2
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将立即以表达式的当前值触发回调。

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

内部原理

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

1
2
3
4
5
6
7
8
9
10
11
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简单修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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不想继续订阅时,循环自己记录的列表来通知他们将自己从他们的依赖列表中移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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中收集依赖的逻辑也需要改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,让他们把自己从依赖中移除:

1
2
3
4
5
6
7
teardown() {
let i = this.deps.length
while(i--) {
this.deps[i].removeSub(this)
}
}

1
2
3
4
5
6
7
8
9
10
export default class Dep {
……

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

deep参数实现原理

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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实例的根数据对象。

举个例子来看看

1
2
3
4
5
6
7
8
9
10
11
12
13
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规定的参数一致的三个参数,并对数组进行处理。

1
2
3
4
5
6
7
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就好,修改的动作会被侦测到。

1
2
3
4
5
6
7
8
9
10
11
12
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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的实现原理和上述代码类似,删除属性后向依赖发消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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()
}

w字总结《深入浅出Vue.js》之变化侦测篇
https://moewang0321.github.io/2021/12/06/w字总结《深入浅出Vue.js》之变化侦测篇/
作者
Moe Wang
发布于
2021年12月6日
许可协议