### JavaScript 非同步 ###### Ajax, Fetch, RxJS ---- #### 課程聊天室:https://tlk.io/mrtjs ![課程聊天室](https://i.imgur.com/R9qiCXX.png) ---- ### 環境建置 0. 打開 vscode 並利用 `ctrl+j` 打開命令列視窗 1. 在命令列執行利用 npm 安裝 `json-server` `npm install -g json-server` 2. 在命令列執行利用 npm 安裝 `lite-server` `npm install --global lite-server` 3. 創建資料夾 js-async ---- 4. 在 js-async 創建檔案 `db.json` ```json { "posts": [ { "id": 1, "title": "json-server", "author": "typicode" } ], "comments": [ { "id": 1, "body": "some comment", "postId": 1 } ], "profile": { "name": "typicode" } } ``` 5. 在 js-async 創建檔案 `bs-config.json` 並貼上以下檔案 ```json { "port": 8000 } ``` ---- 6. 在 js-async 創建檔案 `index.html` 並貼上以下檔案 ```htmlmixed= <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> <script> // write your code below </script> <script> </script> <title>Document</title> </head> <body> </body> </html> ``` ---- 7. 在命令列執行 `json-server --watch db.json` 8. 點擊分割視窗按鈕 ![](https://i.imgur.com/wzA7OSD.png) 9. 在命令列執行 `lite-server` --- ### Ajax ###### asynchronous javascript and xml ###### 泛指瀏覽器提供的非同步功能 ---- ###### XMLHttpRequest - GET HTML ```javascript=1 function reqListener (e) { console.log(e.target.responseText); } var oReq = new XMLHttpRequest(); oReq.addEventListener("load", reqListener); oReq.open("GET", "https://hackmd.io/"); oReq.send(); ``` ---- ###### XMLHttpRequest - GET JSON ```javascript=1 function reqListener (e) { console.log(e.target.responseText); } var oReq = new XMLHttpRequest(); oReq.addEventListener("load", reqListener); oReq.open("GET", "http://localhost:3000/posts"); oReq.send(); ``` ---- ###### XMLHttpRequest - POST JSON ```javascript=1 function reqListener (e) { console.log(e.target.responseText); } var oReq = new XMLHttpRequest(); oReq.addEventListener("load", reqListener); // 準備資料 var data = {}; data.title = 'The Adventures of Tom' data.author = 'Mark Twain' oReq.open("POST", "http://localhost:3000/posts"); oReq.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); oReq.send(JSON.stringify(data)); ``` ---- ###### XMLHttpRequest - PUT JSON ```javascript=1 function reqListener (e) { console.log(e.target.responseText); } var oReq = new XMLHttpRequest(); oReq.addEventListener("load", reqListener); // 準備資料 var data = {}; data.title = 'The Adventures of Tom - updated' data.author = 'Mark Twain' oReq.open("PUT", "http://localhost:3000/posts/1"); oReq.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); oReq.send(JSON.stringify(data)); ``` ---- ###### XMLHttpRequest - DELETE ```javascript=1 function reqListener (e) { console.log(e.target.responseText); } var oReq = new XMLHttpRequest(); oReq.addEventListener("load", reqListener); oReq.open("DELETE", "http://localhost:3000/posts/1"); oReq.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); oReq.send(); ``` ---- ### Exercise > ###### 請於新增單一則 post 完成並收到回應後,再發出拿到所有 post 的請求,當拿到貼文後,再發出刪除最後一篇貼文的請求 ---- ```javascript=1 function reqListener () { var oReq2 = new XMLHttpRequest(); oReq2.addEventListener("load", function(){ var oReq3 = new XMLHttpRequest(); oReq3.addEventListener("load", function(e){ console.log(e.target.responseText); }); oReq3.open("DELETE", "http://localhost:3000/posts/5"); oReq3.send(); }); oReq2.open("GET", "http://localhost:3000/posts"); oReq2.send(); } var oReq = new XMLHttpRequest(); oReq.addEventListener("load", reqListener); var data = {}; data.title = 'The Adventures of Tom' data.author = 'Mark Twain' oReq.open("POST", "http://localhost:3000/posts"); oReq.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); oReq.send(JSON.stringify(data)); ``` --- JQuery Ajax ---- ![jqXHR](https://i.imgur.com/Yj6R65l.png) ---- > ###### The $.ajax() function underlies all Ajax requests sent by jQuery. It is often [unnecessary]() to directly call e.target function, as several higher-level alternatives like $.get() and .load() are available and are easier to use. If less common options are required, though, $.ajax() can be used more flexibly. > ... ---- > ... > ###### The jqXHR Object - The jQuery XMLHttpRequest (jqXHR) object returned by $.ajax() as of jQuery 1.5 is a [superset of the browser's native XMLHttpRequest object](). For example, it contains responseText and responseXML properties, as well as a getResponseHeader() method. When the transport mechanism is something other than XMLHttpRequest (for example, a script tag for a JSONP request) the jqXHR object simulates native XHR functionality where possible. > [name=[jQuery ajax](https://api.jquery.com/jquery.ajax/)] ---- ###### Jquery ajax - GET JSON ```javascript=1 // 寫法 1 $.ajax({ method: 'GET', url: 'http://localhost:3000/posts' }) .done(function(value) { console.log('success', value); }) .fail(function(value) { console.log('error', value); }) .always(function(value) { console.log('complete', value); }) // 寫法 2 $.get('http://localhost:3000/posts') .done(function(value) { console.log('success', value); }) .fail(function(value) { console.log('error', value); }) .always(function(value) { console.log('complete', value); }); // 寫法 3 $.get( "http://localhost:3000/posts", function( value ) { console.log('success', value); }); ``` ---- ###### Jquery ajax - POST JSON ```javascript=1 // 寫法 1 $.ajax({ method: 'POST', url: 'http://localhost:3000/posts', data: { title: 'The Adventures of Tom', author: 'Mark Twain' } }) .done(function(value) { console.log('success', value); }) .fail(function(value) { console.log('error', value); }) .always(function(value) { console.log('complete', value); }); // 寫法 2 $.post('http://localhost:3000/posts', { title: 'The Adventures of Tom', author: 'Mark Twain' }) .done(function(value) { console.log('success', value); }) .fail(function(value) { console.log('error', value); }) .always(function(value) { console.log('complete', value); }); // 寫法 3 $.post( 'http://localhost:3000/posts', { title: 'The Adventures of Tom', author: 'Mark Twain' }, function(value) { console.log('success', value); } ); ``` ---- ###### Jquery ajax - PUT ```javascript=1 $.ajax({ method: 'PUT', url: 'http://localhost:3000/posts/3', data: { title: 'The Adventures of Tom3', author: 'Mark Twain3' } }) .done(function(value) { console.log('success', value); }) .fail(function(value) { console.log('error', value); }) .always(function(value) { console.log('complete', value); }); ``` ---- ###### Jquery ajax - DELETE ```javascript=1 $.ajax({ method: 'DELETE', url: 'http://localhost:3000/posts/1', }) .done(function(value) { console.log('success', value); }) .fail(function(value) { console.log('error', value); }) .always(function(value) { console.log('complete', value); }); ``` ---- ### Exercise > ###### 請於新增單一則 post 完成並收到回應後,再發出拿到所有 post 的請求,當拿到貼文後,再發出刪除最後一篇貼文的請求 ---- > ###### post 完成,拿到新增貼篇的 id ,再由此 id 發出 request 拿到評論,再發出 request 拿到個 like ...... ---- ### 巢狀 callback 問題 - [callback hell](https://medium.com/front-end-weekly/one-promise-to-rule-them-all-f1eb7f35603a) ![callback hell](https://i.imgur.com/Nmcy0hM.jpg) --- Promise > ###### 非同步運算的最終完成或失敗的物件 ---- ```javascript=1 function myAsyncFunction(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() xhr.open("GET", url) xhr.onload = () => resolve(xhr.responseText); xhr.onerror = () => reject(xhr.statusText) xhr.send() }); } new myAsyncFunction('http://localhost:3000/posts/1') .then(function(e){ console.log(e); return new myAsyncFunction('http://localhost:3000/posts/2') }) .then(function(e){ console.log(e); return new myAsyncFunction('http://localhost:3000/posts/3') }) .catch(function(e){ console.log(e); }); ``` ---- ### 使用方式 1. 經由 then 呼叫成功後的邏輯 2. 經由 catch 呼叫失敗後的邏輯 3. 每一個 then 會回傳新的 promise 4. catch 會抓住錯誤,並執行一次錯誤邏輯 ---- ### 特性分析 1. ES6 2. 多種 api, library 實作 3. 從水平延伸走向縱向延伸 4. 解決 callback hell 5. 解決 callback hell 的錯誤處理問題 6. 大部分開發者不需要自訂 Promise 如何實作,而是使用建立好的 Promise ---- [fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) > ###### Fetch API 提供了工具使操作 http pipeline 更加容易, 像是日常會用到的發送和接送資料都可以使用。並且有 global 的 fetch() 可以直接呼叫, 使開發能夠用更簡潔的語法取得非同步資料。 ---- 1. fetch() 回傳的 promise 物件, 404, 500 -> resolve 2. fetch 預設上不傳送或接收任何 cookies,需要使用 cookies 必須額外設定 credentials。 ---- fetch get ```javascript=1 fetch('http://localhost:3000/posts') .then(function(response) { return response.json(); }) .then(function(myJson) { console.log(myJson); }); ``` ---- fetch post ```javascript=1 postData('http://localhost:3000/posts', { title: 'The Adventures of Tom', author: 'Mark Twain' }) .then(data => console.log(data)) // JSON from `response.json()` call .catch(error => console.error(error)) function postData(url, data) { // Default options are marked with * return fetch(url, { body: JSON.stringify(data), // must match 'Content-Type' header cache: 'no-cache', // 決定 request 的 cache 機制 *default, no-cache, reload, force-cache, only-if-cached credentials: 'same-origin', //決定是否送出憑證 include, same-origin, *omit headers: { 'content-type': 'application/json' }, method: 'POST', // *GET, POST, PUT, DELETE, etc. mode: 'cors', // 是否跨域, no-cors, cors, *same-origin redirect: 'follow', // 決定如 status code 301,302 發生時,處理邏輯 manual, *follow, error referrer: 'no-referrer', // 發出請求時的位置 *client, no-referrer }) .then(response => response.json()) // 輸出成 json } ``` ---- fetch put ```javascript=1 putData('http://localhost:3000/posts/1', { title: 'The Adventures of Tom1', author: 'Mark Twain1' }) .then(data => console.log(data)) // JSON from `response.json()` call .catch(error => console.error(error)) function putData(url, data) { // Default options are marked with * return fetch(url, { body: JSON.stringify(data), // must match 'Content-Type' header cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached credentials: 'same-origin', // include, same-origin, *omit headers: { 'content-type': 'application/json' }, method: 'PUT', // *GET, POST, PUT, DELETE, etc. mode: 'cors', // no-cors, cors, *same-origin redirect: 'follow', // manual, *follow, error referrer: 'no-referrer', // *client, no-referrer }) .then(response => response.json()) // 輸出成 json } ``` ---- fetch delete ```javascript=1 deleteData('http://localhost:3000/posts/2') .then(data => console.log(data)) // JSON from `response.json()` call .catch(error => console.error(error)) function deleteData(url, data) { // Default options are marked with * return fetch(url, { method: 'DELETE', // *GET, POST, PUT, DELETE, etc. mode: 'cors', // no-cors, cors, *same-origin }) .then(response => response.json()) // 輸出成 json } ``` ---- ### Exercise - fetch 練習 > ###### 請於新增單一則 post 完成並收到回應後,再發出拿到所有 post 的請求,當拿到貼文後,再發出刪除最後後一篇貼文的請求 ---- ### Exercise - fetch 練習 > ###### 請嘗試更新 id 為 100 的貼文,完成並收到回應後,再發出拿到所有 post 的請求,如果發生錯誤請使用console.error() 抓住錯誤 --- RxJS ---- ### [rx - reactive programming](http://reactivex.io/) ![](https://i.imgur.com/emHm392.png) ---- ### Observer pattern ###### [監聽資料流的變化,推送給訂閱者](https://weihanglo.tw/posts/2017/intro-rx-0-reactivex/) ---- ### [Iterator pattern](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators) ###### 遍歷資料、資料流結束、錯誤發生 ```javascript=1 function makeRangeIterator(start = 0, end = Infinity, step = 1) { let nextIndex = start; let iterationCount = 0; const rangeIterator = { next: function() { let result; if (nextIndex < end) { result = { value: nextIndex, done: false } nextIndex += step; iterationCount++; return result; } return { value: iterationCount, done: true } } }; return rangeIterator; } let it = makeRangeIterator(1, 10, 2); let result = it.next(); while (!result.done) { console.log(result.value); // 1 3 5 7 9 result = it.next(); } console.log("Iterated over sequence of size: ", result.value); ``` ---- ### [functional programming](https://blog.jerry-hong.com/series/fp/think-in-fp-02/) 1. 一等公民 (First-Class) - function 可以被當成參數傳入 function 2. 引用透明 (Referential Transparency) - 相同的參數,相同的 output ---- ### RxJS 範例 - version 6 ###### 使用 [StackBlitz](https://stackblitz.com/) ```javascript=1 import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; const observable = new Observable(subscriber => { subscriber.next(1); subscriber.next(2); subscriber.next(3); setTimeout(() => { subscriber.next(4); subscriber.complete(); }, 1000); }); console.log('just before subscribe'); const observer = { next: function(x) { console.log('got value ' + x); }, error: function(err) { console.error('something wrong occurred: ' + err); }, complete: function() { console.log('done'); } } const subscription = observable .pipe(map(item => (+item + 1))) .subscribe(observer); console.log('just after subscribe'); ``` ---- ### Exercise > ###### 請接續上一範例,不要讓 observable 在 got value 5 的時候結束,延後一秒,產生 got value 6 ,再結束 ---- [核心概念](https://www.slideshare.net/WillHuangTW/angular-2-advanced-topic-rxjs) 1. Observable - 可觀察的物件 2. Observer - 觀察者物件 3. Subscription - 訂閱物件 4. Operators - 運算子 5. Subject - 廣播資料傳個多個 observer 6. Schedulers - 控制事件併發狀況 ---- ###### 發出 request ```javascript=1 import { ajax } from 'rxjs/ajax'; import { map, catchError } from 'rxjs/operators'; import { of } from 'rxjs'; const obs$ = ajax(`http://localhost:3000/posts`).pipe( map(userResponse => userResponse.response), catchError(error => { console.log('error: ', error); return of(error); }) ); obs$.subscribe( val => console.log(val) ) ``` ---- ###### 合併多個訊號源 ```javascript=1 import { merge, interval } from 'rxjs'; import { take, tap } from 'rxjs/operators'; const timer1 = interval(3000).pipe(tap(val => console.log('timer1'), take(3))); const timer2 = interval(5000).pipe(tap(val => console.log('timer2'), take(2))); const merged = merge(timer1, timer2); merged.subscribe(x => console.log(x)); ``` ---- ###### 合併多個訊號源,並控制 response 順序 ```javascript=1 import { of, concat, merge } from 'rxjs'; import { ajax } from 'rxjs/ajax'; import { map, catchError, delay } from 'rxjs/operators'; const obs$ = ajax(`http://localhost:3000/comments`).pipe( delay(1000), map(userResponse => userResponse.response), catchError(error => { console.log('error: ', error); return of(error); }) ); const obs1$ = ajax(`http://localhost:3000/profile`).pipe( map(userResponse => userResponse.response), catchError(error => { console.log('error: ', error); return of(error); }) ); const obs2$ = ajax(`http://localhost:3000/posts`).pipe( map(userResponse => userResponse.response), catchError(error => { console.log('error: ', error); return of(error); }) ); const merged = concat(obs$, obs1$, obs2$); merged.subscribe(x => console.log(x)); ``` ---- ###### ajax post ```javascript=1 import { ajax } from 'rxjs/ajax'; import { of } from 'rxjs'; const users = ajax({ url: 'https://httpbin.org/delay/2', method: 'POST', headers: { 'Content-Type': 'application/json', }, body: { rxjs: 'Hello World!' } }).pipe( map(response => console.log('response: ', response)), catchError(error => { console.log('error: ', error); return of(error); }) ); ``` ---- ###### 單一訊號源多個訂閱 ```javascript=1 import { merge, interval, timer } from 'rxjs'; import { take, tap } from 'rxjs/operators'; const timer1 = interval(2000).pipe(take(3)); timer1.subscribe(x => console.log(x)); setTimeout(function() { timer1.subscribe(x => console.log(`%c三秒後 ${x}`,"color: red")); }, 2500); ``` > ###### 後來的訂閱從資料源頭開始 ---- ### subject ```javascript=1 import { Subject, interval } from 'rxjs'; const subject = new Subject(); interval(2000).subscribe(val => subject.next(val)); subject.subscribe({ next: (v) => console.log(`%cobserverA: ${v}`, 'color: blue') }); setTimeout(() => { subject.subscribe({ next: (v) => console.log(`%cobserverB: ${v}`, 'color: red') }); }, 4000); ``` > ###### 後來的訂閱從最新的資料狀態開始 ---- ### Scheduler - 訂閱排程管理 ```javascript=1 import { Observable, asyncScheduler } from 'rxjs'; import { observeOn } from 'rxjs/operators'; const observable = new Observable((observer) => { observer.next(1); observer.next(2); observer.next(3); observer.complete(); }).pipe( observeOn(asyncScheduler) ); console.log('just before subscribe'); observable.subscribe({ next(x) { console.log('got value ' + x) }, error(err) { console.error('something wrong occurred: ' + err); }, complete() { console.log('done'); } }); console.log('just after subscribe'); ``` ---- ###### [Scheduler 類型](https://blog.kevinyang.net/2018/08/31/rxjs-scheduler/) 1. queue 2. asap 3. async 4. [animationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) ---- Ref: 1. [Using XMLHttpRequest ](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest) 2. [callback hell](https://medium.com/front-end-weekly/one-promise-to-rule-them-all-f1eb7f35603a) 3. [All You Need Is Function](https://blog.jerry-hong.com/series/fp/think-in-fp-02/) 4. [前端工程研究:理解函式編程核心概念與如何進行 JavaScript 函式編程](https://blog.miniasp.com/post/2016/12/10/Functional-Programming-in-JavaScript) 5. [reactivex](http://reactivex.io/) 6. [希望是最淺顯易懂的 RxJS 教學](https://blog.techbridge.cc/2017/12/08/rxjs/) 7. [MDN - Iterators and Generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators) 8. [Rx 入門零:ReactiveX](https://weihanglo.tw/posts/2017/intro-rx-0-reactivex/) 9. [Angular 2 開發實戰:進階開發篇 - RxJS 新手入門](https://www.slideshare.net/WillHuangTW/angular-2-advanced-topic-rxjs) 10. [RxJS Scheduler](https://blog.kevinyang.net/2018/08/31/rxjs-scheduler/) ---- # JavaScript Async [範例練習](https://hackmd.io/@ChadZ/JS-MRT-4-EXERCISE)
{"metaMigratedAt":"2023-06-15T02:54:50.310Z","metaMigratedFrom":"YAML","title":"JavaScript Async","breaks":true,"slideOptions":"{\"title\":\"JavaScript 非同步\",\"theme\":\"night\",\"transition\":\"slide\",\"spotlight\":{\"enabled\":false},\"transitionSpeed\":\"fast\"}","contributors":"[{\"id\":\"7da0a1a8-6add-40a7-9acc-950070ff069c\",\"add\":25842,\"del\":7076}]"}
    485 views