渲染器的设计

渲染器与响应系统结合

📝 渲染器是用来执行渲染任务的。在浏览器平台上,用它来渲染真实 DOM 元素。

渲染器不仅能够渲染真实 DOM 元素,它还是框架跨平台能力的关键。在限定的 DOM 平台,渲染器能够渲染一个真实 DOM,那么下面这个 renderer 函数就是一个合格的渲染器。@vue/reactivity 提供了 IIFE 模块格式,我们可以直接引用:

<div id="app"></div>
<script src="https://unpkg.com/@vue/reactivity@3.0.5/dist/reactivity.global.js"></script>
<script>
    const { effect, ref } = VueReactivity
    // 渲染器
    function renderer(domString, container) {
        container.innerHTML = domString
    }
    // 利用响应系统声明一个原始值响应数据
    const count = ref(1)
    // 利用响应系统对 count 进行依赖收集
    effect(() => {
        renderer(`<h1>${count.value}</h1>`, document.getElementById('app'))
    })
    // 触发副作用函数执行
    setInterval(() => {
        count.value++
    }, 1000)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

渲染器的基本概念

关键词
  • 渲染器(renderer):把虚拟 DOM 渲染为特定平台上的真实元素
  • 渲染(render):动词,渲染
  • 虚拟 DOM(virtual DOM,vdom):与真实 DOM 结构一样的,由节点组成的树形结构
  • 虚拟节点(virtual node,vnode):虚拟 DOM 树的节点,由一个 JavaScript 对象表示
  • 挂载(mount):渲染器把虚拟 DOM 节点渲染为真实 DOM 节点的过程
  • 挂载点(container):指定渲染器挂载的具体位置,渲染器会把该 DOM 元素作为容器元素

我们通过 createRenderer 函数创建一个渲染器,没有直接定义 render 函数的原因是: ✅ 渲染器不同于渲染,它的概念更加宽泛,它不仅可以用来渲染mount,也可以激活已有元素hydrate...

function createRenderer() {
    // 打补丁:根据 n1 旧节点存在与否,内部挂载或更新
    function patch(n1, n2, container) { /*...*/ }

    function render(vnode, container) {
        if (vnode) {
            // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数进行打补丁
            patch(container._vnode, vnode, container)
        } else {
            if (container._vnode) {
                // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作
                container.innerHTML = ''
            }
        }
        // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode
        container._vnode = vnode
    }

    return {
        render
    }
}

const renderer = createRenderer()

// 首次渲染:挂载 patch(undefined, vnode, container)
renderer.render(vnode1, document.querySelector('#app'))
// 第二次渲染:更新 patch(_vnode, vnode, container)
renderer.render(vnode2, document.querySelector('#app'))
// 第三次渲染:卸载 patch(_vnode, null, container)
renderer.render(null, document.querySelector('#app'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

🔥 首次渲染时已经把 oldNode 渲染到 container 内了,所以当再次调用 renderer. Render 函数并尝试渲染 newNode 时,就不能简单地执行挂载动作了。在这种情况下,渲染器会使用 newNode 与上一次渲染的 oldNode 比较,试图找到并更新变更点。这个过程叫做“打补丁”(或更新),英文通常用 patch 来表达。

📝 挂载动作本身也可以看作一直特殊的打补丁,它的特殊之处在于旧的 VNode 不存在。

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

patch 函数不仅可以用来挂载,也可以用来更新;

render 函数是入口,内部判断是 mount、patch 还是 unmount。

自定义渲染器

🔥 渲染器要实现跨平台能力,需要抽象不可复用部分

📝 对于浏览器作为渲染的目标平台时,将浏览器的特定 API 抽离,这样就可以使得渲染器的核心不依赖于浏览器。在此基础上,我们再为那些被抽离的 API 提供可配置的接口,即可实现渲染器的跨平台能力。

// 把操作 DOM 的 API 封装为一个对象,当做参数传递
function createRenderer(options) {
    // 渲染器根据配置得到操作 DOM 的 API,实现通用的渲染器
    const {
        createElement,
        insert,
        setElementText
    } = options

    function mountElement(vnode, container) {
        const el = createElement(vnode.type)
        // 通过传入的 API 操作 DOM
        if (typeof vnode.children === 'string') {
            setElementText(el, vnode.children)
        }
        insert(el, container)
    }

    function patch(n1, n2, container) {
        if (!n1) {
            // 初始挂载
            mountElement(n2, container)
        } else {
            // 更新
        }
    }

    function render(vnode, container) {
        if (vnode) {
            patch(container._vnode, vnode, container)
        } else {
            if (container._vnode) {
                container.innerHTML = ''
            }
        }
        container._vnode = vnode
    }

    return {
        render
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

浏览器渲染器

const renderer = createRenderer({
    createElement(tag) {
        return document.createElement(tag)
    },
    setElementText(el, text) {
        el.textContent = text
    },
    insert(el, parent, anchor = null) {
        parent.insertBefore(el, anchor)
    }
})

const vnode = {
    type: 'h1',
    children: 'hello'
}

renderer.render(vnode, document.querySelector('#app'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

自定义渲染器

通过已有的通用渲染器,我们也可以创建一个用来打印渲染器操作流程的自定义渲染器:

const renderer2 = createRenderer({
    createElement(tag) {
        console.log(`创建元素 ${tag}`)
        return { tag }
    },
    setElementText(el, text) {
        console.log(`设置 ${JSON.stringify(el)} 的文本内容:${text}`)
        el.text = text
    },
    insert(el, parent, anchor = null) {
        console.log(`${JSON.stringify(el)} 添加到 ${JSON.stringify(parent)}`)
        parent.children = el
    }
})

const container = { type: 'root' }
renderer2.render(vnode, container)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

总结

✅ 渲染器与响应系统之间的关系:利用响应系统的能力,我们可以做到,当响应式数据发生变化时自动完成页面更新(或重新渲染)。

✅ 渲染器会执行挂载和打补丁的操作,对于新的元素,渲染器会将它挂载到容器内;对于新旧 vnode 都存在的情况,渲染器则会执行打补丁操作,即对比新旧 vnode,只更新变化的内容。

✅ 通用渲染器将用来创建、修改和删除元素的操作抽象成可配置的对象。用户可以在创建渲染器的时候指定自定义的配置对象,从而实现自定义的行为。

上次更新: 2022/5/31 13:44:49
贡献者: Jinrui Chen, Jerry Chen