Vue编程三部曲之模型树优化示例

目录
  • 前言
  • 为什么要做优化?
  • optimize
  • isStaticKey
  • isPlatformReservedTag
    • HTML 保留标签
    • SVG 保留标签
  • 标记静态节点
  • 判断节点状态并标记
  • 基础元素节点的处理
  • 标记静态根
  • 什么节点会成为静态根?
  • 为什么子节点不能仅为一个文本节点?
  • 标记静态节点和静态根节点有什么区别?
  • 总结

前言

对编译过程的了解会让我们对 Vue 的指令、内置组件等有更好的理解。不过由于编译的过程是一个相对复杂的过程,我们只要求理解整体的流程、输入和输出即可,对于细节我们不必抠太细。由于篇幅较长,这里会用三篇文章来讲 Vue 的编译。这是第二篇,模型树优化。

在上一篇文章中,我们分析了 Vue 编译三部曲的第一步,「如何将 template 编译成 AST ?」

我们简单回顾一下,parse 的目的是将开发者写的 template 模板字符串转换成抽象语法树 AST ,AST 就这里来说就是一个树状结构的 JavaScript 对象,描述了这个模板,这个对象包含了每一个元素的上下文关系。那么整个 parse 的过程是利用很多正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。

当我们的 template 被转换为 AST 之后,接下来我们需要对这棵 AST 语法树做优化。

为什么要做优化?

在源码的注释中找到了下面这段话:

Goal of the optimizer: walk the generated template AST tree and detect sub-trees that are purely static, i.e. parts of the DOM that never needs to change. Once we detect these sub-trees, we can:

Hoist them into constants, so that we no longer need to create fresh nodes for them on each re-render;

Completely skip them in the patching process.

简单理解就是:

  • 永远不需要变化的 DOM 就是静态的。
  • 重新渲染时,作为常量,无需创建新节点;

因为我们知道 Vue 是一个数据驱动视图的响应式框架,但是在开发者书写的 template 中,也不是所有的数据都是响应式的,有很多的数据在首屏渲染完之后就永远不在变化,数据不在变化也就意味着 DOM 不在变化,所以在后续的更新过程进行 patch时完全可以直接跳过他们的比对,从而来提升效率。

接下来我们开始 optimize 源码之旅!看看源码中是如何去优化模型树的?

optimize

template 在经过解析之后,就会进行优化操作。首先这里有一个小逻辑,会判断是否需要进行优化?只有当options.optimize !== false时才会进行优化。

这里抛出几个小问题?options.optimize为什么需要进行这样的判断了?并且如何能关闭模型树优化的操作了?什么情况下会关闭模型树的优化?

var ast = parse(template.trim(), options);
if (options.optimize !== false) {
  optimize(ast, options);
}

在往下,进入到optimize函数,代码很清楚,优化主要做两件事情:

  • markStatic$1(root) 标记静态节点
  • markStaticRoots(root, false) 标记静态根
function optimize (root, options) {
  if (!root) { return }
  isStaticKey = genStaticKeysCached(options.staticKeys || '');
  isPlatformReservedTag = options.isReservedTag || no;
  // 第一步:标记所有静态节点。
  markStatic$1(root);
  // 第二步:标记静态根
  markStaticRoots(root, false);
}

在进行优化操作之前会有两个变量的赋值。

isStaticKey

获取 genStaticKeysCached 函数返回值, 获取 makeMap 函数返回值引用 。

isStaticKey = genStaticKeysCached(options.staticKeys || '');

这里简单了解一下涉及到的 makeMap 函数:

  • makeMap 函数首先根据一个字符串生成一个 map,然后根据该 map 产生一个新函数,新函数接收一个字符串参数作为 key,如果这个 key 在 map 中则返回 true,否则返回 undefined。
  • str 一个以逗号分隔的字符串 、expectsLowerCase 是否小写
  • makeMap 函数返回值是一个根据生成的 map 产生的函数
function makeMap(str, expectsLowerCase) {
    var map = Object.create(null);
    var list = str.split(',');
    for (var i = 0; i < list.length; i++) {
            map[list[i]] = true;
    }
    return expectsLowerCase ?
        function(val) {
           return map[val.toLowerCase()];
        } :
        function(val) {
           return map[val];
        }
}
function genStaticKeys$1 (keys) {
  return makeMap(
    'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
    (keys ? ',' + keys : '')
  )
}
function cached (fn) {
  var cache = Object.create(null);
  return (function cachedFn (str) {
    var hit = cache[str];
    return hit || (cache[str] = fn(str))
  })
}
var genStaticKeysCached = cached(genStaticKeys$1);

这里聊一个题外话,如果你认真看上面这段代码,你会发现,这里大量的使用了闭包,保护和保存数据。这也告诉我们在叼的框架,其实底层也是简单易懂的一些基础思想。

isStaticKey 的值就是利用 makeMap 的返回引用做值的判断。判断节点的属性是否在相对于的范围内:例如有这样一个 template:

<div></div>

然后parse完之后变成这样一个描述对象,所有属性通过 isStaticKey 判断之后,都在上面列出的属性范围中,都是静态属性,所以这就是一个静态节点。

{
  "type": 1,
  "tag": "div",
  "attrsList": [],
  "attrsMap": {},
  "rawAttrsMap": {},
  "children": [],
  "start": 0,
  "end": 11,
  "plain": true
}

另外一个属性是 isPlatformReservedTag。

isPlatformReservedTag

isPlatformReservedTag 用于获取编译器选项 isReservedTag 的引用,检查给定的字符是否是保留的标签。

isPlatformReservedTag = options.isReservedTag || no;

isReservedTag函数如下,用这个函数来判断是否是保留标签,如果一个标签是 html标签或者是 svg标签,那么这个标签就是保留标签。

HTML 保留标签

'html,body,base,head,link,meta,style,title,'+ 'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,'+ 'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,'+ 'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,'+ 's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,'+ 'embed,object,param,source,canvas,script,noscript,del,ins,'+ 'caption,col,colgroup,table,thead,tbody,td,th,tr,'+ 'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,'+ 'output,progress,select,textarea,'+
'details,dialog,menu,menuitem,summary,'+ 'content,element,shadow,template,blockquote,iframe,tfoot'

SVG 保留标签

'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,'+ 'foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,'+ 'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',

var isReservedTag = function(tag) {
	return isHTMLTag(tag) || isSVG(tag)
};

并且在后续的节点标记中会被用到。我们在接着往下看,重点来了。

标记静态节点

function markStatic$1 (node) {
  // ①
  node.static = isStatic(node);
  // ②
  if (node.type === 1) {
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return
    }
    for (var i = 0, l = node.children.length; i < l; i++) {
      var child = node.children[i];
      markStatic$1(child);
      if (!child.static) {
        node.static = false;
      }
    }
    if (node.ifConditions) {
      for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
        var block = node.ifConditions[i$1].block;
        markStatic$1(block);
        if (!block.static) {
          node.static = false;
        }
      }
    }
  }
}

判断节点状态并标记

第一步,判断阶段状态并标记。在这给 AST 元素节点扩展了static属性,通过 isStatic方法调用后返回值,确认哪些节点是静态的,哪些是动态的。

node.static = isStatic(node);

那在 Vue 中那些节点算是动态的,那些阶段算是静态的了?我们先回顾一下上一篇文章在讲生成 AST 时,给每一个元素节点标记type类型,一种有type类型几种?

没错是三种。

  • type = 1的基础元素节点
  • type = 2含有expression和tokens的文本节点
  • type = 3的纯文本节点或者是注释节点
child = {
  type: 1,
  tag:"div",
  parent: null,
  children: [],
  attrsList: []
};
child = {
  type: 2,
  expression: res.expression,
  tokens: res.tokens,
  text: text
};
child = {
  type: 3,
  text: text
};
child = {
  type: 3,
  text: text,
  isComment: true
};

isStatic函数会根据元素的 type和元素的属性进行节点动静态的判断。

如果type = 2说明这一点是一个动态节点,因为包含表达式

如果type = 3说明可能是纯文本节点或者是注释节点,可以标记为静态节点

如果元素节点有:

  • pre 属性,使用了 v-pre指令,标记为静态节点
  • 如果没有动态绑定,没有使用v-if、v-for,不是内置标签(slot,component),是平台保留标签(HTML 标签和 SVG 标签),不是 template 标签的直接子元素并且没有包含在 for 循环中,节点包含的属性只能有 isStaticKey 中指定的几个,那么就标记为静态节点。

现在就知道在什么情况下, Vue 会将一个节点标记为动态节点,什么时候会将一个节点标记为静态节点。

并且在这里也利用到了上面初始赋值的两个变量,isPlatformReservedTag和 isStaticKey,分别用来判断是否是平台保留标签(HTML 标签和 SVG 标签)和间距判断节点的属性只能有 isStaticKey 中指定的几个。

function isStatic(node) {
    if (node.type === 2) {
       return false
    }
    if (node.type === 3) {
       return true
    }
    return !!(node.pre || (
        !node.hasBindings && // no dynamic bindings
        !node.if && !node.for && // not v-if or v-for or v-else
        !isBuiltInTag(node.tag) && // not a built-in
        isPlatformReservedTag(node.tag) && // not a component
        !isDirectChildOfTemplateFor(node) &&
        Object.keys(node).every(isStaticKey)
    ))
}

标记完节点,我们接下往下看。

基础元素节点的处理

来到第二步,这里处理的是节点类型 type = 1的几点。也就是我们的元素节点。

对于我们的元素节点,如果不是平台保留标签(HTML 标签和 SVG 标签、不是 slot 标签、节点是 inline-template那么就会直接返回。

inline-template :内联模板,一般很少被用到,它是一个特殊的 attribute ,当出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。这使得模板的撰写工作更加灵活。但是,在 Vue 3.0 版本去掉了这个内联模板,原因在于 inline-template 会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板。

然后通过 node.children 找到子节点,递归子节点。如果子节点非静态,那么该节点也标注非静态 。这块设计的不太合理有更多好的优化方案,在 Vue3.0 做了优化,编译阶段对静态模板的分析,编译生成了 Block tree。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,而且每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block tree,Vue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,这是一个非常大的性能突破。

 if (!child.static) {
   node.static = false;
 }

最后判断如果节点的 ifConditions 不为空,则遍历 ifConditions拿到所有条件中的 block,block 其实也就是它们对应的 AST 节点,递归执行 markStatic。在这些递归过程中,一旦子节点有不是 static 的情况,则它的父节点的 static 均变成 false。

ifConditions 是撒?

ifConditions 其实是 if 条件的集合,例如有一个模板如下:

<div>
  <div v-if={show}>hello, {{ text }},{{ message }}</div>
  <div v-else-if={show1}>hello, world</div>
  <div v-else>撒也没有!</div>
</div>

那在 parse阶段就会在的 AST 节点中就会给相对于元素的ifConditions添加关联的所有判断集合。

并且每一个ifConditions元素 的block描述就是判断的节点内容。

接下来看下 markStaticRoots。

标记静态根

function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    if (node.static || node.once) {
      node.staticInFor = isInFor
    }
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      node.staticRoot = true
      return
    } else {
      node.staticRoot = false
    }
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

标记静态根节点,整体逻辑大致分为三步:

  • 第一步,已经是 static 的节点或者是 v-once 指令的节点,设置 node.staticInFor = isInFor。
  • 第二步,对于 staticRoot 的判断逻辑。
  • 第三步,遍历 children 以及 ifConditions,递归执行 markStaticRoots。

注意这里的根节点不一定就是 template 最外层的节点,也可能是内部的节点。

什么节点会成为静态根?

从源码来看,一个节点要想成为静态根,必须满足以下几个条件:

  • 自生是一个静态节点
  • 包含子元素
  • 子节点不能仅为一个文本节点(排除注释节点,原因在于除非手动开启保留注释,否则注释节点不会存在)

为什么子节点不能仅为一个文本节点?

当只有纯文本的子节点时,它是一个静态节点,但是不是一个静态根节点。这是为什么了?Vue 官方说明是,如果子节点只有一个纯文本节点,如果优化的话,带来的成本就比好处多了,所以就不优化。

具体为什么不优化了,大家可以思考一下?

标记静态节点和静态根节点有什么区别?

回顾之前这两个标记函数,发现是先将每一个节点都处理了,给每一个节点都加上标记之后,然后利用节点的状态来判断根节点的状态。这样可以利用子节点反推根节点。这就好比:「一个组内部大家都是前端开发,那么间接可以推断,这个组的小组长也是前端开发(当然不是绝对的哈,只是比方)」。

静态根节点和静态节点有一种大包小感觉,利用静态节点的标记函数,间接给静态根节点的标记函数服务。并且通过静态节点的标记函数添加的 static 属性,并不会在后续 DOM 的处理和 render 上使用。但是通过静态根节点的标记函数添加的 staticRoot 属性会在 render中使用。

总结

至此分析完了 optimize 的过程。

optimize前 AST 是这样的:

optimize后 AST 多了static和staticRoot标记:

整个optimize 的过程,就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点表示生成的 DOM 永远不需要改变,这对运行时对模板的更新起到极大的优化作用,提升了运行效率。

参考

内联模板

标记静态节点

Vue原理Compile - 源码版 之 optimize 标记静态节点

编译器 util 工具方法解析

Vue 编译器源码分析 - optimize

以上就是Vue编程三部曲之模型树优化示例的详细内容,更多关于Vue编程模型树优化的资料请关注我们其它相关文章!

(0)

相关推荐

  • vue终极性能优化方案(解决首页加载慢问题)

    目录 前言 1.路由懒加载 2.打包文件中去掉map文件 3.CDN引入第三方库 4.gzip打包 1.npmi-Dcompression-webpack-plugin 2.在vue.config.js中配置 3.在NGINX中配置 5.终极大招,预渲染 1.cnpminstallprerender-spa-plugin--save-dev 2.vue.config.js 3.router.js 4.main.js 总结 前言 用vue开发项目上线以后,发现首页加载速度非常慢,如果项目比较大,甚

  • 分享12个Vue开发中的性能优化小技巧(实用!)

    目录 前言 1.长列表性能优化 1.不做响应式 2.虚拟滚动 2.v-for遍历避免同时使用v-if 3.列表使用唯一key 4.使用v-show复用DOM 5.无状态的组件用函数式组件 6.子组件分割 7.变量本地化 8.第三方插件按需引入 9.路由懒加载 10.keep-alive缓存页面 11.事件的销毁 12.图片懒加载 总结 前言 性能优化,是每一个开发者都会遇到的问题,特别是现在越来越重视体验,以及竞争越来越激烈的环境下,对于我们开发者来说,只完成迭代,把功能做好是远远不够的,最重要

  • Vue项目优化的一些实战策略

    Vue项目完成后就要从开发环境转成生产环境 一些第三方的包体积过大,导致生成js文件过于庞大,这是时候可以生成打包报告来查看项目中的问题 1.生成报告有两种方式,一种使用npm run build --report 2.另一种使用vue脚手架的ui可视化面板,在项目中输入vue ui 3.点击生产环境下的运行按钮,可以看到打包出来的js文件一共有2M之多,js/chunk-vendors.js就是一些项目依赖文件, 再右侧可以看到element-ui 和echarts 以及富文本编辑器,树状表格

  • 22个Vue优化技巧(项目实用)

    目录 代码优化 v-for 中使用 key v-if/v-else-if/v-else 中使用 key 合理的选择 v-if 和 v-show 使用简单的 计算属性 functional 函数式组件(Vue2) 拆分组件 使用局部变量 使用 KeepAlive 事件的销毁 图片加载 采用合理的数据处理算法 其他 首屏/体积优化 体积优化 代码分割 网络 演示代码使用 Vue3 + ts + Vite 编写,但是也会列出适用于 Vue2 的优化技巧,如果某个优化只适用于 Vue3 或者 Vue2,

  • Vue编程三部曲之模型树优化示例

    目录 前言 为什么要做优化? optimize isStaticKey isPlatformReservedTag HTML 保留标签 SVG 保留标签 标记静态节点 判断节点状态并标记 基础元素节点的处理 标记静态根 什么节点会成为静态根? 为什么子节点不能仅为一个文本节点? 标记静态节点和静态根节点有什么区别? 总结 前言 对编译过程的了解会让我们对 Vue 的指令.内置组件等有更好的理解.不过由于编译的过程是一个相对复杂的过程,我们只要求理解整体的流程.输入和输出即可,对于细节我们不必抠太

  • laravel admin实现分类树/模型树的示例代码

    修改模型Category.php <?php namespace App\Admin\Models; use Encore\Admin\Traits\AdminBuilder; use Encore\Admin\Traits\ModelTree; use Illuminate\Database\Eloquent\Model; class Category extends Model { use ModelTree, AdminBuilder; protected $table = 'catego

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

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

  • vue基于Element构建自定义树的示例代码

    说明 做项目的时候要使用到一个自定义的树形控件来构建表格树,在github上搜了一下没有搜索到合适的(好看的)可以直接用的,查看Element的组件说明时发现它的Tree控件可以使用render来自定义节点样式,于是基于它封装了一个可以增.删.改的树形组件,现在分享一下它的使用与实现. 控件演示 github上挂的gif可能会比较卡,有没有大佬知道还有哪里可以挂静态资源的,谢谢..! 控件使用 概要 基于element-ui树形控件的二次封装 提供编辑.删除节点的接口 提供一个next钩子,在业

  • vue项目完成后如何实现项目优化的示例

    目录 一.为开发模式与发布模式指定不同的打包入口 二.通过externals加载外部CDN资源 三.通过CDN优化ElementUI的打包 四.首页内容定制 五.使用路由懒加载 一.为开发模式与发布模式指定不同的打包入口 vue ui创建的项目,隐藏了webpack配置,可以在src根目录新建一个vue.config.js配置文件.在配置文件中向外导出配置对象. 2.默认情况下,vue项目的开发模式和发布模式,共用一个打包的入口文件即(src/main.js).可以使用configureWebp

  • 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使用zTree插件封装树组件操作示例

    本文实例讲述了Vue使用zTree插件封装树组件操作.分享给大家供大家参考,具体如下: 1.通过npm安装jquery npm install jquery --save-dev 2.在build/webpack.base.conf文件当中引入jquery module.exports = { ... resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': reso

  • Java并发编程之内存模型

    目录 一.Java内存模型的基础 1.1 并发编程模型的两个关键问题 1.2 Java内存模型的抽象结构 1.3 从源代码到指令重排序 1.4 写缓冲区和内存屏障 1.4.1 写缓冲区 1.4.2 内存屏障 1.5 happens-before 简介 简介: Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,这一系列几篇文章将揭开Java内存模型的神秘面纱. 这一系列的文章大致分4个部分,分别是: Java内存模型基础,主要介绍内存模型相关基本概念 Java内存模型

  • vue中对接Graphql接口的实现示例

    说明: 本文是本人正在搞nestjs+graphql+serverless训练营中对Graphql讲解的基础知识点,可能有点前后没对接上,文中提到的Graphql授权也是下小节介绍的 一.对原来的Express返回Graphql项目修改 本章节使用的代码是express返回Graphql的代码,在使用前要先对代码进行基本的配置,比如处理跨域问题(Graphql本质也是发送一个http请求,既然是这样在vue项目中自然存在跨域的问题,需要先处理) 1.安装跨域的包,并且配置中间件 npm inst

  • nlp自然语言处理学习CBOW模型类实现示例解析

    目录 实现CBOW模型类 Trainer类的实现 实现CBOW模型类 初始化:初始化方法的参数包括词汇个数 vocab_size 和中间层的神经元个数 hidden_size.首先生成两个权重(W_in 和 W_out),并用一些小的随机值初始化这两个权重.设置astype(‘f’),初始化将使用 32 位的浮点数. 生成层:生成两个输入侧的 MatMul 层.一个输出侧的 MatMul 层,以及一个 Softmax with Loss 层. 保存权重和梯度:将该神经网络中使用的权重参数和梯度分

随机推荐