setState()
是非同步更新 statethis.setState({
name: data.name,
});
需要改成
this.setState(() => ({
name: data.name,
}));
昨天介紹了 React 和完成用 create-react-app
建立了第一個 React app
在開始前,我們先來談談「非同步」
hello-react
,在 public
資料夾建立一個檔案 ayncRequest.html
:內容如下
<!-- ayncRequest.html -->
<html>
<body>
<div>before script</div>
<script type="text/javascript">
console.log('hi'); // 在 Console 印出 "hi"
</script>
<div>after script</div>
</body>
</html>
npm run start
http://localhost:3000/ayncRequest.html
剛剛的頁面console.log()
會印出訊息在 Console 中瀏覽器頁面渲染到一半,一看到 <script/>
就會立刻執行裡面的 javascript,也就是說 console.log()
立刻被執行
對於頁面渲染,console.log() 是同步地被執行。
試試看:你可以用 debug 模式証實它真的有停在那裡。若不會操作,可以看 Day 10- 一周目- 開始玩轉前端(一)
修改 ayncRequest.html
加入 setTimeout()
<!-- ayncRequest.html -->
<html>
<body>
<div>before script</div>
<script type="text/javascript">
console.log('hi'); // 在 Console 印出 "hi"
setTimeout(() => {
console.log('time up'); // 兩秒後印出 "time up"
}, 2000);
</script>
<div>after script</div>
</body>
</html>
再次刷新頁面
我們發現瀏覽器頁面渲染完後,等二秒後才會執行 console.log('time up')
。
瀏覽器在執行渲染時, 下面的箭頭函
() => {
console.log('time up'); // 兩秒後印出 "time up"
}
被 setTimeout()
存在某個地方後,頁面渲染結束後(看到 before script / after script),等個兩秒,箭頭函數才被執行。此時的箭頭函數叫 callback function。
對於頁面渲染,console.log() 是非同步地被執行。因為箭頭函數的執行不是透過「頁面渲染」執行的。
下面的專案很有趣 loupe by Philip Roberts,可以看到瀏覽器非同步執行過程。
很有趣的是,我找不到有人給出非同步函數正式定義,因為非同步函數無法單獨定義,它是由行為所導致結果,如:「頁面渲染」因為 setTimeout()
導致「頁面渲染」是非同步的執行(見:Node.js Asynchronous Function Definition Ask Question)。
本節最後,我說說我對非同步函數的看法是:
當一函數無法立刻得到執行函數的目地,就是非同步函數。一般利用 callback function 回傳結果。
所以未來看到一個函數參數有定義 callback function 大部分都是非同步函數。
以後在二周目我們將會看到如何做出非同步執行的函數:
setTimeout()
…系列上章提出了「非同步」的概念,現在來使用非同步函數,來發出非同步 request 的與伺服溝通。
hello-react
前端開發網頁伺服器hello-express
,它有 POST /api/echo
APIpackage.json
"scripts": {
"start": "PORT=3001 node ./bin/www"
},
PORT
是環境變數,port 換成 3001
npm install cors --save
./app.js
// 加在前面
var cors = require('cors');
// 加在 var app = express(); 之後
app.use(cors()); // 加入一個 middleware
npm run start
前端主機是 http://localhost:3000
後端主機是 http://localhost:3001
你可以用瀏覽器試試是否有正常運作或用 Postman
主要有兩個方法
XMLHttpRequest
物件fetch()
函數
XMLHttpRequest 是比較是很早期就出現的東西,用起來有點麻煩。使用前要建立 XMLHttpRequest 物件,再設定一些 callback,才送出 request。
./src/App.js
,加入 componentDidMount()
成員函數
class App extends Component {
componentDidMount() {
// 送到後端的資料
const data = {
name: 'Billy'
};
// 用 XMLHttpRequest 發起一個非同步的 request
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) { // readyState == 4 為 request 完成
const contentType = xhr.getResponseHeader('content-type');
if (xhr.status === 200 && contentType && contentType.indexOf('application/json') > -1) { // 依回應的資料格式處理,我們只處理 200 && application/json
try {
var result = JSON.parse(xhr.responseText);
console.log(result)
} catch(e) {
console.error(e);
}
} else {
console.error(new Error('無法得到資料'), xhr.responseText);
}
}
}
xhr.open("POST", "http://localhost:3001/api/echo"); // 開啟連線
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); // 設定 header
xhr.send(JSON.stringify(data)); // 以文字字串送出JSON資料
}
render() {
...
}
}
這裡 componentDidMount()
是當 component 的渲染結果(html)插入 DOM tree 中就會呼叫。
使用 XMLHttpRequest
物件,以文字字串送出(send()
) JSON 資料。因為有設定 Content-Type: application/json;charset=UTF-8
,所以後端可以解讀。
每次 request 狀態改變,onreadystatechange
就會被呼叫,當 readyState
等於 4
就是 request 完成(不論成功還是失敗)。更多狀態見:AJAX - onreadystatechange 事件
fetch() 是比較新的函數,支援度也不錯,更重要的是,它可以當函數直接用,回傳 Promise
物件。 Promise
可以說是 javascript 最重要的物件,它包裝非同步操作,只要用 then(resolveCallback).catch(rejectCallback)
,就可以接收非同步的結果。我們在二周目會再次見到它。
把上 使用 XMLHttpRequest
物件的 componentDidMount()
,全部註解掉,貼上下面程式,也會得到一樣的結果
componentDidMount() {
// 送到後端的資料
const data = {
name: 'Billy'
};
// 用 fetch() 發起一個非同步的 request
fetch("http://localhost:3001/api/echo", {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify(data),
})
.then(response => response.json()) // 取出 JSON 資料,並還原成 Object。response.json() 一樣回傳 Promise 物件
.then(data => {
console.log(data);
})
.catch(e => {
console.error(e);
});
}
fetch()
是不是用起來比較自然一點了,可讀性增加了。fetch().then(resolveCallback1).then(resolveCallback2).catch(rejectCallback)
是一種鏈式語法,就是一直串下去要做的事。
流程如下:(你先不要看解釋,看程式就大概可以猜到過程了)
fetch()
一但成功取得資料,假設叫 request
,就會把request
往下一個 then()
裡面的 callback 傳,就如同執行 resolveCallback1(request)
。resolveCallback1(request)
回傳一個由response.json()
產生的 Promise 物件,也會繼續如同之前一樣。 response.json()
成功時取得資料,假設叫 data
,就會往下一個 then()
裡面的 callback 傳,就如同執行 resolveCallback2(data)
。catch(rejectCallback)
中的 rejectCallback
什麼時後被叫呢?就是 fetch()
、 resolveCallback1()
或 resolveCallback2
有任何失敗或產生例外。then(resolveCallback).catch(rejectCallback)
用起來有點像 try...catch
,但強多了,配合 async/await
這語法糖衣,又可以拆掉鍊式語法得到像同步的程式碼。我只寫出結果,看看它利害的地方,以後我們再談它們
componentDidMount() {
// 送到後端的資料
const data = {
name: 'Billy'
};
const workerPromise = (async () => {
// 用 fetch() 發起一個非同步的 request,等待回傳結果
const response = await fetch("http://localhost:3001/api/echo", {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify(data),
})
// 等待 response.json() 回傳的 JSON 物件
const resultData = await response.json();
return resultData;
})();
workerPromise
.then(data => {
console.log(data);
})
.catch(e => {
console.error(e);
});
}
接下來,後端的結果顯示在網頁上。
state
,用來記錄 App component 的內部狀態
state = {
name: '',
}
console.log(data)
後面加入
this.setState({
name: data.name,
});
setState()
會更新的 state 內的值,並引起 App component 重新渲染 render()
。state
,修改 render()
,
render() {
return (
<div className="App" >
Hello React: {this.state.name}
</div >
);
}
完整的 App component 如下:
class App extends Component {
state = {
name: '',
}
componentDidMount() {
// 送到後端的資料
const data = {
name: 'Billy'
};
// 用 fetch() 發起一個非同步的 request
fetch("http://localhost:3001/api/echo", {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify(data),
})
.then(response => response.json()) // 取出 JSON 資料,並還原成 Object。response.json() 一樣回傳 Promise 物件
.then(data => {
console.log(data);
// 更新的 state 內的值,並再一次引起渲染 render()
this.setState({
name: data.name,
});
})
.catch(e => {
console.error(e);
});
}
render() {
return (
<div className="App" >
Hello React: {this.state.name}
</div >
);
}
}
得到結果
今天引進了非同步的概念,並利用 XMLHttpRequest
物件 和 fetch()
函數引發一個 非同步的 request,得到結果後重新渲染畫面。
本篇文章出現 anti-pattern : state 更新是非同步的。
雖然
this.setState({
name: data.name,
});
可以運作,但可能會讓人"誤會" setState()
會立即更新 state
。 比較適合的寫法如下:
this.setState((prevState, props) => ({
name: data.name,
}));
假如你要依序加入後綴(postfix),進行二次 render,會寫二次 this.setState()
// 錯誤寫法,因為 state 還沒更新
this.setState({
name: data.name + '_postfix1',
});
setTimeout(() => {
this.setState({
name: this.state + '_postfix2',
});
}, 2000);
// 正確寫法
this.setState(() => ({
name: data.name + '_postfix1',
}));
setTimeout(() => {
this.setState((prevState) => ({
name: prevState + '_postfix2',
}));
}, 2000);
參考:https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous