Repository: cuixiaorui/mini-vue
Branch: master
Commit: 07e4e10a5ccd
Files: 135
Total size: 221.7 KB
Directory structure:
gitextract_4amh303j/
├── .gitignore
├── .vscode/
│ ├── launch.json
│ └── settings.json
├── LICENSE
├── README.md
├── README_EN.md
├── package.json
├── packages/
│ ├── compiler-core/
│ │ ├── __tests__/
│ │ │ ├── __snapshots__/
│ │ │ │ └── codegen.spec.ts.snap
│ │ │ ├── codegen.spec.ts
│ │ │ ├── parse.spec.ts
│ │ │ └── transform.spec.ts
│ │ ├── package.json
│ │ └── src/
│ │ ├── ast.ts
│ │ ├── codegen.ts
│ │ ├── compile.ts
│ │ ├── index.ts
│ │ ├── parse.ts
│ │ ├── runtimeHelpers.ts
│ │ ├── transform.ts
│ │ ├── transforms/
│ │ │ ├── transformElement.ts
│ │ │ ├── transformExpression.ts
│ │ │ └── transformText.ts
│ │ └── utils.ts
│ ├── reactivity/
│ │ ├── __tests__/
│ │ │ ├── computed.spec.ts
│ │ │ ├── dep.spec.ts
│ │ │ ├── effect.spec.ts
│ │ │ ├── reactive.spec.ts
│ │ │ ├── readonly.spec.ts
│ │ │ ├── ref.spec.ts
│ │ │ └── shallowReadonly.spec.ts
│ │ ├── package.json
│ │ └── src/
│ │ ├── baseHandlers.ts
│ │ ├── computed.ts
│ │ ├── dep.ts
│ │ ├── effect.ts
│ │ ├── index.ts
│ │ ├── reactive.ts
│ │ └── ref.ts
│ ├── runtime-core/
│ │ ├── __tests__/
│ │ │ ├── apiWatch.spec.ts
│ │ │ ├── componentEmits.spec.ts
│ │ │ ├── rendererComponent.spec.ts
│ │ │ └── rendererElement.spec.ts
│ │ ├── package.json
│ │ └── src/
│ │ ├── .pnpm-debug.log
│ │ ├── apiInject.ts
│ │ ├── apiWatch.ts
│ │ ├── component.ts
│ │ ├── componentEmits.ts
│ │ ├── componentProps.ts
│ │ ├── componentPublicInstance.ts
│ │ ├── componentRenderUtils.ts
│ │ ├── componentSlots.ts
│ │ ├── createApp.ts
│ │ ├── h.ts
│ │ ├── helpers/
│ │ │ └── renderSlot.ts
│ │ ├── index.ts
│ │ ├── renderer.ts
│ │ ├── scheduler.ts
│ │ └── vnode.ts
│ ├── runtime-dom/
│ │ ├── package.json
│ │ └── src/
│ │ └── index.ts
│ ├── runtime-test/
│ │ └── src/
│ │ ├── index.ts
│ │ ├── nodeOps.ts
│ │ ├── patchProp.ts
│ │ └── serialize.ts
│ ├── shared/
│ │ ├── package.json
│ │ └── src/
│ │ ├── index.ts
│ │ ├── shapeFlags.ts
│ │ └── toDisplayString.ts
│ └── vue/
│ ├── cypress/
│ │ ├── e2e/
│ │ │ ├── apiInject.cy.js
│ │ │ ├── componentEmit.cy.js
│ │ │ ├── componentSlots.cy.js
│ │ │ ├── componentUpdate.cy.js
│ │ │ ├── customRenderer.cy.js
│ │ │ ├── getCurrentInstance.cy.js
│ │ │ ├── helloworld.cy.js
│ │ │ ├── nextTicker.cy.js
│ │ │ └── patchChildren.cy.js
│ │ ├── fixtures/
│ │ │ └── example.json
│ │ └── support/
│ │ ├── commands.js
│ │ └── e2e.js
│ ├── cypress.config.js
│ ├── dist/
│ │ ├── mini-vue.cjs.js
│ │ └── mini-vue.esm-bundler.js
│ ├── example/
│ │ ├── apiInject/
│ │ │ ├── App.js
│ │ │ └── index.html
│ │ ├── compiler-base/
│ │ │ ├── App.js
│ │ │ └── index.html
│ │ ├── componentEmit/
│ │ │ ├── App.js
│ │ │ ├── Child.js
│ │ │ └── index.html
│ │ ├── componentProxy/
│ │ │ ├── App.js
│ │ │ ├── Child.js
│ │ │ └── index.html
│ │ ├── componentSlots/
│ │ │ ├── App.js
│ │ │ ├── Child.js
│ │ │ └── index.html
│ │ ├── componentUpdate/
│ │ │ ├── App.js
│ │ │ ├── Child.js
│ │ │ └── index.html
│ │ ├── createTextVnode/
│ │ │ ├── App.js
│ │ │ └── index.html
│ │ ├── customRenderer/
│ │ │ ├── App.js
│ │ │ ├── game.js
│ │ │ ├── index.html
│ │ │ ├── main.js
│ │ │ └── renderer.js
│ │ ├── getCurrentInstance/
│ │ │ ├── App.js
│ │ │ └── index.html
│ │ ├── helloWorld/
│ │ │ ├── App.js
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ ├── nextTicker/
│ │ │ ├── App.js
│ │ │ ├── NextTicker.js
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ ├── patchChildren/
│ │ │ ├── App.js
│ │ │ ├── ArrayToArray.js
│ │ │ ├── ArrayToText.js
│ │ │ ├── TextToArray.js
│ │ │ ├── TextToText.js
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ ├── renderComponent/
│ │ │ ├── App.js
│ │ │ ├── Child.js
│ │ │ └── index.html
│ │ └── setupStateRenderComponent/
│ │ ├── App.js
│ │ └── index.html
│ ├── index.js
│ ├── package.json
│ └── src/
│ └── index.ts
├── pnpm-workspace.yaml
├── rollup.config.js
├── tsconfig.json
└── vitest.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules
================================================
FILE: .vscode/launch.json
================================================
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"name": "vscode-jest-tests",
"request": "launch",
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
"args": ["--runInBand", "--watchAll=false"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
}
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"liveServer.settings.port": 5501
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 zenoslin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, libribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
[CN](README.md) / [EN](README_EN.md)
## mini-vue [](https://github.com/cuixiaorui/mini-vue)
实现最简 vue3 模型,用于深入学习 vue3, 让你更轻松的理解 vue3 的核心逻辑
## Usage
[B 站](https://www.bilibili.com/video/BV1Zy4y1J73E) 提供了视频讲解使用方式
## Why
当我们需要深入学习 vue3 时,我们就需要看源码来学习,但是像这种工业级别的库,源码中有很多逻辑是用于处理边缘情况或者是兼容处理逻辑,是不利于我们学习的。
我们应该关注于核心逻辑,而这个库的目的就是把 vue3 源码中最核心的逻辑剥离出来,只留下核心逻辑,以供大家学习。
## How
基于 vue3 的功能点,一点一点的拆分出来。
代码命名会保持和源码中的一致,方便大家通过命名去源码中查找逻辑。
### Tasking
#### runtime-core
- [x] 支持组件类型
- [x] 支持 element 类型
- [x] 初始化 props
- [x] setup 可获取 props 和 context
- [x] 支持 component emit
- [x] 支持 proxy
- [x] 可以在 render 函数中获取 setup 返回的对象
- [x] nextTick 的实现
- [x] 支持 getCurrentInstance
- [x] 支持 provide/inject
- [x] 支持最基础的 slots
- [x] 支持 Text 类型节点
- [x] 支持 $el api
- [x] 支持 watchEffect
#### reactivity
目标是用自己的 reactivity 支持现有的 demo 运行
- [x] reactive 的实现
- [x] ref 的实现
- [x] readonly 的实现
- [x] computed 的实现
- [x] track 依赖收集
- [x] trigger 触发依赖
- [x] 支持 isReactive
- [x] 支持嵌套 reactive
- [x] 支持 toRaw
- [x] 支持 effect.scheduler
- [x] 支持 effect.stop
- [x] 支持 isReadonly
- [x] 支持 isProxy
- [x] 支持 shallowReadonly
- [x] 支持 proxyRefs
### compiler-core
- [x] 解析插值
- [x] 解析 element
- [x] 解析 text
### runtime-dom
- [x] 支持 custom renderer
### runtime-test
- [x] 支持测试 runtime-core 的逻辑
### infrastructure
- [x] support monorepo with pnpm
### build
```shell
pnpm build
```
### example
通过 server 的方式打开 packages/vue/example/\* 下的 index.html 即可
> 推荐使用 [Live Server](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)
### 初始化
#### 流程图

> 可加 vx:cuixr1314 获取所有脑图(备注:github mini-vue 领取脑图)
#### 关键函数调用图

> 可以基于函数名快速搜索到源码内容
### update
#### 流程图

#### 关键函数调用图

> 可以基于函数名快速搜索到源码内容
### 从零到一实现一遍
自从有了 mini-vue 之后 很多同学都问我 能不能带着他从零到一敲一遍
因为对于源码的学习来讲 看在多遍也不如自己写一遍
为此我把 mini-vue 做成了一套视频课 从零到一带着大家实现一遍 不跳过任何一行代码
当然除了功能上的实现还有编程思想融入到了课程内
比如 TDD、小步走、重构手法、TPP
> TDD 测试驱动开发 影响了我整个技术生涯 可以说在我认识到 TDD 之后 技术才有了质的飞跃
课程目录如下:
1. vue3 源码结构的介绍
2. reactivity 的核心流程
3. runtime-core 初始化的核心流程
4. runtime-core 更新的核心流程
5. setup 环境 -> 集成 jest 做单元测试 & 集成 typescript
6. 实现 effect 返回 runner
7. 实现 effect 的 scheduler 功能
8. 实现 effect 的 stop 功能
9. 实现 readonly 功能
10. 实现 isReactive 和 isReadonly
11. 优化 stop 功能
12. 实现 reactive 和 readonly 嵌套对象转换功能
13. 实现 shallowReadonly 功能
14. 实现 isProxy 功能
15. 实现 isProxy 功能
16. 实现 ref 功能
17. 实现 isRef 和 unRef 功能
18. 实现 proxyR 功能
19. 实现 computed 计算属性功能
20. 实现初始化 component 主流程
21. 实现 rollup 打包
22. 实现初始化 element 主流程
23. 实现组件代理对象
24. 实现 shapeFlags
25. 实现注册事件功能
26. 实现组件 props 功能
27. 实现组件 emit 功能
28. 实现组件 slots 功能
29. 实现 Fragment 和 Text 类型节点
30. 实现 getCurrentInstance
31. 实现依赖注入功能 provide/inject
32. 实现自定义渲染器 custom renderer
33. 更新 element 流程搭建
34. 更新 element 的props
35. 更新 element 的 children
36. 双端对比 diff 算法1
37. 双端对比 diff 算法2 - key 的作用
38. 双端对比 diff 算法3 - 最长子序列的作用
39. 学习尤大解决 bug 的处理方式
40. 实现组件更新功能
41. 实现 nextTick 功能
42. 编译模块概述
43. 实现解析插值功能
44. 实现解析 element 标签
45. 实现解析 text 功能
46. 实现解析三种联合类型 template
47. parse 的实现原理&有限状态机
48. 实现 transform 功能
49. 实现代码生成 string 类型
50. 实现代码生成插值类型
51. 实现代码生成三种联合类型
52. 实现编译 template 成 render 函数
53. 实现 monorepo & 使用 vitest 替换 jest
课程内部包含了 vue3 的三大核心模块:reactivity、runtime 以及 compiler 模块
等你自己手写一遍之后 在去看 vue3 源码或者再去看分析解析 vue3 源码的书籍时你会有不同的体验
除此之外 还录制了课程介绍以及课程试听课
- [课程介绍](https://www.bilibili.com/video/BV16Z4y1r7Wp)
- [试听课](https://www.bilibili.com/video/BV1R341177P7)
- [购买链接](https://cua.h5.xeknow.com/s/xDWLc)
> 可以直接购买 也可以加我 wx: cuixr1314 来咨询这门课是否合适你
除了课程内容以外 还有专门的社群来答疑大家在学习上的问题 😊
================================================
FILE: README_EN.md
================================================
[EN](README.md) / [CN](README.md)
## mini-vue
Implement the simplest vue3 model for in-depth study of vue3 source code
## Usage
[Bilibili](https://www.bilibili.com/video/BV1Zy4y1J73E) Provides a video explaining how to use it
> Can follow my [Bilibili](https://space.bilibili.com/175301983),Interpretation of live source code from time to time
## Discuss
You can join the group to discuss the vue3 source code

> with WeChat
## Service
Provide one-to-one video teaching services, and take you to see the mini-vue source code hand in hand
> Can add group communication
## Why
When we need to learn vue3 in depth, we need to look at the source code to learn, but like this kind of industrial-level library, there are a lot of logic in the source code for processing edge cases or compatible processing logic, which is not conducive to our learning.
We should focus on the core logic, and the purpose of this library is to separate the core logic from the vue3 source code, leaving only the core logic for everyone to learn.
## How
Based on the function points of vue3, split it out bit by bit
The code naming will remain consistent with the source code, so that you can find logic in the source code through naming.
### Tasking
- [x] support component type
- [x] support element type
- [x] init props of component
- [x] context can get props and context in setup
- [x] support component emit
- [x] support proxy
- [x] can get the object returned by setup in the render function
- [x] Implementation of nextTick
- [x] support getCurrentInstance
- [x] support provide/inject
- [x] support basic slots
- [x] support text type
### roadmap
- [ ] support english
- [ ] normalize console.log
### build
```shell
yarn build
```
### example
Open index.html under example/\* use server
> Recommended Use [Live Server](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)
### Initialization
#### flow chart

#### Key function call graph


> The source code content can be quickly searched based on the function name
### update
#### flow chart

#### Key function call graph

> The source code content can be quickly searched based on the function name
================================================
FILE: package.json
================================================
{
"private": true,
"version": "0.0.1",
"description": "Help you learn more efficiently vue3 source code",
"main": "lib/mini-vue.cjs.js",
"module": "lib/mini-vue.esm.js",
"scripts": {
"dev": "rollup -c -w",
"build": "rollup -c",
"test": "vitest"
},
"author": "cuixiaorui",
"homepage": "https://github.com/cuixiaorui",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-commonjs": "^13.0.0",
"@rollup/plugin-node-resolve": "^8.1.0",
"@rollup/plugin-replace": "^2.3.3",
"@rollup/plugin-typescript": "^8.2.5",
"rollup": "^2.17.1",
"rollup-plugin-sourcemaps": "^0.6.2",
"tslib": "^2.3.1",
"typescript": "^4.4.3",
"vitest": "^0.22.1"
},
"dependencies": {
"@vue/reactivity": "^3.0.5",
"pixi.js": "^6.2.0"
}
}
================================================
FILE: packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap
================================================
// Vitest Snapshot v1
exports[`element and interpolation 1`] = `
"
const { toDisplayString : _toDisplayString, createElementVNode : _createElementVNode} = Vue
return function render(_ctx) {return _createElementVNode('div', null, 'hi,' + _toDisplayString(_ctx.msg))}"
`;
exports[`interpolation module 1`] = `
"
const { toDisplayString : _toDisplayString} = Vue
return function render(_ctx) {return _toDisplayString(_ctx.hello)}"
`;
================================================
FILE: packages/compiler-core/__tests__/codegen.spec.ts
================================================
import { generate } from "../src/codegen";
import { baseParse } from "../src/parse";
import { transform } from "../src/transform";
import { transformElement } from "../src/transforms/transformElement";
import { transformExpression } from "../src/transforms/transformExpression";
import { transformText } from "../src/transforms/transformText";
test("interpolation module", () => {
const ast = baseParse("{{hello}}");
transform(ast, {
nodeTransforms: [transformExpression],
});
const { code } = generate(ast);
expect(code).toMatchSnapshot();
});
test("element and interpolation", () => {
const ast = baseParse("
hi,{{msg}}
");
transform(ast, {
nodeTransforms: [transformElement, transformText, transformExpression],
});
const { code } = generate(ast);
expect(code).toMatchSnapshot();
});
================================================
FILE: packages/compiler-core/__tests__/parse.spec.ts
================================================
import { ElementTypes, NodeTypes } from "../src/ast";
import { baseParse } from "../src/parse";
describe("parser", () => {
describe("text", () => {
test("simple text", () => {
const ast = baseParse("some text");
const text = ast.children[0];
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: "some text",
});
});
test("simple text with invalid end tag", () => {
const ast = baseParse("some text");
const text = ast.children[0];
expect(text).toStrictEqual({
type: NodeTypes.TEXT,
content: "some text",
});
});
test("text with interpolation", () => {
const ast = baseParse("some {{ foo + bar }} text");
const text1 = ast.children[0];
const text2 = ast.children[2];
// ast.children[1] 应该是 interpolation
expect(text1).toStrictEqual({
type: NodeTypes.TEXT,
content: "some ",
});
expect(text2).toStrictEqual({
type: NodeTypes.TEXT,
content: " text",
});
});
});
describe("Interpolation", () => {
test("simple interpolation", () => {
// 1. 看看是不是一个 {{ 开头的
// 2. 是的话,那么就作为 插值来处理
// 3. 获取内部 message 的内容即可
const ast = baseParse("{{message}}");
const interpolation = ast.children[0];
expect(interpolation).toStrictEqual({
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `message`,
},
});
});
});
describe("Element", () => {
test("simple div", () => {
const ast = baseParse("hello
");
const element = ast.children[0];
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT,
tag: "div",
tagType: ElementTypes.ELEMENT,
children: [
{
type: NodeTypes.TEXT,
content: "hello",
},
],
});
});
test("element with interpolation", () => {
const ast = baseParse("{{ msg }}
");
const element = ast.children[0];
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT,
tag: "div",
tagType: ElementTypes.ELEMENT,
children: [
{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `msg`,
},
},
],
});
});
test("element with interpolation and text", () => {
const ast = baseParse("hi,{{ msg }}
");
const element = ast.children[0];
expect(element).toStrictEqual({
type: NodeTypes.ELEMENT,
tag: "div",
tagType: ElementTypes.ELEMENT,
children: [
{
type: NodeTypes.TEXT,
content: "hi,",
},
{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: "msg",
},
},
],
});
});
test("should throw error when lack end tag ", () => {
expect(() => {
baseParse("
");
}).toThrow("缺失结束标签:span");
});
});
});
================================================
FILE: packages/compiler-core/__tests__/transform.spec.ts
================================================
import { baseParse } from "../src/parse";
import { TO_DISPLAY_STRING } from "../src/runtimeHelpers";
import { transform } from "../src/transform";
describe("Compiler: transform", () => {
test("context state", () => {
const ast = baseParse(`hello {{ world }}
`);
console.log(ast);
// manually store call arguments because context is mutable and shared
// across calls
const calls: any[] = [];
const plugin = (node, context) => {
calls.push([node, { ...context }]);
};
transform(ast, {
nodeTransforms: [plugin],
});
const div = ast.children[0];
expect(calls.length).toBe(4);
expect(calls[0]).toMatchObject([
ast,
{},
// TODO
// {
// parent: null,
// currentNode: ast,
// },
]);
expect(calls[1]).toMatchObject([
div,
{},
// TODO
// {
// parent: ast,
// currentNode: div,
// },
]);
expect(calls[2]).toMatchObject([
div.children[0],
{},
// {
// parent: div,
// currentNode: div.children[0],
// },
]);
expect(calls[3]).toMatchObject([
div.children[1],
{},
// {
// parent: div,
// currentNode: div.children[1],
// },
]);
});
test("should inject toString helper for interpolations", () => {
const ast = baseParse(`{{ foo }}`);
transform(ast, {});
expect(ast.helpers).toContain(TO_DISPLAY_STRING);
});
});
================================================
FILE: packages/compiler-core/package.json
================================================
{
"name": "@mini-vue/compiler-core",
"version": "1.0.0",
"description": "@mini-vue/compiler-core",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@mini-vue/shared": "workspace:^1.0.0"
}
}
================================================
FILE: packages/compiler-core/src/ast.ts
================================================
import { CREATE_ELEMENT_VNODE } from "./runtimeHelpers";
export const enum NodeTypes {
TEXT,
ROOT,
INTERPOLATION,
SIMPLE_EXPRESSION,
ELEMENT,
COMPOUND_EXPRESSION
}
export const enum ElementTypes {
ELEMENT,
}
export function createSimpleExpression(content) {
return {
type: NodeTypes.SIMPLE_EXPRESSION,
content,
};
}
export function createInterpolation(content) {
return {
type: NodeTypes.INTERPOLATION,
content: content,
};
}
export function createVNodeCall(context, tag, props?, children?) {
if (context) {
context.helper(CREATE_ELEMENT_VNODE);
}
return {
// TODO vue3 里面这里的 type 是 VNODE_CALL
// 是为了 block 而 mini-vue 里面没有实现 block
// 所以创建的是 Element 类型就够用了
type: NodeTypes.ELEMENT,
tag,
props,
children,
};
}
================================================
FILE: packages/compiler-core/src/codegen.ts
================================================
import { isString } from "@mini-vue/shared";
import { NodeTypes } from "./ast";
import {
CREATE_ELEMENT_VNODE,
helperNameMap,
TO_DISPLAY_STRING,
} from "./runtimeHelpers";
export function generate(ast, options = {}) {
// 先生成 context
const context = createCodegenContext(ast, options);
const { push, mode } = context;
// 1. 先生成 preambleContext
if (mode === "module") {
genModulePreamble(ast, context);
} else {
genFunctionPreamble(ast, context);
}
const functionName = "render";
const args = ["_ctx"];
// _ctx,aaa,bbb,ccc
// 需要把 args 处理成 上面的 string
const signature = args.join(", ");
push(`function ${functionName}(${signature}) {`);
// 这里需要生成具体的代码内容
// 开始生成 vnode tree 的表达式
push("return ");
genNode(ast.codegenNode, context);
push("}");
return {
code: context.code,
};
}
function genFunctionPreamble(ast: any, context: any) {
const { runtimeGlobalName, push, newline } = context;
const VueBinging = runtimeGlobalName;
const aliasHelper = (s) => `${helperNameMap[s]} : _${helperNameMap[s]}`;
if (ast.helpers.length > 0) {
push(
`
const { ${ast.helpers.map(aliasHelper).join(", ")}} = ${VueBinging}
`
);
}
newline();
push(`return `);
}
function genNode(node: any, context: any) {
// 生成代码的规则就是读取 node ,然后基于不同的 node 来生成对应的代码块
// 然后就是把代码快给拼接到一起就可以了
switch (node.type) {
case NodeTypes.INTERPOLATION:
genInterpolation(node, context);
break;
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context);
break;
case NodeTypes.ELEMENT:
genElement(node, context);
break;
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context);
break;
case NodeTypes.TEXT:
genText(node, context);
break;
default:
break;
}
}
function genCompoundExpression(node: any, context: any) {
const { push } = context;
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (isString(child)) {
push(child);
} else {
genNode(child, context);
}
}
}
function genText(node: any, context: any) {
// Implement
const { push } = context;
push(`'${node.content}'`);
}
function genElement(node, context) {
const { push, helper } = context;
const { tag, props, children } = node;
push(`${helper(CREATE_ELEMENT_VNODE)}(`);
genNodeList(genNullableArgs([tag, props, children]), context);
push(`)`);
}
function genNodeList(nodes: any, context: any) {
const { push } = context;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (isString(node)) {
push(`${node}`);
} else {
genNode(node, context);
}
// node 和 node 之间需要加上 逗号(,)
// 但是最后一个不需要 "div", [props], [children]
if (i < nodes.length - 1) {
push(", ");
}
}
}
function genNullableArgs(args) {
// 把末尾为null 的都删除掉
// vue3源码中,后面可能会包含 patchFlag、dynamicProps 等编译优化的信息
// 而这些信息有可能是不存在的,所以在这边的时候需要删除掉
let i = args.length;
// 这里 i-- 用的还是特别的巧妙的
// 当为0 的时候自然就退出循环了
while (i--) {
if (args[i] != null) break;
}
// 把为 falsy 的值都替换成 "null"
return args.slice(0, i + 1).map((arg) => arg || "null");
}
function genExpression(node: any, context: any) {
context.push(node.content, node);
}
function genInterpolation(node: any, context: any) {
const { push, helper } = context;
push(`${helper(TO_DISPLAY_STRING)}(`);
genNode(node.content, context);
push(")");
}
function genModulePreamble(ast, context) {
// preamble 就是 import 语句
const { push, newline, runtimeModuleName } = context;
if (ast.helpers.length) {
// 比如 ast.helpers 里面有个 [toDisplayString]
// 那么生成之后就是 import { toDisplayString as _toDisplayString } from "vue"
const code = `import {${ast.helpers
.map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(", ")} } from ${JSON.stringify(runtimeModuleName)}`;
push(code);
}
newline();
push(`export `);
}
function createCodegenContext(
ast: any,
{ runtimeModuleName = "vue", runtimeGlobalName = "Vue", mode = "function" }
): any {
const context = {
code: "",
mode,
runtimeModuleName,
runtimeGlobalName,
helper(key) {
return `_${helperNameMap[key]}`;
},
push(code) {
context.code += code;
},
newline() {
// 换新行
// TODO 需要额外处理缩进
context.code += "\n";
},
};
return context;
}
================================================
FILE: packages/compiler-core/src/compile.ts
================================================
import { generate } from "./codegen";
import { baseParse } from "./parse";
import { transform } from "./transform";
import { transformExpression } from "./transforms/transformExpression";
import { transformElement } from "./transforms/transformElement";
import { transformText } from "./transforms/transformText";
export function baseCompile(template, options) {
// 1. 先把 template 也就是字符串 parse 成 ast
const ast = baseParse(template);
// 2. 给 ast 加点料(- -#)
transform(
ast,
Object.assign(options, {
nodeTransforms: [transformElement, transformText, transformExpression],
})
);
// 3. 生成 render 函数代码
return generate(ast);
}
================================================
FILE: packages/compiler-core/src/index.ts
================================================
export { baseCompile } from "./compile";
================================================
FILE: packages/compiler-core/src/parse.ts
================================================
import { ElementTypes, NodeTypes } from "./ast";
const enum TagType {
Start,
End,
}
export function baseParse(content: string) {
const context = createParserContext(content);
return createRoot(parseChildren(context, []));
}
function createParserContext(content) {
console.log("创建 paserContext");
return {
source: content,
};
}
function parseChildren(context, ancestors) {
console.log("开始解析 children");
const nodes: any = [];
while (!isEnd(context, ancestors)) {
let node;
const s = context.source;
if (startsWith(s, "{{")) {
// 看看如果是 {{ 开头的话,那么就是一个插值, 那么去解析他
node = parseInterpolation(context);
} else if (s[0] === "<") {
if (s[1] === "/") {
// 这里属于 edge case 可以不用关心
// 处理结束标签
if (/[a-z]/i.test(s[2])) {
// 匹配
// 需要改变 context.source 的值 -> 也就是需要移动光标
parseTag(context, TagType.End);
// 结束标签就以为这都已经处理完了,所以就可以跳出本次循环了
continue;
}
} else if (/[a-z]/i.test(s[1])) {
node = parseElement(context, ancestors);
}
}
if (!node) {
node = parseText(context);
}
nodes.push(node);
}
return nodes;
}
function isEnd(context: any, ancestors) {
// 检测标签的节点
// 如果是结束标签的话,需要看看之前有没有开始标签,如果有的话,那么也应该结束
// 这里的一个 edge case 是
// 像这种情况下,其实就应该报错
const s = context.source;
if (context.source.startsWith("")) {
// 从后面往前面查
// 因为便签如果存在的话 应该是 ancestors 最后一个元素
for (let i = ancestors.length - 1; i >= 0; --i) {
if (startsWithEndTagOpen(s, ancestors[i].tag)) {
return true;
}
}
}
// 看看 context.source 还有没有值
return !context.source;
}
function parseElement(context, ancestors) {
// 应该如何解析 tag 呢
//
// 先解析开始 tag
const element = parseTag(context, TagType.Start);
ancestors.push(element);
const children = parseChildren(context, ancestors);
ancestors.pop();
// 解析 end tag 是为了检测语法是不是正确的
// 检测是不是和 start tag 一致
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End);
} else {
throw new Error(`缺失结束标签:${element.tag}`);
}
element.children = children;
return element;
}
function startsWithEndTagOpen(source: string, tag: string) {
// 1. 头部 是不是以 开头的
// 2. 看看是不是和 tag 一样
return (
startsWith(source, "") &&
source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase()
);
}
function parseTag(context: any, type: TagType): any {
// 发现如果不是 > 的话,那么就把字符都收集起来 ->div
// 正则
const match: any = /^<\/?([a-z][^\r\n\t\f />]*)/i.exec(context.source);
const tag = match[1];
// 移动光标
//
advanceBy(context, 1);
if (type === TagType.End) return;
let tagType = ElementTypes.ELEMENT;
return {
type: NodeTypes.ELEMENT,
tag,
tagType,
};
}
function parseInterpolation(context: any) {
// 1. 先获取到结束的index
// 2. 通过 closeIndex - startIndex 获取到内容的长度 contextLength
// 3. 通过 slice 截取内容
// }} 是插值的关闭
// 优化点是从 {{ 后面搜索即可
const openDelimiter = "{{";
const closeDelimiter = "}}";
const closeIndex = context.source.indexOf(
closeDelimiter,
openDelimiter.length
);
// TODO closeIndex -1 需要报错的
// 让代码前进2个长度,可以把 {{ 干掉
advanceBy(context, 2);
const rawContentLength = closeIndex - openDelimiter.length;
const rawContent = context.source.slice(0, rawContentLength);
const preTrimContent = parseTextData(context, rawContent.length);
const content = preTrimContent.trim();
// 最后在让代码前进2个长度,可以把 }} 干掉
advanceBy(context, closeDelimiter.length);
return {
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content,
},
};
}
function parseText(context): any {
console.log("解析 text", context);
// endIndex 应该看看有没有对应的 <
// 比如 hello
// 像这种情况下 endIndex 就应该是在 o 这里
// {
const endTokens = ["<", "{{"];
let endIndex = context.source.length;
for (let i = 0; i < endTokens.length; i++) {
const index = context.source.indexOf(endTokens[i]);
// endIndex > index 是需要要 endIndex 尽可能的小
// 比如说:
// hi, {{123}}
// 那么这里就应该停到 {{ 这里,而不是停到 index) {
endIndex = index;
}
}
const content = parseTextData(context, endIndex);
return {
type: NodeTypes.TEXT,
content,
};
}
function parseTextData(context: any, length: number): any {
console.log("解析 textData");
// 1. 直接返回 context.source
// 从 length 切的话,是为了可以获取到 text 的值(需要用一个范围来确定)
const rawText = context.source.slice(0, length);
// 2. 移动光标
advanceBy(context, length);
return rawText;
}
function advanceBy(context, numberOfCharacters) {
console.log("推进代码", context, numberOfCharacters);
context.source = context.source.slice(numberOfCharacters);
}
function createRoot(children) {
return {
type: NodeTypes.ROOT,
children,
helpers: [],
};
}
function startsWith(source: string, searchString: string): boolean {
return source.startsWith(searchString);
}
================================================
FILE: packages/compiler-core/src/runtimeHelpers.ts
================================================
export const TO_DISPLAY_STRING = Symbol(`toDisplayString`);
export const CREATE_ELEMENT_VNODE = Symbol("createElementVNode");
export const helperNameMap = {
[TO_DISPLAY_STRING]: "toDisplayString",
[CREATE_ELEMENT_VNODE]: "createElementVNode"
};
================================================
FILE: packages/compiler-core/src/transform.ts
================================================
import { NodeTypes } from "./ast";
import { TO_DISPLAY_STRING } from "./runtimeHelpers";
export function transform(root, options = {}) {
// 1. 创建 context
const context = createTransformContext(root, options);
// 2. 遍历 node
traverseNode(root, context);
createRootCodegen(root, context);
root.helpers.push(...context.helpers.keys());
}
function traverseNode(node: any, context) {
const type: NodeTypes = node.type;
// 遍历调用所有的 nodeTransforms
// 把 node 给到 transform
// 用户可以对 node 做处理
const nodeTransforms = context.nodeTransforms;
const exitFns: any = [];
for (let i = 0; i < nodeTransforms.length; i++) {
const transform = nodeTransforms[i];
const onExit = transform(node, context);
if (onExit) {
exitFns.push(onExit);
}
}
switch (type) {
case NodeTypes.INTERPOLATION:
// 插值的点,在于后续生成 render 代码的时候是获取变量的值
context.helper(TO_DISPLAY_STRING);
break;
case NodeTypes.ROOT:
case NodeTypes.ELEMENT:
traverseChildren(node, context);
break;
default:
break;
}
let i = exitFns.length;
// i-- 这个很巧妙
// 使用 while 是要比 for 快 (可以使用 https://jsbench.me/ 来测试一下)
while (i--) {
exitFns[i]();
}
}
function traverseChildren(parent: any, context: any) {
// node.children
parent.children.forEach((node) => {
// TODO 需要设置 context 的值
traverseNode(node, context);
});
}
function createTransformContext(root, options): any {
const context = {
root,
nodeTransforms: options.nodeTransforms || [],
helpers: new Map(),
helper(name) {
// 这里会收集调用的次数
// 收集次数是为了给删除做处理的, (当只有 count 为0 的时候才需要真的删除掉)
// helpers 数据会在后续生成代码的时候用到
const count = context.helpers.get(name) || 0;
context.helpers.set(name, count + 1);
},
};
return context;
}
function createRootCodegen(root: any, context: any) {
const { children } = root;
// 只支持有一个根节点
// 并且还是一个 single text node
const child = children[0];
// 如果是 element 类型的话 , 那么我们需要把它的 codegenNode 赋值给 root
// root 其实是个空的什么数据都没有的节点
// 所以这里需要额外的处理 codegenNode
// codegenNode 的目的是专门为了 codegen 准备的 为的就是和 ast 的 node 分离开
if (child.type === NodeTypes.ELEMENT && child.codegenNode) {
const codegenNode = child.codegenNode;
root.codegenNode = codegenNode;
} else {
root.codegenNode = child;
}
}
================================================
FILE: packages/compiler-core/src/transforms/transformElement.ts
================================================
import { createVNodeCall, NodeTypes } from "../ast";
export function transformElement(node, context) {
if (node.type === NodeTypes.ELEMENT) {
return () => {
// 没有实现 block 所以这里直接创建 element
// TODO
// 需要把之前的 props 和 children 等一系列的数据都处理
const vnodeTag = `'${node.tag}'`;
// TODO props 暂时不支持
const vnodeProps = null;
let vnodeChildren = null;
if (node.children.length > 0) {
if (node.children.length === 1) {
// 只有一个孩子节点 ,那么当生成 render 函数的时候就不用 [] 包裹
const child = node.children[0];
vnodeChildren = child;
}
}
// 创建一个新的 node 用于 codegen 的时候使用
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren
);
};
}
}
================================================
FILE: packages/compiler-core/src/transforms/transformExpression.ts
================================================
import { NodeTypes } from "../ast";
export function transformExpression(node) {
if (node.type === NodeTypes.INTERPOLATION) {
node.content = processExpression(node.content);
}
}
function processExpression(node) {
node.content = `_ctx.${node.content}`;
return node
}
================================================
FILE: packages/compiler-core/src/transforms/transformText.ts
================================================
import { NodeTypes } from "../ast";
import { isText } from "../utils";
export function transformText(node, context) {
if (node.type === NodeTypes.ELEMENT) {
// 在 exit 的时期执行
// 下面的逻辑会改变 ast 树
// 有些逻辑是需要在改变之前做处理的
return () => {
// hi,{{msg}}
// 上面的模块会生成2个节点,一个是 text 一个是 interpolation 的话
// 生成的 render 函数应该为 "hi," + _toDisplayString(_ctx.msg)
// 这里面就会涉及到添加一个 “+” 操作符
// 那这里的逻辑就是处理它
// 检测下一个节点是不是 text 类型,如果是的话, 那么会创建一个 COMPOUND 类型
// COMPOUND 类型把 2个 text || interpolation 包裹(相当于是父级容器)
const children = node.children;
let currentContainer;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (isText(child)) {
// 看看下一个节点是不是 text 类
for (let j = i + 1; j < children.length; j++) {
const next = children[j];
if (isText(next)) {
// currentContainer 的目的是把相邻的节点都放到一个 容器内
if (!currentContainer) {
currentContainer = children[i] = {
type: NodeTypes.COMPOUND_EXPRESSION,
loc: child.loc,
children: [child],
};
}
currentContainer.children.push(` + `, next);
// 把当前的节点放到容器内, 然后删除掉j
children.splice(j, 1);
// 因为把 j 删除了,所以这里就少了一个元素,那么 j 需要 --
j--;
} else {
currentContainer = undefined;
break;
}
}
}
}
};
}
}
================================================
FILE: packages/compiler-core/src/utils.ts
================================================
import { NodeTypes } from "./ast";
export function isText(node) {
return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT;
}
================================================
FILE: packages/reactivity/__tests__/computed.spec.ts
================================================
import { computed } from "../src/computed";
import { reactive } from "../src/reactive";
import {vi} from 'vitest'
describe("computed", () => {
it("happy path", () => {
const value = reactive({
foo: 1,
});
const getter = computed(() => {
return value.foo;
});
value.foo = 2;
expect(getter.value).toBe(2);
});
it("should compute lazily", () => {
const value = reactive({
foo: 1,
});
const getter = vi.fn(() => {
return value.foo;
});
const cValue = computed(getter);
// lazy
expect(getter).not.toHaveBeenCalled();
expect(cValue.value).toBe(1);
expect(getter).toHaveBeenCalledTimes(1);
// should not compute again
cValue.value;
expect(getter).toHaveBeenCalledTimes(1);
// should not compute until needed
value.foo = 2;
expect(getter).toHaveBeenCalledTimes(1);
// now it should compute
expect(cValue.value).toBe(2);
expect(getter).toHaveBeenCalledTimes(2);
// should not compute again
cValue.value;
expect(getter).toHaveBeenCalledTimes(2);
});
});
================================================
FILE: packages/reactivity/__tests__/dep.spec.ts
================================================
describe('Dep', () => {
it('new Set ', () => {
// const set = new Set([1])
// console.log(set)
});
});
================================================
FILE: packages/reactivity/__tests__/effect.spec.ts
================================================
import { reactive } from "../src/reactive";
import { effect, stop } from "../src/effect";
import { vi } from "vitest";
describe("effect", () => {
it("should run the passed function once (wrapped by a effect)", () => {
const fnSpy = vi.fn(() => {});
effect(fnSpy);
expect(fnSpy).toHaveBeenCalledTimes(1);
});
it("should observe basic properties", () => {
let dummy;
const counter = reactive({ num: 0 });
effect(() => (dummy = counter.num));
expect(dummy).toBe(0);
counter.num = 7;
expect(dummy).toBe(7);
});
it("should observe multiple properties", () => {
let dummy;
const counter = reactive({ num1: 0, num2: 0 });
effect(() => (dummy = counter.num1 + counter.num1 + counter.num2));
expect(dummy).toBe(0);
counter.num1 = counter.num2 = 7;
expect(dummy).toBe(21);
});
it("should handle multiple effects", () => {
let dummy1, dummy2;
const counter = reactive({ num: 0 });
effect(() => (dummy1 = counter.num));
effect(() => (dummy2 = counter.num));
expect(dummy1).toBe(0);
expect(dummy2).toBe(0);
counter.num++;
expect(dummy1).toBe(1);
expect(dummy2).toBe(1);
});
it("should observe nested properties", () => {
let dummy;
const counter = reactive({ nested: { num: 0 } });
effect(() => (dummy = counter.nested.num));
expect(dummy).toBe(0);
counter.nested.num = 8;
expect(dummy).toBe(8);
});
it("should observe function call chains", () => {
let dummy;
const counter = reactive({ num: 0 });
effect(() => (dummy = getNum()));
function getNum() {
return counter.num;
}
expect(dummy).toBe(0);
counter.num = 2;
expect(dummy).toBe(2);
});
it("scheduler", () => {
let dummy;
let run: any;
const scheduler = vi.fn(() => {
run = runner;
});
const obj = reactive({ foo: 1 });
const runner = effect(
() => {
dummy = obj.foo;
},
{ scheduler }
);
expect(scheduler).not.toHaveBeenCalled();
expect(dummy).toBe(1);
// should be called on first trigger
obj.foo++;
expect(scheduler).toHaveBeenCalledTimes(1);
// // should not run yet
expect(dummy).toBe(1);
// // manually run
run();
// // should have run
expect(dummy).toBe(2);
});
it("stop", () => {
let dummy;
const obj = reactive({ prop: 1 });
const runner = effect(() => {
dummy = obj.prop;
});
obj.prop = 2;
expect(dummy).toBe(2);
stop(runner);
// obj.prop = 3
obj.prop++;
expect(dummy).toBe(2);
// stopped effect should still be manually callable
runner();
expect(dummy).toBe(3);
});
it("events: onStop", () => {
const onStop = vi.fn();
const runner = effect(() => {}, {
onStop,
});
stop(runner);
expect(onStop).toHaveBeenCalled();
});
});
================================================
FILE: packages/reactivity/__tests__/reactive.spec.ts
================================================
import { reactive, isReactive, toRaw, reactiveMap } from "../src/reactive";
describe("reactive", () => {
test("Object", () => {
const original = { foo: 1 };
const observed = reactive(original);
expect(observed).not.toBe(original);
expect(isReactive(observed)).toBe(true);
expect(isReactive(original)).toBe(false);
// get
expect(observed.foo).toBe(1);
// // has
expect("foo" in observed).toBe(true);
// // ownKeys
expect(Object.keys(observed)).toEqual(["foo"]);
});
test("nested reactives", () => {
const original = {
nested: {
foo: 1,
},
array: [{ bar: 2 }],
};
const observed = reactive(original);
expect(isReactive(observed.nested)).toBe(true);
expect(isReactive(observed.array)).toBe(true);
expect(isReactive(observed.array[0])).toBe(true);
});
test("toRaw", () => {
const original = { foo: 1 };
const observed = reactive(original);
expect(toRaw(observed)).toBe(original);
expect(toRaw(original)).toBe(original);
});
});
================================================
FILE: packages/reactivity/__tests__/readonly.spec.ts
================================================
import { isProxy, isReactive, isReadonly, readonly } from "../src/reactive";
describe("readonly", () => {
it("should make nested values readonly", () => {
const original = { foo: 1, bar: { baz: 2 } };
const wrapped = readonly(original);
expect(wrapped).not.toBe(original);
expect(isProxy(wrapped)).toBe(true);
expect(isReactive(wrapped)).toBe(false);
expect(isReadonly(wrapped)).toBe(true);
expect(isReactive(original)).toBe(false);
expect(isReadonly(original)).toBe(false);
expect(isReactive(wrapped.bar)).toBe(false);
expect(isReadonly(wrapped.bar)).toBe(true);
expect(isReactive(original.bar)).toBe(false);
expect(isReadonly(original.bar)).toBe(false);
// get
expect(wrapped.foo).toBe(1);
});
});
================================================
FILE: packages/reactivity/__tests__/ref.spec.ts
================================================
import { effect } from "../src/effect";
import { reactive } from "../src/reactive";
import { isRef, ref, unRef,proxyRefs } from "../src/ref";
describe("ref", () => {
it("should be reactive", () => {
const a = ref(1);
let dummy;
let calls = 0;
effect(() => {
calls++;
dummy = a.value;
});
expect(calls).toBe(1);
expect(dummy).toBe(1);
a.value = 2;
expect(calls).toBe(2);
expect(dummy).toBe(2);
// same value should not trigger
a.value = 2;
expect(calls).toBe(2);
expect(dummy).toBe(2);
});
it("should make nested properties reactive", () => {
const a = ref({
count: 1,
});
let dummy;
effect(() => {
dummy = a.value.count;
});
expect(dummy).toBe(1);
a.value.count = 2;
expect(dummy).toBe(2);
});
it("proxyRefs", () => {
const user = {
age: ref(10),
name: "xiaohong",
};
const proxyUser = proxyRefs(user);
expect(user.age.value).toBe(10);
expect(proxyUser.age).toBe(10);
expect(proxyUser.name).toBe("xiaohong");
(proxyUser as any).age = 20;
expect(proxyUser.age).toBe(20);
expect(user.age.value).toBe(20);
proxyUser.age = ref(10);
expect(proxyUser.age).toBe(10);
expect(user.age.value).toBe(10);
});
it("isRef", () => {
const a = ref(1);
const user = reactive({
age: 1,
});
expect(isRef(a)).toBe(true);
expect(isRef(1)).toBe(false);
expect(isRef(user)).toBe(false);
});
it("unRef", () => {
const a = ref(1);
expect(unRef(a)).toBe(1);
expect(unRef(1)).toBe(1);
});
});
================================================
FILE: packages/reactivity/__tests__/shallowReadonly.spec.ts
================================================
import { isReactive, isReadonly, readonly, shallowReadonly } from "../src/reactive";
describe("shallowReadonly", () => {
test("should not make non-reactive properties reactive", () => {
const props = shallowReadonly({ n: { foo: 1 } });
expect(isReactive(props.n)).toBe(false);
});
test("should differentiate from normal readonly calls", async () => {
const original = { foo: {} };
const shallowProxy = shallowReadonly(original);
const reactiveProxy = readonly(original);
expect(shallowProxy).not.toBe(reactiveProxy);
expect(isReadonly(shallowProxy.foo)).toBe(false);
expect(isReadonly(reactiveProxy.foo)).toBe(true);
});
});
================================================
FILE: packages/reactivity/package.json
================================================
{
"name": "@mini-vue/reactivity",
"version": "1.0.0",
"description": "@mini-vue/reactivity",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@mini-vue/shared": "workspace:^1.0.0"
}
}
================================================
FILE: packages/reactivity/src/baseHandlers.ts
================================================
import { track, trigger } from "./effect";
import {
reactive,
ReactiveFlags,
reactiveMap,
readonly,
readonlyMap,
shallowReadonlyMap,
} from "./reactive";
import { isObject } from "@mini-vue/shared";
const get = createGetter();
const set = createSetter();
const readonlyGet = createGetter(true);
const shallowReadonlyGet = createGetter(true, true);
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
const isExistInReactiveMap = () =>
key === ReactiveFlags.RAW && receiver === reactiveMap.get(target);
const isExistInReadonlyMap = () =>
key === ReactiveFlags.RAW && receiver === readonlyMap.get(target);
const isExistInShallowReadonlyMap = () =>
key === ReactiveFlags.RAW && receiver === shallowReadonlyMap.get(target);
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly;
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly;
} else if (
isExistInReactiveMap() ||
isExistInReadonlyMap() ||
isExistInShallowReadonlyMap()
) {
return target;
}
const res = Reflect.get(target, key, receiver);
// 问题:为什么是 readonly 的时候不做依赖收集呢
// readonly 的话,是不可以被 set 的, 那不可以被 set 就意味着不会触发 trigger
// 所有就没有收集依赖的必要了
if (!isReadonly) {
// 在触发 get 的时候进行依赖收集
track(target, "get", key);
}
if (shallow) {
return res;
}
if (isObject(res)) {
// 把内部所有的是 object 的值都用 reactive 包裹,变成响应式对象
// 如果说这个 res 值是一个对象的话,那么我们需要把获取到的 res 也转换成 reactive
// res 等于 target[key]
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
function createSetter() {
return function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// 在触发 set 的时候进行触发依赖
trigger(target, "set", key);
return result;
};
}
export const readonlyHandlers = {
get: readonlyGet,
set(target, key) {
// readonly 的响应式对象不可以修改值
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
);
return true;
},
};
export const mutableHandlers = {
get,
set,
};
export const shallowReadonlyHandlers = {
get: shallowReadonlyGet,
set(target, key) {
// readonly 的响应式对象不可以修改值
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
);
return true;
},
};
================================================
FILE: packages/reactivity/src/computed.ts
================================================
import { createDep } from "./dep";
import { ReactiveEffect } from "./effect";
import { trackRefValue, triggerRefValue } from "./ref";
export class ComputedRefImpl {
public dep: any;
public effect: ReactiveEffect;
private _dirty: boolean;
private _value
constructor(getter) {
this._dirty = true;
this.dep = createDep();
this.effect = new ReactiveEffect(getter, () => {
// scheduler
// 只要触发了这个函数说明响应式对象的值发生改变了
// 那么就解锁,后续在调用 get 的时候就会重新执行,所以会得到最新的值
if (this._dirty) return;
this._dirty = true;
triggerRefValue(this);
});
}
get value() {
// 收集依赖
trackRefValue(this);
// 锁上,只可以调用一次
// 当数据改变的时候才会解锁
// 这里就是缓存实现的核心
// 解锁是在 scheduler 里面做的
if (this._dirty) {
this._dirty = false;
// 这里执行 run 的话,就是执行用户传入的 fn
this._value = this.effect.run();
}
return this._value;
}
}
export function computed(getter) {
return new ComputedRefImpl(getter);
}
================================================
FILE: packages/reactivity/src/dep.ts
================================================
// 用于存储所有的 effect 对象
export function createDep(effects?) {
const dep = new Set(effects);
return dep;
}
================================================
FILE: packages/reactivity/src/effect.ts
================================================
import { createDep } from "./dep";
import { extend } from "@mini-vue/shared";
let activeEffect = void 0;
let shouldTrack = false;
const targetMap = new WeakMap();
// 用于依赖收集
export class ReactiveEffect {
active = true;
deps = [];
public onStop?: () => void;
constructor(public fn, public scheduler?) {
console.log("创建 ReactiveEffect 对象");
}
run() {
console.log("run");
// 运行 run 的时候,可以控制 要不要执行后续收集依赖的一步
// 目前来看的话,只要执行了 fn 那么就默认执行了收集依赖
// 这里就需要控制了
// 是不是收集依赖的变量
// 执行 fn 但是不收集依赖
if (!this.active) {
return this.fn();
}
// 执行 fn 收集依赖
// 可以开始收集依赖了
shouldTrack = true;
// 执行的时候给全局的 activeEffect 赋值
// 利用全局属性来获取当前的 effect
activeEffect = this as any;
// 执行用户传入的 fn
console.log("执行用户传入的 fn");
const result = this.fn();
// 重置
shouldTrack = false;
activeEffect = undefined;
return result;
}
stop() {
if (this.active) {
// 如果第一次执行 stop 后 active 就 false 了
// 这是为了防止重复的调用,执行 stop 逻辑
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
function cleanupEffect(effect) {
// 找到所有依赖这个 effect 的响应式对象
// 从这些响应式对象里面把 effect 给删除掉
effect.deps.forEach((dep) => {
dep.delete(effect);
});
effect.deps.length = 0;
}
export function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn);
// 把用户传过来的值合并到 _effect 对象上去
// 缺点就是不是显式的,看代码的时候并不知道有什么值
extend(_effect, options);
_effect.run();
// 把 _effect.run 这个方法返回
// 让用户可以自行选择调用的时机(调用 fn)
const runner: any = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
}
export function stop(runner) {
runner.effect.stop();
}
export function track(target, type, key) {
if (!isTracking()) {
return;
}
console.log(`触发 track -> target: ${target} type:${type} key:${key}`);
// 1. 先基于 target 找到对应的 dep
// 如果是第一次的话,那么就需要初始化
let depsMap = targetMap.get(target);
if (!depsMap) {
// 初始化 depsMap 的逻辑
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = createDep();
depsMap.set(key, dep);
}
trackEffects(dep);
}
export function trackEffects(dep) {
// 用 dep 来存放所有的 effect
// TODO
// 这里是一个优化点
// 先看看这个依赖是不是已经收集了,
// 已经收集的话,那么就不需要在收集一次了
// 可能会影响 code path change 的情况
// 需要每次都 cleanupEffect
// shouldTrack = !dep.has(activeEffect!);
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
(activeEffect as any).deps.push(dep);
}
}
export function trigger(target, type, key) {
// 1. 先收集所有的 dep 放到 deps 里面,
// 后面会统一处理
let deps: Array
= [];
// dep
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 暂时只实现了 GET 类型
// get 类型只需要取出来就可以
const dep = depsMap.get(key);
// 最后收集到 deps 内
deps.push(dep);
const effects: Array = [];
deps.forEach((dep) => {
// 这里解构 dep 得到的是 dep 内部存储的 effect
effects.push(...dep);
});
// 这里的目的是只有一个 dep ,这个dep 里面包含所有的 effect
// 这里的目前应该是为了 triggerEffects 这个函数的复用
triggerEffects(createDep(effects));
}
export function isTracking() {
return shouldTrack && activeEffect !== undefined;
}
export function triggerEffects(dep) {
// 执行收集到的所有的 effect 的 run 方法
for (const effect of dep) {
if (effect.scheduler) {
// scheduler 可以让用户自己选择调用的时机
// 这样就可以灵活的控制调用了
// 在 runtime-core 中,就是使用了 scheduler 实现了在 next ticker 中调用的逻辑
effect.scheduler();
} else {
effect.run();
}
}
}
================================================
FILE: packages/reactivity/src/index.ts
================================================
export {
reactive,
readonly,
shallowReadonly,
isReadonly,
isReactive,
isProxy,
} from "./reactive";
export { ref, proxyRefs, unRef, isRef } from "./ref";
export { effect, stop, ReactiveEffect } from "./effect";
export { computed } from "./computed";
================================================
FILE: packages/reactivity/src/reactive.ts
================================================
import {
mutableHandlers,
readonlyHandlers,
shallowReadonlyHandlers,
} from "./baseHandlers";
export const reactiveMap = new WeakMap();
export const readonlyMap = new WeakMap();
export const shallowReadonlyMap = new WeakMap();
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
IS_READONLY = "__v_isReadonly",
RAW = "__v_raw",
}
export function reactive(target) {
return createReactiveObject(target, reactiveMap, mutableHandlers);
}
export function readonly(target) {
return createReactiveObject(target, readonlyMap, readonlyHandlers);
}
export function shallowReadonly(target) {
return createReactiveObject(
target,
shallowReadonlyMap,
shallowReadonlyHandlers
);
}
export function isProxy(value) {
return isReactive(value) || isReadonly(value);
}
export function isReadonly(value) {
return !!value[ReactiveFlags.IS_READONLY];
}
export function isReactive(value) {
// 如果 value 是 proxy 的话
// 会触发 get 操作,而在 createGetter 里面会判断
// 如果 value 是普通对象的话
// 那么会返回 undefined ,那么就需要转换成布尔值
return !!value[ReactiveFlags.IS_REACTIVE];
}
export function toRaw(value) {
// 如果 value 是 proxy 的话 ,那么直接返回就可以了
// 因为会触发 createGetter 内的逻辑
// 如果 value 是普通对象的话,
// 我们就应该返回普通对象
// 只要不是 proxy ,只要是得到了 undefined 的话,那么就一定是普通对象
// TODO 这里和源码里面实现的不一样,不确定后面会不会有问题
if (!value[ReactiveFlags.RAW]) {
return value;
}
return value[ReactiveFlags.RAW];
}
function createReactiveObject(target, proxyMap, baseHandlers) {
// 核心就是 proxy
// 目的是可以侦听到用户 get 或者 set 的动作
// 如果命中的话就直接返回就好了
// 使用缓存做的优化点
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, baseHandlers);
// 把创建好的 proxy 给存起来,
proxyMap.set(target, proxy);
return proxy;
}
================================================
FILE: packages/reactivity/src/ref.ts
================================================
import { trackEffects, triggerEffects, isTracking } from "./effect";
import { createDep } from "./dep";
import { isObject, hasChanged } from "@mini-vue/shared";
import { reactive } from "./reactive";
export class RefImpl {
private _rawValue: any;
private _value: any;
public dep;
public __v_isRef = true;
constructor(value) {
this._rawValue = value;
// 看看value 是不是一个对象,如果是一个对象的话
// 那么需要用 reactive 包裹一下
this._value = convert(value);
this.dep = createDep();
}
get value() {
// 收集依赖
trackRefValue(this);
return this._value;
}
set value(newValue) {
// 当新的值不等于老的值的话,
// 那么才需要触发依赖
if (hasChanged(newValue, this._rawValue)) {
// 更新值
this._value = convert(newValue);
this._rawValue = newValue;
// 触发依赖
triggerRefValue(this);
}
}
}
export function ref(value) {
return createRef(value);
}
function convert(value) {
return isObject(value) ? reactive(value) : value;
}
function createRef(value) {
const refImpl = new RefImpl(value);
return refImpl;
}
export function triggerRefValue(ref) {
triggerEffects(ref.dep);
}
export function trackRefValue(ref) {
if (isTracking()) {
trackEffects(ref.dep);
}
}
// 这个函数的目的是
// 帮助解构 ref
// 比如在 template 中使用 ref 的时候,直接使用就可以了
// 例如: const count = ref(0) -> 在 template 中使用的话 可以直接 count
// 解决方案就是通过 proxy 来对 ref 做处理
const shallowUnwrapHandlers = {
get(target, key, receiver) {
// 如果里面是一个 ref 类型的话,那么就返回 .value
// 如果不是的话,那么直接返回value 就可以了
return unRef(Reflect.get(target, key, receiver));
},
set(target, key, value, receiver) {
const oldValue = target[key];
if (isRef(oldValue) && !isRef(value)) {
return (target[key].value = value);
} else {
return Reflect.set(target, key, value, receiver);
}
},
};
// 这里没有处理 objectWithRefs 是 reactive 类型的时候
// TODO reactive 里面如果有 ref 类型的 key 的话, 那么也是不需要调用 ref.value 的
// (but 这个逻辑在 reactive 里面没有实现)
export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, shallowUnwrapHandlers);
}
// 把 ref 里面的值拿到
export function unRef(ref) {
return isRef(ref) ? ref.value : ref;
}
export function isRef(value) {
return !!value.__v_isRef;
}
================================================
FILE: packages/runtime-core/__tests__/apiWatch.spec.ts
================================================
import { reactive } from "@mini-vue/reactivity";
import { watchEffect } from "../src/apiWatch";
import { nextTick } from "../src/scheduler";
import { vi } from "vitest";
describe("api: watch", () => {
it("effect", async () => {
const state = reactive({ count: 0 });
let dummy;
watchEffect(() => {
dummy = state.count;
});
expect(dummy).toBe(0);
state.count++;
await nextTick();
expect(dummy).toBe(1);
});
it("stopping the watcher (effect)", async () => {
const state = reactive({ count: 0 });
let dummy;
const stop: any = watchEffect(() => {
dummy = state.count;
});
expect(dummy).toBe(0);
stop();
state.count++;
await nextTick();
// should not update
expect(dummy).toBe(0);
});
it("cleanup registration (effect)", async () => {
const state = reactive({ count: 0 });
const cleanup = vi.fn();
let dummy;
const stop: any = watchEffect((onCleanup) => {
onCleanup(cleanup);
dummy = state.count;
});
expect(dummy).toBe(0);
state.count++;
await nextTick();
expect(cleanup).toHaveBeenCalledTimes(1);
expect(dummy).toBe(1);
stop();
expect(cleanup).toHaveBeenCalledTimes(2);
});
});
================================================
FILE: packages/runtime-core/__tests__/componentEmits.spec.ts
================================================
import { nodeOps, render, h } from "@mini-vue/runtime-test";
import {vi} from 'vitest'
describe("component: emits", () => {
test("trigger handlers", () => {
const Foo = {
render() {
return h("foo");
},
setup(props, { emit }) {
// the `emit` function is bound on component instances
emit("foo");
emit("bar");
},
};
const onfoo = vi.fn();
const onBar = vi.fn();
const Comp = {
render() {
return h(Foo, { onfoo, onBar });
},
};
render(h(Comp), nodeOps.createElement("div"));
expect(onfoo).not.toHaveBeenCalled();
// only capitalized or special chars are considered event listeners
expect(onBar).toHaveBeenCalled();
});
test("trigger camelCase handler", () => {
const Foo = {
render() {
return h("foo");
},
setup(props, { emit }) {
emit("test-event");
},
};
const fooSpy = vi.fn();
const Comp = {
render() {
return h(Foo, { onTestEvent: fooSpy });
},
};
render(h(Comp), nodeOps.createElement("div"));
expect(fooSpy).toHaveBeenCalledTimes(1);
});
test("trigger kebab-case handler", () => {
const Foo = {
render() {
return h("foo");
},
setup(props, { emit }) {
emit("test-event");
},
};
const fooSpy = vi.fn();
const Comp = {
render() {
return h(Foo, { "onTest-event": fooSpy });
},
};
render(h(Comp), nodeOps.createElement("div"));
expect(fooSpy).toHaveBeenCalledTimes(1);
});
});
================================================
FILE: packages/runtime-core/__tests__/rendererComponent.spec.ts
================================================
import { h } from "@mini-vue/runtime-dom";
import { nodeOps, render, serializeInner } from "@mini-vue/runtime-test";
describe("renderer: component", () => {
it("should create an Component ", () => {
const Comp = {
render: () => {
return h("div");
},
};
const root = nodeOps.createElement("div");
render(h(Comp), root);
expect(serializeInner(root)).toBe(``);
});
it("should create an Component with direct text children", () => {
const Comp = {
render: () => {
return h("div", null, "test");
},
};
const root = nodeOps.createElement("div");
render(h(Comp), root);
expect(serializeInner(root)).toBe(`test
`);
});
});
================================================
FILE: packages/runtime-core/__tests__/rendererElement.spec.ts
================================================
import { h } from "@mini-vue/runtime-core";
import { nodeOps, render, serializeInner as inner } from "@mini-vue/runtime-test";
describe("renderer: element", () => {
let root;
beforeEach(() => {
root = nodeOps.createElement("div");
});
it("should create an element", () => {
render(h("div"), root);
expect(inner(root)).toBe("");
});
it('should create an element with props', () => {
render(h('div', { id: 'foo', class: 'bar' },[]), root)
expect(inner(root)).toBe('')
})
it('should create an element with direct text children and props', () => {
render(h('div', { id: 'foo' }, "bar"), root)
expect(inner(root)).toBe('bar
')
})
});
================================================
FILE: packages/runtime-core/package.json
================================================
{
"name": "@mini-vue/runtime-core",
"version": "1.0.0",
"description": "@mini-vue/runtime-core",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@mini-vue/reactivity": "workspace:^1.0.0",
"@mini-vue/shared": "workspace:^1.0.0"
}
}
================================================
FILE: packages/runtime-core/src/.pnpm-debug.log
================================================
{
"0 debug pnpm:scope": {
"selected": 1,
"workspacePrefix": "/Users/cxr/projects/mini-vue/code/mini-vue"
},
"1 error pnpm": {
"errno": 1,
"code": "ELIFECYCLE",
"pkgid": "@mini-vue/runtime-core@1.0.0",
"stage": "test",
"script": "jest \"runtime-core\"",
"pkgname": "@mini-vue/runtime-core",
"err": {
"name": "pnpm",
"message": "@mini-vue/runtime-core@1.0.0 test: `jest \"runtime-core\"`\nExit status 1",
"code": "ELIFECYCLE",
"stack": "pnpm: @mini-vue/runtime-core@1.0.0 test: `jest \"runtime-core\"`\nExit status 1\n at EventEmitter. (/opt/homebrew/Cellar/pnpm/6.32.4/libexec/lib/node_modules/pnpm/dist/pnpm.cjs:105736:20)\n at EventEmitter.emit (node:events:527:28)\n at ChildProcess. (/opt/homebrew/Cellar/pnpm/6.32.4/libexec/lib/node_modules/pnpm/dist/pnpm.cjs:92297:18)\n at ChildProcess.emit (node:events:527:28)\n at maybeClose (node:internal/child_process:1092:16)\n at Process.ChildProcess._handle.onexit (node:internal/child_process:302:5)"
}
}
}
================================================
FILE: packages/runtime-core/src/apiInject.ts
================================================
import { getCurrentInstance } from "./component";
export function provide(key, value) {
const currentInstance = getCurrentInstance();
if (currentInstance) {
let { provides } = currentInstance;
const parentProvides = currentInstance.parent?.provides;
// 这里要解决一个问题
// 当父级 key 和 爷爷级别的 key 重复的时候,对于子组件来讲,需要取最近的父级别组件的值
// 那这里的解决方案就是利用原型链来解决
// provides 初始化的时候是在 createComponent 时处理的,当时是直接把 parent.provides 赋值给组件的 provides 的
// 所以,如果说这里发现 provides 和 parentProvides 相等的话,那么就说明是第一次做 provide(对于当前组件来讲)
// 我们就可以把 parent.provides 作为 currentInstance.provides 的原型重新赋值
// 至于为什么不在 createComponent 的时候做这个处理,可能的好处是在这里初始化的话,是有个懒执行的效果(优化点,只有需要的时候在初始化)
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides);
}
provides[key] = value;
}
}
export function inject(key, defaultValue) {
const currentInstance = getCurrentInstance();
if (currentInstance) {
const provides = currentInstance.parent?.provides;
if (key in provides) {
return provides[key];
} else if (defaultValue) {
if (typeof defaultValue === "function") {
return defaultValue();
}
return defaultValue;
}
}
}
================================================
FILE: packages/runtime-core/src/apiWatch.ts
================================================
import { ReactiveEffect } from "@mini-vue/reactivity";
import { queuePreFlushCb } from "./scheduler";
// Simple effect.
export function watchEffect(effect) {
return doWatch(effect);
}
function doWatch(source) {
// 把 job 添加到 pre flush 里面
// 也就是在视图更新完成之前进行渲染(待确认?)
// 当逻辑执行到这里的时候 就已经触发了 watchEffect
const job = () => {
effect.run();
};
// 当触发 trigger 的时候会调用 scheduler
// 这里用 scheduler 的目的就是在更新的时候
// 让回调可以在 render 前执行 变成一个异步的行为(这里也可以通过 flush 来改变)
const scheduler = () => queuePreFlushCb(job);
// cleanup 的作用是为了解决初始化的时候不调用 fn(用户传过来的 cleanup)
// 第一次执行 watchEffect 的时候 onCleanup 会被调用 而这时候只需要把 fn 赋值给 cleanup 就可以
// 当第二次执行 watchEffect 的时候就需要执行 fn 了 也就是 cleanup
let cleanup;
const onCleanup = (fn) => {
// 当 effect stop 的时候也需要执行 cleanup
// 所以可以在 onStop 中直接执行 fn
cleanup = effect.onStop = () => {
fn();
};
};
// 这里是在执行 effect.run 的时候就会调用的
const getter = () => {
// 这个的检测就是初始化不执行 cleanup 的关键点
if (cleanup) {
cleanup();
}
source(onCleanup);
};
const effect = new ReactiveEffect(getter, scheduler);
// 这里执行的就是 getter
effect.run();
// 返回值为 StopHandle
// 只需要调用 stop 即可
return () => {
effect.stop();
};
}
================================================
FILE: packages/runtime-core/src/component.ts
================================================
import { initProps } from "./componentProps";
import { initSlots } from "./componentSlots";
import { emit } from "./componentEmits";
import { PublicInstanceProxyHandlers } from "./componentPublicInstance";
import { proxyRefs, shallowReadonly } from "@mini-vue/reactivity";
export function createComponentInstance(vnode, parent) {
const instance = {
type: vnode.type,
vnode,
next: null, // 需要更新的 vnode,用于更新 component 类型的组件
props: {},
parent,
provides: parent ? parent.provides : {}, // 获取 parent 的 provides 作为当前组件的初始化值 这样就可以继承 parent.provides 的属性了
proxy: null,
isMounted: false,
attrs: {}, // 存放 attrs 的数据
slots: {}, // 存放插槽的数据
ctx: {}, // context 对象
setupState: {}, // 存储 setup 的返回值
emit: () => {},
};
// 在 prod 坏境下的 ctx 只是下面简单的结构
// 在 dev 环境下会更复杂
instance.ctx = {
_: instance,
};
// 赋值 emit
// 这里使用 bind 把 instance 进行绑定
// 后面用户使用的时候只需要给 event 和参数即可
instance.emit = emit.bind(null, instance) as any;
return instance;
}
export function setupComponent(instance) {
// 1. 处理 props
// 取出存在 vnode 里面的 props
const { props, children } = instance.vnode;
initProps(instance, props);
// 2. 处理 slots
initSlots(instance, children);
// 源码里面有两种类型的 component
// 一种是基于 options 创建的
// 还有一种是 function 的
// 这里处理的是 options 创建的
// 叫做 stateful 类型
setupStatefulComponent(instance);
}
function setupStatefulComponent(instance) {
// todo
// 1. 先创建代理 proxy
console.log("创建 proxy");
// proxy 对象其实是代理了 instance.ctx 对象
// 我们在使用的时候需要使用 instance.proxy 对象
// 因为 instance.ctx 在 prod 和 dev 坏境下是不同的
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
// 用户声明的对象就是 instance.type
// const Component = {setup(),render()} ....
const Component = instance.type;
// 2. 调用 setup
// 调用 setup 的时候传入 props
const { setup } = Component;
if (setup) {
// 设置当前 currentInstance 的值
// 必须要在调用 setup 之前
setCurrentInstance(instance);
const setupContext = createSetupContext(instance);
// 真实的处理场景里面应该是只在 dev 环境才会把 props 设置为只读的
const setupResult =
setup && setup(shallowReadonly(instance.props), setupContext);
setCurrentInstance(null);
// 3. 处理 setupResult
handleSetupResult(instance, setupResult);
} else {
finishComponentSetup(instance);
}
}
function createSetupContext(instance) {
console.log("初始化 setup context");
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit,
expose: () => {}, // TODO 实现 expose 函数逻辑
};
}
function handleSetupResult(instance, setupResult) {
// setup 返回值不一样的话,会有不同的处理
// 1. 看看 setupResult 是个什么
if (typeof setupResult === "function") {
// 如果返回的是 function 的话,那么绑定到 render 上
// 认为是 render 逻辑
// setup(){ return ()=>(h("div")) }
instance.render = setupResult;
} else if (typeof setupResult === "object") {
// 返回的是一个对象的话
// 先存到 setupState 上
// 先使用 @vue/reactivity 里面的 proxyRefs
// 后面我们自己构建
// proxyRefs 的作用就是把 setupResult 对象做一层代理
// 方便用户直接访问 ref 类型的值
// 比如 setupResult 里面有个 count 是个 ref 类型的对象,用户使用的时候就可以直接使用 count 了,而不需要在 count.value
// 这里也就是官网里面说到的自动结构 Ref 类型
instance.setupState = proxyRefs(setupResult);
}
finishComponentSetup(instance);
}
function finishComponentSetup(instance) {
// 给 instance 设置 render
// 先取到用户设置的 component options
const Component = instance.type;
if (!instance.render) {
// 如果 compile 有值 并且当组件没有 render 函数,那么就需要把 template 编译成 render 函数
if (compile && !Component.render) {
if (Component.template) {
// 这里就是 runtime 模块和 compile 模块结合点
const template = Component.template;
Component.render = compile(template);
}
}
instance.render = Component.render;
}
// applyOptions()
}
function applyOptions() {
// 兼容 vue2.x
// todo
// options api
}
let currentInstance = {};
// 这个接口暴露给用户,用户可以在 setup 中获取组件实例 instance
export function getCurrentInstance(): any {
return currentInstance;
}
export function setCurrentInstance(instance) {
currentInstance = instance;
}
let compile;
export function registerRuntimeCompiler(_compile) {
compile = _compile;
}
================================================
FILE: packages/runtime-core/src/componentEmits.ts
================================================
import { camelize, hyphenate, toHandlerKey } from "@mini-vue/shared";
export function emit(instance, event: string, ...rawArgs) {
// 1. emit 是基于 props 里面的 onXXX 的函数来进行匹配的
// 所以我们先从 props 中看看是否有对应的 event handler
const props = instance.props;
// ex: event -> click 那么这里取的就是 onClick
// 让事情变的复杂一点如果是烤肉串命名的话,需要转换成 change-page -> changePage
// 需要得到事件名称
let handler = props[toHandlerKey(camelize(event))];
// 如果上面没有匹配的话 那么在检测一下 event 是不是 kebab-case 类型
if (!handler) {
handler = props[(toHandlerKey(hyphenate(event)))]
}
if (handler) {
handler(...rawArgs);
}
}
================================================
FILE: packages/runtime-core/src/componentProps.ts
================================================
export function initProps(instance, rawProps) {
console.log("initProps");
// TODO
// 应该还有 attrs 的概念
// attrs
// 如果组件声明了 props 的话,那么才可以进入 props 属性内
// 不然的话是需要存储在 attrs 内
// 这里暂时直接赋值给 instance.props 即可
instance.props = rawProps;
}
================================================
FILE: packages/runtime-core/src/componentPublicInstance.ts
================================================
import { hasOwn } from "@mini-vue/shared";
const publicPropertiesMap = {
// 当用户调用 instance.proxy.$emit 时就会触发这个函数
// i 就是 instance 的缩写 也就是组件实例对象
$el: (i) => i.vnode.el,
$emit: (i) => i.emit,
$slots: (i) => i.slots,
$props: (i) => i.props,
};
// todo 需要让用户可以直接在 render 函数内直接使用 this 来触发 proxy
export const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
// 用户访问 proxy[key]
// 这里就匹配一下看看是否有对应的 function
// 有的话就直接调用这个 function
const { setupState, props } = instance;
console.log(`触发 proxy hook , key -> : ${key}`);
if (key[0] !== "$") {
// 说明不是访问 public api
// 先检测访问的 key 是否存在于 setupState 中, 是的话直接返回
if (hasOwn(setupState, key)) {
return setupState[key];
} else if (hasOwn(props, key)) {
// 看看 key 是不是在 props 中
// 代理是可以访问到 props 中的 key 的
return props[key];
}
}
const publicGetter = publicPropertiesMap[key];
if (publicGetter) {
return publicGetter(instance);
}
},
set({ _: instance }, key, value) {
const { setupState } = instance;
if (hasOwn(setupState, key)) {
// 有的话 那么就直接赋值
setupState[key] = value;
}
return true
},
};
================================================
FILE: packages/runtime-core/src/componentRenderUtils.ts
================================================
export function shouldUpdateComponent(prevVNode, nextVNode) {
const { props: prevProps } = prevVNode;
const { props: nextProps } = nextVNode;
// const emits = component!.emitsOptions;
// 这里主要是检测组件的 props
// 核心:只要是 props 发生改变了,那么这个 component 就需要更新
// 1. props 没有变化,那么不需要更新
if (prevProps === nextProps) {
return false;
}
// 如果之前没有 props,那么就需要看看现在有没有 props 了
// 所以这里基于 nextProps 的值来决定是否更新
if (!prevProps) {
return !!nextProps;
}
// 之前有值,现在没值,那么肯定需要更新
if (!nextProps) {
return true;
}
// 以上都是比较明显的可以知道 props 是否是变化的
// 在 hasPropsChanged 会做更细致的对比检测
return hasPropsChanged(prevProps, nextProps);
}
function hasPropsChanged(prevProps, nextProps): boolean {
// 依次对比每一个 props.key
// 提前对比一下 length ,length 不一致肯定是需要更新的
const nextKeys = Object.keys(nextProps);
if (nextKeys.length !== Object.keys(prevProps).length) {
return true;
}
// 只要现在的 prop 和之前的 prop 不一样那么就需要更新
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i];
if (nextProps[key] !== prevProps[key]) {
return true;
}
}
return false;
}
================================================
FILE: packages/runtime-core/src/componentSlots.ts
================================================
import { ShapeFlags } from "@mini-vue/shared";
export function initSlots(instance, children) {
const { vnode } = instance;
console.log("初始化 slots");
if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
normalizeObjectSlots(children, (instance.slots = {}));
}
}
const normalizeSlotValue = (value) => {
// 把 function 返回的值转换成 array ,这样 slot 就可以支持多个元素了
return Array.isArray(value) ? value : [value];
};
const normalizeObjectSlots = (rawSlots, slots) => {
for (const key in rawSlots) {
const value = rawSlots[key];
if (typeof value === "function") {
// 把这个函数给到slots 对象上存起来
// 后续在 renderSlots 中调用
// TODO 这里没有对 value 做 normalize,
// 默认 slots 返回的就是一个 vnode 对象
slots[key] = (props) => normalizeSlotValue(value(props));
}
}
};
================================================
FILE: packages/runtime-core/src/createApp.ts
================================================
import { createVNode } from "./vnode";
export function createAppAPI(render) {
return function createApp(rootComponent) {
const app = {
_component: rootComponent,
mount(rootContainer) {
console.log("基于根组件创建 vnode");
const vnode = createVNode(rootComponent);
console.log("调用 render,基于 vnode 进行开箱");
render(vnode, rootContainer);
},
};
return app;
};
}
================================================
FILE: packages/runtime-core/src/h.ts
================================================
import { createVNode } from "./vnode";
export const h = (type: any , props: any = null, children: string | Array = []) => {
return createVNode(type, props, children);
};
================================================
FILE: packages/runtime-core/src/helpers/renderSlot.ts
================================================
import { createVNode, Fragment } from "../vnode";
/**
* Compiler runtime helper for rendering ``
* 用来 render slot 的
* 之前是把 slot 的数据都存在 instance.slots 内(可以看 componentSlot.ts),
* 这里就是取数据然后渲染出来的点
* 这个是由 compiler 模块直接渲染出来的 -可以参看这个 demo https://vue-next-template-explorer.netlify.app/#%7B%22src%22%3A%22%3Cdiv%3E%5Cn%20%20%3Cslot%3E%3C%2Fslot%3E%5Cn%3C%2Fdiv%3E%22%2C%22ssr%22%3Afalse%2C%22options%22%3A%7B%22mode%22%3A%22module%22%2C%22prefixIdentifiers%22%3Afalse%2C%22optimizeImports%22%3Afalse%2C%22hoistStatic%22%3Afalse%2C%22cacheHandlers%22%3Afalse%2C%22scopeId%22%3Anull%2C%22inline%22%3Afalse%2C%22ssrCssVars%22%3A%22%7B%20color%20%7D%22%2C%22bindingMetadata%22%3A%7B%22TestComponent%22%3A%22setup-const%22%2C%22setupRef%22%3A%22setup-ref%22%2C%22setupConst%22%3A%22setup-const%22%2C%22setupLet%22%3A%22setup-let%22%2C%22setupMaybeRef%22%3A%22setup-maybe-ref%22%2C%22setupProp%22%3A%22props%22%2C%22vMySetupDir%22%3A%22setup-const%22%7D%2C%22optimizeBindings%22%3Afalse%7D%7D
* 其最终目的就是在 render 函数中调用 renderSlot 取 instance.slots 内的数据
* TODO 这里应该是一个返回一个 block ,但是暂时还没有支持 block ,所以这个暂时只需要返回一个 vnode 即可
* 因为 block 的本质就是返回一个 vnode
*
* @private
*/
export function renderSlot(slots, name: string, props = {}) {
const slot = slots[name];
console.log(`渲染插槽 slot -> ${name}`);
if (slot) {
// 因为 slot 是一个返回 vnode 的函数,我们只需要把这个结果返回出去即可
// slot 就是一个函数,所以就可以把当前组件的一些数据给传出去,这个就是作用域插槽
// 参数就是 props
const slotContent = slot(props);
return createVNode(Fragment, {}, slotContent);
}
}
================================================
FILE: packages/runtime-core/src/index.ts
================================================
export * from "./h";
export * from "./createApp";
export { getCurrentInstance, registerRuntimeCompiler } from "./component";
export { inject, provide } from "./apiInject";
export { renderSlot } from "./helpers/renderSlot";
export { createTextVNode, createElementVNode } from "./vnode";
export { createRenderer } from "./renderer";
export { toDisplayString } from "@mini-vue/shared";
export { watchEffect } from "./apiWatch";
export {
// core
reactive,
ref,
readonly,
// utilities
unRef,
proxyRefs,
isReadonly,
isReactive,
isProxy,
isRef,
// advanced
shallowReadonly,
// effect
effect,
stop,
computed,
} from "@mini-vue/reactivity";
================================================
FILE: packages/runtime-core/src/renderer.ts
================================================
import { ShapeFlags } from "@mini-vue/shared";
import { createComponentInstance } from "./component";
import { queueJob } from "./scheduler";
import { effect } from "@mini-vue/reactivity";
import { setupComponent } from "./component";
import { Fragment, normalizeVNode, Text } from "./vnode";
import { shouldUpdateComponent } from "./componentRenderUtils";
import { createAppAPI } from "./createApp";
export function createRenderer(options) {
const {
createElement: hostCreateElement,
setElementText: hostSetElementText,
patchProp: hostPatchProp,
insert: hostInsert,
remove: hostRemove,
setText: hostSetText,
createText: hostCreateText,
} = options;
const render = (vnode, container) => {
console.log("调用 patch")
patch(null, vnode, container);
};
function patch(
n1,
n2,
container = null,
anchor = null,
parentComponent = null
) {
// 基于 n2 的类型来判断
// 因为 n2 是新的 vnode
const { type, shapeFlag } = n2;
switch (type) {
case Text:
processText(n1, n2, container);
break;
// 其中还有几个类型比如: static fragment comment
case Fragment:
processFragment(n1, n2, container);
break;
default:
// 这里就基于 shapeFlag 来处理
if (shapeFlag & ShapeFlags.ELEMENT) {
console.log("处理 element");
processElement(n1, n2, container, anchor, parentComponent);
} else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
console.log("处理 component");
processComponent(n1, n2, container, parentComponent);
}
}
}
function processFragment(n1: any, n2: any, container: any) {
// 只需要渲染 children ,然后给添加到 container 内
if (!n1) {
// 初始化 Fragment 逻辑点
console.log("初始化 Fragment 类型的节点");
mountChildren(n2.children, container);
}
}
function processText(n1, n2, container) {
console.log("处理 Text 节点");
if (n1 === null) {
// n1 是 null 说明是 init 的阶段
// 基于 createText 创建出 text 节点,然后使用 insert 添加到 el 内
console.log("初始化 Text 类型的节点");
hostInsert((n2.el = hostCreateText(n2.children as string)), container);
} else {
// update
// 先对比一下 updated 之后的内容是否和之前的不一样
// 在不一样的时候才需要 update text
// 这里抽离出来的接口是 setText
// 注意,这里一定要记得把 n1.el 赋值给 n2.el, 不然后续是找不到值的
const el = (n2.el = n1.el!);
if (n2.children !== n1.children) {
console.log("更新 Text 类型的节点");
hostSetText(el, n2.children as string);
}
}
}
function processElement(n1, n2, container, anchor, parentComponent) {
if (!n1) {
mountElement(n2, container, anchor);
} else {
// todo
updateElement(n1, n2, container, anchor, parentComponent);
}
}
function updateElement(n1, n2, container, anchor, parentComponent) {
const oldProps = (n1 && n1.props) || {};
const newProps = n2.props || {};
// 应该更新 element
console.log("应该更新 element");
console.log("旧的 vnode", n1);
console.log("新的 vnode", n2);
// 需要把 el 挂载到新的 vnode
const el = (n2.el = n1.el);
// 对比 props
patchProps(el, oldProps, newProps);
// 对比 children
patchChildren(n1, n2, el, anchor, parentComponent);
}
function patchProps(el, oldProps, newProps) {
// 对比 props 有以下几种情况
// 1. oldProps 有,newProps 也有,但是 val 值变更了
// 举个栗子
// 之前: oldProps.id = 1 ,更新后:newProps.id = 2
// key 存在 oldProps 里 也存在 newProps 内
// 以 newProps 作为基准
for (const key in newProps) {
const prevProp = oldProps[key];
const nextProp = newProps[key];
if (prevProp !== nextProp) {
// 对比属性
// 需要交给 host 来更新 key
hostPatchProp(el, key, prevProp, nextProp);
}
}
// 2. oldProps 有,而 newProps 没有了
// 之前: {id:1,tId:2} 更新后: {id:1}
// 这种情况下我们就应该以 oldProps 作为基准,因为在 newProps 里面是没有的 tId 的
// 还需要注意一点,如果这个 key 在 newProps 里面已经存在了,说明已经处理过了,就不要在处理了
for (const key in oldProps) {
const prevProp = oldProps[key];
const nextProp = null;
if (!(key in newProps)) {
// 这里是以 oldProps 为基准来遍历,
// 而且得到的值是 newProps 内没有的
// 所以交给 host 更新的时候,把新的值设置为 null
hostPatchProp(el, key, prevProp, nextProp);
}
}
}
function patchChildren(n1, n2, container, anchor, parentComponent) {
const { shapeFlag: prevShapeFlag, children: c1 } = n1;
const { shapeFlag, children: c2 } = n2;
// 如果 n2 的 children 是 text 类型的话
// 就看看和之前的 n1 的 children 是不是一样的
// 如果不一样的话直接重新设置一下 text 即可
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (c2 !== c1) {
console.log("类型为 text_children, 当前需要更新");
hostSetElementText(container, c2 as string);
}
} else {
// 看看之前的是不是 text
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 先清空
// 然后在把新的 children 给 mount 生成 element
hostSetElementText(container, "");
mountChildren(c2, container);
} else {
// array diff array
// 如果之前是 array_children
// 现在还是 array_children 的话
// 那么我们就需要对比两个 children 啦
patchKeyedChildren(c1, c2, container, parentComponent, anchor);
}
}
}
function patchKeyedChildren(
c1: any[],
c2: any[],
container,
parentAnchor,
parentComponent
) {
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1;
let e2 = l2 - 1;
const isSameVNodeType = (n1, n2) => {
return n1.type === n2.type && n1.key === n2.key;
};
while (i <= e1 && i <= e2) {
const prevChild = c1[i];
const nextChild = c2[i];
if (!isSameVNodeType(prevChild, nextChild)) {
console.log("两个 child 不相等(从左往右比对)");
console.log(`prevChild:${prevChild}`);
console.log(`nextChild:${nextChild}`);
break;
}
console.log("两个 child 相等,接下来对比这两个 child 节点(从左往右比对)");
patch(prevChild, nextChild, container, parentAnchor, parentComponent);
i++;
}
while (i <= e1 && i <= e2) {
// 从右向左取值
const prevChild = c1[e1];
const nextChild = c2[e2];
if (!isSameVNodeType(prevChild, nextChild)) {
console.log("两个 child 不相等(从右往左比对)");
console.log(`prevChild:${prevChild}`);
console.log(`nextChild:${nextChild}`);
break;
}
console.log("两个 child 相等,接下来对比这两个 child 节点(从右往左比对)");
patch(prevChild, nextChild, container, parentAnchor, parentComponent);
e1--;
e2--;
}
if (i > e1 && i <= e2) {
// 如果是这种情况的话就说明 e2 也就是新节点的数量大于旧节点的数量
// 也就是说新增了 vnode
// 应该循环 c2
// 锚点的计算:新的节点有可能需要添加到尾部,也可能添加到头部,所以需要指定添加的问题
// 要添加的位置是当前的位置(e2 开始)+1
// 因为对于往左侧添加的话,应该获取到 c2 的第一个元素
// 所以我们需要从 e2 + 1 取到锚点的位置
const nextPos = e2 + 1;
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
while (i <= e2) {
console.log(`需要新创建一个 vnode: ${c2[i].key}`);
patch(null, c2[i], container, anchor, parentComponent);
i++;
}
} else if (i > e2 && i <= e1) {
// 这种情况的话说明新节点的数量是小于旧节点的数量的
// 那么我们就需要把多余的
while (i <= e1) {
console.log(`需要删除当前的 vnode: ${c1[i].key}`);
hostRemove(c1[i].el);
i++;
}
} else {
// 左右两边都比对完了,然后剩下的就是中间部位顺序变动的
// 例如下面的情况
// a,b,[c,d,e],f,g
// a,b,[e,c,d],f,g
let s1 = i;
let s2 = i;
const keyToNewIndexMap = new Map();
let moved = false;
let maxNewIndexSoFar = 0;
// 先把 key 和 newIndex 绑定好,方便后续基于 key 找到 newIndex
// 时间复杂度是 O(1)
for (let i = s2; i <= e2; i++) {
const nextChild = c2[i];
keyToNewIndexMap.set(nextChild.key, i);
}
// 需要处理新节点的数量
const toBePatched = e2 - s2 + 1;
let patched = 0;
// 初始化 从新的index映射为老的index
// 创建数组的时候给定数组的长度,这个是性能最快的写法
const newIndexToOldIndexMap = new Array(toBePatched);
// 初始化为 0 , 后面处理的时候 如果发现是 0 的话,那么就说明新值在老的里面不存在
for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0;
// 遍历老节点
// 1. 需要找出老节点有,而新节点没有的 -> 需要把这个节点删除掉
// 2. 新老节点都有的,—> 需要 patch
for (i = s1; i <= e1; i++) {
const prevChild = c1[i];
// 优化点
// 如果老的节点大于新节点的数量的话,那么这里在处理老节点的时候就直接删除即可
if (patched >= toBePatched) {
hostRemove(prevChild.el);
continue;
}
let newIndex;
if (prevChild.key != null) {
// 这里就可以通过key快速的查找了, 看看在新的里面这个节点存在不存在
// 时间复杂度O(1)
newIndex = keyToNewIndexMap.get(prevChild.key);
} else {
// 如果没key 的话,那么只能是遍历所有的新节点来确定当前节点存在不存在了
// 时间复杂度O(n)
for (let j = s2; j <= e2; j++) {
if (isSameVNodeType(prevChild, c2[j])) {
newIndex = j;
break;
}
}
}
// 因为有可能 nextIndex 的值为0(0也是正常值)
// 所以需要通过值是不是 undefined 或者 null 来判断
if (newIndex === undefined) {
// 当前节点的key 不存在于 newChildren 中,需要把当前节点给删除掉
hostRemove(prevChild.el);
} else {
// 新老节点都存在
console.log("新老节点都存在");
// 把新节点的索引和老的节点的索引建立映射关系
// i + 1 是因为 i 有可能是0 (0 的话会被认为新节点在老的节点中不存在)
newIndexToOldIndexMap[newIndex - s2] = i + 1;
// 来确定中间的节点是不是需要移动
// 新的 newIndex 如果一直是升序的话,那么就说明没有移动
// 所以我们可以记录最后一个节点在新的里面的索引,然后看看是不是升序
// 不是升序的话,我们就可以确定节点移动过了
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
} else {
moved = true;
}
patch(prevChild, c2[newIndex], container, null, parentComponent);
patched++;
}
}
// 利用最长递增子序列来优化移动逻辑
// 因为元素是升序的话,那么这些元素就是不需要移动的
// 而我们就可以通过最长递增子序列来获取到升序的列表
// 在移动的时候我们去对比这个列表,如果对比上的话,就说明当前元素不需要移动
// 通过 moved 来进行优化,如果没有移动过的话 那么就不需要执行算法
// getSequence 返回的是 newIndexToOldIndexMap 的索引值
// 所以后面我们可以直接遍历索引值来处理,也就是直接使用 toBePatched 即可
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: [];
let j = increasingNewIndexSequence.length - 1;
// 遍历新节点
// 1. 需要找出老节点没有,而新节点有的 -> 需要把这个节点创建
// 2. 最后需要移动一下位置,比如 [c,d,e] -> [e,c,d]
// 这里倒循环是因为在 insert 的时候,需要保证锚点是处理完的节点(也就是已经确定位置了)
// 因为 insert 逻辑是使用的 insertBefore()
for (let i = toBePatched - 1; i >= 0; i--) {
// 确定当前要处理的节点索引
const nextIndex = s2 + i;
const nextChild = c2[nextIndex];
// 锚点等于当前节点索引+1
// 也就是当前节点的后面一个节点(又因为是倒遍历,所以锚点是位置确定的节点)
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
if (newIndexToOldIndexMap[i] === 0) {
// 说明新节点在老的里面不存在
// 需要创建
patch(null, nextChild, container, anchor, parentComponent);
} else if (moved) {
// 需要移动
// 1. j 已经没有了 说明剩下的都需要移动了
// 2. 最长子序列里面的值和当前的值匹配不上, 说明当前元素需要移动
if (j < 0 || increasingNewIndexSequence[j] !== i) {
// 移动的话使用 insert 即可
hostInsert(nextChild.el, container, anchor);
} else {
// 这里就是命中了 index 和 最长递增子序列的值
// 所以可以移动指针了
j--;
}
}
}
}
}
function mountElement(vnode, container, anchor) {
const { shapeFlag, props } = vnode;
// 1. 先创建 element
// 基于可扩展的渲染 api
const el = (vnode.el = hostCreateElement(vnode.type));
// 支持单子组件和多子组件的创建
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 举个栗子
// render(){
// return h("div",{},"test")
// }
// 这里 children 就是 test ,只需要渲染一下就完事了
console.log(`处理文本:${vnode.children}`);
hostSetElementText(el, vnode.children);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 举个栗子
// render(){
// Hello 是个 component
// return h("div",{},[h("p"),h(Hello)])
// }
// 这里 children 就是个数组了,就需要依次调用 patch 递归来处理
mountChildren(vnode.children, el);
}
// 处理 props
if (props) {
for (const key in props) {
// todo
// 需要过滤掉vue自身用的key
// 比如生命周期相关的 key: beforeMount、mounted
const nextVal = props[key];
hostPatchProp(el, key, null, nextVal);
}
}
// todo
// 触发 beforeMount() 钩子
console.log("vnodeHook -> onVnodeBeforeMount");
console.log("DirectiveHook -> beforeMount");
console.log("transition -> beforeEnter");
// 插入
hostInsert(el, container, anchor);
// todo
// 触发 mounted() 钩子
console.log("vnodeHook -> onVnodeMounted");
console.log("DirectiveHook -> mounted");
console.log("transition -> enter");
}
function mountChildren(children, container) {
children.forEach((VNodeChild) => {
// todo
// 这里应该需要处理一下 vnodeChild
// 因为有可能不是 vnode 类型
console.log("mountChildren:", VNodeChild);
patch(null, VNodeChild, container);
});
}
function processComponent(n1, n2, container, parentComponent) {
// 如果 n1 没有值的话,那么就是 mount
if (!n1) {
// 初始化 component
mountComponent(n2, container, parentComponent);
} else {
updateComponent(n1, n2, container);
}
}
// 组件的更新
function updateComponent(n1, n2, container) {
console.log("更新组件", n1, n2);
// 更新组件实例引用
const instance = (n2.component = n1.component);
// 先看看这个组件是否应该更新
if (shouldUpdateComponent(n1, n2)) {
console.log(`组件需要更新: ${instance}`);
// 那么 next 就是新的 vnode 了(也就是 n2)
instance.next = n2;
// 这里的 update 是在 setupRenderEffect 里面初始化的,update 函数除了当内部的响应式对象发生改变的时候会调用
// 还可以直接主动的调用(这是属于 effect 的特性)
// 调用 update 再次更新调用 patch 逻辑
// 在update 中调用的 next 就变成了 n2了
// ps:可以详细的看看 update 中 next 的应用
// TODO 需要在 update 中处理支持 next 的逻辑
instance.update();
} else {
console.log(`组件不需要更新: ${instance}`);
// 不需要更新的话,那么只需要覆盖下面的属性即可
n2.component = n1.component;
n2.el = n1.el;
instance.vnode = n2;
}
}
function mountComponent(initialVNode, container, parentComponent) {
// 1. 先创建一个 component instance
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent
));
console.log(`创建组件实例:${instance.type.name}`);
// 2. 给 instance 加工加工
setupComponent(instance);
setupRenderEffect(instance, initialVNode, container);
}
function setupRenderEffect(instance, initialVNode, container) {
// 调用 render
// 应该传入 ctx 也就是 proxy
// ctx 可以选择暴露给用户的 api
// 源代码里面是调用的 renderComponentRoot 函数
// 这里为了简化直接调用 render
// obj.name = "111"
// obj.name = "2222"
// 从哪里做一些事
// 收集数据改变之后要做的事 (函数)
// 依赖收集 effect 函数
// 触发依赖
function componentUpdateFn() {
if (!instance.isMounted) {
// 组件初始化的时候会执行这里
// 为什么要在这里调用 render 函数呢
// 是因为在 effect 内调用 render 才能触发依赖收集
// 等到后面响应式的值变更后会再次触发这个函数
console.log(`${instance.type.name}:调用 render,获取 subTree`);
const proxyToUse = instance.proxy;
// 可在 render 函数中通过 this 来使用 proxy
const subTree = (instance.subTree = normalizeVNode(
instance.render.call(proxyToUse, proxyToUse)
));
console.log("subTree", subTree);
// todo
console.log(`${instance.type.name}:触发 beforeMount hook`);
console.log(`${instance.type.name}:触发 onVnodeBeforeMount hook`);
// 这里基于 subTree 再次调用 patch
// 基于 render 返回的 vnode ,再次进行渲染
// 这里我把这个行为隐喻成开箱
// 一个组件就是一个箱子
// 里面有可能是 element (也就是可以直接渲染的)
// 也有可能还是 component
// 这里就是递归的开箱
// 而 subTree 就是当前的这个箱子(组件)装的东西
// 箱子(组件)只是个概念,它实际是不需要渲染的
// 要渲染的是箱子里面的 subTree
patch(null, subTree, container, null, instance);
// 把 root element 赋值给 组件的vnode.el ,为后续调用 $el 的时候获取值
initialVNode.el = subTree.el;
console.log(`${instance.type.name}:触发 mounted hook`);
instance.isMounted = true;
} else {
// 响应式的值变更后会从这里执行逻辑
// 主要就是拿到新的 vnode ,然后和之前的 vnode 进行对比
console.log(`${instance.type.name}:调用更新逻辑`);
// 拿到最新的 subTree
const { next, vnode } = instance;
// 如果有 next 的话, 说明需要更新组件的数据(props,slots 等)
// 先更新组件的数据,然后更新完成后,在继续对比当前组件的子元素
if (next) {
// 问题是 next 和 vnode 的区别是什么
next.el = vnode.el;
updateComponentPreRender(instance, next);
}
const proxyToUse = instance.proxy;
const nextTree = normalizeVNode(
instance.render.call(proxyToUse, proxyToUse)
);
// 替换之前的 subTree
const prevTree = instance.subTree;
instance.subTree = nextTree;
// 触发 beforeUpdated hook
console.log(`${instance.type.name}:触发 beforeUpdated hook`);
console.log(`${instance.type.name}:触发 onVnodeBeforeUpdate hook`);
// 用旧的 vnode 和新的 vnode 交给 patch 来处理
patch(prevTree, nextTree, prevTree.el, null, instance);
// 触发 updated hook
console.log(`${instance.type.name}:触发 updated hook`);
console.log(`${instance.type.name}:触发 onVnodeUpdated hook`);
}
}
// 在 vue3.2 版本里面是使用的 new ReactiveEffect
// 至于为什么不直接用 effect ,是因为需要一个 scope 参数来收集所有的 effect
// 而 effect 这个函数是对外的 api ,是不可以轻易改变参数的,所以会使用 new ReactiveEffect
// 因为 ReactiveEffect 是内部对象,加一个参数是无所谓的
// 后面如果要实现 scope 的逻辑的时候 需要改过来
// 现在就先算了
instance.update = effect(componentUpdateFn, {
scheduler: () => {
// 把 effect 推到微任务的时候在执行
// queueJob(effect);
queueJob(instance.update);
},
});
}
function updateComponentPreRender(instance, nextVNode) {
// 更新 nextVNode 的组件实例
// 现在 instance.vnode 是组件实例更新前的
// 所以之前的 props 就是基于 instance.vnode.props 来获取
// 接着需要更新 vnode ,方便下一次更新的时候获取到正确的值
nextVNode.component = instance;
// TODO 后面更新 props 的时候需要对比
// const prevProps = instance.vnode.props;
instance.vnode = nextVNode;
instance.next = null;
const { props } = nextVNode;
console.log("更新组件的 props", props);
instance.props = props;
console.log("更新组件的 slots");
// TODO 更新组件的 slots
// 需要重置 vnode
}
return {
render,
createApp: createAppAPI(render),
};
}
function getSequence(arr: number[]): number[] {
const p = arr.slice();
const result = [0];
let i, j, u, v, c;
const len = arr.length;
for (i = 0; i < len; i++) {
const arrI = arr[i];
if (arrI !== 0) {
j = result[result.length - 1];
if (arr[j] < arrI) {
p[i] = j;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
while (u < v) {
c = (u + v) >> 1;
if (arr[result[c]] < arrI) {
u = c + 1;
} else {
v = c;
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}
================================================
FILE: packages/runtime-core/src/scheduler.ts
================================================
const queue: any[] = [];
const activePreFlushCbs: any = [];
const p = Promise.resolve();
let isFlushPending = false;
export function nextTick(fn?) {
return fn ? p.then(fn) : p;
}
export function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job);
// 执行所有的 job
queueFlush();
}
}
function queueFlush() {
// 如果同时触发了两个组件的更新的话
// 这里就会触发两次 then (微任务逻辑)
// 但是着是没有必要的
// 我们只需要触发一次即可处理完所有的 job 调用
// 所以需要判断一下 如果已经触发过 nextTick 了
// 那么后面就不需要再次触发一次 nextTick 逻辑了
if (isFlushPending) return;
isFlushPending = true;
nextTick(flushJobs);
}
export function queuePreFlushCb(cb) {
queueCb(cb, activePreFlushCbs);
}
function queueCb(cb, activeQueue) {
// 直接添加到对应的列表内就ok
// todo 这里没有考虑 activeQueue 是否已经存在 cb 的情况
// 然后在执行 flushJobs 的时候就可以调用 activeQueue 了
activeQueue.push(cb);
// 然后执行队列里面所有的 job
queueFlush()
}
function flushJobs() {
isFlushPending = false;
// 先执行 pre 类型的 job
// 所以这里执行的job 是在渲染前的
// 也就意味着执行这里的 job 的时候 页面还没有渲染
flushPreFlushCbs();
// 这里是执行 queueJob 的
// 比如 render 渲染就是属于这个类型的 job
let job;
while ((job = queue.shift())) {
if (job) {
job();
}
}
}
function flushPreFlushCbs() {
// 执行所有的 pre 类型的 job
for (let i = 0; i < activePreFlushCbs.length; i++) {
activePreFlushCbs[i]();
}
}
================================================
FILE: packages/runtime-core/src/vnode.ts
================================================
import { ShapeFlags } from "@mini-vue/shared";
export { createVNode as createElementVNode }
export const createVNode = function (
type: any,
props?: any,
children?: string | Array
) {
// 注意 type 有可能是 string 也有可能是对象
// 如果是对象的话,那么就是用户设置的 options
// type 为 string 的时候
// createVNode("div")
// type 为组件对象的时候
// createVNode(App)
const vnode = {
el: null,
component: null,
key: props?.key,
type,
props: props || {},
children,
shapeFlag: getShapeFlag(type),
};
// 基于 children 再次设置 shapeFlag
if (Array.isArray(children)) {
vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
} else if (typeof children === "string") {
vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
}
normalizeChildren(vnode, children);
return vnode;
};
export function normalizeChildren(vnode, children) {
if (typeof children === "object") {
// 暂时主要是为了标识出 slots_children 这个类型来
// 暂时我们只有 element 类型和 component 类型的组件
// 所以我们这里除了 element ,那么只要是 component 的话,那么children 肯定就是 slots 了
if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
// 如果是 element 类型的话,那么 children 肯定不是 slots
} else {
// 这里就必然是 component 了,
vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN;
}
}
}
// 用 symbol 作为唯一标识
export const Text = Symbol("Text");
export const Fragment = Symbol("Fragment");
/**
* @private
*/
export function createTextVNode(text: string = " ") {
return createVNode(Text, {}, text);
}
// 标准化 vnode 的格式
// 其目的是为了让 child 支持多种格式
export function normalizeVNode(child) {
// 暂时只支持处理 child 为 string 和 number 的情况
if (typeof child === "string" || typeof child === "number") {
return createVNode(Text, null, String(child));
} else {
return child;
}
}
// 基于 type 来判断是什么类型的组件
function getShapeFlag(type: any) {
return typeof type === "string"
? ShapeFlags.ELEMENT
: ShapeFlags.STATEFUL_COMPONENT;
}
================================================
FILE: packages/runtime-dom/package.json
================================================
{
"name": "@mini-vue/runtime-dom",
"version": "1.0.0",
"description": "@mini-vue/runtime-dom",
"module": "dist/shared.esm-bundler.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@mini-vue/runtime-core": "workspace:^1.0.0",
"@mini-vue/shared": "workspace:^1.0.0"
}
}
================================================
FILE: packages/runtime-dom/src/index.ts
================================================
// 源码里面这些接口是由 runtime-dom 来实现
// 这里先简单实现
import { isOn } from "@mini-vue/shared";
import { createRenderer } from "@mini-vue/runtime-core";
// 后面也修改成和源码一样的实现
function createElement(type) {
console.log("CreateElement", type);
const element = document.createElement(type);
return element;
}
function createText(text) {
return document.createTextNode(text);
}
function setText(node, text) {
node.nodeValue = text;
}
function setElementText(el, text) {
console.log("SetElementText", el, text);
el.textContent = text;
}
function patchProp(el, key, preValue, nextValue) {
// preValue 之前的值
// 为了之后 update 做准备的值
// nextValue 当前的值
console.log(`PatchProp 设置属性:${key} 值:${nextValue}`);
console.log(`key: ${key} 之前的值是:${preValue}`);
if (isOn(key)) {
// 添加事件处理函数的时候需要注意一下
// 1. 添加的和删除的必须是一个函数,不然的话 删除不掉
// 那么就需要把之前 add 的函数给存起来,后面删除的时候需要用到
// 2. nextValue 有可能是匿名函数,当对比发现不一样的时候也可以通过缓存的机制来避免注册多次
// 存储所有的事件函数
const invokers = el._vei || (el._vei = {});
const existingInvoker = invokers[key];
if (nextValue && existingInvoker) {
// patch
// 直接修改函数的值即可
existingInvoker.value = nextValue;
} else {
const eventName = key.slice(2).toLowerCase();
if (nextValue) {
const invoker = (invokers[key] = nextValue);
el.addEventListener(eventName, invoker);
} else {
el.removeEventListener(eventName, existingInvoker);
invokers[key] = undefined;
}
}
} else {
if (nextValue === null || nextValue === "") {
el.removeAttribute(key);
} else {
el.setAttribute(key, nextValue);
}
}
}
function insert(child, parent, anchor = null) {
console.log("Insert");
parent.insertBefore(child, anchor);
}
function remove(child) {
const parent = child.parentNode;
if (parent) {
parent.removeChild(child);
}
}
let renderer;
function ensureRenderer() {
// 如果 renderer 有值的话,那么以后都不会初始化了
return (
renderer ||
(renderer = createRenderer({
createElement,
createText,
setText,
setElementText,
patchProp,
insert,
remove,
}))
);
}
export const createApp = (...args) => {
return ensureRenderer().createApp(...args);
};
export * from "@mini-vue/runtime-core"
================================================
FILE: packages/runtime-test/src/index.ts
================================================
// todo
// 实现 render 的渲染接口
// 实现序列化
import { createRenderer } from "@mini-vue/runtime-core";
import { extend } from "@mini-vue/shared";
import { nodeOps } from "./nodeOps";
import { patchProp } from "./patchProp";
export const { render } = createRenderer(extend({ patchProp }, nodeOps));
export * from "./nodeOps";
export * from "./serialize"
export * from '@mini-vue/runtime-core'
================================================
FILE: packages/runtime-test/src/nodeOps.ts
================================================
export const enum NodeTypes {
ELEMENT = "element",
TEXT = "TEXT",
}
let nodeId = 0;
// 这个函数会在 runtime-core 初始化 element 的时候调用
function createElement(tag: string) {
// 如果是基于 dom 的话 那么这里会返回 dom 元素
// 这里是为了测试 所以只需要反正一个对象就可以了
// 后面的话 通过这个对象来做测试
const node = {
tag,
id: nodeId++,
type: NodeTypes.ELEMENT,
props: {},
children: [],
parentNode: null,
};
return node;
}
function insert(child, parent) {
parent.children.push(child);
child.parentNode = parent;
}
function parentNode(node) {
return node.parentNode;
}
function setElementText(el, text) {
el.children = [
{
id: nodeId++,
type: NodeTypes.TEXT,
text,
parentNode: el,
},
];
}
export const nodeOps = { createElement, insert, parentNode, setElementText };
================================================
FILE: packages/runtime-test/src/patchProp.ts
================================================
export function patchProp(el, key, prevValue, nextValue) {
el.props[key] = nextValue;
}
================================================
FILE: packages/runtime-test/src/serialize.ts
================================================
// 把 node 给序列化
// 测试的时候好对比
import { NodeTypes } from "./nodeOps";
// 序列化: 把一个对象给处理成 string (进行流化)
export function serialize(node) {
if (node.type === NodeTypes.ELEMENT) {
return serializeElement(node);
} else {
return serializeText(node);
}
}
function serializeText(node) {
return node.text;
}
export function serializeInner(node) {
// 把所有节点变成一个string
return node.children.map((c) => serialize(c)).join(``);
}
function serializeElement(node) {
// 把 props 处理成字符串
// 规则:
// 如果 value 是 null 的话 那么直接返回 ``
// 如果 value 是 `` 的话,那么返回 key
// 不然的话返回 key = value(这里的值需要字符串化)
const props = Object.keys(node.props)
.map((key) => {
const value = node.props[key];
return value == null
? ``
: value === ``
? key
: `${key}=${JSON.stringify(value)}`;
})
.filter(Boolean)
.join(" ");
console.log("node---------", node.children);
return `<${node.tag}${props ? ` ${props}` : ``}>${serializeInner(node)}${
node.tag
}>`;
}
================================================
FILE: packages/shared/package.json
================================================
{
"name": "@mini-vue/shared",
"version": "1.0.0",
"description": "@mini-vue/shared",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
================================================
FILE: packages/shared/src/index.ts
================================================
export * from "../src/shapeFlags";
export * from "../src/toDisplayString";
export const isObject = (val) => {
return val !== null && typeof val === "object";
};
export const isString = (val) => typeof val === "string";
const camelizeRE = /-(\w)/g;
/**
* @private
* 把烤肉串命名方式转换成驼峰命名方式
*/
export const camelize = (str: string): string => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ""));
};
export const extend = Object.assign;
// 必须是 on+一个大写字母的格式开头
export const isOn = (key) => /^on[A-Z]/.test(key);
export function hasChanged(value, oldValue) {
return !Object.is(value, oldValue);
}
export function hasOwn(val, key) {
return Object.prototype.hasOwnProperty.call(val, key);
}
/**
* @private
* 首字母大写
*/
export const capitalize = (str: string) =>
str.charAt(0).toUpperCase() + str.slice(1);
/**
* @private
* 添加 on 前缀,并且首字母大写
*/
export const toHandlerKey = (str: string) =>
str ? `on${capitalize(str)}` : ``;
// 用来匹配 kebab-case 的情况
// 比如 onTest-event 可以匹配到 T
// 然后取到 T 在前面加一个 - 就可以
// \BT 就可以匹配到 T 前面是字母的位置
const hyphenateRE = /\B([A-Z])/g;
/**
* @private
*/
export const hyphenate = (str: string) =>
str.replace(hyphenateRE, "-$1").toLowerCase();
================================================
FILE: packages/shared/src/shapeFlags.ts
================================================
// 组件的类型
export const enum ShapeFlags {
// 最后要渲染的 element 类型
ELEMENT = 1,
// 组件类型
STATEFUL_COMPONENT = 1 << 2,
// vnode 的 children 为 string 类型
TEXT_CHILDREN = 1 << 3,
// vnode 的 children 为数组类型
ARRAY_CHILDREN = 1 << 4,
// vnode 的 children 为 slots 类型
SLOTS_CHILDREN = 1 << 5
}
================================================
FILE: packages/shared/src/toDisplayString.ts
================================================
export const toDisplayString = (val) => {
return String(val);
};
================================================
FILE: packages/vue/cypress/e2e/apiInject.cy.js
================================================
describe("apiInject", () => {
it("render", () => {
cy.visit("http://localhost:3000/example/apiInject/");
cy.contains("apiInject")
cy.contains("fooOverride-bar-baz")
});
});
================================================
FILE: packages/vue/cypress/e2e/componentEmit.cy.js
================================================
describe("componentEmit", () => {
it("render", () => {
cy.visit("http://localhost:3000/example/componentEmit/");
cy.contains("你好")
cy.contains("child")
});
});
================================================
FILE: packages/vue/cypress/e2e/componentSlots.cy.js
================================================
describe("componentSlots", () => {
it("render", () => {
cy.visit("http://localhost:3000/example/componentSlots/");
cy.contains("你好");
cy.get("[data-test='child']").within(() => {
cy.contains("child");
cy.contains("我是通过 slot 渲染出来的第一个元素");
cy.contains("我是通过 slot 渲染出来的第一个元素");
cy.contains("我可以接收到");
cy.contains("age: 16");
});
});
});
================================================
FILE: packages/vue/cypress/e2e/componentUpdate.cy.js
================================================
describe("componentUpdate", () => {
it("render", () => {
cy.visit("http://localhost:3000/example/componentUpdate/");
cy.contains("child123")
cy.get("button").click()
cy.contains("child456")
});
});
================================================
FILE: packages/vue/cypress/e2e/customRenderer.cy.js
================================================
describe("customRenderer", () => {
it("render", () => {
cy.visit("http://localhost:3000/example/customRenderer/");
cy.get("canvas").should("exist")
});
});
================================================
FILE: packages/vue/cypress/e2e/getCurrentInstance.cy.js
================================================
describe("currentInstance", () => {
it("render", () => {
cy.visit("http://localhost:3000/example/getCurrentInstance/");
cy.contains("getCurrentInstance")
});
});
================================================
FILE: packages/vue/cypress/e2e/helloworld.cy.js
================================================
describe("helloworld", () => {
it("render", () => {
cy.visit("http://localhost:3000/example/helloWorld/");
cy.contains("主页");
cy.contains("hello world:");
cy.contains("count: 0");
});
});
================================================
FILE: packages/vue/cypress/e2e/nextTicker.cy.js
================================================
describe("nextTicker", () => {
it("render", () => {
cy.visit("http://localhost:3000/example/nextTicker/");
cy.contains("主页")
cy.contains("child1")
cy.contains("count:1")
cy.contains("child2")
cy.contains("count:1")
});
});
================================================
FILE: packages/vue/cypress/e2e/patchChildren.cy.js
================================================
describe("patchChildren", () => {
it("render", () => {
cy.visit("http://localhost:3000/example/patchChildren/");
cy.get("[data-cy='contain']").should("text", "ABCEFG");
cy.get("button").click();
cy.get("[data-cy='contain']").should("text", "ABECDFG");
cy.get("button").click();
cy.get("[data-cy='contain']").should("text", "ABCEFG");
});
});
================================================
FILE: packages/vue/cypress/fixtures/example.json
================================================
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
================================================
FILE: packages/vue/cypress/support/commands.js
================================================
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
================================================
FILE: packages/vue/cypress/support/e2e.js
================================================
// ***********************************************************
// This example support/e2e.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
================================================
FILE: packages/vue/cypress.config.js
================================================
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
video:false
}
})
================================================
FILE: packages/vue/dist/mini-vue.cjs.js
================================================
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var ShapeFlags;
(function (ShapeFlags) {
ShapeFlags[ShapeFlags["ELEMENT"] = 1] = "ELEMENT";
ShapeFlags[ShapeFlags["STATEFUL_COMPONENT"] = 4] = "STATEFUL_COMPONENT";
ShapeFlags[ShapeFlags["TEXT_CHILDREN"] = 8] = "TEXT_CHILDREN";
ShapeFlags[ShapeFlags["ARRAY_CHILDREN"] = 16] = "ARRAY_CHILDREN";
ShapeFlags[ShapeFlags["SLOTS_CHILDREN"] = 32] = "SLOTS_CHILDREN";
})(ShapeFlags || (ShapeFlags = {}));
const toDisplayString = (val) => {
return String(val);
};
const isObject = (val) => {
return val !== null && typeof val === "object";
};
const isString = (val) => typeof val === "string";
const camelizeRE = /-(\w)/g;
const camelize = (str) => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ""));
};
const extend = Object.assign;
const isOn = (key) => /^on[A-Z]/.test(key);
function hasChanged(value, oldValue) {
return !Object.is(value, oldValue);
}
function hasOwn(val, key) {
return Object.prototype.hasOwnProperty.call(val, key);
}
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const toHandlerKey = (str) => str ? `on${capitalize(str)}` : ``;
const hyphenateRE = /\B([A-Z])/g;
const hyphenate = (str) => str.replace(hyphenateRE, "-$1").toLowerCase();
const createVNode = function (type, props, children) {
const vnode = {
el: null,
component: null,
key: props === null || props === void 0 ? void 0 : props.key,
type,
props: props || {},
children,
shapeFlag: getShapeFlag(type),
};
if (Array.isArray(children)) {
vnode.shapeFlag |= 16;
}
else if (typeof children === "string") {
vnode.shapeFlag |= 8;
}
normalizeChildren(vnode, children);
return vnode;
};
function normalizeChildren(vnode, children) {
if (typeof children === "object") {
if (vnode.shapeFlag & 1) ;
else {
vnode.shapeFlag |= 32;
}
}
}
const Text = Symbol("Text");
const Fragment = Symbol("Fragment");
function createTextVNode(text = " ") {
return createVNode(Text, {}, text);
}
function normalizeVNode(child) {
if (typeof child === "string" || typeof child === "number") {
return createVNode(Text, null, String(child));
}
else {
return child;
}
}
function getShapeFlag(type) {
return typeof type === "string"
? 1
: 4;
}
const h = (type, props = null, children = []) => {
return createVNode(type, props, children);
};
function createAppAPI(render) {
return function createApp(rootComponent) {
const app = {
_component: rootComponent,
mount(rootContainer) {
console.log("基于根组件创建 vnode");
const vnode = createVNode(rootComponent);
console.log("调用 render,基于 vnode 进行开箱");
render(vnode, rootContainer);
},
};
return app;
};
}
function initProps(instance, rawProps) {
console.log("initProps");
instance.props = rawProps;
}
function initSlots(instance, children) {
const { vnode } = instance;
console.log("初始化 slots");
if (vnode.shapeFlag & 32) {
normalizeObjectSlots(children, (instance.slots = {}));
}
}
const normalizeSlotValue = (value) => {
return Array.isArray(value) ? value : [value];
};
const normalizeObjectSlots = (rawSlots, slots) => {
for (const key in rawSlots) {
const value = rawSlots[key];
if (typeof value === "function") {
slots[key] = (props) => normalizeSlotValue(value(props));
}
}
};
function emit(instance, event, ...rawArgs) {
const props = instance.props;
let handler = props[toHandlerKey(camelize(event))];
if (!handler) {
handler = props[(toHandlerKey(hyphenate(event)))];
}
if (handler) {
handler(...rawArgs);
}
}
const publicPropertiesMap = {
$el: (i) => i.vnode.el,
$emit: (i) => i.emit,
$slots: (i) => i.slots,
$props: (i) => i.props,
};
const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
const { setupState, props } = instance;
console.log(`触发 proxy hook , key -> : ${key}`);
if (key[0] !== "$") {
if (hasOwn(setupState, key)) {
return setupState[key];
}
else if (hasOwn(props, key)) {
return props[key];
}
}
const publicGetter = publicPropertiesMap[key];
if (publicGetter) {
return publicGetter(instance);
}
},
set({ _: instance }, key, value) {
const { setupState } = instance;
if (hasOwn(setupState, key)) {
setupState[key] = value;
}
return true;
},
};
function createDep(effects) {
const dep = new Set(effects);
return dep;
}
let activeEffect = void 0;
let shouldTrack = false;
const targetMap = new WeakMap();
class ReactiveEffect {
constructor(fn, scheduler) {
this.fn = fn;
this.scheduler = scheduler;
this.active = true;
this.deps = [];
console.log("创建 ReactiveEffect 对象");
}
run() {
console.log("run");
if (!this.active) {
return this.fn();
}
shouldTrack = true;
activeEffect = this;
console.log("执行用户传入的 fn");
const result = this.fn();
shouldTrack = false;
activeEffect = undefined;
return result;
}
stop() {
if (this.active) {
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
function cleanupEffect(effect) {
effect.deps.forEach((dep) => {
dep.delete(effect);
});
effect.deps.length = 0;
}
function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn);
extend(_effect, options);
_effect.run();
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
}
function stop(runner) {
runner.effect.stop();
}
function track(target, type, key) {
if (!isTracking()) {
return;
}
console.log(`触发 track -> target: ${target} type:${type} key:${key}`);
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = createDep();
depsMap.set(key, dep);
}
trackEffects(dep);
}
function trackEffects(dep) {
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
function trigger(target, type, key) {
let deps = [];
const depsMap = targetMap.get(target);
if (!depsMap)
return;
const dep = depsMap.get(key);
deps.push(dep);
const effects = [];
deps.forEach((dep) => {
effects.push(...dep);
});
triggerEffects(createDep(effects));
}
function isTracking() {
return shouldTrack && activeEffect !== undefined;
}
function triggerEffects(dep) {
for (const effect of dep) {
if (effect.scheduler) {
effect.scheduler();
}
else {
effect.run();
}
}
}
const get = createGetter();
const set = createSetter();
const readonlyGet = createGetter(true);
const shallowReadonlyGet = createGetter(true, true);
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
const isExistInReactiveMap = () => key === "__v_raw" && receiver === reactiveMap.get(target);
const isExistInReadonlyMap = () => key === "__v_raw" && receiver === readonlyMap.get(target);
const isExistInShallowReadonlyMap = () => key === "__v_raw" && receiver === shallowReadonlyMap.get(target);
if (key === "__v_isReactive") {
return !isReadonly;
}
else if (key === "__v_isReadonly") {
return isReadonly;
}
else if (isExistInReactiveMap() ||
isExistInReadonlyMap() ||
isExistInShallowReadonlyMap()) {
return target;
}
const res = Reflect.get(target, key, receiver);
if (!isReadonly) {
track(target, "get", key);
}
if (shallow) {
return res;
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
function createSetter() {
return function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, "set", key);
return result;
};
}
const readonlyHandlers = {
get: readonlyGet,
set(target, key) {
console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target);
return true;
},
};
const mutableHandlers = {
get,
set,
};
const shallowReadonlyHandlers = {
get: shallowReadonlyGet,
set(target, key) {
console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target);
return true;
},
};
const reactiveMap = new WeakMap();
const readonlyMap = new WeakMap();
const shallowReadonlyMap = new WeakMap();
var ReactiveFlags;
(function (ReactiveFlags) {
ReactiveFlags["IS_REACTIVE"] = "__v_isReactive";
ReactiveFlags["IS_READONLY"] = "__v_isReadonly";
ReactiveFlags["RAW"] = "__v_raw";
})(ReactiveFlags || (ReactiveFlags = {}));
function reactive(target) {
return createReactiveObject(target, reactiveMap, mutableHandlers);
}
function readonly(target) {
return createReactiveObject(target, readonlyMap, readonlyHandlers);
}
function shallowReadonly(target) {
return createReactiveObject(target, shallowReadonlyMap, shallowReadonlyHandlers);
}
function isProxy(value) {
return isReactive(value) || isReadonly(value);
}
function isReadonly(value) {
return !!value["__v_isReadonly"];
}
function isReactive(value) {
return !!value["__v_isReactive"];
}
function createReactiveObject(target, proxyMap, baseHandlers) {
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, baseHandlers);
proxyMap.set(target, proxy);
return proxy;
}
class RefImpl {
constructor(value) {
this.__v_isRef = true;
this._rawValue = value;
this._value = convert(value);
this.dep = createDep();
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newValue) {
if (hasChanged(newValue, this._rawValue)) {
this._value = convert(newValue);
this._rawValue = newValue;
triggerRefValue(this);
}
}
}
function ref(value) {
return createRef(value);
}
function convert(value) {
return isObject(value) ? reactive(value) : value;
}
function createRef(value) {
const refImpl = new RefImpl(value);
return refImpl;
}
function triggerRefValue(ref) {
triggerEffects(ref.dep);
}
function trackRefValue(ref) {
if (isTracking()) {
trackEffects(ref.dep);
}
}
const shallowUnwrapHandlers = {
get(target, key, receiver) {
return unRef(Reflect.get(target, key, receiver));
},
set(target, key, value, receiver) {
const oldValue = target[key];
if (isRef(oldValue) && !isRef(value)) {
return (target[key].value = value);
}
else {
return Reflect.set(target, key, value, receiver);
}
},
};
function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, shallowUnwrapHandlers);
}
function unRef(ref) {
return isRef(ref) ? ref.value : ref;
}
function isRef(value) {
return !!value.__v_isRef;
}
class ComputedRefImpl {
constructor(getter) {
this._dirty = true;
this.dep = createDep();
this.effect = new ReactiveEffect(getter, () => {
if (this._dirty)
return;
this._dirty = true;
triggerRefValue(this);
});
}
get value() {
trackRefValue(this);
if (this._dirty) {
this._dirty = false;
this._value = this.effect.run();
}
return this._value;
}
}
function computed(getter) {
return new ComputedRefImpl(getter);
}
function createComponentInstance(vnode, parent) {
const instance = {
type: vnode.type,
vnode,
next: null,
props: {},
parent,
provides: parent ? parent.provides : {},
proxy: null,
isMounted: false,
attrs: {},
slots: {},
ctx: {},
setupState: {},
emit: () => { },
};
instance.ctx = {
_: instance,
};
instance.emit = emit.bind(null, instance);
return instance;
}
function setupComponent(instance) {
const { props, children } = instance.vnode;
initProps(instance, props);
initSlots(instance, children);
setupStatefulComponent(instance);
}
function setupStatefulComponent(instance) {
console.log("创建 proxy");
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
const Component = instance.type;
const { setup } = Component;
if (setup) {
setCurrentInstance(instance);
const setupContext = createSetupContext(instance);
const setupResult = setup && setup(shallowReadonly(instance.props), setupContext);
setCurrentInstance(null);
handleSetupResult(instance, setupResult);
}
else {
finishComponentSetup(instance);
}
}
function createSetupContext(instance) {
console.log("初始化 setup context");
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit,
expose: () => { },
};
}
function handleSetupResult(instance, setupResult) {
if (typeof setupResult === "function") {
instance.render = setupResult;
}
else if (typeof setupResult === "object") {
instance.setupState = proxyRefs(setupResult);
}
finishComponentSetup(instance);
}
function finishComponentSetup(instance) {
const Component = instance.type;
if (!instance.render) {
if (compile && !Component.render) {
if (Component.template) {
const template = Component.template;
Component.render = compile(template);
}
}
instance.render = Component.render;
}
}
let currentInstance = {};
function getCurrentInstance() {
return currentInstance;
}
function setCurrentInstance(instance) {
currentInstance = instance;
}
let compile;
function registerRuntimeCompiler(_compile) {
compile = _compile;
}
function provide(key, value) {
var _a;
const currentInstance = getCurrentInstance();
if (currentInstance) {
let { provides } = currentInstance;
const parentProvides = (_a = currentInstance.parent) === null || _a === void 0 ? void 0 : _a.provides;
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides);
}
provides[key] = value;
}
}
function inject(key, defaultValue) {
var _a;
const currentInstance = getCurrentInstance();
if (currentInstance) {
const provides = (_a = currentInstance.parent) === null || _a === void 0 ? void 0 : _a.provides;
if (key in provides) {
return provides[key];
}
else if (defaultValue) {
if (typeof defaultValue === "function") {
return defaultValue();
}
return defaultValue;
}
}
}
function renderSlot(slots, name, props = {}) {
const slot = slots[name];
console.log(`渲染插槽 slot -> ${name}`);
if (slot) {
const slotContent = slot(props);
return createVNode(Fragment, {}, slotContent);
}
}
const queue = [];
const activePreFlushCbs = [];
const p = Promise.resolve();
let isFlushPending = false;
function nextTick(fn) {
return fn ? p.then(fn) : p;
}
function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job);
queueFlush();
}
}
function queueFlush() {
if (isFlushPending)
return;
isFlushPending = true;
nextTick(flushJobs);
}
function queuePreFlushCb(cb) {
queueCb(cb, activePreFlushCbs);
}
function queueCb(cb, activeQueue) {
activeQueue.push(cb);
queueFlush();
}
function flushJobs() {
isFlushPending = false;
flushPreFlushCbs();
let job;
while ((job = queue.shift())) {
if (job) {
job();
}
}
}
function flushPreFlushCbs() {
for (let i = 0; i < activePreFlushCbs.length; i++) {
activePreFlushCbs[i]();
}
}
function shouldUpdateComponent(prevVNode, nextVNode) {
const { props: prevProps } = prevVNode;
const { props: nextProps } = nextVNode;
if (prevProps === nextProps) {
return false;
}
if (!prevProps) {
return !!nextProps;
}
if (!nextProps) {
return true;
}
return hasPropsChanged(prevProps, nextProps);
}
function hasPropsChanged(prevProps, nextProps) {
const nextKeys = Object.keys(nextProps);
if (nextKeys.length !== Object.keys(prevProps).length) {
return true;
}
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i];
if (nextProps[key] !== prevProps[key]) {
return true;
}
}
return false;
}
function createRenderer(options) {
const { createElement: hostCreateElement, setElementText: hostSetElementText, patchProp: hostPatchProp, insert: hostInsert, remove: hostRemove, setText: hostSetText, createText: hostCreateText, } = options;
const render = (vnode, container) => {
console.log("调用 patch");
patch(null, vnode, container);
};
function patch(n1, n2, container = null, anchor = null, parentComponent = null) {
const { type, shapeFlag } = n2;
switch (type) {
case Text:
processText(n1, n2, container);
break;
case Fragment:
processFragment(n1, n2, container);
break;
default:
if (shapeFlag & 1) {
console.log("处理 element");
processElement(n1, n2, container, anchor, parentComponent);
}
else if (shapeFlag & 4) {
console.log("处理 component");
processComponent(n1, n2, container, parentComponent);
}
}
}
function processFragment(n1, n2, container) {
if (!n1) {
console.log("初始化 Fragment 类型的节点");
mountChildren(n2.children, container);
}
}
function processText(n1, n2, container) {
console.log("处理 Text 节点");
if (n1 === null) {
console.log("初始化 Text 类型的节点");
hostInsert((n2.el = hostCreateText(n2.children)), container);
}
else {
const el = (n2.el = n1.el);
if (n2.children !== n1.children) {
console.log("更新 Text 类型的节点");
hostSetText(el, n2.children);
}
}
}
function processElement(n1, n2, container, anchor, parentComponent) {
if (!n1) {
mountElement(n2, container, anchor);
}
else {
updateElement(n1, n2, container, anchor, parentComponent);
}
}
function updateElement(n1, n2, container, anchor, parentComponent) {
const oldProps = (n1 && n1.props) || {};
const newProps = n2.props || {};
console.log("应该更新 element");
console.log("旧的 vnode", n1);
console.log("新的 vnode", n2);
const el = (n2.el = n1.el);
patchProps(el, oldProps, newProps);
patchChildren(n1, n2, el, anchor, parentComponent);
}
function patchProps(el, oldProps, newProps) {
for (const key in newProps) {
const prevProp = oldProps[key];
const nextProp = newProps[key];
if (prevProp !== nextProp) {
hostPatchProp(el, key, prevProp, nextProp);
}
}
for (const key in oldProps) {
const prevProp = oldProps[key];
const nextProp = null;
if (!(key in newProps)) {
hostPatchProp(el, key, prevProp, nextProp);
}
}
}
function patchChildren(n1, n2, container, anchor, parentComponent) {
const { shapeFlag: prevShapeFlag, children: c1 } = n1;
const { shapeFlag, children: c2 } = n2;
if (shapeFlag & 8) {
if (c2 !== c1) {
console.log("类型为 text_children, 当前需要更新");
hostSetElementText(container, c2);
}
}
else {
if (prevShapeFlag & 8) {
hostSetElementText(container, "");
mountChildren(c2, container);
}
else {
patchKeyedChildren(c1, c2, container, parentComponent, anchor);
}
}
}
function patchKeyedChildren(c1, c2, container, parentAnchor, parentComponent) {
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1;
let e2 = l2 - 1;
const isSameVNodeType = (n1, n2) => {
return n1.type === n2.type && n1.key === n2.key;
};
while (i <= e1 && i <= e2) {
const prevChild = c1[i];
const nextChild = c2[i];
if (!isSameVNodeType(prevChild, nextChild)) {
console.log("两个 child 不相等(从左往右比对)");
console.log(`prevChild:${prevChild}`);
console.log(`nextChild:${nextChild}`);
break;
}
console.log("两个 child 相等,接下来对比这两个 child 节点(从左往右比对)");
patch(prevChild, nextChild, container, parentAnchor, parentComponent);
i++;
}
while (i <= e1 && i <= e2) {
const prevChild = c1[e1];
const nextChild = c2[e2];
if (!isSameVNodeType(prevChild, nextChild)) {
console.log("两个 child 不相等(从右往左比对)");
console.log(`prevChild:${prevChild}`);
console.log(`nextChild:${nextChild}`);
break;
}
console.log("两个 child 相等,接下来对比这两个 child 节点(从右往左比对)");
patch(prevChild, nextChild, container, parentAnchor, parentComponent);
e1--;
e2--;
}
if (i > e1 && i <= e2) {
const nextPos = e2 + 1;
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
while (i <= e2) {
console.log(`需要新创建一个 vnode: ${c2[i].key}`);
patch(null, c2[i], container, anchor, parentComponent);
i++;
}
}
else if (i > e2 && i <= e1) {
while (i <= e1) {
console.log(`需要删除当前的 vnode: ${c1[i].key}`);
hostRemove(c1[i].el);
i++;
}
}
else {
let s1 = i;
let s2 = i;
const keyToNewIndexMap = new Map();
let moved = false;
let maxNewIndexSoFar = 0;
for (let i = s2; i <= e2; i++) {
const nextChild = c2[i];
keyToNewIndexMap.set(nextChild.key, i);
}
const toBePatched = e2 - s2 + 1;
let patched = 0;
const newIndexToOldIndexMap = new Array(toBePatched);
for (let i = 0; i < toBePatched; i++)
newIndexToOldIndexMap[i] = 0;
for (i = s1; i <= e1; i++) {
const prevChild = c1[i];
if (patched >= toBePatched) {
hostRemove(prevChild.el);
continue;
}
let newIndex;
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key);
}
else {
for (let j = s2; j <= e2; j++) {
if (isSameVNodeType(prevChild, c2[j])) {
newIndex = j;
break;
}
}
}
if (newIndex === undefined) {
hostRemove(prevChild.el);
}
else {
console.log("新老节点都存在");
newIndexToOldIndexMap[newIndex - s2] = i + 1;
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
}
else {
moved = true;
}
patch(prevChild, c2[newIndex], container, null, parentComponent);
patched++;
}
}
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: [];
let j = increasingNewIndexSequence.length - 1;
for (let i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i;
const nextChild = c2[nextIndex];
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
if (newIndexToOldIndexMap[i] === 0) {
patch(null, nextChild, container, anchor, parentComponent);
}
else if (moved) {
if (j < 0 || increasingNewIndexSequence[j] !== i) {
hostInsert(nextChild.el, container, anchor);
}
else {
j--;
}
}
}
}
}
function mountElement(vnode, container, anchor) {
const { shapeFlag, props } = vnode;
const el = (vnode.el = hostCreateElement(vnode.type));
if (shapeFlag & 8) {
console.log(`处理文本:${vnode.children}`);
hostSetElementText(el, vnode.children);
}
else if (shapeFlag & 16) {
mountChildren(vnode.children, el);
}
if (props) {
for (const key in props) {
const nextVal = props[key];
hostPatchProp(el, key, null, nextVal);
}
}
console.log("vnodeHook -> onVnodeBeforeMount");
console.log("DirectiveHook -> beforeMount");
console.log("transition -> beforeEnter");
hostInsert(el, container, anchor);
console.log("vnodeHook -> onVnodeMounted");
console.log("DirectiveHook -> mounted");
console.log("transition -> enter");
}
function mountChildren(children, container) {
children.forEach((VNodeChild) => {
console.log("mountChildren:", VNodeChild);
patch(null, VNodeChild, container);
});
}
function processComponent(n1, n2, container, parentComponent) {
if (!n1) {
mountComponent(n2, container, parentComponent);
}
else {
updateComponent(n1, n2);
}
}
function updateComponent(n1, n2, container) {
console.log("更新组件", n1, n2);
const instance = (n2.component = n1.component);
if (shouldUpdateComponent(n1, n2)) {
console.log(`组件需要更新: ${instance}`);
instance.next = n2;
instance.update();
}
else {
console.log(`组件不需要更新: ${instance}`);
n2.component = n1.component;
n2.el = n1.el;
instance.vnode = n2;
}
}
function mountComponent(initialVNode, container, parentComponent) {
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent));
console.log(`创建组件实例:${instance.type.name}`);
setupComponent(instance);
setupRenderEffect(instance, initialVNode, container);
}
function setupRenderEffect(instance, initialVNode, container) {
function componentUpdateFn() {
if (!instance.isMounted) {
console.log(`${instance.type.name}:调用 render,获取 subTree`);
const proxyToUse = instance.proxy;
const subTree = (instance.subTree = normalizeVNode(instance.render.call(proxyToUse, proxyToUse)));
console.log("subTree", subTree);
console.log(`${instance.type.name}:触发 beforeMount hook`);
console.log(`${instance.type.name}:触发 onVnodeBeforeMount hook`);
patch(null, subTree, container, null, instance);
initialVNode.el = subTree.el;
console.log(`${instance.type.name}:触发 mounted hook`);
instance.isMounted = true;
}
else {
console.log(`${instance.type.name}:调用更新逻辑`);
const { next, vnode } = instance;
if (next) {
next.el = vnode.el;
updateComponentPreRender(instance, next);
}
const proxyToUse = instance.proxy;
const nextTree = normalizeVNode(instance.render.call(proxyToUse, proxyToUse));
const prevTree = instance.subTree;
instance.subTree = nextTree;
console.log(`${instance.type.name}:触发 beforeUpdated hook`);
console.log(`${instance.type.name}:触发 onVnodeBeforeUpdate hook`);
patch(prevTree, nextTree, prevTree.el, null, instance);
console.log(`${instance.type.name}:触发 updated hook`);
console.log(`${instance.type.name}:触发 onVnodeUpdated hook`);
}
}
instance.update = effect(componentUpdateFn, {
scheduler: () => {
queueJob(instance.update);
},
});
}
function updateComponentPreRender(instance, nextVNode) {
nextVNode.component = instance;
instance.vnode = nextVNode;
instance.next = null;
const { props } = nextVNode;
console.log("更新组件的 props", props);
instance.props = props;
console.log("更新组件的 slots");
}
return {
render,
createApp: createAppAPI(render),
};
}
function getSequence(arr) {
const p = arr.slice();
const result = [0];
let i, j, u, v, c;
const len = arr.length;
for (i = 0; i < len; i++) {
const arrI = arr[i];
if (arrI !== 0) {
j = result[result.length - 1];
if (arr[j] < arrI) {
p[i] = j;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
while (u < v) {
c = (u + v) >> 1;
if (arr[result[c]] < arrI) {
u = c + 1;
}
else {
v = c;
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}
function watchEffect(effect) {
return doWatch(effect);
}
function doWatch(source) {
const job = () => {
effect.run();
};
const scheduler = () => queuePreFlushCb(job);
let cleanup;
const onCleanup = (fn) => {
cleanup = effect.onStop = () => {
fn();
};
};
const getter = () => {
if (cleanup) {
cleanup();
}
source(onCleanup);
};
const effect = new ReactiveEffect(getter, scheduler);
effect.run();
return () => {
effect.stop();
};
}
function createElement(type) {
console.log("CreateElement", type);
const element = document.createElement(type);
return element;
}
function createText(text) {
return document.createTextNode(text);
}
function setText(node, text) {
node.nodeValue = text;
}
function setElementText(el, text) {
console.log("SetElementText", el, text);
el.textContent = text;
}
function patchProp(el, key, preValue, nextValue) {
console.log(`PatchProp 设置属性:${key} 值:${nextValue}`);
console.log(`key: ${key} 之前的值是:${preValue}`);
if (isOn(key)) {
const invokers = el._vei || (el._vei = {});
const existingInvoker = invokers[key];
if (nextValue && existingInvoker) {
existingInvoker.value = nextValue;
}
else {
const eventName = key.slice(2).toLowerCase();
if (nextValue) {
const invoker = (invokers[key] = nextValue);
el.addEventListener(eventName, invoker);
}
else {
el.removeEventListener(eventName, existingInvoker);
invokers[key] = undefined;
}
}
}
else {
if (nextValue === null || nextValue === "") {
el.removeAttribute(key);
}
else {
el.setAttribute(key, nextValue);
}
}
}
function insert(child, parent, anchor = null) {
console.log("Insert");
parent.insertBefore(child, anchor);
}
function remove(child) {
const parent = child.parentNode;
if (parent) {
parent.removeChild(child);
}
}
let renderer;
function ensureRenderer() {
return (renderer ||
(renderer = createRenderer({
createElement,
createText,
setText,
setElementText,
patchProp,
insert,
remove,
})));
}
const createApp = (...args) => {
return ensureRenderer().createApp(...args);
};
var runtimeDom = /*#__PURE__*/Object.freeze({
__proto__: null,
createApp: createApp,
getCurrentInstance: getCurrentInstance,
registerRuntimeCompiler: registerRuntimeCompiler,
inject: inject,
provide: provide,
renderSlot: renderSlot,
createTextVNode: createTextVNode,
createElementVNode: createVNode,
createRenderer: createRenderer,
toDisplayString: toDisplayString,
watchEffect: watchEffect,
reactive: reactive,
ref: ref,
readonly: readonly,
unRef: unRef,
proxyRefs: proxyRefs,
isReadonly: isReadonly,
isReactive: isReactive,
isProxy: isProxy,
isRef: isRef,
shallowReadonly: shallowReadonly,
effect: effect,
stop: stop,
computed: computed,
h: h,
createAppAPI: createAppAPI
});
const TO_DISPLAY_STRING = Symbol(`toDisplayString`);
const CREATE_ELEMENT_VNODE = Symbol("createElementVNode");
const helperNameMap = {
[TO_DISPLAY_STRING]: "toDisplayString",
[CREATE_ELEMENT_VNODE]: "createElementVNode"
};
function generate(ast, options = {}) {
const context = createCodegenContext(ast, options);
const { push, mode } = context;
if (mode === "module") {
genModulePreamble(ast, context);
}
else {
genFunctionPreamble(ast, context);
}
const functionName = "render";
const args = ["_ctx"];
const signature = args.join(", ");
push(`function ${functionName}(${signature}) {`);
push("return ");
genNode(ast.codegenNode, context);
push("}");
return {
code: context.code,
};
}
function genFunctionPreamble(ast, context) {
const { runtimeGlobalName, push, newline } = context;
const VueBinging = runtimeGlobalName;
const aliasHelper = (s) => `${helperNameMap[s]} : _${helperNameMap[s]}`;
if (ast.helpers.length > 0) {
push(`
const { ${ast.helpers.map(aliasHelper).join(", ")}} = ${VueBinging}
`);
}
newline();
push(`return `);
}
function genNode(node, context) {
switch (node.type) {
case 2:
genInterpolation(node, context);
break;
case 3:
genExpression(node, context);
break;
case 4:
genElement(node, context);
break;
case 5:
genCompoundExpression(node, context);
break;
case 0:
genText(node, context);
break;
}
}
function genCompoundExpression(node, context) {
const { push } = context;
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (isString(child)) {
push(child);
}
else {
genNode(child, context);
}
}
}
function genText(node, context) {
const { push } = context;
push(`'${node.content}'`);
}
function genElement(node, context) {
const { push, helper } = context;
const { tag, props, children } = node;
push(`${helper(CREATE_ELEMENT_VNODE)}(`);
genNodeList(genNullableArgs([tag, props, children]), context);
push(`)`);
}
function genNodeList(nodes, context) {
const { push } = context;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (isString(node)) {
push(`${node}`);
}
else {
genNode(node, context);
}
if (i < nodes.length - 1) {
push(", ");
}
}
}
function genNullableArgs(args) {
let i = args.length;
while (i--) {
if (args[i] != null)
break;
}
return args.slice(0, i + 1).map((arg) => arg || "null");
}
function genExpression(node, context) {
context.push(node.content, node);
}
function genInterpolation(node, context) {
const { push, helper } = context;
push(`${helper(TO_DISPLAY_STRING)}(`);
genNode(node.content, context);
push(")");
}
function genModulePreamble(ast, context) {
const { push, newline, runtimeModuleName } = context;
if (ast.helpers.length) {
const code = `import {${ast.helpers
.map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(", ")} } from ${JSON.stringify(runtimeModuleName)}`;
push(code);
}
newline();
push(`export `);
}
function createCodegenContext(ast, { runtimeModuleName = "vue", runtimeGlobalName = "Vue", mode = "function" }) {
const context = {
code: "",
mode,
runtimeModuleName,
runtimeGlobalName,
helper(key) {
return `_${helperNameMap[key]}`;
},
push(code) {
context.code += code;
},
newline() {
context.code += "\n";
},
};
return context;
}
var TagType;
(function (TagType) {
TagType[TagType["Start"] = 0] = "Start";
TagType[TagType["End"] = 1] = "End";
})(TagType || (TagType = {}));
function baseParse(content) {
const context = createParserContext(content);
return createRoot(parseChildren(context, []));
}
function createParserContext(content) {
console.log("创建 paserContext");
return {
source: content,
};
}
function parseChildren(context, ancestors) {
console.log("开始解析 children");
const nodes = [];
while (!isEnd(context, ancestors)) {
let node;
const s = context.source;
if (startsWith(s, "{{")) {
node = parseInterpolation(context);
}
else if (s[0] === "<") {
if (s[1] === "/") {
if (/[a-z]/i.test(s[2])) {
parseTag(context, 1);
continue;
}
}
else if (/[a-z]/i.test(s[1])) {
node = parseElement(context, ancestors);
}
}
if (!node) {
node = parseText(context);
}
nodes.push(node);
}
return nodes;
}
function isEnd(context, ancestors) {
const s = context.source;
if (context.source.startsWith("")) {
for (let i = ancestors.length - 1; i >= 0; --i) {
if (startsWithEndTagOpen(s, ancestors[i].tag)) {
return true;
}
}
}
return !context.source;
}
function parseElement(context, ancestors) {
const element = parseTag(context, 0);
ancestors.push(element);
const children = parseChildren(context, ancestors);
ancestors.pop();
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, 1);
}
else {
throw new Error(`缺失结束标签:${element.tag}`);
}
element.children = children;
return element;
}
function startsWithEndTagOpen(source, tag) {
return (startsWith(source, "") &&
source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase());
}
function parseTag(context, type) {
const match = /^<\/?([a-z][^\r\n\t\f />]*)/i.exec(context.source);
const tag = match[1];
advanceBy(context, match[0].length);
advanceBy(context, 1);
if (type === 1)
return;
let tagType = 0;
return {
type: 4,
tag,
tagType,
};
}
function parseInterpolation(context) {
const openDelimiter = "{{";
const closeDelimiter = "}}";
const closeIndex = context.source.indexOf(closeDelimiter, openDelimiter.length);
advanceBy(context, 2);
const rawContentLength = closeIndex - openDelimiter.length;
const rawContent = context.source.slice(0, rawContentLength);
const preTrimContent = parseTextData(context, rawContent.length);
const content = preTrimContent.trim();
advanceBy(context, closeDelimiter.length);
return {
type: 2,
content: {
type: 3,
content,
},
};
}
function parseText(context) {
console.log("解析 text", context);
const endTokens = ["<", "{{"];
let endIndex = context.source.length;
for (let i = 0; i < endTokens.length; i++) {
const index = context.source.indexOf(endTokens[i]);
if (index !== -1 && endIndex > index) {
endIndex = index;
}
}
const content = parseTextData(context, endIndex);
return {
type: 0,
content,
};
}
function parseTextData(context, length) {
console.log("解析 textData");
const rawText = context.source.slice(0, length);
advanceBy(context, length);
return rawText;
}
function advanceBy(context, numberOfCharacters) {
console.log("推进代码", context, numberOfCharacters);
context.source = context.source.slice(numberOfCharacters);
}
function createRoot(children) {
return {
type: 1,
children,
helpers: [],
};
}
function startsWith(source, searchString) {
return source.startsWith(searchString);
}
function transform(root, options = {}) {
const context = createTransformContext(root, options);
traverseNode(root, context);
createRootCodegen(root);
root.helpers.push(...context.helpers.keys());
}
function traverseNode(node, context) {
const type = node.type;
const nodeTransforms = context.nodeTransforms;
const exitFns = [];
for (let i = 0; i < nodeTransforms.length; i++) {
const transform = nodeTransforms[i];
const onExit = transform(node, context);
if (onExit) {
exitFns.push(onExit);
}
}
switch (type) {
case 2:
context.helper(TO_DISPLAY_STRING);
break;
case 1:
case 4:
traverseChildren(node, context);
break;
}
let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}
function traverseChildren(parent, context) {
parent.children.forEach((node) => {
traverseNode(node, context);
});
}
function createTransformContext(root, options) {
const context = {
root,
nodeTransforms: options.nodeTransforms || [],
helpers: new Map(),
helper(name) {
const count = context.helpers.get(name) || 0;
context.helpers.set(name, count + 1);
},
};
return context;
}
function createRootCodegen(root, context) {
const { children } = root;
const child = children[0];
if (child.type === 4 && child.codegenNode) {
const codegenNode = child.codegenNode;
root.codegenNode = codegenNode;
}
else {
root.codegenNode = child;
}
}
function transformExpression(node) {
if (node.type === 2) {
node.content = processExpression(node.content);
}
}
function processExpression(node) {
node.content = `_ctx.${node.content}`;
return node;
}
var NodeTypes;
(function (NodeTypes) {
NodeTypes[NodeTypes["TEXT"] = 0] = "TEXT";
NodeTypes[NodeTypes["ROOT"] = 1] = "ROOT";
NodeTypes[NodeTypes["INTERPOLATION"] = 2] = "INTERPOLATION";
NodeTypes[NodeTypes["SIMPLE_EXPRESSION"] = 3] = "SIMPLE_EXPRESSION";
NodeTypes[NodeTypes["ELEMENT"] = 4] = "ELEMENT";
NodeTypes[NodeTypes["COMPOUND_EXPRESSION"] = 5] = "COMPOUND_EXPRESSION";
})(NodeTypes || (NodeTypes = {}));
var ElementTypes;
(function (ElementTypes) {
ElementTypes[ElementTypes["ELEMENT"] = 0] = "ELEMENT";
})(ElementTypes || (ElementTypes = {}));
function createVNodeCall(context, tag, props, children) {
if (context) {
context.helper(CREATE_ELEMENT_VNODE);
}
return {
type: 4,
tag,
props,
children,
};
}
function transformElement(node, context) {
if (node.type === 4) {
return () => {
const vnodeTag = `'${node.tag}'`;
const vnodeProps = null;
let vnodeChildren = null;
if (node.children.length > 0) {
if (node.children.length === 1) {
const child = node.children[0];
vnodeChildren = child;
}
}
node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren);
};
}
}
function isText(node) {
return node.type === 2 || node.type === 0;
}
function transformText(node, context) {
if (node.type === 4) {
return () => {
const children = node.children;
let currentContainer;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (isText(child)) {
for (let j = i + 1; j < children.length; j++) {
const next = children[j];
if (isText(next)) {
if (!currentContainer) {
currentContainer = children[i] = {
type: 5,
loc: child.loc,
children: [child],
};
}
currentContainer.children.push(` + `, next);
children.splice(j, 1);
j--;
}
else {
currentContainer = undefined;
break;
}
}
}
}
};
}
}
function baseCompile(template, options) {
const ast = baseParse(template);
transform(ast, Object.assign(options, {
nodeTransforms: [transformElement, transformText, transformExpression],
}));
return generate(ast);
}
function compileToFunction(template, options = {}) {
const { code } = baseCompile(template, options);
const render = new Function("Vue", code)(runtimeDom);
return render;
}
registerRuntimeCompiler(compileToFunction);
exports.computed = computed;
exports.createApp = createApp;
exports.createAppAPI = createAppAPI;
exports.createElementVNode = createVNode;
exports.createRenderer = createRenderer;
exports.createTextVNode = createTextVNode;
exports.effect = effect;
exports.getCurrentInstance = getCurrentInstance;
exports.h = h;
exports.inject = inject;
exports.isProxy = isProxy;
exports.isReactive = isReactive;
exports.isReadonly = isReadonly;
exports.isRef = isRef;
exports.provide = provide;
exports.proxyRefs = proxyRefs;
exports.reactive = reactive;
exports.readonly = readonly;
exports.ref = ref;
exports.registerRuntimeCompiler = registerRuntimeCompiler;
exports.renderSlot = renderSlot;
exports.shallowReadonly = shallowReadonly;
exports.stop = stop;
exports.toDisplayString = toDisplayString;
exports.unRef = unRef;
exports.watchEffect = watchEffect;
//# sourceMappingURL=mini-vue.cjs.js.map
================================================
FILE: packages/vue/dist/mini-vue.esm-bundler.js
================================================
var ShapeFlags;
(function (ShapeFlags) {
ShapeFlags[ShapeFlags["ELEMENT"] = 1] = "ELEMENT";
ShapeFlags[ShapeFlags["STATEFUL_COMPONENT"] = 4] = "STATEFUL_COMPONENT";
ShapeFlags[ShapeFlags["TEXT_CHILDREN"] = 8] = "TEXT_CHILDREN";
ShapeFlags[ShapeFlags["ARRAY_CHILDREN"] = 16] = "ARRAY_CHILDREN";
ShapeFlags[ShapeFlags["SLOTS_CHILDREN"] = 32] = "SLOTS_CHILDREN";
})(ShapeFlags || (ShapeFlags = {}));
const toDisplayString = (val) => {
return String(val);
};
const isObject = (val) => {
return val !== null && typeof val === "object";
};
const isString = (val) => typeof val === "string";
const camelizeRE = /-(\w)/g;
const camelize = (str) => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ""));
};
const extend = Object.assign;
const isOn = (key) => /^on[A-Z]/.test(key);
function hasChanged(value, oldValue) {
return !Object.is(value, oldValue);
}
function hasOwn(val, key) {
return Object.prototype.hasOwnProperty.call(val, key);
}
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const toHandlerKey = (str) => str ? `on${capitalize(str)}` : ``;
const hyphenateRE = /\B([A-Z])/g;
const hyphenate = (str) => str.replace(hyphenateRE, "-$1").toLowerCase();
const createVNode = function (type, props, children) {
const vnode = {
el: null,
component: null,
key: props === null || props === void 0 ? void 0 : props.key,
type,
props: props || {},
children,
shapeFlag: getShapeFlag(type),
};
if (Array.isArray(children)) {
vnode.shapeFlag |= 16;
}
else if (typeof children === "string") {
vnode.shapeFlag |= 8;
}
normalizeChildren(vnode, children);
return vnode;
};
function normalizeChildren(vnode, children) {
if (typeof children === "object") {
if (vnode.shapeFlag & 1) ;
else {
vnode.shapeFlag |= 32;
}
}
}
const Text = Symbol("Text");
const Fragment = Symbol("Fragment");
function createTextVNode(text = " ") {
return createVNode(Text, {}, text);
}
function normalizeVNode(child) {
if (typeof child === "string" || typeof child === "number") {
return createVNode(Text, null, String(child));
}
else {
return child;
}
}
function getShapeFlag(type) {
return typeof type === "string"
? 1
: 4;
}
const h = (type, props = null, children = []) => {
return createVNode(type, props, children);
};
function createAppAPI(render) {
return function createApp(rootComponent) {
const app = {
_component: rootComponent,
mount(rootContainer) {
console.log("基于根组件创建 vnode");
const vnode = createVNode(rootComponent);
console.log("调用 render,基于 vnode 进行开箱");
render(vnode, rootContainer);
},
};
return app;
};
}
function initProps(instance, rawProps) {
console.log("initProps");
instance.props = rawProps;
}
function initSlots(instance, children) {
const { vnode } = instance;
console.log("初始化 slots");
if (vnode.shapeFlag & 32) {
normalizeObjectSlots(children, (instance.slots = {}));
}
}
const normalizeSlotValue = (value) => {
return Array.isArray(value) ? value : [value];
};
const normalizeObjectSlots = (rawSlots, slots) => {
for (const key in rawSlots) {
const value = rawSlots[key];
if (typeof value === "function") {
slots[key] = (props) => normalizeSlotValue(value(props));
}
}
};
function emit(instance, event, ...rawArgs) {
const props = instance.props;
let handler = props[toHandlerKey(camelize(event))];
if (!handler) {
handler = props[(toHandlerKey(hyphenate(event)))];
}
if (handler) {
handler(...rawArgs);
}
}
const publicPropertiesMap = {
$el: (i) => i.vnode.el,
$emit: (i) => i.emit,
$slots: (i) => i.slots,
$props: (i) => i.props,
};
const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
const { setupState, props } = instance;
console.log(`触发 proxy hook , key -> : ${key}`);
if (key[0] !== "$") {
if (hasOwn(setupState, key)) {
return setupState[key];
}
else if (hasOwn(props, key)) {
return props[key];
}
}
const publicGetter = publicPropertiesMap[key];
if (publicGetter) {
return publicGetter(instance);
}
},
set({ _: instance }, key, value) {
const { setupState } = instance;
if (hasOwn(setupState, key)) {
setupState[key] = value;
}
return true;
},
};
function createDep(effects) {
const dep = new Set(effects);
return dep;
}
let activeEffect = void 0;
let shouldTrack = false;
const targetMap = new WeakMap();
class ReactiveEffect {
constructor(fn, scheduler) {
this.fn = fn;
this.scheduler = scheduler;
this.active = true;
this.deps = [];
console.log("创建 ReactiveEffect 对象");
}
run() {
console.log("run");
if (!this.active) {
return this.fn();
}
shouldTrack = true;
activeEffect = this;
console.log("执行用户传入的 fn");
const result = this.fn();
shouldTrack = false;
activeEffect = undefined;
return result;
}
stop() {
if (this.active) {
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
function cleanupEffect(effect) {
effect.deps.forEach((dep) => {
dep.delete(effect);
});
effect.deps.length = 0;
}
function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn);
extend(_effect, options);
_effect.run();
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
}
function stop(runner) {
runner.effect.stop();
}
function track(target, type, key) {
if (!isTracking()) {
return;
}
console.log(`触发 track -> target: ${target} type:${type} key:${key}`);
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = createDep();
depsMap.set(key, dep);
}
trackEffects(dep);
}
function trackEffects(dep) {
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
function trigger(target, type, key) {
let deps = [];
const depsMap = targetMap.get(target);
if (!depsMap)
return;
const dep = depsMap.get(key);
deps.push(dep);
const effects = [];
deps.forEach((dep) => {
effects.push(...dep);
});
triggerEffects(createDep(effects));
}
function isTracking() {
return shouldTrack && activeEffect !== undefined;
}
function triggerEffects(dep) {
for (const effect of dep) {
if (effect.scheduler) {
effect.scheduler();
}
else {
effect.run();
}
}
}
const get = createGetter();
const set = createSetter();
const readonlyGet = createGetter(true);
const shallowReadonlyGet = createGetter(true, true);
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
const isExistInReactiveMap = () => key === "__v_raw" && receiver === reactiveMap.get(target);
const isExistInReadonlyMap = () => key === "__v_raw" && receiver === readonlyMap.get(target);
const isExistInShallowReadonlyMap = () => key === "__v_raw" && receiver === shallowReadonlyMap.get(target);
if (key === "__v_isReactive") {
return !isReadonly;
}
else if (key === "__v_isReadonly") {
return isReadonly;
}
else if (isExistInReactiveMap() ||
isExistInReadonlyMap() ||
isExistInShallowReadonlyMap()) {
return target;
}
const res = Reflect.get(target, key, receiver);
if (!isReadonly) {
track(target, "get", key);
}
if (shallow) {
return res;
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
function createSetter() {
return function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, "set", key);
return result;
};
}
const readonlyHandlers = {
get: readonlyGet,
set(target, key) {
console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target);
return true;
},
};
const mutableHandlers = {
get,
set,
};
const shallowReadonlyHandlers = {
get: shallowReadonlyGet,
set(target, key) {
console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target);
return true;
},
};
const reactiveMap = new WeakMap();
const readonlyMap = new WeakMap();
const shallowReadonlyMap = new WeakMap();
var ReactiveFlags;
(function (ReactiveFlags) {
ReactiveFlags["IS_REACTIVE"] = "__v_isReactive";
ReactiveFlags["IS_READONLY"] = "__v_isReadonly";
ReactiveFlags["RAW"] = "__v_raw";
})(ReactiveFlags || (ReactiveFlags = {}));
function reactive(target) {
return createReactiveObject(target, reactiveMap, mutableHandlers);
}
function readonly(target) {
return createReactiveObject(target, readonlyMap, readonlyHandlers);
}
function shallowReadonly(target) {
return createReactiveObject(target, shallowReadonlyMap, shallowReadonlyHandlers);
}
function isProxy(value) {
return isReactive(value) || isReadonly(value);
}
function isReadonly(value) {
return !!value["__v_isReadonly"];
}
function isReactive(value) {
return !!value["__v_isReactive"];
}
function createReactiveObject(target, proxyMap, baseHandlers) {
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, baseHandlers);
proxyMap.set(target, proxy);
return proxy;
}
class RefImpl {
constructor(value) {
this.__v_isRef = true;
this._rawValue = value;
this._value = convert(value);
this.dep = createDep();
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newValue) {
if (hasChanged(newValue, this._rawValue)) {
this._value = convert(newValue);
this._rawValue = newValue;
triggerRefValue(this);
}
}
}
function ref(value) {
return createRef(value);
}
function convert(value) {
return isObject(value) ? reactive(value) : value;
}
function createRef(value) {
const refImpl = new RefImpl(value);
return refImpl;
}
function triggerRefValue(ref) {
triggerEffects(ref.dep);
}
function trackRefValue(ref) {
if (isTracking()) {
trackEffects(ref.dep);
}
}
const shallowUnwrapHandlers = {
get(target, key, receiver) {
return unRef(Reflect.get(target, key, receiver));
},
set(target, key, value, receiver) {
const oldValue = target[key];
if (isRef(oldValue) && !isRef(value)) {
return (target[key].value = value);
}
else {
return Reflect.set(target, key, value, receiver);
}
},
};
function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, shallowUnwrapHandlers);
}
function unRef(ref) {
return isRef(ref) ? ref.value : ref;
}
function isRef(value) {
return !!value.__v_isRef;
}
class ComputedRefImpl {
constructor(getter) {
this._dirty = true;
this.dep = createDep();
this.effect = new ReactiveEffect(getter, () => {
if (this._dirty)
return;
this._dirty = true;
triggerRefValue(this);
});
}
get value() {
trackRefValue(this);
if (this._dirty) {
this._dirty = false;
this._value = this.effect.run();
}
return this._value;
}
}
function computed(getter) {
return new ComputedRefImpl(getter);
}
function createComponentInstance(vnode, parent) {
const instance = {
type: vnode.type,
vnode,
next: null,
props: {},
parent,
provides: parent ? parent.provides : {},
proxy: null,
isMounted: false,
attrs: {},
slots: {},
ctx: {},
setupState: {},
emit: () => { },
};
instance.ctx = {
_: instance,
};
instance.emit = emit.bind(null, instance);
return instance;
}
function setupComponent(instance) {
const { props, children } = instance.vnode;
initProps(instance, props);
initSlots(instance, children);
setupStatefulComponent(instance);
}
function setupStatefulComponent(instance) {
console.log("创建 proxy");
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
const Component = instance.type;
const { setup } = Component;
if (setup) {
setCurrentInstance(instance);
const setupContext = createSetupContext(instance);
const setupResult = setup && setup(shallowReadonly(instance.props), setupContext);
setCurrentInstance(null);
handleSetupResult(instance, setupResult);
}
else {
finishComponentSetup(instance);
}
}
function createSetupContext(instance) {
console.log("初始化 setup context");
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit,
expose: () => { },
};
}
function handleSetupResult(instance, setupResult) {
if (typeof setupResult === "function") {
instance.render = setupResult;
}
else if (typeof setupResult === "object") {
instance.setupState = proxyRefs(setupResult);
}
finishComponentSetup(instance);
}
function finishComponentSetup(instance) {
const Component = instance.type;
if (!instance.render) {
if (compile && !Component.render) {
if (Component.template) {
const template = Component.template;
Component.render = compile(template);
}
}
instance.render = Component.render;
}
}
let currentInstance = {};
function getCurrentInstance() {
return currentInstance;
}
function setCurrentInstance(instance) {
currentInstance = instance;
}
let compile;
function registerRuntimeCompiler(_compile) {
compile = _compile;
}
function provide(key, value) {
var _a;
const currentInstance = getCurrentInstance();
if (currentInstance) {
let { provides } = currentInstance;
const parentProvides = (_a = currentInstance.parent) === null || _a === void 0 ? void 0 : _a.provides;
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides);
}
provides[key] = value;
}
}
function inject(key, defaultValue) {
var _a;
const currentInstance = getCurrentInstance();
if (currentInstance) {
const provides = (_a = currentInstance.parent) === null || _a === void 0 ? void 0 : _a.provides;
if (key in provides) {
return provides[key];
}
else if (defaultValue) {
if (typeof defaultValue === "function") {
return defaultValue();
}
return defaultValue;
}
}
}
function renderSlot(slots, name, props = {}) {
const slot = slots[name];
console.log(`渲染插槽 slot -> ${name}`);
if (slot) {
const slotContent = slot(props);
return createVNode(Fragment, {}, slotContent);
}
}
const queue = [];
const activePreFlushCbs = [];
const p = Promise.resolve();
let isFlushPending = false;
function nextTick(fn) {
return fn ? p.then(fn) : p;
}
function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job);
queueFlush();
}
}
function queueFlush() {
if (isFlushPending)
return;
isFlushPending = true;
nextTick(flushJobs);
}
function queuePreFlushCb(cb) {
queueCb(cb, activePreFlushCbs);
}
function queueCb(cb, activeQueue) {
activeQueue.push(cb);
queueFlush();
}
function flushJobs() {
isFlushPending = false;
flushPreFlushCbs();
let job;
while ((job = queue.shift())) {
if (job) {
job();
}
}
}
function flushPreFlushCbs() {
for (let i = 0; i < activePreFlushCbs.length; i++) {
activePreFlushCbs[i]();
}
}
function shouldUpdateComponent(prevVNode, nextVNode) {
const { props: prevProps } = prevVNode;
const { props: nextProps } = nextVNode;
if (prevProps === nextProps) {
return false;
}
if (!prevProps) {
return !!nextProps;
}
if (!nextProps) {
return true;
}
return hasPropsChanged(prevProps, nextProps);
}
function hasPropsChanged(prevProps, nextProps) {
const nextKeys = Object.keys(nextProps);
if (nextKeys.length !== Object.keys(prevProps).length) {
return true;
}
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i];
if (nextProps[key] !== prevProps[key]) {
return true;
}
}
return false;
}
function createRenderer(options) {
const { createElement: hostCreateElement, setElementText: hostSetElementText, patchProp: hostPatchProp, insert: hostInsert, remove: hostRemove, setText: hostSetText, createText: hostCreateText, } = options;
const render = (vnode, container) => {
console.log("调用 patch");
patch(null, vnode, container);
};
function patch(n1, n2, container = null, anchor = null, parentComponent = null) {
const { type, shapeFlag } = n2;
switch (type) {
case Text:
processText(n1, n2, container);
break;
case Fragment:
processFragment(n1, n2, container);
break;
default:
if (shapeFlag & 1) {
console.log("处理 element");
processElement(n1, n2, container, anchor, parentComponent);
}
else if (shapeFlag & 4) {
console.log("处理 component");
processComponent(n1, n2, container, parentComponent);
}
}
}
function processFragment(n1, n2, container) {
if (!n1) {
console.log("初始化 Fragment 类型的节点");
mountChildren(n2.children, container);
}
}
function processText(n1, n2, container) {
console.log("处理 Text 节点");
if (n1 === null) {
console.log("初始化 Text 类型的节点");
hostInsert((n2.el = hostCreateText(n2.children)), container);
}
else {
const el = (n2.el = n1.el);
if (n2.children !== n1.children) {
console.log("更新 Text 类型的节点");
hostSetText(el, n2.children);
}
}
}
function processElement(n1, n2, container, anchor, parentComponent) {
if (!n1) {
mountElement(n2, container, anchor);
}
else {
updateElement(n1, n2, container, anchor, parentComponent);
}
}
function updateElement(n1, n2, container, anchor, parentComponent) {
const oldProps = (n1 && n1.props) || {};
const newProps = n2.props || {};
console.log("应该更新 element");
console.log("旧的 vnode", n1);
console.log("新的 vnode", n2);
const el = (n2.el = n1.el);
patchProps(el, oldProps, newProps);
patchChildren(n1, n2, el, anchor, parentComponent);
}
function patchProps(el, oldProps, newProps) {
for (const key in newProps) {
const prevProp = oldProps[key];
const nextProp = newProps[key];
if (prevProp !== nextProp) {
hostPatchProp(el, key, prevProp, nextProp);
}
}
for (const key in oldProps) {
const prevProp = oldProps[key];
const nextProp = null;
if (!(key in newProps)) {
hostPatchProp(el, key, prevProp, nextProp);
}
}
}
function patchChildren(n1, n2, container, anchor, parentComponent) {
const { shapeFlag: prevShapeFlag, children: c1 } = n1;
const { shapeFlag, children: c2 } = n2;
if (shapeFlag & 8) {
if (c2 !== c1) {
console.log("类型为 text_children, 当前需要更新");
hostSetElementText(container, c2);
}
}
else {
if (prevShapeFlag & 8) {
hostSetElementText(container, "");
mountChildren(c2, container);
}
else {
patchKeyedChildren(c1, c2, container, parentComponent, anchor);
}
}
}
function patchKeyedChildren(c1, c2, container, parentAnchor, parentComponent) {
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1;
let e2 = l2 - 1;
const isSameVNodeType = (n1, n2) => {
return n1.type === n2.type && n1.key === n2.key;
};
while (i <= e1 && i <= e2) {
const prevChild = c1[i];
const nextChild = c2[i];
if (!isSameVNodeType(prevChild, nextChild)) {
console.log("两个 child 不相等(从左往右比对)");
console.log(`prevChild:${prevChild}`);
console.log(`nextChild:${nextChild}`);
break;
}
console.log("两个 child 相等,接下来对比这两个 child 节点(从左往右比对)");
patch(prevChild, nextChild, container, parentAnchor, parentComponent);
i++;
}
while (i <= e1 && i <= e2) {
const prevChild = c1[e1];
const nextChild = c2[e2];
if (!isSameVNodeType(prevChild, nextChild)) {
console.log("两个 child 不相等(从右往左比对)");
console.log(`prevChild:${prevChild}`);
console.log(`nextChild:${nextChild}`);
break;
}
console.log("两个 child 相等,接下来对比这两个 child 节点(从右往左比对)");
patch(prevChild, nextChild, container, parentAnchor, parentComponent);
e1--;
e2--;
}
if (i > e1 && i <= e2) {
const nextPos = e2 + 1;
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
while (i <= e2) {
console.log(`需要新创建一个 vnode: ${c2[i].key}`);
patch(null, c2[i], container, anchor, parentComponent);
i++;
}
}
else if (i > e2 && i <= e1) {
while (i <= e1) {
console.log(`需要删除当前的 vnode: ${c1[i].key}`);
hostRemove(c1[i].el);
i++;
}
}
else {
let s1 = i;
let s2 = i;
const keyToNewIndexMap = new Map();
let moved = false;
let maxNewIndexSoFar = 0;
for (let i = s2; i <= e2; i++) {
const nextChild = c2[i];
keyToNewIndexMap.set(nextChild.key, i);
}
const toBePatched = e2 - s2 + 1;
let patched = 0;
const newIndexToOldIndexMap = new Array(toBePatched);
for (let i = 0; i < toBePatched; i++)
newIndexToOldIndexMap[i] = 0;
for (i = s1; i <= e1; i++) {
const prevChild = c1[i];
if (patched >= toBePatched) {
hostRemove(prevChild.el);
continue;
}
let newIndex;
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key);
}
else {
for (let j = s2; j <= e2; j++) {
if (isSameVNodeType(prevChild, c2[j])) {
newIndex = j;
break;
}
}
}
if (newIndex === undefined) {
hostRemove(prevChild.el);
}
else {
console.log("新老节点都存在");
newIndexToOldIndexMap[newIndex - s2] = i + 1;
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
}
else {
moved = true;
}
patch(prevChild, c2[newIndex], container, null, parentComponent);
patched++;
}
}
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: [];
let j = increasingNewIndexSequence.length - 1;
for (let i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i;
const nextChild = c2[nextIndex];
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
if (newIndexToOldIndexMap[i] === 0) {
patch(null, nextChild, container, anchor, parentComponent);
}
else if (moved) {
if (j < 0 || increasingNewIndexSequence[j] !== i) {
hostInsert(nextChild.el, container, anchor);
}
else {
j--;
}
}
}
}
}
function mountElement(vnode, container, anchor) {
const { shapeFlag, props } = vnode;
const el = (vnode.el = hostCreateElement(vnode.type));
if (shapeFlag & 8) {
console.log(`处理文本:${vnode.children}`);
hostSetElementText(el, vnode.children);
}
else if (shapeFlag & 16) {
mountChildren(vnode.children, el);
}
if (props) {
for (const key in props) {
const nextVal = props[key];
hostPatchProp(el, key, null, nextVal);
}
}
console.log("vnodeHook -> onVnodeBeforeMount");
console.log("DirectiveHook -> beforeMount");
console.log("transition -> beforeEnter");
hostInsert(el, container, anchor);
console.log("vnodeHook -> onVnodeMounted");
console.log("DirectiveHook -> mounted");
console.log("transition -> enter");
}
function mountChildren(children, container) {
children.forEach((VNodeChild) => {
console.log("mountChildren:", VNodeChild);
patch(null, VNodeChild, container);
});
}
function processComponent(n1, n2, container, parentComponent) {
if (!n1) {
mountComponent(n2, container, parentComponent);
}
else {
updateComponent(n1, n2);
}
}
function updateComponent(n1, n2, container) {
console.log("更新组件", n1, n2);
const instance = (n2.component = n1.component);
if (shouldUpdateComponent(n1, n2)) {
console.log(`组件需要更新: ${instance}`);
instance.next = n2;
instance.update();
}
else {
console.log(`组件不需要更新: ${instance}`);
n2.component = n1.component;
n2.el = n1.el;
instance.vnode = n2;
}
}
function mountComponent(initialVNode, container, parentComponent) {
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent));
console.log(`创建组件实例:${instance.type.name}`);
setupComponent(instance);
setupRenderEffect(instance, initialVNode, container);
}
function setupRenderEffect(instance, initialVNode, container) {
function componentUpdateFn() {
if (!instance.isMounted) {
console.log(`${instance.type.name}:调用 render,获取 subTree`);
const proxyToUse = instance.proxy;
const subTree = (instance.subTree = normalizeVNode(instance.render.call(proxyToUse, proxyToUse)));
console.log("subTree", subTree);
console.log(`${instance.type.name}:触发 beforeMount hook`);
console.log(`${instance.type.name}:触发 onVnodeBeforeMount hook`);
patch(null, subTree, container, null, instance);
initialVNode.el = subTree.el;
console.log(`${instance.type.name}:触发 mounted hook`);
instance.isMounted = true;
}
else {
console.log(`${instance.type.name}:调用更新逻辑`);
const { next, vnode } = instance;
if (next) {
next.el = vnode.el;
updateComponentPreRender(instance, next);
}
const proxyToUse = instance.proxy;
const nextTree = normalizeVNode(instance.render.call(proxyToUse, proxyToUse));
const prevTree = instance.subTree;
instance.subTree = nextTree;
console.log(`${instance.type.name}:触发 beforeUpdated hook`);
console.log(`${instance.type.name}:触发 onVnodeBeforeUpdate hook`);
patch(prevTree, nextTree, prevTree.el, null, instance);
console.log(`${instance.type.name}:触发 updated hook`);
console.log(`${instance.type.name}:触发 onVnodeUpdated hook`);
}
}
instance.update = effect(componentUpdateFn, {
scheduler: () => {
queueJob(instance.update);
},
});
}
function updateComponentPreRender(instance, nextVNode) {
nextVNode.component = instance;
instance.vnode = nextVNode;
instance.next = null;
const { props } = nextVNode;
console.log("更新组件的 props", props);
instance.props = props;
console.log("更新组件的 slots");
}
return {
render,
createApp: createAppAPI(render),
};
}
function getSequence(arr) {
const p = arr.slice();
const result = [0];
let i, j, u, v, c;
const len = arr.length;
for (i = 0; i < len; i++) {
const arrI = arr[i];
if (arrI !== 0) {
j = result[result.length - 1];
if (arr[j] < arrI) {
p[i] = j;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
while (u < v) {
c = (u + v) >> 1;
if (arr[result[c]] < arrI) {
u = c + 1;
}
else {
v = c;
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}
function watchEffect(effect) {
return doWatch(effect);
}
function doWatch(source) {
const job = () => {
effect.run();
};
const scheduler = () => queuePreFlushCb(job);
let cleanup;
const onCleanup = (fn) => {
cleanup = effect.onStop = () => {
fn();
};
};
const getter = () => {
if (cleanup) {
cleanup();
}
source(onCleanup);
};
const effect = new ReactiveEffect(getter, scheduler);
effect.run();
return () => {
effect.stop();
};
}
function createElement(type) {
console.log("CreateElement", type);
const element = document.createElement(type);
return element;
}
function createText(text) {
return document.createTextNode(text);
}
function setText(node, text) {
node.nodeValue = text;
}
function setElementText(el, text) {
console.log("SetElementText", el, text);
el.textContent = text;
}
function patchProp(el, key, preValue, nextValue) {
console.log(`PatchProp 设置属性:${key} 值:${nextValue}`);
console.log(`key: ${key} 之前的值是:${preValue}`);
if (isOn(key)) {
const invokers = el._vei || (el._vei = {});
const existingInvoker = invokers[key];
if (nextValue && existingInvoker) {
existingInvoker.value = nextValue;
}
else {
const eventName = key.slice(2).toLowerCase();
if (nextValue) {
const invoker = (invokers[key] = nextValue);
el.addEventListener(eventName, invoker);
}
else {
el.removeEventListener(eventName, existingInvoker);
invokers[key] = undefined;
}
}
}
else {
if (nextValue === null || nextValue === "") {
el.removeAttribute(key);
}
else {
el.setAttribute(key, nextValue);
}
}
}
function insert(child, parent, anchor = null) {
console.log("Insert");
parent.insertBefore(child, anchor);
}
function remove(child) {
const parent = child.parentNode;
if (parent) {
parent.removeChild(child);
}
}
let renderer;
function ensureRenderer() {
return (renderer ||
(renderer = createRenderer({
createElement,
createText,
setText,
setElementText,
patchProp,
insert,
remove,
})));
}
const createApp = (...args) => {
return ensureRenderer().createApp(...args);
};
var runtimeDom = /*#__PURE__*/Object.freeze({
__proto__: null,
createApp: createApp,
getCurrentInstance: getCurrentInstance,
registerRuntimeCompiler: registerRuntimeCompiler,
inject: inject,
provide: provide,
renderSlot: renderSlot,
createTextVNode: createTextVNode,
createElementVNode: createVNode,
createRenderer: createRenderer,
toDisplayString: toDisplayString,
watchEffect: watchEffect,
reactive: reactive,
ref: ref,
readonly: readonly,
unRef: unRef,
proxyRefs: proxyRefs,
isReadonly: isReadonly,
isReactive: isReactive,
isProxy: isProxy,
isRef: isRef,
shallowReadonly: shallowReadonly,
effect: effect,
stop: stop,
computed: computed,
h: h,
createAppAPI: createAppAPI
});
const TO_DISPLAY_STRING = Symbol(`toDisplayString`);
const CREATE_ELEMENT_VNODE = Symbol("createElementVNode");
const helperNameMap = {
[TO_DISPLAY_STRING]: "toDisplayString",
[CREATE_ELEMENT_VNODE]: "createElementVNode"
};
function generate(ast, options = {}) {
const context = createCodegenContext(ast, options);
const { push, mode } = context;
if (mode === "module") {
genModulePreamble(ast, context);
}
else {
genFunctionPreamble(ast, context);
}
const functionName = "render";
const args = ["_ctx"];
const signature = args.join(", ");
push(`function ${functionName}(${signature}) {`);
push("return ");
genNode(ast.codegenNode, context);
push("}");
return {
code: context.code,
};
}
function genFunctionPreamble(ast, context) {
const { runtimeGlobalName, push, newline } = context;
const VueBinging = runtimeGlobalName;
const aliasHelper = (s) => `${helperNameMap[s]} : _${helperNameMap[s]}`;
if (ast.helpers.length > 0) {
push(`
const { ${ast.helpers.map(aliasHelper).join(", ")}} = ${VueBinging}
`);
}
newline();
push(`return `);
}
function genNode(node, context) {
switch (node.type) {
case 2:
genInterpolation(node, context);
break;
case 3:
genExpression(node, context);
break;
case 4:
genElement(node, context);
break;
case 5:
genCompoundExpression(node, context);
break;
case 0:
genText(node, context);
break;
}
}
function genCompoundExpression(node, context) {
const { push } = context;
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (isString(child)) {
push(child);
}
else {
genNode(child, context);
}
}
}
function genText(node, context) {
const { push } = context;
push(`'${node.content}'`);
}
function genElement(node, context) {
const { push, helper } = context;
const { tag, props, children } = node;
push(`${helper(CREATE_ELEMENT_VNODE)}(`);
genNodeList(genNullableArgs([tag, props, children]), context);
push(`)`);
}
function genNodeList(nodes, context) {
const { push } = context;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (isString(node)) {
push(`${node}`);
}
else {
genNode(node, context);
}
if (i < nodes.length - 1) {
push(", ");
}
}
}
function genNullableArgs(args) {
let i = args.length;
while (i--) {
if (args[i] != null)
break;
}
return args.slice(0, i + 1).map((arg) => arg || "null");
}
function genExpression(node, context) {
context.push(node.content, node);
}
function genInterpolation(node, context) {
const { push, helper } = context;
push(`${helper(TO_DISPLAY_STRING)}(`);
genNode(node.content, context);
push(")");
}
function genModulePreamble(ast, context) {
const { push, newline, runtimeModuleName } = context;
if (ast.helpers.length) {
const code = `import {${ast.helpers
.map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(", ")} } from ${JSON.stringify(runtimeModuleName)}`;
push(code);
}
newline();
push(`export `);
}
function createCodegenContext(ast, { runtimeModuleName = "vue", runtimeGlobalName = "Vue", mode = "function" }) {
const context = {
code: "",
mode,
runtimeModuleName,
runtimeGlobalName,
helper(key) {
return `_${helperNameMap[key]}`;
},
push(code) {
context.code += code;
},
newline() {
context.code += "\n";
},
};
return context;
}
var TagType;
(function (TagType) {
TagType[TagType["Start"] = 0] = "Start";
TagType[TagType["End"] = 1] = "End";
})(TagType || (TagType = {}));
function baseParse(content) {
const context = createParserContext(content);
return createRoot(parseChildren(context, []));
}
function createParserContext(content) {
console.log("创建 paserContext");
return {
source: content,
};
}
function parseChildren(context, ancestors) {
console.log("开始解析 children");
const nodes = [];
while (!isEnd(context, ancestors)) {
let node;
const s = context.source;
if (startsWith(s, "{{")) {
node = parseInterpolation(context);
}
else if (s[0] === "<") {
if (s[1] === "/") {
if (/[a-z]/i.test(s[2])) {
parseTag(context, 1);
continue;
}
}
else if (/[a-z]/i.test(s[1])) {
node = parseElement(context, ancestors);
}
}
if (!node) {
node = parseText(context);
}
nodes.push(node);
}
return nodes;
}
function isEnd(context, ancestors) {
const s = context.source;
if (context.source.startsWith("")) {
for (let i = ancestors.length - 1; i >= 0; --i) {
if (startsWithEndTagOpen(s, ancestors[i].tag)) {
return true;
}
}
}
return !context.source;
}
function parseElement(context, ancestors) {
const element = parseTag(context, 0);
ancestors.push(element);
const children = parseChildren(context, ancestors);
ancestors.pop();
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, 1);
}
else {
throw new Error(`缺失结束标签:${element.tag}`);
}
element.children = children;
return element;
}
function startsWithEndTagOpen(source, tag) {
return (startsWith(source, "") &&
source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase());
}
function parseTag(context, type) {
const match = /^<\/?([a-z][^\r\n\t\f />]*)/i.exec(context.source);
const tag = match[1];
advanceBy(context, match[0].length);
advanceBy(context, 1);
if (type === 1)
return;
let tagType = 0;
return {
type: 4,
tag,
tagType,
};
}
function parseInterpolation(context) {
const openDelimiter = "{{";
const closeDelimiter = "}}";
const closeIndex = context.source.indexOf(closeDelimiter, openDelimiter.length);
advanceBy(context, 2);
const rawContentLength = closeIndex - openDelimiter.length;
const rawContent = context.source.slice(0, rawContentLength);
const preTrimContent = parseTextData(context, rawContent.length);
const content = preTrimContent.trim();
advanceBy(context, closeDelimiter.length);
return {
type: 2,
content: {
type: 3,
content,
},
};
}
function parseText(context) {
console.log("解析 text", context);
const endTokens = ["<", "{{"];
let endIndex = context.source.length;
for (let i = 0; i < endTokens.length; i++) {
const index = context.source.indexOf(endTokens[i]);
if (index !== -1 && endIndex > index) {
endIndex = index;
}
}
const content = parseTextData(context, endIndex);
return {
type: 0,
content,
};
}
function parseTextData(context, length) {
console.log("解析 textData");
const rawText = context.source.slice(0, length);
advanceBy(context, length);
return rawText;
}
function advanceBy(context, numberOfCharacters) {
console.log("推进代码", context, numberOfCharacters);
context.source = context.source.slice(numberOfCharacters);
}
function createRoot(children) {
return {
type: 1,
children,
helpers: [],
};
}
function startsWith(source, searchString) {
return source.startsWith(searchString);
}
function transform(root, options = {}) {
const context = createTransformContext(root, options);
traverseNode(root, context);
createRootCodegen(root);
root.helpers.push(...context.helpers.keys());
}
function traverseNode(node, context) {
const type = node.type;
const nodeTransforms = context.nodeTransforms;
const exitFns = [];
for (let i = 0; i < nodeTransforms.length; i++) {
const transform = nodeTransforms[i];
const onExit = transform(node, context);
if (onExit) {
exitFns.push(onExit);
}
}
switch (type) {
case 2:
context.helper(TO_DISPLAY_STRING);
break;
case 1:
case 4:
traverseChildren(node, context);
break;
}
let i = exitFns.length;
while (i--) {
exitFns[i]();
}
}
function traverseChildren(parent, context) {
parent.children.forEach((node) => {
traverseNode(node, context);
});
}
function createTransformContext(root, options) {
const context = {
root,
nodeTransforms: options.nodeTransforms || [],
helpers: new Map(),
helper(name) {
const count = context.helpers.get(name) || 0;
context.helpers.set(name, count + 1);
},
};
return context;
}
function createRootCodegen(root, context) {
const { children } = root;
const child = children[0];
if (child.type === 4 && child.codegenNode) {
const codegenNode = child.codegenNode;
root.codegenNode = codegenNode;
}
else {
root.codegenNode = child;
}
}
function transformExpression(node) {
if (node.type === 2) {
node.content = processExpression(node.content);
}
}
function processExpression(node) {
node.content = `_ctx.${node.content}`;
return node;
}
var NodeTypes;
(function (NodeTypes) {
NodeTypes[NodeTypes["TEXT"] = 0] = "TEXT";
NodeTypes[NodeTypes["ROOT"] = 1] = "ROOT";
NodeTypes[NodeTypes["INTERPOLATION"] = 2] = "INTERPOLATION";
NodeTypes[NodeTypes["SIMPLE_EXPRESSION"] = 3] = "SIMPLE_EXPRESSION";
NodeTypes[NodeTypes["ELEMENT"] = 4] = "ELEMENT";
NodeTypes[NodeTypes["COMPOUND_EXPRESSION"] = 5] = "COMPOUND_EXPRESSION";
})(NodeTypes || (NodeTypes = {}));
var ElementTypes;
(function (ElementTypes) {
ElementTypes[ElementTypes["ELEMENT"] = 0] = "ELEMENT";
})(ElementTypes || (ElementTypes = {}));
function createVNodeCall(context, tag, props, children) {
if (context) {
context.helper(CREATE_ELEMENT_VNODE);
}
return {
type: 4,
tag,
props,
children,
};
}
function transformElement(node, context) {
if (node.type === 4) {
return () => {
const vnodeTag = `'${node.tag}'`;
const vnodeProps = null;
let vnodeChildren = null;
if (node.children.length > 0) {
if (node.children.length === 1) {
const child = node.children[0];
vnodeChildren = child;
}
}
node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren);
};
}
}
function isText(node) {
return node.type === 2 || node.type === 0;
}
function transformText(node, context) {
if (node.type === 4) {
return () => {
const children = node.children;
let currentContainer;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (isText(child)) {
for (let j = i + 1; j < children.length; j++) {
const next = children[j];
if (isText(next)) {
if (!currentContainer) {
currentContainer = children[i] = {
type: 5,
loc: child.loc,
children: [child],
};
}
currentContainer.children.push(` + `, next);
children.splice(j, 1);
j--;
}
else {
currentContainer = undefined;
break;
}
}
}
}
};
}
}
function baseCompile(template, options) {
const ast = baseParse(template);
transform(ast, Object.assign(options, {
nodeTransforms: [transformElement, transformText, transformExpression],
}));
return generate(ast);
}
function compileToFunction(template, options = {}) {
const { code } = baseCompile(template, options);
const render = new Function("Vue", code)(runtimeDom);
return render;
}
registerRuntimeCompiler(compileToFunction);
export { computed, createApp, createAppAPI, createVNode as createElementVNode, createRenderer, createTextVNode, effect, getCurrentInstance, h, inject, isProxy, isReactive, isReadonly, isRef, provide, proxyRefs, reactive, readonly, ref, registerRuntimeCompiler, renderSlot, shallowReadonly, stop, toDisplayString, unRef, watchEffect };
//# sourceMappingURL=mini-vue.esm-bundler.js.map
================================================
FILE: packages/vue/example/apiInject/App.js
================================================
// 组件 provide 和 inject 功能
import {
h,
provide,
inject,
} from "../../dist/mini-vue.esm-bundler.js";
const ProviderOne = {
setup() {
provide("foo", "foo");
provide("bar", "bar");
return () => h(ProviderTwo);
},
};
const ProviderTwo = {
setup() {
// override parent value
provide("foo", "fooOverride");
provide("baz", "baz");
const foo = inject("foo");
// 这里获取的 foo 的值应该是 "foo"
// 这个组件的子组件获取的 foo ,才应该是 fooOverride
if (foo !== "foo") {
throw new Error("Foo should equal to foo");
}
return () => h(Consumer);
},
};
const Consumer = {
setup() {
const foo = inject("foo");
const bar = inject("bar");
const baz = inject("baz");
return () => {
return h("div", {}, `${foo}-${bar}-${baz}`);
};
},
};
export default {
name: "App",
setup() {
return () => h("div", {}, [h("p", {}, "apiInject"), h(ProviderOne)]);
},
};
================================================
FILE: packages/vue/example/apiInject/index.html
================================================
Document
================================================
FILE: packages/vue/example/compiler-base/App.js
================================================
// 最简单的情况
// template 只有一个 interpolation
// export default {
// template: `{{msg}}`,
// setup() {
// return {
// msg: "vue3 - compiler",
// };
// },
// };
// 复杂一点
// template 包含 element 和 interpolation
export default {
template: `{{msg}}
`,
setup() {
return {
msg: "vue3 - compiler",
};
},
};
================================================
FILE: packages/vue/example/compiler-base/index.html
================================================
Document
================================================
FILE: packages/vue/example/componentEmit/App.js
================================================
// 组件 emit 逻辑 demo
// click emit 发出 change, 可以触发 App 组件内定义好的侦听函数
// 允许接收多个参数
import { h, ref, reactive } from "../../dist/mini-vue.esm-bundler.js";
import Child from "./Child.js";
export default {
name: "App",
setup() {},
render() {
return h("div", {}, [
h("div", {}, "你好"),
h(Child, {
msg: "your name is child",
onChange(a, b) {
console.log("---------------change------------------");
console.log(a, b);
},
onChangePageName(a, b) {
console.log("---------------change-page-name------------------");
console.log(a, b);
},
}),
]);
},
};
================================================
FILE: packages/vue/example/componentEmit/Child.js
================================================
import { h, ref, reactive } from "../../dist/mini-vue.esm-bundler.js";
export default {
name: "Child",
setup(props, { emit }) {
emit("change", "aaaaa", "bbbbbb");
// 支持多个 -
emit("change-page-name", "start", "game");
},
render() {
return h("div", {}, "child");
},
};
================================================
FILE: packages/vue/example/componentEmit/index.html
================================================
Document
================================================
FILE: packages/vue/example/componentProxy/App.js
================================================
// 在 render 中使用 proxy 调用 emit 函数
// 也可以直接使用 this
// 验证 proxy 的实现逻辑
import { h, ref, reactive } from "../../dist/mini-vue.esm-bundler.js";
import Child from "./Child.js";
export default {
name: "App",
setup() {},
render() {
return h("div", {}, [
h("div", {}, "你好"),
h(Child, {
msg: "your name is child",
onChange(a, b) {
console.log("---------------change------------------");
console.log(a, b);
},
}),
]);
},
};
================================================
FILE: packages/vue/example/componentProxy/Child.js
================================================
import { h, ref, reactive } from "../../dist/mini-vue.esm-bundler.js";
export default {
name: "Child",
setup(props, { emit }) {},
render(proxy) {
const self = this
return h("div", {}, [
h(
"button",
{
onClick() {
console.log(proxy);
console.log("click");
proxy.$emit("change", "aaa", "bbbb");
// 使用 this
console.log(this)
self.$emit("change", "ccc", "ddd");
},
},
"emit"
),
]);
},
};
================================================
FILE: packages/vue/example/componentProxy/index.html
================================================
Document
================================================
FILE: packages/vue/example/componentSlots/App.js
================================================
import { h, ref, reactive } from "../../dist/mini-vue.esm-bundler.js";
import Child from "./Child.js";
export default {
name: "App",
setup() {},
render() {
return h("div", {}, [
h("div", {}, "你好"),
h(
Child,
{
msg: "your name is child",
},
{
default: ({ age }) => [
h("p", {}, "我是通过 slot 渲染出来的第一个元素 "),
h("p", {}, "我是通过 slot 渲染出来的第二个元素"),
h("p", {}, `我可以接收到 age: ${age}`),
],
}
),
]);
},
};
================================================
FILE: packages/vue/example/componentSlots/Child.js
================================================
import { h, ref, reactive, renderSlot } from "../../dist/mini-vue.esm-bundler.js";
export default {
name: "Child",
setup(props, context) {},
render() {
return h("div", {"data-test":"child"}, [
h("div", {}, "child"),
// renderSlot 会返回一个 vnode
// 其本质和 h 是一样的
// 第三个参数给出数据
renderSlot(this.$slots, "default", {
age: 16,
}),
]);
},
};
================================================
FILE: packages/vue/example/componentSlots/index.html
================================================
Document
================================================
FILE: packages/vue/example/componentUpdate/App.js
================================================
// 在 render 中使用 proxy 调用 emit 函数
// 也可以直接使用 this
// 验证 proxy 的实现逻辑
import { h, ref } from "../../dist/mini-vue.esm-bundler.js";
import Child from "./Child.js";
export default {
name: "App",
setup() {
const msg = ref("123");
window.msg = msg
const changeChildProps = () => {
msg.value = "456";
};
return { msg, changeChildProps };
},
render() {
return h("div", {}, [
h("div", {}, "你好"),
h(
"button",
{
onClick: this.changeChildProps,
},
"change child props"
),
h(Child, {
msg: this.msg,
}),
]);
},
};
================================================
FILE: packages/vue/example/componentUpdate/Child.js
================================================
import { h, ref, reactive } from "../../dist/mini-vue.esm-bundler.js";
export default {
name: "Child",
setup(props, { emit }) {},
render(proxy) {
return h("div", {}, [h("div", {}, "child" + this.$props.msg)]);
},
};
================================================
FILE: packages/vue/example/componentUpdate/index.html
================================================
Document
================================================
FILE: packages/vue/example/createTextVnode/App.js
================================================
import { h, ref, reactive, createTextVNode } from "../../dist/mini-vue.esm-bundler.js";
export default {
name: "App",
setup() {},
render() {
return h("div", {}, [
h("div", {}, "你好"),
createTextVNode("这是通过 createTextVNode 创建的节点"),
]);
},
};
================================================
FILE: packages/vue/example/createTextVnode/index.html
================================================
Document
================================================
FILE: packages/vue/example/customRenderer/App.js
================================================
import { h, ref } from "../../dist/mini-vue.esm-bundler.js";
import { game } from "./game.js";
export default {
name: "App",
setup() {
// 通过 ticker 来去更新 x 的值
const x = ref(0);
const y = ref(0);
let dir = 1;
const speed = 2;
game.ticker.add(() => {
if (x.value > 400) {
dir = -1;
} else if (x.value < 0) {
dir = 1;
}
x.value += speed * dir;
});
return {
x,
y,
};
},
render() {
return h("rect", { x: this.x, y: this.y });
},
};
================================================
FILE: packages/vue/example/customRenderer/game.js
================================================
export const game = new PIXI.Application({
width: 500,
height: 500,
});
document.body.append(game.view);
export function createRootContainer() {
return game.stage;
}
================================================
FILE: packages/vue/example/customRenderer/index.html
================================================
Document
================================================
FILE: packages/vue/example/customRenderer/main.js
================================================
import App from "./App.js";
import { createApp } from "./renderer.js";
import { createRootContainer } from "./game.js";
createApp(App).mount(createRootContainer());
================================================
FILE: packages/vue/example/customRenderer/renderer.js
================================================
import { createRenderer } from "../../dist/mini-vue.esm-bundler.js";
// 给基于 pixi.js 的渲染函数
const renderer = createRenderer({
createElement(type) {
const rect = new PIXI.Graphics();
rect.beginFill(0xff0000);
rect.drawRect(0, 0, 100, 100);
rect.endFill();
return rect;
},
patchProp(el, key, prevValue, nextValue) {
el[key] = nextValue;
},
insert(el, parent) {
parent.addChild(el);
},
});
export function createApp(rootComponent) {
return renderer.createApp(rootComponent);
}
================================================
FILE: packages/vue/example/getCurrentInstance/App.js
================================================
// 可以在 setup 中使用 getCurrentInstance 获取组件实例对象
import { h, getCurrentInstance } from "../../dist/mini-vue.esm-bundler.js";
export default {
name: "App",
setup() {
console.log(getCurrentInstance())
return () => h("div", {}, [h("p", {}, "getCurrentInstance")]);
},
};
================================================
FILE: packages/vue/example/getCurrentInstance/index.html
================================================
Document
================================================
FILE: packages/vue/example/helloWorld/App.js
================================================
import { h, ref } from "../../dist/mini-vue.esm-bundler.js";
const count = ref(0);
const HelloWorld = {
name: "HelloWorld",
setup() {},
// TODO 第一个小目标
// 可以在使用 template 只需要有一个插值表达式即
// 可以解析 tag 标签
// template: `
// hi {{msg}}
// 需要编译成 render 函数
// `,
render() {
return h(
"div",
{ tId: "helloWorld" },
`hello world: count: ${count.value}`
);
},
};
export default {
name: "App",
setup() {},
render() {
return h("div", { tId: 1 }, [h("p", {}, "主页"), h(HelloWorld)]);
},
};
================================================
FILE: packages/vue/example/helloWorld/index.html
================================================
Document
================================================
FILE: packages/vue/example/helloWorld/main.js
================================================
import { createApp } from "../../dist/mini-vue.esm-bundler.js";
import App from "./App.js";
const rootContainer = document.querySelector("#root");
createApp(App).mount(rootContainer);
================================================
FILE: packages/vue/example/nextTicker/App.js
================================================
import { h, ref, reactive } from "../../dist/mini-vue.esm-bundler.js";
import NextTicker from "./NextTicker.js";
export default {
name: "App",
setup() {},
render() {
return h("div", { tId: 1 }, [h("p", {}, "主页"), h(NextTicker)]);
},
};
================================================
FILE: packages/vue/example/nextTicker/NextTicker.js
================================================
// 测试 nextTick 逻辑
import { h, ref } from "../../dist/mini-vue.esm-bundler.js";
// 如果 for 循环改变 count 的值 100 次的话
// 会同时触发 100 次的 update 页面逻辑
// 这里可以把 update 页面的逻辑放到微任务中执行
// 避免更改了响应式对象就会执行 update 的逻辑
// 因为只有最后一次调用 update 才是有价值的
window.count = ref(1);
// 如果一个响应式变量同时触发了两个组件的 update
// 会发生什么有趣的事呢?
const Child1 = {
name: "NextTickerChild1",
setup() {},
render() {
return h("div", {}, `child1 count:${window.count.value}`);
},
};
const Child2 = {
name: "NextTickerChild2",
setup() {},
render() {
return h("div", {}, `child2 count:${window.count.value}`);
},
};
export default {
name: "NextTicker",
setup() {},
render() {
return h(
"div",
{ tId: "nextTicker" },
[h(Child1), h(Child2)]
// `for nextTick: count: ${window.count.value}`
);
},
};
================================================
FILE: packages/vue/example/nextTicker/index.html
================================================
Document
================================================
FILE: packages/vue/example/nextTicker/main.js
================================================
import {createApp} from "../../dist/mini-vue.esm-bundler.js";
import App from "./App.js";
const rootContainer = document.querySelector("#root");
createApp(App).mount(rootContainer);
================================================
FILE: packages/vue/example/patchChildren/App.js
================================================
import {h} from '../../dist/mini-vue.esm-bundler.js'
import ArrayToText from "./ArrayToText.js";
import TextToText from "./TextToText.js";
import TextToArray from "./TextToArray.js";
import ArrayToArray from "./ArrayToArray.js";
export default {
name: "App",
setup() {},
render() {
return h("div", { tId: 1 }, [
h("p", {}, "主页"),
// 老的是 array 新的是 text
// h(ArrayToText),
// 老的是 text 新的是 text
// h(TextToText),
// 老的是 text 新的是 array
h(TextToArray)
// 老的是 array 新的是 array
// h(ArrayToArray),
]);
},
};
================================================
FILE: packages/vue/example/patchChildren/ArrayToArray.js
================================================
// 老的是 array
// 新的是 array
import { ref, h } from "../../dist/mini-vue.esm-bundler.js";
// 1. 左侧的对比
// (a b) c
// (a b) d e
// const prevChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "C" }, "C"),
// ];
// const nextChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "D" }, "D"),
// h("p", { key: "E" }, "E"),
// ];
// 2. 右侧的对比
// a (b c)
// d e (b c)
// const prevChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "C" }, "C"),
// ];
// const nextChildren = [
// h("p", { key: "D" }, "D"),
// h("p", { key: "E" }, "E"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "C" }, "C"),
// ];
// 3. 新的比老的长
// 创建新的
// 左侧
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
// const nextChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "C" }, "C"),
// h("p", { key: "D" }, "D"),
// ];
// 右侧
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
// const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
// const nextChildren = [
// h("p", { key: "C" }, "C"),
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// ];
// 4. 老的比新的长
// 删除老的
// 左侧
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// const prevChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "C" }, "C"),
// ];
// const nextChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
// 右侧
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
// const prevChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "C" }, "C"),
// ];
// const nextChildren = [h("p", { key: "B" }, "B"), h("p", { key: "C" }, "C")];
// 5. 对比中间的部分
// 删除老的 (在老的里面存在,新的里面不存在)
// 5.1
// a,b,(c,d),f,g
// a,b,(e,c),f,g
// D 节点在新的里面是没有的 - 需要删除掉
// C 节点 props 也发生了变化
// const prevChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "C", id: "c-prev" }, "C"),
// h("p", { key: "D" }, "D"),
// h("p", { key: "F" }, "F"),
// h("p", { key: "G" }, "G"),
// ];
// const nextChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "E" }, "E"),
// h("p", { key: "C", id:"c-next" }, "C"),
// h("p", { key: "F" }, "F"),
// h("p", { key: "G" }, "G"),
// ];
// 5.1.1
// a,b,(c,e,d),f,g
// a,b,(e,c),f,g
// 中间部分,老的比新的多, 那么多出来的直接就可以被干掉(优化删除逻辑)
// const prevChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "C", id: "c-prev" }, "C"),
// h("p", { key: "E" }, "E"),
// h("p", { key: "D" }, "D"),
// h("p", { key: "F" }, "F"),
// h("p", { key: "G" }, "G"),
// ];
// const nextChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "E" }, "E"),
// h("p", { key: "C", id:"c-next" }, "C"),
// h("p", { key: "F" }, "F"),
// h("p", { key: "G" }, "G"),
// ];
// 2 移动 (节点存在于新的和老的里面,但是位置变了)
// 2.1
// a,b,(c,d,e),f,g
// a,b,(e,c,d),f,g
// 最长子序列: [1,2]
// const prevChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "C" }, "C"),
// h("p", { key: "D" }, "D"),
// h("p", { key: "E" }, "E"),
// h("p", { key: "F" }, "F"),
// h("p", { key: "G" }, "G"),
// ];
// const nextChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "E" }, "E"),
// h("p", { key: "C" }, "C"),
// h("p", { key: "D" }, "D"),
// h("p", { key: "F" }, "F"),
// h("p", { key: "G" }, "G"),
// ];
// 3. 创建新的节点
// a,b,(c,e),f,g
// a,b,(e,c,d),f,g
// d 节点在老的节点中不存在,新的里面存在,所以需要创建
// const prevChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "C" }, "C"),
// h("p", { key: "E" }, "E"),
// h("p", { key: "F" }, "F"),
// h("p", { key: "G" }, "G"),
// ];
// const nextChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "E" }, "E"),
// h("p", { key: "C" }, "C"),
// h("p", { key: "D" }, "D"),
// h("p", { key: "F" }, "F"),
// h("p", { key: "G" }, "G"),
// ];
// 综合例子
// a,b,(c,d,e,z),f,g
// a,b,(d,c,y,e),f,g
// const prevChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "C" }, "C"),
// h("p", { key: "D" }, "D"),
// h("p", { key: "E" }, "E"),
// h("p", { key: "Z" }, "Z"),
// h("p", { key: "F" }, "F"),
// h("p", { key: "G" }, "G"),
// ];
// const nextChildren = [
// h("p", { key: "A" }, "A"),
// h("p", { key: "B" }, "B"),
// h("p", { key: "D" }, "D"),
// h("p", { key: "C" }, "C"),
// h("p", { key: "Y" }, "Y"),
// h("p", { key: "E" }, "E"),
// h("p", { key: "F" }, "F"),
// h("p", { key: "G" }, "G"),
// ];
// fix c 节点应该是 move 而不是删除之后重新创建的
const prevChildren = [
h("p", { key: "A" }, "A"),
h("p", {}, "C"),
h("p", { key: "B" }, "B"),
h("p", { key: "D" }, "D"),
];
const nextChildren = [
h("p", { key: "A" }, "A"),
h("p", { key: "B" }, "B"),
h("p", {}, "C"),
h("p", { key: "D" }, "D"),
];
export default {
name: "ArrayToArray",
setup() {
const isChange = ref(false);
window.isChange = isChange;
return {
isChange,
};
},
render() {
const self = this;
return self.isChange === true
? h("div", {}, nextChildren)
: h("div", {}, prevChildren);
},
};
================================================
FILE: packages/vue/example/patchChildren/ArrayToText.js
================================================
// 老的是 array
// 新的是 text
import { ref, h } from "../../dist/mini-vue.esm-bundler.js";
const nextChildren = "newChildren";
const prevChildren = [h("div", {}, "A"), h("div", {}, "B")];
export default {
name: "ArrayToText",
setup() {
const isChange = ref(false);
window.isChange = isChange;
return {
isChange,
};
},
render() {
const self = this;
return self.isChange === true
? h("div", {}, nextChildren)
: h("div", {}, prevChildren);
},
};
================================================
FILE: packages/vue/example/patchChildren/TextToArray.js
================================================
// 新的是 array
// 老的是 text
import { ref, h } from "../../dist/mini-vue.esm-bundler.js";
const prevChildren = "oldChild";
const nextChildren = [h("div", {}, "A"), h("div", {}, "B")];
export default {
name: "TextToArray",
setup() {
const isChange = ref(false);
window.isChange = isChange;
return {
isChange,
};
},
render() {
const self = this;
console.log("?????????")
return self.isChange === true
? h("div", {}, nextChildren)
: h("div", {}, prevChildren);
},
};
================================================
FILE: packages/vue/example/patchChildren/TextToText.js
================================================
// 新的是 text
// 老的是 text
import { ref, h } from "../../dist/mini-vue.esm-bundler.js";
const prevChildren = "oldChild";
const nextChildren = "newChild";
export default {
name: "TextToText",
setup() {
const isChange = ref(false);
window.isChange = isChange;
return {
isChange,
};
},
render() {
const self = this;
return self.isChange === true
? h("div", {}, nextChildren)
: h("div", {}, prevChildren);
},
};
================================================
FILE: packages/vue/example/patchChildren/index.html
================================================
Document
================================================
FILE: packages/vue/example/patchChildren/main.js
================================================
import { createApp } from "../../dist/mini-vue.esm-bundler.js";
import App from "./App.js";
const rootContainer = document.querySelector("#root");
createApp(App).mount(rootContainer);
================================================
FILE: packages/vue/example/renderComponent/App.js
================================================
import { h, ref, reactive } from "../../dist/mini-vue.esm-bundler.js";
import Child from "./Child.js";
export default {
name: "App",
setup() {},
render() {
return h("div", {}, [
h("div", {}, "你好"),
h(Child, {
msg: "your name is child",
}),
]);
},
};
================================================
FILE: packages/vue/example/renderComponent/Child.js
================================================
import { h, ref, reactive } from "../../dist/mini-vue.esm-bundler.js";
export default {
name: "Child",
setup(props, context) {
console.log("props------------------>", props);
console.log("context---------------->", context);
},
render() {
return h("div", {}, "child");
},
};
================================================
FILE: packages/vue/example/renderComponent/index.html
================================================
Document
================================================
FILE: packages/vue/example/setupStateRenderComponent/App.js
================================================
// 在 render 中可以通过 this.xxx 访问到 setup 返回的对象
import { h, ref, reactive } from "../../dist/mini-vue.esm-bundler.js";
export default {
name: "App",
setup() {
const count = ref(0);
const handleClick = () => {
console.log("click");
count.value++;
};
return {
count,
handleClick,
};
},
render() {
console.log(this.count);
return h("div", {}, [
h("div", {}, String(this.count)),
h("button", { onClick: this.handleClick }, "click"),
]);
},
};
================================================
FILE: packages/vue/example/setupStateRenderComponent/index.html
================================================
Document
================================================
FILE: packages/vue/index.js
================================================
'use strict'
if (process.env.NODE_ENV === 'production') {
// module.exports = require('./dist/mini-vue.cjs.prod.js')
} else {
module.exports = require('./dist/mini-vue.cjs.js')
}
================================================
FILE: packages/vue/package.json
================================================
{
"name": "mini-vue",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "cypress open",
"serve": "serve ."
},
"keywords": [],
"author": "cuixiaorui",
"license": "ISC",
"dependencies": {
"@mini-vue/compiler-core": "workspace:^1.0.0",
"@mini-vue/runtime-dom": "workspace:^1.0.0",
"@mini-vue/shared": "workspace:^1.0.0"
},
"devDependencies": {
"cypress": "^11.0.1",
"serve": "^14.1.1"
}
}
================================================
FILE: packages/vue/src/index.ts
================================================
// 这个文件充当 vue 模块
import * as runtimeDom from "@mini-vue/runtime-dom";
import { registerRuntimeCompiler } from "@mini-vue/runtime-dom";
import { baseCompile } from "@mini-vue/compiler-core";
export * from "@mini-vue/runtime-dom";
function compileToFunction(template, options = {}) {
const { code } = baseCompile(template, options);
// 调用 compile 得到的代码在给封装到函数内,
// 这里会依赖 runtimeDom 的一些函数,所以在这里通过参数的形式注入进去
const render = new Function("Vue", code)(runtimeDom);
return render;
}
registerRuntimeCompiler(compileToFunction);
================================================
FILE: pnpm-workspace.yaml
================================================
packages:
- 'packages/*'
================================================
FILE: rollup.config.js
================================================
import typescript from "@rollup/plugin-typescript";
import sourceMaps from "rollup-plugin-sourcemaps";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import replace from "@rollup/plugin-replace";
export default {
input:"./packages/vue/src/index.ts",
plugins: [
replace({
"process.env.NODE_ENV": JSON.stringify("development"),
"process.env.VUE_ENV": JSON.stringify("browser"),
"process.env.LANGUAGE": JSON.stringify(process.env.LANGUAGE),
}),
resolve(),
commonjs(),
typescript(),
sourceMaps(),
],
output: [
{
format: "cjs",
file: "./packages/vue/dist/mini-vue.cjs.js",
sourcemap: true,
},
{
name: "vue",
format: "es",
file: "./packages/vue/dist/mini-vue.esm-bundler.js",
sourcemap: true,
},
],
onwarn: (msg, warn) => {
// 忽略 Circular 的错误
if (!/Circular/.test(msg)) {
warn(msg);
}
},
};
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"strict": true,
"rootDir": ".",
"moduleResolution": "node",
"esModuleInterop": true,
"target": "es2016",
"module": "esnext",
"noImplicitAny": false,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"downlevelIteration": true,
"lib": ["esnext", "DOM"],
"types": ["vitest/globals"],
"paths": {
"@mini-vue/*": ["packages/*/src"]
}
},
"include": ["packages/*/src", "packages/*/__tests__"]
}
================================================
FILE: vitest.config.ts
================================================
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
globals: true,
},
resolve: {
alias: [
{
find: /@mini-vue\/([\w-]*)/,
replacement: path.resolve(__dirname, "packages") + "/$1/src",
},
],
},
});