Vue-详细说说异步组件和源码解析

vue应用在大部分情况下不用关心模块加载问题,但是当程序规模变得越来越大的时候,就需要考虑性能优化问题了。在这个过程中,你可能使用了拆分代码和延迟加载这两种方法,它们通过将代码块的加截推迟到需要的时候加载,从而使应用程序的初始包变得更小。
在程序中模块的加载方式一般有两种:静态加载和动态加载,像下面这样,

// 静态加载方式
import utils from './utils' 
// 动态加载方式
import('./utils').then(utils => { ... });

对于现代的打包器,比如Webpack(从version2开始)会将这种语法理解为自动为该模块创建一个单独的文件,然后在需要时进行加载。在Vue里也提供了按需加载的功能,叫异步组件,下面将从使用方式、源码等几个角度来说明。
从组件的使用方式来将,可以有:直接渲染异步组件和路由异步组件;而直接渲染异步组件一般是通过component的 is 属性和v-if来动态切换实现。

1、直接渲染异步组件

顾名思义就是在模版直接使用的组件,一般公共组件和应用子组件大都是这种用法,只不是加载的方式是按需加载

1.1 使用方法

Vue文档里提供了3中使用异步组件的方法,也是核心代码里内置实现的,这个后面再说,这三种方式如下:

import Vue from 'vue'
import asyncLoading from './componentss/async-loading-component.vue'
import asyncError from './componentss/async-error-component.vue'

Vue.component('async-webpack-example-c', function (resolve) {
  // 这个特殊的 `require` 语法将会告诉 webpack
  // 自动将你的构建代码切割成多个包,这些包
  // 会通过 Ajax 请求加载
  require(['./componentss/async-component-c.vue'], resolve)
})

Vue.component(
  'async-webpack-example-d',
  // 这个 `import` 函数会返回一个 `Promise` 对象。
  () => import('./componentss/async-component-d.vue')
)

const AsyncComponentA = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./componentss/async-component-a.js'),
  // 异步组件加载时使用的组件
  loading: asyncLoading,
  // 加载失败时使用的组件
  error: asyncError,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  // delay时间之后开开始加载并显示loading组件效果
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 15000
})
// 也可注册局部组件
{...
    components: {
        'aync-component-b': () => import('./componentss/async-component-b.js')
    }
...
}

可以使用v-if和component/is来切换使用,这里使用is特性为例:

<template>
    <div class="demo-item-group">
      <ul>
        <li v-for="item of comptIsNames" :key="item" @click="curComponentName = item"
            :class="{active: curComponentName === item}">{{item}}</li>
      </ul>
      <div>
        <component :is="curComponentName"></component>
      </div>
    </div>
</template>
<script>
export default {
  name: 'basic-async-component',
  data: function () {
    return {
      showCcc: false,
      comptIsNames: ['async-webpack-example-c', 'async-webpack-example-d', 'aync-component-b','aync-component-a'],
      curComponentName: 'global-compt-1'
    }
  },
  components: {
    'aync-component-b': () => import('./componentss/async-component-b.js'),
    'aync-component-a': AsyncComponentA
  }
}
</script>

想查看loading效果可以打开devTools-network-presets改为slow 3G,查看加载错误可设置为Offline

1.2 看源码

看源码能更好的理解异步组件的内部实现,从而更好的使用它们。
异步组件实现的本质是 2 次渲染,除了 0 delay 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 forceRender 强制重新渲染,这样就能正确渲染出异步加载的组件了。
异步组件也要create组件实例,但是由于其定义形式是工厂函数而不是对象,不会直接extend实例构造函数,而是走异步工厂那一套,如下,源文件在:vue/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') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component 异步组件处理
  // Ctor不是对象,不会走上面那一套,Ctor.cid未定义
  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
      )
    }
  }
  ...
  return vnode
}

这里resolveAsyncComponent是处理异步组件的主函数,createAsyncPlaceholder就是第一次渲染生成一个注释节点,在高级异步组件形式的delay=0的情况下直接渲染Ctor返回undefined不生成注释节点,其他情况下都会先生成注释节点,然后再执行resolveAsyncComponent根据加载情况不同进行组件的渲染。
来看一下resolveAsyncComponent,源码位置:vue/src/core/vdom/helpers/resolve-async-component.js,

export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {
  // 处理加载error,一般在再次执行次方法式会实际处理到
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }
  // 处理加载成功resolved,一般在再次执行次方法式会实际处理到
  if (isDef(factory.resolved)) {
    return factory.resolved
  }
  // 处理多个vm实例加载一个异步组件的情况,只加载实例化一次
  const owner = currentRenderingInstance
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    // already pending
    factory.owners.push(owner)
  }
  // 处理加载loading,一般在再次执行次方法式会实际处理到
  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++) {
        (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>) => {
      // 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
      }
    })
    // 包装异步组件加载成功reject方法
    // 标记出错,下一次执行次函数便可处理error,如有error组件可获得实例
    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)) {
      if (isPromise(res)) {
        // () => Promise
        // 处理() => Promise形式异步组件
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      } else if (isPromise(res.component)) {
        // 处理高级异步组件
        res.component.then(resolve, reject)
        // 处理高级异步组件的err组件
        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }
        // 处理高级异步组件的loading组件
        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
           // 处理高级异步组件的加载延时delay
          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)
          }
        }
        // 处理高级异步组件的timeout
        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
  }
}

这个方法比较复杂,它处理了上文说到的3 种异步组件的创建方式,具体直接看上面的代码中已标好的注释。

1.3 封装动态异步组件

封装一个可以根据配置动态加载任意组件的公共组件,这是在某些时候是有需要的。
在vue中动态组件的典型方式是通过该component和is属性来实现,而动态异步组件就是动态+异步,就是这个思路实现而来。
示例如下:

// vc-async-component.vue
<template>
  <transition v-if="transition && keepAlive" mode="out-in"
              :enter-class="transitionClass.enter"
              :enter-active-class="transitionClass.enterActive"
              :enter-to-class="transitionClass.enterTo"
              :leave-class="transitionClass.leave"
              :leave-active-class="transitionClass.leaveActive"
              :leave-to-class="transitionClass.leaveTo">
    <keep-alive>
      <component :is="componentName" v-bind="$attrs" v-on="$listeners"></component>
    </keep-alive>
  </transition>
  <transition v-else-if="transition && !keepAlive" mode="out-in"
              :enter-class="transitionClass.enter"
              :enter-active-class="transitionClass.enterActive"
              :enter-to-class="transitionClass.enterTo"
              :leave-class="transitionClass.leave"
              :leave-active-class="transitionClass.leaveActive"
              :leave-to-class="transitionClass.leaveTo">
    <component :is="componentName" v-bind="$attrs" v-on="$listeners"></component>
  </transition>
  <keep-alive v-else-if="!transition && keepAlive">
    <component :is="componentName" v-bind="$attrs" v-on="$listeners"></component>
  </keep-alive>
  <component v-else :is="componentName" v-bind="$attrs" v-on="$listeners"></component>
</template>

<script>
import vcAsyncLoading from './vc-async-loading'
import vcAsyncError from './vc-async-error'

export default {
  inheritAttrs: false,
  name: 'vc-async-component',
  props: {
    path: {
      type: String,
      required: true
    },
    keepAlive: {
      type: Boolean,
      default: false
    },
    transition: {
      type: Boolean,
      default: false
    },
    delay: {
      type: Number,
      default: 200
    },
    timeout: {
      type: Number,
      default: 3000
    },
    transitionClass: {
      type: Object,
      default: function () {
        return {
          enter: 'v-enter',
          enterTo: 'v-enter-to',
          enterActive: 'v-enter-active',
          leave: 'v-leave',
          leaveTo: 'v-leave-to',
          leaveActive: 'v-leave-active'
        }
      }
    }
  },
  data: function () {
    return {
      componentName: () => ({
        component: import(`@/${this.path}`),
        loading: vcAsyncLoading,
        error: vcAsyncError,
        delay: this.delay,
        timeout: this.timeout
      })
    }
  },
  watch: {
    path: function () {
      this.componentName = () => ({
        component: import(`@/${this.path}`),
        loading: vcAsyncLoading,
        error: vcAsyncError,
        delay: this.delay,
        timeout: this.timeout
      })
    }
  }
}
</script>
<style scoped>
.v-enter,.v-leave-to {
  opacity: 0;
}
.v-enter-to,.v-leave {
  opacity: 1;
}
.v-enter-active,.v-leave-active {
  transition: opacity .5s;
}
</style>

使用方式:

<template>
  <div>
    <vc-async-component :path="comptName" :timeout="timeoutLen" :keep-alive="isKeepAlive"
                        :transition="isTransition" :transition-class="asyncClass">
    </vc-async-component>
  </div>
</template>
<script>
import Vue from 'vue'
export default {
  name: 'basic-async-component',
  data: function () {
    return {
      comptName: 'components/basic/componentss/async-component-c.vue',
      isKeepAlive: false,
      isTransition: false,
      asyncClass: {
        enter: 'async-enter',
        enterActive: 'async-enter-active',
        enterTo: 'async-enter-to',
        leave: 'async-leave',
        leaveActive: 'async-leave-active',
        leaveTo: 'async-leave-to'
      }
    }
  },
  ...
}
</script>

实例效果可进入vc-async-component的异步组件-动态异步组件,组件名目前就下面几个有效组件,可随意切换加载查看

  • components/basic/componentss/async-component-a
  • components/basic/componentss/async-component-b
  • components/basic/componentss/async-component-c
  • components/basic/componentss/async-component-d

输入其他不可识别的组件名就会加载错误,显示error-component

2、路由异步组件

在vue-router中同样需要路由组件的懒加载,把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样更加高效。
结合上面说的Vue的异步组件和Webpack的代码分割功能,可以轻松实现路由组件的懒加载。

2.1 普通异步组件的路由懒加载

const routes = [
  { path: '/', redirect: '/hello' },
  { 
    path: '/hello',
    component: () => import('@/components/hello')
  }
]

2.2 高级异步组件的路由懒加载

import asyncLoading from '@/components/vc-cat/vc-async-loading.vue'
import asyncError from '@/components/vc-cat/vc-async-error.vue'

function lazyLoadView () {
  const asyncComponentForm = () => ({
    component: import('@/components/hello/hello-form'),
    loading: asyncLoading,
    error: asyncError,
    delay: 200,
    timeout: 8000
  })
  return Promise.resolve({
    functional: true,
    render (h, {data, children}) {
      return h(asyncComponentForm, data, children)
    }
  })
}

const routes = [
  { 
    path: '/form',
    component: () => lazyLoadView()
  }
]

参考文档:https://blog.csdn.net/refreeom/article/details/90437394

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 暴裂无声 这真是一个适合在春天看的电影,春天是个希望的季节,这是个充斥无望的电影。 底层人民终究会由于自己的鲁莽也...
    丁圆圆阅读 898评论 0 0
  • 门前桂子飘香,满树金黄,香气弥漫四溢,走得老远,却仍是口颊存香气,衣衫发沾香。 桂花诗很多,多得像...
    采葭小妖阅读 382评论 0 1
  • 一个人,如果连自己的情绪都控制不了,即便给你整个世界,你也早晚毁掉一切。 你成不了心态的主人,必然会...
    畅然随笔阅读 236评论 0 0
  • 果然,没多久人事下发通知,由于公司被收购,薪资及职位可能会有所调整,甚至会有裁员情况,让大家做好心理准备。 办公室...
    灵魂搜索者阅读 82评论 0 0
  • 常用Lambda处理 List to Map<Long,UserDto> Map<Long,UserDto>...
    lbcBoy阅读 665评论 0 0