1.前言
之前总结了一篇博客——《Vue响应式的简单模型》,里面介绍了观察者模式和发布订阅模式,并通过自己实现一个轻量级Vue框架的方式介绍了Vue2.0和3.0响应式的简单原理。
但在自己实现的轻量级Vue框架中,仍然采用“传统”的Options API的方式。我们知道Vue3.0的一个亮点就是引入了Composition API,这篇博客就简单介绍一下Composition API响应式的原理。
和上一篇博客一样,我们先介绍Composition API的核心原理,然后再通过自己实现一个轻量级的Composition API来了解其中的细节。如果你没有看过上一篇博客,推荐先去看一下。此外本篇文章所用到的代码都放到了GitHub上。
2.核心原理
2.1 基本思路
Composition API响应式的实现依靠一个保存有依赖与更新对应关系的WeakMap数据结构。这里的WeakMap数据结构指的就是ES6提供的WeakMap数据结构。依赖就是reactive/ref返回的对象。而computed/watch等都通过一个函数参数消费依赖,这些函数参数就是所谓的更新。Composition API把依赖作为key,更新函数作为value来构建WeakMap,在依赖有变更时在WeakMap中找到对应的更新函数来执行,以此实现响应式。
2.2 依赖和更新对应关系的WeakMap
我们通过一个例子来看这个WeakMap究竟长什么样
const obj = reactive({
name: '特朗普',
info: {
message: '没有人比他更懂',
},
})
const news = computed(() => `${obj.name} 说 ${obj.info.message}`)
// ...其他computed
最后构建出来的WeakMap就长这样:
从上图可以看出,构建WeakMap的过程中创建了如下三个依次嵌套的集合:
当目标对象的某个属性被修改时,就去targetMap中找到目标对象的despMap,再从despMap中找到对应属性的dep,最后执行dep中所有的更新函数。
如果仔细观察上图,就会发现obj.info的值是对象,这个对象也被作为targetMap的key保存,这和reactive的实现逻辑有关,后面会详细介绍。
想要实现上述的逻辑,关键问题有两个:
- 如何构建targetMap:
- 如何监听属性变更
2.3 如何构建targetMap
- reactive/ref返回的可响应对象,其get方法通过Proxy API 进行拦截。当get方法被触发时,可以获取到触发get的目标对象和属性。
- computed/watch等API都通过回调函数的形式消费可响应对象的属性。使用computed/watch等API创建对象时,Composition API会先调用其回调函数,触发所用到属性的get方法。
- 同时,Composition API维护一个单例 activeEffect,Composition API在调用回调函数前将activeEffect赋值为该回调函数,回调函数执行结束后将activeEffect置为null。
- 由于可响应对象的get方法被劫持,在get方法被触发时,调用track函数构建targetMap,将activeEffect所指向的回调函数添加到targetMap对应对象、对应属性的dep中。
2.4 如何监听属性变更
这个问题很好解决,reactive/ref返回的可响应对象,其set方法也通过Proxy API进行拦截。当修改属性时,set方法就会被触发。在set方法中调用trigger函数,从targetMap中找到对应对象、对应属性的dep,执行dep内的回调函数。
3.实现一个轻量级的Composition API
我们还是通过实现一个轻量级的Composition API来了解其中的细节,首先创建一个html文件和js文件
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>轻量级composition-api实现</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./index.js"></script>
</body>
</html>
// ./index.js
import Vue from './src/vue.js'
import {
reactive,
ref,
computed,
} from './reactivity/index.js'
const App = new Vue({
el: '#app',
setup() {
const obj = reactive({
name: '特朗普',
info: {
message: '没有人比他更懂',
},
})
const age = ref(10)
const news = computed(() => `${age.value} 高龄的 ${obj.name} 说 ${obj.info.message}`)
return {
obj,
age,
news,
}
},
render(createElement) {
return createElement(
'div',
[
createElement('span', `${this.$data.news.value}`),
]
)
},
})
setTimeout(() => {
App.$data.obj.name = '懂王'
App.$data.age.value = 80
console.log('news', App.$data.news.value)
}, 2000)
setTimeout(() => {
App.$data.obj.info.message = 'MAGA!!!'
console.log('news', App.$data.news.value)
}, 4000)
setTimeout(() => {
console.log('name', App.$data.obj.name)
console.log('message', App.$data.obj.info.message)
}, 5000)
在index.js中我们创建了一个自己实现的Vue实例,将其挂在到id为app的节点上,接下来实现 ./src/vue.js中的代码
// ./src/vue.js
// effect等同于watch和watchEffect
import { effect } from '../reactivity/effect.js'
class Vue {
constructor(options) {
this.$options = options
this.render = options.render
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 执行setup函数,并保存其返回的对象
this.$data = options.setup()
// 如果数据有变更,刷新视图
effect(() => this.$mount())
}
createElement(tagName, children) {
let element = document.createElement(tagName)
if (Object.prototype.toString.call(children) === '[object Array]') {
children.forEach((child) => {
element.appendChild(child)
})
} else {
element.textContent = children
}
return element
}
$mount() {
const elements = this.render(this.createElement)
this.$el.innerHTML = ''
this.$el.appendChild(elements)
}
}
export default Vue
接下来先处理较为关键的effect.js,因为它涉及到targetMap的构建
// effect.js
// 当前活动的 effect 函数
let activeEffect = null
export function effect(callback) {
activeEffect = callback
// 执行回调函数,会触发所使用数据的get函数
// get函数中又执行了track函数
callback()
// 重置
activeEffect = null
}
// 依赖列表,key是对象,value是map
// map的key的属性,value是set,里面是各个地方收集到的回调
let targetMap = new WeakMap()
// 收集依赖
export function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
// 如果没有,创建 depsMap 并添加到字典中
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
// 如果没有,创建 dep 并添加到字典中
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
// 添加 effect 回调函数
dep.add(activeEffect)
// console.log('targetMap', targetMap)
}
// 触发更新
export function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (!dep) return
// 遍历 dep 集合,执行 effect 回调函数
dep.forEach((callback) => {
callback()
})
}
当定义好 effect/track/trigger之后,就可以编写reactive和ref的逻辑了
// reactive.js
import { track, trigger } from './effect.js'
// 判断val是否是对象
export const isObject = (val) => val !== null && typeof val === 'object'
// 递归处理
export const convert = (target) => (isObject(target) ? reactive(target) : target)
// 判断对象是否存在key属性
export const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key)
export function reactive(target) {
// 不是对象,直接返回
if (!isObject(target)) return target
const handle = {
get(target, key, receiver) {
// 收集依赖
track(target, key)
// console.log('get', target, key)
// 如果key对应的值也是对象,需要再将其转换为响应式对象,用于递归收集下一级的依赖
// 这就是obj.info的值是对象,这个对象也被作为targetMap的key保存的原因
// 如果递归处理对象,则修改obj.info.name无法实现响应
return convert(Reflect.get(target, key, receiver))
},
set(target, key, value, receiver) {
const oldVal = Reflect.get(target, key, receiver)
let result = true
if (oldVal !== value) {
// console.log('set', target, key, value)
result = Reflect.set(target, key, value, receiver)
// 触发更新
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
// 判断 target 中是否有自己的 key 属性
const hadKey = hasOwn(target, key)
// 判断是否删除成功(如果不存在 key 属性,也会返回成功)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
console.log('delete', key)
// 触发更新
trigger(target, key)
}
return result
},
}
return new Proxy(target, handle)
}
ref的逻辑就比较简单了,利用了reactive的已有逻辑
import { convert, isObject } from './reactive.js'
import { track, trigger } from './effect.js'
// 将原始类型转换为响应式对象
export function ref(raw) {
// 判断 raw 是否是 ref 创建的对象,如果是,直接返回
if (isObject(raw) && raw.__v_isRef) return raw
// convert 判断是否是对象,是就调用reactive,不是则直接返回
let value = convert(raw)
const r = {
__v_isRef: true, // 标识,表示该对象是 ref 创建的
get value() {
track(r, 'value')
return value
},
set value(newValue) {
// 判断新旧值是否相等
if (newValue !== value) {
raw = newValue
value = convert(raw)
trigger(r, 'value')
}
},
}
return r
}
computed的逻辑也比较简单,利用了ref和effect的实现
import { ref } from './ref.js'
import { effect } from './effect.js'
export function computed(callback) {
const result = ref()
// 通过 effect 监听响应式数据的变化
// 内部调用 callback 并将结果赋值给 result.value
effect(() => {
result.value = callback()
})
return result
}
Composition API还提供了toRefs方法,这里也一并实现
// 将代理对象转换为ref
const toProxyRef = (proxy, key) => {
return {
get value() {
// proxy 是响应式对象,所以这里不需要收集依赖
return proxy[key]
},
set value(newValue) {
proxy[key] = newValue
},
}
}
export function toRefs(proxy) {
const ret = {}
for (const key in proxy) {
ret[key] = toProxyRef(proxy, key)
}
return ret
}
现在,这个简易版的Composition API就可以在浏览器里看到效果了
4.结束
这里只展示了Composition API响应式最简单的模型,肯定在细节和功能上与源码有很大的差异。但通过自己实现一个简单的Composition API,我们了解了它最基本的原理,下次面试官再问的时候心里就不慌了。