Fancy Front End Fancy Front End
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

让有意义的事变得有意思,让有意思的事变得有意义
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • Plan 计划
  • typescript-utility

  • single-spa源码

    • 开始阅读
    • app与应用管理

    • lifecycles与生命周期管理

      • 本章概要
      • 生命周期:load 和 unload
        • 目录
        • app 是什么?
        • toLoadPromise
        • smellsLikeAPromise
        • flattenFnArray
        • toUnloadPromise
      • 生命周期:bootstrap
      • 生命周期:mount 和 unmount
      • 生命周期:update
    • navigation与路由管理

    • parcel组件

    • 其他

    • single-spa-react

  • qiankun源码

  • webpack

  • axios

  • solid

  • vite源码

  • jquery源码

  • snabbdom

  • am-editor

  • html2canvas

  • express

  • acorn源码

  • immutable.js源码

  • web
  • single-spa源码
  • lifecycles与生命周期管理
jonsam
2022-04-15
目录

生命周期:load 和 unload

本节讲解 single-spa 中的生命周期 load 和 unload 函数的原理。在 single-spa 中 load 是在 bootstrap、mount 阶段之前,注册应用后的阶段。此阶段的主要工作是加载并检验 load 配置、校验配置和钩子函数、规整钩子函数等。unload 是在 unmount 之后 unregister 之前的阶段。

# 目录

  • 目录
  • app 是什么?
  • toLoadPromise
  • smellsLikeAPromise
  • flattenFnArray
  • toUnloadPromise

# app 是什么?

因为上一章中是从宏观上管理微应用,我们只需要知道微应用是一个 object 接口。在这一章中,因为我们要详细探讨微应用的生命周期,这必然要涉及到对微应用中某些属性的操作,因此在探讨 toLoadPromise 之前,先来看下 app 是什么?因为 s-spa 不是 typescript 编写的,我们在文档和代码中还原 app 的原状。以内部命名为准。

参数 描述
name App name that single-spa will register and reference this application with, and will be labelled with in dev tools.
loadApp Application object or a function that returns the resolved application (Promise or not)
activeWhen Can be a path prefix which will match every URL starting with this path, an activity function (as described in the simple arguments) or an array containing both of them.
customProps Will be passed to the application during each lifecycle method.
loadErrorTime 微应用 load 错误的时间点
loadPromise 微应用 load 的 promise
status 微应用的状态
parcels
devtools devtools 的配置

提示

加粗的参数未用户会配置的参数,其余为内部使用参数。

# toLoadPromise

这个函数的结构很复杂,先来探讨下其结构:

function toLoadPromise(app) {
  // P0
  return Promise.resolve().then(() => {
    // ......
    // P1
    return (app.loadPromise = Promise.resolve().then(() => {
      // ......
      // P2
      return app;
    })).catch(err => {
      // ......
      // P3
      return app;
    } )
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Promise 的类型定义参考:qiankun: loadApp 加载微应用

  • P0:返回 Promise.resolve().then 的 return,即 Promise<app> 。
  • P1:P1 的返回就是 P0 的返回。
  • P2/P3:P2/P3 的返回就是 Promise<app> 。

MDN:Promise.resolve()

This function flattens nested layers of promise-like objects (e.g. a promise that resolves to a promise that resolves to something) into a single layer.

Promise.resolve() 会将 Promise 进行 flat,但是这里使用嵌套的 Promise.resolve 来返回 Promise<app> ,目的是为了是让 toLoadPromise,即返回 Promise<app> 这个操作成为 Promise,希望它能够在微任务里执行,提升执行效率。关于 Promise 模拟微任务可参见:30 分钟看懂 React 框架原理。

结合此函数在 reroute 和 performAppChanges 里调用,执行的频率较高,这么做也就可以理解了。

下面来具体看下源码:

export function toLoadPromise(app) {
  // load app 返回 loadPromise,Promise.resolve() 会将嵌套的 promise 摊平
  return Promise.resolve().then(() => {
    // 如果 loadPromise 已经存在,直接返回,不用再生成 loadPromise,执行 load 过程
    if (app.loadPromise) {
      return app.loadPromise;
    }
    // app.status 必须是 NOT_LOADED 或者 LOAD_ERROR
    if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
      return app;
    }
    // 状态修改为 LOADING_SOURCE_CODE,这是因为这 LOADING_SOURCE_CODE 阶段主要目前的加载应用配置对象
    // 配置中包含应用生命周期钩子(加载源码并不是 load 阶段,这部分由外界完成,
    // 如 qiankun 就自定义的加载源代码这块以实现 prefecth 等增强功能) 
    app.status = LOADING_SOURCE_CODE;

    let appOpts, isUserErr;
    // 将 loadPromise 保存在 app 上以
    return (app.loadPromise = Promise.resolve()
      .then(() => {
        // load app promise,规整传递给 loadApp 的参数
        const loadPromise = app.loadApp(getProps(app));
        // 判断是否是 promise,如果不是 promise 就会报错
        // 参见 registerMicroApps 中 app 使用  async 返回对象
        if (!smellsLikeAPromise(loadPromise)) {
          isUserErr = true;
          // throw Error() ......
        }
        return loadPromise.then((val) => {
          app.loadErrorTime = null;

          appOpts = val;

          let validationErrMessage, validationErrCode;
          // loadPromise 必须返回 object
          if (typeof appOpts !== "object") {
            validationErrCode = 34;
          }
          // 有 bootstrap 钩子,但是钩子不合法
          if (
            // ES Modules don't have the Object prototype
            Object.prototype.hasOwnProperty.call(appOpts, "bootstrap") &&
            !validLifecycleFn(appOpts.bootstrap)
          ) {
            validationErrCode = 35;
          }
          // mount 钩子必须有,但是钩子不合法
          if (!validLifecycleFn(appOpts.mount)) {
            validationErrCode = 36;
          }
          // unmount 钩子必须有,但是钩子不合法
          if (!validLifecycleFn(appOpts.unmount)) {
            validationErrCode = 37;
          }
          // 判断 app 是 parcel 还是 application,根据 appOpts.unmountThisParcel 判断
          const type = objectType(appOpts);

          if (validationErrCode) {
            // ......
            handleAppError(validationErrMessage, app, SKIP_BECAUSE_BROKEN);
            return app;
          }

          // 状态由 LOADING_SOURCE_CODE 更新为 NOT_BOOTSTRAPPED
          app.status = NOT_BOOTSTRAPPED;
          // 规整 bootstrap 钩子,将钩子转成 钩子数组,并且返回 promise reduce pipeline
          // 以下类比,注意这里只是返回 promise reduce pipeline,并没有执行
          app.bootstrap = flattenFnArray(appOpts, "bootstrap");
          app.mount = flattenFnArray(appOpts, "mount");
          app.unmount = flattenFnArray(appOpts, "unmount");
          app.unload = flattenFnArray(appOpts, "unload");
          // 规整 timeouts 的配置,并且跟默认配置合并
          app.timeouts = ensureValidAppTimeouts(appOpts.timeouts);
          // load 过程完毕,删除 app.loadPromise, 这个 app.loadPromise 相当于一个互斥锁
          delete app.loadPromise;

          return app;
        });
      })
      .catch((err) => {
        delete app.loadPromise;

        let newStatus;
        // 出错,如果是用户配置问题,将状态置为 SKIP_BECAUSE_BROKEN,否则将状态置为 LOAD_ERROR
        if (isUserErr) {
          newStatus = SKIP_BECAUSE_BROKEN;
        } else {
          newStatus = LOAD_ERROR;
          app.loadErrorTime = new Date().getTime();
        }
        // 初始应用错误
        handleAppError(err, app, newStatus);

        return app;
      }));
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

先从整体上总结这个函数的作用:

  • 函数返回 loadPromise 即, Promise<app> ,在注册应用之后,mount 应用之前,此函数将 load 应用。
  • load 应用主要做如下工作:调用 app.loadApp 加载并检验 load 配置、校验配置和钩子函数、规整钩子函数(转成数组、将钩子数组包装成 reduce pipeline)、错误处理。
  • 此过程中状态为 LOADING_SOURCE_CODE (校验 app.loadPromise 和 app.status 之后) 和 NOT_BOOTSTRAPPED (配置和钩子函数检验完毕之后)。

参考:

  • Applications API | single-spa | registerApplication (opens new window)
  • Promise.resolve() - JavaScript | MDN (opens new window)

# smellsLikeAPromise

判断是否是 Promise。

export function smellsLikeAPromise(promise) {
  return (
    promise &&
    typeof promise.then === "function" &&
    typeof promise.catch === "function"
  );
}
1
2
3
4
5
6
7

为什么不用 p instanceof Promise 呢?我们注意到很多源码中(Vue3 等)都使用上述的方式判断 promise,而非使用 instanceof 判断。这是因为 instanceof 仅可判断由 ES6 实现的 Promise,而对于很多自己实现的 Promise,包括 polyfill 中实现的 Promise,这种方法可能就没法很好判断。而上述的判断是基于 ES 的实现规范的判断方法,这种判断方法更为普遍。

# flattenFnArray

export function flattenFnArray(appOrParcel, lifecycle) {
  let fns = appOrParcel[lifecycle] || [];
  fns = Array.isArray(fns) ? fns : [fns];
  if (fns.length === 0) {
    fns = [() => Promise.resolve()];
  }

  const type = objectType(appOrParcel);
  const name = toName(appOrParcel);

  return function (props) {
    // promise reduce pipeline
    // 执行钩子数组中所有的钩子,并且将最后 promise 返回
    return fns.reduce((resultPromise, fn, index) => {
      return resultPromise.then(() => {
        const thisPromise = fn(props);
        return smellsLikeAPromise(thisPromise)
          ? thisPromise
          : Promise.reject(/** ...... **/)
      });
    }, Promise.resolve());
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

数组中的钩子函数将依次在微任务里执行,知道返回最后一个钩子执行的 promise。

关于 reduce pipeline,在中也 qiankun: loadApp 加载微应用运用了这个技巧。

# toUnloadPromise

toUnloadPromise 函数卸载微应用。

export function toUnloadPromise(app) {
  return Promise.resolve().then(() => {
    const unloadInfo = appsToUnload[toName(app)];
    // 如果已经 unload 没有排队,直接返回,因为 unloadApplication 没有被调用
    if (!unloadInfo) {
      /* No one has called unloadApplication for this app,
       */
      return app;
    }
    // 如果状态已经是 NOT_LOADED,直接 finishUnloadingApp
    if (app.status === NOT_LOADED) {
      /* This app is already unloaded. We just need to clean up
       * anything that still thinks we need to unload the app.
       */
      finishUnloadingApp(app, unloadInfo);
      return app;
    }
    // 可能是 unloadApplication 和 reroute 同时想要 unload 应用
    // 等待已经 unload 执行完毕
    if (app.status === UNLOADING) {
      /* Both unloadApplication and reroute want to unload this app.
       * It only needs to be done once, though.
       */
      return unloadInfo.promise.then(() => app);
    }
    // 如果应用状态不是 NOT_MOUNTED 或者 LOAD_ERROR,没有办法 unload,直接返回
    // 需要先 unmount 到 NOT_MOUNTED 状态才能 unload 
    if (app.status !== NOT_MOUNTED && app.status !== LOAD_ERROR) {
      /* The app cannot be unloaded until it is unmounted.
       */
      return app;
    }

    const unloadPromise =
      app.status === LOAD_ERROR
        ? Promise.resolve()
        : reasonableTime(app, "unload");

    app.status = UNLOADING;
    // 如果不是 LOAD_ERROR 状态就执行 unload 的钩子
    return unloadPromise
      .then(() => {
        // 执行成功后 finishUnloadingApp
        finishUnloadingApp(app, unloadInfo);
        return app;
      })
      .catch((err) => {
        errorUnloadingApp(app, unloadInfo, err);
        return app;
      });
  });
}

function finishUnloadingApp(app, unloadInfo) {
  // 从 unload 队列中删除
  delete appsToUnload[toName(app)];
  // 删除生命周期
  // Unloaded apps don't have lifecycles
  delete app.bootstrap;
  delete app.mount;
  delete app.unmount;
  delete app.unload;
  // 更新状态为 NOT_LOADED
  app.status = NOT_LOADED;
  //  resolve unloadInfo promise,完成整个 unload 过程
  /* resolve the promise of whoever called unloadApplication.
   * This should be done after all other cleanup/bookkeeping
   */
  unloadInfo.resolve();
}

function errorUnloadingApp(app, unloadInfo, err) {
  delete appsToUnload[toName(app)];

  // Unloaded apps don't have lifecycles
  delete app.bootstrap;
  delete app.mount;
  delete app.unmount;
  delete app.unload;

  handleAppError(err, app, SKIP_BECAUSE_BROKEN);
  // 将错误 reject 给外部
  unloadInfo.reject(err);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

使用队列的方式来 unload 应用是因为 unloadApplication 和 reroute 可能会同时想要 unload 应用。

这个函数的核心作用是:

  • 调用 unload 钩子,将应用 unload。
  • 删除应用的生命周期,并且将队列中 unload 任务的 promise 进行 resolve 或者 reject。
编辑 (opens new window)
上次更新: 2022/09/06, 14:25:16
本章概要
生命周期:bootstrap

← 本章概要 生命周期:bootstrap→

最近更新
01
渲染原理之组件结构与 JSX 编译
09-07
02
计划跟踪
09-06
03
开始上手
09-06
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy Front End | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式