手写Redux (2)
目录 1.Selector connect的第一个参数 2.mapDispatchToProps connect的第二个参数 3.connect的意义 4.封装Provider和createStore
Selector
Selector 是connect的第一个参数,由React-Redux库提供。
Selector的第1个功能:实现简写
目前我们的User组件只用到了state.user
const User=connect( ({state}) => {
return <div> User:{state.user.name} </div>
})
所以如果我们可以提供一个选择函数,比如说connect的第一个参数我们用()来表达,这样我们就可以在这里直接拿到user ,下面就不需要state也能直接拿到user了。
index.js
const User = connect( state => {
return {user:state.user}
})( ({user}) => {
return <div> User:{user.name} </div>
})
这种做法什么时候有意义? 如果我们拿数据的. 很长,比如{state.xxx.yyy.zzz.user.name} ,这种方法可以快速获取到局部state。 这只是API的实现,我们还没有对它进行代码的编写。
代码实现 步骤 1.添加参数selector 来到redux.js给connect函数添加一个参数(selector)=> 。 先接受一个参数selector 再接受第二个参数Component 2.使用参数 把state传进去,data就是用户需要的所有数据,再把data放到<Component /> 的props后面。
redux.js
export const connect = (selector) => (Component) => {
return (props) => {
const data=selector ? selector(state) : {state:state}
...
return <Component {...props} {...data} />
}
}
错误处理 如果用户没有传state怎么办? 没传data就是全局state,传了的话就是局部state。 所以要加个判断const data = selector ? selector(state) : {state:state}
3.最后把所有用到connect的地方都改下
index.js
const User = connect( (state) => {
return { user: state.user }
})( ({ user }) => {
return <div> User:{user.name} </div>
})
const UserModifier=connect()(({ dispatch, state })=> {...}
这就是改造user的过程。 selector除了能实现简写的功能,还有个非常重要的作用
Selector的第2个功能:实现精确渲染
组件只在自己的数据变化时render。 比如说一个组件用到了user,那么如果是其它数据group变化了,不应该渲染用到了user的组件
步骤 1.添加数据’前端组’放到第3个儿子里
redux.js
export const store = {
state: {
user: { name: 'frank', age: 18 },
group: { name: '前端组' }
},
}
index.js
const 小儿子 = connect(state => {
return { group: state.group }
})(({ group }) => {
console.log('小儿子执行了', +Math.random())
return <section>小儿子<div>Group:{group.name}</div></section>
})
点击二儿子修改user,发现小儿子的group竟然也触发了,怎么改? 在connect的wrapper里做检查:如果data没变就不要渲染 之前的代码是如果store变化了就直接update,现在添加条件判断。
2.添加条件判断 我先看下你的新数据是什么,然后再看data和newData是否变化了。 newData 得到的过程和data是一样的,只不过你是之前的,上一次的data。这一次是在你订阅发生变化之后我得到的新的statestore.state 然后再看下新旧数据有没有变化
怎么看有没有变化呢? 做个遍历,对于每个旧数据我来看下旧数据的user是否=== 新数据的user,旧数据的group=== 新数据的group,只要有一个变化了就算变化,那我就更新。
一般来说,你的useEffect里面用到了哪些来自于属性的东西,都得写在它的依赖里面,比如这个selector就得写在依赖里。
redux.js
const changed = (oldState, newState) => {
let changed = false
for (let key in oldState) {
if (oldState[key] !== newState[key]) {
changed = true
}
}
return changed
}
export const connect = (selector) => (Component) => {
return (props) => {
const { state, setState } = useContext(appContext);
const [, update] = useState({})
const data=selector ? selector(state) : {state:state}
useEffect(() => {
store.subscribe(() => {
const newDate = selector ? selector(store.state) :
{ state: store.state }
if (changed(data, newDate)) {
console.log('update')
update({})
}
})
}, [selector])
}
}
如果你不做取消订阅,可能在你意想不到的情况下,它会做不停的订阅。 比如说,我们再添加个state依赖[selector,state] ,就会导致下面这种情况:你一次输入n个字母,每次改变都会订阅,按的越多订阅的越多,积累了订阅,就会导致订阅了n次。这是因为只要state变化了,它就会重新订阅。
当然这只是示例,state是不需要写在这里的。但是我们还是要处理下selector,防止会出现重复订阅的情况,那怎么处理?
3.取消订阅:预防selector重复订阅 之前已经写好了取消订阅函数 拿到取消订阅函数,把取消订阅return就好了
export const store = {
...
subscribe(fn) {
store.listeners.push(fn)
return () => {
const index = store.listeners.indexOf(fn)
store.listeners.splice(index)
}
}
}
export const connect = (selector) => (Component) => {
return (props) => {
...
useEffect(() => {
const unsubscribe = store.subscribe(() => {
const newDate = selector ? selector(store.state) :
{ state: store.state }
if (changed(data, newDate)) {
console.log('update')
update({})
}
})
return unsubscribe
}, [selector])
...
}
}
mapDispatchToProps
connect(selector,mapDispatchToProps)
mapDispatchToProps 还是传一个函数,这个函数需要return一个对象。 这个函数接受一个dispatch,return的是你要更新的东西,比如说我要updateUser。 updateUser 就是直接调用dispatch,dispatch第一个参数是它的action,第二个参数是payload,payload接受一个对象,这个对象需要外部传进来。
const UserModifier = connect(null, (dispatch) => {
return {
updateUser: (attrs) => {
dispatch({ type: 'updateUser', payload: attrs })
}
}
})(({ updateUser, state }) => {
const onChange = (e) => {
updateUser({ name: e.target.value })
}
...
})
显的里面的组件很简洁:onChange的时候从props里拿到updateUser,然后把user的信息给传进去。 虽然上面变丑了,但是后续可以继续优化,这里先不讲。
接下来实现connect的第二个参数mapDispatchToProps 。 先把它传进来,它怎么用呢? 看下有没有传,如果传了mapDispatchToProps 就把dispatch 传给这个函数,否则就使用dispatch。
redux.js
export const connect = (selector, mapDispatchToProps) => (Component) => {
return (props) => {
const dispatch = (action) => { setState(reducer(state, action)) }
...
const dispatchers = mapDispatchToProps ?
mapDispatchToProps(dispatch) : { dispatch }
...
return <Component {...props} {...data} {...dispatchers} />
}
}
<Component /> 就不需要使用dispatch了,而是使用我自己创建的dispatchers 这样我们的dispatch也搞定了。
connect的意义
之前我们说过connect是为了让组件与全局的state就行结合,但实际上你可以发现它的函数的调用形式很奇怪。它为什么先要传一个MapStateToProps、MapDispatchToProps 然后再传一个组件呢,为什么不直接三个参数。实际上它是有考虑的。
connect(
MapStateToProps,
MapDispatchToProps)
(组件)
)
优化代码 User用到了user,userModifier也用到了user,实际上这两个应该用到的是同一个selector。 我们可以抽取公共selector。同样dispatch也是如此。
const userSelector = state => {
return { user: state.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, state }) => {
...
})
connect实际上是给了你一种提取读写接口的一种方式,这样你就不用重复的去告诉React怎么读写,但是我们还可以再进一步。connect可以调用两次的另外一个意义是:我们可以直接把这两个部分抽取出来。
const connectToUser = connect(userSelector, userDispatcher)
const User = connectToUser(({ user }) => {
return <div> User:{user.name} </div>
})
const UserModifier = connectToUser(({ updateUser, user }) => {
const onChange = (e) => {
updateUser({ name: e.target.value })
}
return <div>
<input value={user.name} onChange={onChange} />
</div>
})
所以任何的一个组件如果想要数据,直接给自己声明一个connect就可以拿到读和写。 那我们可以创建一个目录connecters,里面可以写各种connectToUser.js。
在src下新建目录connecters ,新建文件connectToUser.js
connectToUser.js
import { connect } from "../redux"
const userSelector = state => {
return { user: state.user }
}
const userDispatcher = (dispatch) => {
return {
updateUser: (attrs) => { dispatch({ type: 'updateUser', payload: attrs }) }
}
}
export const connectToUser = connect(userSelector, userDispatcher)
使用
index.js
import { connectToUser } from './connecters/connectToUser'
现在我们就知道了,MapStateToProps是用来封装读、MapDispatchToProps是用来封装写,connect是用来封装读和写,也就是封装一个资源,你可以对这个资源进行读写,然后你只要再传一个组件就行了。
之所以分成两次调用就是为了方便你先调用一次得到一个半成品,然后等你想用一个组件的时候就可以调用不同的组件。这个半成品可以跟任何组件相结合,它会把读写接口传给任何的组件,这就是connect的意义。
封装Provider和createStore
1.createStore的用法
创建store
createStore(reducer,initState)
打开redux.js 1.store里面的数据(state)不应该是写死的,redux不应该知道外面的数据是怎样的。 2.reducer也不应该是写死的,如果我们不知道数据,理论上我们应该不可能知道如何创建新的数据。 所以我们应该让state和reducer是从外部传进来的。
使用API createStore
redux.js
const store = {
state: undefined,
reducer: undefined,
setState(newState) {
console.log(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)
}
}
}
export const store = {
state: undefined,
reducer: undefined,
}
export const createStore = (reducer, initState) => {
store.state = initState
store.reducer = reducer
return store
}
index.js
import { appContext, createStore, connect } from "./redux"
const reducer = (state, { type, payload }) => {
if (type === 'updateUser') {
return {
...state,
user: {
...state.user,
...payload
}
}
} else {
return state
}
}
const initState = {
user: { name: 'frank', age: 18 },
group: { name: '前端组' }
}
const store = createStore(reducer, initState)
export const connect = (selector, mapDispatchToProps) => (Component) => {
return (props) => {
const dispatch = (action) => {
setState(store.reducer(state, action))
}
}
createStore是用来方便用户把初始化状态以及reducer传到我们的redux里。 现在我们就完成了对createStore的创建。
redux.js export了三个变量:createStore 用来创建store,connect 用于把组件和store连起来,appContext 是上下文,用于在任何地方读取store。
封装Provider
关于上下文,redux文档中是这样写的:
<Provider store={store}>
</Provider>
如何封装Provider 变成redux文档中的写法?
<appContext.Provider value={store}>
<大儿子 />
<二儿子 />
<小儿子 />
</appContext.Provider >
方法: 1.封装Provider ,Provider 应该是一个组件,因为它可以接受属性。
redux.js
export const Provider = ({ store, children }) => {
return (
<appContext.Provider value={store}>
{children}
</appContext.Provider >
)
}
2.使用上下文
index.js
import { Provider, createStore, connect } from "./redux"
export const App = () => {
return (
<Provider store={store}>
<大儿子 />
<二儿子 />
<小儿子 />
</Provider >
)
}
目前我们的redux和真正redux的接口几乎是一致的,通过手写redux,你基本上已经完全理解redux的思想了。
点击 我的博客 查看更多文章
|