Vue3响应式原理

一、前言:

写这篇文章是为了总结我在学习Vue3中的一些心得和需要注意的细节,以及为什么要这样做,这样做的原理是什么。本文会以尽量简洁的语言来概述Vue3响应式原理,所以一些边界情况会省略。

Vue版本:V3.2.47

Vue3与Vue2响应式的区别 :

  • Vue2为了兼容大部分的浏览器,使用Object.defineProperty()来实现响应式,Vue3使用proxy
  • Vue2无法直接监听新增属性,删除属性以及直接通过下标修改数组等操作

相关知识:

  • 代理(proxy)与反射 (Reflect)
  • Map与WeakMap的区别
  • 强引用与弱引用
  • 垃圾回收机制
  • Set数据结构
  • TypeScript

二、目标:

实现如下代码的响应式:

<!-- HTML -->
<body>
  <div id="app">
  </div>
  <div id="star">
  </div>
</body>
//script
const user = {
  name:'林夕',
  age:18
}

const star = {
  name:'张三',
  sex:'男',
  age:42
}

document.querySelector('#app').innerText = `${user.name} - ${user.age}`
document.querySelector('#star').innerText = `${star.name} - ${star.sex} - ${star.age}`

setTimeout(() => {
  user.name = '林夕很帅'
  setTimeout(() => {
    star.age = 20
  }, 1000)
}, 2000)

三、分析:

Vue3中使用的是proxy对数据进行拦截处理的,使用reactive来包装引用类型数据实现响应式,watchEffect函数可以监听对象数据的改变,我们来模仿一下Vue3的响应式实现方式。

我们要使用如下方式实现:

文件目录:

 js文件为ts文件编译后生成。注意这里的tsconfig.json中的module要改为'ES2015',同时关闭全局严格模式

 index.html:

<body>
  <div id="app">
  </div>
  <div id="star">
  </div>
</body>
<script type="module">
  import { reactive } from './reactive.js'
  import { watchEffect } from './effect.js'
  const user = reactive({
    name: "林夕",
    age: 18
  })
  const star = reactive({
    name: '张三',
    sex: '男',
    age: 42
  })
  watchEffect(() => {
    document.querySelector('#app').innerText = `${user.name} - ${user.age}`
  })

  watchEffect(() => {
    document.querySelector('#star').innerText = `${star.name} - ${star.sex} - ${star.age}`
  })

  setTimeout(() => {
    user.name = '林夕很帅'
    setTimeout(() => {
      star.age = 20
    }, 1000)
  }, 2000)
</script>

效果:

1.他有如下几个特点:

  1. 当响应式对象的属性改变时,副作用函数可以监听到,并执行内部的函数
  2. 当响应式对象的属性改变时,视图也发生了改变

2.这其中需要用到的数据有:

  1. 响应式对象的属性(key)
  2. 响应式对象(target)
  3. 副作用函数(effect)

3.他们之间的关系:

  1. 一个属性(key)可以对应多个副作用函数(effect)

  2. 一个相同的属性名(例如:name)可能对应多个不同的响应式对象(user、star)

这里的属性key可以理解为副作用函数的依赖dependency),而这里的属性值被用来执行这个作用,因此这个副作用函数也可以说是一个它依赖的订阅者(subscriber)

4.那么我们可以使用下图所示的数据结构

WeakMap<target, Map<key, Set<effect>>>

菱形表示键,矩形表示值

  • 紫色表示响应式对象target的映射,数据类型为WeakMap
  • 蓝色表示响应式对象的属性key的映射,数据类型为Map
  • 绿色表示副作用函数effect集合(副作用函数桶),数据类型为Set
  • 黄色表示副作用函数effect

四、代码实现

1.watchEffect实现

let activeEffect
export const watchEffect= (update: Function) => {
  const effect = function () {
    activeEffect = effect
    update()
    activeEffect = null
  }
  effect()
}

2.reactive实现

export const reactive = <T extends object>(target: T) => {
  return new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      track(target, key)
      return res
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver)
      // 这里的trigger()一定要放到反射之后,因为反射完成后,原始对象才会改变,track则没有这种问题
      trigger(target, key)
      return res
    },
  })
}

这里的track()函数用来收集副作用函数,trigger()函数用来触发副作用函数

3.track实现

let targetMap = new WeakMap()
export const track = (target, key) => {
  // 响应式对象target的映射
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  // 响应式对象target的key的映射
  let deps = depsMap.get(key)
  if (!deps) {
    deps = new Set()
    depsMap.set(key, deps)
  }
  // 若当前运行的副作用函数不为空,则把副作用函数添加到集合内
  if (activeEffect) {
    deps.add(activeEffect)
  }
}

4.trigger实现

export const trigger = (target, key) => {
  const depsMap = targetMap.get(target)
  const deps = depsMap.get(key)
  deps.forEach((effect) => effect())
}

到这里,我们已经完成了一个简单的响应式。但是如果响应式对象的属性若为引用类型,目前是做不到的,这时我们可以考虑使用递归,当获取的值类型为引用类型时,我们可以再次调用reactive函数

5.递归

reactive.ts:

// null也是对象
const isObject = (target) => target != null && typeof target == 'object'

export const reactive = <T extends object>(target: T) => {
  return new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver) as object  //这里使用类型断言
      track(target, key)
      if (isObject(res)) {
        return reactive(res)
      }
      return res
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver)
      // 这里的trigger()一定要放到反射之后,因为反射完成后,原始对象才会改变,track则没有这种问题
      trigger(target, key)

      return res
    },
  })
}

index.html

<body>
  <div id="app">
  </div>
  <div id="star">
  </div>
</body>
<script type="module">
  import { reactive } from './reactive.js'
  import { watchEffect } from './effect.js'
  const user = reactive({
    name: "林夕",
    age: 18
  })
  const star = reactive({
    name: '张三',
    sex: '男',
    info: {
      age: 42,
    }
  })
  watchEffect(() => {
    document.querySelector('#app').innerText = `${user.name} - ${user.age}`
  })

  watchEffect(() => {
    document.querySelector('#star').innerText = `${star.name} - ${star.sex} - ${star.info.age}`
  })

  setTimeout(() => {
    user.name = '林夕很帅'
    setTimeout(() => {
      star.info.age = 20
    }, 1000)
  }, 2000)
</script>

至此,这个响应式已经可以处理大部分场景了

五、思考

1.targetMap为什么使用WeakMap数据结构?

答:

1.WeakMap的键只能是引用类型

2.WeakMap是弱引用,无需手动删除,即可被垃圾回收机制自动回收

2. depsMap的值为什么使用Set数据结构而不使用数组?

答:Set数据结构中的值具有唯一性,天然支持去重

猜你喜欢

转载自blog.csdn.net/Linxi_001/article/details/130742690