vue2源码解析之处理组件

765 阅读8分钟

基本使用

全局定义组件:

// 通过Vue.component()的形式 定义一个b组件
Vue.component('b', {
      template: '<p>{{ a }}{{ name }}</p>',
      data () {
        return {
          a: 999
        }
      },
      created(){
        // ('子组件的created')
      },
      beforeMount(){
        // ('子组件的beforeMount')
      }
})

局部定义组件:

new Vue({
     components: {
        b: {
          template: '<a>{{ name }}</a>',
          data(){
            return {
              name: '我是组件'
            }
          }
        }
      },
})

全局定义的组件是通过构造器上的component方法实现的;这个方法在全局API篇已经分析过,下面在简单回顾下:

component方法

// 源码
ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      // 不存在definition 表示获取,直接从options下的相应的typs+'s'进行获取
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        // 如果是组件
        if (type === 'component' && isPlainObject(definition)) {
          // 没有name就使用id
          definition.name = definition.name || id
          // 使definition继承vue
          definition = this.options._base.extend(definition)
        }
        // 设置值
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
  
 // 为了便于理解根据源码进行改造
  Vue.component = function (id, definition) {
    definition.name = definition.name || id
    // 创建子组件的类,并且继承自父组件
    definition = this.options._base.extend(definition)
    // 进行记录
    Vue.options.components[id] = definition
  }

从上面代码可以看出,组件只做了两件事,第一就是把name或id赋值给组件配置项的name属性,然后执行extend方法创建此组件,最后放在options下的components中;extend也在全局API篇分析过,下面也进行简单的回顾;

extend方法

 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]
    }
    // 获取到name
    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }
    // 创建一个子类
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    // 原型继承
    Sub.prototype = Object.create(Super.prototype)
    // 修改构造器
    Sub.prototype.constructor = Sub
    // 子类的唯一标识
    Sub.cid = cid++
    // 合并父类和传递进来的参数
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    // 子类中存储父类
    Sub['super'] = Super

    // 如果子类中有props 进行初始化
    if (Sub.options.props) {
      initProps(Sub)
    }
    // 如果子类中有计算属性,进行初始化
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // 把父类的extend,mixin,use放在子类上
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // 把指定的属性全部从父类上拷贝到子类上
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    if (name) {
      Sub.options.components[name] = Sub
    }
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    // 缓存子类
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

extend中主要定义了一个子类,通过原型继承的方式继承之父类;子类内部主要调用了_init方法进行初始化操作;然后进行一些属性的合并,并且把子类存储到缓存中,在函数开始的时候先从缓存中获取;

以上就完成了组件的初始化操作;下面就是通过编译模板解析模板中使用到的组件,如何进行识别创建和挂载的过程;

大概流程

  1. 模板编译之后,在挂载dom的时候,会把render函数进行执行生成Vnode,render函数内部会根据不同类型节点调用不同的函数生成不同的vnode;
  2. 当为元素节点的时候会调用创建元素类型的Vnode函数,此函数内部通过元素的标签名进行判断是否是原始的标签,如果不是原始的标签则为组件;
  3. 如果为组件,则从选项的components中获取到对应的组件数据
  4. 获取到组件数据之后就可以判断出是全局添加的组件还是局部添加的组件
  5. 局部添加的组件就调用父级的extend方法实现组件的继承并且返回子组件的构造函数
  6. 创建组件对应的Vnode
  7. 当通过patch根据Vnode创建真实的dom的时候,会判断当前Vnode是否是组件,如果是组件就执行组件的构造函数,创建子组件的实例,再调用子组件的$mout方法进行生成真实的dom挂载到$el上;

render函数中生成对应的Vnode,在模板编译篇解析过,render函数根据不同的节点类型使用不同的函数进行包裹执行,元素节点通过_c进行包裹,所以具体看下_c函数

// 源码位置 ./src/core/instance/render.js

export function initRender (vm: Component) {
 ...
  // _c创建虚拟节点的函数 内部使用
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  ...
}

可以看到_c函数内部执行了createElement函数并且返回其结果;

// 源码位置 ./src/core/vdom/create-element.js
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  ...
  return _createElement(context, tag, data, children, normalizationType)
}

createElement函数内部又调用了_createElement函数;

// 源码位置 ./src/core/vdom/create-element.js
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  ...
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

如果是原始标签就创建对应的虚拟dom

if (config.isReservedTag(tag)) {
  vnode = new VNode(
    config.parsePlatformTagName(tag), data, children,
    undefined, undefined, context
  )
}

如果是组件就调用createComponent方法

else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
  // component
  vnode = createComponent(Ctor, data, context, children, tag)
}

createComponent函数

// 源码位置 ./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
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  ...
  // install component management hooks onto the placeholder node
  installComponentHooks(data)
  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}

如果ctor不存在直接返回,通过_base获取到Vue的构造器,判断Ctor是否是一个对象,如果是对象表示是内部组件,通过extend创建Ctor组件的类,继承子Vue;

if (isUndef(Ctor)) {
    return
}
const baseCtor = context.$options._base
if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
}

调用installComponentHooks方法,此方法就是用来合并外部和组件内部的一些方法挂载到data的hook下,给后续创建对应的真实的dom的时候使用;主要分析下hook中的init方法

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
     const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  },
  ...
}
const hooksToMerge = Object.keys(componentVNodeHooks)

function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}
function mergeHook (f1: any, f2: any): Function {
  const merged = (a, b) => {
    // flow complains about extra args which is why we use any
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}

init方法内部调用了createComponentInstanceForVnode方法执行了当前组件的构造器,返回了当前组件的实例,并且赋值给componentInstance,最后调用mount进行创建真实的dom;(此方法就是用来创建组件的实例,从而可以调用Vue的init进行初始化组件自身的一些属性和方法,初始完毕之后就执行mout方法创建组件的真实dom并且挂载到组件实例的$el上)

export function createComponentInstanceForVnode (
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any
): Component {
  return new vnode.componentOptions.Ctor(options)
}

createComponentInstanceForVnode函数内部new 组件的类并且返回实例;到此installComponentHooks方法就执行完毕;接着就创建了组件的虚拟dom;

const name = Ctor.options.name || tag
const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
)

以上就是创建好了组件的Vnode;render函数执行完毕创建好对应的Vnode;接下来就进行挂载创建真实的dom;挂载到创建的整个流程可以看源码解析篇的整体流程,具体的dom创建和diff算法可以看diff算法篇;下面直接是创建真实dom中如何进行处理组件的内容;

// 源码位置 ./src/core/vdom/patch.js

  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    ....
  }

createElm函数就是用来创建真实dom的,首先调用了createComponent函数进行判断处理组件的Vnode;

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

获取到组件的data属性,如果data存在就继续往下执行;

if (isDef(i = i.hook) && isDef(i = i.init)) {
    i(vnode, false /* hydrating */)
}

获取到data中的hook,再获取到hook中的init,如果init存在,就执行init函数并且把虚拟节点传递进去,此步就和上面分析的componentVNodeHooks下的init函数联系起来了,init函数内部创建了组件的实例从而初始化组件的数据和创建真实的dom;

if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)

如果组件的实例已经存在了,那么就进行初始化组件的,接着就把组件直接插入到父级元素中;

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


function insert (parent, elm, ref) {
    if (isDef(parent)) {
      if (isDef(ref)) {
        if (nodeOps.parentNode(ref) === parent) {
          nodeOps.insertBefore(parent, elm, ref)
        }
      } else {
        nodeOps.appendChild(parent, elm)
      }
    }
  }

如果是keepAlive就通过reactivateComponent函数处理

if (isTrue(isReactivated)) {
  reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}

function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i
    let innerNode = vnode
    while (innerNode.componentInstance) {
      innerNode = innerNode.componentInstance._vnode
      if (isDef(i = innerNode.data) && isDef(i = i.transition)) {
        for (i = 0; i < cbs.activate.length; ++i) {
          cbs.activate[i](emptyNode, innerNode)
        }
        insertedVnodeQueue.push(innerNode)
        break
      }
    }
    insert(parentElm, vnode.elm, refElm)
}

异步组件

基本使用

// 第一种方式 回调函数
Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的 `require` 语法将会告诉 webpack
  // 自动将你的构建代码切割成多个包,这些包
  // 会通过 Ajax 请求加载
  require(['./my-async-component'], resolve)
})

// 第二种方式 promise
Vue.component(
  'async-webpack-example',
  // 这个 `import` 函数会返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

// 第三种方式 高级玩法
const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

在上面分析的创建组件的vnode期间,在createComponent函数内部也处理了异步组件;

// 源码位置 ./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
  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') {
    return
  }

  // async component
  // 异步组件
  let asyncFactory
  // 如果没有cid表示异步组件,因为不会调用extend方法
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    // 处理异步组件
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    // 如果没有结果 定义并且返回一个占位组件
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }
 ...
}

以上代码。首先判断Ctor是否是一个对象,是一个对象表示它是非异步组件,因为异步组件是函数;通过cid来判断是不是异步组件,如果cid不存在表示异步组件,因为它没有执行extend方法,通过resolveAsyncComponent函数处理异步组件函数,如果返回的记过没有值,那么就创建并且返回一个占位组件;

// 源码位置 ./src/core/vdom/helpers/resolve-async-component.js
export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {
    // 强制渲染函数
    const forceRender = (renderCompleted: boolean) => {
      for (let i = 0, l = owners.length; i < l; i++) {
        (owners[i]: any).$forceUpdate()
      }

      if (renderCompleted) {
        owners.length = 0
        if (timerLoading !== null) {
          clearTimeout(timerLoading)
          timerLoading = null
        }
        if (timerTimeout !== null) {
          clearTimeout(timerTimeout)
          timerTimeout = null
        }
      }
    }
    // resolve函数
    const resolve = once((res: Object | Class<Component>) => {
      // ensureCtor函数内部 通过extend方法创建组件的构造器
      factory.resolved = ensureCtor(res, baseCtor)
      // 强制渲染
      if (!sync) {
        forceRender(true)
      } else {
        owners.length = 0
      }
    })
    // reject函数
    const reject = once(reason => {
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender(true)
      }
    })
    // 执行异步组件的函数并且把resolve和reject传递进去执行  实现第一种定义异步组件的方法
    const res = factory(resolve, reject)
    // 判断返回的结果是不是一个对象
    if (isObject(res)) {
      // 是不是一个promise  实现第二种定义异步组件的方法
      if (isPromise(res)) {
        // 如果factory.resolved不存在表示没有执行过resolve方法
        if (isUndef(factory.resolved)) {
          // 通过then传递resolve和reject
          res.then(resolve, reject)
        }
        // 如果有component属性,实现第三种定义异步组件的方法
      } else if (isPromise(res.component)) {
      // 通过then执行resolve和reject
        res.component.then(resolve, reject)
        // 如果有error属性 定义错误组件
        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }
        // 如果有laoding 定义加载组件
        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
          if (res.delay === 0) { // 没有延迟属性直接启动加载
            factory.loading = true
          } else {
            timerLoading = setTimeout(() => {
              timerLoading = null
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true
                forceRender(false)
              }
            }, res.delay || 200)
          }
        }
        
        if (isDef(res.timeout)) {
          timerTimeout = setTimeout(() => {
            timerTimeout = null
            if (isUndef(factory.resolved)) {
              reject(
                process.env.NODE_ENV !== 'production'
                  ? `timeout (${res.timeout}ms)`
                  : null
              )
            }
          }, res.timeout)
        }
      }
    }

    sync = false
    // 有加载属性就返回加载组件否则就是异步组件
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
}

resolveAsyncComponent方法内部主要定义了resolve和reject函数,并且执行了异步组件的函数,并且把这两个方法传递进去,此时就实现了异步组件的第一种使用方式;接着通过判断返回的结果的类型来实现其他两种方式;如果是promise,那么就通过then传递Resolve和reject方法,此步实现第二种方式;如果结果有component属性,就通过component的then方法传递resolve和reject,又处理了loading,error等属性并且创建了对应的组件,此步实现了第三种;最后判断如果有加载属性就返回加载组件,否则就是当前异步组件;

大概流程:

  • 通过cid判断是否是异步组件,异步组件就创建resolve和reject方法,执行并传递resolve和reject到异步组件函数中,最后判断有没有加载属性,如果有就返回加载组件的构造器,否则就返回当前异步组件的构造器,此时可能还未执行resolve或reject,因此返回的组件的构造器是undefined;
  • 函数外部判断返回的结果如果是undefined,那么就创建一个空的虚拟节点表示占位组件;
  • 生成加载组件的虚拟节点
  • patch创建真实节点,此时加载组件就会渲染到页面上
  • 如果此时异步组件执行了resolve方法,那么在Resolve函数内部会创建异步组件的构造器,并且通过$forceUpdate进行强制更新,此时就会执行watcher的update方法更新视图;把异步组件渲染到页面上