├── 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 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-07-16 15:24:48 4 | * @LastEditors: haifeng.lu 5 | * @LastEditTime: 2022-07-22 08:46:56 6 | * @Description: 7 | */ 8 | export * from './runtime-core' 9 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: /images/logo.svg 4 | --- 5 |
6 | 7 | 学习vue3源码的笔记。参考崔大的 [mini-vue](https://github.com/cuixiaorui/mini-vue), 欢迎大家去start⭐️~ 8 | 9 | 每篇笔记会对应一个提交分支,分支号会标记在笔记中,有兴趣的小伙伴可以切到对应分支配合笔记一起食用。 10 | 11 |
12 | -------------------------------------------------------------------------------- /src/runtime-core/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-07-10 09:39:24 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-07-16 15:33:08 6 | * @Description: 7 | */ 8 | export { createApp } from "./createApp"; 9 | export { h } from './h' 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/.vuepress/public/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/miniVue/notes/runtime-core/01_initComponent.md: -------------------------------------------------------------------------------- 1 | 8 | # 初始化 Component 9 | 10 | ::: tip 11 | 本篇笔记对应的分支号为: `main分支:45a31d4` 12 | ::: 13 | -------------------------------------------------------------------------------- /docs/.vuepress/.temp/pages/index.html.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /docs/.vuepress/.temp/vite-root/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/runtime-core/h.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-07-16 15:33:50 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-07-17 16:33:23 6 | * @Description: 7 | */ 8 | import { createVNode } from "./vnode"; 9 | 10 | export function h(type, props?, children?) { 11 | return createVNode(type, props, children) 12 | } -------------------------------------------------------------------------------- /docs/.vuepress/public/themes/index.js: -------------------------------------------------------------------------------- 1 | const { defaultTheme } = require('@vuepress/theme-default') 2 | const { path } = require('@vuepress/utils') 3 | 4 | module.exports.commentTheme = options => ({ 5 | // 继承默认主题 6 | extends: defaultTheme(options), 7 | 8 | // 覆盖 `404` 布局 9 | layouts: { 10 | Layout: path.resolve(__dirname, "layouts", "Layout.vue"), 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /src/runtime-core/vnode.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-07-10 09:42:52 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-08-01 08:54:23 6 | * @Description: 7 | */ 8 | export function createVNode(type, props?, children?) { 9 | const vnode = { 10 | type, props, children, 11 | el: null 12 | } 13 | 14 | return vnode 15 | } -------------------------------------------------------------------------------- /example/helloWorld/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-07-09 10:23:38 4 | * @LastEditors: haifeng.lu 5 | * @LastEditTime: 2022-07-22 08:49:47 6 | * @Description: 7 | */ 8 | import { App } from './main.js' 9 | import { createApp } from '../../lib/mini-vue.esm.js' 10 | 11 | const rootContainer = document.querySelector('#app') 12 | createApp(App).mount(rootContainer) 13 | -------------------------------------------------------------------------------- /docs/miniVue/notes/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vue3 源码学习 3 | comment: false 4 | --- 5 | 6 | 13 | 14 | # Vue3 源码学习 15 | 16 | 用于记录Vue3源码学习的笔记, 参考崔大的 [mini-vue](https://github.com/cuixiaorui/mini-vue), 欢迎大家去start⭐️~ 17 | 18 | 每篇笔记会对应一个提交分支,分支号会标记在笔记中,有兴趣的小伙伴可以切到对应分支配合笔记一起食用。 19 | -------------------------------------------------------------------------------- /docs/.vuepress/.temp/internal/layoutComponents.js: -------------------------------------------------------------------------------- 1 | import { defineAsyncComponent } from 'vue' 2 | 3 | export const layoutComponents = { 4 | "404": defineAsyncComponent(() => import("/Users/luhaifeng/codes/mine/vue3-study/node_modules/.pnpm/@vuepress+theme-default@2.0.0-beta.48/node_modules/@vuepress/theme-default/lib/client/layouts/404.vue")), 5 | "Layout": defineAsyncComponent(() => import("/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/public/themes/layouts/Layout.vue")), 6 | } 7 | -------------------------------------------------------------------------------- /.idea/vue3-study.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 8 |

9 | 10 | Vue3 源码学习 11 | 12 |

13 | 14 |

15 | 16 | 学习vue3源码的笔记。参考崔大的 [mini-vue](https://github.com/cuixiaorui/mini-vue), 欢迎大家去start⭐️~ 17 | 18 |

19 | 20 | ## 关于 21 | 22 | 每篇笔记会对应一个提交分支,分支号会标记在笔记中,有兴趣的小伙伴可以切到对应分支配合笔记一起食用。笔记下方带有评论区,有疑问,或者有发现笔记中的错误之处,欢迎小伙伴们积极留言~ 23 | -------------------------------------------------------------------------------- /src/runtime-core/createApp.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-07-10 09:39:37 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-07-10 10:37:59 6 | * @Description: 7 | */ 8 | import { createVNode } from "./vnode" 9 | import { render } from './renderer' 10 | export function createApp(rootComponent) { 11 | return { 12 | mount(rootContainer) { 13 | const vnode = createVNode(rootComponent) 14 | 15 | render(vnode, rootContainer) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/helloWorld/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-07-10 09:35:23 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-08-01 08:55:33 6 | * @Description: 7 | */ 8 | import { h } from '../../lib/mini-vue.esm.js' 9 | 10 | // 调试 $el 11 | window.self = null 12 | export const App = { 13 | render() { 14 | window.self = this 15 | return h('div', {}, 'hello ' + this.msg) 16 | }, 17 | 18 | setup() { 19 | return { 20 | msg: 'world' 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 确保脚本抛出遇到的错误 4 | set -e 5 | 6 | # 生成静态文件 7 | npm run docs:build 8 | 9 | # 进入生成的文件夹 10 | cd docs/.vuepress/dist 11 | 12 | # 如果是发布到自定义域名 13 | # echo 'www.example.com' > CNAME 14 | 15 | git init 16 | git add -A 17 | git commit -m 'deploy' 18 | 19 | # 如果发布到 https://.github.io 20 | # git push -f git@github.com:/.github.io.git master 21 | 22 | # 如果发布到 https://.github.io/ 23 | git push -f git@github.com:luhaifeng666/vue3-study.git master:gh-pages 24 | 25 | cd - 26 | -------------------------------------------------------------------------------- /docs/.vuepress/.temp/pages/404.html.js: -------------------------------------------------------------------------------- 1 | export const data = JSON.parse("{\"key\":\"v-3706649a\",\"path\":\"/404.html\",\"title\":\"\",\"lang\":\"en-US\",\"frontmatter\":{\"layout\":\"404\"},\"excerpt\":\"\",\"headers\":[],\"git\":{},\"filePathRelative\":null}") 2 | 3 | if (import.meta.webpackHot) { 4 | import.meta.webpackHot.accept() 5 | if (__VUE_HMR_RUNTIME__.updatePageData) { 6 | __VUE_HMR_RUNTIME__.updatePageData(data) 7 | } 8 | } 9 | 10 | if (import.meta.hot) { 11 | import.meta.hot.accept(({ data }) => { 12 | __VUE_HMR_RUNTIME__.updatePageData(data) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /docs/.vuepress/.temp/internal/siteData.js: -------------------------------------------------------------------------------- 1 | export const siteData = JSON.parse("{\"base\":\"/vue3-study/\",\"lang\":\"en-US\",\"title\":\"vue3 源码学习\",\"description\":\" \",\"head\":[[\"link\",{\"rel\":\"icon\",\"href\":\"/images/logo.svg\"}]],\"locales\":{}}") 2 | 3 | if (import.meta.webpackHot) { 4 | import.meta.webpackHot.accept() 5 | if (__VUE_HMR_RUNTIME__.updateSiteData) { 6 | __VUE_HMR_RUNTIME__.updateSiteData(siteData) 7 | } 8 | } 9 | 10 | if (import.meta.hot) { 11 | import.meta.hot.accept(({ siteData }) => { 12 | __VUE_HMR_RUNTIME__.updateSiteData(siteData) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /example/helloWorld/index.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-07-16 15:24:17 4 | * @LastEditors: haifeng.lu 5 | * @LastEditTime: 2022-07-22 08:48:27 6 | * @Description: 7 | */ 8 | import typescript from "@rollup/plugin-typescript" 9 | export default { 10 | input: "./src/index.ts", 11 | output: [ 12 | { 13 | format: 'cjs', 14 | file: 'lib/mini-vue.cjs.js', 15 | exports: 'auto' 16 | }, 17 | { 18 | format: 'es', 19 | file: 'lib/mini-vue.esm.js', 20 | exports: 'auto' 21 | } 22 | ], 23 | plugins: [ 24 | typescript() 25 | ] 26 | } -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-06-21 22:40:20 4 | * @LastEditors: ext.luhaifeng1 5 | * @LastEditTime: 2022-06-30 15:50:19 6 | * @Description: 全局通用方法 7 | */ 8 | export const extend = Object.assign 9 | 10 | /** 11 | * 判断传入的值是否是个对象 12 | * @param val 13 | * @returns 14 | */ 15 | export const isObject = val => { 16 | return val !== null && typeof(val) === 'object' 17 | } 18 | 19 | /** 20 | * 判断值是否发生变化 21 | * @param val 原值 22 | * @param newValue 新值 23 | * @returns 24 | */ 25 | export const hasChanged = (val, newValue) => !(Object.is(val, newValue)) 26 | -------------------------------------------------------------------------------- /docs/.vuepress/.temp/pages/miniVue/notes/index.html.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ext.luhaifeng1 ext.luhaifeng1@jd.com 3 | * @Date: 2022-06-29 17:43:14 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-06-29 20:11:28 6 | * @Description: 7 | */ 8 | import { shallowReadonly, isReadonly } from '../reactive' 9 | 10 | describe('shallowReadonly', () => { 11 | test('should not make non-reactive properties reactive', () => { 12 | const props = shallowReadonly({ n: { foo:1 }}) 13 | // 最外层的对象是 readonly 对象 14 | expect(isReadonly(props)).toBe(true) 15 | // 内部嵌套的对象不是 readonly 对象 16 | expect(isReadonly(props.n)).toBe(false) 17 | }) 18 | }) -------------------------------------------------------------------------------- /docs/miniVue/notes/prerequisites.md: -------------------------------------------------------------------------------- 1 | 8 | # 开始之前 9 | 10 | 在开始阅读本文档之前,先一起来了解一个思想:`TDD`。 11 | 12 | 那什么是 `TDD` 呢?我们先来看下它的概念: 13 | 14 | ::: tip 什么是 TDD ? 15 | 16 | 1. TDD是测试驱动开发(Test-Driven Development)的英文简称。 17 | 2. 它是敏捷开发中的一项核心实践和技术,也是一种设计方法论。 18 | 3. TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。 19 | 4. TDD虽是敏捷方法的核心实践,但不只适用于XP(Extreme Programming),同样可以适用于其他开发方法和过程。 20 | 21 | ::: 22 | 23 | 简而言之,就是 `测试驱动开发`,在动手开发之前先写好测试用例,然后再进行开发。有兴趣的小伙伴可以自行探究,此处不做赘述。 24 | 25 | 在接下来的文档中,`TDD` 的思想将贯穿始终。话不多说,让我们一起进入 Vue3 源码的学习吧~ 26 | -------------------------------------------------------------------------------- /docs/.vuepress/.temp/pages/miniVue/index.html.js: -------------------------------------------------------------------------------- 1 | export const data = JSON.parse("{\"key\":\"v-f4c6334e\",\"path\":\"/miniVue/\",\"title\":\"\",\"lang\":\"en-US\",\"frontmatter\":{},\"excerpt\":\"\",\"headers\":[],\"git\":{\"updatedTime\":1640835008000,\"contributors\":[{\"name\":\"luhaifeng\",\"email\":\"lhf222458@ncarzone.com\",\"commits\":1}]},\"filePathRelative\":\"miniVue/README.md\"}") 2 | 3 | if (import.meta.webpackHot) { 4 | import.meta.webpackHot.accept() 5 | if (__VUE_HMR_RUNTIME__.updatePageData) { 6 | __VUE_HMR_RUNTIME__.updatePageData(data) 7 | } 8 | } 9 | 10 | if (import.meta.hot) { 11 | import.meta.hot.accept(({ data }) => { 12 | __VUE_HMR_RUNTIME__.updatePageData(data) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/runtime-core/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-08-01 09:01:09 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-08-01 09:06:20 6 | * @Description: 7 | */ 8 | const publicPropertiesMap = { 9 | $el: i => i.vnode.el 10 | } 11 | 12 | export const PublicInstanceProxyHandlers = { 13 | get ({_: instance}, key) { 14 | const { setupState } = instance 15 | // 通过 this.xxxx 获取值 16 | if (key in setupState) { 17 | return setupState[key] 18 | } 19 | // 通过 this.$el 获取值 20 | const publicGetter = publicPropertiesMap[key] 21 | if (publicGetter) { 22 | return publicGetter(instance) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /docs/.vuepress/.temp/pages/index.html.js: -------------------------------------------------------------------------------- 1 | export const data = JSON.parse("{\"key\":\"v-8daa1a0e\",\"path\":\"/\",\"title\":\"\",\"lang\":\"en-US\",\"frontmatter\":{\"home\":true,\"heroImage\":\"/images/logo.svg\"},\"excerpt\":\"\",\"headers\":[],\"git\":{\"updatedTime\":1655196562000,\"contributors\":[{\"name\":\"luhaifeng\",\"email\":\"lhf222458@ncarzone.com\",\"commits\":2},{\"name\":\"luhaifeng666\",\"email\":\"youzui@hotmail.com\",\"commits\":1}]},\"filePathRelative\":\"README.md\"}") 2 | 3 | if (import.meta.webpackHot) { 4 | import.meta.webpackHot.accept() 5 | if (__VUE_HMR_RUNTIME__.updatePageData) { 6 | __VUE_HMR_RUNTIME__.updatePageData(data) 7 | } 8 | } 9 | 10 | if (import.meta.hot) { 11 | import.meta.hot.accept(({ data }) => { 12 | __VUE_HMR_RUNTIME__.updatePageData(data) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /docs/.vuepress/.temp/pages/miniVue/notes/index.html.js: -------------------------------------------------------------------------------- 1 | export const data = JSON.parse("{\"key\":\"v-402aedb2\",\"path\":\"/miniVue/notes/\",\"title\":\"Vue3 源码学习\",\"lang\":\"en-US\",\"frontmatter\":{\"title\":\"Vue3 源码学习\",\"comment\":false},\"excerpt\":\"\",\"headers\":[],\"git\":{\"updatedTime\":1657017188000,\"contributors\":[{\"name\":\"luhaifeng\",\"email\":\"lhf222458@ncarzone.com\",\"commits\":4},{\"name\":\"luhaifeng666\",\"email\":\"youzui@hotmail.com\",\"commits\":2}]},\"filePathRelative\":\"miniVue/notes/README.md\"}") 2 | 3 | if (import.meta.webpackHot) { 4 | import.meta.webpackHot.accept() 5 | if (__VUE_HMR_RUNTIME__.updatePageData) { 6 | __VUE_HMR_RUNTIME__.updatePageData(data) 7 | } 8 | } 9 | 10 | if (import.meta.hot) { 11 | import.meta.hot.accept(({ data }) => { 12 | __VUE_HMR_RUNTIME__.updatePageData(data) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/reactivity/computed.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-07-05 14:25:24 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-07-05 18:08:38 6 | * @Description: 7 | */ 8 | import { ReactiveEffect } from './effect' 9 | 10 | class ComputedRefImpl { 11 | private _getter: any 12 | private _dirty: boolean = true // 标记是否需要触发 getter 13 | private _value: any // 缓存值 14 | private _effect: ReactiveEffect 15 | 16 | constructor(getter) { 17 | this._getter = getter 18 | this._effect = new ReactiveEffect(this._getter, () => { 19 | if (!this._dirty) this._dirty = true 20 | }) 21 | } 22 | 23 | get value() { 24 | // 值没有发生变化时,再次获取不会触发 getter 25 | if (this._dirty) { 26 | this._dirty = false 27 | this._value = this._effect.run() 28 | } 29 | return this._value 30 | } 31 | } 32 | 33 | export const computed = getter => new ComputedRefImpl(getter) -------------------------------------------------------------------------------- /docs/.vuepress/public/themes/layouts/Layout.vue: -------------------------------------------------------------------------------- 1 | 8 | 36 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-06-25 16:53:52 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-06-29 21:03:30 6 | * @Description: 7 | */ 8 | 9 | import { readonly, isReadonly, isProxy } from '../reactive' 10 | 11 | describe('readonly', () => { 12 | it('happy path', () => { 13 | const original = { foo: 1, bar: { baz: 2 }} 14 | const wrapped = readonly(original) 15 | expect(wrapped).not.toBe(original) 16 | expect(wrapped.foo).toBe(1) 17 | // 判断是否是 readonly 对象 18 | expect(isReadonly(original)).toBe(false) 19 | expect(isReadonly(original.bar)).toBe(false) 20 | expect(isReadonly(wrapped.bar)).toBe(true) 21 | expect(isReadonly(wrapped)).toBe(true) 22 | // 判断是否是 Proxy 对象 23 | expect(isProxy(wrapped)).toBe(true) 24 | }) 25 | 26 | it('warn when call set', () => { 27 | console.warn = jest.fn() 28 | const user = readonly({ age: 10 }) 29 | user.age = 11 30 | // 当设置 readonly 对象的值时,需要发出告警 31 | expect(console.warn).toBeCalled() 32 | }) 33 | }) -------------------------------------------------------------------------------- /docs/.vuepress/.temp/pages/miniVue/notes/reactivity/reactive.html.js: -------------------------------------------------------------------------------- 1 | export const data = JSON.parse("{\"key\":\"v-8b74b6cc\",\"path\":\"/miniVue/notes/reactivity/reactive.html\",\"title\":\"effect & reactive & 依赖收集 & 触发依赖\",\"lang\":\"en-US\",\"frontmatter\":{},\"excerpt\":\"\",\"headers\":[{\"level\":2,\"title\":\"副本\",\"slug\":\"副本\",\"children\":[{\"level\":3,\"title\":\"实现 reactive\",\"slug\":\"实现-reactive\",\"children\":[]}]},{\"level\":2,\"title\":\"响应式\",\"slug\":\"响应式\",\"children\":[{\"level\":3,\"title\":\"实现 effect\",\"slug\":\"实现-effect\",\"children\":[]}]}],\"git\":{\"updatedTime\":1656087758000,\"contributors\":[{\"name\":\"luhaifeng666\",\"email\":\"youzui@hotmail.com\",\"commits\":7},{\"name\":\"luhaifeng\",\"email\":\"lhf222458@ncarzone.com\",\"commits\":1}]},\"filePathRelative\":\"miniVue/notes/reactivity/reactive.md\"}") 2 | 3 | if (import.meta.webpackHot) { 4 | import.meta.webpackHot.accept() 5 | if (__VUE_HMR_RUNTIME__.updatePageData) { 6 | __VUE_HMR_RUNTIME__.updatePageData(data) 7 | } 8 | } 9 | 10 | if (import.meta.hot) { 11 | import.meta.hot.accept(({ data }) => { 12 | __VUE_HMR_RUNTIME__.updatePageData(data) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ext.luhaifeng1 ext.luhaifeng1@jd.com 3 | * @Date: 2021-11-14 14:42:52 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-06-29 21:02:56 6 | * @Description: 7 | */ 8 | import { reactive, isReactive, isProxy } from '../reactive' 9 | 10 | describe('reactive', () => { 11 | it('happy path', () => { 12 | const origin = { num: 0 } 13 | // 通过 reactive 创建响应式对象 14 | const reactiveData = reactive(origin) 15 | // 判断响应式对象与原对象不是同一个对象 16 | expect(reactiveData).not.toBe(origin) 17 | // 代理对象中的 num 值应与原对象中的相同 18 | expect(reactiveData.num).toBe(0) 19 | // 判断是否是 reactive 对象 20 | expect(isReactive(origin)).toBe(false) 21 | expect(isReactive(reactiveData)).toBe(true) 22 | // 判断是否是 Proxy 对象 23 | expect(isProxy(reactiveData)).toBe(true) 24 | }) 25 | 26 | it('nested reactive', () => { 27 | const original = { 28 | nested: { 29 | foo: 1 30 | }, 31 | array: [{ bar: 2 }] 32 | } 33 | const observed = reactive(original) 34 | expect(isReactive(observed.nested)).toBe(true) 35 | expect(isReactive(observed.array)).toBe(true) 36 | expect(isReactive(observed.array[0])).toBe(true) 37 | }) 38 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-study", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:luhaifeng666/vue3-study", 6 | "author": "luhaifeng ", 7 | "scripts": { 8 | "docs:dev": "vuepress dev docs", 9 | "docs:build": "vuepress build docs", 10 | "deploy": "bash deploy.sh", 11 | "ca": "git add -A && git-cz -av", 12 | "commit": "git-cz", 13 | "test": "jest", 14 | "build": "rollup -c rollup.config.js" 15 | }, 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@babel/preset-env": "^7.18.2", 19 | "@babel/preset-typescript": "^7.17.12", 20 | "@rollup/plugin-typescript": "^8.3.3", 21 | "@types/jest": "^28.1.1", 22 | "@vue/component-compiler-utils": "^3.3.0", 23 | "@vuepress/client": "^2.0.0-beta.48", 24 | "@vuepress/core": "^2.0.0-beta.48", 25 | "@vuepress/plugin-search": "^2.0.0-beta.48", 26 | "commitizen": "^4.2.3", 27 | "cz-conventional-changelog": "^3.3.0", 28 | "husky": "^5.0.9", 29 | "jest": "^28.1.1", 30 | "lint-staged": "^10.5.4", 31 | "rollup": "^2.77.0", 32 | "vue": "^3.2.37", 33 | "vuepress": "^2.0.0-beta.48", 34 | "vuepress-plugin-comment2": "^2.0.0-beta.82" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/runtime-core/component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-07-10 09:53:01 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-08-01 09:04:00 6 | * @Description: 7 | */ 8 | import { PublicInstanceProxyHandlers } from './componentPublicInstance' 9 | export function createComponentInstance(vnode) { 10 | const component = { 11 | vnode, 12 | type: vnode.type, 13 | setupState: {} 14 | } 15 | 16 | return component 17 | } 18 | 19 | export function setupComponent(instance) { 20 | // initProps() 21 | // initSlots() 22 | 23 | setupStatefulComponent(instance) 24 | } 25 | 26 | function setupStatefulComponent(instance) { 27 | const Component = instance.type 28 | 29 | instance.proxy = new Proxy( 30 | { _: instance }, 31 | PublicInstanceProxyHandlers 32 | ) 33 | 34 | const { setup } = Component 35 | 36 | if (setup) { 37 | const setupResult = setup() 38 | 39 | handleSetupResult(instance, setupResult) 40 | } 41 | } 42 | 43 | function handleSetupResult(instance, setupResult) { 44 | if (typeof setupResult === 'object') { 45 | instance.setupState = setupResult 46 | } 47 | 48 | finishComponentSetup(instance) 49 | } 50 | 51 | function finishComponentSetup(instance) { 52 | const Component = instance.type 53 | 54 | if (Component.render) { 55 | instance.render = Component.render 56 | } 57 | } -------------------------------------------------------------------------------- /src/reactivity/__tests__/computed.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-07-05 14:21:37 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-07-05 15:24:21 6 | * @Description: 7 | */ 8 | import { reactive } from '../reactive' 9 | import { computed } from '../computed' 10 | 11 | describe("computed", () => { 12 | it("happy path", () => { 13 | const user = reactive({ 14 | age: 1 15 | }) 16 | 17 | const age = computed(() => user.age) 18 | 19 | expect(age.value).toBe(1) 20 | }) 21 | 22 | it("should compute lazily", () => { 23 | const value = reactive({ 24 | foo: 1 25 | }) 26 | 27 | const getter = jest.fn(() => value.foo) 28 | const cValue = computed(getter) 29 | 30 | // lazy 31 | expect(getter).not.toHaveBeenCalled() 32 | 33 | expect(cValue.value).toBe(1) 34 | expect(getter).toHaveBeenCalledTimes(1) 35 | 36 | // should not compute again 37 | cValue.value 38 | expect(getter).toHaveBeenCalledTimes(1) 39 | 40 | // should not compute until needed 41 | value.foo = 2 42 | expect(getter).toHaveBeenCalledTimes(1) 43 | 44 | // now it should computed 45 | expect(cValue.value).toBe(2) 46 | expect(getter).toHaveBeenCalledTimes(2) 47 | 48 | // should not compute again 49 | cValue.value 50 | expect(getter).toHaveBeenCalledTimes(2) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /src/reactivity/reactive.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ext.luhaifeng1 ext.luhaifeng1@jd.com 3 | * @Date: 2021-11-14 14:41:24 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-06-29 21:00:07 6 | * @Description: 7 | */ 8 | import { mutableHandlers, readonlyHandlers, shallowReadonlyHandlers } from './baseHandlers' 9 | 10 | export const enum ReactiveFlags { 11 | IS_REACTIVE = '__v_isReactive', 12 | IS_READONLY = '__v_isReadonly' 13 | } 14 | 15 | /** 16 | * 创建proxy对象 17 | * @param raw 需要被代理的对象 18 | * @param baseHandlers 代理拦截 19 | * @returns 20 | */ 21 | function createActiveObject(raw, baseHandlers) { 22 | return new Proxy(raw, baseHandlers) 23 | } 24 | 25 | 26 | /** 27 | * 创建 reactive 对象 28 | * @param raw 需要被代理的对象 29 | * @returns 30 | */ 31 | export const reactive = (raw) => { 32 | return createActiveObject(raw, mutableHandlers) 33 | } 34 | 35 | /** 36 | * 创建 readonly 对象 37 | * @param raw 需要被代理的对象 38 | * @returns 39 | */ 40 | export const readonly = (raw) => { 41 | return createActiveObject(raw, readonlyHandlers) 42 | } 43 | 44 | /** 45 | * 创建 shallowReadonly 对象 46 | * @param raw 47 | * @returns 48 | */ 49 | export const shallowReadonly = raw => { 50 | return createActiveObject(raw, shallowReadonlyHandlers) 51 | } 52 | 53 | // 判断是否是 reactive 对象 54 | export const isReactive = value => !!value[ReactiveFlags.IS_REACTIVE] 55 | 56 | // 判断是否是 readonly 对象 57 | export const isReadonly = value => !!value[ReactiveFlags.IS_READONLY] 58 | 59 | // 判断是否是 Proxy 对象 60 | export const isProxy = value => isReactive(value) || isReadonly(value) 61 | -------------------------------------------------------------------------------- /.github/workflows/blank.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: VUE3-STUDY DOCS CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the main branch 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | branches: [ main ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v2 27 | - uses: actions/setup-node@v2 28 | with: 29 | node-version: '14' 30 | - name: Compiling start 31 | run: echo Compiling start! 32 | - run: git config --global user.email "youzui@hotmail.com" 33 | - run: git config --global user.name "luhaifeng" 34 | - run: npm install && npm run docs:build 35 | - name: Deploy 36 | uses: peaceiris/actions-gh-pages@v3 37 | with: 38 | deploy_key: ${{ secrets.VUE3STUDY }} 39 | external_repository: luhaifeng666/vue3-study #推送到该仓库中,地址格式为github名称/仓库名 40 | publish_branch: gh-pages #分支名 41 | publish_dir: docs/.vuepress/dist # 这里设置的是要推送到external_repository的产物路径 42 | -------------------------------------------------------------------------------- /src/runtime-core/renderer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-07-10 09:44:49 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-08-07 10:27:52 6 | * @Description: 7 | */ 8 | import { createComponentInstance, setupComponent } from "./component" 9 | import { isObject } from '../shared/index'; 10 | export function render(vnode, container) { 11 | patch(vnode, container) 12 | } 13 | 14 | function patch(vnode, container) { 15 | const { type } = vnode 16 | if (typeof type === 'string') { 17 | // 处理Element 18 | processElement(vnode, container) 19 | } else if (isObject(type)) { 20 | // 处理组件 21 | processComponent(vnode, container) 22 | } 23 | } 24 | 25 | function processElement(vnode: any, container: any) { 26 | mountElement(vnode, container) 27 | } 28 | 29 | function mountElement(vnode: any, container: any) { 30 | const { type, children, props } = vnode 31 | const el = (vnode.el = document.createElement(type)) 32 | 33 | if (typeof children === 'string') { 34 | el.textContent = children 35 | } else if (Array.isArray(children)) { 36 | mountChildren(vnode, el) 37 | } 38 | 39 | for(const key in props) { 40 | const val = props[key] 41 | el.setAttribute(key, val) 42 | } 43 | 44 | container.append(el) 45 | } 46 | 47 | function mountChildren(vnode: any, container: any) { 48 | vnode.children.forEach(child => { 49 | patch(child, container) 50 | }) 51 | } 52 | 53 | function processComponent(vnode: any, container: any) { 54 | mountComponent(vnode, container) 55 | } 56 | 57 | function mountComponent(vnode, container) { 58 | const instance = createComponentInstance(vnode) 59 | 60 | setupComponent(instance) 61 | setupRenderEffect(instance, vnode, container) 62 | } 63 | 64 | function setupRenderEffect(instance, vnode, container) { 65 | const { proxy } = instance 66 | const subTree = instance.type.render.call(proxy); 67 | patch(subTree, container) 68 | vnode.el = subTree.el 69 | } -------------------------------------------------------------------------------- /src/reactivity/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2021-11-14 15:06:13 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-06-29 20:46:48 6 | * @Description: 7 | */ 8 | import { track, trigger } from './effect' 9 | import { ReactiveFlags, reactive, readonly } from './reactive' 10 | import { isObject, extend } from '../shared' 11 | 12 | /** 13 | * 用于生成 get 方法 14 | * @param isReadonly 是否是 readonly 对象 15 | * @param isShallow 是否是 shallow 对象 16 | * @returns 17 | */ 18 | function createGetter(isReadonly = false, isShallow = false) { 19 | return function(target, key) { 20 | // 判断是否是 reactive 对象 21 | if (key === ReactiveFlags.IS_REACTIVE) { 22 | return !isReadonly 23 | } else if (key === ReactiveFlags.IS_READONLY) { 24 | // 判断是否是 readonly 对象 25 | return isReadonly 26 | } 27 | const res = Reflect.get(target, key) 28 | 29 | // 如果是 shallow,直接返回结果 30 | if (isShallow) return res 31 | 32 | if (isObject(res)) { 33 | return isReadonly ? readonly(res) : reactive(res) 34 | } 35 | // 收集依赖 36 | !isReadonly && track(target, key) 37 | return res 38 | } 39 | } 40 | 41 | // 用于生成 set 方法 42 | function createSetter() { 43 | return function(target, key, value) { 44 | const res = Reflect.set(target, key, value) 45 | // 触发依赖 46 | trigger(target, key) 47 | return res 48 | } 49 | } 50 | 51 | // 缓存,避免重复调用 52 | const get = createGetter() 53 | const set = createSetter() 54 | const readonlyGet = createGetter(true) 55 | const shallowReadonlyGet = createGetter(true, true) 56 | 57 | export const mutableHandlers = { 58 | get, 59 | set 60 | } 61 | 62 | export const readonlyHandlers = { 63 | get: readonlyGet, 64 | set(target, key, value) { 65 | // 当尝试设置 readonly 的属性值时,需要给出告警提示 66 | console.warn(`${key} can't be setted!`, target) 67 | return true 68 | } 69 | } 70 | 71 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 72 | get: shallowReadonlyGet 73 | }) 74 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/ref.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ext.luhaifeng1 ext.luhaifeng1@jd.com 3 | * @Date: 2022-06-30 15:00:53 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-07-03 10:26:12 6 | * @Description: 7 | */ 8 | 9 | import { effect } from '../effect' 10 | import { reactive } from '../reactive' 11 | import { ref, isRef, unRef, proxyRefs } from '../ref' 12 | 13 | describe('ref', () => { 14 | it('happy path', () => { 15 | const a = ref(1) 16 | expect(a.value).toBe(1) 17 | }) 18 | 19 | it('should be reactive', () => { 20 | const a = ref(1) 21 | let dummy 22 | let calls = 0 23 | effect(() => { 24 | calls++ 25 | dummy = a.value 26 | }) 27 | expect(calls).toBe(1) 28 | expect(dummy).toBe(1) 29 | a.value = 2 30 | expect(calls).toBe(2) 31 | expect(dummy).toBe(2) 32 | // same value should not trigger 33 | a.value = 2 34 | expect(calls).toBe(2) 35 | expect(dummy).toBe(2) 36 | }) 37 | 38 | it('should make nested properties reactive', () => { 39 | const a = ref({ count: 1 }) 40 | let dummy 41 | effect(() => { 42 | dummy = a.value.count 43 | }) 44 | expect(dummy).toBe(1) 45 | a.value.count = 2 46 | expect(dummy).toBe(2) 47 | }) 48 | 49 | it('isRef', () => { 50 | const a = ref(1) 51 | const user = reactive({ age: 10 }) 52 | expect(isRef(a)).toBe(true) 53 | expect(isRef(1)).toBe(false) 54 | expect(isRef(user)).toBe(false) 55 | }) 56 | 57 | it('unRef', () => { 58 | const a = ref(1) 59 | expect(unRef(a)).toBe(1) 60 | expect(unRef(1)).toBe(1) 61 | }) 62 | 63 | it('proxyRefss', () => { 64 | const user = { 65 | age: ref(10), 66 | name: 'xiaohong' 67 | } 68 | // 调用 proxyUser 后,无需通过 .value 的方式获取 ref 的值 69 | const proxyUser = proxyRefs(user) 70 | expect(user.age.value).toBe(10) 71 | expect(proxyUser.age).toBe(10) 72 | expect(proxyUser.name).toBe('xiaohong') 73 | 74 | proxyUser.age = 20 75 | expect(proxyUser.age).toBe(20) 76 | expect(user.age.value).toBe(20) 77 | 78 | proxyUser.age = ref(10) 79 | expect(proxyUser.age).toBe(10) 80 | expect(user.age.value).toBe(10) 81 | }) 82 | }) -------------------------------------------------------------------------------- /docs/.vuepress/.temp/internal/themeData.js: -------------------------------------------------------------------------------- 1 | export const themeData = JSON.parse("{\"lastUpdated\":\"Last Updated\",\"sidebarDepth\":2,\"sidebar\":{\"/miniVue/notes/\":[{\"text\":\"开始之前\",\"link\":\"/miniVue/notes/prerequisites.md\"},{\"text\":\"Reactivity\",\"collapsible\":true,\"children\":[\"/miniVue/notes/reactivity/01_reactive.md\",\"/miniVue/notes/reactivity/02_runner.md\",\"/miniVue/notes/reactivity/03_scheduler.md\",\"/miniVue/notes/reactivity/04_stop.md\",\"/miniVue/notes/reactivity/05_readonly.md\",\"/miniVue/notes/reactivity/06_reactiveOrReadonly.md\",\"/miniVue/notes/reactivity/07_nestedReactiveAndReadonly.md\",\"/miniVue/notes/reactivity/08_shallowReadonly.md\",\"/miniVue/notes/reactivity/09_isProxy.md\",\"/miniVue/notes/reactivity/10_ref.md\",\"/miniVue/notes/reactivity/11_isRef & unRef.md\",\"/miniVue/notes/reactivity/12_proxyRefs.md\",\"/miniVue/notes/reactivity/13_computed.md\"]},{\"text\":\"Runtime-core\",\"collapsible\":true,\"children\":[\"/miniVue/notes/runtime-core/01_initComponent.md\"]}]},\"navbar\":[{\"text\":\"Home\",\"link\":\"/\"},{\"text\":\"Notes\",\"link\":\"/miniVue/notes/\"},{\"text\":\"酱豆腐精的小站\",\"link\":\"https://luhaifeng666.github.io\"},{\"text\":\"GitHub\",\"link\":\"https://github.com/luhaifeng666/vue3-study\"}],\"locales\":{\"/\":{\"selectLanguageName\":\"English\"}},\"colorMode\":\"auto\",\"colorModeSwitch\":true,\"logo\":null,\"repo\":null,\"selectLanguageText\":\"Languages\",\"selectLanguageAriaLabel\":\"Select language\",\"editLink\":true,\"editLinkText\":\"Edit this page\",\"lastUpdatedText\":\"Last Updated\",\"contributors\":true,\"contributorsText\":\"Contributors\",\"notFound\":[\"There's nothing here.\",\"How did we get here?\",\"That's a Four-Oh-Four.\",\"Looks like we've got some broken links.\"],\"backToHome\":\"Take me home\",\"openInNewWindow\":\"open in new window\",\"toggleColorMode\":\"toggle color mode\",\"toggleSidebar\":\"toggle sidebar\"}") 2 | 3 | if (import.meta.webpackHot) { 4 | import.meta.webpackHot.accept() 5 | if (__VUE_HMR_RUNTIME__.updateThemeData) { 6 | __VUE_HMR_RUNTIME__.updateThemeData(themeData) 7 | } 8 | } 9 | 10 | if (import.meta.hot) { 11 | import.meta.hot.accept(({ themeData }) => { 12 | __VUE_HMR_RUNTIME__.updateThemeData(themeData) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /docs/miniVue/notes/reactivity/09_isProxy.md: -------------------------------------------------------------------------------- 1 | 8 | # isProxy 9 | 10 | ::: tip 11 | 本篇笔记对应的分支号为: `main分支: 618f052` 12 | ::: 13 | 14 | 我们还是先来看下官方的描述: 15 | 16 | :::tip 概念 17 | 检查对象是否是由 reactive 或 readonly 创建的 proxy。 18 | ::: 19 | 20 | 其实这个功能非常简单,要想判断一个对象是否是 `Proxy` 对象,只需要判断它是否是 `reactive` 对象或者 `readonly` 对象即可。 21 | 22 | 还记得我们之前用于判断 `reactive` 以及 `readonly` 时所定义的 `isReactive` 以及 `isReadonly` 方法么?在这里我们直接使用就行了。 23 | 24 | 老规矩,还是先奉上相关的测试用例: 25 | 26 | :::: code-group 27 | ::: code-group-item reactive.spec.ts 28 | 29 | ```ts{15} 30 | // src/reactivity/__tests__/reactive.spec.ts 31 | 32 | it('happy path', () => { 33 | const origin = { num: 0 } 34 | // 通过 reactive 创建响应式对象 35 | const reactiveData = reactive(origin) 36 | // 判断响应式对象与原对象不是同一个对象 37 | expect(reactiveData).not.toBe(origin) 38 | // 代理对象中的 num 值应与原对象中的相同 39 | expect(reactiveData.num).toBe(0) 40 | // 判断是否是 reactive 对象 41 | expect(isReactive(origin)).toBe(false) 42 | expect(isReactive(reactiveData)).toBe(true) 43 | // 判断是否是 Proxy 对象 44 | expect(isProxy(reactiveData)).toBe(true) 45 | }) 46 | ``` 47 | 48 | ::: 49 | 50 | ::: code-group-item readonly.spec.ts 51 | 52 | ```ts{14} 53 | // src/reactivity/__tests__/readonly.spec.ts 54 | 55 | it('happy path', () => { 56 | const original = { foo: 1, bar: { baz: 2 }} 57 | const wrapped = readonly(original) 58 | expect(wrapped).not.toBe(original) 59 | expect(wrapped.foo).toBe(1) 60 | // 判断是否是 readonly 对象 61 | expect(isReadonly(original)).toBe(false) 62 | expect(isReadonly(original.bar)).toBe(false) 63 | expect(isReadonly(wrapped.bar)).toBe(true) 64 | expect(isReadonly(wrapped)).toBe(true) 65 | // 判断是否是 Proxy 对象 66 | expect(isProxy(wrapped)).toBe(true) 67 | }) 68 | ``` 69 | 70 | ::: 71 | 72 | :::: 73 | 74 | 接下来,我们需要去定义下 `isProxy` 方法: 75 | 76 | :::: code-group 77 | ::: code-group-item reactive.ts 78 | 79 | ```ts 80 | // src/reactivity/reactive.ts 81 | 82 | // 判断是否是 Proxy 对象 83 | export const isProxy = value => isReactive(value) || isReadonly(value) 84 | ``` 85 | 86 | ::: 87 | :::: 88 | 89 | 至此,`isProxy` 功能就已经实现完成啦~(真的好简单)。 90 | -------------------------------------------------------------------------------- /src/reactivity/ref.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ext.luhaifeng1 ext.luhaifeng1@jd.com 3 | * @Date: 2022-06-30 15:06:08 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-07-03 10:26:01 6 | * @Description: 7 | */ 8 | 9 | import { trackEffects, triggerEffects, isTracking } from './effect' 10 | import { hasChanged, isObject } from '../shared' 11 | import { reactive } from './reactive' 12 | 13 | class RefImpl { 14 | private _value: any 15 | private _rawValue: any // 保存原始值,用于 set 阶段对比值是否发生了变化 16 | deps: Set 17 | __v_isRef = true 18 | 19 | constructor(value: any) { 20 | this._rawValue = value 21 | this._value = convert(value) 22 | this.deps = new Set() 23 | } 24 | 25 | get value() { 26 | // 可以进行 track 时才进行依赖收集 27 | isTracking() && trackEffects(this.deps) 28 | return this._value 29 | } 30 | 31 | set value(newValue: any) { 32 | // 如果值没有发生变化,不会触发 trigger 33 | if (hasChanged(this._rawValue, newValue)) { 34 | this._rawValue = newValue 35 | this._value = convert(newValue) 36 | triggerEffects(this.deps) 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * 判断传入 ref 的值是否是对象类型,如果是对象类型,需要使用 reactive 进行包裹 43 | * @param value 44 | * @returns 45 | */ 46 | function convert(value) { 47 | return isObject(value) ? reactive(value) : value 48 | } 49 | 50 | export function ref(value) { 51 | return new RefImpl(value) 52 | } 53 | 54 | /** 55 | * 用于判断传入的值是否是 ref 56 | * @param ref 57 | * @returns 58 | */ 59 | export function isRef(ref) { 60 | return !!ref.__v_isRef 61 | } 62 | 63 | /** 64 | * 用于返回 ref 的value值 65 | * 如果传入的不是 ref,则原样返回 66 | * @param ref 67 | */ 68 | export function unRef(ref) { 69 | return isRef(ref) ? ref.value : ref 70 | } 71 | 72 | /** 73 | * template 中调用此方法,目的是在页面中无需通过 .value 的方式获取 ref 的值 74 | * @param objectWithRefs 75 | * @returns 76 | */ 77 | export function proxyRefs(objectWithRefs) { 78 | return new Proxy(objectWithRefs, { 79 | get(target, key) { 80 | return unRef(Reflect.get(target, key)) 81 | }, 82 | 83 | set(target, key, value) { 84 | if (isRef(target[key]) && !isRef(value)) { 85 | return target[key].value = value 86 | } else { 87 | return Reflect.set(target, key, value) 88 | } 89 | } 90 | }) 91 | } -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-06-26 10:05:23 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-06-29 17:57:34 6 | * @Description: 7 | */ 8 | 9 | const fs = require('fs') 10 | const { searchPlugin } = require('@vuepress/plugin-search') 11 | const { commentPlugin } = require('vuepress-plugin-comment2') 12 | const { commentTheme } = require('./public/themes') 13 | const BASE_URL = './docs/miniVue/notes' 14 | 15 | let getMenus = function() { 16 | const urls = fs.readdirSync(BASE_URL) 17 | 18 | return urls.filter( 19 | url => ['.md', '.DS_Store'].every( 20 | item => !url.includes(item) 21 | ) 22 | ).map(url => ({ 23 | text: url.split('') 24 | .map((chart, index) => !index ? chart.toUpperCase() : chart) 25 | .join(''), 26 | collapsible: true, 27 | children: fs.readdirSync(`${BASE_URL}/${url}`).map(name => `/miniVue/notes/${url}/${name}`) 28 | })) 29 | } 30 | 31 | let config = { 32 | title: 'vue3 源码学习', 33 | description: ' ', 34 | base: '/vue3-study/', 35 | head: [ 36 | ['link', { rel: 'icon', href: '/images/logo.svg' }] 37 | ], 38 | markdown: { 39 | lineNumbers: true 40 | }, 41 | theme: commentTheme({ 42 | lastUpdated: 'Last Updated', 43 | sidebarDepth: 2, 44 | sidebar: { 45 | '/miniVue/notes/': [ 46 | { 47 | text: '开始之前', 48 | link: '/miniVue/notes/prerequisites.md', 49 | }, 50 | ...getMenus() 51 | ], 52 | }, 53 | navbar: [ 54 | { text: 'Home', link: '/' }, 55 | { text: 'Notes', link: '/miniVue/notes/'}, 56 | { text: '酱豆腐精的小站', link: 'https://luhaifeng666.github.io' }, 57 | { text: 'GitHub', link: 'https://github.com/luhaifeng666/vue3-study' }, 58 | ], 59 | }), 60 | plugins: [ 61 | commentPlugin({ 62 | // 插件选项 63 | provider: 'Giscus', 64 | repo: 'luhaifeng666/vue3-study', 65 | repoId: 'R_kgDOGMPtbQ', 66 | category: 'General', 67 | categoryId: 'DIC_kwDOGMPtbc4CPh3Q', 68 | mapping: 'title' 69 | }), 70 | searchPlugin({ 71 | locales: { 72 | '/': { 73 | placeholder: 'Search', 74 | }, 75 | '/zh/': { 76 | placeholder: '搜索', 77 | }, 78 | }, 79 | }) 80 | ] 81 | } 82 | 83 | module.exports = config 84 | -------------------------------------------------------------------------------- /docs/miniVue/notes/reactivity/11_isRef & unRef.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | # isRef & unRef 10 | 11 | ::: tip 12 | 本篇笔记对应的分支号为: `main分支: 6107262` 13 | ::: 14 | 15 | 这是两个工具方法。 16 | 17 | > - `isRef` 用于判断传入的值是否是 `ref` 对象; 18 | > - `unRef` 用于返回传入 `ref` 对象的 `value` 值。如果传入的不是 `ref` 对象,则将值原样返回。 19 | 20 | ## isRef 21 | 22 | 老规矩,我们还是先来编写对应的测试用例: 23 | 24 | :::: code-group 25 | ::: code-group-item ref.spec.ts 26 | 27 | ```ts 28 | // src/reactivity/__tests__/ref.spec.ts 29 | 30 | it('isRef', () => { 31 | const a = ref(1) 32 | const user = reactive({ age: 10 }) 33 | expect(isRef(a)).toBe(true) 34 | expect(isRef(1)).toBe(false) 35 | expect(isRef(user)).toBe(false) 36 | }) 37 | ``` 38 | 39 | ::: 40 | :::: 41 | 42 | 那我们要如何判断传入的对象是否是个 `ref` 对象呢?我们可以在 `RefImpl` 类中定义一个属性 `__v_isRef`,用于标识 `ref` 对象。之后我们对传入的值进行判断,如果有这个属性,那么它就是个 `ref` 对象: 43 | 44 | :::: code-group 45 | ::: code-group-item ref.ts 46 | 47 | ```ts{7,36-38} 48 | // src/reactivity/ref.ts 49 | 50 | class RefImpl { 51 | private _value: any 52 | private _rawValue: any // 保存原始值,用于 set 阶段对比值是否发生了变化 53 | deps: Set 54 | __v_isRef = true 55 | 56 | constructor(value: any) { 57 | this._rawValue = value 58 | this._value = convert(value) 59 | this.deps = new Set() 60 | } 61 | 62 | get value() { 63 | // 可以进行 track 时才进行依赖收集 64 | isTracking() && trackEffects(this.deps) 65 | return this._value 66 | } 67 | 68 | set value(newValue: any) { 69 | // 如果值没有发生变化,不会触发 trigger 70 | if (hasChanged(this._rawValue, newValue)) { 71 | this._rawValue = newValue 72 | this._value = convert(newValue) 73 | triggerEffects(this.deps) 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * 用于判断传入的值是否是 ref 80 | * @param ref 81 | * @returns 82 | */ 83 | export function isRef(ref) { 84 | return !!ref.__v_isRef 85 | } 86 | ``` 87 | 88 | ::: 89 | :::: 90 | 91 | ## unRef 92 | 93 | 话不多说,上测试用例: 94 | 95 | :::: code-group 96 | ::: code-group-item ref.spec.ts 97 | 98 | ```ts 99 | // src/reactivity/__tests__/ref.spec.ts 100 | 101 | it('unRef', () => { 102 | const a = ref(1) 103 | expect(unRef(a)).toBe(1) 104 | expect(unRef(1)).toBe(1) 105 | }) 106 | ``` 107 | 108 | ::: 109 | :::: 110 | 111 | `unRef` 的实现其实很简单,只要判断传入的值是否是 `ref` 对象即可。上面我们已经实现了 `isRef`,现在要做的只是返回对应的值就好了: 112 | 113 | :::: code-group 114 | ::: code-group-item ref.ts 115 | 116 | ```ts 117 | // src/reactivity/ref.ts 118 | 119 | export function unRef(ref) { 120 | return isRef(ref) ? ref.value : ref 121 | } 122 | ``` 123 | 124 | ::: 125 | :::: 126 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/effect.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ext.luhaifeng1 ext.luhaifeng1@jd.com 3 | * @Date: 2021-11-14 18:35:25 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-06-28 14:54:08 6 | * @Description: 7 | */ 8 | import { reactive } from '../reactive' 9 | import { effect, stop } from '../effect' 10 | 11 | describe('effect', () => { 12 | it('happy path', () => { 13 | // 创建响应式对象 14 | const user = reactive({ 15 | age: 10 16 | }) 17 | let nextAge 18 | effect(() => { 19 | nextAge = user.age + 1 20 | }) 21 | // 传入 effect 的方法会被立即执行一次 22 | expect(nextAge).toBe(11) 23 | // 修改响应式对象的属性值 24 | user.age++ 25 | // 传入 effect 的方法会再次被执行 26 | expect(nextAge).toBe(12) 27 | }) 28 | 29 | it('renturn a runner when call effect', () => { 30 | // 1. 调用 effect 会立即执行传过去的函数 fn 31 | // 2. 调用完成后,effect 会返回一个 runner 函数 32 | // 3. 执行 runner 函数,会再次执行传入 effect 的函数 fn,并返回 fn 的 返回值 33 | let foo = 10 34 | const runner = effect(() => { 35 | foo++ 36 | return 'foo' 37 | }) 38 | expect(foo).toBe(11) 39 | const res = runner() 40 | expect(foo).toBe(12) 41 | expect(res).toBe('foo') 42 | }) 43 | 44 | it('scheduler', () => { 45 | let dummy 46 | let run: any 47 | const scheduler = jest.fn(() => { 48 | run = runner 49 | }) 50 | const obj = reactive({ foo: 1 }) 51 | const runner = effect(() => { 52 | dummy = obj.foo 53 | }, { scheduler }) 54 | // 首次执行 effect 时不会调用 scheduler 方法 55 | expect(scheduler).not.toHaveBeenCalled() 56 | // 首次执行 effect 时会调用传入的第一个方法,此时给 dummy 赋值 57 | expect(dummy).toBe(1) 58 | // 触发响应式对象值改变 59 | obj.foo++ 60 | // 会执行 scheduler 方法,但是不会执行第一个参数 61 | expect(scheduler).toHaveBeenCalledTimes(1) 62 | expect(dummy).toBe(1) 63 | // 执行 runner 方法时,会执行传入的第一个方法 64 | run() 65 | expect(dummy).toBe(2) 66 | }) 67 | 68 | it('stop', () => { 69 | let dummy 70 | const obj = reactive({ prop: 1 }) 71 | const runner = effect(() => { 72 | dummy = obj.prop 73 | }) 74 | obj.prop = 2 75 | expect(dummy).toBe(2) 76 | // 调用 stop 后,响应式对象属性变化时不再触发 fn 77 | stop(runner) 78 | // obj.prop = 3 79 | // obj.prop = obj.prop + 1 80 | // get => set 81 | obj.prop++ 82 | expect(dummy).toBe(2) 83 | // 被停用的 effect 仍可以被调用 84 | runner() 85 | expect(dummy).toBe(3) 86 | }) 87 | 88 | it('onStop', () => { 89 | const obj = reactive({ prop: 1 }) 90 | const onStop = jest.fn() 91 | let dummy 92 | const runner = effect(() => { 93 | dummy = obj.prop 94 | }, { 95 | onStop 96 | }) 97 | expect(dummy).toBe(1) 98 | // 当调用stop时,onStop 会被调用一次 99 | stop(runner) 100 | expect(onStop).toBeCalledTimes(1) 101 | }) 102 | }) -------------------------------------------------------------------------------- /docs/miniVue/notes/reactivity/02_runner.md: -------------------------------------------------------------------------------- 1 | 8 | # runner 9 | 10 | ::: tip 11 | 本篇笔记对应的分支号为: `main分支:8fcd786` 12 | ::: 13 | 14 | 在动手实现之前,我们需要先弄清楚两个问题:`runner` 是什么 以及 `runner` 实现了什么功能。 15 | 16 | ## `runner` 是什么 🤔 17 | 18 | 还记得上一节在实现 `依赖收集` 以及 `依赖触发` 时的 `effect` 方法么? 19 | 20 | > **`runner` 就是 `effect` 的返回的一个 `函数`**。 21 | 22 | ## `runner` 实现了什么功能 🤔 23 | 24 | `runner` 主要做了两件事情: 25 | 26 | > 1. 执行 `runner` 时,会再次执行传入 `effect` 中的方法,我们暂且叫它 `fn`; 27 | > 2. `runner` 会返回 `fn` 执行后的结果。 28 | 29 | ## 实现 `runner` 30 | 31 | 依据上面的描述,我们先来编写 `runner` 的测试用例: 32 | 33 | :::: code-group 34 | :::code-group-item effect.spec.ts 35 | 36 | ```ts 37 | // src/reactivity/__tests__/effect.spec.ts 38 | 39 | it('renturn a runner when call effect', () => { 40 | let foo = 10 41 | const runner = effect(() => { 42 | foo++ 43 | return 'foo' 44 | }) 45 | // 传入 effect 的方法 fn 会被立即执行一次,所以foo = 11 46 | expect(foo).toBe(11) 47 | const res = runner() 48 | // 执行 effect 返回的 runner 函数后,fn 会再次执行,所以foo = 12 49 | expect(foo).toBe(12) 50 | // runner 会返回 fn 的执行结果,所以 res = 'foo' 51 | expect(res).toBe('foo') 52 | }) 53 | ``` 54 | 55 | ::: 56 | :::: 57 | 58 | 接下来,我们便可以根据测试用例来对 `effect` 方法进行改造。我们先来看下现在的 `effect` 方法定义: 59 | 60 | :::: code-group 61 | :::code-group-item effect.ts 62 | 63 | ```ts 64 | // src/reactivity/effect.ts 65 | 66 | export function effect(fn) { 67 | // 实例化 ReactiveEffect 类,并将依赖传入 68 | const _effect = new ReactiveEffect(fn) 69 | 70 | _effect.run() 71 | } 72 | ``` 73 | 74 | ::: 75 | :::: 76 | 77 | 现在,`effect` 需要返回一个 `runner` 函数,并且在执行 `runner` 函数时会再次执行传入 `effect` 的方法 `fn`,而现在 `fn` 的执行是通过 `ReactiveEffect` 暴露出来的 `run` 方法实现的。所以,我们只需要将 `ReactiveEffect` 实例上的 `run` 暴露出去即可: 78 | 79 | ::: warning 注意 80 | 由于 `fn` 被绑定在 `ReactiveEffect` 的实例属性 `_fn` 上,且 `run` 通过 `this._fn()` 的方式来调用它,所以在将 `_effect.run` 暴露出去的时候需要绑定 `this` 作用域。 81 | ::: 82 | 83 | :::: code-group 84 | :::code-group-item effect.ts 85 | 86 | ```ts 87 | // src/reactivity/effect.ts 88 | 89 | export function effect(fn) { 90 | // 实例化 ReactiveEffect 类,并将依赖传入 91 | const _effect = new ReactiveEffect(fn) 92 | 93 | _effect.run() 94 | 95 | return _effect.run.bind(_effect) 96 | } 97 | ``` 98 | 99 | ::: 100 | :::: 101 | 102 | 现在,`effect` 方法已经可以返回 `runner` 函数了,那么我们该如何拿到 `fn` 的返回值呢?其实很简单,只需要改造下 `ReactiveEffect` 中的 `run` 方法,将 `_fn` 的执行结果返回即可: 103 | 104 | :::: code-group 105 | :::code-group-item effect.ts 106 | 107 | ```ts 108 | // src/reactivity/effect.ts 109 | 110 | class ReactiveEffect { 111 | private _fn: any 112 | 113 | constructor(fn) { 114 | this._fn = fn 115 | } 116 | 117 | run() { 118 | activeEffect = this 119 | return this._fn() 120 | } 121 | } 122 | ``` 123 | 124 | ::: 125 | :::: 126 | 127 | 至此,`runner` 功能就实现完成啦~ 128 | -------------------------------------------------------------------------------- /docs/.vuepress/.temp/internal/pagesRoutes.js: -------------------------------------------------------------------------------- 1 | export const pagesRoutes = [ 2 | ["v-8daa1a0e","/",{"title":""},["/index.html","/README.md"]], 3 | ["v-f4c6334e","/miniVue/",{"title":""},["/miniVue/index.html","/miniVue/README.md"]], 4 | ["v-402aedb2","/miniVue/notes/",{"title":"Vue3 源码学习"},["/miniVue/notes/index.html","/miniVue/notes/README.md"]], 5 | ["v-29a0bfe3","/miniVue/notes/prerequisites.html",{"title":"开始之前"},["/miniVue/notes/prerequisites","/miniVue/notes/prerequisites.md"]], 6 | ["v-aec07b30","/miniVue/notes/reactivity/01_reactive.html",{"title":"effect & reactive & 依赖收集 & 触发依赖"},["/miniVue/notes/reactivity/01_reactive","/miniVue/notes/reactivity/01_reactive.md"]], 7 | ["v-f6da2ddc","/miniVue/notes/reactivity/02_runner.html",{"title":"runner"},["/miniVue/notes/reactivity/02_runner","/miniVue/notes/reactivity/02_runner.md"]], 8 | ["v-b8c3a658","/miniVue/notes/reactivity/03_scheduler.html",{"title":"scheduler"},["/miniVue/notes/reactivity/03_scheduler","/miniVue/notes/reactivity/03_scheduler.md"]], 9 | ["v-05db1f42","/miniVue/notes/reactivity/04_stop.html",{"title":"stop"},["/miniVue/notes/reactivity/04_stop","/miniVue/notes/reactivity/04_stop.md"]], 10 | ["v-37892dc3","/miniVue/notes/reactivity/05_readonly.html",{"title":"readonly"},["/miniVue/notes/reactivity/05_readonly","/miniVue/notes/reactivity/05_readonly.md"]], 11 | ["v-bc670b30","/miniVue/notes/reactivity/06_reactiveOrReadonly.html",{"title":"isReactive & isReadOnly"},["/miniVue/notes/reactivity/06_reactiveOrReadonly","/miniVue/notes/reactivity/06_reactiveOrReadonly.md"]], 12 | ["v-464fa7c2","/miniVue/notes/reactivity/07_nestedReactiveAndReadonly.html",{"title":"reactive & readonly 对象嵌套功能"},["/miniVue/notes/reactivity/07_nestedReactiveAndReadonly","/miniVue/notes/reactivity/07_nestedReactiveAndReadonly.md"]], 13 | ["v-ec2b32d8","/miniVue/notes/reactivity/08_shallowReadonly.html",{"title":"shallowReadonly"},["/miniVue/notes/reactivity/08_shallowReadonly","/miniVue/notes/reactivity/08_shallowReadonly.md"]], 14 | ["v-453eef05","/miniVue/notes/reactivity/09_isProxy.html",{"title":"isProxy"},["/miniVue/notes/reactivity/09_isProxy","/miniVue/notes/reactivity/09_isProxy.md"]], 15 | ["v-58f7d3e0","/miniVue/notes/reactivity/10_ref.html",{"title":"ref"},["/miniVue/notes/reactivity/10_ref","/miniVue/notes/reactivity/10_ref.md"]], 16 | ["v-ff176422","/miniVue/notes/reactivity/11_isRef%20&%20unRef.html",{"title":"isRef & unRef"},["/miniVue/notes/reactivity/11_isRef & unRef.html","/miniVue/notes/reactivity/11_isRef%20&%20unRef","/miniVue/notes/reactivity/11_isRef & unRef.md","/miniVue/notes/reactivity/11_isRef%20&%20unRef.md"]], 17 | ["v-6486207a","/miniVue/notes/reactivity/12_proxyRefs.html",{"title":"proxyRefs"},["/miniVue/notes/reactivity/12_proxyRefs","/miniVue/notes/reactivity/12_proxyRefs.md"]], 18 | ["v-79913d35","/miniVue/notes/reactivity/13_computed.html",{"title":"computed"},["/miniVue/notes/reactivity/13_computed","/miniVue/notes/reactivity/13_computed.md"]], 19 | ["v-dae0fb94","/miniVue/notes/runtime-core/01_initComponent.html",{"title":"初始化 Component"},["/miniVue/notes/runtime-core/01_initComponent","/miniVue/notes/runtime-core/01_initComponent.md"]], 20 | ["v-3706649a","/404.html",{"title":""},["/404"]], 21 | ] 22 | -------------------------------------------------------------------------------- /docs/miniVue/notes/reactivity/12_proxyRefs.md: -------------------------------------------------------------------------------- 1 | 8 | # proxyRefs 9 | 10 | :::tip 11 | 本篇笔记对应的分支号为: `main分支: c6f5fd5` 12 | ::: 13 | 14 | 写过 Vue3 代码的小伙伴们应该知道,当我们在 `template` 中使用 `setup` 返回的 `ref` 对象的值时,是不需要通过 `.value` 的方式来获取的。之所以可以这么做,是因为 `proxyRefs` 对 `ref` 对象进行了处理。 15 | 16 | 我们先通过测试用例来看看 `proxyRefs` 都实现了哪些功能: 17 | 18 | :::: code-group 19 | ::: code-group-item ref.spec.ts 20 | 21 | ```ts 22 | // src/reactivity/__tests__/ref.spec.ts 23 | 24 | it('proxyRefss', () => { 25 | const user = { 26 | age: ref(10), 27 | name: 'xiaohong' 28 | } 29 | // 调用 proxyUser 后,无需通过 .value 的方式获取 ref 的值 30 | const proxyUser = proxyRefs(user) 31 | expect(user.age.value).toBe(10) 32 | expect(proxyUser.age).toBe(10) 33 | expect(proxyUser.name).toBe('xiaohong') 34 | 35 | proxyUser.age = 20 36 | expect(proxyUser.age).toBe(20) 37 | expect(user.age.value).toBe(20) 38 | 39 | proxyUser.age = ref(10) 40 | expect(proxyUser.age).toBe(10) 41 | expect(user.age.value).toBe(10) 42 | }) 43 | ``` 44 | 45 | ::: 46 | :::: 47 | 48 | 通过测试用例我们可以得知: 49 | 50 | > 1. 当调用了 `proxyRefs` 方法后,可以不通过 `.value` 的方式获取 `ref` 对象上的值; 51 | > 2. 当给 `proxyRefs` 方法返回值中的 `ref` 对象赋值时,如果是个基本类型, 则修改 `ref` 对象上的 value 值,而如果所赋的值是个 `ref` 对象,则直接覆盖。 52 | 53 | 我们先来实现第一个功能。通过方法名称我们可以知道,`proxyRefs` 返回的是 `proxy` 对象。第一个功能比较简单,既然调用它之后不用使用 `.value` 的方式来获取 `ref` 上的值,那么我们直接将 `.value` 的值返回即可。而针对非 `ref` 对象,只需要将其原样返回。这个功能看着是不是有点眼熟?其实就是上一节我们实现的 `unRef`,这里我们直接使用就行: 54 | 55 | :::: code-group 56 | ::: code-group-item ref.ts 57 | 58 | ```ts 59 | // src/reactivity/ref.ts 60 | 61 | /** 62 | * template 中调用此方法,目的是在页面中无需通过 .value 的方式获取 ref 的值 63 | * @param objectWithRefs 64 | * @returns 65 | */ 66 | export function proxyRefs(objectWithRefs) { 67 | return new Proxy(objectWithRefs, { 68 | get(target, key) { 69 | return unRef(Reflect.get(target, key)) 70 | } 71 | }) 72 | } 73 | ``` 74 | 75 | ::: 76 | :::: 77 | 78 | 接下来我们一起实现下第二个功能。第二个功能拆分下来其实就是以下几种情况: 79 | 80 | > 1. 当给原先是 `ref` 对象的属性设置 **非 `ref` 对象** 的值时,需要修改其 `.value` 的值; 81 | > 2. 当给原先是 `ref` 对象的属性设置 **`ref` 对象** 的值时,直接覆盖; 82 | > 3. 当给原先不是 `ref` 对象的属性设置 **非 `ref` 对象** 的值时,直接覆盖; 83 | > 4. 当给原先不是 `ref` 对象的属性设置 **`ref` 对象** 的值时,直接覆盖; 84 | 85 | 因此,我们只需要对当前修改的属性值类型以及传入的值类型进行判断即可,**当给原先是 `ref` 对象的属性设置非 `ref` 对象的值时,需要修改其 `.value` 的值, 否则直接进行覆盖。** 由此我们可以对 `set` 的逻辑进行补充,这里对 `ref` 对象的判断可以使用我们上一节实现的 `isRef` 方法: 86 | 87 | :::: code-group 88 | ::: code-group-item ref.ts 89 | 90 | ```ts 91 | // src/reactivity/ref.ts 92 | 93 | /** 94 | * template 中调用此方法,目的是在页面中无需通过 .value 的方式获取 ref 的值 95 | * @param objectWithRefs 96 | * @returns 97 | */ 98 | export function proxyRefs(objectWithRefs) { 99 | return new Proxy(objectWithRefs, { 100 | get(target, key) { 101 | return unRef(Reflect.get(target, key)) 102 | } 103 | 104 | set(target, key, value) { 105 | if (isRef(target[key]) && !isRef(value)) { 106 | return target[key].value = value 107 | } else { 108 | return Reflect.set(target, key, value) 109 | } 110 | } 111 | }) 112 | } 113 | ``` 114 | 115 | ::: 116 | :::: 117 | 118 | 至此,`proxyRefs` 功能就实现完成啦~ 119 | -------------------------------------------------------------------------------- /src/reactivity/effect.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: ext.luhaifeng1 ext.luhaifeng1@jd.com 3 | * @Date: 2021-11-14 18:34:45 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-07-05 14:54:48 6 | * @Description: 7 | */ 8 | 9 | import { extend } from '../shared' 10 | 11 | let activeEffect 12 | let shouldTrack = false // 标记是否应该进行收集 13 | export class ReactiveEffect { 14 | private _fn: any 15 | public scheduler: Function | undefined 16 | active = true // 是否需要清空 deps 17 | onStop?: () => void 18 | deps = [] 19 | 20 | constructor(fn, scheduler?: Function) { 21 | this._fn = fn 22 | this.scheduler = scheduler 23 | } 24 | 25 | run() { 26 | if (!this.active) { 27 | return this._fn() 28 | } 29 | 30 | shouldTrack = true 31 | activeEffect = this 32 | const result = this._fn() 33 | shouldTrack = false 34 | 35 | return result 36 | } 37 | 38 | stop() { 39 | if (this.active) { 40 | cleanEffect(this) 41 | if (this.onStop) { 42 | this.onStop() 43 | } 44 | this.active = false 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * 清空 deps 51 | * @param effect effct 对象 52 | */ 53 | function cleanEffect(effect) { 54 | effect.deps.forEach((dep: any) => { 55 | dep.delete(effect) 56 | }) 57 | // 清空deps 58 | effect.deps.length = 0 59 | } 60 | 61 | const targetMap = new Map() // 存放依赖映射关系 62 | 63 | // 判断是否在收集中 64 | export function isTracking() { 65 | return shouldTrack && activeEffect !== undefined 66 | } 67 | 68 | export function trackEffects(deps: Set) { 69 | // 避免重复收集 70 | if (deps.has(activeEffect)) return 71 | // 将依赖对象保存到列 72 | deps.add(activeEffect) 73 | activeEffect.deps.push(deps) 74 | } 75 | 76 | /** 77 | * 收集依赖 78 | * @param target 需要收集依赖的对象 79 | * @param key 收集该key所对应的依赖 80 | */ 81 | export function track(target, key) { 82 | if (!isTracking()) return 83 | // 查找该对象对应的依赖池 84 | let depsMap = targetMap.get(target) 85 | // 如果没有(首次初始化时),则创建新的依赖池 86 | if (!depsMap) { 87 | depsMap = new Map() 88 | targetMap.set(target, depsMap) 89 | } 90 | // 从获取到的依赖池中获取该key所对应的依赖列表 91 | let deps = depsMap.get(key) 92 | // 如果没有,则新建一个该key对应的列表 93 | if (!deps) { 94 | deps = new Set() 95 | depsMap.set(key, deps) 96 | } 97 | trackEffects(deps) 98 | } 99 | 100 | export function triggerEffects(deps) { 101 | for(const dep of deps) { 102 | // 判断是否存在 scheduler 方法,存在的的话执行 scheduler,否则执行run 103 | if(dep.scheduler) { 104 | dep.scheduler() 105 | } else { 106 | dep.run() 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * 触发依赖 113 | * @param target 触发依赖的对象 114 | * @param key 触发该key对应的依赖 115 | */ 116 | export function trigger(target, key) { 117 | // 根据对象与key获取到所有的依赖,并执行 118 | const depsMap = targetMap.get(target) 119 | // 如果没有找到 depsMap, 直接 return 120 | if (!depsMap) { 121 | return 122 | } 123 | const deps = depsMap.get(key) 124 | triggerEffects(deps) 125 | } 126 | 127 | /** 128 | * 129 | * @param fn 130 | * @param options 131 | * @returns runner 132 | */ 133 | export function effect(fn, options: any = {}) { 134 | const _effect = new ReactiveEffect(fn, options.scheduler) 135 | extend(_effect, options) 136 | 137 | _effect.run() 138 | 139 | const runner: any = _effect.run.bind(_effect) 140 | runner.effect = _effect 141 | 142 | return runner 143 | } 144 | 145 | /** 146 | * 停止 effect 147 | * @param runner 148 | */ 149 | export function stop(runner) { 150 | runner.effect.stop() 151 | } 152 | -------------------------------------------------------------------------------- /docs/miniVue/notes/reactivity/03_scheduler.md: -------------------------------------------------------------------------------- 1 | 8 | # scheduler 9 | 10 | ::: tip 11 | 本篇笔记对应的分支号为: `main分支:1b55902` 12 | ::: 13 | 14 | `effect` 方法的第二个入参是一个对象,该对象的其中一个属性就是 `scheduler`。 15 | 16 | ## 什么是 scheduler 17 | 18 | `scheduler` 是个函数,在首次调用 `effect` 时不会被执行。传入 `scheduler` 后,当响应式对象的属性发生变化时,`effect` 的第一个参数 `fn` 不会再次执行,而是会执行 `scheduler`。如果需要执行 `fn`, 需要执行 `effect` 返回的 `runner` 函数。 19 | 20 | ## 实现 scheduler 21 | 22 | 我们先来编写 `scheduler` 的测试用例: 23 | 24 | :::: code-group 25 | ::: code-group-item effect.spec.ts 26 | 27 | ``` ts 28 | // src/reactivity/__tests__/effect.spec.ts 29 | 30 | it('scheduler', () => { 31 | let dummy 32 | let run: any 33 | const scheduler = jest.fn(() => { 34 | run = runner 35 | }) 36 | const obj = reactive({ foo: 1 }) 37 | const runner = effect(() => { 38 | dummy = obj.foo 39 | }, { scheduler }) 40 | // 首次执行 effect 时不会调用 scheduler 方法 41 | expect(scheduler).not.toHaveBeenCalled() 42 | // 首次执行 effect 时会调用传入的第一个方法,此时给 dummy 赋值 43 | expect(dummy).toBe(1) 44 | // 触发响应式对象值改变 45 | obj.foo++ 46 | // 会执行 scheduler 方法,但是不会执行第一个参数 47 | expect(scheduler).toHaveBeenCalledTimes(1) 48 | expect(dummy).toBe(1) 49 | // 执行 runner 方法时,会执行传入的第一个方法 50 | run() 51 | expect(dummy).toBe(2) 52 | }) 53 | ``` 54 | 55 | ::: 56 | :::: 57 | 58 | 接下来,我们根据测试用例来改造下 `effect` 方法。 59 | 60 | 首先,我们需要给 `effect` 添加第二个参数。第二个参数是个对象,可以不传,所以我们设置下默认值为空对象: 61 | 62 | :::: code-group 63 | ::: code-group-item effect.ts 64 | 65 | ``` ts {3} 66 | // src/reactivity/effect.ts 67 | 68 | export function effect (fn, options: any = {}) { 69 | const _effect = new ReactiveEffect(fn) 70 | 71 | _effect.run() 72 | 73 | return _effect.run.bind(_effect) 74 | } 75 | ``` 76 | 77 | ::: 78 | :::: 79 | 80 | 由于传入 `scheduler` 后,传入 `effect` 的方法 `fn` 在响应式对象的属性发生变化时不会被再次触发,因此我们需要在 `trigger` 阶段进行判断 **是否传入了 `scheduler`**。 81 | 82 | 但是 `scheduler` 是在调用 `effect` 方法时传入的,那在 `trigger` 阶段如何判断是否传入了 `scheduler` 呢? 83 | 84 | 我们知道, `trigger` 阶段触发的是 `ReactiveEffect` 实例中的 `run` 方法,因此,我们可以在实例化 `ReactiveEffect` 阶段将 `scheduler` 传入,这样便可以在 `trigger` 时进行判断。 85 | 86 | :::: code-group 87 | ::: code-group-item effect.ts 88 | 89 | ``` ts {7,19} 90 | // src/reactivity/effect.ts 91 | 92 | class ReactiveEffect { 93 | private _fn: any 94 | public scheduler: Function | undefined 95 | 96 | constructor(fn, scheduler?: Function) { 97 | this._fn = fn 98 | this.scheduler = scheduler 99 | } 100 | 101 | run() { 102 | activeEffect = this 103 | return this._fn() 104 | } 105 | } 106 | 107 | export function effect (fn, options: any = {}) { 108 | const _effect = new ReactiveEffect(fn, options.scheduler) 109 | 110 | _effect.run() 111 | 112 | return _effect.run.bind(_effect) 113 | } 114 | ``` 115 | 116 | ::: 117 | :::: 118 | 119 | 最后,我们只需要在 `trigger` 时判断对应的 `ReactiveEffect` 实例中是否存在 `scheduler` 即可,如果存在 `scheduler`,则执行 `scheduler` 方法,否则执行 `run`: 120 | 121 | :::: code-group 122 | ::: code-group-item effect.ts 123 | 124 | ``` ts {14} 125 | // src/reactivity/effect.ts 126 | 127 | /** 128 | * 触发依赖 129 | * @param target 触发依赖的对象 130 | * @param key 触发该key对应的依赖 131 | */ 132 | export function trigger(target, key) { 133 | // 根据对象与key获取到所有的依赖,并执行 134 | const depsMap = targetMap.get(target) 135 | const deps = depsMap.get(key) 136 | for(const dep of deps) { 137 | // 判断是否存在 scheduler 方法,存在的的话执行 scheduler,否则执行run 138 | if(dep.scheduler) { 139 | dep.scheduler() 140 | } else { 141 | dep.run() 142 | } 143 | } 144 | } 145 | ``` 146 | 147 | ::: 148 | :::: 149 | -------------------------------------------------------------------------------- /docs/miniVue/notes/reactivity/08_shallowReadonly.md: -------------------------------------------------------------------------------- 1 | 8 | # shallowReadonly 9 | 10 | ::: tip 11 | 本篇笔记对应的分支号为: `main分支: 4c145d8` 12 | ::: 13 | 14 | 这里引用官方文档中关于 [shallowReadonly](https://v3.cn.vuejs.org/api/basic-reactivity.html#shallowreadonly) 的描述: 15 | 16 | ::: tip 概念 17 | 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。 18 | ::: 19 | 20 | `shallow` 本身有 `浅层,表层` 的意思。那这句话是什么意思呢?拆开来理解的话,`shallowReadonly` 主要包含以下两个特征: 21 | 22 | > 1. `shallowReadonly` 返回的是一个 `readonly` 对象; 23 | > 2. `shallowReadonly` 返回的对象内嵌套的对象 **不是** `readonly` 对象; 24 | 25 | 我们通过测试用例来加深下理解: 26 | 27 | :::: code-group 28 | ::: code-group-item shallowReadonly.spec.ts 29 | 30 | ```ts 31 | // src/reactivity/__tests/shallowReadonly.spec.ts 32 | 33 | import { shallowReadonly, isReadonly } from '../reactive' 34 | 35 | describe('shallowReadonly', () => { 36 | test('should not make non-reactive properties reactive', () => { 37 | const props = shallowReadonly({ n: { foo:1 }}) 38 | // 最外层的对象是 readonly 对象 39 | expect(isReadonly(props)).toBe(true) 40 | // 内部嵌套的对象不是 readonly 对象 41 | expect(isReadonly(props.n)).toBe(false) 42 | }) 43 | }) 44 | ``` 45 | 46 | ::: 47 | :::: 48 | 49 | 现在我们已经知道了 `shallowReadonly` 的功能,那我们该如何实现它呢? 50 | 51 | 还记得我们是怎么实现 `readonly` 的么?`createGetter` 是否返回 `readonly` 对象取决于传入的第一个参数 `isReadonly` 是否为 `true`。我们可以如法炮制,给 `createGetter` 传入第二个参数: `isShallow` 用于标记是否需要返回 `shallow` 对象,之后根据该值进行判断: **如果 `isShallow === true`, 直接返回 `res` , 否则保持原有逻辑。**: 52 | 53 | :::: code-group 54 | ::: code-group-item baseHandlers.ts 55 | 56 | ```ts{20} 57 | // src/reactivity/baseHandlers.ts 58 | 59 | /** 60 | * 用于生成 get 方法 61 | * @param isReadonly 是否是 readonly 对象 62 | * @returns 63 | */ 64 | function createGetter(isReadonly = false, isShallow = false) { 65 | return function(target, key) { 66 | // 判断是否是 reactive 对象 67 | if (key === ReactiveFlags.IS_REACTIVE) { 68 | return !isReadonly 69 | } else if (key === ReactiveFlags.IS_READONLY) { 70 | // 判断是否是 readonly 对象 71 | return isReadonly 72 | } 73 | const res = Reflect.get(target, key) 74 | 75 | // 如果是 shallow,直接返回结果 76 | if (isShallow) return res 77 | 78 | if (isObject(res)) { 79 | return isReadonly ? readonly(res) : reactive(res) 80 | } 81 | // 收集依赖 82 | !isReadonly && track(target, key) 83 | return res 84 | } 85 | } 86 | ``` 87 | 88 | ::: 89 | :::: 90 | 91 | 在此之后,我们需要为 `shallowReadonly` 定义 `shallowReadonlyHandlers`。由于 `shallowReadonly` 返回的也是 `readonly` 对象,它与 `readonly` 的唯一区别在于 `createGetter` 的第二个参数是否是 `true`。因此,我们可以复用之前定义的 `readonlyHandlers`。 92 | 93 | 还记得我们之前抽取的 `extend` 公共方法么? 这里它又派上用场啦: 94 | 95 | :::: code-group 96 | ::: code-group-item baseHandlers.ts 97 | 98 | ```ts 99 | // src/reactivity/baseHandlers.ts 100 | 101 | import { isObject, extend } from '../shared' 102 | 103 | const shallowReadonlyGet = createGetter(true, true) 104 | 105 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 106 | get: shallowReadonlyGet 107 | }) 108 | ``` 109 | 110 | ::: 111 | :::: 112 | 113 | 最后,我们只需要在 `reactive.ts` 中定义 `shallowReadonly` 方法即可: 114 | 115 | :::: code-group 116 | ::: code-group-item reactive.ts 117 | 118 | ```ts 119 | // src/reactivity/reactive.ts 120 | 121 | import { mutableHandlers, readonlyHandlers, shallowReadonlyHandlers } from './baseHandlers' 122 | 123 | /** 124 | * 创建 shallowReadonly 对象 125 | * @param raw 126 | * @returns 127 | */ 128 | export const shallowReadonly = raw => { 129 | return createActiveObject(raw, shallowReadonlyHandlers) 130 | } 131 | ``` 132 | 133 | ::: 134 | :::: 135 | -------------------------------------------------------------------------------- /docs/miniVue/notes/reactivity/07_nestedReactiveAndReadonly.md: -------------------------------------------------------------------------------- 1 | 8 | # reactive & readonly 对象嵌套功能 9 | 10 | ::: tip 11 | 本篇笔记对应的分支号为: `main分支: 26af116` 12 | ::: 13 | 14 | 截止目前为止,我们已经实现了 `reactive` 以及 `readonly` 功能。我们知道,在 `Vue3` 中,响应式对象内部嵌套的对象也应该是响应式的,那我们目前实现的功能是否符合这个特点呢?我们来一起验证下。 15 | 16 | ## nested reactive 17 | 18 | 先来看下 `reactive`,我们先来实现下它的测试用例: 19 | 20 | :::: code-group 21 | ::: code-group-item reactive.spec.ts 22 | 23 | ```ts 24 | // src/reactivity/__tests__/reactive.spec.ts 25 | 26 | it('nested reactive', () => { 27 | const original = { 28 | nested: { 29 | foo: 1 30 | }, 31 | array: [{ bar: 2 }] 32 | } 33 | const observed = reactive(original) 34 | expect(isReactive(observed.nested)).toBe(true) 35 | expect(isReactive(observed.array)).toBe(true) 36 | expect(isReactive(observed.array[0])).toBe(true) 37 | }) 38 | ``` 39 | 40 | ::: 41 | :::: 42 | 43 | ![nested_reactive](https://user-images.githubusercontent.com/9375823/176340959-9a38d9ef-f5b1-4504-b809-91d2c0cc741d.png) 44 | 45 | 我们可以看到,测试没有通过。这是为什么呢? 46 | 47 | 那是因为我们在获取对象属性时, `createGetter` 内部直接返回了 `Reflect.get(target, key)` ,但是没有判断该属性的值是否是个对象,也没有对其做相应的响应式处理。 48 | 49 | 因此,我们需要判断属性值的类型,如果是对象,则需要对其做响应式处理。我们先来定义一个 `isObject` 方法用于判断传入的值是否是对象类型,之后在 `createGetter` 中使用: 50 | 51 | :::: code-group 52 | ::: code-group-item index.ts 53 | 54 | ```ts 55 | // src/reactivity/shared/index.ts 56 | 57 | export function isObject(val) { 58 | return val !== null && typeof(val) === 'object' 59 | } 60 | ``` 61 | 62 | ::: 63 | 64 | ::: code-group-item baseHandlers.ts 65 | 66 | ```ts {3-4,22-24} 67 | // src/reactivity/baseHandlers.ts 68 | 69 | import { ReactiveFlags, reactive } from './reactive' 70 | import { isObject } from '../shared' 71 | 72 | /** 73 | * 用于生成 get 方法 74 | * @param isReadonly 是否是 readonly 对象 75 | * @returns 76 | */ 77 | function createGetter(isReadonly = false) { 78 | return function(target, key) { 79 | // 判断是否是 reactive 对象 80 | if (key === ReactiveFlags.IS_REACTIVE) { 81 | return !isReadonly 82 | } else if (key === ReactiveFlags.IS_READONLY) { 83 | // 判断是否是 readonly 对象 84 | return isReadonly 85 | } 86 | const res = Reflect.get(target, key) 87 | 88 | if (isObject(res)) { 89 | return reactive(res) 90 | } 91 | // 收集依赖 92 | !isReadonly && track(target, key) 93 | return res 94 | } 95 | } 96 | ``` 97 | 98 | ::: 99 | :::: 100 | 101 | 这样一来,就符合了 **响应式对象内部嵌套的对象也是响应式的** 特征。 102 | 103 | ## nested readonly 104 | 105 | 同理,我们再来看一看 `readonly` 的实现是否符合预期: 106 | 107 | :::: code-group 108 | ::: code-group-item readonly.ts 109 | 110 | ```ts{10-11} 111 | // src/reactivity/__tests__/readonly.spec.ts 112 | 113 | it('happy path', () => { 114 | const original = { foo: 1, bar: { baz: 2 }} 115 | const wrapped = readonly(original) 116 | expect(wrapped).not.toBe(original) 117 | expect(wrapped.foo).toBe(1) 118 | // 判断是否是 readonly 对象 119 | expect(isReadonly(original)).toBe(false) 120 | expect(isReadonly(original.bar)).toBe(false) 121 | expect(isReadonly(wrapped.bar)).toBe(true) 122 | expect(isReadonly(wrapped)).toBe(true) 123 | }) 124 | ``` 125 | 126 | ::: 127 | :::: 128 | 129 | ![nested_readonly](https://user-images.githubusercontent.com/9375823/176342418-01bf7be1-ca54-449a-804d-96cd219d91c1.png) 130 | 131 | 哦豁,梅开二度! 132 | 133 |
134 | 135 | 136 | 137 |
138 | 139 | 其实道理是一样的,依旧是在 `createGetter` 时没有判断值类型。因此,我们只需要在刚才进行对象类型判断之处再加上 `isReadonly` 的判断即可。如果 `isReadonly === true` 且当前属性的值是对象类型,则返回 `readonly(res)`: 140 | 141 | :::: code-group 142 | ::: code-group-item baseHandlers.ts 143 | 144 | ```ts{13} 145 | // src/reactivity/baseHandlers.ts 146 | 147 | /** 148 | * 用于生成 get 方法 149 | * @param isReadonly 是否是 readonly 对象 150 | * @returns 151 | */ 152 | function createGetter(isReadonly = false) { 153 | return function(target, key) { 154 | // 省略一大波代码 155 | 156 | if (isObject(res)) { 157 | return isReadonly ? readonly(res) : reactive(res) 158 | } 159 | } 160 | } 161 | ``` 162 | 163 | ::: 164 | :::: 165 | -------------------------------------------------------------------------------- /docs/.vuepress/.temp/internal/pagesData.js: -------------------------------------------------------------------------------- 1 | export const pagesData = { 2 | // path: / 3 | "v-8daa1a0e": () => import(/* webpackChunkName: "v-8daa1a0e" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/index.html.js").then(({ data }) => data), 4 | // path: /miniVue/ 5 | "v-f4c6334e": () => import(/* webpackChunkName: "v-f4c6334e" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/index.html.js").then(({ data }) => data), 6 | // path: /miniVue/notes/ 7 | "v-402aedb2": () => import(/* webpackChunkName: "v-402aedb2" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/index.html.js").then(({ data }) => data), 8 | // path: /miniVue/notes/prerequisites.html 9 | "v-29a0bfe3": () => import(/* webpackChunkName: "v-29a0bfe3" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/prerequisites.html.js").then(({ data }) => data), 10 | // path: /miniVue/notes/reactivity/01_reactive.html 11 | "v-aec07b30": () => import(/* webpackChunkName: "v-aec07b30" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/01_reactive.html.js").then(({ data }) => data), 12 | // path: /miniVue/notes/reactivity/02_runner.html 13 | "v-f6da2ddc": () => import(/* webpackChunkName: "v-f6da2ddc" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/02_runner.html.js").then(({ data }) => data), 14 | // path: /miniVue/notes/reactivity/03_scheduler.html 15 | "v-b8c3a658": () => import(/* webpackChunkName: "v-b8c3a658" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/03_scheduler.html.js").then(({ data }) => data), 16 | // path: /miniVue/notes/reactivity/04_stop.html 17 | "v-05db1f42": () => import(/* webpackChunkName: "v-05db1f42" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/04_stop.html.js").then(({ data }) => data), 18 | // path: /miniVue/notes/reactivity/05_readonly.html 19 | "v-37892dc3": () => import(/* webpackChunkName: "v-37892dc3" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/05_readonly.html.js").then(({ data }) => data), 20 | // path: /miniVue/notes/reactivity/06_reactiveOrReadonly.html 21 | "v-bc670b30": () => import(/* webpackChunkName: "v-bc670b30" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/06_reactiveOrReadonly.html.js").then(({ data }) => data), 22 | // path: /miniVue/notes/reactivity/07_nestedReactiveAndReadonly.html 23 | "v-464fa7c2": () => import(/* webpackChunkName: "v-464fa7c2" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/07_nestedReactiveAndReadonly.html.js").then(({ data }) => data), 24 | // path: /miniVue/notes/reactivity/08_shallowReadonly.html 25 | "v-ec2b32d8": () => import(/* webpackChunkName: "v-ec2b32d8" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/08_shallowReadonly.html.js").then(({ data }) => data), 26 | // path: /miniVue/notes/reactivity/09_isProxy.html 27 | "v-453eef05": () => import(/* webpackChunkName: "v-453eef05" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/09_isProxy.html.js").then(({ data }) => data), 28 | // path: /miniVue/notes/reactivity/10_ref.html 29 | "v-58f7d3e0": () => import(/* webpackChunkName: "v-58f7d3e0" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/10_ref.html.js").then(({ data }) => data), 30 | // path: /miniVue/notes/reactivity/11_isRef%20&%20unRef.html 31 | "v-ff176422": () => import(/* webpackChunkName: "v-ff176422" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/11_isRef & unRef.html.js").then(({ data }) => data), 32 | // path: /miniVue/notes/reactivity/12_proxyRefs.html 33 | "v-6486207a": () => import(/* webpackChunkName: "v-6486207a" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/12_proxyRefs.html.js").then(({ data }) => data), 34 | // path: /miniVue/notes/reactivity/13_computed.html 35 | "v-79913d35": () => import(/* webpackChunkName: "v-79913d35" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/13_computed.html.js").then(({ data }) => data), 36 | // path: /miniVue/notes/runtime-core/01_initComponent.html 37 | "v-dae0fb94": () => import(/* webpackChunkName: "v-dae0fb94" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/runtime-core/01_initComponent.html.js").then(({ data }) => data), 38 | // path: /404.html 39 | "v-3706649a": () => import(/* webpackChunkName: "v-3706649a" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/404.html.js").then(({ data }) => data), 40 | } 41 | -------------------------------------------------------------------------------- /docs/.vuepress/.temp/internal/pagesComponents.js: -------------------------------------------------------------------------------- 1 | import { defineAsyncComponent } from 'vue' 2 | 3 | export const pagesComponents = { 4 | // path: / 5 | "v-8daa1a0e": defineAsyncComponent(() => import(/* webpackChunkName: "v-8daa1a0e" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/index.html.vue")), 6 | // path: /miniVue/ 7 | "v-f4c6334e": defineAsyncComponent(() => import(/* webpackChunkName: "v-f4c6334e" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/index.html.vue")), 8 | // path: /miniVue/notes/ 9 | "v-402aedb2": defineAsyncComponent(() => import(/* webpackChunkName: "v-402aedb2" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/index.html.vue")), 10 | // path: /miniVue/notes/prerequisites.html 11 | "v-29a0bfe3": defineAsyncComponent(() => import(/* webpackChunkName: "v-29a0bfe3" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/prerequisites.html.vue")), 12 | // path: /miniVue/notes/reactivity/01_reactive.html 13 | "v-aec07b30": defineAsyncComponent(() => import(/* webpackChunkName: "v-aec07b30" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/01_reactive.html.vue")), 14 | // path: /miniVue/notes/reactivity/02_runner.html 15 | "v-f6da2ddc": defineAsyncComponent(() => import(/* webpackChunkName: "v-f6da2ddc" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/02_runner.html.vue")), 16 | // path: /miniVue/notes/reactivity/03_scheduler.html 17 | "v-b8c3a658": defineAsyncComponent(() => import(/* webpackChunkName: "v-b8c3a658" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/03_scheduler.html.vue")), 18 | // path: /miniVue/notes/reactivity/04_stop.html 19 | "v-05db1f42": defineAsyncComponent(() => import(/* webpackChunkName: "v-05db1f42" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/04_stop.html.vue")), 20 | // path: /miniVue/notes/reactivity/05_readonly.html 21 | "v-37892dc3": defineAsyncComponent(() => import(/* webpackChunkName: "v-37892dc3" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/05_readonly.html.vue")), 22 | // path: /miniVue/notes/reactivity/06_reactiveOrReadonly.html 23 | "v-bc670b30": defineAsyncComponent(() => import(/* webpackChunkName: "v-bc670b30" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/06_reactiveOrReadonly.html.vue")), 24 | // path: /miniVue/notes/reactivity/07_nestedReactiveAndReadonly.html 25 | "v-464fa7c2": defineAsyncComponent(() => import(/* webpackChunkName: "v-464fa7c2" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/07_nestedReactiveAndReadonly.html.vue")), 26 | // path: /miniVue/notes/reactivity/08_shallowReadonly.html 27 | "v-ec2b32d8": defineAsyncComponent(() => import(/* webpackChunkName: "v-ec2b32d8" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/08_shallowReadonly.html.vue")), 28 | // path: /miniVue/notes/reactivity/09_isProxy.html 29 | "v-453eef05": defineAsyncComponent(() => import(/* webpackChunkName: "v-453eef05" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/09_isProxy.html.vue")), 30 | // path: /miniVue/notes/reactivity/10_ref.html 31 | "v-58f7d3e0": defineAsyncComponent(() => import(/* webpackChunkName: "v-58f7d3e0" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/10_ref.html.vue")), 32 | // path: /miniVue/notes/reactivity/11_isRef%20&%20unRef.html 33 | "v-ff176422": defineAsyncComponent(() => import(/* webpackChunkName: "v-ff176422" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/11_isRef & unRef.html.vue")), 34 | // path: /miniVue/notes/reactivity/12_proxyRefs.html 35 | "v-6486207a": defineAsyncComponent(() => import(/* webpackChunkName: "v-6486207a" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/12_proxyRefs.html.vue")), 36 | // path: /miniVue/notes/reactivity/13_computed.html 37 | "v-79913d35": defineAsyncComponent(() => import(/* webpackChunkName: "v-79913d35" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/reactivity/13_computed.html.vue")), 38 | // path: /miniVue/notes/runtime-core/01_initComponent.html 39 | "v-dae0fb94": defineAsyncComponent(() => import(/* webpackChunkName: "v-dae0fb94" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/miniVue/notes/runtime-core/01_initComponent.html.vue")), 40 | // path: /404.html 41 | "v-3706649a": defineAsyncComponent(() => import(/* webpackChunkName: "v-3706649a" */"/Users/luhaifeng/codes/mine/vue3-study/docs/.vuepress/.temp/pages/404.html.vue")), 42 | } 43 | -------------------------------------------------------------------------------- /lib/mini-vue.esm.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: luhaifeng666 youzui@hotmail.com 3 | * @Date: 2022-07-10 09:42:52 4 | * @LastEditors: luhaifeng666 5 | * @LastEditTime: 2022-08-01 08:54:23 6 | * @Description: 7 | */ 8 | function createVNode(type, props, children) { 9 | var vnode = { 10 | type: type, 11 | props: props, 12 | children: children, 13 | el: null 14 | }; 15 | return vnode; 16 | } 17 | 18 | /* 19 | * @Author: luhaifeng666 youzui@hotmail.com 20 | * @Date: 2022-08-01 09:01:09 21 | * @LastEditors: luhaifeng666 22 | * @LastEditTime: 2022-08-01 09:06:20 23 | * @Description: 24 | */ 25 | var publicPropertiesMap = { 26 | $el: function (i) { return i.vnode.el; } 27 | }; 28 | var PublicInstanceProxyHandlers = { 29 | get: function (_a, key) { 30 | var instance = _a._; 31 | var setupState = instance.setupState; 32 | // 通过 this.xxxx 获取值 33 | if (key in setupState) { 34 | return setupState[key]; 35 | } 36 | // 通过 this.$el 获取值 37 | var publicGetter = publicPropertiesMap[key]; 38 | if (publicGetter) { 39 | return publicGetter(instance); 40 | } 41 | } 42 | }; 43 | 44 | /* 45 | * @Author: luhaifeng666 youzui@hotmail.com 46 | * @Date: 2022-07-10 09:53:01 47 | * @LastEditors: luhaifeng666 48 | * @LastEditTime: 2022-08-01 09:04:00 49 | * @Description: 50 | */ 51 | function createComponentInstance(vnode) { 52 | var component = { 53 | vnode: vnode, 54 | type: vnode.type, 55 | setupState: {} 56 | }; 57 | return component; 58 | } 59 | function setupComponent(instance) { 60 | // initProps() 61 | // initSlots() 62 | setupStatefulComponent(instance); 63 | } 64 | function setupStatefulComponent(instance) { 65 | var Component = instance.type; 66 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 67 | var setup = Component.setup; 68 | if (setup) { 69 | var setupResult = setup(); 70 | handleSetupResult(instance, setupResult); 71 | } 72 | } 73 | function handleSetupResult(instance, setupResult) { 74 | if (typeof setupResult === 'object') { 75 | instance.setupState = setupResult; 76 | } 77 | finishComponentSetup(instance); 78 | } 79 | function finishComponentSetup(instance) { 80 | var Component = instance.type; 81 | if (Component.render) { 82 | instance.render = Component.render; 83 | } 84 | } 85 | 86 | /* 87 | * @Author: luhaifeng666 youzui@hotmail.com 88 | * @Date: 2022-07-10 09:44:49 89 | * @LastEditors: luhaifeng666 90 | * @LastEditTime: 2022-08-01 08:57:37 91 | * @Description: 92 | */ 93 | function render(vnode, container) { 94 | patch(vnode, container); 95 | } 96 | function patch(vnode, container) { 97 | var type = vnode.type; 98 | if (typeof type === 'string') { 99 | // 处理Element 100 | processElement(vnode, container); 101 | } 102 | else { 103 | // 处理组件 104 | processComponent(vnode, container); 105 | } 106 | } 107 | function processElement(vnode, container) { 108 | mountElement(vnode, container); 109 | } 110 | function mountElement(vnode, container) { 111 | var type = vnode.type, children = vnode.children, props = vnode.props; 112 | var el = (vnode.el = document.createElement(type)); 113 | if (typeof children === 'string') { 114 | el.textContent = children; 115 | } 116 | else if (Array.isArray(children)) { 117 | mountChildren(vnode, el); 118 | } 119 | for (var key in props) { 120 | var val = props[key]; 121 | el.setAttribute(key, val); 122 | } 123 | container.append(el); 124 | } 125 | function mountChildren(vnode, container) { 126 | vnode.children.forEach(function (child) { 127 | patch(child, container); 128 | }); 129 | } 130 | function processComponent(vnode, container) { 131 | mountComponent(vnode, container); 132 | } 133 | function mountComponent(vnode, container) { 134 | var instance = createComponentInstance(vnode); 135 | setupComponent(instance); 136 | setupRenderEffect(instance, vnode, container); 137 | } 138 | function setupRenderEffect(instance, vnode, container) { 139 | var proxy = instance.proxy; 140 | var subTree = instance.type.render.call(proxy); 141 | patch(subTree, container); 142 | vnode.el = subTree.el; 143 | } 144 | 145 | /* 146 | * @Author: luhaifeng666 youzui@hotmail.com 147 | * @Date: 2022-07-10 09:39:37 148 | * @LastEditors: luhaifeng666 149 | * @LastEditTime: 2022-07-10 10:37:59 150 | * @Description: 151 | */ 152 | function createApp(rootComponent) { 153 | return { 154 | mount: function (rootContainer) { 155 | var vnode = createVNode(rootComponent); 156 | render(vnode, rootContainer); 157 | } 158 | }; 159 | } 160 | 161 | /* 162 | * @Author: luhaifeng666 youzui@hotmail.com 163 | * @Date: 2022-07-16 15:33:50 164 | * @LastEditors: luhaifeng666 165 | * @LastEditTime: 2022-07-17 16:33:23 166 | * @Description: 167 | */ 168 | function h(type, props, children) { 169 | return createVNode(type, props, children); 170 | } 171 | 172 | export { createApp, h }; 173 | -------------------------------------------------------------------------------- /lib/mini-vue.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | /* 6 | * @Author: luhaifeng666 youzui@hotmail.com 7 | * @Date: 2022-07-10 09:42:52 8 | * @LastEditors: luhaifeng666 9 | * @LastEditTime: 2022-08-01 08:54:23 10 | * @Description: 11 | */ 12 | function createVNode(type, props, children) { 13 | var vnode = { 14 | type: type, 15 | props: props, 16 | children: children, 17 | el: null 18 | }; 19 | return vnode; 20 | } 21 | 22 | /* 23 | * @Author: luhaifeng666 youzui@hotmail.com 24 | * @Date: 2022-08-01 09:01:09 25 | * @LastEditors: luhaifeng666 26 | * @LastEditTime: 2022-08-01 09:06:20 27 | * @Description: 28 | */ 29 | var publicPropertiesMap = { 30 | $el: function (i) { return i.vnode.el; } 31 | }; 32 | var PublicInstanceProxyHandlers = { 33 | get: function (_a, key) { 34 | var instance = _a._; 35 | var setupState = instance.setupState; 36 | // 通过 this.xxxx 获取值 37 | if (key in setupState) { 38 | return setupState[key]; 39 | } 40 | // 通过 this.$el 获取值 41 | var publicGetter = publicPropertiesMap[key]; 42 | if (publicGetter) { 43 | return publicGetter(instance); 44 | } 45 | } 46 | }; 47 | 48 | /* 49 | * @Author: luhaifeng666 youzui@hotmail.com 50 | * @Date: 2022-07-10 09:53:01 51 | * @LastEditors: luhaifeng666 52 | * @LastEditTime: 2022-08-01 09:04:00 53 | * @Description: 54 | */ 55 | function createComponentInstance(vnode) { 56 | var component = { 57 | vnode: vnode, 58 | type: vnode.type, 59 | setupState: {} 60 | }; 61 | return component; 62 | } 63 | function setupComponent(instance) { 64 | // initProps() 65 | // initSlots() 66 | setupStatefulComponent(instance); 67 | } 68 | function setupStatefulComponent(instance) { 69 | var Component = instance.type; 70 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 71 | var setup = Component.setup; 72 | if (setup) { 73 | var setupResult = setup(); 74 | handleSetupResult(instance, setupResult); 75 | } 76 | } 77 | function handleSetupResult(instance, setupResult) { 78 | if (typeof setupResult === 'object') { 79 | instance.setupState = setupResult; 80 | } 81 | finishComponentSetup(instance); 82 | } 83 | function finishComponentSetup(instance) { 84 | var Component = instance.type; 85 | if (Component.render) { 86 | instance.render = Component.render; 87 | } 88 | } 89 | 90 | /* 91 | * @Author: luhaifeng666 youzui@hotmail.com 92 | * @Date: 2022-07-10 09:44:49 93 | * @LastEditors: luhaifeng666 94 | * @LastEditTime: 2022-08-01 08:57:37 95 | * @Description: 96 | */ 97 | function render(vnode, container) { 98 | patch(vnode, container); 99 | } 100 | function patch(vnode, container) { 101 | var type = vnode.type; 102 | if (typeof type === 'string') { 103 | // 处理Element 104 | processElement(vnode, container); 105 | } 106 | else { 107 | // 处理组件 108 | processComponent(vnode, container); 109 | } 110 | } 111 | function processElement(vnode, container) { 112 | mountElement(vnode, container); 113 | } 114 | function mountElement(vnode, container) { 115 | var type = vnode.type, children = vnode.children, props = vnode.props; 116 | var el = (vnode.el = document.createElement(type)); 117 | if (typeof children === 'string') { 118 | el.textContent = children; 119 | } 120 | else if (Array.isArray(children)) { 121 | mountChildren(vnode, el); 122 | } 123 | for (var key in props) { 124 | var val = props[key]; 125 | el.setAttribute(key, val); 126 | } 127 | container.append(el); 128 | } 129 | function mountChildren(vnode, container) { 130 | vnode.children.forEach(function (child) { 131 | patch(child, container); 132 | }); 133 | } 134 | function processComponent(vnode, container) { 135 | mountComponent(vnode, container); 136 | } 137 | function mountComponent(vnode, container) { 138 | var instance = createComponentInstance(vnode); 139 | setupComponent(instance); 140 | setupRenderEffect(instance, vnode, container); 141 | } 142 | function setupRenderEffect(instance, vnode, container) { 143 | var proxy = instance.proxy; 144 | var subTree = instance.type.render.call(proxy); 145 | patch(subTree, container); 146 | vnode.el = subTree.el; 147 | } 148 | 149 | /* 150 | * @Author: luhaifeng666 youzui@hotmail.com 151 | * @Date: 2022-07-10 09:39:37 152 | * @LastEditors: luhaifeng666 153 | * @LastEditTime: 2022-07-10 10:37:59 154 | * @Description: 155 | */ 156 | function createApp(rootComponent) { 157 | return { 158 | mount: function (rootContainer) { 159 | var vnode = createVNode(rootComponent); 160 | render(vnode, rootContainer); 161 | } 162 | }; 163 | } 164 | 165 | /* 166 | * @Author: luhaifeng666 youzui@hotmail.com 167 | * @Date: 2022-07-16 15:33:50 168 | * @LastEditors: luhaifeng666 169 | * @LastEditTime: 2022-07-17 16:33:23 170 | * @Description: 171 | */ 172 | function h(type, props, children) { 173 | return createVNode(type, props, children); 174 | } 175 | 176 | exports.createApp = createApp; 177 | exports.h = h; 178 | -------------------------------------------------------------------------------- /docs/miniVue/notes/reactivity/06_reactiveOrReadonly.md: -------------------------------------------------------------------------------- 1 | 8 | # isReactive & isReadOnly 9 | 10 | ::: tip 11 | 本篇笔记对应的分支号为: `main分支:abec34d` 12 | ::: 13 | 14 | 到目前为止,我们已经初步实现了 `reactive` 以及 `readonly` 的基本功能。现在,我们需要添加两个方法用于判断某个对象是否是 `reactive` 对象或者 `readonly` 对象。 15 | 16 | ## 实现 isReactive 17 | 18 | 在实现之前,我们先来补充下 `reactive` 的测试用例: 19 | 20 | :::: code-group 21 | ::: code-group-item reactive.spec.ts 22 | 23 | ```ts{3,15-16} 24 | // src/reactivity/__tests__/reactive.spec.ts 25 | 26 | import { reactive, isReactive } from '../reactive' 27 | 28 | describe('reactive', () => { 29 | it('happy path', () => { 30 | const origin = { num: 0 } 31 | // 通过 reactive 创建响应式对象 32 | const reactiveData = reactive(origin) 33 | // 判断响应式对象与原对象不是同一个对象 34 | expect(reactiveData).not.toBe(origin) 35 | // 代理对象中的 num 值应与原对象中的相同 36 | expect(reactiveData.num).toBe(0) 37 | // 判断是否是 reactive 对象 38 | expect(isReactive(origin)).toBe(false) 39 | expect(isReactive(reactiveData)).toBe(true) 40 | }) 41 | }) 42 | ``` 43 | 44 | ::: 45 | :::: 46 | 47 | 根据测试用例,`isReactive` 方法接受一个对象作为入参,返回值为布尔值,用于判断传入的对象是否是 `reactive` 对象。那我们通过什么可以判断当前对象是否是 `reactive` 对象呢? 48 | 49 | 试想一下,如果传入 `isReactive` 的是 `reactive` 对象,那么它必然是个 `Proxy` 对象。还记得在上一篇 `readonly` 功能的实现中,我们给 `createGetter` 方法传入了一个 `isReadonly` 标记,用于判断是否是 `readonly` 对象么。**如果当前对象是个 `Proxy` 对象,并且 `isReadonly === false`**, 那么不就说明当前对象是个 `reactive` 对象了么! 50 | 51 | 顺着这个思路,要想触发 `get` 方法,我们可以定义一个静态标记 `__v_isReactive` 用于判断是否是 `reactive` 对象。当我们尝试获取传入对象的 `__v_isReactive` 属性时,就会触发 `get` 方法,此时,我们只要返回 `!isReadOnly` 即可: 52 | 53 | :::: code-group 54 | ::: code-group-item reactive.ts 55 | 56 | ```ts 57 | // src/reactivity/reactive.ts 58 | 59 | export const enum ReactiveFlags { 60 | IS_REACTIVE = '__v_isReactive' 61 | } 62 | 63 | // 判断是否是 reactive 对象 64 | export const isReactive = value => value[ReactiveFlags.IS_REACTIVE] 65 | ``` 66 | 67 | ::: 68 | 69 | ::: code-group-item baseHandlers.ts 70 | 71 | ```ts {13-15} 72 | // src/reactivity/baseHandlers.ts 73 | 74 | import { ReactiveFlags } from './reactive' 75 | 76 | /** 77 | * 用于生成 get 方法 78 | * @param isReadonly 是否是 readonly 对象 79 | * @returns 80 | */ 81 | function createGetter(isReadonly = false) { 82 | return function(target, key) { 83 | // 判断是否是 reactive 对象 84 | if (key === ReactiveFlags.IS_REACTIVE) { 85 | return !isReadonly 86 | } 87 | const res = Reflect.get(target, key) 88 | // 收集依赖 89 | !isReadonly && track(target, key) 90 | return res 91 | } 92 | } 93 | ``` 94 | 95 | ::: 96 | :::: 97 | 98 | 现在,我们回过头来看下测试用例的运行结果,当我们传入的对象为普通对象时,测试用例报错,原因如下: 99 | 100 | ![isReactive](https://user-images.githubusercontent.com/9375823/175804230-c6534c26-1e43-449e-bdd6-0e7f12574d9d.png) 101 | 102 | 预期返回结果是 `false`,结果返回的却是 `undefined`。这是为什么呢? 103 | 104 | 那是因为普通对象并不是 `Proxy` 对象,直接访问 `__v_isReactive` 属性时,并不会触发 `get` 方法,而普通对象上又不存在 `__v_isReactive` 标记,因此测试用例不会通过。而实际上普通对象也的确不是 `reactive` 对象,因此,我们需要对 `isReactive` 方法进行改造,直接返回 `!!value[ReactiveFlags.IS_REACTIVE]` 即可: 105 | 106 | :::: code-group 107 | ::: code-group-item reactive.ts 108 | 109 | ```ts 110 | export const isReactive = value => !!value[ReactiveFlags.IS_REACTIVE] 111 | ``` 112 | 113 | ::: 114 | :::: 115 | 116 | ## 实现 isReadOnly 117 | 118 | `isReadOnly` 的判断方式与 `isReactive` 有异曲同工之妙,皆是通过 `isReadOnly` 来进行判断的,只不过不用取反罢了。 119 | 120 | 我们先来补充下 `isReadonly` 的测试用例: 121 | 122 | :::: code-group 123 | ::: code-group-item readonly.spec.ts 124 | 125 | ```ts{9-10} 126 | // src/reactivity/__tests__/readonly.spec.ts 127 | 128 | it('happy path', () => { 129 | const original = { foo: 1, bar: { bar: 2 }} 130 | const wrapped = readonly(original) 131 | expect(wrapped).not.toBe(original) 132 | expect(wrapped.foo).toBe(1) 133 | // 判断是否是 readonly 对象 134 | expect(isReadonly(original)).toBe(false) 135 | expect(isReadonly(wrapped)).toBe(true) 136 | }) 137 | ``` 138 | 139 | ::: 140 | :::: 141 | 142 | 因此,我们可以参照 `isReactive` 的实现方式,定义一个名为 `__v_isReadonly` 的静态标记,并在 `get` 方法中进行判断即可: 143 | 144 | :::: code-group 145 | ::: code-group-item reactive.ts 146 | 147 | ```ts 148 | // src/reactivity/reactive.ts 149 | 150 | export const enum ReactiveFlags { 151 | IS_REACTIVE = '__v_isReactive', 152 | IS_READONLY = '__v_isReadonly' 153 | } 154 | 155 | // 判断是否是 reactive 对象 156 | export const isReadonly = value => !!value[ReactiveFlags.IS_READONLY] 157 | 158 | // 判断是否是 readonly 对象 159 | export const isReadonly = value => !!value[ReactiveFlags.IS_READONLY] 160 | ``` 161 | 162 | ::: 163 | 164 | ::: code-group-item baseHandlers.ts 165 | 166 | ```ts {15-18} 167 | // src/reactivity/baseHandlers.ts 168 | 169 | import { ReactiveFlags } from './reactive' 170 | 171 | /** 172 | * 用于生成 get 方法 173 | * @param isReadonly 是否是 readonly 对象 174 | * @returns 175 | */ 176 | function createGetter(isReadonly = false) { 177 | return function(target, key) { 178 | // 判断是否是 reactive 对象 179 | if (key === ReactiveFlags.IS_REACTIVE) { 180 | return !isReadonly 181 | } else if (key === ReactiveFlags.IS_READONLY) { 182 | // 判断是否是 readonly 对象 183 | return isReadonly 184 | } 185 | const res = Reflect.get(target, key) 186 | // 收集依赖 187 | !isReadonly && track(target, key) 188 | return res 189 | } 190 | } 191 | ``` 192 | 193 | ::: 194 | :::: 195 | 196 | 至此,我们就实现了 `isReactive` 以及 `isReadOnly` 功能。 197 | -------------------------------------------------------------------------------- /docs/miniVue/notes/reactivity/13_computed.md: -------------------------------------------------------------------------------- 1 | 8 | # computed 9 | 10 | :::tip 11 | 本篇笔记对应的分支号为: `main分支: 139cae9` 12 | ::: 13 | 14 | `computed` 接受一个 getter 函数,并根据 getter 的返回值返回一个 **不可变** 的响应式 `ref` 对象。 15 | 16 | ::: tip 17 | 其实 `computed` 还可以接受一个具有 `get` 和 `set` 函数的对象(参照 [computed](https://v3.cn.vuejs.org/api/computed-watch-api.html#computed) 这篇文档),用来创建可写的 `ref` 对象,这个后期可以考虑实现下,这个章节只考虑第一种情况 18 | ::: 19 | 20 | ## happy path 21 | 22 | 我们还是先从简单的 `happy path` 入手: 23 | 24 | :::: code-group 25 | ::: code-group-item computed.spec.ts 26 | 27 | ```ts 28 | // src/reactivity/__tests__/computed.spec.ts 29 | 30 | it("happy path", () => { 31 | const user = reactive({ 32 | age: 1 33 | }) 34 | 35 | const age = computed(() => user.age) 36 | 37 | expect(age.value).toBe(1) 38 | }) 39 | ``` 40 | 41 | ::: 42 | :::: 43 | 44 | 根据测试用例我们可以得出: 45 | 46 | > 1. `computed` 接受一个函数(按照官方的说法,我们叫它 `getter`); 47 | > 2. 返回一个 `ref` 对象,可以通过 `.value` 的方式去获取值 48 | 49 | 根据以上描述,我们新建 `computed.ts` 文件用于定义 `computed` 方法,并定义 `ComputedRefImpl` 类用于做后续处理: 50 | 51 | :::: code-group 52 | ::: code-group-item computed.ts 53 | 54 | ```ts 55 | // src/reactivity/computed.ts 56 | class ComputedRefImpl { 57 | private _getter: any 58 | 59 | constructor(getter) { 60 | this._getter = getter 61 | } 62 | 63 | get value() { 64 | return this._getter() 65 | } 66 | } 67 | 68 | export const computed = getter => new ComputedRefImpl(getter) 69 | ``` 70 | 71 | ::: 72 | :::: 73 | 74 | 这样一来,我们就已经初步实现了我们的功能。 75 | 76 | ## 实现 computed 的缓存功能 77 | 78 | 熟悉 `Vue` 的小伙伴们应该知道,无论是 `Vue2` 还是 `Vue3`,`computed` 都具有一个特性,那就是 **缓存**。 79 | 80 | > 1. 当 `computed` 中响应式的值没有发生变化的时候,`computed` 不会重复计算,也就是说 `getter` 不会重复执行,而是将上一次的计算结果直接返回。 81 | > 2. 只有当其中响应式的值发生变化时,`getter` 方法才会再次触发,并返回最新的值。 82 | 83 | 我们先来实现第一个特性。首先还是先通过测试用例来捋一捋逻辑: 84 | 85 | :::: code-group 86 | ::: code-group-item computed.spec.ts 87 | 88 | ```ts 89 | // src/reactivity/__tests__/computed.spec.ts 90 | 91 | it("should compute lazily", () => { 92 | const value = reactive({ 93 | foo: 1 94 | }) 95 | 96 | const getter = jest.fn(() => value.foo) 97 | const cValue = computed(getter) 98 | 99 | // lazy 不触发 get 时不会执行 getter 方法 100 | expect(getter).not.toHaveBeenCalled() 101 | // 触发 get 时执行 getter 102 | expect(cValue.value).toBe(1) 103 | expect(getter).toHaveBeenCalledTimes(1) 104 | 105 | // should not compute again 106 | cValue.value 107 | expect(getter).toHaveBeenCalledTimes(1) 108 | }) 109 | ``` 110 | 111 | ::: 112 | :::: 113 | 114 | 那么这个缓存的功能应该怎么去实现呢? 115 | 116 | 这里我们可以定义一个开关 `_dirty`,用于控制是否需要触发 `getter` 方法,如果 `_dirty === true`, 那么就触发,否则不触发。之后定义 `_value` 用于缓存 `getter` 的执行结果: 117 | 118 | :::: code-group 119 | ::: code-group-item computed.ts 120 | 121 | ```ts{5-6,14-18} 122 | // src/reactivity/computed.ts 123 | 124 | class ComputedRefImpl { 125 | private _getter: any 126 | private _dirty: boolean = true // 标记是否需要触发 getter 127 | private _value: any // 缓存值 128 | 129 | constructor(getter) { 130 | this._getter = getter 131 | } 132 | 133 | get value() { 134 | // 值没有发生变化时,再次获取不会触发 getter 135 | if (this._dirty) { 136 | this._dirty = false 137 | this._value = this.getter() 138 | } 139 | return this._value 140 | } 141 | } 142 | ``` 143 | 144 | ::: 145 | :::: 146 | 147 | 现在,我们的 `computed` 就具有了缓存的能力,接下来我们需要实现第二个功能点。别急,开始之前,我们先来补充下测试用例: 148 | 149 | :::: code-group 150 | ::: code-group-item computed.spec.ts 151 | 152 | ```ts{21-31} 153 | // src/reactivity/__tests__/computed.spec.ts 154 | 155 | it("should compute lazily", () => { 156 | const value = reactive({ 157 | foo: 1 158 | }) 159 | 160 | const getter = jest.fn(() => value.foo) 161 | const cValue = computed(getter) 162 | 163 | // lazy 164 | expect(getter).not.toHaveBeenCalled() 165 | 166 | expect(cValue.value).toBe(1) 167 | expect(getter).toHaveBeenCalledTimes(1) 168 | 169 | // should not compute again 170 | cValue.value 171 | expect(getter).toHaveBeenCalledTimes(1) 172 | 173 | // should not compute until needed 174 | value.foo = 2 175 | expect(getter).toHaveBeenCalledTimes(1) 176 | 177 | // now it should computed 178 | expect(cValue.value).toBe(2) 179 | expect(getter).toHaveBeenCalledTimes(2) 180 | 181 | // should not compute again 182 | cValue.value 183 | expect(getter).toHaveBeenCalledTimes(2) 184 | }) 185 | ``` 186 | 187 | ::: 188 | :::: 189 | 190 | 补充完测试用例,我们执行下测试命令,发现 `22-23行` 是通过的。因为我们目前只是设置了 `value.foo` 的值,并没有获取 `cValue.value` ,因此 `getter` 仍然只执行了一次,没毛病。 191 | 192 | 但是接下来当我们尝试获取 `cValue.value` 时,发现测试用例无法通过了: 193 | 194 | ![computed](https://user-images.githubusercontent.com/9375823/177301131-f838dd96-e682-48d2-a277-c5fd2f419fa4.png) 195 | 196 | 通过报错信息我们可以得知,`cValue.value` 的值并没有如我们所愿的变成2。原因也很简单,因为当我们再次获取 `cValue.value` 时,此时的 `_dirty === false`,因此不会重新执行 `getter` 方法,自然也就无法返回最新的值。 197 | 198 | 那么我们应该在什么时候将 `_dirty` 的值重新设置为 `true` 呢?其实在描述 **第二点** 的时候就已经透露了:**在响应式的值发生变化的时候**。在这里,我们的响应式的值其实指的就是 `value.foo`。 199 | 200 | 我们知道,当我们在设置 `value.foo` 的值时,会触发 `set` 方法,`set` 方法中会调用 `trigger` 方法触发依赖,所以我们只需要在触发依赖的过程中将 `_dirty` 设置为 `true` 即可。那我们要怎么才能做到这一点呢? 201 | 202 | ### 第一步:收集依赖 203 | 204 | 要想触发依赖,那得先收集依赖,不然谈何触发? 205 | 206 | 现在我们一起来回忆一下,**依赖收集** 的过程发生在 `get` 阶段,其中的 `track` 方法会收集 `activeEffect` 对象,也就是 `ReactiveEffect` 的实例对象。在上述测试用例中,什么时候会调用 `value.foo` 呢?答案是在首次获取 `cValue.value` 的时候。既然如此,我们就要想办法在此之前将 `this._dirty = true` 的操作存到 `activeEffect` 对象中,这样才能让 `track` 收集到。 207 | 208 | 既如此,我们就需要将 `ReactiveEffect` 抛出来,并在 `computed.ts` 中引入。并在初始化 `ComputedRefImpl` 时实例化它: 209 | 210 | :::: code-group 211 | ::: code-group-item effect.ts 212 | 213 | ```ts 214 | // src/reactivity/effect.ts 215 | 216 | export class ReactiveEffect { 217 | // 省略一大波代码 218 | } 219 | ``` 220 | 221 | ::: 222 | 223 | ::: code-group-item computed.ts 224 | 225 | ```ts 226 | // src/reactivity/computed.ts 227 | import { ReactiveEffect } from './effect' 228 | 229 | class ComputedRefImpl { 230 | private _getter: any 231 | private _dirty: boolean = true // 标记是否需要触发 getter 232 | private _value: any // 缓存值 233 | private _effect: ReactiveEffect 234 | 235 | constructor(getter) { 236 | this._getter = getter 237 | this._effect = new ReactiveEffect(this._getter, () => { 238 | if (!this._dirty) this._dirty = true 239 | }) 240 | } 241 | 242 | get value() { 243 | // 值没有发生变化时,再次获取不会触发 getter 244 | if (this._dirty) { 245 | this._dirty = false 246 | this._value = this._effect.run() 247 | } 248 | return this._value 249 | } 250 | } 251 | ``` 252 | 253 | ::: 254 | :::: 255 | 256 | ::: warning 注意 257 | 这里我们将设置 `_dirty` 的操作放到了 `scheduler` 中。为什么这么做呢?因为在 `trigger` 的逻辑中,如果没有 `scheduler`, 则会执行 `run` 方法,这样一来就会多触发一次 `getter`。 258 | ::: 259 | 260 | 这样一来,在设置 `value.foo` 时,便会执行到 `scheduler` 方法,进而执行 `if (!this._dirty) this._dirty = true` 。当我们再次获取 `cValue.value` 时,此时的 `_dirty === true`,之后便能返回最新的值了。而且重复访问 `cValue.value` 时也就不会再次触发 `getter` 方法了。 261 | 262 | 至此,我们的功能就已实现完毕~ 263 | -------------------------------------------------------------------------------- /docs/miniVue/notes/reactivity/05_readonly.md: -------------------------------------------------------------------------------- 1 | 8 | # readonly 9 | 10 | ::: tip 11 | 本篇笔记对应的分支号为: `main分支:4162b7d` 12 | ::: 13 | 14 | 顾名思义,`readonly` 返回的对象是 `只读` 的,意味着其中的属性不可被修改。如果尝试对其属性进行修改,需要给出相应的提示。 15 | 16 | 它与 `reactive` 方法的异同之处如下: 17 | 18 | > 1. **同**: 它们都返回了一个 `Proxy` 对象; 19 | > 2. **异**: 由于 `readonly` 返回的对象属性不可更改,因此在 `readonly` 中无需进行 `依赖收集` 以及 `依赖触发`。 20 | 21 | ## 实现 readonly 22 | 23 | 根据上文的描述,我们来编写下 `readonly` 的测试用例: 24 | 25 | :::: code-group 26 | ::: code-group-item readonly.spec.ts 27 | 28 | ```ts 29 | // src/reactivity/__tests__/readonly.spec.ts 30 | 31 | import { readonly } from '../reactive' 32 | 33 | describe('readonly', () => { 34 | it('happy path', () => { 35 | const original = { foo: 1, bar: { bar: 2 }} 36 | const wrapped = readonly(original) 37 | expect(wrapped).not.toBe(original) 38 | expect(wrapped.foo).toBe(1) 39 | }) 40 | 41 | it('warn when call set', () => { 42 | console.warn = jest.fn() 43 | const user = readonly({ age: 10 }) 44 | user.age = 11 45 | // 当设置 readonly 对象的值时,需要发出告警 46 | expect(console.warn).toBeCalled() 47 | }) 48 | }) 49 | ``` 50 | 51 | ::: 52 | :::: 53 | 54 | 由于 `readonly` 无需进行 `依赖收集` 以及 `依赖触发`,所以它的实现其实非常简单: 55 | 56 | :::: code-group 57 | ::: code-group-item reactive.ts 58 | 59 | ```ts 60 | // src/reactivity/reactive.ts 61 | 62 | export function readonly(raw) { 63 | return new Proxy(raw, { 64 | get(target, key) { 65 | const res = Reflect.get(target, key) 66 | } 67 | 68 | set(target, key, value) { 69 | // 当尝试设置 readonly 的属性值时,需要给出告警提示 70 | console.warn(`${key} can't be setted!`, target) 71 | return true 72 | } 73 | }) 74 | } 75 | ``` 76 | 77 | ::: 78 | :::: 79 | 80 | ## 代码优化 81 | 82 | 当当~又到了我们熟悉的代码优化环节~ 🥳 83 | 84 | 我们先将 `reactive` 以及 `readonly` 的逻辑放到一起来看下: 85 | 86 | :::: code-group 87 | ::: code-group-item reactive.ts 88 | 89 | ``` ts 90 | // src/reactivity/reactive.ts 91 | 92 | import { track, trigger } from './effect' 93 | 94 | export const reactive = (raw) => { 95 | return new Proxy(raw, { 96 | // 取值 97 | get(target, key) { 98 | const res = Reflect.get(target, key) 99 | // 收集依赖 100 | track(target, key) 101 | return res 102 | }, 103 | // 赋值 104 | set(target, key, value) { 105 | const res = Reflect.set(target, key, value) 106 | // 触发依赖 107 | trigger(target, key) 108 | return res 109 | } 110 | }) 111 | } 112 | 113 | export function readonly(raw) { 114 | return new Proxy(raw, { 115 | get(target, key) { 116 | const res = Reflect.get(target, key) 117 | return res 118 | } 119 | 120 | set(target, key, value) { 121 | // 当尝试设置 readonly 的属性值时,需要给出告警提示 122 | console.warn(`${key} can't be setted!`, target) 123 | return true 124 | } 125 | }) 126 | } 127 | ``` 128 | 129 | ::: 130 | :::: 131 | 132 | 不难发现的是,两者的代码结构非常相似,此时,我们可以将类似的逻辑抽取出来。 133 | 134 | ### 抽取 get 135 | 136 | 两者 `get` 的区别在于是否需要进行依赖收集,我们可以定义一个函数 `createGetter` 用于返回 `get`,并通过传入 `isReadonly` 来决定返回的 `get` 是否需要进行依赖收集: 137 | 138 | :::: code-group 139 | ::: code-group-item reactive.ts 140 | 141 | ```ts 142 | // src/reactivity/reactive.ts 143 | 144 | function createGetter(isReadonly = false) { 145 | return function(target, key) { 146 | const res = Reflect.get(target, key) 147 | // 收集依赖 148 | !isReadonly && track(target, key) 149 | return res 150 | } 151 | } 152 | ``` 153 | 154 | ::: 155 | :::: 156 | 157 | 抽取之后,原先的代码就可以改写成这样: 158 | 159 | :::: code-group 160 | ::: code-group-item reactive.ts 161 | 162 | ``` ts 163 | // src/reactivity/reactive.ts 164 | 165 | import { track, trigger } from './effect' 166 | 167 | function createGetter(isReadonly = false) { 168 | return function(target, key) { 169 | const res = Reflect.get(target, key) 170 | // 收集依赖 171 | !isReadonly && track(target, key) 172 | return res 173 | } 174 | } 175 | 176 | export const reactive = (raw) => { 177 | return new Proxy(raw, { 178 | // 取值 179 | get: createGetter(), 180 | // 赋值 181 | set(target, key, value) { 182 | const res = Reflect.set(target, key, value) 183 | // 触发依赖 184 | trigger(target, key) 185 | return res 186 | } 187 | }) 188 | } 189 | 190 | export function readonly(raw) { 191 | return new Proxy(raw, { 192 | get: createGetter(true), 193 | 194 | set(target, key, value) { 195 | // 当尝试设置 readonly 的属性值时,需要给出告警提示 196 | console.warn(`${key} can't be setted!`, target) 197 | return true 198 | } 199 | }) 200 | } 201 | ``` 202 | 203 | ::: 204 | :::: 205 | 206 | 相较之前,代码就简洁了许多。 207 | 208 | ### 抽取 set 209 | 210 | 依葫芦画瓢,我们可以将 `set` 也抽取出来。由于 `readonly` 的 `set` 实现与 `reactive` 的相似之处不多,因此,我们暂时只对 `reactive` 的进行改造: 211 | 212 | :::: code-group 213 | ::: code-group-item reactive.ts 214 | 215 | ``` ts 216 | // src/reactivity/reactive.ts 217 | 218 | import { track, trigger } from './effect' 219 | 220 | function createGetter(isReadonly = false) { 221 | return function(target, key) { 222 | const res = Reflect.get(target, key) 223 | // 收集依赖 224 | !isReadonly && track(target, key) 225 | return res 226 | } 227 | } 228 | 229 | function createSetter() { 230 | return function(target, key, value) { 231 | const res = Reflect.set(target, key, value) 232 | // 触发依赖 233 | trigger(target, key) 234 | return res 235 | } 236 | } 237 | 238 | export const reactive = (raw) => { 239 | return new Proxy(raw, { 240 | // 取值 241 | get: createGetter(), 242 | // 赋值 243 | set: createSetter() 244 | } 245 | 246 | export function readonly(raw) { 247 | return new Proxy(raw, { 248 | get: createGetter(true), 249 | 250 | set(target, key, value) { 251 | // 当尝试设置 readonly 的属性值时,需要给出告警提示 252 | console.warn(`${key} can't be setted!`, target) 253 | return true 254 | } 255 | }) 256 | } 257 | ``` 258 | 259 | ::: 260 | :::: 261 | 262 | ### 抽取 baseHandlers 263 | 264 | 代码优化到这里,我们来看看还有哪些可以被优化的部分。 265 | 266 | > 1. `reactive` 与 `readonly` 返回的都是 `Proxy` 代理对象,且都做了 `get` 与 `set` 操作的劫持处理。因此,我们可以创建一个 `baseHandlers` 对象,用于定义 `Proxy` 的 `get` 与 `set`,并将其抽取到单独的 `baseHandlers` 模块中; 267 | > 2. 在每次执行 `reactive` 或者 `readonly` 方法时,`createGetter` 与 `createSetter` 方法总是会被执行。此时,我们可以在首次执行时将执行结果缓存下来,以提升性能; 268 | > 3. 两者都返回了 `new Proxy`, 我们可以定义一个更加见名知意的方法 `createActiveObject` 用于返回 `Proxy` 对象。 269 | 270 | :::: code-group 271 | ::: code-group-item baseHandlers.ts 272 | 273 | ``` ts 274 | // src/reactivity/baseHandlers.ts 275 | 276 | import { track, trigger } from './effect' 277 | 278 | function createGetter(isReadonly = false) { 279 | return function(target, key) { 280 | const res = Reflect.get(target, key) 281 | // 收集依赖 282 | !isReadonly && track(target, key) 283 | return res 284 | } 285 | } 286 | 287 | function createSetter() { 288 | return function(target, key, value) { 289 | const res = Reflect.set(target, key, value) 290 | // 触发依赖 291 | trigger(target, key) 292 | return res 293 | } 294 | } 295 | 296 | // 缓存,避免重复调用 297 | const get = createGetter() 298 | const set = createSetter() 299 | const readonlyGet = createGetter(true) 300 | 301 | export const mutableHandlers = { 302 | get, 303 | set 304 | } 305 | 306 | export const readonlyHandlers = { 307 | get: readonlyGet, 308 | set(target, key, value) { 309 | // 当尝试设置 readonly 的属性值时,需要给出告警提示 310 | console.warn(`${key} can't be setted!`, target) 311 | return true 312 | } 313 | } 314 | ``` 315 | 316 | ::: code-group-item reactive.ts 317 | 318 | ``` ts 319 | // src/reactivity/reactive.ts 320 | 321 | import { mutableHandlers, readonlyHandlers } from './baseHandlers' 322 | 323 | /** 324 | * 创建proxy对象 325 | * @param raw 需要被代理的对象 326 | * @param baseHandlers 代理拦截 327 | * @returns 328 | */ 329 | function createActiveObject(raw, baseHandlers) { 330 | return new Proxy(raw, baseHandlers) 331 | } 332 | 333 | /** 334 | * 创建 reactive 对象 335 | * @param raw 需要被代理的对象 336 | * @returns 337 | */ 338 | export const reactive = (raw) => { 339 | return createActiveObject(raw, mutableHandlers) 340 | } 341 | 342 | /** 343 | * 创建 readonly 对象 344 | * @param raw 需要被代理的对象 345 | * @returns 346 | */ 347 | export const readonly = (raw) => { 348 | return createActiveObject(raw, readonlyHandlers) 349 | } 350 | ``` 351 | 352 | ::: 353 | 354 | ::: 355 | :::: 356 | 357 | 至此,我们的优化告一段落~此时的代码就会显得更加简洁,可读性也会更强。 358 | -------------------------------------------------------------------------------- /docs/miniVue/notes/reactivity/01_reactive.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | # effect & reactive & 依赖收集 & 触发依赖 10 | 11 | ::: tip 12 | 本篇笔记对应的分支号为: `main分支:e8bb112` 13 | ::: 14 | 15 | 在 Vue3 中,[reactive](https://v3.cn.vuejs.org/api/basic-reactivity.html#reactive) 方法被用于创建一个对象的 **响应式副本**。这里可以拆成两个部分来理解,即 **响应式** 以及 **副本**。 16 | 17 | ## 副本 18 | 19 | 我们先来看看 **副本** 这个部分。在实现 `reactive` 方法之前,我们先来写下它的测试用例,看看它需要做些啥: 20 | 21 | :::: code-group 22 | ::: code-group-item reactive.spec.ts 23 | 24 | ```ts 25 | // src/reactivity/__tests__/reactive.spec.ts 26 | 27 | describe('reactive', () => { 28 | it('happy path', () => { 29 | const origin = { num: 0 } 30 | // 通过 reactive 创建响应式对象 31 | const reactiveData = reactive(origin) 32 | // 判断响应式对象与原对象不是同一个对象 33 | expect(reactiveData).not.toBe(origin) 34 | // 代理对象中的 num 值应与原对象中的相同 35 | expect(reactiveData.num).toBe(0) 36 | }) 37 | }) 38 | ``` 39 | 40 | ::: 41 | :::: 42 | 43 | ### 实现 `reactive` 44 | 45 | 通过测试用例我们不难发现,其实 `reactive` 做的事情很简单,就是创建一个对象副本,那这个 **副本** 该怎么创建呢?答案是使用 [Proxy](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 👇 46 | 47 | :::: code-group 48 | ::: code-group-item reactive.ts 49 | 50 | ```ts 51 | // src/reactivity/reactive.ts 52 | 53 | export const reactive = (raw) => { 54 | return new Proxy(raw, { 55 | // 取值 56 | get(target, key) { 57 | const res = Reflect.get(target, key) 58 | return res 59 | }, 60 | // 赋值 61 | set(target, key, value) { 62 | const res = Reflect.set(target, key, value) 63 | return res 64 | } 65 | }) 66 | } 67 | ``` 68 | 69 | ::: 70 | :::: 71 | 72 | ## 响应式 73 | 74 | 现在我们已经可以通过 `reactive` 方法获取目标对象的 **副本** 了,那 **响应式** 部分又该如何实现呢? 75 | 76 | 所谓 **响应式**, 其实本质上就做了两件事情: 77 | 78 | > 1. 在读取对象属性时进行 `依赖收集` 79 | > 2. 在修改对象属性时执行 `依赖触发` 80 | 81 | 而这部分的逻辑则交由 `effect` 模块来实现。那 `依赖收集` 跟 `依赖触发` 具体是怎样的一个流程呢?请看下图: 82 | 83 | ![track&trigger](https://user-images.githubusercontent.com/9375823/173803951-43576337-7bba-423d-a985-5c0eb9dfb052.png) 84 | 85 | 对上图的内容简单描述如下: 86 | 87 | > 1. 在读取响应式对象 `Target` 中的属性时进行 `依赖收集` 操作,所有的依赖会被收集到依赖池 `TargetMap` 中; 88 | > 2. 在设置响应式对象 `Target` 的属性值时执行 `依赖触发` 操作,会根据对应的 `Target` 以及 `key` 将依赖从依赖池 `TargetMap` 中取出并执行。 89 | 90 | 现在我们已经知道了 `effect` 模块所要实现的功能,依据上述内容,先来编写下测试用例: 91 | 92 | :::: code-group 93 | ::: code-group-item effect.spec.ts 94 | 95 | ```ts {14} 96 | // src/reactivity/__tests__/effect.spec.ts 97 | 98 | describe('effect', () => { 99 | it('happy path', () => { 100 | // 创建响应式对象 101 | const user = reactive({ 102 | age: 10 103 | }) 104 | let nextAge 105 | effect(() => { 106 | nextAge = user.age + 1 107 | }) 108 | // 传入 effect 的方法会被立即执行一次 109 | expect(nextAge).toBe(11) 110 | // 修改响应式对象的属性值 111 | user.age++ 112 | // 传入 effect 的方法会再次被执行 113 | expect(nextAge).toBe(12) 114 | }) 115 | }) 116 | ``` 117 | 118 | ::: 119 | :::: 120 | 121 | ### 实现 `effect` 122 | 123 | 接下来我们需要实现 `effect` 模块的功能。 124 | 125 | 根据上面的描述,`effect` 接受一个函数作为参数,既如此先定义一下 `effect` 方法: 126 | 127 | :::: code-group 128 | ::: code-group-item effect.ts 129 | 130 | ```ts 131 | // src/reactivity/effect.ts 132 | 133 | export function effect(fn) {} 134 | ``` 135 | 136 | ::: 137 | :::: 138 | 139 | 接下来,我们需要定义依赖池 `targetMap` 用于存放依赖。依赖池中存放的是响应式对象 `target` 所对应的依赖,需要使用对象类型作 key 的话,那么使用 [Map](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map) 自然再合适不过啦: 140 | 141 | :::: code-group 142 | ::: code-group-item effect.ts 143 | 144 | ```ts 145 | // src/reactivity/effect.ts 146 | 147 | const targetMap = new Map() 148 | 149 | export function effect(fn) {} 150 | ``` 151 | 152 | ::: 153 | :::: 154 | 155 | 好了,现在存放依赖的地方有了,那么我们就开始收集它们吧~ 156 | 157 | 上文中我们提到,`收集依赖` 的操作是在读取响应式对象 `target` 中的属性时进行的。还记得 `target` 对象是通过 `Proxy` 创建出来的么?在读取 `target` 的属性时,必然会触发 `get` 方法,那么 `收集依赖` 的操作也应该在 `get` 方法中进行。 158 | 159 | 我们先来定义一个方法 `tarck` 用于依赖收集,并在 `reactive.ts` 中引入它,以便在 `get` 方法中进行调用: 160 | 161 | :::: code-group 162 | ::: code-group-item effect.ts 163 | 164 | ```ts 165 | // src/reactivity/effect.ts 166 | 167 | const targetMap = new Map() 168 | 169 | /** 170 | * 收集依赖 171 | * @param target 需要收集依赖的对象 172 | * @param key 收集该key所对应的依赖 173 | */ 174 | export function track(target, key) { 175 | } 176 | 177 | export function effect(fn) {} 178 | ``` 179 | 180 | ::: 181 | 182 | ::: code-group-item reactive.ts 183 | 184 | ```ts 185 | 186 | // src/reactivity/reactive.ts 187 | 188 | import { track } from './effect' 189 | 190 | export const reactive = (raw) => { 191 | return new Proxy(raw, { 192 | // 取值 193 | get(target, key) { 194 | const res = Reflect.get(target, key) 195 | // 收集依赖 196 | track(target, key) 197 | return res 198 | }, 199 | // 赋值 200 | set(target, key, value) { 201 | const res = Reflect.set(target, key, value) 202 | return res 203 | } 204 | }) 205 | } 206 | ``` 207 | 208 | ::: 209 | :::: 210 | 211 | 接下来,我们需要实现 `track` 这部分的功能。在动手实现之前,我们先来捋一捋 `track` 需要做哪些事情: 212 | 213 | > 1. 由于在初始化时依赖池是空的(也为了避免覆盖),所以在存入 `targetMap` 依赖池之前,需要先判断依赖池中是否已经存在 `target` 所对应的依赖容器 `depsMap`: 214 | > - 如果存在,则取出 `depsMap`; 215 | > - 否则新建一个 `depsMap`, 并将其存入到依赖池 `targetMap` 中; 216 | > 2. 从依赖容器 `depsMap` 中取出响应式对象 `target` 对应属性的依赖 `deps`,由 `步骤1` 可知,`depsMap` 可能是空的,因此也需要对 `deps` 进行判空处理: 217 | > - 如果存在,则取出,并将依赖存入 218 | > - 如果不存在,则新建一个 `deps`,将依赖存入其中,并将 `deps` 存入对应属性的依赖容器 `depsMap` 中。为了避免重复收集依赖,此处使用 [Set](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Set) 进行存储。 219 | 220 | 为了方便理解,我们来一起看下流程图: 221 | 222 | ![tarck](https://user-images.githubusercontent.com/9375823/174035124-13a100ba-3e6a-4da0-a9f9-ff74acef6942.png) 223 | 224 | 代码实现如下: 225 | 226 | :::: code-group 227 | ::: code-group-item effect.ts 228 | 229 | ```ts {25} 230 | // src/reactivity/effect.ts 231 | 232 | const targetMap = new Map() 233 | 234 | /** 235 | * 收集依赖 236 | * @param target 需要收集依赖的对象 237 | * @param key 收集该key所对应的依赖 238 | */ 239 | export function track(target, key) { 240 | // 查找该对象对应的依赖池 241 | let depsMap = targetMap.get(target) 242 | // 如果没有(首次初始化时),则创建新的依赖池 243 | if (!depsMap) { 244 | depsMap = new Map() 245 | targetMap.set(target, depsMap) 246 | } 247 | // 从获取到的依赖池中获取该key所对应的依赖列表 248 | let deps = depsMap.get(key) 249 | // 如果没有,则新建一个该key对应的列表 250 | if (!deps) { 251 | deps = new Set() 252 | depsMap.set(key, deps) 253 | } 254 | // TODO 将依赖对象保存到列表中 255 | } 256 | 257 | export function effect(fn) {} 258 | ``` 259 | 260 | ::: 261 | :::: 262 | 263 | 好,代码写到这里的时候,我们遇到了一个 264 | 265 | ::: warning 问题: 266 | **需要被收集的依赖在 `effect` 方法中,在 `tarck` 里要怎么获取到这个依赖呢?** 267 | ::: 268 | 269 | 针对这个问题,我们可以通过定义一个用于存储依赖的全局变量 `activeEffect` 来解决解决这个问题。那我们直接把依赖塞到 `activeEffect` 中就完事儿了么?当然。。。。 270 | 271 | ![达咩](https://user-images.githubusercontent.com/9375823/174023015-d484f98f-45e1-4a1e-a894-8333ce565729.png) 272 | 273 | 不是!如果只单单为了实现这个功能,无可厚非,但是后续我们还有其他操作(为了代码的健壮性,可读性, 可扩展性),这里我们定义 `ReactiveEffect` 类将依赖收集起来,之后将该类的实例赋值给 `activeEffect` 即可: 274 | 275 | :::: code-group 276 | ::: code-group-item effect.ts 277 | 278 | ```ts {3,13,14,41,48} 279 | // src/reactivity/effect.ts 280 | 281 | let activeEffect 282 | 283 | class ReactiveEffect { 284 | private _fn: any 285 | 286 | constructor(fn) { 287 | this._fn = fn 288 | } 289 | 290 | run() { 291 | activeEffect = this 292 | this._fn() 293 | } 294 | } 295 | 296 | const targetMap = new Map() 297 | 298 | /** 299 | * 收集依赖 300 | * @param target 需要收集依赖的对象 301 | * @param key 收集该key所对应的依赖 302 | */ 303 | export function track(target, key) { 304 | // 查找该对象对应的依赖池 305 | let depsMap = targetMap.get(target) 306 | // 如果没有(首次初始化时),则创建新的依赖池 307 | if (!depsMap) { 308 | depsMap = new Map() 309 | targetMap.set(target, depsMap) 310 | } 311 | // 从获取到的依赖池中获取该key所对应的依赖列表 312 | let deps = depsMap.get(key) 313 | // 如果没有,则新建一个该key对应的列表 314 | if (!deps) { 315 | deps = new Set() 316 | depsMap.set(key, deps) 317 | } 318 | // 将依赖对象保存到列表中 319 | deps.add(activeEffect) 320 | } 321 | 322 | export function effect(fn) { 323 | // 实例化 ReactiveEffect 类,并将依赖传入 324 | const _effect = new ReactiveEffect(fn) 325 | 326 | _effect.run() 327 | } 328 | ``` 329 | 330 | ::: 331 | :::: 332 | 333 | ::: warning 注意 334 | 这里需要注意的是,传入 `effect` 中的方法会被立即执行一次(可以回看上述测试用例中的 `第14行代码`)。所以 `ReactiveEffect` 暴露的 `run` 方法中除了要将依赖存入全局变量 `activeEffect` 中,还得将传入的依赖返回出来用以执行。 335 | ::: 336 | 337 | 到目前为止,`依赖收集` 的功能就已经实现了。接下来便轮到 `依赖触发` 了。相较于 `依赖收集`,`依赖触发` 就简单了,只需要根据传入的 `target` 以及对应的属性 `key`,将依赖项取出执行便可。 338 | 339 | 这里我们在 `effect.ts` 中定义一个 `trigger` 方法用于触发依赖,之后在 `reactive.ts` 中引入。由于触发依赖发生在修改响应式对象 `target` 的属性阶段,所以需要放到 `set` 中执行: 340 | 341 | :::: code-group 342 | ::: code-group-item effect.ts 343 | 344 | ```ts 345 | // src/reactivity/effect.ts 346 | 347 | /** 348 | * 触发依赖 349 | * @param target 触发依赖的对象 350 | * @param key 触发该key对应的依赖 351 | */ 352 | export function trigger(target, key) { 353 | // 根据对象与key获取到所有的依赖,并执行 354 | const depsMap = targetMap.get(target) 355 | const deps = depsMap.get(key) 356 | for(const dep of deps) { 357 | dep.run() 358 | } 359 | } 360 | ``` 361 | 362 | ::: 363 | 364 | ::: code-group-item reactive.ts 365 | 366 | ```ts 367 | // src/reactivity/reactive.ts 368 | 369 | import { track, trigger } from './effect' 370 | 371 | export const reactive = (raw) => { 372 | return new Proxy(raw, { 373 | // 取值 374 | get(target, key) { 375 | const res = Reflect.get(target, key) 376 | // 收集依赖 377 | track(target, key) 378 | return res 379 | }, 380 | // 赋值 381 | set(target, key, value) { 382 | const res = Reflect.set(target, key, value) 383 | // 触发依赖 384 | trigger(target, key) 385 | return res 386 | } 387 | }) 388 | } 389 | ``` 390 | 391 | ::: 392 | :::: 393 | 394 | 至此,`依赖收集` & `触发依赖` 的功能就完成啦~ 395 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": ["es2015", "DOM"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "esnext" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": ["jest"], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | 68 | /* Interop Constraints */ 69 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 70 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 71 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 72 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 73 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 74 | 75 | /* Type Checking */ 76 | "strict": true /* Enable all strict type-checking options. */, 77 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 78 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 79 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 80 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 81 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 82 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 83 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 84 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 85 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 86 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 87 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 88 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 89 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 90 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 91 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 92 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 93 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 94 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 95 | 96 | /* Completeness */ 97 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 98 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /docs/miniVue/notes/reactivity/04_stop.md: -------------------------------------------------------------------------------- 1 | 8 | # stop 9 | 10 | ::: tip 11 | 本篇笔记对应的分支号为: `main分支:aadf196` 12 | ::: 13 | 14 | `stop` 方法的入参为 `effect` 方法返回的 `runner` 函数。当调用 `stop` 方法后,响应式对象的属性发生变化时不会再触发依赖。 15 | 16 | ## 实现 stop 17 | 18 | 根据开头的描述,我们先来编写 `stop` 相关的测试用例: 19 | 20 | :::: code-group 21 | ::: code-group-item effect.spec.ts 22 | 23 | ```ts 24 | // src/reactivity/__tests__/effect.spec.ts 25 | 26 | it('stop', () => { 27 | let dummy 28 | const obj = reactive({ prop: 1 }) 29 | const runner = effect(() => { 30 | dummy = obj.prop 31 | }) 32 | obj.prop = 2 33 | expect(dummy).toBe(2) 34 | // 调用 stop 后,响应式对象属性变化时不再触发 fn 35 | stop(runner) 36 | obj.prop = 3 37 | expect(dummy).toBe(2) 38 | // 被停用的 effect 仍可以被调用 39 | runner() 40 | expect(dummy).toBe(3) 41 | }) 42 | ``` 43 | 44 | ::: 45 | :::: 46 | 47 | `stop` 方法定义在 `effect.ts` 模块中,它接受 `effect` 返回的 `runner` 函数作为参数。我们首先在 `effect.ts` 文件中定义下它: 48 | 49 | :::: code-group 50 | ::: code-group-item effect.ts 51 | 52 | ```ts 53 | // src/reactivity/effect.ts 54 | 55 | export function stop(runner) {} 56 | ``` 57 | 58 | ::: 59 | :::: 60 | 61 | 接下来我们需要解决的问题是 **如何保证在执行完 `stop` 方法后,改变响应式对象的属性值时不会再次触发依赖。** 62 | 63 | 我们来回顾下依赖触发的流程: 64 | 65 | ![trigger](https://user-images.githubusercontent.com/9375823/175494804-d88de5dc-6f1b-4c9d-9ba4-389051456705.png) 66 | 67 | 1. 修改响应式对象 `Target` 的属性 `key` 的值; 68 | 2. 从响应池 `TargetMap` 中取出对应的 `deps`, 即: `TargetMap.get(Target).get(key)` 69 | 3. 遍历 `deps`,调用每个 `effect` 的 `run` 方法以触发依赖。 70 | 71 | 如果想要依赖不被触发,那么只要对应 `effect` 的 `run` 方法不被执行即可。而想要对应 `effect` 的 `run` 方法不被执行,那么我们只需要将对应的 `effect` 删除即可。 72 | 73 | 想要实现这个功能,主要需要完成两件事情: 74 | 75 | > 1. 找到需要删除的 `effect` 对象; 76 | > 2. 找到存放该 `effect` 对象的所有 `deps`,并将之移除; 77 | 78 | ### 找到需要删除的 `effect` 对象 79 | 80 | 现在 `stop` 接受的入参是 `effect` 返回的 `runner` 函数,我们如何能够找到需要删除的 `effect` 对象呢? 81 | 82 | 其实,所谓 **需要删除的 `effect` 对象** 指的就是删除 `runner` 所对应的 `effct`,那么我们可以给 `runner` 添加一个属性: `effect`, 并将实例化 `ReactiveEffect` 后的对象 `_effect` 赋值给它。 83 | 84 | :::: code-group 85 | ::: code-group-item effect.ts 86 | 87 | ```ts{9} 88 | // src/reactivity/effect.ts 89 | 90 | export function effect(fn, options: any = {}) { 91 | const _effect = new ReactiveEffect(fn, options.scheduler) 92 | 93 | _effect.run() 94 | 95 | const runner: any = _effect.run.bind(_effect) 96 | runner.effect = _effect 97 | 98 | return runner 99 | } 100 | ``` 101 | 102 | ::: 103 | :::: 104 | 105 | ### 找到存放 `effect` 对象的所有 `deps` 106 | 107 | 将 `effect` 存放到对应的 `deps` 中是在 `依赖收集` 过程中完成的,要想知道 `effect` 被存放在哪些 `deps` 中,只需要做个 `反向收集` 即可。 108 | 109 | 我们可以在 `ReactiveEffect` 类中创建一个数组,并在 `依赖收集` 时将 `deps` 存入其中。之后,在 `ReactiveEffect` 中创建 `stop` 方法,用于清空该数组,并将之交给外部的 `stop` 方法执行即可。 110 | 111 | :::: code-group 112 | ::: code-group-item effect.ts 113 | 114 | ```ts{6,18-21,49,54} 115 | // src/reactivity/effect.ts 116 | 117 | class ReactiveEffect { 118 | private _fn: any 119 | public scheduler: Function | undefined 120 | deps = [] 121 | 122 | constructor(fn, scheduler?: Function) { 123 | this._fn = fn 124 | this.scheduler = scheduler 125 | } 126 | 127 | run() { 128 | activeEffect = this 129 | return this._fn() 130 | } 131 | 132 | stop() { 133 | this.deps.forEach((dep: any) => { 134 | dep.delete(this) 135 | }) 136 | } 137 | } 138 | 139 | /** 140 | * 收集依赖 141 | * @param target 需要收集依赖的对象 142 | * @param key 收集该key所对应的依赖 143 | */ 144 | export function track(target, key) { 145 | // 查找该对象对应的依赖池 146 | let depsMap = targetMap.get(target) 147 | // 如果没有(首次初始化时),则创建新的依赖池 148 | if (!depsMap) { 149 | depsMap = new Map() 150 | targetMap.set(target, depsMap) 151 | } 152 | // 从获取到的依赖池中获取该key所对应的依赖列表 153 | let deps = depsMap.get(key) 154 | // 如果没有,则新建一个该key对应的列表 155 | if (!deps) { 156 | deps = new Set() 157 | depsMap.set(key, deps) 158 | } 159 | 160 | if (activeEffect) { 161 | // 将依赖对象保存到列表中 162 | deps.add(activeEffect) 163 | activeEffect.deps.push(deps) 164 | } 165 | } 166 | 167 | export function stop(runner) { 168 | runner.effect.stop() 169 | } 170 | ``` 171 | 172 | ::: 173 | :::: 174 | 175 | 至此,`stop` 方法就已经实现完成了~可以参考下面的流程图加深理解: 176 | 177 | ![stop](https://user-images.githubusercontent.com/9375823/175513006-07d2df12-3465-4522-9164-d9fbaf54a89a.png) 178 | 179 | ### 优化 180 | 181 | 代码写完后,我们可以对其做一些优化,这也是 `TDD` 的重要步骤之一。 182 | 183 | #### 提高可读性 184 | 185 | 上述代码中 `19-21` 行是用于清除 `effect` 的。为了提高代码的可读性,我们可以将 `19-21` 的代码逻辑抽取出来,并取名为 `cleanEffect`: 186 | 187 | :::: code-group 188 | ::: code-group-item effect.ts 189 | 190 | ```ts{9,13-17} 191 | // src/reactivity/effect.ts 192 | 193 | class ReactiveEffect { 194 | /** 195 | * 省略一大波代码 196 | */ 197 | 198 | stop() { 199 | cleanEffect(this) 200 | } 201 | } 202 | 203 | function cleanEffect(effect) { 204 | effect.deps.forEach((dep: any) => { 205 | dep.delete(effect) 206 | }) 207 | } 208 | ``` 209 | 210 | ::: 211 | :::: 212 | 213 | #### 避免重复调用 `stop` 214 | 215 | 当我们多次调用 `stop` 方法时,`ReactiveEffect` 中的 `stop` 方法总是会被执行。然而在第一次调用时,`runner` 相关的 `effect` 已经被清空了,所以在此之后没有必要再去执行 `stop` 方法了。因此,我们可以在 `ReactiveEffect` 中添加一个标记:`active`,用于标识 `effect` 是否已经被清空。如果被清空,则不必再次执行 `stop` 方法: 216 | 217 | :::: code-group 218 | ::: code-group-item effect.ts 219 | 220 | ```ts{6,20-23} 221 | // src/reactivity/effect.ts 222 | 223 | class ReactiveEffect { 224 | private _fn: any 225 | public scheduler: Function | undefined 226 | active = true // 是否需要清空 deps 227 | deps = [] 228 | 229 | constructor(fn, scheduler?: Function) { 230 | this._fn = fn 231 | this.scheduler = scheduler 232 | } 233 | 234 | run() { 235 | activeEffect = this 236 | return this._fn() 237 | } 238 | 239 | stop() { 240 | if (this.active) { 241 | cleanEffect(this) 242 | this.active = false 243 | } 244 | } 245 | } 246 | ``` 247 | 248 | ::: 249 | :::: 250 | 251 | ## 实现 onStop 方法 252 | 253 | `onStop` 方法的执行时机是在 `stop` 方法被调用后,可以理解为 `stop` 之后的回调函数。 254 | 255 | `onStop` 方法在 `effect` 的第二个参数中被传入。还记得上一篇的 `scheduler` 么? `onStop` 的传入方式与其一致。 256 | 257 | 老规矩,在实现它之前,我们还是先来编写对应的测试用例: 258 | 259 | :::: code-group 260 | ::: code-group-item effect.spec.ts 261 | 262 | ```ts 263 | // src/reactivity/__tests__/effect.spec.ts 264 | 265 | it('onStop', () => { 266 | const obj = reactive({ prop: 1 }) 267 | const onStop = jest.fn() 268 | let dummy 269 | const runner = effect(() => { 270 | dummy = obj.prop 271 | }, { 272 | onStop 273 | }) 274 | expect(dummy).toBe(1) 275 | // 当调用stop时,onStop 会被调用一次 276 | stop(runner) 277 | expect(onStop).toBeCalledTimes(1) 278 | }) 279 | ``` 280 | 281 | ::: 282 | :::: 283 | 284 | 既然要在 `stop` 被执行后执行,那么 `onStop` 需要在 `ReactiveEffect` 类中的 `stop` 方法中被调用。既然要在 `ReactiveEffect` 中能够调用到 `onStop`,那么我们就需要将其传入 `ReactiveEffect` 中: 285 | 286 | :::: code-group 287 | ::: code-group-item effect.ts 288 | 289 | ```ts{4,13-15,23} 290 | // src/reactivity/effect.ts 291 | 292 | class ReactiveEffect { 293 | onStop?: () => void 294 | 295 | /** 296 | * 省略一些代码 297 | */ 298 | 299 | stop() { 300 | if (this.active) { 301 | cleanEffect(this) 302 | if (this.onStop) { 303 | this.onStop() 304 | } 305 | this.active = false 306 | } 307 | } 308 | } 309 | 310 | export function effect(fn, options: any = {}) { 311 | const _effect = new ReactiveEffect(fn, options.scheduler) 312 | _effect.onStop = options.onStop 313 | 314 | _effect.run() 315 | 316 | const runner: any = _effect.run.bind(_effect) 317 | runner.effect = _effect 318 | 319 | return runner 320 | } 321 | ``` 322 | 323 | ::: 324 | :::: 325 | 326 | 至此,`opStop` 也就实现完成了。 327 | 328 | ### 继续优化 329 | 330 | #### 继续提升代码可读性 331 | 332 | 我们来看下上述 `effect` 方法中的 `_effect.onStop = options.onStop` 这一行代码。 333 | 334 | 在后续功能迭代的过程中,`effect` 的第二个参数 `options` 可能会继续新增其他的属性,而这些属性可能也需要绑定到 `_effect` 上。如果频繁的写 `_effct.xxx = options.xxx` 的话,代码可读性就会很差。 335 | 336 | 此时,我们可以使用 `Object.assign` 来对其进行优化: 337 | 338 | ```ts 339 | Object.assign(_effect, options) 340 | ``` 341 | 342 | 这样,后续的属性也可以添加到 `_effect` 对象上。这个行为其实是在对 `_effect` 的功能进行 **扩展**。那我们不妨用一个更加见名知意的方法 `extend` 来替换它,并且考虑到 `Object.assign` 未来可能会在多处使用,我们可以将其抽取到 `src/shared/index.ts` 模块中,并作为全局共享的方法暴露出去: 343 | 344 | :::: code-group 345 | ::: code-group-item effect.ts 346 | 347 | ```ts 348 | // src/reactivity/effect.ts 349 | import { extend } from '../shared' 350 | 351 | export function effect(fn, options: any = {}) { 352 | /** 省略一些代码 */ 353 | extend(_effect, options) 354 | } 355 | ``` 356 | 357 | ::: 358 | 359 | ::: code-group-item index.ts 360 | 361 | ```ts 362 | // src/shared/index.ts 363 | 364 | export const extend = Object.assign 365 | ``` 366 | 367 | ::: 368 | :::: 369 | 370 | 行文至此,`stop` 以及 `onStop` 方法实现完成~ 371 | 372 | # stop 功能优化 373 | 374 | :::tip 375 | 优化对应的分支号为: `main分支:fca2f92` 376 | ::: 377 | 378 | 在上述 `stop` 的测试用例中,存在一种边缘情况。我们一起来看下当我们将 `obj.prop = 3` 这行代码替换为 `obj.foo++` 会发生什么呢: 379 | 380 | :::: code-group 381 | ::: code-group-item effect.spec.ts 382 | 383 | ```ts{13,16} 384 | // src/reactive/__tests__/effect.spec.ts 385 | 386 | it('stop', () => { 387 | let dummy 388 | const obj = reactive({ prop: 1 }) 389 | const runner = effect(() => { 390 | dummy = obj.prop 391 | }) 392 | obj.prop = 2 393 | expect(dummy).toBe(2) 394 | // 调用 stop 后,响应式对象属性变化时不再触发 fn 395 | stop(runner) 396 | // obj.prop = 3 397 | // obj.prop = obj.prop + 1 398 | // get => set 399 | obj.prop++ 400 | expect(dummy).toBe(2) 401 | // 被停用的 effect 仍可以被调用 402 | runner() 403 | expect(dummy).toBe(3) 404 | }) 405 | ``` 406 | 407 | ::: 408 | :::: 409 | 410 | ![stop](https://user-images.githubusercontent.com/9375823/176107392-a2f7854f-813f-40f0-9ba3-d1b02f06ea78.png) 411 | 412 | 我们可以看到,`第17行` 的测试用例失败了!这是为啥呢? 413 | 414 | 因为当我们执行 `obj.foo++` 操作时,等于执行的是 `obj.foo = obj.foo + 1`,在此期间会触发 `get` 操作,而在进行 `get` 操作的过程中,会进行 `依赖收集`,此时又会将 `activeEffect` 对象收集到 `deps` 中,之后在进行 `set` 操作时,又会执行传入 `effect` 的 `fn`,这样一来不就相当于我们在 `stop` 中进行的清空操作白费了么? 415 | 416 | 因此,我们需要定义一个变量 `shouldTrack` 来标记当前的依赖是否需要被收集。当 `shouldTrack === false` 时,表示当前的依赖不应该被收集。依赖收集的操作在 `track` 中,我们来对其进行修改: 417 | 418 | :::: code-group 419 | ::: code-group-item effect.ts 420 | 421 | ```ts {3,26} 422 | // src/reactivity/effect.ts 423 | 424 | let shouldTrack = false // 标记是否应该进行收集 425 | 426 | /** 427 | * 收集依赖 428 | * @param target 需要收集依赖的对象 429 | * @param key 收集该key所对应的依赖 430 | */ 431 | export function track(target, key) { 432 | // 查找该对象对应的依赖池 433 | let depsMap = targetMap.get(target) 434 | // 如果没有(首次初始化时),则创建新的依赖池 435 | if (!depsMap) { 436 | depsMap = new Map() 437 | targetMap.set(target, depsMap) 438 | } 439 | // 从获取到的依赖池中获取该key所对应的依赖列表 440 | let deps = depsMap.get(key) 441 | // 如果没有,则新建一个该key对应的列表 442 | if (!deps) { 443 | deps = new Set() 444 | depsMap.set(key, deps) 445 | } 446 | if (!activeEffect) return 447 | if (!shouldTrack) return 448 | // 将依赖对象保存到列 449 | deps.add(activeEffect) 450 | activeEffect.deps.push(deps) 451 | } 452 | ``` 453 | 454 | ::: 455 | :::: 456 | 457 | 修改完成后,此时的测试是无法通过的,因为 `shouldTrack` 的初始值为 `false`,当第一次运行 `run` 方法时,依赖收集的逻辑会被跳过。因此,我们需要在 `run` 方法中进行判断,**如果没有执行过 `stop` 操作,我们应保持原有逻辑,否则直接返回 `this._fn()`。** 458 | 459 | 那我们该如何判断是否已经执行过 `stop` 了呢?在实现 `stop` 时,我们当时定义了一个属性:`active`,用于标识是否执行过 `stop`, 因此,我们可以用它来进行判断: 460 | 461 | :::: code-group 462 | ::: code-group-item effect.ts 463 | 464 | ```ts 465 | // src/reactivity/effect.ts 466 | 467 | 468 | class ReactiveEffect { 469 | /** 省略一大波代码 */ 470 | 471 | run() { 472 | if (!this.active) { 473 | return this._fn() 474 | } 475 | 476 | shouldTrack = true 477 | activeEffect = this 478 | const result = this._fn() 479 | shouldTrack = false 480 | 481 | return result 482 | } 483 | } 484 | ``` 485 | 486 | ::: 487 | :::: 488 | 489 | ::: warning 注意: 490 | 在设置完 `shouldTrack = true` 并执行了 `this._fn()` 之后,需要将 `shouldTrack` 还原为 `false`,否则下次依旧会进行依赖收集! 491 | ::: 492 | 493 | 这样一来,我们的测试就可以完美通过了~ 🥳 494 | 495 | ## 代码优化 496 | 497 | 现在我们回过头来再看看代码有什么值得优化的地方。 498 | 499 | 在 `tarck` 方法中,我们通过以下两个逻辑判断是否应该进行依赖收集: 500 | 501 | ```ts 502 | if (!activeEffect) return // 如果 activeEffect 不存在,直接返回 503 | if (!shouldTrack) return // 如果 shouldTrack = false,直接返回 504 | ``` 505 | 506 | 这种写法有点啰嗦,我们可以将这段逻辑封装成一个方法: 507 | 508 | ```ts 509 | // 判断是否在收集中 510 | function isTracking() { 511 | return shouldTrack && activeEffect !== undefined 512 | } 513 | ``` 514 | 515 | 这样一来代码会显得更优雅一些。 516 | -------------------------------------------------------------------------------- /docs/miniVue/notes/reactivity/10_ref.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | # ref 10 | 11 | ::: tip 12 | 本篇笔记对应的分支号为: `main分支: b75748a` 13 | ::: 14 | 15 | 官方文档中,对于 `ref` 功能的描述如下: 16 | 17 | :::tip 18 | 接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 `.value` property,指向该内部值。 19 | ::: 20 | 21 | ## happy path 22 | 23 | 我们先通过简单的 `happy path` 测试用例来看下 `ref` 所实现的功能: 24 | 25 | :::: code-group 26 | ::: code-group-item ref.spec.ts 27 | 28 | ```ts 29 | // src/reactivity/__tests__/ref.spec.ts 30 | 31 | it('happy path', () => { 32 | const a = ref(1) 33 | expect(a.value).toBe(1) 34 | }) 35 | ``` 36 | 37 | ::: 38 | :::: 39 | 40 | 通过测试用例我们可以看到,当我们给 `ref` 传入一个值之后,`ref` 返回了一个对象,对象上有个 `value` 属性,我们可以通过 `value` 访问到对应的值。 41 | 42 | 根据 `happy path`,我们先来实现一版简单的 `ref`。由于 `ref` 的返回值是个对象,因此我们可以定义一个名为 `RefImpl` 的类来包裹一下,当调用 `ref` 时,直接返回 `RefImpl` 的实例: 43 | 44 | :::: code-group 45 | ::: code-group-item ref.ts 46 | 47 | ```ts 48 | // src/reactivity/ref.ts 49 | 50 | class RefImpl { 51 | private _value: any 52 | 53 | constructor(value: any) { 54 | this._value = value 55 | } 56 | 57 | // 通过 get 的方式获取值 58 | get value() { 59 | return this._value 60 | } 61 | } 62 | 63 | export function ref(value) { 64 | return new RefImpl(value) 65 | } 66 | ``` 67 | 68 | ::: 69 | :::: 70 | 71 | 这样一来,我们就实现了 `丐版` 的 `ref`。 72 | 73 | ## 响应式处理 74 | 75 | 通过开头的定义我们知道,`ref` 的返回值也是 **响应式** 的。如果这个值具有 **响应式** 的特征,那么它就应该可以进行 `依赖收集` 以及 `依赖触发`。 76 | 77 | 回顾下之前我们在实现 `reactive` 的功能时,`依赖收集` 是在 `get` 阶段进行的,而 `依赖触发` 则是在 `set` 阶段进行的。同理,在 `ref` 中的 `依赖收集` 以及 `依赖触发` 同样也应该发生在 `get` 以及 `set` 阶段。(这也就是为什么我们需要通过定义 `RefImpl` 类来包裹的原因。因为 `ref` 可以接受基本类型的值,通过 `RefImpl` 的包裹,我们才可以借助类的 `get` 以及 `set` 特性实现 `依赖收集` 以及 `依赖触发`。) 78 | 79 | 我们先来看下测试用例: 80 | 81 | :::: code-group 82 | ::: code-group-item ref.spec.ts 83 | 84 | ```ts 85 | // src/reactivity/__tests__/ref.spec.ts 86 | 87 | it('should be reactive', () => { 88 | const a = ref(1) 89 | let dummy 90 | let calls = 0 91 | effect(() => { 92 | calls++ 93 | dummy = a.value 94 | }) 95 | expect(calls).toBe(1) 96 | expect(dummy).toBe(1) 97 | a.value = 2 98 | expect(calls).toBe(2) 99 | expect(dummy).toBe(2) 100 | }) 101 | ``` 102 | 103 | ::: 104 | :::: 105 | 106 | ### 依赖收集 107 | 108 | 我们先来根据测试用例实现下 `依赖收集` 。先回忆下我们之前依赖收集的实现过程: 109 | 110 | ![reactive](https://user-images.githubusercontent.com/9375823/174035124-13a100ba-3e6a-4da0-a9f9-ff74acef6942.png) 111 | 112 | 与之前不同的是,当 `ref` 传入的是基本类型的值时,不需要 `targetMap` 以及 `depsMap`,只需要 `deps` 就可以了,因此我们可以在 `RefImpl` 中定义 `deps` 用于存放依赖。由于收集依赖到 `deps` 中的逻辑与之前 `reactive` 中的逻辑是一样的,因此,我们可以把这部分逻辑抽取出来,之后,在 `RefImpl` 的 `get` 中调用: 113 | 114 | :::: code-group 115 | ::: code-group-item effect.ts 116 | 117 | ```ts{3-9,32} 118 | // src/reactivity/effect.ts 119 | 120 | export function trackEffects(deps: Set) { 121 | // 避免重复收集 122 | if (deps.has(activeEffect)) return 123 | // 将依赖对象保存到列 124 | deps.add(activeEffect) 125 | activeEffect.deps.push(deps) 126 | } 127 | 128 | /** 129 | * 收集依赖 130 | * @param target 需要收集依赖的对象 131 | * @param key 收集该key所对应的依赖 132 | */ 133 | export function track(target, key) { 134 | if (!isTracking()) return 135 | // 查找该对象对应的依赖池 136 | let depsMap = targetMap.get(target) 137 | // 如果没有(首次初始化时),则创建新的依赖池 138 | if (!depsMap) { 139 | depsMap = new Map() 140 | targetMap.set(target, depsMap) 141 | } 142 | // 从获取到的依赖池中获取该key所对应的依赖列表 143 | let deps = depsMap.get(key) 144 | // 如果没有,则新建一个该key对应的列表 145 | if (!deps) { 146 | deps = new Set() 147 | depsMap.set(key, deps) 148 | } 149 | trackEffects(deps) 150 | } 151 | ``` 152 | 153 | ::: 154 | 155 | ::: code-group-item ref.ts 156 | 157 | ```ts 158 | // src/reactivity/ref.ts 159 | 160 | import { trackEffects } from './effect' 161 | 162 | class RefImpl { 163 | private _value: any 164 | deps: Set 165 | 166 | constructor(value: any) { 167 | this._value = value 168 | this.deps = new Set() 169 | } 170 | 171 | get value() { 172 | trackEffects(this.deps) 173 | return this._value 174 | } 175 | } 176 | ``` 177 | 178 | ::: 179 | :::: 180 | 181 | ### 依赖触发 182 | 183 | 完成 `依赖` 收集后,我们来看看依赖触发的实现。 184 | 185 | 我们知道,依赖应该在值发生改变时被触发, 因此我们首先要为 `RefImpl` 类添加 `set` 方法,用于设置 `_value` 的值: 186 | 187 | :::: code-group 188 | ::: code-group-item ref.ts 189 | 190 | ```ts 191 | // src/reactivity/ref.ts 192 | 193 | class RefImpl { 194 | // 省略一大波代码 195 | 196 | set value(newValue: any) { 197 | this._value = newValue 198 | } 199 | } 200 | ``` 201 | 202 | ::: 203 | :::: 204 | 205 | 接下来,我们需要开始处理 `依赖触发` 环节。所谓的 `依赖触发`,其实就是将 `deps` 中的依赖取出来依次执行,这个逻辑在先前实现 `reactive` 的依赖触发逻辑时就已经实现过了。与 `track` 一样,`ref` 在触发时也不需要 `targetMap` 以及 `depsMap`,只需要直接遍历 `deps` 就可以了。因此,我们把这段逻辑也抽取到名为 `triggerEffects` 的方法中,并在 `ref` 的 `set` 方法中进行调用: 206 | 207 | :::: code-group 208 | ::: code-group-item effect.ts 209 | 210 | ```ts{3-11,27} 211 | // src/reactivity/effect.ts 212 | 213 | export function triggerEffects(deps) { 214 | for(const dep of deps) { 215 | // 判断是否存在 scheduler 方法,存在的的话执行 scheduler,否则执行run 216 | if(dep.scheduler) { 217 | dep.scheduler() 218 | } else { 219 | dep.run() 220 | } 221 | } 222 | } 223 | 224 | /** 225 | * 触发依赖 226 | * @param target 触发依赖的对象 227 | * @param key 触发该key对应的依赖 228 | */ 229 | export function trigger(target, key) { 230 | // 根据对象与key获取到所有的依赖,并执行 231 | const depsMap = targetMap.get(target) 232 | // 如果没有找到 depsMap, 直接 return 233 | if (!depsMap) { 234 | return 235 | } 236 | const deps = depsMap.get(key) 237 | triggerEffects(deps) 238 | } 239 | ``` 240 | 241 | ::: 242 | 243 | ::: code-group-item ref.ts 244 | 245 | ```ts 246 | // src/reactivity/ref.ts 247 | 248 | import { trackEffects, triggerEffects } from './effect' 249 | 250 | class RefImpl { 251 | // 省略一大波代码 252 | 253 | set value(newValue: any) { 254 | this._value = newValue 255 | triggerEffects(this.deps) 256 | } 257 | } 258 | ``` 259 | 260 | ::: 261 | :::: 262 | 263 | 好,这样一来 `依赖收集` 以及 `依赖触发` 的功能我们就已经实现完成了。回过头来看一看我们的测试用例: 264 | 265 | ![ref](https://user-images.githubusercontent.com/9375823/176814839-0ca2cb17-c1bc-4ff2-83cf-91130cd0ef6f.png) 266 | 267 | 嗯?????(黑人问号)怎么回事?怎么 `happy path` 都通过不了了?通过报错信息我们可以得知,无法从 `undefined` 上获取 `deps` 属性。那么问题出在哪儿呢? 268 | 269 | 我们回过头来再看看 `happy path` 做了哪些事情: 270 | 271 | > 1. 在 `happy path` 中,我们通过 `a.value` 的方式获取了 `ref` 返回对象上的值,因此会触发 `get`; 272 | > 2. 而在 `get` 中会执行 `trackEffects` 方法进行依赖收集; 273 | > 3. 在 `trackEffects` 中有个步骤 `activeEffect.deps.push(deps)` ,这一步是用来做反向收集的; 274 | > 4. 此时,`activeEffect` 并不存在,因为没有经过 `effect` 的处理,`ReactiveEffect` 尚未被实例化。 275 | 276 | 好,问题找到了,那我们该如何解决它呢?还记得我们之前抽取的 `isTracking` 方法么: 277 | 278 | ```ts 279 | // 判断是否在收集中 280 | export function isTracking() { 281 | return shouldTrack && activeEffect !== undefined 282 | } 283 | ``` 284 | 285 | 我们这里只需要引入它,并在 `依赖收集` 前进行判断即可: 286 | 287 | :::: code-group 288 | ::: code-group-item ref.ts 289 | 290 | ```ts{9} 291 | // src/reactivity/ref 292 | 293 | import { trackEffects, triggerEffects, isTracking } from './effect' 294 | 295 | class RefImpl { 296 | // 省略一大波代码 297 | get value() { 298 | // 可以进行 track 时才进行依赖收集 299 | isTracking() && trackEffects(this.deps) 300 | return this._value 301 | } 302 | } 303 | ``` 304 | 305 | ::: 306 | :::: 307 | 308 | 这样一来,我们的测试就可以通过啦~ 309 | 310 | ### 阻止重复触发 311 | 312 | 当我们修改 `ref` 返回的对象属性值时,会触发 `set` ,之后会执行 `依赖触发` 操作。试想下,当我们为同一个属性设置相同的值时,如果也要进行 `依赖触发` 的话,是不是就造成性能浪费了呢? 313 | 314 | 因此,我们在 `依赖触发` 之前需要判断值是否发生了改变,如果没有发生改变,那么则不触发 `依赖收集`。我们先来补充下测试用例: 315 | 316 | :::: code-group 317 | ::: code-group-item ref.spec.ts 318 | 319 | ```ts{17-19} 320 | // src/reactivity/__tests__/ref.spec.ts 321 | 322 | it('should be reactive', () => { 323 | const a = ref(1) 324 | let dummy 325 | let calls = 0 326 | effect(() => { 327 | calls++ 328 | dummy = a.value 329 | }) 330 | expect(calls).toBe(1) 331 | expect(dummy).toBe(1) 332 | a.value = 2 333 | expect(calls).toBe(2) 334 | expect(dummy).toBe(2) 335 | // same value should not trigger 336 | a.value = 2 337 | expect(calls).toBe(2) 338 | expect(dummy).toBe(2) 339 | }) 340 | ``` 341 | 342 | ::: 343 | :::: 344 | 345 | 之后,我们在 `RefImpl` 定义一个私有属性 `_rawValue` 用于存放原始值,而后在 `set` 时使用 [Object.is](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is) 判断是否需要更新值并触发依赖: 346 | 347 | :::: code-group 348 | ::: code-group-item ref.ts 349 | 350 | ```ts{6,22} 351 | // src/reactivity/ref.ts 352 | 353 | class RefImpl { 354 | private _value: any 355 | deps: Set 356 | private _rawValue: any // 保存原始值,用于 set 阶段对比值是否发生了变化 357 | 358 | constructor(value: any) { 359 | this._rawValue = value 360 | this._value = value 361 | this.deps = new Set() 362 | } 363 | 364 | get value() { 365 | // 可以进行 track 时才进行依赖收集 366 | isTracking() && trackEffects(this.deps) 367 | return this._value 368 | } 369 | 370 | set value(newValue: any) { 371 | // 如果值没有发生变化,不会触发 trigger 372 | if (Object.is(this._rawValue, newValue)) { 373 | this._rawValue = newValue 374 | this._value = newValue 375 | triggerEffects(this.deps) 376 | } 377 | } 378 | } 379 | ``` 380 | 381 | ::: 382 | :::: 383 | 384 | ## 复杂类型处理 385 | 386 | 在官方文档中,关于 `ref` 还有这样一句描述: 387 | 388 | :::tip 389 | 如果将对象分配为 ref 值,则它将被 `reactive` 函数处理为深层的响应式对象。 390 | ::: 391 | 392 | 上面我们实现的功能只是针对基本类型做的处理,根据官方的描述,当 `ref` 接受的是一个对象时,我们需要将其转换成 `reactive` 对象。我们先来编写下对应的测试用例: 393 | 394 | :::: code-group 395 | ::: code-group-item ref.spec.ts 396 | 397 | ```ts 398 | // src/reactivity/__tests__/ref.spec.ts 399 | 400 | it('should make nested properties reactive', () => { 401 | const a = ref({ count: 1 }) 402 | let dummy 403 | effect(() => { 404 | dummy = a.value.count 405 | }) 406 | expect(dummy).toBe(1) 407 | a.value.count = 2 408 | expect(dummy).toBe(2) 409 | }) 410 | ``` 411 | 412 | ::: 413 | :::: 414 | 415 | 因此,在 `get` 以及 `set` 阶段我们需要对传入的值进行判断, 当传入的值为对象时,我们需要处理成 `reactive` 对象,否则不作处理: 416 | 417 | :::: code-group 418 | ::: code-group-item ref.ts 419 | 420 | ```ts{3-4,13,27} 421 | // src/reactivity/ref.ts 422 | 423 | import { isObject } from '../shared' 424 | import { reactive } from './reactive' 425 | 426 | class RefImpl { 427 | private _value: any 428 | deps: Set 429 | private _rawValue: any // 保存原始值,用于 set 阶段对比值是否发生了变化 430 | 431 | constructor(value: any) { 432 | this._rawValue = value 433 | this._value = isObject(value) ? reactive(value) : value 434 | this.deps = new Set() 435 | } 436 | 437 | get value() { 438 | // 可以进行 track 时才进行依赖收集 439 | isTracking() && trackEffects(this.deps) 440 | return this._value 441 | } 442 | 443 | set value(newValue: any) { 444 | // 如果值没有发生变化,不会触发 trigger 445 | if (hasChanged(this._rawValue, newValue)) { 446 | this._rawValue = newValue 447 | this._value = isObject(newValue) ? reactive(newValue) : newValue 448 | triggerEffects(this.deps) 449 | } 450 | } 451 | } 452 | ``` 453 | 454 | ::: 455 | :::: 456 | 457 | 至此,我们的 `ref` 功能就已经实现完成啦~ 458 | 459 | ## 代码优化 460 | 461 | ### 抽取 `Object.is` 462 | 463 | 上述代码中,我们通过 `Object.is` 来判断值是否发生了改变。与之前用到的 `Object.assign` 一样,它可能会被多次调用。并且为了代码的可读性更高,因此我们可以定义一个名为 `hasChanged` 的方法,并将它抽取到 `share` 中: 464 | 465 | :::: code-group 466 | ::: code-group-item index.ts 467 | 468 | ```ts 469 | // src/shared/index.ts 470 | 471 | export const hasChanged = (val, newValue) => !(Object.is(val, newValue)) 472 | ``` 473 | 474 | ::: 475 | 476 | ::: code-group-item ref.ts 477 | 478 | ```ts{3,9} 479 | // src/reactivity/ref.ts 480 | 481 | import { hasChanged, isObject } from '../shared' 482 | 483 | class RefImpl { 484 | // 省略一大波代码 485 | set value(newValue: any) { 486 | // 如果值没有发生变化,不会触发 trigger 487 | if (hasChanged(this._rawValue, newValue)) { 488 | this._rawValue = newValue 489 | this._value = convert(newValue) 490 | triggerEffects(this.deps) 491 | } 492 | } 493 | } 494 | ``` 495 | 496 | ::: 497 | :::: 498 | 499 | ### 抽取重复代码 500 | 501 | 在 `set` 以及 `get` 中,我们都判断了值是否是对象类型,这边重复的逻辑我们可以将之抽取到名为 `convert` 的方法中: 502 | 503 | :::: code-group 504 | ::: code-group-item ref.ts 505 | 506 | ```ts{10,24,35-37} 507 | // src/reactivity/ref.ts 508 | 509 | class RefImpl { 510 | private _value: any 511 | deps: Set 512 | private _rawValue: any // 保存原始值,用于 set 阶段对比值是否发生了变化 513 | 514 | constructor(value: any) { 515 | this._rawValue = value 516 | this._value = convert(value) 517 | this.deps = new Set() 518 | } 519 | 520 | get value() { 521 | // 可以进行 track 时才进行依赖收集 522 | isTracking() && trackEffects(this.deps) 523 | return this._value 524 | } 525 | 526 | set value(newValue: any) { 527 | // 如果值没有发生变化,不会触发 trigger 528 | if (hasChanged(this._rawValue, newValue)) { 529 | this._rawValue = newValue 530 | this._value = convert(newValue) 531 | triggerEffects(this.deps) 532 | } 533 | } 534 | } 535 | 536 | /** 537 | * 判断传入 ref 的值是否是对象类型,如果是对象类型,需要使用 reactive 进行包裹 538 | * @param value 539 | * @returns 540 | */ 541 | function convert(value) { 542 | return isObject(value) ? reactive(value) : value 543 | } 544 | ``` 545 | 546 | ::: 547 | :::: 548 | -------------------------------------------------------------------------------- /docs/.vuepress/.temp/pages/miniVue/notes/reactivity/reactive.html.vue: -------------------------------------------------------------------------------- 1 | 322 | --------------------------------------------------------------------------------