第8期 - 海南椰子鸡

封面图来源于海南万宁神州半岛。在很久之前的团建去的,由于念念不忘那边的椰子鸡后来又去了一次文昌,味道依旧不错,还偷师学会了做椰子鸡。

技术分享-前端框架相关-渲染

渲染原理

引入一个重要的概念 Virtual DOM

Virtual DOM

简单来说,DOM 表示应用程序的 UI。每当应用程序 UI 的状态发生更改时,DOM 都会更新以表示该更改。经常操纵 DOM 会影响性能,使其变慢。性能明显优于真正的 DOM。虚拟 DOM 只是 DOM 的虚拟表示。 每次应用程序的状态发生变化时,都会更新虚拟 DOM 而不是真正的 DOM

将新元素添加到 UI 时,将创建一个表示为树的虚拟 DOM。每个元素都是此树上的一个节点。如果这些元素中的任何一个的状态发生更改,则会创建一个新的虚拟 DOM 树。然后将此树与先前的虚拟 DOM 树进行比较或“区分”。完成此操作后,虚拟 DOM 会计算出对真实 DOM 进行这些更改的最佳方法。这确保了对真实 DOM 的操作最少。 因此,降低了更新真实 DOM 的性能成本。

标记更改的节点,然后计算虚拟 DOM 树的先前版本与当前虚拟 DOM 树之间的差异。整个父子树将重新呈现,将此更新的树批量更新为实际 DOM

React 渲染

render() 函数是创建 React 元素树的入口点。当组件中的状态或属性更新时,render() 将返回不同的 React 元素树。如果在组件中使用 setState(更新数据),React 会立即检测到状态变化并重新渲染组件。 React 遵循批量更新机制来更新真实的 DOM。对实际 DOM 来说,更新是分批发送的,因此更高效的提高性能。重新绘制 UI 是最昂贵的部分,而 React 有效地确保真正的 DOM 只接收批量更新来重新绘制 UI。

使用高效的 diff 算法来比较虚拟 DOM 的版本 确保将批量更新发送到实际 DOM,以重新绘制或重新呈现 UI

Vue 渲染机制

浏览器的DOM更新,是昂贵的,频繁进行DOM更新性能体验较差。 在 Vue 中,使用 Virtual DOM 。即 用一个原生的 JS 对象去描述一个 DOM 节点(VNode),VNode 是对真实 DOM 的一种抽象描述,通过定义标签名、数据、子节点、键值等关键属性来映射到真实 DOM 的渲染。

Virtual DOM 将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步 挂载:运行时渲染器将会遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树 更新:对于两个不同的虚拟 DOM 树,渲染器进行遍历找出它们之间的区别,并变化应用到真实的 DOM 上

编译-> 挂载-> 更新 Vue 模板被编译为渲染函数用来返回虚拟 DOM 树,构建及运行时编译完成 运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。追踪其中所用到的所有响应式依赖 当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。

流程:模板-渲染函数-组件状态(数据)-虚拟dom-真实dom Vue 的编译器会将模板编译成渲染函数,渲染函数生成一个虚拟 DOM 树(Virtual DOM)当数据发生变化时组件状态驱动虚拟 DOM,虚拟 DOM 与实际 DOM 进行高效更新

差异性

Vue 和 React 都使用虚拟 DOM 来提高性能,并通过响应式系统来实现数据和视图的绑定。

DOM模板:Vue 使用模板(Template)来定义组件的结构和布局,模板通常以HTML字符串的形式定义。Vue 的编译器将模板编译成渲染函数,最终生成虚拟 DOM。React 使用 JSX(JavaScript XML)来描述组件的结构。JSX 是一种在 JavaScript 中嵌入XML标记的语法,更接近于编写原生JavaScript代码。

渲染函数:Vue 的渲染函数由 Vue 的编译器生成,React 中组件的render方法需要自定义,返回虚拟 DOM 树。开发者需要手动编写虚拟 DOM 结构,通常是使用 JSX

响应式:Vue 的响应式系统是内置的,它允许你在数据发生变化时自动触发组件的重新渲染。Vue会在数据变化时更新虚拟 DOM。React 的响应式系统需要使用setState来通知组件状态变化,然后组件通过render方法生成新的虚拟 DOM。React使用一种”单向数据流”的模式,组件不会直接修改状态,而是通过setState方法进行更新。

diff算法: vue 通过对比两个新旧dom树。当组件的状态变化时,Vue 会生成一个新的虚拟 DOM 树。然后,Vue 的差异算法会比较新旧虚拟 DOM 树。Vue 的差异算法采用一种双端比较算法,它会同时从新旧虚拟 DOM 树的两端开始比较节点,以找到最小的变化。它会尽量避免深度优先遍历整个树,从而减少比较的次数。

https://juejin.cn/post/6844903607913938951

react 使用逐层遍历dom树对比。当组件的状态变化时,React 会生成一个新的虚拟 DOM 树。然后,React 的差异算法会比较新旧虚拟 DOM 树。从根节点开始逐层比较新旧虚拟 DOM 树,以找到需要更新的节点。对同一层级的子节点,开发者可以通过 key 来确定哪些子元素可以在不同渲染中保持稳定。两个不同类型的组件会产生两棵不同的树形结构。

https://maimai.cn/article/detail?fid=1756966756&efid=O4drb3felJHy47Q9yh3XKA

------分割线---------2024/0126更新

深入Vue渲染原理

对于一个vue组件,当组件创建或依赖的数据变化 _render -> _update -> _patch 会先进行 _render() 虚拟dom的生成,通过当前组件的 this._vnode 属性,拿到旧的虚拟 DOM 树,再给组件的 this._vnode 属性重新赋值为新组件节点, 对比新旧2个节点树,不存在说明是首次加载组件,直接遍历新树,为每个节点生成真实的DOM,并挂载到每个节点的 elm 属性上

存在,说明之前已经渲染过组件,则通过 patch 函数对新旧树对比,完成对所有真实 DOM 的最小化处理 对于虚拟DOM树的更新,直接将新节点赋值给 this._vnode, 正常情况是生成一个真实DOM,但是为了节能和效率考虑 需要最小化变更真实dom渲染。修改真实的 DOM,并和新的 VNode tree 对应上

根节点比较 如果新旧虚拟节点的标签(tag)类型、key 值均相同(input 元素还需要考虑 type 属性) 将旧节点的真实 DOM 赋值到新节点:newVNode.elm = oldVNode.elm 再对比新旧节点的属性,有变化的更新到真实 DOM 中,再继续进行子节点的对比更新 不同则 新节点递归的新建元素,旧节点直接销毁元素

子节点比较(进行diff) 创建/删除元素 < 移动元素 < 修改元素属性 原则:尽可能少的变动元素

虚拟DOM的优势

虚拟 DOM 本质上是 JavaScript 对象,而操作 DOM 慢,js 运行效率高。以 JS 的计算性能来换取操作真实 DOM 所消耗的性能。当数据发生变化时,我们对比变化前后的虚拟DOM节点,通过DOM-Diff算法计算出需要更新的地方,然后去更新需要更新的视图。

对于web浏览器、小程序、原生系统,真实dom因为依赖的渲染平台不同,渲染机制也不同。虚拟dom抽象原本的真实dom渲染过程,通过 js 对象,模拟真实dom 做到平台通用化。并以最小的代价更新变化的视图,只更新变化部分的视图,主要目的是在进行DOM操作时减少实际DOM操作的次数,从而提高性能。

Virtual DOM 的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。

Patch 补丁

_update 会将新旧两个 VNode 进行 patch 过程,得出两个 VNode 最小差异,然后将这些差异渲染到视图上

创建节点

新的 VNode 中有而旧的 oldVNode 中没有,就在旧的 oldVNode 中创建

// 源码位置: /src/core/vdom/patch.js
function createElm(vnode, parentElm, refElm) {
  const data = vnode.data;
  const children = vnode.children;
  const tag = vnode.tag;
  if (isDef(tag)) {
    vnode.elm = nodeOps.createElement(tag, vnode); // 创建元素节点
    createChildren(vnode, children, insertedVnodeQueue); // 创建元素节点的子节点
    insert(parentElm, vnode.elm, refElm); // 插入到DOM中
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text); // 创建注释节点
    insert(parentElm, vnode.elm, refElm); // 插入到DOM中
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text); // 创建文本节点
    insert(parentElm, vnode.elm, refElm); // 插入到DOM中
  }
}

在DOM(文档对象模型)中,只有三种基本类型的节点可以被创建并插入到DOM中,元素节点、文本节点、注释节点 主要是为了描述和组织文档的结构和内容。

删除节点

新的 VNode 中没有而旧的 oldVNode 中有,就从旧的 oldVNode 中删除

function removeNode(el) {
  const parent = nodeOps.parentNode(el); // 获取父节点
  if (isDef(parent)) {
    nodeOps.removeChild(parent, el); // 调用父节点的removeChild方法
  }
}

更新节点

新的 VNode 和旧的 oldVNode 中都有,就以新的 VNode 为准,更新旧的 oldVNode。 patch 过程中,如果两个 VNode 被认为是同个 VNode,则会进行深度比较,得出最小差异,否则直接删除旧有 DOM 节点,创建新的 DOM 节点。

// 更新节点
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
 // VNode 与 oldVnode 新旧节点相同,不更新,直接结束;
 if (oldVnode === vnode) {
   return;
 }
 const elm = (vnode.elm = oldVnode.elm);

 /** 如果新旧 VNode 都是静态(isStatic)的,同时它们的 key 相同(代表同一节点),并且新的 VNode 是 clone 或者是标记了 once(标记 v-once 属性,只渲染一次),只需要替换 componentInstance **/
 if (
   isTrue(vnode.isStatic) &&
   isTrue(oldVnode.isStatic) &&
   vnode.key === oldVnode.key &&
   (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
 ) {
   vnode.componentInstance = oldVnode.componentInstance;
   return;
 }

 const oldCh = oldVnode.children;
 const ch = vnode.children;
 // 判断 VNode 是否是文本节点,是否有 text 属性, 若没有则需比较子节点
 if (isUndef(vnode.text)) {
   // 如果 VNode 不是文本节点,且存在子节点
   // vnode的子节点与oldVnode的子节点是否都存在?
   if (isDef(oldCh) && isDef(ch)) {
     // 若都存在,判断子节点是否相同,不同则更新子节点
     if (oldCh !== ch)
     // 如果 Vnode 和 oldVnode 均有子节点,则执行 updateChildren 对子节点进行 diff 操作
       updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
   }
   else if (isDef(ch)) {
     /**
      * 判断oldVnode是否有文本?
      * 若没有,则把vnode的子节点添加到真实DOM中
      * 若有,则清空Dom中的文本,再把vnode的子节点添加到真实DOM中
      */
      // 如果 Vnode 有子节点,oldVnode 没有子节点但有文本节点,则将 oldVnode 的文本节点清空,然后插入 Vnode 的子节点;否则直接新增
     if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
     addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
   }
   // 如果 VNode 没有子节点,oldVnode 有子节点,则删除 el 的所有子节点,直接清空
   // 若只有oldnode的子节点存在
   else if (isDef(oldCh)) {
     // 清空DOM中的子节点
     removeVnodes(elm, oldCh, 0, oldCh.length - 1);
   }
   // 若vnode和oldnode都没有子节点,但是oldnode中有文本
   // 如果 VNode 与 oldVNode 都没有子节点,则清空 oldVnode 的文本节点即可
   else if (isDef(oldVnode.text)) {
     // 清空oldnode文本
     nodeOps.setTextContent(elm, "");
   }
   // 上面两个判断一句话概括就是,如果vnode中既没有text,也没有子节点,那么对应的oldnode中有什么就清空什么
 }
 // 若有,vnode的text属性与oldVnode的text属性是否相同?
 // 如果 VNode 是文本节点,也就不存在子节点,如果 oldeVnode 和 Vnode 都有文本节点且不相等,那么会将 oldVnode 的文本节点更新为 Vnode 的文本节点
 else if (oldVnode.text !== vnode.text) {
   // 若不相同:则用vnode的text替换真实DOM的文本
   nodeOps.setTextContent(elm, vnode.text);
 }
}

vnode克隆:列表项中的项进行重新排序或者某个组件的状态发生变化时,新的VNode和旧的VNode的节点类型(element type)必须相同,即表示相同类型的DOM元素或组件。新的VNode和旧的VNode的子节点深度(nested depth)也需要相同。

vue中 key 的作用

key 主要用在 Vue 的虚拟 DOM 的 diff 算法中,是 vnode 的唯一标记,如果设置了 key,就会用 key 再进行比较。通过这个 key,我们的 diff 操作可以更准确、更快速的达到复用节点,更新视图的目的。

  • 如遇到 完整地触发组件的生命周期钩子 或 触发过渡 的场景,可以用于强制替换元素/组件而不是重复使用它
  • 更准确,基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素,通过移动节点位置,避免原地复用或就地修改来达到复用节点,更新视图的目的
  • 利用 key 的唯一性生成 map 对象(oldKeyToIdx 哈希表)来获取对应节点,比遍历方式更快
  • 如果不用 key,Vue 会采用就地复地原则:最小化 element 的移动,并且会尝试尽最大程度在同适当的地方对相同类型的 element,做 patch 或者 复用
  • 如果使用了 key,Vue 会根据 key 的顺序记录 element,曾经拥有了 key 的 element 如果不再出现的话,会被直接 remove 或者destoryed
  • 当拥有新值的 rerender 作为 key 时,拥有了新 key 的组件出现了,那么旧 key 的组件会被移除,新 key 组件触发渲染

节点复用:如果知道了映射关系,我们就很容易判断新节点是否可复用