什么是响应式
响应式是一种可以使我们声明式地处理变化的编程范式。举个栗子,在excel表格中,A3中的值是由A1和A2中的值相加而来,如果改变A1和A2中的值,A3则会自动计算。在js中怎么实现呢?我们需要定义一个函数update(),其中会返回A1和A2的和。这个update函数就被称为副作用effect,而其中用到的A1和A2则被视为这个作用的依赖。然后我们要在依赖变化时,调用这个副作用函数。如何知道依赖变化了呢?JavaScript 中有两种劫持 property 访问的方式:getter / setters 和 Proxies。以下我们来介绍Proxy这种方式,也就是Vue3中reactive响应式。
数据劫持(proxy代理)
Vue3中,reactive响应式数据,是由Proxy实现的。以下是实现的简易版reactive。
reactive.ts
import { track, trigger } from "./effect.js";
const isObject = (target) => target !== null && typeof target === 'object';
export const reactive = (target) => {
return new Proxy(target, {
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver);
track(target, key);
// 如果 target 的属性 还是一个对象,那么将该属性也变成响应式
if (isObject(res)) {
return reactive(res);
}
return Reflect.get(target, key, receiver);
},
set(target, key, newValue, receiver) {
let res = Reflect.set(target, key, newValue, receiver);
// 要在修改属性值之后 进行 依赖处理
trigger(target, key);
return res;
}
});
};
上述代码中,使用Proxy进行数据劫持。如果访问target中的属性时,会触发getter函数。getter函数中,会调用track()函数进行依赖收集,然后判断该属性的属性值是否为一个对象,如果不是,则返回该属性值(Reflect.get()),如果是,则继续调用reactive函数,实现深层次的响应。
依赖收集tracker和副作用触发trigger
在依赖变化时,我们需要知道哪几个依赖变化了,并将他们收集起来。然后触发副作用函数。
track(target, key)中,我们通过WeakMap 这种数据结构来收集副作用函数,WeakMap的键对应的是传入的源对象target,而值是一个Map对象;
Map对象的键对应的是传入的target的属性名key,而值是一个Set;
Set中用于存储副作用函数。
WeakMap、Map、Set是ES6新增的数据结构,不了解的可查看阮一峰的ES6入门文档。
数据格式如下:
trigger(target, key)中,我们根据传入的target和key,通过WeakMap,找到对应的副作用函数Effect,然后执行。
以下是完整示例effect.ts:
let activeEffect: Function
export const effect = (fn: Function) => {
// 这里使用闭包,将副作用函数传递出去
const _effect = function() {
activeEffect = _effect
fn()
}
// 开始时,先触发一次,渲染页面
_effect()
}
// 定义一个WeakMap
const targetMap = new WeakMap()
// 依赖收集
export const track = (target: object, key: string | symbol) => {
let depsMap = targetMap.get(target)
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: object, key: string | symbol) => {
const depsMap = targetMap.get(target)
const deps = depsMap.get(key)
deps.forEach((effect: () => any) => {
effect()
})
}
测试一下
现在我们来测试一下,新建一个html,并将reactive.ts和effect.ts引入:
index.html
<div id="app"></div>
<script type="module">
import {reactive} from './reactive.js'
import {effect} from './effect.js'
const man = reactive({
name: 'zhangsan',
age: 19,
job: {
job1: {
salary: '20k'
}
}
})
effect(()=>{
console.log('数据改变了');
console.log(man.name, man.age,man.job.job1.salary);
document.querySelector('#app').innerText = `${man.name} - ${man.age} - ${man.job.job1.salary}`
})
setTimeout(() => {
man.name = '张三'
setTimeout(()=>{
man.age = 23
setTimeout(()=>{
man.job.job1.salary = '21K'
}, 1000)
},1000)
console.log(man);
},1000)
</script>
这里我们只是模拟一下响应式,简单的操作一下Dom,没有实现Vue中虚拟Dom和模板解析的部分。上面代码首先通过reactive函数生成一个响应式对象。我们在effect中传入一个副作用函数,函数中,访问了name、age、salary,此时,Proxy代理对象就会通过getter函数进行依赖收集。在定时器中,属性值发生变化,Proxy代理对象就会调用setter函数,触发收集到的副作用函数。
我们可以使用live-server启动试验一下:
可以发现,一秒后,name发生变化,两秒后age也跟着改变,三秒后salary也变化了。至此我们的reactive响应式就算是成功了。