課程介紹
近些年,瀏覽器的功能越來越強大,漸漸得成為了復雜應用和圖形的平臺。同時,現有大多數瀏覽器實現了對 WebGL 的支持,但要直接使用 WebGL 相關接口進行開發,則需要學習復雜的著色器語言,且開發周期長,不利于項目的快速開發。
面對這種情況,Three.js 應運而生,它不但對 WebGL 進行了封裝,將復雜的接口簡單化,而且基于面向對象思維,將數據結構對象化,非常方便我們開發。Three.js 的發展十分迅速,然而配套的學習材料卻比較匱乏,于是便有了當前的這個課程。
本課程作為入門課程,不會深入做源碼解析,主要協助初學者了解 Three.js 的數據結構,基礎 API 以及相關輔助插件的使用。幫助初學者達到快速入門的目的。
本課程共包含四大部分。
第一部分(第01-02課),入門前概述,帶你初步認識 Three.js、框架選擇標準、開發工具,源碼獲取,實現一個“Hello World”輔助工具。
第二部分(第03-08課),基礎功能篇,主要包括 Object3D、Scene、Mesh、Group、Geometry、Materials、Lights、Cameras、粒子等相關功能的介紹。
第三部分(第09-15課),進階篇,主要包括 Controls、Loaders、Animation、Tween、核心對象,與場景之間的交互以及性能優化介紹。
第四部分(第16課),實戰篇,帶大家利用所學知識實現一個 3D 小案例。
鄭世強,現就職于上海某網絡公司擔任前端工程師,CSDN 博客作者,長期活躍于各大論壇,擅長前端開發、WEBGL 開發。
WebGL(Web 圖形庫)是一種 JavaScript API,用于在任何兼容的 Web 瀏覽器中呈現交互式 3D 和 2D 圖形,而無需使用插件。WebGL 通過引入一個與 OpenGL ES 2.0 緊密相符合的 API,可以在 HTML5 <canvas> 元素中使用(簡介引自 MDN)。
以我的理解,WebGL 給我們提供了一系列的圖形接口,能夠讓我們通過 JavaScript 去使用 GPU 來進行瀏覽器圖形渲染的工具。
Three.js 是一款 webGL 框架,由于其易用性被廣泛應用。Three.js 在 WebGL 的 API 接口基礎上,又進行的一層封裝。它是由居住在西班牙巴塞羅那的程序員 Ricardo Cabbello Miguel 所開發,他更為人知的網名是 Mr.doob。
Three.js 以簡單、直觀的方式封裝了 3D 圖形編程中常用的對象。Three.js 在開發中使用了很多圖形引擎的高級技巧,極大地提高了性能。另外,由于內置了很多常用對象和極易上手的工具,Three.js 的功能也非常強大。最后,Three.js 還是完全開源的,你可以在 GitHub 上找到它的源代碼,并且有很多人貢獻代碼,幫助 Mr.doob 一起維護這個框架。
WebGL 原生 API 是一種非常低級的接口,而且還需要一些數學和圖形學的相關技術。對于沒有相關基礎的人來說,入門真的很難,Three.js 將入門的門檻降低了一大截,對 WebGL 進行封裝,簡化我們創建三維動畫場景的過程。只要你有一定的 JavaScript 基礎,有一定的前端經驗,我堅信,用不了多長時間,三維制作會變得很簡單。
用最簡單的一句話概括:WebGL 和 Three.js 的關系,相當于 JavaScript 和 jQuery 的關系。
Three.js 作為 WebGL 框架中的佼佼者,由于它的易用性和擴展性,使得它能夠滿足大部分的開發需求,Three.js 的具體功能如下:
Three.js 掩蓋了 3D 渲染的細節:Three.js 將 WebGL 原生 API 的細節抽象化,將 3D 場景拆解為網格、材質和光源(即它內置了圖形編程常用的一些對象種類)。
面向對象:開發者可以使用上層的 JavaScript 對象,而不是僅僅調用 JavaScript 函數。
功能非常豐富:Three.js 除封裝了 WebGL 原始 API 之外,Three.js 還包含了許多實用的內置對象,可以方便地應用于游戲開發、動畫制作、幻燈片制作、髙分辨率模型和一些特殊的視覺效果制作。
速度很快:Three.js 采用了 3D 圖形最佳實踐來保證在不失可用性的前提下,保持極高的性能。
支持交互:WebGL 本身并不提供拾?。≒icking)功能(即是否知道鼠標正處于某個物體上)。而 Three.js 則固化了拾取支持,這就使得你可以輕松為你的應用添加交互功能。
包含數學庫:Three.js 擁有一個強大易用的數學庫,你可以在其中進行矩陣、投影和矢量運算。
內置文件格式支持:你可以使用流行的 3D 建模軟件導出文本格式的文件,然后使用 Three.js 加載,也可以使用 Three.js 自己的 JSON 格式或二進制格式。
擴展性很強:為 Three.js 添加新的特性或進行自定義優化是很容易的事情。如果你需要某個特殊的數據結構,那么只需要封裝到 Three.js 即可。
支持HTML5 Canvas:Three.js 不但支持 WebGL,而且還支持使用 Canvas2D、Css3D 和 SVG 進行渲染。在未兼容 WebGL 的環境中可以回退到其它的解決方案。
雖然 Three.js 的優勢很大,但是它也有它的不足之處:
官網文檔非常粗糙,對于新手極度不友好。
國內的相關資源匱乏。
Three.js 所有的資料都是以英文格式存在,對國內的朋友來說又提高了門檻。
Three.js 不是游戲引擎,一些游戲相關的功能沒有封裝在里面,如果需要相關的功能需要進行二次開發。
隨著 WebGL 的迅速發展,相關的 WebGL 庫也豐富起來,接下來介紹幾個比較火的 WebGL 庫。
Babylon.JS 是最好的 JavaScript 3D 游戲引擎,它能創建專業級三維游戲。主要以游戲開發和易用性為主。與 Three.js 之間的對比:
Three.js 比較全面,而 Babylon.js 專注于游戲方面。
Babylon.js 提供了對碰撞檢測、場景重力、面向游戲的照相機,Three.js 本身不自帶,需要依靠引入插件實現。
對于 WebGL 的封裝,雙方做得各有千秋,Three.js 淺一些,好處是易于擴展,易于向更底層學習;Babylon.js 深一些,好處是易用擴展難度大一些。
Three.js 的發展依靠社區推動,出來的比較早,發展比較成熟,Babylon.js 由微軟公司在2013推出,文檔和社區都比較健全,國內還不怎么火。
PlayCanvas 是一個基于 WebGL 游戲引擎的企業級開源 JavaScript 框架,它有許多的開發工具能幫你快速創建 3D 游戲。與 Three.js 之間的對比:
PlayCanvas 的優勢在于它有云端的在線可視化編輯工具。
PlayCanvas 的擴展性不如 Three.js。
最主要是 PlayCanvas 不完全開源,還商業付費。
Cesium 是國外一個基于 JavaScript 編寫的使用 WebGL 的地圖引擎,支持 3D、2D、2.5D 形式的地圖展示,可以自行繪制圖形,高亮區域。與 Three.js 對比:
Cesium 是一個地圖引擎,專注于 Gis,相關項目推薦使用它,其它項目還是算了。
至于庫的擴展,其它的配套插件,以及周邊的資源都不及Three.js。
通過以上信息我們發現,Three.js 在其庫的擴展性,易用性以及功能方面有很好的優勢。學習 Three.js 入門 3D 開發不但門檻低,而且學習曲線不會太陡,即使以后轉向 WebGL 原生開發,也能通過 Three.js 學習到很多有用的知識。
現在最火的微信小游戲跳一跳也是在 Three.js 的基礎上開發出來的。所以,Three.js 是我們必須要學的 WebGL 框架。
Three.js 可以使用 WebGL 在所有現代瀏覽器上渲染場景。對于舊版瀏覽器,尤其是 Internet Explorer 10 及更低版本,您可能需要回退到其他渲染器(CSS2DRenderer、CSS3DRenderer、SVGRenderer、CanvasRenderer)。
注意:如果您不需要支持這些舊版瀏覽器,則不推薦使用其他渲染器,因為它們速度較慢并且支持的功能比 WebGLRenderer 更少。
即可下載當前版本的代碼及相關案例,文件下載解壓后是這樣的:
其中相關文件夾的內容是:
build:里面含有 Three.js 構建出來的 JavaScript 文件,可以直接引入使用,并有壓縮版;
docs:Three.js 的官方文檔;
editor:Three.js 的一個網頁版的模型編輯器;
examples:Three.js 的官方案例,如果全都學會,必將成為大神;
src:這里面放置的全是編譯 Three.js 的源文件;
test:一些官方測試代碼,我們一般用不到;
utils:一些相關插件;
其他:開發環境搭建、開發所需要的文件,如果不對 Three.js 進行二次開發,用不到。
還有第三種,就是直接去 GitHub 上下載源碼,和在官網上下載的代碼一樣。
<!DOCTYPE html><html><head> <meta charset=utf-8> <title>我的第一個Three.js案例</title> <style> body { margin: 0; } canvas { width: 100%; height: 100%; display: block; } </style></head><body onload="init()"><script src="https://cdn.bootcss.com/three.js/92/three.js"></script><script> //聲明一些全局變量 var renderer, camera, scene, geometry, material, mesh; //初始化渲染器 function initRenderer() { renderer = new THREE.WebGLRenderer(); //實例化渲染器 renderer.setSize(window.innerWidth, window.innerHeight); //設置寬和高 document.body.appendChild(renderer.domElement); //添加到dom } //初始化場景 function initScene() { scene = new THREE.Scene(); //實例化場景 } //初始化相機 function initCamera() { camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); //實例化相機 camera.position.set(0, 0, 15); } //創建模型 function initMesh() { geometry = new THREE.BoxGeometry( 2, 2, 2 ); //創建幾何體 material = new THREE.MeshNormalMaterial(); //創建材質 mesh = new THREE.Mesh( geometry, material ); //創建網格 scene.add( mesh ); //將網格添加到場景 } //運行動畫 function animate() { requestAnimationFrame(animate); //循環調用函數 mesh.rotation.x += 0.01; //每幀網格模型的沿x軸旋轉0.01弧度 mesh.rotation.y += 0.02; //每幀網格模型的沿y軸旋轉0.02弧度 renderer.render( scene, camera ); //渲染界面 } //初始化函數,頁面加載完成是調用 function init() { initRenderer(); initScene(); initCamera(); initMesh(); animate(); }</script></body></html>
請將上面的代碼復制到 HTML 文件中,然后使用瀏覽器打開,我們就會發現下面的效果:
————————————————
版權聲明:本文為CSDN博主「GitChat的博客」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/valada/java/article/details/80871701
用例
讓我們從介紹幾種不同的填充用例開始。
標簽和值
假設你在同一行上有標簽和值,例如 name:zhangsan 和 Phone Number:(555)-555-1234。如果把他們放在一起看起來會有點奇怪,會是這樣:
Name: zhangsan
Phone Number: (555)-555-1234
你可能想要這個。
Name: zhangsan
Phone Number: (555)555-1234
或這個...
Name: zhangsan
Phone Number: (555)555-1234
金額
在中國,顯示價格時通常顯示兩位數的角、分。所以代替這個...
¥10.1
你會想要這個。
¥10.01
日期
對于日期,日期和月份都需要2位數字。所以代替這個...
2020-5-4
你會想要這個。
2020-05-04
時間
與上面的日期類似,對于計時器,你需要2位數字表示秒,3位數字表示毫秒。所以代替這個...
1:1
你會想要這個。
01:001
padstart()
讓我們從 padStart() 以及標簽和值示例開始。假設我們希望標簽彼此正確對齊,以使值在同一位置開始。
Name: zhangsan
Phone Number: (555)555-1234
由于 Phone Number 是兩個標簽中較長的一個,因此我們要在 Name 標簽的開頭加上空格。為了將來的需要,我們不要把它專門填充到電話號碼的長度,我們把它填充到長一點,比如說20個字符。這樣一來,如果你在未來使用較長的標簽,這一招仍然有效。
在填充之前,這是用于顯示此信息的入門代碼。
const label1 = "Name";
const label2 = "Phone Number";
const name = "zhangsan"
const phoneNumber = "(555)-555-1234";
console.log(label1 + ": " + name);
console.log(label2 + ": " + phoneNumber);
//Name: zhangsan
//Phone Number: (555)-555-1234
現在,讓我們填充第一個標簽。要調用 padStart(),你需要傳遞兩個參數:一個用于填充字符串的目標長度,另一個用于你希望填充的字符。在這種情況下,我們希望長度為20,而填充字符為空格。
const label1 = "Name";
const label2 = "Phone Number";
const name = "zhangsan"
const phoneNumber = "(555)-555-1234";
console.log(label1.padStart(20, " ") + ": " + name);
console.log(label2 + ": " + phoneNumber);
// Name: zhangsan
////Phone Number: (555)-555-1234
現在填充第二行。
const label1 = "Name";
const label2 = "Phone Number";
const name = "zhangsan"
const phoneNumber = "(555)-555-1234";
console.log(label1.padStart(20, " ") + ": " + name);
console.log(label2.padStart(20, " ") + ": " + phoneNumber);
// Name: zhangsan
//// Phone Number: (555)-555-1234
padEnd()
對于相同的標簽和值示例,讓我們更改填充標簽的方式。讓我們將標簽向左對齊,以便在末尾添加填充。
初始代碼
const label1 = "Name";
const label2 = "Phone Number";
const name = "zhangsan"
const phoneNumber = "(555)-555-1234";
console.log(label1 + ": " + name);
console.log(label2 + ": " + phoneNumber);
//Name: zhangsan
//Phone Number: (555)-555-1234
現在,讓我們填充第一個標簽,與我們之前所做的類似,但有兩個小區別?,F在,我們使用 padEnd() 而不是padStart(),并且需要在填充之前將冒號與標簽連接起來,這樣我們就能確保冒號在正確的位置。
const label1 = "Name";
const label2 = "Phone Number";
const name = "zhangsan"
const phoneNumber = "(555)-555-1234";
console.log((label1 + ': ').padEnd(20, ' ') + name);
console.log(label2 + ": " + phoneNumber);
//Name: zhangsan
//Phone Number: (555)-555-1234
現在兩行都已填充。
const label1 = "Name";
const label2 = "Phone Number";
const name = "zhangsan"
const phoneNumber = "(555)-555-1234";
console.log((label1 + ': ').padEnd(20, ' ') + name);
console.log((label2 + ': ').padEnd(20, ' ') + phoneNumber);
//Name: zhangsan
//Phone Number: (555)-555-1234
數字(價格、日期、計時器等)呢?
padding函數是專門針對字符串而不是數字的,所以,我們需要先將數字轉換為字符串。
價格
讓我們看一下顯示價格的初始代碼。
const rmb = 10;
const cents = 1;
console.log("¥" + rmb + "." + cents); //¥10.1
要填充分,我們需要先將其轉換為字符串,然后調用 padStart() 函數,指定長度為1且填充字符為'0';
const rmb = 10;
const cents = 1;
console.log("¥" + rmb + "." + cents.toString().padStart(2,0)); //¥10.01
日期
這是顯示日期的初始代碼。
const month = 2;
const year = 2020;
console.log(year + "-" + month); //2020-2
現在,讓我們填充月份以確保它是兩位數。
const month = 2;
const year = 2020;
console.log(year + "-" + month.toString().padStart(2,"0")); // 2020-02
計時器
最后是我們的計時器,我們要格式化兩個不同的數字,即秒和毫秒。盡管有相同的原則。這是初始代碼。
const seconds = 1;
const ms = 1;
console.log(seconds + ":" + ms); //1:1
現在要填充,我將在單獨的行上進行填充,以便于閱讀。
const seconds = 1;
const formattedSeconds = seconds.toString().padStart(2,0);
const ms = 1;
const formattedMs = ms.toString().padStart(3,0);
console.log(formattedSeconds + ":" + formattedMs); // 01:001
最后
雖然編寫自己的padding函數并不難,但既然已經內置在JavaScript中,為什么還要自己去做呢?有很多有趣的函數已經內置了。在你自己構建一些東西之前,可能值得先快速搜索一下。
數組——最簡單的內存數據結構
數組存儲一系列同一種數據類型的值。( Javascript 中不存在這種限制)
對數據的隨機訪問,數組是更好的選擇,否則幾乎可以完全用 「鏈表」 來代替
在很多編程語言中,數組的長度是固定的,當數組被填滿時,再要加入新元素就很困難。Javascript 中數組不存在這個問題。
但是 Javascript 中的數組被實現成了對象,與其他語言相比,效率低下。
數組的一些核心方法
方法 描述
push 方法將一個或多個元素添加到數組的末尾,并返回該數組的新長度。(改變原數組)
pop 方法從數組中刪除最后一個元素,并返回該元素的值。(改變原數組)
shift 方法從數組中刪除第一個元素,并返回該元素的值,如果數組為空則返回 undefined 。(改變原數組)
unshift 將一個或多個元素添加到數組的開頭,并返回該數組的新長度(改變原數組)
concat 連接兩個或多個數組,并返回結果(返回一個新數組,不影響原有的數組。)
every 對數組中的每個元素運行給定函數,如果該函數對每個元素都返回 true,則返回 true。若為一個空數組,,始終返回 true。 (不會改變原數組,[].every(callback)始終返回 true)
some 對數組中的每個元素運行給定函數,如果任一元素返回 true,則返回 true。若為一個空數組,,始終返回 false。(不會改變原數組,)
forEach 對數組中的每個元素運行給定函數。這個方法沒有返回值,沒有辦法中止或者跳出 forEach() 循環,除了拋出一個異常(foreach不直接改變原數組,但原數組可能會被 callback 函數該改變。)
map 對數組中的每個元素運行給定函數,返回每次函數調用的結果組成的數組(map不直接改變原數組,但原數組可能會被 callback 函數該改變。)
sort 按照Unicode位點對數組排序,支持傳入指定排序方法的函數作為參數(改變原數組)
reverse 方法將數組中元素的位置顛倒,并返回該數組(改變原數組)
join 將所有的數組元素連接成一個字符串
indexOf 返回第一個與給定參數相等的數組元素的索引,沒有找到則返回 -1
lastIndexOf 返回在數組中搜索到的與給定參數相等的元素的索引里最大的值,沒有找到則返回 -1
slice 傳入索引值,將數組里對應索引范圍內的元素(淺復制原數組中的元素)作為新數組返回(原始數組不會被改變)
splice 刪除或替換現有元素或者原地添加新的元素來修改數組,并以數組形式返回被修改的內容(改變原數組)
toString 將數組作為字符串返回
valueOf 和 toString 類似,將數組作為字符串返回
棧是一種遵循后進先出(LIFO)原則的有序集合,新添加或待刪除的元素都保存在棧的同一端,稱作棧頂,另一端就叫棧底。在棧里,新元素都靠近棧頂,舊元素都接近棧底。
通俗來講,就是你向一個桶里放書本或者盤子,你要想取出最下面的書或者盤子,你必須要先把上面的都先取出來。
棧也被用在編程語言的編譯器和內存中保存變量、方法調用等,也被用于瀏覽器歷史記錄 (瀏覽器的返回按鈕)。
代碼實現
// 封裝棧隊列是遵循先進先出(FIFO,也稱為先來先服務)原則的一組有序的項。隊列在尾部添加新
元素,并從頂部移除元素。添加的元素必須排在隊列的末尾。
生活中常見的就是排隊
代碼實現
function Queue() {代碼實現
鏈表——存儲有序的元素集合
,但在內存中不是連續放置
的。
鏈表(單向鏈表)中的元素
由存放元素本身「data」
的節點和一個指向下一個「next」
元素的指針組成。牢記這個特點
相比數組,鏈表添加或者移除元素不需要移動其他元素,但是需要使用指針
。訪問元素每次都需要從表頭
開始查找。
代碼實現:
單向鏈表
表頭
、表尾
和 存儲數據的 節點
,其中節點
包含三部分:一個鏈向下一個元素的next
, 另一個鏈向前一個元素的prev
和存儲數據的 data
。牢記這個特點
function doublyLinkedList() {
this.head = null // 表頭:始終指向第一個節點,默認為 null
this.tail = null // 表尾:始終指向最后一個節點,默認為 null
this.length = 0 // 鏈表長度
function Node(data) {
this.data = data
this.prev = null
this.next = null
}
doublyLinkedList.prototype.append = function (data) {
let newNode = new Node(data)
if (this.length === 0) {
// 當插入的節點為鏈表的第一個節點時
// 表頭和表尾都指向這個節點
this.head = newNode
this.tail = newNode
} else {
// 當鏈表中已經有節點存在時
// 注意tail指向的始終是最后一個節點
// 注意head指向的始終是第一個節點
// 因為是雙向鏈表,可以從頭部插入新節點,也可以從尾部插入
// 這里以從尾部插入為例,將新節點插入到鏈表最后
// 首先將新節點的 prev 指向上一個節點,即之前tail指向的位置
newNode.prev = this.tail
// 然后前一個節點的next(及之前tail指向的節點)指向新的節點
// 此時新的節點變成了鏈表的最后一個節點
this.tail.next = newNode
// 因為 tail 始終指向的是最后一個節點,所以最后修改tail的指向
this.tail = newNode
}
this.length++
}
doublyLinkedList.prototype.toString = function () {
return this.backwardString()
}
doublyLinkedList.prototype.forwardString = function () {
let current = this.tail
let str = ''
while (current) {
str += current.data + ''
current = current.prev
}
return str
}
doublyLinkedList.prototype.backwardString = function () {
let current = this.head
let str = ''
while (current) {
str += current.data + ''
current = current.next
}
return str
}
doublyLinkedList.prototype.insert = function (position, data) {
if (position < 0 || position > this.length) return false
let newNode = new Node(data)
if (this.length === 0) {
this.head = newNode
this.tail = newNode
} else {
if (position == 0) {
this.head.prev = newNode
newNode.next = this.head
this.head = newNode
} else if (position == this.length) {
newNode.prev = this.tail
this.tail.next = newNode
this.tail = newNode
} else {
let current = this.head
let index = 0
while( index++ < position){
current = current.next
}
newNode.next = current
newNode.prev = current.prev
current.prev.next = newNode
current.prev = newNode
}
}
this.length++
return true
}
doublyLinkedList.prototype.get = function (position) {
if (position < 0 || position >= this.length) return null
let current = this.head
let index = 0
while (index++) {
current = current.next
}
return current.data
}
doublyLinkedList.prototype.indexOf = function (data) {
let current = this.head
let index = 0
while (current) {
if (current.data === data) {
return index
}
current = current.next
index++
}
return -1
}
doublyLinkedList.prototype.update = function (position, newData) {
if (position < 0 || position >= this.length) return false
let current = this.head
let index = 0
while(index++ < position){
current = current.next
}
current.data = newData
return true
}
doublyLinkedList.prototype.removeAt = function (position) {
if (position < 0 || position >= this.length) return null
let current = this.head
if (this.length === 1) {
this.head = null
this.tail = null
} else {
if (position === 0) { // 刪除第一個節點
this.head.next.prev = null
this.head = this.head.next
} else if (position === this.length - 1) { // 刪除最后一個節點
this.tail.prev.next = null
this.tail = this.tail.prev
} else {
let index = 0
while (index++ < position) {
current = current.next
}
current.prev.next = current.next
current.next.prev = current.prev
}
}
this.length--
return current.data
}
doublyLinkedList.prototype.remove = function (data) {
let index = this.indexOf(data)
return this.removeAt(index)
}
}
感謝你的閱讀~
————————————————
版權聲明:本文為CSDN博主「重慶崽兒Brand」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/brand2014/java/article/details/106134844
ESLint
先說是什么:ESLint 是一個檢查代碼質量與風格的工具,配置一套規則,他就能檢查出你代碼中不符合規則的地方,部分問題支持自動修復。
使用這么一套規則有什么用呢?如果單人開發的話倒是沒什么了,但是一個團隊若是存在兩種風格,那格式化之后處理代碼沖突就真的要命了,統一的代碼風格真的很重要!
(其實以前自己做一個項目的時候,公司電腦和家庭電腦的代碼風格配置不一樣,在家加班的時候也經常順手格式化了,這么循環了幾次不同的風格,導致 diff 極其混亂
事件循環
JavaScript是單線程,非阻塞的
瀏覽器的事件循環
執行棧和事件隊列
宏任務和微任務
node環境下的事件循環
和瀏覽器環境有何不同
事件循環模型
宏任務和微任務
經典題目分析
1. JavaScript是單線程,非阻塞的
單線程:
JavaScript的主要用途是與用戶互動,以及操作DOM。如果它是多線程的會有很多復雜的問題要處理,比如有兩個線程同時操作DOM,一個線程刪除了當前的DOM節點,一個線程是要操作當前的DOM階段,最后以哪個線程的操作為準?為了避免這種,所以JS是單線程的。即使H5提出了web worker標準,它有很多限制,受主線程控制,是主線程的子線程。
非阻塞:通過 event loop 實現。
2. 瀏覽器的事件循環
執行棧和事件隊列
為了更好地理解Event Loop,請看下圖(轉引自Philip Roberts的演講 《Help, I'm stuck in an event-loop》)
Help, I'm stuck in an event-loop
執行棧: 同步代碼的執行,按照順序添加到執行棧中
function a() {
b();
console.log('a');
}
function b() {
console.log('b')
}
a();
我們可以通過使用 Loupe(Loupe是一種可視化工具,可以幫助您了解JavaScript的調用堆棧/事件循環/回調隊列如何相互影響)工具來了解上面代碼的執行情況。
調用情況
執行函數 a()先入棧
a()中先執行函數 b() 函數b() 入棧
執行函數b(), console.log('b') 入棧
輸出 b, console.log('b')出棧
函數b() 執行完成,出棧
console.log('a') 入棧,執行,輸出 a, 出棧
函數a 執行完成,出棧。
事件隊列: 異步代碼的執行,遇到異步事件不會等待它返回結果,而是將這個事件掛起,繼續執行執行棧中的其他任務。當異步事件返回結果,將它放到事件隊列中,被放入事件隊列不會立刻執行起回調,而是等待當前執行棧中所有任務都執行完畢,主線程空閑狀態,主線程會去查找事件隊列中是否有任務,如果有,則取出排在第一位的事件,并把這個事件對應的回調放到執行棧中,然后執行其中的同步代碼。
我們再上面代碼的基礎上添加異步事件,
function a() {
b();
console.log('a');
}
function b() {
console.log('b')
setTimeout(function() {
console.log('c');
}, 2000)
}
a();
此時的執行過程如下
img
我們同時再加上點擊事件看一下運行的過程
$.on('button', 'click', function onClick() {
setTimeout(function timer() {
console.log('You clicked the button!');
}, 2000);
});
console.log("Hi!");
setTimeout(function timeout() {
console.log("Click the button!");
}, 5000);
console.log("Welcome to loupe.");
img
簡單用下面的圖進行一下總結
執行棧和事件隊列
宏任務和微任務
為什么要引入微任務,只有一種類型的任務不行么?
頁面渲染事件,各種IO的完成事件等隨時被添加到任務隊列中,一直會保持先進先出的原則執行,我們不能準確地控制這些事件被添加到任務隊列中的位置。但是這個時候突然有高優先級的任務需要盡快執行,那么一種類型的任務就不合適了,所以引入了微任務隊列。
不同的異步任務被分為:宏任務和微任務
宏任務:
script(整體代碼)
setTimeout()
setInterval()
postMessage
I/O
UI交互事件
微任務:
new Promise().then(回調)
MutationObserver(html5 新特性)
運行機制
異步任務的返回結果會被放到一個任務隊列中,根據異步事件的類型,這個事件實際上會被放到對應的宏任務和微任務隊列中去。
在當前執行棧為空時,主線程會查看微任務隊列是否有事件存在
存在,依次執行隊列中的事件對應的回調,直到微任務隊列為空,然后去宏任務隊列中取出最前面的事件,把當前的回調加到當前指向棧。
如果不存在,那么再去宏任務隊列中取出一個事件并把對應的回到加入當前執行棧;
當前執行棧執行完畢后時會立刻處理所有微任務隊列中的事件,然后再去宏任務隊列中取出一個事件。同一次事件循環中,微任務永遠在宏任務之前執行。
在事件循環中,每進行一次循環操作稱為 tick,每一次 tick 的任務處理模型是比較復雜的,但關鍵步驟如下:
執行一個宏任務(棧中沒有就從事件隊列中獲取)
執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
宏任務執行完畢后,立即執行當前微任務隊列中的所有微任務(依次執行)
當前宏任務執行完畢,開始檢查渲染,然后GUI線程接管渲染
渲染完畢后,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲?。?
簡單總結一下執行的順序:
執行宏任務,然后執行該宏任務產生的微任務,若微任務在執行過程中產生了新的微任務,則繼續執行微任務,微任務執行完畢后,再回到宏任務中進行下一輪循環。
宏任務和微任務
深入理解js事件循環機制(瀏覽器篇) 這邊文章中有個特別形象的動畫,大家可以看著理解一下。
console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('end')
瀏覽器事件循環
全局代碼壓入執行棧執行,輸出 start
setTimeout壓入 macrotask隊列,promise.then 回調放入 microtask隊列,最后執行 console.log('end'),輸出 end
調用棧中的代碼執行完成(全局代碼屬于宏任務),接下來開始執行微任務隊列中的代碼,執行promise回調,輸出 promise1, promise回調函數默認返回 undefined, promise狀態變成 fulfilled ,觸發接下來的 then回調,繼續壓入 microtask隊列,此時產生了新的微任務,會接著把當前的微任務隊列執行完,此時執行第二個 promise.then回調,輸出 promise2
此時,microtask隊列 已清空,接下來會會執行 UI渲染工作(如果有的話),然后開始下一輪 event loop, 執行 setTimeout的回調,輸出 setTimeout
最后的執行結果如下
start
end
promise1
promise2
setTimeout
node環境下的事件循環
和瀏覽器環境有何不同
表現出的狀態與瀏覽器大致相同。不同的是 node 中有一套自己的模型。node 中事件循環的實現依賴 libuv 引擎。Node的事件循環存在幾個階段。
如果是node10及其之前版本,microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行 microtask隊列中的任務。
node版本更新到11之后,Event Loop運行原理發生了變化,一旦執行一個階段里的一個宏任務(setTimeout,setInterval和setImmediate)就立刻執行微任務隊列,跟瀏覽器趨于一致。下面例子中的代碼是按照的去進行分析的。
事件循環模型
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
事件循環各階段詳解
node中事件循環的順序
外部輸入數據 --> 輪詢階段(poll) --> 檢查階段(check) --> 關閉事件回調階段(close callback) --> 定時器檢查階段(timer) --> I/O 事件回調階段(I/O callbacks) --> 閑置階段(idle, prepare) --> 輪詢階段...
這些階段大致的功能如下:
定時器檢測階段(timers): 這個階段執行定時器隊列中的回調如 setTimeout() 和 setInterval()。
I/O事件回調階段(I/O callbacks): 這個階段執行幾乎所有的回調。但是不包括close事件,定時器和setImmediate()的回調。
閑置階段(idle, prepare): 這個階段僅在內部使用,可以不必理會
輪詢階段(poll): 等待新的I/O事件,node在一些特殊情況下會阻塞在這里。
檢查階段(check): setImmediate()的回調會在這個階段執行。
關閉事件回調階段(close callbacks): 例如socket.on('close', ...)這種close事件的回調
poll:
這個階段是輪詢時間,用于等待還未返回的 I/O 事件,比如服務器的回應、用戶移動鼠標等等。
這個階段的時間會比較長。如果沒有其他異步任務要處理(比如到期的定時器),會一直停留在這個階段,等待 I/O 請求返回結果。
check:
該階段執行setImmediate()的回調函數。
close:
該階段執行關閉請求的回調函數,比如socket.on('close', ...)。
timer階段:
這個是定時器階段,處理setTimeout()和setInterval()的回調函數。進入這個階段后,主線程會檢查一下當前時間,是否滿足定時器的條件。如果滿足就執行回調函數,否則就離開這個階段。
I/O callback階段:
除了以下的回調函數,其他都在這個階段執行:
setTimeout()和setInterval()的回調函數
setImmediate()的回調函數
用于關閉請求的回調函數,比如socket.on('close', ...)
宏任務和微任務
宏任務:
setImmediate
setTimeout
setInterval
script(整體代碼)
I/O 操作等。
微任務:
process.nextTick
new Promise().then(回調)
Promise.nextTick, setTimeout, setImmediate的使用場景和區別
Promise.nextTick
process.nextTick 是一個獨立于 eventLoop 的任務隊列。
在每一個 eventLoop 階段完成后會去檢查 nextTick 隊列,如果里面有任務,會讓這部分任務優先于微任務執行。
是所有異步任務中最快執行的。
setTimeout:
setTimeout()方法是定義一個回調,并且希望這個回調在我們所指定的時間間隔后第一時間去執行。
setImmediate:
setImmediate()方法從意義上將是立刻執行的意思,但是實際上它卻是在一個固定的階段才會執行回調,即poll階段之后。
經典題目分析
一. 下面代碼輸出什么
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
先執行宏任務(當前代碼塊也算是宏任務),然后執行當前宏任務產生的微任務,然后接著執行宏任務
從上往下執行代碼,先執行同步代碼,輸出 script start
遇到setTimeout,現把 setTimeout 的代碼放到宏任務隊列中
執行 async1(),輸出 async1 start, 然后執行 async2(), 輸出 async2,把 async2() 后面的代碼 console.log('async1 end')放到微任務隊列中
接著往下執行,輸出 promise1,把 .then()放到微任務隊列中;注意Promise本身是同步的立即執行函數,.then是異步執行函數
接著往下執行, 輸出 script end。同步代碼(同時也是宏任務)執行完成,接下來開始執行剛才放到微任務中的代碼
依次執行微任務中的代碼,依次輸出 async1 end、 promise2, 微任務中的代碼執行完成后,開始執行宏任務中的代碼,輸出 setTimeout
最后的執行結果如下
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
二. 下面代碼輸出什么
console.log('start');
setTimeout(() => {
console.log('children2');
Promise.resolve().then(() => {
console.log('children3');
})
}, 0);
new Promise(function(resolve, reject) {
console.log('children4');
setTimeout(function() {
console.log('children5');
resolve('children6')
}, 0)
}).then((res) => {
console.log('children7');
setTimeout(() => {
console.log(res);
}, 0)
})
這道題跟上面題目不同之處在于,執行代碼會產生很多個宏任務,每個宏任務中又會產生微任務
從上往下執行代碼,先執行同步代碼,輸出 start
遇到setTimeout,先把 setTimeout 的代碼放到宏任務隊列①中
接著往下執行,輸出 children4, 遇到setTimeout,先把 setTimeout 的代碼放到宏任務隊列②中,此時.then并不會被放到微任務隊列中,因為 resolve是放到 setTimeout中執行的
代碼執行完成之后,會查找微任務隊列中的事件,發現并沒有,于是開始執行宏任務①,即第一個 setTimeout, 輸出 children2,此時,會把 Promise.resolve().then放到微任務隊列中。
宏任務①中的代碼執行完成后,會查找微任務隊列,于是輸出 children3;然后開始執行宏任務②,即第二個 setTimeout,輸出 children5,此時將.then放到微任務隊列中。
宏任務②中的代碼執行完成后,會查找微任務隊列,于是輸出 children7,遇到 setTimeout,放到宏任務隊列中。此時微任務執行完成,開始執行宏任務,輸出 children6;
最后的執行結果如下
start
children4
children2
children3
children5
children7
children6
三. 下面代碼輸出什么
const p = function() {
return new Promise((resolve, reject) => {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
resolve(2)
})
p1.then((res) => {
console.log(res);
})
console.log(3);
resolve(4);
})
}
p().then((res) => {
console.log(res);
})
console.log('end');
執行代碼,Promise本身是同步的立即執行函數,.then是異步執行函數。遇到setTimeout,先把其放入宏任務隊列中,遇到p1.then會先放到微任務隊列中,接著往下執行,輸出 3
遇到 p().then 會先放到微任務隊列中,接著往下執行,輸出 end
同步代碼塊執行完成后,開始執行微任務隊列中的任務,首先執行 p1.then,輸出 2, 接著執行p().then, 輸出 4
微任務執行完成后,開始執行宏任務,setTimeout, resolve(1),但是此時 p1.then已經執行完成,此時 1不會輸出。
最后的執行結果如下
3
end
2
4
你可以將上述代碼中的 resolve(2)注釋掉, 此時 1才會輸出,輸出結果為 3 end 4 1。
const p = function() {
return new Promise((resolve, reject) => {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
})
p1.then((res) => {
console.log(res);
})
console.log(3);
resolve(4);
})
}
p().then((res) => {
console.log(res);
})
console.log('end');
3
end
4
1
最后強烈推薦幾個非常好的講解 event loop 的視頻:
What the heck is the event loop anyway? | Philip Roberts | JSConf EU
Jake Archibald: In The Loop - JSConf.Asia
注意:為了方便開發者減少配置項,描述頁面的四個文件必須具有相同的路徑與文件名。
WXML(WeiXin Markup Language)是框架設計的一套標簽語言,結合基礎組件、事件系統,可以構建出頁面的結構。WXML
充當的就是類似 HTML
的角色
要完整了解 WXML 語法,請參考WXML 語法參考。
WXSS (WeiXin Style Sheets)是一套樣式語言,用于描述 WXML 的組件樣式。
WXSS 用來決定 WXML 的組件應該怎么顯示。
為了適應廣大的前端開發者,WXSS 具有 CSS 大部分特性。同時為了更適合開發微信小程序,WXSS 對 CSS 進行了擴充以及修改。
與 CSS 相比,WXSS 擴展的特性有:
尺寸單位
樣式導入
JSON 是一種數據格式,并不是編程語言,在小程序中,JSON扮演的靜態配置的角色。
全局配置
小程序根目錄下的 app.json 文件用來對微信小程序進行全局配置,決定頁面文件的路徑、窗口表現、設置網絡超時時間、設置多 tab 等。
頁面配置
每一個小程序頁面也可以使用同名 .json 文件來對本頁面的窗口表現進行配置,頁面中配置項會覆蓋 app.json 的 window 中相同的配置項。
工具配置 project.config.json
通常大家在使用一個工具的時候,都會針對各自喜好做一些個性化配置,例如界面顏色、編譯配置等等,當你換了另外一臺電腦重新安裝工具的時候,你還要重新配置。
考慮到這點,小程序開發者工具在每個項目的根目錄都會生成一個 project.config.json,你在工具上做的任何配置都會寫入到這個文件,當你重新安裝工具或者換電腦工作時,你只要載入同一個項目的代碼包,開發者工具就自動
注意:
JSON文件都是被包裹在一個大括號中 {},通過key-value的方式來表達數據。JSON的Key必須包裹在一個雙引號中,在實踐中,編寫 JSON 的時候,忘了給 Key 值加雙引號或者是把雙引號寫成單引號是常見錯誤。
JSON的值只能是以下幾種數據格式,其他任何格式都會觸發報錯,例如 JavaScript 中的 undefined。
數字,包含浮點數和整數
字符串,需要包裹在雙引號中
Bool值,true 或者 false
數組,需要包裹在方括號中 []
對象,需要包裹在大括號中 {}
Null
還需要注意的是 JSON 文件中無法使用注釋,試圖添加注釋將會引發報錯。
一個服務僅僅只有界面展示是不夠的,還需要和用戶做交互:響應用戶的點擊、獲取用戶的位置等等。在小程序里邊,我們就通過編寫 JS
腳本文件來處理用戶的操作。
注冊頁面
對于小程序中的每個頁面,都需要在頁面對應的 js 文件中進行注冊,指定頁面的初始數據、生命周期回調、事件處理函數等
使用 Page 構造器注冊頁面
簡單的頁面可以使用 Page() 進行構造。
使用 Component 構造器構造頁面
Page 構造器適用于簡單的頁面。但對于復雜的頁面, Page 構造器可能并不好用。
此時,可以使用 Component 構造器來構造頁面。 Component 構造器的主要區別是:方法需要放在 methods: { } 里面。
————————————————
版權聲明:本文為CSDN博主「前端嵐楓」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/yilanyoumeng3/java/article/details/106292742
應對現在數據可視化的趨勢,越來越多企業需要在很多場景(營銷數據,生產數據,用戶數據)下使用,可視化圖表來展示體現數據,讓數據更加直觀,數據特點更加突出。
常見的數據可視化庫:
D3.js 目前 Web 端評價最高的 Javascript 可視化工具庫(入手難)
ECharts.js 百度出品的一個開源 Javascript 數據可視化庫
Highcharts.js 國外的前端數據可視化庫,非商用免費,被許多國外大公司所使用
AntV 螞蟻金服全新一代數據可視化解決方案 等等
Highcharts 和 Echarts 就像是 Office 和 WPS 的關系
ECharts,一個使用 JavaScript 實現的開源可視化庫,可以流暢的運行在 PC 和移動設備上,兼容當前絕大部分瀏覽器(IE8/9/10/11,Chrome,Firefox,Safari等),底層依賴矢量圖形庫 ZRender,提供直觀,交互豐富,可高度個性化定制的數據可視化圖表。
官網地址:https://www.echartsjs.com/zh/index.html
echarts體驗
下載echarts https://github.com/apache/incubator-echarts/tree/4.5.0
使用步驟(5大步驟):
1.引入echarts 插件文件到html頁面中
2.準備一個具備大小的DOM容器
<div id="main" style="width: 600px;height:400px;"></div>
3.初始化echarts實例對象
var myChart = echarts.init(document.getElementById('main'));
4.指定配置項和數據(option)
var option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line'
}]
};
5.將配置項設置給echarts實例對象
myChart.setOption(option);
這是要求同學們知道以下配置每個模塊的主要作用干什么的就可以了
series
系列列表。每個系列通過 type 決定自己的圖表類型
大白話:圖標數據,指定什么類型的圖標,可以多個圖表重疊。
xAxis:直角坐標系 grid 中的 x 軸
boundaryGap: 坐標軸兩邊留白策略 true,這時候刻度只是作為分隔線,標簽和數據點都會在兩個刻度之間的帶(band)中間。
yAxis:直角坐標系 grid 中的 y 軸
grid:直角坐標系內繪圖網格。
title:標題組件
tooltip:提示框組件
legend:圖例組件
color:調色盤顏色列表
數據堆疊,同個類目軸上系列配置相同的stack值后 后一個系列的值會在前一個系列的值上相加。
option = {
// color設置我們線條的顏色 注意后面是個數組
color: ['pink', 'red', 'green', 'skyblue'],
// 設置圖表的標題
title: {
text: '折線圖堆疊123'
},
// 圖表的提示框組件
tooltip: {
// 觸發方式
trigger: 'axis'
},
// 圖例組件
legend: {
// series里面有了 name值則 legend里面的data可以刪掉
},
// 網格配置 grid可以控制線形圖 柱狀圖 圖表大小
grid: {
left: '3%',
right: '4%',
bottom: '3%',
// 是否顯示刻度標簽 如果是true 就顯示 否則反之
containLabel: true
},
// 工具箱組件 可以另存為圖片等功能
toolbox: {
feature: {
saveAsImage: {}
}
},
// 設置x軸的相關配置
xAxis: {
type: 'category',
// 是否讓我們的線條和坐標軸有縫隙
boundaryGap: false,
data: ['星期一', '周二', '周三', '周四', '周五', '周六', '周日']
},
// 設置y軸的相關配置
yAxis: {
type: 'value'
},
// 系列圖表配置 它決定著顯示那種類型的圖表
series: [
{
name: '郵件營銷',
type: 'line',
data: [120, 132, 101, 134, 90, 230, 210]
},
{
name: '聯盟廣告',
type: 'line',
data: [220, 182, 191, 234, 290, 330, 310]
},
{
name: '視頻廣告',
type: 'line',
data: [150, 232, 201, 154, 190, 330, 410]
},
{
name: '直接訪問',
type: 'line',
data: [320, 332, 301, 334, 390, 330, 320]
}
]
};
1.官網實例
官網默認為我們提供了大量的案例,我們需要使用那一種只需要直接點擊打開后復制他們的相關配置,然后按照我上面說的5大步驟進行使用即可。
2.社區Gallery
官方自帶的圖例,可能在很多時候并不能滿足我們的需要,在社區這里可以找到一些基于echart的高度定制好的圖表,相當于基于jquery開發的插件,這里是基于echarts開發的第三方的圖表。
本案例中使用了地圖模擬飛行的案例就是從社區中進行引用的,
參考社區的例子:https://gallery.echartsjs.com/editor.html?c=x0-ExSkZDM (模擬飛機航線)
實現步驟:
第一需要下載china.js提供中國地圖的js文件
第二個因為里面代碼比較多,我們新建一個新的js文件 myMap.js 引入
使用社區提供的配置即可。
代碼已經上傳至我的碼云如有需要的小伙伴可自行下載:
https://gitee.com/jiuyueqi/echarts
ps:最后呢,如果大家看完樓主的文章覺得對echarts的學習和了解有所幫助,麻煩大家路過點個贊點個關注唄!樓主后續還會繼續更新有關前端方面的面試題資料或者其他方面的知識。
————————————————
版權聲明:本文為CSDN博主「程序猿玖月柒」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/weixin_45257157/java/article/details/106300587
上一篇博客講到了數組的方法,當然里邊比較復雜的就是數組的迭代方法,因為涉及到了回調函數,所以這篇博客我們來詳細講解一下js數組迭代方法的使用。
var arr=[1,2,3,4,5,6];
arr.forEach(function(val,index,arr){
console.log(val,index,arr);
})
// 其中:
// value:每一個數組項的值 必填項
// index:每一個數組項對應的索引
// arr:當前的數組
注意:forEach()方法不返回值,所以回調函數中使用return會打印出來undefined。
var aNum2 = [1.2, 1.8, 2.0, 4.3];
console.log(aNum2.map(Math.floor()));// [1,1,2,4]
var arr=[1,2,3];
console.log(arr.map(function(val,index){
return val*3
}));// 3 6 9
// 其中:
// value:每一個數組項的值 必填項
// index:每一個數組項對應的索引
// arr:當前的數組
注意:map()方法有返回值,返回值為新的數組,所以可以直接再回調函數中return。
var arr=[10,20,30];
console.log(arr.every(function(val){
return val>20;
}));// false
console.log(arr.every(function(val){
return val>0;
}));// true
// 其中:
// value:每一個數組項的值 必填項
// index:每一個數組項對應的索引
// arr:當前的數組
注意:every()方法所有的數組項都符合判斷時返回true,否則返回false。
var arr=[10,20,30];
console.log(arr.some(function(val){
return val>20;
}));// true
console.log(arr.some(function(val){
return val>0;
}));// true
console.log(arr.some(function(val){
return val<0;
}));// false
arr.some(function(val){
console.log(val<0);
});//fasle false false
// 其中:
// value:每一個數組項的值 必填項
// index:每一個數組項對應的索引
// arr:當前的數組
注意:some()方法如果回調函數執行完會根據結果返回true或false,但是回調函數中打印判斷是,只會作為判斷條件的返回值,則會打印多遍。
5.fliter(funcrion(value,index,arr){}):對數組的每一項都運行給定函數,進行過濾,將符合條件的數組項添加到新的數組中,并返回新的數組。
var aNum=[1,2,3,4];
console.log(aNum.filter(function (num) {
return num > 1;
}));//[2,3,4,]
aNum.filter(function (num) {
console.log(num > 1);//true true true
})
注意:filter()方法對數組項進行過濾,然后將符合條件的數組項添加到一個新的數組并返回,但是如果直接打印這個判斷條件,相當于打印的判斷條件的結果,只會返回true或者false。
1.find():返回第一個符合傳入測試(函數)條件的數組元素。
var aNum=[10,20,30,40];
console.log(aNum.find(function (num) {
return num > 19;
}));//1
console.log(aNum.find(function (num) {
return num < 0;
}));//undefined
2.findIndex():返回符合傳入測試(函數)條件的數組元素索引。
console.log(aNum.findIndex(function (num) { return num > 19; }));//3
3.includes():判斷一個數組是否包含一個指定的值。
總結:
forEach()與map()是一對,用于數組遍歷執行指定函數,前者不返回數組,后者返回 處理過的新數組。
every()與some()是一對,分別適用于檢測數組是否全部滿足某條件或者存在滿足的數組項,返回true或false。
filter()則是相當于過濾器的存在,過濾掉數組中不符合條件的數據,將符合條件的數組項添加到新數組,并返回。
————————————————
版權聲明:本文為CSDN博主「Mr_Han119」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_39155611/java/article/details/106294417
在 TypeScript 開發中,tsconfig.json 是個不可或缺的配置文件,它是我們在 TS 項目中最常見的配置文件,那么你真的了解這個文件嗎?它里面都有哪些優秀配置?如何配置一個合理的 tsconfig.json 文件?本文將全面帶大家一起詳細了解 tsconfig.json 的各項配置。
本文將從以下幾個方面全面介紹 tsconfig.json 文件:
了不起的 tsconfig.json 指南.png
水平有限,歡迎各位大佬指點~~
一、tsconfig.json 簡介
1. 什么是 tsconfig.json
TypeScript 使用 tsconfig.json 文件作為其配置文件,當一個目錄中存在 tsconfig.json 文件,則認為該目錄為 TypeScript 項目的根目錄。
通常 tsconfig.json 文件主要包含兩部分內容:指定待編譯文件和定義編譯選項。
從《TypeScript編譯器的配置文件的JSON模式》可知,目前 tsconfig.json 文件有以下幾個頂層屬性:
compileOnSave
compilerOptions
exclude
extends
files
include
references
typeAcquisition
文章后面會詳細介紹一些常用屬性配置。
2. 為什么使用 tsconfig.json
通常我們可以使用 tsc 命令來編譯少量 TypeScript 文件:
/*
參數介紹:
--outFile // 編譯后生成的文件名稱
--target // 指定ECMAScript目標版本
--module // 指定生成哪個模塊系統代碼
index.ts // 源文件
*/
$ tsc --outFile leo.js --target es3 --module amd index.ts
但如果實際開發的項目,很少是只有單個文件,當我們需要編譯整個項目時,就可以使用 tsconfig.json 文件,將需要使用到的配置都寫進 tsconfig.json 文件,這樣就不用每次編譯都手動輸入配置,另外也方便團隊協作開發。
二、使用 tsconfig.json
目前使用 tsconfig.json 有2種操作:
1. 初始化 tsconfig.json
在初始化操作,也有 2 種方式:
手動在項目根目錄(或其他)創建 tsconfig.json 文件并填寫配置;
通過 tsc --init 初始化 tsconfig.json 文件。
2. 指定需要編譯的目錄
在不指定輸入文件的情況下執行 tsc 命令,默認從當前目錄開始編譯,編譯所有 .ts 文件,并且從當前目錄開始查找 tsconfig.json 文件,并逐級向上級目錄搜索。
$ tsc
另外也可以為 tsc 命令指定參數 --project 或 -p 指定需要編譯的目錄,該目錄需要包含一個 tsconfig.json 文件,如:
/*
文件目錄:
├─src/
│ ├─index.ts
│ └─tsconfig.json
├─package.json
*/
$ tsc --project src
注意,tsc 的命令行選項具有優先級,會覆蓋 tsconfig.json 中的同名選項。
更多 tsc 編譯選項,可查看《編譯選項》章節。
三、使用示例
這個章節,我們將通過本地一個小項目 learnTsconfig 來學著實現一個簡單配置。
當前開發環境:windows / node 10.15.1 / TypeScript3.9
1. 初始化 learnTsconfig 項目
執行下面命令:
$ mkdir learnTsconfig
$ cd .\learnTsconfig\
$ mkdir src
$ new-item index.ts
并且我們為 index.ts 文件寫一些簡單代碼:
// 返回當前版本號
function getVersion(version:string = "1.0.0"): string{
return version;
}
console.log(getVersion("1.0.1"))
我們將獲得這么一個目錄結構:
└─src/
└─index.ts
2. 初始化 tsconfig.json 文件
在 learnTsconfig 根目錄執行:
$ tsc --init
3. 修改 tsconfig.json 文件
我們設置幾個常見配置項:
{
"compilerOptions": {
"target": "ES5", // 目標語言的版本
"module": "commonjs", // 指定生成代碼的模板標準
"noImplicitAny": true, // 不允許隱式的 any 類型
"removeComments": true, // 刪除注釋
"preserveConstEnums": true, // 保留 const 和 enum 聲明
"sourceMap": true // 生成目標文件的sourceMap文件
},
"files": [ // 指定待編譯文件
"./src/index.ts"
]
}
其中需要注意一點:
files 配置項值是一個數組,用來指定了待編譯文件,即入口文件。
當入口文件依賴其他文件時,不需要將被依賴文件也指定到 files 中,因為編譯器會自動將所有的依賴文件歸納為編譯對象,即 index.ts 依賴 user.ts 時,不需要在 files 中指定 user.ts , user.ts 會自動納入待編譯文件。
4. 執行編譯
配置完成后,我們可以在命令行執行 tsc 命令,執行編譯完成后,我們可以得到一個 index.js 文件和一個 index.js.map 文件,證明我們編譯成功,其中 index.js 文件內容如下:
function getVersion(version) {
if (version === void 0) { version = "1.0.0"; }
return version;
}
console.log(getVersion("1.0.1"));
//# sourceMappingURL=index.js.map
可以看出,tsconfig.json 中的 removeComments 配置生效了,將我們添加的注釋代碼移除了。
到這一步,就完成了這個簡單的示例,接下來會基于這個示例代碼,講解《七、常見配置示例》。
四、tsconfig.json 文件結構介紹
1. 按頂層屬性分類
在 tsconfig.json 文件中按照頂層屬性,分為以下幾類:
tsconfig.json 文件結構(頂層屬性).png
了不起的 tsconfig.json 指南.png
2. 按功能分類
tsconfig.json 文件結構(功能).png
五、tsconfig.json 配置介紹
1. compileOnSave
compileOnSave 屬性作用是設置保存文件的時候自動編譯,但需要編譯器支持。
{
// ...
"compileOnSave": false,
}
2. compilerOptions
compilerOptions 屬性作用是配置編譯選項。
若 compilerOptions 屬性被忽略,則編譯器會使用默認值,可以查看《官方完整的編譯選項列表》。
編譯選項配置非常繁雜,有很多配置,這里只列出常用的配置。
{
// ...
"compilerOptions": {
"incremental": true, // TS編譯器在第一次編譯之后會生成一個存儲編譯信息的文件,第二次編譯會在第一次的基礎上進行增量編譯,可以提高編譯的速度
"tsBuildInfoFile": "./buildFile", // 增量編譯文件的存儲位置
"diagnostics": true, // 打印診斷信息
"target": "ES5", // 目標語言的版本
"module": "CommonJS", // 生成代碼的模板標準
"outFile": "./app.js", // 將多個相互依賴的文件生成一個文件,可以用在AMD模塊中,即開啟時應設置"module": "AMD",
"lib": ["DOM", "ES2015", "ScriptHost", "ES2019.Array"], // TS需要引用的庫,即聲明文件,es5 默認引用dom、es5、scripthost,如需要使用es的高級版本特性,通常都需要配置,如es8的數組新特性需要引入"ES2019.Array",
"allowJS": true, // 允許編譯器編譯JS,JSX文件
"checkJs": true, // 允許在JS文件中報錯,通常與allowJS一起使用
"outDir": "./dist", // 指定輸出目錄
"rootDir": "./", // 指定輸出文件目錄(用于輸出),用于控制輸出目錄結構
"declaration": true, // 生成聲明文件,開啟后會自動生成聲明文件
"declarationDir": "./file", // 指定生成聲明文件存放目錄
"emitDeclarationOnly": true, // 只生成聲明文件,而不會生成js文件
"sourceMap": true, // 生成目標文件的sourceMap文件
"inlineSourceMap": true, // 生成目標文件的inline SourceMap,inline SourceMap會包含在生成的js文件中
"declarationMap": true, // 為聲明文件生成sourceMap
"typeRoots": [], // 聲明文件目錄,默認時node_modules/@types
"types": [], // 加載的聲明文件包
"removeComments":true, // 刪除注釋
"noEmit": true, // 不輸出文件,即編譯后不會生成任何js文件
"noEmitOnError": true, // 發送錯誤時不輸出任何文件
"noEmitHelpers": true, // 不生成helper函數,減小體積,需要額外安裝,常配合importHelpers一起使用
"importHelpers": true, // 通過tslib引入helper函數,文件必須是模塊
"downlevelIteration": true, // 降級遍歷器實現,如果目標源是es3/5,那么遍歷器會有降級的實現
"strict": true, // 開啟所有嚴格的類型檢查
"alwaysStrict": true, // 在代碼中注入'use strict'
"noImplicitAny": true, // 不允許隱式的any類型
"strictNullChecks": true, // 不允許把null、undefined賦值給其他類型的變量
"strictFunctionTypes": true, // 不允許函數參數雙向協變
"strictPropertyInitialization": true, // 類的實例屬性必須初始化
"strictBindCallApply": true, // 嚴格的bind/call/apply檢查
"noImplicitThis": true, // 不允許this有隱式的any類型
"noUnusedLocals": true, // 檢查只聲明、未使用的局部變量(只提示不報錯)
"noUnusedParameters": true, // 檢查未使用的函數參數(只提示不報錯)
"noFallthroughCasesInSwitch": true, // 防止switch語句貫穿(即如果沒有break語句后面不會執行)
"noImplicitReturns": true, //每個分支都會有返回值
"esModuleInterop": true, // 允許export=導出,由import from 導入
"allowUmdGlobalAccess": true, // 允許在模塊中全局變量的方式訪問umd模塊
"moduleResolution": "node", // 模塊解析策略,ts默認用node的解析策略,即相對的方式導入
"baseUrl": "./", // 解析非相對模塊的基地址,默認是當前目錄
"paths": { // 路徑映射,相對于baseUrl
// 如使用jq時不想使用默認版本,而需要手動指定版本,可進行如下配置
"jquery": ["node_modules/jquery/dist/jquery.min.js"]
},
"rootDirs": ["src","out"], // 將多個目錄放在一個虛擬目錄下,用于運行時,即編譯后引入文件的位置可能發生變化,這也設置可以虛擬src和out在同一個目錄下,不用再去改變路徑也不會報錯
"listEmittedFiles": true, // 打印輸出文件
"listFiles": true// 打印編譯的文件(包括引用的聲明文件)
}
}
3. exclude
exclude 屬性作用是指定編譯器需要排除的文件或文件夾。
默認排除 node_modules 文件夾下文件。
{
// ...
"exclude": [
"src/lib" // 排除src目錄下的lib文件夾下的文件不會編譯
]
}
和 include 屬性一樣,支持 glob 通配符:
* 匹配0或多個字符(不包括目錄分隔符)
? 匹配一個任意字符(不包括目錄分隔符)
**/ 遞歸匹配任意子目錄
4. extends
extends 屬性作用是引入其他配置文件,繼承配置。
默認包含當前目錄和子目錄下所有 TypeScript 文件。
{
// ...
// 把基礎配置抽離成tsconfig.base.json文件,然后引入
"extends": "./tsconfig.base.json"
}
5. files
files 屬性作用是指定需要編譯的單個文件列表。
默認包含當前目錄和子目錄下所有 TypeScript 文件。
{
// ...
"files": [
// 指定編譯文件是src目錄下的leo.ts文件
"scr/leo.ts"
]
}
6. include
include 屬性作用是指定編譯需要編譯的文件或目錄。
{
// ...
"include": [
// "scr" // 會編譯src目錄下的所有文件,包括子目錄
// "scr/*" // 只會編譯scr一級目錄下的文件
"scr/*/*" // 只會編譯scr二級目錄下的文件
]
}
7. references
references 屬性作用是指定工程引用依賴。
在項目開發中,有時候我們為了方便將前端項目和后端node項目放在同一個目錄下開發,兩個項目依賴同一個配置文件和通用文件,但我們希望前后端項目進行靈活的分別打包,那么我們可以進行如下配置:
{
// ...
"references": [ // 指定依賴的工程
{"path": "./common"}
]
}
8. typeAcquisition
typeAcquisition 屬性作用是設置自動引入庫類型定義文件(.d.ts)相關。
包含 3 個子屬性:
enable : 布爾類型,是否開啟自動引入庫類型定義文件(.d.ts),默認為 false;
include : 數組類型,允許自動引入的庫名,如:["jquery", "lodash"];
exculde : 數組類型,排除的庫名。
{
// ...
"typeAcquisition": {
"enable": false,
"exclude": ["jquery"],
"include": ["jest"]
}
}
六、常見配置示例
本部分內容中,我們找了幾個實際開發中比較常見的配置,當然,還有很多配置需要自己摸索喲~~
1. 移除代碼中注釋
tsconfig.json:
{
"compilerOptions": {
"removeComments": true,
}
}
編譯前:
// 返回當前版本號
function getVersion(version:string = "1.0.0"): string{
return version;
}
console.log(getVersion("1.0.1"))
編譯結果:
function getVersion(version) {
if (version === void 0) { version = "1.0.0"; }
return version;
}
console.log(getVersion("1.0.1"));
2. 開啟null、undefined檢測
tsconfig.json:
{
"compilerOptions": {
"strictNullChecks": true
},
}
修改 index.ts 文件內容:
const leo;
leo = new Pingan('leo','hello');
這時候編輯器也會提示錯誤信息,執行 tsc 后,控制臺報錯:
src/index.ts:9:11 - error TS2304: Cannot find name 'Pingan'.
9 leo = new Pingan('leo','hello');
Found 1 error.
3. 配置復用
通過 extends 屬性實現配置復用,即一個配置文件可以繼承另一個文件的配置屬性。
比如,建立一個基礎的配置文件 configs/base.json :
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true
}
}
在tsconfig.json 就可以引用這個文件的配置了:
{
"extends": "./configs/base",
"files": [
"main.ts",
"supplemental.ts"
]
}
4. 生成枚舉的映射代碼
在默認情況下,使用 const 修飾符后,枚舉不會生成映射代碼。
如下,我們可以看出:使用 const 修飾符后,編譯器不會生成任何 RequestMethod 枚舉的任何映射代碼,在其他地方使用時,內聯每個成員的值,節省很大開銷。
const enum RequestMethod {
Get,
Post,
Put,
Delete
}
let methods = [
RequestMethod.Get,
RequestMethod.Post
]
編譯結果:
"use strict";
let methods = [
0 /* Get */,
1 /* Post */
];
當然,我們希望生成映射代碼時,也可以設置 tsconfig.json 中的配置,設置 preserveConstEnums 編譯器選項為 true :
{
"compilerOptions": {
"target": "es5",
"preserveConstEnums": true
}
}
最后編譯結果變成:
"use strict";
var RequestMethod;
(function (RequestMethod) {
RequestMethod[RequestMethod["Get"] = 0] = "Get";
RequestMethod[RequestMethod["Post"] = 1] = "Post";
RequestMethod[RequestMethod["Put"] = 2] = "Put";
RequestMethod[RequestMethod["Delete"] = 3] = "Delete";
})(RequestMethod || (RequestMethod = {}));
let methods = [
0 /* Get */,
1 /* Post */
];
5. 關閉 this 類型注解提示
通過下面代碼編譯后會報錯:
const button = document.querySelector("button");
button?.addEventListener("click", handleClick);
function handleClick(this) {
console.log("Clicked!");
this.removeEventListener("click", handleClick);
}
報錯內容:
src/index.ts:10:22 - error TS7006: Parameter 'this' implicitly has an 'any' type.
10 function handleClick(this) {
Found 1 error.
這是因為 this 隱式具有 any 類型,如果沒有指定類型注解,編譯器會提示“"this" 隱式具有類型 "any",因為它沒有類型注釋?!?。
解決方法有2種:
指定 this 類型,如本代碼中為 HTMLElement 類型:
HTMLElement 接口表示所有的 HTML 元素。一些HTML元素直接實現了 HTMLElement 接口,其它的間接實現HTMLElement接口。
關于 HTMLElement 可查看詳細。
使用 --noImplicitThis 配置項:
在 TS2.0 還增加一個新的編譯選項: --noImplicitThis,表示當 this 表達式值為 any 類型時生成一個錯誤信息。我們設置為 true 后就能正常編譯。
{
"compilerOptions": {
"noImplicitThis": true
}
}
七、Webpack/React 中使用示例
1. 配置編譯 ES6 代碼,JSX 文件
創建測試項目 webpack-demo,結構如下:
webpack-demo/
|- package.json
|- tsconfig.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
|- index.js
|- index.ts
|- /node_modules
安裝 TypeScript 和 ts-loader:
$ npm install --save-dev typescript ts-loader
配置 tsconfig.json,支持 JSX,并將 TypeScript 編譯為 ES5:
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
+ "module": "es6",
+ "target": "es5",
+ "jsx": "react",
"allowJs": true
}
}
還需要配置 webpack.config.js,使其能夠處理 TypeScript 代碼,這里主要在 rules 中添加 ts-loader :
const path = require('path');
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
2. 配置 source map
想要啟用 source map,我們必須配置 TypeScript,以將內聯的 source map 輸出到編譯后的 JavaScript 文件中。
只需要在 tsconfig.json 中配置 sourceMap 屬性:
{
"compilerOptions": {
"outDir": "./dist/",
+ "sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"allowJs": true
}
}
然后配置 webpack.config.js 文件,讓 webpack 提取 source map,并內聯到最終的 bundle 中:
const path = require('path');
module.exports = {
entry: './src/index.ts',
+ devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
八、總結
本文較全面介紹了 tsconfig.json 文件的知識,從“什么是 tsconfig.js 文件”開始,一步步帶領大家全面認識 tsconfig.json 文件。
文中通過一個簡單 learnTsconfig 項目,讓大家知道項目中如何使用 tsconfig.json 文件。在后續文章中,我們將這么多的配置項進行分類學習。最后通過幾個常見配置示例,解決我們開發中遇到的幾個常見問題。
路由設計
本則路由考慮驗證進入登錄頁面,完成登錄操作進入首頁。
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);
import store from "@/store/store";
// (延遲加載)
const Login = () => import("@/views/login");
const Home = () => import("@/views/home");
const HomeRoute = {
path: "/",
name: "首頁",
component: Home
};
export { HomeRoute };
const router = new Router({
base: process.env.BASE_URL,
routes: [
{
path: "/login",
name: "登錄",
component: Login
},
HomeRoute
]
});
router.beforeEach((to, from, next) => {
let loginName = store.state.user.loginName;
if (to.path === "/" && loginName == "") {
next("/login");
} else {
next();
}
});
export default router;
數據模型
const state = {
loginName: ""
};
const mutations = {
SET_LOGINNAME(state, loginName) {
state.loginName = loginName;
}
};
const actions = {
login({ commit }, userInfo) {
return new Promise((res, ret) => {
commit("SET_LOGINNAME", userInfo);
res();
});
},
logout({ commit }) {
return new Promise((res, ret) => {
commit("SET_LOGINNAME", "");
res();
});
}
};
export default {
namespaced: true,
state,
mutations,
actions
};
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
import user from "./modules/user";
const store = new Vuex.Store({
modules: {
user
}
});
export default store;
組件
<div class="modify">
<input
type="text"
@keydown.enter.prevent="handleKeydown"
v-model="currentVal"
placeholder="使用enter鍵切換頻道"
/>
<button @click="reset" style="margin-left:5px;outline:none;cursor:pointer;">復位</button>
</div>
import { mapState, mapMutations, mapActions } from "vuex";
export default {
name: "login",
data() {
return {
currentVal: "",
list: ["咨詢服務", "音悅臺", "體育臺", "財經頻道", "時尚資訊"],
index: 0
};
},
computed: {
...mapState({
loginName: state => state.user.loginName
})
},
methods: {
...mapActions({
login: "user/login"
}),
handleToHome() {
let userInfo = "user";
this.login(userInfo);
this.$router.push({
path: "/"
});
},
藍藍設計的小編 http://www.syprn.cn