React通过引入虚拟DOM、状态、单向数据流等设计理念,行程以组件为核心,用组件搭建UI的开发模式,理顺了UI的开发过程,完美地将数据、组件状态和UI映射到一起,极大地提高了开发大型Web应用的效率。
const/let
箭头函数
模板字符串
解构赋值
rest参数
class
import/export
Node.js:是一个js运行时,让js在服务器端运行成为现实。React在开发编译过程中用到的很多依赖(npm/webpack等)都需要Node.js环境。
npm:是一个模块管理工具,用来管理模块的发布、下载及模块之间的依赖关系。npm已经集成到Node.js安装包中,不需要单独安装。
yarn:是facebook联合exponent、google和tilde共同推出的另一个模块管理工具,可以作为npm的替代工具。
webpack:是用于现代js应用程序的模块打包工具。webpack会递归地构建一个包含应用程序所需的每个模块的依赖关系图,然后将所有模块打包到少量文件中。不仅可以打包js文件,配合相关插件的使用,它还可以打包图片资源和样式文件。
babel:是一个js编译器,它可以将ES6及其以后的语法编译成ES5的语法。babel一般会和webpack一起使用,在webpack编译打包的阶段,利用babel插件将ES6及其以后的语法编译成ES5的语法。
eslint:是一个插件化的js代码检测工具,既可以用于检查常见的js语法错误,又可以进行代码风格检查。使用eslint必须要指定一套代码规范的配置,然后eslint就会根据这套规范对代码进行检查。目前业内比较好的规范是Airbnb的规范,但这套规范过于严格,在实际使用时,可以先集成这套规范,再在它的基础上进行修改。
代码编辑器:推荐使用vs code,此外sublime text、Atom、WebStorm也是使用较多的代码编辑器。
React官方提供的脚手架工程Create React App,用于快速创建React应用,将webpack、babel、eslint等工具的配置做了封装。
JSX是一种用于描述UI的js扩展语法。
DOM类型的标签和React组件类型的标签。
当使用DOM类型的标签时,标签的首字母必须小写;当使用React组件类型的标签时,组件名称的首字母必须大写。React是通过首字母的大小写判断是哪一种类型的。
JSX语法是React.createElement(component,props,...children)
的语法糖,所有的JSX语法最终都会被转换成这个方法的调用。
例如:
//JSX语法
const element=<div className="foo">Hello, React</div>
//转换后
React.createElement('div',{
className:'foo'},'Hello, React');
会完成组件所代表的虚拟DOM节点到浏览器的DOM节点的转换。
这一步实际上是调用了React.Component的constructor方法,用来完成React组件的初始化工作。
事实上,React组件可以看做一个函数,函数的输入时props和state,函数的输出是组件的UI:
UI=Component(props,state)
props和state:React组件正是由props和state两种类型的数据驱动渲染出组件UI。
不是每个组件内部都需要定义state。state用来反映组件内部状态的变化,如果一个组件的内部状态是不变的,那就用不到state,这样的组件称之为无状态组件;反之,一个组件的内部状态会发生变化,就需要使用state来保存变化,这样的组件称之为有状态组件。
定义无状态组件除了使用class的方式外,还可以使用函数定义,即函数组件。一个函数组件接收props作为参数,返回代表这个组件UI的React元素结构。函数组件比类组件的写法更简洁,因此在使用无状态组件时,应尽量将其定义成函数组件。
无状态组件只聚焦于UI的展示,因而更容易被复用。
React应用组件的一般设计思路是,通过定义少数的有状态组件管理整个应用的状态变化,并且将状态通过props传递给其余的无状态组件,由无状态组件完成页面绝大部分UI的渲染工作。
PropTypes:用于校验组件属性的类型,包含组件属性所有可能的类型。在组件内部明显地声明它暴露出哪些接口,以及这些接口的类型是什么。
通过定义一个对象(对象的key是组件的属性名,value是对应属性的类型)实现组件属性类型的校验。
例如:
import PropTypes from 'prop-types';
class PostItem extends React.Component{
//...
}
PostItem.propType={
post:PropTypes.object,
onVote:PropTypes.func
};
PropTypes可以校验的组件属性类型:
类型 | PropTypes对应属性 |
---|---|
String | PropTypes.string |
Number | PropTypes.number |
Boolean | PropTypes.bool |
Function | PropTypes.func |
Object | PropTypes.object ,内部PropTypes.shape |
Array | PropTypes.array ,内部PropTypes.arrayOf |
Symbol | PropTypes.symbol |
Element(React元素) | PropTypes.element |
Node(可被渲染的节点) | PropTypes.node |
使用PropTypes.object
或PropTypes.array
校验属性类型时,我们只知道这个属性是一个对象或一个数组,至于对象的结构或数组元素的类型是什么样的无从得知。这种情况下,建议使用PropTypes.shape
或PropTypes.arrayOf
。例如:
PostItem.propType={
style:PropTypes.shape({
color:PropTypes.string,
fontSize:PropTypes.number
}),
sequence:PropTypes.arrayOf(PropTypes.number)
};
如果属性是组件的必须属性,需要在PropTypes的类型属性上调用isRequired。例如:
PostItem.propType={
post:PropTypes.shape({
id:PropTypes.number,
title:PropTypes.string,
author:PropTypes.string,
date:PropTypes.string,
vote:PropTypes.number
}).isRequired,
onVote:PropTypes.func.isRequired
};
默认属性:为组件属性指定默认值的特性。通过组件的defaultProps实现,当组件属性未被赋值时,组件会使用defaultProps定义的默认属性。例如:
function Welcome(props){
return <h1 className="foo">Hello, {
props.name}</h1>;
}
Welcome.defaultProps={
name:'Stranger'
};
React元素是一个普通的js对象,这个对象通过DOM节点或React组件描述界面是什么样子的。
JSX语法就是用来创建React元素的。
例如:
//Button是一个自定义的React组件
<div className='foo'>
<Button color='blue'>
OK
Button>
div>
上面的JSX代码会创建下面的React元素:
{
type:'div',
props:{
className:'foo',
children:{
type:'Button',
props:{
color:'blue',
children:'OK'
}
}
}
}
React组件是一个class或函数,它接受一些属性作为输入,返回一个React元素。
组件从被创建到被销毁的过程称为组件的生命周期。通常分为三个阶段:挂载阶段、更新阶段、卸载阶段。
只有类组件才具有生命周期方法,函数组件是没有的。
这个阶段组件被创建,执行初始化,并被挂载到DOM中,完成组件的第一次渲染。依次调用的生命周期方法有:
这个方法接收一个props参数,必须在这个方法中先调用super(props)
才能保证props被传入组件中。通常用于初始化组件的state以及绑定事件处理方法等工作。
这个方法在组件被挂载到DOM前调用。这个方法实际很少用到,因为可以在该方法中执行的工作都可以提前到constructor中。在这个方法中调用this.setState()
不会引起组件的重新渲染。
这是定义组件唯一必要的方法。在这个方法中,根据组件的props和state返回一个React元素,用于描述组件的UI。render并不负责组件的实际渲染工作,只是返回一个UI的描述,真正渲染出页面DOM的工作由React自身负责。render是一个纯函数,所以不能在render中调用this.setState()
,这会改变组件的状态。
在组件被挂载到DOM后调用。这时已经可以获取到DOM结构,因此依赖DOM节点的操作可以放到这个方法中。这个方法通常还会用于向服务器端请求数据。在这个方法中调用this.setState()
会引起组件的重新渲染。
组件被挂载到DOM后,组件的props或state可以引起组件更新。
this.setState()
修改组件state来触发的。组件更新阶段,依次调用的生命周期方法有:
这个方法只在props引起的组件更新过程中才会被调用,state引起的组件更新不会触发该方法的执行。但是,父组件render方法的调用并不能保证传递给子组件的props发生变化,也就是nextProps的值可能和子组件当前props的值相等,因此往往需要比较nextProps和this.props的值来决定是否执行props变化后的逻辑。
这个方法内部不能调用setState,否则会引起循环调用问题,render永远无法被调用,组件也无法正常渲染。
这个方法决定组件是否继续执行更新过程。可以用来减少组件不必要的渲染,从而优化组件性能。
一般通过nextProps、nextState和组件当前的props、state决定这个方法的返回结果。
这个方法内部不能调用setState,否则会引起循环调用问题,render永远无法被调用,组件也无法正常渲染。
这个方法在render调用前执行,可以作为组件更新发生前执行某些工作的地方,一般很少用到。
这个方法内部不能调用setState,否则会引起循环调用问题,render永远无法被调用,组件也无法正常渲染。
组件更新后被调用,可以作为操作更新后的DOM的地方。这两个参数prevProps, prevState代表组件更新前的props和state。
组件从DOM中被卸载的过程,这个过程中只有一个生命周期方法componentWillUnmount。
这个方法在组件被卸载前调用,可以在这里执行一些清理工作。比如清除定时器,清除componentDidMount中手动创建的DOM元素等,以避免引起内存泄漏。
React使用key属性来标记列表中的每个元素,当列表数据发生变化时,React就可以通过key知道哪些元素发生了变化,从而只重新渲染发生变化的元素,提高渲染效率。
不推荐使用索引作为key,因为一旦列表中的数据发生重排,数据的索引也会发生变化,不利于React的渲染优化。
React中的事件是合成事件,并不是原生的DOM事件。React根据W3C规范定义了一套兼容各个浏览器的事件对象。
在DOM事件中,可以通过处理函数返回false来阻止事件的默认行为。在React事件中,必须显示地调用事件对象的preventDefault方法来阻止事件的默认行为。除了这一点外,DOM事件和React事件在使用上并无差别。
如果在某些场景下必须使用DOM提供的原生事件,可以通过React事件对象的nativeEvent属性获取。
React事件处理函数的写法有3种,不同的写法解决this指向问题的方式不同:
使用箭头函数:因为箭头函数中的this指向的是函数定义时的对象,所以可以保证this总是指向当前组件的实例对象。直接在render方法中为元素事件定义事件处理函数,最大的问题是每次render调用时,都会重新创建一个新的事件处理函数,带来额外的性能开销,组件所处层级越低,这种开销就越大,因为任何一个上层组件的变化都可能会触发这个组件的render方法。大多数情况下,这点性能损失是可以不必在意的。
使用组件方法:直接将组件的方法赋值给元素的事件属性,同时在类的构造函数中,将这个方法的this通过bind方法绑定到当前对象。这种方式的好处是每次render不会重新创建一个回调函数,没有额外的性能损失。但在构造函数中,为事件处理函数绑定this,尤其是在多个事件处理函数需要绑定时,这样写会显得很繁琐。
属性初始化语法:使用ES7的property initializers会自动为class中定义的方法绑定this。实际也使用了箭头函数。这种方式既不需要手动绑定this,也不需要担心组件重复渲染导致的函数重复创建问题。但是这个特性还在试验阶段,默认不支持;使用create-react-app创建的项目默认支持;也可以自行在项目中引入babel的transform-class-propertities插件获取这个特性支持。
//在组件内部定义方法
handleClick=()=>{
...}
表单自身维护一些状态,而这些状态默认情况下是不受React控制的。
这类状态不受React控制的表单元素称为非受控组件。
在React中,状态的修改必须通过组件的state,非受控组件的行为显然有悖于这一原则。React采用受控组件的技术使表单元素状态的变更也能通过组件的state管理。
如果一个表单元素的值是由React来管理的,那么它就是一个受控组件。
React组件渲染表单元素,并在用户和表单元素发生交互时控制表单元素的行为,从而保证组件的state成为界面上所有元素状态的唯一来源。
三类常用表单元素的控制方式:
包含input元素和textarea元素。
受控的主要原理是:通过表单元素的value属性设置表单元素的值,通过表单元素的onChange事件监听值的变化,并将变化同步到React组件的state中。
处理多个表单元素技巧:为2个input元素分别指定name属性,使用同一个函数handleChange处理元素值的变化,在处理函数中根据元素的name属性区分事件的来源。这样写比为每个元素指定一个处理函数简洁。
下拉列表select。
在React中,通过在select上定义value属性来决定哪一个option元素处于选中状态,不需要关注option元素。
类型为checkbox的input元素和类型为radio的input元素。通常复选框和单选框的值是不变的,需要改变的是checked状态。因此React控制的属性不再是value,而是checked。
使用受控组件虽然保证了表单元素的状态由React统一管理,但需要为每个表单元素定义onChange事件的处理函数,然后把表单状态的更改同步到state。这一过程是比较繁琐的,一种可替代的方案是使用非受控组件。
非受控组件指表单元素的状态依然由表单元素自己管理。使用非受控组件需要使用ref来获取元素的值。设置defaultValue/defaultChecked属性来设置默认值。
非受控组件看似简化了操作表单元素的过程,但这种方式破坏了React对组件状态管理的一致性,往往容易出现不容易排查的问题,因此非特殊情况下,不建议使用。
React16是facebook在2017年9月发布的最新版本。基于Fiber新架构实现,几乎对React底层代码进行了重写,但对外API基本不变。实现了许多新特性。
React16之前,render方法必须返回单个元素,现在支持2种新的返回类型:数组(由React元素组成)和字符串。
React16之前,组件在运行期间如果执行出错会阻塞整个应用的渲染,只能刷新页面恢复。
React16引入了新的错误处理机制,默认情况下,当组件中抛出错误时,这个组件会从组件树中卸载,从而避免整个应用的崩溃。但用户体验还是不够好。
React16还提供了一种更友好的错误处理方式——错误边界。能够捕获子组件的错误并做优雅处理(可以是输出错误日志、显示出错提示等)。
错误边界:定义了componentDidCatch(error,info)这个方法的组件将成为一个错误边界。
示例:
//错误边界类
class ErrorBoundary extends React.Component{
constructor(props){
super(props);
this.state={
hasError:false};
}
componentDidCatch(error,info){
this.setState({
hasError:true});//显示错误UI
console.log(error,info);
}
render(){
if(this.state.hasError){
return <h1>出错了!</h1>
}
return this.props.children;
}
}
//调用类
class App extends Component{
constructor(props){
super(props);
this.state={
user:{
name:'react'}
};
}
onClick=()=>{
//点击按钮,模拟异常
this.setState({
user:null});
}
render(){
return (
<div>
<ErrorBoundary>
<Profile user={
this.state.user}/>
</ErrorBoundary>
<button onClick={
this.onClick}>更新</button>
</div>
);
}
}
const Profile=({
user})=><div>姓名:{
user.name}</div>
点击按钮,user变为null,程序会抛出Type Error,这个错误被ErrorBoundary捕获,并在界面上显示出错提示。
Portals可以把组件渲染到当前组件树以外的DOM节点上,这个特性典型的应用场景是渲染应用的全局弹框。使用Portals,任意组件都可以将弹框组件渲染到根节点上,以方便弹框的显示。
Portals的实现依赖react-dom的一个API:ReactDom.createPortal(child,container)
。
参数child是可以被渲染的React节点;container是一个DOM元素,child将被挂载到这个节点。
React16之前会忽略不识别的HTML和SVG属性,现在React会把不识别的属性传递给DOM元素。
设计原则:
组件state必须能代表一个组件UI呈现的完整状态集,即组件的任何UI改变都可以从state的变化中反映出来。
state还必须代表一个组件UI呈现的最小状态集,即state中的所有状态都用于反映组件UI的变化,没有任何多余的状态,也不应该存在通过其他状态计算而来的中间状态。
state所代表的一个组件UI呈现的完整状态集可以分为2类数据:
state、props、组件的普通属性定义:
this.{属性名}
定义一个class的属性,也可以说属性直接挂载到this下的变量。除了state、props以外的其他组件属性称为组件的普通属性。判断如何定义:
总结组件中一个变量是否应该作为state的4条依据:
修改state的三种陷阱:
React推荐组件的状态是不可变对象。
原因:
一是因为对不可变对象的修改会返回一个新对象,不需要担心原有对象在不小心的情况下被修改导致的错误,方便程序的管理和调试;
二是出于性能考虑,当对象组件状态都是不可变对象时,在组件的shouldComponentUpdate方法中仅需要比较前后两次状态对象的引用就可以判断状态是否真的改变,从而避免不必要的render调用。
只讨论组件从服务器上获取数据。
React组件的正常运转本质上是组件不同生命周期方法的有序执行。因此组件与服务器通信也必定依赖组件的生命周期方法。
React官方推荐在componentDidMount中从服务器获取数据;在componentWillMount中从服务器获取数据也很常见。
componentWillMount会在组件被挂载前调用;componentDidMount在组件被挂载后调用。但获取服务器数据越早就意味着能更快地得到数据,因此很多人青睐于使用componentWillMount。实际这两者执行的时间差很小,可以忽略不计。
componentDidMount是从服务器获取数据最佳的时刻,原因主要有2个:
不推荐在constructor中获取服务器数据的原因:构造函数的意义是执行组件的初始化工作,如设置组件的初始状态,并不适合做数据请求这类有副作用的工作。
组件在更新阶段通常需要再次与服务器通信,获取服务器上的最新数据。例如,组件需要以props中的某个属性作为与服务器通信的请求参数,当这个属性值发生更新时,组件自然需要重新与服务器通信。适合在componentWillReceiveProps中执行。
父组件向子组件通信是通过父组件向子组件的props传递数据完成的。
子组件向父组件通信时,父组件可以通过子组件的props传递给子组件一个回调函数,子组件在需要改变父组件数据时,调用这个回调函数即可。
当2个组件不是父子关系但有相同的父组件时,称为兄弟组件。
兄弟组件不能直接相互传递数据,需要通过状态提升的方式实现兄弟组件的通信,即把组件之间需要共享的状态保存到距离它们最近的共同父组件内。任意一个兄弟组件都可通过父组件传递的回调函数来修改共享状态,父组件中共享状态的变化也会通过props向下传递给所有兄弟组件,从而完成兄弟组件之间的通信。
当组件所处层级太深时,往往需要经过很多层的props传递才能将所需的数据或者回调函数传递给使用组件。这时,以props作为桥梁的组件通信方式会显得很繁琐。
React针对这一缺陷提供了一个context上下文,让任意层级的子组件都可以获取父组件中的状态和方法。
this.context.{属性名}
的方式访问父组件传出的属性。当context中包含数据时,如果要修改context中的数据,也不能直接修改。
过多使用context会让应用中的数据流变得混乱,而且context是个实验性的API,在未来版本中可能被修改或废弃,所以慎重使用context。
在某些场景下,ref的使用可以带来便利,例如控制元素的焦点、文本的选择或者和第三方操作DOM的库集成。
但绝大多数场景下,应该避免使用ref,因为它破坏了React中以props为数据传递介质的典型数据流。
ref常用的使用场景:
ref接收一个回调函数作为值,在组件被挂载或卸载时,回调函数会被调用:在组件被挂载时,回调函数会接收当前DOM元素作为参数;在组件被卸载时,回调函数会接收null作为参数。
例如让input自动获取焦点,这里如果不用ref就很难实现:
class AutoFocusTextInput extends React.Component{
componentDidMount(){
//通过ref让input自动获取焦点
this.textInput.focus();
}
render(){
return (
<div>
<input type="text" ref={
(input)=>{
this.textInput=input;}} />
</div>
);
}
}
在组件上使用ref,ref的回调函数接收的参数是当前组件的实例,提供了一种在组件外部操作组件的方式。
只能为类组件定义ref属性,不能为函数组件定义ref属性。但函数组件内部还是可以使用ref来引用其他DOM元素或类组件。
在一些场景下可能需要在父组件中获取子组件的某个DOM元素。例如需要知道子组件的DOM元素的尺寸或位置信息。
可以通过间接的方式获取子组件的DOM元素:在子组件的DOM元素上定义ref,ref的值是父组件传递给子组件的一个回调函数,回调函数可以通过一个自定义的属性传递,这样父组件的回调函数中就能获取到这个DOM元素。
例如,父组件Parent的inputElement指向的就是子组件的input元素:
//子组件
function Children(props){
return (
<div>
<input ref={
props.inputRef} />
</div>
);
}
//父组件
class Parent extends React.Component{
render(){
return (
<Children inputRef={
el => this.inputElement=el} />
);
}
}
React执行效率高的一个重要原因是它的虚拟DOM机制。
虚拟DOM是真实DOM的一层抽象。
直接操作真实DOM,每一次操作都会引起浏览对网页的重新布局和重新渲染,这个过程很耗时。
软件开发中有一句话:软件开发中遇到的所有问题都可以通过增加一层抽象而得以解决。DOM效率低下的问题同样可以通过增加一层抽象解决,虚拟DOM就是这层抽象,建立在真实DOM之上。
虚拟DOM使用普通的js对象来描述DOM元素,访问js对象自然比访问真实DOM快得多。React元素本身也是一个虚拟DOM节点。
例如,下面是一个DOM结构:
<div className="foo">
<h1>
Hello React
h1>
div>
可以用这样一个js对象来表述:
{
type:'div',
props:{
className:'foo',
children:{
type:'h1',
props:{
children:'Hello React'
}
}
}
}
使用Diff算法原因:每次组件的状态或属性更新,组件的render方法都会返回一个新的虚拟DOM对象,用来表述新的UI结构。如果每次render都直接使用新的虚拟DOM来生成真实DOM结构,那么会带来大量对真实DOM的操作,影响程序执行效率、
事实上React会通过比较两次虚拟DOM结构的变化找出差异部分,更新到真实DOM上,从而减少最终要在真实DOM上执行的操作,提高程序执行效率。这一过程就是React的调和过程,其中的关键就是比较两个树形结构的Diff算法。
正常情况下,比较两个树形结构差异的算法的时间复杂度是O(n^3),这个效率显然是无法接受的。
React通过总结DOM的实际使用场景,提出了两个在绝大多数实践场景下都成立的假设,基于这两个假设,React实现了在O(n)时间复杂度内完成两棵虚拟DOM树的比较。这2个假设是:
React比较两棵树是从树的根节点开始比较的,根节点的类型不同,React执行的操作也不同。
根节点类型的变化是一个很大的变化,React会认为新的树和旧的树完全不同,不会再继续比较其他属性和子节点,而是把整棵树拆掉重建(包括虚拟DOM树和真实DOM树)。
虚拟DOM的节点类型分为两类:一类是DOM元素类型,一类是React组件类型。
在旧的虚拟DOM树被拆除的过程中,旧的DOM元素类型的节点会被销毁,旧的React组件实例的componentWillUnmount会被调用;在重建的过程中,新的DOM元素会被插入DOM树中,新的组件实例的componentWillMount和componentDidMount方法会被调用。重建后,新的虚拟DOM树又会被整体更新到真实DOM树中。
这种情况下需要大量DOM操作,更新效率最低。
这种情况,React会保留根节点,而比较根节点的属性,然后只更新那些变化了的属性。
例如比较后只有className发生了变化,就只更新虚拟DOM树和真实DOM树中对应节点的这一属性。
这种情况,对应的组件实例不会被销毁,只是会执行更新操作,同步变化的属性到虚拟DOM树上,这时组件实例的componentWillReceiveProps和componentWillUpdate会被调用。
注意,对于组件类型的节点,React是无法直接知道如何更新真实DOM树的,需要在组件更新并且render方法执行完成后,根据render返回的虚拟DOM结构决定如何更新真实DOM树。
比较完根节点后,React会议同样的原则继续递归比较子节点,直到比较完两棵树上的所有节点,计算得到最终的差异,更新到真实DOM树中。
问题:当一个节点有多个子节点时,默认情况下,React只会按照顺序逐一比较两棵树上对应的子节点。这种比较方式会导致,只要有一个节点不同,每个节点都被修改,效率低。
因此当渲染列表元素时,需要为每一个元素定义一个key。这个key就是为了帮助React提高Diff算法的效率。当一组子节点定义了key,React会根据key来匹配子节点,在每次渲染之后,只要子节点的key值没有变化,React就认为这是同一个节点。
尽量不要使用元素在列表中的索引值作为key,因为列表中的元素顺序一旦发生改变,就可能导致大量key失效,进而引起大量的修改操作。
React中常用的性能优化方式:
以npm start/npm run dev
启动时,使用的React和第三方依赖库都是开发环境版本的React库,包含大量警告信息便于开发,但是开发环境版本的库体积更大、执行速度更慢,不适合在生产环境使用。
执行npm run build
构建生产环境版本的库,其原理是,第三方库根据process.env.NODE_ENV这个环境变量决定在开发环境和生产环境下执行的代码有哪些不同,当执行npm run build
时,构建脚本会把NODE_ENV的值设置为production,也就是会以生产环境模式编译代码。
背景:当组件的props或state发生变化时,组件的render方法会被重新调用,返回一个新的虚拟DOM对象。但在一些情况下,组件是没有必要重新调用render方法的。
举例:父组件的每一次render都会触发子组件componentWillReceiveProps调用,进而子组件的render方法也会被调用,但是这时候子组件的props可能并没有发生改变,此时子组件的render就是没有必要的,不仅多了一次render方法执行的时间,还多了一次虚拟DOM比较的时间。
解决方法:解决这个问题可以使用shouldComponentUpdate方法,这个方法默认返回true,如果返回false,组件此次的更新将会停止,也就是后续的componentWillUpdate、render等方法都不会再被执行。可以在这个方法中根据组件自身的业务逻辑决定返回true还是false,从而避免不必要的渲染。
shouldComponentUpdate中的比较:执行浅比较会使用===来比较,而不会比较对象属性的内容。React中提供了一个PureComponent组件,这个组件会使用浅比较来比较新旧props和state,因此可以通过让组件继承PureComponent来替代手写shouldComponentUpdate的逻辑。但是使用浅比较在直接修改数据的情况就比较不出来了,因此state中的值推荐使用不可变对象,只需要浅比较对象的引用,而不用进行深比较。
key的使用减少了DOM操作,提高了DOM更新效率。当列表元素数量很多时,key的使用更显得重要。
可以通过Chrome浏览器的Performance工具观察组件的挂载、更新、卸载过程及各阶段使用的时间。
使用方式:
why-did-you-update插件会比较组件的state和props的变化,从而发现render方法不必要的调用。
安装:
npm install why-did-you-update --save-dev
使用:
import React from 'react';
if(process.env.NODE_ENV!=='production'){
const {
whyDidYouUpdate}=require('why-did-you-update');
whyDidYouUpdate(React);
}
高阶组件(Higher Order Component, HOC)主要用来实现组件逻辑的抽象和复用,在很多第三方库(如Redux)中都被使用到。合理使用能提高项目的代码质量。
在js中,高阶函数是以函数为参数,并且返回值也是函数的函数。类似地,高阶组件接收React组件作为参数,并且返回一个新的React组件。高阶组件本质上也是一个函数,并不是一个组件。
主要功能:封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用。
设计模式:装饰者模式。
在被包装组件接收props前,高阶组件可以先拦截到props,对props执行增删改的操作,然后将处理后的props再传递给被包装组件。
高阶组件通过ref获取被包装组件实例的引用,然后高阶组件就具备了直接操作被包装组件的属性或方法的能力。
这种用法在实际项目中很少会被用到,但当高阶组件封装的复用逻辑需要被包装组件的方法或属性的协同支持时,这种用法就有了用武之地。
无状态组件更容易被复用。高阶组件可以通过将被包装组件的状态及相应的状态处理方法,提升到高阶组件自身内部,实现被包装组件的无状态化。
一个典型的场景就是,利用高阶组件将原本受控组件需要自己维护的状态统一提升到高阶组件中。
可以在高阶组件渲染WrappedComponent时添加额外的元素,这种情况通常用于为WrappedComponent增加布局或修改样式。
高阶组件参数传递最简单的方式是,给高阶组件接收其他参数。这种方式很少使用。
例如:
//高阶组件定义
function withPersistentData(WrappedComponent, key){
...}
//调用
const MyComponentWithPersistentData=withPersistentData(MyComponent,'name');
更多的是采用更加灵活、通用的函数形式——支持多个高阶组件组合使用:
HOC(...params)(WrappedComponent)
HOC(…params)的返回值是一个高阶组件,高阶组件需要的参数是先传递给HOC函数的。
示例:
//高阶组件定义
function withPersistentData=(key)=>(WrappedComponent)=>{
...}
//调用
const MyComponentWithPersistentData=withPersistentData('name')(MyComponent);
这种形式的高阶组件大量出现在第三方库中,例如Redux的connect函数。
connect的简化定义:
//connect是高阶组件
connect(mapStateToProps, mapDispatchToProps)(WrappedComponent)
//调用,withLog()为另一个高阶组件
const ConnectedComponentA=connect(mapStateToProps, mapDispatchToProps)(withLog()(ComponentA));
enhance:
//connect是高阶组件
const enhance=connect(mapStateToProps, mapDispatchToProps);
const ConnectedComponentA=enhance(ComponentA);
//调用,withLog()为另一个高阶组件
const ConnectedComponentA=enhance(withLog()(ComponentA));
compose:
把高阶组件嵌套的方式打平。
compose定义:
function compose(...funcs){
if(funcs.length===0){
return arg=>arg;
}
if(funcs.length===1){
return funcs[0];
}
return funcs.reduce((a,b)=>(...args)=>a(b(args)));
}
调用compose(f,g,h)
等价于(...args)=>f(g(h(...args)))
。
compose结合高阶组件使用可以显著提高代码的可读性和逻辑的清晰度。
改写前面使用调用connect和withLog的写法:
//conenct和withLog是2个高阶组件,用compose打平嵌套的写法
const enhance=compose(
conenct(mapStateToProps, mapDispatchToProps),
withLog()
);
//调用
const ConnectComponentA=enhance(ComponentA);
前面介绍的高阶组件的实现方式都是由高阶组件处理通用逻辑,然后将相关属性传递给被包装组件,这种实现方式称为属性代理。
除了属性代理,还可以通过继承方式实现高阶组件:通过继承被包装组件实现逻辑的复用。继承方式实现的高阶组件常用于渲染劫持。
function withAuth(WrappedComponent){
return class extends WrappedComponent{
//继承被包装组件
render(){
if(this.props.loggedIn){
return super.render();//调用父类组件的render方法
}else{
return null;
}
}
}
}
缺点:继承方式实现的高阶组件对被包装组件具有侵入性,当组合多个高阶组件使用时,很容易因为子类组件忘记通过super调用父类组件方法而导致逻辑丢失。
因此,在使用高阶组件时,应尽量通过属性代理方式实现高阶组件。
为了在开发和调试阶段更好地区别包装了不同组件的高阶组件,需要对高阶组件的显示名称做自定义处理。常用处理方式是:把被包装组件的显示名称也包到高阶组件的显示名称中。
示例:
//高阶组件
function withPersistentData(WrappedComponent){
return class extends Component{
static displayName=`HOC(${
getDisplayName(WrappedComponent)})`;
render(){
...}
}
}
//获取被包装组件名称
function getDisplayName(WrappedComponent){
return WrappedComponent.displayName||WrappedComponent.name||'Component';
}
不要在组件的render方法中使用高阶组件,尽量也不要在组件的其他生命周期方法中使用高阶组件。
因为调用高阶组件,每次都会返回一个新的组件,于是每次render,前一次高阶组件创建的组件都会被卸载,然后重新挂载本次创建的新组件。既影响效率又丢失了组件及其子组件的状态。
所以高阶组件最适合使用的地方是在组件定义的外部,这样就不会受到组件生命周期的影响。
如果需要使用被包装组件的静态方法,那么必须手动复制这些静态方法。因为高阶组件返回的新组件不包含被包装组件的静态方法。
示例:
WrappedComponent.staticMethod=function(){
...}
function withHOC(WrappedComponent){
class Enhance extends React.Component{
...
}
Enhance.staticMethod=WrappedComponent.staticMethod;
return Enhance;
}
Refs不会被传递给被包装组件。尽管在定义高阶组件时,会把所有的属性都传递给被包装组件,但是ref并不会传递。
如果希望获取被包装组件的引用,可以自定义一个属性,属性的值是一个函数,传递给被包装组件的ref。
示例:
function FocusInput({
inputRef,...rest}){
return <input ref={
inputRef} {
...rest} />
}
//EnhanceInput是高阶组件
const EnhanceInput=enhance(FocusInput);
//在一个组件的render方法中,自定义属性inputRef代替ref,保证inputRef可以传递给被包装组件
return (
<EnhanceInput inputRef={
el=>this.input=el} />
);
//组件内调用
this.input.focus();
高阶组件强调的是逻辑的抽象。高阶组件是一个函数,函数关注的是逻辑。如果逻辑是与DOM不直接相关的,那么这部分逻辑适合使用高阶组件抽象。如数据校验、请求发送等。
父组件是一个组件,组件主要关注的是UI。如果逻辑是与DOM直接相关的,那么这部分逻辑适合放到父组件中实现。
多页面应用:在传统Web应用中,浏览器根据地址栏的URL向服务器发送一个HTTP请求,服务器根据URL返回一个HTML页面。这种情况下,一个URL对应一个HTML页面,一个Web应用包含很多HTML页面。
后端路由:在多页面应用中,页面路由的控制由服务器端负责。
多页面应用缺点:每次页面切换都需要向服务器发送一次请求,页面使用到的静态资源也需要重新请求加载,存在一定的浪费。而且页面的整体刷新对用户体验也有影响,因为不同页面间往往存在共同的部分(如导航栏、侧边栏等),页面整体刷新也会导致共用部分的刷新。
单页面应用优点:看起来像多页面应用,实际URL的变化可以引起页面内容的变化,但不会向服务器发送新的请求。无论URL如何变化,对应的HTML文件都是同一个。
单页面应用缺点:目前国内的搜索引擎大多对SPA的SEO支持的不好,因此对于SEO非常看重的Web应用(例如企业官方网站、电商网站等),一般还是会选择采用多页面应用。
前端路由:单页面应用中,URL的变化不会向服务器发送新的请求,所以“逻辑页面”的路由只能由前端负责。
React Router包含3个库:react-router、react-router-dom、react-router-native。
react-router提供最基本的路由功能,实际使用时不会直接安装react-router,而是根据应用功能运行环境选择安装react-router-dom(浏览器中使用)或react-router-native(react-native中使用),这两者都依赖于react-router,因此react-router会自动安装。
React Router通过Router和Route2个组件完成路由功能。Router可以理解成路由器,一个应用中只需要一个Router实例,所有的路由配置组件Route都定义为Router的子组件。
在Web应用中,我们一般会使用对Router进行包装的BrowserRouter或HashRouter两个组件。
BrowserRouter
使用HTML5的history API(pushState、replaceState等)实现应用的UI和URL同步。
URL形式:http://example.com/some/path
使用时一般还需对服务器进行配置,让服务器能正确地处理所有可能的URL。
HashRouter
使用URL的hash实现应用的UI和URL的同步。
URL形式:http://example.com/#/some/path
使用时不需要对服务器配置,因为hash部分会被服务器自动忽略,前面部分是固定的。
路由原理:Router会创建一个history对象,history用来跟踪URL,当URL发生变化时,Router的后代组件会重新渲染。React Router中提供的其他组件可以通过context获取history对象,这也隐含说明了React Router中的其他组件必须作为Router组件的后代组件使用。但Router中只能有唯一一个子元素。
Route是React Router中用于配置路由信息的组件。
path:每个Route都需要定义path属性。BrowserRouter中path用来描述这个Route的pathname,HashRouter中path用来描述这个Route的hash。
match:当URL和Route匹配时,Route会创建一个match对象作为props中的一个属性传递给被渲染的组件。这个对象包括以下4个属性:
,当URL为http://example.com/foo/1时。params={id:1}。Route渲染组件的方式:Route定义了3个属性用于定义待渲染的组件:
Switch和exact:当URL和Route匹配时,这些Route都会执行渲染操作。如果只想让第一个匹配的Route渲染,那么可以把这些Route包到一个Switch组件中。如果想让URL和Route完全匹配时,Route才渲染,那么可以使用Route的exact属性。Switch和exact常联合使用,用于应用首页的导航。
嵌套路由:是指在Route渲染的组件内部定义新的Route。Route的嵌套使用让应用可以更加灵活地使用路由。
Link是React Router提供的链接组件,一个Link组件定义了当点击该Link时,页面应该如何路由。
Link使用to属性声明要导航到的URL地址。to可以是string或object类型,当to为object类型时,可以包含pathname、search、hash、state四个属性。
除了使用Link外,还可以使用history对象手动实现导航。history中最常用的两个方法是push(path[,state])和replace(path[,state])。push会向浏览历史记录中新增一条记录,replace会用新记录替换当前记录。
当代码量不多时,把所有代码打包到一个文件的做法不会有什么影响。
对于一个大型应用,当访问一个页面时,浏览器加载的js还包含其他页面的代码,这会延长网页的加载时间。当访问一个页面时,该页面应该只加载自己使用到的代码。
解决这个问题的方案就是代码分片,将js代码分片打包到多个文件中,然后在访问页面时按需加载。
打包后没有单独的css文件了,因为css样式被打包到各个chunk文件中。当chunk文件被加载执行时,会动态地把css样式插入页面中。如果需要把css打包到一个单独的文件中,需要修改webpack使用的ExtractTextPlugin插件的配置。
npm run eject
可以将create-react-app管理的配置文件暴露出来,项目中会多出2个文件:config和scripts,scripts中包含项目启动、编译和测试的脚本,config中包含项目使用的配置文件,webpack的配置文件就在这个路径下。
通过修改webpack.config.prod.js中的ExtractTextPlugin的配置,allChunks设置为true。重新编译项目,各个、chunk文件使用的css样式又会统一打包到main.css中。
React主要的关注点是如何创建可复用的视图层组件,对于组件之间的数据传递和状态管理并没有给出很好的解决方案。
redux通过reducer解析action,reducer接收action为参数,返回一个新的状态state。
redux的主要思想是描述应用的状态如何根据action进行更新,redux通过一系列API将这一主要思想的落地实施进行标准化和规范化。
redux应用只维护一个全局的状态对象,存储在redux的store中。唯一数据源是一种集中式管理应用状态的方式,便于监控任意时刻应用的状态和调试应用,减少出错的可能性。
在任何时候都不能直接修改应用状态。当需要修改应用状态时,必须发送一个action,由这个action描述如何修改应用状态。这一看似繁琐的修改状态的方式实际上是redux状态管理流程的核心,保证了大型复杂应用中状态管理的有序进行。
action表明修改应用状态的意图,真正对应用状态做修改的是reducer。reducer必须是纯函数,所以reducer在接收到action时,不能直接修改原来的状态对象,而是要创建一个新的状态对象返回。
redux应用的主要组成有:action、reducer、store。
action是redux中信息的载体,是store唯一的信息来源。把action发送给store必须通过store的dispatch方法。action是普通的js对象,但每个action必须有一个type属性描述action的类型,type一般被定义为字符串常量。除了type属性外,action的结构完全由自己决定,但应该确保action的结构能清晰地描述实际业务场景。
一般通过action creator 创建action。action creator是返回action的函数。
action用于描述应用发生了什么操作,reducer根据action做出响应,决定如何修改应用的状态state。既然是修改state,那么就应该在编写reducer前设计好state。state既可以包含服务器端获取的数据,也可以包含UI状态。
一般会拆分出多个reducer,每个reducer处理state中的部分状态。
redux还提供了一个combineReducers函数,用于合并多个reducer。
store是redux中的一个对象,也是action和reducer之间的桥梁。
store主要负责以下几个工作:
展示组件:负责应用的UI展示,也就是组件如何渲染,具有很强的内聚性。不关心渲染时使用的数据是如何获取到的。
容器组件:负责应用逻辑的处理,如发送网络请求、处理返回数据、将处理过的数据传递给展示组件使用等。容器组件还提供修改源数据的方法,通过展示组件的props传递给展示组件,当展示组件的状态变更引起源数据变化时,展示组件通过调用容器组件提供的方法同步这些变化。
这样的分工可以是与UI渲染无直接关系的业务逻辑由同期组件集中负责,展示组件只关注UI的渲染逻辑,从而使展示组件更容易被复用。
connect函数用于把react组件和redux的store连接起来,生成一个容器组件,负责数据管理和业务逻辑。
示例:
import {
connect} from 'react-redux';
import TodoList from './TodoList';
const VisibleTodoList=connect(
mapStoreToProps,
mapDispatchToProps
)(TodoList);
mapStateToProps:负责从全局应用状态state中取出所需数据,映射到展示组件的props。每当store中的state更新时,mapStateToProps就会重新执行,重新计算传递给展示组件的props,从而触发组件的重新渲染。
注意,store中的state更新一定会导致mapStateToProps重新执行,但不一定会触发组件render。如果mapStateToProps新返回的对象和之前的对象浅比较相等,组件的shouldComponentUpdate方法就会返回false,组件的render方法也就不会被再次触发。这是react-redux的一个重要优化。
mapDispatchToProps:负责把需要用到的action映射到展示组件的props上。容器组件依赖mapDispatchToProps发送action更新state。mapDispatchToProps接收store.dispatch方法作为参数,返回展示组件用来修改state的函数。
mapStateToProps和mapDispatchToProps还接收第二个参数ownProps代表容器组件的props对象。
Provider组件需要接收一个store属性,然后把store属性保存到context。Provider组件正式通过context把store传递给子组件的,所以使用Provider组件时,一般把它作为根组件,这样内层的任意组件才可以从context中获取store对象。
中间件常用于Web服务器框架中。一个请求在经过中间件处理后,才能到达业务逻辑代码层,多个中间件可以串联起来使用,前一个中间件的输出是下一个中间件的输入,整个处理过程如同管道一般。
redux的中间件概念与此类似,redux的action可类比web框架收到的请求,reducer可类比web框架的业务逻辑层。因此,redux的中间件代表action在到达reducer前经过的处理程序。
一个redux中间件就是一个函数。redux中间件增强了store的功能,可以利用中间件为action添加一些通用功能,如日志输出、异常捕获等。利用applyMiddleware方法可引入第三方中间件。
redux中处理异步操作必须借助中间件的帮助。redux-thunk是处理异步操作最常用的中间件。
在实际项目中,处理一个网络请求往往会使用三个action,分别表示请求开始、请求成功、请求失败。
例如:
{type:'FETCH_DATA_REQUEST'},
{type:'FETCH_DATA_SUCCESS',data:{...}},
{type:'FETCH_DATA_FAILURE',error:'error'}
除了redux-thunk外,常用于异步操作的中间件还有redux-promise、redux-saga等。
设计state时容易犯的2个错误:
合理设计state:像设计数据库一样设计state。把state看做一个数据库,state中的每一部分状态看做数据库中的一张表,状态中的每一个字段对应表的一个字段。
设计一个数据库应该遵循以下3个原则:
根据这3个原则可以得出设计state时的原则:
除了领域数据,还有应用状态数据(反映应用行为的数据)和UI状态数据(UI当前如何显示的数据)。
Redux DevTools
Immutable.js的作用在于以更高效的方式创建不可变对象,主要优点:保证数据的不可变、丰富的API和优异的性能。
可以在reducer中通过Immutable.js的API修改state,防止开发者误改原state。搭配redux-immutable库,代替combineReducers合并reducer。
节省selectors重新计算。
MobX是Redux之后的一个状态管理库,基于响应式管理状态,整体是一个观察者模式的架构,存储state的store是被观察者,使用store的组件是观察者。
MobX可以有多个store对象,store使用的state也是可变对象。相较于redux,MobX更轻量。
MobX通过函数响应式编程的思想使状态管理变得简单和可扩展。和redux一样,MobX也是采用单向数据流管理状态:通过action改变应用的state,state的改变进而会导致受其影响的views更新。
MobX包含的主要概念有4个:state状态,computed value计算值,reaction响应,action动作。
computed value和reaction会自动根据state的改变做最小化的更新,并且这个更新过程是同步执行的。action更改state后,新的state是可以被立即获取的;computed value采用的是延迟更新,只有当computed value被使用时它的值才会被重新计算,当computed value不再被使用时,它将会被自动回收。computed value必须是纯函数,不能使用它修改state。
MobX使用了大量装饰器语法。
略