Vue数据的响应式原理

目录

对象的响应式处理

Object.defineProperty() 方法介绍

属性的getter函数与setter函数

Object.defineProperty()方法

数据代理

defineReative()方法的定义

Observer类的创建

对象响应式的封装文件

数组的响应式处理

数组响应式的原理图

数组响应式原理代码

目录结构

observe.js文件 

Observer.js文件

def.js文件

defineReative.js文件

array.js文件


对象的响应式处理

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)

以上是我自己的一点小结,若果有错误的地方,请指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue数据响应式原理是通过结合数据劫持发布-订阅者模式来实现的。Vue使用Object.defineProperty来劫持data中各个属性的setter和getter方法,并在数据变动时发布消息给订阅者,从而触发相应的监听回调。 具体来说,Vue会创建一个observe对象来对数据进行劫持,将数据对象的每个属性都转换为getter和setter。当数据发生变动时,setter会被调用,然后发布消息给订阅者,通知它们数据已经发生改变了。订阅者会触发相应的监听回调进行相应的更新操作。 在Vue中,每个组件实例都会对应一个watcher实例。当组件渲染过程中使用了数据属性时,watcher会将这些属性收集为依赖。当依赖项的setter被触发时,watcher会被通知,从而使其关联的组件重新渲染。这样就实现了数据响应式更新。 总结起来,Vue数据响应式原理是通过数据劫持和发布-订阅者模式的结合来实现的,利用Object.defineProperty来劫持数据属性的setter和getter方法,然后通过发布-订阅者模式来实现数据变动时的通知和更新操作。这种机制使得Vue能够实现高效的数据更新和视图渲染。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [vue响应式原理](https://blog.csdn.net/dongqian911/article/details/116242827)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [vue响应式原理](https://blog.csdn.net/weixin_48181168/article/details/120158346)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值