├── 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 |
--------------------------------------------------------------------------------