携手创作,共同成长!这是我参与「掘金日新计划 · 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
。当count
被click
事件修改时,就会触发effect
,调用渲染器重新渲染页面。
搭建渲染器雏形
上一小节中我们仅针对DOM平台,设计了DOM的渲染器。接着,我们开始尝试搭建一个不依赖于具体平台的通用渲染器。我们先来描述一个虚拟DOM,这个DOM表示我们要在页面上输出一个hello vue,并用h1
标签标记它。
const vnode = {
type: 'h1',
children: 'hello vue'
}
首先,为了实现通用性,我们需要对具体的API操作进行封装,这里我们还以最常用的DOM为例。想想上一小节中那个简单的例子,这里我们至少需要封装三个操作:
- 创建元素:
document.createElement(tag)
; - 设置文本:
el.textContent
; - 将创建好的元素放到父元素指定的位置上:
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我们也还没有考虑到,让我们在后面的文章中继续完善吧!
参考资料
- 《Vue.js设计与实现》霍春阳
- Vue.js (vuejs.org)
- Tiny-Vue: 一个实现了 Vue 核心功能的微型前端框架。 (gitee.com)