[咖聊]休假去取“模板编译”真经了

冲一杯美式 ☕️ ,读编译真经,岂不快哉?

本文的 ? (表示 例子,☕️ 和 ? 更配哦!全文都会围绕这个 DEMO 做解析):

<div id="app">
    <!-- 这是一个注释节点 -->
    <Child name="yjc" :age="12" v-if="isShow"></Child>
    <input type="text" v-model="inputValue" />
    <div class="abc"></div>
</div>
复制代码
const Child = Vue.extend({
  name: 'Child',

  props: {
    name: String,

    age: Number
  },

  render (h) {
    return h('div', null, [
      h('span', null, this.name),
      h('span', null, this.age),
    ])
  }
})

new Vue({
  el: '#app',

  components: {
    Child
  },

  data () {
    return {
      isShow: true,
      inputValue: '123123'
    };
  }
})
复制代码

? 中包含模板编译处理的节点——注释节点、开始标签、props 属性、DOM 属性、自闭合标签。

拿起 :coffee: ,让我们看看是从哪里开始执行模板编译的。回忆一下 [咖聊]Vue执行过程,其中有一个 options 是否存在 render 的判断。如果是自己手写 render 函数,例如 ? 中的 Child 组件就属于这种情况则不需要走模板编译流程;如果是通过 SFC 或者写 template 的,那么会通过模板编译去生成 render 函数。

这部分代码在 src\platforms\web\entry-runtime-with-compiler.js

/**
 * 挂载组件,带模板编译
 */
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean // 与服务端渲染有关,不考虑
): Component {

  // 挂载dom,query对它做了一些判断,是dom直接返回,是字符串通过querySelector去获取dom
  el = el && query(el)

  // 配置信息
  const options = this.$options

  // resolve template/el and convert to render function
  // 不存在render函数,处理template内容,转换为render函数
  if (!options.render) {
    	// ... 省略一部分获取 template 字符串的过程
    }
    if (template) {
      // ...
      // 执行模板编译,最终结果返回 render 和 staticRenderFns
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      // ...
    }
  }
  /*调用const mount = Vue.prototype.$mount保存下来的不带编译的mount*/
  return mount.call(this, el, hydrating)
}
复制代码

可以看到,模板编译最终得到的结果是 renderstaticRenderFns 函数,这个 staticRenderFns 干嘛用的? ?不是只需要 render 吗?

为了得到编译函数 compileToFunctions, 需要执行以下5步:

  1. src\platforms\web\compiler\index.jscreateCompiler(baseOptions)

  2. src\compiler\create-compiler.jscreateCompilerCreator

  3. src\compiler\to-function.jscreateCompileToFunctionFn (*compile*: Function)

  4. const compiled = baseCompile(template, finalOptions)
    复制代码
  5. export const createCompiler = createCompilerCreator(function baseCompile (
      template: string,
      options: CompilerOptions
    ): CompiledResult {
      // 编译生成AST
      const ast = parse(template.trim(), options)
    
      if (options.optimize !== false) {
        /**
         * 将AST进行优化
         * 优化的目标:生成模板AST,检测不需要进行DOM改变的静态子树。
         * 一旦检测到这些静态树,我们就能做以下这些事情:
         * 1.把它们变成常数,这样我们就再也不需要每次重新渲染时创建新的节点了。
         * 2.在patch的过程中直接跳过。
         */
        optimize(ast, options)
      }
    
      // 根据AST生成所需的code(内部包含render与staticRenderFns)
      const code = generate(ast, options)
      return {
        ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
      }
    })
    复制代码

在执行编译之前,扩展 baseOptions 上的很多配置。同时在开始编译时,就决定了当前的编译环境,后面再更新用的还是这套编译环境,所以也做了编译器的缓存

整装待发,踏入了解析阶段。

parse

这个阶段用一句话概括起来就是“用各种正则表达式去匹配字符串中的开始标签、属性、注、闭合标签等,最终产出 AST的过程”。

首先安利一个正则小工具:regex101 ,页面中每一个板块都极其好用,太香啦 :yum::

regex101.jpg

  • 有详细的正则解释;
  • 可以实时输入查看匹配结果;
  • 如果忘记正则基础知识,还有快速参考模块;
  • 能够输出匹配到的全部分组结果;
  • 保留测试结果,通过链接就能同步给其他小伙伴,(⚠️ 后文中看到的正则都可以点击查看详情​)。

开始之前,先看一个不管任何匹配都会调用的函数 advance

function advance (n) {
    index += n
    html = html.substring(n)
}
复制代码

清晰明了,就是将匹配到的结果从字符串中剔除,然后重置 html

这节我们就通过 ? 中的模板,看 AST 是如何生成的:

<div id="app">
    <!-- 这是一个注释节点 -->
    <Child name="yjc" :age="12" v-if="isShow"></Child>
    <input type="text" v-model="inputValue" />
  	<div class="abc"></div>
</div>
复制代码

按照上面的模板,一步一步讲解匹配过程:

  1. 开始标签 <div id="app">:

    function parseStartTag () {
      const start = html.match(startTagOpen)
      if (start) {
        const match = {
          tagName: start[1],
          attrs: [],
          start: index
        }
        advance(start[0].length)
        let end, attr
        while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
          advance(attr[0].length)
          match.attrs.push(attr)
        }
        if (end) {
          match.unarySlash = end[1]
          advance(end[0].length)
          match.end = index
          return match
        }
      }
    }
    复制代码
    1. 匹配开始标签名,此时会创建一个 match 对象;

    2. 匹配开始标签中的属性,给 match 中的 attrs 添加属性 match 的结果;

    3. 匹配开始标签的结尾 > 字符,将匹配分组信息和结尾位置分别记录到match.unarySlashmatch.end 中。

    4. 紧接着对 match 调用 handleStartTag 做处理:

      function handleStartTag (match) {
        const tagName = match.tagName
        const unarySlash = match.unarySlash
      
        if (expectHTML) {
          if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
            parseEndTag(lastTag)
          }
          if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
            parseEndTag(tagName)
          }
        }
      
        // 判断是不是一元标签,例子的中的 input 这里会是 true,后面再看
        const unary = isUnaryTag(tagName) || !!unarySlash
      
        // 遍历全部的 attrs 
        const l = match.attrs.length
        const attrs = new Array(l)
        for (let i = 0; i < l; i++) {
          const args = match.attrs[i]
          // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
          if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
            if (args[3] === '') { delete args[3] }
            if (args[4] === '') { delete args[4] }
            if (args[5] === '') { delete args[5] }
          }
          const value = args[3] || args[4] || args[5] || ''
          const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
          ? options.shouldDecodeNewlinesForHref
          : options.shouldDecodeNewlines
          
          // 对属性值做编码处理,xss攻击
          attrs[i] = {
            name: args[1],
            value: decodeAttr(value, shouldDecodeNewlines)
          }
        }
      
        // 不是一元标签的情况下将标签名等信息推进 stack 中,并给 lastTag 赋值当前标签名,这个用于后面的标签栈匹配
        if (!unary) {
          stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
          lastTag = tagName
        }
      	
        // 调用 start 生成 ASTElement
        if (options.start) {
          options.start(tagName, attrs, unary, match.start, match.end)
        }
      }
      复制代码

      handleStartTag 先判断当前标签是不是一元标签,然后处理了 attrs 上的值,比如编码处理等。不是一元标签的话,把标签部分信息存到 stack 中,最后调用 start 函数生成 rootElement

      start (tag, attrs, unary) {
        // ...
      
        // 创建 ASTElement
        let element: ASTElement = createASTElement(tag, attrs, currentParent)
      
        // ...
      
        // apply pre-transforms
        for (let i = 0; i < preTransforms.length; i++) {
          element = preTransforms[i](element, options) || element
        }
        // ...
      
        if (!root) {
          root = element
          
          // 校验检查,不要用slot、template做根节点,也不要用 v-for 属性,因为这些都可能产生多个根节点
          checkRootConstraints(root)
        } else {
          // ...
        }
        
        // ...
        // 不是一元标签,把当前的 ASTElement 推入到 stack 中
        if (!unary) {
          currentParent = element
          stack.push(element)
        } else {
          closeElement(element)
        }
      },
      复制代码

      对于 ? 中的 rootElement 比较简单,没有其他逻辑分支处理,就直接贴上结果图:

      root-ASTElement.jpg

    到此开始标签 <div id="app"> 就解析完了。此时的 html 因为 advance 的递进处理,变成了下面这般模样:

        <!-- 这是一个注释节点 -->
        <Child name="yjc" :age="12" v-if="isShow"></Child>
        <input type="text" v-model="inputValue" />
    		<div class="abc"></div>
    </div>
    复制代码
  2. 在解析注释节点之前,我们可以看到有一系列空格,这个处理也比较简单,就是看当前 textEnd (? 中 < 的位置),然后判断是大于 0 的情况,将这些空白字符去掉就行了:

    let text, rest, next
    
    // demo 中这里是 4 ,是大于 0 的
    if (textEnd >= 0) {
      
      /**
       * 直接走到这里,rest 是 
       * <!-- 这是一个注释节点 -->
            <child name="yjc" :age="12" v-if="isShow"></child>
            <input type="text" v-model="inputValue">
            <div class="abc"></div>
        </div>
       */
      rest = html.slice(textEnd)
      while (
        !endTag.test(rest) &&
        !startTagOpen.test(rest) &&
        !comment.test(rest) &&
        !conditionalComment.test(rest)
      ) {
        // < in plain text, be forgiving and treat it as text
        next = rest.indexOf('<', 1)
        if (next < 0) break
        textEnd += next
        rest = html.slice(textEnd)
      }
      text = html.substring(0, textEnd)
      advance(textEnd)
    }
    复制代码

    然后又会进入创建 AST 的过程,这次的回调函数是 options.chars

    chars (text: string) {
    
      // ...
      const children = currentParent.children
      text = inPre || text.trim()
        ? isTextTag(currentParent) ? text : decodeHTMLCached(text)
      // only preserve whitespace if its not right after a starting tag
      : preserveWhitespace && children.length ? ' ' : ''
      if (text) {
        let res
        if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
          children.push({
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text
          })
        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
          children.push({
            type: 3,
            text
          })
        }
      }
    },
    复制代码

    空格字符走进来兜了一圈,因为 trim 之后就啥都不剩了,所以兜了一圈又回到 parseHTML 主流程上啦。:sunglasses:

  3. 接下来是一个注释节点 <!-- 这是一个注释节点 -->

    if (comment.test(html)) 
      // 计算注释节点结束位置
      const commentEnd = html.indexOf('-->')
    
      if (commentEnd >= 0) {
        
        // 是否保存注释节点
        if (options.shouldKeepComment) {
          options.comment(html.substring(4, commentEnd))
        }
        
        // 递进,从 html 中剔除注释节点
        advance(commentEnd + 3)
        continue
      }
    }
    复制代码
    1. 匹配注释节点的开头

    2. 判断是否需要保留注释节点( ⚠️ 这个配置从配置中读取,你可以按照下面的方式配置),不需要的话接着处理 html 模板,否则 AST 会添加一个注释文本节点:

      new Vue({
        el: '#app',
      
        components: {
          Child
        },
      
        // 注意:这里可以配置保存注释信息
        comments: true,
      
        data () {
          return {
            isShow: true,
            inputValue: ''
          };
        }
      })
      复制代码

    处理完了注释节点,模板变成了:

        <Child name="yjc" :age="12" v-if="isShow"></Child>
        <input type="text" v-model="inputValue" />
    		<div class="abc"></div>
    </div>
    复制代码
  4. 处理空白字符,重复步骤2。

  5. 接下来是一个组件节点 <Child name="yjc" :age="12" v-if="isShow"></Child>

    1. parseStartTag 跟前面 <div id="app"> 没有区别,无非就是多循环了几遍 attrs 的处理过程。处理之后的 match 结果如下:

      Child-parseStartTag-result.jpg

    2. 然后执行到 options.start 函数,跟上面 div 相同的逻辑这里就不叙述了。Childdiv 有几点不一样的是:

      1. Childv-if 指令,getAndRemoveAttr 会把 attrsList 中的 v-if 属性删除,然后在 Child AST 上加上 ififCondition 字段;

        function processIf (el) {
          // 获取 v-if 指令的值,例子中是 isShow
          const exp = getAndRemoveAttr(el, 'v-if')
          if (exp) {
            el.if = exp
            addIfCondition(el, {
              exp: exp,
              block: el
            })
          } else {
            if (getAndRemoveAttr(el, 'v-else') != null) {
              el.else = true
            }
            const elseif = getAndRemoveAttr(el, 'v-else-if')
            if (elseif) {
              el.elseif = elseif
            }
          }
        }
        复制代码
      2. 属性的 AST 处理,在上面 <div id="app"> 的时候略过了,现在来看看:

        function processAttrs (el) {
          // 获取属性列表
          const list = el.attrsList
          let i, l, name, rawName, value, modifiers, isProp
          for (i = 0, l = list.length; i < l; i++) {
            name = rawName = list[i].name
            value = list[i].value
        
            /*匹配v-、@以及:,处理el的特殊属性*/
            if (dirRE.test(name)) {
              // mark element as dynamic
              /*标记该ele为动态的*/
              el.hasBindings = true
              // modifiers
              /*解析表达式,比如a.b.c.d得到结果{b: true, c: true, d:true}*/
              modifiers = parseModifiers(name)
              if (modifiers) {
                /*得到第一级,比如a.b.c.d得到a,也就是上面的操作把所有子级取出来,这个把第一级取出来*/
                name = name.replace(modifierRE, '')
              }
              /*如果属性是v-bind的*/
              if (bindRE.test(name)) { // v-bind
                name = name.replace(bindRE, '')
                value = parseFilters(value)
                isProp = false
                if (modifiers) {
                  /**
                   *   https://cn.vuejs.org/v2/api/#v-bind
                   *   这里用来处理v-bind的修饰符
                   */
                  /*.prop - 被用于绑定 DOM 属性。*/
                  if (modifiers.prop) {
                    isProp = true
                    /*将原本用-连接的字符串变成驼峰 aaa-bbb-ccc => aaaBbbCcc*/
                    name = camelize(name)
                    if (name === 'innerHtml') name = 'innerHTML'
                  }
                  /*.camel - (2.1.0+) 将 kebab-case 特性名转换为 camelCase. (从 2.1.0 开始支持)*/
                  if (modifiers.camel) {
                    name = camelize(name)
                  }
                  //.sync (2.3.0+) 语法糖,会扩展成一个更新父组件绑定值的 v-on 侦听器。
                  if (modifiers.sync) {
                    addHandler(
                      el,
                      `update:${camelize(name)}`,
                      genAssignmentCode(value, `$event`)
                    )
                  }
                }
                if (isProp || (
                  !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
                )) {
                  /*将属性放入el的props属性中*/
                  addProp(el, name, value)
                } else {
                  /*将属性放入el的attr属性中*/
                  addAttr(el, name, value)
                }
              } else if (onRE.test(name)) { // v-on
                /*将属性放入el的attr属性中*/
                name = name.replace(onRE, '')
                addHandler(el, name, value, modifiers, false, warn)
              } else { // normal directives
                /*去除@、:、v-*/
                name = name.replace(dirRE, '')
                // parse arg
                const argMatch = name.match(argRE)
                /*比如:fun="functionA"解析出fun="functionA"*/
                const arg = argMatch && argMatch[1]
                if (arg) {
                  name = name.slice(0, -(arg.length + 1))
                }
                /*将参数加入到el的directives中去*/
                addDirective(el, name, rawName, value, arg, modifiers)
                if (process.env.NODE_ENV !== 'production' && name === 'model') {
                  checkForAliasModel(el, value)
                }
              }
            } else {
              // ...
              /*将属性放入el的attr属性中*/
              addAttr(el, name, JSON.stringify(value))
              // #6887 firefox doesn't update muted state if set via attribute
              // even immediately after element creation
              if (!el.component &&
                  name === 'muted' &&
                  platformMustUseProp(el.tag, el.attrsMap.type, name)) {
                addProp(el, name, 'true')
              }
            }
          }
        }
        复制代码

        parseAttrs 遍历 attrsList,处理各种属性情况,例如:v-bind@、值表达式、修饰符等各种场景,就不一个一个逻辑去执行了。只看我们 ? 中name=“yjc”:age="12"。纯文本的比较简单,执行 addAttr(el, name, JSON.stringify(value))AST 上加上 attrs 属性;后者通过 dirREbindRE 去掉 : 符号之后添加到 attrs 中。

      3. 编译 Child 时,root 节点是存在的,这时会构建 parentchildren 的关系:

        // 解析到 Child 时,currentParent 指向的是 div 节点
        if (currentParent && !element.forbidden) {
          if (element.elseif || element.else) {
            processIfConditions(element, currentParent)
          } else if (element.slotScope) { // scoped slot
            currentParent.plain = false
            const name = element.slotTarget || '"default"'
            ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
          } else {
            // div AST 的 children 字段加入 Child AST
            currentParent.children.push(element)
            // Child AST 的 parent 赋值为 div AST
            element.parent = currentParent
          }
        }
        复制代码

    处理完 Child 节点后的结果:

    </Child>
        <input type="text" v-model="inputValue" />
    		<div class="abc"></div>
    </div>
    复制代码
  6. 闭合标签 </Child> 的处理过程:

    1. 先用闭合标签正则惰性地匹配,这个正则就是在开始标签正则的基础上加了一个 / ;

    2. 然后用 advance 剔除闭合标签;

    3. 通过 parseEndTagoptions.end 去更新标签和 ASTstack

        function parseEndTag (tagName, start, end) {
          let pos, lowerCasedTagName
          if (start == null) start = index
          if (end == null) end = index
      
          if (tagName) {
            lowerCasedTagName = tagName.toLowerCase()
          }
      
          // Find the closest opened tag of the same type
          if (tagName) {
            for (pos = stack.length - 1; pos >= 0; pos--) {
              if (stack[pos].lowerCasedTag === lowerCasedTagName) {
                break
              }
            }
          } else {
            // If no tag name is provided, clean shop
            pos = 0
          }
      
          if (pos >= 0) {
            // Close all the open elements, up the stack
            for (let i = stack.length - 1; i >= pos; i--) {
              // ...
              if (options.end) {
                options.end(stack[i].tag, start, end)
              }
            }
      
            // 将数组长度设置成当前位置,提出栈中最后一个标签,并更新 lastTag
            stack.length = pos
            lastTag = pos && stack[pos - 1].tag
          } 
          // ...
        }
      复制代码

      parseEndTag 将标签转成小写之后和栈中最上面的元素做比较,这就是为什么 <Child></child> 这样也不会报标签不匹配的原因。然后调用 options.end 去更新 AST stack

      end () {
        // 处理尾部空格的情况
        const element = stack[stack.length - 1]
        const lastNode = element.children[element.children.length - 1]
        if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
          element.children.pop()
        }
        // 最后一个AST信息弹出栈,并更新当前的currentParent节点
        stack.length -= 1
        currentParent = stack[stack.length - 1]
        
        // 更新了 inVPre 和 inPrV 的状态, ?不需要了解
        closeElement(element)
      },
      复制代码

    处理了 </Child> 之后的结果:

        <input type="text" v-model="inputValue" />
    		<div class="abc"></div>
    </div>
    复制代码
  7. 至此,开始标签、标签属性、闭合标签等都已经通过源码过了一遍,对于下一个 input 节点,我们就看 v-model 和自闭合标签的处理:

    1. parseStartTag 和之前的流程一样;

    2. 执行到 handleStartTagconst unary = isUnaryTag(tagName) || !!unarySlash 时,这里返回的是 true;自闭合标签因为不用匹配闭合标签,所以不需要入栈。直接执行 options.start

    3. 生成 AST 时,90% 的流程都是一样的。v-model="inputValue" 会在执行 processElement -> processAttrs 时调用 addDirective

      export function addDirective (
        el: ASTElement,
        name: string,
        rawName: string,
        value: string,
        arg: ?string,
        modifiers: ?ASTModifiers
      ) {
        (el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers })
        el.plain = false
      }
      复制代码

      会在 AST 节点上添加 directives 数组然后把 modelinputValue 都推进到该数组中。最终 input 生成的 AST 如下图所示:

      v-model-ast.jpg

    解析完 input 节点,html 只剩下:

    		<div class="abc"></div>
    </div>
    复制代码
  8. 最终剩下的模板就非常简单了,就是重复前面的过程处理即可。这里就不写了。(其实这个节点是为了后面的 optimize 做铺垫。??)

  9. html 只剩下 "" 时,最终会再执行一次 parseEndTag,用于栈中清理剩余的标签。

小结

parse 过程就是将 template 字符串通过正则表达式(复杂的正则通过 regex101 工具协助分析,可以梳理匹配场景)去匹配出开始标签、闭合标签、注释节点、标签属性等。补充一个标签栈的匹配过程:

标签栈-动画效果.gif

然后在匹配过程中调用各自的回调函数去生成 AST。每次解析完一个节点之后通过 advance 递进。最终解析完整个字符串,返回 AST 给下一个环节——optimize。在开始分析 optimize 之前,生成 AST 有一个细节还没讲到,就是 AST 中的 type 字段。type 的含义(⚠️ 魔数慎用,降低理解成本):

  • 1 表示的是普通元素;

  • 2 表示表达式;

  • 3 表示纯文本

optimize

本小节目标:

  1. 优化的目的是什么?
  2. 怎样的节点才算是静态节点?
  3. 满足什么条件的节点才能是静态根节点?

带着以上3个问题,开始取“优化”真经。在入口有一个判断:

  if (options.optimize !== false) {
    optimize(ast, options)
  }
复制代码

还有不进行优化的情况吗?对于 web 的情况,这个是 undefined 的,undefined !== false 成立,所以需要进行优化。对于 weex 的情况,options.optimize 是明确成 false 的。看到 optimize

/**
 * 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:
 *
 * 1. Hoist them into constants, so that we no longer need to
 *    create fresh nodes for them on each re-render;
 * 2. Completely skip them in the patching process.
 */
export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  markStatic(root)
  // second pass: mark static roots.
  markStaticRoots(root, false)
}
复制代码

对于第一个问题,optimize 的注释已经给出了答案:

  • 一是将它们提升为静态常量,在每次重新渲染的时候不需要创建新的静态节点;
  • 二是在 patch 过程中可以完全跳过它们;

markStatic

看到第一个主流程 markStatic(root)

function markStatic (node: ASTNode) {
  node.static = isStatic(node)
  if (node.type === 1) {
    // do not make component slot content static. this avoids
    // 1. components not able to mutate slot nodes
    // 2. static slot content fails for hot-reloading
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return
    }
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      if (!child.static) {
        node.static = false
      }
    }
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        const block = node.ifConditions[i].block
        markStatic(block)
        if (!block.static) {
          node.static = false
        }
      }
    }
  }
}

function isStatic (node: ASTNode): boolean {
  // 表达式一定不是静态节点
  if (node.type === 2) { // expression
    return false
  }
  // 纯文本节点一定是静态的
  if (node.type === 3) { // text
    return true
  }
  // vpre 或者 没有绑定值、没有v-if、没有v-for、不是slot、template节点、是html或svg保留的标签(非组件)
  // 不是v-for的template的子节点
  // 任何属性都满足静态的情况
  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)
  ))
}
复制代码

这里能够得到第二个问题(怎样的节点才算是静态节点)的答案:

  • 纯文本;
  • node.prev-pre 指令的内容是静态节点;
  • 没有绑定值、没有 v-if、没有 v-for、不是 slottemplate 节点、是 htmlsvg 保留的标签(非组件),不是 v-fortemplate 子节点、任一属性都是静态的;
  • 对一任意节点,如果孩子节点不是静态节点,那么它就不是静态节点。

回到 ? 中:

<div id="app">
    <!-- 这是一个注释节点 -->
    <Child name="yjc" :age="12" v-if="isShow"></Child>
    <input type="text" v-model="inputValue" />
    <div class="abc"></div>
</div>
复制代码

根据上面静态节点的范畴,那么静态节点有 3 个:

staticNode.jpg

markStaticRoots

第二个主流程是标记静态根节点,什么是静态根节点呢?先看下函数逻辑:

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);
      }
    }
  }
}
复制代码

函数递归调用 markStaticRoots ,如果节点是静态节点并且是 node.once (即 v-once 作用的节点),会加上标记 node.staticInFor = isInFor。如果一个节点在满足自身是静态节点且是普通节点的情况下,如果它的孩子节点不全是文本节点(type === 3)的情况下,那么它就是一个静态根节点。⚠️ 可以看到上述代码的注释,标记这种条件下的静态根节点会有重新更新性能。? 中没有这种节点。所以所有普通节点(type === 1)都会被标记 staticRoot = false

小结

optimize 通过递归的方式给每个节点标记 static 字段,对于满足静态判断条件的节点标记 static: true 。在静态节点的基础上,如果一个普通节点含有一个非纯文本的静态节点时,那么该节点就会标记为静态根节点,标记 staticRoot:true

generate

万事俱备,只欠东风。参谋了很多网上编译的文章,到这一步时可能写累了,都草草地把生成的 render 代码贴上来就做总结了。generate 过程一句话概括起来就是“识别 AST 中的各个字段,经过一系列处理之后转成 render 函数。”这个过程条件判断非常多,这里我们按照 ? 中的 AST 来一步一步走完 generate 过程。

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}
复制代码

入口先创建一个 CodegenState 的实例 state,该实例的作用我们在后面用到的时候再分析。然后调用 genElement 去生成最终的 code

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.staticRoot && !el.staticProcessed) { // 静态根节点
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {  // v-once
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {  // v-for
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {    // v-if
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget) { // template
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {       // slot
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {

      // 生成根节点
      const data = el.plain ? undefined : genData(el, state)

      // 生成孩子节点
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}
复制代码

genElement 判断节点上各个字段,然后做不同的 genXXX 处理。? 生成的 AST 如下截图所示:

rootDiv-ast.jpg

根节点的 AST 属性会执行到 const data = el.plain ? undefined : genData(el, state) 这行代码,进到 genData 里:

export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'

  // directives first.
  // directives may mutate the el's other properties before they are generated.
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','

  // ... 一堆 if,对于当前 AST 执行不到的逻辑先剔除
  // attributes
  if (el.attrs) {
    data += `attrs:{${genProps(el.attrs)}},`
  }
  // ... 一堆 if,对于当前 AST 执行不到的逻辑先剔除
  data = data.replace(/,$/, '') + '}'
  // ...
  return data
}
复制代码

根节点 so easy,就只有 id = app 这个 attrs。最终 return "{attrs:{\"id\":\"app\"}}"。下一步就是遍历 children 去生成子节点的 render 函数,会执行到

const children = el.inlineTemplate ? null : genChildren(el, state, true)
复制代码

? 不是内联模板,所以执行到 genChildren(el, state, true)

export function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  const children = el.children
  if (children.length) {
    const el: any = children[0]
    // optimize single v-for
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      return (altGenElement || genElement)(el, state)
    }
    /**
     * 获取规范化的类型
     * 0 不需要规范化
     * 1 简单的规范化即可(可能是一级的嵌套数组)  -->  子节点 v-if 存在组件
     * 2 完全的规范化  -->  子节点 v-if 并且有 v-for、或者 template 或者 tag 标签
     */
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    const gen = altGenNode || genNode
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}
复制代码

? 中有 child 组件,所以规划化类型是 1。这个有什么用呢?留作悬念!

然后每个子组件循环调用 genNode 函数,去生成各自的 render 函数。

function genNode (node: ASTNode, state: CodegenState): string {
  // 普通节点
  if (node.type === 1) {
    return genElement(node, state)
  // 注释节点
  } if (node.type === 3 && node.isComment) {
    return genComment(node)
  // 文本节点
  } else {
    return genText(node)
  }
}
复制代码

第一个节点是 child,这个节点有 v-if 指令,有点特色,老规矩我先把节点的 AST 截图丢上来:

child-ast.jpg

下面就一起看看是怎么处理这个指令,genNode -> genElement

// ... 
// 存在 v-if,并且没有被标记过
else if (el.if && !el.ifProcessed) {    // v-if
     return genIf(el, state)
}
// ...
复制代码

进入 genIf

export function genIf (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  // 做标记,避免递归
  el.ifProcessed = true // avoid recursion
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
复制代码

进入 genIfConditions

function genIfConditions (
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  if (!conditions.length) {
    return altEmpty || '_e()'
  }

  const condition = conditions.shift()
  if (condition.exp) {
    return `(${condition.exp})?${
      genTernaryExp(condition.block)
    }:${
      genIfConditions(conditions, state, altGen, altEmpty)
    }`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  function genTernaryExp (el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}
复制代码

? 中的 condition.expisShow,所以会进入 if 逻辑,调用 genTernaryExpgenIfConditions

先看 genTernaryExp ,会依次执行 genElement(不同的是此时的 el.ifProcessed 已经是 true 了,所以流程跟上面的 div 节点一毛一样) -> genData,最后生成的代码是:

"_c('child',{attrs:{"name":"yjc","age":12}})"
复制代码

最后看 genIfConditions,? 中的 condition 此时为 0。所以直接返回 _e()。最终这个节点生成的代码:

isShow ? _c('Child', {
    attrs: {
        "name": "yjc",
        "age": 12
    }
}) : _e()
复制代码

第二个孩子节点是空格节点:

{
    text: " ",
    type: 3,
    static: true
}
复制代码

执行到 genText

export function genText (text: ASTText | ASTExpression): string {
  return `_v(${text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}
复制代码

生成的代码:

"_v(\" \")"
复制代码

第三个孩子节点也比较有特色,有 v-model 指令,这个处理起来可谓是非常复杂的了。事不宜迟,先看下 AST

input-ast.jpg

genNode -> genElement -> genData,前面两步都是一样的,到了 getData 时,因为有 directives,所以会执行到 genDirectives

function genDirectives (el: ASTElement, state: CodegenState): string | void {
  const dirs = el.directives
  if (!dirs) return
  let res = 'directives:['
  let hasRuntime = false
  let i, l, dir, needRuntime
  for (i = 0, l = dirs.length; i < l; i++) {
    dir = dirs[i]
    needRuntime = true
      
    // modal 定义,定义在 src\platforms\web\compiler\directives\model.js
    const gen: DirectiveFunction = state.directives[dir.name]
    if (gen) {
      // compile-time directive that manipulates AST.
      // returns true if it also needs a runtime counterpart.
      needRuntime = !!gen(el, dir, state.warn)
    }
    if (needRuntime) {
      hasRuntime = true
      res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
        dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
      }${
        dir.arg ? `,arg:"${dir.arg}"` : ''
      }${
        dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
      }},`
    }
  }
  if (hasRuntime) {
    return res.slice(0, -1) + ']'
  }
}
复制代码

看到 gen 函数的定义,也就是 modal 指令的函数定义:

export default function model (
  el: ASTElement,
  dir: ASTDirective,
  _warn: Function
): ?boolean {
  warn = _warn
  const value = dir.value
  const modifiers = dir.modifiers
  const tag = el.tag
  const type = el.attrsMap.type

  // ...
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers)
  }
  // ...
  return true
}
复制代码

省略掉判断是否组件 v-model、是否 inputcheckboxradiofile 的组合、是否 select 的判断。看到我们 ? 中的 input,进入 genDefaultModel

function genDefaultModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
): ?boolean {
  const type = el.attrsMap.type

  // ...
  const { lazy, number, trim } = modifiers || {}
  const needCompositionGuard = !lazy && type !== 'range'
  const event = lazy
    ? 'change'
    : type === 'range'
      ? RANGE_TOKEN
      : 'input'

  let valueExpression = '$event.target.value'
  
  // v-model.trim 处理去除空格修饰符
  if (trim) {
    valueExpression = `$event.target.value.trim()`
  }
      
  // v-model.number 数字化
  if (number) {
    valueExpression = `_n(${valueExpression})`
  }

  let code = genAssignmentCode(value, valueExpression)
  if (needCompositionGuard) {
    code = `if($event.target.composing)return;${code}`
  }

  addProp(el, 'value', `(${value})`)
  addHandler(el, event, code, null, true)
  if (trim || number) {
    addHandler(el, 'blur', '$forceUpdate()')
  }
}
复制代码

先对 lazynumbertrim 3个修饰符做了处理,最后通过 addPropaddHandlerAST 加上 valueinput 事件。v-model 是语法糖就是这么一个道理:

export function addProp (el: ASTElement, name: string, value: string) {
  (el.props || (el.props = [])).push({ name, value })
  el.plain = false
}


export function addHandler (
  el: ASTElement,
  name: string,
  value: string,
  modifiers: ?ASTModifiers,
  important?: boolean,
  warn?: Function
) {
  modifiers = modifiers || emptyObject

  // ...

  let events
  if (modifiers.native) {
    delete modifiers.native
    events = el.nativeEvents || (el.nativeEvents = {})
  } else {
    events = el.events || (el.events = {})
  }

  // ...  
  const handlers = events[name]
  /* istanbul ignore if */
  if (Array.isArray(handlers)) {
    important ? handlers.unshift(newHandler) : handlers.push(newHandler)
  } else if (handlers) {
    events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
  } else {
    events[name] = newHandler
  }

  el.plain = false
}

复制代码

去掉了不关键的修饰符逻辑跟日志,上面两个函数的逻辑就简单了。生成的 AST 如下:

v-model-afterhandler-ast.jpg

AST 处理完了,回到 genDirectives 中,最终该函数返回的 res 是下面这样一个字符串:

"directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}]"
复制代码

再往上回到 genData,会处理 propsevents 字段:

// DOM props
if (el.props) {
    data += "domProps:{" + (genProps(el.props)) + "},";
}
// event handlers
if (el.events) {
    data += (genHandlers(el.events, false, state.warn)) + ",";
}
复制代码

props 跟上面 attrs 的处理一样,看一下 genHandlers

function genHandlers (
  events,
  isNative,
  warn
) {
  var res = isNative ? 'nativeOn:{' : 'on:{';
  for (var name in events) {
    res += "\"" + name + "\":" + (genHandler(name, events[name])) + ",";
  }
  return res.slice(0, -1) + '}'
}
复制代码

把事件函数挂在 on 字段上,然后将事件逻辑用 genHandler 包起来,这个函数的逻辑有很多事件处理,比如键盘的 key ,事件修饰符等,因为 ? 中不涉及,直接贴生成后的代码 :

"on:{"input":function($event){if($event.target.composing)return;inputValue=$event.target.value}}"
复制代码

最终 input 节点生成的代码:

"_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}],attrs:{\"type\":\"text\"},domProps:{\"value\":(inputValue)},on:{\"input\":function($event){if($event.target.composing)return;inputValue=$event.target.value}}})"
复制代码

最后两个 AST 都比较简单,这里就不展开讲了,有兴趣的童鞋冲一杯 :coffee: 单步调试一下吧。至此,整个 generate 过程就结束了,生成的完整 render 如下:

"with(this){return _c('div',{attrs:{\"id\":\"app\"}},[(isShow)?_c('child',{attrs:{\"name\":\"yjc\",\"age\":12}}):_e(),_v(\" \"),_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}],attrs:{\"type\":\"text\"},domProps:{\"value\":(inputValue)},on:{\"input\":function($event){if($event.target.composing)return;inputValue=$event.target.value}}}),_v(\" \"),_c('div',{staticClass:\"abc\"})],1)}"
复制代码

小结

generate 通过字段匹配、处理,将 optimize 之后的 AST 转换成 render code。整个过程有太多的叉枝,没办法一次性全部讲到位。通过 ? 分析了 v-ifv-model 的生成过程,render 的过程肯定都能够有个大概印象。其他的细节在遇到具体问题时,在恰当的位置进行单步调试,相信很快就能解决问题咯。

总结

整个模板编译过程能够分成 4 卷:

  • 创建编译器,因为不同的平台(webweex)有不一样的编译处理,所以将这种差异在入口处抹平;
  • parse 阶段,通过正则匹配将 template 字符串转成 AST ,期间用到的 regex101 工具,结尾再次推荐一波,嘎嘎香;???
  • optimize 阶段,标记静态节点、静态根节点,在 AST 上加上 staticstaticRoot 信息;
  • generate 阶段,通过节点上的属性符号,将 AST 生成 render 代码。

能读到这里,相信你一定对模板编译的过程有比较清晰地了解。
有问题及时指出哈! ? 红着脸及时纠错。动动小手点个赞吧 ??

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享