1. 程式人生 > >ES6---JS非同步程式設計的幾種解決方法及其優缺點

ES6---JS非同步程式設計的幾種解決方法及其優缺點

前言

因專案需要從LiveScript轉為ES6, 所以最近看了阮一峰的ES6教程,主要感興趣的是ES6對JS的非同步程式設計新的解決方案,ES6增加了promise和Generator等解決方法。現在我們來大致理清一下到ES6為止的JS非同步解決的思路以及他們各自的優缺點。

起因

我們都知道JS是單執行緒的,這也正是非同步程式設計對於JS很重要的原因,因為它無法忍受耗時太長的操作。
正因如此有一系列的實現非同步的方法

方法一 setTimeout

常用於:定時器,動畫效果
用法:setTimeout(func|code, delay)

缺點:

setTimeout 的主要問題在於,它並非那麼精確。譬如通過setTimeout()設定一個任務在10毫秒後執行,但是在9毫秒之後,有一個任務佔用了5毫秒的CPU時間片,再次輪到定時器執行時,時間就已經過期4毫秒
—-《深入淺出Nodejs》

為什麼呢? 我們可以瞭解一下setTimeout執行的事件迴圈圖

Javascript執行引擎的主執行緒執行的時候,產生堆(heap)和棧(stack)。程式中程式碼依次進入棧中等待執行,當呼叫setTimeout()方法時,即圖中右側WebAPIs方法時,瀏覽器核心相應模組開始延時方法的處理,當延時方法到達觸發條件時,方法被新增到用於回撥的任務佇列(注意是任務佇列),只要執行引擎棧中的程式碼執行完畢,主執行緒就會去讀取任務佇列,依次執行那些滿足觸發條件的回撥函式。

方法二 事件監聽

任務的執行不取決於程式碼的順序,而取決於某個事件是否發生。
用法:f1.on(‘done’, f2);
優點:比較容易理解,可以繫結多個事件,每個事件可以指定多個回撥函式,而且可以”去耦合”,有利於實現模組化。
缺點:整個程式都要變成事件驅動型,執行流程會變得很不清晰。

方法三 回撥函式

注意:以下所有的例子中 a.md檔案 存放的字串為”b”, b.md檔案存放的字串為”this is b”;

什麼是回撥函式?

JavaScript語言對非同步程式設計的實現,就是回撥函式。所謂回撥函式,就是把任務的第二段單獨寫在一個函式裡面,等到重新執行這個任務的時候,就直接呼叫這個函式。

這裡有一個誤區

回撥函式是實現JS非同步的一種方法,並不是說回撥函式就是非同步的。
只是我們用的大多數回撥函式都是用於非同步
非同步的定義:
在JavaScript中,回撥函式具體的定義為:函式A作為引數(函式引用)傳遞到另一個函式B中,並且這個函式B執行函式A。我們就說函式A叫做回撥函式。如果沒有名稱(函式表示式),就叫做匿名回撥函式。

因此callback 不一定用於非同步,一般同步(阻塞)的場景下也經常用到回撥,比如要求執行某些操作後執行回撥函式。

簡單的回撥函式例子

var fs = require('fs');

fs.readFile("a.md", function(err, data) {
  console.log(data.toString());
});

執行結果:
b

回撥函式的缺點

回撥函式本身並沒有問題,它的問題出現在多個回撥函式巢狀。
假定讀取A檔案之後,從A檔案中獲取B檔名,再讀取B檔案,程式碼如下。

var fs = require('fs');

fs.readFile("a.md", function (err, data) {
    console.log(data.toString());
    fs.readFile(data.toString() + ".md", function(err, data) {
        console.log(data.toString());
    });
});

執行結果:
b
this is b

想想,如果再巢狀多幾層,程式碼會變得多麼難以理解
這個被稱之為“回撥函式噩夢”(callback hell)!!!

方法四 Promise物件

為了解決上面的問題,我們開始介紹Promise物件,Promise原本只是社群提出的一個構想,一些外部函式庫率先實現了這個功能。ECMAScript 6將其寫入語言標準,因此目前JavaScript語言原生支援Promise物件

假設要依次讀取多個檔案,如果用普通的回撥函式,就會出現多重巢狀。程式碼不是縱向發展,而是橫向發展,很快就會亂成一團,無法管理。

var readFile = require('fs-readfile-promise');

readFile("a.md")
    .then(function(data) {
        console.log(data.toString());
        return readFile(data.toString() + ".md");
    })
    .then(function(data) {
        console.log(data.toString());
    })
    .catch(function (err) {
        console.log(err);
    });

執行結果:
b
this is b

Promise的優缺點:

優點:Promise 的寫法是回撥函式的改進,使用then方法以後,非同步任務的兩段執行看得更清楚了。then將原來非同步函式的巢狀關係轉變為鏈式步驟

缺點:Promise 的最大問題是程式碼冗餘,原來的任務被Promise 包裝了一下,不管什麼操作,一眼看去都是一堆 then,原來的語義變得很不清楚。

所以,ES6在把Promise納入標準的同時,也提供了另一種實現 => Generator 函式

方法五 Generator函式

特點: 帶星號function,yield語句 ,next() 獲取下一個yield表示式中yield後的值,擁有遍歷器介面,與for..of可搭配使用

Generator實現斐波那契的例子:

function * fibonacci() {
    let [prev, curr] = [1, 0];
    for (;;) {
        [prev, curr] = [curr, prev + curr];
        yield curr;
    }
}

for (let n of fibonacci()) {
    if (n > 1000) break;
    console.log(n);
}

執行結果
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987

Generator用於非同步操作

下面程式碼中,Generator函式封裝了一個非同步操作,該操作先讀取一個遠端介面,然後從JSON格式的資料解析資訊。這段程式碼非常像同步操作,除了加上了yield命令

var fetch = require('node-fetch');

function * gen() {
    var url = 'http://api.github.com/users/github';
    var result = yield fetch(url);
    console.log(result.bio);
}

var g = gen();
var result = g.next();

result.value.then(function(data) {
    return data.json();
}).then(function (data) {
    g.next(data);
});

執行結果:
How people build software.

執行過程:

首先執行Generator函式,獲取遍歷器物件,然後使用next 方法(第二行),執行非同步任務的第一階段。由於Fetch模組返回的是一個Promise物件,因此要用then方法呼叫下一個next 方法。

缺點:

可以看到,雖然 Generator 函式將非同步操作表示得很簡潔,但是流程管理卻不方便(即何時執行第一階段、何時執行第二階段)。即如何實現自動化的流程管理。

自此我們引出新的補充方案: Thunk函式和Co模組

Generator函式之–用Thunk函式實現自動化流程管理的例子

var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);

function run(fn) {
    var gen = fn();

    function next(err, data) {
        var result = gen.next(data);
        if (result.done) return;
        result.value(next);
    }

    next();
}

var gen = function *() {
    var f1 = yield readFile('a.md');
    console.log(f1.toString());
    var f2 = yield readFile(f1.toString() + ".md");
    console.log(f2.toString());
};

run(gen);

執行結果:
b
this is b

執行過程:

上面程式碼的run函式,就是一個Generator函式的自動執行器。內部的next函式就是Thunk的回撥函式。next函式先將指標移到Generator函式的下一步(gen.next方法),然後判斷Generator函式是否結束(result.done 屬性),如果沒結束,就將next函式再傳入Thunk函式(result.value屬性),否則就直接退出。

Thunk函式的限制

有了這個執行器,執行Generator函式方便多了。不管有多少個非同步操作,直接傳入run函式即可。當然,前提是每一個非同步操作,都要是Thunk函式,也就是說,跟在yield命令後面的必須是Thunk函式。

Generator函式之–用CO模組來實現自動化流程管理的例子

var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
var co = require('co');

var gen = function* (){
  var r1 = yield readFile('a.md');
  console.log(r1.toString());
  var r2 = yield readFile(r1.toString() + '.md');
  console.log(r2.toString());
};


co(gen).then(function() {
    console.log('Generator函式執行完畢');
});

執行結果:
b
this is b
Generator函式執行完畢

執行過程:

co函式返回一個Promise物件,因此可以用then方法添加回調函式。

CO模組的限制:

co模組其實就是將兩種自動執行器(Thunk函式和Promise物件),包裝成一個模組。使用co的前提條件是,Generator函式的yield命令後面,只能是Thunk函式或Promise物件。

總結

至此,介紹了幾種JS用於處理非同步的方法,我們也可以看到ES6對非同步的處理要比之前的版本更加成熟。整理的幾種方法的使用及其優缺點,有的地方可能還不完善,為了便於理解,使用的例子也比較簡單。後續如果有新的理解會一一補充。

大神TJ

值得一提的是,這其中涉及到的一個極為優秀的程式設計師,TJ Holowaychuk,程式設計師兼藝術家,Koa、Co、Express、jade、mocha、node-canvas、commander.js 等知名開源專案的建立和貢獻者。有興趣的可以自行了解一下該大神的成就。

膜拜至極。

相關推薦

ES6---JS非同步程式設計解決方法及其優缺點

前言 因專案需要從LiveScript轉為ES6, 所以最近看了阮一峰的ES6教程,主要感興趣的是ES6對JS的非同步程式設計新的解決方案,ES6增加了promise和Generator等解決方法。現在我們來大致理清一下到ES6為止的JS非同步解決的思路

火狐瀏覽器如何js關閉窗口的解決方法

div dom style itl 希望 mic rdquo nav 瀏覽器 今天在項目上有一個頁面要求在幾秒後自動關閉,想著還比較簡單,用window.close()就可以了,但是用IE/谷歌/火狐瀏覽器試了一下,發現IE可以,谷歌用網上的兼容方法也可以實現,但是火狐這裏

SVN被鎖定的解決方法

情況 ctr 點擊 tsd lean 一級目錄 svn 開啟 projects 用SVN經常出現被鎖定而無法提交的問題,選擇解鎖又提示沒有文件被鎖定,很是頭疼。 這裏整理了一下SVN 被鎖定的幾種解決方法: 1.出現這個問題後使用“清理”即"Clean up"功能,如果還

跨域解決方法

跨域 -o head 反向 content nbsp ces methods 函數 1.jsonp 目標服務器設置callback 函數 服務器操作 2.cors 服務器設置header :Access-Control-Allow-Origin 服務器操作

php提示undefined index的解決方法

編譯 mpi 初始化過程 itl test 運行時 port class else 平時用$_post[‘‘],$_get[‘‘]獲取表單中參數時會出現Notice: Undefined index: --------; 我們經常接收表單POST過來的數據時報Undefin

Injection of resource dependencies failed;錯誤解決方法

Error creating bean with name 'connDataController': Injection of resource dependencies failed; 最近研究ssm專案的時候出現這個問題,去網上一搜那答案真是差不多,都沒有解決我的問題,後來在重複搭建專案後

es6--js非同步程式設計Generator、Promise、Async

Generator 簡介 基本概念 generator本身並不是用於處理非同步的,但是能夠實現!!! Generator函式是 ES6 提供的一種非同步程式設計解決方案,語法行為與傳統函式完全不同。 執行 Generator 函式會返回一個遍歷器物件,也就是

spring cloud 從註冊中心遠端拉取配置檔案錯誤解決方法

1.檢查config server 是否能正常訪問2.檢查config client配置:1)必須是"bootstrap"命名的引導配置檔案2)spring: application: name: 這裡配置的名稱需要與遠端庫的配置檔名稱一致 或者使用spring.

面試題--三個執行緒迴圈列印ABC10次的解決方法

使用sleep 使用synchronized, wait和notifyAll 使用Lock 和 Condition 使用Semaphore 使用AtomicInteger 下面依次給出每種解決方案的程式碼: 使用sleep Java程式碼 package my.thread.test;  

下拉選單被表單擋住的解決方法

當層遇到下拉框時總是擋不了select框?其實這是IE的BUG,其它的瀏覽器沒有這個問題,對於這個問題論壇裡不少提出,在這裡提供我的幾種方法,各有各的好處,有錯,有好的意見者提出,謝謝.1.最直接的方法:隱藏下拉框.下面提供的是一個比較通用的一組函式:test.htm<

top k 問題的解決方法

top k問題是指給定一組數量為n的數,從中找出前k大的數或第k大的數(k <= n)。由於只要能找出前k大的數,即可以得到第k大的數。所以下面先介紹解決前k大數問題的幾種思路: 1.部分排序 由於我們只需要找到陣列nums的前k大的數,所以不需要

JS 非同步程式設計方案

前言 我們知道Javascript語言的執行環境是"單執行緒"。也就是指一次只能完成一件任務。如果有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務。 這種模式雖然實現起來比較簡單,執行環境相對單純,但是隻要有一個任務耗時很長,後面的任務都必須排隊等著,會拖延整個程式的執行。常見的瀏覽器無響應(

模冪運算的解決方法

【問題】 計算a**b%c的值。 其中,"**"代表冪(Python中就是這樣表示的);"%"代表取模運算。 【分析】 首先由模運算的性質,可以得到下面的公式: (a*b) % c = (a%c) * b % c        【公式一】 將【公式一】繼續展開,可

eclipse中alt+/失效的解決方法

1、次方法用於沒有一點提示的情況:依次開啟eclipse上面的windows ——preferences ——java ——editor —— content assist ,在右上方有一行“select the proposal kinds contained in t

子div撐不開父div的解決方法

如何修正DIV float之後導致的外部容器不能撐開的問題   在寫HTML程式碼的時候,發現在Firefox等符合W3C標準的瀏覽器中,如果有一個DIV作為外部容器,內部的DIV如果設定了float樣式,則外部的容器DIV因為內部沒有clear,導致不

JS建立物件不同方法詳解

1、工廠模式 弊端:沒有解決物件的識別問題,即怎麼知道一個物件的型別。  2、建構函式模式   與工廠模式相比:  1、沒有顯式的建立物件  2、直接將屬性和方法賦給了this物件  3、沒有return語句  要建立person的例項,必須使用new操作符,

[centos] network 無法重啟的解決方法

可能的原因 network 的配置檔案中設定的 MAC 地址與主機的實際 MAC 地址不同 解決方法:使用 ifconfig 檢視與網絡卡名稱對應的 MAC 地址,在配置檔案中更新 MAC

pycharm輸出中文出現亂碼的解決方法以及讀取時打印出現亂碼的解決

pycharm列印中文出現亂碼,有幾種情況 第一種: 對於這種情況,是普通的一種,你需要檢查開頭,是否加了 # -*- coding:utf-8 -*-還有import sys reload(sys) sys.setdefaultencoding('utf-8') 對於

js中的繼承方法

JS作為面向物件的弱型別語言,繼承也是其非常強大的特性之一。 繼承:子承父業;一個原本沒有某些方法或屬性的物件,統一寫方法,拿到了另外一個物件的屬性和方法 下面是js中的幾種繼承方式 1.改變this指向繼承(建構函式繼承):繼承建構函式中的屬性和方法 function Parent(n){

JS非同步程式設計的四方法

一、回撥函式,這是非同步程式設計最基本的方法 假定有兩個函式f1和f2,後者等待前者的執行結果,如果f1是一個很耗時的任務,可以考慮改寫f1,把f2寫成f1的回撥函式。 function f1(callback){   setTimeout(function () {    //