阅读视图

发现新文章,点击刷新页面。

Redux 食用指南

Redux 是一个强大的状态管理框架,被广泛用于管理应用程序的状态。它的设计理念是让状态的更新可预测和透明。本文将简要探讨 Redux 的核心机制和实际应用。

在 Redux 中,有一个状态对象负责应用程序的整个状态.Redux store 是应用程序状态的唯一真实来源

如果应用程序想要更新状态,只能通过 Redux store 执行,单向数据流可以更轻松地对应用程序中的状态进行监测管理。

Redux store 是一个保存和管理应用程序状态的 state,使用 Redux 对象中的 createStore() 来创建一个 redux store,此方法将 reducer 函数作为必需参数.

1
2
3
const reducer = (state = 5) => state;

const store = Redux.createStore(reducer);

获取数据

Redux store 对象提供了几种允许你与之交互的方法,可以使用 getState() 方法检索 Redux store 对象中保存的当前的 state

1
2
3
4
5
6
const store = Redux.createStore(
(state = 5) => state
);

// 更改此行下方的代码
const currentState = store.getState();

更新状态

由于 Redux 是一个状态管理框架,因此更新状态是其核心任务之一。在 Redux 中,所有状态更新都由 dispatch action 触发,action 只是一个 JavaScript 对象,其中包含有关已发生的 action 事件的信息。

Redux store 接收这些 action 对象,然后更新相应的状态。action 对象中必须要带有 type 属性,reducer 才能根据 type 进行区分处理。
action 除了 type 属性外,还可以附带数据给 reducer 做相应的处理,这个数据是可选的。

我们可以将 Redux action 视为信使,将有关应用程序中发生的事件信息提供给 Redux store,然后 store 根据发生的 action 进行状态的更新。

reducer

reducer 将 state 和 action 作为参数,并且它总是返回一个新的 state。这是 reducer 的唯一的作用,它不应有任何其他的作用。比如它不应调用 API 接口,也不应存在任何潜在的副作用。reducer 只是一个接受状态和动作,然后返回新状态的纯函数

在 reducer 中一般通过 switch 进行判断 action 的类型,做不同的处理。

订阅事件

store.subscribe() 可以订阅 store 的数据变化,它接收一个回调函数作为参数。当 store 数据更新时会调用该回调函数。

模块划分

当应用程序的状态开始变得越来越复杂时,将状态划分为多个部分可能是个更好的选择。我们可以考虑将不同的模块进行划分,Login 作为一个模块,Account 作为另一个模块。

但对 state 进行模块划分也不能破坏 redux 中将数据存入简单 state 的原则。因此可以生成多个 reducer, 再将它们合并到 root reducer 中。

redux 提供了 combineReducers() 函数对 reducer 进行合并。它接收一个对象作为参数,对象中的 key/value 别分对应着 module name 和相对应的 reducer 函数。

1
2
3
4
5
6
7

const rootReducer = Redux.combineReducers({
counter: counterReducer,
auth: authReducer
})

const store = Redux.createStore(rootReducer);

异步

redux 本身是不能直接处理异步操作,因此需要引入中间件来处理这些问题。在 createStore 时,还可以传入第二个可选参数,这个参数就是传递给 redux 的中间件函数。

Redux 提供了 applyMiddleware() 来创建一个中间件,一般处理 redux 异步的中间件有 redux-thunkredux-saga 等。

redux-thunk

redux-thunk 允许 action 创建函数返回一个函数而不是一个 action 对象。这个返回的函数接收 dispatchgetState 作为参数,允许直接进行异步操作和状态的分发。

例如,一个异步获取数据的 thunk 可能如下所示:

1
2
3
4
5
6
7
8
9
function fetchData() {
return (dispatch, getState) => {
// 异步操作
fetch('some-api-url')
.then(response => response.json())
.then(data => dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data }))
.catch(error => dispatch({ type: 'FETCH_DATA_ERROR', error }));
};
}

redux-saga

redux-saga 是一个更高级的中间件,它使用 ES6 的 Generator 函数来让你以同步的方式写异步代码。saga 监听发起的 action,并决定基于这些 action 执行哪些副作用(如异步获取数据、访问浏览器缓存等)。

一个简单的 saga 可能如下所示:

1
2
3
4
5
6
7
8
function* fetchDataSaga(action) {
try {
const data = yield call(fetch, 'some-api-url');
yield put({ type: 'FETCH_DATA_SUCCESS', payload: data });
} catch (error) {
yield put({ type: 'FETCH_DATA_ERROR', error });
}
}

React 与 Redux

在 React 应用中,Redux 被用来跨组件共享状态。使用 react-redux 库可以方便地将 Redux 集成到 React 应用中。

Provider 组件

Providerreact-redux 提供的一个组件,它使 Redux store 对 React 应用中的所有组件可用。通常,我们在应用的最顶层包裹 Provider 并传入 store:

1
2
3
4
5
6
7
8
import { Provider } from 'react-redux';
import { store } from './store';

const App = () => (
<Provider store={store}>
<MyRootComponent />
</Provider>
);

connect 函数

connect 是一个高阶函数,用于将 React 组件连接到 Redux store。它接受两个参数:mapStateToPropsmapDispatchToProps,分别用于从 store 中读取状态和向 store 发起 actions。

1
2
3
4
5
6
7
8
9
10
11
import { connect } from 'react-redux';

const mapStateToProps = state => ({
items: state.items
});

const mapDispatchToProps = dispatch => ({
fetchData: () => dispatch(fetchData())
});

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);

总结

Redux 提供了一种统一、可预测的方式来管理应用程序的状态。通过使用 actions, reducers 和 store,开发者可以以一种高度解耦的方式来管理状态和 UI。

当结合异步处理和 React 集成时,Redux 成为了一个强大的工具,能够提升大型应用程序的开发和维护效率。

React 知识回顾 (优化篇)

接下来对 React 性能相关的问题进行知识回顾。

完整目录概览

React 代码复用

  • Render Props
  • 高阶组件 (HOC)
  • 自定义 Hooks
  • Mixins (已被 React 废弃)

Render props

Render props 是一种在 React 组件之间共享代码的简单技术。具体的行为是:

  1. 子组件接收一个用于渲染指定视图的 prop 属性,该属性的类型是函数。
  2. 父组件在组件内部定义该函数后,将函数的引入传给子组件
  3. 子组件将组件内部 state 作为实参传给从外面传来的函数,并将函数的返回结果渲染在指定的视图区域。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 组件使用
<Mouse render={(x, y) => <span>x: {x}, y: {y}</span>} />

// 组件内部大致实现
class Mouse extends React.Component {
state = { x: 0, y: 0 };

render() {
return (
<section>
<header>头部信息</header>
<main>{this.props.render(this.state)}</main>
<footer>底部信息</footer>
</section>
);
}
}

准确来说 Render props 是一个用于告知组件需要渲染什么内容的函数属性。props 的命名可以由自己定义,比如用于在内容区域渲染的 prop 名可以叫 render,同时还可以再接收一个 renderHead 的 prop 用于渲染头部的信息。

高阶函数、高阶组件分别是什么?

高阶函数就是接收其它函数作为参数的函数就称之为高阶函数,像数组的 mapsortfilter 都是高阶函数。

高阶组件(Higher-order component, HOC) 是 React 用于复用组件逻辑的一种高级技巧。它具体的行为是:

函数接收一个组件作为参数,在函数体内定义一个新组件,新组件内编写可复用的逻辑并应用到参数组件中。最后再将新组件作为函数的返回值 return 出去。
redux 中的 connect 函数就是一个高阶组件。

React 性能优化

  1. 对比 props/state 新旧值的变化来决定是否渲染组件,参见:父组件在执行 render 时会不会触发子组件的 render 事件?如果会该怎么避免?
  2. 列表渲染时每项添加唯一的 key。参见:渲染列表为啥要用 key?
  3. 定时器、DOM 事件等在组件销毁时一同销毁,从而避免内存泄露。
  4. 代码分割,使用异步组件。
  5. Hooks 使用 useMemo 缓存上一次计算的结果,避免重复计算值。

父组件在执行 render 时会不会触发子组件的 render 事件?如果会该怎么避免?

如果父组件渲染后,子组件接收的 props 也跟着发生了改变,那么默认情况下会触发子组件的渲染。

若子组件接受的 props 没有发生改变,那就得判断子组件的状况。

如果子组件是继承于 Component 声明的组件,并且没有使用 shouldComponentUpdate 做避免重复渲染的处理,那么子组件会触发 render 事件。

为了避免重复渲染,类组件可以使用 shouldComponentUpdate 来决定是否进行渲染。也可以将继承于 Component 组件改为继承 PureComponment,该组件会浅对比 Props 是否进行改变,从而决定是否渲染组件。

如果是函数组件,可以通过 React.memo 来对函数组件进行缓存。

渲染列表为啥要用 key?

渲染列表时,如果不给列表子项传 key 的话,React 将默认使用 index 作为 key,同时会在控制台发出警告。

key 在兄弟节点之间必须唯一,要避免使用数组下标 index 作为 key。因为使用数组下标作为 `key 时,若数组的顺序发生了改变,将会影响 Diffing 算法的效率。

若列表的节点是组件的话,还可能会影响组件的 state 数据。因为组件实例是基于 key 来决定是否更新与复用。当顺序发生了变化,则 key 也会相应得被修改,从而导致子组件间的数据错乱。

React 使用的 Diffing 算法是通过 tagkey 判断是否是同一个元素(sameNode)。使用唯一的 key 有助于 React 识别哪些元素发生改变,如节点添加或删除。这样有助于减少渲染次数,从而优化性能。

如果数组中的数据没有唯一的 key,可以引入 shortid 预先给数组中每项数据生成唯一的 id

1
2
3
4
5
6
7
8
9
10
const shortid = require('shortid');

function addId(data) {
return {
...data,
id: shortid.generate(),
}
}

const newList = list.map(addId);

若确定没有列表的顺序不会发生变化同时没有其他唯一的 key 来标识列表项时才能使用数组的下标。

虚拟 dom 是如何提升性能的

当组件触发更新时,虚拟 DOM 通过 Diffing 算法比对新旧节点的变化以决定是否渲染 DOM 节点,从而减少渲染提升性能。因为修改真实 DOM 所耗费的性能远比操作 JavaScript 多几倍,因此使用虚拟 DOM 在渲染性能上会高效的多。

简述 React Diffing 算法

Diffing 算法(Diffing Algorithm) 会先比较两个根元素的变化:

  1. 节点类型变化时,将会卸载原有的树而建立新树。如父节点 <div> 标签被修改为 <section> 标签,则它们自身及 children 下的节点都会被重新渲染。
  2. DOM 节点类型相同时,保留相同的 DOM 节点,仅更新发生改变的属性。
  3. 组件类型相同时,组件更新时组件实例保持不变,React 将更新组件实例的 props, 并调用生命周期 componentWillReceiveProps()componentwillupdate(),最后再调用 render。若 render 中还有子组件,将递归触发 Diff。
  4. 列表节点发生变化,列表项没有设置 key 时, 那么 Diffing 算法会逐个对比节点的变化。如果是尾部新增节点,那 Diff 算法会 Diff 到列表末尾,仅新增元素即可,不会有其他的性能损耗。若新增的数据不在数组的尾部而是在中间,那么 Diffing 算法比较到中间时判断出节点发生变化,将会丢弃后面所有节点并重新渲染。
  5. 列表节点发生变化,列表项有设置 key 时, React 可以通过 key 来匹配新旧节点间的对应关系,可以很快完成 Diff 并避免重复渲染的问题。

异步组件怎么使用?

  1. 通过动态 import() 语法对组件代码进行分割。

  2. 使用 React.lazy 函数,结合 import() 语法引入动态组件。在组件首次渲染时,会自动导入包含 MyComponent 的包。

    1
    const MyComponent = React.lazy(() => import('./MyComponent'));
  3. React.Suspense 组件中渲染 lazy 组件,同时可以使用 fallback 做优雅降级(添加 loading 效果):

    1
    2
    3
    <React.Suspense fallback={<div>Loading...</div>}>
    <MyComponent />
    </React.Suspense>
  4. 封装一个错误捕获组件(比如组件命名为 MyErrorBoundary),组件内通过生命周期 getDerivedStateFromError 捕获错误信息。当异步组件加载失败时,将捕获到错误信息处理后给用户做错误提示功能。

    1
    2
    3
    4
    5
    <MyErrorBoundary>
    <React.Suspense fallback={<div>Loading...</div>}>
    <MyComponent />
    </React.Suspense>
    </MyErrorBoundary>

JSX 是如何编译为 js 代码的?

在 React v17 之前,JSX 会被编译为 React.createElement(component, props, ...children) 函数,执行会返回 vnodevnode 通过 patch 之类的方法渲染到页面。

React v17 之后更新了 JSX 转换规则。新的 JSX 转换不会将 JSX 转换为 React.createElement,而是自动从 React 的 package 中引入新的入口函数(react/jsx-runtime)并调用。这意味着我们不用在每个组件文件中显式引入 React

怎么对组件的参数做类型约束呢?

要对组件的参数做类型约束的话,可以引入 prop-types 来配置对应的 propTypes 属性。
FlowTypesScript 则可以对整个应用做类型检查。

React 知识回顾 (使用篇)

使用 React 进行项目开发也有好几个项目了,趁着最近有空来对 React 的知识做一个简单的复盘。

完整目录概览

React 是单向数据流还是双向数据流?它还有其他特点吗?

React 是单向数据流,数据是从上向下流。它的其他主要特点时:

  • 数据驱动视图
  • 声明式编写 UI
  • 组件化开发

setState

React 通过什么方式来更新数据

React 是通过 setState 来更新数据的。调用多个 setState 不会立即更新数据,而会批量延迟更新后再将数据合并。

除了 setState 外还可以使用 forceUpdate 跳过当前组件的 shouldComponentUpdate diff,强制触发组件渲染(避免使用该方式)。

React 不能直接修改 State 吗?

  1. 直接修改 state 不会触发组件的渲染。
  2. 若直接修改 state 引用的值,在实际使用时会导致错误的值出现
  3. 修改后的 state 可能会被后续调用的 setState 覆盖

setState 是同步还是异步的?

出于性能的考虑,React 可能会把多个 setState 合并成一个调用。

React 内有个 batchUpdate(批量更新) 的机制,在 React 可以控制的区域 (如组件生命周期、React 封装的事件处理器) 设置标识位 isBatchingUpdate 来决定是否触发更新。

比如在 React 中注册的 onClick 事件或是 componentDidMount 中直接使用 setState 都是异步的。若想拿到触发更新后的值,可以给 setState 第二个参数传递一个函数,该函数在数据更新后会触发的回调函数,函数的参数就是更新后最新的值。

不受 React 控制的代码快中使用 setState 是同步的,比如在 setTimeout 或是原生的事件监听器中使用。

setState 小测

输出以下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
componentDidMount() {
this.setState({ count: this.state.count + 1 });
console.log("1 -->", this.state.count);

this.setState({ count: this.state.count + 1 });
console.log("2 -->", this.state.count);

setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log("3 -->", this.state.count);
}, 0);

setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log("4 -->", this.state.count);
}, 0);
}

输出结果为:

1
2
3
4
1 --> 0
2 --> 0
3 --> 2
4 --> 3

解答: 调用 setState 后不会立即更新 state,开头两次调用会被异步合并调用,因此只有一次调用。一轮事件循环结束后,调用第 3、4 次 setState。由于在 setTimeout 中调用是同步更新的,因此都能正常的叠加数据。

React 生命周期

React 的生命周期主要是指组件在特定阶段会执行的函数。以下是 class 组件的部分生命周期图谱:

从上图可以看出:React 的生命周期按照类型划分,可分为 挂载时(Mounting)、更新时(Updating)、卸载时(Unmounting) 。图中的生命周期函数效果如下:

constructor (构造函数)

  • 触发条件: 组件初始化时
  • 是否可以使用 setState: X
  • 使用场景: 初始化 state 或者对方法绑定 this。在构造函数中便于自动化测试。

static getDerivedStateFromProps

Tips: 不常用方法

  • 触发条件: 调用 render 函数之前
  • 是否可以使用 setState: X
  • 函数行为: 函数可以返回一个对象用于更新组件内部的 state 数据,若返回 null 则什么都不更新。
  • 使用场景: 用于 state 依赖 props 的情况,也就是状态派生。值得注意的是派生 state 会导致代码冗余,并使组件难以维护。

shouldComponentUpdate

Tips: 不常用方法

  • 触发条件: 当 props/state 发生变化
  • 是否可以使用 setState: X
  • 函数行为: 函数的返回值决定组件是否触发 render,返回值为 true 则触发渲染,反之则阻止渲染。(组件内不写该函数的话,则调用默认函数。默认函数只会返回 true,即只要 props/state 发生变化,就更新组件)
  • 使用场景: 组件的性能优化,仅仅是浅比较 props 和 state 的变化的话,可以使用内置的 PureComponent 来代替 Component 组件。

render

  • 触发条件: 渲染组件时
  • 是否可以使用 setState: X
  • 函数行为: 函数的返回值决定视图的渲染效果
  • 使用场景: class 组件中唯一必须要实现的生命周期函数。

getSnapshotBeforeUpdate

Tips: 不常用方法

  • 触发条件: 在最近一次渲染输出(提交到 DOM 节点)之前调用
  • 是否可以使用 setState: X
  • 函数行为: 函数的返回值将传入给 componentDidUpdate 第三个参数中。若只实现了该函数,但没有使用 componentDidUpdate 的话,React 将会在控制台抛出警告
  • 使用场景: 可以在组件发生更改之前从 DOM 中捕获一些信息(例如,列表的滚动位置)

componentDidMount

  • 触发条件: 组件挂载后(插入 DOM 树中)立即调用,该函数只会被触发一次
  • 是否可以使用 setState: Y (可以直接调用,但会触发额外渲染)
  • 使用场景: 从网络请求中获取数据、订阅事件等

componentDidUpdate

  • 触发条件: 组件更新完毕后(首次渲染不会触发)
  • 是否可以使用 setState: Y (更新语句须放在条件语句中,不然可能会造成死循环)
  • 使用场景: 对比新旧值的变化,进而判断是否需要发送网络请求。比如监听路由的变化

componentWillUnmount

  • 触发条件: 组件卸载及销毁之前直接调用
  • 是否可以使用 setState: X
  • 使用场景: 清除 timer,取消网络请求或清除在 componentDidMount 中创建的订阅等

生命周期阶段

针对 React 生命周期中函数的调用顺序,笔者写了一个简易的 Demo 用于演示: React 生命周期示例

React 组件挂载阶段先后会触发 constuctorstatic getDerivedStateFromPropsrendercomponentDidMount 函数。若 render 函数内还有子组件存在的话,则会进一步递归:

1
2
3
4
5
6
7
8
9
10
[Parent]: constuctor
[Parent]: static getDerivedStateFromProps
[Parent]: render
[Children]: constuctor
[Children]: static getDerivedStateFromProps
[Children]: render
[Children]: componentDidMount
[Children]: 挂载阶段结束!
[Parent]: componentDidMount
[Parent]: 挂载阶段结束!

React 组件更新阶段主要是组件的 props 或 state 发生变化时触发。若组件内还有子组件,则子组件会判断是否也需要触发更新。默认情况下 component 组件是只要父组件发生了变化,子组件也会跟着变化。以下是更新父组件 state 数据时所触发的生命周期函数:

1
2
3
4
5
6
7
8
9
10
11
12
[Parent]: static getDerivedStateFromProps
[Parent]: shouldComponentUpdate
[Parent]: render
[Children]: static getDerivedStateFromProps
[Children]: shouldComponentUpdate
[Children]: render
[Children]: getSnapshotBeforeUpdate
[Parent]: getSnapshotBeforeUpdate
[Children]: componentDidUpdate
[Children]: 更新阶段结束!
[Parent]: componentDidUpdate
[Parent]: 更新阶段结束!

值得注意的是: 在本例 Demo 中没有给子组件传参,但子组件也触发了渲染。但从应用的角度上考虑,既然你子组件没有需要更新的东西,那就没有必要触发渲染吧?

因此 Component 组件上可以使用 shouldComponentUpdate 或者将 Component 组件替换为 PureComponment 组件来做优化。在生命周期图中也可以看到: shouldComponentUpdate 返回 false 时,将不再继续触发下面的函数。

有时你可能在某些情况下想主动触发渲染而又不被 shouldComponentUpdate 阻止渲染该怎么办呢?可以使用 force­Update() 跳过 shouldComponentUpdate 的 diff,进而渲染视图。(需要使用强制渲染的场景较少,一般不推荐这种方式进行开发)

React 组件销毁阶段也没啥好说的了。父组件先触发销毁前的函数,再逐层向下触发:

1
2
3
4
[Parent]: componentWillUnmount
[Parent]: 卸载阶段结束!
[Children]: componentWillUnmount
[Children]: 卸载阶段结束!

其他生命周期

除了上图比较常见的生命周期外,还有一些过时的 API 就没有额外介绍了。因为它们可能在未来的版本会被移除:

上图没有给出错误处理的情况,以下信息作为补充: 当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

React 组件通信

  1. 父组件通过 props 给子组件传递数据。子组件通过触发父组件提供的回调函数来给父组件传递消息或数据
  2. React.Context 可以跨层级组件共享数据
  3. 自定义事件
  4. 引入 Redux/Mobx 之类的状态管理器

React.Context 怎么使用

Context 可以共享对于组件树而言是全局的数据,比如全局主题、首选语言等。使用方式如下:

  1. React.createContext 函数用于生成 Context 对象。可以在创建时给 Context 设置默认值:

    1
    const ThemeContext = React.createContext('light');
  2. Context 对象中有一个 Provider(提供者) 组件,Provider 组件接受一个 value 属性用以将数据传递给消费组件。

    1
    2
    3
    <ThemeContext.Provider value="dark">
    <page />
    </ThemeContext.Provider>
  3. 获取 Context 提供的值可以通过 contextType 或者 Consumer(消费者) 组件中获取。contextType 只能用于类组件,并且只能挂载一个 Context

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyClass extends React.Component {
    componentDidMount() {
    let value = this.context;
    /* 在组件挂载完成后,使用 MyContext 的值执行一些有副作用的操作 */
    }
    render() {
    let value = this.context;
    /* 基于 MyContext 的值进行渲染 */
    }
    }
    MyClass.contextType = MyContext;

    若想给组件挂载多个 Context, 或者在函数组件内使用 Context 可以使用 Consumer 组件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <ThemeContext.Consumer>
    {theme => (
    <UserContext.Consumer>
    {user => (
    <ProfilePage user={user} theme={theme} />
    )}
    </UserContext.Consumer>
    )}
    </ThemeContext.Consumer>

Context 通常适用于传递较为简单的数据信息,若数据太过复杂,还是需要引入状态管理(Redux/Mbox)。

函数组件是什么?与类组件有什么区别?

函数组件本质上是一个纯函数,它接受 props 属性,最后返回 JSX。

与类组件的差别在于: 它没有实例、不能通过 extends 继承于其他方法、也没有生命周期和 state。以前函数组件常作为无状态组件,React 16.8+ 可以引入 Hooks 为函数组件支持状态和副作用操作。

Hooks

Hook vs class

类组件的不足:

  • 状态逻辑复用难,缺少复用机制。渲染属性和高阶组件导致层级冗余。
  • 组件趋向复杂难以维护。生命周期函数混杂不相干逻辑,相干逻辑分散在不同生命周期中。
  • this 指向令人困扰。内联函数过度创建新句柄,类成员函数不能保证 this。

Hooks 的优点:

  • 自定义 Hook 方便复用状态逻辑
  • 副作用的关注点分离
  • 函数组件没有 this 问题

Hooks 现有的不足:

  • 不能完全取代 class 组件的生命周期,部分不常用的生命周期暂时没有实现。
  • Hooks 的运作方式带来了一定的学习成本,需要转换现有的编程思维,增加了心智负担。

Hooks 的使用

描述 Hooks 有哪些常用的方法和大致用途

  1. useState: 使函数组件支持设置 state 数据,可用于代替类组件的 constructor 函数。

  2. useEffect: 使函数组件支持操作副作用 (effect) 的能力,Hook 第二个参数是 effect 的依赖项。当依赖项是空时,effect 函数仅会在组件挂载后执行一遍。若有一个或多个依赖项时,只要任意一个依赖项发生变化,就会触发 effect 函数的执行。effect 函数里可以做一些如获取页面数据、订阅事件等操作。

    除此之外,useEffect 还可以返回一个函数用于做清除操作,这个清除操作时可选的。常用于清理订阅事件、DOM 事件等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 绑定 DOM 事件
    useEffect(() => {
    document.addEventListener('click', handleClick);

    // useEffect 回调函数的返回值是函数的话,当组件卸载时会执行该函数
    // 若没有需要清除的东西,则可以忽略这一步骤
    return () => {
    document.removeEventListener('click', handleClick);
    };
    }, [handleClick]);
  3. useLayoutEffect: useEffect 的 effect 执行的时机是在浏览器完成布局和绘制之后会延迟调用。若想要 DOM 变更的同时同步执行 effect 的话可以使用 useLayoutEffect。它们之间只是执行的时机不同,其他都一样。

  4. useContext: 接收一个 Context 对象,并返回 Context 的当前值。相当于类组件的 static contextType = MyContext

  5. useReduceruseState 的代替方案,它的工作方式有点类似于 Redux,通过函数来操作 state。适合 state 逻辑较为复杂且包含多个子值,或是新的 state 依赖于旧的 state 的场景。

  6. useMemo 主要用于性能优化,它可以缓存变量的值,避免每次组件更新后都需要重复计算值。

  7. useCallbck 用于缓存函数,避免函数被重复创建,它是 useMemo 的语法糖。useCallback(fn, deps) 的效果相当于是 useMemo(() => fn, deps)

Hook 之间的一些差异

  1. React.memo 与 React.useMemo

    memo 针对一个组件的渲染是否重复执行,useMemo 定义一段函数逻辑是否重复执行。

  2. React.useMemo 与 React.useCallback

    useMemo(() => fn) 返回的是一个函数,将等同于 useCallback(fn)

  3. React.useStatus 与 React.useRef

    React.useStatus 相当于类的 stateReact.useRef 相当于类的内部属性。前者参与渲染,后者的修改不会触发渲染。

自定义 Hook 的使用

自定义 Hook 的命名规则是以 use 开头的函数,比如 useLocalStorage 就符合自定义 Hook 的命名规范。
使用自定义 Hook 的场景有很多,如表单处理、动画、订阅声明、定时器等等可复用的逻辑都能通过自定义 Hook 来抽象实现。

在自定义 Hook 中,可以使用 Hooks 函数将可复用的逻辑和功能提取出来,并将内部的 state 或操作的方法从自定义 Hook 函数中返回出来。函数组件使用时就可以像调用普通函数一祥调用自定义 Hook 函数, 并将自定义 Hook 返回的 state 和操作方法通过解构保存到变量中。

下面是 useLocalStorage 的实现,它将 state 同步到本地存储,以使其在页面刷新后保持不变。 用法与 useState 相似,不同之处在于我们传入了本地存储键,以便我们可以在页面加载时默认为该值,而不是指定的初始值。

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

// Usage
function App() {
// Similar to useState but first arg is key to the value in local storage.
const [name, setName] = useLocalStorage('name', 'Bob');

return (
<div>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
);
}

// Hook
function useLocalStorage(key, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});

// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = value => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};

return [storedValue, setValue];
}

注意: 自定义 Hook 函数在定义时,也可以使用另一个自定义 Hook 函数。

Hook 使用约束

  1. 只能在函数组件最顶层调用 Hook,不能在循环、条件判断或子函数中调用。
  2. 只能在函数组件或者是自定义 Hook 函数中调用,普通的 js 函数不能使用。

class 组件与 Hook 之间的映射与转换

函数组件相比 class 组件会缺少很多功能,但大多可以通过 Hook 的方式来实现。

生命周期

  • constructor:class 组件的构造函数一般是用于初始化 state 数据或是给事件绑定 this 指向的。函数组件内没有 this 指向的问题,因此可以忽略。而 state 可以通过 useState/useReducer 来实现。

  • getDerivedStateFromPropsgetDerivedStateFromProps 一般用于在组件 props 发生变化时派生 state。Hooks 实现同等效果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function ScrollView({row}) {
    const [isScrollingDown, setIsScrollingDown] = useState(false);
    const [prevRow, setPrevRow] = useState(null);

    if (row !== prevRow) {
    // Row 自上次渲染以来发生过改变。更新 isScrollingDown。
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
    }

    return `Scrolling down: ${isScrollingDown}`;
    }
  • shouldComponentUpdate: 使用 React.memo 应用到函数组件中后,当 props 发生变化时,会对 props 的新旧值进行前对比,相当于是 PureComponent 的功能。如果你还想自己定义比较函数的话,可以给 React.memo 的第二个参数传一个函数,若函数返回 true 则跳过更新。

    1
    2
    3
    const Button = React.memo((props) => {
    return <button>{props.text}</button>
    });
  • render: 函数组件本身就是一个 render 函数。

  • componentDidMount / componentDidUpdate / componentWillUnmount:

    useEffect 第二个参数的依赖项为空时,相当于 componentDidMount,组件挂载后只会执行一次。每个 useEffect 返回的函数相当于是 componentWillUnmount 同等效果的操作。若有依赖,则 effect 函数相当于是 componentDidUpdate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 没有依赖项,仅执行一次
    useEffect(() => {
    const subscription = props.source.subscribe();

    // 相当于 componentWillUnmount
    return () => {
    subscription.unsubscribe();
    };
    }, []);

    // 若有依赖项,相当于 componentDidUpdate
    // 当 page 发生变化时会触发 effect 函数
    useEffect(() => {
    fetchList({ page });
    }, [page]);

Hooks 没有实现的生命周期钩子

  • getSnapshotBeforeUpdate
  • getDerivedStateFromError
  • componentDidCatch

转换实例变量

使用 useRef 设置可变数据。

强制更新 Hook 组件

设置一个没有实际作用state,然后强制更新 state 的值触发渲染。

1
2
3
4
5
6
7
8
9
10
const Todo = () => {
// 使用 useState,用随机数据更新也行
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

function handleClick() {
forceUpdate();
}

return <button click={handleClick}>强制更新组件</button>
}

获取旧的 props 和 state

可以通过 useRef 来保存数据,因为渲染时不会覆盖掉可变数据。

1
2
3
4
5
6
7
8
9
10
11
12
function Counter() {
const [count, setCount] = useState(0);

const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, []);

const prevCount = prevCountRef.current;

return <h1>Now: {count}, before: {prevCount}</h1>;
}

受控组件与非受控组件的区别

受控组件主要是指表单的值受到 state 的控制,它需要自行监听 onChange 事件来更新 state

由于受控组件每次都要编写事件处理器才能更新 state 数据、可能会有点麻烦,React 提供另一种代替方案是非受控组件

非受控组件将真实数据储存在 DOM 节点中,它可以为表单项设置默认值,不需要手动更新数据。当需要用到表单数据时再通过 ref 从 DOM 节点中取出数据即可。

注意: 多数情况下React 推荐编写受控组件。

扩展资料: 受控和非受控制使用场景的选择

Portals 是什么?

Portals 就像个传送门,它可以将子节点渲染到存在于父组件以外的 DOM 节点的方案。

比如 Dialog 是一个全局组件,按照传统渲染组件的方式,Dialog 可能会受到其容器 css 的影响。因此可以使用 Portals 让组件在视觉上渲染到 <body> 中,使其样式不受 overflow: hiddenz-index 的影响。

React vs Vue

在项目架构时选择合适的前端框架是至关重要的。React 和 Vue 都是流行的选择,但它们在灵活性、易用性和性能方面各有特点。本文旨在深入比较这两个框架,让我们在开发前选择技术架构有个参考。

React

React 在处理复杂业务时显示出较高的灵活性。它提供多样的技术方案选择,适用于需要高度自定义的场景。React 的特点包括:

  • 组件名称需要以大写字母开头。
  • 使用 JSX 语法,组件内需要包裹一个元素,可以使用Fragment作为占位符。
  • 响应式设计,主要关注数据。
  • 事件绑定采用驼峰命名方式。
  • 不允许直接修改 state,以保持性能。
  • 构造函数中接受参数。
  • 单向数据流,专注于视图层和数据渲染。
  • 有助于自动化测试。
  • state 或 props 改变时,render 函数会重新执行。
  • 使用虚拟 DOM 来减少真实 DOM 操作,提升性能。
  • 跨端应用实现,例如 React Native。

但它的缺点也很明显:

  • 学习曲线较陡: JSX 和组件生命周期等概念对新手而言可能较难掌握。
  • 只关注视图层: 需要与其他库结合使用以构建完整的解决方案。但 react 的生态非常丰富,甚至会有多种不同的变成风格,社区中没有一个统一认可的解决方案,这会让不熟悉 react 生态的新用户看的眼花缭乱。

Vue

Vue 提供了丰富的 API,使功能实现变得简单。它适合于快速开发和较少复杂度的项目。Vue 的特点包括:

  • 易学性,提供了详尽的文档和指导。尤其作者是国人,也提供了友好的中文文档支持。
  • 更简洁的模板语法糖,如 v-bind 和 v-model。
  • 详细的错误提示和开发工具,使调试更加方便。
  • 数据双向绑定,简化了表单输入和数据展示。
  • 更轻量级,适合小型到中型项目。
  • 提供了过渡效果和动画的集成支持。
  • 可以更方便地集成到现有的项目中。
  • 提供了类似于 React 的虚拟 DOM 和组件系统。
  • 相比 react 生态的复杂, vue 官方提供了整套最基础的 web 开发架构所需的生态。当官方的提供的库无法满足需求后可以允许你去用其他第三方库,相当于起步阶段减少了选择的烦恼。对新手会比较友好。

然而,Vue 也有它的局限性:

  • 规模限制: 对于非常大型和复杂的应用,Vue 可能不如 React 灵活。
  • 过度依赖单文件组件: 可能导致项目结构和组织方式较为单一

总结

React 和 Vue 各有所长,选择哪一个取决于特定项目的需求、开发团队的技能和偏好。React 更适合需要高度灵活和可扩展性的大型应用,而 Vue 在快速开发和简单性方面表现更佳,适合新手和中小型项目。理解每个框架的优缺点有助于做出更合适的选择。

两年以后,与React道别

Photo by Alex Kubsch on Unsplash 雪猫社从来都少不了折腾。 先是雪先生要求加的表情包FacePack,然后又为了精确统计浏览量连了GA桑的API。之后,因为小图片不能放大看,又加了点击放大图片(Sakurairo里面更喜欢叫灯箱)的simple-img-modal,还为了显示EXIF加了EXIF读取功能。而驱动这些的,自然是前端脚本。 最开始雪猫社的这些附加组件都使用了React作为框架,现在回头看,这并不是一个很匹配我们的使用场景的选择,最直接地体现在React框架给我们带来的重达100kb的额外脚本上。这个大小的脚本会严重地拖慢我们的脚本解析速度,带来性能影响。但其实这也是当时无奈的折中之选:与React 16.x同期的Vue 2.6.x 包大小也要90多近100KB,虽然比React可能小20/30k左右,

来源

❌