渲染器是 Vue.js 中非常重要的一部分,很多功能依赖渲染器来实现。渲染器也是框架性能的核心,其实现直接影响框的性能。
本章我们使用 @vue/reactivity
包提供的响应式 API VueReactivity
进行讲解,以下是一个基本的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <script src="https://unpkg.com/@vue/reactivity@3.0.5/dist/reactivity.global .js"></script>
<body> <div id="app"></div> <script> const { ref, effect } = VueReactivity; function render(domString,container){ container.innerHTML = domString; } const count = ref(0); effect(() => { console.log(count.value); render(`<h1>${count.value}</h1>`, document.getElementById('app')); }); count.value++; </script> </body>
|
渲染器基本概念
renderer
**渲染器(renderer)**的作用是把虚拟 DOM 渲染为特定平台上的真实元素。在浏览器平台上,渲染器会把虚拟 DOM 渲染为真实 DOM 元素。
虚拟 DOM 即 virtual DOM (vdom),和真实 DOM 结构一样,都是由节点组成的树型结构。这个树的节点叫做虚拟节点 virtual node (vnode)。由于任何一个 vnode 节点都可以是一棵子树,vnode 和 vdom 可以替换使用。原书统一使用 vnode。
渲染器把虚拟 DOM 节点渲染为真实 DOM 节点的过程叫作挂载 (mount)。渲染器通常需要接收一个挂载点作为参数,用来指定具体的挂载位置。这里的“挂载点”是一个 DOM 元素,渲染器会把该 DOM 元素作为**容器元素(container)**,并把内容渲染到其中。
渲染器与渲染不同,在实际运用中渲染器要做的不仅是渲染而已。以下是一个创建渲染器的createRenderer
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function createRenderer(){ function render(vnode,container){ } function hydrate(container,component){ } return { render, hydrate } }
const renderer = createRenderer();
renderer.render(`<h1>${count.value}</h1>`, document.getElementById('app'));
|
可以看到渲染器不仅包含render函数,还有hydrate等其他功能的函数。关于其他功能我们在其他章节讲解。
patch
当在同一个容器上多次调用render渲染时,渲染器除了挂载还会执行更新动作,渲染器会比较新旧节点并更新变更点,这个过程叫作打补丁(更新/patch)。挂载也可以看作没有旧节点的打补丁。
我们render渲染时,调用patch函数进行节点更新。节点更新可以简单分为三种情况:初次挂载、更新节点、卸载节点。对应的,patch函数至少需要三个参数:旧节点n1、新节点n2、容器container:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function patch(n1,n2,container){...}
function render(vnode,container){ if(vnode){ patch(container._vnode,vnode,container) }else{ if(container._vnode){ patch(container._vnode,null,container) } } container._vnode = vnode; }
|
自定义渲染器
我们的渲染器不止于把虚拟dom渲染为浏览器的真实dom,还应该作为通用渲染器,可以渲染到任意平台上。但不同平台的对应API不同,为了进行不同平台的渲染兼容,我们应该将渲染时涉及到针对平台的API提取出来,并提供可配置的接口,实现跨平台能力。
比如我们在创建元素时,浏览器api是这样的:
1
| const el = document.createElement(vnode.type)
|
但不同平台创建元素的操作可能就不同,我们应该将createElement
这个方法提取出来,并单独配置。
我们可以将这些操作 DOM 的 API 作为配置项,该配置项可以作为 createRenderer
函数的参数。我们利用这些配置项进行渲染器的内容添加与完善:
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
| function createRenderer(options){ const { createElement, insert, setElementText } = options; function mountElement(vnode,container){ const el = createElement(vnode.type); 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){ ... } ... return { render } }
|
我们在创建渲染器时将操作DOM的API传入配置项:
1 2 3 4 5 6 7 8 9 10 11
| const renderer = createRenderer({ createElement(tag){ return document.createElement(tag); }, insert(el,parent,anchor = null){ parent.insertBefore(el,anchor); }, setElementText(el,text){ el.textContent = text; } });
|
这样createRenderer
就可以通过配置项获取浏览器DOM操作的API了。只要传入不同的配置项,就能够完成非浏览器环境下的渲染工作,比如以下这段console模拟的渲染操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const renderer = createRenderer({ createElement(tag) { console.log(`创建元素 ${tag}`) return { tag } }, setElementText(el, text) { console.log(`设置 ${JSON.stringify(el)} 的文本内容:${text}`) el.textContent = text }, insert(el, parent, anchor = null) { console.log(`将 ${JSON.stringify(el)} 添加到 ${JSON.stringify(parent)} 下`) parent.children = el } })
const vnode = { type: 'h1', children: 'hello' } const container = { type: 'root' } renderer.render(vnode, container)
|