Vue3 源码分析之 – 手写响应式原理

Computed 函数内部实现。 接收一个有返回值的函数作为参数。这个函数的返回值就是计算属性的值。并且我们要监听这个函数内容使用的响应式数据的变化,最后将这个函数执行的结果返回。

export functon computed(getter){

    // 1. 最终要返回一个 ref创建的具有value属性的对象

    const result = ref();

    // 2. 监听响应式数据的变化。当数据变化后会重新执行effect函数,把getter的结果再存储到result中

    effect(() => (result.value = getter()));

    return result

}
复制代码

so,你肯定会觉得: 这么容易就没了?你在逗我玩,你个菜J。

1.从你用过的reactive开始

reactive

  • 接收一个参数,判断这参数对象是否是Object
  • 创建拦截器对象 handler,设置set/get/deleteProperty 方法
  • return Proxy
export function reactive (target) {

  // 先判断 target 是否是对象,不是对象之间 return 出去

  if (!isObject(target)) return target

  

  // 拦截器对象

  const handler = {

    get (target, key, receiver) {
    // code...
    },

    set (target, key, value, receiver) {
    // code...
    },

    deleteProperty (target, key) {
    // code...
    }

  }

  return new Proxy(target, handler)

}
复制代码

那我们的 get 方法目的是获取 targetkey 的值。 如果 key 值本身还是个 Object 的话,那就得继续往下递归? 来,往下coding。


const convert = target => isObject(target) ? reactive(target) : target;


// 为了后续不再重复书写辅助类工具函数,先一并写上。直接看函数名你也就明白了。

const isObject = val => val !== null && typeof val === 'object'

const convert = target => isObject(target) ? reactive(target) : target

const hasOwnProperty = Object.prototype.hasOwnProperty

const hasOwn = (target, key) => hasOwnProperty.call(target, key);

let targetMap = new WeakMap();

// code ...



get (target, key, receiver) {
      // 收集依赖
      // track(target, key)
      console.log('get:',key);
      // 返回target洪的key的值
      const result = Reflect.get(target, key, receiver);

      return convert(result)

},
复制代码

所以,人人都说递归、闭包、嵌套循环不好。但你无法否认的是:真香……

继续。我们的 set 方法。

set (target, key, value, receiver) {

      const oldValue = Reflect.get(target, key, receiver)

      let result = true

      if (oldValue !== value) {

        result = Reflect.set(target, key, value, receiver)

        // 触发更新

        // trigger(target, key);

        console.log('set key:',key,'value:',value);

      }

      return result

},
复制代码

是的,你没看错,你都 set 值了,当然要触发更新了。 但你以为现在就写 trigger 吗?

? 我们来看 deleteProperty 方法

deleteProperty (target, key) {

      const hadKey = hasOwn(target, key); // 头上辣个辅助函数。 23333.

      const result = Reflect.deleteProperty(target, key)

      if (hadKey && result) {

        // 触发更新

        // trigger(target, key)

        console.log('delete:',key);

      }

      return result

}
复制代码

trigger:

export function trigger (target, key) {

  const depsMap = targetMap.get(target); // 憋问 targetMap 哪来的,最前面写了。

  if (!depsMap) return 

  const dep = depsMap.get(key)

  if (dep) {

    dep.forEach(effect => {

      effect()

    })

  }

}
复制代码

effect & track – 收集依赖

先来看个使用栗子

<!DOCTYPE html>

<html lang="en">

<head>

  <meta charset="UTF-8">

  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Document</title>

</head>

<body>

  <script type="module">

    import { reactive, effect } from './text2.js'

    const product = reactive({

      name: '你大爷',

      price: 100,

      count: 1

    })

    let total = 0 

    effect(() => {

      total = product.price * product.count

    })

    console.log(total) // 100

    product.price = 2000

    console.log(total) // 2000

    product.count = 10

    console.log(total) // 20000

  </script>

</body>

</html>
复制代码

如果栗子中三个 console 的结果是正确的话, effect 究竟做了哪些事情?

  • 首次加载,执行effect内部的箭头函数。 箭头函数中访问了 product 。 product 是 reactive返回的响应式对象,也就是代理对象。当我们访问product. price 属性的时候会触发price属性的get方法。 在get方法中要收集依赖。过程就是存储这个属性和这个回调函数。而属性又跟对象相关,所以在代理对象中的get方法中首先会存储target目标对象。然后是target对象的属性,以及箭头函数。
  • 触发更新时,会根据属性找到对应的函数。
  • 继续收集下一个属性的依赖。
let activeEffect = null

export function effect (callback) {

  activeEffect = callback

  callback() // 访问响应式对象属性,去收集依赖

  activeEffect = null

}
复制代码

(默默吐槽下:和react是真的像,网上那些喷子说的也不是完全没道理。)

Track 方法

let targetMap = new WeakMap()

// 收集依赖

export function track (target, key) {

  if (!activeEffect) return // 如果没有依赖,直接出去

  let depsMap = targetMap.get(target) // 寻找目标对象的依赖

  if (!depsMap) {

    targetMap.set(target, (depsMap = new Map())); // 没找到就新创建一个depsMap

  }

  let dep = depsMap.get(key) // 寻找当前key 的 dep

  if (!dep) {

    depsMap.set(key, (dep = new Set())) // 没找到,添加new Set

  }

  dep.add(activeEffect) 

}



// 然后去 get 方法里收集依赖
复制代码

3 trigger – 触发更新

export function trigger (target, key) {

  const depsMap = targetMap.get(target)

  if (!depsMap) return

  const dep = depsMap.get(key)

  if (dep) {

    dep.forEach(effect => {

      effect()

    })

  }

}
复制代码

看实操吧。。

4. ref

export function ref (raw) {

  // 判断 raw 是否是ref 创建的对象,如果是的话直接返回

  if (isObject(raw) && raw.__v_isRef) {

    return

  }

  let value = convert(raw)

  const r = {

    __v_isRef: true, // 不要问为啥这个属性名非得长这样,Vue3源码里就是长这样

    get value () {
      track(r, 'value')
      return value
    },

    set value (newValue) {
      if (newValue !== value) {
        raw = newValue
        value = convert(raw)
        trigger(r, 'value')
      }
    }
  }

  return r

}
复制代码

5. computed

export function computed (getter) {
  const result = ref()
  effect(() => (result.value = getter()))
  return result
}
复制代码

请你回到文章开头, 此刻还会认为我是个菜J吗?是不是简单到哭?

End

最后来比较一下 reactiveref 的区别

  • ref 可以把基本数据类型转成响应式对象
  • ref 返回的对象,即使重新赋值新的对象,也依然是响应式的
  • reactive 返回的对象,重新赋值会丢失响应式
  • reactive 返回的对象无法解构。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享