vue模板编译

1 篇文章 0 订阅
1 篇文章 0 订阅

弄懂vue模板编译需要弄清

  • 编译的目的
  • 什么时候编译
  • 编译过程

编译的目的

我们知道,vue的html部分的代码可以直接书写template字符串,也可以写render函数,那么template和render是什么关系?

new Vue({
            el: '#app',
            template: '<div>this is template</div>',
            render(createElement) {
               return createElement('div', 'this is render')
            }
})

上面一段代码中,页面显示 'this is render',看来render优先级高于template,也就是说,写了render就会忽略template。

而render的目的是生成vnode对象,再遍历vnode生成页面元素,也就是render->vnode->dom。

那么只写template,没有render时,会是template->vnode->dom吗?个人理解,这里跟编译时机有关系

什么时候编译

编译方式有AOT和JIT,名字挺唬人的。

  • AOT,ahead of time,就是提前编译,就是代码运行时,执行一段已经编译好的代码
  • JIT ,just in time, 就是即时编译,就是运行的时候包括了编译过程

vue模板编译也不例外,可以提前编译和即时编译,所以vue有runtime版本和完整版本,runtime版本没有编译功能,体积更小,只用于运行已经编译的代码。

比如我们用webpack构建包时,vue-loader很重要的一个功能就是编译.vue文件中的template。回到编译目的,如果把template编译成一个vnode,这个vnode如此大,从服务器返回到页面,显然不合理,所以template应该是转成render函数(准确说是字符串,然后执行时用with函数解析),运行时再生成vnode,即template->render->vnode-dom。当然了,这只是基于源码后的一个分析,并不是推测出来的过程。

如果运行未编译好的vue项目,则需要使用包含编译的版本,一边运行一边编译

编译的过程

var createCompiler = createCompilerCreator(function baseCompile(
    template,
    options
  ) {
    var ast = parse(template.trim(), options);
    if (options.optimize !== false) {
      optimize(ast, options);
    }
    var code = generate(ast, options);
    return {
      ast: ast,
      render: code.render,
      staticRenderFns: code.staticRenderFns
    }
  });

步骤清晰明了

  • 解析html,生成ast
  • 对ast做一些优化
  • 生成render函数字符串

解析html,生成ast

ast(abstract syntax code)即抽象语法树,一切概念都是纸老虎,它就是一个树状结构的对象,用于描述节点信息,比如

new Vue({
            el: '#app',
            template: "<div class='container'>this is template</div>",
        })

template转成ast

有3类ast

  • type为1的ASTElement,就是有tag的
  • type为2的ASTExpression,就是表达式式,比如{{message}},指令如v-if、v-for
  • type为3的ASTText,就是静态文本节点

解析html这个过程可谓繁琐,大概有几个要点

  • 维护一个index,代表解析到哪个位置了
  • 维护一个当前解析节点对象obj
  • 维护一个stack=[],代表着解析深度

这里很关键的是stack

  1. 当遇到一个标签开始,如 <div ,则解析结果为 obj={tag:'div',end:false,children:[],...} ,end代表是否结束,当遇到 </div> 才结束。把obj push到stack里 。
  2. 接下来如果碰到又一个标签的开始,如<p,重复第1步。对于p,它在stack的前一项对应tag为div的那项,而它的end为false,所以把 p 作为 div 的children。
  3. 接下来如果遇到结束标签,则obj.end=true,stack pop最后一项,并且obj 始终指向stack最后一项

 这个stack有点像,我们打开网页1,上面贴了网页2的地址,我们打开网页2,浏览完了关闭它,再回到网页1。后打开的先看完,并且能自动返回父窗口。

let template = "<div class='container'>" +
    "<p class='item1'>{{message1}}</p>" +
    "</div>"

以上面一段代码为例,维护一个栈stack,index=0,当前节点obj

  1. 用正则匹配到一个标签的开始,<div ,长度为4,index增加4,当前对象 obj={'tag':'div',end:false,children:[],...},并添加到stack里。
  2. 从index为4开始,在遇到 > 之前,中间的内容都收集做为当前对象的属性,如 class='container'  ,同时index往前走,直到遇到 > ,说明该对象属性收集完毕
  3. 继续用正则匹配,发现了<p ,又是一个标签的开始,obj={'tag':'p',end:false,children:[],...},并添加到stack里。然后收集属性,直到遇到  >  。因为stack里上一项div对应的obj,end为false,所以需要将当前对象 p对应的obj添加到div的children里。
  4. 然后遇到 {{ ,它不是一个标签开始,所以在匹配到标签之前,把所以内容都作为一个节点。同理,因为 p 对应obj的end为false,把这部分收集到 p 的children里。
  5. 继续匹配,匹配到 </p> ,是一个结束标签,p 匹配结束,让它end为true, stack长度减 1,obj改变指向为stack最后一项  ... 匹配到 </div> ,同理。
  6. template被解析完,收工

对ast做一些优化

我们知道,vue 重新渲染,会有一个 diff 过程,就是比较新旧vnode对象,然后只针对差异部分进行dom处理。

而对ast做优化就是给ast对象添加一个标记,如果我们可以预知这个节点永远不会更新,那么我们既可以标记它的static为true,然后diff过程中直接将它跳过。

优化分为两步

  • 遍历所有节点,标记是否为静态节点
  • 遍历节点,判断是否为静态根节点
//上面说过有3种节点  
function isStatic(node) {
    if (node.type === 2) { // 表单式节点 expression
      return false
    }
    if (node.type === 3) { // 文本节点 text
      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)
    ))
  }  
function markStatic(node) {
    node.static = isStatic(node);
    if (node.type === 1) {
      if (
        !isPlatformReservedTag(node.tag) &&
        node.tag !== 'slot' &&
        node.attrsMap['inline-template'] == null
      ) {
        return
      }
        // 遍历子节点,若有一个子节点不是static,那么父节点不能为static
      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;
          }
        }
      }
    }
  }

除了 node.ifConditions ,其他都好理解。按道理有v-if,static应该为false。个人理解,这里是为了区分 表达式v-if="true"  和 字符串v-if="'true'" 。

  function markStaticRoots(node, isInFor) {
    if (node.type === 1) {
      if (node.static || node.once) {
        node.staticInFor = isInFor;
      }

      // For a node to qualify(合格; 使合格; 使具备资格) as a static root, it should have children that
      // are not just static text. Otherwise the cost of hoisting(提升) out will
      // outweigh the benefits and it's better off to just always render it fresh.
      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 (var i = 0, l = node.children.length; i < l; i++) {
          markStaticRoots(node.children[i], isInFor || !!node.for);
        }
      }
      if (node.ifConditions) {
        for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
          markStaticRoots(node.ifConditions[i$1].block, isInFor);
        }
      }
    }
  }

注意这里的静态根节点,是一个相对子节点来说的根节点,并非顶层节点那种意思。

注释翻译过来,就是,一个节点static为true,并且有超过1个children,并且第一个child的type不是text时,才标记staticRoot为true,否则这种静态根节点预处理带来的消耗比收益低。这里应该是减少遍历?因为绝大部分节点都会有children。先留个坑。

生成render函数字符串

将ast标记优化好后,遍历ast树,将节点变成一个等待调用的函数,该函数用于创建该节点

几种内部方法
_c:对应的是 createElement 方法,顾名思义,它的含义是创建一个元素(Vnode)
_v:创建一个文本结点。
_s:把一个值转换为字符串。(eg: {{data}})
_m:渲染静态内容

把这几个方法写的这么短的目的是减少代码体积,因为生成的是render函数字符串,字符串在打包时时不会被压缩的。

<template>
  <div class="container">
    {{message1}}
  </div>
</template>

会被转成字符串

let code ='_c('div',{staticClass:"container"},[_v(_s(message1))])'
render =  ("with(this){return " + code + "}")

当调用vue的$mount时,通过new Fucntion就把render字符串当作函数来执行,调用里面的_c、_v、_s 方法,创建vnode,然后创建真实dom。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值