Vue3-手写响应式

170 阅读2分钟

Vue3题目

vue3已经使用有一段时间了,偶然间看到一个题目:

<div id="app"></div>

<script>
  const $app = document.querySelector('#app')

  let state = {
    text: 'hello, world'
  }

  function effect() {
    $app.innerText = state.text
  }

  effect()

  setTimeout(() => {
    // 2秒后希望app的内容变成hello Vue3
    state.text = 'hello Vue3'
  }, 2000)
</script>

A:思路很简单,拦截state对象,在对text取值的时候,收集effect函数依赖,然后text赋值的时候,把收集的effect遍历执行就行。
Q:别口嗨,写出代码。

源码实现

第一版,核心思路两步:

  1. 收集依赖(effect函数)
  2. 触发依赖
const $app = document.querySelector('#app')
const dep = new Set()

const state = new Proxy({text: 'hello world'}, {
  get (target, key) {
    dep.add(effect)
    return target[key]
  },
  set (target, key, newValue) {
    target[key] = newValue
    dep.forEach((fn) => fn())
  }
})

function effect () {
  $app.innerText = state.text
}

effect()
setTimeout(() => {
  state.text = 'hello Vue3'
}, 2000)

点评

Q:功能是实现了,但是这里收集依赖是写死的effect函数名,只要稍微变动一下题目,就不行了。

<div id="container">
  <div id="app1"></div>
  <div id="app2"></div>
</div>

const $app1 = document.querySelector('#app1')
const $app2 = document.querySelector('#app2')

const state = { 
  text: 'hello world', 
  text2: 'hello world2' 
}
// 改变app1的值
function effect1() {
  console.log('执行了effect')
  $app1.innerText = state.text
}
// 改变app2的值
function effect2() {
  console.log('执行了effect2')
  $app2.innerText = state.text2
}
// 2秒钟之后两个div的值要分别改变
setTimeout(() => {
  state.text = 'hello Vue3'
  state.text2 = 'hello Vue3-2'
}, 2000)

支持多属性响应式的修改和注册

A:大意了,应该把effect依赖函数通过某种机制主动注册,这样无论是匿名函数或者具名函数都会一视同仁。

const $app1 = document.querySelector('#app1')
const $app2 = document.querySelector('#app2')
const dep = new Set()
let activeEffect

const effect = function (fn) {
  activeEffect = fn
  fn()
}

const state = new Proxy({text: 'hello world', text2: 'hello world2'}, {
  get (target, key) {
    dep.add(activeEffect)
    return target[key]
  },
  set (target, key, newValue) {
    target[key] = newValue
    dep.forEach((fn) => fn())
  }
})

effect(function effect1 () {
  $app1.innerText = state.text
})

effect(function effect2 () {
  $app2.innerText = state.text2
})
setTimeout(() => {
  state.text = 'hello Vue3'
  state.text2 = 'hello Vue3-2'
}, 2000)

点评

Q:不错,思路灵活,变通很快嘛!不过还有一个问题,给state上增加一个之前不存在的属性,你的dep却会把收集到的依赖执行一次,是不是有点浪费?能否做到efect中依赖了state中的什么值,其值改变了回调才被执行?

重新设置数据结构

A:苦思冥想,终于明白了第二个版本问题所在:没有在effect函数与被操作的目标字段之间建立明确的联系。

const state = new Proxy({text: 'hello world'}, {
  get (target, key) {
    // 无论state什么属性被读取,都会执行get然后收集进dep
    dep.add(effect)
    return target[key]
  },
  set (target, key, newValue) {
    target[key] = newValue
    // 无论state上什么属性被改变,都会触发set,进而收集的依赖被执行
    dep.forEach((fn) => fn())
  }
})

故思考一番,可以用树状结构:

state
    |__key
       |__effectFn

画一个数据结构图来理解一下存储关系: 存储关系

源码实现

const $app = document.querySelector('#app')
const dep = new WeakMap()
let activeEffect

const effect = function (fn) {
  activeEffect = fn
  fn()
}

const state = new Proxy({text: 'hello world', age: 30}, {
  get (target, key) {
    // activeEffect无值,意味着没有执行effect,无法收集依赖
    if (!activeEffect) {
      return
    }
    // 每个target在dep中都是一个Map类型: key => effects
    let depsMap = dep.get(target)
    // 第一次先创建联系
    if (!depsMap) {
      dep.set(target, (depsMap = new Map()))
    }
    // 根据当前读取的key,尝试读取key的effects函数
    let deps = depsMap.get(key)
    if (!deps) {
      depsMap.set(key, (deps = new Set()))
    }
    // 将激活的effectFn存进依赖管理中心
    deps.add(activeEffect)
    return target[key]
  },
  set (target, key, newValue) {
    target[key] = newValue
    // 读取dep
    const depsMap = dep.get(target)
    if (!depsMap) {
      return
    }
    // 读取依赖当前key的effects
    const effects = depsMap.get(key)
    effects && effects.forEach((fn) => fn())
  }
})

effect(() => {
  $app.innerText = `${state.text}, are you ${state.age} years old?`
})

setTimeout(() => {
  state.text = 'hello Vue3'
  state.age = 18
}, 2000)

点评

佩服

Q:能进一步吗,这个只能对state一个对象进行响应式处理,能再封装一下,像Vue3中reactive一样使用?
A:...

reactive抽象

const $app = document.querySelector('#app')
const dep = new WeakMap()
let activeEffect

const effect = function (fn) {
  activeEffect = fn
  fn()
}

// 依赖收集
function track (target, key) {
  // activeEffect无值,意味着没有执行effect,无法收集依赖
  if (!activeEffect) {
    return
  }
  // 每个target在dep中都是一个Map类型: key => effects
  let depsMap = dep.get(target)
  // 第一次先创建联系
  if (!depsMap) {
    dep.set(target, (depsMap = new Map()))
  }
  // 根据当前读取的key,尝试读取key的effects函数
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  // 将激活的effectFn存进依赖管理中心
  deps.add(activeEffect)
}

// 依赖触发
function trigger (target, key) {
  // 读取dep
  const depsMap = dep.get(target)
  if (!depsMap) {
    return
  }
  // 读取依赖当前key的effects
  const effects = depsMap.get(key)
  effects && effects.forEach((fn) => fn())
}

function reactive (state) {
  return new Proxy(state, {
    get (target, key) {
      track(target, key)
      return target[key]
    },
    set (target, key, newValue) {
      target[key] = newValue
      trigger(target, key)
    }
  })
}

const state = reactive({
  name: '张三',
  age: 30
})

effect(() => {
  $app.innerText = `${state.name}, are you ${state.age} years old?`
})

setTimeout(() => {
  state.name = 'hello Vue3'
  state.age = 18
}, 2000)

点评

优秀