React,手写简易redux(二)- By Viga

React,手写简易redux(二)- By Viga,第1张

React,手写简易redux(二)

本章节会完成一个简易的redux实现

该系列内容会逐步实现简易的redux
使用技术栈:vite+react
该系列感谢@方应杭 的react教学视频

目录 实现reducer、dispatch实现connect实现connect参数一selectorconnect参数-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 ;
//   };
// };
// 创建一个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以及精准更新

未完待续

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1320059.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-11
下一篇 2022-06-11

发表评论

登录后才能评论

评论列表(0条)

保存