├── .eslintignore
├── packages
├── compiler-core
│ ├── src
│ │ ├── index.ts
│ │ ├── options.ts
│ │ ├── transform
│ │ │ ├── hoistStatic.ts
│ │ │ ├── transformExpression.ts
│ │ │ ├── transformElement.ts
│ │ │ └── transformText.ts
│ │ ├── runtimeHelpers.ts
│ │ ├── compile.ts
│ │ ├── utils.ts
│ │ ├── transform.ts
│ │ ├── ast.ts
│ │ ├── codegen.ts
│ │ └── parse.ts
│ └── __tests__
│ │ ├── __snapshots__
│ │ └── codegen.spec.ts.snap
│ │ ├── transform.spec.ts
│ │ ├── codegen.spec.ts
│ │ └── parse.spec.ts
├── reactivity
│ ├── __tests__
│ │ ├── index.spec.ts
│ │ ├── shallowReactive.spec.ts
│ │ ├── shallowReadonly.spec.ts
│ │ ├── readonly.spec.ts
│ │ ├── reactive.spec.ts
│ │ ├── computed.spec.ts
│ │ ├── ref.spec.ts
│ │ └── effect.spec.ts
│ └── src
│ │ ├── index.ts
│ │ ├── dep.ts
│ │ ├── computed.ts
│ │ ├── reactive.ts
│ │ ├── baseHandlers.ts
│ │ ├── ref.ts
│ │ └── effect.ts
├── shared
│ └── src
│ │ ├── toDisplayString.ts
│ │ ├── shapeFlags.ts
│ │ └── index.ts
├── vue
│ ├── examples
│ │ ├── provide
│ │ │ ├── main.js
│ │ │ ├── index.html
│ │ │ ├── app.js
│ │ │ ├── baz.js
│ │ │ └── foo.js
│ │ ├── slots
│ │ │ ├── main.js
│ │ │ ├── index.html
│ │ │ ├── foo.js
│ │ │ └── app.js
│ │ ├── update
│ │ │ ├── main.js
│ │ │ ├── index.html
│ │ │ └── app.js
│ │ ├── watch
│ │ │ ├── main.js
│ │ │ ├── index.html
│ │ │ └── app.js
│ │ ├── components
│ │ │ ├── main.js
│ │ │ ├── index.html
│ │ │ ├── app.js
│ │ │ └── foo.js
│ │ ├── helloworld
│ │ │ ├── main.js
│ │ │ ├── index.html
│ │ │ └── app.js
│ │ ├── lifecycle
│ │ │ ├── main.js
│ │ │ ├── index.html
│ │ │ └── app.js
│ │ ├── patchChildren
│ │ │ ├── main.js
│ │ │ ├── index.html
│ │ │ ├── TextToText.js
│ │ │ ├── ArrayToText.js
│ │ │ ├── TextToArray.js
│ │ │ ├── app.js
│ │ │ └── ArrayToArray.js
│ │ └── complie-base
│ │ │ ├── index.html
│ │ │ └── main.js
│ └── src
│ │ └── index.ts
├── runtime-core
│ ├── src
│ │ ├── componentProps.ts
│ │ ├── h.ts
│ │ ├── helpers
│ │ │ └── renderSlot.ts
│ │ ├── index.ts
│ │ ├── componentEmit.ts
│ │ ├── apiCreateApp.ts
│ │ ├── componentRenderUtils.ts
│ │ ├── componentPublicInstance.ts
│ │ ├── componentSlots.ts
│ │ ├── apiInject.ts
│ │ ├── apiLifecycle.ts
│ │ ├── apiWatch.ts
│ │ ├── vnode.ts
│ │ ├── component.ts
│ │ ├── scheduler.ts
│ │ └── renderer.ts
│ └── __tests__
│ │ └── scheduler.spec.ts
└── runtime-dom
│ └── src
│ ├── index.ts
│ ├── patchProp.ts
│ └── nodeOps.ts
├── .gitignore
├── babel.config.js
├── .eslintrc
├── README.md
├── rollup.config.js
├── jest.config.js
├── package.json
└── tsconfig.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | public
--------------------------------------------------------------------------------
/packages/compiler-core/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './compile'
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .idea/
3 | .DS_Store
4 | vscode/
5 | dist/
--------------------------------------------------------------------------------
/packages/reactivity/__tests__/index.spec.ts:
--------------------------------------------------------------------------------
1 | it('init', () => {
2 | expect(true).toBe(true)
3 | })
4 |
--------------------------------------------------------------------------------
/packages/shared/src/toDisplayString.ts:
--------------------------------------------------------------------------------
1 | export const toDisplayString = (val: unknown): string => {
2 | return String(val)
3 | }
4 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@ouduidui/eslint-config-ts"],
3 | "rules": {
4 | "@typescript-eslint/no-use-before-define": "off"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/reactivity/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ref'
2 | export * from './reactive'
3 | export * from './computed'
4 | export { effect } from './effect'
5 |
--------------------------------------------------------------------------------
/packages/vue/examples/provide/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from '../../dist/mini-vue.esm.js'
2 | import { App } from './app.js'
3 |
4 | const rootContainer = document.querySelector('#app')
5 | createApp(App).mount(rootContainer)
6 |
--------------------------------------------------------------------------------
/packages/vue/examples/slots/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from '../../dist/mini-vue.esm.js'
2 | import { App } from './app.js'
3 |
4 | const rootContainer = document.querySelector('#app')
5 | createApp(App).mount(rootContainer)
6 |
--------------------------------------------------------------------------------
/packages/vue/examples/update/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from '../../dist/mini-vue.esm.js'
2 | import { App } from './app.js'
3 |
4 | const rootContainer = document.querySelector('#app')
5 | createApp(App).mount(rootContainer)
6 |
--------------------------------------------------------------------------------
/packages/vue/examples/watch/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from '../../dist/mini-vue.esm.js'
2 | import { App } from './app.js'
3 |
4 | const rootContainer = document.querySelector('#app')
5 | createApp(App).mount(rootContainer)
6 |
--------------------------------------------------------------------------------
/packages/vue/examples/components/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from '../../dist/mini-vue.esm.js'
2 | import { App } from './app.js'
3 |
4 | const rootContainer = document.querySelector('#app')
5 | createApp(App).mount(rootContainer)
6 |
--------------------------------------------------------------------------------
/packages/vue/examples/helloworld/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from '../../dist/mini-vue.esm.js'
2 | import { App } from './app.js'
3 |
4 | const rootContainer = document.querySelector('#app')
5 | createApp(App).mount(rootContainer)
6 |
--------------------------------------------------------------------------------
/packages/vue/examples/lifecycle/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from '../../dist/mini-vue.esm.js'
2 | import { App } from './app.js'
3 |
4 | const rootContainer = document.querySelector('#app')
5 | createApp(App).mount(rootContainer)
6 |
--------------------------------------------------------------------------------
/packages/vue/examples/patchChildren/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from '../../dist/mini-vue.esm.js'
2 | import { App } from './app.js'
3 |
4 | const rootContainer = document.querySelector('#app')
5 | createApp(App).mount(rootContainer)
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mini-vue3
2 |
3 | ## 运行
4 |
5 | ```shell
6 | # 安装依赖
7 | pnpm i
8 |
9 | # 测试
10 | pnpm test:mini-vue3
11 | ```
12 |
13 | ## 实现
14 |
15 | - [x] reactive
16 | - [x] runtime-core
17 | - [x] runtime-dom
18 | - [x] complier-core
19 |
--------------------------------------------------------------------------------
/packages/runtime-core/src/componentProps.ts:
--------------------------------------------------------------------------------
1 | import type { ComponentInternalInstance, Data } from './component'
2 |
3 | export function initProps(instance: ComponentInternalInstance, rawProps: Data | null) {
4 | if (rawProps)
5 | instance.props = rawProps
6 | }
7 |
--------------------------------------------------------------------------------
/packages/runtime-core/src/h.ts:
--------------------------------------------------------------------------------
1 | import type { VNode, VNodeTypes } from 'runtime-core/vnode'
2 | import { createVNode } from 'runtime-core/vnode'
3 |
4 | export function h(type: VNodeTypes, props = null, children = null): VNode {
5 | return createVNode(type, props, children)
6 | }
7 |
--------------------------------------------------------------------------------
/packages/vue/examples/slots/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/vue/examples/components/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/vue/examples/provide/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/vue/examples/watch/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | HelloWorld
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/vue/examples/complie-base/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/vue/examples/helloworld/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | HelloWorld
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/vue/examples/lifecycle/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LifeCycle
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/vue/examples/patchChildren/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | patch Children
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/reactivity/src/dep.ts:
--------------------------------------------------------------------------------
1 | import type { ReactiveEffect } from './effect'
2 |
3 | export type Dep = Set
4 |
5 | /**
6 | * 创建一个依赖收集容器Dep
7 | * @param effects
8 | */
9 | export const createDep = (effects?: ReactiveEffect[]): Dep => {
10 | return new Set(effects) as Dep
11 | }
12 |
--------------------------------------------------------------------------------
/packages/vue/examples/patchChildren/TextToText.js:
--------------------------------------------------------------------------------
1 | import { h } from '../../dist/mini-vue.esm.js'
2 |
3 | const nextChildren = 'new Children'
4 | const prevChildren = 'old Children'
5 |
6 | export default {
7 | setup() {},
8 | render() {
9 | return this.isChange ? h('div', null, nextChildren) : h('div', null, prevChildren)
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/packages/vue/examples/patchChildren/ArrayToText.js:
--------------------------------------------------------------------------------
1 | import { h } from '../../dist/mini-vue.esm.js'
2 |
3 | const nextChildren = 'new Children'
4 | const prevChildren = [h('div', null, 'A'), h('div', null, 'B')]
5 |
6 | export default {
7 | setup() {},
8 | render() {
9 | return this.isChange ? h('div', null, nextChildren) : h('div', null, prevChildren)
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/packages/vue/examples/patchChildren/TextToArray.js:
--------------------------------------------------------------------------------
1 | import { h } from '../../dist/mini-vue.esm.js'
2 |
3 | const nextChildren = [h('div', null, 'A'), h('div', null, 'B')]
4 | const prevChildren = 'old Children'
5 |
6 | export default {
7 | setup() {},
8 | render() {
9 | return this.isChange ? h('div', null, nextChildren) : h('div', null, prevChildren)
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/packages/vue/examples/complie-base/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from '../../dist/mini-vue.esm.js'
2 |
3 | const App = {
4 | name: 'App',
5 | template: 'Hello, {{message}}
',
6 | setup() {
7 | return {
8 | message: 'world',
9 | }
10 | },
11 | }
12 |
13 | const rootContainer = document.querySelector('#app')
14 | createApp(App).mount(rootContainer)
15 |
--------------------------------------------------------------------------------
/packages/reactivity/__tests__/shallowReactive.spec.ts:
--------------------------------------------------------------------------------
1 | import { isReactive, shallowReactive } from '../src/reactive'
2 |
3 | describe('shallowReactive', () => {
4 | it('should not make non-reactive properties reactive', () => {
5 | const props = shallowReactive({ n: { foo: 1 } })
6 | expect(isReactive(props)).toBe(true)
7 | expect(isReactive(props.n)).toBe(false)
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/packages/vue/examples/slots/foo.js:
--------------------------------------------------------------------------------
1 | import { h, renderSlot } from '../../dist/mini-vue.esm.js'
2 |
3 | export const Foo = {
4 | render() {
5 | return h('div', null, [
6 | // 插槽
7 | renderSlot(this.$slots, 'header'),
8 | renderSlot(this.$slots, 'default', { msg: 'HelloWorld' }),
9 | renderSlot(this.$slots, 'footer'),
10 | ])
11 | },
12 | setup() {},
13 | }
14 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript'
2 |
3 | export default {
4 | input: './packages/vue/src/index.ts',
5 | output: [
6 | {
7 | format: 'cjs',
8 | file: './packages/vue/dist/mini-vue.cjs.js',
9 | },
10 | {
11 | format: 'es',
12 | file: './packages/vue/dist/mini-vue.esm.js',
13 | },
14 | ],
15 | plugins: [typescript()],
16 | }
17 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleNameMapper: {
3 | 'compiler-core/(.*?)$': '/packages/compiler-core/src/$1',
4 | 'runtime-core/(.*?)$': '/packages/runtime-core/src/$1',
5 | 'runtime-dom/(.*?)$': '/packages/runtime-dom/src/$1',
6 | 'shared/(.*?)$': '/packages/shared/src/$1',
7 | 'reactivity/(.*?)$': '/packages/reactivity/src/$1',
8 | },
9 | }
10 |
--------------------------------------------------------------------------------
/packages/compiler-core/src/options.ts:
--------------------------------------------------------------------------------
1 | import type { NodeTransform } from './transform'
2 |
3 | export interface TransformOptions {
4 | nodeTransforms?: NodeTransform[]
5 | }
6 |
7 | export interface CodegenOptions {}
8 |
9 | export interface ParserOptions {
10 | isNativeTag?: (tag: string) => boolean
11 | delimiters?: [string, string]
12 | }
13 |
14 | export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
15 |
--------------------------------------------------------------------------------
/packages/vue/examples/update/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | HelloWorld
6 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/packages/vue/examples/components/app.js:
--------------------------------------------------------------------------------
1 | import { h } from '../../dist/mini-vue.esm.js'
2 | import { Foo } from './foo.js'
3 |
4 | export const App = {
5 | render() {
6 | return h('div', { id: 'root' }, [
7 | h('h1', null, 'Components'),
8 | h(Foo, {
9 | msg: 'HelloWorld',
10 | onClickHandle(test) {
11 | console.log(`事件绑定: ${test}`)
12 | },
13 | }),
14 | ])
15 | },
16 | setup() {},
17 | }
18 |
--------------------------------------------------------------------------------
/packages/runtime-dom/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createRenderer } from 'runtime-core/index'
2 | import { extend } from 'shared/index'
3 |
4 | import { nodeOps } from './nodeOps'
5 | import { patchProp } from './patchProp'
6 |
7 | const rendererOptions = extend({ patchProp }, nodeOps)
8 |
9 | const renderer = createRenderer(rendererOptions)
10 |
11 | export const createApp = (...args) => renderer.createApp(...args)
12 |
13 | export * from 'runtime-core/index'
14 |
--------------------------------------------------------------------------------
/packages/runtime-core/src/helpers/renderSlot.ts:
--------------------------------------------------------------------------------
1 | import { Fragment, createVNode } from 'runtime-core/vnode'
2 | import type { Slots } from 'runtime-core/componentSlots'
3 | import type { Data } from 'runtime-core/component'
4 | import type { VNode } from '../vnode'
5 |
6 | export function renderSlot(slots: Slots, name: string, props: Data = {}): VNode | undefined {
7 | const slot = slots[name]
8 | if (slot)
9 | return createVNode(Fragment, {}, slot(props))
10 | }
11 |
--------------------------------------------------------------------------------
/packages/vue/examples/provide/app.js:
--------------------------------------------------------------------------------
1 | import { getCurrentInstance, h, provide } from '../../dist/mini-vue.esm.js'
2 | import { Foo } from './foo.js'
3 |
4 | export const App = {
5 | render() {
6 | return h('div', { id: 'root' }, [h('h1', null, 'Components'), h(Foo)])
7 | },
8 | setup() {
9 | const instance = getCurrentInstance()
10 | console.log('app Instance: ', instance)
11 |
12 | provide('foo', 'fooVal')
13 | provide('baz', 'bazVal')
14 | },
15 | }
16 |
--------------------------------------------------------------------------------
/packages/vue/examples/slots/app.js:
--------------------------------------------------------------------------------
1 | import { createTextVNode, h } from '../../dist/mini-vue.esm.js'
2 | import { Foo } from './foo.js'
3 |
4 | export const App = {
5 | render() {
6 | return h('div', { id: 'root' }, [
7 | h('h1', null, 'Slots'),
8 | h(Foo, null, {
9 | header: () => h('div', null, 'Slot Header'),
10 | footer: () => h('div', null, 'Slot Footer'),
11 | default: props => [createTextVNode('Slot Content'), h('p', null, `props.msg:${props.msg}`)],
12 | }),
13 | ])
14 | },
15 | setup() {},
16 | }
17 |
--------------------------------------------------------------------------------
/packages/runtime-core/src/index.ts:
--------------------------------------------------------------------------------
1 | export { renderSlot } from './helpers/renderSlot'
2 | export { createTextVNode, createElementVNode } from './vnode'
3 | export { getCurrentInstance } from './component'
4 | export { provide, inject } from './apiInject'
5 | export { h } from './h'
6 | export { createRenderer, RendererOptions } from './renderer'
7 | export { onBeforeMount, onMounted, onBeforeUpdate, onUpdated } from './apiLifecycle'
8 | export { nextTick } from './scheduler'
9 | export { watch, watchEffect } from './apiWatch'
10 | export { toDisplayString } from 'shared/index'
11 |
--------------------------------------------------------------------------------
/packages/compiler-core/src/transform/hoistStatic.ts:
--------------------------------------------------------------------------------
1 | import { isSlotOutlet } from 'compiler-core/utils'
2 | import type { ComponentNode, PlainElementNode, RootNode, TemplateChildNode, TemplateNode } from '../ast'
3 | import { NodeTypes } from '../ast'
4 |
5 | export function isSingleElementRoot(
6 | root: RootNode,
7 | child: TemplateChildNode,
8 | ): child is PlainElementNode | ComponentNode | TemplateNode {
9 | const { children } = root
10 | return (
11 | children.length === 1
12 | && child.type === NodeTypes.ELEMENT
13 | && !isSlotOutlet(child)
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/packages/reactivity/__tests__/shallowReadonly.spec.ts:
--------------------------------------------------------------------------------
1 | import { isReadonly, readonly, shallowReadonly } from '../src/reactive'
2 |
3 | describe('shallowReadonly', () => {
4 | it('should not make non-readonly properties readonly', () => {
5 | const props = shallowReadonly({ n: { foo: 1 } })
6 | expect(isReadonly(props)).toBe(true)
7 | expect(isReadonly(props.n)).toBe(false)
8 | })
9 |
10 | it('should call warn when set', () => {
11 | console.warn = jest.fn()
12 |
13 | const user = readonly({ age: 10 })
14 | user.age = 11
15 | expect(console.warn).toBeCalled()
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/packages/runtime-core/src/componentEmit.ts:
--------------------------------------------------------------------------------
1 | import { camelize, toHandlerKey } from 'shared/index'
2 | import type { ComponentInternalInstance } from './component'
3 |
4 | export type EmitFn = (event: string, ...args: any[]) => void
5 |
6 | export function emit(instance: ComponentInternalInstance, event: string, ...rawArgs: any[]) {
7 | const { props } = instance
8 |
9 | let handlerName
10 | const handler = props[(handlerName = toHandlerKey(event))] || props[(handlerName = toHandlerKey(camelize(event)))]
11 |
12 | if (handler && typeof handler === 'function')
13 | handler && handler(...rawArgs)
14 | }
15 |
--------------------------------------------------------------------------------
/packages/runtime-dom/src/patchProp.ts:
--------------------------------------------------------------------------------
1 | import type { RendererOptions } from 'runtime-core/index'
2 | import { isOn } from 'shared/index'
3 |
4 | type DOMRendererOptions = RendererOptions
5 |
6 | export const patchProp: DOMRendererOptions['patchProp'] = (el, key, prevValue, nextValue) => {
7 | if (isOn(key)) {
8 | const event = key.slice(2).toLocaleLowerCase()
9 | el.addEventListener(event, nextValue)
10 | }
11 | else {
12 | if (nextValue === undefined || nextValue === null)
13 | el.removeAttribute(key)
14 | else
15 | el.setAttribute(key, nextValue)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/shared/src/shapeFlags.ts:
--------------------------------------------------------------------------------
1 | export const enum ShapeFlags {
2 | ELEMENT = 1 /* 00 0000 0001 */,
3 | FUNCTIONAL_COMPONENT = 1 << 1 /* 00 0000 0010 */,
4 | STATEFUL_COMPONENT = 1 << 2 /* 00 0000 0100 */,
5 | TEXT_CHILDREN = 1 << 3 /* 00 0000 1000 */,
6 | ARRAY_CHILDREN = 1 << 4 /* 00 0001 0000 */,
7 | SLOTS_CHILDREN = 1 << 5 /* 00 0010 0000 */,
8 | TELEPORT = 1 << 6 /* 00 0100 0000 */,
9 | SUSPENSE = 1 << 7 /* 00 1000 0000 */,
10 | COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8 /* 01 0000 0000 */,
11 | COMPONENT_KEPT_ALIVE = 1 << 9 /* 10 0000 0000 */,
12 | COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT,
13 | }
14 |
--------------------------------------------------------------------------------
/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`codegen element 1`] = `"return function render(_ctx, _cache){return }"`;
4 |
5 | exports[`codegen interpolation 1`] = `
6 | "const { toDisplayString: _toDisplayString } = Vue
7 | return function render(_ctx, _cache){return _toDisplayString(_ctx.message)}"
8 | `;
9 |
10 | exports[`codegen interpolation 2`] = `
11 | "const { toDisplayString: _toDisplayString } = Vue
12 | return function render(_ctx, _cache){return _toDisplayString(_ctx.message)}"
13 | `;
14 |
15 | exports[`codegen string 1`] = `"return function render(_ctx, _cache){return \\"helloworld\\"}"`;
16 |
--------------------------------------------------------------------------------
/packages/compiler-core/src/transform/transformExpression.ts:
--------------------------------------------------------------------------------
1 | import type { SimpleExpressionNode } from 'compiler-core/ast'
2 | import { NodeTypes } from 'compiler-core/ast'
3 | import type { NodeTransform } from 'compiler-core/transform'
4 | import type { ExpressionNode } from './../ast'
5 |
6 | export const transformExpression: NodeTransform = (node) => {
7 | if (node.type === NodeTypes.INTERPOLATION) {
8 | node.content = processExpression(
9 | node.content as SimpleExpressionNode,
10 | )
11 | }
12 | }
13 |
14 | function processExpression(
15 | node: SimpleExpressionNode,
16 | ): ExpressionNode {
17 | node.content = `_ctx.${node.content}`
18 | return node
19 | }
20 |
--------------------------------------------------------------------------------
/packages/runtime-dom/src/nodeOps.ts:
--------------------------------------------------------------------------------
1 | import type { RendererOptions } from 'runtime-core/index'
2 |
3 | const doc = (typeof document !== 'undefined' ? document : null) as Document
4 |
5 | export const nodeOps: Omit, 'patchProp'> = {
6 | insert: (child, parent, anchor) => {
7 | parent.insertBefore(child, anchor || null)
8 | },
9 |
10 | createElement: (tag: string): Element => {
11 | const el = doc.createElement(tag)
12 |
13 | return el
14 | },
15 |
16 | remove: (child: Node) => {
17 | const parent = child.parentNode
18 | if (parent)
19 | parent.removeChild(child)
20 | },
21 |
22 | setElementText: (el, text) => {
23 | el.textContent = text
24 | },
25 | }
26 |
--------------------------------------------------------------------------------
/packages/runtime-core/src/apiCreateApp.ts:
--------------------------------------------------------------------------------
1 | import type { Component } from 'runtime-core/component'
2 | import type { RootRenderFunction } from 'runtime-core/renderer'
3 | import { createVNode } from './vnode'
4 |
5 | export type App = any
6 |
7 | export type CreateAppFunction = (rootComponent: Component) => App
8 |
9 | export function createAppAPI(render: RootRenderFunction): CreateAppFunction {
10 | return function createApp(rootComponent: Component) {
11 | return {
12 | mount(rootContainer) {
13 | // 创建虚拟节点vnode
14 | const vnode = createVNode(rootComponent)
15 | // 进行渲染
16 | render(vnode, rootContainer)
17 | },
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/vue/examples/provide/baz.js:
--------------------------------------------------------------------------------
1 | import { h, inject } from '../../dist/mini-vue.esm.js'
2 |
3 | export const Baz = {
4 | render() {
5 | return h('div', null, [
6 | h('h4', null, 'Baz Comp'),
7 | h('p', null, `inject foo: ${this.foo}`),
8 | h('p', null, `inject baz: ${this.baz}`),
9 | h('p', null, `inject bar: ${this.bar}`),
10 | ])
11 | },
12 | setup() {
13 | const foo = inject('foo')
14 | const baz = inject('baz')
15 | console.log('inject foo: ', foo)
16 | console.log('inject baz: ', baz)
17 |
18 | const bar = inject('bar', function() {
19 | return this.foo
20 | })
21 | console.log(`inject bar: ${bar}`)
22 |
23 | return {
24 | foo,
25 | baz,
26 | bar,
27 | }
28 | },
29 | }
30 |
--------------------------------------------------------------------------------
/packages/compiler-core/src/runtimeHelpers.ts:
--------------------------------------------------------------------------------
1 | export const FRAGMENT = Symbol('Fragment')
2 | export const OPEN_BLOCK = Symbol('OPEN_BLOCK')
3 | export const CREATE_BLOCK = Symbol('CREATE_BLOCK')
4 | export const CREATE_ELEMENT_BLOCK = Symbol('CREATE_ELEMENT_BLOCK')
5 | export const CREATE_VNODE = Symbol('CREATE_VNODE')
6 | export const CREATE_ELEMENT_VNODE = Symbol('CREATE_ELEMENT_VNODE')
7 | export const TO_DISPLAY_STRING = Symbol('toDisplayString')
8 |
9 | export const helperNameMap: any = {
10 | [FRAGMENT]: 'Fragment',
11 | [OPEN_BLOCK]: 'openBlock',
12 | [CREATE_BLOCK]: 'createBlock',
13 | [CREATE_ELEMENT_BLOCK]: 'createElementBlock',
14 | [CREATE_VNODE]: 'createVNode',
15 | [CREATE_ELEMENT_VNODE]: 'createElementVNode',
16 | [TO_DISPLAY_STRING]: 'toDisplayString',
17 | }
18 |
--------------------------------------------------------------------------------
/packages/vue/examples/components/foo.js:
--------------------------------------------------------------------------------
1 | import { h } from '../../dist/mini-vue.esm.js'
2 |
3 | export const Foo = {
4 | render() {
5 | return h('div', null, [
6 | h('h3', null, 'Foo Comp'),
7 | h('p', null, `props.msg = ${this.msg}`),
8 | h(
9 | 'button',
10 | {
11 | onClick: this.clickHandle,
12 | },
13 | 'button',
14 | ),
15 | ])
16 | },
17 | setup(props, { emit }) {
18 | console.log('props', props)
19 |
20 | // props 只读
21 | props.msg = 'Hi World'
22 |
23 | const clickHandle = () => {
24 | console.log('emit', emit)
25 | emit('clickHandle', 'test1')
26 | emit('click-handle', 'test2')
27 | }
28 |
29 | return {
30 | clickHandle,
31 | }
32 | },
33 | }
34 |
--------------------------------------------------------------------------------
/packages/vue/examples/provide/foo.js:
--------------------------------------------------------------------------------
1 | import { getCurrentInstance, h, inject, provide } from '../../dist/mini-vue.esm.js'
2 | import { Baz } from './baz.js'
3 |
4 | export const Foo = {
5 | render() {
6 | return h('div', null, [
7 | h('h3', null, 'Foo Comp'),
8 | h('p', null, `inject foo: ${this.foo}`),
9 | h('p', null, `inject bar: ${this.bar}`),
10 | h(Baz),
11 | ])
12 | },
13 | setup() {
14 | const instance = getCurrentInstance()
15 | console.log('Foo Instance: ', instance)
16 |
17 | const foo = inject('foo')
18 | console.log('inject foo', foo)
19 |
20 | // 设置默认值
21 | const bar = inject('bar', 'barValue')
22 |
23 | provide('foo', 'fooValue2')
24 |
25 | return {
26 | foo,
27 | bar,
28 | }
29 | },
30 | }
31 |
--------------------------------------------------------------------------------
/packages/reactivity/__tests__/readonly.spec.ts:
--------------------------------------------------------------------------------
1 | import { isProxy, isReadonly, readonly } from '../src/reactive'
2 |
3 | describe('readonly', () => {
4 | it('happy path', () => {
5 | const original = { foo: 1, bar: { baz: 2 } }
6 | const wrapped = readonly(original)
7 | expect(wrapped).not.toBe(original)
8 | expect(isReadonly(wrapped)).toBe(true)
9 | expect(isReadonly(wrapped.bar)).toBe(true)
10 | expect(isProxy(wrapped)).toBe(true)
11 | expect(isProxy(wrapped.bar)).toBe(true)
12 |
13 | expect(isReadonly(original)).toBe(false)
14 | expect(wrapped.foo).toBe(1)
15 | })
16 |
17 | it('should call warn when set', () => {
18 | console.warn = jest.fn()
19 |
20 | const user = readonly({ age: 10 })
21 | user.age = 11
22 | expect(console.warn).toBeCalled()
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/packages/compiler-core/src/compile.ts:
--------------------------------------------------------------------------------
1 | import { isString } from 'shared/index'
2 | import type { RootNode } from './ast'
3 | import { baseParse } from './parse'
4 | import { transform } from './transform'
5 | import type { CodegenResult } from './codegen'
6 | import { generate } from './codegen'
7 | import { transformText } from './transform/transformText'
8 | import { transformElement } from './transform/transformElement'
9 | import { transformExpression } from './transform/transformExpression'
10 | import type { CompilerOptions } from './options'
11 |
12 | export function baseCompile(
13 | template: string | RootNode,
14 | options: CompilerOptions = {},
15 | ): CodegenResult {
16 | const ast = isString(template) ? baseParse(template, options) : template
17 | transform(ast, {
18 | nodeTransforms: [transformExpression, transformElement, transformText],
19 | })
20 | return generate(ast)
21 | }
22 |
--------------------------------------------------------------------------------
/packages/runtime-core/src/componentRenderUtils.ts:
--------------------------------------------------------------------------------
1 | import type { VNode } from 'runtime-core/vnode'
2 | import type { Data } from 'runtime-core/component'
3 |
4 | export function shouldUpdateComponent(prevVNode: VNode, nextVNode: VNode) {
5 | const { props: prevProps } = prevVNode
6 | const { props: nextProps } = nextVNode
7 |
8 | if (prevProps === nextProps) return false
9 |
10 | if (!prevProps) return !nextProps
11 |
12 | if (!nextProps) return true
13 |
14 | return hasPropsChanged(prevProps, nextProps)
15 | }
16 |
17 | function hasPropsChanged(prevProps: Data, nextProps: Data): boolean {
18 | const nextKeys = Object.keys(nextProps)
19 |
20 | if (nextKeys.length !== Object.keys(prevProps).length) return true
21 |
22 | for (let i = 0; i < nextKeys.length; i++) {
23 | const key = nextKeys[i]
24 | if (nextProps[key] !== prevProps[key])
25 | return true
26 | }
27 |
28 | return false
29 | }
30 |
--------------------------------------------------------------------------------
/packages/vue/src/index.ts:
--------------------------------------------------------------------------------
1 | import { baseCompile } from 'compiler-core/index'
2 | import type { CompilerOptions } from 'compiler-core/options'
3 | import type { RenderFunction } from 'runtime-core/component'
4 | import { registerRuntimeCompiler } from 'runtime-core/component'
5 | import { isString } from 'shared/index'
6 | import * as runtimeDom from 'runtime-dom/index'
7 |
8 | function compileToFunction(
9 | template: string | HTMLElement,
10 | options?: CompilerOptions,
11 | ): RenderFunction {
12 | if (!isString(template))
13 | template = template.innerHTML
14 |
15 | const { code } = baseCompile(template, options)
16 |
17 | // eslint-disable-next-line no-new-func
18 | const render = new Function('Vue', code)(runtimeDom) as RenderFunction
19 |
20 | return render
21 | }
22 |
23 | registerRuntimeCompiler(compileToFunction)
24 |
25 | export { compileToFunction as compile }
26 | export * from 'reactivity/index'
27 | export * from 'runtime-dom/index'
28 |
--------------------------------------------------------------------------------
/packages/compiler-core/src/utils.ts:
--------------------------------------------------------------------------------
1 | import type { InterpolationNode, RootNode, SlotOutletNode, TemplateChildNode, TextNode } from './ast'
2 | import { ElementTypes, NodeTypes } from './ast'
3 | import { CREATE_BLOCK, CREATE_ELEMENT_BLOCK, CREATE_ELEMENT_VNODE, CREATE_VNODE } from './runtimeHelpers'
4 |
5 | export function isSlotOutlet(
6 | node: RootNode | TemplateChildNode,
7 | ): node is SlotOutletNode {
8 | return node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT
9 | }
10 |
11 | export function getVNodeHelper(isComponent: boolean) {
12 | return isComponent ? CREATE_VNODE : CREATE_ELEMENT_VNODE
13 | }
14 |
15 | export function getVNodeBlockHelper(isComponent: boolean) {
16 | return isComponent ? CREATE_BLOCK : CREATE_ELEMENT_BLOCK
17 | }
18 |
19 | export function isText(
20 | node: TemplateChildNode,
21 | ): node is TextNode | InterpolationNode {
22 | return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
23 | }
24 |
--------------------------------------------------------------------------------
/packages/vue/examples/helloworld/app.js:
--------------------------------------------------------------------------------
1 | import { h } from '../../dist/mini-vue.esm.js'
2 |
3 | window.self = null
4 |
5 | export const App = {
6 | render() {
7 | window.self = this
8 | return h(
9 | 'div',
10 | {
11 | id: 'root',
12 | class: ['pages'],
13 | },
14 | [
15 | h('h1', { class: 'text-1' }, 'Hello World'),
16 | h('h3', { style: 'color: #666' }, this.msg),
17 | h('input', {
18 | placeholder: 'input something',
19 | onInput(e) {
20 | console.log('input keywords: ', e.target.value)
21 | },
22 | }),
23 | h(
24 | 'button',
25 | {
26 | onClick() {
27 | console.log('click events')
28 | },
29 | },
30 | 'test button',
31 | ),
32 | ],
33 | )
34 | },
35 | setup() {
36 | return {
37 | msg: 'This is mini-vue',
38 | }
39 | },
40 | }
41 |
--------------------------------------------------------------------------------
/packages/reactivity/__tests__/reactive.spec.ts:
--------------------------------------------------------------------------------
1 | import { isProxy, isReactive, reactive } from '../src/reactive'
2 |
3 | describe('reactive', () => {
4 | it('happy path', () => {
5 | const original = { foo: 1 }
6 | const observed = reactive(original)
7 |
8 | expect(observed).not.toBe(original)
9 | expect(observed.foo).toBe(1)
10 | expect(isReactive(observed)).toBe(true)
11 | expect(isReactive(original)).toBe(false)
12 | expect(isProxy(observed)).toBe(true)
13 | expect(isProxy(original)).toBe(false)
14 | })
15 |
16 | it('nested reactive', () => {
17 | const original = {
18 | nested: {
19 | foo: 1,
20 | },
21 | array: [{ bar: 2 }],
22 | }
23 | const observed = reactive(original)
24 | expect(isReactive(observed)).toBe(true)
25 | expect(isReactive(observed.nested)).toBe(true)
26 | expect(isReactive(observed.array)).toBe(true)
27 | expect(isReactive(observed.array[0])).toBe(true)
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mini-vue3",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "jest --watch",
8 | "example": "live-server packages/vue",
9 | "build": "rollup -c -w --sourcemap rollup.config.js",
10 | "dev": "pnpm build & pnpm example",
11 | "format": "eslint \"**/*.{vue,ts,js}\" --fix",
12 | "lint": "eslint \"**/*.{vue,ts,js}\""
13 | },
14 | "devDependencies": {
15 | "@babel/core": "^7.16.0",
16 | "@babel/preset-env": "^7.16.4",
17 | "@babel/preset-typescript": "^7.16.0",
18 | "@ouduidui/eslint-config-ts": "^0.0.2",
19 | "@rollup/plugin-typescript": "^8.3.0",
20 | "@types/jest": "^27.0.3",
21 | "@types/rewire": "^2.5.28",
22 | "babel-jest": "^27.3.1",
23 | "eslint": "^8.11.0",
24 | "jest": "^27.3.1",
25 | "live-server": "^1.2.1",
26 | "rewire": "^6.0.0",
27 | "rollup": "^2.60.0",
28 | "tslib": "^2.3.1",
29 | "typescript": "^4.5.2"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/runtime-core/src/componentPublicInstance.ts:
--------------------------------------------------------------------------------
1 | import { hasOwn } from 'shared/index'
2 | import { shallowReadonly } from 'reactivity/reactive'
3 | import type { ComponentInternalInstance } from './component'
4 |
5 | export interface ComponentRenderContext {
6 | [key: string]: any
7 | _: ComponentInternalInstance
8 | }
9 |
10 | export type PublicPropertiesMap = Record any>
11 |
12 | const publicPropertiesMap: PublicPropertiesMap = {
13 | $el: i => i.vnode.el,
14 | $slots: i => shallowReadonly(i.slots),
15 | }
16 |
17 | export const PublicInstanceProxyHandlers: ProxyHandler = {
18 | get({ _: instance }: ComponentRenderContext, key: string) {
19 | const { setupState, props } = instance
20 |
21 | if (hasOwn(setupState, key))
22 | return setupState[key]
23 | else if (hasOwn(props, key))
24 | return props[key]
25 |
26 | const publicGetter = publicPropertiesMap[key]
27 | if (publicGetter)
28 | return publicGetter(instance)
29 | },
30 | }
31 |
--------------------------------------------------------------------------------
/packages/vue/examples/lifecycle/app.js:
--------------------------------------------------------------------------------
1 | import { h, nextTick, onBeforeMount, onBeforeUpdate, onMounted, onUpdated, ref } from '../../dist/mini-vue.esm.js'
2 |
3 | export const App = {
4 | render() {
5 | window.self = this
6 | return h(
7 | 'div',
8 | {
9 | id: 'root',
10 | class: ['pages'],
11 | },
12 | [h('h1', null, 'LifeCycle'), h('h4', null, this.msg)],
13 | )
14 | },
15 | setup() {
16 | const msg = ref('')
17 |
18 | onBeforeMount(() => {
19 | console.log('onBeforeMount')
20 | })
21 |
22 | onMounted(() => {
23 | nextTick(() => {
24 | console.log('nextTick')
25 | })
26 | console.log('onMounted')
27 | setTimeout(() => {
28 | msg.value = 'HelloWorld'
29 | }, 1000)
30 | })
31 |
32 | onBeforeUpdate(() => {
33 | console.log('onBeforeUpdate')
34 | })
35 |
36 | onUpdated(() => {
37 | console.log('onUpdated')
38 | })
39 |
40 | return {
41 | msg,
42 | }
43 | },
44 | }
45 |
--------------------------------------------------------------------------------
/packages/runtime-core/src/componentSlots.ts:
--------------------------------------------------------------------------------
1 | import type { VNode } from 'runtime-core/vnode'
2 | import { ShapeFlags, isArray, isFunction } from 'shared/index'
3 | import type { ComponentInternalInstance } from './component'
4 | import type { VNodeNormalizedChildren } from './vnode'
5 |
6 | export type Slot = (...args: any[]) => VNode[]
7 |
8 | export type Slots = Readonly
9 |
10 | export type InternalSlots = Record
11 |
12 | export type RawSlots = Record
13 |
14 | const normalizeSlotValue = (value: unknown): VNode[] => (isArray(value) ? value : [value])
15 |
16 | function normalizeObjectSlots(rawSlots: RawSlots, slots: InternalSlots) {
17 | for (const key in rawSlots) {
18 | const value = rawSlots[key]
19 | if (isFunction(value))
20 | slots[key] = props => normalizeSlotValue(value(props))
21 | }
22 | }
23 |
24 | export function initSlots(instance: ComponentInternalInstance, children: VNodeNormalizedChildren) {
25 | if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN)
26 | normalizeObjectSlots(children as unknown as RawSlots, (instance.slots = {}))
27 | }
28 |
--------------------------------------------------------------------------------
/packages/runtime-core/src/apiInject.ts:
--------------------------------------------------------------------------------
1 | import { currentInstance } from 'runtime-core/component'
2 | import { isFunction } from 'shared/index'
3 |
4 | export interface InjectionKey extends Symbol {}
5 |
6 | export function provide(key: InjectionKey | string | number, value: T) {
7 | if (currentInstance) {
8 | let provides = currentInstance.provides
9 | const parentProvides = currentInstance.parent && currentInstance.parent.provides
10 |
11 | // 初始化的时候,也就是组件第一次调用provide的时候,绑定通过原型链的方式绑定父级provide
12 | if (provides === parentProvides)
13 | provides = currentInstance.provides = Object.create(parentProvides)
14 |
15 | provides[key as string] = value
16 | }
17 | }
18 |
19 | export function inject(key: InjectionKey | string, defaultValue?: unknown) {
20 | const instance = currentInstance
21 | if (instance) {
22 | const provides = instance.parent && instance.provides
23 |
24 | if (provides && (key as string | symbol) in provides)
25 | return provides[key as string]
26 | else if (defaultValue)
27 | return isFunction(defaultValue) ? defaultValue.call(instance.proxy) : defaultValue
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/runtime-core/src/apiLifecycle.ts:
--------------------------------------------------------------------------------
1 | import type { ComponentInternalInstance } from 'runtime-core/component'
2 | import {
3 | LifecycleHooks,
4 | currentInstance,
5 | setCurrentInstance,
6 | unsetCurrentInstance,
7 | } from 'runtime-core/component'
8 | import { pauseTrack, resetTracking } from 'reactivity/effect'
9 |
10 | function injectHook(type: LifecycleHooks, hook: Function, target: ComponentInternalInstance | null = currentInstance) {
11 | if (target) {
12 | const hooks = target[type] || (target[type] = [])
13 | hooks.push(() => {
14 | pauseTrack()
15 | setCurrentInstance(target)
16 | hook()
17 | unsetCurrentInstance()
18 | resetTracking()
19 | })
20 | }
21 | }
22 |
23 | export const createHook
24 | = any>(lifecycle: LifecycleHooks) =>
25 | (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
26 | injectHook(lifecycle, hook, target)
27 |
28 | export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
29 | export const onMounted = createHook(LifecycleHooks.MOUNTED)
30 | export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
31 | export const onUpdated = createHook(LifecycleHooks.UPDATED)
32 |
--------------------------------------------------------------------------------
/packages/vue/examples/watch/app.js:
--------------------------------------------------------------------------------
1 | import { computed, h, reactive, ref, watch, watchEffect } from '../../dist/mini-vue.esm.js'
2 |
3 | window.self = null
4 |
5 | export const App = {
6 | render() {
7 | return h(
8 | 'div',
9 | {
10 | id: 'root',
11 | class: ['pages'],
12 | },
13 | [
14 | h('h1', null, 'Watch and Computed'),
15 | h('h3', null, `count: ${this.count}`),
16 | h('h3', null, `doubleCount: ${this.doubleCount}`),
17 | h('button', { onClick: this.add }, 'add'),
18 | ],
19 | )
20 | },
21 | setup() {
22 | const count = ref(0)
23 | const doubleCount = computed(() => 2 * count.value)
24 |
25 | const obj = reactive({
26 | count: 0,
27 | })
28 |
29 | const add = () => {
30 | count.value++
31 | obj.count++
32 | }
33 |
34 | watch(count, (val, oldVal) => {
35 | console.log('watch count: ', val, oldVal)
36 | })
37 |
38 | watch(obj, (val, oldVal) => {
39 | console.log('watch obj: ', val, oldVal)
40 | })
41 |
42 | watchEffect(() => {
43 | console.log('watchEffect: ', count.value)
44 | })
45 |
46 | return {
47 | count,
48 | doubleCount,
49 | add,
50 | }
51 | },
52 | }
53 |
--------------------------------------------------------------------------------
/packages/compiler-core/src/transform/transformElement.ts:
--------------------------------------------------------------------------------
1 | import type { TemplateTextChildNode, VNodeCall } from 'compiler-core/ast'
2 | import { NodeTypes, createVNodeCall } from 'compiler-core/ast'
3 | import type { NodeTransform } from 'compiler-core/transform'
4 | import { CREATE_ELEMENT_VNODE } from './../runtimeHelpers'
5 |
6 | export const transformElement: NodeTransform = (node, context) => {
7 | if (node.type === NodeTypes.ELEMENT) {
8 | return () => {
9 | context.helper(CREATE_ELEMENT_VNODE)
10 |
11 | const { tag, props } = node
12 | let vnodeChildren: VNodeCall['children']
13 | // tag
14 | const vnodeTag = `'${tag}'`
15 |
16 | // props
17 | const vnodeProps: VNodeCall['props'] = props
18 |
19 | // children
20 | const children = node.children
21 | if (children.length > 0) {
22 | const child = children[0]
23 | const type = child.type
24 |
25 | if (type === NodeTypes.TEXT)
26 | vnodeChildren = child as TemplateTextChildNode
27 | else
28 | vnodeChildren = children
29 | }
30 |
31 | node.codegenNode = createVNodeCall(
32 | context,
33 | vnodeTag,
34 | vnodeProps,
35 | vnodeChildren,
36 | )
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/packages/compiler-core/src/transform/transformText.ts:
--------------------------------------------------------------------------------
1 | import type { CompoundExpressionNode } from 'compiler-core/ast'
2 | import { NodeTypes } from 'compiler-core/ast'
3 | import type { NodeTransform } from 'compiler-core/transform'
4 | import { isText } from 'compiler-core/utils'
5 |
6 | export const transformText: NodeTransform = (node) => {
7 | if (node.type === NodeTypes.ELEMENT) {
8 | return () => {
9 | const { children } = node
10 | let currentContainer: CompoundExpressionNode | undefined
11 |
12 | for (let i = 0; i < children.length; i++) {
13 | const child = children[i]
14 |
15 | if (isText(child)) {
16 | for (let j = i + 1; j < children.length; j++) {
17 | const next = children[j]
18 | if (isText(next)) {
19 | if (!currentContainer) {
20 | currentContainer = children[i] = {
21 | type: NodeTypes.COMPOUND_EXPRESSION,
22 | children: [child],
23 | }
24 | }
25 | currentContainer.children.push(' + ', next)
26 | children.splice(j, 1)
27 | j--
28 | }
29 | else {
30 | currentContainer = undefined
31 | break
32 | }
33 | }
34 | }
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/reactivity/__tests__/computed.spec.ts:
--------------------------------------------------------------------------------
1 | import { reactive } from '../src/reactive'
2 | import { computed } from '../src/computed'
3 | import { isRef, ref } from '../src/ref'
4 |
5 | describe('computed', () => {
6 | it('happy path', () => {
7 | const user = reactive({
8 | age: 1,
9 | })
10 |
11 | const age = computed(() => {
12 | return user.age
13 | })
14 |
15 | expect(age.value).toBe(1)
16 | expect(isRef(age)).toBe(true)
17 | })
18 |
19 | it('should compute lazily', () => {
20 | const value = reactive({
21 | foo: 1,
22 | })
23 | const getter = jest.fn(() => {
24 | return value.foo
25 | })
26 | const cValue = computed(getter)
27 |
28 | // lazy
29 | expect(getter).not.toHaveBeenCalled()
30 |
31 | expect(cValue.value).toBe(1)
32 | expect(getter).toHaveBeenCalledTimes(1)
33 |
34 | expect(cValue.value).toBe(1)
35 | expect(getter).toHaveBeenCalledTimes(1)
36 |
37 | value.foo = 2
38 | expect(getter).toHaveBeenCalledTimes(1)
39 | expect(cValue.value).toBe(2)
40 | expect(getter).toHaveBeenCalledTimes(2)
41 | })
42 |
43 | it('should support setter', () => {
44 | const n = ref(1)
45 | const plusOne = computed({
46 | get: () => n.value + 1,
47 | set: (val) => {
48 | n.value = val - 1
49 | },
50 | })
51 |
52 | expect(plusOne.value).toBe(2)
53 | n.value++
54 | expect(plusOne.value).toBe(3)
55 |
56 | plusOne.value = 0
57 | expect(n.value).toBe(-1)
58 | })
59 | })
60 |
--------------------------------------------------------------------------------
/packages/compiler-core/__tests__/transform.spec.ts:
--------------------------------------------------------------------------------
1 | import type { TemplateNode, TextNode } from 'compiler-core/ast'
2 | import { NodeTypes } from 'compiler-core/ast'
3 | import { baseParse } from 'compiler-core/parse'
4 | import { transform } from 'compiler-core/transform'
5 |
6 | describe('transform', () => {
7 | it('happy path', () => {
8 | const ast = baseParse('Hello {{message}}
')
9 | const plugin = (node) => {
10 | if (node.type === NodeTypes.TEXT)
11 | node.content += 'World'
12 | }
13 | transform(ast, {
14 | nodeTransforms: [plugin],
15 | })
16 |
17 | const nodeText = (ast.children[0] as TemplateNode).children[0] as TextNode
18 | expect(nodeText.content).toBe('Hello World')
19 | })
20 | })
21 |
22 | describe('codegenNode', () => {
23 | it('string', () => {
24 | const ast = baseParse('HelloWorld')
25 | transform(ast, {})
26 | expect(ast.codegenNode).toStrictEqual({
27 | content: 'HelloWorld',
28 | type: 2,
29 | })
30 | })
31 |
32 | it('interpolation', () => {
33 | const ast = baseParse('Hello {{message}}
')
34 | transform(ast, {})
35 | expect(ast.codegenNode).toStrictEqual({
36 | children: [
37 | {
38 | content: 'Hello ',
39 | type: 2,
40 | },
41 | {
42 | content: {
43 | content: 'message',
44 | type: 4,
45 | },
46 | type: 5,
47 | },
48 | ],
49 | isSelfClosing: false,
50 | props: [],
51 | tag: 'div',
52 | tagType: 0,
53 | type: 1,
54 | codegenNode: undefined,
55 | })
56 | })
57 | })
58 |
--------------------------------------------------------------------------------
/packages/vue/examples/patchChildren/app.js:
--------------------------------------------------------------------------------
1 | import { h, ref } from '../../dist/mini-vue.esm.js'
2 | import ArrayToText from './ArrayToText.js'
3 | import TextToText from './TextToText.js'
4 | import TextToArray from './TextToArray.js'
5 | import generateArrayTpArrayComps from './ArrayToArray.js'
6 |
7 | export const App = {
8 | render() {
9 | return h('div', { id: 'root' }, [
10 | h('h1', null, 'Patch Children'),
11 |
12 | h('h3', null, 'ArrayToText'),
13 | h(ArrayToText, { isChange: this.isChange }),
14 |
15 | h('h3', null, 'TextToText'),
16 | h(TextToText, { isChange: this.isChange }),
17 |
18 | h('h3', null, 'TextToArray'),
19 | h(TextToArray, { isChange: this.isChange }),
20 |
21 | h('h3', null, 'ArrayToArray'),
22 | h('h4', null, 'AB -> ABC'),
23 | h(generateArrayTpArrayComps(0), { isChange: this.isChange }),
24 | h('h4', null, 'BC -> ABC'),
25 | h(generateArrayTpArrayComps(1), { isChange: this.isChange }),
26 | h('h4', null, 'ABC -> AB'),
27 | h(generateArrayTpArrayComps(2), { isChange: this.isChange }),
28 | h('h4', null, 'ABC -> BC'),
29 | h(generateArrayTpArrayComps(3), { isChange: this.isChange }),
30 | h('h4', null, 'ABCEDFG -> ABECFG'),
31 | h(generateArrayTpArrayComps(4), { isChange: this.isChange }),
32 |
33 | h(
34 | 'button',
35 | {
36 | onClick: this.changeHandle,
37 | },
38 | 'update',
39 | ),
40 | ])
41 | },
42 | setup() {
43 | const isChange = ref(false)
44 |
45 | const changeHandle = () => (isChange.value = true)
46 |
47 | return { isChange, changeHandle }
48 | },
49 | }
50 |
--------------------------------------------------------------------------------
/packages/vue/examples/update/app.js:
--------------------------------------------------------------------------------
1 | import { h, ref } from '../../dist/mini-vue.esm.js'
2 |
3 | export const App = {
4 | render() {
5 | console.log(this.props)
6 | return h(
7 | 'div',
8 | {
9 | ...this.props,
10 | },
11 | [
12 | h('h1', null, 'Update'),
13 | h('h3', null, 'Update Children'),
14 | h('p', null, `count: ${this.count}`),
15 | h(
16 | 'button',
17 | {
18 | onClick: this.addHandle,
19 | },
20 | 'add',
21 | ),
22 |
23 | h('h3', null, 'Update Props'),
24 | h('p', null, 'HelloWorld'),
25 | h(
26 | 'button',
27 | {
28 | onClick: this.changePropsStyle,
29 | },
30 | 'changePropsStyle',
31 | ),
32 | h(
33 | 'button',
34 | {
35 | onClick: this.clearPropsClass,
36 | },
37 | 'clearPropsClass',
38 | ),
39 | h(
40 | 'button',
41 | {
42 | onClick: this.resetProps,
43 | },
44 | 'resetProps',
45 | ),
46 | ],
47 | )
48 | },
49 | setup() {
50 | const count = ref(0)
51 | const addHandle = () => {
52 | count.value++
53 | console.log(`count: ${count.value}`)
54 | }
55 |
56 | const props = ref({
57 | style: 'color: red',
58 | class: 'underline',
59 | })
60 |
61 | const changePropsStyle = () => (props.value.style = 'color: blue')
62 | const clearPropsClass = () => (props.value.class = undefined)
63 | const resetProps = () => (props.value = { style: 'color: red' })
64 |
65 | return {
66 | count,
67 | props,
68 | addHandle,
69 | changePropsStyle,
70 | clearPropsClass,
71 | resetProps,
72 | }
73 | },
74 | }
75 |
--------------------------------------------------------------------------------
/packages/reactivity/src/computed.ts:
--------------------------------------------------------------------------------
1 | import { NOOP, isFunction } from 'shared/index'
2 | import { ReactiveEffect } from './effect'
3 |
4 | export type ComputedGetter = (...args: any[]) => T
5 | export type ComputedSetter = (v: T) => void
6 |
7 | export interface WritableComputedOptions {
8 | get: ComputedGetter
9 | set: ComputedSetter
10 | }
11 |
12 | class ComputedRefImpl {
13 | private readonly _setter
14 | public readonly effect: ReactiveEffect
15 | private _dirty = true // 为true的话代表需要更新数据
16 | private _value!: T // 保存缓存值
17 | public readonly __v_isRef = true
18 |
19 | constructor(getter: ComputedGetter, setter: ComputedSetter) {
20 | this._setter = setter
21 |
22 | // 新建ReactiveEffect示例,并且配置scheduler函数,避免响应式数据更新时调用run,从而实现computed缓存特性
23 | this.effect = new ReactiveEffect(getter, () => {
24 | // 将_dirty设置为true,代表下次调用computed值时需要更新数据
25 | if (!this._dirty)
26 | this._dirty = true
27 | })
28 | }
29 |
30 | get value() {
31 | if (this._dirty) {
32 | // 更新value
33 | this._dirty = false
34 | this._value = this.effect.run()
35 | }
36 | return this._value
37 | }
38 |
39 | set value(newValue: T) {
40 | this._setter(newValue)
41 | }
42 | }
43 |
44 | /**
45 | * 计算属性
46 | * @param getterOrOptions
47 | */
48 | export function computed(getterOrOptions: ComputedGetter | WritableComputedOptions) {
49 | let getter: ComputedGetter
50 | let setter: ComputedSetter
51 |
52 | // 判断getterOrOptions是getter还是options
53 | const onlyGetter = isFunction(getterOrOptions)
54 | if (onlyGetter) {
55 | getter = getterOrOptions as ComputedGetter
56 | setter = NOOP
57 | }
58 | else {
59 | getter = (getterOrOptions as WritableComputedOptions).get
60 | setter = (getterOrOptions as WritableComputedOptions).set
61 | }
62 |
63 | return new ComputedRefImpl(getter, setter)
64 | }
65 |
--------------------------------------------------------------------------------
/packages/reactivity/__tests__/ref.spec.ts:
--------------------------------------------------------------------------------
1 | import { effect } from '../src/effect'
2 | import { isRef, proxyRefs, ref, unref } from '../src/ref'
3 | import { reactive } from '../src/reactive'
4 |
5 | describe('ref', () => {
6 | it('happy path', () => {
7 | const a = ref(1)
8 | expect(a.value).toBe(1)
9 | })
10 |
11 | it('should be reactive', () => {
12 | const a = ref(1)
13 | let dummy
14 | let calls = 0
15 | effect(() => {
16 | calls++
17 | dummy = a.value
18 | })
19 |
20 | expect(calls).toBe(1)
21 | expect(dummy).toBe(1)
22 |
23 | a.value = 2
24 | expect(calls).toBe(2)
25 | expect(dummy).toBe(2)
26 |
27 | a.value = 2
28 | expect(calls).toBe(2)
29 | expect(dummy).toBe(2)
30 | })
31 |
32 | it('should make nested properties reactive', () => {
33 | const a = ref({
34 | count: 1,
35 | })
36 | let dummy
37 | effect(() => {
38 | dummy = a.value.count
39 | })
40 |
41 | expect(dummy).toBe(1)
42 | a.value.count = 2
43 | expect(dummy).toBe(2)
44 | })
45 |
46 | it('isRef', () => {
47 | const a = ref(1)
48 | const user = reactive({
49 | age: 1,
50 | })
51 | expect(isRef(a)).toBe(true)
52 | expect(isRef(1)).toBe(false)
53 | expect(isRef(user)).toBe(false)
54 | })
55 |
56 | it('unRef', () => {
57 | const a = ref(1)
58 |
59 | expect(unref(a)).toBe(1)
60 | expect(unref(1)).toBe(1)
61 | })
62 |
63 | it('proxyRefs', () => {
64 | const user = {
65 | age: ref(10),
66 | name: 'ouduidui',
67 | }
68 | const proxyUser = proxyRefs(user)
69 | expect(user.age.value).toBe(10)
70 | expect(proxyUser.age).toBe(10)
71 | expect(proxyUser.name).toBe('ouduidui')
72 |
73 | proxyUser.age = 20
74 | expect(proxyUser.age).toBe(20)
75 | expect(user.age.value).toBe(20)
76 |
77 | proxyUser.age = ref(10)
78 | expect(proxyUser.age).toBe(10)
79 | expect(user.age.value).toBe(10)
80 | })
81 | })
82 |
--------------------------------------------------------------------------------
/packages/vue/examples/patchChildren/ArrayToArray.js:
--------------------------------------------------------------------------------
1 | import { h } from '../../dist/mini-vue.esm.js'
2 |
3 | const demos = [
4 | // AB -> ABC
5 | {
6 | prevChildren: [h('span', { key: 'A' }, 'A'), h('span', { key: 'B' }, 'B')],
7 | nextChildren: [h('span', { key: 'A' }, 'A'), h('span', { key: 'B' }, 'B'), h('span', { key: 'C' }, 'C')],
8 | },
9 | // BC -> ABC
10 | {
11 | prevChildren: [h('span', { key: 'B' }, 'B'), h('span', { key: 'C' }, 'C')],
12 | nextChildren: [h('span', { key: 'A' }, 'A'), h('span', { key: 'B' }, 'B'), h('span', { key: 'C' }, 'C')],
13 | },
14 |
15 | // ABC -> AB
16 | {
17 | prevChildren: [h('span', { key: 'A' }, 'A'), h('span', { key: 'B' }, 'B'), h('span', { key: 'C' }, 'C')],
18 | nextChildren: [h('span', { key: 'A' }, 'A'), h('span', { key: 'B' }, 'B')],
19 | },
20 |
21 | // ABC -> BC
22 | {
23 | prevChildren: [h('span', { key: 'A' }, 'A'), h('span', { key: 'B' }, 'B'), h('span', { key: 'C' }, 'C')],
24 | nextChildren: [h('span', { key: 'B' }, 'B'), h('span', { key: 'C' }, 'C')],
25 | },
26 |
27 | // ABCEDFG -> ABECFG
28 | {
29 | prevChildren: [
30 | h('span', { key: 'A' }, 'A'),
31 | h('span', { key: 'B' }, 'B'),
32 | h('span', { key: 'C' }, 'C'),
33 | h('span', { key: 'E' }, 'E'),
34 | h('span', { key: 'D' }, 'D'),
35 | h('span', { key: 'F' }, 'F'),
36 | h('span', { key: 'G' }, 'G'),
37 | ],
38 | nextChildren: [
39 | h('span', { key: 'A' }, 'A'),
40 | h('span', { key: 'B' }, 'B'),
41 | h('span', { key: 'E' }, 'E'),
42 | h('span', { key: 'C' }, 'C'),
43 | h('span', { key: 'F' }, 'F'),
44 | h('span', { key: 'G' }, 'G'),
45 | ],
46 | },
47 | ]
48 |
49 | const generateArrayTpArrayComps = (idx) => {
50 | return {
51 | setup() {},
52 | render() {
53 | const children = demos[idx]
54 |
55 | return this.isChange ? h('div', null, children.nextChildren) : h('div', null, children.prevChildren)
56 | },
57 | }
58 | }
59 |
60 | export default generateArrayTpArrayComps
61 |
--------------------------------------------------------------------------------
/packages/compiler-core/__tests__/codegen.spec.ts:
--------------------------------------------------------------------------------
1 | import { transform } from 'compiler-core/transform'
2 | import { generate } from 'compiler-core/codegen'
3 | import { baseParse } from 'compiler-core/parse'
4 | import { transformExpression } from 'compiler-core/transform/transformExpression'
5 | import { transformElement } from 'compiler-core/transform/transformElement'
6 | import { transformText } from 'compiler-core/transform/transformText'
7 |
8 | describe('codegen', () => {
9 | it('string', () => {
10 | const ast = baseParse('helloworld')
11 | transform(ast, {})
12 | const { code } = generate(ast)
13 | expect(code).toMatchInlineSnapshot('"return function render(_ctx, _cache){return \\"helloworld\\"}"')
14 | })
15 |
16 | it('interpolation', () => {
17 | const ast = baseParse('{{message}}')
18 | transform(ast, {
19 | nodeTransforms: [transformExpression],
20 | })
21 | const { code } = generate(ast)
22 | expect(code).toMatchInlineSnapshot(`
23 | "const { toDisplayString: _toDisplayString } = Vue
24 | return function render(_ctx, _cache){return _toDisplayString(_ctx.message)}"
25 | `)
26 | })
27 |
28 | it('element', () => {
29 | const ast = baseParse('')
30 | transform(ast, {
31 | nodeTransforms: [transformElement],
32 | })
33 | const { code } = generate(ast)
34 | expect(code).toMatchInlineSnapshot(`
35 | "const { createElementVNode: _createElementVNode } = Vue
36 | return function render(_ctx, _cache){return _createElementVNode('div',[])}"
37 | `)
38 | })
39 |
40 | it('complex element', () => {
41 | const ast = baseParse('Hi, {{message}}
')
42 | transform(ast, {
43 | nodeTransforms: [transformExpression, transformElement, transformText],
44 | })
45 |
46 | const { code } = generate(ast)
47 | expect(code).toMatchInlineSnapshot(`
48 | "const { toDisplayString: _toDisplayString,createElementVNode: _createElementVNode } = Vue
49 | return function render(_ctx, _cache){return _createElementVNode('div',[],[\\"Hi, \\" + _toDisplayString(_ctx.message)])}"
50 | `)
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/packages/reactivity/src/reactive.ts:
--------------------------------------------------------------------------------
1 | import { isObject } from 'shared/index'
2 | import { mutableHandlers, readonlyHandlers, shallowReactiveHandlers, shallowReadonlyHandlers } from './baseHandlers'
3 |
4 | export const enum ReactiveFlags {
5 | IS_REACTIVE = '__v_isReactive',
6 | IS_READONLY = '__v_isReadonly',
7 | }
8 |
9 | /**
10 | * 生成响应式对象
11 | * @param target
12 | */
13 | export function reactive(target: T) {
14 | return createReactiveObject(target, mutableHandlers)
15 | }
16 |
17 | /**
18 | * 生成浅响应式对象
19 | * @param target
20 | */
21 | export function shallowReactive(target: T) {
22 | return createReactiveObject(target, shallowReactiveHandlers)
23 | }
24 |
25 | /**
26 | * 生成只读对象
27 | * @param target
28 | */
29 | export function readonly(target: T) {
30 | return createReactiveObject(target, readonlyHandlers)
31 | }
32 |
33 | /**
34 | * 生成浅只读对象
35 | * @param target
36 | */
37 | export function shallowReadonly(target: T) {
38 | return createReactiveObject(target, shallowReadonlyHandlers)
39 | }
40 |
41 | /**
42 | * 创建Proxy
43 | * @param target
44 | * @param baseHandlers proxy处理器
45 | */
46 | function createReactiveObject(target: object, baseHandlers: ProxyHandler) {
47 | return new Proxy(target, baseHandlers)
48 | }
49 |
50 | /**
51 | * 判断是否为响应式对象
52 | * @param value
53 | */
54 | export function isReactive(value: any): boolean {
55 | return !!(value && value[ReactiveFlags.IS_REACTIVE])
56 | }
57 |
58 | /**
59 | * 判断是否为只读对象
60 | * @param value
61 | */
62 | export function isReadonly(value: any): boolean {
63 | return !!(value && value[ReactiveFlags.IS_READONLY])
64 | }
65 |
66 | /**
67 | * 判断是否由reactive或readonly生成的proxy
68 | * @param value
69 | */
70 | export function isProxy(value: any): boolean {
71 | return isReactive(value) || isReadonly(value)
72 | }
73 |
74 | /**
75 | * 判断是否为对象,是的话进行响应式处理
76 | * @param value
77 | */
78 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
79 | export const toReactive = (value: T): T => (isObject(value) ? reactive(value) : value)
80 |
--------------------------------------------------------------------------------
/packages/reactivity/src/baseHandlers.ts:
--------------------------------------------------------------------------------
1 | import { extend, isObject } from 'shared/index'
2 | import { track, trigger } from './effect'
3 | import { ReactiveFlags, reactive, readonly } from './reactive'
4 |
5 | const get = createGetter() // 响应式
6 | const shallowGet = createGetter(false, true) // 浅响应式
7 | const readonlyGet = createGetter(true) // 只读
8 | const shallowReadonlyGet = createGetter(true, true) // 浅只读
9 |
10 | /**
11 | * 创建Proxy的get处理函数
12 | * @param isReadonly {boolean} 是否只读
13 | * @param shallow {boolean} 是否浅处理
14 | */
15 | function createGetter(isReadonly = false, shallow = false) {
16 | return function get(target, key) {
17 | // 用于isReactive方法,判断是否为reactive
18 | if (key === ReactiveFlags.IS_REACTIVE)
19 | return !isReadonly
20 |
21 | if (key === ReactiveFlags.IS_READONLY)
22 | return isReadonly
23 |
24 | // 获取对应结果
25 | const res = Reflect.get(target, key)
26 |
27 | if (!isReadonly) {
28 | // 只读情况下不需要依赖收集
29 | track(target, key) // 依赖收集
30 | }
31 |
32 | // 浅处理无需只想下列的递归处理
33 | if (shallow)
34 | return res
35 |
36 | // 如果res是对象的话,再次进行处理
37 | if (isObject(res))
38 | return isReadonly ? readonly(res) : reactive(res)
39 |
40 | // 将结果返回出去
41 | return res
42 | }
43 | }
44 |
45 | const set = createSetter() // 响应式
46 |
47 | /**
48 | * 创建Proxy的set处理函数
49 | */
50 | function createSetter() {
51 | return function set(target, key, value) {
52 | // 执行set操作,并获取新的value值
53 | const res = Reflect.set(target, key, value)
54 |
55 | // 触发依赖
56 | trigger(target, key)
57 |
58 | // 将结果返回
59 | return res
60 | }
61 | }
62 |
63 | // 可变的Proxy的handler
64 | export const mutableHandlers: ProxyHandler