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

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

作者: 彪悍de文艺青年 | 来源:发表于2021-01-11 00:12 被阅读0次

    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实例初始化过程,因此组件化的粒度并不是越细越好,合理的抽象组件结构与组件嵌套,可以避免不必要的性能消耗

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/yfrraktx.html