【vue】异步组件是怎么渲染的?__Vue.js
发布于 3 年前 作者 banyungong 1196 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

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方法中。

image.png

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

image.png

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

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

image.png

那异步组件是什么时候渲染的呢? 那当然是异步函数被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
  }
}

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 丢了帽子的路飞 原文链接:https://juejin.im/post/6947589511563968549

回到顶部