定义
渲染器基于虚拟dom渲染
{
tag: 'div',
props: {
id: 'app'
},
chidren: [
{
tag: Container,
props: {
className: 'myclass'
},
chidren: [
'xxxx'
]
}
]
}
vue3 虚拟dom模块
- runtime-core模块: 实现对于节点的增删改查接口。
- runtime-dom模块: 实现具体的web节点操作api
runtime-core 代码实现
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
//插入元素
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
// 删除元素
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
// 创建元素
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
}
return el
}
//...其他操作函数
}
runtime-dom针对runtime-core接口的代码实现
createRenderer 函数区创建了一个渲染器。options传入对应操作dom的函数
export default function createRenderer(options) {
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
cloneNode: hostCloneNode,
insertStaticContent: hostInsertStaticContent
} = options
function render(vnode, container) { }
function mount(vnode, container, isSVG, refNode) { }
function mountElement(vnode, container, isSVG, refNode) { }
function mountText(vnode, container) { }
function patch(prevVNode, nextVNode, container) { }
function replaceVNode(prevVNode, nextVNode, container) { }
function patchElement(prevVNode, nextVNode, container) { }
function patchChildren(
prevChildFlags,
nextChildFlags,
prevChildren,
nextChildren,
container
) { }
function patchText(prevVNode, nextVNode) { }
function patchComponent(prevVNode, nextVNode, container) { }
return { render }
}
createRenderer 最终调用的是 baseCreateRenderer
//vue2硬编码写法
function mountElement(vnode, container, isSVG, refNode) {
const el = isSVG
? document.createElementNS(....)
: document.createElement(vnode.tag)
}
//vue3新增处理逻辑
function mountElement(vnode, container, isSVG, refNode) {
const el = hostCreateElement(vnode.tag, isSVG)
}
渲染逻辑函数
const { render } = createRenderer({
nodeOps: {
createElement() { },
createText() { }
// more...
},
patchData
})
自定义web渲染器
domRenderer.js
import { createRenderer } from '@vue/runtime-core'
const { createApp: originCa } = createRenderer({
createElement(tag) {
return document.createElement(tag)
},
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor)
},
setElementText(el, text) {
el.textContent = text
},
createText(text) {
return document.createTextNode(text)
},
patchProp(el, key, prevValue, nextValue) {
if (key.startsWith('on')) {
// 处理事件绑定
const eventName = key.slice(2).toLowerCase()
if (prevValue) {
el.removeEventListener(eventName, prevValue)
}
el.addEventListener(eventName, nextValue)
} else {
if (nextValue === null || nextValue === undefined) {
el.removeAttribute(key)
} else {
el.setAttribute(key, nextValue)
}
}
},
remove(el) {
const parent = el.parentNode
if (parent) {
parent.removeChild(el)
}
},
parentNode(node) {
return node.parentNode
},
nextSibling(node) {
return node.nextSibling
}
})
function createApp(...args) {
const app = originCa(...args)
return {
mount(selector) {
const div = document.createElement('div')
document.querySelector(selector).appendChild(div)
app.mount(div)
},
}
}
export { createApp }
main.js
import { createApp } from './domRenderer.js'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
App.vue
<template>
<h1>domApp测试</h1>
<div>
<p @click="handleClick"> 点我{{cnt}}</p>
</div>
</template>
<script setup>
import {ref} from 'vue'
const cnt = ref(0)
const handleClick = () => {
console.log("")
cnt.value++
}
</script>
自定义Canvas渲染器
canvasRenderer.js
import { createRenderer } from '@vue/runtime-core'
// 创建canvas上下文
let ctx
function draw(ele, isChild) {
if (!isChild) {
ctx.clearRect(0, 0, 500, 500)
}
ctx.fillStyle = ele.fill || 'white'
if (ele.isShow !== false) {
ctx.fillRect(...ele.pos)
if (ele.text) {
ctx.fillStyle = ele.color || 'white'
ele.fontSize = ele.type == 'h1' ? 20 : 12
ctx.font = (ele.fontSize || 18) + 'px serif'
ctx.fillText(ele.text, ele.pos[0] + 10, ele.pos[1] + ele.fontSize)
}
}
ele.child &&
ele.child.forEach((c) => {
console.log('ele.child', c)
draw(c, true)
})
}
const { createApp: originCa } = createRenderer({
createElement(type) {
return {
type,
}
},
patchProp(el, key, prev, next) {
el[key] = next
},
insert: (child, parent, anchor) => {
console.log('child,parent', child, parent)
if (typeof child == 'string') {
parent.text = child
} else {
child.parent = parent
if (!parent.child) {
parent.child = [child]
} else {
parent.child.push(child)
}
}
if (parent.nodeName) {
draw(child)
if (child.onClick) {
ctx.canvas.addEventListener(
'click',
() => {
//这里把canvas的点击事件委托给了 dom 的click
child.onClick()
setTimeout(() => {
draw(child)
})
},
false
)
}
}
},
remove(el, parent) {},
setElementText(node, text) {
node.text = text
},
createText() {
// Canvas不需要文本节点
},
parentNode(node) {
// Canvas没有父子关系
},
nextSibling(node) {
// 同上
},
})
function createApp(...args) {
const app = originCa(...args)
return {
mount(selector) {
const canvas = document.createElement('canvas')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
document.querySelector(selector).appendChild(canvas)
ctx = canvas.getContext('2d')
app.mount(canvas)
},
}
}
export { createApp }
main.js
import { createApp } from './canvasRenderer.js'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
App.vue
<template>
<div @click="setName('yyyy')" :pos="[10,10,300,300]" fill="#eee">
<h1 :pos="[20,20,200,100]" fill="red" color="#000">counter :{{count}}</h1>
<span :pos="pos" fill="black" >hi {{name}}</span>
<span :isShow="isShow" :pos="[210,10,50,50]" :fill="color" >2222</span>
</div>
</template>
<script setup>
import {ref} from 'vue'
const name = ref('xxxxx')
const color = ref('blue')
const isShow = ref(true)
const pos = ref([20,120,200,100])
const count = ref(1)
const setName = (n)=>{
name.value = n
pos.value[1]+=20
count.value+=1
color.value = 'green'
isShow.value = !isShow.value
}
</script>
自定义three.js渲染器
安装:npm i three@0.168.0
threeRenderer.js
import { createRenderer } from '@vue/runtime-core'
import * as THREE from 'three'
let renderer
function draw(obj) {
const {camera,cameraPos, scene,bgColor, geometry,geometryArg,material,mesh,meshY,meshX} = obj
if([camera,cameraPos, scene,bgColor, geometry,geometryArg,material,mesh,meshY,meshX].filter(v=>v).length<9){
return
}
let cameraObj = new THREE[camera]( 40, window.innerWidth / window.innerHeight, 0.1, 10 )
Object.assign(cameraObj.position,cameraPos)
let sceneObj = new THREE[scene]()
let geometryObj = new THREE[geometry]( ...geometryArg)
let materialObj = new THREE[material]()
let meshObj = new THREE[mesh]( geometryObj, materialObj )
meshObj.rotation.x = meshX
meshObj.rotation.y = meshY
sceneObj.add( meshObj )
sceneObj.background = new THREE.Color(bgColor);
renderer.render( sceneObj, cameraObj );
}
const { createApp: originCa } = createRenderer({
insert: (child, parent, anchor) => {
if(parent.domElement){
draw(child)
}
},
createElement(type, isSVG, isCustom) {
return {
type
}
},
setElementText(node, text) {
},
patchProp(el, key, prev, next) {
el[key] = next
draw(el)
},
parentNode: node => node,
nextSibling: node => node,
createText: text => text,
remove:node=>node
});
function createApp(...args) {
const app = originCa(...args)
return {
mount(selector) {
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
app.mount(renderer)
}
}
}
export { createApp }
main.js
import { createApp } from './threeRenderer.js'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
App.js
<template>
<div
camera="PerspectiveCamera"
:cameraPos=cameraPos
scene="Scene"
:bgColor="bgColor"
geometry="BoxGeometry"
:geometryArg="[0.2,0.2,0.2]"
material="MeshNormalMaterial"
mesh="Mesh"
:meshY="y"
:meshX="x"
>
</div>
</template>
<script>
import {ref} from 'vue'
export default {
setup(){
const y = ref(0.3)
const x = ref(0.3)
const bgColor = ref(0x000000)
const cameraPos = ref({z:1})
window.addEventListener('mousemove',function(e){
y.value = e.clientX/500
x.value = e.clientY/500
})
window.addEventListener('click',function(e){
cameraPos.value.z = cameraPos.value.z + 0.5
bgColor.value = 0x00b578
})
return {y,x,cameraPos,bgColor}
}
}
</script>
自定义pixi.js渲染器
安装:
npm install pixi.js@7.4.2
pixiRenderer.js
import { createRenderer } from '@vue/runtime-core'
import * as PIXI from 'pixi.js';
console.log("PIXI",PIXI)
const { createApp: originCa } = createRenderer({
createElement(type) {
let element;
switch (type) {
case 'container':
element = new PIXI.Container();
break;
case 'sprite':
element = new PIXI.Sprite(PIXI.Texture.WHITE); // 示例为白色纹理精灵
break;
case 'text':
element = new PIXI.Text('Hello, Pixi!', { fill: 0x000000 });
break;
default:
console.warn('Unknown element type:', type);
}
return element;
},
insert(el, parent) {
console.log('insert', el, parent)
if (parent instanceof PIXI.Container) {
parent.addChild(el);
} else {
console.warn('Parent is not a PIXI.Container', parent);
}
},
remove(el) {
if (el.parent) {
el.parent.removeChild(el);
}
},
setElementText(el, text) {
if (el instanceof PIXI.Text) {
el.text = text;
} else {
console.warn('setElementText is not applicable for this element type');
}
},
patchProp(el, key, prevValue, nextValue) {
console.log("patchProp", el, key, prevValue, nextValue)
if (key === 'x') {
el.x = nextValue;
} else if (key === 'y') {
el.y = nextValue;
} else if (key === 'texture') {
el.texture = PIXI.Texture.from(nextValue);
} else if (key === 'fill') {
// 动态设置文字颜色
if (el instanceof PIXI.Text) {
el.style.fill = nextValue;
}
}
},
createText(text) {
return new PIXI.Text(text);
},
parentNode(el) {
return el.parent;
},
nextSibling(el) {
return null; // Pixi 没有兄弟节点的概念
},
setScopeId() {},
cloneNode() {},
insertStaticContent() {},
})
function createApp(...args) {
const app = originCa(...args)
return {
mount(selector) {
const pixi1 = new PIXI.Application({ width: 800, height: 600 ,backgroundColor:0xffff9d});
document.querySelector(selector).appendChild(pixi1.view)
app.mount(pixi1.stage)
},
}
}
export { createApp }
main.js
import { createApp } from './pixiRenderer.js'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
App.vue
<template>
<container>
<sprite :x="x" :y="y" :texture="texture"></sprite>
<text :fill="color" >点击我{{num}}</text>
</container>
</template>
<script>
import {ref} from 'vue'
export default {
setup(){
const y = ref(100)
const x = ref(100)
const num = ref(0)
const color = ref(0x000000)
const texture = ref('https://pixijs.io/examples/examples/assets/bunny.png')
window.addEventListener('click', (e) => { //点击自动移动
y.value = y.value + 3
x.value = x.value + 3
color.value = 0xbbbbbb
num.value++
})
return {y,x,texture,num,color}
}
}
</script>
例子代码
mjsong07/vue3-renderer: vue3自定义渲染器demo (github.com)
其他参考
- uni-app实现
- Vugel - 基于vue渲染webgl Vugel (planning.nl)
- zernonia/vue3-notion
three.js+vue3
pixi.js+vue3
vue-canvas-snake: canvas+ pixi