首页私人日誌Vue编程三部曲之模型树优化实战代码

Vue编程三部曲之模型树优化实战代码

admin 10-14 12:07 328次浏览

  实践是所有展示最好的方法,因此我觉得可以不必十分细致的,但我们的展示却是整体的流程、输入和输出。现在我们就看看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 || 39; 39;);   isPlatformReservedTag = options.isReservedTag || no;   // 第一步:标记所有静态节点。   markStatic$1(root);   // 第二步:标记静态根   markStaticRoots(root, false);   }

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

  isStaticKey

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

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

  这里简单了解一下涉及到的 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( 39;, 39;);   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(    39;type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap 39; +   (keys ? 39;, 39; + keys : 39; 39;)   )   }   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 保留标签

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

   39;details,dialog,menu,menuitem,summary, 39;+ 39;content,element,shadow,template,blockquote,iframe,tfoot 39;

  SVG 保留标签

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

  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 !== 39;slot 39;   node.attrsMap[ 39;inline-template 39;] == 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编程三部曲之模型树优化实战代码
python中第三方库交叉编译问题怎么解决 C#怎么实现十六进制与十进制相互转换及不同进制表示
相关内容