Vuejs设计与实现11-编译优化与同构渲染

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

十四、编译优化

动态节点收集

patchflags

设存在以下代码

<div>
  <div>foo</div>
  <p>{{ bar }}</p>
</div>

编译得到 vnode此时为插值语法 bar 添加一个标志 patchFlag表示此为动态语法无论值为多少只要存在 patchFlag 就表示其为动态语法

patchFlag 有四个值

  1. 数字 1代表节点有动态的 textContent
  2. 数字 2代表元素有动态的 class 绑定
  3. 数字 3代表元素有动态的 style 绑定
  4. 数字 4其他
const vnode = {
  tag: "div",
  children: [
    { tag: "div", children: "foo" },
    { tag: "p", children: ctx.bar, patchFlag: 1 },
  ],
};

dynamicChildren 专门存储提取到的动态节点

此时 vnode 可以称为一个 Block

一个 Block 可以递归的收取当前及其所有子孙的所有动态节点

const vnode = {
  ...

  // 将 children 中的动态节点提取到 dynamicChildren 数组中
  dynamicChildren: [
    // p 标签具有 patchFlag 属性因此它是动态节点
    { tag: "p", children: ctx.bar, patchFlag: PatchFlags.TEXT },
  ],
};

此时执行打补丁更新时直接跳过 vnode 的 children只需要更新其中的 dynamicChildren 即可


动态节点与渲染

createVNode 函数内检测存在补丁标志 patchFlags 的子节点并将他们 push 到 currentDynamicChildren 数组内部

注意函数执行的顺序是从内到外故 currentDynamicChildren 最终能获取所有的动态子代节点


patchElement 函数内部
vnode 存在 dynamicChildren 数组直接调用 patchBlockChildren 函数完成更新不理会所有静态节点

使用靶向更新避免 props 大量重更


Block 树

v-if、v-else 前后标签不一致问题

譬如以下代码v-if 和 v-else 所在标签不一致这导致 vnode 不会触发动态更新打补丁出现 bug

<div>
  <section v-if="foo">
    <p>{{ a }}</p>
  </section>
  <div v-else>
    <p>{{ a }}</p>
  </div>
</div>

解决方案
统一 Block
编译器自动识别上下不一致的标签并将其替换为一致的然后再执行打补丁
故经过编译器识别替换重组后结果代码

<div>
  <section v-if="foo">
    <p>{{ a }}</p>
  </section>
  <section v-else>
    <div>
      <p>{{ a }}</p>
    </div>
  </section>
</div>

结构不稳定问题

原文引用所谓结构不稳定从结果上看指的是更新前后一个 block 的 dynamicChildren 数组中收集的动态节点的数量或顺序不一致

只能放弃靶向更新回退到传统虚拟 DOM 的 Diff 手段


静态提升

存在模板

<div>
  <p>static text</p>
  <p>{{ title }}</p>
</div>

我们需要使用“静态提升”的方法来避免同级别标签中因任意一个存在动态节点而导致整体更新的性能消耗

所谓静态提升就是把静态节点提到渲染函数之外渲染函数只能渲染其引用

// 把静态节点提升到渲染函数之外
const hoist1 = createVNode("p", null, "text");
function render() {
  return (
    openBlock(),
    createBlock("div", null, [
      hoist1, // 静态节点引用
      createVNode("p", null, ctx.title, 1 /* TEXT */),
    ])
  );
}

预字符串化

除了静态提升还可使用预字符串化

把静态节点序列化为字符串生成一静态 vnode

const hoistStatic = createStaticVNode('<p></p><p></p><p></p>...20 个...<p></p>')
render() {
  return (openBlock(), createBlock('div', null, [
    hoistStatic
  ]))
}

缓存内联事件处理函数

缓存内联事件处理函数可以避免不必要的更新

譬如会为诸如 @click="" 事件创建内联函数每次都从 cache 数组中获取内联函数避免重新渲染


十五、同构渲染

客户端渲染 CSR

CSR 流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QKjGyiw6-1673567956852)(…/imgs/vue/vuejs_optimize/vp4.png)]

  1. 首先获取今天 HTML 页面此时处于白屏阶段
  2. 浏览器解释 JS 和 CSS并经过 JS 渲染页面呈现出来
  3. AJAX 请求后端数据把数据贴到页面对应位置

CSR 存在白屏问题且 SEO 不稳定

SSR 不存在白屏问题且对 SEO 友好


同构渲染

同构渲染即 CSR + SSR
同构渲染无法提高 可交互时间TTI

基于 vuejs 的同构渲染流程

  1. 服务器返回浏览器 带有初始化数据的 HTML 页面与 SSR 步骤差不多
  2. 浏览器根据初始化界面中的 script 以及 link 标签请求服务器获取资源与 CSR 步骤差不多
  3. JS 加载完毕执行激活

vuejs 激活操作

在 JS 加载完毕后需要将 vuejs 连接到对应的 HTML 页面上此时就需要两步激活

  1. Vue.js 在当前页面已经渲染的 DOM 元素以及 Vue.js 组件所渲染的虚拟 DOM 之间建立联系
  2. Vue.js 从 HTML 页面中提取由服务端序列化后发送过来的数据用以初始化整个 Vue.js 应用程序

客户端激活

组件代码在客户端中执行时不需要再次创建 DOM 元素它只要做以下两件事

  1. 在页面中的 DOM 元素与虚拟节点对象之间建立联系
  2. 为页面中的 DOM 元素添加事件绑定。

从服务端渲染到客户端激活的模拟流程代码

// html 代表由服务端渲染的字符串
const html = renderComponentVNode(compVNode);
// 假设客户端已经拿到了由服务端渲染的字符串
// 获取挂载点
const container = document.querySelector("#app");
// 设置挂载点的 innerHTML模拟由服务端渲染的内容
container.innerHTML = html;
// 接着调用 hydrate 函数完成激活
renderer.hydrate(compVNode, container);

hydrateNode 函数具体实现

递归地激活当前元素的子节点从第一个子节点 el.firstChild 开始递归地调用 hydrateNode 函数完成激活

function hydrateNode(node, vnode) {
  const { type } = vnode;
  // 1. 让 vnode.el 引用真实 DOM
  vnode.el = node;
  // 2. 检查虚拟 DOM 的类型如果是组件则调用 mountComponent 函数完成激活
  if (typeof type === "object") {
    mountComponent(vnode, container, null);
  } else if (typeof type === "string") {
    // 3. 检查真实 DOM 的类型与虚拟 DOM 的类型是否匹配
    if (node.nodeType !== 1) {
      console.error("mismatch");
      console.error("服务端渲染的真实 DOM 节点是", node);
      console.error("客户端渲染的虚拟 DOM 节点是", vnode);
    } else {
      // 4. 如果是普通元素则调用 hydrateElement 完成激活
      hydrateElement(node, vnode);
    }
  }
  // 5. 重要hydrateNode 函数需要返回当前节点的下一个兄弟节点以便继续进行后续的激活操作
  return node.nextSibling;
}

原文由于服务端渲染的页面中已经存在真实 DOM 元素所以当调用 mountComponent 函数进行组件的挂载时无须再次创建真实 DOM 元素


编写同构代码

组件生命周期

组件代码于服务端中运行时不执行挂载即不执行钩子函数 beforeMount 与 mounted

服务端渲染应用程序的快照故在服务端中设置计时器没有意义
可以在 mounted 函数内设置计时器以便在客户端运行


跨平台 API

避免使用平台特有 API譬如 window 和 document

使用 import.meta.env.xxx 这种全局环境变量来避免


单端引入模块

由于第三方模块存在非同构代码导致出现 bug

可以使用条件引用的方式仅在特点环境下加载模板

如下根据全局环境变量判断不属于某个环境后依次引入对应的第三方模块

<script>
  let storage
  if (!import.meta.env.SSR) {
    // 用于客户端
    storage = import('./storage.js')
  } else {
    // 用于服务端
    storage = import('./storage-server.js')
  }
  export default {
    // ...
  }
</script>

避免交叉请求

要为每一个请求创建独立的应用实例可以避免不同请求共用同一个应用实例所导致的状态污染


<ClientOnly> 组件

使用 <ClientOnly> 标签让无法 SSR 的第三方组件得以只能在客户端中运行

<template>
  <ClientOnly>
    <SsrIncompatibleComp />
  </ClientOnly>
</template>

clientonly 的实现也很简单即设置一标记变量 show默认 false当客户端渲染时才为 true使得只能等到 mounted 钩子函数触发后才渲染该插槽内内容


结语

一些重要的转义规则

  1. 对于普通内容应该对文本中的以下字符进行转义
    将字符 & 转义为实体 &amp;
    将字符 < 转义为实体 &lt;
    将字符 > 转义为实体 &gt;
  2. 对于属性值除了上述三个字符应该转义之外还应该转义下面两个字符
    将字符 " 转义为实体 &quot;
    将字符 ’ 转义为实体 &#39;

原文摘抄懒得总结了呜呜呜务端渲染不存在数据变更后的重新渲染所以无须调用 reactive 函数对 data 等数据进行包装也无须使用 shallowReactive 函数对 props 数据进行包装。正因如此我们也无须调用 beforeUpdate 和 updated 钩子


阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: vue