├── .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('')) {
72 | for (let i = 0; i < ancestors.length; i++) {
73 | const tag = ancestors[i].tag
74 |
75 | if (s.slice(2, 2 + tag.length) == tag) {
76 | return true
77 | }
78 | }
79 | }
80 | // if (parentTag && s.startsWith(`${parentTag}>`)) {
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('')
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