成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

Vuex、Flux、Redux、Redux-saga、Dva、MobX

hiYoHoo / 3272人閱讀

摘要:也就是說不應(yīng)該有公開的,所有都應(yīng)該是私有的,只能有公開的。允許使用方法設(shè)置監(jiān)聽函數(shù),一旦發(fā)生變化,就自動執(zhí)行這個函數(shù)。用一個叫做的純函數(shù)來處理事件??梢酝ㄟ^得到當(dāng)前狀態(tài)。在中,同步的表現(xiàn)就是發(fā)出以后,立即算出。

這篇文章試著聊明白這一堆看起來挺復(fù)雜的東西。在聊之前,大家要始終記得一句話:一切前端概念,都是紙老虎。

不管是Vue,還是 React,都需要管理狀態(tài)(state),比如組件之間都有共享狀態(tài)的需要。什么是共享狀態(tài)?比如一個組件需要使用另一個組件的狀態(tài),或者一個組件需要改變另一個組件的狀態(tài),都是共享狀態(tài)。

父子組件之間,兄弟組件之間共享狀態(tài),往往需要寫很多沒有必要的代碼,比如把狀態(tài)提升到父組件里,或者給兄弟組件寫一個父組件,聽聽就覺得挺啰嗦。

如果不對狀態(tài)進行有效的管理,狀態(tài)在什么時候,由于什么原因,如何變化就會不受控制,就很難跟蹤和測試了。如果沒有經(jīng)歷過這方面的困擾,可以簡單理解為會搞得很亂就對了

在軟件開發(fā)里,有些通用的思想,比如隔離變化,約定優(yōu)于配置等,隔離變化就是說做好抽象,把一些容易變化的地方找到共性,隔離出來,不要去影響其他的代碼。約定優(yōu)于配置就是很多東西我們不一定要寫一大堆的配置,比如我們幾個人約定,view 文件夾里只能放視圖,不能放過濾器,過濾器必須放到 filter 文件夾里,那這就是一種約定,約定好之后,我們就不用寫一大堆配置文件了,我們要找所有的視圖,直接從 view 文件夾里找就行。

根據(jù)這些思想,對于狀態(tài)管理的解決思路就是:把組件之間需要共享的狀態(tài)抽取出來,遵循特定的約定,統(tǒng)一來管理,讓狀態(tài)的變化可以預(yù)測。根據(jù)這個思路,產(chǎn)生了很多的模式和庫,我們來挨個聊聊。

Store 模式

最簡單的處理就是把狀態(tài)存到一個外部變量里面,比如:this.$root.$data,當(dāng)然也可以是一個全局變量。但是這樣有一個問題,就是數(shù)據(jù)改變后,不會留下變更過的記錄,這樣不利于調(diào)試。

所以我們稍微搞得復(fù)雜一點,用一個簡單的 Store 模式:

var store = {
  state: {
    message: "Hello!"
  },
  setMessageAction (newValue) {
    // 發(fā)生改變記錄點日志啥的
    this.state.message = newValue
  },
  clearMessageAction () {
    this.state.message = ""
  }
}

store 的 state 來存數(shù)據(jù),store 里面有一堆的 action,這些 action 來控制 state 的改變,也就是不直接去對 state 做改變,而是通過 action 來改變,因為都走 action,我們就可以知道到底改變(mutation)是如何被觸發(fā)的,出現(xiàn)錯誤,也可以記錄記錄日志啥的。

不過這里沒有限制組件里面不能修改 store 里面的 state,萬一組件瞎胡修改,不通過 action,那我們也沒法跟蹤這些修改是怎么發(fā)生的。所以就需要規(guī)定一下,組件不允許直接修改屬于 store 實例的 state,組件必須通過 action 來改變 state,也就是說,組件里面應(yīng)該執(zhí)行 action 來分發(fā) (dispatch) 事件通知 store 去改變。這樣約定的好處是,我們能夠記錄所有 store 中發(fā)生的 state 改變,同時實現(xiàn)能做到記錄變更 (mutation)、保存狀態(tài)快照、歷史回滾/時光旅行的先進的調(diào)試工具。

這樣進化了一下,一個簡單的 Flux 架構(gòu)就實現(xiàn)了。

Flux

Flux其實是一種思想,就像MVC,MVVM之類的,他給出了一些基本概念,所有的框架都可以根據(jù)他的思想來做一些實現(xiàn)。

Flux把一個應(yīng)用分成了4個部分:

View

Action

Dispatcher

Store

比如我們搞一個應(yīng)用,顯而易見,這個應(yīng)用里面會有一堆的 View,這個 View 可以是Vue的,也可以是 React的,啥框架都行,啥技術(shù)都行。

View 肯定是要展示數(shù)據(jù)的,所謂的數(shù)據(jù),就是 Store,Store 很容易明白,就是存數(shù)據(jù)的地方。當(dāng)然我們可以把 Store 都放到一起,也可以分開來放,所以就有一堆的 Store。但是這些 View 都有一個特點,就是 Store 變了得跟著變。

View 怎么跟著變呢?一般 Store 一旦發(fā)生改變,都會往外面發(fā)送一個事件,比如 change,通知所有的訂閱者。View 通過訂閱也好,監(jiān)聽也好,不同的框架有不同的技術(shù),反正 Store 變了,View 就會變。

View 不是光用來看的,一般都會有用戶操作,用戶點個按鈕,改個表單啥的,就需要修改 Store。Flux 要求,View 要想修改 Store,必須經(jīng)過一套流程,有點像我們剛才 Store 模式里面說的那樣。視圖先要告訴 Dispatcher,讓 Dispatcher dispatch 一個 action,Dispatcher 就像是個中轉(zhuǎn)站,收到 View 發(fā)出的 action,然后轉(zhuǎn)發(fā)給 Store。比如新建一個用戶,View 會發(fā)出一個叫 addUser 的 action 通過 Dispatcher 來轉(zhuǎn)發(fā),Dispatcher 會把 addUser 這個 action 發(fā)給所有的 store,store 就會觸發(fā) addUser 這個 action,來更新數(shù)據(jù)。數(shù)據(jù)一更新,那么 View 也就跟著更新了。

這個過程有幾個需要注意的點:

Dispatcher 的作用是接收所有的 Action,然后發(fā)給所有的 Store。這里的 Action 可能是 View 觸發(fā)的,也有可能是其他地方觸發(fā)的,比如測試用例。轉(zhuǎn)發(fā)的話也不是轉(zhuǎn)發(fā)給某個 Store,而是所有 Store。

Store 的改變只能通過 Action,不能通過其他方式。也就是說 Store 不應(yīng)該有公開的 Setter,所有 Setter 都應(yīng)該是私有的,只能有公開的 Getter。具體 Action 的處理邏輯一般放在 Store 里。

聽聽描述看看圖,可以發(fā)現(xiàn),F(xiàn)lux的最大特點就是數(shù)據(jù)都是單向流動的。

Redux

Flux 有一些缺點(特點),比如一個應(yīng)用可以擁有多個 Store,多個Store之間可能有依賴關(guān)系;Store 封裝了數(shù)據(jù)還有處理數(shù)據(jù)的邏輯。

所以大家在使用的時候,一般會用 Redux,他和 Flux 思想比較類似,也有差別。

Store

Redux 里面只有一個 Store,整個應(yīng)用的數(shù)據(jù)都在這個大 Store 里面。Store 的 State 不能直接修改,每次只能返回一個新的 State。Redux 整了一個 createStore 函數(shù)來生成 Store。

import { createStore } from "redux";
const store = createStore(fn);

Store 允許使用 ?store.subscribe ?方法設(shè)置監(jiān)聽函數(shù),一旦 State 發(fā)生變化,就自動執(zhí)行這個函數(shù)。這樣不管 View 是用什么實現(xiàn)的,只要把 View 的更新函數(shù) subscribe 一下,就可以實現(xiàn) State 變化之后,View 自動渲染了。比如在 React 里,把組件的render方法或setState方法訂閱進去就行。

Action

和 Flux ?一樣,Redux 里面也有 Action,Action 就是 View 發(fā)出的通知,告訴 Store State 要改變。Action 必須有一個 type 屬性,代表 Action 的名稱,其他可以設(shè)置一堆屬性,作為參數(shù)供 State 變更時參考。

const action = {
  type: "ADD_TODO",
  payload: "Learn Redux"
};

Redux 可以用 Action Creator 批量來生成一些 Action。

Reducer

Redux 沒有 Dispatcher 的概念,Store 里面已經(jīng)集成了 dispatch 方法。store.dispatch()是 View 發(fā)出 Action 的唯一方法。

import { createStore } from "redux";
const store = createStore(fn);

store.dispatch({
  type: "ADD_TODO",
  payload: "Learn Redux"
});

Redux 用一個叫做 Reducer 的純函數(shù)來處理事件。Store 收到 Action 以后,必須給出一個新的 State(就是剛才說的Store 的 State 不能直接修改,每次只能返回一個新的 State),這樣 View 才會發(fā)生變化。這種 State 的計算過程就叫做 Reducer。

什么是純函數(shù)呢,就是說沒有任何的副作用,比如這樣一個函數(shù):

function getAge(user) {
  user.age = user.age + 1;
  return user.age;
}

這個函數(shù)就有副作用,每一次相同的輸入,都可能導(dǎo)致不同的輸出,而且還會影響輸入 user 的值,再比如:

let b = 10;
function compare(a) {
  return a >= b;
}

這個函數(shù)也有副作用,就是依賴外部的環(huán)境,b 在別處被改變了,返回值對于相同的 a 就有可能不一樣。

而 Reducer 是一個純函數(shù),對于相同的輸入,永遠都只會有相同的輸出,不會影響外部的變量,也不會被外部變量影響,不得改寫參數(shù)。它的作用大概就是這樣,根據(jù)應(yīng)用的狀態(tài)和當(dāng)前的 action 推導(dǎo)出新的 state:

(previousState, action) => newState

類比 Flux,F(xiàn)lux 有些像:

 (state, action) => state

為什么叫做 Reducer 呢?reduce ?是一個函數(shù)式編程的概念,經(jīng)常和 ?map ?放在一起說,簡單來說,map ?就是映射,reduce ?就是歸納。映射就是把一個列表按照一定規(guī)則映射成另一個列表,而 reduce 是把一個列表通過一定規(guī)則進行合并,也可以理解為對初始值進行一系列的操作,返回一個新的值。

比如 ?Array 就有一個方法叫 reduce,Array.prototype.reduce(reducer, ?initialValue),把 Array 整吧整吧弄成一個 ?newValue。

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15

看起來和 Redux 的 Reducer 是不是好像好像,Redux 的 Reducer 就是 reduce 一個列表(action的列表)和一個 initialValue(初始的 ?State)到一個新的 value(新的 ?State)。

把上面的概念連起來,舉個例子:

下面的代碼聲明了 reducer:

const defaultState = 0;
const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case "ADD":
      return state + action.payload;
    default: 
      return state;
  }
};

createStore接受 Reducer 作為參數(shù),生成一個新的 Store。以后每當(dāng)store.dispatch發(fā)送過來一個新的 Action,就會自動調(diào)用 Reducer,得到新的 State。

import { createStore } from "redux";
const store = createStore(reducer);

createStore?內(nèi)部干了什么事兒呢?通過一個簡單的 createStore 的實現(xiàn),可以了解大概的原理(可以略過不看):

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 };
};

Redux 有很多的 Reducer,對于大型應(yīng)用來說,State 必然十分龐大,導(dǎo)致 Reducer 函數(shù)也十分龐大,所以需要做拆分。Redux ?里每一個 Reducer 負責(zé)維護 State 樹里面的一部分數(shù)據(jù),多個 Reducer 可以通過 combineReducers 方法合成一個根 Reducer,這個根 Reducer 負責(zé)維護整個 State。

import { combineReducers } from "redux";

// 注意這種簡寫形式,State 的屬性名必須與子 Reducer 同名
const chatReducer = combineReducers({
  Reducer1,
  Reducer2,
  Reducer3
})

combineReducers 干了什么事兒呢?通過簡單的 combineReducers 的實現(xiàn),可以了解大概的原理(可以略過不看):

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

再回顧一下剛才的流程圖,嘗試走一遍 ?Redux ?流程:

1、用戶通過 View 發(fā)出 Action:

store.dispatch(action);

2、然后 Store 自動調(diào)用 Reducer,并且傳入兩個參數(shù):當(dāng)前 State 和收到的 Action。 Reducer 會返回新的 State 。

let nextState = xxxReducer(previousState, action);

3、State 一旦有變化,Store 就會調(diào)用監(jiān)聽函數(shù)。

store.subscribe(listener);

4、listener可以通過 ?store.getState() ?得到當(dāng)前狀態(tài)。如果使用的是 React,這時可以觸發(fā)重新渲染 View。

function listerner() {
  let newState = store.getState();
  component.setState(newState);   
}
對比 Flux

和 ?Flux ?比較一下:Flux 中 Store 是各自為戰(zhàn)的,每個 Store 只對對應(yīng)的 View 負責(zé),每次更新都只通知對應(yīng)的View:

Redux 中各子 Reducer 都是由根 Reducer 統(tǒng)一管理的,每個子 Reducer 的變化都要經(jīng)過根 Reducer 的整合:

簡單來說,Redux有三大原則:

單一數(shù)據(jù)源:Flux 的數(shù)據(jù)源可以是多個。

State 是只讀的:Flux 的 State 可以隨便改。

使用純函數(shù)來執(zhí)行修改:Flux 執(zhí)行修改的不一定是純函數(shù)。

Redux 和 Flux 一樣都是單向數(shù)據(jù)流。

中間件

剛才說到的都是比較理想的同步狀態(tài)。在實際項目中,一般都會有同步和異步操作,所以 Flux、Redux 之類的思想,最終都要落地到同步異步的處理中來。

在 ?Redux ?中,同步的表現(xiàn)就是:Action 發(fā)出以后,Reducer 立即算出 State。那么異步的表現(xiàn)就是:Action 發(fā)出以后,過一段時間再執(zhí)行 Reducer。

那怎么才能 Reducer 在異步操作結(jié)束后自動執(zhí)行呢?Redux 引入了中間件 Middleware 的概念。

其實我們重新回顧一下剛才的流程,可以發(fā)現(xiàn)每一個步驟都很純粹,都不太適合加入異步的操作,比如 Reducer,純函數(shù),肯定不能承擔(dān)異步操作,那樣會被外部IO干擾。Action呢,就是一個純對象,放不了操作。那想來想去,只能在 View 里發(fā)送 Action 的時候,加上一些異步操作了。比如下面的代碼,給原來的 ?dispatch ?方法包裹了一層,加上了一些日志打印的功能:

let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
  console.log("dispatching", action);
  next(action);
  console.log("next state", store.getState());
}

既然能加日志打印,當(dāng)然也能加入異步操作。所以中間件簡單來說,就是對 store.dispatch 方法進行一些改造的函數(shù)。不展開說了,所以如果想詳細了解中間件,可以點這里。

Redux 提供了一個 applyMiddleware 方法來應(yīng)用中間件:

const store = createStore(
  reducer,
  applyMiddleware(thunk, promise, logger)
);

這個方法主要就是把所有的中間件組成一個數(shù)組,依次執(zhí)行。也就是說,任何被發(fā)送到 store 的 action 現(xiàn)在都會經(jīng)過thunk,promise,logger 這幾個中間件了。

處理異步

對于異步操作來說,有兩個非常關(guān)鍵的時刻:發(fā)起請求的時刻,和接收到響應(yīng)的時刻(可能成功,也可能失敗或者超時),這兩個時刻都可能會更改應(yīng)用的 state。一般是這樣一個過程:

請求開始時,dispatch ?一個請求開始 Action,觸發(fā) State 更新為“正在請求”狀態(tài),View 重新渲染,比如展現(xiàn)個Loading啥的。

請求結(jié)束后,如果成功,dispatch ?一個請求成功 Action,隱藏掉 ?Loading,把新的數(shù)據(jù)更新到 ?State;如果失敗,dispatch ?一個請求失敗 Action,隱藏掉 ?Loading,給個失敗提示。

顯然,用 ?Redux ?處理異步,可以自己寫中間件來處理,當(dāng)然大多數(shù)人會選擇一些現(xiàn)成的支持異步處理的中間件。比如 redux-thunk 或?redux-promise?。

Redux-thunk

thunk 比較簡單,沒有做太多的封裝,把大部分自主權(quán)交給了用戶:

const createFetchDataAction = function(id) {
    return function(dispatch, getState) {
        // 開始請求,dispatch 一個 FETCH_DATA_START action
        dispatch({
            type: FETCH_DATA_START, 
            payload: id
        })
        api.fetchData(id) 
            .then(response => {
         ? ? ? ?// 請求成功,dispatch 一個 FETCH_DATA_SUCCESS action
                dispatch({
                    type: FETCH_DATA_SUCCESS,
                    payload: response
                })
            })
            .catch(error => {
 ? ? ? ? ? ? ? ?// 請求失敗,dispatch 一個 FETCH_DATA_FAILED action ? 
                dispatch({
                    type: FETCH_DATA_FAILED,
                    payload: error
                })
            }) 
    }
}

//reducer
const reducer = function(oldState, action) {
    switch(action.type) {
    case FETCH_DATA_START : 
        // 處理 loading 等
    case FETCH_DATA_SUCCESS : 
        // 更新 store 等
    case FETCH_DATA_FAILED : 
        // 提示異常
    }
}

缺點就是用戶要寫的代碼有點多,可以看到上面的代碼比較啰嗦,一個請求就要搞這么一套東西。

Redux-promise

redus-promise 和 redux-thunk 的思想類似,只不過做了一些簡化,成功失敗手動 dispatch 被封裝成自動了:

const FETCH_DATA = "FETCH_DATA"
//action creator
const getData = function(id) {
    return {
        type: FETCH_DATA,
        payload: api.fetchData(id) // 直接將 promise 作為 payload
    }
}
//reducer
const reducer = function(oldState, action) {
    switch(action.type) {
    case FETCH_DATA: 
        if (action.status === "success") {
             // 更新 store 等處理
        } else {
                // 提示異常
        }
    }
}

剛才的什么 then、catch 之類的被中間件自行處理了,代碼簡單不少,不過要處理 Loading 啥的,還需要寫額外的代碼。

其實任何時候都是這樣:封裝少,自由度高,但是代碼就會變復(fù)雜;封裝多,代碼變簡單了,但是自由度就會變差。redux-thunk 和 redux-promise 剛好就是代表這兩個面。

redux-thunk 和 redux-promise ?的具體使用就不介紹了,這里只聊一下大概的思路。大部分簡單的異步業(yè)務(wù)場景,redux-thunk 或者 redux-promise 都可以滿足了。

上面說的 Flux 和 Redux,和具體的前端框架沒有什么關(guān)系,只是思想和約定層面。下面就要和我們常用的 Vue 或 React 結(jié)合起來了:

Vuex

Vuex 主要用于 Vue,和 Flux,Redux 的思想很類似。

Store

每一個 Vuex 里面有一個全局的 Store,包含著應(yīng)用中的狀態(tài) State,這個 State 只是需要在組件中共享的數(shù)據(jù),不用放所有的 State,沒必要。這個 State 是單一的,和 Redux 類似,所以,一個應(yīng)用僅會包含一個 Store 實例。單一狀態(tài)樹的好處是能夠直接地定位任一特定的狀態(tài)片段,在調(diào)試的過程中也能輕易地取得整個當(dāng)前應(yīng)用狀態(tài)的快照。

Vuex通過 store 選項,把 state 注入到了整個應(yīng)用中,這樣子組件能通過 this.$store 訪問到 state 了。

const app = new Vue({
  el: "#app",
  // 把 store 對象提供給 “store” 選項,這可以把 store 的實例注入所有的子組件
  store,
  components: { Counter },
  template: `
    
` })
const Counter = {
  template: `
{{ count }}
`, computed: { count () { return this.$store.state.count } } }

State 改變,View 就會跟著改變,這個改變利用的是 Vue 的響應(yīng)式機制。

Mutation

顯而易見,State 不能直接改,需要通過一個約定的方式,這個方式在 Vuex 里面叫做 mutation,更改 Vuex 的 store 中的狀態(tài)的唯一方法是提交 mutation。Vuex 中的 mutation 非常類似于事件:每個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調(diào)函數(shù) (handler)。

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 變更狀態(tài)
      state.count++
    }
  }
})

觸發(fā) mutation 事件的方式不是直接調(diào)用,比如 increment(state) ?是不行的,而要通過 store.commit 方法:

store.commit("increment")

注意:mutation 都是同步事務(wù)。

mutation 有些類似 Redux 的 Reducer,但是 Vuex 不要求每次都搞一個新的 State,可以直接修改 State,這塊兒又和 Flux 有些類似。具尤大的說法,Redux 強制的 immutability,在保證了每一次狀態(tài)變化都能追蹤的情況下強制的 immutability 帶來的收益很有限,為了同構(gòu)而設(shè)計的 API 很繁瑣,必須依賴第三方庫才能相對高效率地獲得狀態(tài)樹的局部狀態(tài),這些都是 Redux 不足的地方,所以也被 Vuex 舍掉了。

到這里,其實可以感覺到 Flux、Redux、Vuex 三個的思想都差不多,在具體細節(jié)上有一些差異,總的來說都是讓 View 通過某種方式觸發(fā) Store 的事件或方法,Store 的事件或方法對 State 進行修改或返回一個新的 State,State 改變之后,View 發(fā)生響應(yīng)式改變。

Action

到這里又該處理異步這塊兒了。mutation 是必須同步的,這個很好理解,和之前的 ?reducer 類似,不同步修改的話,會很難調(diào)試,不知道改變什么時候發(fā)生,也很難確定先后順序,A、B兩個 mutation,調(diào)用順序可能是 A -> B,但是最終改變 State 的結(jié)果可能是 B -> A。

對比Redux的中間件,Vuex 加入了 Action 這個東西來處理異步,Vuex的想法是把同步和異步拆分開,異步操作想咋搞咋搞,但是不要干擾了同步操作。View 通過 store.dispatch("increment") 來觸發(fā)某個 Action,Action 里面不管執(zhí)行多少異步操作,完事之后都通過 store.commit("increment") 來觸發(fā) mutation,一個 Action 里面可以觸發(fā)多個 mutation。所以 Vuex 的Action 類似于一個靈活好用的中間件。

Vuex 把同步和異步操作通過 mutation 和 Action 來分開處理,是一種方式。但不代表是唯一的方式,還有很多方式,比如就不用 Action,而是在應(yīng)用內(nèi)部調(diào)用異步請求,請求完畢直接 commit mutation,當(dāng)然也可以。

Vuex 還引入了 Getter,這個可有可無,只不過是方便計算屬性的復(fù)用。

Vuex 單一狀態(tài)樹并不影響模塊化,把 State 拆了,最后組合在一起就行。Vuex 引入了 Module 的概念,每個 Module 有自己的 state、mutation、action、getter,其實就是把一個大的 Store 拆開。

總的來看,Vuex 的方式比較清晰,適合 Vue 的思想,在實際開發(fā)中也比較方便。

對比Redux

Redux:
view——>actions——>reducer——>state變化——>view變化(同步異步一樣)

Vuex:
view——>commit——>mutations——>state變化——>view變化(同步操作)
view——>dispatch——>actions——>mutations——>state變化——>view變化(異步操作)

React-redux

Redux 和 Flux 類似,只是一種思想或者規(guī)范,它和 React 之間沒有關(guān)系。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。

但是因為 React 包含函數(shù)式的思想,也是單向數(shù)據(jù)流,和 Redux 很搭,所以一般都用 ?Redux 來進行狀態(tài)管理。為了簡單處理 ?Redux ?和 React ?UI ?的綁定,一般通過一個叫 react-redux 的庫和 React 配合使用,這個是 ?react ?官方出的(如果不用 react-redux,那么手動處理 Redux 和 UI 的綁定,需要寫很多重復(fù)的代碼,很容易出錯,而且有很多 UI 渲染邏輯的優(yōu)化不一定能處理好)。

Redux將React組件分為容器型組件和展示型組件,容器型組件一般通過connect函數(shù)生成,它訂閱了全局狀態(tài)的變化,通過mapStateToProps函數(shù),可以對全局狀態(tài)進行過濾,而展示型組件不直接從global state獲取數(shù)據(jù),其數(shù)據(jù)來源于父組件。

如果一個組件既需要UI呈現(xiàn),又需要業(yè)務(wù)邏輯處理,那就得拆,拆成一個容器組件包著一個展示組件。

因為 react-redux 只是 redux 和 react 結(jié)合的一種實現(xiàn),除了剛才說的組件拆分,并沒有什么新奇的東西,所以只拿一個簡單TODO項目的部分代碼來舉例:

入口文件 index.js,把 redux 的相關(guān) store、reducer 通過 Provider 注冊到 App 里面,這樣子組件就可以拿到 ?store ?了。

import React from "react"
import { render } from "react-dom"
import { Provider } from "react-redux"
import { createStore } from "redux"
import rootReducer from "./reducers"
import App from "./components/App"

const store = createStore(rootReducer)

render(
  
    
  ,
  document.getElementById("root")
)

actions/index.js,創(chuàng)建 Action:

let nextTodoId = 0
export const addTodo = text => ({
  type: "ADD_TODO",
  id: nextTodoId++,
  text
})

export const setVisibilityFilter = filter => ({
  type: "SET_VISIBILITY_FILTER",
  filter
})

export const toggleTodo = id => ({
  type: "TOGGLE_TODO",
  id
})

export const VisibilityFilters = {
  SHOW_ALL: "SHOW_ALL",
  SHOW_COMPLETED: "SHOW_COMPLETED",
  SHOW_ACTIVE: "SHOW_ACTIVE"
}

reducers/todos.js,創(chuàng)建 Reducers:

const todos = (state = [], action) => {
  switch (action.type) {
    case "ADD_TODO":
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ]
    case "TOGGLE_TODO":
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      )
    default:
      return state
  }
}

export default todos

reducers/index.js,把所有的 Reducers 綁定到一起:

import { combineReducers } from "redux"
import todos from "./todos"
import visibilityFilter from "./visibilityFilter"

export default combineReducers({
  todos,
  visibilityFilter,
  ...
})

containers/VisibleTodoList.js,容器組件,connect 負責(zé)連接React組件和Redux Store:

import { connect } from "react-redux"
import { toggleTodo } from "../actions"
import TodoList from "../components/TodoList"

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case "SHOW_COMPLETED":
      return todos.filter(t => t.completed)
    case "SHOW_ACTIVE":
      return todos.filter(t => !t.completed)
    case "SHOW_ALL":
    default:
      return todos
  }
}

// mapStateToProps 函數(shù)指定如何把當(dāng)前 Redux store state 映射到展示組件的 props 中
const mapStateToProps = state => ({
  todos: getVisibleTodos(state.todos, state.visibilityFilter)
})

// mapDispatchToProps 方法接收 dispatch() 方法并返回期望注入到展示組件的 props 中的回調(diào)方法。
const mapDispatchToProps = dispatch => ({
  toggleTodo: id => dispatch(toggleTodo(id))
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

簡單來說,react-redux 就是多了個 connect 方法連接容器組件和UI組件,這里的“連接”就是一種映射:

mapStateToProps ?把容器組件的 state 映射到UI組件的 props

mapDispatchToProps 把UI組件的事件映射到 dispatch 方法

Redux-saga

剛才介紹了兩個Redux 處理異步的中間件 redux-thunk 和 redux-promise,當(dāng)然 redux 的異步中間件還有很多,他們可以處理大部分場景,這些中間件的思想基本上都是把異步請求部分放在了 ?action ?creator ?中,理解起來比較簡單。

redux-saga 采用了另外一種思路,它沒有把異步操作放在 action creator 中,也沒有去處理 reductor,而是把所有的異步操作看成“線程”,可以通過普通的action去觸發(fā)它,當(dāng)操作完成時也會觸發(fā)action作為輸出。saga 的意思本來就是一連串的事件。

redux-saga 把異步獲取數(shù)據(jù)這類的操作都叫做副作用(Side ?Effect),它的目標就是把這些副作用管理好,讓他們執(zhí)行更高效,測試更簡單,在處理故障時更容易。

在聊 redux-saga 之前,需要熟悉一些預(yù)備知識,那就是 ES6 的 Generator。

如果從沒接觸過 Generator 的話,看著下面的代碼,給你個1分鐘傻瓜式速成,函數(shù)加個星號就是 Generator?函數(shù)了,Generator 就是個罵街生成器,Generator 函數(shù)里可以寫一堆 yield 關(guān)鍵字,可以記成“丫的”,Generator 函數(shù)執(zhí)行的時候,啥都不干,就等著調(diào)用 next 方法,按照順序把標記為“丫的”的地方一個一個拎出來罵(遍歷執(zhí)行),罵到最后沒有“丫的”標記了,就返回最后的return值,然后標記為 done: true,也就是罵完了(上面只是幫助初學(xué)者記憶,別噴~)。

function* helloWorldGenerator() {
  yield "hello";
  yield "world";
  return "ending";
}

var hw = helloWorldGenerator();

hw.next() // 先把 "hello" 拎出來,done: false 代表還沒罵完
// { value: "hello", done: false } next() 方法有固定的格式,value 是返回值,done 代表是否遍歷結(jié)束

hw.next() // 再把 "world" 拎出來,done: false 代表還沒罵完
// { value: "world", done: false }

hw.next() // 沒有 yield 了,就把最后的 return "ending" 拎出來,done: true 代表罵完了
// { value: "ending", done: true }

hw.next() // 沒有 yield,也沒有 return 了,真的罵完了,只能擠出來一個 undefined 了,done: true 代表罵完了
// { value: undefined, done: true }

這樣搞有啥好處呢?我們發(fā)現(xiàn) Generator?函數(shù)的很多代碼可以被延緩執(zhí)行,也就是具備了暫停和記憶的功能:遇到y(tǒng)ield表達式,就暫停執(zhí)行后面的操作,并將緊跟在yield后面的那個表達式的值,作為返回的對象的value屬性值,等著下一次調(diào)用next方法時,再繼續(xù)往下執(zhí)行。用 Generator 來寫異步代碼,大概長這樣:

function* gen(){
  var url = "https://api.github.com/users/github";
  var jsonData = yield fetch(url);
  console.log(jsonData);
}

var g = gen();
var result = g.next(); 
// 這里的result是 { value: fetch("https://api.github.com/users/github"), done: true }

// fetch(url) 是一個 Promise,所以需要 then 來執(zhí)行下一步
result.value.then(function(data){
  return data.json();
}).then(function(data){
  // 獲取到 json data,然后作為參數(shù)調(diào)用 next,相當(dāng)于把 data 傳給了 jsonData,然后執(zhí)行 console.log(jsonData);
  g.next(data);
});

再回到 redux-saga 來,可以把 saga 想象成開了一個以最快速度不斷地調(diào)用 next 方法并嘗試獲取所有 yield 表達式值的線程。舉個例子:

// saga.js
import { take, put } from "redux-saga/effects"

function* mySaga(){ 
    // 阻塞: take方法就是等待 USER_INTERACTED_WITH_UI_ACTION 這個 action 執(zhí)行
    yield take(USER_INTERACTED_WITH_UI_ACTION);
    // 阻塞: put方法將同步發(fā)起一個 action
    yield put(SHOW_LOADING_ACTION, {isLoading: true});
    // 阻塞: 將等待 FetchFn 結(jié)束,等待返回的 Promise
    const data = yield call(FetchFn, "https://my.server.com/getdata");
    // 阻塞: 將同步發(fā)起 action (使用剛才返回的 Promise.then)
    yield put(SHOW_DATA_ACTION, {data: data});
}

這里用了好幾個yield,簡單理解,也就是每個 yield 都發(fā)起了阻塞,saga 會等待執(zhí)行結(jié)果返回,再執(zhí)行下一指令。也就是相當(dāng)于take、put、call、put 這幾個方法的調(diào)用變成了同步的,上面的全部完成返回了,才會執(zhí)行下面的,類似于 await。

用了 saga,我們就可以很細粒度的控制各個副作用每一部的操作,可以把異步操作和同步發(fā)起 action 一起,隨便的排列組合。saga 還提供 takeEvery、takeLatest 之類的輔助函數(shù),來控制是否允許多個異步請求同時執(zhí)行,尤其是 takeLatest,方便處理由于網(wǎng)絡(luò)延遲造成的多次請求數(shù)據(jù)沖突或混亂的問題。

saga 看起來很復(fù)雜,主要原因可能是因為大家不熟悉 Generator 的語法,還有需要學(xué)習(xí)一堆新增的 API 。如果拋開這些記憶的東西,改造一下,再來看一下代碼:

function mySaga(){ 
    if (action.type === "USER_INTERACTED_WITH_UI_ACTION") {
        store.dispatch({ type: "SHOW_LOADING_ACTION", isLoading: true});
        const data = await Fetch("https://my.server.com/getdata");
        store.dispatch({ type: "SHOW_DATA_ACTION", data: data});
 ? ?}
}

上面的代碼就很清晰了吧,全部都是同步的寫法,無比順暢,當(dāng)然直接這樣寫是不支持的,所以那些 Generator 語法和API,無非就是做一些適配而已。

saga 還能很方便的并行執(zhí)行異步任務(wù),或者讓兩個異步任務(wù)競爭:

// 并行執(zhí)行,并等待所有的結(jié)果,類似 Promise.all 的行為
const [users, repos] = yield [
  call(fetch, "/users"),
  call(fetch, "/repos")
]

// 并行執(zhí)行,哪個先完成返回哪個,剩下的就取消掉了
const {posts, timeout} = yield race({
 ?posts: call(fetchApi, "/posts"),
 ?timeout: call(delay, 1000)
})

saga 的每一步都可以做一些斷言(assert)之類的,所以非常方便測試。而且很容易測試到不同的分支。

這里不討論更多 saga 的細節(jié),大家了解 saga 的思想就行,細節(jié)請看文檔。

對比 Redux-thunk

比較一下 redux-thunk 和 redux-saga 的代碼:

和 redux-thunk 等其他異步中間件對比來說,redux-saga 主要有下面幾個特點:

異步數(shù)據(jù)獲取的相關(guān)業(yè)務(wù)邏輯放在了多帶帶的 saga.js 中,不再是摻雜在 action.js 或 component.js 中。

dispatch 的參數(shù)是標準的 ?action,沒有魔法。

saga 代碼采用類似同步的方式書寫,代碼變得更易讀。

代碼異常/請求失敗 都可以直接通過 try/catch 語法直接捕獲處理。

很容易測試,如果是 thunk 的 Promise,測試的話就需要不停的 mock 不同的數(shù)據(jù)。

其實 redux-saga 是用一些學(xué)習(xí)的復(fù)雜度,換來了代碼的高可維護性,還是很值得在項目中使用的。

Dva

Dva是什么呢?官方的定義是:dva 首先是一個基于 redux 和 redux-saga 的數(shù)據(jù)流方案,然后為了簡化開發(fā)體驗,dva 還額外內(nèi)置了 react-router 和 fetch,所以也可以理解為一個輕量級的應(yīng)用框架。

簡單理解,就是讓使用 react-redux 和 redux-saga 編寫的代碼組織起來更合理,維護起來更方便。

之前我們聊了 redux、react-redux、redux-saga 之類的概念,大家肯定覺得頭昏腦漲的,什么 action、reducer、saga 之類的,寫一個功能要在這些js文件里面不停的切換。

dva 做的事情很簡單,就是讓這些東西可以寫到一起,不用分開來寫了。比如:

app.model({
  // namespace - 對應(yīng) reducer 在 combine 到 rootReducer 時的 key 值
  namespace: "products",
  // state - 對應(yīng) reducer 的 initialState
  state: {
    list: [],
    loading: false,
  },
  // subscription - 在 dom ready 后執(zhí)行
  subscriptions: [
    function(dispatch) {
      dispatch({type: "products/query"});
    },
  ],
  // effects - 對應(yīng) saga,并簡化了使用
  effects: {
    ["products/query"]: function*() {
      yield call(delay(800));
      yield put({
        type: "products/query/success",
        payload: ["ant-tool", "roof"],
      });
    },
  },
  // reducers - 就是傳統(tǒng)的 reducers
  reducers: {
    ["products/query"](state) {
      return { ...state, loading: true, };
    },
    ["products/query/success"](state, { payload }) {
      return { ...state, loading: false, list: payload };
    },
  },
});

以前書寫的方式是創(chuàng)建 ?sagas/products.js, reducers/products.js 和 actions/products.js,然后把 saga、action、reducer 啥的分開來寫,來回切換,現(xiàn)在寫在一起就方便多了。

比如傳統(tǒng)的 TODO 應(yīng)用,用 redux + redux-saga 來表示結(jié)構(gòu),就是這樣:

saga 攔截 add 這個 action, 發(fā)起 http 請求, 如果請求成功, 則繼續(xù)向 reducer 發(fā)一個 addTodoSuccess 的 action, 提示創(chuàng)建成功, 反之則發(fā)送 addTodoFail 的 action 即可。

如果使用 Dva,那么結(jié)構(gòu)圖如下:

整個結(jié)構(gòu)變化不大,最主要的就是把 store 及 saga 統(tǒng)一為一個 model 的概念(有點類似 Vuex 的 Module),寫在了一個 js 文件里。增加了一個 Subscriptions, 用于收集其他來源的 action,比如快捷鍵操作。

app.model({
  namespace: "count",
  state: {
    record: 0,
    current: 0,
  },
  reducers: {
    add(state) {
      const newCurrent = state.current + 1;
      return { ...state,
        record: newCurrent > state.record ? newCurrent : state.record,
        current: newCurrent,
      };
    },
    minus(state) {
      return { ...state, current: state.current - 1};
    },
  },
  effects: {
    *add(action, { call, put }) {
      yield call(delay, 1000);
      yield put({ type: "minus" });
    },
  },
  subscriptions: {
    keyboardWatcher({ dispatch }) {
      key("?+up, ctrl+up", () => { dispatch({type:"add"}) });
    },
  },
});

之前我們說過約定優(yōu)于配置的思想,Dva正式借鑒了這個思想。

MobX

前面扯了這么多,其實還都是 Flux 體系的,都是單向數(shù)據(jù)流方案。接下來要說的 MobX,就和他們不太一樣了。

我們先清空一下大腦,回到初心,什么是初心?就是我們最初要解決的問題是什么?最初我們其實為了解決應(yīng)用狀態(tài)管理的問題,不管是 Redux 還是 MobX,把狀態(tài)管理好是前提。什么叫把狀態(tài)管理好,簡單來說就是:統(tǒng)一維護公共的應(yīng)用狀態(tài),以統(tǒng)一并且可控的方式更新狀態(tài),狀態(tài)更新后,View跟著更新。不管是什么思想,達成這個目標就ok。

Flux 體系的狀態(tài)管理方式,只是一個選項,但并不代表是唯一的選項。MobX 就是另一個選項。

MobX背后的哲學(xué)很簡單:任何源自應(yīng)用狀態(tài)的東西都應(yīng)該自動地獲得。譯成人話就是狀態(tài)只要一變,其他用到狀態(tài)的地方就都跟著自動變。

看這篇文章的人,大概率會對面向?qū)ο蟮乃枷氡容^熟悉,而對函數(shù)式編程的思想略陌生。Flux 或者說 Redux 的思想主要就是函數(shù)式編程(FP)的思想,所以學(xué)習(xí)起來會覺得累一些。而 MobX 更接近于面向?qū)ο缶幊蹋?state 包裝成可觀察的對象,這個對象會驅(qū)動各種改變。什么是可觀察?就是 MobX 老大哥在看著 state 呢。state 只要一改變,所有用到它的地方就都跟著改變了。這樣整個 View 可以被 state 來驅(qū)動。

const obj = observable({
    a: 1,
    b: 2
})

autoRun(() => {
    console.log(obj.a)
})

obj.b = 3 // 什么都沒有發(fā)生
obj.a = 2 // observe 函數(shù)的回調(diào)觸發(fā)了,控制臺輸出:2

上面的obj,他的 obj.a 屬性被使用了,那么只要 obj.a 屬性一變,所有使用的地方都會被調(diào)用。autoRun 就是這個老大哥,他看著所有依賴 obj.a 的地方,也就是收集所有對 obj.a 的依賴。當(dāng) obj.a 改變時,老大哥就會觸發(fā)所有依賴去更新。

MobX 允許有多個 store,而且這些 store 里的 state 可以直接修改,不用像 Redux 那樣每次還返回個新的。這個有點像 Vuex,自由度更高,寫的代碼更少。不過它也會讓代碼不好維護。

MobX 和 Flux、Redux 一樣,都是和具體的前端框架無關(guān)的,也就是說可以用于 React(mobx-react) 或者 Vue(mobx-vue)。一般來說,用到 React 比較常見,很少用于 Vue,因為 Vuex 本身就類似 MobX,很靈活。如果我們把 MobX 用于 React ?或者 ?Vue,可以看到很多 setState() 和 this.state.xxx = 這樣的處理都可以省了。

還是和上面一樣,只介紹思想。具體 MobX 的使用,可以看這里。

對比 Redux

我們直觀地上兩坨實現(xiàn)計數(shù)器代碼:

Redux:

import React, { Component } from "react";
import {
  createStore,
  bindActionCreators,
} from "redux";
import { Provider, connect } from "react-redux";

// ①action types
const COUNTER_ADD = "counter_add";
const COUNTER_DEC = "counter_dec";

const initialState = {a: 0};
// ②reducers
function reducers(state = initialState, action) {
  switch (action.type) {
  case COUNTER_ADD:
    return {...state, a: state.a+1};
  case COUNTER_DEC:
    return {...state, a: state.a-1};
  default:
    return state
  }
}

// ③action creator
const incA = () => ({ type: COUNTER_ADD });
const decA = () => ({ type: COUNTER_DEC });
const Actions = {incA, decA};

class Demo extends Component {
  render() {
    const { store, actions } = this.props;
    return (
      

a = {store.a}

); } } // ④將state、actions 映射到組件 props const mapStateToProps = state => ({store: state}); const mapDispatchToProps = dispatch => ({ // ⑤bindActionCreators 簡化 dispatch actions: bindActionCreators(Actions, dispatch) }) // ⑥connect產(chǎn)生容器組件 const Root = connect( mapStateToProps, mapDispatchToProps )(Demo) const store = createStore(reducers) export default class App extends Component { render() { return ( ) } }

MobX:

import React, { Component } from "react";
import { observable, action } from "mobx";
import { Provider, observer, inject } from "mobx-react";

// 定義數(shù)據(jù)結(jié)構(gòu)
class Store {
  // ① 使用 observable decorator 
  @observable a = 0;
}

// 定義對數(shù)據(jù)的操作
class Actions {
  constructor({store}) {
    this.store = store;
  }
  // ② 使用 action decorator 
  @action
  incA = () => {
    this.store.a++;
  }
  @action
  decA = () => {
    this.store.a--;
  }
}

// ③實例化單一數(shù)據(jù)源
const store = new Store();
// ④實例化 actions,并且和 store 進行關(guān)聯(lián)
const actions = new Actions({store});

// inject 向業(yè)務(wù)組件注入 store,actions,和 Provider 配合使用
// ⑤ 使用 inject decorator 和 observer decorator
@inject("store", "actions")
@observer
class Demo extends Component {
  render() {
    const { store, actions } = this.props;
    return (
      

a = {store.a}

); } } class App extends Component { render() { // ⑥使用Provider 在被 inject 的子組件里,可以通過 props.store props.actions 訪問 return ( ) } } export default App;

比較一下:

Redux 數(shù)據(jù)流流動很自然,可以充分利用時間回溯的特征,增強業(yè)務(wù)的可預(yù)測性;MobX 沒有那么自然的數(shù)據(jù)流動,也沒有時間回溯的能力,但是 View 更新很精確,粒度控制很細。

Redux 通過引入一些中間件來處理副作用;MobX ?沒有中間件,副作用的處理比較自由,比如依靠 autorunAsync 之類的方法。

Redux 的樣板代碼更多,看起來就像是我們要做頓飯,需要先買個調(diào)料盒裝調(diào)料,再買個架子放刀叉。。。做一大堆準備工作,然后才開始炒菜;而 MobX 基本沒啥多余代碼,直接硬來,拿著炊具調(diào)料就開干,搞出來為止。

但其實?Redux 和 MobX 并沒有孰優(yōu)孰劣,Redux 比 Mobx 更多的樣板代碼,是因為特定的設(shè)計約束。如果項目比較小的話,使用 MobX 會比較靈活,但是大型項目,像?MobX 這樣沒有約束,沒有最佳實踐的方式,會造成代碼很難維護,各有利弊。一般來說,小項目建議 MobX 就夠了,大項目還是用 Redux 比較合適。

總結(jié)

時光荏苒,歲月如梭。每一個框架或者庫只能陪你走一段路,最終都會逝去。留在你心中的,不是一條一條的語法規(guī)則,而是一個一個的思想,這些思想才是推動進步的源泉。

帥哥美女,如果你都看到這里了,那么不點個贊,你的良心過得去么?

參考鏈接

https://cn.vuejs.org/v2/guide/state-management.html
https://vuex.vuejs.org/
https://cn.redux.js.org/docs/react-redux/
http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html
http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html
https://redux-saga-in-chinese.js.org
https://juejin.im/post/59e6cd68f265da43163c2821
https://react-redux.js.org/introduction/why-use-react-redux
https://segmentfault.com/a/1190000007248878
http://es6.ruanyifeng.com/#docs/generator
https://juejin.im/post/5ac1cb9d6fb9a028cf32a046
https://zhuanlan.zhihu.com/p/35437092
https://github.com/dvajs/dva/issues/1
https://cn.mobx.js.org
https://zhuanlan.zhihu.com/p/25585910
http://imweb.io/topic/59f4833db72024f03c7f49b4

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/100152.html

相關(guān)文章

  • React 328道最全面試題(持續(xù)更新)

    摘要:希望大家在這浮夸的前端圈里,保持冷靜,堅持每天花分鐘來學(xué)習(xí)與思考。 今天的React題沒有太多的故事…… 半個月前出了248個Vue的知識點,受到很多朋友的關(guān)注,都強烈要求再出多些React相前的面試題,受到大家的邀請,我又找了20多個React的使用者,他們給出了328道React的面試題,由我整理好發(fā)給大家,同時發(fā)布在了前端面試每日3+1的React專題,希望對大家有所幫助,同時大...

    kumfo 評論0 收藏0
  • React組件設(shè)計實踐總結(jié)05 - 狀態(tài)管理

    摘要:要求通過要求數(shù)據(jù)變更函數(shù)使用裝飾或放在函數(shù)中,目的就是讓狀態(tài)的變更根據(jù)可預(yù)測性單向數(shù)據(jù)流。同一份數(shù)據(jù)需要響應(yīng)到多個視圖,且被多個視圖進行變更需要維護全局狀態(tài),并在他們變動時響應(yīng)到視圖數(shù)據(jù)流變得復(fù)雜,組件本身已經(jīng)無法駕馭。今天是 520,這是本系列最后一篇文章,主要涵蓋 React 狀態(tài)管理的相關(guān)方案。 前幾篇文章在掘金首發(fā)基本石沉大海, 沒什么閱讀量. 可能是文章篇幅太長了?掘金值太低了? ...

    ideaa 評論0 收藏0
  • 我的前端知識清單

    摘要:我的前端知識清單做前端也有幾年了,整理了一份平時常用的技術(shù)列表,歡迎大家補充。 我的前端知識清單 做前端也有幾年了,整理了一份平時常用的技術(shù)列表,歡迎大家補充。 html方向 html4標簽 html5標簽,語義化,媒體標簽等 svg canvas web workers manifest pwa:service worker(workbox) css方向 css2語法 css3...

    kidsamong 評論0 收藏0
  • 我的前端知識清單

    摘要:我的前端知識清單做前端也有幾年了,整理了一份平時常用的技術(shù)列表,歡迎大家補充。 我的前端知識清單 做前端也有幾年了,整理了一份平時常用的技術(shù)列表,歡迎大家補充。 html方向 html4標簽 html5標簽,語義化,媒體標簽等 svg canvas web workers manifest pwa:service worker(workbox) css方向 css2語法 css3...

    Alan 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<