【Vue3源码】第三章 readonly详解 从零实现Vue3 readonly API
前言
上一章节我们实现了effect函数的stop和onstop功能,至此effect函数源码的编写就暂时告一段落了,这一章我们继续解读Vue3源码,开始实现Vue3 Reactivity :core 中的readonly API,并且优化之前写的reactive API。
实现readonly
readonly函数相信你在学习vue3的初始api时一定看到过,很简单的功能就是让调用这个函数的响应式对象只读,也就是无法set操作。
1.我们先看下单元测试代码
import { readonly } from "../reactive";
describe("readonlu", () => {
it("happy path", () => {
// not set
const original = { foo: 1, bar: { baz: 2 } };
const wrapped = readonly(original)
expect(wrapped).not.toBe(original)
expect(wrapped.foo).toBe(1)
});
});
2.实现readonly
在reactive.ts文件下导出这个函数
export const readonly = (raw) => {
return new Proxy(raw,{
get(target,key,receiver) {
const res = Reflect.get(target,key,receiver)
return res
},
set(target,key,value,receiver) {
return true
},
})
}
readonly真的很简单,我们的目的是要优化这些函数,readonly和reactive函数的代码高度相似,vue3就重构抽离了这些代码,来增加代码可读性。
3.优化reactive和readonly
reactive和readonly两个函数中代码和逻辑实在太相似了,如果重构抽离相似代码的话,就一点也不优雅了!
优化前:
所以我们要在原基础上对代码重构并且优化,让它们变得更加优雅~
优化后:
优雅~实在是太优雅了!!
3.1 重构get和set操作
vue3把get放入到了一个高阶函数creatGetter中,高阶函数返回get,set也同理,我们现在来封装一下:
function createGetter(isReadonly = true) {
return function get(target, key) {
const res = Reflect.get(target, key)
if (isReadonly) {
track(target, key)
}
return res
}
}
function createSetter() {
return function set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
trigger(target, key)
return res
}
}
export const reactive = (raw) => {
return new Proxy(raw, {
get: createGetter(),
set: createSetter()
})
}
export const readonly = (raw) => {
return new Proxy(raw, {
get: createGetter(false),
set(target, key, value, receiver) {
return true
},
})
}
其中get我们可以通过给定一个传参去判断是否是readonly,而readonly的set有些特殊我们先不进行修改~
但是这个代码还是不够美观,继续优化
3.2 baseHandlers
在src目录下新建一个baseHandlers.ts文件
并且把之前封装好的creatGetter,creatSetter提取到这个文件夹中
import { track, trigger } from "./effect"
function creatGetter(isReadonly = true) {
return function get(target, key) {
const res = Reflect.get(target, key)
if (isReadonly) {
track(target, key)
}
return res
}
}
function creatSetter() {
return function set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
trigger(target, key)
return res
}
}
//返回这个对象
export const mutableHandlers = {
get: creatGetter(),
set: creatSetter()
}
export const readonlyHandlers = {
get: creatGetter(false),
set(target, key, value, receiver) {
return true
},
}
然后我们再去reactive中引入mutableHandlers,readonlyHandlers即可
3.3 createActiveObject函数
为了语义化vue3又把new Proxy的操作也抽离成了createActiveObject函数
import { mutableHandlers, readonlyHandlers } from "./baseHandlers"
export const reactive = (raw) => {
return createActiveObject(raw, mutableHandlers)
}
export const readonly = (raw) => {
return createActiveObject(raw, readonlyHandlers)
}
function createActiveObject(raw,readonlyHandlers) {
return new Proxy(raw, readonlyHandlers)
}
3.4 优化get和set捕获器
我们回到baseHandlers.ts文件里来,最后一步优化
提问:我们有必要每次生成proxy对象时都去creatGetter和creatSetter 生成get和set吗?
显示是没有必要的,所以我们可以利用缓存技术,继续优化代码
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(false)
export const mutableHandlers = {
get,
set
}
export const readonlyHandlers = {
get: readonlyGet,
set(target, key, value, receiver) {
return true
},
}
3.5 警告用户不能set操作
这还没有结束,我们的readonly还差一个触发set操作时,警告用户无法set的功能
export const readonlyHandlers = {
get: readonlyGet,
set(target, key, value, receiver) {
console.warn(`key:${key} set失败,因为target是readonly的`,target)
return true
},
}
新增一个单元测试,测试:用户进行set操作时警告用户无法set操作!
it("warn then call set", () => {
console.warn = jest.fn()
const user = readonly({
age: 10
})
user.age = 11
expect(console.warn).toBeCalled()
})
成功通过所有测试了!
下节预告《vue3源码实现 isReactive 和 isReadonly》