├── README.md
├── tiny-react
├── README.md
├── class01
│ ├── .gitignore
│ ├── App.jsx
│ ├── README.md
│ ├── core
│ │ ├── React.js
│ │ └── ReactDom.js
│ ├── index.html
│ ├── main.jsx
│ ├── package.json
│ └── pnpm-lock.yaml
├── class02
│ ├── .gitignore
│ ├── README.md
│ └── demo
│ │ ├── index.html
│ │ ├── index.js
│ │ └── main.js
├── class03
│ ├── .gitignore
│ ├── App.jsx
│ ├── README.md
│ ├── core
│ │ ├── React.js
│ │ └── ReactDOM.js
│ ├── index.html
│ ├── main.jsx
│ ├── package.json
│ └── pnpm-lock.yaml
└── class04
│ ├── .gitignore
│ ├── App.jsx
│ ├── README.md
│ ├── core
│ ├── React.js
│ └── ReactDOM.js
│ ├── index.html
│ ├── main.jsx
│ ├── package.json
│ └── pnpm-lock.yaml
├── tiny-vue3
├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── packages
│ ├── compiler-core
│ │ ├── __test__
│ │ │ ├── __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
│ │ │ ├── effect.spec.ts
│ │ │ ├── reactive.spec.ts
│ │ │ ├── readonly.spec.ts
│ │ │ ├── ref.spec.ts
│ │ │ └── shallowReadonly.spec.ts
│ │ ├── package.json
│ │ └── src
│ │ │ ├── baseHandlers.ts
│ │ │ ├── computed.ts
│ │ │ ├── effect.ts
│ │ │ ├── index.ts
│ │ │ ├── reactive.ts
│ │ │ └── ref.ts
│ ├── runtime-core
│ │ ├── __tests__
│ │ │ └── apiWatch.spec.ts
│ │ ├── package.json
│ │ └── src
│ │ │ ├── apiInject.ts
│ │ │ ├── apiWatch.ts
│ │ │ ├── component.ts
│ │ │ ├── componentEmit.ts
│ │ │ ├── componentProps.ts
│ │ │ ├── componentPublicInstance.ts
│ │ │ ├── componentSlots.ts
│ │ │ ├── componentUpdateUtils.ts
│ │ │ ├── createApp.ts
│ │ │ ├── h.ts
│ │ │ ├── helpers
│ │ │ └── renderSlots.ts
│ │ │ ├── index.ts
│ │ │ ├── renderer.ts
│ │ │ ├── scheduler.ts
│ │ │ └── vnode.ts
│ ├── runtime-dom
│ │ ├── package.json
│ │ └── src
│ │ │ └── index.ts
│ ├── shared
│ │ ├── package.json
│ │ └── src
│ │ │ ├── ShapeFlags.ts
│ │ │ ├── demo.ts
│ │ │ ├── index.ts
│ │ │ └── toDisplayString.ts
│ └── vue
│ │ ├── examples
│ │ ├── apiInject
│ │ │ ├── App.js
│ │ │ └── index.html
│ │ ├── compiler-base
│ │ │ ├── App.js
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ ├── componentEmit
│ │ │ ├── App.js
│ │ │ ├── Foo.js
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ ├── componentSlot
│ │ │ ├── App.js
│ │ │ ├── Foo.js
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ ├── componentUpdate
│ │ │ ├── App.js
│ │ │ ├── Child.js
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ ├── currentInstance
│ │ │ ├── App.js
│ │ │ ├── Foo.js
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ ├── customRenderer
│ │ │ ├── App.js
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ ├── helloworld
│ │ │ ├── App.js
│ │ │ ├── Foo.js
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ ├── nextTicker
│ │ │ ├── App.js
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ ├── patchChildren
│ │ │ ├── App.js
│ │ │ ├── ArrayToArray.js
│ │ │ ├── ArrayToText.js
│ │ │ ├── TextToArray.js
│ │ │ ├── TextToText.js
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ └── update
│ │ │ ├── App.js
│ │ │ ├── index.html
│ │ │ └── main.js
│ │ ├── package.json
│ │ └── src
│ │ └── index.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── rollup.config.js
├── tsconfig.json
├── vitest.config.ts
└── yarn.lock
└── tiny-webpack
└── index.html
/README.md:
--------------------------------------------------------------------------------
1 | # tiny-series
2 |
3 | 关于 web 前端技术的一些实践。
4 |
5 | ## tiny-react
6 |
7 | 关于 react 一些基本原理的实现
8 |
--------------------------------------------------------------------------------
/tiny-react/README.md:
--------------------------------------------------------------------------------
1 | # 实现统一提交和 FunctionComponent
2 |
3 | ### 前言
4 |
5 | 回顾上一节,我们通过实现任务调度器 `requestIdleCallback` 利用了空余时间去完成每个 task。
6 | 通过`Firber`结构,`React` 可以灵活地遍历和操作组件树。
7 |
8 | 但是,这种方式存在一个问题,就是当中途没有空余时间时,用户可能会看到渲染一半的 dom。我们可以采用统一提交的方式去解决这个问题,先处理链表,最后再统一添加到屏幕中。
9 |
10 | ### 统一提交
11 |
12 | 其本质就是**将 DOM 的挂载操作统一放到最后执行,一次性完成所有 DOM 的挂载**。这种方式可以有效解决因某个 DOM 挂载时出现动画或任务插入导致的页面显示不完整问题,提升用户体验。
13 |
14 | #### 实现思路
15 |
16 | ##### 1. **创建挂载函数**
17 |
18 | 将挂载DOM的内容抽取到一个新的函数`commitWork(fiber)`中,用于挂载单个DOM节点。
19 |
20 | ```js
21 | // 提交任务
22 | function commitWork(fiber) {
23 | if (!fiber) return;
24 | let fiberParent = fiber.parent;
25 | // ...
26 | }
27 | ```
28 |
29 | ##### 2. **遍历 fiber 架构**
30 |
31 | 通过`commitWork`遍历 fiber 树,递归挂载所有节点。
32 |
33 | ```js
34 | // 提交任务
35 | function commitWork(fiber) {
36 | if (!fiber) return;
37 | let fiberParent = fiber.parent;
38 | // 递归挂载所有节点
39 | while (!fiberParent.dom) {
40 | fiberParent = fiberParent.parent;
41 | }
42 | if (fiber.dom) {
43 | fiberParent.dom.append(fiber.dom);
44 | }
45 | commitWork(fiber.child);
46 | commitWork(fiber.sibling);
47 | }
48 | ```
49 |
50 | ##### 3.**统一提交入口**
51 |
52 | 提供一个总的入口函数`commitRoot`,在函数中调用`commitWork`完成所有 DOM 的挂载。
53 |
54 | ```js
55 | // 任务调度
56 | function workLoop(deadline) {
57 | // 是否中断
58 | let shouldYeild = false;
59 | while (!shouldYeild && nextUnitOfWork) {
60 | nextUnitOfWork = performWorkOfUnit(nextUnitOfWork);
61 | shouldYeild = deadline.timeRemaining() < 1;
62 | }
63 | // 统一提交
64 | if (!nextUnitOfWork && root) {
65 | console.log(root);
66 | commitRoot();
67 | }
68 | // 任务放到下次执行
69 | requestIdleCallback(workLoop);
70 | }
71 | function commitRoot() {
72 | // 统一提交任务
73 | commitWork(root.child);
74 | root = null;
75 | }
76 | ```
77 |
78 | #### 优势
79 |
80 | - `统一提交`的处理可以在页面渲染前完成所有 DOM 的挂载,避免用户看到不完整的页面内容。
81 | - 通过一次性挂载所有 DOM,减少了浏览器重绘和重排的次数,提高页面渲染性能。
82 |
83 | ### FunctionComponent
84 |
85 | 自 `React 16.8` 引入 `Hooks` 以来,`函数式组件(FunctionComponent)`彻底改变了 `React` 开发的格局。Function Component 就是以 Function 的形式创建的 React 组件。它不再是简单的无状态组件,而是通过 `Hooks` 机制融合了**状态管理**、**生命周期和副作用处理**,成为 `React` 官方推荐的组件开发范式。
86 |
87 | #### 实现流程图解
88 |
89 | ```js
90 | 1. 调用函数组件 (props) → 生成 JSX
91 | |
92 | 2. 转换为 React Element(虚拟 DOM 节点)
93 | |
94 | 3. 调和阶段(Reconciliation):
95 | - 创建/更新 Fiber 节点
96 | - 处理 Hooks,记录状态和副作用到 Fiber
97 | |
98 | 4. Diff 算法 → 生成更新计划
99 | |
100 | 5. 提交阶段(Commit):
101 | - 更新 DOM
102 | - 执行副作用(useEffect)
103 | ```
104 |
105 | #### 实现思路
106 |
107 | ##### 1. 在处理 Firbe 节点时,判断节点的类型,区分函数组件和原生组件,分别处理。
108 |
109 | ```js
110 | /**
111 | * 执行当前工作单元的工作
112 | * @param {*} fiber
113 | */
114 | function performWorkOfUnit(fiber) {
115 | // 判断节点是否为函数组件
116 | const isFunctionComponent = typeof fiber.type === "function";
117 | if (isFunctionComponent) {
118 | // 函数组件
119 | updateFunctionComponent(fiber);
120 | } else {
121 | updateHostComponent(fiber);
122 | }
123 | //...
124 | }
125 | function updateFunctionComponent(fiber) {...}
126 | function updateHostComponent(fiber) {...}
127 | ```
128 |
129 | ##### 2. 递归生成链表结构
130 |
131 | -
132 | - 采用深度优先遍历:优先处理子节点,再兄弟节点,最后回溯父节点。
133 | - **流程**:子节点 → 兄弟节点 → 叔节点(父节点的兄弟),符合 Fiber 遍历顺序。
134 |
135 | 
136 |
137 | ```js
138 | function performWorkOfUnit(fiber) {
139 | // 判断节点是否为函数类型
140 | const isFunctionComponent = typeof fiber.type === "function";
141 | if (isFunctionComponent) {
142 | updateFunctionComponent(fiber);
143 | } else {
144 | updateHostComponent(fiber);
145 | }
146 |
147 | // 4. 返回下一个要执行的任务
148 | if (fiber.child) {
149 | return fiber.child;
150 | }
151 |
152 | let nextFiber = fiber;
153 | while (nextFiber) {
154 | if (nextFiber.sibling) return nextFiber.sibling;
155 | nextFiber = nextFiber.parent;
156 | }
157 | }
158 |
159 | function updateFunctionComponent(fiber) {
160 | const children = [fiber.type(fiber.props)];
161 | // 3. 转换链表,设置好指针
162 | initChildren(fiber, children);
163 | }
164 |
165 | function updateHostComponent(fiber) {
166 | if (!fiber.dom) {
167 | // 1. 创建dom
168 | const dom = (fiber.dom = createDom(fiber.type));
169 | // 2. 处理props
170 | updateProps(dom, fiber.props);
171 | }
172 |
173 | const children = fiber.props.children;
174 | // 3. 转换链表,设置好指针
175 | initChildren(fiber, children);
176 | }
177 | ```
178 |
179 | #### 优势
180 |
181 | `React` 通过 `函数直接调用`、`Fiber 架构的状态管理` 和 `Hooks 的链表顺序追踪`,实现了`函数组件`的轻量化与高效更新。相较于类组件,函数组件避免了实例化开销,更契合 React 的声明式设计理念,同时 `Hooks` 提供了灵活的状态与副作用管理能力。
182 |
183 | ### 小结
184 |
185 | - 本文主要实现将组件树转换为 `Fiber` 链表,通过迭代方式分步处理每个节点,实现可中断的渲染过程。
186 | - 实现了简易的`统一提交`和`FunctionComponent`的实现思路,省略了 `props`的特殊处理、`DOM` 创建细节和复杂子结构支持。
187 | - 展示了任务分片和链表构建的核心思想,但实际应用中需处理更多`边界情况`和`React`特性。
188 |
189 | ### 系列文章
190 |
191 | - [实现`createElement`和`render`函数](https://juejin.cn/post/7326093660705128460)
192 | - [实现`任务调度器requestIdleCallback`和简易的`Fiber`](https://juejin.cn/post/7471968780866338843)
193 |
--------------------------------------------------------------------------------
/tiny-react/class01/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | .vscode
15 |
16 | # Editor directories and files
17 | .idea
18 | .DS_Store
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
--------------------------------------------------------------------------------
/tiny-react/class01/App.jsx:
--------------------------------------------------------------------------------
1 | import React from "./core/React.js";
2 | const App = React.createElement("div", { id: "app" }, "hihihihi~ ", "hello world, my first mini react demo!");
3 | export default App;
--------------------------------------------------------------------------------
/tiny-react/class01/README.md:
--------------------------------------------------------------------------------
1 | # 实现`createElement`和`render`函数
2 |
3 | ### 前言
4 |
5 | 最近参加了 [崔效瑞老师](https://space.bilibili.com/175301983) 的 7 天 mini-react 训练营,虽然每天工作很忙,但还是抽出一两个小时跟进学习,坚持了 7 天,还是得给坚持到最后的自己点个赞!
6 |
7 | 虽然之前没有接触过 React 的源码,但是通过学习任务的拆分,将复杂模块的任务拆分成一个个小任务,小步走实现,再逐步优化代码。
8 |
9 | 时间虽短,内容却不少。先开始浅浅地记录下我的学习成果,先起个好头,希望今年能顺利更新完这个系列,给 2024 的自己先立第一个 flag!!
10 |
11 | 在这 7 天的 mini-react 训练营中,我学到了:
12 |
13 | - [x] 实现`createElement`和`render`函数
14 | - [x] 实现`任务调度器requestIdleCallback`和简易的`fiber`
15 | - [x] 实现`functionComponent`
16 | - [x] 实现`事件绑定`
17 | - [x] 实现`props`
18 | - [x] 实现`diff更新`
19 | - [x] 实现`useState`
20 | - [x] 实现`useEffect`
21 |
22 | 话不多说,进入今天的主题,先开始`React的初始化`,实现`createElement`和`render`函数
23 |
24 | ### createElement 函数
25 |
26 | `createElement` 函数是用于创建虚拟 DOM 元素的函数。它通常被 JSX 编译器所使用,用来将 JSX 语法转换为对 `React.createElement` 的调用。
27 |
28 | ```js
29 | function createTextNode(text) {
30 | return {
31 | type: "TEXT_ELEMENT",
32 | props: {
33 | nodeValue: text,
34 | children: [],
35 | },
36 | };
37 | }
38 |
39 | function createElement(type, props, ...children) {
40 | return {
41 | type,
42 | props: {
43 | ...props,
44 | children: children.map((child) => {
45 | const isTextNode = typeof child === "string" || typeof child === "number";
46 | return isTextNode ? createTextNode(child) : child;
47 | }),
48 | },
49 | };
50 | }
51 | ```
52 |
53 | `createElement` 函数接受三个参数:`type` 表示元素的类型(例如 `'div'`),`props` 表示元素的属性(例如 `{ className: 'my-class' }`),以及 `children` 表示元素的子元素。在实现中,它返回一个包含 `type` 和 `props` 属性的对象。
54 |
55 | 为了处理文本节点,我们还定义了 `createTextNode` 函数,它创建一个特殊的类型为 `'TEXT_ELEMENT'` 的对象,用于表示文本节点。
56 |
57 | 在实际使用中,React 会递归地处理这些虚拟 DOM 对象,最终将它们转换为实际的 DOM 元素并渲染到页面上。
58 |
59 | ```
60 | 注意: 上述代码只是一个简化版的实现,真实的 React 源码中应该更加复杂,并包含更多的功能和优化。
61 | ```
62 |
63 | ### render 函数
64 |
65 | `render` 函数是 React 中用于将虚拟 DOM 渲染到实际 DOM 的核心函数。
66 |
67 | ```js
68 | function render(el, container) {
69 | const dom = el.type === "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(el.type);
70 |
71 | // id class
72 | Object.keys(el.props).forEach((key) => {
73 | if (key !== "children") {
74 | dom[key] = el.props[key];
75 | }
76 | });
77 | const children = el.props.children;
78 | children.forEach((child) => {
79 | render(child, dom);
80 | });
81 | container.append(dom);
82 | }
83 | ```
84 |
85 | `render` 函数接受两个参数:`element` 表示要渲染的虚拟 DOM 元素,`container` 表示要渲染到的实际 DOM 容器。
86 | 首先,根据虚拟 DOM 的类型创建对应的实际 DOM 元素(文本节点和普通元素分别处理)。然后,将虚拟 DOM 元素的属性设置到实际 DOM 元素上,遍历子元素并递归调用 `render` 函数。最后,将创建好的实际 DOM 元素添加到容器中。
87 |
88 | ### 导出 render 函数
89 |
90 | 在`ReactDom`文件导出`render`函数之后, 调用`ReactDOM.createRoot(element).render` 函数。这样,就可以使用 React 的渲染功能了。
91 |
92 | ```js
93 | import React from "./React.js";
94 | const ReactDOM = {
95 | createRoot(container) {
96 | return {
97 | render(App) {
98 | React.render(App, container);
99 | },
100 | };
101 | },
102 | };
103 |
104 | export default ReactDOM;
105 | ```
106 |
107 | ### 示例解析
108 |
109 | #### 简单的渲染代码
110 |
111 | ```jsx
112 | function App() {
113 | return (
114 |
115 | hello world, my mini react!
116 |
befend
117 |
118 | );
119 | }
120 |
121 | export default App;
122 | ```
123 |
124 | #### 节点创建的过程内容
125 |
126 | 
127 |
128 | #### 渲染结果
129 |
130 | 
131 |
132 | ### 小结
133 |
134 | - `createElement` 和 `render` 是 React 中用于构建和渲染界面的核心函数。
135 | - 通常,`createElement` 函数用于创建虚拟 DOM 元素,通常由 JSX 编译器转换 JSX 语法时调用。而 `render` 函数将虚拟 DOM 渲染到实际 DOM。
136 |
137 | 总的来说,`createElement` 用于创建虚拟 DOM 元素,而 `render` 用于将这些虚拟 DOM 元素渲染到实际 DOM 中,从而构建用户界面。
138 |
139 | 仓库传送门: [https://github.com/Befend/mini-react](https://github.com/Befend/mini-react)
140 |
141 | ### 系列文章
142 |
143 | - [实现`createElement`和`render`函数](https://juejin.cn/post/7326093660705128460)
144 | - [实现`任务调度器requestIdleCallback`和简易的`Fiber`](https://juejin.cn/post/7471968780866338843)
145 |
--------------------------------------------------------------------------------
/tiny-react/class01/core/React.js:
--------------------------------------------------------------------------------
1 | function createTextNode(text) {
2 | return {
3 | type: "TEXT_ELEMENT",
4 | props: {
5 | nodeValue: text,
6 | children: [],
7 | },
8 | }
9 | }
10 |
11 | function createElement(type, props, ...children) {
12 | return {
13 | type,
14 | props: {
15 | ...props,
16 | children: children.map((child) => {
17 | return typeof child === "string" ? createTextNode(child) : child
18 | }),
19 | },
20 | }
21 | }
22 |
23 | function render(el, container) {
24 | const dom =
25 | el.type === "TEXT_ELEMENT"
26 | ? document.createTextNode("")
27 | : document.createElement(el.type)
28 |
29 | // id class
30 | Object.keys(el.props).forEach((key) => {
31 | if (key !== "children") {
32 | dom[key] = el.props[key]
33 | }
34 | })
35 | const children = el.props.children
36 | children.forEach((child) => {
37 | render(child, dom)
38 | })
39 |
40 | container.append(dom)
41 | }
42 |
43 | const React = {
44 | render,
45 | createElement,
46 | }
47 |
48 | export default React
49 |
--------------------------------------------------------------------------------
/tiny-react/class01/core/ReactDom.js:
--------------------------------------------------------------------------------
1 | import React from "./React.js"
2 | const ReactDOM = {
3 | createRoot(container) {
4 | return {
5 | render(App) {
6 | React.render(App, container)
7 | },
8 | }
9 | },
10 | }
11 |
12 | export default ReactDOM
13 |
--------------------------------------------------------------------------------
/tiny-react/class01/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | mini-react
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tiny-react/class01/main.jsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "./core/ReactDom.js";
2 | import App from "./App.jsx";
3 |
4 | ReactDOM.createRoot(document.querySelector("#root")).render(App);
--------------------------------------------------------------------------------
/tiny-react/class01/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mini-react",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "devDependencies": {
15 | "vite": "^5.0.8"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tiny-react/class01/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '6.1'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | devDependencies:
8 | vite:
9 | specifier: ^5.0.8
10 | version: 5.0.8
11 |
12 | packages:
13 |
14 | /@esbuild/aix-ppc64@0.19.12:
15 | resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
16 | engines: {node: '>=12'}
17 | cpu: [ppc64]
18 | os: [aix]
19 | requiresBuild: true
20 | dev: true
21 | optional: true
22 |
23 | /@esbuild/android-arm64@0.19.12:
24 | resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==}
25 | engines: {node: '>=12'}
26 | cpu: [arm64]
27 | os: [android]
28 | requiresBuild: true
29 | dev: true
30 | optional: true
31 |
32 | /@esbuild/android-arm@0.19.12:
33 | resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==}
34 | engines: {node: '>=12'}
35 | cpu: [arm]
36 | os: [android]
37 | requiresBuild: true
38 | dev: true
39 | optional: true
40 |
41 | /@esbuild/android-x64@0.19.12:
42 | resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==}
43 | engines: {node: '>=12'}
44 | cpu: [x64]
45 | os: [android]
46 | requiresBuild: true
47 | dev: true
48 | optional: true
49 |
50 | /@esbuild/darwin-arm64@0.19.12:
51 | resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==}
52 | engines: {node: '>=12'}
53 | cpu: [arm64]
54 | os: [darwin]
55 | requiresBuild: true
56 | dev: true
57 | optional: true
58 |
59 | /@esbuild/darwin-x64@0.19.12:
60 | resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==}
61 | engines: {node: '>=12'}
62 | cpu: [x64]
63 | os: [darwin]
64 | requiresBuild: true
65 | dev: true
66 | optional: true
67 |
68 | /@esbuild/freebsd-arm64@0.19.12:
69 | resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==}
70 | engines: {node: '>=12'}
71 | cpu: [arm64]
72 | os: [freebsd]
73 | requiresBuild: true
74 | dev: true
75 | optional: true
76 |
77 | /@esbuild/freebsd-x64@0.19.12:
78 | resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==}
79 | engines: {node: '>=12'}
80 | cpu: [x64]
81 | os: [freebsd]
82 | requiresBuild: true
83 | dev: true
84 | optional: true
85 |
86 | /@esbuild/linux-arm64@0.19.12:
87 | resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==}
88 | engines: {node: '>=12'}
89 | cpu: [arm64]
90 | os: [linux]
91 | requiresBuild: true
92 | dev: true
93 | optional: true
94 |
95 | /@esbuild/linux-arm@0.19.12:
96 | resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==}
97 | engines: {node: '>=12'}
98 | cpu: [arm]
99 | os: [linux]
100 | requiresBuild: true
101 | dev: true
102 | optional: true
103 |
104 | /@esbuild/linux-ia32@0.19.12:
105 | resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==}
106 | engines: {node: '>=12'}
107 | cpu: [ia32]
108 | os: [linux]
109 | requiresBuild: true
110 | dev: true
111 | optional: true
112 |
113 | /@esbuild/linux-loong64@0.19.12:
114 | resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==}
115 | engines: {node: '>=12'}
116 | cpu: [loong64]
117 | os: [linux]
118 | requiresBuild: true
119 | dev: true
120 | optional: true
121 |
122 | /@esbuild/linux-mips64el@0.19.12:
123 | resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==}
124 | engines: {node: '>=12'}
125 | cpu: [mips64el]
126 | os: [linux]
127 | requiresBuild: true
128 | dev: true
129 | optional: true
130 |
131 | /@esbuild/linux-ppc64@0.19.12:
132 | resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==}
133 | engines: {node: '>=12'}
134 | cpu: [ppc64]
135 | os: [linux]
136 | requiresBuild: true
137 | dev: true
138 | optional: true
139 |
140 | /@esbuild/linux-riscv64@0.19.12:
141 | resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==}
142 | engines: {node: '>=12'}
143 | cpu: [riscv64]
144 | os: [linux]
145 | requiresBuild: true
146 | dev: true
147 | optional: true
148 |
149 | /@esbuild/linux-s390x@0.19.12:
150 | resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==}
151 | engines: {node: '>=12'}
152 | cpu: [s390x]
153 | os: [linux]
154 | requiresBuild: true
155 | dev: true
156 | optional: true
157 |
158 | /@esbuild/linux-x64@0.19.12:
159 | resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==}
160 | engines: {node: '>=12'}
161 | cpu: [x64]
162 | os: [linux]
163 | requiresBuild: true
164 | dev: true
165 | optional: true
166 |
167 | /@esbuild/netbsd-x64@0.19.12:
168 | resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==}
169 | engines: {node: '>=12'}
170 | cpu: [x64]
171 | os: [netbsd]
172 | requiresBuild: true
173 | dev: true
174 | optional: true
175 |
176 | /@esbuild/openbsd-x64@0.19.12:
177 | resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==}
178 | engines: {node: '>=12'}
179 | cpu: [x64]
180 | os: [openbsd]
181 | requiresBuild: true
182 | dev: true
183 | optional: true
184 |
185 | /@esbuild/sunos-x64@0.19.12:
186 | resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==}
187 | engines: {node: '>=12'}
188 | cpu: [x64]
189 | os: [sunos]
190 | requiresBuild: true
191 | dev: true
192 | optional: true
193 |
194 | /@esbuild/win32-arm64@0.19.12:
195 | resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==}
196 | engines: {node: '>=12'}
197 | cpu: [arm64]
198 | os: [win32]
199 | requiresBuild: true
200 | dev: true
201 | optional: true
202 |
203 | /@esbuild/win32-ia32@0.19.12:
204 | resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==}
205 | engines: {node: '>=12'}
206 | cpu: [ia32]
207 | os: [win32]
208 | requiresBuild: true
209 | dev: true
210 | optional: true
211 |
212 | /@esbuild/win32-x64@0.19.12:
213 | resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==}
214 | engines: {node: '>=12'}
215 | cpu: [x64]
216 | os: [win32]
217 | requiresBuild: true
218 | dev: true
219 | optional: true
220 |
221 | /@rollup/rollup-android-arm-eabi@4.13.0:
222 | resolution: {integrity: sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==}
223 | cpu: [arm]
224 | os: [android]
225 | requiresBuild: true
226 | dev: true
227 | optional: true
228 |
229 | /@rollup/rollup-android-arm64@4.13.0:
230 | resolution: {integrity: sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==}
231 | cpu: [arm64]
232 | os: [android]
233 | requiresBuild: true
234 | dev: true
235 | optional: true
236 |
237 | /@rollup/rollup-darwin-arm64@4.13.0:
238 | resolution: {integrity: sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==}
239 | cpu: [arm64]
240 | os: [darwin]
241 | requiresBuild: true
242 | dev: true
243 | optional: true
244 |
245 | /@rollup/rollup-darwin-x64@4.13.0:
246 | resolution: {integrity: sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==}
247 | cpu: [x64]
248 | os: [darwin]
249 | requiresBuild: true
250 | dev: true
251 | optional: true
252 |
253 | /@rollup/rollup-linux-arm-gnueabihf@4.13.0:
254 | resolution: {integrity: sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==}
255 | cpu: [arm]
256 | os: [linux]
257 | requiresBuild: true
258 | dev: true
259 | optional: true
260 |
261 | /@rollup/rollup-linux-arm64-gnu@4.13.0:
262 | resolution: {integrity: sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==}
263 | cpu: [arm64]
264 | os: [linux]
265 | libc: [glibc]
266 | requiresBuild: true
267 | dev: true
268 | optional: true
269 |
270 | /@rollup/rollup-linux-arm64-musl@4.13.0:
271 | resolution: {integrity: sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==}
272 | cpu: [arm64]
273 | os: [linux]
274 | libc: [musl]
275 | requiresBuild: true
276 | dev: true
277 | optional: true
278 |
279 | /@rollup/rollup-linux-riscv64-gnu@4.13.0:
280 | resolution: {integrity: sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==}
281 | cpu: [riscv64]
282 | os: [linux]
283 | libc: [glibc]
284 | requiresBuild: true
285 | dev: true
286 | optional: true
287 |
288 | /@rollup/rollup-linux-x64-gnu@4.13.0:
289 | resolution: {integrity: sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==}
290 | cpu: [x64]
291 | os: [linux]
292 | libc: [glibc]
293 | requiresBuild: true
294 | dev: true
295 | optional: true
296 |
297 | /@rollup/rollup-linux-x64-musl@4.13.0:
298 | resolution: {integrity: sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==}
299 | cpu: [x64]
300 | os: [linux]
301 | libc: [musl]
302 | requiresBuild: true
303 | dev: true
304 | optional: true
305 |
306 | /@rollup/rollup-win32-arm64-msvc@4.13.0:
307 | resolution: {integrity: sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==}
308 | cpu: [arm64]
309 | os: [win32]
310 | requiresBuild: true
311 | dev: true
312 | optional: true
313 |
314 | /@rollup/rollup-win32-ia32-msvc@4.13.0:
315 | resolution: {integrity: sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==}
316 | cpu: [ia32]
317 | os: [win32]
318 | requiresBuild: true
319 | dev: true
320 | optional: true
321 |
322 | /@rollup/rollup-win32-x64-msvc@4.13.0:
323 | resolution: {integrity: sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==}
324 | cpu: [x64]
325 | os: [win32]
326 | requiresBuild: true
327 | dev: true
328 | optional: true
329 |
330 | /@types/estree@1.0.5:
331 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
332 | dev: true
333 |
334 | /esbuild@0.19.12:
335 | resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==}
336 | engines: {node: '>=12'}
337 | hasBin: true
338 | requiresBuild: true
339 | optionalDependencies:
340 | '@esbuild/aix-ppc64': 0.19.12
341 | '@esbuild/android-arm': 0.19.12
342 | '@esbuild/android-arm64': 0.19.12
343 | '@esbuild/android-x64': 0.19.12
344 | '@esbuild/darwin-arm64': 0.19.12
345 | '@esbuild/darwin-x64': 0.19.12
346 | '@esbuild/freebsd-arm64': 0.19.12
347 | '@esbuild/freebsd-x64': 0.19.12
348 | '@esbuild/linux-arm': 0.19.12
349 | '@esbuild/linux-arm64': 0.19.12
350 | '@esbuild/linux-ia32': 0.19.12
351 | '@esbuild/linux-loong64': 0.19.12
352 | '@esbuild/linux-mips64el': 0.19.12
353 | '@esbuild/linux-ppc64': 0.19.12
354 | '@esbuild/linux-riscv64': 0.19.12
355 | '@esbuild/linux-s390x': 0.19.12
356 | '@esbuild/linux-x64': 0.19.12
357 | '@esbuild/netbsd-x64': 0.19.12
358 | '@esbuild/openbsd-x64': 0.19.12
359 | '@esbuild/sunos-x64': 0.19.12
360 | '@esbuild/win32-arm64': 0.19.12
361 | '@esbuild/win32-ia32': 0.19.12
362 | '@esbuild/win32-x64': 0.19.12
363 | dev: true
364 |
365 | /fsevents@2.3.3:
366 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
367 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
368 | os: [darwin]
369 | requiresBuild: true
370 | dev: true
371 | optional: true
372 |
373 | /nanoid@3.3.7:
374 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
375 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
376 | hasBin: true
377 | dev: true
378 |
379 | /picocolors@1.0.0:
380 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
381 | dev: true
382 |
383 | /postcss@8.4.35:
384 | resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==}
385 | engines: {node: ^10 || ^12 || >=14}
386 | dependencies:
387 | nanoid: 3.3.7
388 | picocolors: 1.0.0
389 | source-map-js: 1.0.2
390 | dev: true
391 |
392 | /rollup@4.13.0:
393 | resolution: {integrity: sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==}
394 | engines: {node: '>=18.0.0', npm: '>=8.0.0'}
395 | hasBin: true
396 | dependencies:
397 | '@types/estree': 1.0.5
398 | optionalDependencies:
399 | '@rollup/rollup-android-arm-eabi': 4.13.0
400 | '@rollup/rollup-android-arm64': 4.13.0
401 | '@rollup/rollup-darwin-arm64': 4.13.0
402 | '@rollup/rollup-darwin-x64': 4.13.0
403 | '@rollup/rollup-linux-arm-gnueabihf': 4.13.0
404 | '@rollup/rollup-linux-arm64-gnu': 4.13.0
405 | '@rollup/rollup-linux-arm64-musl': 4.13.0
406 | '@rollup/rollup-linux-riscv64-gnu': 4.13.0
407 | '@rollup/rollup-linux-x64-gnu': 4.13.0
408 | '@rollup/rollup-linux-x64-musl': 4.13.0
409 | '@rollup/rollup-win32-arm64-msvc': 4.13.0
410 | '@rollup/rollup-win32-ia32-msvc': 4.13.0
411 | '@rollup/rollup-win32-x64-msvc': 4.13.0
412 | fsevents: 2.3.3
413 | dev: true
414 |
415 | /source-map-js@1.0.2:
416 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
417 | engines: {node: '>=0.10.0'}
418 | dev: true
419 |
420 | /vite@5.0.8:
421 | resolution: {integrity: sha512-jYMALd8aeqR3yS9xlHd0OzQJndS9fH5ylVgWdB+pxTwxLKdO1pgC5Dlb398BUxpfaBxa4M9oT7j1g503Gaj5IQ==}
422 | engines: {node: ^18.0.0 || >=20.0.0}
423 | hasBin: true
424 | peerDependencies:
425 | '@types/node': ^18.0.0 || >=20.0.0
426 | less: '*'
427 | lightningcss: ^1.21.0
428 | sass: '*'
429 | stylus: '*'
430 | sugarss: '*'
431 | terser: ^5.4.0
432 | peerDependenciesMeta:
433 | '@types/node':
434 | optional: true
435 | less:
436 | optional: true
437 | lightningcss:
438 | optional: true
439 | sass:
440 | optional: true
441 | stylus:
442 | optional: true
443 | sugarss:
444 | optional: true
445 | terser:
446 | optional: true
447 | dependencies:
448 | esbuild: 0.19.12
449 | postcss: 8.4.35
450 | rollup: 4.13.0
451 | optionalDependencies:
452 | fsevents: 2.3.3
453 | dev: true
454 |
--------------------------------------------------------------------------------
/tiny-react/class02/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | .vscode
15 |
16 | # Editor directories and files
17 | .idea
18 | .DS_Store
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
--------------------------------------------------------------------------------
/tiny-react/class02/README.md:
--------------------------------------------------------------------------------
1 | # 实现`任务调度器requestIdleCallback`和简易的`fiber`
2 |
3 | ---
4 |
5 | theme: z-blue
6 | highlight: a11y-dark
7 |
8 | ---
9 |
10 | ### 前言
11 |
12 | 回顾上节,我们实现了[`createElement`和`render`函数](https://juejin.cn/post/7326093660705128460)。
13 |
14 | 如果需要渲染的 dom 树太大,就会导致浏览器卡顿,这是由于 JavaScript 是单线程执行的,因此我们需要利用`requestIdleCallback`方法,来让浏览器在空闲的时候渲染。
15 |
16 | 如下代码所示:
17 |
18 | ```js
19 | let i = 0;
20 | // 当数值过大时,会造成浏览器的卡顿
21 | while (i < 1000000000000) {
22 | // 假如递归多个dom生成
23 | i++;
24 | }
25 | ```
26 |
27 | ### `requestIdleCallback` 方法
28 |
29 | [**`window.requestIdleCallback()`**](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback)方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
30 |
31 | 其目的是为了解决当任务需要长时间占用主进程,导致更高优先级任务(如动画或事件任务),无法及时响应,而带来的页面丢帧(卡死)情况。
32 |
33 | 简单理解就是,在一帧的空闲时间里安排回调执行的方法。
34 |
35 | 那么浏览器的一帧都包括哪些呢?
36 |
37 | - 用户的交互
38 | - JavaScript 脚本执行
39 | - 开始帧
40 | - `requestAnimationFrame`(rAF)的调用
41 | - 布局计算
42 | - 页面重绘
43 |
44 | 具体如下图所示:
45 |
46 | 
47 |
48 | ### `requestIdleCallback` 执行时机
49 |
50 | 在完成一帧中的输入处理、渲染和合成之后,线程会进入空闲时期(idle period),直到下一帧开始,或者队列中的任务被激活,又或者收到了用户新的输入。requestIdleCallback 定义的回调就是在这段空闲时期执行:
51 |
52 | 
53 | 此类空闲期会在活动动画和屏幕更新期间频繁出现,但通常非常短(比如:在 60Hz 的设备下小于 16ms)
54 |
55 | 另一个空闲期的例子就是当用户代理空闲且没有发生屏幕更新时,浏览器其实处于空闲状态。这时候用户代理可能没有即将到来的任务来限制空闲期的结束。为了避免不可预测的任务(例如处理用户输入)中造成用户可感知的延迟,这些空闲周期的长度应限制为最大值[50ms](https://www.w3.org/TR/requestidlecallback/#why50)。一旦空闲期结束,用户代理可以安排另一个空闲期(如果它仍然空闲),使后台工作能够在较长的空闲时间内继续进行:
56 |
57 | 
58 |
59 | ### `requestIdleCallback`的第二参数
60 |
61 | 由于`requestIdleCallback`利用的是帧的空闲时间,所以有可能出现浏览器一直处于繁忙状态,导致回调一直无法执行,那这时候就需要在调用`requestIdleCallback`的时候传递第二个配置参数`timeout`了。
62 |
63 | - 使用 `timeout` 参数可以保证你的代码按时执行,但是 `requestIdleCallback` 本意就是在浏览器的空闲时间调用的,使用 `timeout` 就会强行执行,和 `requestIdleCallback` 的设计初衷就会互相矛盾,所以最好是让浏览器自己决定何时调用。
64 | - 另一方面检查超时也会产生一些额外开销,该 api 调用频率也会增加
65 |
66 | ```js
67 | // 无超时,一般打印值为 49/50 ms
68 | function work(deadline) {
69 | console.log(`当前帧剩余时间: ${deadline.timeRemaining()}`);
70 | requestIdleCallback(work);
71 | }
72 | requestIdleCallback(work);
73 |
74 | // =====================================================================
75 | // 有超时,打印值就不怎么固定了
76 | function work(deadline) {
77 | console.log(`当前帧剩余时间: ${deadline.timeRemaining()}`);
78 | requestIdleCallback(work, { timeout: 1500 });
79 | }
80 | requestIdleCallback(work, { timeout: 1500 });
81 | ```
82 |
83 | ### React 中的任务调度
84 |
85 | ```js
86 | let taskId = 1;
87 | function workLoop(deadline) {
88 | taskId++;
89 | let shouldYeild = false;
90 | while (!shouldYeild) {
91 | console.log(`taskId: ${taskId} run task. runtime: ${deadline.timeRemaining()}`);
92 | shouldYeild = deadline.timeRemaining() < 100;
93 | }
94 | requestIdleCallback(workLoop);
95 | }
96 | // React 核心调度算法模拟实现了 requestIdleCallback
97 | // requestIdleCallback就是浏览器提供给我们用来判断这个时机的api,
98 | // 它会在浏览器的空闲时间来执行传给它的回调函数。
99 | // 另外如果指定了超时时间,会在超时后的下一帧强制执行
100 | requestIdleCallback(workLoop);
101 | ```
102 |
103 | ### 仓库传送门
104 |
105 | - [https://github.com/Befend/tiny-series/tree/master/tiny-react/class02](https://github.com/Befend/tiny-series/tree/master/tiny-react/class02)
106 | - [https://github.com/Befend/tiny-series/tree/master/tiny-react/class03](https://github.com/Befend/tiny-series/tree/master/tiny-react/class03)
107 |
108 | ### 参考
109 |
110 | - [MDN 的 requestIdleCallback](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback)
111 |
112 | ### 系列文章
113 |
114 | - [实现`createElement`和`render`函数](https://juejin.cn/post/7326093660705128460)
115 | - [实现`任务调度器requestIdleCallback`和简易的`Fiber`](https://juejin.cn/post/7471968780866338843)
116 |
--------------------------------------------------------------------------------
/tiny-react/class02/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tiny-react/class02/demo/index.js:
--------------------------------------------------------------------------------
1 | let taskId = 1;
2 | function workLoop(deadline) {
3 | taskId++;
4 | let shouldYeild = false;
5 | while (!shouldYeild) {
6 | console.log(`taskId: ${taskId} run task. runtime: ${deadline.timeRemaining()}`);
7 | shouldYeild = deadline.timeRemaining() < 100;
8 | }
9 | requestIdleCallback(workLoop);
10 | }
11 | // React 核心调度算法模拟实现了 requestIdleCallback
12 | // requestIdleCallback就是浏览器提供给我们用来判断这个时机的api,
13 | // 它会在浏览器的空闲时间来执行传给它的回调函数。
14 | // 另外如果指定了超时时间,会在超时后的下一帧强制执行
15 | requestIdleCallback(workLoop);
16 |
--------------------------------------------------------------------------------
/tiny-react/class02/demo/main.js:
--------------------------------------------------------------------------------
1 | const el = document.createElement("div");
2 |
3 | el.innerText = "Hello, World!";
4 |
5 | document.body.appendChild(el);
6 |
7 | let i = 0;
8 | // 当数值过大时,会造成浏览器的卡顿
9 | while (i < 1000000000000) {
10 | i++;
11 | }
12 |
--------------------------------------------------------------------------------
/tiny-react/class03/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | .vscode
15 |
16 | # Editor directories and files
17 | .idea
18 | .DS_Store
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/tiny-react/class03/App.jsx:
--------------------------------------------------------------------------------
1 | import React from "./core/React.js";
2 | const App = React.createElement("div", { id: "app" }, "hello world, my mini react!");
3 | export default App;
--------------------------------------------------------------------------------
/tiny-react/class03/README.md:
--------------------------------------------------------------------------------
1 | # 实现`任务调度器requestIdleCallback`和简易的`Fiber`
2 |
3 | ---
4 |
5 | theme: z-blue
6 | highlight: a11y-dark
7 |
8 | ---
9 |
10 | ### 前言
11 |
12 | 回顾上节,我们实现了[`createElement`和`render`函数](https://juejin.cn/post/7326093660705128460)。
13 |
14 | 接下来我们继续来聊聊如何实现`任务调度器requestIdleCallback`和简易的`Fiber`。
15 |
16 | 如果需要渲染的 dom 树太大,就会导致浏览器卡顿,这是由于 JavaScript 是单线程执行的,因此我们需要利用`requestIdleCallback`方法,来让浏览器在空闲的时候渲染。
17 |
18 | 如下代码所示:
19 |
20 | ```js
21 | let i = 0;
22 | // 当数值过大时,会造成浏览器的卡顿
23 | while (i < 1000000000000) {
24 | // 假如递归多个dom生成
25 | i++;
26 | }
27 | ```
28 |
29 | ### `requestIdleCallback` 方法
30 |
31 | [**`window.requestIdleCallback()`**](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback)方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
32 |
33 | 其目的是为了解决当任务需要长时间占用主进程,导致更高优先级任务(如动画或事件任务),无法及时响应,而带来的页面丢帧(卡死)情况。
34 |
35 | 简单理解就是,在一帧的空闲时间里安排回调执行的方法。
36 |
37 | 那么浏览器的一帧都包括哪些呢?
38 |
39 | - 用户的交互
40 | - JavaScript 脚本执行
41 | - 开始帧
42 | - `requestAnimationFrame`(rAF)的调用
43 | - 布局计算
44 | - 页面重绘
45 |
46 | 具体如下图所示:
47 |
48 | 
49 |
50 | ### `requestIdleCallback` 执行时机
51 |
52 | 在完成一帧中的输入处理、渲染和合成之后,线程会进入空闲时期(idle period),直到下一帧开始,或者队列中的任务被激活,又或者收到了用户新的输入。requestIdleCallback 定义的回调就是在这段空闲时期执行:
53 |
54 | 
55 | 此类空闲期会在活动动画和屏幕更新期间频繁出现,但通常非常短(比如:在 60Hz 的设备下小于 16ms)
56 |
57 | 另一个空闲期的例子就是当用户代理空闲且没有发生屏幕更新时,浏览器其实处于空闲状态。这时候用户代理可能没有即将到来的任务来限制空闲期的结束。为了避免不可预测的任务(例如处理用户输入)中造成用户可感知的延迟,这些空闲周期的长度应限制为最大值[50ms](https://www.w3.org/TR/requestidlecallback/#why50)。一旦空闲期结束,用户代理可以安排另一个空闲期(如果它仍然空闲),使后台工作能够在较长的空闲时间内继续进行:
58 |
59 | 
60 |
61 | ### `requestIdleCallback`的第二参数
62 |
63 | 由于`requestIdleCallback`利用的是帧的空闲时间,所以有可能出现浏览器一直处于繁忙状态,导致回调一直无法执行,那这时候就需要在调用`requestIdleCallback`的时候传递第二个配置参数`timeout`了。
64 |
65 | - 使用 `timeout` 参数可以保证你的代码按时执行,但是 `requestIdleCallback` 本意就是在浏览器的空闲时间调用的,使用 `timeout` 就会强行执行,和 `requestIdleCallback` 的设计初衷就会互相矛盾,所以最好是让浏览器自己决定何时调用。
66 | - 另一方面检查超时也会产生一些额外开销,该 api 调用频率也会增加
67 |
68 | ```js
69 | // 无超时,一般打印值为 49/50 ms
70 | function work(deadline) {
71 | console.log(`当前帧剩余时间: ${deadline.timeRemaining()}`);
72 | requestIdleCallback(work);
73 | }
74 | requestIdleCallback(work);
75 |
76 | // =====================================================================
77 | // 有超时,打印值就不怎么固定了
78 | function work(deadline) {
79 | console.log(`当前帧剩余时间: ${deadline.timeRemaining()}`);
80 | requestIdleCallback(work, { timeout: 1500 });
81 | }
82 | requestIdleCallback(work, { timeout: 1500 });
83 | ```
84 |
85 | ### React 中的任务调度
86 |
87 | ```js
88 | let taskId = 1;
89 | function workLoop(deadline) {
90 | taskId++;
91 | let shouldYeild = false;
92 | while (!shouldYeild) {
93 | console.log(`taskId: ${taskId} run task. runtime: ${deadline.timeRemaining()}`);
94 | shouldYeild = deadline.timeRemaining() < 100;
95 | }
96 | requestIdleCallback(workLoop);
97 | }
98 | // React 核心调度算法模拟实现了 requestIdleCallback
99 | // requestIdleCallback就是浏览器提供给我们用来判断这个时机的api,
100 | // 它会在浏览器的空闲时间来执行传给它的回调函数。
101 | // 另外如果指定了超时时间,会在超时后的下一帧强制执行
102 | requestIdleCallback(workLoop);
103 | ```
104 |
105 | ### 实现简易的`Fiber`
106 |
107 | `React Fiber` 是对 `React` 核心算法的一次彻底重构。旧版的 `React` 使用的是`“Stack Reconciler”`,它会在一次更新中同步地遍历整个组件树,这样的方式对于大型应用来说,可能会导致卡顿和不流畅的用户体验。而 `Fiber` 则采用了一种增量式的更新方式,使得渲染过程可以被中断和恢复,从而提升性能和响应速度。
108 | 在 `Fiber` 架构中,每个组件对应一个 `Fiber` 节点。这些 `Fiber` 节点构成了一个链表结构,每个节点都包含了该组件的状态、更新队列以及指向子组件、兄弟组件和父组件的指针。通过这种结构,React 可以灵活地遍历和操作组件树。
109 |
110 | ```js
111 | // 任务调度
112 | function workLoop(deadline) {
113 | // 是否中断
114 | let shouldYeild = false;
115 | while (!shouldYeild && nextUnitOfWork) {
116 | nextUnitOfWork = performWorkOfUnit(nextUnitOfWork);
117 | shouldYeild = deadline.timeRemaining() < 1;
118 | }
119 | // 任务放到下次执行
120 | requestIdleCallback(workLoop);
121 | }
122 |
123 | /**
124 | * 执行当前工作单元的工作
125 | * @param {*} fiber
126 | */
127 | function performWorkOfUnit(fiber) {
128 | if (!fiber.dom) {
129 | // 1. 创建dom
130 | const dom = createDom(fiber.type);
131 | fiber.dom = dom;
132 | // 插入节点
133 | fiber.parent.dom.append(dom);
134 |
135 | // 2. 处理props
136 | updateProps(dom, fiber.props);
137 | }
138 |
139 | // 3. 转换链表,设置好指针
140 | initChildren(fiber);
141 |
142 | // 4. 返回下一个要执行的任务
143 | if (fiber.child) {
144 | return fiber.child;
145 | }
146 |
147 | if (fiber.sibling) {
148 | return fiber.sibling;
149 | }
150 | return fiber.parent?.sibling;
151 | }
152 |
153 | function initChildren(fiber) {
154 | let prevFiber = null;
155 | fiber.props.children.forEach((child, index) => {
156 | const nextFiber = {
157 | type: typeof child.type === "object" ? child.type.type : child.type,
158 | props: typeof child.type === "object" ? child.type.props : child.props,
159 | child: null,
160 | parent: fiber,
161 | sibling: null,
162 | dom: null,
163 | };
164 |
165 | if (index === 0) {
166 | fiber.child = nextFiber;
167 | } else {
168 | prevFiber.sibling = nextFiber;
169 | }
170 |
171 | prevFiber = child.type;
172 | });
173 | }
174 | ```
175 |
176 | ### 仓库传送门
177 |
178 | - [https://github.com/Befend/tiny-series/tree/master/tiny-react/class02](https://github.com/Befend/tiny-series/tree/master/tiny-react/class02)
179 | - [https://github.com/Befend/tiny-series/tree/master/tiny-react/class03](https://github.com/Befend/tiny-series/tree/master/tiny-react/class03)
180 |
181 | ### 参考
182 |
183 | - [MDN 的 requestIdleCallback](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback)
184 |
185 | ### 系列文章
186 |
187 | - [实现`createElement`和`render`函数](https://juejin.cn/post/7326093660705128460)
188 | - [实现`任务调度器requestIdleCallback`和简易的`Fiber`](https://juejin.cn/post/7471968780866338843)
189 |
--------------------------------------------------------------------------------
/tiny-react/class03/core/React.js:
--------------------------------------------------------------------------------
1 | function createTextNode(text) {
2 | return {
3 | type: "TEXT_ELEMENT",
4 | props: {
5 | nodeValue: text,
6 | children: [],
7 | },
8 | };
9 | }
10 |
11 | function createElement(type, props, ...children) {
12 | return {
13 | type,
14 | props: {
15 | ...props,
16 | children: children.map((child) => {
17 | return typeof child === "string" ? createTextNode(child) : child;
18 | }),
19 | },
20 | };
21 | }
22 |
23 | // 下一个工作单元 (fiber结构)
24 | let nextUnitOfWork = null;
25 |
26 | // 开启任务调度
27 | requestIdleCallback(workLoop);
28 |
29 | function render(el, container) {
30 | nextUnitOfWork = {
31 | dom: container,
32 | props: {
33 | children: [el],
34 | },
35 | };
36 | // const dom =
37 | // el.type === "TEXT_ELEMENT"
38 | // ? document.createTextNode("")
39 | // : document.createElement(el.type)
40 | // // id class
41 | // Object.keys(el.props).forEach((key) => {
42 | // if (key !== "children") {
43 | // dom[key] = el.props[key]
44 | // }
45 | // })
46 | // const children = el.props.children
47 | // children.forEach((child) => {
48 | // render(child, dom)
49 | // })
50 | // container.append(dom)
51 | }
52 |
53 | // 任务调度
54 | function workLoop(deadline) {
55 | // 是否中断
56 | let shouldYeild = false;
57 | while (!shouldYeild && nextUnitOfWork) {
58 | nextUnitOfWork = performWorkOfUnit(nextUnitOfWork);
59 | shouldYeild = deadline.timeRemaining() < 1;
60 | }
61 | // 任务放到下次执行
62 | requestIdleCallback(workLoop);
63 | }
64 |
65 | function createDom(type) {
66 | return type === "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(type);
67 | }
68 |
69 | function updateProps(dom, props) {
70 | Object.keys(props).forEach((key) => {
71 | if (key !== "children") {
72 | dom[key] = props[key];
73 | }
74 | });
75 | }
76 |
77 | function initChildren(fiber) {
78 | let prevFiber = null;
79 | fiber.props.children.forEach((child, index) => {
80 | const nextFiber = {
81 | type: typeof child.type === "object" ? child.type.type : child.type,
82 | props: typeof child.type === "object" ? child.type.props : child.props,
83 | child: null,
84 | parent: fiber,
85 | sibling: null,
86 | dom: null,
87 | };
88 |
89 | if (index === 0) {
90 | fiber.child = nextFiber;
91 | } else {
92 | prevFiber.sibling = nextFiber;
93 | }
94 |
95 | prevFiber = child.type;
96 | });
97 | }
98 |
99 | /**
100 | * 执行当前工作单元的工作
101 | * @param {*} fiber
102 | */
103 | function performWorkOfUnit(fiber) {
104 | if (!fiber.dom) {
105 | // 1. 创建dom
106 | const dom = createDom(fiber.type);
107 | fiber.dom = dom;
108 | // 插入节点
109 | fiber.parent.dom.append(dom);
110 |
111 | // 2. 处理props
112 | updateProps(dom, fiber.props);
113 | }
114 |
115 | // 3. 转换链表,设置好指针
116 | initChildren(fiber);
117 |
118 | // 4. 返回下一个要执行的任务
119 | if (fiber.child) {
120 | return fiber.child;
121 | }
122 |
123 | if (fiber.sibling) {
124 | return fiber.sibling;
125 | }
126 | return fiber.parent?.sibling;
127 | }
128 |
129 | const React = {
130 | render,
131 | createElement,
132 | };
133 |
134 | export default React;
135 |
--------------------------------------------------------------------------------
/tiny-react/class03/core/ReactDOM.js:
--------------------------------------------------------------------------------
1 | import React from "./React.js";
2 | const ReactDOM = {
3 | createRoot(container) {
4 | return {
5 | render(App) {
6 | React.render(App, container);
7 | },
8 | };
9 | },
10 | };
11 |
12 | export default ReactDOM;
13 |
--------------------------------------------------------------------------------
/tiny-react/class03/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tiny-react/class03/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "./core/React.js";
2 | import ReactDOM from "./core/ReactDOM.js";
3 | import App from "./App.jsx";
4 |
5 | ReactDOM.createRoot(document.querySelector("#root")).render();
--------------------------------------------------------------------------------
/tiny-react/class03/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mini-react",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "test": "vitest",
10 | "preview": "vite preview"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "ISC",
15 | "devDependencies": {
16 | "vite": "^5.0.8",
17 | "vitest": "^1.2.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tiny-react/class04/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | .vscode
15 |
16 | # Editor directories and files
17 | .idea
18 | .DS_Store
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/tiny-react/class04/App.jsx:
--------------------------------------------------------------------------------
1 | import React from "./core/React.js";
2 | // const App = React.createElement("div", { id: "app" }, "hello world, my mini react!");
3 |
4 | function Counter({num}) {
5 | return count:{num}
6 | }
7 |
8 | function CounterContainer() {
9 | return
10 | }
11 |
12 | function App() {
13 | return (
14 |
15 | hello world, my mini react!
16 |
17 |
18 |
befend
19 |
20 | )
21 | }
22 |
23 | export default App;
--------------------------------------------------------------------------------
/tiny-react/class04/README.md:
--------------------------------------------------------------------------------
1 | # 实现`FunctionComponent`
2 |
3 | ### 前言
4 |
5 | 回顾上一节,我们通过实现任务调度器 `requestIdleCallback` 利用了空余时间去完成每个 task。这种方式存在一个问题,当中途没有空余时间时,用户可能会看到渲染一半的 dom。
6 |
7 | 我们可以采用统一提交的方式去解决这个问题,先处理链表,最后再统一添加到屏幕中。
8 |
9 | ### FunctionComponent
10 |
11 | 自 `React 16.8` 引入 `Hooks` 以来,`函数式组件(FunctionComponent)`彻底改变了 `React` 开发的格局。它不再是简单的无状态组件,而是通过 `Hooks` 机制融合了**状态管理**、**生命周期和副作用处理**,成为 `React` 官方推荐的组件开发范式。
12 |
13 | ### 实现流程图解
14 |
15 | ```js
16 | 1. 调用函数组件 (props) → 生成 JSX
17 | |
18 | 2. 转换为 React Element(虚拟 DOM 节点)
19 | |
20 | 3. 调和阶段(Reconciliation):
21 | - 创建/更新 Fiber 节点
22 | - 处理 Hooks,记录状态和副作用到 Fiber
23 | |
24 | 4. Diff 算法 → 生成更新计划
25 | |
26 | 5. 提交阶段(Commit):
27 | - 更新 DOM
28 | - 执行副作用(useEffect)
29 | ```
30 |
31 | ### 总结
32 |
33 | `React` 通过 `函数直接调用`、`Fiber 架构的状态管理` 和 `Hooks 的链表顺序追踪`,实现了`函数组件`的轻量化与高效更新。相较于类组件,函数组件避免了实例化开销,更契合 React 的声明式设计理念,同时 `Hooks` 提供了灵活的状态与副作用管理能力。
34 |
35 | ### 系列文章
36 |
37 | - [实现`createElement`和`render`函数](https://juejin.cn/post/7326093660705128460)
38 | - [实现`任务调度器requestIdleCallback`和简易的`Fiber`](https://juejin.cn/post/7471968780866338843)
39 |
--------------------------------------------------------------------------------
/tiny-react/class04/core/React.js:
--------------------------------------------------------------------------------
1 | function createTextNode(text) {
2 | return {
3 | type: "TEXT_ELEMENT",
4 | props: {
5 | nodeValue: text,
6 | children: [],
7 | },
8 | };
9 | }
10 |
11 | function createElement(type, props, ...children) {
12 | return {
13 | type,
14 | props: {
15 | ...props,
16 | children: children.map((child) => {
17 | const isTextNode = typeof child === "string" || typeof child === "number";
18 | return isTextNode ? createTextNode(child) : child;
19 | }),
20 | },
21 | };
22 | }
23 |
24 | // 下一个工作单元 (fiber结构)
25 | let nextUnitOfWork = null;
26 | let root = null;
27 |
28 | function render(el, container) {
29 | nextUnitOfWork = {
30 | dom: container,
31 | props: {
32 | children: [el],
33 | },
34 | };
35 | root = nextUnitOfWork;
36 | }
37 |
38 | function createDom(type) {
39 | return type === "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(type);
40 | }
41 |
42 | function updateProps(dom, props) {
43 | Object.keys(props).forEach((key) => {
44 | if (key !== "children") {
45 | dom[key] = props[key];
46 | }
47 | });
48 | }
49 |
50 | function initChildren(fiber, children) {
51 | // const children = fiber.props.children
52 | let prevFiber = null;
53 | children.forEach((child, index) => {
54 | const newFiber = {
55 | type: typeof child.type === "object" ? child.type.type : child.type,
56 | props: typeof child.type === "object" ? child.type.props : child.props,
57 | child: null,
58 | parent: fiber,
59 | sibling: null,
60 | dom: null,
61 | };
62 |
63 | if (index === 0) {
64 | fiber.child = newFiber;
65 | } else {
66 | prevFiber.sibling = newFiber;
67 | }
68 |
69 | prevFiber = newFiber;
70 | });
71 | }
72 |
73 | function updateFunctionComponent(fiber) {
74 | const children = [fiber.type(fiber.props)];
75 | // 3. 转换链表,设置好指针
76 | initChildren(fiber, children);
77 | }
78 |
79 | function updateHostComponent(fiber) {
80 | if (!fiber.dom) {
81 | // 1. 创建dom
82 | const dom = (fiber.dom = createDom(fiber.type));
83 | // 2. 处理props
84 | updateProps(dom, fiber.props);
85 | }
86 |
87 | const children = fiber.props.children;
88 | // 3. 转换链表,设置好指针
89 | initChildren(fiber, children);
90 | }
91 |
92 | /**
93 | * 执行当前工作单元的工作
94 | * @param {*} fiber
95 | */
96 | function performWorkOfUnit(fiber) {
97 | const isFunctionComponent = typeof fiber.type === "function";
98 | if (isFunctionComponent) {
99 | updateFunctionComponent(fiber);
100 | } else {
101 | updateHostComponent(fiber);
102 | }
103 |
104 | // 4. 返回下一个要执行的任务
105 | if (fiber.child) {
106 | return fiber.child;
107 | }
108 |
109 | let nextFiber = fiber;
110 | while (nextFiber) {
111 | if (nextFiber.sibling) return nextFiber.sibling;
112 | nextFiber = nextFiber.parent;
113 | }
114 | }
115 |
116 | // 任务调度
117 | function workLoop(deadline) {
118 | // 是否中断
119 | let shouldYeild = false;
120 | while (!shouldYeild && nextUnitOfWork) {
121 | nextUnitOfWork = performWorkOfUnit(nextUnitOfWork);
122 | shouldYeild = deadline.timeRemaining() < 1;
123 | }
124 | // 统一提交
125 | if (!nextUnitOfWork && root) {
126 | console.log(root);
127 | commitRoot();
128 | }
129 | // 任务放到下次执行
130 | requestIdleCallback(workLoop);
131 | }
132 |
133 | function commitRoot() {
134 | // 统一提交任务
135 | commitWork(root.child);
136 | root = null;
137 | }
138 |
139 | // 提交任务
140 | function commitWork(fiber) {
141 | if (!fiber) return;
142 | let fiberParent = fiber.parent;
143 | while (!fiberParent.dom) {
144 | fiberParent = fiberParent.parent;
145 | }
146 | if (fiber.dom) {
147 | fiberParent.dom.append(fiber.dom);
148 | }
149 | commitWork(fiber.child);
150 | commitWork(fiber.sibling);
151 | }
152 |
153 | // 开启任务调度
154 | requestIdleCallback(workLoop);
155 |
156 | const React = {
157 | render,
158 | createElement,
159 | };
160 |
161 | export default React;
162 |
--------------------------------------------------------------------------------
/tiny-react/class04/core/ReactDOM.js:
--------------------------------------------------------------------------------
1 | import React from "./React.js";
2 | const ReactDOM = {
3 | createRoot(container) {
4 | return {
5 | render(App) {
6 | React.render(App, container);
7 | },
8 | };
9 | },
10 | };
11 |
12 | export default ReactDOM;
13 |
--------------------------------------------------------------------------------
/tiny-react/class04/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tiny-react/class04/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "./core/React.js";
2 | import ReactDOM from "./core/ReactDOM.js";
3 | import App from "./App.jsx";
4 |
5 | ReactDOM.createRoot(document.querySelector("#root")).render();
--------------------------------------------------------------------------------
/tiny-react/class04/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mini-react",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "test": "vitest",
10 | "preview": "vite preview"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "ISC",
15 | "devDependencies": {
16 | "vite": "^5.0.8",
17 | "vitest": "^1.2.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tiny-react/class04/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | devDependencies:
11 | vite:
12 | specifier: ^5.0.8
13 | version: 5.4.14
14 | vitest:
15 | specifier: ^1.2.0
16 | version: 1.6.1
17 |
18 | packages:
19 |
20 | '@esbuild/aix-ppc64@0.21.5':
21 | resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
22 | engines: {node: '>=12'}
23 | cpu: [ppc64]
24 | os: [aix]
25 |
26 | '@esbuild/android-arm64@0.21.5':
27 | resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
28 | engines: {node: '>=12'}
29 | cpu: [arm64]
30 | os: [android]
31 |
32 | '@esbuild/android-arm@0.21.5':
33 | resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
34 | engines: {node: '>=12'}
35 | cpu: [arm]
36 | os: [android]
37 |
38 | '@esbuild/android-x64@0.21.5':
39 | resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
40 | engines: {node: '>=12'}
41 | cpu: [x64]
42 | os: [android]
43 |
44 | '@esbuild/darwin-arm64@0.21.5':
45 | resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
46 | engines: {node: '>=12'}
47 | cpu: [arm64]
48 | os: [darwin]
49 |
50 | '@esbuild/darwin-x64@0.21.5':
51 | resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
52 | engines: {node: '>=12'}
53 | cpu: [x64]
54 | os: [darwin]
55 |
56 | '@esbuild/freebsd-arm64@0.21.5':
57 | resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
58 | engines: {node: '>=12'}
59 | cpu: [arm64]
60 | os: [freebsd]
61 |
62 | '@esbuild/freebsd-x64@0.21.5':
63 | resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
64 | engines: {node: '>=12'}
65 | cpu: [x64]
66 | os: [freebsd]
67 |
68 | '@esbuild/linux-arm64@0.21.5':
69 | resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
70 | engines: {node: '>=12'}
71 | cpu: [arm64]
72 | os: [linux]
73 |
74 | '@esbuild/linux-arm@0.21.5':
75 | resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
76 | engines: {node: '>=12'}
77 | cpu: [arm]
78 | os: [linux]
79 |
80 | '@esbuild/linux-ia32@0.21.5':
81 | resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
82 | engines: {node: '>=12'}
83 | cpu: [ia32]
84 | os: [linux]
85 |
86 | '@esbuild/linux-loong64@0.21.5':
87 | resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
88 | engines: {node: '>=12'}
89 | cpu: [loong64]
90 | os: [linux]
91 |
92 | '@esbuild/linux-mips64el@0.21.5':
93 | resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
94 | engines: {node: '>=12'}
95 | cpu: [mips64el]
96 | os: [linux]
97 |
98 | '@esbuild/linux-ppc64@0.21.5':
99 | resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
100 | engines: {node: '>=12'}
101 | cpu: [ppc64]
102 | os: [linux]
103 |
104 | '@esbuild/linux-riscv64@0.21.5':
105 | resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
106 | engines: {node: '>=12'}
107 | cpu: [riscv64]
108 | os: [linux]
109 |
110 | '@esbuild/linux-s390x@0.21.5':
111 | resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
112 | engines: {node: '>=12'}
113 | cpu: [s390x]
114 | os: [linux]
115 |
116 | '@esbuild/linux-x64@0.21.5':
117 | resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
118 | engines: {node: '>=12'}
119 | cpu: [x64]
120 | os: [linux]
121 |
122 | '@esbuild/netbsd-x64@0.21.5':
123 | resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
124 | engines: {node: '>=12'}
125 | cpu: [x64]
126 | os: [netbsd]
127 |
128 | '@esbuild/openbsd-x64@0.21.5':
129 | resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
130 | engines: {node: '>=12'}
131 | cpu: [x64]
132 | os: [openbsd]
133 |
134 | '@esbuild/sunos-x64@0.21.5':
135 | resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
136 | engines: {node: '>=12'}
137 | cpu: [x64]
138 | os: [sunos]
139 |
140 | '@esbuild/win32-arm64@0.21.5':
141 | resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
142 | engines: {node: '>=12'}
143 | cpu: [arm64]
144 | os: [win32]
145 |
146 | '@esbuild/win32-ia32@0.21.5':
147 | resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
148 | engines: {node: '>=12'}
149 | cpu: [ia32]
150 | os: [win32]
151 |
152 | '@esbuild/win32-x64@0.21.5':
153 | resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
154 | engines: {node: '>=12'}
155 | cpu: [x64]
156 | os: [win32]
157 |
158 | '@jest/schemas@29.6.3':
159 | resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
160 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
161 |
162 | '@jridgewell/sourcemap-codec@1.5.0':
163 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
164 |
165 | '@rollup/rollup-android-arm-eabi@4.34.8':
166 | resolution: {integrity: sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==}
167 | cpu: [arm]
168 | os: [android]
169 |
170 | '@rollup/rollup-android-arm64@4.34.8':
171 | resolution: {integrity: sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==}
172 | cpu: [arm64]
173 | os: [android]
174 |
175 | '@rollup/rollup-darwin-arm64@4.34.8':
176 | resolution: {integrity: sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==}
177 | cpu: [arm64]
178 | os: [darwin]
179 |
180 | '@rollup/rollup-darwin-x64@4.34.8':
181 | resolution: {integrity: sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==}
182 | cpu: [x64]
183 | os: [darwin]
184 |
185 | '@rollup/rollup-freebsd-arm64@4.34.8':
186 | resolution: {integrity: sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==}
187 | cpu: [arm64]
188 | os: [freebsd]
189 |
190 | '@rollup/rollup-freebsd-x64@4.34.8':
191 | resolution: {integrity: sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==}
192 | cpu: [x64]
193 | os: [freebsd]
194 |
195 | '@rollup/rollup-linux-arm-gnueabihf@4.34.8':
196 | resolution: {integrity: sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==}
197 | cpu: [arm]
198 | os: [linux]
199 | libc: [glibc]
200 |
201 | '@rollup/rollup-linux-arm-musleabihf@4.34.8':
202 | resolution: {integrity: sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==}
203 | cpu: [arm]
204 | os: [linux]
205 | libc: [musl]
206 |
207 | '@rollup/rollup-linux-arm64-gnu@4.34.8':
208 | resolution: {integrity: sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==}
209 | cpu: [arm64]
210 | os: [linux]
211 | libc: [glibc]
212 |
213 | '@rollup/rollup-linux-arm64-musl@4.34.8':
214 | resolution: {integrity: sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==}
215 | cpu: [arm64]
216 | os: [linux]
217 | libc: [musl]
218 |
219 | '@rollup/rollup-linux-loongarch64-gnu@4.34.8':
220 | resolution: {integrity: sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==}
221 | cpu: [loong64]
222 | os: [linux]
223 | libc: [glibc]
224 |
225 | '@rollup/rollup-linux-powerpc64le-gnu@4.34.8':
226 | resolution: {integrity: sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==}
227 | cpu: [ppc64]
228 | os: [linux]
229 | libc: [glibc]
230 |
231 | '@rollup/rollup-linux-riscv64-gnu@4.34.8':
232 | resolution: {integrity: sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==}
233 | cpu: [riscv64]
234 | os: [linux]
235 | libc: [glibc]
236 |
237 | '@rollup/rollup-linux-s390x-gnu@4.34.8':
238 | resolution: {integrity: sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==}
239 | cpu: [s390x]
240 | os: [linux]
241 | libc: [glibc]
242 |
243 | '@rollup/rollup-linux-x64-gnu@4.34.8':
244 | resolution: {integrity: sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==}
245 | cpu: [x64]
246 | os: [linux]
247 | libc: [glibc]
248 |
249 | '@rollup/rollup-linux-x64-musl@4.34.8':
250 | resolution: {integrity: sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==}
251 | cpu: [x64]
252 | os: [linux]
253 | libc: [musl]
254 |
255 | '@rollup/rollup-win32-arm64-msvc@4.34.8':
256 | resolution: {integrity: sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==}
257 | cpu: [arm64]
258 | os: [win32]
259 |
260 | '@rollup/rollup-win32-ia32-msvc@4.34.8':
261 | resolution: {integrity: sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==}
262 | cpu: [ia32]
263 | os: [win32]
264 |
265 | '@rollup/rollup-win32-x64-msvc@4.34.8':
266 | resolution: {integrity: sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==}
267 | cpu: [x64]
268 | os: [win32]
269 |
270 | '@sinclair/typebox@0.27.8':
271 | resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
272 |
273 | '@types/estree@1.0.6':
274 | resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
275 |
276 | '@vitest/expect@1.6.1':
277 | resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==}
278 |
279 | '@vitest/runner@1.6.1':
280 | resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==}
281 |
282 | '@vitest/snapshot@1.6.1':
283 | resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==}
284 |
285 | '@vitest/spy@1.6.1':
286 | resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==}
287 |
288 | '@vitest/utils@1.6.1':
289 | resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==}
290 |
291 | acorn-walk@8.3.4:
292 | resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
293 | engines: {node: '>=0.4.0'}
294 |
295 | acorn@8.14.0:
296 | resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
297 | engines: {node: '>=0.4.0'}
298 | hasBin: true
299 |
300 | ansi-styles@5.2.0:
301 | resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
302 | engines: {node: '>=10'}
303 |
304 | assertion-error@1.1.0:
305 | resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
306 |
307 | cac@6.7.14:
308 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
309 | engines: {node: '>=8'}
310 |
311 | chai@4.5.0:
312 | resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==}
313 | engines: {node: '>=4'}
314 |
315 | check-error@1.0.3:
316 | resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
317 |
318 | confbox@0.1.8:
319 | resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
320 |
321 | cross-spawn@7.0.6:
322 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
323 | engines: {node: '>= 8'}
324 |
325 | debug@4.4.0:
326 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
327 | engines: {node: '>=6.0'}
328 | peerDependencies:
329 | supports-color: '*'
330 | peerDependenciesMeta:
331 | supports-color:
332 | optional: true
333 |
334 | deep-eql@4.1.4:
335 | resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==}
336 | engines: {node: '>=6'}
337 |
338 | diff-sequences@29.6.3:
339 | resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
340 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
341 |
342 | esbuild@0.21.5:
343 | resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
344 | engines: {node: '>=12'}
345 | hasBin: true
346 |
347 | estree-walker@3.0.3:
348 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
349 |
350 | execa@8.0.1:
351 | resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
352 | engines: {node: '>=16.17'}
353 |
354 | fsevents@2.3.3:
355 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
356 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
357 | os: [darwin]
358 |
359 | get-func-name@2.0.2:
360 | resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
361 |
362 | get-stream@8.0.1:
363 | resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
364 | engines: {node: '>=16'}
365 |
366 | human-signals@5.0.0:
367 | resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
368 | engines: {node: '>=16.17.0'}
369 |
370 | is-stream@3.0.0:
371 | resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
372 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
373 |
374 | isexe@2.0.0:
375 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
376 |
377 | js-tokens@9.0.1:
378 | resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
379 |
380 | local-pkg@0.5.1:
381 | resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==}
382 | engines: {node: '>=14'}
383 |
384 | loupe@2.3.7:
385 | resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
386 |
387 | magic-string@0.30.17:
388 | resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
389 |
390 | merge-stream@2.0.0:
391 | resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
392 |
393 | mimic-fn@4.0.0:
394 | resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
395 | engines: {node: '>=12'}
396 |
397 | mlly@1.7.4:
398 | resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
399 |
400 | ms@2.1.3:
401 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
402 |
403 | nanoid@3.3.8:
404 | resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
405 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
406 | hasBin: true
407 |
408 | npm-run-path@5.3.0:
409 | resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
410 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
411 |
412 | onetime@6.0.0:
413 | resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
414 | engines: {node: '>=12'}
415 |
416 | p-limit@5.0.0:
417 | resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
418 | engines: {node: '>=18'}
419 |
420 | path-key@3.1.1:
421 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
422 | engines: {node: '>=8'}
423 |
424 | path-key@4.0.0:
425 | resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
426 | engines: {node: '>=12'}
427 |
428 | pathe@1.1.2:
429 | resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
430 |
431 | pathe@2.0.3:
432 | resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
433 |
434 | pathval@1.1.1:
435 | resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
436 |
437 | picocolors@1.1.1:
438 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
439 |
440 | pkg-types@1.3.1:
441 | resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
442 |
443 | postcss@8.5.2:
444 | resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==}
445 | engines: {node: ^10 || ^12 || >=14}
446 |
447 | pretty-format@29.7.0:
448 | resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
449 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
450 |
451 | react-is@18.3.1:
452 | resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
453 |
454 | rollup@4.34.8:
455 | resolution: {integrity: sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==}
456 | engines: {node: '>=18.0.0', npm: '>=8.0.0'}
457 | hasBin: true
458 |
459 | shebang-command@2.0.0:
460 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
461 | engines: {node: '>=8'}
462 |
463 | shebang-regex@3.0.0:
464 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
465 | engines: {node: '>=8'}
466 |
467 | siginfo@2.0.0:
468 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
469 |
470 | signal-exit@4.1.0:
471 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
472 | engines: {node: '>=14'}
473 |
474 | source-map-js@1.2.1:
475 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
476 | engines: {node: '>=0.10.0'}
477 |
478 | stackback@0.0.2:
479 | resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
480 |
481 | std-env@3.8.0:
482 | resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==}
483 |
484 | strip-final-newline@3.0.0:
485 | resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
486 | engines: {node: '>=12'}
487 |
488 | strip-literal@2.1.1:
489 | resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
490 |
491 | tinybench@2.9.0:
492 | resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
493 |
494 | tinypool@0.8.4:
495 | resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==}
496 | engines: {node: '>=14.0.0'}
497 |
498 | tinyspy@2.2.1:
499 | resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==}
500 | engines: {node: '>=14.0.0'}
501 |
502 | type-detect@4.1.0:
503 | resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==}
504 | engines: {node: '>=4'}
505 |
506 | ufo@1.5.4:
507 | resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
508 |
509 | vite-node@1.6.1:
510 | resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==}
511 | engines: {node: ^18.0.0 || >=20.0.0}
512 | hasBin: true
513 |
514 | vite@5.4.14:
515 | resolution: {integrity: sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==}
516 | engines: {node: ^18.0.0 || >=20.0.0}
517 | hasBin: true
518 | peerDependencies:
519 | '@types/node': ^18.0.0 || >=20.0.0
520 | less: '*'
521 | lightningcss: ^1.21.0
522 | sass: '*'
523 | sass-embedded: '*'
524 | stylus: '*'
525 | sugarss: '*'
526 | terser: ^5.4.0
527 | peerDependenciesMeta:
528 | '@types/node':
529 | optional: true
530 | less:
531 | optional: true
532 | lightningcss:
533 | optional: true
534 | sass:
535 | optional: true
536 | sass-embedded:
537 | optional: true
538 | stylus:
539 | optional: true
540 | sugarss:
541 | optional: true
542 | terser:
543 | optional: true
544 |
545 | vitest@1.6.1:
546 | resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==}
547 | engines: {node: ^18.0.0 || >=20.0.0}
548 | hasBin: true
549 | peerDependencies:
550 | '@edge-runtime/vm': '*'
551 | '@types/node': ^18.0.0 || >=20.0.0
552 | '@vitest/browser': 1.6.1
553 | '@vitest/ui': 1.6.1
554 | happy-dom: '*'
555 | jsdom: '*'
556 | peerDependenciesMeta:
557 | '@edge-runtime/vm':
558 | optional: true
559 | '@types/node':
560 | optional: true
561 | '@vitest/browser':
562 | optional: true
563 | '@vitest/ui':
564 | optional: true
565 | happy-dom:
566 | optional: true
567 | jsdom:
568 | optional: true
569 |
570 | which@2.0.2:
571 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
572 | engines: {node: '>= 8'}
573 | hasBin: true
574 |
575 | why-is-node-running@2.3.0:
576 | resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
577 | engines: {node: '>=8'}
578 | hasBin: true
579 |
580 | yocto-queue@1.1.1:
581 | resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==}
582 | engines: {node: '>=12.20'}
583 |
584 | snapshots:
585 |
586 | '@esbuild/aix-ppc64@0.21.5':
587 | optional: true
588 |
589 | '@esbuild/android-arm64@0.21.5':
590 | optional: true
591 |
592 | '@esbuild/android-arm@0.21.5':
593 | optional: true
594 |
595 | '@esbuild/android-x64@0.21.5':
596 | optional: true
597 |
598 | '@esbuild/darwin-arm64@0.21.5':
599 | optional: true
600 |
601 | '@esbuild/darwin-x64@0.21.5':
602 | optional: true
603 |
604 | '@esbuild/freebsd-arm64@0.21.5':
605 | optional: true
606 |
607 | '@esbuild/freebsd-x64@0.21.5':
608 | optional: true
609 |
610 | '@esbuild/linux-arm64@0.21.5':
611 | optional: true
612 |
613 | '@esbuild/linux-arm@0.21.5':
614 | optional: true
615 |
616 | '@esbuild/linux-ia32@0.21.5':
617 | optional: true
618 |
619 | '@esbuild/linux-loong64@0.21.5':
620 | optional: true
621 |
622 | '@esbuild/linux-mips64el@0.21.5':
623 | optional: true
624 |
625 | '@esbuild/linux-ppc64@0.21.5':
626 | optional: true
627 |
628 | '@esbuild/linux-riscv64@0.21.5':
629 | optional: true
630 |
631 | '@esbuild/linux-s390x@0.21.5':
632 | optional: true
633 |
634 | '@esbuild/linux-x64@0.21.5':
635 | optional: true
636 |
637 | '@esbuild/netbsd-x64@0.21.5':
638 | optional: true
639 |
640 | '@esbuild/openbsd-x64@0.21.5':
641 | optional: true
642 |
643 | '@esbuild/sunos-x64@0.21.5':
644 | optional: true
645 |
646 | '@esbuild/win32-arm64@0.21.5':
647 | optional: true
648 |
649 | '@esbuild/win32-ia32@0.21.5':
650 | optional: true
651 |
652 | '@esbuild/win32-x64@0.21.5':
653 | optional: true
654 |
655 | '@jest/schemas@29.6.3':
656 | dependencies:
657 | '@sinclair/typebox': 0.27.8
658 |
659 | '@jridgewell/sourcemap-codec@1.5.0': {}
660 |
661 | '@rollup/rollup-android-arm-eabi@4.34.8':
662 | optional: true
663 |
664 | '@rollup/rollup-android-arm64@4.34.8':
665 | optional: true
666 |
667 | '@rollup/rollup-darwin-arm64@4.34.8':
668 | optional: true
669 |
670 | '@rollup/rollup-darwin-x64@4.34.8':
671 | optional: true
672 |
673 | '@rollup/rollup-freebsd-arm64@4.34.8':
674 | optional: true
675 |
676 | '@rollup/rollup-freebsd-x64@4.34.8':
677 | optional: true
678 |
679 | '@rollup/rollup-linux-arm-gnueabihf@4.34.8':
680 | optional: true
681 |
682 | '@rollup/rollup-linux-arm-musleabihf@4.34.8':
683 | optional: true
684 |
685 | '@rollup/rollup-linux-arm64-gnu@4.34.8':
686 | optional: true
687 |
688 | '@rollup/rollup-linux-arm64-musl@4.34.8':
689 | optional: true
690 |
691 | '@rollup/rollup-linux-loongarch64-gnu@4.34.8':
692 | optional: true
693 |
694 | '@rollup/rollup-linux-powerpc64le-gnu@4.34.8':
695 | optional: true
696 |
697 | '@rollup/rollup-linux-riscv64-gnu@4.34.8':
698 | optional: true
699 |
700 | '@rollup/rollup-linux-s390x-gnu@4.34.8':
701 | optional: true
702 |
703 | '@rollup/rollup-linux-x64-gnu@4.34.8':
704 | optional: true
705 |
706 | '@rollup/rollup-linux-x64-musl@4.34.8':
707 | optional: true
708 |
709 | '@rollup/rollup-win32-arm64-msvc@4.34.8':
710 | optional: true
711 |
712 | '@rollup/rollup-win32-ia32-msvc@4.34.8':
713 | optional: true
714 |
715 | '@rollup/rollup-win32-x64-msvc@4.34.8':
716 | optional: true
717 |
718 | '@sinclair/typebox@0.27.8': {}
719 |
720 | '@types/estree@1.0.6': {}
721 |
722 | '@vitest/expect@1.6.1':
723 | dependencies:
724 | '@vitest/spy': 1.6.1
725 | '@vitest/utils': 1.6.1
726 | chai: 4.5.0
727 |
728 | '@vitest/runner@1.6.1':
729 | dependencies:
730 | '@vitest/utils': 1.6.1
731 | p-limit: 5.0.0
732 | pathe: 1.1.2
733 |
734 | '@vitest/snapshot@1.6.1':
735 | dependencies:
736 | magic-string: 0.30.17
737 | pathe: 1.1.2
738 | pretty-format: 29.7.0
739 |
740 | '@vitest/spy@1.6.1':
741 | dependencies:
742 | tinyspy: 2.2.1
743 |
744 | '@vitest/utils@1.6.1':
745 | dependencies:
746 | diff-sequences: 29.6.3
747 | estree-walker: 3.0.3
748 | loupe: 2.3.7
749 | pretty-format: 29.7.0
750 |
751 | acorn-walk@8.3.4:
752 | dependencies:
753 | acorn: 8.14.0
754 |
755 | acorn@8.14.0: {}
756 |
757 | ansi-styles@5.2.0: {}
758 |
759 | assertion-error@1.1.0: {}
760 |
761 | cac@6.7.14: {}
762 |
763 | chai@4.5.0:
764 | dependencies:
765 | assertion-error: 1.1.0
766 | check-error: 1.0.3
767 | deep-eql: 4.1.4
768 | get-func-name: 2.0.2
769 | loupe: 2.3.7
770 | pathval: 1.1.1
771 | type-detect: 4.1.0
772 |
773 | check-error@1.0.3:
774 | dependencies:
775 | get-func-name: 2.0.2
776 |
777 | confbox@0.1.8: {}
778 |
779 | cross-spawn@7.0.6:
780 | dependencies:
781 | path-key: 3.1.1
782 | shebang-command: 2.0.0
783 | which: 2.0.2
784 |
785 | debug@4.4.0:
786 | dependencies:
787 | ms: 2.1.3
788 |
789 | deep-eql@4.1.4:
790 | dependencies:
791 | type-detect: 4.1.0
792 |
793 | diff-sequences@29.6.3: {}
794 |
795 | esbuild@0.21.5:
796 | optionalDependencies:
797 | '@esbuild/aix-ppc64': 0.21.5
798 | '@esbuild/android-arm': 0.21.5
799 | '@esbuild/android-arm64': 0.21.5
800 | '@esbuild/android-x64': 0.21.5
801 | '@esbuild/darwin-arm64': 0.21.5
802 | '@esbuild/darwin-x64': 0.21.5
803 | '@esbuild/freebsd-arm64': 0.21.5
804 | '@esbuild/freebsd-x64': 0.21.5
805 | '@esbuild/linux-arm': 0.21.5
806 | '@esbuild/linux-arm64': 0.21.5
807 | '@esbuild/linux-ia32': 0.21.5
808 | '@esbuild/linux-loong64': 0.21.5
809 | '@esbuild/linux-mips64el': 0.21.5
810 | '@esbuild/linux-ppc64': 0.21.5
811 | '@esbuild/linux-riscv64': 0.21.5
812 | '@esbuild/linux-s390x': 0.21.5
813 | '@esbuild/linux-x64': 0.21.5
814 | '@esbuild/netbsd-x64': 0.21.5
815 | '@esbuild/openbsd-x64': 0.21.5
816 | '@esbuild/sunos-x64': 0.21.5
817 | '@esbuild/win32-arm64': 0.21.5
818 | '@esbuild/win32-ia32': 0.21.5
819 | '@esbuild/win32-x64': 0.21.5
820 |
821 | estree-walker@3.0.3:
822 | dependencies:
823 | '@types/estree': 1.0.6
824 |
825 | execa@8.0.1:
826 | dependencies:
827 | cross-spawn: 7.0.6
828 | get-stream: 8.0.1
829 | human-signals: 5.0.0
830 | is-stream: 3.0.0
831 | merge-stream: 2.0.0
832 | npm-run-path: 5.3.0
833 | onetime: 6.0.0
834 | signal-exit: 4.1.0
835 | strip-final-newline: 3.0.0
836 |
837 | fsevents@2.3.3:
838 | optional: true
839 |
840 | get-func-name@2.0.2: {}
841 |
842 | get-stream@8.0.1: {}
843 |
844 | human-signals@5.0.0: {}
845 |
846 | is-stream@3.0.0: {}
847 |
848 | isexe@2.0.0: {}
849 |
850 | js-tokens@9.0.1: {}
851 |
852 | local-pkg@0.5.1:
853 | dependencies:
854 | mlly: 1.7.4
855 | pkg-types: 1.3.1
856 |
857 | loupe@2.3.7:
858 | dependencies:
859 | get-func-name: 2.0.2
860 |
861 | magic-string@0.30.17:
862 | dependencies:
863 | '@jridgewell/sourcemap-codec': 1.5.0
864 |
865 | merge-stream@2.0.0: {}
866 |
867 | mimic-fn@4.0.0: {}
868 |
869 | mlly@1.7.4:
870 | dependencies:
871 | acorn: 8.14.0
872 | pathe: 2.0.3
873 | pkg-types: 1.3.1
874 | ufo: 1.5.4
875 |
876 | ms@2.1.3: {}
877 |
878 | nanoid@3.3.8: {}
879 |
880 | npm-run-path@5.3.0:
881 | dependencies:
882 | path-key: 4.0.0
883 |
884 | onetime@6.0.0:
885 | dependencies:
886 | mimic-fn: 4.0.0
887 |
888 | p-limit@5.0.0:
889 | dependencies:
890 | yocto-queue: 1.1.1
891 |
892 | path-key@3.1.1: {}
893 |
894 | path-key@4.0.0: {}
895 |
896 | pathe@1.1.2: {}
897 |
898 | pathe@2.0.3: {}
899 |
900 | pathval@1.1.1: {}
901 |
902 | picocolors@1.1.1: {}
903 |
904 | pkg-types@1.3.1:
905 | dependencies:
906 | confbox: 0.1.8
907 | mlly: 1.7.4
908 | pathe: 2.0.3
909 |
910 | postcss@8.5.2:
911 | dependencies:
912 | nanoid: 3.3.8
913 | picocolors: 1.1.1
914 | source-map-js: 1.2.1
915 |
916 | pretty-format@29.7.0:
917 | dependencies:
918 | '@jest/schemas': 29.6.3
919 | ansi-styles: 5.2.0
920 | react-is: 18.3.1
921 |
922 | react-is@18.3.1: {}
923 |
924 | rollup@4.34.8:
925 | dependencies:
926 | '@types/estree': 1.0.6
927 | optionalDependencies:
928 | '@rollup/rollup-android-arm-eabi': 4.34.8
929 | '@rollup/rollup-android-arm64': 4.34.8
930 | '@rollup/rollup-darwin-arm64': 4.34.8
931 | '@rollup/rollup-darwin-x64': 4.34.8
932 | '@rollup/rollup-freebsd-arm64': 4.34.8
933 | '@rollup/rollup-freebsd-x64': 4.34.8
934 | '@rollup/rollup-linux-arm-gnueabihf': 4.34.8
935 | '@rollup/rollup-linux-arm-musleabihf': 4.34.8
936 | '@rollup/rollup-linux-arm64-gnu': 4.34.8
937 | '@rollup/rollup-linux-arm64-musl': 4.34.8
938 | '@rollup/rollup-linux-loongarch64-gnu': 4.34.8
939 | '@rollup/rollup-linux-powerpc64le-gnu': 4.34.8
940 | '@rollup/rollup-linux-riscv64-gnu': 4.34.8
941 | '@rollup/rollup-linux-s390x-gnu': 4.34.8
942 | '@rollup/rollup-linux-x64-gnu': 4.34.8
943 | '@rollup/rollup-linux-x64-musl': 4.34.8
944 | '@rollup/rollup-win32-arm64-msvc': 4.34.8
945 | '@rollup/rollup-win32-ia32-msvc': 4.34.8
946 | '@rollup/rollup-win32-x64-msvc': 4.34.8
947 | fsevents: 2.3.3
948 |
949 | shebang-command@2.0.0:
950 | dependencies:
951 | shebang-regex: 3.0.0
952 |
953 | shebang-regex@3.0.0: {}
954 |
955 | siginfo@2.0.0: {}
956 |
957 | signal-exit@4.1.0: {}
958 |
959 | source-map-js@1.2.1: {}
960 |
961 | stackback@0.0.2: {}
962 |
963 | std-env@3.8.0: {}
964 |
965 | strip-final-newline@3.0.0: {}
966 |
967 | strip-literal@2.1.1:
968 | dependencies:
969 | js-tokens: 9.0.1
970 |
971 | tinybench@2.9.0: {}
972 |
973 | tinypool@0.8.4: {}
974 |
975 | tinyspy@2.2.1: {}
976 |
977 | type-detect@4.1.0: {}
978 |
979 | ufo@1.5.4: {}
980 |
981 | vite-node@1.6.1:
982 | dependencies:
983 | cac: 6.7.14
984 | debug: 4.4.0
985 | pathe: 1.1.2
986 | picocolors: 1.1.1
987 | vite: 5.4.14
988 | transitivePeerDependencies:
989 | - '@types/node'
990 | - less
991 | - lightningcss
992 | - sass
993 | - sass-embedded
994 | - stylus
995 | - sugarss
996 | - supports-color
997 | - terser
998 |
999 | vite@5.4.14:
1000 | dependencies:
1001 | esbuild: 0.21.5
1002 | postcss: 8.5.2
1003 | rollup: 4.34.8
1004 | optionalDependencies:
1005 | fsevents: 2.3.3
1006 |
1007 | vitest@1.6.1:
1008 | dependencies:
1009 | '@vitest/expect': 1.6.1
1010 | '@vitest/runner': 1.6.1
1011 | '@vitest/snapshot': 1.6.1
1012 | '@vitest/spy': 1.6.1
1013 | '@vitest/utils': 1.6.1
1014 | acorn-walk: 8.3.4
1015 | chai: 4.5.0
1016 | debug: 4.4.0
1017 | execa: 8.0.1
1018 | local-pkg: 0.5.1
1019 | magic-string: 0.30.17
1020 | pathe: 1.1.2
1021 | picocolors: 1.1.1
1022 | std-env: 3.8.0
1023 | strip-literal: 2.1.1
1024 | tinybench: 2.9.0
1025 | tinypool: 0.8.4
1026 | vite: 5.4.14
1027 | vite-node: 1.6.1
1028 | why-is-node-running: 2.3.0
1029 | transitivePeerDependencies:
1030 | - less
1031 | - lightningcss
1032 | - sass
1033 | - sass-embedded
1034 | - stylus
1035 | - sugarss
1036 | - supports-color
1037 | - terser
1038 |
1039 | which@2.0.2:
1040 | dependencies:
1041 | isexe: 2.0.0
1042 |
1043 | why-is-node-running@2.3.0:
1044 | dependencies:
1045 | siginfo: 2.0.0
1046 | stackback: 0.0.2
1047 |
1048 | yocto-queue@1.1.1: {}
1049 |
--------------------------------------------------------------------------------
/tiny-vue3/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | admindb/
5 | npm-debug.log*
6 | yarn-debug.log*
7 | yarn-error.log*
8 | /test/unit/coverage/
9 | /test/e2e/reports/
10 | selenium-debug.log
11 |
12 | # Editor directories and files
13 | .idea
14 | .vscode
15 | *.suo
16 | *.ntvs*
17 | *.njsproj
18 | *.sln
19 | package-lock.json
20 | .env.production.local
21 |
--------------------------------------------------------------------------------
/tiny-vue3/README.md:
--------------------------------------------------------------------------------
1 | # toy-vue3
2 | 关于vue3核心源码的reactivity、runtime-core、runtime-dom、compiler-core等模块的mini版实现。
3 |
4 | # Tasking(任务拆分)
5 | reactivity、runtime-core、runtime-dom、compiler-core
6 | ## reactivity
7 |
8 | :white_check_mark: 支持 jest 环境搭建
9 | :white_check_mark: 支持 effect & reactive 依赖搜集 & 依赖触发
10 | :white_check_mark: 支持 effect.runner
11 | :white_check_mark: 支持 effect.scheduler
12 | :white_check_mark: 支持 effect.stop
13 | :white_check_mark: 支持 readonly
14 | :white_check_mark: 支持 isReactive、isReadonly
15 | :white_check_mark: 支持嵌套 reactive
16 | :white_check_mark: 支持 shallowReadonly
17 | :white_check_mark: 支持 isProxy
18 | :white_check_mark: 支持 toRaw
19 | :white_check_mark: 支持 ref
20 | :white_check_mark: 支持 isRef、unRef
21 | :white_check_mark: 支持 proxyRefs
22 | :white_check_mark: 支持 computed 计算属性
23 |
24 | ## runtime-core
25 |
26 | :white_check_mark: 初始化 Component
27 | :white_check_mark: 支持 rollup 打包
28 | :white_check_mark: 初始化 element 主流程
29 | :white_check_mark: 支持 组件代理对象
30 | :white_check_mark: 支持 shapeFlags
31 | :white_check_mark: 支持 注册事件功能
32 | :white_check_mark: 实现 组件 props 逻辑
33 | :white_check_mark: 实现 组件 emit 功能
34 | :white_check_mark: 支持 slots 功能
35 | :white_check_mark: 支持 Fragment 和 Text 类型节点
36 | :white_check_mark: 支持 getCurrentInstance
37 | :white_check_mark: 支持 provide/inject
38 |
39 | ## runtime-dom
40 |
41 | :white_check_mark: 实现自定义渲染器 custom/renderer
42 | :white_check_mark: 更新 element 流程搭建
43 | :white_check_mark: 更新 element props
44 | :white_check_mark: 更新 element children
45 | :white_check_mark: 实现 双端对比 diff 算法
46 | :white_check_mark: 实现 组件更新
47 | :white_check_mark: 支持 nextTick
48 |
49 | ## compiler-core
50 |
51 | :white_check_mark: 实现 解析插值功能
52 | :white_check_mark: 实现 解析 element 标签
53 | :white_check_mark: 实现 解析 text 功能
54 | :white_check_mark: 实现 解析三种联合类型
55 | :white_check_mark: 实现 parse&有限状态机
56 | :white_check_mark: 实现 transform
57 | :white_check_mark: 实现 代码生成 string 类型
58 | :white_check_mark: 实现 代码生成插值类型 cmproj
59 | :white_check_mark: 实现 代码生成三种联合类型
60 | :white_check_mark: 实现 编译 template 成 render 函数
61 |
62 | ## main
63 | :white_check_mark: 实现 monorepo 代码管理
64 | :white_check_mark: 测试框架更换,从 jest 升级为 vitest
65 |
66 | # 项目应用
67 | ## 项目初始化
68 |
69 | ```javascript
70 | yarn install
71 | ```
72 |
73 | ## 项目动态打包
74 | ```javascript
75 | yarn build:w
76 | ```
77 |
78 | ## 项目用例查看
79 |
80 | 打开 example 中的demo文件,用vscode的live-server可以启动查看。
--------------------------------------------------------------------------------
/tiny-vue3/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', {targets: {node: 'current'}}],
4 | '@babel/preset-typescript',
5 | ],
6 | };
--------------------------------------------------------------------------------
/tiny-vue3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tiny-vue3",
3 | "version": "1.0.0",
4 | "description": "A project about learning vue3 knowledge",
5 | "main": "./packages/vue/dist/tiny-vue3.cjs.js",
6 | "module": "./packages/vue/dist/tiny-vue3.esm.js",
7 | "author": "chan",
8 | "license": "MIT",
9 | "scripts": {
10 | "build:w": "rollup -c rollup.config.js --watch",
11 | "build": "rollup -c rollup.config.js",
12 | "build:type": "tsc -p ./tsconfig.type.json",
13 | "test:w": "vitest --watch all",
14 | "test": "vitest"
15 | },
16 | "devDependencies": {
17 | "@babel/core": "^7.17.8",
18 | "@babel/preset-env": "^7.16.11",
19 | "@babel/preset-typescript": "^7.16.7",
20 | "@rollup/plugin-typescript": "^8.3.1",
21 | "rollup": "^2.70.1",
22 | "tslib": "^2.3.1",
23 | "typescript": "^4.6.3",
24 | "vitest": "^0.26.3"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/__test__/__snapshots__/codegen.spec.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1
2 |
3 | exports[`codegen > element 1`] = `
4 | "const { toDisplayString:_toDisplayString, createElementVNode:_createElementVNode } = Vue
5 | return function render(_ctx, _cache){return _createElementVNode('div', null, 'hi,')}"
6 | `;
7 |
8 | exports[`codegen > interpolation 1`] = `
9 | "const { toDisplayString:_toDisplayString } = Vue
10 | return function render(_ctx, _cache){return _toDisplayString(_ctx.message)}"
11 | `;
12 |
13 | exports[`codegen > string 1`] = `
14 | "
15 | return function render(_ctx, _cache){return 'hi'}"
16 | `;
17 |
18 | exports[`codegen element 1`] = `
19 | "const { toDisplayString:_toDisplayString, createElementVNode:_createElementVNode } = Vue
20 | return function render(_ctx, _cache){return _createElementVNode('div', null, 'hi,')}"
21 | `;
22 |
23 | exports[`codegen interpolation 1`] = `
24 | "const { toDisplayString:_toDisplayString } = Vue
25 | return function render(_ctx, _cache){return _toDisplayString(_ctx.message)}"
26 | `;
27 |
28 | exports[`codegen string 1`] = `
29 | "
30 | return function render(_ctx, _cache){return 'hi'}"
31 | `;
32 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/__test__/codegen.spec.ts:
--------------------------------------------------------------------------------
1 | import { baseParse } from "../src/parse"
2 | import { generate } from "../src/codegen";
3 | import { transform } from "../src/transform"
4 | import { transformExpression } from "../src/transforms/transformExpression";
5 | import { transformText } from "../src/transforms/transformText";
6 | import { transformElement } from "../src/transforms/transformElement";
7 |
8 | describe("codegen", () => {
9 | it("string", () => {
10 | const ast = baseParse("hi")
11 | transform(ast)
12 | const { code } = generate(ast)
13 |
14 | // 快照测试
15 | expect(code).toMatchSnapshot()
16 | })
17 |
18 | it("interpolation", () => {
19 | const ast = baseParse("{{message}}");
20 | transform(ast, {
21 | nodeTransforms: [transformExpression],
22 | })
23 | const { code } = generate(ast)
24 | expect(code).toMatchSnapshot()
25 | })
26 |
27 | it("element", () => {
28 | const ast: any = baseParse("hi,{{message}}
")
29 | transform(ast, {
30 | nodeTransforms: [transformExpression, transformText, transformElement]
31 | })
32 |
33 | const { code } = generate(ast)
34 | expect(code).toMatchSnapshot()
35 | })
36 | })
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/__test__/parse.spec.ts:
--------------------------------------------------------------------------------
1 | import { baseParse } from "../src/parse"
2 | import { NodeTypes } from "../src/ast"
3 |
4 | describe("Parse", () => {
5 | describe("interpolation", () => {
6 | test("simple interpolation", () => {
7 | const ast = baseParse("{{message}}")
8 | // root
9 | expect(ast.children[0]).toStrictEqual({
10 | type: NodeTypes.INTERPOLATION,
11 | content: {
12 | type: NodeTypes.SIMPLE_EXPRESSION,
13 | content: "message"
14 | }
15 | })
16 | })
17 | })
18 |
19 | describe("element", () => {
20 | it("simple element div", () => {
21 | const ast = baseParse("")
22 |
23 | expect(ast.children[0]).toStrictEqual({
24 | type: NodeTypes.ELEMENT,
25 | tag: "div",
26 | children: []
27 | })
28 | })
29 | })
30 |
31 | describe("text", () => {
32 | it("simple text", () => {
33 | const ast = baseParse("some text")
34 |
35 | expect(ast.children[0]).toStrictEqual({
36 | type: NodeTypes.TEXT,
37 | content: "some text"
38 | })
39 | })
40 | })
41 |
42 | test("hello world", () => {
43 | const ast = baseParse("hi,{{message}}
")
44 |
45 | expect(ast.children[0]).toStrictEqual({
46 | type: NodeTypes.ELEMENT,
47 | tag: "div",
48 | children: [
49 | {
50 | type: NodeTypes.TEXT,
51 | content: "hi,",
52 | },
53 | {
54 | type: NodeTypes.INTERPOLATION,
55 | content: {
56 | type: NodeTypes.SIMPLE_EXPRESSION,
57 | content: "message",
58 | },
59 | }
60 | ]
61 | })
62 | })
63 |
64 | test("Nested element", () => {
65 | const ast = baseParse("")
66 |
67 | expect(ast.children[0]).toStrictEqual({
68 | type: NodeTypes.ELEMENT,
69 | tag: "div",
70 | children: [
71 | {
72 | type: NodeTypes.ELEMENT,
73 | tag: "p",
74 | children: [
75 | {
76 | type: NodeTypes.TEXT,
77 | content: "hi",
78 | },
79 | ],
80 | },
81 | {
82 | type: NodeTypes.INTERPOLATION,
83 | content: {
84 | type: NodeTypes.SIMPLE_EXPRESSION,
85 | content: "message",
86 | },
87 | }
88 | ]
89 | })
90 | })
91 |
92 | test("should throw error when lack end tag", () => {
93 | expect(() => {
94 | baseParse("
")
95 | }).toThrow(`缺少结束标签:span`)
96 | })
97 | })
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/__test__/transform.spec.ts:
--------------------------------------------------------------------------------
1 | import { NodeTypes } from "../src/ast";
2 | import { baseParse } from "../src/parse";
3 | import { transform } from "../src/transform";
4 |
5 | describe("transform", () => {
6 | it("happy path", () => {
7 | const ast = baseParse("hi,{{message}}
");
8 |
9 | const plugin = (node) => {
10 | if (node.type === NodeTypes.TEXT) {
11 | node.content = node.content + " mini-vue";
12 | }
13 | };
14 |
15 | transform(ast, {
16 | nodeTransforms: [plugin],
17 | });
18 |
19 | const nodeText = ast.children[0].children[0];
20 | expect(nodeText.content).toBe("hi, mini-vue");
21 | });
22 | });
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tiny-vue/compiler-core",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@tiny-vue/shared": "workspace:^1.0.0"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/src/ast.ts:
--------------------------------------------------------------------------------
1 | import { CREATE_ELEMENT_VNODE } from "./runtimeHelpers";
2 |
3 | export const enum NodeTypes {
4 | INTERPOLATION,
5 | SIMPLE_EXPRESSION,
6 | ELEMENT,
7 | TEXT,
8 | ROOT,
9 | COMPOUND_EXPRESSION
10 | }
11 |
12 | export function createVNodeCall(context, tag, props, children) {
13 | context.helper(CREATE_ELEMENT_VNODE)
14 | return {
15 | type: NodeTypes.ELEMENT,
16 | tag,
17 | props,
18 | children
19 | }
20 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/src/codegen.ts:
--------------------------------------------------------------------------------
1 | import { isString } from "@tiny-vue/shared"
2 | import { NodeTypes } from "./ast"
3 | import { CREATE_ELEMENT_VNODE, helperMapName, TO_DISPLAY_STRING } from "./runtimeHelpers"
4 |
5 | export function generate(ast) {
6 | const context = createCodegenContext()
7 | const { push } = context
8 |
9 | genFunctionPreamble(ast, context)
10 |
11 | const functionName = "render"
12 | const args = ["_ctx", "_cache"]
13 | const signature = args.join(", ")
14 |
15 | push(`function ${functionName}(${signature}){`)
16 | push("return ")
17 | genNode(ast.codegenNode, context)
18 | push("}")
19 |
20 | return {
21 | code: context.code
22 | }
23 | }
24 |
25 | function genFunctionPreamble(ast: any, context: any) {
26 | const { push } = context
27 | const VueBinging = "Vue"
28 | const aliasHelper = (s) => `${helperMapName[s]}:_${helperMapName[s]}`
29 | if (ast.helpers.length > 0) {
30 | push(`const { ${ast.helpers.map(aliasHelper).join(", ")} } = ${VueBinging}`)
31 | }
32 | push("\n")
33 | push("return ")
34 | }
35 |
36 | function createCodegenContext(): any {
37 | const context = {
38 | code: "",
39 | push(source) {
40 | context.code += source
41 | },
42 | helper(key) {
43 | return `_${helperMapName[key]}`
44 | }
45 | }
46 |
47 | return context
48 | }
49 |
50 | function genNode(node: any, context) {
51 | switch (node.type) {
52 | case NodeTypes.TEXT:
53 | genText(node, context)
54 | break
55 | case NodeTypes.INTERPOLATION:
56 | genInterpolation(node, context)
57 | break
58 | case NodeTypes.SIMPLE_EXPRESSION:
59 | genExpression(node, context)
60 | break
61 | case NodeTypes.ELEMENT:
62 | genElement(node, context)
63 | break
64 | case NodeTypes.COMPOUND_EXPRESSION:
65 | genCompoundExpression(node, context)
66 | break
67 | default:
68 | break
69 | }
70 | }
71 |
72 | function genCompoundExpression(node: any, context: any) {
73 | const { push } = context
74 | const children = node.children
75 | for (let i = 0; i < children.length; i++) {
76 | const child = children[i];
77 | if (isString(child)) {
78 | push(child)
79 | } else {
80 | genNode(child, context)
81 | }
82 | }
83 | }
84 |
85 | function genElement(node: any, context: any) {
86 | const { push, helper } = context
87 | const { tag, children, props } = node
88 | push(`${helper(CREATE_ELEMENT_VNODE)}(`)
89 | // genNode(children, context)
90 | genNodeList(genNullable([tag, props, children]), context)
91 | push(')')
92 | }
93 |
94 | function genNodeList(nodes, context) {
95 | const { push } = context
96 | for (let i = 0; i < nodes.length; i++) {
97 | const node = nodes[i];
98 | if (isString(node)) {
99 | push(node)
100 | } else {
101 | genNode(node, context)
102 | }
103 |
104 | if (i < nodes.length - 1) {
105 | push(", ")
106 | }
107 | }
108 | }
109 |
110 | function genNullable(args: any) {
111 | return args.map((arg) => arg || "null")
112 | }
113 |
114 | function genText(node: any, context: any) {
115 | const { push } = context
116 | push(`'${node.content}'`)
117 | }
118 |
119 | function genExpression(node: any, context: any) {
120 | const { push } = context
121 | push(`${node.content}`)
122 | }
123 |
124 | function genInterpolation(node: any, context: any) {
125 | // Implement
126 | const { push, helper } = context
127 | push(`${helper(TO_DISPLAY_STRING)}(`)
128 | genNode(node.content, context)
129 | push(')')
130 | }
131 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/src/compile.ts:
--------------------------------------------------------------------------------
1 | import { generate } from "./codegen"
2 | import { baseParse } from "./parse"
3 | import { transform } from "./transform"
4 | import { transformElement } from "./transforms/transformElement"
5 | import { transformExpression } from "./transforms/transformExpression"
6 | import { transformText } from "./transforms/transformText"
7 |
8 | export function baseCompile(template) {
9 | const ast: any = baseParse(template)
10 | transform(ast, {
11 | nodeTransforms: [transformExpression, transformElement, transformText],
12 | })
13 |
14 | return generate(ast)
15 | }
16 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./compile"
2 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/src/parse.ts:
--------------------------------------------------------------------------------
1 | import { NodeTypes } from "./ast"
2 |
3 | const enum TagType {
4 | Start,
5 | End
6 | }
7 | export function baseParse(content: string) {
8 | const context = createParseContext(content)
9 | return createRoot(parseChildren(context, []))
10 | }
11 |
12 | function parseChildren(context, ancestors) {
13 | let nodes: any = []
14 |
15 | while(!isEnd(context, ancestors)) {
16 | let node
17 | const s = context.source
18 | if (s.startsWith("{{")) {
19 | node = parseInterpolation(context)
20 | } else if (s[0] === "<") {
21 | if (/[a-z]/i.test(s[1])) {
22 | node = parseElement(context, ancestors)
23 | }
24 | }
25 |
26 | if (!node) {
27 | node = parseText(context)
28 | }
29 | nodes.push(node)
30 | }
31 | return nodes
32 | }
33 |
34 | function isEnd(context, ancestors) {
35 | // 2.当遇到结束标签的时候
36 | const s = context.source
37 | if (s.startsWith("")) {
38 | for (let i = ancestors.length - 1; i >= 0; i--) {
39 | const tag = ancestors[i].tag;
40 | if (startsWithEndTagOpen(s, tag)) {
41 | return true
42 | }
43 | }
44 | }
45 | // 1. source 有值的时候
46 | return !s
47 | }
48 |
49 | function parseText(context: any) {
50 | let endIndex = context.source.length
51 | let endTokens = ["<", "{{"]
52 |
53 | for (let i = 0; i < endTokens.length; i++) {
54 | const index = context.source.indexOf(endTokens[i]);
55 | if (index !== -1 && endIndex > index) {
56 | endIndex = index;
57 | }
58 | }
59 |
60 | // 1. 获取content
61 | const content = parseTextData(context, endIndex)
62 |
63 | return {
64 | type: NodeTypes.TEXT,
65 | content,
66 | }
67 | }
68 |
69 | function parseTextData(context: any, length: number) {
70 | const content = context.source.slice(0, length)
71 |
72 | // 2. 推进
73 | advanceBy(context, length)
74 | return content
75 | }
76 |
77 | function parseElement(context: any, ancestors) {
78 | // Implement
79 | // 1. 解析 tag
80 | const element:any = parseTag(context, TagType.Start)
81 | ancestors.push(element)
82 | element.children = parseChildren(context, ancestors)
83 | ancestors.pop()
84 |
85 | if (startsWithEndTagOpen(context.source, element.tag)) {
86 | parseTag(context, TagType.End)
87 | } else {
88 | throw new Error(`缺少结束标签:${element.tag}`)
89 | }
90 | return element
91 | }
92 |
93 | function startsWithEndTagOpen(source, tag) {
94 | return (
95 | source.startsWith("") &&
96 | source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase()
97 | )
98 | }
99 |
100 | function parseTag(context: any, type: TagType) {
101 | const match: any = /^<\/?([a-z]*)/i.exec(context.source)
102 | const tag = match[1]
103 | // 2. 删除处理完成的代码
104 | advanceBy(context, match[0].length)
105 | advanceBy(context, 1)
106 |
107 | if (type === TagType.End) return
108 |
109 | return {
110 | type: NodeTypes.ELEMENT,
111 | tag
112 | }
113 | }
114 |
115 | function parseInterpolation(context) {
116 | // {{message}}
117 | const openDelimiter = "{{"
118 | const closeDelimiter = "}}"
119 |
120 | const closeIndex = context.source.indexOf(
121 | closeDelimiter,
122 | openDelimiter.length
123 | )
124 | advanceBy(context, openDelimiter.length)
125 |
126 | const rawContentLength = closeIndex - openDelimiter.length
127 | const rawContent = parseTextData(context, rawContentLength)
128 | const content = rawContent.trim()
129 |
130 | advanceBy(context, closeDelimiter.length)
131 | return {
132 | type: NodeTypes.INTERPOLATION,
133 | content: {
134 | type: NodeTypes.SIMPLE_EXPRESSION,
135 | content: content
136 | }
137 | }
138 | }
139 |
140 | // 推进函数
141 | function advanceBy(context: any, length: number) {
142 | context.source = context.source.slice(length)
143 | }
144 |
145 | function createRoot(children) {
146 | return {
147 | children,
148 | type: NodeTypes.ROOT
149 | }
150 | }
151 |
152 | function createParseContext(content: string): any {
153 | return {
154 | source: content
155 | }
156 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/src/runtimeHelpers.ts:
--------------------------------------------------------------------------------
1 | export const TO_DISPLAY_STRING = Symbol("toDisplayString")
2 | export const CREATE_ELEMENT_VNODE = Symbol("createElementVNode")
3 |
4 | export const helperMapName = {
5 | [TO_DISPLAY_STRING]: 'toDisplayString',
6 | [CREATE_ELEMENT_VNODE]: 'createElementVNode'
7 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/src/transform.ts:
--------------------------------------------------------------------------------
1 | import { NodeTypes } from "./ast"
2 | import { TO_DISPLAY_STRING } from "./runtimeHelpers"
3 |
4 | export function transform(root, options = {}) {
5 | const context = createTransformContext(root, options)
6 | // 1. 遍历 - 深度优先搜索
7 | traverseNode(root, context)
8 | createRootCodegen(root)
9 |
10 | root.helpers = [...context.helpers.keys()]
11 | // 2. 修改 text content
12 | }
13 |
14 | function createRootCodegen(root: any) {
15 | const child = root.children[0]
16 | if (child.type === NodeTypes.ELEMENT) {
17 | root.codegenNode = child.codegenNode
18 | } else {
19 | root.codegenNode = child
20 | }
21 | }
22 |
23 | function createTransformContext(root: any, options: any):any {
24 | const context = {
25 | root,
26 | nodeTransforms: options.nodeTransforms || [],
27 | helpers: new Map(),
28 | helper(key) {
29 | context.helpers.set(key, 1)
30 | }
31 | }
32 |
33 | return context
34 | }
35 |
36 | function traverseNode(node: any, context) {
37 | const nodeTransforms = context.nodeTransforms
38 | const exitFns: any = []
39 | for (let i = 0; i < nodeTransforms.length; i++) {
40 | const transform = nodeTransforms[i];
41 | const onExit = transform(node, context)
42 | if (onExit) exitFns.push(onExit)
43 | }
44 |
45 | switch (node.type) {
46 | case NodeTypes.INTERPOLATION:
47 | context.helper(TO_DISPLAY_STRING)
48 | break
49 | case NodeTypes.ROOT:
50 | case NodeTypes.ELEMENT:
51 | traverseChildren(node, context)
52 | break
53 | default:
54 | break
55 | }
56 |
57 | let i = exitFns.length
58 | while(i--) {
59 | exitFns[i]()
60 | }
61 | }
62 |
63 | function traverseChildren(node: any, context: any) {
64 | const children = node.children
65 |
66 | for (let i = 0; i < children.length; i++) {
67 | const node = children[i];
68 | traverseNode(node, context)
69 | }
70 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/src/transforms/transformElement.ts:
--------------------------------------------------------------------------------
1 | import { createVNodeCall, NodeTypes } from "../ast";
2 |
3 | export function transformElement(node, context) {
4 | if (node.type === NodeTypes.ELEMENT) {
5 | return () => {
6 | // 中间处理层
7 | // tag
8 | const vnodeTag = `'${node.tag}'`
9 |
10 | // props
11 | let vnodeProps
12 |
13 | // children
14 | const children = node.children
15 | let vnodeChildren = children[0]
16 |
17 | node.codegenNode = createVNodeCall(
18 | context,
19 | vnodeTag,
20 | vnodeProps,
21 | vnodeChildren
22 | )
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/src/transforms/transformExpression.ts:
--------------------------------------------------------------------------------
1 | import { NodeTypes } from "../ast";
2 |
3 |
4 | export function transformExpression(node) {
5 | if (node.type === NodeTypes.INTERPOLATION) {
6 | node.content = processExpression(node.content)
7 | }
8 | }
9 |
10 | function processExpression(node: any) {
11 | node.content = `_ctx.${node.content}`
12 | return node
13 | }
14 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/src/transforms/transformText.ts:
--------------------------------------------------------------------------------
1 | import { NodeTypes } from "../ast";
2 | import { isText } from "../utils";
3 |
4 | export function transformText(node) {
5 | if (node.type === NodeTypes.ELEMENT) {
6 | return () => {
7 | const { children } = node
8 |
9 | let currentContainer
10 | for (let i = 0; i < children.length; i++) {
11 | const child = children[i];
12 |
13 | if (isText(child)) {
14 | for (let j = i + 1; j < children.length; j++) {
15 | const next = children[j];
16 |
17 | if (isText(next)) {
18 | if (!currentContainer) {
19 | currentContainer = children[i] = {
20 | type: NodeTypes.COMPOUND_EXPRESSION,
21 | children: [child]
22 | }
23 | }
24 |
25 | currentContainer.children.push(" + ")
26 | currentContainer.children.push(next)
27 | children.splice(j, 1)
28 | j--
29 | } else {
30 | currentContainer = undefined
31 | break
32 | }
33 | }
34 | }
35 | }
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/compiler-core/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { NodeTypes } from "./ast"
2 | export function isText(node) {
3 | return (
4 | node.type === NodeTypes.TEXT || node.type === NodeTypes.INTERPOLATION
5 | )
6 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/reactivity/__tests__/computed.spec.ts:
--------------------------------------------------------------------------------
1 | import { computed } from "../src/computed"
2 | import { reactive } from "../src/reactive"
3 | import { vi } from 'vitest'
4 |
5 | describe('reactivity/computed', () => {
6 | it('happy path', () => {
7 | // ref
8 | // .value
9 | // 1. 缓存
10 | const user = reactive({
11 | age: 1
12 | })
13 | const age = computed(() => {
14 | return user.age
15 | })
16 | expect(age.value).toBe(1)
17 | })
18 |
19 | it('should compute lazily', () => {
20 | const value = reactive({
21 | foo: 1
22 | })
23 | const getter = vi.fn(() => {
24 | return value.foo
25 | })
26 | const cValue = computed(getter)
27 |
28 | // lazy
29 | expect(getter).not.toHaveBeenCalled()
30 | expect(cValue.value).toBe(1)
31 | expect(getter).toHaveBeenCalledTimes(1)
32 |
33 | // should not compute again
34 | cValue.value
35 | expect(getter).toHaveBeenCalledTimes(1)
36 |
37 | // should not compute until needed
38 | value.foo = 2 // 触发trigger -> effect -> set 重新执行了
39 | expect(getter).toHaveBeenCalledTimes(1)
40 |
41 | // now it should compute
42 | expect(cValue.value).toBe(2)
43 | expect(getter).toHaveBeenCalledTimes(2)
44 |
45 | // should not compute again
46 | cValue.value
47 | expect(getter).toHaveBeenCalledTimes(2)
48 | })
49 | })
--------------------------------------------------------------------------------
/tiny-vue3/packages/reactivity/__tests__/effect.spec.ts:
--------------------------------------------------------------------------------
1 | import { reactive } from '../src/reactive'
2 | import { effect, stop } from '../src/effect'
3 | import { vi } from 'vitest'
4 |
5 | describe('effect', () => {
6 | it('happy path', () => {
7 | const user = reactive({
8 | age: 10
9 | })
10 |
11 | let nextAge
12 | effect(() => {
13 | nextAge = user.age + 1
14 | })
15 |
16 | expect(nextAge).toBe(11)
17 |
18 | // update
19 | user.age++
20 | expect(nextAge).toBe(12)
21 | })
22 |
23 | it('should return runner when you call effect', () => {
24 | // 1. effect(fn) -> function(runner) -> fn -> return
25 | let foo = 10
26 | const runner = effect(() => {
27 | foo++
28 | return 'foo'
29 | })
30 |
31 | expect(foo).toBe(11)
32 | const r = runner()
33 | expect(foo).toBe(12)
34 | expect(r).toBe('foo')
35 | })
36 |
37 | it('scheduler', () => {
38 | // 1. 通过 effect 的第二参数给定的一个 scheduler 的 fn
39 | // 2. effect 第一次执行的时候还会执行 fn
40 | // 3. 当响应式对象 set update 不会执行 fn 而是执行 scheduler
41 | // 4. 如果说当执行 runner 的时候,会再次执行 fn
42 | let dummy
43 | let run: any
44 | const scheduler = vi.fn(() => {
45 | run = runner
46 | })
47 | const obj = reactive({ foo: 1 })
48 | const runner = effect(
49 | () => {
50 | dummy = obj.foo
51 | },
52 | { scheduler }
53 | )
54 | expect(scheduler).not.toHaveBeenCalled()
55 | expect(dummy).toBe(1)
56 | // should be called on first trigger
57 | // TODO 下面的有误
58 | obj.foo++
59 | expect(scheduler).toHaveBeenCalledTimes(1)
60 | // should not run yet
61 | expect(dummy).toBe(1)
62 | // manually run
63 | run()
64 | // should have run
65 | expect(dummy).toBe(2)
66 | })
67 |
68 | it('stop', () => {
69 | let dummy
70 | const obj = reactive({ prop: 1 })
71 | const runner = effect(() => {
72 | dummy = obj.prop
73 | })
74 | obj.prop = 2
75 | expect(dummy).toBe(2)
76 | stop(runner)
77 | // obj.prop = 3
78 | obj.prop++
79 | expect(dummy).toBe(2)
80 |
81 | // stopped effect should still be manually callable
82 | runner()
83 | expect(dummy).toBe(3)
84 | })
85 |
86 | it('onStop', () => {
87 | const obj = reactive({ foo: 1 })
88 | const onStop = vi.fn()
89 | let dummy
90 | const runner = effect(
91 | () => {
92 | dummy = obj.foo
93 | },
94 | {
95 | onStop
96 | }
97 | )
98 |
99 | stop(runner)
100 | expect(onStop).toBeCalledTimes(1)
101 | })
102 | })
--------------------------------------------------------------------------------
/tiny-vue3/packages/reactivity/__tests__/reactive.spec.ts:
--------------------------------------------------------------------------------
1 | import { reactive, isReactive, isProxy } from '../src/reactive'
2 |
3 | describe("reactive", () => {
4 | it("happy path", () => {
5 | const original = { foo: 1}
6 | const observed = reactive(original)
7 | expect(observed).not.toBe(original)
8 | expect(observed.foo).toBe(1)
9 | expect(isReactive(observed)).toBe(true)
10 | expect(isReactive(original)).toBe(false)
11 | expect(isProxy(original)).toBe(false)
12 | })
13 |
14 | test("nested reactive", () => {
15 | const original = {
16 | nested: {
17 | foo: 1
18 | },
19 | array: [{ bar: 2}]
20 | }
21 | const observed = reactive(original)
22 | expect(isReactive(observed.nested)).toBe(true)
23 | expect(isReactive(observed.array)).toBe(true)
24 | expect(isReactive(observed.array[0])).toBe(true)
25 |
26 | })
27 | })
--------------------------------------------------------------------------------
/tiny-vue3/packages/reactivity/__tests__/readonly.spec.ts:
--------------------------------------------------------------------------------
1 | import { isReadonly, readonly, isProxy } from '../src/reactive'
2 | import { vi } from 'vitest'
3 | describe('readonly', () => {
4 | it("happy path", () => {
5 | // no set
6 | const original = { foo: 1, bar: { baz: 2 } }
7 | const wrapped = readonly(original)
8 | expect(wrapped).not.toBe(original)
9 | expect(isReadonly(wrapped)).toBe(true)
10 | expect(isReadonly(original)).toBe(false)
11 | expect(isReadonly(wrapped.bar)).toBe(true)
12 | expect(isReadonly(original.bar)).toBe(false)
13 | expect(isProxy(wrapped)).toBe(true)
14 | })
15 |
16 | it("should call console.warn when set", () => {
17 | // console.warn()
18 | // mock
19 | console.warn = vi.fn()
20 |
21 | const user = readonly({
22 | age: 10
23 | })
24 |
25 | user.age = 11
26 | expect(console.warn).toBeCalled()
27 | })
28 | })
--------------------------------------------------------------------------------
/tiny-vue3/packages/reactivity/__tests__/ref.spec.ts:
--------------------------------------------------------------------------------
1 | import { effect } from "../src/effect"
2 | import { reactive } from "../src/reactive"
3 | import { ref, isRef, unRef, proxyRefs } from "../src/ref"
4 |
5 | describe("ref", () => {
6 | it('happy path', () => {
7 | const a = ref(1)
8 | expect(a.value).toBe(1)
9 | // a.value = 2
10 | // expect(a.value).toBe(2)
11 | })
12 |
13 | it('should be reactive', () => {
14 | const a = ref(1)
15 | let dummy
16 | let calls = 0
17 | effect(() => {
18 | calls++
19 | dummy = a.value
20 | })
21 | expect(calls).toBe(1)
22 | expect(dummy).toBe(1)
23 | a.value = 2
24 | expect(calls).toBe(2)
25 | expect(dummy).toBe(2)
26 | // same value should not trigger
27 | a.value = 2
28 | expect(calls).toBe(2)
29 | expect(dummy).toBe(2)
30 | })
31 |
32 | it('should make nested properties reactive', () => {
33 | const a = ref({
34 | count: 1
35 | })
36 | let dummy
37 | effect(() => {
38 | dummy = a.value.count
39 | })
40 | expect(dummy).toBe(1)
41 | a.value.count = 2
42 | expect(dummy).toBe(2)
43 | })
44 |
45 | it("isRef", () => {
46 | const a = ref(1)
47 | const user = reactive({
48 | age: 1
49 | })
50 | expect(isRef(a)).toBe(true)
51 | expect(isRef(1)).toBe(false)
52 | expect(isRef(user)).toBe(false)
53 | })
54 |
55 | it("unRef", () => {
56 | const a = ref(1)
57 | expect(unRef(a)).toBe(1)
58 | expect(unRef(1)).toBe(1)
59 | })
60 |
61 | it("proxyRefs", () => {
62 | const user = {
63 | age: ref(10),
64 | name: "chan"
65 | }
66 | // get -> age
67 | // ref -> 那么给他返回.value
68 | // not ref -> value
69 | const proxyUser = proxyRefs(user)
70 | expect(user.age.value).toBe(10)
71 | expect(proxyUser.age).toBe(10)
72 | expect(proxyUser.name).toBe('chan')
73 |
74 | // set -> ref .value
75 | proxyUser.age = 20
76 | expect(proxyUser.age).toBe(20)
77 | expect(user.age.value).toBe(20)
78 |
79 | proxyUser.age = ref(10)
80 | expect(user.age.value).toBe(10)
81 | expect(proxyUser.age).toBe(10)
82 | })
83 | })
--------------------------------------------------------------------------------
/tiny-vue3/packages/reactivity/__tests__/shallowReadonly.spec.ts:
--------------------------------------------------------------------------------
1 | import { isReadonly, shallowReadonly } from '../src/reactive'
2 | import { vi } from 'vitest'
3 |
4 | describe("shallowReadonly", () => {
5 | test("should not make non-reactive properties reactive", () => {
6 | const props = shallowReadonly({ n: { foo: 1} })
7 | expect(isReadonly(props)).toBe(true)
8 | expect(isReadonly(props.n)).toBe(true)
9 | })
10 | it("should call console.warn when set", () => {
11 | // console.warn()
12 | // mock
13 | console.warn = vi.fn()
14 |
15 | const user = shallowReadonly({
16 | age: 10
17 | })
18 |
19 | user.age = 11
20 | expect(console.warn).toHaveBeenCalled()
21 | })
22 | })
--------------------------------------------------------------------------------
/tiny-vue3/packages/reactivity/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tiny-vue/reactivity",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@tiny-vue/shared": "workspace:^1.0.0"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/reactivity/src/baseHandlers.ts:
--------------------------------------------------------------------------------
1 | import { track, trigger } from "./effect"
2 | import { reactive, readonly, ReactiveFlags } from "./reactive"
3 | import { extend, isObject } from "@tiny-vue/shared"
4 |
5 | const get = createGetter()
6 | const set = createSetter()
7 | const readonlyGet = createGetter(true)
8 | const shallowReadonlyGet = createGetter(true)
9 |
10 | function createGetter(isReadonly = false, shallow = false) {
11 | return function get(target, key) {
12 | if (key === ReactiveFlags.IS_REACTIVE) {
13 | return !isReadonly
14 | } else if (key === ReactiveFlags.IS_READONLY) {
15 | return isReadonly
16 | }
17 | const res = Reflect.get(target, key)
18 |
19 | if (shallow) {
20 | return res
21 | }
22 |
23 | // 看看res是不是object
24 | if (isObject(res)) {
25 | return isReadonly ? readonly(res) : reactive(res)
26 | }
27 |
28 | if (!isReadonly) {
29 | // 依赖收集
30 | track(target, key)
31 | }
32 | return res
33 | }
34 | }
35 |
36 | function createSetter() {
37 | return function set(target, key, value) {
38 | const res = Reflect.set(target, key, value)
39 |
40 | // 触发依赖
41 | trigger(target, key)
42 | return res
43 | }
44 | }
45 |
46 | export const mutableHandler = {
47 | get,
48 | set
49 | }
50 |
51 | export const readonlyHandlers = {
52 | get: readonlyGet,
53 | set(target, key, value) {
54 | console.warn(`key: ${key} set 失败,因为 target 是 readonly`, target)
55 | return true
56 | }
57 | }
58 |
59 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, {
60 | get: shallowReadonlyGet
61 | })
--------------------------------------------------------------------------------
/tiny-vue3/packages/reactivity/src/computed.ts:
--------------------------------------------------------------------------------
1 | import { ReactiveEffect } from './effect';
2 |
3 | class ComputedRefImpl {
4 | private _getter:any
5 | private _dirty:boolean = true
6 | private _value:any
7 | private _effect:any
8 | constructor(getter) {
9 | this._getter = getter
10 | this._effect = new ReactiveEffect(getter, () => {
11 | if (!this._dirty) {
12 | this._dirty = true
13 | }
14 | })
15 | }
16 | get value() {
17 | // get
18 | // get value -> dirty(true)
19 | // 当依赖的响应式对象的值发生改变的时候
20 | // effect
21 | if (this._dirty) {
22 | this._dirty = false
23 | this._value = this._effect.run()
24 | }
25 | return this._value
26 | }
27 | }
28 |
29 |
30 | export function computed(getter) {
31 | return new ComputedRefImpl(getter)
32 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/reactivity/src/effect.ts:
--------------------------------------------------------------------------------
1 | import { extend } from "@tiny-vue/shared"
2 |
3 |
4 | let activeEffect
5 | let shouldTrack
6 | export class ReactiveEffect {
7 | private _fn: any
8 | deps = []
9 | active = true
10 | onStop?:() => void
11 | constructor(fn, public scheduler?) {
12 | this._fn = fn
13 | }
14 |
15 | run() {
16 | activeEffect = this
17 | // 1. 会收集依赖
18 | // shouldTrack 来做区分
19 | if (!this.active) {
20 | return this._fn()
21 | }
22 | shouldTrack = true
23 | activeEffect = this
24 |
25 | const result = this._fn()
26 | // reset
27 | shouldTrack = false
28 | return result
29 | }
30 | stop() {
31 | if (this.active) {
32 | cleanupEffect(this)
33 | if (this.onStop) {
34 | this.onStop()
35 | }
36 | this.active = false
37 | }
38 | }
39 | }
40 |
41 | function cleanupEffect(effect) {
42 | effect.deps.forEach((dep: any) => {
43 | dep.delete(effect)
44 | });
45 | effect.deps.length = 0
46 | }
47 |
48 | const targetMap = new Map()
49 | export function track(target, key) {
50 | // 判断是否处于收集状态
51 | if (!isTracking()) return
52 |
53 | // target -> key -> dep
54 | let desMap = targetMap.get(target)
55 | if (!desMap) {
56 | desMap = new Map()
57 | targetMap.set(target, desMap)
58 | }
59 |
60 | let dep = desMap.get(key)
61 | if(!dep) {
62 | dep = new Set()
63 | desMap.set(key, dep)
64 | }
65 |
66 | trackEffects(dep)
67 | }
68 |
69 | export function trackEffects(dep) {
70 | // 看看dep之前有没有添加过,添加过就不添加了
71 | if (dep.has(activeEffect)) return
72 | dep.add(activeEffect)
73 | activeEffect.deps.push(dep)
74 | }
75 |
76 | export function isTracking() {
77 | return shouldTrack && activeEffect !== undefined
78 | }
79 |
80 | export function trigger(target, key) {
81 | let desMap = targetMap.get(target)
82 | let dep = desMap.get(key)
83 | triggerEffects(dep)
84 | }
85 |
86 | export function triggerEffects(dep) {
87 | for(const effect of dep) {
88 | if (effect.scheduler) {
89 | effect.scheduler()
90 | } else {
91 | effect.run()
92 | }
93 | }
94 | }
95 |
96 | export function effect(fn, options:any = {}) {
97 | // fn
98 | const _effect = new ReactiveEffect(fn, options.scheduler)
99 |
100 | extend(_effect, options)
101 |
102 | _effect.run()
103 |
104 | const runner:any = _effect.run.bind(_effect)
105 | runner.effect = _effect
106 |
107 | return runner
108 | }
109 |
110 | export function stop(runner) {
111 | runner.effect.stop()
112 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/reactivity/src/index.ts:
--------------------------------------------------------------------------------
1 | // 创建第一个测试用例
2 | // export function add(a, b) {
3 | // return a + b
4 | // }
5 |
6 | export {
7 | effect,
8 | stop
9 | } from "./effect";
10 |
11 | export {
12 | isReactive,
13 | isReadonly,
14 | isProxy,
15 | reactive,
16 | readonly,
17 | shallowReadonly,
18 | } from "./reactive";
19 |
20 | export { ref, proxyRefs, unRef, isRef } from "./ref";
--------------------------------------------------------------------------------
/tiny-vue3/packages/reactivity/src/reactive.ts:
--------------------------------------------------------------------------------
1 | import { isObject } from "@tiny-vue/shared"
2 | import { mutableHandler, readonlyHandlers, shallowReadonlyHandlers } from "./baseHandlers"
3 |
4 | export const enum ReactiveFlags{
5 | IS_REACTIVE = "__v_isReactive",
6 | IS_READONLY = "__v_isReadonly"
7 | }
8 |
9 | export function reactive(raw) {
10 | return createReactiveObject(raw, mutableHandler)
11 | }
12 |
13 | export function readonly(raw) {
14 | return createReactiveObject(raw, readonlyHandlers)
15 | }
16 |
17 | export function shallowReadonly(raw) {
18 | return createReactiveObject(raw, shallowReadonlyHandlers)
19 | }
20 |
21 | export function isReactive(value) {
22 | return !!value[ReactiveFlags.IS_REACTIVE]
23 | }
24 |
25 | export function isReadonly(value) {
26 | return !!value[ReactiveFlags.IS_READONLY]
27 | }
28 |
29 | export function isProxy(value) {
30 | return isReactive(value) || isReadonly(value)
31 | }
32 |
33 | function createReactiveObject(target:any, baseHandlers) {
34 | if (!isObject(target)) {
35 | console.warn(`target ${target} 必须是一个对象`)
36 | return target
37 | }
38 | return new Proxy(target, baseHandlers)
39 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/reactivity/src/ref.ts:
--------------------------------------------------------------------------------
1 | import { hasChanged, isObject } from "@tiny-vue/shared"
2 | import { reactive } from "./reactive"
3 | import { isTracking, trackEffects, triggerEffects } from "./effect"
4 |
5 | class RefImpl {
6 | private _value: any
7 | public dep
8 | private _rawValue: any
9 | public __v_isRef = true
10 | constructor(value) {
11 | this._rawValue = value
12 | this._value = convert(value)
13 | // value -> reactive
14 | // 1. 看看value是不是对象
15 | this.dep = new Set()
16 | }
17 | get value() {
18 | trackRefValue(this)
19 | return this._value
20 | }
21 | set value(newValue) {
22 | // 判断新旧值是否相等,相等返回
23 | // 不相等,修改旧值
24 | // newValue -> this._value
25 | if (hasChanged(this._value, newValue)) {
26 | this._rawValue = newValue
27 | this._value = convert(newValue)
28 | triggerEffects(this.dep)
29 | }
30 | }
31 | }
32 |
33 | function convert(value) {
34 | return isObject(value) ? reactive(value) : value
35 | }
36 |
37 | export function trackRefValue(ref) {
38 | if(isTracking()) {
39 | trackEffects(ref.dep)
40 | }
41 | }
42 |
43 | export function ref(value) {
44 | return new RefImpl(value)
45 | }
46 |
47 | export function isRef(ref) {
48 | return !!ref.__v_isRef
49 | }
50 |
51 | export function unRef(ref) {
52 | // 判断是否 ref -> ref.value
53 | return isRef(ref) ? ref.value : ref
54 | }
55 |
56 | export function proxyRefs(objectWithRefs) {
57 | return new Proxy(objectWithRefs, {
58 | get(target, key) {
59 | // get -> age
60 | // ref -> 那么给他返回.value
61 | // not ref -> value
62 | return unRef(Reflect.get(target, key))
63 | },
64 | set(target, key, value) {
65 | // set -> ref .value
66 | if (isRef(target[key]) && !isRef(value)) {
67 | return target[key].value = value
68 | } else {
69 | return Reflect.set(target, key, value)
70 | }
71 | }
72 | })
73 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/__tests__/apiWatch.spec.ts:
--------------------------------------------------------------------------------
1 | import { reactive } from "@tiny-vue/reactivity";
2 | import { nextTick } from "../src/scheduler";
3 | import { vi } from "vitest";
4 | import { watchEffect } from "../src/apiWatch";
5 |
6 | describe("api: watch", () => {
7 | it("effect",async () => {
8 | const state = reactive({ count: 0 })
9 | let dummy
10 | watchEffect(() => {
11 | dummy = state.count
12 | })
13 | expect(dummy).toBe(0)
14 |
15 | state.count++
16 | await nextTick()
17 | expect(dummy).toBe(1)
18 | })
19 |
20 | it.todo("stopping the watcher (effect)", async () => {
21 | const state = reactive({ count: 0})
22 | let dummy
23 | const stop: any = watchEffect(() => {
24 | dummy = state.count
25 | })
26 | expect(dummy).toBe(0)
27 |
28 | stop()
29 | state.count++
30 | await nextTick()
31 | // should not update
32 | expect(dummy).toBe(0)
33 | })
34 |
35 | it.todo("cleanup registration (effect)", async () => {
36 | const state = reactive({ count: 0 })
37 | const cleanup = vi.fn()
38 | let dummy
39 | const stop: any = watchEffect((onCleanup) => {
40 | onCleanup(cleanup)
41 | dummy = state.count
42 | })
43 | expect(dummy).toBe(0)
44 |
45 | state.count++
46 | await nextTick()
47 | // should not update
48 | expect(cleanup).toHaveBeenCalledTimes(1)
49 | expect(dummy).toBe(1)
50 |
51 | stop()
52 | expect(cleanup).toHaveBeenCalledTimes(2)
53 | })
54 | })
55 |
56 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tiny-vue/runtime-core",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@tiny-vue/reactivity": "workspace:^1.0.0",
14 | "@tiny-vue/shared": "workspace:^1.0.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/apiInject.ts:
--------------------------------------------------------------------------------
1 | import { getCurrentInstance } from "./component";
2 |
3 |
4 | export function provide(key, value) {
5 | // 存
6 | // key value
7 | const currentInstance:any = getCurrentInstance()
8 | if(currentInstance) {
9 | let { provides } = currentInstance
10 | const parentProvides = currentInstance.parent.provides
11 |
12 | // init
13 | if (provides === parentProvides) {
14 | provides = currentInstance.provides = Object.create(parentProvides)
15 | }
16 | provides[key] = value
17 | }
18 | }
19 |
20 | export function inject(key, defaultValue) {
21 | // 取
22 | const currentInstance:any = getCurrentInstance()
23 | if(currentInstance) {
24 | const parentProvides = currentInstance.parent.provides
25 | if (key in parentProvides) {
26 | return parentProvides[key]
27 | } else if (defaultValue) {
28 | if (typeof defaultValue === "function") {
29 | return defaultValue()
30 | }
31 | return defaultValue
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/apiWatch.ts:
--------------------------------------------------------------------------------
1 | import { ReactiveEffect } from "../../reactivity/src/effect"
2 | import { queuePreFlushCb } from "./scheduler"
3 |
4 | export function watchEffect(source) {
5 | function job() {
6 | effect.run()
7 | }
8 |
9 | let cleanup
10 | const onCleanup = function(fn) {
11 | cleanup = effect.onStop = () => {
12 | fn()
13 | }
14 | }
15 |
16 | function getter() {
17 | if (cleanup) {
18 | cleanup()
19 | }
20 |
21 | source(onCleanup)
22 | }
23 | const effect = new ReactiveEffect(getter, () => {
24 | queuePreFlushCb(job)
25 | })
26 |
27 | effect.run()
28 |
29 | return () => {
30 | effect.stop()
31 | }
32 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/component.ts:
--------------------------------------------------------------------------------
1 | import { PublicInstanceProxyHandlers } from "./componentPublicInstance"
2 | import { initProps } from "./componentProps"
3 | import { emit } from "./componentEmit"
4 | import { initSlots } from "./componentSlots"
5 | import { proxyRefs, shallowReadonly } from "@tiny-vue/reactivity"
6 |
7 | export function createComponentInstance(vnode, parent) {
8 | console.log("createComponentInstance", parent)
9 | const component = {
10 | vnode,
11 | type: vnode.type,
12 | next: null,
13 | setupState: {},
14 | props: {},
15 | slots: {},
16 | provides: parent ? parent.provides : {},
17 | parent,
18 | isMounted: false,
19 | subTree: {},
20 | emit: () => {}
21 | }
22 | component.emit = emit.bind(null, component) as any
23 | return component
24 | }
25 |
26 | export function setupComponents(instance) {
27 | initProps(instance, instance.vnode.props)
28 | initSlots(instance, instance.vnode.children)
29 | setupStatefulComponent(instance)
30 | }
31 |
32 | function setupStatefulComponent(instance: any) {
33 | const Component = instance.type
34 |
35 | // ctx
36 | instance.proxy = new Proxy({_: instance}, PublicInstanceProxyHandlers)
37 |
38 | const { setup } = Component
39 |
40 | if (setup) {
41 | setCurrentInstance(instance)
42 | const setupResult = setup(shallowReadonly(instance.props), {
43 | emit: instance.emit
44 | })
45 | setCurrentInstance(null)
46 |
47 | handleSetupResult(instance, setupResult)
48 | }
49 | }
50 |
51 | function handleSetupResult(instance, setupResult: any) {
52 | if (typeof setupResult === "object") {
53 | instance.setupState = proxyRefs(setupResult)
54 | }
55 |
56 | finishComponentSetup(instance)
57 | }
58 |
59 | function finishComponentSetup(instance: any) {
60 | const Component = instance.type
61 | if (compiler && !Component.render) {
62 | if (Component.template) {
63 | Component.render = compiler(Component.template)
64 | }
65 | }
66 |
67 | // template
68 | instance.render = Component.render
69 | }
70 |
71 | let currentInstance = null
72 | export function getCurrentInstance() {
73 | return currentInstance
74 | }
75 |
76 | export function setCurrentInstance(instance) {
77 | currentInstance = instance
78 | }
79 |
80 | let compiler
81 | export function registerRuntimeCompiler(_compiler) {
82 | compiler = _compiler
83 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/componentEmit.ts:
--------------------------------------------------------------------------------
1 | import { toHandlerKey, camelize } from "@tiny-vue/shared";
2 |
3 | export function emit(instance, event, ...args) {
4 | console.log("emit", event);
5 |
6 | // instance.props -> event
7 | const { props } = instance
8 |
9 | // TPP
10 | // 先去写一个特定的行为 -> 重构成通用的行为
11 | // add
12 | // const handler = props["onAdd"]
13 |
14 | // 重构
15 | // add -> Add
16 | // add-foo -> addFoo
17 | const handlerName = toHandlerKey(camelize(event))
18 | const handler = props[handlerName]
19 | handler && handler(...args)
20 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/componentProps.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | export function initProps(instance, rawProps) {
4 | instance.props = rawProps || {}
5 | // attrs
6 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/componentPublicInstance.ts:
--------------------------------------------------------------------------------
1 | import { hasOwn } from "@tiny-vue/shared"
2 |
3 | const publicPropertiesMap = {
4 | $el: (i) => i.vnode.el,
5 | $slots: (i) => i.slots,
6 | $props: (i) => i.props
7 | }
8 |
9 |
10 | export const PublicInstanceProxyHandlers = {
11 | get({ _: instance }, key) {
12 | // setupState
13 | const { setupState, props } = instance
14 |
15 | if (hasOwn(setupState, key)) {
16 | return setupState[key]
17 | } else if (hasOwn(props, key)) {
18 | return props[key]
19 | }
20 |
21 | const publicGetter = publicPropertiesMap[key]
22 | if (publicGetter) {
23 | return publicGetter(instance)
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/componentSlots.ts:
--------------------------------------------------------------------------------
1 | import { ShapeFlags } from "@tiny-vue/shared"
2 |
3 | export function initSlots(instance, children) {
4 | // slots
5 | const { vnode } = instance
6 | if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) {
7 | normalizeObjectSlots(children, instance.slots)
8 | }
9 | }
10 |
11 | function normalizeObjectSlots(children: any, slots: any) {
12 | for (const key in children) {
13 | const value = children[key]
14 | slots[key] = (props) => normalizeSlotValue(value(props))
15 | }
16 | }
17 |
18 | function normalizeSlotValue(value) {
19 | return Array.isArray(value) ? value : [value]
20 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/componentUpdateUtils.ts:
--------------------------------------------------------------------------------
1 | export function shouldUpdateComponent(prevVNode, nextVNode) {
2 | const { props: prevProps } = prevVNode
3 | const { props: nextProps } = nextVNode
4 | for (const key in nextProps) {
5 | if (nextProps[key] !== prevProps[key]) {
6 | return true
7 | }
8 | }
9 | return false
10 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/createApp.ts:
--------------------------------------------------------------------------------
1 | import { createVNode } from "./vnode"
2 |
3 | // render
4 | export function createAppAPI(render) {
5 | return function createApp(rootComponent) {
6 | return {
7 | mount(rootContainer) {
8 | // 先转换为 vnode
9 | // component -> vnode
10 | // 后续所有的逻辑操作都会基于 vnode 处理
11 | const vnode = createVNode(rootComponent)
12 | render(vnode, rootContainer)
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/h.ts:
--------------------------------------------------------------------------------
1 | import { createVNode } from "./vnode";
2 |
3 | export function h(type, props?, children?) {
4 | return createVNode(type, props, children)
5 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/helpers/renderSlots.ts:
--------------------------------------------------------------------------------
1 | import { createVNode, Fragment } from "../vnode";
2 |
3 | export function renderSlots(slots, name, props) {
4 | const slot = slots[name]
5 | if (slot) {
6 | // function
7 | if (typeof slot === "function") {
8 | // children 是不可以有 array
9 | // 只需要把 children
10 | return createVNode(Fragment, {}, slot(props))
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/index.ts:
--------------------------------------------------------------------------------
1 | export { h } from "./h"
2 | export { renderSlots } from "./helpers/renderSlots"
3 | export { createTextVNode, createElementVNode } from "./vnode"
4 | export { getCurrentInstance, registerRuntimeCompiler } from "./component"
5 | export { provide, inject } from "./apiInject"
6 | export { createRenderer } from "./renderer"
7 | export { nextTick } from "./scheduler"
8 | export { toDisplayString } from "@tiny-vue/shared";
9 | export * from "@tiny-vue/reactivity";
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/renderer.ts:
--------------------------------------------------------------------------------
1 | import { createComponentInstance, setupComponents } from "./component"
2 | import { ShapeFlags, EMPTY_OBJ } from "@tiny-vue/shared"
3 | import { Fragment, Text } from "./vnode"
4 | import { createAppAPI } from "./createApp"
5 | import { effect } from "@tiny-vue/reactivity"
6 | import { shouldUpdateComponent } from "./componentUpdateUtils"
7 | import { queueJobs } from "./scheduler"
8 |
9 | export function createRenderer(options) {
10 | const {
11 | createElement: hostCreateElement,
12 | patchProp: hostPatchProp,
13 | insert: hostInsert,
14 | remove: hostRemove,
15 | setElementText: hostSetElementText
16 | } = options
17 |
18 | function render(vnode, container) {
19 | // patch
20 | patch(null, vnode, container, null, null)
21 | }
22 |
23 | // n1 -> 老的
24 | // n2 -> 新的
25 | function patch(n1, n2, container, parentComponent, anchor) {
26 | // ShapeFlags
27 | // vnode -> flag
28 | // 判断vnode是 element 和 component 类型\
29 |
30 | const { type, shapeFlag } = n2
31 |
32 | // Fragment -> 只渲染 children
33 | switch (type) {
34 | case Fragment:
35 | processFragment(n1, n2, container, parentComponent, anchor)
36 | break
37 | case Text:
38 | processText(n1, n2, container)
39 | break
40 | default:
41 | if (shapeFlag & ShapeFlags.ELEMENT) { // element
42 | processElement(n1, n2, container, parentComponent, anchor)
43 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { // STATEFUL_COMPONENT
44 | // 处理组件
45 | processComponent(n1, n2, container, parentComponent, anchor)
46 | }
47 | break
48 | }
49 | }
50 |
51 | function processText(n1, n2:any, container:any) {
52 | const { children } = n2
53 | const textNode = (n2.el = document.createTextNode(children))
54 | container.append(textNode)
55 | }
56 |
57 | function processFragment(n1, n2:any, container:any, parentComponent, anchor) {
58 | // Implement
59 | mountChildren(n2.children, container, parentComponent, anchor)
60 | }
61 |
62 | function processElement(n1, n2: any, container: any, parentComponent, anchor) {
63 | if (!n1) {
64 | mountElement(n2, container, parentComponent, anchor)
65 | } else {
66 | patchElement(n1, n2, container, parentComponent, anchor)
67 | }
68 | }
69 |
70 | function patchElement(n1, n2, container, parentComponent, anchor) {
71 | console.log("patchElement");
72 | console.log("n1", n1);
73 | console.log("n2", n2);
74 |
75 | // props
76 | // children
77 | const oldProps = n1.props || EMPTY_OBJ
78 | const newProps = n2.props || EMPTY_OBJ
79 |
80 | const el = (n2.el = n1.el)
81 |
82 | patchChildren(n1, n2, el, parentComponent, anchor)
83 | patchProps(el, oldProps, newProps)
84 | }
85 |
86 | function patchChildren(n1, n2, container, parentComponent, anchor) {
87 | const prevShapeFlag = n1.shapeFlag
88 | const c1 = n1.children
89 | const { shapeFlag } = n2
90 | const c2 = n2.children
91 |
92 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
93 | // children
94 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
95 | // 1. 把老的 children 清空
96 | unmountChildren(n1.children)
97 | // 2. 设置 text
98 | // hostSetElementText(container, c2)
99 | }
100 | // text
101 | if (c1 !== c2) {
102 | hostSetElementText(container, c2)
103 | }
104 | } else {
105 | // new array
106 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
107 | hostSetElementText(container, "")
108 | mountChildren(c2, container, parentComponent, anchor)
109 | } else {
110 | // array diff array
111 | patchKeyedChildren(c1, c2, container, parentComponent, anchor)
112 | }
113 | }
114 | }
115 |
116 | function patchKeyedChildren(c1, c2, container, parentComponent, parentAnchor) {
117 | let i = 0
118 | const l2 = c2.length
119 | let e1 = c1.length - 1
120 | let e2 = l2 - 1
121 |
122 | function isSomeVNodeType(n1, n2) {
123 | // type
124 | // key
125 | return n1.type === n2.type && n1.key === n2.key
126 | }
127 |
128 | // 左侧
129 | while (i <= e1 && i <= e2) {
130 | const n1 = c1[i]
131 | const n2 = c2[i]
132 | if (isSomeVNodeType(n1, n2)) {
133 | patch(n1, n2, container, parentComponent, parentAnchor)
134 | } else {
135 | break
136 | }
137 | i++
138 | }
139 |
140 | // 右侧
141 | while (i <= e1 && i <= e2) {
142 | const n1 = c1[e1]
143 | const n2 = c2[e2]
144 | if (isSomeVNodeType(n1, n2)) {
145 | patch(n1, n2, container, parentComponent, parentAnchor)
146 | } else {
147 | break
148 | }
149 | e1--
150 | e2--
151 | }
152 |
153 | // 3. 新的比老的多 创建
154 | if (i > e1) {
155 | if (i <= e2) {
156 | const nextPos = e2 + 1
157 | const anchor = nextPos < l2 ? c2[nextPos].el : null
158 | while(i <= e2) {
159 | patch(null, c2[i], container, parentComponent, anchor)
160 | i++
161 | }
162 | }
163 | } else if (i > e2) { // 4. 老的比新的多 删除
164 | while (i <= e1) {
165 | hostRemove(c1[i].el)
166 | i++
167 | }
168 | } else {
169 | // 中间对比
170 | let s1 = i
171 | let s2 = i
172 |
173 | const toBePatched = e2 - s2 + 1
174 | let patched = 0
175 | // 存储新表的映射关系
176 | const keyToNewIndexMap = new Map()
177 | // 新序列索引映射关系表
178 | const newIndexToOldIndexMap = new Array(toBePatched)
179 | let moved = false
180 | let maxNewIndexSoFar = 0
181 |
182 | for (let i = 0; i < toBePatched; i++) {
183 | // 初始化
184 | newIndexToOldIndexMap[i] = 0
185 | }
186 |
187 | for (let i = s2; i <= e2; i++) {
188 | const nextChild = c2[i]
189 | // 设置新表的映射关系
190 | keyToNewIndexMap.set(nextChild.key, i)
191 | }
192 |
193 | for (let i = s1; i <= e1; i++) {
194 | const preChild = c1[i]
195 |
196 | if (patched >= toBePatched) {
197 | hostRemove(preChild.el)
198 | continue
199 | }
200 |
201 | // null undefined
202 | let newIndex;
203 | if (preChild.key !== null) {
204 | // 获取新表的映射关系
205 | newIndex = keyToNewIndexMap.get(preChild.key)
206 | } else {
207 | for (let j = s2; j <= e2; j++) {
208 | if (isSomeVNodeType(preChild, c2[j])) {
209 | newIndex = j
210 | break
211 | }
212 | }
213 | }
214 |
215 | if (newIndex === undefined) {
216 | hostRemove(preChild.el)
217 | } else {
218 | if (newIndex >= maxNewIndexSoFar) {
219 | maxNewIndexSoFar = newIndex
220 | } else {
221 | moved = true
222 | }
223 |
224 | newIndexToOldIndexMap[newIndex - s2] = i + 1
225 | patch(preChild, c2[newIndex], container, parentComponent, null)
226 | patched++
227 | }
228 | }
229 |
230 | const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : []
231 | let j = increasingNewIndexSequence.length - 1
232 |
233 | for (let i = toBePatched - 1; i >= 0; i--) {
234 | const nextIndex = i + s2
235 | const nextChild = c2[nextIndex]
236 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null
237 |
238 | if (newIndexToOldIndexMap[i] === 0) {
239 | patch(null, nextChild, container, parentComponent, anchor)
240 | } else if (moved) {
241 | if (j < 0 || i !== increasingNewIndexSequence[j]) {
242 | console.log("移动位置");
243 | hostInsert(nextChild.el, container, anchor)
244 | } else {
245 | j--
246 | }
247 | }
248 | }
249 | }
250 | }
251 |
252 | function unmountChildren(children) {
253 | for (let i = 0; i < children.length; i++) {
254 | const el = children[i].el
255 | // remove
256 | hostRemove(el)
257 | }
258 | }
259 |
260 | function patchProps(el, oldProps, newProps) {
261 | if (oldProps !== newProps) {
262 | for (const key in newProps) {
263 | const prevProp = oldProps[key]
264 | const nextProp = newProps[key]
265 |
266 | if (prevProp !== nextProp) {
267 | hostPatchProp(el, key, prevProp, nextProp)
268 | }
269 | }
270 |
271 | if (oldProps !== EMPTY_OBJ) {
272 | for (const key in oldProps) {
273 | if (!(key in newProps)) {
274 | hostPatchProp(el, key, oldProps[key], null)
275 | }
276 | }
277 | }
278 | }
279 | }
280 |
281 | function mountElement(vnode: any, container: any, parentComponent, anchor) {
282 | // vnode -> element - div
283 |
284 | // canvas -> new Element
285 | const el = (vnode.el = hostCreateElement(vnode.type))
286 |
287 | // string array
288 | const { children, shapeFlag } = vnode
289 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
290 | // text_children
291 | el.textContent = children
292 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
293 | // array_children
294 | mountChildren(vnode.children, el, parentComponent, anchor)
295 | }
296 |
297 | // props
298 | const { props } = vnode
299 | for (const key in props) {
300 | const val = props[key]
301 |
302 | hostPatchProp(el, key, null, val)
303 | }
304 |
305 | // container.append(el)
306 |
307 | // canvas -> addChild()
308 | hostInsert(el, container, anchor)
309 | }
310 |
311 | function mountChildren(children, container, parentComponent, anchor) {
312 | children.forEach((v) => {
313 | patch(null, v, container, parentComponent, anchor)
314 | })
315 | }
316 |
317 | function processComponent(n1, n2: any, container: any, parentComponent, anchor) {
318 | if (!n1) {
319 | mountComponent(n2, container, parentComponent, anchor)
320 | } else {
321 | updateComponent(n1, n2)
322 | }
323 | }
324 |
325 | function updateComponent(n1, n2) {
326 | const instance = (n2.component = n1.component)
327 | if (shouldUpdateComponent(n1, n2)) {
328 | instance.next = n2
329 | instance.update()
330 | } else {
331 | n2.el = n1.el
332 | }
333 | }
334 |
335 | function mountComponent(initialVNode: any, container: any, parentComponent, anchor) {
336 | const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent))
337 | setupComponents(instance)
338 | setupRenderEffect(instance, initialVNode, container, anchor)
339 | }
340 |
341 | function setupRenderEffect(instance: any, initialVNode: any, container: any, anchor) {
342 | instance.update = effect(() => {
343 | if (!instance.isMounted) {
344 | // init
345 | console.log("init");
346 | const { proxy } = instance
347 | const subTree = (instance.subTree = instance.render.call(proxy, proxy))
348 | console.log(subTree);
349 | // vnode -> patch
350 | // vnode -> element -> mountElement
351 | patch(null, subTree, container, instance, anchor)
352 |
353 | // element -> mount
354 | initialVNode.el = subTree.el
355 |
356 | instance.isMounted = true
357 | } else {
358 | // update
359 | console.log("update");
360 | // 需要一个新的 vnode
361 | const { next, vnode } = instance
362 | if (next) {
363 | next.el = vnode.el
364 | updateComponentPreRender(instance, next)
365 | }
366 |
367 | const { proxy } = instance
368 | const subTree = instance.render.call(proxy, proxy)
369 | const preSubTree = instance.subTree
370 | instance.subTree = subTree
371 |
372 | patch(preSubTree, subTree, container, instance, anchor)
373 | }
374 | }, {
375 | scheduler() {
376 | console.log("update - scheduler");
377 | queueJobs(instance.update)
378 | }
379 | })
380 | }
381 |
382 | return {
383 | createApp: createAppAPI(render)
384 | }
385 | }
386 |
387 | function updateComponentPreRender(instance, nextVNode) {
388 | instance.vnode = nextVNode
389 | instance.next = null
390 | instance.props = nextVNode.props
391 | }
392 |
393 | // 最长递增子序列算法
394 | function getSequence(arr) {
395 | const p = arr.slice();
396 | const result = [0];
397 | let i, j, u, v, c;
398 | const len = arr.length;
399 | for (i = 0; i < len; i++) {
400 | const arrI = arr[i];
401 | if (arrI !== 0) {
402 | j = result[result.length - 1];
403 | if (arr[j] < arrI) {
404 | p[i] = j;
405 | result.push(i);
406 | continue;
407 | }
408 | u = 0;
409 | v = result.length - 1;
410 | while (u < v) {
411 | c = (u + v) >> 1;
412 | if (arr[result[c]] < arrI) {
413 | u = c + 1;
414 | } else {
415 | v = c;
416 | }
417 | }
418 | if (arrI < arr[result[u]]) {
419 | if (u > 0) {
420 | p[i] = result[u - 1];
421 | }
422 | result[u] = i;
423 | }
424 | }
425 | }
426 | u = result.length;
427 | v = result[u - 1];
428 | while (u-- > 0) {
429 | result[u] = v;
430 | v = p[v];
431 | }
432 | return result;
433 | }
434 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/scheduler.ts:
--------------------------------------------------------------------------------
1 |
2 | const queue: any[] = []
3 | const activePreFlushCbs: any[] = []
4 | const p = Promise.resolve()
5 | let isFlushPending = false
6 |
7 | export function nextTick(fn?) {
8 | return fn ? p.then(fn) : p
9 | }
10 |
11 | export function queueJobs(job) {
12 | if (!queue.includes(job)) {
13 | queue.push(job)
14 | }
15 |
16 | queueFlush()
17 | }
18 |
19 | export function queuePreFlushCb(job) {
20 | activePreFlushCbs.push(job)
21 | queueFlush()
22 | }
23 |
24 | function queueFlush() {
25 | if (isFlushPending) return
26 | isFlushPending = true
27 | nextTick(flushJobs)
28 | }
29 |
30 | function flushJobs() {
31 | isFlushPending = false
32 |
33 | //
34 | flushPreFlushCbs()
35 |
36 | // components render
37 | let job
38 | while((job = queue.shift())) {
39 | job && job()
40 | }
41 | }
42 |
43 | function flushPreFlushCbs() {
44 | for (let i = 0; i < activePreFlushCbs.length; i++) {
45 | activePreFlushCbs[i]()
46 | }
47 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-core/src/vnode.ts:
--------------------------------------------------------------------------------
1 | import { ShapeFlags } from "@tiny-vue/shared"
2 |
3 | export const Fragment = Symbol("Fragment")
4 | export const Text = Symbol("Text")
5 | export { createVNode as createElementVNode }
6 |
7 | export function createVNode(type, props?, children?) {
8 | const vnode = {
9 | type,
10 | props,
11 | children,
12 | component: null,
13 | key: props && props.key,
14 | shapeFlag: getShapeFlag(type),
15 | el: null
16 | }
17 |
18 | // children
19 | if (typeof children === "string") {
20 | vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN
21 | } else if (Array.isArray(children)) {
22 | vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN
23 | }
24 |
25 | // 组件 + children object
26 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
27 | if (typeof children === "object") {
28 | vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN
29 | }
30 | }
31 |
32 | return vnode
33 | }
34 |
35 | export function createTextVNode(text: string) {
36 | return createVNode(Text, {}, text)
37 | }
38 |
39 | function getShapeFlag(type) {
40 | return typeof type === "string"
41 | ? ShapeFlags.ELEMENT
42 | : ShapeFlags.STATEFUL_COMPONENT
43 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-dom/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tiny-vue/runtime-dom",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@tiny-vue/runtime-core": "workspace:^1.0.0"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/runtime-dom/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createRenderer } from "@tiny-vue/runtime-core";
2 |
3 | function createElement(type) {
4 | console.log("createElement========");
5 | return document.createElement(type)
6 | }
7 |
8 | function patchProp(el, key, preVal, nextVal) {
9 | // 具体的 click -> 通用
10 | // on + Event name -> onMousedown
11 | const isOn = (key:string) => /^on[A-Z]/.test(key)
12 | if (isOn(key)) {
13 | const event = key.slice(2).toLowerCase()
14 | el.addEventListener(event, nextVal)
15 | } else {
16 | if (nextVal === undefined || nextVal === null) {
17 | el.removeAttribute(key)
18 | } else {
19 | el.setAttribute(key, nextVal)
20 | }
21 | }
22 | }
23 |
24 | function insert(child, parent, anchor = null) {
25 | // parent.append(el)
26 | parent.insertBefore(child, anchor)
27 | }
28 |
29 | function remove(child) {
30 | const parent = child.parentNode
31 | if (parent) {
32 | parent.removeChild(child)
33 | }
34 | }
35 |
36 | function setElementText(el, text) {
37 | el.textContent = text
38 | }
39 |
40 | const renderer:any = createRenderer({
41 | createElement,
42 | patchProp,
43 | insert,
44 | remove,
45 | setElementText
46 | })
47 |
48 | export function createApp(...args) {
49 | return renderer.createApp(...args)
50 | }
51 |
52 | export * from "@tiny-vue/runtime-core";
53 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tiny-vue/shared",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC"
12 | }
13 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/shared/src/ShapeFlags.ts:
--------------------------------------------------------------------------------
1 | export const enum ShapeFlags {
2 | ELEMENT = 1, // 0001
3 | STATEFUL_COMPONENT = 1 << 1, // 0010
4 | TEXT_CHILDREN = 1 << 2, // 0100
5 | ARRAY_CHILDREN = 1 << 3, // 1000
6 | SLOT_CHILDREN = 1 << 4, // 10000
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/shared/src/demo.ts:
--------------------------------------------------------------------------------
1 | const ShapeFlags = {
2 | element: 0,
3 | stateful_component: 0,
4 | text_children: 0,
5 | array_children: 0
6 | }
7 |
8 | // vnode -> stateful_component ->
9 | // key-value(不够高效)
10 | // 1. 可以设置 修改
11 | // ShapeFlags.stateful_component = 1;
12 | // ShapeFlags.array_children = 1
13 |
14 |
15 | // if (ShapeFlags.element)
16 | // if (ShapeFlags.stateful_component)
17 |
18 | // 不够高效 -> 位运算的方式
19 | // | 两位都为0, 才为0
20 | // & 两位都为1, 才为1
21 |
22 | // 0000
23 | // 0001 -> element
24 | // 0010 -> stateful
25 | // 0100 -> text_children
26 | // 1000 -> array_children
27 |
28 | // 修改
29 | // 0000 |
30 | // 0001
31 | // ————
32 | // 0001
33 |
34 | // 查找
35 | // 0001 &
36 | // 0001
37 | // ————
38 | // 0001
39 |
40 | // 0010
41 | // 0001
42 | // ————
43 | // 0000
44 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/shared/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./toDisplayString";
2 |
3 | export const extend = Object.assign
4 |
5 | export const EMPTY_OBJ = {}
6 |
7 | export const isObject = (value) => {
8 | return value !== null && typeof value === "object"
9 | }
10 |
11 | export const isString = (value) => {
12 | return typeof value === "string"
13 | }
14 |
15 | export const hasChanged = (oldValue, newValue) => {
16 | return !Object.is(oldValue, newValue)
17 | }
18 |
19 | export const hasOwn = (val, key) => {
20 | return Object.prototype.hasOwnProperty.call(val, key)
21 | }
22 |
23 | export const camelize = (str: string) => {
24 | return str.replace(/-(\w)/g, (_, c:string) => {
25 | return c ? c.toUpperCase() : ""
26 | })
27 | }
28 |
29 | const capitalize = (str: string) => {
30 | return str.charAt(0).toUpperCase() + str.slice(1)
31 | }
32 |
33 | export const toHandlerKey = (str: string) => {
34 | return str ? `on${capitalize(str)}` : ""
35 | }
36 |
37 | export { ShapeFlags } from "./ShapeFlags";
--------------------------------------------------------------------------------
/tiny-vue3/packages/shared/src/toDisplayString.ts:
--------------------------------------------------------------------------------
1 | export function toDisplayString(value) {
2 | return String(value);
3 | }
4 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/apiInject/App.js:
--------------------------------------------------------------------------------
1 | // 组件 provide 和 inject 功能
2 | import { h, provide, inject } from "../../dist/tiny-vue3.esm.js";
3 |
4 | const Provider = {
5 | name: "Provider",
6 | setup() {
7 | provide("foo", "fooVal");
8 | provide("bar", "barVal");
9 | },
10 | render() {
11 | return h("div", {}, [h("p", {}, "Provider"), h(ProviderTwo)]);
12 | },
13 | };
14 |
15 | const ProviderTwo = {
16 | name: "ProviderTwo",
17 | setup() {
18 | provide("foo", "fooTwo");
19 | const foo = inject("foo");
20 |
21 | return {
22 | foo,
23 | };
24 | },
25 | render() {
26 | return h("div", {}, [
27 | h("p", {}, `ProviderTwo foo:${this.foo}`),
28 | h(Consumer),
29 | ]);
30 | },
31 | };
32 |
33 | const Consumer = {
34 | name: "Consumer",
35 | setup() {
36 | const foo = inject("foo");
37 | const bar = inject("bar");
38 | // const baz = inject("baz", "bazDefault");
39 | const baz = inject("baz", () => "bazDefault");
40 |
41 | return {
42 | foo,
43 | bar,
44 | baz,
45 | };
46 | },
47 |
48 | render() {
49 | return h("div", {}, `Consumer: - ${this.foo} - ${this.bar}-${this.baz}`);
50 | },
51 | };
52 |
53 | export default {
54 | name: "App",
55 | setup() {},
56 | render() {
57 | return h("div", {}, [h("p", {}, "apiInject"), h(Provider)]);
58 | },
59 | };
60 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/apiInject/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
16 |
17 |
18 |
19 |
26 |
27 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/compiler-base/App.js:
--------------------------------------------------------------------------------
1 | import { ref } from "../../dist/tiny-vue3.esm.js";
2 |
3 | export const App = {
4 | name: "App",
5 | template: `hi, {{message}}, {{count}}
`,
6 | setup() {
7 | const count = (window.count = ref(1));
8 | return {
9 | count,
10 | message: 'tiny-vue3'
11 | };
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/compiler-base/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/compiler-base/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../dist/tiny-vue3.esm.js";
2 | import { App } from "./App.js";
3 |
4 | const rootContainer = document.querySelector("#app");
5 | createApp(App).mount(rootContainer);
6 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/componentEmit/App.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../dist/tiny-vue3.esm.js"
2 | import { Foo } from "./Foo.js"
3 |
4 | export const App = {
5 | name: "App",
6 | render() {
7 | // emit
8 | return h("div", {}, [h("div", {}, "App"), h(Foo, {
9 | // on + Event
10 | onAdd(a, b) {
11 | console.log("Add", a, b);
12 | },
13 | // add-foo -> addFoo
14 | onAddFoo() {
15 | console.log("onAddFoo");
16 | }
17 | })])
18 | },
19 | setup() {
20 | return {
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/componentEmit/Foo.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../dist/tiny-vue3.esm.js"
2 |
3 | export const Foo = {
4 | setup(props, {emit}) {
5 | const emitAdd = () => {
6 | emit("add", 1, 2)
7 | emit("add-foo")
8 | }
9 |
10 | return {
11 | emitAdd
12 | }
13 | },
14 | render() {
15 | const btn = h("button", {
16 | onClick: this.emitAdd
17 | }, "emitAdd")
18 |
19 | const foo = h("p", {}, "foo")
20 | return h("div", {}, [foo, btn])
21 | }
22 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/componentEmit/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/componentEmit/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../dist/tiny-vue3.esm.js"
2 | import { App } from "./App.js"
3 |
4 | const rootContainer = document.querySelector("#app")
5 | // vue3
6 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/componentSlot/App.js:
--------------------------------------------------------------------------------
1 | import { h, createTextVNode } from "../../dist/tiny-vue3.esm.js"
2 | import { Foo } from "./Foo.js"
3 |
4 | export const App = {
5 | name: "App",
6 | render() {
7 | const app = h("div", {}, "App")
8 | // object key
9 | const foo = h(
10 | Foo,
11 | {},
12 | {
13 | header: ({age}) => [
14 | h("p", {}, "header" + age),
15 | createTextVNode("你好,世界!")
16 | ],
17 | footer: () => h("p", {}, "footer")
18 | }
19 | )
20 | // 数组 vnode
21 | // const foo = h(Foo, {}, h("p", {}, "123"))
22 |
23 | return h("div", {}, [app, foo])
24 | },
25 | setup() {
26 | return {
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/componentSlot/Foo.js:
--------------------------------------------------------------------------------
1 | import { h, renderSlots } from "../../dist/tiny-vue3.esm.js"
2 |
3 | export const Foo = {
4 | setup() {
5 | return {}
6 | },
7 | render() {
8 | const foo = h("p", {}, "foo")
9 |
10 | // Foo .vnode .children
11 | console.log(this.$slots);
12 | // children -> vnode
13 | //
14 | // renderSlots
15 | // 具名插槽
16 | // 1. 获取到要渲染的元素 1
17 | // 2. 要获取到渲染的位置
18 | // 作用域插槽
19 | const age = 18
20 | return h("div", {}, [
21 | renderSlots(this.$slots, "header", {
22 | age
23 | }),
24 | foo,
25 | renderSlots(this.$slots, "footer"),
26 | ])
27 | }
28 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/componentSlot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/componentSlot/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../lib/tiny-vue3.esm.js"
2 | import { App } from "./App.js"
3 |
4 | const rootContainer = document.querySelector("#app")
5 | // vue3
6 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/componentUpdate/App.js:
--------------------------------------------------------------------------------
1 | import { h, ref } from "../../dist/tiny-vue3.esm.js";
2 | import Child from "./Child.js";
3 |
4 | export const App = {
5 | name: "App",
6 | setup() {
7 | const msg = ref("123");
8 | const count = ref(1);
9 |
10 | window.msg = msg;
11 |
12 | const changeChildProps = () => {
13 | msg.value = "456";
14 | };
15 |
16 | const changeCount = () => {
17 | count.value++;
18 | };
19 |
20 | return { msg, changeChildProps, changeCount, count };
21 | },
22 |
23 | render() {
24 | return h("div", {}, [
25 | h("div", {}, "你好"),
26 | h(
27 | "button",
28 | {
29 | onClick: this.changeChildProps,
30 | },
31 | "change child props"
32 | ),
33 | h(Child, {
34 | msg: this.msg,
35 | }),
36 | h(
37 | "button",
38 | {
39 | onClick: this.changeCount,
40 | },
41 | "change self count"
42 | ),
43 | h("p", {}, "count: " + this.count),
44 | ]);
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/componentUpdate/Child.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../dist/tiny-vue3.esm.js";
2 | export default {
3 | name: "Child",
4 | setup(props, { emit }) {},
5 | render(proxy) {
6 | return h("div", {}, [h("div", {}, "child - props - msg: " + this.$props.msg)]);
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/componentUpdate/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/componentUpdate/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../dist/tiny-vue3.esm.js";
2 | import { App } from "./App.js";
3 |
4 | const rootContainer = document.querySelector("#app");
5 | createApp(App).mount(rootContainer);
6 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/currentInstance/App.js:
--------------------------------------------------------------------------------
1 | import { h, getCurrentInstance } from "../../dist/tiny-vue3.esm.js"
2 | import { Foo } from "./Foo.js"
3 |
4 | export const App = {
5 | name: "App",
6 | render() {
7 | return h("div", {}, [h("p", {}, "currentInstance demo"), h(Foo)])
8 | },
9 | setup() {
10 | const instance = getCurrentInstance()
11 | console.log("App:", instance);
12 | }
13 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/currentInstance/Foo.js:
--------------------------------------------------------------------------------
1 | import { h, getCurrentInstance } from "../../dist/tiny-vue3.esm.js"
2 |
3 | export const Foo = {
4 | name: "Foo",
5 | render() {
6 | return h("div", {}, "foo")
7 | },
8 | setup() {
9 | const instance = getCurrentInstance()
10 | console.log("App:", instance);
11 | return {}
12 | }
13 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/currentInstance/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/currentInstance/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../dist/tiny-vue3.esm.js"
2 | import { App } from "./App.js"
3 |
4 | const rootContainer = document.querySelector("#app")
5 | // vue3
6 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/customRenderer/App.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../dist/tiny-vue3.esm.js"
2 |
3 | export const App = {
4 | setup() {
5 | return {
6 | x: 100,
7 | y: 100
8 | }
9 | },
10 | render() {
11 | return h("rect", {x: this.x, y: this.y})
12 | }
13 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/customRenderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/customRenderer/main.js:
--------------------------------------------------------------------------------
1 | import { createRenderer } from "../../dist/tiny-vue3.esm.js"
2 | import { App } from "./App.js"
3 |
4 | console.log(PIXI);
5 | const game = new PIXI.Application({
6 | width: 500,
7 | height: 500
8 | })
9 | document.body.append(game.view)
10 | const renderer = createRenderer({
11 | createElement(type) {
12 | const rect = new PIXI.Graphics()
13 | rect.beginFill(0xff0000)
14 | rect.drawRect(0, 0, 100, 100)
15 | rect.endFill()
16 | return rect
17 | },
18 | patchProp(el, key, val) {
19 | el[key] = val
20 | },
21 | insert(el, parent) {
22 | parent.addChild(el)
23 | }
24 | })
25 | // const rootContainer = document.querySelector("#app")
26 | // // vue3
27 | renderer.createApp(App).mount(game.stage)
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/helloworld/App.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../dist/tiny-vue3.esm.js"
2 | import { Foo } from "./Foo.js"
3 |
4 | export const App = {
5 | name: "App",
6 | // .vue
7 | //
8 | // 必须要写 render
9 | render() {
10 | // ui
11 | window.self = this
12 | return h(
13 | "div", {
14 | id: "root",
15 | class: ["red", "test"],
16 | onClick() {
17 | console.log('click')
18 | },
19 | onMousedown() {
20 | console.log('mousedown')
21 | }
22 | },
23 | [h("div", {}, "hi," + this.msg), h(Foo, {
24 | count: 1
25 | })]
26 | // setupState
27 | // this.$el -> get root element
28 | // "hi, " + this.msg
29 | // string
30 | // "hi, tiny-vue3"
31 |
32 | // array
33 | // [h("p", { class: "red"}, "hi"), h("p", { class: "blue"}, "tiny-vue3")]
34 | )
35 | },
36 | setup() {
37 | // 可参考composition api
38 |
39 | return {
40 | msg: "tiny-vue3"
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/helloworld/Foo.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../dist/tiny-vue3.esm.js"
2 |
3 | export const Foo = {
4 | setup(props) {
5 | // 1. 在setup中接收props
6 | // props.count
7 | console.log(props);
8 |
9 | // 3. props不可修改 shallow readonly
10 | props.count++
11 | console.log(props);
12 | },
13 | render() {
14 | // 2. 在render中可以访问props的属性
15 | return h("div", {}, "foo:" + this.count)
16 | }
17 | }
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/helloworld/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/helloworld/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../dist/tiny-vue3.esm.js"
2 | import { App } from "./App.js"
3 |
4 | const rootContainer = document.querySelector("#app")
5 | // vue3
6 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/nextTicker/App.js:
--------------------------------------------------------------------------------
1 | import { h, ref, getCurrentInstance, nextTick } from "../../dist/tiny-vue3.esm.js";
2 |
3 | export const App = {
4 | name: "App",
5 |
6 | setup() {
7 | const count = ref(1);
8 | const instance = getCurrentInstance()
9 |
10 | function onClick(){
11 | for (let i = 0; i < 100; i++) {
12 | console.log("update");
13 | count.value = i
14 | }
15 |
16 | console.log(instance);
17 | nextTick(() => {
18 | console.log(instance);
19 | })
20 |
21 | // await nextTick()
22 | // console.log(instance);
23 | }
24 |
25 |
26 | return {
27 | count,
28 | onClick
29 | };
30 | },
31 | render() {
32 | const button = h("button", { onClick: this.onClick }, "update")
33 | const p = h("p", {}, "count: " + this.count)
34 | return h("div", {}, [button, p])
35 | },
36 | };
37 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/nextTicker/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/nextTicker/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../dist/tiny-vue3.esm.js";
2 | import { App } from "./App.js";
3 |
4 | const rootContainer = document.querySelector("#app");
5 | createApp(App).mount(rootContainer);
6 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/patchChildren/App.js:
--------------------------------------------------------------------------------
1 | import { h } from "../../dist/tiny-vue3.esm.js";
2 |
3 | import ArrayToText from "./ArrayToText.js";
4 | import TextToText from "./TextToText.js";
5 | import TextToArray from "./TextToArray.js";
6 | import ArrayToArray from "./ArrayToArray.js";
7 |
8 | export default {
9 | name: "App",
10 | setup() {},
11 |
12 | render() {
13 | return h("div", { tId: 1 }, [
14 | h("p", {}, "主页"),
15 | // 老的是 array 新的是 text
16 | // h(ArrayToText),
17 | // 老的是 text 新的是 text
18 | // h(TextToText),
19 | // 老的是 text 新的是 array
20 | // h(TextToArray)
21 | // 老的是 array 新的是 array
22 | h(ArrayToArray),
23 | ]);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/patchChildren/ArrayToArray.js:
--------------------------------------------------------------------------------
1 | // 老的是 array
2 | // 新的是 array
3 |
4 | import { ref, h } from "../../dist/tiny-vue3.esm.js";
5 |
6 | // 1. 左侧的对比
7 | // (a b) c
8 | // (a b) d e
9 | // const prevChildren = [
10 | // h("p", { key: "A" }, "A"),
11 | // h("p", { key: "B" }, "B"),
12 | // h("p", { key: "C" }, "C"),
13 | // ];
14 | // const nextChildren = [
15 | // h("p", { key: "A" }, "A"),
16 | // h("p", { key: "B" }, "B"),
17 | // h("p", { key: "D" }, "D"),
18 | // h("p", { key: "E" }, "E"),
19 | // ];
20 |
21 | // 2. 右侧的对比
22 | // a (b c)
23 | // d e (b c)
24 | // const prevChildren = [
25 | // h("p", { key: "A" }, "A"),
26 | // h("p", { key: "B" }, "B"),
27 | // h("p", { key: "C" }, "C"),
28 | // ];
29 | // const nextChildren = [
30 | // h("p", { key: "D" }, "D"),
31 | // h("p", { key: "E" }, "E"),
32 | // h("p", { key: "B" }, "B"),
33 | // h("p", { key: "C" }, "C"),
34 | // ];
35 |
36 | // 3. 新的比老的长
37 | // 创建新的
38 | // 左侧
39 | // (a b)
40 | // (a b) c
41 | // i = 2, e1 = 1, e2 = 2
42 | // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
43 | // const nextChildren = [
44 | // h("p", { key: "A" }, "A"),
45 | // h("p", { key: "B" }, "B"),
46 | // h("p", { key: "C" }, "C"),
47 | // h("p", { key: "D" }, "D"),
48 | // ];
49 |
50 | // 右侧
51 | // (a b)
52 | // c (a b)
53 | // i = 0, e1 = -1, e2 = 0
54 | // const prevChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
55 | // const nextChildren = [
56 | // h("p", { key: "D" }, "D"),
57 | // h("p", { key: "C" }, "C"),
58 | // h("p", { key: "A" }, "A"),
59 | // h("p", { key: "B" }, "B"),
60 | // ];
61 |
62 | // 4. 老的比新的长
63 | // 删除老的
64 | // 左侧
65 | // (a b) c
66 | // (a b)
67 | // i = 2, e1 = 2, e2 = 1
68 | // const prevChildren = [
69 | // h("p", { key: "A" }, "A"),
70 | // h("p", { key: "B" }, "B"),
71 | // h("p", { key: "C" }, "C"),
72 | // ];
73 | // const nextChildren = [h("p", { key: "A" }, "A"), h("p", { key: "B" }, "B")];
74 |
75 | // 右侧
76 | // a (b c)
77 | // (b c)
78 | // i = 0, e1 = 0, e2 = -1
79 |
80 | // const prevChildren = [
81 | // h("p", { key: "A" }, "A"),
82 | // h("p", { key: "B" }, "B"),
83 | // h("p", { key: "C" }, "C"),
84 | // ];
85 | // const nextChildren = [h("p", { key: "B" }, "B"), h("p", { key: "C" }, "C")];
86 |
87 | // 5. 对比中间的部分
88 | // 删除老的 (在老的里面存在,新的里面不存在)
89 | // 5.1
90 | // a,b,(c,d),f,g
91 | // a,b,(e,c),f,g
92 | // D 节点在新的里面是没有的 - 需要删除掉
93 | // C 节点 props 也发生了变化
94 |
95 | // const prevChildren = [
96 | // h("p", { key: "A" }, "A"),
97 | // h("p", { key: "B" }, "B"),
98 | // h("p", { key: "C", id: "c-prev" }, "C"),
99 | // h("p", { key: "D" }, "D"),
100 | // h("p", { key: "F" }, "F"),
101 | // h("p", { key: "G" }, "G"),
102 | // ];
103 |
104 | // const nextChildren = [
105 | // h("p", { key: "A" }, "A"),
106 | // h("p", { key: "B" }, "B"),
107 | // h("p", { key: "E" }, "E"),
108 | // h("p", { key: "C", id:"c-next" }, "C"),
109 | // h("p", { key: "F" }, "F"),
110 | // h("p", { key: "G" }, "G"),
111 | // ];
112 |
113 | // 5.1.1
114 | // a,b,(c,e,d),f,g
115 | // a,b,(e,c),f,g
116 | // 中间部分,老的比新的多, 那么多出来的直接就可以被干掉(优化删除逻辑)
117 | // const prevChildren = [
118 | // h("p", { key: "A" }, "A"),
119 | // h("p", { key: "B" }, "B"),
120 | // h("p", { key: "C", id: "c-prev" }, "C"),
121 | // h("p", { key: "E" }, "E"),
122 | // h("p", { key: "D" }, "D"),
123 | // h("p", { key: "F" }, "F"),
124 | // h("p", { key: "G" }, "G"),
125 | // ];
126 |
127 | // const nextChildren = [
128 | // h("p", { key: "A" }, "A"),
129 | // h("p", { key: "B" }, "B"),
130 | // h("p", { key: "E" }, "E"),
131 | // h("p", { key: "C", id:"c-next" }, "C"),
132 | // h("p", { key: "F" }, "F"),
133 | // h("p", { key: "G" }, "G"),
134 | // ];
135 |
136 | // 2 移动 (节点存在于新的和老的里面,但是位置变了)
137 |
138 | // 2.1
139 | // a,b,(c,d,e),f,g
140 | // a,b,(e,c,d),f,g
141 | // 最长子序列: [1,2]
142 |
143 | // const prevChildren = [
144 | // h("p", { key: "A" }, "A"),
145 | // h("p", { key: "B" }, "B"),
146 | // h("p", { key: "C" }, "C"),
147 | // h("p", { key: "D" }, "D"),
148 | // h("p", { key: "E" }, "E"),
149 | // h("p", { key: "F" }, "F"),
150 | // h("p", { key: "G" }, "G"),
151 | // ];
152 |
153 | // const nextChildren = [
154 | // h("p", { key: "A" }, "A"),
155 | // h("p", { key: "B" }, "B"),
156 | // h("p", { key: "E" }, "E"),
157 | // h("p", { key: "C" }, "C"),
158 | // h("p", { key: "D" }, "D"),
159 | // h("p", { key: "F" }, "F"),
160 | // h("p", { key: "G" }, "G"),
161 | // ];
162 |
163 | // 3. 创建新的节点
164 | // a,b,(c,e),f,g
165 | // a,b,(e,c,d),f,g
166 | // d 节点在老的节点中不存在,新的里面存在,所以需要创建
167 | // const prevChildren = [
168 | // h("p", { key: "A" }, "A"),
169 | // h("p", { key: "B" }, "B"),
170 | // h("p", { key: "C" }, "C"),
171 | // h("p", { key: "E" }, "E"),
172 | // h("p", { key: "F" }, "F"),
173 | // h("p", { key: "G" }, "G"),
174 | // ];
175 |
176 | // const nextChildren = [
177 | // h("p", { key: "A" }, "A"),
178 | // h("p", { key: "B" }, "B"),
179 | // h("p", { key: "E" }, "E"),
180 | // h("p", { key: "C" }, "C"),
181 | // h("p", { key: "D" }, "D"),
182 | // h("p", { key: "F" }, "F"),
183 | // h("p", { key: "G" }, "G"),
184 | // ];
185 |
186 | // 综合例子
187 | // a,b,(c,d,e,z),f,g
188 | // a,b,(d,c,y,e),f,g
189 |
190 | // const prevChildren = [
191 | // h("p", { key: "A" }, "A"),
192 | // h("p", { key: "B" }, "B"),
193 | // h("p", { key: "C" }, "C"),
194 | // h("p", { key: "D" }, "D"),
195 | // h("p", { key: "E" }, "E"),
196 | // h("p", { key: "Z" }, "Z"),
197 | // h("p", { key: "F" }, "F"),
198 | // h("p", { key: "G" }, "G"),
199 | // ];
200 |
201 | // const nextChildren = [
202 | // h("p", { key: "A" }, "A"),
203 | // h("p", { key: "B" }, "B"),
204 | // h("p", { key: "D" }, "D"),
205 | // h("p", { key: "C" }, "C"),
206 | // h("p", { key: "Y" }, "Y"),
207 | // h("p", { key: "E" }, "E"),
208 | // h("p", { key: "F" }, "F"),
209 | // h("p", { key: "G" }, "G"),
210 | // ];
211 |
212 | // fix
213 | const prevChildren = [
214 | h("p", { key: "A" }, "A"),
215 | h("p", {}, "C"),
216 | h("p", { key: "B" }, "B"),
217 | h("p", { key: "D" }, "D"),
218 | ];
219 |
220 | const nextChildren = [
221 | h("p", { key: "A" }, "A"),
222 | h("p", { key: "B" }, "B"),
223 | h("p", {}, "C"),
224 | h("p", { key: "D" }, "D"),
225 | ];
226 |
227 | export default {
228 | name: "ArrayToArray",
229 | setup() {
230 | const isChange = ref(false);
231 | window.isChange = isChange;
232 |
233 | return {
234 | isChange,
235 | };
236 | },
237 | render() {
238 | const self = this;
239 |
240 | return self.isChange === true
241 | ? h("div", {}, nextChildren)
242 | : h("div", {}, prevChildren);
243 | },
244 | };
245 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/patchChildren/ArrayToText.js:
--------------------------------------------------------------------------------
1 | // 老的是 array
2 | // 新的是 text
3 |
4 | import { ref, h } from "../../dist/tiny-vue3.esm.js";
5 | const nextChildren = "newChildren";
6 | const prevChildren = [h("div", {}, "A"), h("div", {}, "B")];
7 |
8 | export default {
9 | name: "ArrayToText",
10 | setup() {
11 | const isChange = ref(false);
12 | window.isChange = isChange;
13 |
14 | return {
15 | isChange,
16 | };
17 | },
18 | render() {
19 | const self = this;
20 |
21 | return self.isChange === true
22 | ? h("div", {}, nextChildren)
23 | : h("div", {}, prevChildren);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/patchChildren/TextToArray.js:
--------------------------------------------------------------------------------
1 | // 新的是 array
2 | // 老的是 text
3 | import { ref, h } from "../../dist/tiny-vue3.esm.js";
4 |
5 | const prevChildren = "oldChild";
6 | const nextChildren = [h("div", {}, "A"), h("div", {}, "B")];
7 |
8 | export default {
9 | name: "TextToArray",
10 | setup() {
11 | const isChange = ref(false);
12 | window.isChange = isChange;
13 |
14 | return {
15 | isChange,
16 | };
17 | },
18 | render() {
19 | const self = this;
20 |
21 | return self.isChange === true
22 | ? h("div", {}, nextChildren)
23 | : h("div", {}, prevChildren);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/patchChildren/TextToText.js:
--------------------------------------------------------------------------------
1 | // 新的是 text
2 | // 老的是 text
3 | import { ref, h } from "../../dist/tiny-vue3.esm.js";
4 |
5 | const prevChildren = "oldChild";
6 | const nextChildren = "newChild";
7 |
8 | export default {
9 | name: "TextToText",
10 | setup() {
11 | const isChange = ref(false);
12 | window.isChange = isChange;
13 |
14 | return {
15 | isChange,
16 | };
17 | },
18 | render() {
19 | const self = this;
20 |
21 | return self.isChange === true
22 | ? h("div", {}, nextChildren)
23 | : h("div", {}, prevChildren);
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/patchChildren/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/patchChildren/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "../../dist/tiny-vue3.esm.js";
2 | import App from "./App.js";
3 |
4 | const rootContainer = document.querySelector("#root");
5 | createApp(App).mount(rootContainer);
6 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/update/App.js:
--------------------------------------------------------------------------------
1 | import { h, ref } from "../../dist/tiny-vue3.esm.js";
2 |
3 | export const App = {
4 | name: "App",
5 |
6 | setup() {
7 | const count = ref(0);
8 |
9 | const onClick = () => {
10 | count.value++;
11 | };
12 |
13 | const props = ref({
14 | foo: "foo",
15 | bar: "bar",
16 | });
17 | const onChangePropsDemo1 = () => {
18 | props.value.foo = "new-foo";
19 | };
20 |
21 | const onChangePropsDemo2 = () => {
22 | props.value.foo = undefined;
23 | };
24 |
25 | const onChangePropsDemo3 = () => {
26 | props.value = {
27 | foo: "foo",
28 | };
29 | };
30 |
31 | return {
32 | count,
33 | onClick,
34 | onChangePropsDemo1,
35 | onChangePropsDemo2,
36 | onChangePropsDemo3,
37 | props,
38 | };
39 | },
40 | render() {
41 | return h(
42 | "div",
43 | {
44 | id: "root",
45 | ...this.props,
46 | },
47 | [
48 | h("div", {}, "count:" + this.count),
49 |
50 | h(
51 | "button",
52 | {
53 | onClick: this.onClick,
54 | },
55 | "click"
56 | ),
57 |
58 | h(
59 | "button",
60 | {
61 | onClick: this.onChangePropsDemo1,
62 | },
63 | "changeProps - 值改变了 - 修改"
64 | ),
65 |
66 | h(
67 | "button",
68 | {
69 | onClick: this.onChangePropsDemo2,
70 | },
71 | "changeProps - 值变成了 undefined - 删除"
72 | ),
73 |
74 | h(
75 | "button",
76 | {
77 | onClick: this.onChangePropsDemo3,
78 | },
79 | "changeProps - key 在新的里面没有了 - 删除"
80 | )
81 | ]
82 | )
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/update/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/examples/update/main.js:
--------------------------------------------------------------------------------
1 |
2 | import { createApp } from "../../dist/tiny-vue3.esm.js"
3 | import { App } from "./App.js"
4 |
5 | const rootContainer = document.querySelector("#app")
6 | // vue3
7 | createApp(App).mount(rootContainer)
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tiny-vue/vue",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@tiny-vue/compiler-core": "workspace:^1.0.0",
14 | "@tiny-vue/runtime-dom": "workspace:^1.0.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tiny-vue3/packages/vue/src/index.ts:
--------------------------------------------------------------------------------
1 | // mini-vue 出口
2 | export * from "@tiny-vue/runtime-dom"
3 | import { baseCompile } from "@tiny-vue/compiler-core"
4 | import * as runtimeDom from "@tiny-vue/runtime-dom"
5 | import { registerRuntimeCompiler } from "@tiny-vue/runtime-dom"
6 |
7 | function compileToFunction(template) {
8 | const { code } = baseCompile(template)
9 | const render = new Function("Vue", code)(runtimeDom)
10 | return render
11 | }
12 |
13 | registerRuntimeCompiler(compileToFunction)
14 |
--------------------------------------------------------------------------------
/tiny-vue3/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/*"
--------------------------------------------------------------------------------
/tiny-vue3/rollup.config.js:
--------------------------------------------------------------------------------
1 | import pkg from './package.json'
2 | import typescript from "@rollup/plugin-typescript"
3 | export default {
4 | input: './packages/vue/src/index.ts',
5 | output: [
6 | // 1. cjs -> commonjs
7 | // 2. esm
8 | {
9 | format: "cjs",
10 | file: pkg.main
11 | },
12 | {
13 | format: "es",
14 | file: pkg.module
15 | }
16 | ],
17 | plugins: [
18 | typescript()
19 | ]
20 | }
--------------------------------------------------------------------------------
/tiny-vue3/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Enable incremental compilation */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | "lib": ["DOM", "ES6", "ES2016"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 |
26 | /* Modules */
27 | "module": "esnext", /* Specify what module code is generated. */
28 | // "rootDir": "./", /* Specify the root folder within your source files. */
29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
34 | "types": ["vitest/globals"], /* Specify type package names to be included without being referenced in a source file. */
35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
36 | // "resolveJsonModule": true, /* Enable importing .json files */
37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
38 |
39 | /* JavaScript Support */
40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
43 |
44 | /* Emit */
45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
50 | // "outDir": "./", /* Specify an output folder for all emitted files. */
51 | // "removeComments": true, /* Disable emitting comments. */
52 | // "noEmit": true, /* Disable emitting files from a compilation. */
53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
61 | // "newLine": "crlf", /* Set the newline character for emitting files. */
62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
68 |
69 | /* Interop Constraints */
70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
75 |
76 | /* Type Checking */
77 | "strict": true, /* Enable all strict type-checking options. */
78 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
96 |
97 | /* Completeness */
98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
99 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */
100 | "paths": {
101 | "@tiny-vue/*": [
102 | "./packages/*/src"
103 | ]
104 | }
105 | },
106 | "include": [
107 | "packages/*/src",
108 | "packages/*/__tests__"
109 | ]
110 | }
111 |
--------------------------------------------------------------------------------
/tiny-vue3/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 | import path from 'path'
3 |
4 | export default defineConfig({
5 | test: {
6 | globals: true
7 | },
8 | resolve: {
9 | alias: [
10 | {
11 | find: /@tiny-vue\/(\w*)/,
12 | replacement: path.resolve(__dirname, "packages") + "/$1/src"
13 | }
14 | ]
15 | }
16 | })
--------------------------------------------------------------------------------
/tiny-webpack/index.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Befend/tiny-series/e9a65d6a22c5f5ba30a32a67dcda0413556893ed/tiny-webpack/index.html
--------------------------------------------------------------------------------