vue2源码记录4

组件

组件注册

组件注册方式有两种:全局注册和局部注册。

全局注册

使用Vue.component(tagName, options)

js 复制代码
// 定义全局 api:Vue.component、Vue.filter、Vue.directive
// 主要逻辑就是:往 Vue.options 上存放对应的配置
// 最后,在 new Vue 的时候,通过 mergeOptions 将全局注册的组件合并到每个组件的配置对象中
export function initAssetRegisters (Vue: GlobalAPI) {
  // 创建注册方法
  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)
        }

        if (type === 'component' && isPlainObject(definition)) {
          // 组件名称设置:组件配置中有 name,使用组件配置中的 name,没有,使用 id
          definition.name = definition.name || id
          // 通过Vue.extend() 创建子组件,返回子类构造器
          definition = this.options._base.extend(definition)
        }

        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }

        // this.options.compoments[id] = definition
        // this.options.directives[id] = definition
        // this.options.filters[id] = definition
        // 在 new Vue 时通过 mergeOptions 将全局注册的组件合并到每个组件的配置对象中
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

在创建vnode时,使用isDef(Ctor = resolveAsset(context.$options, 'components', tag))判断是否调用createComponent。

js 复制代码
// 返回组件的构造函数,作为createComponent钩子的参数。
export function resolveAsset (
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }
  const assets = options[type]
  // check local registration variations first
  if (hasOwn(assets, id)) return assets[id]
  const camelizedId = camelize(id)
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  const PascalCaseId = capitalize(camelizedId)
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
  // fallback to prototype chain
  const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
    warn(
      'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
      options
    )
  }
  return res
}

判断顺序:id->驼峰形式->驼峰加首字母大写,因此,在使用Vue.component(id, definition)全局注册组件时,id可以市连字符、驼峰或首字母大写的形式。

局部注册

在组件内部使用conponents选项进行组件局部注册,把components合并到vm.$options.components上。

异步组件

异步组件的三种使用方式:

js 复制代码
Vue.component('async-example', function (resolve, reject) {
   // 这个特殊的 require 语法告诉 webpack
   // 自动将编译后的代码分割成不同的块,
   // 这些块将通过 Ajax 请求自动下载。
   require(['./my-async-component'], resolve)
})
js 复制代码
// Promise异步组件
Vue.component(
  'async-webpack-example',
  // 该 `import` 函数返回一个 `Promise` 对象。
  () => import('./my-async-component')
js 复制代码
// 高级异步组件,可以设置loading和error组件
const AsyncComp = () => ({
  // 需要加载的组件。应当是一个 Promise
  component: import('./MyComp.vue'),
  // 加载中应当渲染的组件
  loading: LoadingComp,
  // 出错时渲染的组件
  error: ErrorComp,
  // 渲染加载中组件前的等待时间。默认:200ms。
  delay: 200,
  // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  timeout: 3000
})
Vue.component('async-example', AsyncComp)

对异步组件的处理:

js 复制代码
  // 对异步组件的处理
  // async component
  let asyncFactory;
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor;
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
    // 处理高阶组件0delay创建loading组件,否则返回undefined,创建一个注释节点作为占位符,把asyncFactory和asyncMeta赋值给vnode,执行forceRender,触发组件重新渲染,则返回值不为undefined。
    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.

      // 是创建一个注释节点vnode
      return createAsyncPlaceholder(asyncFactory, data, context, children, tag);
    }
  }

Ctor是函数,不会执行Vue.extendcid是undefined,执行resolveAsyncComponent方法。

js 复制代码
export function resolveAsyncComponent(
  factory: Function,
  baseCtor: Class < Component >
): Class < Component > | void {
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

  if (isDef(factory.resolved)) {
    return factory.resolved
  }

  const owner = currentRenderingInstance
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    // already pending
    factory.owners.push(owner)
  }

  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

  if (owner && !isDef(factory.owners)) {
    const owners = factory.owners = [owner]
    let sync = true
    let timerLoading = null
    let timerTimeout = null

    ;
    (owner: any).$on('hook:destroyed', () => remove(owners, owner))

    const forceRender = (renderCompleted: boolean) => {
      for (let i = 0, l = owners.length; i < l; i++) {
        // $forceUpdate 的逻辑非常简单,就是调用渲染 watcher 的 update 方法,让渲染 watcher 对应的回调函数执行,也就是触发了组件的重新渲染。
        // 之所以这么做是因为 Vue 通常是数据驱动视图重 新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行 $forceUpdate 可以强制组件重新渲染一次。
        (owners[i]: any).$forceUpdate()
      }

      if (renderCompleted) {
        owners.length = 0
        if (timerLoading !== null) {
          clearTimeout(timerLoading)
          timerLoading = null
        }
        if (timerTimeout !== null) {
          clearTimeout(timerTimeout)
          timerTimeout = null
        }
      }
    }

    // once 确保包装的函数只执行一次,使用单例模式实现,多个地方初始化一个异步组件,实际加载只有一次
    const resolve = once((res: Object | Class < Component > ) => {
      // cache resolved
      factory.resolved = ensureCtor(res, baseCtor)
      // invoke callbacks only if this is not a synchronous resolve
      // (async resolves are shimmed as synchronous during SSR)
      if (!sync) {
        forceRender(true)
      } else {
        owners.length = 0
      }
    })

    const reject = once(reason => {
      process.env.NODE_ENV !== 'production' && warn(
        `Failed to resolve async component: ${String(factory)}` +
        (reason ? `\nReason: ${reason}` : '')
      )
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender(true)
      }
    })

    // 普通工厂函数异步组件执行
    const res = factory(resolve, reject)

    if (isObject(res)) {
      // promise 形式异步组件  isPromise: 判断是否 promise 对象的方法
      if (isPromise(res)) {
        // () => Promise
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      } else if (isPromise(res.component)) {
        // 高级异步组件
        res.component.then(resolve, reject)

        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }

        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
          if (res.delay === 0) {
            factory.loading = true
          } else {
          // 延时展示loading
            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 in case resolved synchronously
    return factory.loading ?
      factory.loadingComp :
      factory.resolved
  }
}

总结

  • 组件注册有全局和局部注册两种方式,本质是mergeOptions,执行components中的构造函数。
  • 全局组件的id有三种写法,异步组件有三种类型。
  • 异步组件除了0delay的高级组件本质都是两次渲染,第一次生成注释节点占位。
  • 编程思想:工厂函数以及单例模式
相关推荐
仰望.几秒前
vue 甘特图 vxe-gantt table 依赖线的使用,配置连接线
vue.js·甘特图
小时前端几秒前
谁说 AI 历史会话必须存后端?IndexedDB方案完美翻盘
前端·agent·indexeddb
wordbaby5 分钟前
TanStack Router 基于文件的路由
前端
wordbaby10 分钟前
TanStack Router 路由概念
前端
wordbaby12 分钟前
TanStack Router 路由匹配
前端
cc蒲公英13 分钟前
vue nextTick和setTimeout区别
前端·javascript·vue.js
程序员刘禹锡18 分钟前
Html中常用的块标签!!!12.16日
前端·html
我血条子呢28 分钟前
【CSS】类似渐变色弯曲border
前端·css
DanyHope29 分钟前
LeetCode 两数之和:从 O (n²) 到 O (n),空间换时间的经典实践
前端·javascript·算法·leetcode·职场和发展
hgz071030 分钟前
企业级多项目部署与Tomcat运维实战
前端·firefox