基本概念
了解渲染器所涉及的基本概念,有助于更好的理解框架 API 的设计。
渲染器 & 渲染
通常使用名词 renderer
来表示 "渲染器",使用动词 render
来表示 "渲染"。渲染器 的作用是把虚拟 DOM
渲染 为特定平台上的真实元素,例如,在浏览器平台上,渲染器会把 虚拟 DOM
渲染为 真实 DOM
元素。
虚拟 DOM & 虚拟节点
虚拟 DOM
通常使用英文 virtual DOM
表示,简写为 vdom
。虚拟 DOM
和 真实 DOM
结构是一样的,都是由一个个节点组成的树形结构,而 虚拟节点
使用 virtual node
来表示,简写为 vnode
。虚拟 DOM
是树形结构,其中的任何一个节点 vnode
都可以代表一颗子树,因此 vnode
和 vdom
是可以替换使用的。
挂载
渲染器
把 虚拟 DOM
节点渲染为 真实 DOM
节点的过程叫作 挂载,英文表示为 mount
,例如 在 Vue.js 组件中的 mounted
钩子就会在挂载完成时触发,这就意味着可以在这个钩子中访问到 真实 DOM
元素。
通过一下代码来辅助理解:
function createRenderer(){ fucntion render(vnode, container){ ... } fucntion hydrate(){ ... } // 返回渲染函数和 createApp return { render, hydrate, createApp: createAppAPI(render, hydrate) } } 复制代码
其中 createRenderer
函数用来创建一个渲染器,调用 createRenderer
函数后会得到一个 render
函数,这个 render
函数会以 container
为挂载点,将 vnode
渲染为真实 DOM 并进行挂载。
为什么需要 createRenderer 函数?
渲染器
和 渲染
是不同的,渲染器
是更加宽泛的概念,它包含了 渲染
,渲染器不仅可以用来渲染,还可以用来激活已有的 DOM 元素
,这通常发生在 同构渲染
的情况下。可以看到,当创建渲染器时,渲染器除了包含 render
函数外,还包含了 hydrate
函数,专门用于处理服务端渲染。
// 创建对应平台的渲染器 cosnt renderer = createRenderer() // 首次渲染,进行挂载 renderer.render(vnode, document.querySelector('#app')) // 后续渲染,进行更新 renderer.render(newVnode, document.querySelector('#app')) 复制代码
render 函数的实现思路
为了便于理解,先看下面的 render
部分的伪代码:
function createRenderer() { function render(vnode, container) { if (vnode) { // 新的 vnode 存在,将其与旧的 vnode 一起传递给 patch 函数,进行补丁(挂载 或 更新) patch(container._vnode, vnode, container) } else { if(container._vnode){ // 新的 vnode 不存在,旧的 vnode 存在,说明当前属于 unmount 操作 // 这里简单的通过 container.innerHTML 将 container 的内容清空 container.innerHTML = '' } } // 将新的 vnode 存储到 container._vnode 中,即后续渲染中旧的 vnode container._vnode = vnode } return { render, } } 复制代码
假设连续使用三次 renderer.render
函数执行渲染,如下:
// 容器元素 const app = document.querySelector("#app"); // 创建渲染器 const renderer = createRenderer(); // 首次渲染 renderer.render(vnode1, app); // 第二次渲染 renderer.render(vnode2, app); // 第三次渲染 renderer.render(null, app); 复制代码
- 首次渲染时,会将
vnode1
渲染为真实 DOM
,渲染完成后,vnode1
会被存储到container._vnode
中,作为后续渲染中的 旧vnode
使用 - 第二次渲染时,旧
vnode
存在,此时会把vnode2
作为 新vndoe
,并将 新旧vnode
传递给patch
函数进行补丁 - 第三次渲染时,新
vnode
节点为null
,即什么都不渲染,但此时容器中渲染的是vnode2
的内容,所以渲染器需要清空容器,当然直接通过innerHTML = ''
清空的方式是有问题的,这里只是用于表示达到清空的目的
上面的三次渲染分别对应着:挂载、更新、卸载 的过程,patch
函数是整个渲染器的核心入口,它包含了重要的渲染逻辑,其中 patch
函数的各个参数:
function patch(n1, n2, container){...} 复制代码
n1
代表 旧vnode
节点n2
代表 新vnode
节点container
代表真实的容器元素
自定义渲染器
渲染器不仅应该能够把 虚拟 DOM
渲染为浏览器平台上的 真实 DOM
,也应该能实现在渲染到任意平台上,这就意味需要将渲染器中浏览器特定的 API
进行抽象,这样就可以使得渲染器的核心不依赖于浏览器。在此基础上,再为那些抽离 API 提供可配置的接口,既可实现渲染器跨平台的能力。
抽离和平台强相关的 API
首先针对 patch
函数进行一个简单的实现,并且通过 mountElement
完成挂载操作,如下:
// 渲染器 function createRenderer() { // mountElement function mountElement(vnode, container) { // 创建 dom 元素 const el = document.createElement(vnode.type) // 若子节点是字符串,则代表是文本内容 if (typeof vnode.children === 'string') { el.textContext = el.children; } // 将子元素添加到容器中 container.appendChild(el) } // patch function patch(n1, n2, container) { if (!n1) { mountElement(n2, container) } } // 渲染函数 function render(vnode, container) { if (vnode) { patch(container._vnode, vnode, container) } else { if (container._vnode) { container.innerHTML = '' } } container._vnode = vnode } return { render, } } 复制代码
通过上述内容,我们的目标是设计一个不依赖于浏览器平台的通用渲染器,但是在 mountElement
函数内调用了大量依赖于浏览器的 API(如:createElement、appendChild、textContext)
,因此第一步就是将这些依赖于浏览器的 API
进行抽离。
可以在创建渲染器时通过传入对应的配置项,如下:
// 在创建 renderer 时传入配置项 const renderer = createRenderer({ // 用于创建元素 createdElement(tag) { return document.createElement(tag) }, // 用于设置元素的文本节点 setElementText(el, text) { el.textContent = text }, // 用于在给定的 parent 下添加指定元素 insert(el, parent, anchor = null) { parent.insertBefore(anchor, el) } }) 复制代码
于是在渲染器内就可以通过配置项 options
对获取对应操作 DOM 的 API 了:
// 渲染器 function createRenderer(options) { // 通过配置项获取操作 DOM 的 API const { createElement, setElementText, insert } = options; // mountElement function mountElement(vnode, container) { // 创建 dom 元素 const el = createElement(vnode.type) // 若子节点是字符串,则代表是文本内容 if (typeof vnode.children === 'string') { setElementText(el.children); } // 将子元素添加到容器中 insert(el, container) } // patch function patch(n1, n2, container) { if (!n1) { mountElement(n2, container) } } // 渲染函数 function render(vnode, container) { if (vnode) { patch(container._vnode, vnode, container) } else { if (container._vnode) { container.innerHTML = '' } } container._vnode = vnode } return { render, } } 复制代码
重构后的代码,已经不再直接依赖于浏览器特有的 API
了,并且通过传入不同的配置项,就能够完成非浏览器环境下的渲染工作。
最后
有了对渲染器最基本的了解,在结合 Vue.js
源码的学习会有更深刻的理解,为什么 vue.js
要如此设计其 API
。