Service Worker 相关问题

注册

index.html

service worker 的 scope 范围与 sw.js 所在的路径相关,只能对 sw.js 所在路径以及其子路径生效

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
const url = new URL(location.href);
if ("serviceWorker" in navigator) {
addEventListener("load", () => {
navigator.serviceWorker
.register("./sw.js", { scope: "/" })
.then((registration) => {
var serviceWorker;
if (registration.installing) {
serviceWorker = registration.installing;
// 这里可以处理正在安装状态
} else if (registration.waiting) {
serviceWorker = registration.waiting;
// 这里可以处理等待状态
} else if (registration.active) {
serviceWorker = registration.active;
emitCacheList(serviceWorker);
}

if (serviceWorker) {
serviceWorker.addEventListener("statechange", function (e) {
if (e.target.state === "activated") {
// Service Worker 已经激活,可以发送消息
emitCacheList(serviceWorker);
}
});
}
})
.catch((err) => {
console.warn("desktop_sw register fail.");
});
});
}

安装

1
2
3
this.addEventListener("install", function (event) {
console.log("install");
});

激活

不一定每次都能触发。前一个还在工作状态那么后一个就会进 waiting 阶段,只有等到前一个被 terminated 后,后一个才能完全替换 A 的工作

1
2
3
this.addEventListener("activate", function (event) {
console.log("activate");
});

更新

在这些条件中会执行更新操作

主动在每次注册后执行更新

1
2
3
4
5
6
7
8
9
10
11
12
navigator.serviceWorker
.register("/sw.js", {
scope: "/",
})
.then((registration: any) => {
registration.update();
});

// 使用 workbox
wb.register().then((registration: any) => {
registration.update();
});

关闭

  • terminated
  • 关闭浏览器一段时间
  • 手动清除 serviceworker
  • 在 sw 安装时直接跳过 waiting 阶段 self.skipWaiting();

拦截请求

拦截请求 只有 activate 之后才能工作

页面本身的 URL 不在 Service Worker 的 scope 内时,Service Worker 并不会对该页面加载过程中的任何请求(包括在 scope 内的资源)有控制权。页面的控制权是从最顶层的文档开始的,如果顶层文档(页面)不在 Service Worker 的 scope 内,那么 Service Worker 就不能影响到从这个页面发出的任何请求,即使这些请求本身的目标 URL 是在 Service Worker 的 scope 范围内。

1
2
3
4
5
6
7
8
9
10
11
this.addEventListener("fetch", function (event) {
// 返回jSON HTML
const json = JSON.stringify({ a: 1 }, null, 2);
return event.respondWith(
new Response(json, {
headers: {
"content-type": "application/json;charset=UTF-8",
},
})
);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
// 拦击请求 只有activate之后才能工作
this.addEventListener("fetch", function (event) {
const html = "<html><div>html</div></html>";
return event.respondWith(
new Response(html, {
headers: {
"content-type": "text/html;charset=UTF-8",
},
})
);
// 重定向
return event.respondWith(Response.redirect("https://baidu.com", 301));
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
this.addEventListener("fetch", function (event) {
const url = new URL(event.request.url);
if (location.origin !== url.origin) return;
return event.respondWith(
caches.match(event.request).then((res) => {
if (res) return res;
return fetch(event.request).then((res) => {
if (!res || res.status !== 200 || res.type !== "basic") {
return res;
}
const clone = res.clone();
caches.open(config.CACHE_VERSION).then((caches) => {
caches.put(event.request, clone);
});
return res;
});
})
);
});

通信

客户端向 sw 发送消息,需要保证 sw 已经处于 activated 状态

1
2
3
4
5
6
7
// index.html
serviceWorker.postMessage({ a: 1 });

// sw.js
this.addEventListener("message", function (event) {
console.log("收到页面消息", event.data);
});

数据同步, sw 可以监听客户端发起的 sync 请求,当离线环境时,sw 在后台将任务挂起,当网络恢复会执行回调函数, 相同的 tag 在网络恢复后只会执行一次, tag 不能用于传输数据, 离线数据应该使用持久化保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// index.html
navigator.serviceWorker.ready.then(function (registration) {
document.body.addEventListener("click", () => {
registration.sync
.register("data_sync")
.then(function () {
console.log("后台同步已触发");
})
.catch(function (err) {
console.log("后台同步触发失败", err);
});
});
});

// sw.js
self.addEventListener("sync", function (e) {
switch (e.tag) {
case "data_sync":
break;
default:
return;
}
});

workbox

workbox docs

  • 注册

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // index.html
    if ("serviceWorker" in navigator) {
    const { Workbox } = await import("workbox-window");
    const wb = new Workbox("sw.js");

    // 实现 window 与 sw 通信
    wb.addEventListener("activated", (event) => {
    const urlsToCache = [
    location.href,
    ...performance.getEntriesByType("resource").map((r) => r.name),
    ];
    wb.messageSW({
    type: "CACHE_URLS",
    payload: urlsToCache,
    });
    });
    wb.register();
    }
  • 使用预设

    通常不需要手动设置 workbox-precaching, 应该使用 workbox-build 或 workbox-webpack-plugin 自动生成依赖文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // sw.js

    import { pageCache, staticResourceCache, imageCache } from "workbox-recipes";
    import { precacheAndRoute } from "workbox-precaching";

    pageCache();
    staticResourceCache();
    imageCache();

    // 表示缓存所有 webpack 打包的 manifest 文件
    precacheAndRoute(self.__WB_MANIFEST || []);
  • 使用 workbox-precaching 缓存

    当应用首次载入 install 事件中,workbox-precaching 会查看你要下载的资源,删除重复的并使用 SW 事件下载并缓存资源。资源的 URL 中已经包含了可以用作缓存 key 的信息

  • 使用 workbox-webpack-plugin

    GenerateSW : 适用与预缓存文件,或有简单的缓存需求。 不适用与使用其他的 SW 特新,例如 Web Push, 或有自定一的缓存逻辑

    InjectManifest: InjectManifest 插件将生成一个要预缓存的 url 列表,并将该预缓存清单添加到现有的 service worker 文件中。否则它将使文件保持原样。

    适用于想要更多的控制 SW,缓存文件,自定义路由策略,想要使用其他的 SW 特性,

    会在 output.path 中生成 sw.js 文件,需要手动在 index.html 中引入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // webpack.config.js
    const { InjectManifest } = require("workbox-webpack-plugin");

    module.exports = {
    plugins: [
    new InjectManifest({
    swSrc: "sw.js",
    exclude: [
    /\.map$/,
    /manifest$/,
    /\.htaccess$/,
    /service-worker\.js$/,
    /sw\.js$/,
    ],
    }),
    ],
    };

什么是 Service Worker?它的作用是什么?

Service Worker 是一种运行在浏览器后台的 JavaScript 脚本,独立于页面上下文,并且与页面的生命周期分离。它提供了拦截和处理网络请求的能力,从而使得网页能够支持离线工作、缓存资源和后台同步等功能。它是 渐进式 Web 应用(PWA)的关键组成部分。

离线支持:缓存页面和资源,使得在没有网络连接时,应用仍能工作。
网络请求拦截:可以拦截网络请求并提供自定义响应,例如从缓存中获取资源,或将请求转发给网络。
后台同步:在应用有网络时自动同步数据。
推送通知:支持推送通知,允许在用户不活跃时向其发送消息。

Service Worker 的生命周期是什么样的?

注册:通过 navigator.serviceWorker.register() 方法注册 Service Worker。此时浏览器会检查是否需要安装新的 Service Worker。
安装(Install):注册后,如果没有有效的 Service Worker,浏览器会尝试安装它。在此阶段,通常会缓存一些静态资源。
激活(Activate):安装完成后,Service Worker 会激活。在此阶段,可以清理旧的缓存和控制页面。
控制:Service Worker 激活后,能够控制所有符合条件的页面,开始拦截网络请求。
更新:Service Worker 可能会定期检查更新,新的 Service Worker 安装并激活后,旧的 Service Worker 会被终止。

Service Worker 中的 fetch 事件是如何工作的?

fetch 事件是 Service Worker 中用于拦截和处理网络请求的核心事件。每当页面发起请求时,Service Worker 会通过 fetch 事件捕获请求,允许开发者决定如何响应这些请求(例如,从缓存中获取、从网络获取或自定义响应)。

1
2
3
4
5
6
7
8
9
self.addEventListener("fetch", function (event) {
event.respondWith(
caches
.match(event.request) // 优先从缓存中获取
.then(function (response) {
return response || fetch(event.request); // 如果缓存中没有,则从网络获取
})
);
});

event.respondWith() 方法用于返回一个 Response 对象,覆盖默认的网络请求处理方式。

什么是 Service Worker 的作用域?

Service Worker 的作用域是指它可以控制的 URL 范围。在注册时,可以指定作用域,它决定了 Service Worker 可以拦截哪些请求。默认情况下,Service Worker 会控制它所在路径下的所有页面和资源。

如果没有指定作用域,Service Worker 会默认控制当前路径及其子路径下的页面。

1
navigator.serviceWorker.register("/service-worker.js", { scope: "/" });

如何实现离线缓存?

离线缓存的基本思路是在 install 事件中缓存需要的资源,然后在 fetch 事件中拦截请求,从缓存中提供响应。
在安装时,将文件添加到缓存中,fetch 事件会拦截请求,优先从缓存中返回资源。

cache.add():将单个资源添加到缓存。
cache.addAll():将多个资源添加到缓存。
cache.put():将指定请求和响应对存入缓存。
caches.delete():删除指定缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
self.addEventListener("install", function (event) {
event.waitUntil(
caches.open("my-cache").then(function (cache) {
return cache.addAll([
"/index.html",
"/styles.css",
"/script.js",
"/offline.html",
]);
})
);
});

self.addEventListener("fetch", function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request); // 网络优先
})
);
});

self.skipWaiting() 和 self.clients.claim() 的作用是什么?

self.skipWaiting():在激活新的 Service Worker 时,跳过等待阶段,立即使新的 Service Worker 控制页面。这对实现即时更新非常有用。

self.clients.claim():在 Service Worker 激活后,立即接管当前打开的页面,使其受该 Service Worker 控制,避免等待页面重新加载。

1
2
3
4
5
6
7
self.addEventListener("install", function (event) {
self.skipWaiting(); // 立即激活
});

self.addEventListener("activate", function (event) {
event.waitUntil(self.clients.claim()); // 立即接管控制
});

如何清理旧的缓存?

在 Service Worker 的 activate 事件中,可以清理旧的缓存。例如,在更新时清理不再需要的缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
self.addEventListener("activate", function (event) {
var cacheWhitelist = ["my-cache-v2"]; // 新的缓存名称
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.map(function (cacheName) {
if (!cacheWhitelist.includes(cacheName)) {
return caches.delete(cacheName); // 删除不在白名单中的缓存
}
})
);
})
);
});

如何使用 Service Worker 支持推送通知

推送通知依赖于 Service Worker 的 push 事件。当接收到推送消息时,Service Worker 会在后台处理并显示通知。

1
2
3
4
5
6
7
8
9
10
self.addEventListener("push", function (event) {
var options = {
body: event.data.text(),
icon: "/icon.png",
badge: "/badge.png",
};
event.waitUntil(
self.registration.showNotification("Push Notification", options)
);
});

Service Worker 的缓存策略有哪些

Cache-first:优先从缓存获取资源,如果没有,再从网络获取。
Network-first:优先从网络获取资源,如果失败,再从缓存中获取。
Stale-while-revalidate:先从缓存获取过时的资源,然后异步从网络获取最新的资源,并更新缓存。

如何使用 Service Worker 实现离线数据同步?

在网络恢复的时候,sync 会自动触发。

1
2
3
4
5
6
7
8
9
10
11
//客户端,注册监听事件
navigator.serviceWorker.ready.then((registration) => {
registration.sync.register("syncData");
});

// service worker
self.addEventListener("sync", (event) => {
if (event.tag === "syncData") {
event.waitUntil(syncData());
}
});

Service Worker 如何与多个标签页共享数据

Service Worker 可以通过 客户端 API 与多个标签页进行通信。clients.matchAll() 可以获取所有控制的页面客户端,postMessage 用于与页面通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 页面中
self.addEventListener("message", function (event) {
console.log("Message from client:", event.data);
event.ports[0].postMessage("Hello from Service Worker!");
});

// service worker
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage("Hello from the page!");
navigator.serviceWorker.addEventListener("message", function (event) {
console.log("Message from service worker:", event.data);
});
}

如何在 Service Worker 中使用 self.registration.update()

self.registration.update() 用于强制浏览器检查 Service Worker 是否有更新。如果应用程序需要强制立即检查更新,可以调用该方法。

1
2
3
4
5
self.addEventListener("activate", function (event) {
event.waitUntil(
self.registration.update() // 强制更新 Service Worker
);
});

CSS Flex 布局

flex 弹性布局

可以设置元素 display 属性为 flexinline-flex, inline-flex 可以让 flex 元素保持内联的特性。[CSS 新世界中相关章节]

弹性设置
  • flex-grow:当有剩余空间时,元素延伸占据剩余空间的规则

  • flex-shrink:剩余空间不足时,元素收缩的规则

  • flex-basis: 元素基础宽度,类似于 width 但如果设置了 auto 以外的值,优先级别 width 高.

    flex-basis 属性下的最小尺寸是由内容决定的,而 width 属性下的最小尺寸是 width 属性的计算值决定的。也就是内容过长的时候,设置了 width 可能溢出,而设置了 flex-basis 宽度是最小内容宽度。

CSS 常见问题

BFC 及其应用

block formatting context (块级格式化上下文), BFC 元素可以隔离子元素对外部元素的影响。[CSS 世界中相关章节]

如果一个元素是块级元素,满足以下任一情况会触发 BFC:

  • <html> 元素
  • float 的值不为 none
  • overflow 的值为 auto、scroll 或 hidden;
  • display 的值为 table-cell,table-row,table-caption 和 inline-block 中的任何一个;
  • position 的值不为 relative 和 static。

BFC 可以解决以下问题:

  • 避免 margin 重叠,将元素的外层元素变成 BFC 元素,因为 BFC 的隔离性,可以避免子元素与外层元素 margin 重叠
    但是如果 BFC 元素有上下 margin, 仍然会与外层元素边距重叠

  • 可以让文字环绕图片时,文字自动填充图片右侧空间,而无需设置固定的宽度。如果想要文字于图片保持距离,可使用 margin 等,但是不能使用 p 标签的 margin-left

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <style>
    img {
    width: 100px;
    height: 100px;
    float: left;
    }
    p {
    overflow: hidden;
    }
    </style>
    <div class="a">
    <img src="./01.png" alt="" />
    <p>xx</p>
    </div>
    • 使用 display:table-cell 实现自适应的两栏布局,由于 table-cell 的特性是宽度不会超过父容器的宽度,所以可以给一个很大的宽度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <style>
    .wrapper {
    width: 100%;
    }
    img {
    width: 100px;
    float: left;
    }
    .right {
    display: table-cell;
    word-break: break-all;
    width: 9999px;
    }
    </style>
    <div class="wrapper">
    <img src="./01.png" alt="" />
    <div class="right">xxxx</div>
    </div>

overflow 不同值得区别

overflow 属性原本的作用指定了块容器元素的内容溢出时是否需要裁剪。[CSS 世界中相关章节]

  • 注意剪裁得部分是
  • 除非 overflow-x 和 overflow-y 的属性值都是 visible,否则 visible 会当成 auto 来解析
  • <html> <textarea> 默认会有滚动条,因为 auto 作为默认值。
  • 滚动条的产生会影响表格等样式,方法 1 可以空出右边的滚动条的宽度,方法 2 可以不设最后一列宽度
  • body 设置为 absolute, 宽度 100vw,可以解决滚动条晃动的问题。

三栏布局

  • float 实现,主要元素的排列顺序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <style>
    .container {
    width: 100%;
    overflow: hidden;
    }
    .left {
    float: left;
    width: 20%;
    }
    .right {
    float: right;
    width: 20%;
    }
    .center {
    margin-left: 20%;
    margin-right: 20%;
    background: lightgray;
    }
    </style>
    <div class="container">
    <div class="left">Left</div>
    <div class="right">Right</div>
    <div class="center">Center</div>
    </div>
  • flex 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <style>
    .container {
    display: flex;
    }
    .left {
    flex: 0 0 20%;
    }
    .center {
    flex: 1;
    }
    .right {
    flex: 0 0 20%;
    }
    </style>
    <div class="container">
    <div class="left">Left</div>
    <div class="center">Center</div>
    <div class="right">Right</div>
    </div>
  • grid 布局

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <style>
    .container {
    display: grid;
    grid-template-columns: 20% 1fr 20%;
    }
    </style>
    <div class="container">
    <div class="left">Left</div>
    <div class="center">Center</div>
    <div class="right">Right</div>
    </div>
  • 使用 absolute 绝对定位布局

  • 使用 table 布局

    table 中未设置宽度的单元格会自动占据剩余的空间

    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
    <style>
    .container {
    display: table;
    width: 100%;
    }
    .left,
    .right {
    min-width: 100px;
    }
    </style>
    <table class="container">
    <thead>
    <tr>
    <td width="100px"></td>
    <td></td>

    <td width="100px"></td>
    </tr>
    </thead>
    <table class="container">
    <tbody>
    <tr>
    <td class="cell left">Left</td>
    <td class="cell center">Center</td>
    <td class="cell right">Right</td>
    </tr>
    </tbody>
    </table>
    </table>

    使用 div 也可以由同样的效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <style>
    .container {
    display: table;
    width: 100%;
    }

    .left,
    .right {
    min-width: 100px;
    }

    .cell {
    display: table-cell;
    }
    </style>
    <div class="container">
    <div class="cell left">Left</div>
    <div class="cell center">Center</div>
    <div class="cell right">Right</div>
    </div>
  • float + bfc

    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
    <style>
    .wrapper {
    clear: both;
    }

    .middle {
    width: 100%;
    float: left;
    }

    .main {
    margin-left: 100px;
    margin-right: 100px;
    }

    .left {
    float: left;
    width: 100px;
    margin-left: -100%;
    }

    .right {
    float: right;
    width: 100px;
    margin-left: -100%;
    }
    </style>
    <div class="wrapper">
    <div class="middle">
    <div class="main">中间</div>
    </div>
    <div class="left">左栏</div>
    <div class="right">右栏</div>
    </div>

calc 函数

通常配合变量使用,实现动态计算效果。[CSS 新世界 4.5] [兼容性]

VMWare 配置桥接模式

桥接模式配置

  • 管理员模式启动VMware

  • VMware虚拟网络管理中,选择桥接模式并选择当前网卡

  • 虚拟机网络配置,选择桥接模式

  • 虚拟机网络链接配置,选择手动模式
    ip 地址前三位相同,最后一位保证在当前网段唯一
    其他信息与宿主机保持一致

编码思路-工程化

which/whereis

  • which

    搜索范围:仅在当前用户的 PATH 环境变量指定的目录中搜索。

    输出内容:仅返回第一个匹配的可执行文件路径。

  • whereis

    搜索范围:在预定义的标准系统目录(如 /bin, /usr/include, /usr/share/man 等)中搜索,不依赖 PATH。

    输出内容:返回所有相关文件路径(二进制、手册页、源代码等)。

npm/yarn/pnpm

  • npm

    嵌套依赖结构:早期版本采用嵌套的 node_modules 结构,导致依赖重复和路径过长问题。

    确定性依赖:在 npm@5 后引入 package-lock.json,锁定依赖版本(解决早期版本依赖不确定性)。

    早期有重复依赖的问题,扁平化处理后可能导致幽灵依赖。

  • yarn

    确定性依赖:引入 yarn.lock 文件(早于 npm 的 package-lock.json),锁定依赖树。

    并行下载:利用并行请求提升安装速度。

    离线缓存:全局缓存已下载的依赖包,减少重复下载。

    扁平化结构:将嵌套依赖提升到 node_modules 顶层,减少重复安装(但可能引发依赖冲突)

    PnP 模式: 劫持 Node.js 的模块解析逻辑,使其不再依赖物理的 node_modules 目录,而是通过映射表(.pnp.cjs)直接定位到 .zip 文件中的代码。

  • pnpm

    硬链接 + 符号链接:所有依赖包存储在全局存储目录(类似缓存)。通过硬链接共享相同版本的依赖,减少磁盘占用。使用符号链接在项目中按需链接依赖。

    严格依赖隔离:每个包的依赖在独立的 node_modules 中,避免幽灵依赖。

本地依赖

通常使用 lerna, nx 等工具,原生实现可以使用 file 协议。

1
2
3
4
5
{
"dependencies": {
"@my/cli-util": "file:/mnt/d/Workspace/my-cli/packages/util"
}
}

使用 pnpm

1
2
3
4
// pnpm-workspace.yaml
packages:
- "packages/*"

1
2
3
4
5
{
"dependencies": {
"@my/cli-util": "workspace:*"
}
}

路径处理

  • import-local 优先使用自己的本地安装版本

  • pkg-dir 查找项目的根目录

  • resolve-cwd 从 CWD 目录解析模块的绝对路径。

  • which 在 path 环境变量中查找第一个匹配

  • node 默认处理 .js .json .node 文件, 其他文件格式当作 js 文件处理

    1
    2
    3
    4
    5
    //a.txt
    console.log(1);

    // index.js
    const a = require("./a.txt"); // 不会报错,可以加载

root 检查

root-check, 如果使用 root 用户启动,尝试降级

参数处理

minimist 解析命令行参数

yargs commander 提供交互式的命令行

检查包是否安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let dir = __dirname;

do {
if (fs.statSync(path.join(dir, "node_modules", packageName)).isDirectory()) {
return true;
}
} while (dir !== (dir = path.dirname(dir)));

//require("module") 用于管理模块的接口
for (const internalPath of require("module").globalPaths) {
if (fs.statSync(path.join(internalPath, packageName)).isDirectory()) {
return true;
}
}
return false;

区分使用的是那种包管理器

1
2
3
4
5
6
7
if (fs.existsSync(path.resolve(process.cwd(), "yarn.lock"))) {
packageManager = "yarn";
} else if (fs.existsSync(path.resolve(process.cwd(), "pnpm-lock.yaml"))) {
packageManager = "pnpm";
} else {
packageManager = "npm";
}

原生的命令行交互 readLine

1
2
3
4
5
6
7
8
9
10
11
const readLine = require("readline");

const questionInterface = readLine.createInterface({
input: process.stdin,
output: process.stderr,
});

questionInterface.question("question one (yes/no):", (answer) => {
if (answer.startsWith("y")) {
}
});

子进程的 spawn 和 exec 函数之间的区别

Node.js 的子进程模块(child_process)有两个函数 spawn 和 exec,使用这两个函数,我们可以启动一个子进程来执行系统中的其他程序。刚接触 child_process 的人可能会问,为什么做同一件事会有两个函数,以及应该使用哪个函数。我将解释 spawn 和 exec 之间的区别,以帮助你决定何时使用哪个函数。

child_process.spawn 和 child_process.exec 的最大区别在于它们的返回值–spawn 返回一个流,而 exec 返回一个缓冲区。

child_process.spawn 返回一个包含 stdout 和 stderr 流的对象。您可以点击 stdout 流来读取子进程发回 Node 的数据。作为一个流,stdout 具有流所具有的 “data”(数据)、”end”(结束)和其他事件。当您希望子进程向 Node 返回大量数据(如图像处理、读取二进制数据等)时,最好使用 spawn。

child_process.spawn 是 “异步 asynchronous”(异步不同步)的,这意味着一旦子进程开始执行,它就会以流的形式从子进程发回数据。

这里有一个例子,我用 spawn 读取了 Node 的 curl 请求结果。

child_process.exec 返回子进程输出的整个缓冲区。默认情况下,缓冲区大小为 200k。如果子进程返回的数据超过该值,程序就会崩溃,并显示错误信息 “Error: maxBuffer exceeded”(错误:超过最大缓冲区)。你可以在执行选项中设置更大的缓冲区大小来解决这个问题。但你不应该这样做,因为 exec 并不适合向 Node 返回巨大缓冲区的进程。你应该使用 spawn 来解决这个问题。那么,exec 用来做什么呢?用它来运行返回结果状态而不是数据的程序。

child_process.exec 是 “同步异步 “的,也就是说,虽然 exec 是异步的,但它会等待子进程结束,并尝试一次性返回所有缓冲数据。如果 exec 的缓冲区大小设置得不够大,就会出现 “maxBuffer exceeded”(超过最大缓冲区)错误,导致执行失败。

请看这里的一个示例,我使用 exec 执行 wget 下载文件,并向 Node 更新执行状态。

这就是 Node 的子进程 span 和 exec 之间的区别。当你希望子进程向 Node 返回大量二进制数据时,请使用 spawn;当你希望子进程返回简单的状态信息时,请使用 exec。

1
2
3
4
5
6
7
8
9
10
11
12
13
const cp = require("child_process");
new Promise((resolve, reject) => {
const executedCommand = cp.spawn("echo 1", [], {
stdio: "inherit",
shell: true,
});

executedCommand.on("error", reject);

executedCommand.on("exit", (code) => {
if (code === 0) resolve();
});
}).then(() => {});

区分包的模块化方案

1
2
3
4
5
6
7
const pkgPath = require.resolve(`${packageName}/package.json`);
const pkg = require(pkgPath);
if (pkg.type === "module" || /\.mjs/i.test(pkg.bin[name])) {
import(path.resolve(path.dirname(pkgPath), pkg.bin[name])).catch();
} else {
require(path.resolve(path.dirname(pkgPath), pkg.bin[name]));
}

检查两个命令是否相似

(莱文斯坦距离)[https://zh.wikipedia.org/wiki/%E8%90%8A%E6%96%87%E6%96%AF%E5%9D%A6%E8%B7%9D%E9%9B%A2] (fastest-levenshtein)[https://github.com/ka-weihe/fastest-levenshtein]

获取某个包的最新版本

推荐使用 cross-spawn

1
2
3
4
5
6
7
8
9
10
const cp = require("child_process");

const { output } = cp.spawnSync(
"npm.cmd",
["view", "react@latest", "version"],
{
stdio: "pipe",
}
);
console.log(output.toString());

或者通过 api 拉取 https://registry.npmjs.com/lodash

是否是浏览器环境

1
2
// 使用 typeof, 对于不存在的变量会返回 undefined
const hasDocument = typeof document !== "undefined";

检查 IE 浏览器

1
2
3
var isIE11 =
typeof navigator !== "undefined" &&
navigator.userAgent.indexOf("Trident") !== -1;

版本号对比

1
2
3
4
5
6
7
8
const semver = require("semver");

const version1 = "1.2.3";
const version2 = "2.0.0";

if (semver.gt(version1, version2)) {
console.log(`${version1} is greater than ${version2}`);
}

commander 基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Command } from "commander";
const program = new Command();

program
.name("sun-cli")
.description("这是一个描述")
.option("--targetCommand", "参数的描述", "team1/command")
.action((options) => {
console.log(options);
});

program
.command("clone <source> [destination]")
.description("子命令的描述")
.action((source, destination) => {
console.log("clone command called");
});

program.parse();

React v16 源码分析 ⑬ 性能优化相关

对于以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Child() {
console.log("child");
return <div>2</div>;
}
function App() {
const [state, setState] = useState(1);
console.log("parent");
return (
<div>
<div onClick={() => setState(2)}>click</div>
<Child></Child>
</div>
);
}

首次挂载打印 parent => child;
第一次点击 click 打印 parent=>child
第二次点击 click 打印 parent
第三次点击 click,不打印任何内容

第二次点击的时候没有打印 child 实际上是命中了 bailout 策略,命中该策略的子组件会跳过 reconcile 过程,因此不会进入 render 阶段。

第三次点击没有任何打印,说明父组件和子组件都没有进入 render 阶段,实际上是命中了 eagerState 策略,这是一种发生于触发状态更新时的优化策略,如果命中了该策略不会进入 schedule 阶段,更不会进入 render 阶段。

eagerState

如果某个状态更新前后没有变化就可以跳过更新流程。

state 是基于 update 计算出来的,计算过程发生在 render 的 beginWork, 而 eagerState 则是将计算过程提到了 schedule 之前执行。

前提条件是该 fiberNode 不存在等待执行的更新,如果不存在更新那么就可以作为第一个更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// 如果新状态与当前状态相同,我们可以完全摆脱副作用
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState = queue.lastRenderedState; // 就是 memoizedState
const eagerState = lastRenderedReducer(currentState, action); // 基于 action 提前计算 state
// 如果在我们进入渲染阶段时 reducer 没有改变,那我们可以使用 eager 状态而无需再次调用 reducer。
update.hasEagerState = lastRenderedReducer; // 标记该 update 存在 eagerState
update.eagerState = eagerState; // 存储 eagerState 的值
if (is(eagerState, currentState)) {
return;
}
} catch (error) {
// ...
} finally {
// ...
}
}
}

上面的代码中通过 lastRenderedReducer 提前计算 State,如果前后状态没有变化就会命中 eagerState 策略,如果没有命中但是当前更新是 fiberNode 的第一个更新,也可以作为后续更新的 baseState。

为什么第二次点击的时候,父组件还是要渲染一次,因为进入 eagerState 的条件是

1
fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes);

current.lanes 和 workInProcess.lanes 都需要 NoLanes, 第一次点击时 beginWork 执行结束后 workInProcess.lanes 会被设置为 NoLanes 但此时还没有交换 fiberTree 需要等到 commit 之后执行,因此第一次点击之后,只有 current 被设置为了 NoLanes。
虽然没有命令 eagerState 但是会命中 bailout 这时会被设置为 NoLanes。

1
2
3
4
5
function bailoutHooks(current: Fiber, workInProgress: Fiber, lanes: Lanes) {
workInProgress.updateQueue = current.updateQueue;
// ...
current.lanes = removeLanes(current.lanes, lanes);
}

微前端 ③ systemJs

什么是 systemJs

它可以加载不同模块格式的代码,包括 ES 模块、CommonJS 和 AMD 模块,提供了一致的模块加载体验。

SystemJS 支持将现代 ES 模块代码转译为 System.register 格式(通常需要配合 webpack 等编译工具),以便在不支持 ES 模块的旧版浏览器中运行。这意味着开发者可以编写现代的、基于标准的模块代码,并确保它在旧版浏览器(如 IE11)中也能正常运行。

通过使用接近原生模块加载速度的 System.register 格式,SystemJS 在旧版浏览器中提供了高性能的模块加载能力。

另外还有一下的高级特性:

  • SystemJS 支持顶级 await、动态导入(dynamic import)、循环引用和实时绑定(live bindings),这些都是现代 JavaScript 模块的重要特性。
  • 它还支持 import.meta.url,这是 ES 模块的一部分,允许模块访问其自身的 URL。
  • 支持模块类型(module types)和导入映射(import maps),使得开发者可以更灵活地管理模块依赖关系。
  • 提供对内容安全策略(Content Security Policy, CSP)和完整性检查(integrity)的支持,增强了模块加载的安全性。

它也是 import 提案的解决方案, 在浏览器中原生的 esModule 不允许通过 import React from 'react' 的方式引入依赖,必须使用相对或绝对路径 import React from 'https://xxx/cdn/react.js'import React from './react' ,但配合 webpack, 可以将代码编译成 systemJs 模块化方案的代码,用于加载外部依赖。

systemJs 加载 react 应用

创建 index.html 模板, systemJS 会接管有 type='systemjs-importmap' script 标签中资源的加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<head>
<script type="systemjs-importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react/umd/react.development.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom/umd/react-dom.production.min.js"
}
}
</script>
<script src="https://cdn.staticfile.net/systemjs/6.14.3/system.js"></script>
</head>

<body>
<div id="root"></div>
</body>

配合 webpack 将 scripts 处理成 systemJs 能识别的格式, libraryTarget 配置为 system, 设置 HtmlWebpackPlugin 插件的 scriptLoading: "systemjs-module" 可以为 script 标签自动添加 type 类型。externals 配置是可选的,对于多个子应用有不同版本的相同第三方依赖,可以直接将依赖打包在子应用中,不需要单独在主应用中加载。(对于这个多版本依赖的问题,并没有明确的最佳实践,官方推荐将比较重的第三方依赖变成共享资源, 如果共享资源有依赖关系例如 react react-dom,可以使用 systemJS 的 amd 扩展,或者将依赖放置到 depcache 字段中,推荐使用 amd 扩展能比较好的统一接管)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
entry: path.resolve(__dirname, "./src/index.js"),
output: {
path: path.resolve(__dirname, "dist"),
publicPath: "/",
// 设置打包格式为 system
libraryTarget: "system",
},
// 排除依赖,通过 cdn 加载
externals: {
react: "react",
"react-dom": "react-dom",
},
module: {/** loader 配置 */}
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
scriptLoading: "systemjs-module",
}),
],
};

index.js 文件编译为 System.register 函数调用

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
System.register(
["react", "react-dom"],
function (__WEBPACK_DYNAMIC_EXPORT__, __system_context__) {
var __WEBPACK_EXTERNAL_MODULE_react__ = {};
var __WEBPACK_EXTERNAL_MODULE_react_dom__ = {};
Object.defineProperty(__WEBPACK_EXTERNAL_MODULE_react__, "__esModule", {
value: true,
});
Object.defineProperty(__WEBPACK_EXTERNAL_MODULE_react_dom__, "__esModule", {
value: true,
});
return {
setters: [
function (module) {
Object.keys(module).forEach(function (key) {
__WEBPACK_EXTERNAL_MODULE_react__[key] = module[key];
});
},
function (module) {
Object.keys(module).forEach(function (key) {
__WEBPACK_EXTERNAL_MODULE_react_dom__[key] = module[key];
});
},
],
execute: function () {
__WEBPACK_DYNAMIC_EXPORT__(
(() => {
"use strict";
var __webpack_modules__ = {
"./src/index.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
eval(/** index.js 编译结果 */);
},

"./node_modules/react-dom/client.js": (
__unused_webpack_module,
exports,
__webpack_require__
) => {
eval(/**react-dom/client.js 编译结果 */);
},

react: (module) => {
module.exports = __WEBPACK_EXTERNAL_MODULE_react__;
},

"react-dom": (module) => {
module.exports = __WEBPACK_EXTERNAL_MODULE_react_dom__;
},
};
/************************************************************************/
// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {},
});

// Execute the module function
__webpack_modules__[moduleId](
module,
module.exports,
__webpack_require__
);

// Return the exports of the module
return module.exports;
}

// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");

return __webpack_exports__;
})()
);
},
};
}
);

systemJs 加载流程

  • Q: systemjs-importmap 的作用是什么

    A: systemjs-importmap 作用是建立资源依赖的映射关系,由于浏览器不支持裸导入,因此通过一个自定义的 script 标签建立裸导入名称和资源路径的依赖关系

以单 React 子应用为例,加载主应用以及子应用的过程。

index.html 页面配置如下

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
<!DOCTYPE html>
<html lang="en">
<head>
<title>Root Config</title>
<meta name="importmap-type" content="systemjs-importmap" />
<script type="systemjs-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
"react": "https://cdn.jsdelivr.net/npm/react@16.14.0/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.14.0/umd/react-dom.production.min.js",
"@root/root-config": "//localhost:9000/root-config.js",
"@supos/supos-react-1": "//localhost:3000/supos-supos-react-1.js"
}
}
</script>

<script src="https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.js"></script>
</head>

<body>
<main id="root"></main>
<div id="aa"></div>
<script>
System.import("@root/root-config");
</script>
</body>
</html>
  1. 主应用是一个编译后的 root-config.js 文件,先忽略 single-spa 相关的概念,简单理解为主应用通过 system.import 加载 @root/root-config 也就是通过 systemJS 映射之后的 root-config.js,在文件加载前 systemJS 已经分析了 systemjs-importmap script 标签中的依赖项,并生成了依赖图
1
2
3
4
5
6
7
8
9
10
11
12
13
{
depcache:{},
imports:{
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
"react": "https://cdn.jsdelivr.net/npm/react@16.14.0/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.14.0/umd/react-dom.production.min.js",
"@fefw/root-config": "http://localhost:9000/fefw-root-config.js",
"@supos/supos-react-1": "http://localhost:3000/supos-supos-react-1.js",
"@supos/supos-react-2": "http://localhost:8081/supos-supos-react-2.js"
},
integrity:{},
scope:{}
}
  1. 解析并获取 @root/root-config 的资源路径 http:////localhost:9000/root-config.js, 只要请求的资源与依赖途中匹配,就会解析资源的请求路径,并记录当前环境变量快照后,尝试加载资源。

  2. 资源加载后

    如果资源是 systemJS 模块会自动执行 system.register, 再执行上下文中保存依赖数组,和声明函数。

    如果不是 systemJS 模块,例如公用的 react, react-dom, 会将资源直接包装成 systemJS 声明函数的执行结果,通常会使用 amd 扩展,如果不使用 amd 扩展,例如 react 等 umd 格式的资源会将导出对象直接挂载在 window 对象上,systemJS 会尝试与上一次的环境变量快照比对找到资源导出的对象。但是这种方式将导致污染全局对象,且不能多版本共存。请查看 amd 扩展源码实现

    紧接着执行声明函数,声明函数接受一个 export 函数,用于接受第三方资源中暴露的对象,同时返回一个包含 setters 数组和 execute 方法的对象,如果加载的资源没有依赖项,那就不会有 setters 数组(例如: 主应用无第三方依赖), 延迟执行声明函数可以有时机处理依赖项。

  3. 当资源的声明函数检查无额外依赖,则执行 execute 方法,内部执行 webpack 等构建工具打包后的代码,将返回的对象传递给 export 函数

  4. 此时主应用资源已经执行,会尝试匹配路由,并通过 system.import 加载子应用,再次进入到第 2 步的流程,当子应用加载完成并执行后,获取到了子应用的依赖 ['react','react-dom'], 以及子应用的声明函数,由于此时有依赖项所以需要等待依赖项加载完成,依赖项的加载也会进入到第 2 步的流程,通过 Promise.all 保证所有的依赖加载完成后再,执行声明函数。而第三步的 setter 方法会在子应用加载后调用,依赖中暴露的对象注入到声明函数中。

  5. react 资源加载后,由于并不是一个 systemJS 模块,会通过 amd 扩展,包装成 systemjs 注册的对象,而且由于 react 没有依赖,会在稍后直接执行 execute 方法, 由于 react-dom 不是 systemjs 模块, 声明函数的执行也会被包装成 systemjs 注册的对象, 因为 react-dom 依赖 react, 所以会在缓存中找到已经加载的 react 包装对象, 需要注意这时的 execute 都还没有执行。在所有资源都准备好之后,topLevelLoad 会统一执行 execute 方法,将资源导出对象暴露给依赖对象,接着执行父级资源的 setter 方法,将暴露出的依赖对象注入到,父级资源中。从而完成整个依赖关系的加载。

总结一下 systemJS 核心方法, import 可以递归的加载资源以及其依赖项, setter 方法将依赖资源暴漏的对象注入到父资源中。

systemjs 实现原理

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// 1. 序列化import map
// importMap 只用作描述资源,建立映射关系,不会主动加载资源

function System() {}
var global = window;
// 记录加载的资源
System["@"] = {};

// 记录import资源隐射
var importMap = { imports: {} };

// baseURL 为当前的资源加载路径
var baseUrl = location.href.split("#")[0].split("?")[0];

var importMapPromise = Promise.resolve();
function processScripts() {
document.querySelectorAll("script").forEach((script) => {
// 处理 importmap
if (script.type === "systemjs-importmap") {
if (script.src) {
/** 如果是远程资源,则发起请求 */
} else {
// 解析 importmap
importMapPromise = importMapPromise.then(() =>
extendImportMap(importMap, script.innerHTML, baseUrl)
);
}
}
// 处理systemjs 模块
else if (script.type === "systemjs-module") {
System.import(script.src);
}
});
}

// 尽早执行, 并在 dom 全部加载后重新检测
processScripts();

window.addEventListener("DOMContentLoaded", processScripts);

function extendImportMap(importMap, content, baseUrl) {
const packages = JSON.parse(content);
for (let k in packages) {
importMap[k] = packages[k];
}
}

// 用于加载 system-module 以及依赖项

var firstGlobalProp = (secondGlobalProp = undefined);

function shouldSkipProperty(p) {
return !global.hasOwnProperty(p) || (!isNaN(p) && p < global.length);
}
function noteGlobalProps() {
firstGlobalProp = secondGlobalProp = undefined;
for (var p in global) {
if (shouldSkipProperty(p)) continue;
if (!firstGlobalProp) firstGlobalProp = p;
else if (!secondGlobalProp) secondGlobalProp = p;
lastGlobalProp = p;
}
return lastGlobalProp;
}

System.import = function (id) {
// 加载前记录下当前环境中的属性,依赖加载后和上次环境中的属性做对比,可以知道在环境中挂载了那些新的属性
noteGlobalProps();

return Promise.resolve().then(() => {
var load = getOrCreateLoad(id);
return load.completion || topLevelLoad(load);
});
};

function getOrCreateLoad(id) {
var load = System["@"][id];
if (load) return load;

var ns = Object.create(null);
Object.defineProperty(ns, Symbol.toStringTag, { value: "Module" });

var instantiatePromise = Promise.resolve()
.then(() => {
return System.instantiate(id);
})
.then((registration) => {
//依赖项
var deps = registration[0];

var _export = (name) => {
for (var p in name) {
ns[p] = name[p];
}
load.setter.forEach((setter) => setter(ns));
};

// 参数 __WEBPACK_DYNAMIC_EXPORT__ 函数,接受 webpack 模块化对象
var declare = registration[1](_export);

load.execute = declare.execute;
return [deps, declare.setters];
});

var linkPromise = instantiatePromise.then((res) => {
return Promise.all(
res[0].map((dep, index) => {
var setter = res[1][index];
var depLoad = getOrCreateLoad(importMap.imports[dep]);

if (setter) {
depLoad.setter.push(setter);
}

return depLoad;
})
).then((deps) => {
load.deps = deps;
});
});

return (load = System["@"][id] =
{
id,
importMapPromise,
linkPromise,
ns,
setter: [],
deps: [],
execute: () => {},
completion: void 0,
});
}

System.instantiate = function (id) {
var script = document.createElement("script");
script.async = true;
script.src = id;

// 异步等待 system 模块加载,并执行,执行结束后可以获取到模块注册的依赖信息
return new Promise((resolve, reject) => {
script.addEventListener("load", function () {
document.head.removeChild(script);
var register = System.getRegister();
resolve(register);
});
document.head.appendChild(script);
});
};

// 保存注册依赖以及回调函数

var lastRegister;
System.register = function (deps, declare) {
lastRegister = [deps, declare];
};

function getGlobalProp() {
var foundLastProp, result;
var n = 0;

// 依赖js引擎的行为
for (var p in global) {
if (shouldSkipProperty(p)) continue;

if ((n == 0 && p !== firstGlobalProp) || (n == 1 && secondGlobalProp !== p))
return p;
if (foundLastProp) {
// 下一个属性就是最新被赋值的属性
lastGlobalProp = p;
result = p;
} else {
// 匹配上一次最后一个属性
foundLastProp = p === lastGlobalProp;
}
n++;
}
return result;
}

System.getRegister = function () {
var _lastRegister = lastRegister;
lastRegister = void 0;

if (_lastRegister) return _lastRegister;

// 检查js文件执行之后在环境中添加的属性

var prop = getGlobalProp();

var globalExport = global[prop];

return [
[],
function (_exports) {
return {
execute: function () {
_exports(globalExport);
// 兼容esModule
_exports({ default: globalExport, __useDefault: true });
},
};
},
];
};

function topLevelLoad(load) {
return Promise.resolve(load.linkPromise)
.then(() => {
return Promise.all(load.deps.map((dep) => dep.linkPromise));
})
.then(() => {
// 等待所有依赖加载完成执行
postOrderExec(load);
});
}

function postOrderExec(load) {
var execute = load.execute;

// 首先执行依赖

load.deps.forEach((dep) => {
postOrderExec(dep);
});

execute();
}

amd 扩展源码实现

systemJS amd 扩展用于解决资源加载时对 window 对象的污染,还可以解决资源加载时的依赖问题, 例如 react-dom 依赖 react

reactreact-dom 为例展示资源加载过程, 资源已经被打包为 umd 格式,所以可以兼容 amd 规范。

  1. 首先在全局创建 define 方法,作为 amd 规范定义模块的方法。react 加载后自动执行 define 方法,在执行上下文中保存资源的依赖和执行函数。依赖默认有 exports
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getDepsAndExec(arg1, arg2) {
// define([], function () {})
if (arg1 instanceof Array) {
return [arg1, arg2];
}
}
let amdDefineDeps, amdDefineExec;
global.define = function (name, deps, execute) {
var depsAndExec;

depsAndExec = getDepsAndExec(name, deps);
amdDefineDeps = depsAndExec[0];
amdDefineExec = depsAndExec[1];
};
  1. 当资源加载成功后,会执行 systemJS.getRegister, amd 扩展会重写此方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var getRegister = systemPrototype.getRegister;
var lastRegisterDeclare;
var systemRegister = systemPrototype.register;
systemPrototype.register = function (name, deps, declare) {
lastRegisterDeclare = typeof name === "string" ? declare : deps;
systemRegister.apply(this, arguments);
};

systemPrototype.getRegister = function () {
var register = getRegister.call(this); //[[], function () { return {} }];
// 如果可以获取到 register 注册的依赖,表示此资源是标准的 systemJS 模块,直接返回
if (register && register[1] === lastRegisterDeclare) return register;

// 由于 amd 接管了模块的加载过程,所以在 window 上不会挂载 react 对象,getRegister 会返回空的声明函数结果,作为返回值
// amd 模块默认有 ["exports"] 依赖
// 此时 react 资源的 依赖对象和执行函数 都已经获取到 amdDefineDeps amdDefineExec

createAMDRegister(amdDefineDeps, amdDefineExec);
};
  1. 将 react 的加载包装成 systemJS register 数据结构, 后面继续执行 systemJS 第 4 步的加载逻辑
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
function createAMDRegister(amdDefineDeps, amdDefineExec) {
var exports = {};
var module = { exports: exports };
var depModules = [];
var setters = [];
var splice = 0;
for (var i = 0; i < amdDefineDeps.length; i++) {
var id = amdDefineDeps[i];
var index = setters.length;
if (id === "require") {
depModules[i] = unsupportedRequire;
splice++;
} else if (id === "module") {
depModules[i] = module;
splice++;
} else if (id === "exports") {
depModules[i] = exports;
splice++;
} else {
createSetter(i);
}
if (splice) amdDefineDeps[index] = id;
}
if (splice) amdDefineDeps.length -= splice;
var amdExec = amdDefineExec;
return [
amdDefineDeps,
function (_export) {
_export({ default: exports, __useDefault: true });
return {
setters: setters,
execute: function () {
var amdResult = amdExec.apply(exports, depModules);
if (amdResult !== undefined) module.exports = amdResult;
_export(module.exports);
_export("default", module.exports);
},
};
},
];

// needed to avoid iteration scope issues
function createSetter(idx) {
setters.push(function (ns) {
depModules[idx] = ns.__useDefault ? ns.default : ns;
});
}
}

微前端 ① 什么是微前端

什么是微前端

可以多团队,多框架共同构建 web 应用程序的技术。

微前端的价值

  • 技术栈无关 主框架不限制接入应用的技术栈,子应用具备完全自主权
  • 独立开发、独立部署 子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
  • 独立运行时 每个子应用之间状态隔离,运行时状态不共享

微前端场景解决方案

  • 单实例:即同一时刻,只有一个子应用被展示,子应用具备一个完整的应用生命周期。通常基于 url 的变化来做子应用的切换。

  • 多实例:同一时刻可展示多个子应用。通常使用 Web Components 方案来做子应用封装,子应用更像是一个业务组件而不是应用。

最终目的是将不同框架,不同项目的中的页面或组件整合在指定的路由下同时展示,并能实现数据的隔离和消息的传递等。

架构实践

微前端正是将不同的框架和不同的项目整合在一个路由中,因此微前端框架的定位仅仅是 导航路由 + 资源加载框架

Future State,一个基于前端路由的应用,整个应用可以看作是一个状态树,每一个分支都可以 lazy load, 在程序启动时,描述 lazy load 状态数分支的对象,就可以叫做 Future State。

当主应用导航到 https://a.com/subApp1 的时候子应用加载, 点击子应用的某个链接可能被导航到 https://a.com/subApp1/project,一个微前端框架应该能在路由匹配的时候加载子应用,路由不匹配的时候卸载子应用, 这就是 single-spa 做的工作。

应该用什么集成方式,主应用应该用何种资源形式加载子应用?, 一种方式是 JS Entry, 将子应用的资源打包为 js 资源,例如 single-spa 将要求将子应用打包为 systemJs 格式当作子应用入口, qiankun 要求将子应用打包为 umd 格式(考虑到应用需要独立部署)。另一种是 HTML Entry 直接使用子应用打包出的 html 文件当作入口,本质是解析 html 中引用的资源,因此也可以优化为直接使用资源描述的对象,以减少一次请求。

模块导入

singleSpa 采用 systemJs 的模块化方案,使用 singleSpa 提供的工具,默认只会编译出 js 文件,独立部署需要其他工具支持,通过 systemJs amd 扩展,可以避免将资源加载后添加的对象挂载到 windows 上。

qiankun 采用 umd 的模块化方案。需要使用 windows 快照的方式,获取到哪些对象被注入到 windows 中。

样式隔离方案

css-module,BEM 通过对 class 添加唯一标识保证唯一性。

css-in-js 代表库 emotion ,本质还是将样式转换为唯一的 class 类名,通过 style 标签插入。

shadowDom 浏览器严格隔离

singleSpa 并没有提供样式隔离方案,需要子应用单独处理。

qiankun 提供 experimentalStyleIsolation 配置, 本质上是给最外层元素样式添加唯一标识,相当于 css-module 实现方案,缺点是子应用中的元素如果挂载到了根节点外面会导致样式不生效。如果使用 strictStyleIsolation 配置,会使用 shadowDom 方案,缺点是主应用访问不到子应用的元素。另外,还实现了 Dynamic Stylesheet,原理是应用切出/卸载后,同时卸载掉其样式表,浏览器会对样式表的插入、移除做整个 CSSOM 的重构,因此保证只有一个应用的样式表是生效的,但是这种方式对于多实例的微前端场景无法处理。

js 隔离

singleSpa 并没有提供沙箱的实现, qiankun 实现了基于 Proxy 的多实例沙箱。

一个简单沙箱就是对 windows 快照, 当子应用卸载时,将 windows 对象回滚到之前的状态。

基于 Proxy 的单实例沙箱,创建 Proxy 对象,将对象的操作反映到 windows 的属性改变, 但是当多个子应用实例的时候,对同一属性的修改会导致 windows 属性混乱。

基于 Proxy 的多实例沙箱,不在对 windows 对象操作,直接

框架比较

  • singleSpa 实现了最基本的路由功能,子应用加载功能。

  • qiankuan 基于 singleSpa 的封装,实现了 css 隔离,以及 js 沙箱,保证应用状态隔离, 还提供了更多的特性:

    子应用并行,多个微前端同时存在
    子应用嵌套,微前端嵌套其他的微前端
    父子应用通讯
    预加载,空闲时加载子应用的资源
    公共依赖加载
    按需加载,切换到相应页面的时候才去加载资源

Debian 作为时间服务器

配置权限

添加 sudo 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
su # 切换到root用户

apt install sudo

# 添加用户到sudo组
vi /etc/sudoers
username ALL=(ALL) ALL

# 添加环境路径,防止找不到安装的软件
vi ~/.bashrc
export PATH=$PATH:/usr/sbin::/sbin
. ~/.bashrc

安装 ntp

1
apt install ntp

配置 ntp

1
2
3
4
5
6
7
8
9
vi /etc/ntpsec/ntp.conf

server 127.127.1.0 prefer # 本地时间服务器

# 注释
# pool 0.debian.pool.ntp.org iburst
# pool 1.debian.pool.ntp.org iburst
# pool 2.debian.pool.ntp.org iburst
# pool 3.debian.pool.ntp.org iburst

启动服务

1
2
sudo systemctl start ntpsec
sudo systemctl enable ntpsec

同步

在需要同步的服务器上安装

1
2
3
4
5
sudo apt install ntpdate

# 同步时间

sudo ntpdate <时间服务器ip>

crontab 定时同步

1
2
3
4
5
6
7
sudo apt install cron

# 编辑定时任务
crontab -e

# 添加定时任务,每天执行一次
0 0 * * * sudo /usr/sbin/ntpdate <时间服务器ip>

React v16 源码分析 ⑫ Hooks 原理

针对 hooks 有三种策略,或者说三种类型的 dispatch

HooksDispatcherOnMount 将函数组件初始化的信息挂载到 Fiber 上面

1
2
3
4
5
6
7
8
9
10
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useEffect: mountEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
// ...其他 Hooks
};

HooksDispatcherOnUpdate 函数执行更新的时候,会执行这个对象对应的方法,此时 fiber 上已经存储了函数组件的信息,这些 Hooks 会去维护或更新这些信息。

1
2
3
4
5
6
7
8
9
10
11
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useMemo: updateMemo,
useReducer: updateReducer, /
useRef: updateRef,
useState: updateState,
// ...其他 Hooks
};

ContextOnlyDispatcher 防止开发者在函数组件外部调用 Hooks

1
2
3
4
5
6
7
8
9
10
11
export const ContextOnlyDispatcher: Dispatcher = {
readContext, // 允许读取 Context
useCallback: throwInvalidHookError, // 抛出非法调用错误
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
useRef: throwInvalidHookError,
useState: throwInvalidHookError,
// ...其他 Hooks 同理
};

render 执行的时候会根据不同的上下文环境,给 hooks 赋值不同的方法。

1
2
3
4
5
6
7
const hook = {
memoizedState: null, //当前 Hook 的缓存状态
baseState: null, // 基础状态(用于更新对比或重置)
baseQueue: null, // 基础更新队列(待处理的优先级较低更新)
queue: null, // 当前更新队列(高优先级更新)
next: null, // 指向下一个 Hook 节点的指针
};

当进入一个函数组件时, 会被 renderWithHooks

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
export function renderWithHooks(
current, // 当前 Fiber 节点(null 表示首次挂载)
workInProgress, // 正在处理的 Fiber 节点(本次渲染目标)
Component, // 用户编写的函数组件
props, // 组件接收的 props
secondary, // 次要参数(通常为 ref)
nextRenderLanes // 本次渲染的优先级车道(Lane 模型)
) {
// 设置全局渲染相关变量
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;

// 重置 Hook 链表和副作用队列
workInProgress.memoizedState = null; // 清空 Hooks 链表
workInProgress.updateQueue = null; // 清空 Effect List

// 动态切换 Hooks 分发器
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount // 挂载阶段分发器
: HooksDispatcherOnUpdate; // 更新阶段分发器

// 执行用户函数组件,触发 Hooks 调用
let children = Component(props, secondary);

// 渲染后清理工作
finishRenderingHooks(current, workInProgress);
return children;
}

function finishRenderingHooks(current, workInProgress) {
// 强制切换为错误分发器,防止外部调用 Hooks
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
// ...其他清理逻辑
}

执行 mountState 相关代码,会清空 WorkInProgress 的 memorizedState 以及 updateQueue, 接下来时派那段组件究竟时初始化还是更新,为 ReactCurrentDispatcher.current 赋予不同的上下文

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
function mountState(initialState) {
// 1. 创建 hook 对象
const hook = mountWorkInProgressHook(); // 初始化 hook 对象,关联当前正在渲染的组件

// 2. 判断 initialState 是否是一个函数,如果是则调用它并获得初始状态
if (typeof initialState === "function") {
initialState = initialState(); // 如果 initialState 是函数,调用该函数获取状态值
}

// 2. 初始化 hook 的属性
// 2.1 设置 hook.memoizedState / hook.baseState
hook.memoizedState = hook.baseState = initialState;

// 定义 queue(队列),用于管理状态更新
const queue = {
pending: null, // 队列中待处理的更新
lanes: NoLanes, // 当前更新所属的 lane,通常表示优先级
dispatch: null, // 状态更新函数
lastRenderedReducer: basicStateReducer, // 上一次渲染的状态更新函数
lastRenderedState: initialState, // 上一次渲染的状态
};

// 2.2 设置 hook.queue
hook.queue = queue; // 将队列分配给 hook 的 queue 属性

// 2.3 设置 hook.dispatch
// 将 dispatchSetState 绑定到当前组件的上下文中,并传入队列
const dispatch = (queue.dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue
));

// 3. 返回当前状态和 dispatch 函数
return [hook.memoizedState, dispatch]; // 返回当前状态和状态更新函数
}

function mountWorkInProgressHook() {
const hook = {
memoizedState: null, // Hook 自身维护的状态
baseState: null,
baseQueue: null,
queue: null, // Hook 自身维护的更新队列
next: null, // 指向下一个 Hook
};

// 如果当前组件的 Hook 链表为空,说明这是第一个 Hook
if (workInProgressHook === null) {
// 这是链表的第一个节点(头结点)
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 如果当前组件的 Hook 链表不为空,将新创建的 Hook 添加到链表的末尾(作为尾结点)
workInProgressHook = workInProgressHook.next = hook;
}

return workInProgressHook;
}

组件执行之后就会生成一个 hooks 链表,在更新过程中会移动指针依次指向每一个节点.

这也是为什么不可以将 useHooks 放在条件中,因为他需要按照链表的顺序依次被引用。

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
function updateWorkInProgressHook() {
let nextCurrentHook;

// 获取当前 Fiber 的 alternate(用于双缓冲)
const current = currentlyRenderingFiber.alternate;

if (current !== null) {
// 如果当前有 alternate(即已经存在旧的 Fiber)
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}

// 获取下一个 Hook
nextCurrentHook = currentHook.next;

// 更新 workInProgressHook 的指向
let nextWorkInProgressHook;
if (workInProgressHook === null) {
// 当是第一个 Hook,直接从当前 Fiber 上获取第一个 hook
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// 获取链表的下一个 hook
nextWorkInProgressHook = workInProgressHook.next;
}

// nextWorkInProgressHook 指向当前正在工作的 hook
if (nextWorkInProgressHook !== null) {
// 如果已经有工作中的 hook,重用它
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;

// 当前 hook 指向下一个 hook
currentHook = nextCurrentHook;
} else {
// 克隆当前 hook
if (nextCurrentHook === null) {
const currentFiber = currentlyRenderingFiber.alternate;
if (currentFiber === null) {
// 这是初始渲染,组件挂起、恢复时将渲染一个附加的 hook
const newHook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
nextCurrentHook = newHook; // 设置为当前 hook
} else {
// 如果是更新渲染,应该有一个有效的当前 hook
throw new Error("Rendered more hooks than during the previous render.");
}
}
}

const newHook = {
memoizedState: currentHook.memoizedState, // 当前 Hook 的状态值
baseState: currentHook.baseState, // 当前 Hook 的基础状态
baseQueue: currentHook.baseQueue, // 当前 Hook 的基础更新队列
queue: currentHook.queue, // 当前 Hook 的更新队列
next: null, // 指向下一个 Hook
};

// 如果 workInProgressHook 为 null,说明这是第一个 Hook
if (workInProgressHook === null) {
// 设置当前渲染组件的 memoizedState 为新的 Hook
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// 否则,将新的 Hook 添加到链表的末尾
workInProgressHook = workInProgressHook.next = newHook;
}

return workInProgressHook;
}

useEffect / useLayoutEffect

  • useEffect:回调函数会在 commit 阶段完成后异步执行,所以它不会阻塞视觉渲染

  • useLayoutEffect:回调函数会在 commit 阶段的 Layout 子阶段同步执行,一般用于执行 DOM 相关的操作

  • useInsertionEffect:回调函数会在 commit 阶段的 Mutation 子阶段同步执行,与 useLayoutEffect 的区别在于执行时无法访问对 DOM 的引用。这个 Hook 是专门为 CSS-in-JS 库插入全局的 style 元素设计的。

在内部共同使用一套数据结构, next 与当前的函数作用域内的其他 effect 函数,形成环状链表。

1
2
3
4
5
6
7
8
9
const effect = {
// 用于区分 effect 类型 Passive | Layout | Insertion
tag, // effect 回调函数
create, // effect 创建函数
destroy, // 销毁函数
deps, // 依赖项
// 与当前 FC 的其他 effect 形成环状链表
next: null,
};
  • 声明阶段

    mount子阶段 执行 mountEffectImpl, 生成 hook 对象拿到依赖,将当前的 effect 加入到环状链表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
    // 生成 hook 对象
    const hook = mountWorkInProgressHook();
    // 保存依赖的数组
    const nextDeps = deps === undefined ? null : deps;

    // 修改当前 fiber 的 flag
    currentlyRenderingFiber.flags |= fiberFlags;

    // 将 pushEffect 返回的环形链表保存到 hook 对象的 memoizedState 中
    hook.memoizedState = pushEffect(
    HookHasEffect,
    hookFlags,
    create,
    undefined,
    nextDeps
    );
    }

    update子阶段执行 updateEffectImpl, 获取 hook 新旧值,进行依赖比较,打上响应的 tag,在 commit 阶段统一处理

    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
    function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
    const hook = updateWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps;
    let destroy = undefined;

    if (currentHook !== null) {
    const prevEffect = currentHook.memorizedState;
    destroy = prevEffect.destroy;
    }

    if (nextDeps !== null) {
    const prevDeps = prevEffect.deps;

    // 浅比较依赖是否发生变化
    if (areHookInputsEqual(nextDeps, prevDeps)) {
    hook.memorizedState = pushEffect(hookFlags, create, destroy, nextDeps);
    return;
    }
    }

    if (deps !== prevEffect.deps) {
    fiberFlags |= hookFlags;
    }

    currentlyRenderingFiber.flags |= fiberFlags;

    // pushEffect 的作用是将当前 effect 添加到 FiberNode 的 updateQueue 中,
    // 然后返回当前 effect 保存在 Hook 节点的 memorizedState 属性中
    hook.memorizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps
    );
    }
  • 调度阶段, useEffect 独有的,因为 useEffect 函数会在 commit 之后异步执行,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
    ) {
    if (!rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = true;
    pendingPassiveEffectsRemainingLanes = remainingLanes;
    // scheduleCallback 来自于 Scheduler,用于以某一优先级调度回调函数
    scheduleCallback(NormalSchedulerPriority, () => {
    // 执行 effect 回调函数的具体方法
    flushPassiveEffects();
    return null;
    });
    }
    }

    为了保证下一次 commit 执行前,上一次的 commit 调用的 effect 已经全部执行,因此会在 commit 入口处也会执行 flushPassiveEffects

    1
    2
    3
    4
    5
    function commitRootImpl(root, renderPriorityLevel) {
    do {
    flushPassiveEffects();
    } while (rootWithPendingPassiveEffects !== null);
    }
  • 执行阶段

    commitHookEffectListUnmount 遍历 Effect 链表依次执行 effect.destroy

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function commitHookEffectListUnmont(
    flags: HookFlags,
    finishedWork: Fiber,
    nearestMountedAncestor: Fiber | null
    ) {
    const updateQueue: FunctionComponentUpdateQueue | null =
    finishedWork.updateQueue ? finishedWork.updateQueue : null;

    const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
    if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
    if ((effect.tag & flags) === flags) {
    // Unmount
    // 从 effect 对象上拿到 destroy 函数
    const destroy = effect.destroy;
    effect.destroy = undefined;
    // ...
    }
    effect = effect.next;
    } while (effect !== firstEffect);
    }
    }

    commitHookEffectListMount 遍历 Effect 链表依次执行 effect.create,在声明阶段的时候已经打上了不同的 tag,这时会根据 tag 来执行

    1
    2
    3
    4
    5
    6
    7
    8
    // 类型为 useInsertionEffect 并且存在 HasEffect tag 的 effect 会执行回调
    commitHookEffectListMount(Insertion | HasEffect, fiber);

    // 类型为 useEffect 并且存在 HasEffect tag 的 effect 会执行回调
    commitHookEffectListMount(Passive | HasEffect, fiber);

    // 类型为 useLayoutEffect 并且存在 HasEffect tag 的 effect 会执行回调
    commitHookEffectListMount(Layout | HasEffect, fiber);

useCallback / useMemo

与其他的 hooks 类似,mount 阶段创建 hooks 对象,更行阶段对比依赖,计算结果

1
2
3
4
5
6
7
8
9
10
11
12
13
function mountCallback(callback, deps) {
// 首先还是创建一个 hook 对象
const hook = mountWorkInProgressHook();

// 依赖项
const nextDeps = deps === undefined ? null : deps;

// 把要缓存的函数和依赖数组存到 hook 对象上
hook.memoizedState = [callback, nextDeps];

// 向外部返回缓存函数
return callback;
}
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
function updateCallback(callback, deps) {
// 获取之前的 hook 对象
const hook = updateWorkInProgressHook();

// 新的依赖项
const nextDeps = deps === undefined ? null : deps;

// 之前的值,也就是 [callback, nextDeps]
const prevState = hook.memorizedState;

if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1]; // 获取到之前的依赖项

// 对比依赖项是否相同
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 相同回 callback
return prevState[0];
}
}
}

// 否则重新缓存
hook.memorizedState = [callback, nextDeps];
return callback;
}

useRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function mountRef(initialValue) {
// 创建 hook 对象
const hook = mountWorkInProgressHook();
const ref = { current: initialValue };

// hook对象的 memorizedState 值 { current: initialValue }
hook.memorizedState = ref;
return ref;
}

function updateRef(initialValue) {
// 获取当前的 hook 对象
const hook = updateWorkInProgressHook();
return hook.memorizedState;
}

ref 创建之后会作为组件属性传递,在 react 的 render 阶段会标记 ref。

1
2
3
4
5
6
7
8
9
10
function markRef(current, workInProgress) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
// 标记 Reg tag
workInProgress.flags |= Ref;
}
}

commit 的 mutation 子阶段删除旧的 ref

1
2
3
4
5
6
7
8
9
10
11
function commitLayoutEffectOnFiber(
finishedRoot,
current,
finishedWork,
committedLanes
) {
// 省略代码
if (finishedWork.flags & Ref) {
commitAttachRef(finishedWork);
}
}

commit 的 layout 子阶段会重新赋值新的 ref

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
function commitAttachRef(finishedWork) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
// HostComponent 需要获取对应的 DOM 元素
instanceToUse = getPublicInstance(instance);
break;
default:
// ClassComponent 使用 FiberNode.stateNode 保存实例
instanceToUse = instance;
}

if (typeof ref === "function") {
// 函数类型,执行函数并将实例传入
let retVal;
retVal = ref(instanceToUse);
} else {
// { current: T } 类型则更新 current 指向
ref.current = instanceToUse;
}
}
}

通过 forwardRef 和 useImperativeHandle 操作元素, 尽量避手动绑定 ref,避免 ref 被修改。

  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信