vue3 自定义渲染器createRenderer

255 阅读4分钟

定义

渲染器基于虚拟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接口的代码实现

image.png

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

image.png

//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>

image.png

自定义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>

image.png

自定义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>

image.png

自定义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>

image.png

例子代码

mjsong07/vue3-renderer: vue3自定义渲染器demo (github.com)

其他参考

three.js+vue3

TroisJS three.js+ vue3

pixi.js+vue3

vue-canvas-snake: canvas+ pixi

hairyf/vue3-pixi:

代码参考

玩转Vue 3