vue3源码学习--一步两步魔幻的reactive

前言

随着vue3的发布和完善,许多开发同学正在尝试去将vue2的项目迁移升级vue3,其中跟vue3针对vue2有了很大改进有关系,比如:

  • Vue 3.0 在编译方面优化:静态提升(hoistStatic)、Block Tree、patch flag;
  • vue3.0中Composition Api 比 Vue 2.x 使用的 Options Api(高内聚,低耦合);
  • vue3.0更模块化的代码结构;
  • 基于Proxy 实现的响应式系统;
  • ......

那么我们就基于vue3中的响应式原理系列的学习下其中的实现。

Object.defineProperty到底有什么问题?

1、Object.defineProperty在vue2.x中存在的问题

  • object.defineProperty只监听属性,如果要监听对象的所有属性需要通过object.keys等遍历实现

  • object.defineProperty索引访问或者修改数组中已经存在的元素,是可以出发get和set的,但是对于通过push、unshift增加的元素,会增加一个索引,这种情况需要手动初始化,新增加的元素才能被监听到

  • 监听对象的某个属性又基本数据类型变为对象时,需要递归对象的属性对象;

Proxy

1、Proxy基本使用

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

语法

const p = new Proxy(target, handler)

参数

target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler:  一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理的行为。
复制代码

返回值

proxy代理对象

简单例子:

//定义一个需要代理的对象
let applicant = {
    age: 22,
    name: '张三'
}
//定义handler对象
let hander = {
    get(obj, key) {
        // 对象里有这个属性,就返回属性值,如果没有,就返回默认值66
        return key in obj ? obj[key] : '无身份证'
    },
    set(obj, key, val) {
        obj[key] = val
        return true
    }
}
//把handler对象传入Proxy
let proxyObj = new Proxy(applicant, hander)

// 测试get能否拦截成功
console.log(proxyObj.age) //输出22
console.log(proxyObj.name) //输出张三
console.log(proxyObj.cerNo) //输出默认值无身份证

// 测试set能否拦截成功
proxyObj.age = 25
console.log(proxyObj.age) //输出25 修改成功
复制代码

可以看出,Proxy代理的是整个对象,而不是对象的某个特定属性,不需要我们通过遍历来逐个进行数据绑定。

值得注意的是:之前我们在使用Object.defineProperty()给对象添加一个属性之后,我们对对象属性的读写操作仍然在对象本身。
但是一旦使用Proxy,如果想要读写操作生效,我们就要对Proxy的实例对象proxyObj进行操作。

另外,MDN上明确指出set()方法应该返回一个布尔值,否则会报错TypeError

vue3中的reactive

1、reactive基础实现

基于上面proxy的基本语法及实例,我们来一步一步实现一个rective

/**
 * @description 判断是否是一个对象
 * @param {val} 判断的目标val
 * @return {Boolean} true | false
 */
function isObject(val) {
    return val != null && typeof val === 'object' && Array.isArray(val) === false;
}

/**
 * @description 实现一个reactive方法
 * @param {target} 响应式目标对象
 * @return {Proxy} 创建的代理对象实例
 */
function reactive(target) {
    //判断是否是一个对象,不是的话直接返回
    if (!isObject(target)) {
        return {}
    }
    const proxy = new Proxy(target, {
        get(target, key, receiver) {
            //去代理上取值,执行get
            console.log(`获取属性:${key}, 值为:${target[key]}`, )
            return target[key]
        },
        set(target, key, value, receiver) {
            //去代理上设置值,执行set
            console.log(`设置属性:${key}, 值为:${value}`, )
            return target[key] = value
        }
    })
    return proxy
}


// reactive验证demo
const applicant = {
    name: 'didi',
    age: 22,
}

const state = reactive(applicant)
console.log(state.name) //打印属性 didi
state.name = 'newDidi' //设置属性:name, 值为:newDidi
复制代码

上面我们实现了一个最基础的 reactive 响应式函数,目前已经能够满足基础的属性拦截和设置,但是是否存在什么问题呢?我们接着继续探索.....

2、reactive完善-- Reflect

我们把上面的使用demo稍微进一步的验证:

// 验证case2 --  this 指向问题
const applicant = {
    _name: 'didi',
    age: 22,
    get name() {
        return this._name;
    }
}
const applicantProxy = reactive(applicant)

let admin = {
    __proto__: applicantProxy,
    _name: "Admin"
};
// Expected: Admin
console.log(admin.name); // 输出:didi
复制代码

有没有发出疑问为什么是这样?为什么没有输出我们期望的内容呢?

Proxy 的一个难点就是this绑定。我们希望任何方法都绑定到这个 Proxy,而不是target对象,为什么要尽量把this放在代理对象receiver上,而不建议放原对象target上呢?因为原对象target有可能本来也是是另一个代理的代理对象,所以如果this一直放target上的话,出bug的概率会大大提高。针对什么的问题怎么解决呢?

答案就是: Reflect

**Reflect**的两个方法

  • get(target, key, receiver):个人理解就是,访问targetkey属性,但是this是指向receiver,所以实际是访问的值是receiver的key的值,但是这可不是直接访问receiver[key]属性,Proxy的get对应Reflect.get

  • set(target, key, value, receiver):个人理解就是,设置targetkey属性为value,但是this是指向receiver,所以实际是是设置receiver的key的值为value,但这可不是直接receiver[key] = value,``Proxy的get对应Reflect.set

3、reactive改造--对象重复代理问题

我们验证下如下的情景:

// 验证case3 --  重复代理同一个对象
const applicant = {
    name: 'didi',
    age: 22,
}

const applicantProxy1 = reactive(applicant)
const applicantProxy2 = reactive(applicant)
// 代理对象 applicantProxy1 和 代理对象 applicantProxy2 相等吗?
console.log(applicantProxy1 === applicantProxy2) // 输出 false
复制代码

在真实的使用场景中,这两个代理对象应该指向同一个引用,在创建第一个后如果意外重新调用了 reactive 进行二次代理我们应该是返回上一个引用的,针对这个应该如何解决呢?

如果你对es6使用比较多的话应该很容易想到了一个中处理方式: weakMap

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

如果想了解更多,请移步:developer.mozilla.org/zh-CN/docs/… 学习

现在在我们自己实现的  reactive 代码中使用  weakMap  来解决上面的问题吧

/**
 * @description 判断是否是一个对象
 * @param {val} 判断的目标val
 * @return {Boolean} true | false
 */
function isObject(val) {
    return val != null && typeof val === 'object' && Array.isArray(val) === false;
}

// 维护代理过的目标对象 map 映射
const reactiveWeakMap = new WeakMap()
/**
 * @description 实现一个reactive方法
 * @param {target} 响应式目标对象
 * @return {Proxy} 创建的代理对象实例
 */
function reactive(target) {
    //
    if (!isObject(target)) {
        return
    }

    // 创建对象前验证当前对象是否已经被代理过
    const exisitProxy = reactiveWeakMap.get(target)
    if (exisitProxy) {
        return exisitProxy
    }

    const proxy = new Proxy(target, {
        get(target, key, receiver) {
            //去代理上取值,执行get
            console.log(`获取属性:${key}, 值为:${target[key]}`, )
            return Reflect.get(target, key, receiver)
        },
        set(target, key, value, receiver) {
            //去代理上设置值,执行set
            console.log(`设置属性:${key}, 值为:${value}`, )
            //return target[key] = value
            return Reflect.set(target, key, value, receiver)
        }
    })
    // 首次代理的对象放入map中
    reactiveWeakMap.set(target, proxy)    return proxy
}
复制代码

重新验证我们的 case3,  符合预期,成功通过。

3、reactive改造--代理对象的二次代理

如果把一个已经代理的对象,再次调用 reactive  方法又会摩擦出怎么的火花呢?

// 验证case4--  代理对象的二次代理
const applicant = {
    name: 'didi',
    age: 22,
}

const applicantProxy = reactive(applicant)
const againProxy = reactive(applicantProxy)

// 第一次代理对象 applicantProxy   
// applicantProxy对象再次调用 reactive 的 againProxy 是怎么的关系呢
console.log(applicantProxy === againProxy) // 输出 false
复制代码

经过验证 返回了 false,  针对这种情况又应该如何处理呢?

如何标记一个对象已经被操作过呢?其实也很简单,我们只需要在被操作的对象上增加  flag 标识是不是就完美解决了我们的问题。

继续改造我们的 reactive 代码

/**
 * @description 判断是否是一个对象
 * @param {val} 判断的目标val
 * @return {Boolean} true | false
 */
function isObject(val) {
    return val != null && typeof val === 'object' && Array.isArray(val) === false;
}

// 维护代理过的目标对象 map 映射
const reactiveWeakMap = new WeakMap()
// 创建一个常量对象 维护各种 flag 标识
const REACTIVE_FLAGS = {
    IS_REAXTIVE: 'Has_Reactive'
}

/**
 * @description 实现一个reactive方法
 * @param {target} 响应式目标对象
 * @return {Proxy} 创建的代理对象实例
 */
function reactive(target) {
    // 判断是否是对象
    if (!isObject(target)) {
        return
    }

    // 判断对象是否有代理标识
    if (target[REACTIVE_FLAGS.IS_REAXTIVE]) {
        return target
    }

    // 创建对象前验证当前对象是否已经被代理过
    const exisitProxy = reactiveWeakMap.get(target)
    if (exisitProxy) {
        return exisitProxy
    }

    const proxy = new Proxy(target, {
        get(target, key, receiver) {

            // 第一次普通对象代理,通过 new Proxy代理返回代理对象,
            // 第一次传入的是代理对象,调用get方式获取 Flag 标识时命中改逻辑
            // 返回已经代理过的对象
            if (key === REACTIVE_FLAGS.IS_REAXTIVE) {
                return true;
            }
            //去代理上取值,执行get
            console.log(`获取属性:${key}, 值为:${target[key]}`, )
            return Reflect.get(target, key, receiver)
        },
        set(target, key, value, receiver) {
            //去代理上设置值,执行set
            console.log(`设置属性:${key}, 值为:${value}`, )
            //return target[key] = value
            return Reflect.set(target, key, value, receiver)
        }
    })
    // 首次代理的对象放入map中
    reactiveWeakMap.set(target, proxy)
    return proxy
}
复制代码

再次验证我们的 case 4, 发现完美验证通过。

小结

至此,一个简单的vue3中 reactive 精简版的源码实现完毕,当然正式版本中的逻辑比这个要复杂,我们只是实现了当中的核心源码来学习其中的思想与技巧。

至于 vue3的其他源码,我们会一步一步接着去学习。

未完待续

おすすめ

転載: juejin.im/post/7077103476475428895