React,手写简易redux(二)- By Viga
React,手写简易redux(二)
本章节会完成一个简易的redux实现
该系列内容会逐步实现简易的redux
使用技术栈:vite+react
该系列感谢@方应杭 的react教学视频
目录
- 实现reducer、dispatch
- 实现connect
- 实现connect参数一selector
- connect参数-mapDispatchToProps
connect
上一章中,我们已经实现了简化的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 <Component dispatch={dispatch} state={appState} {...props}/>;
// };
// };
// 创建一个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,}},// 用来修改statesetState:(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,}},// 用来修改statesetState:(newState)=>{store.state = newState;// 在每次修改state时,遍历订阅者这个数组,并调用它们的回调函数,同时也可以传入最新的statestore.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,},},// 用来修改statesetState: (newState) => {store.state = newState;// 在每次修改state时,遍历订阅者这个数组,并调用它们的回调函数,同时也可以传入最新的statestore.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以及精准更新
未完待续
React,手写简易redux(二)- By Viga相关推荐
- 解鞍卸甲——手写简易版Spring框架(终):使用三级缓存解决循环依赖问题
什么是三级缓存 按照目前我们实现的 Spring 框架,是可以满足一个基本需求的,但如果你配置了A.B两个Bean对象互相依赖,那么立马会抛出 java.lang.StackOverflowError ...
- 手写简易版链表及原理分析
好多人都觉得为什么要自己写这样的数据结构,变成里面不是有吗?为什么要去写,有这个疑问,其实这个疑问这我的脑海中也存在了很长一段时间,本人是学习java编程的,直接看java的集合框架不行吗?这个时候如 ...
- spring源码分析01-(前期准备)spring核心原理解析和手写简易spring
1.本文主要介绍内容 本文会把Spring中核心知识点大概解释下.可以对Spring的底层有一个整体的大致了解.主要内容包括: 手写简易spring框架,帮助更好理解spring. 代码点击链接自取 ...
- 【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(二、JavaScript 异步编程)
[学习笔记]Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程.手写 Promise(课前准备) [学习笔记]Part1·JavaScript·深度剖析-函数式编程与 JS 异步 ...
- 手写简易WEB服务器
手写简易WEB服务器 今天我们来写一个类似于Tomcat的简易服务器.可供大家深入理解一下tomcat的工作原理,本文仅供新手参考,请各位大神指正! 首先我们要准备的知识是: Socket编程 HTM ...
- 手写简易版 React 来彻底搞懂 fiber 架构
React 16 之前和之后最大的区别就是 16 引入了 fiber,又基于 fiber 实现了 hooks.整天都提 fiber,那 fiber 到底是啥?它和 vdom 是什么关系? 与其看各种解 ...
- 5 拦截器拦截请求路由_手写简易版axios拦截器,实现微信小程序wx.request的封装与拦截...
前言: axios是一个功能强大的网络请求库,其中拦截器又是axios的精髓.在小程序的开发或者需要手动实现ajax的时候,没有实现对请求的拦截,开发的时候非常不方便,因此手写一个简易版的axios拦 ...
- JS手写上传文件、React手写上传文件
目录 JS手写 React上传文件 JS手写 <!DOCTYPE html> <html lang="en"><head><meta ch ...
- 手写数字图片二值化转换为32*32数组。
最近课设外加生病,本来打算在上一篇机器学习使用k-近邻算法改进约会网站的配对效果.就打算写的一直没有时间.按照<机器学习实战>的流程,手写数字识别是kNN中的最后一部分,也是一个比较经典的 ...
最新文章
- android环境安装之android4.2安装(转)
- 基于深度学习的事件因果关系抽取综述
- Redis设计与实现 第一部分
- python中常见的运行时错误_17个常见Python运行时错误
- python中循环结构关键字,04.循环结构
- ssh框架常见错误与解决方法
- Python 列表 list 数组 array
- 《4月份数据库技术通讯》.pdf
- STC51-键盘检测
- C++学习笔记(十二):重载函数
- 017指北与游移方位惯导系统知识梳理
- Android Builder模式
- 医院业务系统灾备建设,数腾:为生命保驾护航
- 创业公司一年工作总结(转载)
- UVM中 sequence中的starting_phase
- css实现超过两行用...表示
- 电脑连接wifi总是断 手机正常 解决方案
- 聚名:“虎虎酒”商标正在申请中,电视剧《赘婿》引商标注册热潮!
- 微机原理与接口技术系列笔记(一)
- 如何恢复计算机中被隐藏的文件夹,电脑中病毒后,文件与文件夹被隐藏,如何恢复正常显示。...
热门文章
- Java //PP2.16编写一个applet,画出北斗七星,并在夜空中添加一些其他的星星
- --Redis入坑--RedisPipelineException:Pipeline contained one or more invalid commands;WRONGTYPE ...
- Android Notification 详解(各版本对比)——基本操作
- 无线电能传输LCC-S拓扑/WPT MATLAB/simulink仿真模型
- 深信服上网行为管理 准入规则库介绍
- 汉家江湖无名幻境服务器找不到,汉家江湖无名幻境怎么打开_汉家江湖无名幻境开启方式攻略_玩游戏网...
- Probabilistic Foot Contact Estimation by Fusing Information from Dynamics and Differential/Forward K
- NAT模式下如何Ping通百度
- pip install下载一些包很慢,可-i 从豆瓣源下载
- 网上计算机能力提升研修心得,信息技术应用能力提升个人研修总结