简要的描述加复杂的代码表达核心思想
本文源码来自vue/src/core/observer/*.js
我们实际开发中发现,在data
中定义的所有数据,后续无论是在template
中使用,还是在methods
中使用,都能随着数据的变化而变化。为了理解这其中的原理,研究源码后整理出这篇文章,欢迎大家及时指正。
vue 2.x
版本使用的是 Object.defineProperty
详细API文档见Object.defineProperty
用于绑定Object
类型数据,比如定义一个person
:
let person = { name: 'usm', age: 12 } 复制代码
现在希望person
的name
和age
发生改变时,可以触发一些操作,就可以通过 Object.defineProperty
实现:
Object.defineProperty(person, 'name', { enumerable: true, configurable: true, get() { console.log('get name's value'); }, set(val) { console.log(`set value ${val}`); } }); person.name // get name's value person.name = 'new' // set value new 复制代码
其中enumerable
属性表示此属性设置为可枚举,configurable
表示此属性可被修改/删除。
至此,person
对象中的name
属性发生读/写操作时,都可以被监听到,并执行对应的代码。
回到源码,vue
中实现了一个Observer
对象,用来对vue
实例中的每个数据添加监听。
class Observer { constructor (value) { this.value = value def(value, '__ob__', this) if (Array.isArray(value)) { this.observeArray(value) } else { this.walk(value) } } // 遍历Object中每个属性,添加监听 walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } // 监听数组类型数据 observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } } 复制代码
其中def(value, '__ob__', this)
用于给当前对象添加一个__ob__
属性,值就是当前的Observer
,目的时用来标记为已添加监听。
可以看到针对Object
类型的对象,遍历后对每个属性调用了defineReactive
方法。
// defineReactive方法部分内容 function defineReactive (obj, key, val) { ...... Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = val; ... return value }, set: function reactiveSetter (newVal) { const value = val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } ... } }) } 复制代码
get
和set
中省略部分就是数据发生改变后所做的操作。
其中set
时做了优化,判断数据是否变化,无变化或无set
函数时不做操作;当已存在set
函数时直接执行,避免重复监听。
第一步中实现了数据的监听,第二步就要根据数据的变化,来通知对应的dom
进行更新。所以我们要先知道通知谁,也就是谁依赖了这个数据,由于获取数据时会触发get
函数,因此我们就在get
函数中收集依赖。
vue
中实现了一个Dep
类,用来管理当前数据的依赖,只需要对每个添加监听的数据创建一个Dep
类,再当“谁”调用了当前数据,就把“谁”添加到Dep
中,触发set
时再通知Dep
中存放的依赖们。
首先实现Dep
类:
class Dep { constructor () { this.id = uid++ this.subs = [] } addSub (sub) { this.subs.push(sub) } removeSub (sub) { remove(this.subs, sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } 复制代码
Dep
类比较简单,定义了subs
用于存放依赖数组,收集依赖时,触发addSub
方法,派发通知时调用notify
方法,对数组中每个依赖调用update
方法。
Watcher
类在Dep
中,我们发现每个方法都是在处理一个依赖,而这个依赖从何而来,查看源码后发现,vue
还定义了一个Watcher
类,也就是我们说的依赖。
class Watcher { constructor (vm, cb) { this.vm = vm this.cb = cb this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.value = this.get() } get () { pushTarget(this) let value const vm = this.vm value = this.getter.call(vm, vm) popTarget() return value } addDep (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) } } } update () { ... this.run() } depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } } 复制代码
其中在实例化时,调用了get
方法获取value
,这里调用了一个pushTarget
方法,和一个对应的popTarget
方法,位于源码中dep.js
文件中
function pushTarget (target) { targetStack.push(target) Dep.target = target } function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] } 复制代码
我们看到调用pushTarget
方法时,将Dep
的静态属性target
设置为当前的Watcher
对象,同时推入一个target
栈中,调用popTarget
时再弹栈。
回到Observer
类的源码中
// 截取Object.defineProperty部分 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = val if (Dep.target) { dep.depend() } return value }, set: function reactiveSetter (newVal) { const value = val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } dep.notify() } }) 复制代码
得出整体流程如下,初始化Watcher
类,调用get
方法,在get
方法中,调用数据的getter
,而在getter
中,调用了dep.depend
,depend
方法中调用了Watcher
类的addDep
方法,addDep
方法最终调用addSub
方法添加依赖并注册监听。
派发通知时,触发update
方法,从而更新DOM
。
流程图如下:
本文分析了vue
源码中对Object
类型数据的绑定过程。
vue
中定义的各项数据,收集使用到该数据的依赖并注册监听因此当data
中定义的数据发生变化时,所有用到该数据的地方都能发生变化,也就实现了vue
中数据绑定的功能。