vue---剖析vue响应式原理

1. 前言

在vue中,只要数据变化,页面就会重新渲染,这个是怎么做到的呢?
在创建vue实例时,vue会将data中的成员代理给vue实例,目的就是实现响应式,监控数据变化,然后执行某个事件函数。在vue2.0中使用的是Object.defineProperty来实现数据的劫持,配合发布-订阅者模式来实现。

2. Object.defineProperty

首先我们来看一下怎么使用Object.defineProperty,其实使用方法很简单。这个函数接收三个参数:
1.需要监控的对象
2.需要监控的对象的某个属性
3.一个配置对象
前面两个参数很好理解,一目了然,第三个配置对象有很多属性,在这里主要介绍他的两个函数:set、get。
当我们给一个对象添加属性的时候就会调用set函数,当我们读取某个对象的属性的时候就会调用get方法,请看下面这个小例子:

const data = {
    name:'小曹',
    age:18
}
Object.defineProperty(data, "name", {
    get(){
        console.log('读取属性');
        return 'xxx';
    },
    set(value){
        console.log('设置属性:', value)
    }
})
console.log(data.name);//输出结果:读取属性  xxx
data.name = "靓仔";//输出结果:设置属性:靓仔

上面例子的第一个console是获取data的值,触发了get方法。而且诡异的是输出结果还并不是我们想象中的“小曹”,而是‘xxx’。这是因为,Object.defineProperty的get方法返回值就是监控的对象的值,所以我们会打印出‘xxx’。
第二个console是我们重新赋值,这时候触发了set方法,并且det方法接收一个参数,这个参数就是要设置的值,所以我们可以在set方法中对赋值进行控制。
使用这种方式只能监控一个属性,显然这并不符合我们的需求,所以可以使用循环遍历来实现

3. 监控对象属性

实现同时监控多个对象属性,我们可以使用for-in遍历,封装成一个函数:

function defineReactive(data, key, value){
    Object.defineProperty(data, key, {
        get(){
            console.log('读取属性');
            return value;
        },
        set(val){
            console.log('设置属性:', value)
            value = val;
        }
    })
}
function observer(data){
    for(let key in data){
        defineReactive(data, key, data[key]);
    }
}
observer(data);
console.log(data.age);//18
console.log(data.name);//小曹

这样就可实现监控对象的读和写的操作。既然我们已经可以监控到对象的写这个操作,那么在vue中,每次重新赋值就会再次渲染页面,达到响应式。那么我们就可以在set函数里执行渲染函数,我们给这个渲染函数起名为render,render函数内部具体实现在这里我就不多说了,在这里就用一句话代替。渲染函数具体实现可以看这个篇文章https://blog.csdn.net/qq_44197554/article/details/105904564
在我们设置属性值的时候,如果两个值相等,那么我们还有重新渲染的必要吗?是不是就不需要渲染了,这个可以节省性能,所以我们在set函数中加个判断

set(val){
   console.log('设置属性:', value)
   //如果两值相等,则直接结束
   if(value === val){
       return;
   }
   value = val;
   render();
}

4. 递归遍历

现在又有一个问题,如果我们的data对象中的某个属性值是一个对象,这样还能监控到吗,测试过后发现,是不能的,所以我们就需要用到递归了,请看下面代码:

const data = {
    name:'小曹',
    age:18,
    obj:{
        a:1
    }
}
function defineReactive(data, key, value){
	//进行递归遍历
    observer(value);
    Object.defineProperty(data, key, {
        get(){
            console.log('读取属性');
            return value;
        },
        set(val){
            console.log('设置属性:', value)
            if(value === val){
                return;
            }
            value = val;
            render();
        }
    })
}
function observer(data){
	//判断传入的是否为一个对象
    if(typeof data === 'object'){
        for(let key in data){
            defineReactive(data, key, data[key]);
        }
    }
}

经过上面的递归遍历,不管data里嵌套多少层对象,都会被监控到

5. 数组

vue响应式中 Object.defineProperty没有观察数组,原因是太消耗新能。官方说性能与用户体验不成正比
所以我们就需要在observer函数中判断传入的data是否为一个数组

function observer(data){
	//判断是否为数组
    if(Array.isArray(data)){
        return;
    }
    if(typeof data === 'object'){
        for(let key in data){
            defineReactive(data, key, data[key]);
        }
    }
}

那么在vue中操作数组是怎么实现的呢?是因为在vue中重写了数组的方法,所以当我们在vue中使用数组的方法时,就执行了render函数

//保存数组原型
const arrayProto = Array.prototype;
//克隆一个原型对象
const arrayMethods = Object.create(arrayProto);
//重写所有的函数
['push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse'].forEach(method => {
    arrayMethods[method] = function (){
        //改变重写函数的this指向
        //展开传入的值
        arrayProto[method].call(this, ...arguments);
        render();
    }
})

大家是不是有疑问,为什么要重新克隆一个数组的原型呢?
是因为只需要在使用数组变异方法的时候执行这些重写的方法,对于其他的数组不污染其原型。所以更改的是克隆出来的原型,而不是本来的原型。此时数组执行的还是本来原型上的方法,所以需要在observer函数中修改数组的原型指向

function observer(data){
    if(Array.isArray(data)){
        //改变数组原型指向
        data.__proto__ = arrayMethods;
        return;
    }
    if(typeof data === 'object'){
        for(let key in data){
            defineReactive(data, key, data[key]);
        }
    }
}
data.arr.push(100);
console.log(data.arr);//[1, 2, 3, 100]
(1)实现$set

$set方法的返回值就是要修改的值,该方法有三个参数:
1.data:要修改的对象或数组
2.key:要修改哪个属性
3.value:修改成什么

//实现$set方法
function $set(data, key, value){
    //修改数组
    if(Array.isArray(data)){
        data.splice(key, 1, value);
        return value;
    }
    //修改对象
    defineReactive(data, key, value);
    render();
    return value;
}
(2)实现$delete

该函数接收两个参数:
1.data:要删除哪个对象的属性
2.key:删除哪个属性

//实现$delete
function $delete(data, key){
    if(Array.isArray(data)){
        data.splice(key, 1);
        return;
    }
    delete data[key];
    render();
}

6.劣势

因此使用Object.defineProperty实现响应式有几个劣势
1.天生就需要进行递归
2.监听不到数组不存在的索引的改变
3.监听不到数组长度的改变
4.监听不到对象的增删
在vue3.0中解决了递归观察的问题,使用proxy代理

7.所有源码

const data = {
    name:'小曹',
    age:18,
    obj:{
        a:1
    },
    arr:[1, 2, 3]
}

//保存数组原型
const arrayProto = Array.prototype;
//重新创建一个原型对象
const arrayMethods = Object.create(arrayProto);
//重写所有的函数
['push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse'].forEach(method => {
    arrayMethods[method] = function (){
        //改变重写函数的this指向
        //展开传入的值
        arrayProto[method].call(this, ...arguments);
        render();
    }
})

function defineReactive(data, key, value){
    observer(value);
    Object.defineProperty(data, key, {
        get(){
            console.log('读取属性');
            return value;
        },
        set(val){
            console.log('设置属性:', value)
            if(value === val){
                return;
            }
            value = val;
            render();
        }
    })
}
function observer(data){
    if(Array.isArray(data)){
        //改变数组原型指向
        data.__proto__ = arrayMethods;
        return;
    }
    if(typeof data === 'object'){
        for(let key in data){
            defineReactive(data, key, data[key]);
        }
    }
}

function render(){
    console.log('页面渲染了');
}

//实现$set方法
function $set(data, key, value){
    //修改数组
    if(Array.isArray(data)){
        data.splice(key, 1, value);
        return value;
    }
    //修改对象
    defineReactive(data, key, value);
    render();
    return value;
}

//实现$delete
function $delete(data, key){
    if(Array.isArray(data)){
        data.splice(key, 1);
        return;
    }
    delete data[key];
    render();
}

observer(data);
data.arr.push(100);
console.log(data.arr);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值