前言
本文是 vue3
源码解析系列的第一篇文章,项目代码的整体实现是参考了 v3.2.10 版本,项目整体架构可以参考之前我写过的文章 rollup 实现多模块打包。话不多说,让我们通过一个简单例子开始这个系列的文章。
举个例子
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>reactive</title>
</head>
<body>
<div id="app"></div>
<!--响应式模块的代码-->
<script src="../packages/reactivity/dist/reactivity.global.js"></script>
<script>let { reactive, effect } = VueReactivity;const user = {name: 'Alice',age: 25,address: {city: 'New York',state: 'NY'}};let state = reactive(user)effect(() => {app.innerHTML = state.address.city});setTimeout(() => {state.address.city = 'California'}, 1000);
</script>
</body>
</html>
通过例子可以看到1s之后改变数据视图也跟随变,在 vue3
中是那如何实现这一效果的呢?我们先从例子中的 reactive
函数出发。
实现响应式
有了解过 vue3
源码的知道实现响应式的核心是 proxy
,这里关于 proxy
的方法暂时不做过多的介绍也不是本文的重点所在,这里先简单看下核心函数 reactive
代码实现。
reactive
reactive
函数返回一个对象的响应式代理。并且响应式的转换是“深度”的,它影响所有嵌套属性。一个响应式对象还会深度展开任何是 ref
的属性,同时保持响应式。
import { reactiveHandlers } from './baseHandlers'const reactiveMap = new WeakMap()function reactive (target) {return createReactiveObject(target, reactiveHandlers, reactiveMap)
}function createReactiveObject (target, baseHandlers, proxyMap) {if (!isObject(target)) {return target}// target already has corresponding Proxyconst existingProxy = proxyMap.get(target)if (existingProxy) {return existingProxy}const proxy = new Proxy(target, baseHandlers)// WeakMapproxyMap.set(target, proxy)return proxy
}
需要注意的是:
-
如果一个对象已经被创建成响应式对象了,则直接返回该响应式对象,避免重复代理同一个目标对象。
-
使用
WeakMap
可以避免内存泄漏问题,因为当目标对象target
不再被引用时,其对应的代理对象existingProxy
也会被自动垃圾回收。
reactiveHandlers
reactiveHandlers
是实现响应式数据的关键对象,它通过拦截对象的操作来实现对数据的监听和更新。
const reactiveHandlers = {get,set
}
reactiveHandlers
中包含了一些处理数据的方法,比如 get
和 set
。
get
当我们访问某个响应式对象的属性时,get
方法会被触发,它会对该属性进行依赖收集,将响应式对象和该属性建立联系。
import { isObject } from '@vue/shared'const get = createGetter()function createGetter (isReadonly = false, shallow = false) {return function get (target, key, receiver) {const res = Reflect.get(target, key, receiver)track(target, 'get', key) // 收集依赖// 懒代理if (isObject(res)) {return reactive(res) // 递归}return res}
}
这里会有一个懒代理具体的好处包括:
-
性能优化:代理只会在需要的时候触发,避免了不必要的代理和响应式数据更新,提高了组件渲染的性能。
-
减少不必要的内存开销:只有在需要时才会创建响应式数据对象,避免了不必要的内存开销。
-
更加灵活:懒代理可以更细粒度地控制哪些数据需要进行代理和哪些数据不需要进行代理,可以更加灵活地处理组件状态。
set
当我们修改某个响应式对象的属性时,set
方法会被触发,它会更新该属性的值,并通知所有依赖该属性的组件进行更新。
import { hasChanged } from '@vue/shared'const set = createSetter()function createSetter (shallow = false) {return function set (target, key, value, receiver) {// 注意先读取后设置const oldValue = Reflect.get(target, key)const result = Reflect.set(target, key, value, receiver)// 修改if (hasChanged(value, oldValue)) {// 触发更新trigger(target, 'set', key, value, oldValue)}return result}
}
其中函数的参数 shallow
表示是否进行浅层响应式处理。这里后续会提到。
副作用函数
除了上述响应式相关的代码,还有一个更为重要的方法就是副作用函数 effect
。
effect
effect
函数主要用于创建响应式数据的副作用函数,该函数接受一个函数作为参数。该函数会立即运行一次。其中任何响应式属性被更新时,函数将再次运行。
type ReactiveEffectOptions = {lazy?: booleanscheduler?: (...args: any[]) => any
}function effect (fn, options?: ReactiveEffectOptions) {const _effect = createReactiveEffect(fn, options)if (!options || !options.lazy) {_effect() // 默认执行}return _effect
}
使用 effect
函数的好处是,能够简化响应式数据与副作用之间的代码关系,使得代码更加易于理解和维护。同时,effect
函数还支持配置项:
- lazy:是否延迟执行副作用函数。
- scheduler:指定异步任务调度器,可用于节流、防抖等。
createReactiveEffect
createReactiveEffect
函数的作用是创建一个具有响应式能力的函数。
let uid = 0
let activeEffect // 保存当前的effect
let effectStack = [] // 定义一个栈结构,解决effect嵌套的问题function createReactiveEffect (fn, options) {const effect = function reactiveEffect () {// 保证effect唯一性if (!effectStack.includes(effect)) {try {effectStack.push(effect)activeEffect = effectreturn fn() // 执行用户定义的方法并返回值} finally {effectStack.pop()activeEffect = effectStack[effectStack.length - 1]}}}effect.id = uid++ // 区别effecteffect.fn = fn // 保存用户方法effect.options = options // 保存用户属性return effect
}
在这段代码主要作用是:
-
定义
effect
函数,该函数代表了一个具有响应式能力的函数,并且会在响应式状态发生变化时被调用。 -
在
effect
函数中,通过effectStack
数组来保证effect
函数的唯一性。每次执行effect
函数之前,都会将effect
函数压入effectStack
数组中,以此来判断当前是否已经存在同样的effect
函数。如果不存在,则将当前effect
函数设置为activeEffect
,执行用户定义的方法fn()
并返回结果。这个过程中,使用try...finally
语句块来确保effectStack
数组可以被正确地维护。 -
最后,为
effect
函数添加一些属性,包括id
、fn
和options
,并将effect
函数返回。
总之,这段代码的主要目的是为 vue3
的响应式系统中的副作用函数 effect
创建一个具有响应式能力的版本,并保证了每个 effect
函数的唯一性。
依赖收集与派发更新
在创建完响应式对象之后如何在数据更新的时候重新执行 effect
函数呢?这里就需要用到依赖收集(track
)与派发更新(trigger
)。
track
track
函数作用是追踪响应式对象中属性的访问,并将当前的依赖关系与属性关联。
let targetMap = new WeakMap()function track (target, type, key) {if (activeEffect === undefined) return// 获取effectlet depsMap = targetMap.get(target)if (!depsMap) {targetMap.set(target, (depsMap = new Map()))}let dep = depsMap.get(key)if (!dep) {depsMap.set(key, (dep = new Set()))}if (!dep.has(activeEffect)) {dep.add(activeEffect)}
}
在这段代码主要作用是实现了跟踪(track
)依赖项(target
)和副作用(activeEffect
)之间的关系。
trigger
trigger
函数用于触发一个响应式对象上的更新,它的作用是让 vue3
知道该响应式对象已经进行了变化,从而重新渲染相关视图。
function trigger (target, type, key, newValue?, oldValue?) {const depsMap = targetMap.get(target)if (!depsMap) {// never been trackedreturn}let deps = []// schedule runs for SET | ADD | DELETEif (key !== undefined) {deps.push(depsMap.get(key)) // [Set[activeEffect]]}// 副作用函数const effects = []for (const dep of deps) {if (dep) {effects.push(...dep)}}for (const effect of effects) {effect()}
}
在这段代码主要作用是:
-
函数会从
targetMap
中获取与目标对象关联的依赖关系图depsMap
。如果depsMap
不存在,即目标对象从未被追踪过,那么就直接返回。 -
再根据操作类型和属性名,从
depsMap
中获取与之相关的依赖集合(dep
)。如果dep
存在,就将其加入deps
数组中。最后,通过effects
数组收集所有与deps
集合相关的副作用函数,然后依次调用它们,完成重新渲染视图的工作。
需要注意的是,deps
集合中存储的是 effect
而不是具体的响应式对象。这是因为一个副作用函数可以同时依赖多个响应式对象,因此在触发更新时,需要先从 depsMap
中获取与目标对象相关的 deps
集合,然后再从 dep
集合中获取所有副作用函数,最后统一执行它们。
执行过程
本文所涉及的代码已基本编写完成,为了更好的理解每个函数是如何执行的,我们可以通过 debugger
来调试一下。
初始化
在第一次执行之后以下全局变量对应的值。
reactiveMap
reactiveMap
对应的数据如下图所示。
targetMap
在 effect
函数执行过程中 activeEffect
函数会一直存在,此时依赖收集完之后对应的 targetMap
数据如下图所示。
需要注意的是: effect
执行完之后会从 effectStack
数组中删除栈顶元素,并将数组的最后一个元素作为当前激活的 effect
(即 activeEffect
)。这通常在处理依赖项收集时使用,当一个 effect
被触发时,它会将自己推入 effectStack
中,当它完成执行后,会从 effectStack
中弹出自己,然后 activeEffect
变为新的栈顶元素。这样做可以保证在 effect
执行期间,新的 effect
触发时不会干扰当前正在执行的 effect
。
数据更新时
触发 get
当访问 state.address
时又会触发 get
,同时也会触发 track
,由于此时 effect
已经执行完当前没有激活状态下的 effect
,所以此处不会再次进行依赖收集。在触发 get
时对应的数据如下图所示。
需要注意的是:通过 Reflect.get
返回的 res
是一个对象此时又会触发 reactive
但此时的返回值已经存在于 reactiveMap
中,所以不会重复进行响应式的处理。
触发 set
执行 address.city = 'California'
会触发 set
,此时新值和旧值不一样会触发 trigger
。在触发 set
时对应的数据如下图所示。
触发更新
触发 trigger
时会从 targetMap
的子项 depsMap
中获取对应的 effect
函数并执行。
再次执行 effect
由于数据的改变 effect
函数会被再一次触发,再次访问 state.address
同样会触发 get
与第一次不同的是此时的数据已经被更新为最新的值。与此同时 activeEffect
也会被再一次赋值,此时对应的数据如下图所示。
由于第一次访问的时候对应的值已经被缓存,再次访问的时候就不会重复进行依赖收集和响应式的处理,而是直接返回最新的值。
总结
综上所述,vue3
的响应式原理通过使用 Proxy
对象实现数据代理,结合副作用函数和依赖追踪,实现了高效的数据变化追踪和自动更新机制。这种设计使得 vue3
在处理数据和视图之间的关系时更加灵活和高效。