├── .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 │ ├── index.js │ ├── package.json │ └── src │ │ ├── ast.ts │ │ ├── babelUtils.ts │ │ ├── codegen.ts │ │ ├── compile.ts │ │ ├── index.ts │ │ ├── parse.ts │ │ ├── runtimeHelpers.ts │ │ ├── transform.ts │ │ ├── transforms │ │ ├── hoistStatic.ts │ │ ├── transformElement.ts │ │ ├── transformExpression.ts │ │ ├── transformText.ts │ │ ├── vBind.ts │ │ ├── vFor.ts │ │ ├── vIf.ts │ │ ├── vModel.ts │ │ └── vOn.ts │ │ └── utils.ts ├── compiler-dom │ ├── index.js │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── runtimeHelpers.ts │ │ └── transform │ │ ├── vModel.ts │ │ └── vShow.ts ├── reactivity │ ├── __tests__ │ │ ├── computed.spec.ts │ │ ├── effect.spec.ts │ │ ├── mapAndSet.spec.ts │ │ ├── reactive.spec.ts │ │ ├── readonly.spec.ts │ │ └── ref.spec.ts │ ├── index.js │ ├── package.json │ └── src │ │ ├── baseHandlers.ts │ │ ├── collectionHandlers.ts │ │ ├── computed.ts │ │ ├── dep.ts │ │ ├── effect.ts │ │ ├── index.ts │ │ ├── operations.ts │ │ ├── reactive.ts │ │ └── ref.ts ├── runtime-core │ ├── __tests__ │ │ └── watch.spec.ts │ ├── index.js │ ├── package.json │ └── src │ │ ├── apiAsyncComponent.ts │ │ ├── apiCreateApp.ts │ │ ├── apiDefineComponent.ts │ │ ├── apiInject.ts │ │ ├── apiLifecycle.ts │ │ ├── apiWatch.ts │ │ ├── component.ts │ │ ├── component │ │ ├── BaseTransition.ts │ │ ├── KeepAlive.ts │ │ └── Teleport.ts │ │ ├── componentEmits.ts │ │ ├── componentOptions.ts │ │ ├── componentProps.ts │ │ ├── componentPublicInstance.ts │ │ ├── componentRenderContext.ts │ │ ├── componentRenderUtils.ts │ │ ├── componentSlots.ts │ │ ├── directives.ts │ │ ├── h.ts │ │ ├── helpers │ │ ├── renderList.ts │ │ ├── renderSlot.ts │ │ └── resolveAssets.ts │ │ ├── index.ts │ │ ├── renderer.ts │ │ ├── scheduler.ts │ │ └── vnode.ts ├── runtime-dom │ ├── __tests__ │ │ └── index.spec.ts │ ├── index.js │ ├── package.json │ └── src │ │ ├── components │ │ └── Transition.ts │ │ ├── directives │ │ ├── vModel.ts │ │ └── vShow.ts │ │ ├── index.ts │ │ ├── modules │ │ ├── attrs.ts │ │ ├── events.ts │ │ └── props.ts │ │ ├── nodeOps.ts │ │ └── patchProp.ts ├── shared │ ├── __tests__ │ │ └── index.spec.ts │ ├── index.js │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── normalizeProp.ts │ │ ├── patchFlags.ts │ │ ├── shapeFlags.ts │ │ └── toDisplayString.ts └── simplify-vue │ ├── example │ ├── asyncRender │ │ └── index.html │ ├── class │ │ └── index.html │ ├── commentVnode │ │ └── index.html │ ├── compileTemplate │ │ ├── component.html │ │ ├── index.html │ │ ├── multipleRoot.html │ │ ├── v-for.html │ │ ├── v-if.html │ │ ├── v-model.html │ │ └── v-show.html │ ├── componentProps │ │ └── index.html │ ├── currentInstance │ │ └── index.html │ ├── customRenderer │ │ └── index.html │ ├── defineAsyncComponent │ │ ├── Child.js │ │ └── index.html │ ├── demo │ │ └── index.html │ ├── diff │ │ └── index.html │ ├── emit │ │ └── index.html │ ├── functionalComponent │ │ └── index.html │ ├── index.html │ ├── keep-alive │ │ └── index.html │ ├── lifecycle │ │ └── index.html │ ├── nextTick │ │ └── index.html │ ├── optionsApi │ │ └── index.html │ ├── patchChildren │ │ └── index.html │ ├── patchFlags │ │ ├── patchFlagClass.html │ │ ├── patchFlagProps.html │ │ ├── patchFlagText.html │ │ ├── patchFlagVShow.html │ │ ├── patchMultipleRoot.html │ │ ├── patchVFor.html │ │ └── patchVIf.html │ ├── patchProps │ │ └── index.html │ ├── provideAndInject │ │ └── index.html │ ├── setup │ │ └── index.html │ ├── slots │ │ └── index.html │ ├── teleport │ │ └── index.html │ ├── textNode │ │ └── index.html │ ├── transition │ │ └── index.html │ ├── updateComponent │ │ └── index.html │ └── watch │ │ └── index.html │ ├── package.json │ └── src │ └── index.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── rollup.config.js ├── scripts └── dev.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .DS_Store 3 | node_modules 4 | *.log 5 | .idea 6 | .history 7 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simplify-vue3 2 | 通过实现一个简易版的 vue3 源码来加深对框架的理解。 3 | 4 | ### 内容输出 5 | https://github.com/scout-hub/technology-blog/tree/dev/vue/vue3%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90 6 | 7 | ### 测试 8 | pnpm test 9 | 10 | ### 开发 11 | pnpm dev 12 | 13 | ### 生产 14 | pnpm build 15 | 16 | ### 目前实现的功能 17 | - reactivity 18 | - [x] reactive/shallowReactive/isReactive/toReactive 19 | - [x] readonly/shallowReadonly/isReadonly/toReadonly 20 | - [x] isProxy 21 | - [x] toRaw/markRaw 22 | - [x] Object类型的响应式处理 23 | - [x] Set、Map类型的响应式处理 24 | - [x] 数组方法的响应式处理 25 | - [x] computed及其相关属性 26 | - [x] effect 27 | - [x] ref/shallowRef/isRef/unref/toRef/toRefs/proxyRefs 28 | - [x] 等等 29 | - runtime-core 30 | - [x] createApp 31 | - [x] defineAsyncComponent 32 | - [x] Transition组件 33 | - [x] Teleport组件 34 | - [x] KeepAlive组件 35 | - [x] slot插槽、组件类型、element类型、Text类型、Fragment类型、Comment类型节点的渲染更新卸载 36 | - [x] customRender自定义渲染器 37 | - [x] provide/inject 38 | - [x] attrs/props初始化和更新、props类型校验 39 | - [x] emit事件派发/emits选项校验 40 | - [x] $el/$slots/$props/$attrs/$data 41 | - [x] 部分指令(v-show,v-model) 42 | - [x] 生命周期 43 | - [x] nextTick 44 | - [x] watchEffect/watch/watchSyncEffect/watchPostEffect 45 | - [x] render函数 46 | - [x] getCurrentInstance 47 | - [x] 快速diff算法 48 | - [x] patchFlag靶向更新文本节点、class属性、其他动态props、children节点 49 | - [x] block树 50 | - [x] 等等 51 | - runtime-dom 52 | - [x] 属性绑定 53 | - [x] 事件处理 54 | - compiler-core 55 | - [x] 解析插值、文本、元素、组件 56 | - [x] 插值、文本、元素、组件组合(支持嵌套)的template编译、转化、生成 57 | - [x] 普通元素v-show、v-if/v-else-if/v-else、v-bind、v-on、v-for、v-model 58 | - [x] template下多根标签节点渲染 59 | - [x] patchFlag: 动态文本节点标记,动态class属性标记,动态props属性标记,指令标记(v-show) 60 | - [x] block标记:v-if、v-for等等 61 | - shared 62 | - [x] 辅助函数 63 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-20 20:39:29 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-03-20 20:59:37 6 | */ 7 | module.exports = { 8 | presets: [ 9 | ['@babel/preset-env', { 10 | targets: { 11 | node: 'current' 12 | } 13 | }], '@babel/preset-typescript' 14 | ], 15 | }; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-30 21:20:41 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-30 21:26:52 6 | */ 7 | module.exports = { 8 | watchPathIgnorePatterns: ['/node_modules/', '/dist/'], 9 | moduleFileExtensions: ['ts', 'js'], 10 | moduleNameMapper: { 11 | '^@simplify-vue/(.*?)$': '/packages/$1/src', 12 | }, 13 | testMatch: ['/packages/**/__tests__/**/*spec.[jt]s?(x)'], 14 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-test", 3 | "version": "1.0.0", 4 | "description": "a simplified version of vue3 implementation for study", 5 | "scripts": { 6 | "test": "jest", 7 | "build": "rollup -c", 8 | "dev": "node scripts/dev.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/core": "^7.17.9", 15 | "@babel/preset-env": "^7.16.11", 16 | "@babel/preset-typescript": "^7.16.7", 17 | "@rollup/plugin-typescript": "^8.3.2", 18 | "@types/jest": "^27.4.1", 19 | "@types/node": "^17.0.29", 20 | "babel-jest": "^28.0.0", 21 | "esbuild": "^0.14.38", 22 | "jest": "^28.0.0", 23 | "rollup": "^2.70.2", 24 | "rollup-plugin-terser": "^7.0.2", 25 | "ts-jest": "^27.1.4", 26 | "tslib": "^2.4.0", 27 | "typescript": "^4.6.3" 28 | } 29 | } -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Compiler: transform context state 1`] = ` 4 | Object { 5 | "code": "const { resolveComponent: _resolveComponent, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue; 6 | return function render(_ctx) { 7 | const _component_Child = _resolveComponent(\\"Child\\") 8 | return (_openBlock(), _createBlock(_component_Child, null, [], -2, null))}", 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/codegen.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-09 20:34:26 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-11 21:31:48 6 | */ 7 | import { transformShow } from "../../compiler-dom/src/transform/vShow"; 8 | import { generate } from "../src/codegen"; 9 | import { baseParse } from "../src/parse"; 10 | import { transform } from "../src/transform"; 11 | import { transformElement } from "../src/transforms/transformElement"; 12 | import { transformExpression } from "../src/transforms/transformExpression"; 13 | import { transformText } from "../src/transforms/transformText"; 14 | import { transformBind } from "../src/transforms/vBind"; 15 | import { transformFor } from "../src/transforms/vFor"; 16 | import { transformIf } from "../src/transforms/vIf"; 17 | import { transformModel } from "../src/transforms/vModel"; 18 | import { transformOn } from "../src/transforms/vOn"; 19 | describe("Compiler: transform", () => { 20 | test("context state", () => { 21 | const ast = baseParse(``); 22 | 23 | transform(ast, { 24 | nodeTransforms: [ 25 | // transformIf, 26 | // transformFor, 27 | // transformExpression, 28 | transformElement, 29 | // transformText, 30 | ], 31 | // nodeTransforms: [transformExpression, transformElement, transformText], 32 | // directiveTransforms: { 33 | // model: transformModel, 34 | // bind: transformBind, 35 | // on: transformOn, 36 | // show: transformShow, 37 | // }, 38 | }); 39 | 40 | const code = generate(ast); 41 | expect(code).toMatchSnapshot(); 42 | }); 43 | // test("codegen string", () => { 44 | // const ast = baseParse(`hello`); 45 | // transform(ast); 46 | // const { code } = generate(ast); 47 | 48 | // expect(code).toMatchSnapshot(); 49 | // }); 50 | 51 | // test("codegen interplation", () => { 52 | // const ast = baseParse(`{{ hello }}`); 53 | 54 | // transform(ast, { 55 | // nodeTransforms: [transformExpression], 56 | // }); 57 | // const { code } = generate(ast); 58 | 59 | // expect(code).toMatchSnapshot(); 60 | // }); 61 | 62 | // test("codegen element", () => { 63 | // const ast = baseParse(`
`); 64 | 65 | // transform(ast, { 66 | // nodeTransforms: [transformElement], 67 | // }); 68 | // const { code } = generate(ast); 69 | 70 | // expect(code).toMatchSnapshot(); 71 | // }); 72 | 73 | // test("codegen element interplation string", () => { 74 | // const ast = baseParse(`
hello, {{ message }}
`); 75 | 76 | // transform(ast, { 77 | // nodeTransforms: [transformElement, transformExpression, transformText], 78 | // }); 79 | 80 | // const { code } = generate(ast); 81 | 82 | // expect(code).toMatchSnapshot(); 83 | // }); 84 | }); 85 | -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/transform.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-09 20:34:26 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 17:15:23 6 | */ 7 | import { NodeTypes } from "../src/ast"; 8 | import { generate } from "../src/codegen"; 9 | import { baseParse } from "../src/parse"; 10 | import { transform } from "../src/transform"; 11 | import { transformElement } from "../src/transforms/transformElement"; 12 | import { transformText } from "../src/transforms/transformText"; 13 | describe("Compiler: transform", () => { 14 | test("context state", () => { 15 | // const ast = baseParse(`
hello {{ world }}
`); 16 | const ast = baseParse(`

123

xxxx

`); 17 | const plugin = (node, context) => { 18 | if (node.type === NodeTypes.TEXT) { 19 | node.content = node.content + "33"; 20 | } 21 | }; 22 | 23 | transform(ast, { 24 | nodeTransforms: [transformText, transformElement], 25 | }); 26 | 27 | generate(ast); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/compiler-core/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-26 10:54:34 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 11:42:42 6 | */ 7 | export * from './dist/compiler-core.esm.js'; -------------------------------------------------------------------------------- /packages/compiler-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simplify-vue/compiler-core", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@babel/parser": "^7.17.10", 15 | "estree-walker": "^2.0.2", 16 | "@simplify-vue/shared": "workspace:^1.0.0" 17 | } 18 | } -------------------------------------------------------------------------------- /packages/compiler-core/src/ast.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-07 22:07:33 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-12 21:03:33 6 | */ 7 | import { 8 | CREATE_ELEMENT_VNODE, 9 | WITH_DIRECTIVES, 10 | CREATE_ELEMENT_BLOCK, 11 | OPEN_BLOCK, 12 | } from "./runtimeHelpers"; 13 | import { getVNodeBlockHelper, getVNodeHelper } from "./utils"; 14 | 15 | export const enum NodeTypes { 16 | ELEMENT, 17 | SIMPLE_EXPRESSION, 18 | INTERPOLATION, 19 | TEXT, 20 | ROOT, 21 | COMPOUND_EXPRESSION, 22 | ATTRIBUTE, 23 | DIRECTIVE, 24 | COMMENT, 25 | VNODE_CALL, 26 | TEXT_CALL, 27 | JS_CALL_EXPRESSION, 28 | JS_PROPERTY, 29 | JS_OBJECT_EXPRESSION, 30 | JS_ARRAY_EXPRESSION, 31 | IF_BRANCH, 32 | IF, 33 | JS_CONDITIONAL_EXPRESSION, 34 | FOR, 35 | JS_FUNCTION_EXPRESSION, 36 | } 37 | 38 | export const enum ConstantTypes { 39 | NOT_CONSTANT = 0, 40 | CAN_SKIP_PATCH, 41 | CAN_HOIST, 42 | CAN_STRINGIFY, 43 | } 44 | 45 | export const enum ElementTypes { 46 | ELEMENT, 47 | COMPONENT, 48 | } 49 | 50 | export function createVnodeCall( 51 | context, 52 | tag, 53 | props?, 54 | children?, 55 | patchFlag?, 56 | dynamicProps?, 57 | directives?, 58 | isBlock = false, 59 | disableTracking = false, 60 | isComponent = false 61 | ) { 62 | if (context) { 63 | if (isBlock) { 64 | context.helper(OPEN_BLOCK); 65 | context.helper(getVNodeBlockHelper(isComponent)); 66 | } else { 67 | context.helper(getVNodeHelper(isComponent)); 68 | } 69 | if (directives) { 70 | context.helper(WITH_DIRECTIVES); 71 | } 72 | } 73 | 74 | // 创建VNODE_CALL是为了block 75 | return { 76 | type: NodeTypes.VNODE_CALL, 77 | tag, 78 | props, 79 | children, 80 | patchFlag, 81 | dynamicProps, 82 | directives, 83 | isBlock, 84 | disableTracking, 85 | isComponent, 86 | }; 87 | } 88 | 89 | export function createSimpleExpression( 90 | content, 91 | isStatic, 92 | constType = ConstantTypes.NOT_CONSTANT 93 | ) { 94 | // SIMPLE_EXPRESSION 对简单值的标记,比如props中的key和value。这些值如果是静态的会被序列化, 95 | // 如果是动态的则原封不动的使用 96 | return { 97 | type: NodeTypes.SIMPLE_EXPRESSION, 98 | isStatic, 99 | content, 100 | constType: isStatic ? ConstantTypes.CAN_STRINGIFY : constType, 101 | }; 102 | } 103 | 104 | // 插值 105 | export function createCallExpression(callee, args) { 106 | // JS_CALL_EXPRESSION 表示节点需要作为参数被特定的callee函数调用 107 | return { 108 | type: NodeTypes.JS_CALL_EXPRESSION, 109 | callee, 110 | arguments: args, 111 | }; 112 | } 113 | 114 | export function createArrayExpression(elements) { 115 | // JS_ARRAY_EXPRESSION 表示数据需要处理成数组的形式,例如:v-show指令 ['V-SHOW', expression] 116 | return { 117 | type: NodeTypes.JS_ARRAY_EXPRESSION, 118 | elements, 119 | }; 120 | } 121 | 122 | // 结构行表达式 v-if/v-else-if/v-else 123 | export function createConditionalExpression(test, consequent, alternate) { 124 | return { 125 | type: NodeTypes.JS_CONDITIONAL_EXPRESSION, 126 | test, 127 | consequent, 128 | alternate, 129 | }; 130 | } 131 | 132 | // v-for 133 | export function createFunctionExpression(params, returns = undefined) { 134 | return { 135 | type: NodeTypes.JS_FUNCTION_EXPRESSION, 136 | params, 137 | returns, 138 | }; 139 | } 140 | 141 | // 复合类型表达式!aaa.xxx xxx{{text}} 等等 142 | export function createCompoundExpression(children) { 143 | return { 144 | type: NodeTypes.COMPOUND_EXPRESSION, 145 | children, 146 | }; 147 | } 148 | -------------------------------------------------------------------------------- /packages/compiler-core/src/compile.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-10 14:20:47 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-02 19:39:23 6 | */ 7 | import { extend } from "@simplify-vue/shared"; 8 | import { generate } from "./codegen"; 9 | import { baseParse } from "./parse"; 10 | import { transform } from "./transform"; 11 | import { transformElement } from "./transforms/transformElement"; 12 | import { transformExpression } from "./transforms/transformExpression"; 13 | import { transformText } from "./transforms/transformText"; 14 | import { transformBind } from "./transforms/vBind"; 15 | import { transformFor } from "./transforms/vFor"; 16 | import { transformIf } from "./transforms/vIf"; 17 | import { transformOn } from "./transforms/vOn"; 18 | 19 | export function baseCompile(template, options) { 20 | const ast = baseParse(template); 21 | transform( 22 | ast, 23 | extend({}, options, { 24 | nodeTransforms: [ 25 | transformIf, 26 | transformFor, 27 | transformExpression, 28 | transformElement, 29 | transformText, 30 | ], 31 | directiveTransforms: extend({}, options.directiveTransforms, { 32 | bind: transformBind, 33 | on: transformOn, 34 | }), 35 | }) 36 | ); 37 | 38 | return generate(ast); 39 | } 40 | -------------------------------------------------------------------------------- /packages/compiler-core/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-10 14:22:24 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-10 21:00:32 6 | */ 7 | export * from "./compile"; 8 | export { registerRuntimeHelpers } from "./runtimeHelpers"; 9 | export { transformModel } from "./transforms/vModel"; 10 | export { NodeTypes } from "./ast"; 11 | -------------------------------------------------------------------------------- /packages/compiler-core/src/runtimeHelpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-09 22:40:56 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2023-12-14 16:25:17 6 | */ 7 | export const TO_DISPLAY_STRING = Symbol(`toDisplayString`); 8 | export const OPEN_BLOCK = Symbol(`openBlock`); 9 | export const CREATE_BLOCK = Symbol(`createBlock`); 10 | export const CREATE_ELEMENT_BLOCK = Symbol(`createElementBlock`); 11 | export const CREATE_ELEMENT_VNODE = Symbol(`createElementVNode`); 12 | export const CREATE_VNODE = Symbol(`createVNode`); 13 | export const CREATE_TEXT = Symbol(`createTextVNode`); 14 | export const NORMALIZE_CLASS = Symbol(`normalizeClass`); 15 | export const WITH_DIRECTIVES = Symbol(`withDirectives`); 16 | export const CREATE_COMMENT = Symbol(`createCommentVNode`); 17 | export const FRAGMENT = Symbol(`Fragment`); 18 | export const RENDER_LIST = Symbol(`renderList`); 19 | export const RESOLVE_COMPONENT = Symbol(`resolveComponent`); 20 | 21 | 22 | export const helperNameMap: any = { 23 | [TO_DISPLAY_STRING]: `toDisplayString`, 24 | [OPEN_BLOCK]: `openBlock`, 25 | [CREATE_BLOCK]: `createBlock`, 26 | [CREATE_ELEMENT_BLOCK]: `createElementBlock`, 27 | [CREATE_ELEMENT_VNODE]: `createElementVNode`, 28 | [CREATE_VNODE]: `createVNode`, 29 | [CREATE_TEXT]: `createTextVNode`, 30 | [NORMALIZE_CLASS]: `normalizeClass`, 31 | [WITH_DIRECTIVES]: `withDirectives`, 32 | [CREATE_COMMENT]: `createCommentVNode`, 33 | [FRAGMENT]: `Fragment`, 34 | [RENDER_LIST]: `renderList`, 35 | [RESOLVE_COMPONENT]: `resolveComponent`, 36 | }; 37 | 38 | // 注册运行时辅助函数 39 | export function registerRuntimeHelpers(helpers) { 40 | Object.getOwnPropertySymbols(helpers).forEach((s) => { 41 | helperNameMap[s] = helpers[s]; 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/hoistStatic.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-05-08 21:04:07 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-08 21:47:53 6 | */ 7 | 8 | import { isString } from "@simplify-vue/shared"; 9 | import { ConstantTypes, NodeTypes } from "../ast"; 10 | 11 | /** 12 | * @author: Zhouqi 13 | * @description: 获取节点的constant type 14 | * @param node 15 | * @return constant 16 | */ 17 | export function getConstantType(node) { 18 | // TODO 这里只处理 插值/文本/插值结合文本的情况 19 | const { type } = node; 20 | switch (type) { 21 | case NodeTypes.TEXT: 22 | return ConstantTypes.CAN_STRINGIFY; 23 | case NodeTypes.INTERPOLATION: 24 | return getConstantType(node.content); 25 | case NodeTypes.SIMPLE_EXPRESSION: 26 | return node.constType; 27 | case NodeTypes.COMPOUND_EXPRESSION: 28 | const children = node.children; 29 | // 默认是可字符串化的 30 | let returnType = ConstantTypes.CAN_STRINGIFY; 31 | for (let i = 0; i < children.length; i++) { 32 | const child = children[i]; 33 | // 文本直接跳过 34 | if (isString(child)) continue; 35 | const constType = getConstantType(child); 36 | // 如果有一个子节点是非常量,则直接返回 37 | if (constType === ConstantTypes.NOT_CONSTANT) { 38 | return constType; 39 | } else if (returnType > constType) { 40 | returnType = constType; 41 | } 42 | } 43 | return returnType; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/transformExpression.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-09 23:38:10 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-08 21:37:29 6 | */ 7 | import { ConstantTypes, createCompoundExpression, NodeTypes } from "../ast"; 8 | import { parse } from "@babel/parser"; 9 | import { isSimpleIdentifier } from "../utils"; 10 | import { walkIdentifiers } from "../babelUtils"; 11 | import { createSimpleExpression } from "../ast"; 12 | 13 | export function transformExpression(node, context) { 14 | if (node.type === NodeTypes.INTERPOLATION) { 15 | node.content = processExpression(node.content, context); 16 | } else if (node.type === NodeTypes.ELEMENT) { 17 | // 处理普通元素上的指令 18 | const { props } = node; 19 | for (let i = 0; i < props.length; i++) { 20 | const dir = props[i]; 21 | // 对于v-for包裹的插值不需要加ctx 22 | if (dir.type === NodeTypes.DIRECTIVE && dir.name !== "for") { 23 | const { exp } = dir; 24 | 25 | // 处理动态属性值 26 | if (exp && exp.type === NodeTypes.SIMPLE_EXPRESSION) { 27 | dir.exp = processExpression(exp, context); 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | export function processExpression(node, context) { 35 | /** 36 | * 对于普通的插值表达式,例如:{{item}},需要根据上下文判断item是否是当前作用域的中的变量, 37 | * 如果是则返回item即可,否则为_ctx.item 38 | * 39 | * 对于{{item.flag}}这种多重取值或者复杂表达式会进行分词,即将item.flag 40 | * 分为item . flag然后逐一进行操作,这里通过@babel/parser去进行代码字符串的解析 41 | * 然后通过es-walker进行ast的遍历处理 42 | */ 43 | const rewriteIdentifier = (value) => { 44 | const isScopeVarReference = context.identifiers[value]; 45 | return !isScopeVarReference ? `_ctx.${value}` : value; 46 | }; 47 | 48 | const content = node.content; 49 | // 判断内容是不是一个常量 50 | const bailConstant = content.indexOf(`(`) > -1 || content.indexOf(".") > 0; 51 | 52 | // 处理简单标识符,例如{{xxx}} 53 | if (isSimpleIdentifier(content)) { 54 | node.content = rewriteIdentifier(content); 55 | return node; 56 | } 57 | let ast; 58 | try { 59 | ast = parse(`(${content})`); 60 | } catch (e) { 61 | return node; 62 | } 63 | const ids: any = []; 64 | 65 | walkIdentifiers(ast, (node, isReferenced) => { 66 | if (isReferenced) { 67 | node.name = rewriteIdentifier(node.name); 68 | } 69 | ids.push(node); 70 | }); 71 | const children: any = []; 72 | ids.forEach((id, i) => { 73 | const { start, name, end } = id; 74 | const last = ids[i - 1]; 75 | // 截取单词之前的符号,截取方式是:在遍历到下一个单词时,对前一个单词尾部的下一位开始到当前单词的第一位 76 | const leadingText = content.slice(last ? last.end - 1 : 0, start - 1); 77 | // 如果有则添加到children中 78 | if (leadingText) { 79 | children.push(leadingText); 80 | } 81 | children.push( 82 | createSimpleExpression( 83 | name, 84 | false, 85 | id.isConstant ? ConstantTypes.CAN_STRINGIFY : ConstantTypes.NOT_CONSTANT 86 | ) 87 | ); 88 | // 如果标识符后面还有其他字符,则全部截取,例如:_ctx.show === 2中遍历到show之后的 === 2需要全部截取 89 | if (i === ids.length - 1 && end < content.length) { 90 | children.push(content.slice(end)); 91 | } 92 | }); 93 | 94 | if (children.length) { 95 | return createCompoundExpression(children); 96 | } else { 97 | node.constType = bailConstant 98 | ? ConstantTypes.NOT_CONSTANT 99 | : ConstantTypes.CAN_STRINGIFY; 100 | } 101 | return node; 102 | } 103 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/transformText.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-10 11:31:15 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-08 21:53:43 6 | */ 7 | import { PatchFlags } from "@simplify-vue/shared"; 8 | import { 9 | ConstantTypes, 10 | createCallExpression, 11 | createCompoundExpression, 12 | NodeTypes, 13 | } from "../ast"; 14 | import { CREATE_TEXT } from "../runtimeHelpers"; 15 | import { isText } from "../utils"; 16 | import { getConstantType } from "./hoistStatic"; 17 | 18 | export function transformText(node, context) { 19 | if (node.type === NodeTypes.ROOT || node.type === NodeTypes.ELEMENT) { 20 | return () => { 21 | const { children } = node; 22 | let container; 23 | let hasText = false; 24 | 25 | // 找到相邻的字符串/插值节点,通过+去拼接,并且重新定义新的复合类型节点 26 | for (let i = 0; i < children.length; i++) { 27 | const child = children[i]; 28 | if (isText(child)) { 29 | hasText = true; 30 | for (let j = i + 1; j < children.length; j++) { 31 | const nextChild = children[j]; 32 | if (isText(nextChild)) { 33 | // 表明是复合节点,需要重新去修改当前节点数据 34 | if (!container) { 35 | container = children[i] = createCompoundExpression([child]); 36 | } 37 | // 前后需要进行拼接处理 38 | container.children.push(` + `); 39 | container.children.push(nextChild); 40 | // nextChild节点被重新处理过了,需要删除老的 41 | children.splice(j, 1); 42 | // 删除了一个,为了不影响遍历j也要减1 43 | j--; 44 | } else { 45 | // 下一个不是文本或者插值节点,跳出循环结束即可 46 | container = undefined; 47 | break; 48 | } 49 | } 50 | } 51 | } 52 | 53 | // 不处理文本节点的情况 54 | // 1、不存在文本节点 55 | // 2、只有一个文本子节点,且父元素是element/root类型 56 | if ( 57 | !hasText || 58 | (children.length === 1 && 59 | (node.type === NodeTypes.ELEMENT || node.type === NodeTypes.ROOT)) 60 | ) { 61 | return; 62 | } 63 | 64 | // 需要增加createTextVNode的处理,因为有多个子节点且其中有文本节点 65 | for (let i = 0; i < children.length; i++) { 66 | const child = children[i]; 67 | // 处理文本节点或者复合节点(文本+插值) 68 | if (isText(child) || child.type === NodeTypes.COMPOUND_EXPRESSION) { 69 | const callArgs: any = []; 70 | callArgs.push(child); 71 | if (getConstantType(child) === ConstantTypes.NOT_CONSTANT) { 72 | callArgs.push(String(PatchFlags.TEXT)); 73 | } 74 | children[i] = { 75 | type: NodeTypes.TEXT_CALL, 76 | content: child, 77 | codegenNode: createCallExpression( 78 | context.helper(CREATE_TEXT), 79 | callArgs 80 | ), 81 | }; 82 | } 83 | } 84 | }; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/vBind.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-23 10:10:59 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-24 20:58:00 6 | */ 7 | 8 | import { createObjectProperty } from "./transformElement"; 9 | 10 | /** 11 | * @author: Zhouqi 12 | * @description: 转换v-bind指令 13 | * @param dir v-bind属性ast 14 | * @return props 15 | */ 16 | export function transformBind(dir) { 17 | const { arg, exp } = dir; 18 | // arg和exp在parse的时候已经处理成simpleExpression了,无需像静态属性一样createSimpleExpression 19 | const props = createObjectProperty(arg, exp); 20 | return { props: [props] }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/vModel.ts: -------------------------------------------------------------------------------- 1 | import { createCompoundExpression, createSimpleExpression } from "../ast"; 2 | import { createObjectProperty } from "./transformElement"; 3 | 4 | /** 5 | * @author: Zhouqi 6 | * @description: 转化v-model 7 | * @param dir 8 | * @return 9 | */ 10 | export const transformModel = (dir) => { 11 | const { exp } = dir; 12 | const propName = createSimpleExpression("modelValue", true); 13 | const eventName = `onUpdate:modelValue`; 14 | const eventArg = `$event`; 15 | let assignmentExp = createCompoundExpression([ 16 | `${eventArg} => ((`, 17 | exp, 18 | `) = $event)`, 19 | ]); 20 | const props = [ 21 | // modelValue: text 22 | createObjectProperty(propName, dir.exp!), 23 | // "onUpdate:modelValue": $event => (text = $event) 24 | createObjectProperty(eventName, assignmentExp), 25 | ]; 26 | return { props }; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/vOn.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-23 19:47:29 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-07 21:22:44 6 | */ 7 | import { toHandlerKey } from "@simplify-vue/shared"; 8 | import { createSimpleExpression } from "../ast"; 9 | import { createObjectProperty } from "./transformElement"; 10 | 11 | export function transformOn(dir) { 12 | const { arg, exp } = dir; 13 | // @click ===> onClick 14 | let eventName = createSimpleExpression(toHandlerKey(arg.content), true); 15 | const ret = createObjectProperty(eventName, exp); 16 | return { props: [ret] }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/compiler-core/src/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-10 11:33:43 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-12 21:34:46 6 | */ 7 | import { NodeTypes } from "./ast"; 8 | import { 9 | CREATE_BLOCK, 10 | CREATE_ELEMENT_BLOCK, 11 | CREATE_ELEMENT_VNODE, 12 | CREATE_VNODE, 13 | } from "./runtimeHelpers"; 14 | import { createObjectExpression } from "./transforms/transformElement"; 15 | 16 | export function isText(node) { 17 | return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT; 18 | } 19 | 20 | // 判断是否是静态值 21 | export const isStaticExp = (p) => 22 | p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic; 23 | 24 | const nonIdentifierRE = /^\d|[^\$\w]/; 25 | // 是否是简单标识符 26 | export const isSimpleIdentifier = (name: string): boolean => 27 | !nonIdentifierRE.test(name); 28 | 29 | // 在当前ast node上注入prop 30 | export function injectProp(node, prop) { 31 | // 这里简单处理一下v-if的key属性注入 32 | const propsWithInjection = createObjectExpression([prop]); 33 | node.props = propsWithInjection; 34 | } 35 | 36 | export function getVNodeBlockHelper(isComponent: boolean) { 37 | return isComponent ? CREATE_BLOCK : CREATE_ELEMENT_BLOCK; 38 | } 39 | 40 | export function getVNodeHelper(isComponent: boolean) { 41 | return isComponent ? CREATE_VNODE : CREATE_ELEMENT_VNODE; 42 | } 43 | -------------------------------------------------------------------------------- /packages/compiler-dom/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-26 10:55:18 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 11:41:29 6 | */ 7 | export * from './dist/compiler-dom.esm.js' -------------------------------------------------------------------------------- /packages/compiler-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simplify-vue/compiler-dom", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@simplify-vue/compiler-core": "workspace:^1.0.0", 15 | "@simplify-vue/shared": "workspace:^1.0.0" 16 | } 17 | } -------------------------------------------------------------------------------- /packages/compiler-dom/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-24 20:32:13 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-10 20:54:21 6 | */ 7 | import { baseCompile } from "@simplify-vue/compiler-core"; 8 | import { extend } from "@simplify-vue/shared"; 9 | import { transformModel } from "./transform/vModel"; 10 | import { transformShow } from "./transform/vShow"; 11 | 12 | export * from "./runtimeHelpers"; 13 | 14 | export function compile(template, options = {}) { 15 | return baseCompile( 16 | template, 17 | extend({}, options, { 18 | directiveTransforms: { 19 | show: transformShow, 20 | model: transformModel, 21 | }, 22 | }) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/compiler-dom/src/runtimeHelpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-24 20:33:14 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-10 20:58:11 6 | */ 7 | 8 | import { registerRuntimeHelpers } from "@simplify-vue/compiler-core"; 9 | 10 | export const V_SHOW = Symbol(`vShow`); 11 | export const V_MODEL_TEXT = Symbol(`vModelText`); 12 | 13 | registerRuntimeHelpers({ 14 | [V_SHOW]: `vShow`, 15 | [V_MODEL_TEXT]: `vModelText`, 16 | }); 17 | -------------------------------------------------------------------------------- /packages/compiler-dom/src/transform/vModel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-05-10 20:52:05 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-10 21:35:03 6 | */ 7 | import { transformModel as baseTransform } from "@simplify-vue/compiler-core"; 8 | import { V_MODEL_TEXT } from "../runtimeHelpers"; 9 | 10 | export const transformModel = (dir, node, context) => { 11 | const baseResult: any = baseTransform(dir); 12 | const { tag } = node; 13 | if (tag === "input") { 14 | const directiveToUse = V_MODEL_TEXT; 15 | baseResult.needRuntime = context.helper(directiveToUse); 16 | } 17 | return baseResult; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/compiler-dom/src/transform/vShow.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-24 20:32:19 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-10 21:04:02 6 | */ 7 | import { V_SHOW } from "../runtimeHelpers"; 8 | 9 | // 转换v-show 10 | export function transformShow(dir, node, context) { 11 | return { 12 | props: [], 13 | needRuntime: context.helper(V_SHOW), 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/computed.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-26 14:52:24 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2023-12-27 17:45:38 6 | */ 7 | import { computed } from "../src/computed"; 8 | import { effect } from "../src/effect"; 9 | import { reactive } from "../src/reactive"; 10 | 11 | describe("computed", () => { 12 | it("happy path", () => { 13 | const value: any = reactive({ 14 | foo: 1, 15 | }); 16 | 17 | const getter = computed(() => { 18 | return value.foo; 19 | }); 20 | expect(getter.value).toBe(1); 21 | value.foo = 2; 22 | expect(getter.value).toBe(2); 23 | }); 24 | 25 | it("should compute lazily", () => { 26 | const value: any = reactive({ 27 | foo: 1, 28 | }); 29 | const getter = jest.fn(() => { 30 | return value.foo; 31 | }); 32 | const cValue = computed(getter); 33 | 34 | // lazy 35 | expect(getter).not.toHaveBeenCalled(); 36 | 37 | expect(cValue.value).toBe(1); 38 | expect(getter).toHaveBeenCalledTimes(1); 39 | 40 | // should not compute again 41 | cValue.value; 42 | expect(getter).toHaveBeenCalledTimes(1); 43 | 44 | // should not compute until needed 45 | value.foo = 2; 46 | expect(getter).toHaveBeenCalledTimes(1); 47 | 48 | // now it should compute 49 | expect(cValue.value).toBe(2); 50 | expect(getter).toHaveBeenCalledTimes(2); 51 | 52 | // should not compute again 53 | cValue.value; 54 | expect(getter).toHaveBeenCalledTimes(2); 55 | }); 56 | 57 | it("计算属性中的嵌套effect", () => { 58 | const value: any = reactive({ 59 | foo: 1, 60 | }); 61 | let i = 0; 62 | 63 | const getter = computed(() => { 64 | return value.foo; 65 | }); 66 | 67 | effect(() => { 68 | i++; 69 | getter.value; 70 | }); 71 | 72 | value.foo = 2; 73 | expect(i).toBe(2); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/effect.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-20 20:26:23 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2023-12-21 15:34:41 6 | */ 7 | import { reactive } from "../src/reactive"; 8 | import { effect, stop } from "../src/effect"; 9 | 10 | describe("effect", () => { 11 | it("happy path", () => { 12 | const user = reactive({ 13 | age: 10, 14 | }); 15 | let nextAge; 16 | effect(() => { 17 | nextAge = user.age + 1; 18 | }); 19 | user.age++; 20 | expect(nextAge).toBe(12); 21 | }); 22 | 23 | it("runner", () => { 24 | let foo = 10; 25 | const runner = effect(() => { 26 | foo++; 27 | return "foo"; 28 | }); 29 | expect(foo).toBe(11); 30 | const r = runner(); 31 | expect(foo).toBe(12); 32 | expect(r).toBe("foo"); 33 | }); 34 | it("scheduler", () => { 35 | let dummy; 36 | let run: any; 37 | const scheduler = jest.fn(() => { 38 | run = runner; 39 | }); 40 | const obj = reactive({ foo: 1 }); 41 | const runner = effect( 42 | () => { 43 | dummy = obj.foo; 44 | }, 45 | { scheduler } 46 | ); 47 | expect(scheduler).not.toHaveBeenCalled(); 48 | expect(dummy).toBe(1); 49 | // should be called on first trigger 50 | obj.foo++; 51 | expect(scheduler).toHaveBeenCalledTimes(1); 52 | // // should not run yet 53 | expect(dummy).toBe(1); 54 | // // manually run 55 | run(); 56 | // // should have run 57 | expect(dummy).toBe(2); 58 | }); 59 | 60 | it("stop", () => { 61 | let dummy; 62 | const obj = reactive({ prop: 1 }); 63 | const runner = effect(() => { 64 | dummy = obj.prop; 65 | }); 66 | obj.prop = 2; 67 | expect(dummy).toBe(2); 68 | stop(runner); 69 | // obj.prop = 3; 70 | // 是否跟shouldTrack有关? 71 | obj.prop++; 72 | expect(dummy).toBe(2); 73 | 74 | // stopped effect should still be manually callable 75 | runner(); 76 | expect(dummy).toBe(3); 77 | }); 78 | 79 | it("events: onStop", () => { 80 | const onStop = jest.fn(); 81 | const runner = effect(() => { }, { 82 | onStop, 83 | }); 84 | 85 | stop(runner); 86 | expect(onStop).toHaveBeenCalled(); 87 | }); 88 | 89 | it("cleanup", () => { 90 | const user = reactive({ 91 | age: 10, 92 | ok: true, 93 | }); 94 | let i = 0; 95 | let effectFn = () => { 96 | i++; 97 | user.name = user.ok ? user.age : "123"; 98 | }; 99 | effect(effectFn); 100 | // ok为false时,无论age怎么变,都不应该触发副作用函数,因为此时name一直为123 101 | user.ok = false; 102 | user.age = 2; 103 | 104 | expect(i).toBe(2); 105 | }); 106 | 107 | it("obj++", () => { 108 | const user = reactive({ 109 | num: 0, 110 | }); 111 | 112 | effect(() => { 113 | // 无限递归循环 114 | // user.num ++ ====> user.num = user.num + 1; 115 | user.num++; 116 | }); 117 | 118 | user.num = 3; 119 | 120 | expect(user.num).toBe(4); 121 | }); 122 | 123 | it("嵌套effect", () => { 124 | const user = reactive({ 125 | num: 0, 126 | num1: 1, 127 | }); 128 | 129 | let i = 0; 130 | effect(() => { 131 | effect(() => { 132 | i = user.num1; 133 | }); 134 | i = user.num; 135 | }); 136 | 137 | user.num = 2; 138 | 139 | expect(i).toBe(2); 140 | }); 141 | 142 | it("lazy effect", () => { 143 | const user = reactive({ 144 | num: 0, 145 | }); 146 | 147 | const runner = effect( 148 | () => { 149 | user.num++; 150 | }, 151 | { 152 | lazy: true, 153 | } 154 | ); 155 | expect(user.num).toBe(0); 156 | runner(); 157 | expect(user.num).toBe(1); 158 | user.num = 2; 159 | expect(user.num).toBe(3); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/mapAndSet.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-20 20:46:00 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-13 17:16:23 6 | */ 7 | import { effect } from "../src/effect"; 8 | import { 9 | reactive, 10 | readonly, 11 | shallowReactive, 12 | shallowReadonly, 13 | } from "../src/reactive"; 14 | 15 | describe("Map and Set", () => { 16 | test("Set", () => { 17 | const set = new Set([1, 2, 3]); 18 | const r = reactive(set); 19 | let i = 0; 20 | effect(() => { 21 | i++; 22 | r.size; 23 | }); 24 | r.delete(1); 25 | expect(r.size).toBe(2); 26 | expect(i).toBe(2); 27 | r.add(1); 28 | expect(i).toBe(3); 29 | r.add(1); 30 | expect(i).toBe(3); 31 | r.delete(4); 32 | expect(i).toBe(3); 33 | }); 34 | 35 | test("Map", () => { 36 | const map = new Map([["name", "zs"]]); 37 | const r = reactive(map); 38 | let i = 0; 39 | effect(() => { 40 | i++; 41 | r.get("name"); 42 | }); 43 | r.set("name", "1"); 44 | expect(i).toBe(2); 45 | }); 46 | 47 | test("dirty", () => { 48 | const map = new Map([["name", "zs"]]); 49 | const r = reactive(map); 50 | const r1 = reactive(new Map()); 51 | r.set("r1", r1); 52 | let i = 0; 53 | effect(() => { 54 | i++; 55 | (map.get("r1") as any).size; 56 | }); 57 | (map.get("r1") as any).set("age", 1); 58 | expect(i).toBe(1); 59 | }); 60 | 61 | test("forEach", () => { 62 | const set = new Set([1, 2]); 63 | const map = new Map([["name", set]]); 64 | const r = reactive(map); 65 | let i = 0; 66 | effect(() => { 67 | i++; 68 | r.forEach((value, key) => { 69 | value.size; 70 | }); 71 | }); 72 | 73 | r.get("name").add(6); 74 | expect(i).toBe(2); 75 | r.set("name", 2); 76 | expect(i).toBe(3); 77 | }); 78 | 79 | test("iterator for of", () => { 80 | const map = reactive(new Map([["name", 1]])); 81 | let i = 0; 82 | effect(() => { 83 | i++; 84 | for (const [value, key] of map) { 85 | } 86 | }); 87 | map.set("age", 2); 88 | expect(i).toBe(2); 89 | }); 90 | 91 | test("iterator entries", () => { 92 | const map = reactive(new Map([["name", 1]])); 93 | let i = 0; 94 | effect(() => { 95 | i++; 96 | for (const [value, key] of map.entries()) { 97 | } 98 | }); 99 | map.set("age", 2); 100 | expect(i).toBe(2); 101 | }); 102 | 103 | test("iterator values", () => { 104 | const map = reactive(new Map([["name", 1]])); 105 | let i = 0; 106 | effect(() => { 107 | i++; 108 | for (const value of map.values()) { 109 | } 110 | }); 111 | map.set("age", 1); 112 | expect(i).toBe(2); 113 | }); 114 | 115 | test("iterator keys", () => { 116 | const map = reactive(new Map([["name", 1]])); 117 | let i = 0; 118 | effect(() => { 119 | i++; 120 | for (const key of map.keys()) { 121 | } 122 | }); 123 | map.set("name", 2); 124 | expect(i).toBe(1); 125 | map.set("age", 2); 126 | expect(i).toBe(2); 127 | map.delete("age"); 128 | expect(i).toBe(3); 129 | }); 130 | 131 | test("clear", () => { 132 | const map = reactive(new Map([["name", 1]])); 133 | let i = 0; 134 | effect(() => { 135 | i++; 136 | for (const key of map.keys()) { 137 | } 138 | }); 139 | effect(() => { 140 | i++; 141 | for (const key of map.entries()) { 142 | } 143 | }); 144 | effect(() => { 145 | i++; 146 | for (const key of map.values()) { 147 | } 148 | }); 149 | map.clear(); 150 | expect(i).toBe(6); 151 | }); 152 | 153 | test("has", () => { 154 | const map = reactive(new Map([["name", 1]])); 155 | let i = 0; 156 | effect(() => { 157 | i++; 158 | map.has("name"); 159 | }); 160 | map.set("name", 1); 161 | expect(i).toBe(1); 162 | map.set("name", 2); 163 | expect(i).toBe(2); 164 | }); 165 | 166 | test("readonly", () => { 167 | const obj = { name: 1 }; 168 | const map = readonly(new Map([["name", obj]])); 169 | console.warn = jest.fn(); 170 | map.set("name", 2); 171 | expect(console.warn).toHaveBeenCalled(); 172 | expect(map.get("name")).toStrictEqual(obj); 173 | map.get("name").age = 2; 174 | expect(console.warn).toHaveBeenCalled(); 175 | }); 176 | 177 | test("shallowReactive", () => { 178 | const obj = { name: 1 }; 179 | const map = shallowReactive(new Map([["name", obj]])); 180 | let i = 0; 181 | effect(() => { 182 | i++; 183 | map.get("name"); 184 | }); 185 | map.set("name", { name: 1 }); 186 | expect(i).toBe(2); 187 | map.get("name").age = 2; 188 | expect(i).toBe(2); 189 | }); 190 | 191 | test("shallowReadonly", () => { 192 | const obj = { name: 1 }; 193 | const map = shallowReadonly(new Map([["name", obj]])); 194 | console.warn = jest.fn(); 195 | map.set("name", 2); 196 | expect(console.warn).toHaveBeenCalledTimes(1); 197 | expect(map.get("name")).toStrictEqual(obj); 198 | map.get("name").age = 2; 199 | expect(console.warn).toHaveBeenCalledTimes(1); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-20 20:46:00 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-30 20:56:33 6 | */ 7 | import { effect } from "../src/effect"; 8 | import { markRaw, reactive } from "../src/reactive"; 9 | 10 | describe("reactive", () => { 11 | test("happy path", () => { 12 | const org = { foo: 1 }; 13 | const obOrg: any = reactive(org); 14 | expect(obOrg).not.toBe(org); 15 | expect(obOrg.foo).toBe(1); 16 | }); 17 | 18 | test("test rawValue", () => { 19 | let i = 0; 20 | const pObj: any = reactive({ 21 | num: 1, 22 | }); 23 | 24 | const rObj: any = reactive({ num1: 1 }); 25 | effect(() => { 26 | i++; 27 | rObj.num1; 28 | }); 29 | 30 | rObj.num1 = pObj; 31 | expect(i).toBe(2); 32 | rObj.num1 = pObj; 33 | expect(i).toBe(2); 34 | }); 35 | 36 | test("test proxy has ", () => { 37 | let i = 0; 38 | const obj: any = reactive({ 39 | num: 1, 40 | }); 41 | effect(() => { 42 | i++; 43 | "num" in obj; 44 | }); 45 | obj.num++; 46 | expect(i).toBe(2); 47 | }); 48 | 49 | test("test proxy ownkeys ", () => { 50 | let i = 0; 51 | const obj: any = reactive({ 52 | num: 1, 53 | }); 54 | effect(() => { 55 | i++; 56 | for (const key in obj) { 57 | } 58 | }); 59 | 60 | obj.num1 = 0; 61 | 62 | expect(i).toBe(2); 63 | obj.num = 2; 64 | expect(i).toBe(2); 65 | }); 66 | 67 | test("test proxy deleteProperty ", () => { 68 | let i = 0; 69 | const obj: any = reactive({ 70 | num: 1, 71 | num1: 0, 72 | }); 73 | effect(() => { 74 | i++; 75 | for (const key in obj) { 76 | } 77 | }); 78 | 79 | obj.num1 = 1; 80 | expect(i).toBe(1); 81 | 82 | delete obj.num; 83 | expect(i).toBe(2); 84 | }); 85 | 86 | test("test prototype chain", () => { 87 | const obj = { name: "zs" }; 88 | const obj1 = { name1: "ls" }; 89 | const child: any = reactive(obj); 90 | const parent = reactive(obj1); 91 | Object.setPrototypeOf(child, parent); 92 | let i = 0; 93 | effect(() => { 94 | i++; 95 | // 96 | child.name1; 97 | }); 98 | 99 | child.name1 = "ww"; 100 | expect(i).toBe(2); 101 | }); 102 | 103 | test("test array index", () => { 104 | const arr = reactive([1]); 105 | let i; 106 | effect(() => { 107 | i = arr[0]; 108 | }); 109 | arr[0] = 2; 110 | expect(i).toBe(2); 111 | }); 112 | 113 | test("test array length", () => { 114 | const arr: any = reactive([1]); 115 | let i; 116 | effect(() => { 117 | i = arr.length; 118 | if (!arr[0]) { 119 | i++; 120 | } 121 | }); 122 | arr[2] = 2; 123 | expect(i).toBe(3); 124 | arr[2] = 23; 125 | expect(i).toBe(3); 126 | arr.length = 10; 127 | expect(i).toBe(10); 128 | arr.length = 0; 129 | expect(i).toBe(1); 130 | }); 131 | 132 | test("test array for in", () => { 133 | const arr: any = reactive([1]); 134 | let i = 0; 135 | effect(() => { 136 | i++; 137 | for (const key in arr) { 138 | } 139 | }); 140 | 141 | arr.length = 10; 142 | expect(i).toBe(2); 143 | arr[10] = 1; 144 | expect(i).toBe(3); 145 | arr[10] = 2; 146 | expect(i).toBe(3); 147 | }); 148 | 149 | test("test array for of", () => { 150 | const arr: any = reactive([1]); 151 | let i = 0; 152 | effect(() => { 153 | i++; 154 | // for (const key of arr) { 155 | // } 156 | for (const key of arr.values()) { 157 | } 158 | }); 159 | arr.length = 10; 160 | expect(i).toBe(2); 161 | arr[10] = 1; 162 | expect(i).toBe(3); 163 | arr[10] = 2; 164 | expect(i).toBe(4); 165 | }); 166 | 167 | test("test array includes", () => { 168 | const obj = {}; 169 | const arr: any = reactive([obj]); 170 | expect(arr.includes(arr[0])).toBe(true); 171 | }); 172 | 173 | test("test array includes last/indexOf ", () => { 174 | const obj = {}; 175 | const arr: any = reactive([obj]); 176 | expect(arr.includes(obj)).toBe(true); 177 | expect(arr.indexOf(obj)).toBe(0); 178 | expect(arr.lastIndexOf(obj)).toBe(0); 179 | }); 180 | 181 | test("test array push ", () => { 182 | const arr: any = reactive([1]); 183 | let i = 0; 184 | effect(() => { 185 | i++; 186 | arr.push(1); 187 | }); 188 | 189 | effect(() => { 190 | i++; 191 | arr.push(2); 192 | }); 193 | 194 | expect(arr.length).toBe(3); 195 | expect(i).toBe(2); 196 | 197 | arr.push(3); 198 | expect(arr.length).toBe(4); 199 | expect(i).toBe(2); 200 | }); 201 | 202 | test("test array push ", () => { 203 | const arr: any = reactive([1]); 204 | let i = 0; 205 | effect(() => { 206 | arr.push(2); 207 | 208 | effect(() => { 209 | i++; 210 | arr[0]; 211 | }); 212 | }); 213 | 214 | arr[0] = 2; 215 | expect(i).toBe(2); 216 | }); 217 | 218 | test("mark raw", () => { 219 | const org = { foo: 1 }; 220 | const obOrg: any = reactive(markRaw(org)); 221 | expect(obOrg).toBe(org); 222 | }); 223 | }); 224 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-20 20:46:00 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-11 11:25:05 6 | */ 7 | import { readonly, isReactive, isReadonly, isProxy } from "../src/reactive"; 8 | 9 | describe("readonly", () => { 10 | it("readonly", () => { 11 | const org = { foo: 1, bar: { baz: 2 } }; 12 | const obOrg: any = readonly(org); 13 | expect(obOrg).not.toBe(org); 14 | expect(obOrg.foo).toBe(1); 15 | }); 16 | 17 | it("warn set", () => { 18 | console.warn = jest.fn(); 19 | const user: any = readonly({ name: 2 }); 20 | user.name = 3; 21 | expect(console.warn).toHaveBeenCalled(); 22 | }); 23 | 24 | it("should make nested values readonly", () => { 25 | const original = { foo: 1, bar: { baz: 2 } }; 26 | const wrapped: any = readonly(original); 27 | expect(wrapped).not.toBe(original); 28 | expect(isProxy(wrapped)).toBe(true); 29 | expect(isReactive(wrapped)).toBe(false); 30 | expect(isReadonly(wrapped)).toBe(true); 31 | expect(isReactive(original)).toBe(false); 32 | expect(isReadonly(original)).toBe(false); 33 | expect(isReactive(wrapped.bar)).toBe(false); 34 | expect(isReadonly(wrapped.bar)).toBe(true); 35 | expect(isReactive(original.bar)).toBe(false); 36 | expect(isReadonly(original.bar)).toBe(false); 37 | // get 38 | expect(wrapped.foo).toBe(1); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/ref.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-23 21:32:46 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-11 17:41:23 6 | */ 7 | import { effect } from "../src/effect"; 8 | import { reactive } from "../src/reactive"; 9 | import { 10 | ref, 11 | shallowRef, 12 | isRef, 13 | unRef, 14 | proxyRefs, 15 | toRef, 16 | toRefs, 17 | } from "../src/ref"; 18 | describe("ref", () => { 19 | it("should be reactive", () => { 20 | const a = ref(1); 21 | expect(a.value).toBe(1); 22 | let dummy; 23 | let calls = 0; 24 | effect(() => { 25 | calls++; 26 | dummy = a.value; 27 | }); 28 | expect(calls).toBe(1); 29 | expect(dummy).toBe(1); 30 | a.value = 2; 31 | expect(calls).toBe(2); 32 | expect(dummy).toBe(2); 33 | // same value should not trigger 34 | a.value = 2; 35 | expect(calls).toBe(2); 36 | expect(dummy).toBe(2); 37 | }); 38 | 39 | it("should make nested properties reactive", () => { 40 | const obj = { 41 | count: 1, 42 | }; 43 | const a = ref(obj); 44 | let dummy; 45 | let i = 0; 46 | effect(() => { 47 | i++; 48 | dummy = a.value.count; 49 | }); 50 | expect(dummy).toBe(1); 51 | // a.value.count = 2; 52 | a.value = obj; 53 | expect(dummy).toBe(1); 54 | expect(i).toBe(1); 55 | a.value.count = 2; 56 | expect(obj.count).toBe(2); 57 | }); 58 | 59 | it("test rawValue", () => { 60 | let i = 0; 61 | const pObj = reactive({ 62 | num: {}, 63 | }); 64 | 65 | const rObj = ref({ 66 | num: {}, 67 | }); 68 | effect(() => { 69 | i++; 70 | rObj.value; 71 | }); 72 | 73 | rObj.value = pObj; 74 | expect(i).toBe(2); 75 | rObj.value = pObj; 76 | expect(i).toBe(2); 77 | }); 78 | 79 | it("shallow ref", () => { 80 | const obj = { 81 | num: 1, 82 | }; 83 | let i = 0; 84 | const ref = shallowRef(obj); 85 | effect(() => { 86 | i++; 87 | ref.value; 88 | }); 89 | 90 | ref.value.num = 2; 91 | expect(i).toBe(1); 92 | ref.value = 2; 93 | expect(i).toBe(2); 94 | }); 95 | 96 | it("proxyRefs", () => { 97 | const user = { 98 | age: ref(10), 99 | name: "xiaohong", 100 | }; 101 | const proxyUser = proxyRefs(user); 102 | expect(user.age.value).toBe(10); 103 | expect(proxyUser.age).toBe(10); 104 | expect(proxyUser.name).toBe("xiaohong"); 105 | 106 | (proxyUser as any).age = 20; 107 | expect(proxyUser.age).toBe(20); 108 | expect(user.age.value).toBe(20); 109 | 110 | proxyUser.age = ref(20); 111 | expect(proxyUser.age).toBe(20); 112 | expect(user.age.value).toBe(20); 113 | }); 114 | 115 | it("isRef", () => { 116 | const a = ref(1); 117 | const user = reactive({ 118 | age: 1, 119 | }); 120 | expect(isRef(a)).toBe(true); 121 | expect(isRef(1)).toBe(false); 122 | expect(isRef(user)).toBe(false); 123 | }); 124 | 125 | it("unRef", () => { 126 | const a = ref(1); 127 | expect(unRef(a)).toBe(1); 128 | expect(unRef(1)).toBe(1); 129 | }); 130 | 131 | it("toRef", () => { 132 | const obj: any = reactive({ 133 | a: 1, 134 | b: 2, 135 | }); 136 | let i = 0; 137 | const ref = toRef(obj, "a"); 138 | effect(() => { 139 | i = obj.a; 140 | }); 141 | expect(i).toBe(1); 142 | ref.value = 2; 143 | expect(i).toBe(2); 144 | obj.a = 3; 145 | expect(i).toBe(3); 146 | expect(ref.value).toBe(3); 147 | }); 148 | 149 | it("toRefs", () => { 150 | const obj: any = reactive({ 151 | a: 1, 152 | b: 2, 153 | }); 154 | let i = 0; 155 | const refs = toRefs(obj); 156 | effect(() => { 157 | i = obj.a; 158 | }); 159 | expect(i).toBe(1); 160 | refs.a.value = 2; 161 | expect(i).toBe(2); 162 | obj.a = 3; 163 | expect(i).toBe(3); 164 | expect(refs.a.value).toBe(3); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /packages/reactivity/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-26 10:55:33 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 11:42:55 6 | */ 7 | export * from './dist/reactivity.esm.js'; 8 | -------------------------------------------------------------------------------- /packages/reactivity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simplify-vue/reactivity", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@simplify-vue/shared": "workspace:^1.0.0" 15 | } 16 | } -------------------------------------------------------------------------------- /packages/reactivity/src/computed.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-26 14:52:42 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-11 17:50:17 6 | */ 7 | 8 | import { ReactiveEffect } from "./effect"; 9 | import { trackRefValue, triggerRefValue } from "./ref"; 10 | 11 | export type ComputedGetter = (...args: any[]) => T; 12 | 13 | class ComputedRefImpl { 14 | private _dirty = true; // true表示需要重新计算新的值 15 | private _lazyEffect; 16 | private _oldValue!: T; // 缓存的值 17 | 18 | constructor(getter: ComputedGetter) { 19 | this._lazyEffect = new ReactiveEffect(getter, () => { 20 | // 借助scheduler是否执行去判断依赖的响应式对象是否发生变化 21 | if (!this._dirty) { 22 | this._dirty = true; 23 | // 触发value属性相关的effect函数 24 | triggerRefValue(this); 25 | } 26 | }); 27 | } 28 | 29 | // value属性后才去执行getter函数获取值 30 | get value() { 31 | /** 32 | * 当通过.value访问值时,如果计算属性计算时依赖的响应式对象数据没有发生变化 33 | * 则返回旧的值,否则重新计算新的值。 34 | * 这里就需要借助effect去帮助我们实现 当依赖的响应式对象发生变化时 重新计算的逻辑 35 | */ 36 | trackRefValue(this); 37 | if (this._dirty) { 38 | this._dirty = false; 39 | this._oldValue = this._lazyEffect.run(); 40 | } 41 | /** 42 | * 在effect中使用计算属性时会发生嵌套effect的情况 43 | * 由于计算属性内部有单独的lazy effect,因此内部的响应式数据只会收集该effect 44 | * 不会收集外层的effect,导致computed内部的响应式数据变化时,外层effect函数不会触发 45 | * 解决方式: 46 | * 在访问计算属性时,触发依赖收集,将外层effect和计算属性的value值进行关联 47 | */ 48 | return this._oldValue; 49 | } 50 | } 51 | 52 | // 计算属性 53 | // 接受一个getter 54 | export function computed(getter: ComputedGetter) { 55 | return new ComputedRefImpl(getter); 56 | } 57 | -------------------------------------------------------------------------------- /packages/reactivity/src/dep.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-30 17:31:04 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-08-10 15:16:51 6 | */ 7 | 8 | import { ReactiveEffect, trackOpBit } from "./effect"; 9 | 10 | export type Dep = Set & TrackedMarkers; 11 | type TrackedMarkers = { 12 | // wasTracked 13 | w: number; 14 | // newTracked; 15 | n: number; 16 | }; 17 | 18 | export function createDep(): Dep { 19 | const dep = new Set() as Dep; 20 | // 标记dep是否已经被收集过 21 | dep.w = 0; 22 | // 标记dep是否是最新收集的(当前effect层收集的) 23 | dep.n = 0; 24 | return dep; 25 | } 26 | 27 | /** 28 | * @author: Zhouqi 29 | * @description: 是否是已经收集过的依赖 30 | */ 31 | export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0; 32 | 33 | /** 34 | * @author: Zhouqi 35 | * @description: 是否是当前effect层最新收集的依赖 36 | */ 37 | export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0; 38 | 39 | /** 40 | * @author: Zhouqi 41 | * @description: 初始化dep的标记位 42 | */ 43 | export function initDepMarkers({ deps }: ReactiveEffect) { 44 | if (deps.length) { 45 | for (let i = 0; i < deps.length; i++) { 46 | // 表示dep在某一层effect中已经被收集过 47 | deps[i].w |= trackOpBit; 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * @author: Zhouqi 54 | * @description: 重置dep标记位并处理过期依赖 55 | */ 56 | export function finalizeDepMarkers(effect: ReactiveEffect) { 57 | const { deps } = effect; 58 | if (deps.length) { 59 | // 最新的dep长度 60 | let l = 0; 61 | for (let i = 0; i < deps.length; i++) { 62 | const dep = deps[i]; 63 | // 判断是否是已经收集过的但不是最新收集的依赖,是的话就清除 64 | if (wasTracked(dep) && !newTracked(dep)) { 65 | dep.delete(effect); 66 | } else { 67 | l++; 68 | } 69 | // 清除dep中关于当前effect层的标记位 70 | dep.w &= ~trackOpBit; 71 | dep.n &= ~trackOpBit; 72 | } 73 | deps.length = l; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/reactivity/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-03 21:32:55 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-03 21:45:14 6 | */ 7 | export * from "./ref"; 8 | export * from "./effect"; 9 | export * from "./reactive"; 10 | -------------------------------------------------------------------------------- /packages/reactivity/src/operations.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-11 13:13:36 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 19:51:35 6 | */ 7 | export const enum TrackOpTypes { 8 | GET = "get", 9 | HAS = "has", 10 | ITERATE = "iterate", 11 | } 12 | 13 | export const enum TriggerOpTypes { 14 | SET = "set", 15 | ADD = "add", 16 | DELETE = "delete", 17 | CLEAR = "clear", 18 | } 19 | -------------------------------------------------------------------------------- /packages/reactivity/src/reactive.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-20 20:47:45 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2023-03-06 14:01:46 6 | */ 7 | import { def, isObject, toRawType } from "@simplify-vue/shared"; 8 | import { 9 | reactiveHandler, 10 | readonlyHandler, 11 | shallowReadonlyHandler, 12 | shallowReactiveHandler, 13 | } from "./baseHandlers"; 14 | 15 | import { 16 | mutableCollectionHandlers, 17 | readonlyCollectionHandlers, 18 | shallowCollectionHandlers, 19 | shallowReadonlyCollectionHandlers, 20 | } from "./collectionHandlers"; 21 | 22 | export const enum ReactiveFlags { 23 | IS_REACTIVE = "__v_isReactive", 24 | IS_READONLY = "__v_isReadonly", 25 | IS_SHALLOW = "__v_isShallow", 26 | RAW = "__v_raw", 27 | SKIP = "__v_skip", 28 | } 29 | 30 | export interface Target { 31 | [ReactiveFlags.IS_REACTIVE]?: boolean; 32 | [ReactiveFlags.IS_READONLY]?: boolean; 33 | [ReactiveFlags.IS_SHALLOW]?: boolean; 34 | [ReactiveFlags.RAW]?: any; 35 | length?: number; 36 | } 37 | 38 | const enum TargetType { 39 | INVALID, 40 | COMMON, 41 | COLLECTION, 42 | } 43 | 44 | function targetTypeMap(type: string) { 45 | switch (type) { 46 | case "Object": 47 | case "Array": 48 | return TargetType.COMMON; 49 | case "Map": 50 | case "Set": 51 | case "WeakMap": 52 | case "WeakSet": 53 | return TargetType.COLLECTION; 54 | default: 55 | return TargetType.INVALID; 56 | } 57 | } 58 | 59 | // 获取当前数据的类型 60 | function getTargetType(value: Target) { 61 | // 如果对象是被标记为永远不被响应式处理(markRaw)或者对象不能被扩展的,则直接返回INVALID类型 62 | // 否则返回对应的类型 63 | return value[ReactiveFlags.SKIP] || !Object.isExtensible(value) 64 | ? TargetType.INVALID 65 | : targetTypeMap(toRawType(value)); 66 | } 67 | 68 | // 缓存target->proxy的映射关系 69 | export const reactiveMap = new WeakMap(); 70 | export const shallowReactiveMap = new WeakMap(); 71 | export const readonlyMap = new WeakMap(); 72 | export const shallowReadonlyMap = new WeakMap(); 73 | 74 | // 创建响应式对象 75 | export function reactive(raw: object) { 76 | // 如果对象是一个只读的proxy,则直接返回 77 | if (isReadonly(raw)) { 78 | return raw; 79 | } 80 | return createReactiveObject( 81 | raw, 82 | reactiveHandler, 83 | mutableCollectionHandlers, 84 | reactiveMap 85 | ); 86 | } 87 | 88 | // 创建浅响应对象 89 | export function shallowReactive(raw: object) { 90 | return createReactiveObject( 91 | raw, 92 | shallowReactiveHandler, 93 | shallowCollectionHandlers, 94 | shallowReactiveMap 95 | ); 96 | } 97 | 98 | // 创建只读对象 99 | export function readonly(raw: object) { 100 | return createReactiveObject( 101 | raw, 102 | readonlyHandler, 103 | readonlyCollectionHandlers, 104 | readonlyMap 105 | ); 106 | } 107 | 108 | // 创建浅只读对象 109 | export function shallowReadonly(raw: object) { 110 | return createReactiveObject( 111 | raw, 112 | shallowReadonlyHandler, 113 | shallowReadonlyCollectionHandlers, 114 | shallowReadonlyMap 115 | ); 116 | } 117 | 118 | // 对象是不是响应式的 119 | export function isReactive(variable: unknown): boolean { 120 | return !!(variable as Target)[ReactiveFlags.IS_REACTIVE]; 121 | } 122 | 123 | // 对象是不是只读的 124 | export function isReadonly(variable: unknown): boolean { 125 | return !!(variable as Target)[ReactiveFlags.IS_READONLY]; 126 | } 127 | 128 | // 对象是不是readonly或者reactive的 129 | export function isProxy(variable: unknown): boolean { 130 | return isReactive(variable) || isReadonly(variable); 131 | } 132 | 133 | // 返回代理对象的原始对象 134 | export function toRaw(observed: T): T { 135 | const raw = observed && observed[ReactiveFlags.RAW]; 136 | // toRaw返回的对象依旧是代理对象,则递归去找原始对象 137 | return raw ? toRaw(raw) : observed; 138 | } 139 | 140 | // 对传入的值做处理,如果是对象,则进行reactive处理 141 | export const toReactive = (value) => 142 | isObject(value) ? reactive(value) : value; 143 | 144 | // 对传入的值做处理,如果是对象,则进行readonly处理 145 | export const toReadonly = (value: T): T => 146 | isObject(value) ? readonly(value as Record) : value; 147 | 148 | function createReactiveObject( 149 | raw: Target, 150 | baseHandler: ProxyHandler, 151 | collectionHandlers: ProxyHandler, 152 | proxyMap: WeakMap 153 | ) { 154 | // 如果已经是响应式对象了,直接返回。 155 | // TODO 除非是将响应式对象转化为readonly 156 | if (raw[ReactiveFlags.RAW]) { 157 | return raw; 158 | } 159 | 160 | /** 161 | * 如果映射表里有原始对象对应的代理对象,则直接返回,避免因同一个原始对象而创建出的代理对象不同导致比较失败 162 | * 例如: 163 | * const obj = {}; 164 | * const arr: any = reactive([obj]); 165 | * arr.includes(arr[0])应该是true,但是返回了false 166 | * 因为arr[0]是obj的响应式对象,arr.includes通过下标找到arr[0]时也是obj的响应式对象 167 | * 如果不缓存同一个target对应的代理对象,会导致因重复创建而比较失败的情况 168 | */ 169 | const existingProxy = proxyMap.get(raw); 170 | if (existingProxy) return existingProxy; 171 | 172 | const targetType = getTargetType(raw); 173 | // 如果对象被指定为永远不需要响应式处理或者对象不可扩展,则直接返回原始值 174 | if (targetType === TargetType.INVALID) { 175 | return raw; 176 | } 177 | 178 | const proxy = new Proxy( 179 | raw, 180 | // 集合类型例如Set、WeakSet、Map、WeakMap需要另外的handler处理 181 | targetType === TargetType.COLLECTION ? collectionHandlers : baseHandler 182 | ); 183 | proxyMap.set(raw, proxy); 184 | return proxy; 185 | } 186 | 187 | export function markRaw(value: T): T { 188 | def(value, ReactiveFlags.SKIP, true); 189 | return value; 190 | } 191 | -------------------------------------------------------------------------------- /packages/reactivity/src/ref.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-23 21:32:36 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2024-01-03 18:50:28 6 | */ 7 | 8 | import { hasChanged, isArray } from "@simplify-vue/shared"; 9 | import { createDep } from "./dep"; 10 | import { canTrack, trackEffects, triggerEffects } from "./effect"; 11 | import { isReactive, toRaw, toReactive } from "./reactive"; 12 | 13 | class RefImpl { 14 | private _value; 15 | public deps; 16 | private _rawValue; 17 | public readonly __v_isRef = true; 18 | 19 | constructor(value, readonly __v_isShallow = false) { 20 | // 如果不是shallow的情况且value是obj时需要响应式处理 21 | this._value = __v_isShallow ? value : toReactive(value); 22 | // 如果不是shallow的情况且value如果是响应式的,则需要拿到原始对象 23 | // ref.spec.ts(should make nested properties reactive) 24 | this._rawValue = __v_isShallow ? value : toRaw(value); 25 | this.deps = new Set(); 26 | } 27 | 28 | get value() { 29 | trackRefValue(this); 30 | return this._value; 31 | } 32 | 33 | set value(newValue) { 34 | // 如果不是shallow的情况且value如果是响应式的,则需要拿到原始对象 ref.spec.ts(should not trigger when setting value to same proxy) 35 | newValue = this.__v_isShallow ? newValue : toRaw(newValue); 36 | // 比较的时候拿原始值去比较 37 | if (hasChanged(newValue, this._rawValue)) { 38 | this._rawValue = newValue; 39 | // 如果不是shallow的情况且新的值时普通对象的话需要去响应式处理 40 | this._value = this.__v_isShallow ? newValue : toReactive(newValue); 41 | triggerRefValue(this); 42 | } 43 | } 44 | } 45 | 46 | class ObjectRefImpl { 47 | public readonly __v_isRef = true; 48 | constructor(private readonly _target, private readonly _key) { } 49 | get value() { 50 | const val = this._target[this._key]; 51 | return val; 52 | } 53 | set value(newValue) { 54 | this._target[this._key] = newValue; 55 | } 56 | } 57 | 58 | export function ref(value) { 59 | return createRef(value, false); 60 | } 61 | 62 | // 代理ref对象,使之不需要要通过.value去访问值(例如在template里面使用ref时不需要.value) 63 | export function proxyRefs(objectWithRefs) { 64 | // 如果是reactive对象则不需要处理,直接返回对象 65 | return isReactive(objectWithRefs) 66 | ? objectWithRefs 67 | : new Proxy(objectWithRefs, { 68 | get(target, key, receiver) { 69 | return unRef(Reflect.get(target, key, receiver)); 70 | }, 71 | set(target, key, newValue, receiver) { 72 | // 旧的值是ref,但是新的值不是ref时,直接修改.value的值。否则直接设置新值 73 | const oldValue = target[key]; 74 | if (isRef(oldValue) && !isRef(newValue)) { 75 | oldValue.value = newValue; 76 | return true; 77 | } 78 | return Reflect.set(target, key, newValue, receiver); 79 | }, 80 | }); 81 | } 82 | 83 | // 浅ref,只对value做响应式处理 84 | export function shallowRef(value) { 85 | return createRef(value, true); 86 | } 87 | 88 | // 判断一个值是不是ref 89 | export function isRef(ref) { 90 | return !!(ref && ref.__v_isRef === true); 91 | } 92 | 93 | // 如果参数是一个ref,则返回内部值,否则返回参数本身 94 | export function unRef(ref) { 95 | return isRef(ref) ? ref.value : ref; 96 | } 97 | 98 | // 收集ref的依赖函数 99 | export function trackRefValue(ref) { 100 | if (canTrack()) { 101 | trackEffects(ref.deps || (ref.deps = createDep())); 102 | } 103 | } 104 | 105 | // 触发ref依赖函数 106 | export function triggerRefValue(ref) { 107 | if (ref.deps) { 108 | triggerEffects(ref.deps); 109 | } 110 | } 111 | 112 | // 可以用来为源响应式对象上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接 113 | export function toRef(object, key) { 114 | return isRef(object) ? object : new ObjectRefImpl(object, key); 115 | } 116 | 117 | // 将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref 118 | export function toRefs(object) { 119 | if (isRef(object)) return object; 120 | const result = isArray(object) ? new Array(object.length) : {}; 121 | for (const key in object) { 122 | result[key] = toRef(object, key); 123 | } 124 | return result; 125 | } 126 | 127 | // 创建ref的工厂函数 128 | function createRef(value, shallow) { 129 | if (isRef(value)) return value; 130 | return new RefImpl(value, shallow); 131 | } 132 | -------------------------------------------------------------------------------- /packages/runtime-core/__tests__/watch.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-07 14:13:10 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 17:03:16 6 | */ 7 | import { reactive, ref } from "@simplify-vue/reactivity"; 8 | import { watch, watchEffect, watchPostEffect } from "../src"; 9 | 10 | describe("first", () => { 11 | test("watch array", () => { 12 | const ref1: any = ref(1); 13 | let i = 1; 14 | 15 | watch([ref1], () => { 16 | i++; 17 | }); 18 | 19 | ref1.value++; 20 | 21 | expect(i).toBe(2); 22 | }); 23 | 24 | // test("watch array", () => { 25 | // const ref1: any = ref(1); 26 | // const ref2: any = ref(2); 27 | // let i = 1; 28 | 29 | // watch([ref1, ref2], () => { 30 | // i++; 31 | // }); 32 | 33 | // //TODO 在vue中一个事件里面同时修改监听的变量只会执行一次回调 34 | // function change() { 35 | // ref1.value++; 36 | // ref2.value++; 37 | // } 38 | 39 | // change(); 40 | 41 | // expect(i).toBe(2); 42 | // }); 43 | 44 | test("watch reactive object", () => { 45 | const obj: any = reactive({ count: 1 }); 46 | let i = 1; 47 | watch(obj, () => { 48 | i++; 49 | }); 50 | obj.count++; 51 | expect(i).toBe(2); 52 | }); 53 | 54 | test("watch ref value", () => { 55 | const obj: any = ref(1); 56 | let i = 1; 57 | watch(obj, () => { 58 | i++; 59 | }); 60 | obj.value++; 61 | expect(i).toBe(2); 62 | }); 63 | 64 | test("watch function getter", () => { 65 | const obj: any = reactive({ count: 1 }); 66 | let i = 1; 67 | watch( 68 | () => obj.count, 69 | () => { 70 | i++; 71 | } 72 | ); 73 | obj.count++; 74 | expect(i).toBe(2); 75 | }); 76 | 77 | test("watch value changed", () => { 78 | const obj: any = reactive({ count: 1 }); 79 | let i = 1; 80 | let j = 0; 81 | watch( 82 | () => obj.count, 83 | (newValue, oldValue) => { 84 | i = newValue; 85 | j = oldValue; 86 | } 87 | ); 88 | obj.count++; 89 | expect(i).toBe(2); 90 | expect(j).toBe(1); 91 | obj.count++; 92 | expect(i).toBe(3); 93 | expect(j).toBe(2); 94 | }); 95 | 96 | test("watch immediate options", () => { 97 | const obj: any = reactive({ count: 1 }); 98 | let i = 1; 99 | let j = 0; 100 | watch( 101 | () => obj.count, 102 | (newValue, oldValue) => { 103 | i = newValue; 104 | j = oldValue; 105 | }, 106 | { 107 | immediate: true, 108 | } 109 | ); 110 | expect(i).toBe(1); 111 | expect(j).toBe(undefined); 112 | }); 113 | 114 | test("watch flush options", () => { 115 | const obj: any = reactive({ count: 1 }); 116 | let i = 1; 117 | watch( 118 | () => obj.count, 119 | (newValue, oldValue) => { 120 | i = newValue; 121 | }, 122 | { 123 | flush: "post", 124 | } 125 | ); 126 | 127 | obj.count++; 128 | 129 | expect(i).toBe(1); 130 | 131 | Promise.resolve().then(() => { 132 | expect(i).toBe(2); 133 | }); 134 | 135 | setTimeout(() => { 136 | expect(i).toBe(2); 137 | }, 1000); 138 | }); 139 | 140 | test("watch effect be overdue", () => { 141 | const obj: any = reactive({ count: 1 }); 142 | let i = 0; 143 | let result; 144 | const fetch = (data, time) => { 145 | return new Promise((resolve) => { 146 | setTimeout(() => { 147 | resolve(data); 148 | }, time); 149 | }); 150 | }; 151 | watch( 152 | () => obj.count, 153 | async (newValue, oldValue, onCleanup) => { 154 | let flag = false; 155 | onCleanup(() => { 156 | flag = true; 157 | }); 158 | i++; 159 | if (i === 1) { 160 | const res = await fetch("A", 2000); 161 | if (!flag) { 162 | result = res; 163 | } 164 | } else { 165 | const res = await fetch("B", 1000); 166 | if (!flag) { 167 | result = res; 168 | } 169 | } 170 | } 171 | ); 172 | obj.count++; 173 | obj.count++; 174 | 175 | setTimeout(() => { 176 | expect(result).toBe("B"); 177 | }, 3000); 178 | }); 179 | 180 | test("watchEffect", () => { 181 | const refs: any = ref(1); 182 | let i = 1; 183 | 184 | watchEffect(() => { 185 | i++; 186 | refs.value; 187 | }); 188 | refs.value++; 189 | expect(i).toBe(3); 190 | }); 191 | 192 | test("watchEffect flush post", () => { 193 | const refs: any = ref(1); 194 | let i = 1; 195 | 196 | watchEffect( 197 | () => { 198 | i++; 199 | refs.value; 200 | }, 201 | { 202 | flush: "post", 203 | } 204 | ); 205 | expect(i).toBe(1); 206 | 207 | Promise.resolve().then(() => { 208 | expect(i).toBe(2); 209 | }); 210 | }); 211 | 212 | test("watchEffect flush post", () => { 213 | const refs: any = ref(1); 214 | let i = 1; 215 | 216 | watchPostEffect(() => { 217 | i++; 218 | refs.value; 219 | }); 220 | expect(i).toBe(1); 221 | 222 | Promise.resolve().then(() => { 223 | expect(i).toBe(2); 224 | }); 225 | }); 226 | 227 | test("unwatch", () => { 228 | const refs: any = ref(1); 229 | let i = 1; 230 | 231 | const unwatch = watchEffect(() => { 232 | i++; 233 | refs.value; 234 | }); 235 | unwatch(); 236 | refs.value++; 237 | expect(i).toBe(2); 238 | }); 239 | }); 240 | -------------------------------------------------------------------------------- /packages/runtime-core/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-26 10:55:52 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 11:43:08 6 | */ 7 | export * from './dist/runtime-core.esm.js'; 8 | -------------------------------------------------------------------------------- /packages/runtime-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simplify-vue/runtime-core", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@simplify-vue/reactivity": "workspace:^1.0.0", 15 | "@simplify-vue/shared": "workspace:^1.0.0" 16 | } 17 | } -------------------------------------------------------------------------------- /packages/runtime-core/src/apiAsyncComponent.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-14 22:19:23 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2023-12-22 14:44:21 6 | */ 7 | import { isFunction } from "@simplify-vue/shared"; 8 | import { ref } from "@simplify-vue/reactivity"; 9 | import { defineComponent } from "./apiDefineComponent"; 10 | import { currentInstance } from "./component"; 11 | import { createTextVNode, createVNode } from "./vnode"; 12 | import { onBeforeUnmount } from "./apiLifecycle"; 13 | 14 | // 创建异步组件 15 | export function defineAsyncComponent(source) { 16 | // 如果source是一个函数的话,说明传入的就是一个异步加载组件的函数 17 | if (isFunction(source)) { 18 | source = { loader: source }; 19 | } 20 | 21 | const { 22 | // 异步加载组件的函数 23 | loader, 24 | // 加载组件 25 | loadingComponent, 26 | // 组件加载失败时显示的错误组件 27 | errorComponent, 28 | // 展示加载组件前的延迟时间,默认为 200ms 29 | delay = 200, 30 | // 超时时间 31 | timeout, 32 | // suspensible, 33 | onError: userOnError, 34 | } = source; 35 | 36 | // 解析到的组件 37 | let resolvedComp; 38 | // 错误重试次数 39 | let retries = 0; 40 | 41 | // 重试函数,返回异步加载组件的函数 42 | const retry = () => { 43 | retries++; 44 | return load(); 45 | }; 46 | 47 | const load = () => { 48 | return loader() 49 | .then((c) => { 50 | resolvedComp = c.default; 51 | return c.default; 52 | }) 53 | .catch((err) => { 54 | err = new Error("组件加载失败"); 55 | // 如果用户定义了错误处理函数,则将控制权交给用户 56 | if (userOnError) { 57 | return new Promise((resolve, reject) => { 58 | const userRetry = () => resolve(retry()); 59 | const userFail = () => reject(err); 60 | userOnError(err, userRetry, userFail, retries + 1); 61 | }); 62 | } else { 63 | throw err; 64 | } 65 | }); 66 | }; 67 | 68 | return defineComponent({ 69 | name: "AsyncComponentWrapper", 70 | setup() { 71 | const loaded = ref(false); 72 | const errorLoaded = ref(false); 73 | // 注意:0是立即加载,不传默认是200 74 | const delayed = ref(!!delay); 75 | 76 | let timeoutTimer; 77 | let delayTimer; 78 | 79 | // defineAsyncComponent是一层包装组件,当写模板的时候可能会写一些插槽节点,这些节点需要传递给 80 | // 真正异步加载的组件使用,而不是给这个包装组件使用,因此我们需要获取当前包装组件的实例,将其身上的 81 | // slots传递给异步加载的组件 82 | const instance = currentInstance; 83 | 84 | if (delay) { 85 | delayTimer = setTimeout(() => { 86 | /** 87 | * 指定时间后加载loading组件 88 | * 需要指定延迟时间是因为异步组件可能加载很快,如果不加延迟立马使用loading组件,可能会马上又把 89 | * loading组件销毁,这样就会有一个闪烁的过程。因此可以设定一个延迟时间尽可能避免这种情况出现。 90 | */ 91 | if (!loaded.value) { 92 | delayed.value = false; 93 | } 94 | }, delay); 95 | } 96 | 97 | // 如果设置了超时时间,则开启一个定时器,定时回调任务触发时表示组件加载超时了 98 | if (timeout != null) { 99 | timeoutTimer = setTimeout(() => { 100 | // 组件没有加载成功且没有加载失败的情况下,如果加载超时了,则赋值超时错误信息 101 | if (!loaded.value && !errorLoaded.value) { 102 | const error = new Error( 103 | `Async component timed out after ${timeout}ms.` 104 | ); 105 | errorLoaded.value = error; 106 | } 107 | }, timeout); 108 | } 109 | 110 | // 组件销毁前清除定时器 111 | onBeforeUnmount(() => { 112 | clearTimeout(timeoutTimer); 113 | clearTimeout(delayTimer); 114 | delayTimer = null; 115 | timeoutTimer = null; 116 | }); 117 | 118 | load() 119 | .then(() => { 120 | // 组件加载成功,修改标记 121 | loaded.value = true; 122 | }) 123 | .catch((err) => { 124 | // 如果用户定义了错误处理函数并且调了用fail才会进到这里 125 | errorLoaded.value = err; 126 | }); 127 | 128 | return () => { 129 | // 根据组件加载成功标记来渲染 130 | if (loaded.value && resolvedComp) { 131 | return createInnerComp(resolvedComp, instance); 132 | } else if (errorLoaded.value && errorComponent) { 133 | // 将错误信息传递给error组件,便于用户进行更精细的操作 134 | return createVNode(errorComponent, { 135 | error: errorLoaded.value, 136 | }); 137 | } else if (!delayed.value && loadingComponent) { 138 | return createVNode(loadingComponent); 139 | } 140 | return createTextVNode(""); 141 | }; 142 | }, 143 | }); 144 | } 145 | 146 | /** 147 | * @author: Zhouqi 148 | * @description: 生成异步组件的vnode 149 | * @param comp 组件配置 150 | * @return vnode 151 | */ 152 | function createInnerComp(comp, instance?) { 153 | // slots可以理解,不知为何要传props,包装组件没有通过props去接受,上次传递下来的都会放在包装组件的attrs上, 154 | // 这个attrs还是能够传递到异步加载的组件上,并且能和props进行合并 155 | const { 156 | vnode: { children, props }, 157 | } = instance; 158 | return createVNode(comp, props, children); 159 | } 160 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiCreateApp.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-03 14:53:41 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2023-03-10 14:44:44 6 | */ 7 | 8 | import { createVNode } from "./vnode"; 9 | 10 | export function createAppApi(render) { 11 | return function createApp(rootComponent) { 12 | const app = { 13 | _container: null, 14 | use() { }, 15 | mixin() { }, 16 | component() { }, 17 | directive() { }, 18 | mount(rootContainer) { 19 | // 创建虚拟节点 20 | const vnode = createVNode(rootComponent); 21 | // 渲染真实节点 22 | render(vnode, rootContainer); 23 | app._container = rootContainer; 24 | }, 25 | unmount() { 26 | render(null, app._container); 27 | }, 28 | provide() { }, 29 | }; 30 | return app; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiDefineComponent.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-14 22:25:46 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 09:42:01 6 | */ 7 | import { isFunction } from "@simplify-vue/shared"; 8 | 9 | // 返回options,对ts支持度更好 10 | export function defineComponent(options) { 11 | return isFunction(options) ? { setup: options } : options; 12 | } 13 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiInject.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-02 14:43:10 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 09:42:05 6 | */ 7 | import { isFunction } from "@simplify-vue/shared"; 8 | import { currentInstance } from "./component"; 9 | 10 | /** 11 | * @author: Zhouqi 12 | * @description: 依赖提供 13 | * @param key 键 14 | * @param value 值 15 | */ 16 | export function provide(key: string, value: unknown) { 17 | if (!currentInstance) { 18 | return; 19 | } 20 | const instance: any = currentInstance; 21 | let provides = instance.provides; 22 | const parentProvides = instance.parent && instance.parent.provides; 23 | /** 24 | * 默认情况下,当前组件实例的provides继承父组件的provides 25 | * 如果当前组件需要定义provides,则需要实现原型链的方式,避免当前组件实例在创建provides的时候 26 | * 影响到父组件的provides。 27 | * 当通过inject注入的时候,也是按照原型链的方式去查找 28 | */ 29 | if (provides === parentProvides) { 30 | // 如果当前组件实例的provides等于父组件的provides,则表示初始化的状态,此时设置当前组件provides的原型为父组件的provides 31 | provides = instance.provides = Object.create(parentProvides); 32 | } 33 | provides[key] = value; 34 | } 35 | 36 | /** 37 | * @author: Zhouqi 38 | * @description: 依赖注入 39 | * @param key 键 40 | * @param defaultValue 默认值 41 | * @param treatDefaultAsFactory 如果默认值是一个函数,是否执行函数得到返回结果 42 | */ 43 | export function inject( 44 | key: string, 45 | defaultValue, 46 | treatDefaultAsFactory: boolean = false 47 | ) { 48 | const instance: any = currentInstance; 49 | if (instance) { 50 | const provides = instance.parent?.provides; 51 | // 如果要注入的key存在于父组件的provides中则返回值 52 | if (key in provides) { 53 | return provides[key]; 54 | } 55 | // 如果要注册的key不存在于父组件的provides中,则有默认值时返回默认值 56 | if (defaultValue) { 57 | return treatDefaultAsFactory && isFunction(defaultValue) 58 | ? defaultValue.call(instance.proxy) 59 | : defaultValue; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiLifecycle.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-06 10:04:06 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-08 19:38:42 6 | */ 7 | import { 8 | currentInstance, 9 | LifecycleHooks, 10 | setCurrentInstance, 11 | unsetCurrentInstance, 12 | } from "./component"; 13 | 14 | /** 15 | * @author: Zhouqi 16 | * @description: 注入钩子函数 17 | * @param lifecycleHook 生命周期钩子名称 18 | * @param hook 要触发的生命周期函数 19 | */ 20 | function injectHooks( 21 | lifecycleHook: string, 22 | hook: Function, 23 | target: object | null = currentInstance 24 | ) { 25 | if (target) { 26 | const hooks = target[lifecycleHook] || (target[lifecycleHook] = []); 27 | // 包装一层 28 | const wrappedHook = () => { 29 | /** 30 | * 在执行生命周期函数时可能需要访问当前组件实例getCurrentInstance 31 | * 但是执行生命周期回调函数的时机是在setup之后,会访问不到当前组件实例 32 | * 因此我们需要在这里重新设置currentInstance 33 | */ 34 | setCurrentInstance(target); 35 | hook(); 36 | // 重置currentInstance 37 | unsetCurrentInstance(); 38 | }; 39 | hooks.push(wrappedHook); 40 | } 41 | } 42 | 43 | /** 44 | * @author: Zhouqi 45 | * @description: 注册生命周期钩子函数 46 | * @param LifecycleHook 生命周期钩子名称 47 | */ 48 | export const createHook = (lifecycleHook) => (hook) => 49 | injectHooks(lifecycleHook, hook); 50 | 51 | // 只能在setup中使用,因为内部需要使用当前组件实例 52 | // 组件挂载之前触发 53 | export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT); 54 | // 组件挂载后触发 55 | export const onMounted = createHook(LifecycleHooks.MOUNTED); 56 | // 组件更新前触发 57 | export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE); 58 | // 更新后触发 59 | export const onUpdated = createHook(LifecycleHooks.UPDATED); 60 | // 组件卸载之前触发 61 | export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT); 62 | // 组件卸载完成后触发 63 | export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED); 64 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiWatch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-10 19:35:50 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-09-16 16:43:26 6 | */ 7 | import { isReactive, isRef, ReactiveEffect } from "@simplify-vue/reactivity"; 8 | import { 9 | EMPTY_OBJ, 10 | extend, 11 | isArray, 12 | isFunction, 13 | isObject, 14 | isPlainObject, 15 | } from "@simplify-vue/shared"; 16 | import { queuePostRenderEffect } from "./renderer"; 17 | 18 | /** 19 | * @author: Zhouqi 20 | * @description: watch函数 21 | * @param source 监听的内容 22 | * @param cb 监听内容变化时触发的回调函数 23 | * @param options 配置 24 | * @return unwatch 取消观测的函数 25 | */ 26 | export function watch(source, cb, options?) { 27 | return doWatch(source, cb, options); 28 | } 29 | 30 | /** 31 | * @author: Zhouqi 32 | * @description: 立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。 33 | * @param effectFn 34 | * @param options 35 | * @return unwatch 取消观测的函数 36 | */ 37 | export function watchEffect(effectFn, options?) { 38 | // watchEffect 第二个参数为null 39 | return doWatch(effectFn, null, options); 40 | } 41 | 42 | /** 43 | * @author: Zhouqi 44 | * @description: watchEffect 的别名,带有 flush: 'post' 选项 45 | * @param effectFn 46 | * @param options 47 | * @return unwatch 取消观测的函数 48 | */ 49 | export function watchPostEffect(effectFn, options?) { 50 | return doWatch(effectFn, null, extend(options || {}, { flush: "post" })); 51 | } 52 | 53 | /** 54 | * @author: Zhouqi 55 | * @description: watchEffect 的别名,带有 flush: 'sync' 选项 56 | * @param effectFn 57 | * @param options 58 | * @return unwatch 取消观测的函数 59 | */ 60 | export function watchSyncEffect(effectFn, options?) { 61 | return doWatch(effectFn, null, extend(options || {}, { flush: "sync" })); 62 | } 63 | 64 | function doWatch( 65 | source, 66 | cb, 67 | { flush, deep, immediate }: Record = EMPTY_OBJ 68 | ) { 69 | // 定义getter函数 70 | let getter; 71 | // 旧的值 72 | let oldValue; 73 | // 新的值 74 | let newValue; 75 | 76 | // 根据source的类型生成不同的getter 77 | if (isRef(source)) { 78 | getter = () => source.value; 79 | } else if (isReactive(source)) { 80 | getter = () => source; 81 | // 传入的如果是reactive的对象,默认把deep置为true进行深度监听 82 | deep = true; 83 | } else if (isArray(source)) { 84 | getter = () => 85 | source.map((s) => { 86 | if (isRef(s)) { 87 | return s.value; 88 | } else if (isReactive(source)) { 89 | return traverse(s); 90 | } else if (isFunction(s)) { 91 | return s(); 92 | } 93 | }); 94 | } else if (isFunction(source)) { 95 | // 如果传入的是方法,直接赋值给getter 96 | getter = source; 97 | } 98 | 99 | /** 100 | * cleanup:副作用过期的回调 101 | * 假设watch的回调是请求接口,当第一次数据变化时请求一次接口,紧接着第二次数据变化时又请求了一次接口 102 | * 当第一个接口比第二个接口慢时,先获取到了第二个接口的数据,然后获取到第一个接口的数据,这是不正确的 103 | * 实际上执行第二次回调的时候,第一次的回调应该是过期状态,为了提示用户副作用过期了,需要提供一个cleanup 104 | * 函数,这个函数存储的是用户传入的副作用过期的回调,每次执行回调执行,假如有cleanup,则执行一次,告诉用户 105 | * 上一次的已经过期了 106 | */ 107 | let cleanup; 108 | const onCleanup = (fn) => { 109 | cleanup = fn; 110 | }; 111 | 112 | // 包装回调任务 113 | const job = () => { 114 | if (cb) { 115 | // newValue在值变化后触发的scheduler里面获取 116 | newValue = effect.run(); 117 | // 辅助用户提示副作用过期 118 | if (cleanup) { 119 | cleanup(); 120 | } 121 | cb(newValue, oldValue, onCleanup); 122 | // 重新赋值给旧的 123 | oldValue = newValue; 124 | } else { 125 | // 没有cb说明是watchEffect,直接执行副作用函数 126 | effect.run(); 127 | } 128 | }; 129 | 130 | if (cb && deep) { 131 | const baseGetter = getter; 132 | // 深度读取依赖的函数 133 | getter = () => traverse(baseGetter()); 134 | } 135 | 136 | // watch的原理就是监听值的变化,通过自定义调度器来执行回调。当监听到的依赖的值变化时会触发effect上的schedular函数,从而触发回调函数 137 | let scheduler; 138 | if (flush === "sync") { 139 | // 立即执行,没有加入到回调缓冲队列中 140 | scheduler = job; 141 | } else if (flush === "post") { 142 | // 放进微任务队列中执行(组件更新后) 143 | scheduler = () => queuePostRenderEffect(job); 144 | } else { 145 | // pre 146 | scheduler = () => { 147 | // 组件更新前调用 148 | // TODO 加入回调缓冲队列中 组件更新前调用 queuePreFlushCb(job) 149 | job(); 150 | }; 151 | } 152 | 153 | const effect = new ReactiveEffect(getter, scheduler); 154 | 155 | if (cb) { 156 | if (immediate) { 157 | // 立即执行一次回调 158 | job(); 159 | } else { 160 | // 默认执行一次,获取旧的值 161 | oldValue = effect.run(); 162 | } 163 | } 164 | // 没有传入回调函数说明是watchEffect 165 | else if (flush === "post") { 166 | // 如果flush是post,则放入微任务队列中执行 167 | queuePostRenderEffect(effect.run.bind(effect)); 168 | } else { 169 | effect.run(); 170 | } 171 | 172 | return () => { 173 | // 移除依赖 174 | effect.stop(); 175 | }; 176 | } 177 | 178 | // 深度读取属性 179 | function traverse(value, seen = new Set()) { 180 | // 如果值被读取过或者值是一个普通类型则直接返回值 181 | if (!isObject(value) || value === null || seen.has(value)) return value; 182 | 183 | // 通过Set来防止添加重复的值 184 | seen.add(value); 185 | if (isRef(value)) { 186 | traverse(value.value, seen); 187 | } else if (isPlainObject(value)) { 188 | // 是对象则遍历每一个属性,递归调用traverse 189 | for (const key in value) { 190 | traverse(value[key], seen); 191 | } 192 | } 193 | return value; 194 | } 195 | -------------------------------------------------------------------------------- /packages/runtime-core/src/component/BaseTransition.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-17 14:41:48 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-17 19:22:02 6 | */ 7 | export const BaseTransition = { 8 | name: "BaseTransition", 9 | 10 | props: { 11 | // enter 12 | onBeforeEnter: Function, 13 | onEnter: Function, 14 | onLeave: Function, 15 | }, 16 | 17 | setup(props, { slots }) { 18 | return () => { 19 | const vnode = slots.default()[0]; 20 | const enterHooks = resolveTransitionHooks(props); 21 | setTransitionHooks(vnode, enterHooks); 22 | return vnode; 23 | }; 24 | }, 25 | }; 26 | 27 | /** 28 | * @author: Zhouqi 29 | * @description: 解析props 30 | * @param props 31 | * @return 钩子函数 32 | */ 33 | function resolveTransitionHooks(props) { 34 | const { onBeforeEnter, onEnter, onLeave } = props; 35 | 36 | const callhook = (hook, el) => { 37 | hook(el); 38 | }; 39 | 40 | const hooks = { 41 | beforeEnter: (el) => { 42 | let hook = onBeforeEnter; 43 | callhook(hook, el); 44 | }, 45 | enter(el) { 46 | let hook = onEnter; 47 | callhook(hook, el); 48 | }, 49 | leave(el, remove) { 50 | if (onLeave) { 51 | // 动画结束后再执行DOM移除操作 52 | onLeave(el, remove); 53 | } 54 | }, 55 | }; 56 | return hooks; 57 | } 58 | 59 | /** 60 | * @author: Zhouqi 61 | * @description: 在vnode上设置transition hook,供组件在其不同生命周期内调用 62 | * @param vnode 虚拟节点 63 | * @param hooks 钩子函数 64 | */ 65 | function setTransitionHooks(vnode, hooks) { 66 | vnode.transition = hooks; 67 | } 68 | -------------------------------------------------------------------------------- /packages/runtime-core/src/component/Teleport.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-17 10:16:39 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 09:41:39 6 | */ 7 | import { ShapeFlags } from "@simplify-vue/shared"; 8 | 9 | // 判断是否是Telepor组件 10 | export const isTeleport = (type: any): boolean => type.__isTeleport; 11 | 12 | // 判断是否设置了disabled属性 13 | const isTeleportDisabled = (props): boolean => 14 | props && (props.disabled || props.disabled === ""); 15 | 16 | export const Teleport = { 17 | __isTeleport: true, 18 | 19 | // 将teleport的渲染逻辑抽离到这个方法上,避免渲染器代码过多,并且当不用teleport时也利于treeshaking机制,在生成bundle过程中将其逻辑删除 20 | process(n1, n2, container, anchor, parentComponent, internals) { 21 | const { 22 | mc: mountChildren, 23 | pc: patchChildren, 24 | o: { insert, querySelector, createComment }, 25 | } = internals; 26 | 27 | const { props, shapeFlag, children } = n2; 28 | 29 | const disabled = isTeleportDisabled(props); 30 | 31 | if (n1 == null) { 32 | // 挂载 33 | // 同fragment节点,teleport组件本身不渲染任何元素,只是渲染插槽内的元素,因此需要建立锚点节点,防止更新的时候 34 | // 找不到锚点节点导致元素插入位置不对 35 | const startAnchor = (n2.el = createComment("teleport start")); 36 | const endAnchor = (n2.anchor = createComment("teleport end")); 37 | 38 | // 将锚点插入到原本的容器中 39 | insert(startAnchor, container, anchor); 40 | insert(endAnchor, container, anchor); 41 | 42 | // 找到要挂载到的目标节点 43 | const target = (n2.target = querySelector(props.to)); 44 | 45 | // 定义挂载函数 46 | const mount = (container, anchor) => { 47 | // Teleport的子组件一定是Children类型的 48 | if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 49 | mountChildren(children, container, anchor, parentComponent); 50 | } 51 | }; 52 | 53 | if (disabled) { 54 | // 禁用条件下依旧渲染到原来的位置 55 | mount(container, endAnchor); 56 | } else if (target) { 57 | mount(target, null); 58 | } 59 | } else { 60 | // 更新 61 | n2.el = n1.el; 62 | const mainAnchor = (n2.anchor = n1.anchor); 63 | const target = (n2.target = n1.target)!; 64 | const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!; 65 | const hasDisabled = isTeleportDisabled(n1.props); 66 | // 根据disabled属性获取对应的容器和锚点节点 67 | const currentContainer = hasDisabled ? container : target; 68 | const currentAnchor = hasDisabled ? mainAnchor : targetAnchor; 69 | 70 | // 进行节点更新 71 | patchChildren(n1, n2, currentContainer, currentAnchor, parentComponent); 72 | 73 | if (disabled) { 74 | // 如果之前是将插槽渲染到指定位置,现在不是的话,需要把插槽节点从指定位置移到原本位置 75 | if (!hasDisabled) { 76 | moveTeleport(n2, container, mainAnchor, internals); 77 | } 78 | } else { 79 | const { props: oldProps } = n1; 80 | const { props: newProps } = n2; 81 | 82 | // 指定位置的源变了,需要进行移动 83 | if ((newProps && newProps.to) !== (oldProps && oldProps.to)) { 84 | // 找到新的源 85 | const nextTarget = (n2.target = querySelector(newProps.to)); 86 | nextTarget && moveTeleport(n2, nextTarget, null, internals); 87 | } else if (hasDisabled) { 88 | // 如果之前是将插槽节点渲染到原本位置,现在不是的话,需要把节点从原本位置移动到指定位置 89 | moveTeleport(n2, target, targetAnchor, internals); 90 | } 91 | } 92 | } 93 | }, 94 | }; 95 | 96 | /** 97 | * @author: Zhouqi 98 | * @description: 移动节点 99 | * @param vnode 新的虚拟节点 100 | * @param container 容器 101 | * @param parentAnchor 锚点 102 | * @param internals dom操作集合 103 | */ 104 | function moveTeleport(vnode, container, parentAnchor, internals) { 105 | const { shapeFlag, children } = vnode; 106 | const { m: move } = internals; 107 | 108 | // 将子节点全部移动过去 109 | const length = children.length; 110 | if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 111 | for (let i = 0; i < length; i++) { 112 | move(children[i], container, parentAnchor); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentEmits.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-30 09:49:57 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-28 22:37:01 6 | */ 7 | 8 | import { 9 | camelize, 10 | toHandlerKey, 11 | isArray, 12 | extend, 13 | isOn, 14 | hasOwn, 15 | } from "@simplify-vue/shared"; 16 | 17 | /** 18 | * @author: Zhouqi 19 | * @description: 事件触发函数 20 | * @param instance 组件实例 21 | * @param event 事件名 22 | */ 23 | export function emit(instance, event, ...rawArg) { 24 | // 这里需要从instance的vnode上获取事件,在instance上的是找不到注册的事件,因为在initProps的时候 25 | // instance上面的props对象中的数据必须是通过props选项定义过的 26 | const { props } = instance.vnode; 27 | 28 | // 校验参数合法性 29 | const { emitsOptions } = instance; 30 | if (emitsOptions) { 31 | const validator = emitsOptions[event]; 32 | if (validator) { 33 | const isValid = validator(...rawArg); 34 | if (!isValid) { 35 | console.warn("参数不合法"); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * 针对两种事件名做处理 42 | * add-name 烤肉串命名 43 | * addName 驼峰命名 44 | * 如果是烤肉串命名,先转换为驼峰命名,再转化为AddName这种名称类型 45 | */ 46 | const handler = 47 | props[toHandlerKey(event)] || props[toHandlerKey(camelize(event))]; 48 | 49 | if (handler) { 50 | handler(...rawArg); 51 | } 52 | } 53 | 54 | /** 55 | * @author: Zhouqi 56 | * @description: 解析emits选项 57 | * @param component 组件实例 58 | * @return 解析后的结果 59 | */ 60 | export function normalizeEmitsOptions(component) { 61 | // TODO 缓存emits的解析结果,如果组件的emits被解析过了,就不再次解析 62 | const emits = component.emits; 63 | const normalizeResult = {}; 64 | if (isArray(emits)) { 65 | // 数组形式直接遍历赋值为null 66 | emits.forEach((key) => { 67 | normalizeResult[key] = null; 68 | }); 69 | } else { 70 | // 如果是对象,则可能有校验函数,直接合并到结果中即可 71 | extend(normalizeResult, emits); 72 | } 73 | return normalizeResult; 74 | } 75 | 76 | /** 77 | * @author: Zhouqi 78 | * @description: 判断是否是emits选项中定义的事件,如果是的话,不需要添加到attrs中,避免组件根节点被绑定上事件 79 | * @param emitsOptions emits序列化后的选项 80 | * @param key 事件名 81 | * @return 是否应该继续被attrs处理 82 | */ 83 | export function isEmitListener(emitsOptions, key) { 84 | if (!emitsOptions || !isOn(key)) { 85 | return false; 86 | } 87 | key = key.slice(2).toLowerCase(); 88 | return hasOwn(emitsOptions, key); 89 | } 90 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentOptions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-29 20:33:41 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-12 21:50:54 6 | */ 7 | import { reactive } from "@simplify-vue/reactivity"; 8 | import { isFunction, isObject } from "@simplify-vue/shared"; 9 | 10 | /** 11 | * @author: Zhouqi 12 | * @description: 合并vue2的options api 13 | * @param instance 组件实例 14 | */ 15 | export function applyOptions(instance) { 16 | const { data: dataOptions, components } = instance.type; 17 | const proxy = instance.proxy; 18 | 19 | if (dataOptions) { 20 | if (!isFunction(dataOptions)) return; 21 | const data = dataOptions.call(proxy, proxy); 22 | if (!isObject(data)) return; 23 | instance.data = reactive(data); 24 | } 25 | 26 | if (components) instance.components = components; 27 | } 28 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-27 21:17:03 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2024-01-24 17:16:52 6 | */ 7 | import { shallowReadonly } from "@simplify-vue/reactivity"; 8 | import { EMPTY_OBJ, hasOwn } from "@simplify-vue/shared"; 9 | 10 | const enum AccessTypes { 11 | SETUP, 12 | DATA, 13 | PROPS, 14 | } 15 | 16 | // 建立map映射对应vnode上的属性,利于扩展 17 | const publicPropertiesMap = { 18 | $el: (i) => i.vnode.el, 19 | $slots: (i) => shallowReadonly(i.slots), 20 | $props: (i) => shallowReadonly(i.props), 21 | $attrs: (i) => shallowReadonly(i.attrs), 22 | $data: (i) => i.data, 23 | }; 24 | 25 | export const PublicInstanceProxyHandlers = { 26 | get({ _: instance }, key) { 27 | const { setupState, props, accessCache, data, propsOptions } = instance; 28 | /** 29 | * 这里每次都会通过hasOwn去判断属性是不是存在于某一个对象中,这样是不如直接访问对应对象属性来的更快 30 | * 为了能够直接访问到指定对象上的属性,需要建立一个映射表,在第一次读取值的时候将属性和对象之间进行关联 31 | * 比如第一次读取属性值时,通过hasOwn发现是setupState上的属性,那就标记该属性是SETUP,下一次访问 32 | * 该属性时,判断到标记为SETUP,则直接从setupState获取值 33 | */ 34 | if (key[0] !== "$") { 35 | const t = accessCache[key]; 36 | if (t !== undefined) { 37 | switch (t) { 38 | case AccessTypes.SETUP: 39 | return setupState[key]; 40 | case AccessTypes.DATA: 41 | return data[key]; 42 | case AccessTypes.PROPS: 43 | return props[key]; 44 | } 45 | } 46 | else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { 47 | accessCache[key] = AccessTypes.SETUP; 48 | return setupState[key]; 49 | } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { 50 | accessCache[key] = AccessTypes.DATA; 51 | return data[key]; 52 | } else if (hasOwn(propsOptions[0], key)) { 53 | accessCache[key] = AccessTypes.PROPS; 54 | return props[key]; 55 | } 56 | } 57 | 58 | // 属性映射表上有对应的属性则返回对应的属性值 59 | const publicGetter = publicPropertiesMap[key]; 60 | if (publicGetter) { 61 | return publicGetter(instance); 62 | } 63 | 64 | console.warn( 65 | `Property ${JSON.stringify(key)} was accessed during render ` + 66 | `but is not defined on instance.` 67 | ); 68 | }, 69 | set({ _: instance }, key: string, value: any) { 70 | const { data, setupState, accessCache } = instance; 71 | const t = accessCache[key]; 72 | 73 | if (t !== undefined) { 74 | switch (t) { 75 | case AccessTypes.SETUP: 76 | setupState[key] = value; 77 | return true; 78 | case AccessTypes.DATA: 79 | data[key] = value; 80 | return true; 81 | } 82 | } 83 | 84 | if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { 85 | setupState[key] = value; 86 | } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { 87 | data[key] = value; 88 | } 89 | return true; 90 | }, 91 | }; 92 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentRenderContext.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-24 21:49:02 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-24 21:50:05 6 | */ 7 | export let currentRenderInstance = null; 8 | 9 | // 设置当前渲染实例 10 | export function setCurrentRenderingInstance(instance) { 11 | currentRenderInstance = instance; 12 | } 13 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentRenderUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-05 20:00:07 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-04 12:43:47 6 | */ 7 | 8 | import { ShapeFlags } from "@simplify-vue/shared"; 9 | import { setCurrentRenderingInstance } from "./componentRenderContext"; 10 | import { cloneVNode, normalizeVNode } from "./vnode"; 11 | 12 | /** 13 | * @author: Zhouqi 14 | * @description: 生成组件的vnode 15 | * @param instance 组件实例 16 | */ 17 | export function renderComponentRoot(instance) { 18 | const { 19 | attrs, 20 | props, 21 | render, 22 | proxy, 23 | vnode, 24 | inheritAttrs, 25 | type: Component, 26 | emit, 27 | slots, 28 | } = instance; 29 | 30 | let fallthroughAttrs; 31 | let result; 32 | 33 | setCurrentRenderingInstance(instance); 34 | 35 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 36 | // 处理有状态组件 37 | result = normalizeVNode(render.call(proxy, proxy)); 38 | fallthroughAttrs = attrs; 39 | } else { 40 | // 函数式组件就是一个render函数 41 | const render = Component; 42 | // 如果函数式组件定义了1一个以上的参数,则第二个参数为context对象,否则为null 43 | result = normalizeVNode( 44 | render(props, render.length > 1 ? { attrs, slots, emit } : null) 45 | ); 46 | fallthroughAttrs = attrs; 47 | } 48 | 49 | // attrs存在且可以继承attrs属性的情况下 50 | if (fallthroughAttrs && inheritAttrs !== false) { 51 | const attrsKeys = Object.keys(fallthroughAttrs); 52 | const { shapeFlag } = result; 53 | if ( 54 | attrsKeys.length && 55 | shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT) 56 | ) { 57 | result = cloneVNode(result, fallthroughAttrs); 58 | } 59 | } 60 | 61 | return result; 62 | } 63 | 64 | /** 65 | * @author: Zhouqi 66 | * @description: 是否需要更新组件 67 | * @param n1 旧的虚拟节点 68 | * @param n2 新的虚拟节点 69 | */ 70 | export function shouldUpdateComponent(n1, n2) { 71 | const { props: prevProps, children: prevChildren } = n1; 72 | const { props: nextProps, children: nextChildren } = n2; 73 | 74 | if (prevChildren || nextChildren) { 75 | if (!nextChildren || !(nextChildren as any).$stable) { 76 | return true; 77 | } 78 | } 79 | 80 | if (prevProps === nextProps) { 81 | return false; 82 | } 83 | if (!prevProps) { 84 | return !!nextProps; 85 | } 86 | if (!nextProps) { 87 | return true; 88 | } 89 | 90 | return hasPropsChanged(prevProps, nextProps); 91 | } 92 | 93 | /** 94 | * @author: Zhouqi 95 | * @description: 比较新旧props是否变化 96 | * @param prevProps 97 | * @param nextProps 98 | */ 99 | function hasPropsChanged(prevProps, nextProps) { 100 | if (Object.keys(prevProps).length !== Object.keys(prevProps).length) { 101 | return false; 102 | } 103 | for (const key in nextProps) { 104 | if (nextProps[key] !== prevProps[key]) { 105 | return true; 106 | } 107 | } 108 | return false; 109 | } 110 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentSlots.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-30 21:16:45 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 09:42:53 6 | */ 7 | 8 | import { isArray, ShapeFlags } from "@simplify-vue/shared"; 9 | 10 | /** 11 | * 插槽的vnode结构 12 | */ 13 | 14 | /** 15 | * @author: Zhouqi 16 | * @description: 初始化插槽 17 | * @param instance 组件实例 18 | * @param children 插槽节点 19 | */ 20 | export function initSlots(instance, children) { 21 | // 判断是不是插槽节点 22 | if (ShapeFlags.SLOTS_CHILDREN & instance.vnode.shapeFlag) { 23 | normalizeObjectSlots(children, (instance.slots = {})); 24 | } 25 | } 26 | 27 | /** 28 | * @author: Zhouqi 29 | * @description: 将children中的插槽节点赋值到组件实例的slots对象上 30 | * @param children 插槽节点 31 | * @param slots 插槽数据存储目标 32 | */ 33 | function normalizeObjectSlots(children, slots) { 34 | // slots是一个对象,用于实现具名插槽 35 | for (const key in children) { 36 | const slot = children[key]; 37 | // 将插件转换为函数实现作用于插槽 38 | slots[key] = (props) => normalizeSlotValue(slot(props)); 39 | } 40 | } 41 | 42 | /** 43 | * @author: Zhouqi 44 | * @description: 对插槽值对处理,转换成数组类型的子节点 45 | * @param slot 插槽数据 46 | */ 47 | function normalizeSlotValue(slot) { 48 | return isArray(slot) ? slot : [slot]; 49 | } 50 | 51 | /** 52 | * @author: Zhouqi 53 | * @description: 更新插槽节点 54 | * @param instance 组件实例 55 | * @param children 子节点 56 | */ 57 | export function updateSlots(instance, children) { 58 | const { slots } = instance; 59 | let needDeletionCheck = true; 60 | // 判断是不是插槽节点 61 | if (ShapeFlags.SLOTS_CHILDREN & instance.vnode.shapeFlag) { 62 | normalizeObjectSlots(children, slots); 63 | } 64 | // 删除不存在slot key 65 | if (needDeletionCheck) { 66 | for (const key in slots) { 67 | if (!(key in children)) { 68 | delete slots[key]; 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/runtime-core/src/directives.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-24 21:35:42 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-10 21:24:58 6 | */ 7 | import { currentRenderInstance } from "./componentRenderContext"; 8 | 9 | export function withDirectives(vnode, directives) { 10 | const internalInstance: any = currentRenderInstance!; 11 | const instance = internalInstance.proxy; 12 | const bindings = vnode.dirs || (vnode.dirs = []); 13 | for (let i = 0; i < directives.length; i++) { 14 | let [dir, value] = directives[i]; 15 | bindings.push({ 16 | dir, 17 | instance, 18 | value, 19 | oldValue: void 0, 20 | }); 21 | } 22 | return vnode; 23 | } 24 | 25 | /** 26 | * @author: Zhouqi 27 | * @description: 触发指令钩子函数 28 | * @param vnode 29 | * @param oldVnode 30 | * @param name 钩子函数名 31 | */ 32 | export function invokeDirectiveHook(vnode, oldVnode, name) { 33 | const bindings = vnode.dirs; 34 | const oldBindings = oldVnode && oldVnode.dirs; 35 | for (let i = 0; i < bindings.length; i++) { 36 | const binding = bindings[i]; 37 | if (oldBindings) { 38 | binding.oldValue = oldBindings[i].value; 39 | } 40 | const hook = binding.dir[name]; 41 | hook && hook(vnode.el, binding, vnode); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/runtime-core/src/h.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-27 14:37:57 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-03-27 14:38:59 6 | */ 7 | import { createVNode } from "./vnode"; 8 | export function h(type, props?, children?) { 9 | return createVNode(type, props, children); 10 | } 11 | -------------------------------------------------------------------------------- /packages/runtime-core/src/helpers/renderList.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-05-02 19:35:47 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-03 14:34:43 6 | */ 7 | import { isArray } from "@simplify-vue/shared"; 8 | 9 | export function renderList(source, renderItem) { 10 | let result; 11 | if (isArray(source)) { 12 | result = []; 13 | for (let i = 0, l = source.length; i < l; i++) { 14 | result[i] = renderItem(source[i], i); 15 | } 16 | } 17 | return result; 18 | } 19 | -------------------------------------------------------------------------------- /packages/runtime-core/src/helpers/renderSlot.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-30 20:59:56 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 09:41:45 6 | */ 7 | import { isFunction } from "@simplify-vue/shared"; 8 | import { createVNode, Fragment } from "../vnode"; 9 | 10 | /** 11 | * @author: Zhouqi 12 | * @description: 将slot children转化为虚拟节点 13 | * @param slots 插槽数据 14 | * @param name 具名插槽名称 15 | * @param props 作用域插槽要传入的props数据 16 | */ 17 | export function renderSlot(slots, name, props) { 18 | // 取对应名称的插槽————具名插槽 19 | const slot = slots[name]; 20 | if (slot) { 21 | if (isFunction(slot)) { 22 | return createVNode(Fragment, null, slot(props)); 23 | } 24 | } 25 | return createVNode(Fragment, null, []); 26 | } 27 | -------------------------------------------------------------------------------- /packages/runtime-core/src/helpers/resolveAssets.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-05-12 21:26:53 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-12 22:00:29 6 | */ 7 | import { currentRenderInstance } from "../componentRenderContext"; 8 | 9 | const COMPONENTS = "components"; 10 | 11 | export function resolveComponent(name: string) { 12 | return resolveAsset(COMPONENTS, name); 13 | } 14 | 15 | /** 16 | * @author: Zhouqi 17 | * @description: 解析资源,例如组件/指令/过滤器 18 | * @param type 19 | * @param name 20 | * @return 21 | */ 22 | function resolveAsset(type, name: string) { 23 | // 这里先处理组件 24 | const instance: any = currentRenderInstance; 25 | if (instance) { 26 | const Component = instance.type; 27 | const selfName = instance.name; 28 | if (selfName === name) return Component; 29 | const result = instance[type][name]; 30 | if (!result) { 31 | const extra = 32 | `\nIf this is a native custom element, make sure to exclude it from ` + 33 | `component resolution via compilerOptions.isCustomElement.`; 34 | console.warn(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`); 35 | } 36 | return result; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/runtime-core/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-26 21:20:31 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2023-12-22 14:43:48 6 | */ 7 | export { renderSlot } from "./helpers/renderSlot"; 8 | export { 9 | createTextVNode, 10 | createCommentVNode, 11 | createElementBlock, 12 | createElementVNode, 13 | openBlock, 14 | Fragment, 15 | createBlock, 16 | createVNode 17 | } from "./vnode"; 18 | export { getCurrentInstance, registerRuntimeCompiler } from "./component"; 19 | export { provide, inject } from "./apiInject"; 20 | export { createRenderer } from "./renderer"; 21 | export { toDisplayString, normalizeClass } from "@simplify-vue/shared"; 22 | export { h } from "./h"; 23 | export { watch, watchEffect, watchPostEffect } from "./apiWatch"; 24 | export { 25 | onBeforeMount, 26 | onMounted, 27 | onBeforeUpdate, 28 | onUpdated, 29 | onBeforeUnmount, 30 | onUnmounted, 31 | } from "./apiLifecycle"; 32 | export { defineComponent } from "./apiDefineComponent"; 33 | export { defineAsyncComponent } from "./apiAsyncComponent"; 34 | export { KeepAlive } from "./component/KeepAlive"; 35 | export { Teleport } from "./component/Teleport"; 36 | export { BaseTransition } from "./component/BaseTransition"; 37 | export { withDirectives } from "./directives"; 38 | export { renderList } from "./helpers/renderList"; 39 | export { resolveComponent } from "./helpers/resolveAssets"; 40 | export { nextTick } from "./scheduler"; 41 | export * from "@simplify-vue/reactivity"; 42 | -------------------------------------------------------------------------------- /packages/runtime-core/src/scheduler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-05 21:16:28 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2023-03-11 16:24:07 6 | */ 7 | 8 | import { isArray } from "@simplify-vue/shared"; 9 | 10 | // 微任务队列 11 | const queue: Function[] = []; 12 | // 创建微任务 13 | const resolvedPromise = Promise.resolve(); 14 | // 当前正在执行的微任务 15 | let currentFlushPromise; 16 | // 是否正在调度任务 17 | let isFlushing = false; 18 | let isFlushPending = false; 19 | 20 | // 正在等待的PostFlush队列 21 | const pendingPostFlushCbs: Function[] = []; 22 | // 正在执行的PostFlush队列 23 | let activePostFlushCbs: Function[] | null = null; 24 | 25 | let flushIndex = 0 26 | 27 | /** 28 | * @author: Zhouqi 29 | * @description: 将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它 30 | * @param fn 回调任务 31 | */ 32 | export function nextTick(fn) { 33 | const p = currentFlushPromise || resolvedPromise; 34 | return fn ? p.then(fn) : p; 35 | } 36 | 37 | /** 38 | * @author: Zhouqi 39 | * @description: 调度任务队列 40 | * @param job 任务 41 | */ 42 | export function queueJob(job) { 43 | if (!queue.includes(job)) { 44 | queue.push(job); 45 | } 46 | queueFlush(); 47 | } 48 | 49 | /** 50 | * @author: Zhouqi 51 | * @description: 52 | * @param cb postFlush类型的回调任务 53 | * @param activeQueue 正在执行的postFlush队列 54 | * @param pendingQueue 等待执行的postFlush队列 55 | */ 56 | function queueCb(cb, activeQueue: Function[] | null, pendingQueue: Function[]) { 57 | // cb是array说明是组件的生命周期回调函数 58 | if (isArray(cb)) { 59 | pendingQueue.push(...cb); 60 | } else { 61 | // 单独的任务 比如watch的scheduler调度器 62 | pendingQueue.push(cb); 63 | } 64 | queueFlush(); 65 | } 66 | 67 | /** 68 | * @author: Zhouqi 69 | * @description: 往pendingPostFlushCbs中添加postFlush类型的任务 70 | * @param cb 回调任务 71 | */ 72 | export function queuePostFlushCb(cb) { 73 | queueCb(cb, activePostFlushCbs, pendingPostFlushCbs); 74 | } 75 | 76 | /** 77 | * @author: Zhouqi 78 | * @description: 执行postFlush任务 79 | */ 80 | export function flushPostFlushCbs() { 81 | if (!pendingPostFlushCbs.length) return; 82 | const deduped = [...new Set(pendingPostFlushCbs)]; 83 | // 清空队列,避免flushPostFlushCbs多次调用执行多次相同任务 84 | pendingPostFlushCbs.length = 0; 85 | 86 | if (activePostFlushCbs) { 87 | activePostFlushCbs.push(...deduped); 88 | return; 89 | } 90 | activePostFlushCbs = deduped; 91 | for (let i = 0; i < activePostFlushCbs.length; i++) { 92 | const postFlushJob = activePostFlushCbs[i]; 93 | postFlushJob(); 94 | } 95 | activePostFlushCbs = null; 96 | } 97 | 98 | /** 99 | * @author: Zhouqi 100 | * @description: 执行微任务 101 | */ 102 | function queueFlush() { 103 | // 避免多次调用 104 | if (!isFlushing && !isFlushPending) { 105 | isFlushPending = true; 106 | currentFlushPromise = resolvedPromise.then(flushJobs); 107 | } 108 | } 109 | 110 | /** 111 | * @author: Zhouqi 112 | * @description: 执行微任务队列中的任务 113 | */ 114 | function flushJobs() { 115 | isFlushing = true; 116 | isFlushPending = false; 117 | try { 118 | for (flushIndex; flushIndex < queue.length; flushIndex++) { 119 | const job = queue[flushIndex]; 120 | job(); 121 | } 122 | } catch (error) { 123 | console.log(error); 124 | } finally { 125 | isFlushing = false; 126 | flushIndex = 0 127 | // 任务执行完成,重置微任务队列 128 | queue.length = 0; 129 | // 执行需要在更新之后触发的任务 130 | flushPostFlushCbs(); 131 | } 132 | } 133 | 134 | /** 135 | * @author: Zhouqi 136 | * @description: 删除无效的任务 137 | * @param {any} job 任务 138 | */ 139 | export function invalidateJob(job) { 140 | const i = queue.indexOf(job) 141 | if (i > flushIndex) { 142 | queue.splice(i, 1) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /packages/runtime-dom/__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-07 14:13:10 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-07 21:40:29 6 | */ 7 | describe("first", () => { 8 | test("should first", () => {}); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/runtime-dom/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-26 10:56:06 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 11:43:20 6 | */ 7 | export * from './dist/runtime-dom.esm.js'; -------------------------------------------------------------------------------- /packages/runtime-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simplify-vue/runtime-dom", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@simplify-vue/runtime-core": "workspace:^1.0.0", 15 | "@simplify-vue/shared": "workspace:^1.0.0" 16 | } 17 | } -------------------------------------------------------------------------------- /packages/runtime-dom/src/components/Transition.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-17 15:01:49 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2023-03-11 09:13:07 6 | */ 7 | import { BaseTransition, h } from "@simplify-vue/runtime-core"; 8 | export const Transition = (props, { slots }) => 9 | h(BaseTransition, resolveTransitionProps(props), slots); 10 | 11 | Transition.props = { 12 | enterFromClass: String, 13 | enterActiveClass: String, 14 | enterToClass: String, 15 | leaveFromClass: String, 16 | leaveActiveClass: String, 17 | leaveToClass: String, 18 | }; 19 | 20 | /** 21 | * @author: Zhouqi 22 | * @description: 解析transition上的属性 23 | * @param rawProps 24 | * @return 处理后的props 25 | */ 26 | function resolveTransitionProps(rawProps) { 27 | const { 28 | // type, 29 | enterFromClass, 30 | enterToClass, 31 | enterActiveClass, 32 | leaveFromClass, 33 | leaveActiveClass, 34 | leaveToClass, 35 | } = rawProps; 36 | 37 | // 进入动画结束后移除相关类 38 | const finishEnter = (el) => { 39 | removeTransitionClass(el, enterToClass); 40 | removeTransitionClass(el, enterActiveClass); 41 | }; 42 | 43 | // 离开动画结束后移除相关类 44 | const finishLeave = (el, done) => { 45 | removeTransitionClass(el, leaveToClass); 46 | removeTransitionClass(el, leaveActiveClass); 47 | done && done(el); 48 | }; 49 | 50 | function makeEnterHook() { 51 | return (el) => { 52 | const resolve = () => finishEnter(el); 53 | nextFrame(() => { 54 | removeTransitionClass(el, enterFromClass); 55 | addTransitionClass(el, enterToClass); 56 | whenTransitionEnds(el, resolve); 57 | }); 58 | }; 59 | } 60 | 61 | return { 62 | // dom创建到挂载dom阶段触发的函数 63 | onBeforeEnter(el) { 64 | addTransitionClass(el, enterFromClass); 65 | addTransitionClass(el, enterActiveClass); 66 | }, 67 | // dom挂载后触发的函数 68 | onEnter: makeEnterHook(), 69 | onLeave(el, done) { 70 | const resolve = () => finishLeave(el, done); 71 | addTransitionClass(el, leaveFromClass); 72 | addTransitionClass(el, leaveActiveClass); 73 | nextFrame(() => { 74 | removeTransitionClass(el, leaveFromClass); 75 | addTransitionClass(el, leaveToClass); 76 | whenTransitionEnds(el, resolve); 77 | }); 78 | }, 79 | }; 80 | } 81 | 82 | /** 83 | * @author: Zhouqi 84 | * @description: 添加动画类 85 | * @param el 添加类的元素 86 | * @param cls 类名 87 | */ 88 | function addTransitionClass(el, cls) { 89 | const regExp = /\s+/; 90 | // 通过空白符切分多个类名,循环添加到dom上 91 | cls.split(regExp).forEach((c) => c && el.classList.add(c)); 92 | } 93 | 94 | /** 95 | * @author: Zhouqi 96 | * @description: 移除动画类 97 | * @param el 移除类的元素 98 | * @param cls 类名 99 | */ 100 | function removeTransitionClass(el, cls) { 101 | const regExp = /\s+/; 102 | // 通过空白符切分多个类名,循环移除dom上类 103 | cls.split(regExp).forEach((c) => c && el.classList.remove(c)); 104 | } 105 | 106 | /** 107 | * @author: Zhouqi 108 | * @description: 在下一帧执行回调,因为浏览器只会在当前帧绘制DOM, 109 | * 结束状态的类名和起始状态的类名需要在两帧绘制,否则起始状态的类名不会被绘制出来 110 | * @param cb 下一帧触发的操作 111 | */ 112 | function nextFrame(cb) { 113 | requestAnimationFrame(() => { 114 | requestAnimationFrame(cb); 115 | }); 116 | } 117 | 118 | /** 119 | * @author: Zhouqi 120 | * @description: 动画加载完成后移除类 121 | * @param el 要移除动画的元素 122 | * @param resolve 动画结束时的回调 123 | */ 124 | function whenTransitionEnds(el, resolve) { 125 | const onEnd = () => { 126 | el.removeEventListener("transitionend", onEnd); 127 | resolve(); 128 | }; 129 | el.addEventListener("transitionend", onEnd); 130 | } 131 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/directives/vModel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-05-10 21:06:56 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-09-16 15:12:23 6 | */ 7 | import { isArray, invokeArrayFns } from "@simplify-vue/shared"; 8 | import { addEventListener } from "../modules/events"; 9 | 10 | export const vModelText = { 11 | created(el, binding, vnode) { 12 | el._assign = getModelAssigner(vnode); 13 | // 绑定input事件 14 | addEventListener(el, "input", (e) => { 15 | el._assign(el.value); 16 | }); 17 | }, 18 | mounted(el, { value }) { 19 | // 设置value 20 | el.value = value == null ? "" : value; 21 | }, 22 | beforeUpdate(el, { value }, vnode) { 23 | el._assign = getModelAssigner(vnode); 24 | const newValue = value == null ? "" : value; 25 | // 新旧value不同时更新value 26 | if (el.value !== newValue) { 27 | el.value = newValue; 28 | } 29 | }, 30 | }; 31 | 32 | const getModelAssigner = (vnode) => { 33 | const fn = vnode.props!["onUpdate:modelValue"]; 34 | // 执行v-model的默认绑定函数 35 | return isArray(fn) ? (value) => invokeArrayFns(fn, value) : fn; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/directives/vShow.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-24 22:11:19 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-25 09:54:47 6 | */ 7 | export const vShow = { 8 | beforeMount(el, binding) { 9 | const { value } = binding; 10 | setDisplay(el, value); 11 | }, 12 | updated(el, binding) { 13 | const { value, oldValue } = binding; 14 | if (!!value === !!oldValue) { 15 | return; 16 | } 17 | setDisplay(el, value); 18 | }, 19 | }; 20 | 21 | // 设置元素显示隐藏 22 | function setDisplay(el, value) { 23 | el.style.display = value ? "block" : "none"; 24 | } 25 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-26 21:20:44 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-10 21:19:08 6 | */ 7 | import { createRenderer } from "@simplify-vue/runtime-core"; 8 | import { extend } from "@simplify-vue/shared"; 9 | import { nodeOps } from "./nodeOps"; 10 | import { patchProp } from "./patchProp"; 11 | 12 | const rendererOptions = extend({ patchProp }, nodeOps); 13 | 14 | function ensureRenderer() { 15 | return createRenderer(rendererOptions); 16 | } 17 | 18 | export const createApp = (...args: [any]) => { 19 | const app = ensureRenderer().createApp(...args); 20 | // 劫持app实例上原有的mount函数 21 | const { mount } = app; 22 | app.mount = (containerOrSelector) => { 23 | const container = normalizeContainer(containerOrSelector); 24 | if (!container) return; 25 | mount(container); 26 | }; 27 | 28 | return app; 29 | }; 30 | 31 | /** 32 | * @author: Zhouqi 33 | * @description: 识别容器,如果是dom则直接返回;如果是字符串,则通过字符串获取dom 34 | * @param container 挂载元素 35 | */ 36 | function normalizeContainer(container) { 37 | if (typeof container === "string") { 38 | return document.querySelector(container); 39 | } 40 | return container; 41 | } 42 | 43 | export * from "@simplify-vue/runtime-core"; 44 | export { Transition } from "./components/Transition"; 45 | export { vShow } from "./directives/vShow"; 46 | export { vModelText } from "./directives/vModel"; 47 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/modules/attrs.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-04 13:53:46 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-09-14 17:43:17 6 | */ 7 | export function patchAttr(el, key, value) { 8 | // 新的值不存在,则表示删除属性 9 | if (value == null) { 10 | el.removeAttribute(key); 11 | } else { 12 | el.setAttribute(key, value); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/modules/events.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-28 20:16:47 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-10 21:12:05 6 | */ 7 | 8 | import { isArray } from "@simplify-vue/shared"; 9 | 10 | // 事件绑定 11 | export function addEventListener(el, event, handler) { 12 | el.addEventListener(event, handler); 13 | } 14 | 15 | // 事件销毁 16 | export function removeEventListener(el, event, handler) { 17 | el.removeEventListener(event, handler); 18 | } 19 | 20 | // 获取当前时间,默认使用低精度时间 21 | let _getNow = Date.now; 22 | 23 | /** 24 | * 由于不同浏览器事件的timeStamp使用的epoch time(新纪元时间)不同,因此这里需要兼容当前时间的获取 25 | * 当游览器事件使用的timeStamp是低精度时间(新纪元时间为 0:0:0 UTC 1st January 1970.)时,getNow函数需要使用Date.now(低精度) 26 | * 当浏览器事件使用的timeStamp是高精度时间时(新纪元时间为系统启动的时间),getNow函数需要使用performace.now(高精度) 27 | * 28 | * https://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-Event-timeStamp 29 | * https://www.w3.org/TR/hr-time/#sec-domhighrestimestamp 30 | * http://jimliu.net/2014/03/16/hrt-in-js/ 31 | */ 32 | if ( 33 | typeof window !== "undefined" && 34 | window.performance && 35 | window.performance.now 36 | ) { 37 | const eventTimeStamp = document.createEvent("event").timeStamp; 38 | // 假如当前时间大于事件的timeStamp,则认为事件使用的是高精度时间,此时getNow函数也应该返回高精度时间 39 | if (_getNow() > eventTimeStamp) { 40 | _getNow = () => window.performance.now(); 41 | } 42 | } 43 | 44 | // 为了优化频繁调用performance.now的性能,我们在一个事件循环内注册的所有事件统一使用一个timeStamp 45 | let cachedNow = 0; 46 | // 创建微任务,当一个tick执行完时重置cachedNow 47 | const p = Promise.resolve(); 48 | const rest = () => { 49 | cachedNow = 0; 50 | }; 51 | const getNow = () => cachedNow || (p.then(rest), (cachedNow = _getNow())); 52 | 53 | // props上的事件注册函数 54 | export function patchEvent(el, key, preValue, nextValue) { 55 | /** 56 | * 这里创建一个伪造的事件代理函数invoker,将原始事件赋值到invoker的value属性上 57 | * 将invoker作为最终绑定的事件,在执行invoker函数时内部会执行原始的绑定事件,即执行invoker.value() 58 | * 59 | * 新建伪造的事件代理函数有几个作用: 60 | * 1、方便事件更新 61 | * 2、控制原始事件的执行(涉及到事件冒泡机制) 62 | * 3、…………? 63 | * 64 | * 由于原生事件类型有很多,为了不互相覆盖,这里需要建立一个map对象invokers,key指代事件类型,值是伪造的事件代理函数 65 | */ 66 | const invokers = el._vei || (el._vei = {}); 67 | const eventName = key.slice(2).toLowerCase(); 68 | const hasInvoker = invokers[eventName]; 69 | 70 | if (nextValue) { 71 | // 如果存在新的值且旧的事件代理函数存在,则表示更新事件,否则表示添加新的事件绑定 72 | if (hasInvoker) { 73 | /** 74 | * 1、方便事件更新 75 | * 在更新事件时,不需要销毁原来的事件,再绑定新的事件,而只要更新invoker.value属性即可 76 | */ 77 | hasInvoker.value = nextValue; 78 | } else { 79 | const invoker = (invokers[eventName] = createInvoker(nextValue)); 80 | addEventListener(el, eventName, invoker); 81 | invoker.attached = getNow(); 82 | } 83 | } else if (hasInvoker) { 84 | // 新的值不存在且事件代理函数存在,则表示销毁事件绑定 85 | removeEventListener(el, eventName, hasInvoker); 86 | invokers[eventName] = undefined; 87 | } 88 | } 89 | 90 | // 创建事件代理函数 91 | function createInvoker(events) { 92 | const invoker: any = (e) => { 93 | const timestamp = e.timeStamp; 94 | /** 95 | * 2、控制原始事件的执行(涉及到事件冒泡机制) 96 | * 假设父vnode上有onClick事件,事件值取决于一个响应式数据的值,比如:onClick: isTrue ? () => console.log(1) : null, 97 | * 子vnode上有一个绑定事件onClick: () => { isTrue = true },当点击子vnode时会触发click事件,由于事件冒泡机制,click 98 | * 会向上冒泡到父节点,由于isTrue初始为false,因此父节点上不应该有绑定的click事件,但是却打印了1。 99 | * 这是由于vue的更新机制和事件冒泡时机导致的,实际上当isTrue被修改为true时触发了事件更新,更新后父节点上绑定了事件,之后事件才 100 | * 冒泡到父节点上,执行了父节点绑定的click事件。而解决方式就是在执行子元素事件的时候记录事件执行的时间,在这个时间点之后绑定的事件都 101 | * 不要去执行,这时候就需要有控制原始事件执行的功能。 102 | */ 103 | // 事件冒泡时,e会往上传递其中s.timestamp就是事件最开始执行的事件 104 | if (timestamp < invoker.attached) return; 105 | // 如果events是一个数组,则循环执行 106 | isArray(invoker.value) 107 | ? invoker.value.forEach((fn) => fn(e)) 108 | : invoker.value(e); 109 | }; 110 | invoker.value = events; 111 | return invoker; 112 | } 113 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/modules/props.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-04 14:03:59 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-08 19:44:56 6 | */ 7 | /** 8 | * @author: Zhouqi 9 | * @description: 更新dom属性 10 | * @param el dom元素 11 | * @param key 属性 12 | * @param value 值 13 | */ 14 | export function patchDOMProp(el, key, value) { 15 | if (value === "" || value === null) { 16 | const type = typeof el[key]; 17 | // 对于dom属性上的值为boolean的情况下,如果设置的值是空的则需要转化为true 18 | if (type === "boolean") { 19 | el[key] = true; 20 | return; 21 | } 22 | el.removeAttribute(key); 23 | return; 24 | } 25 | el[key] = value; 26 | } 27 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/nodeOps.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-03 15:36:54 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-05 17:15:06 6 | */ 7 | // 平台渲染操作 8 | export const nodeOps = { 9 | parentNode(node) { 10 | return node.parentNode || null; 11 | }, 12 | /** 13 | * @author: Zhouqi 14 | * @description: 创建节点 15 | * @param type 节点类型 16 | */ 17 | createElement(type) { 18 | return document.createElement(type); 19 | }, 20 | /** 21 | * @author: Zhouqi 22 | * @description: 添加节点 23 | * @param child 子节点 24 | * @param parent 父节点 25 | * @param anchor 锚点节点 26 | */ 27 | insert: (child, parent, anchor) => { 28 | parent.insertBefore(child, anchor || null); 29 | }, 30 | /** 31 | * @author: Zhouqi 32 | * @description: 删除节点 33 | * @param child 子节点 34 | */ 35 | remove(child) { 36 | const parent = child.parentNode; 37 | parent && parent.removeChild(child); 38 | }, 39 | /** 40 | * @author: Zhouqi 41 | * @description: 设置元素的文本内容 42 | * @param el 元素 43 | * @param text 文本内容 44 | */ 45 | setElementText(el, text) { 46 | el.textContent = text; 47 | }, 48 | /** 49 | * 50 | * @author: Zhouqi 51 | * @description: 创建文本节点 52 | * @param text 文本内容 53 | * @return 文本节点 54 | */ 55 | createText(text) { 56 | return document.createTextNode(text); 57 | }, 58 | /** 59 | * @author: Zhouqi 60 | * @description: 设置文本节点的文本内容 61 | * @param node 文本节点 62 | * @param text 文本内容 63 | */ 64 | setText(node, text) { 65 | node.nodeValue = text; 66 | }, 67 | /** 68 | * @author: Zhouqi 69 | * @description: 创建注释节点 70 | * @param text 注释内容 71 | * @return 注释节点 72 | */ 73 | createComment: (text) => document.createComment(text), 74 | /** 75 | * @author: Zhouqi 76 | * @description: 获取当前节点的下一个节点 77 | * @param node 当前节点 78 | * @return 当前节点的下一个节点 79 | */ 80 | nextSibling: (node) => node.nextSibling, 81 | /** 82 | * @author: Zhouqi 83 | * @description: 查找节点 84 | * @param selector 选择器 85 | * @return 节点 86 | */ 87 | querySelector: (selector) => document.querySelector(selector), 88 | }; 89 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/patchProp.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-27 15:44:22 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-10 20:29:37 6 | */ 7 | import { isModelListener, isOn } from "@simplify-vue/shared"; 8 | import { patchAttr } from "./modules/attrs"; 9 | import { patchEvent } from "./modules/events"; 10 | import { patchDOMProp } from "./modules/props"; 11 | 12 | /** 13 | * @author: Zhouqi 14 | * @description: 处理props属性 15 | * 处理props时需要理解HTML Attribute 和 DOM property的关系 16 | * 17 | * 例如: 18 | * 我们可以通过input.value属性去获取input的文本值,这个value就是input元素上的属性,也就是DOM property 19 | * 这个value属性可以设置在input标签上,值可以通过getAttribute去获取,这个value就是HTML Attribute 20 | * HTML Attribute可能跟DOM property有对应的映射关系(id->id),也可能没有(aria-)或者有多个映射关系(value->value/defaultValue),同样名称也不一定对应(class->className) 21 | * 22 | * 上的value属性对应着元素input上的value属性,但是input标签上的value值并不总是和input元素上的值相等 23 | * 当我们初始化时,通过input.value获取到的和input标签上的value值是一样的 24 | * 但是当我们修改input里面的内容时,通过input.value去访问value值和通过getAttribute方法获取inpu标签上的value值是不一样的 25 | * input标签上的value值依然还是初始时设置的值,因为我们可以认为HTML Attribute的值是DOM property上的初始值。 26 | * 27 | * 在处理元素属性上需要遵循一个结论:如果对应的属性可以在DOM property上找到,就去设置对应的DOM property,如果没找到就通过setAttribute去设置,当然还有其它特殊情况会慢慢补充 28 | * @param el 元素 29 | * @param key 属性名 30 | */ 31 | export function patchProp(el, key, preValue, nextValue) { 32 | if (shouldSetAsProp(el, key, nextValue)) { 33 | patchDOMProp(el, key, nextValue); 34 | } else if (key === "class") { 35 | // class通过className去设置性能最好 36 | el.className = nextValue; 37 | } else if (isOn(key)) { 38 | if (!isModelListener(key)) { 39 | // 注册事件 40 | patchEvent(el, key, preValue, nextValue); 41 | } 42 | } else { 43 | // 更新html属性 44 | patchAttr(el, key, nextValue); 45 | } 46 | } 47 | 48 | // 是否可以直接设置DOM property 49 | function shouldSetAsProp(el, key, value) { 50 | // form属性是只读的,只能通过setAttribute设置属性 51 | if (key === "form") { 52 | return false; 53 | } 54 | return key in el; 55 | } 56 | -------------------------------------------------------------------------------- /packages/shared/__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-07 14:13:10 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-30 21:24:59 6 | */ 7 | describe("first", () => { 8 | test("should first", () => {}); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/shared/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-26 10:56:22 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 11:44:07 6 | */ 7 | export * from './dist/shared.esm.js'; -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simplify-vue/shared", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC" 13 | } -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-21 20:00:07 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-10 21:10:24 6 | */ 7 | export * from "./shapeFlags"; 8 | export * from "./normalizeProp"; 9 | export * from "./toDisplayString"; 10 | export * from "./patchFlags"; 11 | 12 | // 重新定义方法名,使其更具语义化命名 13 | export const extend = Object.assign; 14 | export const isArray = Array.isArray; 15 | 16 | // 判断值是不是对象 17 | export const isObject = (val: unknown) => 18 | val !== null && typeof val === "object"; 19 | 20 | // 判断值是不是字符串 21 | export const isString = (val: unknown) => typeof val === "string"; 22 | 23 | // 判断是不是Symbol 24 | export const isSymbol = (val: unknown): val is symbol => 25 | typeof val === "symbol"; 26 | 27 | // 判断是不是Map 28 | export const isMap = (val: unknown): val is Map => 29 | toTypeString(val) === "[object Map]"; 30 | 31 | // 判断值是不是函数 32 | export const isFunction = (val: unknown) => typeof val === "function"; 33 | 34 | export const toTypeString = (value: unknown) => 35 | Object.prototype.toString.call(value); 36 | 37 | // 判断是不是一个纯对象 38 | export const isPlainObject = (val: unknown) => 39 | toTypeString(val) === "[object Object]"; 40 | 41 | // 新旧值是否有变化,以及对NaN的判断处理 NaN === NaN为false 42 | export const hasChanged = (value: any, oldValue: any): boolean => 43 | !Object.is(value, oldValue); 44 | 45 | // 判断是否是事件属性:onClick ………… 46 | export const isOn = (key: string) => /^on[^a-z]/.test(key); 47 | 48 | // 烤肉串命名转驼峰 add-name ===> addName 49 | export const camelize = (str: string): string => 50 | str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : "")); 51 | 52 | // 将addName这种转化为AddName 53 | export const capitalize = (str: string) => 54 | str.charAt(0).toUpperCase() + str.slice(1); 55 | 56 | // 处理事件触发的事件名 57 | export const toHandlerKey = (str: string) => 58 | str ? `on${capitalize(str)}` : ""; 59 | 60 | export const EMPTY_OBJ = {}; 61 | export const EMPTY_ARR = []; 62 | 63 | // 循环数组中的所有方法 64 | export const invokeArrayFns = (fns: Function[], args?: any) => { 65 | const length = fns.length; 66 | for (let i = 0; i < length; i++) { 67 | fns[i](args); 68 | } 69 | }; 70 | 71 | // 空函数 72 | export const NOOP = () => {}; 73 | 74 | // 判断某个对象中是否有指定属性 75 | export const hasOwn = (target: object, key: string | symbol) => { 76 | return Object.prototype.hasOwnProperty.call(target, key); 77 | }; 78 | 79 | // 获取数据的原始类型字符串 80 | export const toRawType = (value: unknown): string => { 81 | // {} ===> [object Object] ==> Object 82 | return toTypeString(value).slice(8, -1); 83 | }; 84 | 85 | export const def = (obj: object, key: string | symbol, value: any) => { 86 | Object.defineProperty(obj, key, { 87 | configurable: true, 88 | enumerable: false, 89 | value, 90 | }); 91 | }; 92 | 93 | // 是否是v-model 94 | export const isModelListener = (key: string) => key.startsWith("onUpdate:"); 95 | -------------------------------------------------------------------------------- /packages/shared/src/normalizeProp.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-03 16:57:13 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 13:32:07 6 | */ 7 | import { isArray, isObject, isString } from "./"; 8 | 9 | /** 10 | * @author: Zhouqi 11 | * @description: 规范化class的值 12 | * @param value class的值 13 | */ 14 | export function normalizeClass(value) { 15 | // 处理的情况无非就是三种:字符串,数组,对象 16 | let result = ""; 17 | if (isString(value)) { 18 | // 是字符串则直接拼接 19 | result += value; 20 | } else if (isArray(value)) { 21 | // 是数组情况就递归调用normalizeClass 22 | for (let i = 0; i < value.length; i++) { 23 | result += `${normalizeClass(value[i])} `; 24 | } 25 | } else if (isObject(value)) { 26 | for (const key in value) { 27 | // 值为true的class才需要拼接 28 | if (value[key]) { 29 | result += `${key} `; 30 | } 31 | } 32 | } 33 | return result.trim(); 34 | } 35 | -------------------------------------------------------------------------------- /packages/shared/src/patchFlags.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-05-03 19:40:14 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-06 13:51:28 6 | */ 7 | export const enum PatchFlags { 8 | // 动态文本节点 9 | TEXT = 1, 10 | CLASS = 1 << 1, 11 | PROPS = 1 << 3, 12 | STABLE_FRAGMENT = 1 << 6, 13 | KEYED_FRAGMENT = 1 << 7, 14 | UNKEYED_FRAGMENT = 1 << 8, 15 | NEED_PATCH = 1 << 9, 16 | } 17 | -------------------------------------------------------------------------------- /packages/shared/src/shapeFlags.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-27 15:12:52 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-27 13:22:34 6 | */ 7 | /** 8 | * 用二进制表示组件类型,在判断组件类型和修改类型的时候通过位运算的方式去判断或者比较,性能更优,但是代码可读性相对更低 9 | * 10 | * | 运算 同位置上都为0才为0,否则为1 11 | * & 运算 同位置上都为1才为1,否则为0 12 | * 13 | * 0001 | 0000 = 0001; 0000 | 0000 = 0000; 0100 | 0001 = 0101 14 | * 0001 & 0001 = 0001; 0000 | 0001 = 0000; 0100 & 0101 = 0100 15 | * 16 | * 在修改vnode类型的时候可以用 | 运算,在查找vnode类型的时候可以用 & 运算 17 | */ 18 | export const enum ShapeFlags { 19 | ELEMENT = 1, // 0000000001 20 | FUNCTIONAL_COMPONENT = 1 << 1, // 0000000010 21 | STATEFUL_COMPONENT = 1 << 2, // 0000000100 22 | TEXT_CHILDREN = 1 << 3, // 0000001000 23 | ARRAY_CHILDREN = 1 << 4, // 0000010000 24 | SLOTS_CHILDREN = 1 << 5, // 0000100000 25 | TELEPORT = 1 << 6, // 0001000000 26 | COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 0100000000 27 | COMPONENT_KEPT_ALIVE = 1 << 9, // 1000000000 28 | COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT, // 0000000110 29 | } 30 | -------------------------------------------------------------------------------- /packages/shared/src/toDisplayString.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-26 13:40:50 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-29 16:49:54 6 | */ 7 | import { isString } from "."; 8 | export function toDisplayString(text) { 9 | // null 和 undefined处理为'' 10 | return isString(text) ? text : text == null ? "" : String(text); 11 | } 12 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/asyncRender/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/class/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 24 | 25 | 26 | 27 |
28 | 29 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/commentVnode/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/compileTemplate/component.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/compileTemplate/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 24 | 25 | 26 | 27 |
28 | 29 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/compileTemplate/multipleRoot.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/compileTemplate/v-for.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 24 | 25 | 26 | 27 |
28 | 29 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/compileTemplate/v-if.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/compileTemplate/v-model.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/compileTemplate/v-show.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/componentProps/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 24 | 25 | 26 | 27 |
28 | 29 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/currentInstance/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/customRenderer/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 24 | 25 | 26 | 27 |
28 | 29 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/defineAsyncComponent/Child.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-14 22:37:53 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-04 13:02:22 6 | */ 7 | const { 8 | h, 9 | renderSlot 10 | } = Vue; 11 | 12 | export default { 13 | name: "Child", 14 | props: { 15 | text: [String, Number] 16 | }, 17 | setup(props, { 18 | slots 19 | }) { 20 | console.log(1); 21 | return () => h('div', null, [h('div', null, props.text), renderSlot(slots, 'body')]); 22 | }, 23 | 24 | } -------------------------------------------------------------------------------- /packages/simplify-vue/example/defineAsyncComponent/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/emit/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/functionalComponent/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Document 14 | 15 | 16 | 17 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/keep-alive/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/lifecycle/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/nextTick/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/optionsApi/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/patchFlags/patchFlagClass.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 24 | 25 | 26 | 27 |
28 | 29 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/patchFlags/patchFlagProps.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/patchFlags/patchFlagText.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/patchFlags/patchFlagVShow.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/patchFlags/patchMultipleRoot.html: -------------------------------------------------------------------------------- 1 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Document 21 | 22 | 23 | 24 |
25 | 26 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/patchFlags/patchVFor.html: -------------------------------------------------------------------------------- 1 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Document 21 | 30 | 31 | 32 | 33 |
34 | 35 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/patchFlags/patchVIf.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/patchProps/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/provideAndInject/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/setup/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/slots/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/teleport/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/textNode/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/transition/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 40 | 41 | 42 | 43 |
44 | 45 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/updateComponent/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /packages/simplify-vue/example/watch/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Document 15 | 16 | 17 | 18 |
19 | 20 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /packages/simplify-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simplify-vue", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@simplify-vue/compiler-dom": "workspace:^1.0.0", 15 | "@simplify-vue/runtime-dom": "workspace:^1.0.0" 16 | } 17 | } -------------------------------------------------------------------------------- /packages/simplify-vue/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-27 14:28:06 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-04-26 11:40:40 6 | */ 7 | import { compile } from "@simplify-vue/compiler-dom"; 8 | import { registerRuntimeCompiler } from "@simplify-vue/runtime-dom"; 9 | import * as runtimeDom from "@simplify-vue/runtime-dom"; 10 | 11 | export * from "@simplify-vue/runtime-dom"; 12 | 13 | function compileToFunction(template, options?) { 14 | const { code } = compile(template, options); 15 | const render = new Function("Vue", code)(runtimeDom); 16 | return render; 17 | } 18 | 19 | registerRuntimeCompiler(compileToFunction); 20 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/**" 3 | - "!**/test/**" 4 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-03-27 14:27:34 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-03 15:24:29 6 | */ 7 | const { 8 | terser 9 | } = require('rollup-plugin-terser'); 10 | const typescript = require('@rollup/plugin-typescript'); 11 | const path = require('path'); 12 | // const fs = require('fs'); 13 | // const targets = fs.readdirSync('packages'); 14 | const targets = ['shared', 'reactivity', 'compiler-core', 'compiler-dom', 'runtime-core', 'runtime-dom', 'simplify-vue']; 15 | const packagesDir = path.resolve(__dirname, 'packages'); 16 | const resolve = (dir, p) => path.resolve(dir, p); 17 | const entryFile = 'src/index.ts'; 18 | 19 | const createConfig = (target) => { 20 | const packageDir = resolve(packagesDir, target); 21 | const input = resolve(packageDir, entryFile); 22 | const ouputFile = resolve(packageDir, `dist/${target}.esm.js`); 23 | return { 24 | input, 25 | output: { 26 | format: "es", 27 | file: ouputFile 28 | }, 29 | external: ['@babel/parser', 'estree-walker'], 30 | plugins: [ 31 | typescript({ 32 | exclude: /__tests__/ 33 | }), 34 | terser() 35 | ], 36 | onwarn: (msg, warn) => { 37 | // 去除打包是循环依赖的warning提示 38 | if (!/Circular/.test(msg)) { 39 | warn(msg) 40 | } 41 | }, 42 | } 43 | }; 44 | 45 | const packageConfigs = targets.map(target => createConfig(target)); 46 | 47 | export default packageConfigs; -------------------------------------------------------------------------------- /scripts/dev.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Zhouqi 3 | * @Date: 2022-04-28 15:14:45 4 | * @LastEditors: Zhouqi 5 | * @LastEditTime: 2022-05-07 15:42:33 6 | */ 7 | const { 8 | build 9 | } = require('esbuild') 10 | 11 | build({ 12 | entryPoints: [`./packages/simplify-vue/src/index.ts`], 13 | outfile: './packages/simplify-vue/dist/simplify-vue.global.js', 14 | bundle: true, 15 | sourcemap: true, 16 | format: 'iife', 17 | globalName: 'Vue', 18 | watch: { 19 | onRebuild(error) { 20 | console.log('rebuild simplify-vue/dist/simplify-vue.global.js'); 21 | } 22 | } 23 | }).then(() => { 24 | console.log(`watching simplify-vue/dist/simplify-vue.global.js`) 25 | }) --------------------------------------------------------------------------------