├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── babel.config.js ├── jest.config.js ├── package.json ├── packages ├── compiler-core │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── codegen.spec.ts.snap │ │ ├── codegen.spec.ts │ │ ├── parse.spec.ts │ │ └── transform.spec.ts │ └── src │ │ ├── ast.ts │ │ ├── codegen.ts │ │ ├── compile.ts │ │ ├── index.ts │ │ ├── options.ts │ │ ├── parse.ts │ │ ├── runtimeHelpers.ts │ │ ├── transform.ts │ │ ├── transform │ │ ├── hoistStatic.ts │ │ ├── transformElement.ts │ │ ├── transformExpression.ts │ │ └── transformText.ts │ │ └── utils.ts ├── reactivity │ ├── __tests__ │ │ ├── computed.spec.ts │ │ ├── effect.spec.ts │ │ ├── index.spec.ts │ │ ├── reactive.spec.ts │ │ ├── readonly.spec.ts │ │ ├── ref.spec.ts │ │ ├── shallowReactive.spec.ts │ │ └── shallowReadonly.spec.ts │ └── src │ │ ├── baseHandlers.ts │ │ ├── computed.ts │ │ ├── dep.ts │ │ ├── effect.ts │ │ ├── index.ts │ │ ├── reactive.ts │ │ └── ref.ts ├── runtime-core │ ├── __tests__ │ │ └── scheduler.spec.ts │ └── src │ │ ├── apiCreateApp.ts │ │ ├── apiInject.ts │ │ ├── apiLifecycle.ts │ │ ├── apiWatch.ts │ │ ├── component.ts │ │ ├── componentEmit.ts │ │ ├── componentProps.ts │ │ ├── componentPublicInstance.ts │ │ ├── componentRenderUtils.ts │ │ ├── componentSlots.ts │ │ ├── h.ts │ │ ├── helpers │ │ └── renderSlot.ts │ │ ├── index.ts │ │ ├── renderer.ts │ │ ├── scheduler.ts │ │ └── vnode.ts ├── runtime-dom │ └── src │ │ ├── index.ts │ │ ├── nodeOps.ts │ │ └── patchProp.ts ├── shared │ └── src │ │ ├── index.ts │ │ ├── shapeFlags.ts │ │ └── toDisplayString.ts └── vue │ ├── examples │ ├── complie-base │ │ ├── index.html │ │ └── main.js │ ├── components │ │ ├── app.js │ │ ├── foo.js │ │ ├── index.html │ │ └── main.js │ ├── helloworld │ │ ├── app.js │ │ ├── index.html │ │ └── main.js │ ├── lifecycle │ │ ├── app.js │ │ ├── index.html │ │ └── main.js │ ├── patchChildren │ │ ├── ArrayToArray.js │ │ ├── ArrayToText.js │ │ ├── TextToArray.js │ │ ├── TextToText.js │ │ ├── app.js │ │ ├── index.html │ │ └── main.js │ ├── provide │ │ ├── app.js │ │ ├── baz.js │ │ ├── foo.js │ │ ├── index.html │ │ └── main.js │ ├── slots │ │ ├── app.js │ │ ├── foo.js │ │ ├── index.html │ │ └── main.js │ ├── update │ │ ├── app.js │ │ ├── index.html │ │ └── main.js │ └── watch │ │ ├── app.js │ │ ├── index.html │ │ └── main.js │ └── src │ └── index.ts ├── pnpm-lock.yaml ├── rollup.config.js └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | public -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@ouduidui/eslint-config-ts"], 3 | "rules": { 4 | "@typescript-eslint/no-use-before-define": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | .DS_Store 4 | vscode/ 5 | dist/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mini-vue3 2 | 3 | ## 运行 4 | 5 | ```shell 6 | # 安装依赖 7 | pnpm i 8 | 9 | # 测试 10 | pnpm test:mini-vue3 11 | ``` 12 | 13 | ## 实现 14 | 15 | - [x] reactive 16 | - [x] runtime-core 17 | - [x] runtime-dom 18 | - [x] complier-core 19 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { 3 | 'compiler-core/(.*?)$': '/packages/compiler-core/src/$1', 4 | 'runtime-core/(.*?)$': '/packages/runtime-core/src/$1', 5 | 'runtime-dom/(.*?)$': '/packages/runtime-dom/src/$1', 6 | 'shared/(.*?)$': '/packages/shared/src/$1', 7 | 'reactivity/(.*?)$': '/packages/reactivity/src/$1', 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-vue3", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --watch", 8 | "example": "live-server packages/vue", 9 | "build": "rollup -c -w --sourcemap rollup.config.js", 10 | "dev": "pnpm build & pnpm example", 11 | "format": "eslint \"**/*.{vue,ts,js}\" --fix", 12 | "lint": "eslint \"**/*.{vue,ts,js}\"" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.16.0", 16 | "@babel/preset-env": "^7.16.4", 17 | "@babel/preset-typescript": "^7.16.0", 18 | "@ouduidui/eslint-config-ts": "^0.0.2", 19 | "@rollup/plugin-typescript": "^8.3.0", 20 | "@types/jest": "^27.0.3", 21 | "@types/rewire": "^2.5.28", 22 | "babel-jest": "^27.3.1", 23 | "eslint": "^8.11.0", 24 | "jest": "^27.3.1", 25 | "live-server": "^1.2.1", 26 | "rewire": "^6.0.0", 27 | "rollup": "^2.60.0", 28 | "tslib": "^2.3.1", 29 | "typescript": "^4.5.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`codegen element 1`] = `"return function render(_ctx, _cache){return }"`; 4 | 5 | exports[`codegen interpolation 1`] = ` 6 | "const { toDisplayString: _toDisplayString } = Vue 7 | return function render(_ctx, _cache){return _toDisplayString(_ctx.message)}" 8 | `; 9 | 10 | exports[`codegen interpolation 2`] = ` 11 | "const { toDisplayString: _toDisplayString } = Vue 12 | return function render(_ctx, _cache){return _toDisplayString(_ctx.message)}" 13 | `; 14 | 15 | exports[`codegen string 1`] = `"return function render(_ctx, _cache){return \\"helloworld\\"}"`; 16 | -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/codegen.spec.ts: -------------------------------------------------------------------------------- 1 | import { transform } from 'compiler-core/transform' 2 | import { generate } from 'compiler-core/codegen' 3 | import { baseParse } from 'compiler-core/parse' 4 | import { transformExpression } from 'compiler-core/transform/transformExpression' 5 | import { transformElement } from 'compiler-core/transform/transformElement' 6 | import { transformText } from 'compiler-core/transform/transformText' 7 | 8 | describe('codegen', () => { 9 | it('string', () => { 10 | const ast = baseParse('helloworld') 11 | transform(ast, {}) 12 | const { code } = generate(ast) 13 | expect(code).toMatchInlineSnapshot('"return function render(_ctx, _cache){return \\"helloworld\\"}"') 14 | }) 15 | 16 | it('interpolation', () => { 17 | const ast = baseParse('{{message}}') 18 | transform(ast, { 19 | nodeTransforms: [transformExpression], 20 | }) 21 | const { code } = generate(ast) 22 | expect(code).toMatchInlineSnapshot(` 23 | "const { toDisplayString: _toDisplayString } = Vue 24 | return function render(_ctx, _cache){return _toDisplayString(_ctx.message)}" 25 | `) 26 | }) 27 | 28 | it('element', () => { 29 | const ast = baseParse('
') 30 | transform(ast, { 31 | nodeTransforms: [transformElement], 32 | }) 33 | const { code } = generate(ast) 34 | expect(code).toMatchInlineSnapshot(` 35 | "const { createElementVNode: _createElementVNode } = Vue 36 | return function render(_ctx, _cache){return _createElementVNode('div',[])}" 37 | `) 38 | }) 39 | 40 | it('complex element', () => { 41 | const ast = baseParse('
Hi, {{message}}
') 42 | transform(ast, { 43 | nodeTransforms: [transformExpression, transformElement, transformText], 44 | }) 45 | 46 | const { code } = generate(ast) 47 | expect(code).toMatchInlineSnapshot(` 48 | "const { toDisplayString: _toDisplayString,createElementVNode: _createElementVNode } = Vue 49 | return function render(_ctx, _cache){return _createElementVNode('div',[],[\\"Hi, \\" + _toDisplayString(_ctx.message)])}" 50 | `) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/parse.spec.ts: -------------------------------------------------------------------------------- 1 | import { baseParse } from 'compiler-core/parse' 2 | import { ElementTypes, NodeTypes } from 'compiler-core/ast' 3 | 4 | describe('Parse', () => { 5 | describe('Text', () => { 6 | it('simple text', () => { 7 | const ast = baseParse('some text') 8 | expect(ast.children[0]).toStrictEqual({ 9 | type: NodeTypes.TEXT, 10 | content: 'some text', 11 | }) 12 | }) 13 | }) 14 | 15 | describe('Interpolation', () => { 16 | it('simple interpolation', () => { 17 | const ast = baseParse('{{ message }}') 18 | // root 19 | expect(ast.children[0]).toStrictEqual({ 20 | type: NodeTypes.INTERPOLATION, 21 | content: { 22 | type: NodeTypes.SIMPLE_EXPRESSION, 23 | content: 'message', 24 | }, 25 | }) 26 | }) 27 | }) 28 | 29 | describe('Element', () => { 30 | it('simple div', () => { 31 | const ast = baseParse('
HelloWorld
') 32 | expect(ast.children[0]).toStrictEqual({ 33 | type: NodeTypes.ELEMENT, 34 | tag: 'div', 35 | isSelfClosing: false, 36 | tagType: ElementTypes.ELEMENT, 37 | codegenNode: undefined, 38 | props: [], 39 | children: [ 40 | { 41 | type: NodeTypes.TEXT, 42 | content: 'HelloWorld', 43 | }, 44 | ], 45 | }) 46 | }) 47 | 48 | it('empty', () => { 49 | const ast = baseParse('
') 50 | expect(ast.children[0]).toStrictEqual({ 51 | codegenNode: undefined, 52 | type: NodeTypes.ELEMENT, 53 | tag: 'div', 54 | isSelfClosing: false, 55 | tagType: ElementTypes.ELEMENT, 56 | props: [], 57 | children: [], 58 | }) 59 | }) 60 | 61 | it('self closing', () => { 62 | const ast = baseParse('
after') 63 | expect(ast.children[0]).toStrictEqual({ 64 | codegenNode: undefined, 65 | type: NodeTypes.ELEMENT, 66 | tag: 'div', 67 | isSelfClosing: true, 68 | tagType: ElementTypes.ELEMENT, 69 | props: [], 70 | children: [], 71 | }) 72 | }) 73 | 74 | it('attribute with no value', () => { 75 | const ast = baseParse('
') 76 | expect(ast.children[0]).toStrictEqual({ 77 | type: NodeTypes.ELEMENT, 78 | codegenNode: undefined, 79 | tag: 'div', 80 | isSelfClosing: false, 81 | tagType: ElementTypes.ELEMENT, 82 | props: [ 83 | { 84 | type: NodeTypes.ATTRIBUTE, 85 | name: 'id', 86 | value: undefined, 87 | }, 88 | ], 89 | children: [], 90 | }) 91 | }) 92 | 93 | it('attribute with empty value, double quote', () => { 94 | const ast = baseParse('
') 95 | 96 | expect(ast.children[0]).toStrictEqual({ 97 | type: NodeTypes.ELEMENT, 98 | tag: 'div', 99 | tagType: ElementTypes.ELEMENT, 100 | codegenNode: undefined, 101 | props: [ 102 | { 103 | type: NodeTypes.ATTRIBUTE, 104 | name: 'id', 105 | value: { 106 | type: NodeTypes.TEXT, 107 | content: '', 108 | }, 109 | }, 110 | ], 111 | 112 | isSelfClosing: false, 113 | children: [], 114 | }) 115 | }) 116 | 117 | it('attribute with empty value, single quote', () => { 118 | const ast = baseParse('
') 119 | 120 | expect(ast.children[0]).toStrictEqual({ 121 | type: NodeTypes.ELEMENT, 122 | codegenNode: undefined, 123 | tag: 'div', 124 | tagType: ElementTypes.ELEMENT, 125 | props: [ 126 | { 127 | type: NodeTypes.ATTRIBUTE, 128 | name: 'id', 129 | value: { 130 | type: NodeTypes.TEXT, 131 | content: '', 132 | }, 133 | }, 134 | ], 135 | 136 | isSelfClosing: false, 137 | children: [], 138 | }) 139 | }) 140 | 141 | it('attribute with value, double quote', () => { 142 | const ast = baseParse('
') 143 | 144 | expect(ast.children[0]).toStrictEqual({ 145 | codegenNode: undefined, 146 | type: NodeTypes.ELEMENT, 147 | tag: 'div', 148 | tagType: ElementTypes.ELEMENT, 149 | 150 | props: [ 151 | { 152 | type: NodeTypes.ATTRIBUTE, 153 | name: 'id', 154 | value: { 155 | type: NodeTypes.TEXT, 156 | content: '>\'', 157 | }, 158 | }, 159 | ], 160 | 161 | isSelfClosing: false, 162 | children: [], 163 | }) 164 | }) 165 | 166 | it('attribute with value, single quote', () => { 167 | const ast = baseParse('
"\'>
') 168 | expect(ast.children[0]).toStrictEqual({ 169 | codegenNode: undefined, 170 | type: NodeTypes.ELEMENT, 171 | tag: 'div', 172 | tagType: ElementTypes.ELEMENT, 173 | props: [ 174 | { 175 | type: NodeTypes.ATTRIBUTE, 176 | name: 'id', 177 | value: { 178 | type: NodeTypes.TEXT, 179 | content: '>"', 180 | }, 181 | }, 182 | ], 183 | 184 | isSelfClosing: false, 185 | children: [], 186 | }) 187 | }) 188 | 189 | it('attribute with value, unquoted', () => { 190 | const ast = baseParse('
') 191 | 192 | expect(ast.children[0]).toStrictEqual({ 193 | type: NodeTypes.ELEMENT, 194 | tag: 'div', 195 | tagType: ElementTypes.ELEMENT, 196 | codegenNode: undefined, 197 | props: [ 198 | { 199 | type: NodeTypes.ATTRIBUTE, 200 | name: 'id', 201 | value: { 202 | type: NodeTypes.TEXT, 203 | content: 'a/', 204 | }, 205 | }, 206 | ], 207 | 208 | isSelfClosing: false, 209 | children: [], 210 | }) 211 | }) 212 | 213 | it('multiple attributes', () => { 214 | const ast = baseParse('
') 215 | 216 | expect(ast.children[0]).toStrictEqual({ 217 | type: NodeTypes.ELEMENT, 218 | tag: 'div', 219 | tagType: ElementTypes.ELEMENT, 220 | codegenNode: undefined, 221 | props: [ 222 | { 223 | type: NodeTypes.ATTRIBUTE, 224 | name: 'id', 225 | value: { 226 | type: NodeTypes.TEXT, 227 | content: 'a', 228 | }, 229 | }, 230 | { 231 | type: NodeTypes.ATTRIBUTE, 232 | name: 'class', 233 | value: { 234 | type: NodeTypes.TEXT, 235 | content: 'c', 236 | }, 237 | }, 238 | { 239 | type: NodeTypes.ATTRIBUTE, 240 | name: 'inert', 241 | value: undefined, 242 | }, 243 | { 244 | type: NodeTypes.ATTRIBUTE, 245 | name: 'style', 246 | value: { 247 | type: NodeTypes.TEXT, 248 | content: '', 249 | }, 250 | }, 251 | ], 252 | 253 | isSelfClosing: false, 254 | children: [], 255 | }) 256 | }) 257 | 258 | it('template element', () => { 259 | const ast = baseParse('') 260 | expect(ast.children[0]).toMatchObject({ 261 | type: NodeTypes.ELEMENT, 262 | tag: 'template', 263 | tagType: ElementTypes.TEMPLATE, 264 | }) 265 | }) 266 | 267 | it('native element with `isNativeTag`', () => { 268 | const ast = baseParse('
', { 269 | isNativeTag: tag => tag === 'div', 270 | }) 271 | 272 | expect(ast.children[0]).toMatchObject({ 273 | type: NodeTypes.ELEMENT, 274 | tag: 'div', 275 | tagType: ElementTypes.ELEMENT, 276 | }) 277 | 278 | expect(ast.children[1]).toMatchObject({ 279 | type: NodeTypes.ELEMENT, 280 | tag: 'comp', 281 | tagType: ElementTypes.COMPONENT, 282 | }) 283 | 284 | expect(ast.children[2]).toMatchObject({ 285 | type: NodeTypes.ELEMENT, 286 | tag: 'Comp', 287 | tagType: ElementTypes.COMPONENT, 288 | }) 289 | }) 290 | }) 291 | }) 292 | -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/transform.spec.ts: -------------------------------------------------------------------------------- 1 | import type { TemplateNode, TextNode } from 'compiler-core/ast' 2 | import { NodeTypes } from 'compiler-core/ast' 3 | import { baseParse } from 'compiler-core/parse' 4 | import { transform } from 'compiler-core/transform' 5 | 6 | describe('transform', () => { 7 | it('happy path', () => { 8 | const ast = baseParse('
Hello {{message}}
') 9 | const plugin = (node) => { 10 | if (node.type === NodeTypes.TEXT) 11 | node.content += 'World' 12 | } 13 | transform(ast, { 14 | nodeTransforms: [plugin], 15 | }) 16 | 17 | const nodeText = (ast.children[0] as TemplateNode).children[0] as TextNode 18 | expect(nodeText.content).toBe('Hello World') 19 | }) 20 | }) 21 | 22 | describe('codegenNode', () => { 23 | it('string', () => { 24 | const ast = baseParse('HelloWorld') 25 | transform(ast, {}) 26 | expect(ast.codegenNode).toStrictEqual({ 27 | content: 'HelloWorld', 28 | type: 2, 29 | }) 30 | }) 31 | 32 | it('interpolation', () => { 33 | const ast = baseParse('
Hello {{message}}
') 34 | transform(ast, {}) 35 | expect(ast.codegenNode).toStrictEqual({ 36 | children: [ 37 | { 38 | content: 'Hello ', 39 | type: 2, 40 | }, 41 | { 42 | content: { 43 | content: 'message', 44 | type: 4, 45 | }, 46 | type: 5, 47 | }, 48 | ], 49 | isSelfClosing: false, 50 | props: [], 51 | tag: 'div', 52 | tagType: 0, 53 | type: 1, 54 | codegenNode: undefined, 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /packages/compiler-core/src/ast.ts: -------------------------------------------------------------------------------- 1 | import { OPEN_BLOCK } from './runtimeHelpers' 2 | import type { TransformContext } from './transform' 3 | import { getVNodeBlockHelper, getVNodeHelper } from './utils' 4 | 5 | export const enum NodeTypes { 6 | ROOT, // 根节点 7 | ELEMENT, // 元素 8 | TEXT, // 文本 9 | COMMENT, 10 | SIMPLE_EXPRESSION, 11 | INTERPOLATION, // 插值 12 | ATTRIBUTE, // 属性 13 | VNODE_CALL, // vnode 调用 14 | COMPOUND_EXPRESSION, 15 | } 16 | 17 | export const enum ElementTypes { 18 | ELEMENT, 19 | COMPONENT, 20 | SLOT, 21 | TEMPLATE, 22 | } 23 | 24 | export type JSChildNode = 25 | | VNodeCall 26 | | ExpressionNode 27 | 28 | export type TemplateChildNode = InterpolationNode | TextNode | ElementNode | CompoundExpressionNode 29 | 30 | export interface Node { 31 | type: NodeTypes 32 | } 33 | 34 | // 文本节点类型 35 | export interface TextNode extends Node { 36 | type: NodeTypes.TEXT 37 | content: string 38 | } 39 | 40 | // 插值节点类型 41 | export interface InterpolationNode extends Node { 42 | type: NodeTypes.INTERPOLATION 43 | content: ExpressionNode 44 | } 45 | 46 | export interface CompoundExpressionNode extends Node { 47 | type: NodeTypes.COMPOUND_EXPRESSION 48 | children: ( 49 | | SimpleExpressionNode 50 | | CompoundExpressionNode 51 | | InterpolationNode 52 | | TextNode 53 | | string 54 | | symbol 55 | )[] 56 | } 57 | 58 | export type ExpressionNode = SimpleExpressionNode | CompoundExpressionNode 59 | 60 | export interface SimpleExpressionNode { 61 | type: NodeTypes.SIMPLE_EXPRESSION 62 | content: string 63 | } 64 | 65 | // 元素节点类型 66 | export type ElementNode = PlainElementNode | ComponentNode | TemplateNode | SlotOutletNode 67 | 68 | export interface BaseElementNode extends Node { 69 | type: NodeTypes.ELEMENT 70 | tag: string 71 | tagType: ElementTypes 72 | isSelfClosing: boolean 73 | props: any[] 74 | children: TemplateChildNode[] 75 | } 76 | 77 | export interface PlainElementNode extends BaseElementNode { 78 | tagType: ElementTypes.ELEMENT 79 | codegenNode: VNodeCall | SimpleExpressionNode 80 | } 81 | 82 | export interface ComponentNode extends BaseElementNode { 83 | tagType: ElementTypes.COMPONENT 84 | codegenNode: VNodeCall 85 | } 86 | 87 | export interface TemplateNode extends BaseElementNode { 88 | tagType: ElementTypes.TEMPLATE 89 | codegenNode: undefined 90 | } 91 | 92 | export interface SlotOutletNode extends BaseElementNode { 93 | tagType: ElementTypes.SLOT 94 | codegenNode: any 95 | } 96 | 97 | export interface AttributeNode extends Node { 98 | type: NodeTypes.ATTRIBUTE 99 | name: string 100 | value: TextNode | undefined 101 | } 102 | 103 | // 根节点 104 | export interface RootNode extends Node { 105 | type: NodeTypes.ROOT 106 | children: TemplateChildNode[] 107 | codegenNode?: TemplateChildNode | JSChildNode 108 | helpers: symbol[] 109 | } 110 | 111 | // 父节点 112 | export type ParentNode = RootNode | ElementNode 113 | 114 | export type TemplateTextChildNode = 115 | | TextNode 116 | | InterpolationNode 117 | | CompoundExpressionNode 118 | 119 | export interface VNodeCall extends Node { 120 | type: NodeTypes.VNODE_CALL 121 | tag: string | symbol 122 | props: any 123 | children: 124 | | TemplateChildNode[] // multiple children 125 | | SimpleExpressionNode // hoisted 126 | | TemplateTextChildNode // single text child 127 | | undefined 128 | patchFlag: string | undefined 129 | isBlock: boolean 130 | isComponent: boolean 131 | } 132 | 133 | /** 134 | * 创建一个根AST 135 | * @param children 136 | */ 137 | export function createRoot(children: TemplateChildNode[]): RootNode { 138 | return { 139 | type: NodeTypes.ROOT, 140 | children, 141 | codegenNode: undefined, 142 | helpers: [], 143 | } 144 | } 145 | 146 | export function createVNodeCall( 147 | context: TransformContext | null, 148 | tag: VNodeCall['tag'], 149 | props?: VNodeCall['props'], 150 | children?: VNodeCall['children'], 151 | patchFlag?: VNodeCall['patchFlag'], 152 | isBlock: VNodeCall['isBlock'] = false, 153 | isComponent: VNodeCall['isComponent'] = false, 154 | ): VNodeCall { 155 | if (context) { 156 | if (isBlock) { 157 | context.helper(OPEN_BLOCK) 158 | context.helper(getVNodeBlockHelper(isComponent)) 159 | } 160 | else { 161 | context.helper(getVNodeHelper(isComponent)) 162 | } 163 | } 164 | 165 | return { 166 | type: NodeTypes.VNODE_CALL, 167 | tag, 168 | props, 169 | children, 170 | patchFlag, 171 | isBlock, 172 | isComponent, 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /packages/compiler-core/src/codegen.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isString, isSymbol } from 'shared/index' 2 | import type { CompoundExpressionNode, InterpolationNode, JSChildNode, RootNode, SimpleExpressionNode, TemplateChildNode, TextNode, VNodeCall } from './ast' 3 | import { NodeTypes } from './ast' 4 | import type { CodegenOptions } from './options' 5 | import { OPEN_BLOCK, TO_DISPLAY_STRING, helperNameMap } from './runtimeHelpers' 6 | import { getVNodeBlockHelper, getVNodeHelper } from './utils' 7 | 8 | type CodegenNode = TemplateChildNode | JSChildNode 9 | 10 | interface CodegenContext extends CodegenOptions { 11 | code: string 12 | push(code: string): void 13 | helper(key: symbol): string 14 | } 15 | 16 | export interface CodegenResult { 17 | code: string 18 | ast: RootNode 19 | } 20 | 21 | function createCodegenContext(): CodegenContext { 22 | const context = { 23 | code: '', 24 | helper(key) { 25 | return `_${helperNameMap[key]}` 26 | }, 27 | push(code) { 28 | context.code += code 29 | }, 30 | } 31 | 32 | return context 33 | } 34 | 35 | export function generate(ast: RootNode): CodegenResult { 36 | const context = createCodegenContext() 37 | const { 38 | push, 39 | } = context 40 | 41 | genFunctionPreamble(ast, context) 42 | 43 | const functionName = 'render' 44 | const args = ['_ctx', '_cache'] 45 | const signature = args.join(', ') 46 | push(` function ${functionName}(${signature}){`) 47 | 48 | push('return ') 49 | if (ast.codegenNode) 50 | genNode(ast.codegenNode, context) 51 | else 52 | push('null') 53 | 54 | push('}') 55 | 56 | return { 57 | code: context.code, 58 | ast, 59 | } 60 | } 61 | 62 | function genNode(node: CodegenNode | symbol | string, context: CodegenContext) { 63 | if (node === undefined) return 64 | if (isString(node)) { 65 | context.push(node) 66 | return 67 | } 68 | 69 | if (isSymbol(node)) { 70 | context.push(context.helper(node)) 71 | return 72 | } 73 | 74 | switch (node.type) { 75 | case NodeTypes.ELEMENT: 76 | genNode(node.codegenNode, context) 77 | break 78 | case NodeTypes.TEXT: 79 | genText(node, context) 80 | break 81 | case NodeTypes.SIMPLE_EXPRESSION: 82 | genExpression(node, context) 83 | break 84 | case NodeTypes.INTERPOLATION: 85 | genInterpolation(node, context) 86 | break 87 | case NodeTypes.COMPOUND_EXPRESSION: 88 | genCompoundExpression(node, context) 89 | break 90 | case NodeTypes.VNODE_CALL: 91 | genVNodeCall(node, context) 92 | break 93 | } 94 | } 95 | 96 | function genVNodeCall(node: VNodeCall, context: CodegenContext) { 97 | const { push, helper } = context 98 | const { tag, props, children, isBlock, isComponent } = node 99 | if (isBlock) 100 | push(`(${helper(OPEN_BLOCK)}(), `) 101 | 102 | const callHelper: symbol = isBlock ? getVNodeBlockHelper(isComponent) : getVNodeHelper(isComponent) 103 | push(`${helper(callHelper)}(`) 104 | genNodeList( 105 | genNullableArgs([tag, props, children]), 106 | context, 107 | ) 108 | push(')') 109 | if (isBlock) 110 | push(')') 111 | } 112 | 113 | function genNullableArgs(args: any[]) { 114 | let i = args.length 115 | while (i--) 116 | if (args[i] != null) break 117 | return args.slice(0, i + 1).map(arg => arg || 'null') 118 | } 119 | 120 | function genNodeList( 121 | nodes: (string | symbol | CodegenNode | TemplateChildNode[])[], 122 | context: CodegenContext, 123 | ) { 124 | const { push } = context 125 | for (let i = 0; i < nodes.length; i++) { 126 | const node = nodes[i] 127 | if (isString(node)) 128 | push(node) 129 | 130 | else if (isArray(node)) 131 | genNodeListAsArray(node, context) 132 | 133 | else 134 | genNode(node, context) 135 | 136 | if (i < nodes.length - 1) 137 | push(',') 138 | } 139 | } 140 | 141 | function genNodeListAsArray( 142 | nodes: (string | CodegenNode | TemplateChildNode[])[], 143 | context: CodegenContext, 144 | ) { 145 | context.push('[') 146 | genNodeList(nodes, context) 147 | context.push(']') 148 | } 149 | 150 | function genText( 151 | node: TextNode, 152 | context: CodegenContext, 153 | ) { 154 | context.push(JSON.stringify(node.content)) 155 | } 156 | 157 | function genExpression(node: SimpleExpressionNode, context: CodegenContext) { 158 | const { content } = node 159 | context.push(content) 160 | } 161 | function genInterpolation( 162 | node: InterpolationNode, 163 | context: CodegenContext, 164 | ) { 165 | const { push, helper } = context 166 | push(`${helper(TO_DISPLAY_STRING)}(`) 167 | genNode(node.content, context) 168 | push(')') 169 | } 170 | 171 | function genCompoundExpression( 172 | node: CompoundExpressionNode, 173 | context: CodegenContext, 174 | ) { 175 | for (let i = 0; i < node.children!.length; i++) { 176 | const child = node.children![i] 177 | if (isString(child)) 178 | context.push(child) 179 | else 180 | genNode(child, context) 181 | } 182 | } 183 | 184 | function genFunctionPreamble(ast: RootNode, context: CodegenContext) { 185 | const { 186 | push, 187 | } = context 188 | 189 | const vueBinding = 'Vue' 190 | const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}` 191 | 192 | if (ast.helpers.length > 0) 193 | push(`const { ${ast.helpers.map(aliasHelper).join(',')} } = ${vueBinding}\n`) 194 | 195 | push('return') 196 | } 197 | -------------------------------------------------------------------------------- /packages/compiler-core/src/compile.ts: -------------------------------------------------------------------------------- 1 | import { isString } from 'shared/index' 2 | import type { RootNode } from './ast' 3 | import { baseParse } from './parse' 4 | import { transform } from './transform' 5 | import type { CodegenResult } from './codegen' 6 | import { generate } from './codegen' 7 | import { transformText } from './transform/transformText' 8 | import { transformElement } from './transform/transformElement' 9 | import { transformExpression } from './transform/transformExpression' 10 | import type { CompilerOptions } from './options' 11 | 12 | export function baseCompile( 13 | template: string | RootNode, 14 | options: CompilerOptions = {}, 15 | ): CodegenResult { 16 | const ast = isString(template) ? baseParse(template, options) : template 17 | transform(ast, { 18 | nodeTransforms: [transformExpression, transformElement, transformText], 19 | }) 20 | return generate(ast) 21 | } 22 | -------------------------------------------------------------------------------- /packages/compiler-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compile' 2 | -------------------------------------------------------------------------------- /packages/compiler-core/src/options.ts: -------------------------------------------------------------------------------- 1 | import type { NodeTransform } from './transform' 2 | 3 | export interface TransformOptions { 4 | nodeTransforms?: NodeTransform[] 5 | } 6 | 7 | export interface CodegenOptions {} 8 | 9 | export interface ParserOptions { 10 | isNativeTag?: (tag: string) => boolean 11 | delimiters?: [string, string] 12 | } 13 | 14 | export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions 15 | -------------------------------------------------------------------------------- /packages/compiler-core/src/parse.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AttributeNode, 3 | ElementNode, 4 | InterpolationNode, 5 | RootNode, 6 | TemplateChildNode, 7 | TextNode, 8 | } from 'compiler-core/ast' 9 | import { 10 | ElementTypes, 11 | NodeTypes, 12 | createRoot, 13 | } from 'compiler-core/ast' 14 | import { isArray } from 'shared/index' 15 | import type { ParserOptions } from './options' 16 | 17 | // 默认配置 18 | export const defaultParserOptions: MergedParserOptions = { 19 | delimiters: ['{{', '}}'], // 插值分隔符 20 | } 21 | 22 | type OptionalOptions = 23 | | 'isNativeTag' 24 | type MergedParserOptions = 25 | Omit, OptionalOptions>& 26 | Pick 27 | 28 | export interface ParserContext { 29 | options: MergedParserOptions 30 | source: string 31 | } 32 | 33 | type AttributeValue = 34 | | { 35 | content: string 36 | } 37 | | undefined 38 | 39 | /** 40 | * 核心函数,解析模板字符串 41 | * @param content 42 | */ 43 | export function baseParse(content: string, options: ParserOptions = {}): RootNode { 44 | const context = createParserContext(content, options) // 创建一个解析上下文 45 | // 创建一个根AST 46 | return createRoot(parseChildren(context, []) /* 解析孩子内容 */) 47 | } 48 | 49 | /** 50 | * 初始化配置 51 | * @param content 52 | */ 53 | function createParserContext(content: string, rawOptions: ParserOptions): ParserContext { 54 | // 初始化配置 55 | const options = { ...rawOptions, ...defaultParserOptions } 56 | return { 57 | options, 58 | source: content, // 将模板字符串保存在source里 59 | } 60 | } 61 | 62 | /** 63 | * 解析孩子模板内容 64 | * @param context 65 | * @param ancestors 66 | */ 67 | function parseChildren(context: ParserContext, ancestors: ElementNode[]): TemplateChildNode[] { 68 | // 初始化节点容器 69 | const nodes: TemplateChildNode[] = [] 70 | 71 | // 遍历模板模板内容(每解析完一部分内容,就会将其在source中删除) 72 | while (!isEnd(context, ancestors)) { 73 | // 获取剩余的模板字符串 74 | const s = context.source 75 | // 初始化节点 76 | let node: TemplateChildNode | TemplateChildNode[] | undefined 77 | 78 | if (startsWith(s, context.options.delimiters[0])) { 79 | // 插值节点 80 | node = parseInterpolation(context) 81 | } 82 | else if (s[0] === '<') { 83 | // 解析元素节点 84 | if (/[a-z]/i.test(s[1])) 85 | node = parseElement(context, ancestors) 86 | } 87 | 88 | // 解析文本 89 | if (!node) 90 | node = parseText(context) 91 | 92 | // 将这部分解析完的节点插入容器中 93 | if (isArray(node)) { 94 | for (let i = 0; i < node.length; i++) 95 | nodes.push(node[i]) 96 | } 97 | else { 98 | nodes.push(node!) 99 | } 100 | } 101 | 102 | return nodes 103 | } 104 | 105 | /** 106 | * 解析插值 107 | * @param context 108 | */ 109 | function parseInterpolation(context: ParserContext): InterpolationNode | undefined { 110 | // open -> '{{' close -> '}}' 111 | const [open, close] = context.options.delimiters 112 | // 获取插值结束下标 113 | const closeIndex = context.source.indexOf(close, open.length) 114 | // 没有插值情况 115 | if (closeIndex === -1) return undefined 116 | 117 | // 先删除左分隔符 118 | advanceBy(context, open.length) 119 | // 获取内容长度,包括空格 120 | const rawContentLength = closeIndex - open.length 121 | // 获取内容 122 | const rawContent = parseTextData(context, rawContentLength) 123 | // 去除空格 124 | const content = rawContent.trim() 125 | // 删除剩余部分内容 126 | advanceBy(context, close.length) 127 | 128 | return { 129 | type: NodeTypes.INTERPOLATION, 130 | content: { 131 | type: NodeTypes.SIMPLE_EXPRESSION, 132 | content, 133 | }, 134 | } 135 | } 136 | 137 | // 标签类型 138 | const enum TagType { 139 | Start, 140 | End, 141 | } 142 | 143 | /** 144 | * 解析元素节点 145 | * @param context 146 | * @param ancestors 147 | */ 148 | function parseElement(context: ParserContext, ancestors: ElementNode[]): ElementNode | undefined { 149 | // 解析开始标签 150 | const element = parseTag(context, TagType.Start)! 151 | 152 | // 如果是单标签(自闭合),直接返回 153 | if (element.isSelfClosing) 154 | return element 155 | 156 | // 递归解析孩子标签 157 | ancestors.push(element) 158 | element.children = parseChildren(context, ancestors) 159 | ancestors.pop() 160 | 161 | // 解析闭合标签 162 | if (startsWithEndTagOpen(context.source, element.tag)) 163 | parseTag(context, TagType.End) 164 | 165 | return element 166 | } 167 | 168 | /** 169 | * 解析标签 170 | * @param context 171 | * @param type 172 | */ 173 | function parseTag(context: ParserContext, type: TagType): ElementNode | undefined { 174 | // 正则识别 175 | const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)! 176 | const tag = match[1] 177 | 178 | // 删除标签文本 179 | advanceBy(context, match[0].length) 180 | // 删除空格 181 | advanceSpaces(context) 182 | 183 | // 解析属性 184 | const props = parseAttributes(context, type) 185 | 186 | // 判断是否为单标签(自闭合) 187 | const isSelfClosing = startsWith(context.source, '/>') 188 | advanceBy(context, isSelfClosing ? 2 : 1) 189 | 190 | // 如果是结束标签,无需返回节点 191 | if (type === TagType.End) 192 | return 193 | 194 | // 判断标签类型 195 | let tagType = ElementTypes.ELEMENT 196 | if (tag === 'slot') 197 | tagType = ElementTypes.SLOT 198 | else if (tag === 'template') 199 | tagType = ElementTypes.TEMPLATE 200 | else if (isComponent(tag, props, context)) 201 | tagType = ElementTypes.COMPONENT 202 | 203 | return { 204 | type: NodeTypes.ELEMENT, 205 | tag, 206 | tagType, 207 | props, 208 | isSelfClosing, 209 | children: [], 210 | codegenNode: undefined, 211 | } as ElementNode 212 | } 213 | 214 | function isComponent(tag: string, props: AttributeNode[], context: ParserContext) { 215 | const options = context.options 216 | if (tag === 'component' || /^[A-Z]/.test(tag) || (options.isNativeTag && !options.isNativeTag(tag))) 217 | return true 218 | 219 | for (let i = 0; i < props.length; i++) { 220 | const p = props[i] 221 | if (p.type === NodeTypes.ATTRIBUTE) { 222 | if (p.name === 'is' && p.value) 223 | return true 224 | } 225 | } 226 | } 227 | 228 | /** 229 | * 解析元素属性 230 | * @param context 231 | * @param type 232 | */ 233 | function parseAttributes(context: ParserContext, type: TagType): AttributeNode[] { 234 | const props: AttributeNode[] = [] 235 | 236 | // 遍历 237 | while (context.source.length > 0 && !startsWith(context.source, '>') && !startsWith(context.source, '/>')) { 238 | // 解析单个属性 239 | const attr = parseAttribute(context) 240 | 241 | // 插入props变量中 242 | if (type === TagType.Start) 243 | props.push(attr) 244 | 245 | // 删除空格 246 | advanceSpaces(context) 247 | } 248 | 249 | return props 250 | } 251 | 252 | /** 253 | * 解析单个属性 254 | * @param context 255 | */ 256 | function parseAttribute(context: ParserContext): AttributeNode { 257 | const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)! 258 | const name = match[0] 259 | 260 | // 删除属性名 261 | advanceBy(context, name.length) 262 | 263 | let value: any 264 | 265 | if (/^[\t\r\n\f ]*=/.test(context.source)) { 266 | // 删除等于号 267 | advanceBy(context, 1) 268 | // 解析属性值 269 | value = parseAttributeValue(context) 270 | } 271 | return { 272 | type: NodeTypes.ATTRIBUTE, 273 | name, 274 | value: value && { 275 | type: NodeTypes.TEXT, 276 | content: value.content, 277 | }, 278 | } 279 | } 280 | 281 | /** 282 | * 解析属性值 283 | * @param context 284 | */ 285 | function parseAttributeValue(context: ParserContext): AttributeValue { 286 | let content: string 287 | // 判断属性是否有引号 288 | const quote = context.source[0] 289 | const isQuoted = quote === '"' || quote === '\'' 290 | if (isQuoted) { 291 | // 删除引号 292 | advanceBy(context, 1) 293 | 294 | // 定位下一个引号,获取内容 295 | const endIndex = context.source.indexOf(quote) 296 | 297 | if (endIndex === -1) { 298 | content = parseTextData(context, context.source.length) 299 | } 300 | else { 301 | // 获取内容 302 | content = parseTextData(context, endIndex) 303 | // 删除结束引号 304 | advanceBy(context, 1) 305 | } 306 | } 307 | else { 308 | // 属性值没有加引号的情况 309 | const match = /^[^\t\r\n\f >]+/.exec(context.source) 310 | if (!match) 311 | return undefined 312 | 313 | // 获取内容 314 | content = parseTextData(context, match[0].length) 315 | } 316 | 317 | return { 318 | content, 319 | } 320 | } 321 | 322 | /** 323 | * 解析文本 324 | * @param context 325 | */ 326 | function parseText(context: ParserContext): TextNode { 327 | // 解析到'<'或'{{' 328 | const endTokens = ['<', context.options.delimiters[0]] 329 | 330 | let endIndex = context.source.length 331 | for (let i = 0; i < endTokens.length; i++) { 332 | const index = context.source.indexOf(endTokens[i], 1) 333 | if (index !== -1 && endIndex > index) 334 | endIndex = index 335 | } 336 | 337 | // 解析出文本 338 | const content = parseTextData(context, endIndex) 339 | 340 | return { 341 | type: NodeTypes.TEXT, 342 | content, 343 | } 344 | } 345 | 346 | function startsWith(source: string, searchString: string): boolean { 347 | return source.startsWith(searchString) 348 | } 349 | 350 | /** 351 | * 删除已经解析的字符串 352 | * @param context 353 | * @param numberOfCharacters 354 | */ 355 | function advanceBy(context: ParserContext, numberOfCharacters: number): void { 356 | const { source } = context 357 | context.source = source.slice(numberOfCharacters) 358 | } 359 | 360 | /** 361 | * 判断是否解析结束 362 | * @param context 363 | * @param ancestors 364 | */ 365 | function isEnd(context: ParserContext, ancestors: ElementNode[]): boolean { 366 | const s = context.source 367 | 368 | // 遍历祖先标签,判断是否有未闭合标签 369 | if (startsWith(s, '= 0; i--) { 371 | if (startsWithEndTagOpen(s, ancestors[i].tag)) 372 | return true 373 | } 374 | } 375 | 376 | return !s 377 | } 378 | 379 | /** 380 | * 结束标签 381 | * @param source 382 | * @param tag 383 | */ 384 | function startsWithEndTagOpen(source: string, tag: string): boolean { 385 | return ( 386 | startsWith(source, ']/.test(source[2 + tag.length] || '>') 389 | ) 390 | } 391 | 392 | /** 393 | * 解析文本内容 394 | * @param context 395 | * @param length 396 | */ 397 | function parseTextData(context: ParserContext, length: number) { 398 | // 截取文本原内容 399 | const rawText = context.source.slice(0, length) 400 | // 删除数据 401 | advanceBy(context, length) 402 | return rawText 403 | } 404 | 405 | /** 406 | * 删除空格 407 | * @param context 408 | */ 409 | function advanceSpaces(context: ParserContext): void { 410 | const match = /^[\t\r\n\f ]+/.exec(context.source) 411 | if (match) 412 | advanceBy(context, match[0].length) 413 | } 414 | -------------------------------------------------------------------------------- /packages/compiler-core/src/runtimeHelpers.ts: -------------------------------------------------------------------------------- 1 | export const FRAGMENT = Symbol('Fragment') 2 | export const OPEN_BLOCK = Symbol('OPEN_BLOCK') 3 | export const CREATE_BLOCK = Symbol('CREATE_BLOCK') 4 | export const CREATE_ELEMENT_BLOCK = Symbol('CREATE_ELEMENT_BLOCK') 5 | export const CREATE_VNODE = Symbol('CREATE_VNODE') 6 | export const CREATE_ELEMENT_VNODE = Symbol('CREATE_ELEMENT_VNODE') 7 | export const TO_DISPLAY_STRING = Symbol('toDisplayString') 8 | 9 | export const helperNameMap: any = { 10 | [FRAGMENT]: 'Fragment', 11 | [OPEN_BLOCK]: 'openBlock', 12 | [CREATE_BLOCK]: 'createBlock', 13 | [CREATE_ELEMENT_BLOCK]: 'createElementBlock', 14 | [CREATE_VNODE]: 'createVNode', 15 | [CREATE_ELEMENT_VNODE]: 'createElementVNode', 16 | [TO_DISPLAY_STRING]: 'toDisplayString', 17 | } 18 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transform.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from 'shared/index' 2 | import type { ParentNode, RootNode, TemplateChildNode } from './ast' 3 | import { NodeTypes, createVNodeCall } from './ast' 4 | import type { TransformOptions } from './options' 5 | import { FRAGMENT, TO_DISPLAY_STRING } from './runtimeHelpers' 6 | import { isSingleElementRoot } from './transform/hoistStatic' 7 | 8 | export interface TransformContext { 9 | nodeTransforms: NodeTransform[] 10 | helpers: Map 11 | helper(name: T): T 12 | } 13 | 14 | export type NodeTransform = ( 15 | node: RootNode | TemplateChildNode, 16 | context: TransformContext 17 | ) => void | (() => void) | (() => void)[] 18 | 19 | /** 20 | * 初始化上下文 21 | * @param root 22 | * @param options 23 | */ 24 | function createTransformContext( 25 | root: RootNode, { 26 | nodeTransforms = [], 27 | }: TransformOptions): TransformContext { 28 | const context = { 29 | nodeTransforms, 30 | helpers: new Map(), 31 | helper(name) { 32 | const count = context.helpers.get(name) || 0 33 | context.helpers.set(name, count + 1) 34 | return name 35 | }, 36 | } 37 | return context 38 | } 39 | 40 | /** 41 | * 转义AST树 42 | * @param root 43 | * @param options 44 | */ 45 | export function transform(root: RootNode, options: TransformOptions) { 46 | // 创建上下文 47 | const context = createTransformContext(root, options) 48 | // 递归遍历节点 49 | traverseNode(root, context) 50 | 51 | createRootCodegen(root, context) 52 | 53 | root.helpers = [...context.helpers.keys()] 54 | } 55 | 56 | function createRootCodegen(root: RootNode, context: TransformContext) { 57 | const { helper } = context 58 | const { children } = root 59 | if (children.length === 1) { 60 | const child = children[0] 61 | if (isSingleElementRoot(root, child) && child.codegenNode) 62 | root.codegenNode = child.codegenNode 63 | else 64 | root.codegenNode = child 65 | } 66 | else if (children.length > 1) { 67 | root.codegenNode = createVNodeCall( 68 | context, 69 | helper(FRAGMENT), 70 | undefined, 71 | root.children, 72 | ) 73 | } 74 | } 75 | 76 | /** 77 | * 递归遍历节点 78 | * @param root 79 | * @param context 80 | */ 81 | function traverseNode(root: RootNode | TemplateChildNode, context: TransformContext) { 82 | // 获取处理节点插件 83 | const { nodeTransforms } = context 84 | 85 | const exitFns: (() => void)[] = [] 86 | // 遍历插件,一一执行 87 | for (let i = 0; i < nodeTransforms.length; i++) { 88 | const transform = nodeTransforms[i] 89 | const onExit = transform(root, context) 90 | if (onExit) { 91 | if (isArray(onExit)) 92 | exitFns.push(...onExit) 93 | else 94 | exitFns.push(onExit) 95 | } 96 | } 97 | 98 | // 判断节点类型,分别处理 99 | switch (root.type) { 100 | case NodeTypes.INTERPOLATION: 101 | context.helper(TO_DISPLAY_STRING) 102 | break 103 | case NodeTypes.ELEMENT: 104 | case NodeTypes.ROOT: 105 | // 遍历子节点 106 | traverseChildren(root, context) 107 | } 108 | 109 | let i = exitFns.length 110 | while (i--) 111 | exitFns[i]() 112 | } 113 | 114 | /** 115 | * 遍历递归子节点 116 | * @param parent 117 | * @param context 118 | */ 119 | function traverseChildren( 120 | parent: ParentNode, 121 | context: TransformContext, 122 | ) { 123 | // 遍历所有子节点,一一进行递归 124 | for (let i = 0; i < parent.children.length; i++) 125 | traverseNode(parent.children[i], context) 126 | } 127 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transform/hoistStatic.ts: -------------------------------------------------------------------------------- 1 | import { isSlotOutlet } from 'compiler-core/utils' 2 | import type { ComponentNode, PlainElementNode, RootNode, TemplateChildNode, TemplateNode } from '../ast' 3 | import { NodeTypes } from '../ast' 4 | 5 | export function isSingleElementRoot( 6 | root: RootNode, 7 | child: TemplateChildNode, 8 | ): child is PlainElementNode | ComponentNode | TemplateNode { 9 | const { children } = root 10 | return ( 11 | children.length === 1 12 | && child.type === NodeTypes.ELEMENT 13 | && !isSlotOutlet(child) 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transform/transformElement.ts: -------------------------------------------------------------------------------- 1 | import type { TemplateTextChildNode, VNodeCall } from 'compiler-core/ast' 2 | import { NodeTypes, createVNodeCall } from 'compiler-core/ast' 3 | import type { NodeTransform } from 'compiler-core/transform' 4 | import { CREATE_ELEMENT_VNODE } from './../runtimeHelpers' 5 | 6 | export const transformElement: NodeTransform = (node, context) => { 7 | if (node.type === NodeTypes.ELEMENT) { 8 | return () => { 9 | context.helper(CREATE_ELEMENT_VNODE) 10 | 11 | const { tag, props } = node 12 | let vnodeChildren: VNodeCall['children'] 13 | // tag 14 | const vnodeTag = `'${tag}'` 15 | 16 | // props 17 | const vnodeProps: VNodeCall['props'] = props 18 | 19 | // children 20 | const children = node.children 21 | if (children.length > 0) { 22 | const child = children[0] 23 | const type = child.type 24 | 25 | if (type === NodeTypes.TEXT) 26 | vnodeChildren = child as TemplateTextChildNode 27 | else 28 | vnodeChildren = children 29 | } 30 | 31 | node.codegenNode = createVNodeCall( 32 | context, 33 | vnodeTag, 34 | vnodeProps, 35 | vnodeChildren, 36 | ) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transform/transformExpression.ts: -------------------------------------------------------------------------------- 1 | import type { SimpleExpressionNode } from 'compiler-core/ast' 2 | import { NodeTypes } from 'compiler-core/ast' 3 | import type { NodeTransform } from 'compiler-core/transform' 4 | import type { ExpressionNode } from './../ast' 5 | 6 | export const transformExpression: NodeTransform = (node) => { 7 | if (node.type === NodeTypes.INTERPOLATION) { 8 | node.content = processExpression( 9 | node.content as SimpleExpressionNode, 10 | ) 11 | } 12 | } 13 | 14 | function processExpression( 15 | node: SimpleExpressionNode, 16 | ): ExpressionNode { 17 | node.content = `_ctx.${node.content}` 18 | return node 19 | } 20 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transform/transformText.ts: -------------------------------------------------------------------------------- 1 | import type { CompoundExpressionNode } from 'compiler-core/ast' 2 | import { NodeTypes } from 'compiler-core/ast' 3 | import type { NodeTransform } from 'compiler-core/transform' 4 | import { isText } from 'compiler-core/utils' 5 | 6 | export const transformText: NodeTransform = (node) => { 7 | if (node.type === NodeTypes.ELEMENT) { 8 | return () => { 9 | const { children } = node 10 | let currentContainer: CompoundExpressionNode | undefined 11 | 12 | for (let i = 0; i < children.length; i++) { 13 | const child = children[i] 14 | 15 | if (isText(child)) { 16 | for (let j = i + 1; j < children.length; j++) { 17 | const next = children[j] 18 | if (isText(next)) { 19 | if (!currentContainer) { 20 | currentContainer = children[i] = { 21 | type: NodeTypes.COMPOUND_EXPRESSION, 22 | children: [child], 23 | } 24 | } 25 | currentContainer.children.push(' + ', next) 26 | children.splice(j, 1) 27 | j-- 28 | } 29 | else { 30 | currentContainer = undefined 31 | break 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/compiler-core/src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { InterpolationNode, RootNode, SlotOutletNode, TemplateChildNode, TextNode } from './ast' 2 | import { ElementTypes, NodeTypes } from './ast' 3 | import { CREATE_BLOCK, CREATE_ELEMENT_BLOCK, CREATE_ELEMENT_VNODE, CREATE_VNODE } from './runtimeHelpers' 4 | 5 | export function isSlotOutlet( 6 | node: RootNode | TemplateChildNode, 7 | ): node is SlotOutletNode { 8 | return node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT 9 | } 10 | 11 | export function getVNodeHelper(isComponent: boolean) { 12 | return isComponent ? CREATE_VNODE : CREATE_ELEMENT_VNODE 13 | } 14 | 15 | export function getVNodeBlockHelper(isComponent: boolean) { 16 | return isComponent ? CREATE_BLOCK : CREATE_ELEMENT_BLOCK 17 | } 18 | 19 | export function isText( 20 | node: TemplateChildNode, 21 | ): node is TextNode | InterpolationNode { 22 | return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT 23 | } 24 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from '../src/reactive' 2 | import { computed } from '../src/computed' 3 | import { isRef, ref } from '../src/ref' 4 | 5 | describe('computed', () => { 6 | it('happy path', () => { 7 | const user = reactive({ 8 | age: 1, 9 | }) 10 | 11 | const age = computed(() => { 12 | return user.age 13 | }) 14 | 15 | expect(age.value).toBe(1) 16 | expect(isRef(age)).toBe(true) 17 | }) 18 | 19 | it('should compute lazily', () => { 20 | const value = reactive({ 21 | foo: 1, 22 | }) 23 | const getter = jest.fn(() => { 24 | return value.foo 25 | }) 26 | const cValue = computed(getter) 27 | 28 | // lazy 29 | expect(getter).not.toHaveBeenCalled() 30 | 31 | expect(cValue.value).toBe(1) 32 | expect(getter).toHaveBeenCalledTimes(1) 33 | 34 | expect(cValue.value).toBe(1) 35 | expect(getter).toHaveBeenCalledTimes(1) 36 | 37 | value.foo = 2 38 | expect(getter).toHaveBeenCalledTimes(1) 39 | expect(cValue.value).toBe(2) 40 | expect(getter).toHaveBeenCalledTimes(2) 41 | }) 42 | 43 | it('should support setter', () => { 44 | const n = ref(1) 45 | const plusOne = computed({ 46 | get: () => n.value + 1, 47 | set: (val) => { 48 | n.value = val - 1 49 | }, 50 | }) 51 | 52 | expect(plusOne.value).toBe(2) 53 | n.value++ 54 | expect(plusOne.value).toBe(3) 55 | 56 | plusOne.value = 0 57 | expect(n.value).toBe(-1) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from '../src/reactive' 2 | import { effect, stop } from '../src/effect' 3 | 4 | describe('effect', () => { 5 | it('happy path', () => { 6 | const user = reactive({ age: 10 }) 7 | let nextAge 8 | 9 | effect(() => { 10 | nextAge = user.age + 1 11 | }) 12 | 13 | expect(nextAge).toBe(11) 14 | 15 | // update 16 | user.age++ 17 | expect(nextAge).toBe(12) 18 | }) 19 | 20 | it('should return runner when call effect', () => { 21 | let foo = 10 22 | const runner = effect(() => { 23 | foo++ 24 | return 'foo' 25 | }) 26 | 27 | expect(foo).toBe(11) 28 | 29 | const r = runner() 30 | expect(foo).toBe(12) 31 | expect(r).toBe('foo') 32 | }) 33 | 34 | it('effect scheduler option', () => { 35 | // 当effect第一次执行的时候,会执行fn函数,但不会执行scheduler调度函数 36 | // 当响应式对象被set的时候,effect update的时候不会执行fn函数,而执行scheduler 37 | // 而当执行runner时,会再出执行fn 38 | 39 | let dummy 40 | let run: any 41 | const scheduler = jest.fn(() => { 42 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 43 | run = runner 44 | }) 45 | 46 | const obj = reactive({ foo: 1 }) 47 | const runner = effect( 48 | () => { 49 | dummy = obj.foo 50 | }, 51 | { 52 | scheduler, 53 | }, 54 | ) 55 | 56 | // scheduler不会被调用 57 | expect(scheduler).not.toHaveBeenCalled() 58 | expect(dummy).toBe(1) 59 | 60 | obj.foo++ 61 | // scheduler被调用了一次 62 | expect(scheduler).toHaveBeenCalledTimes(1) 63 | expect(dummy).toBe(1) 64 | run() 65 | expect(dummy).toBe(2) 66 | }) 67 | 68 | it('stop function', () => { 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 | 78 | // 直接赋值只会触发get操作 79 | obj.prop = 3 80 | expect(dummy).toBe(2) 81 | 82 | // obj.prop++ -> obj.prop = obj.prop + 1 83 | // 会触发一次get操作 84 | obj.prop++ 85 | expect(dummy).toBe(2) 86 | 87 | runner() 88 | expect(dummy).toBe(4) 89 | }) 90 | 91 | it('onStop Option', () => { 92 | const obj = reactive({ 93 | foo: 1, 94 | }) 95 | const onStop = jest.fn() 96 | let dummy 97 | const runner = effect( 98 | () => { 99 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 100 | dummy = obj.foo 101 | }, 102 | { onStop }, 103 | ) 104 | 105 | stop(runner) 106 | expect(onStop).toBeCalledTimes(1) 107 | }) 108 | }) 109 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | it('init', () => { 2 | expect(true).toBe(true) 3 | }) 4 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReactive, reactive } from '../src/reactive' 2 | 3 | describe('reactive', () => { 4 | it('happy path', () => { 5 | const original = { foo: 1 } 6 | const observed = reactive(original) 7 | 8 | expect(observed).not.toBe(original) 9 | expect(observed.foo).toBe(1) 10 | expect(isReactive(observed)).toBe(true) 11 | expect(isReactive(original)).toBe(false) 12 | expect(isProxy(observed)).toBe(true) 13 | expect(isProxy(original)).toBe(false) 14 | }) 15 | 16 | it('nested reactive', () => { 17 | const original = { 18 | nested: { 19 | foo: 1, 20 | }, 21 | array: [{ bar: 2 }], 22 | } 23 | const observed = reactive(original) 24 | expect(isReactive(observed)).toBe(true) 25 | expect(isReactive(observed.nested)).toBe(true) 26 | expect(isReactive(observed.array)).toBe(true) 27 | expect(isReactive(observed.array[0])).toBe(true) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReadonly, readonly } from '../src/reactive' 2 | 3 | describe('readonly', () => { 4 | it('happy path', () => { 5 | const original = { foo: 1, bar: { baz: 2 } } 6 | const wrapped = readonly(original) 7 | expect(wrapped).not.toBe(original) 8 | expect(isReadonly(wrapped)).toBe(true) 9 | expect(isReadonly(wrapped.bar)).toBe(true) 10 | expect(isProxy(wrapped)).toBe(true) 11 | expect(isProxy(wrapped.bar)).toBe(true) 12 | 13 | expect(isReadonly(original)).toBe(false) 14 | expect(wrapped.foo).toBe(1) 15 | }) 16 | 17 | it('should call warn when set', () => { 18 | console.warn = jest.fn() 19 | 20 | const user = readonly({ age: 10 }) 21 | user.age = 11 22 | expect(console.warn).toBeCalled() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '../src/effect' 2 | import { isRef, proxyRefs, ref, unref } from '../src/ref' 3 | import { reactive } from '../src/reactive' 4 | 5 | describe('ref', () => { 6 | it('happy path', () => { 7 | const a = ref(1) 8 | expect(a.value).toBe(1) 9 | }) 10 | 11 | it('should be reactive', () => { 12 | const a = ref(1) 13 | let dummy 14 | let calls = 0 15 | effect(() => { 16 | calls++ 17 | dummy = a.value 18 | }) 19 | 20 | expect(calls).toBe(1) 21 | expect(dummy).toBe(1) 22 | 23 | a.value = 2 24 | expect(calls).toBe(2) 25 | expect(dummy).toBe(2) 26 | 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 | 41 | expect(dummy).toBe(1) 42 | a.value.count = 2 43 | expect(dummy).toBe(2) 44 | }) 45 | 46 | it('isRef', () => { 47 | const a = ref(1) 48 | const user = reactive({ 49 | age: 1, 50 | }) 51 | expect(isRef(a)).toBe(true) 52 | expect(isRef(1)).toBe(false) 53 | expect(isRef(user)).toBe(false) 54 | }) 55 | 56 | it('unRef', () => { 57 | const a = ref(1) 58 | 59 | expect(unref(a)).toBe(1) 60 | expect(unref(1)).toBe(1) 61 | }) 62 | 63 | it('proxyRefs', () => { 64 | const user = { 65 | age: ref(10), 66 | name: 'ouduidui', 67 | } 68 | const proxyUser = proxyRefs(user) 69 | expect(user.age.value).toBe(10) 70 | expect(proxyUser.age).toBe(10) 71 | expect(proxyUser.name).toBe('ouduidui') 72 | 73 | proxyUser.age = 20 74 | expect(proxyUser.age).toBe(20) 75 | expect(user.age.value).toBe(20) 76 | 77 | proxyUser.age = ref(10) 78 | expect(proxyUser.age).toBe(10) 79 | expect(user.age.value).toBe(10) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/shallowReactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, shallowReactive } from '../src/reactive' 2 | 3 | describe('shallowReactive', () => { 4 | it('should not make non-reactive properties reactive', () => { 5 | const props = shallowReactive({ n: { foo: 1 } }) 6 | expect(isReactive(props)).toBe(true) 7 | expect(isReactive(props.n)).toBe(false) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReadonly, readonly, shallowReadonly } from '../src/reactive' 2 | 3 | describe('shallowReadonly', () => { 4 | it('should not make non-readonly properties readonly', () => { 5 | const props = shallowReadonly({ n: { foo: 1 } }) 6 | expect(isReadonly(props)).toBe(true) 7 | expect(isReadonly(props.n)).toBe(false) 8 | }) 9 | 10 | it('should call warn when set', () => { 11 | console.warn = jest.fn() 12 | 13 | const user = readonly({ age: 10 }) 14 | user.age = 11 15 | expect(console.warn).toBeCalled() 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/reactivity/src/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import { extend, isObject } from 'shared/index' 2 | import { track, trigger } from './effect' 3 | import { ReactiveFlags, reactive, readonly } from './reactive' 4 | 5 | const get = createGetter() // 响应式 6 | const shallowGet = createGetter(false, true) // 浅响应式 7 | const readonlyGet = createGetter(true) // 只读 8 | const shallowReadonlyGet = createGetter(true, true) // 浅只读 9 | 10 | /** 11 | * 创建Proxy的get处理函数 12 | * @param isReadonly {boolean} 是否只读 13 | * @param shallow {boolean} 是否浅处理 14 | */ 15 | function createGetter(isReadonly = false, shallow = false) { 16 | return function get(target, key) { 17 | // 用于isReactive方法,判断是否为reactive 18 | if (key === ReactiveFlags.IS_REACTIVE) 19 | return !isReadonly 20 | 21 | if (key === ReactiveFlags.IS_READONLY) 22 | return isReadonly 23 | 24 | // 获取对应结果 25 | const res = Reflect.get(target, key) 26 | 27 | if (!isReadonly) { 28 | // 只读情况下不需要依赖收集 29 | track(target, key) // 依赖收集 30 | } 31 | 32 | // 浅处理无需只想下列的递归处理 33 | if (shallow) 34 | return res 35 | 36 | // 如果res是对象的话,再次进行处理 37 | if (isObject(res)) 38 | return isReadonly ? readonly(res) : reactive(res) 39 | 40 | // 将结果返回出去 41 | return res 42 | } 43 | } 44 | 45 | const set = createSetter() // 响应式 46 | 47 | /** 48 | * 创建Proxy的set处理函数 49 | */ 50 | function createSetter() { 51 | return function set(target, key, value) { 52 | // 执行set操作,并获取新的value值 53 | const res = Reflect.set(target, key, value) 54 | 55 | // 触发依赖 56 | trigger(target, key) 57 | 58 | // 将结果返回 59 | return res 60 | } 61 | } 62 | 63 | // 可变的Proxy的handler 64 | export const mutableHandlers: ProxyHandler = { 65 | get, // 拦截获取操作 66 | set, // 拦截设置操作 67 | } 68 | 69 | // 浅处理响应式的handler 70 | export const shallowReactiveHandlers = extend({}, mutableHandlers, { get: shallowGet }) 71 | 72 | // 只读的handler 73 | export const readonlyHandlers: ProxyHandler = { 74 | get: readonlyGet, 75 | set(target, key) { 76 | // 当被设置时发出警告 77 | console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target) 78 | return true 79 | }, 80 | } 81 | 82 | // 浅处理只读的handler 83 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { get: shallowReadonlyGet }) 84 | -------------------------------------------------------------------------------- /packages/reactivity/src/computed.ts: -------------------------------------------------------------------------------- 1 | import { NOOP, isFunction } from 'shared/index' 2 | import { ReactiveEffect } from './effect' 3 | 4 | export type ComputedGetter = (...args: any[]) => T 5 | export type ComputedSetter = (v: T) => void 6 | 7 | export interface WritableComputedOptions { 8 | get: ComputedGetter 9 | set: ComputedSetter 10 | } 11 | 12 | class ComputedRefImpl { 13 | private readonly _setter 14 | public readonly effect: ReactiveEffect 15 | private _dirty = true // 为true的话代表需要更新数据 16 | private _value!: T // 保存缓存值 17 | public readonly __v_isRef = true 18 | 19 | constructor(getter: ComputedGetter, setter: ComputedSetter) { 20 | this._setter = setter 21 | 22 | // 新建ReactiveEffect示例,并且配置scheduler函数,避免响应式数据更新时调用run,从而实现computed缓存特性 23 | this.effect = new ReactiveEffect(getter, () => { 24 | // 将_dirty设置为true,代表下次调用computed值时需要更新数据 25 | if (!this._dirty) 26 | this._dirty = true 27 | }) 28 | } 29 | 30 | get value() { 31 | if (this._dirty) { 32 | // 更新value 33 | this._dirty = false 34 | this._value = this.effect.run() 35 | } 36 | return this._value 37 | } 38 | 39 | set value(newValue: T) { 40 | this._setter(newValue) 41 | } 42 | } 43 | 44 | /** 45 | * 计算属性 46 | * @param getterOrOptions 47 | */ 48 | export function computed(getterOrOptions: ComputedGetter | WritableComputedOptions) { 49 | let getter: ComputedGetter 50 | let setter: ComputedSetter 51 | 52 | // 判断getterOrOptions是getter还是options 53 | const onlyGetter = isFunction(getterOrOptions) 54 | if (onlyGetter) { 55 | getter = getterOrOptions as ComputedGetter 56 | setter = NOOP 57 | } 58 | else { 59 | getter = (getterOrOptions as WritableComputedOptions).get 60 | setter = (getterOrOptions as WritableComputedOptions).set 61 | } 62 | 63 | return new ComputedRefImpl(getter, setter) 64 | } 65 | -------------------------------------------------------------------------------- /packages/reactivity/src/dep.ts: -------------------------------------------------------------------------------- 1 | import type { ReactiveEffect } from './effect' 2 | 3 | export type Dep = Set 4 | 5 | /** 6 | * 创建一个依赖收集容器Dep 7 | * @param effects 8 | */ 9 | export const createDep = (effects?: ReactiveEffect[]): Dep => { 10 | return new Set(effects) as Dep 11 | } 12 | -------------------------------------------------------------------------------- /packages/reactivity/src/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from 'shared/index' 2 | import type { Dep } from './dep' 3 | import { createDep } from './dep' 4 | 5 | // 存储正在被收集依赖的ReactiveEffect实例 6 | let activeEffect: ReactiveEffect | undefined 7 | 8 | // 判断是否依赖收集 9 | let shouldTrack = true 10 | // 存放修改状态前 shouldTrack 状态 11 | const trackStack: boolean[] = [] 12 | 13 | export type EffectScheduler = (...args: any[]) => any 14 | 15 | /** 16 | * 暂停依赖收集 17 | */ 18 | export function pauseTrack() { 19 | trackStack.push(shouldTrack) 20 | shouldTrack = false 21 | } 22 | 23 | /** 24 | * 重置依赖收集 25 | */ 26 | export function resetTracking() { 27 | const last = trackStack.pop() 28 | shouldTrack = last === undefined ? true : last 29 | } 30 | 31 | export class ReactiveEffect { 32 | public fn: () => T 33 | public scheduler: EffectScheduler | null = null 34 | // 存储那些收集到该effect的dep 35 | public deps: Dep[] = [] 36 | active = true 37 | onStop?: () => void 38 | 39 | constructor(fn, scheduler: EffectScheduler | null = null) { 40 | this.fn = fn // 保存fn 41 | this.scheduler = scheduler 42 | } 43 | 44 | run() { 45 | // 当active为false时,即已经取消响应式监听,则无需再进行依赖收集 46 | if (!this.active) 47 | return this.fn() // 执行fn函数,并将结果返回 48 | 49 | activeEffect = this // 将实例赋值给activeEffect,用于依赖收集 50 | shouldTrack = true 51 | const res = this.fn() 52 | shouldTrack = false 53 | activeEffect = undefined 54 | return res 55 | } 56 | 57 | stop() { 58 | if (this.active) { 59 | // 从依赖中将该effect删除 60 | cleanupEffect(this) 61 | // 执行onStop函数 62 | if (this.onStop) 63 | this.onStop() 64 | 65 | // 将active设置为false,避免反复调用stop反复 66 | this.active = false 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * 将effect从依赖中清空 73 | * @param effect {ReactiveEffect} 74 | */ 75 | function cleanupEffect(effect: ReactiveEffect) { 76 | effect.deps.forEach((dep) => { 77 | dep.delete(effect) 78 | }) 79 | // 清空掉effect.deps 80 | effect.deps.length = 0 81 | } 82 | 83 | type KeyToDepMap = Map 84 | // 用于存储target依赖 target -> depsMap 85 | const targetMap = new WeakMap() 86 | 87 | /** 88 | * 依赖收集 89 | * @desc 通过target和key拿到对应的dep依赖收集容器(没有则新建),然后将对应的ReactiveEffect实例存储进去 90 | * @param target 目标对象 91 | * @param key key值 92 | */ 93 | export function track(target: object, key) { 94 | // 判断是否可以收集依赖 95 | if (!isTracking()) return 96 | 97 | // 获取对应依赖的depsMap 98 | let depsMap = targetMap.get(target) 99 | // 没有则初始化 100 | if (!depsMap) 101 | targetMap.set(target, (depsMap = new Map())) 102 | 103 | // 获取对应key值的依赖dep 104 | let dep = depsMap.get(key) 105 | // 没有则初始化 106 | if (!dep) 107 | depsMap.set(key, (dep = createDep())) 108 | 109 | // 存储依赖 110 | trackEffects(dep) 111 | } 112 | 113 | /** 114 | * 收集effects 115 | * @param dep 116 | */ 117 | export function trackEffects(dep: Dep) { 118 | if (!dep.has(activeEffect!)) { 119 | // 将activeEffect存储到dep中 120 | dep.add(activeEffect! /* 非空断言 */) 121 | // 反向存储dep 122 | activeEffect!.deps.push(dep) 123 | } 124 | } 125 | 126 | /** 127 | * 判断是否可以收集依赖 128 | */ 129 | export function isTracking(): boolean { 130 | return shouldTrack && activeEffect !== undefined 131 | } 132 | 133 | /** 134 | * 触发依赖 135 | * @desc 根据target和key获取到对应的dep,然后遍历其中所有的依赖 136 | * @param target 137 | * @param key 138 | */ 139 | export function trigger(target, key) { 140 | const depsMap = targetMap.get(target) 141 | if (!depsMap) return 142 | 143 | const dep = depsMap.get(key) 144 | // 执行effects 145 | triggerEffects(dep) 146 | } 147 | 148 | /** 149 | * 遍历所有依赖,执行effect 150 | * @param dep 151 | */ 152 | export function triggerEffects(dep: Dep) { 153 | // 遍历所有依赖,遍历执行 154 | for (const effect of dep) { 155 | if (effect.scheduler) { 156 | // 如果存在scheduler调度函数,则执行 157 | effect.scheduler() 158 | } 159 | else { 160 | // 否则执行run函数 161 | effect.run() 162 | } 163 | } 164 | } 165 | 166 | interface ReactiveEffectOptions { 167 | scheduler?: EffectScheduler // 调度函数 168 | onStop?: () => void // 停止监听时触发 169 | } 170 | 171 | interface ReactiveEffectRunner { 172 | (): T 173 | 174 | effect: ReactiveEffect 175 | } 176 | 177 | /** 178 | * 主要负责依赖收集 179 | * @param fn {Function} 依赖方法 180 | * @param options {ReactiveEffectOptions} 选项 181 | */ 182 | export function effect(fn: () => T, options?: ReactiveEffectOptions): ReactiveEffectRunner { 183 | // 新建ReactiveEffect示例,将fn存储起来 184 | const _effect = new ReactiveEffect(fn) 185 | 186 | if (options) { 187 | // 合并options到_effect中 188 | extend(_effect, options) 189 | } 190 | 191 | // 执行run方法,调用fn函数,从而触发fn中的响应式数据进行依赖收集 192 | _effect.run() 193 | 194 | // 返回一个runner函数 195 | const runner = _effect.run.bind(_effect) as ReactiveEffectRunner 196 | runner.effect = _effect 197 | return runner 198 | } 199 | 200 | /** 201 | * 停止runner的响应式监听 202 | * @param runner 203 | */ 204 | export function stop(runner: ReactiveEffectRunner) { 205 | return runner.effect.stop() 206 | } 207 | -------------------------------------------------------------------------------- /packages/reactivity/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ref' 2 | export * from './reactive' 3 | export * from './computed' 4 | export { effect } from './effect' 5 | -------------------------------------------------------------------------------- /packages/reactivity/src/reactive.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from 'shared/index' 2 | import { mutableHandlers, readonlyHandlers, shallowReactiveHandlers, shallowReadonlyHandlers } from './baseHandlers' 3 | 4 | export const enum ReactiveFlags { 5 | IS_REACTIVE = '__v_isReactive', 6 | IS_READONLY = '__v_isReadonly', 7 | } 8 | 9 | /** 10 | * 生成响应式对象 11 | * @param target 12 | */ 13 | export function reactive(target: T) { 14 | return createReactiveObject(target, mutableHandlers) 15 | } 16 | 17 | /** 18 | * 生成浅响应式对象 19 | * @param target 20 | */ 21 | export function shallowReactive(target: T) { 22 | return createReactiveObject(target, shallowReactiveHandlers) 23 | } 24 | 25 | /** 26 | * 生成只读对象 27 | * @param target 28 | */ 29 | export function readonly(target: T) { 30 | return createReactiveObject(target, readonlyHandlers) 31 | } 32 | 33 | /** 34 | * 生成浅只读对象 35 | * @param target 36 | */ 37 | export function shallowReadonly(target: T) { 38 | return createReactiveObject(target, shallowReadonlyHandlers) 39 | } 40 | 41 | /** 42 | * 创建Proxy 43 | * @param target 44 | * @param baseHandlers proxy处理器 45 | */ 46 | function createReactiveObject(target: object, baseHandlers: ProxyHandler) { 47 | return new Proxy(target, baseHandlers) 48 | } 49 | 50 | /** 51 | * 判断是否为响应式对象 52 | * @param value 53 | */ 54 | export function isReactive(value: any): boolean { 55 | return !!(value && value[ReactiveFlags.IS_REACTIVE]) 56 | } 57 | 58 | /** 59 | * 判断是否为只读对象 60 | * @param value 61 | */ 62 | export function isReadonly(value: any): boolean { 63 | return !!(value && value[ReactiveFlags.IS_READONLY]) 64 | } 65 | 66 | /** 67 | * 判断是否由reactive或readonly生成的proxy 68 | * @param value 69 | */ 70 | export function isProxy(value: any): boolean { 71 | return isReactive(value) || isReadonly(value) 72 | } 73 | 74 | /** 75 | * 判断是否为对象,是的话进行响应式处理 76 | * @param value 77 | */ 78 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint 79 | export const toReactive = (value: T): T => (isObject(value) ? reactive(value) : value) 80 | -------------------------------------------------------------------------------- /packages/reactivity/src/ref.ts: -------------------------------------------------------------------------------- 1 | import { hasChanged } from 'shared/index' 2 | import { isTracking, trackEffects, triggerEffects } from './effect' 3 | import type { Dep } from './dep' 4 | import { createDep } from './dep' 5 | import { toReactive } from './reactive' 6 | 7 | export interface Ref { 8 | value: T 9 | } 10 | 11 | interface RefBase { 12 | dep?: Dep 13 | value: T 14 | } 15 | 16 | /** 17 | * 依赖收集 18 | * @param ref 19 | */ 20 | function trackRefValue(ref: RefBase) { 21 | if (isTracking()) { 22 | if (!ref.dep) { 23 | // 如果没有dep的话,初始化一个dep 24 | ref.dep = createDep() 25 | } 26 | // 依赖收集 27 | trackEffects(ref.dep!) 28 | } 29 | } 30 | 31 | /** 32 | * 触发依赖 33 | * @param ref 34 | */ 35 | export function triggerRefValue(ref: RefBase) { 36 | if (ref.dep) { 37 | // 触发依赖 38 | triggerEffects(ref.dep) 39 | } 40 | } 41 | 42 | // ref接口 43 | class RefImpl { 44 | private _value: T // 响应式处理后的值 45 | private _rawValue: T // 存储原始值,主要用于与newVal作比较 46 | public dep?: Dep 47 | public readonly __v_isRef = true // 用于isRef检验 48 | 49 | constructor(value) { 50 | this._rawValue = value 51 | this._value = toReactive(value) 52 | } 53 | 54 | get value() { 55 | // 依赖收集 56 | trackRefValue(this) 57 | // 返回值 58 | return this._value 59 | } 60 | 61 | set value(newVal) { 62 | // 判断newValue是否发生改变 63 | if (hasChanged(newVal, this._rawValue)) { 64 | // 赋值 65 | this._rawValue = newVal 66 | this._value = toReactive(newVal) 67 | // 触发依赖 68 | triggerRefValue(this) 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * ref响应式处理 75 | * @param value 76 | */ 77 | export function ref(value: any): Ref { 78 | return new RefImpl(value) 79 | } 80 | 81 | /** 82 | * 判断是否为ref变量 83 | * @param r 84 | */ 85 | export function isRef(r: any): r is Ref { 86 | return !!(r && r.__v_isRef) 87 | } 88 | 89 | /** 90 | * 获取ref的value值,如果不是ref就返回本身 91 | * @param ref 92 | */ 93 | export function unref(ref: T | Ref): T { 94 | return isRef(ref) ? ref.value : ref 95 | } 96 | 97 | // proxyRefs的处理器 98 | const shallowUnwrapHandlers: ProxyHandler = { 99 | get: (target, key) => unref(Reflect.get(target, key)), 100 | set: (target, key, value) => { 101 | const oldValue = target[key] 102 | // 如果oldValue是一个ref值,而newValue不是,则需要特殊处理 103 | if (isRef(oldValue) && !isRef(value)) { 104 | oldValue.value = value 105 | return true 106 | } 107 | else { 108 | return Reflect.set(target, key, value) 109 | } 110 | }, 111 | } 112 | 113 | /** 114 | * ref代理 115 | * @param objectWithRefs 116 | */ 117 | export function proxyRefs(objectWithRefs: T) { 118 | return new Proxy(objectWithRefs, shallowUnwrapHandlers) 119 | } 120 | -------------------------------------------------------------------------------- /packages/runtime-core/__tests__/scheduler.spec.ts: -------------------------------------------------------------------------------- 1 | import { nextTick, queueJob, queuePostFlushCb, queuePreFlushCb } from '../src/scheduler' 2 | 3 | describe('scheduler', () => { 4 | it('nextTick', async() => { 5 | const calls: string[] = [] 6 | const dummyThen = Promise.resolve().then() 7 | const job1 = () => { 8 | calls.push('job1') 9 | } 10 | const job2 = () => { 11 | calls.push('job2') 12 | } 13 | nextTick(job1) 14 | job2() 15 | 16 | expect(calls.length).toBe(1) 17 | await dummyThen 18 | expect(calls.length).toBe(2) 19 | expect(calls).toMatchObject(['job2', 'job1']) 20 | }) 21 | 22 | it('queueJob', async() => { 23 | const calls: string[] = [] 24 | const job1 = () => { 25 | calls.push('job1') 26 | } 27 | const job2 = () => { 28 | calls.push('job2') 29 | } 30 | queueJob(job1) 31 | queueJob(job2) 32 | expect(calls).toEqual([]) 33 | await nextTick() 34 | expect(calls).toEqual(['job1', 'job2']) 35 | }) 36 | 37 | it('queuePreFlushCb', async() => { 38 | const calls: string[] = [] 39 | const cb1 = () => { 40 | calls.push('cb1') 41 | } 42 | const cb2 = () => { 43 | calls.push('cb2') 44 | } 45 | 46 | queuePreFlushCb(cb1) 47 | queuePreFlushCb(cb2) 48 | 49 | expect(calls).toEqual([]) 50 | await nextTick() 51 | expect(calls).toEqual(['cb1', 'cb2']) 52 | }) 53 | 54 | it('queuePostFlushCb', async() => { 55 | const calls: string[] = [] 56 | const cb1 = () => { 57 | calls.push('cb1') 58 | } 59 | const cb2 = () => { 60 | calls.push('cb2') 61 | } 62 | const cb3 = () => { 63 | calls.push('cb3') 64 | } 65 | 66 | queuePostFlushCb([cb1, cb2]) 67 | queuePostFlushCb(cb3) 68 | 69 | expect(calls).toEqual([]) 70 | await nextTick() 71 | expect(calls).toEqual(['cb1', 'cb2', 'cb3']) 72 | }) 73 | 74 | it('pre最先执行,然后执行job,最后执行post', async() => { 75 | const calls: string[] = [] 76 | const job1 = () => { 77 | calls.push('job1') 78 | } 79 | const cb1 = () => { 80 | calls.push('cb1') 81 | queuePostFlushCb(cb3) 82 | queueJob(job1) 83 | queuePreFlushCb(cb2) 84 | } 85 | const cb2 = () => { 86 | calls.push('cb2') 87 | } 88 | 89 | const cb3 = () => { 90 | calls.push('cb3') 91 | } 92 | 93 | queuePreFlushCb(cb1) 94 | await nextTick() 95 | expect(calls).toEqual(['cb1', 'cb2', 'job1', 'cb3']) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiCreateApp.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from 'runtime-core/component' 2 | import type { RootRenderFunction } from 'runtime-core/renderer' 3 | import { createVNode } from './vnode' 4 | 5 | export type App = any 6 | 7 | export type CreateAppFunction = (rootComponent: Component) => App 8 | 9 | export function createAppAPI(render: RootRenderFunction): CreateAppFunction { 10 | return function createApp(rootComponent: Component) { 11 | return { 12 | mount(rootContainer) { 13 | // 创建虚拟节点vnode 14 | const vnode = createVNode(rootComponent) 15 | // 进行渲染 16 | render(vnode, rootContainer) 17 | }, 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { currentInstance } from 'runtime-core/component' 2 | import { isFunction } from 'shared/index' 3 | 4 | export interface InjectionKey extends Symbol {} 5 | 6 | export function provide(key: InjectionKey | string | number, value: T) { 7 | if (currentInstance) { 8 | let provides = currentInstance.provides 9 | const parentProvides = currentInstance.parent && currentInstance.parent.provides 10 | 11 | // 初始化的时候,也就是组件第一次调用provide的时候,绑定通过原型链的方式绑定父级provide 12 | if (provides === parentProvides) 13 | provides = currentInstance.provides = Object.create(parentProvides) 14 | 15 | provides[key as string] = value 16 | } 17 | } 18 | 19 | export function inject(key: InjectionKey | string, defaultValue?: unknown) { 20 | const instance = currentInstance 21 | if (instance) { 22 | const provides = instance.parent && instance.provides 23 | 24 | if (provides && (key as string | symbol) in provides) 25 | return provides[key as string] 26 | else if (defaultValue) 27 | return isFunction(defaultValue) ? defaultValue.call(instance.proxy) : defaultValue 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiLifecycle.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentInternalInstance } from 'runtime-core/component' 2 | import { 3 | LifecycleHooks, 4 | currentInstance, 5 | setCurrentInstance, 6 | unsetCurrentInstance, 7 | } from 'runtime-core/component' 8 | import { pauseTrack, resetTracking } from 'reactivity/effect' 9 | 10 | function injectHook(type: LifecycleHooks, hook: Function, target: ComponentInternalInstance | null = currentInstance) { 11 | if (target) { 12 | const hooks = target[type] || (target[type] = []) 13 | hooks.push(() => { 14 | pauseTrack() 15 | setCurrentInstance(target) 16 | hook() 17 | unsetCurrentInstance() 18 | resetTracking() 19 | }) 20 | } 21 | } 22 | 23 | export const createHook 24 | = any>(lifecycle: LifecycleHooks) => 25 | (hook: T, target: ComponentInternalInstance | null = currentInstance) => 26 | injectHook(lifecycle, hook, target) 27 | 28 | export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT) 29 | export const onMounted = createHook(LifecycleHooks.MOUNTED) 30 | export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE) 31 | export const onUpdated = createHook(LifecycleHooks.UPDATED) 32 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiWatch.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'reactivity/ref' 2 | import { isRef } from 'reactivity/ref' 3 | import { currentInstance } from 'runtime-core/component' 4 | import { isReactive } from 'reactivity/reactive' 5 | import { NOOP, hasChanged, isArray, isFunction, isObject, isPlainObject } from 'shared/index' 6 | import type { EffectScheduler } from 'reactivity/effect' 7 | import { ReactiveEffect } from 'reactivity/effect' 8 | import type { SchedulerJob } from 'runtime-core/scheduler' 9 | import { queuePreFlushCb } from 'runtime-core/scheduler' 10 | 11 | export type WatchSource = Ref | (() => T) 12 | 13 | export type WatchEffect = () => void 14 | 15 | export type WatchCallback = (value: V, oldValue: OV) => any 16 | 17 | export type WatchStopHandle = () => void 18 | 19 | export function watchEffect(effect: WatchEffect): WatchStopHandle { 20 | return doWatch(effect, null) 21 | } 22 | 23 | export function watch(source: T | WatchSource, cb: any): WatchStopHandle { 24 | return doWatch(source as any, cb) 25 | } 26 | 27 | function doWatch(source: WatchSource | WatchSource[] | WatchEffect, cb: WatchCallback | null): WatchStopHandle { 28 | const instance = currentInstance 29 | let getter: () => any 30 | let deep = false 31 | 32 | if (isRef(source)) { 33 | getter = () => source.value 34 | } 35 | else if (isReactive(source)) { 36 | getter = () => source 37 | deep = true 38 | } 39 | else if (isArray(source)) { 40 | getter = () => 41 | // eslint-disable-next-line array-callback-return 42 | source.map((s) => { 43 | if (isRef(s)) 44 | return s.value 45 | else if (isReactive(s)) 46 | return s 47 | else if (isFunction(s)) 48 | return () => s() 49 | }) 50 | } 51 | else if (isFunction(source)) { 52 | getter = () => source() 53 | } 54 | else { 55 | getter = NOOP 56 | } 57 | 58 | if (cb && deep) { 59 | const baseGetter = getter 60 | getter = () => traverse(baseGetter()) 61 | } 62 | 63 | let oldValue = [] 64 | const job: SchedulerJob = () => { 65 | if (!effect.active) return 66 | 67 | if (cb) { 68 | const newValue = effect.run() 69 | 70 | if (deep || hasChanged(newValue, oldValue)) { 71 | cb(newValue, oldValue) 72 | oldValue = newValue 73 | } 74 | } 75 | else { 76 | effect.run() 77 | } 78 | } 79 | 80 | const scheduler: EffectScheduler = () => { 81 | if (!instance || instance.isMounted) 82 | queuePreFlushCb(job) 83 | else 84 | job() 85 | } 86 | 87 | const effect = new ReactiveEffect(getter, scheduler) 88 | 89 | if (cb) 90 | oldValue = effect.run() 91 | else 92 | effect.run() 93 | 94 | return () => { 95 | effect.stop() 96 | } 97 | } 98 | 99 | function traverse(value: unknown, seen?: Set) { 100 | if (!isObject(value)) 101 | return value 102 | 103 | seen = seen || new Set() 104 | 105 | if (seen.has(value)) return value 106 | 107 | seen.add(value) 108 | if (isRef(value)) { 109 | traverse(value.value, seen) 110 | } 111 | else if (isArray(value)) { 112 | for (let i = 0; i < value.length; i++) 113 | traverse(value[i], seen) 114 | } 115 | else if (isPlainObject(value)) { 116 | for (const key in value) 117 | traverse((value as any)[key], seen) 118 | } 119 | 120 | return value 121 | } 122 | -------------------------------------------------------------------------------- /packages/runtime-core/src/component.ts: -------------------------------------------------------------------------------- 1 | import type { VNode, VNodeChild } from 'runtime-core/vnode' 2 | import { EMPTY_OBJ, isFunction, isObject } from 'shared/index' 3 | import { proxyRefs } from 'reactivity/ref' 4 | import { PublicInstanceProxyHandlers } from 'runtime-core/componentPublicInstance' 5 | import { initProps } from 'runtime-core/componentProps' 6 | import { shallowReadonly } from 'reactivity/reactive' 7 | import type { EmitFn } from 'runtime-core/componentEmit' 8 | import { emit } from 'runtime-core/componentEmit' 9 | import type { InternalSlots } from 'runtime-core/componentSlots' 10 | import { initSlots } from 'runtime-core/componentSlots' 11 | import type { CompilerOptions } from 'compiler-core/options' 12 | 13 | export type RenderFunction = () => VNodeChild 14 | 15 | export type Component = any 16 | 17 | export type Data = Record 18 | 19 | export interface ComponentInternalInstance { 20 | vnode: VNode 21 | type: any 22 | parent: ComponentInternalInstance | null 23 | next: VNode | null 24 | subTree: VNode 25 | update: any 26 | render: any 27 | proxy: any // 代理this 28 | ctx: Data 29 | provides: Data 30 | // state 31 | setupState: Data 32 | props: Data 33 | slots: InternalSlots 34 | emit: EmitFn 35 | 36 | // lifecycle 37 | isMounted: boolean 38 | [LifecycleHooks.BEFORE_MOUNT]: LifecycleHook 39 | [LifecycleHooks.MOUNTED]: LifecycleHook 40 | [LifecycleHooks.BEFORE_UPDATE]: LifecycleHook 41 | [LifecycleHooks.UPDATED]: LifecycleHook 42 | } 43 | 44 | type LifecycleHook = TFn[] | null 45 | 46 | export const enum LifecycleHooks { 47 | BEFORE_MOUNT = 'bm', 48 | MOUNTED = 'm', 49 | BEFORE_UPDATE = 'bu', 50 | UPDATED = 'u', 51 | } 52 | 53 | // eslint-disable-next-line import/no-mutable-exports 54 | export let currentInstance: ComponentInternalInstance | null = null 55 | 56 | export const getCurrentInstance = (): ComponentInternalInstance | null => currentInstance 57 | 58 | export const setCurrentInstance = (instance: ComponentInternalInstance) => (currentInstance = instance) 59 | 60 | export const unsetCurrentInstance = () => (currentInstance = null) 61 | 62 | /** 63 | * 创建组件实例 64 | * @param vnode 65 | * @param parent 66 | */ 67 | export function createComponentInstance( 68 | vnode: VNode, 69 | parent: ComponentInternalInstance | null, 70 | ): ComponentInternalInstance { 71 | const instance: ComponentInternalInstance = { 72 | vnode, 73 | type: vnode.type, 74 | parent, 75 | next: null!, 76 | subTree: null!, 77 | update: null!, 78 | render: null, 79 | proxy: null, 80 | provides: parent ? parent.provides : EMPTY_OBJ, 81 | setupState: EMPTY_OBJ, 82 | props: EMPTY_OBJ, 83 | slots: EMPTY_OBJ, 84 | emit: null!, 85 | ctx: EMPTY_OBJ, 86 | isMounted: false, 87 | bm: null, 88 | m: null, 89 | bu: null, 90 | u: null, 91 | } 92 | 93 | instance.ctx = { _: instance } 94 | instance.emit = emit.bind(null, instance) 95 | return instance 96 | } 97 | 98 | /** 99 | * 初始化组件 100 | * @param instance 101 | */ 102 | export function setupComponent(instance: ComponentInternalInstance) { 103 | const { props, children } = instance.vnode 104 | // 初始化属性 105 | initProps(instance, props as Data) 106 | 107 | // 初始化插槽 108 | initSlots(instance, children) 109 | 110 | // 处理成有状态的组件 111 | setupStatefulComponent(instance) 112 | } 113 | 114 | /** 115 | * 组件状态化 116 | * @param instance 117 | */ 118 | function setupStatefulComponent(instance: ComponentInternalInstance) { 119 | const Component = instance.type 120 | const { setup } = Component 121 | 122 | // 初始化组件代理 123 | instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers) 124 | if (setup) { 125 | // 处理setup钩子 126 | setCurrentInstance(instance) 127 | 128 | const setupResult = setup(shallowReadonly(instance.props), { 129 | emit: instance.emit, 130 | }) 131 | 132 | unsetCurrentInstance() 133 | 134 | // 处理setup返回值 135 | handleSetupResult(instance, setupResult) 136 | } 137 | } 138 | 139 | /** 140 | * 处理setup返回值 141 | * @param instance 142 | * @param setupResult 143 | */ 144 | function handleSetupResult(instance: ComponentInternalInstance, setupResult: unknown) { 145 | if (isFunction(setupResult)) { 146 | // TODO function 147 | } 148 | else if (isObject(setupResult)) { 149 | // 将setupResult响应式,并赋值给实例 150 | instance.setupState = proxyRefs(setupResult) 151 | } 152 | 153 | // 当组件状态化后,实现render函数 154 | finishComponentSetup(instance) 155 | } 156 | 157 | type CompileFunction = ( 158 | template: string | object, 159 | options?: CompilerOptions 160 | ) => RenderFunction 161 | 162 | let compile: CompileFunction | undefined 163 | 164 | export function registerRuntimeCompiler(_complie: any) { 165 | compile = _complie 166 | } 167 | 168 | /** 169 | * 当组件状态化后,实现render函数 170 | * @param instance 171 | */ 172 | function finishComponentSetup(instance: ComponentInternalInstance) { 173 | const Component = instance.type 174 | 175 | if (compile && !Component.render) { 176 | if (Component.template) 177 | Component.render = compile(Component.template) 178 | } 179 | 180 | if (Component.render) 181 | instance.render = Component.render 182 | } 183 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { camelize, toHandlerKey } from 'shared/index' 2 | import type { ComponentInternalInstance } from './component' 3 | 4 | export type EmitFn = (event: string, ...args: any[]) => void 5 | 6 | export function emit(instance: ComponentInternalInstance, event: string, ...rawArgs: any[]) { 7 | const { props } = instance 8 | 9 | let handlerName 10 | const handler = props[(handlerName = toHandlerKey(event))] || props[(handlerName = toHandlerKey(camelize(event)))] 11 | 12 | if (handler && typeof handler === 'function') 13 | handler && handler(...rawArgs) 14 | } 15 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentProps.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentInternalInstance, Data } from './component' 2 | 3 | export function initProps(instance: ComponentInternalInstance, rawProps: Data | null) { 4 | if (rawProps) 5 | instance.props = rawProps 6 | } 7 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from 'shared/index' 2 | import { shallowReadonly } from 'reactivity/reactive' 3 | import type { ComponentInternalInstance } from './component' 4 | 5 | export interface ComponentRenderContext { 6 | [key: string]: any 7 | _: ComponentInternalInstance 8 | } 9 | 10 | export type PublicPropertiesMap = Record any> 11 | 12 | const publicPropertiesMap: PublicPropertiesMap = { 13 | $el: i => i.vnode.el, 14 | $slots: i => shallowReadonly(i.slots), 15 | } 16 | 17 | export const PublicInstanceProxyHandlers: ProxyHandler = { 18 | get({ _: instance }: ComponentRenderContext, key: string) { 19 | const { setupState, props } = instance 20 | 21 | if (hasOwn(setupState, key)) 22 | return setupState[key] 23 | else if (hasOwn(props, key)) 24 | return props[key] 25 | 26 | const publicGetter = publicPropertiesMap[key] 27 | if (publicGetter) 28 | return publicGetter(instance) 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentRenderUtils.ts: -------------------------------------------------------------------------------- 1 | import type { VNode } from 'runtime-core/vnode' 2 | import type { Data } from 'runtime-core/component' 3 | 4 | export function shouldUpdateComponent(prevVNode: VNode, nextVNode: VNode) { 5 | const { props: prevProps } = prevVNode 6 | const { props: nextProps } = nextVNode 7 | 8 | if (prevProps === nextProps) return false 9 | 10 | if (!prevProps) return !nextProps 11 | 12 | if (!nextProps) return true 13 | 14 | return hasPropsChanged(prevProps, nextProps) 15 | } 16 | 17 | function hasPropsChanged(prevProps: Data, nextProps: Data): boolean { 18 | const nextKeys = Object.keys(nextProps) 19 | 20 | if (nextKeys.length !== Object.keys(prevProps).length) return true 21 | 22 | for (let i = 0; i < nextKeys.length; i++) { 23 | const key = nextKeys[i] 24 | if (nextProps[key] !== prevProps[key]) 25 | return true 26 | } 27 | 28 | return false 29 | } 30 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import type { VNode } from 'runtime-core/vnode' 2 | import { ShapeFlags, isArray, isFunction } from 'shared/index' 3 | import type { ComponentInternalInstance } from './component' 4 | import type { VNodeNormalizedChildren } from './vnode' 5 | 6 | export type Slot = (...args: any[]) => VNode[] 7 | 8 | export type Slots = Readonly 9 | 10 | export type InternalSlots = Record 11 | 12 | export type RawSlots = Record 13 | 14 | const normalizeSlotValue = (value: unknown): VNode[] => (isArray(value) ? value : [value]) 15 | 16 | function normalizeObjectSlots(rawSlots: RawSlots, slots: InternalSlots) { 17 | for (const key in rawSlots) { 18 | const value = rawSlots[key] 19 | if (isFunction(value)) 20 | slots[key] = props => normalizeSlotValue(value(props)) 21 | } 22 | } 23 | 24 | export function initSlots(instance: ComponentInternalInstance, children: VNodeNormalizedChildren) { 25 | if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) 26 | normalizeObjectSlots(children as unknown as RawSlots, (instance.slots = {})) 27 | } 28 | -------------------------------------------------------------------------------- /packages/runtime-core/src/h.ts: -------------------------------------------------------------------------------- 1 | import type { VNode, VNodeTypes } from 'runtime-core/vnode' 2 | import { createVNode } from 'runtime-core/vnode' 3 | 4 | export function h(type: VNodeTypes, props = null, children = null): VNode { 5 | return createVNode(type, props, children) 6 | } 7 | -------------------------------------------------------------------------------- /packages/runtime-core/src/helpers/renderSlot.ts: -------------------------------------------------------------------------------- 1 | import { Fragment, createVNode } from 'runtime-core/vnode' 2 | import type { Slots } from 'runtime-core/componentSlots' 3 | import type { Data } from 'runtime-core/component' 4 | import type { VNode } from '../vnode' 5 | 6 | export function renderSlot(slots: Slots, name: string, props: Data = {}): VNode | undefined { 7 | const slot = slots[name] 8 | if (slot) 9 | return createVNode(Fragment, {}, slot(props)) 10 | } 11 | -------------------------------------------------------------------------------- /packages/runtime-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { renderSlot } from './helpers/renderSlot' 2 | export { createTextVNode, createElementVNode } from './vnode' 3 | export { getCurrentInstance } from './component' 4 | export { provide, inject } from './apiInject' 5 | export { h } from './h' 6 | export { createRenderer, RendererOptions } from './renderer' 7 | export { onBeforeMount, onMounted, onBeforeUpdate, onUpdated } from './apiLifecycle' 8 | export { nextTick } from './scheduler' 9 | export { watch, watchEffect } from './apiWatch' 10 | export { toDisplayString } from 'shared/index' 11 | -------------------------------------------------------------------------------- /packages/runtime-core/src/renderer.ts: -------------------------------------------------------------------------------- 1 | import { EMPTY_ARR, EMPTY_OBJ, ShapeFlags, invokeArrayFns } from 'shared/index' 2 | import type { VNode, VNodeArrayChildren } from 'runtime-core/vnode' 3 | import { Fragment, Text, isSameVNodeType, normalizeVNode } from 'runtime-core/vnode' 4 | import { ReactiveEffect } from 'reactivity/effect' 5 | import { shouldUpdateComponent } from 'runtime-core/componentRenderUtils' 6 | import { createAppAPI } from './apiCreateApp' 7 | import { createComponentInstance, setupComponent } from './component' 8 | import type { ComponentInternalInstance, Data } from './component' 9 | import type { SchedulerJob } from './scheduler' 10 | import { flushPostFlushCbs, queueJob, queuePostFlushCb } from './scheduler' 11 | 12 | export type RendererNode = Record 13 | 14 | export interface RendererElement extends RendererNode {} 15 | 16 | export type RootRenderFunction = (vnode: VNode | null, container: HostElement) => void 17 | 18 | export interface RendererOptions { 19 | patchProp(el: HostElement, key: string, prevValue: any, nextValue: any): void 20 | 21 | insert(el: HostNode, parent: HostElement, hostInsert: HostNode | null): void 22 | 23 | remove(el: HostNode): void 24 | 25 | createElement(type: string): HostElement 26 | 27 | setElementText(node: HostElement, text: string): void 28 | } 29 | 30 | type PatchFn = ( 31 | n1: VNode | null, 32 | n2: VNode, 33 | container: RendererElement, 34 | anchor?: RendererNode | null, 35 | parentComponent?: ComponentInternalInstance | null 36 | ) => void 37 | 38 | type MountChildrenFn = ( 39 | children: VNodeArrayChildren, 40 | container: RendererElement, 41 | anchor: RendererNode | null, 42 | parentComponent: ComponentInternalInstance | null, 43 | start?: number 44 | ) => void 45 | 46 | export const queuePostRenderEffect = queuePostFlushCb 47 | 48 | /** 49 | * 工厂函数 —— 生成renderer 50 | * @param options 51 | */ 52 | export function createRenderer( 53 | options: RendererOptions, 54 | ) { 55 | return baseCreateRenderer(options) 56 | } 57 | 58 | function baseCreateRenderer(options: RendererOptions): any { 59 | // 解析出节点操作函数 60 | const { 61 | patchProp: hostPatchProp, // 更新属性 62 | remove: hostRemove, // 删除节点 63 | insert: hostInsert, // 插入节点 64 | createElement: hostCreateElement, // 创建节点 65 | setElementText: hostSetElementText, // 设置节点文本 66 | } = options 67 | 68 | /** 69 | * 渲染函数 70 | * @param vnode 71 | * @param container 72 | */ 73 | const render: RootRenderFunction = (vnode, container) => { 74 | if (vnode !== null) 75 | patch(null, vnode, container, null, null) 76 | 77 | flushPostFlushCbs() 78 | } 79 | 80 | /** 81 | * @param n1 82 | * @param n2 83 | * @param container 84 | * @param anchor 85 | * @param parentComponent 86 | */ 87 | const patch: PatchFn = (n1, n2, container, anchor = null, parentComponent = null) => { 88 | if (n1 === n2) return 89 | 90 | const { type, shapeFlag } = n2 91 | 92 | switch (type) { 93 | case Text: 94 | processText(n1, n2, container) 95 | break 96 | case Fragment: 97 | processFragment(n1, n2, container, anchor, parentComponent) 98 | break 99 | default: 100 | // 使用shapeFlag判断vnode类型 101 | if (shapeFlag & ShapeFlags.ELEMENT /* element类型 */) 102 | processElement(n1, n2, container, anchor, parentComponent) 103 | else if (shapeFlag & ShapeFlags.COMPONENT /* 组件类型 */) 104 | processComponent(n1, n2, container, anchor, parentComponent) 105 | } 106 | } 107 | 108 | function processText(n1: VNode | null, n2: VNode, container: RendererElement) { 109 | const { children } = n2 110 | const textNode = document.createTextNode(children as string) 111 | container.append(textNode) 112 | } 113 | 114 | /** 115 | * 处理Fragment,自渲染子节点 116 | * @param n1 117 | * @param n2 118 | * @param container 119 | * @param anchor 120 | * @param parentComponent 121 | */ 122 | function processFragment( 123 | n1: VNode | null, 124 | n2: VNode, 125 | container: RendererElement, 126 | anchor: RendererNode | null, 127 | parentComponent: ComponentInternalInstance | null, 128 | ) { 129 | mountChildren(n2.children as VNodeArrayChildren, container, anchor, parentComponent) 130 | } 131 | 132 | /** 133 | * 处理Element 134 | * @param n1 135 | * @param n2 136 | * @param container 137 | * @param anchor 138 | * @param parentComponent 139 | */ 140 | function processElement( 141 | n1: VNode | null, 142 | n2: VNode, 143 | container: RendererElement, 144 | anchor: RendererNode | null, 145 | parentComponent: ComponentInternalInstance | null, 146 | ) { 147 | if (n1 === null) 148 | mountElement(n2, container, anchor, parentComponent) 149 | else 150 | patchElement(n1, n2, parentComponent) 151 | } 152 | 153 | /** 154 | * 挂载Element 155 | * @param vnode 156 | * @param container 157 | * @param anchor 158 | * @param parentComponent 159 | */ 160 | function mountElement( 161 | vnode: VNode, 162 | container: RendererElement, 163 | anchor: RendererNode | null, 164 | parentComponent: ComponentInternalInstance | null, 165 | ) { 166 | const el = (vnode.el = hostCreateElement(vnode.type)) 167 | 168 | const { children, props, shapeFlag } = vnode 169 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 170 | // 文本节点 171 | el.textContent = children 172 | } 173 | else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 174 | // 虚拟节点数组 175 | mountChildren(children as VNodeArrayChildren, el, null, parentComponent) 176 | } 177 | 178 | for (const key in props) { 179 | const val = props[key] 180 | 181 | hostPatchProp(el, key, '', val) 182 | } 183 | 184 | hostInsert(el, container, anchor) 185 | } 186 | 187 | function patchElement(n1: VNode, n2: VNode, parentComponent: ComponentInternalInstance | null) { 188 | const el = (n2.el = n1.el!) 189 | 190 | const oldProps = n1.props || EMPTY_OBJ 191 | const newProps = n2.props || EMPTY_OBJ 192 | 193 | patchChildren(n1, n2, el, null, parentComponent) 194 | 195 | patchProps(el, n2, oldProps, newProps) 196 | } 197 | 198 | function patchChildren( 199 | n1: VNode, 200 | n2: VNode, 201 | container: RendererElement, 202 | anchor: RendererNode | null, 203 | parentComponent: ComponentInternalInstance | null, 204 | ) { 205 | const c1 = n1 && n1.children 206 | const prevShapeFlag = n1 ? n1.shapeFlag : 0 207 | const c2 = n2.children 208 | const { shapeFlag } = n2 209 | 210 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 211 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 212 | // 将 oldChildren 清空 213 | unmountChildren(c1 as VNode[], parentComponent) 214 | } 215 | 216 | // 更新文本 217 | if (c2 !== c1) 218 | hostSetElementText(container, c2 as string) 219 | } 220 | else { 221 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 222 | if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) 223 | patchKeyedChildren(c1 as VNode[], c2 as VNode[], container, anchor, parentComponent) 224 | else 225 | unmountChildren(c1 as VNode[], parentComponent) 226 | } 227 | else { 228 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) 229 | hostSetElementText(container, '') 230 | 231 | if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) 232 | mountChildren(c2 as VNodeArrayChildren, container, anchor, parentComponent) 233 | } 234 | } 235 | } 236 | 237 | function unmountChildren(children: VNode[], parentComponent: ComponentInternalInstance | null, start = 0) { 238 | for (let i = start; i < children.length; i++) 239 | hostRemove(children[i].el!) 240 | } 241 | 242 | function patchProps(el: RendererElement, vnode: VNode, oldProps: Data, newProps: Data) { 243 | if (oldProps !== newProps) { 244 | for (const key in newProps) { 245 | const next = newProps[key] 246 | const prev = oldProps[key] 247 | 248 | if (next !== prev) 249 | hostPatchProp(el, key, prev, next) 250 | } 251 | 252 | if (oldProps !== EMPTY_OBJ) { 253 | for (const key in oldProps) { 254 | if (!(key in newProps)) 255 | hostPatchProp(el, key, oldProps[key], null) 256 | } 257 | } 258 | } 259 | } 260 | 261 | /** 262 | * 对比两个数组节点 263 | * @param c1 旧节点 264 | * @param c2 新节点 265 | * @param container 266 | * @param parentAnchor 267 | * @param parentComponent 268 | */ 269 | function patchKeyedChildren( 270 | c1: VNode[], 271 | c2: VNode[], 272 | container: RendererElement, 273 | parentAnchor: RendererNode | null, 274 | parentComponent: ComponentInternalInstance | null, 275 | ) { 276 | let i = 0 277 | const l2 = c2.length 278 | let e1 = c1.length - 1 279 | let e2 = l2 - 1 280 | 281 | // (a b) c 282 | // (a b) d e 283 | // 从头开始遍历,当两个虚拟节点为相同类型的话,进行patch 284 | while (i <= e1 && i <= e2) { 285 | const n1 = c1[i] 286 | const n2 = c2[i] 287 | 288 | if (isSameVNodeType(n1, n2)) 289 | patch(n1, n2, container, null, parentComponent) 290 | else 291 | break 292 | 293 | i++ 294 | } 295 | 296 | // a (b c) 297 | // d e (b c) 298 | // 从尾部开始遍历,当两个虚拟节点为相同类型的话,进行patch 299 | while (i <= e1 && i <= e2) { 300 | const n1 = c1[e1] 301 | const n2 = c2[e2] 302 | 303 | if (isSameVNodeType(n1, n2)) 304 | patch(n1, n2, container, null, parentComponent) 305 | else 306 | break 307 | 308 | e1-- 309 | e2-- 310 | } 311 | 312 | // (a b) 313 | // (a b) c 314 | // i = 2, e1 = 1, e2 = 2 315 | // (a b) 316 | // c (a b) 317 | // i = 0, e1 = -1, e2 = 0 318 | // 当旧节点已经遍历完了,没有剩余还没处理的节点,同时新节点还有剩余的时候 319 | // 新增节点 320 | if (i > e1) { 321 | if (i <= e2) { 322 | // 获取相邻锚点容器 323 | const nextPos = e2 + 1 324 | const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor 325 | // 遍历剩余新节点,一一进行patch 326 | while (i <= e2) { 327 | patch(null, c2[i], container, anchor, parentComponent) 328 | i++ 329 | } 330 | } 331 | } 332 | 333 | // (a b) c 334 | // (a b) 335 | // i = 2, e1 = 2, e2 = 1 336 | // a (b c) 337 | // (b c) 338 | // i = 0, e1 = 0, e2 = -1 339 | // 当新节点全部处理完成,没有剩余节点;而旧节点还有剩余节点时 340 | // 删除节点 341 | else if (i > e2) { 342 | while (i <= e1) { 343 | hostRemove(c1[i].el!) 344 | i++ 345 | } 346 | } 347 | 348 | // [i ... e1 + 1]: a b [c d e] f g 349 | // [i ... e2 + 1]: a b [e d c h] f g 350 | // i = 2, e1 = 4, e2 = 5 351 | // 当新旧节点还有剩余的时候,进行暴力解法 352 | else { 353 | const s1 = i 354 | const s2 = i 355 | 356 | // 遍历剩余的新节点,生成一份节点key -> index 的映射表keyToNewIndexMap 357 | const keyToNewIndexMap = new Map() 358 | for (i = s2; i <= e2; i++) { 359 | const nextChild = c2[i] 360 | if (nextChild.key !== null) 361 | keyToNewIndexMap.set(nextChild.key, i) 362 | } 363 | 364 | let j 365 | let patched = 0 // 统计对比过的数量 366 | const toBePatched = e2 - s2 + 1 // 未处理的新节点剩余数量 367 | let moved = false // 判断是否存在移动节点的操作 368 | let maxNewIndexSoFar = 0 369 | 370 | // 生成一个toBePatched长度的数组,用于新节点对应旧节点的下标 371 | const newIndexToOldIndexMap = new Array(toBePatched).fill(0) 372 | 373 | // 遍历剩余的旧节点 374 | for (i = s1; i <= e1; i++) { 375 | const prevChild = c1[i] 376 | 377 | // 当旧节点超过新节点的时候,直接删除节点 378 | if (patched >= toBePatched) { 379 | hostRemove(prevChild.el!) 380 | continue 381 | } 382 | 383 | let newIndex 384 | if (prevChild.key !== null) { 385 | // 如果当前旧节点存在key值,则从keyToNewIndexMap映射表查找有没有对应的新节点,有则获取其下标 386 | newIndex = keyToNewIndexMap.get(prevChild.key) 387 | } 388 | else { 389 | // 如果当前旧节点不存在key值,则遍历剩余新节点,一一匹配,如果匹配到了,则获取其下标 390 | for (j = s2; j <= e2; j++) { 391 | if (newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j])) { 392 | newIndex = j 393 | break 394 | } 395 | } 396 | } 397 | 398 | if (newIndex === undefined) { 399 | // 如果此时还没匹配到对应的新节点的话,则删除该旧节点 400 | hostRemove(prevChild.el!) 401 | } 402 | else { 403 | // 如果匹配到则更新节点 404 | // 更新该新节点对应的旧节点位置 405 | newIndexToOldIndexMap[newIndex - s2] = i + 1 406 | // 判断整个流程是否需要移动到节点 407 | if (newIndex >= maxNewIndexSoFar) 408 | maxNewIndexSoFar = newIndex 409 | else 410 | moved = true 411 | 412 | // 进行节点对比 413 | patch(prevChild, c2[newIndex], container, null, parentComponent) 414 | // 更新已经对比过的数量 415 | patched++ 416 | } 417 | } 418 | 419 | // [2, 5, 3, 1] 420 | // [0, 2] 421 | // 如果需要进行节点移动,则根据newIndexToOldIndexMap生成最长递增子序列在原数组的下标数组 422 | const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : EMPTY_ARR 423 | // increasingNewIndexSequence 最后下标 424 | j = increasingNewIndexSequence.length - 1 425 | // 倒序遍历暴力解法中所有的新节点 426 | for (i = toBePatched - 1; i >= 0; i--) { 427 | const nextIndex = s2 + i // 新节点的下标 428 | const nextChild = c2[nextIndex] // 新节点 429 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor // 获取锚点 430 | if (newIndexToOldIndexMap[i] === 0) { 431 | // 如果newIndexToOldIndexMap[i] === 0,则代表该新新节点在前面没有匹配到对应的旧节点,则新增节点 432 | patch(null, nextChild, container, anchor, parentComponent) 433 | } 434 | else if (moved) { 435 | // 如果需要进行到节点移动 436 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 437 | // j 已经没有了 说明剩下的都需要移动了 438 | // 最长子序列里面的值和当前的值匹配不上,说明当前元素需要移动 439 | hostInsert(nextChild.el!, container, anchor) 440 | } 441 | else { 442 | // 满足 i === increasingNewIndexSequence[j],因此不需要移动了,则递减 j 443 | j-- 444 | } 445 | } 446 | } 447 | } 448 | } 449 | 450 | /** 451 | * 挂载子节点 452 | * @param children 453 | * @param container 454 | * @param anchor 455 | * @param parentComponent 456 | * @param start 457 | */ 458 | const mountChildren: MountChildrenFn = (children, container, anchor, parentComponent, start = 0) => { 459 | for (let i = start; i < children.length; i++) { 460 | const child = normalizeVNode(children[i]) 461 | patch(null, child, container, anchor, parentComponent) 462 | } 463 | } 464 | 465 | /** 466 | * 处理Component 467 | * @param n1 468 | * @param n2 469 | * @param container 470 | * @param anchor 471 | * @param parentComponent 472 | */ 473 | function processComponent( 474 | n1: VNode | null, 475 | n2: VNode, 476 | container: RendererElement, 477 | anchor: RendererNode | null, 478 | parentComponent: ComponentInternalInstance | null, 479 | ) { 480 | if (n1 == null) 481 | mountComponent(n2, container, anchor, parentComponent) 482 | else 483 | updateComponent(n1, n2) 484 | } 485 | 486 | /** 487 | * 挂载Component 488 | * @param initialVNode 489 | * @param container 490 | * @param anchor 491 | * @param parentComponent 492 | */ 493 | function mountComponent( 494 | initialVNode: VNode, 495 | container: RendererElement, 496 | anchor: RendererNode | null, 497 | parentComponent: ComponentInternalInstance | null, 498 | ) { 499 | // 创建组件实例 500 | const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance( 501 | initialVNode, 502 | parentComponent, 503 | )) 504 | // 初始化组件 505 | setupComponent(instance) 506 | // 渲染组件 507 | setupRenderEffect(instance, initialVNode, container, anchor) 508 | } 509 | 510 | /** 511 | * 渲染组件 512 | * @param instance 513 | * @param initialVNode 514 | * @param container 515 | * @param anchor 516 | */ 517 | function setupRenderEffect( 518 | instance: ComponentInternalInstance, 519 | initialVNode: VNode, 520 | container: RendererElement, 521 | anchor: RendererNode | null, 522 | ) { 523 | const componentUpdateFn = () => { 524 | if (!instance.isMounted) { 525 | // 初始化 526 | // 取出代理,在后续绑定render函数 527 | const { proxy, bm, m } = instance 528 | 529 | // beforeMount Hook 530 | if (bm) 531 | invokeArrayFns(bm) 532 | 533 | // 执行render函数,获取返回的vnode 534 | const subTree = (instance.subTree = instance.render.call(proxy, proxy)) 535 | 536 | patch(null, subTree, container, anchor, instance) 537 | 538 | // 当全部组件挂载结束后,赋值el属性 539 | initialVNode.el = subTree.el 540 | 541 | // mounted Hook 542 | if (m) 543 | queuePostRenderEffect(m) 544 | 545 | instance.isMounted = true 546 | } 547 | else { 548 | // 更新 549 | const { proxy, next, vnode, bu, u } = instance 550 | if (next) { 551 | next.el = vnode.el 552 | updateComponentPreRender(instance, next) 553 | } 554 | 555 | if (bu) 556 | invokeArrayFns(bu) 557 | 558 | const nextTree = instance.render.call(proxy, proxy) 559 | 560 | const prevTree = instance.subTree 561 | instance.subTree = nextTree 562 | 563 | patch(prevTree, nextTree, container, anchor, instance) 564 | 565 | if (u) 566 | queuePostRenderEffect(u) 567 | } 568 | } 569 | 570 | const effect = (instance.update = new ReactiveEffect( 571 | componentUpdateFn, 572 | () => queueJob(instance.update), 573 | )) 574 | const update = (instance.update = effect.run.bind(effect) as SchedulerJob) 575 | update() 576 | } 577 | 578 | function updateComponent(n1: VNode, n2: VNode) { 579 | const instance = (n2.component = n1.component)! 580 | if (shouldUpdateComponent(n1, n2)) { 581 | instance.next = n2 582 | instance.update() 583 | } 584 | else { 585 | n2.component = n1.component 586 | n2.el = n1.el 587 | instance.vnode = n2 588 | } 589 | } 590 | 591 | function updateComponentPreRender(instance: ComponentInternalInstance, nextVNode: VNode) { 592 | const { props } = nextVNode 593 | // 更新组件props 594 | instance.props = props || EMPTY_OBJ 595 | } 596 | 597 | return { 598 | render, 599 | createApp: createAppAPI(render), 600 | } 601 | } 602 | 603 | /** 604 | * 求最长递增子序列在原数组的下标数组 605 | * @param arr {number[]} 606 | * @return {number[]} 607 | */ 608 | function getSequence(arr: number[]): number[] { 609 | // 浅拷贝arr 610 | const _arr = arr.slice() 611 | const len = _arr.length 612 | // 存储最长递增子序列对应arr中下标 613 | const result = [0] 614 | 615 | for (let i = 0; i < len; i++) { 616 | const val = _arr[i] 617 | 618 | // 排除等于 0 的情况 619 | if (val !== 0) { 620 | /* 1. 贪心算法 */ 621 | 622 | // 获取result当前最大值的下标 623 | const j = result[result.length - 1] 624 | // 如果当前 val 大于当前递增子序列的最大值的时候,直接添加 625 | if (arr[j] < val) { 626 | _arr[i] = j // 保存上一次递增子序列最后一个值的索引 627 | result.push(i) 628 | continue 629 | } 630 | 631 | /* 2. 二分法 */ 632 | 633 | // 定义二分法查找区间 [left, right] 634 | let left = 0 635 | let right = result.length - 1 636 | while (left < right) { 637 | // 求中间值(向下取整) 638 | const mid = (left + right) >> 1 639 | if (arr[result[mid]] < val) 640 | left = mid + 1 641 | else 642 | right = mid 643 | } 644 | 645 | // 当前递增子序列按顺序找到第一个大于 val 的值,将其替换 646 | if (val < arr[result[left]]) { 647 | if (left > 0) { 648 | // 保存上一次递增子序列最后一个值的索引 649 | _arr[i] = result[left - 1] 650 | } 651 | 652 | // 此时有可能导致结果不正确,即 result[left + 1] < result[left] 653 | // 所以我们需要通过 _arr 来记录正常的结果 654 | result[left] = i 655 | } 656 | } 657 | } 658 | 659 | // 修正贪心算法可能造成最长递增子序列在原数组里不是正确的顺序 660 | let len2 = result.length 661 | let idx = result[len2 - 1] 662 | // 倒序回溯,通过之前 _arr 记录的上一次递增子序列最后一个值的索引 663 | // 进而找到最终正确的索引 664 | while (len2-- > 0) { 665 | result[len2] = idx 666 | idx = _arr[idx] 667 | } 668 | 669 | return result 670 | } 671 | -------------------------------------------------------------------------------- /packages/runtime-core/src/scheduler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 异步更新机制 3 | * 4 | * 在vue3中分为三种任务:job、pre、post;执行顺序为pre、job、post; 5 | * - pre任务常用于watch api(watch api也可以自定义为其他任务类型,默认为pre) 6 | * - job任务常用于组件更新 7 | * - post任务常用于组件生命周期hook函数 8 | */ 9 | 10 | import { isArray } from 'shared/index' 11 | 12 | export type SchedulerJobs = SchedulerJob | SchedulerJob[] 13 | 14 | export interface SchedulerJob extends Function { 15 | id?: number 16 | } 17 | 18 | // 判断是否正在执行异步任务 19 | let isFlushing = false 20 | // 判断是否预备执行异步任务,也就是将异步任务加入异步队列中 21 | let isFlushPending = false 22 | 23 | // job类型任务队列 24 | const queue: SchedulerJob[] = [] 25 | // 正在执行的job任务下标 26 | let flushIndex = 0 27 | 28 | // 等待处理的Pre任务队列 29 | const pendingPreFlushCbs: SchedulerJob[] = [] 30 | // 正在异步执行的Pre任务队列 31 | let activePreFlushCbs: SchedulerJob[] | null = null 32 | // 当前正在执行的Pre任务下标 33 | let preFlushIndex = 0 34 | 35 | // 等待处理的Post任务队列 36 | const pendingPostFlushCbs: SchedulerJob[] = [] 37 | // 正在异步执行的Post任务队列 38 | let activePostFlushCbs: SchedulerJob[] | null = null 39 | // 当前正在执行的Post任务下标 40 | let postFlushIndex = 0 41 | 42 | // 默认Promise 43 | const resolvedPromise: Promise = Promise.resolve() 44 | // 当前执行的异步队列Promise 45 | let currentFlushPromise: Promise | null = null 46 | 47 | const getId = (job: SchedulerJob): number => (job.id == null ? Infinity : job.id) 48 | 49 | /** 50 | * 在下一个异步周期执行fn 51 | * @param fn 52 | */ 53 | export function nextTick(fn?: (this: T) => void) { 54 | // 获取Promise 55 | // 如当前正在执行异步操作,可以直接使用当前异步操作的Promise 56 | const p = currentFlushPromise || resolvedPromise 57 | return fn ? p.then(fn) : p 58 | } 59 | 60 | /** 61 | * 插入队列 62 | * @param cb 63 | * @param activeQueue 64 | * @param pendingQueue 65 | * @param index 66 | */ 67 | function queueCb(cb: SchedulerJobs, activeQueue: SchedulerJob[] | null, pendingQueue: SchedulerJob[], index: number) { 68 | if (!isArray(cb)) { 69 | // 判断是否存在同样的cb,不存在再插入 70 | if (!activeQueue || !activeQueue.includes(cb, index)) 71 | pendingQueue.push(cb) 72 | } 73 | else { 74 | pendingQueue.push(...cb) 75 | } 76 | // 进行冲洗队列 77 | queueFlush() 78 | } 79 | 80 | /** 81 | * 添加job类型任务 82 | * @param job 83 | */ 84 | export function queueJob(job: SchedulerJob) { 85 | // 如果queue队列为空,直接插入队列 86 | // 如果queue有值,判断未执行的任务是否有相同的,没有则插入队列 87 | if (!queue.length || !queue.includes(job, flushIndex)) 88 | queue.push(job) 89 | 90 | // 进行冲洗队列 91 | queueFlush() 92 | } 93 | 94 | /** 95 | * 添加pre类型任务 96 | * @param cb 97 | */ 98 | export function queuePreFlushCb(cb: SchedulerJob) { 99 | queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex) 100 | } 101 | 102 | /** 103 | * 添加post类型任务 104 | * @param cb 105 | */ 106 | export function queuePostFlushCb(cb: SchedulerJobs) { 107 | queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex) 108 | } 109 | 110 | /** 111 | * 冲洗队列 112 | */ 113 | function queueFlush() { 114 | // 判断是否正在冲洗队列或执行异步操作 115 | if (!isFlushing && !isFlushPending) { 116 | isFlushPending = true 117 | // 将冲洗任务加入异步队列 118 | currentFlushPromise = resolvedPromise.then(flushJobs) 119 | } 120 | } 121 | 122 | /** 123 | * 冲洗任务 124 | */ 125 | function flushJobs() { 126 | // 修改状态 127 | isFlushPending = false 128 | isFlushing = true 129 | // 冲洗Pre任务 130 | flushPreFlushCbs() 131 | 132 | // 冲洗job任务 133 | // 将queue队列根据id排序 134 | queue.sort((a, b) => getId(a) - getId(b)) 135 | try { 136 | // 遍历queue队列,一一执行 137 | for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { 138 | const job = queue[flushIndex] 139 | job() 140 | } 141 | } 142 | finally { 143 | // 重置Job队列 144 | flushIndex = 0 145 | queue.length = 0 146 | 147 | // 冲洗Post任务 148 | flushPostFlushCbs() 149 | 150 | // 重置 151 | isFlushing = false 152 | currentFlushPromise = null 153 | 154 | // 最后判断在本次异步执行过程中队列中是否会有新的值 155 | // 是的话重新调用flushJobs进行冲洗 156 | if (queue.length || pendingPreFlushCbs.length || pendingPostFlushCbs.length) 157 | flushJobs() 158 | } 159 | } 160 | 161 | /** 162 | * 冲洗执行Pre任务 163 | */ 164 | export function flushPreFlushCbs() { 165 | if (pendingPreFlushCbs.length) { 166 | // 将pendingPreFlushCbs队列更新到activePreFlushCbs队列,并去重 167 | activePreFlushCbs = [...new Set(pendingPreFlushCbs)] 168 | // 清空pendingPreFlushCbs 169 | pendingPreFlushCbs.length = 0 170 | 171 | // 遍历activePreFlushCbs,一一调用执行 172 | for (preFlushIndex = 0; preFlushIndex < activePreFlushCbs.length; preFlushIndex++) 173 | activePreFlushCbs[preFlushIndex]() 174 | 175 | // 重置 176 | activePreFlushCbs = null 177 | preFlushIndex = 0 178 | 179 | // 自调用,以防在本次执行过程pendingPreFlushCbs有新值 180 | // 为了确保pre任务在job任务之前执行 181 | flushPreFlushCbs() 182 | } 183 | } 184 | 185 | /** 186 | * 冲洗Post任务 187 | */ 188 | export function flushPostFlushCbs() { 189 | if (pendingPostFlushCbs.length) { 190 | // 将pendingPostFlushCbs赋值给deduped并进行去重 191 | const deduped = [...new Set(pendingPostFlushCbs)] 192 | // 清空pendingPostFlushCbs 193 | pendingPostFlushCbs.length = 0 194 | 195 | if (activePostFlushCbs) { 196 | // 如果当前activePostFlushCbs有值的话,则代表正在进行冲洗Post任务 197 | // 因此直接将deduped插入activePostFlushCbs队列即可 198 | activePostFlushCbs.push(...deduped) 199 | return 200 | } 201 | 202 | // 赋值activePostFlushCbs 203 | activePostFlushCbs = deduped 204 | // 根据id进行排序 205 | activePostFlushCbs.sort((a, b) => getId(a) - getId(b)) 206 | 207 | // 遍历执行 208 | for (postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++) 209 | activePostFlushCbs[postFlushIndex]() 210 | 211 | // 重置 212 | activePostFlushCbs = null 213 | postFlushIndex = 0 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /packages/runtime-core/src/vnode.ts: -------------------------------------------------------------------------------- 1 | import type { Component, ComponentInternalInstance, Data } from 'runtime-core/component' 2 | import type { Ref } from 'reactivity/ref' 3 | import { ShapeFlags, isArray, isObject, isString } from 'shared/index' 4 | import type { RendererElement, RendererNode } from './renderer' 5 | 6 | export const Fragment = Symbol('Fragment') 7 | export const Text = Symbol('Text') 8 | 9 | export type VNodeTypes = string | VNode | Component 10 | 11 | export type VNodeRef = string | Ref 12 | 13 | export interface VNodeProps { 14 | key?: string | number | symbol 15 | ref?: VNodeRef 16 | } 17 | 18 | export interface VNode< 19 | HostNode = RendererNode, 20 | HostElement = RendererElement, 21 | ExtraProps = Record, 22 | > { 23 | type: VNodeTypes 24 | props: (VNodeProps & ExtraProps) | null 25 | children: VNodeNormalizedChildren 26 | key: string | number | symbol | null 27 | 28 | // Component 29 | component: ComponentInternalInstance | null 30 | 31 | // DOM 32 | el: HostNode | null 33 | 34 | shapeFlag: number 35 | // patchFlag: number 36 | } 37 | 38 | type VNodeChildAtom = VNode | string | number | boolean | null | undefined | void 39 | 40 | export type VNodeArrayChildren = Array 41 | 42 | export type VNodeChild = VNodeChildAtom | VNodeArrayChildren 43 | 44 | export type VNodeNormalizedChildren = string | VNodeArrayChildren | null 45 | 46 | /** 47 | * 创建vnode 48 | * @param type 49 | * @param props 50 | * @param children 51 | */ 52 | export function createVNode( 53 | type: VNodeTypes, 54 | props: (Data & VNodeProps) | null = null, 55 | children: unknown = null, 56 | ): VNode { 57 | const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : ShapeFlags.COMPONENT 58 | 59 | return createBaseVNode(type, props, children, 0, null, shapeFlag) 60 | } 61 | 62 | const normalizeKey = ({ key }: VNodeProps): VNode['key'] => (key != null ? key : null) 63 | 64 | function createBaseVNode( 65 | type: VNodeTypes, 66 | props: (Data & VNodeProps) | null = null, 67 | children: unknown = null, 68 | patchFlag = 0, 69 | dynamicProps: string[] | null = null, 70 | shapeFlag = 0, 71 | ): VNode { 72 | const vnode = { 73 | type, 74 | props, 75 | children, 76 | shapeFlag, 77 | component: null, 78 | el: null, 79 | key: props && normalizeKey(props), 80 | } as VNode 81 | 82 | if (children) { 83 | // |= 按位运算符 84 | vnode.shapeFlag |= isString(children) ? ShapeFlags.TEXT_CHILDREN : ShapeFlags.ARRAY_CHILDREN 85 | 86 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isObject(children)) 87 | vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN 88 | } 89 | 90 | return vnode 91 | } 92 | 93 | export function createTextVNode(text = ' ') { 94 | return createVNode(Text, {}, text) 95 | } 96 | 97 | export function isSameVNodeType(n1: VNode, n2: VNode): boolean { 98 | return n1.type === n2.type && n1.key === n2.key 99 | } 100 | 101 | export { createVNode as createElementVNode } 102 | 103 | export function normalizeVNode(child: VNodeChild): VNode { 104 | if (child == null || typeof child === 'boolean') { 105 | return createVNode(Comment) 106 | } 107 | else if (isArray(child)) { 108 | return createVNode( 109 | Fragment, 110 | null, 111 | child.slice(), 112 | ) 113 | } 114 | else if (typeof child === 'object') { 115 | return child 116 | } 117 | else { 118 | return createVNode(Text, null, String(child)) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createRenderer } from 'runtime-core/index' 2 | import { extend } from 'shared/index' 3 | 4 | import { nodeOps } from './nodeOps' 5 | import { patchProp } from './patchProp' 6 | 7 | const rendererOptions = extend({ patchProp }, nodeOps) 8 | 9 | const renderer = createRenderer(rendererOptions) 10 | 11 | export const createApp = (...args) => renderer.createApp(...args) 12 | 13 | export * from 'runtime-core/index' 14 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/nodeOps.ts: -------------------------------------------------------------------------------- 1 | import type { RendererOptions } from 'runtime-core/index' 2 | 3 | const doc = (typeof document !== 'undefined' ? document : null) as Document 4 | 5 | export const nodeOps: Omit, 'patchProp'> = { 6 | insert: (child, parent, anchor) => { 7 | parent.insertBefore(child, anchor || null) 8 | }, 9 | 10 | createElement: (tag: string): Element => { 11 | const el = doc.createElement(tag) 12 | 13 | return el 14 | }, 15 | 16 | remove: (child: Node) => { 17 | const parent = child.parentNode 18 | if (parent) 19 | parent.removeChild(child) 20 | }, 21 | 22 | setElementText: (el, text) => { 23 | el.textContent = text 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/patchProp.ts: -------------------------------------------------------------------------------- 1 | import type { RendererOptions } from 'runtime-core/index' 2 | import { isOn } from 'shared/index' 3 | 4 | type DOMRendererOptions = RendererOptions 5 | 6 | export const patchProp: DOMRendererOptions['patchProp'] = (el, key, prevValue, nextValue) => { 7 | if (isOn(key)) { 8 | const event = key.slice(2).toLocaleLowerCase() 9 | el.addEventListener(event, nextValue) 10 | } 11 | else { 12 | if (nextValue === undefined || nextValue === null) 13 | el.removeAttribute(key) 14 | else 15 | el.setAttribute(key, nextValue) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './shapeFlags' 2 | export * from './toDisplayString' 3 | 4 | const onRE = /^on[^a-z]/ 5 | export const isOn = (key: string) => onRE.test(key) 6 | 7 | // 将所有可枚举属性的值从一个或多个源对象分配到目标对象 8 | export const extend = Object.assign 9 | 10 | export const objectToString = Object.prototype.toString 11 | export const toTypeString = (value: unknown): string => objectToString.call(value) 12 | 13 | // 判断是否为字符串 14 | export const isString = (val: unknown): val is string => typeof val === 'string' 15 | // 判断是否为符号 16 | export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol' 17 | // 判断是否为数组 18 | export const isArray = Array.isArray 19 | // 判断是否为函数 20 | export const isFunction = (val: unknown): val is Function => typeof val === 'function' 21 | // 判断是否为对象 22 | export const isObject = (val: unknown): val is Record => val !== null && typeof val === 'object' 23 | export const isPlainObject = (val: unknown): val is object => toTypeString(val) === '[object Object]' 24 | // 判断对象是否存在该属性 25 | const hasOwnProperty = Object.prototype.hasOwnProperty 26 | export const hasOwn = (val: object, key: string | symbol): key is keyof typeof val => hasOwnProperty.call(val, key) 27 | 28 | // 判断两个值是否存在变动 29 | export const hasChanged = (value: any, oldValue: any): boolean => !Object.is(value, oldValue) 30 | 31 | // 空函数 32 | export const NOOP = () => {} 33 | // 空对象 34 | export const EMPTY_OBJ = {} 35 | // 空对象 36 | export const EMPTY_ARR = [] 37 | 38 | /** 39 | * 利用空间优化执行实行的工具函数 40 | * @param fn 41 | */ 42 | const cacheStringFunction = string>(fn: T): T => { 43 | // 缓存空间 44 | const cache: Record = Object.create(null) 45 | 46 | return ((str: string) => { 47 | // 先查看缓存是否存在,不存在则调用函数 48 | const hit = cache[str] 49 | return hit || (cache[str] = fn(str)) 50 | }) as any 51 | } 52 | 53 | const camelizeRE = /-(\w)/g 54 | /** 55 | * 将中划线命名法转成驼峰命名法 56 | * @desc hello-world -> helloWorld 57 | * @private 58 | */ 59 | export const camelize = cacheStringFunction((str: string): string => { 60 | return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : '')) 61 | }) 62 | 63 | /** 64 | * 将str首字母大写 65 | * @private 66 | */ 67 | export const capitalize = cacheStringFunction((str: string) => str.charAt(0).toUpperCase() + str.slice(1)) 68 | 69 | /** 70 | * 处理为事件Key 71 | * @private 72 | */ 73 | export const toHandlerKey = cacheStringFunction((str: string) => (str ? `on${capitalize(str)}` : '')) 74 | 75 | /** 76 | * 执行函数数组 77 | * @param fns 78 | * @param arg 79 | */ 80 | export const invokeArrayFns = (fns: Function[], arg?: any) => { 81 | for (let i = 0; i < fns.length; i++) 82 | fns[i](arg) 83 | } 84 | -------------------------------------------------------------------------------- /packages/shared/src/shapeFlags.ts: -------------------------------------------------------------------------------- 1 | export const enum ShapeFlags { 2 | ELEMENT = 1 /* 00 0000 0001 */, 3 | FUNCTIONAL_COMPONENT = 1 << 1 /* 00 0000 0010 */, 4 | STATEFUL_COMPONENT = 1 << 2 /* 00 0000 0100 */, 5 | TEXT_CHILDREN = 1 << 3 /* 00 0000 1000 */, 6 | ARRAY_CHILDREN = 1 << 4 /* 00 0001 0000 */, 7 | SLOTS_CHILDREN = 1 << 5 /* 00 0010 0000 */, 8 | TELEPORT = 1 << 6 /* 00 0100 0000 */, 9 | SUSPENSE = 1 << 7 /* 00 1000 0000 */, 10 | COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8 /* 01 0000 0000 */, 11 | COMPONENT_KEPT_ALIVE = 1 << 9 /* 10 0000 0000 */, 12 | COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT, 13 | } 14 | -------------------------------------------------------------------------------- /packages/shared/src/toDisplayString.ts: -------------------------------------------------------------------------------- 1 | export const toDisplayString = (val: unknown): string => { 2 | return String(val) 3 | } 4 | -------------------------------------------------------------------------------- /packages/vue/examples/complie-base/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/examples/complie-base/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/mini-vue.esm.js' 2 | 3 | const App = { 4 | name: 'App', 5 | template: '
Hello, {{message}}
', 6 | setup() { 7 | return { 8 | message: 'world', 9 | } 10 | }, 11 | } 12 | 13 | const rootContainer = document.querySelector('#app') 14 | createApp(App).mount(rootContainer) 15 | -------------------------------------------------------------------------------- /packages/vue/examples/components/app.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/mini-vue.esm.js' 2 | import { Foo } from './foo.js' 3 | 4 | export const App = { 5 | render() { 6 | return h('div', { id: 'root' }, [ 7 | h('h1', null, 'Components'), 8 | h(Foo, { 9 | msg: 'HelloWorld', 10 | onClickHandle(test) { 11 | console.log(`事件绑定: ${test}`) 12 | }, 13 | }), 14 | ]) 15 | }, 16 | setup() {}, 17 | } 18 | -------------------------------------------------------------------------------- /packages/vue/examples/components/foo.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/mini-vue.esm.js' 2 | 3 | export const Foo = { 4 | render() { 5 | return h('div', null, [ 6 | h('h3', null, 'Foo Comp'), 7 | h('p', null, `props.msg = ${this.msg}`), 8 | h( 9 | 'button', 10 | { 11 | onClick: this.clickHandle, 12 | }, 13 | 'button', 14 | ), 15 | ]) 16 | }, 17 | setup(props, { emit }) { 18 | console.log('props', props) 19 | 20 | // props 只读 21 | props.msg = 'Hi World' 22 | 23 | const clickHandle = () => { 24 | console.log('emit', emit) 25 | emit('clickHandle', 'test1') 26 | emit('click-handle', 'test2') 27 | } 28 | 29 | return { 30 | clickHandle, 31 | } 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /packages/vue/examples/components/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/examples/components/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/mini-vue.esm.js' 2 | import { App } from './app.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | createApp(App).mount(rootContainer) 6 | -------------------------------------------------------------------------------- /packages/vue/examples/helloworld/app.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/mini-vue.esm.js' 2 | 3 | window.self = null 4 | 5 | export const App = { 6 | render() { 7 | window.self = this 8 | return h( 9 | 'div', 10 | { 11 | id: 'root', 12 | class: ['pages'], 13 | }, 14 | [ 15 | h('h1', { class: 'text-1' }, 'Hello World'), 16 | h('h3', { style: 'color: #666' }, this.msg), 17 | h('input', { 18 | placeholder: 'input something', 19 | onInput(e) { 20 | console.log('input keywords: ', e.target.value) 21 | }, 22 | }), 23 | h( 24 | 'button', 25 | { 26 | onClick() { 27 | console.log('click events') 28 | }, 29 | }, 30 | 'test button', 31 | ), 32 | ], 33 | ) 34 | }, 35 | setup() { 36 | return { 37 | msg: 'This is mini-vue', 38 | } 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /packages/vue/examples/helloworld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HelloWorld 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/examples/helloworld/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/mini-vue.esm.js' 2 | import { App } from './app.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | createApp(App).mount(rootContainer) 6 | -------------------------------------------------------------------------------- /packages/vue/examples/lifecycle/app.js: -------------------------------------------------------------------------------- 1 | import { h, nextTick, onBeforeMount, onBeforeUpdate, onMounted, onUpdated, ref } from '../../dist/mini-vue.esm.js' 2 | 3 | export const App = { 4 | render() { 5 | window.self = this 6 | return h( 7 | 'div', 8 | { 9 | id: 'root', 10 | class: ['pages'], 11 | }, 12 | [h('h1', null, 'LifeCycle'), h('h4', null, this.msg)], 13 | ) 14 | }, 15 | setup() { 16 | const msg = ref('') 17 | 18 | onBeforeMount(() => { 19 | console.log('onBeforeMount') 20 | }) 21 | 22 | onMounted(() => { 23 | nextTick(() => { 24 | console.log('nextTick') 25 | }) 26 | console.log('onMounted') 27 | setTimeout(() => { 28 | msg.value = 'HelloWorld' 29 | }, 1000) 30 | }) 31 | 32 | onBeforeUpdate(() => { 33 | console.log('onBeforeUpdate') 34 | }) 35 | 36 | onUpdated(() => { 37 | console.log('onUpdated') 38 | }) 39 | 40 | return { 41 | msg, 42 | } 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /packages/vue/examples/lifecycle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LifeCycle 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/examples/lifecycle/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/mini-vue.esm.js' 2 | import { App } from './app.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | createApp(App).mount(rootContainer) 6 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/ArrayToArray.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/mini-vue.esm.js' 2 | 3 | const demos = [ 4 | // AB -> ABC 5 | { 6 | prevChildren: [h('span', { key: 'A' }, 'A'), h('span', { key: 'B' }, 'B')], 7 | nextChildren: [h('span', { key: 'A' }, 'A'), h('span', { key: 'B' }, 'B'), h('span', { key: 'C' }, 'C')], 8 | }, 9 | // BC -> ABC 10 | { 11 | prevChildren: [h('span', { key: 'B' }, 'B'), h('span', { key: 'C' }, 'C')], 12 | nextChildren: [h('span', { key: 'A' }, 'A'), h('span', { key: 'B' }, 'B'), h('span', { key: 'C' }, 'C')], 13 | }, 14 | 15 | // ABC -> AB 16 | { 17 | prevChildren: [h('span', { key: 'A' }, 'A'), h('span', { key: 'B' }, 'B'), h('span', { key: 'C' }, 'C')], 18 | nextChildren: [h('span', { key: 'A' }, 'A'), h('span', { key: 'B' }, 'B')], 19 | }, 20 | 21 | // ABC -> BC 22 | { 23 | prevChildren: [h('span', { key: 'A' }, 'A'), h('span', { key: 'B' }, 'B'), h('span', { key: 'C' }, 'C')], 24 | nextChildren: [h('span', { key: 'B' }, 'B'), h('span', { key: 'C' }, 'C')], 25 | }, 26 | 27 | // ABCEDFG -> ABECFG 28 | { 29 | prevChildren: [ 30 | h('span', { key: 'A' }, 'A'), 31 | h('span', { key: 'B' }, 'B'), 32 | h('span', { key: 'C' }, 'C'), 33 | h('span', { key: 'E' }, 'E'), 34 | h('span', { key: 'D' }, 'D'), 35 | h('span', { key: 'F' }, 'F'), 36 | h('span', { key: 'G' }, 'G'), 37 | ], 38 | nextChildren: [ 39 | h('span', { key: 'A' }, 'A'), 40 | h('span', { key: 'B' }, 'B'), 41 | h('span', { key: 'E' }, 'E'), 42 | h('span', { key: 'C' }, 'C'), 43 | h('span', { key: 'F' }, 'F'), 44 | h('span', { key: 'G' }, 'G'), 45 | ], 46 | }, 47 | ] 48 | 49 | const generateArrayTpArrayComps = (idx) => { 50 | return { 51 | setup() {}, 52 | render() { 53 | const children = demos[idx] 54 | 55 | return this.isChange ? h('div', null, children.nextChildren) : h('div', null, children.prevChildren) 56 | }, 57 | } 58 | } 59 | 60 | export default generateArrayTpArrayComps 61 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/ArrayToText.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/mini-vue.esm.js' 2 | 3 | const nextChildren = 'new Children' 4 | const prevChildren = [h('div', null, 'A'), h('div', null, 'B')] 5 | 6 | export default { 7 | setup() {}, 8 | render() { 9 | return this.isChange ? h('div', null, nextChildren) : h('div', null, prevChildren) 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/TextToArray.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/mini-vue.esm.js' 2 | 3 | const nextChildren = [h('div', null, 'A'), h('div', null, 'B')] 4 | const prevChildren = 'old Children' 5 | 6 | export default { 7 | setup() {}, 8 | render() { 9 | return this.isChange ? h('div', null, nextChildren) : h('div', null, prevChildren) 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/TextToText.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/mini-vue.esm.js' 2 | 3 | const nextChildren = 'new Children' 4 | const prevChildren = 'old Children' 5 | 6 | export default { 7 | setup() {}, 8 | render() { 9 | return this.isChange ? h('div', null, nextChildren) : h('div', null, prevChildren) 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/app.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../dist/mini-vue.esm.js' 2 | import ArrayToText from './ArrayToText.js' 3 | import TextToText from './TextToText.js' 4 | import TextToArray from './TextToArray.js' 5 | import generateArrayTpArrayComps from './ArrayToArray.js' 6 | 7 | export const App = { 8 | render() { 9 | return h('div', { id: 'root' }, [ 10 | h('h1', null, 'Patch Children'), 11 | 12 | h('h3', null, 'ArrayToText'), 13 | h(ArrayToText, { isChange: this.isChange }), 14 | 15 | h('h3', null, 'TextToText'), 16 | h(TextToText, { isChange: this.isChange }), 17 | 18 | h('h3', null, 'TextToArray'), 19 | h(TextToArray, { isChange: this.isChange }), 20 | 21 | h('h3', null, 'ArrayToArray'), 22 | h('h4', null, 'AB -> ABC'), 23 | h(generateArrayTpArrayComps(0), { isChange: this.isChange }), 24 | h('h4', null, 'BC -> ABC'), 25 | h(generateArrayTpArrayComps(1), { isChange: this.isChange }), 26 | h('h4', null, 'ABC -> AB'), 27 | h(generateArrayTpArrayComps(2), { isChange: this.isChange }), 28 | h('h4', null, 'ABC -> BC'), 29 | h(generateArrayTpArrayComps(3), { isChange: this.isChange }), 30 | h('h4', null, 'ABCEDFG -> ABECFG'), 31 | h(generateArrayTpArrayComps(4), { isChange: this.isChange }), 32 | 33 | h( 34 | 'button', 35 | { 36 | onClick: this.changeHandle, 37 | }, 38 | 'update', 39 | ), 40 | ]) 41 | }, 42 | setup() { 43 | const isChange = ref(false) 44 | 45 | const changeHandle = () => (isChange.value = true) 46 | 47 | return { isChange, changeHandle } 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | patch Children 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/examples/patchChildren/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/mini-vue.esm.js' 2 | import { App } from './app.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | createApp(App).mount(rootContainer) 6 | -------------------------------------------------------------------------------- /packages/vue/examples/provide/app.js: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance, h, provide } from '../../dist/mini-vue.esm.js' 2 | import { Foo } from './foo.js' 3 | 4 | export const App = { 5 | render() { 6 | return h('div', { id: 'root' }, [h('h1', null, 'Components'), h(Foo)]) 7 | }, 8 | setup() { 9 | const instance = getCurrentInstance() 10 | console.log('app Instance: ', instance) 11 | 12 | provide('foo', 'fooVal') 13 | provide('baz', 'bazVal') 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /packages/vue/examples/provide/baz.js: -------------------------------------------------------------------------------- 1 | import { h, inject } from '../../dist/mini-vue.esm.js' 2 | 3 | export const Baz = { 4 | render() { 5 | return h('div', null, [ 6 | h('h4', null, 'Baz Comp'), 7 | h('p', null, `inject foo: ${this.foo}`), 8 | h('p', null, `inject baz: ${this.baz}`), 9 | h('p', null, `inject bar: ${this.bar}`), 10 | ]) 11 | }, 12 | setup() { 13 | const foo = inject('foo') 14 | const baz = inject('baz') 15 | console.log('inject foo: ', foo) 16 | console.log('inject baz: ', baz) 17 | 18 | const bar = inject('bar', function() { 19 | return this.foo 20 | }) 21 | console.log(`inject bar: ${bar}`) 22 | 23 | return { 24 | foo, 25 | baz, 26 | bar, 27 | } 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /packages/vue/examples/provide/foo.js: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance, h, inject, provide } from '../../dist/mini-vue.esm.js' 2 | import { Baz } from './baz.js' 3 | 4 | export const Foo = { 5 | render() { 6 | return h('div', null, [ 7 | h('h3', null, 'Foo Comp'), 8 | h('p', null, `inject foo: ${this.foo}`), 9 | h('p', null, `inject bar: ${this.bar}`), 10 | h(Baz), 11 | ]) 12 | }, 13 | setup() { 14 | const instance = getCurrentInstance() 15 | console.log('Foo Instance: ', instance) 16 | 17 | const foo = inject('foo') 18 | console.log('inject foo', foo) 19 | 20 | // 设置默认值 21 | const bar = inject('bar', 'barValue') 22 | 23 | provide('foo', 'fooValue2') 24 | 25 | return { 26 | foo, 27 | bar, 28 | } 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /packages/vue/examples/provide/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/examples/provide/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/mini-vue.esm.js' 2 | import { App } from './app.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | createApp(App).mount(rootContainer) 6 | -------------------------------------------------------------------------------- /packages/vue/examples/slots/app.js: -------------------------------------------------------------------------------- 1 | import { createTextVNode, h } from '../../dist/mini-vue.esm.js' 2 | import { Foo } from './foo.js' 3 | 4 | export const App = { 5 | render() { 6 | return h('div', { id: 'root' }, [ 7 | h('h1', null, 'Slots'), 8 | h(Foo, null, { 9 | header: () => h('div', null, 'Slot Header'), 10 | footer: () => h('div', null, 'Slot Footer'), 11 | default: props => [createTextVNode('Slot Content'), h('p', null, `props.msg:${props.msg}`)], 12 | }), 13 | ]) 14 | }, 15 | setup() {}, 16 | } 17 | -------------------------------------------------------------------------------- /packages/vue/examples/slots/foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlot } from '../../dist/mini-vue.esm.js' 2 | 3 | export const Foo = { 4 | render() { 5 | return h('div', null, [ 6 | // 插槽 7 | renderSlot(this.$slots, 'header'), 8 | renderSlot(this.$slots, 'default', { msg: 'HelloWorld' }), 9 | renderSlot(this.$slots, 'footer'), 10 | ]) 11 | }, 12 | setup() {}, 13 | } 14 | -------------------------------------------------------------------------------- /packages/vue/examples/slots/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/examples/slots/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/mini-vue.esm.js' 2 | import { App } from './app.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | createApp(App).mount(rootContainer) 6 | -------------------------------------------------------------------------------- /packages/vue/examples/update/app.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../dist/mini-vue.esm.js' 2 | 3 | export const App = { 4 | render() { 5 | console.log(this.props) 6 | return h( 7 | 'div', 8 | { 9 | ...this.props, 10 | }, 11 | [ 12 | h('h1', null, 'Update'), 13 | h('h3', null, 'Update Children'), 14 | h('p', null, `count: ${this.count}`), 15 | h( 16 | 'button', 17 | { 18 | onClick: this.addHandle, 19 | }, 20 | 'add', 21 | ), 22 | 23 | h('h3', null, 'Update Props'), 24 | h('p', null, 'HelloWorld'), 25 | h( 26 | 'button', 27 | { 28 | onClick: this.changePropsStyle, 29 | }, 30 | 'changePropsStyle', 31 | ), 32 | h( 33 | 'button', 34 | { 35 | onClick: this.clearPropsClass, 36 | }, 37 | 'clearPropsClass', 38 | ), 39 | h( 40 | 'button', 41 | { 42 | onClick: this.resetProps, 43 | }, 44 | 'resetProps', 45 | ), 46 | ], 47 | ) 48 | }, 49 | setup() { 50 | const count = ref(0) 51 | const addHandle = () => { 52 | count.value++ 53 | console.log(`count: ${count.value}`) 54 | } 55 | 56 | const props = ref({ 57 | style: 'color: red', 58 | class: 'underline', 59 | }) 60 | 61 | const changePropsStyle = () => (props.value.style = 'color: blue') 62 | const clearPropsClass = () => (props.value.class = undefined) 63 | const resetProps = () => (props.value = { style: 'color: red' }) 64 | 65 | return { 66 | count, 67 | props, 68 | addHandle, 69 | changePropsStyle, 70 | clearPropsClass, 71 | resetProps, 72 | } 73 | }, 74 | } 75 | -------------------------------------------------------------------------------- /packages/vue/examples/update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HelloWorld 6 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/vue/examples/update/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/mini-vue.esm.js' 2 | import { App } from './app.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | createApp(App).mount(rootContainer) 6 | -------------------------------------------------------------------------------- /packages/vue/examples/watch/app.js: -------------------------------------------------------------------------------- 1 | import { computed, h, reactive, ref, watch, watchEffect } from '../../dist/mini-vue.esm.js' 2 | 3 | window.self = null 4 | 5 | export const App = { 6 | render() { 7 | return h( 8 | 'div', 9 | { 10 | id: 'root', 11 | class: ['pages'], 12 | }, 13 | [ 14 | h('h1', null, 'Watch and Computed'), 15 | h('h3', null, `count: ${this.count}`), 16 | h('h3', null, `doubleCount: ${this.doubleCount}`), 17 | h('button', { onClick: this.add }, 'add'), 18 | ], 19 | ) 20 | }, 21 | setup() { 22 | const count = ref(0) 23 | const doubleCount = computed(() => 2 * count.value) 24 | 25 | const obj = reactive({ 26 | count: 0, 27 | }) 28 | 29 | const add = () => { 30 | count.value++ 31 | obj.count++ 32 | } 33 | 34 | watch(count, (val, oldVal) => { 35 | console.log('watch count: ', val, oldVal) 36 | }) 37 | 38 | watch(obj, (val, oldVal) => { 39 | console.log('watch obj: ', val, oldVal) 40 | }) 41 | 42 | watchEffect(() => { 43 | console.log('watchEffect: ', count.value) 44 | }) 45 | 46 | return { 47 | count, 48 | doubleCount, 49 | add, 50 | } 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /packages/vue/examples/watch/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HelloWorld 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/examples/watch/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/mini-vue.esm.js' 2 | import { App } from './app.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | createApp(App).mount(rootContainer) 6 | -------------------------------------------------------------------------------- /packages/vue/src/index.ts: -------------------------------------------------------------------------------- 1 | import { baseCompile } from 'compiler-core/index' 2 | import type { CompilerOptions } from 'compiler-core/options' 3 | import type { RenderFunction } from 'runtime-core/component' 4 | import { registerRuntimeCompiler } from 'runtime-core/component' 5 | import { isString } from 'shared/index' 6 | import * as runtimeDom from 'runtime-dom/index' 7 | 8 | function compileToFunction( 9 | template: string | HTMLElement, 10 | options?: CompilerOptions, 11 | ): RenderFunction { 12 | if (!isString(template)) 13 | template = template.innerHTML 14 | 15 | const { code } = baseCompile(template, options) 16 | 17 | // eslint-disable-next-line no-new-func 18 | const render = new Function('Vue', code)(runtimeDom) as RenderFunction 19 | 20 | return render 21 | } 22 | 23 | registerRuntimeCompiler(compileToFunction) 24 | 25 | export { compileToFunction as compile } 26 | export * from 'reactivity/index' 27 | export * from 'runtime-dom/index' 28 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript' 2 | 3 | export default { 4 | input: './packages/vue/src/index.ts', 5 | output: [ 6 | { 7 | format: 'cjs', 8 | file: './packages/vue/dist/mini-vue.cjs.js', 9 | }, 10 | { 11 | format: 'es', 12 | file: './packages/vue/dist/mini-vue.esm.js', 13 | }, 14 | ], 15 | plugins: [typescript()], 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", 8 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 9 | "module": "esnext", 10 | /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 11 | "lib": ["DOM", "es6", "es2016"], 12 | /* Specify library files to be included in the compilation. */ 13 | // "allowJs": true, /* Allow javascript files to be compiled. */ 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 16 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 17 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 18 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | // "outDir": "./", /* Redirect output structure to the directory. */ 21 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 22 | // "composite": true, /* Enable project compilation */ 23 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 24 | // "removeComments": true, /* Do not emit comments to output. */ 25 | // "noEmit": true, /* Do not emit outputs. */ 26 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 27 | "downlevelIteration": true, 28 | /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 29 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 30 | 31 | /* Strict Type-Checking Options */ 32 | "strict": true, 33 | /* Enable all strict type-checking options. */ 34 | "noImplicitAny": false, 35 | /* Raise error on expressions and declarations with an implied 'any' type. */ 36 | // "strictNullChecks": true, /* Enable strict null checks. */ 37 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 38 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 39 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 40 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 41 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 42 | 43 | /* Additional Checks */ 44 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 45 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 46 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 47 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 48 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 49 | 50 | /* Module Resolution Options */ 51 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 52 | "baseUrl": "." /* Base directory to resolve non-absolute module names. */, 53 | "paths": { 54 | /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 55 | "compiler-core/*": ["packages/compiler-core/src/*"], 56 | "runtime-core/*": ["packages/runtime-core/src/*"], 57 | "runtime-dom/*": ["packages/runtime-dom/src/*"], 58 | "shared/*": ["packages/shared/src/*"], 59 | "reactivity/*": ["packages/reactivity/src/*"] 60 | }, 61 | 62 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 63 | // "typeRoots": [], /* List of folders to include type definitions from. */ 64 | "types": ["jest"], 65 | /* Type declaration files to be included in compilation. */ 66 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 67 | "esModuleInterop": true, 68 | /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 69 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 70 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 71 | 72 | /* Source Map Options */ 73 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 74 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 75 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 76 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 77 | 78 | /* Experimental Options */ 79 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 80 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 81 | 82 | /* Advanced Options */ 83 | "skipLibCheck": true, 84 | /* Skip type checking of declaration files. */ 85 | "forceConsistentCasingInFileNames": true 86 | /* Disallow inconsistently-cased references to the same file. */ 87 | } 88 | } 89 | --------------------------------------------------------------------------------