之前我的一篇文章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) {
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 ) {
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
内如何收集依赖与触发依赖的实现,日后再说。
文章内容如果有错误之处,还请指出。
参考:
JavaScript 如何完整实现深度Clone对象
Vue 技术内幕