文章总结自:
2022-web前端-春招面试-❤️面试题的讲解💕_哔哩哔哩_bilibili转载https://www.bilibili.com/video/BV1s541177W5?p=52但这个视频总结的其实不是太好,大多数时间都只是在照着PPT念,根本讲不出个所以然。借助视频提供的关键考点,又参考了一些文章,最后总结出了本文。如果文章有什么问题,欢迎大家指正,谢谢!
一、Redux是如何将state注入到React组件中去的?
参考文章:
Redux 入门教程(三):React-Redux 的用法 - 阮一峰的网络日志https://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html在开始接下来的分析之前,必须要先明白一点,就是React组件分为哪几类?
组件的分类方式有很多,这里我们着重去分析React-Redux对组件的分类方式。
React-Redux将所有的组件分为两大类:UI组件、容器组件。
- UI组件:顾名思义,主要负责的就是界面的渲染,不包含任何的业务逻辑处理,渲染所需的所有数据都来自于参数(props)
- 容器组件:专门处理数据的部分,将业务逻辑的执行结果,通过组件参数(props)的形式,交给UI组件,再由UI组件进行渲染
React、Redux、React-Redux三者之间的关系
- React:是一个负责构建用户界面的JS库
- Redux:是一个管理数据的工具
- React-Redux:将 “数据仓库” 与 “构建用户界面的UI组件” 相互绑定的绑定库
Redux
Redux是一个独立于任何语言的数据管理工具。不仅可以在JS中使用,在Java等多门语言中也可以使用。
即使是同一门语言,就比如JS,在使用同一个数据仓库Redux的前提下,不同的 “构建用户界面的JS的库” ,也会对应着不同的绑定库。就像React的绑定库是React-Redux,Vue、Angular的的绑定库是VueJS-Redux、Angular-Redux。
Redux主要负责保存数据,在数据发生变化时,发布变化的数据。React-Redux则是负责订阅发布,将变化的数据交给UI组件,由UI组件将数据呈现到页面上。
这二者各司其职,也正是由于React-Redux的存在,才能将数据仓库Redux与UI组件进行连接。
- Redux原理
Redux的工作原理为:在window上创建一个对象,将数据保存在这个对象上,那么任意组件就可以跨层级传递数据。
Redux数据交互的方式,就是一个经典的发布订阅器
Button.addEventListener('click', ()=>{ ... }); // 订阅 -- 当触发A的时候就执行B
Button.removeEventListener('click', ()=>{ ... }); // 取消订阅 -- 当触发A的时候,不再执行B
Button.onClick; // 发布 -- 触发 A
事件被触发,也就是发布
一个博主发布了一篇文章,所有关注订阅了该博主的用户,而React就相当于是这个博主。
- Redux 核心源码分析(可能会要求手写!)
/** createStore 参数:
* reducer:像下面这样,这就是一个reducer
function todoListReducer(preTodoList, action){
const {type, data} = action;
switch(type){
case 'ADD_TODO':
return {
todoList: [...preTodoList.todoList, data ]
}
case 'REMOVE_TODO':
return {
todoList: preTodoList.todoList.filter(todo => todo.id !== data)
}
default:
return preTodoList;
}
}
export {
todoListReducer
}
* preloadedState 初始化的 state
* enhancer 中间件(数据类型:函数,作用:处理异步Redux)
“中间件” 是一个选填的参数,如果想要填,就必须放在参数的末尾。
如果第二个参数我们想要放中间件,那么就不能有第三个参数;
如果第二个参数放的是初始化的state,第三个参数可以放中间件,也可以为空
*/
export default function createStore(reducer, preloadedState, enhancer) {
// 因为我们要根据形参对数据做相应的处理,很明显第二个参数是用来保存初始化的状态的,第三个参数才是用来保存中间件。
// 所以当第二个参数为中间件时,需要把中间件从第二个参数移到第三个参数上,形参preloadedState置为空即可
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState;
preloadedState = undefined;
}
let currentReducer = reducer;
let currentState = preloadedState; // 整个应用所有的 State 都存储在这个变量里
let currentListeners = []; // 订阅传进来的的回调函数 <=> Button.addEventListener('click', () => { ... })
let nextListeners = currentListeners;
function getState() {
return currentState;
}
// 订阅方法提供给React-Redux的connect,子组件使用该方法订阅store中的数据,以获取变化的数据
function subscribe(listener) {
if (nextListeners === currentListeners) {
nextListeners = [...currentListeners];
}
nextListeners.push(listener);
/*
订阅
const a = Store.subscribe(()=>{});
取消订阅
a.unsubscribe();
因为我们返回的是一个有名字的函数,所以可以通过.funName()的方式调用函数,如果是一个匿名函数,就只能是a()的方式去调用返回的函数了
*/
return function unsubscribe() {
if (nextListeners === currentListeners) {
// 浅复制
nextListeners = [...currentListeners];
}
const index = nextListeners.indexOf(listener);
nextListeners.splice(index, 1);
}
}
/*
订阅
Button.addEventListener('click', () => { ... })
取消订阅
Button.removeEventListener('click', () => { ... })
*/
// 提供给React-Redux的connect,在connect中封装进setState,供子组件调用,来传递子组件修改数据的action
function dispatch(action) {
currentState = currentReducer(currentState, action); // 调用 reducer 来更新数据
const listeners = (currentListeners = nextListeners); // 保证当前的 listeners 是最新的
for (let i = 0; i < listeners.length; i++) {
listeners[i](); // 依次执行回调函数
}
return action;
}
// 手动触发一次 dispatch,初始化
dispatch({type: 'INIT'});
return {
getState,
dispatch,
subscribe
}
}
React-Redux
在React-Redux中,和Store通讯的API只有两个,分别是connect和Provider。
1 connect
connect函数的返回值也是一个函数,这个函数可以接收一个UI 组件作为参数,可以生成一个对应的容器组件,并将二者进行连接。
- counter的使用
// UI组件Counter
class Counter extends Component {
render() {
const { value, onIncreaseClick } = this.props
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
)
}
}
import { connect } from 'react-redux'
const App = connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
用法就像上面这样,Counter是一个纯粹的UI组件,而App就是由connect生成的容器组件,同时,connect会将这二者进行连接。
之后,Counter就只需要负责页面渲染的部分,所有业务逻辑的处理,只需要交给App,App对数据做相应的处理后,通过各种形式,再交给Counter进行渲染。
connect接收两个参数:mapDispatchToProps、mapStateToProps,负责UI组件与容器组件之间的双向交互,也就是
① 输出逻辑:UI组件想要对数据做处理,如何将这一想法转化为Action并交给容器组件
② 输入逻辑:容器组件根据Action对数据做了处理,如何将处理后最新的数据交还给UI组件
- mapDispatchToProps:
主要负责输出逻辑:将UI组件想要对数据进行的操作(例如下面的点击增加3的操作),生成相应的Action。这个Action会由Redux封装进state中,自动发送给Store,让Store对数据去做处理
class Counter extends Component {
render() {
// onIncreaseClick方法就是通过mapDispatchToProps提供给组件的修改state的方法
const { value, onIncreaseClick } = this.props
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
)
}
}
function mapDispatchToProps(dispatch) {
return {
onIncreaseClick: () => dispatch({
// Action
type: 'INCREMENT',
data: 3
})
}
}
- mapStateToProps:
主要负责输入逻辑:mapStateToProps接受一个state作为参数,通过state内Action中的具体行为指令,对数据做相应的逻辑处理,最后将处理完的数据映射到UI组件的props属性中(映射的过程会在下文的connect源码分析中去讲解,我们只需要暂时先记住,mapStateToProps会将修改完的数据映射到props中即可)。这时,UI组件就可以通过props拿到最新的数据。
我们来看个例子:
class Counter extends Component {
render() {
// value的变化就是通过mapStateToProps方法映射来的
const { value, onIncreaseClick } = this.props
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
)
}
}
function mapStateToProps(state) {
return {
todos: todoListReducer(state.todos, state.type);
}
}
const todoListReducer = (todos, type) => {
switch (type) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
default:
return todos;
}
}
可以看到,state中就保存着数据源与Action的属性。
todoListReducer其实就是一个reducer,在reducer中,会根据不同的行为指令,对数据源做不同的处理,然后将处理完的数据源返回出去,最终会交给UI组件,来重新渲染。
- connect核心源码(可能会要求手写!):
import React from 'react';
import PropTypes from 'prop-types';
const connect = (
mapStateToProps = state => todoList,
mapDispatchToProps = {}
// |--------------- 这里是connect方法的返回值,这个返回值也是一个函数 ---------------|
) => (WrapComponent) => {
return class ConnectComponent extends React.Component {
constructor(props, context){
super(props, context);
this.state = {
props: {} // 为组件添一个state,其中包含一个空的props属性
}
}
// 此处为React-Redux的强制要求,子组件只有设置了该属性,才能够获取到context
state contextTypes = {
store: PropTypes.object
}
componentDidMount(){
const {store} = this.context; // 来源于我们在Provider中创建的context对象,其内部就包含一个store属性
store.subscribe(() => this.update()); // 订阅Redux数据的更新,一旦Redux中有数据更新,就会执行update方法
this.update();
}
update(){
const {store} = this.context;
const todoList = mapStateToProps(store.getState());
const dispatchProps = mapDispatchToProps(store.dispatch);
// 将最新的数据、调用dispatch的方法,都放在props中,供子组件使用
this.setState({
props: {
...this.state.props, // 将原有的props属性放进去
...todoList, // 将获取到的最新数据放在props中
...dispatchProps // 再将调用dispatch的方法放在props中
}
})
}
render(){
return <WrapComponent {...this.state.props}></WrapComponent>
}
}
}
connect作用总结:connect可以为子组件添加订阅,在订阅中,会将最新的数据连带调用dispatch的方法一同放在props中。供子组件获取最新的数据,以及调用dispatch传递Action。
2. Provider
作用:将Store挂载到context上,让每个组件都可以访问其中的state。
Redux会自动地将UI组件操作数据的Action发送给Store,但数据处理完后,如何将更新的数据交给UI组件呢?
如果将state作为参数一级一级地传递给各个UI组件,目的肯定是可以达到的,但是效果一定会非常差。
于是Provider就出现了,它的核心原理是借助context,将Store放在context上,使用context的发布订阅机制,让每个子组件就在订阅过后,都可以直接访问到Store中的数据。就省去大量的数据传递。
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
// 创建一个store,并将todoList存储在这个store中
let store = createStore(todoTist);
render(
// 只要将store作为Provider的参数,就可以让store挂到context上
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
这么一来,在数据发生变化时,Redux就会重新在context中发布更新过后的store。UI组件均订阅了context中的store,所在就可以直接拿到更新的数据。
- Provider核心源码
import React from 'react'
import PropTypes from 'prop-types'
export default class Provider extends React.Component{
constructor(props, context) {
super(props, context)
this.store = props.store
}
// 此处为React-Redux的强制要求,作用在下方
getChildContext() {
return { store: this.store }
}
render() {
return React.children.only(this.props.children)
}
}
// 依旧是设定context的强制要求
Provider.childContextTypes = {
store: PropTypes.object
}
定义是双向的,React-Redux强制要求:
- 根组件Provider 提供实例方法childContextTypes,以及类属性getChildContext
- 还要求子组件(UI组件)提供contextTypes属性。
在满足了这两个要求之后,子组件才能够获取到context。缺少任意一个,子组件都无法获取到context
最后,生成store对象,并使用Provider在根组件外面包一层。
React中数据的类型
React中数据有三种:
1. context:往所有子组件、孙组件内传递数据
2. props:父组件向子组件传递数据
3. state:组件自身的数据
答题思路
- 首先。明确React与Redux产生关联的是React-Redux库
- Redux原理,其实就是一个发布订阅器,它用一个变量帮我们存储所有的state,并且提供了发布功能,来帮我们修改数据
- 而React-Redux的作用就是订阅store里数据的更新,他包含两个重要元素,Provider、connect
- Provider的作用就是通过ContextApi将store对象注入到React组件上去
- 而connect方法本质上就是一个高阶组件,在高阶组件中通过订阅Store数据的更新,通过调用setState方法触发组件更新。
二、Redux在实际项目中的使用,以及它所产生的问题
为什么要使用Redux?
如果我们的UI层十分简单,并且没有非常多的交互,那么就没有必要使用Redux,借助Redux会需要非常多的样板代码,例如reducer、action等等。并且修改数据也会十分地麻烦,我们需要借助dispatch调用对应的reducer,然后去触发相应的回调,最终才能更新数据,整个过程会十分地麻烦。
在这种情况下,借助state与setState、props会非常地简便。
但是如果我们的代码层级较为复杂,且需要关系较远的几个组件之间相互通信,借助state与props就会十分地复杂,这个时候,选择使用Redux就会很方便。它可以很好地解决跨组件间数据传递的问题,相比较一层一层传递数据的方式,修改数据的过程也会显得非常清晰。
如果一定是使用,有哪些最佳实践?
较好的最佳实践,其实就是指如何解决上面提到的痛点 - 过多的样板代码。
1. redux-actions
Redux-actions框架之createAction()与handleActions() 用法讲解 - 简书redux-actions redux-actions框架提供了两个常用的API函数 createAction() handleActions() createAction(...https://www.jianshu.com/p/c6096d61ae1c在初始化reducer和action构造器时,减少样板代码
- Action 一般长这样:
const CounterAction = {
increase() {
return {
type: Constants.INCREASE
}
},
decrease() {
return {
type: Constants.DECREASE
}
}
}
- 借助了redux-action的createAction:
import {createAction} from 'redux-actions'
import type from '../../constants/actionType'
import actions from '../../actionCreators/movie'
const getMovieList = createAction(type.MOVIE_LIST, actions.movieList)
const getMovieListForDemo = createAction(type.MOVIE_LIST, actions.movieListForDemo)
const getMovieDetail = createAction(type.MOVIE_DETAIL, actions.movieDetail)
const getMovieStory = createAction(type.MOVIE_STORY, actions.movieStory)
const getMovieShowTimeList = createAction(type.MOVIE_SHOWTIME_LIST, actions.movieShowTimeList)
const getMovieComeingNewList = createAction(type.MOVIE_COMEING_NEW_LIST, actions.movieComeingNewList)
const getMovieComment = createAction(type.MOVIE_COMMENT_LIST, actions.movieCommentList)
const getMiniComment = createAction(type.MOVIE_MINI_COMMENT, actions.movieMiniCommentList)
const getPlusComment = createAction(type.MOVIE_PLUS_COMMENT, actions.moviePlusCommentList)
const getTrailerList = createAction(type.MOVIE_TRAILER_LIST, actions.movieTrailerList)
const getActorList = createAction(type.MOVIE_ACTOR_LIST, actions.movieActorList)
const getPictureList = createAction(type.MOVIE_PICTURE_LIST, actions.moviePictureList)
const actionCreators = {
getMovieList: params => getMovieList(params),
getMovieDetail,
getMovieStory,
getMovieListForDemo,
getMovieShowTimeList,
getMovieComeingNewList,
getMovieComment,
getMiniComment,
getPlusComment,
getTrailerList,
getActorList,
getPictureList,
}
export default {actionCreators}
可以发现,整个action的创建逻辑就变得十分清晰明了,一眼就能看出来在做什么,而且,省去很多的模板语句。
- reducer:
// react-redux
const defaultState = 10
const reducer = (state = defaultState, action) => {
switch (action.type) {
case Constants.INCREASE:
return state + 1
case Constants.DECREASE:
return state - 1
default:
return state
}
}
- 借助了redux-action的handleActions:
import type from '../../constants/actionType'
import {handleActions} from 'redux-actions'
const initialState = {
movieDetail: {},
commentData: {}
}
const actions = {}
actions[type.MOVIE_DETAIL + type.FETCH_SUCCESS_SUFFIX] = (state, action) => {
return {
...state,
movieDetail: action.payload.data
}
}
actions[type.MOVIE_COMMENT_LIST + type.FETCH_SUCCESS_SUFFIX] = (state, action) => {
return {
...state,
commentData: action.payload.data
}
}
const reducer = handleActions(actions, initialState)
export default reducer
2. yeoman
yeoman是一个cli工具,可以用来一键生成模板代码
yo project-name: component my/namespaced/components/name
答题思路
Redux最大的弊端就是样板代码太多,修改数据的链路太长。
解决方案:
- 借助Redux-actions来减少对固定代码的书写,同时可以让action、reducer的解构更清晰
- 借助yeoman等cli工具,帮助我们用命令一键创建模板代码
Redux的异步问题怎么解决?
在Redux遇到如请求后台数据、延迟执行、setTimeout、setInterval等异步问题时,该如何解决?
redux之所以不能处理异步问题,是因为在dispatch中,默认只能接收一个Object类型的action,后面需要根据对象里面的type属性去做相应的处理。但是如果数据类型变为了其他类型,就比如在异步时,通常需要传入一个函数,这时就无法获取到type属性。
我们可以通过Redux-thunk、Redux-saga等其他中间件来使Redux支持异步
redux异步问题可以通过中间件来解决。
1. Redux-thunk
function increment() {
return {
type: 'INCREMENT_COUNTER'
};
}
// 引入了redux-thunk中间件后,就可以异步使用了
function incrementAsync(){
return (dispatch) => {
setTimeout(() => {
dispatch(increment());
}, 1000)
};
}
2. Redux-saga
让异步行为成为架构中独立的一层(成为saga)
三、React中的Hooks
React的Hooks解决了函数式组件一下三个问题:
区别 | 函数式组件 | 类组件 |
---|---|---|
是否有this | 无 | 有 |
是否有声明周期 | 无 | 有 |
是否有state | 无 | 有 |
同时,也让一些公共方法可以被更简便、更实用地定义出来。
原生的Hooks我们就不讲解了,使用起来可以说是非常地简便,网上的文档很多,主要就是讲解一下如何自定义一个Hooks。
需求:提取公共的发送请求的方法
需求:在页面A与页面B上请求一部电影的详细数据
A -> 加载时 -> 发送请求:http://swapi.io/api/films/1
B -> 加载时 -> 发送请求:http://swapi.io/api/films/2
拿到请求后,调用this.setState更新数据。
class App extends React.Component{
constructor(props){
super(props);
this.state = {
loading: false,
data: {}
};
}
componentDidMount() {
this.setState({
loading: true
})
fetch(`http://swapi.io/api/films/${this.props.fileId}`)
.then(data => {
this.setState({
data: data,
loading: false
})
});
}
componentDidUpdate() {
this.setState({
loading: true
})
fetch(`http://swapi.io/api/films/${this.props.fileId}`)
.then(data => {
this.setState({
data: data,
loading: false
})
});
}
// 抽取发送请求以及更改状态的方法:(错误,此处是无法抽取的)
fetchData = () => {
this.setState({
loading: true
})
fetch(`http://swapi.io/api/films/${this.props.fileId}`)
.then(data => {
this.setState({
data: data,
loading: false
})
});
}
render() {
const {loading, data} = this.state;
if(loading === true){
return <p>Loading...</p>
}
return(
<div>
<p>电影名称:{data.title}</p>
<p>导演:{data.producer}</p>
<p>发布日期:{data.release_data}</p>
</div>
);
}
}
可以很明显地看到在DidMount和DidUpdate时都有发送请求的过程,但是却无法被抽离出来,因为这其中有一个this.setState方法,将该方法抽成一个公共方法时,this始终都指向当前组件,无法指向调用该方法的组件,解决方案:抽取成一个自定义的Hooks。
自定义具有特定功能的Hooks
// 注意 hooks 约定必须以 use 开头(useA, useB)
const useFetchData = (filmId) => {
const [loading, setLoading] = useState(false); // 只有在第一次加载的时候才会 被 false 复制
const [data, setData] = useState({});
useEffect(() => {
setLoading(true); // 1. 设置 loading 为 true
fetch(`https://swapi.co/api/films/${filmId}`) // 2. 发送请求
.then(data => {
setData(data); // 3. 收到请求后,设置 data 为请求到的数据
setLoading(false); // 4. 设置 loading 为 false
});
}, [filmId]); // filmId 变化的时候,才触发 useEffect
return loading, data];
};
使用自定义的Hooks
import useFetchData from './useFetchData';
function App({filmId}){
const [loading, data] = useFetchData(fileId);
if(loading === true){
return <p>Loading...</p>
}
return(
<div>
<p>电影名称:{data.title}</p>
<p>导演:{data.producer}</p>
<p>发布日期:{data.release_data}</p>
</div>
);
}
答题思路
- React Hooks是一个全新的API,可以用函数来写所有的组件
- 可以让函数式组件也拥有自己的状态(包括state和声明周期函数)
- 可以通过创建自定义的Hooks来抽离可复用的业务组件