前情提要
上篇讲了 Vue 2.0 相关响应式原理文章:通过手写一个简单版Vue学习其响应式原理
各位好,今天做一篇 Vue 3.0 的学习笔记。Vue 3.0 相信各位已经尝过鲜了,其中的 Composition API 更是让人眼前一亮,那么它们是怎么实现的呢?这次就让我们通过手写 Composition API 来学习它们的实现原理。
完整项目地址:github.com/zhtzhtx/Ter…
reactive
reactive 的作用是返回一个对象的响应式代理。简单来说,就是使用 Proxy 代理来使传入对象转为响应式对象,同时相较于 Object.definePrototype, Proxy 可以通过 handler 的 deleteProperty 方法来拦截 delete 操作。
// 判定是否为对象(不为null)
const isObject = val => val !== null && typeof val === 'object'
// 判断属性值是否为对象,如果是,则要递归将其也转化为响应式对象
const convert = target => isObject(target) ? reactive(target) : target
export function reactive(target) {
// 判定传参是否为对象,如果不是直接返回
if (!isObject(target)) return target
const handler = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
// 判断属性值是否为对象,如果是,则要递归将其也转化为响应式对象
return convert(result)
},
set(target, key, value, receiver) {
// 获取目标对象中对应属性值
const oldValue = Reflect.get(target, key, receiver)
// 在 set 中需要返回一个 boolean 类型的值标志是否赋值成功
let result = true
// 如果属性值不同
if (oldValue !== value) {
// 更新属性值,获取是否更新成功
result = Reflect.set(target, key, value, receiver)
}
return result
},
deleteProperty(target, key) {
// 删除该属性,获取是否删除成功
const result = Reflect.deleteProperty(target, key)
return result
}
}
return new Proxy(target, handler)
}
也许各位和我一样,对 Proxy 的作用比较熟悉,但对 Reflect 的作用并不了解,推荐一篇文章:原文地址,这里就不赘述了。
好了,这样 reactive 函数就基本实现了,当然现在看起来它并没有什么作用,但是别急,之后我们会对它进一步加工。
effect
我们之前在 reactive 函数学习如何将目标对象转化成响应式对象,但仅仅这样还是不够的。在 Vue 3.0 中,使用 effect api 可以监听目标对象的变化,当它数据改变时,会触发 effect 中的回调函数,这是如何实现的呢?
答案是跟 Vue 2.0 一样通过依赖收集,当数据改变时发布更新。如果你对依赖收集概念不了解,可以看我之前发的 Vue 2.0 的响应式原理。
我们先了解一下 Vue 3.0 中的依赖收集方式,它首先创建一个 WeakMap,使用 WeakMap 的原因是当目标对象不存在时,WeakMap 会自动销毁该对象对应的 value。WeakMap 的 key 是目标对象,value 是一个 Map,这个 Map 的 key 是目标对象的属性名,value 是 effect 的回调函数。
在了解完收集依赖的原理后,我们来看 effect 函数:
// 定义一个全局变量用于记录 callback
let activeEffect = null
export function effect(callback) {
// 记录 callback 函数
activeEffect = callback
// 调用 callback 函数
callback()
// 清空记录的 callback 函数
activeEffect = null
}
track
上述的 effect 函数是不是很简单?有人说,你这完全没有收集依赖,activeEffect 变量没有用啊。确实,所以我们还需要另一个函数 track进行依赖收集。
// 定义一个 WeakMap 用于存储目标对象和 Map
let targetMap = new WeakMap()
export function track(target, key) {
// 如果没有目标对象直接返回
if (!activeEffect) return
// 获取当前目标对象对应的 Map 对象
let depsMap = targetMap.get(target)
// 如果没有 Map 对象,则初始化一个 Map 对象
if (!depsMap) {
// 将 Map 对象 和目标对象存储在 WeakMap 中
targetMap.set(target, (depsMap = new Map()))
}
// 获取 Map对象中属性名对应的值
let dep = depsMap.get(key)
// 如果属性值不存在,初始化一个 Set 对象作为属性值
if (!dep) {
// 将属性值和属性名存储在 Map 对象中
depsMap.set(key, (dep = new Set()))
}
// 在属性值中添加回调事件
dep.add(activeEffect)
}
这样 track 函数就完成了,那么我们应该在哪里调用它呢?其实和 Vue 2.0 中一样,在获取目标对象值时调用,也就是在 reactive 函数的 get 方法中:
// 省略。。。
const handler = {
get(target, key, receiver) {
// 收集依赖
track(target, key)
const result = Reflect.get(target, key, receiver)
// 判断属性值是否为对象,如果是,则要递归将其也转化为响应式对象
return convert(result)
}
}
// 省略。。。
当我们在 Vue 3.0 中,调用 effect 函数时,如果函数中存在获取响应式对象的值,则会进入该对象的 get 函数中,然后在 get 方法中,触发 track 方法进行依赖收集。
trigger
在收集依赖后,当然需要触发更新,所以我们定义一个新的函数 trigger,在函数中获取 WeakMap 中目标函数对应的 Map 对象。然后获取 Map 对象中,属性名对应的回调函数组,遍历回调函数组依次调用:
// 定义一个 WeakMap 用于存储目标对象和 Map
let targetMap = new WeakMap()
export function track(target, key) {
// 省略。。。
}
export function trigger(target, key) {
// 获取 WeakMap 中的目标对象对应的 Map 对象
const depsMap = targetMap.get(target)
// 如果不存在 Map 对象直接返回
if (!depsMap) return
// 获取 Map 对象中属性名对应的回调函数组
const dep = depsMap.get(key)
// 如果存在回调函数组
if (dep) {
// 遍历回调函数组
dep.forEach(effect => {
// 触发回调函数
effect()
})
}
}
和 Vue 2.0 中一样,当响应式对象数据更新时,在触发 reactive 函数的 set 方法或者 deleteProperty 方法时,调用 trigger 函数触发回调函数:
// 省略。。。
set(target, key, value, receiver) {
// 获取目标对象中对应属性值
const oldValue = Reflect.get(target, key, receiver)
// 在 set 中需要返回一个 boolean 类型的值标志是否赋值成功
let result = true
// 如果属性值不同
if (oldValue !== value) {
// 更新属性值,获取是否更新成功
result = Reflect.set(target, key, value, receiver)
// 触发更新
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
// 判断是否有该属性名
const hadKey = hasOwn(target, key)
// 删除该属性,获取是否删除成功
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
// 触发更新
trigger(target, key)
}
return result
}
// 省略。。。
ref
下面我们来实现 ref 这个 api,它的作用是返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性,它接受一个内部值作为参数。
简单来就是将一个基础类型的值(如字符串,数字等)包装成一个响应式对象,我们首先判断传入的值是否是 ref 创建的对象,如果是的话直接返回。那么怎么判断是否是 ref 创建的对象。呢?我们在 ref 中定义一个__v_isRef 属性,如果传入的参数具有这个属性则判定它是 ref 创建的对象。
接着,判断属性值是否为对象,如果是,则要递归将其也转化为响应式对象。在 ref 创建的对象中,同样需要在 get 方法中收集依赖,在 set 方法中触发回调函数。
export function ref(raw) {
// 判断raw是否是ref创建的对象,如果是的话直接返回
if (isObject(raw) && raw.__v_isRef) return
// 判断属性值是否为对象,如果是,则要递归将其也转化为响应式对象
let value = convert(raw)
const r = {
__v_isRef: true,
get value() {
// 收集依赖
track(r, 'value')
return value
},
set value(newValue) {
if (newValue !== value) {
raw = newValue
// 判断属性值是否为对象,如果是,则要递归将其也转化为响应式对象
value = convert(raw)
// 触发更新
trigger(r, 'value')
}
}
}
return r
}
toRefs
然后,我们来编写 toRefs 这个 api,它的作用是将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref,这样使用它可以解构/展开返回的对象而不会失去响应性。
在这个函数中,我们先判断传入的参数是数组还是对象,然后创建一个新空数组/对象。然后遍历参数,通过 toProxyRef 方法将响应式对象每个属性值进行封装:
export function toRefs(proxy) {
// 判断是否为数组
const ret = proxy instanceof Array ? new Array(proxy.length) : {}
for (const key in proxy) {
// 将响应式对象的值都通过 ref 方法封装
ret[key] = toProxyRef(proxy, key)
}
return ret
}
function toProxyRef(proxy, key) {
const r = {
// 标志是 ref 对象
__v_isRef: true,
get value() {
return proxy[key]
},
set value(newValue) {
proxy[key] = newValue
}
}
return r
}
computed
最后,我们来实现 computed 这个 api,它接受一个 getter 函数,返回一个只读的响应式 ref 对象。
在 computed 函数中,我们通过 ref 方法创建一个空的响应式对象,然后通过 effect 方法响应数据变化,每次数据变化后都再次调用传入的 getter 函数:
export function computed(getter) {
// 默认value的值是undefined
const result = ref()
// 使用 effect 方法对象响应数据变化
effect(() => (result.value = getter()))
return result
}
总结
这样,我们就学习了 Vue 3.0 中常用的几个 Composition API 的实现原理,相信各位已经对 Vue 3.0 更加了解了吧,希望能给各位带来帮助。