写在前面:本文为个人在日常工作和学习中的一些总结,便于后来查漏补缺,非权威性资料,请带着自己的思考^-^。
说起响应式,首先会想到Vue实例中的data属性,例如:对data中的某一属性重新赋值,如果该属性用在了页面渲染上面,则页面会自动进行重新渲染,这里就以data作为切入点,来看一下Vue中的响应式是怎样的一个实现思路。
Vue实例创建阶段
在创建Vue实例的时候,执行到了一个核心方法:initState,该方法会对methods/props/methods/data/computed/watch进行初始化,此时我们只关注data的初始化:
function initData(vm) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
...
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
...
proxy(vm, '_data', key)
...
}
...
observe(data, true)
}
代码中省略了一些和当前研究的内容无关的代码,用...表示;
可以看到这个方法主要做了两件事:
- 将data代理到vm._data,即:访问data中的属性key vm[key]将触发getter,返回vm._data[key],赋值同理;作用是显而易见的:以后我们想要访问/赋值data中的某个属性key时,直接this[key]这样就可以了,无需this._data[key]这样
- 执行observe(data, true)函数,遍历data中的每一个属性,通过defineObject将其设为响应式的,即:在正常使用场景中,我们访问this[key](其中key为data中的一个属性)时,会由于1中缘故触发getter进而访问this._data[key],这样就又触发了当前添加的getter执行某些操作
proxy代码:
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
observe代码:
observe (value: any, asRootData: ?boolean): Observer | void {
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert && // 新添加属性转为reactive
!isServerRendering() && // 非服务端渲染
(Array.isArray(value) || isPlainObject(value)) && // 数组或者对象
Object.isExtensible(value) && // 可扩展对象
!value._isVue // 非Vue实例
) {
ob = new Observer(value) // 创建Observer实例
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this) // value.__ob__ = this, 且__ob__为不可枚举属性
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys) // value.__proto__ = Array.prototype
this.observeArray(value)
} else {
this.walk(value)
}
}
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 (
obj: Object, // vm instance of Vue
key: string, // '$attr' 等属性名
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
let childOb = !shallow && observe(val) // 对当前属性的值继续进行observe
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
...
},
set: function reactiveSetter (newVal) {
...
}
})
}
仍然逃脱不了粘贴代码,但是找不出比代码更直观的解释了...
不过核心方法是defineReactive,它仍然是使用defineProperty对vm._data中的每一个属性设置了getter/setter,至于getter/setter中的内容,先不去管他。
到了这里,对于data的初始化已经告一段落。
模板编译/挂载阶段
这里是Vue.prototype.$mount的执行阶段,此阶段其实包含了对于模板的编译、对编译结果进行转化生成render函数、render函数的执行进行挂载
这个阶段对于data的操作只存在于render函数的执行进行挂载时,核心函数的执行:new Watcher(vm, updateComponent, noop, null, true)
Watcher代码:
class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function, // 回调函数
options?: ?Object,
isRenderWatcher?: boolean // render时实例的Watcher
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy
this.deps = [] // 依赖列表
this.newDeps = [] // 新的依赖列表
this.depIds = new Set() // 依赖ids
this.newDepIds = new Set() // 新的依赖ids
// parse expression for getter
// 将表达式 expOrFn包装为getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
* 源码其实已经给出了注释,这里是进行render和依赖收集
*/
get () {
pushTarget(this) // 为Dep.target赋值为当前Watcher实例
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 这里是render函数
} catch (e) {
...
} finally {
if (this.deep) {
traverse(value)
}
popTarget() // 弹出Deptarget
this.cleanupDeps() // 本次添加的依赖落入this.deps,同时清空this.newDeps
}
return value
},
addDep (dep: Dep) { // 将Dep实例添加至this.newDeps队列中,这里的Dep实例产生自通过defineReactive为data属性定义getter/setter时,也就是说这里的Dep实例对应一个data属性
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)
}
}
}
...
}
从上面的代码可以看出,响应式相关的核心在于所谓的“依赖收集”,也就是在render函数执行的过程中势必会对页面渲染需要的data属性进行读取,这就触发了响应data属性的getter,还记得之前省略掉的observe函数中执行defineReactive函数时有关data属性getter函数相关的代码吗?
defineReactive (
obj: Object, // vm instance of Vue
key: string, // '$attr' 等属性名
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) { // 在$mount函数中new Watcher进行依赖收集的时候已经为Dep.target赋值为Watcher实例
dep.depend() // 这里的Dep实例对应当前data属性,此处会将当前dep实例放入watcher的依赖列表中
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// setter相关代码
...
}
})
}
// dep.depend 代码:
...
depend () {
if (Dep.target) { // 此处Dep.target已被赋值为Watcher实例
Dep.target.addDep(this)
}
}
...
页面首次渲染时的小结
页面的首次渲染基本上包含上述两个大的过程,这里先主要基于data进行讨论
new Vue(options)中主要做了:
- 将data函数转为data对象赋值给vm._data,此时访问data属性可以通过vm._data[key];
- 通过defineProperty进行一层代理,访问vm[key] 将返回vm._data[key],方便使用;
- 遍历vm._data,执行defineReactive函数,为vm._data的每一个属性添加getter/setter,在getter中进行依赖收集,在setter进行通知响应;
$mount中主要做了:
- 编译模板/生成渲染render函数;
- 通过实例Watcher对象,在执行render函数时,页面渲染所用到的data属性会被访问,从而触发vm._data[key]的getter,
在getter中将当前属性对应的dep实例添加至Watcher实例的deps列表中,同时将Watcher实例添加进dep的subs观察者列表中;
当data属性值发生变化时
为什么data属性变化了,页面会重新渲染得到更新呢?前面做了很多铺垫,接下来看一下data属性的变更会进行哪些操作
还记得前面提到得通过defineReactive函数为vm._data[key]设置得setter吗?当data变化时会触发该setter
...
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) { // 如果更新前后值相同,则直接返回
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal) // 如果newVal为引用类型,则对其属性也进行劫持
dep.notify() // 这里才是属性更新触发操作得核心,它会通知Watcher进行相应得更新
}
...
// dep.notify方法
...
notify () {
const subs = this.subs.slice() // 这里存放的是观察者Watcher列表
for (let i = 0, l = subs.length; i < l; i++) { // 通知每一个Watcher,执行其update方法,进行相应更新
subs[i].update()
}
}
...
// Watcher.prototype.update方法
...
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) { // 同步更新
this.run()
} else {
queueWatcher(this) // 这个方法是nextTick中去执行Watcher.prototype.run方法,也就是说data属性更新触发setter然后通知Watcher去update这个过程通常并非同步执行的,而是会先被放入一个队列,异步执行,落地到我们使用中:我们不用担心同时修改多个data属性带来严重的性能问题,因为其触发的更新并非同步执行的;还有一点是Watcher.prototype.run方法中会执行get方法(还记得在首次渲染进行依赖收集的时候有这个方法吗?)该方法中会执行render进行vnode生成,当然会访问到data中的属性,这样就是一个依赖更新的过程,是不是一个闭环?
}
}
...
queueWatcher(this)
这个方法是nextTick中去执行Watcher.prototype.run方法,也就是说data属性更新触发setter然后通知Watcher去update这个过程通常并非同步执行的,而是会先被放入一个队列,异步执行,落地到我们使用中:我们不用担心同时修改多个data属性带来严重的性能问题,因为其触发的更新并非同步执行的;
还有一点是Watcher.prototype.run方法中会执行get方法(还记得在首次渲染进行依赖收集的时候有这个方法吗?)该方法中会执行render进行vnode生成,当然会访问到data中的属性,这样就是一个依赖更新的过程,是不是一个闭环?
另外不能忽略的一点是,在这个方法执行中还会触发updated 钩子函数,当然这里不做深入研究,只做一个大致了解,因为Vue中的细节很多,但是它不影响我们了解主要流程。
最后
放两张在debugg源码时写的两张图,只有自己能看懂当初想到哪里。。。
THE END
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。