首先callback hell
callback hell叫做'回呼地獄',是指在處理多個相依的非同步操作時,過度巢狀使用回呼函數(callback functions)導致程式碼變得難以閱讀、理解和維護,形成金字塔形的縮排結構,也稱作「金字塔地獄」(Pyramid of Doom)。
例如:
需求: 使用者點按鈕 → 送 API → 拿回資料再更新畫面
button.addEventListener("click", () => {
sendRequest(data => {
parseData(parsed => {
updateUI(() => {
console.log("完成")
})
})
})
})
像這樣一層包一層的結構,就是 callback hell 的樣子。
Promise:
Promise的定義是物件代表一個即將完成、或失敗的非同步操作,以及它所產生的值。
意思是 Promise 代表 在進行非同步請求時,「成功的結果」或「失敗的原因」。
所以Promise只會有fulfilled(成功,拿到 value)跟 rejected(失敗,拿到 error)還有 一個中間狀態pending,等待中。
那基本語法就是:then/catch/finally
那建立基本的用法:
const p = new Promise((resolve, reject) => {
})
//resolve 為成功取得的資料時,回傳資料。
//reject 則會在失敗時回傳 error object。
每當傳入這兩個參數,函式就會直接執行,並在成功時執行resolve,在失敗時執行reject。
而promise很常被搭配使用兩個function:
- .then() 接住 resolve 的結果。
- .catch() 接住 reject 的結果。
例如:
成功:
let example = new Promise((resolve, reject) => {
//這邊Promise 可以想成:「我保證之後會給你結果」 的容器
resolve({type: "object"}); });
//Promise 立刻成功,成功的資料是一個物件 {type:"object"}
example.then((data)=>{console.log(data);});
//example.then,這邊.then 是「當 Promise 成功時,要做什麼」
//(data)就是你當初 resolve(...) 傳出去的那個資料:{ type: "object" }
//輸出{ type: "object" }
失敗:
let example = new Promise((resolve, reject) => {
//建立了一個 Promise
//Promise 內部會給你兩個「控制結果」的函式:
//resolve(value):代表成功
//reject(error):代表失敗
reject(new Error("not allowed"));
//代表這個 Promise 立刻變成失敗(rejected)
//並且失敗原因是一個 Error 物件,訊息是 "not allowed"
});
example.then((data)=>{console.log(data);})
//example.then,.then 只會在 Promise 成功(resolve) 時執行
//但你前面是 reject,所以這個 .then 會被跳過,不會跑
example.catch((error) => {console.log(error);});
//.catch 只會在 Promise 失敗(reject) 時執行
//所以它會跑,並印出錯誤物件
promise的function是可以被串接起來的,叫做 Promise Chain。
舉例:
let example = new Promise((resolve, reject) => {
resolve({ string : "Hello" });
});
example.then((data)=>{console.log(data);})
//先印出物件 { string: "Hello" }
example.then(() => {console.log("HI");})
//再印出 "HI"
//而第二個會跑是因為第一個 .then(...) 成功執行完之後
//它會回傳一個「新的 Promise」
//你在第一個 then 裡沒有 throw 錯誤
//所以它是「成功狀態」
//於是下一個 .then(...) 就接著跑
example.catch((error) => {console.log(error);});
//只要Promise失敗,就會跳到這邊處理錯誤
async/await:
async:
- 放在函式定義前,表示該函式是非同步的。
- async 函式總是返回一個 Promise。
- 若函式返回一個值,該 Promise 狀態為 resolved。
- 若拋出錯誤,該 Promise 狀態為 rejected。
await:
- 只能在 async 函式內部使用。
- 用於等待一個 Promise 的結果。
- 會暫停 async 函式執行,直到 Promise 解析完成,並返回解析後的值。
舉例:
//基本語法
async function name([param[, param[, ... param]]]) {
statements
}
// name 函式名稱。
// param 傳遞至函式的參數名稱。
// statements 組成該函式主體的陳述。
原本的原本 Promise 的寫法(then / catch):
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Hello");
}, 1000);
});
}
getData()
.then((data) => {
console.log("成功:", data);
})
.catch((error) => {
console.log("失敗:", error);
});
//.then(data => ...) → 拿 resolve 的值
//.catch(error => ...) → 拿 reject 的錯誤
用 async / await:
async function run() {
try {
const data = await getData(); //等 Promise resolve
console.log("成功:", data);
} catch (error) {
console.log("失敗:", error); //接 Promise reject
}
}
run();
//await getData()
//等 Promise 完成,直接拿到 resolve 的值
//如果 Promise 失敗
//會丟進 catch(就像 throw error)
那還有一個 fetch
fetch 就是用來「向後端拿資料(API)」的工具,而且它「回傳 Promise」
使用async/await 寫:
async function loadTodo() {
try {
const response = await fetch("網址");
const data = await response.json();
console.log(data);
} catch (error) {
console.log("錯誤", error);
}
}
loadTodo();
那我們來做直接範例:
api網址:https://randomuser.me/
這個是這個api裡面package,所包的東西。
因為我們只想要取裡面的圖片,而包住整個圖片內容的東西叫picture,而我們只要使用最大張large就可以了!
api裡面的東西範例:
{
"results": [
{
"gender": "female",
"name": {
"title": "Miss",
"first": "Jennie",
"last": "Nichols"
},
"location": {
"street": {
"number": 8929,
"name": "Valwood Pkwy",
},
"city": "Billings",
"state": "Michigan",
"country": "United States",
"postcode": "63104",
"coordinates": {
"latitude": "-69.8246",
"longitude": "134.8719"
},
"timezone": {
"offset": "+9:30",
"description": "Adelaide, Darwin"
}
},
"email": "[email protected]",
"login": {
"uuid": "7a0eed16-9430-4d68-901f-c0d4c1c3bf00",
"username": "yellowpeacock117",
"password": "addison",
"salt": "sld1yGtd",
"md5": "ab54ac4c0be9480ae8fa5e9e2a5196a3",
"sha1": "edcf2ce613cbdea349133c52dc2f3b83168dc51b",
"sha256": "48df5229235ada28389b91e60a935e4f9b73eb4bdb855ef9258a1751f10bdc5d"
},
"dob": {
"date": "1992-03-08T15:13:16.688Z",
"age": 30
},
"registered": {
"date": "2007-07-09T05:51:59.390Z",
"age": 14
},
"phone": "(272) 790-0888",
"cell": "(489) 330-2385",
"id": {
"name": "SSN",
"value": "405-88-3636"
},
"picture": {
"large": "https://randomuser.me/api/portraits/men/75.jpg",
"medium": "https://randomuser.me/api/portraits/med/men/75.jpg",
"thumbnail": "https://randomuser.me/api/portraits/thumb/men/75.jpg"
},
"nat": "US"
}
],
"info": {
"seed": "56d27f4a53bd5441",
"results": 1,
"page": 1,
"version": "1.4"
}
}
首先我們要先設定HTML:
HTML:
// HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="index.css">
<title>Document</title>
</head>
<body>
<div class="wrap">
<div class="content">
<button id="test-btn">Click me</button>
<img
id="result-img"
src=""
alt="result"
width="300"
/>
</div>
<div class="word">
<p id="national"></p>
<p id="email"></p>
<p id="gender"></p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
//引入axios
<script src="index.js"></script> // 引入javascript
</body>
</html>
順便設定CSS在上面:
CSS:
//css
.wrap{
display: grid;
justify-items: center;
}
.content{
display: grid;
justify-items: center;
margin-top: 20px;
}
#test-btn{
margin-bottom: 30px;
}
#result-img{
margin-bottom: 30px;
}
那我們javascript要怎麼做呢?
範例如下:
javascript:
//javascript
const button = document.getElementById('test-btn')
const image = document.getElementById('result-img')
const nationalEl = document.getElementById('national')
const emailEl = document.getElementById('email')
const genderEl = document.getElementById('gender')
async function seriesConnectionAPI() {
try{
const response = await axios.get('https://randomuser.me/api/')
// 這邊取api的網址
const user = response.data.results[0]
// 這邊的results是這個api裡面,由於整包叫result
// 因為要取key,使用.result來取
// 前面我們使用變數response來取得api
// 所以這邊使用const user的方式來取整包
// 所以用user = response.data.result
// response這上面api的網址
// .data是指為了取出此結構中名為 "data" 的屬性
// 而.reslts是指整包叫result的東西
const photo = user.picture.large
// 這邊的取圖片
// 我們定義變數photo,去取裡面的內容
// user是裡面整包的東西
// picture是整包裡面都是包圖片的,所以我們使用.picture來取
// 然後我們只要最大張,所以使用.large
console.log(user) // 我們先印user
image.src = photo // 取圖片photo
nationalEl.textContent = `${user.location.country} 、 ${user.location.city}`
emailEl.textContent = `Email: ${user.email}`
genderEl.textContent = `Gender: ${user.gender}`
} catch(error){
console.error(error)
}
}
seriesConnectionAPI()
button.addEventListener('click' , seriesConnectionAPI)
那如果我們後面需要取很多資料的時候我們該怎麼做呢?
我們先看api:https://picsum.photos/v2/list
基本HTML組成:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Template</title>
<!-- CSS import below -->
</head>
<body>
<!-- Write your HTML here -->
<button id="test_bun">點擊我</button>
<img
id="test_img"
src=""
alt=""
width="300"
>
<div id="img_list"></div>
<!-- JS import below -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="test_01.js"></script>
</body>
</html>
javascript:
const buttonEl = document.getElementById('test_bun')
const imgEl = document.getElementById('test_img')
const imgListEl = document.getElementById('img_list')
const apiUrl = 'https://picsum.photos/v2/list'
// API網址
async function testAPI() {
try{
const response = await axios.get(apiUrl)
// 定義一個變數response去接住axios.get拿到的API
const user = response.data
// 定義變數user,去拿到api裡面的資料
// 那如果是只單純要拿第一個就這樣寫:const user = response.data[0]
const fivePhoto =
user.slice().sort(() => Math.random() -0.5).slice(0, 5)
// 我們先使用user.slice()
// 而這邊的.slice()會將user裡面的陣列給複製一份
// 而.sort()這段在做什麼呢?
// .sort()這邊會將原本陣列裡面的元素,我們想排列的方式重新排列
// 它會改變原本的陣列。
// 當回傳 < 0:a 排在 b 前面,而回傳 > 0:a 排在 b 後面
// 而後面.sort(() => Math.random() -0.5)的Math.random() -0.5在做什麼呢?
// Math.random()他是個產生亂數的東西,範圍會收成-0.5 ~ +0.5
// 再讓.sort 每次比較都「隨機決定前後順序」
// 而.slice(0, 5),.slice()的括弧裡面在做的事是,我們先預設.slice(a, b)
// 設定裡面的參數從哪裡開始.slice(a, b)裡面的a
// 設定到哪邊結束就是.slice(a, b)裡面的b
// 但是這邊要注意的事情是.slice()也是會回傳新陣列的,但是原本的陣列不會進行修改
// 而這邊設定.slice(0, 5),就是取前五筆的五筆資料
// 在設定變數 fivePhoto 去接住結果。
// 總之這整段在做的事情是:
// 先複製user裡面的陣列再透過.sort(() => Math.random() -0.5方式去打亂
// 在使用.slice()去取前五筆資料
imgListEl.innerHTML = ''
// 這邊.innerHTML代表「某個元素裡面包含的 HTML 內容(字串)」
// 他會將前面html裡面設定的<div id="img_list"></div>的東西清空
console.log('user:', user)
fivePhoto.forEach((photograph) => {
// 這邊使用forEach來遍歷fivePhoto裡面的資料,而箭頭函式裡面的photograph代表裡面的物件
const img = document.createElement('img')
// 使用變數 img 在記憶體中做出一個 img 元素
img.src = photograph.download_url
// 然後再取圖片的網址出來塞進img.src裡面
img.width = 300
// 圖片顯示寬度設定為 300
const author = document.createElement('photograph')
// 使用變數 author 在記憶體中做出一個 photograph 元素
author.textContent = `'Author': ${photograph.author}`
// 設定作者的文字內容,.textContent 會在裡面設定純文字內容
// 而樣板字串裡面 photograph.author,取裡面的作者 author
imgListEl.appendChild(img)
// .appendChild 這個會將變數 img 這個子元素加到容器裡
imgListEl.appendChild(author)
// .appendChild 這個會將變數 author 這個子元素加到容器裡
})
console.log('imgEl:', imgEl)
console.log('response:',response)
}catch(error){
console.log(error)
}
}
testAPI() // 進行函式輸出
buttonEl.addEventListener('click', testAPI)
// 將按鈕綁定,去click觸及函式 testAPI















