├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .pnpm-debug.log ├── .prettierignore ├── .prettierrc.json ├── README.md ├── package.json ├── packages ├── compiler-core │ ├── notes │ │ └── compilerVFor.md │ ├── package.json │ ├── src │ │ ├── ast.ts │ │ ├── codegen.ts │ │ ├── compile.ts │ │ ├── index.ts │ │ ├── parse.ts │ │ ├── runtimeHelpers.ts │ │ ├── transform.ts │ │ └── transforms │ │ │ ├── transformElement.ts │ │ │ ├── transformText.ts │ │ │ └── transformsExpression.ts │ └── test │ │ ├── __snapshots__ │ │ └── codegen.spec.ts.snap │ │ ├── codegen.spec.ts │ │ ├── parse.spec.ts │ │ └── transform.spec.ts ├── reactivity │ ├── package.json │ ├── src │ │ ├── baseHandlers.ts │ │ ├── collectionHandlers.ts │ │ ├── computed.ts │ │ ├── effect.ts │ │ ├── index.ts │ │ ├── reactive.ts │ │ └── ref.ts │ └── test │ │ ├── computed.spec.ts │ │ ├── effect.spec.ts │ │ ├── reactive.spec.ts │ │ ├── readonly.spec.ts │ │ ├── ref.spec.ts │ │ └── shallowReadonly.spec.ts ├── runtime-core │ ├── package.json │ ├── src │ │ ├── apiDefineAsyncComponent.ts │ │ ├── apiDefineComponent.ts │ │ ├── apiInject.ts │ │ ├── apiLifecycle.ts │ │ ├── apiWatch.ts │ │ ├── commonsetupState.ts │ │ ├── component.ts │ │ ├── componentEmit.ts │ │ ├── componentProps.ts │ │ ├── componentRenderUtils.ts │ │ ├── componentSlots.ts │ │ ├── componentUpdateUtils.ts │ │ ├── components │ │ │ ├── KeepAlive.ts │ │ │ ├── Teleport.ts │ │ │ └── suspense.ts │ │ ├── createApp.ts │ │ ├── h.ts │ │ ├── index.ts │ │ ├── renderer.ts │ │ ├── scheduler.ts │ │ └── vnode.ts │ └── test │ │ └── watch.spec.ts ├── runtime-dom │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── modules │ │ ├── class.ts │ │ ├── event.ts │ │ └── style.ts │ │ └── patchProps.ts ├── shared │ ├── package.json │ └── src │ │ ├── ShapeFlags.ts │ │ ├── general.ts │ │ ├── index.ts │ │ └── toDisplayString.ts └── vue │ ├── example │ ├── HelloVue │ │ ├── App.js │ │ ├── index.html │ │ └── main.js │ ├── KeepAlive │ │ ├── App.js │ │ ├── components │ │ │ ├── bar.js │ │ │ ├── child.js │ │ │ └── foo.js │ │ ├── index.html │ │ └── main.js │ ├── LifeCycle │ │ ├── App.js │ │ ├── component │ │ │ ├── bar.js │ │ │ ├── foo.js │ │ │ └── index.js │ │ ├── index.html │ │ └── main.js │ ├── compiler-basic │ │ ├── App.js │ │ └── index.html │ ├── component │ │ ├── App.js │ │ ├── foo.js │ │ ├── index.html │ │ └── main.js │ ├── componentSlots │ │ ├── App.js │ │ ├── foo.js │ │ ├── index.html │ │ └── main.js │ ├── componentUpdate │ │ ├── App.js │ │ ├── child.js │ │ └── index.html │ ├── componentsAttrs │ │ ├── App.js │ │ ├── cpns │ │ │ └── Child.js │ │ └── index.html │ ├── defineAsyncComponent │ │ ├── App.js │ │ ├── App1.js │ │ ├── App2.js │ │ ├── App3.js │ │ ├── Error.js │ │ ├── asyncComponent.js │ │ ├── defaultPlaceholder.js │ │ └── index.html │ ├── nextTicker │ │ ├── App.js │ │ └── index.html │ ├── numberNotRender │ │ ├── App.js │ │ └── index.html │ ├── provide&inject │ │ ├── App.js │ │ ├── bar.js │ │ ├── foo.js │ │ ├── index.html │ │ └── main.js │ ├── teleport │ │ ├── App.js │ │ └── index.html │ ├── update_children │ │ ├── App.js │ │ ├── ArrayToArray.js │ │ ├── TextToArray&ArrayToText.js │ │ ├── TextToText.js │ │ ├── components │ │ │ ├── bar.js │ │ │ ├── foo.js │ │ │ └── index.js │ │ ├── index.html │ │ └── main.js │ ├── update_props │ │ ├── App.js │ │ ├── index.html │ │ └── main.js │ └── watchEffect │ │ ├── App.js │ │ └── index.html │ ├── package.json │ └── src │ └── index.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── rollup.config.js ├── tsconfig.json └── vitest.config.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'ci' 2 | on: [pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: ctions/checkout@v3 10 | 11 | - name: Install pnpm 12 | uses: pnpm/action-setup@v2 13 | 14 | - name: Set node version to 18 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 18 18 | cache: 'pnpm' 19 | 20 | - run: pnpm install 21 | 22 | - name: Run tests 23 | run: pnpm run test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /lib/ 3 | /.vscode 4 | coverage 5 | /packages/vue/lib/ -------------------------------------------------------------------------------- /.pnpm-debug.log: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | .gitignore 5 | *.html 6 | lib 7 | pnpm-lock.yaml 8 | package.json 9 | babel.config.js 10 | README.md 11 | rollup.config.js 12 | tsconfig.json 13 | coverage -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "vueIndentScriptAndStyle": true, 7 | "singleQuote": true, 8 | "quoteProps": "as-needed", 9 | "bracketSpacing": true, 10 | "trailingComma": "none", 11 | "jsxBracketSameLine": false, 12 | "jsxSingleQuote": false, 13 | "arrowParens": "always", 14 | "insertPragma": false, 15 | "requirePragma": false, 16 | "proseWrap": "never", 17 | "htmlWhitespaceSensitivity": "strict", 18 | "endOfLine": "lf" 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coderwei-mini-vue3 2 | 3 | 手写 vue3 核心源码,理解其原理 by myself。 4 | 5 | 项目包含大量注释,在核心且具有跳跃性的点进行了注释,站着阅读者的视角,特别是关联其他模块某个方法or用到其他 6 | 方法处理的结果,在该位置备注处理函数名及函数位置。保证每个阅读者都能跟着流程走完能够在心里对vue的整个流程有大致的认知。 7 | 8 | 无论是应付面试,还是希望通过有一个跷板进入vue3源码的世界,希望本项目都能够给到你帮助。项目还在持续更新中,力争完成vue的所有核心逻辑,进度在功能清单。 9 | 10 | ## 🕳️ 声明 11 | 项目是通过阅读[vue3](https://github.com/vuejs/core/tree/main)源码,函数名、代码组织方式都与vue3官方保持一致,抽离一切非vue的核心逻辑。**如果大家在阅读过程中发现任何问题,欢迎在issue中提出,同时也欢迎大家提交PR。当然如果在阅读过程中有什么疑惑,也欢迎在issue中提出。** 12 | 13 | ## 🙌 使用方式 14 | 15 | 项目采取monorepo结构,打包入口和打包出口已配置好,甚至包含我进行测试的example和打包后的文件,可以直接去/packages/vue/example直接运行index.html文件 16 | 17 | 当然也可以选择自己打包 18 | ~~~shell 19 | pnpm run build 20 | or 21 | nr build 22 | ~~~ 23 | ## 🗯️ 插件 24 | 1. 这里推荐大家使用[ni](https://github.com/antfu/ni) 25 | 26 | 2. 在运行index.html文件的时候同样推荐大家安装vscode插件[Live Server](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer) 27 | 28 | 29 | 30 | ## ✏ 相关参考 31 | 1. [Vue3 核心原理代码解构](https://juejin.cn/column/7089244418703622175) 32 | 33 | 2. [崔学社](https://github.com/cuixiaorui/mini-vue) 34 | 35 | 3. [Vue.js 的设计与实现](https://item.jd.com/13611922.html) 36 | 37 | ## 🛠 功能清单 38 | 39 | ### reactivity 部分 40 | #### 响应式系统的实现 41 | - [x] 实现 computed 计算属性功能 42 | - [x] 实现 cleanup 功能 & 分支切换 功能 (避免无意义的更新 见/packages/reactivity/test/effect.spec.ts的skip unnecessary update测试) 43 | 44 | #### effect 45 | - [x] 实现 effect 依赖收集和依赖触发 46 | - [x] 实现 effect 返回 runner 47 | - [x] 实现 effect 的 scheduler 功能(调度执行) 48 | - [x] 实现 effect 的 stop 功能 49 | - [x] 优化 stop 功能 50 | - [x] 实现嵌套 effect 函数 51 | 52 | #### reactive 53 | - [x] 实现 reactive 依赖收集和依赖触发 54 | - [x] 实现 readonly 功能 55 | - [x] 实现 isReactive 和 isReadonly 功能 56 | - [x] 实现 readonly 和 reactive 嵌套对象功能 57 | - [x] 实现 shallowReadonly 功能 58 | - [x] 实现 shallowReactive 功能 59 | - [x] 实现 isProxy 功能 60 | - [x] 实现 isShallow 功能 61 | 62 | #### 代理对象 63 | - [x] 实现拦截in操作符(xx in obj 是不会触发 get 和 set 操作的 他会触发 has 操作 所以需要针对in操作符在 proxy 完善 has 拦截器) 64 | - [x] 实现拦截 delete 操作符(delete obj.xxx 是不会触发 get 和 set 操作的 他会触发 deleteProperty 操作 所以需要针对 delete 操作符在 proxy 完善 deleteProperty 拦截器) 65 | - [x] 实现拦截 for in 语句(for(let key in obj){your code...} 是不会触发get和set操作的 他会触发 ownKeys 操作 所以需要针对 in 操作符在 proxy 完善 ownKeys 拦截器) 66 | - [x] 屏蔽由于原型引起的无意义更新 67 | 68 | #### 代理数组 69 | - [x] 实现数组 for in 循环的依赖收集与触发 && length 属性的依赖收集与触发 70 | - [x] 重写 array 的 includes & indexOf & lastIndexOf 方法 71 | - [x] 重写 array 的 push & pop & shift & unshift & splice 方法 (这几个方法会影响数组的length属性 如果不屏蔽对length属性的依赖会造成死循环) 72 | 73 | #### 代理Set 74 | - [x] 针对 Set 类型的 size 属性进行依赖收集与触发 75 | - [x] 重写 Set 类型的 add & delete & forEach 方法 76 | - [x] 重写 Set 类型的迭代器[Sybol.iterator]方法 77 | - [x] 重写 Set 类型的 keys & values & entries 方法 78 | 79 | #### 代理Map 80 | - [x] 针对 Map 类型的 size 属性进行依赖收集与触发 81 | - [x] 重写 Map 类型的 set & get & forEach 方法 82 | - [x] 重写 Map 类型的迭代器[Sybol.iterator]方法 83 | - [x] 重写 Map 类型的 keys & values & entries 方法 84 | 85 | #### 代理原始数组类型 86 | - [x] 实现 ref 功能 87 | - [x] 实现 isRef 和 unRef 功能 88 | - [x] 实现 proxyRefs 功能 89 | - [x] 实现 toRef 功能 90 | - [x] 实现 toRefs 功能 91 | 92 | ### runtime-core 部分 93 | - [x] 实现初始化 component 主流程 94 | - [x] 实现初始化 element 主流程 (通过递归 patch 拆箱操作,最终都会走向 mountElement 这一步) 95 | - [x] 实现组件代理对象 (instance.proxy 解决`render()`函数的 this 指向问题) 96 | - [x] 实现 shapeFlags (利用位运算 左移运算 对 vnode 添加标识,标识是什么类型:子级文本,子级数组,组件,HTML 元素) 97 | - [x] 实现注册事件功能 (通过在 vnode.props 识别 props 对象的 key 是以 on 开头并且后一个字母是大写来判断是否是事件) 98 | - [x] 实现组件 props 功能 (在 render 的 h 函数中可以用 this 访问到,并且是 shallowReadonly) 99 | - [x] 实现组件 attrs 功能 100 | - [x] 实现组件 emit 功能 (获取组件的 props 并判断 props 的'on+事件名'是否是 emit 的第一个参数:事件名匹配,是的话就执行 props 的里面的事件) 101 | - [x] 实现组件 slots 功能 (具名插槽&作用域插槽) 102 | - [x] 实现 Fragment 和 Text 类型节点 (避免固定死外层嵌套某个元素 比如说 div,使用 Fragment/Text 标识符 直接不渲染外层的 div,直接走 mountChildren 函数 处理 children 外层用户需要什么节点进行包裹自行选择) 103 | - [x] 实现 getCurrentInstance 104 | - [x] 实现 provide-inject 功能 105 | - [x] 实现自定义渲染器 custom renderer 106 | - [x] 更新 element 流程搭建 107 | - [x] 更新 element 的 props 108 | - [x] 更新 element 的 children 109 | - [x] 更新 element 的双端对比 diff 算法 110 | - [x] 实现组件更新功能 111 | - [x] 实现更新组件props功能 112 | - [x] 实现更新组件attrs功能 113 | - [x] nextTick 的实现 (vue3 视图更新是异步的,如果我们想在组件更新的时候拿到当前组件的实例或者是操作当前组件的某些数据、dom,正常情况下是拿不到的,因为我们写在 script 标签内的代码都是同步的,那个时候视图还没有更新,拿到的自然都是旧数据) 114 | - [x] 实现 watch 功能 115 | - [x] 实现 watchEffect 功能 116 | - [x] 实现 watchPostEffect 功能 117 | - [x] 实现 watchSyncEffect 功能 118 | - [x] 实现 defineComponent 功能 119 | - [x] 实现 defineAsyncComponent 功能 120 | - [ ] 实现 defineCustomElement 功能 121 | - [x] 实现 KeepAlive 组件 122 | - [x] 实现 Teleport 组件 123 | - [ ] 实现 Suspense 组件 124 | - [x] 实现 vue3的生命周期 125 | 126 | ### runtime-dom 127 | - [x] 实现浏览器端的渲染逻辑(插入节点、删除节点、更新节点等等) 128 | - [ ] 实现 Transition 组件 129 | - [ ] 实现 TransitionGroup 组件 130 | 131 | ### compiler-core 部分 132 | - [x] 实现 parse 模块 133 | - [x] 实现 transform 模块 134 | - [x] 实现 codegen 模块 135 | - [x] 定义统一的出口(定义 baseCompiler 函数) 136 | 137 | ### 兼容vue2部分 138 | - [ ] 兼容options api 139 | - [ ] 兼容vue2的生命周期 (beforeCreate、created等) 140 | 141 | ### 内置指令 142 | - [ ] v-text 143 | - [ ] v-html 144 | - [ ] v-show 145 | - [ ] v-if 146 | - [ ] v-else 147 | - [ ] v-else-if 148 | - [ ] v-for 149 | - [ ] v-on 150 | - [ ] v-bind 151 | - [ ] v-model 152 | - [ ] v-slot 153 | - [ ] v-pre 154 | - [ ] v-once 155 | - [ ] v-memo 156 | - [ ] v-cloak 157 | 158 | 159 | ### monorepo 160 | - [x] 使用monorepo重构整个项目 161 | - [x] 使用vitest进行TDD测试 162 | 163 | ## 遗留 bug 164 | > 先走完整个流程,这里记录调试过程中发现的小 bug,走完整个流程在处理这些 bug,顺便迫使自己重新梳理整个流程 165 | 166 | - [x] 目前处理子组件 children 的逻辑有问题,当子组件的 children 为字符串的时候,渲染会出错 167 | - [x] 组件的切换无法正常切换,初步估计是前面的代码不兼容组件销毁的操作 168 | - [x] h函数children参数传递数字不渲染 (包括使用变量但是变量是一个数字类型) 在进行patchChildren的时候都不成立 patchFlag为1 需要单独处理 169 | - [x] h函数传递两个参数不渲染 h('div','hello word') 170 | - [x] 渲染普通element元素 props传入style 最终生成的dom是style="object Object"的形式 需要在设置属性的时候进行区分(style、class) 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": "true", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "scripts": { 7 | "test": "vitest", 8 | "build": "rollup -c -w", 9 | "prettier": "prettier --write .", 10 | "coverage": "vitest run --coverage" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "typescript": "^4.7.4" 17 | }, 18 | "devDependencies": { 19 | "@rollup/plugin-commonjs": "^24.1.0", 20 | "@rollup/plugin-node-resolve": "^15.0.2", 21 | "@rollup/plugin-replace": "^5.0.2", 22 | "@rollup/plugin-typescript": "^8.3.3", 23 | "@vitest/coverage-c8": "^0.30.1", 24 | "rollup": "^2.79.1", 25 | "rollup-plugin-sourcemaps": "^0.6.3", 26 | "tslib": "^2.4.0", 27 | "vite": "^4.3.5", 28 | "vitest": "^0.30.1" 29 | } 30 | } -------------------------------------------------------------------------------- /packages/compiler-core/notes/compilerVFor.md: -------------------------------------------------------------------------------- 1 | # 编译 v-for 相关的注意事项以及生成 ast 语法树的含义 2 | 3 | ## 当编译遇到 v-for 4 | 5 | 1. 首先明确 v-for 等内置指令,一定是作为元素/组件的属性存在的 6 | 7 | ```html 8 | 9 |
{{item}}
10 | ``` 11 | 12 | 2. 解析 v-for 生成的 ast 类型,这里就不深究探讨对 v-for 的编译流程了 放在下面进行探究 13 | 14 | ```TypeScript 15 | // example 16 | let expectAst = [ 17 | { 18 | type: NodeTypes.ROOT, 19 | children: [ 20 | { 21 | children: [ 22 | { 23 | children: [ 24 | { 25 | type: NodeTypes.INTERPOLATION, 26 | content: { 27 | content: 'item', 28 | type: NodeTypes.SIMPLE_EXPRESSION 29 | } 30 | } 31 | ], 32 | tag: 'div', 33 | type: NodeTypes.ELEMENT 34 | } 35 | ], 36 | source:{ 37 | content:"_ctx.data", 38 | type:NodeTypes.SIMPLE_EXPRESSION 39 | }, 40 | type: NodeTypes.FOR 41 | } 42 | ] 43 | } 44 | ] 45 | 46 | 47 | ``` 48 | 49 | ## compiler 遇到 v-for 的整体处理流程 50 | 51 | ```html 52 | 53 |
{{item}}
54 | ``` 55 | 56 | ### 首先明确编译的入口 57 | 58 | baseParse 首先创建 context 上下文 该上下文有一个属性 source 就是当前的源代码 '\
{{item}}<\/div>' 后续我们就不停的对这个 source 进行操作即可 然后通过调用 parseChildren 进行解析该字符串 并且把最后解析的结果通过 createRoot 函数放到一个对象的 children 里面 这个对象就是我们的根节点 接下来我们就重点看看 parseChildren 函数 毕竟这个函数才是我们的重中之重 59 | 60 | 1. parseChildren 入口 61 | 62 | ~~~typescript 63 | //
{{item}}
首先遇到< 开头的 就看看是否是小写字母a-z 如果是的话 则作为element元素 走parseElement逻辑 我们这里明显是成立的 64 | const nodes = [] 65 | while(isEnd){ 66 | // 判断条件省略了... 67 | node = parseElement(context, ancestors) 68 | } 69 | 70 | return nodes 71 | ~~~ 72 | 73 | 2. parseElement 函数 74 | 75 | - 第一步 先调用 parseTag 函数分析出是什么标签 76 | 77 | ```typescript 78 | const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source) 79 | // 通过正则可以解析出当前当前标签 并且从<一直到后面最近的空格,但是不包含这个空格({{item}}
' 82 | advanceSpaces(context) 83 | //这个也很简单 主要是为了推进空格 于是source变成了:'v-for="item in data">{{item}}' 84 | let props = parseAttributes(context, type) 85 | // 然后紧跟着处理attrs 这里处理的attrs包括原生的属性、自定义指令或者是内置指令 86 | 87 | return { 88 | type: NodeTypes.ELEMENT, 89 | ns, // 命名空间 暂时用不上 90 | tag, // 标签名 91 | tagType, // 标签类型 92 | props, // props 93 | isSelfClosing, //是否闭合标签 94 | children: [], 95 | loc: getSelection(context, start), // 位置信息 96 | codegenNode: undefined // to be created during transform phase 97 | } 98 | ``` 99 | 100 | - parseAttributes 函数 101 | 102 | ~~~typescript 103 | // 本质上也很简单 就是直接调用了parseAttribute 不过这里是通过while不停地调用 因为我们没办法确定用户到底写了多少属性 我们需要拿到所有的属性 104 | while(context.source.length > 0 || context.source.startsWith('>')){ 105 | const attr = parseAttribute(context, attributeNames) 106 | } 107 | ~~~ 108 | 109 | - parseAttribute 函数 110 | 111 | ~~~typescript 112 | const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source) 113 | // 通过这个正则可以匹配到'='前面的所有内容 我们这里就是能够拿到'v-for' 然后推进对应长度(这里就是v-for这个字符串的长度) 114 | name = match[0] // 'v-for' 115 | advanceBy(context, name.length) 116 | // 这个时候source变成了:'="item in data">{{item}}' 紧接着处理value 117 | 118 | if (/^[\t\r\n\f ]*=/.test(context.source)) { 119 | // 这个正则表达式是用于判断一个字符串是否以零个或多个空白字符 如果是 则进入下面的代码 120 | advanceSpaces(context) 121 | // 我们先推移空白部分 122 | advanceBy(context, 1) 123 | // 推移这个'=' 124 | advanceSpaces(context) 125 | // 在推移'='后面的空白部分 经过上面代码 source变成: '"item in data">{{item}}' 126 | value = parseAttributeValue(context) 127 | // 处理value 128 | } 129 | 130 | // 接下来需要处理对应的指令 131 | const repe = /^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(name) //该正则就可以匹配以v-开头的指令 因为我们保存的name是属性的key,他可能是class、id、type等原生element的属性 我们需要在这里加以区分 132 | ~~~ 133 | 134 | - parseAttributeValue 函数 135 | 136 | ~~~typescript 137 | // 简单来说 parseAttributeValue只是负责将解析出"item in data" 这个东西 但是vue源码内部还多做了一个事情 就是做完以上的事情之后还在这里调用了parseTextData 该函数的处理逻辑只是单纯的处理几个html的实体编码 比如说 138 |
hello <word>
139 | // 经过parseTextData会转译成
hello
140 | ~~~ 141 | 142 | 143 | -------------------------------------------------------------------------------- /packages/compiler-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coderwei-mini-vue3/compiler-core", 3 | "version": "1.0.0", 4 | "description": "@coderwei-mini-vue3/compiler-core", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "vitest" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@coderwei-mini-vue3/shared": "workspace:^1.0.0" 14 | } 15 | } -------------------------------------------------------------------------------- /packages/compiler-core/src/ast.ts: -------------------------------------------------------------------------------- 1 | export const enum NodeTypes { 2 | INTERPOLATION, 3 | SIMPLE_EXPRESSION, 4 | ELEMENT, 5 | TEXT, 6 | ROOT, 7 | COMPOUND_EXPRESSION, 8 | FOR, 9 | DIRECTIVE 10 | } 11 | -------------------------------------------------------------------------------- /packages/compiler-core/src/codegen.ts: -------------------------------------------------------------------------------- 1 | import { isString } from '@coderwei-mini-vue3/shared' 2 | import { NodeTypes } from './ast' 3 | import { CREATE_ELEMENT_BLOCK, helperNameMap, TO_DISPLAY_STRING } from './runtimeHelpers' 4 | 5 | export function generate(ast) { 6 | const context = createCodegenContext() 7 | const { push } = context 8 | 9 | // 处理插值语法 10 | if (ast.helpers.length != 0) genFunctionPreamble(ast, context) 11 | push('return ') 12 | const functionName = 'render' 13 | const args = ['_ctx', '_cache'] 14 | const signature = args.join(',') 15 | push(`function ${functionName}(${signature}){`) 16 | push(' return ') 17 | 18 | getNode(ast.codegenNode, context) 19 | push('}') 20 | 21 | return { 22 | code: context.code 23 | } 24 | } 25 | 26 | function genFunctionPreamble(ast: any, context) { 27 | const { push } = context 28 | const VueBinging = 'Vue' 29 | const helpers = ast.helpers 30 | 31 | const ailasHelper = (helperName) => `${helperNameMap[helperName]}: _${helperNameMap[helperName]}` 32 | push(`const { ${helpers.map(ailasHelper).join(', ')} } = ${VueBinging}`) 33 | push('\n') 34 | } 35 | 36 | function getNode(ast, context) { 37 | switch (ast.type) { 38 | case NodeTypes.TEXT: 39 | // 处理文本类型 40 | genText(ast, context) 41 | break 42 | 43 | case NodeTypes.INTERPOLATION: 44 | // 处理插值类型 45 | genInterpolation(ast, context) 46 | break 47 | 48 | case NodeTypes.SIMPLE_EXPRESSION: 49 | // 处理表达式 指的是插值类型里面那个变量 50 | genExpression(ast, context) 51 | break 52 | 53 | case NodeTypes.ELEMENT: 54 | genElement(ast, context) 55 | break 56 | 57 | case NodeTypes.COMPOUND_EXPRESSION: 58 | genCompundExpression(ast, context) 59 | break 60 | 61 | default: 62 | break 63 | } 64 | } 65 | 66 | function genExpression(ast: any, context) { 67 | const { push } = context 68 | push(`${ast.content}`) 69 | } 70 | 71 | function genText(ast: any, context) { 72 | const { push } = context 73 | push(`'${ast.content}'`) 74 | } 75 | 76 | function genInterpolation(ast: any, context: any) { 77 | const { push } = context 78 | push(`_${helperNameMap[TO_DISPLAY_STRING]}(`) 79 | getNode(ast.content, context) 80 | push(')') 81 | } 82 | 83 | function genElement(ast, context) { 84 | const { push } = context 85 | push(`_${helperNameMap[CREATE_ELEMENT_BLOCK]}(`) 86 | genNodeList(genNullable([ast.tag, ast.prop, ast.children]), context) 87 | // getNode(ast.children, context); 88 | push(')') 89 | } 90 | 91 | function genNullable(nodesList: any[]) { 92 | return nodesList.map((node) => node || 'null') 93 | } 94 | 95 | function genNodeList(nodesList: any[], context) { 96 | const { push } = context 97 | for (let i = 0; i < nodesList.length; i++) { 98 | let node = nodesList[i] 99 | if (isString(node)) { 100 | push(node) 101 | } else { 102 | getNode(node, context) 103 | } 104 | if (i < nodesList.length - 1) { 105 | push(',') 106 | } 107 | } 108 | } 109 | 110 | function genCompundExpression(ast: any, context: any) { 111 | const { children } = ast 112 | const { push } = context 113 | 114 | children.forEach((child) => { 115 | if (isString(child)) { 116 | push(child) 117 | } else { 118 | getNode(child, context) 119 | } 120 | }) 121 | } 122 | 123 | function createCodegenContext() { 124 | const context = { 125 | code: '', 126 | push: (source) => (context.code += source) 127 | } 128 | 129 | return context 130 | } 131 | -------------------------------------------------------------------------------- /packages/compiler-core/src/compile.ts: -------------------------------------------------------------------------------- 1 | import { generate } from './codegen' 2 | import { baseParse } from './parse' 3 | import { transform } from './transform' 4 | import { transformElement } from './transforms/transformElement' 5 | import { transformsExpression } from './transforms/transformsExpression' 6 | import { transformText } from './transforms/transformText' 7 | 8 | export const baseCompile = (template) => { 9 | const ast = baseParse(template) 10 | transform(ast, { 11 | nodeTransform: [transformsExpression, transformElement, transformText] 12 | }) 13 | 14 | return generate(ast) 15 | } 16 | -------------------------------------------------------------------------------- /packages/compiler-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compile' 2 | -------------------------------------------------------------------------------- /packages/compiler-core/src/parse.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 处理插值的整体流程 {{message}} 3 | * 思路: 拿到{{message}}后 对其进行处理 首先我们需要明确一点 我们拿到的是字符串 就是无脑切割、推进的一个过程 4 | * 首先先确定结束的位置 根据插值语法的使用 我们很明确的清楚 }}就是我们的结束标记 所以先通过indexOf拿到该从那个位置进行截断 5 | * 然后同样的 {{ 这个也就是我们的开始标记 6 | * 1. 我们先将{{message}} 切割掉"{{"这两个开始的标记获取到message}} 7 | * 2. 然后上面讲了 "}}"这个就是我们的结束标记 我们定义一个变量标志结束的位置 8 | * 3. 根据结束的位置 切割掉"}}" 然后我们就能拿到中间核心的变量了 当然这里要注意 我们获取的结束标记是完整的字符串{{message}} 经过第一步 实际上字符串已经变成了message}}这个玩意 所以我们的结束标记也要-2 9 | * 4. 最后进行推进 因为后面可能还有别的标签我们需要处理 比如说这种dom标签 我们要进行别的处理 推进的方式也很简单 说白了就是将整个{{message}}删掉 然后继续后面的字符串解析 10 | * 11 | */ 12 | 13 | import { extend } from '@coderwei-mini-vue3/shared' 14 | import { NodeTypes } from './ast' 15 | 16 | // 定义开始标识符和结束标识符 17 | const OPENDELIMITER = '{{' 18 | const CLOSEDELIMITER = '}}' 19 | 20 | // 定义标签的开始于结束 21 | export enum TagTypes { 22 | TAGSSTART, 23 | TAGSEND 24 | } 25 | 26 | export const enum TextModes { 27 | ATTRIBUTE_VALUE 28 | } 29 | 30 | interface OptionalOptions { 31 | decodeEntities: (rawText: string) => string 32 | } 33 | export interface ParserContext { 34 | options: OptionalOptions 35 | source 36 | } 37 | 38 | // element元素 39 | export const enum ElementTypes { 40 | ELEMENT, 41 | COMPONENT, 42 | SLOT, 43 | TEMPLATE 44 | } 45 | 46 | const decodeRE = /&(gt|lt|amp|apos|quot);/g 47 | const decodeMap: Record = { 48 | gt: '>', 49 | lt: '<', 50 | amp: '&', 51 | apos: "'", 52 | quot: '"' 53 | } 54 | 55 | export const defaultParserOptions = { 56 | decodeEntities: (rawText: string): string => rawText.replace(decodeRE, (_, p1) => decodeMap[p1]) 57 | } 58 | 59 | export function baseParse(content: string) { 60 | const context = createParseContext(content) 61 | 62 | return createRoot(parseChildren(context, [])) 63 | } 64 | 65 | function isEnd(context: ParserContext, ancestors) { 66 | // 是否结束 67 | // 1. 当遇到结束标签 比如: 68 | // 2. 当context.source.length === 0 69 | const s = context.source 70 | // console.log(ancestors) 71 | if (s.startsWith('`)) { 81 | // return true; 82 | // } 83 | 84 | return !s 85 | } 86 | 87 | function parseChildren(context: ParserContext, ancestors) { 88 | // console.log(context.source, "-------------"); 89 | 90 | const nodes: any[] = [] 91 | while (!isEnd(context, ancestors)) { 92 | let node 93 | if (context.source.startsWith(OPENDELIMITER)) { 94 | node = parseInterpolation(context) 95 | } else if (context.source[0] === '<') { 96 | // console.log("parse"); 97 | if (/[a-z]/i.test(context.source[1])) { 98 | node = parseElement(context, ancestors) 99 | } 100 | } 101 | 102 | if (!node) { 103 | // 如果node没有值的情况下 我们默认当做text类型来处理 就是普通文本 104 | node = parseText(context) 105 | } 106 | nodes.push(node) 107 | } 108 | 109 | return nodes 110 | } 111 | 112 | function parseInterpolation(context: ParserContext) { 113 | // "{{message}}" 114 | // "message}}" 115 | 116 | const closeIndex = context.source.indexOf(CLOSEDELIMITER, OPENDELIMITER.length) 117 | // console.log(closeIndex, "clostIndex"); 118 | 119 | advanceBy(context, OPENDELIMITER.length) 120 | // console.log(context.source.slice(0, closeIndex - 2)); 121 | const rawContent = context.source.slice(0, closeIndex - 2) 122 | const content = rawContent.trim() 123 | advanceBy(context, closeIndex) 124 | 125 | // console.log(context.source, "处理完成之后 content"); 126 | 127 | return { 128 | type: NodeTypes.INTERPOLATION, 129 | content: { 130 | type: NodeTypes.SIMPLE_EXPRESSION, 131 | content: content 132 | } 133 | } 134 | } 135 | 136 | function createParseContext(content: string): ParserContext { 137 | const options = extend({}, defaultParserOptions) 138 | return { 139 | options, 140 | source: content 141 | } 142 | } 143 | 144 | function createRoot(children) { 145 | return { 146 | children, 147 | type: NodeTypes.ROOT 148 | } 149 | } 150 | 151 | // 插值语法的推进函数 152 | function advanceBy(context: ParserContext, length: number) { 153 | context.source = context.source.slice(length) 154 | } 155 | 156 | // 推进多余的空格 157 | function advanceSpaces(context: ParserContext): void { 158 | const match = /^[\t\r\n\f ]+/.exec(context.source) 159 | if (match) { 160 | advanceBy(context, match[0].length) 161 | } 162 | } 163 | 164 | function parseElement(context: ParserContext, ancestors) { 165 | const element: any = parseTag(context, TagTypes.TAGSSTART) //处理开始标签 166 | ancestors.push(element) 167 | element.children = parseChildren(context, ancestors) 168 | ancestors.pop() 169 | // console.log( 170 | // context.source, 171 | // context.source.slice(2, 2 + element.tag.length), 172 | // element.tag, 173 | // '--------------------' 174 | // ) 175 | 176 | if (context.source.slice(2, 2 + element.tag.length) == element.tag) { 177 | // 先判断结束标签是否和开始标签一致 178 | parseTag(context, TagTypes.TAGSEND) //处理结束标签 179 | } else { 180 | throw new Error('没有结束标签') 181 | } 182 | 183 | // console.log(context.source); 184 | 185 | return element 186 | } 187 | 188 | function parseTag(context: ParserContext, type: TagTypes) { 189 | // console.log(context.source) 190 | const match: any = /^<\/?([a-z]*)/i.exec(context.source) 191 | // console.log(match, '------------') 192 | 193 | advanceBy(context, match[0].length) //推进开始标签 194 | advanceSpaces(context) //推进多余的空格 195 | 196 | // 处理attributes 197 | let props = parseAttributes(context, type) 198 | 199 | advanceBy(context, 1) //推进多余的> 200 | 201 | const tag = match[1] 202 | 203 | if (type == TagTypes.TAGSEND) return //如果是结束标签 就没必要返回内容了 204 | 205 | let tagType = ElementTypes.ELEMENT 206 | // TODO tagType 目前写死的ELEMENT 后续需要判断slot、 component、template 207 | return { 208 | type: NodeTypes.ELEMENT, 209 | props, 210 | tag, 211 | tagType 212 | } 213 | } 214 | 215 | function parseText(context: ParserContext): any { 216 | let endIndex = context.source.length 217 | let endToken = ['<', '{{'] 218 | 219 | for (let i = 0; i < endToken.length; i++) { 220 | const index = context.source.indexOf(endToken[i]) 221 | // console.log(index, 'index') 222 | if (index !== -1 && endIndex > index) { 223 | endIndex = index 224 | } 225 | } 226 | 227 | const content = context.source.slice(0, endIndex) 228 | // console.log(content); 229 | 230 | advanceBy(context, content.length) 231 | return { 232 | type: NodeTypes.TEXT, 233 | content 234 | } 235 | } 236 | 237 | function parseAttributes(context: ParserContext, type: TagTypes) { 238 | const props: any[] = [] 239 | const attributesName = new Set() 240 | 241 | if (context.source.startsWith('>') || context.source.startsWith('/>')) return props 242 | const attr = parseAttribute(context, attributesName)! 243 | if (type === TagTypes.TAGSSTART) { 244 | props.push(attr) 245 | } 246 | return props 247 | } 248 | function parseAttribute(context: ParserContext, nameSet) { 249 | // 处理key 250 | const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)! 251 | // 拿到=前面的部分 252 | const name = match[0] 253 | nameSet.add(name) 254 | 255 | advanceBy(context, name.length) 256 | 257 | // 处理value 258 | let value 259 | if (/^[\t\r\n\f]*=/.test(context.source)) { 260 | // 该正则表示如果context.source是以0个或多个空白字符开头并且紧跟一个= 那么就是true 261 | advanceSpaces(context) 262 | advanceBy(context, 1) //推进'=' 263 | advanceSpaces(context) 264 | value = parseAttributeValue(context) 265 | } 266 | 267 | // 处理v-for等指令 268 | if (/^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(name)) { 269 | const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(name)! 270 | let dirName = match[1] 271 | 272 | return { 273 | type: NodeTypes.DIRECTIVE, 274 | name: dirName 275 | } 276 | } 277 | } 278 | 279 | function parseAttributeValue(context: ParserContext) { 280 | let content 281 | let quote = context.source[0] 282 | let isQuoted = quote === `'` || quote === `"` 283 | if (isQuoted) { 284 | // 如果是以" 或者 ' 开头的 285 | advanceBy(context, 1) 286 | const endIndex = context.source.indexOf(quote) 287 | if (endIndex !== -1) { 288 | content = parseTextData(context, endIndex, TextModes.ATTRIBUTE_VALUE) 289 | advanceBy(context, 1) 290 | } 291 | } 292 | return { 293 | content, 294 | isQuoted 295 | } 296 | } 297 | 298 | function parseTextData(context: ParserContext, endIndex, mode: TextModes) { 299 | const rawText = context.source.slice(0, endIndex) 300 | advanceBy(context, endIndex) 301 | if (mode === TextModes.ATTRIBUTE_VALUE) { 302 | return context.options.decodeEntities(rawText) 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /packages/compiler-core/src/runtimeHelpers.ts: -------------------------------------------------------------------------------- 1 | export const TO_DISPLAY_STRING = Symbol('toDisplayString') 2 | export const OPEN_BLOCK = Symbol('openBlock') 3 | export const CREATE_ELEMENT_BLOCK = Symbol('createElementBlock') 4 | 5 | export const helperNameMap = { 6 | [TO_DISPLAY_STRING]: 'toDisplayString', 7 | [OPEN_BLOCK]: 'openBlock', 8 | [CREATE_ELEMENT_BLOCK]: 'createElementBlock' 9 | } 10 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transform.ts: -------------------------------------------------------------------------------- 1 | // 思路 遍历整个ast语法树 采取深度遍历优先 一条路走到黑 2 | 3 | // 采用插件的形式 我们在transform内部去修改node文本的值,其实并不合适,当我们去写其他测试的时候,又要来修改transform遍历的时候的逻辑,其实这里可以采取另一种思路 4 | // 比如说 采用插件的思想,做什么操作由外部通过options传递进来,然后在合适的地方进行调用,而我们这个demo合适的地方就是我们本来修改文本的值的位置 5 | import { NodeTypes } from './ast' 6 | import { TO_DISPLAY_STRING, OPEN_BLOCK, CREATE_ELEMENT_BLOCK } from './runtimeHelpers' 7 | 8 | export const transform = (ast, options: any = {}) => { 9 | const context = createTransformContext(ast, options) 10 | 11 | dfs(ast, context) 12 | 13 | createRootCodegen(ast) 14 | 15 | ast.helpers = [...context.helpers.keys()] 16 | } 17 | 18 | // 遍历整个ast语法树 19 | const dfs = (node, context) => { 20 | // 修改text文本的值 外面传入的修改方法 如何修改给外部决定如何执行 21 | const nodeTransform = context.nodeTransform 22 | const exitFns: any = [] 23 | nodeTransform.forEach((fn) => { 24 | const exitFn = fn(node, context) 25 | if (exitFn) exitFns.push(exitFn) 26 | }) 27 | 28 | // 插值语法 在context.helps(数组)上添加一项toDisplayString,用于后续生成js的时候引入,后续插值语法生成的js需要借助这些工具函数 29 | switch (node.type) { 30 | case NodeTypes.INTERPOLATION: 31 | context.push(TO_DISPLAY_STRING) 32 | break 33 | case NodeTypes.ROOT: 34 | dfsChildren(node, context) 35 | break 36 | case NodeTypes.ELEMENT: 37 | dfsChildren(node, context) 38 | break 39 | default: 40 | break 41 | } 42 | 43 | let len = exitFns.length 44 | console.log(len) 45 | 46 | while (len--) { 47 | exitFns[len]() 48 | } 49 | } 50 | 51 | function createRootCodegen(ast: any) { 52 | const child = ast.children[0] 53 | if (child.type === NodeTypes.ELEMENT) { 54 | ast.codegenNode = child.codegenNode 55 | } else { 56 | ast.codegenNode = ast.children[0] 57 | } 58 | } 59 | 60 | // 遍历ast语法树children 61 | function dfsChildren(node: any, context: any) { 62 | if (node.children) { 63 | node.children.forEach((childrenItem) => { 64 | dfs(childrenItem, context) 65 | }) 66 | } 67 | } 68 | 69 | // 创建transform全局上下文 70 | function createTransformContext(root, options: any) { 71 | return { 72 | nodeTransform: options.nodeTransform || [], 73 | root, 74 | helpers: new Map(), 75 | push(helperName) { 76 | this.helpers.set(helperName, 1) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/transformElement.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../ast' 2 | import { CREATE_ELEMENT_BLOCK } from '../runtimeHelpers' 3 | 4 | export function transformElement(ast, context) { 5 | if (ast.type === NodeTypes.ELEMENT) { 6 | return () => { 7 | context.push(CREATE_ELEMENT_BLOCK) 8 | 9 | // 中间层 10 | // tag 11 | const vnodeTag = `'${ast.tag}'` 12 | 13 | // prop 14 | let vnodeProp 15 | 16 | // children 17 | const children = ast.children 18 | const vnodeChildren = children[0] 19 | 20 | ast.codegenNode = { 21 | type: NodeTypes.ELEMENT, 22 | tag: vnodeTag, 23 | prop: vnodeProp, 24 | children: vnodeChildren 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/transformText.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../ast' 2 | 3 | function isTextorInterpolation(node) { 4 | return node.type === NodeTypes.TEXT || node.type === NodeTypes.INTERPOLATION 5 | } 6 | 7 | export function transformText(ast) { 8 | // 前提是element类型 9 | if (ast.type === NodeTypes.ELEMENT) { 10 | return () => { 11 | const { children } = ast 12 | let currentContainer 13 | for (let i = 0; i < children.length; i++) { 14 | // 如果是text或者插值类型 就将他们组合成一个新的类型 15 | if (isTextorInterpolation(children[i])) { 16 | for (let j = i + 1; j < children.length; j++) { 17 | if (isTextorInterpolation(children[j])) { 18 | if (!currentContainer) { 19 | currentContainer = children[i] = { 20 | type: NodeTypes.COMPOUND_EXPRESSION, 21 | children: [children[i]] 22 | } 23 | } 24 | 25 | currentContainer.children.push(' + ') 26 | currentContainer.children.push(children[j]) 27 | ast.children.splice(j, 1) 28 | j-- 29 | } else { 30 | currentContainer = undefined 31 | break 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/transformsExpression.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../ast' 2 | 3 | export function transformsExpression(ast) { 4 | if (NodeTypes.INTERPOLATION === ast.type) { 5 | ast.content.content = '_ctx.' + ast.content.content 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/compiler-core/test/__snapshots__/codegen.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`codegen > element 1`] = ` 4 | "const { toDisplayString: _toDisplayString, createElementBlock: _createElementBlock } = Vue 5 | return function render(_ctx,_cache){ return _createElementBlock('div',null,'hi,' + _toDisplayString(_ctx.message))}" 6 | `; 7 | 8 | exports[`codegen > happy path 1`] = `"return function render(_ctx,_cache){ return 'hi'}"`; 9 | 10 | exports[`codegen > interpolation 1`] = ` 11 | "const { toDisplayString: _toDisplayString } = Vue 12 | return function render(_ctx,_cache){ return _toDisplayString(_ctx.message)}" 13 | `; 14 | 15 | exports[`codegen element 1`] = ` 16 | "const { toDisplayString: _toDisplayString, createElementBlock: _createElementBlock } = Vue 17 | return function render(_ctx,_cache){ return _createElementBlock('div',null,'hi,' + _toDisplayString(_ctx.message))}" 18 | `; 19 | 20 | exports[`codegen happy path 1`] = `"return function render(_ctx,_cache){ return 'hi'}"`; 21 | 22 | exports[`codegen interpolation 1`] = ` 23 | "const { toDisplayString: _toDisplayString } = Vue 24 | return function render(_ctx,_cache){ return _toDisplayString(_ctx.message)}" 25 | `; 26 | -------------------------------------------------------------------------------- /packages/compiler-core/test/codegen.spec.ts: -------------------------------------------------------------------------------- 1 | import { transform } from '../src/transform' 2 | import { generate } from '../src/codegen' 3 | import { baseParse } from '../src/parse' 4 | import { transformsExpression } from '../src/transforms/transformsExpression' 5 | import { transformElement } from '../src/transforms/transformElement' 6 | import { transformText } from '../src/transforms/transformText' 7 | 8 | describe('codegen', () => { 9 | it('happy path', () => { 10 | const ast = baseParse('hi') 11 | transform(ast) 12 | const { code } = generate(ast) 13 | expect(code).toMatchSnapshot() 14 | }) 15 | it('interpolation', () => { 16 | const ast = baseParse('{{message}}') 17 | 18 | transform(ast, { 19 | nodeTransform: [transformsExpression] 20 | }) 21 | const { code } = generate(ast) 22 | expect(code).toMatchSnapshot() 23 | }) 24 | 25 | it('element', () => { 26 | const ast = baseParse('
hi,{{message}}
') 27 | transform(ast, { 28 | nodeTransform: [transformsExpression, transformElement, transformText] 29 | }) 30 | 31 | const { code } = generate(ast) 32 | expect(code).toMatchSnapshot() 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /packages/compiler-core/test/parse.spec.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../src/ast' 2 | import { baseParse } from '../src/parse' 3 | 4 | describe('Parse', () => { 5 | describe('interpolation', () => { 6 | it('simple interpolation', () => { 7 | const ast = baseParse('{{ message }}') 8 | expect(ast.children[0]).toStrictEqual({ 9 | type: NodeTypes.INTERPOLATION, 10 | content: { 11 | type: NodeTypes.SIMPLE_EXPRESSION, 12 | content: 'message' 13 | } 14 | }) 15 | }) 16 | }) 17 | 18 | describe('element', () => { 19 | it('simple element', () => { 20 | const ast = baseParse('
') 21 | expect(ast.children[0]).toStrictEqual({ 22 | type: NodeTypes.ELEMENT, 23 | props: [], 24 | tag: 'div', 25 | tagType: 0, 26 | children: [] 27 | }) 28 | }) 29 | }) 30 | 31 | describe('text', () => { 32 | it('simple text', () => { 33 | const ast = baseParse('mini vue') 34 | expect(ast.children[0]).toStrictEqual({ 35 | type: NodeTypes.TEXT, 36 | content: 'mini vue' 37 | }) 38 | }) 39 | }) 40 | 41 | test('hello word', () => { 42 | const ast = baseParse('
hi,{{message}}
') 43 | expect(ast.children[0]).toStrictEqual({ 44 | type: NodeTypes.ELEMENT, 45 | tag: 'div', 46 | props: [], 47 | tagType: 0, 48 | children: [ 49 | { 50 | type: NodeTypes.TEXT, 51 | content: 'hi,' 52 | }, 53 | { 54 | type: NodeTypes.INTERPOLATION, 55 | content: { 56 | type: NodeTypes.SIMPLE_EXPRESSION, 57 | content: 'message' 58 | } 59 | } 60 | ] 61 | }) 62 | }) 63 | 64 | test('Nestedn element', () => { 65 | const ast = baseParse('

hi

{{message}}
') 66 | expect(ast.children[0]).toStrictEqual({ 67 | type: NodeTypes.ELEMENT, 68 | tag: 'div', 69 | props: [], 70 | tagType: 0, 71 | children: [ 72 | { 73 | type: NodeTypes.ELEMENT, 74 | tag: 'p', 75 | props: [], 76 | tagType: 0, 77 | children: [ 78 | { 79 | type: NodeTypes.TEXT, 80 | content: 'hi' 81 | } 82 | ] 83 | }, 84 | { 85 | type: NodeTypes.INTERPOLATION, 86 | content: { 87 | type: NodeTypes.SIMPLE_EXPRESSION, 88 | content: 'message' 89 | } 90 | } 91 | ] 92 | }) 93 | }) 94 | 95 | test('should throw error when lack end tag', () => { 96 | // const ast = baseParse("
"); 97 | expect(() => { 98 | baseParse('
') 99 | }).toThrow('没有结束标签') 100 | }) 101 | 102 | test('compiler v-for', () => { 103 | const ast = baseParse("
{{item}}
") 104 | const o = ast 105 | // 期望最后解析的ast语法树对象 106 | let expectAst = { 107 | type: NodeTypes.ROOT, 108 | children: [ 109 | { 110 | type: NodeTypes.ELEMENT, 111 | tag: 'div', 112 | tagType: 0, 113 | props: [ 114 | { 115 | type: NodeTypes.DIRECTIVE, 116 | name: 'for' 117 | } 118 | ], 119 | children: [ 120 | { 121 | type: NodeTypes.INTERPOLATION, 122 | content: { 123 | type: NodeTypes.SIMPLE_EXPRESSION, 124 | content: 'item' 125 | } 126 | } 127 | ] 128 | } 129 | ] 130 | } 131 | 132 | expect(ast).toStrictEqual(expectAst) 133 | }) 134 | }) 135 | -------------------------------------------------------------------------------- /packages/compiler-core/test/transform.spec.ts: -------------------------------------------------------------------------------- 1 | import { baseParse } from '../src/parse' 2 | import { transform } from '../src/transform' 3 | 4 | describe('transform', () => { 5 | describe('happy path', () => { 6 | it('update text node', () => { 7 | const ast: any = baseParse('
hi,{{ message }}
') 8 | 9 | const plugins = (node) => { 10 | node.content = 'hi,mini-vue' 11 | } 12 | transform(ast, { 13 | nodeTransform: [plugins] 14 | }) 15 | 16 | const textNode = ast.children[0].children[0] 17 | expect(textNode.content).toBe('hi,mini-vue') 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/reactivity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coderwei-mini-vue3/reactivity", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "vitest" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@coderwei-mini-vue3/shared": "workspace:^1.0.0" 13 | } 14 | } -------------------------------------------------------------------------------- /packages/reactivity/src/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import { enableTracking, pauseTracking } from './effect' 2 | import { ReactiveFlags } from './reactive' 3 | 4 | /** 5 | * 6 | * @param target 源对象 7 | * @param handlers get/set等方法 8 | * @returns 9 | */ 10 | 11 | export const arrayInstrumentations = {} 12 | ;['includes', 'indexOf', 'lastIndexOf'].forEach((key: string) => { 13 | const originalIncludes = Array.prototype[key] 14 | arrayInstrumentations[key] = function (...args: any) { 15 | let res = originalIncludes.apply(this, args) 16 | if (!res || res === -1) { 17 | res = originalIncludes.apply(this![ReactiveFlags.IS_RAW], args) 18 | } 19 | return res 20 | } 21 | }) 22 | ;['push', 'pop', 'shift', 'unshift', 'splice'].forEach((key: string) => { 23 | const originalIncludes = Array.prototype[key] 24 | arrayInstrumentations[key] = function (...args: any) { 25 | pauseTracking() 26 | const res = originalIncludes.apply(this, args) 27 | enableTracking() 28 | return res 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /packages/reactivity/src/collectionHandlers.ts: -------------------------------------------------------------------------------- 1 | import { isMap, isObject } from '@coderwei-mini-vue3/shared' 2 | import { 3 | enableTracking, 4 | ITERATE_KEY, 5 | MapITERATE_KEY, 6 | pauseTracking, 7 | track, 8 | trigger, 9 | TriggerType 10 | } from './effect' 11 | import { reactive, ReactiveFlags } from './reactive' 12 | const wrap = (val) => (typeof val === 'object' ? reactive(val) : val) 13 | 14 | type IterableCollections = Map | Set 15 | 16 | const mutableInstrumentations = { 17 | add(key) { 18 | const target = this[ReactiveFlags.IS_RAW] 19 | // 因为Set不会出现重复的元素 所以如果开发者传入了相同的项 就没必要重新触发依赖了 20 | const hasKey = target.has(key) 21 | const res = target.add(key) 22 | if (!hasKey) { 23 | trigger(target, key, TriggerType.ADD) 24 | } 25 | return res 26 | }, 27 | delete(key) { 28 | const target = this[ReactiveFlags.IS_RAW] 29 | const hasKey = target.has(key) 30 | const res = target.delete(key) 31 | //只有存在这个key的时候再去触发依赖 开发者随便删除一个不存在的元素 是不需要触发依赖的 32 | if (hasKey) { 33 | trigger(target, key, TriggerType.DELETE) 34 | } 35 | return res 36 | }, 37 | // Map function 38 | get(key) { 39 | const target = this[ReactiveFlags.IS_RAW] 40 | const res = target.get(key) 41 | track(target, key) 42 | return isObject(res) ? reactive(res) : res 43 | }, 44 | set(key, value) { 45 | const target = this[ReactiveFlags.IS_RAW] 46 | const hasKey = target.has(key) 47 | const oldVal = target.get(key) 48 | const rawVal = value[ReactiveFlags.IS_RAW] || value 49 | target.set(key, rawVal) 50 | if (!hasKey) { 51 | // 不存在是新增操作 新增会影响size属性的 要触发size的依赖 52 | trigger(target, key, TriggerType.ADD, value) 53 | } else if (value !== oldVal || (value !== value && oldVal !== oldVal)) { 54 | // 如果key存在 说明是修改操作 55 | trigger(target, key, TriggerType.SET, value) 56 | } 57 | return this 58 | }, 59 | forEach(callback, thisArg) { 60 | const target = this[ReactiveFlags.IS_RAW] 61 | track(target, ITERATE_KEY) 62 | target.forEach((v, k) => { 63 | callback.call(thisArg, wrap(v), wrap(k), this) 64 | }) 65 | }, 66 | 67 | // 迭代器 68 | [Symbol.iterator]: createIterableMethod(Symbol.iterator), 69 | entries: createIterableMethod('entries'), 70 | values: createIterableMethod('values'), 71 | keys: createIterableMethod('keys') 72 | } 73 | 74 | function createIterableMethod(method: string | symbol) { 75 | return function (this: IterableCollections, ...arg) { 76 | const target = (this as any)[ReactiveFlags.IS_RAW] 77 | const targetIsMap = isMap(target) 78 | const isPair = method === 'entries' || (method === Symbol.iterator && targetIsMap) 79 | 80 | // 收集依赖 81 | track(target, targetIsMap ? MapITERATE_KEY : ITERATE_KEY) 82 | const innerIterator = target[method](...arg) 83 | return { 84 | next() { 85 | const { value, done } = innerIterator.next() 86 | return done 87 | ? { value, done } 88 | : { value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), done } 89 | }, 90 | [Symbol.iterator]() { 91 | return this 92 | } 93 | } 94 | } 95 | } 96 | 97 | function createInstrumentationGetter(isReadonly, isShallow) { 98 | return (target, key, receiver) => { 99 | if (key === ReactiveFlags.IS_READONLY) { 100 | return isReadonly 101 | } else if (key === ReactiveFlags.IS_REACTIVE) { 102 | return !isReadonly 103 | } else if (key === ReactiveFlags.IS_SHALLOW) { 104 | return isShallow 105 | } else if (key === ReactiveFlags.IS_RAW) { 106 | return target 107 | } 108 | 109 | // 如果是Set 访问size属性 110 | if (key === 'size') { 111 | track(target, ITERATE_KEY) 112 | return Reflect.get(target, 'size', target) 113 | } 114 | return mutableInstrumentations[key] 115 | } 116 | } 117 | 118 | // 集合类型的handles Set Map WeakMap WeakSet 119 | export const mutableCollectionHandlers = { 120 | get: createInstrumentationGetter(false, false) 121 | } 122 | -------------------------------------------------------------------------------- /packages/reactivity/src/computed.ts: -------------------------------------------------------------------------------- 1 | import { EffectDepend } from './effect' 2 | 3 | class ComputedRefImpl { 4 | private _value!: T 5 | public _getter: computedGetter 6 | private _dirty = true 7 | private _effect: EffectDepend 8 | constructor(get: computedGetter, private set: computedSetter) { 9 | this._getter = get 10 | this._effect = new EffectDepend(get, () => { 11 | if (!this._dirty) { 12 | this._dirty = true 13 | } 14 | }) 15 | } 16 | get value() { 17 | if (this._dirty) { 18 | this._dirty = false 19 | // this._value = this._getter(); 20 | this._value = this._effect.run() 21 | } 22 | return this._value 23 | } 24 | set value(newValue: T) { 25 | this.set(newValue) 26 | } 27 | } 28 | 29 | export type computedGetter = (...args: any[]) => T 30 | 31 | export type computedSetter = (v: T) => void 32 | 33 | export interface WritableComputedOptions { 34 | get: computedGetter 35 | set: computedSetter 36 | } 37 | export function computed(options: WritableComputedOptions): any 38 | export function computed(getter: computedGetter) 39 | 40 | export function computed(getterOption: computedGetter | WritableComputedOptions) { 41 | let getter: computedGetter 42 | let setter: computedSetter 43 | 44 | if (typeof getterOption === 'function') { 45 | getter = getterOption 46 | setter = () => console.error('getter是只读的,不允许赋值') 47 | } else { 48 | getter = getterOption.get 49 | setter = getterOption.set 50 | } 51 | return new ComputedRefImpl(getter, setter) 52 | } 53 | -------------------------------------------------------------------------------- /packages/reactivity/src/effect.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isMap, toRawType } from '@coderwei-mini-vue3/shared' 2 | 3 | let activeEffect 4 | // 在嵌套effect的情况下 5 | let effectStack: EffectDepend[] = [] 6 | let shouldTrack: boolean = false 7 | let trackStack: boolean[] = [] 8 | 9 | export const ITERATE_KEY = Symbol('iterate') 10 | export const MapITERATE_KEY = Symbol('map_iterate') 11 | 12 | export const enum TriggerType { 13 | ADD = 'add', 14 | SET = 'set', 15 | DELETE = 'delete' 16 | } 17 | 18 | export class EffectDepend { 19 | private _fn: Function 20 | public active = true //effect是否存活 21 | public deps: Set[] = [] //存储依赖的集合 方便后续stop方法删除对应的依赖 22 | onStop?: () => void //挂载用户传入的onStop回调函数,在删除之后调用一次 23 | constructor(fn: Function, public scheduler?) { 24 | this._fn = fn 25 | } 26 | run(): any { 27 | if (!this.active) { 28 | return this._fn() 29 | } 30 | let lastShouldTrack = shouldTrack 31 | cleanup(this) 32 | 33 | activeEffect = this 34 | effectStack.push(this) 35 | shouldTrack = true 36 | let returnValue = this._fn() 37 | shouldTrack = lastShouldTrack 38 | effectStack.pop() 39 | activeEffect = effectStack[effectStack.length - 1] 40 | return returnValue 41 | } 42 | 43 | stop() { 44 | //为了从性能考虑没必要每次都执行 因为同一个依赖 删除一次就够了 所以这里进行判断 只有当前依赖存活的时候 才将其依赖 移除的同事将其设置为false(失活) 45 | if (this.active) { 46 | cleanupEffect(this) 47 | this.onStop?.() 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * 54 | * @param effect 响应式实例 55 | * 删除依赖 56 | */ 57 | export function cleanupEffect(effect: EffectDepend) { 58 | // console.log(effect, 'effect') 59 | for (const dep of effect.deps) { 60 | dep.delete(effect) 61 | effect.active = false 62 | } 63 | } 64 | 65 | export interface IeffectOptionsTypes { 66 | scheduler?: () => any 67 | onStop?: () => void 68 | } 69 | 70 | /** 71 | * 72 | * @param fn 需要执行的函数 73 | */ 74 | export interface IeffectRunner { 75 | (): T 76 | effect: EffectDepend 77 | } 78 | 79 | export function effect(fn: () => T, options?: IeffectOptionsTypes) { 80 | const _effect = new EffectDepend(fn, options?.scheduler) 81 | _effect.onStop = options?.onStop 82 | _effect.run() 83 | const runner = _effect.run.bind(_effect) as IeffectRunner 84 | runner.effect = _effect 85 | return runner 86 | } 87 | 88 | export type Dep = Set 89 | // 抽离收集依赖 方便在ref函数中使用 90 | export function tarckEffect(dep: Dep) { 91 | // 如果set中已经有了对应的activeEffect依赖 那么就不需要再次进行收集依赖 92 | if (dep.has(activeEffect)) return 93 | dep.add(activeEffect) 94 | // console.log(dep, 'dep') 95 | activeEffect?.deps.push(dep) 96 | } 97 | 98 | // 抽离触发依赖 方便在ref函数中使用 99 | export function triggerEffect(dep: Dep) { 100 | // 这个位置为什么要重新拷贝一份dep? 101 | // 如果不进行拷贝 会导致死循环 因为我们在run方法内部调用的cleanup方法 会将当前的依赖进行删除 但是这个阶段我们还是添加这个依赖集合进来 [vue.js设计与实现]举了个通俗的例子 102 | /* 103 | const set = new Set([1]) 104 | set.forEach(item => { 105 | set.delete(1) 106 | set.add(1) 107 | console.log('遍历中') //会发现这个位置的打印会一直执行下去 108 | }) 109 | 110 | 我们用的for of 循环 同样会出现这个问题 111 | */ 112 | const effects: Dep = new Set(dep) 113 | for (const effect of effects) { 114 | if (effect.scheduler) { 115 | effect.scheduler() 116 | } else { 117 | effect.run() 118 | } 119 | } 120 | } 121 | 122 | const targetMap = new Map() 123 | 124 | export function isTracking() { 125 | return activeEffect !== undefined && shouldTrack 126 | } 127 | 128 | /** 129 | * 130 | * @param target 数据源对象 131 | * @param key 对应的key值 132 | */ 133 | export function track(target, key) { 134 | // 首先拦截不必要的依赖 135 | if (!isTracking()) return 136 | 137 | // target ---> key ---> dep 138 | // 根据target源对象 拿到一个由key:响应式函数组成的set组成的map, 然后根据这个key获取到对应的set 139 | /** 140 | * targetMap { 141 | * target:{key:Set()} 142 | * } 143 | * 也就是通过 targetMap.get(target); 获取到target对应的val 144 | * 然后由于他还是一个map,然后通过depsMap.get(key); 拿到对应的set 这样就可以获取到对应的依赖集合 145 | */ 146 | let depsMap = targetMap.get(target) 147 | // 考虑第一次没有拿到的情况---初始化 148 | if (!depsMap) { 149 | depsMap = new Map() 150 | targetMap.set(target, depsMap) 151 | } 152 | 153 | let dep = depsMap.get(key) 154 | // 考虑第一次没有拿到的情况---初始化 155 | if (!dep) { 156 | dep = new Set() 157 | depsMap.set(key, dep) 158 | } 159 | tarckEffect(dep) 160 | } 161 | 162 | /** 163 | * 164 | * @param target 用户访问的对象 165 | * @param key 需要触发对应的key 166 | */ 167 | export function trigger(target, key, type?: TriggerType, newVal?) { 168 | const depsMap = targetMap.get(target) 169 | const dep = depsMap?.get(key) //这里用可选运算符 因为没办法保证depsMap一定有对象 170 | const iterateDeps = depsMap?.get(ITERATE_KEY) 171 | 172 | const effectToRun = new Set() 173 | if (dep) { 174 | dep.forEach((effect) => { 175 | effectToRun.add(effect) 176 | }) 177 | } 178 | if ( 179 | iterateDeps && 180 | (type === TriggerType.ADD || 181 | type === TriggerType.DELETE || 182 | (toRawType(target) === 'Map' && type === TriggerType.SET)) 183 | ) { 184 | iterateDeps.forEach((effect) => { 185 | effectToRun.add(effect) 186 | }) 187 | } 188 | // 触发数组length的依赖 189 | if (type === TriggerType.ADD && isArray(target)) { 190 | const deps = depsMap.get('length') 191 | deps && 192 | deps.forEach((effect) => { 193 | effectToRun.add(effect) 194 | }) 195 | } 196 | if (key === 'length' && isArray(target)) { 197 | depsMap.forEach((effect, index) => { 198 | if (index >= newVal) { 199 | effect.forEach((_effect) => { 200 | effectToRun.add(_effect) 201 | }) 202 | } 203 | }) 204 | } 205 | 206 | // map类型 并且是keys方法 且只有当新增或者删除的时候才会触发 207 | if ((type === TriggerType.ADD || type === TriggerType.DELETE) && isMap(target)) { 208 | const deps = depsMap?.get(MapITERATE_KEY) 209 | deps && 210 | deps.forEach((_effect) => { 211 | effectToRun.add(_effect) 212 | }) 213 | } 214 | 215 | triggerEffect(effectToRun) 216 | } 217 | 218 | /** 219 | * 220 | * @param runner effect的返回值 221 | */ 222 | export function stop(runner) { 223 | runner.effect.stop() 224 | } 225 | 226 | // cleanup 移除无意义的依赖 227 | function cleanup(_effect) { 228 | _effect.deps.forEach((effect) => { 229 | effect.delete(_effect) 230 | }) 231 | _effect.deps.length = 0 232 | } 233 | 234 | // 暂停收集依赖 235 | export function pauseTracking() { 236 | trackStack.push(shouldTrack) 237 | shouldTrack = false 238 | } 239 | 240 | // 恢复收集依赖 (注意这里用的是恢复 不是直接设置成true 设置成true还是false要根据之前的状态来决定) 241 | export function enableTracking() { 242 | const res = trackStack.pop() 243 | shouldTrack = res === undefined ? true : res 244 | } 245 | -------------------------------------------------------------------------------- /packages/reactivity/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './effect' 2 | export * from './reactive' 3 | export * from './ref' 4 | export * from './computed' 5 | -------------------------------------------------------------------------------- /packages/reactivity/src/reactive.ts: -------------------------------------------------------------------------------- 1 | import { ITERATE_KEY, track, trigger, TriggerType } from './effect' 2 | 3 | import { arrayInstrumentations } from './baseHandlers' 4 | import { mutableCollectionHandlers } from './collectionHandlers' 5 | 6 | import { isArray, isObject, toRawType } from '@coderwei-mini-vue3/shared' 7 | import { isRef } from './ref' 8 | 9 | /** 10 | * 11 | * @param isReadonly 是否为只读对象 12 | * @param isShallow 是否为shallowReadonly对象 13 | * @returns 14 | */ 15 | export function createGetter(isReadonly = false, isShallow = false) { 16 | return function get(target: T, key: string | symbol, receiver: object) { 17 | const res = Reflect.get(target, key, receiver) 18 | if (key === ReactiveFlags.IS_READONLY) { 19 | return isReadonly 20 | } else if (key === ReactiveFlags.IS_REACTIVE) { 21 | return !isReadonly 22 | } else if (key === ReactiveFlags.IS_SHALLOW) { 23 | return isShallow 24 | } else if (key === ReactiveFlags.IS_RAW) { 25 | return target 26 | } 27 | 28 | // 只读对象不需要收集依赖 29 | if (!isReadonly) { 30 | track(target, key) 31 | } 32 | 33 | if (isArray(target) && arrayInstrumentations.hasOwnProperty(key)) { 34 | return Reflect.get(arrayInstrumentations, key, receiver) 35 | } 36 | 37 | // 如果是ref 要进行解包 38 | if (isRef(res)) { 39 | return res.value 40 | } 41 | 42 | // 判断是否为嵌套对象 如果是嵌套对象并且isShallow为默认值false 根据isReadonly判断递归调用readonly还是reactive 43 | if (isObject(res) && !isShallow) { 44 | return isReadonly ? readonly(res) : reactive(res) 45 | } 46 | return res 47 | } 48 | } 49 | 50 | export function createSetter() { 51 | return function set(target: T, key: string | symbol, val: any, receiver: object) { 52 | // 判断当前是新增属性还是修改属性 53 | const type = isArray(target) 54 | ? Number(key) < target.length 55 | ? TriggerType.SET 56 | : TriggerType.ADD 57 | : Object.prototype.hasOwnProperty.call(target, key) 58 | ? TriggerType.SET 59 | : TriggerType.ADD 60 | let oldValue = target[key] 61 | const res = Reflect.set(target, key, val, receiver) 62 | if (receiver[ReactiveFlags.IS_RAW] === target) { 63 | // 排除NaN的情况 64 | if (oldValue !== val && (oldValue === oldValue || val === val)) 65 | trigger(target, key, type, val) 66 | } 67 | return res 68 | } 69 | } 70 | 71 | export function createHas() { 72 | return function (target, key) { 73 | track(target, key) 74 | return Reflect.has(target, key) 75 | } 76 | } 77 | 78 | export function createOwnKeys() { 79 | return function (target) { 80 | track(target, isArray(target) ? 'length' : ITERATE_KEY) 81 | return Reflect.ownKeys(target) 82 | } 83 | } 84 | 85 | export function createDeleteProperty() { 86 | return function (target, key) { 87 | const res = Reflect.deleteProperty(target, key) 88 | trigger(target, key, TriggerType.DELETE) 89 | return res 90 | } 91 | } 92 | 93 | // 执行一次createGetter/createSetter函数,避免每次调用一次 94 | const get = createGetter() 95 | const set = createSetter() 96 | const has = createHas() 97 | const ownKeys = createOwnKeys() 98 | const deleteProperty = createDeleteProperty() 99 | const readonlyGet = createGetter(true) 100 | 101 | // reactive响应式对象的handle捕获器 102 | export const mutableHandlers: ProxyHandler = { 103 | get, 104 | set, 105 | has, 106 | ownKeys, 107 | deleteProperty 108 | } 109 | 110 | // readonly只读对象的handle捕获器 111 | export const readonlyHandlers: ProxyHandler = { 112 | get: readonlyGet, 113 | set(target, key, val) { 114 | console.warn(`${target} do not set ${String(key)} value ${val}, because it is readonly`) 115 | 116 | return true 117 | } 118 | } 119 | 120 | // 创建代理印射 如果同一个对象已经创建过了 不需要再次创建 121 | let proxyMap = new WeakMap() 122 | 123 | // reactive 对象 124 | export function reactive(raw: T) { 125 | // 如果是一个只读对象或者是一个响应式对象,直接返回 126 | const exitProxy = proxyMap.get(raw) 127 | if (exitProxy) return exitProxy 128 | const p = createReactiveObject(raw, mutableHandlers) 129 | proxyMap.set(raw, p) 130 | return p 131 | } 132 | 133 | //readonly对象 134 | export function readonly(raw: T) { 135 | return createReactiveObject(raw, readonlyHandlers) 136 | } 137 | 138 | // 统一管理isReadonly&isReactive状态 139 | export enum ReactiveFlags { 140 | IS_REACTIVE = '__v_isReactive', 141 | IS_READONLY = '__v_isReadonly', 142 | IS_SHALLOW = '__v_isShallow', 143 | IS_RAW = '__v_raw' 144 | } 145 | 146 | // 为value类型做批注,让value有属性可选,必选使用的时候提示value没有xxx属性 147 | export interface ITarget { 148 | [ReactiveFlags.IS_REACTIVE]?: boolean 149 | [ReactiveFlags.IS_READONLY]?: boolean 150 | [ReactiveFlags.IS_SHALLOW]?: boolean 151 | [ReactiveFlags.IS_RAW]?: unknown 152 | } 153 | 154 | // 判断是否是一个只读对象 155 | export function isReadonly(value: unknown) { 156 | return !!(value as ITarget)[ReactiveFlags.IS_READONLY] 157 | } 158 | 159 | // 判断是否是一个响应式对象 160 | export function isReactive(value: unknown) { 161 | return !!(value as ITarget)[ReactiveFlags.IS_REACTIVE] 162 | } 163 | 164 | // 判断是否是一个shallow对象 165 | export function isShallow(value: unknown) { 166 | return !!(value as ITarget)[ReactiveFlags.IS_SHALLOW] 167 | } 168 | 169 | // 检查对象是否是由 reactive 或 readonly 创建的 proxy。 170 | export function isProxy(value: unknown) { 171 | return isReactive(value) || isReadonly(value) 172 | } 173 | 174 | // toRaw方法 175 | export function toRaw(value) { 176 | const raw = value && (value as ITarget)[ReactiveFlags.IS_RAW] 177 | return raw ? raw : value 178 | } 179 | 180 | // 定义shallowReadonly的handlers 181 | export const shallowReadonlyHandlers: ProxyHandler = { 182 | get: createGetter(true, true), 183 | set(target, key, val) { 184 | console.warn(`${target} do not set ${String(key)} value ${val}, because it is readonly`) 185 | 186 | return true 187 | } 188 | } 189 | 190 | // shallowReadonly的实现 191 | export function shallowReadonly(value: T) { 192 | return createReactiveObject(value, shallowReadonlyHandlers) 193 | } 194 | 195 | // 定义shallowReactive的handlers 196 | export const shallowReactiveHandlers: ProxyHandler = { 197 | get: createGetter(false, true), 198 | set 199 | } 200 | 201 | // shallowReactive的实现 202 | export function shallowReactive(value: T) { 203 | return createReactiveObject(value, shallowReactiveHandlers) 204 | } 205 | 206 | export const enum TargetType { 207 | INVALID = 0, //无效 208 | COMMON = 1, // 普通对象 object / array 209 | COLLECTION = 2 // 集合对象 set / map / weakmap / weakset 210 | } 211 | 212 | export function targetTypeMap(value: string) { 213 | switch (value) { 214 | case 'Object': 215 | case 'Array': 216 | return TargetType.COMMON 217 | case 'Map': 218 | case 'Set': 219 | case 'WeakMap': 220 | case 'WeakSet': 221 | return TargetType.COLLECTION 222 | default: 223 | return TargetType.INVALID 224 | } 225 | } 226 | 227 | export function getTargetType(value: ITarget): TargetType { 228 | return Object.isExtensible(value) ? targetTypeMap(toRawType(value)) : TargetType.INVALID 229 | } 230 | 231 | function createReactiveObject(target: T, handlers) { 232 | // 区分 对象数组和集合对象 不同的对象需要使用不同的handlers 233 | const targetType = getTargetType(target) 234 | return new Proxy( 235 | target, 236 | targetType === TargetType.COLLECTION ? mutableCollectionHandlers : handlers 237 | ) 238 | } 239 | -------------------------------------------------------------------------------- /packages/reactivity/src/ref.ts: -------------------------------------------------------------------------------- 1 | import { effect, isTracking, tarckEffect, triggerEffect } from './effect' 2 | import { isReactive, reactive } from './reactive' 3 | import type { Dep } from './effect' 4 | import { hasChanged, isObject } from '@coderwei-mini-vue3/shared' 5 | 6 | export type Ref = RefImpl 7 | 8 | class RefImpl { 9 | private _value: T 10 | private _rawValue: T 11 | public dep: Dep 12 | public __v_isRef = true 13 | constructor(value: T) { 14 | this._value = convert(value) 15 | this._rawValue = value 16 | this.dep = new Set() 17 | } 18 | get value() { 19 | trackRefValue(this.dep) 20 | return this._value 21 | } 22 | set value(newValue) { 23 | if (hasChanged(this._rawValue, newValue)) return 24 | this._value = convert(newValue) 25 | this._rawValue = newValue //每次更新都需要重新设置一次_rawValue 因为constructor只会执行一次 在new的时候 26 | triggerEffect(this.dep) 27 | } 28 | } 29 | 30 | // 如果ref的新值是一个对象 那么需要进行递归处理 与reactive模块的嵌套对象处理类似 31 | export function convert(value) { 32 | return isObject(value) ? reactive(value) : value 33 | } 34 | 35 | export function trackRefValue(dep) { 36 | if (isTracking()) { 37 | tarckEffect(dep) 38 | } 39 | } 40 | 41 | export function ref(value: T) { 42 | return new RefImpl(value) 43 | } 44 | 45 | // isRef的实现 46 | export function isRef(ref) { 47 | return !!(ref && ref.__v_isRef) 48 | } 49 | 50 | // unref的实现 51 | export function unref(ref) { 52 | return isRef(ref) ? ref.value : ref 53 | } 54 | 55 | // proxyRefs的实现 56 | export function proxyRefs(value) { 57 | return isReactive(value) 58 | ? value 59 | : new Proxy(value, { 60 | get(target, key) { 61 | // console.log("执行了", target, key); 62 | 63 | return unref(Reflect.get(target, key)) 64 | }, 65 | set(target, key, value) { 66 | if (isRef(target[key]) && !isRef(value)) { 67 | target[key].value = value 68 | return true 69 | } 70 | 71 | return Reflect.set(target, key, value) 72 | } 73 | }) 74 | } 75 | 76 | class ObjectRefImpl { 77 | public __v_isRef = true 78 | constructor(public _object, public key, public defaultValue?) {} 79 | 80 | set value(newValue) { 81 | if (!hasChanged(newValue, this._object[this.key])) { 82 | this._object[this.key] = newValue 83 | } 84 | } 85 | 86 | get value() { 87 | const val = this._object[this.key] 88 | return val === undefined ? this.defaultValue : val 89 | } 90 | } 91 | 92 | export function toRef(target, key, defaultValue?) { 93 | return new ObjectRefImpl(target, key, defaultValue) 94 | } 95 | 96 | export function toRefs(target) { 97 | const ret: any = {} 98 | for (const key in target) { 99 | ret[key] = new ObjectRefImpl(target, key) 100 | } 101 | return ret 102 | } 103 | -------------------------------------------------------------------------------- /packages/reactivity/test/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { computed } from '../src/computed' 2 | import { reactive } from '../src/reactive' 3 | import { vi } from 'vitest' 4 | 5 | describe('computed', () => { 6 | it('should return updated value', () => { 7 | const value = reactive({ foo: 1 }) 8 | const cValue = computed(() => value.foo) 9 | expect(cValue.value).toBe(1) 10 | }) 11 | 12 | it('should compute lazily', () => { 13 | const value = reactive({ foo: 1 }) // 创建一个reactive对象 14 | const getter = vi.fn(() => value.foo) // 通过vi.fn()创建一个模拟函数,后续会检测被调用该函数次数 15 | const cValue = computed(getter) // 创建一个computed对象,并传入getter函数 16 | 17 | // lazy功能 18 | expect(getter).not.toHaveBeenCalled() // 因为还没触发cValue的get操作,所以getter是不会被调用的。 19 | 20 | expect(cValue.value).toBe(1) // cValue的get操作被触发,getter执行 21 | expect(getter).toHaveBeenCalledTimes(1) // getter被调用一次 22 | // 缓存功能 23 | // should not compute again 24 | cValue.value // cValue的get操作被触发,又因为value.foo并没有发生改变 25 | expect(getter).toHaveBeenCalledTimes(1) // 这里的getter还是被调用了一次 26 | 27 | // should not compute until needed 28 | value.foo = 2 // 这里的value.foo发生了改变,但是cValue的get操作还没被触发 29 | expect(getter).toHaveBeenCalledTimes(1) // 所以这里getter仍然只会被调用一次 30 | 31 | // now it should compute 32 | expect(cValue.value).toBe(2) // 这里的cValue的get操作被触发,getter执行 33 | expect(getter).toHaveBeenCalledTimes(2) // 这里getter被调用了两次 34 | // should not compute again 35 | cValue.value // cValue的get操作被触发,又因为value.foo并没有发生改变 36 | expect(getter).toHaveBeenCalledTimes(2) // 这里的getter还是被调用了两次 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/reactivity/test/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from '../src/reactive' 2 | import { effect, stop } from '../src/effect' 3 | import { vi } from 'vitest' 4 | import { isObject } from '@coderwei-mini-vue3/shared' 5 | 6 | describe('effect', () => { 7 | it('happy path', () => { 8 | const user = reactive({ 9 | age: 10 10 | }) 11 | let nextAge 12 | effect(() => { 13 | nextAge = user.age + 1 14 | }) 15 | expect(nextAge).toBe(11) 16 | user.age++ 17 | expect(nextAge).toBe(12) 18 | }) 19 | 20 | it('should return runner when effect call', () => { 21 | let foo = 1 22 | let runner = effect(() => { 23 | foo++ 24 | return 'foo' 25 | }) 26 | expect(foo).toBe(2) 27 | let resultValue = runner() 28 | expect(foo).toBe(3) 29 | expect(resultValue).toBe('foo') 30 | }) 31 | 32 | it('scheduler', () => { 33 | /** 34 | * 1. 用户可以在effect选择性传入一个options 配置对象 其中有一个scheduler,val是一个函数 35 | * 2. 当用户传入一个scheduler的时候 第一次继续执行effect的回调函数 36 | * 3. 之后触发的依赖是执行用户传入的scheduler函数 37 | * 4. 当用户执行effect返回的runner之后,触发依赖的时候正常执行effect的回调函数 38 | */ 39 | let dummy 40 | let run: any 41 | const scheduler = vi.fn(() => { 42 | run = runner 43 | }) 44 | const obj = reactive({ foo: 1 }) 45 | const runner = effect( 46 | () => { 47 | dummy = obj.foo 48 | }, 49 | { scheduler } 50 | ) 51 | expect(scheduler).not.toHaveBeenCalled() 52 | expect(dummy).toBe(1) 53 | // should be called on first trigger set操作的时候,也就是说在trigger被调用的时候 54 | obj.foo++ 55 | expect(scheduler).toHaveBeenCalledTimes(1) 56 | // should not run yet 57 | expect(dummy).toBe(1) 58 | // manually run 会触发effect的回调函数 59 | run() 60 | // should have run 61 | expect(dummy).toBe(2) 62 | }) 63 | 64 | it('stop', () => { 65 | let dummy 66 | const obj = reactive({ prop: 1 }) 67 | const runner = effect(() => { 68 | dummy = obj.prop 69 | }) 70 | obj.prop = 2 71 | expect(dummy).toBe(2) 72 | stop(runner) 73 | // 执行proxy的set操作,触发trigger() 74 | obj.prop = 3 75 | expect(dummy).toBe(2) 76 | // stopped effect should still be manually callable 77 | runner() 78 | expect(dummy).toBe(3) 79 | }) 80 | 81 | it('onStop', () => { 82 | const obj = reactive({ foo: 1 }) 83 | const onStop = vi.fn() 84 | let dummy 85 | const runner = effect( 86 | () => { 87 | dummy = obj.foo 88 | }, 89 | { 90 | onStop 91 | } 92 | ) 93 | stop(runner) 94 | // 被调用1次 95 | expect(onStop).toBeCalledTimes(1) 96 | }) 97 | 98 | it('skip unnecessary update', () => { 99 | // 在下面的例子,已经不需要在去触发依赖 100 | let obj = reactive({ 101 | ok: true, 102 | text: 'hello world' 103 | }) 104 | let fn = vi.fn(() => { 105 | dummy = obj.ok ? obj.text : 'not' 106 | }) 107 | let dummy 108 | effect(fn) 109 | 110 | expect(fn).toBeCalledTimes(1) //第一次默认执行 111 | expect(dummy).toBe('hello world') 112 | obj.ok = false 113 | expect(fn).toBeCalledTimes(2) // 当obj.ok发生变化 执行一次获取新值 114 | expect(dummy).toBe('not') 115 | 116 | // should not update 按道理来说 在obj.ok为false的情况下 无论obj.text如何发生变化 依赖都不应该会去重新执行 因为永远都不会去读取obj.text的值 所以不需要去触发依赖 117 | obj.text = 'hi' 118 | expect(fn).toBeCalledTimes(2) //这个时候 就不需要执行fn来获取最新的值了 因为没有意义 119 | }) 120 | 121 | // 嵌套的effect函数 122 | /* 123 | 为什么要考虑effect处于嵌套的情况? 124 | 这完全是由于vue的设计所造成的 可能这样说不太准确 换个角度说 是组件化就需要effect支持嵌套逻辑 125 | 举个例子 126 | 当我们的组件进行嵌套的情况下 vue在内部会进行组件的渲染 也就是说会执行组件的render函数 具体的表现形式类似入下 127 | // Child.vue 128 | const Child = { 129 | name:'Child', 130 | render(){ 131 | return h('div',{},'Child') 132 | } 133 | } 134 | // App.vue 135 | const App = { 136 | name:'App', 137 | render(){ 138 | return h('div',{},[h(Child)]) 139 | } 140 | } 141 | 142 | 他们在渲染的时候会被实际上是在一个effet函数内部执行的 类似于下面这种 143 | effect(()=>{ 144 | App.render() 145 | effect(()=>{ 146 | Child.render() 147 | }) 148 | )} 149 | 150 | 这也是为什么要考虑effect嵌套的情况 151 | */ 152 | it('nesting effect funtion', () => { 153 | let obj = reactive({ 154 | temp1: 1, 155 | temp2: 2 156 | }) 157 | 158 | let temp1, temp2 159 | effect(() => { 160 | effect(() => { 161 | temp2 = obj.temp2 162 | }) 163 | temp1 = obj.temp1 164 | }) 165 | 166 | expect(temp1).toBe(1) 167 | expect(temp2).toBe(2) 168 | obj.temp1 = 3 169 | expect(temp1).toBe(3) 170 | expect(temp2).toBe(2) 171 | }) 172 | 173 | it('reflect on proxy', () => { 174 | // 在谈论proxy的时候 都会说到reflect 总感觉这两个api是形影不离的 那是为何? 175 | /* 176 | 在这里不关心他们在别的场景下 是如何如何的 我们只关心在vue的响应式系统中 他们是如何配合的 177 | 先说总结: 其实就是在proxy的set操作中 通过reflect去触发依赖 至于为何 就是担心所谓的this指向的问题 可以看下面例子 我这里先做解释 178 | 1. 我们定义一个普通对象obj 但是同时还定义了一个访问器属性bar 179 | 2. 然后我们通过reactive去代理这个对象 180 | 3. 当我们在effect中去访问这个访问器属性的时候 会触发get操作 也就是说会收集依赖 181 | ok,我们先不谈后面的事情 先搞清楚一个问题 我们使用effect去访问这个访问器属性的时候 收集依赖 到底应该收集谁呢? bar? foo? 我们仔细看看代码 先下结论 按照逻辑来说 foo和bar都应该被收集 dummy依赖于bar bar依赖于foo 182 | 不就相当于dummy依赖于foo? foo发生变化的时候 dummy理论上也需要发生变化 搞清楚这个我们再看下面 183 | 按照以往的逻辑来说 是做不到这一点的 为什么? 说白了就是bar中的这个this问题 他的this指向的谁? 184 | function reactive(target){ 185 | return new Proxy(target,{ 186 | get(target,key,receiver){ 187 | track(target,key) 188 | return target[key] 问题就出现在这里 当我们访问bar的时候 他的this指向的是obj 而不是proxy 所以我们无法去触发依赖 189 | }, 190 | set(target,key,value,receiver){ 191 | const result = (target[key] = value) 192 | trigger(target,key) 193 | return result 194 | } 195 | }) 196 | } 197 | */ 198 | let obj = { 199 | foo: 1, 200 | get bar() { 201 | return this.foo 202 | }, 203 | set bar(value) { 204 | this.foo = value 205 | } 206 | } 207 | const user = reactive(obj) 208 | let dummy 209 | let fn = vi.fn(() => { 210 | dummy = user.bar 211 | }) 212 | effect(fn) 213 | expect(fn).toBeCalledTimes(1) 214 | expect(dummy).toBe(1) 215 | user.foo = 2 216 | expect(fn).toBeCalledTimes(2) 217 | expect(dummy).toBe(2) 218 | }) 219 | 220 | it('effect with in', () => { 221 | // in 操作符 不会触发get和set操作 所以不会触发依赖 我们需要在proxy中新增一个handler.has的操作 就可以正常触发依赖了 222 | let obj = reactive({ 223 | foo: 1 224 | }) 225 | let fn = vi.fn(() => { 226 | 'foo' in obj 227 | }) 228 | effect(fn) 229 | expect(fn).toBeCalledTimes(1) 230 | obj.foo = 2 231 | expect(fn).toBeCalledTimes(2) 232 | }) 233 | 234 | it('for in with reactive', () => { 235 | /* 236 | for in 也是不会触发get和set操作的 所以也需要在proxy中新增一个handler.ownKeys的操作 237 | 注意我说的是for in循环而已 在for in循环内部操作代理对象(修改属性或者添加属性)是会触发get和set操作的 238 | 如果我们想的片面一点 其实都不需要去拦截for in拦截器 因为我们在使用for in的时候 一般都是在遍历对象的属性 239 | 那么拦截for in 操作的意义在哪里? 240 | ~~~typescript 241 | let obj = reactive({ 242 | foo: 1 243 | }) 244 | effect(()=>{ 245 | for(let key in obj){ 246 | console.log(key) 247 | } 248 | }) 249 | obj.bar = 2 250 | ~~~ 251 | 看上面demo 我们给响应式对象新增了一个属性 但是for in循环并没有重新执行 为什么? 很简单 effect内部的for in循环并没有收集依赖 因为我们添加了属性,for in循环就变成了两次 我们需要for in循环重新执行 252 | 那么我们该如何让他收集依赖呢? 253 | 很简单:上面说了for in循环会触发ownKeys拦截器 我们在这里做事情就好了 254 | 255 | 但是上面demo还有一个性能问题 我们在新增属性 for in循环重新执行了 没问题 但是我如果修改呢? for in循环还是会重新执行 但是我们并不需要这样的效果 这就属于没意义的更新 我们需要for in循环重新执行 只是为了用户在for in循环 256 | 内部每次都能拿到正确数量的key罢了 修改的时候key的数量并不会发生变化 这个解决方案就更简单了 我们在set的时候判断一下 如果是新增属性 我们就不触发依赖 如果是修改属性 我们就触发依赖 257 | */ 258 | 259 | interface Obj { 260 | foo: number 261 | [key: string]: any 262 | } 263 | let obj: Obj = reactive({ 264 | foo: 1 265 | }) 266 | let res: string[] = [] 267 | let fn = vi.fn(() => { 268 | // 当调用for in 循环的时候 我们希望他也能够收集依赖 并且在响应式数据发生变化的时候 也能够触发依赖 269 | res = [] 270 | for (let key in obj) { 271 | res.push(key) 272 | console.log(key) 273 | } 274 | }) 275 | effect(fn) 276 | expect(fn).toBeCalledTimes(1) 277 | expect(res).toEqual(['foo']) 278 | obj.bar = 2 //注意 我们是新增了一个属性 for in循环需要重新执行 279 | expect(fn).toBeCalledTimes(2) 280 | expect(res).toEqual(['foo', 'bar']) 281 | }) 282 | 283 | it('for in with delete', () => { 284 | // 同上 delete操作也会影响for in循环 285 | interface Obj { 286 | foo?: number 287 | [key: string]: any 288 | } 289 | let obj: Obj = reactive({ 290 | foo: 1 291 | }) 292 | let res: string[] = [] 293 | let fn = vi.fn(() => { 294 | // 当调用for in 循环的时候 我们希望他也能够收集依赖 并且在响应式数据发生变化的时候 也能够触发依赖 295 | res = [] 296 | for (let key in obj) { 297 | res.push(key) 298 | console.log(key) 299 | } 300 | }) 301 | effect(fn) 302 | expect(fn).toBeCalledTimes(1) 303 | expect(res).toEqual(['foo']) 304 | delete obj.foo 305 | expect(fn).toBeCalledTimes(2) 306 | expect(res).toEqual([]) 307 | }) 308 | 309 | it('same value is not trigger effect', () => { 310 | let obj = reactive({ 311 | foo: 1, 312 | bar: NaN 313 | }) 314 | let dummy 315 | let fn = vi.fn(() => { 316 | dummy = obj.bar 317 | }) 318 | effect(fn) 319 | expect(fn).toBeCalledTimes(1) 320 | // obj.foo = 1 321 | obj.bar = 2 322 | expect(dummy).toBe(2) 323 | expect(fn).toBeCalledTimes(2) 324 | }) 325 | 326 | it('proto object trigger effect', () => { 327 | let proto = reactive({ 328 | foo: 1 329 | }) 330 | let obj = reactive(Object.create(proto)) 331 | let dummy 332 | let fn = vi.fn(() => { 333 | dummy = obj.foo 334 | }) 335 | effect(fn) 336 | expect(fn).toBeCalledTimes(1) 337 | obj.foo = 2 338 | expect(fn).toBeCalledTimes(2) 339 | }) 340 | 341 | // proxy Array 342 | it('modify array length,effect legnth is trigger', () => { 343 | // effect函数 依赖数组的legnth属性的时候 当我们修改数组长度 比如说新增一项 删除一项的时候 依赖也需要重新执行 344 | const arr = reactive([1]) 345 | let dummy 346 | let ret 347 | effect(() => { 348 | ret = arr[0] 349 | dummy = arr.length 350 | }) 351 | expect(dummy).toBe(1) 352 | arr[1] = 2 //修改数组的长度 依赖应该会重新执行 353 | expect(dummy).toBe(2) 354 | expect(ret).toBe(1) 355 | arr.length = 0 356 | expect(ret).toBe(undefined) 357 | }) 358 | 359 | it('set array length max effect length', () => { 360 | // 只有当重新设置数组的长度大于依赖的index的时候 依赖才会重新执行 比如 361 | /* 362 | let arr = reactive([1,2,3]) 363 | let dummy 364 | effect(() =>{ 365 | dummy = arr[0] 366 | }) 367 | arr.length = 1000 368 | 依赖需要重新执行吗? 这里明显不需要吧 所以我们很容易得出结论 当我们设置length属性的时候 只有这个length属性大于依赖的index的时候 依赖才会重新执行 369 | */ 370 | let arr = reactive([1, 2, 3]) 371 | let dummy 372 | let fn = vi.fn(() => { 373 | dummy = arr[0] 374 | }) 375 | effect(fn) 376 | expect(fn).toBeCalledTimes(1) 377 | arr.length = 1000 //依赖不应该重新执行 378 | expect(fn).toBeCalledTimes(1) 379 | }) 380 | 381 | it('for in with array', () => { 382 | let arr = reactive([1, 2, 3]) 383 | let dummy: string[] = [] 384 | effect(() => { 385 | dummy = [] 386 | for (const key in arr) { 387 | dummy.push(key) 388 | } 389 | }) 390 | 391 | expect(dummy).toEqual(['0', '1', '2']) 392 | arr[3] = 4 393 | expect(dummy).toEqual(['0', '1', '2', '3']) 394 | arr.length = 0 395 | expect(dummy).toEqual([]) 396 | }) 397 | 398 | // array functions 399 | it('array include function with Seven data types that are primitives', () => { 400 | let arr = reactive([1, 2, 3]) 401 | let dummy 402 | let fn = vi.fn(() => { 403 | dummy = arr.includes(4) 404 | }) 405 | effect(fn) 406 | expect(dummy).toBe(false) 407 | arr[3] = 4 408 | expect(dummy).toBe(true) 409 | }) 410 | 411 | it('array include function with Object', () => { 412 | /* 413 | 这里做个简单的解释: 我们知道我的proxy在get的时候 会判断值是不是一个对象 如果是一个对象的话 会对这个对象再次进行reactive操作 于是就有一个问题 414 | arr[0] !== arr[0] 为什么呢? 因为我们在每次访问下标为0的项都会触发get 于是每次都会返回一个新的proxy对象 问题就出现在下面这部分代码 415 | if (isObject(res) && !isShallow) { 416 | return isReadonly ? readonly(res) : reactive(res) 417 | } 418 | 419 | 解决方案: 420 | 我们在reactive函数中加入一个判断 判断当前对象是否被代理过 如果被代理过的话 就不需要再次代理了 反之则需要代理 然后将其存起来 421 | */ 422 | let obj = { foo: 1 } 423 | let arr = reactive([obj]) 424 | let ret = arr.includes(arr[0]) 425 | // 重写includes方法 因为我们arr数组中的项是一个proxy对象 而includes方法是根据值来判断的 所以我们需要重写这个方法 开发者拿着obj对象来判断是否在arr数组中 也是正常的 426 | let ret1 = arr.includes(obj) 427 | expect(ret).toBe(true) 428 | expect(ret1).toBe(true) 429 | }) 430 | 431 | it('array indexOf function and lastIndexOf', () => { 432 | let obj = {} 433 | let arr = reactive([1, 2, obj]) 434 | let i = arr.indexOf(obj) 435 | let j = arr.lastIndexOf(obj) 436 | let k = arr.indexOf(arr[0]) 437 | let l = arr.lastIndexOf(arr[2]) 438 | expect(i).toBe(2) 439 | expect(j).toBe(2) 440 | expect(k).toBe(0) 441 | expect(l).toBe(2) 442 | }) 443 | 444 | it('array push pop shift unshift splice function', () => { 445 | let arr = reactive([1, 2, 3]) 446 | let dummy 447 | let fn = vi.fn(() => { 448 | dummy = arr[0] 449 | }) 450 | effect(() => { 451 | arr.push(4) 452 | }) 453 | effect(() => { 454 | arr.push(5) 455 | }) 456 | expect(arr).toEqual([1, 2, 3, 4, 5]) 457 | effect(fn) 458 | arr.pop() 459 | expect(dummy).toBe(1) 460 | arr.shift() 461 | expect(dummy).toBe(2) 462 | arr.shift() 463 | expect(dummy).toBe(3) 464 | }) 465 | 466 | // Set 467 | it('Set reactive', () => { 468 | const set = reactive(new Set([1, 2, 3])) 469 | let dummy 470 | let fn = vi.fn(() => { 471 | dummy = set.size 472 | }) 473 | effect(fn) 474 | expect(dummy).toBe(3) 475 | set.add(4) 476 | expect(dummy).toBe(4) 477 | set.delete(1) 478 | expect(dummy).toBe(3) 479 | set.add(4) //添加相同的项 不会触发依赖 480 | expect(fn).toBeCalledTimes(3) 481 | set.delete(999) //删除一个不存在的项 不会触发依赖 482 | expect(fn).toBeCalledTimes(3) 483 | }) 484 | 485 | // Map 486 | it('Map reactive', () => { 487 | const map = reactive(new Map([['foo', 1]])) 488 | let dummy 489 | effect(() => { 490 | dummy = map.get('foo') 491 | }) 492 | expect(dummy).toBe(1) 493 | map.set('foo', 2) 494 | expect(dummy).toBe(2) 495 | 496 | const m = new Map() 497 | const map1 = reactive(m) 498 | const p2 = reactive(new Map()) 499 | map1.set('p2', p2) 500 | let dummy1 501 | let fn = vi.fn(() => { 502 | dummy1 = m.get('p2').size 503 | }) 504 | effect(fn) 505 | m.get('p2').set('k', 99) //通过原始数据 不应该触发依赖 [vuejs的设计与实现]中 p140 提到了这个问题(避免污染原始数据) 506 | expect(fn).toBeCalledTimes(1) 507 | }) 508 | 509 | it('Map with forEach', () => { 510 | const s = new Set([1, 2]) 511 | const m = new Map([['s', s]]) 512 | const map = reactive(m) 513 | let dummy 514 | let fn = vi.fn(() => { 515 | map.forEach((value, key) => { 516 | dummy = value.size 517 | }) 518 | }) 519 | effect(fn) 520 | expect(dummy).toBe(2) 521 | map.get('s').add(3) 522 | expect(dummy).toBe(3) 523 | }) 524 | 525 | it('Map with forEach set same key', () => { 526 | const map = reactive(new Map([['foo', 1]])) 527 | let dummy 528 | const fn = vi.fn(() => { 529 | map.forEach((value, key) => { 530 | dummy = value 531 | }) 532 | }) 533 | effect(fn) 534 | expect(fn).toBeCalledTimes(1) 535 | map.set('foo', 1) 536 | expect(fn).toBeCalledTimes(1) 537 | expect(dummy).toBe(1) 538 | }) 539 | 540 | it('Map or Set iterate', () => { 541 | // Map 542 | let obj = reactive({ 543 | foo: 1 544 | }) 545 | const map = reactive(new Map([['foo', obj]])) 546 | let arr: string[] = [] 547 | let dummy 548 | effect(() => { 549 | for (const [key, value] of map) { 550 | arr.push(key) 551 | dummy = value.foo 552 | } 553 | }) 554 | expect(arr).toEqual(['foo']) 555 | expect(dummy).toBe(1) 556 | map.get('foo').foo = 2 557 | expect(dummy).toBe(2) 558 | 559 | // Set 560 | let set = new Set([1, 2, 3, reactive({ foo: 1 })]) 561 | let setArr: number[] = [] 562 | let setDummy 563 | effect(() => { 564 | for (const item of set) { 565 | setArr.push(item) 566 | if (isObject(item)) { 567 | setDummy = item.foo 568 | } 569 | } 570 | }) 571 | expect(setArr).toEqual([1, 2, 3, { foo: 1 }]) 572 | expect(setDummy).toBe(1) 573 | set.forEach((item) => { 574 | if (isObject(item)) { 575 | item.foo = 2 576 | } 577 | }) 578 | expect(setDummy).toBe(2) 579 | }) 580 | 581 | it('Map or Set entries', () => { 582 | let obj = reactive({ 583 | foo: 1 584 | }) 585 | const map = reactive(new Map([['foo', obj]])) 586 | let arr: string[] = [] 587 | let dummy 588 | effect(() => { 589 | for (const [key, value] of map.entries()) { 590 | arr.push(key) 591 | dummy = value.foo 592 | } 593 | }) 594 | expect(arr).toEqual(['foo']) 595 | expect(dummy).toBe(1) 596 | map.get('foo').foo = 2 597 | expect(dummy).toBe(2) 598 | 599 | // Set entries是返回[value, value]的数组 因为Set没有key这个属性 600 | let set = new Set([1, 2, 3, reactive({ foo: 1 })]) 601 | let setArr: any[][] = [] 602 | let setDummy 603 | effect(() => { 604 | for (const item of set.entries()) { 605 | setArr.push(item) 606 | if (isObject(item)) { 607 | setDummy = item[0].foo 608 | } 609 | } 610 | }) 611 | expect(setArr).toEqual([ 612 | [1, 1], 613 | [2, 2], 614 | [3, 3], 615 | [{ foo: 1 }, { foo: 1 }] 616 | ]) 617 | expect(setDummy).toBe(1) 618 | set.forEach((item) => { 619 | if (isObject(item)) { 620 | item.foo = 2 621 | } 622 | }) 623 | expect(setDummy).toBe(2) 624 | }) 625 | 626 | // values 627 | it('Map or Set values', () => { 628 | let obj = reactive({ 629 | foo: 1 630 | }) 631 | const map = reactive(new Map([['foo', obj]])) 632 | let arr: string[] = [] 633 | let dummy 634 | effect(() => { 635 | for (const value of map.values()) { 636 | arr.push(value.foo) 637 | dummy = value.foo 638 | } 639 | }) 640 | expect(arr).toEqual([1]) 641 | expect(dummy).toBe(1) 642 | map.get('foo').foo = 2 643 | expect(dummy).toBe(2) 644 | 645 | // Set values是返回value的数组 646 | let set = new Set([1, 2, 3, reactive({ foo: 1 })]) 647 | let setArr: any[] = [] 648 | let setDummy 649 | effect(() => { 650 | for (const item of set.values()) { 651 | setArr.push(item) 652 | if (isObject(item)) { 653 | setDummy = item.foo 654 | } 655 | } 656 | }) 657 | expect(setArr).toEqual([1, 2, 3, { foo: 1 }]) 658 | expect(setDummy).toBe(1) 659 | set.forEach((item) => { 660 | if (isObject(item)) { 661 | item.foo = 2 662 | } 663 | }) 664 | expect(setDummy).toBe(2) 665 | }) 666 | 667 | // keys 668 | it('Map or Set keys', () => { 669 | let obj = reactive({ 670 | foo: 1 671 | }) 672 | const map = reactive(new Map([['foo', obj]])) 673 | let arr: string[] = [] 674 | let dummy 675 | effect(() => { 676 | for (const key of map.keys()) { 677 | arr.push(key) 678 | dummy = map.get(key).foo 679 | } 680 | }) 681 | expect(arr).toEqual(['foo']) 682 | expect(dummy).toBe(1) 683 | map.get('foo').foo = 2 684 | expect(dummy).toBe(2) 685 | 686 | // Set keys是返回value的数组 687 | let set = new Set([1, 2, 3, reactive({ foo: 1 })]) 688 | let setArr: any[] = [] 689 | let setDummy 690 | effect(() => { 691 | for (const item of set.keys()) { 692 | setArr.push(item) 693 | if (isObject(item)) { 694 | setDummy = item.foo 695 | } 696 | } 697 | }) 698 | expect(setArr).toEqual([1, 2, 3, { foo: 1 }]) 699 | expect(setDummy).toBe(1) 700 | set.forEach((item) => { 701 | if (isObject(item)) { 702 | item.foo = 2 703 | } 704 | }) 705 | expect(setDummy).toBe(2) 706 | }) 707 | 708 | // 性能优化 709 | it('call map keys should not to be call effect ', () => { 710 | // 当调用keys迭代器且设置的新属性是重复的key的时候 不应该触发依赖 keys拿到的全是key 对key的操作并不会造成副作用 711 | let notCallMap = reactive(new Map([['foo', 1]])) 712 | let fn = vi.fn(() => { 713 | for (const item of notCallMap.keys()) { 714 | console.log(item) 715 | } 716 | }) 717 | effect(fn) 718 | expect(fn).toBeCalledTimes(1) 719 | notCallMap.set('foo', 2) 720 | expect(fn).toBeCalledTimes(1) 721 | }) 722 | }) 723 | -------------------------------------------------------------------------------- /packages/reactivity/test/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive, toRaw } from '../src/reactive' 2 | 3 | describe('reactive', () => { 4 | it('happy path', () => { 5 | let original = { age: 1 } 6 | let observed = reactive(original) 7 | expect(original).not.toBe(observed) 8 | expect(observed.age).toBe(1) 9 | }) 10 | 11 | it('toRaw', () => { 12 | const original = { foo: 1 } 13 | const observed = reactive(original) 14 | // 输出的结果必须要等于原始值 15 | expect(toRaw(observed)).toBe(original) 16 | expect(toRaw(original)).toBe(original) 17 | }) 18 | it('nested reactive toRaw', () => { 19 | const original = { 20 | foo: { 21 | name: 'ghx' 22 | } 23 | } 24 | const observed = reactive(original) 25 | const raw = toRaw(observed) 26 | expect(raw).toBe(original) 27 | expect(raw.foo).toBe(original.foo) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/reactivity/test/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive, readonly, isReadonly, isReactive, isProxy } from '../src/reactive' 2 | 3 | describe('readonly', () => { 4 | it('readonly not set', () => { 5 | let original = { 6 | foo: { 7 | fuck: { 8 | name: 'what' 9 | } 10 | }, 11 | arr: [{ color: '#fff' }] 12 | } 13 | let warper = readonly(original) 14 | expect(warper).not.toBe(original) 15 | expect(isReadonly(warper)).toBe(true) 16 | expect(isReadonly(original)).toBe(false) 17 | // 测试嵌套对象的reactive状态 18 | expect(isReadonly(warper.foo.fuck)).toBe(true) 19 | // expect(isReadonly(warper.foo.fuck.name)).toBe(true) 20 | // 因为name是一个基本类型所以isObject会是false,暂时对name生成不了readonly,涉及到往后的知识点 isRef 21 | expect(isReadonly(warper.arr)).toBe(true) 22 | expect(isReadonly(warper.arr[0])).toBe(true) 23 | expect(isProxy(warper)).toBe(true) 24 | expect(warper.foo.fuck.name).toBe('what') 25 | }) 26 | 27 | it('warning when it be call set operation', () => { 28 | let original = { 29 | username: 'ghx' 30 | } 31 | let readonlyObj = readonly(original) 32 | const warn = vi.spyOn(console, 'warn') 33 | // 给readonly做set操作,将会得到一个warning 34 | // readonlyObj.username = "danaizi"; 35 | // expect(warn).toHaveBeenCalled(); 36 | }) 37 | 38 | it('nested reactive', () => { 39 | let original = { 40 | foo: { 41 | name: 'ghx' 42 | }, 43 | arr: [{ age: 23 }] 44 | } 45 | const nested = reactive(original) 46 | expect(isReactive(nested.foo)).toBe(true) 47 | expect(isReactive(nested.arr)).toBe(true) 48 | expect(isReactive(nested.arr[0])).toBe(true) 49 | expect(isReactive(nested.foo)).toBe(true) 50 | // expect(isReactive(nested.foo.name)).toBe(true) // 涉及到往后的知识点 isRef 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/reactivity/test/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '../src/effect' 2 | import { reactive } from '../src/reactive' 3 | import { ref, isRef, unref, proxyRefs, toRef, toRefs } from '../src/ref' 4 | 5 | describe('reactive', () => { 6 | it('should hold a value', () => { 7 | const a = ref(1) 8 | expect(a.value).toBe(1) 9 | a.value = 2 10 | expect(a.value).toBe(2) 11 | }) 12 | 13 | it('should be reactive', () => { 14 | const a = ref(1) 15 | let dummy 16 | let calls = 0 17 | effect(() => { 18 | calls++ 19 | dummy = a.value 20 | }) 21 | expect(calls).toBe(1) 22 | expect(dummy).toBe(1) 23 | a.value = 2 24 | expect(calls).toBe(2) 25 | expect(dummy).toBe(2) 26 | // same value should not trigger 27 | a.value = 2 28 | expect(calls).toBe(2) 29 | }) 30 | 31 | test('isRef', () => { 32 | expect(isRef(ref(1))).toBe(true) 33 | 34 | expect(isRef(0)).toBe(false) 35 | expect(isRef(1)).toBe(false) 36 | // an object that looks like a ref isn't necessarily a ref 37 | expect(isRef({ value: 0 })).toBe(false) 38 | expect(unref(1)).toBe(1) 39 | expect(unref(ref(1))).toBe(1) 40 | }) 41 | 42 | test('proxyRefs', () => { 43 | const user = { 44 | age: ref(10), 45 | name: 'ghx' 46 | } 47 | const original = { 48 | k: 'v' 49 | } 50 | const r1 = reactive(original) 51 | // 传入reactive对象 52 | const p1 = proxyRefs(r1) 53 | // objectWithRefs对象 (带ref 的object) 54 | const proxyUser = proxyRefs(user) 55 | 56 | expect(p1).toBe(r1) 57 | 58 | expect(user.age.value).toBe(10) 59 | expect(proxyUser.age).toBe(10) 60 | expect(proxyUser.name).toBe('ghx') 61 | 62 | proxyUser.age = 20 63 | expect(proxyUser.age).toBe(20) 64 | expect(user.age.value).toBe(20) 65 | 66 | proxyUser.age = ref(10) 67 | proxyUser.name = 'superman' 68 | expect(proxyUser.age).toBe(10) 69 | expect(proxyUser.name).toBe('superman') 70 | expect(user.age.value).toBe(10) 71 | }) 72 | 73 | it('toRef', () => { 74 | const obj = reactive({ 75 | name: 'coderwei', 76 | age: 18 77 | }) 78 | let dummy 79 | effect(() => { 80 | dummy = obj.age 81 | }) 82 | const age = toRef(obj, 'age') 83 | expect(age.value).toBe(18) 84 | expect(dummy).toBe(18) 85 | age.value = 19 86 | expect(age.value).toBe(19) 87 | expect(dummy).toBe(19) 88 | }) 89 | 90 | it('toRef default val', () => { 91 | const obj = reactive({ 92 | name: 'coderwei' 93 | }) 94 | 95 | const age = toRef(obj, 'age', 999) 96 | expect(age.value).toBe(999) 97 | }) 98 | 99 | it('toRefs', () => { 100 | const obj = reactive({ 101 | name: 'coderwei', 102 | age: 18 103 | }) 104 | 105 | let dummy 106 | effect(() => { 107 | dummy = obj.age 108 | }) 109 | const objToRefs = toRefs(obj) 110 | expect(objToRefs.age.value).toBe(18) 111 | expect(dummy).toBe(18) 112 | 113 | objToRefs.age.value++ 114 | expect(objToRefs.age.value).toBe(19) 115 | expect(obj.age).toBe(19) 116 | expect(dummy).toBe(19) 117 | }) 118 | 119 | it('reactive has key is ref', () => { 120 | const foo = ref(1) 121 | const obj = reactive({ 122 | foo 123 | }) 124 | const res = obj.foo 125 | 126 | expect(obj.foo).toBe(1) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /packages/reactivity/test/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, isReadonly, shallowReadonly, shallowReactive } from '../src/reactive' 2 | 3 | it('shallowReadonly basic test', () => { 4 | let original = { 5 | foo: { 6 | name: 'ghx' 7 | } 8 | } 9 | let obj = shallowReadonly(original) 10 | expect(isReadonly(obj)).toBe(true) 11 | // 因为只做表层的readonly,深层的数据还不是proxy 12 | expect(isReadonly(obj.foo)).toBe(false) 13 | expect(isReactive(obj)).toBe(false) 14 | 15 | const state = shallowReactive({ 16 | foo: 1, 17 | nested: { 18 | bar: 2 19 | } 20 | }) 21 | expect(isReactive(state)).toBe(true) 22 | expect(isReactive(state.nested)).toBe(false) //返回false 因为使用shallowReactive代理不到nested这一层 23 | }) 24 | -------------------------------------------------------------------------------- /packages/runtime-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coderwei-mini-vue3/runtime-core", 3 | "version": "1.0.0", 4 | "description": "@coderwei-mini-vue3/runtime-core", 5 | "scripts": { 6 | "test": "vitest" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@coderwei-mini-vue3/reactivity": "workspace:^1.0.0", 13 | "@coderwei-mini-vue3/shared": "workspace:^1.0.0" 14 | } 15 | } -------------------------------------------------------------------------------- /packages/runtime-core/src/apiDefineAsyncComponent.ts: -------------------------------------------------------------------------------- 1 | import { ref } from '@coderwei-mini-vue3/reactivity' 2 | import { isFunction } from '@coderwei-mini-vue3/shared' 3 | import { onUnmounted } from './apiLifecycle' 4 | import { h } from './h' 5 | import { createVNode } from './vnode' 6 | 7 | export type AsyncComponentLoader = () => Promise 8 | 9 | interface AsyncComponentOptions { 10 | loader: AsyncComponentLoader 11 | loadingComponent?: any 12 | errorComponent?: any 13 | delay?: number 14 | timeout?: number 15 | suspensible?: boolean 16 | onError?: (error: Error, retry: () => void, fail: () => void, attempts: number) => any 17 | } 18 | 19 | export function defineAsyncComponent(option: AsyncComponentOptions | AsyncComponentLoader) { 20 | // 处理兼容性 用户可能只是需要最为基础的异步组件的功能而已 不需要超时、错误组件等等 21 | if (isFunction(option)) { 22 | // 如果是一个函数 就说明用户直接传入了一个loader加载函数 23 | option = { 24 | loader: option as AsyncComponentLoader 25 | } 26 | } 27 | const { 28 | loader, 29 | loadingComponent, 30 | errorComponent, 31 | delay = 0, 32 | timeout, 33 | suspensible, 34 | onError: userOnError 35 | } = option as AsyncComponentOptions 36 | // debugger 37 | // 定义异步组件实例 38 | let instance 39 | 40 | // 超时次数 41 | let retries = 0 42 | let retry = () => { 43 | retries++ 44 | return load() 45 | } 46 | 47 | let isError = ref(false) 48 | // 错误的情况 49 | let error: any = ref('') 50 | // 是否是超时的状态 51 | let isTimout = ref(false) 52 | 53 | const load = () => { 54 | return loader() 55 | .then((i) => { 56 | instance = i 57 | console.log('----------') 58 | // 考虑到用户可能会使用onError参数 来发几次请求 他后面的请求如果成功了 我们是需要给他渲染成功状态的组件的 所以这里就把超时状态设置为false 59 | // 超时状态只有在第一次请求 超时 并且 用户没有再次发送请求 他才会true 也会导致load().then()方法内部的代码进不去 就是不会渲染成功的组件 60 | // 这种情况只有在 用户第一次发送异步组件请求 超时了, 然后我们展示了错误的组件 但是过一会 异步组件的请求成功了 我们也不渲染成功的组件 因为timeout超时了 就是失败了 严格按照逻辑来进行 错过了就是错过了 61 | // 比如说 超时时间给了3000 异步组件实际耗时3050 没用 当失败处理 62 | if (retries !== 0) isTimout.value = false 63 | return i 64 | }) 65 | .catch((err) => { 66 | err = err instanceof Error ? err : new Error(err) 67 | isError.value = true 68 | if (userOnError) { 69 | console.log('用户传入了错误处理函数,将错误抛给用户手动处理') 70 | error.value = err 71 | 72 | return new Promise((resolve, reject) => { 73 | const userRetry = () => resolve(retry()) 74 | const userFail = () => reject(err) 75 | userOnError(err, userRetry, userFail, retries + 1) 76 | }) 77 | } else { 78 | throw err 79 | } 80 | }) 81 | } 82 | 83 | return { 84 | name: 'AsyncComponentWrapper', 85 | setup() { 86 | // 标志的异步组件是否加载完成 87 | let loaded = ref(false) 88 | // 标志是否要展示加载中的组件 考虑到有的时候用户的网络特别快 一瞬间就加载出来了 那么我们就不需要给一个默认组件了 不然会有一个默认组件 ==> 异步组件的切换闪烁 89 | // 这个时间是由用户决定了 异步组件的耗时大于用户传入的时间 才渲染个默认组件到界面 90 | let isShowComponent = ref(false) 91 | let timer 92 | 93 | onUnmounted(() => { 94 | // 组件卸载的时候把定时器清除一下 95 | clearTimeout(timer) 96 | }) 97 | 98 | if (timeout != null) { 99 | setTimeout(() => { 100 | // 确保error的value没有值的情况下才去走超时的逻辑 因为获取异步组件已经错误了 在去计算是否超时就没有意义了 101 | if (!isError.value && !error.value && !loaded.value) { 102 | const err = new Error(`Async component timed out after ${timeout}ms.`) 103 | error.value = err 104 | isTimout.value = true 105 | console.error(err) 106 | clearTimeout(timer) 107 | } 108 | }, timeout) 109 | } 110 | 111 | load() 112 | .then((i) => { 113 | if (!isTimout.value) { 114 | console.log(i, '---') 115 | loaded.value = true 116 | } 117 | }) 118 | .catch((err) => { 119 | console.log(err, 'e') 120 | error.value = err 121 | }) 122 | 123 | timer = setTimeout(() => { 124 | // 开启一个定时器 用户传入的时间到了 就说明可以渲染默认组件了 125 | isShowComponent.value = true 126 | }, delay) 127 | 128 | /* 129 | 定义默认的占位符 用户可能不传入加载异步组件中使用的组件 那么我们下面返回出去的函数(也就是render函数)在执行的时候就拿到一个type为Comment的虚拟节点作为子树 去进行渲染 130 | 重点: 异步组件本质就是先渲染一个组件 等这个组件回来的时候我拿着这个组件的实例和一开始默认组件进行patch操作 本质是更新 并不是说等异步组件来了 直接进行挂载的操作 我们用的ref定义的布尔值 等异步组件 131 | 回来的时候 我们修改这个响应式数据 他就会触发视图的更新(或者说重新执行render函数)然后会返回拿到的异步组件出去进行渲染 132 | */ 133 | const defaultPlaceholder = h('div', {}, '') 134 | /* 135 | 这里为什么要返回一个函数? 目前我们通过defineAsyncComponent定义的组件 是没有render函数的 136 | component文件中的handleSetupResult处理函数是处理setup返回值的 那里我们就已经定义了 如果setup函数返回的是一个函数 那么就作为当前组件的render函数 137 | */ 138 | return () => { 139 | if (loaded.value) { 140 | // 如果loaded为true 表示异步组件已经加载下来了 141 | return createVNode(instance) 142 | } else if (loadingComponent && isShowComponent.value && !error.value) { 143 | return createVNode(loadingComponent) 144 | } else if (errorComponent && error.value) { 145 | // 获取异步组件的时候出现错误 如果用户指定了错误组件 我们就渲染它 146 | return createVNode(errorComponent, { 147 | error: error.value 148 | }) 149 | } else if (!loadingComponent && !error.value) { 150 | // 用户没有传递loadin组件 我们给他一个默认的 vue源码内部并没有提供 一个默认的loading组件 151 | return defaultPlaceholder 152 | } 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiDefineComponent.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '@coderwei-mini-vue3/shared' 2 | 3 | export function defineComponent(options: unknown) { 4 | return isFunction(options) ? { setup: options, name: (options as Function).name } : options 5 | } 6 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from '@coderwei-mini-vue3/shared' 2 | import { getCurrentInstance } from './component' 3 | 4 | // provide 函数的实现 5 | export function provide(key: string | number, value: T) { 6 | let currentInstance = getCurrentInstance() 7 | if (currentInstance) { 8 | let { provides } = currentInstance 9 | const parentProvides = currentInstance.parent?.provides 10 | // console.log("provides", provides); 11 | // console.log("parentProvides", parentProvides); 12 | 13 | if (provides === parentProvides) { 14 | // 把provide原型指向父组件的provide 15 | provides = currentInstance.provides = Object.create(parentProvides) 16 | } 17 | 18 | provides[key] = value 19 | } 20 | } 21 | 22 | // inject 函数的实现 23 | export function inject(key: string | any, defaultValue?: any) { 24 | const currentInstance = getCurrentInstance() 25 | if (currentInstance) { 26 | const parentProvide = currentInstance.parent.provides 27 | if (key in parentProvide) { 28 | return parentProvide[key] 29 | } else { 30 | if (isFunction(defaultValue)) { 31 | return defaultValue() 32 | } 33 | return defaultValue 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiLifecycle.ts: -------------------------------------------------------------------------------- 1 | import { currentInstance } from './component' 2 | 3 | export const enum LifeCycleHooks { 4 | BEFORE_CREATE = 'bc', //渲染前 5 | CREATED = 'c', //渲染后 6 | BEFORE_MOUNT = 'bm', //挂载前 7 | MOUNTED = 'm', //挂载后 8 | BEFORE_UPDATE = 'bu', //更新前 9 | UPDATED = 'u', //更新后 10 | BEFORE_UNMOUNT = 'bum', //卸载前 11 | UNMOUNTED = 'um', //卸载后 12 | DEACTIVATED = 'da', //keepAlive 特有的周期 失活状态 就是离开当前组件的瞬间会触发 但是被缓存的组件不会销毁 所以不会走前面的任何一个声明周期 13 | ACTIVATED = 'a' //keepAlive 特有的周期 活跃状态 离开当前组件又回来那个瞬间会触发 同上 14 | } 15 | 16 | // 注入生命周期 17 | export function injectHook(type, hook, target) { 18 | if (target) { 19 | const hooks = target[type] || (target[type] = []) 20 | hooks.push(hook) 21 | } else { 22 | console.error('生命周期钩子只能在setup函数里面使用') 23 | } 24 | } 25 | 26 | /** 27 | * 工厂函数: 我们需要清楚我们是如何使用这几个生命周期的 28 | * beforeUpdate(() =>{ 29 | * code.. 30 | * }) 31 | * 几个生命周期的使用方式是一样的,他是一个函数,接受一个回调函数,会在适当的情况下执行这个回调函数,所以我们就知道createHook这个工厂函数必须返回一个函数 32 | * 33 | * target是一个可选的,我们试想一下,我们在使用生命周期钩子的时候,并没有传递对应的target,我们可以试想,一个项目中会有多个组件,执行injectHook的时候,他是如何区分他该将这个生命周期注入到那个组件实例呢? 34 | * result: 我们需要非常清楚vue的执行逻辑 当代码会走到这里的时候 那就说明一定是在setup函数里面 我们并不允许在其他地方调用生命周期函数(vue3 compostion api的情况下) 所以我们回顾执行setup函数的时候 35 | * 是不是在执行之前将当前组件实例赋值给了一个currentInstance变量(./component的setupStateFulComponent函数) 走到这里的时候就说明在执行setup函数 那么这个时候currentInstance变量一定储存着当前组件实例 36 | * 我们就可以确定该将生命周期函数注入到哪个组件实例了 37 | * */ 38 | export function createHook(type) { 39 | return (hook, target = currentInstance) => { 40 | injectHook(type, hook, target) 41 | } 42 | } 43 | 44 | // 这里注意点: 用户实际上调用的是createHook返回的函数,而返回的函数我们使用闭包留存了type,也就是这里传递进去标志是哪个生命周期的变量 45 | export const onBeforeMount = createHook(LifeCycleHooks.BEFORE_MOUNT) 46 | export const onMounted = createHook(LifeCycleHooks.MOUNTED) 47 | export const onBeforeUpdate = createHook(LifeCycleHooks.BEFORE_UPDATE) 48 | export const onUpdated = createHook(LifeCycleHooks.UPDATED) 49 | export const onBeforeUnmount = createHook(LifeCycleHooks.BEFORE_UNMOUNT) 50 | export const onUnmounted = createHook(LifeCycleHooks.UNMOUNTED) 51 | export const onActivated = createHook(LifeCycleHooks.ACTIVATED) 52 | export const onDeactivated = createHook(LifeCycleHooks.DEACTIVATED) 53 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiWatch.ts: -------------------------------------------------------------------------------- 1 | import { EffectDepend, isReactive, isRef, Ref } from '@coderwei-mini-vue3/reactivity' 2 | import { extend, isArray, isFunction, isObject } from '@coderwei-mini-vue3/shared' 3 | import { queuePosstFlushCb, queuePreFlushCb } from './scheduler' 4 | 5 | export interface watchEffectOptions { 6 | flush?: 'pre' | 'post' | 'sync' 7 | onTrack?: (event) => void 8 | onTrigger?: (event) => void 9 | } 10 | 11 | type WatchSourceType = 12 | | Ref // ref 13 | | (() => T) // getter 14 | | T extends object 15 | ? T 16 | : never // 响应式对象 17 | 18 | export interface watchOptions extends watchEffectOptions { 19 | deep?: boolean 20 | immediate?: boolean 21 | } 22 | 23 | export type watchFnTypes = (onCleanup?) => void 24 | 25 | export function watchEffect(source: watchFnTypes, options: watchEffectOptions = {}) { 26 | return doWatch(source, null, options) 27 | } 28 | 29 | // watchPostEffect就是watchEffect的options传递了post 30 | export function watchPostEffect(source: watchFnTypes, options: watchEffectOptions = {}) { 31 | return doWatch(source, null, extend({}, options, { flush: 'post' })) 32 | } 33 | 34 | // watchSyncEffect就是watchEffect的options传递了sync 35 | export function watchSyncEffect(source: watchFnTypes, options: watchEffectOptions = {}) { 36 | return doWatch(source, null, extend({}, options, { flush: 'sync' })) 37 | } 38 | type vtype = (...arg) => void 39 | function doWatch(source: any, fn: vtype | null, options: watchOptions) { 40 | let oldVal, newVal 41 | 42 | const job = () => { 43 | if (fn) { 44 | newVal = effect.run() 45 | fn(newVal, oldVal, onCleanup) 46 | // 将新的value赋值给oldVal作为旧值 47 | oldVal = newVal 48 | } else { 49 | effect.run() 50 | } 51 | } 52 | 53 | let scheduler: (...arg) => void 54 | if (options.flush === 'post') { 55 | scheduler = scheduler = () => { 56 | queuePosstFlushCb(job) 57 | } 58 | } else if (options.flush === 'sync') { 59 | scheduler = job 60 | } else { 61 | // pre需要放在最后,因为用户不传和主动传递pre都是走这里 62 | scheduler = () => { 63 | queuePreFlushCb(job) 64 | } 65 | } 66 | let cleanup 67 | // 这个clearup函数就是用户调用的onCleanup,用户在调用这个函数的时候会传递一个函数,用于做用户属于自己的操作,他会在每次watchEffect执行的时候先执行一次(不包括第一次,第一次是默认执行的) 68 | const onCleanup = (cb) => { 69 | cleanup = () => { 70 | // console.log('Calls the function passed in by the user') 71 | cb() 72 | } 73 | } 74 | let getter 75 | 76 | getter = () => { 77 | if (cleanup) { 78 | cleanup() 79 | } 80 | // fn有值说明是watch调用的dowatch 81 | if (fn) { 82 | if (isRef(source)) { 83 | return source.value 84 | } else if (isReactive(source)) { 85 | const res = traverse(source) 86 | options.deep = true //当传递一个reactive的时候默认开启深度模式 87 | return res 88 | } else if (isFunction(source)) { 89 | return source() 90 | } 91 | } else { 92 | // 否则的话就是watchEffect调用的dowatch 93 | source(onCleanup) 94 | } 95 | } 96 | 97 | if (options.deep && fn) { 98 | const baseGetter = getter 99 | getter = () => traverse(baseGetter()) 100 | } 101 | 102 | const effect = new EffectDepend(getter, scheduler) 103 | 104 | //当用户没有传入fn的时候,代表用户使用的是watchEffect 执行一次用户传入的source watchEffect是会默认执行一次的 105 | // 当用户传入的时候,说明使用的是watch 它在immediate为false的时候是不需要执行一次的 106 | if (fn) { 107 | // 这里需要清楚,watch既然不执行,那他下次执行的时候就是依赖发生变化的时候,如果依赖发生变化,用户就需要拿到一个旧值,这个旧值(oldVal)不就是getter函数的返回值(这里需要考虑的情况有点多,我这里进行笼统的概括) 108 | // watch的第一个依赖集合(source)可以使多种类型的,比如说ref、reactive、function、甚至是一个Array,区分类型是在getter里面区分好了,我们在这里只需要确定: 我这里执行getter 就能拿到对应类型的返回值 109 | if (options.immediate) { 110 | job() 111 | } else { 112 | oldVal = effect.run() 113 | } 114 | } else { 115 | effect.run() 116 | } 117 | 118 | return () => { 119 | effect.stop() 120 | } 121 | } 122 | 123 | export function watch(source: WatchSourceType, fn, WatchSource: watchOptions = {}) { 124 | return doWatch(source, fn, WatchSource) 125 | } 126 | 127 | // 递归访问注册依赖 128 | export function traverse(value, seen?) { 129 | if (!isObject(value)) { 130 | return value 131 | } 132 | seen = seen || new Set() 133 | if (seen.has(value)) { 134 | return value 135 | } 136 | seen.add(value) 137 | if (isRef(value)) { 138 | traverse(value, seen) 139 | } else if (isArray(value)) { 140 | for (let i = 0; i < value.length; i++) { 141 | traverse(value[i], seen) 142 | } 143 | } else if (Object.prototype.toString.call(value)) { 144 | for (const key in value) { 145 | traverse(value[key], seen) 146 | } 147 | // TODO map set object 148 | } 149 | 150 | return value 151 | } 152 | -------------------------------------------------------------------------------- /packages/runtime-core/src/commonsetupState.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from '@coderwei-mini-vue3/shared' 2 | 3 | export type PublicPropertiesMap = Record any> 4 | 5 | const publicPropertiesMap: PublicPropertiesMap = { 6 | $slots: (instance) => instance.slots, 7 | $props: (instance) => instance.props 8 | } 9 | 10 | export const publicInstanceProxyHandlers: ProxyHandler = { 11 | get({ _: instance }, key: string) { 12 | const { setupState, props } = instance 13 | // console.log(instance) 14 | 15 | // console.log('key', key) 16 | // console.log('setupState', setupState) 17 | 18 | if (hasOwn(setupState, key)) { 19 | return setupState[key] 20 | } else if (hasOwn(props, key)) { 21 | // console.log("hasown", Object.prototype.hasOwnProperty.call(props, key)); 22 | // 用户访问的key是props的某个key 23 | return props[key] 24 | } 25 | 26 | const createGetter = publicPropertiesMap[key] 27 | // console.log(instance); 28 | if (createGetter) return createGetter(instance) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/runtime-core/src/component.ts: -------------------------------------------------------------------------------- 1 | import { emit } from './componentEmit' 2 | import { shallowReadonly, proxyRefs } from '@coderwei-mini-vue3/reactivity' 3 | import { publicInstanceProxyHandlers } from './commonsetupState' 4 | import { isObject, isFunction, isString } from '@coderwei-mini-vue3/shared' 5 | import { initProps } from './componentProps' 6 | import { initSlots } from './componentSlots' 7 | 8 | // 保存组件实例 便于getCurrentInstance 中返回出去 9 | export let currentInstance = null 10 | 11 | // 设置组件实例函数 12 | export function setCurrentInstance(instance: any) { 13 | currentInstance = instance 14 | } 15 | 16 | // 获取当期组件实例 17 | export function getCurrentInstance(): any { 18 | return currentInstance 19 | } 20 | 21 | // 创建组件实例 本质上就是个对象 22 | export function createComponentInstance(vnode: any, parentComponent: any) { 23 | const type = vnode.type 24 | const instance = { 25 | type, 26 | vnode, 27 | props: {}, 28 | emit: () => {}, 29 | slots: {}, 30 | provides: parentComponent ? parentComponent.provides : ({} as Record), //父组件提供的数据 31 | parent: parentComponent, //父组件实例 32 | isMouted: false, //标志组件是否挂载 后续用于判断是更新还是挂载 33 | subTree: {}, //子树的虚拟节点 34 | propsOptions: type.props || {}, //组件的props选项,在组件内部会通过defineProps来定义props,因为在使用组件的直接绑定属性不一定全都是props 35 | setupState: {}, 36 | next: null, 37 | // 生命周期 38 | bc: null, 39 | c: null, 40 | bm: null, 41 | m: null, 42 | bu: null, 43 | u: null, 44 | um: null, 45 | bum: null, 46 | da: null, 47 | a: null, 48 | ctx: {} 49 | } 50 | // console.log("vnode", instance); 51 | // console.log("emit", emit); 52 | // emit初始化 53 | instance.emit = emit.bind(null, instance) as any 54 | instance.ctx = { _: instance } 55 | // console.log(instance); 56 | 57 | return instance 58 | } 59 | 60 | // 安装组件函数 61 | export function setupComponent(instance: any) { 62 | // console.log("setupComponent instance", instance); 63 | 64 | // 初始化props 65 | initProps(instance, instance.vnode.props) 66 | 67 | // 初始化slots 68 | // console.log("初始化slots之前", instance.vnode.children); 69 | 70 | initSlots(instance, instance.vnode.children) 71 | // console.log(instance); 72 | 73 | // 初始化组件状态 74 | setupStateFulComponent(instance) 75 | } 76 | 77 | // 创建setup上下文 78 | export function createSetupContext(instance) { 79 | return { 80 | attrs: instance.attrs, 81 | slots: instance.slots, 82 | emit: instance.emit, 83 | expose: (exposed) => (instance.exposed = exposed || {}) 84 | } 85 | } 86 | 87 | // 初始化组件状态函数 88 | function setupStateFulComponent(instance: any) { 89 | // type是我们创建实例的时候自己手动加上的 -->createComponentInstance函数 90 | const Component = instance.type 91 | instance.proxy = new Proxy(instance.ctx, publicInstanceProxyHandlers) 92 | // console.log("instance", instance); 93 | 94 | const { setup } = Component 95 | 96 | // 考虑用户没有使用setup语法 97 | 98 | if (setup) { 99 | // console.log("instance emit", instance.emit); 100 | setCurrentInstance(instance) 101 | const instanceContext = createSetupContext(instance) 102 | const setupResult = setup(shallowReadonly(instance.props), instanceContext) 103 | 104 | // 这里考虑两种情况,一种是setup返回的是一个对象,那么可以将这个对象注入template上下文渲染,另一种是setup返回的是一个h函数,需要走render函数 105 | handleSetupResult(instance, setupResult) 106 | setCurrentInstance(null) 107 | } 108 | // 结束组件安装 109 | finishComponentSetup(instance) 110 | } 111 | 112 | function handleSetupResult(instance: any, setupResult: any) { 113 | if (isFunction(setupResult)) { 114 | // TODO setup返回值是h函数的情况 115 | if (instance.render) console.warn('setup返回一个函数,忽略render函数') 116 | instance.render = setupResult 117 | } else if (isObject(setupResult)) { 118 | instance.setupState = proxyRefs(setupResult) 119 | } 120 | } 121 | 122 | function finishComponentSetup(instance: any) { 123 | const component = instance.type 124 | // console.log('---------------------------------') 125 | if (!instance.render) { 126 | if (compiler && !component.rennder) { 127 | if (component.template) { 128 | component.render = compiler(component.template) 129 | } 130 | } 131 | instance.render = component.render 132 | } 133 | } 134 | 135 | let compiler 136 | export function createCompiler(_compiler) { 137 | compiler = _compiler 138 | } 139 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { toHandlerKey, camelCase } from '@coderwei-mini-vue3/shared' 2 | export function emit(instance: any, eventName: string, ...arg: unknown[]) { 3 | // console.log(eventName); 4 | 5 | // 先从实例中拿出props 6 | const { props } = instance 7 | 8 | const event = toHandlerKey(camelCase(eventName)) 9 | 10 | // console.log("event", event); 11 | 12 | const handle = props[event] 13 | // console.log(props, "props"); 14 | 15 | handle && handle(...arg) 16 | } 17 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function resolvePorp(propsOptions, rawprops) { 2 | const props = {} 3 | const attrs = {} 4 | /* 5 | propsOptions{ 6 | name:{ 7 | type:String, 8 | default:'xxx' 9 | }, 10 | age:{ 11 | type:Number, 12 | default:xxx 13 | } 14 | } 15 | 16 | rawProps:{ 17 | name:"coderwei", 18 | age:19, 19 | number: 95527666, 20 | cart: 'bar' 21 | } 22 | */ 23 | const keys = Object.keys(propsOptions) 24 | if (rawprops) { 25 | for (const key in rawprops) { 26 | if (keys.includes(key)) { 27 | props[key] = rawprops[key] 28 | } else { 29 | // 作为attrs 30 | attrs[key] = rawprops[key] 31 | } 32 | } 33 | } 34 | 35 | return { 36 | newprops: props, 37 | attrs 38 | } 39 | } 40 | 41 | // 初始化props 42 | export function initProps(instance: any, props: any) { 43 | // 这里我们需要对props和attrs进行区分 因为我们创建实例的时候,无论是props 还是attrs 都统一传给h函数的第二个参数 h函数的第二个参数会被统一挂载到props 44 | const { newprops, attrs } = resolvePorp(instance.propsOptions, props) 45 | instance.props = newprops || {} 46 | instance.attrs = attrs || {} 47 | } 48 | 49 | // 更新props 50 | export function patchComponentProps(props, newProps) { 51 | for (let key in newProps) { 52 | const prop = props[key] 53 | const newProp = newProps[key] 54 | //不同就更新 55 | if (prop !== newProp) { 56 | props[key] = newProps[key] 57 | } 58 | } 59 | // 删除旧的prop不存在与新的prop的属性 60 | /* 61 | newProps:{ 62 | name:"coderwei", 63 | } 64 | oldProps:{ 65 | name:"coder", 66 | age:19 67 | } 68 | // 这个age在新的component中就不需要了 所以这里可以直接选择删除 69 | */ 70 | for (let key in props) { 71 | if (!(key in newProps)) { 72 | delete props[key] 73 | } 74 | } 75 | } 76 | 77 | // 更新组件attrs 78 | export function patchComponentAttrs(attrs, newProps) { 79 | for (const key in attrs) { 80 | const attr = attrs[key] 81 | const newAttr = newProps[key] 82 | if (attr !== newAttr) { 83 | attrs[key] = newProps[key] 84 | } 85 | } 86 | 87 | // 删除没用的key 88 | for (const key in attrs) { 89 | if (!(key in newProps)) { 90 | delete attrs[key] 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentRenderUtils.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from '@coderwei-mini-vue3/shared' 2 | import { normalizeVNode } from './vnode' 3 | 4 | export function renderComponentRoot(instance) { 5 | let result 6 | if (instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 7 | // 有状态的组件 8 | result = normalizeVNode(instance.render.call(instance.proxy, instance.proxy)) 9 | } else { 10 | // 函数式组件 11 | } 12 | 13 | return result 14 | } 15 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from '@coderwei-mini-vue3/shared' 2 | import { createVNode, Fragment, normalizeVNode } from './vnode' 3 | import { isArray } from '@coderwei-mini-vue3/shared' 4 | // 如果children里面有slot,那么把slot挂载到instance上 5 | export function initSlots(instance: any, children: any) { 6 | const { vnode } = instance 7 | if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) { 8 | normalizeObjectSlots(instance.slots, children) 9 | } else { 10 | instance.slots = {} 11 | if (children) { 12 | normalizeVNodeSlots(instance, children) 13 | } 14 | } 15 | } 16 | 17 | function normalizeVNodeSlots(instance, children) { 18 | const normalize = normalizeSlotValue(children) 19 | instance.slots.default = () => normalize 20 | } 21 | 22 | // 具名name作为instance.slots的属性名,属性值是vnode 23 | function normalizeObjectSlots(slots: any, children: any) { 24 | // console.log("slots children===>", children); 25 | // 遍历对象 26 | for (let key in children) { 27 | const value = children[key] 28 | // console.log(value, "value"); 29 | 30 | slots[key] = (props) => normalizeSlotValue(value(props)) 31 | // slots[key] = normalizeSlotValue(value) 32 | } 33 | } 34 | // 转成数组 35 | function normalizeSlotValue(value: any) { 36 | return isArray(value) ? value : [normalizeVNode(value)] 37 | // return isArray(value) ? value : [value] 38 | } 39 | 40 | export function renderSlot(slots: any, name: string = 'default', props: any) { 41 | // console.log("开始执行renderslot"); 42 | 43 | const slot = slots[name] //插槽名字有默认值 如果用户什么都不传 遵循官网的用法 默认使用default 44 | // console.log("slot==>", slots, slot); 45 | if (slot) { 46 | // slot是一个函数的时候说明用户传入的是插槽 47 | if (typeof slot === 'function') { 48 | return createVNode(Fragment, {}, slot(props)) 49 | // return createVNode(Fragment, {}, slot); 50 | } 51 | } else { 52 | return slots 53 | } 54 | } 55 | 56 | // update slots 57 | export function updateSlots(instance, children) { 58 | const { vnode, slots } = instance 59 | if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) { 60 | normalizeObjectSlots(children, slots) 61 | } else if (children) { 62 | normalizeVNodeSlots(instance, children) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentUpdateUtils.ts: -------------------------------------------------------------------------------- 1 | export function shouldUpdateComponent(n1: any, n2: any) { 2 | const { props: preProps } = n1 3 | const { props: nextProps } = n2 4 | 5 | if (n1.children !== n2.children) { 6 | return true 7 | } 8 | 9 | for (const key in nextProps) { 10 | if (preProps[key] != nextProps[key]) { 11 | return true 12 | } 13 | } 14 | 15 | return false 16 | } 17 | -------------------------------------------------------------------------------- /packages/runtime-core/src/components/KeepAlive.ts: -------------------------------------------------------------------------------- 1 | import { invokeArrayFns, isArray, isString, ShapeFlags } from '@coderwei-mini-vue3/shared' 2 | import { getCurrentInstance } from '../component' 3 | import { h } from '../h' 4 | import { isSomeVNodeType, isVnode } from '../vnode' 5 | 6 | function matches(source, name) { 7 | // 三种情况 第一种是Array 第二种是字符串 第三种是正则表达式 8 | // 9 | // 10 | // 11 | 12 | // 13 | // 14 | // 15 | 16 | // 17 | // 18 | // 19 | if (isArray(source)) { 20 | return source.some((a) => a === name) 21 | } else if (source.test) { 22 | // 如果有test方法说明是一个正则 23 | return source.test(name) 24 | } else if (isString(source)) { 25 | return source.split(',').some((a) => a === name) 26 | } 27 | return false 28 | } 29 | 30 | const KeepAliveImpl = { 31 | name: 'KeepAlive', 32 | __isKeepAlive: true, 33 | props: { 34 | include: [RegExp, String, Array], 35 | exclude: [RegExp, String, Array], 36 | max: [String, Number] 37 | }, 38 | setup(props, { slots }) { 39 | // cache 是保存缓存的虚拟节点 40 | const cache = new Map() 41 | // keys用来保存key 当我们去cache中拿虚拟节点的时候需要提供一个key 这个keys就是保存的提供的key 至于为什么要单独保存这个key呢 42 | // 要注意keys是一个Set类型 他是有序的 我们可以通过keys.values().next().value去拿到第一次被插入到keys的元素 然后LRU算法的时候直接把这个key删掉即可 43 | const keys = new Set() 44 | let current 45 | const instance = getCurrentInstance() 46 | console.log(cache, instance) 47 | const sharedContext = instance.ctx 48 | 49 | const { 50 | renderer: { 51 | m: move, 52 | o: { createElement }, 53 | um: _unmount 54 | } 55 | } = instance.ctx 56 | 57 | const storageContainer = createElement('div') 58 | 59 | sharedContext.deactivate = function (vnode) { 60 | move(vnode, storageContainer) 61 | const { da } = vnode.component 62 | da && invokeArrayFns(da) 63 | } 64 | sharedContext.activate = function (vnode, container, anchor) { 65 | move(vnode, container, anchor) 66 | const { a } = vnode.component 67 | a && invokeArrayFns(a) 68 | } 69 | 70 | function unmount(vnode) { 71 | vnode = resetShapeFlag(vnode) 72 | 73 | _unmount(vnode) 74 | } 75 | 76 | return () => { 77 | if (!slots.default) { 78 | return null 79 | } 80 | 81 | const children = slots.default() 82 | const rawVNode = children[0] 83 | 84 | // 为什么要拷贝一份vNode出来呢? 这里简化了流程 在我们这种情况下即便是不拷贝一份出来也无所谓 源码用了getInnerChild方法确保返回的是虚拟节点 85 | // 当包裹在suspense组件内部时,具体的节点保存在vnode.ssContent 前提是外面是一个suspense组件,我们需要拿里面的ssContent 但是我们要返回的还是rawVNode,所以我们不能修改rawVNode 86 | // 不然的话 返回的时候就返回不了了 87 | // 我的判断: 如果你是拿 88 | /** 89 | * 例子 90 | * 直接修改rawVNode的值 91 | * if(vnode.shapeFlag & ShapeFlags.SUSPENSE){ 92 | * rawVNode = rawVNode.ssContent 93 | * }else{ 94 | * rawVnode = rawVNode 95 | * } 96 | * 后续我们想要返回最开始的rawVNode的时候 发现找不到了 被覆盖了 直接寄 所以考虑拷贝一份出来 **在正常情况下vNode和rawVNode保持一致,但是在suspense组件的情况下vNode保存的是rawVNode.ssContent** 97 | */ 98 | let vNode = rawVNode 99 | const comp = vNode.type 100 | const name = comp.name 101 | 102 | // KeepAlive组件是只能传入一个组件 103 | if (children.length > 1) { 104 | current = null 105 | return children 106 | } 107 | 108 | if (!isVnode(vNode) || !(vNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT)) { 109 | // 如果不是一个节点 或者不是一个组件 是无法被缓存的 110 | current = null 111 | return rawVNode 112 | } 113 | const { include, exclude, max } = props 114 | // 如果不匹配用户传入的include\匹配exclude 也直接返回 115 | // include 和exclude 是对等的,存在于include就不能存在于exclude中了, 举个例子: 学校假期调研,留校的填a表单,离校的填b表单,你两个表单都填,明显不符合实际情况 所以就衍生了下面的判断条件 116 | if ( 117 | (include && (!name || !matches(include, name))) || 118 | // 如果一个组件连name都没有 那么肯定是缓存不了的 因为找不到他 所以肯定不是缓存的他 119 | (exclude && name && matches(exclude, name)) 120 | // 排除一个组件 肯定需要name 如果这个组件连name都没有 肯定排除的不是他 因为找不到他 简而言之 一个组件没有name 排除不了且缓存不了 就是不需要缓存 121 | ) { 122 | current = null 123 | return rawVNode 124 | } 125 | 126 | // 将key保存到keys集合 127 | keys.add(vNode.type) 128 | const cacheVNode = cache.get(vNode.type) 129 | if (cacheVNode) { 130 | // 有值 说明被缓存过 131 | vNode.component = cacheVNode.component 132 | vNode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE 133 | // 让当前key变得活跃 也就是位于keys集合的后面 134 | keys.delete(vNode.type) 135 | keys.add(vNode.type) 136 | } else { 137 | // 缓存 在源码中 这个位置他并没有去缓存组件 他缓存组件的地方是setup函数内,就是说他在组件进来的一瞬间 直接缓存 不考虑其他的 这里他就只需要考虑是否需要进行LRU算法来移除最不活跃的组件 也就是最久没有访问的那个组件 138 | // https://leetcode.cn/problems/lru-cache/ 关于LRU算法可以见leetcode这道题 139 | 140 | keys.add(vNode.type) 141 | // 判断是否超过max 142 | if (max && keys.size > parseInt(max, 10)) { 143 | const deleteKey = keys.values().next().value 144 | const cached = cache.get(deleteKey) 145 | if (!isSomeVNodeType(cached, current)) { 146 | unmount(cached) 147 | } 148 | cache.delete(deleteKey) 149 | keys.delete(deleteKey) 150 | } 151 | cache.set(rawVNode.type, vNode) 152 | } 153 | vNode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE 154 | rawVNode.keepAliveInstance = instance 155 | current = vNode 156 | ;(window as any).cacheMap = cache 157 | return rawVNode 158 | } 159 | } 160 | } 161 | 162 | export const KeepAlive = KeepAliveImpl 163 | 164 | export function isKeepAlive(node) { 165 | return node && !!node.type.__isKeepAlive 166 | } 167 | 168 | export function resetShapeFlag(vnode) { 169 | vnode.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE 170 | vnode.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE 171 | return vnode 172 | } 173 | -------------------------------------------------------------------------------- /packages/runtime-core/src/components/Teleport.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from '@coderwei-mini-vue3/shared' 2 | 3 | export function isTeleport(vnode) { 4 | return vnode.__isTeleport 5 | } 6 | 7 | const TeleportImpl = { 8 | __isTeleport: true, 9 | process(n1, n2, container, parentComponent, anchor, internals) { 10 | const { 11 | props: { disabled }, 12 | shapeFlag, 13 | children 14 | } = n2 15 | const { 16 | mc: mountChildren, 17 | pc: patchChildren, 18 | m: move, 19 | o: { querySelect, insert, createText } 20 | } = internals 21 | if (!n1) { 22 | const startPlaceholder = (n2.el = createText('teleport start')) 23 | const endPlaceholder = (n2.anchor = createText('teleport end')) 24 | console.log(container) 25 | insert(startPlaceholder, container, anchor) 26 | insert(endPlaceholder, container, anchor) 27 | 28 | // 获取传送门的目标容器 29 | const target = resolveTarget(n2.props, querySelect) 30 | console.log(target) 31 | 32 | const targetAnchor = createText('') 33 | if (target) { 34 | insert(targetAnchor, target) 35 | } else { 36 | console.warn('请检查to属性,无效的目标元素') 37 | } 38 | const mount = (container, anchor) => { 39 | if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 40 | // 如果children是Array 就遍历插入 mountChildren会自动遍历的 前提是一定要保证children是一个数组 不是一个数组什么都不做 41 | mountChildren(children, container, parentComponent, anchor) 42 | console.log(children) 43 | } 44 | } 45 | if (disabled) { 46 | // 禁用 不移动 挂载在原位置 47 | mount(container, endPlaceholder) 48 | } else { 49 | mount(target, targetAnchor) 50 | } 51 | } else { 52 | // patch Teleport 53 | patchChildren(n1, n2, container, parentComponent) 54 | if (n1.props.to != n2.props.to) { 55 | const newTarget = querySelect(n2.props?.to) 56 | n2.children.forEach((node) => { 57 | move(node, newTarget) 58 | }) 59 | } 60 | } 61 | } 62 | } 63 | function resolveTarget(props: any, querySelect: any) { 64 | const targetSelect = props && props.to 65 | if (targetSelect) { 66 | if (querySelect) { 67 | return querySelect(targetSelect) 68 | return null 69 | } else { 70 | console.warn('select选择器不能为空') 71 | } 72 | } else { 73 | console.warn('to属性不能为空') 74 | return null 75 | } 76 | } 77 | 78 | export const Teleport = TeleportImpl 79 | -------------------------------------------------------------------------------- /packages/runtime-core/src/components/suspense.ts: -------------------------------------------------------------------------------- 1 | export function isSuspense(vnode) { 2 | return !!vnode.__isSuspense 3 | } 4 | 5 | const SuspenseImpl = { 6 | __isSuspense: true, 7 | process(n1, n2) { 8 | if (!n1) { 9 | } else { 10 | } 11 | } 12 | } 13 | 14 | export const Suspense = SuspenseImpl 15 | -------------------------------------------------------------------------------- /packages/runtime-core/src/createApp.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from './vnode' 2 | 3 | export function createAppAPI(render) { 4 | return function createApp(rootComponent: any) { 5 | // app就是我们app.js中导出的组件描述 可以说这个app会是最大的组件的描述 后续我们写的任何组件都在他的下层 6 | // console.log(app); 7 | const app = { 8 | _component: rootComponent, 9 | mount(rootContainer: any) { 10 | const vnode = createVNode(rootComponent) 11 | render(vnode, rootContainer) 12 | } 13 | } 14 | 15 | // 返回的是一个对象 所以我们才可以链式调用mount方法 16 | return app 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/runtime-core/src/h.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isObject, isString } from '@coderwei-mini-vue3/shared' 2 | import { createVNode, isVnode } from './vnode' 3 | // h函数有两种写法 4 | // 1. 两个参数 5 | // 写法1 h('div',{}) 6 | // 写法2 h('div',h('span')) 7 | // 写法3 h('div','he') 8 | // 写法4 h('div',['he']) 9 | 10 | // 3.三个参数 11 | // 写法1 h('div',{},'孩子') 12 | // 写法1 h('div',{},h()) 13 | // 写法2 h('div',{},['孩子','孩子','孩子']) 14 | 15 | // h函数的children 只有两种情况 要么把children处理成数组 要么是字符串 16 | 17 | export function h(type, props?, children?) { 18 | const len = arguments.length 19 | if (len === 2) { 20 | // TODO 兼容两个参数的写法 21 | if (isObject(props) && !isArray(children)) { 22 | // 如果props是一个对象并且不是一个数组 就说明用户传入了一个props 23 | return createVNode(type, props) 24 | } else { 25 | // 如果是数组、文本 26 | return createVNode(type, null, props) 27 | } 28 | } else { 29 | if (len > 3) { 30 | // 三个以上 31 | children = Array.prototype.slice.call(arguments, 2) 32 | } else if (len === 3 && isVnode(children)) { 33 | // 等于三个 并且children是节点 才放入数组中 如果不是节点可以直接渲染 在这里就要统一处理好 后续判断只要是节点 就直接去重复patch 就不管新里面有没有可能是文本类型了 34 | children = [children] 35 | } 36 | return createVNode(type, props, children) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/runtime-core/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 调用关系: 3 | * h --> createVnode --> 4 | * 5 | */ 6 | 7 | export * from './componentSlots' 8 | export * from './commonsetupState' 9 | export * from './component' 10 | export * from './componentEmit' 11 | export * from './componentProps' 12 | export * from './createApp' 13 | export * from './h' 14 | export * from './renderer' 15 | export * from './vnode' 16 | export * from './scheduler' 17 | export * from './apiLifecycle' 18 | export * from './apiInject' 19 | export { watchEffect, watch } from './apiWatch' 20 | export * from '@coderwei-mini-vue3/reactivity' 21 | export * from './components/KeepAlive' 22 | export * from './apiDefineAsyncComponent' 23 | export * from './apiDefineComponent' 24 | export * from './components/Teleport' 25 | 26 | export { toDisplayString } from '@coderwei-mini-vue3/shared' 27 | -------------------------------------------------------------------------------- /packages/runtime-core/src/scheduler.ts: -------------------------------------------------------------------------------- 1 | const queue: any[] = [] 2 | let activePreFlushCbs: any[] = [] 3 | let activePostFlushCbs: any[] = [] 4 | let showExecte = false 5 | let isFlushing = false 6 | 7 | export function nextTick(fn?: () => void) { 8 | return fn ? Promise.resolve().then(fn) : Promise.resolve() 9 | } 10 | 11 | export function queueJobs(job) { 12 | if (!queue.includes(job)) { 13 | // 如果queue这个队列里面没有job 那么才添加 14 | queue.push(job) 15 | } 16 | 17 | queueFlush() 18 | } 19 | 20 | function queueFlush() { 21 | if (showExecte) return 22 | showExecte = true 23 | 24 | nextTick(flushJobs) 25 | } 26 | 27 | function flushJobs() { 28 | showExecte = false 29 | isFlushing = false 30 | 31 | /** 增加个判断条件,因为目前为止,我们视图的异步渲染和watchEffect的异步执行 都是走到这个位置,而在这里watchEffect的第二个参数的flush是pre的时候,需要在视图更新之前执行 32 | 所以我们可以先在这里执行我们收集起来的需要在视图更新之前执行的函数 33 | */ 34 | // for (let i = 0; i < activePreFlushCbs.length; i++) { 35 | // activePreFlushCbs[i]() 36 | // } 37 | let preflush 38 | while ((preflush = activePreFlushCbs.shift())) { 39 | preflush && preflush() 40 | } 41 | 42 | // 下面是处理视图的更新的 vue有个核心概念: 视图的异步渲染 43 | let job 44 | // console.log('view is update') 45 | while ((job = queue.shift())) { 46 | job && job() 47 | } 48 | 49 | // 当watchEffect的options.flush为post的时候 需要在视图更新之后执行 50 | flushPostFlushCbs() 51 | } 52 | 53 | function flushPostFlushCbs() { 54 | let postflush 55 | while ((postflush = activePostFlushCbs.shift())) { 56 | postflush && postflush() 57 | } 58 | } 59 | 60 | export function queuePreFlushCb(fn) { 61 | queueFns(fn, activePreFlushCbs) 62 | } 63 | export function queuePosstFlushCb(fn) { 64 | queueFns(fn, activePostFlushCbs) 65 | } 66 | 67 | function queueFns(fn: any, activePreFlushCbs: any[]) { 68 | if (isFlushing) return 69 | isFlushing = true 70 | activePreFlushCbs.push(fn) 71 | queueFlush() 72 | } 73 | -------------------------------------------------------------------------------- /packages/runtime-core/src/vnode.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isBoolean, ShapeFlags } from '@coderwei-mini-vue3/shared' 2 | import { isObject, isString } from '@coderwei-mini-vue3/shared' 3 | import { isSuspense } from './components/suspense' 4 | import { isTeleport } from './components/Teleport' 5 | export const Fragment = Symbol('Fragment') 6 | export const Text = Symbol('Text') 7 | export const Comment = Symbol('Comment') 8 | 9 | export { createVNode as createElementBlock } 10 | export function createVNode(type, props?, children?) { 11 | const vnode = { 12 | type, 13 | props: props ?? null, 14 | children, 15 | component: null, 16 | key: props && props.key, 17 | shapeFlag: getShapeFlag(type), // 给vnode提供一个标识符 标志是什么类型的vnode 便于扩展 18 | el: null, 19 | __v_isVNode: true 20 | } 21 | // 根据vnode的children类型追加一个新的标识符 22 | normalizeChildren(vnode, children) 23 | 24 | return vnode 25 | } 26 | 27 | export function getShapeFlag(type: any) { 28 | return isString(type) 29 | ? ShapeFlags.ELEMENT 30 | : isTeleport(type) 31 | ? ShapeFlags.TELEPORT 32 | : isSuspense(type) 33 | ? ShapeFlags.SUSPENSE 34 | : isObject(type) 35 | ? ShapeFlags.STATEFUL_COMPONENT 36 | : 0 37 | } 38 | 39 | export function normalizeChildren(vnode: any, children: any) { 40 | // 先把null排除掉 因为isObject(null)返回true 会误进下面的isObject条件 41 | if (children == null) { 42 | children = null 43 | } else if (Array.isArray(children)) { 44 | // children是数组的情况下 45 | vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.ARRAY_CHILDREN 46 | } else if (isObject(children)) { 47 | // 子级是对象 48 | vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.SLOTS_CHILDREN 49 | } else { 50 | // children是字符串/数字的情况下 需要将数字转换为字符串 51 | children = String(children) 52 | vnode.children = children //重新挂载到vnode上面 53 | vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.TEXT_CHILDREN 54 | } 55 | } 56 | 57 | // 当用户传入文本的时候 需要创建一个虚拟节点 不然patch无法渲染的 58 | export function createTextVNode(text: string) { 59 | return createVNode(Text, {}, text) 60 | } 61 | 62 | // 判断是否是一个虚拟节点 63 | export function isVnode(value) { 64 | return value && !!value.__v_isVNode 65 | } 66 | 67 | // diff算法判断是否是同一个节点 68 | export function isSomeVNodeType(n1, n2) { 69 | return n1.type == n2.type && n1.key == n2.key 70 | } 71 | 72 | export function normalizeVNode(children) { 73 | if (children == null || isBoolean(children)) { 74 | // 如果为空 或者是是布尔值 当做注释节点 75 | return createVNode(Comment) 76 | } else if (isArray(children)) { 77 | return createVNode(Fragment, null, children.slice()) 78 | } else if (isObject(children)) { 79 | return children 80 | } 81 | return createVNode(Text, null, String(children)) 82 | } 83 | -------------------------------------------------------------------------------- /packages/runtime-core/test/watch.spec.ts: -------------------------------------------------------------------------------- 1 | import { watch, watchEffect, watchPostEffect, watchSyncEffect } from '../src/apiWatch' 2 | import { reactive, ref } from '@coderwei-mini-vue3/reactivity' 3 | import { nextTick } from '@coderwei-mini-vue3/runtime-core' 4 | import { count } from 'console' 5 | 6 | describe('apiWatch', () => { 7 | it('watchEffect', async () => { 8 | /** 9 | * let count = ref(1) 10 | * let age; 11 | * watchEffect(() =>{ 12 | * age = count.value+1 13 | * }) 14 | * 15 | * count.value => 1 && age => 2 16 | * count.value++ 17 | * count.value => 2 && age => 3 18 | */ 19 | const count = ref(1) 20 | let dummy 21 | watchEffect( 22 | () => { 23 | console.log('watch is call') 24 | dummy = count.value + 1 25 | } 26 | // { 27 | // flush: 'post' 28 | // } 29 | ) 30 | expect(dummy).toBe(2) 31 | count.value++ 32 | await nextTick() //watchEffect默认情况下是在组件渲染之前执行的。是由watchEffect的第二个参数控制的: type: { flush?: 'pre'(默认:组件渲染之前执行) | 'post'(组件渲染之后执行) | 'sync'(依赖触发后立即执行) } 33 | expect(dummy).toBe(3) 34 | }) 35 | 36 | it('watchEffect first parameter is function, it have a onCleanup function', async () => { 37 | // 利用vitest的vi工具模拟一个函数,断言他的调用次数 38 | let count = ref(1) 39 | let dummy 40 | const fn = vi.fn(() => { 41 | console.log('---') 42 | }) 43 | watchEffect((onCleanup) => { 44 | // 第一次onCleanup函数不会执行 45 | // 后续由于依赖发生变化,导致watchEffect执行的时候,就会先调用一次这个onCleanup函数 46 | dummy = count.value //简单触发一个count的get操作,收集下依赖 47 | console.log('watchEffect is call') 48 | onCleanup(fn) 49 | }) 50 | expect(fn).toHaveReturnedTimes(0) 51 | count.value++ 52 | await nextTick() //这里注意 因为我们触发依赖都是异步触发的(在微任务中触发的,他会在同步任务执行完之后才轮到他执行) 所以下面的代码会比触发依赖先执行,如果不等待视图渲染完成,下面断言函数执行一次的测试将永远不会成立 53 | expect(fn).toHaveBeenCalledTimes(1) 54 | }) 55 | 56 | it('watchEffect can return a function to stop monitor', async () => { 57 | let count = ref(1) 58 | let dummy 59 | const fn = vi.fn(() => { 60 | dummy = count.value 61 | }) 62 | const stop = watchEffect(fn) 63 | expect(fn).toBeCalledTimes(1) 64 | count.value++ 65 | await nextTick() 66 | expect(fn).toBeCalledTimes(2) 67 | 68 | stop() 69 | count.value++ 70 | await nextTick() 71 | expect(fn).toBeCalledTimes(2) 72 | count.value++ 73 | await nextTick() 74 | expect(fn).toBeCalledTimes(2) 75 | }) 76 | 77 | describe('watchEffect options', () => { 78 | it('options flush is post', () => { 79 | // 当flush的值为post的时候,回调函数会在组件更新之后执行 80 | let count = ref(1) 81 | watchEffect( 82 | () => { 83 | count.value 84 | console.log('watchEffect callback is run') 85 | }, 86 | { 87 | flush: 'post' 88 | } 89 | ) 90 | console.log('开始触发依赖') 91 | count.value++ 92 | }) 93 | it('watchPostEffect', () => { 94 | // 当flush的值为post的时候,回调函数会在组件更新之后执行 95 | let count = ref(1) 96 | watchPostEffect(() => { 97 | count.value 98 | console.log('watchEffect callback is run') 99 | }) 100 | console.log('开始触发依赖') 101 | count.value++ 102 | }) 103 | 104 | it('options flush is sync', () => { 105 | // 当flush的值为sync的时候,依赖发生变化后立刻执行回调函数 106 | let obj = reactive({ 107 | count: 1 108 | }) 109 | watchEffect( 110 | () => { 111 | obj.count 112 | console.log('watchEffect callback is call') 113 | }, 114 | { 115 | flush: 'sync' 116 | } 117 | ) 118 | obj.count++ 119 | }) 120 | it('watchSyncEffect', () => { 121 | let obj = reactive({ 122 | count: 1 123 | }) 124 | watchSyncEffect( 125 | () => { 126 | obj.count 127 | console.log('watchEffect callback is call') 128 | }, 129 | { 130 | flush: 'sync' 131 | } 132 | ) 133 | obj.count++ 134 | }) 135 | }) 136 | 137 | describe('watch', () => { 138 | it('watch happy path', async () => { 139 | let count = ref(1) 140 | watch(count, (value, oldVal) => { 141 | console.log('watch is run', value, oldVal) 142 | // 这里取巧了: 这里的count.value和count.value-1只是为了确定断言的内容,因为触发了两次依赖,每次断言的值是不同的 但是我们确定新值就是count.value的值 我们的操作时++ 所以旧值就是原有基础上-1 143 | expect(value).toBe(count.value) 144 | expect(oldVal).toBe(count.value - 1) 145 | count.value 146 | }) 147 | count.value++ 148 | await nextTick() 149 | count.value++ 150 | }) 151 | 152 | it('watch is called several times in a row', () => { 153 | let count = ref(1) 154 | watch(count, (value, oldVal) => { 155 | console.log('watch is run', value, oldVal) 156 | // 连续更改两次依赖,在任务队列中做了去重处理,只执行最后一次 拿到最终值(不处理也问题不大,不影响核心逻辑,这里只是为了更加契合官方watch的表现) 157 | expect(value).toBe(3) 158 | expect(oldVal).toBe(1) 159 | count.value 160 | }) 161 | count.value++ 162 | count.value++ 163 | }) 164 | 165 | it('watch onCleanup', async () => { 166 | let count = ref(1) 167 | let fn = vi.fn(() => { 168 | console.log('onCleanup is be call') 169 | }) 170 | watch(count, (value, oldVal, onCleanup) => { 171 | console.log('watch is be call') 172 | onCleanup(() => { 173 | console.log('onCleanup is be call') 174 | }) 175 | }) 176 | 177 | count.value++ 178 | await nextTick 179 | console.log('视图渲染完毕') 180 | count.value++ 181 | await nextTick 182 | console.log('视图渲染完毕') 183 | count.value++ 184 | }) 185 | 186 | describe('source type', () => { 187 | // watch第一个参数的监听源 会有多种类型的参数 不同的参数有着不同的处理方式 188 | it('ref type', () => { 189 | // 上面实现watch核心逻辑的时候就是以ref类型为基础的 所以是已经实现的 这里可以跳过 190 | }) 191 | 192 | it('reactive type', async () => { 193 | // 根据官网的说法 当给watch的第一个监听源传递的是一个reactive类型的时候 应该会自动开启深度监听 也就是deep:true 194 | let obj = reactive({ 195 | age: 18, 196 | foo: { 197 | count: 99 198 | } 199 | }) 200 | 201 | // 注意点: 这里的newVal和oldVal是同一个对象 下面是官网的解释 https://cn.vuejs.org/guide/essentials/watchers.html#deep-watchers 202 | /** 203 | * 当使用 getter 函数作为源时,回调只在此函数的返回值变化时才会触发。如果你想让回调在深层级变更时也能触发, 204 | * 你需要使用 { deep: true } 强制侦听器进入深层级模式。**在深层级模式时,如果回调函数由于深层级的变更而被触发,那么新值和旧值将是同一个对象** 205 | */ 206 | watch(obj, (newVal, oldVal) => { 207 | console.log('watch is be call', newVal.foo.count, oldVal.foo.count) 208 | expect(newVal).toBe(oldVal) 209 | }) 210 | obj.foo.count++ 211 | }) 212 | 213 | it('function type', () => { 214 | let count = ref(1) 215 | let obj = reactive({ 216 | age: 19 217 | }) 218 | watch( 219 | // () => count.value, 220 | () => obj.age, 221 | (newVal, oldVal) => { 222 | console.log('watch is be call', newVal, oldVal) 223 | } 224 | ) 225 | obj.age++ 226 | }) 227 | }) 228 | 229 | describe('watch options', () => { 230 | it('immediate', () => { 231 | // todo watch的immediate & deep 两个配置 232 | let count = ref(1) 233 | watch( 234 | count, 235 | (newVal, oldVal, onCleanup) => { 236 | console.log('watch is be call', newVal, oldVal) 237 | onCleanup(() => { 238 | console.log('oncleanup is be call') 239 | }) 240 | }, 241 | { immediate: true } 242 | ) 243 | count.value++ 244 | }) 245 | 246 | it('deep', () => { 247 | // let info = reactive({ 248 | // count: 1 249 | // }) 250 | let info = ref({ 251 | count: 1 252 | }) 253 | let dummy 254 | watch( 255 | info, 256 | (newVal) => { 257 | console.log('watch is be call') 258 | dummy = newVal 259 | }, 260 | { 261 | immediate: true 262 | // deep: true 263 | } 264 | ) 265 | info.value.count++ 266 | expect(dummy.count).toBe(2) 267 | }) 268 | 269 | it('flush', () => { 270 | // 当flush的值为post的时候,回调函数会在组件更新之后执行 271 | let count = ref(1) 272 | watch( 273 | count, 274 | () => { 275 | count.value 276 | console.log('watchEffect callback is run') 277 | }, 278 | { 279 | flush: 'post' 280 | } 281 | ) 282 | console.log('开始触发依赖') 283 | count.value++ 284 | }) 285 | }) 286 | }) 287 | }) 288 | -------------------------------------------------------------------------------- /packages/runtime-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coderwei-mini-vue3/runtime-dom", 3 | "version": "1.0.0", 4 | "description": "@coderwei-mini-vue3/runtime-dom", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@coderwei-mini-vue3/runtime-core": "workspace:^1.0.0", 13 | "@coderwei-mini-vue3/shared": "workspace:^1.0.0" 14 | } 15 | } -------------------------------------------------------------------------------- /packages/runtime-dom/src/index.ts: -------------------------------------------------------------------------------- 1 | // 定义关于浏览器的渲染器 2 | import { isOn } from '@coderwei-mini-vue3/shared' 3 | import { createRenderer } from '@coderwei-mini-vue3/runtime-core' 4 | 5 | import { patchEvent } from './modules/event' 6 | import { patchClass } from './modules/class' 7 | import { patchStyle } from './modules/style' 8 | 9 | function createElement(type) { 10 | // console.log('create el 操作', type) 11 | const element = document.createElement(type) 12 | return element 13 | } 14 | 15 | function createText(text) { 16 | return document.createTextNode(text) 17 | } 18 | 19 | function setText(node: HTMLElement, text) { 20 | // console.log('调用到这里了', node, text) 21 | 22 | node.nodeValue = text 23 | } 24 | 25 | function setElementText(el, text) { 26 | // console.log('SetElementText', el, text) 27 | el.textContent = text 28 | } 29 | 30 | function patchProp(el, key, preValue, nextValue) { 31 | // preValue 之前的值 32 | // 为了之后 update 做准备的值 33 | // nextValue 当前的值 34 | // console.log(`PatchProp 设置属性:${key} 值:${nextValue}`) 35 | // console.log(`key: ${key} 之前的值是:${preValue}`) 36 | 37 | if (isOn(key)) { 38 | patchEvent(el, key, nextValue) 39 | } else if (key === 'style') { 40 | patchStyle(el, nextValue) 41 | } else { 42 | patchClass(el, key, nextValue) 43 | } 44 | } 45 | 46 | function insert(child, parent, anchor = null) { 47 | // console.log('Insert操作') 48 | parent.insertBefore(child, anchor) 49 | } 50 | 51 | function remove(child) { 52 | const parent = child.parentNode 53 | if (parent) { 54 | parent.removeChild(child) 55 | } 56 | } 57 | 58 | function querySelect(note) { 59 | return document.querySelector(note) 60 | } 61 | 62 | let renderer 63 | 64 | function ensureRenderer() { 65 | // 如果 renderer 有值的话,那么以后都不会初始化了 66 | return ( 67 | renderer || 68 | (renderer = createRenderer({ 69 | createElement, 70 | createText, 71 | setText, 72 | setElementText, 73 | patchProp, 74 | insert, 75 | remove, 76 | querySelect 77 | })) 78 | ) 79 | } 80 | 81 | export const createApp = (...args) => { 82 | return ensureRenderer().createApp(...args) 83 | } 84 | 85 | export * from '@coderwei-mini-vue3/runtime-core' 86 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/modules/class.ts: -------------------------------------------------------------------------------- 1 | export function patchClass(el, key, nextValue) { 2 | if (nextValue === null || nextValue === undefined) { 3 | el.removeAttribute(key) 4 | } else { 5 | el.setAttribute(key, nextValue) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/modules/event.ts: -------------------------------------------------------------------------------- 1 | // 添加事件处理函数的时候需要注意一下 2 | // 1. 添加的和删除的必须是一个函数,不然的话 删除不掉 3 | // 那么就需要把之前 add 的函数给存起来,后面删除的时候需要用到 4 | // 2. nextValue 有可能是匿名函数,当对比发现不一样的时候也可以通过缓存的机制来避免注册多次 5 | // 存储所有的事件函数 6 | export function patchEvent(el: any, key: any, nextValue: any) { 7 | const invokers = el._vei || (el._vei = {}) 8 | const existingInvoker = invokers[key] 9 | if (nextValue && existingInvoker) { 10 | // patch 11 | // 直接修改函数的值即可 12 | existingInvoker.value = nextValue 13 | } else { 14 | const eventName = key.slice(2).toLowerCase() 15 | if (nextValue) { 16 | const invoker = (invokers[key] = nextValue) 17 | el.addEventListener(eventName, invoker) 18 | } else { 19 | el.removeEventListener(eventName, existingInvoker) 20 | invokers[key] = undefined 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/modules/style.ts: -------------------------------------------------------------------------------- 1 | // 处理浏览器端 元素style样式 2 | export function patchStyle(el, value) { 3 | console.log(value) 4 | const { style } = el 5 | for (const key in value) { 6 | style[key] = value[key] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/patchProps.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderwei99/coderwei-mini-vue3/fc516bd02f91debb8015713e76cca979eb2083d9/packages/runtime-dom/src/patchProps.ts -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@coderwei-mini-vue3/shared", 3 | "version": "1.0.0", 4 | "description": "@coderwei-mini-vue3/shared", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC" 11 | } -------------------------------------------------------------------------------- /packages/shared/src/ShapeFlags.ts: -------------------------------------------------------------------------------- 1 | // vnode 类型的标识符 2 | export const enum ShapeFlags { 3 | ELEMENT = 1, // 0000000001 字符串类型 4 | FUNCTIONAL_COMPONENT = 1 << 1, // 0000000010 函数组件类型 5 | STATEFUL_COMPONENT = 1 << 2, // 0000000100 普通的有状态的组件 6 | TEXT_CHILDREN = 1 << 3, // 0000001000 子节点为纯文本 7 | ARRAY_CHILDREN = 1 << 4, // 0000010000 子节点是数组 8 | SLOTS_CHILDREN = 1 << 5, // 0000100000 子节点是插槽 9 | TELEPORT = 1 << 6, // 0001000000 Teleport 10 | SUSPENSE = 1 << 7, // 0010000000 Supspense 11 | COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 0100000000 需要被keep-live的有状态组件 12 | COMPONENT_KEPT_ALIVE = 1 << 9, // 1000000000 已经被keep-live的有状态组件 13 | COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 有状态组件和函数组件都是组件,用component表示 14 | } 15 | -------------------------------------------------------------------------------- /packages/shared/src/general.ts: -------------------------------------------------------------------------------- 1 | // 判断是否为一个对象 2 | export function isObject(value: unknown) { 3 | return value !== null && typeof value === 'object' 4 | } 5 | 6 | // 判断是否是一个函数 7 | export function isFunction(value: unknown): boolean { 8 | return typeof value == 'function' 9 | } 10 | 11 | // 判断是否是一个字符串 12 | export function isString(value: unknown) { 13 | return typeof value == 'string' 14 | } 15 | 16 | // 判断是否是一个布尔值 17 | export function isBoolean(value: unknown) { 18 | return typeof value == 'boolean' 19 | } 20 | 21 | // 判断是否是相同的值 如果ref是相同的值 就不需要触发依赖 22 | export function hasChanged(value, oldValue) { 23 | return Object.is(value, oldValue) 24 | } 25 | 26 | // 判断是否为数组 27 | export const isArray = Array.isArray 28 | 29 | // 判断是否是map 30 | export const isMap = (val: unknown) => toTypeString(val) === '[object Map]' 31 | 32 | // 判断是否是set 33 | export const isSet = (val: unknown) => toTypeString(val) === '[object Set]' 34 | 35 | // 判断某个key是否在指定对象上 36 | export const hasOwn = (target: any, key: any) => Object.prototype.hasOwnProperty.call(target, key) 37 | 38 | // 去除用户输入的事件名中间的- 比如说: 'my-btn-click' ===> myBtnClick 39 | export const camelCase = (str: string) => { 40 | // console.log("str", str); 41 | 42 | return str.replace(/-(\w)/g, (_, $1: string) => { 43 | return $1.toUpperCase() 44 | }) 45 | } 46 | 47 | // 首字母大写处理 48 | export const capitalize = (str: string) => { 49 | return str.charAt(0).toLocaleUpperCase() + str.slice(1) 50 | } 51 | 52 | // 事件名前面+on 并且确保on后的第一个字母为大写 53 | export const toHandlerKey = (eventName: string) => { 54 | return eventName ? 'on' + capitalize(eventName) : '' 55 | } 56 | 57 | //判断字符串是否以on开头并且第三个字符为大写 58 | // example: onClick ==> true、 onclick ==> false 59 | export const isOn = (key: string) => /^on[A-Z]/.test(key) 60 | 61 | // 空对象 62 | export const EMPTY_OBJECT = {} 63 | 64 | // 对象上面的拷贝方法 65 | export const extend = Object.assign 66 | 67 | // 循环执行数组中的方法 68 | export const invokeArrayFns = (fns, ...arg) => { 69 | for (let i = 0; i < fns.length; i++) { 70 | fns[i](...arg) 71 | } 72 | } 73 | 74 | export const objectToString = Object.prototype.toString 75 | export const toTypeString = (value: unknown): string => { 76 | return objectToString.call(value) 77 | } 78 | export const toRawType = (value: unknown): string => { 79 | // [object String] // 只要String 这部分 80 | return toTypeString(value).slice(8, -1) 81 | } 82 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './general' 2 | export { toDisplayString } from './toDisplayString' 3 | export { ShapeFlags } from './ShapeFlags' 4 | -------------------------------------------------------------------------------- /packages/shared/src/toDisplayString.ts: -------------------------------------------------------------------------------- 1 | export function toDisplayString(val) { 2 | return String(val) 3 | } 4 | -------------------------------------------------------------------------------- /packages/vue/example/HelloVue/App.js: -------------------------------------------------------------------------------- 1 | import { h, reactive, ref, effect, stop } from '../../lib/vue3.esm.js' 2 | 3 | export default { 4 | render() { 5 | return h( 6 | 'div', 7 | { 8 | id: 'root', 9 | class: ['flex', 'container-r'], 10 | onClick: this.aclick 11 | }, 12 | [ 13 | h('p', { class: 'red' }, 'hello'), 14 | h( 15 | 'p', 16 | { 17 | class: 'blue', 18 | style: { 19 | color: 'red', 20 | background: 'aqua' 21 | } 22 | }, 23 | ` ${this.wei.age}` 24 | ) 25 | ] 26 | ) 27 | }, 28 | setup() { 29 | // 返回对象或者h()渲染函数 30 | let wei = reactive({ 31 | age: 18 32 | }) 33 | let age 34 | const runner = effect(() => { 35 | age = wei.age 36 | }) 37 | 38 | const aclick = () => { 39 | debugger 40 | wei.age++ 41 | 42 | // console.log(wei.age); 43 | // console.log(age); 44 | stop(runner) 45 | } 46 | 47 | return { 48 | name: 'hi my app', 49 | wei, 50 | aclick 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/vue/example/HelloVue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /packages/vue/example/HelloVue/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/vue3.esm.js' 2 | import App from './App.js' 3 | const rootcontainer = document.querySelector("#app") 4 | 5 | createApp(App).mount(rootcontainer) -------------------------------------------------------------------------------- /packages/vue/example/KeepAlive/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref, KeepAlive } from '../../lib/vue3.esm.js' 2 | 3 | import Foo from './components/foo.js' 4 | import Bar from './components/bar.js' 5 | import Child from './components/child.js' 6 | export default { 7 | name: 'app', 8 | setup() { 9 | const count = ref(0) 10 | window.count = count 11 | return { 12 | count 13 | } 14 | }, 15 | render() { 16 | const components = [h(Foo),h(Bar),h(Child)] 17 | const res = h('div', {}, 18 | h(KeepAlive, {max:2}, components[this.count]), 19 | // h(KeepAlive, {max:2}, 'undefined'), 20 | ) 21 | return res 22 | }, 23 | } -------------------------------------------------------------------------------- /packages/vue/example/KeepAlive/components/bar.js: -------------------------------------------------------------------------------- 1 | import { h, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref,onActivated,onDeactivated } from '../../../lib/vue3.esm.js' 2 | 3 | export default { 4 | name: 'bar', 5 | setup() { 6 | onMounted(() =>{ 7 | console.log('bar onMounted is call '); 8 | }) 9 | onUpdated(() =>{ 10 | console.log('bar onUpdated is call '); 11 | }) 12 | onBeforeMount(() =>{ 13 | console.log('bar onBeforeMount is call '); 14 | }) 15 | onBeforeUpdate(() =>{ 16 | console.log('bar onBeforeUpdate is call '); 17 | }) 18 | onBeforeUnmount(() =>{ 19 | console.log('bar onBeforeUnmount is call '); 20 | }) 21 | onUnmounted(() =>{ 22 | console.log('bar onUnmounted is call '); 23 | }) 24 | onActivated(() => { 25 | console.log('bar onActivated'); 26 | }) 27 | onDeactivated(() => { 28 | console.log('bar onDeactivated'); 29 | }) 30 | return { 31 | } 32 | }, 33 | render() { 34 | return h('div', {}, 'bar') 35 | }, 36 | } -------------------------------------------------------------------------------- /packages/vue/example/KeepAlive/components/child.js: -------------------------------------------------------------------------------- 1 | import { h, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref,onActivated,onDeactivated } from '../../../lib/vue3.esm.js' 2 | 3 | export default { 4 | name: 'child', 5 | setup() { 6 | onMounted(() =>{ 7 | console.log('child onMounted is call '); 8 | }) 9 | onUpdated(() =>{ 10 | console.log('child onUpdated is call '); 11 | }) 12 | onBeforeMount(() =>{ 13 | console.log('child onBeforeMount is call '); 14 | }) 15 | onBeforeUpdate(() =>{ 16 | console.log('child onBeforeUpdate is call '); 17 | }) 18 | onBeforeUnmount(() =>{ 19 | console.log('child onBeforeUnmount is call '); 20 | }) 21 | onUnmounted(() =>{ 22 | console.log('child onUnmounted is call '); 23 | }) 24 | onActivated(() => { 25 | console.log('child onActivated'); 26 | }) 27 | onDeactivated(() => { 28 | console.log('child onDeactivated'); 29 | }) 30 | return { 31 | } 32 | }, 33 | render() { 34 | return h('div', {}, 'child') 35 | }, 36 | } -------------------------------------------------------------------------------- /packages/vue/example/KeepAlive/components/foo.js: -------------------------------------------------------------------------------- 1 | import { h, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref,onActivated,onDeactivated } from '../../../lib/vue3.esm.js' 2 | 3 | export default { 4 | name: 'foo', 5 | setup() { 6 | onMounted(() => { 7 | console.log('foo onMounted is call '); 8 | }) 9 | onUpdated(() => { 10 | console.log('foo onUpdated is call '); 11 | }) 12 | onBeforeMount(() => { 13 | console.log('foo onBeforeMount is call '); 14 | }) 15 | onBeforeUpdate(() => { 16 | console.log('foo onBeforeUpdate is call '); 17 | }) 18 | onBeforeUnmount(() => { 19 | console.log('foo onBeforeUnmount is call '); 20 | }) 21 | onUnmounted(() => { 22 | console.log('foo onUnmounted is call '); 23 | }) 24 | onActivated(() => { 25 | console.log('foo onActivated'); 26 | }) 27 | onDeactivated(() => { 28 | console.log('foo onDeactivated'); 29 | }) 30 | return { 31 | } 32 | }, 33 | render() { 34 | return h('div', {}, 'foo') 35 | }, 36 | } -------------------------------------------------------------------------------- /packages/vue/example/KeepAlive/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/vue/example/KeepAlive/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/vue3.esm.js' 2 | import App from './App.js' 3 | const rootcontainer = document.querySelector("#app") 4 | 5 | createApp(App).mount(rootcontainer) -------------------------------------------------------------------------------- /packages/vue/example/LifeCycle/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/vue3.esm.js' 2 | 3 | import Foo from './components/foo.js' 4 | import Bar from './components/bar.js' 5 | import ComponentToComponent from './components/index.js' 6 | export default { 7 | name: 'app', 8 | setup() { 9 | const isChange = ref(true) 10 | const count =ref(1) 11 | window.isChange = isChange 12 | return { 13 | isChange, 14 | count 15 | } 16 | }, 17 | render() { 18 | const res = h('div', {}, 19 | [ 20 | h("div", { 21 | onClick:function(){ 22 | window.isChange.value = !window.isChange.value 23 | console.log(window.isChange.value); 24 | } 25 | }, '主页'), 26 | h('div', {}, this.isChange ? h(Foo) : h(Bar)), 27 | h('div', {}, this.count), 28 | h('div', {}, '213') 29 | ] 30 | ) 31 | // const res = h(ComponentToComponent) 32 | return res 33 | }, 34 | } -------------------------------------------------------------------------------- /packages/vue/example/LifeCycle/component/bar.js: -------------------------------------------------------------------------------- 1 | import { h, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref } from '../../../lib/vue3.esm.js' 2 | 3 | export default { 4 | name: 'bar', 5 | setup() { 6 | onMounted(() =>{ 7 | console.log('bar onMounted is call '); 8 | }) 9 | onUpdated(() =>{ 10 | console.log('bar onUpdated is call '); 11 | }) 12 | onBeforeMount(() =>{ 13 | console.log('bar onBeforeMount is call '); 14 | }) 15 | onBeforeUpdate(() =>{ 16 | console.log('bar onBeforeUpdate is call '); 17 | }) 18 | onBeforeUnmount(() =>{ 19 | console.log('bar onBeforeUnmount is call '); 20 | }) 21 | onUnmounted(() =>{ 22 | console.log('bar onUnmounted is call '); 23 | }) 24 | return { 25 | } 26 | }, 27 | render() { 28 | return h('div', {}, 'bar') 29 | }, 30 | } -------------------------------------------------------------------------------- /packages/vue/example/LifeCycle/component/foo.js: -------------------------------------------------------------------------------- 1 | import { h, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref } from '../../../lib/vue3.esm.js' 2 | 3 | export default { 4 | name: 'foo', 5 | setup() { 6 | onMounted(() =>{ 7 | console.log('foo onMounted is call '); 8 | }) 9 | onUpdated(() =>{ 10 | console.log('foo onUpdated is call '); 11 | }) 12 | onBeforeMount(() =>{ 13 | console.log('foo onBeforeMount is call '); 14 | }) 15 | onBeforeUpdate(() =>{ 16 | console.log('foo onBeforeUpdate is call '); 17 | }) 18 | onBeforeUnmount(() =>{ 19 | console.log('foo onBeforeUnmount is call '); 20 | }) 21 | onUnmounted(() =>{ 22 | console.log('foo onUnmounted is call '); 23 | }) 24 | return { 25 | } 26 | }, 27 | render() { 28 | return h('div', {}, 'foo') 29 | }, 30 | } -------------------------------------------------------------------------------- /packages/vue/example/LifeCycle/component/index.js: -------------------------------------------------------------------------------- 1 | import Foo from './foo.js' 2 | import Bar from './bar.js' 3 | import { h, ref } from '../../../lib/vue3.esm.js' 4 | 5 | 6 | export default { 7 | name: 'componetToComponent', 8 | setup() { 9 | const isChange = ref(false) 10 | window.isChange = isChange 11 | return { 12 | isChange 13 | } 14 | }, 15 | render() { 16 | console.log('被重新执行咯', this.isChange); 17 | return this.isChange ? h(Foo) : h(Bar) 18 | // const res = h('div', {}, this.isChange ? h('div', {}, 'red') : h('div', {}, 'wop')) 19 | return res 20 | }, 21 | } -------------------------------------------------------------------------------- /packages/vue/example/LifeCycle/index.html: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/vue3.esm.js' 2 | import App from './App.js' 3 | const rootcontainer = document.querySelector("#app") 4 | 5 | createApp(App).mount(rootcontainer) -------------------------------------------------------------------------------- /packages/vue/example/LifeCycle/main.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/vue3.esm.js' 2 | 3 | import Foo from './components/foo.js' 4 | import Bar from './components/bar.js' 5 | import ComponentToComponent from './components/index.js' 6 | export default { 7 | name: 'app', 8 | setup() { 9 | const isChange = ref(true) 10 | const count =ref(1) 11 | window.isChange = isChange 12 | return { 13 | isChange, 14 | count 15 | } 16 | }, 17 | render() { 18 | const res = h('div', {}, 19 | [ 20 | h("div", { 21 | onClick:function(){ 22 | window.isChange.value = !window.isChange.value 23 | console.log(window.isChange.value); 24 | } 25 | }, '主页'), 26 | h('div', {}, this.isChange ? h(Foo) : h(Bar)), 27 | h('div', {}, this.count), 28 | h('div', {}, '213') 29 | ] 30 | ) 31 | // const res = h(ComponentToComponent) 32 | return res 33 | }, 34 | } -------------------------------------------------------------------------------- /packages/vue/example/compiler-basic/App.js: -------------------------------------------------------------------------------- 1 | // 最简单的情况 2 | // template 只有一个 interpolation 3 | // export default { 4 | // template: `{{msg}}`, 5 | // setup() { 6 | // return { 7 | // msg: "vue3 - compiler", 8 | // }; 9 | // }, 10 | // }; 11 | 12 | 13 | // 复杂一点 14 | // template 包含 element 和 interpolation 15 | export default { 16 | template: `

{{msg}}

`, 17 | setup() { 18 | return { 19 | msg: "vue3 - compiler", 20 | }; 21 | }, 22 | }; -------------------------------------------------------------------------------- /packages/vue/example/compiler-basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/vue/example/component/App.js: -------------------------------------------------------------------------------- 1 | import { h, reactive, ref } from '../../lib/vue3.esm.js' 2 | import Foo from './foo.js' 3 | export default { 4 | render() { 5 | return h('div', { class: 'red' }, [ 6 | h( 7 | Foo, 8 | { 9 | count: 1, 10 | onMyemitClick: this.myemitClick 11 | }, 12 | '' 13 | ) 14 | ]) 15 | }, 16 | setup() { 17 | const myemitClick = () => { 18 | console.log('app接收到foo发射的事件') 19 | } 20 | 21 | const foo = ref(1) 22 | const obj = reactive({ foo }) 23 | console.log(obj.foo, '11111111') // 1); 24 | 25 | // 返回对象或者h()渲染函数 26 | return { 27 | name: 'hi my app', 28 | myemitClick 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/vue/example/component/foo.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/vue3.esm.js' 2 | 3 | export default { 4 | render() { 5 | return h( 6 | 'div', 7 | { 8 | onClick: this.myClick 9 | }, 10 | 'foo' + this.count 11 | ) 12 | }, 13 | props: { 14 | count: Number 15 | }, 16 | setup(props, { emit }) { 17 | const myClick = () => { 18 | console.log('Foo - click') 19 | 20 | emit('myemitClick', props.count) 21 | } 22 | console.log('foo--- props', props) 23 | props.count++ //不允许操作的 控制度应该提示props是只读属性 24 | console.log('foo--- props', props) 25 | return { 26 | myClick 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/vue/example/component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /packages/vue/example/component/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/vue3.esm.js' 2 | import App from './App.js' 3 | const rootcontainer = document.querySelector('#app') 4 | 5 | createApp(App).mount(rootcontainer) 6 | -------------------------------------------------------------------------------- /packages/vue/example/componentSlots/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/vue3.esm.js' 2 | import Foo from './foo.js' 3 | export default { 4 | name: 'App', 5 | render() { 6 | return h('div', { 7 | id: 'root', 8 | class: ['flex', 'container-r'], 9 | }, [ 10 | h('p', { 11 | class: 'red' 12 | }, 'red'), 13 | h('p', { 14 | class: 'blue' 15 | }, this.name), 16 | h(Foo, {}, { 17 | header: ({ age }) => h('p', {}, '我是header slot1--年龄:' + age), 18 | footer: () => h('p', {}, '我是footer slot1') 19 | }) 20 | ]) 21 | }, 22 | setup() { 23 | return { 24 | name: 'hi my vue', 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /packages/vue/example/componentSlots/foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlot, getCurrentInstance } from '../../lib/vue3.esm.js' 2 | export default { 3 | name: "Foo", 4 | render() { 5 | //这里的this指向组件的instance 我们需要在setup函数中做文章,vue官网允许setup函数第二个参数可以使emit 同样也可以是slots 所以 6 | // 我们需要在内部执行setup函数的时候 将instance下的slot手动传给setup第二个参数 插槽说白了就是一个函数 返回了一个h函数,到这里我们就可以清楚renderSlot函数的职责 就是执行h函数罢了 7 | const foo = h('p', {}, '原本就在Foo里面的元素') 8 | return h('div', {}, [renderSlot(this.$slots, 'header', { age: 18 }), foo, renderSlot(this.$slots, 'footer')]) 9 | }, 10 | setup(props, { emit }) { 11 | console.log('当前组件实例', getCurrentInstance()); 12 | return { 13 | } 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /packages/vue/example/componentSlots/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /packages/vue/example/componentSlots/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/vue3.esm.js' 2 | import App from './App.js' 3 | const rootcontainer = document.querySelector("#app") 4 | 5 | createApp(App).mount(rootcontainer) -------------------------------------------------------------------------------- /packages/vue/example/componentUpdate/App.js: -------------------------------------------------------------------------------- 1 | // 在 render 中使用 proxy 调用 emit 函数 2 | // 也可以直接使用 this 3 | // 验证 proxy 的实现逻辑 4 | import { 5 | h, 6 | ref, 7 | onBeforeMount, 8 | onMounted, 9 | onUpdated, 10 | onBeforeUnmount, 11 | onUnmounted, 12 | onBeforeUpdate, 13 | } from '../../lib/vue3.esm.js' 14 | 15 | import Child from "./Child.js"; 16 | 17 | export default { 18 | name: "App", 19 | setup() { 20 | const msg = ref("123"); 21 | let count = ref(1); 22 | window.msg = msg 23 | 24 | const changeChildProps = () => { 25 | msg.value = "456"; 26 | }; 27 | 28 | const changeCount = () => { 29 | count.value++ 30 | } 31 | onMounted(() => { 32 | console.log('app onMounted is call '); 33 | }) 34 | onUpdated(() => { 35 | console.log('app onUpdated is call '); 36 | }) 37 | onBeforeMount(() => { 38 | console.log('app onBeforeMount is call '); 39 | }) 40 | onBeforeUpdate(() => { 41 | console.log('app onBeforeUpdate is call '); 42 | }) 43 | onBeforeUnmount(() => { 44 | console.log('app onBeforeUnmount is call '); 45 | }) 46 | onUnmounted(() => { 47 | console.log('app onUnmounted is call '); 48 | }) 49 | 50 | return { 51 | msg, 52 | count, 53 | changeChildProps, 54 | changeCount, 55 | }; 56 | }, 57 | 58 | render() { 59 | const res = h("div", {}, [ 60 | h("div", {}, "你好"), 61 | h( 62 | "button", 63 | { 64 | onClick: this.changeChildProps, 65 | }, 66 | "change child props" 67 | ), 68 | h(Child, { 69 | msg: this.msg, 70 | }), 71 | h('p', {}, `count:${this.count}`), 72 | h('button', { onClick: this.changeCount }, `addBtn`) 73 | ]); 74 | return res 75 | }, 76 | }; -------------------------------------------------------------------------------- /packages/vue/example/componentUpdate/child.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | ref, 4 | onBeforeMount, 5 | onMounted, 6 | onUpdated, 7 | onBeforeUnmount, 8 | onUnmounted, 9 | onBeforeUpdate, 10 | } from '../../lib/vue3.esm.js' 11 | 12 | export default { 13 | name: "Child", 14 | setup(props, { emit }) { 15 | 16 | onMounted(() => { 17 | console.log('children onMounted is call '); 18 | }) 19 | onUpdated(() => { 20 | console.log('children onUpdated is call '); 21 | }) 22 | onBeforeMount(() => { 23 | console.log('children onBeforeMount is call '); 24 | }) 25 | onBeforeUpdate(() => { 26 | console.log('children onBeforeUpdate is call '); 27 | }) 28 | onBeforeUnmount(() => { 29 | console.log('children onBeforeUnmount is call '); 30 | }) 31 | onUnmounted(() => { 32 | console.log('children onUnmounted is call '); 33 | }) 34 | 35 | }, 36 | render(proxy) { 37 | return h("div", {}, h("div", {}, "child" + this.$props.msg)); 38 | }, 39 | }; -------------------------------------------------------------------------------- /packages/vue/example/componentUpdate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/vue/example/componentsAttrs/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/vue3.esm.js' 2 | import Child from './cpns/Child.js' 3 | export default { 4 | name: 'App', 5 | setup() { 6 | const age = ref(18) 7 | const handleClick = () => { 8 | console.log(111) 9 | age.value++ 10 | } 11 | return { handleClick, age } 12 | }, 13 | render() { 14 | return h('div', {}, [ 15 | h('div', 'text App'), 16 | h(Child, { 17 | // 这里会进行了统一的规范: name & age 如果是props 则会在组件内部通过defineProps进行定义 其余的属性统一当做attrs(即不属于props的都会被当成attrs) 18 | name: 'coderwei', 19 | age: 19, 20 | number: this.age, 21 | cart: 'bar' 22 | }), 23 | h('button', { onClick: this.handleClick }, 'add') 24 | ]) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/vue/example/componentsAttrs/cpns/Child.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../../lib/vue3.esm.js' 2 | export default { 3 | name: 'child', 4 | props: { 5 | name: { 6 | type: String, 7 | default: 'coder' 8 | }, 9 | age: { 10 | type: Number 11 | } 12 | }, 13 | setup(props, ctx) { 14 | const handleClick = () => { 15 | console.log(ctx.attrs.number) 16 | } 17 | return { 18 | handleClick 19 | } 20 | }, 21 | render() { 22 | return h( 23 | 'div', 24 | {}, 25 | h('div', `age:${this.age}`), 26 | h('button', { onClick: this.handleClick }, 'add3') 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/vue/example/componentsAttrs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 |
11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/example/defineAsyncComponent/App.js: -------------------------------------------------------------------------------- 1 | import {h,defineAsyncComponent} from '../../lib/vue3.esm.js' 2 | import Child from './asyncComponent.js' 3 | import Loading from './defaultPlaceholder.js' 4 | import ErrorComponent from './Error.js' 5 | export default { 6 | name:"App", 7 | setup(){ 8 | return {} 9 | }, 10 | 11 | render(){ 12 | let flag =true 13 | const fetchComponent = () =>{ 14 | return new Promise((resolve,reject) =>{ 15 | setTimeout(() =>{ 16 | if(flag){ 17 | reject('请求出错啦') 18 | flag = false 19 | }else{ 20 | console.log('请求成功'); 21 | resolve(Child) 22 | } 23 | },4000) 24 | }) 25 | } 26 | 27 | const testError = (err, userRetry, userFail, retries ) =>{ 28 | console.log(retries,'次数'); 29 | userRetry() 30 | } 31 | const com = defineAsyncComponent({ 32 | loader:fetchComponent, 33 | loadingComponent:Loading, 34 | errorComponent:ErrorComponent, 35 | delay:200, 36 | timeout:3000, 37 | onError:testError 38 | }) 39 | return h(com) 40 | } 41 | } -------------------------------------------------------------------------------- /packages/vue/example/defineAsyncComponent/App1.js: -------------------------------------------------------------------------------- 1 | import {h,defineAsyncComponent} from '../../lib/vue3.esm.js' 2 | import Child from './asyncComponent.js' 3 | import Loading from './defaultPlaceholder.js' 4 | import ErrorComponent from './Error.js' 5 | export default { 6 | name:"App", 7 | setup(){ 8 | return {} 9 | }, 10 | // 三秒的超时时间,超时后 先渲染错误组件 然后再过一秒 网络请求回来了 是失败的状态 也无所谓 内部会重新渲染这个component组件 (update 更新的操作) 保证最终传给用户定义的异步组件内部的props.error是网络请求失败的原因 11 | render(){ 12 | const fetchComponent = () =>{ 13 | return new Promise((resolve,reject) =>{ 14 | setTimeout(() =>{ 15 | reject('请求出错啦') 16 | },4000) 17 | }) 18 | } 19 | 20 | const com = defineAsyncComponent({ 21 | loader:fetchComponent, 22 | loadingComponent:Loading, 23 | errorComponent:ErrorComponent, 24 | delay:200, 25 | timeout:3000, 26 | }) 27 | return h(com) 28 | } 29 | } -------------------------------------------------------------------------------- /packages/vue/example/defineAsyncComponent/App2.js: -------------------------------------------------------------------------------- 1 | import {h,defineAsyncComponent} from '../../lib/vue3.esm.js' 2 | import Child from './asyncComponent.js' 3 | import Loading from './defaultPlaceholder.js' 4 | import ErrorComponent from './Error.js' 5 | // 异步组件去请求已经失败了 超时的功能就没意义了 内部就不需要在继续计算超时的状态了 6 | export default { 7 | name:"App", 8 | setup(){ 9 | return {} 10 | }, 11 | 12 | render(){ 13 | const fetchComponent = () =>{ 14 | return new Promise((resolve,reject) =>{ 15 | setTimeout(() =>{ 16 | reject('请求出错啦') 17 | },3000) 18 | }) 19 | } 20 | 21 | const com = defineAsyncComponent({ 22 | loader:fetchComponent, 23 | loadingComponent:Loading, 24 | errorComponent:ErrorComponent, 25 | delay:200, 26 | timeout:5000, 27 | }) 28 | return h(com) 29 | } 30 | } -------------------------------------------------------------------------------- /packages/vue/example/defineAsyncComponent/App3.js: -------------------------------------------------------------------------------- 1 | import {h,defineAsyncComponent} from '../../lib/vue3.esm.js' 2 | import Child from './asyncComponent.js' 3 | import Loading from './defaultPlaceholder.js' 4 | import ErrorComponent from './Error.js' 5 | // 当timeout 超时后 渲染错误组件,然后网络请求成功了,能够请求下来异步组件的时候,又会渲染异步组件,按道理来说异步组件就不渲染了。 6 | export default { 7 | name:"App", 8 | setup(){ 9 | return {} 10 | }, 11 | 12 | render(){ 13 | const fetchComponent = () =>{ 14 | return new Promise((resolve,reject) =>{ 15 | setTimeout(() =>{ 16 | console.log('请求成功'); 17 | resolve(Child) 18 | },5000) 19 | }) 20 | } 21 | 22 | const com = defineAsyncComponent({ 23 | loader:fetchComponent, 24 | loadingComponent:Loading, 25 | errorComponent:ErrorComponent, 26 | delay:200, 27 | timeout:3000, 28 | }) 29 | return h(com) 30 | } 31 | } -------------------------------------------------------------------------------- /packages/vue/example/defineAsyncComponent/Error.js: -------------------------------------------------------------------------------- 1 | import { effect, h,toRef } from "../../lib/vue3.esm.js" 2 | 3 | export default { 4 | name:"ErrorComponent", 5 | setup(props){ 6 | console.log(props.error.message,'props'); 7 | let message = toRef(props,'error'); 8 | return { 9 | message 10 | } 11 | }, 12 | render(props){ 13 | // return h('div',{},this.message.message) 14 | return h('div',{},'error component') 15 | } 16 | } -------------------------------------------------------------------------------- /packages/vue/example/defineAsyncComponent/asyncComponent.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/vue3.esm.js" 2 | 3 | export default { 4 | name:"home", 5 | setup(){}, 6 | render(){ 7 | return h('div',{},'home') 8 | } 9 | } -------------------------------------------------------------------------------- /packages/vue/example/defineAsyncComponent/defaultPlaceholder.js: -------------------------------------------------------------------------------- 1 | import { h } from "../../lib/vue3.esm.js" 2 | 3 | export default { 4 | name:"loading", 5 | setup(){ 6 | }, 7 | render(){ 8 | return h('h1',{},'正在加载中...') 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /packages/vue/example/defineAsyncComponent/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 |
13 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /packages/vue/example/nextTicker/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref, getCurrentInstance, nextTick } from "../../lib/vue3.esm.js" 2 | 3 | export default { 4 | setup() { 5 | const count = ref(1) 6 | const instance = getCurrentInstance() 7 | const click = () => { 8 | for (let i = 0; i < 100; i++) { 9 | count.value++ 10 | } 11 | nextTick(() => { 12 | console.log(instance, 'instance'); 13 | }) 14 | } 15 | return { 16 | count, 17 | click 18 | } 19 | }, 20 | render() { 21 | return h('div', {}, [ 22 | h('div', {}, `count:${this.count}`), 23 | h('button', { onClick: this.click }, 'button'), 24 | ]) 25 | }, 26 | } -------------------------------------------------------------------------------- /packages/vue/example/nextTicker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/vue/example/numberNotRender/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref, getCurrentInstance, nextTick } from "../../lib/vue3.esm.js" 2 | 3 | export default { 4 | name:"App", 5 | setup(){}, 6 | render(){ 7 | const res = h('div',{},21) 8 | return res 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /packages/vue/example/numberNotRender/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 |
12 | 13 | 14 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /packages/vue/example/provide&inject/App.js: -------------------------------------------------------------------------------- 1 | import { h, provide } from '../../lib/vue3.esm.js' 2 | import Bar from './bar.js' 3 | export default { 4 | name: 'App', 5 | render() { 6 | return h('div', { 7 | id: 'root', 8 | class: ['flex', 'container-r'], 9 | }, [ 10 | h('p', { 11 | class: 'red' 12 | }, 'red'), 13 | h('p', { 14 | class: 'blue' 15 | }, this.name), 16 | h(Bar, {}, [ 17 | ]) 18 | ]) 19 | }, 20 | setup() { 21 | provide('foo', 'fooVal') 22 | provide('bar', 'barVal') 23 | return { 24 | name: 'hi my vue', 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /packages/vue/example/provide&inject/bar.js: -------------------------------------------------------------------------------- 1 | import { h, inject, provide } from '../../lib/vue3.esm.js' 2 | import Foo from './foo.js' 3 | export default { 4 | name: "bar", 5 | render() { 6 | return h(Foo, {}, []) 7 | }, 8 | setup(props, { emit }) { 9 | provide('foo', 'fooVal-bar') 10 | provide('bar', 'barVal-bar') 11 | const foo = inject('foo1', '默认值') 12 | const bar = inject('bar') 13 | const bar1 = inject('bar1', () => '函数默认值') 14 | console.log('foo1', foo); 15 | console.log('bar1', bar1); 16 | return { 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /packages/vue/example/provide&inject/foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlot, getCurrentInstance } from '../../lib/vue3.esm.js' 2 | export default { 3 | name: "Foo", 4 | render() { 5 | const foo = h('p', {}, '原本就在Foo里面的元素') 6 | return h('div', {}, [foo]) 7 | }, 8 | setup(props, { emit }) { 9 | 10 | console.log('当前组件实例', getCurrentInstance()); 11 | return { 12 | } 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /packages/vue/example/provide&inject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /packages/vue/example/provide&inject/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/vue3.esm.js' 2 | import App from './App.js' 3 | const rootcontainer = document.querySelector("#app") 4 | 5 | createApp(App).mount(rootcontainer) -------------------------------------------------------------------------------- /packages/vue/example/teleport/App.js: -------------------------------------------------------------------------------- 1 | import { h, Teleport } from '../../lib/vue3.esm.js' 2 | 3 | export default { 4 | name: 'App', 5 | setup() {}, 6 | render() { 7 | const Tele = h( 8 | Teleport, 9 | { to: 'body' }, 10 | h('div', { id: 'tem', style: { color: 'red' } }, 'teleport is here') 11 | ) 12 | return h('div', { id: 'demo' }, [h('div', 'i an in demo container'), Tele]) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/vue/example/teleport/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 |
12 | 13 | 14 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /packages/vue/example/update_children/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/vue3.esm.js' 2 | 3 | import TextToText from './TextToText.js' 4 | import TextToArray from './TextToArray&ArrayToText.js' 5 | import ArrayToArray from './ArrayToArray.js' 6 | import Foo from './components/foo.js' 7 | import Bar from './components/bar.js' 8 | import ComponentToComponent from './components/index.js' 9 | export default { 10 | name: 'app', 11 | setup() { 12 | const isChange = ref(true) 13 | window.isChange = isChange 14 | return { 15 | isChange 16 | } 17 | }, 18 | render() { 19 | const res = h('div', {}, 20 | h("div", { 21 | onClick:function(){ 22 | window.isChange.value = !window.isChange.value 23 | console.log(window.isChange.value); 24 | } 25 | }, '主页'), 26 | // h(TextToText) 27 | // h(TextToArray), 28 | h(ArrayToArray) 29 | // h(ComponentToComponent), 30 | // h('div', {}, this.isChange ? h(Foo) : h(Bar)) 31 | ) 32 | // const res = h(ComponentToComponent) 33 | console.log(res,'res'); 34 | return res 35 | }, 36 | } -------------------------------------------------------------------------------- /packages/vue/example/update_children/ArrayToArray.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/vue3.esm.js' 2 | 3 | 4 | // 1. 左侧的对比 5 | // (a b) c 6 | // (a b) d e 7 | // const prevChildren = [ 8 | // h("p", { key: "A" }, "A"), 9 | // h("p", { key: "B" }, "B"), 10 | // h("p", { key: "C" }, "C"), 11 | // ]; 12 | // const nextChildren = [ 13 | // h("p", { key: "A" }, "A"), 14 | // h("p", { key: "B" }, "B"), 15 | // h("p", { key: "D" }, "D"), 16 | // h("p", { key: "E" }, "E"), 17 | // ]; 18 | 19 | // 2. 右侧的对比 20 | // a (b c) 21 | // d e (b c) 22 | // const prevChildren = [ 23 | // h("p", { key: "A" }, "A"), 24 | // h("p", { key: "B" }, "B"), 25 | // h("p", { key: "C" }, "C"), 26 | // ]; 27 | // const nextChildren = [ 28 | // h("p", { key: "D" }, "D"), 29 | // h("p", { key: "E" }, "E"), 30 | // h("p", { key: "B" }, "B"), 31 | // h("p", { key: "C" }, "C"), 32 | // ]; 33 | 34 | // 3. 新的比老的长 35 | // 创建新的 36 | // 左侧 37 | // (a b) 38 | // (a b) c 39 | // i = 2, e1 = 1, e2 = 2 40 | // const prevChildren = [ 41 | // h("p", { key: "A" }, "A"), 42 | // h("p", { key: "B" }, "B") 43 | // ]; 44 | // const nextChildren = [ 45 | // h("p", { key: "A" }, "A"), 46 | // h("p", { key: "B" }, "B"), 47 | // h("p", { key: "C" }, "C"), 48 | // h("p", { key: "C" }, "D"), 49 | // ]; 50 | 51 | // 右侧 52 | // (a b) 53 | // c (a b) 54 | // i = 0, e1 = -1, e2 = 0 55 | // const prevChildren = [ 56 | // h("p", { key: "A" }, "A"), 57 | // h("p", { key: "B" }, "B") 58 | // ]; 59 | // const nextChildren = [ 60 | // h("p", { key: "C" }, "C"), 61 | // h("p", { key: "D" }, "D"), 62 | // h("p", { key: "A" }, "A"), 63 | // h("p", { key: "B" }, "B"), 64 | // ]; 65 | 66 | // 4. 老的比新的长 67 | // 删除老的 68 | // 左侧 69 | // (a b) c 70 | // (a b) 71 | // i = 2, e1 = 2, e2 = 1 72 | // const prevChildren = [ 73 | // h("p", { key: "A" }, "A"), 74 | // h("p", { key: "B" }, "B"), 75 | // h("p", { key: "C" }, "C"), 76 | // ]; 77 | // const nextChildren = [ 78 | // h("p", { key: "A" }, "A"), 79 | // h("p", { key: "B" }, "B") 80 | // ]; 81 | 82 | // 右侧 83 | // a (b c) 84 | // (b c) 85 | // i = 0, e1 = 0, e2 = -1 86 | 87 | // const prevChildren = [ 88 | // h("p", { key: "A" }, "A"), 89 | // h("p", { key: "B" }, "B"), 90 | // h("p", { key: "C" }, "C"), 91 | // ]; 92 | // const nextChildren = [ 93 | // h("p", { key: "B" }, "B"), 94 | // h("p", { key: "C" }, "C") 95 | // ]; 96 | 97 | // 5. 对比中间的部分 98 | // 删除老的 (在老的里面存在,新的里面不存在) 99 | // 5.1 100 | // a,b,(c,d),f,g 101 | // a,b,(e,c),f,g 102 | // D 节点在新的里面是没有的 - 需要删除掉 103 | // C 节点 props 也发生了变化 104 | 105 | // const prevChildren = [ 106 | // h("p", { key: "A" }, "A"), 107 | // h("p", { key: "B" }, "B"), 108 | // h("p", { key: "C", id: "c-prev" }, "C"), 109 | // h("p", { key: "D" }, "D"), 110 | // h("p", { key: "F" }, "F"), 111 | // h("p", { key: "G" }, "G"), 112 | // ]; 113 | 114 | // const nextChildren = [ 115 | // h("p", { key: "A" }, "A"), 116 | // h("p", { key: "B" }, "B"), 117 | // h("p", { key: "E" }, "E"), 118 | // h("p", { key: "C", id: "c-next" }, "C"), 119 | // h("p", { key: "F" }, "F"), 120 | // h("p", { key: "G" }, "G"), 121 | // ]; 122 | 123 | // 5.1.1 124 | // a,b,(c,e,d),f,g 125 | // a,b,(e,c),f,g 126 | // 中间部分,老的比新的多, 那么多出来的直接就可以被干掉(优化删除逻辑) 127 | // const prevChildren = [ 128 | // h("p", { key: "A" }, "A"), 129 | // h("p", { key: "B" }, "B"), 130 | // h("p", { key: "C", id: "c-prev" }, "C"), 131 | // h("p", { key: "E" }, "E"), 132 | // h("p", { key: "D" }, "D"), 133 | // h("p", { key: "F" }, "F"), 134 | // h("p", { key: "G" }, "G"), 135 | // ]; 136 | 137 | // const nextChildren = [ 138 | // h("p", { key: "A" }, "A"), 139 | // h("p", { key: "B" }, "B"), 140 | // h("p", { key: "E" }, "E"), 141 | // h("p", { key: "C", id: "c-next" }, "C"), 142 | // h("p", { key: "F" }, "F"), 143 | // h("p", { key: "G" }, "G"), 144 | // ]; 145 | 146 | // 2 移动 (节点存在于新的和老的里面,但是位置变了) 147 | 148 | // 2.1 149 | // a,b,(c,d,e),f,g 150 | // a,b,(e,c,d),f,g 151 | // 最长子序列: [1,2] 152 | 153 | // const prevChildren = [ 154 | // h("p", { key: "A" }, "A"), 155 | // h("p", { key: "B" }, "B"), 156 | // h("p", { key: "C" }, "C"), 157 | // h("p", { key: "D" }, "D"), 158 | // h("p", { key: "E" }, "E"), 159 | // h("p", { key: "F" }, "F"), 160 | // h("p", { key: "G" }, "G"), 161 | // ]; 162 | 163 | // const nextChildren = [ 164 | // h("p", { key: "A" }, "A"), 165 | // h("p", { key: "B" }, "B"), 166 | // h("p", { key: "E" }, "E"), 167 | // h("p", { key: "C" }, "C"), 168 | // h("p", { key: "D" }, "D"), 169 | // h("p", { key: "F" }, "F"), 170 | // h("p", { key: "G" }, "G"), 171 | // ]; 172 | 173 | // 2.2 174 | // a,b,(c,d,e,z),f,g 175 | // a,b,(d,c,y,e),f,g 176 | // 最长子序列: [1,3] 177 | 178 | const prevChildren = [ 179 | h("p", { key: "A" }, "A"), 180 | h("p", { key: "B" }, "B"), 181 | 182 | h("p", { key: "C" }, "C"), 183 | h("p", { key: "D" }, "D"), 184 | h("p", { key: "E" }, "E"), 185 | h("p", { key: "Z" }, "Z"), 186 | 187 | 188 | h("p", { key: "F" }, "F"), 189 | h("p", { key: "G" }, "G"), 190 | ]; 191 | 192 | const nextChildren = [ 193 | h("p", { key: "A" }, "A"), 194 | h("p", { key: "B" }, "B"), 195 | 196 | h("p", { key: "D" }, "D"), 197 | h("p", { key: "C" }, "C"), 198 | h("p", { key: "Y" }, "Y"), 199 | h("p", { key: "E" }, "E"), 200 | 201 | h("p", { key: "F" }, "F"), 202 | h("p", { key: "G" }, "G"), 203 | ]; 204 | 205 | // 3. 创建新的节点 206 | // a,b,(c,e),f,g 207 | // a,b,(e,c,d),f,g 208 | // d 节点在老的节点中不存在,新的里面存在,所以需要创建 209 | // const prevChildren = [ 210 | // h("p", { key: "A" }, "A"), 211 | // h("p", { key: "B" }, "B"), 212 | // h("p", { key: "C" }, "C"), 213 | // h("p", { key: "E" }, "E"), 214 | // h("p", { key: "F" }, "F"), 215 | // h("p", { key: "G" }, "G"), 216 | // ]; 217 | 218 | // const nextChildren = [ 219 | // h("p", { key: "A" }, "A"), 220 | // h("p", { key: "B" }, "B"), 221 | // h("p", { key: "E" }, "E"), 222 | // h("p", { key: "C" }, "C"), 223 | // h("p", { key: "D" }, "D"), 224 | // h("p", { key: "F" }, "F"), 225 | // h("p", { key: "G" }, "G"), 226 | // ]; 227 | export default { 228 | name: 'app', 229 | setup() { 230 | const isChange = ref(true) 231 | window.isChange = isChange 232 | return { 233 | isChange 234 | } 235 | 236 | }, 237 | render() { 238 | return this.isChange ? h('div', {}, prevChildren) : h('div', {}, nextChildren) 239 | }, 240 | } -------------------------------------------------------------------------------- /packages/vue/example/update_children/TextToArray&ArrayToText.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/vue3.esm.js' 2 | 3 | 4 | export default { 5 | name: 'text', 6 | setup() { 7 | const isChange = ref(true) 8 | window.isChange = isChange 9 | return { 10 | isChange 11 | } 12 | }, 13 | render() { 14 | return this.isChange ? h('div', {}, "string") : h('div', {}, [ 15 | h('div', {}, 'array1'), 16 | h('div', {}, 'array1'), 17 | ]) 18 | }, 19 | } -------------------------------------------------------------------------------- /packages/vue/example/update_children/TextToText.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/vue3.esm.js' 2 | 3 | 4 | export default { 5 | name: 'app', 6 | setup() { 7 | const isChange = ref(false) 8 | window.isChange = isChange 9 | return { 10 | isChange 11 | } 12 | }, 13 | render() { 14 | return this.isChange ? h('div', {}, "string") : h('div', {}, 'hahahha') 15 | }, 16 | } -------------------------------------------------------------------------------- /packages/vue/example/update_children/components/bar.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../../lib/vue3.esm.js' 2 | 3 | export default { 4 | name: 'bar', 5 | setup() { 6 | console.log('bar 的setup执行'); 7 | return { 8 | } 9 | }, 10 | render() { 11 | return h('div', {}, 'bar') 12 | }, 13 | } -------------------------------------------------------------------------------- /packages/vue/example/update_children/components/foo.js: -------------------------------------------------------------------------------- 1 | import { h, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref } from '../../../lib/vue3.esm.js' 2 | 3 | export default { 4 | name: 'foo', 5 | setup() { 6 | onMounted(() =>{ 7 | console.log('onMounted is call '); 8 | }) 9 | onUpdated(() =>{ 10 | console.log('onUpdated is call '); 11 | }) 12 | onBeforeMount(() =>{ 13 | console.log('onBeforeMount is call '); 14 | }) 15 | onBeforeUpdate(() =>{ 16 | console.log('onBeforeUpdate is call '); 17 | }) 18 | onBeforeUnmount(() =>{ 19 | console.log('onBeforeUnmount is call '); 20 | }) 21 | onUnmounted(() =>{ 22 | console.log('onUnmounted is call '); 23 | }) 24 | return { 25 | } 26 | }, 27 | render() { 28 | return h('div', {}, 'foo') 29 | }, 30 | } -------------------------------------------------------------------------------- /packages/vue/example/update_children/components/index.js: -------------------------------------------------------------------------------- 1 | import Foo from './foo.js' 2 | import Bar from './bar.js' 3 | import { h, ref } from '../../../lib/vue3.esm.js' 4 | 5 | 6 | export default { 7 | name: 'componetToComponent', 8 | setup() { 9 | const isChange = ref(false) 10 | window.isChange = isChange 11 | return { 12 | isChange 13 | } 14 | }, 15 | render() { 16 | console.log('被重新执行咯', this.isChange); 17 | return this.isChange ? h(Foo) : h(Bar) 18 | // const res = h('div', {}, this.isChange ? h('div', {}, 'red') : h('div', {}, 'wop')) 19 | return res 20 | }, 21 | } -------------------------------------------------------------------------------- /packages/vue/example/update_children/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /packages/vue/example/update_children/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/vue3.esm.js' 2 | import App from './App.js' 3 | const rootcontainer = document.querySelector("#app") 4 | 5 | createApp(App).mount(rootcontainer) -------------------------------------------------------------------------------- /packages/vue/example/update_props/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/vue3.esm.js' 2 | 3 | 4 | export default { 5 | name: 'app', 6 | setup() { 7 | let colors = ref('red') 8 | let _foo = ref('_foo') 9 | const count = ref(0) 10 | const click = () => { 11 | // colors.value = "change" 12 | count.value++ 13 | } 14 | const removeClass = () => { 15 | colors.value = undefined 16 | } 17 | 18 | const removeFoo = () => { 19 | _foo.value = undefined 20 | } 21 | return { 22 | count, 23 | click, 24 | colors, 25 | removeClass, 26 | removeFoo, 27 | _foo 28 | } 29 | }, 30 | render() { 31 | return h('div', { class: this.colors, foo: this._foo }, [ 32 | h('p', {}, `count: ${this.count}`), 33 | h('button', { onClick: this.click }, '点击更新'), 34 | h('button', { onClick: this.removeClass }, '点击移除class'), 35 | h('button', { onClick: this.removeFoo }, '点击移除foo'), 36 | ]) 37 | }, 38 | } -------------------------------------------------------------------------------- /packages/vue/example/update_props/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /packages/vue/example/update_props/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/vue3.esm.js' 2 | import App from './App.js' 3 | const rootcontainer = document.querySelector("#app") 4 | 5 | createApp(App).mount(rootcontainer) -------------------------------------------------------------------------------- /packages/vue/example/watchEffect/App.js: -------------------------------------------------------------------------------- 1 | // 在 render 中使用 proxy 调用 emit 函数 2 | // 也可以直接使用 this 3 | // 验证 proxy 的实现逻辑 4 | import { h, ref,watchEffect,watch ,reactive} from '../../lib/vue3.esm.js' 5 | 6 | 7 | export default { 8 | name: "App", 9 | setup() { 10 | let count = ref(1); 11 | let p =document.querySelector('#app') 12 | const changeCount = () => { 13 | // p = 14 | // count.value++ 15 | info.age++ 16 | } 17 | watchEffect(() =>{ 18 | count.value 19 | console.log('watchEffect is call',p?.innerHTML); 20 | },{ 21 | flush:'post' 22 | }) 23 | 24 | let info = reactive({ 25 | age:19 26 | }) 27 | debugger 28 | watch(()=>info.age,() =>{ 29 | console.log('watch is be call'); 30 | }) 31 | 32 | return { 33 | count, 34 | info, 35 | changeCount, 36 | }; 37 | }, 38 | 39 | render() { 40 | return h("div", {}, [ 41 | h('p', {}, `count:${this.info.age}`), 42 | h('button', { onClick: this.changeCount }, `addBtn`) 43 | ]); 44 | }, 45 | }; -------------------------------------------------------------------------------- /packages/vue/example/watchEffect/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 17 | 18 | -------------------------------------------------------------------------------- /packages/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coderwei-mini-vue3", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "coderwei", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@coderwei-mini-vue3/compiler-core": "workspace:^1.0.0", 14 | "@coderwei-mini-vue3/runtime-dom": "workspace:^1.0.0", 15 | "@coderwei-mini-vue3/shared": "workspace:^1.0.0" 16 | } 17 | } -------------------------------------------------------------------------------- /packages/vue/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@coderwei-mini-vue3/runtime-dom' 2 | 3 | import { baseCompile } from '@coderwei-mini-vue3/compiler-core' 4 | import * as runtimeDom from '@coderwei-mini-vue3/runtime-dom' 5 | 6 | import { createCompiler } from '@coderwei-mini-vue3/runtime-dom' 7 | 8 | function compilerToFunction(template) { 9 | const { code } = baseCompile(template) 10 | 11 | const render = new Function('Vue', code)(runtimeDom) 12 | return render 13 | } 14 | 15 | createCompiler(compilerToFunction) 16 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import sourceMaps from "rollup-plugin-sourcemaps"; 3 | import resolve from "@rollup/plugin-node-resolve"; 4 | import commonjs from "@rollup/plugin-commonjs"; 5 | 6 | export default { 7 | input: "./packages/vue/src/index.ts", 8 | output: [ 9 | { 10 | file: "./packages/vue/lib/vue3.cjs.js", 11 | format: "cjs", 12 | sourcemap:true 13 | }, 14 | { 15 | name:"vue", 16 | file: "./packages/vue/lib/vue3.esm.js", 17 | format: "es", 18 | sourcemap:true 19 | }, 20 | ], 21 | plugins: [ 22 | resolve(), 23 | commonjs(), 24 | typescript(), 25 | sourceMaps() 26 | ], 27 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "esnext", 5 | "rootDir": ".", 6 | "moduleResolution": "node", 7 | "baseUrl": ".", 8 | "types": [ 9 | "vitest/globals", 10 | "node" 11 | ], 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "strict": true, 15 | "noImplicitAny": false, 16 | "skipLibCheck": true, 17 | "sourceMap": true, 18 | "lib": [ 19 | "es2016", 20 | "DOM" 21 | ], 22 | "paths": { 23 | "@coderwei-mini-vue3/*": [ 24 | "packages/*/src" 25 | ] 26 | } 27 | }, 28 | "include": [ 29 | "packages/*/src", 30 | "packages/*/test" 31 | ] 32 | } -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import path from 'path' 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true 7 | }, 8 | resolve: { 9 | alias: [ 10 | { 11 | find: /@coderwei-mini-vue3\/([\w-]*)/, 12 | replacement: path.resolve(__dirname, 'packages') + '/$1/src' 13 | } 14 | ] 15 | } 16 | }) 17 | --------------------------------------------------------------------------------