渲染器是 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)
  |