一、React基础
主要包括:
- JSX语法
- 组件
- 列表
- 事件处理
- 表单
1、 JSX简介
JSX是一种用于描述UI的JavaScript扩展语法,React使用这种语法描述组件的UI。
(1)、 基本语法
使用成对的标签构成一个树状解决的数据。
const element = (<div><span>Hello</span></div>)
(2)、 标签类型
- DOM类型 (例如
div, span,必须小写) - React组件类型 (必须大写)
(3)、 JavaScript表达式
因为JSX本质上Javascript,所以JSX可以使用Javascript表达式。
JSX的使用主要场景:
- 1.通过表达式给标签属性赋值;
- 2.通过表达式定义组件
// 通过表达式给标签属性赋值
const element = <MyComponent foo={ 1 + 2 } />
// 通过表达式定义子组件
const element = ( <ul>{todos.map(message => <Item key={message} message= {message} />)} </ul> )
注意:JSX中只能使用Javascript表达式,而不能使用多行JavaScript多行语句。 但是可以使用三目运算符或者短路运算符
(4)、 标签属性
当JSX标签是DOM类型的标签时,对应DOM标签支持的属性JSX也支持。但是,部分属性的名称会有 所改变,主要的变化有:class 要写成className,事件属性名采用驼峰 格式,例如onclick 要写成 onClick。 当JSX标签是React组件类型时,可以任意自定义标签的属性名。 5. JSX不是必需的 JSX语法对使用React来说并不是必需的,JSX语法只是 React.createElement (component, props, ...children)的语法糖,所有的JSX 语法最终都会被转换成对这个方法的调用。
//JSX语法
const element = <div className='foo'>Hello, React</div>
//转换后
const element = React.createElement('div', {className: 'foo'}, 'Hello, React')
2、 组件
(1)、 组件定义
组件是React的核心概念,组件将应用的 UI拆分成独立的、可复用的模块,React应用程序正是由一个一个组件 搭建而成的。
定义一个组件有两种方式:
- 使用ES 6 class(类组件)
- 使用函数 (函数组件)
使用class定义组件需要满足两个条件:
- class继承自React.Component。
- class内部必须定义render方法,render方法返回代表该组件UI 的React元素。
ReactDOM.render(<PostList />, document.getElementById("root"));
使用ReactDOM.render() 需要先导入react-dom库,这个库会 完成组件所代表的虚拟DOM节点到浏览器的DOM节点的转换。
(2)、 组件的props
组件的props用于把父组件中 的数据或方法传递给子组件,供子组件使用。 props是一个简单结构的对象,它包含的属性正是由组 件作为JSX标签使用时的属性组成。
(3)、 组件的state
组件的state是组件内部的状态,state的变化最终将反映到组件UI的 变化上。 React组件正是由props和state两种类型的数据驱动渲染出组件UI。 props是组件对外的接口,组件通过props接收外部传入的数据(包括方法)。
(4)、 有状态组件和无状态组件
react组件可分为:有状态组件和无状态组件 React应用组件设计的一般思路是,通过定义少数的 有状态组件管理整个应用的状态变化,并且将状态通过props传递给其 余的无状态组件,由无状态组件完成页面绝大部分UI的渲染工作。 总之,有状态组件主要关注处理状态变化的业务逻辑,无状态组件主要关 注组件UI的渲染。
(5)、 属性校验和默认属性
props是一个组件对外暴露的接口,明显地声明它暴露出哪些接口,以及这些接口的类型是什么。 React提供了PropTypes这个对象,用于校验组件属性的类型。 React还提供了为组件属性指定默认值的特性,这个特性通过组件 的defaultProps实现。
(6)、 组件样式
- 外部css样式 这种方式和我们平时开发Web应用时使用外部CSS文件相同。
使用CSS样式表经常遇到的一个问题是class名称冲突。 业内解决这个问题的一个常用方案是使用CSS Modules,CSS Modules 会对样式文件中的class名称进行重命名从而保证其唯一性,但CSS Modules并不是必需的,create-react-app创建的项目,默认配置也是不支 持这一特性的。
- 内联样式
function Welcome(props) {
return (
<h1 style={{
width: "100%", height: "50px", backgroundColor: "blue", fontSize: "20px"
}} > Hello, {props.name} </h1>
)}
style使用了两个大括号,这可能会让你感到迷惑。其实,第一个大 括号表示style的值是一个JavaScript表达式,第二个大括号表示这个 JavaScript表达式是一个对象。 注意:样式的属性名必须使 用驼峰格式的命名
3、组件的生命周期
组件的生命周期可分为三个阶段:
- 挂载阶段
- 更新阶段
- 卸载阶段
(1)、 挂载阶段
这个阶段组件被创建,执行初始化,并被挂载到DOM中,完成组 件的第一次渲染。
组件挂载依次调用的生命周期方法有:
(1)constructor
(2)componentWillMount
(3)render
(4)componentDidMount
render并不负责组件的实际渲染工作,它只是返回一个UI的描述,真正的渲染出页面DOM的工作由React自身负责。render是一个纯函数,在这 个方法中不能执行任何有副作用的操作,所以不能在render中调用 this.setState。
componentDidMount依赖DOM节点的操作可以放到这个方法中。 这个方法通常还会用于向服务器端请求数据。
(2)、 更新阶段
组件被挂载到DOM后,组件的props或state可以引起组件更新。
组件更新阶段,依次调 用的生命周期方法有:
(1)componentWillReceiveProps
(2)shouldComponentUpdate
(3)componentWillUpdate
(4)render
(5)componentDidUpdate
componentWillReceiveProps(nextProps)这个方法只在props引起的组件更新过程中,才会被调用。State引 起的组件更新并不会触发该方法的执行。方法的参数nextProps是父组件传递给当前组件的新的props。
往往需要比较nextProps和this.props来决定是否执行props发生变化后的逻辑。例如:比较组件当前的loading状态和最新返回的loading状态。
componentWillReceiveProps(nextProps){
if(this.props.loading !== nextProps.loading && nextProps.loading){
const {customerId,init, search, searchCustomer} = nextProps;
init(customerId);
search(customerId);
}
}
!注意:
componentWillReceiveProps、shouldComponentUpdate和componentWillUpdate`中都不能调用setState,否则会引起循环调用问题。
(3)、 卸载阶段
这个方法在组件被卸载前调用,可以在这里执行一些清理工作。比如清除组件中使用的定时器,清除componentDidMount中手动创建的 DOM元素等,以避免引起内存泄漏。
4、列表和keys
<Select placeholder="标题" showSearch mode="multiple">
{
nurses.toJS().map(opt => {
return (<Option key={opt.id+''}
value={opt.id+''}>{opt.name}</Option>);
})
}
</Select>
列表中的每个元素添加一个名为key的属性。那么这个属性有什么作用呢?原来,React使用key属性来标记列 表中的每个元素,当列表数据发生变化时,React就可以通过key知道哪些元素发生了变化,从而只重新渲染发生变化的元素,提高渲染效率。
5、事件处理
在React元素中绑定事件有两点需要注意:
- (1)在React中,事件的命名采用驼峰命名方式,而不是DOM元素中的小写字母命名方式。例如,onclick要写成onClick,onchange要写成 onChange等。
- (2)处理事件的响应函数要以对象的形式赋值给事件属性,而不是DOM中的字符串形式。
在React组件中处理事件最容易出错的地方是事件处理函数中this的指向问题,因为ES 6 的class并不会为方法自动绑定this到当前对象。
(1)、使用箭头函数,作函数方法调用
(2)、使用组件方法。直接将组件的方法赋值给元素的事件属性,同时在类的构造函数 中,将这个方法的this绑定到当前对象。
this.handleClick = this.handleClick.bind(this);
<button onClick={this.handleClick}> Click </button>
有些开发者还习惯在为元素的事件属性赋值时,同时为事件处理函 数绑定this,例如:
<button onClick={this.handleClick.bind(this)}> Click </button>
(3)、属性初始化语法(property initializer syntax)
handleClick = (event) => {
const number = ++this.state.number;
this.setState({ number: number });
}
React中的事件是合成事件,并不是原生的DOM事件。React根据 W3C规范定义了一套兼容各个浏览器的事件对象。在DOM事件中,可 以通过处理函数返回false来阻止事件的默认行为,但在React事件中,必 须显式地调用事件对象的preventDefault方法来阻止事件的默认行为。
6、表单
(1)、受控组件
如果一个表单元素的值是由React来管理的,那么它就是一个受控 组件。有很多,类似如下:
<input type="text" name="name" value= {this.state.name} onChange={this.handleChange} />
(2)、非受控组件
- 受控组件限制:使用受控组件虽然保证了表单元素的状态也由React统一管理,但需要为每个表单元素定义onChange事件的处理函数,然后把表单状态的 更改同步到React组件的state,这一过程是比较烦琐的。
- 使用非受控组件:一种可替代的解决方案是使用非受控组件。非受控组件指表单元素的状态依然由表单元素自己管理,而不是交给React组件管理。使用非受控组件需要有一种方式可以获取到表单元素的值,React中提供了一个特殊的属性
ref,用来引用React组件或DOM元素的实例。
handleSubmit(event) { // 通过this.input 获取到input元素的值
alert('The title you submitted was ' + this.input.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
title: {/* this.input 指向当前input元素 */}
<input type="text" ref={(input) => this.input = input}/>
</label>
<input type="submit" value="Submit"/>
</form>
);
}
6、React 16新特性
(1)、render新的返回类型
React 16之前,render方法必须返回单个元素。现在,render方法支 持两种新的返回类型:数组(由React元素组成)和字符串。定义一个 ListComponent组件,它的render方法返回数组:
class ListComponent extends Component {
render() {
return [
<li key="A">First item</li>,
<li key="B">Second item</li>,
<li key="C">Third item</li>
];
}
}
(2)、错误处理
- React 16之前,组件在运行期间如果执行出错,就会阻塞整个应用 的渲染,这时候只能刷新页面才能恢复应用。
- React 16引入了新的错误 处理机制,默认情况下,当组件中抛出错误时,这个组件会从组件树中卸载,从而避免整个应用的崩溃。
- 这种方式比起之前的处理方式有所进步,但用户体验依然不够友好。React16还提供了一种更加友好的错误处理方式——错误边界(Error Boundaries)。错误边界是能够捕获子组件的错误并对其做优雅处理的组件。优雅的处理可以是输出错误日志、显示出错提示等,显然这比直接卸载组件要更加友好。
定义了componentDidCatch(error, info)这个方法的组件将成为一个错误边界,现在我们创建一个组件ErrorBoundary:
componentDidCatch(error, info) {
// 显示错误UI
this.setState({ hasError: true });
// 同时输出错误日志
console.log(error, info);
}
然后在App中使用ErrorBoundary:
<ErrorBoundary>
<Profile user={this.state.user} />
</ErrorBoundary>
(3)、Portals
React 16的Portals特性让我们可以把组件渲染到当前组件树以外的DOM节点上,这个特性典型的应用场景是渲染应用的全局弹框,使用 Portals后,任意组件都可以将弹框组件渲染到根节点上,以方便弹框的显示。
Portals的实现依赖ReactDOM的一个新的API: ReactDOM.createPortal(child, container)
- 第一个参数child是可以被渲染的React节点,例如React元素、由React元素组成的数组、字符串等,container是一个DOM元素,child将 被挂载到这个DOM节点。
(4)、自定义DOM属性
- React 16之前会忽略不识别的HTML和SVG属性,现在React会把不 识别的属性传递给DOM元素。例如,React 16之前,下面的React元素
<div custom-attribute="something" />在浏览器中渲染出的DOM节点为:<div /> - 而React 16渲染出的DOM节点为:
<div custom-attribute="something" />
二、深入理解组件
1、组件state
(1)、设计合理的state
组件state必须能代表一个组件UI呈现的完整状态集,即组件的任何UI改变都可以从state的变化中反映出来;同时,state还必须代表一个组 件UI呈现的最小状态集,即state中的所有状态都用于反映组件UI的变化,没有任何多余的状态,也不应该存在通过其他状态计算而来的中间状态。
// 错误的state示例
{
purchaseList:[],
totalCost: 0
}
错误实例原因,如下:
这里的state是初始状态,因此purchaseList初始化为一个空数组,totalCost初始化为0,这个state的设计确实可以满足组件UI呈现的完整状 态集这一条件,但是它包含一个无用的状态totalCost,因为totalCost可以根据购买的每一项物品的价格和数量计算得出,所以有了 purchaseList,就可以计算出totalCost,totalCost属于中间状态,可以省略。
总结一下,组件中用到的一个变量是不是应该作为state可以通过下 面的4条依据进行判断:
- (1)这个变量是否通过props从父组件中获取?如果是,那么它不 是一个状态。
- (2)这个变量是否在组件的整个生命周期中都保持不变?如果 是,那么它不是一个状态。
- (3)这个变量是否可以通过其他状态(state)或者属性(props) 计算得到?如果是,那么它不是一个状态。
- (4)这个变量是否在组件的render方法中使用?如果不是,那么它不是一个状态。这种情况下,这个变量更适合定义为组件的一个普通属性。
(2)、state 的更新是异步的
调用setState时,组件的state并不会立即改变,setState只是把要修改的状态放入一个队列中,React会优化真正的执行时机,并且出于性能 原因,可能会将多次setState的状态修改合并成一次状态修改。所以不要 依赖当前的state,计算下一个state。
// 错误的更新state示例
this.setState({quantity: this.state.quantity + 1})
// 正确
this.setState((preState, props) => ({ counter: preState.quantity + 1; }))
(3)、state与不可变对象
React官方建议把state当作不可变对象,一方面,直接修改this.state,组件并不会重新render;另一方面,state中包含的所有状态都 应该是不可变对象。当state中的某个状态发生变化时,应该重新创建这 个状态对象,而不是直接修改原来的状态。
那么,当状态发生变化时, 如何创建新的状态呢?根据状态的类型可以分成以下三种情况:
- 1.状态的类型是不可变类型(数字、字符 串、布尔值、null、undefined)
这种情况最简单,因为状态是不可变类型,所以直接给要修改的状态赋一个新值即可。
- 2.状态的类型是数组 例如有一个数组类型的状态books,当向books中增加一本书时,可 使用数组的concat方法或ES6的数组扩展语法(spread syntax):
// 方法一:使用preState、concat创建新数组
this.setState(preState => ({
books: preState.books.concat(['React Guide']);
}))
// 方法二:ES6 spread syntax
this.setState(preState => ({
books: [...preState.books, 'React Guide'];
}))
注意,不要使用push、pop、shift、unshift、splice等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改的,而concat、 slice、filter会返回一个新的数组。
- 3.状态的类型是普通对象(不包含字符 串、数组)
(1)使用ES6的Object.assgin方法:
this.setState(preState => ({
owner: Object.assign({}, preState.owner, {name: 'Jason'});
}))
(2)使用对象扩展语法(object spread properties):
this.setState(preState => ({
owner: {...preState.owner, name: 'Jason'};
}))
总结一下,创建新的状态对象的关键是,避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法。当然,也可以使用一些Immutable的JS库(如Immutable.js)实现类似的效果。
为什么React推荐组件的状态是不可变对象呢?
- 一方面是因为对不 可变对象的修改会返回一个新对象,不需要担心原有对象在不小心的情况下被修改导致的错误,方便程序的管理和调试;
- 另一方面是出于性能考虑,当对象组件状态都是不可变对象时,在组件的shouldComponentUpdate方法中仅需要比较前后两次状态对象的引用就可以判断状态是否真的改变,从而避免不必要的render调用。
2、组件与服务器通信
(1)、组件挂载阶段通信
componentDidMount中与服务器进行通信的, 这时候组件已经挂载,真实DOM也已经渲染完成,是调用服务器API最安全的地方,也是React官方推荐的进行服务器通信的地方。
componentDidMount() {
var that = this;
fetch('/path/to/user-api').then(function(response) {
response.json().then(function(data) {
that.setState({users: data})
});
});
}
componentDidMount是执行组件与服务器通信的最佳地方,原因主 要有两个:
- (1)在
componentDidMount中执行服务器通信可以保证获取到数据时,组件已经处于挂载状态,这时即使要直接操作DOM也是安全的, 而componentWillMount无法保证这一点。 - (2)当组件在服务器端渲染时(本书不涉及服务器渲染内容),
componentWillMount会被调用两次,一次是在服务器端,另一次是在浏 览器端,而componentDidMount能保证在任何情况下只会被调用一次, 从而不会发送多余的数据请求。
当然:
componentWillMount会在组件被挂载前调用,因此从时间上来讲,在componentWillMount中执行服务器通信要早于在componentDidMount中执行,执行得越早意味着服务器数据越能更快地返回组件。这也是很多人青睐在componentWillMount中执行服务器通信的重要原因。但实际 上,componentWillMount与componentDidMount执行的时间差微乎其 微,完全可以忽略不计。
(2)、组件更新阶段通信
componentWillReceiveProps非常适合做这个工作。在执行fetch请求时,要先对新老props中的category做比较,只有不一致才说明category有了更新,才需要重新进行服务器通信。componentWillReceiveProps的执行并不能保证props一定发生了修改。
componentWillReceiveProps(nextProps) {
if(nextProps.category !== this.props.category) {
fetch('/path/to/user-api?category='+ nextProps.category).then(function(response) {
response.json().then(function(data) {
that.setState({users: data})
});
});
}
}
3、组件间通信
(1)、父子组件通信
当子组件需要向父组件通信时,通过props传值。父组件可以通过子组件的props传递给子组件一个回调函数,子组件在需要改变父组件数据时,调用这个回调函数即可。
(2)、兄弟组件通信
兄弟组件不能直接相互传送数据,需要通过状态提升的方式实现兄弟组件的通信,即把组件之间需要共享的状态保存到距离它们最近的共 同父组件内,任意一个兄弟组件都可以通过父组件传递的回调函数来修改共享状态,父组件中共享状态的变化也会通过props向下传递给所有兄弟组件,从而完成兄弟组件之间的通信。
(3)、Context
- 业务场景:当组件所处层级太深时,往往需要经过很多层的props传递才能将所需的数据或者回调函数传递给使用组件。这时,以props作为桥梁的 组件通信方式便会显得很烦琐。
- 解决方案:React提供了一个context上下 文,让任意层级的子组件都可以获取父组件中的状态和方法。
使用 context, 我们可以避免通过中间元素传递 props:
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。
总结延伸
- 前面介绍的三种组件通信方式都是依赖React组件自身的语法特性。
- 其实,还有更多的方式可以来实现组件通信。实际的大型项目中,都会引入专门的状态管理库实现组件通信和组件状态的管理,例如Redux和MobX是当前非常受欢迎的两种状态管理库。
4、特殊的ref
注意:
ref不仅 可以用来获取表单元素,还可以用来获取其他任意DOM元素,甚至可以用来获取React组件实例。在一些场景下,ref的使用>可以带来便利, 例如控制元素的焦点、文本的选择或者和第三方操作DOM的库集成。
但绝大多数场景下,应该避免使用ref,因为它破坏了React中以props为 数据传递介质的典型数据流。
(1)、在DOM元素上使用ref
在DOM元素上使用ref是最常见的使用场景。
ref接收一个回调函数作为值,在组件被挂载或卸载时,回调函数会被调用,在组件被挂载时,回调函数会接收当前DOM元素作为参数;在组件被卸载时,回调 函数会接收null作为参数。
export default class AutoFocusTextInput extends React.Component {
componentDidMount() { // 通过ref让input自动获取焦点
this.textInput.focus();
}
render() {
return (<div>
<input type="text" ref={(input) => {
this.textInput = input;
}}/></div>);
}
}
(2)、在组件上使用ref<--!!>
React组件也可以定义ref,此时ref的回调函数接收的参数是当前组件的实例,这提供了一种在组件外部操作组件的方式。
例如,在使用 AutoFocusTextInput组件的外部组件Container中控制 AutoFocusTextInput:
// Container 组件
class Container extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() { // 通过ref调用AutoFocusTextInput组件的方法
this.inputInstance.blur();
}
render() {
return (
<div><AutoFocusTextInput ref={(input) => {
this.inputInstance = input
}}/>
<button onClick={this.handleClick}>失去焦点</button>
</div>
);
}
}
// AutoFocusTextInput 组件
class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props);
this.blur = this.blur.bind(this);
}
componentDidMount() { // 通过ref让input自动获取焦点
this.textInput.focus();
}
// 让input失去焦点
blur() {
this.textInput.blur();
}
render() {
return (<div><input type="text" ref={(input) => {
this.textInput = input;
}}/></div>);
}
}
注意:
- 只能为类组件定义ref属性,而不能为函数组件定义ref属性。
- 函数组件虽然不能定义ref属性,但这并不影响在函数组件内部使用。ref来引用其他DOM元素或组件。例如下面的例子是可以正常工作的。
function MyFunctionalComponent() {
let textInput = null;
function handleClick() {
textInput.focus();
}
return (<div><input type="text" ref={(input) => {
textInput = input;
}}/>
<button onClick={handleClick}>获取焦点</button>
</div>)
}
(3)、父组件访问子组件的DOM节点
在一些场景下,我们可能需要在父组件中获取子组件的某个DOM元素,例如父组件需要知道这个DOM元素的尺寸或位置信息,这时候直接使用ref是无法实现的,因为ref只能获取子组件的实例对象,而不能获取子组件中的某个DOM元素。
不过,我们可以采用一种间接的方式获取子组件的DOM元素:在子组件的DOM元素上定义ref,ref的值是父组件传递给子组件的一个回调函数,回调函数可以通过一个自定义的 属性传递,例如inputRef,这样父组件的回调函数中就能获取到这个 DOM元素。
function Children(props) { // 子组件使用父组件传递的inputRef,为input的ref赋值
return (<div><input ref={props.inputRef}/></div>);
}
class Parent extends React.Component {
render() { // 自定义一个属性inputRef,值是一个函数
return (<Children
inputRef={el => this.inputElement = el}/>);
}
}
三、虚拟DOM和性能优化
React之所以执行效率高,一个很重要的原因是它的虚拟DOM机 制。React应用常用的性能优化方法也大都与虚拟DOM机制相关。
1、虚拟DOM
虚拟DOM使用普通的JavaScript对象来描述DOM元素,React元素本身就是一个虚拟DOM节点。例如,下面是一个DOM结构:
<div className="foo">
<h1>Hello React</h1>
</div>
可以用这样的一个JavaScript对象来表述:
{
type: 'div',
props: {
className: 'foo',
children: {
type: 'h1',
props:{
children: 'Hello React'
}
}
}
}
有了虚拟DOM这一层,当我们需要操作DOM时,就可以操作虚拟 DOM,而不操作真实DOM,虚拟DOM是普通的JavaScript对象,访问 JavaScript对象当然比访问真实DOM要快得多。
2、Diff算法
React会通过比较两次虚拟DOM结构的变化找出差异部分,更新到真实DOM上,从而减少最终要在真实DOM上执行的操作,提高程序执行效率。这一过程就是React的调和过程(Reconciliation),其中的关键是比 较两个树形结构的Diff算法。
正常情况下,比较两个树形结构差异的算法的时间复杂度是 O(N^3),这个效率显然是无法接受的。
React通过总结DOM的实际使用 场景提出了两个在绝大多数实践场景下都成立的假设,基于这两个假设,React实现了在O(N)时间 复杂度内完成两棵虚拟DOM树的比较。
这两个假设是:
- (1)如果两个元素的类型不同,那么它们将生成两棵不同的树。
- (2)为列表中的元素设置key属性,用key标识对应的元素在多次 render过程中是否发生变化。
(1)、当根节点是不同类型时
从div变成p、从ComponentA变成ComponentB,或者从ComponentA变成div,这些都是节点类型发生变化的情况。根节点类型的变化是一个很大的变化,React会认为新的树和旧的树完全不同,不会再继续比较其他属性和子节点,而是把整棵树拆掉重建(包括虚拟DOM树和真实DOM树)。
虚拟DOM的节点类型分为两类:
- 一类是DOM元素类型,比如div、p等;
- 一类是React组件类型,比如自定义的 React组件。
这种情况下,需 要大量DOM操作,更新效率最低。
(2)、当根节点是相同的DOM元素类型时
如果两个根节点是相同类型的DOM元素,React会保留根节点,而 比较根节点的属性,然后只更新那些变化了的属性。例如:
<div className="foo" title="React" />
<div className="bar" title="React" />
React比较这两个元素,发现只有className属性发生了变化,然后只更新虚拟DOM树和真实DOM树中对应节点的这一属性。
(3)、当根节点是相同的组件类型时
如果两个根节点是相同类型的组件,对应的组件实例不会被销毁,只是会执行更新操作,同步变化的属性到虚拟DOM树上,这一过程组件实例的componentWillReceiveProps()和componentWillUpdate()会被调用。
比较完根节点后,React会以同样的原则继续递归比较子节点,每一个子节点相对于其层级以下的节点来说又是一个根节点。如此递归比 较,直到比较完两棵树上的所有节点,计算得到最终的差异,更新到 DOM树中。
但是,会出现一个问题:但如果在子节点的开始位置新增一个节点,那之后所有的子节点都会重新比较一遍,这种比较方式会导致每一个节点都被修改。
// befort
<ul>
<li>first</li>
<li>second</li>
</ul>
// after
<ul>
<li>third</li>
<li>first</li>
<li>second</li>
</ul>
为了解决这种低效的更新方式,React提供了一个key属性,当渲染列表元素时,需要为每一个元素定义一个key。这个key就是为了帮助React提高Diff算法的效率。当一组子节点定义了 key,React会根据key来匹配子节点,在每次渲染之后,只要子节点的 key值没有变化,React就认为这是同一个节点。
3、性能优化
虽然,React通过虚拟DOM、高效的Diff算法等技术极大地提高了操作 DOM的效率。但只要是程序,总会有一些优化的措施。
(1)、使用生产环境版本的库
React是开发环境版本的React库,包含大量警告消息,以帮助我们在开发过程中避免一些常见的错误,比如组件props类型的校验等。开发环境版本的库不仅体积更大,而且执行速度也更慢,显然不适合在生产环境中使用。
那就使用生成环境的。打包时,利于process.env.NODE_ENV生成生产环境的代码。
(2)、避免不必要的组件渲染
React组件的生命周期方法中提供了一个shouldComponentUpdate方法,这个方法的默认返回值是true,如果返回false,组件此次的更新将会停止,也就是后续的componentWillUpdate、render等方法都不会再被执行。
(3)、使用key
React会根据key索引元素,在render前后,拥有相同key值的元素是同一个元素,提高执行效率。
4、性能检测工具
(1)、React Developer Tools for Chrome
(2)、Chrome Performance Tab
在开发模式下,可以通过Chrome浏览器提供的Performance工具观 察组件的挂载、更新、卸载过程及各阶段使用的时间。
(3)、why-did-you-update
why-did-you-update会比较组件的 state 和 props 的变化,从而发现 组件render方法不必要的调用。
四、高阶组件
高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件
高阶组件是React 中一个很重要且较复杂的概念,主要用来实现组件逻辑的抽象和复用,在很多第三方库(如Redux)中都被使用到。即使开发一般的业务项目,如果能合理地使用高阶组件,也能显著提高项目的代码质量。
1、基本概念
在JavaScript中,高阶函数是以函数为参数,并且返回值也是函数的函数。类似地,高阶组件(简称HOC)接收React组件作为参数,并且返回一个新的React组件。高阶组件本质上也是一个函数,并不是一个组件。高阶组件的函数形式如下:
const EnhancedComponent = higherOrderComponent(WrappedComponent);
现在有一个组件MyComponent,需要从LocalStorage中获取数据,然后渲染到界面。但当其他组件也需要从LocalStorage中获取同样的数 据展示出来时,每个组件都需要重写一次componentWillMount中的代码,这显然是很冗余的。下面让我们来看看使用高阶组件改写这部分代码。
Demo1:
// 高阶组件封装
import React, {Component} from 'react'
function withPersistentData(WrappedComponent) {
return class extends Component {
componentWillMount() {
let data = localStorage.getItem('data');
this.setState({data});
}
render() { // 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}
// 在普通组件中使用高阶组件
class MyComponent extends Component {
render() {
return <div>{this.props.data}</div>
}
}
const MyComponentWithPersistentData = withPersistentData(MyComponent)
Demo2:
// 定义一个高阶组件
import React, { Component } from 'react'
export default (WrappedComponent, name) => {
class NewComponent extends Component {
constructor () {
super()
this.state = { data: 36 }
}
componentWillMount () {
let data = localStorage.getItem(name)
this.setState({ data })
}
render () {
return <WrappedComponent data={this.state.data} />
}
}
return NewComponent
}
// 在普通组件中使用高阶组件
import wrapWithLoadData from './wrapWithLoadData'
import React, { Component } from 'react'
class InputWithUserName extends Component {
render () {
return <input value={this.props.data} />
}
}
InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
export default InputWithUserName
可以看出,高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用。
高阶组件的这种实现方式本质上是装饰者设计模式。
2、使用场景
高阶组件的使用场景主要有以下4种:
- (1)操纵props
- (2)通过ref访问组件实例
- (3)组件状态提升
- (4)用其他元素包装组件
(1)、操纵props
在被包装组件接收props前,高阶组件可以先拦截到props,对props执行增加、删除或修改的操作,然后将处理后的props再传递给被包装组件。
(2)、通过ref访问组件实例
高阶组件通过ref获取被包装组件实例的引用,然后高阶组件就具备 了直接操作被包装组件的属性或方法的能力。
function withRef(wrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.someMethod = this.someMethod.bind(this);
}
someMethod() {
this.wrappedInstance.someMethodInWrappedComponent();
}
render() { //为被包装组件添加ref属性,从而获取该组件实例并赋值给
this.wrappedInstance
return <WrappedComponent ref={(instance) => {
this.wrappedInstance = instance
}} {...this.props} />
}
}
}
(3)、组件状态提升
高阶组件可以 通过将被包装组件的状态及相应的状态处理方法提升到高阶组件自身内部实现被包装组件的无状态化。
一个典型的场景是,利用高阶组件将原 本受控组件需要自己维护的状态统一提升到高阶组件中。
// 定义高阶组件
function withControlledState(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleValueChange = this.handleValueChange.bind(this);
}
handleValueChange(event) {
this.setState({value: event.target.value});
}
render() { // newProps 保存受控组件需要使用的属性和事件处理函数
const newProps = {controlledProps: {value: this.state.value, onChange: this.handleValueChange}};
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}
// 使用高阶组件
class SimpleControlledComponent extends React.Component {
render() {
//此时的SimpleControlledComponent为无状态组件,状态由高阶组件 维护
return <input name="simple" {...this.props.controlledProps}/>
}
}
const ComponentWithControlledState = withControlledState(SimpleControlledComponent);
(4)、用其他元素包装组件
我们还可以在高阶组件渲染WrappedComponent时添加额外的元 素,这种情况通常用于为WrappedComponent增加布局或修改样式。
function withRedBackground(WrappedComponent) {
return class extends React.Component {
render() {
return (
<div style={{backgroundColor: 'red'}}>
<WrappedComponent {...this.props}/>
</div> )
}
}
}
3、参数传递
高阶组件的参数并非只能是一个组件,它还可以接收其他参数。
// 定义高阶组件
import React, {Component} from 'react'
function withPersistentData(WrappedComponent, key) {
return class extends Component {
componentWillMount() {
let data = localStorage.getItem(key);
this.setState({data});
}
render() { // 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}
// 使用高阶组件
class MyComponent extends Component {
render() {
return <div>{this.props.data}</div>
}
}
// 获取key=’data’的数据
const MyComponent1WithPersistentData = withPersistentData(MyComponent, 'data');
// 获取key=’name’的数据
const MyComponent2WithPersistentData = withPersistentData(MyComponent, 'name');
实际情况中,我们很少使用这种方式传递参数,而是采用更加灵活、更具通用性的函数形式:
HOC(...params)(WrappedComponent)
HOC(…params)的返回值是一个高阶组件,高阶组件需要的参数是 先传递给HOC函数的。用这种形式改写withPersistentData如下(注意: 这种形式的高阶组件使用箭头函数定义更为简洁):
// 简易的方式
// 定义高阶组件
import React, {Component} from 'react'
const withPersistentData = (key) => (WrappedComponent) => {
return class extends Component {
constructor(props){
super(props)
this.state = {
data: '222'
}
}
componentWillMount() {
this.setState({data: key});
}
render() {
// 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}
// 使用高阶组件
class MyComponent extends Component {
render() {
return (
<div>
<div>{this.props.data}</div>
<h2>234</h2>
</div>
)
}
}
// 获取key=’data’的数据
const MyComponent1WithPersistentData = withPersistentData('data')(MyComponent);
// 获取key=’name’的数据
const MyComponent2WithPersistentData = withPersistentData('name')(MyComponent);
实际上,这种形式的高阶组件大量出现在第三方库中,例如react- redux中的connect函数就是一个典型的例子。
connect的简化定义如下:
connect(mapStateToProps, mapDispatchToProps) (WrappedComponent)
这个函数会将一个React组件连接到Redux的store上,在连接的过程中,connect通过函数参数mapStateToProps从全局store中取出当前组件需要的state,并把state转化成当前组件的props;同时通过函数参数 mapDispatchToProps把当前组件用到的Redux的action creators以props的 方式传递给当前组件。connect并不会修改传递进去的组件的定义,而是会返回一个新的组件。
注意:
connect的参数mapStateToProps、mapDispatchToProps是函数类型,说明高阶组件的参数也可以是函数类型。
例如,把组件ComponentA连接到Redux上的写法类似于:
const ConnectedComponentA = connect(mapStateToProps, mapDispatchToProps) (ComponentA);
拆分来看:
// connect 是一个函数,返回值enhance也是一个函数
const enhance = connect(mapStateToProps, mapDispatchToProps);
// enhance是一个高阶组件
const ConnectedComponentA = enhance(ComponentA);
4、继承方式实现高阶组件
前面介绍的高阶组件的实现方式都是由高阶组件处理通用逻辑,然后将相关属性传递给被包装组件,我们称这种实现方式为属性代理。
除了属性代理外,还可以通过继承方式实现高阶组件:通过继承被包装组件实现逻辑的复用。继承方式实现的高阶组件常用于渲染劫持。
继承方式实现的高阶组件对被包 装组件具有侵入性,当组合多个高阶组件使用时,很容易因为子类组件忘记通过super调用父类组件方法而导致逻辑丢失。因此,在使用高阶组件时,应尽量通过代理方式实现高阶组件。
5、使用高阶组件的注意事项
(1)为了在开发和调试阶段更好地区别包装了不同组件的高阶组件,需要对高阶组件的显示名称做自定义处理。常用的处理方式是,把被包装组件的显示名称也包到高阶组件的显示名称中。以 withPersistentData为例:
function withPersistentData(WrappedComponent) {
return class extends Component {
//结合被包装组件的名称,自定义高阶组件的名称
static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
render() {
//...
}
}
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
(2)不要在组件的render方法中使用高阶组件,尽量也不要在组件的其他生命周期方法中使用高阶组件。因为调用高阶组件,每次都会返 回一个新的组件,于是每次render,前一次高阶组件创建的组件都会被卸载(unmount),然后重新挂载(mount)本次创建的新组件,既影响效率,又丢失了组件及其子组件的状态。
render() {
// 每次render,enhance都会创建一个新的组件,尽管被包装的组件没有变
const EnhancedComponent = enhance(MyComponent);
// 因为是新的组件,所以会经历旧组件的卸载和新组件的重新挂载
return <EnhancedComponent />;
}
所以,高阶组件最适合使用的地方是在组件定义的外部,这样就不 会受到组件生命周期的影响。
(3)如果需要使用被包装组件的静态方法,那么必须手动复制这些静态方法。因为高阶组件返回的新组件不包含被包装组件的静态方法。例如:
// WrappedComponent组件定义了一个静态方法staticMethod
WrappedComponent.staticMethod = function() {
//...
}
function withHOC(WrappedComponent) {
class Enhance extends React.Component {
//...
}
// 手动复制静态方法到Enhance上
Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance; }
(4)Refs不会被传递给被包装组件。尽管在定义高阶组件时,我们会把所有的属性都传递给被包装组件,但是ref并不会传递给被包装组 件。如果在高阶组件的返回组件中定义了ref,那么它指向的是这个返回的新组件,而不是内部被包装的组件。如果希望获取被包装组件的引 用,那么可以自定义一个属性,属性的值是一个函数,传递给被包装组件的ref。下面的例子就是用inputRef这个属性名代替常规的ref命名:
function FocusInput({ inputRef, ...rest }) {
// 使用高阶组件传递的inputRef作为ref的值
return <input ref={inputRef} {...rest} />;
}
//enhance 是一个高阶组件
const EnhanceInput = enhance(FocusInput);
// 在一个组件的render方法中,自定义属性inputRef代替ref,
// 保证inputRef可以传递给被包装组件
return (<EnhanceInput inputRef={(input) => { this.input = input }}>)
// 组件内,让FocusInput自动获取焦点
this.input.focus();
(5)与父组件的区别。高阶组件在一些方面和父组件很相似。例如,我们完全可以把高阶组件中的逻辑放到一个父组件中去执行,执行 完成的结果再传递给子组件,但是高阶组件强调的是逻辑的抽象。高阶组件是一个函数,函数关注的是逻辑;父组件是一个组件,组件主要关 注的是UI/DOM。如果逻辑是与DOM直接相关的,那么这部分逻辑适合放到父组件中实现;如果逻辑是与DOM不直接相关的,那么这部分逻辑适合使用高阶组件抽象,如数据校验、请求发送等。