Vue3渲染器与编译器深入浅析

渲染器

相关概念

渲染器是Vue框架性能的核心,Vue3的渲染器不仅包含传统的Diff算法,还独创了快捷的路径更新方式,可以充分利用渲染器提供的信息,大大提升更新和渲染的性能;

  • Vue中的h函数返回的就是一个对象,其作用是让编写的虚拟DOM变得更加轻松,换句话说h函数就是一个辅助创建虚拟DOM的工具函数而已;
  • 虚拟DOM就是用来描述真实DOM的普通JS对象,渲染器会将该对象渲染成真实DOM元素,当然虚拟DOM也可以描述组件信息,而组件又可以理解成为一组DOM元素的封装
  • 组件的渲染函数:一个组件要渲染的内容是通过渲染函数来描述的,也就是Vue中的render函数,Vue会根据组件的render函数的返回值拿到虚拟DOM,然后将组件的内容渲染出来;
  • 渲染器在渲染组件时,需要先通过执行组件的渲染函数拿到返回值即组件要渲染的内容,可以称为为subTree,最后再递归调用渲染器将subTree渲染出来即可

组件可以通过函数JS对象来实现

  • 通过函数实现时:该函数返回一个描述DOM节点的虚拟DOM即可,此时渲染器的Tag属性值就是该组件函数了,对应渲染器中也需要进行判断Tag为函数时进行特殊处理即可(即通过执行该函数拿到对应描述普通节点的虚拟DOM然后执行渲染器renderer即可)
  • 通过JS对象实现时:其renderer返回值就是一个描述节点的虚拟DOM,因此可以直接执renderer函数即可得到虚拟DOM
// MyComponent 是一个对象
const MyComponent = {
 render() {
 return {
 tag: "div",
 props: {
 onClick: () => alert("hello"),
 },
 children: "click me",
 };
 },
};
function mountComponent(vnode, container) {
 // 调用组件函数,获取组件要渲染的内容(虚拟 DOM)
 const subtree = vnode.tag();
 // 递归地调用 renderer 渲染 subtree
 renderer(subtree, container);
}
// MyComponent 是一个函数
function renderer(vnode, container) {
 if (typeof vnode.tag === "string") {
 // 说明 vnode 描述的是标签元素
 // mountElement内部实现与下文的「实现renderer渲染器逻辑一致」
 mountElement(vnode, container);
 } else if (typeof vnode.tag === "function") {
 // 说明 vnode 描述的是组件
 mountComponent(vnode, container);
 }
}
function mountComponent(vnode, container) {
 // 调用组件函数,获取组件要渲染的内容(虚拟 DOM)
 const subtree = vnode.tag();
 // 递归地调用 renderer 渲染 subtree
 renderer(subtree, container);
}

render与renderer

  • renderer代表渲染器,其作用是将虚拟DOM渲染为特定平台上的真实DOM,如在浏览器平台上,渲染器会将虚拟DOM渲染为真实DOM
  • render表示渲染,是执行的动作
  • 渲染器不仅用来渲染,还可以用来执行其他操作,如激活已有DOMSSR等;是更宽泛的概念,renderer包含render;
  • 渲染器除了普通的创建节点功能外,还需要实现精确的定点更新对应变化的虚拟DOM到界面上,其内部原理简单,归根结底就是通过熟悉的DOM操作API来完成渲染工作

实现renderer渲染器

function renderer(vnode, container) {
 // 使用 vnode.tag 作为标签名称创建对应的 DOM 元素
 const el = document.createElement(vnode.tag);
 // 遍历 vnode.props,将属性、事件添加到 DOM 元素
 for (const key in vnode.props) {
 if (/^on/.test(key)) {
 // 如果 key 以 on 开头,说明它是事件
 el.addEventListener(
 key.substr(2).toLowerCase(), // 事件名称 onClick ---> click
 vnode.props[key] // 事件处理函数
 );
 }
 }
 // 处理 children
 if (typeof vnode.children === "string") {
 // 如果 children 是字符串,说明它是元素的文本子节点
 el.appendChild(document.createTextNode(vnode.children));
 } else if (Array.isArray(vnode.children)) {
 // 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
 vnode.children.forEach((child) => renderer(child, el));
 }
 // 将元素添加到挂载点下
 container.appendChild(el);
}
  • 创建元素:对应虚拟DOM中的Tag属性
  • 为创建的元素添加属性和事件:对应虚拟DOM中的Props属性
  • 添加子元素:对应虚拟DOM中的children属性
    • 这里的children可以是数组或字符串,数组时需要以刚才新建的DOM节点为父节点进行回调渲染器renderer进行递归渲染,当是普通字符串时表明子节点是一个普通文本,直接调用createTextNode API进行创建后添加到新创建的元素内部即可

具体代码实现逻辑

角色解析

  • 控制着Vue生命周期里试图的挂载、更新和渲染

具体作用

  • 创建VNode(h函数)
  • 挂载(mount)/渲染(render)/更新(patch)
  • 渲染的核心diff算法

Vue3特殊内置组件解析

<template>
 // Portal 把里面的内容挂载到 #app
 <Portal target="#app">
 <div class="overlay"></div>
 </Portal>
</template>
  • Fragment
    • 只将该VNode的子节点渲染到页面上
    • 相当于Vue2中的template标签
  • Portal
    • 允许将其中的内容渲染到任何地方

渲染器阶段分析

mount挂载阶段

mount就是将VNode挂载到真实DOM上

patch阶段

patch就是使用新的VNode和旧的VNode进行对比,用最少的资源实现DOM更新,也叫「打补丁」

简单实现

渲染器接受两个参数,要被渲染的VNode和挂载点(承载内容的container)

function createRenderer() {
 function render(vnode, container) {
 if (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,
 };
}

编译器

编译器在模板编译时可以识别哪些是静态属性,哪些是动态属性,从而在编译生成代码时可以将这些信息附加到编译后代码中,这样可以避免渲染器花费力气去寻找变更点

相关概念

编译器和渲染器一样,不同之处在于编译器是将模板编译成渲染函数,而模板就是一个普通的字符串,编译器会分析该字符串并生成一个功能与之相同的渲染函数;
模板在Vue中体现在.vue文件中的<template>标签的内容,本身.vue文件就是一个组件,编译器会将模板内容编译成渲染函数并添加到<script>标签的组件对象上;

<template>
 <div @click="login">
 登录
 </div>
</template>
<script>
 export default {
 data() {
 },
 methods: {
 login: () => { }
 }
 }
</script>
//编译后
export default {
 data() {},
 methods: {
 login: () => { }
 },
 render() {
 return h('div', { onClick: login }, '登录')
 }
}

总结

通过将渲染器和编译器相配合,从而达到进一步提升性能的目的

  • 虚拟DOM和模板都可以描述UI,虚拟DOM比模板更加灵活,模板比虚拟DOM更加直观

组件的实现需要依赖于渲染器,模板的编译需要依赖于编译器,而且编译后的代码是根据渲染器和虚拟DOM的设计决定的,因此Vue中各个模块之间是相互关联、相互制约的;

模板工作原理:无论是模板还是直接手写渲染函数,对于一个组件来说,其渲染内容最终都是通过渲染函数产生的,然后渲染器再将渲染函数返回 的虚拟DOM渲染为真实DOM

作者:FE杂志社

%s 个评论

要回复文章请先登录注册