我正在参加「掘金·启航计划」
1.什么是响应性
这个术语在今天的各种编程讨论中经常出现,但人们说它的时候究竟是想表达什么意思呢?本质上,响应性是一种可以使我们声明式地处理变化的编程范式。
对于js而言
let A0 = 1
let A1 = 2
let A2 = A0 + A1
console.log(A2) // 3
A0 = 2
console.log(A2) // 仍然是 3
当我们更改 A0 后,A2 不会自动更新
。
那么我们如何在 JavaScript 中做到这一点呢?首先,为了能重新运行计算的代码来更新 A2,我们需要将其包装为一个函数:
let A2
function effect() {
A2 = A0 + A1
}
然后,我们在定义几个术语
- 这个 effect() 函数会产生一个副作用,或者就简称为作用 (effect),因为它会更改程序里的状态。
- A0 和 A1 被视为这个作用的依赖 (dependency),因为它们的值被用来执行这个作用。因此这次作用也可以说是一个它依赖的订阅者 (subscriber)。
所以我们希望有个函数,能够在 A0
或 A1
(这两个依赖) 变化时调用 effect() (产生作用)
whenDepsChange(update)
我们希望whenDepsChange
函数应该有如下功能:
1.在变量被读取的时候进行追踪,比如我们在A0+A1时,我们应该就跟踪到A0和A1
2.当变量在挡在effect()
执行是被访问,那effect()
需要在第一次调用后,成为A0和A1的订阅者
3.当A0或者A1发生变化后,我们应该通知其effect()
函数执行
举个简单的例子:
let A0 = 1
let A1 = 2
const effect = () => {A2 = A1 + A0 }
effect() //先执行一次
console.log(A2) // 3
A0= 2
effect() //再执行一次
console.log(A2) // 4
但这样也会存在问题,就是如果存在一个依赖在多处使用的情况,那我们就需要产生多个effect,举个简单的例子
let A0 = 1
let A1 = 2
const effect = () => {A2 = A1 + A0 }
const effect1 = () => {A2 = A1 + A0 + 5 }
effect() //先执行一次
effect1() //先执行一次
console.log(A2) // 3
console.log(A2) // 8
A0= 2
effect() //再执行一次
effect1() //再执行一次
console.log(A2) // 4
console.log(A2) // 9
2.什么是track和trigger
针对于上面的问题,想出来的解决办法就是,用track
函数把所有依赖于A2
变量的effect函数都收集起来,放在dep
中,以后只要A2
变量一改变,就执行trigger
函数通知dep
里面所有的依赖A2
变量的函数执行,实现依赖变量的更新,代码演示如下:
let A0 = 1
let A1 = 2
const effect = () => {A2 = A1 + A0 }
const effect1 = () => {A2 = A1 + A0 + 5 }
const dep = new Set()
const track = () => {
dep.add(effect)
dep.add(effect1)
}
const trigger = () => {
dep.forEach(effect => effect())
}
effect() //先执行一次
effect() //先执行一次
console.log(A2) // 3
console.log(A2) // 8
A0= 2
trigger()
console.log(A2) // 4
console.log(A2) // 9
用一张图来说明依赖收集和响应更新的过程
总结:就是收集A0变量,在几个地方使用,就有几个副作用effect
,如果后续数据发生变化,那么就通知其所有的副作用effect
再次执行
想一想,如果是在一个对象
里面怎么办,或者多个对象
里面怎么办
举个简单的例子:我们要收集不同的对象下面的属性,做出不同的变更
let person = {
name:"张三",
age:18
}
let animal = {
name:"大黄",
age:2
}
let personStr
let animalStr
const personEffect = () => { personStr = `名字叫${person.name},年龄${person.age}`}
const animalEffect = () => { animalStr = `名字叫${animal.name},年龄${animal.age}`}
//收集依赖的过程
const targetMap = new WeakMap()
const track = (target,key) => {
const depsMap = targetMap.get(target)
if(!depsMap){
targetMap.set(target,depsMap = new Map())
}
let dep = depsMap.get(key)
if(!dep){
depsMap.set(key, dep = new Set())
}
if(target === person){
if(key ==='name'){
dep.add(personEffect1)
dep.add(personEffect2)
}else if(key ==='age'){
dep.add(personEffect1)
}
}else if(target === animal){
if(key ==='name'){
dep.add(animalEffect1)
dep.add(animalEffect2)
}else if(key ==='age'){
dep.add(animalEffect1)
}
}
}
personEffect()
animalEffect()
console.log(personStr,animalEffect)
// 名字叫张三,年龄18 名字叫大黄,年龄2
//收集对应对象下面的对应数据的依赖
track(person,'name')
track(person,'age')
track(animal,'name')
track(animal,'age')
//在set的时候执行trigger
function trigger(target, key) {
let depsMap = targetMap.get(target)
if (depsMap) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
}
person.name = '李四'
persong.age = 25
animal.name = '小黄'
animal.age = 3
trigger(person,'name')
trigger(person,'age')
trigger(animal,'name')
trigger(animal,'age')
console.log(personStr,animalEffect)
// 名字叫李四,年龄25 名字叫小黄,年龄3
一张图,表示多对象收集收集依赖
总结:
- 一个对象里面,那么对象中那一个属性被用到,就去追踪到那个属性
- 一个属性如果在多个地方用到,那么就会产生多个effect
- 最终得出来的结论就是,那一个对象的那一个属性,在哪里调用,就产生一个effect
3.实现track和trigger
- 明确第一点,就是我们要知道当前执行的副作用函数effect是在收集那个依赖
let state = reactive({name:'ly'},age:12,arr:[1,23])
effect(() => {
app.innerHTML = state.name;
})
1.依赖收集的实现(track)
1.我们要让effect变成响应式的effect,可以等到数据变化的时候就重新执行
export function effect(fn,options:any={}){
//我需要让这个effect变成响应式的effect,可以坐到数据变化就重新执行
const effect = createReactiveEffect(fn,options)
//默认effect会先执行一次
effect()
return effect
}
- createReactiveEffect的实现,保证当前的activeEffect存在
let uid = 0
const effectStack = []
let activeEffect; //存储当前的effect
export function createReactiveEffect(fn,options){
const effect = function reactiveEffect(){
if(!effectStack.includes(effect)){
try {
effectStack.push(effect)
activeEffect = effect
return fn() //函数执行时取值, 会执行get方法
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
}
effect.id = uid++ //制作一个effect标识,用于区分effect
effect._isEffect = true //用于标识这个是响应式的effect
effect.raw = fn //保留effect对应的原函数
effect.options = options //在effect上面保存用户的属性
return effect
}
- 在effect函数执行的执行,就会走到get方法中,get方法中会知道是哪个一个对象的,那一个属性,正在收集依赖,保证使那个对象的那个属性,收集到对应的effect
get(target, key, receiver) { // let proxy = reactive({obj:{}})
const res = Reflect.get(target, key, receiver); // target[key];
if(!isReadonly){
console.log('执行effect时会取值','收集effect')
track(target,'get',key)
}
}
- track方法,收集依赖
let targetMap = new WeakMap()
export function track(target,type,key){
//activeEffect //当前正在运行的effect
console.log(activeEffect)
if(activeEffect === undefined){ //
return
}
//然后收集对应对象中,对应的key
let depsMap = targetMap.get(target)
if(!depsMap){
targetMap.set(target,(depsMap = new Map()))
}
let dep = depsMap.get(key) //获取属性,所对应收集的effect数组
if(!dep){
depsMap.set(key,(dep = new Set()))
}
if(!dep.has(activeEffect)){
dep.add(activeEffect)
}
console.log(targetMap)
}
2.trigger的触发更新
let state = reactive({name:'ly'},age:12,arr:[1,23])
effect(() => {
app.innerHTML = state.name;
})
setTimeout(() => {
state.name = 'zs'; // 更改namne属性需要重新执行
}, 1000);
1.在set方法中判断,判断是数组还是对象,判断是新增属性还是添加属性
set(target, key, value, receiver) {
const oldValue = target[key]; // 获取老的值
//判断是新增还是修改方法
let hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target,key)
const result = Reflect.set(target, key, value, receiver); // target[key] = value
if(!hadKey){
//表示是新增
trigger(target,'add',key,value)
}else if(hasChanged(oldValue,value)){
//表示是修改
trigger(target,'set',key,value,oldValue)
}
return result;
}
}
2.找到对应的对象下面的属性,所收集的依赖,然后触发更新
export function trigger(target,type,key?,value?,oldValue?){
//如果这个属性没有收集过effect,那不需要做任何操作
const depsMap = targetMap.get(target)
if(!depsMap) return
const effects = new Set()
const add = (effectsToAdd) => {
if(effectsToAdd){
effectsToAdd.forEach(effect => effects.add(effect))
}
}
if(key !== undefined){
add(depsMap.get(key)) //修改对象的值
}
//总结,就是把对应effect收集起来,去重后,在批量执行
effects.forEach((effect:any) => effect())
}