本章节会完成一个简易的redux实现
目录 实现reducer、dispatch实现connect实现connect参数一selectorconnect参数-mapDispatchToProps connect该系列内容会逐步实现简易的redux
使用技术栈:vite+react
该系列感谢@方应杭 的react教学视频
上一章中,我们已经实现了简化的reduce和dispatch,这一张我们需要实现react中connect函数,通过connect来包装每个组件为其提供redux的方法。
// 创建一个createWrapper函数,用于包装组件
// const createWrapper = (Component) => {
// return (props) => {
// const { appState, setValue } = useContext(appContext);
// const dispatch = (action) => {
// setValue(reducer(appState, action));
// };
// return ;
// };
// };
// 创建一个createWrapper函数,用于包装组件
// 我们修改一下名称
const connect= (Component) => {
return (props) => {
const { appState, setValue } = useContext(appContext);
const dispatch = (action) => {
setValue(reducer(appState, action));
};
return <Component dispatch={dispatch} state={appState} {...props}/>;
};
};
const 张三Wrapper = connect(张三);
以上代码通过给connect传递一个组件后返回一个包装后的组件,包装后的组件拥有dispatch于state的props。
上一章末尾留了个说明,目前代码是有问题的,具体有什么问题,我们加上一些console打印一下各组件看看。
const 张三 = ({ dispatch, state }) => {
// 可以从props中拿到dispatch和state
// 修改年龄值
function changeAge(e) {
const val = e.target.value;
dispatch({ type: "changeAge", payload: { age: val } });
}
console.log("张三");
return (
<div>
<p>张三</p>
<div>
<input value={state.user.age} onChange={changeAge} />
</div>
</div>
);
};
const 李四 = () => (
console.log("lisi"),
(
<div>
<p>李四</p>名字:{useContext(appContext).appState.user.name},年龄:
{useContext(appContext).appState.user.age}
</div>
)
);
const 王五 = () => {
console.log("王五")
return <div>王五</div>;
}
我给每个组件都加上了console.log打印,问题发生了,当编辑了张三后, 张三、李四、王五都进行了render。其中王五并没有用到任何的状态,但也发生了更新。这肯定是不需要的,问题出在,我的setValue
上,setValue是在App根组件中的,react规定,当调用setState函数后,一定会重新执行函数,一旦重新之心。
这个问题,其实你也可以通过useMemo来解决,缓存props后子组件中接受的props地址不变就不会重新渲染。
不过在这里,我们更期望能提供一种方式:
在user数据变化时,只有使用到user数据的组件才执行更新
实现这个功能的第一步,就需要把App组件中的useState干掉,我们在外部定义一个对象叫store
,
const connect = (Component) => {
return (props) => {
const { state, setState } = useContext(appContext);
const dispatch = (action) => {
setState(reducer(state, action));
};
return <Component dispatch={dispatch} state={state} {...props} />;
};
};
// Store
const store ={
// 用来存放初始数据
state:{
user: {
name: "张三",
age: 18,
}
},
// 用来修改state
setState:(newState)=>{
store.state = newState;
}
}
const App = () => {
// 使用store代替contextValue
// const contextValue = { appState, setValue };
return (
<appContext.Provider value={store}>
{/**将store传递给上下文*/}
<div className="container">
<div className="box">
<张三Wrapper />
</div>
<div className="box">
<李四 />
</div>
<div className="box">
<王五 />
</div>
</div>
</appContext.Provider>
);
};
我在外部定义了一个store对象,内部有一个state用于存放共享的属性,还有一个setState来修改state内容,再把store代替之前的contextValue传递给上下文。让子组件使用的是store中的state和setState。
注意,以上代码有个问题,让输入的时候虽然实际数据有变化但是并不会更新视图,原因是我们没有调用useState来驱动视图更新
这里需要添加一个方法,让数据变更时,可以执行更新视图,我们在connect里修改。
const connect = (Component) => {
return (props) => {
const { state, setState } = useContext(appContext);
const [,update] =useState({}) // +
const dispatch = (action) => {
setState(reducer(state, action));
update({}) // +执行这个update 因为{}==={} //false
};
return <Component dispatch={dispatch} state={state} {...props} />;
};
};
运行
只有张三组件更新,李四没有更新,原因我的更新是在connect中的dispatch,但是每个被connect包装后的组件,dispatch都是唯一的,我们实际上只执行了张三的update。
这一步我们就需要做一个订阅功能,让所有订阅了user数据的组件,在user变化是,都执行。
// 创建一个createWrapper函数,用于包装组件
const connect = (Component) => {
return (props) => {
const { state, setState } = useContext(appContext);
const [,update] =useState({})
useEffect(()=>{
// 在组件第一次渲染时,订阅store,给store传入自身的update功能
store.subscribe(()=>{
update({})
})
},[])
const dispatch = (action) => {
setState(reducer(state, action));
};
return <Component dispatch={dispatch} state={state} {...props} />;
};
};
// Store
const store ={
// 用来存放初始数据
state:{
user: {
name: "张三",
age: 18,
}
},
// 用来修改state
setState:(newState)=>{
store.state = newState;
// 在每次修改state时,遍历订阅者这个数组,并调用它们的回调函数,同时也可以传入最新的state
store.listeners.map(fn=>fn(store.state))
},
listeners:[], // + 订阅者
// 订阅函数
subscribe(callback){
store.listeners.push(callback)
// 应该返回一个取消订阅功能
return ()=>{
const index = store.listeners.indexOf(callback)
store.listeners.splice(index,1)
}
}
}
const 张三Wrapper = connect(张三);
const 李四Wrapper = connect(李四); // + 将李四也通过connect包裹一下,这样等于加入了store的大家庭,可以共享数据于更新
const App = () => {
return (
<appContext.Provider value={store}>
{/**将store传递给上下文*/}
<div className="container">
<div className="box">
<张三Wrapper />
</div>
<div className="box">
<李四Wrapper />
</div>
<div className="box">
<王五 />
</div>
</div>
</appContext.Provider>
);
};
现在测试一下。
只有张三和李四更新了,王五没有,功能成功。
灰色是devtool自带的打印,忽略即可
接下来优化一下这次的代码,目前已经基本实现了redux的大部分功能,我们把它抽离到一个单独的文件中,新建一个redux.js,将connect、store、appContext、reducer移进来
// redux.js
import React, { useContext, useEffect, useState } from "react";
// 1、创建上下文
const appContext = React.createContext(null);
// reducer
const reducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case "changeAge":
return {
...state,
user: {
...state.user,
...payload,
},
};
default:
return state;
}
};
// Store
const store = {
// 用来存放初始数据
state: {
user: {
name: "张三",
age: 18,
},
},
// 用来修改state
setState: (newState) => {
store.state = newState;
// 在每次修改state时,遍历订阅者这个数组,并调用它们的回调函数,同时也可以传入最新的state
store.listeners.map((fn) => fn(store.state));
},
listeners: [],
// 订阅函数
subscribe(callback) {
store.listeners.push(callback);
return () => {
const index = store.listeners.indexOf(callback);
store.listeners.splice(index, 1);
};
},
};
// 创建一个connect函数,用于包装组件
const connect = (Component) => {
return (props) => {
const { state, setState } = useContext(appContext);
const [, update] = useState({});
useEffect(() => {
// 在组件第一次渲染时,订阅store,给store传入自身的update功能
const upSubscription = store.subscribe(() => {
update({});
});
return upSubscription;
}, []);
const dispatch = (action) => {
setState(reducer(state, action));
};
return <Component dispatch={dispatch} state={state} {...props} />;
};
};
export {
connect,appContext,reducer,store
}
App中只保留组件,同时
// App.jsx
import React from "react";
import "./App.css";
import { connect ,appContext,store} from './redux.jsx'
const 张三 = connect(({ dispatch, state }) => {
// 可以从props中拿到dispatch和state
// 修改年龄值
function changeAge(e) {
const val = e.target.value;
dispatch({ type: "changeAge", payload: { age: val } });
}
console.log("张三");
return (
<div>
<p>张三</p>
<div>
<input value={state.user.age} onChange={changeAge} />
</div>
</div>
);
})
// 使用connect包装一下,这样可以拿到store中的dispatch和state
const 李四 = connect(({dispatch, state}) => (
console.log('李四'),
(
<div>
<p>李四</p>名字:{state.user.name},年龄:
{state.user.age}
</div>
)
))
const 王五 = () => {
console.log("王五")
return <div>王五</div>;
}
// const 张三Wrapper = connect(张三) -
// const 李四Wrapper = connect(李四) -
const App = () => {
return (
<appContext.Provider value={store}>
{/**将store传递给上下文*/}
<div className="container">
<div className="box">
<张三 />
</div>
<div className="box">
<李四 />
</div>
<div className="box">
<王五 />
</div>
</div>
</appContext.Provider>
);
};
export default App;
总结
至此,一个简单的redux就大部分完成了,回顾一下本章,一开始我们通过在App中定义state并传递上下文达到全局共享state的功能,但这样的问题就是每次修改state后,所有的子组件都会更新渲染。为了防止这种事件,我们在外部定义一个store来存放state数据和setState修改函数,同时由于setState函数本身并不会引起组件重新渲染,我们需要在connect中定义一个update来强制执行组件更新。
并且为了让所有使用到数据的组件更新,我们需要写一个订阅功能,订阅state,当state变化时更新自身。
以上功能还存在一个问题,那就是我们订阅的是state,在全局state变化时组件就会执行更新。但是假设2个组件使用的属性不同,比如一个用到了state.user 一个用到了state.name ,在user更新时,只使用name的组件是不应该执行更新的,也就是,组件应该只在自身使用到的数据变化时才更新
,也就是我们下一章要实现的精准更新
。
下一章将实现connect的第一个参数中的selector以及精准更新
未完待续欢迎分享,转载请注明来源:内存溢出
评论列表(0条)