前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >[咖聊] “模板编译”真经

[咖聊] “模板编译”真经

作者头像
码农小余
发布2022-06-16 16:34:53
发布2022-06-16 16:34:53
1.1K00
代码可运行
举报
文章被收录于专栏:码农小余码农小余
运行总次数:0
代码可运行

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

本文的 🍪 (表示 例子,☕️ 和 🍪 更配哦!全文都会围绕这个 DEMO 做解析。⚠️ 因不能直接跳转到外链,注意有标注的地方,文末有对应的地址哦,跳转着看更容易理解哦!):

代码语言:javascript
代码运行次数:0
运行
复制
<div id="app">
    <!-- 这是一个注释节点 -->
    <Child name="yjc" :age="12" v-if="isShow"></Child>
    <input type="text" v-model="inputValue" />
    <div class="abc"></div>
</div>
代码语言:javascript
代码运行次数:0
运行
复制
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 属性、自闭合标签。

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

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

代码语言:javascript
代码运行次数:0
运行
复制
/**
 * 挂载组件,带模板编译
 */
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,需要绕大半个“地球”,最终进入到真正的编译:

代码语言:javascript
代码运行次数:0
运行
复制
 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的过程”。

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

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

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

代码语言:javascript
代码运行次数:0
运行
复制
function advance (n) {
    index += n
    html = html.substring(n)
}

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

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

代码语言:javascript
代码运行次数:0
运行
复制
<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">:

代码语言:javascript
代码运行次数:0
运行
复制
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
    }
  }
}

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

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

代码语言:javascript
代码运行次数:0
运行
复制
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

代码语言:javascript
代码运行次数:0
运行
复制
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 的递进处理,变成了下面这般模样:

代码语言:javascript
代码运行次数:0
运行
复制
    <!-- 这是一个注释节点 -->
    <Child name="yjc" :age="12" v-if="isShow"></Child>
    <input type="text" v-model="inputValue" />
  <div class="abc"></div>
</div>

在解析注释节点之前,我们可以看到有一系列空格,这个处理也比较简单,就是看当前 textEnd (🍪 中 < 的位置),然后判断是大于 0 的情况,将这些空白字符去掉就行了:

代码语言:javascript
代码运行次数: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

代码语言:javascript
代码运行次数:0
运行
复制
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 主流程上啦。😅

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

代码语言:javascript
代码运行次数:0
运行
复制
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
  }
}

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

代码语言:javascript
代码运行次数:0
运行
复制
new Vue({
  el: '#app',

  components: {
    Child
  },

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

  data () {
    return {
      isShow: true,
      inputValue: ''
    };
  }
})

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

代码语言:javascript
代码运行次数:0
运行
复制
    <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 字段;

代码语言:javascript
代码运行次数:0
运行
复制
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"> 的时候略过了,现在来看看:

代码语言:javascript
代码运行次数:0
运行
复制
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 属性;后者通过 dirRE6 和 bindRE7 去掉 : 符号之后添加到 attrs 中。

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

代码语言:javascript
代码运行次数:0
运行
复制
// 解析到 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 节点后的结果:

代码语言:javascript
代码运行次数:0
运行
复制
</Child>
    <input type="text" v-model="inputValue" />
  <div class="abc"></div>
</div>

闭合标签 </Child> 的处理过程:先用闭合标签正则8惰性地匹配,这个正则就是在开始标签正则的基础上加了一个 / ;然后用 advance 剔除闭合标签;通过 parseEndTagoptions.end 去更新标签和 ASTstack

代码语言:javascript
代码运行次数:0
运行
复制
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

代码语言:javascript
代码运行次数:0
运行
复制
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> 之后的结果:

代码语言:javascript
代码运行次数:0
运行
复制
    <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

代码语言:javascript
代码运行次数:0
运行
复制
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 只剩下:

代码语言:javascript
代码运行次数:0
运行
复制
  <div class="abc"></div>
</div>

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

小结

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

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

  • 1 表示的是普通元素;
  • 2 表示表达式;
  • 3 表示纯文本

optimize

本小节目标:

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

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

代码语言:javascript
代码运行次数:0
运行
复制
if (options.optimize !== false) {
    optimize(ast, options)
}

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

代码语言:javascript
代码运行次数:0
运行
复制
/**
 * 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)

代码语言:javascript
代码运行次数:0
运行
复制
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 子节点、任一属性都是静态的;
  • 对一任意节点,如果孩子节点不是静态节点,那么它就不是静态节点。

回到 🍪 中:

代码语言:javascript
代码运行次数:0
运行
复制
<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

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

代码语言:javascript
代码运行次数:0
运行
复制
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 过程。

代码语言:javascript
代码运行次数:0
运行
复制
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

代码语言:javascript
代码运行次数:0
运行
复制
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 里:

代码语言:javascript
代码运行次数:0
运行
复制
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 函数,会执行到

代码语言:javascript
代码运行次数:0
运行
复制
const children = el.inlineTemplate ? null : genChildren(el, state, true)

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

代码语言:javascript
代码运行次数:0
运行
复制
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 函数。

代码语言:javascript
代码运行次数:0
运行
复制
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

代码语言:javascript
代码运行次数:0
运行
复制
// ... 
// 存在 v-if,并且没有被标记过
else if (el.if && !el.ifProcessed) {    // v-if
     return genIf(el, state)
}
// ...

进入 genIf

代码语言:javascript
代码运行次数:0
运行
复制
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

代码语言:javascript
代码运行次数:0
运行
复制
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,最后生成的代码是:

代码语言:javascript
代码运行次数:0
运行
复制
"_c('child',{attrs:{"name":"yjc","age":12}})"

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

代码语言:javascript
代码运行次数:0
运行
复制
isShow ? _c('Child', {
    attrs: {
        "name": "yjc",
        "age": 12
    }
}) : _e()

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

代码语言:javascript
代码运行次数:0
运行
复制
{
    text: " ",
    type: 3,
    static: true
}

执行到 genText

代码语言:javascript
代码运行次数:0
运行
复制
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))
  })`
}

生成的代码:

代码语言:javascript
代码运行次数:0
运行
复制
"_v(\" \")"

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

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

代码语言:javascript
代码运行次数:0
运行
复制
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 指令的函数定义:

代码语言:javascript
代码运行次数:0
运行
复制
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

代码语言:javascript
代码运行次数:0
运行
复制
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 是语法糖就是这么一个道理:

代码语言:javascript
代码运行次数:0
运行
复制
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 是下面这样一个字符串:

代码语言:javascript
代码运行次数:0
运行
复制
"directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}]"

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

代码语言:javascript
代码运行次数:0
运行
复制
// 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

代码语言:javascript
代码运行次数:0
运行
复制
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 ,事件修饰符等,因为 🍪 中不涉及,直接贴生成后的代码 :

代码语言:javascript
代码运行次数:0
运行
复制
"on:{"input":function($event){if($event.target.composing)return;inputValue=$event.target.value}}"

最终 input 节点生成的代码:

代码语言:javascript
代码运行次数:0
运行
复制
"_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 如下:

代码语言:javascript
代码运行次数:0
运行
复制
"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 代码。

标注链接:

  1. regex101:https://regex101.com/
  2. 开始标签名:https://regex101.com/r/OF2uqU/1
  3. 属性:https://regex101.com/r/pHBNNi/1
  4. 结尾 > 字符:https://regex101.com/r/Oup1Ef/1
  5. 注释节点的开头:https://regex101.com/r/gWMsTl/1
  6. dirRE:https://regex101.com/r/dPE6Md/1)
  7. bindRE:https://regex101.com/r/0fYDna/1
  8. 闭合标签正则:https://regex101.com/r/hBAfCG/1
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-06-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农小余 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • parse
    • 小结
  • optimize
    • markStatic
    • markStaticRoots
    • 小结
  • generate
    • 小结
  • 总结
  • 标注链接:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档