1. 1. Vue响应式原理
    1. 1.1. Object.defineProperty
    2. 1.2. 实例化一个Vue对象
      1. 1.2.1. 数据代理
      2. 1.2.2. 数据劫持
      3. 1.2.3. 依赖收集
      4. 1.2.4. 派发更新
      5. 1.2.5. 模板编译
    3. 1.3. 响应流程图
Vue响应式原理(01)

Vue响应式原理

Vue 2.x 版本中,实现数据双向绑定的主要原理就是通过数据劫持的方式,即Object.definePropertygettersetter方法,配合发布-订阅模式,来监听到数据的赋值与变化,从而通知相关的视图进行更新。

那么,具体是怎么实现的呢?

Object.defineProperty

首先,我们先了解一下最核心的Object.defineProperty的基本用法:

1
Object.defineProperty(obj, prop, descriptor)
  • obj:要定义属性的对象
  • prop:要定义或修改的属性的名称或Symbol
  • descriptor:要定义或修改的属性描述符,包含以下属性:
    • configurable:当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false
    • enumerable:当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。默认为 false
    • value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为undefined
    • writable:当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值。默认为false
    • get:属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为undefined
    • set:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为undefined

注意点getset不能和writablevalue共存,否则浏览器会报错

1
2
3
Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object>
at Function.defineProperty (<anonymous>)
at <anonymous>:1:8

▼简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let obj = {}
let value = null
Object.defineProperty(obj, 'property1', {
set: function (val) {
console.log('set value')
value = val
},
get: function () {
console.log('get value')
return value
}
})

// 执行赋值
obj.property1 = 1
// 输出:set value

// 获取值
obj.property1
// 输出:get value

实例化一个Vue对象

以下面一个简单的Vue实例作为分析参考:

1
2
3
4
5
<div id="app">
<div @click="changeMsg">
{{ message }}
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
methods: {
changeMsg () {
this.message = 'Hello World!'
}
}
})

数据代理

Vue中,我们定义在data的数据可以通过this.xxx来获取,主要原因在于Vuedata中的数据做了一层数据代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}

function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function initData (vm) {
let data = vm.$options.data
vm._data = data
// 判断data的类型是object
if (!isPlainObject(data)) {
data = {}
}
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
// 数据代理
proxy(vm, `_data`, key)
}
observe(data, true /* asRootData */)
}

代理的实现方式并不难,通过Object.definePropertytarget[sourceKey][key]的读写变成了对target[key]的读写。

为什么这样处理呢?

  • 方便读取:比起用this._data.xxx来读取,这样的方式更加直接方便
  • 保证数据统一:为了实现this.xxx的方式获取,当然也可以通过for循环把数据一个一个赋值到实例上(methods的处理方式),但是这样做就会导致维护了两份数据,增加了维护的成本
  • 不影响依赖的收集与更新:当对this.xxx进行读取的时候,就会触发_data.xxx中的getset方法,不会影响到data中数据的依赖收集与更新

数据劫持

因为我们需要在data数据更新的时候,通知视图的更新,因此我们对data中的每一个数据都生成对应的响应式对象,给对象的每一个属性都加上gettersetter,在gettersetter中插入消息绑定与发布的动作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// observe 的作用,就是判断传入的value是否符合条件
// 对符合条件的对象,生成一个Observer对象实例
export function observe (value, asRootData) {
if (!isObject(value)) {
return
}
let ob
if (
// 判断是否已经定义过响应对象,避免重复定义
hasOwn(value, '__ob__') && value.__ob__ instanceof Observer
) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Observer的作用就是将传入的value中的每个属性批量处理
// defineReactive就是处理添加getter与setter的方法
export class Observer {
constructor (value) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 对已生成响应对象的value增加__ob__属性进行标识
def(value, '__ob__', this)
this.walk(value)
}
// PS: 这里只放了对于Object类型的处理,Array类型的处理需要另外的考虑
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
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
export function defineReactive (obj, key, val) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)

if (property && property.configurable === false) {
// 调用Object.freeze()方法时,configurable会被设为false
// 实际上是Vue中提升性能的一种方法
// 当有对象被freeze之后,就不能对对象再进行修改,因此也不需要再做数据劫持
return
}

if (arguments.length === 2) {
val = obj[key]
}
// 监听当前val的所有子属性
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = val
if (Dep.target) {
// 依赖收集
dep.depend()
// 存在子属性的响应对象,需要对子属性也进行依赖的收集
if (childOb) {
childOb.dep.depend()
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
// 对新的值进行设置响应对象,保证数据响应式
childOb = observe(newVal)
// 派发更新
dep.notify()
}
})
}

通过defineReactive初始化Dep对象实例,接着拿到obj的属性描述符,然后对子对象进行递归调用ovserve方法,这样就保证了无论obj的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改obj中一个嵌套较深的属性,也能触发gettersetter

依赖收集

defineReactive中,我们实例化了一个Dep对象,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
35
36
37
38
39
40
41
42
43
44
45
46
47
let uid = 0

export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}

addSub (sub) {
this.subs.push(sub)
}

removeSub (sub) {
remove(this.subs, sub)
}

depend () {
// 依赖收集,如果当前有正在处理的Wacter
// 将该Dep放进当前Wacter的deps中
if (Dep.target) {
Dep.target.addDep(this)
}
}

notify () {
// slice的作用是复制当前的subs队列
// 循环处理队列中的每个Watcher的update方法
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// 标记全局唯一的一个正在处理的Watcher
// 在同一时间内,控制只有一个Watcher正在执行
Dep.target = null
// 待处理的Watcher队列
const targetStack = []

export function pushTarget (_target) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}

export function popTarget () {
Dep.target = targetStack.pop()
}

Dep类中,定义了一个静态属性target,是为了存储全局唯一的Watcher。因为我们在操作数据的时候,必然会涉及到数据的读取,但是我们只需要收集到需要Watcher的对象的依赖就好了,因此需要用Dep.target来判断是否要进行依赖收集,当我们运行Watcher.get()的时候,Dep.target才会被赋值。并且在同一段时间内,只能处理一个Watcher

Watcher所扮演的角色就是观察者,它的主要作用就是为我们需要观察的属性提供回调与收集依赖,当被观察的值发生变化时,就会受到来着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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
let uid = 0

export default class Watcher {
constructor (
vm,
expOrFn,
cb
) {
this.vm = vm
this.cb = cb
this.id = ++uid
this.deps = []
this.depIds = new Set()
this.expression = expOrFn.toString()
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
}
}
this.value = this.get()
}

get () {
pushTarget(this)
const vm = this.vm
// 这里的 this.getter 会触发对应data的defineProperty
// 触发后会将这个Watcher添加到Dep的队列中
let value = this.getter.call(vm, vm)
// 执行完成后退出Watcher队列
popTarget()
return value
}

addDep (dep) {
const id = dep.id
// 保证同一数据不会被添加多个观察者
if (!this.depIds.has(id)) {
// 将自己加入到当前dep的subs队列
this.depIds.add(dep.id)
this.deps.push(dep)
dep.addSub(this)
}
}

update () {
const value = this.get()
if (value !== this.value || isObject(value)) {
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
}
}

当执行new Watcher生成一个新的观察者时,就会执行Watcher类中的get方法,从而触发数据接触中的getter方法,将自身加入到Depsubs队列中,以此完成依赖收集。

派发更新

同理,当在代码中出现this.xxx = xxx的赋值行为时,就会触发数据劫持的setter方法,此时Dep会通知subs队列中的每一个观察者都执行自身的update,以此触发new Watcher时所绑定的callback函数。

模板编译

Vue中,render的实现也是一大核心,这里先略过,用最简单的DOM操作来完成一些简单指令的实现。举例模板语法,通过正则匹配到message,然后生成对应的Watcher

1
2
3
4
5
6
7
8
9
createWatcher: function (node, vm, exp, dir, ext) {
// 获取对应指令的updater方法
const updaterFn = updater[dir + 'Updater']
updaterFn && updaterFn(node, this._getVMVal(vm, exp), undefined, ext)
// 生成一个Watcher,在每次Dep通知更新时会执行updater
new Watcher(vm, exp, (value, oldValue) => {
updaterFn && updaterFn(node, value, oldValue, ext)
})
}
1
2
3
4
// 更新文本的Updater方法
[DIRECTIVE.TEXT + 'Updater']: function (node, value) {
node.textContent = isDef(value) ? value : ''
}

响应流程图