react-copy-write 一个比较新的 React 状态管理库,通过 immer 实现了可变状态接口和部分 reselect 记忆功能。可通过下图快速了解该库的整体架构:
Context
新的 Context 给出多个 API 解决了深层传递数据的笨重。 其中 Provider 提供的数据在整个组件树中可认为是 “全局” 的,而 Consumer 则可以在任一层自由的订阅该 “全局” 数据,所有的 Consumer 会随着 Provider 的数据改变而重新渲染。但是有时会出现不必要的重新渲染,其主要原因是判断 “全局” 数据是否改变的依据是 Object.is 。
class App extends React.Component {
render() {
return (
<Provider value={{ something: 'something' }}>
<Toolbar />
</Provider>
)
}
}
每次 App 组件渲染 Provider 中 value 都是新的对象。解决该类问题方法就是将 value 的数据提升。
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
value: { something: 'something' }
}
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
)
}
}
还有一种情况也会出现多余的渲染,当 Consumer 只订阅了部分 Provider 提供的数据,因为 setState 的原因,每次都会得到新的数据,此时即使数据的值没有改变, Consumer 仍然会重新渲染,。
const themes = {
light: {
foreground: '#ffffff',
background: '#222222'
},
dark: {
foreground: '#000000',
background: '#eeeeee'
}
}
const ThemeContext = React.createContext({
theme: themes.dark,
another: 'aaa'
})
function ThemeButton(props) {
return <button onClick={props.changeTheme}>Change Theme</button>
}
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
theme: themes.light,
another: 'bbb'
}
this.toggleTheme = () => {
this.setState(state => ({
theme: state.theme === themes.dark ? themes.light : themes.dark
}))
}
}
render() {
return (
<ThemeContext.Provider value={this.state.another}>
<ThemeButton changeTheme={this.toggleTheme} />
<ThemeContext.Consumer>
{state => {
console.log('re-render')
return <button>{state}</button>
}}
</ThemeContext.Consumer>
</ThemeContext.Provider>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
每次点击 ThemeButton 时,this.state.another 数值并没有改变,但是控制栏都会显示 're-render' 。解决这种问题的方法是使用 shouldComponentUpdate 方法,但针对深层数据 共享结构 才是最佳选择,immutable.js 和 immer 都提供了共享结构的方案。
immer
共享结构或结构化共享或结构共享化(structural sharing)保证在更新数据时重用未改变的数据引用,immutable.js 和 immer 库都拥有这种特性。
import produce from 'immer'
const baseState = [
{
todo: 'Learn typescript',
done: true
},
{
todo: 'Try immer',
done: false
}
]
const nextState = produce(baseState, draftState => {
draftState.push({ todo: 'Tweet about it' })
draftState[1].done = true
})
expect(baseState.length).toBe(2)
expect(nextState.length).toBe(3)
expect(baseState[1].done).toBe(false)
expect(nextState[1].done).toBe(true)
expect(nextState[0]).toBe(baseState[0]) // 未改变部分
expect(nextState[1]).not.toBe(baseState[1])
将上述两部分链接在一起,就是 react-copy-write 的目的。
react-copy-write
查看 react-copy-write 源码仅有不到 200 行(注释占了一半),该库对外提供了一个 API,接口返回一个对象
return {
Provider: CopyOnWriteStoreProvider,
Consumer: CopyOnWriteConsumer,
Mutator: CopyOnWriteMutator,
update,
createMutator
}
Provider
Provider 作用与 Context.Provider 完全一致。
import createState from 'react-copy-write'
const State = createState({
user: null,
loggedIn: false
})
class App extends React.Component {
render() {
return (
<State.Provider>
<AppBody />
</State.Provider>
)
}
}
Consumer
Consumer 含有一个 selector 属性,用来选取关注的数据
const UserAvatar = ({ id }) => (
<State.Consumer selector={state => state.users[id].avatar}>
{avatar => (
<div>
<img src={avatar.src} />
</div>
)}
</State.Consumer>
)
selector 属性可以是多个 selectors 组合,甚至是数组
// 多个 selectors 组合
const UserPosts = () => (
<State.Consumer selector={state => ({ posts: state.posts, userId: state.user.id }}>
{({posts, userId}) => {
const filteredPosts = posts.filter(post => post.id === userId)
return posts.map(post => <Post id={post.id} />)
}
</State.Consumer>
)
// selector 数组
const UserPosts = () => (
<State.Consumer selector={[state => state.posts, state => state.userId]}>
{[posts, userId] =>
const filteredPosts = posts.filter(post => post.id === userId)
return posts.map(post => <Post id={post.id} />)
)}
</State.Consumer>
)
在使用多个 selectors 时, selector 会依次执行,不过上述写法会带来不必要的渲染,因为每次都会生成新的对象。可以修改为
const UserPosts = () => (
<State.Consumer selector={state => state.posts}>
{posts => (
<State.Consumer selector={state => state.user.id}>
{userId => {
const filteredPosts = posts.filter(post => post.id === userId)
return posts.map(post => <Post id={post.id} />)
}}
</State.Consumer>
)}
</State.Consumer>
)
可这种做法又过度防护重新渲染,因为如果 state.posts 没有改变,而 state.user.id 改变了,组件则没有正常重新渲染。解决该问题的一个选择便是使用数组方式。
update
Consumer 和 Context.Consumer 的区别在 children 属性, Consumer 的 children 会通过一个中间组件,该中间组件不仅进行了 shouldComponentUpdate 判断,还将 update 赋予了 children 。
update 首先使用了 immer 的 produce 生成新的 state ,然后将其发送给 Provider。
const Post = ({ id }) => (
<div className="post">
<State.Consumer selector={state => state.posts[id]}>
// mutate 就是 update
{(post, mutate) => (
<>
<h1>{post.title}</h1>
<img src={post.image} />
<p>{post.body}</p>
<button
onClick={() =>
// Here's the magic:
mutate(draft => {
draft.posts[id].praiseCount += 1
})
}
>
Praise
</button>
</>
)}
</State.Consumer>
</div>
)
回顾最初的图,至此状态管理形成了闭环:
update通过 immer 生成共享结构的 state ,Provider更新 state ,Consumer通过 selector 订阅部分 state, 并将update传递给children组件,children组件通过交互调用update
个人拙见
- 在 state 开销能够承受的情况下,
Provider完全可以作为根组件存在。 - 能否将
Provider继续包装成Observable,声明式更加方便(对了,createMutator是update的声明式语法糖),也可以将 DOM 敏感和数据敏感结合。 - 为了模块清晰调试简单或实现时间旅行,是否可提供类似
combineReducers的方法,将Provider组合。