异步和性能
1.异步:现在与将来
1.1什么是异步运行?
程序中将来执行的部分并不一定在现在运行的代码之后就立即执行,换句话说,现在无法完成的任务将会异步完成。因此并不会出现人们本能的认为会出现或者希望出现的阻塞情况。
任何时候,只要把一段代码包装成一个函数,并制定它在相应某个事件(定时器,鼠标点击,ajax相应等)时执行,你就是在代码中创建了一个将来执行的块。也由此在程序中引入了异步机制。
1.2什么是时间循环
所有的JS的运行环境都有一个共同的特点,即他们都提供一种机制来处理程序中多个块的执行,且执行每个块时调用JS引擎,这种机制被称为时间循环。如果在队列中有等待的时间,那么就会从队列中摘下一个事件并执行。这些事件就是你的回调函数。定时器的第二个参数设置一个毫秒值的意思是在这个毫秒只之后将这个回调函数放在事件循环队列中,如果队列中本来就有等待的任务,那么这个刚刚放进去的回调函数并不会立即执行,而是必须要等待前面的任务执行完了之后再执行。
1.3异步和并行是一回事吗?
异步和并行常常被混为一谈,但是实际上他们的意义完全不同。记住,异步是关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行:在不用的处理器,但多个线程能够共享单个进程的内存。与之相对的是,事件循环是把自身的工作分成一个个任务并且顺序执行,不允许对共享内存的并行访问和修改。JS中的函数是具有运行完整性的,就是说这个函数没有执行完,就不能执行下一个函数或者别的语句。这在一定程度上避免了多线程带来的运行结果的不确定性。但是JS的运行结果也不是绝对确定的,因为有时候并不能确定哪个函数先执行。在JS中,这种函数顺序的不确定性就是通常所说的竞态条件。
1.4你知道并行和并发的区别吗,你知道进程和线程的区别吗?
并行和并发的区别在于“同时”,举个例子:当你吃饭的时候来了个电话,你停下了手中的筷子,拿起手机接电话这可以看成并发。当然,你也可以一般吃饭一边接电话,这就是并行。并发的关键是你有处理多个任务的能力,但是不一定同时执行,并行是你可以同时处理多个任务。并行和并发都是可以处理多个线程的,只不过是一个cpu处理多个线程我们叫并发,多个cpu处理多个线程我们叫并行。
通常需要将这些“并发”(有别于操作系统中的并发概念)执行的进程进行某种形式的交互协调,比如需要确保执行顺序或者需要放置竞态的出现。这些“进程”也可以把自己分成更小的块以便其他“进程”可以插进去。
进程和线程是包含关系,举个例子:一个工程的供电设备功率小,只能支持一个车间的工作,一个车间就是一个进程,这就代表一个cpu一个时刻只能处理一个进程。但是一个车间里面有好多工人,这些工人的都是为这个车间工作的,工人就代表线程,就是所一个进程里面可以有多个线程。同时一个车间的工人们活动的空间是相同的,就是说线程可以享用同一块内存。
2.回调
回调是编写和处理JS程序异步逻辑的最常用的方式,确实,回调是这门语言中最基础的异步形式。事件循环队列处理到这个项目的时候就会运行回调函数。换句话说,回调函数包裹或者说封装了程序的延续。
2.1回调的问题是什么?
虽然在执行层级上我们的大脑是以异步的方式运作的,但是我们的任务计划还是以顺序,同步的方式进行的:“我要先去商店,然后买点牛奶,然后去干洗店。“我们在思考的时候一般是按照顺序仔细的计划着,并且会假定有某一种形式的临时阻塞来保证B会等待A完成,C会等待B完成。开发者在写代码的时候也是在仔细计划着一系列动作的发生。我们的思考方式是一步一步的,但是从同步转换到异步之后,可用的工具(回调)却不是按照一步一步的方式来表达的。我们的顺序阻塞式的大脑计划行为无法很好地映射到面向回调的异步代码。这就是回调方式最主要的缺陷:对于他们在代码中表达异步的方式,我们的大脑需要努力才能同步得上。
还有一个问题就是信任问题,我们平时用的一些支持回调的函数,比如setTimeout,或者第三方的ajax,这些函数的具体实现都不是我们可见的,所以当我们把自己的回调函数交给这些函数的时候,我们能够保证我们的回调函数就会按照我们预期的结果来执行吗?这就是可信任问题,并不是所有的第三方库中的函数都是100%可信任的。我们把自己程序一部分的执行控制交给某个第三方库的情况叫做控制反转。回调最大的问题就是控制反转,它会导致信任连的完全断裂。
2.2尝试挽救回调
一种尝试挽救回调的方法是分离回调,就是将成功和失败的回调进行分离而不是在一个回调中进行判断。。但是这并没有涉及阻止或者过滤不想要的重复调用回调的问题。反而现在的情况更加糟糕了,因为现在你可能得到成功或者失败的结果,或者都没有,并且你还不得不编码处理这些问题。
永远异步调用回调,即使就在事件循环的下一轮。
3.Promise
如果我们不把自己程序的continuation传给第三方,而是希望第三方给我们提供了解其任务何时结束的能力,然后由我们自己来决定下一步怎么做,这种范式就称为Promise。Promise决议后就是外部不可变的值,我们可以安全地把这个值传递给第三方,并且确信它不会被有意无意地修改。不可变性听起来似乎是一个学术话题,但实际上这是Promise设计中最基础和最重要的因素,我们不应该随意忽略这一点。
对一个Promise调用then的时候,即使这个Promise已经决议,提供给then()的回调也总是被异步调用。一个Promise决议后,这个Promise上所有的通过then()注册的回调都会在当前执行栈的最后执行,在事件循环队列的前面执行。如果你对一个Promise注册了一个完成回调和一个拒绝回调,那么Promise在决议的时总会调用其中的一个。又因为Promise只能被决议一次,所有then注册的回调最多被调用一次,所以then注册的回调只能被调用一次。
当我们需要给resolve或者reject传递多个值的时候,必须把多个值封装到一个对象或者数组中。因为除了第一个参数之后的参数都会被默默忽略。示例代码如下:
var p = new Promise((res,rej)=>{
setTimeout(()=>{
res(['first','second'])
},1000)
})
p.then((value)=>{
console.log(value)
})
从Promise.resolve()得到的是一个真正的Promise,是一个可以信任的值。如果你传入的已经是一个真正的Promise,那么你得到的就是它本身,如果传入的是一个立即值,那么也会返回一个Proimse。示例代码如下:
var p = new Promise((res,rej)=>{
setTimeout(()=>{
rej('123')//rejected PromiseResult='123'
res('123')//fullfilled PromiseResult='123'
},0)
})
console.log(Promise.resolve(p))
console.log(Promise.resolve('啦啦'))//fullfilled PromiseResult='啦啦'
注意:当我们传一个Promise的时候,resolve只是把这个Promise展开,这个Promise是完成那么依旧返回完成,如果是拒绝那么还是返回拒绝,而不是任何经过resolve的Promise都会变成完成。
当我们使用Promise的时候,不管有多少个异步的步骤,每一个步骤都能根据需要等待下一个步骤,或者不等!下面的代码是依次异步读取三个文件,并把读取结果放在一个数组里面的示例:
const fs = require('fs')
new Promise((res,rej)=>{
let value = []
fs.readFile('./1.txt',(err,data)=>{
value.push(data.toString())
res(value)
})
}).then((value)=>{
return new Promise((res,rej)=>{
fs.readFile('./2.txt',(err,data)=>{
value.push(data.toString())
res(value)
})
})
}).then((value)=>{
return new Promise((res,rej)=>{
fs.readFile('./3.txt',(err,data)=>{
value.push(data.toString())
res(value)
})
})
}).then((value)=>{
console.log('content: ',value)
})
Promise的异步执行顺序:
var p = new Promise((res,rej)=>{
setTimeout(()=>{
res()
},1000)
})
setTimeout(()=>{
console.log('E')
},0)
p.then(()=>{
setTimeout(()=>{
console.log('A')
},0)
p.then(()=>{
console.log('B')
})
console.log('C')
})
p.then(()=>{
setTimeout(()=>{
console.log('D')
},0)
})
Promise只能被决议一次,后面的决议会自动忽略掉,示例代码如下:
var obj = {
then: (res,rej)=>{
res()
rej('oops')//不会执行到这里
}
}
var p = Promise.resolve(obj)
p.then((value)=>{
console.log('success')
},
()=>{
console.log('fail')
})
Promise.all([])和Promise.race([])一个是门,都完成了才会返回Promise,一个是竞态,只要有一个完成就返回结束了,然后返回完成的这个。all返回一个有所有传入的Promise的完成消息组成的数组,与指定的顺序一致。如果这些Promise中有任何一个被拒绝的话,all方法就会立刻返回拒绝,并丢弃所有来自其他Promise的全部结果。race也是有任何一个被拒绝就会拒绝。如果传的是一个空数组,就永远不会决议。all传入空数组会立即完成。
4.生成器
现在我们把注意力转移到一种顺序、看似同步的异步流程控制表达风格。使这种风格成为可能的魔法是ES6的生成器。
4.1打破完整运行
在前面我们了解到,一个函数一旦开始执行,就会运行到结束,期间不会有其他代码能够打断它并插入其间。不过,ES6引入了一种新的函数类型,它并不符合这种运行到结束的特性,这类新的函数被称为生成器。
下面的一个小的demo可以帮助你理解一下生成器是怎么工作的:
var x = 1
function *foo(){
x++
yield
console.log(x)
}
function bar(){
x++
}
var it = foo()//得到一个迭代器
it.next()//开启foo函数的执行,当遇到yield的时候就会暂停
bar()//这时候已经暂停了,调用bar函数
it.next()//再次启动foo函数,这时会输出3
yield会导致生成器在执行过程中发送一个值,这有点类似于中间return。这个next函数调用的结果是一个对象,他有一个value属性,持有从*foo()返回的值。我们在这里需要阐述一个重要的事实:即消息是双向传递的,yield作为一个表达式可以发出消息相应next()调用,next()也可以向暂停的yield表达式发送值。这就是说在生成器的执行过程中构成了一个双向消息传递系统,示例代码如下:
function *foo(x){
var y = x*(yield "hello")
return y
}
var it = foo(6)
var res = it.next()
console.log(res.value)//hello
res = it.next(7)
console.log(res.value)//42
next()函数的返回值,是从这个next开始到yield为止,生成器中函数的运行结果,如果yield指定了返回值,那么就是这个指定的值。然后next的参数的值,是赋给这个yield的。相当于next起到了一个承上启下的作用。
4.2生产器产生值
for···of循环在每次迭代中自动调用next(),它不会向next()传入任何值。并且会在接受到done:true之后自动停止。这对于在一组数据上循环很方便。
Object内部没有迭代器,可以通过如下方式来遍历对象。数组,类数组,map,set都是有默认的迭代器的。
var obj = {
name:'zh',
age:18
}
//方式一
for(var key of Object.keys(obj)){
console.log(obj[key])
}
//方式二
for(var i in obj){
console.log(obj[i])
}
for···of循环遍历数组拿到的是数组的元素,for···in循环拿到的是数组的索引。
4.2.1 iterable和迭代器。
iterable指一个包含可以在其值上迭代的迭代器的对象。从ES6开始,从一个iterable中提取迭代器的方法是:iterable必须支持一个函数,这个函数的名称是固定的,就是Symbol.iterable。调用这个函数时,它会返回一个迭代器。通常每次调用会返回一个全新的迭代器,虽然这一点并不是必须的。当一个对象中有一个next()方法的时候,这个对象就叫做一个迭代器。
4.2.2生成器和迭代器
可以把生成器看做一个值的生产者,我们通过迭代器接口的next()调用一个就获取一个值。严格来说,生成器本生并不是iterable,尽管非常类似--当你执行一个生成器时,就会得到一个迭代器。
4.3异步迭代生成器
一个用生成器来进行的异步操作。我们在生成器内部有完全同步的代码,但隐藏在背后的是,foo(···)内部的运行可以完全异步。
//这个函数封装一个异步的操作
function foor(x,y){
setTimeout(()=>{
it.next(x+y)
},3000)
}
function *main(){
var text = yield foor(12,17)//yield关键字把生成器阻塞了
console.log('text:'+text)//29
}
var it = main()
it.next()