vue源码学习总结 深入解析template编译成render函数过程

简述编译流程

总的来说,在beforeMount之前执行编译过程,第一步通过html-parser将template解析成ast抽象语法树,第二步通过optimize优化ast并标记静态节点和静态根节点,第三步通过generate将ast抽象语法树编译成render字符串并将静态部分放到staticRenderFns中,最后通过new Function(render)生成render函数。在beforeMount和mounted之间执行render函数生成VNode,然后通过patch(VNode)生成dom树并挂载,调用mounted。

编译入口

//entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function ( //挂载前先做一些预处理
  el?: string | Element,
  hydrating?: boolean
): Component {
  const { render, staticRenderFns } = compileToFunctions(template, {
    shouldDecodeNewlines,
    delimiters: options.delimiters,
    comments: options.comments
  }, this)
  options.render = render
  options.staticRenderFns = staticRenderFns
  return mount.call(this, el, hydrating)
}
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  callHook(vm, 'beforeMount')
  let updateComponent = () => {
    vm._update(vm._render(), hydrating)//调用render生成VNode,然后patch渲染到页面
  }
  vm._watcher = new Watcher(vm, updateComponent, noop)
  hydrating = false
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

由此可知,在beforeMount之前执行编译过程,然后调用beforeMount,然后执行render函数,然后调用mounted

编译过程

//src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options) //生成抽象语法树
  optimize(ast, options) //优化抽象语法树,标记静态节点和静态根节点
  const code = generate(ast, options) //生成render函数字符串
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns //静态无需重新渲染的部分
  }
})

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      const finalOptions = Object.create(baseOptions)
      const compiled = baseCompile(template, finalOptions)
      return compiled
    }
    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}
export function createCompileToFunctionFn (compile: Function): Function {
  const cache: {
    [key: string]: CompiledFunctionResult;
  } = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn

    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    const compiled = compile(template, options)

    const res = {}
    const fnGenErrors = []
    res.render = createFunction(compiled.render, fnGenErrors) // 将render字符串转换成函数
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors) //将静态render字符串转换成函数
    })
    return (cache[key] = res) //缓存结果,当template不变时直接返回缓存
  }
}

function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
  }
}

由此可知,编译过程为:第一步通过html-parser将template解析成ast抽象语法树,第二步通过optimize优化ast并标记静态节点和静态根节点,第三步通过generate将ast抽象语法树编译成render字符串并将静态部分放到staticRenderFns中,最后通过new Function(render)生成render函数。

将template解析成ast抽象语法树

src/compiler/index.js
src/compiler/parser/html-parser.js

export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {

  const stack = []
  const preserveWhitespace = options.preserveWhitespace !== false
  let root
  let currentParent
  let inVPre = false
  let inPre = false
  let warned = false
  
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldKeepComment: options.comments,
    start (tag, attrs, unary) {
      // 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)
      }

      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      if (ns) {
        element.ns = ns
      }

      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.'
        )
      }

      // apply pre-transforms
      for (let i = 0; i < preTransforms.length; i++) {
        element = preTransforms[i](element, options) || element
      }

      if (!inVPre) {
        processPre(element)
        if (element.pre) {
          inVPre = true
        }
      }
      if (platformIsPreTag(element.tag)) {
        inPre = true
      }
      if (inVPre) {
        processRawAttrs(element)
      } else if (!element.processed) {
        // structural directives
        processFor(element)
        processIf(element)
        processOnce(element)
        // element-scope stuff
        processElement(element, options)
      }

      function checkRootConstraints (el) {
        if (process.env.NODE_ENV !== 'production') {
          if (el.tag === 'slot' || el.tag === 'template') {
            warnOnce(
              `Cannot use <${el.tag}> as component root element because it may ` +
              'contain multiple nodes.'
            )
          }
          if (el.attrsMap.hasOwnProperty('v-for')) {
            warnOnce(
              'Cannot use v-for on stateful component root element because ' +
              'it renders multiple elements.'
            )
          }
        }
      }

      // tree management
      if (!root) {
        root = element
        checkRootConstraints(root)
      } else if (!stack.length) {
        // allow root elements with v-if, v-else-if and v-else
        if (root.if && (element.elseif || element.else)) {
          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.`
          )
        }
      }
      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 {
          currentParent.children.push(element)
          element.parent = currentParent
        }
      }
      if (!unary) {
        currentParent = element
        stack.push(element)
      } else {
        endPre(element)
      }
      // apply post-transforms
      for (let i = 0; i < postTransforms.length; i++) {
        postTransforms[i](element, options)
      }
    },

    end () {
      // remove trailing whitespace
      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()
      }
      // pop stack
      stack.length -= 1
      currentParent = stack[stack.length - 1]
      endPre(element)
    },

    chars (text: string) {
      if (!currentParent) {
        if (process.env.NODE_ENV !== 'production') {
          if (text === template) {
            warnOnce(
              'Component template requires a root element, rather than just text.'
            )
          } else if ((text = text.trim())) {
            warnOnce(
              `text "${text}" outside root element will be ignored.`
            )
          }
        }
        return
      }
      // IE textarea placeholder bug
      /* istanbul ignore if */
      if (isIE &&
        currentParent.tag === 'textarea' &&
        currentParent.attrsMap.placeholder === text
      ) {
        return
      }
      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 expression
        if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {
          children.push({
            type: 2,
            expression,
            text
          })
        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
          children.push({
            type: 3,
            text
          })
        }
      }
    },
    comment (text: string) {
      currentParent.children.push({
        type: 3,
        text,
        isComment: true
      })
    }
  })
  return root
}
export function parseHTML (html, options) {
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  let index = 0
  let last, lastTag
  while (html) {
    last = html
    // Make sure we're not in a plaintext content element like script/style
    if (!lastTag || !isPlainTextElement(lastTag)) {
      let textEnd = html.indexOf('<')
      if (textEnd === 0) { //
        // Comment: 碰到注释节点直接前进
        if (comment.test(html)) {
          const commentEnd = html.indexOf('-->')

          if (commentEnd >= 0) {
            if (options.shouldKeepComment) {
              options.comment(html.substring(4, commentEnd))
            }
            advance(commentEnd + 3)
            continue
          }
        }

        // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
        // 碰到条件注释节点直接前进
        if (conditionalComment.test(html)) {
          const conditionalEnd = html.indexOf(']>')

          if (conditionalEnd >= 0) {
            advance(conditionalEnd + 2)
            continue
          }
        }

        // Doctype:
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          advance(doctypeMatch[0].length)
          continue
        }

        // End tag:
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }

        // Start tag:
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(lastTag, html)) {
            advance(1)
          }
          continue
        }
      }

      let text, rest, next
      if (textEnd >= 0) {
        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)
      }

      if (textEnd < 0) {
        text = html
        html = ''
      }

      if (options.chars && text) {
        options.chars(text)
      }
    } else {
      let endTagLength = 0
      const stackedTag = lastTag.toLowerCase()
      const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'))
      const rest = html.replace(reStackedTag, function (all, text, endTag) {
        endTagLength = endTag.length
        if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
          text = text
            .replace(/<!--([\s\S]*?)-->/g, '$1')
            .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)
    }

    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}"`)
      }
      break
    }
  }

  // Clean up any remaining tags
  parseEndTag()

  function advance (n) {
    index += n
    html = html.substring(n)
  }

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

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

    const unary = isUnaryTag(tagName) || !!unarySlash

    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] || ''
      attrs[i] = {
        name: args[1],
        value: decodeAttr(
          value,
          options.shouldDecodeNewlines
        )
      }
    }

    if (!unary) {
      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
      lastTag = tagName
    }

    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }

  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 (process.env.NODE_ENV !== 'production' &&
          (i > pos || !tagName) &&
          options.warn
        ) {
          options.warn(
            `tag <${stack[i].tag}> has no matching end tag.`
          )
        }
        if (options.end) {
          options.end(stack[i].tag, start, end)
        }
      }

      // Remove the open elements from the stack
      stack.length = pos
      lastTag = pos && stack[pos - 1].tag
    } else if (lowerCasedTagName === 'br') {
      if (options.start) {
        options.start(tagName, [], true, start, end)
      }
    } else if (lowerCasedTagName === 'p') {
      if (options.start) {
        options.start(tagName, [], false, start, end)
      }
      if (options.end) {
        options.end(tagName, start, end)
      }
    }
  }
}

假设有如下代码:

new Vue({
  el: '#app',
  template: `
  <div id="app">
    <div class="aa" :class="{'bb':bb}" v-if="cc" @click="meClick">
      <input type="text" v-model="dd">
      <div v-for="item in arr">{{item}}</div>
      <div><span>我是静态内容</span></div>
    </div>
  </div>
  `,
  data() {
    return {
      bb:true,
      cc:true,
      dd:'input',
      arr:[1,2,3],
    }
  },
  methods:{
    meClick(){}
  },
})

生成的ast树如下:

let ast = {
  type: 1,// type= 1普通元素 2表达式(动态) 3文本节点(静态)
  tag: 'div',
  attrsMap: {id: 'app'},
  parent: undefined, //没有父元素
  static: false,
  staticRoot: false,
  children: [
    {
      type: 1,
      tag: "div",
      attrsMap: {class: "aa", ':class': "{'bb':bb}", 'v-if': "cc", '@click': "meClick"},
      if: "cc",
      ifConditions: [{
        exp: "cc"
      }],
      staticClass: "aa",
      classBinding: "{'bb':bb}",
      hasBindings: true,
      events: {click: {value: "meClick", dynamic: false}},
      static: false,
      staticRoot: false,
      ifProcessed: true,
      children: [
        {
          type: 1,
          tag: "input",
          attrsMap: {type: "text", 'v-model': "dd"},
          directives:[{name: "model", rawName: "v-model", value: "dd", arg: null, isDynamicArg: false,modifiers: undefined,}],
          static: false,
          staticRoot: false,
          props:[{name: "value", value: "(dd)"}],
          events:[{input:{value: "if($event.target.composing)return;dd=$event.target.value"}}]
        },
        {
          type: 1,
          tag: "div",
          attrsMap: {'v-for': "item in arr"},
          for: "arr",
          alias: "item",
          plain: true,
          static: false,
          staticRoot: false,
          children:[{type: 2, expression: "_s(item)",tokens:[{'@binding': "item"}],text: "{{item}}"}]
        },
        {
          type: 1,
          tag: "div",
          static: true,//第二步中被optimize标记的静态节点
          staticInFor: false,
          staticRoot: true,//第二步中被optimize标记的静态根节点
          staticProcessed: true,
          children:[
            {
              type: 1,
              tag: "span",
              static: true,//第二步中被optimize标记的静态节点
              children:[{type: 3, text: "我是静态内容", static: true}]//文本类型,第二步中被optimize标记的静态节点
            }
          ]
        }
      ]
    }
  ]
}

通过optimize标记ast的静态节点和静态根节点

/src/compiler/optimizer.js

function markStatic (node: ASTNode) {
  node.static = isStatic(node)
  if (node.type === 1) {
    // 不要使组件槽内容静态。这就避免了
    // 组件不能改变插槽节点
    // 静态插槽内容热加载失败
    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) {//只要某个if条件为真,则不是静态
          node.static = false
        }
      }
    }
  }
}
function isStatic (node: ASTNode): boolean {
  if (node.type === 2) { // 表达式类型节点
    return false
  }
  if (node.type === 3) { // 文本类型节点
    return true
  }
  return !!(node.pre || (
    !node.hasBindings && // 不是动态绑定
    !node.if && !node.for && // 不是 v-if 或 v-for 或 v-else
    !isBuiltInTag(node.tag) && // 不是内置标签slot,component
    isPlatformReservedTag(node.tag) && // 不是容器组件
    !isDirectChildOfTemplateFor(node) && // 不是template或v-for的直接子元素
    Object.keys(node).every(isStaticKey) // 当前节点所有属性key都是静态的 (比如说type,tag,attrsList,attrsMap,plain,parent,children,attrs)
  ))
}
function markStaticRoots (node: ASTNode, isInFor: boolean) {
  // 当前元素是静态的且有子元素,则是静态根元素
  if (node.type === 1) { 
    if (node.static || node.once) {
      node.staticInFor = isInFor
    }
    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 (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

第一步中的<div><span>我是静态内容</span></div>会被标记为静态节点,该节点对象为

{
  type: 1,
  tag: "div",
  static: true,//被optimize标记的静态节点
  staticInFor: false,
  staticRoot: true,//被optimize标记的静态根节点
  staticProcessed: true,
  children:[
    {
      type: 1,
      tag: "span",
      static: true,//被optimize标记的静态节点
      children:[{type: 3, text: "我是静态内容", static: true}]//文本类型,被optimize标记的静态节点
    }
  ]
}

通过generate将ast抽象语法树编译成render字符串

源码在src/compiler/codegen/index.js中
将第二步中的ast编译后如下:

//render
with(this){return _c('div',{attrs:{"id":"app"}},[(cc)?_c('div',{staticClass:"aa",class:{'bb':bb},on:{"click":meClick}},[_c('input',{directives:[{name:"model",rawName:"v-model",value:(dd),expression:"dd"}],attrs:{"type":"text"},domProps:{"value":(dd)},on:{"input":function($event){if($event.target.composing)return;dd=$event.target.value}}}),_v(" "),_l((arr),function(item){return _c('div',[_v(_s(item))])}),_v(" "),_m(0)],2):_e()])}
//staticRenderFns
["with(this){return _c('div',[_c('span',[_v("我是静态内容")])])}"]
//render中函数简称原名
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
}

最后通过options.render= new Function(render)options.staticRenderFns = new Function(staticRenderFns) 获得渲染函数和静态渲染函数数组并赋值到options上

至此,编译过程告一段落,后面的虚拟dom和patch请听下回分解。。。

ps:今天看到一句诗我很喜欢,"醉里不知天在水,满船清梦压星河",什么时候我才能过上这样的日子啊+_+。。。

发布了39 篇原创文章 · 获赞 66 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/lyt_angularjs/article/details/105250391