當前位置:
首頁 > 知識 > whatwg-fetch源碼分析

whatwg-fetch源碼分析

fetch 是什麼

XMLHttpRequest的最新替代技術

fetch優點
  • 介面更簡單、簡潔,更加語義化
  • 基於promise,更加好的流程化控制,可以不斷then把參數傳遞,外加 async/await,非同步變同步的代碼書寫風格
  • 利於同構,isomorphic-fetch是對 whatwg-fetch和node-fetch的一種封裝,你一份代碼就可以在兩種環境下跑起來了
  • 新的web api很多內置支持fetch,比如 service worker

fetch 缺點

  • 兼容性
  • 不支持progress事件(可以藉助 response.body.getRender方法來實現)
  • 默認不帶cookie
  • 某些錯誤的http狀態下如400、500等不會reject,相反它會被resolve
  • 不支持timeout處理
  • 不支持jsonp,當然可以引入 fetch-jsonp來支持

這些缺點,後面的參考裡面有各種解決方案

fetch兼容性(2017-08-08):

whatwg-fetch源碼分析

  • 如果瀏覽器不支持promise,可以引入 es6-promise或者promise。
  • 如果希望IE8也支持,可以參考 使用fetch遇到過的坑或者使用fetch-ie8

fetch參數

上面你對fetch有基本的了解了,而且提供了不少的鏈接解惑,那麼我們進入正題,whatwg-fetch源碼分析

依舊是先刪除無用的代碼,

(function (self) {
"use strict";
if (self.fetch) {
return
}

// 封裝的 Headers,支持的方法參考https://developer.mozilla.org/en-US/docs/Web/API/Headers
function Headers(headers) {
......
}

//方法參考:https://developer.mozilla.org/en-US/docs/Web/API/Body
function Body {
......
}

// 請求的Request對象 ,https://developer.mozilla.org/en-US/docs/Web/API/Request
// cache,context,integrity,redirect,referrerPolicy 在MDN定義中是存在的
function Request(input, options) {
......
}

Body.call(Request.prototype) //把Body方法屬性綁到 Reques.prototype

function Response(bodyInit, options) {
}

Body.call(Response.prototype) //把Body方法屬性綁到 Reques.prototype

self.Headers = Headers //暴露Headers
self.Request = Request //暴露Request
self.Response = Response //暴露Response

self.fetch = function (input, init) {
return new Promise(function (resolve, reject) {
var request = new Request(input, init) //初始化request對象
var xhr = new XMLHttpRequest // 初始化 xhr

xhr.onload = function { //請求成功,構建Response,並resolve進入下一階段
var options = {
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders || "")
}
options.url = "responseURL" in xhr ? xhr.responseURL : options.headers.get("X-Request-URL")
var body = "response" in xhr ? xhr.response : xhr.responseText
resolve(new Response(body, options))
}

//請求失敗,構建Error,並reject進入下一階段
xhr.onerror = function {
reject(new TypeError("Network request failed"))
}

//請求超時,構建Error,並reject進入下一階段
xhr.ontimeout = function {
reject(new TypeError("Network request failed"))
}

// 設置xhr參數
xhr.open(request.method, request.url, true)

// 設置 credentials
if (request.credentials === "include") {
xhr.withCredentials = true
} else if (request.credentials === "omit") {
xhr.withCredentials = false
}

// 設置 responseType
if ("responseType" in xhr && support.blob) {
xhr.responseType = "blob"
}

// 設置Header
request.headers.forEach(function (value, name) {
xhr.setRequestHeader(name, value)
})
// 發送請求
xhr.send(typeof request._bodyInit === "undefined" ? null : request._bodyInit)
})
}
//標記是fetch是polyfill的,而不是原生的
self.fetch.polyfill = true
})(typeof self !== "undefined" ? self : this); // IIFE函數的參數,不用window,web worker, service worker裡面也可以使用

簡單分析一下

  • 如果自身支持fetch,直接返回,用自身的

  • 內部核心 Headers, Body, Request, Response,

    • Request和Resonse原型上有Body的方法屬性,或者說,繼承了

    • Headers,Request ,Reponse暴露到全局

  • fetch本質就是對XMLHttpRequest 請求的封裝

這麼一看其實到沒什麼了,不過完整代碼裡面有一些東西還是提一下(後面的參考都有鏈接)

  • Symbol, Iterator : ES6裡面很多集合是自帶默認Iterator的,作用就是在 let...of,數組解構,新Set,Map初始化等情況會被調用。

  • DataView , TypedArray:都是對TypeArray讀寫的API

  • Blob,FileReader :File API,這個也沒啥多說的

  • URLSearchParams: 這個支持度還不高,用來解析和構建 URL Search 參數的,例如 new URLSearchParams(window.location.search).get("a")

這面重點解析幾個重點函數和方法,其他的相對容易

iteratorFor

在定義中,Headers實例,headers.keys, headers.values, headers.entries返回的都是Iterator, 下面代碼讀起來可能有點繞,

你這樣理解,定義iterator 是保證能使用next方法來遍歷

定義iterator[Symbol.iterator] 是設置默認 Iterator,能使用 let...of,Array.from,數組解構等相對高級一些方法訪問到

// 枚舉器, http://es6.ruanyifeng.com/#docs/iterator

// 覺得可以如下 ,同樣支持 next 和 for ...of 等形式訪問 ,之後才是不支持iterable的情況,添加next方法來訪問
// if ((support.iterable && items[Symbol.iterator]) {
// return items[Symbol.iterator]
// }

function iteratorFor(items) { // 這裡你就可以 res.headers.keys.next.value這樣調用 var iterator = { next: function { var value = items.shift return { done: value === undefined, value: value } } } if (support.iterable) { // 添加默認Iterator // for...of,解構賦值,擴展運算符,yield*,Map, Set, WeakMap, WeakSet,Promise.all,Promise.race都會調用默認Iterator iterator[Symbol.iterator] = function { return iterator } } // 到這裡就支持了兩種訪問形式了 // res.headers.keys.next.value // for(let key in headers.keys) return iterator }

Body.call

實現繼承,把body的方法屬性綁定指定對象原型

Body.call(Request.prototype)
Body.call(Response.prototype)

這兩個理解上,就基本可以無大礙了,那我貼出完整帶注釋的代碼

(function (self) {
"use strict";

//如果自身支持fetch,直接返回原生的fetch
if (self.fetch) {
// return
}

// 一些功能檢測
var support = {
searchParams: "URLSearchParams" in self, // queryString 處理函數,https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams,http://caniuse.com/#search=URLSearchParams
iterable: "Symbol" in self && "iterator" in Symbol, // Symbol(http://es6.ruanyifeng.com/#docs/symbol)E6新數據類型,表示獨一無二的值 和 iterator枚舉
blob: "FileReader" in self && "Blob" in self && (function {
try {
new Blob
return true
} catch (e) {
return false
}
}), // Blob 和 FileReader
formData: "FormData" in self, // FormData
arrayBuffer: "ArrayBuffer" in self // ArrayBuffer 二進位數據存儲
}

// 支持的 ArrayBuffer類型
if (support.arrayBuffer) {
var viewClasses = [
"[object Int8Array]",
"[object Uint8Array]",
"[object Uint8ClampedArray]",
"[object Int16Array]",
"[object Uint16Array]",
"[object Int32Array]",
"[object Uint32Array]",
"[object Float32Array]",
"[object Float64Array]"
]

// 檢查是不是DataView,DataView是來讀寫ArrayBuffer的 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
var isDataView = function (obj) {
return obj && DataView.prototype.isPrototypeOf(obj)
}

// 檢查是不是有效的ArrayBuffer view,TypedArray均返回true ArrayBuffer.isView(new ArrayBuffer(10)) 為false, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/isView
var isArrayBufferView = ArrayBuffer.isView || function (obj) {
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
}
}

// 檢查header name,並轉為小寫
function normalizeName(name) {
// 不是字元串,轉為字元串
if (typeof name !== "string") {
name = String(name)
}
// 不以 a-z 0-9 -#$%*+.^_`|~ 開頭,拋出錯誤
if (/[^a-z0-9-#$%&"*+.^_`|~]/i.test(name)) {
throw new TypeError("Invalid character in header field name")
}
//轉為小寫
return name.toLowerCase
}

// 轉換header的值
function normalizeValue(value) {
if (typeof value !== "string") {
value = String(value)
}
return value
}

// 枚舉器, http://es6.ruanyifeng.com/#docs/iterator
// 覺得可以如下 ,同樣支持 next 和 for ...of 等形式訪問 ,之後才是不支持iterable的情況,添加next方法來訪問
// if ((support.iterable && items[Symbol.iterator]) {
// return items[Symbol.iterator]
// }
function iteratorFor(items) {
// 這裡你就可以 res.headers.keys.next.value這樣調用
var iterator = {
next: function {
var value = items.shift
return { done: value === undefined, value: value }
}
}

if (support.iterable) {
// 添加默認Iterator
// for...of,解構賦值,擴展運算符,yield*,Map, Set, WeakMap, WeakSet,Promise.all,Promise.race都會調用默認Iterator
iterator[Symbol.iterator] = function {
return iterator
}
}

// 到這裡就支持了兩種訪問形式了
// res.headers.keys.next.value
// for(let key in headers.keys)
return iterator
}

// 封裝的 Headers,支持的方法參考https://developer.mozilla.org/en-US/docs/Web/API/Headers
function Headers(headers) {
this.map = {} // headers 最終存儲的地方

if (headers instanceof Headers) { // 如果已經是 Headers的實例,複製鍵值
headers.forEach(function (value, name) {
this.append(name, value)
}, this) // this修改forEach執行函數上下文為當前上下文,就可以直接調用append方法了
} else if (Array.isArray(headers)) { // 如果是數組,[["Content-Type":""],["Referer",""]]
headers.forEach(function (header) {
this.append(header[0], header[1])
}, this)
} else if (headers) {
// 對象 {"Content-Type":"",Referer:""}
Object.getOwnPropertyNames(headers).forEach(function (name) {
this.append(name, headers[name])
}, this)
}
}

// 添加或者追加Header
Headers.prototype.append = function (name, value) {
name = normalizeName(name)
value = normalizeValue(value)
var oldValue = this.map[name]
// 支持 append, 比如 Accept:text/html ,後來 append("Accept","application/xhtml+xml") 那麼最終 Accept:"text/html,application/xhtml+xml"
this.map[name] = oldValue ? oldValue + "," + value : value
}

//刪除名為name的Header
Headers.prototype["delete"] = function (name) {
delete this.map[normalizeName(name)]
}

//獲得名為Name的Header
Headers.prototype.get = function (name) {
name = normalizeName(name)
return this.has(name) ? this.map[name] : null
}

//查詢時候有名為name的Header
Headers.prototype.has = function (name) {
return this.map.hasOwnProperty(normalizeName(name))
}
//設置或者覆蓋名為name,值為vaue的Header
Headers.prototype.set = function (name, value) {
this.map[normalizeName(name)] = normalizeValue(value)
}
//遍歷Headers
Headers.prototype.forEach = function (callback, thisArg) {
//遍歷屬性
//我覺得也挺不錯 Object.getOwnPropertyNames(this.map).forEach(function(name){ callback.call(thisArg, this.map[name], name, this) },this)
for (var name in this.map) {
//檢查是不是自己的屬性
if (this.map.hasOwnProperty(name)) {
//調用
callback.call(thisArg, this.map[name], name, this)
}
}
}

// 所有的鍵,keys, values, entries, res.headers返回的均是 iterator
Headers.prototype.keys = function {
var items =
this.forEach(function (value, name) { items.push(name) })
return iteratorFor(items)
}
// 所有的值,keys, values, entries, res.headers返回的均是 iterator
Headers.prototype.values = function {
var items =
this.forEach(function (value) { items.push(value) })
return iteratorFor(items)
}
// 所有的entries,格式是這樣 [[name1,value1],[name2,value2]],keys, values, entries, res.headers返回的均是 iterator
Headers.prototype.entries = function {
var items =
this.forEach(function (value, name) { items.push([name, value]) })
return iteratorFor(items)
}

//設置Headers原型默認的Iterator,keys, values, entries, res.headers返回的均是 iterator
if (support.iterable) {
Headers.prototype[Symbol.iterator] = Headers.prototype.entries
}

//是否已經消費/讀取過,如果讀取過,會直接到catch或者error處理函數
function consumed(body) {
if (body.bodyUsed) {
return Promise.reject(new TypeError("Already read"))
}
body.bodyUsed = true
}

// FileReader讀取完畢
function fileReaderReady(reader) {
return new Promise(function (resolve, reject) {
reader.onload = function {
resolve(reader.result)
}
reader.onerror = function {
reject(reader.error)
}
})
}

// 讀取blob為ArrayBuffer對象,https://www.w3.org/TR/FileAPI/#dfn-filereader
function readBlobAsArrayBuffer(blob) {
var reader = new FileReader
var promise = fileReaderReady(reader)
reader.readAsArrayBuffer(blob)
return promise
}
// 讀取blob為文本,https://www.w3.org/TR/FileAPI/#dfn-filereader
function readBlobAsText(blob) {
var reader = new FileReader
var promise = fileReaderReady(reader)
reader.readAsText(blob)
return promise
}

// ArrayBuffer讀為文本
function readArrayBufferAsText(buf) {
var view = new Uint8Array(buf)
var chars = new Array(view.length)

for (var i = 0; i < view.length; i++) {
chars[i] = String.fromCharCode(view[i])
}
return chars.join("")
}

//克隆ArrayBuffer
function bufferClone(buf) {
if (buf.slice) { //支持 slice,直接slice(0)複製,數據基本都是這樣複製的
return buf.slice(0)
} else {
//新建填充模式複製
var view = new Uint8Array(buf.byteLength)
view.set(new Uint8Array(buf))
return view.buffer
}
}

//方法參考:https://developer.mozilla.org/en-US/docs/Web/API/Body
function Body {
this.bodyUsed = false

this._initBody = function (body) {
// 把最原始的數據存下來
this._bodyInit = body
// 判斷body數據類型,然後存下來
if (!body) {
this._bodyText = ""
} else if (typeof body === "string") {
this._bodyText = body
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
this._bodyBlob = body
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
this._bodyFormData = body
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this._bodyText = body.toString //數據格式是這樣的 a=1&b=2&c=3
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
// ArrayBuffer一般是通過DataView或者各種Float32Array,Uint8Array來操作的, https://hacks.mozilla.org/2017/01/typedarray-or-dataview-understanding-byte-order/
// 如果是DataView, DataView的數據是存在 DataView.buffer上的
this._bodyArrayBuffer = bufferClone(body.buffer) // 複製ArrayBuffer
// IE 10-11 can"t handle a DataView body.
this._bodyInit = new Blob([this._bodyArrayBuffer]) // 重新設置_bodyInt
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
// ArrayBuffer一般是通過DataView或者各種Float32Array,Uint8Array來操作的,
// https://hacks.mozilla.org/2017/01/typedarray-or-dataview-understanding-byte-order/
this._bodyArrayBuffer = bufferClone(body)
} else {
throw new Error("unsupported BodyInit type")
}

// 設置content-type
if (!this.headers.get("content-type")) {
if (typeof body === "string") {
this.headers.set("content-type", "text/plain;charset=UTF-8")
} else if (this._bodyBlob && this._bodyBlob.type) {
this.headers.set("content-type", this._bodyBlob.type)
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
this.headers.set("content-type", "application/x-www-form-urlencoded;charset=UTF-8")
}
}
}

if (support.blob) {
// 使用 fetch(...).then(res=>res.blob)
this.blob = function {
//標記為已經使用
var rejected = consumed(this)
if (rejected) {
return rejected
}

// resolve,進入then
if (this._bodyBlob) {
return Promise.resolve(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(new Blob([this._bodyArrayBuffer]))
} else if (this._bodyFormData) {
throw new Error("could not read FormData body as blob")
} else {
return Promise.resolve(new Blob([this._bodyText]))
}
}
// 使用 fetch(...).then(res=>res.arrayBuffer)
this.arrayBuffer = function {
if (this._bodyArrayBuffer) {
return consumed(this) || Promise.resolve(this._bodyArrayBuffer)
} else {
return this.blob.then(readBlobAsArrayBuffer) //如果有blob,讀取成ArrayBuffer
}
}
}

// 使用 fetch(...).then(res=>res.text)
this.text = function {
var rejected = consumed(this)
if (rejected) {
return rejected
}

if (this._bodyBlob) {
return readBlobAsText(this._bodyBlob)
} else if (this._bodyArrayBuffer) {
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
} else if (this._bodyFormData) {
throw new Error("could not read FormData body as text")
} else {
return Promise.resolve(this._bodyText)
}
}

// 使用 fetch(...).then(res=>res.formData)
if (support.formData) {
this.formData = function {
return this.text.then(decode)
}
}

// 使用 fetch(...).then(res=>res.json)
this.json = function {
return this.text.then(JSON.parse)
}

return this
}

// HTTP methods whose capitalization should be normalized
var methods = ["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"]

// 方法名大寫
function normalizeMethod(method) {
var upcased = method.toUpperCase
return (methods.indexOf(upcased) > -1) ? upcased : method
}

// 請求的Request對象 ,https://developer.mozilla.org/en-US/docs/Web/API/Request
// cache,context,integrity,redirect,referrerPolicy 在MDN定義中是存在的
function Request(input, options) {
options = options || {}
var body = options.body

//如果已經是Request的實例,解析賦值
if (input instanceof Request) {
if (input.bodyUsed) {
throw new TypeError("Already read")
}
this.url = input.url //請求的地址
this.credentials = input.credentials //登陸憑證
if (!options.headers) { //headers
this.headers = new Headers(input.headers)
}
this.method = input.method //請求方法 GET,POST......
this.mode = input.mode // same-origin,cors,no-cors
if (!body && input._bodyInit != null) { //標記Request已經使用
body = input._bodyInit
input.bodyUsed = true
}
} else {
this.url = String(input)
}

this.credentials = options.credentials || this.credentials || "omit"
if (options.headers || !this.headers) {
this.headers = new Headers(options.headers)
}
this.method = normalizeMethod(options.method || this.method || "GET")
this.mode = options.mode || this.mode || null //same-origin,cors,no-cors
this.referrer = null

if ((this.method === "GET" || this.method === "HEAD") && body) {
throw new TypeError("Body not allowed for GET or HEAD requests")
}
this._initBody(body) //解析值 和設置content-type
}

// 克隆
Request.prototype.clone = function {
return new Request(this, { body: this._bodyInit })
}

// body存為 FormData
function decode(body) {
var form = new FormData
body.trim.split("&").forEach(function (bytes) {
if (bytes) {
var split = bytes.split("=")
var name = split.shift.replace(/+/g, " ")
var value = split.join("=").replace(/+/g, " ")
form.append(decodeURIComponent(name), decodeURIComponent(value))
}
})
return form
}

// 用於接續 xhr.getAllResponseHeaders, 數據格式
//Cache-control: private
//Content-length:554
function parseHeaders(rawHeaders) {
var headers = new Headers
// Replace instances of
and
followed by at least one space or horizontal tab with a space
// https://tools.ietf.org/html/rfc7230#section-3.2
var preProcessedHeaders = rawHeaders.replace(/
?
[ ]+/g, " ")
preProcessedHeaders.split(/
?
/).forEach(function (line) {
var parts = line.split(":")
var key = parts.shift.trim
if (key) {
var value = parts.join(":").trim
headers.append(key, value)
}
})
return headers
}

Body.call(Request.prototype) //把Body方法屬性綁到 Reques.prototype

// Reponse對象,https://developer.mozilla.org/en-US/docs/Web/API/Response
function Response(bodyInit, options) {
if (!options) {
options = {}
}

this.type = "default"
this.status = options.status === undefined ? 200 : options.status
this.ok = this.status >= 200 && this.status < 300 // 200 - 300 ,https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
this.statusText = "statusText" in options ? options.statusText : "OK"
this.headers = new Headers(options.headers)
this.url = options.url || ""
this._initBody(bodyInit) // 解析值和設置content-type
}

Body.call(Response.prototype) //把Body方法屬性綁到 Reques.prototype

// 克隆Response
Response.prototype.clone = function {
return new Response(this._bodyInit, {
status: this.status,
statusText: this.statusText,
headers: new Headers(this.headers),
url: this.url
})
}

//返回一個 error性質的Response,靜態方法
Response.error = function {
var response = new Response(null, { status: 0, statusText: "" })
response.type = "error"
return response
}

var redirectStatuses = [301, 302, 303, 307, 308]

// 重定向,本身並不產生實際的效果,靜態方法,https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect
Response.redirect = function (url, status) {
if (redirectStatuses.indexOf(status) === -1) {
throw new RangeError("Invalid status code")
}

return new Response(null, { status: status, headers: { location: url } })
}

self.Headers = Headers //暴露Headers
self.Request = Request //暴露Request
self.Response = Response //暴露Response

self.fetch = function (input, init) {
return new Promise(function (resolve, reject) {
var request = new Request(input, init) //初始化request對象
var xhr = new XMLHttpRequest // 初始化 xhr

xhr.onload = function { //請求成功,構建Response,並resolve進入下一階段
var options = {
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders || "")
}
options.url = "responseURL" in xhr ? xhr.responseURL : options.headers.get("X-Request-URL")
var body = "response" in xhr ? xhr.response : xhr.responseText
resolve(new Response(body, options))
}

//請求失敗,構建Error,並reject進入下一階段
xhr.onerror = function {
reject(new TypeError("Network request failed"))
}

//請求超時,構建Error,並reject進入下一階段
xhr.ontimeout = function {
reject(new TypeError("Network request failed"))
}

// 設置xhr參數
xhr.open(request.method, request.url, true)

// 設置 credentials
if (request.credentials === "include") {
xhr.withCredentials = true
} else if (request.credentials === "omit") {
xhr.withCredentials = false
}

// 設置 responseType
if ("responseType" in xhr && support.blob) {
xhr.responseType = "blob"
}

// 設置Header
request.headers.forEach(function (value, name) {
xhr.setRequestHeader(name, value)
})
// 發送請求
xhr.send(typeof request._bodyInit === "undefined" ? null : request._bodyInit)
})
}
//標記是fetch是polyfill的,而不是原生的
self.fetch.polyfill = true
})(typeof self !== "undefined" ? self : this); // IIFE函數的參數,不用window,web worker, service worker裡面也可以使用

小結:

  • 可以看出,有些屬性是沒有實現的,但是一般的請求足以
  • Response.body 這種ReadableStream沒有實現,自然就沒有fetch原生處理progress的方法

fetch("/").then(response => {
// response.body is a readable stream.
// Calling getReader gives us exclusive access to the stream"s content
var reader = response.body.getReader;
var bytesReceived = 0;

// read returns a promise that resolves when a value has been received
reader.read.then(function processResult(result) {
// Result objects contain two properties:
// done - true if the stream has already given you all its data.
// value - some data. Always undefined when done is true.
if (result.done) {
console.log("Fetch complete");
return;
}

// result.value for fetch streams is a Uint8Array
bytesReceived += result.value.length;
console.log("Received", bytesReceived, "bytes of data so far");

// Read some more, and call this function again
return reader.read.then(processResult);
});
});

參考:

Fetch Standard

File API

TypedArray or DataView: Understanding byte order

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 科技優家 的精彩文章:

一篇關於Python裝飾器的博文
「Git」1、常用Git命令行總結(一)
jQchart 介紹
用 Node.js 把玩一番 Alfred Workflow
Docker Swarm——集群管理

TAG:科技優家 |

您可能感興趣

PopupWindow源碼分析
sys.path源碼分析
Flutter圖片緩存 Image.network源碼分析
spring源碼分析——spring大綱
React Native BackHandler exitApp 源碼分析
ThreadLocal源碼分析
Prometheus原理和源碼分析
druid-spring-boot-starter源碼解析
TinyHttpd源碼分析
網關 Spring-Cloud-Gateway 源碼解析——路由之RouteDefinitionLocator一覽
hash map源碼剖析
kafka 源碼分析 3 : Producer
AtomicInteger 源碼解析
HashMap源碼分析
Selenium3源碼之common package篇
閱讀Android源碼BitmapFactory
Kafka 源碼分析2 : Network 相關
JsBridge 源碼分析
兄弟連區塊鏈培訓open-ethereum-pool以太坊礦池源碼分析(2)API分析
Spark 源碼分析之ShuffleMapTask內存數據Spill和合併