Стремление к компонентно-ориентированной разработке
Размышления на основе организации существующих реализаций
До сих пор мы реализовали API createApp, систему реактивности и систему Virtual DOM в небольшом масштабе.
С текущей реализацией мы можем динамически изменять пользовательский интерфейс с помощью системы реактивности и выполнять эффективный рендеринг с помощью системы Virtual DOM.
Однако, как интерфейс разработчика, всё написано в createAppAPI.
На самом деле, я хочу больше разделить файлы и реализовать общие компоненты для повторного использования.
Сначала давайте рассмотрим части, которые в настоящее время являются беспорядочными в существующей реализации. Пожалуйста, взгляните на функцию render в renderer.ts.
const render: RootRenderFunction = (rootComponent, container) => {
const componentRender = rootComponent.setup!()
let n1: VNode | null = null
let n2: VNode = null!
const updateComponent = () => {
const n2 = componentRender()
patch(n1, n2, container)
n1 = n2
}
const effect = new ReactiveEffect(updateComponent)
effect.run()
}
В функции render информация о корневом компоненте определена напрямую.
На самом деле, n1, n2, updateComponent и effect существуют для каждого компонента.
Фактически, с этого момента я хочу определить компонент (в некотором смысле, конструктор) на стороне пользователя и создать его экземпляр.
И я хочу, чтобы экземпляр имел такие свойства, как n1, n2 и updateComponent.
Итак, давайте подумаем об инкапсуляции их как экземпляра компонента.
Давайте определим что-то под названием ComponentInternalInstance
в ~/packages/runtime-core/component.ts
.
Это будет тип экземпляра.
export interface ComponentInternalInstance {
type: Component // Исходный компонент, определенный пользователем (старый rootComponent (на самом деле не только корневой компонент))
vnode: VNode // Будет объяснено позже
subTree: VNode // Старый n1
next: VNode | null // Старый n2
effect: ReactiveEffect // Старый effect
render: InternalRenderFunction // Старый componentRender
update: () => void // Старый updateComponent
isMounted: boolean
}
export type InternalRenderFunction = {
(): VNodeChild
}
Свойства vnode, subTree и next, которые имеет этот экземпляр, немного сложны, но с этого момента мы будем реализовывать так, чтобы ConcreteComponent можно было указать как тип VNode.
В instance.vnode мы будем хранить сам VNode.
А subTree и next будут хранить результат рендеринга VNode этого компонента. (Это то же самое, что и раньше с n1 и n2)
В плане образа,
const MyComponent = {
setup() {
return h('p', {}, ['hello'])
},
}
const App = {
setup() {
return h(MyComponent, {}, [])
},
}
Вы можете использовать это так, и если вы позволите экземпляру быть экземпляром MyComponent, instance.vnode будет хранить результат h(MyComponent, {}, [])
, а instance.subTree будет хранить результат h("p", {}, ["hello"])
.
Пока что давайте реализуем так, чтобы вы могли указать компонент в качестве первого аргумента функции h.
Однако, это просто вопрос получения объекта, который определяет компонент как тип.
В ~/packages/runtime-core/vnode.ts
export type VNodeTypes = string | typeof Text | object // Добавить object;
В ~/packages/runtime-core/h.ts
export function h(
type: string | object, // Добавить object
props: VNodeProps
) {..}
Давайте также убедимся, что VNode имеет экземпляр компонента.
export interface VNode<HostNode = any> {
// .
// .
// .
component: ComponentInternalInstance | null // Добавить
}
В результате рендерер также должен обрабатывать компоненты.
Реализуйте processComponent
аналогично processElement
и processText
для обработки компонентов, а также реализуйте mountComponent
и patchComponent
(или updateComponent
).
Сначала давайте начнем с обзора и подробного объяснения.
const patch = (n1: VNode | null, n2: VNode, container: RendererElement) => {
const { type } = n2
if (type === Text) {
processText(n1, n2, container)
} else if (typeof type === 'string') {
processElement(n1, n2, container)
} else if (typeof type === 'object') {
// Добавить ветвление
processComponent(n1, n2, container)
} else {
// ничего не делать
}
}
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
) => {
if (n1 == null) {
mountComponent(n2, container)
} else {
updateComponent(n1, n2)
}
}
const mountComponent = (initialVNode: VNode, container: RendererElement) => {
// TODO:
}
const updateComponent = (n1: VNode, n2: VNode) => {
// TODO:
}
Теперь давайте рассмотрим mountComponent
. Есть три вещи, которые нужно сделать.
- Создать экземпляр компонента.
- Выполнить функцию
setup
и сохранить результат в экземпляре. - Создать
ReactiveEffect
и сохранить его в экземпляре.
Сначала давайте реализуем функцию в component.ts
для создания экземпляра компонента (аналогично конструктору).
export function createComponentInstance(
vnode: VNode,
): ComponentInternalInstance {
const type = vnode.type as Component
const instance: ComponentInternalInstance = {
type,
vnode,
next: null,
effect: null!,
subTree: null!,
update: null!,
render: null!,
isMounted: false,
}
return instance
}
Хотя тип каждого свойства не является null, мы инициализируем их значением null при создании экземпляра (следуя дизайну оригинального Vue.js).
const mountComponent = (initialVNode: VNode, container: RendererElement) => {
const instance: ComponentInternalInstance = (initialVNode.component =
createComponentInstance(initialVNode))
// TODO: настройка компонента
// TODO: настройка эффекта
}
Далее идет функция setup
.
Нам нужно переместить код, который ранее был написан непосредственно в функции render
, сюда и сохранить результат в экземпляре вместо использования переменных.
const mountComponent = (initialVNode: VNode, container: RendererElement) => {
const instance: ComponentInternalInstance = (initialVNode.component =
createComponentInstance(initialVNode))
const component = initialVNode.type as Component
if (component.setup) {
instance.render = component.setup() as InternalRenderFunction
}
// TODO: настройка эффекта
}
Наконец, давайте объединим код для создания эффекта в функцию под названием setupRenderEffect
.
Опять же, основная задача - переместить код, который ранее был реализован непосредственно в функции render
, сюда, используя состояние экземпляра.
const mountComponent = (initialVNode: VNode, container: RendererElement) => {
const instance: ComponentInternalInstance = (initialVNode.component =
createComponentInstance(initialVNode))
const component = initialVNode.type as Component
if (component.setup) {
instance.render = component.setup() as InternalRenderFunction
}
setupRenderEffect(instance, initialVNode, container)
}
const setupRenderEffect = (
instance: ComponentInternalInstance,
initialVNode: VNode,
container: RendererElement,
) => {
const componentUpdateFn = () => {
const { render } = instance
if (!instance.isMounted) {
// процесс монтирования
const subTree = (instance.subTree = normalizeVNode(render()))
patch(null, subTree, container)
initialVNode.el = subTree.el
instance.isMounted = true
} else {
// процесс патча
let { next, vnode } = instance
if (next) {
next.el = vnode.el
next.component = instance
instance.vnode = next
instance.next = null
} else {
next = vnode
}
const prevTree = instance.subTree
const nextTree = normalizeVNode(render())
instance.subTree = nextTree
patch(prevTree, nextTree, hostParentNode(prevTree.el!)!) // ※ 1
next.el = nextTree.el
}
}
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn))
const update = (instance.update = () => effect.run()) // Регистрация в instance.update
update()
}
※ 1: Пожалуйста, реализуйте функцию под названием parentNode
в nodeOps
, которая извлекает родительский узел.
parentNode: (node) => {
return node.parentNode;
},
Я думаю, что это не особенно сложно, хотя и немного длинно.
В функции setupRenderEffect
функция для обновления регистрируется как метод update
экземпляра, поэтому в updateComponent
нам просто нужно вызвать эту функцию.
const updateComponent = (n1: VNode, n2: VNode) => {
const instance = (n2.component = n1.component)!
instance.next = n2
instance.update()
}
Наконец, поскольку реализация, которая была определена в функции render
до сих пор, больше не нужна, мы удалим ее.
const render: RootRenderFunction = (rootComponent, container) => {
const vnode = createVNode(rootComponent, {}, [])
patch(null, vnode, container)
}
Теперь мы можем рендерить компоненты. Давайте попробуем создать компонент playground
в качестве примера.
Таким образом, мы можем разделить рендеринг на компоненты.
import { createApp, h, reactive } from 'chibivue'
const CounterComponent = {
setup() {
const state = reactive({ count: 0 })
const increment = () => state.count++
return () =>
h('div', {}, [
h('p', {}, [`count: ${state.count}`]),
h('button', { onClick: increment }, ['increment']),
])
},
}
const app = createApp({
setup() {
return () =>
h('div', { id: 'my-app' }, [
h(CounterComponent, {}, []),
h(CounterComponent, {}, []),
h(CounterComponent, {}, []),
])
},
})
app.mount('#app')
Исходный код до этого момента: chibivue (GitHub)