React基础知识(二)

React基础知识(二)

发表于 2024-08-20
更新于 2024-08-24
编程
阅读时长:25分钟
阅读量:13
AI 总结
|

一、redux

1.1 基本认识

Redux是JavaScript的状态容器,提供了可供预测的状态管理。

它不仅可以和react一起使用,也可以和其他界面库(如vue)一起使用。并且其体积非常小(只有2kb)

核心概念

1、Store

数据需要统一保存在Store中

const initialState = { friend: [ { name: "linming", age: 22 } ] }

2、action

所有的数据的变化,必须通过派发(dispatch)action来更新

const action = { type: "ADD_FRIEND", info: { name: "lihua", age: 20 } }

3、reducer

reducer是一个纯函数,这个函数的做的事情就是将传入的state和action结合起来生成一个新的state

三大原则

1、单一数据源

整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个store中。单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改

2、state是只读的

唯一修改state的方法一定是触发action,不能试图通过其他方式来修改state

3、使用纯函数来执行修改

通过reducer将旧state和actions联系在一起,并且返回一个新的state。所有的reducer都应该是纯函数,不能产生任何副作用

1.2 基本使用

安装:yarn add redux

简单的案例

const redux = require("redux");

const initialState = {
  counter: 0,
};

// reducer
function reducer(state = initialState, action) {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, counter: state.counter + 1 };
    case "DECREMENT":
      return { ...state, counter: state.counter - 1 };
    case "ADD_NUMBER":
      return { ...state, counter: state.counter + action.num };
    case "SUB_NUMBER":
      return { ...state, counter: state.counter - action.num };
    default:
      return state;
  }
}

// store(创建时需要传入一个reducer)
const store = redux.createStore(reducer);

//订阅store的修改
store.subscribe(() => {
  console.log("counter:", store.getState().counter);
});

// actions
const action1 = { type: "INCREMENT" };
const action2 = { type: "DECREMENT" };
const action3 = { type: "ADD_NUMBER", num: 5 };
const action4 = { type: "SUB_NUMBER", num: 12 };

// 派发action
store.dispatch(action1);
store.dispatch(action2);
store.dispatch(action3);
store.dispatch(action4);

//输出:
//counter: 1
//counter: 0
//counter: 5
//counter: -7

但是在真实的开发环境下,我们一般不会这么使用redux。而是会对其进行解构的抽离,使其解构更加地清晰

1、创建store文件夹,专门用来保存redux相关的代码

//store/index.js
const redux = require('redux')
const reducer = require('./reducer.js')
const store = redux.createStore(reducer) //创建store,传入reducer

module.exports = store

2、创建reducer.js文件,编写reducer函数

//reducer.js
const { ADD_NUMBER, SUB_NUMBER } = require('./constants.js')

const defaultStore = {
  counter: 0
}

function reducer(state=defaultStore, action) {
  switch (action.type) {
    case ADD_NUMBER:
      return {...state, counter: state.counter + action.num }
    case SUB_NUMBER:
      return {...state, counter: state.counter - action.num }
    default:
      return state
  }
}

module.exports = reducer

3、创建actionCreators.js文件,编写具体的修改state的action函数

//actionCreators.js
const { ADD_NUMBER, SUB_NUMBER } = require('./constants.js')
const addAction = num => {
  return {
    type: ADD_NUMBER,
    num
  }
}
const subAction = num => {
  return {
    type: SUB_NUMBER,
    num
  }
}
module.exports = {
  addAction,
  subAction
}

4、抽离type到一个统一的文件中

//constants.js
const ADD_NUMBER = "ADD_NUMBER"
const SUB_NUMBER = "SUB_NUMBER"

module.exports = {
  ADD_NUMBER,
  SUB_NUMBER
}

5、最后在入口文件main.js中进行修改派发及修改订阅

//main.js
const store = require('./store/index.js')
const { addAction, subAction } = require('./store/actionCreators.js')

// 订阅store的修改
store.subscribe(() => {
  console.log(store.getState());
})


// 派发action,进行数据修改
store.dispatch(addAction(10))
store.dispatch(subAction(15))

1.3 在react中使用

在react开发中会将redux相关的代码存放在一个store文件夹下,如上方的做法

这里简单说一下怎么使用

import React, { PureComponent } from "react";
import store from "./store";
const { addAction } = require("./store/actionCreators");
export default class App extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      counter: store.getState().counter,
    };
  }
  //在生命周期中订阅该变化
  componentDidMount() {
    store.subscribe(() => {
      this.setState({
        counter: store.getState().counter,
      });
    });
  }

  //数据变化时,重新执行render函数
  render() {
    return (
      <div>
        <h2>当前计数: {this.state.counter}</h2>
        <button onClick={(e) => this.inctement(1)}>+1</button>
        <button onClick={(e) => this.inctement(5)}>+5</button>
      </div>
    );
  }
  //派发action
  inctement(num) {
    store.dispatch(addAction(num));
  }
}

最后,我们还需要在组件卸载前将订阅取消掉

componentDidMount() {
    this.unSubscribe = store.subscribe(() => {
        this.setState({
            counter: store.getState().counter,
        });
    });
}
componentWillUnmount() {
    this.unSubscribe()
}

1.4 connect高阶函数

上方的代码中,我们实现了在react中使用redux。但是会发现为了使用redux会使整个组件变得比较复杂,如果多个页面要是用redux,也需要编写逻辑相同额度重复代码。

所以我们可以将里边重复的代码抽离到一个connect高阶组件中

//utils/connect.js
import { PureComponent } from "react"
import store from '../store/index'

export default function connect(mapStateToProps, mapDispatchToProps) {
  return function handleMapCpn(WrappedComponent) {
    return class extends PureComponent{
      constructor(props) {
        super(props)
        this.state = {
          storeState: mapStateToProps(store.getState())
        }
      }
    //订阅
      componentDidMount() {
        this.unsubscribe = store.subscribe(() => {
          this.setState({
            storeState: mapStateToProps(store.getState())
          })
        })
      }
     //删除订阅
      componentWillUnmount() {
        this.unsubscribe()
      }
      render() {
        //处理这些props,进行数据传递
        return <WrappedComponent {... this.props}
                                 {...mapStateToProps(store.getState())}
                                 {...mapDispatchToProps(store.dispatch)}/>
      }
    }
  }
}

注:

1、connect函数接收两个参数:mapStateToProps和mapDispatchToProps,第一个参数用来传递组件的变量,第二个参数用来提交组件想要执行的dispatch

2、connect函数最终返回一个处理过的组件

//应用
import React, { PureComponent } from "react";
import connect from './utils/connect'
const { addAction } = require("./store/actionCreators");
class App extends PureComponent {
  render() {
    return (
      <div>
        <h2>当前计数: {this.props.counter}</h2>
        <button onClick={(e) => this.props.inctement(1)}>+1</button>
        <button onClick={(e) => this.props.inctement(5)}>+5</button>
      </div>
    );
  }
}
const mapStateToProps = state => {
  return {
    counter: state.counter
  }
}
const mapDispatchToProps = dispatch => {
  return {
    inctement: function(num) {
      dispatch(addAction(num))
    }
  }
}
export default connect(mapStateToProps,mapDispatchToProps)(App)

1.5 react-redux库

在真实的开发中,如果手动去封装connect这个高阶函数就太麻烦了。事实上react提供了一个react-redux的库,实现了我们想要的功能

1、安装:yarn add react-redux

2、使用步骤

//入口文件
import { Provider } from 'react-redux'
import store from './store'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

//App.js组件导入
import { connect } from 'react-redux'

其余的步骤不用改变,这样一来我们就省略了编写connect函数的步骤

案例分析——发送异步网络请求,保存数据

1、书写相应的action函数,同时定义类型type常量

//actioncCreators.js
const { SAVE_BANNERS } = require('./constants.js')
const saveBannersAction = banners => {
  return {
    type: SAVE_BANNERS,
    banners
  }
}
module.exports = {
  saveBannersAction
}

2、书写reducer函数

const { ADD_NUMBER, SUB_NUMBER, SAVE_BANNERS } = require('./constants.js')

const defaultStore = {
  banners: [] //定义一个空的原始值
}

function reducer(state=defaultStore, action) {
  switch (action.type) {
    case SAVE_BANNERS:
      return {...state, banners: action.banners}
    default:
      return state
  }
}

module.exports = reducer

3、在App组件中编写业务代码

import React, { PureComponent } from "react";
import axios from "axios";
import { connect } from "react-redux";
import { saveBannersAction } from "./store/actionCreators";

class App extends PureComponent {
  componentDidMount() {
  //在生命周期函数中发送网络请求,并保存数据
    axios({
      url: "http://123.207.32.32:8000/home/multidata",
    }).then((res) => {
      this.props.saveBanners(res.data.data.banner.list);
    });
  }
  render() {
    return (
      <div>
        {this.props.banners.map((item) => {
          return <li key={item.acm}>{item.title}</li>;
        })}
      </div>
    );
  }
}
const mapStateToProps = (state) => {
  return {
    banners: state.banners,
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    saveBanners(banners) {
      dispatch(saveBannersAction(banners));
    },
  }
}
export default connect(mapStateToProps, mapDispatchToProps)(App);

1.6 redux中异步操作

事实上,发送网络请求的数据也属于状态管理的一部分(不应该在生命周期中进行),最好也交给redux来管理。在redux中可以在中间件中发送网络请求

中间件

redux中间件的目的是在dispatch的action和最终到达的reducer函数之间,扩展一些自己的代码。比如调用异步接口,添加代码调试功能等等

官方推荐的网络请求的中间件redux-thunk

redux-thunk

默认情况下dispatch(action),action需要是一个JavaScript对象。

redux-thunk可以让dispatch(对象),可以接收一个函数作为参数。

其原理是:该函数会被调用,并且传给这个函数一个dispatch函数和getState函数

安装:yarn add redux-thunk

具体使用步骤

1、在store中的index.js文件中导入该中间件,并应用这个中间件

import reducer from './reducer'
import thunkMiddleware from 'redux-thunk'
import { createStore, applyMiddleware } from 'redux'
//应用中间件
const storeEnhancer = applyMiddleware(thunkMiddleware)

const store = createStore(reducer, storeEnhancer)

export default store

2、在组件中调用获取数据的函数

  componentDidMount() {
    this.props.getMuitidata();
  }

注:此时组件中还没有这个函数,我们需要进行映射

3、映射getMuitidata到组件上

import { getMutidataAction } from "./store/actionCreators";
const mapDispatchToProps = (dispatch) => ({
  getMuitidata() {
    dispatch(getMutidataAction); //派发一次action,不过该getMutidataAction是个函数
  },
});

4、编写对应的getMutidataAction函数

//actionCreators.js
export const getMutidataAction = dispatch => {
      axios({
      url: "http://123.207.32.32:8000/home/multidata",
    }).then((res) => {
      dispatch(saveBannersAction(res.data.data.banner.list)) //在派发的action里,再次进行派发
    });
}

在这里发送网络请求,并进行下一次派发,将网络请求的内容发送出去

devtools工具

在谷歌商店安装该插件Redux-DevTools

另外还需要进行一些配置才能使用

//store/index.js
import reducer from './reducer'
import thunkMiddleware from 'redux-thunk'
import { createStore, applyMiddleware, compose } from 'redux'


//composeEnhancers函数
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose

//应用中间件
const storeEnhancer = applyMiddleware(thunkMiddleware)
const store = createStore(reducer, composeEnhancers(storeEnhancer))

export default store

1.7 异步操作saga

redux-saga是另一个比较常用在redux发送异步请求的中间件,它的使用更加灵活

安装:yarn add redux-saga

使用步骤

1、引用saga

//store/index
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';

import saga from './saga';
import reducer from './reducer.js';

// 应用一些中间件--创建sagaMiddleware中间件
const sagaMiddleware = createSagaMiddleware();

const storeEnhancer = applyMiddleware(sagaMiddleware);
const store = createStore(reducer, storeEnhancer);

sagaMiddleware.run(saga);

export default store;

运行saga中间件时要求传入一个saga的生成器函数

2、创建saga生成器函数

//store/saga.js
import { takeEvery, put, all, takeLatest } from 'redux-saga/effects';
import axios from 'axios';
import {
  FETCH_HOME_MULTIDATA, ADD_NUMBER
} from './constants';
import {
  changeBannersAction,
  changeRecommendAction
} from './actionCreators';

function* fetchHomeMultidata(action) {
  const res = yield axios.get("http://123.207.32.32:8000/home/multidata");
  const banners = res.data.data.banner.list;
  const recommends = res.data.data.recommend.list;
  // yield put(changeBannersAction(banners));
  // yield put(changeRecommendAction(recommends));
  yield all([
    yield put(changeBannersAction(banners)),
    yield put(changeRecommendAction(recommends))
  ])
}

function* mySaga() {
  // takeLatest takeEvery区别:
  // takeLatest: 依次只能监听一个对应的action
  // takeEvery: 每一个都会被执行
  yield all([
    takeLatest(FETCH_HOME_MULTIDATA, fetchHomeMultidata),
    // takeLatest(ADD_NUMBER, fetchHomeMultidata),
  ]);
}

export default mySaga;

二、router

2.1 基本使用

react router不是集中在一个包中进行管理

1、react-router是router的核心部分代码

2、react-router-dom是用于浏览器的

3、react-router-native是用于原生应用的

安装:yarn add react-router-dom@5.2.0(会自动帮助我们安装react-router的依赖,ts需要额外安装类型库)

react-router最主要的API是给我们提供了一些组件

1、BrowserRouter或HashRouter

Router包含了对路径改变的监听,并且会将相应的路径传递给子组件。

BrowserRouter使用history模式,HashRouter使用hash模式

2、Link和NavLink

通常路径的跳转是使用Link组件,但最终会被渲染成a元素;

NavLink是Link基础上增加了一些样式属性;

to属性:Link中最重要的属性,用于设置跳转到的路径

3、Route

Route用于路径的匹配;

path属性用于设置匹配到的路径;

component属性:设置匹配到的路径后,渲染的组件

exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件

    return (
      <div>
        <BrowserRouter>
          <Link to="/">首页</Link>
          <Link to="/about">关于</Link>
          <Link to="/profile">我的</Link>

          <Route exact path="/" component={home}/>
          <Route path="/about" component={about}/>
          <Route path="/profile" component={profile}/>
        </BrowserRouter>
      </div>
    );

2.2 navlink与switch

NavLink

我们有时候会有这种需求,选择对应的路由时,对应的a元素发生某些变化。这个时候就可以使用navlink组件来代替link组件

属性说明:

1、activeStyle:活跃时的样式

2、activeClassName:活跃时添加的class(默认会给活跃的a标签一个active的类名)

3、exact:是否实现精准匹配

<BrowserRouter>
    <NavLink to="/" exact activeStyle={{clolr:"red", fontSize: "30px"}}>首页</NavLink>
    <NavLink to="/about" activeStyle={{clolr:"red", fontSize: "30px"}}>关于</NavLink>
    <NavLink to="/profile" activeStyle={{clolr:"red", fontSize: "30px"}}>我的</NavLink>

    <Route exact path="/" component={home}/>
    <Route path="/about" component={about}/>
    <Route path="/profile" component={profile}/>
</BrowserRouter>

当然,也可以给navLink一个类名activeClassName,到专门的样式文件中添加样式

Switch

在开发中往往存在多个路由被匹配的情况。但是真实情况是我们仅需要匹配一个路由

比如:

在开发中我们会增加一个其他路由都没被匹配时,跳转到的路由

<Route component={NoMatch}/>

或者当需要路由传参时,也会被经常匹配到

<Route path="/:id"/>

针对以上这些情况我们在开发中就可以使用switch来将所有的Route进行包裹

<Switch>
    <Route exact path="/" component={home} />
    <Route path="/about" component={about} />
    <Route path="/profile" component={profile} />
</Switch>

redirect

redirect用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中

render() {
    return this.state.isLogin ? (
    <div>
            <div>user</div>
            <div>otherInfo</div>
    </div>
    ) : <Redirect to="/login" />
}

2.3 嵌套路由

function CultureCpn () {
  return <h3>企业文化</h3>
}
function HistoryCpn () {
  return <h3>我们拥有悠久的历史</h3>
}
function ConnacteCpn () {
  return <h3>联系电话:020-123122144414</h3>
}

export default class about extends PureComponent {
  render() {
    return (
      <div>
        <NavLink exact to=`${props.match.url}/history`>企业历史</NavLink>
        <NavLink exact to=`${props.match.url}/culture`>企业文化</NavLink>
        <NavLink exact to=`${props.match.url}/contact`>联系我们</NavLink>

        <Switch>
          <Route exact path=`${props.match.path}/history` component={HistoryCpn}></Route>
          <Route exact path=`${props.match.path}/culture` component={CultureCpn}></Route>
          <Route exact path=`${props.match.path}/contact` component={ConnacteCpn}></Route>
        </Switch>
      </div>
    )
  }
}

2.4 手动路由跳转

目前我们实现的跳转主要是通过Link或者NavLink进行跳转的,实际上我们也可以通过JavaScript代码进行跳转。

这样做有个好处:我们可以自定义想要跳转的tag,而不是使用a标签

function JoinUsCpn () {
  return <h3>简历投递:221231@163.com</h3>
}
export default class about extends PureComponent {
  render() {
    return (
      <div>
        <NavLink exact to="/about">企业历史</NavLink>
        <button onClick={ e => this.ToJoinUs()}>加入我们</button>

        <Switch>
          <Route exact path="/about" component={HistoryCpn}></Route>
          <Route exact path="/about/joinus" component={JoinUsCpn}></Route>
        </Switch>
      </div>
    )
  }
  ToJoinUs() {
      // 跳转路由
  }
}

问题1:我们要怎么实现改变url呢?这里并不提供vue中那样的router对象,也不能直接访问到history对象

其实这里about组件是被路由创建的<Route path="/about" component={about} />,在创建about组件时,react-router往组件中的props中传递了一个history对象,我们可以利用这个对象实现跳转。

  ToJoinUs() {
      // 跳转路由
      console.log(this.props.history)
      this.props.history.push('/about/joinus')
  }

问题2:但是App组件不是路由创建,里边没有history属性,要怎么实现自定义tag跳转?

要是App组件拥有props属性,可以将APP组件包裹在一个withRouter高阶组件中,而withRouter组件有需要包裹在Router组件内

所以需要修改一下代码

//入口文件
ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
,
  document.getElementById('root')
);
//App.js
class App extends PureComponent {
  render() {
    return (
      <div>
          <NavLink to="/" exact >
            首页
          </NavLink>
          <NavLink to="/about" >
            关于
          </NavLink>
          <NavLink to="/profile" >
            我的
          </NavLink>
          <button onClick={e => this.goToShare()}>分享</button>

          <Switch>
            <Route exact path="/" component={home} />
            <Route path="/about" component={about} />
            <Route path="/profile" component={profile} />
          </Switch>
      </div>
    );
  }
  goToShare() {
    console.log(this.props.history);
  }
}
export default withRouter(App)

这样一来,就能获取到this.props.history

2.5 动态路由

传递参数的方式有三种:

1、动态路由的方式

2、search传递参数

3、Link中to传入对象

动态路由的方式

  render() {
    const { id } = this.state
    return (
      <div>
          <NavLink to="/" exact>首页</NavLink>
          <NavLink to="/about">关于</NavLink>
          <NavLink to={`/profile/${id}`}>我的</NavLink>
          <Switch>
            <Route exact path="/" component={home} />
            <Route path="/about" component={about} />
            <Route path="/profile/:id" component={profile} />
          </Switch>
      </div>
    );
  }
}

//在profile组件中可以获取到这个参数
  render() {
    console.log(this.props.match.params.id); //123
    return <h2>profile</h2>
  }

该方式最后通过this.props.match来获取传递的参数

Link中to传入对象

方式二不推荐使用

在传递参数时,如果传入的是复杂的数据(比如一个对象),那么最好使用to属性进行传递

    return (
      <div>
          <NavLink to="/" exact activeStyle={{ clolr: "red", fontSize: "30px" }}>首页</NavLink>
          <NavLink to={{
            pathname: "/detail",
            state: { name: 'linming', age: 22 }
          }}>
            详情
          </NavLink>

          <Switch>
            <Route exact path="/" component={home} />
            <Route path="/detail" component={detail} />
          </Switch>
      </div>
    );
  }

  //在detail组件中获取
  export default class detail extends PureComponent {
  render() {
    console.log(this.props.location); {/*获取到info对象*/}
    return (
      <div>
        <h2>detail</h2>
      </div>
    )
  }
}

2.6 react-router-config

从上边的演示中,我们发现react将所有路由的配置都放在一起,这样子非常不利于管理。

通过react-router-cinfig这个插件可以实现像vue一样通过配置文件来划分结构

安装yarn add react-router-config

//router/index.js
export const routes = [
    {
        path: "/"
        exact: true, // 精准匹配
        component: Home
    },
    {
        path: "/about",
        component: About,
        routes: [] //子路由
    }
]

使用{renderRoutes(routes)}

注:如果是嵌套路由,在使用时应该是这样子renderRoutes(this.props.route.routes)

三、Hook

3.1 基本认识

Hook是react 16.8的新增特性,它可以让我们在不编写class的情况下使用state以及其他react特性

Hook可以基本替代class组件

A、class组件与函数式组件的对比

1、class组件可以定义state,并保留内部状态。函数式组件做不到

2、class组件有自己的生命周期,在生命周期中执行网络请求等操作比较方便

3、class组件可以在状态改变时只重新执行render函数,而函数式组件整个会被执行

B、class组件存在的问题

1、复杂的组件变得难以理解。随着业务的增多,class组件会变得非常复杂,很多的逻辑往往混在一起。

2、难以理解的class。为了使用class组件,首先要掌握ES的class,并且在class组件中this的指向是比较复杂的

3、组件复用状态困难

3.2 useState

//改变值
setChange(newValue)

//函数式
setChange((prev) => prev + 1 )


//合并更新对象
setChange(prev => ({...prev, ...newValue})


//初始值--惰性
//对于初始值是通过复杂计算获得,可以传入一个函数。该函数只在初次渲染时调用
//,后续渲染将被忽略(性能优化)
const [data, setData] = useState(() => {
    const initialData = complexCompute()
    return initialData 
})

计数器案例

实现方式:class组件

import React, { PureComponent } from 'react'

export default class App extends PureComponent {
    constructor(props) {
        super(props)
        this.state = {
            counter: 0
        }
    }
    render() {
        return(
            <div>
                <h2>当前计数:{this.state.counter}</h2>
                <button onClick={e => this.increment()}>+1</button>
                <button onClick={e => this.decrement()}>-1</button>
            </div>
        )
    }
    increment() {
        this.setState({
            counter: this.state.counter + 1
        })
    }
    decrement() {
        this.setState({
            counter: this.state.counter - 1
        })
    }
}

实现方式:hook

useState来自于react,需要从react中导入,它是一个hook

  • 参数:默认值
  • 返回值:数组(元素1是当前的state的值,元素2是setState函数)
import React, { useState } from "react";

export default function CounterHook() {
  const arr = useState(0)
  const state = arr[0]
  const setState = arr[1]
  return (
  <div>
    <h2>当前计数:{state}</h2>
    <button onClick={e => setState(state + 1)}>+1</button>
    <button onClick={e => setState(state - 1)}>-1</button>
  </div>
  );
}

实现方式:hook(优化)

还可以更加简洁

import React, {useState} from 'react'

export default function CounterHookBetter() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <h2>当前计数:{count}</h2>
      <button onClick={e => setCount(count + 1)}>+1</button>
      <button onClick={e => setCount(count - 1)}>-1</button>
    </div>
  )
}

3.3 Effect Hook

Effect Hook可以让你来完成一些类似于class中生命周期的功能

案例理解

点击按钮修改state中的title数据,同时使网页的标题绑定这个title

实现方式一:class组件

{/*核心代码*/}
  // 挂载时改变
  componentDidMount() {
    document.title = this.state.title
  }
  //更新时改变
  componentDidUpdate() {
    document.title = this.state.title
  }

可以看到,为了实现这一效果在class组件中使用了两个生命周期函数

实现方式二:使用effect hook的方式

import React, { useEffect, useState } from 'react'

export default function ChangeTitleHook() {
  const [title, setTitle] = useState(0)
  useEffect(() => {
    document.title = title
  })
  return (
    <div>
    当前的标题:{title}
    <button onClick={e => setTitle(title + 1)}>change</button>
  </div>
  )
}

useEffect是一个hook,传入一个回调函数。这个回调函数会在组件第一次挂载时、更新时回调

模拟实现生命周期的功能

在effect hook中可以模拟实现componentDidMount和componentDidUpdate的功能

//子组件
export default function HookDemo() {
  useEffect(() => {
    console.log("这里是类似于componentDidMount");
    return () => {
      console.log("这里是类似于componentDidUpdate");
    }
  })
  return (
    <div>
      模拟生命周期
    </div>
  )
}

//在父组件控制子组件的显示与隐藏
    return (
      <div>
        {isShow ? null : <HookDemo/> }
        <div>
          <button onClick={e => this.changeIsShow()}>显示/隐藏</button>
        </div>
      </div>
    )

可以看到,当组件显示时,打印这里是类似于componentDidMount,当组件隐藏时,打印这里是类似于componentDidUpdate

1、所以不难分析useEffect可以模拟组件挂载、更新、销毁时的生命周期

2、useEffect可以多次调用,每次可以执行不同的内容

与class组件的生命周期不同,class组件的生命周期会将所有需要的代码涵盖其中

export default function HookEffect() {
  useEffect(() => {
    console.log("修改了DOM");
  })
  useEffect(() => {
    console.log("订阅了事件");
  })
  useEffect(() => {
    console.log("发送了网络请求");
  })
  const [counter, setCounter] = useState(0)
  return (
    <div>
      当前计数: {counter}
      <button onClick={e => setCounter(counter + 1)}>+1</button>
    </div>
  )
}

useEffect可以调用多中内容的操作,并且不用堆在一个useEffect里边,增强了代码的可阅读性

问题:当执行页面的更新操作时,useEffect会默认全部执行。点击counter+1,修改了DOM,但是不相干的useEffect也执行了,影响性能。

解决:

3、useEffect的第二个参数:传入一个数组,数组中的元素发生更新时,该useEffect才会更新。当传入一个空数组时,默认只执行一次

  const [counter, setCounter] = useState(0)
  useEffect(() => {
    console.log("修改了DOM");
  },[counter])
  useEffect(() => {
    console.log("订阅了事件");
  },[])
  useEffect(() => {
    console.log("发送了网络请求");
  },[])

3.4 useContext

在之前的开发中,我们在组件中共享的context有两种方式:

  • 类组件可以通过类名.contextType = myContext方式,并在类中获取context
  • 在函数式组件中通过MyContext.Consumer方式共享context

但是在函数式组件中使用起来及其麻烦,useContext提供了一些简便的方式

//父组件(提供者)
export const MyContext = React.createContext();
export default class App extends PureComponent {
  render() {
    return (
      <div>
        <MyContext.Provider value={{name: "李逍遥", age: 999}}>
          <UseContext />
        </MyContext.Provider>
      </div>
    );
  }
}

//子组件(接收)
import React, { useContext } from 'react'
import { MyContext } from '../App'
export default function UseContext() {
  const userInfo = useContext(MyContext)
  return (
    <div>
      UseContext----{userInfo.name}----{userInfo.age}
    </div>
  )
}

3.5 其他的Hook

1、useReducer

useReducer是useState的一种替代方案:在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分。

import React, { useReducer } from 'react'

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return {...state, counter: state.counter + 1}
    case "decrement":
      return {...state, counter: state.counter - 1}
    default:
      return state
  }
}

export default function UseReducerHook() {
  const [state, dispatch] = useReducer(reducer, { counter: 0})
  return (
    <div>
      <h2>当前计数:{state.counter}</h2>
      <button onClick={e => dispatch({type: "increment"})}>+1</button>
      <button onClick={e => dispatch({type: "decrement"})}>-1</button>
    </div>
  )
}

2、useRef

useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变

常用的两种用法:

1、引入DOM(或者组件,但是需要是class组件)元素

2、保存一个数据,这个对象在整个生命周期中可以保存不变

//访问DOM,并进行修改
export default function UseRefHook() {
  const changeH2 = useRef()
  const changeDOM = () => {
    changeH2.current.innerHTML = "hello word"
  }
  return (
    <div>
      <h2 ref={changeH2}>修改DOM</h2>
      <button onClick={changeDOM}>change</button>
    </div>
  )
}

这个hook可以访问类组件(但是不能访问函数组件,访问直接报错)

// 类组件
class ClassCpn extends React.Component {
  render() {
    return <h2>我是类组件</h2>;
  }
}

export default function UseRefHook() {
  const Cpn_c = useRef()
  const fn = () => {
    console.log(Cpn_c.current); {/*正常访问*/}
  };
  return (
    <div>
      <ClassCpn ref={Cpn_c}/>
      <button onClick={fn}>change</button>
    </div>
  );
}

也可以使用该hook来保存一些数据,并且这些数据不会被改变

export default function UseRefSaveData() {
  const numRef = useRef(10)
  return (
    <div>
      <h2>numRef的值{numRef.current}</h2>
    </div>
  )
export default function UseRefSaveData() {
  const [count, setCount] = useState(10)
  const numRef = useRef(count)
  return (
    <div>
      <h2>numRef的值{numRef.current}</h2>
      <h2>count的值{count}</h2>
      <button onClick={e => setCount(count + 10)}>+10</button>
    </div>
  )
}

当点击+10时,{numRef.current}的值始终保持不变

3、useLayoutEffect

useLayoutEffect 和 useEffect 不同的之处在于它采用了同步执行

首先 useLayoutEffect 是在 DOM 更新之后,浏览器绘制之前,这样可以方便修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,

如果修改 DOM 布局放在 useEffect ,那 useEffect 执行是在浏览器绘制视图之后,接下来又改 DOM ,就可能会导致浏览器再次回流和重绘。而且由于两次绘制,视图上可能会造成闪现突兀的效果。

注意:在useLayoutEffect编写过于复杂的代码,会阻塞浏览器绘制

4、useMemo

与vue的computed类似,仅有当依赖的目标对象改变了,才会重新计算。这样子有利于避免在每次渲染时都进行高开销的计算。

基本使用

const cacheValue = useMemo(() => {
    return computeToGetValue(num)
}, [num])

接收两个参数,第一个参数是一个回调函数,该回调中运行一个复杂的计算返回一个值。第二个参数是一个数组,即依赖项。

组件仅在初次渲染执行并进行缓存,后续仅在num发生变化时才重新执行

5、useCallback

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

差别就在于它返回的是一个函数

6、useImperativeHandle

在类组件中,我们可以直接通过ref获取到类子组件的实例。但是在函数式组件中,ref不能获取到。

但是使用useImperativeHandle可以实现这个效果,useImperativeHandle 可以配合 forwardRef 自定义暴露给父组件的实例值

import React, { forwardRef, useImperativeHandle, useRef } from 'react';

function Child(props: any, ref: any) {
  const inputRef = useRef(null)
  useImperativeHandle(ref, () => ({
      focus: () => {
      // @ts-ignore
      inputRef.current.focus() //子组件暴露的方法
    }
  }))
  return (
    <input type="text" ref={inputRef} />
  )
}
const ChildC = forwardRef(Child)

export default function App() {
  const inputRef = useRef(null)
  const focusAction = () => {
    // @ts-ignore
    inputRef.current.focus()
  }
    return (
    <div className="App">
      <ChildC ref={inputRef}/>
      <button onClick={focusAction}>get</button>
    </div>
  );
}

四、React SSR

4.1 SPA的弊端

为什么需要SSR?

因为单页面富应用的局限性。

在开发程序的过程中采用spa的方式,浏览器是怎么执行代码的

// 1、请求index.html
// 2、下载js等资源
// 3、执行js代码
// 4、发送网络请求获取数据
// 5、继续执行js代码,并将请求到的数据填充到index.html中
// 6、展示页面

所以,最终整个应用只有一个html文件。

由此产生了两个严重的问题:

1、首屏显示的速度较慢

2、不利于SEO

为什么不利于SEO呢

因为许多搜索引擎直接就是只检索index.html页面,而不会等js代码执行完毕后在执行检索。

此时检索出来的index.html是基本没有什么关键词的

我们可以利用postman来看看一个没有执行js等资源的index.html是什么样子

120.25.202.60:8888
<!DOCTYPE html>
<html lang="">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="icon" href="/favicon.ico">
    <title>找直播网红后台系统</title>
    <link href="/css/chunk-0000611c.4e123bba.css" rel="prefetch">
    <link href="/css/chunk-21cbafe4.1734b55c.css" rel="prefetch">
    <link href="/css/chunk-2256714a.835e8b5e.css" rel="prefetch">
    <link href="/css/chunk-24143e1e.a0aad18f.css" rel="prefetch">
    <link href="/css/chunk-2791b526.da146432.css" rel="prefetch">
    <link href="/css/chunk-476dbc9a.8603e9a9.css" rel="prefetch">
    <link href="/css/chunk-56455a2a.a9a55b6c.css" rel="prefetch">
    <link href="/css/chunk-9574d16c.42c373b7.css" rel="prefetch">
    <link href="/css/chunk-9d3a2c4c.c9115805.css" rel="prefetch">
    <link href="/css/chunk-d786db44.46cd7e9a.css" rel="prefetch">
    <link href="/js/chunk-0000611c.296e68ce.js" rel="prefetch">
    <link href="/js/chunk-21cbafe4.2992b648.js" rel="prefetch">
    <link href="/js/chunk-2256714a.297ab406.js" rel="prefetch">
    <link href="/js/chunk-24143e1e.88519606.js" rel="prefetch">
    <link href="/js/chunk-2791b526.b6fe96f0.js" rel="prefetch">
    <link href="/js/chunk-2d0ae507.4bcecfe1.js" rel="prefetch">
    <link href="/js/chunk-2d0bce34.3d8af904.js" rel="prefetch">
    <link href="/js/chunk-476dbc9a.c0f6d035.js" rel="prefetch">
    <link href="/js/chunk-56455a2a.a95003c4.js" rel="prefetch">
    <link href="/js/chunk-9574d16c.b93cee82.js" rel="prefetch">
    <link href="/js/chunk-9d3a2c4c.ff6bdc1d.js" rel="prefetch">
    <link href="/js/chunk-d786db44.7cb9b4a8.js" rel="prefetch">
    <link href="/css/app.0e139c77.css" rel="preload" as="style">
    <link href="/css/chunk-vendors.834272ee.css" rel="preload" as="style">
    <link href="/js/app.1e9cfc71.js" rel="preload" as="script">
    <link href="/js/chunk-vendors.a4d945e4.js" rel="preload" as="script">
    <link href="/css/chunk-vendors.834272ee.css" rel="stylesheet">
    <link href="/css/app.0e139c77.css" rel="stylesheet">
</head>

<body>
    <noscript><strong>We're sorry but ic_lechouchou doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript>
    <div id="app"></div>
    <script src="/js/chunk-vendors.a4d945e4.js"></script>
    <script src="/js/app.1e9cfc71.js"></script>
</body>

</html>

所以,为了提高首屏加载速度、提供网站搜索排名,我们需要SSR

4.2 SSR基本认识

SSR(Server Side Rendering,服务端渲染),指的是页面在服务端已经生成了完整的HTML页面结构,不需要浏览器解析。与SSR对应的就是CSR(Client Side Rendering,客户端渲染),即SPA页面通常就是CSR

早期的服务端渲染包括PHP、JSP、ASP等方式。

在目前前后端分离的开发模式下,我们可以借助于在利用Node来帮助我们执行js代码,提前完成页面的渲染

什么是同构

一套代码既可以在服务端运行又可以在客户端运行,这就是同构应用。同构是一种SSR的形态。当用户发出请求时,现在服务器通过SSR渲染出首页的内容,对应的代码同样也可以在客户端被执行,执行的目的包括事件绑定等以及其他页面切换时也可以在客户端被渲染

4.3 react的SSR

目前react比较成熟的服务端渲染方案是next.js,而vue是nuxt.js

我们可以来体验一下next.js

//安装
npm install –g create-next-app

//创建项目
create-next-app next-demo

//运行项目
yarn dev

一个文件就是一个页面,在pages下增加一个页面,就可以不用配置路由,直接通过ip/文件名访问到

评论
  • 支持 Markdown 格式
  • 评论需要登录
  • 邮箱会回复提醒(也许会在垃圾箱内)

0 /400 字