React学习笔记
React简介
React另辟蹊径,通过引入虚拟DOM、状态、单向数据流等设计理念,形成以组件为核心,用组件搭建UI的开发模式,理顺了UI的开发过程,完美地将数据、组件状态和UI映射到一起,极大地提高了开发大型Web应用的效率。
React基础
JSX简介
JSX是一种用于描述UI的JavaScript扩展语法
,React使用这种语法描述组件的UI。
使用类似HTML的标签描述组件的UI,让UI结构直观清晰,同时因为JSX本质上仍然是JavaScript,所以可以使用更多的JS语法,构建更加复杂的UI结构。
JSX语法
JSX的基本语法和XML语法相同,都是使用成对的标签构成一个树状结构的数据
标签类型
DOM类型的标签(div、span等)和React组件类型的标签
- 使用DOM类型的标签时,标签的首字母必须小写;
- 当使用React组件类型的标签时,组件名称的首字母必须大写。
- React正是通过首字母的大小写判断渲染的是一个DOM类型的标签还是一个React组件类型的标签。
JavaScript表达式
- JSX本质上仍然是JavaScript。
- 在JSX中使用JavaScript表达式需要将表达式用大括号“{}”包起来。
- 表达式在JSX中的使用场景主要有两个:通过表达式给标签属性赋值和通过表达式定义子组件。
- JSX中可以使用三目运算符或逻辑与(&&)运算符代替if语句的作用。
标签属性
- 当JSX标签是DOM类型的标签时,对应DOM标签支持的属性JSX也支持,例如id、class、style、onclick等。
- 部分属性的名称会有所改变,主要的变化有:class要写成className,事件属性名采用驼峰格式,例如onclick要写成onClick。原因是,class是JavaScript的关键字,所以改成className;
- React对DOM标签支持的事件重新做了封装,封装时采用了更常用的驼峰命名法命名事件。
- 当JSX标签是React组件类型时,可以任意自定义标签的属性名
- JSX中的注释需要用大括号“{}”将/**/包裹起来。
JSX不是必需的
JSX语法只是React.createElement(component, props, ...children)的语法糖,所有的JSX语法最终都会被转换成对这个方法的调用。
组件
定义一个组件有两种方式,使用ES 6 class(类组件)和使用函数(函数组件)。
使用class定义组件需要满足两个条件:
(1)class继承自React.Component。
(2)class内部必须定义render方法,render方法返回代表该组件UI的React元素。
挂载到页面的DOM节点上,需要使用ReactDOM.render()完成
组件的props
组件的props用于把父组件中的数据或方法传递给子组件,供子组件使用。
props是一个简单结构的对象,它包含的属性正是由组件作为JSX标签使用时的属性组成。
组件的state
- 组件的state是组件内部的状态,state的变化最终将反映到组件UI的变化上。
- 在组件的构造方法constructor中通过this.state定义组件的初始状态
- 通过调用this.setState方法改变组件状态(也是改变组件状态的唯一方式),进而组件UI也会随之重新渲染。
有三个需要注意的地方:
(1)在组件的构造方法constructor内,首先要调用super(props),这一步实际上是调用了React.Component这个class的constructor方法,用来完成React组件的初始化工作。
(2)在constructor中,通过this.state定义了组件的状态。
(3)在render方法中,我们为标签定义了处理点击事件的响应函数,在响应函数内部会调用this.setState更新组件的状态。
总结
组件的props和state都会直接影响组件的UI。事实上,React组件可以看作一个函数,函数的输入是props和state,函数的输出是组件的UI。
UI = Component (props, state)
- React组件正是由props和state两种类型的数据驱动渲染出组件UI。
- props是组件对外的接口,组件通过props接收外部传入的数据(包括方法)
- state是组件对内的接口,组件内部状态的变化通过state来反映。
- props是只读的,你不能在组件内部修改props;
- state是可变的,组件状态的变化通过修改state来实现。
有状态组件和无状态组件
state用来反映组件内部状态的变化,如果一个组件的内部状态是不变的,当然就用不到state,这样的组件称之为无状态组件
定义无状态组件除了使用ES 6 class的方式外,还可以使用函数定义:
一个函数组件接收props作为参数,返回代表这个组件UI的React元素结构。
- 尽可能多地使用无状态组件,无状态组件不用关心状态的变化,只聚焦于UI的展示,因而更容易被复用。
- React应用组件设计的一般思路是,通过定义少数的有状态组件管理整个应用的状态变化,并且将状态通过props传递给其余的无状态组件,由无状态组件完成页面绝大部分UI的渲染工作。
- 总之,有状态组件主要关注处理状态变化的业务逻辑,无状态组件主要关注组件UI的渲染。
属性校验和默认属性
React提供了PropTypes这个对象,用于校验组件属性的类型。PropTypes包含组件属性所有可能的类型,我们通过定义一个对象(对象的key是组件的属性名,value是对应属性的类型)实现组件属性类型的校验。
组件属性类型和PropTypes属性的对应关系
当使用PropTypes.object或PropTypes.array校验属性类型时,我们只知道这个属性是一个对象或一个数组,至于对象的结构或数组元素的类型是什么样的,依然无从得知。
这种情况下,更好的做法是使用PropTypes.shape或PropTypes.arrayOf。
React还提供了为组件属性指定默认值的特性,这个特性通过组件的defaultProps实现。当组件属性未被赋值时,组件会使用defaultProps定义的默认属性。
组件样式
为组件添加样式的方法主要有两种:外部CSS样式表和内联样式。
外部CSS样式表
CSS样式表中根据HTML标签类型、ID、class等选择器定义元素的样式。唯一的区别是,React元素要使用className来代替class作为选择器。
样式表的引入方式有两种,一种是在使用组件的HTML页面中通过标签引入:
- 一种是在使用组件的HTML页面中通过标签引入,引入样式表的方式常用于该样式表文件作用于整个应用的所有组件(一般是基础样式表)
- 第二种引入样式表的方式常用于该样式表作用于某个组件(相当于组件的私有样式)
内联样式
内联样式实际上是一种CSS in JS的写法:将CSS样式写到JS文件中,用JS对象表示CSS样式,然后通过DOM类型节点的style属性引用相应样式对象。
- 第一个大括号表示style的值是一个JavaScript表达式
- 第二个大括号表示这个JavaScript表达式是一个对象。
组件和元素
React元素是一个普通的JavaScript对象,这个对象通过DOM节点或React组件描述界面是什么样子的。JSX语法就是用来创建React元素的
React组件是一个class或函数,它接收一些属性作为输入,返回一个React元素。React组件是由若干React元素组建而成的。
组件的生命周期
组件从被创建到被销毁的过程称为组件的生命周期
组件的生命周期可以被分为三个阶段:挂载阶段、更新阶段、卸载阶段。
挂载阶段
这个阶段组件被创建,执行初始化,并被挂载到DOM中,完成组件的第一次渲染。
依次调用的生命周期方法有:
(1)constructor
- 这个构造方法接收一个props参数,props是从父组件中传入的属性对象,如果父组件中没有传入属性而组件自身定义了默认属性,那么这个props指向的就是组件的默认属性。
- 你必须在这个方法中首先调用super(props)才能保证props被传入组件中。
- constructor通常用于初始化组件的state以及绑定事件处理方法等工作。
(2)componentWillMount
这个方法在组件被挂载到DOM前调用,且只会被调用一次
(3)render
- 在这个方法中,根据组件的props和state返回一个React元素,用于描述组件的UI,通常React元素使用JSX语法定义。
- 需要注意的是,render并不负责组件的实际渲染工作,它只是返回一个UI的描述,真正的渲染出页面DOM的工作由React自身负责。
- render是一个纯函数,在这个方法中不能执行任何有副作用的操作,所以不能在render中调用this.setState,这会改变组件的状态。
(4)componentDidMount
- 在组件被挂载到DOM后调用,且只会被调用一次。
- 这时候已经可以获取到DOM结构,因此依赖DOM节点的操作可以放到这个方法中。
- 这个方法通常还会用于向服务器端请求数据。在这个方法中调用this.setState会引起组件的重新渲染
更新阶段
- 组件被挂载到DOM后,组件的props或state可以引起组件更新。
- props引起的组件更新,本质上是由渲染该组件的父组件引起的,也就是当父组件的render方法被调用时,组件会发生更新过程,这个时候,组件props的值可能发生改变,也可能没有改变,因为父组件可以使用相同的对象或值为组件的props赋值。但是,无论props是否改变,父组件render方法每一次调用,都会导致组件更新。
- State引起的组件更新,是通过调用this.setState修改组件state来触发的。
组件更新阶段,依次调用的生命周期方法有:
(1)componentWillReceiveProps(nextProps)
- 这个方法只在props引起的组件更新过程中,才会被调用。
- State引起的组件更新并不会触发该方法的执行。
- 方法的参数nextProps是父组件传递给当前组件的新的props。
- 父组件render方法的调用并不能保证传递给子组件的props发生变化,也就是说nextProps的值可能和子组件当前props的值相等,因此往往需要比较nextProps和this.props来决定是否执行props发生变化后的逻辑,比如根据新的props调用this.setState触发组件的重新渲染。
注意:
- 在componentWillReceiveProps中调用setState,只有在组件render及其之后的方法中,this.state指向的才是更新后的state。在render之前的方法shouldComponentUpdate、componentWillUpdate中,this.state依然指向的是更新前的state。
- 通过调用setState更新组件状态并不会触发componentWillReceiveProps的调用,否则可能会进入一个死循环,componentWillReceiveProps→this.setState→componentWillReceiveProps→this.setState……
(2)shouldComponentUpdate(nextProps, nextState)
- 这个方法决定组件是否继续执行更新过程。
- 当方法返回true时(true也是这个方法的默认返回值),组件会继续更新过程;
- 当方法返回false时,组件的更新过程停止,后续的componentWillUpdate、render、componentDidUpdate也不会再被调用。
- 一般通过比较nextProps、nextState和组件当前的props、state决定这个方法的返回结果。这个方法可以用来减少组件不必要的渲染,从而优化组件的性能。
(3)componentWillUpdate(nextProps, nextState)
这个方法在组件render调用前执行,可以作为组件更新发生前执行某些工作的地方,一般也很少用到。
(4)render
(5)componentDidUpdate(prevProps, prevState)
组件更新后被调用,可以作为操作更新后的DOM的地方。这个方法的两个参数prevProps、prevState代表组件更新前的props和state。
卸载阶段组件
从DOM中被卸载的过程,这个过程中只有一个生命周期方法:componentDidMount
- 这个方法在组件被卸载前调用,可以在这里执行一些清理工作,比如清除组件中使用的定时器,清除componentDidMount中手动创建的DOM元素等,以避免引起内存泄漏。
- 只有类组件才具有生命周期方法,函数组件是没有生命周期方法的,因此永远不要在函数组件中使用生命周期方法。
事件处理
在React元素中绑定事件有两点需要注意:
(1)在React中,事件的命名采用驼峰命名方式,而不是DOM元素中的小写字母命名方式。例如,onclick要写成onClick,onchange要写成onChange等。
(2)处理事件的响应函数要以对象的形式赋值给事件属性,而不是DOM中的字符串形式
- React中的事件是合成事件,并不是原生的DOM事件。
- 在DOM事件中,可以通过处理函数返回false来阻止事件的默认行为,但在React事件中,必须显式地调用事件对象的preventDefault方法来阻止事件的默认行为。
- 除了这一点外,DOM事件和React事件在使用上并无差别。如果在某些场景下必须使用DOM提供的原生事件,可以通过React事件对象的nativeEvent属性获取。
- 在React组件中处理事件最容易出错的地方是事件处理函数中this的指向问题,因为ES 6 class并不会为方法自动绑定this到当前对象。
React事件处理函数的写法主要有三种方式,不同的写法解决this指向问题的方式也不同:
- 使用箭头函数:
- 因为箭头函数中的this指向的是函数定义时的对象,所以可以保证this总是指向当前组件的实例对象。当事件处理逻辑比较复杂时,如果把所有的逻辑直接写在onClick的大括号内,就会导致render函数变得臃肿,不容易直观地看出组件的UI结构,代码可读性也不好。这时,可以把逻辑封装成组件的一个方法,然后在箭头函数中调用这个方法。
2.使用组件方法:
点击Button的事件响应函数是组件的方法:handleClick。这种方式的好处是:每次render方法的调用,不会重新创建一个新的事件响应函数,没有额外的性能损失。但是,使用这种方式要在构造函数中为作为事件响应的方法(handleClick),手动绑定this: this.handleClick = this.handleClick.bind(this),这是因为ES6 语法的缘故,ES6 Class 的方法默认不会把this绑定到当前的实例对象上,需要我们手动绑定。
使用bind会创建一个新的函数,因此这种写法依然存在每次render都会创建一个新函数的问题。但在需要为处理函数传递额外参数时,这种写法就有了用武之地。
3.属性初始化语法(property initializer syntax)
表单
受控组件
- 如果一个表单元素的值是由React来管理的,那么它就是一个受控组件。
- React组件渲染表单元素,并在用户和表单元素发生交互时控制表单元素的行为,从而保证组件的state成为界面上所有元素状态的唯一来源。
- 文本框文本框包含类型为text的input元素和textarea元素。它们受控的主要原理是:通过表单元素的value属性设置表单元素的值,通过表单元素的o'nChange事件监听值的变化,并将变化同步到React组件的state中。
- 下拉列表select。在React中,通过在select上定义value属性来决定哪一个option元素处于选中状态,不需要关注option元素。
- 复选框和单选框
类型为checkbox的input元素和类型为radio的input元素。通常复选框和单选框的值是不变的,需要改变的是checked状态。因此React控制的属性不再是value,而是checked。