最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 重学Vue【异步组件原理分析】

    正文概述 掘金(道道里)   2021-03-10   585

    重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈)github 上。

    正文

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

    我们可以通过上面的例子创建一个全局组件,和上篇分析组件注册有点不一样的是,例子中注册的全局组件不是一个对象,而是一个工厂函数,函数有两个参数 resolvereject,下面来看下定义对象和定义工厂函数的区别。

    回顾一下在组件注册中提到的 assets

    if (type === 'component' && isPlainObject(definition)) {
      definition.name = definition.name || id
      definition = this.options._base.extend(definition)
    }
    // ...
    this.options[type + 's'][id] = definition
    return definition
    

    如果传入的是一个组件,就设置 name,然后把传入的 definition 通过 Vue.extend 转化成一个构造器,那如果定义的是工厂函数,就会把这个工厂函数赋值给大 Vue.options.components,上面的例子就成了 Vue.options.components.example = definition

    接着在创建组件的vnode的时候,会执行 _createElement 方法:

    if (typeof tag === 'string') {
      let Ctor
      ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
      if (config.isReservedTag(tag)) {
        // platform built-in elements
        vnode = new VNode(
          config.parsePlatformTagName(tag), data, children,
          undefined, undefined, context
        )
      } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
        // component
        vnode = createComponent(Ctor, data, context, children, tag)
      } else {
        // unknown or unlisted namespaced elements
        // check at runtime because it may get assigned a namespace when its
        // parent normalizes children
        vnode = new VNode(
          tag, data, children,
          undefined, undefined, context
        )
      }
    } else {
      // direct component options / constructor
      vnode = createComponent(tag, data, context, children)
    }
    

    和组件注册类似,也会走到 resolveAsset 里面,然后执行下面的 createComponent,这里传入的 Ctor 就是工厂函数了,接着看下 createComponent 里面:

    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)
      }
      
      // ...
    
      // async component
      let asyncFactory
      if (isUndef(Ctor.cid)) {
        asyncFactory = Ctor
        Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
        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
          )
        }
      }
    }
    

    由于传入的 Ctor 是一个函数,所以不会执行 baseCtor.extend(Ctor),也就是不会执行 Vue.extend(Ctor),所以也就没有 cid,自然就进入了异步组件的创建逻辑。可以看到用 asyncFactory 保存了这个工厂函数,然后执行了一个 resolveAsyncComponent 方法,传入的参数分别是工厂函数、大Vue和当前实例,该方法定义在 src/core/vdom/helpers/resolve-async-component.js

    export function resolveAsyncComponent (
      factory: Function,
      baseCtor: Class<Component>,
      context: Component
    ): Class<Component> | void {
      if (isTrue(factory.error) && isDef(factory.errorComp)) {
        return factory.errorComp
      }
    
      if (isDef(factory.resolved)) {
        return factory.resolved
      }
    
      if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
        return factory.loadingComp
      }
    
      if (isDef(factory.contexts)) {
        // already pending
        factory.contexts.push(context)
      } else {
        const contexts = factory.contexts = [context]
        let sync = true
    
        const forceRender = () => {
          for (let i = 0, l = contexts.length; i < l; i++) {
            contexts[i].$forceUpdate()
          }
        }
    
        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()
          }
        })
    
        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()
          }
        })
    
        const res = factory(resolve, reject)
    
        if (isObject(res)) {
          if (typeof res.then === 'function') {
            // () => Promise
            if (isUndef(factory.resolved)) {
              res.then(resolve, reject)
            }
          } else if (isDef(res.component) && typeof res.component.then === 'function') {
            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 {
                setTimeout(() => {
                  if (isUndef(factory.resolved) && isUndef(factory.error)) {
                    factory.loading = true
                    forceRender()
                  }
                }, res.delay || 200)
              }
            }
    
            if (isDef(res.timeout)) {
              setTimeout(() => {
                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
      }
    }
    

    resolveAsyncComponent 函数的逻辑比较复杂,里面包含了3种异步处理组件的创建方式,除了上述的工厂函数,还支持 Promise 创建组件:

    Vue.component(
    	"example",
      () => import("./Component")
    )
    

    和高级异步组件(也就是官网提到的):

    const AsyncComp = () => ({
      // 需要加载的组件。应当是一个 Promise
      component: import('./MyComp.vue'),
      // 加载中应当渲染的组件
      loading: LoadingComp,
      // 出错时渲染的组件
      error: ErrorComp,
      // 渲染加载中组件前的等待时间。默认:200ms。
      delay: 200,
      // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
      timeout: 3000
    })
    Vue.component('async-example', AsyncComp)
    

    工厂函数异步组件

    直接跳过前面的几个if判断(它们是为高级组件用的),直接到 factory.contexts = [context],这个 context 就是Vue实例,后面使用了一个 once 函数来定义 resolvereject

    export function once (fn: Function): Function {
      let called = false
      return function () {
        if (!called) {
          called = true
          fn.apply(this, arguments)
        }
      }
    }
    

    一个简单的封装,确保传进来的函数只执行一次,之后的 called 就是true了,也就是说确保 resolvereject 只执行一次,因为多次使用异步组件的话,也只能 resolve 一次,接着会执行 factory(就是工厂函数),最后的返回值判断会返回一个 undefined

    再回到 createComponent 方法里,如果 Ctor 是一个 undefined,就执行一个 createAsyncPlaceholder 方法:

    export function createAsyncPlaceholder (
      factory: Function,
      data: ?VNodeData,
      context: Component,
      children: ?Array<VNode>,
      tag: ?string
    ): VNode {
      const node = createEmptyVNode()
      node.asyncFactory = factory
      node.asyncMeta = { data, context, children, tag }
      return node
    }
    

    这个方法创建了一个空vnode,也就是一个空的注释节点(就长 <!----> 这个样子插入到dom中,当一个占位符用),然后渲染这个节点,到这里这个异步组件加载完了,然后 resolve 这个组件,也就是执行:

    function ensureCtor (comp: any, base) {
      if (
        comp.__esModule ||
        (hasSymbol && comp[Symbol.toStringTag] === 'Module')
      ) {
        comp = comp.default
      }
      return isObject(comp)
        ? base.extend(comp)
        : comp
    }
    // ...
    const forceRender = () => {
      for (let i = 0, l = contexts.length; i < l; i++) {
        contexts[i].$forceUpdate()
      }
    }
    // ...
    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()
      }
    })
    

    这里的 res 就是最上面例子传入的 res,它其实就是代码里 export 出来的那个组件对象,然后执行 ensureCtor,这个方法做了一个兼容,传入的是一个 module 或者其他,最终都可以拿到 export 出来的对象,然后返回了一个异步组件的构造器,把它赋值给 factory.resolved,然后执行 forceRender 函数,这个函数就是遍历所有的vm实例,每个实例都执行一个 $forceUpdate,它定义在 src/core/instance/lifecycle.js:

    Vue.prototype.$forceUpdate = function () {
      const vm: Component = this
      if (vm._watcher) {
        vm._watcher.update()
      }
    }
    

    它调用了渲染watcher的 update,然后会走渲染watcher的 getter,最终就会走到 mountComponent 方法里的下面这一段:

    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
    

    也就是说通过 $forceUpdate,来强制让它渲染一次,这样就会强制又走 render,触发组件的重新渲染,然后就又会走一遍 createComponent 方法。

    然后又会执行到 resolveAsynComponent 方法里,再来看下上面忽略的前几个if判断:

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

    它的 factory.resolved 判断就成了 true(因为第一次把 ensureCtor 返回的构造器赋值给它),这样就会直接return出去,接着在 createComponent 里就不会执行 Ctor === undefined 这个逻辑了,接着就和普通的构造器Ctor走一样的逻辑了:安装组件钩子,创建组件vnode,patch……

    捋一下:由于 Ctor 是一个工厂函数,所以没有 cid,就会走 resolveAsyncComponent 方法,第一次 factory 参数在前面的if判断里都不满足,所以走到定义 resolvereject ,这里调用了 once 方法保证了 resolvereject 多次异步也只会走一次,接着走 factory(也就是传入的工厂函数),然后return一个 undefined。接着走 createComponentcreateAsyncPlaceholder 方法,返回一个空vnode(注释节点),然后会走一个patch过程(因为在 createComponent 里)。接着后面回调 resolve,传入代码里 export 出来的那个组件对象,会走 ensureCtor 函数并且赋值给 factory.resolved,然后通过 forceRender 调用 $forceUpdate 来强制渲染一次,接着会又执行到 resolveAsyncComponent 里面去,此时有了 factory.resolved 就直接return,后面就安装组件钩子,创建组件vnode,patch……,等patch之后,那个空vnode(注释节点)就被替换掉了。

    简化版:第一步创建了空注释节点,第二步渲染挂载DOM来替换空注释节点。

    Promise异步组件

    先来看下 Promise 的写法:

    Vue.component(
      'example',
      // 该 `import` 函数返回一个 `Promise` 对象。
      () => import('./Component')
    )
    

    在执行 resolveAsyncComponent 的时候,和工厂函数类似,不过在走到 res = factory(resovle, reject) 的时候,res其实是例子中 import 返回的 promise 对象,所以res就有了 then 方法,那就会走到下面的逻辑:

    // ...
    if (typeof res.then === 'function') {
      // () => Promise
      if (isUndef(factory.resolved)) {
        res.then(resolve, reject)
      }
    }
    

    Promise 方式的话,第一次的 factory.resolved 也是 undefined,所以就会走 res.then(resolve, reject)

    然后在异步组件加载成功之后,走这个 resolve,也就是走上面的定义好的resolve,如果加载失败就执行 reject:

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

    后面的逻辑就和工厂函数一样了。

    高级异步组件

    该写法是在2.3.0+新增的一种写法:

    const AsyncComp = () => ({
      // 需要加载的组件 (应该是一个 `Promise` 对象)
      component: import('./Component.vue'),
      // 加载中应当渲染的组件
      loading: {
    		template:"<div>loading</div>"
    	},
      // 出错时渲染的组件
      error: {
    		template:"<div>error</div>"
    	},
      // 渲染加载中组件前的等待时间,默认:200ms
      delay: 200,
      // 最长等待时间,超出该时间就渲染错误组件,默认:Infinity
      timeout: 3000
    })
    Vue.component("example", AsyncComp)
    

    这样就定义了一个高级异步组件,猛地一看有种节流的感觉,下面分析一下它的实现。

    它依然会走到 resolveAsyncComponent 方法里面,然后第一次执行的时候,最前面的几个if判断:errorresolvedloading 依然是 undefined,接着定义了 resolvereject 等,然后执行 factory,它会执行前面定义 AsyncComp 返回的对象,接着把它赋值给了 res,然后走到下面:

    if (isObject(res)) {
      if (typeof res.then === 'function') {
        // () => Promise
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      } else if (isDef(res.component) && typeof res.component.then === 'function') {
        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 {
            setTimeout(() => {
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true
                forceRender()
              }
            }, res.delay || 200)
          }
        }
    
        if (isDef(res.timeout)) {
          setTimeout(() => {
            if (isUndef(factory.resolved)) {
              reject(
                process.env.NODE_ENV !== 'production'
                ? `timeout (${res.timeout}ms)`
                : null
              )
            }
          }, res.timeout)
        }
      }
    }
    

    因为 res 是一个对象,不是一个Promise,所以没有then方法,同时 res 定义了 component(就是加载的异步组件),并且它有then方法,所以就走 res.component.then,接着后面做了一系列判断,如果定义了 error,就创建一个 error 的组件构造器并且扩展到 factory.errorComp 上,如果定义了 loading,就创建一个 loading 的组件构造器扩展到 factor.loadingComp 上,

    • 如果有loading,并且 delay 为0(也就是没有延迟时间),就直接把 loading 设置为true,这个 loading 为true的话,直接影响的就是返回值,返回值就会返回这个 factory.loadingComp,这样的话,返回值就不是 undefined 了,那走到 createComponent 的时候,就不会走 createAsyncPlaceholder 了,会直接渲染这个 loadingComp
    • 如果 delay 不是0,就定义一个定时器,注意此时 loading 还是没有的(定时器是异步的),这样返回值就还是 undefined,就会前面一样,渲染一个注释节点,如果后面组件没有加载成功,就会把 loading 设置为true,并且会执行 forceRender 方法,就又会重新进入 resolveAsyncComponent,此时如果有 resolved,就执行 resolved,如果 laoding 是true,并且有 loadingComp,就渲染 loadingComp

    接着判断 timeout,如果过了这个时间还没有 resolved 的话,就走 reject

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

    除了警告信息外,如果定义了 errorComp,就把 factory.error 设置为true,然后再调用 forceRender,这样再回来的时候,第一个判断:

    if (isTrue(factory.error) && isDef(factory.errorComp)) {
      return factory.errorComp
    }
    

    errorComp 就会执行(这个error判断放在方法的第一行,可见它的优先级是最高的),这样整体过程其实就是先一个空注释节点,再是替换成loading节点,最后替换成真正要展示的组件。

    总结

    1. 异步组件实现的本质是2次渲染,先渲染成注释节点,当组件加载成功后,再通过 forceRender 重新渲染(通常是2次,上面的loading例子其实是3次)。

    2. 第二种 Promise 的设计是有 webpackimport 语法的支持实现的。

    3. 三种创建异步组件的方式里,高级异步组件是最灵活的,基本上每个状态都会有一个判定,然后通过配置实现了 loadingresolverejecttimeout 4种状态。

    我的公众号:道道里的前端栈,每一天一篇前端文章,嚼碎的感觉真奇妙~


    起源地下载网 » 重学Vue【异步组件原理分析】

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元