Vue 异步组件-工厂函数执行流程

1,314 阅读3分钟

工厂函数

写法:

这个工厂函数会收到一个 resolve 回调,这个回调函数会在你从服务器得到组件定义的时候被调用。你也可以调用 reject(reason) 来表示加载失败。

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

执行流程:

代码:

// main.js
import Vue from "vue";
import App from "./App.vue";

Vue.component("MyComponent", function(resolve) {
  require(["./components/myComponent.vue"], resolve);
});
new Vue({
  render: (h) => h(App),
}).$mount("#app");
// App.vue
<template>
  <div id="app">
    <button @click="isShow = true">请点击!</button>
    <my-component v-if="isShow"></my-component>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      isShow: false
    };
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
// MyComponent.vue
<template>
  <h1>我是MyComponent组件</h1>
</template>

<script>
export default {
  name: "MyComponent"
};
</script>

<style scoped>
</style>

一、注册全局异步组件

执行Vue.component()注册全局组件时,会调用以下代码:

var ASSET_TYPES = [
  'component',
  'directive',
  'filter'
];
// forEach ASSET_TYPES数组,创建三个全局方法:Vue.component()、Vue.directive()、Vue.filter()
ASSET_TYPES.forEach(function (type) {
  Vue[type] = function (
    id,
    definition
  ) {
    if (!definition) {
      return this.options[type + 's'][id]
    } else {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && type === 'component') {
        validateComponentName(id);
      }
      // 虽然此时type是 component  但是 definition是一个工厂函数不是对象
      if (type === 'component' && isPlainObject(definition)) {
        definition.name = definition.name || id;
        definition = this.options._base.extend(definition);
      }
      if (type === 'directive' && typeof definition === 'function') {
        definition = { bind: definition, update: definition };
      }
      // 将这个 工厂函数 添加到 Vue.options.components对象中,id 是组件名(MyComponent)
      this.options[type + 's'][id] = definition;
      // 返回这个工厂函数
      return definition
    }
  };
});

二、首次渲染

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

执行new Vue()的初始化后,会进行$mount挂载

挂载阶段大概流程:

  • 判断有没有用户提供render函数,没有render就会进入编译阶段(不细说),接着进入mountComponent函数

  • mountComponent函数,主要做了以下几件事:

    • 创建一个渲染watcher,收集updateComponent函数中的依赖(这个函数执行阶段使用到的变量都会收集到此渲染watcher中,一但发生变化就会通知渲染watcher重新渲染

      updateComponent = function () {
            vm._update(vm._render(), hydrating);
          };
      
    • 执行vm._render()根据render函数生成App组件的组件节点

      // App组件的占位符节点
      vnode: {
        child: undefined
        tag: "vue-component-1-App"
        data: {
          on: undefined
          hook:
          init: ƒ init(vnode, hydrating)
          prepatch: ƒ prepatch(oldVnode, vnode)
          insert: ƒ insert(vnode)
          destroy: ƒ destroy(vnode)
        }
        children: undefined
        text: undefined
        elm: undefined
        ns: undefined
        context: Vue {_uid: 0, _isVue: true, $options: {…}, …}
        fnContext: undefined
        fnOptions: undefined
        fnScopeId: undefined
        key: undefined
        componentOptions: {
          Ctor: ƒ VueComponent(options) // app的组件构造器 cid为1
          propsData: undefined
          listeners: undefined
          tag: undefined
          children: undefined
        }
        componentInstance: undefined
        parent: undefined
        raw: false
        isStatic: false
        isRootInsert: true
        isComment: false
        isCloned: false
        isOnce: false
        asyncFactory: undefined
        asyncMeta: undefined
        isAsyncPlaceholder: false
      }
      
    • 执行vm._update函数,进入patch阶段,patch方法会调用createElm函数试图创建该vnode的真实vnode。由于此vnode 是一个组件vnode,会执行createComponent函数。

    • createComponent函数会,通过componentVNodeHooks中init方法,根据此组件vnode创建app组件实例,并赋值到componentInstance

    • 执行app组件实例的mount方法,编译app组件的template模板生成该组件的render渲染函数,执行渲染函数生成该组件的渲染vnode。

    // app组件的渲染函数
    var render = function() {
      var _vm = this
      var _h = _vm.$createElement
      var _c = _vm._self._c || _h
      return _c(
        "div",
        { attrs: { id: "app" } },
        [
          _c(
            "button",
            {
              on: {
                click: function($event) {
                  _vm.isShow = true
                }
              }
            },
            [_vm._v("请点击!")]
          ),
          // 这里不会执行 isShow为false,所以忽略
          _vm.isShow ? _c("my-component") : _vm._e()
        ],
        1
      )
    }
    
    // app组件的渲染vnode
    vnode: {
      child: undefined
      tag: "div"
      data:{
      		attrs: {id: "app"}
      }
      children: [
          0: VNode {tag: "button", data: {…}, children: Array(1), …}
          1: VNode {tag: undefined, data: undefined, children: undefined, …}
      ]
      text: undefined
      elm: undefined
      ns: undefined
      context: VueComponent {_uid: 1, _isVue: true, $options: {…}, _self: VueComponent, …}
      fnContext: undefined
      fnOptions: undefined
      fnScopeId: undefined
      key: undefined
      componentOptions: undefined
      componentInstance: undefined
      parent: undefined
      raw: false
      isStatic: false
      isRootInsert: true
      isComment: false
      isCloned: false
      isOnce: false
      asyncFactory: undefined
      asyncMeta: undefined
      isAsyncPlaceholder: false
    }
    
    • 再经过_update=》patch等方法,将最终生成的DOM节点添加到app组件实例的$el上
    • 回到createComponent,执行initComponent方法,将app组件实例上的$el(真实dom)赋值给 该组件占位符vnode的elm,并将vnode.elm插入父组件(body节点中),至此首次渲染结束

三、更新视图

当点击页面中的按钮后,会设置isShow为true。此时vue中isShow数据变化,会通知渲染watcher重新渲染页面。

将此渲染watcher加入异步更新队列,nextTick后根据队列中的先后顺序执行任务。

执行updateComponent方法:

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

ps:(此时的watcher是app组件的渲染watcher,所以会通知app组件实例重新渲染,不会通知根实例)

渲染函数是不变的

这里会重新执行渲染函数生成vnode:

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c(
    "div",
    { attrs: { id: "app" } },
    [
      _c(
        "button",
        {
          on: {
            click: function($event) {
              _vm.isShow = true
            }
          }
        },
        [_vm._v("请点击!")]
      ),
      // isShow发生变化会执行啦
      _vm.isShow ? _c("my-component") : _vm._e()
    ],
    1
  )
}

四、_c("my-component")(第一次)

  • 执行_createElement方法
function _createElement (
  context,	// app组件实例
  tag,	// "my-component"
  data,
  children,
  normalizationType
) {
  var vnode, ns;
  if (typeof tag === 'string') {
    var Ctor;
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
    if (config.isReservedTag(tag)) {
    		... // 无关代码省略
      );
      // 第一步 进入 resolvAsset 获取组件 
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 此时Ctor已经为 工厂函数,跳转第二步 createComponent过程
      // 从第二步返回过来,vnode 为一个异步注释节点
      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);
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) { applyNS(vnode, ns); }
    if (isDef(data)) { registerDeepBindings(data); }
    // 返回这个注释节点
    return vnode
  } else {
    return createEmptyVNode()
  }
}

第一步=》resolveAsset

获取全局定义的组件,将并工厂函数赋值给Ctor。

function resolveAsset (
  options,	// app组件实例的$optionss
  type,
  id,
  warnMissing
) {
  var assets = options[type];
  // check local registration variations first
  if (hasOwn(assets, id)) { return assets[id] }
  var camelizedId = camelize(id);
  if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
  var PascalCaseId = capitalize(camelizedId);
  if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
  // fallback to prototype chain
  var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
  if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
    warn(
      'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
      options
    );
  }
  // res 就是工厂函数 
  /*
  function(resolve) {
  	require(["./components/myComponent.vue"], resolve);
	}
	*/
  return res
}

第二步=》createComponent

function createComponent (
  Ctor,	// component的工厂函数
  data,	// undefined
  context,	// app组件实例
  children,	// undefined
  tag	// "my-component"
) {
    // baseCtor = Vue构造器 cid:0
  var baseCtor = context.$options._base;

	// Ctor 不是对象,if不会执行
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }

  // 异步组件
  var asyncFactory;
  if (isUndef(Ctor.cid)) {
    // 将 异步组件工厂函数赋值给 asyncFactory
    asyncFactory = Ctor;
   	// 调用 resolveAsyncComponent函数,获取构造器 跳转第三步
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
    // 执行完 第三步 Ctor = undefined
    if (Ctor === undefined) {
      // 返回一个异步的注释节点占位符,回到_createElement
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

第三步=》resolveAsyncComponent

function resolveAsyncComponent (
  factory,	// 工厂函数
  baseCtor	// Vue cid:0
) {
  if (isDef(factory.resolved)) {
    return factory.resolved
  }
	// currentRenderingInstance 是在_render中设置的 当前是 app组件实例
  var owner = currentRenderingInstance;
 	// 当前不满足
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    // already pending
    factory.owners.push(owner);
  }
	// 当前满足
  if (owner && !isDef(factory.owners)) {
    // 给当前工厂函数添加 owners 属性 是一个数组,并将当前 app组件实例添加进去
    var owners = factory.owners = [owner];
    // 设置 sync 值为ture
    var sync = true;
    var timerLoading = null;
    var timerTimeout = null
		// 给当前 app组件实例添加一个“hook:destroyed”监听器,如果被$emit 就将owner从 当前owners中移除
    // 当前 app组件 _events 多了一个事件 “hook:destroyed”
    ;(owner).$on('hook:destroyed', function () { return remove(owners, owner); });
	
    var forceRender = function (renderCompleted) {
      for (var i = 0, l = owners.length; i < l; i++) {
        (owners[i]).$forceUpdate();
      }

      if (renderCompleted) {
        owners.length = 0;
        if (timerLoading !== null) {
          clearTimeout(timerLoading);
          timerLoading = null;
        }
        if (timerTimeout !== null) {
          clearTimeout(timerTimeout);
          timerTimeout = null;
        }
      }
    };

    var resolve = once(function (res) {
      // 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;
      }
    });

    var reject = once(function (reason) {
     // ...
    });
		// 执行工厂函数 并传入两个函数作为参数 ,此时会调到工厂函数并执行,工厂函数中require是异步的,所以执行完又会跳到这里,此时res=undefined
      //  require(["./components/myComponent.vue"], resolve);
    var res = factory(resolve, reject);

  
    sync = false;
    // return undefined
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}

五、_update

isShow状态改变后,第一次通过_render()异步加载组件时,异步组件会先返回一个注释节点,然后经过_update的patch到视图中,此时异步组件还未加载到视图中,先用一个注释节点顶替。等到同步流程结束,会执行异步函数的回调,此时就是resolve回调:

require(["./components/myComponent.vue"], resolve);

六、resolve回调

once方法,确保只执行一次:

function once (fn) {
  var called = false;
  return function () {
    if (!called) {
      called = true;
      // arguments是一个异步组件选项
/*        arguments: Arguments(1)
            0: Module
                default:
                beforeCreate: [ƒ]
                beforeDestroy: [ƒ]
                name: "MyComponent"
                render: ƒ ()
                staticRenderFns: []
                __file: "src/components/myComponent.vue"
                _compiled: true
                _scopeId: "data-v-717ad35e"
*/
      fn.apply(this, arguments);
    }
  }
}
 var resolve = once(function (res) {
      // ensureCtor (myComponent 组件选项 , Vue)
     // ensureCtor 返回子组件构造器并缓存resolved
     factory.resolved = ensureCtor(res, baseCtor);
     // 此时 sync=false 
      if (!sync) {
        // 强制重新渲染
        forceRender(true);
      } else {
        owners.length = 0;
      }
    });

ensureCtor:

function ensureCtor (comp, base) {
  if (
    comp.__esModule ||
    (hasSymbol && comp[Symbol.toStringTag] === 'Module')
  ) {
    comp = comp.default;
  }
  // 更具这个组件选项,扩展子组件构造器
  return isObject(comp)
    ? base.extend(comp)
    : comp
}

七、forceRender强制渲染

var forceRender = function (renderCompleted) {
      for (var i = 0, l = owners.length; i < l; i++) {
          // 调用 app组件的强制渲染,vm._watcher.update() 异步渲染
        (owners[i]).$forceUpdate();
     	 }
      }

八、_c("my-component")第二次

function createComponent (
  Ctor,	// component的工厂函数
  data,	// undefined
  context,	// app组件实例
  children,	// undefined
  tag	// "my-component"
) {
  var baseCtor = context.$options._base;
  var asyncFactory;
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor;
    // 第一步 获取异步组件
    // 现在可以获取子组件构造器
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
  }
  data = data || {};

  var listeners = data.on;
  data.on = data.nativeOn;
 // 给data中添加相关hook
  installComponentHooks(data);

  // return a placeholder vnode
  var name = Ctor.options.name || tag;
  var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    data, undefined, undefined, undefined, context,
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
    asyncFactory
  );
 // 返回 异步获取的组件vnode
  return vnode
}

resolveAsyncComponent

function resolveAsyncComponent (
  factory, // 工厂函数
  baseCtor // Vue cid:0
) {
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }
	// 此时 已经有了resolved
  if (isDef(factory.resolved)) {
      // 直接返回这个mycomponent构造器
    return factory.resolved
  }

九、_update

获得了APP组件的渲染节点,进入patch流程,创建mycomponent组件渲染节点。