之前我的一篇文章vue响应式原理学习(一)讲述了vue数据响应式原理的一些简单知识。 众所周知,
Vue的data属性,是默认深度监听的,这次我们再深度分析下,Observer观察者的源码实现。
先写个深拷贝热热身
既然data属性是被深度监听,那我们就首先自己实现一个简单的深拷贝,理解下思路。
深拷贝的原理有点像递归, 其实就是遇到引用类型,调用自身函数再次解析。
function deepCopy(source) {
// 类型校验,如果不是引用类型 或 全等于null,直接返回
if (source === null || typeof source !== 'object') {
return source;
}
let isArray = Array.isArray(source),
result = isArray ? [] : {};
// 遍历属性
if (isArray) {
for(let i = 0, len = source.length; i < len; i++) {
let val = source[i];
// typeof [] === 'object', typeof {} === 'object'
// 考虑到 typeof null === 'object' 的情况, 所以要加个判断
if (val && typeof val === 'object') {
result[i] = deepCopy(val);
} else {
result[i] = val;
}
}
// 简写
// result = source.map(item => {
// return (item && typeof item === 'object') ? deepCopy(item) : item
// });
} else {
const keys = Object.keys(source);
for(let i = 0, len = keys.length; i < len; i++) {
let key = keys[i],
val = source[key];
if (val && typeof val === 'object') {
result[key] = deepCopy(val);
} else {
result[key] = val;
}
}
// 简写
// keys.forEach((key) => {
// let val = source[key];
// result[key] = (val && typeof val === 'object') ? deepCopy(val) : val;
// });
}
return result;
}
为什么是简单的深拷贝,因为没考虑 RegExp, Date, 原型链,DOM/BOM对象等等。要写好一个深拷贝,不简单。
有的同学可能会问,为什么不直接一个 for in 解决。如下:
function deepCopy(source) {
let result = Array.isArray(source) ? [] : {};
// 遍历对象
for(let key in source) {
let val = source[key];
result[key] = (val && typeof val === 'object') ? deepCopy(val) : val;
}
return result;
}
其实 for in有一个痛点就是原型链上的非内置方法也会被遍历。例如开发者自己在对象的 prototype上扩展的方法。
又有的同学可能会说,加 hasOwnProperty 解决呀。如果是 Object 类型,确实可以解决,但如何是 Array 的话,就获取不到数组的索引啦。
说到 for in,再加个注意项,就是 for in 也是可以 continue 的,而数组的 forEach 方法不可以。因为 forEach的内部实现是在一个for循环中依次执行你传入的函数。
分析 Vue 的 Observer
这里我主要是为代码添加注释,建议看官们最好打开源码来看。
代码来源:Vue项目下的 src/core/observer/index.js
Vue 将 Observer 封装成了一个 class
Observer
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor(value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 每观察一个对象,就在对象上添加 __ob__ 属性,值为当前 Observer 实例
// 当然,前提是 value 本身是一个数组或对象,而非基础数据类型,如数字,字符串等。
def(value, '__ob__', this)
// 如果是数组
if (Array.isArray(value)) {
// 这两行代码后面再讲解
// 这里代码的作用是 为数组的操作函数赋能
// 也就是,当我们使用 push pop splice 等数组的api时,也可以触发数据响应,更新视图。
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
// 遍历数组并观察
this.observeArray(value)
} else {
// 遍历对象并观察
// 这里会有存在 value 不是 Object 的情况,
// 不过没事,Object.keys的参数为数字,字符串时 会 返回一个空数组。
this.walk(value)
}
}
// 遍历对象并观察
walk(obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 观察对象,defineReactive 函数内部调用了 observe 方法,
// observe 内部 调用了 Observer 构造函数
defineReactive(obj, keys[i])
}
}
// 遍历数组并观察
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
// 观察对象,observe 内部 调用了 Observer 构造函数
observe(items[i])
}
}
}
function protoAugment(target, src: Object, keys: any) {
target.__proto__ = src
}
function copyAugment(target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
上面的代码中,细心的同学可能对observe、def,defineReactive这些函数不明所以,接下来说说这几个函数
observe 函数
用来调用 Observer构造函数
export function observe(value: any, asRootData: ?boolean): Observer | void {
// 如果不是对象,或者是VNode实例,直接返回。
if (!isObject(value) || value instanceof VNode) {
return
}
// 定义一个 变量,用来存储 Observer 实例
let ob: Observer | void
// 如果对象已经被观察过,Vue会自动给对象加上一个 __ob__ 属性,避免重复观察
// 如果对象上已经有 __ob__属性,表示已经被观察过,就直接返回 __ob__
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve && // 是否应该观察
!isServerRendering() && // 非服务端渲染
(Array.isArray(value) || isPlainObject(value)) && // 是数组或者Object对象
Object.isExtensible(value) && // 对象是否可扩展,也就是是否可向对象添加新属性
!value._isVue // 非 Vue 实例
) {
ob = new Observer(value)
}
if (asRootData && ob) { // 暂时还不清楚,不过我们可以先忽略它
ob.vmCount++
}
return ob // 返回 Observer 实例
}
可以发现 observe 函数,只是 返回 一个 Observer 实例,只是多了些许判断。为了方便理解,我们完全可以把代码缩减:
// 这就清晰多了
function observe(value) {
let ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.___ob___
} else {
ob = new Observer(value)
}
return ob;
}
def 函数
其实就是 Object.defineProperty 的封装
export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
// 默认不可枚举,也就意味着正常情况,Vue帮我们在对象上添加的 __ob__属性,是遍历不到的
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
defineReactive 函数
defineReactive函数的功能较多,主要是用来 初始化时收集依赖 和 改变属性时触发依赖
export function defineReactive(
obj: Object, // 被观察对象
key: string, // 对象的属性
val: any, // 用户给属性赋值
customSetter?: ?Function, // 用户额外自定义的 set
shallow?: boolean // 是否深度观察
) {
// 用于收集依赖
const dep = new Dep()
// 如果不可修改,直接返回
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 如果用户自己 未在对象上定义get 或 已在对象上定义set,且用户没有传入 val 参数
// 则先计算对象的初始值,赋值给 val 参数
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// !shallow 表示 深度观察,shallow 不为 true 的情况下,表示默认深度观察
// 如果是深度观察,执行 observe 方法观察对象
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
// 判断值是否改变
// (newVal !== newVal && value !== value) 用来判断 NaN !== NaN 的情况
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// 非生产环境,触发用户额外自定义的 setter
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// 触发对象原有的 setter,如果没有的话,用新值(newVal)覆盖旧值(val)
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 如果是深度观察,属性被更改后,重新观察
childOb = !shallow && observe(newVal)
// 触发依赖。收集依赖和触发依赖是个比较大的流程,日后再说
dep.notify()
}
})
}
入口在哪
说了这么多,那Vue观察对象的初始化入口在哪里呢,当然是在初始化Vue实例的地方了,也就是 new Vue 的时候。
代码来源:Vue项目下src/core/instance/index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) // 这个方法 定义在 initMixin 函数内
}
// 就是这里,initMixin 函数会在 Vue 的 prototype 上扩展一个 _init 方法
// 我们 new Vue 的时候就是执行的 this._init(options) 方法
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
initMixin 函数在 Vue.prototype 上扩展一个 _init 方法,_init方法会有一个initState函数进行数据初始化
initState(vm) // vm 为当前 Vue 实例,Vue 会将我们传入的 data 属性赋值给 vm._data
initState 函数会在内部执行一段代码,观察 vm实例上的data属性
代码来源:Vue项目下 src/core/instance/state.js。无用的代码我先注释掉了,只保留初始化 data 的代码。
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)
// 如果传入了 data 属性
// 这里的 data 就是我们 new Vue 时传入的 data 属性
if (opts.data) {
// initData 内部会将 我们传入的 data属性 规范化。
// 如果传入的 data 不是函数,则直接 observe(data)
// 如果传入的 data 是函数,会先执行函数,将 返回值 赋值给 data,覆盖原有的值,再observe(data)。
// 这也就是为什么我们写组件时 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)
// }
}
总结
我们 new Vue 的时候 Vue 对我们传入的 data 属性到底做了什么操作?
- 如果我们传入的
data是一个函数,会先执行函数得到返回值。并赋值覆盖data。如果传入的是对象,则不做操作。 - 执行
observe(data)- observe 内部会执行
new Observer(data) new Observer(data)会在data对象 上扩展一个不可枚举的属性__ob__,这个属性有大作用。- 如果
data是个数组- 执行
observeArray(data)。这个方法会遍历data对象,并对每一个数组项执行observe。之后的流程参考第2步
- 执行
- 如果
data是对象- 执行
walk(data)。这个方法会遍历data对象,并对每一个属性执行defineReactive。 defineReactive内部会对传入的对象属性执行observe。之后的流程参考第2步
- 执行
- observe 内部会执行
篇幅和精力有限,关于 protoAugment和copyAugment的作用,defineReactive 内如何收集依赖与触发依赖的实现,日后再说。
文章内容如果有错误之处,还请指出。
参考: