预习视频
【最新】零基础快速入门React 17.x
视频P56-P73
今日学习目标
1、掌握函数式组件
2、掌握Hooks的写法
3、掌握Fragment
4、了解错误边界
5、掌握PureComponent
6、了解HOC高阶组件
7、掌握LazyLoad与Suspense、fallback
一、React Hooks介绍【了解】
官网地址:https://react.docschina.org/docs/hooks-intro.html
二、函数式组件【掌握】
纯函数组件有以下特点:
因存在如上特点,使得纯函数组件只能做UI展示的功能, 涉及到状态的管理与切换就不得不用到类组件或者redux。 但因为简单的页面也是用类组件,同时要继承一个React实例,使得代码会显得很重。
以前我们可以使用class来声明一个组件,其实使用function也可以定义一个组件:
创建 App1.js
:
import React from 'react'
function App1(){
return (
<div>
<h1>function 声明的App</h1>
</div>
)
}
export default App1;
备注:
在vscode中,如果安装过 ES7 React/Redux/GraphQL/React-Native snippets
这个插件,即可直接使用 rfc
快捷键敲出以下模板:
import React from 'react';
const App = () => {
return (
<div>
</div>
);
}
export default App;
在 index.js
中调用:
import ReactDOM from 'react-dom'
import App from './App1'
ReactDOM.render(
<App />,
document.getElementById('root')
)
效果如下:
三、常用Hooks【掌握】
按照我们之前的类声明编程,写一个累加器如下:
import React, { Component } from 'react'
export default class App extends Component {
constructor(p){
super(p)
this.state = {num: 0}
}
render() {
return (
<div>
<h2>{this.state.num}</h2>
<button onClick={this.addNum.bind(this)}>累加</button>
</div>
)
}
addNum(){
this.setState({
num: this.state.num+1
})
}
}
效果如下:
1、useState
现在我们改成函数式编程:
// useState就是hooks提供的一个api
import React, { useState } from 'react'
function App(){
// 这里useState(0)中的0,就是定义num的初始值
const [num, setNum] = useState(0);
return (
<div>
<h2>{num}</h2>
<button onClick={()=>{setNum(num+1)}}>累加</button>
</div>
)
}
export default App;
累加效果一样可以实现。
2、Hooks开发注意点【注意】
Hooks的定义不能放在条件判断的语句中,如:
if(true){
const [num, setNum] = useState(0);
}
这种写法会报错:
这里注意:
Hooks的API只能在函数内的最外层声明。
3、useEffect
React函数式编程没有生命周期,因此需要借助useEffect来实现。
* componentDidMount与componentDidUpdate
如果使用class编程,我们检测数据变化就需要这么做:
import React, { Component } from 'react'
export default class App extends Component {
constructor(p){
super(p)
this.state = {num: 0}
}
render() {
return (
<div>
<h2>{this.state.num}</h2>
<button onClick={this.addNum.bind(this)}>累加</button>
</div>
)
}
componentDidMount(){
console.log('Mount')
}
componentDidUpdate(){
console.log('Update')
}
addNum(){
this.setState({
num: this.state.num+1
})
}
}
结合componentDidMount与componentDidUpdate两个生命周期函数来检测。
我们来看看function编程与Hooks怎么解决这一问题:
import React, { useState, useEffect } from 'react'
function App1(){
const [num, setNum] = useState(0);
useEffect(()=>{
console.log('useEffect')
})
return (
<div>
<h2>{num}</h2>
<button onClick={()=>{setNum(num+1)}}>累加</button>
</div>
)
}
export default App1;
一个useEffect就可以替代两个生命周期函数:
这里注意:
useEffect是异步的,不会阻断试图更新。
如果有两个字段都在页面中有更新,但只想指定其中某个来检测,那么可以给 useEffect
添加第二个参数,第二个参数是个数组,数组为空时,表示不检测,数组项为某个字段名称时,表示只检测指定字段的更新:
import {useState, useEffect} from 'react'
function App1(){
const [num, setNum] = useState(0)
const [num1, setNum1] = useState(1)
useEffect(()=>{
console.log('视图更新了')
}, [num]) // 这里只检测num更新,num1的更新不会检测到
return (
<div>
<h1>{num}</h1>
<button onClick={()=>setNum(num+1)}>累加</button>
<h1>{num1}</h1>
<button onClick={()=>setNum1(num1+1)}>累加</button>
</div>
)
}
export default App1;
* componentWillUnmount
当我们需要在组件销毁时做一些清除工作,就需要使用componentWillUnmount。但函数式编程没有这个生命周期,因此也同样需要借助useEffect实现。
由于这里需要做页面的销毁,在 index.js
中最后添加:
setTimeout(()=>{
ReactDOM.render(
<p>你好</p>,
document.getElementById('root')
)
}, 3000)
创建 App2.js
:
import {useState, useEffect} from 'react'
export default function App2() {
let [num, setNum] = useState(0);
let [num1, setNum1] = useState(0);
// 这个hook检测num更新
useEffect(()=>{
console.log('渲染了num');
}, [num]) // [num] 代表这个useEffect只检测num这个字段的更新
// 这个hook检测离开组件
useEffect(()=>{
return ()=>{
console.log('离开了App1组件')
}
},[]) // [] 代表这个useEffect不检测任何一个字段更新
return (
<div>
<h2>num:{num}</h2>
<button onClick={() => setNum(num+1)}>累加</button>
<hr />
<h2>num1:{num1}</h2>
<button onClick={() => setNum1(num1+1)}>累加</button>
</div>
)
}
⚠️ 这里注意:
同个useEffect下,在检测销毁和检测字段更新之间,只能二选一。留下空数组,可以检测到return中的销毁。数组非空时,视图更新会带动return返回值,因此如果要检测字段更新,就无法检测销毁。
4、useContext
函数式的组件,如果需要父传子,那么就需要使用useContext。
* 用法一:
useContext有两种用法,第一种是使用useContext来调用上下文内容。代码如下:
/*
createContext用于创建上下文
useContext用于调用上下文
*/
import {useState, createContext, useContext} from 'react'
// 1、创建上下文
const NumContext = createContext();
// 子组件
function Count(){
// 3、调用上下文内容
const num = useContext(NumContext)
return (
<h3>{num}</h3>
)
}
function App3(){
const [num, setNum] = useState(0)
return (
<>
{/* 2、设置提供器,并传入value,包含子组件 */}
<NumContext.Provider value={num}>
<Count />
</NumContext.Provider>
<button onClick={()=>{setNum(num+1)}}>累加</button>
</>
)
}
export default App3;
* 用法二:
使用 Consumer
来接收数据,这种方法还可以使用 Chrome 扩展 react-context-devtool3.2.crx
来观测数据。
首先,将 react-context-devtool3.2.crx
直接拽入 Chrome 的扩展中。
扩展下载链接: https://pan.baidu.com/s/1ejGlj6_Sierd6w6GQ74mYw 提取码: 1wv6
其次,安装 react-context-devtool
:
npm install react-context-devtool
# or
yarn add react-context-devtool
然后在项目入口文件 src>index.js
中:
// 将原本代码改写为:
import ReactDOM from 'react-dom'
import App from './App5'
// 引入react-context-devtool
import { debugContextDevtool } from 'react-context-devtool';
let container = document.getElementById('root')
ReactDOM.render(
<App />,
container
)
debugContextDevtool(container);
这里注意:
debugContextDevtool方法有第二个参数,是用来传入配置项的。主要配置项有:
Name
Type
Default
Description
enable/disable useReducer debug
enable/disable context debug
disable react-context-devtool including manual mode
但我们直接忽略不填就行了。具体参考网址:
Debug ReactJS Context and useReducer hook with React Context Devtool
引入组件并在Provider使用:
import { ContextDevTool } from 'react-context-devtool';
<NumContext.Provider value={num}>
<ContextDevTool context={NumContext} />
<Sub />
</NumContext.Provider>
然后在子组件中修改:
const Sub = () => {
return (
<NumContext.Consumer>
{
num => {
if (window._REACT_CONTEXT_DEVTOOL) {
window._REACT_CONTEXT_DEVTOOL({ num });
}
return <h1>{num}</h1>;
}
}
</NumContext.Consumer>
)
}
* 最终代码:
重新修改 App3.jsx
:
import React, {useState, useContext, createContext} from 'react';
import { ContextDevTool } from 'react-context-devtool';
// 创建上下文
const NumContext = createContext()
const Sub = () => {
// const num = useContext(NumContext)
return (
<NumContext.Consumer>
{
num => {
if (window._REACT_CONTEXT_DEVTOOL) {
window._REACT_CONTEXT_DEVTOOL({ id: 'uniqContextId', displayName: 'Context Display Name', num });
}
return <h1>{num}</h1>;
}
}
</NumContext.Consumer>
)
}
const App5 = () => {
const [num, setNum] = useState(0);
return (
<div>
<NumContext.Provider value={num}>
<ContextDevTool context={NumContext} id="uniqContextId" displayName="Context Display Name" />
<Sub />
</NumContext.Provider>
<button onClick={()=>setNum(num+1)}>累加</button>
</div>
);
}
export default App5;
5、useReducer
useReducer与useContext不太一样,有点类似于Redux。
import {useReducer} from 'react'
// 1、定义一个reducer
function numReducer(state, action){
// 3、进行一步深拷贝,因为state是不允许直接修改的
let newState = JSON.parse(JSON.stringify(state));
switch(action.type){
case 'add':
newState.num++;
return newState;
case 'cut':
newState.num--;
return newState;
default:
return newState;
}
}
function App4(){
// 2、使用reducer并解构出state与dispatch, {num:0}表示state的默认值
const [state, dispatch] = useReducer(numReducer, {num: 0});
return (
<>
<h2>{state.num}</h2>
{/* 使用dispatch调用 */}
<button onClick={()=>{dispatch({type: 'add'})}}>累加</button>
<button onClick={()=>{dispatch({type: 'cut'})}}>减少</button>
</>
)
}
export default App4;
6、useContext结合useReducer实现redux
* 第一步
将以上 1、定义一个reducer
这一步的函数抽离到 store>reducer.js
中:
// 1、定义一个reducer
export const numReducer = function(state, action){
// 3、进行一步深拷贝,因为state是不允许直接修改的
let newState = JSON.parse(JSON.stringify(state));
switch(action.type){
case 'add':
newState.num++;
return newState;
case 'cut':
newState.num--;
return newState;
default:
return newState;
}
}
* 第二步
引入reducer,并引入useContext与createContext
import {useReducer, useContext, createContext} from 'react'
import {numReducer} from './store/reducer'
// 创建上下文
const NumContext = createContext()
* 第三步
定义Sub1和Sub2子组件,Sub1用于累加,Sub2用于减少。
// 定义Sub1子组件
function Sub1(){
const dispatch = useContext(NumContext) // 从提供器中传过来
return <button onClick={()=>{dispatch({type: 'add'})}}>累加</button>
}
// 定义Sub2子组件
function Sub2(){
const dispatch = useContext(NumContext)
return <button onClick={()=>{dispatch({type: 'cut'})}}>减少</button>
}
* 第四步
在App4组件中调用Sub1和Sub2,并使用Provider:
function App4(){
// 2、使用reducer并解构出state与dispatch, {num:0}表示state的默认值
const [state, dispatch] = useReducer(numReducer, {num: 0});
return (
<NumContext.Provider value={dispatch}>
<h2>{state.num}</h2>
{/* 使用dispatch调用 */}
<Sub1 />
<Sub2 />
</NumContext.Provider>
)
}
* 完整代码
import {useReducer, useContext, createContext} from 'react'
import {numReducer} from './store/reducer'
// 创建上下文
const NumContext = createContext()
// 定义Sub1子组件
function Sub1(){
const dispatch = useContext(NumContext)
return <button onClick={()=>{dispatch({type: 'add'})}}>累加</button>
}
// 定义Sub2子组件
function Sub2(){
const dispatch = useContext(NumContext)
return <button onClick={()=>{dispatch({type: 'cut'})}}>减少</button>
}
function App4(){
// 2、使用reducer并解构出state与dispatch, {num:0}表示state的默认值
const [state, dispatch] = useReducer(numReducer, {num: 0});
return (
<NumContext.Provider value={dispatch}>
<h2>{state.num}</h2>
{/* 使用dispatch调用 */}
<Sub1 />
<Sub2 />
</NumContext.Provider>
)
}
export default App4;
* 提升:
如果还想把h2标签抽离成组件,那么代码要改为:
import {useReducer, useContext, createContext} from 'react'
import {numReducer} from './store/reducer'
// 创建上下文
const NumContext = createContext()
// 定义Sub1子组件
function Sub1(){
const {dispatch} = useContext(NumContext)
return <button onClick={()=>{dispatch({type: 'add'})}}>累加</button>
}
// 定义Sub2子组件
function Sub2(){
const {dispatch} = useContext(NumContext)
return <button onClick={()=>{dispatch({type: 'cut'})}}>减少</button>
}
// 定义Sub3子组件——h2标签组件
function Sub2(){
const {state} = useContext(NumContext)
return <h2>{state.num}</h2>
}
function App4(){
// 2、使用reducer并解构出state与dispatch, {num:0}表示state的默认值
const [state, dispatch] = useReducer(numReducer, {num: 0});
return (
{/* 这里注意:value中要传对象过去context,因此useContext获得的内容要解构 */}
<NumContext.Provider value={{state, dispatch}}>
<Sub3 />
{/* 使用dispatch调用 */}
<Sub1 />
<Sub2 />
</NumContext.Provider>
)
}
export default App4;
这里再提供Consumer写法:
可以把子组件的useContext改为:
function Sub1() {
// const {dispatch} = useContext(NumContext)
return (
<NumberContext.Consumer>
{
({dispatch}) => {
return <button onClick={() => dispatch({ type: 'addNum', value: 1 })}>累加</button>
}
}
</NumberContext.Consumer>
)
}
function Sub2() {
// const {state} = useContext(NumContext)
return (
<NumberContext.Consumer>
{
({state}) => {
return <h2>{state.num}</h2>
}
}
</NumberContext.Consumer>
)
}
7、useRef
ref就是类似于id的属性,用于获取dom元素,比较简单:
import {useRef} from 'react'
function App5(){
const element = useRef(null)
const handleClick = () => {
console.log(element.current) // 获取input
console.log(element.current.value) // 获取到input中的值
}
return (
<>
<input type="text" ref={element} />
<button onClick={handleClick}>获取input标签</button>
</>
)
}
export default App5;
除此之外还有useMemo等多个Hooks,但我们没必要全部掌握,了解常用的即可。
四、Fragment与空标签【掌握】
组件中每个return都必须包含一个根标签,通常我们会使用 <div></div>
,但这也比较麻烦,毕竟有时真的不想套一个div,那我们可以这样:
// 使用空标签
function Sub2(){
const dispatch = useContext(NumContext)
return (
<>
<button>减少</button>
</>
)
}
或者是:
import {Fragment} from 'react'
// 使用Fragment,Fragment不会渲染成为标签节点
function Sub2(){
const dispatch = useContext(NumContext)
return (
<Fragment>
<button>减少</button>
</Fragment>
)
}
* 注意:
<></> 语法不能接受键值或属性,而 <Fragment>
可以。key 和 children 是唯一可以传递给 Fragment
的属性,主要用在做for循环时可以拿key来做唯一标识。未来可能会添加对其他属性的支持,例如事件。
五、错误边界【了解】
官网地址:https://react.docschina.org/docs/error-boundaries.html
错误边界是一种 React 组件,可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且渲染出备用 UI。错误边界在渲染期间、生命周期和整个组件树的构造函数中捕获错误。
它无法在以下场景捕获错误:
异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
错误边界有两种,一种是componentDidCatch()
,另一种是 static getDerivedStateFromError
。这是两种生命周期方法,只要class组件用了任意一种,那它就变成一个错误边界组件。
1、componentDidCatch()
声明一个错误边界组件 ErrorBoundary.jsx
,这个组件可以直接应用到全局
import React, { Component } from 'react'
export default class ErrorBoundary extends Component {
state = {
hasError: false
}
// 捕获错误
componentDidCatch(error, errorInfo){
this.setState({
error,
errorInfo,
hasError: true
})
}
render() {
if(this.state.hasError){
return (
<div>
<h2>出错了!!</h2>
</div>
)
}else{
return this.props.children
}
}
}
声明一个测试组件 Test.jsx
,只要输入的是非字母就会报错:
import React, { Component } from 'react'
export default class Test extends Component {
state = {
value: ""
}
/*
* JS中的||符号:
1、运算方法:
只要“||”前面为false,不管“||”后面是true还是false,都返回“||”后面的值。
只要“||”前面为true,不管“||”后面是true还是false,都返回“||”前面的值。
2、总结:真前假后
* throw语句会抛出一个错误。当错误发生时, JavaScript 会停止执行并抛出错误信息。
*/
render() {
const value = this.state.value;
if(/^[a-zA-Z]+$/.test(value) || value===""){
return <input type="text" placeholder="只能填写字母" value={value} onChange={this.changeFn.bind(this)} />
}
}
changeFn(e){
this.setState({
value: e.currentTarget.value
})
}
}
在整个App中的组件最外层,套上这个错误边界组件:
import React, { Component } from 'react'
import ErrorBoundary from './ErrorBoundary'
import Test from './Test'
export default class App6 extends Component {
render(){
return(
<ErrorBoundary>
<Test />
</ErrorBoundary>
)
}
}
如上,可以测试到,只要输入的是非字母,就会报错:
2、static getDerivedStateFromError()
将错误边界组件中的 componentDidCatch()
改为:
import React from 'react'
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
// 使用static getDerivedStateFromError()
// 意思为:获得从错误边界中产生的状态
static getDerivedStateFromError(error) {
return { hasError: true };
// 这里最好写为: return {hasError: error}
// 本质上是根据error是否存在来判断是否设置 hasError 为 true,但其实能够进入这个函数的话,也就代表一定会有错误,因此直接设置 hasError 为 true 也未尝不可
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>出错了!!</h2>
</div>
)
}
//正常则返回子元素,即该组件包裹的元素
return this.props.children;
}
}
六、PureComponent【熟悉】
我们在学习生命周期的时候,了解过 shouldComponentUpdate
,它是用来判断字段修改前后值是否相等的,而且必须返回true或false,我们来看:
import React, { Component } from 'react'
// 子组件
class Sub extends Component {
// 通过shouldComponentUpdate返回的boolean,决定是否触发UI修改
shouldComponentUpdate(nextProps, nextState){
return nextProps.num !== this.props.num;
}
render(){
return <h1>{this.props.num}</h1>
}
}
export default class App7 extends Component {
state = {
num: 1
}
render() {
return (
<div>
{/* 传值 */}
<Sub num={this.state.num} />
<button onClick={this.handleClick.bind(this)}>修改num</button>
</div>
)
}
handleClick(){
this.setState({
num: 2 // 想改成相同的值,可以num: 1
})
}
}
我们有个替代的方案,那就是 PureComponent
。它其实就是在帮我们做这样一件事:自动的帮我们编写shouldComponentUpdate
方法, 避免我们为每个组件都编写一次的麻烦。我们只需要这样, 就可以一步到位:
import React, { Component, PureComponent } from 'react'
class Sub extends PureComponent {
render(){
return <h1>{this.props.num}</h1>
}
}
export default class App7 extends Component {
state = {
num: 1
}
render() {
return (
<div>
{/* 传值 */}
<Sub num={this.state.num} />
<button onClick={this.handleClick.bind(this)}>修改num</button>
</div>
)
}
handleClick(){
this.setState({
num: 1 // 当为1时,你会发现改不动
})
}
}
七、高阶组件HOC【掌握】
官网地址:https://react.docschina.org/docs/higher-order-components.html
**高阶组件(HOC)**是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
可以简单将其理解成 “类工厂”,一般在class组件时使用。我们先来写个简单的案例:
import React, { Component } from 'react'
class Sub1 extends Component {
state = {
num: 1
}
addNumFn(){
// 一秒后将num修改为2
setTimeout(()=>{
this.setState({num: 2})
}, 1000)
}
render(){
return <button onClick={this.addNumFn.bind(this)}>Sub1数字为:{this.state.num}</button>
}
}
class Sub2 extends Component {
state = {
num: 1
}
addNumFn(){
// 两秒后将num修改为2
setTimeout(()=>{
this.setState({num: 2})
}, 2000)
}
render(){
return <button onClick={this.addNumFn.bind(this)}>Sub2数字为:{this.state.num}</button>
}
}
export default class App8 extends Component {
render() {
return (
<div>
<Sub1></Sub1>
<hr/>
<Sub2></Sub2>
</div>
)
}
}
上面的Sub1和Sub2都是在定时器结束后修改num,大家可以看到代码几乎完全一样。那么这时候你肯定会想到代码抽离,所以我们定义一个函数来抽离:
import React, { Component } from 'react'
// 定义一个高阶组件HOC(本质就是高阶函数HOF)
// 参数传入一个Component,也就是调用这个函数时需要传入的组件
const hocFunc = (Component, timer) => {
// 返回一个组件,这里class后面可以不跟组件名
return class extends Component {
state = {
num: 1
}
addNumFn(){
// 两秒后将num修改为2
setTimeout(()=>{
this.setState({num: 2})
}, 2000)
}
render(){
// 将state的值传递给组件,并返回这个组件
return <Component num={this.state.num} addNumFn={this.addNumFn.bind(this)} />
}
}
}
class Sub1 extends Component {
render(){
// 要用props接收
return <button onClick={this.props.addNumFn}>Sub1数字为:{this.props.num}</button>
}
}
class Sub2 extends Component {
render(){
return <button onClick={this.props.addNumFn}>Sub2数字为:{this.props.num}</button>
}
}
const Sub1Com = hocFunc(Sub1, 1000); // 使用高阶组件返回出来的是组件,可以在App8中直接调用
const Sub2Com = hocFunc(Sub2, 2000);
export default class App8 extends Component {
render() {
return (
<div>
{/* 渲染的是返回出来的值 */}
<Sub1Com></Sub1Com>
<hr/>
<Sub2Com></Sub2Com>
</div>
)
}
}
如此,咱们就实现了高阶组件。
八、LazyLoad【熟悉】
React有组件懒加载的功能。
我们先定义一个 Sub.js
子组件:
import React, { Component } from 'react'
export default class Sub extends Component {
render() {
return (
<h2>Sub</h2>
)
}
}
再在父组件引用:
import React, { Component } from 'react'
// 使用 React.lazy引入
const Sub = React.lazy(()=>import('./Sub'))
export default class App9 extends Component {
render() {
return (
<Sub />
)
}
}
但以上代码是无效的,因为:懒加载必须结合Suspense一起使用。
九、Suspense【熟悉】
Suspense主要解决的就是网络IO问题,是用来包裹异步组件,添加loading效果等。
我们添加Suspense代码:
import React, { Component, Suspense } from 'react'
// 懒加载
const Sub = React.lazy(() => import('./Sub'));
export default class App9 extends Component {
render() {
return (
// Suspense包裹异步组件
<Suspense fallback={<div>loading...</div>}>
<Sub />
</Suspense>
)
}
}
至此,我们将network的 No throtting
调为 slow 3G
,然后刷新页面,就可以看到loading的效果。
十、作业
作业一(初级)
使用 Function Component结合Hooks完成Tab栏切换。
作业二(中级)
使用Function Component结合Hooks完成微博发布。
项目效果:http://codesohigh.com/vue-weibo/
作业三(高级)
开始搭建 《IT猿题库》项目,并完成首页布局。(打开项目时记得先清空localStorage)
项目psd设计稿:链接: https://pan.baidu.com/s/1h5Zz45ZpX6fspbYWqsBtdA 提取码: 8w1g