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 来处理异步逻辑;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
      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
    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
    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 或者众多的小程序框架本质上和如下列出的框架不是解决同一类问题所以不在对比之列。

阅读更多