3C科技 娛樂遊戲 美食旅遊 時尚美妝 親子育兒 生活休閒 金融理財 健康運動 寰宇綜合

Zi 字媒體

2017-07-25T20:27:27+00:00
加入好友
作者|徐飛4月18日,在QCon北京2017大會現場,來自螞蟻金服高級前端開發專家徐飛分享了主題為《單頁應用的數據流方案探索》的演講。獲取QCon北京2017相關PPT,請關注前端之巔公眾號併發送「QCon2017」。大家好,現在是2017年4月。過去的3年裡,前端開發領域可謂風起雲湧,革故鼎新。除了開發語言的語法增強和工具體系的提升之外,大部分人開始習慣幾件事:組件化MDV(Model Driven View)所謂組件化,很容易理解,把視圖按照功能,切分為若干基本單元,所得的東西就可以稱為組件,而組件又可以一級一級組合而成複合組件,從而在整個應用的規模上,形成一棵倒置的組件樹。這種方法論歷史久遠,其實現方式或有瑜亮,理念則大同小異。而MDV,則是對很多低級DOM操作的簡化,把對DOM的手動修改屏蔽了,通過從數據到視圖的一個映射關係,達到了只要操作數據,就能改變視圖的效果。Model-Driven-View給定一個數據模型,可以得到對應的的視圖,這一過程可以表達為:其中的f就是從Model到View的映射關係,在不同的框架中,實現方式有差異,整體理念則是類似的。當數據模型產生變化的時候,其對應的視圖也會隨之變化:V + ΔV = f(M + ΔM)另外一個方面,如果從變更的角度去解讀Model,數據模型不是無緣無故變化的,它是由某個操作引起的,我們也可以得出另外一個表達式:ΔM = perform(action)把每次的變更綜合起來,可以得到對整個應用狀態的表達:state := actions.reduce(reducer, initState)這個表達式的含義是:在初始狀態上,依次疊加後續的變更,所得的就是當前狀態。這就是當前最流行的數據流方案Redux的核心理念。從整體來說,使用Redux,相當於把整個應用都實現為命令模式,一切變動都由命令驅動。Reactive Programming 庫簡介在傳統的編程實踐中,我們可以:復用一種數據復用一個函數復用一組數據和函數的集合但是,很難做到:提供一種會持續變化的數據讓其他模塊復用。而一些基於Reactive Programming的庫可以提供一種能力,把數據包裝成可持續變更、可觀測的類型,供後續使用,這種庫包括:RxJS,xstream,most.js等等。對數據的包裝過程類似如下:const a$ = xs.of(1) const arr$ = xs.from([1, 2, 3]) const interval$ = xs.periodic(1000)這段代碼中的a$、arr$、interval$都是一種可觀測的數據包裝,如果對它們進行訂閱,就可以收到所有產生的變更。interval$.subscribe(console.log)我們可以把這種封裝結構視為數據管道,在這種管道上,可以添加統一的處理規則,這種規則會作用在管道中的每個數據上,並且形成新的管道:const interval$ = xs.periodic(1000) const result$ = interval$ .filter(num => num % 3) .map(num => num * 2)管道可被連續拼接,並形成新的管道。需要注意的是:管道是懶執行的。一個拼接起來的數據管道,只有最末端被訂閱的時候,附加在管道上的所有邏輯才會被執行。一般情況下,管道的執行過程可以被共享,比如b$和c$兩個管道,都從a$變形得出,它們就共享了a$之前的所有執行過程。也可以把多個管道組合在一起形成新的管道:const priv$ = xs.combine(user$, article$) .map(arr => { const [user, article] = arr return user.isAdmin || article.creator === user.id })從這個關係中可以看出,當user$或task$中的數據發生變更的時候,priv$都會自動計算出最新結果。在業務開發的過程中,可以使用數據流的理念,把很多東西提高一個抽象等級:const data$ = xs.fromPromise(service(params)) .map(data => ({ loading: false, data })) .replaceError(error => xs.of({ loading: false, error })) .startWith({ loading: true, error: null, })比如上面這個例子,統一處理了一個普通請求過程中的三種狀態:請求前、成功、異常,並且把它們的數據:loading、正常數據、異常數據都統一成一種,視圖直接訂閱處理就行了。高度抽象的數據來源很多時候,我們進行業務開發,都是在一種比較低層次的抽象維度上,在低層抽象上,存在著太多的冗餘過程。如果能夠對數據的來源和去向做一些歸納會怎樣呢?比如說,從實體的角度,很可能一份數據初始狀態有多個來源:應用的默認配置HTTP請求本地存儲...等等也很可能有多個事件都是在修改同一個東西:用戶從視圖發起的操作來自WebSocket的推送消息來自Worker的處理消息來自其它窗體的postMessage調用等等……如果不做歸納,可能會寫出包含以上各種東西的邏輯組合。若干個類似的操作,在過濾掉額外信息之後,可能都是一樣的。從應用狀態的角度,我們不會需要關心一個數據究竟是從哪裡來的,也不會需要關心是通過什麼東西發起的修改。用傳統的Redux寫法,可能會提取出一些公共方法:const changeTodo = todo => { dispatch({type: 'updateTodo', payload: todo}) } const changefromDOMEvent = => { const todo = formState changeTodo(todo) } const changefromWebSocket = => { const todo = fromWS changeTodo(todo) }基於方法調用的邏輯不能很好地展示一份數據的生命周期,它可能有哪些來源?可能被什麼修改?它是經過幾千年怎樣的辛苦修鍊之後才能夠化成人形,跟你坐在一張桌子上喝咖啡?我們可以藉助RxJS或者xstream這樣的庫,以數據管道的理念,把這些東西更加直觀地組織在一起:初始狀態來源const fromInitState$ = xs.of(todo) const fromLocalStorage$ = xs.of(getTodoFromLS) // initState const init$ = xs .merge( fromInitState$, fromLocalStorage$ ) .filter(todo => !todo) .startWith({})數據變更過程的統一const changeFromHTTP$ = xs.fromPromise(getTodo) .map(result => result.data) const changeFromDOMEvent$ = xs .fromEvent($('.btn', 'click')) .map(evt => evt.data) const changeFromWebSocket$ = xs .fromEvent(ws, 'message') .map(evt => evt.data) // 合併所有變更來源 const changes$ = xs .merge( changeFromHTTP$, changeFromDOMEvent$, changeFromWebSocket$ )在這樣的機制里,我們可以很清楚地看到一塊數據的來龍去脈,它最初是哪裡來的,後來可能會被誰修改過。所有這樣的數據都放置在管道中,除了指定的入口,不會有其他東西能夠修改這些數據,視圖可以很安全地訂閱他們。基於Reactive理念的這些數據流庫,一般是沒有針對業務開發的強約束的,也以直接訂閱並設置組件狀態,也可以拿它按照Redux的理念來使用,豐儉由人。簡單的使用changes$.subscribe(({ payload }) => { xxx.setState({ todo: payload }) })類似Redux的使用方式const updateActions$ = changes$ .map(todo => ({type: 'updateTodo', payload: todo})) const todo$ = changeActions$ .fold((state, action) => { const { payload } = action return {...state, ...payload} }, initState)組件與外置狀態我們前面提到,組件樹是一個樹形結構。理想中的組件化,是所有視圖狀態全部內置在組件中,一級一級傳遞。只有這樣,才能達到組件的最佳可復用狀態,並且,組件可以放心把自己該做的事情都做了。但事實上,組件樹的層級可能很多,這會導致傳遞層級很多,很繁瑣,而且,存在一個經典問題,那就是兄弟組件,或者是位於組件樹的不同樹枝上的組件之間的通信很麻煩,必須通過共同的最近的祖先節點去轉發。像Redux這樣的機制,把狀態的持有和更新外置,然後通過connect這樣的方法,去把特定組件所需的外部狀態從props設置進去,但它不僅僅是一個轉發器。我們可以看到如下事實:轉發器在組件樹之外部分數據在組件樹之外對這部分數據的修改過程在組件樹之外修改完數據之後,通知組件樹更新所以:組件可以通過中轉器修改其他組件的狀態組件可以通過中轉器修改自身的狀態組件可以通過中轉器修改全局的其他狀態這樣看來,可以通過中轉器修改應用中的一切狀態。那麼,如果所有狀態都可以通過中轉器修改,是否意味著都應當通過它修改?這個問題很大程度上等價於:組件是否應當擁有自己的內部狀態?我們可能會有如下的選擇:一切狀態外置,組件不管理自己狀態部分內置,由組件自己管理,另外一些由全局Store管理這兩種方式,在傳統軟體開發領域分別稱為貧血組件、充血組件,它們的差別是:組件究竟是純展示,還是帶一些邏輯。也可以拿蟻群和人群來形容這兩種組件實踐。單個螞蟻的智能程度很低,但它可以接受蟻王的指令去做某些事情,所有的麻煩事情都集中在上層,決策層的事務非常繁瑣。而人類則不同,每個人都有自己的思考和執行能力,一個管理有序的體系中,管理者只需決定他和自己直接下屬所需要做的事情就可以了。在React體系中,純展示組件可被簡化為這樣的形式:const ComponentA = (props) => { return ({props.data}) }顯而易見,這種組件的優勢在於它的展示結果只跟輸入數據有關,所有狀態外置,因此,在熱替換等方面,可以做到極致。然而,一旦這個組件複雜起來,自帶交互,可能就需要在事件、生命周期上做文章,免不了會需要一些中間狀態來表達組件自身的形態。我們當然可以把這種狀態也外置,但這麼做有幾個問題:這樣的狀態只跟某組件自己有關,放出去到全局Store,會增加Store的不必要的複雜度組件的自身形態狀態被外置,將導致組件與狀態的距離變遠,從而對這些狀態的讀寫變得比原先繁瑣帶交互的組件,無法獨立、完整地描述自身的行為,必須藉助外部管理器如果是一種單獨提供的組件庫,比如像Ant Design這樣的,卻要依賴一個外部的狀態管理器,這是很不合適的,它會導致組件庫帶有傾向性,從而對使用者造成困擾。總的來說,狀態全外置,組件退化為貧血組件這種實踐,可以得到不少好處,但代價是比較大的。在You might not need Redux__這篇文章中,Redux的作者Dan Abramov提到:因此,我們就可能會面臨一個尷尬的狀況,在大部分實踐中:一個組件的狀態,可能一半在組件內管理,一半在全局的Store里以React為例,大致是這樣一個狀況:constructor(props) { super(props) this.state = { b: 1 } } render(props) { const a = this.state.b + props.c; return ({a}我們看到,在render裡面,需要合併state和props的數據,但是在這裡做這個事情,是破壞了render函數的純潔性的。可是,除了這裡,別的地方也不太適合做這種合併,怎麼辦呢?所以,我們需要一種機制,能夠把本地狀態和props在render之外統一起來,這可能就是很多實踐者傾向於把本地狀態也外置的最重要原因。在React + Redux的實踐中,通常會使用connect對視圖組件包裝一層,變成一種叫做容器組件的東西,這個connect所做的事情就是把全局狀態映射到組件的props中。那麼,考慮如下代碼:const mapStateToProps = (state: { a }) => { return { a } } // const localState = { b: 1 } // const mapLocalStateToProps = localState => localState const ComponentA = (props) => { const { a, b } = props const c = a + b return ({ c }) } return connect(mapStateToProps/*, mapLocalStateToProps*/)(ComponentA)我們是否可以把一個組件的內部狀態外置到被註釋掉的這個位置,然後也connect進來呢?這段代碼其實是不起作用的,因為對localState的改變不會被檢測到,所以組件不會刷新。我們先探索這種模式是否可行,然後再來考慮實現的問題。MVI架構在Plug and Play All Your Observable Streams With Cycle.js__這篇文章中,我們可以看到一組理念:一切都是事件源使用Reactive的理念構建程序的骨架使用sink來定義應用的邏輯使用driver來隔離有副作用的行為(網路請求、DOM渲染)基於這套理念,編寫代碼的方式可以變得很簡潔流暢:從driver中獲取action把action映射成數據流處理數據流,並且渲染成界面從界面的事件中,派發action去進行後續事項的處理在CycleJS的理念中,這種模式叫做MVI(Model View Intent)。在這套理念中,我們的應用可以分為三個部分:Intent,負責從外部的輸入中,提取出所需信息Model,負責從Intent生成視圖展示所需的數據View,負責根據視圖數據渲染視圖整體結構可以這樣描述:App := View(Model(Intent({ DOM, Http, WebSocket })))對比Redux這樣的機制,它的差異在於:Intent實際上做的是action執行過程的高級抽象,提取了必要的信息Model做的是reducer的事情,把action的信息轉換之後合併為狀態對象View跟其他框架沒什麼區別,從狀態對象渲染成視圖。此外,在CycleJS中,View是純展示,連事件監聽也不做,這部分監聽的工作放在Intent中去做。const model = (a$, b$) => { return xs.combine(a$, b$) } const view = (state$) => { return state$.map(({ a, b }) => { const c = a + b; return h2('c is ' + c) }) }我們可以從中發掘這麼一些東西:View還是純渲染,接受的唯一參數就是一個表達視圖狀態的數據流Model的返回結果就是上面那個流,不分內外狀態,全部合併起來Model所合併的東西的來源,是從Intent中來的對我們來說,這裡面最大關鍵在於:所有東西的輸入輸出都是數據流,甚至連視圖接受的參數、還有它的渲染結果也是一個流!奧秘就在這裡。因此,我們只需在把待傳入視圖的props與視圖的state以流的方式合併,直接把合併之後的流的結果傳入視圖組件,就能達到我們在上一節中提出的需求。組件化與分形我們之前提到過一點,在一個應用中,組件是形成倒置的樹形結構的。當組件樹上的某一塊越來越複雜,我們就把它再拆開,延伸出新的樹枝和葉子,這個過程,與分形__有異曲同工之妙。然而,因為全局狀態和本地狀態的分離,導致每一次分形,我們都要兼顧本組件、下級組件、全局狀態、本地狀態,在它們之間作一些權衡,這是一個很麻煩的過程。在React的主流實踐中,一般可以利用connect這樣的高階函數,把全局狀態映射進組件的props,轉化為本地狀態。上一節提及的MVI結構,不僅僅能夠描述一個應用的執行過程,還可以單獨描述一個組件的執行過程。Component := View(Model(Intent({ DOM, Http, WebSocket })))所以,從整體來理解我們的應用,就是這樣一個關係:這樣一直分形下去,每一級組件都可以擁有自己的View、Model、Intent。狀態的變更過程在模型驅動視圖這個理念下,視圖始終會是調用鏈的最後一段,它的職責就是消費已經計算好的數據,渲染出來。所以,從這個角度看,我們的重點工作在於怎麼管理狀態,包括結構的定義和變更的流轉過程。Redux提供了對狀態定義和變更過程的管理思路,但有不少值得探討的地方。基於標準Flux/Redux的實踐有一個共同點:繁瑣。產生這種繁瑣的最主要原因是,它們都是以自定義事件為核心的,自定義事件本身就是繁瑣的。由於收發事件通常位於兩個以上不相同的模塊中,不得不以封裝的事件對象為通信載體,並且必須顯式定義事件的key,否則接收方無法指定自己的響應。一旦整個應用都是以此為基石,其中的繁瑣程度可想而知,所以社區會存在一些簡化action創建,或者通過約定來減少action收發中間環節的Redux周邊。如果不從根本上對事件這種機制進行抽象,就不可能徹底解決繁瑣的問題,基於Reactive理念的這幾個庫天然就是為了處理對事件機制的抽象而出現的,所以用在這種場景下有奇效,能把action的派發與處理過程描述得優雅精妙。const updateActions$ = changes$ .map(todo => ({type: 'updateTodo', payload: todo})) const todo$ = updateActions$ .fold((state, action) => { const { payload } = action return {...state, ...payload} }, initState)注意一個問題,既然我們之前得到一種思路,把全局狀態和本地狀態分開,然後合併注入組件,就需要考慮這樣的問題:如何管理本地狀態和全局狀態,使用相同的方式去管理嗎?在Redux體系中,我們在修改全局狀態的時候,使用指定的action去修改狀態,原因是要區分那個哪個action修改state的什麼部分,怎樣修改。但是考慮本地狀態的情況,它反映的只是組件內部的數據變化,一般而言,其結構複雜程度遠遠低於全局狀態,繼續採用這種方式的話並不划算。Redux這類東西出現的初衷只是為了提供一種單向數據流的思路,防止狀態修改的混亂。但是在基於數據管道的這些庫中,數據天然就是單向流動的。在剛才那段代碼里,其實action的type是沒有意義的,一直就沒有用到。實際上,這個代碼中的updateActions$自身就表達了updateTodo的含義,而它後續的fold操作,實際上就是直接在reduce。理解了這一點之後,我們就可以寫出反映若干種數據變更的合集了,這個時候,可以根據不同的action去選擇不同的reducer操作:// 我們可以先把這些action全部merge之後再fold,跟Redux的理念類似 const actions = xs.merge( addActions$, updateActions$, deleteActions$ ) const localState$ = actions.fold((state, action) => { switch(action.type) { case 'addTodo': return addTodo(state, action) case 'updateTodo': return updateTodo(state, action) case 'deleteTodo': return deleteTodo(state, action) } }, initState)我們注意到,這裡是把所有action全部merge了之後再fold的,這是符合Redux方式的做法。有沒有可能各自fold之後再merge呢?其實是有可能的,我們只要能夠確保action導致的reducer粒度足夠小,比如只修改state的同一個部分,是可以按照這種維度去組織action的。const a$ = actionsA$.fold(reducerA, initA) const b$ = actionsB$.fold(reducerB, initB) const c$ = actionsC$.fold(reducerC, initC) const state$ = xs.combine(a$, b$, c$) .map(([a, b, c]) => ({a, b, c}))如果我們一個組件的內部狀態足夠簡單,甚至連action的類型都可以不需要,直接從操作映射到狀態結果。const state$ = xs.fromEvent($('.btn'), click) .map(e => e.data)這樣,我們可以在組件內運行這種簡化版的Redux機制,而在全局狀態上運行比較完善的。這兩種都是基於數據管道的,然後在容器組件中可以把它們合併,傳入視圖組件。整個流程如圖所示:狀態的分組與管理基於redux-saga的封裝庫dva提供了一種分類機制,可以把一類業務的東西進行分組:export const project = { namespace: 'project', state: {}, reducers: {}, effects: {}, subscriptions: {} }從這個結構可以看出,這個在dva中被稱為model的東西,定義了:它是面向的什麼業務模型需要在全局存儲什麼樣的數據結構經過哪些操作去變更數據面向同一種業務實體的數據結構、業務邏輯可以組織到一起,這樣,對業務代碼的維護是比較有利的。對一個大型應用來說,可以根據業務來劃分model。Vue技術棧的Vuex也是用類似的結構來進行業務歸類的,它們都是受elm的啟發而創建,因此會有類似結構。回想到上一節,我們提到,如果若干個reducer修改的是state的不同位置,可以分別收斂之後,再進行合併。如果我們把狀態結構按照上面這種業務模型的方式進行管理,就可以採用這種機制來分別收斂。這樣,單個model內部就形成了一個閉環,能夠比較清晰的描述自身所代表的業務含義,也便於做測試等等。MobX的Store就是類似這樣的一個組織形式:class TodoStore { authorStore @observable todos = @observable isLoading = true constructor(authorStore) { this.authorStore = authorStore this.loadTodos } loadTodos {} updateTodoFromServer(json) {} createTodo {} removeTodo(todo) {} }依照之前的思路,我們所謂的model其實就是一個合併之後生成state結構的數據管道,因為我們的管道是可以組合的,所以沒有特別的必要去按照上面那種結構定義。那麼,在整個應用的最上層,是否還有必要去做combineReducer這種操作呢?我們之前提到一個表達式:整個React-Redux體系,都是傾向於讓使用者儘可能去從整體的角度關注變化,比如說,Redux的輸入輸出結果是整個應用變更前後的完整狀態,React接受的是整個組件的完整狀態,然後,內部再去做diff。我們需要注意到,為什麼不是直接把Redux接在React上,而是通過一個叫做react-redux的庫呢?因為它需要藉助這個庫,去從整體的state結構上檢出變化的部分,拿給對應的組件去重繪。所以,我們發現如下事實:在觸發reducer的時候,我們是精確知道要修改state的什麼位置的合併完reducer之後,輸出結果是個完整state對象,已經不知道state的什麼位置被修改過了視圖組件必須精確地拿到變更的部分,才能排除無效的渲染整個過程,是經歷了變更信息的擁有——丟失——重新擁有過程的。如果我們的數據流是按照業務模型去分別建立的,我們可以不需要去做這個全合併的操作,而是根據需要,選擇合併其中一部分去進行運算。這樣的話,整個變更過程都是精確的,減少了不必要的diff和緩存。如果為了使用redux-tool的話,可以全部合併起來,往redux-tool裡面寫入每次的全局狀態變更信息,供調試使用,而因為數據管道是懶執行的,我們可以做到開發階段訂閱整個state,而運行時不訂閱,以減少不必要的合併開銷。Model的結構我們從宏觀上對業務模型作了分類的組織,接下來就需要關注每種業務模型的數據管道上,數據格式應當如何管理了。在Redux,Vuex這樣的實踐中,很多人都會有這樣的糾結:在store中,應當以什麼樣的形式存放數據?通常,會有兩種選擇:打平了的數據,儘可能以id這樣的key去索引貼近視圖的數據,比如樹形結構前者有利於查詢和更新,而後者能夠直接給視圖使用。我們需要思考一個問題:將處理過後的視圖狀態存放在store中是否合理?我認為不應當存太偏向視圖結構的數據,理由如下:某一種業務數據,很可能被不同的視圖使用,它們的結構未必一致,如果按照視圖的格式存儲,就要在store中存放不同形式的多份,它們之間的同步是個大問題,也會導致store嚴重膨脹,隨著應用規模的擴大,這個問題更加嚴重。既然這樣,那就要解決從這種數據到視圖所需數據的關聯關係,這個處理過程放在哪裡合適呢?在Redux和Vuex中,為了數據的變更受控,應當在reducer或者mutation中去做狀態變更,但這兩者修改的又是store,這又繞回去了:為了視圖渲染方便而計算出來的數據,如果在reducer或者mutation中做,還是得放在store里。所以,就有了一個結論:從原始數據到視圖數據的處理過程不應當放在reducer或mutation中,那很顯然就應當放在視圖組件的內部去做。[ View <-- VM ] <-- State ↓ ↑ Action --> Reducer這個圖中,方括弧的部分是視圖組件,它內部包含了從原始state到view所需數據的變動,以React為例,用代碼表示:render(props) { const { flatternData } = props const viewData = formatData(flatternData) // ...render viewData }經過這樣的拆分之後,store中的結構更加簡單清晰,reducer的職責也更少了,視圖有更大的自主權,去從原始數據組裝成自己要的樣子。在大型業務開發的過程中,store的結構應當儘早穩定無爭議,避免因為視圖的變化而不停調整,因此,存放相對原始一些的數據是更合理的,這樣也會避免視圖組件在理解數據上的歧義。多個視圖很可能以不同的業務含義去看待狀態樹上的同一個分支,這會造成很多麻煩。我們期望在store中存儲更偏向於更扁平化的原始數據。即使是對於從後端返回的層級數據,也可以藉助normalizr這樣的輔助庫去展開。展開前:[{ id: 1, title: 'Some Article', author: { id: 1, name: 'Dan' } }, { id: 2, title: 'Other Article', author: { id: 1, name: 'Dan' } }]展開后:{ result: [1, 2], entities: { articles: { 1: { id: 1, title: 'Some Article', author: 1 }, 2: { id: 2, title: 'Other Article', author: 1 } }, users: { 1: { id: 1, name: 'Dan' } } } }很明顯,這樣的結構對我們的後續操作是比較便利的。因為我們手裡有數據管道這樣的利器,所以不擔心數據是比較原始的、離散的,因為對它們作聚合處理是比較容易的,所以可以放心地把這些數據打成比較原始的形態。前端的數據建模之前我們提到過store裡面存放的是扁平化的原始數據,但是需要注意到,同樣是扁平化,可能有像map那樣基於id作索引的,也可能有基於數組形式存放的,很多時候,我們是兩種都要的。在更複雜的情況下,還會需要有對象關係的關聯,一對一,一對多,多對多,這就導致視圖在需要使用store中的數據進行組合的時候,不管是store的結構定義還是組合操作都比較麻煩。如果前端是單一業務模型,那我們按照前一節的方案,已經可以做到當數據變更的時候,把當前狀態推送給訂閱它的組件,但實際情況下,都會比這個複雜,業務模型之間會存在關聯關係,在一個模型變更的時候,可能需要自動觸發所關聯到的模型的更新。如果複雜度較低,我們可以手動處理這種關聯,如果聯動關係非常複雜,可以考慮對數據按照實體、關係進行建模,甚至加入一個迷你版的類似ORM的庫來定義這種關係。組織可以有下層組織組織下可以有人員組織和人員是一對多的關係如果一個數據流訂閱了某個組織的基本信息,它可能只反映這個組織自身實體上的變更,而另外一個數據流訂閱了該組織的全部信息,用於形成一個實時更新的組織全視圖,則需要聚合該組織和可能的下級組織、人員的變動匯總。上層視圖可以根據自己的需要,選擇從不同的數據流訂閱不同複雜度的信息。在這種情況下,可以把整個ORM模塊整體視為一個外部的數據源。整個流程如下:這裡面有幾個需要注意的地方:一個action實際上還是對應到一個reducer,然後發起對state的更改,但因為state已經不是簡單結構了,所以我們不能直接改,而是通過這層類似ORM的關係去改。對ORM的一次修改,可能會產生對state的若干處改動,比如說,改了一個數據,可能會推導出業務上與之有關係的一塊關聯數據的變更。如果是基於react-redux這樣基於diff的機制,同時修改state的多個位置是可以的,但在我們這套機制里,因為沒有了先合併修改再diff的過程,所以很可能多個位置的修改需要通過ORM的關聯,延伸出不同的管道來。視圖訂閱的state變更,只能組合運算,不應當再干別的事情了。在這麼一種體系下,實際上前端存在著一個類似資料庫的機制,我們可以把每種數據的變動原子化,一次提交只更新單一類型的實體。這樣,我們相當於在前端部分做了一個讀寫分離,讀取的部分是被實時更新的,可以包含一種類似游標的機制,供視圖組件訂閱。下面是Redux-ORM的簡單示例,是不是很像在操作資料庫?class Todo extends Model {} Todo.modelName = 'Todo'; Todo.fields = { user: fk('User', 'todos'), tags: many('Tag', 'todos'), }; class Tag extends Model {} Tag.modelName = 'Tag'; Tag.backend = { idAttribute: 'name'; }; class User extends Model {} User.modelName = 'User';小 結文章最開始,我們提到最理想的組件化開發方式是依託組件樹的結構,每個組件完成自己內部事務的處理。當組件之間出現通信需求的時候,不得不藉助於Redux之類的庫來做轉發。但是Redux的理念,又不僅僅是只定位於做轉發,它更是期望能管理整個應用的狀態,這反過來對組件的實現,甚至應用的整體架構造成了較大的影響。我們仍然會期望有一種機制,能夠像分形那樣進行開發,但又希望能夠避免狀態管理的混亂,因此,MVI這樣的模式某種程度上能夠滿足這種需求,並且達到邏輯上的自洽。如果以MVI的理念來進行開發,它的一個組件其實是:數據模型、動作、視圖三者的集合,這麼一個MVI組件相當於React-Redux體系中,connect了store之後的高階組件。因此,我們只需把傳統的組件作一些處理:這樣,組件就是自洽的一個東西,它不關注外面是不是Redux,有沒有全局的store,每個組件自己內部運行著一個類似Redux的東西,這樣的一個組件可以更加容易與其他組件進行配合。與Redux相比,這套機制的特點是:不需要顯式定義整個應用的state結構全局狀態和本地狀態可以良好地統一起來可以存在非顯式的action,並且action可以不集中解析,而是分散執行可以存在非顯式的reducer,它附著在數據管道的運算中非同步操作先映射為數據,然後通過單向聯動關係組合計算出視圖狀態回顧整個操作過程:數據的寫入部分,都是通過類似Redux的action去做數據的讀取部分,都是通過數據管道的組合訂閱去做藉助RxJS或者xstream這樣的數據管道的理念,我們可以直觀地表達出數據的整個變更過程,也可以把多個數據流進行便捷的組合。如果使用Redux,正常情況下,需要引入至少一種非同步中間件,而RxJS因為自身就是為處理非同步操作而設計的,所以,只需用它控制好從非同步操作到同步的收斂,就可以達到Redux一樣的數據單向流動。如果想要在數據管道中接入一段承擔中間件職責的東西,也是非常容易的。而RxJS、xstream所提供的數據流組合功能非常強大,天然提供了一切非同步操作的統一抽象,這一點是其他非同步方案很難相比的。所以,這些庫,因為擁有下面這些特性,很適合做數據流控制:對事件的高度抽象同步和非同步的統一化處理數據變更的持續訂閱(訂閱模式)數據的連續變更(管道拼接)數據變更的的組合運算(管道組合)懶執行(無訂閱者,則不執行)緩存的中間結果可重放的歷史記錄等等……今日薦文 前端每周清單第9期:向重度jQuery介紹Vue;React Studio 1.0.2、ECharts GL 1.0 alpha、Prettier 1.0發布視野拓展InfoQ主辦的移動和前端開發領域的精品大會【GMTC 2017】將於6月9~10日在北京舉行,作為首屆以「大前端」為主題的大會,GMTC涉及移動、前端、跨平台、AI應用等多個技術領域,幫助你方方面面提高技術水平。掃描下圖二維碼或戳閱讀原文,前往官網了解詳細信息!前端之巔「前端之巔」是InfoQ旗下關注前端技術的垂直社群,加入前端之巔學習群請關注「前端之巔」公眾號后回復「加群」。推薦分享或投稿請發郵件到editors@cn.infoq.com,註明「前端之巔投稿」。

本文由yidianzixun提供 原文連結

寫了 5860316篇文章,獲得 23313次喜歡
精彩推薦