一、AMD和CMD規范(了解)
1.1傳統的前端開發多個js文件的關系
yuan.js中定義了一個函數
function mianji(r){ return 3.14 * r * r }
main.js文件中調用這個函數:
alert(mianji(10))
在頁面上按順序引入這兩個js文件:
<html> <head> <title>Document</title> </head> <body> </body> <script type="text/javascript" src="yuan.js"></script> <script type="text/javascript" src="main.js"></script> </html>
能運算,yuan.js中定義的函數,是window對象的屬性,所以main.js中能放同一個window全局函數,但每個js文件之間沒有“強的引用”關系,它們都是向window暴露自己的函數,其他人通過window周轉。
1.2 CMD規范
function mianji(r){ return 3.14 * r * r } //暴露 exports.mianji = mianji;
var yuan = require("./yuan.js"); console.log(yuan.mianji(10))
exports、require(),在瀏覽器會報錯,因為這兩個對象是Nodejs才有的,在客戶端瀏覽器不認識。
實際上在(比Nodejs還早),就有美國的程序員發明了Common.js,中國程序員阿里巴巴玉伯發明了sea.js,都解決了exports和require的識別問題。因為這個規范是Commonjs提出的,所以叫CMD規范,Common Module Definition,通用模塊定義。Nodejs也遵循CMD規范,但Nodejs並不是CMD規范的發明人。Nodejs是CommonJS規范的實現,后面學習的webpack也是以CommonJS的形式來書寫。
所以,Node.js的模塊系統,就是參照CommonJS規范實現的。
1.3 AMD規范-require.js(過時)
概述:比如前期學習js基礎時,經常引入其他的js文件,但是有一個文件之間的先后順序問題:
比如haha.js里面有xixi.js需要的某一個函數,那么引入haha.js文件必須在引入xixi.js文件之前;
因此有人,研究出一些規范,這些規范是規范引用js文件的先后順序。
require.js庫是AMD規范的代表。
AMD,Asynchronous Module Definition,即異步模塊定義。它是適用於瀏覽器端的一種模塊加載方式。從名字可知,AMD采用的是異步加載方式(js中最典型的異步例子就是ajax)。瀏覽器需要使用的js文件(第一次加載,忽略緩存)都存放在服務器端,從服務器端加載文件到瀏覽器是受網速等各種環境因素的影響的,如果采用同步加載方式,一旦js文件加載受阻,后續在排隊等待執行的js語句將執行出錯,會導致頁面的‘假死’,用戶無法使用一些交互。所以在瀏覽器端是無法使用CommonJS的模式的。而CommonJS是適用於服務器端的,著名的Node執行環境就是采用的CommonJS模式。
AMD和CMD規范和你開發什么業務無關,和用什么設計模式無關。
AMD、CMD是JS文件之間的互相引用關系,設計模式是js類和類之間的關系。
二、ES6中的模塊(CMD規范)
重點:在ES6中新增了js文件的暴露和引入的新寫法:(import和export)
2.1 import和export基本使用
之前學習過Nodejs,用了require、exports來引入和暴露。
W3C那些老頭想:Ryan Dahi那個小孩挺牛逼,在Nodejs發明了require()和exports挺好用,我們也制定一個標准吧!於是研究出了這樣的語法:
require() → import exports.*** → export module.exports → default
l 使用export const暴露函數,import {} from "./路徑"; 接收函數。
在yuan.js文件暴露
export const mianji = (r) => 3.14 * r * r; export const zhouchang = (r) => 2 * 3.14 * r;
在main.js文件接收:
import {mianji,zhouchang} from "./yuan.js"; //引入 console.log(mianji(10)); console.log(zhouchang(10));
注意兩個問題:
l 暴露時是什么名字,接收時一定是什么名字,比如暴露的是mianji,接收就叫mianji。
l 路徑必須“./”開頭
W3C新發明的CMD規范(export和import)關鍵字,到今天為止,你會發現如今的瀏覽器和Node平台都不支持。
Google黑板報說2018年將原生支持(export和import)
那現在不支持怎么辦?
webpack應運而生,webpack是一個很智能的文件打包器,你只需告訴它主入口文件是誰,它就能順着import鏈進行打包,最后能合並成為一個新的js文件,將不再有import和export的語法。
webpack打包命令
webpack main.js all.js
webpack進行了3個事情: 1)從main.js出發,順着import鏈尋找每一個被引用的js文件 2)將每一個js文件智能合並打包。比如一個export暴露的函數沒有被使用,將不會被打包; 3)去import、export化,也就是說all.js是IE8都兼容的。
webpack是一個構建工具,在html中只需要引入all.js一個文件即可
2.2命名空間
比如增加一個fang.js也向外暴露兩個函數,也叫mianji和zhouchang
export const mianji = (a)=> 3.14 * a * a;
export const zhouchang = (a)=> 3.14 * a * 4;
此時主入口文件:
import {mianji,zhouchang} from "./yuan.js"; import {mianji,zhouchang} from "./fang.js"; console.log(mianji(15)) console.log(zhouchang(15))
會報錯,因為函數名重復了:
解決方法就是命名空間:用import * as fang from "./路徑"; 來接收函數
import * as yuan from "./yuan.js"; import * as fang from "./fang.js"; console.log(yuan.mianji(10)) console.log(fang.mianji(10))
此時函數必須通過yuan打點,或fang打點調用,因為接收到的是一個JSON。
也就是說import * as fang from "./文件";的語法,可以有效避免函數的名字沖突。
注意:import * as的這個名字,必須和文件名相同。
2.3默認暴露
如果js文件中是一個類(構造函數),此時不希望有命名空間,用默認暴露:
export default
相當於nodejs中的module.exports
比如寫一個文件叫People.js這個文件向外暴露一個類:
export default class People { constructor(name,age,sex){ this.name = name; this.age = age; this.sex = sex; } change(){ alert(`啦啦啦!我是賣報的${this.name},今年${this.age}`) } }
在主入口文件,就不用{}引用這個類:
import People from "./People.js"; //類叫什么名字,import后就叫什么名字
var xiaoming = new People("小明",12,"男");
xiaoming.change();
一個文件可以有多個普通暴露,但是只能有一個默認暴露
// 默認暴露 export default class People { constructor(name,age,sex){ this.name = name; this.age = age; this.sex = sex; } change(){ alert(`啦啦啦!我是賣報的${this.name},今年${this.age}`) } } //以下是普通暴露 export const a = 100; export const b = 200; export const f1 = function(){ alert("我是普通的暴露函數f1") }; export const f2 = function(){ alert("我是普通的暴露函數f2") };
注意:引入時,接收默認的暴露,不能加{},接收普通暴露的加{}
// import People from "./People.js"; //類叫什么名字,import后就叫什么名字 import People, {a,b,f1,f2} from "./People.js"; console.log(a) console.log(b) f1(); f2(); var xiaoming = new People("小明",12,"男"); xiaoming.change();
暴露模塊:
普通暴露 |
export const |
默認暴露 |
export default |
引用模塊:
普通引用 |
import {mianji,zhouchang} "./yuan.js" import * as yuan "./yuan.js" |
默認引用(類的引用) |
import People from "./People.js" |
默認暴露和普通暴露一起引用 |
import People,{a,b,f1,f2} from "./People.js"; |
三、webpack(模塊打包器)
3.1 webpack的簡介和安裝
官網:https://doc.webpack-china.org/
webpack 是一個模塊打包器。它的主要目標是將 JavaScript 文件打包在一起,打包后的文件用於在瀏覽器中使用,但它也能夠勝任轉換(transform)、打包(bundle)或包裹(package)任何資源(resource or asset)
安裝webpack工作流工具:
npm install -g webpack@3.10.0
webpack有4代版本,優點是:打包合並文件速度快,快了90%。
3.2 webpack基本使用
在項目文件夾中運行CMD命令,webpack打包命令:
webpack main.js all.js
main.js是主入口文件
all.js 是出口文件(被打包的)
沒有yuan.js的事情,因為在main.js中import {mianji} from "./yuan.js"; 所以webpack就鏈着import尋找yuan.js進行打包合並。
3.3 webpack.config.js文件
可以在項目中的根目錄寫webpack.config.js文件,來指導webpack工作。
就如同.babelrc文件一樣,指導babel工作。
webpack.config.js文件的配置:
https://webpack.docschina.org/configuration/
先配置一個文件,以后用就行了:
const path = require('path'); module.exports = { //程序的入口文件 entry:"./www/app/main.js", //程序的出口(打包的文件) output:{ //打包文件輸出的路徑 path: path.resolve(__dirname, "./www/dist"), //打包文件的名稱 filename: 'all.js' }, //自動監聽文件的變化 watch:true }
webpack.config.js文件配置好后,就可以使用webpack命令打包了。
3.4 babel-loader
webpack只是幫我們智能合並打包文件,但是沒有翻譯ES6、7、8語法為ES5語法。
所以現在的任務是,在webpack打包的過程中,對每一個js文件用babel進行翻譯。
webpack提供了babel-loader功能,可以讓webpack在打包文件時,順便的翻譯ES678語法。
安裝依賴:
npm install --save-dev babel-core@6 npm install --save-dev babel-loader@7 npm install --save-dev babel-preset-es2015 npm install --save-dev babel-preset-es2016
babel-core 是babel的核心
babel-loader 是babel和webpack一起使用的橋梁
babel-preset-es2015 是翻譯的字典
注意:babel-loader默認安裝的是8代版本,但babel-core默認安裝的是6,所以版本不兼容,會報錯。解決方法是babel-loader降級安裝7代,或者babel-core升級安裝為7代。
安裝完3個依賴,並配置好webpack.config.js文件后,webpack不但能打包,還能翻譯了。
webpack.config.js文件配置
const path = require('path'); module.exports = { //程序的入口文件 entry:"./www/app/main.js", //程序的出口(打包的文件) output:{ //打包文件輸出的路徑 path: path.resolve(__dirname, "./www/dist"), //打包文件的名稱 filename: 'all.js' }, //監聽文件的變化(自動打包) watch:true, //配置webpack模塊插件 module:{ //關於模塊的配置規則 rules:[{ // 模塊規則(配置 loader、解析器等選項) test: /\.js$/, //解析的時候匹配js文件 loader:"babel-loader", //翻譯什么文件夾中的文件 include: [ path.resolve(__dirname, "www/app")], //不翻譯什么文件夾中的文件 exclude: [ path.resolve(__dirname, "node_modules")], //配置翻譯語法 options:{ presets:["es2015","es2016"] } }] } }
之前寫的.babelrc文件,現在webpack在webpack.config.js文件中提供了,所以可以刪除了。
四、Promise對象(重點)
Promise對象是關於解決異步的書寫美觀問題。
4.1復習同步和異步
當CPU面對一個長時間、設備處理的諸如文件讀取、磁盤I/O、數據庫查詢、數據庫的寫入等等操作的時候,此時有兩種模式:
① 同步模式:死等這個設備處理完成,此時CPU自己被阻塞
② 異步模式:不等這個設備處理完成,而是先執行后面的語句,等這個設備處理完成的時候,執行回調函數。
比如下面的代碼,讓CPU先執行一段計算,然后命令硬盤異步讀取文件,讀取的過程中不阻塞CPU,CPU提前執行后面的計算語句,等硬盤讀取完畢之后,執行回調函數。
var fs = require("fs"); for(var i = 0; i < 100; i++){ i++; } console.log(i); //異步讀取文件 fs.readFile("./txt/1.txt", (err,data) =>{ console.log(data.toString()); }); for(var i = 0; i < 100; i++){ i++; } console.log(i);
4.2異步回調語法不美觀
先讀1.txt,然后讀2.txt,最后讀3.txt
fs.readFile("./data/1.txt", (err,data) =>{ console.log(data.toString()); fs.readFile("./data/2.txt", (err,data) =>{ console.log(data.toString()); fs.readFile("./data/3.txt", (err,data) =>{ console.log(data.toString()); }); }); });
寫Ajax請,順序讀取3個api地址,分別是1.json、2.json、3.json
$.get("data/1.json", function(data1){ $.get("data/2.json", function(data2){ $.get("data/3.json", function(data3){ console.log(data1,data2,data3) }) }) })
通過以上兩個案例,我們遇見了回調黑洞問題(一層嵌套一層)
回調就是當有多個異步的事情,要排隊進行的時候,此時必須回調嵌套回調。
之所以回調函數,因為fs.readFile()和ajax是異步的,需要有回調函數告訴主程序它讀取完畢了。
那么有沒有方法,讓異步語句形式變的更好看?從嵌套寫法,變為火車寫法,而不是一層嵌套一層的縮進?
ES6推出了Promise對象,就是優雅的解決回調函數的黑洞問題。
http://es6.ruanyifeng.com/#docs/promise
4.3 ES6的Promise對象
ES6 規定,Promise對象是一個構造函數,用來生成Promise實例。
下面代碼創造了一個Promise實例。
const promise = new Promise(function(resolve, reject){ if(/* 異步操作成功 */){ resolve(value); }else{ reject(error); } });
Promise構造函數接受一個函數作為參數,該函數的兩個參數分別是resolve和reject。它們是兩個函數,由 JavaScript 引擎提供,不用自己部署。
resolve 表示成功執行的回調函數
reject 表示失敗執行的回調函數
封裝一個函數,叫讀文件duwenjian(),這個函數接收url作為路徑參數:
var fs = require("fs"); function duwenjian(url){ return new Promise(function(resolve,reject){ fs.readFile(url, (err,data)=>{ if(err){ reject(); //失敗執行 return; } resolve(data.toString()); //成功執行 }) }) } duwenjian("./data/1.txt").then((data)=>{ console.log(data); return duwenjian("./data/2.txt"); }).then((data)=>{ console.log(data); return duwenjian("./data/3.txt"); }).then((data)=>{ console.log(data); })
回調黑洞問題解決了,將原來的“嵌套模式”變為“隊列模式”
可以認為這是一根語法糖,因為沒有改變原理,只改變了寫法。
Promise實例生成以后,可以用then方法分別指定resolved狀態和rejected狀態的回調函數。
then方法可以接受兩個回調函數作為參數。第一個回調函數是Promise對象的狀態變為resolved時調用,第二個回調函數是Promise對象的狀態變為rejected時調用。其中,第二個函數是可選的,不一定要提供。這兩個函數都接受Promise對象傳出的值作為參數。
紅色表示成功執行的回調函數,藍色表示失敗執行的回調函數:
duwenjian("./data/1.txt").then((data)=>{ console.log(data); },()=>{ console.log("讀取失敗") })
then表示然后。
這個函數返回一個Promise對象,這個對象是構造函數,ES6新增的。
Promise一定是某個函數的唯一返回值,Promise自己調用沒有任何意義,必須當做一個函數的返回值才有意義!本例中,Promise就是duwenjian函數的返回值!!
這個函數中,一定是一個異步語句,我們的案例是fs.readFile()
異步語句的成功,要將數據通過resolve(數據)傳出去,這個數據將成為then里面的data。
異步語句的失敗,要將錯誤信息通過reject(err)傳出去,這個數據將成then里面的第二個data。
Promise的實例擁有then()方法,也就是說,then方法是定義在原型對象。
在Ajax中使用promise對象,按順序讀取三個文件:
為了解決黑洞問題,此時用promise來解決:
function duwenjian(url){ return new Promise(function(resolve,reject){ $.ajax({ url : url, success : function(data){ resolve(data); }, error : function(){ reject(); } }) }) } $("button").click(function(event) { duwenjian("data/1.json").then(function(data){ console.log(data); return duwenjian("data/2.json"); }).then((data)=>{ console.log(data); return duwenjian("data/3.json"); }).then((data)=>{ console.log(data); }) });
promise表示承諾、契約:當異步完畢之后,一定會執行resolve,從而then被執行,里面能夠得到值。
4.4 ES7的async和await
ES6中的Promise對象,解決了回調黑洞,但實際上又產生了火車黑洞,then()過多不好看。
所以ES7中提出了async/await可以讓我們用同步的方式寫異步,不寫then了。
async表示異步,await表示異步等待。
用寫同步的方法,寫異步函數:
注意:
1)必須寫那個return Promise對象的函數;
2)await不能裸用,必須寫在async字樣的函數里;
3)所有異步語句前都要加await,但是后面的函數必須return Promise實例的函數。
function duwenjian(url){ return new Promise(function(resolve,reject){ $.ajax({ url : url, success : function(data){ resolve(data); }, error : function(){ reject(); } }) }) } async function main(){ var data1 = await duwenjian("data/1.json").then(data=>data); var data2 = await duwenjian("data/2.json").then(data=>data); var data3 = await duwenjian("data/3.json").then(data=>data); console.log(data1) console.log(data2) console.log(data3) }; main();
Nodejs讀文件:

var fs = require("fs"); function duwenjian(url){ return new Promise(function(resolve,reject){ fs.readFile(url, (err,data)=>{ if(err){ reject(); //失敗執行 return; } resolve(data.toString()); //成功執行 }) }) } async function main(){ var data1 = await duwenjian("data/1.txt").then(data=>data); var data2 = await duwenjian("data/2.txt").then(data=>data); var data3 = await duwenjian("data/3.txt").then(data=>data); console.log(data1) console.log(data2) console.log(data3) }; main();
任何一個函數都可以在function之前加async這個關鍵字,表示這個函數中出現了await。
await后面只能跟着一個返回promise對象的實例的函數的調用,then里面的函數的返回值將自動被等號左邊接收
使用async/await的哲學:用寫同步語句的感覺,去寫異步語句。
async寫在function這個之前,await寫在異步語句之前
await表示等待后面的操作執行完畢,再執行下一行代碼。
4.5 ES8的fetch()
ES8中繼續提出了fetch方法,這個方法發出ajax請求,返回Promise的實例
fetch()函數是ES8中提出的新的特性,它:
1)能夠發出Ajax請求,並且請求機理不是xhr,而是新的機理;
2)天生可以跨域,但是需要設置頭,服務器可以寫腳本識別這個頭部判斷是否應該拒絕它;
3) fetch()返回Promise對象,所以可以用then來跟着,then里面的第一個函數就是resolve,這個resolve的返回值將自動被await等號左邊的變量的接收。
4)不要忘記寫async和await。
async function main(){ var data1 = await fetch("data/1.json").then(data=>data.json()); var data2 = await fetch("data/2.json").then(data=>data.json()); var data3 = await fetch("data/3.json").then(data=>data.json()); console.log(data1) console.log(data2) console.log(data3) }; main();
可以不用jQuery了
babel沒有任何插件可以翻譯fetch為傳統ajax。
fetch只能用在瀏覽器環境,不能用在Nodejs環境。
經典面試題:
Promise是什么:
傳統的ajax或者fs寫異步的時候,會遇見回調黑洞,回調套用回調,不美觀。所以ES6中提出了Promise對象,此時可以封裝一個函數,返回Promise的實例,new Promise的時候要傳入一個函數,這個函數里面寫具體的異步,當異步成功調用resolve(),異步失敗調用reject()。此時在外部,我們就可以用.then().then()火車的形式,來實現異步、異步的順序處理。
async/await是什么:
Promise雖然解決了回調黑洞,但是變成了火車的黑洞。我們可以給函數加上async的前綴,里面的異步語句加上await前綴,這樣就可以用類似於同步的寫法寫異步。等號左側就能有接收值,語句自然一條一條的執行了。
fetch是什么:
fetch就是ajax,但是返回的是promise的實例,可以直接then,而不需要再次封裝一個什么函數了。
五、Generator函數
產生器。
我們沒有任何理由封裝它,都是別人的框架提供,比如react-saga、dva等。
解釋:
l如同async寫在函數前面一樣,*要寫在function函數后面
l如同await寫在語句前面一樣,yield要寫在語句前面,但后面不一定是異步語句,表示“產出”
l函數可以像被打了斷點,逐步執行,到了yield就停止執行。
l函數不能直接調用執行,直接調用返回一個對象,比如下面的hw,然后hw必須用next()調用函數。
function* helloWorldGenerator() { console.log("哈哈"); yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator(); console.log(hw.next()); console.log(hw.next()); console.log(hw.next()); console.log(hw.next()); console.log(hw.next());