7-渲染器的设计

83 阅读3分钟

渲染器的设计

渲染器与响应系统的结合

渲染器不仅能够渲染真实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由一个个节点组成的树形结构

虚拟节点:vnodevdom这棵树中的任何一个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)
 }