20220111のJavaScriptに関する記事は25件です。

【サイト高速化】Lazy Load(画像の遅延読み込み)の導入方法

前提 本記事ではJavaScriptライブラリ(vanilla-lazyload)を利用して、画像の遅延読み込みに対応する方法について記載します。下記の状況を想定しています。 WordPressを利用していない PageSpeed Insightsで改善項目として「オフスクリーン画像の遅延読み込み」を挙げられた ※記載に誤りがあったり、「もっとよい方法があるよ!」という場合はご教授いただけますと幸いです。 Lazy Loadの導入手順 1.vanilla-lazyloadを読み込む 下記GitHubページにアクセスし、verlok氏のvanilla-lazyloadをダウンロードまたはCDNを利用して読み込みます。 https://github.com/verlok/vanilla-lazyload 自分の環境にダウンロードする場合 「code」ボタンをクリックし、「Download ZIP」を選択します。 ダウンロードしたフォルダの中にある「dist」フォルダの「lazyload.min.js」を自分の環境に保存します。 HTMLのbodyタグ内の最後でlazyload.min.jsを読み込みます。 index.html ~ <script src="path/to/lazyload.min.js"></script> </body> CDNを利用する場合 HTMLのbodyタグ内の最後でlazyload.min.jsを読み込みます。 index.html ~ <script src="https://cdn.jsdelivr.net/npm/vanilla-lazyload@17.5.0/dist/lazyload.min.js"></script> </body> 2.imgタグに"lazy"クラスを追加する あとは画像要素に"lazy"クラスを追加するだけで読み込みが遅延されます…なんて便利なんだ。 src属性に「data-」を付けなければならないことにだけ注意が必要です。 <!-- 公式より引用 --> <img alt="A lazy image" class="lazy" data-src="lazy.jpg" /> おまけ "lazy"クラスを使わない方法 以下は、ページに画像が多い&すでにいくつもクラスが付いていて一括置換が面倒…となった人向けです。 1.imgタグすべてを遅延読み込みの対象とする vanilla-lazyloadを読み込んだら、imgタグすべてを遅延読み込みの対象とするオプションを設定します。 index.html ~ <script src="path/to/lazyload.min.js"></script> <script> var lazyloadInstance = new LazyLoad({ elements_selector: "img", }); </script> </body> 2.imgタグで「data-src」「src」を使い分ける 先ほどimgタグすべてを遅延読み込みの対象とする設定を行ないました。しかし、ファーストビューに入る画像については、読み込みを遅延させると逆に表示速度が遅くなり、PageSpeed Insightsの評価も下がってしまいます。 LazyLoadによって画像を読み込むには「data-src」属性を用いることになっているので、これを利用して、遅延読み込みしたくないものは「src」を使うと遅延読み込みの対象から外すことができます。 index.html <!-- ファーストビューに入らない画像(遅延読み込みする) --> <img data-src="path/to/img.jpg" alt="mainImg"> <!-- ファーストビューに入る画像(遅延読み込みしない) --> <img src="path/to/img.jpg" alt="img"> 参考サイトさま
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

親のチェックボックスの状態を子のチェックボックスにも連動させるJavaScriptの書き方

実現したい機能 親のチェックボックスにチェックが入ったら、子のチェックボックスにもチェックを入れる 親のチェックボックスでチェックが外れたら、子のチェックボックスからもチェックが外れる 子のチェックボックスでチェックが入ったり、外れたりしても親のチェックボックスは反応しない サンプルページ Github コード HTML index.html <ul> <li class="checkbox"> <input type="checkbox" name="parent">親チェックボックス <ul> <li class="checkbox"><input type="checkbox" name="child1">子チェックボックス1</li> <li class="checkbox"><input type="checkbox" name="child2">子チェックボックス2</li> <li class="checkbox"><input type="checkbox" name="child3">子チェックボックス3</li> </ul> </li> </ul> JavaScript check.js $(function (){ $('input[name^="parent"]').change(function() { if ($(this).is(':checked')) { $(this).parents('.checkbox').find('input:checkbox').prop('checked', true); }else{ $(this).parents('.checkbox').find('input:checkbox').prop('checked', false); } }); }); 解説 コードの解説 前提として、親のチェックボックスである<input type="checkbox" name="parent">の状態が変化した時、check.jsが発火する。 <input type="checkbox" name="parent">の状態がcheckedになった際、次の処理が行われる。 check.js $(this).parents('.checkbox').find('input:checkbox').prop('checked', true); このコードがそれぞれ何を指すのか分解してみる。 $(this): <input type="checkbox" name="parent">の指定。 parents('.checkbox'): <input type="checkbox" name="parent">の親である<li class="checkbox">の指定。 find('input:checkbox'): <li class="checkbox">の中にある<input type="checkbox">を探す。 prop('checked', true): checked入れる。 以上を踏まえて、文章にしてみると、 <input type="checkbox" name="parent">の親である<li class="checkbox">を探して、その中にある<input type="checkbox">の状態をcheckedにする。 となる。 .prop('checked', false)の場合は、checkedを外す処理が行われる。 仕組みの解説 このコードが何故親のチェックボックスに反応して、子のチェックボックスも反応できるようになるのかというと、親と子における<li>タグと<input>タグで同じclass名を付けているから。 checkedにする動きで考えてみる。 check.js $('input[name^="parent"]').change(function() { if ($(this).is(':checked')) { if文のtrue分岐に入る時点で、親のチェックボックスにはcheckedが入っている。 その状態で、 check.js $(this).parents('.checkbox').find('input:checkbox').prop('checked', true); このコードが動くと、親の状態とかは関係なく、jsは<li class="checkbox"><input type="checkbox">の構成をしている部分をHTMLから探す。 その結果、<li class="checkbox"><input type="checkbox">の構成をしていて、まだcheckedが入っていない、子のチェックボックスにcheckedが入り、親と子で同時にチェックが入る(ように見える)。 流れを簡潔にまとめると、以下のようになる。 1. 親のチェックボックスにチェックが入り、check.jsが発火 2. <li class="checkbox"><input type="checkbox">に該当するHTMLタグの状態をcheckedにする 3. 親にはもうcheckedが入っているから見た目に変化はない。子はcheckedに変化 参考資料 .parent() | jQuery 1.9 日本語リファレンス | js STUDIO 親にチェックするだけで、子や孫のチェックボックスもまとめてチェック出来る | かちびと.net 【脱jQuery】.parents()で親要素よりも上の要素の取得をネイティブJavascriptに書き換え | ウェブラボ(株)スタッフブログ | ウェブラボ(株)スタッフブログ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

シフトで負の値を与えるとどうなるのか?

左シフトと右シフト(符号なし)に負値を与えてみます。 実行環境は macOS 12.1 (Intel版) のターミナル ターミナル $ node Welcome to Node.js v16.13.1. Type ".help" for more information. > (1<<(-1)).toString(16) '-80000000' > (1<<(-2)).toString(16) '40000000' > (1<<(-3)).toString(16) '20000000' > (1<<(-31)).toString(16) '2' > (1<<(-32)).toString(16) '1' > (1<<(-33)).toString(16) '-80000000' > (0x1234<<(-4)).toString(16) '40000000' > (0x1234<<(-8)).toString(16) '34000000' > (0x1234<<(-16)).toString(16) '12340000' > (0x1234<<(-32)).toString(16) '1234' > (0x1234<<(-36)).toString(16) '40000000' > (1>>>(-1)).toString(16) '0' > (0x80000000>>>(-1)).toString(16) '1' > (0x80000000>>>(-2)).toString(16) '2' > (0x80000000>>>(-30)).toString(16) '20000000' > (0x80000000>>>(-31)).toString(16) '40000000' > (0x80000000>>>(-32)).toString(16) '80000000' > (0x80000000>>>(-33)).toString(16) '1' > (0xFEDC0000>>>(-1)).toString(16) '1' > (0xFEDC0000>>>(-4)).toString(16) 'f' > (0xFEDC0000>>>(-8)).toString(16) 'fe' > (0xFEDC0000>>>(-16)).toString(16) 'fedc' > (0xFEDC0000>>>(-32)).toString(16) 'fedc0000' > (0xFEDC0000>>>(-33)).toString(16) '1' > 1<<33 2 という結果が得られました。 仕様がどうなっているのか知らない(定義されてない気がする)けど、実際の挙動は function left_shift(x, n) { return x << (n & 31); } function right_shift(x, n) { return x >> (n & 31); } となっているようです。プロセッサ依存だったりしないのかな?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

インタープリタを作る その32

概要 インタープリタを作ってみた。 jsforthを使ってみた。 sandbox作ってみた。 フィボナッチ数列 : fib 7 0 1 rot 0 do swap over + dup . loop ; ok fib 1 2 3 5 8 13 21 九九 : kuku 10 1 do 10 1 do i j * . loop cr loop ; kuku 1 2 3 4 5 6 7 8 9 2 4 6 8 10 12 14 16 18 3 6 9 12 15 18 21 24 27 4 8 12 16 20 24 28 32 36 5 10 15 20 25 30 35 40 45 6 12 18 24 30 36 42 48 54 7 14 21 28 35 42 49 56 63 8 16 24 32 40 48 56 64 72 9 18 27 36 45 54 63 72 81 fizzbuzz : sp 32 emit ; : fizzbuzz 100 1 do i 3 mod 0 = i 5 mod 0 = or if i 3 mod 0 = if ." fizz" then i 5 mod 0 = if ." buzz" then else i . then sp loop ; fizzbuzz 1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz 16 17 fizz 19 buzz fizz 22 23 fizz buzz 26 fizz 28 29 fizzbuzz 31 32 fizz 34 buzz fizz 37 38 fizz buzz 41 fizz 43 44 fizzbuzz 46 47 fizz 49 buzz fizz 52 53 fizz buzz 56 fizz 58 59 fizzbuzz 61 62 fizz 64 buzz fizz 67 68 fizz buzz 71 fizz 73 74 fizzbuzz 76 77 fizz 79 buzz fizz 82 83 fizz buzz 86 fizz 88 89 fizzbuzz 91 92 fizz 94 buzz fizz 97 98 fizz zundoko : zundoko 0 begin 2 random if ." zun " 1 + else ." doko " drop 0 then dup 4 = until ." doko kiyoshi!" ; ok zundoko zun zun doko zun zun doko doko zun zun zun doko zun doko zun zun zun zun doko kiyoshi! 成果物 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

別の配列から重複しない値の配列を作り直したい

配列A と配列Bにそれぞれ値があるが、配列Aに 含まれる値を 取り除いて配列Bを作り直したい const list = [ { id: 'f7747f80-9386-11eb-89b0', }, { id: 'r7747f80-9386-11eb-89b0', } ]; const list2 = [ {   id: 'f7747f80-9386-11eb-89b0', }, { iid: 'a7747f80-9386-11eb-89b0', } ]; const targetList = list2.filter(o => !list.find(o => o.id === list2.id)); console.debug(1, targetList); const allList = [...list, ...targetList] console.debug(2, allList);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

scikit-learnやlightgbmなどのONNX形式モデルがブラウザで動作しないとき

やりたいこと Pythonで訓練したモデルをONNX形式でブラウザ上(JavaScript)で推論させたい。 使用ライブラリ Python scikit-learn/lightgbm onnxmltools JavaScript onnxruntime-web 問題 JSのサンプルコード通りに書いてもsession.run()部分で4123988のようなエラーが出て動かない。 解決法 PythonでモデルをONNX形式に変換する際、zipmapオプションを無効にしないとブラウザでは動かないらしい。 例えば、LightGBMモデルだとこんな感じ。 import onnxmltools onnx_model = onnxmltools.convert_lightgbm(model, initial_types=initial_types, zipmap=False) 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascriptでRPG#14

こんにちは 今回は、石の採掘を作っていこうと思います では、作っていきます つるはしの大きさ 今回から項目ごとに太字で書いていきます では、前作ったつるはしの大きさが大きすぎると思ったので、小さくします main.js //石のツルハシ表示 if(pl["stone_pickel"]==1&&map_r==3){ ctx.drawImage(system["stone_pickel"],0,0,32,32,0,64,64,64); } この文章を、石のつるはし表示というところと、交換してください これで、つるはしを入手してから、アイテム欄を確認すると、小さくなっています 装備の確認 では、次にいま装備しているアイテムは何かを見れるものを作ります main.js main.js "use strict"; const pl = { "x":0, "y":0, "map":0, "stone_pickel":0 } const system = { "ctx_font":"32px monospace", "ctx_font2":"16px monospace", "talk-1":0, "mapchip_size":32, "pl_animation":0, "mapchip":"", "pl1":"", "stone_pickel":"", "item_box":0, "item_box_i":0, "current_item":0 } let pl_g_up = false; let pl_g_left = false; let pl_g_right = false; let pl_g_down = false; let pl_k = ""; let pl_n = 0; let talk_number=1; let mapdate; let map_r=0; let map_l=1; //メインループ function main_loop() { const canvas = document.getElementById("canvas");//キャンバスの取得 const ctx = canvas.getContext("2d");//2Dコンテキスト取得 ctx.clearRect(0, 0, 640, 640);//canvasの初期化 for( let y = 0; y < mapdate.length; y++ ){ for( let x = 0; x < mapdate[0].length; x++ ){ //マップの表示 ctx.drawImage( system["mapchip"], mapdate[y][x]*32,0,system["mapchip_size"],system["mapchip_size"], x*system["mapchip_size"]-(pl["x"]),y*system["mapchip_size"]-(pl["y"]),32,system["mapchip_size"]); } } //キャラクターの表示 ctx.drawImage(system["pl1"],(system["pl_animation"]%3)*32,(Math.floor(system["pl_animation"]/3))*32, system["mapchip_size"],system["mapchip_size"],320,320,32,system["mapchip_size"]); //フォントを設定 ctx.font = system["ctx_font"]; //移動の準備 if(pl_n==0){ //上 if(pl_g_up&&pl["y"]>-320&&!(move_no.includes(mapdate[pl["y"]/32+9][pl["x"]/32+10]))&&map_r==0){ pl_k="up"; pl_n=32; system["pl_animation"]=9; } //左 if(pl_g_left&&pl["x"]>-320&&!(move_no.includes(mapdate[pl["y"]/32+10][pl["x"]/32+9]))&&map_r==0){ pl_k="left"; pl_n=32; system["pl_animation"]=3; } //下 if(pl_g_down&&!(move_no.includes(mapdate[pl["y"]/32+11][pl["x"]/32+10]))&&map_r==0){ pl_k="down"; pl_n=32; system["pl_animation"]=0; } //右 if(pl_g_right&&!(move_no.includes(mapdate[pl["y"]/32+10][pl["x"]/32+11]))&&map_r==0){ pl_k="right"; pl_n=32; system["pl_animation"]=6; } } //移動の命令・上 if(pl_k=="up"&&pl_n!==0){ pl["y"]-=4; pl_n -= 4; if(system["pl_animation"]==11){ system["pl_animation"]=9; } system["pl_animation"]+=1; } //移動の命令・下 if(pl_k=="down"&&pl_n!==0){ pl["y"]+=4; pl_n -= 4; if(system["pl_animation"]==2){ system["pl_animation"]=0; } system["pl_animation"]+=1; } //移動の命令・左 if(pl_k=="left"&&pl_n!==0){ pl["x"]-=4; pl_n -= 4; if(system["pl_animation"]==5){ system["pl_animation"]=3; } system["pl_animation"]+=1; } //移動の命令・右 if(pl_k=="right"&&pl_n!==0){ pl["x"]+=4; pl_n -= 4; if(system["pl_animation"]==8){ system["pl_animation"]=6; } system["pl_animation"]+=1; } //下の吹き出し表示 ctx.beginPath(); ctx.fillStyle = "rgba(" + [0, 0, 0, 0.5] + ")"; ctx.fillRect(16, 512, 608, 128); //文字を取得し、表示 ctx.font=system["ctx_font"]; ctx.fillStyle = "rgba(" + [255, 255, 255, 1] + ")"; ctx.fillText(talk[Number(talk_number)],16,544) //スペースキーで次へを挿入 if(talk_number==1||talk_number==4||talk_number==6||talk_number==7||talk_number==8||talk_number==9||talk_number==10){ ctx.font=system["ctx_font2"]; ctx.fillStyle = "rgba(" + [255, 255, 255, 1] + ")"; ctx.fillText("スペースキーで次へ",464,624); } //現在持っているアイテムを表示する枠の表示 if(system["item_box"]==0){ ctx.fillStyle = "rgba(" + [0, 0, 0, 1] + ")"; ctx.fillRect(0, 0, 100, 44); ctx.fillStyle = "rgba(" + [0, 0, 0, 1] + ")"; ctx.fillRect(0, 44, 5, 100); ctx.fillStyle = "rgba(" + [0, 0, 0, 1] + ")"; ctx.fillRect(0, 144, 100, 5); ctx.fillStyle = "rgba(" + [0, 0, 0, 1] + ")"; ctx.fillRect(95, 44, 5, 100); ctx.font=system["ctx_font"]; ctx.fillStyle = "rgba(" + [255, 255, 255, 1] + ")"; ctx.fillText("装備中",0,30); } if(system["current_item"]==1){ ctx.drawImage(system["stone_pickel"],0,0,32,32,0,40,114,114); } //Enterで次へを挿入 if(map_r==1){ ctx.font=system["ctx_font2"]; ctx.fillStyle = "rgba(" + [255, 255, 255, 1] + ")"; ctx.fillText("上下矢印キーとEnterで選択",400,624) } //家に入る時のイベント if(map_r==0&&mapdate[pl["y"]/32+9][pl["x"]/32+10]==4){ talk_number=4; }else{ if(map_r==0&&talk_number>=4){ talk_number=5; } } //家に入るときの『はい』と『いいえ』の選択肢の『はいver』 if(map_r==1&&map_l==1){ talk_number=5; ctx.font=system["ctx_font"]; ctx.fillStyle = "rgba(" + [255, 255, 255, 1] + ")"; ctx.fillText("はい←",16,544) ctx.font=system["ctx_font"]; ctx.fillStyle = "rgba(" + [255, 255, 255, 1] + ")"; ctx.fillText("いいえ",16,624) } //家に入るときの『はい』と『いいえ』の選択肢の『いいえver』 if(map_r==1&&map_l==0){ talk_number=5; ctx.font=system["ctx_font"]; ctx.fillStyle = "rgba(" + [255, 255, 255, 1] + ")"; ctx.fillText("はい",16,544) ctx.font=system["ctx_font"]; ctx.fillStyle = "rgba(" + [255, 255, 255, 1] + ")"; ctx.fillText("いいえ←",16,624) } //家に入るときの『はい』と『いいえ』の選択肢の制御用 if(map_l>=2)map_l=0; //家の中のNPC1と喋るとき if(pl["map"]==1&&pl["x"]==64&&pl["y"]==64){ if(system["talk-1"]==0){ //初めて話す場合 system["talk-1"]=1; map_r=2; talk_number=6; } if(system["talk-1"]==1){ //2回目以降で話す場合 } } //アイテム欄の表示 if(system["item_box"]==1){ ctx.beginPath(); ctx.fillStyle = "rgba(" + [0, 0, 0, 0.8] + ")"; ctx.fillRect(0, 0, 640, 320); ctx.beginPath(); ctx.fillStyle = "rgba(" + [156, 156, 156, 1] + ")"; ctx.fillRect(0, 64, 640, 5); ctx.font=system["ctx_font"]; ctx.fillStyle = "rgba(" + [255, 255, 255, 1] + ")"; ctx.fillText("〜アイテム〜",224,32) ctx.font=system["ctx_font2"]; ctx.fillStyle = "rgba(" + [255, 255, 255, 1] + ")"; ctx.fillText("もう一度シフトキーを押して閉じる",192,56) } //石のツルハシ表示 if(pl["stone_pickel"]==1&&map_r==3){ ctx.drawImage(system["stone_pickel"],0,0,32,32,0,64,64,64); } //家から出るときのイベント if(pl["map"]==1&&pl["x"]==160&&pl["y"]==160){ mapdate=[...world]; pl["map"]=0; pl["x"]=96; pl["y"]=96; } //メインループ終了 } //ブラウザ起動時の処理 window.onload = function(){ system["mapchip"] = new Image(); system["mapchip"].src = 'https://raw.githubusercontent.com/sugoruru/rurukun3-save/main/tile-set4.png';//マップタイルの読み込み system["stone_pickel"] = new Image(); system["stone_pickel"].src = 'https://raw.githubusercontent.com/sugoruru/rurukun3-save/main/d-tile.png';//石のつるはしの読み込み system["pl1"] = new Image(); system["pl1"].src = 'https://raw.githubusercontent.com/sugoruru/rurukun3-save/main/p-tile-set1.png';//キャラクタータイルの読み込み setInterval(function() {main_loop()},20);//「main_loop」を20mm秒ごとに呼び出し //世界のマップ mapdate=[...world]; } //キーボード操作 document.addEventListener("keydown", key_down, false);//押されたときの操作→「key_down」へ addEventListener("keyup", key_up, false);//押すのをやめたときの処理→「key_up」へ //キーが押されたとき function key_down(e) { if(talk_number>1){ if(e.which==38){ //上キーが押されたとき pl_g_up=true; if(talk_number==2)talk_number++; if(map_r==1)map_l++; } if(e.which==37){ //左キーが押されたとき pl_g_left=true; if(talk_number==2)talk_number++; } if(e.which==40){ //下キーが押されたとき pl_g_down=true; if(talk_number==2)talk_number++; if(map_r==1)map_l++; } if(e.which==39){ //右キーが押されたとき pl_g_right=true; if(talk_number==2)talk_number++; } } if(e.which==32){ //スペースキーが押されたとき //はじめの「ここは...?」の文章を進む if(talk_number==1){ talk_number++; } //「家に入りますか?」の質問 if(mapdate[pl["y"]/32+9][pl["x"]/32+10]==4){ map_r=1; } //NPC1の会話終了 if(talk_number==10){ talk_number=5; map_r=0; pl["stone_pickel"]=1; system["current_item"]=1; } //NPC1の会話で次へ進む if(talk_number==6||talk_number==7||talk_number==8||talk_number==9){ talk_number++; } } if(e.which==13){ //エンターキーが押されたとき //家に入る(いいえ) if(map_r==1&&map_l==0){ map_r=0; } //家に入る(はい) if(map_r==1&&map_l==1){ pl["map"]=1; mapdate=[...home1]; map_r=0; pl["x"]=160; pl["y"]=128; } } if(e.which==16){ //シフトキーが押されたとき if(system["item_box"]==0&&system["item_box_i"]==0){ map_r=3; system["item_box"]=1; system["item_box_i"]=1; } if(system["item_box"]==1&&system["item_box_i"]==0){ map_r=0; system["item_box"]=0; system["item_box_i"]=1; } } } //キーが上げられたとき function key_up(e){ pl_g_up=false; pl_g_left=false; pl_g_down=false; pl_g_right=false; system["item_box_i"]=0; } こちらはすべて載せてありますので、コピーしてください。 comment.txt これは、「qiita」で皆様と作っているときの備考用です ↓#0 https://qiita.com/rurukun82/items/b5e376f188bc8a1b5c7d ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー map_rについて 0→何もなし 1→家の中に入るときの制限 2→NPC1と喋るとき 3→持ち物欄選択 ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー mapdateについて world→実際の世界 home1→鍛冶屋さんがいる家 ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー pl変数について x→今いるdatemap変数のときの座標 y→今いるdatemap変数のときの座標 map→今どのマップに居るのか stone_pickel→石のつるはしを持っているか ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー pl変数(map)について 0→world 1→home1 ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー system変数について ctx_font→32pxの文字のfont ctx_font2→16pxの文字のfont talk-1→鍛冶屋の人との関わり mapchip_size→マップタイルの大きさ pl_animation→プレイヤーのアニメーション番号 mapchip→マップチップ画像挿入用 pl1→プレイヤー1の画像挿入用 stone_pickel→石のつるはしの画像挿入用 item_box→アイテム欄の表示イベント中かどうか item_box_i→アイテム欄のイベント制御用 current_item→現在の持っているアイテムはなにか ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー これで、枠が表示されて、つるはしも入手したら表示されていると思います 変えた部分がわかりにくくなったので、main.jsは全て載せました comment.txtもついでに変更しました 仕組みはこうです ①system変数に「current_item」をついか→comment.txtを参照 ②この変数にそってアイテムを表示
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ExpressとMySQLを使用して検索機能を作る方法(部分検索も実装する!!)

目的 アパレル商品のデータを調べるときに用いる検索機能を作る。 該当する商品名を入力することで画面に出力。(部分検索も取り入れる) 該当しない商品はデータがありませんと出力する。 使用ツール "express": "^4.17.2" "ejs": "^3.1.6", "morgan": "^1.10.0", "mysql2": "^2.3.3" npmを使用してパッケージをインストール。 npm i express ejs morgan mysql2 ※mysql2は[mysql]というパッケージがある。パフォーマンス重視ならmysql2を選択。 データベースGUIはA5:SQL Mk-2を使用。 構成ファイル datasearch ---node_modules ---roots routing.js ---views data.ejs style.css ---package-lock.json ---package.json ---script.js 手順 ①script.jsにて下記コードを記載。 script.js const express = require(`express`); const morgan = require(`morgan`); const router = require(`./roots/routing`); const app = express(); const port = process.env.PORT || 4000; app.use(morgan('dev')); app.set(`view engine`,`ejs`); app.use(express.urlencoded({extended: false})); app.use(`/data`,router); app.use(express.static(`views`)); app.listen(port); morganを用いることでGET,POSTなどのログをコンソールに出力できる。 HTTPログを可視化したい方はインストール必須。 ルーティング処理を別ファイルで行うためにrequire("./roots/routing")を使用してrouting.jsを読み込む。 express()インスタンスをapp変数に格納。 インスタンスの中にはuseやsetなどのメソッドやプロパティが含まれている。 process.env.PORT || 4000ポート番号を4000に設定。 app.useを用いてミドルウェア関数を実行。(useではパスの設定も可能) ・app.use(morgan('dev')) HTTPログを可視化する。 ・app.set(`view engine`,`ejs`)※この部分だけでは、ejsからHTMへコンパイルできない。 サーバーの動作を構成する。第一引数をview engineにして第二引数をテンプレートエンジンにする。 ・app.use(express.urlencoded({extended: false})) クライアントがリクエストを送った際の本文のみを解析する。 inputで入力した値を取得する際に必要。 ・app.use(`/data`,router) 第一引数はURLパスを設定。 パスを設定することでrouterのパスと結び付けれる。 別ファイルのrouterミドルウェア関数を実行。 ・app.use(express.static(`views`)) 静的ファイルを提供する。 引数に設定したルートディレクトリに存在する静的ファイルを画面へ出力させる。 CSS,HTMLなどが可能。 ・app.listen(port) 設定したポート番号でサーバーを立てる。 下記のようにコンソールで出力すれば、URLを確認できる。(エディターがVSコードなら直接URLから飛べる) script.js app.listen(port,() => console.log(`http://localhost:${port}/data`)); ②routing.jsで下記のコードを記載。 routing.js const express = require(`express`); const router = express.Router(); let mysql = require(`mysql2`); let user = { host: `localhost`, user: `root`, password: `hiromu4675nogi`, database: `product` } let connection = mysql.createConnection(user); router.get(`/`,(req,res) => { res.render(`data`, { no_data: null, navigation: '<P>どの商品をお探しですか?</p>' }) }); router.post(`/`,(req,res) => { let sql = "select 商品ID,商品名,販売金額,在庫数 from product_data where 商品名 like ?" let valus = ['%' + req.body.dataValue + '%'] connection.query(sql,valus,(err,results) => { if(Object.keys(results).length === 0){ res.render('data', { results_len: null, no_data: '<p>データがありません</p>' }) }else{ res.render('data', { obj_list: results, no_data: undefined, results_len:undefined }) } }); }); module.exports = router; routing.js const express = require(`express`); const router = express.Router(); 読み込んだexpressインスタンスからRouterオブジェクトを作成する。 Routerオブジェクトを用いることで別ファイルでもミドルウェア関数やルーディングを行える。 routing.js let mysql = require(`mysql2`); let user = { host: `localhost`, user: `root`, password: `hiromu4675nogi`, database: `product` } let connection = mysql.createConnection(user); mysql2を変数に格納。 userオブジェクトを作成してデーターベース設定を行う。 hostのデフォルトはlocalhost。hostを変えてない人は省略可能。 ※user,password,databaseは必須。 下記のコードでも実行できる。 routing.js let connection = mysql.createConnection({ host: `localhost`, user: `root`, password: `hiromu4675nogi`, database: `product` ); routing.js router.get(`/`,(req,res) => { res.render(`data`, { no_data: null, navigation: '<P>どの商品をお探しですか?</p>' }) }); getを受け取った際のルーティングを決める。 上記のgetはクライアントがページに訪れた際の処理です。 get,postなどのHTTPメソッドでは二つの引数を取る。 第一引数はパス設定。 パス設定で/を用いることで現在のURLパスを指します。 script.jsでapp.use(`/data`,router)を設定しているため、現在のパスはhttp://localhost:4000/dataです。 第二引数ではコールバック。 コールバックには二つの引数を設定。 request→HTTPリクエスト(パラメータ、本文、HTTPヘッダーなどのプロパティがある) response→HTTPレスポンス(サーバーから送信する処理が含まれる) ※慣例によってreq,resと表記します。 res.renderではレンダリングをレスポンスとして返す。 renderはデフォルトでviewsディレクトリーのファイルをレンダリングする。 今回の場合はdata.ejsをレンダリングしたい。そのためには識別子を無くしてdataに設定。 renderではローカル変数を定義できる。例えば下記のコード。 routing.js res.render(`data`, { navigation: '<P>どの商品をお探しですか?</p>' }) 続いてejsコード。 data.ejs <!DOCTYPE html> <html lang="ja"> <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"> <title>Document</title> </head> <body> <%- no_data %> </body> </html> 簡単に説明するとejsでは<% %>タグを記すことでjs側で設定した値を出力できる。 つまり<% %>の空間だけJavascript領域となる。 (<%- %>ではHTMLタグを検知してくれる。ブラウザで確認すると、pタグが付く) 上記のコードで言えば、 navigationでは'どの商品をお探しですか?'を定義している。 jsで定義している変数名をejsで見つけると、値が格納される。 結果、画面にどの商品をお探しですか?を描画できる。 続いてpostルーティングを設定。 routing.js router.post(`/`,(req,res) => { let sql = "select 商品ID,商品名,販売金額,在庫数 from product_data where 商品名 like ?" let valus = ['%' + req.body.dataValue + '%'] connection.query(sql,valus,(err,results) => { if(Object.keys(results).length === 0){ res.render('data', { results_len: null, no_data: '<p>データがありません</p>' }) }else{ res.render('data', { obj_list: results, no_data: undefined, results_len:undefined }) } }); }); sql文とプレースホルダーを変数に格納。 プレースホルダーはsql文の?に値が入る。 let valus = ['%' + req.body.dataValue + '%']では、クライアントがリクエストを送った際のbodyを取得しています。 inputで入力された値を取得する際もreq.bodyで取得します。 bodyの続きにはinputタグで設定したnameの値です。 例えばejsで下記のinputタグを設定したと仮定する。 data.ejs <input type="search" name="dataValue"> nameと合わせないとエラーになる。 更にscript.jsで設定したapp.use(express.urlencoded({extended: false}))も無いとbodyが解析されないので要注意! sql文ではlike句を設定。この句を使うことで部分検索を実現させています。 例えばパンツを検索すると、データーベースにアクセスして商品名からパンツを含んだ名前を取得する。 mysql.createConnectionを格納した変数connectionを使ってsqlの結果を res.renderで返す。 routing.js connection.query(sql,valus,(err,results) => { if(Object.keys(results).length === 0){ if(err) thow err; res.render('data', { results_len: null, no_data: '<p>データがありません</p>' }) }else{ res.render('data', { obj_list: results, no_data: undefined, results_len:undefined }) } }); queryのコールバック引数にはerr,resultsが付けれます。 errはエラー時の処理。resultsはsql文の結果。(エラー時の処理は、if(err) thow err;のみでも構わない) resultsの型タイプはobjectです。 objectのkeysのlengthが0ならデータがありませんとレンダリング。 lengthが1以上ならresultsを画面にレンダリング。 if文を用いることでデータがない処理とデータがある処理を行えます。 注意点を述べると、ejsに設定した変数が格納されてないとエラーが起きる。 下記はejsコード。 data.ejs <body> <div class="form_block"> <form action="/data" method="post" class="form_input"> <input type="search" name="dataValue" value=""> </form> <div class="productlist"> <% if (no_data === null) { %> <%- navigation %> <% } else if (results_len === null) { %> <%- no_data %> <% } else { %> <table border="1"> <tr> <th>商品ID</th> <th>商品名</th> <th>販売金額</th> <th>在庫数</th> </tr> <% for( let i = 0; i< obj_list.length; i++ ) { %> <% let product_ID = obj_list[i].商品ID %> <% let product_name = obj_list[i].商品名 %> <% let product_value = obj_list[i].販売金額 %> <% let pruduct_stock = obj_list[i].在庫数 %> <tr> <%- `<td>${product_ID}</td>`%> <%- `<td>${product_name}</td>`%> <%- `<td>${product_value}円</td>` %> <%- `<td>${pruduct_stock}</td>` %> </tr> <% } %> </table> <% } %> </div> </div> </body> ejs側でもif文を用いて結果を分岐させています。 <% if (no_data === null) { %>では最初の画面を出力させる処理(検索する前) <% } else if (results_len === null) { %>ではデータない際の処理。 <% } else { %>ではデータがある際の処理。 仮にデータが存在してもif文を設定しているため、ejsは条件式を読み込んでしまう。 もし、no_dataや results_len の変数を定義しないとエラーが発生する (no_data is not defined`変数が定義されてないとエラーログが出ます) エラー防止策として、データを見つけてもno_dataとresults_lenを定義すること。 routing.js no_data: undefined, results_len:undefined 勿論,nullや``でも代用可能です。(if文の条件式を考えながら変数を定義する) 他の方法としては、パスを設定して別画面で結果を返す方法もあります。 しかしその方法では他のファイルも作成したり、レンダリングする必要がある。 検索した結果を返すだけなら一つのファイルでも完結できる。 データがある際の処理は下記の通り。 data.ejs <% } else { %> <table border="1"> <tr> <th>商品ID</th> <th>商品名</th> <th>販売金額</th> <th>在庫数</th> </tr> <% for( let i = 0; i< obj_list.length; i++ ) { %> <% let product_ID = obj_list[i].商品ID %> <% let product_name = obj_list[i].商品名 %> <% let product_value = obj_list[i].販売金額 %> <% let pruduct_stock = obj_list[i].在庫数 %> <tr> <%- `<td>${product_ID}</td>`%> <%- `<td>${product_name}</td>`%> <%- `<td>${product_value}円</td>` %> <%- `<td>${pruduct_stock}</td>` %> </tr> <% } %> </table> <% } %> obj_list変数にはsqlの結果が入っています。(jsにて定義しているから) 型タイプはobjectです。 このまま出力させると、オブジェクトのまま画面に表示される。 実用的に施すなら下記のように出力する方が綺麗に作れる。 まず、検索して取得した商品を出力させるためにfor文で回します。 data.ejs <% for( let i = 0; i< obj_list.length; i++ ) { %> <% let product_ID = obj_list[i].商品ID %> <% let product_name = obj_list[i].商品名 %> <% let product_value = obj_list[i].販売金額 %> <% let pruduct_stock = obj_list[i].在庫数 %> <% } %> 変数に格納したら画面に出力させる。 data.ejs <%- `<td>${product_ID}</td>`%> <%- `<td>${product_name}</td>`%> <%- `<td>${product_value}円</td>` %> <%- `<td>${pruduct_stock}</td>` %> CSSやブラウザ上でjsを行いたい時は、最初に説明した下記ミドルウェア関数を実行する。 app.use(express.static(`views`)) テーブルにカラーをつけることも可能です。 ejsタグについては、他の方が詳しく説明したものがあります。 下記URLからご覧ください。 https://qiita.com/miwashutaro0611/items/36910f2d784ff70a527d 全文コードは下記です。 script.js const express = require(`express`); const morgan = require(`morgan`); const router = require(`./roots/routing`); const app = express(); const port = process.env.PORT || 4000; app.use(morgan('dev')); app.set(`view engine`,`ejs`); app.use(express.urlencoded({extended: false})); app.use(`/data`,router); app.use(express.static(`views`)); app.listen(port,() => console.log(`http://localhost:${port}/data`)); data.ejs <!DOCTYPE html> <html lang="ja"> <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"> <title>search</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="form_block"> <form action="/data" method="post" class="form_input"> <input type="search" name="dataValue" value=""> </form> <div class="productlist"> <% if (no_data === null) { %> <%- navigation %> <% } else if (results_len === null) { %> <%- no_data %> <% } else { %> <table border="1"> <tr> <th>商品ID</th> <th>商品名</th> <th>販売金額</th> <th>在庫数</th> </tr> <% for( let i = 0; i< obj_list.length; i++ ) { %> <% let product_ID = obj_list[i].商品ID %> <% let product_name = obj_list[i].商品名 %> <% let product_value = obj_list[i].販売金額 %> <% let pruduct_stock = obj_list[i].在庫数 %> <tr> <%- `<td>${product_ID}</td>`%> <%- `<td>${product_name}</td>`%> <%- `<td>${product_value}円</td>` %> <%- `<td>${pruduct_stock}</td>` %> </tr> <% } %> </table> <% } %> </div> </div> </body> </html> routing.js const express = require(`express`); const router = express.Router(); let mysql = require(`mysql2`); let user = { host: `localhost`, user: `root`, password: `hiromu4675nogi`, database: `product` } let connection = mysql.createConnection(user); router.get(`/`,(req,res) => { res.render(`data`, { no_data: null, navigation: '<P>どの商品をお探しですか?</p>' }) }); router.post(`/`,(req,res) => { let sql = "select 商品ID,商品名,販売金額,在庫数 from product_data where 商品名 like ?" let valus = ['%' + req.body.dataValue + '%'] connection.query(sql,valus,(err,results) => { if(err) throw err; if(Object.keys(results).length === 0){ res.render('data', { results_len: null, no_data: '<p>データがありません</p>' }) }else{ res.render('data', { obj_list: results, no_data: ``, results_len:`` }) } }); }); module.exports = router; ※CSSは省略。 総評 制作期間は1週間程度掛かりました。 初めてのサーバ領域での開発。試行錯誤の末、何とか形にできた。 ejsでfor文やif文も回せるということは制作の幅も広がる。 覚えておいて損はないでしょう。 後、サーバーに触れるならHTTPの理解は必要です。(僕もHTTPに関してはまだまだ理解が必要です笑) GET,POST辺りを知れると、理解速度が早くなるかもしれない。 下記URLの記事を参考にしました。 https://www.youtube.com/watch?v=SccSCuHhOw0 https://expressjs.com/ja/4x/api.html https://qiita.com/Sekky0905/items/dff3d0da059d6f5bfabf 宣伝 Twitterではプログラミングに関する知見を発信しています。 プログラミングを始めた3ヶ月以内の人に向けた内容です。 ブランク期間を含めると1年半程度、僕はプログラミングに触れてきました。 ぜひ、質問などがあればTwitterのリプやDMでお待ちしております。 投稿頻度は遅めですが、ぜひフォローしていただけると嬉しいです! @hiromu_edit 尚、過去に制作したポートフォリオをまとめたサイトを公開中です。 ぜひ、参考程度に眺めていってください! portfolio-0105.jp
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

propsと$emitを使って親子間のデータを更新する

やりたかったこと ハンバーガーボタンをクリックしてメニューを開閉、 この時ハンバーガーボタンは三本線からバツマークに変わる。 開いたメニューの後ろをクリックしてもメニューが閉じる(ハンバーガーボタンも三本線に戻る) 子コンポーネント:ハンバーガーボタン 親コンポーネント:ハンバーガーメニューが置いてあるヘッダー (別コンポーネントに開閉後のメニュー) 試したこと 子コンポーネントでは、下記の記述をしていました。 Child.vue(ハンバーガーボタン) <template> <div class="hamburger_btn" @click="activeBtn = !activeBtn"> <span class="line line_01" :class="{btn_line01: activeBtn}"></span> <span class="line line_02" :class="{btn_line02: activeBtn}"></span> <span class="line line_03" :class="{btn_line03: activeBtn}"></span> </div> </template> <script> export default { data() { return { activeBtn: false, }; }, }; </script> // styleは割愛 クリックイベントでactiveBtnを発火させて、v-bindでクラスを付与する形にしていました。 (btn_line01〜03はバツボタンになった時のスタイルを当てています) ですが、これだと子コンポーネント内でしか値が更新されないため この子コンポーネントを他の場所でも使いやすくするため、propsと$emitを使って 親から子へ値を更新したいと考えました。 1. まずemitで子から親へ値を渡す MenuButton.vue(子コンポーネント) <template> <div class="hamburger_btn" @click="$emit('is-active')"> <span class="line line_01" :class="{btn_line01: activeBtn}"></span> <span class="line line_02" :class="{btn_line02: activeBtn}"></span> <span class="line line_03" :class="{btn_line03: activeBtn}"></span> </div> </template> <script> export default { data() { return { activeBtn: false, }; }, }; </script> // styleは割愛 Header.vue(親コンポーネント) <template>  <HamburgerMenuButton @is-active="menuToggle()" /> <!-- v-onでイベント名を受け取る -->  <transition>  <div v-if="activeButton">  <div class="bg" /> <!-- メニューが開いている時の後ろの黒背景 -->  <MenuBar /> <!-- メニュー(別コンポーネント) -->  </div>  </transition> </template> <script> import MenuButton '@/components/MenuButton'; import MenuBar from '@/components/MenuBar'; export default { components: { MenuButton, MenuBar, }, data() { return { activeButton: false, }; }, methods: { menuToggle() { this.activeButton = !this.activeButton; }, }, }; </script> 子コンポーネントの@clickで、$emit('is-active')で設定したイベント名を 親コンポーネントの使いたい場所で@is-active="メソッド名"という形で設定して受け取ります。 今回はハンバーガーメニューをクリックでメニューを開閉させる かつ メニューボタンのアニメーション(三本線からバツボタン)を実行させたいので、 menuToggle()というメソッドを用意して、その中にactiveButtonのtrue/falseトグル処理を書きました。 ただこれだけだと、親コンポーネント内でのデータの書き換えになるので 子コンポーネントにはactiveButtonがfalseやtrueになったことが渡っていません。 2. propsで親から子へデータを渡す Vueでは、propsは子のコンポーネントに記述します。 MenuButton.vue(子コンポーネント) <template> <div class="hamburger_btn" @click="$emit('is-active')"> <span class="line line_01" :class="{btn_line01: activeBtn}"></span> <span class="line line_02" :class="{btn_line02: activeBtn}"></span> <span class="line line_03" :class="{btn_line03: activeBtn}"></span> </div> </template> <script> export default { props: { activeBtn: { type: Boolean, default: false, }, }, }; </script> 親から値が渡ってきて更新されればバツボタンの表示になってくれるので dataからpropsに置き換えました。 親コンポーネントでは下記のように記述します。 Header.vue(親コンポーネント) <template>  <HamburgerMenuButton @is-active="menuToggle()" :active-button="this.activeButton" <!-- 子に渡すデータを指定 --> />  <transition>  <div v-if="activeButton" @click="menuClose()"> <!-- メニューを閉じるクリックイベントを追加 -->  <div class="bg" />  <MenuBar />  </div>  </transition> </template> <script> import MenuButton '@/components/MenuButton'; import MenuBar from '@/components/MenuBar'; export default { components: { MenuButton, MenuBar, }, data() { return { activeButton: false, }; }, methods: { menuToggle() { this.activeButton = !this.activeButton; }, menuClose() { this.activeButton = false; }, }, }; </script> propsでもらったデータは動的に子に渡したいので:active-button="this.activeButton"と記述しましいた。 注意点としては、子コンポーネントのpropsで定義した変数はキャメルケース(activeButton)ですが template内ではケバブケース(active-button)となります。名前が一致していないのでここがややこしいです…。 propsでもらったデータはそのまま書き換えるのではなく、dataに入れて書き換えて使います。 参考記事 Vue.jsをシンプルに理解しよう その4 -propsとemitについて-
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JSのデータタイプのメカニズムとそれによっておこる現象(useCallback React.memo)

今回の記事の内容 データタイプについて コピーのメカニズム 参照の同一性 レンダリングについて データタイプ JSのデータタイプには二種類 プリミティブ型 メソッドを持たないデータ型 string number BigInt boolean undefined symbol オブジェクト型 プロパティとメソッドの集まり array object function RegExp  JSのコピーのメカニズム プリミティブ型 JSで変数などを宣言したら、その変数はパソコンのメモリに保存される。プリミティブ型の変数はコピーする際に新しいメモリ空間を確保して独立的な値を保存。 let string = "りんご"; //←独立して保存される let newString = string; newString = "apple" stringをコピーしてnewStringという変数を生成。そしてnewStringの値を変更。 console.log(string); //りんご console.log(newString); // apple それでも原本のstringには何の影響もない。これは違うメモリに保存されているため。 オブジェクト型 プリミティブタイプのように新しいメモリに保存するのではなく、原本のメモリアドレスを渡される。つまり、原本とコピー本が同じメモリに保存されている同じデータを共有する。 const object = { name: "apple" }; const newObject = object; newObject.name = "banana" console.log(object); // {name: "banana"} console.log(newObject); // {name: "banana"} プリミティブ型と同じように変更すると、このようになる これは同じデータを共有しているからこうなる。  参照の同一性 これどうなる? [] === [] {} === {} (() => {}) === (() => {}) 0 === 0 "string" === "string" true === true false === false //false [] === [] {} === {} (() => {}) === (() => {}) //true 0 === 0 "string" === "string" true === true false === false こっから本編  レンダリングについて useCallback useCallbackはメモ化されたコールバックを返すHook。 依存配列(=[deps] コールバック関数が依存している要素が格納された配列)の要素のいずれかが変化した場合のみ、メモ化した値を再計算する。 メモ化とは? コストが高い呼び出しの結果を保存し、同じ入力が再び発生したときにキャッシュされた結果を返すことによってプログラム実行速度を向上させる技術。 公式では 不必要なレンダーを避けるために参照の同一性を見るよう最適化されたコンポーネントにコールバックを渡す場合に便利 と書かれている。 逆に言うたら 「参照の同一性を見るよう最適化されたコンポーネントにコールバックを渡す場合じゃないと別に要らない」 となる。 ここで基礎的な知識として Reactコンポーネントは自分のstateが変更されたり、親コンポーネントから渡されるpropsが変更された場合再レンダリングされる。 コンポーネントが再レンダリングされると、その中で宣言されている関数や変数は以前保存されていたメモリを空けて新しいメモリに再び保存される(garbage collection)。 これらの知識から分かること こんなボタンコンポーネントがあったとする const CountButton = function CountButton({ onClick, count }) { return <button onClick={onClick}>{count}</button>; }; function DualCounter() { const [count1, setCount1] = React.useState(0); const increment1 = () => setCount1(c => c + 1); const [count2, setCount2] = React.useState(0); const increment2 = () => setCount2(c => c + 1); return ( <> <CountButton count={count1} onClick={increment1} /> <CountButton count={count2} onClick={increment2} /> </> ); } DualCounterのstateであるcount1が変更される DualCounterが再レンダリングされる DualCounterの中の変数や関数(count1、setCount1、increment1など)たち全部がもともと保存されてたメモリを空けて新しいメモリに保存される CountButtonは引数で渡される変数と関数の変更チェック increment1とincrement2がオブジェクト型のため、保存されたメモリが変わったことで新しいやつだと判断 CountButton両方とも再レンダリングされる。 結局一つのCountButtonを押しただけなのにCountButtonが全部再レンダリングされてしまう。 次 こんなコンポーネントがあったとする const CountButton = function CountButton({ onClick, count }) { return <button onClick={onClick}>{count}</button>; }; function DualCounter() { const [count1, setCount1] = React.useState(0); const increment1 = React.useCallback(() => setCount1(c => c + 1), []); const [count2, setCount2] = React.useState(0); const increment2 = React.useCallback(() => setCount2(c => c + 1), []); return ( <> <CountButton count={count1} onClick={increment1} /> <CountButton count={count2} onClick={increment2} /> </> ); } どうなる? まだ全部再レンダリングされる 今のCountButtonたちは渡される引数に変更がなくても親であるDualCounterが再レンダリングされたので自分自身も再レンダリングされた。 これが子コンポーネントを最適化しなければならない理由。 答えはこう const CountButton = React.memo(function CountButton({ onClick, count }) { return <button onClick={onClick}>{count}</button> }) function DualCounter() { const [count1, setCount1] = React.useState(0) const increment1 = React.useCallback(() => setCount1(c => c + 1), []) const [count2, setCount2] = React.useState(0) const increment2 = React.useCallback(() => setCount2(c => c + 1), []) return ( <> <CountButton count={count1} onClick={increment1} /> <CountButton count={count2} onClick={increment2} /> </> ) } React.memoはパフォーマンス最適化のための高階コンポーネント(HOC, higher-order component)で,props の変更のみをチェックする。 高階コンポーネントとは、コンポーネントを引数としてもらって新しいコンポーネントを返す関数。React.memoの場合は引数としてコンポーネントをもらい、最適化されたコンポーネントを返す。 もしあるコンポーネントが同じ props を与えられたときに同じ結果をレンダーするなら、結果を記憶してパフォーマンスを向上させるためにそれを React.memo でラップすることができます。つまり、React はコンポーネントのレンダーをスキップし、最後のレンダー結果を再利用します。 要するにReact.memoでコンポーネントを囲んだら、渡された引数に変化があるかどうかチェックして変化がある場合のみ再レンダリングされる機能が追加される。 結論 なんでもかんでもuseCallback使えばおkというわけではない 既に上記のCountButtonの例でuseCallbackだけ使ったら子コンポーネントの再レンダリングを防げないことを学びました。useCallbackの目的である「不必要なレンダーを避ける」ができてないのによい使い方のはずがないでしょう。 それに関数宣言はコストが安い処理なので、わざわざuseCallbackまで使って防げるべきものではない。 useCallbackを使うことにもコストがかかる。useCallbackというHookを読み込むし、第2引数で配列の宣言もするし、レンダリングの度にuseCallbackが動きます。useCallbackを使ったほうが得な時もあるが、場合によっては逆に余計なメモリを食う時だってある。 「When to useMemo and useCallback」の著者であるKent C. Doddsはこう言いました。 MOST OF THE TIME YOU SHOULD NOT BOTHER OPTIMIZING UNNECESSARY RERENDERS. React is VERY fast and there are so many things I can think of for you to do with your time that would be better than optimizing things like this. ほとんどの場合、不要なレンダリングの最適化は気にしなくていいです。Reactは非常に速いし、このようなものを最適化するよりも、他に時間を割いてやるべきことはたくさんあります そして今まで話した最適化の必要性も極稀と言ってます。彼がPayPalで働いた3年間、そしてそれよりも長いReact歴の間もそいういう最適化が必要な瞬間はなかったようです。 要するにuseCallbackを使うべき瞬間はそんなに多くないということになる。それなのに必要でもないHookを「とりあえず入れとこう」という気持ちで使うことはやめたほうがいい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【超初心者向け】JavaScriptでWEBアプリケーションを作る

備忘録として、QiitaでJavaScriptの記事を書いていきたいと思います。相当初歩的な内容なので、暖かく見守っていただけると幸いです。 やりたいこと 私は経済学部に所属しているので、身近なGDPを計算する簡単なWEBアプリケーション(以下アプリ)を作成しようと思います。 jQueryとかも一旦使わないピュアなJavaScriptでコードを書きますので、超初心者も安心です。 (自分も超初心者) GDPの公式 GDPの計算の公式は、 Y = C + I + G + (X - M) Y:GDP C:民間消費 I:民間投資 G:政府支出(消費 + 投資) X:輸出 M:輸入 こんな感じです。 「経済学興味ないよ」って方は、これを覚える必要はありませんが、一応UI(ユーザーインターフェイス)に説明として書くようにしましょう。 簡単な設計 それでは簡単にアプリの設計をしましょう。 とはいえ、面倒なので今回はデザインとかは完全にスキップしてCSSのファイルは作りません。 やりたいことは、C, I, G, X, Mを入力して、ボタンを押したらYが出力される、ってだけの処理。 必要な機能はざっと以下の通り。 ・HTMLのinput要素 ・計算開始用のボタン ・inputから数字を読み取って計算 ・HTMLに計算結果を表示 やることはこれだけですね、単純で明快です。 ファイルを作る では早速ファイルを作ります。 ……とその前にフォルダ(ディレクトリ)を作って、適当に名前をつけます。 今回はGDPと名付けましょう(安直)。 GDP/index.html GDP/script.js この二つのファイルだけ作ったら、次はいよいよコードを書いていきます。 HTMLを書く では、HTMLのコードを書いていきます。 まずはinput要素から書きましょうか。 <p>民間消費(C): <input id="c" type="number" value=""></p> <p>民間投資(I): <input id="i" type="number" value=""></p> <p>政府支出(G): <input id="g" type="number" value=""></p> <p>輸出(X): <input id="x" type="number" value=""></p> <p>輸入(M): <input id="m" type="number" value=""></p> 1行目を書いたらコピペで5つに分身させて、id要素だけ変えましょう。その方が楽だしミスもないです。 そして、その下にボタンを配置します。 <button id="keisan_button"><p id="keisan">計算開始!</p></button> 思わず押したくなるようなボタンが出来上がったら、今度は計算結果を出力する場所を用意します。 <p id="output">GDP: <span id="y"></span>億円</p> これでHTMLの準備は完了です。 JavaScriptを書く前に、bodyの1番下にscript.jsを読み込むための「魔法の一文」を加えましょう。 <script src="script.js"></script> 一応、body内のコードを全て書くとこうなります。 <!-- 前略 --> <body> <p>民間消費(C): <input id="c" type="number" value=""></p> <p>民間投資(I): <input id="i" type="number" value=""></p> <p>政府支出(G): <input id="g" type="number" value=""></p> <p>輸出(X): <input id="x" type="number" value=""></p> <p>輸入(M): <input id="m" type="number" value=""></p> <button id="keisan_button"><p id="keisan">計算開始!</p></button> <p id="output">GDP: <span id="y"></span>億円</p> <script src="script.js"></script> </body> JavaScriptを書く 次にアプリの機能を作っていきましょう。 私はJavaScript初学者なので、もっと良い記述方法もあると思いますが、とりあえず今は「動けばOK」精神で書きます。 まずはinput要素から数値を取り出して、計算結果を返すmath()を作ります。 function math(){ let c = parseInt(document.getElementById("c").value); let i = parseInt(document.getElementById("i").value); let g = parseInt(document.getElementById("g").value); let x = parseInt(document.getElementById("x").value); let m = parseInt(document.getElementById("m").value); return c + i + g + (x - m); }; 変数c, i, g, x, mに、それぞれの数値を代入します。このコードもコピペ推奨です、コピペは神。 そして、returnで計算結果を戻り値として返します。 一応、parseIntで数値として取り出すよ〜的なこと書いたんですが、無くても大丈夫かも?(HTML側でtypeをnumberで指定しているため) 次に、このmath()をボタンのクリック時に呼び出すコードを書きましょう。 document.getElementById("keisan_button").onclick = function(){ let y = math(); document.getElementById("y").textContent = y; }; なんか、この1行目はもっと綺麗に書ける気もするのですが、機能すれば良いのでとりあえずこれでよし。 ボタンがクリックされたら、先ほど作ったmath()を呼び出して変数yに代入。 その後、span id="y"に表示してねって感じ。 正直、ひとつのfunctionにまとめても書けるけど、どっちが良いのか分からなかったので2つに分けました。 一応、index.htmlのパスをコピーして、Chromeで確認してみてください。 終わりに とりあえず簡単なアプリを作ることができました。 デザインとか、もう少し機能つけてみたりしたら需要あるかも?? この記事を通して、JavaScriptでアプリ開発することのハードルが下がれば良いなと思っています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Djangoで郵便番号の入力から住所を反映させる

郵便番号の入力から住所を自動で反映させる フォームから住所を書く際に郵便番号を入力するだけで自動で住所が入力されたら楽なのでその流れをDjangoのコードで書けるようにメモ。 ※既にアプリは出来てるものとして、その部分は割愛します。 ルーティング設定(urls.py) urls.py from django.urls import path,include from .import views from django.views.generic import TemplateView app_name = 'アプリ名' urlpatterns = [ path('', views.IndexView.as_view(),name="index"), path('inquiry/', views.InquiryView.as_view(),name="inquiry"), ] 後にInquiry classを作る予定でこのpath設定。 ビューの編集(views.py) views.py from .forms import InquiryForm #forms.pyをインポート from django.urls import reverse_lazy #処理が終わった後に画面に飛ばす処理 class InquiryView(generic.FormView): template_name = "inquiry.html" form_class = InquiryForm success_url = reverse_lazy('アプリ名:inquiry') フォームの設定(forms.py) forms.py import os from django import forms from django.db.models import fields #(省略) Zipcode = { 'zip_code': forms.TextInput( attrs={'class': 'p-postal-code','placeholder': '記入例:1060022'}, ), } class InquiryForm(forms.Form):#クラス名は必要に応じて変える # 郵便番号からcityまでを自動入力 zipcode = forms.RegexField(label='郵便番号(ハイフンなし)', regex=r'^[0-9]+$', max_length=7, widget=forms.TextInput(attrs={'onKeyUp' : "AjaxZip3.zip2addr(this,'','state','city')"}), ) state = forms.CharField(label='都道府県',max_length=6) city = forms.CharField(label='市区町村',max_length=10) address_1 = forms.CharField(label='番地',max_length=10) address_2 = forms.CharField(label='建物名・部屋番号',max_length=10) def __init__(self,*args,**kwargs):#入力フォーム表示用 self.fields['zipcode'].widget.attrs['class']='form-control col-11' self.fields['zipcode'].widget.attrs['placeholder']='例:1600022 ' self.fields['state'].widget.attrs['class']='form-control col-11' self.fields['state'].widget.attrs['placeholder']='都道府県をここに入力してください' self.fields['city'].widget.attrs['class']='form-control col-11' self.fields['city'].widget.attrs['placeholder']='市区町村をここに入力してください' self.fields['address_1'].widget.attrs['class']='form-control col-11' self.fields['address_1'].widget.attrs['placeholder']='番地をここに入力してください' self.fields['address_2'].widget.attrs['class']='form-control col-11' self.fields['address_2'].widget.attrs['placeholder']='建物名・部屋番号をここに入力してください' いい感じに動いてもらうためのJavaScript 今回はAjaxzip3を使ってるので、そこから引用してます。 ※今回はbase.htmlにべた書きしましたが、他におすすめがあれば教えてください。 base.html <script src="https://ajaxzip3.github.io/ajaxzip3.js" charset="UTF-8"></script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

郵便番号の入力から住所を自動で反映させる【Django】

郵便番号の入力から住所を自動で反映させる フォームから住所を書く際に郵便番号を入力するだけで自動で住所が入力されたら楽なのでその流れをDjangoのコードで書けるようにメモ。 ※既にアプリは出来てるものとして、その部分は割愛します。 ルーティング設定(urls.py) urls.py from django.urls import path,include from .import views from django.views.generic import TemplateView app_name = 'アプリ名' urlpatterns = [ path('', views.IndexView.as_view(),name="index"), path('inquiry/', views.InquiryView.as_view(),name="inquiry"), ] 後にInquiry classを作る予定でこのpath設定。 ビューの編集(views.py) views.py from .forms import InquiryForm #forms.pyをインポート from django.urls import reverse_lazy #処理が終わった後に画面に飛ばす処理 class InquiryView(generic.FormView): template_name = "inquiry.html" form_class = InquiryForm success_url = reverse_lazy('アプリ名:inquiry') フォームの設定(forms.py) forms.py import os from django import forms from django.db.models import fields #(省略) Zipcode = { 'zip_code': forms.TextInput( attrs={'class': 'p-postal-code','placeholder': '記入例:1060022'}, ), } class InquiryForm(forms.Form):#クラス名は必要に応じて変える # 郵便番号からcityまでを自動入力 zipcode = forms.RegexField(label='郵便番号(ハイフンなし)', regex=r'^[0-9]+$', max_length=7, widget=forms.TextInput(attrs={'onKeyUp' : "AjaxZip3.zip2addr(this,'','state','city')"}), ) state = forms.CharField(label='都道府県',max_length=6) city = forms.CharField(label='市区町村',max_length=10) address_1 = forms.CharField(label='番地',max_length=10) address_2 = forms.CharField(label='建物名・部屋番号',max_length=10) def __init__(self,*args,**kwargs):#入力フォーム表示用 self.fields['zipcode'].widget.attrs['class']='form-control col-11' self.fields['zipcode'].widget.attrs['placeholder']='例:1600022 ' self.fields['state'].widget.attrs['class']='form-control col-11' self.fields['state'].widget.attrs['placeholder']='都道府県をここに入力してください' self.fields['city'].widget.attrs['class']='form-control col-11' self.fields['city'].widget.attrs['placeholder']='市区町村をここに入力してください' self.fields['address_1'].widget.attrs['class']='form-control col-11' self.fields['address_1'].widget.attrs['placeholder']='番地をここに入力してください' self.fields['address_2'].widget.attrs['class']='form-control col-11' self.fields['address_2'].widget.attrs['placeholder']='建物名・部屋番号をここに入力してください' いい感じに動いてもらうためのJavaScript 今回はAjaxzip3を使ってるので、そこから引用してます。 ※今回はbase.htmlにべた書きしましたが、他におすすめがあれば教えてください。 base.html <script src="https://ajaxzip3.github.io/ajaxzip3.js" charset="UTF-8"></script> AjaxzipのGitHubはこちら
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Stripe ElementとNext.jsを利用して、クレジットカードの登録画面を構築する

サービスによっては、決済を行う前にクレジットカード情報を入力してもらう動線を作りたい場合があります。 StripeのSetupIntent APIとElementのconfirmSetupメソッドを利用し、Reactでカード登録フォームを実装する方法を紹介します。 前準備 このサンプルでは、Next.jsを利用します。これはSetupIntentの作成にREST APIが必要なため、APIとReactアプリを両方まとめて管理できるためです。 Next.jsでのアプリ起動は、create-next-appを利用します。 % npx create-next-app .envファイルでStripeのAPIキーを環境変数経由で利用できるようにします。 .env.local NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx STRIPE_SECRET_KEY=sk_test_xxxx また、実装で利用するライブラリを追加でインストールしましょう。 % npm install stripe @stripe/stripe-js @stripe/react-stripe-js Step1: Next.jsでSetupIntentを作成するAPIを用意する 支払い方法を保存する場合は、setupIntentAPIを利用します。 import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, { apiVersion: '2020-08-27' }) ... const response = await stripe.setupIntents.create({...}) { ... "client_secret": "seti_xxxxxxxl" } 保存した決済方法を後から利用できるようにするため、顧客(Customer)データの作成も行いましょう。 const customer = await stripe.customers.create() const response = await stripe.setupIntents.create({ customer: customer.id, payment_method_types: ['card'] }) Next.jsの場合、ファイル名がそのままAPIパスになりますので、以下のようなファイルを用意しましょう。 pages/api/setup-intent.js import Stripe from 'stripe' const stripe = new Stripe(process.env.STRIPE_SECRET_KEY) export default async function handler( req, res ) { if (req.method.toLocaleLowerCase() !== 'post') { return res.status(405).end() } const customer = await stripe.customers.create({ metadata: { app_username: req.body.username } }) const response = await stripe.setupIntents.create({ customer: customer.id, payment_method_types: ['card'] }) return res.status(200).json({ client_secret: setupIntent.client_secret, customer_id: customer.id, }) }) customers.createで設定しているmetadataは、アプリ側でなにかしらのユーザー管理機能がある場合を想定した設定です。 Stripeの顧客情報からアプリのユーザー情報を取得・アクセスしたい場合などにこのメタデータを利用すると便利です。 Step2: Reactでカード情報入力フォームを用意する 続いてSetupIntentを利用してカード情報を入力するフォームを実装します。 pages/index.jsx import { loadStripe } from "@stripe/stripe-js"; import { Elements, PaymentElement } from "@stripe/react-stripe-js"; import { useEffect, useState } from 'react'; const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY); const Home = () => { const [intent, setIntent] = useState(null) useEffect(() => { fetch('/api/setup-intent', { method: 'post' }).then(res => res.json()) .then(data => { setIntent(data) if (data.customer_id) { window.localStorage.setItem('customer_id', data.customer_id) } }) .catch(e => { console.log(e) }) },[setIntent]) if (!intent || !intent.client_secret) return <p>loading</p> const options = { appearance:{ theme: 'stripe', }, clientSecret: intent.client_secret, } return ( <Elements stripe={stripePromise} options={options}> <PaymentElement /> </Elements> ) } export default Home useEffectを利用し、ページが表示されたタイミングでSetupIntentとCustomerを作成しています。 その後、取得したSetupIntentデータをuseStateでStateに格納し、@stripe/react-stripe-jsライブラリのElementsコンポーネントに渡しています。 また、CustomerのIDの保存にブラウザのlocalStorageを利用しました。あくまでデモ用途ですので、実装時にはfirebaseやCognitoなどのmetadataに保存することをお勧めします。 Step3: 表示を一度確認する ここまでで一度表示を確認してみましょう。 $ yarn dev -p 3000 yarn run v1.22.17 $ next dev -p 3000 ready - started server on 0.0.0.0:3000, url: http://localhost:3000 info - Loaded env from /Users/hideokamoto/sandbox/element-demo/.env.local event - compiled client and server successfully in 376 ms (171 modules) エラーが出ていなければ、カード情報を入力するフォームが表示されます。 Step4: 入力したカード情報を保存する ここからは表示させたカード情報入力フォームのデータを保存する処理を実装します。 pages/index.jsxにPaymentコンポーネントを追加しましょう。 import { Elements, PaymentElement, + useElements, useStripe } from "@stripe/react-stripe-js"; import { useEffect, useState } from 'react'; const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY); const Payment = () => { const stripe = useStripe(); const elements = useElements(); return ( <form onSubmit={async e => { e.preventDefault() if (!stripe || !elements) return; await stripe.confirmSetup({ elements, confirmParams: { return_url: 'http://localhost:3000' } }) }}> <PaymentElement /> <button type="submit">Submit</button> </form> ) } その後、Homeコンポーネントを以下のように変更します。 <Elements stripe={stripePromise} options={options}> - <PaymentElement /> + <Payment /> </Elements> フォームsubmit時の処理に利用するuseStripeとuseElementsは、Elementsの子孫要素でしか利用できません。 そのため、少し手間ですがElementsを利用しているコンポーネントとは別のコンポーネントで実装する必要があります。 Step5: テストカード情報を入力してみる あとは実際にフォームを動かして動作を確認してみましょう。 テストモードのAPIキーを利用していれば、4242424242424242や5555555555554444などのテスト用カード番号と、現在の日付より未来の年月、ランダムな3桁の数字の3種類でカード情報を入力できます。 保存に成功すれば、Stripe Dashboardで顧客データを確認できます。 Tips: Customerデータの更新・登録タイミング ユーザー名やEmailなどの顧客情報をStripeのCustomerに登録したい場合、今回のサンプルではSetupIntentの作成APIにデータを送信する必要があります。 pages/index.jsx fetch('/api/setup-intent', { method: 'post', body: { username: getCurrentUsername(), email: getCurrentUserEmail(), } }).then(res => res.json()) pages/api/setup-intent.js const customer = await stripe.customers.create({ + name: req.body.username, + email: req.body.email, metadata: { app_username: req.body.username } }) もしカード情報の入力フォームと一緒に情報を入力させたい場合は、confirmSetupより先にAPIを呼び出すことをお勧めします。 await fetch('/api/update-customer', { method: 'post', body: { ... } }) await stripe.confirmSetup(...) Appendix: 保存後のリダイレクトについて stripe.confirmSetupで保存処理を実行する際、return_urlに指定したURLへのリダイレクトが実行されます。 そしてリダイレクト後のURlには、以下のクエリ文字列が追加されています。 setup_intent setup_intent_client_secret redirect_status Stripe JS SDKでretrieveSetupIntentを実行したい場合などに利用できます。 Next.jsのuseRouterなどを使って取得しましょう。 [PR] Stripe開発者向け情報をQiitaにて配信中! 2021年12月よりQiitaにて、Stripe開発者のためのブログ記事更新を開始しました。 [Stripe Updates]:開発者向けStripeアップデート紹介・解説 ユースケース別のStripe製品や実装サンプルの紹介 Stripeと外部サービス・OSSとの連携方法やTipsの紹介 初心者向けのチュートリアル(予定) など、Stripeを利用してオンラインビジネスを始める方法について随時更新してまいります。 -> Stripe Organizationsをフォローして最新情報をQiitaで受け取る
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MDN Web Docsの「ウェブ開発を学ぶ - 非同期 JavaScript」を翻訳してみた Part.5

こんにちはWebエンジニアのmasakichiです。 MDN Web Docsの「ウェブ開発を学ぶ - 非同期 JavaScript」を翻訳してみた5つ目の記事です。 全部で5記事あります。 Introducing asynchronous JavaScript Cooperative asynchronous JavaScript: Timeouts and intervals Graceful asynchronous programming with Promises Making asynchronous programming easier with async and await Choosing the right approach ←いまここ 翻訳箇所について こちらのページの日本語未翻訳記事です。 なぜ翻訳したか JavaScriptの非同期処理。特にPromiseというやつが、ぼんやりわかっているけど完全には理解していない状態がずっと続いていました。 そんな中、MDN Web Docsの解説がすごくわかりやすく一気に理解できました。 しかし、これらの記事は日本語に翻訳されていないという問題が。。。 ぜひ非同期処理で悩める同志にも読んでほしい。という想いで翻訳作業をしてみました。 留意点 筆者は英語がそこまで得意というわけではありません。DeepLの力を借りて、翻訳していますので、日本語訳が不自然なところや一部、情報を省略・意訳しています。あらかじめご了承ください。 ライセンスは下記です。 This Article by Mozilla Contributors is licensed under CC-BY-SA 2.5. 正しいアプローチの選択ß このモジュールの最後には、これまで説明してきたさまざまなコーディングテクニックや機能について、どのような場合に使用すべきかを簡単に説明し、必要に応じて推奨事項やよくある落とし穴について注意喚起します。このリソースは、時間の経過とともに追加していく予定です。 前提条件 基本的なコンピュータリテラシー / JavaScriptの基本をそれなりに理解していること。 目的 様々な非同期プログラミング技術を使用するタイミングを適切に選択できるようになる。 非同期コールバック 一般に古いスタイルのAPIでは、ある関数が別の関数にパラメータとして渡され、その関数が非同期処理の完了時に呼び出され、コールバックがその結果に対して何かを行うというものである。これはプロミスの前身となるもので、効率や柔軟性に欠ける。必要なときだけ使ってください. コード例 XMLHttpRequest APIでリソースをロードする例です。 function loadAsset(url, type, callback) { let xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = type; xhr.onload = function() { callback(xhr.response); }; xhr.send(); } function displayImage(blob) { let objectURL = URL.createObjectURL(blob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } loadAsset('coffee.jpg', 'blob', displayImage); 落とし穴 コールバックが入れ子になっていると、煩雑で読みにくい(=「コールバック地獄」)。 コールバック関数のエラーで起きた場合はネストの各レベルで一度呼ばれる必要がありますが、プロミスではチェーン全体のエラーを処理するために .catch() ブロックをひとつ使うだけで済みます。 非同期コールバックはあまり優雅ではありません。 プロミスコールバックはイベントキューに入れられた厳密な順序で常に呼び出されます; 非同期コールバックはそうではありません。 非同期コールバックは、サードパーティライブラリに渡されたときに、関数がどのように実行されるかの完全な制御を失います。 ブラウザの互換性 APIにおけるコールバックの正確なサポートは、特定のAPIに依存します。より具体的なサポート情報については、使用しているAPIのリファレンスドキュメントを参照してください。 setTimeout() setTimeout()は、任意の時間が経過した後に関数を実行することができるメソッドです。 コード例 ここでは、ブラウザは匿名関数を実行する前に2秒間待機し、その後アラートメッセージを表示します。 let myGreeting = setTimeout(function() { alert('Hello, Mr. Universe!'); }, 2000) 落とし穴 再帰的なsetTimeout()呼び出しは、setInterval()と同様に、次のようなコードで、関数を繰り返し実行することができます。 let i = 1; setTimeout(function run() { console.log(i); i++; setTimeout(run, 100); }, 100); 再帰的なsetTimeout()とsetInterval()には違いがあります。 再帰的な setTimeout() は、少なくとも指定した時間 (この例では 100 ミリ秒) の実行間隔を保証します。この間隔は、コードの実行時間に関係なく同じになります。 setInterval() では、選択した間隔に、実行したいコードの実行にかかる時間が含まれます。例えば、コードの実行に 40 ミリ秒かかるとすると、間隔を 60 ミリ秒に設定するだけです。 コードの実行時間が割り当てた時間間隔よりも長くなる可能性がある場合は、再帰的な setTimeout() を使用したほうがよいでしょう - これならコードの実行時間に関係なく実行間隔を一定に保つことができ、エラーになることもありません。 setInterval() setInterval()は、関数の実行間隔を設定して繰り返し実行することができるメソッドです。requestAnimationFrame()ほど効率的ではありませんが、実行速度/フレームレートを選択することができます。 コード例 次の関数は、新しい Date() オブジェクトを作成し、toLocaleTimeString() を使ってそこから時間文字列を取り出し、UIに表示します。そして、setInterval()を使って1秒に1回実行し、1秒に1回更新されるデジタル時計の効果を作り出しています。 function displayTime() { let date = new Date(); let time = date.toLocaleTimeString(); document.querySelector('.clock').textContent = time; } displayTime(); const createClock = setInterval(displayTime, 1000); 落とし穴 フレームレートはアニメーションが実行されているシステムに最適化されておらず、やや非効率的である可能性があります。特定の (遅い) フレームレートを選択する必要がない限り、一般的には requestAnimationFrame() を使用したほうがよいでしょう。 requestAnimationFrame() requestAnimationFrame() は、現在のブラウザ/システムで利用可能な最高のフレームレートで、関数を繰り返し効率的に実行できるようにするメソッドです。特定のフレームレートが必要な場合を除き、可能な限り setInterval()/recursive setTimeout() の代わりにこのメソッドを使用する必要があります。 コード例 シンプルなアニメーションのスピナーです。 const spinner = document.querySelector('div'); let rotateCount = 0; let startTime = null; let rAF; function draw(timestamp) { if(!startTime) { startTime = timestamp; } rotateCount = (timestamp - startTime) / 3; if(rotateCount > 359) { rotateCount %= 360; } spinner.style.transform = 'rotate(' + rotateCount + 'deg)'; rAF = requestAnimationFrame(draw); } draw(); 落とし穴 requestAnimationFrame()では、特定のフレームレートを選択することができません。より遅いフレームレートでアニメーションを実行する必要がある場合は、 setInterval() や再帰的な setTimeout() を使用する必要があります。 Promises プロミスは、非同期処理を実行し、それが確実に完了するまで待ってから、その結果に基づいて別の処理を実行することができるJavaScriptの機能である。プロミスは現代の非同期JavaScriptのバックボーンとなっています。 コード例 次のコードは、サーバーから画像を取得し、<img>要素内に表示するものです。 fetch('coffee.jpg') .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { return response.blob(); } }) .then(myBlob => { let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }) .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); 落とし穴 プロミスチェーンは複雑で、解析が困難な場合があります。いくつものプロミスをネストすると、コールバック地獄と似たようなトラブルに見舞われることがあります。例えば remotedb.allDocs({ include_docs: true, attachments: true }).then(function (result) { let docs = result.rows; docs.forEach(function(element) { localdb.put(element.doc).then(function(response) { alert("Pulled doc with id " + element.doc._id + " and added to local db."); }).catch(function (err) { if (err.name == 'conflict') { localdb.get(element.doc._id).then(function (resp) { localdb.remove(resp._id, resp._rev).then(function (resp) { // et cetera... プロミスの連鎖力を利用して、よりフラットでパースしやすい構造で行く方が良いのです。 remotedb.allDocs(...).then(function (resultOfAllDocs) { return localdb.put(...); }).then(function (resultOfPut) { return localdb.get(...); }).then(function (resultOfGet) { return localdb.put(...); }).catch(function (err) { console.log(err); }); もしくは、こうも書けます。 remotedb.allDocs(...) .then(resultOfAllDocs => { return localdb.put(...); }) .then(resultOfPut => { return localdb.get(...); }) .then(resultOfGet => { return localdb.put(...); }) .catch(err => console.log(err)); これで基本的なことはかなりカバーできたと思います。もっと完全な扱いについては、Nolan Lawsonによる優れたWe have a problem with promises をご覧ください。 Promise.all() 複数のプロミスが完了するのを待ってから、他のすべてのプロミスの結果に基づいてさらに処理を実行することができるJavaScriptの機能です。 コード例 次の例では、サーバーから複数のリソースを取得し、Promise.all()を使ってそれらのリソースがすべて利用可能になるのを待ってから、すべてのリソースを表示します。 function fetchAndDecode(url, type) { return fetch(url).then(response => { if(type === 'blob') { return response.blob(); } else if(type === 'text') { return response.text(); } }) .catch(e => { console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message); }); } let coffee = fetchAndDecode('coffee.jpg', 'blob'); let tea = fetchAndDecode('tea.jpg', 'blob'); let description = fetchAndDecode('description.txt', 'text'); Promise.all([coffee, tea, description]).then(values => { console.log(values); let objectURL1 = URL.createObjectURL(values[0]); let objectURL2 = URL.createObjectURL(values[1]); let descText = values[2]; let image1 = document.createElement('img'); let image2 = document.createElement('img'); image1.src = objectURL1; image2.src = objectURL2; document.body.appendChild(image1); document.body.appendChild(image2); let para = document.createElement('p'); para.textContent = descText; document.body.appendChild(para); }); 落とし穴 Promise.all()が拒否する場合、その配列パラメータに入力されている1つ以上のプロミスが拒否しているか、プロミスを全く返さない可能性があります。あるいは、プロミスを返さないかもしれません。 Async/await プロミスの上に構築された糖衣構文で、同期のコールバックコードを書くような構文で非同期処理を実行することができます。 コード例 以下の例は、以前見た画像を取得して表示するシンプルなプロミスの例をリファクタリングして、async/awaitを使って書いたものです。 async function myFetch() { let response = await fetch('coffee.jpg'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { let myBlob = await response.blob(); let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } } myFetch() .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); 落とし穴 await 演算子は、非同期関数の内部や、コードのトップレベルのコンテキストで使用することはできません。このため、ラッパーの作成が必要になることがあります。これは、状況によっては少しイライラするかもしれませんが、たいていの場合、それだけの価値があります。 ブラウザの async/await のサポートは promises のサポートほど良くはありません。もしあなたがasync/awaitを使いたいが、古いブラウザのサポートが気になるなら、BabelJSライブラリの使用を検討することができます - これは、最新のJavaScriptを使ってアプリケーションを書き、ユーザーのブラウザに必要な変更があれば、Babelがそれを判断することを可能にします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの非同期を理解するにはMDN Web Docsを読むべき。翻訳しといたよ Part.5

こんにちはWebエンジニアのmasakichiです。 JavaScriptの非同期を理解するにはMDN Web Docsを読むべき。翻訳しといたよ 5つ目の記事です。 全部で5記事あります。 Introducing asynchronous JavaScript Cooperative asynchronous JavaScript: Timeouts and intervals Graceful asynchronous programming with Promises Making asynchronous programming easier with async and await Choosing the right approach ←いまここ 翻訳箇所について こちらのページの日本語未翻訳記事です。 なぜ翻訳したか JavaScriptの非同期処理。特にPromiseというやつが、ぼんやりわかっているけど完全には理解していない状態がずっと続いていました。 そんな中、MDN Web Docsの解説がすごくわかりやすく一気に理解できました。 しかし、これらの記事は日本語に翻訳されていないという問題が。。。 ぜひ非同期処理で悩める同志にも読んでほしい。という想いで翻訳作業をしてみました。 留意点 筆者は英語がそこまで得意というわけではありません。DeepLの力を借りて、翻訳していますので、日本語訳が不自然なところや一部、情報を省略・意訳しています。あらかじめご了承ください。 ライセンスは下記です。 This Article by Mozilla Contributors is licensed under CC-BY-SA 2.5. 正しいアプローチの選択ß このモジュールの最後には、これまで説明してきたさまざまなコーディングテクニックや機能について、どのような場合に使用すべきかを簡単に説明し、必要に応じて推奨事項やよくある落とし穴について注意喚起します。このリソースは、時間の経過とともに追加していく予定です。 前提条件 基本的なコンピュータリテラシー / JavaScriptの基本をそれなりに理解していること。 目的 様々な非同期プログラミング技術を使用するタイミングを適切に選択できるようになる。 非同期コールバック 一般に古いスタイルのAPIでは、ある関数が別の関数にパラメータとして渡され、その関数が非同期処理の完了時に呼び出され、コールバックがその結果に対して何かを行うというものである。これはプロミスの前身となるもので、効率や柔軟性に欠ける。必要なときだけ使ってください. コード例 XMLHttpRequest APIでリソースをロードする例です。 function loadAsset(url, type, callback) { let xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = type; xhr.onload = function() { callback(xhr.response); }; xhr.send(); } function displayImage(blob) { let objectURL = URL.createObjectURL(blob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } loadAsset('coffee.jpg', 'blob', displayImage); 落とし穴 コールバックが入れ子になっていると、煩雑で読みにくい(=「コールバック地獄」)。 コールバック関数のエラーで起きた場合はネストの各レベルで一度呼ばれる必要がありますが、プロミスではチェーン全体のエラーを処理するために .catch() ブロックをひとつ使うだけで済みます。 非同期コールバックはあまり優雅ではありません。 プロミスコールバックはイベントキューに入れられた厳密な順序で常に呼び出されます; 非同期コールバックはそうではありません。 非同期コールバックは、サードパーティライブラリに渡されたときに、関数がどのように実行されるかの完全な制御を失います。 ブラウザの互換性 APIにおけるコールバックの正確なサポートは、特定のAPIに依存します。より具体的なサポート情報については、使用しているAPIのリファレンスドキュメントを参照してください。 setTimeout() setTimeout()は、任意の時間が経過した後に関数を実行することができるメソッドです。 コード例 ここでは、ブラウザは匿名関数を実行する前に2秒間待機し、その後アラートメッセージを表示します。 let myGreeting = setTimeout(function() { alert('Hello, Mr. Universe!'); }, 2000) 落とし穴 再帰的なsetTimeout()呼び出しは、setInterval()と同様に、次のようなコードで、関数を繰り返し実行することができます。 let i = 1; setTimeout(function run() { console.log(i); i++; setTimeout(run, 100); }, 100); 再帰的なsetTimeout()とsetInterval()には違いがあります。 再帰的な setTimeout() は、少なくとも指定した時間 (この例では 100 ミリ秒) の実行間隔を保証します。この間隔は、コードの実行時間に関係なく同じになります。 setInterval() では、選択した間隔に、実行したいコードの実行にかかる時間が含まれます。例えば、コードの実行に 40 ミリ秒かかるとすると、間隔を 60 ミリ秒に設定するだけです。 コードの実行時間が割り当てた時間間隔よりも長くなる可能性がある場合は、再帰的な setTimeout() を使用したほうがよいでしょう - これならコードの実行時間に関係なく実行間隔を一定に保つことができ、エラーになることもありません。 setInterval() setInterval()は、関数の実行間隔を設定して繰り返し実行することができるメソッドです。requestAnimationFrame()ほど効率的ではありませんが、実行速度/フレームレートを選択することができます。 コード例 次の関数は、新しい Date() オブジェクトを作成し、toLocaleTimeString() を使ってそこから時間文字列を取り出し、UIに表示します。そして、setInterval()を使って1秒に1回実行し、1秒に1回更新されるデジタル時計の効果を作り出しています。 function displayTime() { let date = new Date(); let time = date.toLocaleTimeString(); document.querySelector('.clock').textContent = time; } displayTime(); const createClock = setInterval(displayTime, 1000); 落とし穴 フレームレートはアニメーションが実行されているシステムに最適化されておらず、やや非効率的である可能性があります。特定の (遅い) フレームレートを選択する必要がない限り、一般的には requestAnimationFrame() を使用したほうがよいでしょう。 requestAnimationFrame() requestAnimationFrame() は、現在のブラウザ/システムで利用可能な最高のフレームレートで、関数を繰り返し効率的に実行できるようにするメソッドです。特定のフレームレートが必要な場合を除き、可能な限り setInterval()/recursive setTimeout() の代わりにこのメソッドを使用する必要があります。 コード例 シンプルなアニメーションのスピナーです。 const spinner = document.querySelector('div'); let rotateCount = 0; let startTime = null; let rAF; function draw(timestamp) { if(!startTime) { startTime = timestamp; } rotateCount = (timestamp - startTime) / 3; if(rotateCount > 359) { rotateCount %= 360; } spinner.style.transform = 'rotate(' + rotateCount + 'deg)'; rAF = requestAnimationFrame(draw); } draw(); 落とし穴 requestAnimationFrame()では、特定のフレームレートを選択することができません。より遅いフレームレートでアニメーションを実行する必要がある場合は、 setInterval() や再帰的な setTimeout() を使用する必要があります。 Promises プロミスは、非同期処理を実行し、それが確実に完了するまで待ってから、その結果に基づいて別の処理を実行することができるJavaScriptの機能である。プロミスは現代の非同期JavaScriptのバックボーンとなっています。 コード例 次のコードは、サーバーから画像を取得し、<img>要素内に表示するものです。 fetch('coffee.jpg') .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { return response.blob(); } }) .then(myBlob => { let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }) .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); 落とし穴 プロミスチェーンは複雑で、解析が困難な場合があります。いくつものプロミスをネストすると、コールバック地獄と似たようなトラブルに見舞われることがあります。例えば remotedb.allDocs({ include_docs: true, attachments: true }).then(function (result) { let docs = result.rows; docs.forEach(function(element) { localdb.put(element.doc).then(function(response) { alert("Pulled doc with id " + element.doc._id + " and added to local db."); }).catch(function (err) { if (err.name == 'conflict') { localdb.get(element.doc._id).then(function (resp) { localdb.remove(resp._id, resp._rev).then(function (resp) { // et cetera... プロミスの連鎖力を利用して、よりフラットでパースしやすい構造で行く方が良いのです。 remotedb.allDocs(...).then(function (resultOfAllDocs) { return localdb.put(...); }).then(function (resultOfPut) { return localdb.get(...); }).then(function (resultOfGet) { return localdb.put(...); }).catch(function (err) { console.log(err); }); もしくは、こうも書けます。 remotedb.allDocs(...) .then(resultOfAllDocs => { return localdb.put(...); }) .then(resultOfPut => { return localdb.get(...); }) .then(resultOfGet => { return localdb.put(...); }) .catch(err => console.log(err)); これで基本的なことはかなりカバーできたと思います。もっと完全な扱いについては、Nolan Lawsonによる優れたWe have a problem with promises をご覧ください。 Promise.all() 複数のプロミスが完了するのを待ってから、他のすべてのプロミスの結果に基づいてさらに処理を実行することができるJavaScriptの機能です。 コード例 次の例では、サーバーから複数のリソースを取得し、Promise.all()を使ってそれらのリソースがすべて利用可能になるのを待ってから、すべてのリソースを表示します。 function fetchAndDecode(url, type) { return fetch(url).then(response => { if(type === 'blob') { return response.blob(); } else if(type === 'text') { return response.text(); } }) .catch(e => { console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message); }); } let coffee = fetchAndDecode('coffee.jpg', 'blob'); let tea = fetchAndDecode('tea.jpg', 'blob'); let description = fetchAndDecode('description.txt', 'text'); Promise.all([coffee, tea, description]).then(values => { console.log(values); let objectURL1 = URL.createObjectURL(values[0]); let objectURL2 = URL.createObjectURL(values[1]); let descText = values[2]; let image1 = document.createElement('img'); let image2 = document.createElement('img'); image1.src = objectURL1; image2.src = objectURL2; document.body.appendChild(image1); document.body.appendChild(image2); let para = document.createElement('p'); para.textContent = descText; document.body.appendChild(para); }); 落とし穴 Promise.all()が拒否する場合、その配列パラメータに入力されている1つ以上のプロミスが拒否しているか、プロミスを全く返さない可能性があります。あるいは、プロミスを返さないかもしれません。 Async/await プロミスの上に構築された糖衣構文で、同期のコールバックコードを書くような構文で非同期処理を実行することができます。 コード例 以下の例は、以前見た画像を取得して表示するシンプルなプロミスの例をリファクタリングして、async/awaitを使って書いたものです。 async function myFetch() { let response = await fetch('coffee.jpg'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { let myBlob = await response.blob(); let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } } myFetch() .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); 落とし穴 await 演算子は、非同期関数の内部や、コードのトップレベルのコンテキストで使用することはできません。このため、ラッパーの作成が必要になることがあります。これは、状況によっては少しイライラするかもしれませんが、たいていの場合、それだけの価値があります。 ブラウザの async/await のサポートは promises のサポートほど良くはありません。もしあなたがasync/awaitを使いたいが、古いブラウザのサポートが気になるなら、BabelJSライブラリの使用を検討することができます - これは、最新のJavaScriptを使ってアプリケーションを書き、ユーザーのブラウザに必要な変更があれば、Babelがそれを判断することを可能にします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MDN Web Docsの「ウェブ開発を学ぶ - 非同期 JavaScript」を翻訳してみた Part.4

こんにちはWebエンジニアのmasakichiです。 MDN Web Docsの「ウェブ開発を学ぶ - 非同期 JavaScript」を翻訳してみた4つ目の記事です。 全部で5記事あります。 Introducing asynchronous JavaScript Cooperative asynchronous JavaScript: Timeouts and intervals Graceful asynchronous programming with Promises Making asynchronous programming easier with async and await ←いまここ Choosing the right approach 翻訳箇所について こちらのページの日本語未翻訳記事です。 なぜ翻訳したか JavaScriptの非同期処理。特にPromiseというやつが、ぼんやりわかっているけど完全には理解していない状態がずっと続いていました。 そんな中、MDN Web Docsの解説がすごくわかりやすく一気に理解できました。 しかし、これらの記事は日本語に翻訳されていないという問題が。。。 ぜひ非同期処理で悩める同志にも読んでほしい。という想いで翻訳作業をしてみました。 留意点 筆者は英語がそこまで得意というわけではありません。DeepLの力を借りて、翻訳していますので、日本語訳が不自然なところや一部、情報を省略・意訳しています。あらかじめご了承ください。 ライセンスは下記です。 This Article by Mozilla Contributors is licensed under CC-BY-SA 2.5. asyncとawaitで非同期プログラミングを容易にする 最近、JavaScriptには、ECMAScript 2017でasync関数とawaitキーワード追加されました。これらの機能は基本的にPromiseの糖衣構文として機能し、非同期コードを書きやすくし、その後の読み込みも容易にします。これらは非同期コードをより旧来の同期コードのように見せるので、学ぶ価値は十分にあります。この記事はあなたが知る必要のあることを提供します。 前提条件 基本的なコンピュータリテラシー / JavaScriptの基礎の合理的な理解、非同期コード一般とPromiseに関する理解 目的 async/awaitの使い方を理解すること。 async/awaitの基礎知識 コードでasync/awaitを使用するには、2つのパートがあります。 asyncキーワード まず、asyncキーワードですが、これは関数宣言の前に置くと非同期関数になります。async関数とは、awaitキーワードが非同期コードを呼び出すために使われる可能性を想定している関数です。 ブラウザのJSコンソールに次の行を入力してみてください。 function hello() { return "Hello" }; hello(); この関数は "Hello "を返しますが、これは特別なことではありませんね。 しかし、これを非同期関数にしたらどうでしょうか?次のようにしてみてください。 async function hello() { return "Hello" }; hello(); 関数を呼び出すと、今度はPromiseが返されるようになりました。これは非同期関数の特徴の1つで、その戻り値はPromiseに変換されることが保証されています。 また、以下のように非同期関数式を作成することもできます。 let hello = async function() { return "Hello" }; hello(); アロー機能を使うこともできます。 let hello = async () => "Hello"; これらはすべて基本的に同じことをします。 Promiseが実行されたときに返される値を実際に消費するには、Promiseを返しているので、.then()ブロックを使用することができます。 hello().then((value) => console.log(value)); 以下のような省略形でも構いません。 hello().then(console.log) 前回の記事で見た形式ですね。 このように、関数にasyncキーワードを追加すると、値を直接返すのではなく、Promiseを返すように指示することができるのです。 awaitキーワード awaitは、通常のJavaScriptコードの非同期関数の中でしか動作しませんが、JavaScriptモジュールでは単独で使用することができます。 await を非同期Promise関数の前に置くと、Promiseが成立するまでその行のコードを一時停止し、その結果の値を返します。 Web API 関数を含め、Promise を返す任意の関数を呼び出す際に await を使用することができます。 以下は些細な例です。 async function hello() { return await Promise.resolve("Hello"); }; hello().then(alert); もちろん、上記の例は構文の説明にはなっていますが、あまり役に立ちません。次に、実際の例を見てみましょう。 async/awaitでPromiseコードを書き直す 前回の記事で見た、簡単なfetch()の例を振り返ってみましょう。 fetch('coffee.jpg') .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.blob(); }) .then(myBlob => { let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }) .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); ここまでで、Promiseとその仕組みについてそれなりに理解できたと思いますが、これをasync/awaitを使うように変換して、どれだけ物事がシンプルになるかを見てみましょう。 async function myFetch() { let response = await fetch('coffee.jpg'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } let myBlob = await response.blob(); let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } myFetch() .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); コードがよりシンプルになり、理解しやすくなりました。.then()ブロックをあちこちに使うのはもはや必要ありません。 asyncキーワードは関数をPromiseに変えるので、Promiseとawaitのハイブリッドアプローチを使うようにコードをリファクタリングして、関数の後半を新しいブロックに出し、より柔軟性を持たせることもできます。 async function myFetch() { let response = await fetch('coffee.jpg'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.blob(); } myFetch() .then(blob => { let objectURL = URL.createObjectURL(blob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }) .catch(e => console.log(e)); どういう仕組みなのでしょうか 上記のコードは関数内にラップされており、functionキーワードの前にasyncキーワードを含んでいることに注意してください。これは必要なことです。非同期コードを実行するコードブロックを定義するために、非同期関数を作成しなければなりません。先ほど述べたように、awaitは非同期関数の中でしか動作しません。 myFetch()関数定義の内部では、コードが以前のPromiseバージョンに酷似していることがわかりますが、いくつかの相違点があることがわかります。Promiseの各メソッドの末尾に .then() ブロックを連結する必要がなく、メソッド呼び出しの前に await キーワードを追加し、結果を変数に代入するだけでよいのです。awaitキーワードは、JavaScriptの処理を一時停止させ、非同期関数呼び出しがその結果を返すまで、他のコードの実行を許可しないようにします。 そして、それが完了すると、コードは次の行から実行されます。例えば let response = await fetch('coffee.jpg'); fullfilledしたfetch()のPromiseが返すレスポンスは、そのレスポンスが利用可能になった時点でresponse変数に代入され、パーサーはそれが発生するまでこの行で一時停止します。レスポンスが利用可能になると、パーサーは次の行に移動し、そこからBlobを作成します。この行も非同期Promiseのメソッドを呼び出すので、ここでもawaitを使用しています。操作の結果が返ってきたら、それをmyFetch()関数から返しています。 つまり、myFetch()関数を呼び出すとPromiseが返されるので、その末尾に.then()をチェーンして、画面上にBlobを表示する処理を行うことができるのです。 .then()ブロックが少なく、同期コードのように見えるので、とても直感的に操作できます。 エラー処理の追加 エラー処理を追加したい場合は、いくつかのオプションがあります。 async/awaitで同期的なtry...catch構造を使うことができます。この例は、上で紹介した最初のバージョンのコードを発展させたものです。 async function myFetch() { try { let response = await fetch('coffee.jpg'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } let myBlob = await response.blob(); let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } catch(e) { console.log(e); } } myFetch(); catch() {} ブロックには、e と呼ばれるエラーオブジェクトが渡されます。これをコンソールにログ出力すると、コードのどこでエラーが発生したかを示す詳細なエラーメッセージが表示されます。 もし、上で紹介したコードの2番目の(リファクタリングされた)バージョンを使いたい場合は、ハイブリッドアプローチを続けて、.then()呼び出しの最後に.catch()ブロックをこのように連結する方がよいでしょう。 async function myFetch() { let response = await fetch('coffee.jpg'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.blob(); } myFetch() .then(blob => { let objectURL = URL.createObjectURL(blob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }) .catch(e => console.log(e)); これは、.catch()ブロックが、非同期関数呼び出しとPromiseチェーンの両方で発生するエラーをキャッチするためです。ここで try/catch ブロックを使ってしまうと、myFetch() 関数が呼び出されたときに、まだ未処理のエラーが発生する可能性があります。 Promise.all()のAwait async/awaitはPromiseの上に構築されているので、Promiseが提供するすべての機能と互換性があります。これは Promise.all() を含みます。単純な同期コードのように見える方法で、変数に返されたすべての結果を得るために Promise.all() の呼び出しを待つことができます。再び、前回の記事で見た例に戻りましょう。別のタブで開いておいて、以下の新しいバージョンと比較対照してください。 これをasync/awaitに変換すると、次のようになります。 async function fetchAndDecode(url, type) { let response = await fetch(url); let content; if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { if(type === 'blob') { content = await response.blob(); } else if(type === 'text') { content = await response.text(); } } return content; } async function displayContent() { let coffee = fetchAndDecode('coffee.jpg', 'blob'); let tea = fetchAndDecode('tea.jpg', 'blob'); let description = fetchAndDecode('description.txt', 'text'); let values = await Promise.all([coffee, tea, description]); let objectURL1 = URL.createObjectURL(values[0]); let objectURL2 = URL.createObjectURL(values[1]); let descText = values[2]; let image1 = document.createElement('img'); let image2 = document.createElement('img'); image1.src = objectURL1; image2.src = objectURL2; document.body.appendChild(image1); document.body.appendChild(image2); let para = document.createElement('p'); para.textContent = descText; document.body.appendChild(para); } displayContent() .catch(e => console.log(e)); fetchAndDecode()関数が、ほんの少しの変更で簡単に非同期関数に変換されたことが分かると思います。Promise.all()の行をご覧ください。 let values = await Promise.all([coffee, tea, description]); ここでawaitを使うことで、3つのPromiseの結果がすべて利用可能になったときに、同期コードのように非常によく見える方法で、値の配列に返されるすべての結果を取得することができるようになりました。すべてのコードを新しい非同期関数 displayContent() で包まなければならず、コードの行数はあまり減っていませんが、コードの大部分を .then() ブロックから移動させることができるので、より読みやすいプログラムを残して、素晴らしく便利な簡略化を実現しています。 エラー処理のために、displayContent()の呼び出しに.catch()ブロックを含めました。これは、両方の関数で発生したエラーを処理します。 async/awaitの遅延への対応 Async/awaitはあなたのコードを同期的に見せ、ある意味ではより同期的に動作させることができます。awaitキーワードは、同期操作とまったく同じように、Promiseが実行されるまで、それに続くすべてのコードの実行をブロックします。その間に他のタスクが実行されることはありますが、awaitされたコードはブロックされます。例えば、以下のようになります。 async function makeResult(items) { let newArr = []; for(let i = 0; i < items.length; i++) { newArr.push('word_' + i); } return newArr; } async function getResult() { let result = await makeResult(items); // Blocked on this line useThatResult(result); // Will not be executed before makeResult() is done } その結果、多数の待ち受けPromiseが次々と発生し、コードが遅くなる可能性があります。それぞれのawaitは前のものが終了するのを待ちます。一方、実際にあなたが望むのは、async/awaitを使っていないときのように、同時に処理を開始する約束のためかもしれません。 slow-async-await.html (ソースコード参照) と fast-async-await.html (ソースコード参照) という2つの例を見てみましょう。どちらも、setTimeout()呼び出しで非同期処理を装ったカスタムPromise関数から始まります。 function timeoutPromise(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ resolve("done"); }, interval); }); }; それぞれにtimeTest()非同期関数が含まれており、3回のtimeoutPromise()呼び出しを待ちます。 async function timeTest() { ... } それぞれ、開始時刻の記録、timeTest()Promiseが実行されるまでの時間を確認、終了時刻を記録、操作にかかった総時間を報告して終了します。 let startTime = Date.now(); timeTest() .then(() => { let finishTime = Date.now(); let timeTaken = finishTime - startTime; alert("Time taken in milliseconds: " + timeTaken); }) それぞれのケースで異なるのはtimeTest()関数です。 slow-async-await.htmlの例では、timeTest()は次のようになります。 async function timeTest() { await timeoutPromise(3000); await timeoutPromise(3000); await timeoutPromise(3000); } ここでは、3つのtimeoutPromise()呼び出しを直接待ち、それぞれ3秒後に警告を出すようにしています。最初の例を実行すると、アラートボックスに合計約9秒の実行時間が表示されるのがわかるでしょう。 fast-async-await.htmlの例では、timeTest()は次のようになります。 async function timeTest() { const timeoutPromise1 = timeoutPromise(3000); const timeoutPromise2 = timeoutPromise(3000); const timeoutPromise3 = timeoutPromise(3000); await timeoutPromise1; await timeoutPromise2; await timeoutPromise3; } ここでは、3つのPromiseオブジェクトを変数に格納し、関連するプロセスをすべて同時に起動させる効果を持たせています。 次に、その結果を待ちます。約束はすべて基本的に同時に処理を始めたので、約束はすべて同時に達成されます。2番目の例を実行すると、アラートボックスで実行時間の合計が3秒強であることが報告されます! エラー処理 しかし、上記のパターンには問題があり、処理されないエラーが発生する可能性があります。 前の例を更新して、今度は拒否されたPromiseを追加し、最後にcatch文を追加してみましょう。 function timeoutPromiseResolve(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ resolve("successful"); }, interval); }); }; function timeoutPromiseReject(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ reject("error"); }, interval); }); }; async function timeTest() { await timeoutPromiseResolve(5000); await timeoutPromiseReject(2000); await timeoutPromiseResolve(3000); } let startTime = Date.now(); timeTest() .then(() => {}) .catch(e => { console.log(e); let finishTime = Date.now(); let timeTaken = finishTime - startTime; alert("Time taken in milliseconds: " + timeTaken); }) 上記の例では、エラーは適切に処理され、約7秒後にアラートが表示されます。 次に2つ目のパターンです。 function timeoutPromiseResolve(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ resolve("successful"); }, interval); }); }; function timeoutPromiseReject(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ reject("error"); }, interval); }); }; async function timeTest() { const timeoutPromiseResolve1 = timeoutPromiseResolve(5000); const timeoutPromiseReject2 = timeoutPromiseReject(2000); const timeoutPromiseResolve3 = timeoutPromiseResolve(3000); await timeoutPromiseResolve1; await timeoutPromiseReject2; await timeoutPromiseResolve3; } let startTime = Date.now(); timeTest() .then(() => {}) .catch(e => { console.log(e); let finishTime = Date.now(); let timeTaken = finishTime - startTime; alert("Time taken in milliseconds: " + timeTaken); }) この例では、コンソールに未処理のエラーが発生し(2秒後)、約5秒後にアラートが表示されています。 Promiseを並列に起動し、エラーを適切にキャッチするには、先に説明したように Promise.all() を使用すればよいでしょう。 function timeoutPromiseResolve(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ resolve("successful"); }, interval); }); }; function timeoutPromiseReject(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ reject("error"); }, interval); }); }; async function timeTest() { const timeoutPromiseResolve1 = timeoutPromiseResolve(5000); const timeoutPromiseReject2 = timeoutPromiseReject(2000); const timeoutPromiseResolve3 = timeoutPromiseResolve(3000); const results = await Promise.all([timeoutPromiseResolve1, timeoutPromiseReject2, timeoutPromiseResolve3]); return results; } let startTime = Date.now(); timeTest() .then(() => {}) .catch(e => { console.log(e); let finishTime = Date.now(); let timeTaken = finishTime - startTime; alert("Time taken in milliseconds: " + timeTaken); }) この例では、約2秒後にエラーが適切に処理され、また約2秒後にアラートが表示されています。 Promise.all()は、入力された約束のいずれかが拒否されたときに拒否されます。もし、いくつかの約束が拒否されたときでも、すべての約束を解決して、その満たされた値のいくつかを使いたいなら、代わりに Promise.allSettled() を使うことができます。 Async/awaitクラスのメソッド 最後に、クラスやオブジェクトのメソッドの前にasyncを追加して、それらがPromiseを返すようにしたり、その中でPromiseを待ったりすることも可能です。オブジェクト指向JavaScriptの記事で見たESクラスのコードを見て、そしてasyncメソッドを使った修正版を見てみてください。 class Person { constructor(first, last, age, gender, interests) { this.name = { first, last }; this.age = age; this.gender = gender; this.interests = interests; } async greeting() { return await Promise.resolve(`Hi! I'm ${this.name.first}`); }; farewell() { console.log(`${this.name.first} has left the building. Bye for now!`); }; } let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']); 最初のクラスのメソッドは、次のように使うことができます。 han.greeting().then(console.log); ブラウザサポート async/awaitを使用するかどうかを決定する際に考慮すべきことの1つは、古いブラウザのサポートです。これらはほとんどのブラウザのモダンバージョンで利用可能で、Promiseと同じです; 主なサポートの問題は Internet Explorer と Opera Mini で起こります。 もし、async/awaitを使いたいが、古いブラウザのサポートが心配であれば、BabelJSライブラリの使用を検討してみてください。非同期/待機をサポートしないブラウザに遭遇した場合、Babelのポリフィルは自動的に古いブラウザで動作するフォールバックを提供することができます。 結論 async/awaitは、読みやすく保守しやすい非同期コードを書くためのシンプルで素晴らしい方法を提供します。この記事を書いている時点では、ブラウザのサポートは他の非同期コード機構よりも制限されていますが、現在も将来も、学習して利用を検討する価値は十分にあると思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの非同期を理解するにはMDN Web Docsを読むべき。翻訳しといたよ Part.4

こんにちはWebエンジニアのmasakichiです。 JavaScriptの非同期を理解するにはMDN Web Docsを読むべき。翻訳しといたよ 4つ目の記事です。 全部で5記事あります。 Introducing asynchronous JavaScript Cooperative asynchronous JavaScript: Timeouts and intervals Graceful asynchronous programming with Promises Making asynchronous programming easier with async and await ←いまここ Choosing the right approach 翻訳箇所について こちらのページの日本語未翻訳記事です。 なぜ翻訳したか JavaScriptの非同期処理。特にPromiseというやつが、ぼんやりわかっているけど完全には理解していない状態がずっと続いていました。 そんな中、MDN Web Docsの解説がすごくわかりやすく一気に理解できました。 しかし、これらの記事は日本語に翻訳されていないという問題が。。。 ぜひ非同期処理で悩める同志にも読んでほしい。という想いで翻訳作業をしてみました。 留意点 筆者は英語がそこまで得意というわけではありません。DeepLの力を借りて、翻訳していますので、日本語訳が不自然なところや一部、情報を省略・意訳しています。あらかじめご了承ください。 ライセンスは下記です。 This Article by Mozilla Contributors is licensed under CC-BY-SA 2.5. asyncとawaitで非同期プログラミングを容易にする 最近、JavaScriptには、ECMAScript 2017でasync関数とawaitキーワード追加されました。これらの機能は基本的にPromiseの糖衣構文として機能し、非同期コードを書きやすくし、その後の読み込みも容易にします。これらは非同期コードをより旧来の同期コードのように見せるので、学ぶ価値は十分にあります。この記事はあなたが知る必要のあることを提供します。 前提条件 基本的なコンピュータリテラシー / JavaScriptの基礎の合理的な理解、非同期コード一般とPromiseに関する理解 目的 async/awaitの使い方を理解すること。 async/awaitの基礎知識 コードでasync/awaitを使用するには、2つのパートがあります。 asyncキーワード まず、asyncキーワードですが、これは関数宣言の前に置くと非同期関数になります。async関数とは、awaitキーワードが非同期コードを呼び出すために使われる可能性を想定している関数です。 ブラウザのJSコンソールに次の行を入力してみてください。 function hello() { return "Hello" }; hello(); この関数は "Hello "を返しますが、これは特別なことではありませんね。 しかし、これを非同期関数にしたらどうでしょうか?次のようにしてみてください。 async function hello() { return "Hello" }; hello(); 関数を呼び出すと、今度はPromiseが返されるようになりました。これは非同期関数の特徴の1つで、その戻り値はPromiseに変換されることが保証されています。 また、以下のように非同期関数式を作成することもできます。 let hello = async function() { return "Hello" }; hello(); アロー機能を使うこともできます。 let hello = async () => "Hello"; これらはすべて基本的に同じことをします。 Promiseが実行されたときに返される値を実際に消費するには、Promiseを返しているので、.then()ブロックを使用することができます。 hello().then((value) => console.log(value)); 以下のような省略形でも構いません。 hello().then(console.log) 前回の記事で見た形式ですね。 このように、関数にasyncキーワードを追加すると、値を直接返すのではなく、Promiseを返すように指示することができるのです。 awaitキーワード awaitは、通常のJavaScriptコードの非同期関数の中でしか動作しませんが、JavaScriptモジュールでは単独で使用することができます。 await を非同期Promise関数の前に置くと、Promiseが成立するまでその行のコードを一時停止し、その結果の値を返します。 Web API 関数を含め、Promise を返す任意の関数を呼び出す際に await を使用することができます。 以下は些細な例です。 async function hello() { return await Promise.resolve("Hello"); }; hello().then(alert); もちろん、上記の例は構文の説明にはなっていますが、あまり役に立ちません。次に、実際の例を見てみましょう。 async/awaitでPromiseコードを書き直す 前回の記事で見た、簡単なfetch()の例を振り返ってみましょう。 fetch('coffee.jpg') .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.blob(); }) .then(myBlob => { let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }) .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); ここまでで、Promiseとその仕組みについてそれなりに理解できたと思いますが、これをasync/awaitを使うように変換して、どれだけ物事がシンプルになるかを見てみましょう。 async function myFetch() { let response = await fetch('coffee.jpg'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } let myBlob = await response.blob(); let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } myFetch() .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); コードがよりシンプルになり、理解しやすくなりました。.then()ブロックをあちこちに使うのはもはや必要ありません。 asyncキーワードは関数をPromiseに変えるので、Promiseとawaitのハイブリッドアプローチを使うようにコードをリファクタリングして、関数の後半を新しいブロックに出し、より柔軟性を持たせることもできます。 async function myFetch() { let response = await fetch('coffee.jpg'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.blob(); } myFetch() .then(blob => { let objectURL = URL.createObjectURL(blob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }) .catch(e => console.log(e)); どういう仕組みなのでしょうか 上記のコードは関数内にラップされており、functionキーワードの前にasyncキーワードを含んでいることに注意してください。これは必要なことです。非同期コードを実行するコードブロックを定義するために、非同期関数を作成しなければなりません。先ほど述べたように、awaitは非同期関数の中でしか動作しません。 myFetch()関数定義の内部では、コードが以前のPromiseバージョンに酷似していることがわかりますが、いくつかの相違点があることがわかります。Promiseの各メソッドの末尾に .then() ブロックを連結する必要がなく、メソッド呼び出しの前に await キーワードを追加し、結果を変数に代入するだけでよいのです。awaitキーワードは、JavaScriptの処理を一時停止させ、非同期関数呼び出しがその結果を返すまで、他のコードの実行を許可しないようにします。 そして、それが完了すると、コードは次の行から実行されます。例えば let response = await fetch('coffee.jpg'); fullfilledしたfetch()のPromiseが返すレスポンスは、そのレスポンスが利用可能になった時点でresponse変数に代入され、パーサーはそれが発生するまでこの行で一時停止します。レスポンスが利用可能になると、パーサーは次の行に移動し、そこからBlobを作成します。この行も非同期Promiseのメソッドを呼び出すので、ここでもawaitを使用しています。操作の結果が返ってきたら、それをmyFetch()関数から返しています。 つまり、myFetch()関数を呼び出すとPromiseが返されるので、その末尾に.then()をチェーンして、画面上にBlobを表示する処理を行うことができるのです。 .then()ブロックが少なく、同期コードのように見えるので、とても直感的に操作できます。 エラー処理の追加 エラー処理を追加したい場合は、いくつかのオプションがあります。 async/awaitで同期的なtry...catch構造を使うことができます。この例は、上で紹介した最初のバージョンのコードを発展させたものです。 async function myFetch() { try { let response = await fetch('coffee.jpg'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } let myBlob = await response.blob(); let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } catch(e) { console.log(e); } } myFetch(); catch() {} ブロックには、e と呼ばれるエラーオブジェクトが渡されます。これをコンソールにログ出力すると、コードのどこでエラーが発生したかを示す詳細なエラーメッセージが表示されます。 もし、上で紹介したコードの2番目の(リファクタリングされた)バージョンを使いたい場合は、ハイブリッドアプローチを続けて、.then()呼び出しの最後に.catch()ブロックをこのように連結する方がよいでしょう。 async function myFetch() { let response = await fetch('coffee.jpg'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.blob(); } myFetch() .then(blob => { let objectURL = URL.createObjectURL(blob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }) .catch(e => console.log(e)); これは、.catch()ブロックが、非同期関数呼び出しとPromiseチェーンの両方で発生するエラーをキャッチするためです。ここで try/catch ブロックを使ってしまうと、myFetch() 関数が呼び出されたときに、まだ未処理のエラーが発生する可能性があります。 Promise.all()のAwait async/awaitはPromiseの上に構築されているので、Promiseが提供するすべての機能と互換性があります。これは Promise.all() を含みます。単純な同期コードのように見える方法で、変数に返されたすべての結果を得るために Promise.all() の呼び出しを待つことができます。再び、前回の記事で見た例に戻りましょう。別のタブで開いておいて、以下の新しいバージョンと比較対照してください。 これをasync/awaitに変換すると、次のようになります。 async function fetchAndDecode(url, type) { let response = await fetch(url); let content; if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { if(type === 'blob') { content = await response.blob(); } else if(type === 'text') { content = await response.text(); } } return content; } async function displayContent() { let coffee = fetchAndDecode('coffee.jpg', 'blob'); let tea = fetchAndDecode('tea.jpg', 'blob'); let description = fetchAndDecode('description.txt', 'text'); let values = await Promise.all([coffee, tea, description]); let objectURL1 = URL.createObjectURL(values[0]); let objectURL2 = URL.createObjectURL(values[1]); let descText = values[2]; let image1 = document.createElement('img'); let image2 = document.createElement('img'); image1.src = objectURL1; image2.src = objectURL2; document.body.appendChild(image1); document.body.appendChild(image2); let para = document.createElement('p'); para.textContent = descText; document.body.appendChild(para); } displayContent() .catch(e => console.log(e)); fetchAndDecode()関数が、ほんの少しの変更で簡単に非同期関数に変換されたことが分かると思います。Promise.all()の行をご覧ください。 let values = await Promise.all([coffee, tea, description]); ここでawaitを使うことで、3つのPromiseの結果がすべて利用可能になったときに、同期コードのように非常によく見える方法で、値の配列に返されるすべての結果を取得することができるようになりました。すべてのコードを新しい非同期関数 displayContent() で包まなければならず、コードの行数はあまり減っていませんが、コードの大部分を .then() ブロックから移動させることができるので、より読みやすいプログラムを残して、素晴らしく便利な簡略化を実現しています。 エラー処理のために、displayContent()の呼び出しに.catch()ブロックを含めました。これは、両方の関数で発生したエラーを処理します。 async/awaitの遅延への対応 Async/awaitはあなたのコードを同期的に見せ、ある意味ではより同期的に動作させることができます。awaitキーワードは、同期操作とまったく同じように、Promiseが実行されるまで、それに続くすべてのコードの実行をブロックします。その間に他のタスクが実行されることはありますが、awaitされたコードはブロックされます。例えば、以下のようになります。 async function makeResult(items) { let newArr = []; for(let i = 0; i < items.length; i++) { newArr.push('word_' + i); } return newArr; } async function getResult() { let result = await makeResult(items); // Blocked on this line useThatResult(result); // Will not be executed before makeResult() is done } その結果、多数の待ち受けPromiseが次々と発生し、コードが遅くなる可能性があります。それぞれのawaitは前のものが終了するのを待ちます。一方、実際にあなたが望むのは、async/awaitを使っていないときのように、同時に処理を開始する約束のためかもしれません。 slow-async-await.html (ソースコード参照) と fast-async-await.html (ソースコード参照) という2つの例を見てみましょう。どちらも、setTimeout()呼び出しで非同期処理を装ったカスタムPromise関数から始まります。 function timeoutPromise(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ resolve("done"); }, interval); }); }; それぞれにtimeTest()非同期関数が含まれており、3回のtimeoutPromise()呼び出しを待ちます。 async function timeTest() { ... } それぞれ、開始時刻の記録、timeTest()Promiseが実行されるまでの時間を確認、終了時刻を記録、操作にかかった総時間を報告して終了します。 let startTime = Date.now(); timeTest() .then(() => { let finishTime = Date.now(); let timeTaken = finishTime - startTime; alert("Time taken in milliseconds: " + timeTaken); }) それぞれのケースで異なるのはtimeTest()関数です。 slow-async-await.htmlの例では、timeTest()は次のようになります。 async function timeTest() { await timeoutPromise(3000); await timeoutPromise(3000); await timeoutPromise(3000); } ここでは、3つのtimeoutPromise()呼び出しを直接待ち、それぞれ3秒後に警告を出すようにしています。最初の例を実行すると、アラートボックスに合計約9秒の実行時間が表示されるのがわかるでしょう。 fast-async-await.htmlの例では、timeTest()は次のようになります。 async function timeTest() { const timeoutPromise1 = timeoutPromise(3000); const timeoutPromise2 = timeoutPromise(3000); const timeoutPromise3 = timeoutPromise(3000); await timeoutPromise1; await timeoutPromise2; await timeoutPromise3; } ここでは、3つのPromiseオブジェクトを変数に格納し、関連するプロセスをすべて同時に起動させる効果を持たせています。 次に、その結果を待ちます。約束はすべて基本的に同時に処理を始めたので、約束はすべて同時に達成されます。2番目の例を実行すると、アラートボックスで実行時間の合計が3秒強であることが報告されます! エラー処理 しかし、上記のパターンには問題があり、処理されないエラーが発生する可能性があります。 前の例を更新して、今度は拒否されたPromiseを追加し、最後にcatch文を追加してみましょう。 function timeoutPromiseResolve(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ resolve("successful"); }, interval); }); }; function timeoutPromiseReject(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ reject("error"); }, interval); }); }; async function timeTest() { await timeoutPromiseResolve(5000); await timeoutPromiseReject(2000); await timeoutPromiseResolve(3000); } let startTime = Date.now(); timeTest() .then(() => {}) .catch(e => { console.log(e); let finishTime = Date.now(); let timeTaken = finishTime - startTime; alert("Time taken in milliseconds: " + timeTaken); }) 上記の例では、エラーは適切に処理され、約7秒後にアラートが表示されます。 次に2つ目のパターンです。 function timeoutPromiseResolve(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ resolve("successful"); }, interval); }); }; function timeoutPromiseReject(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ reject("error"); }, interval); }); }; async function timeTest() { const timeoutPromiseResolve1 = timeoutPromiseResolve(5000); const timeoutPromiseReject2 = timeoutPromiseReject(2000); const timeoutPromiseResolve3 = timeoutPromiseResolve(3000); await timeoutPromiseResolve1; await timeoutPromiseReject2; await timeoutPromiseResolve3; } let startTime = Date.now(); timeTest() .then(() => {}) .catch(e => { console.log(e); let finishTime = Date.now(); let timeTaken = finishTime - startTime; alert("Time taken in milliseconds: " + timeTaken); }) この例では、コンソールに未処理のエラーが発生し(2秒後)、約5秒後にアラートが表示されています。 Promiseを並列に起動し、エラーを適切にキャッチするには、先に説明したように Promise.all() を使用すればよいでしょう。 function timeoutPromiseResolve(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ resolve("successful"); }, interval); }); }; function timeoutPromiseReject(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ reject("error"); }, interval); }); }; async function timeTest() { const timeoutPromiseResolve1 = timeoutPromiseResolve(5000); const timeoutPromiseReject2 = timeoutPromiseReject(2000); const timeoutPromiseResolve3 = timeoutPromiseResolve(3000); const results = await Promise.all([timeoutPromiseResolve1, timeoutPromiseReject2, timeoutPromiseResolve3]); return results; } let startTime = Date.now(); timeTest() .then(() => {}) .catch(e => { console.log(e); let finishTime = Date.now(); let timeTaken = finishTime - startTime; alert("Time taken in milliseconds: " + timeTaken); }) この例では、約2秒後にエラーが適切に処理され、また約2秒後にアラートが表示されています。 Promise.all()は、入力された約束のいずれかが拒否されたときに拒否されます。もし、いくつかの約束が拒否されたときでも、すべての約束を解決して、その満たされた値のいくつかを使いたいなら、代わりに Promise.allSettled() を使うことができます。 Async/awaitクラスのメソッド 最後に、クラスやオブジェクトのメソッドの前にasyncを追加して、それらがPromiseを返すようにしたり、その中でPromiseを待ったりすることも可能です。オブジェクト指向JavaScriptの記事で見たESクラスのコードを見て、そしてasyncメソッドを使った修正版を見てみてください。 class Person { constructor(first, last, age, gender, interests) { this.name = { first, last }; this.age = age; this.gender = gender; this.interests = interests; } async greeting() { return await Promise.resolve(`Hi! I'm ${this.name.first}`); }; farewell() { console.log(`${this.name.first} has left the building. Bye for now!`); }; } let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']); 最初のクラスのメソッドは、次のように使うことができます。 han.greeting().then(console.log); ブラウザサポート async/awaitを使用するかどうかを決定する際に考慮すべきことの1つは、古いブラウザのサポートです。これらはほとんどのブラウザのモダンバージョンで利用可能で、Promiseと同じです; 主なサポートの問題は Internet Explorer と Opera Mini で起こります。 もし、async/awaitを使いたいが、古いブラウザのサポートが心配であれば、BabelJSライブラリの使用を検討してみてください。非同期/待機をサポートしないブラウザに遭遇した場合、Babelのポリフィルは自動的に古いブラウザで動作するフォールバックを提供することができます。 結論 async/awaitは、読みやすく保守しやすい非同期コードを書くためのシンプルで素晴らしい方法を提供します。この記事を書いている時点では、ブラウザのサポートは他の非同期コード機構よりも制限されていますが、現在も将来も、学習して利用を検討する価値は十分にあると思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MDN Web Docsの「ウェブ開発を学ぶ - 非同期 JavaScript」を翻訳してみた Part.3

こんにちはWebエンジニアのmasakichiです。 MDN Web Docsの「ウェブ開発を学ぶ - 非同期 JavaScript」を翻訳してみた3つ目の記事です。 全部で5記事あります。 Introducing asynchronous JavaScript Cooperative asynchronous JavaScript: Timeouts and intervals Graceful asynchronous programming with Promises ←いまここ Making asynchronous programming easier with async and await Choosing the right approach 翻訳箇所について こちらのページの日本語未翻訳記事です。 なぜ翻訳したか JavaScriptの非同期処理。特にPromiseというやつが、ぼんやりわかっているけど完全には理解していない状態がずっと続いていました。 そんな中、MDN Web Docsの解説がすごくわかりやすく一気に理解できました。 しかし、これらの記事は日本語に翻訳されていないという問題が。。。 ぜひ非同期処理で悩める同志にも読んでほしい。という想いで翻訳作業をしてみました。 留意点 筆者は英語がそこまで得意というわけではありません。DeepLの力を借りて、翻訳していますので、日本語訳が不自然なところや一部、情報を省略・意訳しています。あらかじめご了承ください。 ライセンスは下記です。 This Article by Mozilla Contributors is licensed under CC-BY-SA 2.5. Promiseを用いた優美な非同期プログラミング PromiseはJavaScript言語の比較的新しい機能で、前のアクションが完了するまで次のアクションを延期したり、その失敗に対して応答したりすることができるものです。これは一連の非同期処理を正しく動作するよう設定するのに便利です。この記事では、Promiseがどのように機能するか、Web APIとしてどのように使うのか、そしてその記述方法について説明します。 前提条件 基本的なコンピュータリテラシー / JavaScriptの基本をそれなりに理解していること。 目的 Promiseについて、使い方も含めて理解すること。 Promiseとは何か? 講座の第1回でPromiseについて簡単に見てきましたが、ここではもっと深く見ていきましょう。 基本的に、Promiseは操作の中間状態を表すオブジェクトで、事実上、将来のある時点で何らかの結果が返されることをPromiseするものです。いつオペレーションが完了し結果が返されるかは保証されていませんが、結果が利用可能になったとき、あるいはPromiseが失敗したときに、成功した結果に対して何か別の処理を行うため、あるいは失敗したケースを優雅に処理するために、提供したコードが実行されるという保証はあります。 一般的に、非同期処理が結果を返すまでにかかる時間にはあまり興味がなく(もちろん、あまりにも時間がかかりすぎる場合は別です!)、結果が返されたときにいつでも対応できることに興味があるのではないでしょうか。そしてもちろん、残りのコードが実行されるのをブロックしないのはいいことです。 Promiseの中で最も一般的なのは、Promiseを返すWebAPIです。仮想的なビデオチャットアプリケーションを考えてみましょう。このアプリケーションには、ユーザーの友人のリストが表示されたウィンドウがあり、ユーザーの横にあるボタンをクリックすると、そのユーザーとのビデオ通話が開始されます。 このボタンのハンドラは、ユーザーのカメラとマイクにアクセスするために、getUserMedia()を呼び出します。getUserMedia()は、ユーザーがこれらのデバイスを使用する許可を持っていることを確認し、どのマイクを使用するか、どのカメラを使用するか(または音声のみの通話にするか、その他のオプション)をユーザーに尋ねる必要があるため、これらの決定だけでなく、カメラとマイクが作動するまでプログラムをブロックしてしまいます。また、ユーザーはこれらの許可要求にすぐには応じないかもしれません。これは、潜在的に長い時間がかかる可能性があります。 getUserMedia()の呼び出しはブラウザのメインスレッドから行われるので、getUserMedia()が戻るまでブラウザ全体がブロックされることになるのは、許容できる選択肢ではありません。Promiseがなければ、ユーザーがカメラとマイクについてどうするか決定するまで、ブラウザ内のすべてが使用不能になります。そこで、ユーザーを待ち、選択したデバイスを有効にし、選択したソースから作成したストリームのMediaStreamを直接返す代わりに、getUserMedia()はPromiseを返し、MediaStreamが利用可能になった時点で解決するようにしています。 ビデオチャットアプリケーションが使用するコードは、次のようなものでしょう。 function handleCallButton(evt) { setStatusMessage("Calling..."); navigator.mediaDevices.getUserMedia({video: true, audio: true}) .then(chatStream => { selfViewElem.srcObject = chatStream; chatStream.getTracks().forEach(track => myPeerConnection.addTrack(track, chatStream)); setStatusMessage("Connected"); }).catch(err => { setStatusMessage("Failed to connect"); }); } この関数は、まずsetStatusMessage()という関数を使って、ステータス表示を「Calling...」というメッセージで更新し、通話を試みていることを示します。次に getUserMedia() を呼び出して、ビデオとオーディオの両方のトラックを持つストリームを要求し、それが取得されると、カメラから来るストリームを「セルフビュー」として表示するようにビデオ要素をセットアップし、ストリームのトラックをそれぞれ取得して、他のユーザーとの接続を表す WebRTC RTCであるPeerConnection に追加しています。その後、ステータス表示を更新し、「接続済み」と表示します。 getUserMedia()が失敗すると、catchブロックが実行されます。これは setStatusMessage() を使用して、エラーが発生したことを示すためにステータスボックスを更新します。 ここで重要なのは、カメラストリームがまだ取得されていなくても、getUserMedia()の呼び出しがほぼ即座に返ってくるということです。handleCallButton()関数が、それを呼び出したコードにすでに戻っていたとしても、getUserMedia()の動作が終了すると、あなたが提供したハンドラが呼び出されるのです。アプリは、ストリーミングが開始されたと仮定しない限り、そのまま実行し続けることができます。 コールバックの問題点 Promiseがなぜ良いものなのかを十分に理解するためには、古いスタイルのコールバックを思い返し、なぜそれが問題なのかを理解することが役立ちます。 例として、ピザの注文の話をしましょう。注文を成功させるためには、いくつかのステップを踏まなければなりませんが、そのステップを順番に実行しなかったり、順番に実行しても、前のステップが完全に終了する前に実行しては意味がなくなります。 好きなトッピングを選びます。優柔不断な人は時間がかかるかもしれませんし、どうしても決まらなかったり、カレーにした場合は失敗する可能性があります。 そして、注文をします。ピザを返すのに時間がかかったり、ピザを焼くのに必要な材料が店になかったりすると失敗することがあります。 ピザを受け取り、食べます。このとき、財布を忘れて代金を支払えなかったりすると、失敗するかもしれません。 古いスタイルのコールバックでは、上記の機能を擬似的にコード化すると次のようになります。 chooseToppings(function(toppings) { placeOrder(toppings, function(order) { collectOrder(order, function(pizza) { eatPizza(pizza); }, failureCallback); }, failureCallback); }, failureCallback); ごちゃごちゃして読みにくいですね(「コールバック地獄」と言われます)。また、ステップが失敗した時の処理であるfailureCallback()を複数回(ネストした関数ごとに1回)呼び出す必要があります。さらに他の問題もあるでしょう。 Promiseによる改善策 Promiseは、上記のような状況をより簡単に記述し、解析し、実行することができます。もし上記の疑似コードを非同期Promiseを代わりに使って表現すると、以下のようなものになります。 chooseToppings() .then(function(toppings) { return placeOrder(toppings); }) .then(function(order) { return collectOrder(order); }) .then(function(pizza) { eatPizza(pizza); }) .catch(failureCallback); 何が起こっているのかがわかりやすく、すべてのエラーを処理するための .catch() ブロックがひとつで済み、メインスレッドをブロックせず(つまり、ピザが手に入れるまでの間もビデオゲームを続けられる)、各処理は前の処理が完了するのを待ってから実行することが保証されているのです。この方法で複数の非同期アクションを連鎖的に実行することができます。なぜなら、それぞれの .then() ブロックは新しいPromiseを返し、.then() ブロックの実行が終了したときに解決されるからです。賢いでしょう? アロー関数を使えば、さらにコードをシンプルにすることができます。 chooseToppings() .then(toppings => placeOrder(toppings) ) .then(order => collectOrder(order) ) .then(pizza => eatPizza(pizza) ) .catch(failureCallback); また、こうも書けます chooseToppings() .then(toppings => placeOrder(toppings)) .then(order => collectOrder(order)) .then(pizza => eatPizza(pizza)) .catch(failureCallback); これは、アロー関数の場合、() => x が () => { return x; } の有効な略記法であるため、うまくいきます。 このように、関数は引数を直接渡すだけなので、余計な関数のレイヤーは必要ないのです。 chooseToppings().then(placeOrder).then(collectOrder).then(eatPizza).catch(failureCallback); しかし、この構文は読みやすくありませんし、ここで紹介したものよりも複雑なブロックの場合は使えないかもしれません。 注:async/await構文でさらに改良することができますので、次回の記事で掘り下げます。 基本的に、Promiseはイベントリスナーとよく似ていますが、いくつかの違いがあります。 Promiseは一度だけ成功または失敗することができます。2回成功したり失敗したりすることはできませんし、処理が完了した後に成功から失敗に切り替わることもありません。 Promiseが成功または失敗した後、成功/失敗のコールバックを追加すると、イベントが先に発生した場合でも、正しいコールバックが呼び出されます。 基本的なPromiseの構文 : 実例 最近の Web API の多くは、潜在的に長大なタスクを実行する関数にPromiseを使用しているため、Promiseを理解することは重要です。最新のWeb技術を使うには、Promiseを使う必要があります。この章の後半で、あなた自身のPromiseを書く方法を見ていきますが、今は、あなたがWeb APIで遭遇するいくつかの簡単な例を見ていきます。 最初の例では、fetch()メソッドでウェブから画像を取得し、Response.blob()メソッドで取得したレスポンスの生のボディコンテンツをBlobオブジェクトに変換し、<img>要素内にそのBlobを表示することにします。これはシリーズの最初の記事で見た例と非常によく似ていますが、あなた自身のPromiseベースのコードを構築するために、少し違った方法で行います。 注:以下の例は、ファイルから直接実行しただけでは(つまり、file://のURL経由で)動作しません。ローカルのテストサーバーで実行するか、GlitchやGitHubページなどのオンラインソリューションを使用する必要があります。 まず、簡単なHTMLテンプレートと、取得するサンプル画像ファイルをダウンロードします。 HTMLの<body>の一番下に<script>要素を追加します。 <script>要素の中に、次の行を追加してください。 javascript let promise = fetch('coffee.jpg'); これは fetch() メソッドを呼び出し、パラメータとしてネットワークから取得する画像の URL を渡します。これはオプションの第2パラメータとしてoptionsオブジェクトを受け取ることもできますが、ここでは最も単純なバージョンを使用します。fetch()が返すpromiseオブジェクトをpromiseという変数に格納しています。前述したように、このオブジェクトは最初は成功でも失敗でもない中間状態を表します。この状態のPromiseの正式な用語はpendingです。 Primiseの操作が成功したとき(この場合はResponseが返されたとき)に対応するには、Promiseオブジェクトの.then()メソッドを呼び出します。.then()ブロック内のコールバックは、Promiseの呼び出しが正常に完了し、Responseオブジェクトを返したときだけ実行されます。返されたResponseオブジェクトはパラメータとして渡されます。 >注意: .then()ブロックの動作は、AddEventListener()を使ってオブジェクトにイベントリスナーを追加するときと似ています。イベントが発生するまで(Promiseが実行されるまで)実行されません。最も顕著な違いは、イベントリスナーが複数回呼び出される可能性があるのに対し、.then()は使用されるたびに一度だけ実行されることです。 このレスポンスに対して直ちにblob()メソッドを実行し、レスポンスのボディが完全にダウンロードされたことを確認し、利用可能になったら、それを何かできるようにBlobオブジェクトに変換します。この結果は、次のように返されます。 response => response.blob() <br> ショートハンドを使わずに書くとこうなります。 function(response) { return response.blob(); } 残念ながら、もう少し多くのことを行う必要があります。FetchのPromiseは404や500のエラーで失敗しません - ネットワーク障害のような壊滅的な何かでだけ失敗します。しかし、404や500のエラーは成功しますが、response.okプロパティがfalseに設定されています。たとえば404でエラーを発生させるには、response.okの値をチェックし、falseの場合はエラーをスローし、trueの場合にのみblobを返す必要があります。これは次のように行うことができます。JavaScriptの最初の行の下に次の行を追加します。 let promise2 = promise.then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { return response.blob(); } }); 5. .then()は、毎回新しいPromiseを作成します。blob()メソッドもPromiseを返すので、2つ目のPromiseの.then()メソッドを呼び出せば、blob()メソッドが返すBlobオブジェクトをfullfilled時に処理することができます。Blobに対して単一のメソッドを実行して結果を返すだけでなく、もう少し複雑なことをしたいので、今回は関数本体を中括弧で囲む必要があります(そうしないとエラーがスローされます)。コードの末尾に以下を追加します。 ```javascript let promise3 = promise2.then(myBlob => {) }); ``` 6. 次に、.then()コールバックの本体を埋めてみましょう。中括弧の中に次の行を追加します。 ```javascript let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); ``` ここでは、URL.createObjectURL()メソッドを実行し、2番目のPromiseのfullfilledで返されたBlobをパラメータとして渡しています。これは、オブジェクトを指すURLを返します。次に、`<img>` 要素を作成し、その src 属性をオブジェクトの URL と同じに設定して DOM に追加すると、画像がページに表示されます! 作成したHTMLファイルを保存して、ブラウザで読み込むと、期待通りにページ内に画像が表示されていることが確認できます。お疲れ様でした。 失敗への対応 現在、Promiseの1つが失敗した(Promiseで言えばrejected)場合のエラーを明示的に処理するものは何もありません。前のPromiseから .catch() メソッドを実行することで、エラー処理を追加することができます。下記を追加してください。 let errorCase = promise3.catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); これを実際に見るには、画像のURLを間違えて、ページを再読み込みしてみてください。ブラウザのデベロッパーツールのコンソールにエラーが表示されます。 これは、わざわざ.catch()ブロックを含めるほどでもありませんが、考えてみて下さい。実際のアプリケーションでは、.catch()ブロックは画像の取得を再試行したり、デフォルトの画像を表示したり、別の画像URLを提供するようユーザーに促したり、さまざまなことが可能です。 ブロックを連鎖させる 今までのコードは非常に長ったらしい書き出しをしていますが、何が起こっているのかを明確に理解してもらうために、あえてそうしています。この記事の前半で示したように、.then()ブロック(と.catch()ブロック)を連鎖させることが可能です。次のように書くこともできます。 fetch('coffee.jpg') .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { return response.blob(); } }) .then(myBlob => { let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }) .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); Promiseが返す値は、次の .then() ブロックのコールバック関数に渡されるパラメータになることに留意してください。 注:Promiseにおける .then()/.catch() ブロックは基本的に同期コードにおける try...catch ブロックの非同期版に相当します.同期的なtry...catchは非同期的なコードでは動作しないことを心に留めておいてください。 Promise用語のおさらい 上記のセクションでカバーすべきことがたくさんありましたので、簡単に振り返ります。記憶をリフレッシュするために使える短いガイドを提供しましょう。また、これらの概念が定着するように、上記のセクションをもう何度か読み返してみてください。 Promiseが作成されたとき、それは成功でも失敗でもない状態です。保留状態であるという。 Promiseが戻ってきたとき、それは解決されたと言います。 解決に成功したPromiseは,fulfilledと呼ばれます.Promiseは値を返し,その値は .then() ブロックをPromiseチェーンの末尾に連結することでアクセスすることができます..then()ブロック内のコールバック関数には,Promiseの戻り値が格納されます. 解決に失敗したPromiseはrejectedと言われています。これは、Promiseが拒否された理由を示すエラーメッセージを返します。この理由には,Promise・チェーンの最後に .catch() ブロックを連結してアクセスすることができます. 複数のPromiseのfullfilledに応答してコードを実行させる 上記の例では、Promiseを使用するための基本をいくつか示しました。次に、より高度な機能を見てみましょう。まず最初に、ある処理を次々と発生させるチェーニングは問題ありませんが、もしあるコードを実行するときに、たくさんのPromiseがすべてfullfilledした後に実行したいとしたらどうでしょうか? これは、Promise.all() という独創的な名前の静的メソッドで実現できます。これは入力パラメータとしてPromiseの配列を受け取り、配列内のすべてのPromiseがfullfilledした場合にのみfullfilledする新しいPromiseオブジェクトを返します。このような感じです。 Promise.all([a, b, c]).then(values => { ... }); それらがすべてfullfilledした場合、連鎖した .then() ブロックのコールバック関数に、それらの結果をすべて含む配列がパラメータとして渡されます。Promise.all()に渡されたPromiseのいずれかがrejectedされた場合、ブロック全体がrejectedされます。 これはとても便利なことです。ページ上のUI機能に動的にコンテンツを投入するために情報を取得することを想像してください。多くの場合、部分的な情報を表示するよりも、すべてのデータを受け取ってから完全なコンテンツを表示する方が理にかなっています。 このことを示すために、別の例を作ってみましょう。 テンプレートの新しいコピーをダウンロードし、再び終了</body>タグの直前に<script>要素を配置します。 ソースファイル(coffee.jpg、tea.jpg、description.txt)をダウンロードするか、ご自由にお使いください。 このスクリプトでは、まず Promise.all() に渡したいPromiseを返す関数を定義します。これは、単に3つのfetch()処理の完了に応答してPromise.all()ブロックを実行したい場合は簡単でしょう。次のようなことをすればいいのです。 ```javascript let a = fetch(url1); let b = fetch(url2); let c = fetch(url3); Promise.all([a, b, c]).then(values => { ... }); Promiseがfullfilledになると、ハンドラーに渡される値には、完了したfetch()操作ごとに1つずつ、合計3つのResponseオブジェクトが含まれることになります。しかし、このようなことはしたくありません。私たちのコードは、fetch() 操作がいつ完了したかを気にしていません。私たちが欲しいのは、読み込まれたデータなのです。つまり、画像を表す使用可能なBlobと使用可能なテキスト文字列を取得したときに、Promise.all()ブロックを実行したいのです。これを実現する関数を作成します。`<script>`要素内に次のように記述します。 <br> javascript function fetchAndDecode(url, type) { return fetch(url).then(response => { if(!response.ok) { throw new Error(HTTP error! status: ${response.status}); } else { if(type === 'blob') { return response.blob(); } else if(type === 'text') { return response.text(); } } }) .catch(e => { console.log(There has been a problem with your fetch operation for resource "${url}": + e.message); }); } ``` 少し複雑に見えるので、順を追って説明しましょう。 まず最初に、関数を定義し、URL と取得するリソースの種類を表す文字列を渡します。 関数本体の内部は、最初の例で見たものと同様の構造になっています。指定されたURLのリソースを取得するためにfetch()関数を呼び出し、それを別のPromiseに連結して、デコードした(または「読み取り」)レスポンスボディを返します。これは、前の例では常にblob()メソッドでした。 しかし、ここでは2つの点が異なっています。 まず、型値が何であるかによって、2番目に返すPromiseが異なります。.then()コールバック関数の内部には、デコードする必要があるファイルのタイプに応じて異なるPromiseを返す、単純なif ... else if文が含まれています(この場合、blobまたはtextの選択肢がありますが、他のタイプを扱うためにこれを拡張することは容易です)。 次に、fetch()呼び出しの前にreturnキーワードを追加しています。この効果は、チェーン全体を実行し、最終結果(つまり、blob()またはtext()が返すPromise)を今定義した関数の戻り値として実行することです。事実上、return文は結果をチェーンの上部に渡しています。 ブロックの最後に、.catch()呼び出しにチェーンして、.all()に配列で渡されたPromiseで発生する可能性のあるあらゆるエラーケースを処理します。もしどれかのPromiseがrejectedされたら、.catch()ブロックはどれが問題だったかを知らせます。.all()ブロック(下記参照)はまだfullfilledされますが、問題があったリソースを表示することはありません。一度 .catch() ブロックでPromiseを処理すると、結果のPromiseは解決されたとみなされますが、値は未定義であることを覚えておいてください; このため、この場合 .all() ブロックは常にfullfilledされます。もし、.all()を拒否したい場合は、代わりに.catch()ブロックを.all()の最後に連結する必要があります。 関数本体内のコードは非同期でPromiseベースなので、事実上、関数全体がPromiseのように動作します。 次に、関数を3回呼び出して、画像とテキストのフェッチとデコードの処理を開始し、返されたPromiseをそれぞれ変数に格納します。前のコードの下に以下を追加してください。 javascript let coffee = fetchAndDecode('coffee.jpg', 'blob'); let tea = fetchAndDecode('tea.jpg', 'blob'); let description = fetchAndDecode('description.txt', 'text'); 次に、上に格納した3つのPromiseがすべて成功したときだけ、あるコードを実行する Promise.all() ブロックを定義することにします。まず始めに、.then()の呼び出しの中に空のコールバック関数を持つブロックを以下のように追加します。 ```javascript Promise.all([coffee, tea, description]).then(values => { }); ``` パラメータとしてPromiseを含む配列を受け取っているのがわかると思います。.then()コールバック関数は3つのPromiseが解決したときだけ実行されます; それが起こるとき、それは個々のPromiseからの結果を含む配列に渡されます(すなわち、デコードされた応答ボディ)、[coffee-results, tea-results, description-results] みたいな感じです。 最後に、コールバック内に以下を追加します。ここでは、かなり単純な同期コードを使用して、結果を個別の変数に格納し(blobからオブジェクトURLを作成)、画像とテキストをページに表示します。 ```javascript console.log(values); // Store each value returned from the promises in separate variables; create object URLs from the blobs let objectURL1 = URL.createObjectURL(values[0]); let objectURL2 = URL.createObjectURL(values[1]); let descText = values[2]; // Display the images in elements let image1 = document.createElement('img'); let image2 = document.createElement('img'); image1.src = objectURL1; image2.src = objectURL2; document.body.appendChild(image1); document.body.appendChild(image2); // Display the text in a paragraph let para = document.createElement('p'); para.textContent = descText; document.body.appendChild(para); ``` 保存して更新すると、UIコンポーネントがすべて読み込まれているのが見えるはずです。 ここで提供した項目を表示するコードはかなり初歩的なものですが、とりあえず説明用として機能します。 Promiseの実行後に、結果に関わらず最終的なコードを実行する Promiseが完了した後、それがfullfilledしたか否かに関わらず、最終的なコードブロックを実行したい場合があります。以前は、例えば .then() と .catch() の両方のコールバックで同じコードを記述する必要がありました。 myPromise .then(response => { doSomething(response); runFinalCode(); }) .catch(e => { returnError(e); runFinalCode(); }); 最近のモダンブラウザでは、.finally()メソッドが利用可能で、通常のPromiseチェーンの末尾に連結することができ、コードの繰り返しを減らし、よりエレガントに物事を行うことができるようになりました。上記のコードは以下のように書くことができます。 myPromise .then(response => { doSomething(response); }) .catch(e => { returnError(e); }) .finally(() => { runFinalCode(); }); 実際の例として、promise-finally.htmlのデモをご覧ください(ソースコードもご覧ください)。これは上のセクションで見たPromise.all()のデモと同じように動作しますが、fetchAndDecode()関数の中で、チェーンの終わりにfinally()を連結している点が異なります。 function fetchAndDecode(url, type) { return fetch(url).then(response => { if(!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { if(type === 'blob') { return response.blob(); } else if(type === 'text') { return response.text(); } } }) .catch(e => { console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message); }) .finally(() => { console.log(`fetch attempt for "${url}" finished.`); }); } .finally(()では、それぞれのfetchが終了したときに、コンソールに簡単なログを出しています。 独自のカスタムPromiseを構築 ある意味で、あなたはすでに自分自身のPromiseを構築しています。複数のPromiseを .then() ブロックで連結したり、組み合わせて独自の機能を作ったりしているとき、あなたはすでに独自の非同期Promiseベースの関数を作っていることになります。例えば、先ほどの例の fetchAndDecode() 関数を見てみましょう。 異なるPromiseベースのAPIを組み合わせてカスタム機能を作成することは,Promiseを使って物事をカスタムする最も一般的な方法であり,最新のAPIのほとんどが同じ原理をベースにしていることの柔軟性とパワーを示しています。しかし、もう一つの方法があります。 Promise()コンストラクタの使用 Promise()コンストラクタを使用して、独自のPromiseを構築することが可能です。主な用途としては、Promiseに対応していない旧来の非同期APIをベースとしたコードをPromise化する場合です。これは、既存の古いプロジェクトのコードやライブラリ、フレームワークを、最新のPromiseベースのコードと一緒に使う必要がある場合に便利です。 ここでは、setTimeout()呼び出しをPromiseでラップしています。これは2秒後に関数を実行し、(渡されたresolve()呼び出しを使用して)Promiseを解決して「成功!」という文字列を返します。 let timeoutPromise = new Promise((resolve, reject) => { setTimeout(() => { resolve('Success!'); }, 2000); }); resolve()とreject()は、新しく作成されたPromiseをfulfillまたはrejectするために呼び出される関数です。この場合、Promiseは "Success!"という文字列でfullfilledされます。 つまり、このPromiseを呼び出したら、その末尾に.then()ブロックをチェーンさせれば、「成功!」という文字列が渡されることになるわけです。以下のコードでは、そのメッセージを警告しています。 timeoutPromise .then((message) => { alert(message); }) もしくは、こうも書けます。 timeoutPromise.then(alert); 上記の例では、Promiseは単一の文字列でしか実現できず、reject()条件も指定されていません(確かに、setTimeout()には失敗条件がないので、この単純な例では問題にはなりません)。 カスタムPromiseをrejectする resolve()と同じように、これは1つの値を取りますが、この場合、それをrejectする理由、つまり.catch()ブロックに渡されるエラーです。 先ほどの例を拡張して、reject() の条件をいくつか設定し、成功時にさまざまなメッセージを渡せるようにしましょう。 前の例をコピーして、既存のtimeoutPromise()の定義を次のように置き換えてください。 function timeoutPromise(message, interval) { return new Promise((resolve, reject) => { if (message === '' || typeof message !== 'string') { reject('Message is empty or not a string'); } else if (interval < 0 || typeof interval !== 'number') { reject('Interval is negative or not a number'); } else { setTimeout(() => { resolve(message); }, interval); } }); } ここでは、カスタム関数に2つの引数を渡しています。何かをするためのメッセージと、何かをする前に経過させる時間間隔です。関数内では新しいPromiseオブジェクトを返しています。この関数を呼び出すと、使用したいPromiseが返されます。 Promiseコンストラクタの内部では、if ... else構造体の中でいくつかのチェックを行っています。 まず、メッセージが警告されるにふさわしいかどうかをチェックする。もしそれが空文字列であったり、文字列でない場合は、適切なエラーメッセージを表示してPromiseを拒否します。 次に、intervalが適切なインターバル値であるかどうかをチェックする。もしそれが負の値であったり、数値でない場合は、適切なエラーメッセージとともにPromiseを拒否します。 最後に,パラメータに問題がなければ,setTimeout()を用いて,指定された時間経過後に,指定されたメッセージとともにPromiseを解決します. timeoutPromise()関数はPromiseを返すので、.then()や.catch()などを連鎖させてその機能を利用することができます。それでは使ってみましょう。先ほどのtimeoutPromiseの使い方をこの関数に置き換えてみてください。 timeoutPromise('Hello there!', 1000) .then(message => { alert(message); }) .catch(e => { console.log('Error: ' + e); }); このまま保存して実行すると、1秒後にメッセージのアラートが表示されます。ここで、例えばメッセージに空文字列を設定したり、間隔に負の数を設定してみると、適切なエラーメッセージとともにPromiseがrejectされるのがわかるはずです!また、解決したメッセージに対して、単にアラートを出すだけでなく、何か別の処理をしてみることもできます。 結論 Promiseは、関数の戻り値や戻り値にかかる時間が分からない場合に、非同期アプリケーションを構築するのに適した方法です。また、同期的なtry...catch文と同じようなエラーハンドリングもサポートしています。 Promiseはすべてのモダンブラウザの最新バージョンで動作します。Promiseのサポートが問題になるのは、Opera MiniとIE11およびそれ以前のバージョンだけです。 この記事では、Promiseのすべての機能には触れず、最も興味深く、便利な機能だけを紹介しました。Promiseについてもっと学び始めると、さらなる機能やテクニックに出会えるでしょう。 最近のWeb APIはほとんどがPromise型なので、Promiseを理解しないと使いこなせません。それらのAPIの中には、WebRTC、Web Audio API、Media Capture and Streams、その他多くのものがあります。Promiseは時代が進むにつれてますます重要になるので、Promiseの使用と理解を学ぶことは、モダンなJavaScriptを学ぶ上で重要なステップとなります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの非同期を理解するにはMDN Web Docsを読むべき。翻訳しといたよ Part.3

こんにちはWebエンジニアのmasakichiです。 JavaScriptの非同期を理解するにはMDN Web Docsを読むべき。翻訳しといたよ 3つ目の記事です。 全部で5記事あります。 Introducing asynchronous JavaScript Cooperative asynchronous JavaScript: Timeouts and intervals Graceful asynchronous programming with Promises ←いまここ Making asynchronous programming easier with async and await Choosing the right approach 翻訳箇所について こちらのページの日本語未翻訳記事です。 なぜ翻訳したか JavaScriptの非同期処理。特にPromiseというやつが、ぼんやりわかっているけど完全には理解していない状態がずっと続いていました。 そんな中、MDN Web Docsの解説がすごくわかりやすく一気に理解できました。 しかし、これらの記事は日本語に翻訳されていないという問題が。。。 ぜひ非同期処理で悩める同志にも読んでほしい。という想いで翻訳作業をしてみました。 留意点 筆者は英語がそこまで得意というわけではありません。DeepLの力を借りて、翻訳していますので、日本語訳が不自然なところや一部、情報を省略・意訳しています。あらかじめご了承ください。 ライセンスは下記です。 This Article by Mozilla Contributors is licensed under CC-BY-SA 2.5. Promiseを用いた優美な非同期プログラミング PromiseはJavaScript言語の比較的新しい機能で、前のアクションが完了するまで次のアクションを延期したり、その失敗に対して応答したりすることができるものです。これは一連の非同期処理を正しく動作するよう設定するのに便利です。この記事では、Promiseがどのように機能するか、Web APIとしてどのように使うのか、そしてその記述方法について説明します。 前提条件 基本的なコンピュータリテラシー / JavaScriptの基本をそれなりに理解していること。 目的 Promiseについて、使い方も含めて理解すること。 Promiseとは何か? 講座の第1回でPromiseについて簡単に見てきましたが、ここではもっと深く見ていきましょう。 基本的に、Promiseは操作の中間状態を表すオブジェクトで、事実上、将来のある時点で何らかの結果が返されることをPromiseするものです。いつオペレーションが完了し結果が返されるかは保証されていませんが、結果が利用可能になったとき、あるいはPromiseが失敗したときに、成功した結果に対して何か別の処理を行うため、あるいは失敗したケースを優雅に処理するために、提供したコードが実行されるという保証はあります。 一般的に、非同期処理が結果を返すまでにかかる時間にはあまり興味がなく(もちろん、あまりにも時間がかかりすぎる場合は別です!)、結果が返されたときにいつでも対応できることに興味があるのではないでしょうか。そしてもちろん、残りのコードが実行されるのをブロックしないのはいいことです。 Promiseの中で最も一般的なのは、Promiseを返すWebAPIです。仮想的なビデオチャットアプリケーションを考えてみましょう。このアプリケーションには、ユーザーの友人のリストが表示されたウィンドウがあり、ユーザーの横にあるボタンをクリックすると、そのユーザーとのビデオ通話が開始されます。 このボタンのハンドラは、ユーザーのカメラとマイクにアクセスするために、getUserMedia()を呼び出します。getUserMedia()は、ユーザーがこれらのデバイスを使用する許可を持っていることを確認し、どのマイクを使用するか、どのカメラを使用するか(または音声のみの通話にするか、その他のオプション)をユーザーに尋ねる必要があるため、これらの決定だけでなく、カメラとマイクが作動するまでプログラムをブロックしてしまいます。また、ユーザーはこれらの許可要求にすぐには応じないかもしれません。これは、潜在的に長い時間がかかる可能性があります。 getUserMedia()の呼び出しはブラウザのメインスレッドから行われるので、getUserMedia()が戻るまでブラウザ全体がブロックされることになるのは、許容できる選択肢ではありません。Promiseがなければ、ユーザーがカメラとマイクについてどうするか決定するまで、ブラウザ内のすべてが使用不能になります。そこで、ユーザーを待ち、選択したデバイスを有効にし、選択したソースから作成したストリームのMediaStreamを直接返す代わりに、getUserMedia()はPromiseを返し、MediaStreamが利用可能になった時点で解決するようにしています。 ビデオチャットアプリケーションが使用するコードは、次のようなものでしょう。 function handleCallButton(evt) { setStatusMessage("Calling..."); navigator.mediaDevices.getUserMedia({video: true, audio: true}) .then(chatStream => { selfViewElem.srcObject = chatStream; chatStream.getTracks().forEach(track => myPeerConnection.addTrack(track, chatStream)); setStatusMessage("Connected"); }).catch(err => { setStatusMessage("Failed to connect"); }); } この関数は、まずsetStatusMessage()という関数を使って、ステータス表示を「Calling...」というメッセージで更新し、通話を試みていることを示します。次に getUserMedia() を呼び出して、ビデオとオーディオの両方のトラックを持つストリームを要求し、それが取得されると、カメラから来るストリームを「セルフビュー」として表示するようにビデオ要素をセットアップし、ストリームのトラックをそれぞれ取得して、他のユーザーとの接続を表す WebRTC RTCであるPeerConnection に追加しています。その後、ステータス表示を更新し、「接続済み」と表示します。 getUserMedia()が失敗すると、catchブロックが実行されます。これは setStatusMessage() を使用して、エラーが発生したことを示すためにステータスボックスを更新します。 ここで重要なのは、カメラストリームがまだ取得されていなくても、getUserMedia()の呼び出しがほぼ即座に返ってくるということです。handleCallButton()関数が、それを呼び出したコードにすでに戻っていたとしても、getUserMedia()の動作が終了すると、あなたが提供したハンドラが呼び出されるのです。アプリは、ストリーミングが開始されたと仮定しない限り、そのまま実行し続けることができます。 コールバックの問題点 Promiseがなぜ良いものなのかを十分に理解するためには、古いスタイルのコールバックを思い返し、なぜそれが問題なのかを理解することが役立ちます。 例として、ピザの注文の話をしましょう。注文を成功させるためには、いくつかのステップを踏まなければなりませんが、そのステップを順番に実行しなかったり、順番に実行しても、前のステップが完全に終了する前に実行しては意味がなくなります。 好きなトッピングを選びます。優柔不断な人は時間がかかるかもしれませんし、どうしても決まらなかったり、カレーにした場合は失敗する可能性があります。 そして、注文をします。ピザを返すのに時間がかかったり、ピザを焼くのに必要な材料が店になかったりすると失敗することがあります。 ピザを受け取り、食べます。このとき、財布を忘れて代金を支払えなかったりすると、失敗するかもしれません。 古いスタイルのコールバックでは、上記の機能を擬似的にコード化すると次のようになります。 chooseToppings(function(toppings) { placeOrder(toppings, function(order) { collectOrder(order, function(pizza) { eatPizza(pizza); }, failureCallback); }, failureCallback); }, failureCallback); ごちゃごちゃして読みにくいですね(「コールバック地獄」と言われます)。また、ステップが失敗した時の処理であるfailureCallback()を複数回(ネストした関数ごとに1回)呼び出す必要があります。さらに他の問題もあるでしょう。 Promiseによる改善策 Promiseは、上記のような状況をより簡単に記述し、解析し、実行することができます。もし上記の疑似コードを非同期Promiseを代わりに使って表現すると、以下のようなものになります。 chooseToppings() .then(function(toppings) { return placeOrder(toppings); }) .then(function(order) { return collectOrder(order); }) .then(function(pizza) { eatPizza(pizza); }) .catch(failureCallback); 何が起こっているのかがわかりやすく、すべてのエラーを処理するための .catch() ブロックがひとつで済み、メインスレッドをブロックせず(つまり、ピザが手に入れるまでの間もビデオゲームを続けられる)、各処理は前の処理が完了するのを待ってから実行することが保証されているのです。この方法で複数の非同期アクションを連鎖的に実行することができます。なぜなら、それぞれの .then() ブロックは新しいPromiseを返し、.then() ブロックの実行が終了したときに解決されるからです。賢いでしょう? アロー関数を使えば、さらにコードをシンプルにすることができます。 chooseToppings() .then(toppings => placeOrder(toppings) ) .then(order => collectOrder(order) ) .then(pizza => eatPizza(pizza) ) .catch(failureCallback); また、こうも書けます chooseToppings() .then(toppings => placeOrder(toppings)) .then(order => collectOrder(order)) .then(pizza => eatPizza(pizza)) .catch(failureCallback); これは、アロー関数の場合、() => x が () => { return x; } の有効な略記法であるため、うまくいきます。 このように、関数は引数を直接渡すだけなので、余計な関数のレイヤーは必要ないのです。 chooseToppings().then(placeOrder).then(collectOrder).then(eatPizza).catch(failureCallback); しかし、この構文は読みやすくありませんし、ここで紹介したものよりも複雑なブロックの場合は使えないかもしれません。 注:async/await構文でさらに改良することができますので、次回の記事で掘り下げます。 基本的に、Promiseはイベントリスナーとよく似ていますが、いくつかの違いがあります。 Promiseは一度だけ成功または失敗することができます。2回成功したり失敗したりすることはできませんし、処理が完了した後に成功から失敗に切り替わることもありません。 Promiseが成功または失敗した後、成功/失敗のコールバックを追加すると、イベントが先に発生した場合でも、正しいコールバックが呼び出されます。 基本的なPromiseの構文 : 実例 最近の Web API の多くは、潜在的に長大なタスクを実行する関数にPromiseを使用しているため、Promiseを理解することは重要です。最新のWeb技術を使うには、Promiseを使う必要があります。この章の後半で、あなた自身のPromiseを書く方法を見ていきますが、今は、あなたがWeb APIで遭遇するいくつかの簡単な例を見ていきます。 最初の例では、fetch()メソッドでウェブから画像を取得し、Response.blob()メソッドで取得したレスポンスの生のボディコンテンツをBlobオブジェクトに変換し、<img>要素内にそのBlobを表示することにします。これはシリーズの最初の記事で見た例と非常によく似ていますが、あなた自身のPromiseベースのコードを構築するために、少し違った方法で行います。 注:以下の例は、ファイルから直接実行しただけでは(つまり、file://のURL経由で)動作しません。ローカルのテストサーバーで実行するか、GlitchやGitHubページなどのオンラインソリューションを使用する必要があります。 まず、簡単なHTMLテンプレートと、取得するサンプル画像ファイルをダウンロードします。 HTMLの<body>の一番下に<script>要素を追加します。 <script>要素の中に、次の行を追加してください。 javascript let promise = fetch('coffee.jpg'); これは fetch() メソッドを呼び出し、パラメータとしてネットワークから取得する画像の URL を渡します。これはオプションの第2パラメータとしてoptionsオブジェクトを受け取ることもできますが、ここでは最も単純なバージョンを使用します。fetch()が返すpromiseオブジェクトをpromiseという変数に格納しています。前述したように、このオブジェクトは最初は成功でも失敗でもない中間状態を表します。この状態のPromiseの正式な用語はpendingです。 Primiseの操作が成功したとき(この場合はResponseが返されたとき)に対応するには、Promiseオブジェクトの.then()メソッドを呼び出します。.then()ブロック内のコールバックは、Promiseの呼び出しが正常に完了し、Responseオブジェクトを返したときだけ実行されます。返されたResponseオブジェクトはパラメータとして渡されます。 >注意: .then()ブロックの動作は、AddEventListener()を使ってオブジェクトにイベントリスナーを追加するときと似ています。イベントが発生するまで(Promiseが実行されるまで)実行されません。最も顕著な違いは、イベントリスナーが複数回呼び出される可能性があるのに対し、.then()は使用されるたびに一度だけ実行されることです。 このレスポンスに対して直ちにblob()メソッドを実行し、レスポンスのボディが完全にダウンロードされたことを確認し、利用可能になったら、それを何かできるようにBlobオブジェクトに変換します。この結果は、次のように返されます。 response => response.blob() <br> ショートハンドを使わずに書くとこうなります。 function(response) { return response.blob(); } 残念ながら、もう少し多くのことを行う必要があります。FetchのPromiseは404や500のエラーで失敗しません - ネットワーク障害のような壊滅的な何かでだけ失敗します。しかし、404や500のエラーは成功しますが、response.okプロパティがfalseに設定されています。たとえば404でエラーを発生させるには、response.okの値をチェックし、falseの場合はエラーをスローし、trueの場合にのみblobを返す必要があります。これは次のように行うことができます。JavaScriptの最初の行の下に次の行を追加します。 let promise2 = promise.then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { return response.blob(); } }); 5. .then()は、毎回新しいPromiseを作成します。blob()メソッドもPromiseを返すので、2つ目のPromiseの.then()メソッドを呼び出せば、blob()メソッドが返すBlobオブジェクトをfullfilled時に処理することができます。Blobに対して単一のメソッドを実行して結果を返すだけでなく、もう少し複雑なことをしたいので、今回は関数本体を中括弧で囲む必要があります(そうしないとエラーがスローされます)。コードの末尾に以下を追加します。 ```javascript let promise3 = promise2.then(myBlob => {) }); ``` 6. 次に、.then()コールバックの本体を埋めてみましょう。中括弧の中に次の行を追加します。 ```javascript let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); ``` ここでは、URL.createObjectURL()メソッドを実行し、2番目のPromiseのfullfilledで返されたBlobをパラメータとして渡しています。これは、オブジェクトを指すURLを返します。次に、`<img>` 要素を作成し、その src 属性をオブジェクトの URL と同じに設定して DOM に追加すると、画像がページに表示されます! 作成したHTMLファイルを保存して、ブラウザで読み込むと、期待通りにページ内に画像が表示されていることが確認できます。お疲れ様でした。 失敗への対応 現在、Promiseの1つが失敗した(Promiseで言えばrejected)場合のエラーを明示的に処理するものは何もありません。前のPromiseから .catch() メソッドを実行することで、エラー処理を追加することができます。下記を追加してください。 let errorCase = promise3.catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); これを実際に見るには、画像のURLを間違えて、ページを再読み込みしてみてください。ブラウザのデベロッパーツールのコンソールにエラーが表示されます。 これは、わざわざ.catch()ブロックを含めるほどでもありませんが、考えてみて下さい。実際のアプリケーションでは、.catch()ブロックは画像の取得を再試行したり、デフォルトの画像を表示したり、別の画像URLを提供するようユーザーに促したり、さまざまなことが可能です。 ブロックを連鎖させる 今までのコードは非常に長ったらしい書き出しをしていますが、何が起こっているのかを明確に理解してもらうために、あえてそうしています。この記事の前半で示したように、.then()ブロック(と.catch()ブロック)を連鎖させることが可能です。次のように書くこともできます。 fetch('coffee.jpg') .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { return response.blob(); } }) .then(myBlob => { let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }) .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); }); Promiseが返す値は、次の .then() ブロックのコールバック関数に渡されるパラメータになることに留意してください。 注:Promiseにおける .then()/.catch() ブロックは基本的に同期コードにおける try...catch ブロックの非同期版に相当します.同期的なtry...catchは非同期的なコードでは動作しないことを心に留めておいてください。 Promise用語のおさらい 上記のセクションでカバーすべきことがたくさんありましたので、簡単に振り返ります。記憶をリフレッシュするために使える短いガイドを提供しましょう。また、これらの概念が定着するように、上記のセクションをもう何度か読み返してみてください。 Promiseが作成されたとき、それは成功でも失敗でもない状態です。保留状態であるという。 Promiseが戻ってきたとき、それは解決されたと言います。 解決に成功したPromiseは,fulfilledと呼ばれます.Promiseは値を返し,その値は .then() ブロックをPromiseチェーンの末尾に連結することでアクセスすることができます..then()ブロック内のコールバック関数には,Promiseの戻り値が格納されます. 解決に失敗したPromiseはrejectedと言われています。これは、Promiseが拒否された理由を示すエラーメッセージを返します。この理由には,Promise・チェーンの最後に .catch() ブロックを連結してアクセスすることができます. 複数のPromiseのfullfilledに応答してコードを実行させる 上記の例では、Promiseを使用するための基本をいくつか示しました。次に、より高度な機能を見てみましょう。まず最初に、ある処理を次々と発生させるチェーニングは問題ありませんが、もしあるコードを実行するときに、たくさんのPromiseがすべてfullfilledした後に実行したいとしたらどうでしょうか? これは、Promise.all() という独創的な名前の静的メソッドで実現できます。これは入力パラメータとしてPromiseの配列を受け取り、配列内のすべてのPromiseがfullfilledした場合にのみfullfilledする新しいPromiseオブジェクトを返します。このような感じです。 Promise.all([a, b, c]).then(values => { ... }); それらがすべてfullfilledした場合、連鎖した .then() ブロックのコールバック関数に、それらの結果をすべて含む配列がパラメータとして渡されます。Promise.all()に渡されたPromiseのいずれかがrejectedされた場合、ブロック全体がrejectedされます。 これはとても便利なことです。ページ上のUI機能に動的にコンテンツを投入するために情報を取得することを想像してください。多くの場合、部分的な情報を表示するよりも、すべてのデータを受け取ってから完全なコンテンツを表示する方が理にかなっています。 このことを示すために、別の例を作ってみましょう。 テンプレートの新しいコピーをダウンロードし、再び終了</body>タグの直前に<script>要素を配置します。 ソースファイル(coffee.jpg、tea.jpg、description.txt)をダウンロードするか、ご自由にお使いください。 このスクリプトでは、まず Promise.all() に渡したいPromiseを返す関数を定義します。これは、単に3つのfetch()処理の完了に応答してPromise.all()ブロックを実行したい場合は簡単でしょう。次のようなことをすればいいのです。 ```javascript let a = fetch(url1); let b = fetch(url2); let c = fetch(url3); Promise.all([a, b, c]).then(values => { ... }); Promiseがfullfilledになると、ハンドラーに渡される値には、完了したfetch()操作ごとに1つずつ、合計3つのResponseオブジェクトが含まれることになります。しかし、このようなことはしたくありません。私たちのコードは、fetch() 操作がいつ完了したかを気にしていません。私たちが欲しいのは、読み込まれたデータなのです。つまり、画像を表す使用可能なBlobと使用可能なテキスト文字列を取得したときに、Promise.all()ブロックを実行したいのです。これを実現する関数を作成します。`<script>`要素内に次のように記述します。 <br> javascript function fetchAndDecode(url, type) { return fetch(url).then(response => { if(!response.ok) { throw new Error(HTTP error! status: ${response.status}); } else { if(type === 'blob') { return response.blob(); } else if(type === 'text') { return response.text(); } } }) .catch(e => { console.log(There has been a problem with your fetch operation for resource "${url}": + e.message); }); } ``` 少し複雑に見えるので、順を追って説明しましょう。 まず最初に、関数を定義し、URL と取得するリソースの種類を表す文字列を渡します。 関数本体の内部は、最初の例で見たものと同様の構造になっています。指定されたURLのリソースを取得するためにfetch()関数を呼び出し、それを別のPromiseに連結して、デコードした(または「読み取り」)レスポンスボディを返します。これは、前の例では常にblob()メソッドでした。 しかし、ここでは2つの点が異なっています。 まず、型値が何であるかによって、2番目に返すPromiseが異なります。.then()コールバック関数の内部には、デコードする必要があるファイルのタイプに応じて異なるPromiseを返す、単純なif ... else if文が含まれています(この場合、blobまたはtextの選択肢がありますが、他のタイプを扱うためにこれを拡張することは容易です)。 次に、fetch()呼び出しの前にreturnキーワードを追加しています。この効果は、チェーン全体を実行し、最終結果(つまり、blob()またはtext()が返すPromise)を今定義した関数の戻り値として実行することです。事実上、return文は結果をチェーンの上部に渡しています。 ブロックの最後に、.catch()呼び出しにチェーンして、.all()に配列で渡されたPromiseで発生する可能性のあるあらゆるエラーケースを処理します。もしどれかのPromiseがrejectedされたら、.catch()ブロックはどれが問題だったかを知らせます。.all()ブロック(下記参照)はまだfullfilledされますが、問題があったリソースを表示することはありません。一度 .catch() ブロックでPromiseを処理すると、結果のPromiseは解決されたとみなされますが、値は未定義であることを覚えておいてください; このため、この場合 .all() ブロックは常にfullfilledされます。もし、.all()を拒否したい場合は、代わりに.catch()ブロックを.all()の最後に連結する必要があります。 関数本体内のコードは非同期でPromiseベースなので、事実上、関数全体がPromiseのように動作します。 次に、関数を3回呼び出して、画像とテキストのフェッチとデコードの処理を開始し、返されたPromiseをそれぞれ変数に格納します。前のコードの下に以下を追加してください。 javascript let coffee = fetchAndDecode('coffee.jpg', 'blob'); let tea = fetchAndDecode('tea.jpg', 'blob'); let description = fetchAndDecode('description.txt', 'text'); 次に、上に格納した3つのPromiseがすべて成功したときだけ、あるコードを実行する Promise.all() ブロックを定義することにします。まず始めに、.then()の呼び出しの中に空のコールバック関数を持つブロックを以下のように追加します。 ```javascript Promise.all([coffee, tea, description]).then(values => { }); ``` パラメータとしてPromiseを含む配列を受け取っているのがわかると思います。.then()コールバック関数は3つのPromiseが解決したときだけ実行されます; それが起こるとき、それは個々のPromiseからの結果を含む配列に渡されます(すなわち、デコードされた応答ボディ)、[coffee-results, tea-results, description-results] みたいな感じです。 最後に、コールバック内に以下を追加します。ここでは、かなり単純な同期コードを使用して、結果を個別の変数に格納し(blobからオブジェクトURLを作成)、画像とテキストをページに表示します。 ```javascript console.log(values); // Store each value returned from the promises in separate variables; create object URLs from the blobs let objectURL1 = URL.createObjectURL(values[0]); let objectURL2 = URL.createObjectURL(values[1]); let descText = values[2]; // Display the images in elements let image1 = document.createElement('img'); let image2 = document.createElement('img'); image1.src = objectURL1; image2.src = objectURL2; document.body.appendChild(image1); document.body.appendChild(image2); // Display the text in a paragraph let para = document.createElement('p'); para.textContent = descText; document.body.appendChild(para); ``` 保存して更新すると、UIコンポーネントがすべて読み込まれているのが見えるはずです。 ここで提供した項目を表示するコードはかなり初歩的なものですが、とりあえず説明用として機能します。 Promiseの実行後に、結果に関わらず最終的なコードを実行する Promiseが完了した後、それがfullfilledしたか否かに関わらず、最終的なコードブロックを実行したい場合があります。以前は、例えば .then() と .catch() の両方のコールバックで同じコードを記述する必要がありました。 myPromise .then(response => { doSomething(response); runFinalCode(); }) .catch(e => { returnError(e); runFinalCode(); }); 最近のモダンブラウザでは、.finally()メソッドが利用可能で、通常のPromiseチェーンの末尾に連結することができ、コードの繰り返しを減らし、よりエレガントに物事を行うことができるようになりました。上記のコードは以下のように書くことができます。 myPromise .then(response => { doSomething(response); }) .catch(e => { returnError(e); }) .finally(() => { runFinalCode(); }); 実際の例として、promise-finally.htmlのデモをご覧ください(ソースコードもご覧ください)。これは上のセクションで見たPromise.all()のデモと同じように動作しますが、fetchAndDecode()関数の中で、チェーンの終わりにfinally()を連結している点が異なります。 function fetchAndDecode(url, type) { return fetch(url).then(response => { if(!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { if(type === 'blob') { return response.blob(); } else if(type === 'text') { return response.text(); } } }) .catch(e => { console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message); }) .finally(() => { console.log(`fetch attempt for "${url}" finished.`); }); } .finally(()では、それぞれのfetchが終了したときに、コンソールに簡単なログを出しています。 独自のカスタムPromiseを構築 ある意味で、あなたはすでに自分自身のPromiseを構築しています。複数のPromiseを .then() ブロックで連結したり、組み合わせて独自の機能を作ったりしているとき、あなたはすでに独自の非同期Promiseベースの関数を作っていることになります。例えば、先ほどの例の fetchAndDecode() 関数を見てみましょう。 異なるPromiseベースのAPIを組み合わせてカスタム機能を作成することは,Promiseを使って物事をカスタムする最も一般的な方法であり,最新のAPIのほとんどが同じ原理をベースにしていることの柔軟性とパワーを示しています。しかし、もう一つの方法があります。 Promise()コンストラクタの使用 Promise()コンストラクタを使用して、独自のPromiseを構築することが可能です。主な用途としては、Promiseに対応していない旧来の非同期APIをベースとしたコードをPromise化する場合です。これは、既存の古いプロジェクトのコードやライブラリ、フレームワークを、最新のPromiseベースのコードと一緒に使う必要がある場合に便利です。 ここでは、setTimeout()呼び出しをPromiseでラップしています。これは2秒後に関数を実行し、(渡されたresolve()呼び出しを使用して)Promiseを解決して「成功!」という文字列を返します。 let timeoutPromise = new Promise((resolve, reject) => { setTimeout(() => { resolve('Success!'); }, 2000); }); resolve()とreject()は、新しく作成されたPromiseをfulfillまたはrejectするために呼び出される関数です。この場合、Promiseは "Success!"という文字列でfullfilledされます。 つまり、このPromiseを呼び出したら、その末尾に.then()ブロックをチェーンさせれば、「成功!」という文字列が渡されることになるわけです。以下のコードでは、そのメッセージを警告しています。 timeoutPromise .then((message) => { alert(message); }) もしくは、こうも書けます。 timeoutPromise.then(alert); 上記の例では、Promiseは単一の文字列でしか実現できず、reject()条件も指定されていません(確かに、setTimeout()には失敗条件がないので、この単純な例では問題にはなりません)。 カスタムPromiseをrejectする resolve()と同じように、これは1つの値を取りますが、この場合、それをrejectする理由、つまり.catch()ブロックに渡されるエラーです。 先ほどの例を拡張して、reject() の条件をいくつか設定し、成功時にさまざまなメッセージを渡せるようにしましょう。 前の例をコピーして、既存のtimeoutPromise()の定義を次のように置き換えてください。 function timeoutPromise(message, interval) { return new Promise((resolve, reject) => { if (message === '' || typeof message !== 'string') { reject('Message is empty or not a string'); } else if (interval < 0 || typeof interval !== 'number') { reject('Interval is negative or not a number'); } else { setTimeout(() => { resolve(message); }, interval); } }); } ここでは、カスタム関数に2つの引数を渡しています。何かをするためのメッセージと、何かをする前に経過させる時間間隔です。関数内では新しいPromiseオブジェクトを返しています。この関数を呼び出すと、使用したいPromiseが返されます。 Promiseコンストラクタの内部では、if ... else構造体の中でいくつかのチェックを行っています。 まず、メッセージが警告されるにふさわしいかどうかをチェックする。もしそれが空文字列であったり、文字列でない場合は、適切なエラーメッセージを表示してPromiseを拒否します。 次に、intervalが適切なインターバル値であるかどうかをチェックする。もしそれが負の値であったり、数値でない場合は、適切なエラーメッセージとともにPromiseを拒否します。 最後に,パラメータに問題がなければ,setTimeout()を用いて,指定された時間経過後に,指定されたメッセージとともにPromiseを解決します. timeoutPromise()関数はPromiseを返すので、.then()や.catch()などを連鎖させてその機能を利用することができます。それでは使ってみましょう。先ほどのtimeoutPromiseの使い方をこの関数に置き換えてみてください。 timeoutPromise('Hello there!', 1000) .then(message => { alert(message); }) .catch(e => { console.log('Error: ' + e); }); このまま保存して実行すると、1秒後にメッセージのアラートが表示されます。ここで、例えばメッセージに空文字列を設定したり、間隔に負の数を設定してみると、適切なエラーメッセージとともにPromiseがrejectされるのがわかるはずです!また、解決したメッセージに対して、単にアラートを出すだけでなく、何か別の処理をしてみることもできます。 結論 Promiseは、関数の戻り値や戻り値にかかる時間が分からない場合に、非同期アプリケーションを構築するのに適した方法です。また、同期的なtry...catch文と同じようなエラーハンドリングもサポートしています。 Promiseはすべてのモダンブラウザの最新バージョンで動作します。Promiseのサポートが問題になるのは、Opera MiniとIE11およびそれ以前のバージョンだけです。 この記事では、Promiseのすべての機能には触れず、最も興味深く、便利な機能だけを紹介しました。Promiseについてもっと学び始めると、さらなる機能やテクニックに出会えるでしょう。 最近のWeb APIはほとんどがPromise型なので、Promiseを理解しないと使いこなせません。それらのAPIの中には、WebRTC、Web Audio API、Media Capture and Streams、その他多くのものがあります。Promiseは時代が進むにつれてますます重要になるので、Promiseの使用と理解を学ぶことは、モダンなJavaScriptを学ぶ上で重要なステップとなります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MDN Web Docsの「ウェブ開発を学ぶ - 非同期 JavaScript」を翻訳してみた Part.2

こんにちはWebエンジニアのmasakichiです。 MDN Web Docsの「ウェブ開発を学ぶ - 非同期 JavaScript」を翻訳してみた2つ目の記事です。 全部で5記事あります。 Introducing asynchronous JavaScript Cooperative asynchronous JavaScript: Timeouts and intervals ←いまここ Graceful asynchronous programming with Promises Making asynchronous programming easier with async and await Choosing the right approach 翻訳箇所について こちらのページの日本語未翻訳記事です。 なぜ翻訳したか JavaScriptの非同期処理。特にPromiseというやつが、ぼんやりわかっているけど完全には理解していない状態がずっと続いていました。 そんな中、MDN Web Docsの解説がすごくわかりやすく一気に理解できました。 しかし、これらの記事は日本語に翻訳されていないという問題が。。。 ぜひ非同期処理で悩める同志にも読んでほしい。という想いで翻訳作業をしてみました。 留意点 筆者は英語がそこまで得意というわけではありません。DeepLの力を借りて、翻訳していますので、日本語訳が不自然なところや一部、情報を省略・意訳しています。あらかじめご了承ください。 ライセンスは下記です。 This Article by Mozilla Contributors is licensed under CC-BY-SA 2.5. 協調的な非同期JavaScript:タイムアウトとインターバル このチュートリアルでは、設定した時間が経過した後、あるいは一定の間隔(例えば1秒間に何回)でコードを非同期に実行するためにJavaScriptが利用できる従来の方法を取り上げ、それらが何のために有用であるかを議論し、固有の問題を考察しています。 前提条件 基本的なコンピュータリテラシー / JavaScriptの基本をそれなりに理解していること。 目的 非同期ループとインターバルについて理解し、何に役立つかを理解する。 イントロダクション 以前からWebプラットフォームでは、JavaScriptプログラマーに対して、ある時間間隔が経過した後に非同期でコードを実行したり、停止を指示するまで非同期でコードブロックを繰り返し実行したりする機能が数多く提供されています。 これらの機能は setTimeout(): 指定された時間経過後に、指定されたコードブロックを1回実行する。 setInterval(): 指定されたコードのブロックを、呼び出しのたびに一定の時間遅らせて繰り返し実行する。 requestAnimationFrame(): setInterval()の現代版。ブラウザが次にディスプレイを再描画する前に指定したコードブロックを実行し、実行中の環境に関係なく適切なフレームレートでアニメーションを実行できるようにします。 これらの関数で設定された非同期コードは、(指定されたタイマーが経過した後)メインスレッドで実行されます。 setTimeout()の呼び出しが実行される前や、setInterval()の繰り返しの間に、他のコードを実行できる(そしてしばしば実行する)ことを知っておくことが重要です。非同期コードはメインスレッドが利用可能になった後にのみ実行されるため、これらの操作がどれほどプロセッサ集約的であるかによって、それらは非同期コードをさらに遅らせることができます。(なぜなら、非同期コードはメインスレッドが使用可能になった後にのみ実行されるからです(言い換えれば、スタックが空になったとき)。この件に関しては、この記事を読み進めるにつれて詳しく知ることができます。 いずれにせよ、これらの関数は、Webサイトやアプリケーションで一定のアニメーションやその他のバックグラウンド処理を実行するために使用されます。以下のセクションでは、これらの関数の使用方法について説明します。 setTimeout() 先に述べたように、setTimeout()は指定された時間が経過した後、特定のコードブロックを一回実行します。これは以下のパラメータをとります。 実行する関数、または他の場所で定義された関数への参照。 コードを実行する前に待機する時間間隔をミリ秒単位で表した数値(1000ミリ秒は1秒に相当)。0を指定すると(あるいは値を省略すると)、関数はできるだけ早く実行されます。(なぜ「すぐに」ではなく「できるだけ早く」実行するのかについては、以下の注釈を参照してください)。なぜこのようなことをしたいのかについては、後で詳しく説明します。 ゼロまたはそれ以上の値で、関数が実行されるときに渡したいパラメータを表します。 注意:指定された時間(または遅延時間)は、実行までの保証時間ではなく、実行までの最短時間です。これらの関数に渡すコールバックは、メインスレッド上のスタックが空になるまで実行できません。 その結果、setTimeout(fn, 0)のようなコードは、スタックが空になるとすぐに実行されるのではなく、すぐに実行されます。setTimeout(fn, 0)のようなコードを実行し、その直後に1から100億までカウントするループを実行すると、コールバックは数秒後に実行されることになります。 次の例では、ブラウザは匿名関数を実行する前に2秒間待機し、その後アラートメッセージを表示します。 let myGreeting = setTimeout(() => { alert('Hello, Mr. Universe!'); }, 2000); 指定する関数は、匿名である必要はありません。関数に名前をつけて、どこか別の場所で定義して、setTimeout()に関数の参照を渡すことも可能です。次の2つのバージョンのコードスニペットは、最初のコードと同等です。 // With a named function let myGreeting = setTimeout(function sayHi() { alert('Hello, Mr. Universe!'); }, 2000); // With a function defined separately function sayHi() { alert('Hello Mr. Universe!'); } let myGreeting = setTimeout(sayHi, 2000); 例えば、setTimeout()からの呼び出しされり、とあるイベントに呼び出されたりと、複数のことに対応する必要がある関数の場合には役立ちます。また、setTimeout()のコールバックが数行以上のコードである場合、コードを整理整頓するのにも特に役立ちます。 setTimeout() は、タイムアウトを停止させたいときなど、後で参照するために使用できる識別子の値を返します。その方法については、タイムアウトの解除 (後述) を参照ください。 setTimeout() 関数へのパラメータの渡し方 setTimeout()の内部で実行される関数に渡したいパラメータは、リストの最後に追加パラメータとして渡す必要があります。 例えば、前の関数をリファクタリングして、渡された人の名前が何であれ、こんにちはと言うようにすることができます。 function sayHi(who) { alert(`Hello ${who}!`); } ここで、setTimeout()の呼び出しに、3番目のパラメータとして人の名前を渡すことができます。 let myGreeting = setTimeout(sayHi, 2000, 'Mr. Universe'); タイムアウトのクリア 最後に、タイムアウトを作成した場合、指定した時間が経過する前に、clearTimeout()を呼び出し、パラメータとしてsetTimeout()呼び出しの識別子を渡すことで、タイムアウトをキャンセルすることができます。つまり、上記のタイムアウトをキャンセルするには、次のようにします。 clearTimeout(myGreeting); setInterval() setTimeout() は、一定時間後に一度だけコードを実行する必要がある場合に完璧に機能します。しかし、何度も何度もコードを実行する必要がある場合、たとえばアニメーションの場合はどうなるのでしょうか。 そこで登場するのが、setInterval()です。これは setTimeout() と非常によく似た方法で動作しますが、 最初のパラメータとして渡した関数が一度だけでなく、 二番目のパラメータで指定したミリ秒以上の間隔で繰り返し実行される点が異なります。setInterval()の呼び出しの後続のパラメータとして、実行される関数が必要とする任意のパラメータを渡すことも可能です。 例を見てみましょう。次の関数は、新しい Date() オブジェクトを作成し、toLocaleTimeString() を使用してそこから時間文字列を抽出し、UI に表示します。そして、setInterval() を使って1秒に1回この関数を実行し、1秒に1回更新されるデジタル時計の効果を作り出しています。 function displayTime() { let date = new Date(); let time = date.toLocaleTimeString(); document.getElementById('demo').textContent = time; } const createClock = setInterval(displayTime, 1000); setTimeout()と同様に、setInterval()は、後で間隔を空ける必要があるときに使用できる識別値を返します。 インターバルのクリア setInterval() は、あなたが何かしない限り、永遠にタスクを実行し続けます。しかし時にはタスクを止めたいこともあるでしょう。そうしないと、ブラウザがタスクのそれ以上のバージョンを完了できないとき、またはタスクによって処理されるアニメーションが終了したときに、エラーを受け取ることになるかもしれません。停止方法は setTimeout() と同じです。setInterval() の呼び出しによって返された識別子を clearInterval() 関数に渡すことで行えます。 const myInterval = setInterval(myFunction, 2000); clearInterval(myInterval); setTimeout() と setInterval() の注意点 setTimeout()とsetInterval()を扱う際には、いくつか注意すべき点があります。今一度、これらを確認しておきましょう。 再帰的タイムアウト setTimeout()の使い方はもう一つあります。setInterval()を使う代わりに、再帰的に呼び出して同じコードを繰り返し実行させることができます。 以下の例では、再帰的に setTimeout() を使用して、渡された関数を100ミリ秒ごとに実行しています。 let i = 1; setTimeout(function run() { console.log(i); i++; setTimeout(run, 100); }, 100); 上の例と次の例を比べてみてください。これは、setInterval()を使って同じ効果を得ています。 let i = 1; setInterval(function run() { console.log(i); i++; }, 100); 再帰的なsetTimeout()とsetInterval()の違い 上記の2つのコードの違いは、微妙なところです。 再帰的なsetTimeout()は、コードの実行完了から次の呼び出しまでに与えられた遅延を保証します。つまり次の実行は、コードの実行が終了してからカウントを開始するため、コードの実行にかかった時間は除かれます。この例では、100ミリ秒は実行コードが終了してから次の実行が呼び出されるまでの遅延時間になります。 setInterval()を使用した例では、多少異なる点があります。繰り返す間隔には、実行したコードの実行にかかる時間が含まれてしまいます。例えば、コードの実行に 40 ミリ秒かかるとすると、間隔は 60 ミリ秒にしかなりません。 setTimeout()を再帰的に使用する場合、各反復処理では、次の反復処理を実行する前に異なる遅延を計算することができます。つまり、第2引数の値によって、コードを再実行するまでの待機時間をミリ秒単位で指定できます。 コードの実行に割り当てた時間間隔よりも長い時間がかかる可能性がある場合は、再帰的な setTimeout() を使用したほうがよいでしょう。この方法では、コードの実行にかかる時間にかかわらず、実行間隔を一定に保ち、エラーが発生することはありません。 timeoutの即時実行 setTimeout()の値として0を使用すると、指定されたコールバック関数をできるだけ早く実行しますが、メインコードスレッドが実行された後にのみ実行されます。 例えば、以下のコードでは、最初のアラートでOKをクリックするとすぐに「Hello」を含むアラートが出力され、次に「World」を含むアラートが出力されます。 setTimeout(function() { alert('World'); }, 0); alert('Hello'); これは、メインスレッドの実行がすべて終了したらすぐに実行するコードブロックを設定したい場合に便利です。非同期イベントループに置くと、その直後に実行されます。 requestAnimationFrame() requestAnimationFrame()は、ブラウザ上でアニメーションを効率的に実行するために作られた、特殊なエンキュー関数です。これは、ブラウザが次にディスプレイを再描画する前に指定されたコードのブロックを実行し、デバイスのディスプレイフレームレートと組み合わせて実行できるようにします。 setInterval()のような従来の非同期関数では、デバイスに最適化されたフレームレートで実行されず、場合によってはフレームが落ちるなどの問題が指摘されていたため、この関数が作成されました。また、アニメーションに適した最適化も欠けており、タブがアクティブでない場合やアニメーションがページの外にスクロールされた場合などに実行を停止するなどの問題がありました。 このメソッドは、再描画の前に呼び出されるコールバックを引数として受け取ります。一般的なパターンとしては下記のように使用されます。 function draw() { // Drawing code goes here requestAnimationFrame(draw); } draw(); 上記では、まずアニメーションの更新(スプライトの移動、スコアの更新、データのリフレッシュなど)を行う関数を定義しています。そして、この関数を呼び出して、処理を開始します。関数ブロックの最後に、関数自体をパラメータとして渡した requestAnimationFrame() を呼び出し、次のディスプレイの再描画時に再び関数を呼び出すようにブラウザに指示します。これは、requestAnimationFrame() を再帰的に呼び出しているため、継続的に実行されます。 アニメーションの再生速度は? アニメーションの滑らかさは、アニメーションのフレームレートに直接依存し、それはfps(frames per second)単位で測定されます。この数値が高ければ高いほど、アニメーションの見た目はある程度滑らかになります。 ほとんどの画面のリフレッシュレートは60Hzなので、Webブラウザで作業する場合、最も速いフレームレートは60フレーム/秒(FPS)を目指すことができます。しかし、フレーム数が増えると処理量が増えるので、しばしば吃音や音飛びが発生することがあります。 リフレッシュレート60Hzのモニタで60FPSを実現する場合、各フレームをレンダリングするためのアニメーションコードを実行する時間は約16.7ミリ秒(1000 / 60)です。これは、アニメーションのループを通過するたびに実行しようとするコードの量に注意する必要があることを心掛けるべきということです。 requestAnimationFrame() は常に、この魔法の 60 FPS 値にできるだけ近づけようとします。時にはそれが不可能なこともあります。本当に複雑なアニメーションで、遅いコンピュータで実行している場合は、フレームレートが低くなります。どのような場合でも、requestAnimationFrame() は常に可能な限り最善を尽くします。 requestAnimationFrame() と setInterval() や setTimeout() はどう違うか? ここで、requestAnimationFrame()メソッドが、先ほど使用した他のメソッドとどのように違うのか、少し詳しく説明します。先ほどのコードを見てみましょう function draw() { // Drawing code goes here requestAnimationFrame(draw); } draw(); 次に、setInterval()を使って同じことをする方法を見てみましょう。 function draw() { // Drawing code goes here } setInterval(draw, 17); 先ほど取り上げたように、requestAnimationFrame()には時間間隔を指定しません。ただ、現在の状況で可能な限り速く、スムーズに実行されるだけです。また、ブラウザは、何らかの理由でアニメーションが画面の外に出てしまった場合などにも、無駄な時間をかけて実行することはありません。 一方、setInterval()では、間隔を指定する必要があります。1000ミリ秒 / 60Hzの計算式で17という値を導き出し、それを切り上げました。切り上げるのは良いアイデアです。切り下げると、ブラウザは60FPSより速い速度でアニメーションを実行しようとするかもしれませんし、どのみちアニメーションの滑らかさには違いがありません。先にも述べたように、60Hzは標準的なリフレッシュレートです。 タイムスタンプを含める requestAnimationFrame()関数に渡される実際のコールバックには、パラメータとして、requestAnimationFrame()の実行開始からの時間を表すタイムスタンプ値も与えることができます。 これは、デバイスの速さや遅さに関係なく、特定の時間に一定のペースで処理を実行できるようにするために便利です。一般的なパターンは次のようなものです。 let startTime = null; function draw(timestamp) { if (!startTime) { startTime = timestamp; } currentTime = timestamp - startTime; // Do something based on current time requestAnimationFrame(draw); } draw(); ブラウザサポート requestAnimationFrame() は setInterval() / setTimeout() よりも最近のブラウザでサポートされています。Internet Explorer 10 以降で利用可能です。 そのため、古いバージョンのIEに対応する必要がない限り、requestAnimationFrame()を使わない理由はほとんどないでしょう。 requestAnimationFrame()の呼び出しをクリアする requestAnimationFrame()の呼び出しをクリアするには、cancelAnimationFrame()メソッドを呼び出すことで可能です。(関数名は "set..." メソッドのように "clear" ではなく、 "cancel" で始まることに注意してください)。 変数rAFに格納した、requestAnimationFrame()の値を渡すだけで、キャンセルすることができます。 cancelAnimationFrame(rAF);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの非同期を理解するにはMDN Web Docsを読むべき。翻訳しといたよ Part.2

こんにちはWebエンジニアのmasakichiです。 JavaScriptの非同期を理解するにはMDN Web Docsを読むべき。翻訳しといたよ 2つ目の記事です。 全部で5記事あります。 Introducing asynchronous JavaScript Cooperative asynchronous JavaScript: Timeouts and intervals ←いまここ Graceful asynchronous programming with Promises Making asynchronous programming easier with async and await Choosing the right approach 翻訳箇所について こちらのページの日本語未翻訳記事です。 なぜ翻訳したか JavaScriptの非同期処理。特にPromiseというやつが、ぼんやりわかっているけど完全には理解していない状態がずっと続いていました。 そんな中、MDN Web Docsの解説がすごくわかりやすく一気に理解できました。 しかし、これらの記事は日本語に翻訳されていないという問題が。。。 ぜひ非同期処理で悩める同志にも読んでほしい。という想いで翻訳作業をしてみました。 留意点 筆者は英語がそこまで得意というわけではありません。DeepLの力を借りて、翻訳していますので、日本語訳が不自然なところや一部、情報を省略・意訳しています。あらかじめご了承ください。 ライセンスは下記です。 This Article by Mozilla Contributors is licensed under CC-BY-SA 2.5. 協調的な非同期JavaScript:タイムアウトとインターバル このチュートリアルでは、設定した時間が経過した後、あるいは一定の間隔(例えば1秒間に何回)でコードを非同期に実行するためにJavaScriptが利用できる従来の方法を取り上げ、それらが何のために有用であるかを議論し、固有の問題を考察しています。 前提条件 基本的なコンピュータリテラシー / JavaScriptの基本をそれなりに理解していること。 目的 非同期ループとインターバルについて理解し、何に役立つかを理解する。 イントロダクション 以前からWebプラットフォームでは、JavaScriptプログラマーに対して、ある時間間隔が経過した後に非同期でコードを実行したり、停止を指示するまで非同期でコードブロックを繰り返し実行したりする機能が数多く提供されています。 これらの機能は setTimeout(): 指定された時間経過後に、指定されたコードブロックを1回実行する。 setInterval(): 指定されたコードのブロックを、呼び出しのたびに一定の時間遅らせて繰り返し実行する。 requestAnimationFrame(): setInterval()の現代版。ブラウザが次にディスプレイを再描画する前に指定したコードブロックを実行し、実行中の環境に関係なく適切なフレームレートでアニメーションを実行できるようにします。 これらの関数で設定された非同期コードは、(指定されたタイマーが経過した後)メインスレッドで実行されます。 setTimeout()の呼び出しが実行される前や、setInterval()の繰り返しの間に、他のコードを実行できる(そしてしばしば実行する)ことを知っておくことが重要です。非同期コードはメインスレッドが利用可能になった後にのみ実行されるため、これらの操作がどれほどプロセッサ集約的であるかによって、それらは非同期コードをさらに遅らせることができます。(なぜなら、非同期コードはメインスレッドが使用可能になった後にのみ実行されるからです(言い換えれば、スタックが空になったとき)。この件に関しては、この記事を読み進めるにつれて詳しく知ることができます。 いずれにせよ、これらの関数は、Webサイトやアプリケーションで一定のアニメーションやその他のバックグラウンド処理を実行するために使用されます。以下のセクションでは、これらの関数の使用方法について説明します。 setTimeout() 先に述べたように、setTimeout()は指定された時間が経過した後、特定のコードブロックを一回実行します。これは以下のパラメータをとります。 実行する関数、または他の場所で定義された関数への参照。 コードを実行する前に待機する時間間隔をミリ秒単位で表した数値(1000ミリ秒は1秒に相当)。0を指定すると(あるいは値を省略すると)、関数はできるだけ早く実行されます。(なぜ「すぐに」ではなく「できるだけ早く」実行するのかについては、以下の注釈を参照してください)。なぜこのようなことをしたいのかについては、後で詳しく説明します。 ゼロまたはそれ以上の値で、関数が実行されるときに渡したいパラメータを表します。 注意:指定された時間(または遅延時間)は、実行までの保証時間ではなく、実行までの最短時間です。これらの関数に渡すコールバックは、メインスレッド上のスタックが空になるまで実行できません。 その結果、setTimeout(fn, 0)のようなコードは、スタックが空になるとすぐに実行されるのではなく、すぐに実行されます。setTimeout(fn, 0)のようなコードを実行し、その直後に1から100億までカウントするループを実行すると、コールバックは数秒後に実行されることになります。 次の例では、ブラウザは匿名関数を実行する前に2秒間待機し、その後アラートメッセージを表示します。 let myGreeting = setTimeout(() => { alert('Hello, Mr. Universe!'); }, 2000); 指定する関数は、匿名である必要はありません。関数に名前をつけて、どこか別の場所で定義して、setTimeout()に関数の参照を渡すことも可能です。次の2つのバージョンのコードスニペットは、最初のコードと同等です。 // With a named function let myGreeting = setTimeout(function sayHi() { alert('Hello, Mr. Universe!'); }, 2000); // With a function defined separately function sayHi() { alert('Hello Mr. Universe!'); } let myGreeting = setTimeout(sayHi, 2000); 例えば、setTimeout()からの呼び出しされり、とあるイベントに呼び出されたりと、複数のことに対応する必要がある関数の場合には役立ちます。また、setTimeout()のコールバックが数行以上のコードである場合、コードを整理整頓するのにも特に役立ちます。 setTimeout() は、タイムアウトを停止させたいときなど、後で参照するために使用できる識別子の値を返します。その方法については、タイムアウトの解除 (後述) を参照ください。 setTimeout() 関数へのパラメータの渡し方 setTimeout()の内部で実行される関数に渡したいパラメータは、リストの最後に追加パラメータとして渡す必要があります。 例えば、前の関数をリファクタリングして、渡された人の名前が何であれ、こんにちはと言うようにすることができます。 function sayHi(who) { alert(`Hello ${who}!`); } ここで、setTimeout()の呼び出しに、3番目のパラメータとして人の名前を渡すことができます。 let myGreeting = setTimeout(sayHi, 2000, 'Mr. Universe'); タイムアウトのクリア 最後に、タイムアウトを作成した場合、指定した時間が経過する前に、clearTimeout()を呼び出し、パラメータとしてsetTimeout()呼び出しの識別子を渡すことで、タイムアウトをキャンセルすることができます。つまり、上記のタイムアウトをキャンセルするには、次のようにします。 clearTimeout(myGreeting); setInterval() setTimeout() は、一定時間後に一度だけコードを実行する必要がある場合に完璧に機能します。しかし、何度も何度もコードを実行する必要がある場合、たとえばアニメーションの場合はどうなるのでしょうか。 そこで登場するのが、setInterval()です。これは setTimeout() と非常によく似た方法で動作しますが、 最初のパラメータとして渡した関数が一度だけでなく、 二番目のパラメータで指定したミリ秒以上の間隔で繰り返し実行される点が異なります。setInterval()の呼び出しの後続のパラメータとして、実行される関数が必要とする任意のパラメータを渡すことも可能です。 例を見てみましょう。次の関数は、新しい Date() オブジェクトを作成し、toLocaleTimeString() を使用してそこから時間文字列を抽出し、UI に表示します。そして、setInterval() を使って1秒に1回この関数を実行し、1秒に1回更新されるデジタル時計の効果を作り出しています。 function displayTime() { let date = new Date(); let time = date.toLocaleTimeString(); document.getElementById('demo').textContent = time; } const createClock = setInterval(displayTime, 1000); setTimeout()と同様に、setInterval()は、後で間隔を空ける必要があるときに使用できる識別値を返します。 インターバルのクリア setInterval() は、あなたが何かしない限り、永遠にタスクを実行し続けます。しかし時にはタスクを止めたいこともあるでしょう。そうしないと、ブラウザがタスクのそれ以上のバージョンを完了できないとき、またはタスクによって処理されるアニメーションが終了したときに、エラーを受け取ることになるかもしれません。停止方法は setTimeout() と同じです。setInterval() の呼び出しによって返された識別子を clearInterval() 関数に渡すことで行えます。 const myInterval = setInterval(myFunction, 2000); clearInterval(myInterval); setTimeout() と setInterval() の注意点 setTimeout()とsetInterval()を扱う際には、いくつか注意すべき点があります。今一度、これらを確認しておきましょう。 再帰的タイムアウト setTimeout()の使い方はもう一つあります。setInterval()を使う代わりに、再帰的に呼び出して同じコードを繰り返し実行させることができます。 以下の例では、再帰的に setTimeout() を使用して、渡された関数を100ミリ秒ごとに実行しています。 let i = 1; setTimeout(function run() { console.log(i); i++; setTimeout(run, 100); }, 100); 上の例と次の例を比べてみてください。これは、setInterval()を使って同じ効果を得ています。 let i = 1; setInterval(function run() { console.log(i); i++; }, 100); 再帰的なsetTimeout()とsetInterval()の違い 上記の2つのコードの違いは、微妙なところです。 再帰的なsetTimeout()は、コードの実行完了から次の呼び出しまでに与えられた遅延を保証します。つまり次の実行は、コードの実行が終了してからカウントを開始するため、コードの実行にかかった時間は除かれます。この例では、100ミリ秒は実行コードが終了してから次の実行が呼び出されるまでの遅延時間になります。 setInterval()を使用した例では、多少異なる点があります。繰り返す間隔には、実行したコードの実行にかかる時間が含まれてしまいます。例えば、コードの実行に 40 ミリ秒かかるとすると、間隔は 60 ミリ秒にしかなりません。 setTimeout()を再帰的に使用する場合、各反復処理では、次の反復処理を実行する前に異なる遅延を計算することができます。つまり、第2引数の値によって、コードを再実行するまでの待機時間をミリ秒単位で指定できます。 コードの実行に割り当てた時間間隔よりも長い時間がかかる可能性がある場合は、再帰的な setTimeout() を使用したほうがよいでしょう。この方法では、コードの実行にかかる時間にかかわらず、実行間隔を一定に保ち、エラーが発生することはありません。 timeoutの即時実行 setTimeout()の値として0を使用すると、指定されたコールバック関数をできるだけ早く実行しますが、メインコードスレッドが実行された後にのみ実行されます。 例えば、以下のコードでは、最初のアラートでOKをクリックするとすぐに「Hello」を含むアラートが出力され、次に「World」を含むアラートが出力されます。 setTimeout(function() { alert('World'); }, 0); alert('Hello'); これは、メインスレッドの実行がすべて終了したらすぐに実行するコードブロックを設定したい場合に便利です。非同期イベントループに置くと、その直後に実行されます。 requestAnimationFrame() requestAnimationFrame()は、ブラウザ上でアニメーションを効率的に実行するために作られた、特殊なエンキュー関数です。これは、ブラウザが次にディスプレイを再描画する前に指定されたコードのブロックを実行し、デバイスのディスプレイフレームレートと組み合わせて実行できるようにします。 setInterval()のような従来の非同期関数では、デバイスに最適化されたフレームレートで実行されず、場合によってはフレームが落ちるなどの問題が指摘されていたため、この関数が作成されました。また、アニメーションに適した最適化も欠けており、タブがアクティブでない場合やアニメーションがページの外にスクロールされた場合などに実行を停止するなどの問題がありました。 このメソッドは、再描画の前に呼び出されるコールバックを引数として受け取ります。一般的なパターンとしては下記のように使用されます。 function draw() { // Drawing code goes here requestAnimationFrame(draw); } draw(); 上記では、まずアニメーションの更新(スプライトの移動、スコアの更新、データのリフレッシュなど)を行う関数を定義しています。そして、この関数を呼び出して、処理を開始します。関数ブロックの最後に、関数自体をパラメータとして渡した requestAnimationFrame() を呼び出し、次のディスプレイの再描画時に再び関数を呼び出すようにブラウザに指示します。これは、requestAnimationFrame() を再帰的に呼び出しているため、継続的に実行されます。 アニメーションの再生速度は? アニメーションの滑らかさは、アニメーションのフレームレートに直接依存し、それはfps(frames per second)単位で測定されます。この数値が高ければ高いほど、アニメーションの見た目はある程度滑らかになります。 ほとんどの画面のリフレッシュレートは60Hzなので、Webブラウザで作業する場合、最も速いフレームレートは60フレーム/秒(FPS)を目指すことができます。しかし、フレーム数が増えると処理量が増えるので、しばしば吃音や音飛びが発生することがあります。 リフレッシュレート60Hzのモニタで60FPSを実現する場合、各フレームをレンダリングするためのアニメーションコードを実行する時間は約16.7ミリ秒(1000 / 60)です。これは、アニメーションのループを通過するたびに実行しようとするコードの量に注意する必要があることを心掛けるべきということです。 requestAnimationFrame() は常に、この魔法の 60 FPS 値にできるだけ近づけようとします。時にはそれが不可能なこともあります。本当に複雑なアニメーションで、遅いコンピュータで実行している場合は、フレームレートが低くなります。どのような場合でも、requestAnimationFrame() は常に可能な限り最善を尽くします。 requestAnimationFrame() と setInterval() や setTimeout() はどう違うか? ここで、requestAnimationFrame()メソッドが、先ほど使用した他のメソッドとどのように違うのか、少し詳しく説明します。先ほどのコードを見てみましょう function draw() { // Drawing code goes here requestAnimationFrame(draw); } draw(); 次に、setInterval()を使って同じことをする方法を見てみましょう。 function draw() { // Drawing code goes here } setInterval(draw, 17); 先ほど取り上げたように、requestAnimationFrame()には時間間隔を指定しません。ただ、現在の状況で可能な限り速く、スムーズに実行されるだけです。また、ブラウザは、何らかの理由でアニメーションが画面の外に出てしまった場合などにも、無駄な時間をかけて実行することはありません。 一方、setInterval()では、間隔を指定する必要があります。1000ミリ秒 / 60Hzの計算式で17という値を導き出し、それを切り上げました。切り上げるのは良いアイデアです。切り下げると、ブラウザは60FPSより速い速度でアニメーションを実行しようとするかもしれませんし、どのみちアニメーションの滑らかさには違いがありません。先にも述べたように、60Hzは標準的なリフレッシュレートです。 タイムスタンプを含める requestAnimationFrame()関数に渡される実際のコールバックには、パラメータとして、requestAnimationFrame()の実行開始からの時間を表すタイムスタンプ値も与えることができます。 これは、デバイスの速さや遅さに関係なく、特定の時間に一定のペースで処理を実行できるようにするために便利です。一般的なパターンは次のようなものです。 let startTime = null; function draw(timestamp) { if (!startTime) { startTime = timestamp; } currentTime = timestamp - startTime; // Do something based on current time requestAnimationFrame(draw); } draw(); ブラウザサポート requestAnimationFrame() は setInterval() / setTimeout() よりも最近のブラウザでサポートされています。Internet Explorer 10 以降で利用可能です。 そのため、古いバージョンのIEに対応する必要がない限り、requestAnimationFrame()を使わない理由はほとんどないでしょう。 requestAnimationFrame()の呼び出しをクリアする requestAnimationFrame()の呼び出しをクリアするには、cancelAnimationFrame()メソッドを呼び出すことで可能です。(関数名は "set..." メソッドのように "clear" ではなく、 "cancel" で始まることに注意してください)。 変数rAFに格納した、requestAnimationFrame()の値を渡すだけで、キャンセルすることができます。 cancelAnimationFrame(rAF);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MDN Web Docsの「ウェブ開発を学ぶ - 非同期 JavaScript」を翻訳してみた Part.1

こんにちはWebエンジニアのmasakichiです。 MDN Web Docsの「ウェブ開発を学ぶ - 非同期 JavaScript」を翻訳してみた1つ目の記事です。 全部で5記事あります。 Introducing asynchronous JavaScript ←いまここ Cooperative asynchronous JavaScript: Timeouts and intervals Graceful asynchronous programming with Promises Making asynchronous programming easier with async and await Choosing the right approach 翻訳箇所について こちらのページの日本語未翻訳記事です。 なぜ翻訳したか JavaScriptの非同期処理。特にPromiseというやつが、ぼんやりわかっているけど完全には理解していない状態がずっと続いていました。 そんな中、MDN Web Docsの解説がすごくわかりやすく一気に理解できました。 しかし、これらの記事は日本語に翻訳されていないという問題が。。。 ぜひ非同期処理で悩める同志にも読んでほしい。という想いで翻訳作業をしてみました。 留意点 筆者は英語がそこまで得意というわけではありません。DeepLの力を借りて、翻訳していますので、日本語訳が不自然なところや一部、情報を省略・意訳しています。あらかじめご了承ください。 ライセンスは下記です。 This Article by Mozilla Contributors is licensed under CC-BY-SA 2.5. 非同期JavaScriptの紹介 この記事では、同期的なJavaScriptに関連する問題を簡単に振り返り、あなたが遭遇するさまざまな非同期技術のいくつかを最初に見て、それらがどのようにその問題を解決するのに役立つかについて説明します。 前提条件 基本的なコンピュータリテラシー / JavaScriptの基本をそれなりに理解していること。 目的 JavaScriptの非同期とは何か、同期とどう違うのか、どのようなユースケースがあるのかについて理解を深める。 同期型JavaScript JavaScriptの非同期を理解するために、まずJavaScriptの同期について理解することから始める必要があります。このセクションでは、前回の記事で見た情報のいくつかをまとめます。 これまでの学習モジュールで見てきた機能の多くは同期型です。つまり、あるコードを実行すると、ブラウザがそのコードを実行できるようになると同時に結果が返されるのです。簡単な例を見てみましょう。 const btn = document.querySelector('button'); btn.addEventListener('click', () => { alert('You clicked me!'); let pElem = document.createElement('p'); pElem.textContent = 'This is a newly-added paragraph.'; document.body.appendChild(pElem); }); 各操作が処理されている間は、他のことは何もできません - レンダリングは一時停止されます。これは、前回の記事で述べたように、JavaScriptがシングルスレッドであることに起因しています。1つのメインスレッドで一度に発生するのは1つのことだけであり、他のすべては操作が完了するまでブロックされます。 非同期JavaScript 先に説明した理由(ブロッキングに関連するものなど)により、現在では多くのWebAPI機能が非同期コードを使用して実行されています。特に、ネットワークからファイルを取得する、データベースにアクセスしてデータを返す、WebCamからビデオストリームにアクセスする、VRヘッドセットに表示をブロードキャストするなど、外部デバイスから何らかのリソースにアクセスしたり取得したりするものがそうです。 なぜ、非同期コードを使うと難しいのでしょうか?簡単な例を見てみましょう。サーバーから画像を取得する場合、その結果をすぐに返すことはできません。つまり、次のような(擬似)コードは動作しないことになります。 let response = fetch('myImage.png'); // fetch is asynchronous let blob = response.blob(); // display your image blob in the UI somehow なぜなら、画像のダウンロードにどれくらい時間がかかるかわからないからです。そのため、2行目を実行しようとすると、まだレスポンスが得られていないため、(断続的に、あるいは毎回)エラーを投げてしまうのです。その代わりに、レスポンスが返ってくるまで待機するコードが必要なのです。 JavaScriptのコードで目にする非同期コードのスタイルには、大きく分けて旧来のコールバック型と新しいプロミス型の2種類があります。以下のセクションでは、それぞれを順番にレビューしていきます。 非同期コールバック 非同期コールバックは、バックグラウンドでコードの実行を開始する関数を呼び出す際に、引数として指定される関数です。バックグラウンドのコードの実行が終了すると、コールバック関数が呼び出され、作業が完了したことを知らせたり、興味深いことが起こったことを知らせたりします。コールバックの使用は、今では少し古くなりましたが、古いけれどもよく使われるAPIでは、まだ使われているのを見かけることができます。 非同期コールバックの例として、addEventListener()メソッドの2番目のパラメータがあります(上のアクションで見たとおりです)。 btn.addEventListener('click', () => { alert('You clicked me!'); let pElem = document.createElement('p'); pElem.textContent = 'This is a newly-added paragraph.'; document.body.appendChild(pElem); }); 最初のパラメータはリスニングするイベントのタイプで、2番目のパラメータはイベントが発生したときに呼び出されるコールバック関数です。 コールバック関数を他の関数に引数として渡す場合、関数の参照を引数として渡しているだけであり、コールバック関数はすぐには実行されません。コールバック関数を内包する関数に非同期的に「呼び出されます」(だからコールバック)。コールバック関数を実行するのは、コールバック関数を内包する関数なのです。 コールバックを含む独自の関数は簡単に書くことができます。XMLHttpRequestAPIを使ってリソースをロードする例を見てみましょう。 function loadAsset(url, type, callback) { let xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = type; xhr.onload = function() { callback(xhr.response); }; xhr.send(); } function displayImage(blob) { let objectURL = URL.createObjectURL(blob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } loadAsset('coffee.jpg', 'blob', displayImage); ここでは、オブジェクトの URL として渡された blob を表す displayImage() 関数を作成し、その URL を表示する画像を作成して、ドキュメントの <body> に追加しています。そして次に、画像を取得する URL ・コンテンツタイプ・コールバックの3つをパラメータとして受け取る loadAsset() 関数を作成します。XMLHttpRequest(しばしば「XHR」と省略されます)を使用して、指定されたURLのリソースを取得し、その応答をコールバックに渡して処理を行います。この場合、コールバックは XHR 呼び出しがリソースのダウンロードを完了するのを待ってから(onload イベント ハンドラを使用して)、コールバックにリソースを渡します。 コールバックは多機能です。関数の実行順序や関数間で渡されるデータを制御できるだけでなく、状況に応じて異なる関数にデータを渡すことができます。つまり、ダウンロードしたレスポンスに対して、processJSON()、displayText()などの異なるアクションを実行させることができるのです。 すべてのコールバックが非同期であるわけではなく、同期で実行されるものもあることに注意してください。例えば、Array.prototype.forEach()を使用して配列内の項目をループする場合です。 const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus']; gods.forEach(function (eachName, index){ console.log(index + '. ' + eachName); }); この例では、ギリシャ神話の神々の配列をループして、インデックス番号と値をコンソールに出力しています。forEach() のパラメータとして期待されるのはコールバック関数で、それ自身は配列名とインデックス値への参照という 2 つのパラメータを受け取ります。しかし、この関数は何も待たず、すぐに実行されます。 Promises プロミスは、最近のWeb APIで使われている新しいスタイルの非同期コードです。その良い例が fetch() API で、基本的には XMLHttpRequest の現代的でより効率的なバージョンのようなものです。サーバーからのデータ取得の記事から、簡単な例を見てみましょう。 fetch('products.json').then(function(response) { return response.json(); }).then(function(json) { let products = json; initialize(products); }).catch(function(err) { console.log('Fetch problem: ' + err.message); }); ここでは、fetch()が1つのパラメータ(ネットワークから取り出したいリソースのURL)を受け取り、プロミスを返す様子を示しています。プロミスは、非同期処理の完了または失敗を表すオブジェクトです。いわば、中間状態を表しています。要するに、ブラウザが「できるだけ早く答えを返すことを約束します」という意味で、"promise "という名前になっているのです。 この考え方は、慣れるまでちょっとシュレーディンガーの猫のような感じがします。どちらの結果もまだ起こっていないので、フェッチ操作は現在、将来のある時点でブラウザが操作を完了しようとした結果を待っている状態です。fetch()の末尾には、さらに3つのコードブロックが連結されています。 2つのthen()ブロックです。両方とも、直前の操作が成功した場合に実行されるコールバック関数を含んでおり、それぞれのコールバックは直前の成功した操作の結果を入力として受け取るので、前に進んでそれに対して何か他の操作をすることができます。それぞれの .then() ブロックは別のプロミスを返すので、複数の .then() ブロックを互いに連結して、複数の非同期処理を順番に実行させることができることを意味します。 最後の catch() ブロックは、 .then() ブロックのいずれかが失敗した場合に実行されます。同期 try...catch ブロックと同様の方法で、エラーオブジェクトが catch() の中で利用可能になり、発生したエラーの種類を報告するために使用することができます。しかし、同期try...catchはプロミスでは動作しませんが、後で学ぶようにasync/awaitでは動作することに注意してください。 イベントキュー プロミスのような非同期処理はイベントキューに入れられ、メインスレッドが処理を終えた後に実行されるので、後続のJavaScriptコードの実行をブロックすることがありません。キューに入れられた処理は、できるだけ早く完了し、その結果を JavaScript 環境に返します。 プロミス vs コールバック プロミスは旧来のコールバックと類似している部分があります。プロミスは基本的にreturnされたオブジェクトにコールバック関数に渡すもので、関数の中にコールバックを渡す必要はありません。 しかし、プロミスは非同期処理を行うために特化して作られており、旧来のコールバックと比較して多くの利点があります。 複数の.then()を使って複数の非同期処理を連鎖させ、一つの結果を次の処理に入力として渡すことができる。これはコールバックでは非常に難しく、しばしば厄介な「破滅のピラミッド」(コールバック地獄とも呼ばれる)になってしまう。 Promiseコールバックは、常にイベントキューに入れられた厳密な順番で呼び出されます。 エラー処理は、「ピラミッド」の各レベルで個別に処理されるのではなく、ブロックの最後にある単一の .catch() ブロックですべてのエラーが処理されます。 プロミスは、サードパーティライブラリにコールバックを渡すと関数がどのように実行されるかの完全な制御を失う古いスタイルのコールバックとは異なり、制御の逆転を回避することができます。 非同期コードの性質 非同期コードの性質をさらに説明する例として、コードの実行順序を十分に意識しない場合に起こりうること、非同期コードを同期コードのように扱おうとした場合の問題点などを探ってみましょう。次の例は、以前に見たものとかなり似ています。ひとつ違うのは、console.log()ステートメントをいくつも使って、コードの実行順序を説明していることです。 console.log ('Starting'); let image; fetch('coffee.jpg').then((response) => { console.log('It worked :)') return response.blob(); }).then((myBlob) => { let objectURL = URL.createObjectURL(myBlob); image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }).catch((error) => { console.log('There has been a problem with your fetch operation: ' + error.message); }); console.log ('All done!'); ブラウザはコードの実行を開始し、最初のconsole.log()ステートメント(Starting)を見て実行し、その後、image変数を作成することになります。 その後、次の行に移動して fetch() ブロックの実行が始まりますが、fetch() はブロックせずに非同期で実行されるため、プロミス関連のコードの後もコードの実行が続き、最後の console.log() 文 (All done!) に到達してコンソールに出力されることになります。 fetch() ブロックの実行が完全に終了し、その結果を .then() ブロックで配信して初めて、ようやく 2 つ目の console.log() メッセージ (It worked) が表示されるのです。このように、メッセージは期待とは異なる順序で表示されています。 もし、これで混乱したのなら、次のような小さな例を考えてみてください。 console.log("registering click handler"); button.addEventListener('click', () => { console.log("get click"); }); console.log("all done"); この動作が先程のコードと非常に似ています。1つ目と3つ目のconsole.log()メッセージはすぐに表示されますが、2つ目のメッセージは誰かがマウスボタンをクリックするまで実行されません。前の例も同じように動作しますが、この場合、2つ目のメッセージはクリックではなく、リソースを取得して画面に表示するプロミスチェーンでブロックされます。 この些細なコードの設定例は問題を引き起こす可能性があります。つまり、同期処理にその結果を依存されている非同期コードブロックを設定することはできないのです。ブラウザが同期ブロックを処理する前に非同期関数が返されることを保証できないからです。 結論 JavaScriptは最も基本的な形式として、同期、ブロッキング、シングルスレッド言語であり、一度に一つの処理しか行うことができません。しかし、Webブラウザは関数やAPIを定義しており、同期的に実行されないで、何らかのイベント(時間の経過、ユーザーのマウス操作、ネットワーク上のデータの到着など)が発生したときに非同期に呼び出すべき関数を登録することができる。つまり、メインスレッドを停止したりブロックしたりすることなく、コードに複数の処理を同時に行わせることができるのです。 コードを同期的に走らせるか、非同期的に走らせるかは、何をしようとしているかによって変わってきます。 物事をロードしてすぐに実行させたい場合があります。例えば、ユーザー定義のスタイルをウェブページに適用する場合、できるだけ早くスタイルを適用したいでしょう。 しかし、データベースに問い合わせを行い、その結果をテンプレートに反映させるような時間のかかる処理を行う場合は、スタックから外して非同期にタスクを完了させた方がよいでしょう。時間をかければ、どのような場合に同期ではなく非同期の手法を選択するのがより理にかなっているかがわかるようになります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの非同期を理解するにはMDN Web Docsを読むべき。翻訳しといたよ Part.1

こんにちはWebエンジニアのmasakichiです。 JavaScriptの非同期を理解するにはMDN Web Docsを読むべき。翻訳しといたよ 1つ目の記事です。 全部で5記事あります。 Introducing asynchronous JavaScript ←いまここ Cooperative asynchronous JavaScript: Timeouts and intervals Graceful asynchronous programming with Promises Making asynchronous programming easier with async and await Choosing the right approach 翻訳箇所について こちらのページの日本語未翻訳記事です。 なぜ翻訳したか JavaScriptの非同期処理。特にPromiseというやつが、ぼんやりわかっているけど完全には理解していない状態がずっと続いていました。 そんな中、MDN Web Docsの解説がすごくわかりやすく一気に理解できました。 しかし、これらの記事は日本語に翻訳されていないという問題が。。。 ぜひ非同期処理で悩める同志にも読んでほしい。という想いで翻訳作業をしてみました。 留意点 筆者は英語がそこまで得意というわけではありません。DeepLの力を借りて、翻訳していますので、日本語訳が不自然なところや一部、情報を省略・意訳しています。あらかじめご了承ください。 ライセンスは下記です。 This Article by Mozilla Contributors is licensed under CC-BY-SA 2.5. 非同期JavaScriptの紹介 この記事では、同期的なJavaScriptに関連する問題を簡単に振り返り、あなたが遭遇するさまざまな非同期技術のいくつかを最初に見て、それらがどのようにその問題を解決するのに役立つかについて説明します。 前提条件 基本的なコンピュータリテラシー / JavaScriptの基本をそれなりに理解していること。 目的 JavaScriptの非同期とは何か、同期とどう違うのか、どのようなユースケースがあるのかについて理解を深める。 同期型JavaScript JavaScriptの非同期を理解するために、まずJavaScriptの同期について理解することから始める必要があります。このセクションでは、前回の記事で見た情報のいくつかをまとめます。 これまでの学習モジュールで見てきた機能の多くは同期型です。つまり、あるコードを実行すると、ブラウザがそのコードを実行できるようになると同時に結果が返されるのです。簡単な例を見てみましょう。 const btn = document.querySelector('button'); btn.addEventListener('click', () => { alert('You clicked me!'); let pElem = document.createElement('p'); pElem.textContent = 'This is a newly-added paragraph.'; document.body.appendChild(pElem); }); 各操作が処理されている間は、他のことは何もできません - レンダリングは一時停止されます。これは、前回の記事で述べたように、JavaScriptがシングルスレッドであることに起因しています。1つのメインスレッドで一度に発生するのは1つのことだけであり、他のすべては操作が完了するまでブロックされます。 非同期JavaScript 先に説明した理由(ブロッキングに関連するものなど)により、現在では多くのWebAPI機能が非同期コードを使用して実行されています。特に、ネットワークからファイルを取得する、データベースにアクセスしてデータを返す、WebCamからビデオストリームにアクセスする、VRヘッドセットに表示をブロードキャストするなど、外部デバイスから何らかのリソースにアクセスしたり取得したりするものがそうです。 なぜ、非同期コードを使うと難しいのでしょうか?簡単な例を見てみましょう。サーバーから画像を取得する場合、その結果をすぐに返すことはできません。つまり、次のような(擬似)コードは動作しないことになります。 let response = fetch('myImage.png'); // fetch is asynchronous let blob = response.blob(); // display your image blob in the UI somehow なぜなら、画像のダウンロードにどれくらい時間がかかるかわからないからです。そのため、2行目を実行しようとすると、まだレスポンスが得られていないため、(断続的に、あるいは毎回)エラーを投げてしまうのです。その代わりに、レスポンスが返ってくるまで待機するコードが必要なのです。 JavaScriptのコードで目にする非同期コードのスタイルには、大きく分けて旧来のコールバック型と新しいプロミス型の2種類があります。以下のセクションでは、それぞれを順番にレビューしていきます。 非同期コールバック 非同期コールバックは、バックグラウンドでコードの実行を開始する関数を呼び出す際に、引数として指定される関数です。バックグラウンドのコードの実行が終了すると、コールバック関数が呼び出され、作業が完了したことを知らせたり、興味深いことが起こったことを知らせたりします。コールバックの使用は、今では少し古くなりましたが、古いけれどもよく使われるAPIでは、まだ使われているのを見かけることができます。 非同期コールバックの例として、addEventListener()メソッドの2番目のパラメータがあります(上のアクションで見たとおりです)。 btn.addEventListener('click', () => { alert('You clicked me!'); let pElem = document.createElement('p'); pElem.textContent = 'This is a newly-added paragraph.'; document.body.appendChild(pElem); }); 最初のパラメータはリスニングするイベントのタイプで、2番目のパラメータはイベントが発生したときに呼び出されるコールバック関数です。 コールバック関数を他の関数に引数として渡す場合、関数の参照を引数として渡しているだけであり、コールバック関数はすぐには実行されません。コールバック関数を内包する関数に非同期的に「呼び出されます」(だからコールバック)。コールバック関数を実行するのは、コールバック関数を内包する関数なのです。 コールバックを含む独自の関数は簡単に書くことができます。XMLHttpRequestAPIを使ってリソースをロードする例を見てみましょう。 function loadAsset(url, type, callback) { let xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = type; xhr.onload = function() { callback(xhr.response); }; xhr.send(); } function displayImage(blob) { let objectURL = URL.createObjectURL(blob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } loadAsset('coffee.jpg', 'blob', displayImage); ここでは、オブジェクトの URL として渡された blob を表す displayImage() 関数を作成し、その URL を表示する画像を作成して、ドキュメントの <body> に追加しています。そして次に、画像を取得する URL ・コンテンツタイプ・コールバックの3つをパラメータとして受け取る loadAsset() 関数を作成します。XMLHttpRequest(しばしば「XHR」と省略されます)を使用して、指定されたURLのリソースを取得し、その応答をコールバックに渡して処理を行います。この場合、コールバックは XHR 呼び出しがリソースのダウンロードを完了するのを待ってから(onload イベント ハンドラを使用して)、コールバックにリソースを渡します。 コールバックは多機能です。関数の実行順序や関数間で渡されるデータを制御できるだけでなく、状況に応じて異なる関数にデータを渡すことができます。つまり、ダウンロードしたレスポンスに対して、processJSON()、displayText()などの異なるアクションを実行させることができるのです。 すべてのコールバックが非同期であるわけではなく、同期で実行されるものもあることに注意してください。例えば、Array.prototype.forEach()を使用して配列内の項目をループする場合です。 const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus']; gods.forEach(function (eachName, index){ console.log(index + '. ' + eachName); }); この例では、ギリシャ神話の神々の配列をループして、インデックス番号と値をコンソールに出力しています。forEach() のパラメータとして期待されるのはコールバック関数で、それ自身は配列名とインデックス値への参照という 2 つのパラメータを受け取ります。しかし、この関数は何も待たず、すぐに実行されます。 Promises プロミスは、最近のWeb APIで使われている新しいスタイルの非同期コードです。その良い例が fetch() API で、基本的には XMLHttpRequest の現代的でより効率的なバージョンのようなものです。サーバーからのデータ取得の記事から、簡単な例を見てみましょう。 fetch('products.json').then(function(response) { return response.json(); }).then(function(json) { let products = json; initialize(products); }).catch(function(err) { console.log('Fetch problem: ' + err.message); }); ここでは、fetch()が1つのパラメータ(ネットワークから取り出したいリソースのURL)を受け取り、プロミスを返す様子を示しています。プロミスは、非同期処理の完了または失敗を表すオブジェクトです。いわば、中間状態を表しています。要するに、ブラウザが「できるだけ早く答えを返すことを約束します」という意味で、"promise "という名前になっているのです。 この考え方は、慣れるまでちょっとシュレーディンガーの猫のような感じがします。どちらの結果もまだ起こっていないので、フェッチ操作は現在、将来のある時点でブラウザが操作を完了しようとした結果を待っている状態です。fetch()の末尾には、さらに3つのコードブロックが連結されています。 2つのthen()ブロックです。両方とも、直前の操作が成功した場合に実行されるコールバック関数を含んでおり、それぞれのコールバックは直前の成功した操作の結果を入力として受け取るので、前に進んでそれに対して何か他の操作をすることができます。それぞれの .then() ブロックは別のプロミスを返すので、複数の .then() ブロックを互いに連結して、複数の非同期処理を順番に実行させることができることを意味します。 最後の catch() ブロックは、 .then() ブロックのいずれかが失敗した場合に実行されます。同期 try...catch ブロックと同様の方法で、エラーオブジェクトが catch() の中で利用可能になり、発生したエラーの種類を報告するために使用することができます。しかし、同期try...catchはプロミスでは動作しませんが、後で学ぶようにasync/awaitでは動作することに注意してください。 イベントキュー プロミスのような非同期処理はイベントキューに入れられ、メインスレッドが処理を終えた後に実行されるので、後続のJavaScriptコードの実行をブロックすることがありません。キューに入れられた処理は、できるだけ早く完了し、その結果を JavaScript 環境に返します。 プロミス vs コールバック プロミスは旧来のコールバックと類似している部分があります。プロミスは基本的にreturnされたオブジェクトにコールバック関数に渡すもので、関数の中にコールバックを渡す必要はありません。 しかし、プロミスは非同期処理を行うために特化して作られており、旧来のコールバックと比較して多くの利点があります。 複数の.then()を使って複数の非同期処理を連鎖させ、一つの結果を次の処理に入力として渡すことができる。これはコールバックでは非常に難しく、しばしば厄介な「破滅のピラミッド」(コールバック地獄とも呼ばれる)になってしまう。 Promiseコールバックは、常にイベントキューに入れられた厳密な順番で呼び出されます。 エラー処理は、「ピラミッド」の各レベルで個別に処理されるのではなく、ブロックの最後にある単一の .catch() ブロックですべてのエラーが処理されます。 プロミスは、サードパーティライブラリにコールバックを渡すと関数がどのように実行されるかの完全な制御を失う古いスタイルのコールバックとは異なり、制御の逆転を回避することができます。 非同期コードの性質 非同期コードの性質をさらに説明する例として、コードの実行順序を十分に意識しない場合に起こりうること、非同期コードを同期コードのように扱おうとした場合の問題点などを探ってみましょう。次の例は、以前に見たものとかなり似ています。ひとつ違うのは、console.log()ステートメントをいくつも使って、コードの実行順序を説明していることです。 console.log ('Starting'); let image; fetch('coffee.jpg').then((response) => { console.log('It worked :)') return response.blob(); }).then((myBlob) => { let objectURL = URL.createObjectURL(myBlob); image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }).catch((error) => { console.log('There has been a problem with your fetch operation: ' + error.message); }); console.log ('All done!'); ブラウザはコードの実行を開始し、最初のconsole.log()ステートメント(Starting)を見て実行し、その後、image変数を作成することになります。 その後、次の行に移動して fetch() ブロックの実行が始まりますが、fetch() はブロックせずに非同期で実行されるため、プロミス関連のコードの後もコードの実行が続き、最後の console.log() 文 (All done!) に到達してコンソールに出力されることになります。 fetch() ブロックの実行が完全に終了し、その結果を .then() ブロックで配信して初めて、ようやく 2 つ目の console.log() メッセージ (It worked) が表示されるのです。このように、メッセージは期待とは異なる順序で表示されています。 もし、これで混乱したのなら、次のような小さな例を考えてみてください。 console.log("registering click handler"); button.addEventListener('click', () => { console.log("get click"); }); console.log("all done"); この動作が先程のコードと非常に似ています。1つ目と3つ目のconsole.log()メッセージはすぐに表示されますが、2つ目のメッセージは誰かがマウスボタンをクリックするまで実行されません。前の例も同じように動作しますが、この場合、2つ目のメッセージはクリックではなく、リソースを取得して画面に表示するプロミスチェーンでブロックされます。 この些細なコードの設定例は問題を引き起こす可能性があります。つまり、同期処理にその結果を依存されている非同期コードブロックを設定することはできないのです。ブラウザが同期ブロックを処理する前に非同期関数が返されることを保証できないからです。 結論 JavaScriptは最も基本的な形式として、同期、ブロッキング、シングルスレッド言語であり、一度に一つの処理しか行うことができません。しかし、Webブラウザは関数やAPIを定義しており、同期的に実行されないで、何らかのイベント(時間の経過、ユーザーのマウス操作、ネットワーク上のデータの到着など)が発生したときに非同期に呼び出すべき関数を登録することができる。つまり、メインスレッドを停止したりブロックしたりすることなく、コードに複数の処理を同時に行わせることができるのです。 コードを同期的に走らせるか、非同期的に走らせるかは、何をしようとしているかによって変わってきます。 物事をロードしてすぐに実行させたい場合があります。例えば、ユーザー定義のスタイルをウェブページに適用する場合、できるだけ早くスタイルを適用したいでしょう。 しかし、データベースに問い合わせを行い、その結果をテンプレートに反映させるような時間のかかる処理を行う場合は、スタックから外して非同期にタスクを完了させた方がよいでしょう。時間をかければ、どのような場合に同期ではなく非同期の手法を選択するのがより理にかなっているかがわかるようになります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue-infinite-loading で no-resultsなどの設定を共通化する方法

<div slot="spinner">読込中</div> <div slot="no-more">全件取得しました</div> <div slot="no-results">データがありません</div> <div slot="spinner"> </div> これ毎回コンポーネントにかくのだるくね? だるいだるいだるい!!!!! やったこと 公式ドキュメントを読んだ。 https://peachscript.github.io/vue-infinite-loading/ 英語と中国語 ? 解決策 import Vue from 'vue' import InfiniteLoading from 'vue-infinite-loading' Vue.use(InfiniteLoading, { slots: { spinner: "データないです", noMore: 'もうないです。', noResults: "データがない", }, }); おそらくInfinitive loadを入れる際にプラグイン?として任意のフォルダにjsファイルを入れているかと思います。(忘れている人は思いだして。) そこに共通の設定が書けるようになっていました。なんて便利なんだ! 注意点として変数?として"-"が使えないようなので?キャメルケースでの記述に変換するようです。 no-more -> noMore no-results -> noResults <div slot="no-more">全件取得しました</div> ↓こんな感じかな。 noMore: '全件取得しました。', 結論 開発がんばろう。!!!!!!!!!!!!!!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む