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

2021-01-11  本文已影响0人  彪悍de文艺青年

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

模板编译简介

模板编译主要目的是将模板(template)转换为渲染函数(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函数

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字符串

为什么要使用抽象语法树

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

组件化简介

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

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

组件之间也可以进行嵌套

什么是Vue组件?

组件注册

全局组件

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

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选项注册

组件的创建过程

组件本身也是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
      }
    }
  }

组件总结

上一篇下一篇

猜你喜欢

热点阅读