vue3渲染器(1)

35 阅读4分钟

前言

响应式系统和渲染器

前面我们讨论了Vue3响应式系统的原理,那么响应式系统跟渲染器是如何搭配使用的呢?下面先用一个见简单的例子帮我们感受一下:

// 响应式系统与渲染器的关系
import { effect, ref } from 'vue'

const count = ref(0)

const renderer = () => {
    document.body.innerHTML = count.value
}

effect(() => {
    renderer()
})


Promise.resolve().then(() => {
    count.value = 1111
})

上面代码我们定义了一个极其简单的渲染器,在副作用函数中执行渲染器,并且读取了响应式数据count,当count发生改变后,renderer重新执行,页面刷新。这便是响应式系统和渲染器搭配使用的一个非常简单的例子,在开始了解渲染器之前,我们先来了解几个概念。

概念

渲染器(renderer)

rendererrender是不同的,render是动词,表示渲染,render函数执行后会返回vnoderenderer则将vnode渲染成特定平台的节点

vnode

vnode,即虚拟节点,本质是一个Javascript对象,用来描述真实节点,使用虚拟节点的一个好处是脱离平台,有利于跨平台的开发

挂载

渲染器将vnode渲染成真实节点的过程

容器

渲染的节点一个放在哪里,需要指定一个节点作为容器。

正文

言归正传,一个渲染器的基本实现大概长这样,后面我们会在此基础上进行改造

// 渲染器的简易实现
const createRenderer = () => {
    // 客户端渲染
    const render  = (vnode, container) => {
        if (vnode) {
            patch(container._vnode, vnode, container) // 渲染的入口
        } else {
            if (container._vnode) { // 旧节点存在而新节点不存在
                // ... 卸载操作
            }
        }
        container._vnode = vnode
    }
    // 服务端渲染
    const hydrate = () => {
        // ....
    }
    // 创建元素
    const mountElement = (vnode, container) => {
        const dom = document.createElement(vnode.type) 
        // 暂时考虑子元素节点为文本的情况
        if (typeof vnode.children === 'string') {
            dom.textContent = vnode.children
        }
        container.append(dom)
    }
    const patch = (n1, n2, container) => {
        // 旧节点不存在,说明是创建,即挂载
        if (!n1) {
            mountElement(n2, container)
        } else {
            // 打补丁
        }
    }
    return {
        render,
        hydrate
    }
}

这里使用createRenderer的原因是渲染器是更加宽泛的概念,它包含渲染。渲染器不仅可以用来渲染,还可以用来激活已有的 DOM 元素,返回的是renderhyrate, 这里先关注前者,在此条件上我们来分析以上代码:

  1. render的签名是vnodecontainer,提供要渲染的vnode和指定容器
  2. 判断vnode是否存在,存在则执行patch方法,该方法实际上就是渲染的入口
  3. patch的签名是新旧节点,旧节点不存在,新节点存在,说明是创建/挂载操作,container._vnode = vnode保存本次vnode,作为下一次更新的旧节点;如果新旧节点同时存在,说明是更新操作。那么进行“打补丁”操作,涉及diff算法,后面会详细讨论;如果新节点不存在,旧节点存在而新节点不存在,则执行的是卸载操作。
  4. 根据vnode的类型执行不同的操作,这里只先提供了创建普通标签的方法

测试:

    const vnode = {
        type: 'div',
        children: '这是一个文本节点'
    }
    const renderer = createRenderer()
    renderer.render(vnode, document.querySelector('#app'))

自定义渲染函数

上面的createRenderer创建的renderer只对浏览器有效,因为里面包含了大量与浏览器相关的API,如果我们希望它是跨平台的,那么需要将特殊的API抽离出来,将它设置为可配置的,即操作的方法都是从外面传进去的,那么有

const createRenderer = (options) => {
    const {
        createElement,
        setElementText,
        insert
    } = options
    // 客户端渲染
    const render  = (vnode, container) => {
        if (vnode) {
            patch(container._vnode, vnode, container) // 渲染的入口
        } else {
            if (container._vnode) { // 旧节点存在而新节点不存在
                // ... 卸载操作
            }
        }
        container._vnode = vnode
    }
    // 服务端渲染
    const hydrate = () => {
        // ....
    }
    // 创建元素
    const mountElement = (vnode, container) => {
        const dom = createElement(vnode)
        // 暂时考虑子元素节点为文本的情况
        if (typeof vnode.children === 'string') {
            setElementText(dom, vnode.children)
        }
        insert(container, dom)
    }
    const patch = (n1, n2, container) => {
        // 旧节点不存在,说明是创建,即挂载
        if (!n1) {
            mountElement(n2, container)
        } else {
            // 打补丁
        }
    }
    return {
        render,
        hydrate
    }
}

这样,就可以实现跨平台了,上面的代码可以在浏览器中运行,也可以在nodejs环境中运行,然后我们测试如下:

const vnode = {
    type: 'div',
    children: '这是一个文本节点'
}
const renderer = createRenderer({
    createElement(vnode) {
        console.log(vnode)
        // return document.createElement(vnode.type)
    },
    setElementText(dom, text) {
        console.log('-----setElement----')
        // dom.textContent = text
    },
    insert(container, dom) {
        console.log('---insert---')
        // container.append(dom)
    }
})
renderer.render(vnode, document.querySelector('#app'))

如此,一个自定义渲染器就实现啦。本文就先到这里啦~