原文 (opens new window)

# 前言

vue2的响应式是通过Object.defineProperty方法,劫持对象的getter和setter,在getter中收集依赖,在setter中触发依赖,但是这种方式有一些缺点

  1. 由于是遍历递归监听属性,当属性过多或嵌套层级过深时会影响性能
  2. 无法监听对象新增的属性和删除属性,只能监听对象本身存在属性,所以设计了$set 和 $delete
  3. 如果监听数组的话,无法监听数组的增减,只能监听通过下标可以访问到的数组中已有的属性,由于使用Object.defineProperty 遍历监听数组原有元素过于消耗,vue放弃使用 Object.defineProperty 监听数组,而采用重写数组原型的方式来监听对数组数据的操作,并用$set 和 splice方法来更新数组,$set和splice会调用重写后的数组方法

# Vue3响应式的实现

# Proxy对象

针对Object.defineProperty的弊病,在ES6中引入了一个新的对象-Proxy(代理)

proxy对象

用于创建一个对象的代理,主要用于改变对象的某些默认行为,Proxy可以理解成,在目标对象之前假设一层拦截,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外面的访问进行过滤和改写。基本语法如下

/**
 * target: 目标对象
 * handler: 配置对象,用来定义拦截的行为
 * proxy:Proxy构造器实例
 */
var proxy = new Proxy(target, handler)

拦截get,取值操作

var proxy = new Proxy({}, {
    get: function(target, propKey) {
        return 35
    }
})
proxy.time // 35
proxy.name // 35
proxy.title // 35

可以拦截的操作有

函数 操作
get 读取一个值
set 设置一个值
has in操作符
deleteProperty delete
getPrototypeOf Object.getPrototypeOf()
setPrototypeOf Object.setPrototypeOf()

# Vue3的reactive 和 ref

Vue3的reactive和ref正式借助了Proxy来实现

# reactive

作用:创建原始对象的响应时副本,即将「引用类型」数据转换为「响应式」数据

参数: reactive数据必须是对象或数组

reactive函数实现

// 判断是否是对象
const isObject = val => val !== null && typeof val === 'object';
// 判断key是否存在
const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key);

export function reactive(target) {
    // 首先判断是否为对象
    if(!isObject(target)) return target;

    const handler = {
        get(target, key, receiver) {
            console.log(`获取对象属性${key}`);
            // 收集依赖
            const result = Reflect.get(target, key, receiver);
            // 深度监听(惰性)
            if(isObject(result)) {
                return reactive(result);
            }
            return result;
        },
        set(target, key, value, receiver) {
            console.log(`设置对象属性${key}值为${value}`);
            // 首先获取旧值
            const oldValue = Reflect.get(target, key, reactive);

            let result = Reflect.set(target, key, value, receiver);

            if(result && oldValue !== value) {
                // 更新操作...
            }
            return result;
        },
        deleteProperty(target, key) {
            console.log(`删除对象属性${key}`);
            // 先判断是否有key
            const hasKey = hasOwn(target, key);
            const result = Reflect.deleteProperty(target, key);

            if(hadKey && result) {
                // 更新操作
            }
            return result;
        }
        // 其他方法
        // ....
    }
    return new Proxy(target, handler);
}

const obj = {a: {b: {c: 6}}};
const proxy = reactive(obj);

proxy.a.b.c = 10;
// 获取对象属性a值
// 获取对象属性b值
// 设置对象属性c值 77

至此,引用类型的对象我们已经把它转换为响应式对象了,Proxy对象只能代理引用类型的对象,对于基本数据类型如何实现响应式呢。

Vue的解决方法是把基本数据类型变成一个对象:这个对象只有一个value属性,value属性的值就等于这个基本数据类型的值。然后就可以用reative方法将这个对象编程响应式的proxy对象

// 实际上就是
ref{0} --> reactive({value: 0})

但是这是 Vue3 最初的设计,现在不太一样了,我们接着来看。

# ref

# 总结

  1. reactive将引用类型值变成响应式,使用proxy实现
  2. ref可将基本类型和引用类型都变成响应式,通过监听类的valeu属性的get和set实现,但是当传入的值为引用类型时实际上内部还是使用reactive方法进行处理
  3. ref经修改实现方式后性能更高,推荐使用ref一把梭