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

資訊專欄INFORMATION COLUMN

Redux 源碼拾遺

CloudwiseAPM / 3481人閱讀

摘要:循環(huán)還沒(méi)有結(jié)束,其中的某個(gè)對(duì)進(jìn)行了添加或者刪除,都會(huì)影響到此次循環(huán)的進(jìn)行,帶來(lái)不可預(yù)期的錯(cuò)誤。

首先來(lái)一段 redux 結(jié)合 中間件 thunk、logger 的使用demo 了解一下應(yīng)該如何使用

const redux = require("redux")
const { 
  createStore,
  combineReducers,
  bindActionCreators,
  compose,
  applyMiddleware
} = redux

const simpleState = { num: 0 }
const complexState = { num: 1 }

// logger中間件負(fù)責(zé)打印日志
function logger({dispatch, getState}) {
  return function(next) {
    return function(action) {
      console.log("logger start:" + action.type)
      const res = next(action)
      console.log("logger end:" + action.type)
      return res
    }
  }
}

// thunk中間件負(fù)責(zé)異步
function thunk({dispatch, getState}) {
  return function(next) {
    return function(action) {
      if (typeof action === "function") {
        return action(dispatch)
      }
      return next(action)
    }
  }
}

// 派發(fā)的增加方法
function increment(num) {
  return {
    type: "increment",
    payload: num
  }
}

// 派發(fā)的減少的方法
function decrement(num) {
  return {
    type: "decrement",
    payload: num
  }
}

// 派發(fā)的乘法的方法
function multiply(num) {
  return {
    type: "multiply",
    payload: num
  }
}

// 派發(fā)的除法的方法
function divide(num) {
  return {
    type: "divide",
    payload: num
  }
}

// 派發(fā)異步事件方法
function syncDivide() {
  return (dispatch) => {
    setTimeout(() => {
      dispatch(divide(10))
    }, 2000)
  }
}

// 負(fù)責(zé)加減法的reducer
function simpleCalcReducer(state = simpleState, action) {
  switch (action.type) {
    case "increment":
      return Object.assign({}, state, { num: state.num + action.payload })
    case "decrement":
      return Object.assign({}, state, { num: state.num - action.payload })
    default: 
      return state
  }
}

// 負(fù)責(zé)乘除法的reducer
function complexCalcReducer(state = complexState, action) {
  switch (action.type) {
    case "multiply":
      return Object.assign({}, state, { num: state.num * action.payload })
    case "divide":
      return Object.assign({}, state, { num: state.num / action.payload })
    default: 
      return state
  }
}

const middleware = applyMiddleware(thunk, logger)
const allReducers = combineReducers({
  simpleCalcReducer,
  complexCalcReducer
})
const store = createStore(allReducers, middleware)

// 所有action
const allActions = {
  increment,
  decrement,
  multiply,
  divide,
  syncDivide
}
const actions = bindActionCreators(allActions, store.dispatch)

// 派發(fā)action
actions.increment(2);
actions.decrement(1);
actions.multiply(10);
actions.divide(5);
actions.syncDivide()
setTimeout(() => {
  console.log(store.getState())
}, 2500)

打印結(jié)果如下:

上面只是簡(jiǎn)單使用了redux的部分api,來(lái)盡量實(shí)現(xiàn)一個(gè)標(biāo)準(zhǔn)項(xiàng)目中所使用到的redux的基本操作流程,下面進(jìn)入源碼的分析階段

1.項(xiàng)目結(jié)構(gòu)
第一張圖,先來(lái)看一下 redux 的項(xiàng)目結(jié)構(gòu)目錄

---src
------utils // 工具函數(shù)庫(kù)
---------actionTypes.js // redux用來(lái)初始化state,自身調(diào)用的action
---------isPlainObject.js // 用來(lái)檢測(cè)action 是否為一個(gè)明確的對(duì)象
---------warning.js // 用來(lái)進(jìn)行警告的公共方法
------applyMiddleware.js // 中間件,負(fù)責(zé)對(duì)store.dispatch方法進(jìn)行包裝
------bindActionCreators.js // 對(duì)派發(fā)action的語(yǔ)法進(jìn)行優(yōu)化,使之可以更好的應(yīng)用于react或其他
------combineReducers.js // 將分離的reducer合并成一個(gè)對(duì)象,并返回一個(gè)執(zhí)行這些reducer的函數(shù)
------compose.js // 合并函數(shù), 將函數(shù)參數(shù)按照從左向右的順序進(jìn)行合并,返回一個(gè)新函數(shù)
------createStore.js // 核心方法,返回一個(gè)store對(duì)象,用來(lái)獲取state、派發(fā)action、添加訂閱者subscribe 等
------index.js // 入口文件,對(duì)外導(dǎo)出的方法

2.utils 工具方法的分析
將utils的三個(gè)方法放在最前面來(lái)分析,主要是為了先對(duì)三個(gè)工具方法有個(gè)簡(jiǎn)單概念和了解,這樣在閱讀下面的內(nèi)容時(shí),對(duì)于有用到工具方法的地方可以直接理解,不用再打斷思緒去分析工具方法,影響整體的閱讀體驗(yàn)

① warning.js -- 控制臺(tái)打印異常日志

export default function warning(message) {
  if (typeof console !== "undefined" && typeof console.error === "function") {
    console.error(message)
  }
  try {
    throw new Error(message)
  } catch (e) {}
}

總結(jié):
warining 函數(shù)非常簡(jiǎn)單,當(dāng) console.error 方法存在時(shí)來(lái)打印 錯(cuò)誤的 message,這里的一層判斷是為了兼容ie6789瀏覽器只有在開(kāi)啟控制臺(tái)的情況下,console對(duì)象才會(huì)創(chuàng)建,否則會(huì)報(bào)console未定義而導(dǎo)出程序無(wú)法進(jìn)行。
至于下面的為什么要 try 里面去拋出異常,本身這樣做對(duì)程序是沒(méi)有什么影響的,這樣的意義又是什么呢?源碼的注釋里解釋道 “This error was thrown as a convenience so that if you enable "break on all exceptions" in your console, it would pause the execution at this line ”,翻譯過(guò)來(lái)就是,拋出這個(gè)錯(cuò)誤是為了方便查看錯(cuò)誤源。只要我們開(kāi)啟了斷點(diǎn)異常,那么程序就會(huì)在拋出錯(cuò)誤的那行代碼上打上斷點(diǎn),來(lái)方便進(jìn)行調(diào)試和追蹤。那么在谷歌里面這個(gè)異常怎么開(kāi)啟呢?F12打開(kāi)谷歌的開(kāi)發(fā)者工具,1點(diǎn)擊 Sources - 2點(diǎn)擊藍(lán)色的pause icon - 3勾選 Pause on caught exceptions,如下圖所示

在控制臺(tái)里測(cè)試如下代碼

鍵入回車后,瀏覽器出現(xiàn)斷點(diǎn),跳轉(zhuǎn)至sources資源文件,并高亮了拋出錯(cuò)誤的那行代碼,非常方便

② isPlainObject.js -- 判斷目標(biāo)是否為明確的對(duì)象

export default function isPlainObject(obj) {
  if (typeof obj !== "object" || obj === null) return false

  let proto = obj
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}

總結(jié):
Object.getPrototypeOf 方法用來(lái)返回對(duì)象的隱式原型,即構(gòu)造這個(gè)對(duì)象的構(gòu)造函數(shù)的原型。
使用 while 循環(huán)來(lái)查找目標(biāo)對(duì)象的原型鏈頂層,因?yàn)?Object.prototype.__proto__ === null,所以O(shè)bject.prototype 就是原型鏈的頂層,查找到最后一層時(shí),proto 一定被賦值為 Object.prototype。
這樣做的意義是什么? Array數(shù)據(jù)類型 或者 dom的 document等數(shù)據(jù)類型的typeof 也是 object, 他們都不是直接由Object 函數(shù)來(lái)構(gòu)建的對(duì)象,以 Array 數(shù)組的構(gòu)造函數(shù)來(lái)舉例。

var ary = new Array()  
var pro = Object.getPrototypeOf(ary)
console.log(pro === ary.__proto__) // true
console.log(ary.__proto__ === Array.prototype)  // true

var pro2 = Object.getPrototypeOf(pro)
console.log(pro2 === Object.prototype)  // true
Object.prototype.__proto__ === null  // true

可見(jiàn) ary 第一次獲取到的原型是 Array.prototype,而 Array.prototype 本身也是一個(gè)對(duì)象,必然由 Object 函數(shù)創(chuàng)建,所以 Array.prototype.__proto__ 又指向了 Object.prototype,到此循環(huán)結(jié)束。最終 pro = Object.prototype。這就造成了最終的 Object.getPrototypeOf(obj) 和 proto 是不等的。
所以這個(gè)方法的就很清晰了,只有直接使用 Object 函數(shù)創(chuàng)建的對(duì)象才會(huì)被判斷為真,因?yàn)橹挥兴玩湸嬖谝粚?/p>

③ actionTypes.js -- redux 內(nèi)部做初始化state的action

const randomString = () =>
  Math.random()
    .toString(36)
    .substring(7)
    .split("")
    .join(".")

const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`,
  REPLACE: `@@redux/REPLACE${randomString()}`,
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}

export default ActionTypes

總結(jié):
randomString() 方法隨機(jī)生成一個(gè)36進(jìn)制的數(shù)字,然后切割拼接,最終生成 和"5.b.p.3.b.8" 格式一樣的字符串
這個(gè)方法 導(dǎo)出了一個(gè)對(duì)象,對(duì)象包含3個(gè)key: INIT、REPLACE、PROBE_UNKNOWN_ACTION,前兩個(gè)是字符串,后面一個(gè)是方法,方法也是返回一個(gè)拼接好的字符串,其實(shí)這三個(gè)都是redux內(nèi)部用來(lái)派發(fā)action 的 type

3.模塊分析(去掉注釋)
① 入口文件 index.js**

import createStore from "./createStore"
import combineReducers from "./combineReducers"
import bindActionCreators from "./bindActionCreators"
import applyMiddleware from "./applyMiddleware"
import compose from "./compose"
import warning from "./utils/warning"
import __DO_NOT_USE__ActionTypes from "./utils/actionTypes"

function isCrushed() {}

if (
  process.env.NODE_ENV !== "production" &&
  typeof isCrushed.name === "string" &&
  isCrushed.name !== "isCrushed"
) {
  warning(
    "You are currently using minified code outside of NODE_ENV === "production". " +
      "This means that you are running a slower development build of Redux. " +
      "You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify " +
      "or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) " +
      "to ensure you have the correct code for your production build."
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}

總結(jié):
入口文件作用就時(shí)將幾個(gè)模塊文件引入和導(dǎo)出,里面有一個(gè)空的 isCrushed 函數(shù),這個(gè)函數(shù)的意義就是判斷當(dāng)前的構(gòu)建工具是否為 node 環(huán)境,如果是 node 環(huán)境并且非生產(chǎn)環(huán)境,那么就要判斷當(dāng)前的 redux 文件有沒(méi)有被壓縮。
判斷的目的就是希望開(kāi)發(fā)者,在非生產(chǎn)環(huán)境下不要壓縮代碼,如果項(xiàng)目比較大,我們只是修改了一個(gè)小小的樣式,這時(shí)候如果開(kāi)啟代碼的混淆壓縮,那么我們項(xiàng)目的所有依賴的文件都會(huì)被混淆壓縮,項(xiàng)目越大被壓縮的內(nèi)容越多耗時(shí)越長(zhǎng),從而導(dǎo)致調(diào)試的時(shí)間增加,降低開(kāi)發(fā)效率。這也正是redux在 warning 警告里提到的 "This means that you are running a slower development build of Redux",你正在一個(gè)緩慢的開(kāi)發(fā)環(huán)境下使用 redux。

② 核心文件 creteStore.js
cretaeStore.js 是redux的核心文件,在這個(gè)方法里,redux 向外提供了 dispatch、subscribe、getState、replaceReducer 這四個(gè)核心方法。此外還有一個(gè) [$$observable] 方法,這個(gè)方法并不是很好理解他的作用和意義,放在文章最后來(lái)說(shuō)明。下面是移除了注釋的源代碼

import $$observable from "symbol-observable"
import ActionTypes from "./utils/actionTypes"
import isPlainObject from "./utils/isPlainObject"

export default function createStore(reducer, preloadedState, enhancer) {
  // 判斷1
  if (
    (typeof preloadedState === "function" && typeof enhancer === "function") ||
    (typeof enhancer === "function" && typeof arguments[3] === "function")
  ) {
    throw new Error(
      "It looks like you are passing several store enhancers to " +
        "createStore(). This is not supported. Instead, compose them " +
        "together to a single function."
    )
  }

  // 判斷2
  if (typeof preloadedState === "function" && typeof enhancer === "undefined") {
    enhancer = preloadedState
    preloadedState = undefined
  }

  // 判斷3
  if (typeof enhancer !== "undefined") {
    if (typeof enhancer !== "function") {
      throw new Error("Expected the enhancer to be a function.")
    }
    return enhancer(createStore)(reducer, preloadedState)
  }

  // 判斷4
  if (typeof reducer !== "function") {
    throw new Error("Expected the reducer to be a function.")
  }

  // 內(nèi)部變量
  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  function ensureCanMutateNextListeners() {
    // ...
  }

  function getState() {
    // ...
  }

  function subscribe(listener) {
    // ...
  }

  function dispatch(action) {
    // ...
  }

  function replaceReducer(nextReducer) {
    // ...
  }

  function observable() {
    // ...
  }

  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

createStore 方法接收3個(gè)參數(shù),分別是 reducer,preloadedState,enhancer。
1.reducer 就是一個(gè)純函數(shù)用來(lái)返回 state
2.preloadedState 是初始化的state,在實(shí)際開(kāi)發(fā)中,很少有傳遞這個(gè)這個(gè)參數(shù)。其實(shí)這個(gè)參數(shù)就是為了初始化必須的原始數(shù)據(jù)。此外,如果使用了combineReducer這個(gè)方法來(lái)組合多個(gè)reducer,相應(yīng)的preloadedState 里的key 也必須要和 combineReducer 中的key相對(duì)應(yīng)
3.enhancer 翻譯過(guò)來(lái)就是增強(qiáng)器,它是一個(gè)類似的高階函數(shù)用來(lái)包裝和增強(qiáng) creteStore 內(nèi)部的 dispatch、subscribe、getState 等方法,通過(guò)這個(gè)高階函數(shù)可以實(shí)現(xiàn) 中間件、時(shí)間旅行、持久化state等,在redux內(nèi)只實(shí)現(xiàn)了一個(gè)enhancer,它就是中間件 applyMIddleware,用來(lái)強(qiáng)化 dispatch方法。

講完三個(gè)參數(shù),開(kāi)始解釋代碼

  if (
    (typeof preloadedState === "function" && typeof enhancer === "function") ||
    (typeof enhancer === "function" && typeof arguments[3] === "function")
  ) {
    throw new Error(
      "It looks like you are passing several store enhancers to " +
        "createStore(). This is not supported. Instead, compose them " +
        "together to a single function."
    )
  }

判斷1:提示很明確,當(dāng)使用多個(gè)enhancer時(shí),需要使用compose 方法將多個(gè)enhancer合并成一個(gè)單一的函數(shù)。

// 判斷2
  if (typeof preloadedState === "function" && typeof enhancer === "undefined") {
    enhancer = preloadedState
    preloadedState = undefined
  }

判斷2:個(gè)人感覺(jué)這里的判斷就是為了符合更大眾化的開(kāi)發(fā)需求,preloadedState 這個(gè)參數(shù)在實(shí)際開(kāi)發(fā)中,真的很少傳遞。所以在這里做了一個(gè)判斷,如果開(kāi)發(fā)中沒(méi)有用到這個(gè)初始的preloadedState,完全可以將它省略掉,直接傳遞最后一個(gè)enhancer函數(shù),redux在內(nèi)部幫開(kāi)發(fā)者,完成了這部分參數(shù)轉(zhuǎn)換的處理。

// 判斷3
  if (typeof enhancer !== "undefined") {
    if (typeof enhancer !== "function") {
      throw new Error("Expected the enhancer to be a function.")
    }
    return enhancer(createStore)(reducer, preloadedState)
  }

判斷3:當(dāng)enhancer 存在并且為函數(shù)的時(shí)候,直接將當(dāng)前creteStore方法作為參數(shù)傳遞給 enhancer。而這個(gè)enhancer(createStore),返回的就是 createStore 的加強(qiáng)版可以稱他為 creteStore-X,至于如何增強(qiáng)先放到后面的applyMiddleware這個(gè)enhancer來(lái)說(shuō)明。這里先只需要知道,通過(guò)enhancer包裝過(guò)后的 createStore,內(nèi)部的某些方法被加強(qiáng)了。

  // 判斷4
  if (typeof reducer !== "function") {
    throw new Error("Expected the reducer to be a function.")
  }

判斷4:reducer 必須是一個(gè)函數(shù) 函數(shù) 函數(shù)

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

內(nèi)部變量:
這里分別使用了,currentReducer 和 currentState 來(lái)接收初始傳遞的 reducer 和 preloadedState,重新賦值是因?yàn)?currentReducer 和 currentState 都是可變的,當(dāng)他們被修改的時(shí)候不會(huì)影響初始的reducer 和 preloadedState。
currentListeners 是一個(gè)數(shù)組用來(lái)存儲(chǔ)訂閱的函數(shù)列表,為什么還要多定義一個(gè) nextListeners = currentListeners 呢?這個(gè)問(wèn)題放到后面看比較好理解,先掠過(guò)
isDispatching 比較好理解,用來(lái)判斷redux是否處于派發(fā)action的狀態(tài)中,即是否在執(zhí)行reducer。

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

ensureCanMutateNextListeners:返回currentListeners 的淺拷貝
這里的意圖不是很明顯,當(dāng)nextListeners 和 currentListeners 全等的時(shí)候,返回一個(gè) currentListeners 的 淺拷貝賦值給 nextListenenrs,意義是什么呢接著向下看。

 function getState() {
    if (isDispatching) {
      throw new Error(
        "You may not call store.getState() while the reducer is executing. " +
          "The reducer has already received the state as an argument. " +
          "Pass it down from the top reducer instead of reading it from the store."
      )
    }

    return currentState
  }

getState:返回當(dāng)前的currentState
返回當(dāng)前的currentState,當(dāng)action還在 派發(fā)當(dāng)中時(shí),如果調(diào)用這個(gè)方法會(huì)拋出錯(cuò)誤

  function subscribe(listener) {
     // 被添加的訂閱者必須是一個(gè)函數(shù)
    if (typeof listener !== "function") {
      throw new Error("Expected the listener to be a function.")
    }
    // 處于dispatch的時(shí)候,不允許添加訂閱者
    if (isDispatching) {
      throw new Error(
        "You may not call store.subscribe() while the reducer is executing. " +
          "If you would like to be notified after the store has been updated, subscribe from a " +
          "component and invoke store.getState() in the callback to access the latest state. " +
          "See https://redux.js.org/api-reference/store#subscribe(listener) for more details."
      )
    }
    // 這里是一個(gè)閉包,isSubscribed 用來(lái)表示當(dāng)前的訂閱者已經(jīng)成功的被
    // 添加到了訂閱的列表中
    let isSubscribed = true
    // 當(dāng)nexListeners 和 currentListeners 全等時(shí)候,返回了一個(gè)新的
    // nextListeners, 然后將訂閱者添加到新的 nextListeners
    ensureCanMutateNextListeners() // ?1
    nextListeners.push(listener)
    // 返回一個(gè) unsubscribe, 用來(lái)取消已經(jīng)添加到訂閱列表中的訂閱者
    return function unsubscribe() {
        // 如果當(dāng)前的訂閱者已經(jīng)被取消了 直接返回
      if (!isSubscribed) {
        return
      }
      // 處于dispatch的時(shí)候,不允許取消訂閱者
      if (isDispatching) {
        throw new Error(
          "You may not unsubscribe from a store listener while the reducer is executing. " +
            "See https://redux.js.org/api-reference/store#subscribe(listener) for more details."
        )
      }
      // 將訂閱者的狀態(tài)改為false
      isSubscribed = false
      // 繼續(xù)更新 nextListeners
      ensureCanMutateNextListeners()
      // 將取消的訂閱者從 nextListeners 中刪除
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

subscribe:用來(lái)添加和刪除訂閱者
這樣粗略看下來(lái)似乎也沒(méi)太看清楚這個(gè) ensureCanMutateNextListeners 方法有什么卵用,繼續(xù)翻看redux 對(duì)ensureCanMutateNextListeners 方法的解釋,里面提到
"This makes a shallow copy of currentListeners so we can use nexListeners as a temporary list while dispatching"
"This prevents any bugs around consumers calling subscribe/unsubscribe in the middle of a dispatch"
主要就是后面這一句,這個(gè)方法 可以防止用戶在執(zhí)行dispatch中調(diào)用訂閱或者取消訂閱時(shí)出現(xiàn)任何錯(cuò)誤。還是不明所以,繼續(xù)向下尋找答案

  function dispatch(action) {
      // 判斷 action 是否為明確的 object 對(duì)象
    if (!isPlainObject(action)) {
      throw new Error(
        "Actions must be plain objects. " +
          "Use custom middleware for async actions."
      )
    }
     // action 必須有 type 這個(gè)字段類型
    if (typeof action.type === "undefined") {
      throw new Error(
        "Actions may not have an undefined "type" property. " +
          "Have you misspelled a constant?"
      )
    }
      // 不允許同時(shí)執(zhí)行多個(gè) dispatch
    if (isDispatching) {
      throw new Error("Reducers may not dispatch actions.")
    }
      // 將執(zhí)行狀態(tài)設(shè)置為true, 開(kāi)始執(zhí)行reudcer, 并接收 currentState
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
        // 無(wú)論成功失敗 都將執(zhí)行狀態(tài)重置
      isDispatching = false
    }
      // reducer 執(zhí)行完畢之后,開(kāi)始循環(huán)執(zhí)行 訂閱者數(shù)組列表
    const listeners = (currentListeners = nextListeners) // ?2
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i] // ?3
      listener()
    }

    return action
  }

dispatch:用來(lái)派發(fā)action,執(zhí)行reducer
問(wèn)題2,每次執(zhí)行訂閱者列表的時(shí)候,為什么要新定義一個(gè) listeners 來(lái)接收 nextListeners,直接 currentListeners = nextListeners 然后使用 currentListeners 來(lái)循環(huán)不行嗎?
問(wèn)題3,循環(huán)里面為什么不直接使用 listeners[i]() 來(lái)執(zhí)行調(diào)用,而是定義一個(gè)變量來(lái)接收然后再執(zhí)行?
再結(jié)合上面 subscribe 中 ensureCanMutateNextListeners 的到底有何意義?
其實(shí),這一切的細(xì)節(jié)都是為了解決一種特殊的應(yīng)用場(chǎng)景,在訂閱事件內(nèi)部 再次 添加訂閱或者取消訂閱。如
store.subscribe( () => { store.subscribe(...) })
如果是這種場(chǎng)景,因?yàn)槭窃谘h(huán)中去觸發(fā) listener,單純的使用 currentListeners 來(lái)存儲(chǔ)訂閱列表是無(wú)法滿足的。循環(huán)還沒(méi)有結(jié)束,其中的某個(gè) listener 對(duì) currentListeners 進(jìn)行了添加或者刪除,都會(huì)影響到此次循環(huán)的進(jìn)行,帶來(lái)不可預(yù)期的錯(cuò)誤。至此這些細(xì)節(jié)就變得清晰起來(lái)了。
引用官方的注釋更為準(zhǔn)確,每次dispatch后,循環(huán)使用的是當(dāng)前nextListeners 的快照,同樣也就是 currentListeners,它在循環(huán)結(jié)束之前是不會(huì)被改變的。想象一下,假如在訂閱事件的內(nèi)部繼續(xù)調(diào)用 store.subsrcibe 來(lái)添加訂閱者,那么就會(huì)調(diào)用 ensureCanMutateNextListeners 這個(gè)方法,如果currentListeners 和 nextListeners 是完全相等的說(shuō)明nextListeners 還未被改變,此時(shí)淺拷貝一份 currentListenrs 的隊(duì)列賦值為 nextListeners,nextListeners 就變成了一個(gè)全新的訂閱隊(duì)列,然后將 添加的訂閱者放到新的 nextListeners,這樣就完全不會(huì)影響到之前已經(jīng)開(kāi)始的循環(huán)。當(dāng)下次disptach 再次發(fā)起的時(shí)候,將 currentListeners 同步為最新的 nextListeners 隊(duì)列。
問(wèn)題 2 應(yīng)該如何理解呢?找到了早期redux的提交記錄如下:

這里還沒(méi)有對(duì) currentListeners 和 nextListeners 做概念的區(qū)分,只是將每次listeners 淺拷貝了一層用來(lái) 安全的執(zhí)行循環(huán)。所以 const listeners = (currentListeners = nextListeners) 中聲明的 listeners并不是必須的,他的存在只是為了在之后在循環(huán)中使用 listeners 代替 currentListeners 少打幾個(gè)字母而已
問(wèn)題 2 應(yīng)該如何理解呢?其實(shí)在這里是為了確保在 listener 當(dāng)中的 this 與我們對(duì) js 當(dāng)中的函數(shù)內(nèi)部 this 指向誰(shuí)的預(yù)期保持一致。這里就是將 this 重新綁定為默認(rèn)的全局對(duì)象,如果直接使用 listeners[i]() 來(lái)調(diào)用,那么其內(nèi)部的this 變指向了listeners 這個(gè)數(shù)組本身。

  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== "function") {
      throw new Error("Expected the nextReducer to be a function.")
    }
    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

replaceReducer:替換reducer,并且重新 dispatch
replaceReducer 這個(gè)方法比較簡(jiǎn)單,當(dāng)我們的項(xiàng)目啟用了模塊懶加載,我們的最開(kāi)始的reducer可能只是部分的模塊的reducer,這時(shí)候如果要引入新的模塊就需要能夠動(dòng)態(tài)的替換 reducer 來(lái)更新state。官方的注釋里還寫道,如果在開(kāi)發(fā)模式下啟用了熱更新,同樣需要這個(gè)函數(shù)來(lái)進(jìn)行替換。

  function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeof observer !== "object" || observer === null) {
          throw new TypeError("Expected the observer to be an object.")
        }
        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }
        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },
      [$$observable]() {
        return this
      }
    }
  }

observable:此方法返回一個(gè) observable 對(duì)象,該對(duì)象擁有 subscribe 的方法來(lái)添加一個(gè)可觀測(cè)的對(duì)象。那么這個(gè)方法到底時(shí)干什么用的呢?注釋里有寫道 “For more information, see the observable proposal:https://github.com/tc39/propo...”,打開(kāi)這個(gè)地址看到這個(gè)項(xiàng)目是將 可觀察類型引入到ECMAScript標(biāo)準(zhǔn)庫(kù)中,而redux 這里就是實(shí)現(xiàn)了 observable 的觀察對(duì)象。這里不是很清楚如何使用 ... 略過(guò)。

  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }

最后使用 dispatch({ type: ActionTypes.INIT }) 生成一個(gè)初始化的state,這也就是為什么preloadedState不需要傳遞,就可以得到初始化的state了。因?yàn)閞edux內(nèi)部在執(zhí)行 createStore 這個(gè)方法的時(shí)候,自動(dòng)執(zhí)行了一次 disptach。最后將眾方法返回

③ combineReducers -- 將多個(gè)reducer 合并成一個(gè)函數(shù)

import ActionTypes from "./utils/actionTypes"
import warning from "./utils/warning"
import isPlainObject from "./utils/isPlainObject"

// 函數(shù)1
function getUndefinedStateErrorMessage(key, action) {
  // ...
}

// 函數(shù)2
function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  // ...
}

// 函數(shù)3
function assertReducerShape(reducers) {
  // ...
}

// 函數(shù)4 
export default function combineReducers(reducers) {
  // ...
}
function getUndefinedStateErrorMessage(key, action) {
  const actionType = action && action.type
  const actionDescription =
    (actionType && `action "${String(actionType)}"`) || "an action"

  return (
    `Given ${actionDescription}, reducer "${key}" returned undefined. ` +
    `To ignore an action, you must explicitly return the previous state. ` +
    `If you want this reducer to hold no value, you can return null instead of undefined.`
  )
}

getUndefinedStateErrorMessage: 方法比較簡(jiǎn)單,返回一段提示信息,提示 reducer不能返回undefined

function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? "preloadedState argument passed to createStore"
      : "previous state received by the reducer"
    // reducer 不存在時(shí)進(jìn)行的提示
  if (reducerKeys.length === 0) {
    return (
      "Store does not have a valid reducer. Make sure the argument passed " +
      "to combineReducers is an object whose values are reducers."
    )
  }
      // state 為非明確的對(duì)象的時(shí)候的提示
  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join("", "")}"`
    )
  }
  // 當(dāng)state當(dāng)中的某個(gè)key值無(wú)法在reducers當(dāng)中找到,并且還未被加入到unexpectedKeyCache
  // 那么就把這個(gè)key篩選出來(lái)
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )
  // 將上一步驟篩選出來(lái)的key,存儲(chǔ)到unexpectedKeyCache里面
  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })
  // 如果是使用了 replaceReducer 替換了reducer,那么就不需要進(jìn)行提示了,因?yàn)橹?  // 的state 的數(shù)據(jù)可能和 新的reducer 不能保持一致
  if (action && action.type === ActionTypes.REPLACE) return
  // 將state里面reducer不能操作的key打印出來(lái)
  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? "keys" : "key"} ` +
      `"${unexpectedKeys.join("", "")}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join("", "")}". Unexpected keys will be ignored.`
    )
  }
  }

getUnexpectedStateShapeWarningMessage: 在非生產(chǎn)環(huán)境對(duì)finalReducer進(jìn)行的校驗(yàn),將state內(nèi)異常的 key 拋出,進(jìn)而提示開(kāi)發(fā)者。

function assertReducerShape(reducers) {
    // 循環(huán)所有reducer, 并使用 ActionTypes.INIT 和 ActionTypes.PROBE_UNKNOWN_ACTION()
    // 兩個(gè)type類型的 action 校驗(yàn)所有reducer 是否返回了符合規(guī)范的 state
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    const initialState = reducer(undefined, { type: ActionTypes.INIT })

    if (typeof initialState === "undefined") {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
          `If the state passed to the reducer is undefined, you must ` +
          `explicitly return the initial state. The initial state may ` +
          `not be undefined. If you don"t want to set a value for this reducer, ` +
          `you can use null instead of undefined.`
      )
    }

    if (
      typeof reducer(undefined, {
        type: ActionTypes.PROBE_UNKNOWN_ACTION()
      }) === "undefined"
    ) {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
          `Don"t try to handle ${
            ActionTypes.INIT
          } or other actions in "redux/*" ` +
          `namespace. They are considered private. Instead, you must return the ` +
          `current state for any unknown actions, unless it is undefined, ` +
          `in which case you must return the initial state, regardless of the ` +
          `action type. The initial state may not be undefined, but can be null.`
      )
    }
  })
}

assertReducerShape:用來(lái)斷言reducers返回的結(jié)果是不是符合要求的類型,如果不滿足判斷它會(huì)拋出一個(gè)error
可以看出在不使用 combineReducers 的時(shí)候,我們編寫的唯一的reducer是不用這些校驗(yàn)的,如果我們?cè)谄渲蟹祷亓?undefined 那么必然最終的 currentState 就變成了 undefined。
那么為什么 combineReducers 這個(gè)方法強(qiáng)制所有的 reduer 不能返回 undefined 呢?
找到了redux的中文文檔:http://cn.redux.js.org/docs/a...
里面提到:”combineReducers 函數(shù)設(shè)計(jì)的時(shí)候有點(diǎn)偏主觀,就是為了避免新手犯一些常見(jiàn)錯(cuò)誤。也因些我們故意設(shè)定一些規(guī)則,但如果你自己手動(dòng)編寫根 redcuer 時(shí)并不需要遵守這些規(guī)則“
這樣就很清楚了,原來(lái)多的這些校驗(yàn)是為了更好的提示新手用戶,reducer的正確使用規(guī)范。
這里還有一個(gè)問(wèn)題,使用 {type: ActionTypes.INIT} 來(lái)校驗(yàn) reducer 是否返回正確的state,ActionTypes.INIT 應(yīng)該必然是 reducer 內(nèi)部未知的 action 了。但是為什么下面還要用 { type: ActionTypes.PROBE_UNKNOWN_ACTION() } 在重復(fù)校驗(yàn)一次嗎?難道說(shuō)只是為了說(shuō)明兩次校驗(yàn)的出發(fā)目的不一樣?但是這樣是不是多余了

export default function combineReducers(reducers) {
  // 接收一個(gè)對(duì)象形式的 reducers 集合如 { reducer1: () => {}, reducer2: () => {}, }
  const reducerKeys = Object.keys(reducers)
  // 最終將要被執(zhí)行的reducers 集合
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
     // 判斷當(dāng)前如果是非生成環(huán)境 并且 reducers[key] 不存在時(shí)在控制臺(tái)打印警告信息
    if (process.env.NODE_ENV !== "production") {
      if (typeof reducers[key] === "undefined") {
        warning(`No reducer provided for key "${key}"`)
      }
    }
     // 只有當(dāng) reducers[key] 為函數(shù)類型時(shí) 才添加到 finalReducers 當(dāng)中
    if (typeof reducers[key] === "function") {
      finalReducers[key] = reducers[key]
    }
  }
  // 獲取finalReducers 當(dāng)中的 key
  const finalReducerKeys = Object.keys(finalReducers)
  // 用來(lái)存放state中不能被操作的 key
  let unexpectedKeyCache
  if (process.env.NODE_ENV !== "production") {
      // 為了優(yōu)化性能只在非生產(chǎn)模式下進(jìn)行校驗(yàn)
    unexpectedKeyCache = {}
  }
  // 用來(lái)校驗(yàn)finalReducers中每一個(gè)reducer是否合乎規(guī)范
  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }
  return function combination(state = {}, action) {
      // 到這里開(kāi)始執(zhí)行 reducer
    if (shapeAssertionError) {
        // 如果reducer返回了undefined直接拋出錯(cuò)誤
      throw shapeAssertionError
    }
    // 非生產(chǎn)環(huán)境進(jìn)行提示
    if (process.env.NODE_ENV !== "production") {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }
    // 判斷 state 有沒(méi)用被更改
    let hasChanged = false
    // 重新生成的state
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === "undefined") {
          // 如果reducer 返回了undefined 拋出錯(cuò)誤
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      // 如果state 內(nèi)的某個(gè)key的數(shù)據(jù)已經(jīng)被更改過(guò) 此處必然是 true
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 根據(jù)state是否改動(dòng)過(guò) 返回對(duì)應(yīng)的state
    return hasChanged ? nextState : state
  }
}

combineReducers:這個(gè)方法就是將多個(gè)reducers循環(huán)執(zhí)行后最終返回一個(gè)合并后的state,這個(gè)方法還是比較簡(jiǎn)單的。

④-- bindActionCreators,提供了一種調(diào)用dispatch的其他方式,代碼本身比較簡(jiǎn)單,這里就不在贅述了

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === "function") {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== "object" || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${
        actionCreators === null ? "null" : typeof actionCreators
      }. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }

  const boundActionCreators = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === "function") {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

⑤ -- compose, 組合函數(shù),將多個(gè)函數(shù)參數(shù)按照傳入的順序組合成一個(gè)函數(shù),并返回。第二個(gè)參數(shù)作為第一個(gè)函數(shù)的參數(shù),第三個(gè)參數(shù)作為第二個(gè)函數(shù)的參數(shù),依次類推。返回的函數(shù)接收的參數(shù),將作為compose 初始參數(shù)的最后一個(gè)函數(shù)的參數(shù),源碼如下

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

  if (funcs.length === 1) {
    return funcs[0]
  }
   // 最終 compose 返回的是一個(gè) (...args) => a(b(...args)) 這樣的函數(shù)
  // 那么這個(gè)函數(shù)內(nèi)部調(diào)用的順序也就清楚了,執(zhí)行這個(gè)函數(shù)的時(shí)候,首先會(huì)執(zhí)行 
  // var c = b(...args),  然后將c 作為參數(shù)傳遞給 a,如果多個(gè)函數(shù)的話依次類推,
  // 最先執(zhí)行最里面的那個(gè)參數(shù),由里向外
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

問(wèn)題1,先執(zhí)行了b(...args)函數(shù),不就直接開(kāi)始從里面執(zhí)行了嗎?demo1

var funcs2 = [
  function a() {
    console.log("a")
  },
  function b() {
    console.log("b")
  },
  function c(arg) {
    console.log(arg)
    console.log("c")
  }
]
var t = compose(funcs2)
t("d")
// d c b a

說(shuō)好的剝洋蔥呢,怎么就成半個(gè)了?
其實(shí)compose這個(gè)方法只是提供了組合函數(shù)的功能,真正想要實(shí)現(xiàn)完整的從外到內(nèi)這種完整的洋蔥模型,還需要對(duì)傳遞到compose參數(shù)的每個(gè)函數(shù)做處理,它的每個(gè)函數(shù)一定是返回了一個(gè)新的函數(shù),這樣才能確保它不會(huì)被簡(jiǎn)單的執(zhí)行,思考下面的demo

var funcs = [
  function a(b) {
    return () => {
      console.log("a")
      return b();
    }
  },
  function b(c) {
    return () => {
      console.log("b");
      return c();
    }
  },
  function c(arg) {
    return () => {
      console.log("c");
      console.log(arg)
    }
  }
]
var f = compose(funcs)("d");
f()
// a b c d

這就和文章最開(kāi)始實(shí)現(xiàn)的 logger 和 thunk 的中間件很相似了。a 里執(zhí)行 b , 然后 b 里執(zhí)行 c,一個(gè)中間件的雛形已經(jīng)出現(xiàn)了。

⑥ -- applyMiddleware,redux內(nèi)唯一實(shí)現(xiàn)的enhancer,用來(lái)擴(kuò)展dispatch方法,也是redux中最難理解的地方,一睹真容吧,為了便于對(duì)執(zhí)行過(guò)程的理解,這里貼一下中間件redux-thunk源碼的簡(jiǎn)化版

({ dispatch, getState }) => next => action => {
  if (typeof action === "function") {
    return action(dispatch, getState);
  }
  return next(action);
}
import compose from "./compose"
export default function applyMiddleware(...middlewares) {
   // 這里 applyMiddleware 返回的函數(shù)接收的第一個(gè)參數(shù)是 creteStore,
   // 這就是之前 createStore內(nèi)直接使用 enhancer(createStore)(reducer, preloadedState)
   // 的原因了。 
  return createStore => (...args) => {
      // store 就是最原始的 store 對(duì)象,這里還未對(duì)store.dispatch 進(jìn)行擴(kuò)展
    const store = createStore(...args)
      // 聲明一個(gè)dispatch的方法,注意這里使用的時(shí)let,表示這個(gè)dispatch將要會(huì)被改變
      // 而改變過(guò)后的dispatch 就是加入了中間價(jià)增強(qiáng)版的 dispatch
    let dispatch = () => {
      throw new Error(
        "Dispatching while constructing your middleware is not allowed. " +
          "Other middleware would not be applied to this dispatch."
      )
    }
    // middlewareAPI和字面意思一樣就是,中間件需要用到的原始的store對(duì)象的方法,,
    // 這里提供了兩個(gè)方法,一個(gè)getState, 一個(gè)是dispatch。等等這里好像有點(diǎn)不一樣的東西
    // 為什么這里 要寫成 dispatch: (...args) => dispatch(...args),而不是直接
    // dispatch: dispatch  先留下繼續(xù)向下看。
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 這里參考上面的redux-thunk的源碼 chain最終返回的結(jié)果就是
    /*
       chain = [ next => action => {
          if (typeof action === "function") {
            return action(dispatch, getState);
            // 此處的dispatch 和 getState 即為 middlewareAPI的dispatch 和 getState
          }
          return next(action);
        } ]
        */
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 結(jié)合之前已經(jīng)分析過(guò)的compose,compose(...chain)最后一個(gè)參數(shù)來(lái)接收 store.dispatch
    // 因?yàn)檫@里只傳遞了一個(gè)thunk中間件,所以,這里的 dispatch 就變成了
    /* 
        dispatch = (action) => {
          if (typeof action === "function") {
            return action(dispatch, getState);
            // 此處的dispatch 和 getState 依然為 middlewareAPI的dispatch 和 getState
          }
          // 由于next被賦值為store.dispatch, 此處實(shí)際為
          return store.dispatch(action);
        }
    */
    // 也因?yàn)檫@里dispatch 被重新賦值,導(dǎo)致middlewareAPI內(nèi)dispatch屬性
    // 里使用到的 dispatch 也變了,不再是拋出error錯(cuò)誤的那個(gè)函數(shù)了。
    // 這就是為什么一定要寫成 dispatch: (...args) => dispatch(...args) 的原因了
    // 如果寫成 dispatch: dispatch, 相當(dāng)于只是初始聲明了這個(gè)方法,后續(xù)dispatch的修改就與它無(wú)關(guān)了
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
    // 返回的dispatch 替換掉了 store 內(nèi)部初始的 dispatch, dispatch被擴(kuò)展了
  }
}

總結(jié):
通過(guò)對(duì)源碼的翻閱,了解到了整個(gè)redux的執(zhí)行流程和機(jī)制。通過(guò)createStore來(lái)初始化state,當(dāng)需要使用異步action的時(shí)候,可以使用 applyMiddleware 將redux-thunk redux-saga 等中間件對(duì)store.dispatch 進(jìn)行擴(kuò)展。每次調(diào)用 dispatch 都會(huì)執(zhí)行所有的reducer,reducer執(zhí)行完畢后,會(huì)更新所有的訂閱事件。

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

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

相關(guān)文章

  • Redux 源碼拾遺

    摘要:循環(huán)還沒(méi)有結(jié)束,其中的某個(gè)對(duì)進(jìn)行了添加或者刪除,都會(huì)影響到此次循環(huán)的進(jìn)行,帶來(lái)不可預(yù)期的錯(cuò)誤。 首先來(lái)一段 redux 結(jié)合 中間件 thunk、logger 的使用demo 了解一下應(yīng)該如何使用 const redux = require(redux) const { createStore, combineReducers, bindActionCreators, ...

    zhangfaliang 評(píng)論0 收藏0
  • 【underscore 源碼解讀】Array Functions 相關(guān)源碼拾遺 & 小結(jié)

    摘要:最近開(kāi)始看源碼,并將源碼解讀放在了我的計(jì)劃中。將轉(zhuǎn)為數(shù)組同時(shí)去掉第一個(gè)元素之后便可以調(diào)用方法總結(jié)數(shù)組的擴(kuò)展方法就解讀到這里了,相關(guān)源碼可以參考這部分。放個(gè)預(yù)告,下一篇會(huì)暫緩下,講下相關(guān)的東西,敬請(qǐng)期待。 Why underscore 最近開(kāi)始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計(jì)劃中。 閱讀一些著名框架類庫(kù)的源碼,就好...

    SimpleTriangle 評(píng)論0 收藏0
  • 【underscore 源碼解讀】Object Functions 相關(guān)源碼拾遺 & 小結(jié)

    摘要:直接來(lái)看例子一目了然,第一個(gè)參數(shù)是對(duì)象,第二個(gè)參數(shù)可以是一系列的值,也可以是數(shù)組數(shù)組中含,也可以是迭代函數(shù),我們根據(jù)值,或者迭代函數(shù)來(lái)過(guò)濾中的鍵值對(duì),返回新的對(duì)象副本。 Why underscore 最近開(kāi)始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計(jì)劃中。 閱讀一些著名框架類庫(kù)的源碼,就好像和一個(gè)個(gè)大師對(duì)話,你會(huì)學(xué)到很多。...

    neuSnail 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<