Object.defineProperty()
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。
Object.defineProperty方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回该对象。
方法的第一个参数是对象,第二个是对象的属性,第三个参数也是一个对象,用来操作属性。该参数有如下几个常用属性及方法:
- value:属性值
- writable:是否可写
- enumerable:是否可以枚举
- configurable:是否可以被配置,默认false
- set():改变属性值时触发,需要参数
- get():访问属性时触发,不能与value或writable同时出现,需要返回值
var obj={};
Object.defineProperty(obj,'a',{
// value:3,
// 是否可写
// writable:true,
// 是否可以被枚举
enumerable:true,
// 有value或writable没get
// 外部访问a属性时被执行
get(){
console.log('a的get')
// 要有返回值
return 1
},
// 外部改变a属性时被执行
set(newValue){
console.log('a的set',newValue)
}
});
Object.defineProperty(obj,'b',{
value:5,
})
Object.defineProperty(obj,'c',{
value:6,
})
console.log(obj.a)//触发get方法 1
obj.a=10//触发set方法 10
console.log(obj.a)//触发get方法 1
在上边的代码中,obj.a=10调用set方法给a赋值10,但是再访问a,其值仍为get的返回值1。
我们需要在这两个方法之间借助一个变量实现值的改变。
var obj={};
var tmp;
Object.defineProperty(obj,'a',{
enumerable:true,
get(){
console.log('a的get')
// 要有返回值
return tmp;
},
// 外部改变a属性时被执行
set(newValue){
console.log('a的set',newValue)
tmp=newValue
}
});
console.log(obj.a)//触发get方法 undefined
obj.a=10//触发set方法
console.log(obj.a)
在这里,我们定义了一个全局变量tmp,实现了set与get之间的周转。但是,这并不是一个好的策略,因为出现了全局变量。
利用闭包实现:
var obj = {};
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable:true,
get() {
console.log('a的get')
// 要有返回值
return val;
},
// 外部改变a属性时被执行
set(newValue) {
console.log('a的set', newValue)
val = newValue
}
});
}
defineReactive(obj,'a',1)
console.log(obj.a)//触发get方法 undefined
obj.a = 10//触发set方法
console.log(obj.a)//10
外层函数的val参数就实现了闭包。
如果对象里边嵌套对象,上边的代码做不到让子对象的属性也成为响应式,所以需要实现递归。
index.js
import observe from './observe'
var obj = {
a:{
m:{
n:333
}
},
b:3
};
// 创建observe函数,注意,不是observer
observe(obj);
obj.a.m.n=345
observe.js
import Observer from './Observer'
export default function(value){
if(typeof value != 'object')return;// m[n]不再是object,返回
// 定义ob
var ob;
if(typeof value.__ob__!=='undefined'){
ob=value.__ob__;
}else{
ob=new Observer(value);
}
return ob;
}
Observer.js
import utils, { def } from './utils'
import defineReactive from './defineReactive'
export default class Observer{
constructor(value){
console.log('Observer构造器');
// 给实例(this)添加__ob__属性, 构造函数的this不是类本身,而是实例
def(value,'__ob__',this,false);// 这样的话,value对象就有了__ob__属性,第一次时obj调用,于是obj有了__ob__属性
// Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式的object(可以被侦测的)
this.walk(value)
}
// 遍历
walk(value){
for(let k in value){//obj有了__ob__,调用walk,a和b就
defineReactive(value,k) // 把value的k属性变成响应式的
}
}
}
defineReactive.js
import observe from './observe'
export default
function defineReactive(data, key, val) {
console.log('defineReactive',data,key)
if (arguments.length == 2) {
val = data[key]//当传进来(obj,a)时,val=obj[a] 最后一次递归调用是传的m[n]
}
// 子元素要进行observe,至此形成递归,这个递归不是函数自己调用自己,而是多个函数循环调用
let childOb = observe(val);//obj[a]也有了__ob__属性 传m[n]时,由于不是object,observe直接返回了,所以n是没有__ob__属性的,b也没有,因为b也不是对象
Object.defineProperty(data, key, {//第一次data是obj,key是a,于是a属性变成了响应式的,之后是(a,m),m响应式,(m,n),n响应式
enumerable: true,
configurable: true,
get() {
console.log('你试图访问'+key+'属性')
// 要有返回值
return val;
},
// 外部改变a属性时被执行
set(newValue) {
console.log('你试图改变'+key+'属性', newValue)
val = newValue
// 当设置了新值,也要observe,防止新值也是个对象
childOb=observe(newValue)
}
});
}
utils.js
export const def = function(obj,key,value,enumerable){
Object.defineProperty(obj,key,{
value,
enumerable,//可枚举
writable:true,
configurable:true
});
}
这个递归并不是函数自己调用自己的那种递归,而是几个函数循环调用,当最后一次传入的不再是对象时,return空,此时,结束递归。
流程是这样的:

observe(obj),进入observe函数,我们知道,obj对象是没有__ob__属性的,所以会new一个Observer对象,并把这个对象设置为__ob__属性,给obj添加上。(def(value,'__ob__',this,false);,这里的def方法就是给对象添加__ob__属性,它是在utils.js中定义的)。紧接着,让obj调用walk方法,遍历obj的所有属性,调用defineReactive方法,而在defineReactive中,让val = data[key],然后val调用observe,这个val就是obj的属性,于是,完成了递归调用。我们可以理解为,从observe出发,又回到了observe。一直循环到m的属性n调用observe时,由于n不是一个对象,所以observe返回空,递归结束。回到了defineReactive上,接着执行后边的Object.defineProperty,将属性变为响应式的。
数组的响应式处理
当数据是一个数组时,应该怎么对它进行处理才能成为响应式的呢?
数组也是对象,所以如果obj还有一个属性c,c:[1,2,3,4,5],使用上边的代码,c也会被添加上__ob__属性,但是如果要改变c数组,并不会触发子元素的get或者set方法。
首先是,数组的七个方法被改写了:
- push
- pop
- shift
- unshift
- splice
- sort
- reverse
数组的响应式原理就是,以Array.prototype为原型重新定义一个对象,并且将改写的7个方法加到这个对象上,然后强制改变数组的原型链,将数组的原型改为我们新定义的这个对象。这样,当数组再调用七个方法的时候,调用的就是我们修改过的方法了。
import { def } from './utils'
// 得到Array.prototype
const arrayPrototype = Array.prototype;
// 我们要以arrayPrototype为原型创建arrayMethods对象 暴露出去
export const arrayMethods = Object.create(arrayPrototype);
// 要被改写的7个方法
const methodsNeedChange = ['push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsNeedChange.forEach(methodName => {
// 备份原来的方法--需要保留原来的功能
const original = arrayPrototype[methodName];
// 定义新的方法 methodName,把它加到arrayMethods对象上,7个方法重新在arrayMethods上定义
def(arrayMethods, methodName, function () {
// 把数组的__ob__取出来
const ob = this.__ob__;
// push unshift splice方法能够插入新值,现在要把插入的新值也要变成observe的
let inserted = [];
switch (methodName) {
case 'push':
case 'unshift':
inserted=arguments;
break;
case 'splice'://splice(下标,数量,插入的新项) splice() 方法可删除从 index 处开始的零个或多个元素,并且用参数列表中声明的一个或多个值来替换那些被删除的元素
// inserted=Array.prototype.slice.apply(arguments).slice(2);//选择 插入的新项 类数组转数组
inserted=[...arguments].slice(2)
break;
}
// 判断有没有要插入的新项,让新项也变为响应式的
if(inserted){
ob.observeArray(inserted)//ob是一个Observer
}
const result=original.apply(this, arguments)//这里如果直接调用origin,那是window在调用,所以需要apply或者call
console.log(123)
return result
}, false)
})
在Observer.js中,我们需要加一个判断,判断传进来的是不是数组,如果是的话,要修改它的原型。
import utils, { def } from './utils'
import defineReactive from './defineReactive'
import arrayPrototype from './array'
import {arrayMethods} from './array'
import observe from './observe';
export default class Observer{
constructor(value){
console.log('Observer构造器');
// 给实例(this)添加__ob__属性, 构造函数的this不是类本身,而是实例
def(value,'__ob__',this,false);// 这样的话,value对象就有了__ob__属性,第一次时obj调用,于是obj有了__ob__属性
// Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式的object(可以被侦测的)
// 检查是数组还是对象
if(Array.isArray(value)){
// 如果是数组,将原型指向arrayMethods
Object.setPrototypeOf(value,arrayMethods)
}
this.walk(value)
}
// 遍历
walk(value){
for(let k in value){//obj有了__ob__,调用walk,a和b就
defineReactive(value,k) // 把value的k属性变成响应式的
}
}
// 数组的特殊遍历
observeArray(arr){
for(let i=0,l=arr.length;i<l;i++){
// 逐项observe
observe(arr[i])
}
}
}
七个方法中的push、unshift、splice由于可以向数组中插入新的元素,所以需要单独判断,我们要保证新插入的值也得是响应式的。
668

被折叠的 条评论
为什么被折叠?



