Javascript

Vue3 响应式原理和实现笔记

本文主要是介绍Vue3 响应式原理和实现笔记,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

reactive 的实现

源码位置

const toProxy = new WeakMap()
const toRaw = new WeakMap()

function reactive(target) {
  let observed = toProxy.get(target)
  // 原数据已有相应的响应式数据,直接返回
  if (observed !== void 0) {
    return observed
  }
  // 原数据已经是响应式数据了
  if (toRaw.get(target)) {
    return target
  }
  observed = new Proxy(target, handler)
  // 设置缓存
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  return observed
}

复制代码

toProxytoRaw 主要是缓存 原始数据响应式数据

封装Proxy的handler方法

const isObject = (obj) => obj !== null && typeof obj === 'object'
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (val, key) => hasOwnProperty.call(val, key)

const handler = {
  get: (target, key, receiver) => {
    const res = Reflect.get(target, key, receiver)
    // 收集依赖
    track(target, key)
    // 递归寻找
    return isObject(res) ? reactive(res) : res
  },
  set: (target, key, value, receiver) => {
    const hadKey = hasOwn(target, key)
    const oldValue = target[key]
    const res = Reflect.set(target, key, value, receiver)
    // 触发更新
    if (!hadKey || value !== oldValue) {
      trigger(target, key, oldValue, value)
    }
    return res
  },
}
复制代码
  • 代码 isObject(res) ? reactive(res) : res

由于 proxy 代理的对象只能代理到第一层,当对多层级的对象操作时,set 并不能感知到,但是 get 会触发,需要我们进行递归实现,利用 Reflect.get() 返回的“多层级对象中内层” ,再对“内层数据”做一次代理。

  • 代码!hadKey || oldValue !== value

当输入

const arr = ['a', 'b']
const r = reactive(arr)
r.push('c')

// 结果
// ['a', 'b'] 2 'c'
// ['a', 'b', 'c'] 'length' 3
复制代码

r.push('c') 会触发 set 执行两次,一次是值本身 'c' ,一次是 length 属性设置。 此时的 length 属性其实是属于数组本身的一个属性

为了避免触发多次 trigger,通过判断 key 是否为 target 自身属性,以及设置 value 是否跟 target[key] 相等 可以确定 trigger 的类型,并且避免多余的 trigger

这边只做了逻辑或的判断,在Vue3源码中通过 !hadKeyvalue !== oldValue 分别进行 ADDSET 类型的判断进行不同的操作

tracktrigger 主要的作用于 effect,下面我们分别实现一下 track, triggereffect

注:传送门 Proxy 、Reflect

effect 的实现

源码位置

const effectStack = [] // 存储effect
function effect(fn, options = {}) {
  const reactiveEffect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    reactiveEffect()
  }
  return reactiveEffect
}

function createReactiveEffect(fn, options) {
  const effect = function effect(...args) {
    return run(effect, fn, args)
  }
  effect.deps = []
  effect.computed = options.computed
  effect.lazy = options.lazy
  return effect
}
复制代码

reactive与effect的相结合

function run(effect, fn, args) {
  if (!effectStack.includes(effect)) {
    try {
      // 将effect push到全局数组中
      effectStack.push(effect)
      return fn(...args)
    } finally {
      // 清除已经收集过的effect
      effectStack.pop()
    }
  }
}
复制代码

track 收集依赖

源码位置

// 收集依赖最终数据的结构
targetMap = {
   target: {
    name: [effect], (Set对象)
    age: [effect] (Set对象)
  }
}
复制代码
const targetMap = new WeakMap() // 缓存effect
function track(target, key) {
  const effect = effectStack[effectStack.length - 1]
  if (effect === void 0) {
    return
  }
  let depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 收集依赖时,通过 key 建立一个 Set
  let dep = depsMap.get(key)
  if (dep === void 0) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(effect)) {
    dep.add(effect)
    effect.deps.push(dep)
  }
}

复制代码

首先全局会存在一个 targetMap,它用来建立 数据 -> 依赖 的映射,它是一个 WeakMap 数据结构。

targetMap 通过查找 target,可以获取到 depsMap,它用来存放这个数据对应的所有响应式依赖。

depsMap 的每一项则是一个 Set 数据结构,而这个 Set 就存放着对应 key 的更新函数

trigger 触发更新

源码位置

const targetMap = new WeakMap()
function trigger(target, key, oldValue, newValue) {
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    return
  }
  const effects = new Set()
  const computedRunners = new Set()
  if (key) {
    // 通过 key 找到所有更新函数,依次执行
    let deps = depsMap.get(key)
    deps.forEach((effect) => {
      if (effect.computed) {
        computedRunners.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }
  const run = (effect) => effect()
  effects.forEach(run)
  computedRunners.forEach(run)
}
复制代码

computed的实现

源码位置

当然在我们实现完 effect 以后,computed的实现就显得简单许多了

function computed(fn) {
  const runner = effect(fn, { computed: true, lazy: true })
  return {
    effect: runner,
    get value() {
      return runner()
    },
  }
}
复制代码

完整代码

<!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>
  <div id="app"></div>
  <button id="btn">加1</button>
  <script>
    // utils
    const isObject = (obj) => obj !== null && typeof obj === 'object'
    const hasOwnProperty = Object.prototype.hasOwnProperty
    const hasOwn = (val, key) => hasOwnProperty.call(val, key)

    const toProxy = new WeakMap()
    const toRaw = new WeakMap()
    const handler = {
      get: (target, key, receiver) => {
        const res = Reflect.get(target, key, receiver)
        track(target, key)
        return isObject(res) ? reactive(res) : res
      },
      set: (target, key, value, receiver) => {
        const hadKey = hasOwn(target, key)
        const oldValue = target[key]
        const res = Reflect.set(target, key, value, receiver)
        if (!hadKey || value !== oldValue) {
          trigger(target, key, oldValue, value)
        }
        return res
      },
    }

    function reactive(target) {
      let observed = toProxy.get(target)
      if (observed !== void 0) {
        return observed
      }
      if (toRaw.get(target)) {
        return target
      }
      observed = new Proxy(target, handler)
      toProxy.set(target, observed)
      toRaw.set(observed, target)
      return observed
    }

    const effectStack = []
    const targetMap = new WeakMap() // 缓存effect

    // 依赖收集
    function track(target, key) {
      const effect = effectStack[effectStack.length - 1]
      if (effect === void 0) {
        return
      }
      let depsMap = targetMap.get(target)
      if (depsMap === void 0) {
        targetMap.set(target, (depsMap = new Map()))
      }
      let dep = depsMap.get(key)
      if (dep === void 0) {
        depsMap.set(key, (dep = new Set()))
      }
      if (!dep.has(effect)) {
        dep.add(effect)
        effect.deps.push(dep)
      }
    }

    // 触发更新
    function trigger(target, key, oldValue, newValue) {
      const depsMap = targetMap.get(target)
      if (depsMap === void 0) {
        return
      }
      const effects = new Set()
      const computedRunners = new Set()
      if (key) {
        let deps = depsMap.get(key)
        deps.forEach((effect) => {
          if (effect.computed) {
            computedRunners.add(effect)
          } else {
            effects.add(effect)
          }
        })
      }
      const run = (effect) => effect()
      effects.forEach(run)
      computedRunners.forEach(run)
    }

    // 副作用
    function effect(fn, options = {}) {
      const reactiveEffect = createReactiveEffect(fn, options)
      if (!options.lazy) {
        reactiveEffect()
      }
      return reactiveEffect
    }

    function createReactiveEffect(fn, options) {
      const effect = function effect(...args) {
        return run(effect, fn, args)
      }
      effect.deps = []
      effect.computed = options.computed
      effect.lazy = options.lazy
      return effect
    }

    function run(effect, fn, args) {
      if (!effectStack.includes(effect)) {
        try {
          effectStack.push(effect)
          return fn(...args)
        } finally {
          effectStack.pop()
        }
      }
    }

    function computed(fn, options) {
      const runner = effect(fn, { ...options, computed: true, lazy: true })
      return {
        effect: runner,
        get value() {
          return runner()
        },
      }
    }
  </script>
  <script>
    const app = document.getElementById('app')
    const btn = document.getElementById('btn')
    const obj = reactive({
      name: 'Charles',
      age: 18,
      location: {
        city: 'Guangzhou'
      }
    })

    let double = computed(() => obj.age * 2)
    effect(() => {
      app.innerHTML = `My name is ${obj.name}, ${obj.age} years old, ${double.value}`
    })
    btn.addEventListener('click', () => { obj.age += 1 }, false)
  </script>
</body>

</html>
复制代码

参考地址

Vue3 中的数据侦测

Vue3 的响应式和以前有什么区别,Proxy 无敌?

这篇关于Vue3 响应式原理和实现笔记的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!