最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 【vue】异步组件是怎么渲染的?

    正文概述 掘金(丢了帽子的路飞)   2021-04-05   659

    1、从一个例子开始

    为了更好的说明,异步组件会在1分钟后被resolve

    Vue.component('AsyncComp', (resolve) => {
      setTimeout(() => {
        resolve({
          props: {
            message: String
          },
          data() {
            return {
              msg: "1"
            }
          },
          render(h) {
            return h('div', [
              h('div', 'hello'),
              h('div', this.message),
            ])
          }
        })
      }, 60 * 1000)
    })
    
    new Vue({
      el: '#app',
      data() {
        return {
          msg: "1"
        }
      },
      methods: {
        change() {
          this.msg = String(Math.random())
        }
      },
      render(h) {
        return h('div', [
          h('button', {
            on: {
              click: this.change
            }
          }, 'change'),
          h('AsyncComp', {
            props:{
              message: this.msg
            }
          })
        ])
      }
    })
    

    2、$mount作为入口

    runtime版的vue入口文件中$mount是这样定义的。这里提一下compiler版的vue入口重写了$mount方法,多了一个将template编译成render方法的过程,因为接下来的逻辑render是一个很重要的东西。

    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && inBrowser ? query(el) : undefined
      return mountComponent(this, el, hydrating)
    }
    

    3、mountComponent

    mountComponent里面会创建一个renderWatch来完成组件vnode的创建和patch的逻辑。如果你对vue里面响应式里面发布订阅中涉及到的ObserverDepWathcer还不熟悉的话可以先去了解下。但是不了解也不要紧,因为这篇文章讲述到的内容没有涉及到响应式的部分。在下面的代码中,你只需了解在创建Watcher对象时将会执行updateComponent方法。

      new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
    

    那么updateComponent又是什么呢?

    4、updateComponent

    updateComponent的逻辑很简单,调用vm._render()创建新的vnode,在vm._update内部调用__patch__对新旧的两个vnode进行diff

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

    5、创建出的vnode是什么样子的?

    1. vnode是我们上面例子中定义的render方法执行返回的。
    const { render, _parentVnode } = vm.$options
    // ...
    
    vnode = render.call(vm._renderProxy, vm.$createElement)
    

    如果打断点的话,此时应该会进入到我们定义的render方法中。

    【vue】异步组件是怎么渲染的?

    1. 我们是通过调用render方法的第一个参数h来创建vnode的, 其实h也就是vm.$createElement

    vm.$createElement内部如果创建的是普通的标签vnode

    vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
    )
    

    如果创建的是自定义组件vnode, 逻辑会复杂些:

    vnode = createComponent(Ctor, data, context, children, tag)
    

    我们只关心异步组件的逻辑, Ctor是通过Ctor = baseCtor.extend(Ctor)创造出来的构造函数,当然如果是异步组件在这个例子中是不会执行Ctor = baseCtor.extend(Ctor)的。因为只有当Ctor是一个options对象时才会进行extend处理得到一个构造函数,每一个构造函数上都有一个cid属性,感兴趣的朋友可以去看下Vue.extend的实现。

    if (isObject(Ctor)) {
        // 通过继承返回一个新的子类构造器
        Ctor = baseCtor.extend(Ctor)
    }
    

    所以当Ctor.cid不存在时,那么这就是一个异步组件。

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

    在异步组件第一次渲染时因为此时还未获取到组件的配置对象options,所以Ctor是undefined, 此时会返回一个异步组件的占位vnode。异步组件vnode特有的两个属性asyncFactory保存异步函数,asyncMeta保存从父组件获取的属性数据,子节点等信息, 等异步函数内部被resolve时重新渲染时会用到。

    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是什么样的呢?

    【vue】异步组件是怎么渲染的?

    异步组件未被resolve之前是渲染成一个空节点的。 创建好vnode之后就是进行patch的逻辑, 但是我们现在不关心patch的逻辑, 从上面创建的vnode也能看出来这一次patch是与异部组件无关的, 因为异步组件被一个占位vnode替代了。

    此次patch之后, 页面是这样的:

    【vue】异步组件是怎么渲染的?

    那异步组件是什么时候渲染的呢? 那当然是异步函数被resolve的时候, resolve之后又是怎么做的呢? 让我们看下resolveAsyncComponent的逻辑就知道了。

    6、resolveAsyncComponent

    resolveAsyncComponent的作用是获取异步组件,异步组件在loading、error、resolve状态下渲染的内容可能是不一样的。由于resolveAsyncComponent的代码是实现异步组件的关键逻辑, 并且逻辑比较集中我就把所有代码给贴出来了。大概的思路是这样的:

    1. 如果factory(也就是我们定义的异步组件函数)上的factory.error为真那么表示reject了,此时如果errorComp存在,则显示errorComp组件。

    2. factory.resolved为真表示异步组件已经resolve,此时可以将异步组件的内容渲染出来了。

    3. factory.loading为真表示异步组件第一次渲染并且还未被resolvereject, 此时页面渲染的是loading组件。

    4. 如果上面的条件都不满足, 则执行const res = factory(resolve, reject)异步请求组件。

    5. resolve之后做的事情:

      1. factory.resolved = ensureCtor(res, baseCtor)通过Vue.extend(options)获取到组件构造函数。
      2. 执行forceRender(true), 父组件调用$forceUpdate()强制刷新父组件, 父组件ptach的时候就会创建真正的异步组件内容。
      3. 重新执行第二部的updateComponent逻辑。与第一次执行updateComponent不同的是:
        • 第一次是同步执行,第二次是异步执行,因为$forceUpdate()内部会调用renderWatcher的update方法。将当前renderWatcher加入queueWatcher队列, 而queueWatcher队列是在nextTick中执行的。
        • 这一次将会从factory.resolved上获取到异步组件构造函数。
    export function resolveAsyncComponent (
      factory: Function,
      baseCtor: Class<Component>
    ): Class<Component> | void {
      // 如果异步组件出现错误
      if (isTrue(factory.error) && isDef(factory.errorComp)) {
        return factory.errorComp
      }
    
      // 异步组件获取的到的options会在resolved属性上
      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)
      }
    
      // loading展示
      if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
        return factory.loadingComp
      }
    
      // 第一次使用
      if (owner && !isDef(factory.owners)) {
        // owners用来保存那些组件实例用到了这个异步组件
        const owners = factory.owners = [owner]
        let sync = true
        let timerLoading = null
        let timerTimeout = null
        // 当父组件被销毁时,父组件从owners上移除
        ;(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()
          }
          // 渲染完成后清空owners
          if (renderCompleted) {
            owners.length = 0
            if (timerLoading !== null) {
              clearTimeout(timerLoading)
              timerLoading = null
            }
            if (timerTimeout !== null) {
              clearTimeout(timerTimeout)
              timerTimeout = null
            }
          }
        }
    
        // 当异步组件被rsolve时
        const resolve = once((res: Object | Class<Component>) => {
          // cache resolved
          // 使用extend的到子类构造函数,并且缓存在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)
    
        // 如果factory执行后返回的是一个对象
        if (isObject(res)) {
          // 返回的是一个promise对象
          /**
            function (resolve, reject) {
              setTimeout(function () {
                resolve({
                  template: '<div>I am async!</div>'
                })
              }, 1000)
            }
            */
          if (isPromise(res)) {
            // () => Promise
            if (isUndef(factory.resolved)) {
              res.then(resolve, reject)
            }
    
          /*
            const AsyncComponent = () => ({
              // 需要加载的组件 (应该是一个 `Promise` 对象)
              component: import('./MyComponent.vue'),
              // 异步组件加载时使用的组件
              loading: LoadingComponent,
              // 加载失败时使用的组件
              error: ErrorComponent,
              // 展示加载时组件的延时时间。默认值是 200 (毫秒)
              delay: 200,
              // 如果提供了超时时间且组件加载也超时了,
              // 则使用加载失败时使用的组件。默认值是:`Infinity`
              timeout: 3000
            })
          */
          } else if (isPromise(res.component)) {
            res.component.then(resolve, reject)
    
            // error组件
            if (isDef(res.error)) {
              factory.errorComp = ensureCtor(res.error, baseCtor)
            }
    
            // loading组件
            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 in case resolved synchronously
        return factory.loading
          ? factory.loadingComp
          : factory.resolved
      }
    }
    
    

    起源地下载网 » 【vue】异步组件是怎么渲染的?

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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