以前个人一篇文章vue响应式原理学习(一)讲述了vue数据响应式原理的一些简单知识。 众所周知,
Vue
的data
属性,是默认深度监听的,此次咱们再深度分析下,Observer
观察者的源码实现。javascript
既然data
属性是被深度监听,那咱们就首先本身实现一个简单的深拷贝,理解下思路。html
深拷贝的原理有点像递归, 其实就是遇到引用类型,调用自身函数再次解析。vue
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对象等等。要写好一个深拷贝,不简单。java
有的同窗可能会问,为何不直接一个 for in
解决。以下:react
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
上扩展的方法。api
又有的同窗可能会说,加 hasOwnProperty
解决呀。若是是 Object
类型,确实能够解决,但如何是 Array
的话,就获取不到数组的索引啦。数组
说到 for in
,再加个注意项,就是 for in
也是能够 continue
的,而数组的 forEach
方法不能够。由于 forEach
的内部实现是在一个for
循环中依次执行你传入的函数。函数
这里我主要是为代码添加注释,建议看官们最好打开源码来看。post
代码来源:Vue项目下的 src/core/observer/index.js
学习
Vue 将 Observer
封装成了一个 class
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)
new Observer(data)
new Observer(data)
会在 data
对象 上扩展一个不可枚举的属性 __ob__
,这个属性有大做用。data
是个数组
observeArray(data)
。这个方法会遍历data
对象,并对每个数组项执行observe
。以后的流程参考第2步data
是对象
walk(data)
。这个方法会遍历data
对象,并对每个属性执行 defineReactive
。defineReactive
内部会对传入的对象属性执行 observe
。以后的流程参考第2步篇幅和精力有限,关于 protoAugment
和copyAugment
的做用,defineReactive
内如何收集依赖与触发依赖的实现,往后再说。
文章内容若是有错误之处,还请指出。
参考: