vue原理解析(一)——通过Object.defineProperty深入理解数据代理和数据劫持
前言
Object.defineProperty
是vue2
为实现响应式所用到的js中的一个重要的API,我们只有在掌握了这个核心方法之后,才能更深入地理解vue原理。
接下来让我们通过源码解析及手写代码的方式由浅入深地理解Object.defineProperty
方法有什么作用,以及vue2
中是怎么使用这个方法的,本篇重点主要在于数据代理和数据劫持方面,所以在对vue2
源码的解析及代码实现中只会着重讲解这部分。
什么是Object.defineProperty
Object.defineProperty
是js提供的一个方法,该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
语法
Object.defineProperty(obj, prop, descriptor)
参数
obj(必传)
要定义属性的对象。也就是我们的目标对象,即我们要在哪个对象上进行属性的定义或修改。
prop(必传)
要定义或修改属性的名称。
descriptor(必传)
要定义或修改的属性描述符。可以这么理解,该参数是一个对象,我们可以通过这个对象的一些属性去定义目标属性的特性。
descriptor描述符
数据描述符
value
该属性对应的值,默认为undefined
示例-设置value
<script>
let divInfo = {
width: 300
}
Object.defineProperty(divInfo,'height',{
value: 200
})
console.log(divInfo)
</script>
我们在js中定义一个divInfo
对象,然后通过Object.defineProperty
方法为该对象添加一个新属性height
,接着我们打印divInfo
查看结果
接着我们尝试一下修改新增的属性,并且打印一下divInfo
的可枚举属性
示例-属性值能否被修改
<script>
let divInfo = {
width: 300
}
Object.defineProperty(divInfo,'height',{
value: 200
})
divInfo.height = 300
console.log(divInfo)
console.log(Object.keys(divInfo))
</script>
可以看到,虽然我们对height
重新赋值为300,但并没有改变divInfo
中的height
属性值,并且divInfo
的枚举属性中没有我们新增的height
属性。
也就是说通过Object.defineProperty
方法新增的属性默认不支持修改,且该属性不可枚举。
writable
该属性是否可被修改,默认为false
接着上面的示例,我们设置writable
为true
示例-设置writable
<script>
let divInfo = {
width: 300
}
Object.defineProperty(divInfo,'height',{
value: 200,
writable: true
})
divInfo.height = 300
console.log(divInfo)
</script>
可以看到,设置writable
为true
后,height
属性就支持更改
enumerable
该属性是否支持枚举,在枚举对象属性时会被枚举到(for...in
或 Object.keys
方法),默认为false
示例-设置enumerable
<script>
let divInfo = {
width: 300
}
Object.defineProperty(divInfo,'height',{
value: 200,
enumerable: true
})
console.log(Object.keys(divInfo))
</script>
设置enumerable
为true
后,height
属性就支持枚举
configurable
该属性是否支持删除或重新修改描述符,默认为false
下面查看示例,如果我们不设置configurable
,看新增属性能否被删除
示例-属性值能否被删除
<script>
let divInfo = {
width: 300
}
Object.defineProperty(divInfo,'height',{
value: 200
})
delete divInfo.height
console.log(divInfo)
</script>
查看打印结果,发现height
属性还在,说明通过Object.defineProperty
新增的属性默认不支持删除
接着修改一下示例,设置configurable
为true
示例-设置configurable
<script>
let divInfo = {
width: 300
}
Object.defineProperty(divInfo,'height',{
value: 200,
configurable: true
})
delete divInfo.height
console.log(divInfo)
</script>
查看打印结果,height
属性已被删除
下面我们再写个示例验证一下,设置configurable
为true
后可以重新修改描述符
示例-描述符能否被修改
<script>
let divInfo = {
width: 300
}
Object.defineProperty(divInfo,'height',{
value: 200
})
console.log('1:准备开始重写')
divInfo.height = 300
console.log('2:查看重写有没生效',divInfo)
console.log('3:开始重设writable属性')
Object.defineProperty(divInfo,'height',{
value: 300,
writable: true
})
console.log('4:再次准备开始重写')
divInfo.height = 500
console.log('5:查看重写有没生效',divInfo)
</script>
我们看到当我们想要重设divInfo
对象属性的描述符时,控制台会报错,这是因为configurable
默认为false
,即不允许修改描述符。
接着,我们在一开始定义属性的地方加上configurable
为true
示例-设置configurable验证描述符修改
<script>
let divInfo = {
width: 300
}
Object.defineProperty(divInfo,'height',{
value: 200,
configurable: true
})
console.log('1:准备开始重写')
divInfo.height = 300
console.log('2:查看重写有没生效',divInfo)
console.log('3:开始重设writable属性')
Object.defineProperty(divInfo,'height',{
value: 300,
writable: true
})
console.log('4:再次准备开始重写')
divInfo.height = 500
console.log('5:查看重写有没生效',divInfo)
</script>
发现控制台这次没有报错,且设置configurable
为true
后,我们可以再次修改属性的描述符writable
存取描述符
注意:当使用了存取描述符,不能再使用数据描述符中的
value
和writable
get
该属性的getter
函数,当访问该属性时,会调用此函数,该函数的返回值会被用作属性的值。 默认为undefined
set
属性的setter
函数,当修改该属性时,会调用此函数,该函数接受一个参数(被赋予的新值)
接下来我们写一个示例来感受一下get和set
示例-设置get和set
let divInfo = {
width: 300
}
Object.defineProperty(divInfo,'height',{
get: function() {
console.log('height属性被获取')
return 200
},
set: function(value) {
console.log(`height属性被修改为${value}`)
}
})
console.log('1:开始获取height属性')
console.log(divInfo.height)
console.log('2:开始修改height属性')
divInfo.height = 400
打印结果如下,当我们访问divInfo.height
时触发了get
函数,get
函数的返回值200作为height
属性的值;当我们重新对height
属性赋值时,触发了set
函数,set
函数会接受我们对height
属性赋的新值
但是,在上个示例中,我们的get函数返回的是一个固定值200,那么该如何返回我们设置的值,保证属性每次被修改后,再次获取时获取到的是修改后的最新值?
很简单,我们定义一个初始值就可以
示例-设置get和set
let divInfo = {
width: 300
}
let originValue = 200
Object.defineProperty(divInfo,'height',{
get: function() {
return originValue
},
set: function(value) {
originValue = value
}
})
console.log('1:初始获取height属性')
console.log(divInfo.height)
console.log('2:开始修改height属性')
divInfo.height = 400
console.log('3:再次获取height属性')
console.log(divInfo.height)
好了,到这里,我们已基本上了解并掌握了Object.defineProperty
这个方法,那么这个方法在vue2
中是如何使用的呢?
Object.defineProperty在vue2中的使用
数据代理
我们知道,在vue中数据的定义都写在data
里面,data
可以是一个对象,也可以是一个函数
// data是一个对象
<script>
let vm = new Vue({
data: {
name: 'John'
}
})
</script>
// data是一个函数
<script>
let vm = new Vue({
data() {
return {
name: 'John'
}
}
})
</script>
定义好data
后,我们通过this.name
(this = vue实例)就可以访问定义在data
下的数据,说明data
下的这些数据挂载到了vue
实例上,那么vue2
是如何实现这种数据代理的呢?
答案就在我们的标题中
源码解析
接下来我们就带着疑问在vue2
源码(本篇以2.5.2为例)中去查找Object.defineProperty
,根据Object.defineProperty
去溯源
initState
vue2
在初始化initState
的时候有一个initData
方法会对data
进行初始化
function initState (vm) {
vm._watchers = [];
var 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
我们接着查看initData
这个方法,可以发现以下几点
- 在初始化数据的时候,
vue2
先定义了一个_data
的属性用来存储我们定义的data
,并且把_data
挂载到了vue
实例中 vue2
在获取我们定义的data
的时候,会先进行判断,判断data
是否为function
,如果为function
则执行该function
获取数据,否则直接获取,这也就是为什么我们可以定义data
为一个对象,也可以定义data
为一个函数vue2
在代理数据之前,会进行判断,判断属性名称是否与方法名称或父级传值属性名称重复,如果重复则报错- 最终我们找到了
proxy
这个方法,这个方法就是代理数据的方法,当然这个方法是在循环data
的key
值中调用的
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) {
data = {};
"development" !== '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
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
if (props && hasOwn(props, key)) {
"development" !== '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 */);
}
proxy
接着查看proxy
方法,可以看到传入的target
参数就是vue实例,vue2
循环data
中的属性调用Object.defineProperty
方法为每个属性设置了一个getter
和setter
,并且将属性挂载到了vue实例中,从而实现了数据代理。当我们访问this.xxx
的时候,实际上是访问的this._data.xxx
,我们对基础类型的数据赋值的时候this.xxx = '123'
,实际上是对this._data.xxx
重新赋值。
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
总结数据代理流程
vue2
在初始化的时候,会在initState
方法中判断有没有定义data
,如果有则执行initData
方法- 在
initData
方法中- 首先会定义一个
_data
属性存储data
,并将_data
挂载到实例中,方便后续操作 - 其次进行一系列的判断,保证
data
中的属性合法 - 利用
Object.keys
循环遍历data
中的key
值,调用proxy
方法处理数据代理
- 首先会定义一个
- 在
proxy
方法中,调用Object.defineProperty
方法,在vue实例中定义属性(属性名称为data
中的key
值,属性值通过key
从实例中的_data
属性取值),从而将data
中的属性挂载到vue实例上,实现数据代理
手写代码
了解了Object.defineProperty
方法和vue2
中的数据代理原理之后,接下来我们尝试着自己手写代码实现数据代理的效果
创建Vue类
我们在新建vue实例的时候,一般会写这样的代码,Vue实际上是一个构造函数,我们会往这个构造函数中传入一系列options(例如el、data、生命周期、methods等),我们称这种方式为options API
<script>
let vm = new Vue({
el: '#app',
data() {
return {
}
},
...
})
</script>
那么我们就可以利用es6的构造函数来创建一个Vue类,新建一个index.js
文件,定义一个类,类名为Vue
class Vue {
constructor(options) {
console.log('获取到传入的数据',options)
}
}
验证Vue类
新建一个index.html
文件,引入index.js
,创建一个Vue实例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>手写数据代理</title>
</head>
<body>
<script src="./index.js"></script>
<script>
let vm = new Vue({
data: {
divInfo: {
width: 300,
height: 200
},
testMessage: '测试'
}
})
</script>
</body>
</html>
访问index.html
,查看控制台打印数据,能够正确打印,说明Vue类创建成功
完善Vue类
index.js
class Vue {
constructor(options) {
let vm = this;
// 先将传入的options存储起来,挂载到实例上
vm.$options = options
this.initState(vm)
}
// 初始化
initState(vm) {
let options = vm.$options
// 如果有定义data,则初始化数据
if(options.data) {
this.initData(vm)
}
}
// 初始化数据
initData(vm) {
let data = vm.$options.data
// 将data存储到_data中,并将_data挂载到实例上
data = vm._data = typeof(data) === 'function'
? data.call(vm)
: data || {}
// 获取data的key
let keys = Object.keys(data)
for(let i = 0; i < keys.length; i++) {
this.proxy(vm,'_data',keys[i])
}
}
// 数据代理
proxy(vm,target,key) {
Object.defineProperty(vm,key,{
enumerable: true,
configurable: true,
get: function proxyGetter() {
console.log('【Vue类】获取数据')
return vm[target][key]
},
set: function proxySetter(value) {
console.log('【Vue类】修改数据为',value)
vm[target][key] = value
}
})
}
}
index.html
<script>
let vm = new Vue({
data: {
divInfo: {
width: 300,
height: 200
},
testMessage: '测试'
}
})
console.log('1:开始获取数据')
console.log('获取到的数据为',vm.testMessage)
console.log('2:开始修改数据')
vm.testMessage = '更改了测试数据'
console.log('3:再次获取数据')
console.log('获取到的数据为',vm.testMessage)
</script>
查看打印结果
在控制台输入vm
查看数据挂载情况,可以看到,我们已基本实现数据的代理
数据劫持
当我们实现了数据代理之后,只是完成了把data
中定义的属性挂载到vue实例中去,并且只实现了针对data
中基础类型数据的劫持,也就是说,当我们在data
中定义了一个对象或数组时,以目前的数据代理方法并不能监听到对象中属性的变化和数组的变化。
所以vue2
中还会对data
中的复杂类型数据进行数据劫持,针对对象,会循环遍历对象中定义的每个属性,都为其设置一个getter
和setter
,针对数组,会重写一些数组方法,为后续的响应式做好准备。
源码解析
initData
我们发现在vue2源码的initData
方法中,还调用了observe
方法
function initData (vm) {
var data = vm.$options.data;
...
// observe data
observe(data, true /* asRootData */);
}
observe
接着查看observe
方法,在该方法中,进行了如下处理:判断数据类型,如果不为object
则直接return
,否则就return
一个Observer
类
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
Observer
查看Observer
类,这里面对数据进行了判断,如果数据为数组,则重写一些数组方法,并执行observeArray
对数组中每项数据继续调用observe
方法深入观察;如果数据为对象,则执行walk
方法,walk
方法会循环对象中每个属性,调用defineReactive
方法
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
} else {
this.walk(value);
}
};
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]]);
}
};
/**
* Observe a list of Array items.
*/
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
defineReactive
查看defineReactive
方法,这是vue处理响应式的核心方法,在这个方法中,会对对象中的每个属性都添加一个getter
和setter
,并且会递归调用observe
处理对象中的属性还是一个对象的情况,修改数据时,如果修改的新值是一个对象,又会继续调用observe
/**
* Define a reactive property on an Object.
*/
function defineReactive (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var 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) {
var 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 ("development" !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
重写数组方法
因为Object.defineProperty
本身是针对对象进行数据劫持,处理不了数组,而数组的某些方法会更改原数组,如果不对这些方法进行拦截,就无法对数据进行响应式处理。所以为了实现对这些方法的拦截,就需要重写这些数组方法。
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var result = original.apply(this, args);
var ob = this.__ob__;
var 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
});
});
总结数据劫持流程
- 在初始化数据
initData
的方法中,调用observe
对传入数据进行观察 - 在
observe
方法中,先判断数据类型,如果为基本类型,则不执行后续操作,否则调用Observer
方法 Observer
方法中- 如果数据为数组,则调用
observeArray
方法循环观察数组中每项数据 - 如果数据为对象,则直接调用
walk
方法,通过defineReactive
方法为每个属性设置响应属性
- 如果数据为数组,则调用
defineReactive
方法中- 传入的数据有可能还是一个数组或对象,所以先对传入的数据调用
observe
方法做递归处理 - 通过
Object.defineProperty
劫持数据,设置getter和setter - 在
set
方法中,因为设置的新值也有可能是复杂类型数据,所以对设置的新值也要执行observe
观察
- 传入的数据有可能还是一个数组或对象,所以先对传入的数据调用
- 针对数组,重写数组方法
- 监听能引起原数组改变的方法,做响应式处理,达到数据变化,视图更新
- 数组新增的元素,有可能是对象,对象又需要进行数据劫持做响应式处理
手写代码
了解了数据劫持原理后,我们就可以尝试着自己来实现数据劫持。
定义observe
方法
observe
方法中,主要是先将基础类型判断过滤,再执行Observer
function observe(data) {
if(typeof(data) !== 'object' || data === null) {
return
}
let ob
if(Object.prototype.hasOwnProperty.call(data,'__ob__') && data.__ob__ instanceof Observer) {
ob = data.__ob__
}else {
ob = new Observer(data)
}
return ob
}
定义Observer
类
Observer
类主要处理对象和数组,给对象和数组中各项数据的对象绑定响应式属性
class Observer {
constructor(data) {
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false,
writable: true,
configurable: true
});
if(Array.isArray(data)) {
// 将重写的数组方法放入data的原型链上
data.__proto__ = arrayMethods
this.observeArray(data)
}else {
this.walk(data)
}
}
walk(obj) {
let keys = Object.keys(obj)
for(let i = 0; i < keys.length; i++) {
defineReactive(obj,keys[i],obj[keys[i]])
}
}
observeArray(array) {
for(let i = 0; i < array.length; i++) {
observe(array[i])
}
}
}
defineReactive
defineReactive
中首先对传入数据进行递归观察,再进行拦截,对set
中设置的新值再进行观察
function defineReactive(obj,key,value) {
observe(obj[key])
Object.defineProperty(obj,key,{
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log(`【reactiveGetter】获取数据${key}`)
return value
},
set: function reactiveSetter(newValue) {
if(newValue === value) {
return
}
observe(newValue)
console.log(`【reactiveSetter】修改数据${key}为`,newValue)
value = newValue
}
})
}
重写数组方法
// 保存数组原型上的方法
let arrayProto = Array.prototype
// 定义一个新的对象,拷贝数组原型方法
let arrayMethods = Object.create(arrayProto)
// 要重写的方法
let needEditMethods = [
'push', // 往数组最后添加一个或多个元素
'pop', // 删除数组中最后一个元素
'shift', // 删除数组中第一个元素
'unshift', // 往数组开头添加一个或多个元素
'splice', // 在数组指定位置删除或添加元素
'sort', // 数组排序
'reverse' // 数组反转
]
needEditMethods.forEach(method => {
arrayMethods[method] = function() {
// 类数组转为数组
let args = Array.prototype.slice.call(arguments)
// 执行原数组方法
let result = arrayProto[method].apply(this,args)
let ob = this.__ob__
// 定义一个数组,储存新增加的数据
let insertedArray
switch(method) {
case 'push':
case 'unshift':
insertedArray = args
break
case 'splice':
// splice有三个参数,1:起始位置,2:个数,3:要插入的一个或多个值,这里取第3个参数
insertedArray = args.slice(2)
break
}
if(insertedArray) {
ob.observeArray(insertedArray)
}
return result
}
})
在initData方法中调用observe
initData(vm) {
let data = vm.$options.data
// 将data存储到_data中,并将_data挂载到实例上
data = vm._data = typeof(data) === 'function'
? data.call(vm)
: data || {}
// 获取data的key
let keys = Object.keys(data)
for(let i = 0; i < keys.length; i++) {
this.proxy(vm,'_data',keys[i])
}
observe(data)
}
验证代码
到这里,我们已基本实现了一个简陋版的vue数据代理和数据劫持,接下来我们验证一下自己写的代码。 在html中定义一些嵌套对象和数组
let vm = new Vue({
data: {
divInfo: {
style: {
width: 200,
height: 200
},
class: 'test'
},
dataList: [
{ id: 1, name: '111' },
{ id: 2, name: '222' }
],
testMessage: '测试'
}
})
在浏览器控制台输入vm,查看数据
继续展开dataList的原型,查看重写的数组方法
可以看到:
- 定义的每个数据
divInfo
、dataList
、testMessage
都设置好了proxyGetter和proxySetter代理 - 对象
divInfo
中每个属性都设置了reactiveGetter和reactiveSetter,实现了数据劫持 - 数组
dataList
中每一项中的属性id
和name
都有reactiveGetter和reactiveSetter - 数组
dataList
原型链中有我们重写的数组方法
接着,测试更改对象值,修改divInfo下style对象中的width属性
分别触发divInfo
的代理get,divInfo
的响应式get,divInfo
下的嵌套对象style
的响应式get,最后触发style
对象中width
属性的响应式set
测试增加数组元素,往数组dataList
新增对象,新增的对象也被设置了响应式get和set
总结
为了更清晰的了解原理和理清整个流程,我用一张流程图来进行总结。
转载自:https://juejin.cn/post/7212529038874886203