Vue数据绑定原理之数据劫持
首先我们这次的源码分析不仅仅是通过源码分析其实现原理,我们还会通过Vue项目编写的测试用例了解更多细节。
原理结构
根据官方的指导图来看,数据(data
)在变更的时候会触发setter
而引起通知事件(notify
),告知Watcher
数据已经变了,然后Watcher
再出发重新渲染事件(re-render
),会调用组件的渲染函数去重新渲染DOM
。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
其实看完官方介绍的我还是一脸懵逼,毕竟我更希望知道它的实现细节,所以我们一步一步的来看,首先是图中的Data
(紫色)部分。
数据劫持
Vue
使用的是MVVM模式,Model
层的改变会更新View-Model
层,那么它是如何检测到数据层的改变的呢?
从官方指导文档-深入响应式原理中我们其实已经知道Vue
是使用Object.defineProperty()
实现数据劫持的,并且该属性无法通过其他兼容方法完美的实现,正是因为如此,Vue才不支持IE8以下的浏览器。
好了我们重头开始,查看源码我们可以看见顺着Vue
对象的实例化过程,其中有个步骤叫做initState(vm)
,这个方法中做的一部分事情就是观测组件中声明的data
,它调用了initData(vm)
。
// instance/state.js
function initData (vm: Component) {
1. 代理data,props,methods到实例上,以便直接用this就可以调用
2. observe(data, true /* asRootData */)
}
到这里,终于进入正题。
observe()
在initData
方法中调用了observe
方法,并将data
作为参数传了进去,根据函数名和参数我们其实可以猜到,这个方法就是用来观测数据变化的。那首先我们从单元测试来看一看observe
有啥需要注意的:
// test/unit/modules/observer/observer.spec.js
// it("create on object")
const obj = {
a: {},
b: {}
}
// 也可以是以下方法创建的
// const obj = Object.create(null)
// obj.a = {}
// obj.b = {}
const ob1 = observe(obj)
expect(ob1 instanceof Observer).toBe(true)
expect(ob1.value).toBe(obj)
expect(obj.__ob__).toBe(ob1)
// should've walked children
expect(obj.a.__ob__ instanceof Observer).toBe(true)
expect(obj.b.__ob__ instanceof Observer).toBe(true)
// should return existing ob on already observed objects
const ob2 = observe(obj)
expect(ob2).toBe(ob1)
// test/unit/modules/observer/observer.spec.js
// it("create on array")
// on array
const arr = [{}, {}]
const ob1 = observe(arr)
expect(ob1 instanceof Observer).toBe(true)
expect(ob1.value).toBe(arr)
expect(arr.__ob__).toBe(ob1)
// should've walked children
expect(arr[0].__ob__ instanceof Observer).toBe(true)
expect(arr[1].__ob__ instanceof Observer).toBe(true)
我们可以看到,observe
方法为obj
和其子对象都绑定了一个Observer
实例,如果是数组的话,则会遍历数组给数组中的每一个对象也绑定一个Observer
实例。实际上就是循环加上递归,给每一个数组或对象(plainObject
)都绑定一个Observer
实例,并且重复调用observe
方法只会得到同一实例,也就是单例模式。
Observer类
上面我们可以看到observe
方法是响应化data
的一个入口,而它实际上又是通过实例化Observer
类实现的,那么Observer
实例化的过程中究竟做了哪些事呢。源码中,该类的代码有一段注释:
Observer class that is attached to each observed object. Once attached, the observer converts the target object’s property keys into getter/setters that collect dependencies and dispatch updates.
Observer类会被关联在每个被观测的对象上。一旦关联上,这个观测器就会把目标对象上的每个属性都转换为getter/setter,以便用来收集依赖和分发更新事件。
再来看看源码:
export class Observer {
value: any;
dep: Dep;
constructor (value: any) {
0. 把传入的value绑定给this.value
1. 新建Dep实例绑定给this.dep
2. 将this绑定在传入的value的原型属性"__ob__"上
3. 如果value是数组,遍历数组对每个元素调用 observe(数组第i个元素)
4. 不是数组,则对对象的每个可枚举的属性调用 defineReactive
}
}
我总结出了这个方法主要做了这三件事:
1. 将对象标记为依赖
2. 循环观测数组元素
3. 响应化对象的每个可枚举属性
接下来我们重点看看响应化数据这个功能是如何实现的。
defineReactive
话不多说,直接先上源码概括:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
0. 设置闭包实例 const dep = new Dep()
1. 如果 property.configurable === false 直接 return
2. 设置闭包变量 val 的值
3. let childOb = !shallow && observe(val) 观测该属性
4. Object.defineProperty(obj, key, {...}) !!!
}
从源码概括中咱们可以看到defineReactive
其实主要做了这三件事:
1. 将属性标记为依赖
2. 递归观测属性
3. 数据劫持
而数据劫持这里,使用到的就是我们前面提到的Object.defineProperty
!我们来细品:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = 调用自带getter或者获取闭包变量val
if (Dep.target) {
// 依赖收集
}
return value
},
set: function reactiveSetter (newVal) {
调用自带setter.call(obj, newVal)或者设置闭包变量val = newVal
// 重新观测新值
childOb = !shallow && observe(newVal)
// 依赖变更通知
dep.notify()
}
})
我们把注意力放到重点上,一些小细节代码就没放上来。
首先,这里设置了属性的set
和get
(如果不了解的同学还需要先学习defineProperty)。在set
中,会更新闭包变量val
的值(如果属性有自带setter则会调用setter),并且它会调用依赖的通知方法,这个方法会告诉依赖的所有观测者并调用每个观测者的update
方法(我们稍后再细讲),这也就是官网提到的:
当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染
而在get
中,附加功能就只有依赖收集,那为什么把依赖收集放到get
中呢。咱们反向思考一下,如果要收集依赖,那么就要调用属性的get
也就是获取属性值,哪里会获取到属性值呢,当然是模板里,也就是模板渲染的时候,要把占位符替(比如{{ msg }}
)换为实际值,这个时候就会进行依赖收集。而模板没有用到的属性,则不会进行依赖收集。官网也有提到:
它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。
DIY
OK,看懂了吗,接下来让我们自己来简单的复现一下以上功能。
首先是observe
方法,注意事项是返回Observer
实例,并且是单例:
function observe(value) {
let ob
if (value.hasOwnProperty("__ob__")) {
ob = value.__ob__
} else if (Array.isArray(value) || ArrisPlainObject(value)) {
ob = new Observer(value)
}
return ob
}
function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === "[object Object]"
}
其次是Observer
对象,它会标记依赖,绑定观测实例到数据上,会处理数组,响应化所有属性:
class Observer {
constructor(value) {
this.value = value
// this.dep = new Dep()
// 挂载实例到value上
const proto = Object.getPrototypeOf(value)
proto.__ob__ = this
if (Array.isArray(value)) {
this.observeArray(value)
} else {
this.walk(value)
}
}
observeArray(value) {
// 观测数组的每个元素
value.forEach((item) => {
observe(item)
})
}
walk(value) {
// 响应化对象所有可枚举属性
Object.keys(value).forEach((key) => {
defineReactive(value, key)
})
}
}
最后是defineReactive
,它会递归观测属性,标记依赖,响应化传入的属性:
function defineReactive(obj, key, val) {
// 创建闭包依赖标记
// const dep = new Dep()
// 闭包存储实际值
val = obj[key]
// 递归观测属性
observe(val)
Object.defineProperty(obj, key, {
set(newVal) {
val = newVal
observe(newVal)
// dep.notify()
},
get() {
// 收集依赖
return val
},
})
}
考虑到我们还没有了解Dep
,所以相关代码先忽略。并且我们实现的是一个最简版本,没有考虑到过多的边缘情况。
接下来我们试验一下:
var data = {
a: 0,
b: {
c: {
d: 1,
},
},
}
observe(data)
我们再控制台中打印出data
,发现我们已经为data,a,b,c
绑定好了__ob__
。只不过现在它还不能收集依赖以及更新依赖。
依赖标记
我称其为依赖标记,因为它会和被观测的数据进行绑定,也就是说我们把响应式数据看做是一个依赖,而这个依赖标记会去处理和依赖有关的事情,比如记录观测者,分发更新事件等。
Dep类
这个类其实十分简单,功能也很明确。我们先来看看源码概括:
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)
}
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()
}
}
}
我们先来看看这明明白白的三个功能:
1. 添加订阅者addSub
2. 删除订阅者removeSub
3. 通知订阅者更新notify
并且我们可以看出依赖里面存储的订阅者是一个Watcher
数组。也就是说实际和Watcher
交互的是Dep
。他还有一个静态属性target
该属性指向的也是一个Watcher
实例。
让我们我们再回过头来看看Observer
中的相关操作。
export function defineReactive () {
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
get: function reactiveGetter() {
const value = 获取value
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
// ...
childOb = !shallow && observe(newVal);
dep.notify();
},
});
}
先讲解最简单的set
,在更新响应式值的时候只调用了该值所属的dep.notify()
,它会通知所有订阅者说我这边数据变了,麻烦你更新同步一下。
而在获取(“接触”)该值的时候,调用了值得get
,开始了依赖收集。首先如果有收集者,也就是Dep.target
,那么该值作为一个依赖被收入,如果该值是一个数组或者对象,那么该值被观测后的Observer
也作为一个依赖被收入,并且如果是数组的话,会循环收入每个元素也作为依赖。总结一下:
如果当前有收集者 Dep.target -- 依赖+1
如果当前值是对象或数组 -- 依赖+1
如果当前值是数组 -- 依赖+n
Observer
特意将和Watcher
相关的代码抽分出来为Dep
,目的也是让整个数据响应过程更加松散,可能某天观测数据变更方法不再是Observer
的时候,还能继续进行依赖收集和更新通知。
另外Dep
还提供了两个静态方法,用来修改Dep.target
。
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
从这里我们可以看出,Dep.target
是全局唯一的,一个时间点只会有一个Dep.target
。
那具体哪里会用到它呢?注意到Dep
的定义中,target
的类型是Watcher
,所以我们需要在了解Watcher
之后才能知道它会在什么时候被设置上。
DIY
由于Dep
的功能主要和Watcher
相关,并且其功能很简单,所以在我们掌握Wathcer
之后再来实现它。
数据观测
接下来我们来到了最关键的一步,它能将咱们劫持的数据真正的用于视图更新,并在视图更新时同步数据。
Watcher类
之前提了很多Watcher
,我们从上文知道,它会在某个时间点成为收集者Dep.target
去收集依赖addDep
,它会在数据变更时响应通知update
。我们先来看看它的构造函数:
class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// 初始化属性 deps,newDeps,depIds,newDepIds...
// 设置getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
// 获取监听的值
this.value = this.get()
}
}
这里有个参数expOrFn
有点隐晦,它可以是一个函数或者一个表达式,作为表达式,它通常是a.b
,msg
这样的值。你可能有点熟悉了,当我们在Vue组件中自定义watch的时候,用的也是类似的表达式。
watch: { msg (val, newVal) {} }
没错,源码注释里有提到,$watch() 和 指令都是用的Watcher
。
而expOrFn
是用来转换为获取组件中的值的getter
。比如expOrFn === 'msg'
,实际上被转换为了以下内容:
// 简单表示
this.getter = function (obj) {
return obj.msg
}
不过这里的this.value = this.get()
用的却不是直接的getter
,这是为什么呢?
我们再来看看get()
方法:
class Watcher {
// ...
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
// 递归收集可收集依赖
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
}
这里用到了我们前面提到的Dep
的两个静态方法pushTarget
,popTarget
。我们知道这两个方法是用来设置和取消Dep.target
的,而我们在Observer
中了解到,在获取属性值的get
方法中,会根据Dep.target
来搜集依赖。
而在这里的watcher.get
方法中,我们可以看到,首先添加了当前Watcher
作为Dep.target
,然后获取属性的值触发属性的get
方法,调用dep.depend()
让Wathcer
收集当前依赖。我们把Observer
中属性的get
方法中收集依赖折合一下:
Object.defineProperty(obj, key, {
get: function reactiveGetter () {
const dep = new Dep()
// ...
if (Dep.target) {
// wathcer.addDep(dep)
Dep.target.addDep(dep)
}
// ...
}
}
而这里的addDep
就是Watcher
的收集依赖的方法:
class Watcher {
constructor () {
// ...
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
}
// ...
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)
}
}
}
}
而这个方法主要的作用就是去重然后存储目标依赖dep
到watcher.newDeps
里。然后将watcher
存储到dep.subs
里,建立一个双向引用。
然后接下来就是递归收集子对象依赖(如果有),然后清除Dep.target
引用,最后调用this.cleanupDeps
,而这个方法做的事也很简单:
-
旧依赖列表有而新依赖列表没有的这些依赖,由于新依赖中已经没有了
dep -> watcher
的引用,所以对应的也要清除dep <- wathcer
引用,这里调用了dep.removeSub(this)
,就是告诉你和我撇清关系,我的心里已经没有你了。 -
将
newDeps
和newDepIds
赋值给deps
和depIds
,然后清空newDeps
和newDepIds
,重新开始生活。
到这里,Watcher
的初始化就已经完成了。
小小的总结
当然我们的分析还没结束,只不过我们需要短暂的总结一下,消化之前的概念,才能更深刻的理解接下来的步骤。
上图是我们目前所了解到的一个关系图,黄色的流程表示依赖收集的过程,绿色的表示数据变更的过程。
绑定数据到DOM
目前为止,我们只是了解了如何劫持数据,并且在数据变更时更新它的观测者。比如:
假设我们已经收集完依赖了,也就是每个响应化属性都有一个订阅列表subs
,这里面装着和该属性相关的观测者。当属性出现变更时,由于数据被劫持(set
),这些观测者都会得到通知,调用各自的update
,去执行自己的回调函数。
但是,什么时候才会收集依赖呢?我们写的{{msg}}
模板表达式怎么和data.msg
关联起来的呢?Watcher
是什么时候实例化的?
我们下一篇继续分析。