[TOC]
# Hook
简单来说 Hook 拥抱了函数式编程,Fiber 架构从底层优化了 React 的性能。
使用 Hook
```js
import React, { useState, useEffect } from 'react';
// 自定义hook
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null); // 将 setState 的统一操作抽离为一个个的函数
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// useEffect 简化了事件监听器的添加与移除的书写
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
// 使用自定义hook
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
```
不使用 Hook
```js
class XXX extend Component {
state = {
isOnline: false
}
handleStatusChange (xxx) {
this.setState({
isOnline: xxx
})
}
componentDidMount =()=> {
ChatAPI.subscribeToFriendStatus(friendID, this.handleStatusChange);
}
componentWillUnmount = () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, this.handleStatusChange);
}
render () {
XXXXXXX
}
}
```
## state hook(让函数组件拥有 state)
```js
import React, { useState } from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
```
等价的 Class 示例:
```js
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
```
`const [count, setCount] = useState(0);`
这条语句到底做了哪些事情?
我们声明了一个叫`count`的 state 变量,然后把它设为`0`。React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用`setCount`来更新当前的`count`。
下面具体分析下`useState()`方法
1、调用 useState 方法的时候做了什么?它定义一个 “state 变量”。我们的变量叫`count`, 这是一种在函数调用时保存变量的方式 ——`useState`是一种新方法,它与 class 里面的`this.state`提供的功能完全相同。一般来说,在函数退出后变量就就会”消失”,而 state 中的变量会被 React 保留。
2、`useState`需要哪些参数?`useState()`方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了`0`作为变量的初始 state。(如果我们想要在 state 中存储两个不同的变量,只需调用`useState()`两次即可。)
3、`useState`方法的返回值是什么?返回值为:当前 state 以及更新 state 的函数。这就是我们写`const [count, setCount] = useState()`的原因。这与 class 里面`this.state.count`和`this.setState`类似,唯一区别就是你需要成对的获取它们。
### 读取与更新 state
读取 state:
当我们想在 class 中显示当前的 count,我们读取`this.state.count`:
~~~
<p>You clicked {this.state.count} times</p>
~~~
在函数中,我们可以直接用`count`:
~~~
<p>You clicked {count} times</p>
~~~
更新 state:
在 class 中,我们需要调用`this.setState()`来更新`count`值:
~~~
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
~~~
在函数中,我们已经有了`setCount`和`count`变量,所以我们不需要`this`:
~~~
<button onClick={() => setCount(count + 1)}>
Click me
</button>
~~~
## Effect Hook(让函数组件拥有生命周期)
你可以把`useEffect`Hook 看做`componentDidMount`,`componentDidUpdate`和`componentWillUnmount`这三个函数的组合。
<br/>
*Effect Hook* 可以让你在函数组件中执行副作用操作,在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的,所以对应的 Effect Hook 也分为无需清除的 effect 和需要清除的 effect。
<br/>
默认情况下,effect 将在每轮渲染结束后执行,但也可以选择让其在只有某些值改变的时候才执行。
### 无需清除的 effect
有时候,我们只想**在 React 更新 DOM 之后运行一些额外的代码**。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。
```js
// 使用 class
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
// 我们基本上都希望在 React 更新 DOM 之后才执行我们的操作
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
```
在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。
这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。
```js
// 使用 hook
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。
// React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。
useEffect(() => {
document.title = `You clicked ${count} times`; // 可以直接访问 count state 变量或其他 prop,它们保存在函数作用域中
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
```
### 需要清除的 effect
之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如**订阅外部数据源**。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。(常见的如手动添加事件处理程序,需要在组件销毁之前移除)
```js
// 使用 class
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
// 与 componentDidMount 逻辑相对应的,我们必须这样拆分代码~~~
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
```
### effect 的条件执行
默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。
<br/>
然而,在某些场景下这么做可能会矫枉过正。比如,在上一章节的订阅示例中,我们不需要在每次组件更新时都创建新的订阅,而是仅需要在`source`prop 改变时重新创建。
<br/>
要实现这一点,可以给`useEffect`传递第二个参数,它是 effect 所依赖的值数组。更新后的示例如下:
```
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
```
此时,只有当`props.source`改变后才会重新创建订阅。
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组(`[]`)作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循输入数组的工作方式。
[关于依赖列表是否为空的注意事项](https://react.docschina.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies)
## 自定义 Hook
[https://zh-hans.reactjs.org/docs/hooks-custom.html#gatsby-focus-wrapper](https://zh-hans.reactjs.org/docs/hooks-custom.html#gatsby-focus-wrapper)
- 将组件逻辑提取到可重用的函数中(使组件可以共享某一重复的逻辑)
例如,将一个获取鼠标位置的状态逻辑写成自定义 Hook:
```
import React, { useState, useEffect } from 'react'
// 自定义 Hook 是一个函数,其名称必须以 "use" 开头(约定)
const useMousePosition = () => {
const [ positions, setPositions ] = useState({x: 0, y: 0})
useEffect(() => {
const updateMouse = (event) => {
setPositions({ x: event.clientX, y: event.clientY })
}
document.addEventListener('mousemove', updateMouse)
return () => {
document.removeEventListener('mousemove', updateMouse)
}
})
return positions
}
export default useMousePosition
```
在组件中使用自定义 Hook 也很简单:
```
function App() {
const position = useMousePosition()
return (
<div className="App">
<header className="App-header">
<h1>{position.x}</h1>
</header>
</div>
)
}
```
在两个组件中使用相同的 Hook 不会共享 state。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
# Fiber
## Fiber 架构解决了什么问题
在页面元素很多,且需要频繁刷新的场景下,React 15 会出现掉帧的现象。请看以下例子:
![](https://img.kancloud.cn/9a/f4/9af4c676d8771189740c052dac66c489_550x280.gif =400x)
其根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用`setState`更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI。整个过程是一气呵成,不能被打断的。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。
针对这一问题,React 团队从框架层面对 web 页面的运行机制做了优化,得到很好的效果。
![](https://img.kancloud.cn/70/6c/706ceaa233acb9d0d3b713ccf7948da7_550x280.gif =400x)
## 实现浅析
React 框架内部的运作可以分为 3 层:
* Virtual DOM 层,描述页面长什么样。
* Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。
* Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。
这次改动最大的当属 Reconciler 层了,React 团队也给它起了个新的名字,叫`Fiber Reconciler`。这就引入另一个关键词:Fiber。
先看一下`stack-reconciler`下的 React 是怎么工作的。代码中创建(或更新)一些元素, React 会根据这些元素创建(或更新)Virtual DOM,然后 React 根据更新前后 Virtual DOM 的区别,去修改真正的 DOM。注意,**在 stack reconciler 下,DOM 的更新是同步的,也就是说,在 Virtual DOM 的比对过程中,发现一个 Instance 有更新,会立即执行 DOM 操作**。
![](https://img.kancloud.cn/9d/d9/9dd907076cd23c005f57ae1a1aad358e_1133x639.png =400x)
而`Fiber Reconciler`下,操作是可以分成很多小部分,并且可以被中断的,所以同步操作 DOM 可能会导致 fiber-tree 与实际 DOM 的不同步。对于每个节点来说,其不光存储了对应元素的基本信息,还要保存一些用于任务调度的信息。因此,fiber 仅仅是一个对象,表征 reconciliation 阶段所能拆分的最小工作单元,和上图中的 react instance一一对应。通过`stateNode`属性管理 Instance 自身的特性。通过`child`和`sibling`表征当前工作单元的下一个工作单元,`return`表示处理完成后返回结果所要合并的目标,通常指向父节点。整个结构是一个链表树。每个工作单元(fiber)执行完成后,都会查看是否还继续拥有主线程时间片,如果有继续下一个,如果没有则先处理其他高优先级事务,等主线程空闲下来继续执行。
Fiber 就是一种数据结构,它可以用一个纯 JS 对象来表示:
```js
const fiber = {
stateNode: {}, // 管理 Instance 自身的特性
child: {}, // 表征当前工作单元的下一工作单元
sibling: {}, // 表征当前工作单元的下一工作单元
return: {}, // 表示处理完成后返回结果所要合并的目标,通常指向父节点
}
```
### 举个例子
当前页面包含一个列表,通过该列表渲染出一个 button 和一组 Item,Item 中包含一个 div,其中的内容为数字。通过点击 button,可以使列表中的所有数字进行平方。另外有一个按钮,点击可以调节字体大小。
![](https://img.kancloud.cn/b6/7d/b67d7f9ffd717781b31aea6a99ed89b7_678x673.png =200x)
页面渲染完成后,就会初始化生成一个`fiber-tree`,这一过程与初始化 Virtual DOM Tree 类似。
![](https://img.kancloud.cn/70/7a/707ad710a5ea7ee428f4738ad9df1821_451x817.png =250x)
同时,React 还会维护一个`workInProgressTree`,`workInProgressTree`用于计算更新,完成 reconciliation 过程。
![](https://img.kancloud.cn/b3/1f/b31f5c759e308e6b1706c1ab80855f47_653x844.png =300x)
用户点击平方按钮后,利用各个元素平方后的 list 调用 setState,React 会把当前的更新送入 list 组件对应的`update queue`中。但是 React 并不会立即执行对比并修改 DOM 的操作。而是交给 scheduler 去处理。
scheduler 会根据当前主线程的使用情况去处理这次 update。为了实现这种特性,使用了`requestIdelCallback`API。对于不支持这个 API 的浏览器,react 会加上 pollyfill。
总的来讲,通常,客户端线程执行任务时会以帧的形式划分,大部分设备控制在 30-60 帧是不会影响用户体验;在两个执行帧之间,主线程通常会有一小段空闲时间,`requestIdleCallback`可以在这个 **空闲期(Idle Period)** 调用 **空闲期回调(Idle Callback)**,执行一些任务
![](https://img.kancloud.cn/32/5d/325d0263851b3180b058effdee674b37_737x139.png)
1、低优先级任务由`requestIdleCallback`处理;
2、高优先级任务,如动画相关的由`requestAnimationFrame`处理;
3、`requestIdleCallback`可以在多个空闲期调用空闲期回调,执行任务;
4、`requestIdleCallback`方法提供 deadline,即任务执行限制时间,以切分任务,避免长时间执行,阻塞 UI 渲染而导致掉帧;
![](https://img.kancloud.cn/6d/4e/6d4e202604ed0584204e6c5a48ef3ceb_1048x786.png =450x)
整个过程,简单来说,先通过`requestIdleCallback`获得可用的时间片,然后检查节点的`update queue`看是否需要更新,每处理完一个节点都会检查时间片是否用完,如果没用完,根据其保存的下一个工作单元的信息处理下一个节点。详细过程见第四个参考链接。
# 参考资料
[React Hook 探究](https://www.jianshu.com/p/d6e2bd342476)
[官方文档](https://react.docschina.org/docs/hooks-reference.html)
[React Fiber 原理](https://segmentfault.com/a/1190000018250127?utm_source=tag-newest)
[https://juejin.im/post/5ab7b3a2f265da2378403e57#heading-2](https://juejin.im/post/5ab7b3a2f265da2378403e57#heading-2)
[https://usehooks.com/](https://usehooks.com/)
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直线、矩形、多边形
- Part2-曲线图形
- Part3-线条操作
- Part4-文本操作
- Part5-图像操作
- Part6-变形操作
- Part7-像素操作
- Part8-渐变与阴影
- Part9-路径与状态
- Part10-物理动画
- Part11-边界检测
- Part12-碰撞检测
- Part13-用户交互
- Part14-高级动画
- CSS
- SCSS
- codePen
- 速查表
- 面试题
- 《CSS Secrets》
- SVG
- 移动端适配
- 滤镜(filter)的使用
- JS
- 基础概念
- 作用域、作用域链、闭包
- this
- 原型与继承
- 数组、字符串、Map、Set方法整理
- 垃圾回收机制
- DOM
- BOM
- 事件循环
- 严格模式
- 正则表达式
- ES6部分
- 设计模式
- AJAX
- 模块化
- 读冴羽博客笔记
- 第一部分总结-深入JS系列
- 第二部分总结-专题系列
- 第三部分总结-ES6系列
- 网络请求中的数据类型
- 事件
- 表单
- 函数式编程
- Tips
- JS-Coding
- Framework
- Vue
- 书写规范
- 基础
- vue-router & vuex
- 深入浅出 Vue
- 响应式原理及其他
- new Vue 发生了什么
- 组件化
- 编译流程
- Vue Router
- Vuex
- 前端路由的简单实现
- React
- 基础
- 书写规范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 与 Hook
- 《深入浅出React和Redux》笔记
- 前半部分
- 后半部分
- react-transition-group
- Vue 与 React 的对比
- 工程化与架构
- Hybird
- React Native
- 新手上路
- 内置组件
- 常用插件
- 问题记录
- Echarts
- 基础
- Electron
- 序言
- 配置 Electron 开发环境 & 基础概念
- React + TypeScript 仿 Antd
- TypeScript 基础
- 样式设计
- 组件测试
- 图标解决方案
- Algorithm
- 排序算法及常见问题
- 剑指 offer
- 动态规划
- DataStruct
- 概述
- 树
- 链表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 课程实战记录
- 服务器
- 操作系统基础知识
- Linux
- Nginx
- redis
- node.js
- 基础及原生模块
- express框架
- node.js操作数据库
- 《深入浅出 node.js》笔记
- 前半部分
- 后半部分
- 数据库
- SQL
- 面试题收集
- 智力题
- 面试题精选1
- 面试题精选2
- 问答篇
- Other
- markdown 书写
- Git
- LaTex 常用命令