├── src ├── index.ts ├── reactivity │ ├── src │ │ ├── index.ts │ │ ├── computed.ts │ │ ├── reactive.ts │ │ ├── ref.ts │ │ ├── baseHandlers.ts │ │ └── effect.ts │ └── __tests__ │ │ ├── shallowReactive.spec.ts │ │ ├── shallowReadonly.spec.ts │ │ ├── reactive.spec.ts │ │ ├── computed.spec.ts │ │ ├── readonly.spec.ts │ │ ├── ref.spec.ts │ │ └── effect.spec.ts ├── runtime-core │ ├── componentProps.ts │ ├── h.ts │ ├── index.ts │ ├── componentEmit.ts │ ├── helpers │ │ └── renderSlots.ts │ ├── componentUpdateUtils.ts │ ├── createApp.ts │ ├── componentSlots.ts │ ├── componentPublicInstance.ts │ ├── scheduler.ts │ ├── vnode.ts │ ├── apiInject.ts │ ├── component.ts │ └── renderer.ts ├── shared │ ├── shapeFlags.ts │ └── index.ts └── runtime-dom │ └── index.ts ├── .gitignore ├── images ├── Vue3 源码结构.png ├── runtime-core $el 测试通过.png ├── runtime-core 全流程函数调用版.png ├── runtime-core 全流程文字描述版.png ├── runtime-core 具名插槽测试通过.png ├── runtime-core emit 测试通过.gif ├── runtime-core props 测试通过.png ├── runtime-core 作用域插槽测试通过.png ├── runtime-core 插槽处理方式-div.png ├── runtime-core 注册事件功能测试通过.png ├── runtime-core 第一个测试修改通过.png ├── runtime-core 第一个测试完整通过.png ├── runtime-core diff-AddHead.png ├── runtime-core diff-AddMid.png ├── runtime-core diff-AddTail.png ├── runtime-core diff-DelHead.png ├── runtime-core diff-DelMid.png ├── runtime-core diff-DelTail.png ├── runtime-core diff-MoveMid.png ├── runtime-core diff-add 测试通过.gif ├── runtime-core diff-del 测试通过.gif ├── runtime-core nextTick 测试通过.gif ├── runtime-core 处理 Text 测试通过.png ├── runtime-core 插槽最基本实现测试通过1.png ├── runtime-core 插槽最基本实现测试通过2.png ├── runtime-core diff-move 测试通过.gif ├── runtime-core 实现 Element 更新流程.png ├── runtime-core 插槽处理方式-Fragment.png ├── runtime-core 更新 Component 测试通过.gif ├── runtime-core 父子组件间 provide-inject.png ├── runtime-core getCurrentInstance 测试通过.png ├── runtime-core 更新 Element 的 props 测试通过.gif ├── runtime-core 跨层次组件间 provide-inject.png └── runtime-core 更新 Element 的 children 前三种情况测试通过.gif ├── .editorconfig ├── jest.config.js ├── babel.config.js ├── example ├── nextTick │ ├── main.js │ ├── index.html │ └── App.js ├── HelloWorld │ ├── main.js │ ├── index.html │ └── App.js ├── provide-inject │ ├── main.js │ ├── index.html │ └── App.js ├── Component-emit │ ├── main.js │ ├── index.html │ ├── App.js │ └── Foo.js ├── Component-props │ ├── main.js │ ├── Foo.js │ ├── index.html │ └── App.js ├── Component-slots │ ├── main.js │ ├── Foo.js │ ├── Bar.js │ ├── Baz.js │ ├── index.html │ └── App.js ├── update-Component │ ├── main.js │ ├── Foo.js │ ├── index.html │ └── App.js ├── getCurrentInstance │ ├── main.js │ ├── App.js │ ├── Foo.js │ └── index.html ├── update-Element-props │ ├── main.js │ ├── index.html │ └── App.js ├── ready-to-update-Element │ ├── main.js │ ├── index.html │ └── App.js ├── update-Element-children_A-A │ ├── main.js │ ├── AddHead.js │ ├── AddTail.js │ ├── DelHead.js │ ├── DelTail.js │ ├── AddMid.js │ ├── DelMid.js │ ├── MoveMid.js │ ├── Array2Array.js │ ├── index.html │ └── App.js └── update-Element-children_T-A │ ├── main.js │ ├── Text2Text.js │ ├── Array2Text.js │ ├── Text2Array.js │ ├── index.html │ └── App.js ├── rollup.config.js ├── package.json ├── README.md └── tsconfig.json /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './runtime-dom' 2 | export * from './reactivity/src' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | yarn.lock 7 | lib -------------------------------------------------------------------------------- /images/Vue3 源码结构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/Vue3 源码结构.png -------------------------------------------------------------------------------- /images/runtime-core $el 测试通过.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core $el 测试通过.png -------------------------------------------------------------------------------- /images/runtime-core 全流程函数调用版.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 全流程函数调用版.png -------------------------------------------------------------------------------- /images/runtime-core 全流程文字描述版.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 全流程文字描述版.png -------------------------------------------------------------------------------- /images/runtime-core 具名插槽测试通过.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 具名插槽测试通过.png -------------------------------------------------------------------------------- /images/runtime-core emit 测试通过.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core emit 测试通过.gif -------------------------------------------------------------------------------- /images/runtime-core props 测试通过.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core props 测试通过.png -------------------------------------------------------------------------------- /images/runtime-core 作用域插槽测试通过.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 作用域插槽测试通过.png -------------------------------------------------------------------------------- /images/runtime-core 插槽处理方式-div.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 插槽处理方式-div.png -------------------------------------------------------------------------------- /images/runtime-core 注册事件功能测试通过.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 注册事件功能测试通过.png -------------------------------------------------------------------------------- /images/runtime-core 第一个测试修改通过.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 第一个测试修改通过.png -------------------------------------------------------------------------------- /images/runtime-core 第一个测试完整通过.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 第一个测试完整通过.png -------------------------------------------------------------------------------- /images/runtime-core diff-AddHead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core diff-AddHead.png -------------------------------------------------------------------------------- /images/runtime-core diff-AddMid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core diff-AddMid.png -------------------------------------------------------------------------------- /images/runtime-core diff-AddTail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core diff-AddTail.png -------------------------------------------------------------------------------- /images/runtime-core diff-DelHead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core diff-DelHead.png -------------------------------------------------------------------------------- /images/runtime-core diff-DelMid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core diff-DelMid.png -------------------------------------------------------------------------------- /images/runtime-core diff-DelTail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core diff-DelTail.png -------------------------------------------------------------------------------- /images/runtime-core diff-MoveMid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core diff-MoveMid.png -------------------------------------------------------------------------------- /images/runtime-core diff-add 测试通过.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core diff-add 测试通过.gif -------------------------------------------------------------------------------- /images/runtime-core diff-del 测试通过.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core diff-del 测试通过.gif -------------------------------------------------------------------------------- /images/runtime-core nextTick 测试通过.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core nextTick 测试通过.gif -------------------------------------------------------------------------------- /images/runtime-core 处理 Text 测试通过.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 处理 Text 测试通过.png -------------------------------------------------------------------------------- /images/runtime-core 插槽最基本实现测试通过1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 插槽最基本实现测试通过1.png -------------------------------------------------------------------------------- /images/runtime-core 插槽最基本实现测试通过2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 插槽最基本实现测试通过2.png -------------------------------------------------------------------------------- /images/runtime-core diff-move 测试通过.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core diff-move 测试通过.gif -------------------------------------------------------------------------------- /images/runtime-core 实现 Element 更新流程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 实现 Element 更新流程.png -------------------------------------------------------------------------------- /images/runtime-core 插槽处理方式-Fragment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 插槽处理方式-Fragment.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # all files 5 | [*] 6 | indent_style = space 7 | indent_size = 2 -------------------------------------------------------------------------------- /images/runtime-core 更新 Component 测试通过.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 更新 Component 测试通过.gif -------------------------------------------------------------------------------- /images/runtime-core 父子组件间 provide-inject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 父子组件间 provide-inject.png -------------------------------------------------------------------------------- /images/runtime-core getCurrentInstance 测试通过.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core getCurrentInstance 测试通过.png -------------------------------------------------------------------------------- /images/runtime-core 更新 Element 的 props 测试通过.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 更新 Element 的 props 测试通过.gif -------------------------------------------------------------------------------- /images/runtime-core 跨层次组件间 provide-inject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 跨层次组件间 provide-inject.png -------------------------------------------------------------------------------- /src/reactivity/src/index.ts: -------------------------------------------------------------------------------- 1 | export { effect } from './effect' 2 | export { shallowReadonly } from './reactive' 3 | export { proxyRefs, ref } from './ref' 4 | -------------------------------------------------------------------------------- /images/runtime-core 更新 Element 的 children 前三种情况测试通过.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stan9726/Mini-Vue3/HEAD/images/runtime-core 更新 Element 的 children 前三种情况测试通过.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node' 5 | } 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript' 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/runtime-core/componentProps.ts: -------------------------------------------------------------------------------- 1 | // 用于将 props 对象赋值给组件实例对象的 props property 2 | export function initProps(instance, rawProps) { 3 | instance.props = rawProps || {} 4 | } 5 | -------------------------------------------------------------------------------- /src/runtime-core/h.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from './vnode' 2 | 3 | // 用于调用 createVNode 返回一个 VNode 4 | export function h(type, props?, children?) { 5 | return createVNode(type, props, children) 6 | } 7 | -------------------------------------------------------------------------------- /example/nextTick/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import App from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | const app = createApp(App) 6 | app.mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/HelloWorld/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | const app = createApp(App) 6 | app.mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/provide-inject/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import App from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | const app = createApp(App) 6 | app.mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/Component-emit/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | const app = createApp(App) 6 | app.mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/Component-props/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | const app = createApp(App) 6 | app.mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/Component-slots/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | const app = createApp(App) 6 | app.mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/update-Component/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | const app = createApp(App) 6 | app.mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/getCurrentInstance/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | const app = createApp(App) 6 | app.mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/update-Element-props/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | const app = createApp(App) 6 | app.mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/ready-to-update-Element/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | const app = createApp(App) 6 | app.mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/update-Element-children_A-A/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | const app = createApp(App) 6 | app.mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/update-Element-children_T-A/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from '../../lib/mini-vue3.esm.js' 2 | import { App } from './App.js' 3 | 4 | const rootContainer = document.querySelector('#app') 5 | const app = createApp(App) 6 | app.mount(rootContainer) 7 | -------------------------------------------------------------------------------- /example/update-Component/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue3.esm.js' 2 | 3 | // Foo 组件选项对象 4 | export const Foo = { 5 | name: 'Foo', 6 | setup() { 7 | return {} 8 | }, 9 | render() { 10 | // 通过 this.$props 获取 props 对象 11 | return h('div', {}, `Foo Component count: ${this.$props.count}`) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/shallowReactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, shallowReactive } from '../src/reactive' 2 | 3 | describe('shallowReactive', () => { 4 | test('should not make non-reactive properties reactive', () => { 5 | const props = shallowReactive({ n: { foo: 1 } }) 6 | expect(isReactive(props.n)).toBe(false) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /src/runtime-core/index.ts: -------------------------------------------------------------------------------- 1 | export { h } from './h' 2 | export { renderSlots } from './helpers/renderSlots' 3 | export { createTextVNode } from './vnode' 4 | export { getCurrentInstance } from './component' 5 | export { provide, inject } from './apiInject' 6 | export { createRenderer } from './renderer' 7 | export { nextTick } from './scheduler' 8 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/shallowReadonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isReactive, shallowReadonly } from '../src/reactive' 2 | 3 | describe('reactivity/shallowReadonly', () => { 4 | test('should not make non-reactive properties reactive', () => { 5 | const props = shallowReadonly({ n: { foo: 1 } }) 6 | expect(isReactive(props.n)).toBe(false) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /example/Component-slots/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots } from '../../lib/mini-vue3.esm.js' 2 | 3 | // Foo 组件选项对象 4 | export const Foo = { 5 | name: 'Foo', 6 | setup() { 7 | return {} 8 | }, 9 | render() { 10 | // 通过 this.$slots 获取父组件传递的插槽 11 | return h('div', {}, [h('p', {}, 'Foo component'), renderSlots(this.$slots)]) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/runtime-core/componentEmit.ts: -------------------------------------------------------------------------------- 1 | import { camelize, toHandlerKey } from '../shared' 2 | 3 | // 用于调用 props 对象中的指定方法 4 | export function emit(instance, event, ...args) { 5 | // 通过解构赋值获取组件实例对象的 props property 6 | const { props } = instance 7 | 8 | const handlerName = toHandlerKey(camelize(event)) 9 | const handler = props[handlerName] 10 | handler && handler(...args) 11 | } 12 | -------------------------------------------------------------------------------- /example/getCurrentInstance/App.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from '../../lib/mini-vue3.esm.js' 2 | import { Foo } from './Foo.js' 3 | 4 | export const App = { 5 | name: 'App', 6 | setup() { 7 | // 获取当前组件实例对象 8 | const instance = getCurrentInstance() 9 | console.log('App:', instance) 10 | 11 | return {} 12 | }, 13 | render() { 14 | return h(Foo) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/getCurrentInstance/Foo.js: -------------------------------------------------------------------------------- 1 | import { h, getCurrentInstance } from '../../lib/mini-vue3.esm.js' 2 | 3 | // Foo 组件选型对象 4 | export const Foo = { 5 | name: 'Foo', 6 | setup() { 7 | // 获取当前组件实例对象 8 | const instance = getCurrentInstance() 9 | console.log('Foo:', instance) 10 | 11 | return {} 12 | }, 13 | render() { 14 | return h('p', {}, 'Foo component') 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/runtime-core/helpers/renderSlots.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, Fragment } from '../vnode' 2 | 3 | // 用于利用 Fragment 对插槽进行包裹 4 | export function renderSlots(slots, name, props) { 5 | // 通过 name 获取创建相应插槽的方法 6 | const slot = slots[name] 7 | 8 | if (slot) { 9 | if (typeof slot === 'function') { 10 | // 将创建插槽方法的执行结果作为 children 传入 11 | return createVNode(Fragment, {}, slot(props)) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/Component-slots/Bar.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots } from '../../lib/mini-vue3.esm.js' 2 | 3 | // Bar 组件选项对象 4 | export const Bar = { 5 | name: 'Bar', 6 | setup() {}, 7 | render() { 8 | return h('div', {}, [ 9 | // 通过在调用 renderSlots 函数时传入第二个参数指定在此位置渲染的插槽 10 | renderSlots(this.$slots, 'header'), 11 | h('p', {}, 'Bar component'), 12 | renderSlots(this.$slots, 'footer') 13 | ]) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/runtime-core/componentUpdateUtils.ts: -------------------------------------------------------------------------------- 1 | // 用于判断组件是否需要更新 2 | export function shouldUpdateComponent(prevVNode, nextVNode): boolean { 3 | const { props: prevProps } = prevVNode 4 | const { props: nextProps } = nextVNode 5 | 6 | // 对比新旧 VNode 的 props 对象,若不相等则返回 true,否则返回 false 7 | for (const key in nextProps) { 8 | if (nextProps[key] !== prevProps[key]) { 9 | return true 10 | } 11 | } 12 | 13 | return false 14 | } 15 | -------------------------------------------------------------------------------- /example/Component-props/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue3.esm.js' 2 | 3 | // Foo 组件选项对象 4 | export const Foo = { 5 | // props 对象是 setup 的第一个参数 6 | setup(props) { 7 | console.log(props) 8 | 9 | // props 对象是只读的,但不是深度只读的 10 | props.count++ 11 | console.log(props.count) 12 | }, 13 | render() { 14 | // 在 render 函数中通过 this 获取 props 对象的 property 15 | return h('div', {}, 'foo: ' + this.count) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/nextTick/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/Component-emit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/Component-props/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/Component-slots/Baz.js: -------------------------------------------------------------------------------- 1 | import { h, renderSlots } from '../../lib/mini-vue3.esm.js' 2 | 3 | // Baz 组件选项对象 4 | export const Baz = { 5 | name: 'Baz', 6 | setup() { 7 | return {} 8 | }, 9 | render() { 10 | const msg = 'this is a slot' 11 | 12 | // 通过在调用 renderSlots 函数时传入第三个参数指定传入插槽函数的参数 13 | return h( 14 | 'div', 15 | {}, 16 | this.$slots.content({ 17 | msg 18 | }) 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/Component-slots/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/getCurrentInstance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/provide-inject/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/update-Component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/ready-to-update-Element/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/update-Element-props/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/shared/shapeFlags.ts: -------------------------------------------------------------------------------- 1 | // 用于判断 VNode 的 shapeFlag property 2 | export const enum ShapeFlags { 3 | // 用于判断 VNode 类型是否是 Element 4 | ELEMENT = 1, // 00001 5 | // 用于判断 VNode 类型是否是 Component 6 | STATEFUL_COMPONENT = 1 << 1, // 00010 7 | // 用于判断 children 类型是否是 string 8 | TEXT_CHILDREN = 1 << 2, // 00100 9 | // 用于判断 children 类型是否是 Array 10 | ARRAY_CHILDREN = 1 << 3, // 01000 11 | // 用于判断 children 是否是插槽 12 | SLOTS_CHILDREN = 1 << 4 // 10000 13 | } 14 | -------------------------------------------------------------------------------- /example/Component-props/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue3.esm.js' 2 | import { Foo } from './Foo.js' 3 | 4 | export const App = { 5 | render() { 6 | return h( 7 | 'div', 8 | { 9 | id: 'root' 10 | }, 11 | [ 12 | h('div', {}, 'hello, ' + this.name), 13 | // 创建 Foo 组件,向其中传入 count prop 14 | h(Foo, { count: 1 }) 15 | ] 16 | ) 17 | }, 18 | setup() { 19 | return { 20 | name: 'mini-vue3' 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/update-Element-children_T-A/Text2Text.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue3.esm.js' 2 | 3 | const prev = 'prevChild' 4 | const next = 'nextChild' 5 | 6 | export default { 7 | name: 'Text2Text', 8 | setup() { 9 | const isUpdateT2T = ref(false) 10 | window.isUpdateT2T = isUpdateT2T 11 | 12 | return { 13 | isUpdateT2T 14 | } 15 | }, 16 | render() { 17 | const self = this 18 | 19 | return h('div', {}, self.isUpdateT2T ? next : prev) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json' 2 | import typescript from '@rollup/plugin-typescript' 3 | 4 | // 可以直接使用 ESM 5 | export default { 6 | // 库的入口文件 7 | input: './src/index.ts', 8 | // 打包完成后的输出 9 | output: [ 10 | // CommonJS 11 | { 12 | format: 'cjs', 13 | file: pkg.main 14 | }, 15 | // ESM 16 | { 17 | format: 'es', 18 | file: pkg.module 19 | } 20 | ], 21 | // 配置插件 @rollup/plugin-typescript 22 | plugins: [typescript()] 23 | } 24 | -------------------------------------------------------------------------------- /src/runtime-core/createApp.ts: -------------------------------------------------------------------------------- 1 | import { createVNode } from './vnode' 2 | 3 | // 用于返回 createApp 4 | export function createAppAPI(render) { 5 | // 用于创建应用实例 6 | return function createApp(rootComponent) { 7 | return { 8 | component() {}, 9 | directive() {}, 10 | use() {}, 11 | // 用于将应用挂载到根容器中 12 | mount(rootContainer) { 13 | // 将根组件转换为 VNode 14 | const vnode = createVNode(rootComponent) 15 | 16 | render(vnode, rootContainer) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/update-Element-children_T-A/Array2Text.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue3.esm.js' 2 | 3 | const prev = [h('div', {}, 'prevChild1'), h('div', {}, 'prevChild2')] 4 | const next = 'nextChild' 5 | 6 | export default { 7 | name: 'Array2Text', 8 | setup() { 9 | const isUpdateA2T = ref(false) 10 | window.isUpdateA2T = isUpdateA2T 11 | 12 | return { isUpdateA2T } 13 | }, 14 | render() { 15 | const self = this 16 | 17 | return h('div', {}, self.isUpdateA2T ? next : prev) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/update-Element-children_T-A/Text2Array.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue3.esm.js' 2 | 3 | const prev = 'prevChild' 4 | const next = [h('div', {}, 'nextChild1'), h('div', {}, 'nextChild2')] 5 | 6 | export default { 7 | name: 'Text2Array', 8 | setup() { 9 | const isUpdateT2A = ref(false) 10 | window.isUpdateT2A = isUpdateT2A 11 | 12 | return { 13 | isUpdateT2A 14 | } 15 | }, 16 | render() { 17 | const self = this 18 | 19 | return h('div', {}, self.isUpdateT2A ? next : prev) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/HelloWorld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /example/Component-emit/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue3.esm.js' 2 | import { Foo } from './Foo.js' 3 | 4 | export const App = { 5 | render() { 6 | return h('div', {}, [ 7 | h('div', {}, 'App'), 8 | h( 9 | Foo, 10 | // 使用 Foo 组件时在 props 对象中声明 onBar 方法和 onBarBaz 方法 11 | { 12 | onBar(a, b) { 13 | console.log('onBar', a, b) 14 | }, 15 | onBarBaz(c, d) { 16 | console.log('onBarBaz', c, d) 17 | } 18 | } 19 | ) 20 | ]) 21 | }, 22 | setup() { 23 | return {} 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/ready-to-update-Element/App.js: -------------------------------------------------------------------------------- 1 | import { ref, h } from '../../lib/mini-vue3.esm.js' 2 | 3 | export const App = { 4 | name: 'App', 5 | setup() { 6 | // 响应式数据 7 | const count = ref(0) 8 | 9 | const onClick = () => { 10 | // 修改响应式数据 11 | count.value++ 12 | } 13 | 14 | return { 15 | count, 16 | onClick 17 | } 18 | }, 19 | render() { 20 | return h( 21 | 'div', 22 | { 23 | id: 'root' 24 | }, 25 | [ 26 | h('p', {}, `count: ${this.count}`), 27 | h( 28 | 'button', 29 | { 30 | onClick: this.onClick 31 | }, 32 | 'plus 1' 33 | ) 34 | ] 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-vue3", 3 | "version": "0.0.0", 4 | "main": "lib/mini-vue3.cjs.js", 5 | "module": "lib/mini-vue3.esm.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "rollup -c rollup.config.js" 9 | }, 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@babel/core": "^7.15.5", 13 | "@babel/preset-env": "^7.15.6", 14 | "@babel/preset-typescript": "^7.15.0", 15 | "@rollup/plugin-typescript": "^8.3.0", 16 | "@types/jest": "^27.0.2", 17 | "babel-jest": "^27.2.4", 18 | "jest": "^27.2.4", 19 | "rollup": "^2.60.2", 20 | "ts-jest": "^27.0.5", 21 | "ts-lib": "^0.0.5", 22 | "tslib": "^2.3.1", 23 | "typescript": "^4.4.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mini-Vue3 2 | 3 | 手写实现 mini 版 Vue3 4 | 5 | 感谢 [阿崔cxr](https://github.com/cuixiaorui) 的 [mini-vue](https://github.com/cuixiaorui/mini-vue) 6 | 7 | 如果觉得不错还请 star 支持一下,也欢迎在掘金关注[我](https://juejin.cn/user/158789396594765)或者关注我的[手写 Mini-Vue3 专栏](https://juejin.cn/column/7028767798591815710),和我一起手写实现自己的 Mini-Vue3! 8 | 9 | [手写 Mini-Vue3 专栏](https://juejin.cn/column/7028767798591815710)文章列表: 10 | 11 | 1. [了解 Vue3 源码结构 + 初始化项目](https://juejin.cn/post/7029145067755421726) 12 | 2. [一篇文章两个小时彻底搞懂 Vue3 的 reactivity](https://juejin.cn/post/7028771190416080933) 13 | 3. [一起来看看 Vue3 在渲染时具体做了哪些工作(上)](https://juejin.cn/post/7037023568877584414) 14 | 4. [一起来看看 Vue3 在渲染时具体做了哪些工作(下)](https://juejin.cn/post/7055109077659025415) 15 | 5. [带你理解 Vue3 如何把页面上的 0 更新成 1 ](https://juejin.cn/post/7096386880161185806) 16 | -------------------------------------------------------------------------------- /example/update-Element-children_A-A/AddHead.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue3.esm.js' 2 | 3 | const prev = [ 4 | h('span', { key: 'A' }, 'A'), 5 | h('span', { key: 'B' }, 'B'), 6 | h('span', { key: 'C' }, 'C') 7 | ] 8 | 9 | const next = [ 10 | h('span', { key: 'X', class: 'added' }, 'X'), 11 | h('span', { key: 'Y', class: 'added' }, 'Y'), 12 | h('span', { key: 'A' }, 'A'), 13 | h('span', { key: 'B' }, 'B'), 14 | h('span', { key: 'C' }, 'C') 15 | ] 16 | 17 | export default { 18 | name: 'AddHead', 19 | setup() { 20 | const isAddHead = ref(false) 21 | window.isAddHead = isAddHead 22 | 23 | return { 24 | isAddHead 25 | } 26 | }, 27 | render() { 28 | const self = this 29 | 30 | return h('div', {}, self.isAddHead ? next : prev) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/update-Element-children_A-A/AddTail.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue3.esm.js' 2 | 3 | const prev = [ 4 | h('span', { key: 'A' }, 'A'), 5 | h('span', { key: 'B' }, 'B'), 6 | h('span', { key: 'C' }, 'C') 7 | ] 8 | 9 | const next = [ 10 | h('span', { key: 'A' }, 'A'), 11 | h('span', { key: 'B' }, 'B'), 12 | h('span', { key: 'C' }, 'C'), 13 | h('span', { key: 'X', class: 'added' }, 'X'), 14 | h('span', { key: 'Y', class: 'added' }, 'Y') 15 | ] 16 | 17 | export default { 18 | name: 'AddTail', 19 | setup() { 20 | const isAddTail = ref(false) 21 | window.isAddTail = isAddTail 22 | 23 | return { 24 | isAddTail 25 | } 26 | }, 27 | render() { 28 | const self = this 29 | 30 | return h('div', {}, self.isAddTail ? next : prev) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/runtime-core/componentSlots.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from '../shared' 2 | 3 | // 用于将插槽挂载到组件实例对象的 slots property 上 4 | export function initSlots(instance, children) { 5 | // 通过解构赋值获得组件对应的 VNode 6 | const { vnode } = instance 7 | 8 | // 若 children 是插槽则进行处理 9 | if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) { 10 | normalizeObjectSlots(children, instance.slots) 11 | } 12 | } 13 | 14 | // 用于遍历 children,将创建插槽对应的 VNode 数组的函数挂载到组件实例对象的 slots property 上 15 | function normalizeObjectSlots(children, slots) { 16 | for (const key in children) { 17 | const value = children[key] 18 | 19 | slots[key] = props => normalizeSlotValue(value(props)) 20 | } 21 | } 22 | 23 | // 用于将一个 VNode 转为数组 24 | function normalizeSlotValue(value) { 25 | return Array.isArray(value) ? value : [value] 26 | } 27 | -------------------------------------------------------------------------------- /example/update-Element-children_A-A/DelHead.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue3.esm.js' 2 | 3 | const prev = [ 4 | h('span', { key: 'A', class: 'deleted' }, 'A'), 5 | h('span', { key: 'B', class: 'deleted' }, 'B'), 6 | h('span', { key: 'C' }, 'C'), 7 | h('span', { key: 'D' }, 'D'), 8 | h('span', { key: 'E' }, 'E') 9 | ] 10 | 11 | const next = [ 12 | h('span', { key: 'C' }, 'C'), 13 | h('span', { key: 'D' }, 'D'), 14 | h('span', { key: 'E' }, 'E') 15 | ] 16 | 17 | export default { 18 | name: 'DelHead', 19 | setup() { 20 | const isDelHead = ref(false) 21 | window.isDelHead = isDelHead 22 | 23 | return { 24 | isDelHead 25 | } 26 | }, 27 | render() { 28 | const self = this 29 | 30 | return h('div', {}, self.isDelHead ? next : prev) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/update-Element-children_A-A/DelTail.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue3.esm.js' 2 | 3 | const prev = [ 4 | h('span', { key: 'A' }, 'A'), 5 | h('span', { key: 'B' }, 'B'), 6 | h('span', { key: 'C' }, 'C'), 7 | h('span', { key: 'D', class: 'deleted' }, 'D'), 8 | h('span', { key: 'E', class: 'deleted' }, 'E') 9 | ] 10 | 11 | const next = [ 12 | h('span', { key: 'A' }, 'A'), 13 | h('span', { key: 'B' }, 'B'), 14 | h('span', { key: 'C' }, 'C') 15 | ] 16 | 17 | export default { 18 | name: 'DelTail', 19 | setup() { 20 | const isDelTail = ref(false) 21 | window.isDelTail = isDelTail 22 | 23 | return { 24 | isDelTail 25 | } 26 | }, 27 | render() { 28 | const self = this 29 | 30 | return h('div', {}, self.isDelTail ? next : prev) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/update-Element-children_A-A/AddMid.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue3.esm.js' 2 | 3 | const prev = [ 4 | h('span', { key: 'A' }, 'A'), 5 | h('span', { key: 'B' }, 'B'), 6 | h('span', { key: 'C' }, 'C'), 7 | h('span', { key: 'D' }, 'D') 8 | ] 9 | 10 | const next = [ 11 | h('span', { key: 'A' }, 'A'), 12 | h('span', { key: 'B' }, 'B'), 13 | h('span', { key: 'X', class: 'added' }, 'X'), 14 | h('span', { key: 'Y', class: 'added' }, 'Y'), 15 | h('span', { key: 'C' }, 'C'), 16 | h('span', { key: 'D' }, 'D') 17 | ] 18 | 19 | export default { 20 | name: 'AddMid', 21 | setup() { 22 | const isAddMid = ref(false) 23 | window.isAddMid = isAddMid 24 | 25 | return { 26 | isAddMid 27 | } 28 | }, 29 | render() { 30 | const self = this 31 | 32 | return h('div', {}, self.isAddMid ? next : prev) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/update-Element-children_A-A/DelMid.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue3.esm.js' 2 | 3 | const prev = [ 4 | h('span', { key: 'A' }, 'A'), 5 | h('span', { key: 'B' }, 'B'), 6 | h('span', { key: 'C', class: 'deleted' }, 'C'), 7 | h('span', { key: 'D', class: 'deleted' }, 'D'), 8 | h('span', { key: 'E' }, 'E'), 9 | h('span', { key: 'F' }, 'F') 10 | ] 11 | 12 | const next = [ 13 | h('span', { key: 'A' }, 'A'), 14 | h('span', { key: 'B' }, 'B'), 15 | h('span', { key: 'E' }, 'E'), 16 | h('span', { key: 'F' }, 'F') 17 | ] 18 | 19 | export default { 20 | name: 'DelMid', 21 | setup() { 22 | const isDelMid = ref(false) 23 | window.isDelMid = isDelMid 24 | 25 | return { 26 | isDelMid 27 | } 28 | }, 29 | render() { 30 | const self = this 31 | 32 | return h('div', {}, self.isDelMid ? next : prev) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/runtime-core/componentPublicInstance.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn } from '../shared' 2 | 3 | // 用于保存组件实例对象 property 及对应的 getter 4 | const publicPropertiesMap = { 5 | $el: i => i.vnode.el, 6 | $slots: i => i.slots, 7 | $props: i => i.props 8 | } 9 | 10 | // 组件实例对象 proxy property 对应的 handlers 11 | export const PublicInstanceHandlers = { 12 | get({ _: instance }, key) { 13 | // 通过解构赋值获取组件实例对象的 setupState property 和 props property 14 | const { setupState, props } = instance 15 | 16 | // 若 setupState property 或 props property 上有该 property 则返回其值 17 | if (hasOwn(setupState, key)) { 18 | return setupState[key] 19 | } else if (hasOwn(props, key)) { 20 | return props[key] 21 | } 22 | 23 | // 若获取指定 property 则调用对应 getter 并返回其返回值 24 | const publicGetter = publicPropertiesMap[key] 25 | if (publicGetter) { 26 | return publicGetter(instance) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | // 为 Object.assign 方法创建别名 2 | export const extend = Object.assign 3 | 4 | // 用于判断一个变量是否为对象 5 | export const isObject = value => typeof value === 'object' && value !== null 6 | 7 | // 用于判断两个值或对象是否相等 8 | export const hasChanged = (value, oldValue): boolean => 9 | !Object.is(value, oldValue) 10 | 11 | // 用于判断对象中是否有某个 property 12 | export const hasOwn = (val, key) => 13 | Object.prototype.hasOwnProperty.call(val, key) 14 | 15 | // 用于将带连字符的字符串转换为驼峰式 16 | export const camelize = (str: string) => { 17 | return str.replace(/-(\w)/g, (_, c: string) => { 18 | return c ? c.toUpperCase() : '' 19 | }) 20 | } 21 | 22 | // 用于将字符串首字母转换为大写 23 | export const capitalize = (str: string) => { 24 | return str.charAt(0).toUpperCase() + str.slice(1) 25 | } 26 | 27 | // 用于在字符串之前加上 on 28 | export const toHandlerKey = (str: string) => { 29 | return str ? 'on' + capitalize(str) : '' 30 | } 31 | 32 | export * from './shapeFlags' 33 | -------------------------------------------------------------------------------- /example/update-Element-children_A-A/MoveMid.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue3.esm.js' 2 | 3 | const prev = [ 4 | h('span', { key: 'A' }, 'A'), 5 | h('span', { key: 'B' }, 'B'), 6 | h('span', { key: 'C' }, 'C'), 7 | h('span', { key: 'D' }, 'D'), 8 | h('span', { key: 'E' }, 'E'), 9 | h('span', { key: 'F' }, 'F') 10 | ] 11 | 12 | const next = [ 13 | h('span', { key: 'A' }, 'A'), 14 | h('span', { key: 'D', class: 'moved' }, 'D'), 15 | h('span', { key: 'B' }, 'B'), 16 | h('span', { key: 'E', class: 'moved' }, 'E'), 17 | h('span', { key: 'C' }, 'C'), 18 | h('span', { key: 'F' }, 'F') 19 | ] 20 | 21 | export default { 22 | name: 'MoveMid', 23 | setup() { 24 | const isMoveMid = ref(false) 25 | window.isMoveMid = isMoveMid 26 | 27 | return { 28 | isMoveMid 29 | } 30 | }, 31 | render() { 32 | const self = this 33 | 34 | return h('div', {}, self.isMoveMid ? next : prev) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/runtime-core/scheduler.ts: -------------------------------------------------------------------------------- 1 | // 用于保存组件实例对象的 update 方法 2 | const queue: any[] = [] 3 | 4 | const p = Promise.resolve() 5 | // 用于标志是否将清空队列的操作放到微任务队列中 6 | let isFlushPending = false 7 | 8 | // 用于将回调函数推迟到下一个 DOM 更新周期之后执行 9 | export function nextTick(fn) { 10 | // 若传入了回调函数则将其放到微任务队列中并返回一个 Promise,否则直接返回一个 Promise 11 | return fn ? p.then(fn) : p 12 | } 13 | 14 | // 用于将组件实例对象的 update 方法保存到队列中并将清空队列的操作放到微任务队列中 15 | export function queueJobs(job) { 16 | if (!queue.includes(job)) { 17 | queue.push(job) 18 | } 19 | 20 | queueFlush() 21 | } 22 | 23 | // 用于清空队列 24 | function queueFlush() { 25 | if (isFlushPending) { 26 | return 27 | } 28 | 29 | isFlushPending = true 30 | 31 | // 利用 nextTick 将 flushJobs 函数放到微任务队列中 32 | nextTick(flushJobs) 33 | } 34 | 35 | // 用于依次从队列中弹出组件实例对象的 update 方法并执行 36 | function flushJobs() { 37 | isFlushPending = false 38 | 39 | let job 40 | while ((job = queue.shift())) { 41 | job && job() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example/HelloWorld/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue3.esm.js' 2 | 3 | window.self = null 4 | // 根组件选项对象 5 | export const App = { 6 | // render 函数 7 | render() { 8 | window.self = this 9 | // 在 render 函数中通过 this 获取 setup 返回对象的 property 10 | return h( 11 | 'div', 12 | { 13 | id: 'root', 14 | class: 'root-div', 15 | // 注册事件 16 | onClick() { 17 | console.log('you clicked root-div') 18 | }, 19 | onMousedown() { 20 | console.log('your mouse down on root-div') 21 | } 22 | }, 23 | 'hello, ' + this.name 24 | ) 25 | // return h('div', { id: 'root', class: 'root' }, [ 26 | // h('p', { id: 'p1', class: 'p1' }, 'hello, mini-vue3'), 27 | // h('p', { id: 'p2', class: 'p2' }, 'this is mini-vue3') 28 | // ]) 29 | }, 30 | // composition API 31 | setup() { 32 | // 返回一个对象 33 | return { 34 | name: 'mini-vue3' 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/reactive.spec.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReactive, reactive } from '../src/reactive' 2 | 3 | describe('reactivity/reactive', () => { 4 | it('Object', () => { 5 | const original = { foo: 1 } 6 | // 创建响应式对象 7 | const observed = reactive(original) 8 | // 响应式对象与原始对象不相等 9 | expect(observed).not.toBe(original) 10 | // 对响应式对象调用 isReactive 返回 true 11 | expect(isReactive(observed)).toBe(true) 12 | // 对普通对象调用 isReactive 返回 false 13 | expect(isReactive(original)).toBe(false) 14 | // 对响应式对象调用 isProxy 返回 true 15 | expect(isProxy(observed)).toBe(true) 16 | // 对普通对象调用 isProxy 返回 false 17 | expect(isProxy(original)).toBe(false) 18 | // 响应式对象的 property 的值与原始对象的相等 19 | expect(observed.foo).toBe(1) 20 | }) 21 | 22 | it('nested reactives', () => { 23 | const original = { foo: { bar: 1 } } 24 | const observed = reactive(original) 25 | // 嵌套对象是响应式的 26 | expect(isReactive(observed.foo)).toBe(true) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /example/Component-emit/Foo.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue3.esm.js' 2 | 3 | // Foo 组件选项对象 4 | export const Foo = { 5 | setup(props, { emit }) { 6 | const emitBar = () => { 7 | console.log('emit bar') 8 | // 通过 emit 触发使用 Foo 组件时在 props 对象中声明的 onBar 方法 9 | emit('bar', 1, 2) 10 | } 11 | 12 | const emitBarBaz = () => { 13 | console.log('emit bar baz') 14 | // 通过 emit 触发使用 Foo 组件时在 props 对象中声明的 onBarBaz 方法 15 | emit('bar-baz', 3, 4) 16 | } 17 | 18 | return { 19 | emitBar, 20 | emitBarBaz 21 | } 22 | }, 23 | render() { 24 | const btnBar = h( 25 | 'button', 26 | { 27 | // 在 render 函数中通过 this 获取 setup 返回对象的方法 28 | onClick: this.emitBar 29 | }, 30 | 'emitBar' 31 | ) 32 | 33 | const btnBaz = h( 34 | 'button', 35 | { 36 | onClick: this.emitBarBaz 37 | }, 38 | 'emitBarBaz' 39 | ) 40 | 41 | return h('div', {}, [btnBar, btnBaz]) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/reactivity/src/computed.ts: -------------------------------------------------------------------------------- 1 | import { ReactiveEffect } from './effect' 2 | 3 | // Ref 接口的实现类 4 | class ComputedImpl { 5 | // 用于保存 ReactiveEffect 类的实例 6 | private _effect: ReactiveEffect 7 | // 用于保存 getter 函数的执行结果 8 | private _value 9 | // 用于记录是否不使用缓存 10 | private _dirty = true 11 | 12 | constructor(getter) { 13 | // 利用 getter 函数和一个方法创建 ReactiveEffect 类的实例 14 | this._effect = new ReactiveEffect( 15 | getter, 16 | // 用于关闭缓存 17 | () => { 18 | this._dirty = true 19 | } 20 | ) 21 | } 22 | 23 | // value property 的 get 返回调用私有 property _effect 的 run 方法的返回值,即调用 getter 函数的返回值 24 | get value() { 25 | if (this._dirty) { 26 | // 调用 ReactiveEffect 类的实例的 run 方法,即执行 getter 函数,将结果赋值给 _value property 27 | this._value = this._effect.run() 28 | this._dirty = false 29 | } 30 | 31 | return this._value 32 | } 33 | } 34 | 35 | export function computed(getter) { 36 | // 返回 RefImpl 类的实例,即 ref 对象 37 | return new ComputedImpl(getter) 38 | } 39 | -------------------------------------------------------------------------------- /example/Component-slots/App.js: -------------------------------------------------------------------------------- 1 | import { createTextVNode, h } from '../../lib/mini-vue3.esm.js' 2 | import { Bar } from './Bar.js' 3 | import { Baz } from './Baz.js' 4 | import { Foo } from './Foo.js' 5 | 6 | export const App = { 7 | name: 'App', 8 | setup() { 9 | return {} 10 | }, 11 | render() { 12 | // 传入一个 VNode 作为插槽 13 | // return h(Foo, {}, h('p', {}, 'a slot')) 14 | // 传入一个 VNode 数组,数组中每一项为一个插槽 15 | // return h(Foo, {}, [h('p', {}, 'a slot'), h('p', {}, 'another slot')]) 16 | // 传入一个对象,对象中每个 property 为一个插槽 17 | // return h( 18 | // Bar, 19 | // {}, 20 | // { 21 | // header: h('p', {}, 'header slot'), 22 | // footer: h('p', {}, 'footer slot') 23 | // } 24 | // ) 25 | // 传入一个对象,对象中方法为创建插槽的函数 26 | return h( 27 | Baz, 28 | {}, 29 | { 30 | content: props => [ 31 | h('p', {}, 'content: ' + props.msg), 32 | createTextVNode('a text node') 33 | ] 34 | } 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /example/update-Element-children_A-A/Array2Array.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue3.esm.js' 2 | 3 | const prev = [ 4 | h('span', { key: 'A' }, 'A'), 5 | h('span', { key: 'B' }, 'B'), 6 | h('span', { key: 'C', class: 'deleted' }, 'C'), 7 | h('span', { key: 'D' }, 'D'), 8 | h('span', { key: 'E' }, 'E'), 9 | h('span', { key: 'F' }, 'F'), 10 | h('span', { key: 'G' }, 'G') 11 | ] 12 | 13 | const next = [ 14 | h('span', { key: 'A' }, 'A'), 15 | h('span', { key: 'B' }, 'B'), 16 | h('span', { key: 'E', class: 'moved' }, 'E'), 17 | h('span', { key: 'D' }, 'D'), 18 | h('span', { key: 'X', class: 'added' }, 'X'), 19 | h('span', { key: 'F' }, 'F'), 20 | h('span', { key: 'G' }, 'G') 21 | ] 22 | 23 | export default { 24 | name: 'Array2Array', 25 | setup() { 26 | const isUpdateA2A = ref(false) 27 | window.isUpdateA2A = isUpdateA2A 28 | 29 | return { 30 | isUpdateA2A 31 | } 32 | }, 33 | render() { 34 | const self = this 35 | 36 | return h('div', {}, self.isUpdateA2A ? next : prev) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/update-Component/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue3.esm.js' 2 | import { Foo } from './Foo.js' 3 | 4 | export const App = { 5 | name: 'App', 6 | setup() { 7 | // 响应式数据 8 | const fooCount = ref(0) 9 | // 响应式数据 10 | const count = ref(0) 11 | 12 | const onChangeFooProps = () => { 13 | // 修改响应式数据 14 | fooCount.value++ 15 | } 16 | 17 | const onChangeCount = () => { 18 | // 修改响应式数据 19 | count.value++ 20 | } 21 | 22 | return { fooCount, count, onChangeFooProps, onChangeCount } 23 | }, 24 | render() { 25 | return h('div', {}, [ 26 | h(Foo, { 27 | // 创建 Foo 组件,向其中传入 count prop 28 | count: this.fooCount 29 | }), 30 | h( 31 | 'button', 32 | { 33 | onClick: this.onChangeFooProps 34 | }, 35 | 'change Foo count' 36 | ), 37 | h('div', {}, 'count: ' + this.count), 38 | h( 39 | 'button', 40 | { 41 | onClick: this.onChangeCount 42 | }, 43 | 'change count' 44 | ) 45 | ]) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/update-Element-children_T-A/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 38 | 39 | 40 | 41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /example/update-Element-children_A-A/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 41 | 42 | 43 | 44 |
45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/nextTick/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | ref, 4 | getCurrentInstance, 5 | nextTick 6 | } from '../../lib/mini-vue3.esm.js' 7 | 8 | export default { 9 | name: 'App', 10 | setup() { 11 | // 响应式数据 12 | const count = ref(0) 13 | const instance = getCurrentInstance() 14 | 15 | function onPlus() { 16 | // 修改响应式数据 17 | count.value++ 18 | 19 | // 同步获取视图中显示的内容 20 | console.log(instance.vnode.el.childNodes[0].innerHTML) 21 | 22 | // 在 nextTick 的回调函数中获取视图中显示的内容 23 | nextTick(() => { 24 | console.log(instance.vnode.el.childNodes[0].innerHTML) 25 | }) 26 | } 27 | 28 | async function onMinus() { 29 | // 修改响应式数据 30 | count.value-- 31 | 32 | // 同步获取视图中显示的内容 33 | console.log(instance.vnode.el.childNodes[0].innerHTML) 34 | 35 | // 等待 nextTick 执行后再获取视图中显示的内容 36 | await nextTick() 37 | console.log(instance.vnode.el.childNodes[0].innerHTML) 38 | } 39 | 40 | return { 41 | count, 42 | onPlus, 43 | onMinus 44 | } 45 | }, 46 | render() { 47 | return h('div', {}, [ 48 | h('p', {}, `count: ${this.count}`), 49 | h('button', { onClick: this.onPlus }, 'plus 1'), 50 | h('button', { onClick: this.onMinus }, 'minus 1') 51 | ]) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { computed } from '../src/computed' 2 | import { reactive } from '../src/reactive' 3 | 4 | describe('reactivity/computed', () => { 5 | it('should return updated value', () => { 6 | const value = reactive({ foo: 1 }) 7 | // 接受一个 getter 函数创建只读响应式 ref 对象, 8 | const cValue = computed(() => value.foo) 9 | expect(cValue.value).toBe(1) 10 | value.foo = 2 11 | expect(cValue.value).toBe(2) 12 | }) 13 | 14 | it('should compute lazily', () => { 15 | const value = reactive({ foo: 1 }) 16 | const getter = jest.fn(() => value.foo) 17 | const cValue = computed(getter) 18 | 19 | // 在获取 ref 对象的 value property 的值时才执行 getter 20 | expect(getter).not.toHaveBeenCalled() 21 | expect(cValue.value).toBe(1) 22 | expect(getter).toHaveBeenCalledTimes(1) 23 | // 若依赖的响应式对象的 property 的值没有更新,则再次获取 ref 对象的 value property 的值不会重复执行 getter 24 | cValue.value 25 | expect(getter).toHaveBeenCalledTimes(1) 26 | // 修改依赖的响应式对象的 property 的值时不会执行 getter 27 | value.foo = 1 28 | expect(getter).toHaveBeenCalledTimes(1) 29 | 30 | // 在依赖的响应式对象的 property 的值没有更新后,获取 ref 对象的 value property 的值再次执行 getter 31 | expect(cValue.value).toBe(1) 32 | expect(getter).toHaveBeenCalledTimes(2) 33 | cValue.value 34 | expect(getter).toHaveBeenCalledTimes(2) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/runtime-core/vnode.ts: -------------------------------------------------------------------------------- 1 | import { ShapeFlags } from '../shared' 2 | 3 | export const Fragment = Symbol('Fragment') 4 | export const Text = Symbol('Text') 5 | 6 | // 用于创建 VNode 7 | export function createVNode(type, props?, children?) { 8 | const vnode = { 9 | // HTML 标签名、组件 10 | type, 11 | // 保存 attribute、prop 和事件的对象 12 | props, 13 | // 子 VNode 14 | children, 15 | // 对应组件实例对象 16 | component: null, 17 | // VNode 和 children 类型的标志位 18 | shapeFlag: getShapeFlag(type), 19 | // 对应组件的 key attribute 20 | key: props?.key, 21 | // 对应组件的根元素 22 | el: null 23 | } 24 | 25 | // 根据 children 的类型设置 shapeFlag 对应的位 26 | if (typeof children === 'string') { 27 | vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN 28 | } else if (Array.isArray(children)) { 29 | vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN 30 | } 31 | 32 | // 若 VNode 类型为 Component 同时 children 类型为对象,则 children 为插槽,设置 shapeFlag 对应的位 33 | if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 34 | if (typeof children === 'object') { 35 | vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN 36 | } 37 | } 38 | 39 | return vnode 40 | } 41 | 42 | // 用于创建 Text 类型的 VNode 43 | export function createTextVNode(text: string) { 44 | return createVNode(Text, {}, text) 45 | } 46 | 47 | // 用于根据 VNode 的 type property 设置 shapeFlag 对应的位 48 | function getShapeFlag(type) { 49 | return typeof type === 'string' 50 | ? ShapeFlags.ELEMENT 51 | : ShapeFlags.STATEFUL_COMPONENT 52 | } 53 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/readonly.spec.ts: -------------------------------------------------------------------------------- 1 | import { isProxy, isReactive, isReadonly, readonly } from '../src/reactive' 2 | 3 | describe('reactivity/readonly', () => { 4 | it('should make values readonly', () => { 5 | const original = { foo: 1 } 6 | // 创建 readonly 响应式对象 7 | const wrapped = readonly(original) 8 | console.warn = jest.fn() 9 | // readonly 响应式对象与原始对象不相等 10 | expect(wrapped).not.toBe(original) 11 | // 对 readonly 响应式对象调用 isReactive 返回 false 12 | expect(isReactive(wrapped)).toBe(false) 13 | // 对 readonly 响应式对象调用 isReadonly 返回 true 14 | expect(isReadonly(wrapped)).toBe(true) 15 | // 对普通对象调用 isReactive 返回 false 16 | expect(isReactive(original)).toBe(false) 17 | // 对普通对象调用 isReadonly 返回 false 18 | expect(isReadonly(original)).toBe(false) 19 | // 对 readonly 响应式对象调用 isProxy 返回 true 20 | expect(isProxy(wrapped)).toBe(true) 21 | // 对普通对象调用 isProxy 返回 false 22 | expect(isProxy(original)).toBe(false) 23 | expect(wrapped.foo).toBe(1) 24 | // readonly 响应式对象的 property 是只读的 25 | wrapped.foo = 2 26 | expect(wrapped.foo).toBe(1) 27 | // 修改 readonly 响应式对象的 property 的值时会调用 console.warn 发出警告 28 | expect(console.warn).toBeCalled() 29 | }) 30 | 31 | it('should make nested values readonly', () => { 32 | const original = { foo: { bar: 1 } } 33 | const wrapped = readonly(original) 34 | // 嵌套对象是响应式的 35 | expect(isReadonly(wrapped.foo)).toBe(true) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/runtime-core/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from './component' 2 | 3 | // 用于注入依赖 4 | export function provide(key, value) { 5 | // 获取当前组件实例对象 6 | const currentInstance: any = getCurrentInstance() 7 | 8 | if (currentInstance) { 9 | // 通过解构赋值获取当前组件实例对象的 provides property 10 | let { provides } = currentInstance 11 | 12 | // 获取父组件实例对象的 provides property 13 | const parentProvides = currentInstance.parent.provides 14 | 15 | // 若判断当前组件实例对象和父组件实例对象的 provides property 相等,则是在当前组件 setup 中第一次调用 provide 函数 16 | if (provides === parentProvides) { 17 | // 利用 Object.create() 创建一个以父组件实例对象的 provides property 为原型的空对象,将其赋值给当前组件实例对象的 provides property 18 | provides = currentInstance.provides = Object.create(parentProvides) 19 | } 20 | 21 | // 将依赖挂载到当前组件实例对象的 provides property 上 22 | provides[key] = value 23 | } 24 | } 25 | 26 | // 用于引入依赖 27 | export function inject(key, defaultValue) { 28 | // 获取当前组件实例对象 29 | const currentInstance: any = getCurrentInstance() 30 | 31 | if (currentInstance) { 32 | // 获取父组件实例对象的 parent property 33 | const parentProvides = currentInstance.parent.provides 34 | 35 | // 若父组件实例对象的 provides property 上有相应的 property 则直接返回 36 | if (key in parentProvides) { 37 | return parentProvides[key] 38 | } 39 | // 否则,若传入了默认值或默认值函数则返回默认值或默认值函数的返回值 40 | else if (defaultValue) { 41 | if (typeof defaultValue === 'function') { 42 | return defaultValue() 43 | } 44 | 45 | return defaultValue 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/update-Element-props/App.js: -------------------------------------------------------------------------------- 1 | import { h, ref } from '../../lib/mini-vue3.esm.js' 2 | 3 | export const App = { 4 | name: 'App', 5 | setup() { 6 | const props = ref({ 7 | foo: 'foo', 8 | bar: 'bar', 9 | baz: 'baz' 10 | }) 11 | 12 | const onChangePropsDemo1 = () => { 13 | // 修改 props 对象中的 property 14 | props.value.foo = 'newFoo' 15 | } 16 | 17 | const onChangePropsDemo2 = () => { 18 | // 将 props 对象中的 property 赋值为 undefined 或 null 19 | props.value.bar = undefined 20 | } 21 | 22 | const onChangePropsDemo3 = () => { 23 | // 删除 props 对象中的 property 24 | const { baz, ...newProps } = props.value 25 | props.value = newProps 26 | } 27 | 28 | return { 29 | props, 30 | onChangePropsDemo1, 31 | onChangePropsDemo2, 32 | onChangePropsDemo3 33 | } 34 | }, 35 | render() { 36 | return h( 37 | 'div', 38 | { 39 | id: 'demo', 40 | ...this.props 41 | }, 42 | [ 43 | h( 44 | 'button', 45 | { 46 | onClick: this.onChangePropsDemo1 47 | }, 48 | 'Demo1' 49 | ), 50 | h( 51 | 'button', 52 | { 53 | onClick: this.onChangePropsDemo2 54 | }, 55 | 'Demo2' 56 | ), 57 | h( 58 | 'button', 59 | { 60 | onClick: this.onChangePropsDemo3 61 | }, 62 | 'Demo3' 63 | ) 64 | ] 65 | ) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /example/provide-inject/App.js: -------------------------------------------------------------------------------- 1 | import { h, provide, inject } from '../../lib/mini-vue3.esm.js' 2 | 3 | // 第一级组件 4 | const Provider_I = { 5 | name: 'Provider_I', 6 | setup() { 7 | // 通过 provide 注入 foo 和 bar 8 | provide('foo', 'FooFromI') 9 | provide('bar', 'BarFromI') 10 | }, 11 | render() { 12 | return h('div', {}, [h('p', {}, 'Provider_I'), h(Provider_II)]) 13 | } 14 | } 15 | 16 | // 第二级组件 17 | const Provider_II = { 18 | name: 'Provider_II', 19 | setup() { 20 | // 通过 provide 注入 foo 21 | provide('foo', 'FooFromII') 22 | }, 23 | render() { 24 | return h('div', {}, [h('p', {}, 'Provider_II'), h(Consumer)]) 25 | } 26 | } 27 | 28 | // 第三级组件 29 | const Consumer = { 30 | name: 'Consumer', 31 | setup() { 32 | // 通过 inject 引入 foo 和 bar 33 | const foo = inject('foo') // => FooFromII 34 | const bar = inject('bar') // => BarFromI 35 | 36 | // 通过 inject 引入 baz,同时传入默认值或默认值函数 37 | const baz1 = inject('baz', 'defaultBaz1') // => defaultBaz1 38 | const baz2 = inject('baz', () => 'defaultBaz2') // => defaultBaz2 39 | 40 | return { 41 | foo, 42 | bar, 43 | baz1, 44 | baz2 45 | } 46 | }, 47 | render() { 48 | return h('div', {}, [ 49 | h( 50 | 'p', 51 | {}, 52 | `Consumer: inject ${this.foo}, ${this.bar}, ${this.baz1}, and ${this.baz2}` 53 | ) 54 | ]) 55 | } 56 | } 57 | 58 | export default { 59 | name: 'App', 60 | setup() {}, 61 | render() { 62 | return h('div', {}, [h('p', {}, 'provide-inject'), h(Provider_I)]) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/reactivity/src/reactive.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from '../../shared' 2 | import { 3 | mutableHandlers, 4 | ReactiveFlags, 5 | readonlyHandlers, 6 | shallowHandlers, 7 | shallowReadonlyHandlers 8 | } from './baseHandlers' 9 | 10 | export function reactive(raw) { 11 | return createReactiveObject(raw, mutableHandlers) 12 | } 13 | 14 | export function readonly(raw) { 15 | return createReactiveObject(raw, readonlyHandlers) 16 | } 17 | 18 | export function shallowReactive(raw) { 19 | return createReactiveObject(raw, shallowHandlers) 20 | } 21 | 22 | export function shallowReadonly(raw) { 23 | return createReactiveObject(raw, shallowReadonlyHandlers) 24 | } 25 | 26 | // 用于创建 Proxy 实例的工具函数 27 | function createReactiveObject(raw, baseHandlers) { 28 | // 返回 Proxy 的实例 29 | return new Proxy(raw, baseHandlers) 30 | } 31 | 32 | // 用于检查对象是否是由 reactive 创建的响应式对象 33 | export function isReactive(value): boolean { 34 | // 获取对象的某个特殊 property 的值,从而触发 get,property 名为 __v_isReactive 35 | return !!value[ReactiveFlags.IS_REACTIVE] 36 | } 37 | 38 | // 用于检查对象是否是由 readonly 创建的 readonly 响应式对象 39 | export function isReadonly(value): boolean { 40 | // 获取对象的某个特殊 property 的值,从而触发 get,property 名为 __v_isReactive 41 | return !!value[ReactiveFlags.IS_READONLY] 42 | } 43 | 44 | // 用于检查对象是否是由 reactive 或 readonly 创建的响应式对象 45 | export function isProxy(value): boolean { 46 | // 利用 isReactive 和 isReadonly 进行判断 47 | return isReactive(value) || isReadonly(value) 48 | } 49 | 50 | // 用于对值进行处理,若为对象则利用 reactive 进行响应式处理,否则直接返回 51 | export const toReactive = value => (isObject(value) ? reactive(value) : value) 52 | -------------------------------------------------------------------------------- /example/update-Element-children_T-A/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue3.esm.js' 2 | 3 | import Array2Text from './Array2Text.js' 4 | import Text2Text from './Text2Text.js' 5 | import Text2Array from './Text2Array.js' 6 | 7 | export const App = { 8 | name: 'App', 9 | setup() { 10 | const onUpdateA2T = () => { 11 | // 触发 Array2Text 组件中 Element 的更新 12 | window.isUpdateA2T.value = true 13 | } 14 | 15 | const onUpdateT2T = () => { 16 | // 触发 Text2Text 组件中 Element 的更新 17 | window.isUpdateT2T.value = true 18 | } 19 | 20 | const onUpdateT2A = () => { 21 | // 触发 Text2Array 组件中 Element 的更新 22 | window.isUpdateT2A.value = true 23 | } 24 | 25 | return { onUpdateA2T, onUpdateT2T, onUpdateT2A } 26 | }, 27 | render() { 28 | return h('div', { class: 'container' }, [ 29 | // Array2Text 的测试 30 | h('div', { class: 'demo demo-array2text' }, [ 31 | // Array2Text 组件 32 | h(Array2Text), 33 | h( 34 | 'button', 35 | { 36 | onCLick: this.onUpdateA2T 37 | }, 38 | 'Array2Text' 39 | ) 40 | ]), 41 | // Text2Text 的测试 42 | h('div', { class: 'demo demo-text2text' }, [ 43 | // Text2Text 组件 44 | h(Text2Text), 45 | h( 46 | 'button', 47 | { 48 | onCLick: this.onUpdateT2T 49 | }, 50 | 'Text2Text' 51 | ) 52 | ]), 53 | // Text2Array 的测试 54 | h('div', { class: 'demo demo-text2array' }, [ 55 | // Text2Array 组件 56 | h(Text2Array), 57 | h( 58 | 'button', 59 | { 60 | onCLick: this.onUpdateT2A 61 | }, 62 | 'Text2Array' 63 | ) 64 | ]) 65 | ]) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '../src/effect' 2 | import { reactive } from '../src/reactive' 3 | import { isRef, proxyRefs, ref, unref } from '../src/ref' 4 | 5 | describe('reactivity/ref', () => { 6 | it('should hold a value', () => { 7 | // 创建 ref 对象 8 | const a = ref(1) 9 | // ref 对象的 value property 的值等于传入的值 10 | expect(a.value).toBe(1) 11 | // ref 对象的 value property 的值是可变的 12 | a.value = 2 13 | expect(a.value).toBe(2) 14 | }) 15 | 16 | it('should be reactive', () => { 17 | const a = ref(1) 18 | let dummy 19 | let calls = 0 20 | effect(() => { 21 | calls++ 22 | dummy = a.value 23 | }) 24 | expect(calls).toBe(1) 25 | expect(dummy).toBe(1) 26 | // ref 对象是响应式的 27 | a.value = 2 28 | expect(calls).toBe(2) 29 | expect(dummy).toBe(2) 30 | // ref 对象的 value property 的 set 具有缓存,不会重复触发依赖 31 | a.value = 2 32 | expect(calls).toBe(2) 33 | expect(dummy).toBe(2) 34 | }) 35 | 36 | it('should make nested properties reactive', () => { 37 | const a = ref({ 38 | count: 1 39 | }) 40 | let dummy 41 | effect(() => { 42 | dummy = a.value.count 43 | }) 44 | expect(dummy).toBe(1) 45 | // ref 对象的 value property 的是一个响应式对象 46 | a.value.count = 2 47 | expect(dummy).toBe(2) 48 | }) 49 | 50 | it('isRef', () => { 51 | expect(isRef(ref(1))).toBe(true) 52 | expect(isRef(reactive({ foo: 1 }))).toBe(false) 53 | expect(isRef(0)).toBe(false) 54 | expect(isRef({ bar: 0 })).toBe(false) 55 | }) 56 | 57 | it('unref', () => { 58 | expect(unref(1)).toBe(1) 59 | expect(unref(ref(1))).toBe(1) 60 | }) 61 | 62 | it('proxyRefs', () => { 63 | const obj = { 64 | foo: ref(1), 65 | bar: 'baz' 66 | } 67 | const proxyObj = proxyRefs(obj) 68 | expect(proxyObj.foo).toBe(1) 69 | expect(proxyObj.bar).toBe('baz') 70 | 71 | proxyObj.foo = 2 72 | expect(proxyObj.foo).toBe(2) 73 | 74 | proxyObj.foo = ref(3) 75 | expect(proxyObj.foo).toBe(3) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /src/runtime-dom/index.ts: -------------------------------------------------------------------------------- 1 | import { createRenderer } from '../runtime-core' 2 | 3 | // 用于创建元素 4 | function createElement(type) { 5 | // 利用 document.createElement() 创建 DOM 元素 6 | return document.createElement(type) 7 | } 8 | 9 | // 用于将 props 对象中的 property 或方法添加到元素上 10 | function patchProp(el, key, nextVal) { 11 | // 用于通过正则判断该 property 的 key 是否以 on 开头,是则为注册事件,否则为 attribute 或 property 12 | const isOn = (key: string) => /^on[A-Z]/.test(key) 13 | 14 | // 若为注册事件 15 | if (isOn(key)) { 16 | // 利用 Element.addEventListener() 将该方法添加到元素上 17 | // 其中 key 去掉前两位(也就是 on)再转为小写后的字符串作为事件名,value 作为 listener 18 | const event = key.slice(2).toLowerCase() 19 | el.addEventListener(event, nextVal) 20 | } 21 | // 否则 22 | else { 23 | // 若 props 对象中的 property 为 undefined 或 null 24 | if (nextVal == null) { 25 | // 利用 Element.removeAttribute() 将该 property 从元素上移除 26 | el.removeAttribute(key) 27 | } 28 | // 否则 29 | else { 30 | // 利用 Element.setAttribute() 将该 property 添加到元素上 31 | // 其中 key 作为元素的 attribute 或 property 名,value 作为 attribute 或 property 的值 32 | el.setAttribute(key, nextVal) 33 | } 34 | } 35 | } 36 | 37 | // 用于将元素添加到根容器/父元素中 38 | function insert(child, parent, anchor) { 39 | // 利用 Element.insertBefore() 将元素添加到根容器/父元素中 40 | parent.insertBefore(child, anchor || null) 41 | } 42 | 43 | // 用于创建文本节点 44 | function createText(text) { 45 | // 利用 document.createTextNode() 创建文本节点 46 | return document.createTextNode(text) 47 | } 48 | 49 | // 用于移除元素 50 | function remove(child) { 51 | // 获取当前元素的父元素 52 | const parent = child.parentNode 53 | 54 | if (parent) { 55 | // 利用 Element.removeChild() 将元素从其父元素中移除 56 | parent.removeChild(child) 57 | } 58 | } 59 | 60 | // 用于修改元素的文本内容 61 | function setElementText(el, text) { 62 | el.textContent = text 63 | } 64 | 65 | /** 66 | * 调用 createRenderer 函数,并传入包含 createText 函数、createElement 函数、 67 | * patchProp 函数、insert 函数、remove 函数和 setElementText 函数的对象 68 | */ 69 | const renderer: any = createRenderer({ 70 | createText, 71 | createElement, 72 | patchProp, 73 | insert, 74 | remove, 75 | setElementText 76 | }) 77 | 78 | // 用于创建应用实例 79 | export function createApp(...args) { 80 | // 调用 createRenderer 函数返回对象的 createApp 方法 81 | return renderer.createApp(...args) 82 | } 83 | 84 | export * from '../runtime-core' 85 | -------------------------------------------------------------------------------- /src/reactivity/src/ref.ts: -------------------------------------------------------------------------------- 1 | import { hasChanged } from '../../shared' 2 | import { isTracking, trackEffects, triggerEffects } from './effect' 3 | import { toReactive } from './reactive' 4 | 5 | // ref 对象的接口 6 | interface Ref { 7 | value 8 | } 9 | 10 | // Ref 接口的实现类,对操作进行封装 11 | class RefImpl { 12 | // 用于保存传入的值和 set 的值 13 | private _rawValue 14 | private _value 15 | // 用于保存与当前 ref 对象相关的依赖 16 | private dep 17 | // 用于标志实例是一个 ref 对象 18 | public __v_isRef = true 19 | 20 | constructor(value) { 21 | // 将传入的值赋值给实例的私有 property _rawValue 22 | this._rawValue = value 23 | // 对传入的值进行处理,将结果赋值给实例的私有 property _value 24 | this._value = toReactive(value) 25 | this.dep = new Set() 26 | } 27 | 28 | // value property 的 get 返回私有 property _value 的值 29 | get value() { 30 | if (isTracking()) { 31 | // 收集依赖 32 | trackEffects(this.dep) 33 | } 34 | 35 | // 返回实例的私有 property _value 的值 36 | return this._value 37 | } 38 | 39 | // value property 的 set 修改私有 property _value 的值 40 | set value(newVal) { 41 | // 若 set 的值与之前不同则修改并触发依赖 42 | if (hasChanged(newVal, this._rawValue)) { 43 | // 将 set 的值赋值给实例的私有 property _rawValue 44 | this._rawValue = newVal 45 | // 对 set 的值进行处理,将结果赋值给实例的私有 property _value 46 | this._value = toReactive(newVal) 47 | // 触发依赖 48 | triggerEffects(this.dep) 49 | } 50 | } 51 | } 52 | 53 | export function ref(value): Ref { 54 | // 返回 RefImpl 类的实例,即 ref 对象 55 | return new RefImpl(value) 56 | } 57 | 58 | // 用于判断一个值是否是 ref 对象 59 | export function isRef(value): boolean { 60 | return !!value.__v_isRef 61 | } 62 | 63 | // 用于获取 ref 对象的 value property 的值 64 | export function unref(ref) { 65 | return isRef(ref) ? ref.value : ref 66 | } 67 | 68 | export function proxyRefs(objectWithRefs) { 69 | // 返回 Proxy 的实例 70 | return new Proxy(objectWithRefs, { 71 | // 对传入的对象的 property 的 get 和 set 进行代理 72 | get: function (target, key) { 73 | // 获取传入的对象的 property 的值,再调用 unref 进行处理 74 | return unref(Reflect.get(target, key)) 75 | }, 76 | set: function (target, key, value) { 77 | const oldValue = target[key] 78 | // 若传入的对象的 property 的值是一个 ref 对象,而 set 的值不是一个 ref 对象,则修改该 ref 对象的值,否则直接修改 property 的值 79 | if (isRef(oldValue) && !isRef(value)) { 80 | oldValue.value = value 81 | return true 82 | } else { 83 | return Reflect.set(target, key, value) 84 | } 85 | } 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /src/reactivity/src/baseHandlers.ts: -------------------------------------------------------------------------------- 1 | import { extend, isObject } from '../../shared' 2 | import { track, trigger } from './effect' 3 | import { reactive, readonly } from './reactive' 4 | 5 | // 对 get 和 set 进行缓存,防止重复调用工具函数 6 | const get = createGetter() 7 | const readonlyGet = createGetter(true) 8 | const shallowGet = createGetter(false, true) 9 | const shallowReadonlyGet = createGetter(true, true) 10 | const set = createSetter() 11 | 12 | // 用于保存 isReactive 和 isReadonly 中使用的特殊 property 的名 13 | export const enum ReactiveFlags { 14 | IS_REACTIVE = '__v_isReactive', 15 | IS_READONLY = '__v_isReadonly' 16 | } 17 | 18 | // 用于生成 get 函数的工具函数 19 | function createGetter(isReadonly = false, shallow = false) { 20 | return function (target, key) { 21 | // 当 property 名为 __v_isReactive 时,表明正在调用 isReactive,直接返回 !isReadonly 22 | if (key === ReactiveFlags.IS_REACTIVE) { 23 | return !isReadonly 24 | } 25 | // 当 property 名为 __v_isReadonly 时,表明正在调用 isReadonly,直接返回 isReadonly 26 | else if (key === ReactiveFlags.IS_READONLY) { 27 | return isReadonly 28 | } 29 | 30 | const res = Reflect.get(target, key) 31 | 32 | // 利用 reactive 和 shallowReactive 进行响应式转换时才进行依赖收集 33 | if (!isReadonly) { 34 | // 收集依赖 35 | track(target, key) 36 | } 37 | 38 | // 若利用 shallowReactive 和 shallowReadonly 进行响应式转换则直接返回 39 | if (shallow) { 40 | return res 41 | } 42 | 43 | // 若 property 的值为对象,则利用 reactive 和 readonly 进行响应式转换 44 | if (isObject(res)) { 45 | return isReadonly ? readonly(res) : reactive(res) 46 | } 47 | 48 | return res 49 | } 50 | } 51 | 52 | // 用于生成 set 函数的工具函数 53 | function createSetter() { 54 | return function (target, key, value) { 55 | const res = Reflect.set(target, key, value) 56 | // 触发依赖 57 | trigger(target, key) 58 | return res 59 | } 60 | } 61 | 62 | // reactive 对应的 handlers 63 | export const mutableHandlers = { 64 | get, 65 | set 66 | } 67 | 68 | // readonly 对应的 handlers 69 | export const readonlyHandlers = { 70 | get: readonlyGet, 71 | set(target, key) { 72 | // 调用 console.warn 发出警告 73 | console.warn( 74 | `Set operation on key "${key}" failed: target is readonly.`, 75 | target 76 | ) 77 | return true 78 | } 79 | } 80 | 81 | // shallowRreactive 对应的 handlers 是由 mutableHandlers 替换 get property 得到的 82 | export const shallowHandlers = extend({}, mutableHandlers, { 83 | get: shallowGet 84 | }) 85 | 86 | // shallowReadonly 对应的 handlers 是由 readonlyHandlers 替换 get property 得到的 87 | export const shallowReadonlyHandlers = extend({}, readonlyHandlers, { 88 | get: shallowReadonlyGet 89 | }) 90 | -------------------------------------------------------------------------------- /src/reactivity/__tests__/effect.spec.ts: -------------------------------------------------------------------------------- 1 | import { effect, stop } from '../src/effect' 2 | import { reactive } from '../src/reactive' 3 | 4 | describe('effect', () => { 5 | it('should run the passed function once (wrapped by a effect)', () => { 6 | // 创建 mock 函数 7 | const fnSpy = jest.fn(() => {}) 8 | effect(fnSpy) 9 | // 当程序执行时,传入的方法会被执行 10 | expect(fnSpy).toHaveBeenCalledTimes(1) 11 | }) 12 | 13 | it('should observe basic properties', () => { 14 | let dummy 15 | // 创建响应式对象 16 | const counter = reactive({ num: 0 }) 17 | // 在传入的函数中使用了响应式对象的 property 18 | effect(() => (dummy = counter.num)) 19 | 20 | expect(dummy).toBe(0) 21 | // 当该 property 的值更新时,会再次执行该函数 22 | counter.num = 7 23 | expect(dummy).toBe(7) 24 | }) 25 | 26 | it('should return a function to be called manually', () => { 27 | let foo = 0 28 | // 用一个变量 runner 接受 effect 执行返回的函数 29 | const runner = effect(() => { 30 | foo++ 31 | return 'foo' 32 | }) 33 | expect(foo).toBe(1) 34 | // 调用 runner 时会再次执行传入的函数 35 | const res = runner() 36 | expect(foo).toBe(2) 37 | // runner 执行返回该函数的返回值 38 | expect(res).toBe('foo') 39 | }) 40 | 41 | it('scheduler', () => { 42 | let dummy 43 | let run: number 44 | // 创建 mock 函数 45 | const scheduler = jest.fn(() => { 46 | run++ 47 | }) 48 | const obj = reactive({ foo: 1 }) 49 | const runner = effect( 50 | () => { 51 | dummy = obj.foo 52 | }, 53 | { scheduler } 54 | ) 55 | // 程序运行时会首先执行传入的函数,而不会调用 scheduler 方法 56 | expect(scheduler).not.toHaveBeenCalled() 57 | expect(dummy).toBe(1) 58 | // 当传入的函数依赖的响应式对象的 property 的值更新时,会调用 scheduler 方法而不会执行传入的函数 59 | obj.foo++ 60 | expect(scheduler).toHaveBeenCalledTimes(1) 61 | expect(dummy).toBe(1) 62 | // 只有当调用 runner 时才会执行传入的函数 63 | runner() 64 | expect(scheduler).toHaveBeenCalledTimes(1) 65 | expect(dummy).toBe(2) 66 | }) 67 | 68 | it('stop', () => { 69 | let dummy 70 | const obj = reactive({ prop: 1 }) 71 | const runner = effect(() => { 72 | dummy = obj.prop 73 | }) 74 | obj.prop = 2 75 | expect(dummy).toBe(2) 76 | // 调用`stop`函数后,当传入的函数依赖的响应式对象的 property 的值更新时不会再执行该函数 77 | stop(runner) 78 | obj.prop = 3 79 | expect(dummy).toBe(2) 80 | obj.prop++ 81 | expect(dummy).toBe(2) 82 | // 只有当调用`runner`时才会恢复执行该函数 83 | runner() 84 | expect(dummy).toBe(4) 85 | }) 86 | 87 | it('events: onStop', () => { 88 | // 创建 mock 函数 89 | const onStop = jest.fn() 90 | const runner = effect(() => {}, { 91 | onStop 92 | }) 93 | 94 | // 调用 stop 函数时,会执行 onStop 方法 95 | stop(runner) 96 | expect(onStop).toHaveBeenCalled() 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /src/runtime-core/component.ts: -------------------------------------------------------------------------------- 1 | import { proxyRefs, shallowReadonly } from '../reactivity/src' 2 | import { emit } from './componentEmit' 3 | import { initProps } from './componentProps' 4 | import { PublicInstanceHandlers } from './componentPublicInstance' 5 | import { initSlots } from './componentSlots' 6 | 7 | // 用于创建组件实例对象 8 | export function createComponentInstance(vnode, parent) { 9 | const component = { 10 | vnode, 11 | type: vnode.type, 12 | setupState: {}, 13 | props: {}, 14 | slots: {}, 15 | // 若存在父组件则赋值为 父组件实例对象的 provides property,否则为空对象 16 | provides: parent ? parent.provides : {}, 17 | parent, 18 | subTree: {}, 19 | update: () => {}, 20 | next: null, 21 | proxy: null, 22 | isMounted: false, 23 | emit: () => {} 24 | } 25 | 26 | // 通过 Function.prototype.bind() 将 emit 函数第一个参数指定为组件实例对象,将新函数赋值给组件实例对象的 emit 方法 27 | component.emit = emit.bind(null, component) as any 28 | 29 | return component 30 | } 31 | 32 | // 用于初始化 props、初始化 slots 和调用 setup 以及设置 render 函数 33 | export function setupComponent(instance) { 34 | // 将组件对应 VNode 的 props property 赋值给组件实例对象的 props property 35 | initProps(instance, instance.vnode.props) 36 | 37 | // 若 children 为插槽则将其挂载到组件实例对象的 slots property 上 38 | initSlots(instance, instance.vnode.children) 39 | 40 | setupStatefulComponent(instance) 41 | } 42 | 43 | // 用于初始化有状态的组件(相对的是没有状态的函数式组件) 44 | function setupStatefulComponent(instance) { 45 | // 通过组件实例对象的 type property 获取组件选项对象 46 | const Component = instance.type 47 | 48 | // 利用 Proxy 对组件实例对象的 proxy property 的 get 进行代理 49 | instance.proxy = new Proxy({ _: instance }, PublicInstanceHandlers) 50 | 51 | // 通过解构赋值获取组件选项对象中的 setup 52 | const { setup } = Component 53 | 54 | // 若组件选项对象中包含 setup 则调用该方法并处理其返回值 55 | if (setup) { 56 | // 将全局变量 currentInstance 赋值为当前组件实例对象 57 | setCurrentInstance(instance) 58 | 59 | // 调用 setup 传入 props 对象的 shallowReactive 响应式副本和包含 emit 方法的对象并获取其返回值 60 | const setupResult = setup(shallowReadonly(instance.props), { 61 | emit: instance.emit 62 | }) 63 | 64 | // 将全局变量 currentInstance 赋值为 null 65 | setCurrentInstance(null) 66 | 67 | // 处理 setup 的返回值 68 | handleSetupResult(instance, setupResult) 69 | } 70 | } 71 | 72 | // 用于处理 setup 的返回值 73 | function handleSetupResult(instance, setupResult) { 74 | // 根据 setup 返回值类型的不同进行不同的处理 75 | // 若返回一个对象则调用 proxyRefs 并传入该对象,将返回值赋值给组件实例对象的 setupState property 76 | if (typeof setupResult === 'object') { 77 | instance.setupState = proxyRefs(setupResult) 78 | } 79 | // 若返回一个函数则将其作为组件的 render 函数 80 | else if (typeof setupResult === 'function') { 81 | // TODO: 处理 function 82 | } 83 | 84 | finishComponentSetup(instance) 85 | } 86 | 87 | // 用于设置 render 函数 88 | function finishComponentSetup(instance) { 89 | // 通过组件实例对象的 type property 获取组件选项对象 90 | const Component = instance.type 91 | 92 | // 将组件选项对象中的 render 函数赋值给组件实例对象的 render 方法 93 | if (Component.render) { 94 | instance.render = Component.render 95 | } 96 | } 97 | 98 | // 用于保存当前组件实例对象 99 | let currentInstance = null 100 | 101 | // 用于获取当前组件的实例对象 102 | export function getCurrentInstance() { 103 | return currentInstance 104 | } 105 | 106 | // 用于给全局变量 currentInstance 赋值 107 | function setCurrentInstance(instance) { 108 | currentInstance = instance 109 | } 110 | -------------------------------------------------------------------------------- /example/update-Element-children_A-A/App.js: -------------------------------------------------------------------------------- 1 | import { h } from '../../lib/mini-vue3.esm.js' 2 | 3 | import AddHead from './AddHead.js' 4 | import AddTail from './AddTail.js' 5 | import DelHead from './DelHead.js' 6 | import DelTail from './DelTail.js' 7 | import DelMid from './DelMid.js' 8 | import AddMid from './AddMid.js' 9 | import MoveMid from './MoveMid.js' 10 | import Array2Array from './Array2Array.js' 11 | 12 | export const App = { 13 | name: 'App', 14 | setup() { 15 | const onAddHead = () => { 16 | // 触发 AddHead 组件中 Element 的更新 17 | window.isAddHead.value = true 18 | } 19 | 20 | const onAddTail = () => { 21 | // 触发 AddTail 组件中 Element 的更新 22 | window.isAddTail.value = true 23 | } 24 | 25 | const onAddMid = () => { 26 | // 触发 AddMid 组件中 Element 的更新 27 | window.isAddMid.value = true 28 | } 29 | 30 | const onDelHead = () => { 31 | // 触发 DelHead 组件中 Element 的更新 32 | window.isDelHead.value = true 33 | } 34 | 35 | const onDelTail = () => { 36 | // 触发 DelTail 组件中 Element 的更新 37 | window.isDelTail.value = true 38 | } 39 | 40 | const onDelMid = () => { 41 | // 触发 DelMid 组件中 Element 的更新 42 | window.isDelMid.value = true 43 | } 44 | 45 | const onMoveMid = () => { 46 | // 触发 MoveMid 组件中 Element 的更新 47 | window.isMoveMid.value = true 48 | } 49 | 50 | const onUpdateA2A = () => { 51 | // 触发 Array2Array 组件中 Element 的更新 52 | window.isUpdateA2A.value = true 53 | } 54 | 55 | return { 56 | onAddHead, 57 | onAddTail, 58 | onAddMid, 59 | onDelHead, 60 | onDelTail, 61 | onDelMid, 62 | onMoveMid, 63 | onUpdateA2A 64 | } 65 | }, 66 | render() { 67 | return h('div', { class: 'container' }, [ 68 | // AddHead 的测试 69 | h('div', { class: 'demo add-head' }, [ 70 | // AddHead 组件 71 | h(AddHead), 72 | h( 73 | 'button', 74 | { 75 | onCLick: this.onAddHead 76 | }, 77 | 'AddHead' 78 | ) 79 | ]), 80 | // AddTail 的测试 81 | h('div', { class: 'demo add-tail' }, [ 82 | // AddTail 组件 83 | h(AddTail), 84 | h( 85 | 'button', 86 | { 87 | onCLick: this.onAddTail 88 | }, 89 | 'AddTail' 90 | ) 91 | ]), 92 | // AddMid 的测试 93 | h('div', { class: 'demo add-mid' }, [ 94 | // AddMid 组件 95 | h(AddMid), 96 | h( 97 | 'button', 98 | { 99 | onCLick: this.onAddMid 100 | }, 101 | 'AddMid' 102 | ) 103 | ]), 104 | // DelHead 的测试 105 | h('div', { class: 'demo del-head' }, [ 106 | // DelHead 组件 107 | h(DelHead), 108 | h( 109 | 'button', 110 | { 111 | onCLick: this.onDelHead 112 | }, 113 | 'DelHead' 114 | ) 115 | ]), 116 | // DelTail 的测试 117 | h('div', { class: 'demo del-tail' }, [ 118 | // DelTail 组件 119 | h(DelTail), 120 | h( 121 | 'button', 122 | { 123 | onCLick: this.onDelTail 124 | }, 125 | 'DelTail' 126 | ) 127 | ]), 128 | // DelMid 的测试 129 | h('div', { class: 'demo del-mid' }, [ 130 | // DelMid 组件 131 | h(DelMid), 132 | h( 133 | 'button', 134 | { 135 | onCLick: this.onDelMid 136 | }, 137 | 'DelMid' 138 | ) 139 | ]), 140 | // MoveMid 的测试 141 | h('div', { class: 'demo move-mid' }, [ 142 | // MoveMid 组件 143 | h(MoveMid), 144 | h( 145 | 'button', 146 | { 147 | onCLick: this.onMoveMid 148 | }, 149 | 'MoveMid' 150 | ) 151 | ]), 152 | // Array2Array 的测试 153 | h('div', { class: 'demo array2array' }, [ 154 | // Array2Array 组件 155 | h(Array2Array), 156 | h( 157 | 'button', 158 | { 159 | onCLick: this.onUpdateA2A 160 | }, 161 | 'Arr2Arr' 162 | ) 163 | ]) 164 | ]) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/reactivity/src/effect.ts: -------------------------------------------------------------------------------- 1 | import { extend } from '../../shared' 2 | // 用于记录是否应该收集依赖,防止调用 stop 后触发响应式对象的 property 的 get 时收集依赖 3 | let shouldTrack: boolean = false 4 | 5 | // 抽离出一个 ReactiveEffect 类,对操作进行封装 6 | export class ReactiveEffect { 7 | private _fn: any 8 | // 用于保存与当前实例相关的响应式对象的 property 对应的 Set 实例 9 | deps: Array> = [] 10 | // 用于记录当前实例状态,为 true 时未调用 stop 方法,否则已调用,防止重复调用 stop 方法 11 | active: boolean = true 12 | // 用于保存当前实例的 onStop 方法 13 | onStop?: () => void 14 | 15 | // 构造函数接受可选的第二个参数,保存为实例的公共变量 scheduler 16 | constructor(fn, public scheduler?) { 17 | // 将传入的函数赋值给实例的私有 property _fn 18 | this._fn = fn 19 | } 20 | 21 | // 用于执行传入的函数 22 | run() { 23 | // 若已调用 stop 方法则直接返回传入的函数执行的结果 24 | if (!this.active) { 25 | return this._fn() 26 | } 27 | 28 | // 应该收集依赖 29 | shouldTrack = true 30 | // 调用 run 方法时,用全局变量 activeEffect 保存当前实例 31 | activeEffect = this 32 | 33 | const res = this._fn() 34 | // 重置 35 | shouldTrack = false 36 | 37 | // 返回传入的函数执行的结果 38 | return res 39 | } 40 | 41 | // 用于停止传入的函数的执行 42 | stop() { 43 | if (this.active) { 44 | cleanupEffect(this) 45 | // 在调用 stop 方法时,调用 onStop 方法 46 | if (this.onStop) { 47 | this.onStop() 48 | } 49 | this.active = false 50 | } 51 | } 52 | } 53 | 54 | // 用于将传入的 ReactiveEffect 类的实例从与该实例相关的响应式对象的 property 对应的 Set 实例中删除 55 | function cleanupEffect(effect: ReactiveEffect) { 56 | effect.deps.forEach((dep: any) => { 57 | dep.delete(effect) 58 | }) 59 | } 60 | 61 | // 接受一个函数作为第一个参数,接受一个对象作为第二个参数 62 | export function effect(fn, options: any = {}) { 63 | // 利用传入的函数创建 ReactiveEffect 类的实例,并将 scheduler 方法传给 ReactiveEffect 类的构造函数 64 | const _effect: ReactiveEffect = new ReactiveEffect(fn, options.scheduler) 65 | // 将第二个参数即 options 对象的属性和方法赋值给 ReactiveEffect 类的实例 66 | extend(_effect, options) 67 | 68 | // 调用 ReactiveEffect 实例的 run 方法,执行传入的函数 69 | _effect.run() 70 | 71 | // 用一个变量 runner 保存将 _effect.run 的 this 指向指定为 _effect 的结果 72 | const runner: any = _effect.run.bind(_effect) 73 | // 将 _effect 赋值给 runner 的 effect property 74 | runner.effect = _effect 75 | 76 | // 返回 runner 77 | return runner 78 | } 79 | 80 | /** 81 | * 用于保存程序运行中的所有依赖 82 | * key 为响应式对象 83 | * value 为 Map 的实例,用于保存该响应式对象的所有依赖 84 | */ 85 | const targetsMap = new WeakMap() 86 | 87 | // 用于保存正在执行的 ReactiveEffect 类的实例 88 | let activeEffect: ReactiveEffect | undefined 89 | 90 | // 用于收集依赖 91 | export function track(target, key) { 92 | // 若不应该收集依赖则直接返回 93 | if (!isTracking()) { 94 | return 95 | } 96 | 97 | // 获取当前响应式对象对应的 Map 实例,若为 undefined 则进行初始化并保存到 targetsMap 中 98 | /** 99 | * 用于保存当前响应式对象的所有依赖 100 | * key 为响应式对象的 property 101 | * value 为 Set 的实例,用于保存与该 property 相关的 ReactiveEffect 类的实例 102 | */ 103 | let depsMap: Map> | undefined = 104 | targetsMap.get(target) 105 | 106 | if (!depsMap) { 107 | depsMap = new Map>() 108 | targetsMap.set(target, depsMap) 109 | } 110 | 111 | // 获取当前 property 对应的 Set 实例,若为 undefined 则进行初始化并保存到 depsMap 中 112 | /** 113 | * 用于保存与当前 property 相关的函数 114 | * value 为与该 property 相关的 ReactiveEffect 类的实例 115 | */ 116 | let dep: Set | undefined = depsMap.get(key) 117 | 118 | if (!dep) { 119 | dep = new Set() 120 | depsMap.set(key, dep) 121 | } 122 | 123 | trackEffects(dep) 124 | } 125 | 126 | // 用于判断是否应该收集依赖 127 | export function isTracking() { 128 | return shouldTrack && activeEffect !== undefined 129 | } 130 | 131 | // 用于将当前正在执行的 ReactiveEffect 类的实例添加到 dep 中, 同时将 dep 添加到当前正在执行的 ReactiveEffect 类的实例的 deps property 中 132 | export function trackEffects(dep) { 133 | // 若 dep 中包括当前正在执行的 ReactiveEffect 类的实例则直接返回 134 | if (dep.has(activeEffect!)) { 135 | return 136 | } 137 | 138 | // 将当前正在执行的 ReactiveEffect 类的实例添加到 dep 中 139 | dep.add(activeEffect!) 140 | // 将 dep 添加到当前正在执行的 ReactiveEffect 类的实例的 deps property 中 141 | activeEffect?.deps.push(dep) 142 | } 143 | 144 | // 用于触发依赖 145 | export function trigger(target, key) { 146 | // 获取当前响应式对象对应的 Map 实例 147 | const depsMap: Map> = targetsMap.get(target) 148 | // 获取当前 property 对应的 Set 实例 149 | const dep: Set = depsMap.get(key)! 150 | 151 | triggerEffects(dep) 152 | } 153 | 154 | // 用于遍历 dep,调用每一个 ReactiveEffect 类的实例的 scheduler 方法或 run 方法 155 | export function triggerEffects(dep) { 156 | /** 157 | * 遍历 dep,判断每一个 ReactiveEffect 类的实例的 scheduler property 是否存在 158 | * 若不为 undefined 则调用 scheduler 方法,否则调用 run 方法 159 | */ 160 | for (const reactiveEffect of dep) { 161 | if (reactiveEffect.scheduler) { 162 | reactiveEffect.scheduler() 163 | } else { 164 | reactiveEffect.run() 165 | } 166 | } 167 | } 168 | 169 | // 用于停止传入的函数的执行 170 | export function stop(runner) { 171 | // 调用 runner 的 effect property 的 stop 方法 172 | runner.effect.stop() 173 | } 174 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": [ 10 | "DOM", 11 | "ES6", 12 | "ES2016" 13 | ], /* Specify library files to be included in the compilation. */ 14 | // "allowJs": true, /* Allow javascript files to be compiled. */ 15 | // "checkJs": true, /* Report errors in .js files. */ 16 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 17 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 18 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 19 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 20 | // "outFile": "./", /* Concatenate and emit output to single file. */ 21 | // "outDir": "./", /* Redirect output structure to the directory. */ 22 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 23 | // "composite": true, /* Enable project compilation */ 24 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 25 | // "removeComments": true, /* Do not emit comments to output. */ 26 | // "noEmit": true, /* Do not emit outputs. */ 27 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 28 | "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 29 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 30 | 31 | /* Strict Type-Checking Options */ 32 | "strict": true, /* Enable all strict type-checking options. */ 33 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 34 | // "strictNullChecks": true, /* Enable strict null checks. */ 35 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 36 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 37 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 38 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 39 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 40 | 41 | /* Additional Checks */ 42 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 43 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 44 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 45 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 46 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 47 | 48 | /* Module Resolution Options */ 49 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 50 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 51 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 52 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 53 | // "typeRoots": [], /* List of folders to include type definitions from. */ 54 | "types": ["jest"], /* Type declaration files to be included in compilation. */ 55 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 56 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 57 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 58 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 59 | 60 | /* Source Map Options */ 61 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 65 | 66 | /* Experimental Options */ 67 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 68 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 69 | 70 | /* Advanced Options */ 71 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 72 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/runtime-core/renderer.ts: -------------------------------------------------------------------------------- 1 | import { effect } from '../reactivity/src' 2 | import { ShapeFlags } from '../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 | // 用于创建 render 函数 10 | export function createRenderer(options) { 11 | /** 12 | * 通过解构赋值获取 createText 函数、createElement 函数、patchProp 函数、 13 | * insert 函数、remove 函数和 setElementText 函数 14 | */ 15 | const { 16 | createText: hostCreateText, 17 | createElement: hostCreateElement, 18 | patchProp: hostPatchProp, 19 | insert: hostInsert, 20 | remove: hostRemove, 21 | setElementText: hostSetElementText 22 | } = options 23 | 24 | // 用于对根组件对应的 VNode 进行处理 25 | function render(vnode, container) { 26 | patch(null, vnode, container, null, null) 27 | } 28 | 29 | // 用于处理 VNode 30 | function patch(n1, n2, container, parentComponent, anchor) { 31 | // 根据新 VNode 类型的不同调用不同的函数 32 | const { type, shapeFlag } = n2 33 | 34 | // 通过新 VNode 的 type property 判断 VNode 类型 35 | switch (type) { 36 | case Fragment: 37 | processFragment(n1, n2, container, parentComponent, anchor) 38 | break 39 | case Text: 40 | processText(n1, n2, container) 41 | break 42 | default: 43 | // 通过新 VNode 的 shapeFlag property 与枚举变量 ShapeFlags 进行与运算来判断类型是 Element 或 Component 44 | if (shapeFlag & ShapeFlags.ELEMENT) { 45 | processElement(n1, n2, container, parentComponent, anchor) 46 | } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { 47 | processComponent(n1, n2, container, parentComponent, anchor) 48 | } 49 | break 50 | } 51 | } 52 | 53 | // 用于处理 Fragment 54 | function processFragment(n1, n2, container, parentComponent, anchor) { 55 | mountChildren(n2.children, container, parentComponent, anchor) 56 | } 57 | 58 | // 用于处理 Text 59 | function processText(n1, n2, container) { 60 | // 通过解构赋值获取 Text 对应 VNode 的 children,即文本内容 61 | const { children } = n2 62 | // 根据文本内容创建文本节点 63 | const textNode = hostCreateText(children) 64 | // 将文本节点添加到根容器/其父元素中 65 | hostInsert(textNode, container) 66 | } 67 | 68 | // 用于处理 Element 69 | function processElement(n1, n2, container, parentComponent, anchor) { 70 | // 若旧 VNode 不存在则初始化 Element 71 | if (!n1) { 72 | mountElement(n2, container, parentComponent, anchor) 73 | } 74 | // 否则更新 Element 75 | else { 76 | patchElement(n1, n2, container, parentComponent, anchor) 77 | } 78 | } 79 | 80 | // 用于初始化 Element 81 | function mountElement(vnode, container, parentComponent, anchor) { 82 | // 根据 Element 对应 VNode 的 type property 创建元素并赋值给 VNode 的 el property 83 | const el = (vnode.el = hostCreateElement(vnode.type)) 84 | 85 | // 通过解构赋值获取 Element 对应 VNode 的 props 对象、shapeFlag property 和 children 86 | const { props, shapeFlag, children } = vnode 87 | 88 | // 遍历 props 对象,将其中的 property 或方法挂载到新元素上 89 | for (const key in props) { 90 | const val = props[key] 91 | hostPatchProp(el, key, val) 92 | } 93 | 94 | // 通过 VNode 的 shapeFlag property 与枚举变量 ShapeFlags 进行与运算来判断 children 类型 95 | if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { 96 | el.textContent = children 97 | } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { 98 | mountChildren(children, el, parentComponent, anchor) 99 | } 100 | 101 | // 将新元素添加到根容器/父元素中 102 | hostInsert(el, container, anchor) 103 | } 104 | 105 | // 用于更新 Element 106 | function patchElement(n1, n2, container, parentComponent, anchor) { 107 | const oldProps = n1.props || {} 108 | const newProps = n2.props || {} 109 | 110 | // 获取旧 VNode 的 el property 并将其挂载到新 VNode 上 111 | const el = (n2.el = n1.el) 112 | 113 | patchChildren(n1, n2, el, parentComponent, anchor) 114 | patchProps(el, oldProps, newProps) 115 | } 116 | 117 | // 用于更新 Element 的 children 118 | function patchChildren(n1, n2, container, parentComponent, anchor) { 119 | // 通过结构赋值分别获得新旧 VNode 的 children 和 shapeFlag property 120 | const { children: c1, shapeFlag: prevShapeFlag } = n1 121 | const { children: c2, shapeFlag: nextShapeFlag } = n2 122 | 123 | // 若新 VNode 的 children 类型为 string 124 | if (nextShapeFlag & ShapeFlags.TEXT_CHILDREN) { 125 | // 同时旧 VNode 的 children 类型为 Array 126 | if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 127 | // 移除旧 VNode 的 children 128 | unmountChildren(c1) 129 | } 130 | 131 | // 若新旧 VNode 的 children 不同 132 | if (c1 !== c2) { 133 | // 将根容器/父元素的文本内容设置为新 VNode 的 children 134 | hostSetElementText(container, c2) 135 | } 136 | } 137 | // 若新 VNode 的 children 类型为 Array 138 | else if (nextShapeFlag & ShapeFlags.ARRAY_CHILDREN) { 139 | // 同时旧 VNode 的 children 类型为 string 140 | if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { 141 | // 将根容器/父元素的文本内容设置为空字符串 142 | hostSetElementText(container, '') 143 | 144 | // 将新 VNode 的 children 添加到根容器/父元素中 145 | mountChildren(c2, container, parentComponent, anchor) 146 | } 147 | // 同时旧 VNode 的 children 类型为 Array 148 | else { 149 | patchKeyedChildren(c1, c2, container, parentComponent, anchor) 150 | } 151 | } 152 | } 153 | 154 | // 用于遍历 children,移除其中的所有 VNode 155 | function unmountChildren(children) { 156 | for (const child of children) { 157 | // 移除 VNode 158 | hostRemove(child) 159 | } 160 | } 161 | 162 | // 用于更新 Element 的 props 163 | function patchProps(el, oldProps, newProps) { 164 | if (oldProps !== newProps) { 165 | // 遍历新 VNode 的 props 对象 166 | for (const key in newProps) { 167 | const prev = oldProps[key] 168 | const next = newProps[key] 169 | 170 | // 若新旧 VNode 的 props 对象中的 property 或方法不相等 171 | if (prev !== next) { 172 | // 将新 VNode 的 property 或方法挂载到元素上 173 | hostPatchProp(el, key, next) 174 | } 175 | } 176 | 177 | if (oldProps !== {}) { 178 | // 遍历旧 VNode 的 props 对象 179 | for (const key in oldProps) { 180 | // 若新 VNode 的 props 对象中不包含该 property 或方法 181 | if (!(key in newProps)) { 182 | // 将元素上该 property 或方法赋值为 null 183 | hostPatchProp(el, key, null) 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | // 用于遍历 children,对其中每个 VNode 调用 patch 方法进行处理 191 | function mountChildren(children, container, parentComponent, anchor) { 192 | for (const child of children) { 193 | patch(null, child, container, parentComponent, anchor) 194 | } 195 | } 196 | 197 | // 用于针对数组情况更新 Element 的 children 198 | function patchKeyedChildren( 199 | c1, 200 | c2, 201 | container, 202 | parentComponent, 203 | parentAnchor 204 | ) { 205 | const l2 = c2.length 206 | // 用于双端对比的指针(索引) 207 | let i = 0 208 | let e1 = c1.length - 1 209 | let e2 = l2 - 1 210 | 211 | // 用于判断两个元素是否是同一 VNode 212 | function isSameVNodeType(n1, n2) { 213 | // 若 type property 和 key property 均相同则返回 true 214 | return n1.type === n2.type && n1.key === n2.key 215 | } 216 | 217 | // 从头遍历 218 | while (i <= e1 && i <= e2) { 219 | const n1 = c1[i] 220 | const n2 = c2[i] 221 | 222 | // 若两个元素是同一 VNode 则对其调用 patch 方法进行更新 223 | if (isSameVNodeType(n1, n2)) { 224 | patch(n1, n2, container, parentComponent, parentAnchor) 225 | } 226 | // 否则结束遍历,此时 i 为从头遍历时差异位置在两个数组中的索引 227 | else { 228 | break 229 | } 230 | 231 | i++ 232 | } 233 | 234 | // 从尾遍历 235 | while (i <= e1 && i <= e2) { 236 | const n1 = c1[e1] 237 | const n2 = c2[e2] 238 | 239 | // 若两个元素是同一 VNode 则对其调用 patch 方法进行更新 240 | if (isSameVNodeType(n1, n2)) { 241 | patch(n1, n2, container, parentComponent, parentAnchor) 242 | } 243 | // 否则结束遍历,此时 e1 和 e2 分别为从尾遍历时差异位置在两个数组中的索引 244 | else { 245 | break 246 | } 247 | 248 | e1-- 249 | e2-- 250 | } 251 | 252 | // 若 i > e1 则说明新的数组比旧的长,将多出的元素依次向旧的中插入 253 | if (i > e1) { 254 | if (i <= e2) { 255 | // 要插入位置的下一个元素的索引是 e2+1 256 | const nextPos = e2 + 1 257 | // 若 e2+1 小于新的数组长度,则锚点为新的数组中索引为 e2+1 的 VNode 的 el property,否则为 null 258 | const anchor = nextPos < l2 ? c2[nextPos].el : null 259 | 260 | // 依次对子数组[i,e2]中的 VNode 调用 patch 方法进行插入 261 | while (i <= e2) { 262 | patch(null, c2[i], container, parentComponent, anchor) 263 | 264 | i++ 265 | } 266 | } 267 | } 268 | // 若 i > e2 则说明旧的数组比新的长,将多出的元素依次从旧的中移除 269 | else if (i > e2) { 270 | // 依次移除子数组[i,e1]中的 VNode 271 | while (i <= e1) { 272 | hostRemove(c1[i].el) 273 | 274 | i++ 275 | } 276 | } 277 | // 若 i <= e1 且 i <= e2 则说明子数组[i,e1]和子数组[i,e2]有差异 278 | else { 279 | let s = i 280 | 281 | // 用于保存需要调用 patch 方法的次数 282 | const toBePatched = e2 - s + 1 283 | // 用于记录调用 patch 方法的次数 284 | let patched = 0 285 | 286 | // 用于保存子数组[i,e2]中元素的索引,key 为 VNode 的 key property,value 为索引 287 | const keyToNewIndexMap = new Map() 288 | // 用于保存子数组[i,e2]中元素在旧的数组中的索引,默认为 0,在保存时加 1 289 | const newIndexToOldIndexMap = new Array(toBePatched) 290 | // 用于标志是否存在元素移动 291 | let moved = false 292 | // 用于保存遍历子数组[i,e1]时其中元素在新的数组中的最大索引 293 | let maxNewIndexSoFar = 0 294 | 295 | for (let i = 0; i < toBePatched; i++) { 296 | newIndexToOldIndexMap[i] = 0 297 | } 298 | 299 | // 遍历子数组[i,e2],保存其中元素的索引 300 | for (let i = s; i <= e2; i++) { 301 | const nextChild = c2[i] 302 | 303 | keyToNewIndexMap.set(nextChild.key, i) 304 | } 305 | 306 | // 遍历子数组[i,e1],依次进行处理 307 | for (let i = s; i <= e1; i++) { 308 | const prevChild = c1[i] 309 | 310 | // 若 patched 大于等于 toBePatched,则说明当前元素是要移除的元素,可直接移除 311 | if (patched >= toBePatched) { 312 | hostRemove(prevChild.el) 313 | 314 | continue 315 | } 316 | 317 | let newIndex 318 | // 若当前元素包含 key property,则从 keyToNewIndexMap 中获取其在新的数组中的索引 319 | if (prevChild.key != null) { 320 | newIndex = keyToNewIndexMap.get(prevChild.key) 321 | } 322 | // 否则 323 | else { 324 | // 遍历子数组[i,e2]获取其索引 325 | for (let j = s; j <= e2; j++) { 326 | if (isSameVNodeType(prevChild, c2[j])) { 327 | newIndex = j 328 | 329 | break 330 | } 331 | } 332 | } 333 | 334 | // 若当前元素在新的数组中的索引不存在则其不在新的数组中,可直接移除 335 | if (newIndex === undefined) { 336 | hostRemove(prevChild.el) 337 | } 338 | // 否则对当前元素调用 patch 方法进行更新 339 | else { 340 | // 若当前元素在新的数组中的索引大于等于 maxNewIndexSoFar,则更新后者 341 | if (newIndex >= maxNewIndexSoFar) { 342 | maxNewIndexSoFar = newIndex 343 | } 344 | // 否则说明存在元素移动,将 moved 的值变为 true 345 | else { 346 | moved = true 347 | } 348 | 349 | // 保存当前元素在旧的数组中的索引 350 | newIndexToOldIndexMap[newIndex - s] = i + 1 351 | // 对当前元素调用 patch 方法进行更新 352 | patch(prevChild, c2[newIndex], container, parentComponent, null) 353 | 354 | patched++ 355 | } 356 | } 357 | 358 | /** 359 | * 用于保存最长递增子序列 360 | * 若 moved 为 true 则存在元素移动 361 | * 通过 getSequence() 函数获取 newIndexToOldIndexMap 的最长递增子序列 362 | */ 363 | const increasingNewIndexSequence = moved 364 | ? getSequence(newIndexToOldIndexMap) 365 | : [] 366 | // 用于倒序遍历最长递增子序列 367 | let j = increasingNewIndexSequence.length - 1 368 | 369 | // 倒序遍历子数组[i,e2],依次进行处理 370 | for (let i = toBePatched - 1; i >= 0; i--) { 371 | // 用于保存当前元素在新的数组中的索引 372 | const nextIndex = i + s 373 | const nextChild = c2[nextIndex] 374 | /** 375 | * 若 nextIndex+1 小于新的数组长度 376 | * 则锚点为新的数组中索引为 nextIndex+1 的 VNode 的 el property 377 | * 否则为 null 378 | */ 379 | const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null 380 | 381 | /** 382 | * 若在 newIndexToOldIndexMap 中当前元素在子数组[i,e2]索引对应的值为 0 383 | * 则说明该元素不在旧的数组中,对其调用 patch 方法进行插入 384 | */ 385 | if (newIndexToOldIndexMap[i] === 0) { 386 | patch(null, nextChild, container, parentComponent, anchor) 387 | } 388 | // 若存在元素移动 389 | else if (moved) { 390 | /** 391 | * 若 j 小于 0,即最长递增子序列已遍历完 392 | * 或者当前元素在子数组[i,e2]索引与最长递增子序列中索引为 j 的值不相等 393 | * 则说明当前元素是要移动的元素 394 | */ 395 | if (j < 0 || i !== increasingNewIndexSequence[j]) { 396 | // 将当前元素插入到其要移动到的位置 397 | hostInsert(nextChild.el, container, anchor) 398 | } else { 399 | j-- 400 | } 401 | } 402 | } 403 | } 404 | } 405 | 406 | // 用于处理 Component 407 | function processComponent(n1, n2, container, parentComponent, anchor) { 408 | // 若旧 VNode 不存在则初始化 Component 409 | if (!n1) { 410 | mountComponent(n2, container, parentComponent, anchor) 411 | } 412 | // 否则更新 Component 413 | else { 414 | updateComponent(n1, n2) 415 | } 416 | } 417 | 418 | // 用于初始化 Component 419 | function mountComponent(vnode, container, parentComponent, anchor) { 420 | // 通过组件对应的 VNode 创建组件实例对象,用于挂载 props、slots 等,并将其挂载到 VNode 上 421 | const instance = (vnode.component = createComponentInstance( 422 | vnode, 423 | parentComponent 424 | )) 425 | 426 | setupComponent(instance) 427 | 428 | setupRenderEffect(instance, vnode, container, anchor) 429 | } 430 | 431 | // 用于更新 Component 432 | function updateComponent(n1, n2) { 433 | // 获取旧 VNode 对应组件实例对象并将其挂载到新 VNode 上 434 | const instance = (n2.component = n1.component) 435 | 436 | // 若需要更新组件 437 | if (shouldUpdateComponent(n1, n2)) { 438 | // 将新 VNode 挂载到组件实例对象上 439 | instance.next = n2 440 | // 调用组件实例对象的 update 方法 441 | instance.update() 442 | } 443 | // 否则 444 | else { 445 | // 将旧 VNode 的 el property 挂载到新 VNode 上 446 | n2.el = n1.el 447 | // 将新 VNode 挂载到组件实例对象上 448 | instance.vnode = n2 449 | } 450 | } 451 | 452 | // 用于处理 VNode 树 453 | function setupRenderEffect(instance, vnode, container, anchor) { 454 | // 利用 effect 将调用 render 函数和 patch 方法的操作收集,并将 effect 返回的函数保存为组件实例对象的 update 方法 455 | instance.update = effect( 456 | () => { 457 | // 根据组件实例对象的 isMounted property 判断是初始化或更新 VNode 树 458 | // 若为 false 则是初始化 459 | if (!instance.isMounted) { 460 | // 通过解构赋值获取组件实例对象的 proxy property 461 | const { proxy } = instance 462 | 463 | // 调用组件实例对象中 render 函数获取 VNode 树,同时将 this 指向指定为 proxy property,并将其挂载到组件实例对象上 464 | const subTree = (instance.subTree = instance.render.call(proxy)) 465 | 466 | // 调用 patch 方法处理 VNode 树 467 | patch(null, subTree, container, instance, anchor) 468 | 469 | // 将 VNode 树的 el property 赋值给 VNode 的 el property 470 | vnode.el = subTree.el 471 | 472 | // 将组件实例对象的 isMounted property 赋值为 true 473 | instance.isMounted = true 474 | } 475 | // 否则是更新 476 | else { 477 | // 通过解构赋值获取组件对应新旧 VNode 以及组件实例对象的 proxy property 和旧 VNode 树 478 | const { next, vnode, proxy, subTree: preSubTree } = instance 479 | 480 | if (next) { 481 | // 将旧 VNode 的 el property 挂载到新 VNode 上 482 | next.el = vnode.el 483 | 484 | updateComponentPreRender(instance, next) 485 | } 486 | 487 | // 调用组件实例对象中 render 函数获取新 VNode 树,同时将 this 指向指定为 proxy property,并将其挂载到组件实例对象上 488 | const subTree = (instance.subTree = instance.render.call(proxy)) 489 | 490 | // 调用 patch 方法处理新旧 VNode 树 491 | patch(preSubTree, subTree, container, instance, anchor) 492 | } 493 | }, 494 | // 传入包含 scheduler 方法的对象作为第二个参数 495 | { 496 | // 在 scheduler 方法中将组件实例对象的 update 方法保存到队列中 497 | scheduler() { 498 | queueJobs(instance.update) 499 | } 500 | } 501 | ) 502 | } 503 | 504 | // 返回一个包含 createApp 的对象,方法具体为调用 createAppAPI 函数并传入 render 函数 505 | return { 506 | createApp: createAppAPI(render) 507 | } 508 | } 509 | 510 | // 用于更新组件实例对象的 property 511 | function updateComponentPreRender(instance, nextVNode) { 512 | instance.vnode = nextVNode 513 | instance.next = null 514 | instance.props = nextVNode.props 515 | } 516 | 517 | function getSequence(arr: number[]): number[] { 518 | const p = arr.slice() 519 | const result = [0] 520 | let i, j, u, v, c 521 | const len = arr.length 522 | for (i = 0; i < len; i++) { 523 | const arrI = arr[i] 524 | if (arrI !== 0) { 525 | j = result[result.length - 1] 526 | if (arr[j] < arrI) { 527 | p[i] = j 528 | result.push(i) 529 | continue 530 | } 531 | u = 0 532 | v = result.length - 1 533 | while (u < v) { 534 | c = (u + v) >> 1 535 | if (arr[result[c]] < arrI) { 536 | u = c + 1 537 | } else { 538 | v = c 539 | } 540 | } 541 | if (arrI < arr[result[u]]) { 542 | if (u > 0) { 543 | p[i] = result[u - 1] 544 | } 545 | result[u] = i 546 | } 547 | } 548 | } 549 | u = result.length 550 | v = result[u - 1] 551 | while (u-- > 0) { 552 | result[u] = v 553 | v = p[v] 554 | } 555 | return result 556 | } 557 | --------------------------------------------------------------------------------