Vue 3 разделяет рантайм на две части:
Вы можете создать свой рендерер, заменив операции с DOM на операции с любой другой "платформой": Canvas, WebGL, Terminal, iOS/Android (через NativeScript/Capacitor).
import { createRenderer } from '@vue/runtime-core'
const { render, createApp } = createRenderer({
// nodeOps — объект с операциями над "узлами"
// Создать элемент
createElement(type, isSVG, isCustom) {
return createMyNode(type)
},
// Создать текстовый узел
createText(text) {
return createMyTextNode(text)
},
// Вставить узел в родителя
insert(el, parent, anchor) {
parent.insertBefore(el, anchor)
},
// Удалить узел
remove(el) {
el.parent?.removeChild(el)
},
// Установить свойство/атрибут
patchProp(el, key, prevValue, nextValue) {
el[key] = nextValue
},
// Установить текст
setElementText(el, text) {
el.textContent = text
},
// Создать комментарий (для v-if пустых веток)
createComment(text) {
return { type: 'comment', text }
},
// ... ещё несколько методов
})// Рендерим Vue компоненты на HTML5 Canvas
const canvasRenderer = createRenderer({
createElement(type) {
return new CanvasElement(type) // прямоугольник, круг и т.д.
},
patchProp(el, key, prev, next) {
if (key === 'fill') el.style.fill = next
if (key === 'x') el.x = next
},
insert(child, parent) {
parent.addChild(child)
canvas.requestRender()
},
// ...
})// Рендеринг в терминал через blessed/ink-подобные библиотеки
const termRenderer = createRenderer({
createElement(type) {
if (type === 'box') return blessed.box()
if (type === 'text') return blessed.text()
},
patchProp(el, key, prev, next) {
if (key === 'content') el.setContent(next)
if (key === 'style') el.style = next
},
// ...
})DOM-рендерер Vue — это именно createRenderer с nodeOps для браузера:
// Упрощённо — что делает @vue/runtime-dom:
const { render } = createRenderer({
createElement: (tag) => document.createElement(tag),
createText: (text) => document.createTextNode(text),
insert: (el, parent, anchor) => parent.insertBefore(el, anchor || null),
remove: (el) => el.parentNode?.removeChild(el),
patchProp: (el, key, prev, next) => {
if (key === 'class') el.className = next
else if (key.startsWith('on')) el.addEventListener(key.slice(2).toLowerCase(), next)
else el.setAttribute(key, next)
},
setElementText: (el, text) => { el.textContent = text },
createComment: (text) => document.createComment(text),
querySelector: (sel) => document.querySelector(sel),
parentNode: (el) => el.parentNode,
nextSibling: (el) => el.nextSibling,
})Реализация Custom Renderer — рендеринг Vue-подобных компонентов в строковое "дерево" вместо DOM
// Реализуем кастомный рендерер: вместо DOM используем
// дерево JavaScript-объектов. Это демонстрирует,
// как createRenderer() работает под капотом.
// =====================================================
// Node операции (nodeOps) — платформо-специфичная часть
// =====================================================
const StringNodeOps = {
// Создать "элемент"
createElement(type) {
return {
type,
props: {},
children: [],
parent: null,
_text: null,
}
},
// Создать текстовый узел
createText(text) {
return { type: '#text', text, parent: null }
},
// Создать комментарий (для v-if пустых блоков)
createComment(text) {
return { type: '#comment', text, parent: null }
},
// Вставить дочерний элемент в родителя
insert(el, parent, anchor = null) {
if (el.parent) {
const idx = el.parent.children.indexOf(el)
if (idx !== -1) el.parent.children.splice(idx, 1)
}
el.parent = parent
if (anchor) {
const anchorIdx = parent.children.indexOf(anchor)
parent.children.splice(anchorIdx, 0, el)
} else {
parent.children.push(el)
}
},
// Удалить элемент
remove(el) {
if (el.parent) {
const idx = el.parent.children.indexOf(el)
if (idx !== -1) el.parent.children.splice(idx, 1)
el.parent = null
}
},
// Применить prop/атрибут
patchProp(el, key, prevVal, nextVal) {
if (nextVal === null || nextVal === undefined) {
delete el.props[key]
} else {
el.props[key] = nextVal
}
},
// Установить текстовое содержимое
setElementText(el, text) {
el.children = []
el._text = text
},
// Получить текст
getText(el) {
return el._text
},
// Родительский узел
parentNode(el) {
return el.parent
},
// Следующий сиблинг
nextSibling(el) {
if (!el.parent) return null
const idx = el.parent.children.indexOf(el)
return el.parent.children[idx + 1] || null
},
}
// =====================================================
// Движок рендеринга (аналог @vue/runtime-core)
// =====================================================
function createCustomRenderer(nodeOps) {
function render(vnode, container) {
if (vnode == null) {
// Unmount
if (container._vnode) unmount(container._vnode)
} else {
patch(container._vnode, vnode, container)
}
container._vnode = vnode
}
function patch(n1, n2, container, anchor = null) {
if (n2 == null) return
if (typeof n2 === 'string' || typeof n2 === 'number') {
const textNode = nodeOps.createText(String(n2))
nodeOps.insert(textNode, container, anchor)
return
}
if (typeof n2.type === 'function') {
// Компонент
patchComponent(n1, n2, container, anchor)
return
}
if (n1 == null) {
mountElement(n2, container, anchor)
} else {
updateElement(n1, n2, container)
}
}
function mountElement(vnode, container, anchor) {
const el = nodeOps.createElement(vnode.type)
vnode.el = el
// Применяем props
for (const [key, val] of Object.entries(vnode.props || {})) {
if (key !== 'children') nodeOps.patchProp(el, key, null, val)
}
// Монтируем детей
const children = vnode.children || []
if (typeof children === 'string') {
nodeOps.setElementText(el, children)
} else {
children.forEach(child => patch(null, child, el))
}
nodeOps.insert(el, container, anchor)
}
function updateElement(n1, n2, container) {
const el = n2.el = n1.el
// Patch props
const oldProps = n1.props || {}
const newProps = n2.props || {}
for (const [k, v] of Object.entries(newProps)) {
if (oldProps[k] !== v) nodeOps.patchProp(el, k, oldProps[k], v)
}
for (const k of Object.keys(oldProps)) {
if (!(k in newProps)) nodeOps.patchProp(el, k, oldProps[k], null)
}
}
function patchComponent(n1, n2, container, anchor) {
const rendered = n2.type(n2.props || {})
patch(null, rendered, container, anchor)
}
function unmount(vnode) {
if (vnode.el) nodeOps.remove(vnode.el)
}
return { render }
}
// =====================================================
// Сериализатор — рендер в строку
// =====================================================
function serialize(node, indent = 0) {
if (!node) return ''
const pad = ' '.repeat(indent)
if (node.type === '#text') return pad + node.text
if (node.type === '#comment') return pad + `<!-- ${node.text} -->`
const attrs = Object.entries(node.props)
.map(([k, v]) => `${k}="${v}"`)
.join(' ')
const tag = node.type + (attrs ? ' ' + attrs : '')
if (node._text) return `${pad}<${tag}>${node._text}</${node.type}>`
if (node.children.length === 0) return `${pad}<${tag} />`
const inner = node.children.map(c => serialize(c, indent + 1)).join('\n')
return `${pad}<${tag}>\n${inner}\n${pad}</${node.type}>`
}
// =====================================================
// Использование Custom Renderer
// =====================================================
const renderer = createCustomRenderer(StringNodeOps)
// Создаём "корневой контейнер"
const root = StringNodeOps.createElement('root')
// VNode-дерево
function App(props) {
return {
type: 'div',
props: { id: 'app', class: 'container' },
children: [
{
type: 'h1',
props: { class: 'title' },
children: 'Привет, Custom Renderer!'
},
{
type: 'ul',
props: { class: 'list' },
children: (props.items || []).map((item, i) => ({
type: 'li',
props: { class: i % 2 === 0 ? 'even' : 'odd' },
children: item,
}))
},
{
type: 'footer',
props: {},
children: '© 2024'
}
]
}
}
// Первый рендер
renderer.render(
{ type: App, props: { items: ['Vue', 'React', 'Angular', 'Svelte'] } },
root
)
console.log('=== Custom Renderer Output ===')
console.log(serialize(root))
console.log('\n=== Статистика дерева ===')
function countNodes(node) {
if (!node || !node.children) return 1
return 1 + node.children.reduce((sum, c) => sum + countNodes(c), 0)
}
console.log('Всего узлов в дереве:', countNodes(root))
console.log('Потомков корня:', root.children.length)
Vue 3 разделяет рантайм на две части:
Вы можете создать свой рендерер, заменив операции с DOM на операции с любой другой "платформой": Canvas, WebGL, Terminal, iOS/Android (через NativeScript/Capacitor).
import { createRenderer } from '@vue/runtime-core'
const { render, createApp } = createRenderer({
// nodeOps — объект с операциями над "узлами"
// Создать элемент
createElement(type, isSVG, isCustom) {
return createMyNode(type)
},
// Создать текстовый узел
createText(text) {
return createMyTextNode(text)
},
// Вставить узел в родителя
insert(el, parent, anchor) {
parent.insertBefore(el, anchor)
},
// Удалить узел
remove(el) {
el.parent?.removeChild(el)
},
// Установить свойство/атрибут
patchProp(el, key, prevValue, nextValue) {
el[key] = nextValue
},
// Установить текст
setElementText(el, text) {
el.textContent = text
},
// Создать комментарий (для v-if пустых веток)
createComment(text) {
return { type: 'comment', text }
},
// ... ещё несколько методов
})// Рендерим Vue компоненты на HTML5 Canvas
const canvasRenderer = createRenderer({
createElement(type) {
return new CanvasElement(type) // прямоугольник, круг и т.д.
},
patchProp(el, key, prev, next) {
if (key === 'fill') el.style.fill = next
if (key === 'x') el.x = next
},
insert(child, parent) {
parent.addChild(child)
canvas.requestRender()
},
// ...
})// Рендеринг в терминал через blessed/ink-подобные библиотеки
const termRenderer = createRenderer({
createElement(type) {
if (type === 'box') return blessed.box()
if (type === 'text') return blessed.text()
},
patchProp(el, key, prev, next) {
if (key === 'content') el.setContent(next)
if (key === 'style') el.style = next
},
// ...
})DOM-рендерер Vue — это именно createRenderer с nodeOps для браузера:
// Упрощённо — что делает @vue/runtime-dom:
const { render } = createRenderer({
createElement: (tag) => document.createElement(tag),
createText: (text) => document.createTextNode(text),
insert: (el, parent, anchor) => parent.insertBefore(el, anchor || null),
remove: (el) => el.parentNode?.removeChild(el),
patchProp: (el, key, prev, next) => {
if (key === 'class') el.className = next
else if (key.startsWith('on')) el.addEventListener(key.slice(2).toLowerCase(), next)
else el.setAttribute(key, next)
},
setElementText: (el, text) => { el.textContent = text },
createComment: (text) => document.createComment(text),
querySelector: (sel) => document.querySelector(sel),
parentNode: (el) => el.parentNode,
nextSibling: (el) => el.nextSibling,
})Реализация Custom Renderer — рендеринг Vue-подобных компонентов в строковое "дерево" вместо DOM
// Реализуем кастомный рендерер: вместо DOM используем
// дерево JavaScript-объектов. Это демонстрирует,
// как createRenderer() работает под капотом.
// =====================================================
// Node операции (nodeOps) — платформо-специфичная часть
// =====================================================
const StringNodeOps = {
// Создать "элемент"
createElement(type) {
return {
type,
props: {},
children: [],
parent: null,
_text: null,
}
},
// Создать текстовый узел
createText(text) {
return { type: '#text', text, parent: null }
},
// Создать комментарий (для v-if пустых блоков)
createComment(text) {
return { type: '#comment', text, parent: null }
},
// Вставить дочерний элемент в родителя
insert(el, parent, anchor = null) {
if (el.parent) {
const idx = el.parent.children.indexOf(el)
if (idx !== -1) el.parent.children.splice(idx, 1)
}
el.parent = parent
if (anchor) {
const anchorIdx = parent.children.indexOf(anchor)
parent.children.splice(anchorIdx, 0, el)
} else {
parent.children.push(el)
}
},
// Удалить элемент
remove(el) {
if (el.parent) {
const idx = el.parent.children.indexOf(el)
if (idx !== -1) el.parent.children.splice(idx, 1)
el.parent = null
}
},
// Применить prop/атрибут
patchProp(el, key, prevVal, nextVal) {
if (nextVal === null || nextVal === undefined) {
delete el.props[key]
} else {
el.props[key] = nextVal
}
},
// Установить текстовое содержимое
setElementText(el, text) {
el.children = []
el._text = text
},
// Получить текст
getText(el) {
return el._text
},
// Родительский узел
parentNode(el) {
return el.parent
},
// Следующий сиблинг
nextSibling(el) {
if (!el.parent) return null
const idx = el.parent.children.indexOf(el)
return el.parent.children[idx + 1] || null
},
}
// =====================================================
// Движок рендеринга (аналог @vue/runtime-core)
// =====================================================
function createCustomRenderer(nodeOps) {
function render(vnode, container) {
if (vnode == null) {
// Unmount
if (container._vnode) unmount(container._vnode)
} else {
patch(container._vnode, vnode, container)
}
container._vnode = vnode
}
function patch(n1, n2, container, anchor = null) {
if (n2 == null) return
if (typeof n2 === 'string' || typeof n2 === 'number') {
const textNode = nodeOps.createText(String(n2))
nodeOps.insert(textNode, container, anchor)
return
}
if (typeof n2.type === 'function') {
// Компонент
patchComponent(n1, n2, container, anchor)
return
}
if (n1 == null) {
mountElement(n2, container, anchor)
} else {
updateElement(n1, n2, container)
}
}
function mountElement(vnode, container, anchor) {
const el = nodeOps.createElement(vnode.type)
vnode.el = el
// Применяем props
for (const [key, val] of Object.entries(vnode.props || {})) {
if (key !== 'children') nodeOps.patchProp(el, key, null, val)
}
// Монтируем детей
const children = vnode.children || []
if (typeof children === 'string') {
nodeOps.setElementText(el, children)
} else {
children.forEach(child => patch(null, child, el))
}
nodeOps.insert(el, container, anchor)
}
function updateElement(n1, n2, container) {
const el = n2.el = n1.el
// Patch props
const oldProps = n1.props || {}
const newProps = n2.props || {}
for (const [k, v] of Object.entries(newProps)) {
if (oldProps[k] !== v) nodeOps.patchProp(el, k, oldProps[k], v)
}
for (const k of Object.keys(oldProps)) {
if (!(k in newProps)) nodeOps.patchProp(el, k, oldProps[k], null)
}
}
function patchComponent(n1, n2, container, anchor) {
const rendered = n2.type(n2.props || {})
patch(null, rendered, container, anchor)
}
function unmount(vnode) {
if (vnode.el) nodeOps.remove(vnode.el)
}
return { render }
}
// =====================================================
// Сериализатор — рендер в строку
// =====================================================
function serialize(node, indent = 0) {
if (!node) return ''
const pad = ' '.repeat(indent)
if (node.type === '#text') return pad + node.text
if (node.type === '#comment') return pad + `<!-- ${node.text} -->`
const attrs = Object.entries(node.props)
.map(([k, v]) => `${k}="${v}"`)
.join(' ')
const tag = node.type + (attrs ? ' ' + attrs : '')
if (node._text) return `${pad}<${tag}>${node._text}</${node.type}>`
if (node.children.length === 0) return `${pad}<${tag} />`
const inner = node.children.map(c => serialize(c, indent + 1)).join('\n')
return `${pad}<${tag}>\n${inner}\n${pad}</${node.type}>`
}
// =====================================================
// Использование Custom Renderer
// =====================================================
const renderer = createCustomRenderer(StringNodeOps)
// Создаём "корневой контейнер"
const root = StringNodeOps.createElement('root')
// VNode-дерево
function App(props) {
return {
type: 'div',
props: { id: 'app', class: 'container' },
children: [
{
type: 'h1',
props: { class: 'title' },
children: 'Привет, Custom Renderer!'
},
{
type: 'ul',
props: { class: 'list' },
children: (props.items || []).map((item, i) => ({
type: 'li',
props: { class: i % 2 === 0 ? 'even' : 'odd' },
children: item,
}))
},
{
type: 'footer',
props: {},
children: '© 2024'
}
]
}
}
// Первый рендер
renderer.render(
{ type: App, props: { items: ['Vue', 'React', 'Angular', 'Svelte'] } },
root
)
console.log('=== Custom Renderer Output ===')
console.log(serialize(root))
console.log('\n=== Статистика дерева ===')
function countNodes(node) {
if (!node || !node.children) return 1
return 1 + node.children.reduce((sum, c) => sum + countNodes(c), 0)
}
console.log('Всего узлов в дереве:', countNodes(root))
console.log('Потомков корня:', root.children.length)
Реализуй упрощённый custom renderer для ASCII-терминала. Функция `createTerminalRenderer()` возвращает объект с методом `render(vnode, container)`. Container — объект `{ lines: [] }`. Поддерживаемые типы vnode: строка/число → добавить в lines, объект `{ type, props, children }` → сгенерировать ASCII-представление: тип "box" → обернуть children в рамку из символов, тип "text" → добавить строку с props.content, тип "list" → каждый child с префиксом "• ". Метод `toString(container)` возвращает содержимое container.lines.join("\n").
В renderNode для type === "box": создай вложенный контейнер { lines: [] }, рендерь children в него, затем вызови makeBox(innerContainer.lines) и добавь результат в основной container.lines. Для type === "list": рендерь каждый child во временный контейнер, возьми его lines и добавь каждую с префиксом "• ". В makeBox: const w = Math.max(...lines.map(l => l.length)); border = "+" + "-".repeat(w+2) + "+"; inner = lines.map(l => "| " + l.padEnd(w) + " |").
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке