easycodesniper

blog by chen qiyi

Vue2数据响应式原理

Object.defineProperty()方法

Object.defineProperty()的作用是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性

1
Object.defineProperty(obj, prop, desc)

obj:需要操作的当前对象
prop:当前需要操作的属性名
desc:属性描述符

属性描述符
通过Object.defineProperty()为对象定义属性,有两种形式,且不能混合使用,分别为数据描述符,存取描述符

  1. 数据描述符(value, writabl)
    value就是属性的值,
    writable用来设置属性的值是否可以被改变(默认为false),
    enumerable用来设置属性是否可以被枚举(默认为false)

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let obj = {}

Object.defineProperty(obj, 'a', {
value: 123
})

Object.defineProperty(obj, 'b', {
value: 456,
writable: true
})

console.log(obj);
obj.a ++
obj.b ++
console.log(obj);

举第二个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let obj = {}

Object.defineProperty(obj, 'a', {
value: 123,
enumerable: false
})

Object.defineProperty(obj, 'b', {
value: 456,
writable: true,
enumerable: true
})

Object.defineProperty(obj, 'c', {
value: 789,
enumerable: true
})

console.log(obj);

for(let k in obj) {
console.log(k);
}
  1. 存取描述符(get,set)
    get:该方法的返回值被用作属性值,默认值为undefined
    set:该方法接收唯一参数,并将该唯一参数赋值为属性的新值,默认值为undefined

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let obj = {}
let num = 123
Object.defineProperty(obj, 'a', {
get() {
console.log('你试图访问a属性')
return num
},
set(val) {
console.log('你试图改变a属性')
num = 456
}
})
console.log(obj.a);
obj.a = 456
console.log(obj.a);

数据描述符和存取描述均具有以下描述符
enumerable用来设置属性是否可以被枚举(默认为false)
configrable 描述属性是否配置,以及可否删除

defineReactive函数

在Object.defineProperty()的存取描述符中,你需要定义一个变量来周转属性值,如果没有定义这个变量,你通过set设置的新值在get中无法读取

1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {}
Object.defineProperty(obj, 'a', {
get() {
console.log('你试图访问a属性')
return 123
},
set(val) {
console.log('你试图改变a属性',val)
}
})
console.log(obj.a);
obj.a = 456
console.log(obj.a);

这样写会导致代码不是那么优雅,所以我们用一个函数将Object.defineProperty()包裹起来,形成一个闭包,下面代码中第三个参数val就是这个闭包的核心,它很好的解决了前面的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let obj = {}

function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log('你试图访问a属性')
return val
},
set(newvalue) {
console.log('你试图改变a属性',newvalue)
val = newvalue
}
})
}
defineReactive(obj, 'a', 10)

console.log(obj.a);
//我们改变a的值,会调用set方法进行赋值
obj.a = 100
console.log(obj.a);

现在的defineReactive无法为每一层的数据都添加get和set方法

举个例子:

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
//obj对象本身就包含了a属性,且有好几层
let obj = {
a: {
m: {
n: 5
}
}
}
function defineReactive(data, key, val) {
//判断,当传入的参数是两个时,val就等于对象data的本身值
if(arguments.length == 2) {
val = data[key]
}
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log('你试图访问' + key +'属性')
return val
},
set(newvalue) {
console.log('你试图改变' + key + '属性',newvalue)
val = newvalue
}
})
}
//这里a是一个对象,不可以重新给a赋值,先不传第三个参数
defineReactive(obj, 'a')
console.log(obj.a.m.n);

控制台打印的结果:打印的是试图访问a属性,而不是n属性,所以对n属性的改变无法监视到

递归侦测对象全部属性

基于上面defineReactive()的问题,我们在这里创建一个Observer(中文解释:观察者,观测者)类,它的作用是将一个正常的object对象转化成每一层属性都是响应式(可以被侦测到)的object对象

我们先定义一个observe函数(函数名没有 r),给他传入一个对象,observe首先会在它身上寻找有没有__ob__属性,如果没有,调用Observer类实例化一个ob对象,将这个ob对象添加到__ob__身上(这个__ob__属性在后面会用到)

observe函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Observer from './Observer'

//创建一个observe函数,注意没有r
export const observe = function(value) {
//如果传入的value不是一个对象,那么什么也不做
if(typeof value != 'object') return

//定义ob,用于储存observer类的实例
var ob
if(typeof value.__ob__ != 'undefined') {
ob = value.__ob__
} else {
ob = new Observer(value)
}

return ob
}

Observer类
Observer类的作用是将一个对象转化成每一层都是响应式的对象
def是一个工具函数,用来给传入的对象加上一个__ob__属性,定义它主要是为了简化Observer类中的代码,使逻辑清晰

1
2
3
4
5
6
7
8
9
10
11
12
/*
工具函数
*/
export const def = function(obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
//设置__ob__属性不可遍历
enumerable,
writable: true,
configurable: true
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import {def} from './utils'
import defineReactive from './defineReactive';

export default class Observer {
constructor(value) {
//给实例加上__ob__属性
//一定要注意,构造函数中的this不是表示类本身,而是表示实例
//添加__ob__属性,值是这次new的实例
def(value, '__ob__', this, false)
this.walk(value)
}
//遍历
//为子元素进行侦测,变成响应式
walk(value) {
for(let k in value) {
defineReactive(value, k)
}
}
}

defineReactive()函数

在defineReactive()函数中会调用observe()函数,给它的子元素添加__ob__属性,并在observe中会调用Observer类,对它的子元素进行侦测,实现响应式,也是在此处形成三个函数的递归调用

注意:当个某个元素赋值时,对新的值也要调用observe,因为新值可能是一个对象,如果是一个对象,对它里面的属性也要侦测

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
import { observe } from "./observe"

export default function defineReactive(data, key, val) {
//判断,当传入的参数是两个时,val就等于对象data的本身值
if(arguments.length == 2) {
val = data[key]
}
//子元素要进行observe,至此形成了递归,这个递归是observe, Observer, defineReactive三者之间的递归
let childOb = observe(val)

Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log('你试图访问' + key +'属性')
return val
},
set(newvalue) {
console.log('你试图改变' + key + '属性',newvalue)
val = newvalue
//接着观察传入的新值,因为新值可能是一个对象,里面会有新的属性
childOb = observe(newvalue)
}
})
}

流程图:

我们用一个例子来检测一下能否侦测对象的全部属性:

至此完成了对对象全部属性的侦测,但是,现在还不能对数组进行响应式处理

数组的响应式处理

在Vue的底层,为了实现对数组的响应式处理,将数组的七个方法 (push, pop, shift, unshift, splice, sort, reverse)进行了改写

这些方法都被定义在Array.prototype中,Vue根据Array.prototype为原型创建一个arrayMethods对象,在arrayMethods中对七个方法进行改写,并且利用ES6中的Object.setPrototypeOOf()让Array实例的__proto__强制的指向这个arrayMethods对象

首先我们创建一个array.js对这7个方法进行改写。
在下面的代码中,我们先以Array的原型为原型创建一个对象。
然后遍历要改写的方法,对他们进行逐一改写。使用forEach进行遍历,先将这7个方法备份(以便在后面恢复它们的原始功能),然后调用def将arrayMethods对象上的这7个方法进行重写。
在这里我们先恢复7个方法原理的功能,然后让他打印一些东西来测试是否可用,

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
import {def} from './utils'
//获取Array的原型
const arrayPrototype = Array.prototype

//以Array.prototype为原型创建arrayMethods对象
export const arrayMethods = Object.create(arrayPrototype)

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

methodsChange.forEach(method => {
//备份原来的方法
const original = arrayPrototype[method]
//定义新的方法
def(arrayMethods, method, function(){
//恢复原来的功能
//因为是某个数组调用方法,此处的this指向调用它的数组的上下文,
//此处的函数不能用箭头函数:1.箭头函数没有arguments 2.箭头函数的上下文不明确
//要记得返回原来方法的值,因为像pop等方法会返回删除的值
const result = original.apply(this, arguments)
console.log('哈哈哈');

return result
}, false)
})

接下来我们在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
25
26
27
28
29
30
31
32
33
34
import {def} from './utils'
import defineReactive from './defineReactive';
import { arrayMethods } from './array'

export default class Observer {
constructor(value) {
//给实例加上__ob__属性
//一定要注意,构造函数中的this不是表示类本身,而是表示实例
//添加__ob__属性,值是这次new的实例
def(value, '__ob__', this, false)

if(Array.isArray(value)) {
//如果是数组,将数组的原型强行指向arrayMethods
Object.setPrototypeOf(value, arrayMethods)
//让数组的每一项也observe一下,因为它们可能是对象或者包含对象
this.observeArr(value)
} else {
this.walk(value)
}
}
//遍历
walk(value) {
for(let k in value) {
defineReactive(value, k)
}
}

//数组遍历
observeArr(arr) {
for(let i = 0, l = arr.length; i < l; i++) {
observe(arr[i])
}
}
}

让我们来做一些阶段性的测试,看看上面的改动是否有效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {observe} from './observe'
let obj = {
a: {
m: {
n: 100
}
},
b: [1,2,3]
}

observe(obj)

obj.b.push(4)

console.log(obj.b);

我们接下来还需要对push,unshift,splice做一些特殊的处理,因为这个三个方法都可以插入新值,我们也需要监测新值(新值可能是对象或者包含着对象)

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
40
41
42
43
44
45
46
47
48
import {def} from './utils'
const arrayPrototype = Array.prototype

//以Array.prototype为原型创建arrayMethods对象
export const arrayMethods = Object.create(arrayPrototype)

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

methodsChange.forEach(method => {
//备份原来的方法
const original = arrayPrototype[method]
//定义新的方法
def(arrayMethods, method, function(){

const result = original.apply(this, arguments)
//arguments是一个类数组对象,需要将她变成数组。在之后splice方法的改写中要用到数组上的方法
const args = [...arguments]

const ob = this.__ob__
let inserted = []

switch(method) {
case 'push':
case 'unshift':
inserted = args;
break;
//splice可以传入三个参数,第三个参数才是要替换或者插入的项
case 'splice':
inserted = args.slice(2);
break;
}
//如果是有要插入的项,将要插入的项也observe一下,因为可能插入的项是或者有对象
if(inserted) {
ob.observeArr(inserted)
}

return result
}, false)
})

这样我们就实现了对数组的响应式,通过一些例子来测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {observe} from './observe'
let obj = {
a: {
m: {
n: 100
}
},
b: [1,2,3]
}

observe(obj)

obj.b.splice(2, 0, 1234)

console.log(obj.b);

obj.b.push({
name: 'cqy'
})

console.log(obj.b);

依赖收集

需要用到数据的地方,称为依赖。
在Vue 2.x 中,用到数据的组件叫依赖
依赖要被收集起来,当数据发生变化时通过循环通知所有依赖
在用到数据的地方就会触发getter,当改变数据时会触发setter,所以就有一个结论:

在getter中收集依赖,在setter中触发依赖

把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个Observer的实例中都有一个Dep的实例。

在这里我们先创建一个Dep类,

1
2
3
4
5
export default class Dep {
constructor() {}
//当数据发生更新时,通知组件
notify() {}
}

并在Observer实例中实例化Dep类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default class Observer {
constructor(value) {
//每个Observr的实例上都有一个Dep实例
this.dep = new Dep()

//....
}
//遍历
walk(value) {
for(let k in value) {
defineReactive(value, k)
}
}

//数组遍历
observeArr(arr) {
for(let i = 0, l = arr.length; i < l; i++) {
observe(arr[i])
}
}
}

除此之外,我们需要在可能发生数据更新的地方都调用Dep的notify()方法来通知组件

在defineReactive中调用

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 default function defineReactive(data, key, val) {
const dep = new Dep()

if(arguments.length == 2) {
val = data[key]
}
let childOb = observe(val)

Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
// ...
},
set(newvalue) {
console.log('你试图改变' + key + '属性',newvalue)
val = newvalue
childOb = observe(newvalue)

//数据更改时发布订阅模式,通知dep
dep.notify()
}
})
}

在数组的响应式中调用

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
methodsChange.forEach(method => {
//备份原来的方法
const original = arrayPrototype[method]
//定义新的方法
def(arrayMethods, method, function(){

const result = original.apply(this, arguments)
const args = [...arguments]

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.observeArr(inserted)
}
//数据更改时发布订阅模式,通知dep
ob.dep.notify()

return result
}, false)
})

这样设置之后,当我们改变数据就会被Dep监测,然后发布订阅模式通知组件

让我们接着来看Dep类:Dep类中的depend方法用于添加依赖,当全局位置Dep.target(之后会说明)存在时,会调用addSubs方法将这个全局位置存入subs数组。当数据发生改变时,在前面我们已经知道会调用notify方法,而它又会调用Watcher类中的update方法(subs数组存放的是Watcher实例)

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
export default class Dep {
constructor() {
//创建数组来存储自己的订阅者
//这个数组中放的是Watcher的实例
this.subs = []
}
//添加订阅
addSubs(sub) {
this.subs.push(sub)
}
//添加依赖
depend() {
//Dep.target是我们指定的一个全局位置
if(Dep.target) {
this.addSubs(Dep.target)
}
}
//通知更新,当数据发生更新时通知订阅的依赖
notify() {
//浅克隆一份subs
const subs = this.subs.slice()

for(let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

Watcher类是一个观察者对象。依赖收集以后的Watcher实例被保存在Dep的subs中,数据变动的时候Dep会通知Watcher实例,然后由Watcher实例调用回调函数进行视图更新。

1
2
3
4
5
6
7
export default class Watcher {
//构造函数传入三个参数:要监听的对象,这个对象的属性, 回调函数
constructor(target, expression, callback) {
this.obj = target
this.callback = callback
}
}

我们来模拟一下Watcher观察某个数据的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let obj = {
a: {
m: {
n: 100
}
},
b: [1,2,3]
}

observe(obj)
//传入的对象属性是字符串模式,需要在Watcher中将字符串解析为点语法
new Watcher(obj, 'a.m.n', (val) => {
console.log("@@@@", val);
})

将字符串解析为点语法的函数

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 default class Watcher {
//构造函数传入三个参数:要监听的对象,这个对象的属性, 回调函数
constructor(target, expression, callback) {
this.obj = target
this.callback = callback
this.getter = parsePath(expression)
}
}
//这个解析函数将字符串按照"."进行拆分成数组,返回一个函数。
//在Watcher的构造函数中用变量getter接收这个函数,
//到时候我们只需要将构造函数传入的target(监听对象)传入getter函数就可以将解析好的点语法拼接到对象后面

//在这个例子中,我们调用this.getter(this.obj)就可以得到 obj.a.m.n
function parsePath(str) {
let segments = str.split('.')

return (obj) => {
for(let i=0; i<segments.length; i++) {
obj = obj[segments[i]]
}

return obj
}
}

当Watcher实例化之后就会去读取相应的数据,那么这个时候会触发数据的getter,我们需要在这个时候将全局位置存入Dep的subs数组中

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 default function defineReactive(data, key, val) {
const dep = new Dep()
if(arguments.length == 2) {
val = data[key]
}
let childOb = observe(val)

Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log('你试图访问' + key +'属性')
//如果处于依赖收集阶段
if(Dep.target) {
dep.depend()
}
return val
},
set(newvalue) {
//....
dep.notify()
}
})
}

我们在构造函数中定义变量value用来存储要观测的值,实例化时value会调用get()方法,在这个方法中,将Watcher实例本身设置为全局位置,并调用getter来获取要存储的值。
当这次的全局位置存入Dep中之后,记得将全局位置设置为null,为之后观测其他值做准备

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
export default class Watcher {
//构造函数传入三个参数:要监听的对象,这个对象的属性, 回调函数
constructor(target, expression, callback) {
this.obj = target
this.getter = parsePath(expression)
this.callback = callback
this.value = this.get()
}
//进入依赖收集阶段
get() {
//将Dep.target设置为Watcher本身,那么就进入依赖收集模式
Dep.target = this

var value
value = this.getter(this.obj)

//当依赖收集完成时,要将Dep.target设置为null,用于之后的依赖收集
//退出依赖收集阶段
Dep.target = null

return value
}
}

function parsePath(str) {
let segments = str.split('.')

return (obj) => {
for(let i=0; i<segments.length; i++) {
obj = obj[segments[i]]
}

return obj
}
}

如前所述,当监测的数据发生了更新,就会触发数据的setter,在setter中会调用dep的notify()方法,发布订阅模式,通知相应组件更新视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default class Dep {
constructor() {
this.subs = []
}
//...
//通知更新,当数据发生更新时通知订阅的依赖
notify() {
//浅克隆一份subs
const subs = this.subs.slice()

for(let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

Dep的notify方法又会调用Watcher实例的update方法,update方法又会调用getAndInvoke方法,这个方法会再调用一次get方法来得到新的值,然后通过Watcher实例化时传入的回调函数进行视图更新

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
export default class Watcher {
//构造函数传入三个参数:要监听的对象,这个对象的属性, 回调函数
constructor(target, expression, callback) {
this.obj = target
this.getter = parsePath(expression)
this.callback = callback
this.value = this.get()
}
//数据发生更新时调用
update() {
this.getAndInvoke(this.callback)
}
//进入依赖收集阶段
get() {
//将Dep.target设置为Watcher本身,那么就进入依赖收集模式
Dep.target = this
console.log("全局位置",Dep.target);

var value
value = this.getter(this.obj)

Dep.target = null

return value
}

getAndInvoke(cb) {
const value = this.get()

if(value !== this.value || typeof value == 'object') {
const oldValue = this.value
this.value = value
cb.call(this.obj, value, oldValue)
}
}
}

进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let obj = {
a: {
m: {
n: 100
}
},
b: [1,2,3]
}

observe(obj)

new Watcher(obj, 'a.m.n', (val) => {
console.log("@@@@", val);
})

obj.a.m.n = 200

console.log(obj.a.m.n);

最后

响应式的实现完整代码

在defineReactive()函数中会调用observe()函数,给它的子元素添加__ob__属性,并在observe中会调用Observer类,对它的子元素进行侦测,实现响应式

defineReactive()函数

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
import Dep from "./Dep"
import { observe } from "./observe"

export default function defineReactive(data, key, val) {
const dep = new Dep()
//判断,当传入的参数是两个时,val就等于对象data的本身值
if(arguments.length == 2) {
val = data[key]
}
//子元素要进行observe,至此形成了递归,这个递归是observe, Observer, defineReactive三者之间的递归
let childOb = observe(val)

Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log('你试图访问' + key +'属性')
//如果处于依赖收集阶段
if(Dep.target) {
dep.depend()
}
return val
},
set(newvalue) {
console.log('你试图改变' + key + '属性',newvalue)
val = newvalue
//接着观察传入的新值,因为新值可能是一个对象,里面会有新的属性
childOb = observe(newvalue)
//数据更改时发布订阅模式,通知dep
dep.notify()
}
})
}

observe()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Observer from './Observer'

//创建一个observe函数,注意没有r
export const observe = function(value) {
//如果传入的value不是一个对象,那么什么也不做
if(typeof value != 'object') return

//定义ob,用于储存observer类的实例
var ob
if(typeof value.__ob__ != 'undefined') {
ob = value.__ob__
} else {
ob = new Observer(value)
}

return ob
}

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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import {def} from './utils'
import defineReactive from './defineReactive';
import { arrayMethods } from './array'
import { observe } from './observe';
import Dep from './Dep';

export default class Observer {
constructor(value) {
//每个Observr的实例上都有一个Dep实例
this.dep = new Dep()
//给实例加上__ob__属性
//一定要注意,构造函数中的this不是表示类本身,而是表示实例
//添加__ob__属性,值是这次new的实例
def(value, '__ob__', this, false)

if(Array.isArray(value)) {
//如果是数组,将数组的原型强行指向arrayMethods
Object.setPrototypeOf(value, arrayMethods)
//让数组的每一项也observe一下,因为它们可能是对象或者包含对象
this.observeArr(value)
} else {
this.walk(value)
}
}
//遍历
walk(value) {
for(let k in value) {
defineReactive(value, k)
}
}

//数组遍历
observeArr(arr) {
for(let i = 0, l = arr.length; i < l; i++) {
observe(arr[i])
}
}
}

def是一个工具函数,用来给传入的对象加上一个__ob__属性,定义它主要是为了简化Observer类中的代码,使逻辑清晰

1
2
3
4
5
6
7
8
9
10
11
12
/*
工具函数
*/
export const def = function(obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
//设置__ob__属性不可遍历
enumerable,
writable: true,
configurable: true
})
}

arrayMethods对象用于重写数组的7个方法

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import {def} from './utils'
const arrayPrototype = Array.prototype

//以Array.prototype为原型创建arrayMethods对象
export const arrayMethods = Object.create(arrayPrototype)

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

methodsChange.forEach(method => {
//备份原来的方法
const original = arrayPrototype[method]
//定义新的方法
def(arrayMethods, method, function(){

//恢复原来的功能
//因为是某个数组调用方法,此处的this指向调用它的数组的上下文,
//此处的函数不能用箭头函数:1.箭头函数没有arguments 2.箭头函数的上下文不明确
//要记得返回原来方法的值,因为像pop等方法会返回删除的值
const result = original.apply(this, arguments)
//arguments是一个类数组对象,需要将她变成数组。在之后splice方法的改写中要用到数组上的方法
const args = [...arguments]

const ob = this.__ob__
let inserted = []

switch(method) {
case 'push':
case 'unshift':
inserted = args;
break;
//splice可以传入三个参数,第三个参数才是要替换或者插入的项
case 'splice':
inserted = args.slice(2);
break;
}
//如果是有要插入的项,将要插入的项也observe一下,因为可能插入的项是或者有对象
if(inserted) {
ob.observeArr(inserted)
}
//
ob.dep.notify()

return result
}, false)
})

依赖收集完整代码

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
export default class Dep {
constructor() {
//创建数组来存储自己的订阅者
//这个数组中放的是Watcher的实例
this.subs = []
}
//添加订阅
addSubs(sub) {
this.subs.push(sub)
}
//添加依赖
depend() {
//Dep.target是我们指定的一个全局位置
if(Dep.target) {
this.addSubs(Dep.target)
}
}
//通知更新,当数据发生更新时通知订阅的依赖
notify() {
//浅克隆一份subs
const subs = this.subs.slice()

for(let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

Watcher类

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
40
41
42
43
44
45
46
47
48
49
import Dep from "./Dep";
export default class Watcher {
//构造函数传入三个参数:要监听的对象,这个对象的属性, 回调函数
constructor(target, expression, callback) {
this.obj = target
this.getter = parsePath(expression)
this.callback = callback
this.value = this.get()
}
//数据发生更新时调用
update() {
this.getAndInvoke(this.callback)
}
//进入依赖收集阶段
get() {
//将Dep.target设置为Watcher本身,那么就进入依赖收集模式
Dep.target = this
console.log("全局位置",Dep.target);

var value
value = this.getter(this.obj)

Dep.target = null

return value
}

getAndInvoke(cb) {
const value = this.get()

if(value !== this.value || typeof value == 'object') {
const oldValue = this.value
this.value = value
cb.call(this.obj, value, oldValue)
}
}
}

function parsePath(str) {
let segments = str.split('.')

return (obj) => {
for(let i=0; i<segments.length; i++) {
obj = obj[segments[i]]
}

return obj
}
}