手写Redux (2)

手写Redux (2)

目录
1.Selectorconnect的第一个参数
2.mapDispatchToPropsconnect的第二个参数
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放到的props后面。

redux.js
export const connect = (selector) => (Component) => {
 return (props) => {
 //const data = selector(state)
  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 }
        //然后看data和newData是否变化了
          if (changed(data, newDate)) {
            console.log('update')
            update({})
          }
        })
    //注意这里最好 取消订阅,否则在selector变化时会出现重复订阅
      }, [selector]) //添加依赖
    }
  }

如果你不做取消订阅,可能在你意想不到的情况下,它会做不停的订阅。
比如说,我们再添加个state依赖[selector,state],就会导致下面这种情况:你一次输入n个字母,每次改变都会订阅,按的越多订阅的越多,积累了订阅,就会导致订阅了n次。这是因为只要state变化了,它就会重新订阅。
手写Redux (2)_第1张图片

当然这只是示例,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)
//connect的第二个参数`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} />
  }
}

就不需要使用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 //return
}
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)) //用到reducer的改为store.reducer
    }
}

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 }) => { //封装Provider
  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的思想了。

点击 我的博客 查看更多文章

你可能感兴趣的:(手写Redux)