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相关推荐

  1. 解鞍卸甲——手写简易版Spring框架(终):使用三级缓存解决循环依赖问题

    什么是三级缓存 按照目前我们实现的 Spring 框架,是可以满足一个基本需求的,但如果你配置了A.B两个Bean对象互相依赖,那么立马会抛出 java.lang.StackOverflowError ...

  2. 手写简易版链表及原理分析

    好多人都觉得为什么要自己写这样的数据结构,变成里面不是有吗?为什么要去写,有这个疑问,其实这个疑问这我的脑海中也存在了很长一段时间,本人是学习java编程的,直接看java的集合框架不行吗?这个时候如 ...

  3. spring源码分析01-(前期准备)spring核心原理解析和手写简易spring

    1.本文主要介绍内容 本文会把Spring中核心知识点大概解释下.可以对Spring的底层有一个整体的大致了解.主要内容包括: 手写简易spring框架,帮助更好理解spring. 代码点击链接自取 ...

  4. 【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(二、JavaScript 异步编程)

    [学习笔记]Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程.手写 Promise(课前准备) [学习笔记]Part1·JavaScript·深度剖析-函数式编程与 JS 异步 ...

  5. 手写简易WEB服务器

    手写简易WEB服务器 今天我们来写一个类似于Tomcat的简易服务器.可供大家深入理解一下tomcat的工作原理,本文仅供新手参考,请各位大神指正! 首先我们要准备的知识是: Socket编程 HTM ...

  6. 手写简易版 React 来彻底搞懂 fiber 架构

    React 16 之前和之后最大的区别就是 16 引入了 fiber,又基于 fiber 实现了 hooks.整天都提 fiber,那 fiber 到底是啥?它和 vdom 是什么关系? 与其看各种解 ...

  7. 5 拦截器拦截请求路由_手写简易版axios拦截器,实现微信小程序wx.request的封装与拦截...

    前言: axios是一个功能强大的网络请求库,其中拦截器又是axios的精髓.在小程序的开发或者需要手动实现ajax的时候,没有实现对请求的拦截,开发的时候非常不方便,因此手写一个简易版的axios拦 ...

  8. JS手写上传文件、React手写上传文件

    目录 JS手写 React上传文件 JS手写 <!DOCTYPE html> <html lang="en"><head><meta ch ...

  9. 手写数字图片二值化转换为32*32数组。

    最近课设外加生病,本来打算在上一篇机器学习使用k-近邻算法改进约会网站的配对效果.就打算写的一直没有时间.按照<机器学习实战>的流程,手写数字识别是kNN中的最后一部分,也是一个比较经典的 ...

最新文章

  1. android环境安装之android4.2安装(转)
  2. 基于深度学习的事件因果关系抽取综述
  3. Redis设计与实现 第一部分
  4. python中常见的运行时错误_17个常见Python运行时错误
  5. python中循环结构关键字,04.循环结构
  6. ssh框架常见错误与解决方法
  7. Python 列表 list 数组 array
  8. 《4月份数据库技术通讯》.pdf
  9. STC51-键盘检测
  10. C++学习笔记(十二):重载函数
  11. 017指北与游移方位惯导系统知识梳理
  12. Android Builder模式
  13. 医院业务系统灾备建设,数腾:为生命保驾护航
  14. 创业公司一年工作总结(转载)
  15. UVM中 sequence中的starting_phase
  16. css实现超过两行用...表示
  17. 电脑连接wifi总是断 手机正常 解决方案
  18. 聚名:“虎虎酒”商标正在申请中,电视剧《赘婿》引商标注册热潮!
  19. 微机原理与接口技术系列笔记(一)
  20. 如何恢复计算机中被隐藏的文件夹,电脑中病毒后,文件与文件夹被隐藏,如何恢复正常显示。...

热门文章

  1. Java //PP2.16编写一个applet,画出北斗七星,并在夜空中添加一些其他的星星
  2. --Redis入坑--RedisPipelineException:Pipeline contained one or more invalid commands;WRONGTYPE ...
  3. Android Notification 详解(各版本对比)——基本操作
  4. 无线电能传输LCC-S拓扑/WPT MATLAB/simulink仿真模型
  5. 深信服上网行为管理 准入规则库介绍
  6. 汉家江湖无名幻境服务器找不到,汉家江湖无名幻境怎么打开_汉家江湖无名幻境开启方式攻略_玩游戏网...
  7. Probabilistic Foot Contact Estimation by Fusing Information from Dynamics and Differential/Forward K
  8. NAT模式下如何Ping通百度
  9. pip install下载一些包很慢,可-i 从豆瓣源下载
  10. 网上计算机能力提升研修心得,信息技术应用能力提升个人研修总结