通过手写 Composition API 学习 Vue 3.0 相关知识

119 阅读8分钟

前情提要


上篇讲了 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 的回调函数。

image.png

在了解完收集依赖的原理后,我们来看 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 更加了解了吧,希望能给各位带来帮助。