基于Vue2源码,寻找defineProperty To Proxy的原因
前言
大家都知道,新推出Vue3的中采用Proxy API对数据进行响应式处理,而弃用了defineProperty API,至于为什么,我把答案放在了最后,感兴趣的朋友记得读下去喔,(✿◡‿◡)
严格来说,对于Object.defineProperty对应数据劫持方式,而Proxy对应数据代理,只是代理过程存在一层拦截处理
下载
如果你也对Vue2中这一部分的代码感兴趣,想尝试Vue2源码阅读,可以把源码下载到本地,进行代码阅读。
- 输入命令
git clone https://github.com/vuejs/vue.git
- 注意
Vue.js的源码利用了Flow做了静态类型检查,所以存在一些静态类型检查语法
源码目录介绍
src
├── compiler # 编译相关
├── core # 核心代码
├── platforms # 不同平台的支持
├── server # 服务端渲染
├── sfc # .vue 文件解析
├── shared # 共享代码(工具方法)
core目录: 是Vue.js的核心代码目录,包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数等等。
当然这篇文张主要分析数据劫持的过程,当然自己感兴趣的话可以尝试阅读其他部分的内容。
我们从src入口开始,找到core下的observer文件,好,平常我们被问到最多的数据劫持,双向绑定(除模版解析外)的代码都在这个observer文件目录里面
- 核心文件介绍
observer
├── array.js # 手动为数组提供视图更新
├── dep.js # 发布订阅器实现
├── index.js # 数据劫持主文件入口
├── watcher.js # 模版解析过程,所需的订阅者
核心
对于这部分,我只梳理数据劫持部分的代码,所以如果对于vue响应式原理感兴趣的小伙伴们,可以尝试下这篇文章。
找到observer下的index.js文件正式开始我们的内容。
我们把observer/index.js打开,当然这里面的代码我们从哪里开始呢?
所以我们要找到开始数据Vue实例化过程中,数据劫持的入口。
1. 找到整个Vue的数据初始化入口
找到Vue实例文件,在src/core/instance/index.js中:
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
看一眼,有个init方法,所以是在数据劫持初始化肯定在这里,Vue本身没有这方法,所以应该是原型上方法,找下initMixin,...下面这几个中有没有init定义
good,initMixin方法中找到了。
看到instance/init.js中:
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
数据观测应该在created生命周期之前,beforeCreate之后,在看下函数命名,应该在initState中。
来到instance/state.js下:
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)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
明显就是initData和observe那,同文件看到initData定义,好家伙终于看到了核心代码
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
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
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
找到数据劫持关键observe(data, true /* asRootData */),我们回到observer/index.js文件下
2. 重回observer/index.js
2.1 observer方法:
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
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
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
上面代码比较简单,首先判断是否是不是对象或者是虚拟节点,不然直接退出,然后判断
自身属性__ob__,shouldObserve是否观测(默认true),isServerRendering是否服务端渲染,isExtensible属性是否还存在扩展性,等等,然后进入实例化内容new Observer(value)
2.2 Observer 实例对象
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods) // 自行阅读
} else {
copyAugment(value, arrayMethods, arrayKeys) // 自行阅读
}
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])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
上面代码中protoAugment,copyAugment希望能自己阅读下哈,就不说了
不过,上面的源码中我们看到了一个问题,在Observer实例化的过程中,代码中判断是否为数组,然后进行分开处理,但是为什么要这样做呢?
- Object.defineProperty对数组的不友好
来看个例子:
var list = ["name","age","sex"];
console.log(Object.keys(list));
Object.keys(list).forEach((key)=>{
let value = list[key]
Object.defineProperty(list, key,{
get: function(){
console.log("getting is running")
return value
},
set: function(newValue){
console.log("setting is running")
value = newValue
}
})
})
console.log(list[0]);
list[0] = "name123";
console.log(list);
console.log(Array.isArray(list)); // true
上面的例子中,我们如果以index下标为key值,上面看着是正常的,但确有一个问题,打印list,会发现原来的数组list变成了[ [Getter/Setter], [Getter/Setter], [Getter/Setter] ],get、set的方式进行存储数据
当然这一点上没有什么问题,defineProperty可以实现对数组类型的数据劫持。
但是我们从另一个角度来考虑,数组如果很长,那就会产生频繁的defineProperty调用,这从性能角度来讲是不友好的,这也采用Proxy的重要原因。
- 我们来看下Proxy怎么进行数组数据处理
const list = [];
const arr = new Proxy(list,{
get(target,key, receiver){
console.log(key);
console.log(typeof key);
console.log("getting is running");
return Reflect.get(target,key)
},
set(target, key, value){
console.log("setting is running");
return Reflect.set(target, key, value)
}
})
arr[0] = "min"; // setting is running
console.log(list); // ["min"]
console.log(arr); // Proxy {0: "min"}
console.log(arr[0]); // 0 ; string ; getting is running ; min
上面看到使用Proxy,数组list还是原来的数组list,且仍然可以对数据进行劫持操作,而且并不会出现大面积的性能问题。
- 代理对象为数组,且使用数组方法的情况
const list = [];
const arr = new Proxy(list,{
get(target,key, receiver){
console.log(`getting: key ${key}`);
return Reflect.get(target,key)
},
set(target, key, value){
console.log(`setting: key ${key}, value: ${value}`);
return Reflect.set(target, key, value)
}
})
arr.push("min");
想一想,上面的执行情况是怎么样的,自己打印下结果
是不是发现了Proxy的强大,四次操作分别对应执行取push, 取值length,赋值数组第0项,赋值length的操作。
我们继续回到observer/index.js的代码中
2.3 对于数组类型的处理
由于defineProperty的性能缺陷,Vue2中对数组的处理主要采用手动触发更新的方式来实现
找到observer/array.js文件:
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
上面的代码中,将数组方法模拟,然后ob.dep.notify()实现手动触发订阅,来实现视图更新的过程
上面代码中,Object.create的使用值得我们思考,避免了对Array.prototype的污染。
2.4 对象的数据劫持
Observer class中看到,对于对象的处理采用defineReactive方法,我们来看下这个方法里面写了什么内容
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
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
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
上面的代码中,涉及到dep.js,watcher.js中的内容,可以自己去找对应文件了解下
简单来说,dep.js:一个发布订阅器;watcher.js:视图依赖,模版解析时,实例化一个Watcher,Dep.target指向watcher实例,get时进行Watcher收集
Object.defineProperty进行get进行依赖收集,set进行触发更新。
上面的代码中是不是又看到了一个问题,我们来看看数据劫持中的这2行代码
let childOb = !shallow && observe(val)
childOb = !shallow && observe(newVal) // set中处理新赋值的内容
是不是看出了什么,由于defineProperty只能依据进行key值处理,所以如果数据对象存在过深的结构层级,一开始我们就会执行大量的递归调用,来保证响应式。
所以这个问题上如果存在大量数据的初始化场景下,会存在极大的性能问题,相信这也是Vue团队废弃defineProperty而使用Proxy的原因之一。
- 看下proxy对这方面的处理
function isObject (obj){
return obj !== null && typeof obj === 'object'
}
const obj = {
world: {
person: "min"
}
};
function reactive(obj){
return new Proxy(obj,{
get(target,key, receiver){
console.log(`getting: key ${key}`);
const res = Reflect.get(target,key);
return isObject(res)? reactive(res): res
},
set(target, key, value){
console.log(`setting: key ${key}, value: ${value}`);
return Reflect.set(target, key, value)
}
})
}
const newObj = reactive(obj);
//已被注释 console.log(newObj.world.person);
newObj.world.person = {min: {age: 23}};
上面的过程是是惰性的,newObj.world.person = {min: {age: 23}};会先触发world属性的get,从而又会触发person的set。
区别defineProperty一开始就递归对子数据响应式定义,Proxy是获取到深层数据时,再利用reactive进一步定义响应式。
总结
先放上一张网上普遍存在的图:
(1)第一条不太正确: defineProperty可以监听数组变动,但存在性能风险
(2)第二条不太正确:defineProperty需要递归遍历实现整个对象劫持;Proxy采用惰性的方式进行劫持,也并非是一开始就劫持整个对象
(3)(4)(5):基本正确
谢谢你读完本篇文章,希望对你能有所帮助,如有问题欢迎各位指正。
我是him8(✿◡‿◡),如果觉得写得可以的话,请点个赞吧❤。