一次學(xué)習(xí),多端受用
react中抽象了一層虛擬dom,所以我們可以頻繁的修改狀態(tài),但是更改的都是虛擬dom。當虛擬dom發(fā)生變化后,會集中更新到真實的dom,因為虛擬dom的存在,只要替換掉底層的渲染引擎,就可以突破瀏覽器。pureComponent
class PureComponent extends Component {
shouldComponentUpdate (nextProps, nextState) {
const { state, props } = this
function shdowCompare(a, b) {
return a === b || Object.keys(a).every(k => a[key] === b[k])
}
return shdowCompare(state, nextState) && shdowCompare(props, nextProps)
}
}
react中使用createElement和JSX來實例化組件
componentWillReceiveProps 一般用來將新的props同步到state。
為了提升性能,react會把多次setState合并成一次。組件和事件
react事件是天生的事件代理,看起來散落在元素上,其實react僅僅在根元素綁定事件,所有事件通過事件代理響應(yīng)。
事件中的事件對象并不是原生事件對象,而是封裝過的,屏蔽了瀏覽器的差異。react也提供了訪問原生事件的方式。
class EventEmitter {
constructor () {
this.eventMap = {}
}
sub (name, cb) {
const eventList = this.eventMap[name] = this.eventMap[name] || []
eventList.push(cb)
}
pub(name, ...data) {}
}
- 高階組件
function HOC1(innerComponent) {
return class WrapComponent extends Components {
render () {
return (<innerComponent>{{ this.props.children }}</innerComponent>)
}
}
}
function HOC2(innerComponent) {
return class WrapComponent extends innerComponent{
}
}
function SetTimeoutHOC (InnerComponent) {
return class wrapComponent extends InnerComponent {
componentwill
}
}
通過ref調(diào)用react組件以及組件中的方法,同時也可以調(diào)用原生dom
react會對輸出的內(nèi)容進行xss過濾,但有時不需要,比如這個接口返回html片段的情況下,通過dangerousSetInnerHTML可以將HTML片段直接設(shè)置到DOM上。
- 初識store
let store = {
dispatch,
getState,
subscribe,
replaceReducer
}
dispatch: 派發(fā)action
subscribe(listener): 訂閱頁面數(shù)據(jù)狀態(tài),即store中state的變化
getState: 獲取當前頁面狀態(tài)樹
replaceReducer(nextReducer): 社區(qū)一些熱更新或者代碼分離技術(shù)可能會使用到。
- 編寫reducer函數(shù)更新數(shù)據(jù)
const updateReducer (preState, action) {
switch (action.type) {
case 'case1':
return newState1
default:
return preState
}
}
當無法匹配action時,默認返回preState
當頁面數(shù)據(jù)狀態(tài)更新之后,如何促使頁面發(fā)生UI更新?實際上是store.subscribe(cb)訂閱數(shù)據(jù)更新,并由cb完成UI更新。
合理拆分reducer函數(shù)
普通變量存在于棧內(nèi)存中,但是對象其實存在于堆內(nèi)存。當我們獲取對象時,首先獲取棧內(nèi)存的引用地址,然后根據(jù)引用地址從堆內(nèi)存中獲取所需要的值。deepClone
const deepClone = data => {
let t = type(data), o, i, length;
// 創(chuàng)建新的數(shù)組和對象
if (t === 'array) {
o = []
} else if (t === 'object') {
o = {}
} else { return data }
if (t == 'array') {
data.forEach(v => o.push(deepClone(v)))
return o
}
}
實際開發(fā)中,如果數(shù)據(jù)有多層。深拷貝對于開發(fā)性能并不友好。
Redux 中間件和異步
中間件就是在派發(fā)action和執(zhí)行reducer之間,添加自定義擴展功能。
中間件可以在action到達reducer之前進行日志記錄,中斷action觸發(fā),甚至修改action。
- redux-thunk中間件
假如有一個異步需求,比如需要派發(fā)一個網(wǎng)絡(luò)請求action,在網(wǎng)絡(luò)請求返回之后再派發(fā)一個action返回數(shù)據(jù)渲染頁面。
設(shè)想一下:如果dispatch可以接收一個函數(shù)作為參數(shù),在函數(shù)體內(nèi)進行異步操作,并在異步完成后再派發(fā)action。
store.dispatch(fetchNewBook('learnRedux'))
function fetchNewBook (book) {
return (dispatch) => {
dispatch({})
ajax({}).then(v => { dispatch({}) })
}
}
給dispatch函數(shù)傳一個異步函數(shù)fetchNewBook,這就是redux-thunk中間件對dispatch功能的增強,注意:中間件參數(shù)順序有講究。
react和redux的銜接點
root組件需要獲取頁面的狀態(tài)數(shù)據(jù),并向下進行派發(fā)。這樣,store.getState()的返回值就需要傳遞給root組件,作為props的存在。同時又需要在組件中調(diào)用dispatch方法。再通過store.subscribe()訂閱state的改變。使用react-redux庫
容器組件:指的是數(shù)據(jù)狀態(tài)和邏輯的容器。它并不負責(zé)展示,只維護內(nèi)部狀態(tài),進行數(shù)據(jù)分發(fā)和派發(fā)action。
展示組件:只負責(zé)接收數(shù)據(jù)并展示。
那么react-redux怎么生成容器組件?
Connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(component)
connect用來連接容器組件和展示組件。connect的核心是將開發(fā)者定義的組件包裝轉(zhuǎn)換成容器組件。所生成的容器組件能使用store中的哪些數(shù)據(jù),全由connect參數(shù)決定。
connect是典型的柯里化函數(shù),第一次設(shè)置參數(shù),第二次接收一個組件,并在該組件的基礎(chǔ)上返回一個容器組件。第一個參數(shù)是一個函數(shù),它的返回值將設(shè)置給組件的props,默認情況下dispatch會注入到組件的props上。
那么connect是如何獲取store中的內(nèi)容呢?
一般做法是將provider作為整個應(yīng)用的根組件,并獲取store作為它的prop。
深入理解redux
- redux源碼探索-store的實現(xiàn)
store = {
dispatch,
getState,
subscribe,
replaceReducer
}
const render = () => {
document.body.innerText = store.getState()
}
store.subscribe(render)
render()
接下來,我們思考如何實現(xiàn)store。
const createStore = (reducer) => {
let state
let listeners = []
const getState = () => state
const dispatch = (action) => {
state = reducer(state, action)
listeners.forEach(listener => listener())
}
const subscribe = (listener) => {
listeners.push(listener)
return () => {
listeners = listeners.filter (item => item !== listener)
}
}
return {
getState,
dispatch,
subscribe
}
}
subscribe返回一個函數(shù)用于取消訂閱,其實現(xiàn)方式是數(shù)組的filter方法。
createStore被調(diào)用后,Redux就會設(shè)置一個初始的空狀態(tài),我們只需要在createStore方法中加入如下一行觸發(fā)一個空action即可。
- combineReducers的實現(xiàn)
const combineReducers = (reducers) => {
return (state = {}, action) => {
return Object.keys(reducers).reduce((nextState, key) => {
// 進入不存在的action會返回原來的state
nextState[key] = reducers[key] (state[key], action)
return nextState
}, {})
}
}
combineReducers返回一個rootReducers, rootReducers返回經(jīng)過各個reducer處理后的全新數(shù)據(jù)狀態(tài),既更新后的state。
為了獲取所有reducer的計算結(jié)果,我們使用Object.keys對Object進行遍歷,這樣我們就可以根據(jù)數(shù)組的每一項進行state的計算,因為這個數(shù)組的每一項都是自定義的reducer函數(shù)。
- dispatch的改造 - 實現(xiàn)記錄日志
dispatch實質(zhì)就是一個函數(shù),它負責(zé)調(diào)用reducer方法,依靠reducer的執(zhí)行進行狀態(tài)變更,接著依次執(zhí)行各監(jiān)聽函數(shù),目的是實現(xiàn)視圖的更新。
這樣我們的入手點就在dispatch函數(shù)的入手前后。
創(chuàng)建一個addLogToDispatch函數(shù),用來生成取代原始的dispatch方法。這個函數(shù)對store中的dispatch進行攔截,并記錄原始的dispatch為rawDispatch,addLogToDispatch在行為應(yīng)予原始dispatch保持一致。
const addLogToDispatch = (store) => {
const rawDispatch = store.dispatch
return (action) => {
console.log('pre state:', store.getState())
const returnvalue = rawDispatch(action)
console.log('next state:', store.getState())
return returnvalue
}
}
- dispatch的改造 - 識別Promise
異步場景原理是令dispatch接收一個函數(shù),在這個函數(shù)中進行異步操作,既然dispatch可以接收一個函數(shù),那么也可以接收一個promise
思路是dispatch接收一個promise對象,在這個promise對象resolve后,我們使用原始的dispatch進行觸發(fā)。
const addLogToDispatch = (store) => {
const rawDispatch = store.dispatch
return (action) => {
if (typeof action.then == 'function') {
return action.then(rawDispatch)
}
rawDispatch(action)
}
}
這里使用一種比較投機的方式,檢查action參數(shù)是否有then方法,且這個方法是函數(shù)類型,既判斷action是否是一個thenable對象,如果是就會等待promise resolve,生成一個js對象,這個對象就是標準的action。代碼執(zhí)行時,每一個中間件獲得的dispatch都已經(jīng)被改造,聲明一個中間件·數(shù)組是一個更好的方式,redux的思想就是先將dispatch增強改造函數(shù)保存起來,然后提供給redux,每一個中間件都對dispatch進行改造,并將改造后的dispatch,既next向下傳遞。
中間件的執(zhí)行過程依賴中間件數(shù)據(jù)和dispatch
const wrapDispatchM = (store, middleware) => {
middleware.forEach(m =>store.dispatch = m(store)(store.dispatch))
}
const promise = (store) => (next) => (action) => {
if (typeof action.then === 'function') {
return action.then(next)
}
return next(action)
}
m數(shù)組的執(zhí)行順序與我們預(yù)期執(zhí)行的順序相反
const wrapDispatchM = (store, middleware) => {
middleware.slice().reverse().forEach(m =>store.dispatch = m(store)(store.dispatch))
}
中間件實際上就是action在到達reducer之前,增加的一個中間環(huán)節(jié)。
function applyMiddleware (...middlewares) {
return (next) =>
(reducer, initialState) => {
let store = next(reducer, initialState)
let dispatch = store.dispatch
let chain = []
let middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(m => m(middlewareAPI))
dispatch = compose(...chain, store.dispatch)
return { ...store, dispatch }
}
}
function compose (...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return func[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
A.B.C.store.dispatch => A(B(C(store.dispatch)))
A.B.C.D => A(B(C(D)))
[A.B.C.D].reduce((a. b) => a(b)) = A(B(C(D)))
let listeners = []
let state = ;
function dispatch (action) {
state = reducer(state, action)
listeners.slice().forEach(i => i())
return action
}
function subscribe (listener) {
listeners.push(listener)
return function (i) {
listeners = listeners.filter (i => i !== listener)
}
}
- react-redux 究竟是什么
provider組件
connect組件
- 通過context獲取provider的store,因此它具有了訪問store.state
揭秘react同構(gòu)應(yīng)用
一個完整的應(yīng)用除了純粹的靜態(tài)內(nèi)容,還包括各種響應(yīng)事件,用戶交互等。這就意味瀏覽器還要執(zhí)行js腳本,以完成綁定事件、處理異步交互等工作。瀏覽器進一步渲染的過程中,判斷已有的dom結(jié)構(gòu)和即將渲染的結(jié)構(gòu)是否相同,若相同,則不重新渲染dom結(jié)構(gòu),只進行事件綁定即可。
renderToString生成的HTMl字符串的每個DOM節(jié)點都有一個data-react-id屬性,根節(jié)點會有一個data-checksum. 如果兩個組件有相同的props和DOM結(jié)構(gòu),那么data-checksum屬性是一樣的。瀏覽器通過data-checksum判斷react組件是否只渲染一次。
使用ReactDOM.hydrate 會最大限度的保留服務(wù)器端渲染的結(jié)構(gòu)。
react16還提供了renderToNodeStream 方法實現(xiàn)服務(wù)器端渲染。該方法將持續(xù)產(chǎn)生字節(jié)流,最終通過流的形式返回HTML字符串,這樣有利于頁面初始加載和首屏?xí)r間。
- 因為在服務(wù)器端不支持組件掛載的瀏覽器環(huán)境,所以react組件只有componentDidMount之前的生命周期方法,所以我們最好將依賴瀏覽器的特性放到componentDidMount中處理。
因為服務(wù)器不存在ajax的概念,所以可以試用isomorphic-fetch實現(xiàn)一致性的封裝。
可以通過window是瀏覽器特有的對象這一特點,進行環(huán)境和邏輯區(qū)分。
瀏覽器端可以通過服務(wù)器端注入的全局變量得到初始狀態(tài)。
function throttle (fn, interval) {
let doing = false;
return () => {
if (doing) return;
fn()
setTimeout(() => { doing = false }, interval || 500)
}
}
- Function as Child Component
class ScrollPos extends Component {
state = { position: null }
componentDidMount = () => {
window.addEventListener ('scroll', this.handleScroll)
}
componentWillUnMount = () => {
window.removeEventListener ('scroll', this.handleScroll)
}
render() {
return (
<div>{this.props.children(this.state.position)}</div>
)
}
}
this.props.children會有多種類型,其中包括undefined, array,object
- setState
setState 存在延遲批處理情況,但有一些更新又是同步,setState除了更改this.state的值之外,還要負責(zé)觸發(fā)重新渲染邏輯,這里面要經(jīng)過核心的diff算法,最后才決定是否渲染,以及如何渲染。
深入源碼,setState中會根據(jù)isBatchingUpdates變量判斷是直接更新state,還是放到隊列中稍后更新。
總結(jié):在react控制的事件處理中,setState不會同步更新狀態(tài),而在react控制之外則會同步更新。在交互過程中,使用的都是在React庫中封裝的事件,例如select,input,button等,這種情況下setState就會以異步的方式執(zhí)行。
實際上繞過react,直接通過js原生直接添加事件處理函數(shù),就會出現(xiàn)同步更新的狀態(tài)。
在編寫react組件時,我們使用JSX來描述虛擬DOM,事實上,JSX總被編譯成createElement,一般babel為我們做了這件事情。
const helloWorld = React.createElement('div', null, 'hello')
ReactDOM.render(helloWorld, document.getElementById('root'))
我們需要使helloWorld創(chuàng)建的節(jié)點渲染出來并插入到root節(jié)點中
function anElement(ele, child) {
if (typeof ele === 'function') {
return ele()
} else {
const anele = document.createElement(ele)
anele,innerHTML = child.join('')
return anele
}
}
function createElement (el, props, ...child) {
return anElement(el, child)
}
window.React = {
createElement
}
window.ReactDOM = {
render: (el, root) => { root.appendChild(el) }
}
再深入考慮,children也不僅僅是簡單的文本節(jié)點,它還可能有其他子組件。
加入我們需要創(chuàng)建一個Component父類,在此類中進行props的初始化和賦值。
class Component {
constructor (props) {
this.props = props
}
}
- 優(yōu)化
- 對已經(jīng)實例化的class進行緩存
- 當處理一個已經(jīng)在緩存池中的class時,直接返回實例
- 使用標志位對class進行標識
- 對于新的渲染訴求,生成新的dom樹
- 計算兩顆dom樹的diff
- 將diff更新到真實的dom中
- 使用render方法對v-dom進行操作,而不是直接應(yīng)用在真實的dom中
- 收集diff,并計算出對真實的dom所做的最小更新
- 將最小更新應(yīng)用在真實的dom中
關(guān)于diff的細節(jié),將兩棵樹比較時間復(fù)雜度降為了o(n),具體表現(xiàn)為
- 前端頁面中,跨層級的操作特別少,可以忽略不計
- 擁有相同類型的兩個組件,擁有相似的樹形結(jié)構(gòu),擁有不同類型的兩個組件,擁有不同的樹形結(jié)構(gòu)
- 對于同一層級的一組節(jié)點,可以通過唯一id區(qū)分
基于第一點,react對樹的比較算法,實際上只對樹進行分層比較,兩棵樹只會對同一層的節(jié)點比較。這樣只需要遍歷一次樹。基于第二點,react在diff時,同一類型的組件,按照原策略進行比較,如果類型不同,則直接進行替換。基于第三點,列表節(jié)點組件通過開發(fā)者設(shè)置的唯一key,來協(xié)助實現(xiàn)添加、刪除和排序操作。
當然react允許開發(fā)者通過shuoldComponentUpdate方法直接決定組件是否需要diff
redux數(shù)據(jù)扁平化策略
體積過大的性能優(yōu)化,基于公共資源的分割,給公共的資源包添加緩存。
基于業(yè)務(wù)的代碼分割:
有了以上認知,我們進一步思考,是否可以將app也進行拆分
合理選擇分割維度
- 按照業(yè)務(wù)邏輯和依賴庫劃分
- 按照路由劃分
- 按照組件劃分
import Loadable from 'react-loadable'
const MyLoadingSpinner = () => {
}
- 按需加載實現(xiàn)原理
使用syntax-dynamic-import 這樣一個babel插件,通過動態(tài)導(dǎo)入實現(xiàn)按需加載,默認情況下,導(dǎo)入的模塊是靜態(tài)的,接下來我們看一看,react如何與動態(tài)導(dǎo)入相結(jié)合。
對按需加載的關(guān)注點在控制加載上,比如如何加載腳本,腳本是否加載。
class Async extends Component {
componentWillMount = () => {
this.cancelUpdate = false
this.props.load.then(c => {
this.C = c
if (!this.cancelUpdate) {
this.forceUpdate()
}
})
}
componentWillUnmount = () => { this.cancelUpdate = true }
render = () => {
const { componentProps } = this.props
return this.C ? this.C.default ? <this.C.default {...componentProps} />
: <this.C {...componentProps} /> : null
}
}
- 正確理解虛擬dom帶來的優(yōu)化
瀏覽器解析HTML之后,渲染引擎負責(zé)展現(xiàn)和渲染頁面樣式。還需要配合解析css,構(gòu)建渲染樹。
react通過以下幾種方式保證虛擬dom diff算法和更新的高效性能。
- 高效的diff算法
- Batch操作
當任何一個組件使用setState方法時,會觸發(fā)組件本身重新渲染。同時因其維護兩套虛擬dom,一套更新后的,一套更新前的。通過對這兩套虛擬運用diff,找到需要變化的最小單元集,然后把這個最小單元集運用在真實的DOM中。
當同層組件比較時,如果state或props發(fā)生變化,則直接重新渲染組件本身。同一層節(jié)點比較時,開發(fā)者可以使用key屬性來‘聲明’同一層級節(jié)點的更新方式。
找到最小單元集后,更新不一定同步進行。react會進行setState的Batch操作。
淺淺比較,復(fù)雜類型只判斷引用是否相同。
在使用purecomponent的時候,在更新props和state的時候,返回一個新的對象和數(shù)組。