Vue模型树优化的原因是什么,具体怎么做
Admin 2022-08-09 群英技术资讯 275 次浏览
在上一篇文章中,我们分析了 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.
简单理解就是:
因为我们知道 Vue 是一个数据驱动视图的响应式框架,但是在开发者书写的 template 中,也不是所有的数据都是响应式的,有很多的数据在首屏渲染完之后就永远不在变化,数据不在变化也就意味着 DOM 不在变化,所以在后续的更新过程进行 patch时完全可以直接跳过他们的比对,从而来提升效率。
接下来我们开始 optimize 源码之旅!看看源码中是如何去优化模型树的?
template 在经过解析之后,就会进行优化操作。首先这里有一个小逻辑,会判断是否需要进行优化?只有当options.optimize !== false时才会进行优化。
这里抛出几个小问题?options.optimize为什么需要进行这样的判断了?并且如何能关闭模型树优化的操作了?什么情况下会关闭模型树的优化?
var ast = parse(template.trim(), options); if (options.optimize !== false) { optimize(ast, options); }
在往下,进入到optimize函数,代码很清楚,优化主要做两件事情:
function optimize (root, options) { if (!root) { return } isStaticKey = genStaticKeysCached(options.staticKeys || ''); isPlatformReservedTag = options.isReservedTag || no; // 第一步:标记所有静态节点。 markStatic$1(root); // 第二步:标记静态根 markStaticRoots(root, false); }
在进行优化操作之前会有两个变量的赋值。
获取 genStaticKeysCached 函数返回值, 获取 makeMap 函数返回值引用 。
isStaticKey = genStaticKeysCached(options.staticKeys || '');
这里简单了解一下涉及到的 makeMap 函数:
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 用于获取编译器选项 isReservedTag 的引用,检查给定的字符是否是保留的标签。
isPlatformReservedTag = options.isReservedTag || no;
isReservedTag函数如下,用这个函数来判断是否是保留标签,如果一个标签是 html标签或者是 svg标签,那么这个标签就是保留标签。
'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,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类型几种?
没错是三种。
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说明可能是纯文本节点或者是注释节点,可以标记为静态节点
如果元素节点有:
现在就知道在什么情况下, 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) } } } }
标记静态根节点,整体逻辑大致分为三步:
注意这里的根节点不一定就是 template 最外层的节点,也可能是内部的节点。
从源码来看,一个节点要想成为静态根,必须满足以下几个条件:
当只有纯文本的子节点时,它是一个静态节点,但是不是一个静态根节点。这是为什么了?Vue 官方说明是,如果子节点只有一个纯文本节点,如果优化的话,带来的成本就比好处多了,所以就不优化。
具体为什么不优化了,大家可以思考一下?
回顾之前这两个标记函数,发现是先将每一个节点都处理了,给每一个节点都加上标记之后,然后利用节点的状态来判断根节点的状态。这样可以利用子节点反推根节点。这就好比:「一个组内部大家都是前端开发,那么间接可以推断,这个组的小组长也是前端开发(当然不是绝对的哈,只是比方)」。
静态根节点和静态节点有一种大包小感觉,利用静态节点的标记函数,间接给静态根节点的标记函数服务。并且通过静态节点的标记函数添加的 static 属性,并不会在后续 DOM 的处理和 render 上使用。但是通过静态根节点的标记函数添加的 staticRoot 属性会在 render中使用。
至此分析完了 optimize 的过程。
optimize前 AST 是这样的:
optimize后 AST 多了static和staticRoot标记:
整个optimize 的过程,就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点表示生成的 DOM 永远不需要改变,这对运行时对模板的更新起到极大的优化作用,提升了运行效率。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:mmqy2019@163.com进行举报,并提供相关证据,查实之后,将立刻删除涉嫌侵权内容。
猜你喜欢
目录vue当前页push当前页无效vue push报错TypeError: Cannot read property ‘push‘ of undefined解决方法vue当前页push当前页无效当在当前页面中push页面跳转当前页,只是push的参数不同时,只能用字符串拼接,parames和query都不会起作用。不知
本篇文章给大家带来了关于javascript的相关知识,其中主要整理了switch的四种写法相关问题,包括了IIFE 封装、封成策略等等内容,下面一起来看一下,希望对大家有帮助。
这篇文章主要为大家介绍了在Iconfont还是不能上传,要如何维护你的Icon,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪<BR>
这篇文章主要为大家介绍了JS数组中常用的方法技巧,学会了你就在进阶成为大佬的道路上又迈进了一步,希望能够有所帮助,祝大家多多进步
这篇文章主要介绍了vue-amap根据地址回显地图并mark的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
成为群英会员,开启智能安全云计算之旅
立即注册Copyright © QY Network Company Ltd. All Rights Reserved. 2003-2020 群英 版权所有
增值电信经营许可证 : B1.B2-20140078 粤ICP备09006778号 域名注册商资质 粤 D3.1-20240008