【拆解Vue3】渲染器是如何实现的(上篇)?

302 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

本篇内容基于【拆解Vue3】ref是如何实现的?实现。

渲染器是什么?

通俗点理解,渲染就是画画,渲染器就是一个会画画的机器。对于一台会画画的机器来说,它掌握的是仅仅是画画的技能,要完成一幅作品,还需要颜料和画布

我们在【拆解Vue3】Vue是如何运行的?中,已经简单的聊过了这个问题。对Vue来说,Vue的渲染器需要的颜料就是虚拟DOM(vDOM, virtual DOM),但没提到的是,Vue作为一个前端框架,前端不仅仅只有DOM,常见的还有Canvas等,这些都可以作为渲染器的画布。

这里需要进一步说明,我们为什么要用颜料和画布来做比喻。在宣纸上画画,要用墨画;在亚麻布上画画,要用颜料画。同样的,渲染器在DOM上渲染,与在Canvas上渲染,我们当然需要不同的API来进行渲染操作。这些都需要我们在编码前能够考虑到。

在DOM下实现一个简单渲染器

在一开始,为了简化问题,我们就先认为,我们的画布就是DOM,配合JavaScript的DOM API就可以完成渲染的工作了。现在,我们想在DOM上渲染一个数字要怎么做呢?这里我们使用innerHTML来实现。

想到这里就能发现一个问题,DOM操作要求我们,在操作DOM时,需要指定被操作的DOM节点。所以渲染器在渲染时,至少需要两个前提条件,一是要知道渲染的内容,二是要知道把内容渲染在哪里。按照这个思路,我们可以先来实现一个简单的渲染器,并进行简单的渲染了。

function renderer(domString, container) {
  container.innerHTML = domString
}

好了,现在我们已经有了一个简单的渲染器函数了。接下来让我们和响应式结合一下,看看有什么神奇的效果。

const count = ref(1)
effect(() => {
  renderer(`<h1>${count.value}</h1>`, document.getElementById('app'))
})
const addBtn = document.getElementById('addBtn')
addBtn.addEventListener('click', () => count.value++)
const minBtn = document.getElementById('minBtn')
minBtn.addEventListener('click', () => count.value--)

我们把渲染器用effect包裹,定义了一个原始类型的响应式数据count。当countclick事件修改时,就会触发effect,调用渲染器重新渲染页面。

搭建渲染器雏形

上一小节中我们仅针对DOM平台,设计了DOM的渲染器。接着,我们开始尝试搭建一个不依赖于具体平台的通用渲染器。我们先来描述一个虚拟DOM,这个DOM表示我们要在页面上输出一个hello vue,并用h1标签标记它。

const vnode = {
  type: 'h1',
  children: 'hello vue'
}

首先,为了实现通用性,我们需要对具体的API操作进行封装,这里我们还以最常用的DOM为例。想想上一小节中那个简单的例子,这里我们至少需要封装三个操作:

  1. 创建元素:document.createElement(tag)
  2. 设置文本:el.textContent
  3. 将创建好的元素放到父元素指定的位置上:parent.insertBefore(el, anchor)
const DOMOptions = {
  createElement(tag) {
    return document.createElement(tag)
  },
  setElementText(el, text) {
    el.textContent = text
  },
  insert(el, parent, anchor = null) {
    parent.insertBefore(el, anchor)
  }
}

这里我们做的仅仅是模拟对一个元素的渲染,在Vue中,还有更加复杂的组件渲染,因此,我们单独把元素渲染的逻辑,用函数封装起来。沿用上面的思路,一是要知道渲染的内容,二是要知道把内容渲染在哪里。注意,在渲染时我们要务必保证平台无关。

function mountElement(vnode, container) {
  const el = createElement(vnode.type)
  if(typeof vnode.children === 'string') { // (1)
    setElementText(el, vnode.children)
  }
  insert(el, container)
}

在(1)处我们增加了一个判断,这里是考虑到vnode.children不是只有string一种选择。然后,我们就可以把mountElement这个渲染节点的功能,放入到渲染器里了。

function createRender(options) {
  const { createElement, setElementText, insert } = options // (2)
  function render(vnode, container) { 
    // 如果渲染的是元素 
    mountElement(vnode, container)
    function mountElement(vnode, container) {
      const el = createElement(vnode.type)
      if(typeof vnode.children === 'string') {
        setElementText(el, vnode.children)
      }
      insert(el, container)
    }
  }
  return {
    render
  }
}
const vnode = {
  type: 'h1',
  children: 'hello vue'
}
const renderer = createRender(DOMOptions)
renderer.render(vnode, document.querySelector('#app'))

从(2)处可以看到,不同的平台只要传入不同的options,就能完成对应的渲染。我们将不同平台的API封装成配置项,就可以屏蔽对特定平台的依赖 这里我们针对给出的vnode初步实现了一个简单的通用渲染器。这里只包含了一段简单的hello vue文本,在真实开发场景下vnode一定要比我们所实现的复杂的多,很多case我们也还没有考虑到,让我们在后面的文章中继续完善吧!

参考资料