这里有一份简洁的前端知识体系等待你查收,看看吧,会有惊喜哦~如果觉得不错,麻烦star哈~
设计维度
目标
为了解决用户的问题,技术本身要达成什么目标。这层定义“做到什么”。
命令式编程VS声明式编程
命令式编程
命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
声明式编程
告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
在React中,每个组件通过render函数返回“这个组件应该长得什么样”,而不去描述“怎么样去让这个组件长成这个样子”。
声明式编程的好处
- 让开发者的工作简化了
- 减少了重复工作
- 留下了改进的空间:比如React Fiber,虽然算法改头换面,但是组件却几乎不用改,因为组件只操心“显示什么”而不操心“如何显示”啊,当然不受影响了。
- 提供了全局协调能力:在React的未来,每个组件还是只声明“想要画成什么样子”,但React却可以改进协调算法,让React组件根据不同优先级来渲染,提高用户感知性能,但是React组件的代码不需要改变
react的核心理念之一就是函数式编程。
JSX
在JS中写HTML标记,这体现了高内聚。要达到这种效果,就必须依赖JSX。
JSX 的本质不是模板引擎,而是动态创建组件的语法糖,它允许我们在JS代码中直接写HTML标记。最终生成的代码就是React.CreateElement。
如果在 JSX 中往 DOM 元素中传入自定义属性,React 是不会渲染的。如果要使用 HTML 自定义属性,要使用 data- 前缀,这与 HTML 标准也是一致的。然而,在自定义标签中任意的属性都是被支持的,以 aria- 开头的网络无障碍属性同样可以正常使用。
JSX的优点
- 直观:声明式创建界面
- 灵活:代码动态创建界面
- 易上手:无需学习新的模板语言
约定
- 自定义组件以大写字母开头
- react 认为小写的 tag 是原生 DOM 节点,如 div
- JSX标记可以直接使用属性语法,例如
<menu.Item />
实现原理
为了达到设计目标,该技术采用了什么原理和机制。实现原理层回答“怎么做到”的问题。把实现原理弄懂,并且讲清楚,是技术人员的基本功。
生命周期函数
生命周期函数指的是在某一个时刻组件会自动调用执行的函数。

也可以参考网上的这张图。
一些注意点:
不管是挂载阶段还是更新阶段,都要到render时才能获取到更新后的this.state。在componentWillMount、 componentWillReceiveProps、 shouldComponentUpdate 和 componentWillUpdate 中也还是无法获取到更新后的 this.state。
mountComponent 本质上是通过递归渲染内容的,由于递归的特性,父组件的 componentWillMount 在其子组件的 componentWillMount 之前调用,而父组件的 componentDidMount 在其子组件的 componentDidMount 之后调用。updateComponent同理。
updateComponent 负责管理生命周期中的 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate。在 componentWillReceiveProps 中调用 setState,是不会触发 re-render 的,而是会进行 state 合并。禁止在 shouldComponentUpdate 和 componentWillUpdate 中调用 setState,这会造成循环调用,直至耗光浏览器内存后崩溃。
在 componentWillUnmount 中调用 setState,是不会触发 re-render 的。
无状态组件只是一个 render 方法,并没有组件类的实例化过程,也没有实例返回。无状态组件没有状态,没有生命周期,只是简单地接受 props 渲染生成 DOM 结构,是一个纯粹为渲染而生的组件。
这里简单介绍下各个生命周期函数:
constructor
- 用于初始化内部状态,很少使用
- 唯一可以直接修改 state 的地方
getDerivedStateFromProps
- 当 state 需要从 props 初始化时使用
- 尽量不要使用:维护两者状态一致性会增加复杂度
- 每次 render 都会调用
- 典型场景:表单控件获取默认值
componentDidMount
- UI 渲染完成后调用
- 只执行一次
- 典型场景:获取外部资源
componentWillUnmount
- 组件移除时被调用
- 典型场景:资源释放
getSnapshotBeforeUpdate
- 在元素被渲染并写入 DOM 之前调用,这样,你在 DOM 更新前捕获 DOM 信息(例如:滚动位置)。
- 在页面 render t前调用,state 已更新
- 典型场景:获取 render 之前的 DOM 状态
componentDididUpdate
- 每次 UI 更新时被调用
- 典型场景:页面需要根据 props 变化重新获取数据
shouldComponentUpdate
- 决定 VDOM 是否要重绘
- 一般可以由 PureComponent 自动实现
- 典型场景:性能优化
componentWillReceiveProps
注意下update阶段,触发组件update有两种情况,props或者state的修改。
可以看到,这两种情况生命周期函数是有重合的。唯一的不同就是props改变时,会先调用 componentWillReceiveProps
- 一个组件要从父组件接受参数
- 如果这个组件第一次存在于父组件中,不会执行
- 如果这个组件之前已经存在于父组件中,才会执行
MV* 与 Flux
MVC/MVVM
MVC/MVVM 简称 MC* 模式,其中 MVVM 是从 MVC 演进而来的。
MVC 是一种架构设计模式,它通过关注数据界面分离,来鼓励改进应用程序结构。具体地 说,MVC 强制将业务数据(Model)与用户界面(View)隔离,用控制器(Controller)管理逻 辑和用户输入。
Model 负责保存应用数据,和后端交互同步应用数据,或校验数据。
View 是 Model 的可视化表示,表示当前状态的视图。
Controller负责连接 View 和 Model,Model 的任何改变会应用到 View 中,View 的操作会通过 Controller应用到 Model 中。
Controller 管理了应用程序中 Model 和 View 之间的逻辑和协调。
MVC 的致命缺点:混乱的数据流动方式,此外,前端 MVC 模式的实现各有各的理解,千奇百怪。

MVVM 出现于 2005 年,最大变化在于 VM(ViewModel)代替了 C(Controller)。其关键“改 进”是数据绑定(DataBinding),也就是说,View 的数据状态发生变化可以直接影响 VM,反之 亦然。

Flux 的解决方案
Flux 的核心思想就是数据和逻辑永远单向流动

Flux 核心思想,也就是中心化控制。中心化控制让所有的请求与改变都只能通过 action 发出,统一 由 dispatcher 来分配。这样View就可以保持高度简洁,发生问题时也便于定位。比起 MVC 架构下数据或逻 辑的改动可能来自多个完全不同的源头,Flux 架构追查问题的复杂度和困难度显然要小得多。
Flux 的不足:冗余代码过多,每个应用中都需要手动创建一个 dispatcher 的示例,这还是让很多开发者觉得烦恼
如果非要把Flux 和MVC 做一个结构对比,那么, Flux 的Dispatcher 相当于MVC 的Controller, Flux 的Store 相当于MVC 的Model, Flux 的View 当然就对应MVC 的View了,至于多出来的这个Action ,可以理解为对应给MVC 框架的用户请求
Redux
Redux 是一个可预测的状态容器。简单地说,在摒弃了传统 MVC 的发布/订阅模式并通过 Redux 三大原则强化对状态 的修改后,使用 Redux 可以让你的应用状态管理变得可预测、可追溯。
redux的相关知识繁多,还包含了Mobx、dva,为此我将他抽离出来,请看这里。
render的执行
- 当组件的state和props发生改变时,render函数就会重新执行
- 父组件render函数被执行时,它的子组件的render函数都将被重新运行一次
applyMiddleWare
applyMiddleWare 的实现:
- 拿到原生的store跟dispatch
- 对dispatch做了一层扩展
- 将原生store中的dispatch覆盖掉
为什么需要VDOM?
如果没有VDOM,state改变,如何渲染页面?
最原始的做法:
- state 数据
- JSX 模板
- 数据 + 模板 结合,生成真实DOM,来显示
- state发生改变
- 数据 + 模板 结合,生成真实DOM,替换原来的DOM
这样做的缺陷:
- 第一次生成了完整的DOM片段
- 第二次生成了完整的DOM片段
- 第二次的DOM替换第一次的DOM,非常耗性能
改进的做法:
- state 数据
- JSX 模板
- 数据 + 模板 结合,生成真实DOM,来显示
- state发生改变
- 数据 + 模板 结合,生成真实DOM,不直接替换原来的DOM
- 新的DOM(DocumentFragment) 和 原始的DOM 做对比,找差异
- 只替换有变动的DOM元素
这样做的缺陷:性能提升不明显,因为对比DOM也消耗了性能
react的做法
- state 数据
- JSX 模板
- 数据 + 模板结合,生成VDOM(VDOM就是一个JS对象,用他来描述真实DOM)
- 用VDOM,生成真实DOM,来显示
- state发生改变
- 生成新的VDOM (极大提升性能)
- 比较原始VDOM和新的VDOM的区别 (极大提升性能)
- 只替换有变动的DOM元素
优点:
- 性能提升了
- 方便与其他平台集成,跨端应用得以实现
VDOM原理
JSX的运行基础就是VDOM。
VDOM的运行机制是广度优先分层比较。
VDOM 的两个假设:
- 组件的DOM结构是相对稳定的(很少发生跨层移动的场景)
- 类型相同的兄弟节点可以被唯一标识
事件系统
VDOM 在内存中是以对象的形式存在的,如果想要在这些对象上添加事件,就会非常简单。
React 基于 VDOM 实现了一个 SyntheticEvent (合成事件)层,我们所定义的事件处理器会接收到一个 SyntheticEvent 对象的实例,它完全符合 W3C 标准,不会存在任何 IE 标准的兼容性问题。并且与原生的浏览器事件一样拥有同样的接口,同样支持事件的冒泡机制,我们可以使用 stopPropagation() 和 preventDefault() 来中断它。所有事件都自动绑定到最外层上。如果需要访问原生事件对象,可以使用 nativeEvent 属性。
合成事件的实现机制
在 React 底层,主要对合成事件做了两件事:事件委派和自动绑定
事件委派
react 并不会把事件处理函数直接绑定到真实的节点上,而是把所有事件绑定到结构的最外层,使用一个统一的事件监听器。
事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。
当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象
当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。
这样做简化了事件处理和回收机制,效率也有很大提升。
自动绑定
在 React 组件中,每个方法的上下文都会指向该组件的实例,即自动绑定 this 为当前组件。 而且 React 还会对这种引用进行缓存,以达到 CPU 和内存的最优化。在使用 ES6 classes 或者纯函数时,这种自动绑定就不复存在了,我们需要手动实现 this 的绑定。
常见的绑定方法有:
- 双冒号语法:
<button onClick={::this.handleClick}>Test</button>
- 构造器内使用bind绑定
- 箭头函数
合成事件与原生事件对比
事件对象
原生 DOM 事件对象在 W3C 标准和 IE 标准下存在着差异。在低版本的 IE 浏览器中,只能使用 window.event 来获取事件对象。
而在 React 合成事件系统中,不存在这种兼容性问题,在事件处理函数中可以得到一个合成事件对象。
事件类型
React 合成事件的事件类型是 JS 原生事件类型的一个子集
事件传播与阻止事件传播
事件传播分为捕获阶段、目标阶段、冒泡阶段。
事件捕获在程序开发中的意义不大,还有兼容性问题。所以,React 的合成事件则并没有实现事件捕获,仅仅支持了事件冒泡机制。
阻止事件传播:阻止原生事件传播需要使用 e.preventDefault(),不过对于不支持该方法的浏览器(IE9 以 下),只能使用 e.cancelBubble = true 来阻止。而在 React 合成事件中,只需要使用 e.preventDefault() 即可。
事件绑定方式
原生事件有三种方式:
- 直接在DOM元素中绑定:
<button onclick="alert(1)">Test</button>
- 在JS中,通过为元素的事件属性赋值的方式实现绑定:
el.onclick = e => {console.log(e)}
- 通过事件监听函数来实现绑定:
el.addEventListener("click", ()=>{},false); el.attachEvent("onclick", ()=>{})
React 合成事件的绑定方式则简单得多:<button onClick={this.handleClick}>Test</button>
获取真实DOM的方式
要获取真实的DOM节点有两种方式,一种是通过e.target,一种是ref。
但能不使用ref尽量不用
注意事项
一、原生事件
componentDidMount 会在组件已经完成安装并且在浏览器中存在真实的 DOM 后调用,此时我们就可以完成原生事件的绑定。
在 React 中使用 DOM 原生事件时,一定要在组件卸载时手动移除,否则很 可能出现内存泄漏的问题。而使用合成事件系统时则不需要,因为 React 内部已经帮你妥善地处理了。
二、合成事件与原生事件混用
尽量避免在 React 中混用合成事件和原生 DOM 事件。
阻止 React 事件冒泡的行为只能用于 React 合成事件系统 中,且没办法阻止原生事件的冒泡。反之,在原生事件中的阻止冒泡行为,却可以阻止 React 合成事件的传播。
React 的合成事件系统只是原生 DOM 事件系统的一个子集。它仅仅实现了 DOM Level 3 的事件接口,并且统一了浏览器间的兼容问题。有些事件 React 并没有实现,或者受某些限制没办法去实现,比如 window 的 resize 事件。
优劣局限
每种技术实现,都有其局限性,在某些条件下能最大化的发挥效能,缺少了某些条件则暴露出其缺陷。优劣局限层回答“做得怎么样”的问题。对技术优劣局限的把握,更有利于应用时总结最佳实践,是分析各种“坑”的基础。
演进趋势
技术是在迭代改进和不断淘汰的。了解技术的前生后世,分清技术不变的本质,和变化的脉络,以及与其他技术的共生关系,能体现你对技术发展趋势的关注和思考。这层体现“未来如何”。
拥抱异步渲染
react v16.0.0 引入了叫 Fiber 这个全新的架构。这个架构使得 React 用异步渲染成为可能,但要注意,这个改变只是让异步渲染(async rendering)成为“可能”,React 却并没有在 v16 发布的时候立刻开启这种“可能”,也就是说,React 在 v16 发布之后依然使用的是同步渲染。
不过,虽然异步渲染没有立刻采用,Fiber 架构还是打开了通向新世界的大门,React v16 一系列新功能几乎都是基于 Fiber 架构。
要面向 React 未来,我们首先要理解这个异步渲染的概念。
同步渲染的问题
长期以来,React 一直用的是同步渲染,这样对 React 实现非常直观方便,但是会带来性能问题。
当要渲染的组件树非常庞大,JS的单线程遇到react的同步渲染,结果就是同步渲染霸占 JS 唯一的线程,其他的操作什么都做不了,在这 1 秒钟内,如果用户要点击什么按钮,或者在某个输入框里面按键,都不会看到立即的界面反应,这也就是俗话说的“卡顿”。
在同步渲染下,要解决“卡顿”的问题,只能是尽量缩小组件树的大小,以此缩短渲染时间,但是,应用的规模总是在增大的,不是说缩小就能缩小的,虽然我们利用定义 shouldComponentUpdate 的方法可以减少不必要的渲染,但是这也无法从根本上解决大量同步渲染带来的“卡顿”问题。
异步渲染:两阶段渲染
React Fiber 引入了异步渲染,有了异步渲染之后,React 组件的渲染过程是分时间片的,不是一口气从头到尾把子组件全部渲染完,而是每个时间片渲染一点,然后每个时间片的间隔都可去看看有没有更紧急的任务(比如用户按键),如果有,就去处理紧急任务,如果没有那就继续照常渲染。
根据 React Fiber 的设计,一个组件的渲染被分为两个阶段:第一个阶段(也叫做 render 阶段)是可以被 React 打断的,一旦被打断,这阶段所做的所有事情都被废弃,当 React 处理完紧急的事情回来,依然会重新渲染这个组件,这时候第一阶段的工作会重做一遍;第二个阶段叫做 commit 阶段,一旦开始就不能中断,也就是说第二个阶段的工作会稳稳当当地做到这个组件的渲染结束。
两个阶段的分界点,就是 render 函数。render 函数之前的所有生命周期函数(包括 render)都属于第一阶段,之后的都属于第二阶段。
开启异步渲染,虽然我们获得了更好的感知性能,但是考虑到第一阶段的的生命周期函数可能会被重复调用,不得不对历史代码做一些调整。
在 React v16.3 之前,render 之前的生命周期函数(也就是第一阶段生命周期函数)包括这些:
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- componentWillMount
- render
React 官方告诫开发者,虽然目前所有的代码都可以照常使用,但是未来版本中会废弃掉,为了将来,使用 React 的程序应该快点去掉这些在第一阶段生命函数中有副作用的功能。
一个典型的错误用例,也是我被问到做多的问题之一:为什么不在 componentWillMount 里去做AJAX?componentWillMount 可是比 componentDidMount 更早调用啊,更早调用意味着更早返回结果,那样性能不是更高吗?
首先,一个组件的 componentWillMount 比 componentDidMount 也早调用不了几微秒,性能没啥提高;而且,等到异步渲染开启的时候,componentWillMount 就可能被中途打断,中断之后渲染又要重做一遍,想一想,在 componentWillMount 中做 AJAX 调用,代码里看到只有调用一次,但是实际上可能调用 N 多次,这明显不合适。相反,若把 AJAX 放在 componentDidMount,因为 componentDidMount 在第二阶段,所以绝对不会多次重复调用,这才是 AJAX 合适的位置
getDerivedStateFromProps
到了 React v16.3,React 干脆引入了一个新的生命周期函数 getDerivedStateFromProps,这个生命周期函数是一个 static 函数,在里面根本不能通过 this 访问到当前组件,输入只能通过参数,对组件渲染的影响只能通过返回值。没错,getDerivedStateFromProps 应该是一个纯函数,React 就是通过要求这种纯函数,强制开发者们必须适应异步渲染。
static getDerivedStateFromProps(nextProps, prevState) {
//根据nextProps和prevState计算出预期的状态改变,返回结果会被送给setState
}
React v16发布时,还增加了异常处理的生命周期函数。
如果异常发生在第一阶段(render阶段),React就会调用getDerivedStateFromError
,如果异常发生在第二阶段(commit阶段),React会调用componentDidCatch
。这个区别也体现出两个阶段的区分对待。
适应异步渲染的组件原则
当 React 开启异步渲染的时候,你的代码应该做到在 render 之前最多只能这些函数被调用:
- 构造函数
- getDerivedStateFromProps
- shouldComponentUpdate
幸存的这些第一阶段函数,除了构造函数,其余两个全都必须是纯函数,也就是不应该做任何有副作用的操作。
Suspense带来的异步操作革命
Suspense 应用的场合就是异步数据处理,最常见的例子,就是通过 AJAX 从服务器获取数据,每一个 React 开发者都曾为这个问题纠结。
如果用一句话概括 Suspense 的功用,那就是:用同步的代码来实现异步操作。
React 同步操作的不足
React 最初的设计,整个渲染过程都是同步的。同步的意思是,当一个组件开始渲染之后,就必须一口气渲染完,不能中断,对于特别庞大的组件树,这个渲染过程会很耗时,而且,这种同步处理,也会导致我们的代码比较麻烦。
当我们开始渲染某个组件的时候,假设这个组件需要从服务器获取数据,那么,要么由这个组件的父组件想办法拿到服务器的数据,然后通过 props 传递进来,要么就要靠这个组件自力更生来获取数据,但是,没有办法通过一次渲染完成这个过程,因为渲染过程是同步的,不可能让 React 等待这个组件调用 AJAX 获取数据之后再继续渲染。
常用的做法,需要组件的 render 和 componentDidMount 函数配合。
- 在 componentDidMount 中使用 AJAX,在 AJAX 成功之后,通过 setState 修改自身状态,这会引发一次新的渲染过程。
- 在 render 函数中,如果 state 中没有需要的数据,就什么都不渲染或者渲染一个“正在装载”之类提示;如果 state 中已经有需要的数据,就可以正常渲染了,但这也必定是在 componentDidMount 修改了 state 之后,也就是只有在第二次渲染过程中才可以。
下面是代码实例:
class Foo extends React.Component {
state = {
data: null,
};
render () {
if (!this.state.data) {
return null;
} else {
return <div>this.state.data</div>;
}
}
componentDidMount () {
callAPI ().then (result => {
this.setState ({data: result});
});
}
}
这种方式虽然可行,我们也照这种套路写过不少代码,但它的缺点也是很明显的。
- 组件必须要有自己的 state 和 componentDidMount 函数实现,也就不可能做成纯函数形式的组件。
- 需要两次渲染过程,第一次是 mount 引发的渲染,由 componentDidMount 触发 AJAX 然后修改 state,然后第二次渲染才真的渲染出内容。
- 代码啰嗦,十分啰嗦。
理想中的代码形式
而 Suspense 就是为了克服上述 React 的缺点。
在了解 Suspense 怎么解决这些问题之前,我们不妨自己想象一下,如果要利用 AJAX 获取数据,代码怎样写最简洁高效?
我先来说一说自己设想的最佳代码形式。首先,我不想写一个有状态的组件,因为通过 AJAX 获取的数据往往也就在渲染用一次,没必要存在 state 里;其次,想要使数据拿来就用,不需要经过 componentDidMount 走一圈。所以,代码最好是下面这样:
const Foo = () => {
const data = callAPI ();
return <div>{data}</div>;
};
够简洁吧,可是目前的 React 版本做不到啊!
因为 callAPI 肯定是一个异步操作,不可能获得同步数据,无法在同步的 React 渲染过程中立足。
不过,现在做不到,不代表将来做不到,将来 React 会支持这样的代码形式,这也就是 Suspense。
有了Suspense,我们可以这样写代码:
const Foo = () => {
const data = createFetcher (callAJAX).read ();
return <div>{data}</div>;
};
接下来,我们就介绍一下 Suspense 的原理。
在 React 推出 v16 的时候,就增加了一个新生命周期函数 componentDidCatch。如果某个组件定义了 componentDidCatch,那么这个组件中所有的子组件在渲染过程中抛出异常时,这个 componentDidCatch 函数就会被调用。
可以这么设想,componentDidCatch 就是 JS 语法中的 catch,而对应的 try 覆盖所有的子组件,就像下面这样:
try {
//渲染子组件
} catch (error) {
// componentDidCatch被调用
}
Suspense 就是巧妙利用 componentDidCatch 来实现同步形式的异步处理。
Suspense 提供的 createFetcher 函数会封装异步操作,当尝试从 createFetcher 返回的结果读取数据时,有两种可能:一种是数据已经就绪,那就直接返回结果;还有一种可能是异步操作还没有结束,数据没有就绪,这时候 createFetcher 会抛出一个“异常”。
你可能会说,抛出异常,渲染过程不就中断了吗?
的确会中断,不过,createFetcher 抛出的这个“异常”比较特殊,这个“异常”实际上是一个 Promise 对象,这个 Promise 对象代表的就是异步操作,操作结束时,也是数据准备好的时候。当 componentDidCatch 捕获这个 Promise 类型的“异常”时,就可以根据这个 Promise 对象的状态改变来重新渲染对应组件,第二次渲染,肯定就能够成功。
下面是 createFetcher 的一个简单实现方式:
var NO_RESULT = {};
export const createFetcher = task => {
let result = NO_RESULT;
return () => {
const p = task ();
p.then (res => {
result = res;
});
if (result === NO_RESULT) {
throw p;
}
return result;
};
};
在上面的代码中,createFetcher 的参数 task 被调用应该返回一个 Promise 对象,这个对象在第一次调用时会被 throw 出去,但是,只要这个对象完结,那么 result 就有实际的值,不会再被 throw。
还需要一个和 createFetcher 配合的 Suspense,代码如下:
class Suspense extends React.Component {
state = {
pending: false,
};
componentDidCatch (error) {
// easy way to detect Promise type
if (typeof error.then === 'function') {
this.setState ({pending: true});
error.then (() =>
this.setState ({
pending: false,
})
);
}
}
render () {
return this.state.pending ? null : this.props.children;
}
}
上面的 Suspense 组件实现了 componentDidCatch,如果捕获的 error 是 Promise 类型,那就说明子组件用 createFetcher 获取异步数据了,就会等到它完结之后重设 state,引发一次新的渲染过程,因为 createFetcher 中会记录异步返回的结果,新的渲染就不会抛出异常了。
使用 createFetcher 和 Suspense 的示例代码如下:
const getName = () =>
new Promise (resolve => {
setTimeout (() => {
resolve ('Morgan');
}, 1000);
});
const fetcher = createFetcher (getName);
const Greeting = () => {
return <div>Hello {fetcher ()}</div>;
};
const SuspenseDemo = () => {
return (
<Suspense>
<Greeting />
</Suspense>
);
};
上面的 getName 利用 setTimeout 模拟了异步 AJAX 获取数据,第一次渲染 Greeting 组件时,会有 Promise 类型的异常抛出,被 Suspense 捕获。1 秒钟之后,当 getName 返回实际结果的时候,Suspense 会引发重新渲染,这一次 Greeting 会显示出 hello Morgan。
上面的 createFetcher 和 Suspense 是一个非常简陋的实现,主要用来让读者了解 Suspense 的工作原理,正式发布的 Suspense 肯定会具备更强大的功能。
Suspense 带来的 React 使用模式改变
Suspense 被推出之后,可以极大地减少异步操作代码的复杂度。
之前,只要有 AJAX 这样的异步操作,就必须要用两次渲染来显示 AJAX 结果,这就需要用组件的 state 来存储 AJAX 的结果,用 state 又意味着要把组件实现为一个 class。总之,我们需要做这些:
- 实现一个 class;
- class 中需要有 state;
- 需要实现 componentDidMount 函数;
- render 必须要根据 this.state 来渲染不同内容。
有了 Suspense 之后,不需要做上面这些杂事,只要一个函数形式组件就足够了。
在介绍 Redux 时,我们提到过在 Suspense 面前,Redux 的一切异步操作方案都显得繁琐,读者现在应该能够通过代码理解这一点了。
很可惜,目前 Suspense 还不支持服务器端渲染,当 Suspense 支持服务器端渲染的时候,那就真的会对 React 社区带来革命性影响。
函数化的 Hooks
Hooks 的目的,简而言之就是让开发者不需要再用 class 来实现组件。
useState
Hooks 会提供一个叫 useState 的方法,它开启了一扇新的定义 state 的门,对应 Counter 的代码可以这么写:
import {useState} from 'react';
const Counter = () => {
const [count, setCount] = useState (0);
return (
<div>
<div>{count}</div>
<button onClick={() => setCount (count + 1)}>+</button>
<button onClick={() => setCount (count - 1)}>-</button>
</div>
);
};
注意看,Counter 拥有自己的“状态”,但它只是一个函数,不是 class。
useState 只接受一个参数,也就是 state 的初始值,它返回一个只有两个元素的数组,第一个元素就是 state 的值,第二个元素是更新 state 的函数。
这个例子中,我们可以利用 count 可以读取到这个 state,利用 setCount 可以更新这个 state。
因为 useState 在 Counter 这个函数体中,每次 Counter 被渲染的时候,这个 useState 调用都会被执行,useState 自己肯定不是一个纯函数,因为它要区分第一次调用(组件被 mount 时)和后续调用(重复渲染时),只有第一次才用得上参数的初始值,而后续的调用就返回“记住”的 state 值。
读者看到这里,心里可能会有这样的疑问:如果组件中多次使用 useState 怎么办?React 如何“记住”哪个状态对应哪个变量?
React 是完全根据 useState 的调用顺序来“记住”状态归属的,假设组件代码如下:
const Counter = () => {
const [count, setCount] = useState (0);
const [foo, updateFoo] = useState ('foo');
// ...
};
每一次 Counter 被渲染,都是第一次 useState 调用获得 count 和 setCount,第二次 useState 调用获得 foo 和 updateFoo。
React 不知道你把 useState 等 Hooks API 返回的结果赋值给什么变量,但是它也不需要知道,它只需要按照 useState 调用顺序记录就好了。
正因为这个原因,Hooks,千万不要在 if 语句或者 for 循环语句中使用!
像下面的代码,肯定会出乱子的:
const Counter = () => {
const [count, setCount] = useState (0);
if (count % 2 === 0) {
const [foo, updateFoo] = useState ('foo');
}
const [bar, updateBar] = useState ('bar');
};
因为条件判断,让每次渲染中 useState 的调用次序不一致了,于是 React 就错乱了。
useEffect
除了 useState,React 还提供 useEffect,用于支持组件中增加副作用的支持。
在 React 组件生命周期中如果要做有副作用的操作,代码放在哪里?
当然是放在 componentDidMount 或者 componentDidUpdate 里,但是这意味着组件必须是一个 class。
在 Counter 组件,如果我们想要在用户点击“+”或者“-”按钮之后把计数值体现在网页标题上,这就是一个修改 DOM 的副作用操作,所以必须把 Counter 写成 class,而且添加下面的代码:
componentDidMount () {
document.title = `Count: ${this.state.count}`;
}
componentDidUpdate () {
document.title = `Count: ${this.state.count}`;
}
而有了 useEffect,我们就不用写一个 class 了,对应代码如下:
import {useState, useEffect} from 'react';
const Counter = () => {
const [count, setCount] = useState (0);
useEffect (() => {
document.title = `Count: ${count}`;
});
return (
<div>
<div>{count}</div>
<button onClick={() => setCount (count + 1)}>+</button>
<button onClick={() => setCount (count - 1)}>-</button>
</div>
);
};
useEffect 的参数是一个函数,组件每次渲染之后,都会调用这个函数参数,这样就达到了 componentDidMount 和 componentDidUpdate 一样的效果。
虽然本质上,依然是 componentDidMount 和 componentDidUpdate 两个生命周期被调用,但是现在我们关心的不是 mount 或者 update 过程,而是“after render”事件,useEffect 就是告诉组件在“渲染完”之后做点什么事。
读者可能会问,现在把 componentDidMount 和 componentDidUpdate 混在了一起,那假如某个场景下我只在 mount 时做事但 update 不做事,用 useEffect 不就不行了吗?
其实,用一点小技巧就可以解决。useEffect 还支持第二个可选参数,只有同一 useEffect 的两次调用第二个参数不同时,第一个函数参数才会被调用,所以,如果想模拟 componentDidMount,只需要这样写:
useEffect(() => {
// 这里只有mount时才被调用,相当于componentDidMount
}, [123]);
在上面的代码中,useEffect 的第二个参数是 [123],其实也可以是任何一个常数,因为它永远不变,所以 useEffect 只在 mount 时调用第一个函数参数一次,达到了 componentDidMount 一样的效果。
useContext
Context API的设计不完美,在多个 Context 嵌套的时候尤其麻烦。
比如,一段 JSX 如果既依赖于 ThemeContext 又依赖于 LanguageContext,那么按照 React Context API 应该这么写:
<ThemeContext.Consumer>
{theme => (
<LanguageContext.Cosumer>
language => {
//可以使用theme和lanugage了
}
</LanguageContext.Cosumer>
)}
</ThemeContext.Consumer>;
因为 Context API 要用 render props,所以用两个 Context 就要用两次 render props,也就用了两个函数嵌套,这样的缩格看起来也的确过分了一点点。
使用 Hooks 的 useContext,上面的代码可以缩略为下面这样:
const theme = useContext(ThemeContext);
const language = useContext(LanguageContext);
// 这里就可以用theme和language了
这个useContext把一个需要很费劲才能理解的 Context API 使用大大简化,不需要理解render props,直接一个函数调用就搞定。
但是,useContext也并不是完美的,它会造成意想不到的重新渲染,我们看一个完整的使用useContext的组件。
const ThemedPage = () => {
const theme = useContext (ThemeContext);
return (
<div>
<Header color={theme.color} />
<Content color={theme.color} />
<Footer color={theme.color} />
</div>
);
};
因为这个组件ThemedPage使用了useContext,它很自然成为了Context的一个消费者,所以,只要Context的值发生了变化,ThemedPage就会被重新渲染,这很自然,因为不重新渲染也就没办法重新获得theme值,但现在有一个大问题,对于ThemedPage来说,实际上只依赖于theme中的color属性,如果只是theme中的size发生了变化但是color属性没有变化,ThemedPage依然会被重新渲染,当然,我们通过给Header、Content和Footer这些组件添加shouldComponentUpdate实现可以减少没有必要的重新渲染,但是上一层的ThemedPage中的JSX重新渲染是躲不过去了。
说到底,useContext需要一种表达方式告诉React:“我没有改变,重用上次内容好了。”
希望Hooks正式发布的时候能够弥补这一缺陷。
Hooks 带来的代码模式改变
Hooks 将大大简化使用 React 的代码。
我们可能不再需要 class了,只用函数形式来编写组件。
对于 useContext,它并没有为消除 class 做贡献,却为消除 render props 模式做了贡献
对了,所有的 Hooks API 都只能在函数类型组件中调用,class 类型的组件不能用,从这点看,很显然,class 类型组件将会走向消亡。