├── docs
├── miniVue
│ ├── README.md
│ └── notes
│ │ ├── runtime-core
│ │ └── 01_initComponent.md
│ │ ├── README.md
│ │ ├── prerequisites.md
│ │ └── reactivity
│ │ ├── 09_isProxy.md
│ │ ├── 11_isRef & unRef.md
│ │ ├── 02_runner.md
│ │ ├── 12_proxyRefs.md
│ │ ├── 03_scheduler.md
│ │ ├── 08_shallowReadonly.md
│ │ ├── 07_nestedReactiveAndReadonly.md
│ │ ├── 06_reactiveOrReadonly.md
│ │ ├── 13_computed.md
│ │ ├── 05_readonly.md
│ │ ├── 01_reactive.md
│ │ ├── 04_stop.md
│ │ └── 10_ref.md
├── .vuepress
│ ├── .temp
│ │ ├── styles
│ │ │ ├── index.scss
│ │ │ └── palette.scss
│ │ ├── pages
│ │ │ ├── 404.html.vue
│ │ │ ├── miniVue
│ │ │ │ ├── index.html.vue
│ │ │ │ ├── notes
│ │ │ │ │ ├── index.html.vue
│ │ │ │ │ ├── index.html.js
│ │ │ │ │ └── reactivity
│ │ │ │ │ │ ├── reactive.html.js
│ │ │ │ │ │ └── reactive.html.vue
│ │ │ │ └── index.html.js
│ │ │ ├── index.html.vue
│ │ │ ├── 404.html.js
│ │ │ └── index.html.js
│ │ ├── vite-root
│ │ │ └── index.html
│ │ └── internal
│ │ │ ├── layoutComponents.js
│ │ │ ├── siteData.js
│ │ │ ├── themeData.js
│ │ │ ├── pagesRoutes.js
│ │ │ ├── pagesData.js
│ │ │ └── pagesComponents.js
│ ├── styles
│ │ └── index.styl
│ ├── public
│ │ ├── images
│ │ │ └── logo.svg
│ │ └── themes
│ │ │ ├── index.js
│ │ │ └── layouts
│ │ │ └── Layout.vue
│ └── config.js
└── README.md
├── .npmrc
├── _config.yml
├── .czrc
├── .commitlintrc.yml
├── .huskyrc.yml
├── .gitignore
├── .idea
├── .gitignore
├── vcs.xml
├── modules.xml
└── vue3-study.iml
├── src
├── reactivity
│ ├── dep.ts
│ ├── __tests__
│ │ ├── shallowReadonly.spec.ts
│ │ ├── readonly.spec.ts
│ │ ├── reactive.spec.ts
│ │ ├── computed.spec.ts
│ │ ├── ref.spec.ts
│ │ └── effect.spec.ts
│ ├── computed.ts
│ ├── reactive.ts
│ ├── baseHandlers.ts
│ ├── ref.ts
│ └── effect.ts
├── index.ts
├── runtime-core
│ ├── index.ts
│ ├── h.ts
│ ├── vnode.ts
│ ├── createApp.ts
│ ├── componentPublicInstance.ts
│ ├── component.ts
│ └── renderer.ts
└── shared
│ └── index.ts
├── babel.config.js
├── .lintstagedrc.yml
├── example
└── helloWorld
│ ├── index.js
│ ├── main.js
│ └── index.html
├── README.md
├── deploy.sh
├── rollup.config.js
├── package.json
├── .github
└── workflows
│ └── blank.yml
├── lib
├── mini-vue.esm.js
└── mini-vue.cjs.js
└── tsconfig.json
/docs/miniVue/README.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/docs/.vuepress/.temp/styles/index.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/.vuepress/.temp/styles/palette.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.czrc:
--------------------------------------------------------------------------------
1 | { "path": "cz-conventional-changelog" }
--------------------------------------------------------------------------------
/docs/.vuepress/.temp/pages/404.html.vue:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.commitlintrc.yml:
--------------------------------------------------------------------------------
1 | extends:
2 | - '@commitlint/config-conventional'
3 |
--------------------------------------------------------------------------------
/docs/.vuepress/.temp/pages/miniVue/index.html.vue:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.huskyrc.yml:
--------------------------------------------------------------------------------
1 | hooks:
2 | pre-commit: lint-staged
3 | commit-msg: 'commitlint -E HUSKY_GIT_PARAMS'
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | docs/.vuepress/dist
3 | yarn-error.log
4 | docs/.vuepress/.temp
5 | docs/.vuepress/.cache
6 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/docs/.vuepress/styles/index.styl:
--------------------------------------------------------------------------------
1 | #valine-vuepress-comment {
2 | max-width: 740px;
3 | margin: 0 auto;
4 | padding: 2rem 2.5rem;
5 | }
6 |
--------------------------------------------------------------------------------
/src/reactivity/dep.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 存储effects对象
3 | * @param effects
4 | */
5 | export function createDep(effects) {
6 | return new Set(effects)
7 | }
8 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ["@babel/preset-env", { targets: { node: "current" } }],
4 | "@babel/preset-typescript",
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/.lintstagedrc.yml:
--------------------------------------------------------------------------------
1 | '**/*.{js, jsx, vue}':
2 | - 'eslint --fix'
3 | - 'git add'
4 | '**/*.{less, md}':
5 | - 'prettier --write'
6 | - 'git add'
7 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
学习vue3源码的笔记。参考崔大的 mini-vue
每篇笔记会对应一个提交分支,分支号会标记在笔记中,有兴趣的小伙伴可以切到对应分支配合笔记一起食用。
4 |用于记录Vue3源码学习的笔记, 参考崔大的 mini-vue
每篇笔记会对应一个提交分支,分支号会标记在笔记中,有兴趣的小伙伴可以切到对应分支配合笔记一起食用。
11 |
136 |
137 | TIP
10 |本篇笔记对应的分支号为: main分支:e8bb112
在 Vue3 中,reactive
我们先来看看 副本 这个部分。在实现 reactive 方法之前,我们先来写下它的测试用例,看看它需要做些啥:
// src/reactivity/__tests__/reactive.spec.ts
18 |
19 | describe('reactive', () => {
20 | it('happy path', () => {
21 | const origin = { num: 0 }
22 | // 通过 reactive 创建响应式对象
23 | const reactiveData = reactive(origin)
24 | // 判断响应式对象与原对象不是同一个对象
25 | expect(reactiveData).not.toBe(origin)
26 | // 代理对象中的 num 值应与原对象中的相同
27 | expect(reactiveData.num).toBe(0)
28 | })
29 | })
30 | reactive通过测试用例我们不难发现,其实 reactive 做的事情很简单,就是创建一个对象副本,那这个 副本 该怎么创建呢?答案是使用 Proxy
// src/reactivity/reactive.ts
37 |
38 | export const reactive = (raw) => {
39 | return new Proxy(raw, {
40 | // 取值
41 | get(target, key) {
42 | const res = Reflect.get(target, key)
43 | return res
44 | },
45 | // 赋值
46 | set(target, key, value) {
47 | const res = Reflect.set(target, key, value)
48 | return res
49 | }
50 | })
51 | }
52 | 现在我们已经可以通过 reactive 方法获取目标对象的 副本 了,那 响应式 部分又该如何实现呢?
所谓 响应式, 其实本质上就做了两件事情:
57 |58 |63 |59 |
62 |- 在读取对象属性时进行
60 |依赖收集- 在修改对象属性时执行
61 |依赖触发
而这部分的逻辑则交由 effect 模块来实现。那 依赖收集 跟 依赖触发 具体是怎样的一个流程呢?请看下图:

对上图的内容简单描述如下:
66 |67 |72 |68 |
71 |- 在读取响应式对象
69 |Target中的属性时进行依赖收集操作,所有的依赖会被收集到依赖池TargetMap中;- 在设置响应式对象
70 |Target的属性值时执行依赖触发操作,会根据对应的Target以及key将依赖从依赖池TargetMap中取出并执行。
现在我们已经知道了 effect 模块所要实现的功能,依据上述内容,先来编写下测试用例:
// src/reactivity/__test__/effect.spec.ts
76 |
77 | describe('effect', () => {
78 | it('happy path', () => {
79 | // 创建响应式对象
80 | const user = reactive({
81 | age: 10
82 | })
83 | let nextAge
84 | effect(() => {
85 | nextAge = user.age + 1
86 | })
87 | // 传入 effect 的方法会被立即执行一次
88 | expect(nextAge).toBe(11)
89 | // 修改响应式对象的属性值
90 | user.age++
91 | // 传入 effect 的方法会再次被执行
92 | expect(nextAge).toBe(12)
93 | })
94 | })
95 | effect接下来我们需要实现 effect 模块的功能。
根据上面的描述,effect 接受一个函数作为参数,既如此先定义一下 effect 方法:
// src/reactivity/effect.ts
103 |
104 | export function effect(fn) {}
105 | 接下来,我们需要定义依赖池 targetMap 用于存放依赖。依赖池中存放的是响应式对象 target 所对应的依赖,需要使用对象类型作 key 的话,那么使用 Map
// src/reactivity/effect.ts
111 |
112 | const targetMap = new Map()
113 |
114 | export function effect(fn) {}
115 | 好了,现在存放依赖的地方有了,那么我们就开始收集它们吧~
118 |上文中我们提到,收集依赖 的操作是在读取响应式对象 target 中的属性时进行的。还记得 target 对象是通过 Proxy 创建出来的么?在读取 target 的属性时,必然会触发 get 方法,那么 收集依赖 的操作也应该在 get 方法中进行。
我们先来定义一个方法 tarck 用于依赖收集,并在 reactive.ts 中引入它,以便在 get 方法中进行调用:
// src/reactivity/effect.ts
123 |
124 | const targetMap = new Map()
125 |
126 | /**
127 | * 收集依赖
128 | * @param target 需要收集依赖的对象
129 | * @param key 收集该key所对应的依赖
130 | */
131 | export function track(target, key) {
132 | }
133 |
134 | export function effect(fn) {}
135 |
138 | // src/reactivity/reactive.ts
139 |
140 | import { track } from './effect'
141 |
142 | export const reactive = (raw) => {
143 | return new Proxy(raw, {
144 | // 取值
145 | get(target, key) {
146 | const res = Reflect.get(target, key)
147 | // 收集依赖
148 | track(target, key)
149 | return res
150 | },
151 | // 赋值
152 | set(target, key, value) {
153 | const res = Reflect.set(target, key, value)
154 | return res
155 | }
156 | })
157 | }
158 | 接下来,我们需要实现 track 这部分的功能。在动手实现之前,我们先来捋一捋 track 需要做哪些事情:
162 |177 |163 |
176 |- 由于在初始化时依赖池是空的(也为了避免覆盖),所以在存入
169 |targetMap依赖池之前,需要先判断依赖池中是否已经存在target所对应的依赖容器depsMap: 164 |165 |
168 |- 如果存在,则取出
166 |depsMap;- 否则新建一个
167 |depsMap, 并将其存入到依赖池targetMap中;- 从依赖容器
175 |depsMap中取出响应式对象target对应属性的依赖deps,由步骤1可知,depsMap可能是空的,因此也需要对deps进行判空处理: 170 |171 |
174 |- 如果存在,则取出,并将依赖存入
172 |- 如果不存在,则新建一个
173 |deps,将依赖存入其中,并将deps存入对应属性的依赖容器depsMap中。为了避免重复收集依赖,此处使用 Set进行存储。
为了方便理解,我们来一起看下流程图:
178 |
代码实现如下:
180 |// src/reactivity/effect.ts
183 |
184 | const targetMap = new Map()
185 |
186 | /**
187 | * 收集依赖
188 | * @param target 需要收集依赖的对象
189 | * @param key 收集该key所对应的依赖
190 | */
191 | export function track(target, key) {
192 | // 查找该对象对应的依赖池
193 | let depsMap = targetMap.get(target)
194 | // 如果没有(首次初始化时),则创建新的依赖池
195 | if (!depsMap) {
196 | depsMap = new Map()
197 | targetMap.set(target, depsMap)
198 | }
199 | // 从获取到的依赖池中获取该key所对应的依赖列表
200 | let deps = depsMap.get(key)
201 | // 如果没有,则新建一个该key对应的列表
202 | if (!deps) {
203 | deps = new Set()
204 | depsMap.set(key, deps)
205 | }
206 | // TODO 将依赖对象保存到列表中
207 | }
208 |
209 | export function effect(fn) {}
210 | 好,代码写到这里的时候,我们遇到了一个
213 |问题:
214 |需要被收集的依赖在 effect 方法中,在 tarck 里要怎么获取到这个依赖呢?
针对这个问题,我们可以通过定义一个用于存储依赖的全局变量 activeEffect 来解决解决这个问题。那我们直接把依赖塞到 activeEffect 中就完事儿了么?当然。。。。

不是!如果只单单为了实现这个功能,无可厚非,但是后续我们还有其他操作(为了代码的健壮性,可读性, 可扩展性),这里我们定义 ReactiveEffect 类将依赖收集起来,之后将该类的实例赋值给 activeEffect 即可:
// src/reactivity/effect.ts
222 |
223 | let activeEffect
224 |
225 | class ReactiveEffect {
226 | private _fn: any
227 |
228 | constructor(fn) {
229 | this._fn = fn
230 | }
231 |
232 | run() {
233 | activeEffect = this
234 | this._fn()
235 | }
236 | }
237 |
238 | const targetMap = new Map()
239 |
240 | /**
241 | * 收集依赖
242 | * @param target 需要收集依赖的对象
243 | * @param key 收集该key所对应的依赖
244 | */
245 | export function track(target, key) {
246 | // 查找该对象对应的依赖池
247 | let depsMap = targetMap.get(target)
248 | // 如果没有(首次初始化时),则创建新的依赖池
249 | if (!depsMap) {
250 | depsMap = new Map()
251 | targetMap.set(target, depsMap)
252 | }
253 | // 从获取到的依赖池中获取该key所对应的依赖列表
254 | let deps = depsMap.get(key)
255 | // 如果没有,则新建一个该key对应的列表
256 | if (!deps) {
257 | deps = new Set()
258 | depsMap.set(key, deps)
259 | }
260 | // 将依赖对象保存到列表中
261 | deps.add(activeEffect)
262 | }
263 |
264 | export function effect(fn) {
265 | // 实例化 ReactiveEffect 类,并将依赖传入
266 | const _effect = new ReactiveEffect(fn)
267 |
268 | _effect.run()
269 | }
270 | 注意
273 |这里需要注意的是,传入 effect 中的方法会被立即执行一次(可以回看上述测试用例中的 第14行代码)。所以 ReactiveEffect 暴露的 run 方法中除了要将依赖存入全局变量 activeEffect 中,还得将传入的依赖返回出来用以执行。
到目前为止,依赖收集 的功能就已经实现了。接下来便轮到 依赖触发 了。相较于 依赖收集,依赖触发 就简单了,只需要根据传入的 target 以及对应的属性 key,将依赖项取出执行便可。
这里我们在 effect.ts 中定义一个 trigger 方法用于触发依赖,之后在 reactive.ts 中引入。由于触发依赖发生在修改响应式对象 target 的属性阶段,所以需要放到 set 中执行:
// src/reactivity/effect.ts
280 |
281 | /**
282 | * 触发依赖
283 | * @param target 触发依赖的对象
284 | * @param key 触发该key对应的依赖
285 | */
286 | export function trigger(target, key) {
287 | // 根据对象与key获取到所有的依赖,并执行
288 | const depsMap = targetMap.get(target)
289 | const deps = depsMap.get(key)
290 | for(const dep of deps) {
291 | dep.run()
292 | }
293 | }
294 | // src/reactivity/reactive.ts
297 |
298 | import { track, trigger } from './effect'
299 |
300 | export const reactive = (raw) => {
301 | return new Proxy(raw, {
302 | // 取值
303 | get(target, key) {
304 | const res = Reflect.get(target, key)
305 | // 收集依赖
306 | track(target, key)
307 | return res
308 | },
309 | // 赋值
310 | set(target, key, value) {
311 | const res = Reflect.set(target, key, value)
312 | // 触发依赖
313 | trigger(target, key)
314 | return res
315 | }
316 | })
317 | }
318 | 至此,依赖收集 & 触发依赖 的功能就完成啦~