├── 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 | ![image.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/e69416fc65464cdfbd17b3a90c275052~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bGx5ran5ZCs6bm_6bij:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMzQ5MTcwNDY2MzE5MDYwNSJ9&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1740915958&x-orig-sign=KGVNud1kRvFdc%2BPrTYLrDd29JQ8%3D) 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 | ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eb58b3c985b747e9a1686bc2bb806f13~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=600&h=362&s=34665&e=png&b=fdfafa) 127 | 128 | #### 渲染结果 129 | 130 | ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/84da0e33191b4472bf2a5a8495a8214a~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=987&h=382&s=40593&e=png&b=ffffff) 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 | ![c7885e81d59849868445d3c0b37d655d~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d4157d62272d448985534f4072e07133~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1512&h=492&s=252446&e=png&b=faf0eb) 47 | 48 | ### `requestIdleCallback` 执行时机 49 | 50 | 在完成一帧中的输入处理、渲染和合成之后,线程会进入空闲时期(idle period),直到下一帧开始,或者队列中的任务被激活,又或者收到了用户新的输入。requestIdleCallback 定义的回调就是在这段空闲时期执行: 51 | 52 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/16670e20845843369f611cfd6207d14a~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=737&h=139&s=19307&e=png&a=1&b=cde0f1) 53 | 此类空闲期会在活动动画和屏幕更新期间频繁出现,但通常非常短(比如:在 60Hz 的设备下小于 16ms) 54 | 55 | 另一个空闲期的例子就是当用户代理空闲且没有发生屏幕更新时,浏览器其实处于空闲状态。这时候用户代理可能没有即将到来的任务来限制空闲期的结束。为了避免不可预测的任务(例如处理用户输入)中造成用户可感知的延迟,这些空闲周期的长度应限制为最大值[50ms](https://www.w3.org/TR/requestidlecallback/#why50)。一旦空闲期结束,用户代理可以安排另一个空闲期(如果它仍然空闲),使后台工作能够在较长的空闲时间内继续进行: 56 | 57 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dcea367449f147d0bae3957575ff6f38~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=670&h=173&s=19545&e=png&a=1&b=fff1cc) 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 | ![c7885e81d59849868445d3c0b37d655d~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d4157d62272d448985534f4072e07133~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1512&h=492&s=252446&e=png&b=faf0eb) 49 | 50 | ### `requestIdleCallback` 执行时机 51 | 52 | 在完成一帧中的输入处理、渲染和合成之后,线程会进入空闲时期(idle period),直到下一帧开始,或者队列中的任务被激活,又或者收到了用户新的输入。requestIdleCallback 定义的回调就是在这段空闲时期执行: 53 | 54 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/16670e20845843369f611cfd6207d14a~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=737&h=139&s=19307&e=png&a=1&b=cde0f1) 55 | 此类空闲期会在活动动画和屏幕更新期间频繁出现,但通常非常短(比如:在 60Hz 的设备下小于 16ms) 56 | 57 | 另一个空闲期的例子就是当用户代理空闲且没有发生屏幕更新时,浏览器其实处于空闲状态。这时候用户代理可能没有即将到来的任务来限制空闲期的结束。为了避免不可预测的任务(例如处理用户输入)中造成用户可感知的延迟,这些空闲周期的长度应限制为最大值[50ms](https://www.w3.org/TR/requestidlecallback/#why50)。一旦空闲期结束,用户代理可以安排另一个空闲期(如果它仍然空闲),使后台工作能够在较长的空闲时间内继续进行: 58 | 59 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dcea367449f147d0bae3957575ff6f38~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=670&h=173&s=19545&e=png&a=1&b=fff1cc) 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("

hi

{{message}}
") 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("= 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(" { 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 --------------------------------------------------------------------------------