js是单线程的,就是一次只能完成一件任务,如果有多个任务就必须排队,前面一个任务完成之后再去执行后一个任务;
这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。
"同步模式"就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。
"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。
1.回调函数:
回调中嵌套回调函数,可能造成回调地狱;
ajax(url,() => { // 处理逻辑})
这种代码的可读性和可维护性都是非常差的,因为嵌套的层级太多。而且还有一个严重的问题,就是每次任务可能会失败,需要在回调里面对每个任务的失败情况进行处理,增加了代码的混乱程度。
ajax(url,() => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})})
2.promise
不仅能捕获错误,也能很好解决回调地狱的问题;
ajax(url)
.then(res => {
console.log(res)
return ajax(url1)
}).then(res => {
console.log(res)
return ajax(url2)
}).then(res => console.log(res))promise的链式调用:
- 每次调用返回的都是一个新的Promise实例;
- 如果then中返回的是一个结果的话,会把这个结果传递给下一个then中的成功回调;
- 如果then中出现异常,则会走失败的回调;
- 在then中使用了return,那么return的值会被Promise.resolve()包装;
- then中可以不传递参数,如果不传递那么会到下一个then中
- catch会捕获没有捕获到的异常;
缺点是:无法取消Promise,错误需要回调函数来捕获;
3.生成器generator/yield:
ES6提供的,Generator最大的特点就是可以控制函数的执行;利用协程完成 Generator 函数,用 co 库让代码依次执行完,同时以同步的方式书写,也让异步操作按顺序执行。
- 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
- Generator 函数除了状态机,还是一个遍历器对象生成函数。
- 可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果。
- yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
关于生成器以及协程的理解:
-------------
生成器是一个带*的函数,不是真正的函数,能通过yield关键字暂停执行和恢复执行的;
function*gen() {
console.log("enter");
let a = yield 1;
let b = yield (function () {return 2})();
return 3;
}var g = gen() // 阻塞住,不会执行任何语句
console.log(typeof g) // object 看到了吗?不是"function" console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
//enter
//{ value: 1, done: false }
//{ value: 2, done: false }
//{ value: 3, done: true }
//{ value: undefined, done: true }生成器执行有几个关键点:
1.调用gen()后,程序会阻塞住,不会执行任何语句;
2.调用g.next()后,程序会接着上一次的yield继续执行,直到遇到yield程序暂停;
3.next方法返回一个对象, 有两个属性: value 和 done。value 为当前 yield 后面的结果,done 表示是否执行完,遇到了return 后,done 会由false变为true。
--------------
function*foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)}
let it = foo(5)
console.log(it.next())
// => {value: 6, done: false}console.log(it.next(12))
// => {value: 8, done: false}console.log(it.next(13))
// => {value: 42, done: true}解析:
首先 Generator 函数调用和普通函数不同,它会返回一个迭代器
当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
当执行第二次 next 时,传入的参数12就会被当作上一个yield表达式的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
当执行第三次 next 时,传入的参数13就会被当作上一个yield表达式的返回值,所以 z = 13, x = 5, y = 24,相加等于 42
---------
生成器实现机制----协程
什么是协程?
协程是一种比线程更加轻量级的存在,协程处在线程的环境中,一个线程可以存在多个协程,可以将协程理解为线程中的一个个任务。不像进程和线程,协程并不受操作系统的管理,而是被具体的应用程序代码所控制。
协程的运作过程
那你可能要问了,JS 不是单线程执行的吗,开这么多协程难道可以一起执行吗?
答案是:并不能。一个线程一次只能执行一个协程。比如当前执行 A 协程,另外还有一个 B 协程,如果想要执行 B 的任务,就必须在 A 协程中将JS 线程的控制权转交给 B协程,那么现在 B 执行,A 就相当于处于暂停的状态。
--------
如何让Generator的异步代码按照顺序执行完毕?
thunk函数(偏函数)
举个例子,比如我们现在要判断数据类型。可以写如下的判断逻辑:
let isString = (obj) => {
return
Object.prototype.toString.call(obj) === '[object String]';
};
let isFunction = (obj) => {
return Object.prototype.toString.call(obj)
=== '[object Function]';
};
let isArray = (obj) => {
return
Object.prototype.toString.call(obj) === '[object Array]';
};
let isSet = (obj) => {
return
Object.prototype.toString.call(obj) === '[object Set]';
};
// ...可以看到,出现了非常多重复的逻辑。我们将它们做一下封装:
let isType = (type) => {
return (obj) => {
return
Object.prototype.toString.call(obj) === `[object ${type}]`;
}
}
现在我们这样做即可:
let isString = isType('String');
let isFunction = isType('Function');
//...相应的
//isString和isFunction是由isType生产出来的函数,
//但它们依然可以判断出参数是否为String(Function),而且代码简洁了不少。
isString("123");
isFunction(val => val);isType这样的函数我们称为thunk 函数。它的核心逻辑是接收一定的参数,生产出定制化的函数,然后使用定制化的函数去完成功能。thunk函数的实现会比单个的判断函数复杂一点点,但就是这一点点的复杂,大大方便了后续的操作。
Generator和异步
let fs = require('fs')
function read(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if (err) reject(err)
resolve(data)
})
})
}
function* r() {
let r1 = yield read('./1.txt')
let r2 = yield read(r1)
let r3 = yield read(r2)
console.log(r1)
console.log(r2)
console.log(r3)
}
let it = r()
let { value, done } = it.next()
value.then(function(data) { // value是个promise
console.log(data) //data=>2.txt
let { value, done } = it.next(data)
value.then(function(data) {
console.log(data) //data=>3.txt
let { value, done } = it.next(data)
value.then(function(data) {
console.log(data) //data=>结束
})
})
})
// 2.txt=>3.txt=>结束
采用 co 库
以上我们针对 thunk 函数和Promise两种Generator异步操作的一次性执行完毕做了封装,但实际场景中已经存在成熟的工具包了,如果大名鼎鼎的co库, 其实核心原理就是我们已经手写过了(就是刚刚封装的Promise情况下的执行代码),只不过源码会各种边界情况做了处理。使用起来非常简单:
安装co库只需:npm install co
上面例子只需两句话就可以轻松实现
function* r() {
let r1 = yield read('./1.txt')
let r2 = yield read(r1)
let r3 = yield read(r2)
console.log(r1)
console.log(r2)
console.log(r3)
}
let co = require('co')
co(r()).then(function(data) {
console.log(data)
})
// 2.txt=>3.txt=>结束=>undefined
4.async+await:
凡是加上async的函数都默认返回一个Promise对象,并且async+await也能让异步代码以同步的方式来书写;async用来申明一个function是异步的,而await用于等待一个异步方法执行完成;
async函数返回的是一个Promise对象;如果在函数中return一个直接量,async会把这个直接量通过Promise.resolve()封装成Promise对象;
await 到底在等啥
一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。
因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。
由于promise传递参数的过程比较复杂,所以用async+await的方法可以简化代码;
参考:
www.ruanyifeng.com/blog/2012/1…