渲染器的设计

本篇为阅读《Vue.js》所作笔记~ 感受一下渲染器的重要性

  • 渲染器是Vue.js中非常重要的一部分,很多功能依赖渲染器来实现,例如Transition组件、Teleport组件、Suspense组件以及template ref 和自定义指令等

渲染器与响应系统的结合

渲染器是用来执行渲染任务的,在浏览器平台上,用它来渲染其中的真实DOM元素。渲染器还是框架跨平台能力的关键,在设计渲染器的时候需要考虑好自定义的能力。本节将渲染器限定在DOM平台,下面的函数就是一个合格的渲染器:

js 复制代码
    function renderer(domString, container) {
        container.innerHTML = domString
    }
    renderer(`<h1>hello</h1>`, document.getElementById('app'))

不仅可以渲染静态的字符串,还可以渲染动态拼接的HTML内容 如果是一个响应式数据,可以联想到副作用函数 利用响应系统 可以让整个渲染过程自动化

js 复制代码
   const count = ref(1)
   effect(() => {
       renderer(`<h1>${count}</h1>`, document.getElementById('app'))
   })
   count++

渲染器的基本概念

  1. 通常用renderer来表示'渲染器',render表示'渲染'。渲染器的作用是把虚拟DOM渲染为特定平台上的真实元素。在浏览器平台上,渲染器会把虚拟DOM渲染为真实DOM元素

    虚拟DOM通常用 virtual DOM来表达,简写成vdom 虚拟DOM和真实DOM的结构一样,都是由一个个节点组成的树型结构 所以经常能听到'虚拟节点'这样的词,即 virtual node,简写成vnode 虚拟DOM是树型结构,这棵树中的任何一个vnode节点都可以是一颗子树 因此vdom和vnode有时可替换使用,以下统一使用vnode

  2. 渲染器把虚拟DOM节点渲染为真实DOM节点的过程叫做 挂载(mount),例如Vue.js组件中的mounted钩子就会在挂载完成时触发。这意味着,在mounted钩子中可以访问真实DOM元素。

  3. 渲染器将真实DOM挂载在哪里呢?渲染器通常需要接收一个挂载点作为参数,用来指定具体的挂载位置('挂载点'其实就是一个DOM元素,渲染器会把该DOM元素作为容器元素,并把内容渲染到其中)

    js 复制代码
        function createRenderer() {
            function render(vnode, container) { }
            function hydrate(vnode, container) { }
            return {
                render,
                hydrate
            }
        }

    如上代码,createRenderer函数用来创建一个渲染器,为什么需要这个函数,直接定义render不就可以了吗?

    渲染器与渲染是不同的,渲染器是更加宽泛的概念,包含渲染, 把vnode渲染为真实DOM的render函数只是其中一部分。渲染器不仅可以用来渲染,还可以用来激活已有的DOM元素,这个过程通常发生在同构渲染的情况下。在Vue.js3中,甚至连创建应用的createApp函数也是渲染器的一部分

  4. 有了渲染器,我们可以用它来执行渲染任务了,如下:

    js 复制代码
        const renderer = createRenderer()
        // 首次渲染
        renderer.render(vnode, document.querySelector('#app'))

    首先调用了createRenderer创建了一个渲染器,接着调用渲染器的render函数执行渲染,首次调用render函数时,只需要创建新的DOM元素即可,这个过程只涉及挂载

    当多次在同一个container上调用render函数进行渲染时,渲染器除了要执行挂载动作外,还要执行更新动作

    js 复制代码
        // 首次渲染
        renderer.render(oldVnode, document.querySelector('#app'))
        // 第二次渲染
        renderer.render(newVnode, document.querySelector('#app'))

    首次渲染已经将oldVnode渲染到container内了,再次调用render函数并尝试渲染newVnode时,就不能简单地执行挂载动作了,在这种情况下,渲染器会使用newVnode与oldVnode进行比较,试图找到并更新变更点。这个过程叫做'打补丁'(或更新patch),挂载动作本身也可以看作一种特殊的打补丁,特殊之处在于旧的vnode是不存在的,代码如下:

    js 复制代码
        function createRenderer() {
            function render(vnode, container) {
                if (vnode) {
                    // 新vnode存在 将其与旧的vnode一起传递给patch函数 进行打补丁
                    patch(container._vnode, vnode, container)
                } else {
                    if (container_vnode) {
                        // 旧vnode存在 且新的vnode不存在 说明是卸载(unmount)操作 只需要将container内的DOM清空即可
                        container.innerHTML = ''
                    }
                }
                // 把vnode存储到container_vnode 下 即后续渲染中的旧vnode
                container._vnode = vnode
            }
            return {
                render,
            }
        }
        const renderer = createRenderer()
        // 首次渲染
        renderer.render(vnode1, document.querySelector('#app'))
        // 第二次渲染
        renderer.render(vnode2, document.querySelector('#app'))
        // 第三次渲染
        renderer.render(null, document.querySelector('#app'))
    1. 首次渲染时,渲染器将vnode1渲染为真实DOM。渲染完成后,vnode1会存储到容器元素的container._vnode属性中,它会在后续渲染中作为旧vnode使用
    2. 第二次渲染时,旧vnode存在,此时渲染器会把vnode2作为新vnode,并将新旧vnode一同传递给patch函数进行打补丁
    3. 第三次渲染时,新vnode的值为null,即什么都不渲染,但此时容器中渲染的是vnode2所描述的内容,所以渲染器需要清空容器,上述代码使用container.innerHTML = ''来清空,需要注意这样子清空容器是有问题的,不过暂时使用它来达到目的
  5. patch (container._vnode, vnode, container)

    该函数是整个渲染器的核心入口,它承载了最重要的渲染逻辑

    js 复制代码
        function patch(n1, n2, container) {}

    至少接收三个参数:

    • n1:旧vnode
    • n2:新vnode
    • container:容器

    在首次渲染时,容器元素的container._vnode属性是不存在的,即undefined。这意味着,在首次渲染时传递给n1也是undefined。这时,patch函数会进行挂载动作,它会忽略n1,并直接将n2描述的内容渲染到容器中。从这点可以看出,patch函数不仅可以用来打补丁,也可以用来执行挂载

自定义渲染器

  • 以下以浏览器作为渲染的目标平台,编写一个渲染器,在这个过程中,看看哪些内容是可以抽象的,然后通过抽象,将浏览器特定的API抽离,这样就可以使得渲染器的核心不依赖于浏览器,在此基础上,再为那些被抽离的API提供可配置的接口,即可实现渲染器的跨平台能力
  1. 从渲染一个普通的<h1>标签开始。

    js 复制代码
        const vnode = {
            type: 'h1',
            children: 'hello'
        }
        const renderer = createRenderer()
        renderer.render(vnode, document.querySelector('#app'))

    使用type属性来描述一个vnode的类型,不同类型的type属性值可以描述多种类型的vnode 当type属性是字符串类型值时,可以认为它描述的是普通标签,并使用该type属性的字符串值作为标签的名称。 对于这样一个vnode,我们可以使用render函数渲染它

  2. 为了完成渲染工作,需要补充patch函数

    js 复制代码
        function createRenderer() {
            function patch(n1, n2, container) {
                if (!n1) {    // 如果n1不存在 意味着挂载 则调用mountElement函数完成挂载
                    mountElement(n2, container)
                } else {
                    // n1存在 意味着打补丁
                }
            }
    
            function mountElement(vnode, container) {
                // 创建DOM元素
                const el = document.createElement(vnode.type)
                // 处理子节点 如果子节点是字符串 代表元素具有文本节点
                if (typeof vnode.children === 'string') {
                    // 只需要设置元素的 textContent 属性即可
                    el.textContent = vnode.children
                }
                // 将元素添加到容器中
                container.appendChildren(el)
            }
    
            function render(vnode, container) {}
            return {
                render
            }
        }

    如上,将patch函数、mountElement函数编写在createRenderer函数内

    这样子,就完成了一个vnode的挂载

  3. 分析代码存在问题:mountElement函数内调用了大量依赖于浏览器的API,要设计通用的渲染器,需要将这些浏览器特有的API抽离。可以将这些操作DOM的API作为配置项,该配置项可以作为createRenderer函数的参数,如下:

    在mountElement等函数内就可以通过配置项来取得操作DOM的API

    重构后的mountElement函数在功能上没有任何变化。不同的是,它不直接依赖于浏览器的特有API

    只要传入不同的配置项,就能完成非浏览器环境下的渲染工作

    js 复制代码
        const renderer = createRenderer({
            createElement(tag) {    // 用于创建元素
                return document.createElement(tag)
            },
            setElementText(el, text) {   // 用于设置元素的文本节点
                el.textContent = text
            },
            insert(el, parent, anchor = null) {  // 用于在给定的parent下添加指定元素
                parent.insertBefore(el, anchor)
            }
        })
    
        function createRenderer(options) {
            const {
                createElement,
                setElementText,
                insert
            } = options
            function mountElement(vnode, container) {
                const el = createElement(vnode.type)
                if (typeof vnode.children === 'string') {
                    setElementText(el, vnode.children)
                }
                insert(el, container)
            }
            return {
                render
            }
        }
        renderer.render(vnode, document.querySelector('#div'))
  1. 通过传入不同配置项,可以实现一个用来打印渲染器操作流程的自定义渲染器,如下:

    js 复制代码
        const renderer = createRenderer({
            createElement(tag) {
                console.log(`创建元素${tag}`);
                console.log({ tag });
                return { tag }
            },
            setElementText(el, text) {
                console.log(`设置${JSON.stringify(el)}的文本内容为${text}`);
                el.text = text
            },
            insert(el, container, anchor = null) {
                console.log(`将${JSON.stringify(el)}添加到${JSON.stringify(container)}下`);
            }
        })
        const container = { type: 'root' }  // 设置一个挂载点
        renderer.render(vnode, container)

  • 在createElement内,不再调用浏览器的API,而是仅仅返回一个对象{tag},并将其作为创建出来的'DOM'元素
  • 在setElementText和insert函数内,同样没有调用浏览器相关的API,而是自定义了一些逻辑,并打印信息到控制台
  • 上面实现的自定义渲染器不依赖浏览器特有的API,所以这段代码不仅可以在浏览器中运行,也可以在Node.js中运行

自定义渲染器只是通过抽象的手段,让核心代码不再依赖平台特有的API,再通过个性化配置的能力实现跨平台

相关推荐
活宝小娜3 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点3 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow3 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
刚刚好ā4 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
会发光的猪。6 小时前
css使用弹性盒,让每个子元素平均等分父元素的4/1大小
前端·javascript·vue.js
天下代码客7 小时前
【vue】vue中.sync修饰符如何使用--详细代码对比
前端·javascript·vue.js
周全全7 小时前
Spring Boot + Vue 基于 RSA 的用户身份认证加密机制实现
java·vue.js·spring boot·安全·php
ZwaterZ8 小时前
vue el-table表格点击某行触发事件&&操作栏点击和row-click冲突问题
前端·vue.js·elementui·c#·vue
码农六六8 小时前
vue3封装Element Plus table表格组件
javascript·vue.js·elementui
徐同保8 小时前
el-table 多选改成单选
javascript·vue.js·elementui