vue2与vue3的区别之响应式原理

314 阅读6分钟

什么是响应式?

通俗来讲,响应式原理就是指当数据一旦变化,就会立刻触发视图的更新。它是实现数据驱动视图的第一步。

vue2是如何实现响应式的

 (一)Object.defineProperty(数据劫持)

这个方法就是在一个对象上定义一个新的属性,或者改变一个对象现有的属性,并且返回这个对象。里面有两个字段 set(用来取属性的值), get(获取属性的值)。defineProperty 函数并不是直接对所需要被操作的对象进行值的获取与修改操作,而是重新 copy 出一个完全一样的对象,再对 copy 出来的对象进行操作从而达到操作原对象的目的(也就是说,原对象中的值自始至终都没有变,我们修改及打印出来的都是 copy 对象)。举个例子:

var obj = {  // 定义出来的 obj 对象
    b: 20
};    
var obj2 = {  // copy 出来和 obj 一模一样的对象
    b: 20
};

Object.defineProperty(obj, "b", {  // 第一个参数为要定义属性的对象,第二个参数为要定义或修改的属性,第三个参数是一个提供了 get 方法和 set 方法的对象
    get: function () {
        console.log('正在获取b')
        return obj2.b;  // 返回出 copy 对象的值
    },
    set: function (newValue) {  //接收一个参数 newvalue
        console.log('正在设置b')
         obj2.b = newValue;  // 对 copy 出来的对象进行修改
    }
});

obj.b = 30;  // 想要修改 obj 对象中 b 的数据值,set 函数被调用
console.log(obj.b)  // get 函数被调用

最终的打印结果为:

正在设置b
正在获取b
30

由此我们可以得知,obj对象中 b 的值,并不是直接被打印出来的。我们可以理解为对象 obj 被Object.defineProperty()函数劫持了,当编译到代码 obj.b = 30 时,就会调用 set() 函数,对 b 的值进行修改(其实本质上是对 copy 对象的属性进行修改),当获取 b 的值时,调用 get() 函数

(二)vue2 的响应式原理

我们直接通过一段代码来进行解释:

function type(data){
    return Object.prototype.toString.call(data).slice(8,-1)
}

let oldArrayPrototype = Array.prototype   // 先把数组原型上的方法保留下来做备份
let proto = Object.create(oldArrayPrototype) // 创建实例对象继承

// 改写数组上的方法
Array.from(['push', 'shift', 'unshift', 'pop']).forEach(method => {
    proto[method] = function () {
        oldArrayPrototype[method].call(this, ...arguments)
        updateView()  // 手动视图更新
    }
})


// 观察者函数
function observer(target){     // 专门用于劫持数据的
    if(type(typeof target !== 'object' || typeof target == null)){    // 判断劫持的对象类型,若不是对象,则不劫持,直接返回
        return target
    }

    if (Array.isArray(target)) {   // 判断劫持的对象类型,若是数组,则改变其 prototype 对象
        // target.__proto__ = proto
        Object.setPrototypeOf(target, proto)   // 将target对象的prototype对象设置为proto,让调用的数组相关函数都是我们重写的函数
    }
    
    for(let key in target){    // 一定是对象
        defineReactive(target,key,target[key])
    }
}

// 响应式
function defineReactive(target,key,value){
    observer(value)
    Object.defineProperty(target,key,{    // 只能劫持对象
        get(){
            return value
        },
        set(newValue){
            if(newValue !== value){
                value = newValue
                updateView()
            }
        }
    })
}

function updateView(){
    console.log('更新视图');
}

// Object.defineProperty () 可以重新定义属性 给属性安插 getter setter 方法
let data = {
    name:'小李',
    grades:{
        math:90,
        chinese:88
    },
    hobbies:['dance','sing']
}

observer(data)



data.name = '小李子'    // 改变对象中为基本数据类型的属性时,视图更新
data.grades.math = 100    // 改变被劫持对象中的对象属性的值,进入递归,视图更新
data.grades={   // 改变对象的属性值,视图更新
    math:70,
    chinese:88
}
data.age = 18   // 视图不更新,因为 age 是新增属性,在 observer 函数中,遍历不到 age 这个key值,所以不会进入 defineReactive 函数,从而也就无法触发视图更新
data.hobbies.push('draw')   // 视图更新,hobbies是个数组,被调用的 push 方法是被重写的方法

Vue2的缺点:

1.vue2 对象中不存在的属性不能被拦截,当给对象添加不存在的属性时不是响应式的。

2.数组改变 length 属性是无效的,不会触发视图更新。

3.vue2 中递归是自动执行的,浪费性能。

Vue3是如何实现响应式的

(一)Proxy

该对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。语法:

const p = new Proxy(target, handler)   
// target 是要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
// handler 是一个通常以函数作为属性的对象,它包含有 Proxy 的各个捕获器(trap),用来定制拦截行为。

(二)vue3 的响应式原理

let toProxy = new WeakMap()  // 弱引用映射表  存放 {原对象:代理过的对象}
let toRaw = new WeakMap()  // {代理过的对象:原对象}

// 判断对象为对象引用类型,只要不是原始类型就返回true
function isObject(val) {
    return typeof val === 'object' && val !== null
}


function hasOwn(target,key){
    return target.hasOwnProperty(key)
}


// 响应式的核心方法
function reative(target) {
    return createReactiveObject(target)
}


// 创建响应式对象   
function createReactiveObject(target) {
    if (!isObject(target)) {   // 是原始类型,直接返回
        return target
    }

    let proxy = toProxy.get(target)  // 检查弱引用对象中是否存在值为 target 的 key 
    if(proxy){  // 已经代理过了   防止多次代理
        return proxy
    }
    if(toRaw.has(target)){  // 防止重复代理
        return target
    }

    let baseHandler = {      // 定制拦截行为
        get(target, key, receiver) {    // receiver 是代理的对象,也就是下文的 observed
            console.log('获取');
            // Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API
            let result = Reflect.get(target, key, receiver)  // Reflect做反射,用 result 来读取值,尽可能不操作 target   优点:Reflect 如果出错,会返回 false,不会影响后续代码的执行
            return isObject ? reative(result) : result    // 递归,只有用到了数据源中深层次的对象时才递归,判断 result 是否是个对象,从而实现深层次的代理
        },
        set(target, key, value, receiver) {
            // 判断是新增属性还是修改属性
            let hadKey = hasOwn(target,key)
            let oldValue = target[key]
            let res = Reflect.set(target, key, value, receiver)

            if(!hadKey){  // 新增属性
                console.log('新增属性');
            }
            else if( oldValue !== value ){   // 修改属性
                console.log('修改属性');
            }
            return res
        },
        deleteProperty(target, key) {
            console.log('删除');
            let res = Reflect.deleteProperty(target, key)
            return res
        }
    }

    let observed = new Proxy(target, baseHandler)    // observed 是代理的对象
    toProxy.set(target,observed)  // 往 toProxy 中存值
    toRaw.set(observed,target)
    return observed
}


// 数组
let arr = [1,2,3]
let proxy1 = reative(arr)
proxy1.push(4)   // 新增属性
proxy1.length = 5    // 修改属性,数组的 length 属性被修改


let proxy2 = reative({ name: '小李', grades: { math: 98 } })   // 多层代理  利用 get 读取时才代理
proxy.name = '小吴'   // 改变对象中为基本数据类型的属性时,修改属性,
delete proxy.name   // 删除基本数据类型的属性时,删除属性

proxy.age.n = 19   // 改变对象属性的属性值时,修改属性



// 对同一个对象进行重复代理————解决:被代理的对象需要记录一下,防止再次代理
// reative(proxy)
// reative(proxy)
// reative(proxy)

// 对已经被代理过的对象进行多层代理
// let proxy2 = reative(proxy)
// let proxy3 = reative(proxy2)

Vue2 和 Vue3 的响应式区别

1.在 Vue2 中对数组的操作需要通过重写方法来实现,而 Vue3 基于 Proxy 和 Reflect ,可以原生监听数组,可以监听对象属性的添加和删除。

2.vue2 中递归是自动执行的,vue3 只有用到了数据源中深层次的对象时才递归,性能相对较好。