Vue.js 源码分析(二十七) 高级应用 异步组件 详解

摘要:
当我们的项目足够大时,将使用许多组件。一次加载所有组件需要时间。没有必要在开始时加载所有组件。此时,可以使用异步组件进行优化。简而言之,异步组件在页面上显示之前不会从服务器加载,如果未显式显示,则不会加载。这可以提高客户端的访问速度,减少对服务器的请求数量,这是一个强大的优化工具。异步组件通常有三种实现:工厂功能、promise加载和高级

当我们的项目足够大,使用的组件就会很多,此时如果一次性加载所有的组件是比较花费时间的。一开始就把所有的组件都加载是没必要的一笔开销,此时可以用异步组件来优化一下。

异步组件简单的说就是只有等到在页面里显示该组件的时候才会从服务器加载,不显式的话就不会加载,这样即可提高客户端的访问速度也可以降低对服务器的请求次数,可谓优化的一个利器。

异步组件常用有3种异步组件的实现:工厂函数、Promise加载和高级异步组件。

注:一般的项目都是在vue-router的路由里面创建vue-router实例时通过routes属性指定路由的,其实在vue里面也可以实现。

OK,开干,先搭建一个环境,我们先用Vue-li3搭建一个脚手架 ,默认的配置搭建完后在浏览器输入:http://localhost:8080即可打开页面,默认部分如下:

Vue.js 源码分析(二十七) 高级应用 异步组件 详解第1张

页面下部分显式的就不截图了,然后点击about可以切换路由,为了测试我们对异步组件的分析,我们把main.js和app.js和/src/components/HelloWorld.vue进行改写,如下:

对于/src/components/HelloWorld.vue组件,为了我们测试更方便,直接更改为:

<template>
  <div class="hello">
    <p>Hello World!</p>
  </div>
</template>

只显示Hello World!就好了,对于main.js文件,修改如下:

修改前的内容为:

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

修改为:

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

import helloworld  from './components/HelloWorld.vue'
Vue.component('HelloWorld',helloworld)

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

修改后HelloWorld作为一个全局的组件形式存在。然后修改app.vue文件

修改前的内容为:

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

我们把它修改为:

<template>
  <div id="app">
    <button @click="show=true">Test</button>
    <HelloWorld v-if="show"></HelloWorld>      
  </div>
</template>
<script>
  export default{
    data(){
      return{
        show:false
      }
    }
  }
</script>

 渲染后的页面为:

 Vue.js 源码分析(二十七) 高级应用 异步组件 详解第2张

当我们点击Test这个按钮时,Hello World组件就会显式出来,如下:

Vue.js 源码分析(二十七) 高级应用 异步组件 详解第3张

这里我们定义的Vue.component('HelloWorld',helloworld)是一个常规组件,非异步组件,下面我们通过修改main.js来模拟不同的异步组件例子,然后通过代码去看看它的实现原理

 一:工厂函数

Vue.js允许将组件定义为一个工厂函数,动态的解析组件,Vue.js只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。

例如我们把main.js修改成这样:

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

Vue.component('HelloWorld',function(resolve,reject){       //重写HelloWorld组件的定义
    require(['./components/HelloWorld'],function(res){
        resolve(res)
    })
})

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

只有当我们点击Test这个按钮时这个组件才会加载进来

源码分析


当组件执行_render函数转换成虚拟VNode时遇到组件时会执行createComponent()函数,如下:

function createComponent (      //第4184行  创建组件Vnode     
  Ctor,                                     //Ctor:组件的构造函数
  data,                                     //data:数组 
  context,                                  //context:Vue实例
  children,                                 //child:组件的子节点
  tag
) {
  if (isUndef(Ctor)) {
    return
  }

  var 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
  var asyncFactory;
  if (isUndef(Ctor.cid)) {                    //如果Ctor.cid为空,那么Ctor就是一个函数,表明这是一个异步组件
    asyncFactory = Ctor;                                                //获取异步组件的函数
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context);      //执行resolveAsyncComponent()函数
    if (Ctor === undefined) {                                           //如果Ctor是个空的,调用该函数返回一个空的注释节点
      // 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
}

对于一个组件来说,比如Vue.component(component-name,obj|func),组件的值可以是一个对象,也可以是一个函数,如果是对象,则注册时会执行Vue.extend()函数,如下:

if (type === 'component' && isPlainObject(definition)) {    //第4866行 注册组件时,如果组件是个对象,则执行Vue.extend()
    definition.name = definition.name || id;
    definition = this.options._base.extend(definition);
}

去构造子组件的基础构造函数,此时会在构造函数上新增一个cid属性(在4789行),所以我们这里通过cid来判断该组件是否为一个函数。

回到主线,接着执行resolveAsyncComponent()函数,工厂函数相关的如下:

function resolveAsyncComponent (      //第2283行  异步组件   factory:异步组件的函数 baseCtor:大Vue  context:当前的Vue实例
  factory,
  baseCtor,
  context
) {
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

  if (isDef(factory.resolved)) {                          //工厂函数异步组件第二次执行这里时会返回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 {
    var contexts = factory.contexts = [context];                //将context作为数组保存到contexts里,也就是当前Vue实例
    var sync = true;  

    var forceRender = function () {                //遍历contexts里的所有元素                                         下一个tick执行到这里
      for (var i = 0, l = contexts.length; i < l; i++) {      //依次调用该元素的$forceUpdate()方法 该方法会强制渲染一次
        contexts[i].$forceUpdate();
      }
    };

    var resolve = once(function (res) {                         //定义一个resolve函数
      // 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();
      }
    });

    var reject = once(function (reason) {                       //定义一个reject函数
      "development" !== 'production' && warn(
        "Failed to resolve async component: " + (String(factory)) +
        (reason ? ("
Reason: " + reason) : '')
      );
      if (isDef(factory.errorComp)) {
        factory.error = true;
        forceRender();
      }
    });

    var res = factory(resolve, reject);                       //执行factory()函数

    if (isObject(res)) {
      /*高级组件的逻辑*/
    }

    sync = false;
    // return in case resolved synchronously
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}

resolveAsyncComponent内部会定义一个resolve和reject函数,然后执行factory()函数,factory()就是我们在main.js里给HelloWorld组件定义的函数,函数内会执行require函数,由于require()是个异步操作,所以resolveAsyncComponent就会返回undefined

回到resolveAsyncComponent,我们给factory()函数的执行下一个断点,如下:

 Vue.js 源码分析(二十七) 高级应用 异步组件 详解第4张

可以看到返回一个undefined,最后resolveAsyncComponent()也会返回undefined,回到createComponent()函数,由于返回的是undefined,则会执行createAsyncPlaceholder()去创建一个注释节点,渲染后对应的DOM节点树如下:

Vue.js 源码分析(二十七) 高级应用 异步组件 详解第5张

可以看到对于工厂函数来说,组件完全加载时对应的DOM节点是一个注释节点

在下一个tick等require()加载成功后就会执行resolve(res)函数,也就是在resolveAsyncComponent()内定义的resolve函数,

resolve函数会将结果保存到工厂函数的resolved属性里(也就是组件的定义)然后执行的forceRender()函数,也就是上面标记的蓝色的注释对应的代码

再次重新渲染执行到resolveAsyncComponent的时候此时局部变量factory.resolved存在了,就直接返回该变量, 如下:

Vue.js 源码分析(二十七) 高级应用 异步组件 详解第6张

此时就会走组件的常规逻辑,进行渲染组件了。

二:Promise加载

Promise()比较简单,可以认为是工厂函数扩展成语法糖的知识,他主要是可以很好的配合webpack的语法糖,webpack的import的语法糖就是返回一个promise对象,Vue实际上做异步组件也是为了配合Webpack的语法糖来实现Promise()的趋势。

例如我们把main.js改成如下的:

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

Vue.component('HelloWorld',()=>import('./components/HelloWorld'))

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

和工厂函数一样,也会执行两次resolveAsyncComponent,下一个tick的逻辑是一样的,不一样的是触发resolve()的逻辑不通,如下:

源码分析


function resolveAsyncComponent (          //异步组件
  factory,  
  baseCtor,
  context
) {
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

  if (isDef(factory.resolved)) {                              //第一次执行到这里时factory.resolved也不存在
    return factory.resolved
  }

    /**/
    var res = factory(resolve, reject);                       //我们这里返回一个含有then的对象

    if (isObject(res)) {                                      
      if (typeof res.then === 'function') {                      //如果res是一个函数,即Promise()方式加载时
        // () => Promise
        if (isUndef(factory.resolved)) {                            //如果factory.resolved不存在
          res.then(resolve, reject);                                  //用then方法指定resolve和reject的回调函数
        }
      } else if (isDef(res.component) && typeof res.component.then === 'function') {
        /**/
      }
    }

    sync = false;
    // return in case resolved synchronously
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}

例子里执行到factory()后返回的res对象如下:

Vue.js 源码分析(二十七) 高级应用 异步组件 详解第7张

等到加载成功后就会执行resolve了,后面的步骤和工厂函数的流程是一样的。

三:高级异步组件

高级异步组件可以定义更多的状态,比如加载该组件的超时时间、加载过程中显式的组件、出错时显式的组件、延迟时间等

writer by:大沙漠 QQ:22969969

高级异步组件也是定义一个函数,返回值是一个对象,对象的每个属性在官网说得挺详细的了,如下,连接::https://cn.vuejs.org/v2/guide/components-dynamic-async.html#%E5%A4%84%E7%90%86%E5%8A%A0%E8%BD%BD%E7%8A%B6%E6%80%81

对于高级异步组件来说,他和promise()方法加载的逻辑是一样的,不同的是多了几个属性,如下:

源码分析


function resolveAsyncComponent (        //第2283行  异步组件
  factory,
  baseCtor,
  context
) {
    /**/
    if (isObject(res)) {
      if (typeof res.then === 'function') {                       //promise的分支
        // () => Promise
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject);
        }
      } else if (isDef(res.component) && typeof res.component.then === 'function') {   //高级异步组件的分支
        res.component.then(resolve, reject);                    //还是调用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) {                                                //如果等待时间为0
            factory.loading = true;                                                 //直接设置factory.loading为true
          } else {
            setTimeout(function () {
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true;
                forceRender();
              }
            }, res.delay || 200);
          }
        }

        if (isDef(res.timeout)) {                                           //超时时间
          setTimeout(function () {
            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
  }
}

OK,搞定,流程就这样吧

免责声明:文章转载自《Vue.js 源码分析(二十七) 高级应用 异步组件 详解》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇windows命令行下简单使用javac、java、javap详细演示Python+Appium学习篇之元素定位下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

手把手教你写vue插件并发布(二)

  前记:上一篇 https://www.cnblogs.com/adouwt/p/9211003.html, 说到了一个完整的vue插件开发、发布的流程,总结下来就讲了这么一个事,如何注入vue, 如果封装vue插件,如何测试vue插件,以及如何发布vue插件到npm。但是,这里开发测试到发布是分开在两个项目的,未免有些多余,今天的笔记讲的就是在上一篇的...

Vue项目引用百度地图并实现搜索定位等功能 Marco

Tip:本篇文章为案例分析,技术点较多,所以篇幅较长,认真阅览的你一定会学到很多知识。 前言:百度地图开放平台 给开发者们提供了丰富的地图功能与服务,使我们的项目中可以轻松地实现地图定位、地址搜索、路线导航等功能。本文给大家介绍如何在vue项目中引用百度地图,并设计实现简单的地图定位、地址搜索功能。 一、效果图及功能点 先来看一下效果图 效果图看不够? 点...

vue使用iframe嵌入html,js方法互调

前段时间 使用h5搞了个用cesium.js做的地图服务功能,后来想整合到vue项目,当然最简单的就是iframe直接拿来用了。但html和vue的方法交互就是成了问题,vue调用html种方法还好,尤其是html调用vue中的方法当初就没有解决,忙着项目上线直接搞了个setInterval不停轮询,哎不说他了;现在空点了来把问题解决了,俗话说得好闲时学来...

ubuntu 16.04 安装nodejs

Ubuntu 上安装 Node.js Node.js 源码安装 以下部分我们将介绍在Ubuntu Linux下安装 Node.js 。 其他的Linux系统,如Centos等类似如下安装步骤。 在 Github 上获取 Node.js 源码: $ sudo git clone https://github.com/nodejs/node.git Clon...

【Linux】 源码安装make命令详解,避免踩坑

正常的编译安装/卸载: 源码的安装一般由3个步骤组成:配置(configure)、编译(make)、安装(make install)。   configure文件是一个可执行的脚本文件,它有很多选项,在待安装的源码目录下使用命令./configure –help可以输出详细的选项列表。   其中--prefix选项是配置安装目录,如果不配置该选项,安装后可...

Chrome实用调试技巧

如今Chrome浏览器无疑是最受前端青睐的工具,原因除了界面简洁、大量的应用插件,良好的代码规范支持、强大的V8解释器之外,还因为Chrome开发者工具提供了大量的便捷功能,方便我们前端调试代码,我们在日常开发中是越来越离不开Chrome,是否熟练掌握Chrome调试技巧恐怕也会成为考量前端技术水平的标杆。 介绍Chrome调试技巧的文章很多,本文结合我自...