填坑之路:Flux迂求、Redux、Context與Mobx一網(wǎng)打盡

文中涉及的React demo代碼都使用了16.8的新增特性Hooks

它可以讓你在不編寫(xiě)class的情況下使用state以及其他的React特性晃跺。

前言

剛立項(xiàng)時(shí)揩局,你的所有代碼可能就只有一個(gè)根組件Root —— 擼起袖子就是干!

項(xiàng)目慢慢有了起色掀虎,一些哥們就拆分了一些子組件凌盯,必然付枫,它們間將有一些數(shù)據(jù)流動(dòng) —— 問(wèn)題不大,可以讓它們緊密聯(lián)系驰怎。

父子相連

現(xiàn)在項(xiàng)目進(jìn)展火爆阐滩,業(yè)務(wù)N倍增長(zhǎng),不得不拆出更多的子孫組件出來(lái)县忌,實(shí)現(xiàn)更多復(fù)雜業(yè)務(wù) —— 但愿邏輯比較簡(jiǎn)單掂榔,數(shù)據(jù)流動(dòng)是一層層往下

組件樹(shù)

不過(guò),現(xiàn)實(shí)總是很殘酷症杏,父子孫組件間關(guān)系往往混亂無(wú)比装获。

邏輯混亂

怎么辦,怎么辦厉颤?穴豫??

只要思想不滑坡逼友,辦法總比困難多

  • 方案1精肃,梳理項(xiàng)目邏輯,重新設(shè)計(jì)組件??
  • 方案2帜乞,辭職司抱,換個(gè)公司重開(kāi)???

確實(shí),項(xiàng)目迭代過(guò)程中,不可避免地就會(huì)出現(xiàn)組件間狀態(tài)共享,而導(dǎo)致邏輯交錯(cuò),難以控制。

那我們就會(huì)想:"能不能有一種實(shí)踐規(guī)范茂蚓,將所有可能公用的狀態(tài)、數(shù)據(jù)及能力提取到組件外雅宾,數(shù)據(jù)流自上往下唆迁,哪里需要哪里自己獲取,而不是prop drilling"后频,大概長(zhǎng)這樣:

單向數(shù)據(jù)流

于是這樣一種數(shù)據(jù)結(jié)構(gòu)冒了出來(lái):

const store = {
    state: {
        text: 'Goodbye World!'
    },
    setAction (text) {
        this.text = text
    },
    clearAction () {
        this.text = ''
    }
}

存在外部變量store

  • state來(lái)存儲(chǔ)數(shù)據(jù)
  • 有一堆功能各異的action來(lái)控制state的改變

再加上強(qiáng)制約束:只能通過(guò)調(diào)用action來(lái)改變state梳庆,然后我們就可以通過(guò)action清晰地掌握著state的動(dòng)向,那么日志卑惜、監(jiān)控膏执、回滾等能力還有啥擔(dān)心的。

其實(shí)露久,這就是Flux的早早期雛形更米。

Flux

2013年,F(xiàn)acebook亮出React的時(shí)候毫痕,也跟著帶出的Flux征峦。Facebook認(rèn)為兩者相輔相成迟几,結(jié)合在一起才能構(gòu)建大型的JavaScript應(yīng)用。

做一個(gè)容易理解的對(duì)比栏笆,React是用來(lái)替換jQuery的类腮,那么Flux就是以替換Backbone.jsEmber.jsMVC一族框架為目的蛉加。

Flux data flow

如上圖蚜枢,數(shù)據(jù)總是“單向流動(dòng)”,相鄰部分不存在互相流動(dòng)數(shù)據(jù)的現(xiàn)象针饥,這也是Flux一大特點(diǎn)厂抽。

  • View發(fā)起用戶的Action
  • Dispatcher作為調(diào)度中心,接收Action打厘,要求Store進(jìn)行相應(yīng)更新
  • Store處理主要邏輯修肠,并提供監(jiān)聽(tīng)能力,當(dāng)數(shù)據(jù)更新后觸發(fā)監(jiān)聽(tīng)事件
  • View監(jiān)聽(tīng)到Store的更新事件后觸發(fā)UI更新

感興趣可以看看每個(gè)部分的具體含義:

Action

plain javascript object户盯,一般使用typepayload描述了該action的具體含義嵌施。

Flux中一般定義actions:一組包含派發(fā)action對(duì)象的函數(shù)。

// actions.js
import AddDispatcher from '@/dispatcher'

export const counterActions = {
    increment (number) {
        const action = {
            type: 'INCREMENT',
            payload: number
        }

        AddDispatcher.dispatch(action)
    }
}

以上代碼莽鸭,使用counterActions.increment吗伤,將INCREMENT派發(fā)到Store

Dispatcher

Action派發(fā)到Store硫眨,通過(guò)Flux提供的Dispatcher注冊(cè)唯一實(shí)例足淆。

Dispatcher.register方法用來(lái)登記各種Action的回調(diào)函數(shù)

import { CounterStore } from '@/store'
import AddDispatcher from '@/dispatcher'

AppDispatcher.register(function (action) {
  switch (action.type) {
    case INCREMENT:
      CounterStore.addHandler();
      CounterStore.emitChange();
      break;
    default:
    // no op
  }
});

以上代碼,AppDispatcher收到INCREMENT動(dòng)作礁阁,就會(huì)執(zhí)行回調(diào)函數(shù)巧号,對(duì)CounterStore進(jìn)行操作。

Dispatcher只用來(lái)派發(fā)Action姥闭,不應(yīng)該有其他邏輯丹鸿。

Store

應(yīng)用狀態(tài)的處理中心。

Store中復(fù)雜處理業(yè)務(wù)邏輯棚品,而由于數(shù)據(jù)變更后View需要更新靠欢,所以它也負(fù)責(zé)提供通知視圖更新的能力。

因?yàn)槠潆S用隨注冊(cè)铜跑,一個(gè)應(yīng)用可以注冊(cè)多個(gè)Store的能力门怪,更新Data Dlow為

mul-store

細(xì)心的朋友可以發(fā)現(xiàn)在上一小節(jié)CounterStore中調(diào)用了emitChange的方法 —— 對(duì),它就是用來(lái)通知變更的锅纺。

import { EventEmitter } from "events"

export const CounterStore = Object.assign({}, EventEmitter.prototype, {
  counter: 0,
  getCounter: function () {
    return this.counter
  },
  addHandler: function () {
    this.counter++
  },
  emitChange: function () {
    this.emit("change")
  },
  addChangeListener: function (callback) {
    this.on("change", callback)
  },
  removeChangeListener: function (callback) {
    this.removeListener("change", callback)
  }
});

以上代碼掷空,CounterStore通過(guò)繼承EventEmitter.prototype獲得觸發(fā)emit與監(jiān)聽(tīng)on事件能力。

View

Store中的數(shù)據(jù)的視圖展示

View需要監(jiān)聽(tīng)視圖中數(shù)據(jù)的變動(dòng)來(lái)保證視圖實(shí)時(shí)更新,即

  • 在組件中需要添加addChangeListerner
  • 在組件銷毀時(shí)移除監(jiān)聽(tīng)removeChangeListener

我們看個(gè)簡(jiǎn)單的Couter例子拣帽,更好的理解下實(shí)際使用疼电。

(手動(dòng)分割)

認(rèn)真體驗(yàn)的朋友可能會(huì)注意到:

  • 點(diǎn)擊reset后,store中的couter被更新(沒(méi)有emitChange 所以沒(méi)實(shí)時(shí)更新視圖)减拭;
  • 業(yè)務(wù)邏輯與數(shù)據(jù)處理邏輯交錯(cuò)蔽豺,代碼組織混亂;

好拧粪,打住修陡,再看個(gè)新的數(shù)據(jù)流。

Redux

Redux Data Flow
  • 用戶與View進(jìn)行交互
  • 通過(guò)Action Creator派發(fā)action
  • 到達(dá)Store后拿到當(dāng)前的State可霎,一并交給Reducer
  • Reducer經(jīng)過(guò)處理后返回全新的StateStore
  • Store更新后通知View魄鸦,完成一次數(shù)據(jù)更新

Flux的基本原則是“單向數(shù)據(jù)流”,Redux在此基礎(chǔ)上強(qiáng)調(diào):

  • 唯一數(shù)據(jù)源(Single Source of Truth):整個(gè)應(yīng)用只保持一個(gè)Store癣朗,所有組件的數(shù)據(jù)源就是該Store的狀態(tài)拾因。
  • 保持狀態(tài)只讀(State is read-only):不直接修改狀態(tài),要修改Store的狀態(tài)旷余,必須要通過(guò)派發(fā)一個(gè)action對(duì)象完成绢记。
  • 數(shù)據(jù)改變只能通過(guò)純函數(shù)完成(Changes are made with pure funtions):這里所說(shuō)的純函數(shù)指reducer

感興趣可以看看每個(gè)部分的具體含義:

(Redux的源碼及其短小優(yōu)雅正卧,有想嘗試閱讀源碼的朋友可以從它開(kāi)始)

Store

應(yīng)用唯一的數(shù)據(jù)存儲(chǔ)中心

import { createStore } from 'redux'

const store = createStore(fn)

以上代碼蠢熄,使用redux提供的createStore函數(shù),接受另一個(gè)函數(shù)fn(即稍后提到的Reducers)作為參數(shù)炉旷,生成應(yīng)用唯一的store签孔。

可以看看簡(jiǎn)單實(shí)現(xiàn)的createStore函數(shù)

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(l => l !== listener)
    }
  }

  dispatch({})

  return { getState, dispatch, subscribe }
}

本人看源碼有個(gè)小技巧,一般先從導(dǎo)出找起窘行,再看return饥追。

如上,return出去三個(gè)能力:

  • getState: 獲取state的唯一方法罐盔,state被稱為store的快照
  • dispatch: view派發(fā)action的唯一方法
  • subscribe: 注冊(cè)監(jiān)聽(tīng)函數(shù)(核心但绕,待會(huì)要考),返回解除監(jiān)聽(tīng)

注意到以上代碼片段最后翘骂,dispatch了一個(gè)空對(duì)象壁熄,是為了生成初始的state帚豪,學(xué)習(xí)了reducer的寫(xiě)法后可以解釋原理碳竟。

當(dāng)然,createStore還可以接收更多的參數(shù)狸臣,如:preloadedState(默認(rèn)state)莹桅,enhancerstore的超能力蘑菇)等,我們后面會(huì)分析到。

Action

plain javascript object诈泼,一般使用typepayload描述了該action的具體含義懂拾。

reduxtype屬性是必須的铐达,表示Action的名稱岖赋,其他屬性可以自由設(shè)置,參照規(guī)范瓮孙。

const actions = {
    type: 'ADD_TODO',
    payload: 'Learn Redux'
}

可以用Action Creator批量來(lái)生成一些Action唐断,如下addTodo就是一個(gè)Action Creator,它接受不同的參數(shù)生成不同的action:

function addTodo(text) {
  return {
    type: 'ADD_TODO',
    payload: text
  }
}

const action = addTodo('Learn Redux')

reducer

純函數(shù)杭抠,根據(jù)action更新store

 (previousState, action) => newState

以上脸甘,是reducer的函數(shù)簽名,接收來(lái)自viewaction偏灿,并從store上拿到最新state丹诀,經(jīng)過(guò)處理后返回一個(gè)全新的state更新視圖。

const reducers = (state = defaultState, action) => {
    const { type, payload } = action
    
    switch (type) {
        case 'ADD_TODO':
            return {
                ...state,
                counter: state.counter + (+payload)
            }
        default:
            return state
    }
}

以上代碼翁垂,createStore留下的懸念可以從default分支獲得答案铆遭。

reducer返回的結(jié)果一定要是一個(gè)全新的state,尤其是涉及到引用數(shù)據(jù)類型的操作時(shí)沮峡,因?yàn)?code>react對(duì)數(shù)據(jù)更新的判斷都是淺比較疚脐,如果更新前后是同一個(gè)引用,那么react將會(huì)忽略這一次更新邢疙。

理想狀態(tài)state結(jié)構(gòu)層級(jí)可能比較簡(jiǎn)單棍弄,那么如果state樹(shù)枝葉后代比較復(fù)雜時(shí)怎么辦(state.a.b.c)?

const reducers = (state = {}, action) => {
    const { type, payload } = action
    
    switch(type) {
        case 'ADD':
            return {
                ...state,
                a: {
                    ...state.a,
                    b: {
                        ...state.a.b,
                        c: state.a.b.c.concat(payload)
                    }
                }
            }
        default:
            return state
    }
}

先不討論以上寫(xiě)法風(fēng)險(xiǎn)如何疟游,就這一層層看著都吐呼畸。

既然這樣,我們?cè)傧胂朕k法颁虐。

前面提到蛮原,Reduxstore唯一,所以我們只要能保證在reducer中返回的state是一個(gè)完整的結(jié)構(gòu)就行另绩,那是不是可以:

const reducers = (state = {}, action) => {
    return {
         A: reducer1(state.A, action),
         B: reducer2(state.B, action),
         C: reducer3(state.C, action)
    }
}

以上儒陨,我們曲線救國(guó),將復(fù)雜的數(shù)據(jù)結(jié)構(gòu)拆分笋籽,每個(gè)reducer管理state樹(shù)不同枝干蹦漠,最后再將所有reducer合并后給createStore,這正是combineReducer的設(shè)計(jì)思路车海。

combineReducer

import { combineReducers, createStore } from 'redux'

const reducers = combineReducers({
  A: reducer1,
  B: reducer2,
  C: reducer3
})

const store = createStore(reducers)

以上笛园,根據(jù)statekey去執(zhí)行相應(yīng)的子reducer,并將返回結(jié)果合并成一個(gè)大的state對(duì)象。

可以看下簡(jiǎn)單實(shí)現(xiàn):

const combineReducers = reducers => (state = {}, action) => {
    return Object.keys(reducers).reduce((nextState, key) => {
        nextState[key] = reducers[key](state[key], action)
        return nextState
    }, {})
}

以上介紹了Redux的基本能力研铆,再看個(gè)Demo加深加深印象埋同。

(再次手動(dòng)分割)

可以注意到一個(gè)痛點(diǎn):

  • component得主動(dòng)去訂閱store.subscribe``state的變更,讓代碼顯得很蠢棵红,不太“雅”凶赁。

Flux vs Redux

好,redux的基本面都覆蓋了逆甜,它是基于Flux的核心思想實(shí)現(xiàn)的一套解決方案哟冬,從以上分析我們可以感受到區(qū)別:

Flux vs Redux

以上,從storedispatcher兩個(gè)本質(zhì)區(qū)別比對(duì)了二者忆绰,相信你們英文一定比我好浩峡,就不翻譯了。

(不要問(wèn)我為什么要麻將牌+英文排列错敢,問(wèn)就是“中西合璧”)

ReduxFlux類似翰灾,只是一種思想或者規(guī)范,它和React之間沒(méi)有關(guān)系稚茅。Redux支持React纸淮、AngularEmber亚享、jQuery甚至純JavaScript咽块。

因?yàn)?code>React包含函數(shù)式的思想,也是單向數(shù)據(jù)流欺税,和Redux很搭侈沪,所以一般都用Redux來(lái)進(jìn)行狀態(tài)管理。

當(dāng)然晚凿,不是所有項(xiàng)目都無(wú)腦推薦redux亭罪,Dan Abramov很早前也提到“You Might Not Need Redux”,只有遇到react不好解決的問(wèn)題我們才考慮使用redux歼秽,比如:

  • 用戶的使用方式復(fù)雜
  • 不同身份的用戶有不同的使用方式(比如普通用戶和管理員)
  • 多個(gè)用戶之間可以協(xié)作/與服務(wù)器大量交互应役,或者使用了WebSocket
  • View要從多個(gè)來(lái)源獲取數(shù)據(jù)
  • ...

(再再次手動(dòng)分割)

好,我們繼續(xù)來(lái)聊Redux燥筷。

以上箩祥,我們處理的都是同步且邏輯簡(jiǎn)單的Redux使用場(chǎng)景,真正的業(yè)務(wù)開(kāi)發(fā)場(chǎng)景遠(yuǎn)比這復(fù)雜肆氓,各種異步任務(wù)不可避免袍祖,這時(shí)候怎么辦?

一起跟著Redux的Data Flow分析一下:

  • Viewstate的視覺(jué)層做院,與之一一對(duì)應(yīng)盲泛,不合適承擔(dān)其他功能;
  • Action:描述一個(gè)動(dòng)作的具體內(nèi)容键耕,只能被操作寺滚,自己不能進(jìn)行任何操作
  • Reducer:純函數(shù),只承擔(dān)計(jì)算state的功能屈雄,不合適承擔(dān)其他功能

看來(lái)如果想要在action發(fā)出后做一些額外復(fù)雜的同步/異步操作村视,只有在派發(fā)action,即dispatch時(shí)可以做點(diǎn)手腳酒奶,我們稱負(fù)責(zé)這些復(fù)雜操作:中間件Middleware蚁孔。

Middleware

It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

以上直譯:Middleware提供了第三方的拓展能力,作用于在發(fā)起actionaction到達(dá)reducer之間惋嚎。

比如我們想在發(fā)送action前后添加打印功能杠氢,中間件雛形大概就是這樣:

let next = store.dispatch
store.dispatch = function Logger(store, action) {
  console.log('dispatching', action)
  next(action)
  console.log('next state', store.getState())
}

// 遵循middleware規(guī)范的currying寫(xiě)法
const Logger = store => next => action => {
  console.log('dispatching', action)
  next(action)
  console.log('next state', store.getState())
}

先補(bǔ)充個(gè)前置知識(shí),前面說(shuō)過(guò)createStore可以接收除了reducers之外更多的參數(shù)另伍,其中一個(gè)參數(shù)enhancer就是表示你要注冊(cè)的中間件們鼻百,再看看createStore怎么用它?

// https://github.com/reduxjs/redux/blob/v4.0.4/src/createStore.js#L53
...
enhancer(createStore)(reducer, preloadedState)
...

了解了以上代碼后摆尝,我們來(lái)看看redux源碼是如何實(shí)現(xiàn)store.dispatch的偷梁換柱的温艇。

// https://github.com/reduxjs/redux/blob/v4.0.4/src/applyMiddleware.js
export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

可以看到,applyMiddleware接收的所有中間件使用map去了currying最外面的一層堕汞,這里的middlewareAPI即簡(jiǎn)易版的store勺爱,它保證每個(gè)中間件都能拿到當(dāng)前的同一個(gè)store,拿到的chain[next => action => {}, ...]這樣一個(gè)數(shù)組讯检。

而后琐鲁,使用compose(函數(shù)組合),將以上得到的chain串起來(lái):

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

簡(jiǎn)單明了人灼,compose的能力就是將[a, b, c]組合成(...args) => a(b(c(...args)))

回到上面绣否,將中間件鏈組合后,再接收store.dispatch(可以理解挡毅,這里就是我們需要的next)蒜撮,增強(qiáng)后的dispatch

dispatch = middleware1(middleware2(middleware3(store.dispatch)))

結(jié)合我們中間件的范式:next => action => next(action)store.dispatch作為middleware3next跪呈,...段磨,middleware2(middleware3(store.dispatch))作為middleware1next,豁然開(kāi)朗耗绿,就這樣dispatch得到了升華苹支,不過(guò)如此♂?。

(你看看误阻,你看看债蜜,核心代碼晴埂,就這短短幾行,卻韻味十足寻定,還有天理嗎儒洛?心動(dòng)了嗎?心動(dòng)了還不打開(kāi)gayhub操作起來(lái)?)

當(dāng)然講到這里,如果對(duì)React生態(tài)有些許了解的同學(xué)可能會(huì)說(shuō)咙俩,“React里面不是有種概念叫 Context,而且隨著版本迭代恼蓬,功能越來(lái)越強(qiáng)大,我可以不用Redux嗎僵芹?处硬??”

Context

React文檔官網(wǎng)并未對(duì)Context給出明確定義拇派,更多是描述使用場(chǎng)景郁油,以及如何使用Context

In some cases, you want to pass data through the component tree without having to pass the props down manuallys at every level. you can do this directly in React with the powerful ‘context’ API.

簡(jiǎn)單說(shuō)就是攀痊,當(dāng)你不想在組件樹(shù)中通過(guò)逐層傳遞props或者state的方式來(lái)傳遞數(shù)據(jù)時(shí)桐腌,可以使用Context api來(lái)實(shí)現(xiàn)跨層級(jí)的組件數(shù)據(jù)傳遞。

import { createContext } from "react";

export const CounterContext = createContext(null);

我們聲明一個(gè)CounterContext簡(jiǎn)單講解使用方法苟径,ceateContext接收默認(rèn)值案站。

Provider

包裹目標(biāo)組件,聲明value作為share state

import React, { useState } from "react"
import { CounterContext } from "./context"

import App from "./App"

const Main = () => {
    const [counter, setCounter] = useState(0)
    return (
        <CounterContext.Provider
            value={{
                counter,
                add: () => setCounter(counter + 1),
                dec: () => setCounter(counter - 1)
            }}
        >
            <App />
        </CounterContext.Provider>
    )
}

如上棘街,在App外層包裹Provider蟆盐,并提供了counter的一些運(yùn)算。

Comsumer

消費(fèi)Provider提供的value

import React, { useContext } from "react";
import { CounterContext } from "./context";
import "./styles.css";

export default function App(props) {
  let state = useContext(CounterContext);

  return (
      <>
        ...
      </>
  )
}

(以上使用了Contexthooks新寫(xiě)法遭殉,注意確定您的React版本>=16.8后再做以上嘗試)

App的任意子孫組件都可以隨地使用useContext取到Prodider上的值石挂。

以上就是Context的全部?jī)?nèi)容了,我們老規(guī)矩险污,簡(jiǎn)單看個(gè)Counter后于Redux做個(gè)比較痹愚。

Context vs Redux

Context vs Redux

其實(shí)吧,這二者沒(méi)太多可比較的蛔糯。

Context api可以說(shuō)是簡(jiǎn)化版的Redux拯腮,它不能結(jié)合強(qiáng)大的middleware擴(kuò)展自己的超能力,比如redux-thunkredux-saga等做復(fù)雜的異步任務(wù)蚁飒,也沒(méi)有完善的開(kāi)發(fā)/定位能力动壤,不過(guò)如果你只是想找個(gè)地方存share data來(lái)避開(kāi)惡心的props drilling的問(wèn)題,那么Context api的確值得你為他拍手叫好淮逻。

react-redux

Redux作為數(shù)據(jù)層琼懊,出色地完成了所有數(shù)據(jù)層面的事物阁簸,而React作為一個(gè)UI框架,給我一個(gè)state我就能給你一個(gè)UI view哼丈,現(xiàn)在的關(guān)鍵在于需要將Reduxstate的更新通知到React启妹,讓其及時(shí)更新UI

于是React團(tuán)隊(duì)出手了削祈,他們動(dòng)手給React做了適配,它的產(chǎn)物就是react-redux脑漫。

Provider

包裹目標(biāo)組件髓抑,接收store作為share state

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'

import App from './pages'
import reducers from './reducers'

const store = createStore(reducers)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

以上就是一個(gè)標(biāo)準(zhǔn)的React項(xiàng)目入口,Provider接收Redux提供的唯一store优幸。

connect

連接componentstore吨拍,賦予component使用statedispatch action的能力

import { connect } from "react-redux"

const mapStateToProps = (state) => ({
  counter: state.counter
});

const mapDispatchToProps = {
  add: () => ({ type: 'INCREMENT' }),
  dec: () => ({ type: 'DECREMENT' })
};

export default connect(mapStateToProps, mapDispatchToProps)(App)

以上代碼片段,

  • mapStateToProps接收state网杆,獲取component想要的值
  • mapDispatchToProps聲明了一些action creator羹饰,并由connect提供dispatch能力,賦予component派發(fā)action的能力
  • 它還接收mergePropsoptions等自定義參數(shù)

老規(guī)矩碳却,我們來(lái)看看基于react-redux實(shí)現(xiàn)的Counter队秩。

Redux痛點(diǎn)

回顧一下,我們?cè)谑褂?code>Redux的實(shí)例時(shí)昼浦,分析其痛點(diǎn)馍资,是什么?

對(duì)(雖然沒(méi)人回答关噪,但是我從你們心里聽(tīng)到了)

“ 組件需要主動(dòng)訂閱store的更新 ”

react-reduxdemo與之相比鸟蟹,比較直觀的感受就是:不再是哪里需要就哪里subscribe,而只需要connect使兔。

那斗膽問(wèn)一句:“以現(xiàn)有的知識(shí)建钥,結(jié)合剛剛分析的用法,你會(huì)怎么實(shí)現(xiàn)react-redux虐沥?”

源碼分析

沒(méi)錯(cuò)熊经,必然是Context api啊,一起簡(jiǎn)單看看源碼驗(yàn)證下猜想欲险。

搜索整個(gè)項(xiàng)目奈搜,我們只用到react-redux提供的唯一兩個(gè)api,我們可以很快從入口處找到他們的蹤跡盯荤。

Provider

react-redux汲取了Context api的的精華 才得以實(shí)現(xiàn)在app的每個(gè)角落都能拿到storestate

import React, { useMemo, useEffect } from 'react'
import { ReactReduxContext } from './Context'
// 對(duì)store.subscribe的抽象
import Subscription from '../utils/Subscription'

function Provider({ store, context, children }) {
  const contextValue = useMemo(() => {
    const subscription = new Subscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription,
    }
  }, [store])

  // 使用userMemo緩存數(shù)據(jù)馋吗,避免多余的re-render
  const previousState = useMemo(() => store.getState(), [store])

  // 當(dāng)contectValue, previousState變化時(shí),通知訂閱者作出響應(yīng)
  useEffect(() => {
    const { subscription } = contextValue
    subscription.trySubscribe()

    if (previousState !== store.getState()) {
      subscription.notifyNestedSubs()
    }
    return () => {
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])
  
  // context nested
  const Context = context || ReactReduxContext

  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

拋開(kāi)復(fù)雜的nested contextre-render的優(yōu)化處理秋秤,Provider無(wú)非就是將接受的store通過(guò)Context api傳遞到每個(gè)組件宏粤。

connect

首先脚翘,我們明確一點(diǎn):connect的目的是從store取得想要的props給到component

所以我們知道只要從provider上拿到store绍哎,然后在connect中使用一個(gè)組件在mounted時(shí)添加對(duì)指定值的subscribe来农,此后它的更新都會(huì)引起被connected的后代組件的re-render,就達(dá)到目的了崇堰。

以上分析其實(shí)就是connect的實(shí)現(xiàn)原理沃于,但是我們知道在React中,props變化的成本很高海诲,它的每次變更都將一起所有后代組件跟隨著它re-render繁莹,所以以下絕大部分代碼都是為了優(yōu)化這一巨大的re-render開(kāi)銷。

export function createConnect({
  connectHOC = connectAdvanced,
  mapStateToPropsFactories = defaultMapStateToPropsFactories,
  mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
  mergePropsFactories = defaultMergePropsFactories,
  selectorFactory = defaultSelectorFactory,
} = {}) {
  return function connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    {
      pure = true,
      areStatesEqual = strictEqual,
      areOwnPropsEqual = shallowEqual,
      areStatePropsEqual = shallowEqual,
      areMergedPropsEqual = shallowEqual,
      ...extraOptions
    } = {}
  ) {
    const initMapStateToProps = match(
      mapStateToProps,
      mapStateToPropsFactories,
      'mapStateToProps'
    )
    const initMapDispatchToProps = match(
      mapDispatchToProps,
      mapDispatchToPropsFactories,
      'mapDispatchToProps'
    )
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

    return connectHOC(selectorFactory, {
      // used in error messages
      methodName: 'connect',

      // used to compute Connect's displayName from the wrapped component's displayName.
      getDisplayName: (name) => `Connect(${name})`,

      // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
      shouldHandleStateChanges: Boolean(mapStateToProps),

      // passed through to selectorFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,

      // any extra options args can override defaults of connect or connectAdvanced
      ...extraOptions,
    })
  }
}

export default /*#__PURE__*/ createConnect()

好奇怪特幔,默認(rèn)導(dǎo)出是createConnectreturn func咨演,它接受了一堆默認(rèn)參數(shù),為什么多此一舉蚯斯?

(認(rèn)真看前面注釋薄风,這些是為了方便更好地做testing case

然后我們繼續(xù)看其內(nèi)部實(shí)現(xiàn),接受的四個(gè)來(lái)自用戶的參數(shù)拍嵌,然后使用match給前三個(gè)初始化了一下

match

很簡(jiǎn)單遭赂,接受一個(gè)工廠函數(shù),以及每次需要初始化的key横辆,從后往前遍歷工廠嵌牺,任何一個(gè)response不為空,則返回(其實(shí)就是為了兼容用戶傳入的參數(shù)龄糊,保證格式與去空)逆粹。

然后是connectHOC,這是處理核心炫惩,它接收了一個(gè)SelectorFactory僻弹。

SelectorFactory

根據(jù)傳入的option.pure(默認(rèn)true)的值來(lái)決定每次返回props是否要緩存,這樣將有效的減少不必要的計(jì)算他嚷,優(yōu)化性能蹋绽。

connectHOC
export default function connectAdvanced(
  /*
    selectorFactory is a func that is responsible for returning the selector function used to
    compute new props from state, props, and dispatch. For example:
      export default connectAdvanced((dispatch, options) => (state, props) => ({
        thing: state.things[props.thingId],
        saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)),
      }))(YourComponent)
    Access to dispatch is provided to the factory so selectorFactories can bind actionCreators
    outside of their selector as an optimization. Options passed to connectAdvanced are passed to
    the selectorFactory, along with displayName and WrappedComponent, as the second argument.
    Note that selectorFactory is responsible for all caching/memoization of inbound and outbound
    props. Do not use connectAdvanced directly without memoizing results between calls to your
    selector, otherwise the Connect component will re-render on every state or props change.
  */
  selectorFactory,
  // options object:
  {
    // the func used to compute this HOC's displayName from the wrapped component's displayName.
    // probably overridden by wrapper functions such as connect()
    getDisplayName = (name) => `ConnectAdvanced(${name})`,

    // shown in error messages
    // probably overridden by wrapper functions such as connect()
    methodName = 'connectAdvanced',

    // REMOVED: if defined, the name of the property passed to the wrapped element indicating the number of
    // calls to render. useful for watching in react devtools for unnecessary re-renders.
    renderCountProp = undefined,

    // determines whether this HOC subscribes to store changes
    shouldHandleStateChanges = true,

    // REMOVED: the key of props/context to get the store
    storeKey = 'store',

    // REMOVED: expose the wrapped component via refs
    withRef = false,

    forwardRef = false,

    // the context consumer to use
    context = ReactReduxContext,

    // additional options are passed through to the selectorFactory
    ...connectOptions
  } = {}
) {
  if (process.env.NODE_ENV !== 'production') {
    if (renderCountProp !== undefined) {
      throw new Error(
        `renderCountProp is removed. render counting is built into the latest React Dev Tools profiling extension`
      )
    }
    if (withRef) {
      throw new Error(
        'withRef is removed. To access the wrapped instance, use a ref on the connected component'
      )
    }

    const customStoreWarningMessage =
      'To use a custom Redux store for specific components, create a custom React context with ' +
      "React.createContext(), and pass the context object to React Redux's Provider and specific components" +
      ' like: <Provider context={MyContext}><ConnectedComponent context={MyContext} /></Provider>. ' +
      'You may also pass a {context : MyContext} option to connect'

    if (storeKey !== 'store') {
      throw new Error(
        'storeKey has been removed and does not do anything. ' +
          customStoreWarningMessage
      )
    }
  }

  const Context = context

  return function wrapWithConnect(WrappedComponent) {
    if (
      process.env.NODE_ENV !== 'production' &&
      !isValidElementType(WrappedComponent)
    ) {
      throw new Error(
        `You must pass a component to the function returned by ` +
          `${methodName}. Instead received ${stringifyComponent(
            WrappedComponent
          )}`
      )
    }

    const wrappedComponentName =
      WrappedComponent.displayName || WrappedComponent.name || 'Component'

    const displayName = getDisplayName(wrappedComponentName)

    const selectorFactoryOptions = {
      ...connectOptions,
      getDisplayName,
      methodName,
      renderCountProp,
      shouldHandleStateChanges,
      storeKey,
      displayName,
      wrappedComponentName,
      WrappedComponent,
    }

    const { pure } = connectOptions

    function createChildSelector(store) {
      return selectorFactory(store.dispatch, selectorFactoryOptions)
    }

    // If we aren't running in "pure" mode, we don't want to memoize values.
    // To avoid conditionally calling hooks, we fall back to a tiny wrapper
    // that just executes the given callback immediately.
    const usePureOnlyMemo = pure ? useMemo : (callback) => callback()

    function ConnectFunction(props) {
      const [
        propsContext,
        reactReduxForwardedRef,
        wrapperProps,
      ] = useMemo(() => {
        // Distinguish between actual "data" props that were passed to the wrapper component,
        // and values needed to control behavior (forwarded refs, alternate context instances).
        // To maintain the wrapperProps object reference, memoize this destructuring.
        const { reactReduxForwardedRef, ...wrapperProps } = props
        return [props.context, reactReduxForwardedRef, wrapperProps]
      }, [props])

      const ContextToUse = useMemo(() => {
        // Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
        // Memoize the check that determines which context instance we should use.
        return propsContext &&
          propsContext.Consumer &&
          isContextConsumer(<propsContext.Consumer />)
          ? propsContext
          : Context
      }, [propsContext, Context])

      // Retrieve the store and ancestor subscription via context, if available
      const contextValue = useContext(ContextToUse)

      // The store _must_ exist as either a prop or in context.
      // We'll check to see if it _looks_ like a Redux store first.
      // This allows us to pass through a `store` prop that is just a plain value.
      
      const didStoreComeFromProps =
        Boolean(props.store) &&
        Boolean(props.store.getState) &&
        Boolean(props.store.dispatch)
      const didStoreComeFromContext =
        Boolean(contextValue) && Boolean(contextValue.store)

      if (
        process.env.NODE_ENV !== 'production' &&
        !didStoreComeFromProps &&
        !didStoreComeFromContext
      ) {
        throw new Error(
          `Could not find "store" in the context of ` +
            `"${displayName}". Either wrap the root component in a <Provider>, ` +
            `or pass a custom React context provider to <Provider> and the corresponding ` +
            `React context consumer to ${displayName} in connect options.`
        )
      }

      // Based on the previous check, one of these must be true
      const store = didStoreComeFromProps ? props.store : contextValue.store

      const childPropsSelector = useMemo(() => {
        // The child props selector needs the store reference as an input.
        // Re-create this selector whenever the store changes.
        return createChildSelector(store)
      }, [store])

      const [subscription, notifyNestedSubs] = useMemo(() => {
        if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY

        // This Subscription's source should match where store came from: props vs. context. A component
        // connected to the store via props shouldn't use subscription from context, or vice versa.
        const subscription = new Subscription(
          store,
          didStoreComeFromProps ? null : contextValue.subscription
        )

        // `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
        // the middle of the notification loop, where `subscription` will then be null. This can
        // probably be avoided if Subscription's listeners logic is changed to not call listeners
        // that have been unsubscribed in the  middle of the notification loop.
        const notifyNestedSubs = subscription.notifyNestedSubs.bind(
          subscription
        )

        return [subscription, notifyNestedSubs]
      }, [store, didStoreComeFromProps, contextValue])

      // Determine what {store, subscription} value should be put into nested context, if necessary,
      // and memoize that value to avoid unnecessary context updates.
      const overriddenContextValue = useMemo(() => {
        if (didStoreComeFromProps) {
          // This component is directly subscribed to a store from props.
          // We don't want descendants reading from this store - pass down whatever
          // the existing context value is from the nearest connected ancestor.
          return contextValue
        }

        // Otherwise, put this component's subscription instance into context, so that
        // connected descendants won't update until after this component is done
        return {
          ...contextValue,
          subscription,
        }
      }, [didStoreComeFromProps, contextValue, subscription])

      // We need to force this wrapper component to re-render whenever a Redux store update
      // causes a change to the calculated child component props (or we caught an error in mapState)
      const [
        [previousStateUpdateResult],
        forceComponentUpdateDispatch,
      ] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)

      // Propagate any mapState/mapDispatch errors upwards
      if (previousStateUpdateResult && previousStateUpdateResult.error) {
        throw previousStateUpdateResult.error
      }

      // Set up refs to coordinate values between the subscription effect and the render logic
      const lastChildProps = useRef()
      const lastWrapperProps = useRef(wrapperProps)
      const childPropsFromStoreUpdate = useRef()
      const renderIsScheduled = useRef(false)

      const actualChildProps = usePureOnlyMemo(() => {
        // Tricky logic here:
        // - This render may have been triggered by a Redux store update that produced new child props
        // - However, we may have gotten new wrapper props after that
        // If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
        // But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
        // So, we'll use the child props from store update only if the wrapper props are the same as last time.
        if (
          childPropsFromStoreUpdate.current &&
          wrapperProps === lastWrapperProps.current
        ) {
          return childPropsFromStoreUpdate.current
        }

        // TODO We're reading the store directly in render() here. Bad idea?
        // This will likely cause Bad Things (TM) to happen in Concurrent Mode.
        // Note that we do this because on renders _not_ caused by store updates, we need the latest store state
        // to determine what the child props should be.
        return childPropsSelector(store.getState(), wrapperProps)
      }, [store, previousStateUpdateResult, wrapperProps])

      // We need this to execute synchronously every time we re-render. However, React warns
      // about useLayoutEffect in SSR, so we try to detect environment and fall back to
      // just useEffect instead to avoid the warning, since neither will run anyway.
      useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
        lastWrapperProps,
        lastChildProps,
        renderIsScheduled,
        wrapperProps,
        actualChildProps,
        childPropsFromStoreUpdate,
        notifyNestedSubs,
      ])

      // Our re-subscribe logic only runs when the store/subscription setup changes
      useIsomorphicLayoutEffectWithArgs(
        subscribeUpdates,
        [
          shouldHandleStateChanges,
          store,
          subscription,
          childPropsSelector,
          lastWrapperProps,
          lastChildProps,
          renderIsScheduled,
          childPropsFromStoreUpdate,
          notifyNestedSubs,
          forceComponentUpdateDispatch,
        ],
        [store, subscription, childPropsSelector]
      )

      // Now that all that's done, we can finally try to actually render the child component.
      // We memoize the elements for the rendered child component as an optimization.
      const renderedWrappedComponent = useMemo(
        () => (
          <WrappedComponent
            {...actualChildProps}
            ref={reactReduxForwardedRef}
          />
        ),
        [reactReduxForwardedRef, WrappedComponent, actualChildProps]
      )

      // If React sees the exact same element reference as last time, it bails out of re-rendering
      // that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
      const renderedChild = useMemo(() => {
        if (shouldHandleStateChanges) {
          // If this component is subscribed to store updates, we need to pass its own
          // subscription instance down to our descendants. That means rendering the same
          // Context instance, and putting a different value into the context.
          return (
            <ContextToUse.Provider value={overriddenContextValue}>
              {renderedWrappedComponent}
            </ContextToUse.Provider>
          )
        }

        return renderedWrappedComponent
      }, [ContextToUse, renderedWrappedComponent, overriddenContextValue])

      return renderedChild
    }

    // If we're in "pure" mode, ensure our wrapper component only re-renders when incoming props have changed.
    const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction

    Connect.WrappedComponent = WrappedComponent
    Connect.displayName = displayName

    if (forwardRef) {
      const forwarded = React.forwardRef(function forwardConnectRef(
        props,
        ref
      ) {
        return <Connect {...props} reactReduxForwardedRef={ref} />
      })

      forwarded.displayName = displayName
      forwarded.WrappedComponent = WrappedComponent
      return hoistStatics(forwarded, WrappedComponent)
    }

    return hoistStatics(Connect, WrappedComponent)
  }
}

內(nèi)容很多很多很多,使用了hooks的語(yǔ)法筋蓖,看起來(lái)更加復(fù)雜卸耘,不過(guò)沒(méi)關(guān)系,按老規(guī)矩我們從底往上看粘咖。

可以看到最終return的是hoistStatics(Connect, WrappedComponent)蚣抗,這個(gè)方法是把WrappedComponent掛的靜態(tài)方法屬性拷貝到結(jié)果組件上,于是我們?nèi)フ?code>Connect瓮下。

往上幾行看到connect根據(jù)pure做了一層react.memo來(lái)包裹ConnectFunction翰铡,我們知道這是為了阻止props引起的不必要的re-render钝域。

再來(lái)看ConnectFunction,這是一個(gè)關(guān)鍵函數(shù)锭魔,returnrenderedChild例证,而renderedChildmemo包裹了renderedWrappedComponent, 而它接收了actualChildProps迷捧,看其定義就是我們需要的mapStateToprops返回的結(jié)果了织咧。

ok,現(xiàn)在我們知道了這個(gè)HOC的渲染邏輯漠秋,那么它是如何做到store更新就重新計(jì)算然后觸發(fā)re-render呢笙蒙?

分析一波:組件要想re-render,那必須是propsstate其一膛堤,那這里只能是state了手趣。

好家伙晌该,我們看到了useReducer肥荔,看到了forceComponentUpdateDispatch,這變量名一聽(tīng)就有戲朝群。

checkForUpdates中通過(guò)newChildProps === lastChildProps.current的比對(duì)燕耿,如果前后兩次子props相同,說(shuō)明props沒(méi)變姜胖,那就不更新誉帅,否則通過(guò)dispatch,修改state右莱,強(qiáng)行觸發(fā)組件更新蚜锨,成!

那么問(wèn)題來(lái)了,checkForUpdates是何方神圣慢蜓,它又怎么感知到store更新呢亚再?

原來(lái)我們剛一開(kāi)始漏掉了一個(gè)狠角色,useIsomorphicLayoutEffectWithArgs晨抡。這家伙是兼容ssr版本的useLayoutEffect氛悬,在組件每次更新后執(zhí)行,我們看到組件渲染進(jìn)來(lái)耘柱,然后里面通過(guò)subscription.trySubscribe進(jìn)行了訂閱以及onStatechnage綁定了checkforUpdate 如捅,所以每次store有變化這里的subscription 都會(huì)觸發(fā)checkforupdate

就這么簡(jiǎn)單5骷濉>登病!

Mobx

不得不注意到士袄,除了Redux烈涮,社區(qū)里近年來(lái)還有另一產(chǎn)品呼聲很高朴肺,那就是Mobx

它是一個(gè)功能強(qiáng)大坚洽,上手非常容易的狀態(tài)管理工具戈稿。就連Redux的作者也曾經(jīng)向大家推薦過(guò)它,在不少情況下你的確可以使用Mobx來(lái)替代掉Redux讶舰。

再次強(qiáng)調(diào)Flux鞍盗、Redux與Mobx等并不與react強(qiáng)綁定,你可以在任何框架中使用他們跳昼,所以才會(huì)有react-redux般甲,mobx-react等庫(kù)的必要性。

Mobx Data Flow

Mobx比較簡(jiǎn)單鹅颊,相信從Vue轉(zhuǎn)React的朋友應(yīng)該會(huì)很容易上手敷存,它就三個(gè)基本要點(diǎn):

創(chuàng)建可監(jiān)測(cè)的狀態(tài)

一般,我們使用observable來(lái)創(chuàng)建可被監(jiān)測(cè)的狀態(tài)堪伍,它可以是對(duì)象锚烦,數(shù)組,類等等帝雇。

import { observable } from "mobx"

class Store {
  @observable counter = 0
}

const store = new Store()

創(chuàng)建視圖響應(yīng)狀態(tài)變更

state創(chuàng)建后涮俄,如果是開(kāi)發(fā)應(yīng)用我們需要有視圖來(lái)讓感知變更,MobX會(huì)以一種最小限度的方式來(lái)更新視圖尸闸,并且它有著令人匪夷所思的高效彻亲。

以下我們以react class component為例。

import React from 'react'
import {observer} from 'mobx-react'

@observer
class Counter extends React.Component {
    render() {
        return (
            <div>
                <div>{this.props.state.counter}</div>
                <button onClick={this.props.store.add}>Add</button>
                <button onClick={this.props.store.dec}>Dec</button>
                <button onClick={() => (this.props.store.counter = 0)}>clear</button>
            </div>
        )
    }
}

export default Counter

觸發(fā)狀態(tài)變更

修改第一節(jié)中創(chuàng)建監(jiān)測(cè)狀態(tài)的代碼

import { observable, action } from "mobx"

class Store {
  @observable counter = 0
  @action add = () => {
    this.counter++
  }

  @action dec = () => {
    this.counter--
  }
}

const store = new Store()

結(jié)合上節(jié)視圖吮廉,add苞尝、dec兩算法都是通過(guò)調(diào)用store提供的方法,合情合理宦芦。

可怕的是宙址,clear直接就給state的counter賦值,居然也能成功踪旷,而且視圖是及時(shí)更新曼氛,不禁回想起flux章節(jié)中的clear,恐懼更甚令野,讓人望而退步舀患。

其實(shí)大可不必,這就是mobx的魔力气破,其實(shí)跟vue一般聊浅,它也是通過(guò)Proxy注冊(cè)監(jiān)聽(tīng),實(shí)現(xiàn)動(dòng)態(tài)及時(shí)響應(yīng)。

為了滿足React用戶對(duì)于這種狀態(tài)不可控的恐懼低匙,它也提供了api來(lái)限制這種操作旷痕,必須通過(guò)action來(lái)修改store。

enforceAction

規(guī)定只有action才能改store顽冶。

import { configure } from 'mobx'

configure({enforceAction: true})

provider

當(dāng)然欺抗,為了幫助開(kāi)發(fā)者更合理的制定目錄結(jié)構(gòu)與開(kāi)發(fā)規(guī)范,它也提供了同react-redux相似的Provider强重,后代組件使用inject绞呈,接收來(lái)自Provider注入的狀態(tài),再使用observer連接react組件和 mobx狀態(tài)间景,達(dá)到實(shí)時(shí)相應(yīng)狀態(tài)變化的效果佃声。

還有一些比如autorunreaction倘要,when computed等能力能在狀態(tài)滿足特定條件自動(dòng)被觸發(fā)圾亏,有興趣的可以自行做更多了解

老規(guī)矩封拧,通過(guò)一個(gè)Counter來(lái)看看效果志鹃。

Mobx vs Redux

通過(guò)上面簡(jiǎn)單的介紹以及demo的體驗(yàn),相信你也有了大致的感受哮缺,我們?cè)俸?jiǎn)單的比對(duì)下它與Redux弄跌。

Mobx vs Redux

無(wú)拘無(wú)束甲喝,這既是Mobx的優(yōu)點(diǎn)也是它的缺點(diǎn)尝苇,當(dāng)項(xiàng)目規(guī)模較大,涉及到多人開(kāi)發(fā)時(shí)埠胖,這種不加管束的自由將是"災(zāi)難"的開(kāi)始糠溜。

咳,點(diǎn)到即可直撤,懂的都懂非竿。

(有疏漏或偏頗的地方感謝指正!D笔:熘)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蓖乘,隨后出現(xiàn)的幾起案子锤悄,更是在濱河造成了極大的恐慌,老刑警劉巖嘉抒,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件零聚,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)隶症,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)政模,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人蚂会,你說(shuō)我怎么就攤上這事淋样。” “怎么了胁住?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵习蓬,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我措嵌,道長(zhǎng)躲叼,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任企巢,我火速辦了婚禮枫慷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘浪规。我一直安慰自己或听,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布笋婿。 她就那樣靜靜地躺著誉裆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪缸濒。 梳的紋絲不亂的頭發(fā)上足丢,一...
    開(kāi)封第一講書(shū)人閱讀 51,578評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音庇配,去河邊找鬼斩跌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛捞慌,可吹牛的內(nèi)容都是我干的耀鸦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼啸澡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼袖订!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起嗅虏,我...
    開(kāi)封第一講書(shū)人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤洛姑,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后旋恼,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體吏口,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奄容,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了产徊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昂勒。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖舟铜,靈堂內(nèi)的尸體忽然破棺而出戈盈,到底是詐尸還是另有隱情,我是刑警寧澤谆刨,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布塘娶,位于F島的核電站,受9級(jí)特大地震影響痊夭,放射性物質(zhì)發(fā)生泄漏刁岸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一她我、第九天 我趴在偏房一處隱蔽的房頂上張望虹曙。 院中可真熱鬧,春花似錦番舆、人聲如沸酝碳。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)疏哗。三九已至,卻和暖如春禾怠,著一層夾襖步出監(jiān)牢的瞬間返奉,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工刃宵, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留衡瓶,地道東北人徘公。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓牲证,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親关面。 傳聞我的和親對(duì)象是個(gè)殘疾皇子坦袍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355