手写 redux

1. 创建一个全局读写的 state

import React, { useContext, useState } from "react";
const appContext = React.createContext(null);

const App = () => {
  const [appState, setAppState] = useState({
    user: {
      name: "lifa",
      age: 18
    }
  });
  return (
    <appContext.Provider value={{ appState, setAppState }}>
      <大老婆 />
      <二老婆 />
      <三老婆 />
    </appContext.Provider>
  );
};
const 大老婆 = () => (
  <section>
    大老婆
    <User />
  </section>
);
const 二老婆 = () => (
  <section>
    二老婆
    <UserModifier />
  </section>
);
const 三老婆 = () => <section>三老婆</section>;
const User = () => {
  const contextValue = useContext(appContext);
  return <div>User: {contextValue.appState.user.name}</div>;
};
const UserModifier = () => {
  const { appState, setAppState } = useContext(appContext);
  const onChange = (event) => {
    appState.user.name = event.target.value;
    setAppState({
      ...appState
    });
  };
  return (
    <div>
      <input value={appState.user.name} onChange={onChange} />
    </div>
  );
};
export default App;

2. reducer 的由来(规范 state,创建流程的一个函数)

appState.user.name = event.target.value;

上面我们的代码修改 state 的时候直接修改了原始 state,所以我们需要一个函数来创建新的 state,reducer就是这个函数,所以reducer 就是用来规范 state,创建流程的一个函数

const reducer = (state, actionType, actionData) => {
  if (actionType === "updateUser") {
    return {
      ...state,
      user: {
        ...state.user,
        ...actionData
      }
    };
  } else {
    return state;
  }
};
setAppState(
      reducer(appState, "updateUser", { name: event.target.value })
 );

对 reducer 的参数进行修改

const reducer = (state, {type, payload}) => {
  if (type === "updateUser") {
    return {
      ...state,
      user: {
        ...state.user,
        ...payload
      }
    };
  } else {
    return state;
  }
};

setAppState(
    reducer(appState, {type: "updateUser", payload: { name: event.target.value} })
);

3. dispatch 的由来(规范 setState 流程)

我们每次 setState 的时候都要写一遍 setAppState(reducer(appState, ...)),比如我们要修改 state 里的 age

setAppState(
     reducer(appState, {type: "updateUser", payload: { age: 16 } })
);

这样我们干脆用一个函数来代替setAppState(reducer(appState这一部分,规范 setState 流程

const dispatch = (action) => {
  setAppState(reducer(appState, action))
}
const UserModifier = () => {
  const {appState, setAppState} = useContext(appContext)
  dispatch({type: 'updateUser', payload: {name: event.target.value}})
}

问题:上面代码我们没办法正常运行,因为我们的 dispatch 是当前的全局函数,它是没有办法获取到我们组件内部的 setAppState 和 appState 的
解决方法:使用一个组件包裹 dispatch 然后把这个 dispatch 作为 props 传给 UserModifier 组件

const 二老婆 = () => (
  <section>
    二老婆
    <Wrapper />
  </section>
);
const Wrapper = () => {
  const { appState, setAppState } = useContext(appContext);
  const dispatch = (action) => {
    setAppState(reducer(appState, action));
  };
  return <UserModifier dispatch={dispatch} state={appState} />;
};
const UserModifier = ({ dispatch, state }) => {
  const onChange = (event) => {
    dispatch({ type: "updateUser", payload: { name: event.target.value } });
  };
  return (
    <div>
      <input value={state.user.name} onChange={onChange} />
    </div>
  );
};

4. connect 的由来(让组件与全局状态连接起来)

上面我们使用 Wrapper 封装了 UserModifiler 然后把 dispatch 和 state 传给了它,那么是不是我们每个组件只要用到 dispatch 都要封装一个 Wrapper 哪?
所以我们需要用一个函数来帮我们自动的创建 Wrapper

const createWrapper = (Component) => {
  const Wrapper = () => {
    const { appState, setAppState } = useContext(appContext);
    const dispatch = (action) => {
      setAppState(reducer(appState, action));
    };
    return <Component dispatch={dispatch} state={appState} />;
  };
  return Wrapper;
}
const UserModifier = ({ dispatch, state }) => {
  const onChange = (event) => {
    dispatch({ type: "updateUser", payload: { name: event.target.value } });
  };
  return (
    <div>
      <input value={state.user.name} onChange={onChange} />
    </div>
  );
};
const Wrapper = createWrapper(UserModifier)

对上面的代码我们可以直接把UserModifier组件写到createWrapper里,然后把我们的 Wrapper 直接写成我们组件的名字,再把我们的 createWrapper 改名为 connect

const connect = (Component) => {
  const Wrapper = () => {
    const { appState, setAppState } = useContext(appContext);
    const dispatch = (action) => {
      setAppState(reducer(appState, action));
    };
    return <Component dispatch={dispatch} state={appState} />;
  };
  return Wrapper;
}
const UserModifier = connect(({ dispatch, state }) => {
  const onChange = (event) => {
    dispatch({ type: "updateUser", payload: { name: event.target.value } });
  };
  return (
    <div>
      <input value={state.user.name} onChange={onChange} />
    </div>
  );
})

然后我们 connect 里的 Wrapper 组件也可以直接返回

const connect = (Component) => {
  return () => {
    const { appState, setAppState } = useContext(appContext);
    const dispatch = (action) => {
      setAppState(reducer(appState, action));
    };
    return <Component dispatch={dispatch} state={appState} />;
  };
}

如果组件需要传参,我们需要把我们的 props 直接传入到我们对应的子组件里

const connect = (Component) => {
  return (props) => {
    const { appState, setAppState } = useContext(appContext);
    const dispatch = (action) => {
      setAppState(reducer(appState, action));
    };
    return <Component {...props} dispatch={dispatch} state={appState} />;
  };
}

如果组件里面有内容或者子组件,我们只需要在我们对应组件里拿到 children 就好了,因为children也在上面的 props 里

<UserModifier age={18}>
      小姐姐
</UserModifier>
const UserModifier = connect(({ dispatch, state, children, age }) => {
  const onChange = (event) => {
    dispatch({ type: "updateUser", payload: { name: event.target.value } });
  };
  return (
    <div>
      {children}
      {age}
      <input value={state.user.name} onChange={onChange} />
    </div>
  );
})

我们的 connect 就是一个高阶组件,所谓的高阶组件就是一个函数接受一个组件返回一个新的组件;

4.1. 利用 connect 减少 render

上面的代码我们每次修改二老婆里的数据都会调用父组件App的 setAppState,而 react 里只要你调用这个组件的 setState 并且传了一个新的引用那么这个组件就一定会重新 render,而 App 重新 render 了,它里面的子组件也一定会重新 render。
那么怎么避免或者减少这些无用的 render 那?

  1. 我们可以使用 useMemo 对每个子组件包一层,只有在初始化的时候才让他重新渲染
const 小姐姐 = useMemo(() => {
    return <大老婆 />
  }, [appState.user.age])

但是这样会非常麻烦我们需要对每个组件都加一层 useMemo

  1. 我们想只有用到 user 的地方,user 变化的时候才重新执行,所以我们不能使用 setState,这时我们可以用一个全局的 store
const store = {
  state: {
    user: {name: 'linlin', age: 12}
  },
  setState(newState) {
    store.state = newState
  }
}
const App = () => {
  return (
    <appContext.Provider value={store}>
      <大老婆 />
      <二老婆 />
      <三老婆 />
    </appContext.Provider>
  );
};
const connect = (Component) => {
  return (props) => {
    const { state, setState } = useContext(appContext);
    const dispatch = (action) => {
      setState(reducer(state, action));
    };
    return <Component {...props} dispatch={dispatch} state={state} />;
  };
};
const UserModifier = connect(({ dispatch, state, children, age }) => {
  const onChange = (event) => {
    dispatch({ type: "updateUser", payload: { name: event.target.value } });
  };
  return (
    <div>
      {children}
      {age}
      <input value={state.user.name} onChange={onChange} />
    </div>
  );
});

问题1:上面的代码我们修改 input 内容没有改变
原因:是因为我们没有调用 useState 里的 setState 所以 ui 不会更新
解决办法:在 store 的 setState 后紧接着调用一下 useState 的 setState

const connect = (Component) => {
  return (props) => {
    const { state, setState } = useContext(appContext);
+    const [, update] = useState({});
    const dispatch = (action) => {
      setState(reducer(state, action));
+      update({});
    };
    return <Component {...props} dispatch={dispatch} state={state} />;
  };
};

问题2:上面我们的 input 内容可以修改了,但是大老婆里用到的 use.name 还是之前的没有更新?
原因:我们每一个组件都有一个自己的dispatch,所以每个组件只能更新自己
解决方法:使用发布订阅模式,让每个组件订阅每一次的变化

const store = {
  state: {
    user: { name: "linlin", age: 12 }
  },
  setState(newState) {
    store.state = newState;
    store.listeners.map((fn) => fn(store.state));
  },
  listeners: [],
  subscribe(fn) {
    store.listeners.push(fn);
    return () => {
      const index = store.listeners.indexOf(fn);
      store.listeners.splice(index, 1);
    };
  }
}
const connect = (Component) => {
  return (props) => {
    const { state, setState } = useContext(appContext);
    const [, update] = useState({});
    useEffect(() => {
      store.subscribe(() => {
        update({});
      });
    }, []);
    const dispatch = (action) => {
      setState(reducer(state, action));
    };
    return <Component {...props} dispatch={dispatch} state={state} />;
  };
};
const User = connect(() => {
  const contextValue = useContext(appContext);
  return <div>User: {contextValue.state.user.name}</div>;
});

上面的代码有几个 connect 组件就会在listeners push 几个更新的函数,所以只要有一个组件使用了 setState,其他的组件也都会触发 setState,重新 render
对 redux 文件拆分
https://codesandbox.io/s/cocky-heisenberg-t3fpi?file=/src/redux.js

4.2. connect 支持 mapStateToProps

我们通过上面的 connect 拿到的 state 是全部的 state,这样如果我们 state 的层级很深的话,我们想要用 state 里某一个局部属性,写起来就会很复杂,所以我们考虑能不能我用哪个属性,connect 可以直接返回给我

export const connect = (selector) => (Component) => {
  return (props) => {
    const { state, setState } = useContext(appContext);
    const [, update] = useState({});
    const data = selector ? selector(state) : {state};
    useEffect(() => {
      store.subscribe(() => {
        update({});
      });
    }, []);
    const dispatch = (action) => {
      setState(reducer(state, action));
    };
    return <Component {...props} {...data} dispatch={dispatch} state={state} />;
  };
};
const User = connect(state => {
  return {user: state.user}
})(({user}) => {
  return <div>User: {user.name}</div>
})

4.3. 精准渲染(组件只在自己的数据变化时 render)

state: {
    user: { name: "linlin", age: 12 },
    group: { name: 'one', num: 5}
  },
const 二老婆 = () => (
  <section>
    二老婆
    <UserModifier age={18}>小姐姐</UserModifier>
  </section>
);
const 三老婆 = connect((state) => {
  return { group: state.group };
})(({ group }) => {
  console.log("三老婆执行了", Math.random());
  return <section>三老婆 {group.name}</section>;
});
const User = connect((state) => {
  return { user: state.user };
})(({ user }) => {
  console.log("user执行了", Math.random());
  return <div>User: {user.name}</div>;
});

上面代码我们每次修改 UserModifier 组件里的 user.name 的时候三老婆组件也会重新 render

解决方法:在 connect 里判断如果 data 没有改变就不去渲染

const changed = (oldState, newState) => {
  let changed = false;
  for (let key in oldState) {
    if (oldState[key] !== newState[key]) {
      changed = true;
    }
  }
  return changed;
};
const data = selector ? selector(state) : { state };
    useEffect(() => {
      store.subscribe(() => {
      // 因为这里是每次 setState 后执行的,所以通过这里拿到的store.state 就是最新的
        const newData = selector
          ? selector(store.state)
          : { state: store.state };
        if (changed(data, newData)) {
          update({});
        }
      });
    }, [selector]);

问题:如果我们 selector 变了会触发多次的 update,所以我们需要在每一次 selector 更新的时候 取消之前的订阅

useEffect(() => {
      return store.subscribe(() => {
        const newData = selector
          ? selector(store.state)
          : { state: store.state };
        if (changed(data, newData)) {
          console.log("update");
          update({});
        }
      });
    }, [selector]);

上面代码初始化和每次 selector 变了的时候都会执行 store.subscribe 方法,而 return 后面 store.subscribe 执行的返回函数,只有在旧的 selector 销毁,新的 selector 产生(selector 变化的时候)才会执行,所以每次都是先执行 store.subscribe(),selector 变化的时候再执行 return 里的取消订阅,这样每次新添加一个订阅前都会取消旧的订阅(除初始化外)

4.4. connect 支持 mapDispatchToProps

connect(selector, mapDispatchToProps)(组件)
我们上面的 connect 如果需要通过 dipatch 修改 state ,需要写下面这样的代码

const UserModifier = connect()(({ dispatch, state, children, age }) => {
  const onChange = (event) => {
    dispatch({ type: "updateUser", payload: { name: event.target.value } });
  };
}

我们希望可以通过下面代码的方式来触发 dsipatch

const UserModifier = connect(null, (dispatch) => {
  return {
    updateUser: (attrs) => dispatch({type: 'updateUser', payload: attrs})
  }
})(({ updateUser, state, children, age }) => {
  const onChange = (event) => {
    updateUser({ name: event.target.value });
  };
  ...
}

改造我们的 connect

export const connect = (selector, dispatchSelector) => (Component) => {
  return (props) => {
    const dispatch = (action) => {
      setState(reducer(state, action));
    };
 +   const dispatchers = dispatchSelector ? dispatchSelector(dispatch) : { dispatch };
+    return <Component {...props} {...data} {...dispatchers} />;
  };
};

4.5. connect api 的意义

connect(mapStateToProps, mapDispatchToProps)(组件)

1). 提供了提取读写接口的一种方式,这样你就不用重复的去告诉 react 你该怎么去读,该怎么去写
比如:

const User = connect((state) => {
  return { user: state.user };
})(({ user }) => {
  console.log("user执行了", Math.random());
  return <div>User: {user.name}</div>;
});
const UserModifier = connect(null, (dispatch) => {
  return {
    updateUser: (attrs) => dispatch({type: 'updateUser', payload: attrs})
  }
})

我们可以对上面的代码里的 user 和 updateUser 进行提取,这样我们后面用到的时候就不用再重复的去写一遍

// 提取的读 user 的接口
const userSelector = state => {
  return {user: state.user}
}
// 提取的写 user 的接口
const userDispatcher = dispatch => {
  return {
    updateUser: (attrs) => dispatch({type: 'updateUser', payload: attrs})
  }
}
const User = connect(userSelector)(({ user }) => {
  return <div>User: {user.name}</div>;
});
const UserModifier = connect(userSelector, userDispatcher)(({ updateUser, user }) => {
  const onChange = (event) => {
    updateUser({ name: event.target.value });
  };
  return (
    <div>
      <input value={user.name} onChange={onChange} />
    </div>
  );
});

2). 如果我们的读写接口每个都需要用到我们还可以进一步的把读写接口封装成一个,这样我们只需要维护我们这个读写接口方法的 connect 就可以了

const connectToUser = connect(userSelector, userDispatcher);
const User = connectToUser(({ user }) => {
  return <div>User: {user.name}</div>;
});
const UserModifier = connectToUser(({ updateUser, user }) => {
  const onChange = (event) => {
    updateUser({ name: event.target.value });
  };
  return (
    <div>
      <input value={user.name} onChange={onChange} />
    </div>
  );
});

5. createStore(reducer, initState)

我们上面的 store 和 reducer 是直接在外面写死的,所以我们需要通过一个 api 让用户传入自己定义的 reducer 和 初始化 store 传到 redux 里,也就是 createStore

export const store = {
  state: undefined,
  reducer: undefined,
  ...
}
export const createStore = (reducer, initState) => {
  store.state = initState;
  store.reducer = reducer;
  return store;
};

使用 createStore 修改我们的代码

const reducer = (state = initState, { type, payload }) => {
  if (type === "updateUser") {
    return {
      ...state,
      user: {
        ...state.user,
        ...payload
      }
    };
  } else {
    return state;
  }
};
const initState = {
  user: { name: "linlin", age: 18 },
  group: { name: "ccc" }
};
const store = createStore(reducer, initState);

const App = () => {
  return (
    <appContext.Provider value={store}>
      <大老婆 />
      <二老婆 />
      <三老婆 />
    </appContext.Provider>
  );
};

6. 封装 Provider

export const Provider = ({store, children}) => (
  <appContext.Provider value={store}>
      {children}
  </appContext.Provider>
)
const App = () => {
  return (
    <Provider store={store}>
      <大老婆 />
      <二老婆 />
      <三老婆 />
    </Provider>
  );
};

7. redux 支持异步 action

7.1. 函数 action

到现在为止我们的 redux 不支持异步 action,如果我们要写异步的 action 就会像下面的代码这样

const ajax = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({data: {name: '3秒后的lifa'}})
    }, 3000)
  })
}
const fetchUser = (dispatch) => {
  ajax('/user').then(response => {
    dispatch({type: 'updateUser', payload: response.data})
  })
}
const UserModify = connect(state => {
  return {
    name: state.user.name
  }
}, null)(({dispatch, name}) => {
  const onClick = () => {
    fetchUser(dispatch)
  }
  return (
    <div>
      <div>User: {name}</div>
      <button onClick={onClick}>异步获取 user</button>
    </div>
  )
})

上面我们对 fetchUser 进行了单独封装,但是这时候我们会有两点疑惑
1). 我们为什么要把 disaptch 传给 fetchUser 哪?
2). 为什么是 fetchUser(dispatch) 而不是 dispatch(fetchUser) 哪?

实现 dispatch(fetchUser)

const newDispatch = (fn) => {
    fn(dispatch)
  }
newDispatch(fetchUser)

那我们干脆把 newDispatch 就叫 dispatch,因为我们的 dispatch 已经存在了,所以我们需要先把 dispatch 赋值给一个新的变量,然后再重新赋值一个 dispatch;

let prevDispatch = dispatch;
var dispatch = (fn) => {
    fn(dispatch)
  }
const onClick = () => {
    dispatch(fetchUser)
  }

而我们上面的代码不能直接在使用的时候写,而要在 redux 里写,所以我们需要 redux 支持异步 action,就是为了不写 fetchUser(dispatch) 写 dispatch(fetchUser)

改造我们的 redux

let dispatch = store.dispatch;
const prevDispatch = dispatch;
dispatch = (action) => {
  if (action instanceof Function) {
    action(dispatch)
  } else {
  // 如果不是函数就直接触发普通的 action
    prevDispatch(action)
  }
}

7.2. 支持 Promise action

使用

const onClick = () => {
    dispatch({type: 'updateUser', payload: ajax('/user').then(res => res.data)})
  }
  • redux
const prevDispatch2 = dispatch;
dispatch = (action) => {
  if (action.payload instanceof Promise) {
    action.payload.then(res => {
    // 这里因为有可能 res 还是一个 Promise,所以需要用 dispatch 进行递归
      dispatch({...action, payload: res})
    })
  } else {
  // 如果不是 Promise 就进行上面的函数判断
    prevDispatch2(action);
  }
}

7.3. 中间件

我们上面写的让 redux 支持函数和 promise,redux 本身并没有内置,但是可以在 createStore 里传入第三个参数

const store = createStore(reducer, initState, applyMiddleware(reduxThunk, reduxPromise))

然后它就会把代码运用到 dispatch 上,和我们上面写的一样;
问题:怎么通过中间件来让 redux 支持异步 action?
答:有两个比较出名的
1). redux-thunk
它发现 action 是一个函数就直接调用它,否则直接进入下一个中间件
2). redux-promise
它发现如果 action.payload 是一个Promise,就在 action.payload 后接上一个 then 拿到结果赋给 payload

github 地址:https://github.com/wanglifa/redux

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容