Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 中的异步处理 #11

Open
sweeetcc opened this issue Oct 9, 2016 · 1 comment
Open

JavaScript 中的异步处理 #11

sweeetcc opened this issue Oct 9, 2016 · 1 comment
Assignees
Labels

Comments

@sweeetcc
Copy link
Owner

sweeetcc commented Oct 9, 2016

JS异步编程

javascript

JavaScript 运行在 JavaScript 引擎(JavaScript Engine) 中,并且是单线程的。也就是说同一时间内,只能运行一个 调用栈(Call stack),调用栈中执行一个函数,所以同一时间内只能做一件事情。

既然只有一个线程,那么 JavaScript 的并发来自于哪里呢?原因是 JavaScript 的事件驱动模型。在浏览器环境中,浏览器不只是有 runtime,还包括 webapis:如用户 DOM 操作、Ajax 请求、setTimeout。

JavaScript 的并发模型 事件驱动(Event loop)。

运行时概念:

以下是来自于 MDN 关于 JavaScript 引擎的图片:
event-loop

其中:

  • 栈(Stack):是指由调用函数形成的 frames 栈;
  • 堆(Heap):对象被分配到堆中;
  • 队列(Queue):JavaScript 运行时包含一个待处理任务的队列。每一条任务都与一个函数相关联。当栈为空时,从队列中取出一个任务进行处理。当任务处理结束,栈为空的时候,再次从任务队列中取出任务进行处理。

事件循环(Event loop):

  1. 每一个任务执行完成之后,其他任务才会被执行;缺点在于,如果当前任务耗时过长,其他任务可能需要在任务队列中等待较长的时间,对于网络请求或用户操作的任务,可能需要弹窗、loading等来提示用户;
  2. 任务的添加方式:比如在浏览器环境中,当一个事件出现并且有一个事件监听器被绑定时,任务会被随时添加;另外,使用 setTimeout 函数会在一段时间之后往任务队列中添加一条任务,如果任务队列中没有其他任务,这个任务会被马上处理,但是如果有任务正在执行,需要等到执行完成之后处理,所以 setTimeout 中传的第二个时间参数,代表的是最少等待时间而非确切时间。

非阻塞(Non blocking):

JavaScript 处理 I/O 通常由事件或者回调函数实现。例如等待 ajax 返回时,仍然接收用户输入等。

异步模型:

JS 中的异步任务通常可以分为两大类:I/O 函数(Ajax、readFile等)和计时函数(setTimeout、setInterval)。

javascript

JavaScript 运行时产生对象堆和调用栈,调用栈中执行函数,当调用栈中的函数执行完成时(即栈清空时),就去执行任务队列中的回调函数,而这些回调函数的产生源自于各种 WebAPI 的调用:如 DOM 事件、网络请求、定时器等。任务执行的大致流程如下图所示:

javascript

分解一下大致流程:

  1. 主线程执行任务,形成调用栈;
  2. 被触发的异步事件后,如果异步事件有相应的回调函数,该函数被推入任务队列中;
  3. 调用栈栈清空后,读取任务队列中的任务并执行;
  4. 重复以上。

举例说明:

function print(what) {
    console.log('--- This is ** ' + what + ' **');
}

print('A');

setTimeout(function() {
    print('B');
});

print('C');

输出为:

demo-output

程序解析:

  1. 程序执行时,首先执行到 print('A') ,在执行栈(call stack)中执行,输出 '--- This is ** A ** --';
  2. 然后是 setTimeout 中的 print('B') 由于是异步任务,返回后进入异步队列;
  3. 然后是 print('C'),在执行栈中执行,输出 '--- This is ** C ** --';
  4. 执行栈清空后,任务队列中的回调进入执行栈执行,输出 '--- This is ** B ** --'

JS 异步编程方法

一、回调函数:

回调函数在上文中的异步编程介绍中提到过,回调函数是指一个被作为参数传递给另一个函数(otherFunction)的函数,回调函数的调用是在 otherFunction 中,回调函数可以使命名函数,也可以是匿名函数。

下面以 Ajax 调用为例:

下面通过 Github API 查询 star 数最多的 JavaScript 项目,并通过回调函数将结果打印出来:

function queryAPI(url, sucCallback, errCallback) {
    var xhr, result;
    xhr = new XMLHttpRequest();
    xhr.open('GET', url, true); // 第三个参数为 async:意味着是否执行异步操作
    xhr.onload = function(e) {
       if (xhr.status === 200) {
            sucCallback(JSON.parse(xhr.responseText));
        } else {
            errCallback(new Error(xhr.statusText));
        }
    };
    xhr.onerror = function () {
        errCallback(new Error(xhr.statusText));
    };

    xhr.send();
}

function sucCallback (result) {
    console.log(result);
}

function errCallback (error) {
    console.error(error);
}

var url = 'https://api.github.com/search/repositories?q=node+language:javascript&sort=stars&order=desc';
queryAPI(url, sucCallback, errCallback);

执行结果:

callback

回调函数本身非常简单,也很好理解,但是由于回调函数嵌套多层使用时,会产生回调地狱(callback hell),类似于:

setTimeout(function() {
    setTimeout(function() {
        setTimeout(function() {
            setTimeout(function() {
                setTimeout(function() {
                    ...
                });
            });
        });
    });
});

代码之间的高度耦合,代码变得愈发不可维护,流程控制更是变得混乱。所以有了以下更多的解决方案。

二、 Promise

A promise represents the eventual result of an asynchronous operation. —— Promises A+

Promises A+ 规范中说明,一个 Promise 是一个容器,里面是一个异步操作的最终结果。

根据规范实现的 Promise 可以将复杂的异步处理轻松地进行模式化,降低了异步处理的复杂度。

Promise 对象:

Promise 对象是一个返回值的代理,这个返回值在 Promise 对象创建时未必已知。异步方法可以像同步方法那样返回值,异步方法返回一个包含了原返回值的 Promise 对象。它允许你为异步操作的成功返回值或者失败信息指定处理方法。

Promise 状态:

Promise 对象有以下几种状态:

  • pending: 初始状态
  • fulfilled: 成功地操作
  • rejected: 失败的操作
  • settled: fulfilled(成功) 或 rejected(失败)。

resolved:表示 Promise 对象的状态为 settled,非 pending 状态。

pending 状态的 Promise 对象可以转换成带着一个成功值的fulfilled 状态,也可变为带着一个失败信息的 rejected 状态。

Promise 状态转换:

promises

var promise = new Promise(function(resolve, reject) {
    // 异步处理
    // 处理结束后、调用resolve 或 reject
    // resolve 时:onFulfilled 被调用
    // reject 时:onRejected 被调用
});

promise.then(onFulfilled, onRejected)

当 Promise 对象的状态发生转换时,promise.then 绑定的方法被调用。因为Promise.prototype.then和 Promise.prototype.catch方法返回 promises对象, 所以它们可以被链式调用。

promise.then 成功和失败时都可以使用。 另外在只想对异常进行处理时可以采用 promise.then(undefined, onRejected) 这种方式,只指定reject时的回调函数即可。 不过这种情况下 promise.catch(onRejected) 应该是个更好的选择。

promise-states

举例来说,使用 Promise 重写以上请求 github API 的例子:

function queryAPI(url) {
    return new Promise(function(resolve, reject) {
        var xhr, result;
        xhr = new XMLHttpRequest();
        xhr.open('GET', url, true); // 第三个参数为 async:意味着是否执行异步操作
        xhr.onload = function () {
            if (xhr.status === 200) {
                resolve(JSON.parse(xhr.responseText));
            } else {
                reject(new Error(xhr.statusText));
            }
        };
        xhr.onerror = function () {
            reject(new Error(xhr.statusText));
        };

        xhr.send();
    });
}

function sucCallback (json) {
    console.log(json);
}

function errCallback (error) {
    console.error(error);
}

var url = 'https://api.github.com/search/repositories?q=node+language:javascript&sort=stars&order=desc';
queryAPI(url).then(sucCallback).catch(errCallback);

三、ES6 Generator:

生成器函数(generator function)

function* 声明(function关键字后跟一个星号)定义一个 generator(生成器)函数,返回一个Generator对象。

生成器是一种可以从中退出并在之后重新进入的函数。生成器的环境(绑定的变量)会在每次执行后被保存,下次进入时可继续使用。

调用一个生成器函数并不马上执行它的主体,而是返回一个这个生成器函数的迭代器(iterator)对象。当这个迭代器的next()方法被调用时,生成器函数的主体会被执行直至第一个yield表达式,该表达式定义了迭代器返回的值,或者,被 yield*委派至另一个生成器函数。next()方法返回一个对象,该对象有一个value属性,表示产出的值,和一个done属性,表示生成器是否已经产出了它最后的值。

代码示例:

function* numGenerator() {
    var i = 0;
    console.info('Generator function start');
    while(i < 3) {
        console.info('Yield start');
        yield i++;
        console.info('Yield end');
    } 
    console.info('Generator function end');
}

var result = numGenerator();

执行结果:

generator-function

yield* 示例:

function* anotherGenerator(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

function* generator(i){
  yield i;
  yield* anotherGenerator(i);
  yield i + 10;
}

var result = generator(10);

执行结果:

yield

#### 使用 co 进行异步流程控制:

示例:

var co = require('co');

co(function *(){
  // resolve multiple promises in parallel
  var a = Promise.resolve(1);
  var b = Promise.resolve(2);
  var c = Promise.resolve(3);
  var res = yield [a, b, c];
  console.log(res);
}).catch(onerror)

function onerror(err) {
  console.error(err.stack);
}

执行结果:

co

### 四、 async/await #### 什么是 async/await:

async 函数就是 Generator 函数的语法糖。

基本规则:

  1. async 表示这是一个async函数,await只能用在这个函数里面;
  2. await 表示在这里等待promise返回结果了,再继续执行;
  3. await 后面跟着的应该是一个promise对象。

使用方式:

var sleep = function (time) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve();
        }, time);
    })
};

var start = async function () {
    // 在这里使用起来就像同步代码那样直观
    console.log('start');
    await sleep(3000);
    console.log('end');
};

start();

参考体验异步的终极解决方案


备注:

@sweeetcc sweeetcc added the js label Oct 15, 2016
@sweeetcc sweeetcc changed the title hello world 6 JavaScript 中的异步处理 Oct 15, 2016
@sweeetcc
Copy link
Owner Author

sweeetcc commented Oct 16, 2016

以下话题待细化总结:

  • Node.JS 事件循环机制
  • Promise
  • 生成器函数和 co 用法
  • async/await

@sweeetcc sweeetcc self-assigned this Oct 18, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant