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与es6 #10

Open
hawx1993 opened this issue May 4, 2017 · 0 comments
Open

异步JavaScript与es6 #10

hawx1993 opened this issue May 4, 2017 · 0 comments

Comments

@hawx1993
Copy link
Owner

hawx1993 commented May 4, 2017

异步JavaScript

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

任务分成了两种:

  • 同步任务:在主线程中排队依次执行的任务
  • 异步任务:不进入主线程,进入任务队列的任务。只有任务队列通知主线程某异步任务可以执行,该任务才会进入主线程执行。

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

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

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

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

  • 回调函数(callback)陷入回调地狱,解耦程度特别低
  • 事件监听(Listener)JS 和浏览器提供的原生方法基本都是基于事件触发机制的
  • 发布/订阅(观察者模式)把事件全部交给控制器管理,可以完全掌握事件被订阅的次数,以及订阅者的信息,管理起来特别方便。
  • Promise 对象实现方式,如jQuery Promise

定时器

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

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

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

  • while 循环阻塞 setTimeout 执行
var d = new Date();
setTimeout(function(){
    console.log('一秒后输出,但实际却花了: ' + ( new Date() -d  ));
}, 1000);
while(true) if( new Date() - d > 2000 ) break;

上面代码,我们期望 console 在 1s 后打出结果,可事实却是在 2000ms+ 之后运行的,这就是 Javascript 单线程给我们带来的烦恼,while 循环阻塞了 setTimeout 的执行。

  • try...catch捕获不到错误
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}
  • 将任意函数转为async function
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);

总结

  • 回调函数:
    优点:简单、容易理解
    缺点:不利于维护,代码耦合高

  • 事件监听(采用时间驱动模式,取决于某个事件是否发生)
    优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数
    缺点:事件驱动型,流程不够清晰

  • Promise对象
    优点:可以利用then方法,进行链式写法;可以书写错误时的回调函数;
    缺点:编写和理解,相对比较难

  • Generator函数
    优点:函数体内外的数据交换、错误处理机制
    缺点:流程管理不方便

  • async函数
    优点:内置执行器、更好的语义、更广的适用性、返回的是Promise、结构清晰。
    缺点:错误处理机制

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant