本文首发于个人博客: 《一张图理清 Vue 3.0 的响应式系统》
随着 Vue 3.0 Pre Alpha 版本的公布,咱们得以一窥其源码的实现。Vue 最巧妙的特性之一是其响应式系统,而咱们也可以在仓库的 packages/reactivity 模块下找到对应的实现。虽然源码的代码量很少,网上的分析文章也有一堆,可是要想清晰地理解响应式原理的具体实现过程,仍是挺费脑筋的事情。通过一天的研究和整理,我把其响应式系统的原理总结成了一张图,而本文也将围绕这张图去讲述具体的实现过程。javascript
文章涉及到的代码我也已经上传到 仓库,结合代码阅读本文会更为流畅哦!
Vue 3.0 的响应式系统是独立的模块,能够彻底脱离 Vue 而使用,因此咱们在 clone 了源码下来之后,能够直接在 packages/reactivity 模块下调试。html
yarn dev reactivity
,而后进入 packages/reactivity
目录找到产出的 dist/reactivity.global.js
文件。新建一个 index.html
,写入以下代码:vue
<script src="./dist/reactivity.global.js"></script> <script> const { reactive, effect } = VueObserver const origin = { count: 0 } const state = reactive(origin) const fn = () => { const count = state.count console.log(`set count to ${count}`) } effect(fn) </script>
state.count++
,即可看到输出 set count to 1
。在上述的例子中,咱们使用 reactive()
函数把 origin
对象转化成了 Proxy 对象 state
;使用 effect()
函数把 fn()
做为响应式回调。当 state.count
发生变化时,便触发了 fn()
。接下来咱们将以这个例子结合上文的流程图,来说解这套响应式系统是怎么运行的。java
在初始化阶段,主要作了两件事。react
origin
对象转化成响应式的 Proxy 对象 state
。fn()
做为一个响应式的 effect 函数。首先咱们来分析第一件事。git
你们都知道,Vue 3.0 使用了 Proxy 来代替以前的 Object.defineProperty()
,改写了对象的 getter/setter,完成依赖收集和响应触发。可是在这一阶段中,咱们暂时先无论它是如何改写对象的 getter/setter 的,这个在后续的”依赖收集阶段“会详细说明。为了简单起见,咱们能够把这部分的内容浓缩成一个只有两行代码的 reactive()
函数:github
export function reactive(target) { const observed = new Proxy(target, handler) return observed }
完整代码在
reactive.js。这里的
handler
就是改造 getter/setter 的关键,咱们放到后文讲解。
接下来咱们分析第二件事。数组
当一个普通的函数 fn()
被 effect()
包裹以后,就会变成一个响应式的 effect 函数,而 fn()
也会被当即执行一次。浏览器
因为在 fn()
里面有引用到 Proxy 对象的属性,因此这一步会触发对象的 getter,从而启动依赖收集。函数
除此以外,这个 effect 函数也会被压入一个名为”activeReactiveEffectStack“(此处为 effectStack)的栈中,供后续依赖收集的时候使用。
来看看代码(完成代码请看 effect.js):
export function effect (fn) { // 构造一个 effect const effect = function effect(...args) { return run(effect, fn, args) } // 当即执行一次 effect() return effect } export function run(effect, fn, args) { if (effectStack.indexOf(effect) === -1) { try { // 往池子里放入当前 effect effectStack.push(effect) // 当即执行一遍 fn() // fn() 执行过程会完成依赖收集,会用到 effect return fn(...args) } finally { // 完成依赖收集后从池子中扔掉这个 effect effectStack.pop() } } }
至此,初始化阶段已经完成。接下来就是整个系统最关键的一步——依赖收集阶段。
这个阶段的触发时机,就是在 effect 被当即执行,其内部的 fn()
触发了 Proxy 对象的 getter 的时候。简单来讲,只要执行到相似 state.count
的语句,就会触发 state 的 getter。
依赖收集阶段最重要的目的,就是创建一份”依赖收集表“,也就是图示的”targetMap"。targetMap 是一个 WeakMap,其 key 值是当前的 Proxy 对象 代理前的对象state
origin
,而 value 则是该对象所对应的 depsMap。
depsMap 是一个 Map,key 值为触发 getter 时的属性值(此处为 count
),而 value 则是触发过该属性值所对应的各个 effect。
仍是有点绕?那么咱们再举个例子。假设有个 Proxy 对象和 effect 以下:
const state = reactive({ count: 0, age: 18 }) const effect1 = effect(() => { console.log('effect1: ' + state.count) }) const effect2 = effect(() => { console.log('effect2: ' + state.age) }) const effect3 = effect(() => { console.log('effect3: ' + state.count, state.age) })
那么这里的 targetMap 应该为这个样子:
这样,{ target -> key -> dep }
的对应关系就创建起来了,依赖收集也就完成了。代码以下:
export function track (target, operationType, key) { const effect = effectStack[effectStack.length - 1] if (effect) { let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (dep === void 0) { depsMap.set(key, (dep = new Set())) } if (!dep.has(effect)) { dep.add(effect) } } }
弄明白依赖收集表 targetMap 是很是重要的,由于这是整个响应式系统核心中的核心。
回顾上一章节的例子,咱们获得了一个 { count: 0, age: 18 }
的 Proxy,并构造了三个 effect。在控制台上看看效果:
效果符合预期,那么它是怎么实现的呢?首先来看看这个阶段的原理图:
当修改对象的某个属性值的时候,会触发对应的 setter。
setter 里面的 trigger() 函数会从依赖收集表里找到当前属性对应的各个 dep,而后把它们推入到 effects
和 computedEffects(计算属性)
队列中,最后经过 scheduleRun()
挨个执行里面的 effect。
因为已经创建了依赖收集表,因此要找到属性所对应的 dep 也就垂手可得了,能够看看具体的代码实现:
export function trigger (target, operationType, key) { // 取得对应的 depsMap const depsMap = targetMap.get(target) if (depsMap === void 0) { return } // 取得对应的各个 dep const effects = new Set() if (key !== void 0) { const dep = depsMap.get(key) dep && dep.forEach(effect => { effects.add(effect) }) } // 简化版 scheduleRun,挨个执行 effect effects.forEach(effect => { effect() }) }
这里的代码没有处理诸如数组的 length 被修改的一些特殊状况,感兴趣的读者能够查看 vue-next 对应的源码,或者 这篇文章,看看这些状况都是怎么处理的。
至此,响应式阶段完成。
阅读源码的过程充满了挑战性,但同时也经常被 Vue 的一些实现思路给惊艳到,收获良多。本文按照响应式系统的运行过程,划分了”初始化“,”依赖收集“和”响应式“三个阶段,分别阐述了各个阶段所作的事情,应该可以较好地帮助读者理解其核心思路。最后附上文章实例代码的仓库地址,有兴趣的读者能够自行把玩: