1. 前言
我们在开发项目的过程中往往会遇到一些很复杂的应用场景,复杂的场景必然避免不了多个组件的嵌套使用,这时对于数据的传递大多数人会去一层一层的传递数据,其实我们这样做是有隐患的,props传递层级过深,如果出现bug需逐层排查,而且代码不够优秀,不利于后人理解,并且维护成本高
!!!如果你已经足够了解context和useReducer如何使用,请直接阅读双剑合璧
通常我们的解决办法一般是用context或者使用一些状态管理器(redux,mobx,recoil...),但是有一点我们需要知道,并不是所有的项目都适合用redux,有时redux使用不当无疑是增加了我们的开发成本
什么时候使用redux?
- 组件需要根据状态发生显示变化
- state并不总是以单向的方式线性流动
- 存在组件需要更新全局状态
- 存在组件需要更新另一个组件的状态
- 存在状态以许多不同的方式更新
- 状态树结构复杂
- 某个状态需要在全局使用或共享(例如角色权限等信息)
2. context的使用
我们新建一个名为Context的页面,在里面嵌套一个层级很深的组件(一般开发中很常见),去实现一个跨级传参和兄弟组件传参
- Context 页面
- ParentComOne 父组件1
- ParentComTwo 父组件2
- SonCom 子组件
使用props传递的依赖图:
代码:
import React, {useState, useImperativeHandle, useRef, forwardRef} from 'react';
//页面
const Context = () => {
const [count, setCount] = useState(0);
const ref = useRef(); //定义引用用来获取ParentComOne向外暴露的方法
return (
<div>
<h1>context</h1>
{/*传递count和setCount*/}
<ParentComOne ref={ref} count={count} setCount={setCount}/>
{/*获取到setNow方法后传递给ParentComTwo组件*/}
<ParentComTwo setNow={ref.current?.setNow} />
</div>
);
};
//父组件1
const ParentComOne = forwardRef((props, ref) => {
const [now, setNow] = useState(Date.now());
//使用useImperativeHandle将setNow方法暴露
useImperativeHandle(ref, () => {
return {
setNow
}
})
return <div>
<h3>ParentComOne----{now}</h3>
{/*传递count和setCount*/}
<SonCom {...props}/>
</div>
})
//父组件2
const ParentComTwo = ({setNow}) => {
//调用ParentComOne的setNow方法
return <div onClick={() => setNow(Date.now())}>
<h3>ParentComTwo</h3>
</div>
}
//子组件
const SonCom = (props) => {
const {count, setCount} = props;
// 调用组件组件传递的值和方法去更改视图
return <div onClick={() => setCount(count + 1)}>
<h3>SonCom---{count}</h3>
</div>
}
export default Context;
效果如下:
这种方法虽然可以实现我们想要的效果,但是文章开头有提到过,它并不是一个好的方案
小试牛刀
使用createContext和useContext实现的依赖图:
代码:
import React, {useState, createContext, useContext} from 'react';
const TestContext = createContext({});
//页面
const Context = () => {
const [count, setCount] = useState(0);
const [now, setNow] = useState(Date.now());
return (
<div>
<h1>context</h1>
<TestContext.Provider value={{count, setCount, now, setNow}}>
<ParentComOne/>
<ParentComTwo/>
</TestContext.Provider>
</div>
);
};
//父组件1
const ParentComOne = () => {
const {now} = useContext(TestContext);
return <div>
<h3>ParentComOne----{now}</h3>
<SonCom/>
</div>
}
//父组件2
const ParentComTwo = () => {
const {setNow} = useContext(TestContext);
return <div onClick={() => setNow(Date.now())}>
<h3>ParentComTwo</h3>
</div>
}
//子组件
const SonCom = () => {
const {count, setCount} = useContext(TestContext);
return <div onClick={() => setCount(count + 1)}>
<h3>SonCom---{count}</h3>
</div>
}
export default Context;
效果如下,发现同样可以实现,我们集中管理各个组件状态,并且代码通俗易懂,依赖关系不复杂
3. useReducer的使用
import React, {useReducer} from 'react';
const initialState = {count: 0, now: Date.now()};
const reducer = (state, action) => {
switch (action.type) {
case 'now':
state.now = Date.now();
return {...state};
case 'add':
state.count++;
return {...state};
default :
return state;
}
};
const Reducer = () => {
const [state, dispath] = useReducer(reducer,initialState)
return (
<div>
<h1>reducer</h1>
<h3 onClick={() => dispath({type: 'now'})}>{state.now}</h3>
<h3 onClick={() => dispath({type: 'add'})}>{state.count}</h3>
</div>
);
};
export default Reducer;
useReducer的使用很简单,它可以使我们的状态与视图分离,让代码看上去更直观
效果如下:
4. 让大脑飞一会
我们明白context和reducer的特性以后,想办法把它们关联到一起就能去模拟redux啦,其实原理非常简单,有想法的小伙伴可以暂停阅读,先去项目里试试,如果想不到再回来继续看也不迟~
第一步: 初始化自己的state和reducer
第二部: 使用createContext创建context
第三部: 在最外层组件使用useReducer去创建自己的reducer,然后使用context.Provider去包裹需要进行状态管理的组件,最后将useReducer返回的state和dispatch传给context
第四步: 在组件内使用useContext去获取state和dispatch方法并使用
5. 双剑合璧
好了,那有了实现方案以后我们一步一步的去进行,为了演示效果更直观一些,我们将会使用reducer去管理一个列表数据,它可以进行增加数据和删除数据
在根目录下新建store以及store/index.js
import {createContext} from 'react';
export const initialState = {
//列表数据
list: [...Array.from({length: 3}).keys()]
};
const actionFunc = {
//增加一项
add(state, {value, callback}) {
//判断如果无内容给提示并return
if (value.trim() === '') {
alert('请输入内容')
return;
}
//push一条
state.list.push(value);
//添加成功的回调函数
callback();
},
//删除某一项
del(state, id) {
state.list = state.list.filter(item => item !== id);
}
}
export const reducer = (state, {type, payload}) => {
//浅拷贝数据
const newState = {...state};
//actionFunc对有应方法才去执行对应方法
actionFunc[type] && actionFunc[type](newState, payload);
return newState;
};
export const StoreContext = createContext();
修改App.js
import {HashRouter, Switch, Route, Redirect} from 'react-router-dom';
import ContextPage from './pages/context/Context'
import ReducerPage from './pages/reducer/Reducer'
import Test from "./pages/test/Test";
//引入我们刚刚定义的state、reducer、context
import {StoreContext, initialState, reducer} from './store';
import {useReducer} from 'react';
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<HashRouter>
<Switch>
{/*!!!将state和dispatch传递给context,这样它下面的所有组件都可以去使用!!!*/}
<StoreContext.Provider value={{state, dispatch}}>
<Route path={'/context'}><ContextPage/></Route>
<Route path={'/reducer'}><ReducerPage/></Route>
<Route path={'/test'}><Test/></Route>
</StoreContext.Provider>
</Switch>
</HashRouter>
);
}
export default App;
然后新建test页面
import React, {useContext, useRef} from 'react';
import {StoreContext} from '../../store';
import {Link} from 'react-router-dom';
const delStyles = {
paddingLeft: 20,
cursor: 'pointer'
}
const liStyles = {
margin: '5px 0px'
}
const Test = () => {
const {state: {list}, dispatch} = useContext(StoreContext);
const inputRef = useRef();
return (
<div>
<h1>test</h1>
<Link to={'/reducer'}>去reducer页面</Link><br/>
<input ref={inputRef} type="text"/>
<button onClick={() => {
dispatch({
type: 'add',
payload: {
value: inputRef.current?.value,
callback: () => {
inputRef.current.value = '';
}
}
})
}}>添加
</button>
<ul>
{
list.map(item => {
return <li key={item} style={liStyles}>
<span>{item}</span>
<span
style={delStyles}
onClick={() => {
dispatch({
type: 'del',
payload: item
})
}
}>删除</span>
</li>
})
}
</ul>
</div>
);
};
export default Test;
效果如下:
6. 结尾
到这里我们最终的效果就实现了,原理是不是很简单呢
它就这几行代码,真的能实现redux所有的功能吗?
这种方式会不会性能不太好啊?
如果同学们有上面的疑问,我暂时先不回答哈,等有时间我会在出一篇redux源码解析的文章,有疑问的同学记得来捧场哦~
最后的最后附上git仓库地址