<!-- 这是一个注释节点 <Child n...">
最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • [咖聊]休假去取“模板编译”真经了

    正文概述 掘金(Jouryjc)   2021-06-07   424

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

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

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

    开始之前,先看一个不管任何匹配都会调用的函数 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 比较简单,没有其他逻辑分支处理,就直接贴上结果图:

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

          <!-- 这是一个注释节点 -->
          <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 结果如下:

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

      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 属性;后者通过 dirRE 和 bindRE 去掉 : 符号之后添加到 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>
          <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 如下图所示:

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

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

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

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

    小结

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

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

    然后在匹配过程中调用各自的回调函数去生成 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 个:

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

    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 如下截图所示:

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

    根节点的 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: " ",
        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

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

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

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

    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 代码。

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


    起源地下载网 » [咖聊]休假去取“模板编译”真经了

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元