在这篇文章中我们将要实现一个最基本的响应式系统,即在修改对象特定值时,自动重新执行注册的副作用函数,实现更新。如以下代码所示:
// vue/examples/reactivity/basicImplementationOfReactive.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
</body>
<script>
const { reactive, effect } = Vue
const obj = reactive({
name: 'echo',
})
effect(() => {
document.body.innerText = obj.name
})
// 2s后自动更新body中的内容为yahaha
setTimeout(() => {
obj.name = 'yahaha'
}, 2000)
</script>
</html>
首先我们需要知道的是一个响应式系统的工作流程:
- 在读取操作发生时,将副作用函数收集到“桶”中;
- 在设置操作发生时,将副作用函数从“桶”中取出并执行。
从上面介绍的内容我们可以看到我们需要“桶”来存放副作用函数,同时也需要一个提供一个用来注册副作用函数的机制。
“桶”
// reactivity/src/effect.ts
// 存储副作用的容器
export const targetSet = new Set<() => any>()
注册副作用函数的方法
// reactivity/src/effect.ts
// 用一个全局变量存储被注册的副作用函数
export let activeEffect: undefined | (() => any)
// 用于注册副作用函数
export function effect(fn: () => any) {
// 当调用effect注册副作用函数时,将fn赋值给activeEffect
activeEffect = fn
// 执行副作用函数
fn()
实现响应式系统的工作流程
参照vue3的源码,我们将读取和设置操作以以下代码形式实现:
// reactivity/src/baseHandlers.ts
import { activeEffect, targetSet } from './effect'
const get = createGetter()
const set = createSetter()
function createGetter() {
// 此处target应该为object类型,但是ts类型检查无法通过,此处先这样声明,在后续实现中进行修改
return function get(target: Record<string, any>, key: string) {
// 将activeEffect中存储的副作用函数收集到桶中
if (activeEffect)
targetSet.add(activeEffect)
return target[key]
}
}
function createSetter() {
return function set(target: Record<string, any>, key: string, newValue: unknown) {
target[key] = newValue
// 在设置操作时执行副作用函数
targetSet.forEach(fn => fn())
return true
}
}
// proxy的代理函数
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
}
我们在reactive.ts中添加reactive方法,从而代理我们想要响应式更新的数据。
// reactivity/src/reactive.ts
import { mutableHandlers } from './baseHandlers'
export function reactive(target: object) {
return new Proxy(target, mutableHandlers)
}
方法导出
将reactivity库中的函数导出:
// reactivity/src/index.ts
export { effect } from './effect'
export { reactive } from './reactive'
然而在vue库中也进行导出:
// vue/src/index.ts
export { reactive, effect } from '@my-vue3/reactivity'
编译后就可以看到文章开头所提到的效果了。
存在的问题
// vue/examples/reactivity/problem.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
</body>
<script>
const { reactive, effect } = Vue
const obj = reactive({
name: 'echo',
})
effect(() => {
// 打印两次
console.log('effect run')
document.body.innerText = obj.name
})
setTimeout(() => {
// 副作用函数并没有对此字段进行读取
obj.noExist = 'hello yahaha'
}, 2000)
</script>
</html>
如上面的代码所示,如果我们修改了一个了文章开头的示例代码中的setTimeout中obj不存在的字段时,在effect的匿名函数中并没有对这个字段进行读取,但是'effect run'还是会执行两次。这个问题我将在下一篇文章中分析并解决。