hawx1993 / tech-blog

📦My personal tech blog,not regularly update
http://sf.gg/u/trigkit4/articles
339 stars 30 forks source link

异步JavaScript与es6 #10

Open hawx1993 opened 7 years ago

hawx1993 commented 7 years ago

异步JavaScript

异步在javascript就是延时执行,异步函数没有返回值,值将会被传递给回调函数。也不能使用throw关键字

任务分成了两种:

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

在js中,有许多场景是异步的:

1.定时器 setTimeout和setInterval 2.事件监听,如click,onload等事件 3.ajax请求

而在ES6诞生以前,js异步编程模型,大概有下面四种:

定时器

定时器并不会精准的执行,会存在一定的误差。定时器有诸多弊端,比如一道和setTimeout有关的经典笔试题:

for(var i=0;i<10;i++){
    setTimeout(function() {
        console.log(i);//输出10个10
    }, 0);
}

因为异步函数必须等主进程运行完毕才会运行,setTimeout()内部回调运行的时候,主进程已经运行完毕了,此时i=10,所以输出10。

try {
    setTimeout(function() {
        throw new Error('我不希望这个错误出现');
    }, 1000);
}
catch(e) {
    console.log(e.message);
}

事件

let img = new Image();
img.src = 'xxx';
img.onload = function(){
    console.log('loaded')
};
console.log('init load')
//init load
//loaded

异步函数的处理结果传递方式不能通过简单的函数返回值来进行传递,而需要通过回调函数参数来传递

function asyncAdd(a, callback) {
    setTimeout(function () {
        var result = a + 1;
        callback(result);
    }, 500);
}
var a = 1;
asyncAdd(a, function(result) {
    console.log(result);
});

异步执行脚本

加了 defer 之后<script>放在 <head><body> 是没有区别的。 async - 脚本的并行化

<script async src="a.js"></script>
<script async src="b.js"></script>

这两个脚本会以任意次序运行,而且会立即运行,不论文档是否就绪。

如果同时使用 defer 和 async ,async 会覆盖掉 defer 。

JavaScript 语言对异步编程的实现,就是回调函数,但并不是回调函数就是异步。回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。假如读取A文件之后,再读取B文件,循环下去,就会出现回调地狱(callback hell)。Promise 对象就是为解决这个问题而提出的

它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。采用 Promise,连续读取多个文件,写法如下:

var readFile = require('readfile-promise');
    readFile(fileA)
    .then(function (data) {
    console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {
  console.log(data.toString());
})
.catch(function (err) {
  console.log(err);
});

Node与异步非阻塞I/O

我们知道,Node是基于事件驱动的单线程异步非阻塞模型, node是单线程的,异步是通过一次次的循环事件队列来实现的。每当接收到一个请求的时候,node的javascript模块会调用底层的c++模块,对传入的回调事件进行封装成请求对象,然后放入I/O线程池等待,由观察者管控何时调用。然后Javascript线程继续执行后续的操作。

所以,在Node中,除了代码,一切都是并行的:

异步readFile

var fs = require("fs");
fs.readFile("./src/pages/index/index.html", "utf8", function(error, file) {
    if (error) throw error;
    console.log(file);
    console.log("我读完文件了!");
});
console.log("我不会被阻塞!");
//我不会被阻塞
//输出file内容
//我读完文件了!

以上代码异步读取file内容,后面代码不受I/O阻塞,读取完成后执行后面的回调函数

es6与异步操作

async

async函数可以看做多个异步操作包装成的一个Promise对象,而await命令就是内部then命令的语法糖。async函数返回Promise对象

//async函数内部return语句返回的值,会成为then方法回调函数的参数。
async function f(){
    return 'hello'
}
console.log(f());//Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: "hello"}

f().then((value)=>console.log(value))
//hello
//Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: undefined}

//sleep in async
var sleep = function (time) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve('hello es6');
        }, time);
    })
};

var start = async function () {
    for (var i = 1; i <= 10; i++) {
        console.log(`第${i}次等待..`);
        await sleep(1000);
    }
};
start()
//第1次等待..
//Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
//第2次等待.
//...
//第10次等待

//sleep in Promise
function sleep(ms) {
    return(
        new Promise(function(resolve, reject) {
            setTimeout(function() {
                 resolve();
            }, ms);
        })
    );
}

sleep(1000).then(function() {
    console.log('1');
    sleep(1000).then(function() {
        console.log('2')
    })
});
//1
//2

await关键字只能用在aync定义的函数内,如果把上述的for循环改为forEach则会出错,因为此时await不在async函数的上下文。

await命令后面是一个 Promise 对象。如果不是,会被转成一个立即resolve的 Promise 对象。也可以是原始类型的值(但这时等同于同步操作)

async function func() {
  return await ({}).toString.call('123');
}
func().then(v => console.log(v))
//[object String]
//Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: undefined}
const fetch = require('node-fetch');
const showGithubUser = async(handle)=> {
    const url = `https://api.github.com/users/${handle}`;
    const response = await fetch(url);
    return await response.json();
};
showGithubUser('hawx1993')
    .then(res=>{
        console.log(res.name);//trigkit4
        console.log(res.location);//Xiamen,China
});

//convert any function into async function
const fetch = require('node-fetch');
class GitHubApiClient{
    async fetchUser(name){
        const url = `https://api.github.com/users/${name}`;
        const response = await fetch(url);
        return await response.json();
    }
}
(async ()=> {
    const client = new GitHubApiClient();
    const user = await client.fetchUser('hawx1993');
    console.log(user.name);//trigkit4
    console.log(user.location);//Xiamen,China
})();

异步Promise

详情见:Promise 探索与实践

Promise.race(iterable) 方法返回一个 promise,在可迭代的 resolves 或 rejects 中 promises 有一个完成或失败,将显示其值或原因。

function resolveAfter(millis, value) {
    return new Promise(resolve => setTimeout(() => resolve(value), millis));
}

function rejectAfter(millis, reason) {
    return new Promise((_, reject) => setTimeout(() => reject(reason), millis));
}

Promise.race([
    resolveAfter(5000, 1),
    resolveAfter(3000, 2),
    resolveAfter(4000, 3)
]).then(value => console.log(value)); // outputs "2" after 3 second.

Promise.race([
    rejectAfter(1000, new Error('bad things!')),
    resolveAfter(2000, 2)
])
    .then(value => console.log(value)) // does not output anything
    .catch(error => console.log(error.message)); //

co函数库与并发的异步操作

co函数接受 Generator 函数作为参数,返回Promise对象,使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。

 const co = require('co');
 // errors can be try/catched
 co(function *(){
   try {
     yield Promise.reject(new Error('boom'));
   } catch (err) {
     console.error(err.message); // "boom"
  }
 }).catch(onerror);
 function onerror(err) {
   console.error(err.stack);
 }

co 支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下一步。co可以将一个generator函数处理成一个异步操作。这样你可以在generator函数里面使用yield来实现“顺序调用,异步执行”的效果。在co的4.0版本里它完全采用了Promise,它会将最终返回值作为参数传递到promise的then当中。

// 数组的写法
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res); 
}).catch(onerror);

// 对象的写法
co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2),
  };
  console.log(res); 
}).catch(onerror);

总结