这是我参与「第五届青训营 」伴学笔记创作活动的第 14 天
vue2的不足
- 对象只能劫持设置好的数据,新增的数据需要Vue. Set(xxx)数组只能操作七种方法,修改某一项值无法劫持。
- 对象新增的属性没有响应式
- 基于Object.defineProperty,不具备监听数组的能力,需要重新定义数组的原型来达到响应式。
- Object.defineProperty 无法检测到对象属性的添加和删除 。
- 由于Vue会在初始化实例时对属性执行getter/setter转化,所有属性必须在data对象上存在才能让Vue将它转换为响应式。
- 深度监听需要一次性递归,对性能影响比较大。
什么是副作用函数
举个例子,这里是两个完全不同的函数,第一个函数的入参有个默认值,主要作用是把传入的值渲染到body中,第二个函数的作用则是打印body中的值,如果我们改变第一个函数的值,则第二个函数的输出也会收到影响,这里称为副作用
// 设置 body 中文本内容
function setTextForBody(text = 'hello vue3'){
document.body.innerText = text
}
// 获取 body 中的文本并输出
function getTextFromBody(){
console.log("document.body = ", document.body.innerText)
}
什么是响应式数据
举个例子,当我们修改某一个值时,副作用函数可以自动执行,不用我们人为去设置,这就是响应式数据
// 初始数据
const data = { text: 'hello world' }
// 副作用函数
function effect(){
document.body.innerText = data.text
}
// 修改数据
setTimeout(() => {
data.text = 'hello vue3'
}, 3000);
用最少的代码实现响应式系统
思路
- 将原始数据进行代理,实现 getter 和 setter 函数
- 当执行副作用 effect 函数时,会触发对应数据的 getter 函数,此时将这个 effect 函数保存到容器 activeEffect 中,等待在未来某时刻执行
- 当执行 data.text = xxx 操作时,会触发对应数据的 setter 函数,此时从容器 activeEffect 中取出所有 effect 函数并执行它们
副作用函数Effect
- 首先创建一个容器来装我们的依赖,一开始要做的是将我们要触发的函数保存起来,所以才创建了_effect以便于我们后面执行
- 然后就是收集依赖,首先创建一个track函数,参数为target和key,为什么是这两个,因为后面的Proxy的get方法有这两个参数,方面我们做后续的操作。
- 接着创建一个WeakMap,为什么不是Map呢? 问就是性能优化。我们在这个WeakMap中的key主要存储传过来的响应式对象数据源,value则存储另外一个map
- 这里放WeakMap的用法,注意这里的key必须是对象,这刚好对应我们的响应式对象数据源
- developer.mozilla.org/zh-CN/docs/…
- 而另外一个map的key则是存储响应数据源的key,value则是一个Set,里面存放着我们的依赖函数
- --------------------分割线--------------------------
- trigger的实现就简单了,我们先根据数据源(对象)来通过WeakMap获取到map,然后通过key来获取到对应的函数,然后执行,
- 这就是为什么map的key存的是响应式对象数据源的key,方便我们执行执行对应的依赖
- 这里有个疑问🤔️,那这样随便修改一个值都是执行同一个依赖了,这引发了我的思考
/**
* 当依赖发生变换,去执行对应的副作用函数
*/
let activeEffect;
export const effect = (fn) => {
const _effect = () => {
activeEffect = _effect
fn()
}
_effect()
}
/**
* 实现依赖的收集
*/
const targetMap = new WeakMap()
export const track = (target, key) => {
// console.log(target,key)
let depsMap = targetMap.get(target)
// console.log(depsMap)
// 如果没有响应式数据,我们就创建一个空的
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
}
export const trigger = (target, key) => {
// 触发依赖
const depsMap = targetMap.get(target)
const deps = depsMap.get(key)
deps.forEach((effect) => {
effect()
})
}
这里放一张流程图
reactive
import { track, trigger } from './effect.js'
const isObject = (target: any) => target !== null && typeof target == 'object'
export const reactive = <T extends object>(target: T): any => {
return new Proxy(target, {
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver) as object
// 收集依赖
track(target, key)
if (isObject(res)) {
return reactive(res)
}
return res
},
set(target, key, value, receiver) {
let res = Reflect.set(target, key, value, receiver)
// 触发依赖
trigger(target, key)
return res
},
})
}
测试代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
<script type="module">
import { reactive } from './reactive.js'
import { effect } from './effect.js'
const user = reactive({
name: 'xxx',
age: 18,
})
effect(() => {
// 这里会执行一次收集依赖
document.querySelector('#app').innerHTML = `<h1>${user.name} + ${user.age}</h1> `
})
setTimeout(() => {
//这里首先执行get,然后执行set
user.name = 'yyy'
setTimeout(() => {
user.age = 20
}, 1000)
}, 1000)
</script>
</html>
为什么需要Reflect来返回
特定情况会造成上下文的错乱,有可能造成 this 指向错误,产生死循环,具体看霍春阳的《Vue设计与实现》,或者看这篇文章
总结
这里只是简易的实现了响应式系统,vue中真正的响应式系统还设计了很多东西,待我有空一点点去啃食他。