Loading...
墨滴

jouryjc

2021/07/11  阅读:30  主题:橙心

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

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

本文的 🍪 (表示 例子,☕️ 和 🍪 更配哦!全文都会围绕这个 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: {
    nameString,

    ageNumber
  },

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

new Vue({
  el'#app',

  components: {
    Child
  },

  data () {
    return {
      isShowtrue,
      inputValue'123123'
    };
  }
})

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

拿起 ☕️ ,让我们看看是从哪里开始执行模板编译的。回忆一下 [咖聊]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,需要绕大半个“地球”,最终进入到真正的编译:

 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::

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

开始之前,先看一个不管任何匹配都会调用的函数 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>

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

开始标签 <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
    }
  }
}

匹配开始标签名,此时会创建一个 match 对象;匹配开始标签中的属性,给 match 中的 attrs 添加属性 match 的结果;匹配开始标签的结尾 > 字符,将匹配分组信息和结尾位置分别记录到match.unarySlashmatch.end 中。

紧接着对 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 比较简单,没有其他逻辑分支处理,就直接贴上结果图:

到此开始标签 <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>

在解析注释节点之前,我们可以看到有一系列空格,这个处理也比较简单,就是看当前 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 < 0break
    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({
        type2,
        expression: res.expression,
        tokens: res.tokens,
        text
      })
    } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
      children.push({
        type3,
        text
      })
    }
  }
},

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

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

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
  }
}

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

new Vue({
  el'#app',

  components: {
    Child
  },

  // 注意:这里可以配置保存注释信息
  commentstrue,

  data () {
    return {
      isShowtrue,
      inputValue''
    };
  }
})

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

    <Child name="yjc" :age="12" v-if="isShow"></Child>
    <input type="text" v-model="inputValue" />
  <div class="abc"></div>
</div>

处理空白字符,重复步骤2。

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

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

然后执行到 options.start 函数,跟上面 div 相同的逻辑这里就不叙述了。Childdiv 有几点不一样的是: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
    }
  }
}

属性的 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 中。

编译 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>

闭合标签 </Child> 的处理过程:先用闭合标签正则惰性地匹配,这个正则就是在开始标签正则的基础上加了一个 / ;然后用 advance 剔除闭合标签;通过 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>

至此,开始标签、标签属性、闭合标签等都已经通过源码过了一遍,对于下一个 input 节点,我们就看 v-model 和自闭合标签的处理:parseStartTag 和之前的流程一样;执行到 handleStartTagconst unary = isUnaryTag(tagName) || !!unarySlash 时,这里返回的是 true;自闭合标签因为不用匹配闭合标签,所以不需要入栈。直接执行 options.start;生成 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 如下图所示:

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

  <div class="abc"></div>
</div>

最终剩下的模板就非常简单了,就是重复前面的过程处理即可。这里就不写了。(其实这个节点是为了后面的 optimize 做铺垫。:laughing::laughing:)当 html 只剩下 "" 时,最终会再执行一次 parseEndTag,用于栈中清理剩余的标签。

小结

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

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

  • 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 个:

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)的情况下,那么它就是一个静态根节点。:warning: 可以看到上述代码的注释,标记这种条件下的静态根节点会有重新更新性能。🍪 中没有这种节点。所以所有普通节点(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 如下截图所示:

根节点的 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 截图丢上来:

下面就一起看看是怎么处理这个指令,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" ",
    type3,
    statictrue
}

执行到 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

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, nulltrue)
  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 如下:

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 都比较简单,这里就不展开讲了,有兴趣的童鞋冲一杯 ☕️ 单步调试一下吧。至此,整个 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 工具,结尾再次推荐一波,嘎嘎香;:happy::happy::happy:
  • optimize 阶段,标记静态节点、静态根节点,在 AST 上加上 staticstaticRoot 信息;
  • generate 阶段,通过节点上的属性符号,将 AST 生成 render 代码。

jouryjc

2021/07/11  阅读:30  主题:橙心

作者介绍

jouryjc