ReactHooks没有魔法只是数组

Hooks规则

Hooks在使用的时候有两条铁律:

  • 不要在循环,条件语句,深层函数调用Hooks

  • 只在React函数组件中调用Hooks

useState实现

下面代码只是一个demo,是为了让我们理解hooks大概是怎么运作的。这不是 React 中的真正内部实现。

state初始化

创建两个空数组,分别用来存放 setters 和 state,将 指针 指到 0 的位置:

组件首次render

当首次render这个函数组件的时候。

每一个 useState 调用,当 首次 执行的时候,在 setter 数组里加入一个 setter 函数(和对应的数组index关联);然后,将 state 加入对应的 state 数组里:

组件后续(非首次)render

后续组件的每次render,指针都会重置为 0 ,每调用一次 useState,都会返回指针对应的两个数组里的 state 和 setter,然后将指针位置 +1。

setter调用处理

每一个 setter 函数,都关联了对应的指针位置。当调用某个 setter 函数式,就可以通过这个函数所关联的指针,找到对应的 state,修改state数组里对应位置的值:

为什么hooks的调用顺序不能变呢?

useState 是在一个 条件分支里。看看这样引入的bug。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let firstRender = true;

function RenderFunctionComponent() {
let initName;

if(firstRender){
[initName] = useState("Rudi");
firstRender = false;
}
const [firstName, setFirstName] = useState(initName);
const [lastName, setLastName] = useState("Yardley");

return (
<Button onClick={() => setFirstName("Fred")}>Fred</Button>
);
}
第一次render

第一个render之后,我们的两个state,firstName 和 lastName 都对应了正确的值。接下来看看组件第二次render的时候,会发生什么情况。

第二次render

第二次render之后,我们的两个state, firstName和 lastName 都成了 Rudi。这显然是错误的,必须要避免这样使用hooks!但是这也给我们演示了,hooks的调用顺序,为什么不能改变。

react团队明确强调了hooks的2个使用原则,如果不按照这些原则来使用hooks,将会导致我们数据的不一致性!

将hooks的操作想象成数组的操作,你可能不太会违背这些原则

OK,现在你应该清楚,为什么我们不能在条件块或者循环语句里调用hooks了。因为调用hooks的过程中,我们是在操作数组上的指针,如果你在多次render中,改变了hooks的调用顺序,将导致数组上的指针和组件里的 useState 不匹配,从而返回错误的 state 以及 setter 。

React作为UI运行时

宿主树

宿主树是对UI的描述,类似用json描述组织架构树一样。

但比常说的vdom的概念要更具体一点,并不是广义的结构描述模型,宿主树就是由具体的节点实例构成的。

宿主树通常有它自己的命令式 API 。而 React 就是它上面的那一层。

基于宿主树有两个最关键的部分:

  1. 稳定性,因为宿主树是对UI的描述,所以不会大范围改变,相对稳定。
  2. 通用型,每个UI的样式和交互行为,都可以拆分成可复用的最小单位。

最重要的一点是,React的宿主树是随时间变化的树,通过时间数据,完成对宿主实力的操作。

宿主实例

宿主实例就是我们通常所说的 DOM 节点 — 就像当你调用 document.createElement(‘div’) 时获得的对象。

React会帮助你调用宿主实例的API,完成对实例的操作

渲染器

帮助React与宿主树通信以及如何管理宿主实例。React DOM、React Native都可以叫做渲染器。

通常渲染有两种模式:

  1. 直接对宿主实例的修改,也就是突变模式
  2. 克隆宿主树并对宿主树顶级子树,也就是变化的树中的根节点进行操作,由于宿主树的不可变性,使得多线程更加容易。

元素

对于React来说,宿主实例就是React元素。即一个普通的javaScript对象。

但React元素并不是一直存在,它会在删除和创建之间循环

而且具有不可变性,不可以因为UI的改变而直接修改React元素,而是要重新创建它。

所以React元素可以描述UI在特定时间点的样子,因为它不会再改变。

入口

每一个 React 渲染器都有一个“入口”。正是那个特定的 API 让我们告诉 React ,将特定的 React 元素树渲染到真正的宿主实例中去。

React DOM 的入口就是 ReactDOM.render

协调

React需要精确的处理渲染之间的细微差别,例如同时调用两次ReactDOM.render()

1
2
3
4
5
6
7
8
9
10
11
12
13
ReactDOM.render(
<button className="blue" />,
document.getElementById('container')
);

// ... 之后 ...

// 应该替换掉 button 宿主实例吗?
// 还是在已有的 button 上更新属性?
ReactDOM.render(
<button className="red" />,
document.getElementById('container')
);

如果简单来做,需要删除掉原有的宿主实例,重新创建这个代价的巨大的。

所以,通过协模块来处理React元素映射到宿主树的过程。上面的例子中React会更新className属性,而不是重新创建宿主实例。

1
2
3
let domNode = domContainer.firstChild;
// 更新已有的宿主实例
domNode.className = 'red';

换句话说,React 需要决定何时更新一个已有的宿主实例来匹配新的 React 元素,何时该重新创建新的宿主实例。

所以如果相同的元素类型在同一个地方先后出现两次,React 会重用已有的宿主实例。

下面例子很好的解释了跟新还是创建

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
// let domNode = document.createElement('button');
// domNode.className = 'blue';
// domContainer.appendChild(domNode);
ReactDOM.render(
<button className="blue" />,
document.getElementById('container')
);

// 能重用宿主实例吗?能!(button → button)
// domNode.className = 'red';
ReactDOM.render(
<button className="red" />,
document.getElementById('container')
);

// 能重用宿主实例吗?不能!(button → p)
// domContainer.removeChild(domNode);
// domNode = document.createElement('p');
// domNode.textContent = 'Hello';
// domContainer.appendChild(domNode);
ReactDOM.render(
<p>Hello</p>,
document.getElementById('container')
);

// 能重用宿主实例吗?能!(p → p)
// domNode.textContent = 'Goodbye';
ReactDOM.render(
<p>Goodbye</p>,
document.getElementById('container')
);

对于子树来说会先判断父元素是否需要重新创建,在对每一个子元素重复执行这个过程。

条件

对于父元素来说前后两次渲染的子元素可能不同

1
2
3
4
5
6
7
8
9
10
11
12
function Form({ showMessage }) {
let message = null;
if (showMessage) {
message = <p>I was just added here!</p>;
}
return (
<dialog>
{message}
<input />
</dialog>
);
}

不管 showMessage 是 true 还是 false ,在渲染的过程中 总是在第二个孩子的位置且不会改变。

这样一来输入框中的状态就不会丢失了。

列表

比较树中同一位置的元素类型对于是否该重用还是重建相应的宿主实例往往已经足够。

但这只适用于当子元素是静止的并且不会重排序的情况。在上面的例子中,即使 message 不存在,我们仍然知道输入框在消息之后,并且再没有其他的子元素。

而当遇到动态列表时,我们不能确定其中的顺序总是一成不变的。

React 只会对其中的每个元素进行更新而不是将其重新排序。这样做会造成性能上的问题和潜在的 bug 。例如,当商品列表的顺序改变时,原本在第一个输入框的内容仍然会存在于现在的第一个输入框中 — 尽管事实上在商品列表里它应该代表着其他的商品!

这就是为什么每次当输出中包含元素数组时,React 都会让你指定一个叫做 key 的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
function ShoppingList({ list }) {
return (
<form>
{list.map(item => (
<p key={item.productId}>
You bought {item.name}
<br />
Enter how many do you want: <input />
</p>
))}
</form>
)
}

在渲染前后当key仍然相同时,React会重用先前的宿主实例,然后重新排序其兄弟元素。

给key赋予什么值最好呢?最好的答案就是:一个元素不会改变即使它在父元素中的顺序被改变

纯净

在 React 中,幂等性比纯净性更加重要。

在 React 组件中不允许有用户可以直接看到的副作用。换句话说,仅调用函数式组件时不应该在屏幕上产生任何变化。

控制反转

控制反转 (Inversion of control) 并不是一项新的技术,是 Martin Fowler 教授提出的一种软件设计模式。那到底什么被反转了?获得依赖对象的过程被反转了。控制反转 (下文统一简称为 IoC) 把传统模式中需要自己通过 new 实例化构造函数,或者通过工厂模式实例化的任务交给容器。通俗的来理解,就是本来当需要某个类(构造函数)的某个方法时,自己需要主动实例化变为被动,不需要再考虑如何实例化其他依赖的类,只需要依赖注入 (Dependency Injection, 下文统一简称为 DI), DI 是 IoC 的一种实现方式。所谓依赖注入就是由 IoC 容器在运行期间,动态地将某种依赖关系注入到对象之中。所以 IoC 和 DI 是从不同的角度的描述的同一件事情,就是通过引入 IoC 容器,利用依赖注入的方式,实现对象之间的解耦。

  • 组件不仅仅只是函数,它与宿主树紧密相连加上组件自身的状态提供更多的信息,包括时事件交互等。React可以知道组件的存,如果手动调用需要自己构建这些特性。

  • 组件类型参与协调 组件的类型决定了组件是否需要渲染。

  • React 能够推迟协调。 如果让 React 控制调用你的组件,它能做很多有趣的事情。例如,它可以让浏览器在组件调用之间做一些工作,这样重渲染大体量的组件树时就不会阻塞主线程。想要手动编排这个过程而不依赖 React 的话将会十分困难。

  • 更好的可调试性。 如果组件是库中所重视的一等公民,我们就可以构建丰富的开发者工具,用于开发中的自省。

惰性求值

1
2
3
4
5
6
7
8
9
10
function Page({ currentUser, children }) {
if (!currentUser.isLoggedIn) {
return <h1>Please login</h1>;
}
return (
<Layout>
{children}
</Layout>
);
}
1
2
3
<Page>
{Comments()}
</Page>

如果是手动调用组件,即使Page并不会返回Comments的内容,但是Comments还是会被渲染。使我们的代码变得不那么脆弱。

状态

宿主实例能够拥有所有相关的局部状态:focus、selection、input 等等。我们想要在渲染更新概念上相同的 UI 时保留这些状态。

我们也想可预测性地摧毁它们,当我们在概念上渲染的是完全不同的东西时。局部状态是如此有用,以至于 React 让你的组件也能拥有它。 组件仍然是函数但是 React 用对构建 UI 有好处的许多特性增强了它。在树中每个组件所绑定的局部状态就是这些特性之一。

一致性

即使我们想将协调过程本身分割成非阻塞的工作块,我们仍然需要在同步的循环中对真实的宿主实例进行操作。这样我们才能保证用户不会看见半更新状态的 UI ,浏览器也不会对用户不应看到的中间状态进行不必要的布局和样式的重新计算。

这也是为什么 React 将所有的工作分成了“渲染阶段”和“提交阶段”的原因。渲染阶段 是当 React 调用你的组件然后进行协调的时段。在此阶段进行干涉是安全的且在未来这个阶段将会变成异步的。提交阶段 就是 React 操作宿主树的时候。而这个阶段永远是同步的。

缓存

当父组件通过 setState 准备更新时,React 默认会协调整个子树。因为 React 并不知道在父组件中的更新是否会影响到其子代,所以 React 默认保持一致性。

当树的深度和广度达到一定程度时,你可以让 React 去缓存子树并且重用先前的渲染结果。 useMemo() Hooks

默认情况下,React 不会故意缓存组件。许多组件在更新的过程中总是会接收到不同的 props ,所以对它们进行缓存只会造成净亏损。

原始模型

React 并没有使用Proxy的系统来支持细粒度的更新。换句话说,任何在顶层的更新只会触发协调而不是局部更新那些受影响的组件。

这样的设计是有意而为之的。对于 web 应用来说交互时间是一个关键指标,而通过遍历整个模型去设置细粒度的监听器只会浪费宝贵的时间。此外,在很多应用中交互往往会导致或小(按钮悬停)或大(页面转换)的更新,因此细粒度的订阅只会浪费内存资源。

React 的设计原则之一就是它可以处理原始数据。如果你拥有从网络请求中获得的一组 JavaScript 对象,你可以将其直接交给组件而无需进行预处理。没有关于可以访问哪些属性的问题,或者当结构有所变化时造成的意外的性能缺损。React 渲染是 O(视图大小) 而不是 O(模型大小) ,并且你可以通过 windowing 显著地减少视图大小。

有那么一些应用细粒度订阅对它们来说是有用的 — 例如股票代码。这是一个极少见的例子,因为“所有的东西都需要在同一时间内持续更新”。虽然命令式的方法能够优化此类代码,但 React 并不适用于这种情况。同样的,如果你想要解决该问题,你就得在 React 之上自己实现细粒度的订阅。

注意,即使细粒度订阅和“反应式”系统也无法解决一些常见的性能问题。 例如,渲染一棵很深的树(在每次页面转换的时候发生)而不阻塞浏览器。改变跟踪并不会让它变得更快 — 这样只会让其变得更慢因为我们执行了额外的订阅工作。另一个问题是我们需要等待返回的数据在渲染视图之前。在 React 中,我们用并发渲染来解决这些问题。

批量更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Parent() {
let [count, setCount] = useState(0);
return (
<div onClick={() => setCount(count + 1)}>
Parent clicked {count} times
<Child />
</div>
);
}

function Child() {
let [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Child clicked {count} times
</button>
);
}

当事件被触发时,子组件的 onClick 首先被触发(同时触发了它的 setState )。然后父组件在它自己的 onClick 中调用 setState 。

如果 React 立即重渲染组件以响应 setState 调用,最终我们会重渲染子组件两次:

1
2
3
4
5
6
7
8
9
*** 进入 React 浏览器 click 事件处理过程 ***
Child (onClick)
- setState
- re-render Child // 😞 不必要的重渲染
Parent (onClick)
- setState
- re-render Parent
- re-render Child
*** 结束 React 浏览器 click 事件处理过程 ***

第一次 Child 组件渲染是浪费的。并且我们也不会让 React 跳过 Child 的第二次渲染因为 Parent 可能会传递不同的数据由于其自身的状态更新。

这就是为什么 React 会在组件内所有事件触发完成后再进行批量更新的原因:

1
2
3
4
5
6
7
8
9
*** 进入 React 浏览器 click 事件处理过程 ***
Child (onClick)
- setState
Parent (onClick)
- setState
*** Processing state updates ***
- re-render Parent
- re-render Child
*** 结束 React 浏览器 click 事件处理过程 ***

组件内调用 setState 并不会立即执行重渲染。相反,React 会先触发所有的事件处理器,然后再触发一次重渲染以进行所谓的批量更新。

调用树

React 与通常意义上的编程语言进行时不同因为它针对于渲染 UI 树,这些树需要保持“活性”,这样才能使我们与其进行交互。在第一次 ReactDOM.render() 出现之前,DOM 操作并不会执行。

这也许是对隐喻的延伸,但我喜欢把 React 组件当作 “调用树” 而不是 “调用栈” 。当我们调用完 Article 组件,它的 React “调用树” 帧并没有被摧毁。我们需要将局部状态保存以便映射到宿主实例的某个地方。

这些“调用树”帧会随它们的局部状态和宿主实例一起被摧毁,但是只会在协调规则认为这是必要的时候执行。如果你曾经读过 React 源码,你就会知道这些帧其实就是 Fibers 。

Fibers 是局部状态真正存在的地方。当状态被更新后,React 将其下面的 Fibers 标记为需要进行协调,之后便会调用这些组件。

函数式组件与类组件有何不同?

性能问题可以忽略

性能主要取决于代码的作用,而不是选择函数式还是类组件。在我们的观察中,尽管优化策略各有略微不同,但性能差异可以忽略不计。

hooks慢是因为在渲染中创建了函数么

现在浏览器中闭包与类没有明显的性能差别,除非在极端的情况下,hooks在两点上更有效率

  • 避免了类组件实例化,和constructor中绑定时间处理函数的消耗

  • 常规代码使用hooks不需要过深的组件树嵌套,在封装的基础库中使用高阶组建,渲染props和context很常见

传统上,React内联函数的性能问题主要与如何在子组件中为每个渲染断点shouldComponentUpdate传递新的回调有关。

Hooks 使用三个HooksApi来解决

  • useCallback 在每次渲染中保持了相同的句柄引用,所以shouldComponentUpdate继续工作

  • useMemo 控制子组件何时渲染,不在需要prueCompnent

  • useReducer 避免了需要吧会掉函数传递过深

类组件存在的问题

下面模拟在点击一个按钮之后发送一个关注请求

index.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
import React from "react";
import ReactDOM from "react-dom";

import ProfilePageFunction from './ProfilePageFunction';
import ProfilePageClass from './ProfilePageClass';

class App extends React.Component {
state = {
user: 'Dan',
};
render() {
return (
<>
<label>
<b>Choose profile to view: </b>
<select
value={this.state.user}
onChange={e => this.setState({ user: e.target.value })}
>
<option value="Dan">Dan</option>
<option value="Sophie">Sophie</option>
<option value="Sunil">Sunil</option>
</select>
</label>
<h1>Welcome to {this.state.user}’s profile!</h1>
<p>
<ProfilePageFunction user={this.state.user} />
<b> (function)</b>
</p>
<p>
<ProfilePageClass user={this.state.user} />
<b> (class)</b>
</p>
<p>
Can you spot the difference in the behavior?
</p>
</>
)
}
}


const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

ClassCompnent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';

class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};

handleClick = () => {
setTimeout(this.showMessage, 3000);
};

render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}

export default ProfilePage;

再点击按钮三秒之后会弹出关注的姓名,但是如果在点击之后马上修改下啦列表的值,那么三秒之后显示的是修改之后的值,这显然史有问题的

造成这个问题的原因就是this是不断在改变的,虽然在第一次渲染结束的时候,已经为setTimeout的回调函数传入了类方法showMessage,但是当点击按钮之后迅速切换下拉框,state的改变会导致组建重新加载,这时子组件会接受到一个过于新的props, 而setTimeout中的回调函数并没有与之前的旧的props绑定,而是直接读取了,this中较新的props

也许你会想使bind,但是这并不起作用,当读取props的时候,this中的props已经被修改,bind只是绑定了类方法的执行上下文,但是并没有固定为上一次的this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ProfilePage extends React.Component {
constructor(props) {
super(props);
this.showMessage = this.showMessage.bind(this);
this.handleClick = this.handleClick.bind(this);
}

showMessage() {
alert('Followed ' + this.props.user);
}

handleClick() {
setTimeout(this.showMessage, 3000);
}

render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}

类组件问题解决方法

也许你会想到绑定回调函数之前先把用到的props缓存起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ProfilePage extends React.Component {
showMessage = (user) => {
alert('Followed ' + user);
};

handleClick = () => {
const {user} = this.props;
setTimeout(() => this.showMessage(user), 3000);
};

render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}

这种方法使得代码明显变得更加冗长,并且随着时间推移容易出错。如果我们需要的不止是一个props怎么办?如果我们还需要访问state怎么办?如果 showMessage 调用了另一个方法,然后那个方法中读取了 this.props.something 或者 this.state.something,我们又将遇到同样的问题。然后我们不得不将this.props和this.state以函数参数的形式在被showMessage调用的每个方法中一路传递下去。

所以我们想到用闭包的方式解决props不能被保存的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ProfilePage extends React.Component {
render() {
// Capture the props!
const props = this.props;

// Note: we are *inside render*.
// These aren't class methods.
const showMessage = () => {
alert('Followed ' + props.user);
};

const handleClick = () => {
setTimeout(showMessage, 3000);
};

return <button onClick={handleClick}>Follow</button>;
}
}

既然只需要在render函数中定义各种函数,所以自然想到,可以省略class,直接使用函数式组件

1
2
3
4
5
6
7
8
9
10
11
12
13
function ProfilePage({ user }) {
const showMessage = () => {
alert('Followed ' + user);
};

const handleClick = () => {
setTimeout(showMessage, 3000);
};

return (
<button onClick={handleClick}>Follow</button>
);
}

而hooks其实就是捕获了state中的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function MessageThread() {
const [message, setMessage] = useState('');

const showMessage = () => {
alert('You said: ' + message);
};

const handleSendClick = () => {
setTimeout(showMessage, 3000);
};

const handleMessageChange = (e) => {
setMessage(e.target.value);
};

return (
<>
<input value={message} onChange={handleMessageChange} />
<button onClick={handleSendClick}>Send</button>
</>
);
}

Hooks与未来值

上面我们已经能拿到一个状态的过去值,但是如何能拿到一个状态的未来值

我们在一个effect内部执行赋值操作以便让ref的值只会在DOM被更新后才会改变。这确保了我们的变量突变不会破坏依赖于可中断渲染的时间切片和 Suspense等特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ProfilePage(props) {
const ref = useRef(null);
const showMessage = () => {
alert("Followed " + ref.current.user);
};
useEffect(() => {
ref.current ={user:props.user};
}, [props,ref]);
const handleClick = () => {
setTimeout(showMessage, 3000);
};

return <button onClick={handleClick}>Follow</button>;
}

总结

所谓的“陈旧的闭包”问题的出现多是由于错误的假设了“函数不会改变”或者“props永远是一样的”。事实并非如此。

函数捕获了他们的props和state —— 因此它们的标识也同样重要。这不是一个bug,而是一个函数式组件的特性。例如,对于useEffect或者useCallback来说,函数不应该被排除在“依赖数组”之外。(正确的解决方案通常是使用上面说过的useReducer或者useRef)

React Redux 解析

如何整合 UI 的更新

redux 只有简单的 subscribe 和 dispatch 的方法,而且 subscribe 执行的时候无法从回调函数中获取到数据的更新。

因此需要将 render 方法添加监听,并在监听中获取 store, 解析出需要的数据用于 UI 渲染。

1
2
3
4
5
6
7
8
9
const store = createStore(counter);
store.subscribe(render);

function render() {
const state = store.getState();
const newValue = state.toString();
const valueEl = document.getElementById("value");
valueEl.innerHTML = newValue;
}

如果想要手动在 react 中集成,类似于以下的效果。但是这样存在的问题就是任何的 dispatch 都会触发 subscribe 订阅方法的执行,因此为了优化不必要的渲染,还需要做以下的事情

  • 从 store 中解构出需要的数据
  • 与上一次保存的数据对比, 只有在获取的数据和最后保存的数据不一致时才会更新 UI
  • 保存当前的数据为了下一次对比使用
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
import { store } from "app/store"; // 1

class TodoList extends React.Component {
constructor(props) {
super(props);

this.state = {
todos: store.getState().todos, // 4
};

store.subscribe(this.handleStoreUpdate); // 2
}

handleStoreUpdate = () => {
const { todos } = store.getState(); // 3.1, 3.2
this.setState({ todos }); // 3.3
};

render() {
const { todos } = this.state;
const listItems = todos.map((todo) => <TodoItem todo={todo} />);

return <div>{listItems}</div>;
}
}

这也就是 react-redux connect 方法产生的原因,它帮助解析并对比数据,只有在数据改变时才会更新 UI

connect 接受 map 方法和组件,会将 map 之后的值和 dispatch 方法传递给组件,消费者只要简单的调用 dispatch 就会触发组件的更新。

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
function connect(mapStateToProps, mapDispatchToProps) {
return function (WrappedComponent) {
return class ConnectWrapper extends React.Component {
componentDidMount() {
this.unsubscribe = store.subscribe(this.handleChange);
}

componentWillUnmount() {
this.unsubscribe();
}

handleChange = () => {
this.forceUpdate();
};

render() {
return (
<WrappedComponent
{...this.props}
// plus props calculated from Redux store
{...mapStateToProps(store.getState(), this.props)}
{...mapDispatchToProps(store.dispatch, this.props)}
/>
);
}
};
};
}

4.x

每个执行 connect 的组件都会添加一个回调函数到 subscribe 中,第一次触发 action 所有的回调函数都会执行

是否更新的判断依赖于 store 的不可变性, 会进行以下三个检查

  • prevStoreState !== store.getState()
  • 如果不相等,current = mapState() 检查 prev === current
  • 合并所有参数 mergeProps(ownProps,stateProps,dispatchProps)

每次更新都会创建一个新组建,但是由于 React 的检查,只要完全相同的两个组件就会跳过更新,一定程度上避免子组件的渲染。

5.x

核心是优化了自顶向下的更新。下面的例子中点击父组件的按钮,子组件

react中的keep-alive

什么是 keep alive

keep-alive是vue内置的一个组件,而这个组件的作用就是能够缓存不活动的组件,一般情况下,组件进行切换的时候,默认会进行销毁,如果有需求,某个组件切换后不进行销毁,而是保存之前的状态,那么就可以利用keep-alive来实现。

这对于某些路由切换等场景非常好用,例如,如果我们需要实现一个列表页和详情页,但在用户从详情页返回列表的时候,我们不希望重新请求接口获取,也不希望重置列表的过滤、排序等条件,那这时就可以对列表页的组件用 keep-alive 包裹一下,这样,当路由切换时,会将这个组件“失活”并缓存起来,而不是直接卸载掉。

最简单的方案

大部分开发者可能都会直接使用 display: none 来将 DOM 隐藏:

1
2
3
<div style={shouldHide ? {display: 'none'} : {}}>
<Foo/>
</div>

虽然在视觉上实现了keep-alive,但并没有真正的移除组件,所以导致转场动画难(TransitionGroup)以实现

使用 Portals Api 实现 keep-alive

Portals Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

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
import React, { useEffect, useState,useRef, useCallback } from 'react';
import ReactDOM from 'react-dom'

const KeepAlive = (props)=>{
// 创建DOM元素用于缓存react子元素
const [targetElement] = useState(()=>document.createElement('div'));
// 用于挂载缓存的子元素
const containerRef = useRef(null)
// 通过外层属性判断是否需要渲染自元素
useEffect(()=>{
if (props.active) {
containerRef.current.appendChild(targetElement)
} else {
try {
containerRef.current.removeChild(targetElement)
} catch (e) {}
}
},[
props.active,
targetElement
])

return (
<>
<div ref={containerRef} />
{ReactDOM.createPortal(props.children, targetElement)}
</>
)
}

const Count = ()=>{
const [count,setCount]= useState(1);
const addCount = useCallback(()=>{
setCount(state=>state+1);
},[
setCount
])
return (<div>
<div onClick={addCount}>+1</div>
<div>
{count}
</div>
</div>)
}

const App = ()=>{
const [shouldHide,setShouldHide] = useState(false);

const toggleVisable = useCallback(()=>{
setShouldHide(state=>!state)
},[
setShouldHide
])

return (
<div>
<div>xxx</div>
<div onClick={toggleVisable}>显示隐藏</div>
<KeepAlive active={!shouldHide}>
<Count/>
</KeepAlive>
</div>
)
}

export default App;

用懒加载优化 Portals 方案

目前我们的 Conditional 组件还有一点小小的瑕疵:当组件初次渲染时,不论当前的 active 是 true 还是 false , Conditional 组件都会将 props.children 渲染。这对大型应用可能会带来非常明显的性能问题

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
const KeepAlive = (props)=>{
const [targetElement] = useState(()=>document.createElement('div'));
const containerRef = useRef(null);
const activeMark = useRef(false);
//一旦第一次加载后,会被标记为true,已加载并不会再改变
activeMark.current = activeMark.current || props.active;
useEffect(()=>{
if (props.active) {
containerRef.current.appendChild(targetElement)
} else {
try {
containerRef.current.removeChild(targetElement)
} catch (e) {}
}
},[
props.active,
targetElement
])
return (
<>
<div ref={containerRef} />
{
activeMark.current && ReactDOM.createPortal(props.children, targetElement)
}
</>
)
}

Portals 方案一些存在的问题

  • 需要手动控制 active ,不能直接基于子组件销毁/创建的生命周期事件

  • 缺少失活/激活的生命周期事件,子组件无法感知自己是不是被缓存起来了

  • 依赖了 ReactDOM ,对 SSR 不够友好

另一种实现思路

我们希望使用的时候可以像一个普通组件一样使用, 使用了KeepAlive包裹的组件将会被缓存

1
2
3
4
5
{show && (
<KeepAlive>
<Counter />
</KeepAlive>
)}

实现Wrapper组件

通过一个外层的高阶组件缓存需要被keep-alive组件的信息

提供一个keep方法并发放到下层组件中,用于收集kepp-alive组件信息

把组件挂载到一个节点上

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
import React, { createContext, useCallback, useRef, useState,memo, useEffect, useContext, useLayoutEffect } from 'react'

const KeepAliveContext = createContext()

export const AliveScope = memo((props) => {
const nodes = useRef({});
const [cache,setCache] = useState({});
const promiseThen = useRef([]);

useLayoutEffect(()=>{
while(promiseThen.current.length) {
const [resolve,id] = promiseThen.current.pop();
resolve(nodes.current[id]);
}
},[
cache,
])

const keep = useCallback((id,children)=>(
new Promise(resolve=>{
setCache(cache=> ({...cache,[id]:children}));
promiseThen.current.push([resolve,id])
})
),[setCache]);

return (
<KeepAliveContext.Provider value={keep}>
{props.children}
{Object.entries(cache).map(([id, children]) => (
<div
key={id}
ref={node => {
nodes.current[id] = node
}}
>
{children}
</div>
))}
</KeepAliveContext.Provider>
)
})

keep-alive组件的实现

通过id找到缓存的组件,并append到指定的节点上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const KeepAlive = memo(({id,children})=>{
const keep = useContext(KeepAliveContext);
const mountRef= useRef(null);

useEffect(()=>{
(async ()=>{
const element = await keep(id,children);
console.log(element);
mountRef.current && mountRef.current.appendChild(element)
})()
},[
id,
children,
keep
])

return (<div ref = {mountRef}></div>)
})

ubuntu取消代理

如果设置过代理可能导致pip3下载不可用,yarn安装错误

  1. 查看当前使用代理,如果有,需要去掉。
1
env | grep -i proxy
  1. 去掉相关代理
1
gedit /etc/enviroment 
  1. 还有~/.bashrc /etc/profile中的代理,然后source两个文件。
1
2
source .bashrc
source /etc/profile
  1. 如果还存在代理,运行:
1
2
3
4
5
6
7
unset http_proxy
unset https_proxy


export -n http_proxy
export -n https_proxy
export -n no_proxy

React Saga 解析

项目启动时 saga 启动一个生成器函数,并提供对应的指令,可以利用 saga 的中间件控制指令的执行。

最小化应用

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
// app.js
import { configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";

import counterReducer from "./reducers/counterSlice";
import counterSaga from "./sagas/counterSaga";

import App from "./App";


const sagaMiddleware = createSagaMiddleware();

const store = configureStore({
reducer: {
counter: counterReducer, // Add counter slice reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(sagaMiddleware), // Add saga middleware
});


sagaMiddleware.run(counterSaga);

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(//...);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// sagas/counterSaga.js

import { takeEvery, put, delay } from "redux-saga/effects";
import { increment, setLoading } from "../reducers/counterSlice";

export const asyncIncrement = () => ({ type: "counter/asyncIncrement" });

function* handleAsyncIncrement() {
yield put(setLoading(true));
yield delay(1000);
yield put(increment());
yield put(setLoading(false));
}

function* counterSaga() {
yield takeEvery("counter/asyncIncrement", handleAsyncIncrement);
}

export default counterSaga;

指令

saga 的生成器函数执行完成,中间件就执行结束,无法再响应 action.

  • take 监听某个 action,监听一次, yield 返回完整 action, 造成阻塞, 等待对应的 action 被触发。

  • all 接受一个数组,数组中放入生成器,等待所有的生成器结束才会继续执行,可以用于拆分监听到不同的文件中,造成阻塞

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // rootSaga.js
    import { takeEvery, put, delay, all } from "redux-saga/effects";
    import product from "./product";
    import counterSaga from "./counterSaga";

    export default function* () {
    yield all([counterSaga(), product()]);
    // 下面的代码不会执行,直到所有的生成器直接结束
    }
  • takeEvery 循环监听 action,不会阻塞,也就是这个生成器永远不会结束。

    1
    2
    3
    4
    export default function* () {
    yield takeEvery("some action", function* () {});
    // 下面的代码会执行,不会阻塞
    }
  • delay 延迟执行,阻塞

    1
    2
    3
    export default function* () {
    yield delay(1000);
    }
  • put 触发一个 action, 非阻塞

  • call 用于副作用的函数调用,是否阻塞取决与调用的函数是否异步

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function fetchData() {
    return new Promise((resolve) => setTimeout(resolve));
    }

    function fetchDataReject() {
    return new Promise((resolve, reject) => setTimeout(reject));
    }

    export default function* () {
    let data;
    try {
    data = yield fetchDataReject();
    // 如果报错,saga会通过生成器的throw抛出错误,
    // 需要自己处理
    } catch {}

    // 推荐使用 call 来处理异步
    data = yield call(fetchData, "调用参数");

    data = yield call(["this 的绑定对象",fetchData], "自定义参数");
    // 效果相同
    data = yield call({context:this,fn:fetchData}, "自定义参数");
    }
  • select 取出 store 中的数据

    1
    2
    3
    export default function* () {
    const state = yield select((store) => store);
    }
  • cps 用于调用采用回调方式的异步函数

    1
    2
    3
    4
    5
    6
    7
    8
    function asyncFn = ('调用参数',fn) => {
    setTimeout(fn,3000)
    }

    export default function* () {
    // 无需传入回调函数 saga 自动处理
    const state = yield cps(asyncFn,'调用参数');
    }
  • fork 启用一个新任务,不会阻塞,返回一个 Task 内部对象,可以配合 cancel 取消本次任务
    cancel 不传递参数取消当前任务线。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function* nextTask() {}

    export default function* () {
    let task;
    while (true) {
    if (task) {
    yield cancel(task);
    }
    task = yield fork(nextTask);
    }
    }
  • cancelled 判断当前任务线是否被取消掉。

  • race 传递多个指令,其中一个结束就结束。

saga 简单实现

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
export function createSagaMiddleware() {
return function sagaMiddleware(store) {
// 会使用到store中的数据,所以要写在里面

const env = {
store,
channel: new Channel(),
};
sagaMiddleware.run = runSaga.bind(null, env);
return function (next) {
return function (action) {
const result = next(action);
// 触发 take 的订阅
env.channel.put(action.type);

return result;
};
};
};
}

//
function runSaga(env, generatorFn) {
const iterator = generatorFn();

if (typeof generatorFn == "function") {
//如果是普通函数直接执行
}
next();
// nextValue 是给将要执行下一次迭代传递的值
function next(nextValue, err, isOver) {
let result;
if (err) {
result = iterator.throw(err);
} else if (isOver) {
result = iterator.return();
} else {
result = iterator.next(nextValue);
}

if (result.done) {
return result;
}

// 解析 Effect 对象
// { @@redux-saga/IO :true,combinator:true,payload:[],type: ALL

solveResult(result.value);
}

function solveResult(effect) {
if (EffectType[effect.type]) {
runEffect(env, effect, next);
} else {
// 可以是 Promise 或是一般值
if (effect instanceof Promise) {
effect.then((res) => next(res)).catch((err) => next(void 0, err));
} else {
next(effect);
}
}
}

// 用于取消任务
return new Task(next);
}

// 所有的指令都是返回Effect对象,通过不同的处理函数,执行effect对象
function createEffect(type, payload) {
return {
type,
payload,
};
}
// 接受store对象,
function runEffect(env, effect, next) {
switch (effect.type) {
case EffectType.CALL:
handleCall(effect, next);
break;
case EffectType.PUT:
handlePut(env, effect, next);
break;
case EffectType.TAKE:
handleTake(env, effect, next);
break;
case EffectType.FORK:
handleFork(env, effect, next);
break;
case EffectType.TAKE_EVERY:
handleTakeEvery(env, effect, next);
break;
default:
return;
}
}

export function call(fn, ...args) {
return createEffect(EffectType.CALL, [fn, args]);
}

function handleCall(effect, next) {
const {
payload: [fn, args],
} = effect;
if (typeof fn === "function") {
const res = fn(...args);
if (res instanceof Promise) {
res.then((res) => next(res));
} else {
next(res);
}
}
}

// delay 就是用 call 实现的延迟方法
export function delay(timer = 0) {
return call(function () {
return new Promise((resolve) => {
setTimeout(resolve, timer);
});
});
}

export function put(action) {
return createEffect(EffectType.PUT, [action]);
}

function handlePut(env, effect, next) {
const { dispatch } = env.store;
const [action] = effect.payload;
const res = dispatch(action);
next(res);
}

export function take(action) {
return createEffect(EffectType.TAKE, [action]);
}

// take 的阻塞行为意味着需要能监听dispatch被触发
// 使用观察者模式监听take的订阅
function handleTake(env, effect, next) {
const { channel } = env;
const [action] = effect.payload;

channel.take(action.type, () => {
next(action);
});
}

export function fork(generatorFn) {
return createEffect(EffectType.FORK, [generatorFn]);
}

// 接受一个generator
function handleFork(env, effect, next) {
const [generatorFn] = effect.payload;
const task = runSaga(env, generatorFn);

next(task);
}

export function takeEvery(action, generatorFn) {
return createEffect(EffectType.TAKE_EVERY, [action, generatorFn]);
}

function handleTakeEvery(env, effect, next) {
const [action, generatorFn] = effect.payload;

return fork(function* () {
while (true) {
yield take(action);
yield fork(generatorFn);
}
});
}

export function all(generators) {
return createEffect(EffectType.TAKE_EVERY, [generators]);
}

const EffectType = {
ALL: "ALL",
CALL: "CALL",
PUT: "PUT",
TAKE: "TAKE",
FORK: "FORK",
TAKE_EVERY: "TAKE_EVERY",
};

export class Channel {
listeners = {};
take(prop, fn) {
if (!this.listeners[prop]) this.listeners[prop] = [];
this.listeners[prop].push(fn);
}

// 触发监听
put(prop, ...args) {
if (!this.listeners[prop]) return;

const listeners = this.listeners[prop];

// 一定要先删除再运行,避免take中会再次添加新的监听
this.listeners[prop] = void 0;

listeners.forEach((fn) => {
fn(...args);
});
}
}
class Task {
constructor(next) {
this.next = next;
}
cancel() {
this.next(null, null, true);
}
}

Ubuntu Server 安装

  • 选择语言

  • 是否版本更新,可以选择跳过

  • 键盘配置选择英文

  • 选择安装方式

  • 网络配置,选择 DHCP

  • 代理配置,保持默认空

  • 镜像源配置,保持默认或填写国内镜像源

  • 存储配置,保持默认

  • 配置用户和密码

  • 是否升级到专业版

  • SSH 配置, 勾选安装 OpenSSH Server

  • 额外的安装包,按情况勾选

  • 等待系统安装

执行文件分析

同步模块加载流程

webpack4 和 webpack5 同步模块加载流程基本相同

  1. 默认执行入口文件
  2. 查找模块缓存
  3. 如果没有缓存创建缓存对象,否则直接返回
  4. 创建模块后执行模块
  5. 通过__webpack_require__.r为模块添加module标识
  6. 如果有模块依赖通过__webpack_require__重新执行步骤 (1)
  7. 把导出结果添加到__webpack_exports__ ,default 属性上
  8. 导出到下一个模块使用

webpack4 通过闭包以参数的形式导入,webpack5 则是直接写在作用于内部

webpack4 模块的返回值直接添加到exprots对象中,webpack5的返回值则是通过函数,类似注入的形式添加

webapck4 同步模块

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
(function(modules) { // webpackBootstrap
// 模块的缓存
var installedModules = {};

// 核心require方法
function __webpack_require__(moduleId) {
// 如果存在缓存直接返回
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建一个新的module并放入的缓存中
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};

// 执行模块函数
// 导出的对象会被添加到module.exports对象中
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

// 标识为loading状态
module.l = true;

// 返回模块中导出的对象
return module.exports;
}


// expose the modules object (__webpack_modules__)
// 所有的模块对象
__webpack_require__.m = modules;

// expose the module cache
// 被缓存的模块对象
__webpack_require__.c = installedModules;


// 在导出对象上定义 __esModule 属性,es6模块化规范
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};

// 加载入口文件并导出
// __webpack_require_方法上添加一个静态属性s,标识入口文件模块索引
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
/************************************************************************/
({
"./src/app.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
console.log('app')
function App(){};
__webpack_exports__["default"] = (App)
}),

"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
var _app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/app.js");
Object(_app__WEBPACK_IMPORTED_MODULE_0__["default"])();
console.log("index.js");
})
});

webpack5 同步模块

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
(() => {
"use strict";
// 类似react hooks的写法,直接在内部作用域中定义并执行
var __webpack_modules__ = ({
"./src/app.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);

// 在指定的对象上面添加属性
__webpack_require__.d(__webpack_exports__, {
// 定义一个函数不会使用到未初始化的变量
"default": () => (__WEBPACK_DEFAULT_EXPORT__)
});
console.log('app');
function App(){}
const __WEBPACK_DEFAULT_EXPORT__ = (App);
}),
"./src/index.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
var _app__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/app.js");
// ???????
(0,_app__WEBPACK_IMPORTED_MODULE_0__.default)();
console.log("index.js");
})
});
// 模块缓存
var __webpack_module_cache__ = {};

// __webpack_require__没有变化
function __webpack_require__(moduleId) {
// Check if module is in cache
if(__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}

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;
}

(() => {
// define getter functions for harmony(和谐) exports
//
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
// 如果exports没有想添加的属性时
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
// 在exports上添加注入的属性
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();

// hasOwnProperty简写
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();

// 添加Module标识
(() => {
__webpack_require__.r = (exports) => {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();


// 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");

})()
;

webpack4异步模块加载过程

  • 定义全局的变量 webpackJsonp用于接受异步的chunk,重写了webpackJsonp.push方法,在异步chunkpush的时候会调用 webpackJsonpCallback

  • __webpack_require__(__webpack_require__.s = "./src/index.js"); 执行入口文件

    • 在缓存中能查到直接返回
    • 创建一个初始化的模块对象,并加入到缓存中
    • 执行模块对应的函数 –> 进入下面环节
    • 执行结束后模块加载状态标记为以完成
  • 如果文件中依赖了异步的chunk,会通过 __webpack_require__.e(chunk名称)方法引入

    • __webpack_require__.e 创建promises[]用于保存异步模块创建的Promise
    • 如果缓存中有正在pending中的模块,则把 Promise 对象添加到数组中
    • 创建Promise对象 保存 [resolve, reject, Promise]
    • 创建script标签appendhead标签中, (pending中,执行入口文件后面的方法)
    • 异步模块加载完成后自动执行,调用 window[“webpackJsonp”] push 方法,执行webpackJsonpCallback函数
    • 把异步chunk创建prosmie对象时的resolve函数添加到resolves数组中
    • 在缓存中把异步chunk标记为完成(0)
    • 在模块集合中添加新加载的异步模块
    • 在全局的数组webpackJsonp添加异步加载的模块
    • 循环执行resolve方法,全部执行后执行Promise.allthen方法
  • __webpack_require__.bind(null, "./src/app.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
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
(function(modules) {
// 加载异步模块成功后的回调函数
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];

// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
// 把模块请求promise的resolve函数添加到数组中
resolves.push(installedChunks[chunkId][0]);
}
// 标记模块已经加载完成
installedChunks[chunkId] = 0;
}

//在模块集合中添加异步请求的模块
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}

// 异步模块的信息添加到全局的jsonpArray中
if(parentJsonpFunction) parentJsonpFunction(data);

while(resolves.length) {
resolves.shift()();
}
};


// The module cache
var installedModules = {};

// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
// 标识模块加载的4个状态
var installedChunks = {
"main": 0
};

// script path function
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".js"
}

// require方法无变化
function __webpack_require__(moduleId) {
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}

// 这个文件只包含入口chunk
// 额外的chunks使用chunk loading函数
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// JSONP chunk loading for javascript
// 检查已经安装的包中有没有当前模块
var installedChunkData = installedChunks[chunkId];
if(installedChunkData !== 0) { // 0 means "already installed".

// a Promise means "currently loading".
// 4个状态中只有Promise为真
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// installedChunkData = [resolve, reject,Promise]
promises.push(installedChunkData[2] = promise);

// 开始加载异步模块
var script = document.createElement('script');
var onScriptComplete;

script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);

// create error before stack unwound to get useful stacktrace later
var error = new Error();
onScriptComplete = function (event) {
// avoid mem leaks in IE.
// 避免IE内存泄漏
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
var timeout = setTimeout(function(){
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
};

// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;

// expose the module cache
__webpack_require__.c = installedModules;

// define __esModule on exports
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};

// __webpack_public_path__
__webpack_require__.p = "";

// on error function for async loading
__webpack_require__.oe = function(err) { console.error(err); throw err; };


// 定义全局变量
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// push方法会调用webpackJsonpCallback
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;


// 加载入口模块
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
/************************************************************************/
({
"./src/index.js":
(function(module, exports, __webpack_require__) {
__webpack_require__.e(0)
.then(__webpack_require__.bind(null, "./src/app.js"))
.then(module=>{(module.default)()});
console.log("index.js");
})

});

/0.js

1
2
3
4
5
6
7
8
9
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/app.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
console.log('app');
function App(){};
__webpack_exports__["default"] = (App);
})
}]);
  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信