JavaScript | 非同步處理

AsGiant發表於2019-05-13

1 非同步

  • 所謂"非同步",簡單說就是一個任務分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段,比如,有一個任務是讀取檔案進行處理,非同步的執行過程就是下面這樣。

image.png

這種不連續的執行,就叫做非同步。相應地,連續的執行,就叫做同步。

2 高階函式

函式作為一等公民,可以作為引數和返回值,也可以作為函式的引數

2.1 可以用於批量生成函式

// 判斷一個引數是否是字串
function isString(param){
	return Object.prototype.toString.call(param) == '[object String]';
}
isString(123);

// 判斷一個引數是否是陣列
function isArray(param){
	return Object.prototype.toString.call(param) == '[object Array]';
}
isArray([]);
複製程式碼

函式可以作為返回值

function isType(type){
	return function(param){
  	return Object.prototype.toString.call(param) == `[object ${type}]`;
  }
}
let isString = isType('String');
let isArray = isType('Array');
console.log(isString({}))
console.log(isArray([]))

複製程式碼

2.2 可以用於需要呼叫多次才執行的函式

函式可以作為引數傳到另外一個函式裡面

function eat(){
	console.log("吃完了")
}

// 讓他執行幾次才會執行
function after(times,fn){
  let count = 0;
	return function(){
  	if(count++==times){
       fn();
    }
  }
}

let newEat = after(3,eat);
newEat();
newEat();
newEat();
複製程式碼

3. 非同步程式設計的語法目標,就是怎樣讓它更像同步程式設計,有以下幾種

  • 回撥函式實現
  • 事件監聽
  • 釋出訂閱
  • Promise/A+ 和生成器函式
  • async/await

4. 回撥

比如我現在要讀取一個檔案,非同步讀取

let fs = require('fs');
fs.readFile('./1.txt','utf8',function(err,data){
	if(err){ // 如果err有值,就表示程式出錯
  	console.log(err);
  }else{ // 如果err為空就表示成功沒有錯誤
  	console.log(data);
  }
});
複製程式碼

回撥函式的問題

  1. 無法捕捉錯誤 try catch return
  2. 不能return
  3. 回撥地獄
function read(filename){
	fs.readFile(filename,'utf8',function(err,data){
    if(err){ // 如果err有值,就表示程式出錯
      console.log(err);
    }else{ // 如果err為空就表示成功沒有錯誤
      console.log(data);
    }
  });
}

try{
	 read('1.txt');	
}catch(e){
	console.log(e);
}

console.log(2);
複製程式碼

當你訪問伺服器的時候,比如請求一個html頁面,比如使用者列表。伺服器一方面會去讀取模板檔案,可能是ejs、pug、jade、handlebar、另外一方面還要讀取資料(可能會放在檔案裡,也可以會放在資料裡),它們都很慢,所以都是非同步的.

  • 這種寫法很難看
  • 非常難以維護
  • 效率比較低,因為它是序列的
fs.readFile('./template.txt','utf8',function(err,template){
  fs.readFile('./data.txt','utf8',function(err,data){
    console.log(template+''+data);
  })
})
複製程式碼

如何解決這個回撥巢狀的問題

6. 非同步流程解決方案

6.1 通過事件釋出訂閱來實現

// 這node核心模組中一個類,通過它可以穿件時間發射器的例項,裡面有兩個核心方法
// 一個叫on emit,on表示註冊監聽,emit表示發射事件
let EventEmitter = require('events');
let eve = new EventEmitter();
// 這個html物件是存放
let html = {}; // template data

// 監聽資料獲取成功事件,當事件發生之後呼叫回撥函式
eve.on('ready',function(key,value){
	html[key] = value;
  if(Object.key(html).lenght==2){
  	console.log(html);
  }
})

fs.readFile('./template.txt','utf8',function(err,template){
  // 1事件名 2引數往後是傳遞給回撥函式的引數
	eve.emit('ready','template',template);
})

fs.readFile('./data.txt','utf8',function(err,data){
  // 
	eve.emit('ready','data',data);
})
複製程式碼

6.2 哨兵變數

// 通過一個哨兵來解決
let html = {}
function done(key,value){
	html[key]=value;
  if(Object.keys(html).length===2){
  	console.log(html);
  }
};

fs.readFile('./template.txt','utf8',function(err,template){
	done("template",data)
})

fs.readFile('./data.txt','utf8',function(err,data){
	done("data",data)
})
複製程式碼
// 通過一個哨兵來解決
let html = {}

function render(lenght,cb){
	let html = {};
  if(Object.keys(html).length===lenght){
  	cb(html);
  }
}

let done = render(2,function(html){
	console.log(html);
});

fs.readFile('./template.txt','utf8',function(err,template){
	done("template",data)
})

fs.readFile('./data.txt','utf8',function(err,data){
	done("data",data)
})
複製程式碼

6.4 生成器Generators/ yield

生成器是一個函式,可以用了生成迭代器
生成器函式和普通函式不一樣,普通函式一旦呼叫一定會執行完
但是生成器函式中間可以展廳,可以執行一會歇一會

image.png

生成器函式有一個特點,需要加個* 生成器有若干個階段,如何劃分這些階段呢 yield 定義通過迭代器協議從生成器函式返回的值。如果省略,則返回undefined。

function *go(params) {
        console.log(1);
        // 此處的b是提供外界輸入進來
        // 這一行實現輸入和輸出,本次的輸出放在yield後面,下次的輸入放在yield前面
        let b = yield 'a';
        console.log(2);
        let c = yield b;
        console.log(3);
        return c;
    }
// 生成器函式和普通函式的函式不一樣,呼叫它的話,函式並不會立刻執行
// 它會返回生成器的迭代器,迭代器是一個物件,每呼叫一次next就可以返回一個值物件
let it = go();;
let r1 = it.next();
// 第一次呼叫next返回一個物件,此物件有兩個屬性,一個value就是yield後面那個值,一個是done表示是否迭代完成
console.log(r1); //{value:'a',done:false};
// next 第一次執行不需要傳參,傳參是沒有意義的
let r2 = it.next('B值');//傳給了b
console.log(r2); // {value:'a',done:false};
let r3 = it.next();// {value:undefined,done:true};
console.log(r3);
複製程式碼

6.5 Promise來處理

Promise 物件是一個代理物件(代理一個值),被代理的值在Promise物件建立時可能是未知的。它允許你為非同步操作的成功和失敗分別繫結相應的處理方法(handlers)。

6.5.1 例子
let p1 = new Promise((resolve, reject) => {
     setTimeout(() => {
         resolve(1000)
     }, 1000);
});

p1.then(res=>{
	console.log(res)
})
複製程式碼

6.5.2 promise.all

會接收到promise陣列,如果promise全部完成了這個,promise才會成功,如果有一個失敗,整體就失敗了

同時非同步請求多個資料的時候,會用all

let p1 = new Promise((resolve, reject) => {
     setTimeout(() => {
         resolve(1000)
     }, 1000);
});

let p2 = new Promise((resolve, reject) => {
     setTimeout(() => {
         resolve(1000)
     }, 1000);
});

Promise.all([p1,p2]).then(res=>{
    console.log('====================================');
    console.log(res);
    console.log('====================================');
},err=>{
 		console.log('====================================');
    console.log(err);
    console.log('====================================');
})
複製程式碼

原理

function gen(times,cb) {
    let result = [],count=0
    return function(i,data){
        result[i] = data;
        if (++count == times) {
            cb(result);
        }
    }    
}

Promise.alls = function (promises) {
    return new Promise(function (resovle, reject) {
        let result = []
        let count =0
        let done = gen(promises.length,resovle)
        // function done(i,data){
        //     result[i] = data;
        //     if (++count== promises.length) {
        //         resovle(result)
        //     }
        // }
        for (let i = 0; i < promises.length; i++) {
        //   promises[i].then(done.bind(null,i));
             promises[i].then(function (data) {
                 done(i,data)
             }, reject);
        }
    })
}

Promise.alls([p1,p2]).then(res=>{
    console.log('====================================');
    console.log(res);
    console.log('====================================');
},error=>{
    console.log('====================================');
    console.log(error);
    console.log('====================================');
})
複製程式碼

6.5.3 promise.race

會接收到一個promise陣列,只要一個成功,則就成功了,只有一個失敗就是失敗了

當你有三個介面都不穩定,可你可以同時請求三個介面,誰先回來用誰的

let p1 = new Promise((resolve, reject) => {
     setTimeout(() => {
         resolve(1000)
     }, 1000);
});

let p2 = new Promise((resolve, reject) => {
     setTimeout(() => {
         resolve(1000)
     }, 1000);
});

Promise.race([p1,p2]).then(res=>{
    console.log('====================================');
    console.log(res);
    console.log('====================================');
})
複製程式碼

原理

Promise.race = function (promises) {
    return new Promise(function (resovle, reject) {
        for (let i = 0; i < promises.length; i++) {
             promises[i].then(resovle, reject);
        }
    })
}
複製程式碼

6.6 Co

co是一個為Node.js和瀏覽器打造的基於生成器的流程控制工具,藉助於Promise,你可以使用更加優雅的方式編寫非阻塞程式碼。

let fs = require('fs');
function readFile(filename) {
  return new Promise(function (resolve, reject) {
    fs.readFile(filename, function (err, data) {
      if (err)
        reject(err);
      else
        resolve(data);
    })
  })
}
function *read() {
  let template = yield readFile('./template.txt');
  let data = yield readFile('./data.txt');
  return template + '+' + data;
}
co(read).then(function (data) {
  console.log(data);
}, function (err) {
  console.log(err);
});
複製程式碼
function co(gen) {
  let it = gen();
  return new Promise(function (resolve, reject) {
    !function next(lastVal) {
      let {value, done} = it.next(lastVal);
      if (done) {
        resolve(value);
      } else {
        value.then(next, reason => reject(reason));
      }
    }();
  });
}
複製程式碼

6.7 Async/ await

使用async關鍵字,你可以輕鬆地達成之前使用生成器和co函式所做到的工作 但是其實是它,只要generator+promise語法

Async的優點

  • 內建執行器
  • 更好的語義
  • 更廣的適用性
async function timeout() {
   return 'hello world'
}

timeout().then(res=>{
  console.log(res);
})
console.log('雖然在後面,但是我先執行');
複製程式碼

async 返回是一個 promise

image.png

舉一個例子

function loading(num) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2 * num)
        }, 2000);
    } )
}
// 這是一個函式構造器
複製程式碼

這時候我想傳入三個值30、50、40 通過計算後求和。 延遲的時間模擬網路請求時間

async function testResult() {
  let one = await loading(30);
  let two = await loading(50);
  let three = await loading(40);
  console.log(first + second + third);
}

testResult();
複製程式碼

相關文章