目录
对象的响应式处理
Object.defineProperty()
方法介绍
官方说明:Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
var obj={}
Object.defineProperty(obj,'a',{
value:1
})
Object.defineProperty(obj,'b',{
value:2
})
console.log(obj.a);
console.log(obj.b);
为什么会使用Object.defineProperty()
方法来添加属性,而不是直接添加属性呢?
由于Object.defineProperty()
方法可以给新增的属性添加许多隐藏的性质,例如:
var obj={}
Object.defineProperty(obj,'a',{
value:1,
// 属性是否可写
writable:true,
// 判断属性是否可枚举
enumerable:true
})
Object.defineProperty(obj,'b',{
value:2,
// 属性是否可写
writable:false,
// 判断属性是否可枚举
enumerable:false
})
obj.a++
obj.b++
console.log(obj.a); //2,可写
console.log(obj.b); //2,不可写
for(var k in obj){
console.log(k);
} // 2 2 a(b属性不可枚举
属性的getter函数与setter函数
属性的 getter 函数,如果没有 getter,则为 undefined
。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this
对象(由于继承关系,这里的this
并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined。
属性的 setter 函数,如果没有 setter,则为 undefined
。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this
对象。 默认为 undefined。
var obj={}
Object.defineProperty(obj,'a',{
// value:1,get函数不能同时跟value使用
get(){
console.log('get函数被调用!');
},
Set(){
console.log('set方法被调用!');
}
})
Object.defineProperty(obj,'b',{
value:2,
})
obj.a++
console.log(obj.a); //get函数被调用!get函数被调用!undefined
console.log(obj.b); //2
当访问对象属性的时候,会调用get()方法,同理,修改属性的时候,会调用set方法,为什么get方法不能跟value并存,由于当我们访问属性的时候(未定义get方法),会调用属性默认的get方法,来获取数据,如果我们自定义get方法,相当于方法的重写,会根据我们自定义的get方法来获取属性,因此,两者不能共存。
Object.defineProperty()
方法
如果单纯的使用defineProperty()而不做任何处理的话是有弊端的,如下:
var obj={}
Object.defineProperty(obj,'a',{
// value:1,get函数不能同时跟value使用
get(){
return 7;
},
set(newValue){
console.log('set被调用'+newValue);
}
})
Object.defineProperty(obj,'b',{
value:2,
})
console.log(obj.a); //7
obj.a++ //set被调用8
console.log(obj.a); //7
get方法与set方法其实是有弊端的,get()方法中返回的值会作为属性的值,因此输出属性a的值是7,但通过++去改变属性a的值时,虽然调用了set()方法,但set方法并没有对属性的值做出有用的改变,因此再次输出a属性的值时,结果还是7.
数据代理
针对上述问题,就需要用数据代理来解决
var obj={}
var temp
Object.defineProperty(obj,'a',{
// value:1,get函数不能同时跟value使用
get(){
console.log('get被调用');
return temp;
},
set(newValue){
console.log('set被调用'+newValue);
temp=newValue
}
})
Object.defineProperty(obj,'b',{
value:2,
})
console.log(obj.a); //get被调用 undefined
obj.a=1 //set被调用1
obj.a+=1 //get被调用 set被调用2
console.log(obj.a); //get被调用 2
采用一个临时变量temp,作为数据的返回值,而数据更改,也是更改temp的值,这是defineReative()
方法的前身。
defineReative()
方法的定义
其实就是对上面步骤的一个封装
var obj={}
function defineReactive(data,key,val){
Object.defineProperty(data,key,{
get(){
console.log('get被调用');
return val;
},
set(newValue){
console.log('set被调用'+newValue);
val=newValue
}
})
}
defineReactive(obj,'a',1)
console.log(obj.a); //get被调用 1
obj.a+=1 //get被调用 set被调用2
console.log(obj.a); //get被调用 2
由于我们不想要一个全局的temp作为临时变量,这时候就采用一个函数来进行封装,data代表处理的对象,key代表处理的属性,val代表修改属性的值,get()方法返回的是val,而set()方法修改后的值也将赋值给val,这样就将上面的步骤封装起来了。
Observer类的创建
上面的步骤只能实现对象单个属性的响应式,若想实现嵌套属性的响应式,就需要实现递归,下面是大概的原理图:
首先通过observe()方法判断obj是否是对象,如果是对象,则创建new Observer()实例(这里先不创建__ob__)属性,再通过遍历对对象的属性进行defineReactive处理(其实就是将对象的每一层的每个属性进行响应式处理)
代码如下:
// 入口文件,主要是为了判断属性的值是不是对象,
// 如果不是,直接返回,如果是,构造Observer实例
function observe(value){
if(typeof value!=='object'){
return;
}
new Observer(value)
}
// 遍历对象的每个属性,把他们都变为响应式数据(可以通过 Object.defineProperty的get和set方法访问到)
// 构造函数的this是实例对象
class Observer{
constructor(value){
this.value=value
this.work()
}
work(){
Object.keys(this.value).forEach(key=>defineReactive(this.value,key))
}
}
function defineReactive(value,key,val){
if(arguments.length==2){
val=value[key]
}
observe(val)
Object.defineProperty(value,key,{
get(){
console.log("属性"+key+"被监听");
return val
},
set(newValue){
console.log("属性"+key+"被监听");
val=newValue
observe(newValue)
}
})
}
var obj={
a:{
b:1
},
c:10
}
observe(obj)
console.log(obj.a.b);
对象响应式的封装文件
对上面的全部步骤进行封装,结构目录:
defineReative.js
import observe from "./observe"
// 创建与修改属性值
export default function defineReative(value,key,val=value[key]) {
// 对属性的属性进行响应式
observe(val)
// 若没有属性,就创建属性,有属性就修改该属性
Object.defineProperty(value,key,{
get(){
console.log(key+"属性正在被读取!");
return val;
},
set(newValue){
console.log(key+"属性正在被改写!"+newValue);
val=newValue
// 更改后的值也要进行响应式
observe(newValue)
}
})
}
observe.js
import Observer from './Observer'
// 主要是用来判断是对象还是基本类型,如果是对象,添加响应式
export default function observe(value){
if(typeof value!='object'){
return
}else{
new Observer(value)
}
}
Observer.js
import defineReative from './defineReative'
// 在这个类中添加响应式
export default class Observer{
constructor(value){
this.value=value
this.wolk()
}
// 遍历添加响应式
wolk(){
Object.keys(this.value).forEach(key=>{
defineReative(this.value,key)
})
}
}
index.js(测试文件)
import observe from "./observe"
// 实现简单对象的监听
var obj={
a:{
d:2
},
b:1
}
observe(obj)
obj.a.d=5
obj.a.d++
console.log(obj.a.d);
数组的响应式处理
数组响应式的原理图
当使用数组的pop,push等方法时,并不会触发get,set方法,因此,数组是不能实现响应式的,这时候就需要对数组的push,pop等方法进行重写,添加响应式,那如何添加响应式呢?使用拦截器覆盖Array.prototype
上的方法,在执行原型上的方法之外做数据的响应式。
1、首先应该以Array.prototype
为原型创建arrayMethods
对象,这时arrayMethods
对象就拥有Array.prototype
原型上的所有方法
2、将方法pop,push,unshift
,shift,splice,sort,reverse进行响应式处理
3、将这些重写的方法覆盖掉arrayMethods
对象上原有的方法
4、将数组的proto
属性指向arrayMethods
对象,默认的proto
属性会指向Array.prototype
5、由于数组中的属性还可能是数组或对象,如果想要完全的实现响应式处理,需要遍历数组,就需要将数组中的数组进行响应式处理,数组中的对象进行wolk()
方法处理
数组响应式原理代码
目录结构
observe.js
文件
import Observer from './Observer'
// 主要是用来判断是对象还是基本类型,如果是对象,添加响应式
export default function observe(value){
if(typeof value!='object'){
return
}else{
// value.__ob__相当于一个标识,如果已经创建了Observer就不用再创建
// 若没有创建就直接创建实例
let ob
if(value.__ob__!=undefined){
ob=value.__ob__
}else{
ob=new Observer(value)
}
}
}
在observe.js
文件中,相比只有对象的observe.js
文件,添加了一个value.__ob__
属性,其实这个属性主要起到一个标识的作用,用来记录该对象是否已经响应式,避免重复给对象添加响应式。这个判断其实在单纯判断对象响应式的时候就可以添加,但我为了简单化,就没有,现在添加,是因为后面数组添加响应式的时候会用到。
Observer.js
文件
import defineReative from './defineReative'
import def from './def'
import {arrayPrototype} from './array'
import observe from './observe'
// 在这个类中添加响应式
export default class Observer{
constructor(value){
def(value,'__ob__',this,false)
this.value=value
if(Array.isArray(value)){
Object.setPrototypeOf(value,arrayPrototype)
this.observeArray(value)
}else{
this.wolk()
}
}
// 遍历添加响应式
wolk(){
Object.keys(this.value).forEach(key=>{
defineReative(this.value,key)
})
}
// 数组的遍历
observeArray(){
for(let i=0,len=this.value.length;i<len;i++){
observe(this.value[i])
}
}
}
在Observer.js
文件中,在构造函数中给传入的value
值添加_ob__属性,它的属性值就是Observer实例,与observe.js
文件中的判断相对应,随后还要对value进行判断,如果是对象,就进行对象的遍历,如果数组,就进行数组的遍历
def.js
文件
// 新建一个属性
export default function def(obj,key,value,configurable){
Object.defineProperty(obj,key,{
value,
Configurable:configurable,
Writable:true
})
}
def.js
文件主要是对属性添加功能的封装,主要用于添加__ob__
属性,__ob__
属性的作用前面已经说过了,这里不再重复
defineReative.js
文件
import observe from "./observe"
// 创建与修改属性值
export default function defineReative(value,key,val=value[key]) {
// 对属性的属性进行响应式
observe(val)
// 若没有属性,就创建属性,有属性就修改该属性
Object.defineProperty(value,key,{
get(){
console.log(key+"属性正在被读取!");
return val;
},
set(newValue){
console.log(key+"属性正在被改写!"+newValue);
val=newValue
// 更改后的值也要进行响应式
observe(newValue)
}
})
}
defineReative.js
文件是对对象遍历添加响应式的封装,给对象的每个属性都添加响应式
array.js
文件
import def from './def'
// 修改数组指向的原型对象
export const arrayPrototype=Object.create(Array.prototype)
const ArrayMethods=[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
ArrayMethods.forEach(method=>{
const original=Array.prototype[method]
def(arrayPrototype,method,function(){
const result = original.apply(this,arguments)
// 有三个方法比较特殊,push,unshift,splice会添加新元素,因此也要将元素进行响应式
const arg=[...arguments]
let inserted=[]
const ob =this.__ob__
switch(method){
case 'push':
case 'unshift':
inserted=arg
break;
case 'splice':
inserted=arg.slice(2)
break
}
if(inserted){
ob.observeArray(inserted)
}
return result
},false)
})
在Array.prototype
为原型创建arrayMethods
对象,这时候arrayMethods
对象就拥有Array.prototype
的所有方法,将方法pop,push,unshift
,shift,splice,sort,reverse进行响应式处理,最后覆盖掉arrayMethods
对象上原有的pop,push,unshift
,shift,splice,sort,reverse方法,最后返回。有三个方法比较特殊,push,unshift
,splice方法,,会添加新的元素,因此新的元素也要响应式,通过argument获取参数, inserted数组获取全部的新元素,再通过observeArray
方法遍历数组(splice方法有三个参数,新添加的元素是从第三个参数开始,因此是arg.slice(2)
)
以上是我自己的一点小结,若果有错误的地方,请指正。