Vue.js3.2的vnode部分优化升级使用示例详解

目录
  • 背景
  • 什么是 vnode
    • 普通元素 vnode
    • 组件 vnode
    • vnode 的优势
  • 如何创建 vnode
  • 创建 vnode 过程的优化
  • 总结

背景

上一篇文章,分析了 Vue.js 3.2 关于响应式部分的优化,此外,在这次优化升级中,还有一个运行时的优化:

~200% faster creation of plain element VNodes

即针对普通元素类型 vnode 的创建,提升了约 200% 的性能。这也是一个非常伟大的优化,是 Vue 的官方核心开发者 HcySunYang 实现的,可以参考这个 PR

那么具体是怎么做的呢,在分析实现前,我想先带你了解一些 vnode 的背景知识。

什么是 vnode

vnode 本质上是用来描述 DOM 的 JavaScript 对象,它在 Vue.js 中可以描述不同类型的节点,比如普通元素节点、组件节点等。

普通元素 vnode

什么是普通元素节点呢?举个例子,在 HTML 中我们使用 <button> 标签来写一个按钮:

<button class="btn" style="width:100px;height:50px">click me</button>

我们可以用 vnode 这样表示 <button> 标签:

const vnode = {
  type: 'button',
  props: {
    'class': 'btn',
    style: {
      width: '100px',
      height: '50px'
    }
  },
  children: 'click me'
}

其中,type 属性表示 DOM 的标签类型;props 属性表示 DOM 的一些附加信息,比如 styleclass 等;children 属性表示 DOM 的子节点,在该示例中它是一个简单的文本字符串,当然,children 也可以是一个 vnode 数组。

组件 vnode

vnode 除了可以像上面那样用于描述一个真实的 DOM,也可以用来描述组件。举个例子,我们在模板中引入一个组件标签 <custom-component>

<custom-component msg="test"></custom-component>

我们可以用 vnode 这样表示 <custom-component> 组件标签:

const CustomComponent = {
  // 在这里定义组件对象
}
const vnode = {
  type: CustomComponent,
  props: {
    msg: 'test'
  }
}

组件 vnode 其实是对抽象事物的描述,这是因为我们并不会在页面上真正渲染一个 <custom-component> 标签,而最终会渲染组件内部定义的 HTML 标签。

除了上述两种 vnode 类型外,还有纯文本 vnode、注释 vnode 等等。

另外,Vue.js 3.x 内部还针对 vnodetype,做了更详尽的分类,包括 SuspenseTeleport 等,并且把 vnode 的类型信息做了编码,以便在后面 vnode 的挂载阶段,可以根据不同的类型执行相应的处理逻辑:

// runtime-core/src/vnode.ts
const shapeFlag = isString(type)
  ? 1 /* ELEMENT */
  : isSuspense(type)
    ? 128 /* SUSPENSE */
    : isTeleport(type)
      ? 64 /* TELEPORT */
      : isObject(type)
        ? 4 /* STATEFUL_COMPONENT */
        : isFunction(type)
          ? 2 /* FUNCTIONAL_COMPONENT */
          : 0;

vnode 的优势

知道什么是 vnode 后,你可能会好奇,那么 vnode 有什么优势呢?为什么一定要设计 vnode 这样的数据结构呢?

首先是抽象,引入 vnode,可以把渲染过程抽象化,从而使得组件的抽象能力也得到提升。

其次是跨平台,因为 patch vnode 的过程不同平台可以有自己的实现,基于 vnode 再做服务端渲染、weex 平台、小程序平台的渲染都变得容易了很多。

不过这里要特别注意,在浏览器端使用 vnode 并不意味着不用操作 DOM 了,很多人会误以为 vnode 的性能一定比手动操作原生 DOM 好,这个其实是不一定的。

因为这种基于 vnode 实现的 MVVM 框架,在每次组件渲染生成 vnode 的过程中,会有一定的 JavaScript 耗时,尤其是是大组件。举个例子,一个 1000 * 10 的 Table 组件,组件渲染生成 vnode 的过程会遍历 1000 * 10 次去创建内部 cell vnode,整个耗时就会变得比较长,再加上挂载 vnode 生成 DOM 的过程也会有一定的耗时,当我们去更新组件的时候,用户会感觉到明显的卡顿。

虽然 diff 算法在减少 DOM 操作方面足够优秀,但最终还是免不了操作 DOM,所以说性能并不是 vnode 的优势。

如何创建 vnode

通常我们开发组件都是编写组件的模板,并不会手写 vnode,那么 vnode 是如何创建的呢?

我们知道,组件模板经过编译,会生成对应的 render 函数,在 render 函数内部,会执行 createVNode 函数创建 vnode 对象,我们来看一下 Vue.js 3.2 之前它的实现:

function createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) {
  if (!type || type === NULL_DYNAMIC_COMPONENT) {
    if ((process.env.NODE_ENV !== 'production') && !type) {
      warn(`Invalid vnode type when creating vnode: ${type}.`)
    }
    type = Comment
  }
  if (isVNode(type)) {
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    return cloned
  }
  // 类组件的标准化
  if (isClassComponent(type)) {
    type = type.__vccOpts
  }
  // class 和 style 标准化.
  if (props) {
    if (isProxy(props) || InternalObjectKey in props) {
      props = extend({}, props)
    }
    let { class: klass, style } = props
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass)
    }
    if (isObject(style)) {
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }
  // 根据 vnode 的类型编码
  const shapeFlag = isString(type)
    ? 1 /* ELEMENT */
    : isSuspense(type)
      ? 128 /* SUSPENSE */
      : isTeleport(type)
        ? 64 /* TELEPORT */
        : isObject(type)
          ? 4 /* STATEFUL_COMPONENT */
          : isFunction(type)
            ? 2 /* FUNCTIONAL_COMPONENT */
            : 0
  if ((process.env.NODE_ENV !== 'production') && shapeFlag & 4 /* STATEFUL_COMPONENT */ && isProxy(type)) {
    type = toRaw(type)
    warn(`Vue received a Component which was made a reactive object. This can ` +
      `lead to unnecessary performance overhead, and should be avoided by ` +
      `marking the component with `markRaw` or using `shallowRef` ` +
      `instead of `ref`.`, `\nComponent that was made reactive: `, type)
  }
  const vnode = {
    __v_isVNode: true,
    __v_skip: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIds: null,
    children: null,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
  }
  if ((process.env.NODE_ENV !== 'production') && vnode.key !== vnode.key) {
    warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
  }
  normalizeChildren(vnode, children)
  // 标准化 suspense 子节点
  if (shapeFlag & 128 /* SUSPENSE */) {
    type.normalize(vnode)
  }
  if (isBlockTreeEnabled > 0 &&
    !isBlockNode &&
    currentBlock &&
    (patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) &&
    patchFlag !== 32 /* HYDRATE_EVENTS */) {
    currentBlock.push(vnode)
  }
  return vnode
}

可以看到,创建 vnode 的过程做了很多事情,其中有很多判断的逻辑,比如判断 type 是否为空:

if (!type || type === NULL_DYNAMIC_COMPONENT) {
  if ((process.env.NODE_ENV !== 'production') && !type) {
    warn(`Invalid vnode type when creating vnode: ${type}.`)
  }
  type = Comment
}

判断 type 是不是一个 vnode 节点:

if (isVNode(type)) {
  const cloned = cloneVNode(type, props, true /* mergeRef: true */)
  if (children) {
    normalizeChildren(cloned, children)
  }
  return cloned
}

判断 type 是不是一个 class 类型的组件:

if (isClassComponent(type)) {
    type = type.__vccOpts
  }

除此之外,还会对属性中的 styleclass 执行标准化,其中也会有一些判断逻辑:

if (props) {
  if (isProxy(props) || InternalObjectKey in props) {
    props = extend({}, props)
  }
  let { class: klass, style } = props
  if (klass && !isString(klass)) {
    props.class = normalizeClass(klass)
  }
  if (isObject(style)) {
    if (isProxy(style) && !isArray(style)) {
      style = extend({}, style)
    }
    props.style = normalizeStyle(style)
  }
}

接下来还会根据 vnode 的类型编码:

const shapeFlag = isString(type)
  ? 1 /* ELEMENT */
  : isSuspense(type)
    ? 128 /* SUSPENSE */
    : isTeleport(type)
      ? 64 /* TELEPORT */
      : isObject(type)
        ? 4 /* STATEFUL_COMPONENT */
        : isFunction(type)
          ? 2 /* FUNCTIONAL_COMPONENT */
          : 0

然后就是创建 vnode 对象,创建完后还会执行 normalizeChildren 去标准化子节点,这个过程也会有一系列的判断逻辑。

创建 vnode 过程的优化

仔细想想,vnode 本质上就是一个 JavaScript 对象,之所以在创建过程中做很多判断,是因为要处理各种各样的情况。然而对于普通元素 vnode 而言,完全不需要这么多的判断逻辑,因此对于普通元素 vnode,使用 createVNode 函数创建就是一种浪费。

顺着这个思路,就可以在模板编译阶段,针对普通元素节点,使用新的函数来创建 vnode,Vue.js 3.2 就是这么做的,举个例子:

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

借助于模板导出工具,可以看到它编译后的 render 函数:

import { createElementVNode as _createElementVNode, resolveComponent as _resolveComponent, createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = { class: "home" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("img", {
  alt: "Vue logo",
  src: "../assets/logo.png"
}, null, -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_HelloWorld = _resolveComponent("HelloWorld")
  return (_openBlock(), _createElementBlock("template", null, [
    _createElementVNode("div", _hoisted_1, [
      _hoisted_2,
      _createVNode(_component_HelloWorld, { msg: "Welcome to Your Vue.js App" })
    ])
  ]))
}

针对于 div 节点,这里使用了 createElementVNode 方法而并非 createVNode 方法,而 createElementVNode 在内部是 createBaseVNode 的别名,来看它的实现:

function createBaseVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, shapeFlag = type === Fragment ? 0 : 1 /* ELEMENT */, isBlockNode = false, needFullChildrenNormalization = false) {
  const vnode = {
    __v_isVNode: true,
    __v_skip: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIds: null,
    children,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
  }
  if (needFullChildrenNormalization) {
    normalizeChildren(vnode, children)
    if (shapeFlag & 128 /* SUSPENSE */) {
      type.normalize(vnode)
    }
  }
  else if (children) {
    vnode.shapeFlag |= isString(children)
      ? 8 /* TEXT_CHILDREN */
      : 16 /* ARRAY_CHILDREN */
  }
  if ((process.env.NODE_ENV !== 'production') && vnode.key !== vnode.key) {
    warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
  }
  if (isBlockTreeEnabled > 0 &&
    !isBlockNode &&
    currentBlock &&
    (vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) &&
    vnode.patchFlag !== 32 /* HYDRATE_EVENTS */) {
    currentBlock.push(vnode)
  }
  return vnode
}

可以看到,createBaseVNode 内部仅仅是创建了 vnode 对象,然后做了一些 block 逻辑的处理。相比于之前的 createVNode 的实现,createBaseVNode 少执行了很多判断逻辑,自然性能就获得了提升。

createVNode 的实现,是基于 createBaseVNode 做的一层封装:

function createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) {
  if (!type || type === NULL_DYNAMIC_COMPONENT) {
    if ((process.env.NODE_ENV !== 'production') && !type) {
      warn(`Invalid vnode type when creating vnode: ${type}.`)
    }
    type = Comment$1
  }
  if (isVNode(type)) {
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    return cloned
  }
  if (isClassComponent(type)) {
    type = type.__vccOpts
  }
  if (props) {
    props = guardReactiveProps(props)
    let { class: klass, style } = props
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass)
    }
    if (isObject$1(style)) {
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }
  const shapeFlag = isString(type)
    ? 1 /* ELEMENT */
    : isSuspense(type)
      ? 128 /* SUSPENSE */
      : isTeleport(type)
        ? 64 /* TELEPORT */
        : isObject$1(type)
          ? 4 /* STATEFUL_COMPONENT */
          : isFunction$1(type)
            ? 2 /* FUNCTIONAL_COMPONENT */
            : 0
  if ((process.env.NODE_ENV !== 'production') && shapeFlag & 4 /* STATEFUL_COMPONENT */ && isProxy(type)) {
    type = toRaw(type)
    warn(`Vue received a Component which was made a reactive object. This can ` +
      `lead to unnecessary performance overhead, and should be avoided by ` +
      `marking the component with `markRaw` or using `shallowRef` ` +
      `instead of `ref`.`, `\nComponent that was made reactive: `, type)
  }
  return createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true)
}

createVNode 的实现还是和之前类似,需要执行一堆判断逻辑,最终执行 createBaseVNode 函数创建 vnode,注意这里 createBaseVNode 函数最后一个参数传 true,也就是 needFullChildrenNormalizationtrue,那么在 createBaseVNode 的内部,还需要多执行 normalizeChildren 的逻辑。

组件 vnode 还是通过 createVNode 函数来创建。

总结

虽然看上去只是少执行了几行代码,但由于大部分页面都是由很多普通 DOM 元素构成,创建普通元素 vnode 过程的优化,对整体页面的渲染和更新都会有很大的性能提升。

由于存在模板编译的过程,Vue.js 可以利用编译 + 运行时优化,来实现整体的性能优化。比如 Block Tree 的设计,就优化了 diff 过程的性能。

其实对一个框架越了解,你就会越有敬畏之情,Vue.js 在编译、运行时的实现都下了非常大的功夫,处理的细节很多,因此代码的体积也难免变大。而且在框架已经足够成熟,有大量用户使用的背景下还能从内部做这么多的性能优化,并且保证没有 regression bug,实属不易。

开源作品的用户越多,受到的挑战也会越大,需要考虑的细节就会越多,如果一个开源作品都没啥人用,玩具级别,就真的别来碰瓷 Vue 了,根本不是一个段位的。

以上就是Vue.js3.2的vnode部分优化升级使用示例详解的详细内容,更多关于Vue.js vnode优化升级的资料请关注我们其它相关文章!

(0)

相关推荐

  • Vue3.0的优化总结

    1.源码优化: a.使用monorepo来管理源码 Vue.js 2.x 的源码托管在 src 目录,然后依据功能拆分出了 compiler(模板编译的相关代码).core(与平台无关的通用运行时代码).platforms(平台专有代码).server(服务端渲染的相关代码).sfc(.vue 单文件解析相关代码).shared(共享工具代码)等目录. Vue.js 3.0,整个源码是通过 monorepo 的方式维护的,根据功能将不同的模块拆分到 packages 目录下面不同的子目录中,每个

  • Vue2.x 项目性能优化之代码优化的实现

    众所周知,Vue项目采用了数据双向绑定和虚拟DOM基础,在数据驱动代替DOM频繁渲染已经算是非常高效了,对开发者而言已经非常优化了,那为什么还会有Vue性能优化这一说呢? 因为目前Vue 2.x使用了webpack等第三方打包构建工具,并且支持其他第三方的插件,我们在项目中使用这些工具时可能不同的操作在运行或打包效率上会有不同的效果,下面就来详细说明优化的方向. 1 v-if 和 v-show 的使用 v-if 为false的时候不会渲染DOM到视图,为true的时候才会渲染到视图: v-sho

  • Vue.js之VNode的使用

    什么是VNode 在vue.js中存在一个VNode类,使用它可以实例化不同类型的vnode实例,而不同类型的vnode实例各自表示不同类型的DOM元素. 例如,DOM元素有元素节点,文本节点,注释节点等,vnode实例也会对应着有元素节点和文本节点和注释节点. VNode类代码如下: export default class VNode { constructor(tag, data, children, text, elm, context, componentOptions, asyncF

  • Vue性能优化的方法

    今天来谈一谈Vue中一些性能优化的问题,仅仅是个人使用中的一些小心得,来,今天我一句废话不多说,直接上内容好吧 1.v-if和v-show的使用, 我们都知道这两个都可以控制显隐,那我们用哪个呢,个人觉得要从两个方面入手来确定使用哪个, 1.权限的问题,只要涉及到权限相关的展示用v-if比较好 2.切换地频率,如果频繁的切换我们用v-show,不频繁的切换用v-if 其实两者各有优缺,就看你是怎么选择了,用v-if能减少页面中的DOM总数,加快渲染的速度,而且我们要清楚一个事情 v-if是'真正

  • Vue.js3.2的vnode部分优化升级使用示例详解

    目录 背景 什么是 vnode 普通元素 vnode 组件 vnode vnode 的优势 如何创建 vnode 创建 vnode 过程的优化 总结 背景 上一篇文章,分析了 Vue.js 3.2 关于响应式部分的优化,此外,在这次优化升级中,还有一个运行时的优化: ~200% faster creation of plain element VNodes 即针对普通元素类型 vnode 的创建,提升了约 200% 的性能.这也是一个非常伟大的优化,是 Vue 的官方核心开发者 HcySunYa

  • Vue实现高德坐标转GPS坐标功能的示例详解

    首先介绍一下常见的几种地图的坐标类型: WGS-84:这是一个国际标准,也就是GPS坐标(Google Earth.或者GPS模块采集的都是这个类型). GCJ-02:中国坐标偏移标准,像是Google Map.高德.腾讯地图都是采用这种坐标展示. BD-09:百度坐标偏移标准,百度地图专用的便宜标准. 所以说这篇博文主要是实现GCJ-02坐标转换成WGS-84坐标. 什么时候会用到需要解决坐标转换的问题呢?起因是一个demo,它使用GPS模块采集经纬度数据,然后使用高德地图进行转换,是的,高德

  • vue实现鼠标滑动预览视频封面组件示例详解

    目录 组件效果 组件设计 1.视频截取关键帧 2.鼠标移入封面时显示对应关键帧 3.视频和封面的状态切换 功能实现 1.视频截取关键帧图片列表 1.1 截取指定帧 1.2 截取stepNums张关键帧图片 2.鼠标移入封面时显示对应关键帧 2.1 鼠标移动事件监听 2.2 鼠标移出事件监听 3.视频和封面的状态切换 3.1 播放视频 3.2 视频暂停 组件使用 组件库引用 组件效果 https://www.jb51.net/Special/926.htm 组件设计 我们首先应该要对组件进行一个简

  • Android性能优化大图治理示例详解

    目录 引言 1 自定义大图View 1.1 准备工作 1.2 图片宽高适配 1.3 BitmapRegionDecoder 2 大图View的手势事件处理 2.1 GestureDetector 2.2 双击放大效果处理 2.3 手指放大效果处理 引言 在实际的Android项目开发中,图片是必不可少的元素,几乎所有的界面都是由图片构成的:像列表页.查看大图页等,都是需要展示图片,而且这两者是有共同点的,列表展示的Item数量多,如果全部加载进来势必会造成OOM,因此列表页通常采用分页加载,加上

  • vue实现前端展示后端实时日志带颜色示例详解

    目录 vue实现前端展示后端带颜色的日志 需求 操作 采用innerHTML例子 需求: 解决 效果 vue实现前端展示后端带颜色的日志 需求 通过loki获取项目产生的日志,并且在前端显示出来,一开始在没有经过处理的数据会显示一些乱码,并没有将字符转换 经过一番查询后,发现可以使用ansi_up来对日志进行操作颜色代码进行转化. 操作 ansi_up 能够装换颜色代码 GitHub地址 https://github.com/drudru/ansi_up 安装 npm install ansi_

  • java接口性能从20s优化到500ms示例详解

    目录 前言 1. 案发现场 2. 现状 3. 第一次优化 4. 第二次优化 5. 第三次优化 5.1 前端做分页 5.2 分批调用接口 前言 接口性能问题,对于从事后端开发的同学来说,是一个绕不开的话题.想要优化一个接口的性能,需要从多个方面着手. 其实,我之前也写过一篇接口性能优化相关的文章<java接口性能优化小技巧>,发表之后在全网广受好评,感兴趣的小伙们可以仔细看看. 本文将会接着接口性能优化这个话题,从实战的角度出发,聊聊我是如何优化一个慢查询接口的. 上周我优化了一下线上的批量评分

  • vue+webpack实现异步加载三种用法示例详解

    1.第一例 const Home = resolve => { import("@/components/home/home.vue").then( module => { resolve(module) } } 注:(上面import的时候可以不写后缀) export default [{ path: '/home', name:'home', component: Home, meta: { requireAuth: true, // 添加该属性可以判断出该页面是否需要

  • Vue编程三部曲之将template编译成AST示例详解

    目录 前言 编译准备 源码编译链式调用 compileToFunctions parse 解析 template 标签匹配相关的正则 stack advance while 解析开始标签 解析结束标签 当前 template < 不在第一个字符串 处理 stack 栈中剩余未处理的标签 生成 AST start 钩子函数 end 钩子函数 为什么回退? 解析 <p> 解析 </p> chars 钩子函数 commit 钩子函数 番外(可跳过) createASTElement

  • Android如何自定义升级对话框示例详解

    前言 本文主要给大家介绍了关于Android自定义升级对话框的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 实现的效果如下所示 其实这也只是一个DialogFragment 而已,重点只是在于界面的设计 想要使用做出这样一个DialogFragment ,需要自定义一个View,然后将该View传入到该Dialog中 先定义布局,一个TextView用于标题,一个TextView用于升级内容阐述,一个ImageView,一个确认升级的按钮 <?xml version

  • vue.js中methods watch和computed的区别示例详解

    目录 前言 介绍 一.作用机制上 二.从性质上 三.watch和computed的对比 四.methods不处理数据逻辑关系,只提供可调用的函数 五.从功能的互补上看待methods,watch和computed的关系 六.利用computed处理watch在特定情况下代码冗余的现象,简化代码 总结 computed watch 前言 这篇文章主要简述vue中的watch和computer区别,还有methods 首先,先说一下这几个不同在哪里,那当然是长得不一样啦~~~, 哈哈哈哈哈不开玩笑了

随机推荐