谈谈对Vue2响应式系统的理解
对于Vue的响应式系统随着框架不断的迭代,使用者不断的学习,对其已经相当的熟悉。再加上Vue官方文档上也有对于响应式系统的深入讲解。使我们对其实现的原理也有了足够的了解。
现在是时候通过Vue的源码来对响应式系统进行一个细致的了解了。
响应式系统的构成及局限
通过官方文档我们都知道Vue的响应式系统是通过Object.defineProperty
方法来设置一个普通js对象的属性描述符。进而通过自定义js对象属性的getter/setter实现了数据拦截的效果。使其可以在获取对象的某个属性时记录下将会如何使用此数据,在设置对象的对应属性时触发对应的记录,再次合理使用该数据。而整个响应式系统因为Object.defineProperty
只能对纯对象的属性进行描述又因此产生了对应的限制:
- 只能对既有的属性进行描述符重写,新添加属性则无法享受拦截效果。可使用Vue.set/vm.$set手动重写,使其被拦截。
- 无法检测到删除属性操作。可使用Vue.delete/vm.$delete手动去除去依赖并触发已收集依赖。
- 无法检测数组变异方法改变了数组中的数据。Vue重写了数组变异方法,并通过增加中间原型或重写实例方法的实现来实现了对变异方法的检测。
- 无法检测数据下标操作。可使用Vue.set/vm.$set方法触发数组的splice变异方法实现可响应。
- 无法检测Map,Set数据集的操作。
即便具有一定的局限性,但是Vue的响应式系统依旧是优秀且便利的。它让我们在开发过程中将关注点更多的聚焦到数据变动及处理上。而整个响应式系统在Vue中只主要依靠三个大类就实现了:
- Observer类,标记及转换类(标记者)。通过这个类将普通的js数据对象进行了转换,数据的getter/setter拦截器就是在初始化此实例对象时通过通用方法defineReactive进行的。
- Dep类,依赖(观察者)收集/触发的中间类(收集者)。在__ob__实例对象中收集记录下对象、数组的全部依赖,为Vue.set, Vue.delete方法提供触发依赖。在Watcher中提供唯一标识避免依赖的重复收集。
- Watcher类,观察者类(观察者)。实际的依赖收集,及变化通知实现者。
响应式系统的初始化
在Vue中有很多数据都和响应式系统有关,而其中组件状态是最直接的。其整个数据对象都被转换为响应式数据,以便利的控制组件视图。通过Vue初始化来看整个响应式系统的运行。以下将主要记录对响应式系统的运行解析和学习,不会花太多篇幅在vue的初始化上。后续会继续学习补充vue的运行流程、编译函数、patch vnode的diff算法解析。
// src/core/instance/state.js
// 初始化组件状态
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 先初始化props
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
// 初始化数据,并传入了组件实例
// 其在props之后,故可以在data选项中使用props初始化data的值
if (opts.data) {
initData(vm)
} else {
// 未传递data选项,初始化为空对象
observe(vm._data = {
}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {
}
if (!isPlainObject(data)) {
data = {
}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
// 对props data methods 三个选项做命名冲突的校验
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
// method命名与data冲突,需要修改method命名
if (methods && hasOwn(methods, key)) {
warn(
`Method "${
key}" has already been defined as a data property.`,
vm
)
}
}
// props与data命名冲突,需要重命名data
// 因为不会代理data的数据到vm上
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${
key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
// 检测是否为$ _开头的命名
// 因vue自身数据都以$或_开头 故不会代理此种数据到vm实例
} else if (!isReserved(key)) {
// 将_data数据代理到vm实例上
// 这样开发者才能通过this直接取到_data中的数据
proxy(vm, `_data`, key)
}
}
// observe data
// 进行响应式数据标记、转换
observe(data, true /* asRootData */)
}
接下来就将进入响应式数据的转换步骤中。
// src/core/instance/observe/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 转换数据的方法还会在其他地方调用,所以需要校验参数
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 已经转换过的数据,取出其标记对象__ob__
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
/*
以上条件:
1. shouldObserve开关开启
2. 非服务端渲染
3. 纯对象或数组数据
4. 可拓展对象
5. 非vue实例
满足这些条件将进行转换,由此就知道只有数组和纯对象可以转换
*/
ob = new