react-copy-write 一個比較新的 React 狀態管理庫,經過 immer 實現了可變狀態接口和部分 reselect 記憶功能。可經過下圖快速瞭解該庫的總體架構:html
新的 Context 給出多個 API 解決了深層傳遞數據的笨重。 其中 Provider
提供的數據在整個組件樹中可認爲是 「全局」 的,而 Consumer
則能夠在任一層自由的訂閱該 「全局」 數據,全部的 Consumer
會隨着 Provider
的數據改變而從新渲染。可是有時會出現沒必要要的從新渲染,其主要緣由是判斷 「全局」 數據是否改變的依據是 Object.is
。react
class App extends React.Component {
render() {
return (
<Provider value={{ something: 'something' }}> <Toolbar /> </Provider>
)
}
}
複製代碼
每次 App 組件渲染 Provider
中 value
都是新的對象。解決該類問題方法就是將 value
的數據提高。git
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
仍然會從新渲染,。github
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 都提供了共享結構的方案。typescript
共享結構或結構化共享或結構共享化(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 源碼僅有不到 200 行(註釋佔了一半),該庫對外提供了一個 API,接口返回一個對象ide
return {
Provider: CopyOnWriteStoreProvider,
Consumer: CopyOnWriteConsumer,
Mutator: CopyOnWriteMutator,
update,
createMutator
}
複製代碼
Provider
做用與 Context.Provider
徹底一致。post
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
含有一個 selector
屬性,用來選取關注的數據this
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
改變了,組件則沒有正常從新渲染。解決該問題的一個選擇即是使用數組方式。
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
Provider
徹底能夠做爲根組件存在。Provider
繼續包裝成 Observable
,聲明式更加方便(對了, createMutator
是 update
的聲明式語法糖),也能夠將 DOM 敏感和數據敏感結合。combineReducers
的方法,將 Provider
組合。