Включаем рендеринг HTML-элементов
Что такое функция h?
До сих пор мы заставили работать следующий исходный код:
import { createApp } from 'vue'
const app = createApp({
render() {
return 'Hello world.'
},
})
app.mount('#app')
Это функция, которая просто отображает "Hello World." на экране.
Поскольку с одним сообщением немного одиноко, давайте подумаем об интерфейсе разработчика, который также может отображать HTML-элементы.
Вот где пригодится функция h
. Это h
означает hyperscript
и предоставляется как функция для написания HTML (Hyper Text Markup Language) в JavaScript.
h() - это сокращение от hyperscript, что означает "JavaScript, который создает HTML (язык гипертекстовой разметки)". Это название унаследовано от соглашений, принятых во многих реализациях Virtual DOM. Более описательным названием могло бы быть createVnode(), но более короткое название помогает, когда вам приходится вызывать эту функцию много раз в функции рендеринга.
Цитата: https://vuejs.org/guide/extras/render-function.html#creating-vnodes
Давайте посмотрим на функцию h в Vue.js.
import { createApp, h } from 'vue'
const app = createApp({
render() {
return h('div', {}, [
h('p', {}, ['HelloWorld']),
h('button', {}, ['click me!']),
])
},
})
app.mount('#app')
В базовом использовании функции h вы указываете имя тега в качестве первого аргумента, атрибуты в качестве второго аргумента и массив дочерних элементов в качестве третьего аргумента.
Здесь я особо упомянул "базовое использование", потому что функция h на самом деле имеет несколько синтаксисов для своих аргументов, и вы можете опустить второй аргумент или не использовать массив для дочерних элементов.
Однако здесь мы реализуем ее в самом базовом синтаксисе.
Как мы должны ее реализовать? 🤔
Теперь, когда мы понимаем интерфейс разработчика, давайте решим, как его реализовать.
Важно отметить, как он используется в качестве возвращаемого значения функции рендеринга.
Это означает, что функция h
возвращает какой-то объект и использует этот результат внутренне.
Поскольку сложно понять с сложными дочерними элементами, давайте рассмотрим результат реализации простой функции h.
const result = h('div', { class: 'container' }, ['hello'])
Какой результат должен быть сохранен в result
? (Как мы должны форматировать результат и как мы должны его отобразить?)
Давайте предположим, что в result
хранится следующий объект:
const result = {
type: 'div',
props: { class: 'container' },
children: ['hello'],
}
Другими словами, мы получим объект, подобный приведенному выше, из функции рендеринга и используем его для выполнения операций DOM и отображения.
Картина выглядит так (внутри mount
функции createApp
):
const app: App = {
mount(rootContainer: HostElement) {
const node = rootComponent.render!()
render(node, rootContainer)
},
}
Что ж, единственное, что изменилось, - это то, что мы изменили строку message
на объект node
.
Все, что нам теперь нужно сделать, - это выполнить операции DOM на основе объекта в функции рендеринга.
На самом деле, этот объект имеет название, "Virtual DOM".
Мы более подробно объясним о Virtual DOM в главе о Virtual DOM, так что пока просто запомните название.\
Реализация функции h
Сначала создадим необходимые файлы.
pwd # ~
touch packages/runtime-core/vnode.ts
touch packages/runtime-core/h.ts
Определим типы в vnode.ts. Это все, что мы сделаем в vnode.ts.
export interface VNode {
type: string
props: VNodeProps
children: (VNode | string)[]
}
export interface VNodeProps {
[key: string]: any
}
Далее реализуем тело функции в h.ts.
export function h(
type: string,
props: VNodeProps,
children: (VNode | string)[],
) {
return { type, props, children }
}
Пока что давайте попробуем использовать функцию h в playground.
import { createApp, h } from 'chibivue'
const app = createApp({
render() {
return h('div', {}, ['Hello world.'])
},
})
app.mount('#app')
Отображение на экране не работает, но если вы добавите лог в apiCreateApp, вы увидите, что он работает, как ожидалось.
mount(rootContainer: HostElement) {
const vnode = rootComponent.render!();
console.log(vnode); // Проверяем лог
render(vnode, rootContainer);
},
Теперь давайте реализуем функцию рендеринга.
Реализуем createElement
, createText
и insert
в RendererOptions.
export interface RendererOptions<HostNode = RendererNode> {
createElement(type: string): HostNode // Добавлено
createText(text: string): HostNode // Добавлено
setElementText(node: HostNode, text: string): void
insert(child: HostNode, parent: HostNode, anchor?: HostNode | null): void // Добавлено
}
Реализуем функцию renderVNode
в функции рендеринга. Пока мы игнорируем props
.
export function createRenderer(options: RendererOptions) {
const {
createElement: hostCreateElement,
createText: hostCreateText,
insert: hostInsert,
} = options
function renderVNode(vnode: VNode | string) {
if (typeof vnode === 'string') return hostCreateText(vnode)
const el = hostCreateElement(vnode.type)
for (const child of vnode.children) {
const childEl = renderVNode(child)
hostInsert(childEl, el)
}
return el
}
const render: RootRenderFunction = (vnode, container) => {
const el = renderVNode(vnode)
hostInsert(el, container)
}
return { render }
}
В nodeOps runtime-dom определим фактические операции DOM.
export const nodeOps: RendererOptions<Node> = {
// Добавлено
createElement: tagName => {
return document.createElement(tagName)
},
// Добавлено
createText: (text: string) => {
return document.createTextNode(text)
},
setElementText(node, text) {
node.textContent = text
},
// Добавлено
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
}
Ну, на этом этапе вы должны быть в состоянии отображать элементы на экране.
Попробуйте написать и протестировать различные вещи в playground!
import { createApp, h } from 'chibivue'
const app = createApp({
render() {
return h('div', {}, [
h('p', {}, ['Hello world.']),
h('button', {}, ['click me!']),
])
},
})
app.mount('#app')
Ура! Теперь мы можем использовать функцию h для отображения различных тегов!
Исходный код до этого момента:
chibivue (GitHub)