React官方技术文档:
https://reactjs.org/
一、初识React
React是一个用于构建用户界面的JavaScript库,主要用来写HTML页面或构建Web应用。它起源于 Facebook 的内部项目,用来架设 Instagram 的网站,并于 2013 年 5 月开源。React 拥有较高的性能,代码逻辑非常简单。从MVC(M是指业务模型,V是指用户界面,C则是控制器)角度来看,React仅仅是视图层(V),也就是只负责视图的渲染,而并非提供完整的M和C功能
1、React的特点
-
声明式设计: 只需要描述UI(HTML)看起来式什么样子,就跟写HTML一样。React负责渲染UI,并在数据变化时更新UI -
基于组件: 过 React 构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中 -
高效灵活: React通过对DOM的模拟,最大限度地减少与DOM的交互,也可以与已知的库或框架很好地配合 -
JSX: SX 是 JavaScript 语法的扩展,建议在React开发中使用 -
单向响应数据流: React 实现了单向响应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单 -
随处使用: 使用React可以开发Web应用、移动端原生应用(react-native)、VR(react 360)
2、安装React
React 可以直接下载使用,下载包中也提供了很多学习的实例,下载地址 React官网 安装React前需安装node和npm,可使用node -v 和npm -v 查看是否安装及安装的版本。国内npm较慢,可以设置成淘宝的镜像来代替原有的,操作方法为cmd输入命令npm config set registry https://registry.npm.taobao.org 安装React使用命令npm i react react-dom ,这里同时安装了两个包react和react-dom,其中react包是核心,提供创建元素、组件等功能,react-dom包提供DOM相关功能,二者相互配合
3、我的第一个React实例
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<!-- 第一步:引入JS文件,这里需要注意引入顺序 -->
<!-- React核心库 -->
<script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
<!-- 提供与DOM相关的功能 -->
<script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
<!-- Babel 可以将 ES6 代码转为 ES5 代码,这样我们就能在目前不支持 ES6 浏览器上执行 React 代码 -->
<script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<!-- 第二步:创建React元素,并渲染React元素 -->
<script type="text/babel">
const title = React.createElement('h1', null, 'Hello, react!')
ReactDOM.render(title, document.getElementById('root'))
</script>
</body>
</html>
4、React脚手架的使用
4.1 React脚手架的意义
- 脚手架是开发现代Web应用的必备工具
- 充分利用Webpack、Babel、ESLint等工具辅助项目开发
- 零配置,无需手动配置繁琐的工具即可使用
4.2 使用React脚手架初始化项目
- 初始化项目:命令为
npx create-react-app my-app - 启动项目:在创建的根目录下执行命令
npm start - 使用脚手架:使用模块化语法导入react和react-dom包,使用React,此时保存后可以自动重新编译,浏览器自动刷新
- 不自动刷新的解决方案: 在package.json同级目录下新建一个文件名为.env的文件,并添加
FAST_REFRESH=false ,并重新npm start
import React from 'react'
import ReactDOM from 'react-dom'
const title = React.createElement('h1', null, 'Hello, react 脚手!')
ReactDOM.render(title, document.getElementById('root'))
二、JSX语法
1、JSX的基本使用
1.1 creatElemet()的问题
-
繁琐不简洁 -
不直观,无法一眼看出所描述的结构 -
不优雅,用户体验不爽
creatElemet()方法
React.createElement(
'div',
{className: 'shopping-list'},
React.createElement('h1', null, 'Shopping List'),
React.createElement( 'ul',
null,
React.createElement('li', null, 'Instagram'),
React.createElement('li', null, 'WhatsApp')
)
)
JSX诞生
<div className="shopping-list">
<h1>Shopping List</h1>
<ul>
<li>Instagram</li>
<li>WhatsApp</li>
</ul>
</div>
1.2 JSX简介
React 使用 JSX 来替代常规的 JavaScript,它是一种 JavaScript 的语法扩展,其全称是JavaScript XML。
- 它是类型安全的,在编译过程中就能发现错误
- JSX 执行更快,因为它在编译为 JavaScript 代码后进行了优化
- 声明式语法更加直观,与HTML结构相同,使用 JSX 编写模板更加简单快速,降低了学习成本,提升开发效率
1.3 为什么脚手架中可以直接使用JSX语法
- JSX 不是标准的ECMAScript语法,它是ECMAScript的语法拓展
- 需要使用babel编译处理后,才能在浏览器环境中使用
- create-react-app脚手架中已经默认有该配置,无需手动配置
- 编译JSX语法的包: @bable/preset-react
2、JSX基本语法
2.1 JSX注意点
- React元素的属性名使用驼峰命名法
- 特殊属性名:class -> className,for -> htmlFor,tabindex -> tabIndex
- 如果没有子节点(这里包括文本节点)的React元素可以用
/> 来结束 - 可以在 JSX 中使用 JavaScript 表达式,表达式写在花括号 {} 中,从而避免JS中自动插入分号报错
2.2 在JSX中嵌入JS表达式
语法: {JavaScritp表达式}
实例:
ReactDOM.render(
<div>
<h1>{1+1}</h1>
</div>
,
document.getElementById('example')
);
注意点:
- 只要是合法的js表达式都可以进行嵌入
- JSX自身也是js表达式,也可以放入JSX的JS花括号中
- js中的对象是例外,一般不合法,但可以出现在style属性中
- 在{}中不能出现语句,如if、for等
样式:
- 不推荐使用行内样式
<li key={item.id} style={{'color': 'red',"backgroundColor": 'pink'}}>{item.name}</li>
- React 推荐使用内联样式,可以使用 camelCase语法来设置内联样式, React 会在指定元素数字后自动添加 px。
var myStyle = {
fontSize: 100,
color: '#FF0000'
};
ReactDOM.render(
<h1 style = {myStyle}>菜鸟教程</h1>,
document.getElementById('example')
);
注释:
{}
数组:
JSX 允许在模板中插入数组,数组会自动展开所有成员:
var arr = [
<h1>我是数组元素1</h1>,
<h2>我是数组元素2</h2>,
];
ReactDOM.render(
<div>{arr}</div>,
document.getElementById('root')
);
2.3 条件渲染
根据条件渲染特定的JSX结构,可以使用if else、三元运算符、逻辑运算符实现
let isLoading = true
let loading = ()=>{
if(isLoading){
return <div>Loading...</div>
}
return <div>加载完成</div>
}
2.4 列表渲染
- 如果需要渲染一组数据,我们应该使用数组的map() 方法
- 渲染列表的时候需要添加key属性,且key属性的值要保证唯一,注意要尽量避免使用索引号作为key(插入、修改数据等操作时可能引发性能问题),渲染的列表中不会有key,其仅用作React内部
- 原则: map()遍历谁,就给谁添加key属性
const songs =[
{id: 1, name: '两只老虎'},
{id: 2, name: '小红帽'},
{id: 3, name: '小兔子乖乖'}
]
const list = (
<ul>
{songs.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
)
ReactDOM.render(list, document.getElementById('root'))
三、React组件及事件处理
组件表示页面中的部分功能,组合多个组件实现完整的页面功能,其特点有:可复用、独立、可组合
1、创建组件的两种方式
1.1 使用函数创建组件
1. 函数组件:
使用JS的函数创建组件,直接用函数名作为组件标签名,组件标签可以是单标签,也可以是双标签
2. 两个约定:
- 函数名称必须以大写字母开头,否则会报错
- 函数组件必须有返回值,表示该组件的结构,如果返回值为null,表示不渲染任何内容
function Hello() {
return (
<div>这是我的第一个函数组件</div>
)
}
ReactDOM.render(<Hello />, document.getElementById('root'))
1.2 使用类创建组件
1. 类组件:
使用ES6语法的class创建的组件
2. 四个约定:
- 类名称必须要大写字母开头
- 类组件应该继承React.Component父类,从而可以使用父类中提供的方法或者属性
- 类组件必须提供 render 方法
- render方法中必须要有return返回值,返回值为null,表示不渲染任何内容
class Hello extends React.Component{
render(){
return (
<div>这是我的第一个类组件</div>
)
}
}
ReactDOM.render(<Hello />, document.getElementById('root'))
1.3 将创建好的组件抽离到JS文件
组件作为一个独立的个体,一般都会放到一个单独的JS文件中
实例:
- 创建Hello.js
- 在Hello.js 中导入React,创建组件,在Hello.js中导出
import React from 'react'
class Hello extends React.Component {
render(){
return (
<div>我的第一个单独抽离出来的组件Hello</div>
)
}
}
export default Hello
- 在index.js中导入Hello组件,渲染到页面
import Hello from './js/Hello'
ReactDOM.render(<Hello />, document.getElementById('root'))
2、React事件绑定和事件对象
2.1 事件绑定
React 元素的事件处理和 DOM 元素类似。但是有一点语法上的不同:
- React 事件绑定属性的命名采用驼峰式写法,而不是小写,如:
onMouseEnter - 如果采用 JSX 的语法你需要传入一个函数作为事件处理函数,而不是一个字符串(DOM 元素的写法)
- 语法:on + 事件名称 = { 事件处理程序 },如:
onClick = { () => {}}
HTML:
<button onclick="activateLasers()">
激活按钮了
</button>
React:
<button onClick={activateLasers}>
激活按钮了
</button>
2.2 事件对象
- 可以通过事件处理函数的参数e获取到事件对象,而无需为一个已创建的 DOM 元素添加监听器
- React中的事件对象e是一个合成事件,兼容所有浏览器,无需担心跨浏览器兼容问题
HTML:
<a href="#" onclick="console.log('点击链接'); return false">
点击按钮,不会触发默认事件
</a>
React:
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('链接被点击了');
}
return (
<a href="#" onClick={handleClick}>
点击按钮,不会触发默认事件
</a>
);
}
3、React State(状态)
React 把组件看成是一个状态机(State Machines)。状态(State)即数据,通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致。React 里,只需更新组件的 state,然后根据新的 state 重新渲染用户界面(不要操作 DOM)。
3.1 有状态组件和无状态组件
- 函数组件又叫做无状态组件,类组件又叫做有状态组件
- 函数组件没有自己的状态,只负责数据展示(静)
- 类组件有自己的状态,负责更新UI,让页面动起来(动)
计数器就是一个有状态组件:
计数器:0 → 计数器:1
3.2 组件的State和setState
State:
- 状态(State)即数据,是组件内部的私有数据,只能在组件内部使用
- state的值是对象,表示一个组件中可以有多个数据
- 通过
this.state 来获取状态
export default class extends React.Component {
constructor(){
super()
}
state = {
count: 0
}
render(){
return (
<div>计数器 :{this.state.count}</div>
)
}
}
使用setState()修改状态:
- 状态是可变的,通过
this.setState({要修改的数据}) 修改状态,多个数据只需加入需修改的数据 - 不要直接修改state中的值,这是错误的,不能改变数据
- setState() 作用:修改state,并更新UI
- 其思想是:数据驱动视图
export default class extends React.Component {
state = {
count: 0
}
render(){
return (
<div>
<div>计数器 :{this.state.count}</div>
{}
<button onClick={() => {
this.setState({
count: this.state.count+1
})
}}>点我+1</button>
</div>
)
}
}
3.3 从JSX中抽离事件处理程序
JSX中参杂过多JS逻辑代码会显得非常混乱,从中抽出可保证JSX结构清晰。
class Hello extends React.Component {
state = {
count: 0
}
onIncrement(){
this.setState({
count: this.state.count + 1
})
}
render(){
return(
<div>
<div>计数器:{this.state.count}</div>
{}
<button onClick={this.onIncrement}>点我+1</button>
</div>
)
}
}
解决方案有三种:
1.箭头函数:
利用箭头函数自身不绑定的特点,render方法中的this为组件实例,可以获取到setState()
class Hello extends React.Component {
state = {
count: 0
}
onIncrement(){
this.setState({
count: this.state.count + 1
})
}
render(){
return(
<div>
<div>计数器:{this.state.count}</div>
<button onClick={() => this.onIncrement()}>点我+1</button>
</div>
)
}
}
2.Function.prototype.bind():
利用ES5中的bind方法,将事件处理程序中的this与组件实例绑定到一起
class Hello extends React.Component {
construct() {
super()
this.onIncrement = this.onIncrement.bind(this)
}
state = {
count: 0
}
onIncrement(){
this.setState({
count: this.state.count + 1
})
}
render(){
return(
<div>
<div>计数器:{this.state.count}</div>
<button onClick={this.onIncrement}>点我+1</button>
</div>
)
}
}
3.class的实例方法(推荐):
利用箭头函数形式的class实例方法解决this指向问题,该语法是实验性语法,但由于babel的存在可以使用
class Hello extends React.Component {
state = {
count: 0
}
onIncrement = () => {
this.setState({
count: this.state.count + 1
})
}
render(){
return(
<div>
<div>计数器:{this.state.count}</div>
{}
<button onClick={this.onIncrement}>点我+1</button>
</div>
)
}
}
4、表单处理
4.1受控组件
受控组件即值受react控制的表单元素。HTML中的表单元素是可输入的,也就是有自己的可变状态,React将state与表单元素值value绑定在一起,由state的值来控制表单元素的值
使用步骤:
- 在state中添加一个状态,作为表单元素的value值
- 给表单元素绑定change事件,将表单元素的值设置为state的值
class App extends React.Component {
constructor(){
super()
this.inputChange = this.inputChange.bind(this)
}
state = {
txt : ''
}
inputChange(e){
this.setState({
txt: e.target.value
})
}
render(){
console.log(this.state);
return (
<div>
{}
<input type="text" value={this.state.txt} onChange={this.inputChange}/>
</div>
)
}
}
ReactDOM.render(<App />,document.getElementById('root'))
多表单元素优化:
每个表单元素都有一个单独的事件处理函数,这样太繁琐,可以使用一个事件处理程序同时处理多个表单元素
使用步骤:
- 给表单元素添加name属性(用来区分是哪一个表单),名称与state相同(用来更新数据的)
- 根据表单内容来获取对应值
- 在change事件处理程序中通过
[name] (结构,将变量用用作属性名)来修改对应的state
inputChange(e){
let target = e.target;
let value = target.type == 'checkbox' ? target.checked : target.value;
this.setState({
[e.target.name]: value
})
}
<input type="text" value={this.state.txt} name="txt" onChange={this.inputChange}/>
<input type="checkbox" value={this.state.isChecked} name="isChecked" onChange={this.inputChange}/>
4.2 非受控组件
非受控组件借助于ref,使用原生DOM的方式来获取表单元素的值,ref在其中起到获取DOM或组件的作用(直接操作DOM,不推荐)
使用步骤:
- 调用
React.createRef() 方法创建ref对象
constructor() {
super()
this.txtRef = React.createRef()
}
<input type="text" ref={this.txtRef}></input>
Consle.log(this.txtRef.current.value)
四、组件通讯
组件是独立且封闭的单元,默认情况下,只能使用组件自己的数据。在组件化过程中,我们将一个完整的功能拆分成多个组件,以更好的完成整个应用的功能。而在这个过程中,多个组件之间不可避免的要共享某些数据。为了实现这些功能,就需要打破组件的独立封闭性,让其与外界沟通,这个过程就是组件通讯
1、组件的props
1.1 组件如何接收数据
- 组件是封闭的,要接收外部数据应该通过props来实现
- 函数组件通过参数props接收数据,类组件通过this.props接收数据
- 传递数据的方式是给组件标签添加属性
传递数据:
<Hello name="jack" age={21} />
函数组件接收数据:
function Hello(props) {
congsole.log(props)
return (
<div>接收到的数据为:{props.name}</div>
)
}
类组件接收数据:
class Hello extends React.Component {
render() {
return (
<div>接收到的数据为:{this.props.age}</div>
)
}
}
1.2 props的特点
- 传递的数据可以是任意类型(字符串、数组、函数、数值、JSX、对象等),字符串使用
"" ,其他类型使用{} - props是只读属性,不能对值进行修改
- 使用类组件时,如果写了构造函数,应该将props传递给super(),否则,无法在构造函数中获取到props,其他的地方是可以拿到
含构造函数的类组件使用props:
class Hello extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div>接收到的数据为:{this.props.age}</div>
)
}
}
2、组件通讯的几种方式
2.1 父组件传递数据给子组件
- 父组件提供要传递的state数据
- 给子组件标签添加属性,值为state中的数据
- 子组件中通过props接收父组件中传递的数据
父组件传递数据给子组件:
class Parent extends React.Component {
state = {
lastName: hao
}
render() {
return (
<div>
传递数据给子组件:
<Child name={this.state.lastName}/>
</div>
)
}
}
子组件接收数据:
function Child(props) {
return <div>子组接收数据:{props.name}/></div>
}
2.2 子组件传递数据给父组件:
- 利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数
- 父组件提供一个回调函数,用来接收数据
- 将该函数作为属性的值,传递给子组件
父组件提供回调函数:
class Parent extends React.Component {
getChildMsg = (msg) => {
console.log('接收到子组件的数据', msg)
}
render() {
return (
<div>
子组件:
<Child getMsg={this.getChildMsg}/>
</div>
)
}
}
子组件通过props调用回调函数:
class Child extends React.Component {
state = { childMsg: 'React' }
handleClick = () => {
this.props.getMsg(this.state.childMsg)
}
return (
<button onClick={this.handleClick}>点我给父组件传递数据</button>
)
}
2.3 兄弟组件之间的通讯
- 将共享状态(数据)提升到最近的公共父组件中,由公共父组件管理这个状态,即状态提升
- 公共父组件职责: 1. 提供共享状态 2.提供操作共享状态的方法
- 要通讯的子组件只需要通过props接收状态或操作状态的方法
demo:
- 定义布局结构,一个Counter里面包含两个子组件,一个是计数器的提示,一个是按钮
class Counter extends React.Component {
render() {
return (<div>
<Child1 />
<Child2 />
</div>
)
}
}
class Child1 extends React.Component {
render() {
return (
<h1>计数器:</h1>
)
}
}
class Child2 extends React.Component {
render() {
return (
<button>+1</button>
)
}
}
- 在父组件里定义共享状态,把这个状态传递给第一个子组件
class Counter extends React.Component {
state = {
count: 0
}
render() {
return (<div>
{}
<Child1 count={this.state.count}/>
<Child2 />
</div>
)
}
}
class Child1 extends React.Component {
render() {
return (
<h1>计数器:{this.props.count}</h1>
)
}
}
- 在父组件中提供共享方法,通过属性传递给第二个子组件,方便第二个子组件来进行调用
onIncrement = (res) => {
this.setState({
count: this.state.count + res
})
}
render() {
return (<div>
...
{}
<Child2 onIncrement={this.onIncrement} />
</div>
)
}
- 在第二个子组件里面通过props来获取到对应函数,然后进行调用
class Child2 extends React.Component {
handleClick = () => {
this.props.onIncrement(2)
}
render() {
return (
<button onClick={this.handleClick}>+</button>
)
}
}
2.4 Context
如果出现层级比较多的情况下(例如:爷爷传递数据给孙子),我们会使用Context来进行传递,其作用是跨组件传递数据
步骤:
- 调用
React.createContext() 创建 Provider(提供数据)和 Consumer(消费数据) 两个组件
const {Provider, Consumer} = React.createContext()
- 使用Provider 组件作为父节点,并设置value属性,表示要传递的数据
<Provider value="pink">
<div className="App">
<Child1 />
</div>
</Provider>
- 哪一层想要接收数据,就用Consumer进行包裹,在里面回调函数中的参数就是传递过来的值
<Consumer>
{data => <span>data参数表示接收到的数据:{data}</span>}
</Consumer>
3、props进阶
3.1 children属性
- children属性: 表示组件标签的子节点,当组件标签有子节点时,props就会有该属性
- children属性与普通的props一样,值可以是任意值(文本、react元素、组件、甚至是函数)
function Hello(props) {
return (
<div>
组件的子节点:{props.chidren}
</div>
)
}
<Hello>我是子节点</Hello>
3.2 props校验
对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据,简单来说就是组件调用者可能不知道封装的组件需要什么样的数据,如果传入的数据不对,可能会导致报错,这个时候就需要用到props校验了
使用步骤:
- 安装prop-types包:
npm i props-types - 导入prop-types 包
- 使用
组件名.propTypes={} 来给组件的props添加校验规则 - 校验规则通过PropTypes对象来指定
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
function App(props) {
return (
<h1>Hi, {props.colors}</h1>
)
}
App.propTypes = {
colors: PropTypes.array
}
ReactDOM.render(<App colors={7} />, document.getElementById('root'))
常见约束规则:
- 常见类型:array、bool、number、string、object、func
- React元素类型(element)
- 必填项:isrequired
- 特定结构的对象
App.propTypes = {
optionalFunc: PropTypes.func,
requiredFunc: PropTypes.func.isRequired,
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
})
}
3.3 props的默认值
function App(props) {
return (
<div>
props默认值:{props.pageSize}
</div>
)
}
App.defaultProps = {
pageSize: 10
}
ReactDOM.render(<App />, document.getElementById('root'))
五、组件的生命周期
组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程就是组件的生命周期。生命周期的每个阶段总是伴随着一些方法调用,这些方法就是生命周期的钩子函数。钩子函数为开发人员在不同阶段操作组件提供了时机。掌握组件的生命周期有助于理解组件的运行方式,完成更复杂的组件功能、分析组件错误原因等。只有类组件才有生命周期。
1、组件生命周期的三个阶段
1.1 创建时(挂载阶段)
- 执行时机:组件创建时(页面加载时)
- 执行顺序:
constructor →render →componentDidMount
钩子函数 | 触发时机 | 作用 |
---|
constructor | 创建组件时,最先执行 | 1.初始化state;2.为事件处理程序绑定this | render | 每次组件渲染都会触发 | 渲染UI( 注意: 不能调用setState() ,无限递归) | componentDidMount | 组件挂载(完成DOM渲染)后 | 1.发送网络请求;2.DOM操作 |
1.2 更新时
- 执行时机:setState()、 forceUpdate()、 组件接收到新的props,以上三者任意一种变化,组件就会重新渲染
- 执行顺序:
render →componentDidMount
钩子函数 | 触发时机 | 作用 |
---|
render | 每次组件渲染都会触发 | 渲染UI( 与挂载阶段是同一个render) | componentDidMount | 组件挂载(完成DOM渲染)后 | 1.发送网络请求;2.DOM操作;(注意: 如果要setState()必须放在应该条件判断中,避免无限递归,可判断props是否相同) |
1.3 卸载时
钩子函数 | 触发时机 | 作用 |
---|
compontentWillUnmount | 组件卸载(从页面中消失) | 执行清理工作(如清理定时器) |
1.4 不常用的钩子函数
五、render-props和React高阶组件
如果两个组件中的部分功能相似或相同,如何复用相似的功能?需要复用哪些?由哪些方式?我们可以通过render props模式和高阶组件(HOC)复用state和操作state的方法实现功能复用。这两种方式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式
1、render-props模式
使用步骤:
-
创建Mouse组件,在组件中提供复用的逻辑代码 -
将要复用的状态作为 props.render(state)方法的参数,暴露到组件外部(注意: 并不是该模式叫 render props就必须使用名为render的prop,实际上可以使用任意名称的prop,我们把prop是一个函数并且告诉组件要渲染什么内容的技术叫做render props模式,推荐使用children代替render属性,更加直观) -
使用props.render() 的返回值作为要渲染的内容
class Mouse extends React.Component {
state = {
x: 0,
y: 0
}
handleMouseMove = e => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
render() {
return this.props.render_children(this.state)
}
}
class App2 extends React.Component {
render() {
return (
<div>
<h1> render props模式</h1>
{}
{}
<Mouse render_children={(mouse) => {
return (
<p>
鼠标位置:{mouse.x} {mouse.y}
</p>
)
}} />
{}
{}
<Mouse render_children={mouse => {
return <img src={img} alt="小天使" style={{
position: 'absolute',
top: mouse.y - 40,
left: mouse.x - 40
}} />
}} />
</div>
)
}
}
ReactDOM.render(<App2 />, document.getElementById('root'))
2、高阶组件HOC
使用步骤:
- 创建一个函数,名称约定以with开头,指定函数参数,参数应该以大写字母开头(因为参数是要作为渲染的组件)
- 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
- 在该组件中,渲染参数组件,同时将状态通过prop传递给参数组件
- 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面
包装函数
function withMouse(WrappedComponent) {
class Mouse extends React.Component {
state = {
x: 0,
y: 0
}
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
render() {
return <WrappedComponent {...this.state} />
}
}
return Mouse
}
哪个组件需要加强,通过调用withMouse 这个函数,然后把返回的值设置到父组件中即可
const Position = props => (
<p>
鼠标当前位置:(x: {props.x},y:{props.y})
</p>
)
const Angel = props => (
<img src={img} alt="小天使" style={{
position: 'absolute',
top: props.y - 40,
left: props.x - 40
}} />
)
const MousePosition = withMouse(Position)
const MouseAngel = withMouse(Angel)
class App extends React.Component {
render() {
return (
<div>
<h1>高阶组件</h1>
{}
<MousePosition />
<MouseAngel />
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
tips:设置displayName
displayName 用于设置调试信息(React Developer Tools信息。默认情况下,React使用组件名称作为displayName ,那么通过高阶组件构造的两个组件的名称就是相同的,如上在调试时,会显示由两个Mouse组件。为高阶组件设置displayName ,便于在调试时区分不同的组件。
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
传递props
如果没有传递props,会导致props((组件的属性))丢失问题, 渲染WrappedComponent 时,将state和props一起传递给组件
<WrappedComponent {...this.state} {...this.props} />
六、React原理
1、setState()
-
setState() 更新数据是异步的,那么使用该语法,后面的setState 不要依赖前面setState 的值,多次调用setState ,只会触发一次render -
推荐语法: setState((state,props) => {}) ,参数state:表示最新的state,参数props表示最新的props,同样是异步的
setState() 更新数据是异步的
handleClick = () => {
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
console.log('count:', this.state.count)
}
使用推荐语法后的效果,虽然同样是异步更新的,该方法的不同点是setState的参数是回调函数,后边是基于前边的
handleClick = () => {
this.setState((state, props) => {
return {
count: this.state.count + 1
}
})
this.setState((state, props) => {
return {
console.log(state.count)
count: this.state.count + 1
}
})
console.log('count:', this.state.count)
}
setState(update, [callback]) ,在状态更新(页面完成重新渲染)后立即执行某个操作,可以操作DOM
2、JSX语法的转化过程
- JSX仅仅是
createElement() 方法的语法糖(简化语法) - JSX语法被 @babel/preset-react 插件编译为
createElement() 方法 - React 元素: 是一个对象,用来描述你希望在屏幕上看到的内容
3、组件更新机制及性能优化
3.1 组件更新机制
我们都知道setState()的两个作用分别是修改state和更新组件,整个过程中,父组件重新渲染时,也会重新渲染子组件,但只会渲染当前组件子树(当前组件以其所有子孙组件)
3.2 组件性能优化
-
减轻state: 只存储跟组件渲染相关的数据(比如:count/ 列表数据 /loading等),不用做渲染的数据不要放在state中,对于这种需要在多个方法中用到的数据,应该放到this中 -
避免不必要的重新渲染: 父组件更新会引起子组件也被更新,问题是子组件没有任何变化时也会重新渲染,通过使用钩子函数shouldComponentUpdate(nextProps, nextState) ,根据条件决定是否程序渲染组件
nextProps 和nextState 是最新的状态以及属性,自身的用state,接收到的用props- 通过返回值判断是否需要重新渲染,返回值为
true 代表需要重新渲染,返回值为false 代表不需要重新渲染 - 这个钩子函数的触发事件为更新阶段,组件重新渲染前执行(shouldComponentUpdate → render)
-
纯组件: React.PureComponent 与 React.Component 功能相似,但PureComponent 内部自动实现了 shouldComponentUpdate钩子函数,不需要手动比较。其内部通过分别比对前后两次 props和state的值,来决定是否重新渲染组件
减轻state:
class Hello extends Component {
componentDidMount() {
this.timerId = setInterval(() => {}, 2000)
}
componentWillUnmount() {
clearInterval(this.timerId)
}
render() {...}
}
避免不必要的重新渲染:
class Hello extends Component {
shouldComponentUpdate() {
return false
}
render() {...}
}
纯组件:
class Hello extends React.PureComponent {
render() {
return (
<div>纯组件</div>
)
}
}
纯组件实现原理:
纯组件内部的对比是shallow compare(浅层对比),对于值类型来说比较两个值是否相同,对于引用类型来说只比对对象的引用地址是否相同。因此要注意:state 或 props 中属性值为引用类型时,应该创建新数据,不要直接修改原数据
4、 虚拟DOM和Diff算法
React更新视图的思想是只要state变化就重新渲染视图,但组件中只有一个DOM元素需要更新时,也得把整个组件的内容重新渲染的话显然效率不高,为了提高效率只更新变化的地方才是理想的,在React中就是运用虚拟DOM 配合Diff 算法完成的
1、虚拟DOM
本质上就是一个JS对象,用来描述你希望在屏幕上看到的内容
2、Diff算法
执行过程
- 初次渲染时,React会根据初始化的state(model),创建一个虚拟DOM对象(树)
- 根据虚拟DOM生成真正的DOM,渲染到页面
- 当数据变化后(setState()),会重新根据新的数据,创建新的虚拟DOM对象(树)
- 与上一次得到的虚拟DOM对象,使用Diff算法比对(找不同),得到需要更新的内容
- 最终,React只将变化的内容更新(patch)到DOM中,重新渲染到页面
*详细算法解析参见博文
七、React路由基础
现代的前端应用大多数是SPA(单页应用程序),也就是只有一个HTML页面的应用程序。因为它的用户体验更好、对服务器压力更小,所以更受欢迎。为了有效的使用单个页面来管理多页面的功能,前端路由应运而生。前端路由的作用就是让用户从一个视图(页面)导航到另一个视图(页面)。它是一套映射规则,在React中,是URL路径与组件的对应关系,使用React路由简单来说,就是配置路径和组件
1、路由的基本使用
1.1 安装React路由
npm install react-router --save 安装路由npm install react-router-dom --save 安装路由DOM
1.2 使用步骤
- 导入路由的三个核心组件: Router/Route/Link
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'
const App = () => (
<Router>
<div>
<h1>React 路由</h1>
</div>
</Router>
)
<Link to="/first">页面一</Link>
- 使用Route组件配置路由规则和要展示的组件(路由出口)
const First = () => <p>我是页面一</p>
const App = () => (
<Router>
<div>
<h1>React 路由</h1>
</div>
{}
<Link to="/first">页面一</Link>
{}
<Route path="/first" component={First} />
</Router>
)
ReactDOM.render(<App />, document.getElementById('root'))
1.3常用组件说明
- Router组件: 包裹整个应用,一个React应用只需要使用一次
- 两种常用的Router: HashRouter和BrowserRouter
- HashRouter: 使用URL的哈希值实现 (localhost:3000/#/first),带#不推荐使用
- BrowserRouter(推荐): 使用H5的history API实现(localhost3000/first)
- Link组件: 用于指定导航链接(a标签)
- 最终Link会编译成a标签,而to属性会被编译成 a标签的href属性
- Route组件: 指定路由展示组件相关信息
- path属性: 路由规则,这里需要跟Link组件里面to属性的值一致
- component属性: 展示的组件
- 组件位置: Route写在哪,渲染出来的组件就在哪
1.4 路由指向过程
- 当我们点击Link组件的时候,修改了浏览器地址栏中的url
- React路由监听地址栏url的变化
- React路由内部遍历所有的Route组件,拿着Route里面path规则与url的pathname进行匹配
1.5. 默认路由
- 默认路由: 表示进入页面时就会匹配的路由
- 默认路由:只需要把path设置为
'/'
2、编程式导航
- 场景: 点击登陆按钮,登陆成功后,通过代码跳转到后台首页,如何实现?
- 编程式导航: 通过JS代码来实现页面跳转
- history是React路由提供的,用于获取浏览器历史记录的相关信息
- push(path): 跳转到某个页面,参数path表示要跳转的路径
- go(n):前进或后退功能,参数n表示前进或后退页面数量
class Login extends Component {
handleLogin => () {
this props.history.push('/home')
}
render() {...}
}
3、匹配模式
3.1 模糊匹配模式
- 当Link组件的to属性值为
'/login' 时候,为什么默认路由也被匹配成功? - 默认情况下,React路由是模糊匹配模式
- 模糊匹配规则:只要pathname以path开头就会匹配成功
3.2 精准匹配
- 默认路由认可情况下都会展示,如果避免这种问题?
- 给Route组件添加exact属性,让其变为精准匹配模式
- 精确匹配:只有当path和pathname完全匹配时才会展示改路由
|