关于javascript:Vue源码解读七模板编译

35次阅读

共计 52864 个字符,预计需要花费 133 分钟才能阅读完成。

在最开始的章节提到过,咱们在应用 vue-cli 创立我的项目的时候,提供了两个版本供咱们应用,Runtime Only 版本和 Runtime + Compiler 版本。Runtime Only 版本是不蕴含编译器的,在我的项目打包的时候会把模板编译成 render 函数,也叫预编译。Runtime + Compiler 版本蕴含编译器,能够把编译过程放在运行时做。

入口

这一块代码量比拟多,次要是对各种状况做了一些边界解决。这里只关注主流程。对细节感兴趣的搭档们能够自行去钻研。个别咱们应用 Runtime + Compiler 版本可能比拟多一些,先来找到入口:

// src/platforms/web/entry-runtime-with-compiler.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // ...
  if (!options.render) {
    // 模版就绪,进入编译阶段
    if (template) {
      // 编译模版,失去 动静渲染函数和动态渲染函数
      const {render, staticRenderFns} = compileToFunctions(template, {
        // 在非生产环境下,编译时记录标签属性在模版字符串中开始和完结的地位索引
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        // 界定符,默认 {{}}
        delimiters: options.delimiters,
        // 是否保留正文
        comments: options.comments
      }, this)
    }
  }
}

compileToFunctions  办法就是把 template  编译而失去 render 以及 staticRenderFns

compileToFunctions

// src/platforms/web/compiler/index.js
import {baseOptions} from './options'
import {createCompiler} from 'compiler/index'

const {compile, compileToFunctions} = createCompiler(baseOptions)

export {compile, compileToFunctions}

createCompiler

// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 将模版解析为 AST
  const ast = parse(template.trim(), options)
  // 优化 AST,动态标记
  if (options.optimize !== false) {optimize(ast, options)
  }
  // 生成渲染函数,,将 ast 转换成可执行的 render 函数的字符串模式
  // code = {//   render: `with(this){return ${_c(tag, data, children, normalizationType)}}`,
  //   staticRenderFns: [_c(tag, data, children, normalizationType), ...]
  // }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

createCompiler 是通过调用 createCompilerCreator 返回的,这里传入了一个 baseCompile 函数作为参数,这个函数是重点,编译的外围过程就是在这个函数中执行的。

createCompilerCreator

// src/compiler/create-compiler.js
export function createCompilerCreator (baseCompile: Function): Function {return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      // 以平台特有的编译配置为原型创立编译选项对象
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []
      // 日志,负责记录将 error 和 tip
      let warn = (msg, range, tip) => {(tip ? tips : errors).push(msg)
      }
      if (options) {if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          // $flow-disable-line
          const leadingSpaceLength = template.match(/^\s*/)[0].length
          warn = (msg, range, tip) => {const data: WarningMessage = { msg}
            if (range) {if (range.start != null) {data.start = range.start + leadingSpaceLength}
              if (range.end != null) {data.end = range.end + leadingSpaceLength}
            }
            (tip ? tips : errors).push(data)
          }
        }
        // 合并配置项 options 到 finalOptions
        // merge custom modules
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }
        // merge custom directives
        if (options.directives) {
          finalOptions.directives = extend(Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        // copy other options
        for (const key in options) {if (key !== 'modules' && key !== 'directives') {finalOptions[key] = options[key]
          }
        }
      }
      finalOptions.warn = warn
      // 外围编译函数,传递模版字符串和最终的配置项,失去编译后果
      const compiled = baseCompile(template.trim(), finalOptions)
      if (process.env.NODE_ENV !== 'production') {detectErrors(compiled.ast, warn)
      }
      // 将编译期间产生的谬误和提醒挂载到编译后果上
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }
    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

createCompilerCreator 返回了一个 createCompiler 函数,createCompiler 返回了一个对象,包含了 compilecompileToFunctions,这个 compileToFunctions 对应的就是 $mount 中调用的 compileToFunctions 办法。在 createCompiler 函数内定义了 compile 办法,并把它传递给 createCompileToFunctionFncompile 次要目标就是对特有平台的配置项做一些合并,如 web 平台和解决一些在编译期间产生的谬误。

createCompileToFunctionFn

// src/compiler/to-function.js
export function createCompileToFunctionFn (compile: Function): Function {const cache = Object.create(null)
  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 传递进来的编译选项
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      // detect possible CSP restriction
      // 检测可能的 CSP 限度
      try {new Function('return 1')
      } catch (e) {if (e.toString().match(/unsafe-eval|CSP/)) {
          warn(
            'It seems you are using the standalone build of Vue.js in an' +
            'environment with Content Security Policy that prohibits unsafe-eval.' +
            'The template compiler cannot work in this environment. Consider' +
            'relaxing the policy to allow unsafe-eval or pre-compiling your' +
            'templates into render functions.'
          )
        }
      }
    }
    // check cache
    // 如果有缓存,则跳过编译,间接从缓存中获取上次编译的后果
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {return cache[key]
    }
    // compile
    // 执行编译函数,失去编译后果
    const compiled = compile(template, options)
    // check compilation errors/tips
    // 查看编译期间产生的 errors/tips,别离输入到控制台
    if (process.env.NODE_ENV !== 'production') {if (compiled.errors && compiled.errors.length) {if (options.outputSourceRange) {
          compiled.errors.forEach(e => {
            warn(`Error compiling template:\n\n${e.msg}\n\n` +
              generateCodeFrame(template, e.start, e.end),
              vm
            )
          })
        } else {
          warn(`Error compiling template:\n\n${template}\n\n` +
            compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
            vm
          )
        }
      }
      if (compiled.tips && compiled.tips.length) {if (options.outputSourceRange) {compiled.tips.forEach(e => tip(e.msg, vm))
        } else {compiled.tips.forEach(msg => tip(msg, vm))
        }
      }
    }
    // turn code into functions
    // 转换编译失去的字符串代码为函数,通过 new Function(code) 实现
    const res = {}
    const fnGenErrors = []
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {return createFunction(code, fnGenErrors)
    })
    // check function generation errors.
    // this should only happen if there is a bug in the compiler itself.
    // mostly for codegen development use
    /* istanbul ignore if */
    // 解决下面代码转换过程中呈现的谬误
    if (process.env.NODE_ENV !== 'production') {if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn(
          `Failed to generate render function:\n\n` +
          fnGenErrors.map(({err, code}) => `${err.toString()} in\n\n${code}\n`).join('\n'),
          vm
        )
      }
    }
    // 缓存编译后果
    return (cache[key] = res)
  }
}

createCompileToFunctionFn 返回了 compileToFunctions 这个就是咱们要找的最终定义所在了,它次要做了这么几件事:

  • 执行编译函数失去编译后果。
  • 将编译失去的字符串代码转换成可执行的函数。
  • 解决异样
  • 缓存

小结

通过以上的代码能够看出真正编译个过程就在 createCompilerCreator 函数传递的 baseCompile 中,次要分为这么几个局部:

  • 将模板解析成 AST

    const ast = parse(template.trim(), options)
  • 优化 AST (动态标记)

    optimize(ast, options)
  • 生成代码字符串

    const code = generate(ast, options)

之所以在真正编译之前做了这么多前戏,目标就是为了对不同平台做一些解决。上面次要针对这三个局部看看做了一些什么事件。

parse

parse 次要作用就是将模板解析成 AST,它是一种形象语法树。这个过程比较复杂,它会在每个节点的 AST 对象上设置元素的所有信息,比方:父节点、子节点、标签信息、属性信息、插槽信息等等。在期间会用到许多正则表达式对模板进行匹配。

parse

// src/compiler/parser/index.js
export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  warn = options.warn || baseWarn
  // 是否为 pre 标签
  platformIsPreTag = options.isPreTag || no
  // 必须应用 props 进行绑定的属性
  platformMustUseProp = options.mustUseProp || no
  // 获取标签的命名空间
  platformGetTagNamespace = options.getTagNamespace || no
  // 是否是保留标签(html + svg)
  const isReservedTag = options.isReservedTag || no
  // 是否为一个组件
  maybeComponent = (el: ASTElement) => !!(
    el.component ||
    el.attrsMap[':is'] ||
    el.attrsMap['v-bind:is'] ||
    !(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag))
  )
  // 别离获取 options.modules 下的 class、model、style 三个模块中的 transformNode、preTransformNode、postTransformNode 办法
  // 负责解决元素节点上的 class、style、v-model
  transforms = pluckModuleFunction(options.modules, 'transformNode')
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
  
  // 界定符,比方: {{}}
  delimiters = options.delimiters
  
  const stack = []
  // 空格选项
  const preserveWhitespace = options.preserveWhitespace !== false
  const whitespaceOption = options.whitespace
  // 根节点,以 root 为根,解决后的节点都会依照层级挂载到 root 下,最初 return 的就是 root,一个 ast 语法树
  let root
  // 以后元素的父元素
  let currentParent
  let inVPre = false
  let inPre = false
  let warned = false
  function warnOnce (msg, range) {if (!warned) {
      warned = true
      warn(msg, range)
    }
  }
  
  // 因为代码比拟长,前面几个办法独自拿进去。function closeElement (element) {/*...*/}
  
  function trimEndingWhitespace (el) {/*...*/}
  
  function checkRootConstraints (el) {/*...*/}
  
  parseHTML(template, {/*...*/})
  return root
}

parse 接管两个参数 templateoptions,也就是模板字符串和配置选项,options 定义在 /src/platforms/web/compiler/options 中,这里次要是不同的平台(web 和 weex)的配置选项不同。

closeElement

// src/compiler/parser/index.js
function closeElement (element) {
  // 移除节点开端的空格
  trimEndingWhitespace(element)
  // 以后元素不再 pre 节点内,并且也没有被解决过
  if (!inVPre && !element.processed) {
    // 别离解决元素节点的 key、ref、插槽、自闭合的 slot 标签、动静组件、class、style、v-bind、v-on、其它指令和一些原生属性 
    element = processElement(element, options)
  }
  // 解决根节点上存在 v-if、v-else-if、v-else 指令的状况
  // 如果根节点存在 v-if 指令,则必须还提供一个具备 v-else-if 或者 v-else 的同级别节点,避免根元素不存在
  // tree management
  if (!stack.length && element !== root) {
    // allow root elements with v-if, v-else-if and v-else
    if (root.if && (element.elseif || element.else)) {if (process.env.NODE_ENV !== 'production') {checkRootConstraints(element)
      }
      addIfCondition(root, {
        exp: element.elseif,
        block: element
      })
    } else if (process.env.NODE_ENV !== 'production') {
      warnOnce(
        `Component template should contain exactly one root element. ` +
        `If you are using v-if on multiple elements, ` +
        `use v-else-if to chain them instead.`,
        {start: element.start}
      )
    }
  }
  // 建设父子关系,将本人放到父元素的 children 数组中,而后设置本人的 parent 属性为 currentParent
  if (currentParent && !element.forbidden) {if (element.elseif || element.else) {processIfConditions(element, currentParent)
    } else {if (element.slotScope) {
        // scoped slot
        // keep it in the children list so that v-else(-if) conditions can
        // find it as the prev node.
        const name = element.slotTarget || '"default"'
          ; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
      }
      currentParent.children.push(element)
      element.parent = currentParent
    }
  }
  // 设置子元素,将本人的所有非插槽的子元素设置到 element.children 数组中
  // final children cleanup
  // filter out scoped slots
  element.children = element.children.filter(c => !(c: any).slotScope)
  // remove trailing whitespace node again
  trimEndingWhitespace(element)
  // check pre state
  if (element.pre) {inVPre = false}
  if (platformIsPreTag(element.tag)) {inPre = false}
  // 别离为 element 执行 model、class、style 三个模块的 postTransform 办法
  // 然而 web 平台没有提供该办法
  // apply post-transforms
  for (let i = 0; i < postTransforms.length; i++) {postTransforms[i](element, options)
  }
}

closeElement 办法次要做了三件事:

  • 如果元素没有被解决,调用 processElement 解决元素上的一些属性。
  • 设置以后元素的父元素。
  • 设置以后元素的子元素。

trimEndingWhitespace

// src/compiler/parser/index.js
function trimEndingWhitespace (el) {
  // remove trailing whitespace node
  if (!inPre) {
    let lastNode
    while ((lastNode = el.children[el.children.length - 1]) &&
      lastNode.type === 3 &&
      lastNode.text === ' '
    ) {el.children.pop()
    }
  }
}

trimEndingWhitespace 作用就是删除元素中空白的文本节点。

checkRootConstraints

// src/compiler/parser/index.js
function checkRootConstraints (el) {if (el.tag === 'slot' || el.tag === 'template') {
    warnOnce(`Cannot use <${el.tag}> as component root element because it may ` +
      'contain multiple nodes.',
      {start: el.start}
    )
  }
  if (el.attrsMap.hasOwnProperty('v-for')) {
    warnOnce(
      'Cannot use v-for on stateful component root element because' +
      'it renders multiple elements.',
      el.rawAttrsMap['v-for']
    )
  }
}

checkRootConstraints 的作用是对根元素的查看,不能应用 slottemplate 作为根元素,不能在有状态组件的根元素上应用 v-for,因为它会渲染出多个元素。

parseHTML

// src/compiler/parser/index.js
parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    /**
     * 开始解决标签
     * @param {*} tag 标签名
     * @param {*} attrs [{name: attrName, value: attrVal, start, end}, ...] 模式的属性数组
     * @param {*} unary 自闭合标签
     * @param {*} start 标签在 html 字符串中的开始索引
     * @param {*} end 标签在 html 字符串中的完结索引
     */
    start (tag, attrs, unary, start, end) {
      // 如果存在命名空间,,则继承父命名空间
      // check namespace.
      // inherit parent ns if there is one
      const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
      // handle IE svg bug
      /* istanbul ignore if */
      if (isIE && ns === 'svg') {attrs = guardIESVGBug(attrs)
      }
      // 创立 AST 对象
      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      // 设置命名空间
      if (ns) {element.ns = ns}
      if (process.env.NODE_ENV !== 'production') {if (options.outputSourceRange) {
          element.start = start
          element.end = end
          element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {cumulated[attr.name] = attr
            return cumulated
          }, {})
        }
        attrs.forEach(attr => {if (invalidAttributeRE.test(attr.name)) {
            warn(
              `Invalid dynamic argument expression: attribute names cannot contain ` +
              `spaces, quotes, <, >, / or =.`,
              {start: attr.start + attr.name.indexOf(`[`),
                end: attr.start + attr.name.length
              }
            )
          }
        })
      }
      // 非服务端渲染的状况下,模版中不应该呈现 <style></style>、<script></script> 标签
      if (isForbiddenTag(element) && !isServerRendering()) {
        element.forbidden = true
        process.env.NODE_ENV !== 'production' && warn(
          'Templates should only be responsible for mapping the state to the' +
          'UI. Avoid placing tags with side-effects in your templates, such as' +
          `<${tag}>` + ', as they will not be parsed.',
          {start: element.start}
        )
      }
      // 为 element 对象别离执行 class、style、model 模块中的 preTransforms 办法
      // 解决存在 v-model 指令的 input 标签,别离解决 input 为 checkbox、radio、其它的状况
      // apply pre-transforms
      for (let i = 0; i < preTransforms.length; i++) {element = preTransforms[i](element, options) || element
      }
      if (!inVPre) {
        // 是否存在 v-pre 指令,存在则设置 element.pre = true
        processPre(element)
        if (element.pre) {
          // 存在 v-pre 指令,则设置 inVPre 为 true
          inVPre = true
        }
      }
      // 如果 pre 标签,则设置 inPre 为 true
      if (platformIsPreTag(element.tag)) {inPre = true}
      if (inVPre) {
        // 存在 v-pre 指令,这样的节点只会渲染一次,将节点上的属性都设置到 el.attrs 数组对象中,作为动态属性,数据更新时不会渲染这部分内容
        processRawAttrs(element)
      } else if (!element.processed) {
        // structural directives
        // 解决 v-for 属性
        processFor(element)
        // 解决 v-if、v-else-if、v-else
        processIf(element)
        // 解决 v-once 指令
        processOnce(element)
      }
      // root 不存在,以后解决的元素为第一个元素,即组件的 根 元素
      if (!root) {
        root = element
        if (process.env.NODE_ENV !== 'production') {
          // 查看根元素,不能应用 slot、template、v-for 
          checkRootConstraints(root)
        }
      }
      if (!unary) {
        // 非自闭合标签,通过 currentParent 记录以后元素,下一个元素在解决的时候,就晓得本人的父元素是谁
        currentParent = element
        // 而后将 element push 到 stack 数组,未来解决到以后元素的闭合标签时用
        stack.push(element)
      } else {
        // 阐明以后元素为自闭合标签
        closeElement(element)
      }
    },
    /**
      * 解决完结标签
      * @param {*} tag 完结标签的名称
      * @param {*} start 完结标签的开始索引
      * @param {*} end 完结标签的完结索引
      */
    end (tag, start, end) {
      // 完结标签对应的开始标签的 AST 对象
      const element = stack[stack.length - 1]
      // pop stack
      stack.length -= 1
      currentParent = stack[stack.length - 1]
      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {element.end = end}
      // 设置父子关系
      closeElement(element)
    },
    // 解决文本,基于文本生成 ast 对象,将该 AST 放到它的父元素的 children 中。chars (text: string, start: number, end: number) {
      // 异样解决
      if (!currentParent) {if (process.env.NODE_ENV !== 'production') {if (text === template) {
            warnOnce(
              'Component template requires a root element, rather than just text.',
              {start}
            )
          } else if ((text = text.trim())) {
            warnOnce(`text "${text}" outside root element will be ignored.`,
              {start}
            )
          }
        }
        return
      }
      // IE textarea placeholder bug
      /* istanbul ignore if */
      if (isIE &&
        currentParent.tag === 'textarea' &&
        currentParent.attrsMap.placeholder === text
      ) {return}
      const children = currentParent.children
      if (inPre || text.trim()) {// 文本在 pre 标签内 或者 text.trim() 不为空
        text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
      } else if (!children.length) {// 阐明文本不在 pre 标签内而且 text.trim() 为空,而且以后父元素也没有子节点,则将 text 置为空
        // remove the whitespace-only node right after an opening tag
        text = ''
      } else if (whitespaceOption) {
        // 压缩解决
        if (whitespaceOption === 'condense') {
          // in condense mode, remove the whitespace node if it contains
          // line break, otherwise condense to a single space
          text = lineBreakRE.test(text) ? '':' '
        } else {text = ' '}
      } else {text = preserveWhitespace ? '' :''}
      // 如果通过解决后 text 还存在
      if (text) {if (!inPre && whitespaceOption === 'condense') {
          // 不在 pre 节点中,并且配置选项中存在压缩选项,则将多个间断空格压缩为单个
          // condense consecutive whitespaces into single space
          text = text.replace(whitespaceRE, ' ')
        }
        let res
        // 基于 text 生成 AST 对象
        let child: ?ASTNode
        if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
          // 文本中存在表达式(即有界定符(占位符))child = {
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text
          }
        } else if (text !== '' || !children.length || children[children.length - 1].text !==' ') {
          // 纯文本节点
          child = {
            type: 3,
            text
          }
        }
        // push 到父元素的 children 中
        if (child) {if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
            child.start = start
            child.end = end
          }
          children.push(child)
        }
      }
    },
    // 解决正文节点
    comment (text: string, start, end) {
      // adding anything as a sibling to the root node is forbidden
      // comments should still be allowed, but ignored
      // 禁止将任何内容作为 root 的节点的同级进行增加,正文应该被容许,然而会被疏忽
      // 如果 currentParent 不存在,阐明正文和 root 为同级,疏忽
      if (currentParent) {
        // 正文节点的 AST
        const child: ASTText = {
          type: 3,
          text,
          isComment: true
        }
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          // 记录节点的开始索引和完结索引
          child.start = start
          child.end = end
        }
        // push 到父元素的 children 中
        currentParent.children.push(child)
      }
    }
  })
  return root
}

对模板的解析次要是通过 parseHTML 函数,他定义在 src/compiler/parser/html-parser 中:

// src/compiler/parser/html-parser
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
// #7298: escape - to avoid being passed as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/
// Special Elements (can contain anything)
export const isPlainTextElement = makeMap('script,style,textarea', true)
const reCache = {}
const decodingMap = {
  '&lt;': '<',
  '&gt;': '>',
  '&quot;': '"','&amp;':'&','&#10;':'\n','&#9;':'\t','&#39;':"'"
}
const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g
// #5992
const isIgnoreNewlineTag = makeMap('pre,textarea', true)
const shouldIgnoreFirstNewline = (tag, html) => tag && isIgnoreNewlineTag(tag) && html[0] === '\n'
function decodeAttr (value, shouldDecodeNewlines) {
  const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr
  return value.replace(re, match => decodingMap[match])
}
/**
 * 通过循环遍历 html 模版字符串,顺次解决其中的各个标签,以及标签上的属性
 * @param {*} html html 模版
 * @param {*} options 配置项
 */
export function parseHTML (html, options) {const stack = []
  const expectHTML = options.expectHTML
  // 是否是自闭合标签
  const isUnaryTag = options.isUnaryTag || no
  // 是否能够只有开始标签
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  // 记录以后在原始 html 字符串中的开始地位
  let index = 0
  let last, lastTag
  while (html) {
    last = html
    // Make sure we're not in a plaintext content element like script/style
    // 确保不是在 script、style、textarea 这样的纯文本元素中
    if (!lastTag || !isPlainTextElement(lastTag)) {
      // 找第一个 < 字符
      let textEnd = html.indexOf('<')
      // textEnd === 0 阐明在结尾找到了
      // 别离解决可能找到的正文标签、条件正文标签、Doctype、开始标签、完结标签
      // 每解决完一种状况,就会截断(continue)循环,并且重置 html 字符串,将解决过的标签截掉,下一次循环解决残余的 html 字符串模版
      if (textEnd === 0) {
        // Comment:
        // 解决正文标签 <!-- xx -->
        if (comment.test(html)) {
          // 正文标签的完结索引
          const commentEnd = html.indexOf('-->')
          if (commentEnd >= 0) {
            // 是否应该保留 正文
            if (options.shouldKeepComment) {
              // 失去:正文内容、正文的开始索引、完结索引
              options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
            }
            // 调整 html 和 index 变量
            advance(commentEnd + 3)
            continue
          }
        }
        // 解决条件正文标签:<!--[if IE]>
        // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
        if (conditionalComment.test(html)) {
          // 完结地位
          const conditionalEnd = html.indexOf(']>')
          if (conditionalEnd >= 0) {
            // 调整 html 和 index 变量
            advance(conditionalEnd + 2)
            continue
          }
        }
        // 解决 Doctype,<!DOCTYPE html>
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {advance(doctypeMatch[0].length)
          continue
        }
        /**
         * 解决开始标签和完结标签是这整个函数中的核型局部
         * 这两局部就是在结构 element ast
         */
        // 解决完结标签,比方 </div>
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          // 解决完结标签
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }
        // 解决开始标签
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          // 进一步解决上一步失去后果,并最初调用 options.start 办法
          // 真正的解析工作都是在这个 start 办法中做的
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {advance(1)
          }
          continue
        }
      }
      let text, rest, next
      if (textEnd >= 0) {
        // 能走到这儿,阐明尽管在 html 中匹配到到了 <xx,然而这不属于上述几种状况,// 它就只是一个一般的一段文本:< 我是文本
        // 于是从 html 中找到下一个 <,直到 <xx 是上述几种状况的标签,则完结,// 在这整个过程中始终在调整 textEnd 的值,作为 html 中下一个无效标签的开始地位
        // 截取 html 模版字符串中 textEnd 之后的内容,rest = <xx
        rest = html.slice(textEnd)
        // 这个 while 循环就是解决 <xx 之后的纯文本状况
        // 截取文本内容,并找到无效标签的开始地位(textEnd)while (!endTag.test(rest) &&
          !startTagOpen.test(rest) &&
          !comment.test(rest) &&
          !conditionalComment.test(rest)
        ) {
          // 则认为 < 前面的内容为纯文本,而后在这些纯文本中再次找 <
          next = rest.indexOf('<', 1)
          // 如果没找到 <,则间接完结循环
          if (next < 0) break
          // 走到这儿阐明在后续的字符串中找到了 <,索引地位为 textEnd
          textEnd += next
          // 截取 html 字符串模版 textEnd 之后的内容赋值给 rest,持续判断之后的字符串是否存在标签
          rest = html.slice(textEnd)
        }
        // 走到这里,阐明遍历完结,有两种状况,一种是 < 之后就是一段纯文本,要不就是在前面找到了无效标签,截取文本
        text = html.substring(0, textEnd)
      }
      // 如果 textEnd < 0,阐明 html 中就没找到 <,那阐明 html 就是一段文本
      if (textEnd < 0) {text = html}
      // 将文本内容从 html 模版字符串上截取掉
      if (text) {advance(text.length)
      }
      // 解决文本
      // 基于文本生成 ast 对象,而后将该 ast 放到它的父元素的中
      // 即 currentParent.children 数组中
      if (options.chars && text) {options.chars(text, index - text.length, index)
      }
    } else {
      // 解决 script、style、textarea 标签的闭合标签
      let endTagLength = 0
      // 开始标签的小写模式
      const stackedTag = lastTag.toLowerCase()
      const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
      // 匹配并解决开始标签和完结标签之间的所有文本,比方 <script>xx</script>
      const rest = html.replace(reStackedTag, function (all, text, endTag) {
        endTagLength = endTag.length
        if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
          text = text
            .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
            .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1')
        }
        if (shouldIgnoreFirstNewline(stackedTag, text)) {text = text.slice(1)
        }
        if (options.chars) {options.chars(text)
        }
        return ''
      })
      index += html.length - rest.length
      html = rest
      parseEndTag(stackedTag, index - endTagLength, index)
    }
    // 到这里就解决完结,如果 stack 数组中还有内容,则阐明有标签没有被闭合,给出提示信息
    if (html === last) {options.chars && options.chars(html)
      if (process.env.NODE_ENV !== 'production' && !stack.length && options.warn) {options.warn(`Mal-formatted tag at end of template: "${html}"`, {start: index + html.length})
      }
      break
    }
  }
  // Clean up any remaining tags
  parseEndTag()
  /**
   * 重置 html,html = 从索引 n 地位开始的向后的所有字符
   * index 为 html 在 原始的 模版字符串 中的的开始索引,也是下一次该解决的字符的开始地位
   * @param {*} n 索引
   */
  function advance (n) {
    index += n
    html = html.substring(n)
  }
  /**
   * 解析开始标签,比方:<div id="app">
   * @returns {tagName: 'div', attrs: [[xx], ...], start: index }
   */
  function parseStartTag () {const start = html.match(startTagOpen)
    if (start) {
      // 处理结果
      const match = {
        // 标签名
        tagName: start[1],
        // 属性,占位符
        attrs: [],
        // 标签的开始地位
        start: index
      }
      /**
        * 调整 html 和 index,比方:*   html = 'id="app">'
        *   index = 此时的索引
        *   start[0] = '<div'
        */
      advance(start[0].length)
      let end, attr
      // 解决 开始标签 内的各个属性,并将这些属性放到 match.attrs 数组中
      while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
        attr.start = index
        advance(attr[0].length)
        attr.end = index
        match.attrs.push(attr)
      }
      // 开始标签的完结,end = '>' 或 end = '/>'
      if (end) {match.unarySlash = end[1]
        advance(end[0].length)
        match.end = index
        return match
      }
    }
  }
  /**
   *  进一步解决开始标签的解析后果 ——— match 对象
   *  解决属性 match.attrs,如果不是自闭合标签,则将标签信息放到 stack 数组,待未来解决到它的闭合标签时再将其弹出 stack,示意该标签处理完毕,这时标签的所有信息都在 element ast 对象上了
   *  接下来调用 options.start 办法解决标签,并依据标签信息生成 element ast,*  以及解决开始标签上的属性和指令,最初将 element ast 放入 stack 数组
   * 
   * @param {*} match {tagName: 'div', attrs: [[xx], ...], start: index }
   */
  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)
      }
    }
    // 一元标签,比方 <hr />
    const unary = isUnaryTag(tagName) || !!unarySlash
    // 解决 match.attrs,失去 attrs = [{name: attrName, value: attrVal, start: xx, end: xx}, ...]
    // 比方 attrs = [{name: 'id', value: 'app', start: xx, end: xx}, ...
    const l = match.attrs.length
    const attrs = new Array(l)
    for (let i = 0; i < l; i++) {const args = match.attrs[i]
      // 比方:args[3] => 'id',args[4] => '=',args[5] => 'app'
      const value = args[3] || args[4] || args[5] || ''const shouldDecodeNewlines = tagName ==='a'&& args[1] ==='href'
        ? options.shouldDecodeNewlinesForHref
        : options.shouldDecodeNewlines
      // attrs[i] = {id: 'app'}
      attrs[i] = {name: args[1],
        value: decodeAttr(value, shouldDecodeNewlines)
      }
      // 非生产环境,记录属性的开始和完结索引
      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {attrs[i].start = args.start + args[0].match(/^\s*/).length
        attrs[i].end = args.end
      }
    }
    // 如果不是自闭合标签,则将标签信息放到 stack 数组中,待未来解决到它的闭合标签时再将其弹出 stack
    // 如果是自闭合标签,则标签信息就没必要进入 stack 了,间接解决泛滥属性,将他们都设置到 element ast 对象上,就没有解决 完结标签的那一步了,这一步在解决开始标签的过程中就进行了
    if (!unary) {// 将标签信息放到 stack 数组中,{ tag, lowerCasedTag, attrs, start, end}
      stack.push({tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
      // 标识以后标签的完结标签为 tagName
      lastTag = tagName
    }
    /**
      * 调用 start 办法,次要做了以下 6 件事件:
      *   1、创立 AST 对象
      *   2、解决存在 v-model 指令的 input 标签,别离解决 input 为 checkbox、radio、其它的状况
      *   3、解决标签上的泛滥指令,比方 v-pre、v-for、v-if、v-once
      *   4、如果根节点 root 不存在则设置以后元素为根节点
      *   5、如果以后元素为非自闭合标签则将本人 push 到 stack 数组,并记录 currentParent,在接下来解决子元素时用来通知子元素本人的父节点是谁
      *   6、如果以后元素为自闭合标签,则示意该标签要解决完结了,让本人和父元素产生关系,以及设置本人的子元素
      */
    if (options.start) {options.start(tagName, attrs, unary, match.start, match.end)
    }
  }
  /**
   * 解析完结标签,比方:</div>
   * 最次要的事就是:*   1、解决 stack 数组,从 stack 数组中找到以后完结标签对应的开始标签,而后调用 options.end 办法
   *   2、解决完完结标签之后调整 stack 数组,保障在失常状况下 stack 数组中的最初一个元素就是下一个完结标签对应的开始标签
   *   3、解决一些异常情况,比方 stack 数组最初一个元素不是以后完结标签对应的开始标签,还有就是
   *      br 和 p 标签独自解决
   * @param {*} tagName 标签名,比方 div
   * @param {*} start 完结标签的开始索引
   * @param {*} end 完结标签的完结索引
   */
  function parseEndTag (tagName, start, end) {
    let pos, lowerCasedTagName
    if (start == null) start = index
    if (end == null) end = index
    // 倒序遍历 stack 数组,找到第一个和以后完结标签雷同的标签,该标签就是完结标签对应的开始标签的形容对象
    // 实践上,不出异样,stack 数组中的最初一个元素就是以后完结标签的开始标签的形容对象
    // Find the closest opened tag of the same type
    if (tagName) {lowerCasedTagName = tagName.toLowerCase()
      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
    }
    // 如果在 stack 中始终没有找到雷同的标签名,则 pos 就会 < 0,进行前面的 else 分支
    if (pos >= 0) {
      // 这个 for 循环负责敞开 stack 数组中索引 >= pos 的所有标签
      // 为什么要用一个循环,下面说到失常状况下 stack 数组的最初一个元素就是咱们要找的开始标签,// 然而有些异常情况,就是有些元素没有给提供完结标签,比方:// stack = ['span', 'div', 'span', 'h1'],以后解决的完结标签 tagName = div
      // 匹配到 div,pos = 1,那索引为 2 和 3 的两个标签(span、h1)阐明就没提供完结标签
      // 这个 for 循环就负责敞开 div、span 和 h1 这三个标签,// 并在开发环境为 span 和 h1 这两个标签给出”未匹配到完结标签的提醒”// Close all the open elements, up the stack
      for (let i = stack.length - 1; i >= pos; i--) {
        if (process.env.NODE_ENV !== 'production' &&
          (i > pos || !tagName) &&
          options.warn
        ) {
          options.warn(`tag <${stack[i].tag}> has no matching end tag.`,
            {start: stack[i].start, end: stack[i].end }
          )
        }
        if (options.end) {
          // 走到这里,阐明下面的异常情况都解决完了,调用 options.end 解决失常的完结标签
          options.end(stack[i].tag, start, end)
        }
      }
      // 将方才解决的那些标签从数组中移除,保障数组的最初一个元素就是下一个完结标签对应的开始标签
      // Remove the open elements from the stack
      stack.length = pos
      // lastTag 记录 stack 数组中未解决的最初一个开始标签
      lastTag = pos && stack[pos - 1].tag
    } else if (lowerCasedTagName === 'br') {
      // 以后解决的标签为 <br /> 标签
      if (options.start) {options.start(tagName, [], true, start, end)
      }
    } else if (lowerCasedTagName === 'p') {
      // 解决 <p> 标签
      if (options.start) {options.start(tagName, [], false, start, end)
      }
      // 解决 </p> 标签
      if (options.end) {options.end(tagName, start, end)
      }
    }
  }
}

parseHTML 的次要逻辑就是循环解析,用正则做匹配,而后做不同的解决,在匹配的过程中利用 advance 函数一直扭转索引,直到解析结束。

通过正则能够匹配到正文标签、文档类型标签、开始标签、完结标签。通过 handleStartTag 办法解析开始标签,将非一元标签构建进去的 AST 对象推入 stack 中,通过 parseEndTag 办法对闭合标签做解析,也就是倒序的 stack,找到第一个和以后完结标签雷同的标签,该标签就是完结标签对应的开始标签的形容对象。

AST 元素节点总共有 3 种类型,type 为 1 示意是一般元素,为 2 示意是表达式,为 3 示意是纯文本。parse 的作用就是利用正则将模板字符创解析成 AST 语法树。

optimize

在模板解析之后,生成 AST 树,接下来就是对 AST 的一些优化。

optimize

// src/compiler/optimizer.js
/**
 * 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
  /**
  * options.staticKeys = 'staticClass,staticStyle'
  * isStaticKey = function(val) {return map[val] }
  */
  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)
}

markStatic

// src/compiler/optimizer.js
function markStatic (node: ASTNode) {
  // 通过 node.static 来标识节点是否为 动态节点
  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
    /**
   * 不要将组件的插槽内容设置为动态节点,这样能够防止:*   1、组件不能扭转插槽节点
   *   2、动态插槽内容在热重载时失败
   */
    if (!isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      // 递归终止条件,如果节点不是平台保留标签  && 也不是 slot 标签 && 也不是内联模版,则间接完结
      return
    }
    // 遍历子节点,递归调用 markStatic 来标记这些子节点的 static 属性
    for (let i = 0, l = node.children.length; i < l; i++) {const child = node.children[i]
      markStatic(child)
      // 如果子节点是非动态节点,则将父节点更新为非动态节点
      if (!child.static) {node.static = false}
    }
    // 如果节点存在 v-if、v-else-if、v-else 这些指令,则顺次标记 block 中节点的 static
    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}
      }
    }
  }
}

/**
 * 判断节点是否为动态节点:*  通过自定义的 node.type 来判断,2: 表达式 => 动静,3: 文本 => 动态
 *  但凡有 v-bind、v-if、v-for 等指令的都属于动静节点
 *  组件为动静节点
 *  父节点为含有 v-for 指令的 template 标签,则为动静节点
 */
function isStatic (node: ASTNode): boolean {if (node.type === 2) { // expression
    // 比方:{{msg}}
    return false
  }
  if (node.type === 3) { // text
    return true
  }
  return !!(node.pre || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey)
  ))
}

markStaticRoots

// src/compiler/optimizer.js
function markStaticRoots (node: ASTNode, isInFor: boolean) {if (node.type === 1) {if (node.static || node.once) {
      // 曾经是 static 的节点或者是 v-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
    )) {
      // 除了自身是一个动态节点外,必须满足领有 children,并且 children 不能只是一个文本节点
      node.staticRoot = true
      return
    } else {node.staticRoot = false}
    // 以后节点不是动态根,递归遍历其子节点,标记动态根
    if (node.children) {for (let i = 0, l = node.children.length; i < l; i++) {markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    // 如果节点存在 v-if、v-else-if、v-else 指令,则为 block 节点标记动态根
    if (node.ifConditions) {for (let i = 1, l = node.ifConditions.length; i < l; i++) {markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

在模板不是所有数据都是响应式的,有些数据是首次渲染之后就不会在发生变化的,通过遍历生成的模板 AST 树,对这些节点进行标记,就能够在 patch 的过程中跳过它们,从而进步比照的性能。

optimize 中实际上就是通过 markStatic(root) 对动态节点进行标记和 应用 markStaticRoots(root, false) 标记动态根。

generate

在生成 AST 语法树后,对 AST 进行优化,标记动态节点和动态根。最初就是通过 generate 生成代码字符串。

generate

// src/compiler/codegen/index.js
export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {const state = new CodegenState(options)
  // fix #11483, Root level <script> tags should not be rendered.
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
  return {render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

state 是 CodegenState 的一个实例,在生成代码的时候会用到其中的一些属性和办法。generate 函数次要是通过 genElement 生成 code , 而后在用 with(this){return ${code}} 将其包裹起来。

genElement

// src/compiler/codegen/index.js
export function genElement (el: ASTElement, state: CodegenState): string {if (el.parent) {el.pre = el.pre || el.parent.pre}
  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 && !state.pre) {
    // 解决子节点
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    // 解决插槽
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      // 解决动静组件
      code = genComponent(el.component, el, state)
    } else {
      // 自定义组件和原生标签
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        // 非一般元素或者带有 v-pre 指令的组件走这里,解决节点的所有属性
        data = 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 的作用次要就是对 AST 节点上的属性应用不同办法做解决,而生成代码函数。

genStatic

// src/compiler/codegen/index.js
// hoist static sub-trees out
function genStatic (el: ASTElement, state: CodegenState): string {
  // 标记以后动态节点曾经被解决
  el.staticProcessed = true
  // Some elements (templates) need to behave differently inside of a v-pre
  // node.  All pre nodes are static roots, so we can use this as a location to
  // wrap a state change and reset it upon exiting the pre node.
  const originalPreState = state.pre
  if (el.pre) {state.pre = el.pre}
  // 将生成的代码增加到 staticRenderFns 中
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  state.pre = originalPreState
  // 返回 _m 函数,state.staticRenderFns.length - 1 示意数组中的下标
  return `_m(${state.staticRenderFns.length - 1}${el.staticInFor ? ',true' : ''})`
}

genOnce

// src/compiler/codegen/index.js
function genOnce (el: ASTElement, state: CodegenState): string {
  // 标记以后节点的 v-once 指令已被解决
  el.onceProcessed = true
  if (el.if && !el.ifProcessed) {
    // 含有 v-if 并且 V-if 没有被解决,则解决 V-if 最终生成一段三元运算符的代码
    return genIf(el, state)
  } else if (el.staticInFor) {
    // 阐明以后节点是被包裹在还有 v-for 指令节点外部的动态节点
    let key = ''
    let parent = el.parent
    while (parent) {if (parent.for) {
        key = parent.key
        break
      }
      parent = parent.parent
    }
    if (!key) {
      process.env.NODE_ENV !== 'production' && state.warn(
        `v-once can only be used inside v-for that is keyed. `,
        el.rawAttrsMap['v-once']
      )
      return genElement(el, state)
    }
    // 生成 _o 函数
    return `_o(${genElement(el, state)},${state.onceId++},${key})`
  } else {
    // 下面状况都不合乎,阐明是简略的动态节点,生成 _m 函数
    return genStatic(el, state)
  }
}

genFor

// src/compiler/codegen/index.js
export function genFor (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''const iterator2 = el.iterator2 ? `,${el.iterator2}` :''
  // 提醒 v-for 在组件上时必须应用 key
  if (process.env.NODE_ENV !== 'production' &&
    state.maybeComponent(el) &&
    el.tag !== 'slot' &&
    el.tag !== 'template' &&
    !el.key
  ) {
    state.warn(`<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
      `v-for should have explicit keys. ` +
      `See https://vuejs.org/guide/list.html#key for more info.`,
      el.rawAttrsMap['v-for'],
      true /* tip */
    )
  }
  // 标记以后节点上的 v-for 指令已被解决
  el.forProcessed = true // avoid recursion
  /**
   * 生成 _l 函数,比方:*     v-for="(item,index) in data"
   * 
   *    _l((data), function (item, index) {*     return genElememt(el, state)
   *   })
   */
  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
    `return ${(altGen || genElement)(el, state)}` +
    '})'
}

genIf

// src/compiler/codegen/index.js
export function genIf (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  // 标记以后节点的 v-if 指令曾经被解决
  el.ifProcessed = true // avoid recursion
  // 失去三元表达式,condition ? render1 : render2
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

function genIfConditions (
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  // 长度为空,间接返回一个空节点渲染函数
  if (!conditions.length) {return altEmpty || '_e()'
  }
  // 顺次从 conditions 获取第一个 condition
  const condition = conditions.shift()
  if (condition.exp) {
    // 通过对 condition.exp 去生成一段三元运算符的代码,
    // : 后是递归调用,如果有多个 conditions,就生成多层三元运算
    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)
  }
}

genChildren

// src/compiler/codegen/index.js
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'
    ) {
      // 一个子节点 && 节点上有 v-for && 不是 template 标签 && 不是 slot
      // 则间接调用 genElement,从而进入到 genFor
      const normalizationType = checkSkip
        ? state.maybeComponent(el) ? `,1` : `,0`
        : ``
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    // 生成代码的一个函数
    const gen = altGenNode || genNode
    // 返回一个数组,数组的每个元素都是一个子节点的渲染函数,// 格局:['_c(tag, data, children, normalizationType)', ...]
    return `[${children.map(c => gen(c, state)).join(',')}]${normalizationType ? `,${normalizationType}` : ''
      }`
  }
}

genSlot

// src/compiler/codegen/index.js
function genSlot (el: ASTElement, state: CodegenState): string {
  // 插槽名称
  const slotName = el.slotName || '"default"'
  // 解决子节点
  const children = genChildren(el, state)
  // 最终返回 _t 函数
  let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}`
  const attrs = el.attrs || el.dynamicAttrs
    ? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
      // slot props are camelized
      name: camelize(attr.name),
      value: attr.value,
      dynamic: attr.dynamic
    })))
    : null
  const bind = el.attrsMap['v-bind']
  if ((attrs || bind) && !children) {res += `,null`}
  if (attrs) {res += `,${attrs}`
  }
  if (bind) {res += `${attrs ? '':',null'},${bind}`
  }
  return res + ')'
}

genProps

// src/compiler/codegen/index.js
function genProps (props: Array<ASTAttr>): string {
  // 动态属性
  let staticProps = ``
  // 动静属性
  let dynamicProps = ``
  // 遍历属性数组
  for (let i = 0; i < props.length; i++) {
    // 属性
    const prop = props[i]
    // 属性值
    const value = __WEEX__
      ? generateValue(prop.value)
      : transformSpecialNewlines(prop.value)
    if (prop.dynamic) {
      // 动静属性,`dAttrName,dAttrVal,...`
      dynamicProps += `${prop.name},${value},`
    } else {
      // 动态属性,'attrName,attrVal,...'
      staticProps += `"${prop.name}":${value},`
    }
  }
  // 去掉动态属性最初的逗号
  staticProps = `{${staticProps.slice(0, -1)}}`
  if (dynamicProps) {
    // 如果存在动静属性则返回:// _d(动态属性字符串,动静属性字符串)
    return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])`
  } else {
    // 阐明属性数组中不存在动静属性,间接返回动态属性字符串
    return staticProps
  }
}

genData

// src/compiler/codegen/index.js
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 + ','
  // key
  if (el.key) {data += `key:${el.key},`
  }
  // ref
  if (el.ref) {data += `ref:${el.ref},`
  }
  if (el.refInFor) {data += `refInFor:true,`}
  // pre
  if (el.pre) {data += `pre:true,`}
  // record original tag name for components using "is" attribute
  if (el.component) {data += `tag:"${el.tag}",`
  }
  // 为节点执行模块 (class、style) 的 genData 办法,// module data generation functions
  for (let i = 0; i < state.dataGenFns.length; i++) {data += state.dataGenFns[i](el)
  }
  // attributes
  if (el.attrs) {data += `attrs:${genProps(el.attrs)},`
  }
  // DOM props
  if (el.props) {data += `domProps:${genProps(el.props)},`
  }
  // event handlers
  // 自定义事件, 如 {`on${eventName}:handleCode` } 或者 {`on_d(${eventName}:handleCode`, `${eventName},handleCode`) }
  if (el.events) {data += `${genHandlers(el.events, false)},`
  }
  // 带 .native 修饰符的事件,if (el.nativeEvents) {data += `${genHandlers(el.nativeEvents, true)},`
  }
  // slot target
  // only for non-scoped slots
  // 非作用域插槽
  if (el.slotTarget && !el.slotScope) {data += `slot:${el.slotTarget},`
  }
  // scoped slots
  // 作用域插槽
  if (el.scopedSlots) {data += `${genScopedSlots(el, el.scopedSlots, state)},`
  }
  // component v-model
  if (el.model) {data += `model:{value:${el.model.value},callback:${el.model.callback},expression:${el.model.expression}},`
  }
  // inline-template
  if (el.inlineTemplate) {const inlineTemplate = genInlineTemplate(el, state)
    if (inlineTemplate) {data += `${inlineTemplate},`
    }
  }
  data = data.replace(/,$/, '') +'}'
  // v-bind dynamic argument wrap
  // v-bind with dynamic arguments must be applied using the same v-bind object
  // merge helper so that class/style/mustUseProp attrs are handled correctly.
  if (el.dynamicAttrs) {data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
  }
  // v-bind data wrap
  if (el.wrapData) {data = el.wrapData(data)
  }
  // v-on data wrap
  if (el.wrapListeners) {data = el.wrapListeners(data)
  }
  return data
}

genData 函数就是依据 AST 元素节点的属性结构出一个 data 对象字符串,这个在前面创立 VNode 的时候的时候会作为参数传入。

genComponent

// src/compiler/codegen/index.js
function genComponent (
  componentName: string,
  el: ASTElement,
  state: CodegenState
): string {
  // 所有的子节点
  const children = el.inlineTemplate ? null : genChildren(el, state, true)
  // 返回 `_c(compName, data, children)`
  // compName 是 is 属性的值
  return `_c(${componentName},${genData(el, state)}${children ? `,${children}` : ''
    })`
}

举个栗子

模板

<ul :class="bindCls" class="list" v-if="isShow">
  <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>

parse 生成 AST

ast = {
  'type': 1,
  'tag': 'ul',
  'attrsList': [],
  'attrsMap': {
    ':class': 'bindCls',
    'class': 'list',
    'v-if': 'isShow'
  },
  'if': 'isShow',
  'ifConditions': [{
    'exp': 'isShow',
    'block': // ul ast element
  }],
  'parent': undefined,
  'plain': false,
  'staticClass': 'list',
  'classBinding': 'bindCls',
  'children': [{
    'type': 1,
    'tag': 'li',
    'attrsList': [{
      'name': '@click',
      'value': 'clickItem(index)'
    }],
    'attrsMap': {'@click': 'clickItem(index)',
      'v-for': '(item,index) in data'
    },
    'parent': // ul ast element
      'plain': false,
    'events': {
      'click': {'value': 'clickItem(index)'
      }
    },
    'hasBindings': true,
    'for': 'data',
    'alias': 'item',
    'iterator1': 'index',
    'children': [
      'type': 2,
      'expression': '_s(item)+":"+_s(index)'
      'text': '{{item}}:{{index}}',
      'tokens': [{ '@binding': 'item'},
        ':',
        {'@binding': 'index'}
      ]
    ]
  }]
}

optimize 优化 AST

ast = {
  'type': 1,
  'tag': 'ul',
  'attrsList': [],
  'attrsMap': {
    ':class': 'bindCls',
    'class': 'list',
    'v-if': 'isShow'
  },
  'if': 'isShow',
  'ifConditions': [{
    'exp': 'isShow',
    'block': // ul ast element
  }],
  'parent': undefined,
  'plain': false,
  'staticClass': 'list',
  'classBinding': 'bindCls',
  'static': false,
  'staticRoot': false,
  'children': [{
    'type': 1,
    'tag': 'li',
    'attrsList': [{
      'name': '@click',
      'value': 'clickItem(index)'
    }],
    'attrsMap': {'@click': 'clickItem(index)',
      'v-for': '(item,index) in data'
    },
    'parent': // ul ast element
      'plain': false,
    'events': {
      'click': {'value': 'clickItem(index)'
      }
    },
    'hasBindings': true,
    'for': 'data',
    'alias': 'item',
    'iterator1': 'index',
    'static': false,
    'staticRoot': false,
    'children': [
      'type': 2,
      'expression': '_s(item)+":"+_s(index)'
      'text': '{{item}}:{{index}}',
      'tokens': [{ '@binding': 'item'},
        ':',
        {'@binding': 'index'}
      ],
      'static': false
    ]
  }]
}

generate 生成代码

with (this) {return (isShow) ?
    _c('ul', {
      staticClass: "list",
      class: bindCls
    },
      _l((data), function (item, index) {
        return _c('li', {
          on: {"click": function ($event) {clickItem(index)
            }
          }
        },
          [_v(_s(item) + ":" + _s(index))])
      })
    ) : _e()}

最终生成了许多简写函数,比方 _c_t_l_m , 这些函数都定义在:

// src/core/instance/render-helpers/index.js
export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

到此,Vue 的源码大体梳理结束,在此期间查看了许多大佬的文章,感激大佬们的自私分享,感激敌对的前端圈。

相干链接

Vue 源码解读(预):手写一个简易版 Vue

Vue 源码解读(一):筹备工作

Vue 源码解读(二):初始化和挂载

Vue 源码解读(三):响应式原理

Vue 源码解读(四):更新策略

Vue 源码解读(五):render 和 VNode

Vue 源码解读(六):update 和 patch

Vue 源码解读(七):模板编译

如果感觉还对付的话,给个赞吧!!!也能够来我的集体博客逛逛 https://www.mingme.net/

正文完
 0