Vue响应式原理及源码ref,reactive实现~

响应式实现原理

❄响应式系统的设计原则

01:JS的程序性
let obj = {
    num:5,
    value:2
}
let total = obj.num*obj.value
console.log(total) //10
obj.value = 4
console.log(total) //10
​

此时当我们第二次打印total时,想要的打印结果是20,但是并没有得到想要的结果

因为JS是程序性的,所以想要得到20必须要做一些额外的处理

let obj = {
    num:5,
    value:2
}
let total
const effect = ()=>{
    total = obj.num*obj.value
}
console.log(total) //10
obj.value = 4
effect() 
console.log(total) //20

这样就可以得到想要的结果了,但是每次都需要重新调用一下effect函数才行。

02:Vue2的响应式原理

Vue2通过Object.defineProperty AP来实现响应性的:

let value = 2
let obj = {
    num:5,
    value:value
}
let total
const effect = ()=>{
    total = obj.num*obj.value
}
Object.defineProperty(obj,"value",{
    get(){
        return value
    },
    set(newVal){
        value = newVal 
        effect()
    }
})
console.log(total) //10
obj.value = 4
console.log(total) //20

这样就不需要每次手动调用effectt函数了,这样每次修改value的值,都会触发set修改value的值并且拿到最新的total的值。

由于javascript的限制,导致vue2中响应性的限制:

  1. 当data中没有对应的属性时,向data中新增的属性都是不具备响应性的。
  2. 通过数组下标的形式新增元素时,此时也不具备响应性

因为Object.defineProperty 只能监听指定对象的指定属性,所以再vue中data中没有预先定义的属性是没有响应性的

03:Vue3的响应性Proxy

由于Object.defineProperty 的缺陷,因此Vue3使用Proxy来实现响应性

let obj = {
    num:5,
    value:2
}
let total
const effect = ()=>{
    total = p1.num*p1.value
}
const p1 = new Proxy(obj,{
    get(target, key, receiver){
        return target[key]
    },
    set(target, key, newVal,receiver){
        target[key] = newVal
        effect()
        return true
    }
})
console.log(total) //10
p1.value = 4
console.log(total) //20

如上例子,我们可以总结vue2和vue3响应式的区别

Proxy

  1. proxy将代理一个被代理对象obj,返回 一个代理对象,他代理的是整个对象而不是指定对象的指定属性
  2. 当需要修改属性时,我们可以通过代理对象来修改

Object.defineProperty

  1. 只可以代理指定对象的指定属性,
  2. 当想要修改属性时,通过原对象进行修改

❄ 源码实现——reactive

创建如下测试实例:

<body>
    <div id="app"></div>
  </body>
  <script>
    const { reactive, effect } = Vue
    const obj = reactive({
      name: 'zhangsan'
    })
    effect(() => {
      document.querySelector('#app').innerText = obj.name
    })
    setTimeout(() => {
      obj.name = 'lisi'
    }, 2000)
    console.log(obj)
  </script>

在源码中首先会执行reactive方法,reactive方法中会返回createObjectReactive方法的调用,主要逻辑会这个方法中进行,这个方法接收三个参数:

  1. target对象即调用reactive传的对象,
  2. baseHandlers函数封装好的getset函数,
  3. proxyMap用过缓存处理的Weakmap对象。

接下来我们进行实现:

/**
 * 
 * @param target 代理对象
 * @param mutableHandlers get set函数封装
 * @param reactiveMap 弱引用 map 用过缓存处理
 * 
 */
export function reactive(target: Object) {
  return createObjectReactive(target, mutableHandlers, reactiveMap)
}
​
function createObjectReactive(target, baseHandlers, proxyMap) {
  // 从缓存中读取
  const existingProxy = proxyMap.get(target)
  // 如果已经存在直接返回 无需再创建
  if (existingProxy) {
    return existingProxy
  }
  // 通过Proxy代理传过来的target对象
  const proxy = new Proxy(target, baseHandlers)
  // 标志为一个reactive
  proxy[ReactiveFlag.IS_REACTIVE] = true
  // 缓存处理
  proxyMap.set(target, proxy)
  return proxy
}

baseHandlers函数:

export const createGetter = () => {
  return function get(target: Object, key: any, receiver: Object) {
      //读取代理对象值触发
    const res = Reflect.get(target, key, receiver)
    return res
  }
}
export const createSetter = () => {
  return function set(target: Object, key: any, value: unknown, receiver: Object) {
      //修改代理对象值触发
    const result = Reflect.set(target, key, value, receiver)
    return result
  }
}
const get = createGetter()
const set = createSetter()
​
export const mutableHandlers: ProxyHandler<object> = {
  get, set
}
​

reactive函数执行完毕,接下来会执行到我们测试实例中的effect方法:

源码中会调用effect方法,创建ReactiveEffect实例,ReactiveEffect是一个类,该类中有一个run和stop方法,该类接收一个fn函数,即调用effect传的匿名函数。调用ReactiveEffect的run方法就会调用fn函数.

代理如下:

let activeEffect = nul
export class ReactiveEffect<T = any> {
  constructor(public fn: () => T) {
  }
  run() {
    activeEffect = this
    return this.fn()
  }
  stop() { }
}
// effect  fn函数即 () => {document.querySelector('#app').innerText = obj.name}
export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
  const _effect = new ReactiveEffect(fn)
  if (!options ) {
      //执行fn函数
    _effect.run()
  }
}

调用fn函数以后,因为document.querySelector('#app').innerText = obj.name会读取obj.name即会调用代理对象的get函数,这次触发get函数,和以往不一样,会通过track函数进行依赖收集

代码如下:

export let activeEffect: ReactiveEffect | undefined
/**
 * 收集所有依赖的 WeakMap 实例:
 * 1. `key`:响应性对象
 * 2. `value`:`Map` 对象
 *      1. `key`:响应性对象的指定属性
 *      2. `value`:指定对象的指定属性的 执行函数
 */
export let targetMap = new WeakMap<any, KeyToDepMap>()
//get函数
export const createGetter = () => {
  return function get(target: Object, key: any, receiver: Object) {
    const res = Reflect.get(target, key, receiver)
    // 依赖收集
    track(target, key)
    return res
  }
}
// 收集依赖
export function track(target: object, key: unknown) {
    //当前不存在执行函数 直接return
  if (!activeEffect) {
    return
  }
    //通过target对象获取map对象
  let depsMap = targetMap.get(target)
  if (!depsMap) {
      //获取不到进行初始化
    targetMap.set(target, (depsMap = new Map()))
  }
    //给map对象的指定属性 即target对象的执行属性设置对应的回调函数 建立联系
    depsMap.set(key,activeEffect )
}
​

二秒之后执行setTimeout(() => {obj.name = 'lisi'}, 2000),即会触发代理对象的set函数,这时触发会进行依赖触发即trigger函数

// 触发依赖
export function trigger(target: Object, key: unknown) {
    //获取get时存储的依赖map对象
  const depsMap = targetMap.get(target)
  //没获取到直接return
  if (!depsMap) {
    return
  }
  //通过指定属性名获取对象的activeEffect
  const effect = depsMap.get(key) as ReactiveEffect
  if (!effect) {
    return
  }
   //触发run方法即触发fn函数 获取到最新的值 并修改视图
   //document.querySelector('#app').innerText = obj.name  fn函数
  effect.run()
}

测试实例执行完成,基本的reactive函数已经构建完成

但是此时存在一个问题,每个响应式数据只能处理一个effect的回调,如下:

<body>
  <div id="app">
    <p id="p1"></p>
    <p id="p2"></p>
  </div>
</body><script>
  const { reactive, effect } = Vue
​
  const obj = reactive({
    name: '张三'
  })
​
  // 调用 effect 方法
  effect(() => {
    document.querySelector('#p1').innerText = obj.name
  })
  effect(() => {
    document.querySelector('#p2').innerText = obj.name
  })
​
  setTimeout(() => {
    obj.name = '李四'
  }, 2000);
</script>

要想处理一个响应式数据可以对应多个effect模块,那必须要处理一对多的现象,也就是当我们处理上述desmap的时候存储的activeEffect要变成一个数组。

要想实现,源码中时通过增加一个dep set对象实现的

targetMap的结构

  • key(taget) 响应性对象
  • value map对象
  • key 响应性对象的指定属性
  • value Set对象 存储唯一不会重复
  • reactiveEffect 响应性对象指定属性对应的effect回调函数fn

如下图所示结构:

✑ 代码实现:

//dep 创建set对象模块
export type Dep = Set<ReactiveEffect>
​
export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set<ReactiveEffect>(effects) as Dep
  return dep
}
// 收集依赖
export function track(target: object, key: unknown) {
  if (!activeEffect) {
    return
  }
   //通过target获取对应的map对象
  let depsMap = targetMap.get(target)
  //获取不到则进行存储
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
   //再通过属性名 获取存储多个fn函数的set对象
  let dep = depsMap.get(key)
  //获取不到则进行初始化
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }
    //存储当前activeEffect  即fn函数
  trackEffects(dep)
}
export function trackEffects(dep: Dep) {
  dep.add(activeEffect!)
}
// 触发依赖
export function trigger(target: Object, key: unknown) {
     //通过target获取对应的map对象
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
     //再通过属性名 获取存储多个fn函数的set对象
  const dep: Dep | undefined = depsMap.get(key)
  if (!dep) {
    return
  }
  triggerEffects(dep)
}
export const triggerEffects = (dep: Dep) => {
    //因为是一对多  则进行遍历触发fn函数
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
      triggerEffect(effect)
  }
}
export const triggerEffect = (effect: ReactiveEffect) => {
    effect.run()
}

✑此时,一个响应性对象属性就可以对应多个effect函数了

reactive函数的局限性,对于 reactive 函数而言,它会把传入的 object 作为 proxytarget 参数,而对于 proxy 而言,他只能代理 对象,而不能代理简单数据类型,所以说:我们不可以使用 reactive 函数,构建简单数据类型的响应性

为了构建简单数据类型的响应式Vue是通过ref来实现的

❄ 源码实现——ref

ref处理复杂类型

创建如下测试实例:

<body>
    <div id="app"></div>
    <script>
      const { ref, effect } = Vue
      const obj = ref({
        name: '张三'
      })
      effect(() => {
        document.querySelector('#app').innerText = obj.value.name
      })
      setTimeout(() => {
        obj.value.name = '李四'
      }, 2000)
    </script>
  </body>

首先会进入ref函数,ref函数会返回createRef函数的调用,这个方法中会通过一个isRef方法判断当前传入的值是否已经是一个ref的数据,如果是就直接返回,如果不是会通过一个RefImpl类创建实例,该类主要提供了get valueset value来监听,这也是为什么ref为什么要.value的原因。

✑代码实现:

export function ref(value: unknown) {
  return createRef(value, false)
}
​
export function isRef(r) {
  return !!(r && r.__v_isRef === true)
}
​
class RefImpl<T> {
  private _value: T 
  public dep?: Dep = undefined  //用来进行依赖收集的set对象
  private _rawValue: T
  public readonly __v_isRef = true  //用来判断当前是否为ref数据
  constructor(value: T, shallow: boolean) {
    this._rawValue = value
      //判断当前传过来的数据是否是对象,是对象则用reactive来处理,不是则原值返回
    this._value = toReactive(value)
  }
// .value 会触发
  get value() {
    return this._value
  }
  set value(newValue) {
    if (hasChanged(newValue, this._rawValue)) {
      this._rawValue = newValue
      this._value = toReactive(newValue)
    }
  }
}
/**
 * 创建 RefImpl 实例
 * @param rawValue 原始数据
 * @param shallow boolean 形数据,表示《浅层的响应性(即:只有 .value 是响应性的)》
 * @returns
 */
export function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

toReactive方法

//判断当前传过来的数据是否是对象,是对象则用reactive来处理
export function toReactive(value) {
  return isObject(value) ? reactive(value) : value
}

ref函数执行完毕,接下来执行effect函数,执行逻辑和之前并无差别,但是obj.value.name会触发RefImpl类的get value方法,这个方法首先会进行依赖收集,将当前的ReactiveEffect放入dep Set对象当中,因为他是复杂类型数据,响应性是通过reactive实现的,因此会触发reactive函数的getter进行依赖收集,逻辑与之前一样。

class RefImpl<T> {
  private _value: T
  public dep?: Dep = undefined
  private _rawValue: T
  public readonly __v_isRef = true
  constructor(value: T, shallow: boolean) {
    this._rawValue = value
    this._value = toReactive(value)
  }
  get value() {
      //依赖收集
    trackRefValue(this)
    return this._value
  }
  set value(newValue) {
    if (hasChanged(newValue, this._rawValue)) {
      this._rawValue = newValue
      this._value = toReactive(newValue)
      triggerRefValue(this)
    }
  }
}
export function triggerRefValue(ref) {
  if (ref.dep) {
    triggerEffects(ref.dep)
  }
}
​
​
export function trackRefValue(ref) {
//如果当前activeEffect存在才去收集依赖
  if (activeEffect) {
    trackEffects(ref.dep || (ref.dep = createDep()))
  }
}
//依赖收集
export function trackEffects(dep: Dep) {
  dep.add(activeEffect!)
}
​

effect函数执行完毕,两秒以后执行obj.value.name = '李四'

这个代码可以分解为:

const value = obj.value
value.name = "李四"

由上可知,首先会触发RefImplget value,再次收集依赖,但是此时的activeEffect是之前触发effect函数的fn函数,因为Set对象存储的值唯一不会重复,所以此时不会被收集到dep当中。此时会触发reactive函数的setter触发之前收集的依赖,也就是触发effect函数中的fn函数,视图发生改变变成李四。

ref处理简单数据类型

创建如下测试实例:

<body>
    <div id="app"></div>
    <script>
      const { ref, effect } = Vue
      const obj = ref('张三')
      effect(() => {
        document.querySelector('#app').innerText = obj.value
      })
      setTimeout(() => {
        obj.value = '李四'
      }, 2000)
    </script>
  </body>

首先执行ref函数,创建一个RefImpl实例接着执行effect函数,执行 document.querySelector('#app').innerText = obj.valueobj.value会触发RefImpl的get value进行依赖收集,两秒之后执行obj.value = '李四',触发set value进行依赖触发。

代码实现:

export function ref(value: unknown) {
  return createRef(value, false)
}
​
export function isRef(r) {
  return !!(r && r.__v_isRef === true)
}
class RefImpl<T> {
  private _value: T
  public dep?: Dep = undefined
  private _rawValue: T //保存当前值 触发set的时候用来比较新值和旧值是否相同
  public readonly __v_isRef = true
  constructor(value: T, shallow: boolean) {
    this._rawValue = value
    this._value = toReactive(value)
  }
  get value() {
      //依赖收集
    trackRefValue(this)
    return this._value
  }
  set value(newValue) {
     //判断新值和旧值是否相同
    if (hasChanged(newValue, this._rawValue)) {
        //将新值赋值给旧值
      this._rawValue = newValue
      this._value = toReactive(newValue)
      triggerRefValue(this)
    }
  }
}
export function triggerRefValue(ref) {
  if (ref.dep) {
    triggerEffects(ref.dep)
  }
}
​
//依赖收集
export function trackRefValue(ref) {
  if (activeEffect) {
    trackEffects(ref.dep || (ref.dep = createDep()))
  }
}
//循环触发依赖
export const triggerEffects = (dep: Dep) => {
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
      triggerEffect(effect)
  }
}
//依赖触发
export const triggerEffect = (effect: ReactiveEffect) => {
    effect.run()
}
//hasChanged方法   判断两个值是否相同
export const hasChanged = (newValue, oldValue) => {
  return !Object.is(newValue, oldValue)
}

简单数据类型,不具备数据件监听的概念,即本身并不是响应性的。

只是因为 vue 通过了 set value() 的语法,把 函数调用变成了属性调用的形式,让我们通过主动调用该函数,来完成了一个 “类似于” 响应性的结果。

原文链接:https://juejin.cn/post/7325132195147628570 作者:前端咸鱼sunsy

(0)
上一篇 2024年1月18日 下午4:45
下一篇 2024年1月18日 下午4:55

相关推荐

发表回复

登录后才能评论