学习目标
- 基础概念,重点是Hooks
- 官方文档导读
- 组件性能和组件开发检查表
- 知识扩展
- 参考文献
基础概念
React框架
- React框架只是一个View框架
- 页面展示
- 目标:展示后台JSON格式的View Model数据
- JSON数据是树形结构,和组件树之间存在对应关系,参见React理念
- 基本操作:获取后台数据,绑定到组件上
Virtual DOM
- 解决问题:用户操作或后台数据推送导致数据变化,如何展示变化后的数据?
- 最简单方案
- 全部重绘
- 存在问题:性能
- 最理想方案
- 仅局部重绘变化的部分
- 关键问题:怎么知道哪部分变化了?
- Virtual DOM概念
- DOM的内存表示形式
- 作用:比较Virtual DOM的差异,仅将差异部分更新到实际的DOM
- JSX(JavaScript XML)
- 什么是JSX
<h1 className="greeting"> Hello, world! </h1>
- JSX是表达式,仅是一种Syntax Sugar,由Babel转换为
React.createElement( 'h1', {className: 'greeting'}, 'Hello, world!' )
- 计算结果是JSON对象
{ type: 'h1', props: { className: 'greeting', children: 'Hello, world' } }
- 对应的HTML标记
<h1 class="greeting">Hello, world</h1>
- View Model和Virtual DOM的对应关系
- View Model是树形的JSON数据
- JSON数据和组件树存在对应关系
- 因此:Virutal DOM的差异问题转化为JSON数据的差异问题
- 什么是JSX
组件(Component)
- React基于组件概念
- 重复模式的标记子树可以抽象为“组件”
HTML
页面也是一颗组件树- Web Component
- 新的技术标准
- 目前有四种方式
- 在框架中作为实现技术
- React组件
- React组件相关的数据包括:
- 属性(Properties)
- 只读
- 输入输出参数,其中输出参数是函数类型,可理解为回调函数向父组件传递结果
- 状态(State)
- 记录组件的内部状态
- 使用
useState
Hook更改状态,因为涉及框架的界面更新逻辑 - 注意:更改状态操作是异步的
- 没有状态的组件是Stateless组件,也称为Presentational组件,有状态的也称为Container组件
- 属性(Properties)
- 事件处理
- 生命周期回调
- React组件相关的数据包括:
数据不可变性(Immutability)
-
解决问题:如何最有效地判断哪些数据已发生了变化?
-
- 原数据没有发生变化,即不变性
- 关键:只需比较引用,即可判断子对象是否发生了变化
-
实现方案
-
使用Object和Array的API实现
- Oject实现示意
{...oldObject, updateKey: updateValue }
- 相当于mutable的实现: oldObject.updateKey = updateValue;
- Array实现示意
- 增:
[ newItem, ...oldArray ]
- 改:
Array.map(item => item.id === modified.id ? modified : item)
- 删:
Array.filter(item => item.id !== deletedId)
- 增:
- 说明
- 理解在React中为什么要使用这样的实现方式?
- JSON树较深时可读性差
- Oject实现示意
-
- 可以使用mutable API直接更改draft副本
- 目前尚未在生成代码中使用
-
官方文档导读
Hello World
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
ReactDOM
来自react-dom
库<h1>Hello, world!</h1>
是JSX表达式document.getElementById
是DOM API- 含义:在
id="root"
的元素位置展示以JSX表示的Virtual DOM - 实际应用程序中往往是展示顶层
<App/>
组件
JSX
- 为避免不必要的
<div>
,JSX支持<Fragment>...</Fragment>
,也可以写为<>...</>
- 运行时选择类型
const components = { photo: PhotoStory, video: VideoStory }; function Story(props) { const SpecificStory = components[props.storyType]; return <SpecificStory story={props.story} />; }
组件绘制
- 在实际生产开发中,大多数React应用只会调用一次
ReactDOM.render()
- 函数式组件绘制、重绘的流程如下:
- 调用组件函数返回JSX,即返回Virtual DOM的JSON数据表示形式
- Reconciliation:比较和已有Virtual DOM的差异
- Commit:将差异更新到DOM中
- 其中第二和第三步由框架内部实现
- 使用
React Developer Tools
Chrome插件
组件 & Props
- 组件从概念上看就像是函数,接收任意的属性值(称之为"props"),返回在页面上展示的React元素
- 组件定义
- 使用函数方式定义组件
- 函数定义的完整示例在
src/components/example.tsx
中- 在TypeScript中定义组件,需增加properties的类型定义,注意是只读的
- 使用
React.memo()
将组件指定为Pure,相当于使用类定义方式从React.PureComponent
继承- 关键:只做浅层比较(shallow comparison): 值类型比较值,对象类型比较引用
- State/Props是不可变对象
- 所有子组件都是Pure的
State & 生命周期
- 状态
- 使用
useState()
定义状态
const [count, setCount] = useState(0);
- 如果计算状态的初始值开销很大,则可定义为arrow function
const [count, setCount] = useState(() => heavyComputation());
setCount
用于异步更新状态,了解 requestAnimationFrame- Hook运行机制说明
function Form() { // 1. Use the name state variable const [name, setName] = useState('Mary'); // 2. Use an effect for persisting the form useEffect(function persistForm() { localStorage.setItem('formData', name); }); // 3. Use the surname state variable const [surname, setSurname] = useState('Poppins'); // 4. Use an effect for updating the title useEffect(function updateTitle() { document.title = name + ' ' + surname; }); // ... } // ------------ // 第一次render // ------------ useState('Mary') // 1. 将状态变量name初始化为'Mary' useEffect(persistForm) // 2. 新增执行persistForm effect useState('Poppins') // 3. 将状态变量surname初始化为'Poppins' useEffect(updateTitle) // 4. 新增执行updateTitle effect // ------------- // 第二次render // ------------- useState('Mary') // 1. 读取name变量 (参数被忽略) useEffect(persistForm) // 2. 更新执行persistForm effect useState('Poppins') // 3. 读取surname变量 (参数被忽略) useEffect(updateTitle) // 4. 更新执行updateTitle effect // ...
- 使用
事件处理
- React元素的事件处理和DOM元素的很相似
- 传入一个函数作为事件处理函数
- 不能使用返回
false
的方式阻止默认行为,必须明确的使用preventDefault
- 注意,使用arrow function避免
bind(this)
- 可以向事件处理程序传递参数
条件渲染
- 可以使用以下方式条件渲染:
if
语句&&
逻辑表达式? :
三元表达式
- 返回
null
不渲染
列表 & Keys
- 使用
Array.map
渲染列表 - 对于列表组件,每个组件增加唯一的
key
属性优化渲染 - 由于JSX是表达式,
Array.map
中可以包括JSX
表单
- 其值由React控制的输入表单元素称为“受控组件”,“非受控组件”的值保存在DOM对象中
- 多个输入的解决方法
Context API
- Context设计目的是为共享那些被认为对于一个组件树而言是“全局”的数据
createContext
创建- Context.Provider
- 使用
useContext
使用Context
高级指引
-
Accessibility:尚未在产品中运用
-
Code-Splitting:v16.6的
React.lazy()
和Suspend
已运用 -
Context:使用
useContext
记录登录等基本全局信息 -
Error Boundaries:类似try…catch概念,已使用
-
Forwarding Ref:未使用
-
Fragments:
<>...</>
,推荐使用 -
高阶组件: 优先考虑Hook实现
-
和其他库集成:尚未集成第三方jquery组件
-
深入JSX
-
性能优化:使用
create-react-app
production -
Portals:实现弹出窗口,待定
-
Profiler:暂未使用
-
不使用 ES6:使用TypeScript
-
不使用JSX:使用
-
协调(Reconciliation): 了解对比算法
-
Refs & DOM:使用Refs可直接访问DOM,一般地不推荐使用
-
Render Props: 优先考虑Hook实现
-
静态类型检查:使用TypeScript类型定义
-
strict mode:总是使用
-
PropTypes 进行类型检查:使用TypeScript类型定义
-
非受控组件:使用ref,不推荐使用
-
Web Components:暂不使用
组件性能
目标
- 提升性能的关键:
- 快速检测变化部分
- 减少不必要的重绘
快速检测
- 快速检测变化的核心是使用Pure Component,即通过一次组件props引用的比较,即可判断数据是否发生了变化
- 为实现该功能,数据必需是Immutable的
减少重绘
- 组件在以下两种情况下重绘:
- 组件props发生了变化
- 组件state发生了变化
- 减少子组件的重绘
- 使用Immutable数据减少不必要的数据变化,参见
test-memo
- 对于props中的callback函数,使用
useCallback
记住能避免不必要的子组件重绘,参见test-useCallback
- 使用Immutable数据减少不必要的数据变化,参见
- 对于列表,
key
属性必需设置为唯一的业务键以优化比较,不能设置为易变的下标值
检查表
- 组件是否用作组件树中的叶节点?如果是,该组件是有状态的吗?建议叶节点组件是无状态的
- 组件是否是Pure Component?即是否使用了
React.memo
?建议总是使用Pure Component - 列表组件的
key
属性是否是业务上唯一?注意不要使用index
下标作为key
- 组件状态的初始值是常量吗?建议使用常量,如果初始状态需跟着
props
改变,请使用useEffect
设置状态 - 组件状态的初始值的计算开销很大吗?如果是,请使用
lambda
表达式 - 组件状态数据是否是Immutable的?建议总是使用Immutable数据
- 组件的
useState
是否很多?如果是,建议使用useReducer
封装状态逻辑 - 组件状态改变时,各子组件是否会因为callback函数的变化而重绘?如果是,请使用
useCallback
- 组件状态改变时,其中是否有开销很大的计算步骤?如果有,请使用
useMemo