├── .babelrc ├── .gitignore ├── README.md ├── data ├── data1.json └── data2.json ├── package.json ├── part1-basic ├── 01-what-is-async.md ├── 02-event-loop.md └── 03-event-bind.md ├── part2-jquery ├── 01-jquery-ajax.md ├── 02-jquery-deferred.md ├── 03-jquery-promise.md └── test.html ├── part3-promise ├── 01-promise-in-es6.md ├── 02-promise-use.md ├── 03-promise-standard.md ├── 04-promise-callback.md ├── 05-promise-q.md └── test.js ├── part4-generator ├── 01-generator-in-es6.md ├── 02-iterator.md ├── 03-iterator-use.md ├── 04-thunk.md ├── 05-generator-for-async.md ├── 06-generator-for-koa.md ├── 07-generator-callback.md └── test.js ├── part5-async-await ├── 01-async-await-in-es7.md ├── 02-use-in-node-v6.md ├── test-entry.js └── test.js └── part6-end └── 01-summary.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["stage-3", "es2015"], 3 | "plugins": ["transform-runtime"] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 深入理解 JavaScript 异步 2 | 3 | ## 前言 4 | 5 | 2014年秋季写完了《[深入理解javascript原型和闭包系列](http://www.cnblogs.com/wangfupeng1988/p/4001284.html)》,已经帮助过很多人走出了 js 原型、作用域、闭包的困惑,至今仍能经常受到好评的留言。 6 | 7 | 很早之前我就总结了**JS三座大山**这个概念(虽然没有到处宣扬),前两座(原型、作用域)已经基本讲明白,而第三座(异步)也应该做一个总结。 8 | 9 | 于是,2017年初春,我花费大约一周的业余时间来对 JS 异步做一个完整的总结,和各位同学共勉共进步! 10 | 11 | ## 目录 12 | 13 | **part1 基础部分** 14 | 15 | - [什么是异步](./part1-basic/01-what-is-async.md) 16 | - [异步和 event-loop](./part1-basic/02-event-loop.md) 17 | - [事件绑定算不算异步?](./part1-basic/03-event-bind.md) 18 | 19 | **part2 jQuery的解决方案** 20 | 21 | - [jQuery-1.5 之后的 ajax](./part2-jquery/01-jquery-ajax.md) 22 | - [jQuery deferred](./part2-jquery/02-jquery-deferred.md) 23 | - [jQuery promise](./part2-jquery/03-jquery-promise.md) 24 | 25 | **part3 ES6-Promise** 26 | 27 | - [Promise 加入 ES6 标准](./part3-promise/01-promise-in-es6.md) 28 | - [Promise 在 ES6 中的具体应用](./part3-promise/02-promise-use.md) 29 | - [对标一下 Promise/A+ 规范](./part3-promise/03-promise-standard.md) 30 | - [Promise 真的取代 callback 了吗?](./part3-promise/04-promise-callback.md) 31 | - [用 Q.js 库](./part3-promise/05-promise-q.md) 32 | 33 | **part4 Generator** 34 | 35 | - [ES6 中的 Generator](./part4-generator/01-generator-in-es6.md) 36 | - [Iterator 遍历器](./part4-generator/02-iterator.md) 37 | - [Generator 的具体应用](./part4-generator/03-iterator-use.md) 38 | - [Thunk 函数](./part4-generator/04-thunk.md) 39 | - [Generator 与异步操作](./part4-generator/05-generator-for-async.md) 40 | - [koa 中使用 Generator](./part4-generator/06-generator-for-koa.md) 41 | - [Generator 的本质是什么?是否取代了 callback](./part4-generator/07-generator-callback.md) 42 | 43 | **part5 async-await** 44 | 45 | - [ES7 中引入 async-await](./part5-async-await/01-async-await-in-es7.md) 46 | - [如何在 nodejs `v6.x`版本中使用 async-await](./part5-async-await/02-use-in-node-v6.md) 47 | 48 | **part6 总结** 49 | 50 | - [总结](./part6-end/01-summary.md) 51 | 52 | ## 运行程序的说明 53 | 54 | 要求本地 node 在`v6`或以上版本,然后执行以下命令下载代码并安装依赖的插件 55 | 56 | ```shell 57 | $ cd ~ 58 | $ git clone git@github.com:wangfupeng1988/js-async-tutorial.git 59 | $ cd js-async-tutorial 60 | $ npm i 61 | ``` 62 | 63 | 最后,本地可能需要启动一个静态服务器来运行页面,我使用`http-server`插件 64 | 65 | ```shell 66 | $ npm install http-server -g 67 | $ cd js-async-tutorial 68 | $ http-server -p 8881 69 | ``` 70 | 71 | 然后浏览器访问`http://localhost:8881/xxx/xxx.html`即可 72 | 73 | ## 关于作者 74 | 75 | - 关注作者的博客 - 《[深入理解javascript原型和闭包系列](http://www.cnblogs.com/wangfupeng1988/p/4001284.html)》《[深入理解javascript异步系列](https://github.com/wangfupeng1988/js-async-tutorial)》《[换个思路学习nodejs](https://github.com/wangfupeng1988/node-tutorial)》《[CSS知多少](http://www.cnblogs.com/wangfupeng1988/p/4325007.html)》 76 | - 学习作者的教程 - 《[前端JS高级面试](https://coding.imooc.com/class/190.html)》《[前端JS基础面试题](http://coding.imooc.com/class/115.html)》《[React.js模拟大众点评webapp](http://coding.imooc.com/class/99.html)》《[zepto设计与源码分析](http://www.imooc.com/learn/745)》《[json2.js源码解读](http://study.163.com/course/courseMain.htm?courseId=691008)》 77 | 78 | ## 求打赏 79 | 80 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 81 | 82 |  83 | -------------------------------------------------------------------------------- /data/data1.json: -------------------------------------------------------------------------------- 1 | ["a", "b", 100, true] -------------------------------------------------------------------------------- /data/data2.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": 100, 3 | "b": 200 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-async-tutorial", 3 | "version": "1.0.0", 4 | "description": "深入理解 JavaScript 异步", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/wangfupeng1988/js-async-tutorial.git" 12 | }, 13 | "author": "https://github.com/wangfupeng1988", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/wangfupeng1988/js-async-tutorial/issues" 17 | }, 18 | "homepage": "https://github.com/wangfupeng1988/js-async-tutorial#readme", 19 | "dependencies": { 20 | "babel-core": "^6.23.1", 21 | "babel-plugin-transform-runtime": "^6.23.0", 22 | "babel-preset-es2015": "^6.22.0", 23 | "babel-preset-stage-3": "^6.22.0", 24 | "babel-runtime": "^6.23.0", 25 | "co": "^4.6.0", 26 | "node-fetch": "^1.6.3", 27 | "q": "^1.4.1", 28 | "thunkify": "^2.1.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /part1-basic/01-what-is-async.md: -------------------------------------------------------------------------------- 1 | # 什么是异步 2 | 3 | 提醒:如果你是初学 js 的同学,尚未有太多项目经验和基础知识,请就此打住,不要看这篇教程 4 | 5 | 我思考问题、写文章一般都不按讨论出牌,别人写过的东西我不会再照着抄一遍。因此,后面所有的内容,都是我看了许多资料之后,个人重新思考提炼总结出来的,这肯定不能算是初级教程。 6 | 7 | 如果你是已有 js 开发经验,并了解异步的基础知识,到这里来想深入了解一下`Promise` `Generator`和`async-await`,那就太好了,非常欢迎。 8 | 9 | 10 | 11 | ## 本节内容概述 12 | 13 | - JS 为何会有异步 14 | - 异步的实现原理是什么 15 | - 常用的异步操作有哪些 16 | 17 | ## JS 为何会有异步 18 | 19 | 首先记住一句话 —— **JS 是单线程的语言**,所谓“单线程”就是一根筋,对于拿到的程序,一行一行的执行,上面的执行为完成,就傻傻的等着。例如 20 | 21 | ```javascript 22 | var i, t = Date.now() 23 | for (i = 0; i < 100000000; i++) { 24 | } 25 | console.log(Date.now() - t) // 250 (chrome浏览器) 26 | ``` 27 | 28 | 上面的程序花费 250ms 的时间执行完成,执行过程中就会有卡顿,其他的事儿就先撂一边不管了。 29 | 30 | 执行程序这样没有问题,但是对于 JS 最初使用的环境 ———— 浏览器客户端 ———— 就不一样了。因此在浏览器端运行的 js ,可能会有大量的网络请求,**而一个网络资源啥时候返回,这个时间是不可预估的。这种情况也要傻傻的等着、卡顿着、啥都不做吗?**———— 那肯定不行。 31 | 32 | 因此,JS 对于这种场景就设计了异步 ———— 即,发起一个网络请求,就先不管这边了,先干其他事儿,网络请求啥时候返回结果,到时候再说。这样就能保证一个网页的流程运行。 33 | 34 | 35 | ## 异步的实现原理 36 | 37 | 先看一段比较常见的代码 38 | 39 | ```javascript 40 | var ajax = $.ajax({ 41 | url: '/data/data1.json', 42 | success: function () { 43 | console.log('success') 44 | } 45 | }) 46 | ``` 47 | 48 | 上面代码中`$.ajax()`需要传入两个参数进去,`url`和`success`,其中`url`是请求的路由,`success`是一个函数。这个函数传递过去不会立即执行,而是等着请求成功之后才能执行。**对于这种传递过去不执行,等出来结果之后再执行的函数,叫做`callback`,即回调函数** 49 | 50 | 再看一段更加能说明回调函数的 nodejs 代码。和上面代码基本一样,唯一区别就是:上面代码时网络请求,而下面代码时 IO 操作。 51 | 52 | ```javascript 53 | var fs = require('fs') 54 | fs.readFile('data1.json', (err, data) => { 55 | console.log(data.toString()) 56 | }) 57 | ``` 58 | 59 | 从上面两个 demo 看来,**实现异步的最核心原理,就是将`callback`作为参数传递给异步执行函数,当有结果返回之后再触发 `callback`执行**,就是如此简单! 60 | 61 | 62 | ## 常用的异步操作 63 | 64 | 开发中比较常用的异步操作有: 65 | 66 | - 网络请求,如`ajax` `http.get` 67 | - IO 操作,如`readFile` `readdir` 68 | - 定时函数,如`setTimeout` `setInterval` 69 | 70 | 最后,**请思考,事件绑定是不是也是异步操作**?例如`$btn.on('click', function() {...})`。这个问题很有意思,我会再后面的章节经过分析之后给出答案,各位先自己想一下。 71 | 72 | ## 求打赏 73 | 74 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 75 | 76 |  77 | 78 | 79 | -------------------------------------------------------------------------------- /part1-basic/02-event-loop.md: -------------------------------------------------------------------------------- 1 | # 异步和 event-loop 2 | 3 | 提到异步,就必须提 event-loop 。event-loop 中文翻译叫做“事件轮询”,它是能体现出单线程中异步操作是如何被执行的。 4 | 5 | 首先,**强烈大家观看一个歪果仁的视频《[what the hack is event loop](http://www.tudou.com/programs/view/ACDNKZJm6pQ/)》**,只有不到半个小时的时间,但是将的非常详细。*如果那个链接失效,访问[这里](http://pan.baidu.com/s/1c1E0rjM)(密码: xx9f)* 6 | 7 | 其次,再结合阮一峰老师的《[什么是event loop](http://www.ruanyifeng.com/blog/2014/10/event-loop.html)》一起看一下。将这两个看完就基本了解 event loop 了 8 | 9 | 最后,event-loop 是一块内容比较独立的技术性知识,它是什么样子就是什么样子,讲解起来可变通性非常小。因此,本节说一下我对 event-loop 的理解和体会 10 | 11 | ## 本节内容概述 12 | 13 | - 举例说明 14 | - 核心概念 15 | - 思考两个问题 16 | 17 | ## 举例说明 18 | 19 | 给出一段简单的 js 代码,并用比较通俗、简单的说法介绍一下执行过程。详细过程还需各位去看视频,因为我没必要把半小时的视频都写到这里。 20 | 21 | ```javascript 22 | console.log('line 1') 23 | setTimeout(console.log, 1000, 'line 2') 24 | console.log('line 3') 25 | ``` 26 | 27 | 以上一共三行代码,该程序被执行的时候,会依次挨行执行 28 | 29 | - 第一步,执行第一行,将结果`line 1`打印出来 30 | - 第二步,执行第二行,注意此时会将这个操作暂时存储到其他地方,因为`setTimeout`是一个异步执行操作。 31 | - 第三步,执行第三行,将结果`line 3`打印出出来 32 | - 第四步,等待最后一行程序(一共三行)都全部执行完了,然后立马实时查看刚才暂存的异步操作有没有。如果有可执行的,就立即拿到出来继续执行。 33 | - 第五步,执行完毕之后,再实时查看暂存位置中是否还有未执行的异步回调。 34 | 35 | 以上只拿了`setTimeout`举例子,但是对于网络请求、IO操作、事件绑定道理都是一样的。**如果我讲的简单例子你还是看不懂,一定要去看文章最初提到的《what the hack is event loop》视频,重要重要!!!** 36 | 37 | 38 | ## 思考三个问题 39 | 40 | **第一题,以下代码的输出顺序是什么** 41 | 42 | ```javascript 43 | setTimeout(console.log, 0, 'a') 44 | console.log('b') 45 | console.log('c') 46 | ``` 47 | 48 | 答案是`b c a`,有疑问的需要再去看上面的介绍或者那个视频。 49 | 50 | **第二题,以下代码中,最后输出的结果是否是 500** 51 | 52 | ```javascript 53 | var i, t = Date.now() 54 | for (i = 0; i < 100000000; i++) { 55 | } 56 | function fn() { 57 | console.log(Date.now() - t) // 输出多少??? 58 | } 59 | setTimeout(fn, 500) 60 | ``` 61 | 62 | 答案是大于 500ms ,因为 for 函数需要花费一些时间,等 for 执行完之后再开始计算 500ms 之后执行 fn 63 | 64 | **第三题,事件绑定是不是异步操作?** 65 | 66 | 这个问题大家根据 event-loop 的讲解和视频来思考,我们下一节再给出解答。 67 | 68 | 69 | ## 求打赏 70 | 71 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 72 | 73 |  74 | -------------------------------------------------------------------------------- /part1-basic/03-event-bind.md: -------------------------------------------------------------------------------- 1 | # 事件绑定算不算异步? 2 | 3 | 如果你认真看了上一节的 event-loop 的,你会发现原来事件绑定和异步操作的实现机制是一样的,那么事件绑定是不是就是异步操作呢?(声明一下,这里说的事件绑定是如下代码的形式) 4 | 5 | ```javascript 6 | $btn.on('click', function (e) { 7 | console.log('你点击了按钮') 8 | }) 9 | ``` 10 | 11 | **PS:这个问题貌似没有加过有人讨论或者发起讨论,但是当我了解了 event-loop 之后,我就发现这两者有很大联系,很早就像讨论一下这个话题。不知道哪位同仁跟我有一样的想法?** 12 | 13 | ## 本节内容概述 14 | 15 | - 共同之处 16 | - 不同之处 17 | - 我的观点 18 | 19 | ## 共同之处 20 | 21 | 从技术实现以及书写方法上来讲,他们是一样的。例如事件绑定和 IO 操作的写法基本相同 22 | 23 | ```javascript 24 | $btn.on('click', function (e) { 25 | console.log('你点击了按钮') 26 | }) 27 | fs.readFile('data1.json', function (err, data) { 28 | // 获取数据 29 | }) 30 | ``` 31 | 32 | 最终执行的方式也基本一样,都会被放在 **call-stack** 中通过 event-loop 来调用。 33 | 34 | ## 不同之处 35 | 36 | 在我看来至少有两处不同。 37 | 38 | 第一,event-loop 执行时,调用的源不一样。异步操作是系统自动调用,无论是`setTimeout`时间到了还是`$.ajax`请求返回了,系统会自动调用。而事件绑定就需要用户手动触发 39 | 40 | 第二,从设计上来将,事件绑定有着明显的“订阅-发布”的设计模式,而异步操作却没有。 41 | 42 | ## 我的观点 43 | 44 | 我个人看代码比较偏重设计,一个东西是什么要看它是为什么而设计的。因此,我倾向于**事件绑定不是异步操作**。虽然它也是通过 event-loop 实现调用的,但是它的设计目录却和异步操作完全不一样。 45 | 46 | 其实,事件绑定在 js 中扮演着非常重要的角色,各个地方都会用到事件绑定的形式。例如 web 页面监控鼠标、键盘,以及 nodejs 中的 `EventEmitter` 应用非常广泛(特别是涉及到数据流时)。而事件绑定被应用到非常广泛,却没有发生像异步操作带来的程序逻辑问题,反而大家用的非常开心————这又一个两者不一样的例证。 47 | 48 | 如果你觉得我的观点有问题,也可以大胆提出自己的建议和意见,发表出来!说对说错都无所谓,也不会扣你落户积分,只要能自圆其说就是好的。 49 | 50 | ## 求打赏 51 | 52 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 53 | 54 |  55 | 56 | -------------------------------------------------------------------------------- /part2-jquery/01-jquery-ajax.md: -------------------------------------------------------------------------------- 1 | # jQuery-1.5 之后的 ajax 2 | 3 | `$.ajax`这个函数各位应该都比较熟悉了,要完整的讲解 js 的异步操作,就必须先从`$.ajax`这个方法说起。 4 | 5 | 想要学到全面的知识,大家就不要着急,跟随我的节奏来,并且相信我。我安排的内容,肯定都是有用的,对主题无用的东西,我不会拿来占用大家的时间。 6 | 7 | ## 本节内容概述 8 | 9 | - 传统的`$.ajax` 10 | - 1.5 版本之后的`$.ajax` 11 | - 改进之后的好处 12 | - 和后来的`Promise`的关系 13 | - 如何实现的? 14 | 15 | ## 传统的`$.ajax` 16 | 17 | 先来一段最常见的`$.ajax`的代码,当然是使用万恶的`callback`方式 18 | 19 | ```javascript 20 | var ajax = $.ajax({ 21 | url: 'data.json', 22 | success: function () { 23 | console.log('success') 24 | }, 25 | error: function () { 26 | console.log('error') 27 | } 28 | }) 29 | 30 | console.log(ajax) // 返回一个 XHR 对象 31 | 32 | ``` 33 | 34 | 至于这么做会产生什么样子的诟病,我想大家应该都很明白了。不明白的自己私下去查,但是你也可以继续往下看,你只需要记住这样做很不好就是了,要不然 jquery 也不会再后面进行改进 35 | 36 | ## 1.5 版本之后的`$.ajax` 37 | 38 | 但是从`v1.5`开始,以上代码就可以这样写了:可以链式的执行`done`或者`fail`方法 39 | 40 | ```javascript 41 | var ajax = $.ajax('data.json') 42 | ajax.done(function () { 43 | console.log('success 1') 44 | }) 45 | .fail(function () { 46 | console.log('error') 47 | }) 48 | .done(function () { 49 | console.log('success 2') 50 | }) 51 | 52 | console.log(ajax) // 返回一个 deferred 对象 53 | ``` 54 | 55 | 大家注意看以上两段代码中都有一个`console.log(ajax)`,但是返回值是完全不一样的。 56 | 57 | - `v1.5`之前,返回的是一个`XHR`对象,这个对象不可能有`done`或者`fail`的方法的 58 | - `v1.5`开始,返回一个`deferred`对象,这个对象就带有`done`和`fail`的方法,并且是等着请求返回之后再去调用 59 | 60 | ## 改进之后的好处 61 | 62 | 这是一个标志性的改造,不管这个概念是谁最先提出的,它在 jquery 中首先大量使用并让全球开发者都知道原来 ajax 请求还可以这样写。这为以后的`Promise`标准制定提供了很大意义的参考,你可以以为这就是后面`Promise`的原型。 63 | 64 | 记住一句话————**虽然 JS 是异步执行的语言,但是人的思维是同步的**————因此,开发者总是在寻求如何使用逻辑上看似同步的代码来完成 JS 的异步请求。而 jquery 的这一次更新,让开发者在一定程度上得到了这样的好处。 65 | 66 | 之前无论是什么操作,我都需要一股脑写到`callback`中,现在不用了。现在成功了就写到`done`中,失败了就写到`fail`中,如果成功了有多个步骤的操作,那我就写很多个`done`,然后链式连接起来就 OK 了。 67 | 68 | ## 和后来的`Promise`的关系 69 | 70 | 以上的这段代码,我们还可以这样写。即不用`done`和`fail`函数,而是用`then`函数。`then`函数的第一个参数是成功之后执行的函数(即之前的`done`),第二个参数是失败之后执行的函数(即之前的`fail`)。而且`then`函数还可以链式连接。 71 | 72 | ```javascript 73 | var ajax = $.ajax('data.json') 74 | ajax.then(function () { 75 | console.log('success 1') 76 | }, function () { 77 | console.log('error 1') 78 | }) 79 | .then(function () { 80 | console.log('success 2') 81 | }, function () { 82 | console.log('error 2') 83 | }) 84 | ``` 85 | 86 | 如果你对现在 ES6 的`Promise`有了解,应该能看出其中的相似之处。不了解也没关系,你只需要知道它已经和`Promise`比较接近了。后面马上会去讲`Promise` 87 | 88 | ## 如何实现的? 89 | 90 | 明眼人都知道,jquery 不可能改变异步操作需要`callback`的本质,它只不过是自己定义了一些特殊的 API,并对异步操作的`callback`进行了封装而已。 91 | 92 | 那么 jquery 是如何实现这一步的呢?请听下回分解! 93 | 94 | ## 求打赏 95 | 96 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 97 | 98 |  -------------------------------------------------------------------------------- /part2-jquery/02-jquery-deferred.md: -------------------------------------------------------------------------------- 1 | # jQuery deferred 2 | 3 | 上一节讲到 jquery v1.5 版本开始,`$.ajax`可以使用类似当前`Promise`的`then`函数以及链式操作。那么它到底是如何实现的呢?在此之前所用到的`callback`在这其中又起到了什么作用?本节给出答案 4 | 5 | 本节使用的代码参见[这里](./test.html) 6 | 7 | ## 本节内容概述 8 | 9 | - 写一个传统的异步操作 10 | - 使用`$.Deferred`封装 11 | - 应用`then`方法 12 | - 有什么问题? 13 | 14 | ## 写一个传统的异步操作 15 | 16 | 给出一段非常简单的异步操作代码,使用`setTimeout`函数。 17 | 18 | ```javascript 19 | var wait = function () { 20 | var task = function () { 21 | console.log('执行完成') 22 | } 23 | setTimeout(task, 2000) 24 | } 25 | wait() 26 | ``` 27 | 28 | 以上这些代码执行的结果大家应该都比较明确了,即 2s 之后打印出`执行完成`。**但是我如果再加一个需求 ———— 要在执行完成之后进行某些特别复杂的操作,代码可能会很多,而且分好几个步骤 ———— 那该怎么办?** 大家思考一下! 29 | 30 | 如果你不看下面的内容,而且目前还没有`Promise`的这个思维,那估计你会说:直接在`task`函数中写就是了!不过相信你看完下面的内容之后,会放弃你现在的想法。 31 | 32 | ## 使用`$.Deferred`封装 33 | 34 | 好,接下来我们让刚才简单的几行代码变得更加复杂。**为何要变得更加复杂?是因为让以后更加复杂的地方变得简单**。这里我们使用了 jquery 的`$.Deferred`,至于这个是个什么鬼,大家先不用关心,**只需要知道`$.Deferred()`会返回一个`deferred`对象**,先看代码,`deferred`对象的作用我们会面会说。 35 | 36 | ```javascript 37 | function waitHandle() { 38 | var dtd = $.Deferred() // 创建一个 deferred 对象 39 | 40 | var wait = function (dtd) { // 要求传入一个 deferred 对象 41 | var task = function () { 42 | console.log('执行完成') 43 | dtd.resolve() // 表示异步任务已经完成 44 | } 45 | setTimeout(task, 2000) 46 | return dtd // 要求返回 deferred 对象 47 | } 48 | 49 | // 注意,这里一定要有返回值 50 | return wait(dtd) 51 | } 52 | ``` 53 | 54 | 以上代码中,又使用一个`waitHandle`方法对`wait`方法进行再次的封装。`waitHandle`内部代码,我们分步骤来分析。跟着我的节奏慢慢来,保证你不会乱。 55 | 56 | - 使用`var dtd = $.Deferred()`创建`deferred`对象。通过上一节我们知道,一个`deferred`对象会有`done` `fail`和`then`方法(不明白的去看上一节) 57 | - 重新定义`wait`函数,但是:第一,要传入一个`deferred`对象(`dtd`参数);第二,当`task`函数(即`callback`)执行完成之后,要执行`dtd.resolve()`告诉传入的`deferred`对象,革命已经成功。第三;将这个`deferred`对象返回。 58 | - 返回`wait(dtd)`的执行结果。因为`wait`函数中返回的是一个`deferred`对象(`dtd`参数),因此`wait(dtd)`返回的就是`dtd`————如果你感觉这里很乱,没关系,慢慢捋,一行一行看,相信两三分钟就能捋顺! 59 | 60 | 最后总结一下,`waitHandle`函数最终`return wait(dtd)`即最终返回`dtd`(一个`deferred`)对象。针对一个`deferred`对象,它有`done` `fail`和`then`方法(上一节说过),它还有`resolve()`方法(其实和`resolve`相对的还有一个`reject`方法,后面会提到) 61 | 62 | 63 | ## 应用`then`方法 64 | 65 | 接着上面的代码继续写 66 | 67 | ```javascript 68 | var w = waitHandle() 69 | w.then(function () { 70 | console.log('ok 1') 71 | }, function () { 72 | console.log('err 1') 73 | }).then(function () { 74 | console.log('ok 2') 75 | }, function () { 76 | console.log('err 2') 77 | }) 78 | ``` 79 | 80 | 上面已经说过,`waitHandle`函数最终返回一个`deferred`对象,而`deferred`对象具有`done` `fail` `then`方法,现在我们正在使用的是`then`方法。至于`then`方法的作用,我们上一节已经讲过了,不明白的同学抓紧回去补课。 81 | 82 | 执行这段代码,我们打印出来以下结果。可以将结果对标以下代码时哪一行。 83 | 84 | ``` 85 | 执行完成 86 | ok 1 87 | ok 2 88 | ``` 89 | 90 | 此时,你再回头想想我刚才说提出的需求(*要在执行完成之后进行某些特别复杂的操作,代码可能会很多,而且分好几个步骤*),是不是有更好的解决方案了? 91 | 92 | 有同学肯定发现了,代码中`console.log('err 1')`和`console.log('err 2')`什么时候会执行呢 ———— 你自己把`waitHandle`函数中的`dtd.resolve()`改成`dtd.reject()`试一下就知道了。 93 | 94 | - `dtd.resolve()` 表示革命已经成功,会触发`then`中第一个参数(函数)的执行, 95 | - `dtd.reject()` 表示革命失败了,会触发`then`中第二个参数(函数)执行 96 | 97 | ## 有什么问题? 98 | 99 | 总结一下一个`deferred`对象具有的函数属性,并分为两组: 100 | 101 | - `dtd.resolve` `dtd.reject` 102 | - `dtd.then` `dtd.done` `dtd.fail` 103 | 104 | 我为何要分成两组 ———— 这两组函数,从设计到执行之后的效果是完全不一样的。第一组是主动触发用来改变状态(成功或者失败),第二组是状态变化之后才会触发的监听函数。 105 | 106 | 既然是完全不同的两组函数,就应该彻底的分开,否则很容易出现问题。例如,你在刚才执行代码的最后加上这么一行试试。 107 | 108 | ```javascript 109 | w.reject() 110 | ``` 111 | 112 | 那么如何解决这一个问题?请听下回分解! 113 | 114 | ## 求打赏 115 | 116 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 117 | 118 |  119 | 120 | -------------------------------------------------------------------------------- /part2-jquery/03-jquery-promise.md: -------------------------------------------------------------------------------- 1 | # jQuery promise 2 | 3 | 上一节通过一些代码演示,知道了 jquery 的`deferred`对象是解决了异步中`callback`函数的问题,但是 4 | 5 | 本节使用的代码参见[这里](./test.html) 6 | 7 | ## 本节内容概述 8 | 9 | - 返回`promise` 10 | - 返回`promise`的好处 11 | - promise 的概念 12 | 13 | ## 返回`promise` 14 | 15 | 我们对上一节的的代码做一点小小的改动,只改动了一行,下面注释。 16 | 17 | ```javascript 18 | function waitHandle() { 19 | var dtd = $.Deferred() 20 | var wait = function (dtd) { 21 | var task = function () { 22 | console.log('执行完成') 23 | dtd.resolve() 24 | } 25 | setTimeout(task, 2000) 26 | return dtd.promise() // 注意,这里返回的是 primise 而不是直接返回 deferred 对象 27 | } 28 | return wait(dtd) 29 | } 30 | 31 | var w = waitHandle() // 经过上面的改动,w 接收的就是一个 promise 对象 32 | $.when(w) 33 | .then(function () { 34 | console.log('ok 1') 35 | }) 36 | .then(function () { 37 | console.log('ok 2') 38 | }) 39 | ``` 40 | 41 | 改动的一行在这里`return dtd.promise()`,之前是`return dtd`。`dtd`是一个`deferred`对象,而`dtd.promise`就是一个`promise`对象。 42 | 43 | `promise`对象和`deferred`对象最重要的区别,记住了————**`promise`对象相比于`deferred`对象,缺少了`.resolve`和`.reject`这俩函数属性**。这么一来,可就完全不一样了。 44 | 45 | 上一节我们提到一个问题,就是在程序的最后一行加一句`w.reject()`会导致乱套,你现在再在最后一行加`w.reject()`试试 ———— 保证乱套不了 ———— 而是你的程序不能执行,直接报错。因为,`w`是`promise`对象,不具备`.reject`属性。 46 | 47 | ## 返回`promise`的好处 48 | 49 | 上一节提到`deferred`对象有两组属性函数,而且提到应该把这两组彻底分开。现在通过上面一行代码的改动,就分开了。 50 | 51 | - `waitHandle`函数内部,使用`dtd.resolve()`来该表状态,做主动的修改操作 52 | - `waitHandle`最终返回`promise`对象,只能去被动监听变化(`then`函数),而不能去主动修改操作 53 | 54 | 一个“主动”一个“被动”,完全分开了。 55 | 56 | ## promise 的概念 57 | 58 | jquery v1.5 版本发布时间距离现在(2017年初春)已经老早之前了,那会儿大家网页标配都是 jquery 。无论里面的`deferred`和`promise`这个概念和想法最早是哪位提出来的,但是最早展示给全世界开发者的是 jquery ,这算是`Promise`这一概念最先的提出者。 59 | 60 | 其实本次课程主要是给大家分析 ES6 的`Promise` `Generator`和`async-await`,但是为何要从 jquery 开始(大家现在用 jquery 越来越少)?就是要给大家展示一下这段历史的一些起点和发展的知识。有了这些基础,你再去接受最新的概念会非常容易,因为所有的东西都是从最初顺其自然发展进化而来的,我们要去用一个发展进化的眼光学习知识,而不是死记硬背。 61 | 62 | ## 求打赏 63 | 64 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 65 | 66 |  67 | -------------------------------------------------------------------------------- /part2-jquery/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |jquery test
9 | 10 | 11 | 90 | 91 | -------------------------------------------------------------------------------- /part3-promise/01-promise-in-es6.md: -------------------------------------------------------------------------------- 1 | ## Promise 加入 ES6 标准 2 | 3 | 从 jquery v1.5 发布经过若干时间之后,Promise 终于出现在了 ES6 的标准中,而当下 ES6 也正在被大规模使用。 4 | 5 | 本节展示的代码参考[这里](./test.js) 6 | 7 | ## 本节内容概述 8 | 9 | - 写一段传统的异步操作 10 | - 用`Promise`进行封装 11 | 12 | ## 写一段传统的异步操作 13 | 14 | 还是拿之前讲 jquery `deferred`对象时的那段`setTimeout`程序 15 | 16 | ```javascript 17 | var wait = function () { 18 | var task = function () { 19 | console.log('执行完成') 20 | } 21 | setTimeout(task, 2000) 22 | } 23 | wait() 24 | ``` 25 | 26 | 之前我们使用 jquery 封装的,接下来将使用 ES6 的`Promise`进行封装,大家注意看有何不同。 27 | 28 | ## 用`Promise`进行封装 29 | 30 | ```javascript 31 | const wait = function () { 32 | // 定义一个 promise 对象 33 | const promise = new Promise((resolve, reject) => { 34 | // 将之前的异步操作,包括到这个 new Promise 函数之内 35 | const task = function () { 36 | console.log('执行完成') 37 | resolve() // callback 中去执行 resolve 或者 reject 38 | } 39 | setTimeout(task, 2000) 40 | }) 41 | // 返回 promise 对象 42 | return promise 43 | } 44 | ``` 45 | 46 | 注意看看程序中的注释,那都是重点部分。从整体看来,感觉这次比用 jquery 那次简单一些,逻辑上也更加清晰一些。 47 | 48 | - 将之前的异步操作那几行程序,用`new Promise((resolve,reject) => {.....})`包装起来,最后`return`即可 49 | - 异步操作的内部,在`callback`中执行`resolve()`(表明成功了,失败的话执行`reject`) 50 | 51 | 接着上面的程序继续往下写。`wait()`返回的肯定是一个`promise`对象,而`promise`对象有`then`属性。 52 | 53 | ```javascript 54 | const w = wait() 55 | w.then(() => { 56 | console.log('ok 1') 57 | }, () => { 58 | console.log('err 1') 59 | }).then(() => { 60 | console.log('ok 2') 61 | }, () => { 62 | console.log('err 2') 63 | }) 64 | ``` 65 | 66 | `then`还是和之前一样,接收两个参数(函数),第一个在成功时(触发`resolve`)执行,第二个在失败时(触发`reject`)时执行。而且,`then`还可以进行链式操作。 67 | 68 | 以上就是 ES6 的`Promise`的基本使用演示。看完你可能会觉得,这跟之前讲述 jquery 的不差不多吗 ———— 对了,这就是我要在之前先讲 jquery 的原因,让你感觉一篇一篇看起来如丝般顺滑! 69 | 70 | 接下来,将详细说一下 ES6 `Promise` 的一些比较常见的用法,敬请期待吧! 71 | 72 | ## 求打赏 73 | 74 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 75 | 76 |  77 | 78 | -------------------------------------------------------------------------------- /part3-promise/02-promise-use.md: -------------------------------------------------------------------------------- 1 | # Promise 在 ES6 中的具体应用 2 | 3 | 上一节对 ES6 的 Promise 有了一个最简单的介绍,这一节详细说一下 Promise 那些最常见的功能 4 | 5 | 本节展示的代码参考[这里](./test.js) 6 | 7 | ## 本节课程概述 8 | 9 | - 准备工作 10 | - 参数传递 11 | - 异常捕获 12 | - 串联多个异步操作 13 | - `Promise.all`和`Promise.race`的应用 14 | - `Promise.resolve`的应用 15 | - 其他 16 | 17 | ## 准备工作 18 | 19 | 因为以下所有的代码都会用到`Promise`,因此干脆在所有介绍之前,先封装一个`Promise`,**封装一次,为下面多次应用**。 20 | 21 | ```javascript 22 | const fs = require('fs') 23 | const path = require('path') // 后面获取文件路径时候会用到 24 | const readFilePromise = function (fileName) { 25 | return new Promise((resolve, reject) => { 26 | fs.readFile(fileName, (err, data) => { 27 | if (err) { 28 | reject(err) // 注意,这里执行 reject 是传递了参数,后面会有地方接收到这个参数 29 | } else { 30 | resolve(data.toString()) // 注意,这里执行 resolve 时传递了参数,后面会有地方接收到这个参数 31 | } 32 | }) 33 | }) 34 | } 35 | ``` 36 | 37 | 以上代码一个一段 nodejs 代码,将读取文件的函数`fs.readFile`封装为一个`Promise`。经过上一节的学习,我想大家肯定都能看明白代码的含义,要是看不明白,你就需要回炉重造了! 38 | 39 | ## 参数传递 40 | 41 | 我们要使用上面封装的`readFilePromise`读取一个 json 文件`../data/data2.json`,这个文件内容非常简单:`{"a":100, "b":200}` 42 | 43 | 先将文件内容打印出来,代码如下。大家需要注意,`readFilePromise`函数中,执行`resolve(data.toString())`传递的参数内容,会被下面代码中的`data`参数所接收到。 44 | 45 | ```javascript 46 | const fullFileName = path.resolve(__dirname, '../data/data2.json') 47 | const result = readFilePromise(fullFileName) 48 | result.then(data => { 49 | console.log(data) 50 | }) 51 | ``` 52 | 53 | 再加一个需求,在打印出文件内容之后,我还想看看`a`属性的值,代码如下。之前我们已经知道`then`可以执行链式操作,如果`then`有多步骤的操作,那么前面步骤`return`的值会被当做参数传递给后面步骤的函数,如下面代码中的`a`就接收到了`return JSON.parse(data).a`的值 54 | 55 | ```javascript 56 | const fullFileName = path.resolve(__dirname, '../data/data2.json') 57 | const result = readFilePromise(fullFileName) 58 | result.then(data => { 59 | // 第一步操作 60 | console.log(data) 61 | return JSON.parse(data).a // 这里将 a 属性的值 return 62 | }).then(a => { 63 | // 第二步操作 64 | console.log(a) // 这里可以获取上一步 return 过来的值 65 | }) 66 | ``` 67 | 68 | 总结一下,这一段内容提到的“参数传递”其实有两个方面: 69 | 70 | - 执行`resolve`传递的值,会被第一个`then`处理时接收到 71 | - 如果`then`有链式操作,前面步骤返回的值,会被后面的步骤获取到 72 | 73 | ## 异常捕获 74 | 75 | 我们知道`then`会接收两个参数(函数),第一个参数会在执行`resolve`之后触发(还能传递参数),第二个参数会在执行`reject`之后触发(其实也可以传递参数,和`resolve`传递参数一样),但是上面的例子中,**我们没有用到`then`的第二个参数。这是为何呢 ———— 因为不建议这么用**。 76 | 77 | 对于`Promise`中的异常处理,我们建议用`catch`方法,而不是`then`的第二个参数。请看下面的代码,以及注释。 78 | 79 | ```javascript 80 | const fullFileName = path.resolve(__dirname, '../data/data2.json') 81 | const result = readFilePromise(fullFileName) 82 | result.then(data => { 83 | console.log(data) 84 | return JSON.parse(data).a 85 | }).then(a => { 86 | console.log(a) 87 | }).catch(err => { 88 | console.log(err.stack) // 这里的 catch 就能捕获 readFilePromise 中触发的 reject ,而且能接收 reject 传递的参数 89 | }) 90 | ``` 91 | 92 | 在若干个`then`串联之后,我们一般会在最后跟一个`.catch`来捕获异常,而且执行`reject`时传递的参数也会在`catch`中获取到。这样做的好处是: 93 | 94 | - 让程序看起来更加简洁,是一个串联的关系,没有分支(如果用`then`的两个参数,就会出现分支,影响阅读) 95 | - 看起来更像是`try - catch`的样子,更易理解 96 | 97 | ## 串联多个异步操作 98 | 99 | 如果现在有一个需求:先读取`data2.json`的内容,当成功之后,再去读取`data1.json`。这样的需求,如果用传统的`callback`去实现,会变得很麻烦。而且,现在只是两个文件,如果是十几个文件这样做,写出来的代码就没法看了(臭名昭著的`callback-hell`)。但是用刚刚学到的`Promise`就可以轻松胜任这项工作 100 | 101 | ```javascript 102 | const fullFileName2 = path.resolve(__dirname, '../data/data2.json') 103 | const result2 = readFilePromise(fullFileName2) 104 | const fullFileName1 = path.resolve(__dirname, '../data/data1.json') 105 | const result1 = readFilePromise(fullFileName1) 106 | 107 | result2.then(data => { 108 | console.log('data2.json', data) 109 | return result1 // 此处只需返回读取 data1.json 的 Promise 即可 110 | }).then(data => { 111 | console.log('data1.json', data) // data 即可接收到 data1.json 的内容 112 | }) 113 | ``` 114 | 115 | 上文“参数传递”提到过,如果`then`有链式操作,前面步骤返回的值,会被后面的步骤获取到。**但是,如果前面步骤返回值是一个`Promise`的话,情况就不一样了 ———— 如果前面返回的是`Promise`对象,后面的`then`将会被当做这个返回的`Promise`的第一个`then`来对待** ———— 如果你这句话看不懂,你需要将“参数传递”的示例代码和这里的示例代码联合起来对比着看,然后体会这句话的意思。 116 | 117 | ## `Promise.all`和`Promise.race`的应用 118 | 119 | 我还得继续提出更加奇葩的需求,以演示`Promise`的各个常用功能。如下需求: 120 | 121 | 读取两个文件`data1.json`和`data2.json`,现在我需要一起读取这两个文件,等待它们全部都被读取完,再做下一步的操作。此时需要用到`Promise.all` 122 | 123 | ```javascript 124 | // Promise.all 接收一个包含多个 promise 对象的数组 125 | Promise.all([result1, result2]).then(datas => { 126 | // 接收到的 datas 是一个数组,依次包含了多个 promise 返回的内容 127 | console.log(datas[0]) 128 | console.log(datas[1]) 129 | }) 130 | ``` 131 | 132 | 133 | 读取两个文件`data1.json`和`data2.json`,现在我需要一起读取这两个文件,但是只要有一个已经读取了,就可以进行下一步的操作。此时需要用到`Promise.race` 134 | 135 | ```javascript 136 | // Promise.race 接收一个包含多个 promise 对象的数组 137 | Promise.race([result1, result2]).then(data => { 138 | // data 即最先执行完成的 promise 的返回值 139 | console.log(data) 140 | }) 141 | ``` 142 | 143 | ## `Promise.resolve`的应用 144 | 145 | 从 jquery 引出,到此即将介绍完 ES6 的`Promise`,现在我们再回归到 jquery 。 146 | 147 | 大家都是到 jquery v1.5 之后`$.ajax()`返回的是一个`deferred`对象,而这个`deferred`对象和我们现在正在学习的`Promise`对象已经很接近了,但是还不一样。那么 ———— `deferred`对象能否转换成 ES6 的`Promise`对象来使用?? 148 | 149 | 答案是能!需要使用`Promise.resolve`来实现这一功能,请看以下代码: 150 | 151 | ```javascript 152 | // 在浏览器环境下运行,而非 node 环境 153 | cosnt jsPromise = Promise.resolve($.ajax('/whatever.json')) 154 | jsPromise.then(data => { 155 | // ... 156 | }) 157 | ``` 158 | 159 | **注意:这里的`Promise.resolve`和文章最初`readFilePromise`函数内部的`resolve`函数可千万不要混了,完全是两码事儿**。JS 基础好的同学一看就明白,而这里看不明白的同学,要特别注意。 160 | 161 | 实际上,并不是`Promise.resolve`对 jquery 的`deferred`对象做了特殊处理,**而是`Promise.resolve`能够将`thenable`对象转换为`Promise`对象**。什么是`thenable`对象?———— 看个例子 162 | 163 | ```javascript 164 | // 定义一个 thenable 对象 165 | const thenable = { 166 | // 所谓 thenable 对象,就是具有 then 属性,而且属性值是如下格式函数的对象 167 | then: (resolve, reject) => { 168 | resolve(200) 169 | } 170 | } 171 | 172 | // thenable 对象可以转换为 Promise 对象 173 | const promise = Promise.resolve(thenable) 174 | promise.then(data => { 175 | // ... 176 | }) 177 | ``` 178 | 179 | 上面的代码就将一个`thenalbe`对象转换为一个`Promise`对象,只不过这里没有异步操作,所有的都会同步执行,但是不会报错的。 180 | 181 | 其实,在我们的日常开发中,这种将`thenable`转换为`Promise`的需求并不多。**真正需要的是,将一些异步操作函数(如`fs.readFile`)转换为`Promise`**(就像文章一开始`readFilePromise`做的那样)。这块,我们后面会在介绍`Q.js`库时,告诉大家一个简单的方法。 182 | 183 | ## 其他 184 | 185 | 以上都是一些日常开发中非常常用的功能,其他详细的介绍,请参考阮一峰老师的 [ES6 教程 Promise 篇](http://es6.ruanyifeng.com/#docs/promise) 186 | 187 | 最后,本节我们只是介绍了`Promise`的一些应用,通俗易懂拿来就用的东西,但是没有提升到理论和标准的高度。有人可能会不屑 ———— 我会用就行了,要那么空谈的理论干嘛?———— **你只会使用却上升不到理论高度,永远都是个搬砖的,搬一块砖挣一毛钱,不搬就不挣钱!** 在我看来,所有的知识应该都需要上升到理论高度,将实际应用和标准对接,知道真正的出处,才能走的长远。 188 | 189 | 下一节我们介绍 Promise/A+ 规范 190 | 191 | ## 求打赏 192 | 193 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 194 | 195 |  -------------------------------------------------------------------------------- /part3-promise/03-promise-standard.md: -------------------------------------------------------------------------------- 1 | # 对标一下 Promise/A+ 规范 2 | 3 | Promise/A 是由 CommonJS 组织制定的异步模式编程规范,后来又经过一些升级,就是当前的 Promise/A+ 规范。上一节讲述的`Promise`的一些功能实现,就是根据这个规范来的。 4 | 5 | ## 本节内容概述 6 | 7 | - 介绍规范的核心内容 8 | - 状态变化 9 | - `then`方法 10 | - 接下来... 11 | 12 | ## 介绍规范的核心内容 13 | 14 | 网上有很多介绍 Promise/A+ 规范的文章,大家可以搜索来看,但是它的核心要点有以下几个,我也是从看了之后自己总结的 15 | 16 | **关于状态** 17 | 18 | - promise 可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected) 19 | - promise 的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换 20 | 21 | **关于`then`方法** 22 | 23 | - promise 必须实现`then`方法,而且`then`必须返回一个 promise ,同一个 promise 的`then`可以调用多次(链式),并且回调的执行顺序跟它们被定义时的顺序一致 24 | - `then`方法接受两个参数,第一个参数是成功时的回调,在 promise 由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在 promise 由“等待”态转换到“拒绝”态时调用 25 | 26 | 下面挨个介绍这些规范在上一节代码中的实现,所谓理论与实践相结合。**在阅读以下内容时,你要时刻准备参考上一节的代码**。 27 | 28 | ## 状态变化 29 | 30 | > promise 可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected) 31 | 32 | 拿到上一节的`readFilePromise`函数,然后执行`const result = readFilePromise(someFileName)`会得到一个`Promise`对象。 33 | 34 | - 刚刚创建时,就是 等待(pending)状态 35 | - 如果读取文件成功了,`readFilePromise`函数内部的`callback`中会自定调用`resolve()`,这样就变为 已完成(fulfilled)状态 36 | - 如果很不幸读取文件失败了(例如文件名写错了,找不到文件),`readFilePromise`函数内部的`callback`中会自定调用`reject()`,这样就变为 已拒绝(rejeced)状态 37 | 38 | > promise 的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换 39 | 40 | 这个规则还是可以参考读取文件的这个例子。从一开始准备读取,到最后无论是读取成功或是读取失败,都是不可逆的。另外,读取成功和读取失败之间,也是不能互换的。这个逻辑没有任何问题,很好理解。 41 | 42 | ## `then`方法 43 | 44 | > promise 必须实现`then`方法,而且`then`必须返回一个 promise ,同一个 promise 的`then`可以调用多次(链式),并且回调的执行顺序跟它们被定义时的顺序一致 45 | 46 | - `promise`对象必须实现`then`方法这个无需解释,没有`then`那就不叫`promise` 47 | - “而且`then`必须返回一个`promise`,同一个 promise 的`then`可以调用多次(链式)” ———— 这两句话说明了一个意思 ———— `then`肯定要再返回一个`promise`,要不然`then`后面怎么能再链式的跟一个`then`呢? 48 | 49 | > `then`方法接受两个参数,第一个参数是成功时的回调,在 promise 由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在 promise 由“等待”态转换到“拒绝”态时调用 50 | 51 | 这句话比较好理解了,我们从一开始就在 demo 中演示。 52 | 53 | ## 接下来... 54 | 55 | `Promise`的应用、规范都介绍完了,看起来挺牛的,也解决了异步操作中使用`callback`带来的很多问题。但是`Promise`本质上到底是一种什么样的存在,它是真的把`callback`弃而不用了吗,还是两者有什么合作关系?它到底是真的神通广大,还是使用了障眼法? 56 | 57 | 这些问题,大家学完`Promise`之后应该去思考,不能光学会怎么用就停止了。下一节我们一起来探讨~ 58 | 59 | ## 求打赏 60 | 61 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 62 | 63 |  -------------------------------------------------------------------------------- /part3-promise/04-promise-callback.md: -------------------------------------------------------------------------------- 1 | # Promise 真的取代 callback 了吗 2 | 3 | Promise 虽然改变了 JS 工程师对于异步操作的写法,但是却改变不了 JS 单线程、异步的执行模式。 4 | 5 | ## 本节概述 6 | 7 | - JS 异步的本质 8 | - Promise 只是表面的写法上的改变 9 | - Promise 中不能缺少 callback 10 | - 接下来... 11 | 12 | ## JS 异步的本质 13 | 14 | 从最初的 ES3、4 到 ES5 再到现在的 ES6 和即将到来的 ES7,语法标准上更新很多,但是 JS 这种单线程、异步的本质是没有改变的。nodejs 中读取文件的代码一直都可以这样写 15 | 16 | ```javascript 17 | fs.readFile('some.json', (err, data) => { 18 | }) 19 | ``` 20 | 21 | 既然异步这个本质不能改变,伴随异步在一起的永远都会有`callback`,因为没有`callback`就无法实现异步。因此`callback`永远存在。 22 | 23 | ## Promise 只是表面的写法上的改变 24 | 25 | JS 工程师不会讨厌 JS 异步的本质,但是很讨厌 JS 异步操作中`callback`的书写方式,特别是遇到万恶的`callback-hell`(嵌套`callback`)时。 26 | 27 | 计算机的抽象思维和人的具象思维是完全不一样的,人永远喜欢看起来更加符合逻辑、更加易于阅读的程序,因此现在特别强调代码可读性。而`Promise`就是一种代码可读性的变化。大家感受一下这两种不同(**这其中还包括异常处理,加上异常处理会更加复杂**) 28 | 29 | 第一种,传统的`callback`方式 30 | 31 | ```javascript 32 | fs.readFile('some1.json', (err, data) => { 33 | fs.readFile('some2.json', (err, data) => { 34 | fs.readFile('some3.json', (err, data) => { 35 | fs.readFile('some4.json', (err, data) => { 36 | 37 | }) 38 | }) 39 | }) 40 | }) 41 | ``` 42 | 43 | 第二种,`Promise`方式 44 | 45 | ```javascript 46 | readFilePromise('some1.json').then(data => { 47 | return readFilePromise('some2.json') 48 | }).then(data => { 49 | return readFilePromise('some3.json') 50 | }).then(data => { 51 | return readFilePromise('some4.json') 52 | }) 53 | ``` 54 | 55 | 这两种方式对于代码可读性的对比,非常明显。**但是最后再次强调,`Promise`只是对于异步操作代码可读性的一种变化,它并没有改变 JS 异步执行的本质,也没有改变 JS 中存在`callback`的现象**。 56 | 57 | ## Promise 中不能缺少 callback 58 | 59 | 上文已经基本给出了上一节提问的答案,但是这里还需要再加一个补充:`Promise`不仅仅是没有取代`callback`或者弃而不用,反而`Promise`中要使用到`callback`。因为,JS 异步执行的本质,必须有`callback`存在,否则无法实现。 60 | 61 | 再次粘贴处之前章节的封装好的一个`Promise`函数(进行了一点点简化) 62 | 63 | ```javascript 64 | const readFilePromise = function (fileName) { 65 | return new Promise((resolve, reject) => { 66 | fs.readFile(fileName, (err, data) => { 67 | resolve(data.toString()) 68 | }) 69 | }) 70 | } 71 | ``` 72 | 73 | 上面的代码中,`promise`对象的状态要从`pending`变化为`fulfilled`,就需要去执行`resolve()`函数。那么是从哪里执行的 ———— **还得从`callback`中执行`resolve`函数 ———— 这就是`Promise`也需要`callback`的最直接体现**。 74 | 75 | ## 接下来... 76 | 77 | 一块技术“火”的程度和第三方开源软件的数量、质量以及使用情况有很大的正比关系。例如为了简化 DOM 操作,jquery 风靡全世界。Promise 用的比较多,第三方库当然就必不可少,它们极大程度的简化了 Promise 的代码。 78 | 79 | 接下来我们一起看看`Q.js`这个库的使用,学会了它,将极大程度提高你写 Promise 的效率。 80 | 81 | ## 求打赏 82 | 83 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 84 | 85 |  -------------------------------------------------------------------------------- /part3-promise/05-promise-q.md: -------------------------------------------------------------------------------- 1 | # 使用 Q.js 库 2 | 3 | 如果实际项目中使用`Promise`,还是强烈建议使用比较靠谱的第三方插件,会极大增加你的开发效率。除了将要介绍的`Q.js`,还有`bluebird`也推荐使用,去 github 自行搜索吧。 4 | 5 | 另外,使用第三方库不仅仅是提高效率,**它还让你在浏览器端(不支持`Promise`的环境中)使用`promise`**。 6 | 7 | 本节展示的代码参考[这里](./test.js) 8 | 9 | ## 本节内容概述 10 | 11 | - 下载和安装 12 | - 使用`Q.nfcall`和`Q.nfapply` 13 | - 使用`Q.defer` 14 | - 使用`Q.denodeify` 15 | - 使用`Q.all`和`Q.any` 16 | - 使用`Q.delay` 17 | - 其他 18 | 19 | ## 下载和安装 20 | 21 | 可以直接去它的 [github 地址](https://github.com/kriskowal/q) (近 1.3W 的 star 数量说明其用户群很大)查看文档。 22 | 23 | 如果项目使用 CommonJS 规范直接 `npm i q --save`,如果是网页外链可寻找可用的 cdn 地址,或者干脆下载到本地。 24 | 25 | 以下我将要演示的代码,都是使用 CommonJS 规范的,因此我要演示代码之前加上引用,以后的代码演示就不重复加了。 26 | 27 | ```javascript 28 | const Q = require('q') 29 | ``` 30 | 31 | ## 使用`Q.nfcall`和`Q.nfapply` 32 | 33 | 要使用这两个函数,你得首先了解 JS 的`call`和`apply`,如果不了解,先去看看。熟悉了这两个函数之后,再回来看。 34 | 35 | `Q.nfcall`就是使用`call`的语法来返回一个`promise`对象,例如 36 | 37 | ```javascript 38 | const fullFileName = path.resolve(__dirname, '../data/data1.json') 39 | const result = Q.nfcall(fs.readFile, fullFileName, 'utf-8') // 使用 Q.nfcall 返回一个 promise 40 | result.then(data => { 41 | console.log(data) 42 | }).catch(err => { 43 | console.log(err.stack) 44 | }) 45 | ``` 46 | 47 | `Q.nfapply`就是使用`apply`的语法返回一个`promise`对象,例如 48 | 49 | ```javascript 50 | const fullFileName = path.resolve(__dirname, '../data/data1.json') 51 | const result = Q.nfapply(fs.readFile, [fullFileName, 'utf-8']) // 使用 Q.nfapply 返回一个 promise 52 | result.then(data => { 53 | console.log(data) 54 | }).catch(err => { 55 | console.log(err.stack) 56 | }) 57 | ``` 58 | 59 | 怎么样,体验了一把,是不是比直接自己写`Promise`简单多了? 60 | 61 | ## 使用`Q.defer` 62 | 63 | `Q.defer`算是一个比较偏底层一点的 API ,用于自己定义一个`promise`生成器,如果你需要在浏览器端编写,而且浏览器不支持`Promise`,这个就有用处了。 64 | 65 | ```javascript 66 | function readFile(fileName) { 67 | const defer = Q.defer() 68 | fs.readFile(fileName, (err, data) => { 69 | if (err) { 70 | defer.reject(err) 71 | } else { 72 | defer.resolve(data.toString()) 73 | } 74 | }) 75 | return defer.promise 76 | } 77 | readFile('data1.json') 78 | .then(data => { 79 | console.log(data) 80 | }) 81 | .catch(err => { 82 | console.log(err.stack) 83 | }) 84 | ``` 85 | 86 | 87 | ## 使用`Q.denodeify` 88 | 89 | 我们在很早之前的一节中自己封装了一个`fs.readFile`的`promise`生成器,这里再次回顾一下 90 | 91 | ```javascript 92 | const readFilePromise = function (fileName) { 93 | return new Promise((resolve, reject) => { 94 | fs.readFile(fileName, (err, data) => { 95 | if (err) { 96 | reject(err) 97 | } else { 98 | resolve(data.toString()) 99 | } 100 | }) 101 | }) 102 | } 103 | ``` 104 | 105 | 虽然看着不麻烦,但是还是需要很多行代码来实现,如果使用`Q.denodeify`,一行代码就搞定了! 106 | 107 | ```javascript 108 | const readFilePromise = Q.denodeify(fs.readFile) 109 | ``` 110 | 111 | `Q.denodeify`就是一键将`fs.readFile`这种有回调函数作为参数的异步操作封装成一个`promise`生成器,非常方便! 112 | 113 | ## 使用`Q.all`和`Q.any` 114 | 115 | 这两个其实就是对应了之前讲过的`Promise.all`和`Promise.race`,而且应用起来一模一样,不多赘述。 116 | 117 | ```javascript 118 | const r1 = Q.nfcall(fs.readFile, 'data1.json', 'utf-8') 119 | const r2 = Q.nfcall(fs.readFile, 'data2.json', 'utf-8') 120 | Q.all([r1, r2]).then(arr => { 121 | console.log(arr) 122 | }).catch(err => { 123 | console.log(err) 124 | }) 125 | ``` 126 | 127 | ## 使用`Q.delay` 128 | 129 | `Q.delay`,顾名思义,就是延迟的意思。例如,读取一个文件成功之后,再过五秒钟之后,再去做xxxx。这个如果是自己写的话,也挺费劲的,但是`Q.delay`就直接给我们分装好了。 130 | 131 | ```javascript 132 | const result = Q.nfcall(fs.readFile, 'data1.json', 'utf-8') 133 | result.delay(5000).then(data => { 134 | // 得到结果 135 | console.log(data.toString()) 136 | }).catch(err => { 137 | // 捕获错误 138 | console.log(err.stack) 139 | }) 140 | ``` 141 | 142 | ## 其他 143 | 144 | 以上就是`Q.js`一些最常用的操作,其他的一些非常用技巧,大家可以去搜索或者去官网查看文档。 145 | 146 | 至此,ES6 `Promise`的所有内容就已经讲完了。但是异步操作的优化到这里没有结束,更加精彩的内容还在后面 ———— `Generator` 147 | 148 | ## 求打赏 149 | 150 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 151 | 152 |  -------------------------------------------------------------------------------- /part3-promise/test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Q = require('q') 4 | 5 | // 封装一个 Promise 6 | const readFilePromise = function (fileName) { 7 | return new Promise((resolve, reject) => { 8 | fs.readFile(fileName, (err, data) => { 9 | if (err) { 10 | reject(err) 11 | } else { 12 | resolve(data.toString()) 13 | } 14 | }) 15 | }) 16 | } 17 | 18 | // ------- 最简单的 Promise 演示 ------- 19 | function fn1() { 20 | const wait = function () { 21 | // 定义一个 promise 对象 22 | const promise = new Promise((resolve, reject) => { 23 | // 将之前的异步操作,包括到这个 new Promise 函数之内 24 | const task = function () { 25 | console.log('执行完成') 26 | resolve() // callback 中去执行 resolve 或者 reject 27 | } 28 | setTimeout(task, 2000) 29 | }) 30 | // 返回 promise 对象 31 | return promise 32 | } 33 | const w = wait() 34 | w.then(() => { 35 | console.log('ok 1') 36 | }, () => { 37 | console.log('err 1') 38 | }).then(() => { 39 | console.log('ok 2') 40 | }, () => { 41 | console.log('err 2') 42 | }) 43 | } 44 | // fn1() 45 | 46 | 47 | // --------- 参数传递 -------- 48 | function fn2() { 49 | const fullFileName = path.resolve(__dirname, '../data/data2.json') 50 | const result = readFilePromise(fullFileName) 51 | result.then(data => { 52 | console.log(data) 53 | return JSON.parse(data).a 54 | }).then(a => { 55 | console.log(a) 56 | }).catch(err => { 57 | console.log(err.stack) 58 | }) 59 | } 60 | // fn2() 61 | 62 | 63 | // ------- 串联读取多个文件内容 ------ 64 | function fn3() { 65 | 66 | const fullFileName2 = path.resolve(__dirname, '../data/data2.json') 67 | const result2 = readFilePromise(fullFileName2) 68 | const fullFileName1 = path.resolve(__dirname, '../data/data1.json') 69 | const result1 = readFilePromise(fullFileName1) 70 | 71 | result2.then(data => { 72 | console.log('data2.json', data) 73 | return result1 74 | }).then(data => { 75 | console.log('data1.json', data) 76 | }) 77 | 78 | } 79 | // fn3() 80 | 81 | 82 | // ------- Promise.all 和 Promise.race ---------- 83 | function fn4() { 84 | 85 | const fullFileName2 = path.resolve(__dirname, '../data/data2.json') 86 | const result2 = readFilePromise(fullFileName2) 87 | const fullFileName1 = path.resolve(__dirname, '../data/data1.json') 88 | const result1 = readFilePromise(fullFileName1) 89 | 90 | // Promise.all 接收一个包含多个 promise 对象的数组 91 | Promise.all([result1, result2]).then(datas => { 92 | // 接收到的 datas 是一个数组,依次包含了多个 promise 返回的内容 93 | console.log(datas[0]) 94 | console.log(datas[1]) 95 | }) 96 | 97 | // Promise.race 接收一个包含多个 promise 对象的数组 98 | Promise.race([result1, result2]).then(data => { 99 | // data 即最先执行完成的 promise 的返回值 100 | console.log(data) 101 | }) 102 | 103 | } 104 | // fn4() 105 | 106 | 107 | // ----- Promise.resolve() 的使用 ----- 108 | function fn5() { 109 | 110 | // Promise.resolve('foo') 的写法等价于以下代码, 111 | // new Promise( resolve => resolve('foo') ) 112 | 113 | // 如果传入的参数是一个 promise 实例,则原封不动的返回 114 | const p1 = new Promise((resvole, reject) => { 115 | resolve(100) 116 | }) 117 | console.log( 1, p1 === Promise.resolve(p1) ) // true 118 | 119 | 120 | // 参数是一个 thenalbe 对象,Promise.resovle() 会将对象转换为 Promise 对象,并立即执行其 then 方法 121 | const thenable = { 122 | then: (resolve, reject) => { 123 | resolve(200) 124 | } 125 | } 126 | const p2 = Promise.resolve(thenable) 127 | p2.then(val => { 128 | console.log(2, val) 129 | }) 130 | 131 | 132 | // 实际应用,例如将 jquery.ajax 返回的 deferred 对象转换为 promise 实例 133 | // 这里的 deferred 对象就是一个 thenable 对象 134 | // var jsPromise = Promise.resolve($.ajax('/whatever.json')); 135 | } 136 | // fn5() 137 | 138 | 139 | // ------ Q.nfcall 和 Q.nfapply -------- 140 | function fn6() { 141 | 142 | const fullFileName1 = path.resolve(__dirname, '../data/data1.json') 143 | const result1 = Q.nfcall(fs.readFile, fullFileName1, 'utf-8') 144 | result1.then(data => { 145 | console.log(data) 146 | }).catch(err => { 147 | console.log(err.stack) 148 | }) 149 | 150 | const fullFileName2 = path.resolve(__dirname, '../data/data2.json') 151 | const result2 = Q.nfapply(fs.readFile, [fullFileName2, 'utf-8']) // 使用 Q.nfapply 返回一个 promise 152 | result2.then(data => { 153 | console.log(data) 154 | }).catch(err => { 155 | console.log(err.stack) 156 | }) 157 | 158 | } 159 | fn6() 160 | 161 | 162 | -------------------------------------------------------------------------------- /part4-generator/01-generator-in-es6.md: -------------------------------------------------------------------------------- 1 | # ES6 中的 Generator 2 | 3 | 在 ES6 出现之前,基本都是各式各样类似`Promise`的解决方案来处理异步操作的代码逻辑,但是 ES6 的`Generator`却给异步操作又提供了新的思路,马上就有人给出了如何用`Generator`来更加优雅的处理异步操作。 4 | 5 | ## 本节内容概述 6 | 7 | - `Generator`简介 8 | - `Generator`最终如何处理异步操作 9 | - 接下来... 10 | 11 | ## `Generator`简介 12 | 13 | 先来一段最基础的`Generator`代码 14 | 15 | ```javascript 16 | function* Hello() { 17 | yield 100 18 | yield (function () {return 200})() 19 | return 300 20 | } 21 | 22 | var h = Hello() 23 | console.log(typeof h) // object 24 | 25 | console.log(h.next()) // { value: 100, done: false } 26 | console.log(h.next()) // { value: 200, done: false } 27 | console.log(h.next()) // { value: 300, done: true } 28 | console.log(h.next()) // { value: undefined, done: true } 29 | ``` 30 | 31 | 在 nodejs 环境执行这段代码,打印出来的数据都在代码注释中了,也可以自己去试试。将这段代码简单分析一下吧 32 | 33 | - 定义`Generator`时,需要使用`function*`,其他的和定义函数一样。内部使用`yield`,至于`yield`的用处以后再说 34 | - 执行`var h = Hello()`生成一个`Generator`对象,经验验证`typeof h`发现不是普通的函数 35 | - 执行`Hello()`之后,`Hello`内部的代码不会立即执行,而是出于一个**暂停**状态 36 | - 执行第一个`h.next()`时,会激活刚才的暂停状态,开始执行`Hello`内部的语句,但是,直到遇到`yield`语句。一旦遇到`yield`语句时,它就会将`yield`后面的表达式执行,并返回执行的结果,然后又立即进入**暂停**状态。 37 | - 因此第一个`console.log(h.next())`打印出来的是`{ value: 100, done: false }`,`value`是第一个`yield`返回的值,`done: false`表示目前处于暂停状态,尚未执行结束,还可以再继续往下执行。 38 | - 执行第二个`h.next()`和第一个一样,不在赘述。此时会执行完第二个`yield`后面的表达式并返回结果,然后再次进入**暂停**状态 39 | - 执行第三个`h.next()`时,程序会打破暂停状态,继续往下执行,但是遇到的不是`yield`而是`return`。这就预示着,即将执行结束了。因此最后返回的是`{ value: 300, done: true }`,`done: true`表示执行结束,无法再继续往下执行了。 40 | - 再去执行第四次`h.next()`时,就只能得到`{ value: undefined, done: true }`,因为已经结束,没有返回值了。 41 | 42 | 一口气分析下来,发现并不是那么简单,虽然这只是一个最最简单的`Generator`入门代码 ———— 可见`Generator`的学习成本多高 ———— 但是一旦学会,那将受用无穷!别着急,跟着我的节奏慢慢来,一行一行代码看,你会很快深入了解`Genarator` 43 | 44 | 但是,你要详细看一下上面的所有步骤,争取把我写的每一步都搞明白。如果搞不明白细节,至少要明白以下几个要点: 45 | 46 | - `Generator`不是函数,不是函数,不是函数 47 | - `Hello()`不会立即出发执行,而是一上来就暂停 48 | - 每次`h.next()`都会打破暂停状态去执行,直到遇到下一个`yield`或者`return` 49 | - 遇到`yield`时,会执行`yeild`后面的表达式,并返回执行之后的值,然后再次进入暂停状态,此时`done: false`。 50 | - 遇到`return`时,会返回值,执行结束,即`done: true` 51 | - 每次`h.next()`的返回值永远都是`{value: ... , done: ...}`的形式 52 | 53 | 54 | ## `Generator`最终如何处理异步操作 55 | 56 | 上面只是一个最基本最简单的介绍,但是我们看不到任何与异步操作相关的事情,那我们接下来就先展示一下最终我们将使用`Generator`如何做异步操作。 57 | 58 | 之前讲解`Promise`时候,依次读取多个文件,我们是这么操作的(看不明白的需要回炉重造哈),主要是使用`then`做链式操作。 59 | 60 | ```javascript 61 | readFilePromise('some1.json').then(data => { 62 | console.log(data) // 打印第 1 个文件内容 63 | return readFilePromise('some2.json') 64 | }).then(data => { 65 | console.log(data) // 打印第 2 个文件内容 66 | return readFilePromise('some3.json') 67 | }).then(data => { 68 | console.log(data) // 打印第 3 个文件内容 69 | return readFilePromise('some4.json') 70 | }).then(data=> { 71 | console.log(data) // 打印第 4 个文件内容 72 | }) 73 | ``` 74 | 75 | 而如果学会`Generator`那么读取多个文件就是如下这样写。先不要管如何实现的,光看一看代码,你就能比较出哪个更加简洁、更加易读、更加所谓的优雅! 76 | 77 | ```javascript 78 | co(function* () { 79 | const r1 = yield readFilePromise('some1.json') 80 | console.log(r1) // 打印第 1 个文件内容 81 | const r2 = yield readFilePromise('some2.json') 82 | console.log(r2) // 打印第 2 个文件内容 83 | const r3 = yield readFilePromise('some3.json') 84 | console.log(r3) // 打印第 3 个文件内容 85 | const r4 = yield readFilePromise('some4.json') 86 | console.log(r4) // 打印第 4 个文件内容 87 | }) 88 | ``` 89 | 90 | 不过,要学到这一步,还需要很长的路要走。不过不要惊慌,也不要请如来佛祖,跟着我的节奏来,认真看,一天包教包会是没问题的! 91 | 92 | ## 接下来... 93 | 94 | 接下来我们不会立刻讲解如何使用`Generator`做异步操作,而是看一看`Generator`是一个什么东西!说来话长,这要从 ES6 的另一个概念`Iterator`说起。 95 | 96 | ## 求打赏 97 | 98 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 99 | 100 |  -------------------------------------------------------------------------------- /part4-generator/02-iterator.md: -------------------------------------------------------------------------------- 1 | # Iterator 遍历器 2 | 3 | ES6 中引入了很多此前没有但是却非常重要的概念,`Iterator`就是其中一个。`Iterator`对象是一个指针对象,实现类似于单项链表的数据结构,通过`next()`将指针指向下一个节点 ———— 这里也就是先简单做一个概念性的介绍,后面将通过实例为大家演示。 4 | 5 | 本节演示的代码可参考[这里](./test.js) 6 | 7 | ## 本节内容概述 8 | 9 | - 简介`Symbol`数据类型 10 | - 具有`[Symbol.iterator]`属性的数据类型 11 | - 生成`Iterator`对象 12 | - `Generator`返回的也是`Iterator`对象 13 | - 接下来... 14 | 15 | ## 简介`Symbol`数据类型 16 | 17 | `Symbol`是一个特殊的数据类型,和`number` `string`等并列,详细的教程可参考[阮一峰老师 ES6 入门的 Symbol 篇](http://es6.ruanyifeng.com/#docs/symbol)。先看两句程序 18 | 19 | ```javascript 20 | console.log(Array.prototype.slice) // [Function: slice] 21 | console.log(Array.prototype[Symbol.iterator]) // [Function: values] 22 | ``` 23 | 24 | 数组的`slice`属性大家都比较熟悉了,就是一个函数,可以通过`Array.prototype.slice`得到。这里的`slice`是一个字符串,但是我们获取`Array.prototype[Symbol.iterator]`可以得到一个函数,只不过这里的`[Symbol.iterator]`是`Symbol`数据类型,不是字符串。但是没关系,`Symbol`数据类型也可以作为对象属性的`key`。如下: 25 | 26 | ```javascript 27 | var obj = {} 28 | obj.a = 100 29 | obj[Symbol.iterator] = 200 30 | console.log(obj) // {a: 100, Symbol(Symbol.iterator): 200} 31 | ``` 32 | 33 | 在此小节中,你只需要知道`[Symbol.iterator]`是一个特殊的数据类型`Symbol`类型,但是也可以像`number` `string`类型一样,作为对象的属性`key`来使用 34 | 35 | ## 原生具有`[Symbol.iterator]`属性的数据类型 36 | 37 | 在 ES6 中,原生具有`[Symbol.iterator]`属性数据类型有:数组、某些类似数组的对象(如`arguments`、`NodeList`)、`Set`和`Map`。其中,`Set`和`Map`也是 ES6 中新增的数据类型。 38 | 39 | ```javascript 40 | // 数组 41 | console.log([1, 2, 3][Symbol.iterator]) // function values() { [native code] } 42 | // 某些类似数组的对象,NoeList 43 | console.log(document.getElementsByTagName('div')[Symbol.iterator]) // function values() { [native code] } 44 | ``` 45 | 46 | 原生具有`[Symbol.iterator]`属性数据类型有一个特点,就是可以使用`for...of`来取值,例如 47 | 48 | ```javascript 49 | var item 50 | for (item of [100, 200, 300]) { 51 | console.log(item) 52 | } 53 | // 打印出:100 200 300 54 | // 注意,这里每次获取的 item 是数组的 value,而不是 index ,这一点和 传统 for 循环以及 for...in 完全不一样 55 | ``` 56 | 57 | 而具有`[Symbol.iterator]`属性的对象,都可以一键生成一个`Iterator`对象。如何生成以及生成之后什么样子,还有生成之后的作用,下文分解。 58 | 59 | 不要着急,也不要跳过本文的任何步骤,一步一步跟着我的节奏来看。 60 | 61 | ## 生成`Iterator`对象 62 | 63 | 定义一个数组,然后生成数组的`Iterator`对象 64 | 65 | ```javascript 66 | const arr = [100, 200, 300] 67 | const iterator = arr[Symbol.iterator]() // 通过执行 [Symbol.iterator] 的属性值(函数)来返回一个 iterator 对象 68 | ``` 69 | 70 | 好,现在生成了`iterator`,那么该如何使用它呢 ———— 有两种方式:`next`和`for...of`。 71 | 72 | 先说第一种,`next` 73 | 74 | ```javascript 75 | console.log(iterator.next()) // { value: 100, done: false } 76 | console.log(iterator.next()) // { value: 200, done: false } 77 | console.log(iterator.next()) // { value: 300, done: false } 78 | console.log(iterator.next()) // { value: undefined, done: true } 79 | ``` 80 | 81 | 看到这里,再结合上一节内容,是不是似曾相识的感觉?(额,没有的话,那你就回去重新看上一节的内容吧) `iterator`对象可以通过`next()`方法逐步获取每个元素的值,以`{ value: ..., done: ... }`形式返回,`value`就是值,`done`表示是否到已经获取完成。 82 | 83 | 再说第二种,`for...of` 84 | 85 | ```javascript 86 | let i 87 | for (i of iterator) { 88 | console.log(i) 89 | } 90 | // 打印:100 200 300 91 | ``` 92 | 93 | 上面使用`for...of`遍历`iterator`对象,可以直接将其值获取出来。这里的“值”就对应着上面`next()`返回的结果的`value`属性 94 | 95 | ## `Generator`返回的也是`Iterator`对象 96 | 97 | 看到这里,你大体也应该明白了,上一节演示的`Generator`,就是生成一个`Iterator`对象。因此才会有`next()`,也可以通过`for...of`来遍历。拿出上一节的例子再做一次演示: 98 | 99 | ```javascript 100 | function* Hello() { 101 | yield 100 102 | yield (function () {return 200})() 103 | return 300 104 | } 105 | const h = Hello() 106 | console.log(h[Symbol.iterator]) // [Function: [Symbol.iterator]] 107 | ``` 108 | 109 | 执行`const h = Hello()`得到的就是一个`iterator`对象,因为`h[Symbol.iterator]`是有值的。既然是`iterator`对象,那么就可以使用`next()`和`for...of`进行操作 110 | 111 | ```javascript 112 | console.log(h.next()) // { value: 100, done: false } 113 | console.log(h.next()) // { value: 200, done: false } 114 | console.log(h.next()) // { value: 300, done: false } 115 | console.log(h.next()) // { value: undefined, done: true } 116 | 117 | let i 118 | for (i of h) { 119 | console.log(i) 120 | } 121 | ``` 122 | 123 | ## 接下来... 124 | 125 | 这一节我们花费很大力气,从`Iterator`又回归到了`Generator`,目的就是为了看看`Generator`到底是一个什么东西。了解其本质,才能更好的使用它,否则总有一种抓瞎的感觉。 126 | 127 | 接下来我们就`Generator`具体有哪些使用场景。 128 | 129 | ## 求打赏 130 | 131 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 132 | 133 |  -------------------------------------------------------------------------------- /part4-generator/03-iterator-use.md: -------------------------------------------------------------------------------- 1 | # Generator 的具体应用 2 | 3 | 前面用两节的内容介绍了`Generator`可以让执行处于暂停状态,并且知道了`Generator`返回的是一个`Iterator`对象,这一节就详细介绍一下`Generator`的一些基本用法。 4 | 5 | 本节演示的代码可参考[这里](./test.js) 6 | 7 | ## 本节内容概述 8 | 9 | - `next`和`yield`参数传递 10 | - `for...of`的应用示例 11 | - `yield* `语句 12 | - `Generator`中的`this` 13 | - 接下来... 14 | 15 | ## `next`和`yield`参数传递 16 | 17 | 我们之前已经知道,`yield`具有返回数据的功能,如下代码。`yield`后面的数据被返回,存放到返回结果中的`value`属性中。这算是一个方向的参数传递。 18 | 19 | ```javascript 20 | function* G() { 21 | yield 100 22 | } 23 | const g = G() 24 | console.log( g.next() ) // {value: 100, done: false} 25 | ``` 26 | 27 | 还有另外一个方向的参数传递,就是`next`向`yield`传递,如下代码。 28 | 29 | ```javascript 30 | function* G() { 31 | const a = yield 100 32 | console.log('a', a) // a aaa 33 | const b = yield 200 34 | console.log('b', b) // b bbb 35 | const c = yield 300 36 | console.log('c', c) // c ccc 37 | } 38 | const g = G() 39 | g.next() // value: 100, done: false 40 | g.next('aaa') // value: 200, done: false 41 | g.next('bbb') // value: 300, done: false 42 | g.next('ccc') // value: undefined, done: true 43 | ``` 44 | 45 | 捋一捋上面代码的执行过程: 46 | 47 | - 执行第一个`g.next()`时,为传递任何参数,返回的`{value: 100, done: false}`,这个应该没有疑问 48 | - 执行第二个`g.next('aaa')`时,传递的参数是`'aaa'`,这个`'aaa'`就会被赋值到`G`内部的`a`标量中,然后执行`console.log('a', a)`打印出来,最后返回`{value: 200, done: false}` 49 | - 执行第三个、第四个时,道理都是完全一样的,大家自己捋一捋。 50 | 51 | **有一个要点需要注意,就`g.next('aaa')`是将`'aaa'`传递给上一个已经执行完了的`yield`语句前面的变量,而不是即将执行的`yield`前面的变量**。这句话要能看明白,看不明白就说明刚才的代码你还没看懂,继续看。 52 | 53 | ## `for...of`的应用示例 54 | 55 | 针对`for...of`在`Iterator`对象的操作之前已经介绍过了,不过这里用一个非常好的例子来展示一下。用简单几行代码实现斐波那契数列。通过之前学过的`Generator`知识,应该不难解读这份代码。 56 | 57 | ```javascript 58 | function* fibonacci() { 59 | let [prev, curr] = [0, 1] 60 | for (;;) { 61 | [prev, curr] = [curr, prev + curr] 62 | // 将中间值通过 yield 返回,并且保留函数执行的状态,因此可以非常简单的实现 fibonacci 63 | yield curr 64 | } 65 | } 66 | for (let n of fibonacci()) { 67 | if (n > 1000) { 68 | break 69 | } 70 | console.log(n) 71 | } 72 | ``` 73 | 74 | ## `yield* `语句 75 | 76 | 如果有两个`Generator`,想要在第一个中包含第二个,如下需求: 77 | 78 | ```javascript 79 | function* G1() { 80 | yield 'a' 81 | yield 'b' 82 | } 83 | function* G2() { 84 | yield 'x' 85 | yield 'y' 86 | } 87 | ``` 88 | 89 | 针对以上两个`Generator`,我的需求是:一次输出`a x y b`,该如何做?有同学看到这里想起了刚刚学到的`for..of`可以实现————不错,确实可以实现(大家也可以想想到底该如何实现) 90 | 91 | 但是,这要演示一个更加简洁的方式`yield* `表达式 92 | 93 | ```javascript 94 | function* G1() { 95 | yield 'a' 96 | yield* G2() // 使用 yield* 执行 G2() 97 | yield 'b' 98 | } 99 | function* G2() { 100 | yield 'x' 101 | yield 'y' 102 | } 103 | for (let item of G1()) { 104 | console.log(item) 105 | } 106 | ``` 107 | 108 | 之前学过的`yield`后面会接一个普通的 JS 对象,而`yield* `后面会接一个`Generator`,而且会把它其中的`yield`按照规则来一步一步执行。**如果有多个`Generator`串联使用的话(例如`Koa`源码中),用`yield* `来操作非常方便**。 109 | 110 | ## `Generator`中的`this` 111 | 112 | 对于以下这种写法,大家可能会和构造函数创建对象的写法产生混淆,这里一定要注意 —— **Generator 不是函数,更不是构造函数** 113 | 114 | ```javascript 115 | function* G() {} 116 | const g = G() 117 | ``` 118 | 119 | 而以下这种写法,更加不会成功。只有构造函数才会这么用,构造函数返回的是`this`,而`Generator`返回的是一个`Iterator`对象。完全是两码事,千万不要搞混了。 120 | 121 | ```javascript 122 | function* G() { 123 | this.a = 10 124 | } 125 | const g = G() 126 | console.log(g.a) // 报错 127 | ``` 128 | 129 | ## 接下来... 130 | 131 | 本节基本介绍了`Generator`的最常见的用法,但是还是没有和咱们的最终目的————异步操作————沾上关系,而且现在看来有点八竿子打不着的关系。但是话说回来,这几节内容,你也学到了不少知识啊。 132 | 133 | 别急哈,即便是下一节,它们还不会有联系,再下一节就真相大白了。下一节我们又给出一个新概念————`Thunk`函数 134 | 135 | ## 求打赏 136 | 137 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 138 | 139 |  -------------------------------------------------------------------------------- /part4-generator/04-thunk.md: -------------------------------------------------------------------------------- 1 | # Thunk 函数 2 | 3 | 要想让`Generator`和异步操作产生联系,就必须过`thunk`函数这一关。这一关过了之后,立即就可以着手异步操作的事情,因此大家再坚持坚持。至于`thunk`函数是什么,下文会详细演示。 4 | 5 | 本节演示的代码可参考[这里](./test.js) 6 | 7 | ## 本节内容概述 8 | 9 | - 一个普通的异步函数 10 | - 封装成一个`thunk`函数 11 | - `thunk`函数的特点 12 | - 使用`thunkify`库 13 | - 接下来... 14 | 15 | ## 一个普通的异步函数 16 | 17 | 就用 nodejs 中读取文件的函数为例,通常都这么写 18 | 19 | ```javascript 20 | fs.readFile('data1.json', 'utf-8', (err, data) => { 21 | // 获取文件内容 22 | }) 23 | ``` 24 | 25 | 其实这个写法就是将三个参数都传递给`fs.readFile`这个方法,其中最后一个参数是一个`callback`函数。这种函数叫做 **多参数函数**,我们接下来做一个改造 26 | 27 | ## 封装成一个`thunk`函数 28 | 29 | 改造的代码如下所示。不过是不是感觉越改造越复杂了?不过请相信:你看到的复杂仅仅是表面的,**这一点东西变的复杂,是为了让以后更加复杂的东西变得简单**。对于个体而言,随性比较简单,遵守规则比较复杂;但是对于整体(包含很多个体)而言,大家都随性就不好控制了,而大家都遵守规则就很容易管理 ———— 就是这个道理! 30 | 31 | ```javascript 32 | const thunk = function (fileName, codeType) { 33 | // 返回一个只接受 callback 参数的函数 34 | return function (callback) { 35 | fs.readFile(fileName, codeType, callback) 36 | } 37 | } 38 | const readFileThunk = thunk('data1.json', 'utf-8') 39 | readFileThunk((err, data) => { 40 | // 获取文件内容 41 | }) 42 | ``` 43 | 44 | 先自己看一看以上代码,应该是能看懂的,但是你可能就是看懂了却不知道这么做的意义在哪里。意义先不管,先把它看懂,意义下一节就会看到。 45 | 46 | - 执行`const readFileThunk = thunk('data1.json', 'utf-8')`返回的其实是一个函数 47 | - `readFileThunk`这个函数,只接受一个参数,而且这个参数是一个`callback`函数 48 | 49 | ## `thunk`函数的特点 50 | 51 | 就上上面的代码,我们经过对传统的异步操作函数进行封装,**得到一个只有一个参数的函数,而且这个参数是一个`callback`函数,那这就是一个`thunk`函数**。就像上面代码中`readFileThunk`一样。 52 | 53 | ## 使用`thunkify`库 54 | 55 | 上面代码的封装,是我们手动来做的,但是没遇到一个情况就需要手动做吗?在这个开源的时代当让不会这样,直接使用第三方的`thunkify`就好了。 56 | 57 | 首先要安装`npm i thunkify --save`,然后在代码的最上方引用`const thunkify = require('thunkify')`。最后,上面我们手动写的代码,完全可以简化成这几行,非常简单! 58 | 59 | ```javascript 60 | const thunk = thunkify(fs.readFile) 61 | const readFileThunk = thunk('data1.json', 'utf-8') 62 | readFileThunk((err, data) => { 63 | // 获取文件内容 64 | }) 65 | ``` 66 | 67 | ## 接下来... 68 | 69 | 了解了`thunk`函数,我们立刻就将`Generator`和异步操作进行结合 70 | 71 | ## 求打赏 72 | 73 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 74 | 75 |  -------------------------------------------------------------------------------- /part4-generator/05-generator-for-async.md: -------------------------------------------------------------------------------- 1 | # Generator 与异步操作 2 | 3 | 这一节正式开始讲解`Generator`如何进行异步操作,以前我们花了好几节的时间各种打基础,现在估计大家也都等急了,好戏马上开始! 4 | 5 | 本节演示的代码可参考[这里](./test.js) 6 | 7 | ## 本节内容概述 8 | 9 | - 在`Genertor`中使用`thunk`函数 10 | - 挨个读取两个文件的内容 11 | - 自驱动流程 12 | - 使用`co`库 13 | - `co`库和`Promise` 14 | - 接下来... 15 | 16 | ## 在`Genertor`中使用`thunk`函数 17 | 18 | 这个比较简单了,之前都讲过的,直接看代码即可。代码中表达的意思,是要依次读取两个文件的内容 19 | 20 | ```javascript 21 | const readFileThunk = thunkify(fs.readFile) 22 | const gen = function* () { 23 | const r1 = yield readFileThunk('data1.json') 24 | console.log(r1) 25 | const r2 = yield readFileThunk('data2.json') 26 | console.log(r2) 27 | } 28 | ``` 29 | 30 | ## 挨个读取两个文件的内容 31 | 32 | 接着以上的代码继续写,注释写的非常详细,大家自己去看,看完自己写代码亲身体验。 33 | 34 | ```javascript 35 | const g = gen() 36 | 37 | // 试着打印 g.next() 这里一定要明白 value 是一个 thunk函数 ,否则下面的代码你都看不懂 38 | // console.log( g.next() ) // g.next() 返回 {{ value: thunk函数, done: false }} 39 | 40 | // 下一行中,g.next().value 是一个 thunk 函数,它需要一个 callback 函数作为参数传递进去 41 | g.next().value((err, data1) => { 42 | // 这里的 data1 获取的就是第一个文件的内容。下一行中,g.next(data1) 可以将数据传递给上面的 r1 变量,此前已经讲过这种参数传递的形式 43 | // 下一行中,g.next(data1).value 又是一个 thunk 函数,它又需要一个 callback 函数作为参数传递进去 44 | g.next(data1).value((err, data2) => { 45 | // 这里的 data2 获取的是第二个文件的内容,通过 g.next(data2) 将数据传递个上面的 r2 变量 46 | g.next(data2) 47 | }) 48 | }) 49 | ``` 50 | 51 | 上面 6 行左右的代码,却用了 6 行左右的注释来解释,可见代码的逻辑并不简单,不过你还是要去尽力理解,否则接下来的内容无法继续。再说,我已经写的那么详细了,你只要照着仔细看肯定能看明白的。 52 | 53 | 也许上面的代码给你带来的感觉并不好,第一它逻辑复杂,第二它也不是那么易读、简洁呀,用`Generator`实现异步操作就是这个样子的?———— 当然不是,继续往下看。 54 | 55 | ## 自驱动流程 56 | 57 | 以上代码中,读取两个文件的内容都是手动一行一行写的,而我们接下来要做一个自驱动的流程,定义好`Generator`的代码之后,就让它自动执行。完整的代码如下所示: 58 | 59 | ```javascript 60 | // 自动流程管理的函数 61 | function run(generator) { 62 | const g = generator() 63 | function next(err, data) { 64 | const result = g.next(data) // 返回 { value: thunk函数, done: ... } 65 | if (result.done) { 66 | // result.done 表示是否结束,如果结束了那就 return 作罢 67 | return 68 | } 69 | result.value(next) // result.value 是一个 thunk 函数,需要一个 callback 函数作为参数,而 next 就是一个 callback 形式的函数 70 | } 71 | next() // 手动执行以启动第一次 next 72 | } 73 | 74 | // 定义 Generator 75 | const readFileThunk = thunkify(fs.readFile) 76 | const gen = function* () { 77 | const r1 = yield readFileThunk('data1.json') 78 | console.log(r1.toString()) 79 | const r2 = yield readFileThunk('data2.json') 80 | console.log(r2.toString()) 81 | } 82 | 83 | // 启动执行 84 | run(gen) 85 | ``` 86 | 87 | 其实这段代码和上面的手动编写读取两个文件内容的代码,原理上是一模一样的,只不过这里把流程驱动给封装起来了。我们简单分析一下这段代码 88 | 89 | - 最后一行`run(gen)`之后,进入`run`函数内部执行 90 | - 先`const g = generator()`创建`Generator`实例,然后定义一个`next`方法,并且立即执行`next()` 91 | - 注意这个`next`函数的参数是`err, data`两个,和我们`fs.readFile`用到的`callback`函数形式完全一样 92 | - 第一次执行`next`时,会执行`const result = g.next(data)`,而`g.next(data)`返回的是`{ value: thunk函数, done: ... }`,`value`是一个`thunk`函数,`done`表示是否结束 93 | - 如果`done: true`,那就直接`return`了,否则继续进行 94 | - `result.value`是一个`thunk`函数,需要接受一个`callback`函数作为参数传递进去,因此正好把`next`给传递进去,让`next`一直被执行下去 95 | 96 | 大家照着这个过程来捋一捋,不是特别麻烦,然后自己试着写完运行一下,基本就能了解了。 97 | 98 | ## 使用`co`库 99 | 100 | 刚才我们定义了一个`run`还是来做自助流程管理,是不是每次使用都得写一遍`run`函数呢?———— 肯定不是的,直接用大名鼎鼎的`co`就好了。用`Generator`的工程师,肯定需要用到`co`,两者天生一对,难舍难分。 101 | 102 | 使用之前请安装`npm i co --save`,然后在文件开头引用`const co = require('co')`。`co`到底有多好用,我们将刚才的代码用`co`重写,就变成了如下代码。非常简洁 103 | 104 | ```javascript 105 | // 定义 Generator 106 | const readFileThunk = thunkify(fs.readFile) 107 | const gen = function* () { 108 | const r1 = yield readFileThunk('data1.json') 109 | console.log(r1.toString()) 110 | const r2 = yield readFileThunk('data2.json') 111 | console.log(r2.toString()) 112 | } 113 | const c = co(gen) 114 | ``` 115 | 116 | 而且`const c = co(gen)`返回的是一个`Promise`对象,可以接着这么写 117 | 118 | ```javascript 119 | c.then(data => { 120 | console.log('结束') 121 | }) 122 | ``` 123 | 124 | ## `co`库和`Promise` 125 | 126 | 刚才提到`co()`最终返回的是`Promise`对象,后知后觉,我们已经忘记`Promise`好久了,现在要重新把它拾起来。**如果使用`co`来处理`Generator`的话,其实`yield`后面可以跟`thunk`函数,也可以跟`Promise`对象。** 127 | 128 | `thunk`函数上文一直在演示,下面演示一下`Promise`对象的,也权当再回顾一下久别的`Promise`。其实从形式上和结果上,都跟`thunk`函数一样。 129 | 130 | ```javascript 131 | const readFilePromise = Q.denodeify(fs.readFile) 132 | 133 | const gen = function* () { 134 | const r1 = yield readFilePromise('data1.json') 135 | console.log(r1.toString()) 136 | const r2 = yield readFilePromise('data2.json') 137 | console.log(r2.toString()) 138 | } 139 | 140 | co(gen) 141 | ``` 142 | 143 | ## 接下来... 144 | 145 | 经过了前几节的技术积累,我们用一节的时间就讲述了`Generator`如何进行异步操作。接下来要介绍一个开源社区中比较典型的使用`Generator`的框架 ———— Koa 146 | 147 | ## 求打赏 148 | 149 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 150 | 151 |  -------------------------------------------------------------------------------- /part4-generator/06-generator-for-koa.md: -------------------------------------------------------------------------------- 1 | # koa 中使用 Generator 2 | 3 | [koa](https://github.com/koajs/koa) 是一个 nodejs 开发的 web 框架,所谓 web 框架就是处理 http 请求的。开源的 nodejs 开发的 web 框架最初是 [express](https://github.com/expressjs/express)。 4 | 5 | 我们此前说过,既然是处理 http 请求,是一种网络操作,肯定就会用到异步操作。express 使用的异步操作是传统的`callbck`,而 koa 用的是我们刚刚讲的`Generator`(koa `v1.x`用的是`Generator`,已经被广泛使用,而 koa `v2.x`用到了 ES7 中的`async-await`,不过因为 ES7 没有正式发布,所以 koa `v2.x`也没有正式发布,不过可以试用) 6 | 7 | koa 是由 express 的原班开发人员开发的,比 express 更加简洁易用,**因此 koa 是目前最为推荐的 nodejs web 框架**。阿里前不久就依赖于 koa 开发了自己的 nodejs web 框架 [egg](https://github.com/eggjs/egg) 8 | 9 | 国内可以通过[koa.bootcss.com](http://koa.bootcss.com/)查阅文档,*不过这网站依赖了 Google 的服务,因此如果不科学上网,估计会访问会很慢*。 10 | 11 | **提醒:如果你是初学`Generator`而且从来没有用过 koa ,那么这一节你如果看不懂,没有问题。看不懂就不要强求,可以忽略,继续往下看!** 12 | 13 | 本节演示的代码可参考[这里](./test.js) 14 | 15 | ## 本节内容概述 16 | 17 | - koa 中如何应用`Generator` 18 | - koa 的这种应用机制是如何实现的 19 | - 接下来... 20 | 21 | ## koa 中如何应用`Generator` 22 | 23 | koa 是一个 web 框架,处理 http 请求,但是这里我们不去管它如何处理 http 请求,而是直接关注它使用`Genertor`的部分————**中间件**。 24 | 25 | 例如,我们现在要用 3 个`Generator`输出`12345`,我们如下代码这么写。应该能看明白吧?看不明白回炉重造! 26 | 27 | ```javascript 28 | let info = '' 29 | function* g1() { 30 | info += '1' // 拼接 1 31 | yield* g2() // 拼接 234 32 | info += '5' // 拼接 5 33 | } 34 | function* g2() { 35 | info += '2' // 拼接 2 36 | yield* g3() // 拼接 3 37 | info += '4' // 拼接 4 38 | } 39 | function* g3() { 40 | info += '3' // 拼接 3 41 | } 42 | 43 | var g = g1() 44 | g.next() 45 | console.log(info) // 12345 46 | ``` 47 | 48 | 但是如果用 koa 的 **中间件** 的思路来做,就需要如下这么写。 49 | 50 | ```javascript 51 | app.use(function *(next){ 52 | this.body = '1'; 53 | yield next; 54 | this.body += '5'; 55 | console.log(this.body); 56 | }); 57 | app.use(function *(next){ 58 | this.body += '2'; 59 | yield next; 60 | this.body += '4'; 61 | }); 62 | app.use(function *(next){ 63 | this.body += '3'; 64 | }); 65 | ``` 66 | 67 | 解释几个关键点 68 | 69 | - `app.use()`中传入的每一个`Generator`就是一个 **中间件**,中间件按照传入的顺序排列,顺序不能乱 70 | - 每个中间件内部,`next`表示下一个中间件。`yield next`就是先将程序暂停,先去执行下一个中间件,等`next`被执行完之后,再回过头来执行当前代码的下一行。**因此,koa 的中间件执行顺序是一种[洋葱圈模型](https://eggjs.org/zh-cn/intro/egg-and-koa.html#midlleware),不过这里看不懂也没问题**。 71 | - 每个中间件内部,`this`可以共享变量。即第一个中间件改变了`this`的属性,在第二个中间件中可以看到效果。 72 | 73 | ## koa 的这种应用机制是如何实现的 74 | 75 | 前方高能————上面介绍 koa 的中间价估计有些新人就开始蒙圈了,不过接下来还有更加有挑战难度的,就是以上这种方式是如何实现的。你就尽量去看,看懂了更好,看不懂也没关系————当然,你完全可以选择跳过本教程直接去看下一篇,这都 OK 76 | 77 | 加入我们自己实现一个简单的 koa ———— MyKoa ,那么仅需要几十行代码就可以搞定上面的问题。直接写代码,注意看重点部分的注释 78 | 79 | ```javascript 80 | class MyKoa extends Object { 81 | constructor(props) { 82 | super(props); 83 | 84 | // 存储所有的中间件 85 | this.middlewares = [] 86 | } 87 | 88 | // 注入中间件 89 | use (generator) { 90 | this.middlewares.push(generator) 91 | } 92 | 93 | // 执行中间件 94 | listen () { 95 | this._run() 96 | } 97 | 98 | _run () { 99 | const ctx = this 100 | const middlewares = ctx.middlewares 101 | co(function* () { 102 | let prev = null 103 | let i = middlewares.length 104 | //从最后一个中间件到第一个中间件的顺序开始遍历 105 | while (i--) { 106 | // ctx 作为函数执行时的 this 才能保证多个中间件中数据的共享 107 | //prev 将前面一个中间件传递给当前中间件,才使得中间件里面的 next 指向下一个中间件 108 | prev = middlewares[i].call(ctx, prev); 109 | } 110 | //执行第一个中间件 111 | yield prev; 112 | }) 113 | } 114 | } 115 | ``` 116 | 117 | 最后我们执行代码实验一下效果 118 | 119 | ```javascript 120 | var app = new MyKoa(); 121 | app.use(function *(next){ 122 | this.body = '1'; 123 | yield next; 124 | this.body += '5'; 125 | console.log(this.body); // 12345 126 | }); 127 | app.use(function *(next){ 128 | this.body += '2'; 129 | yield next; 130 | this.body += '4'; 131 | }); 132 | app.use(function *(next){ 133 | this.body += '3'; 134 | }); 135 | app.listen(); 136 | ``` 137 | 138 | ## 接下来... 139 | 140 | `Generator`的应用基本讲完,从一开始的基础到后面应用到异步操作,再到本节的高级应用 koa ,算是比较全面了。接下来,我们要再回到最初的起点,探讨`Generator`的本质,以及它和`callback`的关系。 141 | 142 | 还是那句话,搞明白原理,才能用的更加出色! 143 | 144 | ## 求打赏 145 | 146 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 147 | 148 |  -------------------------------------------------------------------------------- /part4-generator/07-generator-callback.md: -------------------------------------------------------------------------------- 1 | # Generator 的本质是什么?是否取代了 callback 2 | 3 | 其实标题中的问题,是一个伪命题,因为`Generator`和`callback`根本没有任何关系,只是我们通过一些方式(而且是很复杂的方式)强行将他俩产生了关系,才会有现在的`Generator`处理异步。 4 | 5 | ## 本节内容概述 6 | 7 | - `Generator`的本质 8 | - 和`callback`的结合 9 | 10 | ## `Generator`的本质 11 | 12 | 介绍`Generator`的[第一节](./01-generator-in-es6.md)中,多次提到 **暂停** 这个词 ———— **“暂停”才是`Generator`的本质** ———— 只有`Generator`能让一段程序执行到指定的位置先暂停,然后再启动,再暂停,再启动。 13 | 14 | 而这个 **暂停** 就很容易让它和异步操作产生联系,因为我们在处理异步操作时,即需要一种“开始读取文件,然后**暂停**一下,等着文件读取完了,再干嘛干嘛...”这样的需求。因此将`Generator`和异步操作联系在一起,并且产生一些比较简明的解决方案,这是顺其自然的事儿,大家要想明白这个道理。 15 | 16 | 不过,**JS 还是 JS,单线程还是单线程,异步还是异步,`callback`还是`callback`。这一切都不会因为有一个`Generator`而有任何变化**。 17 | 18 | ## 和`callback`的结合 19 | 20 | 之前在介绍`Promise`的最后,拿`Promise`和`callback`做过一些比较,最后发现`Promise`其实是利用了`callback`才能实现的。而这里,**`Generator`也必须利用`callback`才能实现**。 21 | 22 | 拿介绍`co`时的代码举例(代码如下),如果`yield`后面用的是`thunk`函数,那么`thunk`函数需要的就是一个`callback`参数。如果`yield`后面用的是`Promise`对象,`Promise`和`callback`的联系之前已经介绍过了。 23 | 24 | ```javascript 25 | co(function* () { 26 | const r1 = yield readFilePromise('some1.json') 27 | console.log(r1) // 打印第 1 个文件内容 28 | const r2 = yield readFileThunk('some2.json') 29 | console.log(r2) // 打印第 2 个文件内容 30 | }) 31 | ``` 32 | 33 | 因此,`Generator`离不开`callback`,`Promise`离不开`callback`,异步也离不开`callback`。 34 | 35 | ## 求打赏 36 | 37 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 38 | 39 |  -------------------------------------------------------------------------------- /part4-generator/test.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('fs') 3 | const path = require('path') 4 | const thunkify = require('thunkify') 5 | const co = require('co') 6 | const Q = require('q') 7 | 8 | function fn1() { 9 | function* Hello() { 10 | yield 100 11 | yield (function () {return 200})() 12 | return 300 13 | } 14 | var h = Hello() 15 | console.log(typeof h) // object 16 | console.log(h.next()) // { value: 100, done: false } 17 | console.log(h.next()) // { value: 200, done: false } 18 | console.log(h.next()) // { value: 300, done: true } 19 | console.log(h.next()) // { value: undefined, done: true } 20 | } 21 | // fn1() 22 | 23 | 24 | function fn2() { 25 | 26 | const symbolA = Symbol.a 27 | const obj = { 28 | 'a': 100, 29 | 'b': function () {console.log('this is b')}, 30 | symbolA: function () {console.log('this is symbol')} 31 | } 32 | 33 | } 34 | // fn2() 35 | 36 | 37 | function fn3() { 38 | 39 | // console.log(Array.prototype.slice) 40 | // console.log(Array.prototype[Symbol.iterator]) 41 | 42 | // let item 43 | // for (item of [100, 200, 300]) { 44 | // console.log(item) 45 | // } 46 | 47 | const arr = [100, 200, 300] 48 | const iterator = arr[Symbol.iterator]() 49 | // console.log(iterator.next()) 50 | // console.log(iterator.next()) 51 | // console.log(iterator.next()) 52 | // console.log(iterator.next()) 53 | 54 | // let i 55 | // for (i of iterator) { 56 | // console.log(i) 57 | // } 58 | 59 | function* Hello() { 60 | yield 100 61 | yield (function () {return 200})() 62 | return 300 63 | } 64 | const h = Hello() 65 | console.log(h[Symbol.iterator]) // [Function: [Symbol.iterator]] 66 | // console.log(h.next()) // { value: 100, done: false } 67 | // console.log(h.next()) // { value: 200, done: false } 68 | // console.log(h.next()) // { value: 300, done: false } 69 | // console.log(h.next()) // { value: undefined, done: true } 70 | 71 | let i 72 | for (i of h) { 73 | console.log(i) 74 | } 75 | } 76 | // fn3() 77 | 78 | 79 | function fn4() { 80 | 81 | // function* G() { 82 | // const a = yield 100 83 | // console.log('a', a) 84 | // const b = yield 200 85 | // console.log('b', b) 86 | // const c = yield 300 87 | // console.log('c', c) 88 | // } 89 | // const g = G() 90 | // g.next() // value: 100 91 | // g.next('aaa') // value: 200 92 | // g.next('bbb') // value: 300 93 | // g.next('ccc') // value: undefined 94 | 95 | 96 | // function* fibonacci() { 97 | // let [prev, curr] = [0, 1] 98 | // for (;;) { 99 | // [prev, curr] = [curr, prev + curr] 100 | // // 将中间值通过 yield 返回,并且保留函数执行的状态,因此可以非常简单的实现 fibonacci 101 | // yield curr 102 | // } 103 | // } 104 | // for (let n of fibonacci()) { 105 | // if (n > 1000) { 106 | // break 107 | // } 108 | // console.log(n) 109 | // } 110 | 111 | function* G1() { 112 | yield 'a' 113 | yield* G2() 114 | yield 'b' 115 | } 116 | function* G2() { 117 | yield 'x' 118 | yield 'y' 119 | } 120 | for (let item of G1()) { 121 | console.log(item) 122 | } 123 | 124 | } 125 | // fn4() 126 | 127 | 128 | function fn5() { 129 | 130 | // const thunk = function (fileName, codeType) { 131 | // // 返回一个只接受 callback 参数的函数 132 | // return function (callback) { 133 | // fs.readFile(fileName, codeType, callback) 134 | // } 135 | // } 136 | // const fileName = path.resolve(__dirname, '../data/data1.json') 137 | // const readFileThunk = thunk(fileName, 'utf-8') 138 | // readFileThunk((err, data) => { 139 | // console.log(data.toString()) 140 | // }) 141 | 142 | const thunk = thunkify(fs.readFile) 143 | const fileName = path.resolve(__dirname, '../data/data1.json') 144 | const readFileThunk = thunk(fileName, 'utf-8') 145 | readFileThunk((err, data) => { 146 | // 获取文件内容 147 | console.log(data) 148 | }) 149 | 150 | } 151 | // fn5() 152 | 153 | 154 | function fn6() { 155 | 156 | const readFileThunk = thunkify(fs.readFile) 157 | const fileName1 = path.resolve(__dirname, '../data/data1.json') 158 | const fileName2 = path.resolve(__dirname, '../data/data2.json') 159 | const gen = function* () { 160 | const r1 = yield readFileThunk(fileName1) 161 | console.log(111, r1.toString()) 162 | const r2 = yield readFileThunk(fileName2) 163 | console.log(222, r2.toString()) 164 | } 165 | 166 | const g = gen() 167 | 168 | g.next().value((err, data1) => { 169 | g.next(data1).value((err, data2) => { 170 | g.next(data2) 171 | }) 172 | }) 173 | 174 | } 175 | // fn6() 176 | 177 | function fn7() { 178 | // 自动流程管理的函数 179 | function run(generator) { 180 | const gen = generator() 181 | function next(err, data) { 182 | const result = gen.next(data) // 返回 { value: [Function], done: ... } 183 | if (result.done) { 184 | // result.done 表示是否结束 185 | return 186 | } 187 | result.value(next) // result.value 是一个 thunk 函数 188 | 189 | // 思考:如果 yield 后面不是 thunk 函数而是 promise 对象,上一句的 result.value(next) 就可以变为 result.value.then(next) 190 | } 191 | next() // 手动执行以启动第一次 next 192 | } 193 | 194 | const readFileThunk = thunkify(fs.readFile) 195 | const fileName1 = path.resolve(__dirname, '../data/data1.json') 196 | const fileName2 = path.resolve(__dirname, '../data/data2.json') 197 | const gen = function* () { 198 | const r1 = yield readFileThunk(fileName1) 199 | console.log(111, r1.toString()) 200 | const r2 = yield readFileThunk(fileName2) 201 | console.log(222, r2.toString()) 202 | } 203 | 204 | // run(gen) 205 | 206 | const c = co(gen) 207 | c.then(data => { 208 | console.log('结束') 209 | }) 210 | 211 | } 212 | // fn7() 213 | 214 | function fn8() { 215 | 216 | const fileName1 = path.resolve(__dirname, '../data/data1.json') 217 | const fileName2 = path.resolve(__dirname, '../data/data2.json') 218 | const readFilePromise = Q.denodeify(fs.readFile) 219 | 220 | const gen = function* () { 221 | const r1 = yield readFilePromise(fileName1) 222 | console.log(111, r1.toString()) 223 | const r2 = yield readFilePromise(fileName2) 224 | console.log(222, r2.toString()) 225 | } 226 | 227 | co(gen) 228 | } 229 | // fn8() 230 | 231 | 232 | function fn9() { 233 | 234 | let info = '' 235 | function* g1() { 236 | info += '1' 237 | yield* g2() 238 | info += '5' 239 | } 240 | function* g2() { 241 | info += '2' 242 | yield* g3() 243 | info += '4' 244 | } 245 | function* g3() { 246 | info += '3' 247 | } 248 | 249 | var g = g1() 250 | g.next() 251 | console.log(info) // 12345 252 | 253 | } 254 | // fn9() 255 | 256 | function fn10() { 257 | 258 | class MyKoa extends Object { 259 | constructor(props) { 260 | super(props); 261 | 262 | // 存储所有的中间件 263 | this.middlewares = [] 264 | } 265 | 266 | // 注入中间件 267 | use (generator) { 268 | this.middlewares.push(generator) 269 | } 270 | 271 | // 执行中间件 272 | listen () { 273 | this._run() 274 | } 275 | 276 | _run () { 277 | const ctx = this 278 | const middlewares = ctx.middlewares 279 | co(function* () { 280 | let prev = null 281 | let i = middlewares.length 282 | //从最后一个中间件到第一个中间件的顺序开始遍历 283 | while (i--) { 284 | //实际koa的ctx应该指向server的上下文,这里做了简化 285 | //prev 将前面一个中间件传递给当前中间件 286 | prev = middlewares[i].call(ctx, prev); 287 | } 288 | //执行第一个中间件 289 | yield prev; 290 | }) 291 | } 292 | } 293 | 294 | // 实验效果 295 | var app = new MyKoa(); 296 | app.use(function *(next){ 297 | this.body = '1'; 298 | yield next; 299 | this.body += '5'; 300 | console.log(this.body); 301 | }); 302 | app.use(function *(next){ 303 | this.body += '2'; 304 | yield next; 305 | this.body += '4'; 306 | }); 307 | app.use(function *(next){ 308 | this.body += '3'; 309 | }); 310 | app.listen(); 311 | 312 | } 313 | fn10() -------------------------------------------------------------------------------- /part5-async-await/01-async-await-in-es7.md: -------------------------------------------------------------------------------- 1 | # ES7 中引入 async-await 2 | 3 | 前面介绍完了`Generator`的异步处理,可以说是跌跌撞撞,经过各种基础介绍和封装,好容易出了一个比较简洁的异步处理方案,学习成本非常高————这显然不是我们想要的! 4 | 5 | 因此,还未发布的 ES7 就干脆自己参照`Generator`封装了一套异步处理方案————`async-await`。说是参照,其实可以理解为是`Generator`的语法糖! 6 | 7 | 本节示例代码参照[这里](./test.js) 8 | 9 | ## 本节内容概述 10 | 11 | - `Generator`和`async-await`的对比 12 | - 使用`async-await`的不同和好处 13 | - 接下来... 14 | 15 | ## `Generator`和`async-await`的对比 16 | 17 | 先来一段`Generator`处理异步的代码,前面已经介绍过了,看不明白的再获取接着看。 18 | 19 | ```javascript 20 | co(function* () { 21 | const r1 = yield readFilePromise('some1.json') 22 | console.log(r1) // 打印第 1 个文件内容 23 | const r2 = yield readFilePromise('some2.json') 24 | console.log(r2) // 打印第 2 个文件内容 25 | }) 26 | ``` 27 | 28 | 再来一段`async-await`的执行代码如下,两者做一个比较。 29 | 30 | ```javascript 31 | const readFilePromise = Q.denodeify(fs.readFile) 32 | 33 | // 定义 async 函数 34 | const readFileAsync = async function () { 35 | const f1 = await readFilePromise('data1.json') 36 | const f2 = await readFilePromise('data2.json') 37 | console.log('data1.json', f1.toString()) 38 | console.log('data2.json', f2.toString()) 39 | 40 | return 'done' // 先忽略,后面会讲到 41 | } 42 | // 执行 43 | const result = readFileAsync() 44 | ``` 45 | 46 | 从上面两端代码比较看来,`async function`代替了`function* `,`await`代替了`yield`,其他的再没有什么区别了。哦,还有,使用`async-await`时候不用再引用`co`这种第三方库了,直接执行即可。 47 | 48 | ## 使用`async-await`的不同和好处 49 | 50 | 第一,`await`后面不能再跟`thunk`函数,而必须跟一个`Promise`对象(因此,`Promise`才是异步的终极解决方案和未来)。跟其他类型的数据也OK,但是会直接同步执行,而不是异步。 51 | 52 | 第二,执行`const result = readFileAsync()`返回的是个`Promise`对象,而且上面代码中的`return 'done'`会直接被下面的`then`函数接收到 53 | 54 | ```javascript 55 | result.then(data => { 56 | console.log(data) // done 57 | }) 58 | ``` 59 | 60 | 第三,从代码的易读性来将,`async-await`更加易读简介,也更加符合代码的语意。而且还不用引用第三方库,也无需学习`Generator`那一堆东西,使用成本非常低。 61 | 62 | **因此,如果 ES7 正式发布了之后,强烈推荐使用`async-await`。但是现在尚未正式发布,从稳定性考虑,还是`Generator`更好一些。** 63 | 64 | ## 接下来... 65 | 66 | node `v7` 版本已经开始原生支持`async-await`了,不过 node 的目前稳定版本还是`v6`,尚不支持,怎么办?———— 当然是万能的`babel`!下一节就介绍。 67 | 68 | ## 求打赏 69 | 70 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 71 | 72 |  -------------------------------------------------------------------------------- /part5-async-await/02-use-in-node-v6.md: -------------------------------------------------------------------------------- 1 | # 如何在 nodejs `v6.x`版本中使用 async-await 2 | 3 | 本节介绍一下如何使用`babel`来让 node `v6` 版本也能运行`async-await` 4 | 5 | ## 本节内容概述 6 | 7 | - 安装必要的插件 8 | - 创建入口文件并执行 9 | 10 | ## 安装必要的插件 11 | 12 | 运行`npm i babel-core babel-plugin-transform-runtime babel-preset-es2015 babel-preset-stage-3 babel-runtime --save`安装一堆需要的插件。 13 | 14 | 然后在项目根目录创建`.babelrc`文件,文件内容编写为 15 | 16 | ```json 17 | { 18 | "presets": ["stage-3", "es2015"], 19 | "plugins": ["transform-runtime"] 20 | } 21 | ``` 22 | 23 | ## 创建入口文件并执行 24 | 25 | 加入你编写`async-await`的代码文件是`test.js`,那么你需要创建另一个文件,例如`test-entry.js`作为入口文件。入口文件内容编写为 26 | 27 | ```javascript 28 | require("babel-core/register"); 29 | require("./test.js"); 30 | ``` 31 | 32 | 然后直接运行`node test-entry.js`就可以了 33 | 34 | ## 求打赏 35 | 36 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 37 | 38 |  -------------------------------------------------------------------------------- /part5-async-await/test-entry.js: -------------------------------------------------------------------------------- 1 | require("babel-core/register"); 2 | require("./test.js"); -------------------------------------------------------------------------------- /part5-async-await/test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Q = require('q') 4 | 5 | function fn1() { 6 | 7 | const fileName1 = path.resolve(__dirname, '../data/data1.json') 8 | const fileName2 = path.resolve(__dirname, '../data/data2.json') 9 | const readFilePromise = Q.denodeify(fs.readFile) 10 | 11 | // 定义 async 函数 12 | const readFileAsync = async function () { 13 | const f1 = await readFilePromise(fileName1) 14 | const f2 = await readFilePromise(fileName2) 15 | console.log('data1.json', f1.toString()) 16 | console.log('data2.json', f2.toString()) 17 | 18 | return 'done' 19 | } 20 | // 执行 21 | const result = readFileAsync() 22 | 23 | result.then(data => { 24 | console.log(data) 25 | }) 26 | } 27 | fn1() -------------------------------------------------------------------------------- /part6-end/01-summary.md: -------------------------------------------------------------------------------- 1 | # 总结 2 | 3 | 一周左右的业余时间总结完,写完,也是累得我够呛。不算什么体力活,但是天天的坐在书桌旁写这些东西也是很考验一个人的定力,没点耐性是肯定不行的 ———— 这算是获奖感言吗 😂 4 | 5 | ## 本节内容概述 6 | 7 | - 基础知识不可忽略 8 | - 异步操作代码的变化 9 | - 写在最后 10 | 11 | ## 础知识不可忽略 12 | 13 | 这里的基础知识分为两部分,都不能忽略,都需要深入研究和思考 14 | 15 | - 什么是异步,异步的实现原理,event-loop,以及和事件绑定的关系。这些在最初介绍时,都讲过,不要看完了就忘记了; 16 | - 无论异步操作的写法如何变化,JS 还是单线程、异步执行的语言,`callback`一直都存在而且发挥作用,这个在此前的章节一直强调; 17 | 18 | ## 异步操作代码的变化 19 | 20 | 最后我们来感受一下,从一开始`callback`方式到后来的`async-await`方式,前前后后编写异步代码的变化。从变化中就可以体会到,确实越来越简洁,越来越易读。 21 | 22 | **`callback`方式** 23 | 24 | ```javascript 25 | fs.readFile('some1.json', (err, data) => { 26 | fs.readFile('some2.json', (err, data) => { 27 | fs.readFile('some3.json', (err, data) => { 28 | fs.readFile('some4.json', (err, data) => { 29 | 30 | }) 31 | }) 32 | }) 33 | }) 34 | ``` 35 | 36 | **`Promise`方式** 37 | 38 | ```javascript 39 | readFilePromise('some1.json').then(data => { 40 | return readFilePromise('some2.json') 41 | }).then(data => { 42 | return readFilePromise('some3.json') 43 | }).then(data => { 44 | return readFilePromise('some4.json') 45 | }) 46 | ``` 47 | 48 | **`Generator`方式** 49 | 50 | ```javascript 51 | co(function* () { 52 | const r1 = yield readFilePromise('some1.json') 53 | const r2 = yield readFilePromise('some2.json') 54 | const r3 = yield readFilePromise('some3.json') 55 | const r4 = yield readFilePromise('some4.json') 56 | }) 57 | ``` 58 | 59 | **`async-await`方式** 60 | 61 | ```javascript 62 | const readFileAsync = async function () { 63 | const f1 = await readFilePromise('data1.json') 64 | const f2 = await readFilePromise('data2.json') 65 | const f3 = await readFilePromise('data3.json') 66 | const f4 = await readFilePromise('data4.json') 67 | } 68 | ``` 69 | 70 | ## 写在最后 71 | 72 | 写到这里,也没啥可写的了,这里希望大家多多按照自己的思路来思考问题吧。最后,欢迎扫码转账给我打赏,哈哈! 73 | 74 | ## 求打赏 75 | 76 | 如果你看完了,感觉还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容 77 | 78 |  79 | --------------------------------------------------------------------------------