# 响应式原理
当一个
Vue
实例创建时,vue
会遍历data
选项的属性,用Object.defineProperty
将它们转为getter/setter
并且在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的watcher
程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter
被调用时,会通知watcher
重新计算,触发相应的监听回调。从而致使它关联的组件得以更新。
vue
的响应式原理主要基于:数据劫持、依赖收集(收集 Watcher
)和异步更新(发布/订阅模式),通过对象劫持来做 依赖的收集 和 数据变化的侦测,通过维持一个队列来异步更新视图。
TIP
Vue
主要通过以下 4
个步骤来实现数据双向绑定的:
- 实现一个监听器
Observer
:对数据对象进行遍历,包括子属性对象的属性,利用Object.defineProperty()
对属性都加上setter
和getter
。这样的话,给这个对象的某个值赋值,就会触发setter
,那么就能监听到了数据变化。响应式原理的入口,根据数据类型处理观测逻辑 - 实现一个解析器
Compile:
解析Vue
模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。 - 实现一个订阅者
Watcher
:Watcher
订阅者是Observer
和Compile
之间通信的桥梁 ,主要的任务是订阅Observer
中的属性值变化的消息,当收到属性值变化的消息时,触发解析器Compile
中对应的更新函数。 - 实现一个订阅器
Dep
:订阅器采用 发布-订阅 设计模式,用来收集订阅者Watcher
,对监听器Observer
和 订阅者Watcher
进行统一管理。依赖收集器,属性都会有一个Dep
,方便发生变化时能够找到对应的依赖触发更新
# Observe 对数据进行观测
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
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
TIP
- 为观测的属性添加
__ob__
属性,它的值等于this
,即当前Observe
的实例 - 为数组添加重写的数组方法,比如:
push、unshift、splice、pop、shift、sort、reverse
等方法,重写目的是在调用这些方法时,进行更新渲染 - 观测数组内的数据,
observe
内部会调用new Observe
,形成递归观测 - 观测对象数据,
defineReactive
为数据定义get
和set
,即数据劫持
# 如何数据劫持
getter
收集依赖,setter
触发依赖
// 部分vue代码
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
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
定义一个响应式数据。也就是这个函数中进行变化追踪,封装后只需要传递
data key val
就行。
# 递归侦测所有数据
封装一个Observer
类。这个类的作用是将一个数据内的所有属性(包括子属性)都转换成getter/setter
形式,然后追踪它们的变化:
/** Observer类会附加到每一个被侦测到的object上。一旦被附加上,Observer会将object的所有属性转换为getter/setter的形式
* 来收集属性的依赖,并且当属性发生变化时会通知这些依赖
*
*/
export class Observer {
constructor (value: any) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
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(); // 为每个属性创建Dep依赖搜集
// Object.defineProperty 代码
}
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
# Dep
为数据收集依赖
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 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
TIP
- 数据收集依赖的主要方法,
Dep.target
是一个watcher
实例 - 添加
watcher
到数组中,也就是添加依赖 - 属性在变化时会调用
notify
方法,通知每一个依赖进行更新 Dep.target
用来记录watcher
实例,是全局唯一的,主要作用是为了在收集依赖的过程中找到相应的watcher
pushTarget
和popTarget
这两个方法显而易见是用来设置Dep.target
的。Dep.target
也是一个关键点
# 收集谁(谁是依赖)-- 订阅者
Watcher
,换句话说是当依赖发生变化通知Watcher
依赖注入到
Dep
中后,每当数据发生变化时,就会让依赖列表中的所有依赖循环触发update
方法,也就是Watcher
中的update
方法。而update
方法会执行参数中的回调函数,将value/oldValue
传到参数中。Watcher
的原理是先把自己设置到全局唯一的指定位置(如window.target
,这里是Dep.target
)然后读取数据。因为读取了数据所以会触发这个数据的getter
。接着在getter
中就会从全局唯一的那个位置读取当前正在读取数据的Watcher
,并把这个Watcher
收集到Dep
中去。通过这样的方式,Watcher
可以主动去订阅任意一个数据的变化
let id = 0
export class Watcher {
constructor(vm, exprOrFn, cb, options){
this.id = ++id // watcher 唯一标识
this.vm = vm
this.cb = cb
this.options = options
// 1
this.getter = exprOrFn
this.deps = []
this.depIds = new Set()
this.get()
}
run() {
this.get()
}
get() {
pushTarget(this)
this.getter()
popTarget(this)
}
// 2
addDep(dep) {
// 防止重复添加 dep
if (!this.depIds.has(dep.id)) {
this.depIds.add(dep.id)
this.deps.push(dep)
dep.addSub(this)
}
}
// 3
update() {
queueWatcher(this)
}
}
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
TIP
this.getter
存储的是更新视图的函数watcher
存储dep
,同时dep
也存储watcher
,进行双向记录- 触发更新,
queueWatcher
是为了进行异步更新,异步更新会调用run
方法进行更新页面
# 响应式原理流程
# 数据观测
数据在初始化时会通过 observe
方法调用 Observe
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在初始化时,observe
拿到的 data
就是我们在 data
函数内返回的对象。
observe
函数只对object
类型数据进行观测- 观测过的数据都会被添加上
__ob__
属性,通过判断该属性是否存在,防止重复观测 - 创建
Observe
实例,开始处理观测逻辑
# 对象观测
defineReactive
方法内部使用 Object.defineProperty
对数据进行劫持,是实现响应式原理最核心的地方。
- 由于值可能是对象类型,这里需要调用
observe
进行递归观测 - 这里的
dep
就是上面讲到的每一个属性都会有一个dep
,它是作为一个闭包的存在,负责收集依赖和通知更新 - 在初始化时,
Dep.target
是组件的渲染watcher
,这里dep.depend
收集的依赖就是这个watcher
,childOb.dep.depend
主要是为数组收集依赖 - 设置的新值可能是对象类型,需要对新值进行观测
- 值发生改变,
dep.notify
通知watcher
更新,这是我们改变数据后能够实时更新页面的触发点
通过上面的逻辑,也能得出为什么 Vue3.0
要使用 Proxy
代替 Object.defineProperty
了。Object.defineProperty
只能对单个属性进行定义,如果属性是对象类型,还需要递归去观测,会很消耗性能。而 Proxy
是代理整个对象,只要属性发生变化就会触发回调。
# 数组观测
对于数组类型观测,会调用 observeArray
方法
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
2
3
4
5
与对象不同,它执行
observe
对数组内的对象类型进行观测,并没有对数组的每一项进行Object.defineProperty
的定义,也就是说数组内的项是没有dep
的。我们通过数组索引对项进行修改时,是不会触发更新的。但可以通过this.$set
来修改触发更新。
# 数组方法重写
当数组元素新增或删除,视图会随之更新。这并不是理所当然的,而是
Vue
内部重写了数组的方法,调用这些方法时,数组会更新检测,触发视图更新。 回到Observe
的类中,当观测的数据类型为数组时,会调用protoAugment
方法。 这个方法里把数组原型替换为arrayMethods
,当调用改变数组的方法时,优先使用重写后的方法。
if (Array.isArray(value)) {
if (hasProto) { // 判断有无 __proto__ const hasProto = '__proto__' in {}
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value) // 观察数组
} else {
this.walk(value) // 观察对象
}
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src // 覆盖target原型
/* eslint-enable no-proto */
}
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])
}
}
// arrayMethods 实现
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original 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)
// notify change
ob.dep.notify()
return result
})
})
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
57
58
59
60
61
62
63
- 将数组的原型保存起来,因为重写的数组方法里,还是需要调用原生数组方法的
arrayMethods
是一个对象,用于保存重写的方法,这里使用Object.create(arrayProto)
创建对象是为了使用者在调用非重写方法时,能够继承使用原生的方法- 调用原生方法,存储返回值,用于设置重写函数的返回值
inserted
存储新增的值,若inserted
存在,对新值进行观测ob.dep.notify
向数组的依赖发送通知watcher
,执行update
__proto__
其实是Object.getPrototypeOf
和Object.setPrototypeOf
的早期实现,所以使用 ES6 的Object.setPrototypeOf
来代替__proto__
完全可以,只是目前浏览器支持效果不是很理想。
copyAugment
逻辑非常粗暴:如果不能使用__proto__
,就直接降arrayMethods
身上这些方法设置到被观测的数组上。因为当访问一个对象的方法时,只有其身上不存在这个方法,才会去它的原型上找这个方法
Array
收集依赖的方式和Object
一样,都是在getter
中收集。但是由于使用依赖的位置不同,数组要在拦截器中向依赖发送消息,所以依赖不能像Object那样保存在defineReactive
中,而是把依赖保存在Observer
实例上在
Observer
中,我们对每个侦测了变化的数据都标上印记__ob__
,并把this(Observer 实例)
保存在__ob__
上。主要两个作用:一方面为了标记数据是否被侦测了变化(保证同一个数据只被侦测一次),另一方面可以方便通过数据取到__ob__
,从而拿到保存在Observer
实例上的依赖,当拦截到数组发生变化时,向依赖发送通知。
# 依赖收集
- 首先初始化数据,调用
defineReactive
函数对数据进行劫持。 - 初始化将
watcher
挂载到Dep.target
,this.getter
开始渲染页面。渲染页面需要对数据取值,触发get
回调,dep.depend
收集依赖。 Dep.target
为watcher
,调用addDep
方法,并传入dep
实例。addDep
中添加完dep
后,调用dep.addSub
并传入当前watcher
实例。
// 删减版代码
export class Watcher {
constructor(vm, exprOrFn, cb, options){
this.getter = exprOrFn
this.get()
}
get() {
pushTarget(this)
this.getter()
popTarget(this)
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
}
class Dep{
constructor() {
this.id = id++
this.subs = []
}
depend() {
Dep.target.addDep(this)
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
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
通常页面上会绑定很多属性变量,渲染会对属性取值,此时每个属性收集的依赖都是同一个 watcher
,即组件的渲染 watcher
TIP
Vue
中通过Object.defineProperty
来将对象的key
转换成getter/setter
,但是只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。
Data
通过Observer
转换成getter/setter
形式追踪变化。- 当外界通过
Watcher
读取数据时,会触发getter
从而将Watcher
添加到依赖中。 - 当数据发生变化时,会触发
setter
,从而向Dep
中的依赖Watcher
发送通知。 Watcher
接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数。
模板渲染解析时对应绑定指令,此时会调用订阅者初始化(
watcher
中的get()
方法,去触发对应属性在发布者observer
的getter
,发布者会判断是不是通过订阅者初始化调用的,只有是才有通过Dep
收集依赖。发布者通过depend
通知Dep
类收集。此后每次的数据更新都会通过发布者的setter
去触发Dep
类的回调update
执行收集依赖的所有方法,更新订阅者的所有状态及更新视图。即将来data
中数据⼀旦发生变化,会首先找到对应的Dep
,通知所有Watcher
执行更新函数
# 手写一个vue双向绑定
# 执行初始化
// 执行初始化,对data执行响应式处理
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
//对data进行响应处理
observe(this.$data)
//代理data到vm上
proxy(this)
//执行编译
new Compile(options.el,this)
}
}
// 对data选项执行响应式操作
function observe(obj) {
if(typeof obj !== 'object' || obj == null) {
return;
}
new Observe(obj)
}
class Observe {
constructor(value) {
this.value = value;
this.walk(value)
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj,key,obj[key])
})
}
}
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
# 编译Compile
// 对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => {
if(this.isElement(node)) {
// 判断是否为节点
console.log('编译元素'+ node.nodeName)
} else if(this.isInterpolation(node)) {
// 判断是否为插值文本
console.log('编译插值文本'+node.textContent)
}
if(node.childNodes && node.childNodes.length > 0) {
// 判断是否有子元素
this.compile(node)// 对子元素进行递归遍历
}
})
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 依赖收集
视图中会用到
data
中某key
,这称为依赖。同⼀个key
可能出现多次,每次都需要收集出来用⼀个Watcher
来维护它们,此过程称为依赖收集 多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知
- 实现思路:
defineReactive
时为每个key
创建一个Dep
实例- 初始化视图时读取某个
key
,例如name1
,创建一个watcher1
- 由于触发
name1
的getter
方法,便将watcher1
添加到name1
对应的Dep
中 - 当
name1
更新,setter
触发时,便可通过对应Dep
通知其管理所有Watcher
更新
// 负责更新视图
class Watcher {
constructor(vm,key,updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this
// 读一下key,触发get
vm[key]
//置空
Dep.target = null
}
// 未来执行dom更新函数,由Dep调用
update() {
this.updaterFn.call(this.vm,this.vm[this.key])
}
}
// 声明Dep
class Dep {
constructor() {
this.deps = [] //依赖管理
}
addDept(dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach((dep) => dep.update())
}
}
// 创建watcher时触发getter
class Watcher {
constructor(vm,key,updaterFn) {
Dep.target = this;
this.vm[this.key]
Dep.target = null
}
}
// 依赖收集,创建Dep实例
function defineReactive(obj,key,val) {
this.observe(val)
const dep = new Dep()
Object.defineProperty(obj,key,{
get() {
Dep.target && dep.addDept(Dep.target) // Dep.target也就是Watcher实例
return val
},
set(newVal) {
if(newVal === val) return;
dep.notify() // 通知dep执行更新方法
}
})
}
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
# 双向绑定的优点
- 基于数据劫持/依赖收集 的双向绑定优点:
- 不需要显示调用,利用数据劫持+发布订阅,可以直接通知变化并且驱动视图更新
- 直接得到精确的变化数据,劫持了属性
setter
,属性值改变我们可以精确获得变化的内容newVal
,不需要额外的diff
操作。
# vue2.0对比vue3.0
Object.defineProperty
缺点:- 不能监听数组,没有
getter/setter
。数组长度太长性能负担大。 - 只能监听属性,而不是整个对象。需要遍历属性。
- 只能监听属性变化,不能监听增删属性。
- 不能监听数组,没有
proxy
优缺点:可以监听数组
监听整个对象
13种拦截方法
返回新对象而不是直接修改原对象,更符合
immutable
兼容性不好,无法用
polyfill
磨平
← 指令的奥秘 虚拟dom和虚拟节点VNode →