02-响应式原理reactive&effect

162 阅读3分钟

Vue3 对比 Vue2 的变化

  • 在 Vue2 的时候使用 defineProperty 来进行数据劫持,通过getter & setter 对属性进行重新。因此,性能差(与Vue3对比)。
  • 当新增属性和删除属性时,无法监控变化。需要通过 $set & $delete 实现。
  • 数组不采用 defineProperty 来进行劫持(浪费性能、对所有索引进行劫持造成性能浪费)需要对数组进行单独进行处理。

Vue3 中采用 Proxy 来实现响应式数据变化,从而解决上述问题。

reactive & effect

// operation.ts
const TrackOpType {
    GET = 'get'
}
const TriggerOpType {
    SET = 'set'
}

reactive

  • 使用 Proxy & Reflect 实现数据响应式
  • 参数必须是数组或对象,否则会报警告
  • 当传入同一对象时,会返回同一个 proxy
  • 当传入 proxy 对象时,该proxy 对象会直接被返回
// reacitve.js
const enum ReactiveFlags = {
    IS_REACTIVE = '__v_isReactive'
}
const proxyMap = new WeakMap(); // 缓存 已经被 代理 的数据
function reactive(target) {
    if(!isObject(target)) {
        // target 如果不是数组或对象,则无法被 代理
        return console.warn(`value can not be reactive ${target}`)
    }
    // 判断 target 是否已经被 代理 过
    const exisitingProxy = proxyMap.get(target); // 
    if(exisitingProxy) {
        // 如果被代理过,直接返回被代理过的数据
        return exisitingProxy
    }
    // 判断 target 是否是一个已经被代理过的 proxy
    if(target[ReactiveFlags.IS_REACTIVE]){
        // 如果 target 是一个已经被代理过的 proxy,直接返回 target
        return target
    }
    const proxy = new Proxy(target, mutableHandler);
    // 将被代理的数据放入缓存
    poxyMap.set(target,proxy)
    return proxy
}

const mutableHandler = {
    get(target,key,receiver) {
        // 当被代理的对象访问 '__v_isReactive' 属性时,返回true,以判断该对象是否是被代理过的对象
        if(key === ReactiveFlags.IS_REACTIVE) {
            return true;
        }
        const res = Refelect(target,key,receiver)
        track(target,TrackOpType.GET,key)
         // 实现深层 track & trigger
        if(isObject(res)) {
            reactive(res)
        }
        return res
    },
    set(target,key,value,receiver) {
        const oldValue = target[key]
        const res = Refelect(target,key,value,receiver)
        if(oldValue !== value) {
            // 当新旧值,不相等时,则执行 trigger 触发副作用
            trigger(target,TriggerOpType.SET,value,oldValue)
        }
        return res
    }
}

effect

effect 副作用函数

  • 参数必须是一个函数;

  • 该函数会被立即执行一次,

  • 该函数会被当做参数,创建一个 ReactiveEffct 的实例

  • 该实例,会记录实例的一些属性

    • 是否处于激活状态
    • 上一个 activeEffect(Vue3 采用的链表记录,上一个实例是谁,而 Vue2 采用 栈 实现该逻辑)
// effect.js
  let activeEffect = null; // 记录处于运行状态的 effect,这样全局任何地方都可以拿到当前运行的 effect
  class ReactiveEffect {
      public active = true // 当前实例的状态
      public parent = null // 上一个运行的 effect
      public deps = []; // 保存 副作用 对应的 依赖
      constructor(fn) {
          this.fn = fn;
      }
      run() {
          if(!this.active) {
             // 如果当前实例,处于非激活状态,仅执行 fn 函数
              return this.fn()
          }
          try {
             this.parent = activeEffect; // 记录上一个运行的effect
             activeEffect = this; // 将当前运行的 effect 改为 当前实例
             return this.fn() // 执行函数 
          } finally {
              // 当函数执行完成后,将当前运行的 effct 置为 上一个 实例
              activeEffect = this.parent
          }
          
      }
  }
  function effect(fn) {
      const _effect = new ReactiveEffect(fn)
      _effect.run()
  }

依赖收集

track 副作用追踪

  • 当属性被get时,则调用 track,将当前的 activeEffect放入当对应对象的属性上,以方便set的时候,能够一一取出,并被执行。
const targetMap = new WeakMap(); // 缓存 对象 属性上对应 依赖 的 effect
function track(target,type,key) {
    const desMap = targetMap.get(target)
    if(!desMap) {
        const despMap = new Map()
        targetMap.set(target,depsMap)
    }
    const deps = depsMap.get(key)
    if(!deps) {
        deps = new Set()
        depsMap.set(key,deps)
    }
    trackEffects(deps)
}

function trackEffects(des) {
    if(activeEffect) {
        deps.add(activeEffect)
        activeEffect.deps.push(deps)
    }
}

trigger 触发依赖的副作用函数

  • 当属性被set的时,则查找到对应属性依赖的副作用,并执行这些副作用
 function trigger(target,type,key,value,oldValue) {
    const depsMap = targetMap.get(target);
    if(!depsMap) return
    const deps = depsMap.get(key)
    if(!deps) return
    triggerEffect(deps)
    
 }
 
 function triggerEffect(deps) {
     const effects = new Set(deps)
     effects.forEach(effect => {
        if(effect !== activeEffect)  {
            // 当副作用 不等于 当前副作用 时,执行该副作用,否则会出现 循环调用 error
            effect.run()
        }
    })
 }
 
 // 循环调用副作用的例子
 const obj = {
     age: 16
 }
 effect(()=> {
     obj.age = Math.random(); // 这里就会出现 循环调用,在副作用里对副作用对应的属性赋予随机值
     app.innerHTML = obj.age
 })
 setTimeout(()=> {
     obj.age = 18
 })
  • effect map 之间的关系

01-effect-map.png

分支切换实现的原理

举个例子

<script>
const obj = {
    name: 'jyp',
    age: 18,
    flag: true,
}
const state = reactive(obj)
effect(()=> {
    console.log('render')
    app.innerHTML = state.flag ? state.name : state.age  
})
setTimeout(()=> {
    state.flag = false;
    setTimeout(()=> {
        // 思考一下: name 改变了,effect 还需要执行吗?
        // 答案:不应该执行,因为 flag = flase,所以该副作用应该是 age 改变时,才执行
        // 现状:'render' 会被打印三次,最后一次是因为 name 改变,执行副作用的结果
        // 期望: name 改变,不会引发 effect 执行
        // 解决方案:在每次执行副作用前,清除该副作用之前所有依赖,然后重新添加依赖
        state.name = 'zs'
    },1000)
},2000)
</script>
  • 代码思路
// effect.js

// 改造 triggerEffect 函数
function triggerEffect(deps) {
    // deps 是 Set<ReactiveEffect>[] 数组
    const effects = [...deps]; // 浅拷贝,因为待会清除时,会操作 deps,这样就避免 循环引用的问题
    effects.forEach(effect => {
        effect.run()
    })
}


// 改造 ReactiveEffect 类中 run 方法
class ReactiveEffect {
    run() {
        if(!this.active){
            return this.fn()
        }
        
        try {
            this.parent = ativeEffect;
            activeEffect = this.parent;
            cleanupEffect(this) // 在执行函数之前,先清除依赖
            return this.fn()
        } finally {
            activeEffect = this.parent
        }
    }
}
// cleanupEffect
function cleanupEffect(effect) {
    let deps = effect.deps
    if(deps) {
        deps.forEach(dep => {
            dep && dep.delete(effect) // 清除当前的 effect
        })
    }
    effect.deps = [] // 将当前 effect 的 deps 记录,置为空数组
}

readonly

  • readonly 基本思路与 reactive 基本一致,只是在 proxy handler 中的 getter & setter 中的处理不同。
// readonlyHandler 对象
const readonlyHanlder = {
    get(target,key,receiver){
        const res = Reflect(target,key,receiver)
        track(target,TrackOpType.GET,key)
        if(isObject(res)){
           return readonly(res)
        }
        return res
    },
    set(target,key,value,receiver) {
       return console.warn(`Set operation on "${String(key)}" failed: target is readonly`)
    }
}

shallowReactive

  • shallowReactive 与 reactive最重要的区别在于,就只有一层数据是响应式的,其他的都是非响应式的。

  • Proxy的特点是,

    • (1)会监听被代理的对象所有的取值与赋值操作,
    • (2)取值逻辑,层层触发的。比如,proxy.obj.name运行时,get方法会执行2次。第一次执行proxy.obj,第二次在第一次的结果上执行obj.name。如果 obj 不是 proxy,那对obj 的属性进行赋值操作时,不会走set方法。
  • 要实现一层响应式,就只要在get方法中直接返回Reflect.get返回的结果即可,而不对Reflect.get返回结果使用reactive 递归产生的 proxy对象即可。

const shallowReactiveHanlders = {
    get(target,key,receiver) {
        const res = Reflect.get(target,key,receiver)
        track(target.TrackOpType.GET,key)
        return res
    },
    set(target,key,value,receiver) {
        const oldValue = target[key];
        const res = Reflect.set(target,key,value,receiver);
        if(oldValue !== value) {
            trigger(target,TriggerOpType.SET,key,value,oldValue);
        }
        return res
    }
}

shallowReadonly

  • 数据不是响应式的,
  • 只有第一层不可以set
const shallowReadonlyHandlers = {
    get(target,key,receiver) {
        const res = Reflect(target,key,receiver)
        return res
    },
    set(target,key,value,receiver) {
        return console.log(`Set operation on "$String(key)" failed: target is readonly`)
    }
}

isReactive & isReadony

  • 判断一个对象是否被代理,在 Vue 内部主要是在访问该对象 ReactiveFlag.IS_REACTIVE 这个属性。如果该对象是被代理过的,那么进入get方法后,返回 取反后的isReadonly的值。

  • 如果该代理是 readonly 创建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true。

    • 如果该对象是 isReadonly

    • 访问对象上Reactive.IS_RAW 属性,

    • 根据isReaonly 和 shallow 变量的值,取出对应WeakMap 缓存的对象,

    • 判断 receiver 是否与 缓存对象是否相等。

      • 相等就返回该对象
function isRective(value) {
  if (isReadonly(value)) {
    return isReactive((value as Target)[ReactiveFlags.IS_RAW]);
  }
  return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE]);
}

// hanlder 中 get 函数
const handers = {
    get(target,key,receiver) {
        if(key === ReactiveFlags.IS_REACTIVE) {
            return !isReaonly
        }else if (
         key === ReactiveFlags.IS_RAW &&
         receiver ===
        (isReaodly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target;
    }
    }
}
  • 同理 isReadony 直接访问对象上的 ReactiveFlag.IS_READONLY 属性,然后get方法直接返回isReadonly属性
function isReadonly(value) {
    return !!(value && value[ReactiveFlag.IS_READONLY])
}

// handler 中的 get 函数
const handlers = {
    get(target,key,receiver) {
        if(key === ReacitveFlag.IS_READONLY) {
            return isReadonly
        }
    }
}

isProxy

  • 检查对象是否是由 reactive 或 readonly 创建的 proxy
function isProxy(value) {
    return isReactive(value) || isReadonly(value)
}