堆高车厂家
免费服务热线

Free service

hotline

010-00000000
堆高车厂家
热门搜索:
成功案例
当前位置:首页 > 成功案例

看一看:一张图理清Vue 3.0的响应式系统

发布时间:2022-03-31 17:27:17 阅读: 来源:堆高车厂家
一张图理清Vue 3.0的响应式系统 作者:jrainlau 2019-12-06 10:44:53 开发 前端 本文按照响应式系统的运行过程,划分了”初始化“,”依赖收集“和”响应式“三个阶段,分别阐述了各个阶段所做的事情,应该能够较好地帮助读者理解其核心思路。

本文首发于我的博客:《一张图理清 Vue 3.0 的响应式系统》

随着 Vue 3.0 Pre Alpha 版本的公布,我们得以一窥其源码的实现。Vue 最巧妙的特性之一是其响应式系统,而我们也能够在仓库的 packages/reactivity 模块下找到对应的实现。虽然源码的代码量不多,网上的分析文章也有一堆,但是要想清晰地理解响应式原理的具体实现过程,还是挺费脑筋的事情。经过一天的研究和整理,我把其响应式系统的原理总结成了一张图,而本文也将围绕这张图去讲述具体的实现过程。

文章涉及到的代码我也已经上传到仓库,结合代码阅读本文会更为流畅哦!

一个基本的例子

Vue 3.0 的响应式系统是独立的模块,可以完全脱离 Vue 而使用,所以我们在 clone 了源码下来以后,可以直接在 packages/reactivity 模块下调试。

  1. 在项目根目录运行 yarn dev reactivity,然后进入 packages/reactivity 目录找到产出的 dist/reactivity.global.js 文件。
  2. 新建一个 index.html,写入如下代码:
  1. <scriptsrc="./dist/reactivity.global.js"></script>
  2. <script>
  3. const{reactive,effect}=VueObserver
  4. constorigin={
  5. count:0
  6. }
  7. conststate=reactive(origin)
  8. constfn=()=>{
  9. constcount=state.count
  10. console.log(`setcountto${count}`)
  11. }
  12. effect(fn)
  13. </script>

3. 在浏览器打开该文件,于控制台执行 state.count++,便可看到输出 set count to 1。

在上述的例子中房屋被强拆无法评估了怎么办,我们使用 reactive() 函数把 origin 对象转化成了 Proxy 对象 state;使用 effect() 函数把 fn() 作为响应式回调。当 state.count 发生变化时,便触发了 fn()。接下来我们将以这个例子结合上文的流程图,来讲解这套响应式系统是怎么运行的。

初始化阶段

在初始化阶段,主要做了两件事。

  1. 把 origin 对象转化成响应式的 Proxy 对象 state。
  2. 把函数 fn() 作为一个响应式的 effect 函数。

首先我们来分析第一件事。

大家都知道,Vue 3.0 使用了 Proxy 来代替之前的 Object.defineProperty(),改写了对象的 getter/setter,完成依赖收集和响应触发。但是在这一阶段中,我们暂时先不管它是如何改写对象的 getter/setter 的,这个在后续的依赖收集阶段,会详细说明。为了简单起见,我们可以把这部分的内容浓缩成一个只有两行代码的 reactive() 函数:

  1. exportfunctionreactive(target){
  2. constobserved=newProxy(target,handler)
  3. returnobserved
  4. }

完整代码在 reactive.js。这里的 handler 就是改造 getter/setter 的关键,我们放到后文讲解。

接下来我们分析第二件事。

当一个普通的函数 fn() 被 effect() 包裹之后,就会变成一个响应式的 effect 函数,而 fn() 也会被立即执行一次。

由于在 fn() 里面有引用到 Proxy 对象的属性,所以这一步会触发对象的 getter,从而启动依赖收集。

除此之外,这个 effect 函数也会被压入一个名为activeReactiveEffectStack,(此处为 effectStack)的栈中,供后续依赖收集的时候使用。

来看看代码(完成代码请看 effect.js):

  1. exportfunctioneffect(fn){
  2. //构造一个effect
  3. consteffect=functioneffect(...args){
  4. returnrun(effect,fn,args)
  5. }
  6. //立即执行一次
  7. effect()
  8. returneffect
  9. }
  10. exportfunctionrun(effect,fn,args){
  11. if(effectStack.indexOf(effect)===-1){
  12. try{
  13. //往池子里放入当前effect
  14. effectStack.push(effect)
  15. //立即执行一遍fn()
  16. //fn()执行过程会完成依赖收集,会用到effect
  17. returnfn(...args)
  18. }finally{
  19. //完成依赖收集后从池子中扔掉这个effect
  20. effectStack.pop()
  21. }
  22. }
  23. }

至此,初始化阶段已经完成。接下来就是整个系统最关键的一步——依赖收集阶段。

依赖收集阶段

这个阶段的触发时机,就是在 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 如下:

  1. conststate=reactive({
  2. count:0,
  3. age:18
  4. })
  5. consteffecteffect1=effect(()=>{
  6. console.log('effect1:'+state.count)
  7. })
  8. consteffecteffect2=effect(()=>{
  9. console.log('effect2:'+state.age)
  10. })
  11. consteffecteffect3=effect(()=>{
  12. console.log('effect3:'+state.count,state.age)
  13. })

那么这里的 targetMap 应该为这个样子:

这样,{ target -> key -> dep } 的对应关系就建立起来了,依赖收集也就完成了。代码如下:

  1. exportfunctiontrack(target,operationType,key){
  2. consteffect=effectStack[effectStack.length-1]
  3. if(effect){
  4. letdepsMap=targetMap.get(target)
  5. if(depsMap===void0){
  6. targetMap.set(target,(depsMap=newMap()))
  7. }
  8. letdep=depsMap.get(key)
  9. if(dep===void0){
  10. depsMap.set(key,(dep=newSet()))
  11. }
  12. if(!dep.has(effect)){
  13. dep.add(effect)
  14. }
  15. }
  16. }

弄明白依赖收集表 targetMap 是非常重要的,因为这是整个响应式系统核心中的核心。

响应阶段

回顾上一章节的例子,我们得到了一个 { count: 0, age: 18 } 的 Proxy,并构造了三个 effect。在控制台上看看效果:

效果符合预期,那么它是怎么实现的呢?首先来看看这个阶段的原理图:

当修改对象的某个属性值的时候,会触发对应的 setter。

setter 里面的 trigger() 函数会从依赖收集表里找到当前属性对应的各个 dep,然后把它们推入到 effects 和 computedEffects(计算属性) 队列中,最后通过 scheduleRun() 挨个执行里面的 effect。

由于已经建立了依赖收集表,所以要找到属性所对应的 dep 也就轻而易举了,可以看看具体的代码实现:

  1. exportfunctiontrigger(target,operationType,key){
  2. //取得对应的depsMap
  3. constdepsMap=targetMap.get(target)
  4. if(depsMap===void0){
  5. return
  6. }
  7. //取得对应的各个dep
  8. consteffects=newSet()
  9. if(key!==void0){
  10. constdep=depsMap.get(key)
  11. dep&&dep.forEach(effect=>{
  12. effects.add(effect)
  13. })
  14. }
  15. //简化版scheduleRun,挨个执行effect
  16. effects.forEach(effect=>{
  17. effect()
  18. })
  19. }

这里的代码没有处理诸如数组的 length 被修改的一些特殊情况,感兴趣的读者可以查看 vue-next 对应的源码,或者这篇文章,看看这些情况都是怎么处理的。

至此,响应式阶段完成。

总结

阅读源码的过程充满了挑战性,但同时也常常被 Vue 的一些实现思路给惊艳到,收获良多。本文按照响应式系统的运行过程,划分了初始化,,依赖收集,和响应式,三个阶段,分别阐述了各个阶段所做的事情,应该能够较好地帮助读者理解其核心思路。最后附上文章实例代码的仓库地址,有兴趣的读者可以自行把玩:

tiny-reactive