跨域CORS

cors是什么

由于浏览器的同源策略,不允许访问非同源的资源,cors的目的就是解决这个问题,也就是常说的跨域访问。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

两种请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。

1.请求方法是以下三种方法之一: HEAD GET POST
2.HTTP的头信息不超出以下几种字段: Accept | Accept-Language | Content-Language | Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

1
2
3
4
5
6
7
fetch("http://xxx",{
headers:{
'content-type':'application/json'
}
method:'POST',
body: JSON.stringify(body)
})

这是为了兼容表单(form),因为历史上表单一直可以发出跨域请求。AJAX 的跨域设计就是,只要表单可以发,AJAX 就可以直接发。

凡是不同时满足上面两个条件,就属于非简单请求。

简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

Origin: http://xxx

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://xxx
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。

  • Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

  • Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

  • Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回FooBar字段的值。

  • withCredentials 属性

上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials: true

另一方面,开发者必须在AJAX请求中打开withCredentials属性。

1
2
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。

1
xhr.withCredentials = false;

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

1
2
3
4
5
6
7
8
OPTIONS /cors HTTP/1.1
Origin: xxx
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,”预检”请求的头信息包括两个特殊字段。

  • Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。

  • Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

预检请求的回应

服务器收到”预检”请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://xxx
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://xxx可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

1
Access-Control-Allow-Origin: *

如果服务器否定了”预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

1
2
XMLHttpRequest cannot load http://xxx
Origin http://xxx is not allowed by Access-Control-Allow-Origin.

服务器回应的其他CORS相关字段如下。

1
2
3
4
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
  • Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

  • Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。

  • Access-Control-Allow-Credentials

该字段与简单请求时的含义相同。

  • Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

浏览器的正常请求和回应

一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

下面是”预检”请求之后,浏览器的正常CORS请求。

1
2
3
4
5
6
7
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面头信息的Origin字段是浏览器自动添加的。

下面是服务器正常的回应。

1
2
Access-Control-Allow-Origin: http://xxx
Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

与JSONP的比较

CORS与JSONP的使用目的相同,但是比JSONP更强大。

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

fetch跨域遇到的问题

如果服务器不支持CORS,fetch提供了三种模式,其中no-cors可以继续访问服务器

fetch的mode配置项有3个值,如下:

  • same-origin:该模式是不允许跨域的,它需要遵守同源策略,否则浏览器会返回一个error告知不能跨域;其对应的response type为basic。

  • cors: 该模式支持跨域请求,顾名思义它是以CORS的形式跨域;当然该模式也可以同域请求不需要后端额外的CORS支持;其对应的response type为cors。

  • no-cors: 该模式用于跨域请求但是服务器不带CORS响应头,也就是服务端不支持CORS;这也是fetch的特殊跨域请求方式;其对应的response type为opaque。

no-cors该模式允许浏览器发送本次跨域请求,但是不能访问响应返回的内容,这也是其response type为opaque透明的原因。

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

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

  • 等待系统安装

  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信