likes
comments
collection
share

从官方源码中解析vue3的响应式原理

作者站长头像
站长
· 阅读数 17

前言

学习了前端这么多年,也看许多大大小小的文章,在vue响应式这一块我觉得自己也有了一些理解。之前有几次想要去看vue官方的源码,但无一例外,我都看不懂🤔。 之后我经常会看一些博客,也会学习一些mini-vue的项目,但我总感觉有哪些地方不太对劲。

比如有些博客会说各种各样抽象的名词,什么订阅者watcher啦,什么观察者Observer啦,然后还会有很详细的图来表示它们的关系,这样我们当然可以去理解部分vue的原理,但我个人觉得这些都是非常抽象层面,脱离代码层面以一种抽象的方式讲解,并且正所谓一千个读者就有一千个哈姆雷特,每个人对于它的理解不同,所以我们从这些博客中理解到的极大程度的依赖与写这篇文章的人(maybe那个人还是看了别人的博客写的),而自己最近代码能力有些许提升,于是就又跑去翻了翻vue的源码,发现竟然可以大概看懂一部分了,所以我想写一篇从源码里去带大家分析响应式原理的文章。

除此之外,通过本篇文章你可以收获一份入门源码的信心,也就是说,你可以通过本片文章所讲的内容作为入口去更深入学习vue的源码

ps:以下内容我都会贴上vue中官方仓库的源码片段,并表明我们需要关注的函数或者变量以及源码所在文件夹和行数,以及github地址


ps:以下贴图的代码不需要大家仔细研究,只需要关注我画红圈或者蓝圈的地方以及所在函数的位置即可,重点关注解读部分


仓库结构

在解析源码之前,我们首先要大概讲一下代码结构

路径packages/

从官方源码中解析vue3的响应式原理

我们可以看到,packages里面有非常多的包

  • compiler-*:编译相关的包(可忽略)
  • reactivity-*:响应式相关的包(核心为core)
  • runtime-*:运行时相关的包(核心为core)

不要怕,我们只需要重点关注两个包即可,即reactivity-coreruntime-core

前置内容

在解析之前我们还需要说一些前置知识,vue运行的的响应式是以组件为单位去触发的。就是说,在一个组件内的响应式变量一般都是只影响当前组件的更新渲染的,而组件的更新,则是通过生成虚拟dom,通过diff算法去比对虚拟dom,然后再去更新。

为什么这样做呢?大家可以去想一想,高精度的控制(以单个dom为单位),意味着我们更新的的速度越快,更新越精确,但会损耗大量内存资源占用;而如果是以整个应用为单位更新,那恐怕页面会卡的不行或者卡死。所以组件正好就是一个折中的方案。

针对这一块你可以单独去详细了解,推荐去看一下《深入浅出vue》这本书

源码解读

入口

接下来我不会直接从响应式部分入手,而是通过第一次渲染的流程,来逐步引入大家去了解响应式原理,因为这样有助于大家对vue的全貌有一个基本认识,不是干巴巴的只了解响应式部分。对于入口我不会做大量解说,只是通过入口引入到我们的响应式部分。

路径packages/runtime-core/src/apiCreateApp.ts

functioncreateAppAPI -> createApp

行数326

从官方源码中解析vue3的响应式原理

解读: 这个createAppApi会被runtime-dom这个包引入然后再导出我们经常使用的那个方法,当然不用关注这个。我们可以看到在我们调用mount方法后,vue会先创建一个虚拟dom Vnode,然后会调用render函数渲染这个虚拟dom,那么响应式就是在就是在render里面去绑定的啦。

ps:mount方法会在runtime-dom中被重写,因为在创建vue App应用的时候需要一些额外的操作,不需要关注

render函数

在render函数中其实有很多的子方法,比如mountChildren、patchElement等等,这些其实与diff有很多的关系,但我们不关注这个,我们只需要关注一个函数,那就是mountComponent

ps:我去整个render函数2000行,吐槽一下。。。😂

寻找组件渲染的函数

路径packages/runtime-core/src/renderer.ts

functionbaseCreateRenderer -> mountComponent

行数1218

从官方源码中解析vue3的响应式原理 从官方源码中解析vue3的响应式原理

解读:mountComponet中我们可以看到,首先根据虚拟dom生成一个组件实例,这个阶段组件里面是空空如也的,也没有进行渲染。

接下来我们关注一个函数,setupComponent,这个阶段可以说是对组件进行初始化,在初始化完后肯定要进行渲染啦。

那么就该setupRenderEffect函数登场啦,这个函数可以说是组件响应式绑定的核心了,在这个函数里会引入响应式部分的函数,并且在对组件第一次渲染的时候进行响应式绑定。不着急,让我们一点一点来看。

寻找组件渲染时绑定响应式变量的函数

路径packages/runtime-core/src/renderer.ts

functionsetupRenderEffect

行数1541

从官方源码中解析vue3的响应式原理

解读: 组件更新的函数,这个函数里面会调用我们自己写的组件的render函数,并返回虚拟dom,然后渲染到页面上

从官方源码中解析vue3的响应式原理

解读:这一部分的代码就是核心中的核心了。首先我们可以看到,它new了一个ReactiveEffect,熟悉effect的小伙伴可能知道,effecf是一个副作用函数,可以绑定响应式变量的,我们先不看这个类怎么实现的,我先说这个ReactiveEffect所生成的实例的作用。

ReactiveEffect需要我们传入一个内部带有响应式变量(我们在自己的组件中写的那些响应式变量)的函数,第二个参数是响应式变量更新后所调用的函数,生成实例后并不会立即执行第一个参数传入的函数。而生成的effect实例,会有一个run方法,这个run方法才会真正执行第一个参数传入的参数,也就是内部有响应式变量的函数。

当我们调用run的时候,会发生什么?

我们知道调用run,就会访问到响应式变量,进而触发响应式变量的get函数。我们想一想,响应式变量更新,它怎么就知道要调用哪个函数呢?唯一的答案就是,在我们调用run之前,先把某个我们要更新的函数挂载到一个全局变量上,然后调用run,触发getget函数内部去找这个全局变量,把这个更新函数保存到这个响应式变量可以找到的地方,然后响应式变量更新,触发set,进而找到这个更新函数,再调用这个函数触发更新即可。

这么一说,整条链路是不是就打通了?但我目前讲的响应式部分还是抽象的,接下来我会带你具体看一下,vue的响应式部分是怎么实现的。

响应式部分源码解读

看到这里,我们终于可以进入正题了,最起码你已经知道,组件在渲染的时候是怎么绑定响应式的了,也知道组件怎么通过改变响应式变量更新的了。

effect副作用

上面我们看到了一个类ReactiveEffect,我们来看看这个类是怎么实现的。

ReactiveEffect

路径packages/reactivity/src/effect.ts

functionReactiveEffect

行数100

从官方源码中解析vue3的响应式原理

解读:首先是这个类的传参,这是我们上面所提到的,这个fn就是上面componentUpdatFn

从官方源码中解析vue3的响应式原理

ps:在看这段代码,你只需要关注三个部分,第一个run这个函数的位置,其他两个我圈住的部分,其余的代码不需要关注。

解读:这个run方法就是上面我们提到的,在调用run的时候会把自己挂载到activeEffect上面,这个activeEffect是什么呢

从官方源码中解析vue3的响应式原理

没错就是我上面提到的全局变量,怎么样我没骗你吧。

挂载之后才会触发fn,进而触发响应式变量。那么接下来,我们是不是就应该去看一看,响应式变量的get是怎么写的了,是不是在get里面去找到这个activeEffect呢

响应式变量的get函数

路径packages/reactivity/src/reactive.ts

functioncreateReactiveObject

行数98 and 214

响应式变量声明的方式主要有两种,reactiveref,我们主要看一看reactive

从官方源码中解析vue3的响应式原理

解读:reactive会调用一个creaReactiveObject函数去创建响应式对象

从官方源码中解析vue3的响应式原理

解读:es6的代理大家应该很熟悉了,第一个是我们需要监控的目标对象,第二个是一个handler对象,里面有getset函数

从官方源码中解析vue3的响应式原理

解读:而handler函数是通过mutableHhandles传进来的,让我们看看mutableHhandles这个对象。

路径packages/reactivity/src/baseHandlers.ts

functioncreateGetter -> get

行数94 and 135

从官方源码中解析vue3的响应式原理

从官方源码中解析vue3的响应式原理

从官方源码中解析vue3的响应式原理

从官方源码中解析vue3的响应式原理

解读:我们可以看到mutableHhandlesget函数是通过createGetter创建的,而在createGetter中,我们终于可以看到get函数的全貌的,get函数里面有很多判断,我们都不需要关注,我们只需要关注一个地方,track,顾名思义,跟踪就是指我们要收集这个响应式变量的依赖,而在这个track中又做了哪些事呢?

路径packages/reactivity/src/effect.ts

functiontrack

行数215 and 228

从官方源码中解析vue3的响应式原理

解读:可以看到,在track函数中真的引用了activeEffect这个变量,还记得我们上面说的,在触发响应式变量之前,会在run方法中把自己挂载到这个activeEffect变量上吗。

还有一个地方就是,targetMap,又是什么东西呢?,事实上targetMap也是一个全局变量,它放的是一个Map对象,这个Map保存原始target对象以及对应的Dep Map对象,即(target,depsMap)

那这个dep又是什么玩意呢?

我们知道一个对象有很多属性,不同的属性肯定要有所区分,所以depsMap保存的就是对象的不同属性和这个属性所有的更新函数了。

你可以通俗理解为,这个targetMap就是存放响应式变量和它的更新函数(effect)的地方,可以让响应式变量找到它到底有哪些依赖它的effect函数,从而在更新时调用。

虽然在track函数中引用了activeEffect,但只是做了一个判断而已,我来看看trackEffects这个函数

functiontrackEffects

行数248

从官方源码中解析vue3的响应式原理

解读:可以看到,在trackEffects这个函数中才真正把activeEffect添加到这个响应式变量对应的dep中,以便下次这个响应式变量可以找到这个effect函数。

总结

至此,我们总算打通了响应式变量收集依赖的过程,核心函数就是这个track了。我们接下来再看看响应式变量更新的过程。

响应式变量的set函数

通过上面的get函数的路径我们就可以找到它的set函数,所以重复的地方我就不截图了

路径packages/reactivity/src/baseHandlers.ts

functioncreateSetter -> set

行数193

从官方源码中解析vue3的响应式原理

解读:可以看到,在set函数中,我们要关注一个函数,trigger函数,顾名思义,触发,也就是触发响应式变量所有的effect的调度函数,就是我们最开始在ReactiveEffect传入的第二个函数,已经忘了的小伙伴可以向上翻一翻再看一下。

我们来看看trigger函数里面做了哪些事

路径packages/reactivity/src/effect.ts

functiontrigger

行数267 and 340 and 373

从官方源码中解析vue3的响应式原理

解读:我们可以看到,在trigger函数里面也是先去全局变量targetMap找deps,这一步就是找get函数里面绑定的依赖了。在trigger函数的末尾,会调用triggerEffects这个函数👇👇👇(看下面这个图)

从官方源码中解析vue3的响应式原理

解读:看到这里,基本上就真相大白了。寻找effect,可以看到,在最后会判断有没有scheduler调度函数,也就是ReactiveEffect的第二个函数,如果没有直接执行run方法。

总结

看到这里,整个响应式变量的更新过程也就清晰了,无非主要就是两个全局变量,一个用来放更新用的effect,一个是存储响应式变量依赖的数据中心。

结尾

感谢大家阅读这篇文章,如果有不对的地方欢迎指正👏👏👏。