⑤独一无二-单例模式

单例模式

确保一类只有一个实例,并提供全局访问点

这个模式很简单,目的就是让类只能被实例化一次,思路就是如果没有被实例化就实例化类,如果已经实例化直接返回实例化的对象。

因为在 JS 中,类的静态方法不能访问类的实例属性,所以考虑把类的实例缓存在类的静态属性中。

1
2
3
4
5
6
7
8
9
10
class Singleton {
static instance: InstanceType<typeof Singleton> | null = null;
static getInstance() {
if (!this.instance) {
return (this.instance = new Singleton());
} else {
return this.instance;
}
}
}

如果不使用 ES6 class 实现,可以使用函数的方式配合模块化规范,导出创建方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface ISingleton {}
interface ISingletonConstructor {
new (): ISingleton;
}
class Singleton implements ISingleton {
constructor() {}
}

let getSingletonInstance = (Singleton: ISingletonConstructor): ISingleton => {
let instance: ISingleton = new Singleton();
getSingletonInstance = (Singleton?: ISingletonConstructor): ISingleton =>
instance;
return instance;
};

export { getSingletonInstance };

④隐藏new操作符-工厂模式

new 有什么不对

实例化对象的过程不应该总是公开的进行,这里面会有一些耦合的问题。

对 new 本身来讲并没有什么不对,需要通过 new 操作符实例化对象。但是对于设计模式来讲,new 操作符让我们针对业务编写代码,初始化的逻辑可能在一段条件语句中,如果添加了新的类,必须修改原来的代码。也就是说我们代码没有对修改关闭。

当有一些相关类实例化的时候,可能会写出下面的代码,每当加入新的类这段代码就会被修改,也就违反对修改关闭的设计原则。

1
2
3
if (picnic) {duck = new MallardDuck();}
else if (hunting) (duck = new DecoyDuck();)
else if (inBathTub) {duck = new RubberDuck()}

简单工厂

一个最简单的工厂就是把实例化对象的过程提取出来,单独放到一个工厂类中,并暴露方法,允许第三方类通过这个方法实例化对象。

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
abstract class FruitFactory {
// 抽象工厂类的公用方法
common() {}
// 需要子类重新实现
abstract createTea(type: string): AppleTea;
}

interface AppleTea {
name(): void;
}

class Type1AppleTea implements AppleTea {
name() {
console.log("苹果茶类型1");
}
}
class Type2AppleTea implements AppleTea {
name() {
console.log("苹果茶类型2");
}
}
class Apple extends FruitFactory {
taste() {
console.log("苹果很甜");
}
public createTea(type: string): AppleTea {
if (type == "appleTeaType1") {
return new Type1AppleTea();
} else if (type == "appleTeaType1") {
return new Type2AppleTea();
}
return new Type1AppleTea();
}
}

class FruitStore {
factory: FruitFactory;
constructor(factory: FruitFactory) {
this.factory = factory;
}
makeFruitTea(type: string) {
return this.factory.createTea("type1");
}
orderTea() {
const tea = this.makeFruitTea("type1");
tea.name();
}

// 公用的茶的制作方法
// ...
}

const store = new FruitStore(new Apple());
store.orderTea();

工厂方法模式: 定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个的,工厂方法让类把实例化推迟到子类。

上面的代码中可以看到工厂是外部传入,可以创建不同类型的茶,但是茶的制作行为更像是 Store 的一部分,所以做一点改变。

  • 定义一个 Store 的抽象类,所有的 Store 都要实现这个抽象类。
  • 在 Store 抽象类中定义 createTea 的抽象方法,这个方法的内容就是之前外部传入的工厂。
  • 换句话说, 工厂方法转移到子类中实现
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
interface AppleTea {
name(): void;
}

class Type1AppleTea implements AppleTea {
name() {
console.log("苹果茶类型1");
}
}
class Type2AppleTea implements AppleTea {
name() {
console.log("苹果茶类型2");
}
}

abstract class FruitStore {
abstract createTea(type: string): any;
}

class AppleFruitStore extends FruitStore {
constructor() {
super();
}
public createTea(type: string): AppleTea {
if (type == "type1") {
return new Type1AppleTea();
} else if (type == "type2") {
return new Type2AppleTea();
}
return new Type1AppleTea();
}
orderTea() {
const tea = this.createTea("type1");
tea.name();
}
}

const store = new AppleFruitStore();
store.orderTea();

所有的工厂模式都是用来封装对象创建的。创建者类 需要有一个抽象创建者类,定义了一个抽象工厂方法,所有实现了抽象创建者类的子类,都可以用自己实现的工厂方法生产产品。产品类 是在创建者实例的工厂方法中创建出来的。

依赖倒置与抽象工厂

依赖倒置原则:要依赖抽象,不要依赖具体类,这句话很像是要面向接口编程,而不是具体的类。没错,但是这句话更强调,高层的组件不应该依赖低层的组件,而是应该两者都依赖于 抽象。抽象可以是接口,也可以是抽象类。

抽象工厂模式:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类

思考

  • 当只有一个创建者类的时候是否应该使用工厂模式,答案是肯定的,工厂模式让你便于扩展,并将产品类的创建与使用解耦。
  • 所有的工厂都是用来封装对象的创建
  • 简单工厂,虽然不是真正的设计模式,但仍不失为一个简单的方法,可以将客户程序从具体类解耦。
  • 工厂方法使用继承:把对象的创建委托给子类,子类实现工厂方法来创建对象。
  • 抽象工厂使用对象组合:对象的创建被实现在工厂接口所暴露出来的方法中
  • 所有工厂模式都通过减少应用程序和具体类之间的依赖促进松耦合。
  • 工厂方法允许类将实例化延迟到子类进行。
  • 抽象工厂创建相关的对象家族,而不需要依赖它们的具体类。
  • 依赖倒置原则,指导我们避免依赖具体类型,而要尽量依赖抽象。
  • 工厂是很有威力的技巧,帮助我们针对抽象编程,而不要针对具体类编程。

①从继承中解脱-策略模式

继承真的合理么

想象一下有一个汽车类,你用它创造了很多汽车,他们都跑在虚拟游戏世界里面。

1
2
3
4
5
6
7
8
9
10
11
class CarSuper {
public run() {
console.log("快快跑");
}
public blow() {
console.log("滴滴叫");
}
}

const benz = new CarSuper();
const bmw = new CarSuper();

但是并不是所有的汽车都是 快快跑 ,它们都有自己的极限速度,所以你很容易想到应该有一个子类,让子类去实现特有方法。

1
2
3
4
5
6
class Benz extends CarSuper {
public run() {
console.log("飞速跑");
}
}
const benz = new Benz();

现在你已经在继承中体会到了好处,突然有一天,你觉得创造出来的汽车应该可以载人,这正是你擅长的东西,所以不假思索的在 CarSuper类上添加了 carrying 公用方法,并开心的睡觉去了。

1
2
3
4
5
6
7
8
9
class CarSuper {
public run() {
console.log("快快跑");
}
public blow() {
console.log("滴滴叫");
}
public carrying() {}
}

第二天起来,你不敢相信自己的眼睛,一位乘客上了一辆玩具车,所以你赶紧关停了游戏服务。并紧急的思考对策,显然载人的行为并不通用,每一个不能载人的车,都需要重写载人的方法,例如玩具车。

其实你还有一个想法,既然载人的方法不通用,不如把类似载人这样的功能看作是一个接口,每一个子类都需要实现这个接口

1
2
3
4
5
6
7
8
9
10
11
12
interface Carrying {
carrying: () => void;
}

class Benz extends CarSuper implements Carrying {
public run() {
console.log("飞速跑");
}
public carrying() {
console.log("拉商人");
}
}

这显然不是一个好办法,接口不具有实现代码,虽然子类自己实现增加了灵活性,但却使子类显的非常冗余,每一个子类后面都跟着一串各不相同且需要独立实现的接口。

为了复用而是用继承可能是导致难以维护
比如上面的玩具车可以载人,是因为没有重写子类载人的方法。

找到变化之处

类的行为在不断改变,所以一旦把这个行为变成类的一部分,就需要大量的经历去跟踪这些行为会在那里造成影响,有一个原则能帮助我们 ❤‍🔥 把变化的内容独立出来,不要和稳定的代码混在一起

现在就可以着手于将载人的行为分离出来,那么这个行为应该以哪种形式存在呢,方法,类,还是对象? 其实一谈到设计模式绕不开的就是 OO(面向对象设计模式),所以我们还是采用 OO 的思想,使用类来实现这个行为。

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
interface CarryingBehavior {
carrying: () => void;
}

class BusinessCarrying implements CarryingBehavior {
public carrying() {
console.log("载商人");
}
}

class ToyCarrying implements CarryingBehavior {
public carrying() {
console.log("载玩具");
}
}

class CarSuper {
protected carryingBehavior: CarryingBehavior;
// 提供初始化时的参数,以提供默认的行为
constructor({ carryingBehavior }: { carryingBehavior: CarryingBehavior }) {
this.carryingBehavior = carryingBehavior;
}
public run() {}
public blow() {}
public carrying() {
this.carryingBehavior.carrying();
}
// 增加set方法是为了可以在运行时改变对象的行为
public setCarryingBehavior(behavior: CarryingBehavior) {
this.carryingBehavior = behavior;
}
}

class Benz extends CarSuper {
constructor() {
super({
carryingBehavior: new BusinessCarrying(),
});
}
public run() {
console.log("飞速跑");
}
}
const benz = new Benz();
benz.carrying();
benz.setCarryingBehavior(new ToyCarrying());
benz.carrying();

我们将汽车的行为抽象为 BusinessCarrying 类,抽象为类和抽象为接口的区别就是分离了实现

BusinessCarrying 类有自己的行为类接口,可就保证了行为类的灵活性,为相同的行为实现不同的效果,例如汽车载人坐在前排还是后排。

通过上面的改造,只需要让汽车类提供一个能设置行为的方法,就可以实现行为的动态化,这也是另一个设计原则 ❤‍🔥 针对接口,而不是实现编程

这样的话和私有方法有什么区别? 行为是可抽象的,是可以被穷举的,他会动态的散布在各种各样的汽车子类中。而私有方法是无法被穷举的,一旦定义一个新的子类,那么他就有自己的私有方法和私有属性。

现在我们分离了一种行为类,在共有类中被声明但不会实现,而是在子类中实例化这个行为类,这其中的原则就是 ❤‍🔥 多用组合,少用继承, 巧合的是 React 哲学中也提到了这样的思想。

最后用官方语言定义策略模式: 定义算法族,分别封装起来,让他们之间可以相互替换,让算法的变换独立于使用算法的客户

  • 知道 OO 基础,并不足以让你设计出良好的 OO 系统。
  • 良好的 OO 设计必须具备可复用、可扩充、可维护三个特性。
  • 模式可以让我们建造出具有良好 OO 设计质量的系统。
  • 模式被认为是历经验证的 O0 设计经验。
  • 模式不是代码,而是针对设计问题的通用解决方案。你可把它们应用到特定的应用中。
  • 模式不是被发明,而是被发现。
  • 大多数的模式和原则,都着眼于软件变化的主题。
  • 大多数的模式都允许系统局部改变独立于其他部分。
  • 我们常把系统中会变化的部分抽出来封装。
  • 模式让开发人员之间有共享的语言,能够最大化沟通的价值。

②发布+订阅=观察者模式

像广播一样发消息

思考一个问题,有没有一种可能,另一个人A,不知道你这个人的存在,但是却可以给你发消息。A 就像是一个广播站一样,将你们联系在一起的就是手中的收音机,你等待着 A 发布消息,而你等待消息就是一个订阅的行为。

或者换一种说法,你就像是一个观察者一样,观察消息有没有到来。无论哪种定义无非都是在描述一种关系,接受消息者和发送消息者之间的关系。

下面要实现一个新闻广播站,每当有新的新闻,会调用新闻对象的broadcastNews,并向观察者们推送消息。下面是一个非常不好的写法:

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
interface Observer {
update(message?: { news: string[] }): void;
}

class NewsObserver implements Observer {
update(message): void {
console.log(message);
}
}

const new1 = new NewsObserver();
const new2 = new NewsObserver();

interface Subscribe {
register(observer: Observer): void;
unregister(observer: Observer): void;
getWeather(city: string): NewsInfo;
notify(): void;
}

class NewsSubscribe {
getNews() {
return "获取到新闻";
}
broadcastNews() {
const news = this.getNews();
new1.update(news);
new2.update(news);
}
}

const newsSubscribe = new NewsSubscribe();

这种写法存在着很多严重的问题:

  • 观察者与被观察者紧耦合在了一起
  • 没办法在程序执行的时候动态添加或删除观察者
  • 观察者是会动态变化的对象,但是没有独立且封装

设计观察者模式

首先我们让被观察者有注册观察者和取消注册观察者的能力。这样能让观察者与被观察者解耦,并在程序执行的时候动态的添加或删除。

观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class NewsSubscribe implements Subscribe {
private observerList: Observer[];
constructor() {
this.observerList = [];
}
register(observer: Observer): void {
this.observerList.push(observer);
}

unregister(observer: Observer): void {
const index = this.observerList.indexOf(observer);
if (index > 0) {
this.observerList.splice(index, 1);
}
}
notify(): void {
this.observerList.forEach((observer) => {
observer.update();
});
}
}

被观察者不知道观察者的细节,只知道观察者实现了观察者接口。

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
interface NewsInfo {
news: string[];
}

interface Observer {
update(subscribe: NewsSubscribe): void;
}

class NewsObserver implements Observer {
update(subscribe: NewsSubscribe): void {
console.log(subscribe.getNews());
}
}

const new1 = new NewsObserver();

interface Subscribe {
register(observer: Observer): void;
unregister(observer: Observer): void;
getNews(city: string): NewsInfo;
notify(): void;
}

class NewsSubscribe implements Subscribe {
private observerList: Observer[];
constructor() {
this.observerList = [];
}
register(observer: Observer): void {
this.observerList.push(observer);
}

unregister(observer: Observer): void {
const index = this.observerList.indexOf(observer);
if (index > 0) {
this.observerList.splice(index, 1);
}
}

notify(): void {
this.observerList.forEach((observer) => {
observer.update(this);
});
}

getNews(): NewsInfo {
return {
news: ["新闻"],
};
}
}

const newsSubscribe = new NewsSubscribe();
newsSubscribe.register(new NewsObserver());
newsSubscribe.register(new NewsObserver());

newsSubscribe.notify();
  • 可以为观察者模式设置 setChanged hasChanged 方法,此方法可以控制通知的条件,避免通知的频率过高
  • 观察者模式定义了对象之间一对多的关系。主题 (也就是可观察者) 用共同的接口来更新观察者
  • 观察者和可观察者之间用松耦合方式结合 (loosecoupling),可观察者不知道观察者的细节,只知道观察者实现了观察者接口。
    有弹
  • 使用此模式时,你可从被观察者处推 (push)或拉 (pul1)数据(然而,推的方式被认为更“正确”)。
  • 有多个观察者时,不可以依赖特定的通知次序

webpack执行流程

主要流程

  • 初始化阶段(Initialization):

    解析配置:Webpack 开始处理配置文件(如 webpack.config.js),包括解析入口点、加载器、插件等。
    初始化插件:加载并初始化配置中指定的插件。
    环境准备:设置编译环境,例如选择开发模式或生产模式。

  • 编译阶段(Compilation):

    创建编译器:Webpack 创建一个编译器实例,它管理整个编译过程。
    创建编译对象:创建一个新的编译对象,它包含了此次编译的所有细节。
    读取记录:从之前的编译中读取记录(如果有),以优化编译。
    解析入口:根据配置的入口点,分析出所有依赖的模块。

  • 构建阶段(Building):

    加载模块:Webpack 递归地加载每个依赖模块,这可能涉及到使用不同的加载器处理不同类型的文件。
    模块转换:应用加载器和插件,转换模块内容(如 TS 转 JS,SASS 转 CSS)。
    构建依赖图:构建模块间的依赖关系图。

  • 优化阶段(Optimization):

    优化模块:应用各种优化策略,以减小最终资产的大小。
    代码分割:根据需要将代码分割成不同的块。
    树摇(Tree Shaking):移除未使用的代码。

  • 输出阶段(Output):

    生成资产:根据依赖图,Webpack 将所有模块打包成
    少量的打包文件(资产),通常是一个或多个 JavaScript 文件、CSS 文件和其他静态资源文件。

  • 输出资源:将生成的打包文件写入到文件系统中,通常是输出到指定的 dist 目录。

    完成阶段(Completion):
    执行插件:执行各种插件的完成钩子,完成额外的任务或清理工作。
    输出结果:Webpack 提供编译过程的摘要和详情,如编译时间、打包后的文件大小等。
    监听模式:如果启用了监听模式(watch mode),Webpack 将保持活跃状态,并在源文件更改时重新编译。
    在这个过程中,Webpack 通过其强大的插件系统和加载器机制,提供了高度的可扩展性和灵活性,允许开发人员针对不同的需求和场景进行定制和优化。

Initialization

cli.run

使用 commander 处理命令行参数

1
2
3
4
5
6
7
8
9
10
11
12
// 重写异常退出
this.program.exitOverride(async((async) => {}));

// 监听选项 以及选项触发时的处理函数
this.program.option("--no-color", "Disable colors on console.");
this.program.on("option:no-color", function () {});

// 绑定处理函数
// 最终执行webpack
this.program.action(async () => {
compiler = this.webpack(config.options);
});

创建 compiler

node_modules\webpack\lib\webpack.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
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
const createCompiler = (rawOptions) => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options);
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging,
}).apply(compiler);
// 初始化所有的插件
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else if (plugin) {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();

// 注册内部依赖插件
// ExternalsPlugin
// ChunkPrefetchPreloadPlugin
// ArrayPushCallbackChunkFormatPlugin
// EnableChunkLoadingPlugin
// JsonpChunkLoadingPlugin
// ImportScriptsChunkLoadingPlugin
// EnableWasmLoadingPlugin
// CleanPlugin
// JavascriptModulesPlugin
// JsonModulesPlugin
// AssetModulesPlugin
// EntryOptionPlugin
// EntryPlugin
// RuntimePlugin
// InferAsyncModulesPlugin
// DataUriPlugin
// FileUriPlugin
// CompatibilityPlugin

// HarmonyModulesPlugin
// 模块解析和绑定:它帮助 Webpack 解析和绑定 ES6 模块的 import 和 export 语句,确保模块之间的依赖关系被正确处理。
// 树摇(Tree Shaking):这个插件支持树摇优化,即移除未使用的模块或模块部分,以减小最终打包文件的大小。这是通过静态分析 import 和 export 语句来实现的。
// ES6 模块的原生支持:由于 ES6 模块是 JavaScript 语言的一部分,HarmonyModulesPlugin 提供了对这些模块的原生支持,无需转换为其他格式。
// 代码分割和异步加载:插件支持基于 ES6 模块的代码分割和异步加载,这有助于提高大型应用的性能。
// 与其他 Webpack 特性的集成:HarmonyModulesPlugin 与 Webpack 的其他功能(如模块热替换、代码压缩等)紧密集成。

// InferAsyncModulesPlugin
// DataUriPlugin
// FileUriPlugin
// CompatibilityPlugin
// HarmonyModulesPlugin
// AMDPlugin
// RequireJsStuffPlugin
// CommonJsPlugin
// LoaderPlugin
// NodeStuffPlugin
// APIPlugin
// ExportsInfoApiPlugin
// WebpackIsIncludedPlugin
// ConstPlugin
// UseStrictPlugin
// RequireIncludePlugin
// RequireEnsurePlugin
// RequireContextPlugin
// ImportPlugin
// ImportMetaContextPlugin
// SystemPlugin
// ImportMetaPlugin
// URLPlugin
// DefaultStatsFactoryPlugin
// DefaultStatsPresetPlugin
// DefaultStatsPrinterPlugin
// JavascriptMetaInfoPlugin
// EnsureChunkConditionsPlugin
// RemoveEmptyChunksPlugin
// MergeDuplicateChunksPlugin
// FlagIncludedChunksPlugin
// SideEffectsFlagPlugin
// FlagDependencyExportsPlugin
// FlagDependencyUsagePlugin
// InnerGraphPlugin
// MangleExportsPlugin
// ModuleConcatenationPlugin
// SplitChunksPlugin
// RuntimeChunkPlugin
// NoEmitOnErrorsPlugin
// RealContentHashPlugin
// WasmFinalizeExportsPlugin
// DeterministicModuleIdsPlugin
// DeterministicChunkIdsPlugin
// DefinePlugin
// SizeLimitsPlugin
// TemplatedPathPlugin
// RecordIdsPlugin
// WarnCaseSensitiveModulesPlugin
// AddManagedPathsPlugin
// ResolverCachePlugin

WorkerPlugin;
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};

compiler.run()

1
2
3
4
5
6
7
this.hooks.beforeRun.callAsync(this, (err) => {
this.hooks.run.callAsync(this, (err) => {
this.readRecords((err) => {
this.compile(onCompiled);
});
});
});

React 18 新特性

兼容性

V18 已不再支持 IE11,计划时间是 2022 年 6 月 15 日, 因为用到的一些现代浏览器新特性如 micro-tasks,在 IE 中无法充分 polyfill .

并发

V18 版本在 V17 的基础上又做了一些调整。在过去的 V17 版本中,传统模式和并发模式是共存的, 通过 createRoot API 就可以启用并发模式。React 为向并发模式迁移的最初策略是设计三种模式。

  • Legacy 模式: V17 中默认使用的模式。默认开启严格模式。默认同步更新。Legacy Suspense semantics。
  • Blocking 模式: Legacy 和 Concurrent 混合模式。默认开启严格模式。默认同步更新。开放一些新特性。
  • Concurrent 模式:V18 使用的模式。默认开启严格模式。默认并发更新。开发所有的新特性。

React 最初的计划是用户可以从 Legacy 切换到 Blocking 模式,并不需要修改任何语法,配合严格模式(StrictMode)修改其中的报错,当所有错误被解决之后,可以直接切换到的 Concurrent 模式。

但在实际的场景 React 思考了以下几个问题:

  • 项目中会有成百上千个文件,开启严格模式,会有大量的错误信息,虽然不影响程序运行,但是会干扰开发并且不能快速的一次性解决。
  • 启用并发模式的好处不言而喻,可能会在未来默认启用。提供 startTransition Suspense 等 API 在内部实现并发特性。用户可以增量的选择性的使用这些 API 从而获得并发的特性。
  • 如果按照上述的思路,那么 Concurrent 模式和 Blocking 模式的唯一区别就只有是否提示错误信息。那么如果默认不启用并发模式,就可以不开启严格模式,用于提示错误信息。

基于上面的思考,V18 的策略是不会默认启用并发的特性,即使用 createRoot 启用并发模式并不能体验并发的特性,如果想体验并发的特性,需要使用例如 startTransition 等支持并发特性的 API. 所以官方描述为 没有并发模式,只有并发特性

API

  • startTransition

这个 API 可以防止渲染任务立即执行,允许将应用程序中的某些更新标记为非紧急更新,因此它们会暂停,同时优先考虑更紧急的更新。这可以在一个复杂的更新中相应用户输入。

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 aaaaaaaaaaaaaaa() {
for (let i = 0; i < 100000000; i += 1) {
const a = 1;
}
}
const [num, setNum] = useState<string>("");
const [list, setList] = useState<any[]>([]);
useEffect(() => {
setList(new Array(20000).fill(null));
setTimeout(() => {
const event = document.createEvent("MouseEvents");
event.initEvent("click", true, true);
document.getElementById("button")!.dispatchEvent(event);
}, 500);
}, []);
return (
<>
<button
id="button"
type="button"
onClick={() => {
setNum(() => {
aaaaaaaaaaaaaaa();
return "123";
});
}}
>
点击
</button>
<p>{num}</p>
{list.map((_, i) => (
<div key={Math.random()}>{i}</div>
))}
</>
);

在没有使用并发特性的时候,列表的渲染是一个同步任务,不会相应模拟的用户事件

当开启了并发特性之后,会被拆分成小任务异步执行

1
2
3
startTransition(() => {
setList(new Array(20000).fill(null));
});

  • useDeferredValue

会创建一个数据的副本,如果当前更新是一个紧急更新,useDeferredValue 会返回之前的状态,从而优先响应紧急更新。当紧急更新渲染完成后,才会去执行的当前更新。底层实现与 useDeferredValue 类似。

使用 useDeferredValue 也可以实现相同的效果

1
2
const [list, setList] = useState<any[]>([]);
const dList = useDeferredValue(list);
  • useId

生成一个唯一 ID, 在服务端与客户端生成的相同,防止 ID 不匹配

  • useSyncExternalStore

useSyncExternalStore  是一个新的 api,经历了一次修改,由  useMutableSource  改变而来,主要用来解决外部数据撕裂问题。

useSyncExternalStore 能够通过强制同步更新数据让 React 组件在 CM 下安全地有效地读取外接数据源。 在 Concurrent Mode 下,React 一次渲染会分片执行(以 fiber 为单位),中间可能穿插优先级更高的更新。假如在高优先级的更新中改变了公共数据(比如 redux 中的数据),那之前低优先的渲染必须要重新开始执行,否则就会出现前后状态不一致的情况。

useSyncExternalStore 一般是三方状态管理库使用,我们在日常业务中不需要关注。因为 React 自身的 useState 已经原生的解决的并发特性下的 tear(撕裂)问题。useSyncExternalStore 主要对于框架开发者,比如 redux,它在控制状态时可能并非直接使用的 React 的 state,而是自己在外部维护了一个 store 对象,用发布订阅模式实现了数据更新,脱离了 React 的管理,也就无法依靠 React 自动解决撕裂问题。因此 React 对外提供了这样一个 API。

目前 React-Redux 8.0 已经基于 useSyncExternalStore 实现。

  • useInsertionEffect

这个 Hooks 只建议  css-in-js 库来使用。 这个 Hooks 执行时机在 DOM 生成之后,useLayoutEffect 之前,它的工作原理大致和  useLayoutEffect  相同,只是此时无法访问  DOM  节点的引用,一般用于提前注入  <style>  脚本。

  • Suspense

官方对 空的 fallback 属性的处理方式做了改变:不再跳过 缺失值 或 值为 null 的 fallback 的 Suspense。如果没有指定 fallback 将会把 fallback 呈现为 null。

批处理

在 18 之前,只有在 react 事件处理函数中,才会自动执行批处理,其它情况会多次更新

在 18 之后,任何情况都会自动执行批处理,多次更新始终合并为一次

如果想要跳出批处理使用 flushSync

关于卸载组件时的更新状态警告

有的时候会遇到如下的错误

这个错误表示:无法对未挂载(已卸载)的组件执行状态更新。这是一个无效操作,并且表明我们的代码中存在内存泄漏。

实际上,这个错误并不多见,在以往的版本中,这个警告被广泛误解,并且有些误导。

这个错误的初衷,原本旨在针对一些特殊场景,譬如 你在 useEffect 里面设置了定时器,或者订阅了某个事件,从而在组件内部产生了副作用,而且忘记 return 一个函数清除副作用,则会发生内存泄漏…… 之类的场景

但是在实际开发中,更多的场景是,我们在 useEffect 里面发送了一个异步请求,在异步函数还没有被 resolve 或者被 reject 的时候,我们就卸载了组件。 在这种场景中,警告同样会触发。但是,在这种情况下,组件内部并没有内存泄漏,因为这个异步函数已经被垃圾回收了,此时,警告具有误导性。

综上所述原因,在 React 18 中,官方删除了这个报错。

返回值类型

在 React 18 中,不再检查因返回 undefined 而导致崩溃。既能返回 null,也能返回 undefined。但需要修改相应的 dts 文件。

Strict Mode

严格模式会打印两次日志,可以在 React DevTools 中关闭。

在 React 17 中,取消了其中一次渲染的控制台日志,以便让日志更容易阅读。

在 React 18 中,官方取消了这个限制。如果你安装了 React DevTools,第二次渲染的日志信息将显示为灰色,以柔和的方式显式在控制台。

Javascript 执行机制

变量提升与执行上下文

JS 代码运行会分为编译执行两个阶段

所谓的变量提升,是指在 JavaScript 代码编译过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值并存到内存中,这个默认值就是我们熟悉的 undefined。

最终在编译阶段会生成两部分内容:执行上下文(Execution context)和 可执行代码

执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。

  • 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
  • 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
  • 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。

调用栈

先进先出,执行上下文通过调用栈来管理

全局执行是上下文最先被压入栈中, 接下来如果有函数执行,当为函数创建好执行上下文后,也会被压入栈中,当函数返回时,执行上下文会从栈顶弹出.

调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。

使用 console.trace() 可以查看当前的调用栈信息.

作用域

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

ES6 总共有三种作用于, 全局作用域,函数作用域,块级作用域,如果没有块级作用域会存在两个问题

  • 变量容易在不被察觉的情况下被覆盖掉
  • 本应销毁的变量没有被销毁

JS 通过 const, let 实现块级作用域,在创建执行上下文的时候, 这两个关键字会单独存放在词法环境中,而 var 声明的变量于函数会存放在变量环境中.

1
2
3
4
5
6
7
8
9
10
function foo() {
var a = 1;
let b = 2;
{
let b = 3;
var c = 4;
let d = 5;
}
}
foo();

在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。这里所讲的变量是指通过 let 或者 const 声明的变量。

查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。

作用域链于词法作用域

作用域链是由词法作用域决定的.

下面的代码当 barfoo 内部调用的时候, 会先查找自己的词法环境中有没有 name, 然后查找自己的环境变量.

但是当发现都没有的时候并不会在 foo 的词法环境和变量环境中查找,而是直接查找全局执行上下文中的词法环境和变量环境

1
2
3
4
5
6
7
8
9
function bar() {
console.log(name);
}
function foo() {
var name = "one";
bar();
}
var name = "tow";
foo();

控制变量查找顺序的就是作用域链,其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外 部的执行上下文,这个外部引用称为 outer. 但是决定作用域链的不是执行上下文,而是词法作用域

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。

foobar 的上级作用域都是全局作用域,所以如果 foo 或者 bar 函数使用了一个它们没有定义的变量,那么它们会到全局作用域去查找。也就是说,词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。

闭包

根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
let name = "one";
const obj = {
getName() {
return name;
},
setName(_name) {
name = _name;
},
};
return obj;
}

var bar = foo();
bar.setName("two");
bar.getName();

当 obj 对象返回给全局变量 bar 时,虽然 foo 函数已经执行结束,但是 getName 和 setName 函数依然可以使用 foo 函数中的变量 name.

虽然 foo 函数执行结束后执行上下文已经从调用栈中弹出,但是由于 obj 对象的方法使用了内部 name 变量,所以 name 变量还是保存在内存中,而保存 name 变量称作闭包. 无论在那里调用 obj 对象的方法,都可以访问到 name 变量.

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

而调用 obj 方法的时候,作用域链的顺序就是:当前执行上下文–>foo 函数闭包–> 全局执行上下文

闭包回收

如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。

如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

this

实现 this 的一个初衷就是 在对象内部的方法中使用对象内部的属性

因为作用域链由词法作用域决定,所以调用 getName 并不会获取对象属性而是在全局的执行上下文中查找

1
2
3
4
5
6
const obj = {
name: "one",
getName() {
console.log(name);
},
};
全局执行上下文中的 this

this 和作用域链的唯一交点,作用域链的最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象。

这种设计很容易误操作,所以严格模式下默认执行一个函数 this 为 undefined

函数执行上下文中的 this

直接调用一个函数,其执行上下文中的 this 也是指向 window 对象的. 但是可以通过 call apply bind 修改 this 的指向

通过对象调用方法 this

this 是指向对象本身的。 也可以理解为在调用的时候转化为了这样的形式 myObj.showThis.call(myObj)

构造函数中的 this

this 指向创建的实例. new 操作符实际上做了一下几件事.

  • 创建一个空的简单 JavaScript 对象(即{});
  • 为步骤 1 新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  • 将步骤 1 新创建的对象作为 this 的上下文 ;
  • 如果该函数没有返回对象,则返回 this。

浏览器相关知识点

Web 应用趋势

  • 应用程序 Web 化,视频 , 音频 ,游戏的占比越来越高

  • 是 Web 应用移动化, Google 推出了 PWA 方案来整合 Web 和本地程序各自的优势

  • Web 操作系统化 ,可能以后浏览器会提供更多的模块给上层应用使用,如新支持的 WebAssembly;

OSI 七层网络模型

  • 应用层: HTTP,HTTPS,FTP,NFS,FMTP,DHCP,SNMP,TELNET,POP3,IRC,NNTP 这些应用层协议为应用提供服务,可以执行用户活动。
  • 表示层: 从应用层接受数据,并将 ASCII 码转化为 EBCDIC 码(二进制),执行数据压缩(有损/无损),使用 SSL (加密/解密)保证数据安全。
  • 会话层: 会话管理,身份验证,授权方面
  • 传输层:TCP,UDP, 流量控制,数据校验
  • 网络层:IP 逻辑寻址
  • 数据链层: 数据单元被称之为帧
  • 物理层:传输物理信号

进程与线程

进程是一个程序的运行实例,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。 线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。

进程和线程之间的关系有以下特点:

  • 进程中的任意一线程执行出错,都会导致整个进程的崩溃。

  • 线程之间可以对进程的公共数据进行读写操作。

  • 当一个进程关闭之后,操作系统会回收进程所占用的内存。

    虽然有些程序因为代码原因,或安装了额外的插件,可能导致内存泄漏.但只要关闭进程,内存就会被回收.

  • 进程之间的内容相互隔离。

    进程隔离是为保护操作系统中进程互不干扰的技术,每一个进程只能访问自己占有的数据.所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信(IPC)的机制了。

目前浏览器主要包含以下几个进程, 查看 Chrome 的进程类型

  • 1 个浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能

  • 多个渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • 1 个 GPU 进程。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

  • 1 个网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,后面独立出来,成为一个单独的进程。

  • 多个插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

渲染进程的个数如何计算:

  • 如果从一个标签页中打开了另一个新标签页,当新标签页和当前标签页属于同一站点的话,那么新标签页会复用当前标签页的渲染进程。 Chrome 浏览器会将浏览上下文组中属于同一站点的标签分配到同一个渲染进程中,要同时满足 A 和 B 属于同一站点,且 A 和 B 之间有链接关系
    <a> 元素的 rel 属性为 noopener noreferrer 时会阻止使用同一个渲染进程。
  • chrome 浏览器实现了站点隔离,所以 iframe 也会遵循以下的渲染规则,如果和父页面属于同一站点,且有链接关系,也会运行在相同的渲染进程中。

请求的各个阶段

DNS 域名解析过程

域名使用 . 划分, 最后由根域名服务器统一管理,根域名服务器是一个服务器集群,有十几个根域名,但是有一千多台根域名服务器,这就需要通过任播技术找到最近的一台根域名服务器。

顶级域名服务器会管理 .net .com 这些域名, 权威域名服务器会管理 .qq baidu 这些域名。

  • 发起解析前会检查本地是否有缓存文件。

  • 调用 DNS 客户端发起查询请求,本地的 DNS 服务端一般有互联网服务提供商(ISP)管理,DNS 服务端会查询是否有缓存,如果有则返回带有 Non-authoritative 字段的数据。如果是根域名服务器则返回带有 Authoritative 的字段。缓存需要定期更新。
    DNS 迭代查询的时候使用的时 UDP 请求,数据量比较小,可以用一个 UDP 包返回,比较注重效率。更新缓存时使用的时 TCP 查询。

  • 获取最近的根域名服务器的地址,查询 .com 域名的服务器地址。

  • 获取最近的 .com 服务器的地址,查询 baidu.com 的服务器地址。

  • 获取最近的 baidu.com 权威域名服务器的地址,查询 mail.baidu.com 对应的 IP。

  • 可能会返回 mail.baidu.com 对应的 CDN(内容分发服务器) 地址,这就是最终的地址。

DNS 预解析

dns-prefetch(DNS 预获取)是前端网络性能优化的一种措施。它根据浏览器定义的规则,提前解析之后可能会用到的域名,使解析结果缓存到系统缓存中,缩短 DNS 解析时间,进而提高网站的访问速度。

每当在首次 DNS 解析后会对其 IP 进行缓存。至于缓存时长,每种浏览器都不一样,比如 Chrome 的过期时间是 1 分钟,在这个期限内不会重新请求 DNS。

每当 Chrome 浏览器启动的时候,就会自动的快速解析浏览器最近一次启动时记录的前 10 个域名。所以经常访问的网址就不存在 DNS 解析的延迟,进而打开速度更快。

  • dns-prefetch 仅对跨域域上的 DNS 查找有效,因此请避免使用它来指向相同域。这是因为,到浏览器看到提示时,您站点域背后的 IP 已经被解析。

  • http 页面下所有的 a 标签的 href 都会自动去启用 DNS Prefetch,也就是说网页的 a 标签 href 带的域名,是不需要在 head 里面加上 link 手动设置的。

    可以通过在服务器端发送 X-DNS-Prefetch-Control 报头,或是在文档中使用值为 http-equiv 的 <meta> 标签:

    1
    <meta http-equiv="x-dns-prefetch-control" content="on" />
  • 强制查询特定主机名,使用 rel 属性值为 link type 中的 dns-prefetch 的 标签来对特定域名进行预读取:

    1
    <link rel="dns-prefetch" href="http://www.baidu.com/" />
Keep-Alive

通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接。不过如果浏览器或者服务器在其头信息中加入了:

1
Connection:Keep-Alive

那么 TCP 连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个 TCP 连接发送请求。保持 TCP 连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。比如,一个 Web 页面中内嵌的图片就都来自同一个 Web 站点,如果初始化了一个持久连接,你就可以复用该连接,以请求其他资源,而不需要重新再建立新的 TCP 连接。

如何保证页面文件能被完整地送达浏览器?

在网络中,一个文件通常会被拆分为很多数据包来进行传输,而数据包在传输过程中又有很大概率丢失或者出错。

联网,实际上是一套理念和协议组成的体系架构。其中,协议是一套众所周知的规则和标准,如果各方都同意使用,那么它们之间的通信将变得毫无障碍。

IP:把数据包送达目的主机

数据包要在互联网上进行传输,就要符合网际协议(Internet Protocol,简称 IP)标准, 访问网站就是向另一台有明确地址的计算机请求信息.

想通信一定要知道双方 IP 地址, 这些附加的信息会被装进一个叫 IP 头的数据结构里。IP 头是 IP 数据包开头的信息,包含 IP 版本、源 IP 地址、目标 IP 地址、生存时间等信息

  • 将要发送的数据交给网络进程
  • 创建 IP 头并附加到数据包上,并交给底层传输
  • 底层通过物理网络,将数据发送到 B
  • 数据被传输到 B 的网络层,会拆开 IP 头信息,并将数据交给上层处理
UDP:把数据包送达应用程序

到达主机后还需要把数据送给应用,,需要基于 IP 之上开发能和应用打交道的协议,最常见的是 用户数据包协议(User Datagram Protocol),简称 UDP。

UDP 中一个最重要的信息是端口号,端口号其实就是一个数字,每个想访问网络的程序都需要绑定一个端口号。通过端口号 UDP 就能把指定的数据包发送给指定的程序了,所以 IP 通过 IP 地址信息把数据包发送给指定的电脑,而 UDP 通过端口号把数据包分发给正确的程序。和 IP 头一样,端口号会被装进 UDP 头里面,UDP 头再和原始数据包合并组成新的 UDP 数据包。UDP 头中除了目的端口,还有源端口号等信息。

虽然 UDP 可以校验数据是否正确,但是对于错误的数据包,UDP 并不提供重发机制,只是丢弃当前的包,而且 UDP 在发送之后也无法知道是否能达到目的地。虽说 UDP 不能保证数据可靠性,但是传输速度却非常快,所以 UDP 会应用在一些关注速度、但不那么严格要求数据完整性的领域,如在线视频、互动游戏等。

  • 传输层会在数据包前面附加上 UDP 头,组成新的 UDP 数据包,再将新的 UDP 数据包交给网络层
  • 网络层再将 IP 头附加到数据包上,组成新的 IP 数据包,并交给底层;
  • 数据包被传输到主机 B 的网络层,在这里主机 B 拆开 IP 头信息,并将拆开来的数据部分交给传输层;
  • 在传输层,数据包中的 UDP 头会被拆开,并根据 UDP 中所提供的端口号,把数据部分交给上层的应用程序;
TCP:确保数据包的完整性

使用 UDP 来传输会存在两个问题:

  • 数据包在传输过程中容易丢失;

  • 大文件会被拆分成很多小的数据包来传输,这些小的数据包会经过不同的路由,并在不同的时间到达接收端,而 UDP 协议并不知道如何组装这些数据包,从而把这些数据包还原成完整的文件。

基于这两个问题,我们引入 TCP 了。TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。相对于 UDP,TCP 有下面两个特点:

  • 对于数据包丢失的情况,TCP 提供重传机制;

  • TCP 引入了数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件。

既然要实现重传和数据包排序,必然需要一套链接机制,这也就是三次握手,和四次挥手

  • 首先,建立连接阶段。这个阶段是通过三次握手来建立客户端和服务器之间的连接。TCP 提供面向连接的通信传输。面向连接是指在数据通信开始之前先做好两端之间的准备工作,通过三次握手建立链接.

  • 其次,传输数据阶段。在该阶段,接收端需要对每个数据包进行确认操作,也就是接收端在接收到数据包之后,需要发送确认数据包给发送端。所以当发送端发送了一个数据包之后,在规定时间内没有接收到接收端反馈的确认消息,则判断为数据包丢失,并触发发送端的重发机制。同样,一个大的文件在传输过程中会被拆分成很多小的数据包,这些数据包到达接收端后,接收端会按照 TCP 头中的序号为其排序,从而保证组成完整的数据。

  • 最后,断开连接阶段。数据传输完毕之后,就要终止连接了,涉及到最后一个阶段“四次挥手”来保证双方都能断开连接。

流量控制:通过滑动窗口,取值为 min(拥塞窗口,接受窗口);
拥塞控制:慢启动(从 1 开始指数增加),达到阈值之后每次+1(拥塞避免),如果超时阈值修改为当前窗口的一半,重新慢启动开始传输。(TCP Tahoe 版本已废弃)
快重传,快恢复,如果接收到三个确认重传的 ack,会直接将阈值修改为当前窗口的一半,并每次传输时窗口大小增加 1(拥塞避免)。(TCP Reno 版本)

HTTP1.1 HTTP2 HTTP3

  • HTTP1.1

    增加了持久连接的方法,它的特点是在一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么该 TCP 连接会一直保持,持久连接在 HTTP/1.1 中是默认开启的,所以你不需要专门为了持久连接去 HTTP 请求头设置信息,如果你不想要采用持久连接,可以在 HTTP 请求头中加上 Connection: close。目前浏览器中对于同一个域名,默认允许同时建立 6 个 TCP 持久连接。

    提供虚拟主机的支持,HTTP/1.0 中,每个域名绑定了一个唯一的 IP 地址,因此一个服务器只能支持一个域名.但是随着虚拟主机技术的发展,需要实现在一台物理主机上绑定多个虚拟主机,每个虚拟主机都有自己的单独的域名,这些单独的域名都公用同一个 IP 地址。HTTP/1.1 的请求头中增加了 Host 字段,用来表示当前的域名地址,这样服务器就可以根据不同的 Host 值做不同的处理。

    对动态生成的内容提供了支持,在设计 HTTP/1.0 时,需要在响应头中设置完整的数据大小,但是大部分页面内容都是动态的,,因此在传输数据之前并不知道最终的数据大小。HTTP/1.1 通过引入 Chunk transfer 机制来解决这个问题,服务器会将数据分割成若干个任意大小的数据块,每个数据块发送时会附上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志。这样就提供了对动态内容的支持。

    安全性,cookies

    存在的问题

    TCP 慢启动,导致小的核心资源并不能占满带宽立即下载
    多个 TCP 链接会竞争贷款,可能让核心资源的下载速度变慢
    队头阻塞的问题,同一时间只能一个 TCP 链接只能处理一个请求,其他的任务需要排队
    头信息没有压缩,并且被频繁的重复传输

  • HTTP2
    解决 HTTP1.1 存在的问题 一个域名只使用一个 TCP 长连接和消除队头阻塞问题 。但是 HTTP2 仍然是基于 TCP 协议的,TCP 传输过程中的丢包,会导致 TCP 队头阻塞,所以随着丢包率的增加,HTTP/2 的传输效率也会越来越差。有测试数据表明,当系统达到了 2% 的丢包率时,HTTP/1.1 的传输效率反而比 HTTP/2 表现得更好。

    HTTP2 构建了一个 2 进制分帧层,其中是两个核心概念。帧是数据传输的最小单位。帧的最外层格式如下。

    HTTP2 把请求和响应报文分成头部帧和数据帧。由 type 字段标识。一个流用多个帧组成,同一个流的帧共用一个流 ID 标识,可以将乱序的帧关联起来。

    使用 HPACK 算法,减少头信息的小,减小发送包的体积,从而降低延迟。此算法包含静态表,动态表,Huffman 编码,静态表预定义了 61 个 header 的 key,value。动态表由双方维护自定义的 header 字段,Huffman 编码用于消除重复的字节。

    可以设置请求的权重,高优先级的请求会优先响应。
    可以服务端推送,如果请求的网页有依赖资源,服务器会主动将资源推送到客户端。

    但是 HTTP2 并没有解决 TCP 层面的队头阻塞,如果传输过程中的包丢失,依然会重传并等待。

  • HTTP3
    在 UDP 协议之上,实现了 QUIC 协议,实现了类似 TCP 的流量控制、传输可靠性的功能。虽然 UDP 不提供可靠性的传输,但 QUIC 在 UDP 的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥塞控制以及其他一些 TCP 中存在的特性。

    集成了 TLS 加密功能。目前 QUIC 使用的是 TLS1.3,相较于早期版本 TLS1.3 有更多的优点,其中最重要的一点是减少了握手所花费的 RTT 个数。

    实现了 HTTP/2 中的多路复用功能。和 TCP 不同,QUIC 实现了在同一物理连接上可以有多个独立的逻辑数据流(如下图)。实现了数据流的单独传输,就解决了 TCP 中队头阻塞的问题。

    实现了快速握手功能。由于 QUIC 是基于 UDP 的,所以 QUIC 可以实现使用 0-RTT 或者 1-RTT 来建立连接,这意味着 QUIC 可以用最快的速度来发送和接收数据,这样可以大大提升首次打开页面的速度。

    同时也存在一下问题:

    浏览器的实现和官方定义差距过大,系统内核对 UPD 的优化不佳。中间设备僵化导致丢包率远大于 TCP 链接。

TLS 握手过程

  • 客户端发送给服务端,TLS 版本,加密套件,seq(随机数)1
  • 服务端响应 TLS 版本,加密套件,seq2
  • 服务端发送证书
  • 服务端发送公钥
  • 服务点通知客户端发送完成
  • 客户端生成 seq3(预主密钥) 用接收到的公钥加密后在发送给服务端
  • 服务端用私钥解密,获取预主密钥
  • 服务端和客户端分别用 seq1+seq2+seq3(预主密钥) 生成会话密钥
  • 后续的通信使用会话密钥

TLS 只有在握手的过程中使用非对称加密,非对称加密对资源的消耗很大,通信过程使用加密后的会话密钥。

报文格式

  • HTTP

页面性能指标

  • FP(First Paint):从开始加载到浏览器首次绘制像素到屏幕上的时间,也就是页面在屏幕上首次发生视觉变化的时间。

  • FCP(First Contentful Paint):浏览器首次绘制来自 DOM 的内容的时间。这是用户第一次开始看到页面内容,但仅仅有内容,并不意味着它是有用的内容(例如 Header、导航栏等),也不意味着有用户要消费的内容。

  • FMP(First Meaningful Paint):页面的主要内容绘制到屏幕上的时间。主要内容的定义因页面而异,例如对于博客文章,它的主要内容是标题和摘要,对于搜索页面,它的主要内容是搜索结果,对于电商的页面,图片则是主要内容。

  • FSP(First Screen Paint):页面从开始加载到首屏内容全部绘制完成的时间,用户可以看到首屏的全部内容。

  • TTI(Time to Interactive):表示网页第一次完全达到可交互状态的时间点,浏览器已经可以持续性的响应用户的输入。

HTTP 缓存

强制缓存

HTTP/1.1 定义的 Cache-Control 头用来区分对缓存机制的支持情况, 请求头和响应头都支持这个属性。通过它提供的不同的值来定义缓存策略。

  • 每次都提供最新的内容

    此方式下,每次有请求发出时,缓存会将此请求发到服务器(该请求应该会带有与本地缓存相关的验证字段),服务器端会验证请求中所描述的缓存是否过期,若未过期(注:实际就是返回 304),则缓存才使用本地缓存副本。

    1
    Cache-Control: no-cache
  • 过期
    过期机制中,最重要的指令是 “max-age=“,表示资源能够被缓存(保持新鲜)的最大时间。max-age 是距离请求发起的时间的秒数。针对应用中那些不会改变的文件,通常可以手动设置一定的时长以保证缓存有效,例如图片、css、js 等静态资源。

    Expires 响应头包含日期/时间, 即在此时候之后,响应过期。如果在 Cache-Control 响应头设置了 “max-age” 或者 “s-max-age” 指令,那么 Expires 头会被忽略。

协商缓存

强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存

  • Last-Modified / If-Modified-Since

    Last-Modified 是由服务器发送给客户端的 HTTP 响应头标签,时间值是当前资源文件的修改时间

    If-Modified-Since 是由客户端发送给服务器的 HTTP 请求头标签,客户端再次发起该请求时,会携带上次请求返回的 Last-Modified 的时间值,对比 If-Modified-Since 的时间和该资源在服务器端最后被修改的时间,决定是否更新资源

  • Etag / If-None-Match

    Etag 是由服务器发送给客户端的 HTTP 响应头标签,是服务器端生成的资源文件的一个唯一标识

    If-None-Match 是由客户端发送给服务器的 HTTP 请求头标签,客户端再次发起该请求时,会携带上次请求返回的 Etag 值,对比服务器端的 Etag 值和 If-None-Match 的值,决定是否更新资源

优先级:Etag / If-None-Match > Last-Modified / If-Modified-Since

重绘与重排

如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。

如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

如果更改一个既不要布局也不要绘制的属性,把这个过程叫做合成

使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。

请求时间线

  • Queuing(排队), 导致排队状态有很多
    遇到 CSS、HTML、JavaScript 等高优先级资源,图片、视频、音频 这些低优先级资源会让路。
    浏览器会为每个域名最多维护 6 个 TCP 连接,如果超过这个数量会处于排队状态。
    网络进程在为数据分配磁盘空间时,新的 HTTP 请求也需要短暂地等待磁盘分配结束。

    优化方案: 使用域名分片技术,提高整体的 TCP 链接数量, 把站点升级到 HTTP2。

  • Stalled(停滞), 一些其他的原因导致链接被推

  • Initial connection(初始化链接), 建立 TCP 所花费的时间如果是 HTTPS 请求,还会有 SSL 握手时间。

  • Request sent(请求发送),由于只需要把缓冲区中的数据发送出去,所以速度比较快。

  • TTFB(第一字节时间),收服务器第一个字节的数据,TTFB 时间越短,就说明服务器响应越快。

    优化方案: 提高运营商的网络带宽,使用 CDN 技术。服务器响应时间过长,问题可能出现在渲染,信息处理等方面,可以考虑提高缓存的使用效率。

  • Content Download(资源下载时间)

    优化方案: 压缩文件,流式传输。

如何验证数字证书

  • 申请数字证书
    网站需要准备一套私钥和公钥,私钥留着自己使用;
    向 CA 机构提交公钥、公司、站点等信息并等待认证,这个认证过程可能是收费的;
    CA 通过线上、线下等多种渠道来验证极客时间所提供信息的真实性,如公司是否存在、企业是否合法、域名是否归属该企业等;
    如信息审核通过,CA 会向极客时间签发认证的数字证书,包含了极客时间的公钥、组织信息、CA 的信息、有效时间、证书序列号等,这些信息都是明文的,同时包含一个 CA 生成的签名。

  • 浏览器验证证书合法性
    首先,浏览器利用证书的原始信息计算出信息摘要;然后,利用 CA 的公钥来解密数字证书中的数字签名,解密出来的数据也是信息摘要;最后,判断这两个信息摘要是否相等就可以了。
    浏览器是怎么获取到 CA 公钥的,当部署 HTTP 服务器的时候,除了部署当前的数字证书之外,还需要部署 CA 机构的数字证书,CA 机构的数字证书包括了 CA 的公钥,以及 CA 机构的一些基础信息。服务器会有两个数字证书,域名的数字证书,CA 机构的数字证书。然后在建立 HTTPS 链接时,服务器会将这两个证书一同发送给浏览器,于是浏览器就可以获取到 CA 的公钥了。
    证明 CA 机构的合法性,操作系统中内置这些 CA 机构的数字证书,,CA 机构众多,因此操作系统不可能将每家 CA 的数字证书都内置进操作系统。于是人们又想出来一个折中的方案,将颁发证书的机构划分为两种类型,根 CA(Root CAs)和中间 CA(Intermediates CAs),通常申请者都是向中间 CA 去申请证书的,而根 CA 作用就是给中间 CA 做认证,一个根 CA 会认证很多中间的 CA,而这些中间 CA 又可以去认证其他的中间 CA,也就形成了数字证书链

FAQ

  • 浏览器可以同时打开多个页签,他们端口一样吗?如果一样,数据怎么知道去哪个页签?

    端口一样的,网络进程知道每个 tcp 链接所对应的标签是那个,所以接收到数据后,会把数据分发给对应的渲染进程。

  • TCP 传送数据时 浏览器端就做渲染处理了么?如果前面数据包丢了 后面数据包先来是要等么?类似的那种实时渲染怎么处理?针对数据包的顺序性?

    接收到 http 响应头中的 content-type 类型时就开始准备渲染进程了,响应体数据一旦接受到便开始做 DOM 解析了!基于 http 不用担心数据包丢失的问题,因为丢包和重传都是在 tcp 层解决的。http 能保证数据按照顺序接收的(也就是说,从 tcp 到 http 的数据就已经是完整的了,即便是实时渲染,如果发生丢包也得在重传后才能开始渲染)

  • http 和 websocket 都是属于应用层的协议吗?

    都是应用层协议,而且 websocket 名字取的比较有迷惑性,其实和 socket 完全不一样,可以把 websocket 看出是 http 的改造版本,增加了服务器向客户端主动发送消息的能力。

浏览器渲染过程

用户输入

用户发出 URL 请求到页面开始解析的这个过程,就叫做导航。

搜索内容: 使用浏览器默认的搜索引擎合成新的带搜索关键字的 URL
合法的 URL: 加上协议,合成为完整的 URL

执行搜索交互后,会先执行 beforeunload 事件,可以通过此事件取消导航。此时浏览器的状态是在加载中,但是页面内容并没有被替换,因为需要等待提交文档阶段.

URL 请求过程

浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程。

是否命中 DNS 缓存,如果命中缓存直接返回。接下来就是利用 IP 地址和服务器建立 TCP 连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。

如果时通过链接打开页面,如果两个页面是同一站点,新页面会复用父页面的渲染进程。

提交文档

  • 网络进程获取到响应之后,向渲染进程发起提交文档的消息
  • 渲染进程接收到提交文档的消息后,会和网络进程建立传输数据的管道
  • 等文档数据传输完成之后,渲染进程会返回确认提交的消息给浏览器进程
  • 浏览器进程在收到确认提交的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。

页面渲染

  • 构建 DOM 树
    HTML 解析器(HTMLParser)负责将 HTML 字节流转换为 DOM 结构。网络进程接收到响应头之后,会根据响应头中的 content-type 字段来判断文件的类型,如果这是一个 HTML 类型的文件,然后为该请求选择或者创建一个渲染进程。网络进程和渲染进程之间会建立一个共享数据的管道,网络进程加载了多少数据,HTML 解析器便解析多少数据。

    第一个阶段,通过分词器将字节流转换为 Token。

    第二个和第三个阶段是同步进行的,需要将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。HTML 解析器维护了一个 Token 栈结构,使用栈解构实现了匹配算法。

  • 构建 StyleSheet
    属性标准化
    计算每个节点的样式,css 继承规则和层叠规则

  • 创建渲染树,遍历 DOM 树中的可见节点,计算出每个元素的样式(即 ComputedStyle)。这里的计算包括所有继承的样式和从 CSS 规则中继承来的样式。

  • 创建布局树
    计算布局信息,布局信息保存在布局树中。

  • 创建无障碍树

  • 分层,创建图层树
    定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素,需要被剪裁等,都拥有层叠上下文属性
    可以通过 will-change 属性,让元素单独在合成线程中执行。

  • 图层绘制,渲染进程生成绘制指令,真实的绘制由渲染进程中的合成线程来完成。
    通常只绘制视口附近的图像,合成线程会将图层划分为图块。

    合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。

    合成过程会使用 GPU 加速生成,这涉及到块进程操作。渲染进程把生成指令发送给 GPU 进程,生成的位图保存在 GPU 内存中。

    一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

资源加载/解析

  • JS 在 Css 前面,且在 head 中,会阻塞 DOM 的解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <html>
    <head>
    <script>
    console.log(document.querySelector("p"));
    </script>
    <link rel="stylesheet" href="http://localhost:8000/big.css" />
    </head>

    <body>
    <p id="test">xxx</p>
    </body>
    </html>

    JS 在 Css 后面,且在head 中,会等待 Css 加载,因为可能会通过 JS 获取样式,而且阻塞 DOM 解析, 也就是说只要在 Js 文件后面 DOM 解析都会被阻止。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <html>
    <head>
    <link rel="stylesheet" href="http://localhost:8000/big.css" />
    <script>
    console.log(document.querySelector("p")); //null
    </script>
    </head>

    <body>
    <p id="test">xxx?</p>
    </body>
    </html>

    在 Firefox, Chrome, Edge 中,如果 <link rel="stylesheet" href="xxx"> 后面跟着 <script>, 则 CSS 加载完成后, 才能触发 DOMContentLoaded

  • Css 文件如果在 <head> 中,那么 Css 文件的加载不会影响 DOM 的解析,但是影响 DOM 的渲染
    因为 Css 文件的加载可以放在预解析线程中,所以不会影响 DOM 解析,但是 DOM 解析完成后,必须等待样式文件被解析,才能渲染页面。

    Css 文件如果在 <body> 中,那么 Css 文件的加载会阻塞 Css 资源后面的 DOM 的解析(阻塞 DOMContentLoaded 执行的时机)。这种情况称为 FOUC(Flash of Unstyled Content) 样式闪烁。也就是说 Css 资源前面的 DOM 会被渲染,Css 后面的 DOM 必须要等待 Css 资源加载完成后才会被渲染,而之前已经渲染的 DOM 会在 Css 资源加载后重绘。

react-draggable 源码分析

react-draggable 一个可以让组件实现拖拽的库

整体结构

只有两个核心文件,Draggable 和 DraggableCore

Draggable 的作用是初始化组件,并且预处理一些参数,最终的核心逻辑由 DraggableCore 完成

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
class Draggable extends React.Component<DraggableProps, DraggableState> {
//可选属性
static propTypes = {
allowAnyClick: PropTypes.bool, //任意鼠标键可以拖动
disabled: PropTypes.bool, // 禁止拖动
enableUserSelectHack: PropTypes.bool, //如果禁止页面元素可以被选择无效,可以使用这个属性
offsetParent: PropTypes.HTMLElement, // 计算初始偏移量的时候会相对于这个元素
grid: PropTypes.arrayOf(PropTypes.number), //以网格的形式移动元素
handle: PropTypes.string, // 指定元素可拖动的区域
cancel: PropTypes.string, //指定元素不能拖动的区域
nodeRef: PropTypes.object, //用于直接指定那个元素可以被拖动
onStart: PropTypes.func, //自定义拖动开始时的回调函数
onDrag: PropTypes.func, //自定义拖动中的回调函数
onStop: PropTypes.func, //自定义拖动结束时候的回调函数
onMouseDown: PropTypes.func,
scale: PropTypes.number, // 拖动时候的偏移比例
axis: PropTypes.oneOf(["both", "x", "y", "none"]), //指明可拖动的方向轴
// 拖动边界
bounds: PropTypes.oneOfType([
PropTypes.shape({
left: PropTypes.number,
right: PropTypes.number,
top: PropTypes.number,
bottom: PropTypes.number,
}),
PropTypes.string,
PropTypes.oneOf([false]),
]),
//自定义拖动时候的样式
defaultClassName: PropTypes.string,
defaultClassNameDragging: PropTypes.string,
defaultClassNameDragged: PropTypes.string,
// 默认位置
defaultPosition: PropTypes.shape({
x: PropTypes.number,
y: PropTypes.number,
}),
// 默认偏移量
positionOffset: PropTypes.shape({
x: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
y: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
}),
};
// 初始化状态
constructor(props: DraggableProps) {
super(props);
this.state = {
dragging: false,
dragged: false,
x: props.position ? props.position.x : props.defaultPosition.x,
y: props.position ? props.position.y : props.defaultPosition.y,
prevPropsPosition: { ...props.position },
// 用于计算在边界外的拖动
slackX: 0,
slackY: 0,
isElementSVG: false,
};
}

// 如果组件是受控的,检查新传入的值时候与上一次更新时候的值不同,避免组件频繁更新
static getDerivedStateFromProps(
{ position }: DraggableProps,
{ prevPropsPosition }: DraggableState
): ?$Shape<DraggableState> {
if (
position &&
(!prevPropsPosition ||
position.x !== prevPropsPosition.x ||
position.y !== prevPropsPosition.y)
) {
return {
x: position.x,
y: position.y,
prevPropsPosition: { ...position },
};
}
return null;
}
findDOMNode() {}
// 预处理用户提供的事件绑定方法
// 其他的事件属性与这个类似
onDragStart(e, coreData) {
// 创建一个事件对象,返回给使用者
const shouldStart = this.props.onStart(
e,
createDraggableData(this, coreData)
);
// 如果绑定方法返回 false,那么同样放回false,在实际调用时停止程序执行
if (shouldStart === false) return false;
//标记状态, 表示拖动开始
this.setState({ dragging: true, dragged: true });
}
onDrag() {
// Draggable 中的状态与 DraggableCore 中的状态是区分开的
if (!this.state.dragging) return false;

// 在这里处理边界问题和偏移量比例的问题
// 相当于把用户的自定义配置影响分离出来,DraggableCore 只处理核心拖拽的问题
function createDraggableData(draggable, coreData) {
const scale = draggable.props.scale;
return {
node: coreData.node,
x: draggable.state.x + coreData.deltaX / scale,
y: draggable.state.y + coreData.deltaY / scale,
deltaX: coreData.deltaX / scale,
deltaY: coreData.deltaY / scale,
lastX: draggable.state.x,
lastY: draggable.state.y,
};
}
const uiData = createDraggableData(this, coreData);

const newState: $Shape<DraggableState> = {
x: uiData.x,
y: uiData.y,
};

// Keep within bounds.
if (this.props.bounds) {
// Save original x and y.
const { x, y } = newState;

// Add slack to the values used to calculate bound position. This will ensure that if
// we start removing slack, the element won't react to it right away until it's been
// completely removed.
newState.x += this.state.slackX;
newState.y += this.state.slackY;

const [newStateX, newStateY] = getBoundPosition(
this,
newState.x,
newState.y
);
newState.x = newStateX;
newState.y = newStateY;

// Recalculate slack by noting how much was shaved by the boundPosition handler.
newState.slackX = this.state.slackX + (x - newState.x);
newState.slackY = this.state.slackY + (y - newState.y);

// Update the event we fire to reflect what really happened after bounds took effect.
uiData.x = newState.x;
uiData.y = newState.y;
uiData.deltaX = newState.x - this.state.x;
uiData.deltaY = newState.y - this.state.y;
}

// Short-circuit if user's callback killed it.
const shouldUpdate = this.props.onDrag(e, uiData);
if (shouldUpdate === false) return false;

this.setState(newState);
}
onDragStop() {}
render() {
return (
<DraggableCore
{...draggableCoreProps}
onStart={this.onDragStart}
onDrag={this.onDrag}
onStop={this.onDragStop}
>
{React.cloneElement(React.Children.only(children), {
className: className,
style: { ...children.props.style, ...style },
transform: svgTransform,
})}
</DraggableCore>
);
}
}

class DraggableCore extends React.Component<
DraggableCoreProps,
DraggableCoreState
> {
componentDidMount() {
this.mounted = true;
// 组件挂载时去查找需要挂载的元素,并绑定事件
const thisNode = this.findDOMNode();
// 因为移动短的touch 事件可能会让屏幕发生滚动,所以必须通过原生事件绑定传入 passive 参数
if (thisNode) {
addEvent(thisNode, eventsFor.touch.start, this.onTouchStart, {
passive: false,
});
}
}
findDOMNode(): ?HTMLElement {
return this.props?.nodeRef
? this.props?.nodeRef?.current
: ReactDOM.findDOMNode(this);
}

handleDragStart() {}
handleDrag() {}
handleDragStop() {}
// 在组件上绑定下面四个方法,当方法被触发时,可以区分是否是触摸设备
onMouseDown() {
const eventsFor = {
touch: {
start: "touchstart",
move: "touchmove",
stop: "touchend",
},
mouse: {
start: "mousedown",
move: "mousemove",
stop: "mouseup",
},
};
dragEventFor = eventsFor.mouse; // on touchscreen laptops we could switch back to mouse
return this.handleDragStart(e);
}
onMouseUp() {}
onTouchStart() {}
onTouchEnd() {}
}

getBoundPosition

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
function getBoundPosition(
draggable: Draggable,
x: number,
y: number
): [number, number] {
// If no bounds, short-circuit and move on
if (!draggable.props.bounds) return [x, y];

// Clone new bounds
let { bounds } = draggable.props;
bounds = typeof bounds === "string" ? bounds : cloneBounds(bounds);
const node = findDOMNode(draggable);

if (typeof bounds === "string") {
const { ownerDocument } = node;
const ownerWindow = ownerDocument.defaultView;
let boundNode;
if (bounds === "parent") {
boundNode = node.parentNode;
} else {
boundNode = ownerDocument.querySelector(bounds);
}
if (!(boundNode instanceof ownerWindow.HTMLElement)) {
throw new Error(
'Bounds selector "' + bounds + '" could not find an element.'
);
}
const boundNodeEl: HTMLElement = boundNode; // for Flow, can't seem to refine correctly
const nodeStyle = ownerWindow.getComputedStyle(node);
const boundNodeStyle = ownerWindow.getComputedStyle(boundNodeEl);
// Compute bounds. This is a pain with padding and offsets but this gets it exactly right.
bounds = {
left:
-node.offsetLeft +
int(boundNodeStyle.paddingLeft) +
int(nodeStyle.marginLeft),
top:
-node.offsetTop +
int(boundNodeStyle.paddingTop) +
int(nodeStyle.marginTop),
right:
innerWidth(boundNodeEl) -
outerWidth(node) -
node.offsetLeft +
int(boundNodeStyle.paddingRight) -
int(nodeStyle.marginRight),
bottom:
innerHeight(boundNodeEl) -
outerHeight(node) -
node.offsetTop +
int(boundNodeStyle.paddingBottom) -
int(nodeStyle.marginBottom),
};
}

// Keep x and y below right and bottom limits...
if (isNum(bounds.right)) x = Math.min(x, bounds.right);
if (isNum(bounds.bottom)) y = Math.min(y, bounds.bottom);

// But above left and top limits.
if (isNum(bounds.left)) x = Math.max(x, bounds.left);
if (isNum(bounds.top)) y = Math.max(y, bounds.top);

return [x, y];
}

handleDragStart

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
const handleDragStart: EventHandler<MouseTouchEvent> = (e) => {
// 用于自定义的 onMouseDown 事件
this.props.onMouseDown(e);

// 任意鼠标按键都一颗拖动,否则只有鼠标左键可以拖动
if (
!this.props.allowAnyClick &&
typeof e.button === "number" &&
e.button !== 0
)
return false;

// 查找到需要拖动的元素
const thisNode = this.findDOMNode();
if (!thisNode || !thisNode.ownerDocument || !thisNode.ownerDocument.body) {
throw new Error("<DraggableCore> not mounted on DragStart!");
}
const { ownerDocument } = thisNode;

// 控制可拖动区域和不可拖动区域
// handle cancel 属性传入的是控制区域的元素标签名称
// 用原生的 matches 方法检查这个标签是否在拖动元素的内部,如果有就停止执行 element.matches('p')
// 这是真正的 onStart 方法还没有执行,不需要重置状态
if (
this.props.disabled ||
!(e.target instanceof ownerDocument.defaultView.Node) ||
(this.props.handle &&
!matchesSelectorAndParentsTo(e.target, this.props.handle, thisNode)) ||
(this.props.cancel &&
matchesSelectorAndParentsTo(e.target, this.props.cancel, thisNode))
) {
return;
}

// Prevent scrolling on mobile devices, like ipad/iphone.
// Important that this is after handle/cancel.
if (e.type === "touchstart") e.preventDefault();

// Set touch identifier in component state if this is a touch event. This allows us to
// distinguish between individual touches on multitouch screens by identifying which
// touchpoint was set to this element.
// 记录触摸点的 id
const touchIdentifier = getTouchIdentifier(e);
this.setState({ touchIdentifier });

// Get the current drag point from the event. This is used as the offset.
// 首先如果存在触摸点,则回去touch 事件,否则获取原生的 event
// 通过 findDOMNode 查找需要拖动的元素, 获取用于计算相对位置的元素 offsetParent 如果没有这个属性就用上级 parentNode || document
// const x = (evt.clientX + offsetParent.scrollLeft - offsetParentRect.left) / scale;
// const y = (evt.clientY + offsetParent.scrollTop - offsetParentRect.top) / scale;
// 获取相对于父元素的鼠标位置
const position = getControlPosition(e, touchIdentifier, this);
if (position == null) return; // not possible but satisfies flow
const { x, y } = position;

// Create an event object with all the data parents need to make a decision here.
const coreEvent = createCoreData(this, x, y);

// 真正执行 onStart 方法
const shouldUpdate = this.props.onStart(e, coreEvent);
// 如果返回值为false 停止执行
if (shouldUpdate === false || this.mounted === false) return;

// Add a style to the body to disable user-select. This prevents text from
// being selected all over the page.
if (this.props.enableUserSelectHack) addUserSelectStyles(ownerDocument);

// Initiate dragging. Set the current x and y as offsets
// so we know how much we've moved during the drag. This allows us
// to drag elements around even if they have been moved, without issue.
this.setState({
dragging: true,

lastX: x,
lastY: y,
});

// Add events to the document directly so we catch when the user's mouse/touch moves outside of
// this element. We use different events depending on whether or not we have detected that this
// is a touch-capable device.
addEvent(ownerDocument, dragEventFor.move, this.handleDrag);
addEvent(ownerDocument, dragEventFor.stop, this.handleDragStop);
};

handleDrag

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
const handleDrag: EventHandler<MouseTouchEvent> = (e) => {
// Get the current drag point from the event. This is used as the offset.
const position = getControlPosition(e, this.state.touchIdentifier, this);
if (position == null) return;
let { x, y } = position;

// 实现按网格拖动
if (Array.isArray(this.props.grid)) {
let deltaX = x - this.state.lastX,
deltaY = y - this.state.lastY;
const deltaX = Math.round(deltaX / this.props.grid[0]) * this.props.grid[0];
const deltaY = Math.round(deltaY / this.props.grid[0]) * this.props.grid[0];
if (!deltaX && !deltaY) return; // skip useless drag
(x = this.state.lastX + deltaX), (y = this.state.lastY + deltaY);
}

const coreEvent = createCoreData(this, x, y);

// Call event handler. If it returns explicit false, trigger end.
// 指定自定义方法,如果返回 false 手动停止执行
const shouldUpdate = this.props.onDrag(e, coreEvent);
if (shouldUpdate === false || this.mounted === false) {
try {
// $FlowIgnore
this.handleDragStop(new MouseEvent("mouseup"));
} catch (err) {}
return;
}

this.setState({
lastX: x,
lastY: y,
});
};

handleDragStop

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
const handleDragStop: EventHandler<MouseTouchEvent> = (e) => {
if (!this.state.dragging) return;

const position = getControlPosition(e, this.state.touchIdentifier, this);
if (position == null) return;
let { x, y } = position;

// Snap to grid if prop has been provided
if (Array.isArray(this.props.grid)) {
let deltaX = x - this.state.lastX || 0;
let deltaY = y - this.state.lastY || 0;
[deltaX, deltaY] = snapToGrid(this.props.grid, deltaX, deltaY);
(x = this.state.lastX + deltaX), (y = this.state.lastY + deltaY);
}

const coreEvent = createCoreData(this, x, y);

// Call event handler
const shouldContinue = this.props.onStop(e, coreEvent);
if (shouldContinue === false || this.mounted === false) return false;

const thisNode = this.findDOMNode();
if (thisNode) {
// Remove user-select hack
if (this.props.enableUserSelectHack)
removeUserSelectStyles(thisNode.ownerDocument);
}

// 重置状态并移除事件
// Reset the el.
this.setState({
dragging: false,
lastX: NaN,
lastY: NaN,
});

if (thisNode) {
// Remove event handlers
removeEvent(thisNode.ownerDocument, dragEventFor.move, this.handleDrag);
removeEvent(thisNode.ownerDocument, dragEventFor.stop, this.handleDragStop);
}
};
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信