学习笔记(十七)Vue.js源码剖析 - 模板编译和组件化

Vue.js 源码剖析 - 模板编译和组件化

模板编译简介

模板编译主要目的是将模板(template)转换为渲染函数(render)

模板编译的作用

  • Vue2.x使用VNode描述视图及各种交互,用户自己编写VNode比较复杂
  • 用户通过编写类似HTML的代码,即Vue.js的template模板,通过编译器将模板转换为返回VNode的render函数
  • .vue文件会被webpack在构建的过程中转换成render函数

Vue Template Explorer

Vue Template Explorer是Vue官方提供的一款在线将template模板转换成render函数的工具

Vue2.x https://template-explorer.vuejs.org/

Vue3.x https://vue-next-template-explorer.netlify.app/

使用示例

<div id="app"><div>{{ msg }}</div><div>hello</div><comp/></div>

转成render函数

function render() {
  with(this) {
    return _c('div', {
        attrs: {
          "id": "app"
        }
      }, [_c('div', [_v(_s(msg))]), _c('div', [_v("hello")]), _c('comp')],
      1)
  }
}

通过查看生成的render函数内容,可以了解到,render'函数的核心是_c()这个实例方法,用于创建并返回VNode

vm._c()vm.$createElement()类似,在相同的位置定义,都调用createElement()函数,差别只是最后一个处理children内容的参数值不同

tips:Vue2.x中,元素节点内容中的空格、换行等会被全部保留到render函数中,因此不必要的空格与换行会增加处理成本,而Vue3.x中,空格与换行不再保留到render函数中

模板编译的入口

在包含编译器版本的入口文件entry-runtime-with-compiler.js中,通过调用compileToFunctions()将template转换成render函数

  • compileToFunctions()

    • 定义位置:src/platforms/web/compiler/index.js
    • 通过createCompiler(baseOptions)函数调用返回
  • createCompiler(baseOptions)

    • 定义位置:src/compiler/index.js

    • 参数baseOptions

      // web平台相关的选项以及一些辅助函数
      export const baseOptions: CompilerOptions = {
        expectHTML: true,
        modules, //模块 处理class、style、model
        directives, // 指令 处理v-model、v-text、v-html
        isPreTag,
        isUnaryTag,
        mustUseProp,
        canBeLeftOpenTag,
        isReservedTag,
        getTagNamespace,
        staticKeys: genStaticKeys(modules)
      }
      
    • 返回compilecompileToFunctions:createCompileToFunctionFn(compile)

    • 通过createCompilerCreator()工厂函数调用返回

  • createCompilerCreator(baseCompile)

    • 定义位置:src/compiler/create-compiler.js
    • 参数baseCompile:编译模板的核心函数
      • 解析
      • 优化
      • 生成
    • 返回createCompiler函数
  • createCompileToFunctionFn(compile)

    • 定义位置:src/compiler/to-function.js
    • 参数compile
    • 返回compileToFunctions()
image-20210104212822765

模板编译过程

compileToFunctions

将模板字符串编译为render函数

export function createCompileToFunctionFn (compile: Function): Function {
  // 创建缓存
  const cache = Object.create(null)
 
  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 备份options避免污染
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn
 
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      // detect possible CSP restriction
      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
    // 检查缓存中是否存在CompileFunctionResult,有则直接返回
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }
 
    // compile
    // 把模板编译为编译对象{render, staticRenderFns, errors, tips},字符串形式的js代码
    const compiled = compile(template, options)
 
    // check compilation 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
    // 通过createFunction将js字符串转换成函数 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
        )
      }
    }
 
    // 缓存并返回编译结果res
    return (cache[key] = res)
  }
}

compile

模板字符串编译为编译对象,返回render的js函数字符串

核心作用是合并baseOptionsoptions选项,并调用baseCompile编译,记录错误,返回编译后的对象

function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []
 
      let warn = (msg, range, tip) => {
        (tip ? tips : errors).push(msg)
      }
 
      // 处理合并baseOptions与options为finalOptions
      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)
          }
        }
        // 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
 
      // 调用baseCompile编译template模板
      const compiled = baseCompile(template.trim(), finalOptions)
      if (process.env.NODE_ENV !== 'production') {
        detectErrors(compiled.ast, warn)
      }
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }

baseCompile

解析模板字符串,生成抽象语法树ast,优化、生成字符串形式js代码的渲染函数

function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 调用parse将模板字符串转换成ast抽象语法树
  // 抽象语法树: 以树的形式描述代码结构
  const ast = parse(template.trim(), options)
  // 优化抽象语法树
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 把抽象语法树生成字符串形式js代码
  const code = generate(ast, options)
  return {
    ast,
    render: code.render, // 渲染函数
    staticRenderFns: code.staticRenderFns // 静态渲染函数,生成静态VNode树
  }
}

AST

AST(Abstract Syntax Tree) 抽象语法树,以树的形式描述代码结构

此处抽象语法树用来描述树形结构的HTML字符串

为什么要使用抽象语法树

  • 模板字符串转换成AST后,可以通过AST对模板做优化处理
  • 标记模板中的静态内容,在patch的时候直接跳过静态内容
  • 在patch过程中静态内容不需要比对和重新渲染,从而提高性能

https://astexplorer.net/ 是一个在线的ast转换工具,可以查看和转换不同语言的ast生成结果

// AST对象
export function createASTElement (
  tag: string,
  attrs: Array<ASTAttr>,
  parent: ASTElement | void
): ASTElement {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent,
    children: []
  }
}

parse

把HTML模板字符串转换成ast对象

对HTML模板字符串的解析借鉴了第三方库simplehtmlparser

通过正则表达式对各种标签进行解析,并处理各种属性与指令

parse的整体代码较为复杂,这里不做深入分析

/**
* Convert HTML string to AST.
*/
export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  // 处理options
  warn = options.warn || baseWarn
 
  platformIsPreTag = options.isPreTag || no
  platformMustUseProp = options.mustUseProp || no
  platformGetTagNamespace = options.getTagNamespace || no
  const isReservedTag = options.isReservedTag || no
  maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
 
  transforms = pluckModuleFunction(options.modules, 'transformNode')
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')
 
  delimiters = options.delimiters
 
  // 一些变量与方法定义
  ...
  ...
 
  // 解析模板字符串
  // 借鉴了simplehtmlparser.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,
    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
              }
            )
          }
        })
      }
 
      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 }
        )
      }
 
      // apply pre-transforms
      for (let i = 0; i < preTransforms.length; i++) {
        element = preTransforms[i](element, options) || element
      }
 
      // 处理v-pre指令
      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
        // 处理结构化指令 v-for v-if v-once
        processFor(element)
        processIf(element)
        processOnce(element)
      }
 
      if (!root) {
        root = element
        if (process.env.NODE_ENV !== 'production') {
          checkRootConstraints(root)
        }
      }
 
      if (!unary) {
        currentParent = element
        stack.push(element)
      } else {
        closeElement(element)
      }
    },
 
    end (tag, start, end) {
      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)
    },
 
    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()) {
        text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
      } else if (!children.length) {
        // 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 ? ' ' : ''
      }
      if (text) {
        if (!inPre && whitespaceOption === 'condense') {
          // condense consecutive whitespaces into single space
          text = text.replace(whitespaceRE, ' ')
        }
        let res
        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
          }
        }
        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
      if (currentParent) {
        const child: ASTText = {
          type: 3,
          text,
          isComment: true
        }
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          child.start = start
          child.end = end
        }
        currentParent.children.push(child)
      }
    }
  })
  return root
}

optimize

优化的目标是寻找并标记静态子树,静态内容不会改变,不需要重新渲染,在patch过程中可以直接跳过

/**
* 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)
}
 
function markStatic (node: ASTNode) {
  // 判断是否静态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
    // 不将组件中的slot内容标记成静态节点
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {    
      return
    }
    // 遍历children
    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 markStaticRoots (node: ASTNode, isInFor: boolean) {
  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
    }
    // 遍历所有子节点,递归调用markStaticRoots
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    // 遍历所有条件渲染子节点,递归调用markStaticRoots
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

generate

将AST对象转换成字符串形式js代码

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // 如果传入的ast对象有值
  // 调用核心方法genElement生成js字符串代码code
  // 否则返回'_c("div")',即使用渲染函数创建空div节点
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

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) {
    // 处理template子节点内容,如果非有效内容,返回'void 0'
    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 {
      // 处理内置标签
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        // 字符串拼接生成元素的各种属性、指令、事件等
        // 处理各种指令,包括genDirectives (v-model/v-text/v-html)
        data = genData(el, state)
      }

      // 遍历el.children子节点ast数组
      // 调用genNode处理相应类型的节点并转换成字符串js拼接返回
      // 对单个子节点的v-for使用genElement进行优化处理
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      // 拼接_c()渲染函数调用字符串代码
      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
  }
}

// 处理静态根节点
// 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.
  // 备份state.pre
  const originalPreState = state.pre
  if (el.pre) {
    state.pre = el.pre
  }
  // 调用genElement并将生成的代码放入staticRenderFns数组
  // 使用数组是因为可能存在多个静态根节点
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  state.pre = originalPreState
  // 拼接返回_m()函数调用的字符串代码
  // _m函数是定义在原型上的实例方法,指向renderStatic方法实现
  // 主要用处是调用staticRenderFns中的代码渲染静态节点
  return `_m(${
    state.staticRenderFns.length - 1
  }${
    el.staticInFor ? ',true' : ''
  })`
}

模板编译过程总结

image-20210110214905073
  • 模板编译的过程是将模板字符串转换成render渲染函数的过程
  • render函数通过compileToFunctions()函数创建
    • 优先返回缓存中已编译好的render函数
    • 调用compile函数创建
  • compile函数主要合并了options,主要的编译逻辑通过baseCompile函数来完成
  • baseCompile函数实现了模板的解析、优化、生成三个核心步骤
    • parse:解析,将模板字符串template转换成AST抽象语法树
    • optimize:优化,标记AST中的静态子树,避免不必要的重新渲染以及patch过程
    • generate:将AST生成js字符串代码
  • 最终字符串代码通过createFunction函数,使用new Function(code)的方式转换成实际执行的渲染函数

组件化简介

组件化可以让我们方便的将一个页面拆分成多个可重用的组件

使用组件可以使我们重用页面中的内容

组件之间也可以进行嵌套

什么是Vue组件?

  • 一个Vue组件就是一个拥有预定义选项的Vue实例
  • 一个组件可以组成页面上功能完备的区域,组件可以包含脚本(script)、样式(style)、模板(template)

组件注册

全局组件

全局组件使用Vue静态方法component来完成注册

  • 使用示例

    Vue.component('component-a', { } )

  • 定义位置

    src\core\global-api\assets.js

  • 源码解析

    export function initAssetRegisters (Vue: GlobalAPI) {
      /**
       * Create asset registration methods.
       */
      // 初始化全局component、directive、filter的注册
      ASSET_TYPES.forEach(type => {
        Vue[type] = function (
          id: string,
          definition: Function | Object
        ): Function | Object | void {
          if (!definition) {
            return this.options[type + 's'][id]
          } else {
            /* istanbul ignore if */
            if (process.env.NODE_ENV !== 'production' && type === 'component') {
              validateComponentName(id)
            }
            // Vue.component('comp', { template: '' })
            if (type === 'component' && isPlainObject(definition)) {
              // 没有指定name选项,使用id作为组件名
              definition.name = definition.name || id
              // 把组件配置转换成组件的构造函数
              definition = this.options._base.extend(definition)
            }
            if (type === 'directive' && typeof definition === 'function') {
              definition = { bind: definition, update: definition }
            }
            // 全局注册,存储资源并赋值
            // this.options['components']['comp'] = definition
            this.options[type + 's'][id] = definition
            return definition
          }
        }
      })
    }
    

Vue.extend

将组件配置转换成组件的构造函数

组件的构造函数继承自Vue的构造函数,因此组件对象拥有与Vue实例同样的成员(组件本身就是Vue实例)

Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    // Vue构造函数
    const Super = this
    const SuperId = Super.cid
    // 从缓存中加载组件的构造函数 有就直接返回
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      // 验证组件名称
      validateComponentName(name)
    }

    // 组件Sub构造函数
    const Sub = function VueComponent (options) {
      // 调用_init()初始化
      this._init(options)
    }
    // 为Sub继承Vue原型
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    // 合并options
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    
    // 一系列初始化工作
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    // 从Vue继承静态方法
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    // 初始化组件的component、directive、filter的注册
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    // 保存组件构造函数到Ctor.options.components.comp = Ctor
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    
    // 保存options引用
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    // 缓存组件的构造函数
    cachedCtors[SuperId] = Sub
    // 返回组件构造函数
    return Sub
  }

局部组件

局部组件使用components选项注册

  • 使用示例

    new Vue({
      el: '#app',
      components: {
        'component-a': ComponentA,
        'component-b': ComponentB
      }
    })
    

组件的创建过程

组件本身也是Vue的实例,组件的创建过程也会执行Vue的一系列创建过程,并调用createElement创建VNode

当判断要创建的VNode是自定义组件时,执行createComponent创建并返回自定义组件的VNode

// src\core\vdom\create-component.js
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  // 如果Ctor是对象而不是构造函数
  // 使用Vue.extend()创建组件的构造函数
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  // 异步组件处理
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  // 处理组件选项
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  // 处理组件v-model指令
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  // 安装组件的钩子函数 init/prepatch/insert/destroy
  // 组件实例在init钩子函数中调用createComponentInstanceForVnode创建
  installComponentHooks(data)

  // return a placeholder vnode
  // 获取组件名称
  const name = Ctor.options.name || tag
  // 创建组件对应VNode对象
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}

组件的patch过程

组件实例在init钩子函数中调用createComponentInstanceForVnode创建的

init钩子函数在patch过程中被调用

// src\core\vdom\patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        // 调用init钩子函数,创建和挂载组件实例
        // init()的过程中创建了组件的真实DOM,挂载到了vnode.elm
        i(vnode, false /* hydrating */)
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
        // 调用钩子函数
        // 1. VNode钩子函数初始化属性/事件/样式等
        // 2. 组件的钩子函数
        initComponent(vnode, insertedVnodeQueue)
        // 将组件对应的DOM插入到父元素中
        // 先创建父组件 后创建子组件
        // 先挂载子组件 后挂载父组件
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

组件总结

  • Vue组件是Vue实例,选项不同
  • 父子组件创建顺序是先创建父组件,后创建子组件
  • 父子组件挂载顺序是先挂载子组件,后挂载父组件
  • 通过查看源码我们知道,每个组件的创建过程都会执行一系列Vue实例初始化过程,因此组件化的粒度并不是越细越好,合理的抽象组件结构与组件嵌套,可以避免不必要的性能消耗
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容