简述
之所以称「响应式」是因为,在 Vue 中, vm 中 Data(指 vm.$props
和 vm.$data
里的属性)的变化会自动触发页面上引用了这些 Data 的 DOM 的变化,我们已经知道 DOM 的变化可以通过对比 VNode 的差异由 vm 自行完成(这一过程称为 re-render), 现在的问题是 Data 变化时如何实现「自动触发」re-render。Vue 对此的实现,本质上是利用事件订阅/发布模式:在 vm.$mount
时去订阅页面上用到的 Data,然后在这些 Data 变化时,去通知 vm 进行 re-render。订阅的动作在 Data 的 getter 中完成,发布的动作在 Data 的 setter 中完成,于是 vm.$mount
的过程中订阅某些 Data 后(访问时触发 getter),当后续这些 Data 变化时(赋值时触发 setter),vm 就得到通知要去 re-render,这就是所谓 Vue 响应式的基本原理。
源码简析
在此之前,先了解源码里几个概念的作用,Dep、Watcher 和 Observer,它们是源码中的三个类。Dep 的作用相当于 EventEmitter,负责 on 和 fire;Watcher 的角色是订阅者;Observer 的作用是为 Data 添加 getter/setter。
Dep
interface DepInterface {
id: number
subs: Array<Watcher>
addSub (sub: Watcher): void
removeSub (sub: Watcher): void
depend (): void
notify (): void
}
let uid = 1
class Dep implements DepInterface {
static target = null
id = uid++
subs = []
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
if (this.subs.length) {
const index = this.subs.indexOf(sub)
if (index > -1) {
this.subs.splice(index, 1)
}
}
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
for (let i = 0, length = this.subs.length; i < length; ++i) {
this.subs[i].update()
}
}
}
const targetStack = []
function pushTarget (_target) {
if (Dep.target) { targetStack.push(Dep.target) }
Dep.target = _target
}
function popTarget () {
Dep.target = targetStack.pop()
}
前面说了,Dep 既然是一个 EventEmitter,那么它就包括(取消)订阅、发布的方法,depend
方法的最终结果是调用 addSub
。id
是每个 dep 实例的唯一标识,与一个 Data 一一对应,因为每个 Data 被添加 getter 时都会生成一个与之对应的 dep 实例,正是由这个 dep 实例来处理相关的订阅/发布的逻辑 ;subs
是由一组订阅了该 dep 的 watcher 实例组成的数组。静态属性 target
用来存储当前正在执行订阅动作的 watcher 实例,变量 targetStack
用来存储所有这样的实例,但同一时间只允许一个 watcher 实例执行订阅动作;因为可能出现一个 watcher 正在执行订阅动作的过程中,一个新的 watcher 要优先执行,所以 targetStack
的作用便是保存旧的 watcher,新 watcher 来了便将旧 watcher 入栈,新 watcher 订阅完成后便将旧 watcher 出栈并让其成为 Dep.target
。
Observer
interface ObserverInterface {
value: Object | Array<any>
vmCount: number
dep: Dep
walk (obj: Object): void
observeArray (arr: Array<any>): void
}
class Observer implements ObserverInterface {
value: Object | Array<any>
vmCount = 0
dep = new Dep()
constructor (value: Object | Array<any>) {
this.value = value
def(value, '__ob__', this)
if (Array.isArray(value)) {
this.observeArray(value)
} else {
this.walk(value)
}
}
observeArray (arr: Array<any>): void {
for (let i = 0, l = arr.length; i < l; ++i) {
observe(arr[i])
}
}
walk (obj: Object): void {
const keys = Object.keys(obj)
for (let i = 0, l = keys.length; i< l; ++i) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
function observe (value: any, asRootData?: boolean): Observer | void {
if (!(typeof value === 'object' && value !== null) || value instanceof VNode) {
return
}
let ob: Observer | void
if (Object.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (Array.isArray(value) || ''.toString.call(value) === '[object Object]') {
ob = new Observer(value)
}
if (ob && asRootData) {
ob.vmCount++
}
return ob
}
function defineReactive (obj: Object, key: string, val: any): void {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, val)
if (property && !property.configurable) {
return
}
const getter = property && property.get
const setter = property && property.set
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
}
return value
},
set: function reactiveSetter (value: any) {
if (setter) {
setter.call(obj, value)
} else {
val = value
}
dep.notify()
}
})
}
function def (obj: Object, key: string, value: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
Observer.constructor(value)
的逻辑为:
- 将 value 值赋给 observer 实例的 value 属性
- 将生成的 observer 实例本身挂在 ob 属性下
- 当 value 为数组时,调用
this.observeArray(value)
,这个方法会递归调用new Observer
- 否则 value 会是一个对象,然后对 value 的每一个属性调用
defineReactive(value, key, value[key])
,这个方法即为每一个属性添加一个 getter 和 setter,同时生成一个 dep 实例,它在 getter 里添加监听的 watcher 实例,这也是所谓的依赖收集(即对依赖value[key]
这个属性的 watcher 的收集),然后在 setter 里 nontify。
Watcher
let uuid = 0
interface WatcherInterface {
vm: Component
cb: Function
id: number
deps: Array<Dep>
newDeps: Array<Dep>
depIds: Set<number>
newDepIds: Set<number>
getter: Function | void
value: any
get (): any
addDep (dep: Dep): void
update (): void
}
class Watcher implements WatcherInterface {
vm: Component
cb: Function
id: number = ++uid
deps: Array<Dep> = []
newDeps: Array<Dep> = []
depIds: Set<number> = new Set()
newDepIds: Set<number> = new Set()
getter: Function | void
value: any
constructor (
vm: Component,
fn: Function,
cb: Function,
options?: Object,
isRenderWatcher?: boolean
) {
this.vm = vm
this.cb = cb
this.getter = fn
this.value = this.get()
}
get (): any {
pushTarget(this)
const vm = this.vm
const value = this.getter && this.getter.call(vm, vm)
popTarget()
return value
}
addDep (dep: Dep): void {
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 (): void {
const value = this.get()
const oldValue = this.value;
this.value = value
this.cb.call(this.vm, value, oldValue);
}
}
Vue 里 watcher 分为 renderWatcher(即每个 vm 实例用来监听 vm 里的 Data 然后专门 re-render 的 watcher)、computedWatcher(用来 watch computed)和由 watch () {}
以及人工手动调用 vm.$watch
生成的 watcher。
renderWatcher 依赖收集的时机是 vm.$mount()
,mount 的过程中访问了 data、props、computed 中的数据,在 created hook 之前;除了手动调用 vm.$watch
以外生成的其他 watcher 依赖收集的时机都是 initState
,在 created hook 之后。可以看到在 new Watcher()
时会调用传进来的 fn()
,而正是在 vm.$mount()
的里,每个 vm 会新建一个 renderWatcher(具体代码为 new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
),同时传给 new Watcher
的 fn
的值 updateComponent
的作用就是 re-render,当某个依赖的 dep.notify()
时,vm 就会执行 updateComponent()
,从而 re-render。
心得
- 理解原理是前提,原理理解了就等于知晓了大概的逻辑,大的脉络通了之后才能从整体上把握,而不至于半天云里雾里。
- 源码里面不少都是增加健壮性和兼容性的代码,选择性无视它们会大大降低初看源码的压力。
- 理解原理加以总结,甚至探索原理背后的原理,抽象的结果就是做出合理的判断,合理的判断就是知识。
- 抽象的程度越高越普适,但相应地,针对性也小了。