ES6-Async & 异步
Posted 橙云生
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ES6-Async & 异步相关的知识,希望对你有一定的参考价值。
依赖文件地址 :https://github.com/chanceLe/ES6-Basic-Syntax/tree/master/js
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title>[es6]-16-异步操作和Async函数</title> 6 <script src="./js/browser.js"></script> 7 <script src="./js/babel-pollyfill.js"></script> 8 <script type="text/babel"> 9 /* 10 * 异步编程对js语言太重要。js语言的执行环境是单线程的,如果没有异步编程,根本没法用。 11 * 12 * ES6以前,异步编程的方法,大概有四种: 13 * 回调函数 事件监听 发布/订阅 Promise对象 14 * 15 * ES6将js异步编程带入了一个新阶段,ES7的Async函数更是提出了异步编程的终极解决方案。 16 * 17 * 所谓异步,就是一个任务分成两段,先执行一段,然后转而执行其他任务,等做好了准备,再回头执行第二段。 18 * 这种不连续的执行,就叫异步。 19 * 相应的,连续的执行叫同步,由于是连续执行,所以要等待IO,这时候程序暂停,cpu浪费。 20 * 21 * 回调函数: 22 * js语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面。 23 * 等到重新执行这个任务的时候,就直接调用这个函数。 24 * 回调函数不多说,这里只说Node.js约定,回调函数的第一个参数是err,原因是执行分成两段,这两段之间抛出的 25 * 错误,程序无法捕捉,只能当做参数,传入第二段。 26 * 27 * Promise: 28 * 回调函数本身并没有问题,问题出在多个回调函数嵌套。如果多重嵌套,代码就横向发展,很快就乱成一团,这种 29 * 情况称为 回调函数噩梦。 30 * Promise就是为了解决回调函数噩梦的问题提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套, 31 * 改成链式调用。 32 * 33 * Promise 的写法只是回调函数的改进,使用then方法后,异步任务的两段执行看的更清楚了,除此之外,并无新意。 34 * Promise的最大问题是代码冗余,原来的任务被Promise包装了以下,不管什么操作,一眼看去都是then,原来的语义 35 * 变得很不清楚。 36 * 37 * 更好的写法... 38 * Generator函数 39 * 协程:传统的编程语言,早有异步编程解决方案。其中有一种叫做协程,意思就是多个线程互相协作,完成异步任务。 40 * 协程有点像函数,又有点像线程。运行流程大致如下: 41 * 第一步,协程A开始执行。 42 * 第二步,协程A执行到一半,进入暂停,执行权转到协程B。 43 * 第三步,(一段时间后)协程B交还执行权。 44 * 第四步,协程A恢复执行。 45 * 举例来说,读取文件的协程写法如下: 46 * function* asyncJob(){ 47 * // 其他代码 48 * var f = yield readFile(fileA); 49 * //其他代码 50 * } 51 * 上面代码的奥妙在于其中的yield命令。它表示执行到此处,执行权交给其他协程。也就是说yield是异步两个阶段的分界线。 52 * 协程遇到yield命令会暂停,等到执行权返回,再从暂停的地方继续往后执行。它最大的优点是代码的写法 53 * 非常像同步操作,除了yield命令,几乎一模一样。 54 * 55 * Generator函数的概念 56 * Generator函数是协程在ES6的实现,最大的特点就是可以交出函数的执行权(即暂停执行)。 57 * 整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。 58 * 59 * 60 * 异步任务的封装: 61 * var fetch = require("node-fetch"); 62 * function *gen(){ 63 * var url = ‘https://api.github.com/users/github‘; 64 * var result = yield fetch(url); 65 * console.log(result.bio) 66 * } 67 * 68 * 上面代码中,Generator函数封装了一个异步操作,该操作先读取一个远程接口,然后从json格式的数据解析信息。 69 * 执行这段代码的方法如下: 70 * var g = gen(); 71 * var result = g.next(); 72 * result.value.then(function(data){ 73 * return data.json(); 74 * }).then(function(data){ 75 * g.next(data); 76 * }) 77 * 78 * 虽然Generator函数将异步操作表示的很简洁,但是流程管理却不方便,(什么时候执行第一段,什么时候执行第二段) 79 */ 80 81 /* 82 * Thunk函数 83 * 参数的求值策略: 84 * Thunk函数在上世纪60年代就诞生了。那时,编程语言刚起步,计算机科学家还在研究,编译器怎么写比较好。 85 * 争论的一个焦点是 求值策略,即函数的参数到底应该何时求值。 86 * 87 * 一种意见是传值调用,即在进入函数体之前,就计算参数表达式的值。 88 * 另一种是传名调用,直接将函数的参数表达式传入函数体,只在用到它的时候求值,Haskell语言采用这种策略。 89 * 90 * 传值调用比较简单,时但是催参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。 91 * 92 * Thunk函数的含义: 93 * 编译器的传名调用的实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体,这个临时函数就叫做Thunk函数。 94 */ 95 { 96 var x = 2; 97 function f(m){ 98 return m*2; 99 } 100 console.log(f(x+5)); //14 101 //等同于 102 var thunk = function(){ 103 return x+5; 104 } 105 function f1(thunk){ 106 return thunk()*2; 107 } 108 109 //上面代码中,函数f的参数x+5被一个函数替换了。凡是用到原参数的地方,对thunk函数求值即可。 110 } 111 //这就是thunk函数的定义:它是传名调用的一种实现策略,用来替换某个表达式。 112 113 /* 114 * js中的Thunk函数 115 * js是传值调用,它的Thunk函数含义有所不同。在js语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本 116 * ,且只接受回调函数作为参数。 117 */ 118 /* 119 { 120 //正常版本的readFile(多参数版本) 121 fs.readFile(filename,callback); 122 //Thunk版本的readFile(单参数版本) 123 var readFileThunk = Thunk(fileName); 124 readFileThunk(callback); 125 126 var Thunk = function(fileName){ 127 return function(callback){ 128 return fs.readFile(fileName,callback); 129 } 130 } 131 132 //上面代码中,fs模块的readFile方法是一个多参数函数,两个参数分别是文件名和回调函数。 133 //经过转换器处理,它变成了一个单参数函数,只接受回调函数作为参数。 134 //这个单参数版本,就叫做Thunk函数。 135 } 136 //任何函数,只要参数有回调函数,就能协程Thunk函数的形式。下面是一个简单的Thunk函数转换器 137 138 { 139 //es5版本 140 var Thunk = function(fn){ 141 return function(){ 142 var args = Array.prototype.slice.call(arguments); 143 return function(callback){ 144 args.push(callback); 145 return fn.apply(this,args); 146 } 147 148 } 149 } 150 } 151 { 152 //es6 版本 153 var Thunk = function(fn){ 154 return function(...args){ 155 return function(callback){ 156 return fn.call(this,...args,callback); 157 } 158 } 159 } 160 } 161 */ 162 /* 163 * Thunk函数在以前确实没什么用,但有了ES6的Generator函数之后,Thunk函数可以用于Generator函数的 164 * 自动流程管理。 165 */ 166 //Generator函数可以自动执行 167 168 { 169 function* gen(){ 170 //some code 171 yield 1; 172 yield 2; 173 yield 3; 174 yield 4; 175 } 176 var g = gen(); 177 var res = g.next(); 178 179 while(!res.done){ 180 console.log(res.value); 181 res = g.next(); 182 } 183 } 184 /* 185 上面代码中,Generator函数gen会自动执行完所有步骤。但是,这并不适合异步操作。 186 如果必须保证前一步执行完,后一步才执行,那上面的自动执行就不可行。这时Thunk函数就能派上用场。 187 在Generator函数中,yield命令用于将程序的执行权移出Generator函数,那么需要一种方法将 188 执行权还给Generator函数。这种方法就是Thunk函数,因为它可以在回调函数里,将执行权交还给 189 Generator函数。 190 */ 191 192 //Thunk函数真正的威力,在于可以自动执行Generator函数。 193 //下面是一个基于Thunk函数的Generator执行器。 194 195 function run(fn){ 196 var ge = fn(); 197 function next(err,data){ 198 var result = ge.next(data); 199 if(result.done) return; 200 // result.value(next); 201 console.log(result.value); 202 result.value = next(); 203 204 } 205 next(); 206 } 207 function* g4(){ 208 yield 1; 209 yield 2; 210 yield 3; 211 yield 4; 212 } 213 run(g4); 214 /* 215 * 上面的代码的run函数,就是一个Generator函数的自动执行器。内部的next函数就是Thunk的 216 * 回调函数。next先将指针移到Generator函数的下一步。然后判断Generator函数是否结束,如果没结束,就将 217 * next函数,再传入Thunk函数(result.value属性),否则直接退出。 218 * 有了这个执行器,执行Generator函数方便多了。不管内部有多少个异步操作,直接把Generator函数传入 219 * run函数即可。当然前提是,每个异步操作都要是Thunk函数,也就是说跟在yield命令后面的必须是Thunk函数。 220 * 221 * var g = function*(){ 222 * var f1 = yield readFile("fileA") 223 * var f2 = yield readFile("fileB") 224 * var f3 = yield readFile("fileC") 225 * ... 226 * var f4 = yield readFile("fileD") 227 * } 228 * run(g); 229 * 230 * 上面代码中,函数g封装了n个异步的读取文件操作,只要执行run函数,这些操作就会自动完成。 231 * 这样一来,异步操作不仅可以写的像同步操作,而且一行代码就可以执行。 232 * 233 * Thunk函数并不是唯一自动执行Generator函数的方案。因为自动执行的关键是,必须有一种机制, 234 * 自动控制Generator函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise对象也可以做到这一点。 235 */ 236 237 /* 238 * co模块 239 * 是著名程序员TJ Holowaychuk于2013年6月发布的小工具,用于Generator函数的自动执行。 240 * 比如有一个Generator函数,co模块可以让你不用编写Generator函数的执行器。 241 * var co = require(‘co‘); 242 co(gen); 243 上面代码中,Generator函数只要传入co函数,就会自动执行。 244 co函数返回一个Promise对象,因此可以用then方法添加回调函数。 245 246 co(gen).then(function(){ 247 console.log("Generator函数执行完成。") 248 }) 249 250 co模块的原理? 251 Generator就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。 252 两种方法可以做到这点: 253 1.回调函数。 将异步操作包装成Thunk函数,在回调函数里面交回执行权。 254 2.Promise对象。将异步对象包装成Promise对象,用then方法交回执行权。 255 256 co模块其实就是将两种自动执行器(Thunk函数和Promise对象),包装成一个模块。 257 使用co的前提条件是,Generator函数的yield后面,只能在是Thunk函数或Promise对象。 258 */ 259 260 /* 261 *基于Promise对象的自动执行。 262 * 跟Thunk函数的差别就是用then方法,层层添加回调函数具体实现如下 263 */ 264 /* 265 function run(gen){ 266 var g = gen(); 267 function next(data){ 268 var result = g.next(data); 269 if(result.done){ 270 return result.value; 271 } 272 result.value.then(function(data){ 273 next(data); 274 }) 275 } 276 next(); 277 } 278 279 run(gen); 280 //上面代码中,只要Generator函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。 281 * */ 282 283 284 /* 285 * co模块是Generator自动执行器的扩展,源码只有几十行非常简单。 286 * 首先,co函数接受Generator函数作为参数,返回一个Promise对象。 287 288 289 function co(gen){ 290 var ctx = this; 291 return new Promise(function(resolve,reject){ 292 }) 293 } 294 */ 295 /* 296 * 在返回的Promise对象里面,co先检查函数gen是否为Generator函数。如果是,就执行该函数,得到一个内部指针对象 297 * 如果不是就返回,并将Promise的状态改为resolved。 298 299 function co(gen){ 300 var ctx = this; 301 return new Promise(function(resolve,reject){ 302 if(typeof gen === ‘function‘){ 303 gen = gen.call(ctx); 304 } 305 if(!gen || typeof gen.next !== ‘function‘){ 306 return resolve(gen); 307 } 308 }) 309 } 310 */ 311 312 /* 313 * 接着,co将Generator函数的内部指针对象的next方法,包装成onFullfilled函数。 314 * 这主要是为了能够捕捉抛出的错误。 315 */ 316 317 function co(gen){ 318 var ctx = this; 319 return new Promise(function(resolve,reject){ 320 if(typeof gen === ‘function‘){ 321 gen = gen.call(ctx); 322 } 323 if(!gen || typeof gen.next !== ‘function‘){ 324 return resolve(gen); 325 } 326 327 onFullfilled(); 328 function onFullfilled(res){ 329 var ret; 330 try { 331 ret = gen.next(res); 332 }catch(e){ 333 return reject(e); 334 } 335 next(ret); 336 } 337 }) 338 } 339 //最后,就是关键的next函数,它会反复调用自身。 340 function next(ret){ 341 if(ret.done){ 342 return resolve(ret.value); 343 } 344 var value = toPromise.call(ctx,ret.value); 345 if(value && isPromise(value)){ 346 return value.then(onFullfilled,onRejected); 347 } 348 return onRejected(new TypeError("You may only yield a function,promise,generator," 349 +"array or object,but the following object was passed:‘"+String(ret.value)+"‘")); 350 } 351 352 /* 353 * 上面代码中,next函数的内部代码, 354 * 第一行,检查当前行为是否为Generator函数的最后一步,如果是就返回。 355 * 第二行确保每一步的返回值,是Promise对象。 356 * 第三行,使用then方法,为返回值加上回调函数,然后通过onFullfilled函数再次调用next函数。 357 */ 358 359 /* 360 * 处理并发的异步操作 361 * co支持并发的异步操作,即允许某些操作同时进行,等到他们全部完成,才进行下一步。 362 * 这时,要把并发的操作都放在数组或对象里面,跟在yield语句后面。 363 * 364 */ 365 //数组的写法 366 co(function*(){ 367 var res = yield [ 368 Promise.resolve(1), 369 Promise.resolve(2) 370 ]; 371 console.log(res); 372 }).catch(onerror); 373 //对象的写法 374 co(function*(){ 375 var res = yield { 376 1:Promise.resolve(1), 377 2:Promise.resolve(2) 378 }; 379 console.log(res); 380 }).catch(onerror); 381 382 //下面还有一个例子 383 co(function*(){ 384 var values = [n1,n2,n3]; 385 yield values.map(somethingAsync); 386 }); 387 function* somethingAsync(x){ 388 //do something async 389 return y; 390 } 391 //上面的代码允许并发三个somethingAsync异步操作,等到他们全部完成,才会进行下一步。 392 </script> 393 </head> 394 <body> 395 </body> 396 </html>
以上是关于ES6-Async & 异步的主要内容,如果未能解决你的问题,请参考以下文章
[工作积累] UE4 并行渲染的同步 - Sync between FParallelCommandListSet & FRHICommandListImmediate calls(代码片段