之前一直对VUE的响应式原理一知半解,这次好好看了这部分源码,发现还有很多可以说的点,索性把最近的学到的写一篇博文供大家观赏
生成响应式对象
我们知道Vue使用ES5中的Object.defineProperty方法生成响应式对象,看下这部分源码
在New Vue时会initState, 会对props, methods, data等分别进行init, 我们这次主要看对data的处理
在src/instance/state.js中
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm) // 对data进行处理,生成响应式对象
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
在initData中
在src/instance/state.js中
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function' // 获取定义的data对象
? getData(data, vm)
: data || {}
...
// observe data
observe(data, true /* asRootData */) // 基于observe生成响应式对象
}
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
...
ob = new Observer(value) // 生成Observer的实例
return ob
}
该方法实例化一个Observer
我们同时看看Observer这个class是如何写的
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方法
}
}
最终走入walk方法,该方法最终会调用 defineReactive方法
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
...
let childOb = !shallow && observe(val)
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()
}
})
}
在defineReactive方法中,将传入的data对象变成了一个响应式对象,在触发get的时候搜集依赖,在触发set的时候派发更新
依赖搜集
上面我们看到了对data的init最终是将data变成了一个响应式对象,那vue是如何在get被触发时实现依赖搜集的呢?
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
let childOb = !shallow && observe(val)
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) {
})
}
对于依赖搜集看代码有两点重要: 实例化Dep; 同时触发dep.depend()
我们看下关于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)
}
}
}
Dep这个类有个静态属性是target, 在每次render时,该target都是固定且一样的,即是每个实例化出来的dep都会有相同的target属性
那这个target到底是什么?target是一个Watcher的实例
我们看下Watcher这个class
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
...
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
...
return value
}
/**
* Add a dependency to this directive.
*/
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)
}
}
}
...
addDep方法相当于调用了对应dep中的addSub方法,将对应的watcher搜集放到dep.subs这个数组中.
需要注意的是:只有在render过程中才会触发get,所以在首次initdata的时候,其实watcher还没有实例化。在全局中只会有一个渲染watcher,最后add进入数组的是userwatcher
当每次get触发时,就会创建一个dep属性,就会将所有的watcher都搜集起来作为订阅者,放入每个属性dep.subs数组中,等待后续的notify,这种时观察者模式的使用
派发更新
那既然get搜集依赖,那set就是派发更新,即是在set被触发时,通知所有搜集的watcher,并完成更新
那派发更新是如何实现的呢?
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
set: function reactiveSetter (newVal) {
dep.notify()
}
})
}
在set被触发时,调用了dep中的notify方法
notify () {
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
由于subs中是由多个watcher组成的数组,因此会调用watcher中的update方法
update () {
queueWatcher(this)
}
queueWatcher会将watcher中对应的回调按照id大小进行排序,并在nextTick时按序执行
最终运行会走到watcher.run
run () {
if (this.active) {
const value = this.get()
this.cb.call(this.vm, value, oldValue)
}
}
}
}
最终执行对应的callback
全部流程
- 在
new Vue时会做两件事: 首先是对data进行initData,创建响应式对象; 其次是在执行mountComponent方法
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
。。。
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
return vm
}
在执行mountComponent方法时,对Watcher进行实例化,而实例化时会调用watcher的get方法,进而会调用pushTarget方法
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
该方法实现对Dep类中target复制,使得所有实例化中的target都是同一个渲染watcher
同时定义callback函数为updateComponent, 在set触发notify后会触发dep.run进入触发updateComponent完成更新
- 响应式对象已经创建完毕后,在第一次
reder时,会触发get,将所有的watcher都搜集进入对应的dep.subs数组中
第一次render
const vnode = vm._render()
在后面每次触发set时都是走到updateComponent方法
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
写在最后
在网上找到一张图,Vue基于响应式对象和Dep实现watcher管理,数据发生变动通知watcher进而触发render
撰文不易,看完还请点赞再走