渲染器的设计
渲染器与响应系统的结合
渲染器不仅能够渲染真实DOM元素,还是框架跨平台能力的关键
在这一章,我们使用@vue/reactivity这个包提供的响应式API进行学习,引入方式:
<script src="https://unpkg.com/@vue/reactivity@3.0.5/dist/reactivity.global.js"></script>
所以在这里可以测试渲染器与响应系统的结合:
下述代码会在页面上先输出1,一秒后再改为2
<div id="app"></div>
<script>
const {effect, ref} = VueReactivity
function renderer(domString, container){
container.innerHTML = domString
}
const count = ref(1)
effect(() => {
renderer(`<h1>${count.value}</h1>`, document.getElementById('app'))
})
setTimeout(() => {
count.value++
}, 1000)
</script>
渲染器的基本概念
基础概念
渲染器:renderer,作用是把虚拟DOM渲染为特定平台上的真实元素,在浏览器平台上,渲染器把虚拟DOM渲染为真实DOM元素
虚拟DOM:vdom,由一个个节点组成的树形结构
虚拟节点:vnode,vdom这棵树中的任何一个vnode节点都可以是一棵子树,与vdom概念有时候可以互换
挂载:mount,把虚拟DOM节点渲染为真实DOM节点的过程叫做挂载,vue中的mounted钩子就会在挂载完成时触发
容器:container,渲染器需要接受一个挂载点作为参数,用来指定具体的挂载位置
渲染器的构建
此处的渲染器不仅包含render函数,还包括其他很多函数,将vnode渲染为真实DOM只是渲染器的一部分
function createRenderer() {
function render(vnode, container){
}
function hyrate(vnode, container) {
}
return {
render,
hyrate
}
}
更新动作
首先我们要先创建一个渲染器,然后对其连续调用两次渲染
const renderer = createRenderer()
//首次渲染
renderer.render(oldVNode, document.querySelector('#app'))
//第二次渲染
renderer.render(newVNode, document.querySelector('#app'))
在首次渲染中,只需要创建新的DOM即可,这个过程只涉及挂载
而在二次渲染的时候,不能简单的执行挂载动作了,在这种情况下,渲染器会使用newVNode与上一次渲染的oldVNode作比较,找出变更点并更新,这个过程也就是打补丁(更新),即patch
patch:整个渲染器的核心入口,承载了最重要的渲染逻辑需要接收三个参数:旧
vnode、新vnode、容器container首次渲染时,
container._vnode属性不存在(undefined),则说明patch函数执行挂载动作如果存在
container._vnode属性,则执行打补丁动作
以下是代码简单思路:
function createRenderer() {
function render(vnode, container){
if(vnode){
//新vnode存在,则将其与旧vnode一起传递给patch函数进行打补丁
patch(container._vnode, vnode, container)
}else{
//如果新vnode不存在,旧node存在,则是卸载操作(unmount)
if(container._vnode){
//清空container即可
container.innerHTML = ''
}
}
//存储vnode,即后续渲染中的旧vnode
container._vnode = vnode
}
return {
render,
}
}
自定义渲染器
渲染器不仅能够把虚拟DOM渲染为浏览器平台上的真实DOM,还要为其设计为可配置的通用渲染器,可以实现渲染到任意平台
定义patch
此处只需要用它来完成挂载即可,暂时省略打补丁
function patch(n1, n2, container){
if(!n1){
mountElement(n2, container)
}else{
//n1存在则要打补丁,省略
}
}
定义mountElement
需要创建一个vnode.type类型的元素,然后处理children,如果是字符串类型则表示该元素具有文本子节点,这是只需要设置元素的textContent即可,最后将该元素挂载到容器元素内
function mountElement(vnode, container){
const el = document.createElement(vnode.type)
if(typeof vnode.children === 'string'){
el.textContent = vnode.children
}
container.appendChild(el)
}
传入配置项
由于要设计一个不依赖于浏览器平台的通用渲染器,所以要把其中的浏览器API抽离
现在设计这些操作DOM的API作为配置项,这个配置项作为createRenderer函数的参数
const renderer = createRenderer({
//创建元素
createElement(tag){
return document.createElement(tag)
},
//用于设置元素的文本节点
setElementText(el, text){
el.textContent = text
},
//在指定parent下添加指定元素
insert(el, parent, anchor = null){
parent.insertBefore(el, anchor)
}
})
对createRenderer函数进行修改:
function createRenderer(options){
const {
createElement,
insert,
setElementText
} = options
//可以在这个作用域中访问这些API
function mountElement(vnode, container){ /** 省略 */}
function patch(n1, n2, container){ /** 省略 */}
return {
...
}
}
重写mountElement
使用配置项中的API重写mountElement
function mountElement(vnode, container){
const el = createElement(vnode.type)
if(typeof vnode.children === 'string'){
setElementText(el, vnode.children)
}
insert(el, container)
}