├── .gitignore ├── pnpm-workspace.yaml ├── packages ├── compiler-core │ ├── src │ │ ├── index.ts │ │ ├── utils.ts │ │ ├── transforms │ │ │ ├── transformExpression.ts │ │ │ ├── transformElement.ts │ │ │ └── transformText.ts │ │ ├── runtimeHelpers.ts │ │ ├── ast.ts │ │ ├── compile.ts │ │ ├── transform.ts │ │ ├── codegen.ts │ │ └── parse.ts │ ├── package.json │ └── __tests__ │ │ ├── transform.spec.ts │ │ ├── codegen.spec.ts │ │ ├── __snapshots__ │ │ └── codegen.spec.ts.snap │ │ └── parse.spec.ts ├── shared │ ├── src │ │ ├── toDisplayString.ts │ │ ├── ShapeFlags.ts │ │ └── index.ts │ └── package.json ├── runtime-core │ ├── src │ │ ├── componentProps.ts │ │ ├── h.ts │ │ ├── componentUpdateUtils.ts │ │ ├── componentEmit.ts │ │ ├── index.ts │ │ ├── createApp.ts │ │ ├── componentPublicInstance.ts │ │ ├── apiWatch.ts │ │ ├── componentSlots.ts │ │ ├── scheduler.ts │ │ ├── apiInject.ts │ │ ├── vnode.ts │ │ ├── component.ts │ │ └── renderer.ts │ ├── helpers │ │ └── renderSlots.ts │ ├── package.json │ └── __tests__ │ │ └── apiWatch.spec.ts ├── vue │ ├── examples │ │ ├── update │ │ │ ├── main.js │ │ │ ├── index.html │ │ │ └── App.js │ │ ├── helloworld │ │ │ ├── main.js │ │ │ ├── Foo.js │ │ │ ├── index.html │ │ │ └── App.js │ │ ├── nextTicker │ │ │ ├── main.js │ │ │ ├── index.html │ │ │ └── App.js │ │ ├── pathChildren │ │ │ ├── main.js │ │ │ ├── index.html │ │ │ ├── TextToText.js │ │ │ ├── ArrayToText.js │ │ │ ├── TextToArray.js │ │ │ ├── App.js │ │ │ └── ArrayToArray.js │ │ ├── provideInject │ │ │ ├── main.js │ │ │ ├── index.html │ │ │ └── App.js │ │ ├── compiler-base │ │ │ ├── main.js │ │ │ ├── App.js │ │ │ └── index.html │ │ ├── componentEmit │ │ │ ├── main.js │ │ │ ├── index.html │ │ │ ├── Foo.js │ │ │ └── App.js │ │ ├── componentSlot │ │ │ ├── main.js │ │ │ ├── index.html │ │ │ ├── Foo.js │ │ │ └── App.js │ │ ├── componentUpdate │ │ │ ├── main.js │ │ │ ├── Child.js │ │ │ ├── index.html │ │ │ └── App.js │ │ ├── getCurrentInstance │ │ │ ├── main.js │ │ │ ├── Foo.js │ │ │ ├── index.html │ │ │ └── App.js │ │ └── customRenderer │ │ │ ├── App.js │ │ │ ├── index.html │ │ │ └── main.js │ ├── package.json │ ├── src │ │ └── index.ts │ └── dist │ │ ├── tiny-vue.esm.js │ │ └── tiny-vue.cjs.js ├── reactivity │ ├── src │ │ ├── index.ts │ │ ├── computed.ts │ │ ├── reactive.ts │ │ ├── baseHandlers.ts │ │ ├── ref.ts │ │ └── effect.ts │ ├── package.json │ └── __tests__ │ │ ├── shallowReadonly.spec.ts │ │ ├── readonly.spec.ts │ │ ├── reactive.spec.ts │ │ ├── computed.spec.ts │ │ ├── ref.spec.ts │ │ └── effect.spec.ts └── runtime-dom │ ├── package.json │ └── src │ └── index.ts ├── .vscode └── settings.json ├── babel.config.js ├── article ├── shapeFlag │ └── index.md └── reactive、ref、effect流程解析 │ ├── index.js │ └── index.md ├── vitest.config.ts ├── rollup.config.js ├── package.json ├── README.md └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | draft.js 3 | background 4 | lib -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /packages/compiler-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compile' 2 | -------------------------------------------------------------------------------- /packages/shared/src/toDisplayString.ts: -------------------------------------------------------------------------------- 1 | export function toDisplayString(value) { 2 | return String(value) 3 | } 4 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentProps.ts: -------------------------------------------------------------------------------- 1 | export function initProps(instance, rawProps) { 2 | instance.props = rawProps || {} 3 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "cSpell.words": ["vnode"], 4 | // 保存时格式化代码 5 | "editor.formatOnSave": true 6 | } 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript',], 3 | }; -------------------------------------------------------------------------------- /packages/vue/examples/update/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/tiny-vue.esm.js' 2 | import { App } from './App.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /packages/vue/examples/helloworld/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/tiny-vue.esm.js' 2 | import { App } from './App.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /packages/vue/examples/nextTicker/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/tiny-vue.esm.js' 2 | import { App } from './App.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /packages/vue/examples/pathChildren/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/tiny-vue.esm.js' 2 | import App from './App.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /packages/vue/examples/provideInject/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/tiny-vue.esm.js' 2 | import App from './App.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /packages/vue/examples/compiler-base/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/tiny-vue.esm.js' 2 | import { App } from './App.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /packages/vue/examples/componentEmit/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/tiny-vue.esm.js' 2 | import { App } from './App.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /packages/vue/examples/componentSlot/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/tiny-vue.esm.js' 2 | import { App } from './App.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /packages/vue/examples/componentUpdate/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/tiny-vue.esm.js' 2 | import { App } from './App.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /packages/runtime-core/src/h.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from "./vnode" 2 | 3 | export function h(type, props?, children?) { 4 | return createVNode(type, props, children) 5 | } -------------------------------------------------------------------------------- /packages/vue/examples/getCurrentInstance/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../dist/tiny-vue.esm.js' 2 | import { App } from './App.js' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /packages/compiler-core/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from './ast' 2 | 3 | export function isText(node) { 4 | return node.type === NodeTypes.TEXT || node.type === NodeTypes.INTERPOLATION 5 | } 6 | -------------------------------------------------------------------------------- /packages/reactivity/src/index.ts: -------------------------------------------------------------------------------- 1 | export { ref, isRef, unRef, proxyRefs } from './ref' 2 | export * from './reactive' 3 | export * from './computed' 4 | export { effect, ReactiveEffect } from './effect' 5 | -------------------------------------------------------------------------------- /article/shapeFlag/index.md: -------------------------------------------------------------------------------- 1 | 位运算符基础知识 2 | | 两位都为0,才为0 3 | & 两位都为1,才为1 4 | 5 | 修改 | 6 | 0000 7 | 0001 | 8 | ——————— 9 | 0001 10 | 11 | 查找 & 12 | 0001 13 | 0001 & 14 | ——————— 15 | 0001 16 | 17 | 0010 18 | 0001 & 19 | ______ 20 | 0000 -------------------------------------------------------------------------------- /packages/shared/src/ShapeFlags.ts: -------------------------------------------------------------------------------- 1 | export const enum ShapeFlags { 2 | ELEMENT = 1, // 0001 3 | STATEFUL_COMPONENT = 1 << 1, // 0010 4 | TEXT_CHILDREN = 1 << 2, // 0100 5 | ARRAY_CHILDREN = 1 << 3, // 10004 6 | SLOT_CHILDREN = 1 << 4 7 | } -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/transformExpression.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../ast' 2 | 3 | export function transformExpression(node) { 4 | if (node.type === NodeTypes.INTERPOLATION) { 5 | node.content.content = '_ctx.' + node.content.content 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/vue/examples/componentUpdate/Child.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/tiny-vue.esm.js' 2 | export default { 3 | name: 'Child', 4 | setup(props, { emit }) {}, 5 | render(proxy) { 6 | return h('div', {}, [h('div', {}, 'child-props-msg:' + this.$props.msg)]) 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /packages/vue/examples/helloworld/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/tiny-vue.esm.js' 2 | export const Foo = { 3 | setup(props) { 4 | console.log('Foo的props', props) 5 | props.count++ 6 | }, 7 | render() { 8 | return h('div', {}, 'Foo:' + this.count) 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /packages/compiler-core/src/runtimeHelpers.ts: -------------------------------------------------------------------------------- 1 | export const TO_DISPLAY_STRING = Symbol('toDisplayString') 2 | export const CREATE_ELEMENT_VNODE = Symbol('createElementVNode') 3 | 4 | export const helperMapName = { 5 | [TO_DISPLAY_STRING]: 'toDisplayString', 6 | [CREATE_ELEMENT_VNODE]: 'createElementVNode', 7 | } 8 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tiny-vue/shared", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /packages/vue/examples/compiler-base/App.js: -------------------------------------------------------------------------------- 1 | import { ref } from '../../dist/tiny-vue.esm.js' 2 | 3 | export const App = { 4 | name: 'App', 5 | template: `
hi,{{count}}
`, 6 | setup() { 7 | const count = (window.count = ref(1)) 8 | return { 9 | count, 10 | } 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /packages/vue/examples/customRenderer/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/tiny-vue.esm.js' 2 | export const App = { 3 | setup() { 4 | return { 5 | x: 100, 6 | y: 100, 7 | } 8 | }, 9 | render() { 10 | return h('rect', { 11 | x: this.x, 12 | y: this.y, 13 | }) 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /packages/runtime-core/helpers/renderSlots.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, Fragment } from '../src/vnode' 2 | 3 | export function renderSlots(slots, name, props) { 4 | const slot = slots[name] 5 | if (slot) { 6 | if (typeof slot === 'function') { 7 | return createVNode(Fragment, {}, slot(props)) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/vue/examples/getCurrentInstance/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from '../../dist/tiny-vue.esm.js' 2 | export const Foo = { 3 | name: 'Foo', 4 | setup() { 5 | const instance = getCurrentInstance() 6 | console.log(instance) 7 | return {} 8 | }, 9 | render() { 10 | return h('div', {}, 'foo') 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentUpdateUtils.ts: -------------------------------------------------------------------------------- 1 | export function shouldUpdateComponent(prevVNode, nextVNode) { 2 | const { props: prevProps } = prevVNode 3 | const { props: nextProps } = nextVNode 4 | for (const key in nextProps) { 5 | if (nextProps[key] !== prevProps[key]) { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | }, 8 | resolve: { 9 | alias: [ 10 | { 11 | find: /@tiny-vue\/(\w*)/, 12 | replacement: path.resolve(__dirname, 'packages') + '/$1/src', 13 | }, 14 | ], 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /packages/reactivity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tiny-vue/reactivity", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@tiny-vue/shared": "workspace:^1.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/compiler-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tiny-vue/compiler-core", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@tiny-vue/shared": "workspace:^1.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/vue/examples/update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/examples/componentSlot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/examples/nextTicker/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/examples/provideInject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/examples/getCurrentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-vue", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@tiny-vue/compiler-core": "workspace:^1.0.0", 14 | "@tiny-vue/runtime-dom": "workspace:^1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vue/examples/pathChildren/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/runtime-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tiny-vue/runtime-core", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@tiny-vue/reactivity": "workspace:^1.0.0", 14 | "@tiny-vue/shared": "workspace:^1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { camelize, toHandlerKey } from '@tiny-vue/shared' 2 | 3 | // 执行子组件的emit方法,触发父组件props中的注册事件 4 | export function emit(instance, event, ...args) { 5 | console.log('emit', event) 6 | 7 | const { props } = instance 8 | 9 | // 将传入的event名称转化为props中的注册事件名 10 | const handlerKeyName = toHandlerKey(camelize(event)) 11 | const handler = props[handlerKeyName] 12 | handler && handler(...args) 13 | } 14 | -------------------------------------------------------------------------------- /packages/runtime-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tiny-vue/runtime-dom", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@tiny-vue/runtime-core": "workspace:^1.0.0", 14 | "@tiny-vue/shared": "workspace:^1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vue/examples/componentSlot/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots } from '../../dist/tiny-vue.esm.js' 2 | export const Foo = { 3 | setup() { 4 | return {} 5 | }, 6 | render() { 7 | const foo = h('p', {}, 'foo') 8 | console.log(this.$slots) 9 | return h('div', {}, [ 10 | renderSlots(this.$slots, 'header', { 11 | type: 'good', 12 | }), 13 | foo, 14 | renderSlots(this.$slots, 'footer'), 15 | ]) 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /packages/compiler-core/src/ast.ts: -------------------------------------------------------------------------------- 1 | import { CREATE_ELEMENT_VNODE } from './runtimeHelpers' 2 | 3 | export enum NodeTypes { 4 | INTERPOLATION, 5 | SIMPLE_EXPRESSION, 6 | ELEMENT, 7 | TEXT, 8 | ROOT, 9 | COMPOUND_EXPRESSION, 10 | } 11 | 12 | export function createVNodeCall(context, tag, props, children) { 13 | context.helper(CREATE_ELEMENT_VNODE) 14 | return { 15 | type: NodeTypes.ELEMENT, 16 | tag, 17 | props, 18 | children, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/runtime-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { h } from './h' 2 | export { renderSlots } from '../helpers/renderSlots' 3 | export { createTextVNode, createElementVNode } from './vnode' 4 | export { getCurrentInstance, registerRuntimeCompiler } from './component' 5 | export { provide, inject } from './apiInject' 6 | export { createRenderer } from './renderer' 7 | export { nextTick } from './scheduler' 8 | export { toDisplayString } from '@tiny-vue/shared' 9 | 10 | export * from '@tiny-vue/reactivity' 11 | -------------------------------------------------------------------------------- /packages/vue/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@tiny-vue/runtime-dom' 2 | 3 | import { baseCompile } from '@tiny-vue/compiler-core' 4 | import * as runtimeDom from '@tiny-vue/runtime-dom' 5 | import { registerRuntimeCompiler } from '@tiny-vue/runtime-dom' 6 | 7 | function compileToFunction(template) { 8 | const { code } = baseCompile(template) 9 | const render = new Function('Vue', code)(runtimeDom) 10 | console.log(render) 11 | return render 12 | } 13 | 14 | registerRuntimeCompiler(compileToFunction) 15 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json' 2 | import typescript from '@rollup/plugin-typescript' 3 | export default { 4 | input: './packages/vue/src/index.ts', 5 | output: [ 6 | // cjs -> commonjs 7 | // 在项目中进行包查找时,cjs情况下默认查找main,esm默认查找module 8 | { 9 | format: 'cjs', 10 | file: './packages/vue/dist/tiny-vue.cjs.js', 11 | }, 12 | { 13 | format: 'es', 14 | file: './packages/vue/dist/tiny-vue.esm.js', 15 | }, 16 | ], 17 | plugins: [typescript()], 18 | } 19 | -------------------------------------------------------------------------------- /packages/vue/examples/componentUpdate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/vue/examples/customRenderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/vue/examples/pathChildren/TextToText.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../dist/tiny-vue.esm.js' 2 | const prevChildren = 'oldChild' 3 | const nextChildren = 'nexChild' 4 | 5 | export default { 6 | name: 'TextToText', 7 | setup() { 8 | const isChange = ref(false) 9 | window.isChange = isChange 10 | 11 | return { 12 | isChange, 13 | } 14 | }, 15 | render() { 16 | return this.isChange 17 | ? h('div', {}, nextChildren) 18 | : h('div', {}, prevChildren) 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /packages/vue/examples/getCurrentInstance/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | createTextVNode, 4 | getCurrentInstance, 5 | } from '../../dist/tiny-vue.esm.js' 6 | import { Foo } from './Foo.js' 7 | export const App = { 8 | name: 'App', 9 | render() { 10 | const app = h('div', {}, 'getCurrentInstance demo') 11 | const foo = h(Foo) 12 | 13 | return h('div', {}, [app, foo]) 14 | }, 15 | setup() { 16 | const instance = getCurrentInstance() 17 | console.log(instance) 18 | return {} 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /packages/runtime-core/src/createApp.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from './vnode' 2 | 3 | export function createAppAPI(render) { 4 | return function createApp(rootComponent) { 5 | return { 6 | mount(rootContainer) { 7 | const _rootContainer = 8 | typeof rootContainer === 'string' 9 | ? document.querySelector(rootContainer) 10 | : rootContainer 11 | const vnode = createVNode(rootComponent) 12 | render(vnode, _rootContainer) 13 | }, 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vue/examples/helloworld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/examples/compiler-base/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/examples/componentEmit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/vue/examples/pathChildren/ArrayToText.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../dist/tiny-vue.esm.js' 2 | 3 | const nextChildren = 'newChildren' 4 | const prevChildren = [h('div', {}, 'A'), h('div', {}, 'B')] 5 | export default { 6 | name: 'ArrayToText', 7 | setup() { 8 | const isChange = ref(false) 9 | window.isChange = isChange 10 | return { 11 | isChange, 12 | } 13 | }, 14 | render() { 15 | const vnode = this.isChange 16 | ? h('div', {}, nextChildren) 17 | : h('div', {}, prevChildren) 18 | return vnode 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /packages/vue/examples/pathChildren/TextToArray.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../dist/tiny-vue.esm.js' 2 | 3 | const nextChildren = [h('div', {}, 'A'), h('div', {}, 'B')] 4 | const prevChildren = 'oldChildren' 5 | export default { 6 | name: 'ArrayToText', 7 | setup() { 8 | const isChange = ref(false) 9 | window.isChange = isChange 10 | return { 11 | isChange, 12 | } 13 | }, 14 | render() { 15 | const vnode = this.isChange 16 | ? h('div', {}, nextChildren) 17 | : h('div', {}, prevChildren) 18 | return vnode 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /packages/vue/examples/componentEmit/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/tiny-vue.esm.js' 2 | export const Foo = { 3 | setup(props, { emit }) { 4 | const emitAdd = () => { 5 | console.log('btn has been click') 6 | emit('add', 1, 2) 7 | emit('add-foo', 3, 4) 8 | } 9 | return { 10 | emitAdd, 11 | } 12 | }, 13 | render() { 14 | const btn = h( 15 | 'button', 16 | { 17 | onClick: this.emitAdd, 18 | }, 19 | 'emitAdd' 20 | ) 21 | const foo = h('p', {}, 'foo') 22 | return h('div', {}, [foo, btn]) 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /packages/compiler-core/src/compile.ts: -------------------------------------------------------------------------------- 1 | import { generate } from './codegen' 2 | import { baseParse } from './parse' 3 | import { transform } from './transform' 4 | import { transformElement } from './transforms/transformElement' 5 | import { transformExpression } from './transforms/transformExpression' 6 | import { transformText } from './transforms/transformText' 7 | 8 | export function baseCompile(template) { 9 | const ast = baseParse(template) 10 | transform(ast, { 11 | nodeTransforms: [transformExpression, transformElement, transformText], 12 | }) 13 | 14 | return generate(ast) 15 | } 16 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReadonly, shallowReadonly } from '../src/reactive' 2 | 3 | describe('shallowReadonly', () => { 4 | test('should not mak non-reactive properties reactive', () => { 5 | const props = shallowReadonly({ n: { foo: 1 } }) 6 | expect(isReadonly(props)).toBe(true) 7 | expect(isReadonly(props.n)).toBe(false) 8 | }) 9 | 10 | it('wran when call set', () => { 11 | console.warn = vi.fn() 12 | const user = shallowReadonly({ 13 | age: 10, 14 | }) 15 | user.age = 11 16 | expect(console.warn).toBeCalled() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-vue", 3 | "version": "1.0.0", 4 | "main": "lib/tiny-vue.cjs.js", 5 | "module": "lib/tiny-vue.esm.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "vitest", 9 | "build": "rollup -c rollup.config.js --watch" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.16.0", 13 | "@babel/preset-env": "^7.16.0", 14 | "@babel/preset-typescript": "^7.16.0", 15 | "@rollup/plugin-typescript": "^8.3.0", 16 | "rollup": "^2.62.0", 17 | "tslib": "^2.3.1", 18 | "typescript": "^4.4.4", 19 | "vite": "^4.0.3", 20 | "vitest": "^0.26.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/transformElement.ts: -------------------------------------------------------------------------------- 1 | import { createVNodeCall, NodeTypes } from '../ast' 2 | 3 | export function transformElement(node, context) { 4 | if (node.type === NodeTypes.ELEMENT) { 5 | return () => { 6 | // 中间处理层 7 | 8 | // tag 9 | const vnodeTag = `'${node.tag}'` 10 | 11 | // props 12 | let vnodeProps 13 | 14 | // children 15 | const children = node.children 16 | let vnodeChildren = children[0] 17 | 18 | node.codegenNode = createVNodeCall( 19 | context, 20 | vnodeTag, 21 | vnodeProps, 22 | vnodeChildren 23 | ) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from '@tiny-vue/shared' 2 | 3 | const publicPropertiesMap = { 4 | $el: (i) => i.vnode.el, 5 | $slots: (i) => i.slots, 6 | $props: (i) => i.props, 7 | } 8 | 9 | export const PublicInstanceProxyHandlers = { 10 | get({ _: instance }, key) { 11 | const { setupState, props } = instance 12 | 13 | if (hasOwn(setupState, key)) { 14 | return setupState[key] 15 | } else if (hasOwn(props, key)) { 16 | return props[key] 17 | } 18 | 19 | const publicGetter = publicPropertiesMap[key] 20 | if (publicGetter) { 21 | return publicGetter(instance) 22 | } 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /packages/reactivity/src/computed.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from "./effect" 2 | 3 | class ComputedRefImpl { 4 | private _getter: any 5 | private _dirty: boolean = true 6 | private _value: any 7 | private _effect: ReactiveEffect 8 | constructor(getter) { 9 | this._getter = getter 10 | this._effect = new ReactiveEffect(getter, () => { 11 | this._dirty = true 12 | }) 13 | } 14 | get value() { 15 | if (this._dirty) { 16 | this._dirty = false 17 | this._value = this._effect.run() 18 | } 19 | return this._value 20 | } 21 | } 22 | 23 | 24 | 25 | 26 | export function computed(getter) { 27 | return new ComputedRefImpl(getter) 28 | } -------------------------------------------------------------------------------- /packages/runtime-core/src/apiWatch.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from '@tiny-vue/reactivity' 2 | import { queuePreFlushCb } from './scheduler' 3 | 4 | export function watchEffect(source) { 5 | function job() { 6 | effect.run() 7 | } 8 | 9 | let cleanup 10 | const onCleanup = function (fn) { 11 | cleanup = effect.onStop = () => { 12 | fn() 13 | } 14 | } 15 | 16 | function getter() { 17 | if (cleanup) { 18 | cleanup() 19 | } 20 | source(onCleanup) 21 | } 22 | 23 | const effect = new ReactiveEffect(getter, () => { 24 | queuePreFlushCb(job) 25 | }) 26 | 27 | effect.run() 28 | 29 | return () => { 30 | effect.stop() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/vue/examples/componentSlot/App.js: -------------------------------------------------------------------------------- 1 | import { h, createTextVNode } from '../../dist/tiny-vue.esm.js' 2 | import { Foo } from './Foo.js' 3 | export const App = { 4 | name: 'App', 5 | render() { 6 | const app = h('div', {}, 'App') 7 | const foo = h( 8 | Foo, 9 | {}, 10 | { 11 | header: ({ type }) => [ 12 | h('p', {}, 'header ' + type), 13 | createTextVNode('创建了text节点'), 14 | ], 15 | footer: () => h('p', {}, 'footer'), 16 | } 17 | ) 18 | // const foo = h(Foo, {}, h('p', {}, 'this is second slot')) 19 | 20 | return h('div', {}, [app, foo]) 21 | }, 22 | setup() { 23 | return {} 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /packages/vue/examples/pathChildren/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/tiny-vue.esm.js' 2 | 3 | import ArrayToText from './ArrayToText.js' 4 | import TextToText from './TextToText.js' 5 | import TextToArray from './TextToArray.js' 6 | import ArrayToArray from './ArrayToArray.js' 7 | 8 | export default { 9 | name: 'App', 10 | setup() {}, 11 | render() { 12 | return h('div', { tId: 1 }, [ 13 | h('p', {}, '主页'), 14 | // 老的是 array 新的是 text 15 | // h(ArrayToText), 16 | // 老的是 text 新的是 text 17 | // h(TextToText), 18 | // 老的是 text 新的是 array 19 | // h(TextToArray), 20 | // 老的是 array 新的是 array 21 | h(ArrayToArray), 22 | ]) 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/transform.spec.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../src/ast' 2 | import { baseParse } from '../src/parse' 3 | import { transform } from '../src/transform' 4 | 5 | describe('transform', () => { 6 | it('happy path', () => { 7 | const ast = baseParse('
hi,{{message}}
') 8 | const plugins = [ 9 | (node) => { 10 | if (node.type === NodeTypes.TEXT) { 11 | node.content = node.content + 'tiny-vue' 12 | } 13 | }, 14 | ] 15 | transform(ast, { 16 | nodeTransforms: plugins, 17 | }) 18 | 19 | const nodeText = ast.children[0].children[0] 20 | expect(nodeText.content).toBe('hi,tiny-vue') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/runtime-core/src/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from '@tiny-vue/shared' 2 | 3 | export function initSlots(instance, children) { 4 | // 判断是否具名插槽 5 | const { vnode } = instance 6 | if (vnode.shapeFlag & ShapeFlags.SLOT_CHILDREN) { 7 | normalizeObjectSlots(children, instance.slots) 8 | } 9 | } 10 | 11 | // 处理具名插槽 12 | function normalizeObjectSlots(children: any, slots: any) { 13 | for (const key in children) { 14 | const value = children[key] 15 | // 使用function是为了向插槽传参 16 | slots[key] = (props) => normalizeSlotValue(value(props)) 17 | } 18 | } 19 | 20 | function normalizeSlotValue(value) { 21 | // 判断children是否数组的目的:让用户写slot时,既可以填写数组,也可以填写单个 22 | return Array.isArray(value) ? value : [value] 23 | } 24 | -------------------------------------------------------------------------------- /packages/vue/examples/componentEmit/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/tiny-vue.esm.js' 2 | import { Foo } from './Foo.js' 3 | window.self = null 4 | export const App = { 5 | render() { 6 | window.self = this 7 | return h( 8 | 'div', 9 | { 10 | id: 'root', 11 | class: ['yellow', 'green'], 12 | }, 13 | [ 14 | h('div', {}, 'hi,' + this.msg), 15 | h(Foo, { 16 | onAdd(a, b) { 17 | console.log('来自foo组件的onAdd事件', a, b) 18 | }, 19 | onAddFoo(c, d) { 20 | console.log('onAddFoo', c, d) 21 | }, 22 | }), 23 | ] 24 | ) 25 | }, 26 | 27 | setup() { 28 | return { 29 | msg: 'tiny-vue,ni hao', 30 | } 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /packages/vue/examples/customRenderer/main.js: -------------------------------------------------------------------------------- 1 | import { createRenderer } from '../../dist/tiny-vue.esm.js' 2 | import { App } from './App.js' 3 | console.log(PIXI) 4 | 5 | const game = new PIXI.Application({ 6 | width: 500, 7 | height: 500, 8 | }) 9 | document.body.append(game.view) 10 | 11 | const renderer = createRenderer({ 12 | createElement(type) { 13 | if (type === 'rect') { 14 | const rect = new PIXI.Graphics() 15 | rect.beginFill(0xff0000) 16 | rect.drawRect(0, 0, 100, 100) 17 | rect.endFill() 18 | 19 | return rect 20 | } 21 | }, 22 | patchProp(el, key, val) { 23 | el[key] = val 24 | }, 25 | insert(el, parent) { 26 | parent.addChild(el) 27 | }, 28 | }) 29 | 30 | renderer.createApp(App).mount(game.stage) 31 | -------------------------------------------------------------------------------- /packages/vue/examples/nextTicker/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | getCurrentInstance, 3 | h, 4 | ref, 5 | nextTick, 6 | } from '../../dist/tiny-vue.esm.js' 7 | export const App = { 8 | name: 'App', 9 | setup() { 10 | const count = ref(0) 11 | const instance = getCurrentInstance() 12 | function onClick() { 13 | for (let i = 0; i < 100; i++) { 14 | count.value++ 15 | } 16 | 17 | debugger 18 | console.log(instance) 19 | nextTick(() => { 20 | console.log(instance) 21 | }) 22 | } 23 | return { 24 | count, 25 | onClick, 26 | } 27 | }, 28 | render() { 29 | const button = h('button', { onClick: this.onClick }, '更新') 30 | const p = h('p', {}, 'count:' + this.count) 31 | return h('div', {}, [button, p]) 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /packages/vue/examples/helloworld/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../dist/tiny-vue.esm.js' 2 | import { Foo } from './Foo.js' 3 | window.self = null 4 | export const App = { 5 | render() { 6 | window.self = this 7 | return h( 8 | 'div', 9 | { 10 | id: 'root', 11 | class: ['yellow', 'green'], 12 | onClick() { 13 | console.log('click') 14 | }, 15 | onMousedown() { 16 | console.log('mousedown') 17 | }, 18 | }, 19 | [h('div', {}, 'hi,' + this.msg), h(Foo, { count: 1 })] 20 | // [ 21 | // h("p", { class: "yellow" }, "你好"), 22 | // h("p", { class: "green" }, "tiny-vue") 23 | // ] 24 | ) 25 | }, 26 | 27 | setup() { 28 | return { 29 | msg: 'tiny-vue,ni hao', 30 | } 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReadonly, readonly, isProxy } from '../src/reactive' 2 | 3 | describe('readonly', () => { 4 | it('happy path', () => { 5 | const original = { foo: 1, bar: { baz: 2 } } 6 | const wrapped = readonly(original) 7 | expect(wrapped).not.toBe(original) 8 | expect(wrapped.foo).toBe(1) 9 | expect(isReadonly(wrapped)).toBe(true) 10 | expect(isReadonly(original)).toBe(false) 11 | expect(isReadonly(wrapped.bar)).toBe(true) 12 | expect(isReadonly(original.bar)).toBe(false) 13 | expect(isProxy(wrapped)).toBe(true) 14 | expect(isProxy(original)).toBe(false) 15 | }) 16 | 17 | it('warn when call set', () => { 18 | console.warn = vi.fn() 19 | const user = readonly({ 20 | age: 10, 21 | }) 22 | user.age = 11 23 | expect(console.warn).toBeCalled() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, reactive, isProxy } from '../src/reactive' 2 | 3 | describe('reactive', () => { 4 | it('happy path', () => { 5 | const origin = { foo: 1 } 6 | const observed = reactive(origin) 7 | expect(observed).not.toBe(origin) 8 | expect(observed.foo).toBe(1) 9 | expect(isReactive(observed)).toBe(true) 10 | expect(isReactive(origin)).toBe(false) 11 | expect(isProxy(observed)).toBe(true) 12 | expect(isProxy(origin)).toBe(false) 13 | }) 14 | 15 | test('nested reactive', () => { 16 | const original = { 17 | nested: { 18 | foo: 1, 19 | }, 20 | array: [{ bar: 2 }], 21 | } 22 | const observed = reactive(original) 23 | expect(isReactive(observed.nested)).toBe(true) 24 | expect(isReactive(observed.array)).toBe(true) 25 | expect(isReactive(observed.array[0])).toBe(true) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export const extend = Object.assign 2 | 3 | export const EMPTY_OBJ = {} 4 | 5 | export const isObject = (val) => { 6 | return val !== null && typeof val === 'object' 7 | } 8 | export const isString = (val) => typeof val === 'string' 9 | 10 | export const hasChanged = (newVal, val) => { 11 | return !Object.is(newVal, val) 12 | } 13 | 14 | export const isOn = (val) => /^on[A-Z]/.test(val) 15 | 16 | export const hasOwn = (val, key) => 17 | Object.prototype.hasOwnProperty.call(val, key) 18 | 19 | export const camelize = (str: string) => { 20 | return str.replace(/-(\w)/g, (_, c) => { 21 | return c ? c.toUpperCase() : '' 22 | }) 23 | } 24 | const capitalize = (str: string) => { 25 | return str.charAt(0).toUpperCase() + str.slice(1) 26 | } 27 | export const toHandlerKey = (str: string) => { 28 | return str ? 'on' + capitalize(str) : '' 29 | } 30 | 31 | export * from './toDisplayString' 32 | export * from './ShapeFlags' 33 | -------------------------------------------------------------------------------- /packages/runtime-core/src/scheduler.ts: -------------------------------------------------------------------------------- 1 | const queue: any[] = [] 2 | const activePreFlushCbs: any[] = [] 3 | 4 | let isFlushPending = false 5 | 6 | const p = Promise.resolve() 7 | export function nextTick(fn?) { 8 | return fn ? p.then(fn) : p 9 | } 10 | export function queueJobs(job) { 11 | if (!queue.includes(job)) { 12 | queue.push(job) 13 | } 14 | 15 | queueFlush() 16 | } 17 | 18 | function queueFlush() { 19 | if (isFlushPending) return 20 | isFlushPending = true 21 | 22 | nextTick(flushJob) 23 | } 24 | 25 | export function queuePreFlushCb(job) { 26 | activePreFlushCbs.push(job) 27 | 28 | queueFlush() 29 | } 30 | 31 | function flushJob() { 32 | isFlushPending = false 33 | 34 | flushPreFlushCbs() 35 | 36 | let job 37 | while ((job = queue.shift())) { 38 | job && job() 39 | } 40 | } 41 | 42 | function flushPreFlushCbs() { 43 | for (let i = 0; i < activePreFlushCbs.length; i++) { 44 | activePreFlushCbs[i]() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/runtime-core/src/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from './component' 2 | 3 | export function provide(key, value) { 4 | const currentInstance: any = getCurrentInstance() 5 | if (currentInstance) { 6 | let { provides } = currentInstance 7 | const parentProvides = currentInstance.parent.provides 8 | 9 | // 如果是第一次在当前组件provide,则把父组件的provide挂到当前组件provides的原型链上 10 | if (provides === parentProvides) { 11 | provides = currentInstance.provides = Object.create(parentProvides) 12 | } 13 | provides[key] = value 14 | } 15 | } 16 | export function inject(key, defaultValue) { 17 | const currentInstance: any = getCurrentInstance() 18 | if (currentInstance) { 19 | const parentProvides = currentInstance.parent.provides 20 | if (key in parentProvides) { 21 | return parentProvides[key] 22 | } else if (defaultValue) { 23 | if(typeof defaultValue === 'function'){ 24 | return defaultValue() 25 | } 26 | return defaultValue 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/vue/examples/componentUpdate/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../dist/tiny-vue.esm.js' 2 | import Child from './Child.js' 3 | 4 | export const App = { 5 | name: 'App', 6 | setup() { 7 | const msg = ref('123') 8 | const count = ref(1) 9 | 10 | window.msg = msg 11 | 12 | function changeChildProps() { 13 | msg.value = '456' 14 | } 15 | 16 | function changeCount() { 17 | count.value++ 18 | } 19 | 20 | return { 21 | msg, 22 | changeChildProps, 23 | count, 24 | changeCount, 25 | } 26 | }, 27 | 28 | render() { 29 | return h('div', {}, [ 30 | h('div', {}, '你好'), 31 | h( 32 | 'button', 33 | { 34 | onClick: this.changeChildProps, 35 | }, 36 | 'change child props' 37 | ), 38 | h(Child, { 39 | msg: this.msg, 40 | }), 41 | h( 42 | 'button', 43 | { 44 | onClick: this.changeCount, 45 | }, 46 | 'change self count' 47 | ), 48 | h('p', {}, 'count:' + this.count), 49 | ]) 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /packages/reactivity/src/reactive.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from '@tiny-vue/shared' 2 | import { 3 | mutableHandlers, 4 | readonlyHandlers, 5 | shallowReadonlyHandlers, 6 | } from './baseHandlers' 7 | 8 | export function reactive(raw) { 9 | return createActiveObject(raw, mutableHandlers) 10 | } 11 | 12 | export function readonly(raw) { 13 | return createActiveObject(raw, readonlyHandlers) 14 | } 15 | 16 | export function shallowReadonly(raw) { 17 | return createActiveObject(raw, shallowReadonlyHandlers) 18 | } 19 | 20 | function createActiveObject(raw: any, baseHandlers) { 21 | if (!isObject(raw)) { 22 | console.warn(`target ${raw} 必须是一个对象`) 23 | return raw 24 | } 25 | return new Proxy(raw, baseHandlers) 26 | } 27 | 28 | export const enum ReactiveFlags { 29 | IS_REACTIVE = '__v_isReactive', 30 | IS_READONLY = '__v_isReadonly', 31 | } 32 | 33 | export function isReactive(raw) { 34 | return !!raw[ReactiveFlags.IS_REACTIVE] 35 | } 36 | 37 | export function isReadonly(raw) { 38 | return !!raw[ReactiveFlags.IS_READONLY] 39 | } 40 | 41 | export function isProxy(value) { 42 | return isReactive(value) || isReadonly(value) 43 | } 44 | -------------------------------------------------------------------------------- /packages/runtime-dom/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createRenderer } from '@tiny-vue/runtime-core' 2 | import { isOn } from '@tiny-vue/shared' 3 | function createElement(type) { 4 | return document.createElement(type) 5 | } 6 | function patchProp(el, key, prevVal, nextVal) { 7 | if (isOn(key)) { 8 | const event = key.slice(2).toLowerCase() 9 | el.addEventListener(event, nextVal) 10 | } else { 11 | if (nextVal === undefined || nextVal === null) { 12 | el.removeAttribute(key) 13 | } else { 14 | el.setAttribute(key, nextVal) 15 | } 16 | } 17 | } 18 | function insert(child, parent, anchor) { 19 | parent.insertBefore(child, anchor || null) 20 | } 21 | 22 | function remove(child) { 23 | const parent = child.parentNode 24 | if (parent) { 25 | parent.removeChild(child) 26 | } 27 | } 28 | function setElementText(el, text) { 29 | el.textContent = text 30 | } 31 | const renderer: any = createRenderer({ 32 | createElement, 33 | patchProp, 34 | insert, 35 | remove, 36 | setElementText, 37 | }) 38 | 39 | export function createApp(...args) { 40 | return renderer.createApp(...args) 41 | } 42 | 43 | export * from '@tiny-vue/runtime-core' 44 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transforms/transformText.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../ast' 2 | import { isText } from '../utils' 3 | 4 | export function transformText(node) { 5 | if (node.type === NodeTypes.ELEMENT) { 6 | return () => { 7 | const { children } = node 8 | 9 | let currentContainer 10 | for (let i = 0; i < children.length; i++) { 11 | const child = children[i] 12 | if (isText(child)) { 13 | for (let j = i + 1; j < children.length; j++) { 14 | const next = children[j] 15 | if (isText(next)) { 16 | if (!currentContainer) { 17 | currentContainer = children[i] = { 18 | type: NodeTypes.COMPOUND_EXPRESSION, 19 | children: [child], 20 | } 21 | } 22 | 23 | currentContainer.children.push(' + ') 24 | currentContainer.children.push(next) 25 | 26 | children.splice(j, 1) 27 | j-- 28 | } else { 29 | currentContainer = undefined 30 | break 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vue3 的简单实现,学习参考 [mini-vue](https://github.com/cuixiaorui/mini-vue) 2 | 3 | 目前已实现功能: 4 | 5 | - [x] effect、reactive、依赖收集、依赖触发 6 | - [x] effect 的 scheduler 功能 7 | - [x] readonly 功能 8 | - [x] isReactive、isReadonly 工具函数 9 | - [x] stop 功能 10 | - [x] reactive、readonly 的对象嵌套 11 | - [x] shallowReadonly 功能 12 | - [x] isProxy 工具函数 13 | - [x] ref 功能 14 | - [x] isRef、unRef 工具函数 15 | - [x] proxyRefs 功能 16 | - [x] computed 计算属性 17 | - [x] component 初始化的主流程 18 | - [x] rollup 打包配置 19 | - [x] 初始化 element 流程 20 | - [x] 组件代理对象 21 | - [x] shapeFlags 22 | - [x] 事件注册 23 | - [x] 组件 props 逻辑 24 | - [x] 组件 emit 逻辑 25 | - [x] 组件 slot 逻辑 26 | - [x] getCurrentInstance 功能 27 | - [x] provide/inject 功能 28 | - [x] createRenderer 功能 29 | - [x] element 更新流程 30 | - [x] 更新 element 的 props 31 | - [x] children 的更新及 diff 算法 32 | - [x] 组件的更新功能 33 | - [x] nextTick 功能 34 | - [x] 解析插值功能 35 | - [x] 解析 element 功能 36 | - [x] 解析 text 功能 37 | - [x] 解析三种联合类型 38 | - [x] transform 功能 39 | - [x] 代码生成 string 类型 40 | - [x] 代码生成插值类型 41 | - [x] 代码生成三种联合类型 42 | - [x] 编译 template 成 render 函数 43 | - [x] 实现 watchEffect 方法 44 | - [x] 升级 monorepo 架构,替换 vitest 45 | 46 | 目前已编写的文档: 47 | 48 | - [x] reactive、ref、effect 的流程解析 [掘金传送门](https://juejin.cn/post/7043265644594200613) 49 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { computed } from '../src/computed' 2 | import { reactive } from '../src/reactive' 3 | 4 | describe('computed', () => { 5 | it('happy path', () => { 6 | const user = reactive({ 7 | age: 1, 8 | }) 9 | const age = computed(() => user.age) 10 | expect(age.value).toBe(1) 11 | }) 12 | 13 | it('should compute lazily', () => { 14 | const value = reactive({ 15 | foo: 1, 16 | }) 17 | const getter = vi.fn(() => { 18 | return value.foo 19 | }) 20 | const cValue = computed(getter) 21 | 22 | // lazy 23 | expect(getter).not.toHaveBeenCalled() 24 | 25 | expect(cValue.value).toBe(1) 26 | expect(getter).toHaveBeenCalledTimes(1) 27 | 28 | // should not compute again 29 | cValue.value //get 30 | expect(getter).toHaveBeenCalledTimes(1) 31 | 32 | // should not compute until needed 33 | value.foo = 2 34 | expect(getter).toHaveBeenCalledTimes(1) 35 | 36 | // now it should compute 37 | expect(cValue.value).toBe(2) 38 | expect(getter).toHaveBeenCalledTimes(2) 39 | 40 | // should not compute again 41 | cValue.value 42 | expect(getter).toHaveBeenCalledTimes(2) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /packages/runtime-core/src/vnode.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from '@tiny-vue/shared' 2 | 3 | export const Fragment = Symbol('Fragment') 4 | export const Text = Symbol('Text') 5 | 6 | export { createVNode as createElementVNode } 7 | 8 | export function createVNode(type, props?, children?) { 9 | const vnode = { 10 | type, 11 | props, 12 | children, 13 | component: null, 14 | shapeFlag: getShapeFlag(type), 15 | key: props && props.key, 16 | el: null, 17 | subTree: {}, 18 | isMounted: false, 19 | } 20 | 21 | // 对children进行标识 22 | if (typeof children === 'string') { 23 | vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN 24 | } else if (Array.isArray(children)) { 25 | vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN 26 | } 27 | 28 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 29 | if (typeof children === 'object') { 30 | vnode.shapeFlag |= ShapeFlags.SLOT_CHILDREN 31 | } 32 | } 33 | return vnode 34 | } 35 | 36 | export function createTextVNode(text: string) { 37 | return createVNode(Text, {}, text) 38 | } 39 | 40 | function getShapeFlag(type) { 41 | return typeof type === 'string' 42 | ? ShapeFlags.ELEMENT 43 | : ShapeFlags.STATEFUL_COMPONENT 44 | } 45 | -------------------------------------------------------------------------------- /packages/vue/examples/provideInject/App.js: -------------------------------------------------------------------------------- 1 | import { h, provide, inject } from '../../dist/tiny-vue.esm.js' 2 | 3 | const Provider = { 4 | name: 'Provider', 5 | setup() { 6 | provide('foo', 'fooVal') 7 | provide('bar', 'barVal') 8 | }, 9 | render() { 10 | return h('div', {}, [h('p', {}, 'Provider'), h(ProviderTwo)]) 11 | }, 12 | } 13 | 14 | const ProviderTwo = { 15 | name: 'ProviderTwo', 16 | setup() { 17 | provide('foo', 'fooTwo') 18 | const foo = inject('foo') 19 | return { foo } 20 | }, 21 | render() { 22 | return h('div', {}, [ 23 | h('p', {}, `ProviderTwo foo:${this.foo}`), 24 | h(Consumer), 25 | ]) 26 | }, 27 | } 28 | 29 | const Consumer = { 30 | name: 'Consumer', 31 | setup() { 32 | const foo = inject('foo') 33 | const bar = inject('bar') 34 | const baz = inject('baz', 'bazDefault') 35 | const fun = inject('fun', () => 'funVal') 36 | return { 37 | foo, 38 | bar, 39 | baz, 40 | fun, 41 | } 42 | }, 43 | render() { 44 | return h( 45 | 'div', 46 | {}, 47 | `Consumer: - ${this.foo} - ${this.bar} - ${this.baz} - ${this.fun}` 48 | ) 49 | }, 50 | } 51 | 52 | export default { 53 | name: 'App', 54 | setup() {}, 55 | render() { 56 | return h('div', {}, [h('p', {}, 'provideInject'), h(Provider)]) 57 | }, 58 | } 59 | -------------------------------------------------------------------------------- /packages/runtime-core/__tests__/apiWatch.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from '@tiny-vue/reactivity' 2 | import { watchEffect } from '../src/apiWatch' 3 | import { nextTick } from '../src/scheduler' 4 | 5 | describe('api: watch', () => { 6 | it('effect', async () => { 7 | const state = reactive({ count: 0 }) 8 | let dummy 9 | watchEffect(() => { 10 | dummy = state.count 11 | }) 12 | expect(dummy).toBe(0) 13 | 14 | state.count++ 15 | await nextTick() 16 | expect(dummy).toBe(1) 17 | }) 18 | 19 | it('stopping the watcher (effect)', async () => { 20 | const state = reactive({ count: 0 }) 21 | let dummy 22 | const stop: any = watchEffect(() => { 23 | dummy = state.count 24 | }) 25 | expect(dummy).toBe(0) 26 | 27 | stop() 28 | state.count++ 29 | await nextTick() 30 | 31 | expect(dummy).toBe(0) 32 | }) 33 | 34 | it('cleanup registration (effect)', async () => { 35 | const state = reactive({ count: 0 }) 36 | const cleanup = vi.fn() 37 | let dummy 38 | const stop: any = watchEffect((onCleanup) => { 39 | onCleanup(cleanup) 40 | dummy = state.count 41 | }) 42 | expect(dummy).toBe(0) 43 | 44 | state.count++ 45 | await nextTick() 46 | 47 | expect(cleanup).toHaveBeenCalledTimes(1) 48 | expect(dummy).toBe(1) 49 | 50 | stop() 51 | expect(cleanup).toHaveBeenCalledTimes(2) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/codegen.spec.ts: -------------------------------------------------------------------------------- 1 | import { generate } from '../src/codegen' 2 | import { baseParse } from '../src/parse' 3 | import { transform } from '../src/transform' 4 | import { transformElement } from '../src/transforms/transformElement' 5 | import { transformExpression } from '../src/transforms/transformExpression' 6 | import { transformText } from '../src/transforms/transformText' 7 | 8 | describe('codegen', () => { 9 | it('string', () => { 10 | const ast = baseParse('hi') 11 | transform(ast) 12 | const { code } = generate(ast) 13 | // 快照测试 14 | expect(code).toMatchSnapshot() 15 | }) 16 | 17 | it('interpolation', () => { 18 | const ast = baseParse('{{message}}') 19 | transform(ast, { 20 | nodeTransforms: [transformExpression], 21 | }) 22 | const { code } = generate(ast) 23 | // 快照测试 24 | expect(code).toMatchSnapshot() 25 | }) 26 | 27 | it('element', () => { 28 | const ast = baseParse('
') 29 | transform(ast, { 30 | nodeTransforms: [transformElement], 31 | }) 32 | const { code } = generate(ast) 33 | // 快照测试 34 | expect(code).toMatchSnapshot() 35 | }) 36 | 37 | it('union', () => { 38 | const ast = baseParse('
hi, {{message}}
') 39 | transform(ast, { 40 | nodeTransforms: [transformExpression, transformElement, transformText], 41 | }) 42 | 43 | const { code } = generate(ast) 44 | // 快照测试 45 | expect(code).toMatchSnapshot() 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /packages/vue/examples/update/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../dist/tiny-vue.esm.js' 2 | export const App = { 3 | name: 'App', 4 | setup() { 5 | const count = ref(0) 6 | function onClick() { 7 | count.value++ 8 | } 9 | 10 | const props = ref({ 11 | foo: 'foo', 12 | bar: 'bar', 13 | }) 14 | function setNewFoo() { 15 | props.value.foo = 'new-foo' 16 | } 17 | function setNullFoo() { 18 | props.value.foo = null 19 | } 20 | function delBar() { 21 | props.value = { 22 | foo: 'foo', 23 | } 24 | } 25 | return { 26 | count, 27 | onClick, 28 | setNewFoo, 29 | setNullFoo, 30 | delBar, 31 | props, 32 | } 33 | }, 34 | render() { 35 | return h( 36 | 'div', 37 | { 38 | id: 'root', 39 | ...this.props, 40 | }, 41 | [ 42 | h('div', {}, 'count:' + this.count), 43 | h( 44 | 'button', 45 | { 46 | onClick: this.onClick, 47 | }, 48 | 'click' 49 | ), 50 | h( 51 | 'button', 52 | { 53 | onClick: this.setNewFoo, 54 | }, 55 | 'new-foo' 56 | ), 57 | h( 58 | 'button', 59 | { 60 | onClick: this.setNullFoo, 61 | }, 62 | 'foo-undefined' 63 | ), 64 | h( 65 | 'button', 66 | { 67 | onClick: this.delBar, 68 | }, 69 | 'delBar' 70 | ), 71 | ] 72 | ) 73 | }, 74 | } 75 | -------------------------------------------------------------------------------- /packages/reactivity/src/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import { extend, isObject } from '@tiny-vue/shared' 2 | import { track, trigger } from './effect' 3 | import { reactive, ReactiveFlags, readonly } from './reactive' 4 | const get = createGetter() 5 | const set = createSetter() 6 | const readonlyGet = createGetter(true) 7 | const shallowReadonlyGet = createGetter(true, true) 8 | export const mutableHandlers = { 9 | get, 10 | set, 11 | } 12 | export const readonlyHandlers = { 13 | get: readonlyGet, 14 | set(target, key, value) { 15 | console.warn( 16 | `key ${key} can not be set, because target is readonly`, 17 | target 18 | ) 19 | return true 20 | }, 21 | } 22 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 23 | get: shallowReadonlyGet, 24 | }) 25 | 26 | function createGetter(isReadonly = false, isShallow = false) { 27 | return function (target, key) { 28 | if (key === ReactiveFlags.IS_REACTIVE) { 29 | return !isReadonly 30 | } else if (key === ReactiveFlags.IS_READONLY) { 31 | return isReadonly 32 | } 33 | 34 | const res = Reflect.get(target, key) 35 | 36 | if (isShallow) { 37 | return res 38 | } 39 | 40 | if (isObject(res)) { 41 | return isReadonly ? readonly(res) : reactive(res) 42 | } 43 | 44 | if (!isReadonly) { 45 | track(target, key) 46 | } 47 | return res 48 | } 49 | } 50 | 51 | function createSetter() { 52 | return function (target, key, value) { 53 | const res = Reflect.set(target, key, value) 54 | trigger(target, key) 55 | return res 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`codegen > element 1`] = ` 4 | "const { createElementVNode:_createElementVNode } = Vue 5 | return function render(_ctx, _cache){return _createElementVNode('div', null, null)}" 6 | `; 7 | 8 | exports[`codegen > interpolation 1`] = ` 9 | "const { toDisplayString:_toDisplayString } = Vue 10 | return function render(_ctx, _cache){return _toDisplayString(_ctx.message)}" 11 | `; 12 | 13 | exports[`codegen > string 1`] = ` 14 | " 15 | return function render(_ctx, _cache){return 'hi'}" 16 | `; 17 | 18 | exports[`codegen > union 1`] = ` 19 | "const { toDisplayString:_toDisplayString, createElementVNode:_createElementVNode } = Vue 20 | return function render(_ctx, _cache){return _createElementVNode('div', null, 'hi, ' + _toDisplayString(_ctx.message))}" 21 | `; 22 | 23 | exports[`codegen element 1`] = ` 24 | "const { createElementVNode:_createElementVNode } = Vue 25 | return function render(_ctx, _cache){return _createElementVNode('div', null, null)}" 26 | `; 27 | 28 | exports[`codegen interpolation 1`] = ` 29 | "const { toDisplayString:_toDisplayString } = Vue 30 | return function render(_ctx, _cache){return _toDisplayString(_ctx.message)}" 31 | `; 32 | 33 | exports[`codegen string 1`] = ` 34 | " 35 | return function render(_ctx, _cache){return 'hi'}" 36 | `; 37 | 38 | exports[`codegen union 1`] = ` 39 | "const { toDisplayString:_toDisplayString, createElementVNode:_createElementVNode } = Vue 40 | return function render(_ctx, _cache){return _createElementVNode('div', null, 'hi, ' + _toDisplayString(_ctx.message))}" 41 | `; 42 | -------------------------------------------------------------------------------- /packages/reactivity/src/ref.ts: -------------------------------------------------------------------------------- 1 | import { hasChanged, isObject } from '@tiny-vue/shared' 2 | import { isTracking, trackEffects, triggerEffects } from './effect' 3 | import { reactive } from './reactive' 4 | 5 | class RefImpl { 6 | private _value: any 7 | public dep 8 | private _rawValue: any 9 | public __v_isRef = true 10 | constructor(raw) { 11 | this._rawValue = raw 12 | this._value = convert(raw) 13 | this.dep = new Set() 14 | } 15 | get value() { 16 | // 收集依赖 17 | trackRefEffects(this) 18 | return this._value 19 | } 20 | set value(newValue) { 21 | // 一定是先修改了值的,触发依赖 22 | if (hasChanged(newValue, this._rawValue)) { 23 | this._rawValue = newValue 24 | this._value = convert(newValue) 25 | triggerEffects(this.dep) 26 | } 27 | } 28 | } 29 | function trackRefEffects(ref) { 30 | if (isTracking()) { 31 | trackEffects(ref.dep) 32 | } 33 | } 34 | function convert(value) { 35 | return isObject(value) ? reactive(value) : value 36 | } 37 | export function ref(raw) { 38 | return new RefImpl(raw) 39 | } 40 | 41 | export function isRef(value) { 42 | return !!value.__v_isRef 43 | } 44 | 45 | export function unRef(ref) { 46 | return isRef(ref) ? ref.value : ref 47 | } 48 | 49 | export function proxyRefs(objectWidthRefs) { 50 | return new Proxy(objectWidthRefs, { 51 | get(target, key) { 52 | return unRef(Reflect.get(target, key)) 53 | }, 54 | set(target, key, value) { 55 | if (isRef(target[key]) && !isRef(value)) { 56 | return (target[key].value = value) 57 | } else { 58 | return Reflect.set(target, key, value) 59 | } 60 | }, 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /packages/compiler-core/src/transform.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from './ast' 2 | import { TO_DISPLAY_STRING } from './runtimeHelpers' 3 | 4 | export function transform(root, options = {}) { 5 | const context = createTransformContext(root, options) 6 | // 遍历-深度优先搜索 7 | traverseNode(root, context) 8 | 9 | createRootCodegen(root) 10 | 11 | root.helpers = [...context.helpers.keys()] 12 | } 13 | function traverseNode(node: any, context) { 14 | const nodeTransforms = context.nodeTransforms 15 | const exitFns: any = [] 16 | if (nodeTransforms) { 17 | for (let i = 0; i < nodeTransforms.length; i++) { 18 | const transform = context.nodeTransforms[i] 19 | const onExit = transform(node, context) 20 | if (onExit) exitFns.push(onExit) 21 | } 22 | } 23 | 24 | switch (node.type) { 25 | case NodeTypes.INTERPOLATION: 26 | context.helper(TO_DISPLAY_STRING) 27 | break 28 | case NodeTypes.ROOT: 29 | case NodeTypes.ELEMENT: 30 | traverseChildren(node, context) 31 | break 32 | default: 33 | break 34 | } 35 | 36 | let i = exitFns.length 37 | while (i--) { 38 | exitFns[i]() 39 | } 40 | } 41 | function traverseChildren(node: any, context: any) { 42 | const children = node.children 43 | for (let i = 0; i < children.length; i++) { 44 | const node = children[i] 45 | traverseNode(node, context) 46 | } 47 | } 48 | 49 | function createTransformContext(root: any, options: any) { 50 | const context = { 51 | root, 52 | nodeTransforms: options.nodeTransforms, 53 | helpers: new Map(), 54 | helper(key) { 55 | context.helpers.set(key, 1) 56 | }, 57 | } 58 | return context 59 | } 60 | function createRootCodegen(root: any) { 61 | const child = root.children[0] 62 | if (child.type === NodeTypes.ELEMENT) { 63 | root.codegenNode = child.codegenNode 64 | } else { 65 | root.codegenNode = child 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /article/reactive、ref、effect流程解析/index.js: -------------------------------------------------------------------------------- 1 | let activeEffect; 2 | class ReactiveEffect { 3 | constructor(fn) { 4 | this._fn = fn 5 | } 6 | run() { 7 | activeEffect = this 8 | this._fn() 9 | } 10 | } 11 | 12 | function effect(fn) { 13 | const _effect = new ReactiveEffect(fn) 14 | _effect.run() 15 | } 16 | 17 | const targetMap = new Map() 18 | function track(target, key) { 19 | let depsMap = targetMap.get(target) 20 | if (!depsMap) { 21 | depsMap = new Map() 22 | targetMap.set(target, depsMap) 23 | } 24 | 25 | let dep = depsMap.get(key) 26 | if (!dep) { 27 | dep = new Set() 28 | depsMap.set(key, dep) 29 | } 30 | trackEffects(dep) 31 | } 32 | function trackEffects(dep){ 33 | dep.add(activeEffect) 34 | } 35 | 36 | function trigger(target, key) { 37 | const depsMap = targetMap.get(target) 38 | const dep = depsMap.get(key) 39 | triggerEffects(dep) 40 | } 41 | function triggerEffects(dep) { 42 | dep.forEach(effect => effect.run()) 43 | } 44 | 45 | function reactive(raw){ 46 | return new Proxy(raw, { 47 | get(target, key) { 48 | const res = Reflect.get(target, key) 49 | 50 | // 依赖收集 51 | track(target, key) 52 | return res 53 | }, 54 | set(target, key, value) { 55 | const res = Reflect.set(target, key, value) 56 | 57 | // 触发依赖 58 | trigger(target, key) 59 | return res 60 | } 61 | }) 62 | } 63 | 64 | const son = reactive({ 65 | user: 25 66 | }) 67 | let dad 68 | effect(() => { 69 | dad = son.age + 25 70 | }) 71 | son.age = 26 72 | 73 | console.log(dad) 74 | 75 | function ref(raw) { 76 | return new RefImpl(raw) 77 | } 78 | class RefImpl { 79 | constructor(raw) { 80 | this._value = raw 81 | this.dep = new Set() 82 | } 83 | get value() { 84 | trackEffects(this.dep) 85 | return this._value 86 | } 87 | set value(newValue) { 88 | this._value = newValue 89 | triggerEffects(this.dep) 90 | } 91 | } 92 | 93 | const apple = ref(1) 94 | let banana 95 | effect(() => { 96 | banana = apple.value + 2 97 | }) 98 | apple.value = 2 99 | console.log(banana) 100 | apple.value = 3 101 | console.log(banana) -------------------------------------------------------------------------------- /packages/reactivity/__tests__/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '../src/effect' 2 | import { reactive } from '../src/reactive' 3 | import { isRef, ref, unRef, proxyRefs } from '../src/ref' 4 | 5 | describe('ref', () => { 6 | it('happy path', () => { 7 | const a = ref(1) 8 | expect(a.value).toBe(1) 9 | }) 10 | 11 | it('should be reactive', () => { 12 | const a = ref(1) 13 | let dummy 14 | let calls = 0 15 | effect(() => { 16 | calls++ 17 | dummy = a.value 18 | }) 19 | expect(calls).toBe(1) 20 | expect(dummy).toBe(1) 21 | a.value = 2 22 | expect(calls).toBe(2) 23 | expect(dummy).toBe(2) 24 | // same value should not trigger 25 | a.value = 2 26 | expect(calls).toBe(2) 27 | expect(dummy).toBe(2) 28 | }) 29 | 30 | it('should make nested properties reactive', () => { 31 | const raw = { 32 | count: 1, 33 | } 34 | const a = ref(raw) 35 | let dummy 36 | let calls = 0 37 | effect(() => { 38 | calls++ 39 | dummy = a.value.count 40 | }) 41 | expect(calls).toBe(1) 42 | expect(dummy).toBe(1) 43 | a.value.count = 2 44 | expect(calls).toBe(2) 45 | expect(dummy).toBe(2) 46 | a.value = raw 47 | expect(calls).toBe(2) 48 | }) 49 | 50 | it('isRef', () => { 51 | const raw = ref(1) 52 | const a = 1 53 | const user = reactive({ 54 | age: 11, 55 | }) 56 | expect(isRef(raw)).toBe(true) 57 | expect(isRef(a)).toBe(false) 58 | expect(isRef(1)).toBe(false) 59 | expect(isRef(user)).toBe(false) 60 | }) 61 | 62 | it('unRef', () => { 63 | const raw = ref(1) 64 | const a = 1 65 | expect(unRef(raw)).toBe(1) 66 | expect(unRef(a)).toBe(1) 67 | expect(unRef(1)).toBe(1) 68 | }) 69 | 70 | it('proxyRefs', () => { 71 | const user = { 72 | age: ref(25), 73 | name: '文正', 74 | } 75 | 76 | const proxyUser = proxyRefs(user) 77 | expect(user.age.value).toBe(25) 78 | expect(proxyUser.age).toBe(25) 79 | expect(proxyUser.name).toBe('文正') 80 | 81 | proxyUser.age = 26 82 | expect(user.age.value).toBe(26) 83 | expect(proxyUser.age).toBe(26) 84 | 85 | proxyUser.age = ref(27) 86 | expect(user.age.value).toBe(27) 87 | expect(proxyUser.age).toBe(27) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /packages/reactivity/__tests__/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from '../src/reactive' 2 | import { effect, stop } from '../src/effect' 3 | describe('effect', () => { 4 | it('happy path', () => { 5 | const user = reactive({ 6 | age: 10, 7 | }) 8 | 9 | let nextAge 10 | effect(() => { 11 | nextAge = user.age + 1 12 | }) 13 | 14 | expect(nextAge).toBe(11) 15 | 16 | user.age++ 17 | expect(nextAge).toBe(12) 18 | }) 19 | 20 | it('return runner', () => { 21 | let foo = 10 22 | const runner = effect(() => { 23 | foo++ 24 | return 'foo' 25 | }) 26 | 27 | expect(foo).toBe(11) 28 | const r = runner() 29 | expect(foo).toBe(12) 30 | expect(r).toBe('foo') 31 | }) 32 | 33 | it('scheduler', () => { 34 | // 1、 通过 effect 的第二个参数,给定一个 scheduler 的 fn 35 | // 2、 effect 第一次执行的时候,会执行fn 36 | // 3、 当响应式对象set update 的时候不会执行 fn, 而是执行 scheduler 37 | // 4、 当执行 runner 的时候,会再次的执行 fn 38 | let dummy 39 | let run: any 40 | const scheduler = vi.fn(() => [(run = runner)]) 41 | const obj = reactive({ foo: 1 }) 42 | const runner = effect( 43 | () => { 44 | dummy = obj.foo 45 | }, 46 | { scheduler } 47 | ) 48 | expect(scheduler).not.toHaveBeenCalled() 49 | expect(dummy).toBe(1) 50 | // should be called on first trigger 51 | obj.foo++ 52 | expect(scheduler).toHaveBeenCalledTimes(1) 53 | // // should not run yet 54 | expect(dummy).toBe(1) 55 | // // manually run 56 | run() 57 | // // should have run 58 | expect(dummy).toBe(2) 59 | }) 60 | 61 | it('stop', () => { 62 | let dummy 63 | const obj = reactive({ prop: 1 }) 64 | const runner = effect(() => { 65 | dummy = obj.prop 66 | }) 67 | obj.prop = 2 68 | expect(dummy).toBe(2) 69 | stop(runner) 70 | // obj.prop = 3 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('onStop', () => { 80 | const obj = reactive({ 81 | foo: 1, 82 | }) 83 | const onStop = vi.fn() 84 | let dummy 85 | const runner = effect( 86 | () => { 87 | dummy = obj.foo 88 | }, 89 | { 90 | onStop, 91 | } 92 | ) 93 | 94 | stop(runner) 95 | expect(onStop).toBeCalledTimes(1) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /packages/runtime-core/src/component.ts: -------------------------------------------------------------------------------- 1 | import { shallowReadonly, proxyRefs } from '@tiny-vue/reactivity' 2 | import { emit } from './componentEmit' 3 | import { initProps } from './componentProps' 4 | import { PublicInstanceProxyHandlers } from './componentPublicInstance' 5 | import { initSlots } from './componentSlots' 6 | 7 | export function createComponentInstance(vnode, parent) { 8 | const component = { 9 | vnode, 10 | type: vnode.type, 11 | next: null, // 新的VNode 12 | setupState: {}, 13 | props: {}, 14 | emit: () => {}, 15 | slots: {}, 16 | provides: parent ? parent.provides : {}, 17 | parent, 18 | } 19 | // bind的作用:默认emit的第一个参数是组件实例。 20 | // 用户在调用emit时就可以只传事件名称,不用传父组件实例了。 21 | component.emit = emit.bind(null, component) as any 22 | return component 23 | } 24 | 25 | export function setupComponent(instance) { 26 | initProps(instance, instance.vnode.props) 27 | initSlots(instance, instance.vnode.children) 28 | setupStatefulComponent(instance) 29 | } 30 | 31 | function setupStatefulComponent(instance) { 32 | const Component = instance.type 33 | 34 | // 对render中的this指向进行代理 35 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers) 36 | 37 | const { setup } = Component 38 | 39 | if (setup) { 40 | setCurrentInstance(instance) 41 | const setupResult = setup(shallowReadonly(instance.props), { 42 | emit: instance.emit, 43 | }) 44 | // getCurrentInstance 会在 setup 里面被执行,执行完之后就把全局变量重置回 null 45 | setCurrentInstance(null) 46 | handleSetupResult(instance, setupResult) 47 | } 48 | } 49 | 50 | function handleSetupResult(instance, setupResult) { 51 | if (typeof setupResult === 'object') { 52 | // 此处的proxyRefs是在为了render中自动解构ref的.value 53 | instance.setupState = proxyRefs(setupResult) 54 | } 55 | 56 | finishComponentSetup(instance) 57 | } 58 | 59 | function finishComponentSetup(instance) { 60 | const Component = instance.type 61 | 62 | if (compiler && !Component.render) { 63 | if (Component.template) { 64 | Component.render = compiler(Component.template) 65 | } 66 | } 67 | 68 | instance.render = Component.render 69 | } 70 | 71 | let currentInstance = null 72 | export function getCurrentInstance() { 73 | return currentInstance 74 | } 75 | function setCurrentInstance(value) { 76 | currentInstance = value 77 | } 78 | 79 | let compiler 80 | export function registerRuntimeCompiler(_compiler) { 81 | compiler = _compiler 82 | } 83 | -------------------------------------------------------------------------------- /packages/compiler-core/__tests__/parse.spec.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from '../src/ast' 2 | import { baseParse } from '../src/parse' 3 | describe('Parse', () => { 4 | describe('interpolation', () => { 5 | test('simple interpolation', () => { 6 | const ast = baseParse('{{ message }}') 7 | 8 | expect(ast.children[0]).toStrictEqual({ 9 | type: NodeTypes.INTERPOLATION, 10 | content: { 11 | type: NodeTypes.SIMPLE_EXPRESSION, 12 | content: 'message', 13 | }, 14 | }) 15 | }) 16 | }) 17 | 18 | describe('element', () => { 19 | test('simple element div', () => { 20 | const ast = baseParse('
') 21 | 22 | expect(ast.children[0]).toStrictEqual({ 23 | type: NodeTypes.ELEMENT, 24 | tag: 'div', 25 | children: [], 26 | }) 27 | }) 28 | }) 29 | 30 | describe('text', () => { 31 | it('simple text', () => { 32 | const ast = baseParse('some text') 33 | 34 | expect(ast.children[0]).toStrictEqual({ 35 | type: NodeTypes.TEXT, 36 | content: 'some text', 37 | }) 38 | }) 39 | }) 40 | 41 | test('three union type', () => { 42 | const ast = baseParse('
hi,{{message}}
') 43 | expect(ast.children[0]).toStrictEqual({ 44 | type: NodeTypes.ELEMENT, 45 | tag: 'div', 46 | children: [ 47 | { 48 | type: NodeTypes.TEXT, 49 | content: 'hi,', 50 | }, 51 | { 52 | type: NodeTypes.INTERPOLATION, 53 | content: { 54 | type: NodeTypes.SIMPLE_EXPRESSION, 55 | content: 'message', 56 | }, 57 | }, 58 | ], 59 | }) 60 | }) 61 | 62 | test('nested element', () => { 63 | const ast = baseParse('

hi

{{message}}
') 64 | expect(ast.children[0]).toStrictEqual({ 65 | type: NodeTypes.ELEMENT, 66 | tag: 'div', 67 | children: [ 68 | { 69 | type: NodeTypes.ELEMENT, 70 | tag: 'p', 71 | children: [ 72 | { 73 | type: NodeTypes.TEXT, 74 | content: 'hi', 75 | }, 76 | ], 77 | }, 78 | { 79 | type: NodeTypes.INTERPOLATION, 80 | content: { 81 | type: NodeTypes.SIMPLE_EXPRESSION, 82 | content: 'message', 83 | }, 84 | }, 85 | ], 86 | }) 87 | }) 88 | 89 | test('should throw error when lack end tag', () => { 90 | // baseParse('
') 91 | expect(() => { 92 | baseParse('
') 93 | }).toThrow() 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /packages/reactivity/src/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from '@tiny-vue/shared' 2 | 3 | let activeEffect 4 | let shouldTrack = false 5 | 6 | export class ReactiveEffect { 7 | private _fn: any 8 | deps = [] 9 | active = true 10 | onStop?: () => void 11 | public scheduler: Function | undefined 12 | constructor(fn, scheduler?: Function) { 13 | this._fn = fn 14 | this.scheduler = scheduler 15 | } 16 | run() { 17 | if (!this.active) { 18 | return this._fn() 19 | } 20 | 21 | // 应该收集 22 | shouldTrack = true 23 | activeEffect = this 24 | const result = this._fn() 25 | 26 | // 重置 27 | shouldTrack = false 28 | 29 | return result 30 | } 31 | stop() { 32 | if (this.active) { 33 | cleanupEffect(this) 34 | if (this.onStop) { 35 | this.onStop() 36 | } 37 | this.active = false 38 | } 39 | } 40 | } 41 | 42 | function cleanupEffect(effect) { 43 | effect.deps.forEach((dep: any) => { 44 | dep.delete(effect) 45 | }) 46 | 47 | // 把 effect.deps 清空 48 | effect.deps.length = 0 49 | } 50 | 51 | const targetMap = new Map() 52 | export function track(target, key) { 53 | if (!isTracking()) return 54 | // target -> key -> dep 55 | let depsMap = targetMap.get(target) 56 | if (!depsMap) { 57 | depsMap = new Map() 58 | targetMap.set(target, depsMap) 59 | } 60 | 61 | let dep = depsMap.get(key) 62 | if (!dep) { 63 | dep = new Set() 64 | depsMap.set(key, dep) 65 | } 66 | trackEffects(dep) 67 | } 68 | export function trackEffects(dep) { 69 | // 看看 dep 之前有没有添加过,添加过的话,那么就不添加了 70 | if (dep.has(activeEffect)) return 71 | dep.add(activeEffect) 72 | activeEffect.deps.push(dep) 73 | } 74 | 75 | export function isTracking() { 76 | return shouldTrack && activeEffect !== undefined 77 | } 78 | 79 | export function trigger(target, key) { 80 | const depsMap = targetMap.get(target) 81 | const dep = depsMap.get(key) 82 | triggerEffects(dep) 83 | } 84 | export function triggerEffects(dep) { 85 | for (const effect of dep) { 86 | if (effect.scheduler) { 87 | effect.scheduler() 88 | } else { 89 | effect.run() 90 | } 91 | } 92 | } 93 | 94 | export function effect(fn, options: any = {}) { 95 | const { scheduler } = options 96 | const _effect = new ReactiveEffect(fn, scheduler) 97 | 98 | extend(_effect, options) 99 | _effect.run() 100 | 101 | const runner: any = _effect.run.bind(_effect) 102 | runner.effect = _effect 103 | return runner 104 | } 105 | 106 | export function stop(runner) { 107 | runner.effect.stop() 108 | } 109 | -------------------------------------------------------------------------------- /packages/compiler-core/src/codegen.ts: -------------------------------------------------------------------------------- 1 | import { isString } from '@tiny-vue/shared' 2 | import { NodeTypes } from './ast' 3 | import { 4 | CREATE_ELEMENT_VNODE, 5 | helperMapName, 6 | TO_DISPLAY_STRING, 7 | } from './runtimeHelpers' 8 | 9 | export function generate(ast) { 10 | const context = createCodegenContext() 11 | const { push } = context 12 | 13 | genFunctionPreamble(ast, context) 14 | 15 | const functionName = 'render' 16 | const args = ['_ctx', '_cache'] 17 | const signature = args.join(', ') 18 | push(`function ${functionName}(${signature}){`) 19 | 20 | push('return ') 21 | 22 | genNode(ast.codegenNode, context) 23 | 24 | push('}') 25 | 26 | return { 27 | code: context.code, 28 | } 29 | } 30 | function genFunctionPreamble(ast, context) { 31 | const { push, helper } = context 32 | const VueBinging = 'Vue' 33 | const aliasHelper = (s) => `${helper(s)}:_${helper(s)}` 34 | if (ast.helpers.length > 0) { 35 | push(`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinging}`) 36 | } 37 | push('\n') 38 | push('return ') 39 | } 40 | 41 | function genNode(node: any, context: any) { 42 | switch (node.type) { 43 | case NodeTypes.TEXT: 44 | genText(node, context) 45 | break 46 | case NodeTypes.INTERPOLATION: 47 | genInterpolation(node, context) 48 | break 49 | case NodeTypes.SIMPLE_EXPRESSION: 50 | genExpression(node, context) 51 | break 52 | case NodeTypes.SIMPLE_EXPRESSION: 53 | genExpression(node, context) 54 | break 55 | case NodeTypes.ELEMENT: 56 | genElement(node, context) 57 | break 58 | case NodeTypes.COMPOUND_EXPRESSION: 59 | genCompoundExpression(node, context) 60 | break 61 | default: 62 | break 63 | } 64 | } 65 | function genText(node: any, context: any) { 66 | const { push } = context 67 | push(`'${node.content}'`) 68 | } 69 | function genElement(node, context) { 70 | const { push, helper } = context 71 | const { tag, children, props } = node 72 | push(`_${helper(CREATE_ELEMENT_VNODE)}(`) 73 | genNodeList(genNullable([tag, props, children]), context) 74 | push(')') 75 | } 76 | function genNodeList(nodes, context) { 77 | const { push } = context 78 | for (let i = 0; i < nodes.length; i++) { 79 | const node = nodes[i] 80 | if (isString(node)) { 81 | push(node) 82 | } else { 83 | genNode(node, context) 84 | } 85 | if (i < nodes.length - 1) { 86 | push(', ') 87 | } 88 | } 89 | } 90 | function genNullable(args: any[]) { 91 | return args.map((arg) => arg || 'null') 92 | } 93 | function genCompoundExpression(node, context) { 94 | const { push } = context 95 | const children = node.children 96 | for (let i = 0; i < children.length; i++) { 97 | const child = children[i] 98 | if (isString(child)) { 99 | push(child) 100 | } else { 101 | genNode(child, context) 102 | } 103 | } 104 | } 105 | 106 | function createCodegenContext() { 107 | const context = { 108 | code: '', 109 | push(source) { 110 | context.code += source 111 | }, 112 | helper(key) { 113 | return `${helperMapName[key]}` 114 | }, 115 | } 116 | return context 117 | } 118 | function genInterpolation(node: any, context: any) { 119 | const { push, helper } = context 120 | push(`_${helper(TO_DISPLAY_STRING)}(`) 121 | genNode(node.content, context) 122 | push(')') 123 | } 124 | function genExpression(node, context) { 125 | const { push } = context 126 | push(`${node.content}`) 127 | } 128 | -------------------------------------------------------------------------------- /packages/compiler-core/src/parse.ts: -------------------------------------------------------------------------------- 1 | import { NodeTypes } from './ast' 2 | const enum TagType { 3 | START, 4 | END, 5 | } 6 | export function baseParse(content: string) { 7 | const context = createParserContext(content) 8 | return createRoot(parseChildren(context, [])) 9 | } 10 | function createParserContext(content: string) { 11 | return { 12 | source: content, 13 | } 14 | } 15 | function createRoot(children) { 16 | return { 17 | children, 18 | type: NodeTypes.ROOT, 19 | } 20 | } 21 | function parseChildren(context, ancestors): any { 22 | const nodes: any = [] 23 | 24 | while (!isEnd(context, ancestors)) { 25 | let node 26 | const s = context.source 27 | if (s.startsWith('{{')) { 28 | node = parseInterpolation(context) 29 | } else if (s[0] === '<') { 30 | if (/[a-z]/i.test(s[1])) { 31 | node = parseElement(context, ancestors) 32 | } 33 | } 34 | if (!node) { 35 | node = parseText(context) 36 | } 37 | nodes.push(node) 38 | } 39 | 40 | return nodes 41 | } 42 | function isEnd(context, ancestors) { 43 | const s = context.source 44 | // 1、遇到结束标签 45 | for (let i = ancestors.length - 1; i >= 0; i--) { 46 | const tag = ancestors[i].tag 47 | if (startsWithEndTagOpen(s, tag)) { 48 | return true 49 | } 50 | } 51 | // 2、source没有值的时候 52 | return !s 53 | } 54 | function parseInterpolation(context) { 55 | const openDelimiter = '{{' 56 | const closeDelimiter = '}}' 57 | 58 | const closeIndex = context.source.indexOf(closeDelimiter) 59 | advanceBy(context, openDelimiter.length) 60 | 61 | const rawContentLength = closeIndex - openDelimiter.length 62 | const rawContent = parseTextData(context, rawContentLength) 63 | const content = rawContent.trim() 64 | advanceBy(context, closeDelimiter.length) 65 | 66 | return { 67 | type: NodeTypes.INTERPOLATION, 68 | content: { 69 | type: NodeTypes.SIMPLE_EXPRESSION, 70 | content: content, 71 | }, 72 | } 73 | } 74 | 75 | function advanceBy(context: any, length) { 76 | context.source = context.source.slice(length) 77 | } 78 | function parseElement(context: any, ancestors) { 79 | const element: any = parseTag(context, TagType.START) 80 | ancestors.push(element) 81 | element.children = parseChildren(context, ancestors) 82 | ancestors.pop() 83 | 84 | if (startsWithEndTagOpen(context.source, element.tag)) { 85 | parseTag(context, TagType.END) 86 | } else { 87 | throw new Error(`缺少结束标签${element.tag}`) 88 | } 89 | 90 | return element 91 | } 92 | 93 | function startsWithEndTagOpen(source: any, tag: any) { 94 | return ( 95 | source.startsWith(' index) { 119 | endIndex = index 120 | } 121 | } 122 | const content = parseTextData(context, endIndex) 123 | 124 | return { 125 | type: NodeTypes.TEXT, 126 | content, 127 | } 128 | } 129 | function parseTextData(context: any, length) { 130 | const content = context.source.slice(0, length) 131 | advanceBy(context, content.length) 132 | return content 133 | } 134 | -------------------------------------------------------------------------------- /article/reactive、ref、effect流程解析/index.md: -------------------------------------------------------------------------------- 1 | 我们在使用 vue3 响应式的时候,一般是直接在 setup 里边运用 reactive 或 ref,例如下边这种写法 2 | 3 | ````javascript 4 | 10 | ```` 11 | 12 | 然后 user、tree 就和 ui 视图形成了响应式关系。我们在改变数据的时候,视图也会随之改变。 13 | 14 | 但 vue3 的响应式是能够和视图抽离出来的,上面的方法中其实还隐藏了一层 effect 函数。而本文会先介绍 reactive、ref、effect,后边再介绍setup。 15 | 16 | #### 回看原始的响应式。 17 | 18 | ````javascript 19 | let a = 1 20 | let b = a + 1 21 | a = 2 22 | // 我们如果想要 b 随着 a 的变化而改变,可以重新赋值一下,也就是重新执行一遍 23 | b = a + 1 24 | ```` 25 | 26 | 再往上一步,我们可以把赋值的过程封装成一个函数,每次 a 改变的时候,就去执行一遍。这也是一种方法呀。 27 | 28 | ````javascript 29 | let a = 1 30 | let b 31 | function update() { 32 | b = a + 1 33 | } 34 | a = 2 35 | update() 36 | ```` 37 | 38 | 而 vue3 的 reactivity 是怎么做的呢? 39 | 40 | ````javascript 41 | let a = ref(1) 42 | let b 43 | effect(() => { 44 | b = a + 1 45 | }) 46 | a.value = 2 47 | console.log(b) // 3 48 | ```` 49 | 50 | 它会通过 reactive 或 ref 对原始数据做一层代理,借助 effect 收集依赖,在原始数据改变的时候,去触发依赖,也就是自动执行一遍 effect 的函数。 51 | 52 | #### 探索 vue3 中 reactive 的实现方式 53 | 54 | ````javascript 55 | import { track, trigger } from "./effect" 56 | function reactive(raw){ 57 | return new Proxy(raw, { 58 | get(target, key) { 59 | const res = Reflect.get(target, key) 60 | 61 | // 依赖收集 62 | track(target, key) 63 | return res 64 | }, 65 | set(target, key, value) { 66 | const res = Reflect.set(target, key, value) 67 | 68 | // 触发依赖 69 | trigger(target, key) 70 | return res 71 | } 72 | }) 73 | } 74 | ```` 75 | 76 | 我们把 effect 相关的方法独立开来,先来看 reactive 方法。 77 | 78 | vue3 的 reactive 其实就是返回一个 Proxy , 它在 get 的时候,先运用 `Reflect.get` 返回对应值(相当于 `target[key]`),然后通过 track 方法收集依赖;它在 set 的时候,先运用 `Reflect.set` 设置对应值(相当于 `target[key] = newValue`),然后通过 trigger 触发依赖。 79 | 80 | 但是,它在这一步中,仅仅是返回了一个 Proxy 代理,收集和触发依赖的过程并没有执行。 81 | 82 | 接着我们来看 effect 方法。 83 | 84 | ````javascript 85 | let activeEffect; 86 | class ReactiveEffect { 87 | constructor(fn) { 88 | this._fn = fn 89 | } 90 | run() { 91 | activeEffect = this 92 | this._fn() 93 | } 94 | } 95 | 96 | function effect(fn) { 97 | const _effect = new ReactiveEffect(fn) 98 | _effect.run() 99 | } 100 | 101 | const targetMap = new Map() 102 | function track(target, key) { 103 | let depsMap = targetMap.get(target) 104 | if (!depsMap) { 105 | depsMap = new Map() 106 | targetMap.set(target, depsMap) 107 | } 108 | 109 | let dep = depsMap.get(key) 110 | if (!dep) { 111 | dep = new Set() 112 | depsMap.set(key, dep) 113 | } 114 | 115 | dep.add(activeEffect) 116 | } 117 | 118 | function trigger(target, key) { 119 | const depsMap = targetMap.get(target) 120 | const dep = depsMap.get(key) 121 | dep.forEach(effect => effect.run()) 122 | } 123 | ```` 124 | 125 | 这里我们借助一个例子来说明 126 | 127 | ````javascript 128 | let son = reactive({ 129 | age: 25 130 | }) 131 | let dad 132 | effect(() => { 133 | dad = son.age + 25 134 | }) 135 | son.age = 26 136 | console.log(dad) // 51 137 | ```` 138 | 139 | effect 首先接收一个函数。然后 vue3 是运用了一个面向对象的思想,在 effect 中新建一个对象,对象里边把传入的 fn 存起来,在 run 方法中把 this 指向挂载到全局变量 activeEffect 中,紧接着执行一遍传入的函数 fn,也就是执行 `dad = son.age + 25`,执行的过程中其实就触发了 reactive 返回 proxy 中的 get 方法,它会相应执行 effect 里边的 track 方法,收集依赖。 140 | 141 | ##### 那么收集依赖又是怎么做的呢? 142 | 143 | 首先,effect 文件中有个公共变量 targetMap ,是 Map 数据类型(允许将对象作为 key )。 144 | 145 | 接着看 track 方法。在例子中,它接受的 target 变量,也就是 targetMap 的 key 其实就是 `{ age: 25 }` ,这个 target 再对应一个 Map 数据类型 ( depsMap ) 。 146 | 147 | depsMap 中,以原始对象的 key ,即 age 作为自身 key,对应着一个 Set 数据类型(其元素具有唯一性),然后把 effect 中创建的依赖对象 _effect 存进去。 148 | 149 | 最终的数据结构就类似于这样: 150 | 151 | ````javascript 152 | targetMap: { 153 | // key 是对象,value 是 depsMap 154 | {age: 25} : { 155 | // key 是对象里边的 key, value 是 dep 156 | age: [ ...此处存储一个个依赖 ] 157 | } 158 | } 159 | ```` 160 | 161 | 我们继续看例子,effect 里边的逻辑已经走完了,往下执行 `son.age = 26` 。显而易见,这会触发 proxy 代理中的 set 方法,执行 trigger,从 depsMap 中根据 target、key 找到收集依赖的 dep,迭代 dep 执行里边的 run 方法,也即是自动执行了 `dad = son.age + 25` 。 162 | 163 | #### 探索 vue3 中 ref 的实现方式 164 | 165 | 我们知道 reactive 是用来代理复杂数据的,而 ref 一般用于 数字、字符串、布尔值 这类简单数据。那么我们收集依赖的时候,就不需要复杂的 targetMap 了,只需要一个 Set 数据结构就够了。 166 | 167 | 我们先把 track 和 trigger 中的最后一步提取出来 168 | 169 | ````javascript 170 | function track(target, key) { 171 | ...... 172 | // dep.add(activeEffect) 173 | trackEffects(dep) 174 | } 175 | function trackEffects(dep) { 176 | dep.add(activeEffect) 177 | } 178 | 179 | function trigger(target, key) { 180 | ...... 181 | // dep.forEach(effect => effect.run()) 182 | } 183 | function triggerEffects(dep) { 184 | dep.forEach(effect => effect.run()) 185 | } 186 | ```` 187 | 188 | 接着看 ref 的简单实现 189 | 190 | ````javascript 191 | function ref(raw) { 192 | return new RefImpl(raw) 193 | } 194 | class RefImpl { 195 | constructor(raw) { 196 | this._value = raw 197 | this.dep = new Set() 198 | } 199 | get value() { 200 | trackEffects(this.dep) 201 | return this._value 202 | } 203 | set value(newValue) { 204 | this._value = newValue 205 | triggerEffects(this.dep) 206 | } 207 | } 208 | ```` 209 | 210 | 再看个简单的例子 211 | 212 | ````javascript 213 | const apple = ref(1) 214 | let banana 215 | effect(() => { 216 | banana = apple.value + 2 217 | }) 218 | apple.value = 2 219 | console.log(banana) // 4 220 | ```` 221 | 222 | 首先看第一步 `const apple = ref(1)` ,ref 函数内部新建一个 RefImpl 实例,实例内部把传入的值存为私有变量 _value ,同时新建一个存储依赖的 dep 。 223 | 224 | get value 的时候收集依赖到 dep 里边,同时返回值;set value 的时候设置新值,同时触发依赖。同样的,这里仅仅返回实例对象,并没有执行 get 和 set 方法。 225 | 226 | 接着看 effect 里边,和 reactive 那个例子是类似的,先创建一个 ReactiveEffect 对象,再把传入的方法执行一遍。 227 | 228 | 执行的过程中,读取了 `apple.value` ,触发 RefImpl 的 get 方法。此时,trackEffects 方法会把 effect 新建的 ReactiveEffect 实例对象存储到 RefImpl 自身的 dep 中,这里就不是存储到 effect 文件里边的公共变量 targetMap 了。 229 | 230 | 接着看下一步 `apple.value = 2` ,触发 RefImpl 的 set 方法,把自己的 dep 拿出来,迭代执行里边的 run 方法。如此便自动执行了 `banana = apple.value + 2`。 231 | -------------------------------------------------------------------------------- /packages/vue/examples/pathChildren/ArrayToArray.js: -------------------------------------------------------------------------------- 1 | // 老的是 array 2 | // 新的是 array 3 | 4 | import { ref, h } from '../../dist/tiny-vue.esm.js' 5 | 6 | // 1. 左侧的对比 7 | // (a b) c 8 | // (a b) d e 9 | // const prevChildren = [ 10 | // h('p', { key: 'A' }, 'A'), 11 | // h('p', { key: 'B' }, 'B'), 12 | // h('p', { key: 'C' }, 'C'), 13 | // ] 14 | // const nextChildren = [ 15 | // h('p', { key: 'A' }, 'A'), 16 | // h('p', { key: 'B' }, 'B'), 17 | // h('p', { key: 'D' }, 'D'), 18 | // h('p', { key: 'E' }, 'E'), 19 | // ] 20 | 21 | // 2. 右侧的对比 22 | // a (b c) 23 | // d e (b c) 24 | // const prevChildren = [ 25 | // h('p', { key: 'A' }, 'A'), 26 | // h('p', { key: 'B' }, 'B'), 27 | // h('p', { key: 'C' }, 'C'), 28 | // ] 29 | // const nextChildren = [ 30 | // h('p', { key: 'D' }, 'D'), 31 | // h('p', { key: 'E' }, 'E'), 32 | // h('p', { key: 'B' }, 'B'), 33 | // h('p', { key: 'C' }, 'C'), 34 | // ] 35 | 36 | // 3. 新的比老的长 37 | // 创建新的 38 | // 左侧 39 | // (a b) 40 | // (a b) c 41 | // i = 2, e1 = 1, e2 = 3 42 | // const prevChildren = [h('p', { key: 'A' }, 'A'), h('p', { key: 'B' }, 'B')] 43 | // const nextChildren = [ 44 | // h('p', { key: 'A' }, 'A'), 45 | // h('p', { key: 'B' }, 'B'), 46 | // h('p', { key: 'C' }, 'C'), 47 | // h('p', { key: 'D' }, 'D'), 48 | // ] 49 | 50 | // 右侧 51 | // (a b) 52 | // c (a b) 53 | // i = 0, e1 = -1, e2 = 1 54 | // const prevChildren = [h('p', { key: 'A' }, 'A'), h('p', { key: 'B' }, 'B')] 55 | // const nextChildren = [ 56 | // h('p', { key: 'D' }, 'D'), 57 | // h('p', { key: 'C' }, 'C'), 58 | // h('p', { key: 'A' }, 'A'), 59 | // h('p', { key: 'B' }, 'B'), 60 | // ] 61 | 62 | // 4. 老的比新的长 63 | // 删除老的 64 | // 左侧 65 | // (a b) c 66 | // (a b) 67 | // i = 2, e1 = 2, e2 = 1 68 | // const prevChildren = [ 69 | // h('p', { key: 'A' }, 'A'), 70 | // h('p', { key: 'B' }, 'B'), 71 | // h('p', { key: 'C' }, 'C'), 72 | // ] 73 | // const nextChildren = [h('p', { key: 'A' }, 'A'), h('p', { key: 'B' }, 'B')] 74 | 75 | // 右侧 76 | // a (b c) 77 | // (b c) 78 | // i = 0, e1 = 0, e2 = -1 79 | 80 | // const prevChildren = [ 81 | // h("p", { key: "A" }, "A"), 82 | // h("p", { key: "B" }, "B"), 83 | // h("p", { key: "C" }, "C"), 84 | // ]; 85 | // const nextChildren = [h("p", { key: "B" }, "B"), h("p", { key: "C" }, "C")]; 86 | 87 | // 5. 对比中间的部分 88 | // 删除老的 (在老的里面存在,新的里面不存在) 89 | // 5.1 90 | // a,b,(c,d),f,g 91 | // a,b,(e,c),f,g 92 | // D 节点在新的里面是没有的 - 需要删除掉 93 | // C 节点 props 也发生了变化 94 | // i = 2, e2 = 3, e1 = 3 95 | // const prevChildren = [ 96 | // h('p', { key: 'A' }, 'A'), 97 | // h('p', { key: 'B' }, 'B'), 98 | // h('p', { key: 'C', id: 'c-prev' }, 'C'), 99 | // h('p', { key: 'D' }, 'D'), 100 | // h('p', { key: 'F' }, 'F'), 101 | // h('p', { key: 'G' }, 'G'), 102 | // ] 103 | 104 | // const nextChildren = [ 105 | // h('p', { key: 'A' }, 'A'), 106 | // h('p', { key: 'B' }, 'B'), 107 | // h('p', { key: 'E' }, 'E'), 108 | // h('p', { key: 'C', id: 'c-next' }, 'C'), 109 | // h('p', { key: 'F' }, 'F'), 110 | // h('p', { key: 'G' }, 'G'), 111 | // ] 112 | 113 | // 5.1.1 114 | // a,b,(c,e,d),f,g 115 | // a,b,(e,c),f,g 116 | // 中间部分,老的比新的多, 那么多出来的直接就可以被干掉(优化删除逻辑) 117 | // const prevChildren = [ 118 | // h('p', { key: 'A' }, 'A'), 119 | // h('p', { key: 'B' }, 'B'), 120 | // h('p', { key: 'C', id: 'c-prev' }, 'C'), 121 | // h('p', { key: 'E' }, 'E'), 122 | // h('p', { key: 'D' }, 'D'), 123 | // h('p', { key: 'F' }, 'F'), 124 | // h('p', { key: 'G' }, 'G'), 125 | // ] 126 | 127 | // const nextChildren = [ 128 | // h('p', { key: 'A' }, 'A'), 129 | // h('p', { key: 'B' }, 'B'), 130 | // h('p', { key: 'E' }, 'E'), 131 | // h('p', { key: 'C', id: 'c-next' }, 'C'), 132 | // h('p', { key: 'F' }, 'F'), 133 | // h('p', { key: 'G' }, 'G'), 134 | // ] 135 | 136 | // 2 移动 (节点存在于新的和老的里面,但是位置变了) 137 | 138 | // 2.1 139 | // a,b,(c,d,e),f,g 140 | // a,b,(e,c,d),f,g 141 | // 最长子序列: [1,2] 142 | 143 | // const prevChildren = [ 144 | // h("p", { key: "A" }, "A"), 145 | // h("p", { key: "B" }, "B"), 146 | // h("p", { key: "C" }, "C"), 147 | // h("p", { key: "D" }, "D"), 148 | // h("p", { key: "E" }, "E"), 149 | // h("p", { key: "F" }, "F"), 150 | // h("p", { key: "G" }, "G"), 151 | // ]; 152 | 153 | // const nextChildren = [ 154 | // h("p", { key: "A" }, "A"), 155 | // h("p", { key: "B" }, "B"), 156 | // h("p", { key: "E" }, "E"), 157 | // h("p", { key: "C" }, "C"), 158 | // h("p", { key: "D" }, "D"), 159 | // h("p", { key: "F" }, "F"), 160 | // h("p", { key: "G" }, "G"), 161 | // ]; 162 | 163 | // 3. 创建新的节点 164 | // a,b,(c,e),f,g 165 | // a,b,(e,c,d),f,g 166 | // d 节点在老的节点中不存在,新的里面存在,所以需要创建 167 | // const prevChildren = [ 168 | // h("p", { key: "A" }, "A"), 169 | // h("p", { key: "B" }, "B"), 170 | // h("p", { key: "C" }, "C"), 171 | // h("p", { key: "E" }, "E"), 172 | // h("p", { key: "F" }, "F"), 173 | // h("p", { key: "G" }, "G"), 174 | // ]; 175 | 176 | // const nextChildren = [ 177 | // h("p", { key: "A" }, "A"), 178 | // h("p", { key: "B" }, "B"), 179 | // h("p", { key: "E" }, "E"), 180 | // h("p", { key: "C" }, "C"), 181 | // h("p", { key: "D" }, "D"), 182 | // h("p", { key: "F" }, "F"), 183 | // h("p", { key: "G" }, "G"), 184 | // ]; 185 | 186 | // 综合例子 187 | // a,b,(c,d,e,z),f,g 188 | // a,b,(d,c,y,e),f,g 189 | 190 | // const prevChildren = [ 191 | // h('p', { key: 'A' }, 'A'), 192 | // h('p', { key: 'B' }, 'B'), 193 | // h('p', { key: 'C' }, 'C'), 194 | // h('p', { key: 'D' }, 'D'), 195 | // h('p', { key: 'E' }, 'E'), 196 | // h('p', { key: 'Z' }, 'Z'), 197 | // h('p', { key: 'F' }, 'F'), 198 | // h('p', { key: 'G' }, 'G'), 199 | // ] 200 | 201 | // const nextChildren = [ 202 | // h('p', { key: 'A' }, 'A'), 203 | // h('p', { key: 'B' }, 'B'), 204 | // h('p', { key: 'D' }, 'D'), 205 | // h('p', { key: 'C' }, 'C'), 206 | // h('p', { key: 'Y' }, 'Y'), 207 | // h('p', { key: 'E' }, 'E'), 208 | // h('p', { key: 'F' }, 'F'), 209 | // h('p', { key: 'G' }, 'G'), 210 | // ] 211 | 212 | // fix c 节点应该是 move 而不是删除之后重新创建的 213 | const prevChildren = [ 214 | h('p', { key: 'A' }, 'A'), 215 | h('p', {}, 'C'), 216 | h('p', { key: 'B' }, 'B'), 217 | h('p', { key: 'D' }, 'D'), 218 | ] 219 | 220 | const nextChildren = [ 221 | h('p', { key: 'A' }, 'A'), 222 | h('p', { key: 'B' }, 'B'), 223 | h('p', {}, 'C'), 224 | h('p', { key: 'D' }, 'D'), 225 | ] 226 | 227 | export default { 228 | name: 'ArrayToArray', 229 | setup() { 230 | const isChange = ref(false) 231 | window.isChange = isChange 232 | 233 | return { 234 | isChange, 235 | } 236 | }, 237 | render() { 238 | const self = this 239 | 240 | return self.isChange === true 241 | ? h('div', {}, nextChildren) 242 | : h('div', {}, prevChildren) 243 | }, 244 | } 245 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "DOM", 17 | "es6", 18 | "ES2016" 19 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 20 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 21 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 22 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 23 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 24 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 25 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 26 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 27 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 28 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 29 | 30 | /* Modules */ 31 | "module": "esnext" /* Specify what module code is generated. */, 32 | // "rootDir": "./", /* Specify the root folder within your source files. */ 33 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 34 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 35 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 36 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 37 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 38 | "types": [ 39 | "jest" 40 | ] /* Specify type package names to be included without being referenced in a source file. */, 41 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files */ 43 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 44 | 45 | /* JavaScript Support */ 46 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 47 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 48 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 49 | 50 | /* Emit */ 51 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 52 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 53 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 54 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 55 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 56 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 57 | // "removeComments": true, /* Disable emitting comments. */ 58 | // "noEmit": true, /* Disable emitting files from a compilation. */ 59 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 60 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 61 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 62 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 63 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 64 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 65 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 66 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 67 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 68 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 69 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 70 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 71 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 72 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 77 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 78 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 79 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 80 | 81 | /* Type Checking */ 82 | "strict": true /* Enable all strict type-checking options. */, 83 | "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied `any` type.. */, 84 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 85 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 86 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 87 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 88 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 89 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 90 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 91 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 92 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 93 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 94 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 95 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 96 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 97 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 98 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 99 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 100 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 101 | 102 | /* Completeness */ 103 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 104 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 105 | "paths": { 106 | "@tiny-vue/*": ["./packages/*/src"] 107 | } 108 | }, 109 | "include": ["packages/*/src", "packages/*/__tests__"] 110 | } 111 | -------------------------------------------------------------------------------- /packages/runtime-core/src/renderer.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '@tiny-vue/reactivity' 2 | import { EMPTY_OBJ, ShapeFlags } from '@tiny-vue/shared' 3 | import { createComponentInstance, setupComponent } from './component' 4 | import { shouldUpdateComponent } from './componentUpdateUtils' 5 | import { createAppAPI } from './createApp' 6 | import { queueJobs } from './scheduler' 7 | import { Fragment, Text } from './vnode' 8 | 9 | export function createRenderer(options) { 10 | const { 11 | createElement: hostCreateElement, 12 | patchProp: hostPatchProp, 13 | insert: hostInsert, 14 | remove: hostRemove, 15 | setElementText: hostSetElementText, 16 | } = options 17 | function render(vnode, container) { 18 | patch(null, vnode, container, null, null) 19 | } 20 | 21 | // n1 -> 老的vnode 22 | // n2 -> 新的vnode 23 | function patch(n1, n2, container, parentComponent, anchor) { 24 | const { type, shapeFlag } = n2 25 | switch (type) { 26 | case Fragment: 27 | processFragment(n1, n2, container, parentComponent, anchor) 28 | break 29 | case Text: 30 | processText(n1, n2, container) 31 | break 32 | default: 33 | if (shapeFlag & ShapeFlags.ELEMENT) { 34 | processElement(n1, n2, container, parentComponent, anchor) 35 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 36 | processComponent(n1, n2, container, parentComponent, anchor) 37 | } 38 | break 39 | } 40 | } 41 | function processFragment( 42 | n1: any, 43 | n2: any, 44 | container: any, 45 | parentComponent, 46 | anchor 47 | ) { 48 | mountChildren(n2, container, parentComponent, anchor) 49 | } 50 | 51 | function processText(n1: any, n2: any, container: any) { 52 | const { children } = n2 53 | const textNode = (n2.el = document.createTextNode(children)) 54 | container.append(textNode) 55 | } 56 | 57 | function processElement(n1, n2, container, parentComponent, anchor) { 58 | if (!n1) { 59 | mountElement(n2, container, parentComponent, anchor) 60 | } else { 61 | patchElement(n1, n2, container, parentComponent, anchor) 62 | } 63 | } 64 | 65 | function patchElement(n1, n2, container, parentComponent, anchor) { 66 | console.log('patchElement') 67 | console.log('n1', n1) 68 | console.log('n2', n2) 69 | 70 | const oldProps = n1.props || EMPTY_OBJ 71 | const newProps = n2.props || EMPTY_OBJ 72 | 73 | const el = (n2.el = n1.el) 74 | patchChildren(n1, n2, el, parentComponent, anchor) 75 | patchProps(el, oldProps, newProps) 76 | } 77 | 78 | function patchChildren(n1: any, n2: any, container, parentComponent, anchor) { 79 | const { shapeFlag: prevShapeFlag, children: c1 } = n1 80 | const { shapeFlag, children: c2 } = n2 81 | 82 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 83 | // 如果新的是 文本 84 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 85 | // 1. 把老的 children 清空 86 | unmountChildren(c1) 87 | } 88 | if (c1 !== c2) { 89 | // 2. 设置text 90 | hostSetElementText(container, c2) 91 | } 92 | } else { 93 | // 如果新的是数组 94 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { 95 | hostSetElementText(container, '') 96 | mountChildren(c2, container, parentComponent, anchor) 97 | } else { 98 | patchKeyedChildren(c1, c2, container, parentComponent, anchor) 99 | } 100 | } 101 | } 102 | 103 | function patchKeyedChildren( 104 | c1, 105 | c2, 106 | container, 107 | parentComponent, 108 | parentAnchor 109 | ) { 110 | const l2 = c2.length 111 | let i = 0 112 | let e1 = c1.length - 1 113 | let e2 = l2 - 1 114 | 115 | function isSameVNodeType(n1, n2) { 116 | return n1.type === n2.type && n1.key === n2.key 117 | } 118 | 119 | // 1、左侧的对比 120 | while (i <= e1 && i <= e2) { 121 | const n1 = c1[i] 122 | const n2 = c2[i] 123 | 124 | if (isSameVNodeType(n1, n2)) { 125 | patch(n1, n2, container, parentComponent, parentAnchor) 126 | } else { 127 | break 128 | } 129 | 130 | i++ 131 | } 132 | // 2、右侧的对比 133 | while (i <= e1 && i <= e2) { 134 | const n1 = c1[e1] 135 | const n2 = c2[e2] 136 | 137 | if (isSameVNodeType(n1, n2)) { 138 | patch(n1, n2, container, parentComponent, parentAnchor) 139 | } else { 140 | break 141 | } 142 | 143 | e1-- 144 | e2-- 145 | } 146 | // 3、新的比老的多,创建 147 | if (i > e1) { 148 | if (i <= e2) { 149 | const nextPos = e2 + 1 150 | const anchor = nextPos < l2 ? c2[nextPos].el : null 151 | while (i <= e2) { 152 | patch(null, c2[i], container, parentComponent, anchor) 153 | i++ 154 | } 155 | } 156 | // 老的比新的长,删除 157 | } else if (i > e2) { 158 | while (i <= e1) { 159 | hostRemove(c1[i].el) 160 | i++ 161 | } 162 | } else { 163 | // 中间对比 164 | let s1 = i, 165 | s2 = i 166 | 167 | const toBePatched = e2 - s2 + 1 168 | let patched = 0 169 | 170 | const keyToNewIndexMap = new Map() // 记录新children中间部分的节点index 171 | const newIndexToOldIndexMap = new Array(toBePatched).fill(0) // 创建一个定长的数组 172 | 173 | let moved = false 174 | let maxNewIndexSoFar = 0 175 | 176 | for (let i = s2; i <= e2; i++) { 177 | // 此处的 nextChild/prevChild 是相对于更新前、更新后 178 | const nextChild = c2[i] 179 | keyToNewIndexMap.set(nextChild.key, i) 180 | } 181 | 182 | for (let i = s1; i <= e1; i++) { 183 | const prevChild = c1[i] 184 | 185 | // 如果新节点都被遍历完了,说明剩下的老节点都是不存在了的,直接删除 186 | if (patched >= toBePatched) { 187 | hostRemove(prevChild.el) 188 | continue 189 | } 190 | 191 | let newIndex 192 | // key 不为 null/undefined 193 | if (prevChild.key != null) { 194 | // 根据key去查找该节点在新数据中的位置 195 | newIndex = keyToNewIndexMap.get(prevChild.key) 196 | } else { 197 | // 循环查找该节点在新数据中的位置 198 | for (let j = s2; j <= e2; j++) { 199 | if (isSameVNodeType(prevChild, c2[j])) { 200 | newIndex = j 201 | break 202 | } 203 | } 204 | } 205 | 206 | // 如果新数据中没有该节点了,就把它删除 207 | if (newIndex === undefined) { 208 | hostRemove(prevChild.el) 209 | } else { 210 | if (newIndex >= maxNewIndexSoFar) { 211 | maxNewIndexSoFar = newIndex 212 | } else { 213 | moved = true 214 | } 215 | 216 | // 如果存在,则记录新老位置 217 | // +1是因为i可能为0,但0和数据初始化的值相同,会被认为老节点不存在了 218 | newIndexToOldIndexMap[newIndex - s2] = i + 1 219 | // 对它继续深度遍历 220 | patch(prevChild, c2[newIndex], container, parentComponent, null) 221 | patched++ 222 | } 223 | } 224 | 225 | const increasingNewIndexSequence = moved 226 | ? getSequence(newIndexToOldIndexMap) 227 | : [] 228 | let j = increasingNewIndexSequence.length - 1 229 | 230 | for (let i = toBePatched - 1; i >= 0; i--) { 231 | const nextIndex = i + s2 232 | const nextChild = c2[nextIndex] 233 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null 234 | if (newIndexToOldIndexMap[i] === 0) { 235 | patch(null, nextChild, container, parentComponent, anchor) 236 | } else if (moved) { 237 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 238 | console.log('移动位置') 239 | hostInsert(nextChild.el, container, anchor) 240 | } else { 241 | j-- 242 | } 243 | } 244 | } 245 | } 246 | } 247 | 248 | function unmountChildren(children) { 249 | for (let i = 0; i < children.length; i++) { 250 | const el = children[i].el 251 | hostRemove(el) 252 | } 253 | } 254 | function patchProps(el, oldProps, newProps) { 255 | if (oldProps !== newProps) { 256 | for (const key in newProps) { 257 | const prevProp = oldProps[key] 258 | const nextProp = newProps[key] 259 | 260 | if (prevProp !== nextProp) { 261 | hostPatchProp(el, key, prevProp, nextProp) 262 | } 263 | } 264 | 265 | if (oldProps !== EMPTY_OBJ) { 266 | for (const key in oldProps) { 267 | if (!(key in newProps)) { 268 | hostPatchProp(el, key, oldProps[key], null) 269 | } 270 | } 271 | } 272 | } 273 | } 274 | 275 | function mountElement(vnode, container, parentComponent, anchor) { 276 | // 这个虚拟节点是属于element的 277 | const el = (vnode.el = hostCreateElement(vnode.type)) 278 | 279 | // 对子组件进行解析 280 | const { children, shapeFlag } = vnode 281 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 282 | el.textContent = children 283 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 284 | mountChildren(vnode.children, el, parentComponent, anchor) 285 | } 286 | 287 | // 对props进行解析 288 | const { props } = vnode 289 | for (const key in props) { 290 | const val = props[key] 291 | // 事件注册 292 | hostPatchProp(el, key, null, val) 293 | } 294 | 295 | hostInsert(el, container, anchor) 296 | } 297 | 298 | function mountChildren(children, container, parentComponent, anchor) { 299 | children.forEach((v) => { 300 | patch(null, v, container, parentComponent, anchor) 301 | }) 302 | } 303 | 304 | function processComponent(n1, n2, container, parentComponent, anchor) { 305 | if (!n1) { 306 | mountComponent(n2, container, parentComponent, anchor) 307 | } else { 308 | updateComponent(n1, n2) 309 | } 310 | } 311 | 312 | function updateComponent(n1, n2) { 313 | const instance = (n2.component = n1.component) 314 | if (shouldUpdateComponent(n1, n2)) { 315 | instance.next = n2 316 | instance.update() 317 | } else { 318 | n2.el = n1.el 319 | instance.vnode = n2 320 | } 321 | } 322 | 323 | function mountComponent(initialVNode, container, parentComponent, anchor) { 324 | const instance = (initialVNode.component = createComponentInstance( 325 | initialVNode, 326 | parentComponent 327 | )) 328 | 329 | setupComponent(instance) 330 | setupRenderEffect(instance, initialVNode, container, anchor) 331 | } 332 | 333 | function setupRenderEffect(instance, initialVNode, container, anchor) { 334 | // effect:在第一次执行render的时候收集依赖 335 | instance.update = effect( 336 | () => { 337 | if (!instance.isMounted) { 338 | console.log('init') 339 | const { proxy } = instance 340 | const subTree = (instance.subTree = instance.render.call( 341 | proxy, 342 | proxy 343 | )) 344 | 345 | patch(null, subTree, container, instance, anchor) 346 | 347 | // 要在所有element挂载完之后,把根元素的el挂载到组件的虚拟节点上 348 | initialVNode.el = subTree.el 349 | 350 | instance.isMounted = true 351 | } else { 352 | console.log('update') 353 | // 需要新的虚拟节点 354 | const { next, vnode } = instance 355 | if (next) { 356 | next.el = vnode.el 357 | updateComponentPreRender(instance, next) 358 | } 359 | const { proxy } = instance 360 | const subTree = instance.render.call(proxy, proxy) 361 | const prevSubTree = instance.subTree 362 | instance.subTree = subTree 363 | 364 | patch(prevSubTree, subTree, container, instance, anchor) 365 | } 366 | }, 367 | { 368 | scheduler() { 369 | console.log('doing scheduler') 370 | queueJobs(instance.update) 371 | }, 372 | } 373 | ) 374 | } 375 | 376 | return { 377 | createApp: createAppAPI(render), 378 | } 379 | } 380 | 381 | function updateComponentPreRender(instance, nextVNode) { 382 | instance.vnode = nextVNode 383 | instance.next = null 384 | instance.props = nextVNode.props 385 | } 386 | 387 | function getSequence(arr) { 388 | const p = arr.slice() 389 | const result = [0] 390 | let i, j, u, v, c 391 | const len = arr.length 392 | for (i = 0; i < len; i++) { 393 | const arrI = arr[i] 394 | if (arrI !== 0) { 395 | j = result[result.length - 1] 396 | if (arr[j] < arrI) { 397 | p[i] = j 398 | result.push(i) 399 | continue 400 | } 401 | u = 0 402 | v = result.length - 1 403 | while (u < v) { 404 | c = (u + v) >> 1 405 | if (arr[result[c]] < arrI) { 406 | u = c + 1 407 | } else { 408 | v = c 409 | } 410 | } 411 | if (arrI < arr[result[u]]) { 412 | if (u > 0) { 413 | p[i] = result[u - 1] 414 | } 415 | result[u] = i 416 | } 417 | } 418 | } 419 | u = result.length 420 | v = result[u - 1] 421 | while (u-- > 0) { 422 | result[u] = v 423 | v = p[v] 424 | } 425 | return result 426 | } 427 | -------------------------------------------------------------------------------- /packages/vue/dist/tiny-vue.esm.js: -------------------------------------------------------------------------------- 1 | const Fragment = Symbol('Fragment'); 2 | const Text = Symbol('Text'); 3 | function createVNode(type, props, children) { 4 | const vnode = { 5 | type, 6 | props, 7 | children, 8 | component: null, 9 | shapeFlag: getShapeFlag(type), 10 | key: props && props.key, 11 | el: null, 12 | subTree: {}, 13 | isMounted: false, 14 | }; 15 | // 对children进行标识 16 | if (typeof children === 'string') { 17 | vnode.shapeFlag |= 4 /* TEXT_CHILDREN */; 18 | } 19 | else if (Array.isArray(children)) { 20 | vnode.shapeFlag |= 8 /* ARRAY_CHILDREN */; 21 | } 22 | if (vnode.shapeFlag & 2 /* STATEFUL_COMPONENT */) { 23 | if (typeof children === 'object') { 24 | vnode.shapeFlag |= 16 /* SLOT_CHILDREN */; 25 | } 26 | } 27 | return vnode; 28 | } 29 | function createTextVNode(text) { 30 | return createVNode(Text, {}, text); 31 | } 32 | function getShapeFlag(type) { 33 | return typeof type === 'string' 34 | ? 1 /* ELEMENT */ 35 | : 2 /* STATEFUL_COMPONENT */; 36 | } 37 | 38 | function h(type, props, children) { 39 | return createVNode(type, props, children); 40 | } 41 | 42 | function renderSlots(slots, name, props) { 43 | const slot = slots[name]; 44 | if (slot) { 45 | if (typeof slot === 'function') { 46 | return createVNode(Fragment, {}, slot(props)); 47 | } 48 | } 49 | } 50 | 51 | function toDisplayString(value) { 52 | return String(value); 53 | } 54 | 55 | const extend = Object.assign; 56 | const EMPTY_OBJ = {}; 57 | const isObject = (val) => { 58 | return val !== null && typeof val === 'object'; 59 | }; 60 | const isString = (val) => typeof val === 'string'; 61 | const hasChanged = (newVal, val) => { 62 | return !Object.is(newVal, val); 63 | }; 64 | const isOn = (val) => /^on[A-Z]/.test(val); 65 | const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key); 66 | const camelize = (str) => { 67 | return str.replace(/-(\w)/g, (_, c) => { 68 | return c ? c.toUpperCase() : ''; 69 | }); 70 | }; 71 | const capitalize = (str) => { 72 | return str.charAt(0).toUpperCase() + str.slice(1); 73 | }; 74 | const toHandlerKey = (str) => { 75 | return str ? 'on' + capitalize(str) : ''; 76 | }; 77 | 78 | let activeEffect; 79 | let shouldTrack = false; 80 | class ReactiveEffect { 81 | constructor(fn, scheduler) { 82 | this.deps = []; 83 | this.active = true; 84 | this._fn = fn; 85 | this.scheduler = scheduler; 86 | } 87 | run() { 88 | if (!this.active) { 89 | return this._fn(); 90 | } 91 | // 应该收集 92 | shouldTrack = true; 93 | activeEffect = this; 94 | const result = this._fn(); 95 | // 重置 96 | shouldTrack = false; 97 | return result; 98 | } 99 | stop() { 100 | if (this.active) { 101 | cleanupEffect(this); 102 | if (this.onStop) { 103 | this.onStop(); 104 | } 105 | this.active = false; 106 | } 107 | } 108 | } 109 | function cleanupEffect(effect) { 110 | effect.deps.forEach((dep) => { 111 | dep.delete(effect); 112 | }); 113 | // 把 effect.deps 清空 114 | effect.deps.length = 0; 115 | } 116 | const targetMap = new Map(); 117 | function track(target, key) { 118 | if (!isTracking()) 119 | return; 120 | // target -> key -> dep 121 | let depsMap = targetMap.get(target); 122 | if (!depsMap) { 123 | depsMap = new Map(); 124 | targetMap.set(target, depsMap); 125 | } 126 | let dep = depsMap.get(key); 127 | if (!dep) { 128 | dep = new Set(); 129 | depsMap.set(key, dep); 130 | } 131 | trackEffects(dep); 132 | } 133 | function trackEffects(dep) { 134 | // 看看 dep 之前有没有添加过,添加过的话,那么就不添加了 135 | if (dep.has(activeEffect)) 136 | return; 137 | dep.add(activeEffect); 138 | activeEffect.deps.push(dep); 139 | } 140 | function isTracking() { 141 | return shouldTrack && activeEffect !== undefined; 142 | } 143 | function trigger(target, key) { 144 | const depsMap = targetMap.get(target); 145 | const dep = depsMap.get(key); 146 | triggerEffects(dep); 147 | } 148 | function triggerEffects(dep) { 149 | for (const effect of dep) { 150 | if (effect.scheduler) { 151 | effect.scheduler(); 152 | } 153 | else { 154 | effect.run(); 155 | } 156 | } 157 | } 158 | function effect(fn, options = {}) { 159 | const { scheduler } = options; 160 | const _effect = new ReactiveEffect(fn, scheduler); 161 | extend(_effect, options); 162 | _effect.run(); 163 | const runner = _effect.run.bind(_effect); 164 | runner.effect = _effect; 165 | return runner; 166 | } 167 | 168 | const get = createGetter(); 169 | const set = createSetter(); 170 | const readonlyGet = createGetter(true); 171 | const shallowReadonlyGet = createGetter(true, true); 172 | const mutableHandlers = { 173 | get, 174 | set, 175 | }; 176 | const readonlyHandlers = { 177 | get: readonlyGet, 178 | set(target, key, value) { 179 | console.warn(`key ${key} can not be set, because target is readonly`, target); 180 | return true; 181 | }, 182 | }; 183 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 184 | get: shallowReadonlyGet, 185 | }); 186 | function createGetter(isReadonly = false, isShallow = false) { 187 | return function (target, key) { 188 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 189 | return !isReadonly; 190 | } 191 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 192 | return isReadonly; 193 | } 194 | const res = Reflect.get(target, key); 195 | if (isShallow) { 196 | return res; 197 | } 198 | if (isObject(res)) { 199 | return isReadonly ? readonly(res) : reactive(res); 200 | } 201 | if (!isReadonly) { 202 | track(target, key); 203 | } 204 | return res; 205 | }; 206 | } 207 | function createSetter() { 208 | return function (target, key, value) { 209 | const res = Reflect.set(target, key, value); 210 | trigger(target, key); 211 | return res; 212 | }; 213 | } 214 | 215 | function reactive(raw) { 216 | return createActiveObject(raw, mutableHandlers); 217 | } 218 | function readonly(raw) { 219 | return createActiveObject(raw, readonlyHandlers); 220 | } 221 | function shallowReadonly(raw) { 222 | return createActiveObject(raw, shallowReadonlyHandlers); 223 | } 224 | function createActiveObject(raw, baseHandlers) { 225 | if (!isObject(raw)) { 226 | console.warn(`target ${raw} 必须是一个对象`); 227 | return raw; 228 | } 229 | return new Proxy(raw, baseHandlers); 230 | } 231 | function isReactive(raw) { 232 | return !!raw["__v_isReactive" /* IS_REACTIVE */]; 233 | } 234 | function isReadonly(raw) { 235 | return !!raw["__v_isReadonly" /* IS_READONLY */]; 236 | } 237 | function isProxy(value) { 238 | return isReactive(value) || isReadonly(value); 239 | } 240 | 241 | class RefImpl { 242 | constructor(raw) { 243 | this.__v_isRef = true; 244 | this._rawValue = raw; 245 | this._value = convert(raw); 246 | this.dep = new Set(); 247 | } 248 | get value() { 249 | // 收集依赖 250 | trackRefEffects(this); 251 | return this._value; 252 | } 253 | set value(newValue) { 254 | // 一定是先修改了值的,触发依赖 255 | if (hasChanged(newValue, this._rawValue)) { 256 | this._rawValue = newValue; 257 | this._value = convert(newValue); 258 | triggerEffects(this.dep); 259 | } 260 | } 261 | } 262 | function trackRefEffects(ref) { 263 | if (isTracking()) { 264 | trackEffects(ref.dep); 265 | } 266 | } 267 | function convert(value) { 268 | return isObject(value) ? reactive(value) : value; 269 | } 270 | function ref(raw) { 271 | return new RefImpl(raw); 272 | } 273 | function isRef(value) { 274 | return !!value.__v_isRef; 275 | } 276 | function unRef(ref) { 277 | return isRef(ref) ? ref.value : ref; 278 | } 279 | function proxyRefs(objectWidthRefs) { 280 | return new Proxy(objectWidthRefs, { 281 | get(target, key) { 282 | return unRef(Reflect.get(target, key)); 283 | }, 284 | set(target, key, value) { 285 | if (isRef(target[key]) && !isRef(value)) { 286 | return (target[key].value = value); 287 | } 288 | else { 289 | return Reflect.set(target, key, value); 290 | } 291 | }, 292 | }); 293 | } 294 | 295 | class ComputedRefImpl { 296 | constructor(getter) { 297 | this._dirty = true; 298 | this._getter = getter; 299 | this._effect = new ReactiveEffect(getter, () => { 300 | this._dirty = true; 301 | }); 302 | } 303 | get value() { 304 | if (this._dirty) { 305 | this._dirty = false; 306 | this._value = this._effect.run(); 307 | } 308 | return this._value; 309 | } 310 | } 311 | function computed(getter) { 312 | return new ComputedRefImpl(getter); 313 | } 314 | 315 | // 执行子组件的emit方法,触发父组件props中的注册事件 316 | function emit(instance, event, ...args) { 317 | console.log('emit', event); 318 | const { props } = instance; 319 | // 将传入的event名称转化为props中的注册事件名 320 | const handlerKeyName = toHandlerKey(camelize(event)); 321 | const handler = props[handlerKeyName]; 322 | handler && handler(...args); 323 | } 324 | 325 | function initProps(instance, rawProps) { 326 | instance.props = rawProps || {}; 327 | } 328 | 329 | const publicPropertiesMap = { 330 | $el: (i) => i.vnode.el, 331 | $slots: (i) => i.slots, 332 | $props: (i) => i.props, 333 | }; 334 | const PublicInstanceProxyHandlers = { 335 | get({ _: instance }, key) { 336 | const { setupState, props } = instance; 337 | if (hasOwn(setupState, key)) { 338 | return setupState[key]; 339 | } 340 | else if (hasOwn(props, key)) { 341 | return props[key]; 342 | } 343 | const publicGetter = publicPropertiesMap[key]; 344 | if (publicGetter) { 345 | return publicGetter(instance); 346 | } 347 | }, 348 | }; 349 | 350 | function initSlots(instance, children) { 351 | // 判断是否具名插槽 352 | const { vnode } = instance; 353 | if (vnode.shapeFlag & 16 /* SLOT_CHILDREN */) { 354 | normalizeObjectSlots(children, instance.slots); 355 | } 356 | } 357 | // 处理具名插槽 358 | function normalizeObjectSlots(children, slots) { 359 | for (const key in children) { 360 | const value = children[key]; 361 | // 使用function是为了向插槽传参 362 | slots[key] = (props) => normalizeSlotValue(value(props)); 363 | } 364 | } 365 | function normalizeSlotValue(value) { 366 | // 判断children是否数组的目的:让用户写slot时,既可以填写数组,也可以填写单个 367 | return Array.isArray(value) ? value : [value]; 368 | } 369 | 370 | function createComponentInstance(vnode, parent) { 371 | const component = { 372 | vnode, 373 | type: vnode.type, 374 | next: null, 375 | setupState: {}, 376 | props: {}, 377 | emit: () => { }, 378 | slots: {}, 379 | provides: parent ? parent.provides : {}, 380 | parent, 381 | }; 382 | // bind的作用:默认emit的第一个参数是组件实例。 383 | // 用户在调用emit时就可以只传事件名称,不用传父组件实例了。 384 | component.emit = emit.bind(null, component); 385 | return component; 386 | } 387 | function setupComponent(instance) { 388 | initProps(instance, instance.vnode.props); 389 | initSlots(instance, instance.vnode.children); 390 | setupStatefulComponent(instance); 391 | } 392 | function setupStatefulComponent(instance) { 393 | const Component = instance.type; 394 | // 对render中的this指向进行代理 395 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 396 | const { setup } = Component; 397 | if (setup) { 398 | setCurrentInstance(instance); 399 | const setupResult = setup(shallowReadonly(instance.props), { 400 | emit: instance.emit, 401 | }); 402 | // getCurrentInstance 会在 setup 里面被执行,执行完之后就把全局变量重置回 null 403 | setCurrentInstance(null); 404 | handleSetupResult(instance, setupResult); 405 | } 406 | } 407 | function handleSetupResult(instance, setupResult) { 408 | if (typeof setupResult === 'object') { 409 | // 此处的proxyRefs是在为了render中自动解构ref的.value 410 | instance.setupState = proxyRefs(setupResult); 411 | } 412 | finishComponentSetup(instance); 413 | } 414 | function finishComponentSetup(instance) { 415 | const Component = instance.type; 416 | if (compiler && !Component.render) { 417 | if (Component.template) { 418 | Component.render = compiler(Component.template); 419 | } 420 | } 421 | instance.render = Component.render; 422 | } 423 | let currentInstance = null; 424 | function getCurrentInstance() { 425 | return currentInstance; 426 | } 427 | function setCurrentInstance(value) { 428 | currentInstance = value; 429 | } 430 | let compiler; 431 | function registerRuntimeCompiler(_compiler) { 432 | compiler = _compiler; 433 | } 434 | 435 | function provide(key, value) { 436 | const currentInstance = getCurrentInstance(); 437 | if (currentInstance) { 438 | let { provides } = currentInstance; 439 | const parentProvides = currentInstance.parent.provides; 440 | // 如果是第一次在当前组件provide,则把父组件的provide挂到当前组件provides的原型链上 441 | if (provides === parentProvides) { 442 | provides = currentInstance.provides = Object.create(parentProvides); 443 | } 444 | provides[key] = value; 445 | } 446 | } 447 | function inject(key, defaultValue) { 448 | const currentInstance = getCurrentInstance(); 449 | if (currentInstance) { 450 | const parentProvides = currentInstance.parent.provides; 451 | if (key in parentProvides) { 452 | return parentProvides[key]; 453 | } 454 | else if (defaultValue) { 455 | if (typeof defaultValue === 'function') { 456 | return defaultValue(); 457 | } 458 | return defaultValue; 459 | } 460 | } 461 | } 462 | 463 | function shouldUpdateComponent(prevVNode, nextVNode) { 464 | const { props: prevProps } = prevVNode; 465 | const { props: nextProps } = nextVNode; 466 | for (const key in nextProps) { 467 | if (nextProps[key] !== prevProps[key]) { 468 | return true; 469 | } 470 | } 471 | return false; 472 | } 473 | 474 | function createAppAPI(render) { 475 | return function createApp(rootComponent) { 476 | return { 477 | mount(rootContainer) { 478 | const _rootContainer = typeof rootContainer === 'string' 479 | ? document.querySelector(rootContainer) 480 | : rootContainer; 481 | const vnode = createVNode(rootComponent); 482 | render(vnode, _rootContainer); 483 | }, 484 | }; 485 | }; 486 | } 487 | 488 | const queue = []; 489 | const activePreFlushCbs = []; 490 | let isFlushPending = false; 491 | const p = Promise.resolve(); 492 | function nextTick(fn) { 493 | return fn ? p.then(fn) : p; 494 | } 495 | function queueJobs(job) { 496 | if (!queue.includes(job)) { 497 | queue.push(job); 498 | } 499 | queueFlush(); 500 | } 501 | function queueFlush() { 502 | if (isFlushPending) 503 | return; 504 | isFlushPending = true; 505 | nextTick(flushJob); 506 | } 507 | function flushJob() { 508 | isFlushPending = false; 509 | flushPreFlushCbs(); 510 | let job; 511 | while ((job = queue.shift())) { 512 | job && job(); 513 | } 514 | } 515 | function flushPreFlushCbs() { 516 | for (let i = 0; i < activePreFlushCbs.length; i++) { 517 | activePreFlushCbs[i](); 518 | } 519 | } 520 | 521 | function createRenderer(options) { 522 | const { createElement: hostCreateElement, patchProp: hostPatchProp, insert: hostInsert, remove: hostRemove, setElementText: hostSetElementText, } = options; 523 | function render(vnode, container) { 524 | patch(null, vnode, container, null, null); 525 | } 526 | // n1 -> 老的vnode 527 | // n2 -> 新的vnode 528 | function patch(n1, n2, container, parentComponent, anchor) { 529 | const { type, shapeFlag } = n2; 530 | switch (type) { 531 | case Fragment: 532 | processFragment(n1, n2, container, parentComponent, anchor); 533 | break; 534 | case Text: 535 | processText(n1, n2, container); 536 | break; 537 | default: 538 | if (shapeFlag & 1 /* ELEMENT */) { 539 | processElement(n1, n2, container, parentComponent, anchor); 540 | } 541 | else if (shapeFlag & 2 /* STATEFUL_COMPONENT */) { 542 | processComponent(n1, n2, container, parentComponent, anchor); 543 | } 544 | break; 545 | } 546 | } 547 | function processFragment(n1, n2, container, parentComponent, anchor) { 548 | mountChildren(n2, container, parentComponent, anchor); 549 | } 550 | function processText(n1, n2, container) { 551 | const { children } = n2; 552 | const textNode = (n2.el = document.createTextNode(children)); 553 | container.append(textNode); 554 | } 555 | function processElement(n1, n2, container, parentComponent, anchor) { 556 | if (!n1) { 557 | mountElement(n2, container, parentComponent, anchor); 558 | } 559 | else { 560 | patchElement(n1, n2, container, parentComponent, anchor); 561 | } 562 | } 563 | function patchElement(n1, n2, container, parentComponent, anchor) { 564 | console.log('patchElement'); 565 | console.log('n1', n1); 566 | console.log('n2', n2); 567 | const oldProps = n1.props || EMPTY_OBJ; 568 | const newProps = n2.props || EMPTY_OBJ; 569 | const el = (n2.el = n1.el); 570 | patchChildren(n1, n2, el, parentComponent, anchor); 571 | patchProps(el, oldProps, newProps); 572 | } 573 | function patchChildren(n1, n2, container, parentComponent, anchor) { 574 | const { shapeFlag: prevShapeFlag, children: c1 } = n1; 575 | const { shapeFlag, children: c2 } = n2; 576 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 577 | // 如果新的是 文本 578 | if (prevShapeFlag & 8 /* ARRAY_CHILDREN */) { 579 | // 1. 把老的 children 清空 580 | unmountChildren(c1); 581 | } 582 | if (c1 !== c2) { 583 | // 2. 设置text 584 | hostSetElementText(container, c2); 585 | } 586 | } 587 | else { 588 | // 如果新的是数组 589 | if (prevShapeFlag & 4 /* TEXT_CHILDREN */) { 590 | hostSetElementText(container, ''); 591 | mountChildren(c2, container, parentComponent, anchor); 592 | } 593 | else { 594 | patchKeyedChildren(c1, c2, container, parentComponent, anchor); 595 | } 596 | } 597 | } 598 | function patchKeyedChildren(c1, c2, container, parentComponent, parentAnchor) { 599 | const l2 = c2.length; 600 | let i = 0; 601 | let e1 = c1.length - 1; 602 | let e2 = l2 - 1; 603 | function isSameVNodeType(n1, n2) { 604 | return n1.type === n2.type && n1.key === n2.key; 605 | } 606 | // 1、左侧的对比 607 | while (i <= e1 && i <= e2) { 608 | const n1 = c1[i]; 609 | const n2 = c2[i]; 610 | if (isSameVNodeType(n1, n2)) { 611 | patch(n1, n2, container, parentComponent, parentAnchor); 612 | } 613 | else { 614 | break; 615 | } 616 | i++; 617 | } 618 | // 2、右侧的对比 619 | while (i <= e1 && i <= e2) { 620 | const n1 = c1[e1]; 621 | const n2 = c2[e2]; 622 | if (isSameVNodeType(n1, n2)) { 623 | patch(n1, n2, container, parentComponent, parentAnchor); 624 | } 625 | else { 626 | break; 627 | } 628 | e1--; 629 | e2--; 630 | } 631 | // 3、新的比老的多,创建 632 | if (i > e1) { 633 | if (i <= e2) { 634 | const nextPos = e2 + 1; 635 | const anchor = nextPos < l2 ? c2[nextPos].el : null; 636 | while (i <= e2) { 637 | patch(null, c2[i], container, parentComponent, anchor); 638 | i++; 639 | } 640 | } 641 | // 老的比新的长,删除 642 | } 643 | else if (i > e2) { 644 | while (i <= e1) { 645 | hostRemove(c1[i].el); 646 | i++; 647 | } 648 | } 649 | else { 650 | // 中间对比 651 | let s1 = i, s2 = i; 652 | const toBePatched = e2 - s2 + 1; 653 | let patched = 0; 654 | const keyToNewIndexMap = new Map(); // 记录新children中间部分的节点index 655 | const newIndexToOldIndexMap = new Array(toBePatched).fill(0); // 创建一个定长的数组 656 | let moved = false; 657 | let maxNewIndexSoFar = 0; 658 | for (let i = s2; i <= e2; i++) { 659 | // 此处的 nextChild/prevChild 是相对于更新前、更新后 660 | const nextChild = c2[i]; 661 | keyToNewIndexMap.set(nextChild.key, i); 662 | } 663 | for (let i = s1; i <= e1; i++) { 664 | const prevChild = c1[i]; 665 | // 如果新节点都被遍历完了,说明剩下的老节点都是不存在了的,直接删除 666 | if (patched >= toBePatched) { 667 | hostRemove(prevChild.el); 668 | continue; 669 | } 670 | let newIndex; 671 | // key 不为 null/undefined 672 | if (prevChild.key != null) { 673 | // 根据key去查找该节点在新数据中的位置 674 | newIndex = keyToNewIndexMap.get(prevChild.key); 675 | } 676 | else { 677 | // 循环查找该节点在新数据中的位置 678 | for (let j = s2; j <= e2; j++) { 679 | if (isSameVNodeType(prevChild, c2[j])) { 680 | newIndex = j; 681 | break; 682 | } 683 | } 684 | } 685 | // 如果新数据中没有该节点了,就把它删除 686 | if (newIndex === undefined) { 687 | hostRemove(prevChild.el); 688 | } 689 | else { 690 | if (newIndex >= maxNewIndexSoFar) { 691 | maxNewIndexSoFar = newIndex; 692 | } 693 | else { 694 | moved = true; 695 | } 696 | // 如果存在,则记录新老位置 697 | // +1是因为i可能为0,但0和数据初始化的值相同,会被认为老节点不存在了 698 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 699 | // 对它继续深度遍历 700 | patch(prevChild, c2[newIndex], container, parentComponent, null); 701 | patched++; 702 | } 703 | } 704 | const increasingNewIndexSequence = moved 705 | ? getSequence(newIndexToOldIndexMap) 706 | : []; 707 | let j = increasingNewIndexSequence.length - 1; 708 | for (let i = toBePatched - 1; i >= 0; i--) { 709 | const nextIndex = i + s2; 710 | const nextChild = c2[nextIndex]; 711 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null; 712 | if (newIndexToOldIndexMap[i] === 0) { 713 | patch(null, nextChild, container, parentComponent, anchor); 714 | } 715 | else if (moved) { 716 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 717 | console.log('移动位置'); 718 | hostInsert(nextChild.el, container, anchor); 719 | } 720 | else { 721 | j--; 722 | } 723 | } 724 | } 725 | } 726 | } 727 | function unmountChildren(children) { 728 | for (let i = 0; i < children.length; i++) { 729 | const el = children[i].el; 730 | hostRemove(el); 731 | } 732 | } 733 | function patchProps(el, oldProps, newProps) { 734 | if (oldProps !== newProps) { 735 | for (const key in newProps) { 736 | const prevProp = oldProps[key]; 737 | const nextProp = newProps[key]; 738 | if (prevProp !== nextProp) { 739 | hostPatchProp(el, key, prevProp, nextProp); 740 | } 741 | } 742 | if (oldProps !== EMPTY_OBJ) { 743 | for (const key in oldProps) { 744 | if (!(key in newProps)) { 745 | hostPatchProp(el, key, oldProps[key], null); 746 | } 747 | } 748 | } 749 | } 750 | } 751 | function mountElement(vnode, container, parentComponent, anchor) { 752 | // 这个虚拟节点是属于element的 753 | const el = (vnode.el = hostCreateElement(vnode.type)); 754 | // 对子组件进行解析 755 | const { children, shapeFlag } = vnode; 756 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 757 | el.textContent = children; 758 | } 759 | else if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 760 | mountChildren(vnode.children, el, parentComponent, anchor); 761 | } 762 | // 对props进行解析 763 | const { props } = vnode; 764 | for (const key in props) { 765 | const val = props[key]; 766 | // 事件注册 767 | hostPatchProp(el, key, null, val); 768 | } 769 | hostInsert(el, container, anchor); 770 | } 771 | function mountChildren(children, container, parentComponent, anchor) { 772 | children.forEach((v) => { 773 | patch(null, v, container, parentComponent, anchor); 774 | }); 775 | } 776 | function processComponent(n1, n2, container, parentComponent, anchor) { 777 | if (!n1) { 778 | mountComponent(n2, container, parentComponent, anchor); 779 | } 780 | else { 781 | updateComponent(n1, n2); 782 | } 783 | } 784 | function updateComponent(n1, n2) { 785 | const instance = (n2.component = n1.component); 786 | if (shouldUpdateComponent(n1, n2)) { 787 | instance.next = n2; 788 | instance.update(); 789 | } 790 | else { 791 | n2.el = n1.el; 792 | instance.vnode = n2; 793 | } 794 | } 795 | function mountComponent(initialVNode, container, parentComponent, anchor) { 796 | const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent)); 797 | setupComponent(instance); 798 | setupRenderEffect(instance, initialVNode, container, anchor); 799 | } 800 | function setupRenderEffect(instance, initialVNode, container, anchor) { 801 | // effect:在第一次执行render的时候收集依赖 802 | instance.update = effect(() => { 803 | if (!instance.isMounted) { 804 | console.log('init'); 805 | const { proxy } = instance; 806 | const subTree = (instance.subTree = instance.render.call(proxy, proxy)); 807 | patch(null, subTree, container, instance, anchor); 808 | // 要在所有element挂载完之后,把根元素的el挂载到组件的虚拟节点上 809 | initialVNode.el = subTree.el; 810 | instance.isMounted = true; 811 | } 812 | else { 813 | console.log('update'); 814 | // 需要新的虚拟节点 815 | const { next, vnode } = instance; 816 | if (next) { 817 | next.el = vnode.el; 818 | updateComponentPreRender(instance, next); 819 | } 820 | const { proxy } = instance; 821 | const subTree = instance.render.call(proxy, proxy); 822 | const prevSubTree = instance.subTree; 823 | instance.subTree = subTree; 824 | patch(prevSubTree, subTree, container, instance, anchor); 825 | } 826 | }, { 827 | scheduler() { 828 | console.log('doing scheduler'); 829 | queueJobs(instance.update); 830 | }, 831 | }); 832 | } 833 | return { 834 | createApp: createAppAPI(render), 835 | }; 836 | } 837 | function updateComponentPreRender(instance, nextVNode) { 838 | instance.vnode = nextVNode; 839 | instance.next = null; 840 | instance.props = nextVNode.props; 841 | } 842 | function getSequence(arr) { 843 | const p = arr.slice(); 844 | const result = [0]; 845 | let i, j, u, v, c; 846 | const len = arr.length; 847 | for (i = 0; i < len; i++) { 848 | const arrI = arr[i]; 849 | if (arrI !== 0) { 850 | j = result[result.length - 1]; 851 | if (arr[j] < arrI) { 852 | p[i] = j; 853 | result.push(i); 854 | continue; 855 | } 856 | u = 0; 857 | v = result.length - 1; 858 | while (u < v) { 859 | c = (u + v) >> 1; 860 | if (arr[result[c]] < arrI) { 861 | u = c + 1; 862 | } 863 | else { 864 | v = c; 865 | } 866 | } 867 | if (arrI < arr[result[u]]) { 868 | if (u > 0) { 869 | p[i] = result[u - 1]; 870 | } 871 | result[u] = i; 872 | } 873 | } 874 | } 875 | u = result.length; 876 | v = result[u - 1]; 877 | while (u-- > 0) { 878 | result[u] = v; 879 | v = p[v]; 880 | } 881 | return result; 882 | } 883 | 884 | function createElement(type) { 885 | return document.createElement(type); 886 | } 887 | function patchProp(el, key, prevVal, nextVal) { 888 | if (isOn(key)) { 889 | const event = key.slice(2).toLowerCase(); 890 | el.addEventListener(event, nextVal); 891 | } 892 | else { 893 | if (nextVal === undefined || nextVal === null) { 894 | el.removeAttribute(key); 895 | } 896 | else { 897 | el.setAttribute(key, nextVal); 898 | } 899 | } 900 | } 901 | function insert(child, parent, anchor) { 902 | parent.insertBefore(child, anchor || null); 903 | } 904 | function remove(child) { 905 | const parent = child.parentNode; 906 | if (parent) { 907 | parent.removeChild(child); 908 | } 909 | } 910 | function setElementText(el, text) { 911 | el.textContent = text; 912 | } 913 | const renderer = createRenderer({ 914 | createElement, 915 | patchProp, 916 | insert, 917 | remove, 918 | setElementText, 919 | }); 920 | function createApp(...args) { 921 | return renderer.createApp(...args); 922 | } 923 | 924 | var runtimeDom = /*#__PURE__*/Object.freeze({ 925 | __proto__: null, 926 | createApp: createApp, 927 | h: h, 928 | renderSlots: renderSlots, 929 | createTextVNode: createTextVNode, 930 | createElementVNode: createVNode, 931 | getCurrentInstance: getCurrentInstance, 932 | registerRuntimeCompiler: registerRuntimeCompiler, 933 | provide: provide, 934 | inject: inject, 935 | createRenderer: createRenderer, 936 | nextTick: nextTick, 937 | toDisplayString: toDisplayString, 938 | ref: ref, 939 | isRef: isRef, 940 | unRef: unRef, 941 | proxyRefs: proxyRefs, 942 | effect: effect, 943 | ReactiveEffect: ReactiveEffect, 944 | reactive: reactive, 945 | readonly: readonly, 946 | shallowReadonly: shallowReadonly, 947 | isReactive: isReactive, 948 | isReadonly: isReadonly, 949 | isProxy: isProxy, 950 | computed: computed 951 | }); 952 | 953 | const TO_DISPLAY_STRING = Symbol('toDisplayString'); 954 | const CREATE_ELEMENT_VNODE = Symbol('createElementVNode'); 955 | const helperMapName = { 956 | [TO_DISPLAY_STRING]: 'toDisplayString', 957 | [CREATE_ELEMENT_VNODE]: 'createElementVNode', 958 | }; 959 | 960 | var NodeTypes; 961 | (function (NodeTypes) { 962 | NodeTypes[NodeTypes["INTERPOLATION"] = 0] = "INTERPOLATION"; 963 | NodeTypes[NodeTypes["SIMPLE_EXPRESSION"] = 1] = "SIMPLE_EXPRESSION"; 964 | NodeTypes[NodeTypes["ELEMENT"] = 2] = "ELEMENT"; 965 | NodeTypes[NodeTypes["TEXT"] = 3] = "TEXT"; 966 | NodeTypes[NodeTypes["ROOT"] = 4] = "ROOT"; 967 | NodeTypes[NodeTypes["COMPOUND_EXPRESSION"] = 5] = "COMPOUND_EXPRESSION"; 968 | })(NodeTypes || (NodeTypes = {})); 969 | function createVNodeCall(context, tag, props, children) { 970 | context.helper(CREATE_ELEMENT_VNODE); 971 | return { 972 | type: NodeTypes.ELEMENT, 973 | tag, 974 | props, 975 | children, 976 | }; 977 | } 978 | 979 | function generate(ast) { 980 | const context = createCodegenContext(); 981 | const { push } = context; 982 | genFunctionPreamble(ast, context); 983 | const functionName = 'render'; 984 | const args = ['_ctx', '_cache']; 985 | const signature = args.join(', '); 986 | push(`function ${functionName}(${signature}){`); 987 | push('return '); 988 | genNode(ast.codegenNode, context); 989 | push('}'); 990 | return { 991 | code: context.code, 992 | }; 993 | } 994 | function genFunctionPreamble(ast, context) { 995 | const { push, helper } = context; 996 | const VueBinging = 'Vue'; 997 | const aliasHelper = (s) => `${helper(s)}:_${helper(s)}`; 998 | if (ast.helpers.length > 0) { 999 | push(`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinging}`); 1000 | } 1001 | push('\n'); 1002 | push('return '); 1003 | } 1004 | function genNode(node, context) { 1005 | switch (node.type) { 1006 | case NodeTypes.TEXT: 1007 | genText(node, context); 1008 | break; 1009 | case NodeTypes.INTERPOLATION: 1010 | genInterpolation(node, context); 1011 | break; 1012 | case NodeTypes.SIMPLE_EXPRESSION: 1013 | genExpression(node, context); 1014 | break; 1015 | case NodeTypes.SIMPLE_EXPRESSION: 1016 | genExpression(node, context); 1017 | break; 1018 | case NodeTypes.ELEMENT: 1019 | genElement(node, context); 1020 | break; 1021 | case NodeTypes.COMPOUND_EXPRESSION: 1022 | genCompoundExpression(node, context); 1023 | break; 1024 | } 1025 | } 1026 | function genText(node, context) { 1027 | const { push } = context; 1028 | push(`'${node.content}'`); 1029 | } 1030 | function genElement(node, context) { 1031 | const { push, helper } = context; 1032 | const { tag, children, props } = node; 1033 | push(`_${helper(CREATE_ELEMENT_VNODE)}(`); 1034 | genNodeList(genNullable([tag, props, children]), context); 1035 | push(')'); 1036 | } 1037 | function genNodeList(nodes, context) { 1038 | const { push } = context; 1039 | for (let i = 0; i < nodes.length; i++) { 1040 | const node = nodes[i]; 1041 | if (isString(node)) { 1042 | push(node); 1043 | } 1044 | else { 1045 | genNode(node, context); 1046 | } 1047 | if (i < nodes.length - 1) { 1048 | push(', '); 1049 | } 1050 | } 1051 | } 1052 | function genNullable(args) { 1053 | return args.map((arg) => arg || 'null'); 1054 | } 1055 | function genCompoundExpression(node, context) { 1056 | const { push } = context; 1057 | const children = node.children; 1058 | for (let i = 0; i < children.length; i++) { 1059 | const child = children[i]; 1060 | if (isString(child)) { 1061 | push(child); 1062 | } 1063 | else { 1064 | genNode(child, context); 1065 | } 1066 | } 1067 | } 1068 | function createCodegenContext() { 1069 | const context = { 1070 | code: '', 1071 | push(source) { 1072 | context.code += source; 1073 | }, 1074 | helper(key) { 1075 | return `${helperMapName[key]}`; 1076 | }, 1077 | }; 1078 | return context; 1079 | } 1080 | function genInterpolation(node, context) { 1081 | const { push, helper } = context; 1082 | push(`_${helper(TO_DISPLAY_STRING)}(`); 1083 | genNode(node.content, context); 1084 | push(')'); 1085 | } 1086 | function genExpression(node, context) { 1087 | const { push } = context; 1088 | push(`${node.content}`); 1089 | } 1090 | 1091 | function baseParse(content) { 1092 | const context = createParserContext(content); 1093 | return createRoot(parseChildren(context, [])); 1094 | } 1095 | function createParserContext(content) { 1096 | return { 1097 | source: content, 1098 | }; 1099 | } 1100 | function createRoot(children) { 1101 | return { 1102 | children, 1103 | type: NodeTypes.ROOT, 1104 | }; 1105 | } 1106 | function parseChildren(context, ancestors) { 1107 | const nodes = []; 1108 | while (!isEnd(context, ancestors)) { 1109 | let node; 1110 | const s = context.source; 1111 | if (s.startsWith('{{')) { 1112 | node = parseInterpolation(context); 1113 | } 1114 | else if (s[0] === '<') { 1115 | if (/[a-z]/i.test(s[1])) { 1116 | node = parseElement(context, ancestors); 1117 | } 1118 | } 1119 | if (!node) { 1120 | node = parseText(context); 1121 | } 1122 | nodes.push(node); 1123 | } 1124 | return nodes; 1125 | } 1126 | function isEnd(context, ancestors) { 1127 | const s = context.source; 1128 | // 1、遇到结束标签 1129 | for (let i = ancestors.length - 1; i >= 0; i--) { 1130 | const tag = ancestors[i].tag; 1131 | if (startsWithEndTagOpen(s, tag)) { 1132 | return true; 1133 | } 1134 | } 1135 | // 2、source没有值的时候 1136 | return !s; 1137 | } 1138 | function parseInterpolation(context) { 1139 | const openDelimiter = '{{'; 1140 | const closeDelimiter = '}}'; 1141 | const closeIndex = context.source.indexOf(closeDelimiter); 1142 | advanceBy(context, openDelimiter.length); 1143 | const rawContentLength = closeIndex - openDelimiter.length; 1144 | const rawContent = parseTextData(context, rawContentLength); 1145 | const content = rawContent.trim(); 1146 | advanceBy(context, closeDelimiter.length); 1147 | return { 1148 | type: NodeTypes.INTERPOLATION, 1149 | content: { 1150 | type: NodeTypes.SIMPLE_EXPRESSION, 1151 | content: content, 1152 | }, 1153 | }; 1154 | } 1155 | function advanceBy(context, length) { 1156 | context.source = context.source.slice(length); 1157 | } 1158 | function parseElement(context, ancestors) { 1159 | const element = parseTag(context, 0 /* START */); 1160 | ancestors.push(element); 1161 | element.children = parseChildren(context, ancestors); 1162 | ancestors.pop(); 1163 | if (startsWithEndTagOpen(context.source, element.tag)) { 1164 | parseTag(context, 1 /* END */); 1165 | } 1166 | else { 1167 | throw new Error(`缺少结束标签${element.tag}`); 1168 | } 1169 | return element; 1170 | } 1171 | function startsWithEndTagOpen(source, tag) { 1172 | return (source.startsWith(' index) { 1195 | endIndex = index; 1196 | } 1197 | } 1198 | const content = parseTextData(context, endIndex); 1199 | return { 1200 | type: NodeTypes.TEXT, 1201 | content, 1202 | }; 1203 | } 1204 | function parseTextData(context, length) { 1205 | const content = context.source.slice(0, length); 1206 | advanceBy(context, content.length); 1207 | return content; 1208 | } 1209 | 1210 | function transform(root, options = {}) { 1211 | const context = createTransformContext(root, options); 1212 | // 遍历-深度优先搜索 1213 | traverseNode(root, context); 1214 | createRootCodegen(root); 1215 | root.helpers = [...context.helpers.keys()]; 1216 | } 1217 | function traverseNode(node, context) { 1218 | const nodeTransforms = context.nodeTransforms; 1219 | const exitFns = []; 1220 | if (nodeTransforms) { 1221 | for (let i = 0; i < nodeTransforms.length; i++) { 1222 | const transform = context.nodeTransforms[i]; 1223 | const onExit = transform(node, context); 1224 | if (onExit) 1225 | exitFns.push(onExit); 1226 | } 1227 | } 1228 | switch (node.type) { 1229 | case NodeTypes.INTERPOLATION: 1230 | context.helper(TO_DISPLAY_STRING); 1231 | break; 1232 | case NodeTypes.ROOT: 1233 | case NodeTypes.ELEMENT: 1234 | traverseChildren(node, context); 1235 | break; 1236 | } 1237 | let i = exitFns.length; 1238 | while (i--) { 1239 | exitFns[i](); 1240 | } 1241 | } 1242 | function traverseChildren(node, context) { 1243 | const children = node.children; 1244 | for (let i = 0; i < children.length; i++) { 1245 | const node = children[i]; 1246 | traverseNode(node, context); 1247 | } 1248 | } 1249 | function createTransformContext(root, options) { 1250 | const context = { 1251 | root, 1252 | nodeTransforms: options.nodeTransforms, 1253 | helpers: new Map(), 1254 | helper(key) { 1255 | context.helpers.set(key, 1); 1256 | }, 1257 | }; 1258 | return context; 1259 | } 1260 | function createRootCodegen(root) { 1261 | const child = root.children[0]; 1262 | if (child.type === NodeTypes.ELEMENT) { 1263 | root.codegenNode = child.codegenNode; 1264 | } 1265 | else { 1266 | root.codegenNode = child; 1267 | } 1268 | } 1269 | 1270 | function transformElement(node, context) { 1271 | if (node.type === NodeTypes.ELEMENT) { 1272 | return () => { 1273 | // 中间处理层 1274 | // tag 1275 | const vnodeTag = `'${node.tag}'`; 1276 | // props 1277 | let vnodeProps; 1278 | // children 1279 | const children = node.children; 1280 | let vnodeChildren = children[0]; 1281 | node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren); 1282 | }; 1283 | } 1284 | } 1285 | 1286 | function transformExpression(node) { 1287 | if (node.type === NodeTypes.INTERPOLATION) { 1288 | node.content.content = '_ctx.' + node.content.content; 1289 | } 1290 | } 1291 | 1292 | function isText(node) { 1293 | return node.type === NodeTypes.TEXT || node.type === NodeTypes.INTERPOLATION; 1294 | } 1295 | 1296 | function transformText(node) { 1297 | if (node.type === NodeTypes.ELEMENT) { 1298 | return () => { 1299 | const { children } = node; 1300 | let currentContainer; 1301 | for (let i = 0; i < children.length; i++) { 1302 | const child = children[i]; 1303 | if (isText(child)) { 1304 | for (let j = i + 1; j < children.length; j++) { 1305 | const next = children[j]; 1306 | if (isText(next)) { 1307 | if (!currentContainer) { 1308 | currentContainer = children[i] = { 1309 | type: NodeTypes.COMPOUND_EXPRESSION, 1310 | children: [child], 1311 | }; 1312 | } 1313 | currentContainer.children.push(' + '); 1314 | currentContainer.children.push(next); 1315 | children.splice(j, 1); 1316 | j--; 1317 | } 1318 | else { 1319 | currentContainer = undefined; 1320 | break; 1321 | } 1322 | } 1323 | } 1324 | } 1325 | }; 1326 | } 1327 | } 1328 | 1329 | function baseCompile(template) { 1330 | const ast = baseParse(template); 1331 | transform(ast, { 1332 | nodeTransforms: [transformExpression, transformElement, transformText], 1333 | }); 1334 | return generate(ast); 1335 | } 1336 | 1337 | function compileToFunction(template) { 1338 | const { code } = baseCompile(template); 1339 | const render = new Function('Vue', code)(runtimeDom); 1340 | console.log(render); 1341 | return render; 1342 | } 1343 | registerRuntimeCompiler(compileToFunction); 1344 | 1345 | export { ReactiveEffect, computed, createApp, createVNode as createElementVNode, createRenderer, createTextVNode, effect, getCurrentInstance, h, inject, isProxy, isReactive, isReadonly, isRef, nextTick, provide, proxyRefs, reactive, readonly, ref, registerRuntimeCompiler, renderSlots, shallowReadonly, toDisplayString, unRef }; 1346 | -------------------------------------------------------------------------------- /packages/vue/dist/tiny-vue.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | const Fragment = Symbol('Fragment'); 6 | const Text = Symbol('Text'); 7 | function createVNode(type, props, children) { 8 | const vnode = { 9 | type, 10 | props, 11 | children, 12 | component: null, 13 | shapeFlag: getShapeFlag(type), 14 | key: props && props.key, 15 | el: null, 16 | subTree: {}, 17 | isMounted: false, 18 | }; 19 | // 对children进行标识 20 | if (typeof children === 'string') { 21 | vnode.shapeFlag |= 4 /* TEXT_CHILDREN */; 22 | } 23 | else if (Array.isArray(children)) { 24 | vnode.shapeFlag |= 8 /* ARRAY_CHILDREN */; 25 | } 26 | if (vnode.shapeFlag & 2 /* STATEFUL_COMPONENT */) { 27 | if (typeof children === 'object') { 28 | vnode.shapeFlag |= 16 /* SLOT_CHILDREN */; 29 | } 30 | } 31 | return vnode; 32 | } 33 | function createTextVNode(text) { 34 | return createVNode(Text, {}, text); 35 | } 36 | function getShapeFlag(type) { 37 | return typeof type === 'string' 38 | ? 1 /* ELEMENT */ 39 | : 2 /* STATEFUL_COMPONENT */; 40 | } 41 | 42 | function h(type, props, children) { 43 | return createVNode(type, props, children); 44 | } 45 | 46 | function renderSlots(slots, name, props) { 47 | const slot = slots[name]; 48 | if (slot) { 49 | if (typeof slot === 'function') { 50 | return createVNode(Fragment, {}, slot(props)); 51 | } 52 | } 53 | } 54 | 55 | function toDisplayString(value) { 56 | return String(value); 57 | } 58 | 59 | const extend = Object.assign; 60 | const EMPTY_OBJ = {}; 61 | const isObject = (val) => { 62 | return val !== null && typeof val === 'object'; 63 | }; 64 | const isString = (val) => typeof val === 'string'; 65 | const hasChanged = (newVal, val) => { 66 | return !Object.is(newVal, val); 67 | }; 68 | const isOn = (val) => /^on[A-Z]/.test(val); 69 | const hasOwn = (val, key) => Object.prototype.hasOwnProperty.call(val, key); 70 | const camelize = (str) => { 71 | return str.replace(/-(\w)/g, (_, c) => { 72 | return c ? c.toUpperCase() : ''; 73 | }); 74 | }; 75 | const capitalize = (str) => { 76 | return str.charAt(0).toUpperCase() + str.slice(1); 77 | }; 78 | const toHandlerKey = (str) => { 79 | return str ? 'on' + capitalize(str) : ''; 80 | }; 81 | 82 | let activeEffect; 83 | let shouldTrack = false; 84 | class ReactiveEffect { 85 | constructor(fn, scheduler) { 86 | this.deps = []; 87 | this.active = true; 88 | this._fn = fn; 89 | this.scheduler = scheduler; 90 | } 91 | run() { 92 | if (!this.active) { 93 | return this._fn(); 94 | } 95 | // 应该收集 96 | shouldTrack = true; 97 | activeEffect = this; 98 | const result = this._fn(); 99 | // 重置 100 | shouldTrack = false; 101 | return result; 102 | } 103 | stop() { 104 | if (this.active) { 105 | cleanupEffect(this); 106 | if (this.onStop) { 107 | this.onStop(); 108 | } 109 | this.active = false; 110 | } 111 | } 112 | } 113 | function cleanupEffect(effect) { 114 | effect.deps.forEach((dep) => { 115 | dep.delete(effect); 116 | }); 117 | // 把 effect.deps 清空 118 | effect.deps.length = 0; 119 | } 120 | const targetMap = new Map(); 121 | function track(target, key) { 122 | if (!isTracking()) 123 | return; 124 | // target -> key -> dep 125 | let depsMap = targetMap.get(target); 126 | if (!depsMap) { 127 | depsMap = new Map(); 128 | targetMap.set(target, depsMap); 129 | } 130 | let dep = depsMap.get(key); 131 | if (!dep) { 132 | dep = new Set(); 133 | depsMap.set(key, dep); 134 | } 135 | trackEffects(dep); 136 | } 137 | function trackEffects(dep) { 138 | // 看看 dep 之前有没有添加过,添加过的话,那么就不添加了 139 | if (dep.has(activeEffect)) 140 | return; 141 | dep.add(activeEffect); 142 | activeEffect.deps.push(dep); 143 | } 144 | function isTracking() { 145 | return shouldTrack && activeEffect !== undefined; 146 | } 147 | function trigger(target, key) { 148 | const depsMap = targetMap.get(target); 149 | const dep = depsMap.get(key); 150 | triggerEffects(dep); 151 | } 152 | function triggerEffects(dep) { 153 | for (const effect of dep) { 154 | if (effect.scheduler) { 155 | effect.scheduler(); 156 | } 157 | else { 158 | effect.run(); 159 | } 160 | } 161 | } 162 | function effect(fn, options = {}) { 163 | const { scheduler } = options; 164 | const _effect = new ReactiveEffect(fn, scheduler); 165 | extend(_effect, options); 166 | _effect.run(); 167 | const runner = _effect.run.bind(_effect); 168 | runner.effect = _effect; 169 | return runner; 170 | } 171 | 172 | const get = createGetter(); 173 | const set = createSetter(); 174 | const readonlyGet = createGetter(true); 175 | const shallowReadonlyGet = createGetter(true, true); 176 | const mutableHandlers = { 177 | get, 178 | set, 179 | }; 180 | const readonlyHandlers = { 181 | get: readonlyGet, 182 | set(target, key, value) { 183 | console.warn(`key ${key} can not be set, because target is readonly`, target); 184 | return true; 185 | }, 186 | }; 187 | const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 188 | get: shallowReadonlyGet, 189 | }); 190 | function createGetter(isReadonly = false, isShallow = false) { 191 | return function (target, key) { 192 | if (key === "__v_isReactive" /* IS_REACTIVE */) { 193 | return !isReadonly; 194 | } 195 | else if (key === "__v_isReadonly" /* IS_READONLY */) { 196 | return isReadonly; 197 | } 198 | const res = Reflect.get(target, key); 199 | if (isShallow) { 200 | return res; 201 | } 202 | if (isObject(res)) { 203 | return isReadonly ? readonly(res) : reactive(res); 204 | } 205 | if (!isReadonly) { 206 | track(target, key); 207 | } 208 | return res; 209 | }; 210 | } 211 | function createSetter() { 212 | return function (target, key, value) { 213 | const res = Reflect.set(target, key, value); 214 | trigger(target, key); 215 | return res; 216 | }; 217 | } 218 | 219 | function reactive(raw) { 220 | return createActiveObject(raw, mutableHandlers); 221 | } 222 | function readonly(raw) { 223 | return createActiveObject(raw, readonlyHandlers); 224 | } 225 | function shallowReadonly(raw) { 226 | return createActiveObject(raw, shallowReadonlyHandlers); 227 | } 228 | function createActiveObject(raw, baseHandlers) { 229 | if (!isObject(raw)) { 230 | console.warn(`target ${raw} 必须是一个对象`); 231 | return raw; 232 | } 233 | return new Proxy(raw, baseHandlers); 234 | } 235 | function isReactive(raw) { 236 | return !!raw["__v_isReactive" /* IS_REACTIVE */]; 237 | } 238 | function isReadonly(raw) { 239 | return !!raw["__v_isReadonly" /* IS_READONLY */]; 240 | } 241 | function isProxy(value) { 242 | return isReactive(value) || isReadonly(value); 243 | } 244 | 245 | class RefImpl { 246 | constructor(raw) { 247 | this.__v_isRef = true; 248 | this._rawValue = raw; 249 | this._value = convert(raw); 250 | this.dep = new Set(); 251 | } 252 | get value() { 253 | // 收集依赖 254 | trackRefEffects(this); 255 | return this._value; 256 | } 257 | set value(newValue) { 258 | // 一定是先修改了值的,触发依赖 259 | if (hasChanged(newValue, this._rawValue)) { 260 | this._rawValue = newValue; 261 | this._value = convert(newValue); 262 | triggerEffects(this.dep); 263 | } 264 | } 265 | } 266 | function trackRefEffects(ref) { 267 | if (isTracking()) { 268 | trackEffects(ref.dep); 269 | } 270 | } 271 | function convert(value) { 272 | return isObject(value) ? reactive(value) : value; 273 | } 274 | function ref(raw) { 275 | return new RefImpl(raw); 276 | } 277 | function isRef(value) { 278 | return !!value.__v_isRef; 279 | } 280 | function unRef(ref) { 281 | return isRef(ref) ? ref.value : ref; 282 | } 283 | function proxyRefs(objectWidthRefs) { 284 | return new Proxy(objectWidthRefs, { 285 | get(target, key) { 286 | return unRef(Reflect.get(target, key)); 287 | }, 288 | set(target, key, value) { 289 | if (isRef(target[key]) && !isRef(value)) { 290 | return (target[key].value = value); 291 | } 292 | else { 293 | return Reflect.set(target, key, value); 294 | } 295 | }, 296 | }); 297 | } 298 | 299 | class ComputedRefImpl { 300 | constructor(getter) { 301 | this._dirty = true; 302 | this._getter = getter; 303 | this._effect = new ReactiveEffect(getter, () => { 304 | this._dirty = true; 305 | }); 306 | } 307 | get value() { 308 | if (this._dirty) { 309 | this._dirty = false; 310 | this._value = this._effect.run(); 311 | } 312 | return this._value; 313 | } 314 | } 315 | function computed(getter) { 316 | return new ComputedRefImpl(getter); 317 | } 318 | 319 | // 执行子组件的emit方法,触发父组件props中的注册事件 320 | function emit(instance, event, ...args) { 321 | console.log('emit', event); 322 | const { props } = instance; 323 | // 将传入的event名称转化为props中的注册事件名 324 | const handlerKeyName = toHandlerKey(camelize(event)); 325 | const handler = props[handlerKeyName]; 326 | handler && handler(...args); 327 | } 328 | 329 | function initProps(instance, rawProps) { 330 | instance.props = rawProps || {}; 331 | } 332 | 333 | const publicPropertiesMap = { 334 | $el: (i) => i.vnode.el, 335 | $slots: (i) => i.slots, 336 | $props: (i) => i.props, 337 | }; 338 | const PublicInstanceProxyHandlers = { 339 | get({ _: instance }, key) { 340 | const { setupState, props } = instance; 341 | if (hasOwn(setupState, key)) { 342 | return setupState[key]; 343 | } 344 | else if (hasOwn(props, key)) { 345 | return props[key]; 346 | } 347 | const publicGetter = publicPropertiesMap[key]; 348 | if (publicGetter) { 349 | return publicGetter(instance); 350 | } 351 | }, 352 | }; 353 | 354 | function initSlots(instance, children) { 355 | // 判断是否具名插槽 356 | const { vnode } = instance; 357 | if (vnode.shapeFlag & 16 /* SLOT_CHILDREN */) { 358 | normalizeObjectSlots(children, instance.slots); 359 | } 360 | } 361 | // 处理具名插槽 362 | function normalizeObjectSlots(children, slots) { 363 | for (const key in children) { 364 | const value = children[key]; 365 | // 使用function是为了向插槽传参 366 | slots[key] = (props) => normalizeSlotValue(value(props)); 367 | } 368 | } 369 | function normalizeSlotValue(value) { 370 | // 判断children是否数组的目的:让用户写slot时,既可以填写数组,也可以填写单个 371 | return Array.isArray(value) ? value : [value]; 372 | } 373 | 374 | function createComponentInstance(vnode, parent) { 375 | const component = { 376 | vnode, 377 | type: vnode.type, 378 | next: null, 379 | setupState: {}, 380 | props: {}, 381 | emit: () => { }, 382 | slots: {}, 383 | provides: parent ? parent.provides : {}, 384 | parent, 385 | }; 386 | // bind的作用:默认emit的第一个参数是组件实例。 387 | // 用户在调用emit时就可以只传事件名称,不用传父组件实例了。 388 | component.emit = emit.bind(null, component); 389 | return component; 390 | } 391 | function setupComponent(instance) { 392 | initProps(instance, instance.vnode.props); 393 | initSlots(instance, instance.vnode.children); 394 | setupStatefulComponent(instance); 395 | } 396 | function setupStatefulComponent(instance) { 397 | const Component = instance.type; 398 | // 对render中的this指向进行代理 399 | instance.proxy = new Proxy({ _: instance }, PublicInstanceProxyHandlers); 400 | const { setup } = Component; 401 | if (setup) { 402 | setCurrentInstance(instance); 403 | const setupResult = setup(shallowReadonly(instance.props), { 404 | emit: instance.emit, 405 | }); 406 | // getCurrentInstance 会在 setup 里面被执行,执行完之后就把全局变量重置回 null 407 | setCurrentInstance(null); 408 | handleSetupResult(instance, setupResult); 409 | } 410 | } 411 | function handleSetupResult(instance, setupResult) { 412 | if (typeof setupResult === 'object') { 413 | // 此处的proxyRefs是在为了render中自动解构ref的.value 414 | instance.setupState = proxyRefs(setupResult); 415 | } 416 | finishComponentSetup(instance); 417 | } 418 | function finishComponentSetup(instance) { 419 | const Component = instance.type; 420 | if (compiler && !Component.render) { 421 | if (Component.template) { 422 | Component.render = compiler(Component.template); 423 | } 424 | } 425 | instance.render = Component.render; 426 | } 427 | let currentInstance = null; 428 | function getCurrentInstance() { 429 | return currentInstance; 430 | } 431 | function setCurrentInstance(value) { 432 | currentInstance = value; 433 | } 434 | let compiler; 435 | function registerRuntimeCompiler(_compiler) { 436 | compiler = _compiler; 437 | } 438 | 439 | function provide(key, value) { 440 | const currentInstance = getCurrentInstance(); 441 | if (currentInstance) { 442 | let { provides } = currentInstance; 443 | const parentProvides = currentInstance.parent.provides; 444 | // 如果是第一次在当前组件provide,则把父组件的provide挂到当前组件provides的原型链上 445 | if (provides === parentProvides) { 446 | provides = currentInstance.provides = Object.create(parentProvides); 447 | } 448 | provides[key] = value; 449 | } 450 | } 451 | function inject(key, defaultValue) { 452 | const currentInstance = getCurrentInstance(); 453 | if (currentInstance) { 454 | const parentProvides = currentInstance.parent.provides; 455 | if (key in parentProvides) { 456 | return parentProvides[key]; 457 | } 458 | else if (defaultValue) { 459 | if (typeof defaultValue === 'function') { 460 | return defaultValue(); 461 | } 462 | return defaultValue; 463 | } 464 | } 465 | } 466 | 467 | function shouldUpdateComponent(prevVNode, nextVNode) { 468 | const { props: prevProps } = prevVNode; 469 | const { props: nextProps } = nextVNode; 470 | for (const key in nextProps) { 471 | if (nextProps[key] !== prevProps[key]) { 472 | return true; 473 | } 474 | } 475 | return false; 476 | } 477 | 478 | function createAppAPI(render) { 479 | return function createApp(rootComponent) { 480 | return { 481 | mount(rootContainer) { 482 | const _rootContainer = typeof rootContainer === 'string' 483 | ? document.querySelector(rootContainer) 484 | : rootContainer; 485 | const vnode = createVNode(rootComponent); 486 | render(vnode, _rootContainer); 487 | }, 488 | }; 489 | }; 490 | } 491 | 492 | const queue = []; 493 | const activePreFlushCbs = []; 494 | let isFlushPending = false; 495 | const p = Promise.resolve(); 496 | function nextTick(fn) { 497 | return fn ? p.then(fn) : p; 498 | } 499 | function queueJobs(job) { 500 | if (!queue.includes(job)) { 501 | queue.push(job); 502 | } 503 | queueFlush(); 504 | } 505 | function queueFlush() { 506 | if (isFlushPending) 507 | return; 508 | isFlushPending = true; 509 | nextTick(flushJob); 510 | } 511 | function flushJob() { 512 | isFlushPending = false; 513 | flushPreFlushCbs(); 514 | let job; 515 | while ((job = queue.shift())) { 516 | job && job(); 517 | } 518 | } 519 | function flushPreFlushCbs() { 520 | for (let i = 0; i < activePreFlushCbs.length; i++) { 521 | activePreFlushCbs[i](); 522 | } 523 | } 524 | 525 | function createRenderer(options) { 526 | const { createElement: hostCreateElement, patchProp: hostPatchProp, insert: hostInsert, remove: hostRemove, setElementText: hostSetElementText, } = options; 527 | function render(vnode, container) { 528 | patch(null, vnode, container, null, null); 529 | } 530 | // n1 -> 老的vnode 531 | // n2 -> 新的vnode 532 | function patch(n1, n2, container, parentComponent, anchor) { 533 | const { type, shapeFlag } = n2; 534 | switch (type) { 535 | case Fragment: 536 | processFragment(n1, n2, container, parentComponent, anchor); 537 | break; 538 | case Text: 539 | processText(n1, n2, container); 540 | break; 541 | default: 542 | if (shapeFlag & 1 /* ELEMENT */) { 543 | processElement(n1, n2, container, parentComponent, anchor); 544 | } 545 | else if (shapeFlag & 2 /* STATEFUL_COMPONENT */) { 546 | processComponent(n1, n2, container, parentComponent, anchor); 547 | } 548 | break; 549 | } 550 | } 551 | function processFragment(n1, n2, container, parentComponent, anchor) { 552 | mountChildren(n2, container, parentComponent, anchor); 553 | } 554 | function processText(n1, n2, container) { 555 | const { children } = n2; 556 | const textNode = (n2.el = document.createTextNode(children)); 557 | container.append(textNode); 558 | } 559 | function processElement(n1, n2, container, parentComponent, anchor) { 560 | if (!n1) { 561 | mountElement(n2, container, parentComponent, anchor); 562 | } 563 | else { 564 | patchElement(n1, n2, container, parentComponent, anchor); 565 | } 566 | } 567 | function patchElement(n1, n2, container, parentComponent, anchor) { 568 | console.log('patchElement'); 569 | console.log('n1', n1); 570 | console.log('n2', n2); 571 | const oldProps = n1.props || EMPTY_OBJ; 572 | const newProps = n2.props || EMPTY_OBJ; 573 | const el = (n2.el = n1.el); 574 | patchChildren(n1, n2, el, parentComponent, anchor); 575 | patchProps(el, oldProps, newProps); 576 | } 577 | function patchChildren(n1, n2, container, parentComponent, anchor) { 578 | const { shapeFlag: prevShapeFlag, children: c1 } = n1; 579 | const { shapeFlag, children: c2 } = n2; 580 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 581 | // 如果新的是 文本 582 | if (prevShapeFlag & 8 /* ARRAY_CHILDREN */) { 583 | // 1. 把老的 children 清空 584 | unmountChildren(c1); 585 | } 586 | if (c1 !== c2) { 587 | // 2. 设置text 588 | hostSetElementText(container, c2); 589 | } 590 | } 591 | else { 592 | // 如果新的是数组 593 | if (prevShapeFlag & 4 /* TEXT_CHILDREN */) { 594 | hostSetElementText(container, ''); 595 | mountChildren(c2, container, parentComponent, anchor); 596 | } 597 | else { 598 | patchKeyedChildren(c1, c2, container, parentComponent, anchor); 599 | } 600 | } 601 | } 602 | function patchKeyedChildren(c1, c2, container, parentComponent, parentAnchor) { 603 | const l2 = c2.length; 604 | let i = 0; 605 | let e1 = c1.length - 1; 606 | let e2 = l2 - 1; 607 | function isSameVNodeType(n1, n2) { 608 | return n1.type === n2.type && n1.key === n2.key; 609 | } 610 | // 1、左侧的对比 611 | while (i <= e1 && i <= e2) { 612 | const n1 = c1[i]; 613 | const n2 = c2[i]; 614 | if (isSameVNodeType(n1, n2)) { 615 | patch(n1, n2, container, parentComponent, parentAnchor); 616 | } 617 | else { 618 | break; 619 | } 620 | i++; 621 | } 622 | // 2、右侧的对比 623 | while (i <= e1 && i <= e2) { 624 | const n1 = c1[e1]; 625 | const n2 = c2[e2]; 626 | if (isSameVNodeType(n1, n2)) { 627 | patch(n1, n2, container, parentComponent, parentAnchor); 628 | } 629 | else { 630 | break; 631 | } 632 | e1--; 633 | e2--; 634 | } 635 | // 3、新的比老的多,创建 636 | if (i > e1) { 637 | if (i <= e2) { 638 | const nextPos = e2 + 1; 639 | const anchor = nextPos < l2 ? c2[nextPos].el : null; 640 | while (i <= e2) { 641 | patch(null, c2[i], container, parentComponent, anchor); 642 | i++; 643 | } 644 | } 645 | // 老的比新的长,删除 646 | } 647 | else if (i > e2) { 648 | while (i <= e1) { 649 | hostRemove(c1[i].el); 650 | i++; 651 | } 652 | } 653 | else { 654 | // 中间对比 655 | let s1 = i, s2 = i; 656 | const toBePatched = e2 - s2 + 1; 657 | let patched = 0; 658 | const keyToNewIndexMap = new Map(); // 记录新children中间部分的节点index 659 | const newIndexToOldIndexMap = new Array(toBePatched).fill(0); // 创建一个定长的数组 660 | let moved = false; 661 | let maxNewIndexSoFar = 0; 662 | for (let i = s2; i <= e2; i++) { 663 | // 此处的 nextChild/prevChild 是相对于更新前、更新后 664 | const nextChild = c2[i]; 665 | keyToNewIndexMap.set(nextChild.key, i); 666 | } 667 | for (let i = s1; i <= e1; i++) { 668 | const prevChild = c1[i]; 669 | // 如果新节点都被遍历完了,说明剩下的老节点都是不存在了的,直接删除 670 | if (patched >= toBePatched) { 671 | hostRemove(prevChild.el); 672 | continue; 673 | } 674 | let newIndex; 675 | // key 不为 null/undefined 676 | if (prevChild.key != null) { 677 | // 根据key去查找该节点在新数据中的位置 678 | newIndex = keyToNewIndexMap.get(prevChild.key); 679 | } 680 | else { 681 | // 循环查找该节点在新数据中的位置 682 | for (let j = s2; j <= e2; j++) { 683 | if (isSameVNodeType(prevChild, c2[j])) { 684 | newIndex = j; 685 | break; 686 | } 687 | } 688 | } 689 | // 如果新数据中没有该节点了,就把它删除 690 | if (newIndex === undefined) { 691 | hostRemove(prevChild.el); 692 | } 693 | else { 694 | if (newIndex >= maxNewIndexSoFar) { 695 | maxNewIndexSoFar = newIndex; 696 | } 697 | else { 698 | moved = true; 699 | } 700 | // 如果存在,则记录新老位置 701 | // +1是因为i可能为0,但0和数据初始化的值相同,会被认为老节点不存在了 702 | newIndexToOldIndexMap[newIndex - s2] = i + 1; 703 | // 对它继续深度遍历 704 | patch(prevChild, c2[newIndex], container, parentComponent, null); 705 | patched++; 706 | } 707 | } 708 | const increasingNewIndexSequence = moved 709 | ? getSequence(newIndexToOldIndexMap) 710 | : []; 711 | let j = increasingNewIndexSequence.length - 1; 712 | for (let i = toBePatched - 1; i >= 0; i--) { 713 | const nextIndex = i + s2; 714 | const nextChild = c2[nextIndex]; 715 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null; 716 | if (newIndexToOldIndexMap[i] === 0) { 717 | patch(null, nextChild, container, parentComponent, anchor); 718 | } 719 | else if (moved) { 720 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 721 | console.log('移动位置'); 722 | hostInsert(nextChild.el, container, anchor); 723 | } 724 | else { 725 | j--; 726 | } 727 | } 728 | } 729 | } 730 | } 731 | function unmountChildren(children) { 732 | for (let i = 0; i < children.length; i++) { 733 | const el = children[i].el; 734 | hostRemove(el); 735 | } 736 | } 737 | function patchProps(el, oldProps, newProps) { 738 | if (oldProps !== newProps) { 739 | for (const key in newProps) { 740 | const prevProp = oldProps[key]; 741 | const nextProp = newProps[key]; 742 | if (prevProp !== nextProp) { 743 | hostPatchProp(el, key, prevProp, nextProp); 744 | } 745 | } 746 | if (oldProps !== EMPTY_OBJ) { 747 | for (const key in oldProps) { 748 | if (!(key in newProps)) { 749 | hostPatchProp(el, key, oldProps[key], null); 750 | } 751 | } 752 | } 753 | } 754 | } 755 | function mountElement(vnode, container, parentComponent, anchor) { 756 | // 这个虚拟节点是属于element的 757 | const el = (vnode.el = hostCreateElement(vnode.type)); 758 | // 对子组件进行解析 759 | const { children, shapeFlag } = vnode; 760 | if (shapeFlag & 4 /* TEXT_CHILDREN */) { 761 | el.textContent = children; 762 | } 763 | else if (shapeFlag & 8 /* ARRAY_CHILDREN */) { 764 | mountChildren(vnode.children, el, parentComponent, anchor); 765 | } 766 | // 对props进行解析 767 | const { props } = vnode; 768 | for (const key in props) { 769 | const val = props[key]; 770 | // 事件注册 771 | hostPatchProp(el, key, null, val); 772 | } 773 | hostInsert(el, container, anchor); 774 | } 775 | function mountChildren(children, container, parentComponent, anchor) { 776 | children.forEach((v) => { 777 | patch(null, v, container, parentComponent, anchor); 778 | }); 779 | } 780 | function processComponent(n1, n2, container, parentComponent, anchor) { 781 | if (!n1) { 782 | mountComponent(n2, container, parentComponent, anchor); 783 | } 784 | else { 785 | updateComponent(n1, n2); 786 | } 787 | } 788 | function updateComponent(n1, n2) { 789 | const instance = (n2.component = n1.component); 790 | if (shouldUpdateComponent(n1, n2)) { 791 | instance.next = n2; 792 | instance.update(); 793 | } 794 | else { 795 | n2.el = n1.el; 796 | instance.vnode = n2; 797 | } 798 | } 799 | function mountComponent(initialVNode, container, parentComponent, anchor) { 800 | const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent)); 801 | setupComponent(instance); 802 | setupRenderEffect(instance, initialVNode, container, anchor); 803 | } 804 | function setupRenderEffect(instance, initialVNode, container, anchor) { 805 | // effect:在第一次执行render的时候收集依赖 806 | instance.update = effect(() => { 807 | if (!instance.isMounted) { 808 | console.log('init'); 809 | const { proxy } = instance; 810 | const subTree = (instance.subTree = instance.render.call(proxy, proxy)); 811 | patch(null, subTree, container, instance, anchor); 812 | // 要在所有element挂载完之后,把根元素的el挂载到组件的虚拟节点上 813 | initialVNode.el = subTree.el; 814 | instance.isMounted = true; 815 | } 816 | else { 817 | console.log('update'); 818 | // 需要新的虚拟节点 819 | const { next, vnode } = instance; 820 | if (next) { 821 | next.el = vnode.el; 822 | updateComponentPreRender(instance, next); 823 | } 824 | const { proxy } = instance; 825 | const subTree = instance.render.call(proxy, proxy); 826 | const prevSubTree = instance.subTree; 827 | instance.subTree = subTree; 828 | patch(prevSubTree, subTree, container, instance, anchor); 829 | } 830 | }, { 831 | scheduler() { 832 | console.log('doing scheduler'); 833 | queueJobs(instance.update); 834 | }, 835 | }); 836 | } 837 | return { 838 | createApp: createAppAPI(render), 839 | }; 840 | } 841 | function updateComponentPreRender(instance, nextVNode) { 842 | instance.vnode = nextVNode; 843 | instance.next = null; 844 | instance.props = nextVNode.props; 845 | } 846 | function getSequence(arr) { 847 | const p = arr.slice(); 848 | const result = [0]; 849 | let i, j, u, v, c; 850 | const len = arr.length; 851 | for (i = 0; i < len; i++) { 852 | const arrI = arr[i]; 853 | if (arrI !== 0) { 854 | j = result[result.length - 1]; 855 | if (arr[j] < arrI) { 856 | p[i] = j; 857 | result.push(i); 858 | continue; 859 | } 860 | u = 0; 861 | v = result.length - 1; 862 | while (u < v) { 863 | c = (u + v) >> 1; 864 | if (arr[result[c]] < arrI) { 865 | u = c + 1; 866 | } 867 | else { 868 | v = c; 869 | } 870 | } 871 | if (arrI < arr[result[u]]) { 872 | if (u > 0) { 873 | p[i] = result[u - 1]; 874 | } 875 | result[u] = i; 876 | } 877 | } 878 | } 879 | u = result.length; 880 | v = result[u - 1]; 881 | while (u-- > 0) { 882 | result[u] = v; 883 | v = p[v]; 884 | } 885 | return result; 886 | } 887 | 888 | function createElement(type) { 889 | return document.createElement(type); 890 | } 891 | function patchProp(el, key, prevVal, nextVal) { 892 | if (isOn(key)) { 893 | const event = key.slice(2).toLowerCase(); 894 | el.addEventListener(event, nextVal); 895 | } 896 | else { 897 | if (nextVal === undefined || nextVal === null) { 898 | el.removeAttribute(key); 899 | } 900 | else { 901 | el.setAttribute(key, nextVal); 902 | } 903 | } 904 | } 905 | function insert(child, parent, anchor) { 906 | parent.insertBefore(child, anchor || null); 907 | } 908 | function remove(child) { 909 | const parent = child.parentNode; 910 | if (parent) { 911 | parent.removeChild(child); 912 | } 913 | } 914 | function setElementText(el, text) { 915 | el.textContent = text; 916 | } 917 | const renderer = createRenderer({ 918 | createElement, 919 | patchProp, 920 | insert, 921 | remove, 922 | setElementText, 923 | }); 924 | function createApp(...args) { 925 | return renderer.createApp(...args); 926 | } 927 | 928 | var runtimeDom = /*#__PURE__*/Object.freeze({ 929 | __proto__: null, 930 | createApp: createApp, 931 | h: h, 932 | renderSlots: renderSlots, 933 | createTextVNode: createTextVNode, 934 | createElementVNode: createVNode, 935 | getCurrentInstance: getCurrentInstance, 936 | registerRuntimeCompiler: registerRuntimeCompiler, 937 | provide: provide, 938 | inject: inject, 939 | createRenderer: createRenderer, 940 | nextTick: nextTick, 941 | toDisplayString: toDisplayString, 942 | ref: ref, 943 | isRef: isRef, 944 | unRef: unRef, 945 | proxyRefs: proxyRefs, 946 | effect: effect, 947 | ReactiveEffect: ReactiveEffect, 948 | reactive: reactive, 949 | readonly: readonly, 950 | shallowReadonly: shallowReadonly, 951 | isReactive: isReactive, 952 | isReadonly: isReadonly, 953 | isProxy: isProxy, 954 | computed: computed 955 | }); 956 | 957 | const TO_DISPLAY_STRING = Symbol('toDisplayString'); 958 | const CREATE_ELEMENT_VNODE = Symbol('createElementVNode'); 959 | const helperMapName = { 960 | [TO_DISPLAY_STRING]: 'toDisplayString', 961 | [CREATE_ELEMENT_VNODE]: 'createElementVNode', 962 | }; 963 | 964 | var NodeTypes; 965 | (function (NodeTypes) { 966 | NodeTypes[NodeTypes["INTERPOLATION"] = 0] = "INTERPOLATION"; 967 | NodeTypes[NodeTypes["SIMPLE_EXPRESSION"] = 1] = "SIMPLE_EXPRESSION"; 968 | NodeTypes[NodeTypes["ELEMENT"] = 2] = "ELEMENT"; 969 | NodeTypes[NodeTypes["TEXT"] = 3] = "TEXT"; 970 | NodeTypes[NodeTypes["ROOT"] = 4] = "ROOT"; 971 | NodeTypes[NodeTypes["COMPOUND_EXPRESSION"] = 5] = "COMPOUND_EXPRESSION"; 972 | })(NodeTypes || (NodeTypes = {})); 973 | function createVNodeCall(context, tag, props, children) { 974 | context.helper(CREATE_ELEMENT_VNODE); 975 | return { 976 | type: NodeTypes.ELEMENT, 977 | tag, 978 | props, 979 | children, 980 | }; 981 | } 982 | 983 | function generate(ast) { 984 | const context = createCodegenContext(); 985 | const { push } = context; 986 | genFunctionPreamble(ast, context); 987 | const functionName = 'render'; 988 | const args = ['_ctx', '_cache']; 989 | const signature = args.join(', '); 990 | push(`function ${functionName}(${signature}){`); 991 | push('return '); 992 | genNode(ast.codegenNode, context); 993 | push('}'); 994 | return { 995 | code: context.code, 996 | }; 997 | } 998 | function genFunctionPreamble(ast, context) { 999 | const { push, helper } = context; 1000 | const VueBinging = 'Vue'; 1001 | const aliasHelper = (s) => `${helper(s)}:_${helper(s)}`; 1002 | if (ast.helpers.length > 0) { 1003 | push(`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinging}`); 1004 | } 1005 | push('\n'); 1006 | push('return '); 1007 | } 1008 | function genNode(node, context) { 1009 | switch (node.type) { 1010 | case NodeTypes.TEXT: 1011 | genText(node, context); 1012 | break; 1013 | case NodeTypes.INTERPOLATION: 1014 | genInterpolation(node, context); 1015 | break; 1016 | case NodeTypes.SIMPLE_EXPRESSION: 1017 | genExpression(node, context); 1018 | break; 1019 | case NodeTypes.SIMPLE_EXPRESSION: 1020 | genExpression(node, context); 1021 | break; 1022 | case NodeTypes.ELEMENT: 1023 | genElement(node, context); 1024 | break; 1025 | case NodeTypes.COMPOUND_EXPRESSION: 1026 | genCompoundExpression(node, context); 1027 | break; 1028 | } 1029 | } 1030 | function genText(node, context) { 1031 | const { push } = context; 1032 | push(`'${node.content}'`); 1033 | } 1034 | function genElement(node, context) { 1035 | const { push, helper } = context; 1036 | const { tag, children, props } = node; 1037 | push(`_${helper(CREATE_ELEMENT_VNODE)}(`); 1038 | genNodeList(genNullable([tag, props, children]), context); 1039 | push(')'); 1040 | } 1041 | function genNodeList(nodes, context) { 1042 | const { push } = context; 1043 | for (let i = 0; i < nodes.length; i++) { 1044 | const node = nodes[i]; 1045 | if (isString(node)) { 1046 | push(node); 1047 | } 1048 | else { 1049 | genNode(node, context); 1050 | } 1051 | if (i < nodes.length - 1) { 1052 | push(', '); 1053 | } 1054 | } 1055 | } 1056 | function genNullable(args) { 1057 | return args.map((arg) => arg || 'null'); 1058 | } 1059 | function genCompoundExpression(node, context) { 1060 | const { push } = context; 1061 | const children = node.children; 1062 | for (let i = 0; i < children.length; i++) { 1063 | const child = children[i]; 1064 | if (isString(child)) { 1065 | push(child); 1066 | } 1067 | else { 1068 | genNode(child, context); 1069 | } 1070 | } 1071 | } 1072 | function createCodegenContext() { 1073 | const context = { 1074 | code: '', 1075 | push(source) { 1076 | context.code += source; 1077 | }, 1078 | helper(key) { 1079 | return `${helperMapName[key]}`; 1080 | }, 1081 | }; 1082 | return context; 1083 | } 1084 | function genInterpolation(node, context) { 1085 | const { push, helper } = context; 1086 | push(`_${helper(TO_DISPLAY_STRING)}(`); 1087 | genNode(node.content, context); 1088 | push(')'); 1089 | } 1090 | function genExpression(node, context) { 1091 | const { push } = context; 1092 | push(`${node.content}`); 1093 | } 1094 | 1095 | function baseParse(content) { 1096 | const context = createParserContext(content); 1097 | return createRoot(parseChildren(context, [])); 1098 | } 1099 | function createParserContext(content) { 1100 | return { 1101 | source: content, 1102 | }; 1103 | } 1104 | function createRoot(children) { 1105 | return { 1106 | children, 1107 | type: NodeTypes.ROOT, 1108 | }; 1109 | } 1110 | function parseChildren(context, ancestors) { 1111 | const nodes = []; 1112 | while (!isEnd(context, ancestors)) { 1113 | let node; 1114 | const s = context.source; 1115 | if (s.startsWith('{{')) { 1116 | node = parseInterpolation(context); 1117 | } 1118 | else if (s[0] === '<') { 1119 | if (/[a-z]/i.test(s[1])) { 1120 | node = parseElement(context, ancestors); 1121 | } 1122 | } 1123 | if (!node) { 1124 | node = parseText(context); 1125 | } 1126 | nodes.push(node); 1127 | } 1128 | return nodes; 1129 | } 1130 | function isEnd(context, ancestors) { 1131 | const s = context.source; 1132 | // 1、遇到结束标签 1133 | for (let i = ancestors.length - 1; i >= 0; i--) { 1134 | const tag = ancestors[i].tag; 1135 | if (startsWithEndTagOpen(s, tag)) { 1136 | return true; 1137 | } 1138 | } 1139 | // 2、source没有值的时候 1140 | return !s; 1141 | } 1142 | function parseInterpolation(context) { 1143 | const openDelimiter = '{{'; 1144 | const closeDelimiter = '}}'; 1145 | const closeIndex = context.source.indexOf(closeDelimiter); 1146 | advanceBy(context, openDelimiter.length); 1147 | const rawContentLength = closeIndex - openDelimiter.length; 1148 | const rawContent = parseTextData(context, rawContentLength); 1149 | const content = rawContent.trim(); 1150 | advanceBy(context, closeDelimiter.length); 1151 | return { 1152 | type: NodeTypes.INTERPOLATION, 1153 | content: { 1154 | type: NodeTypes.SIMPLE_EXPRESSION, 1155 | content: content, 1156 | }, 1157 | }; 1158 | } 1159 | function advanceBy(context, length) { 1160 | context.source = context.source.slice(length); 1161 | } 1162 | function parseElement(context, ancestors) { 1163 | const element = parseTag(context, 0 /* START */); 1164 | ancestors.push(element); 1165 | element.children = parseChildren(context, ancestors); 1166 | ancestors.pop(); 1167 | if (startsWithEndTagOpen(context.source, element.tag)) { 1168 | parseTag(context, 1 /* END */); 1169 | } 1170 | else { 1171 | throw new Error(`缺少结束标签${element.tag}`); 1172 | } 1173 | return element; 1174 | } 1175 | function startsWithEndTagOpen(source, tag) { 1176 | return (source.startsWith(' index) { 1199 | endIndex = index; 1200 | } 1201 | } 1202 | const content = parseTextData(context, endIndex); 1203 | return { 1204 | type: NodeTypes.TEXT, 1205 | content, 1206 | }; 1207 | } 1208 | function parseTextData(context, length) { 1209 | const content = context.source.slice(0, length); 1210 | advanceBy(context, content.length); 1211 | return content; 1212 | } 1213 | 1214 | function transform(root, options = {}) { 1215 | const context = createTransformContext(root, options); 1216 | // 遍历-深度优先搜索 1217 | traverseNode(root, context); 1218 | createRootCodegen(root); 1219 | root.helpers = [...context.helpers.keys()]; 1220 | } 1221 | function traverseNode(node, context) { 1222 | const nodeTransforms = context.nodeTransforms; 1223 | const exitFns = []; 1224 | if (nodeTransforms) { 1225 | for (let i = 0; i < nodeTransforms.length; i++) { 1226 | const transform = context.nodeTransforms[i]; 1227 | const onExit = transform(node, context); 1228 | if (onExit) 1229 | exitFns.push(onExit); 1230 | } 1231 | } 1232 | switch (node.type) { 1233 | case NodeTypes.INTERPOLATION: 1234 | context.helper(TO_DISPLAY_STRING); 1235 | break; 1236 | case NodeTypes.ROOT: 1237 | case NodeTypes.ELEMENT: 1238 | traverseChildren(node, context); 1239 | break; 1240 | } 1241 | let i = exitFns.length; 1242 | while (i--) { 1243 | exitFns[i](); 1244 | } 1245 | } 1246 | function traverseChildren(node, context) { 1247 | const children = node.children; 1248 | for (let i = 0; i < children.length; i++) { 1249 | const node = children[i]; 1250 | traverseNode(node, context); 1251 | } 1252 | } 1253 | function createTransformContext(root, options) { 1254 | const context = { 1255 | root, 1256 | nodeTransforms: options.nodeTransforms, 1257 | helpers: new Map(), 1258 | helper(key) { 1259 | context.helpers.set(key, 1); 1260 | }, 1261 | }; 1262 | return context; 1263 | } 1264 | function createRootCodegen(root) { 1265 | const child = root.children[0]; 1266 | if (child.type === NodeTypes.ELEMENT) { 1267 | root.codegenNode = child.codegenNode; 1268 | } 1269 | else { 1270 | root.codegenNode = child; 1271 | } 1272 | } 1273 | 1274 | function transformElement(node, context) { 1275 | if (node.type === NodeTypes.ELEMENT) { 1276 | return () => { 1277 | // 中间处理层 1278 | // tag 1279 | const vnodeTag = `'${node.tag}'`; 1280 | // props 1281 | let vnodeProps; 1282 | // children 1283 | const children = node.children; 1284 | let vnodeChildren = children[0]; 1285 | node.codegenNode = createVNodeCall(context, vnodeTag, vnodeProps, vnodeChildren); 1286 | }; 1287 | } 1288 | } 1289 | 1290 | function transformExpression(node) { 1291 | if (node.type === NodeTypes.INTERPOLATION) { 1292 | node.content.content = '_ctx.' + node.content.content; 1293 | } 1294 | } 1295 | 1296 | function isText(node) { 1297 | return node.type === NodeTypes.TEXT || node.type === NodeTypes.INTERPOLATION; 1298 | } 1299 | 1300 | function transformText(node) { 1301 | if (node.type === NodeTypes.ELEMENT) { 1302 | return () => { 1303 | const { children } = node; 1304 | let currentContainer; 1305 | for (let i = 0; i < children.length; i++) { 1306 | const child = children[i]; 1307 | if (isText(child)) { 1308 | for (let j = i + 1; j < children.length; j++) { 1309 | const next = children[j]; 1310 | if (isText(next)) { 1311 | if (!currentContainer) { 1312 | currentContainer = children[i] = { 1313 | type: NodeTypes.COMPOUND_EXPRESSION, 1314 | children: [child], 1315 | }; 1316 | } 1317 | currentContainer.children.push(' + '); 1318 | currentContainer.children.push(next); 1319 | children.splice(j, 1); 1320 | j--; 1321 | } 1322 | else { 1323 | currentContainer = undefined; 1324 | break; 1325 | } 1326 | } 1327 | } 1328 | } 1329 | }; 1330 | } 1331 | } 1332 | 1333 | function baseCompile(template) { 1334 | const ast = baseParse(template); 1335 | transform(ast, { 1336 | nodeTransforms: [transformExpression, transformElement, transformText], 1337 | }); 1338 | return generate(ast); 1339 | } 1340 | 1341 | function compileToFunction(template) { 1342 | const { code } = baseCompile(template); 1343 | const render = new Function('Vue', code)(runtimeDom); 1344 | console.log(render); 1345 | return render; 1346 | } 1347 | registerRuntimeCompiler(compileToFunction); 1348 | 1349 | exports.ReactiveEffect = ReactiveEffect; 1350 | exports.computed = computed; 1351 | exports.createApp = createApp; 1352 | exports.createElementVNode = createVNode; 1353 | exports.createRenderer = createRenderer; 1354 | exports.createTextVNode = createTextVNode; 1355 | exports.effect = effect; 1356 | exports.getCurrentInstance = getCurrentInstance; 1357 | exports.h = h; 1358 | exports.inject = inject; 1359 | exports.isProxy = isProxy; 1360 | exports.isReactive = isReactive; 1361 | exports.isReadonly = isReadonly; 1362 | exports.isRef = isRef; 1363 | exports.nextTick = nextTick; 1364 | exports.provide = provide; 1365 | exports.proxyRefs = proxyRefs; 1366 | exports.reactive = reactive; 1367 | exports.readonly = readonly; 1368 | exports.ref = ref; 1369 | exports.registerRuntimeCompiler = registerRuntimeCompiler; 1370 | exports.renderSlots = renderSlots; 1371 | exports.shallowReadonly = shallowReadonly; 1372 | exports.toDisplayString = toDisplayString; 1373 | exports.unRef = unRef; 1374 | --------------------------------------------------------------------------------