国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

資源依賴問題在 bowl 中的一種解決方式

Ilikewhite / 3255人閱讀

摘要:多個異步任務(wù)的順序執(zhí)行通過方法,取得了一個描述加載順序的二維數(shù)組。同時,二維數(shù)組的長度也是不定的,更不能窮舉。利用這個特性,只需要遍歷原二維數(shù)組,將每個放在一個中的函數(shù)中執(zhí)行并返回即可因為的返回值就是一個,有一種惰性執(zhí)行的感覺。

問題

bowl 是一個利用 local storage 進行靜態(tài)資源緩存和加載的工具庫,在開發(fā)過程中遇到過一些問題,其中比較典型的是加載多個資源的時候資源之間可能出現(xiàn)相互依賴的情況,假設(shè)有一個基于 Angular 的應(yīng)用,開發(fā)者在構(gòu)建工具,如 webpack,中構(gòu)建出了兩個 JS 文件,一個文件包含了項目所有的依賴模塊,比如 Angular, jQuery, lodash 等等,名為 vendor.js,另一個 JS 文件則全部是業(yè)務(wù)相關(guān)的代碼,名為 app.js。顯然,app.js 的加載依賴 vendor.js 的先行加載。如果先加載并執(zhí)行 app.js 的話,會由于全局環(huán)境中還不存在 Angualr 和 jQuery 這些庫或框架而報錯。

思考

問題描述完了,這種問題實際上也是很常見的問題,但在 bowl 的場景下,需要結(jié)合 bowl 的實現(xiàn)原理來進行分析。

在 bowl 的內(nèi)部,需要加載的資源分為幾種類型,一種是存在于和頁面同域下的資源且用戶需要緩存的,它會使用 XMLHttpRequest 發(fā)起請求的方式獲取資源內(nèi)容,另一種純第三方資源,比如在頁面中直接引用第三方 CDN 域名上的資源,如 jQuery 等都提供了 CDN 的資源鏡像,它屬于跨域資源,無法用 XMLHttpRequest 的方式獲取,那么只能退一步使用常規(guī)的 HTML 標(biāo)簽的方式請求數(shù)據(jù)。另外這里用 promise 包裝了標(biāo)簽加載的代碼,在 onload 事件中進行 resolve 操作,將同步的加載過程用異步的方式呈現(xiàn),目的是和異步請求資源內(nèi)容的方式保持一致,保證流程可控。第三種和第一種相似,不同點在于用戶聲明不需要緩存,這種類型也使用了和第二種相同的加載方式。

對于資源間依賴關(guān)系的聲明,首先進行的是 API 的設(shè)計,這里采用了比較簡單的方式:

bowl.add[{
  key: "vendor",
  url: "vendor.js"
}, {
  key: "app",
  url: "app.js",
  dependencies: ["vendor"]
}]

如果 a 資源依賴 b 資源,那么在 a 資源的 dependencies 屬性中寫入一個數(shù)組,里面包含依賴資源的 key 名即可。

bowl 中執(zhí)行資源請求并注入的方法是 inject(),那么在調(diào)用這個方法的時候首先要做的就是分析資源的依賴關(guān)系,一開始我沒有什么特別好的想法,為了鍛煉一下自己的思維能力也沒有谷歌什么現(xiàn)成的解決方案,就在紙上隨手寫寫畫畫:

不行,太抽象了,換一種方式:

看起來有點眼熟,原來是典型的有向圖數(shù)據(jù)結(jié)構(gòu),想到這個就有思路了(旁邊是歸類的依賴類型,這個稍后說)。

有向圖

有向圖是圖數(shù)據(jù)結(jié)構(gòu)的一種,在圖中,分為兩種數(shù)據(jù)單元,一種是頂點,另一種是邊。其中邊又分為兩種類型,無向的和有向的,只包含無向邊的叫無向圖,包含有向邊的就叫有向圖了。在當(dāng)前的場景中,資源之間的依賴是單向的,a 依賴 b 但不代表 b 依賴 a,因此有向圖是合適的數(shù)據(jù)結(jié)構(gòu)。

在這里的有向圖中,每個資源對應(yīng)有向圖中的頂點,資源間的依賴關(guān)系則是兩個頂點之間的邊了。如果 a 依賴 b,那么在 a 和 b 之間就有一條由 b 指向 a 的邊。

在 JS 中實現(xiàn)一個簡單的有向圖數(shù)據(jù)結(jié)構(gòu)還是挺簡單的:

function Graph() {
  this.vertices = {}
}

用一個對象包含所有的頂點,資源的 key 作為這個頂點的鍵值,每個頂點還需要有各自的屬性:

name: 頂點的名字,對應(yīng)資源名稱

prev: 頂點的入度,這里表示該資源依賴其他資源的數(shù)量

next: 頂點的出度,這里表示該資源被多少其他資源依賴

adjList: 以該頂點為起點的邊指向的頂點列表,這里就是依賴本資源的其他資源名稱列表

有了這個就可以實現(xiàn) addVertexaddEdge 方法了:

Graph.prototype.addVertex = function(v) {
  // 檢測頂點是否已存在
  if (isObject(this.vertices[v])) {
    return
  }
  var newVertex = {
    name: v,
    prev: 0,
    next: 0,
    adjList: []
  }
  this.vertices[v] = newVertex
}

Graph.prototype.addEdge = function(begin, end) {
  // 檢查兩個頂點是否存在
  if (!this.vertices[begin] ||
      !this.vertices[end] ||
       this.vertices[begin].adjList.indexOf(end) > -1) {
    return
  }
  ++this.vertices[begin].next
  this.vertices[begin].adjList.push(end)
  ++this.vertices[end].prev
}

有了這兩個方法,在調(diào)用 bowl.inject 的時候可以根據(jù)已添加的資源生成一個描述資源依賴關(guān)系的圖數(shù)據(jù)結(jié)構(gòu)了,舉例如下:

Graph {
  vertices: {
    a: {
      name: "a",
      next: 0,
      prev: 1,
      adjList: []
    },
    b: {
      name: "b",
      next: 1,
      prev: 0,
      adjList: ["c"]
    },
    c: {
      name: "c",
      next: 1,
      prev: 1,
      adjList: ["a"]
    }
  }
}
分析圖中的環(huán)

環(huán)在有向圖中表示有向邊構(gòu)成的環(huán)路,兩個頂點之間存在互相指向?qū)Ψ降倪叺那闆r也稱為環(huán)。在 bowl 中如果出現(xiàn)了環(huán),就表示資源之前出現(xiàn)了循環(huán)依賴或相互依賴的情況。而這種情況是不應(yīng)該出現(xiàn)的,如果出現(xiàn)了需要報錯。因此,我們首先要做的是分析圖中是否存在環(huán)。

對于環(huán)的檢測,常用的算法是深度優(yōu)先遍歷,例如在 Angular 中注入器檢測循環(huán)依賴用的就是這個算法。

實際上,在 bowl 中我使用了另一種名為 Kahn 算法的環(huán)檢測的算法,它是拓?fù)渑判蛩惴ǖ囊环N,相比于深度優(yōu)先遍歷算法來說它比較直觀。它的原理歸納起來有三點:

遍歷圖中所有的頂點,將所有入度為 0 的頂點依次入棧

如果棧非空,則從棧頂取出頂點,刪除該頂點以及以該頂點為起點的邊,如果刪除的邊的另一個頂點入度為 0 了,則把它入棧

最后,如果圖中還存在頂點,則表示圖中有環(huán)

這個算法結(jié)合業(yè)務(wù)場景會很好理解,入度為 0 的頂點表示其對應(yīng)的資源沒有任何依賴,將頂點和邊刪除后剩下的入度為 0 的頂點表示只依賴前一個資源的資源,前一個資源加載后,當(dāng)前資源就可以加載了,以此類推。最后如果還有頂點被剩下的話,說明可順序加載的資源都加載完了還有無法加載的資源,這些資源之間一定存在循環(huán)依賴的關(guān)系。

Kahn 算法寫成代碼如下:

Graph.prototype.hasCycle = function() {
  const cycleTestStack = []
  const vertices = merge({}, this.vertices) // 復(fù)制一份數(shù)據(jù)進行操作
  let popVertex = null

  for (let k in vertices) {
    if (vertices[k].prev === 0) { // 入度為 0 的資源入棧
      cycleTestStack.push(vertices[k])
    }
  }
  while (cycleTestStack.length > 0) {
    popVertex = cycleTestStack.pop()
    delete vertices[popVertex.name]
    popVertex.adjList.forEach(nextVertex => {
      --vertices[nextVertex].prev
      if (vertices[nextVertex].prev === 0) {
        cycleTestStack.push(vertices[nextVertex])
      }
    })
  }
  return Object.keys(vertices).length > 0
}
計算加載順序

如果圖能夠通過環(huán)檢測,說明其中的資源不存在循環(huán)依賴關(guān)系,下一步就是要計算資源的加載順序了。很明顯,這里要做的是圖的遍歷,上面提到的深度遍歷也是可以用的,但是這是否是最好的方式呢?

我認(rèn)為不是的,假設(shè)有依賴關(guān)系的資源如下:

a<---b<---c<---d
     ^
     |
     -----e<---f

如果用深度遍歷來進行資源加載的話,加載順序?qū)?a->b->c->d->e->f,每個資源順序加載。而這里 bowl 加載資源的行為都是被包裝在 promise 中的,請求也可以并發(fā)出去,并發(fā)的多個請求只要通過 Promise.all 取到 resolve 的時間點就可以保證全部加載完成了,所以,較為理想的加載順序應(yīng)該是 a->b->[c, e]->[d, f]

要得到這樣的結(jié)果,實際上可以直接利用 Kahn 算法的思想,每次遍歷過濾出一批沒有依賴未加載的資源,最后得到一個分批次的加載順序。

要得到上面提到的分批加載順序,可以通過以下代碼:

Graph.prototype.getGroup = function() {
  if (this.hasCycle()) { // 有環(huán)則報錯
    throw new Error("There are cycles in resource"s dependency relation")
    return
  }
  const result = []
  const graphCopy = new Graph(this.vertices)
  while (Object.keys(graphCopy.vertices).length) {
    const noPrevVertices = []
    for (let k in graphCopy.vertices) {
      if (graphCopy.vertices[k].prev === 0) {
        noPrevVertices.push(k)
      }
    }
    if (noPrevVertices.length) {
      result.push(noPrevVertices)
      noPrevVertices.forEach(vertex => {
        graphCopy.vertices[vertex].adjList.forEach(next => {
          --graphCopy.vertices[next].prev
        })
        delete graphCopy.vertices[vertex]
      })
    }
  }
  return result
}

當(dāng)然除了深度優(yōu)先和 Kahn 算法,廣度優(yōu)先也是可用的算法,在這幾種算法中,DFS 和 BFS 的時間復(fù)雜度都是 O(n^2)(這里的代碼中使用的可以看成是一個鄰接矩陣),如果用鄰接鏈表的方式表示圖的話,時間復(fù)雜度將會是 O(n+e)。對于 Kahn 算法,時間復(fù)雜度明顯是 O(n^2)。既然這里用了鄰接矩陣的方式,時間復(fù)雜度都是一樣的,效率上差別不大。而且在前端資源的加載場景下,不會出現(xiàn)那么多的資源要去分析,這點差別是可以忽略的。

多個異步任務(wù)的順序執(zhí)行

通過 getGroup 方法,取得了一個描述加載順序的二維數(shù)組:[["a"], ["b"], ["c", "e"], ["d", "f"]]。下面要做的是加載它們,對于這個數(shù)組中的每個子數(shù)組中的資源,它們都是可以同時加載的,把這塊邏輯抽出來,返回一個 promise 即可:

const batchFetch = (group) => {
  const fetches = []
  group.forEach(item => {
    fetches.push(this.injector.inject(this.ingredients.find(ingredient => ingredient.key === item)))
  })
  return Promise.all(fetches)
}

這段代碼的具體細(xì)節(jié)就省略了,最后通過一個 Promise.all 返回一個包裝后的 promise,group 中的資源全部加載完成后這個 promise 會被 resolve。

這個時候問題就來了,對于這個二維數(shù)組,不能簡單的將每個子數(shù)組都一股腦傳入 batchFetch 方法中,因為傳入 Promise 構(gòu)造函數(shù)中的函數(shù)是會立即執(zhí)行的,而后一個子數(shù)組中的資源必需在前一個 batchFetch promise 被 resolve 后才能加載。同時,二維數(shù)組的長度也是不定的,更不能窮舉。

這里就是一個典型的多個 promise 異步任務(wù)的場景,每個異步任務(wù)的構(gòu)建依賴前一個任務(wù)的完成狀態(tài)。一開始由于我對異步編程不是特別熟悉,有點想不通,在 bluebird 這個 promise 庫中找到了 Promise.reducePromise.each 這兩個靜態(tài)方法是可以解決問題的,但是對于 bowl 這么一個小型庫來說,引入一個 bluebird 有點殺雞用牛刀的感覺,不太合適。

最終通過查 Promise/A+ 規(guī)范以及一些嘗試,找到了一個解決方案,其實很簡單。對于 promise 中的 then 回調(diào)函數(shù),它返回的是一個新的 Promise,而每個 then 中的 onFulfill 回調(diào)都會在前一個 Promise resolve 后執(zhí)行。利用這個特性,只需要遍歷原二維數(shù)組,將每個 batchFetch(group) 放在一個 then 中的 onFulfill 函數(shù)中執(zhí)行并返回即可(因為 batchFetch 的返回值就是一個 promise),有一種惰性執(zhí)行的感覺。

let ret = Promise.resolve() // 強行開啟一個 promise 鏈
resolvedIngredients.forEach(group => {
  ret = ret.then(function() {
    return batchFetch(group)
  })
})
return ret

這樣,最終 ret 被 resolve 的時候,說明所有資源都按順序加載完了。

參考資料:

「學(xué)習(xí) JavaScript 數(shù)據(jù)結(jié)構(gòu)與算法」,人民郵電出版社

http://www.cnblogs.com/TenosDoIt/p/3644225.html

http://shmilyaw-hotmail-com.iteye.com/blog/2116275

https://promisesaplus.com/

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

轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/91301.html

相關(guān)文章

  • bowl - 輕量級的前端資源緩存加載器

    摘要:另外,它不僅可以管理前端資源的緩存,在不需要緩存的時候也可以作為一個普通的加載器來使用,頁面中用到的和資源都可以用它來加載。 現(xiàn)在單頁應(yīng)用越來越多,前端能做的事也越來越多,但隨之而來的問題是一個單頁應(yīng)用的 CSS 和 JavaScript 代碼的體積也越來越大。應(yīng)用每次初始化的時候都要加載這些龐大的資源,雖然瀏覽器有自己的緩存機制,但首先它并不一定靠譜,其次即使緩存有效,每次加載資源時...

    longshengwang 評論0 收藏0
  • bowl - 輕量級的前端資源緩存加載器

    摘要:另外,它不僅可以管理前端資源的緩存,在不需要緩存的時候也可以作為一個普通的加載器來使用,頁面中用到的和資源都可以用它來加載。 現(xiàn)在單頁應(yīng)用越來越多,前端能做的事也越來越多,但隨之而來的問題是一個單頁應(yīng)用的 CSS 和 JavaScript 代碼的體積也越來越大。應(yīng)用每次初始化的時候都要加載這些龐大的資源,雖然瀏覽器有自己的緩存機制,但首先它并不一定靠譜,其次即使緩存有效,每次加載資源時...

    changfeng1050 評論0 收藏0
  • bowl - 輕量級的前端資源緩存加載器

    摘要:另外,它不僅可以管理前端資源的緩存,在不需要緩存的時候也可以作為一個普通的加載器來使用,頁面中用到的和資源都可以用它來加載。 現(xiàn)在單頁應(yīng)用越來越多,前端能做的事也越來越多,但隨之而來的問題是一個單頁應(yīng)用的 CSS 和 JavaScript 代碼的體積也越來越大。應(yīng)用每次初始化的時候都要加載這些龐大的資源,雖然瀏覽器有自己的緩存機制,但首先它并不一定靠譜,其次即使緩存有效,每次加載資源時...

    niuxiaowei111 評論0 收藏0
  • 使用betty.js將Javascript代碼存儲到LocalStorage

    摘要:前言是一款極輕量的使用存儲代碼的工具。跨域緩存會默認(rèn)使用請求待緩存的資源,如果跨域則會請求出錯。會以格式存儲代碼,例如所以和有一個發(fā)生變化,都會引起重新請求并存儲。 前言 betty.js是一款極輕量的、使用localStorage存儲Javascript代碼的工具。市面上已經(jīng)有很多類似的工具:比如餓了么團隊最近發(fā)布的bowl.js,微信團隊的MOON(未開源),以及這個想法的鼻祖ba...

    buildupchao 評論0 收藏0
  • 使用betty.js將Javascript代碼存儲到LocalStorage

    摘要:前言是一款極輕量的使用存儲代碼的工具。跨域緩存會默認(rèn)使用請求待緩存的資源,如果跨域則會請求出錯。會以格式存儲代碼,例如所以和有一個發(fā)生變化,都會引起重新請求并存儲。 前言 betty.js是一款極輕量的、使用localStorage存儲Javascript代碼的工具。市面上已經(jīng)有很多類似的工具:比如餓了么團隊最近發(fā)布的bowl.js,微信團隊的MOON(未開源),以及這個想法的鼻祖ba...

    fish 評論0 收藏0

發(fā)表評論

0條評論

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