摘要:在處理異步回調(diào)函數(shù)的情況有著越來越值得推崇的方法及類庫,下面會依次介紹處理異步函數(shù)的發(fā)展史,及源碼解讀。而對象的狀態(tài),是由第一個的參數(shù)成功回調(diào)函數(shù)或失敗回調(diào)函數(shù)的返回值決定的。
函數(shù)的執(zhí)行分為同步和異步兩種。
同步即為 同步連續(xù)執(zhí)行,通俗點講就是做完一件事,再去做另一件事。
異步即為 先做一件事,中間可以去做其他事情,稍后再回來做第一件事情。
同時還要記住兩個特性:1.異步函數(shù)是沒有返回值的,return不管用哦 2.try{}catch(e){}不能捕獲異步函數(shù)中的異常。
js在處理異步回調(diào)函數(shù)的情況有著越來越值得推崇的方法及類庫,下面會依次介紹js處理異步函數(shù)的發(fā)展史,及源碼解讀。
(本文代碼是運行在node環(huán)境中)
let fs = require("fs"); fs.readFile("./1.txt","utf8",function(err,data){ console.log(data); })
如果只有一個異步請求,那用callback還好,但是相信大多數(shù)前端開發(fā)者都遇到過這兩種情況:
a.一個異步請求獲取到的結(jié)果是下一個異步請求的參數(shù)。(一直嵌套callback,代碼不好管理會形成回調(diào)地獄);
let fs = require("fs"); fs.readFile("./1.txt","utf8",(err,data)=>{ fs.readFile(data,"utf8",(err,data)=>{ console.log(data); }) })
b.發(fā)出兩個請求,只有當兩個請求都成功獲取到數(shù)據(jù),在執(zhí)行下一步操作。
let fs =require("fs"); fs.readFile("./1.txt","utf8",(err,data)=>{ console.log(data); }) fs.readFile("./2.txt","utf8",(err,data)=>{ console.log(data); })
像類似這種情況,只有當讀取到1.txt 和2.txt的文件的時候,我們同時獲取到兩個異步請求的結(jié)果。我們可以寫一個計數(shù)器的函數(shù),統(tǒng)一處理回調(diào);
function after(time,callback){ let arr = []; return function(data){ arr.push(data) if(--time==0){ callback(arr); } } } //統(tǒng)一處理回調(diào)結(jié)果的回調(diào)傳到after函數(shù)中。 let out = after(2,(res)=>{console.log(res)}); let fs =require("fs"); fs.readFile("./1.txt","utf8",(err,data)=>{ out(data); }) fs.readFile("./2.txt","utf8",(err,data)=>{ out(data); })
tips:
方便我們更好的了解計數(shù)器的實現(xiàn)原理,我們需要了解一個概念:高階函數(shù)
高階函數(shù):可以把函數(shù)作為參數(shù) 或者 return返回出一個函數(shù)。
舉個例子:
①.判斷一個變量是不是屬于一個類型:
function isType(type,content){ return Object.protoType.toString.call(content) ==`[Object ${type}]` } let a = [1,2,3]; isType("Array", a) == true;
②.js數(shù)據(jù)類型有好多,我們每次調(diào)用都要傳入他的類型,麻不麻煩。所以我們寫一個方法,可以批量生成函數(shù)。
function isType(type){ return function(content){ return Object.protoType.toString.call(content) == `[Oject ${type}]` } } let isArray = isType("Array"); let a = [1,2,3] isArray(a);
前兩種示例講的是return返回一個函數(shù),下面示例是一個預(yù)置函數(shù)及返回函數(shù)參數(shù)的結(jié)合示例(預(yù)置函數(shù))。
③.場景加入我有一個函數(shù),執(zhí)行第三次的時候我想輸出"我很可愛";平常我們可以這樣去實現(xiàn):
let time =0; function say(){ if(++item==3){ console.log("我很可愛") } } say(); say(); say();
高階函數(shù)實現(xiàn)的話:
function after(time,callback){ return function(){ if(--time ==0){ callback(); } } } function say(){ console.log("我很可愛"); } let out =after(3,say) out(); out(); out();
高階函數(shù)實現(xiàn)了將計時任務(wù)與業(yè)務(wù)邏輯拆分,高階函數(shù)的實現(xiàn)主要得益于作用域的查找。
2.Promise在看完了上面的callback講述,主要其實還是講述了callback的弊端:
a.回調(diào)地獄(callback無法解決)
b.并發(fā)請求,同時拿到結(jié)果(可通過計數(shù)器方式,但是太費勁,不太樂觀)
這個時候duang~duang~duang~,ES6帶著Promise來了~
Promise主要是es6提供的主要用于處理異步請求的一個對象,他能夠很好的解決回調(diào)地獄以及并發(fā)請求。
在寫promise源碼之前,我們先通過幾個調(diào)用promise的示例,了解一下promise的一些原理及特性,這在我們封裝promise的時候能夠起到很大的作用:
普通調(diào)用實例:
let fs = require("fs"); let p = new Promise(function(resolve,reject){ fs.readFile("./1.txt","utf8",(err,data)=>{ err?reject(err):resolve(data); }) }) p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
1.promise實例可以多次調(diào)用then方法;
p.then((data)=>{console.log(data)},(err)=>{console.log(err)}); p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
2.promise實例可以支持then方法的鏈式調(diào)用,jquery實現(xiàn)鏈式是通過返回當前的this。但是promise不可以通過返回this來實現(xiàn)。因為后續(xù)通過鏈式增加的then不是通過原始的promise對象的狀態(tài)來決定走成功還是走失敗的。
p.then((data)=>{console.log(data)},(err)=>{console.log(err)}).then((data)=>{console.log(data)})
3.只要then方法中的成功回調(diào)和失敗回調(diào),有返回值(包括undefiend),都會走到下個then方法中的成功回調(diào)中,并且把返回值作為下個then成功回調(diào)的參數(shù)傳進去。
第一個then走成功: p.then((data)=>{return undefined},(err)={console.log()}).then((data)=>{console.log(data)}) 輸出:undefiend 第一個then走失敗: p.then((data)=>{console.log(1)},(err)={return undefined).then((data)=>{console.log(data)}) 輸出:undefiend
4.只要then方法中的成功回調(diào)和失敗回調(diào),有一個拋出異常,則都會走到下一個then中的失敗回調(diào)中;
第一個then走成功: p.then((data)=>{throw new Err("錯誤")},(err)={console.log(1)}).then((data)=>{console.log("成功")},(err)=>{console.log(err)}) 輸出:錯誤 第一個then走失敗: p.then((data)=>{console.log(1)},(err)={throw new Err("錯誤")).then((data)=>{console.log("成功")},(err)=>{console.log(err)}) 輸出:錯誤
5.成功和失敗 只能走一個,如果成功了,就不會走失敗,如果失敗了,就不會走成功;
6.如果then方法中,返回的不是一個普通值,仍舊是一個promise對象,該如何處理?
答案:它會等待這個promise的執(zhí)行結(jié)果,并且傳給下一個then方法。如果成功,就把這個promise的結(jié)果傳給下一個then的成功回調(diào)并且執(zhí)行,如果失敗就把錯誤傳給下一個then的失敗回調(diào)并且執(zhí)行。
7.具備catch捕獲錯誤;如果catche前面的所有then方法都沒有失敗回調(diào),則catche會捕獲到錯誤信息執(zhí)行他就是用來兜兒底用的。
p是一個失敗的回調(diào): p.then((data)=>{console.log("成功")}).then((data)=>{成功}).catche(e){console.log("錯誤")}
8.返回的結(jié)果和 promise是同一個,永遠不會成功和失敗
var r = new Promise(function(resolve,reject){ return r; }) r.then(function(){ console.log(1) },function(err){ console.log(err) })
以上是經(jīng)過調(diào)用es6提供的promise,發(fā)現(xiàn)的一些特性,下面我們會根據(jù)這些特性去封裝Promise類。
一.我們先通過初步了解的promise和簡單的基本調(diào)用,簡單的實現(xiàn)一個promise;
1.Promise支持傳入一個參數(shù),函數(shù)類型,這個函數(shù)往往是我們自己發(fā)起異步請求的函數(shù),我們稱它為執(zhí)行器actuator,這個函數(shù)會在調(diào)用new Promise()的作用域內(nèi)立即執(zhí)行,并且傳入兩個函數(shù)一個resolve另一個是reject作為參數(shù);
2.promise對象支持.then()的方法,then方法支持兩個參數(shù)一個為onFulfilled成功回調(diào)另一個為onRejected失敗回調(diào);onFulfilled接受參數(shù)data為異步請求拿到的數(shù)據(jù),onRejected接受的參數(shù)為捕獲到的異常錯誤。
3.當異步回調(diào)成功時,執(zhí)行resolve,并且把回調(diào)結(jié)果傳給resolve函數(shù)。失敗則執(zhí)行reject,把異常信息傳給reject函數(shù)。(這一步往往是在actuator執(zhí)行器函數(shù)中我們自己去控制執(zhí)行的)
4.一個promise對象,執(zhí)行了resolve,就不會在去執(zhí)行reject。執(zhí)行了reject,也不會在去執(zhí)行resolve;
所以promise內(nèi)部中有一個類似狀態(tài)機的機制,它分為三種狀態(tài),創(chuàng)建一個promise對象,默認狀態(tài)為"pending"狀態(tài),當執(zhí)行了resolve,則該狀態(tài)變?yōu)?fulfilled",若果執(zhí)行了reject則該狀態(tài)變?yōu)?rejected",所以我們在then方法中需要根據(jù)狀態(tài)作出判斷;
5.promise對象已經(jīng)是成功狀態(tài)或是失敗狀態(tài)時,都可以繼續(xù)通過then傳入函數(shù),會通過當前的狀態(tài),來決定執(zhí)行成功還失敗,并且把結(jié)果或是錯誤傳給相應(yīng)的函數(shù)。所以我們需要拿到的結(jié)果和捕獲的錯誤。
function Promise(fn){ this.status = "pending";//狀態(tài)機 //一個promise支持執(zhí)行多個then,所以需要一個池子把他的回調(diào)函數(shù)存儲起來,統(tǒng)一遍歷執(zhí)行; this.onFulfilledCallbacks = []; this.onRejectedCallbacks =[]; //保存結(jié)果或者錯誤異常 this.result = "";//當前promise回調(diào)成功獲取到的數(shù)據(jù); this.reason = "";//當前promise失敗的原因 var self = this; function resolve(data){ //執(zhí)行了reject就不能執(zhí)行resolve,所以必須保證是pending狀態(tài); //當執(zhí)行回調(diào)成功,在執(zhí)行器調(diào)用resolve,我們?nèi)ケ闅v成功回調(diào)的池子,依次執(zhí)行; //保存結(jié)果,并且將當前狀態(tài)設(shè)置為"fulfilled" if(self.status=="pending"){ self.result = data; self.status = "fulfilled"; self.onFulfilledCallbacks.forEach((fn)=>{ fn(data); }) } } function reject(err){ //執(zhí)行了resolve就不能執(zhí)行reject,所以必須保證是pending狀態(tài); //當執(zhí)行回調(diào)失敗,在執(zhí)行器調(diào)用reject,我們?nèi)ケ闅v成功回調(diào)的池子,依次執(zhí)行; //保存錯誤原因并且將當前狀態(tài)設(shè)置為"rejected" if(self.status=="pending"){ self.reason= err; self.status ="rejected"; self.onRejectedCallbacks.forEach((fn)=>{ fn(err); }) } } fn(resolve,reject) } Promise.prototype.then= function(onFulfilled,onRejected){ //如果當前promise對象成功狀態(tài),則直接執(zhí)行onFulfilled回調(diào)函數(shù),并且把拿到的已經(jīng)保存的成功數(shù)據(jù)傳進去。 if(this.status =="fulfilled"){ onFulfilled(this.result) } //如果當前promise對象失敗狀態(tài),則直接執(zhí)行rejected回調(diào)函數(shù),并且把已經(jīng)保存的補貨失敗的原因傳進去。 if(this.status =="rejected"){ onRejected(this.reason); } if(this.status == "pending"){ this.onFulfilledCallbacks.push(onFulfilled); this.onRejectedCallbacks.push(onRejected); } }
到目前為止我們已經(jīng)封裝了一個簡易版的promise了,我們可以通過一些case去測試一下,是否滿足上面所描述的特性。
let fs = require("fs"); let p = new Promise((resolve,reject)=>{ fs.readFile("./1.txt","utf8",function (err,data) { err ? reject(err):resolve(data); }) }); p.then(data=>{console.log(data)},err=>{console.log(err)}); p.then(data=>{console.log(data)},err=>{console.log(err)});
二、我們簡易版的promise類,已經(jīng)初步實現(xiàn)了一些promise的基本特性;這一節(jié)我們我們簡易版的promise進行改版,把promise的更復(fù)雜的功能增加進去。
1.當我們調(diào)用promise時,傳入的執(zhí)行器會立刻執(zhí)行,執(zhí)行器函數(shù)內(nèi)部是一個同步的過程,我們可以用try...catch捕獲錯誤,并且應(yīng)該直接調(diào)用失敗的函數(shù)。
2.promise支持鏈式寫法,then后面繼續(xù).then ,原理并不是像jquery一樣返回一個this;而是不管當前promise狀態(tài)是什么,都返回一個新的promise對象,官方文檔命名這個新的promise對象為promise2。
3.鏈式寫法中第二個then中的回調(diào)走成功還是走失敗,取決于上一個then中返回的promise(就是promise2)對象的狀態(tài)。 而 promise2對象的狀態(tài),是由第一個then的參數(shù)(成功回調(diào)函數(shù)或失敗回調(diào)函數(shù))的返回值決定的。如果返回的是一個值(包括返回的是undefined、""),則第二個then走成功;如果返回的仍舊是一個promise對象,那么promise2會等待返回的這個promise對象的回調(diào)結(jié)果而確定promise2的狀態(tài)值,如果回調(diào)結(jié)果拿到的是一個值(成功),那么promise2會將此值作為參數(shù)傳入字節(jié)的reosolve中并執(zhí)行,如果回調(diào)中拋出異常(失敗),那么promise2會把異常傳到reject中并且執(zhí)行;
function Promise(fn){ this.status = "pending"; this.onFulfilledCallbacks = []; this.onRejectedCallbacks =[]; this.result = ""; this.reason = ""; var self = this; function resolve(data){ if(self.status=="pending"){ self.result = data; self.status = "fulfilled"; self.onFulfilledCallbacks.forEach((fn)=>{ fn(data); }) } } function reject(err){ if(self.status=="pending"){ self.reason= err; self.status ="rejected"; self.onRejectedCallbacks.forEach((fn)=>{ fn(err); }) } } try{ fn(resolve,reject) }catch(e){ reject(e) } } Promise.prototype.then= function(onFulfilled,onRejected){ //then方法什么都不傳,也可以支持連續(xù)調(diào)用 onFulfilled = onFulfilled ?onFulfilled :function(data){ return data}; onRejected =onFulfilled ? onFulfilled :function(err){throw new Error(err)} let self = this; let Promise2;//聲明primise2 if(this.status =="fulfilled"){ Promise2 = new Promise(function(resolve,reject){ //promise2的狀態(tài),決定下一個then方法中執(zhí)行成功還是失敗。 //promise2的狀態(tài),是由第一個then的onFulfilled的返回值決定的。 //當我們執(zhí)行onFulfilled(我們通過then方法傳進來的自己的函數(shù))的時候,是同步操作,需要通過trycatch捕獲異常,如果發(fā)現(xiàn)異常就直接走下一個then的reject失敗回調(diào)。 //promise官方文檔規(guī)定,每一個resolve或是reject回調(diào)的執(zhí)行必須保證是在異步中執(zhí)行,所以我們強制加定時器,保證onFulfilled是異步執(zhí)行的。 setTimeOut(function(){ try{ let x = onFulfilled(self.result); //獲取到返回值,需要去解析,從而判斷出promise2應(yīng)該走失敗還是成功。 resolvePromise(Promise2,x,resolve,reject) }catch(e){ //執(zhí)行reject,下一個then就會走失敗 reject(e); } }) }) } if(this.status =="rejected"){ Promise2 = new Promise(function(resolve,reject){ setTimeout(function(){ try{ let x = onRejected(self.reason); resolvePromise(Promise2,x,resolve,reject) }catch(e){ reject(e) } }) }) } if(this.status == "pending"){ Promise2 = new Promise(function(resolve,reject){ self.onFulfilledCallbacks.push(function(){ setTimeout(function(){ try{ let x = onFulfilled(self.result); resolvePromise(Promise2,x,resolve,reject); }catch (e){ reject(e) } }) }); self.onRejectedCallbacks.push(function(){ setTimeout(function(){ try { let x = onRejected(self.reason); resolvePromise(Promise2,x,resolve,reject) }catch (e){ reject(e); } }) }); }) } return Promise2; } function resolvePromise(promise2,x,resolve,reject){ //此處如果相等會爆出類型錯誤; if(promise2 == x){ reject(new TypeError("循環(huán)引用了")) } //如果x是對象或函數(shù)(引用類型的值),則需要進一步判斷。(這塊兒要想的多一些,因為x是開發(fā)人員寫的函數(shù)返回的,第一個then中回調(diào)返回的) //若果x是一個普通值,則直接執(zhí)行resolve,并且傳給下個then的成功; //如果返回的是一個promise對象,則promise2則會等待返回的promise對象執(zhí)行完成,如果執(zhí)行完成后,看這個promise走的成功還是失敗,如果失敗則拋出異常。如果成功則將獲取的數(shù)據(jù)作為onFulfilled返回的結(jié)果,用于判斷promise2走成功或者失敗,因為返回的結(jié)果可能還是promise對象,所以用遞歸去執(zhí)行,知道拿到數(shù)據(jù)或者異常。(遞歸) //判斷是不是promise對象,通過有沒有then方法 //捕獲異常是因為判斷不嚴謹,存在then方法,可能也不是promise對象,調(diào)用它的then可能會報錯。 let called =false; if(x!==null &&(typeof x =="object"|| typeof x =="function")){ try{ let then =x.then; if(typeof then =="function"){ //promise對象 then.call(x,function(y){ if(called)return; called = true; resolvePromise(promise2,y,resolve,reject) },function(err){ if(called)return; called = true; reject(err) }) }else{ //普通對象 resolve(x) } }catch(e){ if(called)return; called = true; reject(e) } }else{ resolve(x); } } 到此,Promise的大部分特性都已經(jīng)具備了。但是Promise對象還有一些其他的方法,可供調(diào)用,比如說catch方法,還有他的私有屬性all 、race、defferd,如果前面的Promise封裝懂了,那這些方法就so easy了,下面會根據(jù)這些方法的功能一一進行封裝,
1.all方法處理 并發(fā)請求,同時獲得結(jié)果。一個失敗,則失敗,都成功,才算成功.這個時候我們就想到前面我們寫的計數(shù)器的用法。
Promise.all([read("./1.txt"),read("./2.txt")]).then(res=>{console.log(res)}) Promise.all = function(promiseArray){ return new Promise(function(resolve,reject){ var result = []; var i=0; function processData(index,res){ result[index] = res; if(++i==promiseArray.length){ resolve(result) } } promiseArray.forEach((item,index)=>{ item.then(res=>{processData(index,res)},reject) }) }) };
2.race方法,Pomise.race,顧名思義“賽拍”,傳入多個異步promise,只要有一個成功,則就成功,有一個失敗則失敗,后面也可跟then方法。
Promise.race = function(promiseArray){ return new Promise(function(resolve,reject){ promiseArray.forEach((item,index)=>{ item.then(resolve,reject); }) }) } Promise.race([read("./1.txt"),read("./5.txt")]).then(res=>{console.log(res)},err=>{console.log(err)})
3.生成一個成功的promise,把傳入的參數(shù),傳入到then的成功回調(diào)中,該方法返回一個promise
Promise.resolve=function(value){ return new Promise(function(resolve,reject){ //promise規(guī)范 resolve和reject函數(shù)必須是在異步回調(diào)中執(zhí)行 setTimeout(function(){ resolve(value); }) }) } Promise.resolve("123").then(res=>{console.log(res)})
4.生成一個失敗的promise,把傳入的參數(shù),傳入到then的失敗回調(diào)中。該方法返回一個promise
Promise.reject = function(err){ return new Promise(function(resolve,reject){ setTimeout(function(){ reject(err); }) }) } Promise.reject("error").then(res=>{console.log(res)},err=>{console.log(err)})
5.catch托底捕獲錯誤,這個方法是實例的共有方法,應(yīng)該放到Promise的原型上,每一個 promise實例都可以調(diào)用.它支持一個參數(shù),該參數(shù)是之前所有的then中,并沒有失敗回調(diào),當發(fā) 生錯誤時,最后統(tǒng)一在catch中進行捕獲
Promise.prototype.catch = function(calllback){ return this.then(null,callback) }
6.很多人都用過jquery的deferrd對象,他和promise的deffer對象很類似。promise的deferred對象只是對promise進行了一次封裝
Promise.defer = Promise.deferred=function(){ var obj = {}; obj.promise = new Promise(function(resolve,reject){ obj.resolve = resolve; obj.reject = reject; }) return obj; } let fs = require("fs"); function read2 (url){ var deferr = Promise.deferred(); fs.readFile("./1.txt","utf8",(err,res)=>{ err?deferr.reject(err):deferr.resolve(res); }) return deferr; } read2("./1.txt").then(data=>{console.log(data)})
至此,一個完整的Promise.js封裝完成,當然最后是需要模塊化導(dǎo)出的,我們采用CommonJS規(guī)范導(dǎo)出一個模塊 采用
module.exports = Promise;
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/93554.html
摘要:接下來我們看下三類異步編程的實現(xiàn)。事件監(jiān)聽事件發(fā)布訂閱事件監(jiān)聽是一種非常常見的異步編程模式,它是一種典型的邏輯分離方式,對代碼解耦很有用處。 一、 一道面試題 前段時間面試,考察比較多的是js異步編程方面的相關(guān)知識點,如今,正好輪到自己分享技術(shù),所以想把js異步編程學(xué)習(xí)下,做個總結(jié)。下面這個demo 概括了大多數(shù)面試過程中遇到的問題: for(var i = 0; i < 3; i++...
摘要:最受歡迎的引擎是,由和使用,用于,以及使用的。引擎它們是如何工作的全局執(zhí)行上下文和調(diào)用堆棧剛剛了解了引擎如何讀取變量和函數(shù)聲明,它們最終被放入了全局內(nèi)存堆中。事件循環(huán)只有一個任務(wù)它檢查調(diào)用堆棧是否為空。 為了保證可讀性,本文采用意譯而非直譯。 想閱讀更多優(yōu)質(zhì)文章請猛戳GitHub博客,一年百來篇優(yōu)質(zhì)文章等著你! 有沒有想過瀏覽器如何讀取和運行JS代碼? 這看起來很神奇,我們可以通過瀏覽...
摘要:事件循環(huán)從回調(diào)隊列中獲取并將其推送到調(diào)用堆棧。如何工作請注意,不會自動將您的回調(diào)函數(shù)放到事件循環(huán)隊列中。它設(shè)置了一個計時器,當計時器到期時,環(huán)境將您的回調(diào)函數(shù)放入事件循環(huán)中,以便將來的某個事件會將其選中并執(zhí)行它。 我們將通過回顧第一篇文章中單線程編程的缺點,然后在討論如何克服它們來構(gòu)建令人驚嘆的JavaScript UI。在文章結(jié)尾處,我們將分享5個關(guān)于如何使用async / awai...
摘要:異步編程三座大山原型原型鏈作用域閉包同步異步。異步操作執(zhí)行完畢后,再執(zhí)行該回調(diào)函數(shù),確保回調(diào)在異步操作之后執(zhí)行。回調(diào)函數(shù)本身是我們約定俗成的一種叫法,我們定義它,但是并不會自己去執(zhí)行它,它最終被其他人執(zhí)行了。 JS異步編程 JS三座大山:原型原型鏈、作用域閉包、同步異步。之前有寫過自己對閉包的理解,今天來總結(jié)一下JS中的異步。 思考(案例來自stackoverflow): functi...
摘要:以下展示它是如何工作的函數(shù)使用構(gòu)造函數(shù)創(chuàng)建一個新的對象,并立即將其返回給調(diào)用者。在傳遞給構(gòu)造函數(shù)的函數(shù)中,我們確保傳遞給,這是一個特殊的回調(diào)函數(shù)。 本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。 歡迎關(guān)注我的專欄,之后的博文將在專欄同步: Encounter的掘金專欄 知乎專欄...
閱讀 1440·2021-11-17 09:33
閱讀 3030·2021-10-13 09:39
閱讀 2707·2021-10-09 10:01
閱讀 2454·2021-09-29 09:35
閱讀 3902·2021-09-26 10:01
閱讀 3524·2019-08-26 18:37
閱讀 3155·2019-08-26 13:46
閱讀 1918·2019-08-26 13:39