接下來兩節某些地方可能會稍微有一點點抽象,可是我會盡量用簡單的方式進行講解。若是你以爲理解起來有點困難,能夠把這幾節多讀多理解幾遍,其實咱們一路走來都是符合「邏輯」的,都是發現問題、思考問題、優化代碼的過程。因此最好可以用心留意、思考咱們每個提出來的問題。html
細心的朋友能夠發現,其實咱們以前的例子當中是有比較嚴重的性能問題的。咱們在每一個渲染函數的開頭打一些 Log 看看:緩存
function renderApp (appState) { console.log('render app...') renderTitle(appState.title) renderContent(appState.content) } function renderTitle (title) { console.log('render title...') const titleDOM = document.getElementById('title') titleDOM.innerHTML = title.text titleDOM.style.color = title.color } function renderContent (content) { console.log('render content...') const contentDOM = document.getElementById('content') contentDOM.innerHTML = content.text contentDOM.style.color = content.color }
依舊執行一次初始化渲染,和兩次更新,這裏代碼保持不變:app
const store = createStore(appState, stateChanger) store.subscribe(() => renderApp(store.getState())) // 監聽數據變化 renderApp(store.getState()) // 首次渲染頁面 store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小書》' }) // 修改標題文本 store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改標題顏色
能夠在控制檯看到:函數
前三個毫無疑問是第一次渲染打印出來的。中間三個是第一次 store.dispatch
致使的,最後三個是第二次 store.dispatch
致使的。能夠看到問題就是,每當更新數據就從新渲染整個 App,但其實咱們兩次更新都沒有動到 appState
裏面的 content
字段的對象,而動的是 title
字段。其實並不須要從新 renderContent
,它是一個多餘的更新操做,如今咱們須要優化它。性能
這裏提出的解決方案是,在每一個渲染函數執行渲染操做以前先作個判斷,判斷傳入的新數據和舊的數據是否是相同,相同的話就不渲染了。優化
function renderApp (newAppState, oldAppState = {}) { // 防止 oldAppState 沒有傳入,因此加了默認參數 oldAppState = {} if (newAppState === oldAppState) return // 數據沒有變化就不渲染了 console.log('render app...') renderTitle(newAppState.title, oldAppState.title) renderContent(newAppState.content, oldAppState.content) } function renderTitle (newTitle, oldTitle = {}) { if (newTitle === oldTitle) return // 數據沒有變化就不渲染了 console.log('render title...') const titleDOM = document.getElementById('title') titleDOM.innerHTML = newTitle.text titleDOM.style.color = newTitle.color } function renderContent (newContent, oldContent = {}) { if (newContent === oldContent) return // 數據沒有變化就不渲染了 console.log('render content...') const contentDOM = document.getElementById('content') contentDOM.innerHTML = newContent.text contentDOM.style.color = newContent.color }
而後咱們用一個 oldState
變量保存舊的應用狀態,在須要從新渲染的時候把新舊數據傳進入去:spa
const store = createStore(appState, stateChanger) let oldState = store.getState() // 緩存舊的 state store.subscribe(() => { const newState = store.getState() // 數據可能變化,獲取新的 state renderApp(newState, oldState) // 把新舊的 state 傳進去渲染 oldState = newState // 渲染完之後,新的 newState 變成了舊的 oldState,等待下一次數據變化從新渲染 }) ...
但願到這裏沒有把你們忽悠到,上面的代碼根本不會達到咱們的效果。看看咱們的 stateChanger
:3d
function stateChanger (state, action) { switch (action.type) { case 'UPDATE_TITLE_TEXT': state.title.text = action.text break case 'UPDATE_TITLE_COLOR': state.title.color = action.color break default: break } }
即便你修改了 state.title.text
,可是 state
仍是原來那個 state
,state.title
仍是原來的 state.title
,這些引用指向的仍是原來的對象,只是對象內的內容發生了改變。因此即便你在每一個渲染函數開頭加了那個判斷又什麼用?這就像是下面的代碼那樣自欺欺人:code
let appState = { title: { text: 'React.js 小書', color: 'red', }, content: { text: 'React.js 小書內容', color: 'blue' } } const oldState = appState appState.title.text = '《React.js 小書》' oldState !== appState // false,其實兩個引用指向的是同一個對象,咱們卻但願它們不一樣。
可是,咱們接下來就要讓這種事情變成可能。htm
但願你們都知道這種 ES6 的語法:
const obj = { a: 1, b: 2} const obj2 = { ...obj } // => { a: 1, b: 2 }
const obj2 = { ...obj }
其實就是新建一個對象 obj2
,而後把 obj
全部的屬性都複製到 obj2
裏面,至關於對象的淺複製。上面的 obj
裏面的內容和 obj2
是徹底同樣的,可是倒是兩個不一樣的對象。除了淺複製對象,還能夠覆蓋、拓展對象屬性:
const obj = { a: 1, b: 2} const obj2 = { ...obj, b: 3, c: 4} // => { a: 1, b: 3, c: 4 },覆蓋了 b,新增了 c
咱們能夠把這種特性應用在 state
的更新上,咱們禁止直接修改原來的對象,一旦你要修改某些東西,你就得把修改路徑上的全部對象複製一遍,例如,咱們不寫下面的修改代碼:
appState.title.text = '《React.js 小書》'
取而代之的是,咱們新建一個 appState
,新建 appState.title
,新建 appState.title.text
:
let newAppState = { // 新建一個 newAppState ...appState, // 複製 appState 裏面的內容 title: { // 用一個新的對象覆蓋原來的 title 屬性 ...appState.title, // 複製原來 title 對象裏面的內容 text: '《React.js 小書》' // 覆蓋 text 屬性 } }
若是咱們用一個樹狀的結構來表示對象結構的話:
appState
和 newAppState
實際上是兩個不一樣的對象,由於對象淺複製的緣故,其實它們裏面的屬性 content
指向的是同一個對象;可是由於 title
被一個新的對象覆蓋了,因此它們的 title
屬性指向的對象是不一樣的。一樣地,修改 appState.title.color
:
let newAppState1 = { // 新建一個 newAppState1 ...newAppState, // 複製 newAppState1 裏面的內容 title: { // 用一個新的對象覆蓋原來的 title 屬性 ...newAppState.title, // 複製原來 title 對象裏面的內容 color: "blue" // 覆蓋 color 屬性 } }
咱們每次修改某些數據的時候,都不會碰原來的數據,而是把須要修改數據路徑上的對象都 copy 一個出來。這樣有什麼好處?看看咱們的目的達到了:
appState !== newAppState // true,兩個對象引用不一樣,數據變化了,從新渲染 appState.title !== newAppState.title // true,兩個對象引用不一樣,數據變化了,從新渲染 appState.content !== appState.content // false,兩個對象引用相同,數據沒有變化,不須要從新渲染
修改數據的時候就把修改路徑都複製一遍,可是保持其餘內容不變,最後的全部對象具備某些不變共享的結構(例如上面三個對象都共享 content
對象)。大多數狀況下咱們能夠保持 50% 以上的內容具備共享結構,這種操做具備很是優良的特性,咱們能夠用它來優化上面的渲染性能。
咱們修改 stateChanger
,讓它修改數據的時候,並不會直接修改原來的數據 state
,而是產生上述的共享結構的對象:
function stateChanger (state, action) { switch (action.type) { case 'UPDATE_TITLE_TEXT': return { // 構建新的對象而且返回 ...state, title: { ...state.title, text: action.text } } case 'UPDATE_TITLE_COLOR': return { // 構建新的對象而且返回 ...state, title: { ...state.title, color: action.color } } default: return state // 沒有修改,返回原來的對象 } }
代碼稍微比原來長了一點,可是是值得的。每次須要修改的時候都會產生新的對象,而且返回。而若是沒有修改(在 default
語句中)則返回原來的 state
對象。
由於 stateChanger
不會修改原來對象了,而是返回對象,因此咱們須要修改一下 createStore
。讓它用每次 stateChanger(state, action)
的調用結果覆蓋原來的 state
:
function createStore (state, stateChanger) { const listeners = [] const subscribe = (listener) => listeners.push(listener) const getState = () => state const dispatch = (action) => { state = stateChanger(state, action) // 覆蓋原對象 listeners.forEach((listener) => listener()) } return { getState, dispatch, subscribe } }
保持上面的渲染函數開頭的對象判斷不變,再看看控制檯:
前三個是首次渲染。後面的 store.dispatch
致使的從新渲染都沒有關於 content
的 Log 了。由於產生共享結構的對象,新舊對象的 content
引用指向的對象是同樣的,因此觸發了 renderContent
函數開頭的:
... if (newContent === oldContent) return ...
咱們成功地把沒必要要的頁面渲染優化掉了,問題解決。另外,並不須要擔憂每次修改都新建共享結構對象會有性能、內存問題,由於構建對象的成本很是低,並且咱們最多保存兩個對象引用(oldState
和 newState
),其他舊的對象都會被垃圾回收掉。
本節完整代碼:
function createStore (state, stateChanger) { const listeners = [] const subscribe = (listener) => listeners.push(listener) const getState = () => state const dispatch = (action) => { state = stateChanger(state, action) // 覆蓋原對象 listeners.forEach((listener) => listener()) } return { getState, dispatch, subscribe } } function renderApp (newAppState, oldAppState = {}) { // 防止 oldAppState 沒有傳入,因此加了默認參數 oldAppState = {} if (newAppState === oldAppState) return // 數據沒有變化就不渲染了 console.log('render app...') renderTitle(newAppState.title, oldAppState.title) renderContent(newAppState.content, oldAppState.content) } function renderTitle (newTitle, oldTitle = {}) { if (newTitle === oldTitle) return // 數據沒有變化就不渲染了 console.log('render title...') const titleDOM = document.getElementById('title') titleDOM.innerHTML = newTitle.text titleDOM.style.color = newTitle.color } function renderContent (newContent, oldContent = {}) { if (newContent === oldContent) return // 數據沒有變化就不渲染了 console.log('render content...') const contentDOM = document.getElementById('content') contentDOM.innerHTML = newContent.text contentDOM.style.color = newContent.color } let appState = { title: { text: 'React.js 小書', color: 'red', }, content: { text: 'React.js 小書內容', color: 'blue' } } function stateChanger (state, action) { switch (action.type) { case 'UPDATE_TITLE_TEXT': return { // 構建新的對象而且返回 ...state, title: { ...state.title, text: action.text } } case 'UPDATE_TITLE_COLOR': return { // 構建新的對象而且返回 ...state, title: { ...state.title, color: action.color } } default: return state // 沒有修改,返回原來的對象 } } const store = createStore(appState, stateChanger) let oldState = store.getState() // 緩存舊的 state store.subscribe(() => { const newState = store.getState() // 數據可能變化,獲取新的 state renderApp(newState, oldState) // 把新舊的 state 傳進去渲染 oldState = newState // 渲染完之後,新的 newState 變成了舊的 oldState,等待下一次數據變化從新渲染 }) renderApp(store.getState()) // 首次渲染頁面 store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小書》' }) // 修改標題文本 store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改標題顏色