MobX小抄

MobX 是一款简单可扩展的状态管理库。它通过运用透明的函数式响应编程(Transparent Functional Reactive Programming,TFRP)使状态管理变得简单和可扩展。

MobX 应用示例

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
import React from 'react';
import ReactDOM from 'react-dom';
import { makeAutoObservable } from 'mobx';
import { observer } from 'mobx-react';

// 对应用状态进行建模。
class Timer {
secondsPassed = 0;

constructor() {
makeAutoObservable(this);
}

increase() {
this.secondsPassed += 1;
}

reset() {
this.secondsPassed = 0;
}
}

// 生成应用状态实例
const myTimer = new Timer();

// 构建一个使用 observable 状态的“用户界面”。
const TimerView = observer(({ timer }) => (
<button onClick={() => timer.reset()}>已过秒数:{timer.secondsPassed}</button>
));

ReactDOM.render(<TimerView timer={myTimer} />, document.body);

// 每秒更新一次‘已过秒数:X’中的文本。
setInterval(() => {
myTimer.increase();
}, 1000);

围绕 React 组件 TimerViewobserver 包装会自动侦测到依赖于 observable timer.secondsPassed 的渲染——即使这种依赖关系没有被明确定义出来。 响应性系统会负责在未来恰好那个字段被更新的时候将组件重新渲染。
每个事件(onClicksetInterval)都会调用一个用来更新 observable 状态 myTimer.secondsPassed 的 action(myTimer.increasemyTimer.reset)。Observable 状态的变更会被精确地传送到 TimerView 中所有依赖于它们的计算和副作用里。

状态流转概念图如下所示:

mobx_inner

MobX 应用方式

MobX 有两种 React 绑定方式,其中 mobx-react-lite 仅支持函数组件,mobx-react 还支持基于类的组件。可以使用 Yarn、NPM、CDN 集成 MobX 到项目中:

  • Yarn: yarn add mobx
  • NPM: npm install --save mobx
  • CDN: https://cdnjs.com/libraries/mobx 或者 https://unpkg.com/mobx/dist/mobx.umd.production.min.js

因为使用到了类属性特性,在与 Typescript 或 Babel 一起使用时且计划使用类时,需要转换类字段。

  • Babel: 版本>7.12 使用 @babel/plugin-proposal-class-properties 插件,配置 ["@babel/plugin-proposal-class-properties", { "loose": false }]
  • Typescript: 在 tsconfig.json 中启用编译器选项 "useDefineForClassFields": true

MobX 使用到了 Proxy 特性,如果在不支持 Proxy 特性的运行时上使用 MobX ,需要明确启用降级方案:

1
2
import { configure } from 'mobx';
configure({ useProxies: 'never' }); // Or "ifavailable".

在 MobX6 中,为了与标准 Javascript 兼容,放弃了装饰器语法,如果需要使用 @observable 等装饰器,需要明确启用同时使用 Typescript 或 Babel 进行转译:

  • Typescript:在 tsconfig.json 中启用编译器选项 "experimentalDecorators": true"useDefineForClassFields": true
  • Babel: 使用 @babel/plugin-proposal-class-properties@babel/plugin-proposal-decorators 插件,配置:
1
2
3
4
5
6
7
{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": false }]
// 与MobX 4/5不同的是, "loose" 必须为 false! ^
]
}

MobX 概念

MobX区分了应用程序中的以下三个概念:

  • State(状态)
  • Actions(动作)
  • Derivations(派生)

State(状态): 是驱动你的应用程序的数据。通常来说,状态有领域 特定状态视图状态。State 可以使用任何数据结构,但是需要被标记为 observable 从而使 MobX 可跟踪它。
Action(动作) : 是任意可以改变 State(状态) 的代码,比如用户事件处理、后端推送数据处理、调度器事件处理等等。
Derivation(派生): 任何 来源是 State(状态) 并且不需要进一步交互的东西都是 Derivation(派生)。
Mobx 区分了两种 Derivation :

  • Computed values:总是可以通过纯函数从当前的可观测 State 中派生
  • Reactions:当 State 改变时需要自动运行的副作用 (命令式编程和响应式编程之间的桥梁)

MobX 数据流向

Mobx 使用单向数据流,利用 action 改变 state ,进而更新所有受影响的 view:

mobx_inner

  1. 所有的 derivations 将在 state 改变时自动且原子化地更新。因此不能观察中间值。
  2. 所有的 derivations 默认将会同步更新,这意味着 action 可以在 state 改变之后安全的直接获得 computed value。
  3. computed value 的更新是惰性的,任何 computed value 在需要他们的副作用发生之前都是不激活的。
  4. 所有的 computed value 都应是纯函数,他们不应该修改 state。

MobX核心

创建可观察(observable) state

makeObservable

用法: makeObservable(target, annotations?, options?)

makeObservable 为每个属性指定一个注解:

  • observable 定义一个存储 state 的可追踪字段。
  • action 将一个方法标记为可以修改 state 的 action。
  • computed 标记一个可以由 state 派生出新的值并且缓存其输出的 getter。
  • flow 创建一个 flow 管理异步进程。
  • override 用于子类覆盖继承的 actionflowcomputedaction.bound
  • autoAction 不应被显式调用,但 makeAutoObservable 内部会对其进行调用,以便根据调用上下文将方法标识为 action 或者派生值。

一般情况下,makeObservable 是在类的构造函数中调用的,并且它的第一个参数是 this

所有带注解 的字段都是 不可配置的。
所有的不可观察(无状态)的字段(action, flow)都是 不可写的。

makeAutoObservable

用法:makeAutoObservable(target, overrides?, options?)

makeAutoObservable 就像是加强版的 makeObservable,在默认情况下它将推断所有的属性。makeAutoObservable 不能被用于带有 super 的类或 子类。

推断规则:

  • 所有 _自有_ 属性都成为 observable
  • 所有 getters 都成为 computed
  • 所有 setters 都成为 action
  • 所有 prototype 中的 functions 都成为 autoAction
  • 所有 prototype 中的 generator functions 都成为 flow。(需要注意,generators 函数在某些编译器配置中无法被检测到,如果 flow 没有正常运行,请务必明确地指定 flow 注解。)
  • overrides 参数中标记为 false 的成员将不会被添加注解。例如,将其用于像标识符这样的只读字段。

observable

用法:observable(source, overrides?, options?)

observable 注解可以作为一个函数进行调用,从而一次性将整个对象变成可观察的,之后被添加到这个对象中的属性也将被侦测并使其转化为可观察对象。

使用 actions 更新 state

action

用法:

  • action (注解)
  • action(fn)
  • action(name, fn)

action 注解表示了一段修改 state 的代码。
Actions 可以帮助你更好的组织你的代码并提供以下性能优势:

  1. 它们在 transactions 内部运行。任何可观察对象在最外层的 action 完成之前都不会被更新,这一点保证了在 action 完成之前,action 执行期间生成的中间值或不完整的值对应用程序的其余部分都是不可见的。
  2. 默认情况下,不允许在 actions 之外改变 state。这有助于在代码中清楚地对状态更新发生的位置进行定位。

action 注解应该仅用于会修改 state 的函数。带有 action 注解的成员是不可枚举的。

action.bound

用法: action.bound (注解)

action.bound 注解可用于将方法自动绑定到正确的实例,这样 this 会始终被正确绑定在函数内部。

runInAction

用法:runInAction(fn)

使用这个工具函数来创建一个会被立即调用的临时 action。

flow

用法:

  • flow (注解)
  • flow(function* (args) { })

flow 包装器是一个可选的 async / await 替代方案,它让 MobX action 使用起来更加容易。

flow 将一个 generator 函数 作为唯一输入。 在 generator 内部,你可以使用 yield 串联 Promise(使用 yield somePromise 代替 await somePromise)。 flow 机制将会确保 generator 在 Promise resolve 之后继续运行或者抛出错误。

带有 flow 注解的成员是不可枚举的。 flow 的返回值是一个 Promise,在 generator 函数运行完成时它将会被 resolve。 返回的 Promise 中还有一个 cancel() 方法,该方法可以打断正在运行的 generator 并取消它。 所有 try / finally 语句仍然会被运行。

flow.bound

用法: flow.bound (注解)

flow.bound 注解可用于将方法自动绑定到正确的实例,这样 this 会始终被正确绑定在函数内部。

通过 computeds 派生信息

computed

用法:

  • computed (注解)
  • computed(options) (注解)
  • computed(fn, options?)

computed 可以用来从其他可观察对象中派生信息。

使用 computed value 时,需遵循以下规则:

  1. 它们不应该有副作用或者更新其他可观察对象
  2. 避免创建和返回新的可观察对象
  3. 它们不应该依赖非可观察对象的值

使用 reactions 处理副作用

autorun

autorun 函数接受一个函数作为参数,每当该函数所观察的值发生变化时,它都应该运行。
autorun 通过在响应式上下文运行 effect 来工作。在给定的函数执行期间,MobX 会持续跟踪被 effect 直接或间接读取过的所有可观察对象和计算值。 一旦函数执行完毕,MobX 将收集并订阅所有被读取过的可观察对象,并等待其中任意一个再次发生改变。 一旦有改变发生,autorun 将会再次触发,重复整个过程。

mobx_autorun_inner

reaction

reaction 类似于 autorun,但可以让你更加精细地控制要跟踪的可观察对象。 它接受两个函数作为参数:第一个,data 函数,其是被跟踪的函数并且其返回值将会作为第二个函数,effect 函数,的输入。 重要的是要注意,副作用只会对 data 函数中被访问过的数据做出反应,这些数据可能少于 effect 函数中实际使用的数据。
一般的模式是在 data 函数中返回你在副作用中需要的所有数据, 并以这种方式更精确地控制副作用触发的时机。 与 autorun 不同,副作用在初始化时不会自动运行,而只会在 data 表达式首次返回新值之后运行。

when

when 会观察并运行给定的 predicate 函数,直到其返回 true。 一旦 predicate 返回了 true,给定的 effect 函数就会执行并且自动执行器函数将会被清理掉。
如果你没有传入 effect 函数,when 函数返回一个 Promise 类型的 disposer,并允许你手动取消。

使用 reactive context 需要遵守一些规则:

  1. 默认情况下,如果可观察对象发生了改变,受其影响的 reactions 会立即(同步)运行。然而,它们直到当前最外层的 (trans)action 执行结束后才会运行。
  2. autorun 只会跟踪给定函数在同步执行过程中所读取的可观察对象,不会跟踪异步发生的变化。
  3. autorun 不会跟踪被其调用的 action 所读取的可观察对象,因为 action 始终不会被追踪。
  4. reactions 总是会返回一个 disposer 函数,一旦不再需要这些方法中的副作用时,需要调用它们所返回的 disposer 函数。 否则可能导致内存泄漏。

使用 mobx-react 等库时,绑定中的 observer 等方式会间接创建 reaction,无需手动创建。在手动创建 reaction 之前,需要检查是否符合以下原则:

  1. 只有在引起副作用的一方与副作用之间没有直接关系的情况下才使用 reaction
  2. reactions 不应该更新其他可观察对象
  3. reactions 应该是独立的

MobX 和 Redux 的区别

参见 Redux 和 MobX 的区别

React-diff算法原理及优化

React的渲染过程

  • 如果是HTML标签则直接渲染真实DOM
  • 如果是JSX,则按以下流程进行
    1. 将JSX转换成 createElement 的代码
    2. 执行 createElement 创建虚拟DOM, 得到虚拟DOM树
    3. 根据虚拟DOM树在界面上生成真实DOM

JSX

1
2
3
4
<div>
<div><p>我是段落</p></div>
<div><span>我是span</span></div>
</div>

转换为 createElement 代码

1
2
3
4
5
6
React.createElement("div", null,
React.createElement("div", null,
React.createElement("p", null, "我是段落")),
React.createElement("div", null,
React.createElement("span", null, "我是span"))
);

生成虚拟DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
targetName: 'div',
children:[
{
targetName: 'div',
children:[
{
targetName: 'p'
}
]
},
{
targetName: 'div',
children:[
{
targetName: 'span'
}
]
}
]
}

Diff算法的原理

更新时为了避免,为了计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行原生 DOM 操作,而非重新渲染整个页面。React 使用了 Diff 算法对虚拟 DOM 的变化进行比较,基本原则为:

  • 进行同层同位置的比较
  • 如果是相同类型的元素,记录变化
  • 如果是不同类型的元素,删除以前的,使用新的

Diff算法的优化

Diff算法虽然只进行同层同位置的比较,但也有一些优化:

  1. 同层节点之间相互比较,不会跨节点比较(tree diff);
  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。(component diff);
  3. 可以通过唯一 key 来指定哪些节点在不同的渲染下保持稳定(element diff);

React18新功能和作用

2022 年 3 月 29 日 React18 正式发布。

React18 放弃了对 IE11 的支持。

新增 createRoot API 并支持并发模式渲染

为了更好的管理 root 节点,React18 引入了一个新的 root API,新的 root API 还支持并发模式的渲染(new concurrent renderer),允许进入并发模式(concurrent mode)。

React18 从 同步不可中断的更新 变成了 异步可中断的更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// React 17
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

const root = document.getElementById('root')!;

ReactDOM.render(<App />, root);

// React 18
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = document.getElementById('root')!;

ReactDOM.createRoot(root).render(<App />);

同时,在卸载组件时,我们也需要将 unmountComponentAtNode 升级为 root.unmount :

1
2
3
4
5
// React 17
ReactDOM.unmountComponentAtNode(root);

// React 18
root.unmount();

除此之外,React18 还从 render 方法中删除了回调函数,因为当使用 Suspense 时,它通常不会有预期的结果。

在新版本中,如果需要在 render 方法中使用回调函数,我们可以在组件中通过 useEffect 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// React 17
const root = document.getElementById('root')!;
ReactDOM.render(<App />, root, () => {
console.log('渲染完成');
});

// React 18
const AppWithCallback: React.FC = () => {
useEffect(() => {
console.log('渲染完成');
}, []);
return <App />;
};
const root = document.getElementById('root')!;
ReactDOM.createRoot(root).render(<AppWithCallback />);

最后,如果项目使用了服务端渲染(SSR),需要把 ReactDOM.hydration 升级为 ReactDOM.hydrateRoot

1
2
3
4
5
6
7
8
9
// React 17
import ReactDOM from 'react-dom';
const root = document.getElementById('root');
ReactDOM.hydrate(<App />, root);

// React 18
import ReactDOM from 'react-dom/client';
const root = document.getElementById('root')!;
ReactDOM.hydrateRoot(root, <App />);

另外,还需要更新 TypeScript 类型定义,如果项目使用了 TypeScript,最值得注意的变化是,现在在定义 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
// React 17
interface MyButtonProps {
color: string;
}

const MyButton: React.FC<MyButtonProps> = ({ children }) => {
// 在 React 17 的 FC 中,默认携带了 children 属性
return <div>{children}</div>;
};

export default MyButton;

// React 18
interface MyButtonProps {
color: string;
children?: React.ReactNode;
}

const MyButton: React.FC<MyButtonProps> = ({ children }) => {
// 在 React 18 的 FC 中,不存在 children 属性,需要手动申明
return <div>{children}</div>;
};

export default MyButton;

setState 自动批量处理更新

批处理是指为了获得更好的性能,在数据层,将多个状态更新批量处理,合并成一次更新(在视图层,将多个渲染合并成一次渲染)。

在 React18 之前,只有在 react 事件处理函数中,才会自动执行批处理,其它情况会多次更新;在 React18 之后,任何情况都会自动执行批处理,多次更新始终合并为一次。

React18 通过在默认情况下执行批处理来实现了开箱即用的性能改进。

新增 flushSync 手动退出批量更新

批处理是一个破坏性改动,如果想退出批量更新,可以使用 flushSync

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
import React, { useState } from 'react';
import { flushSync } from 'react-dom';

const App: React.FC = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
onClick={() => {
flushSync(() => {
setCount1(count => count + 1);
});
// 第一次更新
flushSync(() => {
setCount2(count => count + 1);
});
// 第二次更新
}}
>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</div>
);
};

export default App;

注意:flushSync 函数内部的多个 setState 仍然为批量更新,这样可以精准控制哪些不需要的批量更新。

删除了卸载组件后再更新组件状态时的警告

删除了以下警告:

1
Warining: Can't perfform a React state update on an unmounted component...

这个错误的初衷,原本旨在针对一些特殊场景,譬如 在 useEffect 里面设置了定时器,或者订阅了某个事件,从而在组件内部产生了副作用,而且忘记 return 一个函数清除副作用,则会发生内存泄漏之类的场景
但是在实际开发中,更多的场景是,我们在 useEffect 里面发送了一个异步请求,在异步函数还没有被 resolve 或者被 reject 的时候,我们就卸载了组件。 在这种场景中,警告同样会触发。但是,在这种情况下,组件内部并没有内存泄漏,因为这个异步函数已经被垃圾回收了,此时,警告具有误导性。

React 组件的返回值可以为 undefined

在 React17 中,如果需要返回一个空组件,React 只允许返回 null 。如果显式的返回了 undefined,控制台则会在运行时抛出一个错误。在 React18 中,不再检查因返回 undefined 而导致崩溃。既能返回 null,也能返回 undefined。

严格模式(Strict Mode)取消了第二次渲染的控制台日志

当使用严格模式时,React 会对每个组件进行两次渲染,以便观察一些意想不到的结果。在 React17 中,取消了其中一次渲染的控制台日志,以便让日志更容易阅读。如果安装了 React DevTools,第二次渲染的日志信息将显示为灰色,以柔和的方式显式在控制台。

Suspense 不再需要 fallback 来捕获

在 React18 的 Suspense 组件中,官方对空的 fallback 属性的处理方式做了改变:不再跳过缺失值或值为 null 的 fallback 的 Suspense 边界。相反,会捕获边界并且向外层查找,如果查找不到,将会把 fallback 呈现为 null。

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
// React 17
const App = () => {
return (
<Suspense fallback={<Loading />}> // <--- 这个边界被使用,显示 Loading 组件
<Suspense> // <--- 这个边界被跳过,没有 fallback 属性
<Page />
</Suspense>
</Suspense>
);
};

export default App;

// React 18
const App = () => {
return (
<Suspense fallback={<Loading />}> // <--- 不使用
<Suspense> // <--- 这个边界被使用,将 fallback 渲染为 null
<Page />
</Suspense>
</Suspense>
);
};

export default App;

新增 userId API

1
const id = useId();

支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不兼容,这解决了在 React17 及 17 以下版本中已经存在的问题。因为我们的服务器渲染时提供的 HTML 是无序的,useId 的原理就是每个 id 代表该组件在组件树中的层级结构。

新增 useSyncExternalStore API

useSyncExternalStore 能够通过强制同步更新数据让 React 组件在并发模式下安全地有效地读取外接数据源。 在并发模式下,React 一次渲染会分片执行(以 fiber 为单位),中间可能穿插优先级更高的更新。假如在高优先级的更新中改变了公共数据(比如 Redux 中的数据),那之前低优先的渲染必须要重新开始执行,否则就会出现前后状态不一致的情况。useSyncExternalStore 一般是三方状态管理库使用。React 自身的 useState 已经原生的解决了并发特性下 state 更新问题。目前 React-Redux 8.0 已经基于 useSyncExternalStore 实现。

新增 useInsertionEffect API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const useCSS = rule => {
useInsertionEffect(() => {
if (!isInserted.has(rule)) {
isInserted.add(rule);
document.head.appendChild(getStyleForRule(rule));
}
});
return rule;
};

const App: React.FC = () => {
const className = useCSS(rule);
return <div className={className} />;
};

export default App;

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

新增 startTransition API

startTransition,主要为了能在大量的任务下也能保持 UI 响应。这个新的 API 可以通过将特定更新标记为“过渡”来显著改善用户交互,简单来说,就是被 startTransition 回调包裹的 setState 触发的渲染被标记为不紧急渲染,这些渲染可能被其他紧急渲染所抢占。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState, useEffect, useTransition } from 'react';

const App: React.FC = () => {
const [list, setList] = useState<any[]>([]);
const [isPending, startTransition] = useTransition();
useEffect(() => {
// 使用了并发特性,开启并发更新
startTransition(() => {
setList(new Array(10000).fill(null));
});
}, []);
return (
<>
{list.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};

export default App;

由于 setListstartTransition 的回调函数中执行(使用了并发特性),所以 setList 会触发 并发更新

新增 useDeferredValue API

useDeferredValue 返回一个延迟响应的值,可以让一个 state 延迟生效,只有当前没有紧急更新时,该值才会变为最新值。useDeferredValuestartTransition 一样,都是标记了一次非紧急更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState, useEffect, useDeferredValue } from 'react';

const App: React.FC = () => {
const [list, setList] = useState<any[]>([]);
useEffect(() => {
setList(new Array(10000).fill(null));
}, []);
// 使用了并发特性,开启并发更新
const deferredList = useDeferredValue(list);
return (
<>
{deferredList.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};

export default App;

Redux小抄

Redux 是一款小巧的 JavaScript 状态容器,提供可预测化的状态管理。Redux 常见于 React 应用的数据状态管理,但是 Redux 不仅仅局限于 React,还支持其它 UI 库。

Redux 应用示例

在 Redux 中,应用的整体全局状态以对象树的方式存放于单个 store 中。唯一改变状态树(state tree)的方法是创建 action。action 是一个描述发生了什么的对象,并将其 dispatch 给 store。要指定状态树如何响应 action 来进行更新,可以编写纯 reducer 函数,这些函数根据旧 state 和 action 来计算新 state。

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 { createStore } from 'redux';

/**
* 这是一个 reducer 函数:接受当前 state 值和描述“发生了什么”的 action 对象,它返回一个新的 state 值。
* reducer 函数签名是 : (state, action) => newState
*
* Redux state 应该只包含普通的 JS 对象、数组和基本类型。
* 根状态值通常是一个对象。 重要的是,不应该改变 state 对象,而是在 state 发生变化时返回一个新对象。
*
* 你可以在 reducer 中使用任何条件逻辑。 在这个例子中,我们使用了 switch 语句,但这不是必需的。
*
*/
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case 'counter/incremented':
return { value: state.value + 1 };
case 'counter/decremented':
return { value: state.value - 1 };
default:
return state;
}
}

// 创建一个包含应用程序 state 的 Redux store。
// 它的 API 有 { subscribe, dispatch, getState }.
let store = createStore(counterReducer);

// 你可以使用 subscribe() 来更新 UI 以响应 state 的更改。
// 通常你会使用视图绑定库(例如 react-redux)而不是直接使用 subscribe()。
// 可能还有其他用例对 subscribe 也有帮助。

store.subscribe(() => console.log(store.getState()));

// 改变内部状态的唯一方法是 dispatch 一个 action。
// 这些 action 可以被序列化、记录或存储,然后再重放。
store.dispatch({ type: 'counter/incremented' });
// {value: 1}
store.dispatch({ type: 'counter/incremented' });
// {value: 2}
store.dispatch({ type: 'counter/decremented' });
// {value: 1}

Redux 的应用场景

  1. 同一个 state 需要在多个 Component 中共享,例如应用需要登录,登录后的用户信息可以存放于 store 中
  2. 需要操作一些全局性的常驻 Component,比如 Notifications,Tooltips 等
  3. 太多 props 需要在组件树中传递,但其中大部分只是为了透传给子组件
  4. 业务太复杂导致 Component 文件太大,可以考虑将业务逻辑拆出来放到 Reducer 中

不适用的场景:
使用 Redux 需要创建很多模版代码,会让 state 的更新变得非常繁琐,如果应用数据流向比较简单,可以不使用 Redux 。

Redux 数据流向

严格的单向数据流是 Redux 架构的设计核心。Redux 应用中数据的生命周期遵循下面 4 个步骤:

  1. View 中的某个操作调用 store.dispatch(action)
  2. Redux store 调用传入的 reducer 函数,入参为 当前的 state 和 action。
  3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  4. Redux store 保存了根 reducer 返回的完整 state 树。

Redux 数据流向

redux_inner

带 middleware 的 Redux 数据流向

redux_middleware_inner

Action: Action 是把数据从应用传到 store 的有效载荷,本质上是 JavaScript 普通对象。action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。
Reducer: Reducer 指定了应用状态的变化如何响应 action 并发送到 store 的。它是一个纯函数,接收旧的 state 和action,返回新的 state 。
State: State 记录了应用的状态。在 Redux 应用中,所有的 state 都被保存在一个单一对象中,即 store 中。
Store: Redux 应用只有一个单一的 store,它维持应用的 state,提供 getState() 方法获取 state;提供 dispatch(action) 方法更新 state;通过 subscribe(listener) 注册监听器;通过 subscribe(listener) 返回的函数注销监听器。

Redux 和 Flux 的区别

Flux

  • Flux 是用于构建用户界面的应用程序架构,通过单向数据流补充 React 可组合的视图组件
  • Flux 更像模式而非框架,没有任何硬依赖
  • Flux 架构的应用包含 4 部分
    • Action
      • 通过 Action creators 创建
      • 每个 Action 拥有 type 或类似属性
      • 传递给 Dispatcher
    • Dispatcher
      • 分发 Actions 给所有注册 Store 的回调函数
    • Store
      • 接受 Action 更新数据后,触发 change 事件,通知 View
      • 可以由多个 Store
    • View 视图组件,即 Controller-View
      • change 事件,从 Store 取回数据,将数据传递给子组件或更新组件状态
      • 响应用户输入,生成新的 Action

Redux

  • Redux 是 JavaScript 应用的可预测状态容器
  • Redux 对不同框架都有完整实现,Facebook 官方推荐使用代替 Flux
  • Redux 架构与 Flux 基本一致,但做了简化
    • State 只读,更改 State 的方式是返回新的对象,即引入 Reducer 纯函数
    • Action 与 Dispatcher ,只需返回包含 type 和 payload 属性的对象
    • Store
      • store 唯一
      • createStore 基于 Reducer 纯函数创建
      • store.dispatch() 调用 Action
    • View
      • 通过 store.getState() 获取最新状态
      • 通过 store.subscribe() 订阅状态更新
        • store.subscribe() 返回函数可取消订阅

综上,Redux 与 Flux 都基于单向数据流,架构相似,但 Redux 默认应用只有唯一 Store,精简掉 Dispatcher,引入 Reducer 纯函数,通过返回新对象,而不是更改对象,更新状态。

对比 Flux 的官方实现,Redux 的 API 更简洁,并且提供了如 combineReducers 等工具函数及 React-Toolkit 工具集,以及对状态的撤销、重做和持久化等更复杂的功能。提供如 React-Redux 等简化 Redux 与其他第三方库的连接。

Redux 和 Vuex 的区别

Vuex 和 Redux 的本质思想是一致的,均是将数据从视图中抽离的方案,通过单一的数据源 store 和可预测的数据变化反馈视图的改变。

  1. Redux 不仅仅局限于 React,还支持其它 UI 库;Vuex 和 Vue 深度绑定
  2. Vuex 定义了 state、getter、mutation、action 四个对象;Redux 定义了 store、reducer、action
  3. Vuex 事件触发方式 包括 commit 同步和 dispatch 异步;Redux 同步和异步都是用 dispatch
  4. Vuex 中 state 统一存放;Redux 中 store 依赖所有 reducer 的初始值
  5. Vuex 中有 getter 可以便捷的得到 state; React-redux 中 mapStateToProps 参数做了这个工作
  6. Vuex 中的 action 可以使用异步 ajax 请求;Redux 中的 action 仅支持发送数据对象,异步 ajax 需要使用 redux-thunk 或 redux-saga 等第三方 middleware
  7. Redux 中的是不可变数据,Vuex 中的数据是可变的。Redux 每次使用新的 store 替换旧的 store,Vuex 是直接修改 state
  8. Redux 在检测数据变化的时候,是要通过 diff 的方式进行比较的,而 Vuex 是通过 getter/setter 来比较的

Redux 和 MobX 的区别

  1. Redux 的编程范式是函数式;Mobx 是面向对象的
  2. Redux 中的数据是不可变对象,每次更新返回一个新的数据;MobX 中的数据从始至终都是同一份引用
  3. MobX 没有全局的状态树,状态分散在各个独立的 store 中;Redux 中的 store 是全局唯一的
  4. MobX 相对 Redux 来说会少些工程化模板
  5. MobX 中使用 async/await 或 flow 来处理异步逻辑;Redux 需要使用第三方 middleware

Redux 的核心原则

Redux 设计和使用遵循三个基本原则:

  • 单一数据源,Store 唯一
    整个应用程序的状态 State 存储在单一对象树 Object tree 中
    Object tree 只存在唯一的 Store 中
    单一对象树让跟踪状态的时间变化,调试和检查应用程度都更加容易
  • 状态是只读的
    Redux 假设开发者永远不会更改数据,而是在 Reducer 中返回新对象来更新状态
    更改状态的唯一方法是发出一个动作 Action,Action 是对已发生事情的抽象描述的对象
    数据变更,如用户输入和网络请求都不能直接更改状态
  • Reducer 是纯函数,用来归并状态 State
    接受原状态和 Action,返回新状态 reducer(state, action) => new State
    纯函数,无副作用,输出和输入一一对应,与执行上下文、时间、调用次数无关。不应在函数内请求 API,操作 DOM,使用 Date.now() 等时间耦合方法或随机值

React Context 和 Redux 的区别

ReactContext

  • React Context API 是为了解决跨组件层级传递 props 的效率问题
  • 试验性的 Context API 存在问题
    • 提供数据源的父组件和接收数据的子组件间的某个组件的 shouldComponentUpdate 返回 false 跳过更新,子组件也会被动跳过更新
  • ContextAPI 正式在 React16.3 引入,使用方法
    • 创建 context 对象实例: const MyContext = React.createContext(defaultValue)
      订阅 context 的变化: 使用 <MyContext.Provider value={/* 某个值 */}> 组件去声明想要传递的数据源
    • 消费数据
      ​高阶组件写法 <MyContext.Consumer>{vaule=> /* 基于 context 值进行渲染 */ }</Consumer>
      Hook写法 const value = useContext(MyContext)

Redux

  • Redux 是 JavaScript 应用的可预测状态容器
  • Redux 使用方法

    • 声明 reducer 函数
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      const reducer = (state, action) => {
      switch (action.type) {
      case 'INCREMENT':
      return state + 1;
      case 'DECREMENT':
      return state - 1;
      default:
      return state;
      }
      };
    • 创建 store 对象
      1
      2
      import { createStore } from 'redux';
      const store = createStore(reducer);
    • 使用 state dispatch
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      const MyComponent = ({ value, onIncrement, onDecrement }) => (
      <>
      {value}
      <button onClick={onIncrement}>+</button>
      <button onClick={onDecrement}>-</button>
      </>
      );
      const App = () => (
      <MyComponent
      value={store.getState()}
      onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
      onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
      />
      );
  • 目的

    • ReactContext 解决跨组件层级传递 props 的效率问题
    • Redux 是 JavaScript 应用的可预测状态容器,拥有完整的状态管理功能
  • 更新机制
    • ReactContext:Context 的 value 更新,它内部所有消费组件都会重新渲染,并且不受制于 shouldComponentUpdate 函数,需要手动优化
      • 避免使用对象字面量作为 value
      • 拆分 Context
      • 记忆化
      • 使用 createContext 的第二参数手动优化
    • Redux
      • 只有当前组件所消费的状态值改变,当前组件才会被更新
  • 调试
    • ReactContext 支持 ReactDevTools 调试
    • Redux 支持 Redux DevTools 调试
      • 可以方便地跟踪应用的状态何时、何处、为什么及如何改变
      • Redux 架构允许开发者记录更改,使用“时间旅行调试”
  • 中间件
    • ReactContext 不支持中间件
    • Redux 支持 applyMiddleware 将所有中间件组成一个数据,依次执行,最后执行 store.dispatch,依靠中间件扩展 Redux 功能,如简化异步操作等

React 访问 ReduxStore 的方法

  • connect:适合 React 组件访问 ReduxStore
1
2
3
4
5
import React from 'react'
import { connect } from 'react-redux'
const MyComponent = ({value}) => <>{value}</>
const mapStateToProps= ({value}) => ({value})
export connect(mapStateToProps)(MyComponent)
  • 导出 store:适合非服务端渲染
    • 创建 store 并导出
      1
      2
      3
      4
      import { createStore }from 'redux'
      import reducer from './reducer'
      conststore = createStore(reducer)
      export defaultstore
    • 引入 sotre 通过 getState 获取状态
      1
      2
      3
      4
      5
      import store from './store';
      export function get() {
      const state = store.getState();
      const { value } = state;
      }
  • 使用 redux-thunk 的第二参数 getState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducer';
const store = createStore(reducer, applyMiddleware(thunk));
const get = () => (dispatch, getState) => {
dispatch({ type: 'getStart' });
const { token } = getState();
fetch('/user/info', {
method: 'GET',
header: {
Authorization: `Bearer ${token}`
}
})
.then(res => res.json())
.then(json => {
dispatch({ type: 'getSuccess', payload: { user: json } });
});
};
store.dispatch(get());
  • 手写中间件 middleware,截获 action 的 payload,或者直接输出 getState 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducer';
const payload = null;
const myMiddleware = store => next => action => {
if (typeof action === 'function') {
return action(store);
} else {
payload = action.payload;
}
return next(action);
};
const store = createStore(reducer, applyMiddleware(myMiddleware));
store.dispatch(store => store.getState()); // 直接调用 store 的方法
store.dispatch({
type: 'start',
payload: 'payload'
});
console.log(payload); // payload

Redux 中异步请求数据时发送多 Action 方法

异步请求数据等异步操作通常要发出三种 Action

  • 操作发起时 Action,以 start 为例
  • 操作成功时 Action,以 success 为例
  • 操作失败时 Action,以 failure 为例

发送多 Action 方法:

  • mapDispatchToprops
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const mapDispatchToprops = dispatch => {
    return {
    asyncAction() {
    dispatch({ type: 'start' })
    fetch('/api').then(res => res.json).then(data => {
    dispatch({ type: 'success', payload: { data } })
    }).catch(error=> {
    dispatch({ type: 'failure', payload: { error } })
    }))
    }
    }
    }
  • redux-thunk,使 dispatch 支持传入函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { createStore, applyMiddleware } from 'redux'
    import thunk from 'redux-thunk'
    import reducer from './reducer'
    const store = createStore(reducer, applyMiddleware(thunk))
    const asyncAction = dispatch => {
    dispatch({ type: 'start' })
    fetch('/api').then(res => res.json).then(data => {
    dispatch({ type: 'success', payload: { data } })
    }).catch(error => {
    dispatch({ type: 'failure', payload: { error } })
    }))
    }
    store.dispatch(asyncAction())
  • redux-promise,使 dispatch 支持传入 Promise

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { createStore, applyMiddleware } from 'redux'
    import promiseMiddleware from 'redux-promise'
    import reducer from './reducer'
    const stroe = createStore(reducer, applyMiddleware(promiseMiddleware))
    const asyncAction = dispatch => fetch('/api').then(res =>res.json).then(data => {
    dispatch({ type: 'success', payload: { data } })
    }).catch(error => {
    dispatch({ type: 'failure', payload: { error } })
    }))
    store.dispatch({ type: 'start' })
    store.dispatch(asyncAction())
  • redux-saga,采用 Generator 生成器函数支持异步多 Action

    sagas.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
    function* asyncAction() {
    try {
    const data = yield call('/api');
    yield put({ type: 'success', payload: { data } });
    } catch (error) {
    yield put({ type: 'failure', payload: { error } });
    }
    }
    function* mySaga() {
    // 支持并发
    yield takeEvery('start', asyncAction);
    }
    function* mySage() {
    //不支持并发,前个处理中的相同 type 的 Action 会被取消
    yield takeLatest('start', asyncAction);
    }
    export default mySage;
    main.js
    1
    2
    3
    4
    5
    6
    7
    import { createStore, applyMiddleware } from 'redux';
    import createSagaMiddleware from 'react-saga';
    import reducer from './reducer';
    import mySaga from './sagas';
    const sagaMiddleware = createSagaMiddleware();
    conststore = createStore(reducer, applyMiddleware(sagaMiddleware));
    sagaMiddleware.run(mySaga);

如何判断项目需要引入 Redux

并非所有应用程序都需要 Redux,是否引入 Redux 由以下决定

  • 正在构建的应用程序类型
  • 需要解决的问题类型
  • 哪些工具可以更好地解决问题

Redux 可以共享和管理状态

  • 通过可预测的行为来帮助回答:状态何时、何处、为什么及如何改变
  • 增加概念、代码和限制,增加学习成本和项目复杂度

平衡利弊,在以下情况引入 Redux 最有用

  • 应用程序许多地方都需要状态
  • 应用程序的状态经常更新
  • 更新状态的逻辑比较复杂
  • 项目较大,需要比较多的人协作
  • 想查看状态何时、何处、为什么及如何改变

React Virtual DOM

  • Virtual DOM 是一种编程概念
    • UI 以一种理想化的,或者说“虚拟的”表现形式被保存在内存中
      • 支持可以优化的 Diff 算法
      • 避免多次调用 DOM 操作影响渲染无效的内容
    • 通过 ReactDOM 等类库与真实 DOM 同步,这一过程也被叫做协调(reconciliation)
      • 支持按照优先级更新,并行可中断的协调策略
      • 支持 ReactCanvas 和 ReactNative 等其他渲染方式,甚至非浏览器环境
  • Virtual DOM 赋予 React 声明式的 API
    • 告诉 React 希望让 UI 是什么状态,React 就确保 DOM 匹配该状态
    • 开发者不必关心属性操作、事件处理和手动 DOM 更新这些构建应用程序必要的操作
  • Virtual DOM 在 React 中是一种视图更新技术或设计模式,
    • Virtual DOM 通常与 React 元素关联,代表用户界面的对象
    • React 使用 fibers 内部对象来存放组件树的附加信息
    • React Fiber 是 React 中的协调引擎,主要目的是使 Virtual DOM 可以增量式渲染

React Hook

什么是 React Hook

Hook 是 React 16.8 的新增特性

  • 允许开发者在函数组件里使用 React state 及生命周期等特性的函数
    • React 内置如 useState、useEffect 等 Hook
  • Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
    • 非强制按照生命周期划分,避免每个生命周期包含不相关的逻辑
  • 开发者可以在不编写 class 的情况下使用 state 以及其他的 React 特性
    • 无需考虑 this,无需考虑函数和 class 组件的区别和应用场景
    • 便于使用 Prepack 试验 component folding,使代码更易于优化
    • 拥抱函数式编程
  • Hook 和现有代码可以同时工作,渐进式地使用

阅读更多

React组件的状态state与属性props

什么是 React 的状态(state)

  • React 的状态 state 是一个对象
    • 类组件中,状态通过 this.state 创建,通过 this.setState 合并更改,异步更新
    • React Hook 中,状态通过 this.useStatethis.useReducer 使用
  • React 将组件看做状态机,状态改变触发渲染
  • React 建议减少有状态的组件,提高组件复用度,利于维护
    • 只将无法从 props 传递,无法从其他数据计算,并且随时间可能变化的数据作为 state
    • 多个组件 state 的数据源相同,应将状态提升到父组件或容器组件
    • 避免使用 context,仅在 React 的状态管理无法满足需求时使用 Redux

阅读更多

React组件

什么是 React 组件

React 组件允许用户将 UI 拆分成独立可复用的代码片段,并对每个片段进行独立构思。React 组件从概念上类似于 JavaScript 函数,接受任意的入参 Props,返回用于描述页面展示内容的 React 元素。

阅读更多

对比Angular和React

  • 核心功能
    • React 核心库只提供构建 UI 组件的方法,其他功能通过社区提供
    • Angular 集成了 路由、异步请求、表单、模块化 CSS 等功能
  • 组件
    • React 组件推荐使用 JSX,可以一个文件包含 HTML、CSS 和 JS,也可以分开
    • Angular 组件 HTML、CSS 和 TS 分别是一个文件
  • DOM
    • React 基于 Virtual DOM,组件会被编译成 JS 对象,数据更改时通过 Diff 算法更新
    • Angular 基于 Incremental DOM,组件会被编译成指令,数据更改时就地更新。没有使用规定指令的组件可以被 Tree Shaking
  • 数据绑定
    • React 单向数据绑定,声明状态,更新视图
    • Angular 双向数据绑定,数据改变,更新视图
  • 全局状态管理
    • React 可以用全局对象或 Redux 实现
    • Angualr 可以用 Service 依赖注入实现
  • 上手成本
    • React 推荐了解 JSX,可以作为库函数渐进式使用
    • Angluar 需要了解 TypeScript,Rxjs,OOP 和装饰器等,推荐作为web应用的基础框架独立使用

对比Vue和React

Vue3 和 React16.8 引入了较大的变更,所以 Vue 和 React 的对比一般指 Vue2 和 React16.7 的对比。

相同点

  • 使用 Virtual DOM
  • 提供了响应式(Reactive)和组件化(Composable)的视图组件
  • 核心库与路由(react-router、vue-router)和状态管理(redux、vuex)分离
  • 支持 JSX,移动端都支持原生渲染
  • 提供了命令行工具(create-react-app、vue-cli)
  • 提供了跨端解决方案(React Native、weex)

不同点

  • 预编译
    • React 可以通过 Prepack 优化 JavaScript 源代码,在编译时执行原本在运行时的计算过程,通过简单的赋值序列提高 JavaScript 代码的执行效率,消除中间计算过程及分配对象操作。缓存 JavaScript 解析结果,优化效果最佳
    • Vue 可以静态分析 template,构造 AST 树,通过 PatchFlags 标记节点变化类型
  • 渲染
    • React
      • 通过 shouldComponentUpdate / setState,使用 PureCompoent 等对比前后状态和属性,手动决定是否渲染来优化
      • 推荐 jsx 语法,可扩展性好,可以渐进式应用
      • CSS in JS
    • Vue
      • 推荐 template 语法,自动追踪组件依赖,精确渲染状态改变的组件
      • 支持并且默认单文件组件,样式仍旧是 CSS 语法,迁移方便
  • 事件处理
    • React
      • 事件委托到 document,之后委托到 根节点
      • 所有事件被合并为合成事件并兼容不同浏览器
      • 事件处理函数中的 this 需要手动绑定或使用箭头函数声明
    • Vue
      • 支持原生事件
      • this 自动绑定执行上下文

React router速读速懂

React Router 是一个基于 React 之上的路由库,它可以让你向应用中快速地添加视图和数据流,同时保持页面与 URL 间的同步。

React Router 是建立在 history 库之上的。 history 监听浏览器地址栏的变化,解析 URL 并转化为 location 对象, 然后 router 使用它匹配到路由,最后正确地渲染对应的组件。

该文章写于 2021-11-29 10:56:40 目前 React-router 最新版本为 6.0.2,React-router 不同版本 API 相差较大,该文章以当前最新版本为标准。

阅读更多

前端技术选型

本文主要面向前端基础框架和库的选型,不包括构建工具、 UI 库和框架配套解决方案的选择。由于前端发展速度很快,框架层出不穷,特标明本文发布时间为 2021年6月6日,更新时间 2022年2月21日,更新时间 2022年3月14日,只适用于2021年前端基础框架技术选型。只选择了 web 应用开发时使用的框架,桌面端框架如 Electron、Tauri、Flutter,或者跨平台框架例如 React Native、Weex 或者众多的小程序框架本质上和如下列出的框架不是解决同一类问题所以不在对比之列。

阅读更多