vue模板编译

弄懂vue模板编译需要弄清

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

编译的目的

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

new Vue({
            el: '#app',
            template: '
this is template
', 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: "
this is template
", })

template转成ast

vue模板编译_第1张图片

有3类ast

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

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

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

这里很关键的是stack

  1. 当遇到一个标签开始,如 ,则解析结果为 obj={tag:'div',end:false,children:[],...} ,end代表是否结束,当遇到
才结束。把obj push到stack里 。
  • 接下来如果碰到又一个标签的开始,如,重复第1步。对于p,它在stack的前一项对应tag为div的那项,而它的end为false,所以把 p 作为 div 的children。
  • 接下来如果遇到结束标签,则obj.end=true,stack pop最后一项,并且obj 始终指向stack最后一项
  •  这个stack有点像,我们打开网页1,上面贴了网页2的地址,我们打开网页2,浏览完了关闭它,再回到网页1。后打开的先看完,并且能自动返回父窗口。

    let template = "
    " + "

    {{message1}}

    " + "
    "

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

    1. 用正则匹配到一个标签的开始, ,长度为4,index增加4,当前对象 obj={'tag':'div',end:false,children:[],...},并添加到stack里。
    2. 从index为4开始,在遇到 > 之前,中间的内容都收集做为当前对象的属性,如 class='container'  ,同时index往前走,直到遇到 > ,说明该对象属性收集完毕
    3. 继续用正则匹配,发现了 ,又是一个标签的开始,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 匹配结束,让它end为true, stack长度减 1,obj改变指向为stack最后一项  ... 匹配到 
    ,同理。
  • 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函数字符串,字符串在打包时时不会被压缩的。

    会被转成字符串

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

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

    你可能感兴趣的:(vue,模板编译,html解析,vue)