Skip to content

vue3 响应式原理

实现一个简单的响应式系统

html
<div>
  <h1 id="h1El"></h1>
</div>
<script>
  const h1El = document.getElementById('h1El')

  const effects = new WeakMap()

  const proxyObj = (o) => {
    return new Proxy(o, {
      get(target, key) {
        track(target, render)
        return target[key]
      },
      set(target, key, value) {
        target[key] = value
        trigger(target)
      },
    })
  }

  const obj = proxyObj({
    text: 'hello world',
  })

  function render() {
    h1El.innerText = obj.text
  }

  render()

  function track(target, fn) {
    console.log('追踪依赖,收集副作用')
    effects.set(target, fn)
  }
  function trigger(target) {
    console.log('触发副作用')
    effects.get(target)?.()
  }

  setTimeout(() => {
    obj.text = 'hello vue'
  }, 1000)
</script>

实现较完善的响应式系统

reactive.js

js
const bucket = new WeakMap()

let activeEffect
function track(target, key) {
  if (!activeEffect) return
  const depsMap = bucket.get(target) || new Map()
  const deps = depsMap.get(key) || new Set()
  deps.add(activeEffect)
  depsMap.set(key, deps)
  bucket.set(target, depsMap)
  activeEffect.deps.push(deps)
}

const queueJob = new Set()
const p = Promise.resolve()
let isFlushing = false

function trigger(target, key) {
  const deps = bucket.get(target)?.get(key)
  if (!deps?.size) return
  // 这里需要一个新set,避免遍历中 delete add 后,出现重新访问,出现无限循环现象
  for (const dep of new Set(deps)) {
    queueJob.add(dep)
  }

  if (isFlushing) return
  isFlushing = true
  p.then(() => {
    queueJob.forEach((job) => job())
  }).finally(() => {
    isFlushing = false
    queueJob.clear()
  })
}

export const reactive = (o) => {
  return new Proxy(o, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key)
      return true
    },
  })
}

export const watchEffect = (fn) => {
  const effectFn = () => {
    // 由于执行副作用函数,会进行依赖收集,所以将其从相关依赖中移除,重新收集
    // 避免由分支条件的切换,引起历史依赖遗留
    activeEffect = effectFn
    effectFn.deps.forEach((deps) => {
      deps.delete(effectFn)
    })
    effectFn.deps.length = 0
    fn() // 触发 track
  }
  effectFn.deps = []
  effectFn()
}

test.js

js
//
import { reactive, watchEffect } from './reactive.js'
const h1El = document.getElementById('h1El')

const obj = reactive({
  ok: true,
  text: 'hello world',
})

watchEffect(() => {
  // console.log("1", obj.ok);
  console.log('watchEffect...')
  h1El.innerText = obj.ok ? obj.text : 'not'
  // console.log("watchEffect..");
})

watchEffect(() => {
  console.log('watchEffect2', obj.text)
})

setTimeout(() => {
  obj.ok = false
  setTimeout(() => {
    console.log('更新分支')
    obj.text = 'hello vue3'
  }, 1000)
}, 1000)

html

html
<div>
  <h1 id="h1El"></h1>
</div>
<script type="module" src="./test.js"></script>

Released under the MIT License.