vue中组件的理解

152 阅读3分钟

1. 定义

1.1全局定义

// 组件定义 
Vue.component('comp', { 
    template: '<div>xxxxx</div>' 
    }) 

源码分析 src/core/global-api/assets.js

import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'

export function initAssetRegisters (Vue: GlobalAPI) {
//ASSET_TYPES =  [ 'component',  'directive',  'filter']

//最终输出: 
//Vue['component']('comp',{}) 定义组件 等价于Vue.component('comp',{})
//Vue['component']('comp') 根据组件id获取组件
  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)
        }
        //definition刚开始是组件的配置对象如{   template: '<div>xxxxx</div>'   }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)//通过extend创建自定义的子构造函数,并且赋值给definition,此时definition变成了构造函数
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        //this.options['components'].comp = definition
        this.options[type + 's'][id] = definition //注入到全局vue的配置,给后面所有子组件merge合并继承使用。
        return definition
      }
    }
  })
}

src/core/global-api/extend.js

import { ASSET_TYPES } from 'shared/constants'
import { defineComputed, proxy } from '../instance/state'
import { extend, mergeOptions, validateComponentName } from '../util/index'

export function initExtend (Vue: GlobalAPI) {
  Vue.cid = 0
  let cid = 1
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    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)
    }

    //创建一个vuecomponent的类
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    //Sub子类通过原型链 继承与 Vue类 原型链拷贝
    Sub.prototype = Object.create(Super.prototype) //这里的super 来自与this,this又是vue
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    //选项合并,把上面全局vue的 components 也合并到单独的vue组件里。
    //为什么全局组件能再子组件生效因为这里合并
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super
    
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    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.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
} 

1.2单文件定义

<template> 
    <div>xxx</div> 
</template> 

单个文件配置,会被vue-loader转化成render渲染函数,渲染函数得到vnode(组件配置对象)。

由于单文件最终是以局部组件存在,则是在patch打补丁时候动态创建生成。 src/core/vdom/patch.js createElm(vnode) -> createComponent(vnode) -> componentVNodeHooks.init(vnode)执行之前安装好的钩子 -> initComponent(vnode) -> child = createComponentInstanceForVnode(vnode) -> child.$mount()


  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested // for transition enter check
    //判断如果当前是组件则走 createComponent方法
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    
    //下面的代码是vm根组件的逻辑

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          creatingElmInVPre++
        }
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }

      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        if (!appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } else {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }
  
  
  //创建组件
  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)) {//这里调用初始化钩子函数
      //如:componentVNodeHooks的  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {}
        i(vnode, false /* hydrating */)
      }
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

  function initComponent (vnode, insertedVnodeQueue) {
    if (isDef(vnode.data.pendingInsert)) {
      insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
      vnode.data.pendingInsert = null
    }
    vnode.elm = vnode.componentInstance.$el
    if (isPatchable(vnode)) {
      invokeCreateHooks(vnode, insertedVnodeQueue) //初始化钩子函数
      setScope(vnode)
    } else {
      // empty component root.
      // skip all element-related modules except for ref (#3455)
      registerRef(vnode)
      // make sure to invoke the insert hook
      insertedVnodeQueue.push(vnode)
    }
  }

//src/core/vdom/create-component.js 定义所有初始化的钩子函数
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {//如果是keep-alive走缓存
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
    //这里开始创建组件实例,当然这里涉及到子vnode的递归创建。 所以组件创建是自上而下,而挂载是自下而上
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      //创建完立刻挂载
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  ...
  },

  insert (vnode: MountedComponentVNode) {
   ...
  },

  destroy (vnode: MountedComponentVNode) {
  ...
  }
}


//真正调用构造函数初始化的地方  new vnode.componentOptions.Ctor(options) 创建实例
export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)//使用之前定义好的构造函数创建实例
}

2. 优点

src/core/instance/lifecycle.js mountComponent()

  • 通过一个组件对应一个watcher,当data发生变化时候,只触发一个watcher的updateComponent更新方法。里面调用 vm._update(vm._render(), hydrating),触发渲染函数,最终通过patch打补丁的方式更新真实dom。
  • 只要我们合理的切割业务代码的颗粒度,把频繁更新的业务抽离成一个单独组件,那么组件的更新与打补丁都只会在该组件发生,其他不受任何影响。能大大提高刷新的效率。

// 挂载组件
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

3.总结

  1. 组件是独立和可复用的代码组织单元。用户可以使用小型,独立,可复用的组件构建大型应用。组件也是 Vue 核心特性之一。
  2. 组件化可以有效提高开发效率、测试性、复用性等。
  3. 组件应该是高内聚单一的、低耦合的(配合单向数据流,通过props传入)。是一种分治思想。
  4. 遵循单向数据流的原则。
  5. 组件按分类有:页面组件(具体单页面内容)、业务组件(用户等级图标组件)、通用组件(按钮,输入框)。
  6. 工程化的vue组件是基于配置的,如单个.vue组件是组件配置而非组件。通过vue-loader框架后续会生成其构造函数,它们基于VueComponent,扩展于Vue。
  7. vue中常见组件化技术有:属性prop,自定义事件,插槽等,它们主要用于组件通信、扩展等;
  8. 合理的划分组件,有助于提升应用性能。