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的高级组件本质都是两次渲染,第一次生成注释节点占位。
  • 编程思想:工厂函数以及单例模式
相关推荐
DN金猿7 分钟前
使用npm install或cnpm install报错解决
前端·npm·node.js
丘山子7 分钟前
一些鲜为人知的 IP 地址怪异写法
前端·后端·tcp/ip
志存高远6620 分钟前
Kotlin 的 suspend 关键字
前端
www_pp_32 分钟前
# 构建词汇表:自然语言处理中的关键步骤
前端·javascript·自然语言处理·easyui
YuShiYue1 小时前
pnpm monoreop 打包时 node_modules 内部包 typescript 不能推导出类型报错
javascript·vue.js·typescript·pnpm
天天扭码1 小时前
总所周知,JavaScript中有很多函数定义方式,如何“因地制宜”?(ˉ﹃ˉ)
前端·javascript·面试
一个专注写代码的程序媛1 小时前
为什么vue的key值,不用index?
前端·javascript·vue.js
vvilkim1 小时前
React 与 Vue:两大前端框架的深度对比
vue.js·react.js·前端框架
장숙혜1 小时前
ElementUi的Dropdown下拉菜单的详细介绍及使用
前端·javascript·vue.js
火柴盒zhang1 小时前
websheet之 编辑器
开发语言·前端·javascript·编辑器·spreadsheet·websheet