思来想去很久我决定在新我的项目中应用context与reducer来搭建数据层,以前我是Redux的忠诚拥护者,在一些我的项目中我甚至把所有的state放在redux当中进行保护,为的就是寻求一种state的对立,使数据更加长久化,让整个数据层更加明了;然而在接触context与reducer的应用之后我决定做出扭转;context与reducer的联合应用切实是太香了,context与reducer的联合应用比Redux的流程优化了不少;当然这是context与reducer的联合应用比照Redux的操作流程我得出的论断,然而比照于React hooks的其余办法来说context与reducer仍旧是比拟艰涩难懂的知识点;这也不是必须要学会的知识点(只有面试官不问),因为我感觉光useState与effect就能解决开发中百分之九十五的问题,如果余下的百分之五须要你多层级的父子组件当中进行数据通信也没必要非要用到context与reducer,因为一层层的组件透传,redux,mobx,都能实现;
言归正传,context与reducer以及他们如何搭配应用;我从Reducer开始讲起;
Reducer
首先咱们先明确一个概念什么是Reducer,仔细的你可能曾经发现Redux当中也有Reducer这个概念;而Reducer的中文翻译是还原剂的意思,我到当初都没明确为什么要这样子命名(想骂鬼佬的取名),所以我把它当成一个概念来了解,一句话就是接管一个初始值与扭转这个初始值的办法,并返回一个state与一个和state配套的dispatch办法,而接管的这个办法就是reducer;官网给出的解释是:
useState 的代替计划。它接管一个形如 (state, action) => newState 的 reducer,并返回以后的 state 以及与其配套的 dispatch 办法。(如果你相熟 Redux 的话,就曾经晓得它如何工作了。)
在某些场景下,useReducer 会比 useState 更实用,例如 state 逻辑较简单且蕴含多个子值,或者下一个 state 依赖于之前的 state 等。并且,应用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你能够向子组件传递 dispatch 而不是回调函数 。
如果你相熟Vue你会发现这个hooks和computed很像,都是为了解决一些单数据或者多数据的属性计算;
那么如何来应用Reducer?首先咱们须要构想一个简略的应用场景:
某天下午六点当你高高兴兴的拾掇好货色筹备和去女友约会,一位不怀好意的独身的产品经理出于嫉妒的目标让你在页面中加一个计数器性能(程序员怎么可能会有女朋友),此时的你会抉择怎么做?打这个产品经理一顿出气?还是老老实实的实现以下代码?(法制社会,当然选写代码)
import react, {useReducer} from 'react'import styles from './index.less';export default function IndexPage() { const [count,dispatch] = useReducer((state,action)=>{ if (action === 'sub') { return state - 1 }else if(action === 'add'){ return state +1 }else{ return state } },0) return ( <div> <button onClick={()=>{dispatch('add')}}>+</button> <button onClick={()=>{dispatch('sub')}}>-</button> <h1>{count}</h1> </div> );}
不太好懂,我解释下
应用useReducer,须要在useReducer中传入一个reducer和初始值,他会返回一个state(为了不混同,我决定在代码中应用count代替)和dispatch办法,你能够用这个dispatch去扭转这个state,而后再说下reducer,也就是useReducer接管的第一个参数,它须要传入一个state与action,state是它要扭转的值,而action是扭转这个值的判断条件,代码中是判断action是不是add如果是add则返回state+1,是sub则返回state-1,如果都不是就摆烂返回原来的state,而后在jsx语法中咱们应用h1去包裹咱们须要渲染的count,并通过两个按钮调用dispatch办法并传入sub和add去扭转这个count的值;总的来说不是特地好懂,倡议手敲两遍看看成果而后再联合文字很快你就明确它的原理了,reducer是一个不是很说的分明,然而一做就明确的办法;
而后咱们再把话题放回到你与产品经理的相持,
当你信念满满的公布这段代码到服务器并向产品经理夸耀你那温顺可人的女友时,此时你们独身四十年还秃顶的前端主管正站在你的身后看着你的代码,头上冒着青筋手里的饮料瓶被他捏的吱吱响,你意识到问题不对了。这时主管发话了,“你就是这样子写计数器的?!这么简略的代码也须要用到reducer?!”一段灵魂提问之后,你把代码改成了以下这样:
import react, {useState} from 'react'import styles from './index.less';export default function IndexPage() { const [count, setCount] = useState(0); return ( <div> <button onClick={()=>{setCount(count-1)}}>+</button> <button onClick={()=>{setCount(count-1)}}>-</button> <h1 className={styles.title}>{count}</h1> </div> );}
这两段代码实现的成果是一样的,主管说的没错,对于一些简略的计算逻辑useState比useReducer更加快捷不便甚至易懂,也使你的代码更加清晰;然而如果是一些简单逻辑比方购物车的卡券组合,银行的汇率计算等等,此时你刚好又在各个组件当中进行各种通信,我会倡议你应用useReducer,因为你能够通过联合context来进行diapatch的透传从而扭转你的state,这样子会使你的代码可读性变得更高,也会防止疯狂的组件间向下传值,使你的数据通信更加明了,从而优化你的数据流;
而后咱们讲讲reducer的好搭档context;
Context
在以往咱们编写React我的项目中咱们都是通过props在组件中自内向内传值的,这样子其实会造成一个很大的问题如果是多层组件,而你恰好须要从最外层向最内层传值将会变得十分苦楚,而且会让代码变得十分难懂,在这里咱们就须要用到Context,官网是这样子形容Context的
在一个典型的 React 利用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但此种用法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都须要的。Context 提供了一种在组件之间共享此类值的形式,而不用显式地通过组件树的逐层传递 props。
那么咱们要怎么来应用Context?咱们持续来构想一个场景:
还是这个晦气的产品经理,当他看到你给的计数器之后并不是很称心,他想要把计数器展现加在页面底部,而底部与计数器显示页面并不是一个组件,而后在你的骂骂咧咧中你实现了以下代码:
import React, {createContext, useContext, useState} from "react";const CountContext = createContext(null);//底部组件const Bottom = ()=>{ let count = useContext(CountContext) return <div>{count}</div>}//显示页面export default ()=>{ const [count,setCount] = useState(0) return ( <div> <button onClick={()=>setCount(count+1)}>+</button> <button onClick={()=>setCount(count-1)}>-</button> <CountContext.Provider value={count}> <Bottom/> </CountContext.Provider> </div> )}
我来解释下以上代码,为了让展现更加直观我将展现页面与底部组件写在了同一个页面;首先咱们要先创立一个Context用来贮存与派发数据,所以咱们要用到createContext这个办法,
const CountContext = createContext(null);
这个办法有一个参数是你须要传输的数据的默认值,因为咱们这里用到的组件当中不须要默认值,并且咱们须要对这个值在展现页面中创立和应用,所以不须要独自设置默认值,因而我将这个值设为null;创立完Context之后,你所创立的Context会返回一个Provider组件,此时你须要将你须要传值的组件进行包裹;
//显示页面export default ()=>{ //在此咱们创立一个state用于扭转计数器的值 const [count,setCount] = useState(0) return ( <div> <button onClick={()=>setCount(count+1)}>+</button> <button onClick={()=>setCount(count-1)}>-</button> /** 应用useContext来接管Context,接管参数为你所创立的Context, 这个hooks会返回一个state给你,也就是你用Provider所传输的参数, 文中接管的参数为count**/ <CountContext.Provider value={count}> <Bottom/> </CountContext.Provider> </div> )}
当实现Context的创立与count的存储传输之后咱们须要在子组件当中对count进行接管,此时咱们须要在子组件中调用useCntext这个hooks对count进行接管
import React, {createContext, useContext, useState} from "react";const CountContext = createContext(null);const Bottom = ()=>{/* 应用useContext来接管Context,接管参数为你所创立的Context, 这个hooks会返回一个state给你,也就是你用Provider所传输的参数, 文中接管的参数为count*/ let count = useContext(CountContext) return <div>{count}</div>}
此时如果你实现了以上代码,你会发现曾经能够通过Context来对子组件进行数据通信了;
Context与Reducer搭建数据层
既然咱们曾经理解了Context与Reducer的运行,应该也能联想出Context与Reducer所搭建的数据流的运行流程;
应用Context来进行state的存储与通信,Reducer用来返回批改state的dispatch办法,整套流程上存储派发更新三个步骤都有了;然而咱们要怎么样实现数据层呢?
比方某天那个不懂事的产品经理有一个需要,让你写一个批改用户信息的需要,而用户信息咱们存在于数据流中进行保护;这时候如果咱们须要批改用户信息,让数据在全局进行扭转与渲染则须要应用Context与Reducer的搭配应用;
为了不便演示我临时不将它拆开封装,等讲完原理我会将它分类拆开封装;首先咱们须要编写一个Store文件;
import React, {createContext, useReducer} from "react";//创立一个contextexport const StoreContext = createContext({});//以下为为了辨别事件所创建的typesKeyexport const UPDATE_NAME = 'UPDATE_NAME';//更新名字export const UPDATE_AGE = 'UPDATE_AGE';//更新年龄export const UPDATE_SEX = 'UPDATE_SEX';//更新性别export const ADD_AGE = 'ADD_AGE';//年龄加一export const SUB_AGE = 'SUB_AGE';//年龄减一export const TOGGLE_SEX = 'TOGGLE_SEX';//切换性别export const UPDATE_ALL_INFO = 'UPDATE_ALL_INFO'//更新全副信息//默认Stateconst defaultState = { name: '丧彪', age: 18, sex: '男'}const reducer = (state, action) => { //在reducer中须要对事件进行判断来达到不同的state更新 if (action.type === UPDATE_NAME) { state.name = action.name } else if (action.type === UPDATE_AGE) { state.age = action.age } else if (action.type === ADD_AGE) { state.age++ } else if (action.type === SUB_AGE) { state.age-- } else if (action.type === TOGGLE_SEX) { state.sex = state.sex==='男'?'女':'男' } else if (action.type === UPDATE_ALL_INFO) { state.name = action.name state.age = action.age state.sex = action.sex } return JSON.parse(JSON.stringify(state))}export const Store = (props: any) => { //应用useReducer来创立须要向下传值的state与扭转state的dispatch办法 const [state, dispatch] = useReducer(reducer, defaultState); //在这里咱们须要把dispatch也传给子组件,使子组件领有跟新性能 return (<StoreContext.Provider value={{state, dispatch}}> {props.children} </StoreContext.Provider> )}
因为曾经讲了reducer和context的创立与应用流程,这段代码曾经难不倒大家了,所以我只是简略的在代码中进行留神点编注;须要留神的是defaultState是一个对象,所以当咱们应用useReducer之后所生成的state是一个援用类型,所以react并不能很好的监听数据变动来进行render与commit,所以须要在reducer中每次返回一个新对象,因而我应用简略的JSON数据类型转化来生成一个新对象返回;
实现以上代码咱们须要创立两个组件,一个组件用来展现咱们的数据,另一个组件用来扭转咱们的数据;
展现组件
import React,{useContext} from "react";import {StoreContext} from "@/store";export default ()=>{ const {state} = useContext(StoreContext); return ( <div style={{background:'blue'}}> 用户姓名是:{state.name} <br/> 用户性别是:{state.sex} <br/> 用户年龄是:{state.age} </div> )}
更新组件
import React, {useContext} from "react";import { StoreContext, UPDATE_NAME, UPDATE_AGE, UPDATE_SEX, ADD_AGE, SUB_AGE, TOGGLE_SEX} from "@/store";export default () => { const {dispatch} = useContext(StoreContext); let [name, age, sex] = [null, null, null] return ( <div> 更新姓名 <input type="text" onInput={e => { name = e.target.value }}/> <button onClick={()=>dispatch({type:UPDATE_NAME,name})}>update</button> <br/> <br/> <br/> 性别切换: <button onClick={()=>dispatch({type:TOGGLE_SEX})}>toggle</button> <br/> <br/> <br/> 更新性别 <input type="text" onInput={e => { sex = e.target.value }}/> <button onClick={()=>dispatch({type:UPDATE_SEX,sex})}>update</button> <br/> <br/> <br/> 更新年龄: <input type="text" onInput={e => { age = e.target.value }}/> <button onClick={()=>dispatch({type:UPDATE_AGE,age})}>update</button> <br/> <br/> <br/> 年龄+1: <button onClick={()=>dispatch({type:ADD_AGE})}>add</button> <br/> <br/> <br/> 更新-1: <button onClick={()=>dispatch({type:SUB_AGE})}>sub</button> </div> )}
再而后咱们在入口文件中引入咱们所写的Store,展现组件和更新组件;并用Store对展现组件和更新组件进行包裹向下传值
import react from 'react'import styles from './index.less';import {Store} from "@/store";import Preview from '@/pages/Preview'import Update from '@/pages/Update'export default function IndexPage() { return ( <Store> <Preview/> <Update/> </Store> );}
以上咱们便实现了数据层存储,传值,批改的整个过程;
然而如果是构建企业级的数据层咱们的store尽管曾经实现了整个周期,然而因为把所有文件都写在一起使代码可读性变得很差,所以在这里咱们须要对store进行拆分封装,拆分封装次要是依据集体习惯,以及是否适宜我的项目来进行拆分;以下我举一个例子,我将其拆分为:
- actionTypes.ts 用来封装Reducer判断数据跟新形式的key;
- actions.ts 用来封装扭转数据与数据初始化的Reducer返回值办法;
- state.ts 用来封装默认state;
- index.tsx store 入口文件,用来裸露Types与创立的Context Provider组件;
actionTypes.ts 用来封装Reducer判断数据跟新形式的key
export const UPDATE_NAME = 'UPDATE_NAME';//更新名字export const UPDATE_AGE = 'UPDATE_AGE';//更新年龄export const UPDATE_SEX = 'UPDATE_SEX';//更新性别export const ADD_AGE = 'ADD_AGE';//年龄加一export const SUB_AGE = 'SUB_AGE';//年龄减一export const TOGGLE_SEX = 'TOGGLE_SEX';//切换性别export const UPDATE_ALL_INFO = 'UPDATE_ALL_INFO'//更新全副信息
actions.ts 用来封装扭转数据与数据初始化的Reducer返回值办法
import * as types from './actionTypes'export default { [types.UPDATE_NAME](state,actions){ state.name = actions.name return state }, [types.UPDATE_AGE](state,actions){ state.age = actions.age return state }, [types.UPDATE_SEX](state,actions){ state.sex = actions.sex return state }, [types.ADD_AGE](state){ state.age++ return state }, [types.SUB_AGE](state){ state.age-- return state }, [types.TOGGLE_SEX](state){ state.sex = state.sex==='男'?'女':'男' return state },}
state.ts 用来封装默认state
export default { name: '丧彪', age: 18, sex: '男'}
index.tsx store 入口文件,用来裸露Types与创立的Context Provider组件
import React, {createContext, useReducer} from "react";import defaultState from './state'import * as Types from './actionTypes'import actions from './actions'export const types = Typesexport const StoreContext = createContext({});const reducer = (state, action) => { return actions[action.type]?JSON.parse(JSON.stringify(actions[action.type](state,action))):state}export const Store = (props: any) => { const [state, dispatch] = useReducer(reducer, defaultState); return (<StoreContext.Provider value={{state, dispatch}}> {props.children} </StoreContext.Provider> )}
以下为封装好的数据层在页面中援用:
- Priview.tsx 展现组件;
- Update.tsx 更新组件;
- Index.tsx 展现组件与更新组件的父组件;
Priview.tsx 展现组件;
import React,{useContext} from "react";import {StoreContext} from "@/store";export default ()=>{ const {state} = useContext(StoreContext); return ( <div style={{background:'blue'}}> 用户姓名是:{state.name} <br/> 用户性别是:{state.sex} <br/> 用户年龄是:{state.age} </div> )}
Update.tsx 更新组件
import React, {useContext} from "react";import { StoreContext, types} from "@/store";export default () => { const {dispatch} = useContext(StoreContext); let [name, age, sex] = [null, null, null] return ( <div> 更新姓名 <input type="text" onInput={e => { name = e.target.value }}/> <button onClick={()=>dispatch({type:types.UPDATE_NAME,name})}>update</button> <br/> <br/> <br/> 性别切换: <button onClick={()=>dispatch({type:types.TOGGLE_SEX})}>toggle</button> <br/> <br/> <br/> 更新性别 <input type="text" onInput={e => { sex = e.target.value }}/> <button onClick={()=>dispatch({type:types.UPDATE_SEX,sex})}>update</button> <br/> <br/> <br/> 更新年龄: <input type="text" onInput={e => { age = e.target.value }}/> <button onClick={()=>dispatch({type:types.UPDATE_AGE,age})}>update</button> <br/> <br/> <br/> 年龄+1: <button onClick={()=>dispatch({type:types.ADD_AGE})}>add</button> <br/> <br/> <br/> 更新-1: <button onClick={()=>dispatch({type:types.SUB_AGE})}>sub</button> </div> )}
Index.tsx 展现组件与更新组件的父组件
import react from 'react'import styles from './index.less';import {Store} from "@/store";import Preview from '@/pages/Preview'import Update from '@/pages/Update'export default function IndexPage() { return ( <Store> <Preview/> <Update/> </Store> );}
其余
context与reducer的封装差不多就这点货色,如果想晓得整个运行流程请参照reducer与context搭配应用的未拆分流程;这里说一句每次调用dispatch的时候其实都会返回一个新的state(蕴含全副数据的对象),因而在这里我拿简略的解决深拷贝的形式来解决了下,集体不太喜爱这种形式,因为对性能有损耗,然而如果你谋求整个我的项目的性能,其实能够用到一些第三方库,比方immerjs或者immutablejs来搭建一个长久化数据层,如果你这样子做了,那么对我的项目的内存读取以及避免内存溢出下面会有较大晋升,然而这些扭转是肉眼无奈轻易觉察的,如果你的我的项目是短暂的巨型的那么我倡议你应用immerjs与immutablejs;
下一期讲讲immerjs以及以及简略的搭建长久化数据层;