- 投稿日:2020-09-19T23:21:10+09:00
vue-cliで手っ取り早くVue 3 + TypeScriptのプロジェクトを作ろう!
はじめに
Vue 3がとうとう来ましたね。試しましょう。
https://github.com/vuejs/vue-next/releases/tag/v3.0.0この記事の概要
タイトルどおり、Vue 3 + TypeScriptはどんな感じなのかを試すためのプロジェクトを作成します。
この記事に記載されている内容は下記の通りです。
- 環境を作る手順。
- 「class-style使います?」という質問に対しては、「No」と答えましょう。
- 手順で聞かれるclass-styleって何?
vue-cliをグローバルにインストール
npm i -g @vue/cli # インストール後にバージョンを確認 vue --version # @vue/cli 4.5.6プロジェクトを作る
コマンドを打つ
プロジェクトを作りたい場所でコマンドを打ちます。
my-vue-3
は任意のプロジェクト名です。ご自由に決めてください。vue create my-vue-3対話側のCLIでどんなプロジェクトにするか決める
対話型のUIが出てくるので、
Manually select features(手動で構成を選ぶ)
を選択しましょう。Vue CLI v4.5.6 ? Please pick a preset: Default ([Vue 2] babel, eslint) Default (Vue 3 Preview) ([Vue 3] babel, eslint) ❯ Manually select featuresTypeScriptが試したいので、spaceキーで
TypeScript
にチェックを入れましょう。
Choose Vue version
は最初からチェックが入っていると思うので、外さないようにしましょう。
他の項目は自由に決めて頂いて大丈夫です。決まったらEnterです。Vue CLI v4.5.6 ? Please pick a preset: Manually select features ? Check the features needed for your project: ◉ Choose Vue version ◉ Babel ❯◉ TypeScript ◯ Progressive Web App (PWA) Support ◯ Router ◯ Vuex ◉ CSS Pre-processors ◉ Linter / Formatter ◯ Unit Testing ◯ E2E TestingVueのバージョンを聞かれます。もちろんVueは
3.x (Preview)
を選びましょう。Vue CLI v4.5.6 ? Please pick a preset: Manually select features ? Check the features needed for your project: Choose Vue version, Babel, TS, CSS Pre-processors, Linter ? Choose a version of Vue.js that you want to start the project with 2.x ❯ 3.x (Preview)次の質問が出てきます。
Use class-style component syntax?
(class-styleを使いますか?)
一応デフォルトだとNoにはなっているのですが、質問に対して「よくわからんからヨシ!」とyを脳死で打ってしまう人もいると思います。しかしこの質問は結構大事なポイントです。? Use class-style component syntax? (y/N)この質問に回答するにはある程度の前提知識が必要です。
Vue + TypeScriptでは2つの書き方が存在します。それは、
- Class style
- Object style
の2つです。
Class style
対話で質問されているclass-style componentは、Vue 2の時に一般的だったVue + TypeScriptの書き方です。Vue 2の頃にTypeScriptを書いたことのある人ならばわかると思いますが、
vue-class-component
やvue-property-decorator
からimportしてあれこれしましたよね。その書き方です。
もし、class-style componentを選択したら、App.vueのscriptは下記のように生成されます。vue-class-component
からimportしているのが分かると思います。<script lang="ts"> import { Options, Vue } from 'vue-class-component'; import HelloWorld from './components/HelloWorld.vue'; @Options({ components: { HelloWorld, }, }) export default class App extends Vue {} </script>この例だと分かりにくいですが、TS無しのVueと比較して、class-styleだと書き方がかなり変わります。
様々な情報サイトでVueの例として紹介されるコードは基本的にバニラのVueなので、そのコードをTSにしたい時にclass-styleへと脳内変換をする面倒臭さがありました。Object style
一方で、Vue 3ではclass-styleではなく、Composition APIというものを使った新しい書き方がスタンダードになるはずです。
"Object styleという新しいスタイルが生まれる"というよりは、これまで通りの生JSの時の普通のVueにより近い書き方をしながら、強力な型推論の恩恵を受けることができるようになる、という感じのようです。class-styleを選択しない場合だと生成されるApp.vueは下記のようになります。この
defineComponent
というメソッドが型推論を手助けしてくれます。<script lang="ts"> import { defineComponent } from 'vue'; import HelloWorld from './components/HelloWorld.vue'; export default defineComponent({ name: 'App', components: { HelloWorld } }); </script>以上のことを踏まえて、
Use class-style component syntax?
という質問には、Noと答えましょう。
別にclass styleと回答してしまっても自分でobject styleに書き直すことはできるので、そんなに神経質になる必要もないのかもしれませんが、質問の内容自体は理解しておくのが良いと思います。対話で聞かれる残りの質問は、(この記事とは無関係なので)お好きなように回答しましょう。
参考
https://v3.vuejs.org/guide/typescript-support.html
https://speakerdeck.com/sunecosuri/migrated-class-style-component-for-vuejs-and-typescrpit
https://github.com/vuejs/rfcs/pull/17#issuecomment-494242121
- 投稿日:2020-09-19T22:12:19+09:00
Node.jsで画像ファイルのExif情報を削除する
TL;DR
gmパッケージを使いNode.jsで画像ファイルのExif情報を削除する方法を紹介します。
サンプルとして2パターンの使い方を示します。
- ローカルの画像ファイルを処理する
- ブラウザからアップロードした画像をNest.js(Typescript)で受け取り処理する
セットアップ
gmパッケージを使います。他、GraphicsMagickとImageMagickにも依存するので別途インストールしておきます。
検証環境
macOS v.10.15.4
$ node -v v12.16.1 $ npm -v 6.13.4 $ brew info graphicsmagick graphicsmagick: stable 1.3.35 (bottled), HEAD $ brew info imagemagick imagemagick: stable 7.0.10-27 (bottled), HEADサンプルコード
a. ローカルの画像ファイルを処理する
基本的な使い方です。
const gm = require('gm'); gm.subClass({ imageMagick: true }); gm(__dirname + '/image.jpg') .autoOrient() // exifから回転方向を設定しておく .noProfile() // exifを削除 .write(__dirname + '/image_after.jpg', async err => { if (err) { console.error(err); return; } });b. ブラウザからアップロードした画像をNest.jsで受け取る(Typescript)
Nest.jsのセットアップなどは割愛します。
画像ファイルのアップロード
- 選択したファイルをFormDataに詰めてfetchで送信します
- "my-images"の部分は任意で、サーバーサイドでも同じ文字列を使用します
<label> 画像を選択 <input id="input-files" type="file" multiple /> </label> <input id="input-upload" type="button" value="アップロード" /> <script> const inputFiles = document.querySelector('#input-files'); document .querySelector('#input-upload') .addEventListener('click', event => { formData = new FormData(); for (let i = 0; i < inputFiles.files.length; i++) { formData.append('my-images', inputFiles.files[i]); } fetch(`/upload`, { method: 'POST', body: formData, }); }); </script>サーバーサイド
import { Controller, Get, Post, UploadedFiles, UseInterceptors, } from '@nestjs/common'; import { FilesInterceptor } from '@nestjs/platform-express'; import { AppService } from './app.service'; import * as gm from 'gm'; gm.subClass({ imageMagick: true }); @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Post('/upload') @UseInterceptors(FilesInterceptor('my-images')) async upload(@UploadedFiles() files: any[]) { for (let i = 0; i < files.length; i++) { const filePath = __dirname + '/' + files[i].originalname; gm(files[i].buffer) .autoOrient() .noProfile() .write(filePath, err => { if (err) { console.error(err); return; } }); } } }
- 投稿日:2020-09-19T15:40:36+09:00
JavaScript: 引数が多いときはオブジェクトリテラルで渡すといい
引数が多い関数、たとえば
const introduce = (name, age, from, job) => { console.log(`${name}さんは${age}歳、${from}出身の${job}です。`) } introduce('山田太郎', 20, '宮城県', 'イラストレーター'); //山田太郎さんは20歳、宮城県出身のイラストレーターです。下のように書くといい。
const introduce = ({ name, age, from, job }) => { console.log(`${name}さんは${age}歳、${from}出身の${job}です。`) } introduce({ name: '山田太郎', age: 20, from: '宮城県', job: 'イラストレーター', }); //山田太郎さんは20歳、宮城県出身のイラストレーターです。引数の順番を変えても問題く関数を実行できる。
introduce({ age: 20, from: '宮城県', job: 'イラストレーター', name: '山田太郎', });
- 投稿日:2020-09-19T15:00:46+09:00
友達作りに敬語なんて必要なし!敬語禁止Discord Bot「Breako」リリース
敬語を判定する簡単なBotを作成しました!
概要
こんな感じで簡単な敬語を感知して返信するBotです。
「河童様」は反応、様は付くけど無礼な言葉とされる「貴様」には無反応なことを例に
誤検知を防ぐよう設計しています。
コマンド「@Breako」を入力すると設定されたNGワードを確認できます。作った理由
Twitterのつぶやきは基本ため口ですが、そこまで親しくない人からリプライが飛んできた場合は敬語になりませんか?
親しくなりたいけどついよそよそしくなってしまいがちです。
なのでDiscord内でサーバー側から敬語を禁止したらたくさん友達ができるんじゃないかという魂胆で作成しました。密かに人見知りや繊細な人にも枷を外してインターネットを楽しめる世の中にしたいという野望があります。
参考にした記事
使用した技術
- Glitch
- discord.js
v11.6.412.3.1- Google Apps Script
コード
基本、「誰でも作れる!Discord Bot(基礎編)」の記事の順序に従って作成しました。
server.jsconst ng = require('./ng.json') const ok = require('./ok.json')ng.json[ "です", "ます", "ました", "でした", ...ok.json[ "かます", "ますます", "さます", "覚ます", "冷ます", ...管理しやすいようNGワード、除外ワードを配列でjsonファイルに保存して呼び出しています。
server.js// 禁止ワード設定 var ng_reg = ng.map(v => { return new RegExp(v, "g"); }); var ok_reg = ok.map(v => { return new RegExp(v, "g"); }); var ok_under = ok.map(v => { for (let i = 0; i < ng_reg.length; i++) { v = v.replace(ng_reg[i], " __" + ng[i] + "__ "); } return v; }); var ok_under_reg = ok_under.map(v => { return new RegExp(v, "g"); }); const ng_list = ng.join(", ");NG、除外ワードの配列を正規表現化しています。
ok_underは除外ワードをDiscord用に下線で装飾するようreplaceで置換されたワードを上書きするために下線で置き換えしています。
(例: 覚ます → 覚 _ます_)server.js// 禁止ワード判定 // 禁止ワードがあればカウントプラス、OKワードがあればマイナス var match_count = 0; ng_reg.forEach((v) => { match_count = match_count + ( message.content.match( v ) || [] ).length; }); ok.forEach((v) => { match_count = match_count - ( message.content.match( v ) || [] ).length; }); if (match_count >= 1) { let text = message.content; for (let i = 0; i < ng_reg.length; i++) { text = text.replace(ng_reg[i], " __" + ng[i] + "__ "); } for (let i = 0; i < ok_under_reg.length; i++) { text = text.replace(ok_under_reg[i], ok[i]); } let Breako = "敬語が含まれているよ!: " + text; sendReply(message, Breako); return; }NGワード数をmatchでカウントして、除外ワードでマイナスしています。1以上ならば敬語が含まれていると感知して返信をする仕組みです。
「天狗様、目を覚ます。」であれば
「様」、「ます」で+2、「覚ます」で-1で合計1になり敬語が含まれている判定になっています。そして前半の
.replace(ng_reg, ' __' + ng + '__ ')
で「天狗_様_ 、目を覚 _ます_ 。」となり
後半の.replace(ok_under_reg,ok)
で「覚_ます_」を「覚ます」置き換えて「天狗_様_ 、目を覚ます 。」となります。server.jsconst ng_list = ng.join(', ') client.on('message', message =>{ if (message.author.id == client.user.id || message.author.bot){ return; } if(message.isMemberMentioned(client.user)){ sendReply(message, "禁止された敬語: " + ng_list); return; }@Breakoの部分です。NGワードの配列を文字列化して返信します。
振り返り
replaceを複数条件で置き換えするためにメソッドチェーンを利用したんですがかなり長いコードになってしまいました。
他によい方法が見つからずこのような形になっています。メンテナンスもしづらいのでよい方法があったら是非教えていただきたいです。
コメントのご指摘通り修正させていただきました!ありがとうございます!さいごに
NGワード、除外ワードについてはかなり杜撰なのでご意見あればTwitterなどにお願いします!
- 投稿日:2020-09-19T13:59:44+09:00
ついにFirestoreに != クエリが来たので検証してみた
JSのClient SDK v7.21.0で、ついにFirestoreに
!=
クエリが来ました??? 他のSDKにも来るのが楽しみですね!他にもnot-in
クエリが来ています!v7.21.0 of @Firebase JavaScript client for Web / Node.js is available. Release notes: https://t.co/VyOe9cYsYA
— Firebase Release (@FirebaseRelease) September 18, 2020ひとまず
!=
がどんな動きになるのか気になったので早速試してみました。import * as firebase from 'firebase' import 'firebase/firestore' firebase.initializeApp({ // your config }) // 事前にこのデータをFirestoreのusers配下に作成しておく const list = [ { name: '1', status: 'a' }, { name: '2', status: 'b' }, { name: '3', status: 'c' }, { name: '4', status: null }, { name: '5', }, ] const db = firebase.firestore() const usersRef = db.collection('users') const result1 = await usersRef.where('status', '!=', 'a').get() // statusに値が設定されている(nullを除く)ドキュメントの中で、status != aのものが取得される // result1 = [ // { // "name": "2", // "status": "b" // }, // { // "name": "3", // "status": "c" // } // ] const result2 = await usersRef.where('status', '!=', null).get() // statusに値が設定されているドキュメントが取得される // result2 = [ // { // "name": "1", // "status": "a" // }, // { // "name": "2", // "status": "b" // }, // { // "name": "3", // "status": "c" // } // ] const result3 = await usersRef.where('status', '!=', undefined).get() // thrown FirebaseError: Function Query.where() requires a valid third argument, but it was undefined.whereで指定したフィールドがnullやそもそも存在しないドキュメントの取り扱いは、もしかしたら想定とはズレるかもしれません。特に元々フィールドが存在しない場合(この例で言う
name: 5
のもの)は、当然ながらwhereでは取ってくることができないようです。ReleaseNotesに書いてあるとおりでしたね。!= finds documents where a specified field's value does not equal the specified value. Neither query operator will match documents where the specified field is not present.
よかったらTwitterも見ていってください〜FirebaseやFlutterをはじめとして、サービス開発全般のことをつぶやいてます?
ついにFirestoreに != クエリが来たので検証してみた https://t.co/GcpOR2Gz9P #Qiita
— moga? (@_mogaming) September 19, 2020
よかったらLGTMしてくれよな!
- 投稿日:2020-09-19T13:32:18+09:00
備忘録 Javascript 文字列のカウント(サロゲートペア)
知らなかったら、嵌ると思って備忘録として残しておきます。
先に結論としてコードを載せておきます。
var msg = "こんにちは"; console.log(msg.length); //5 var msg2 = "?"; console.log(msg2.length); //2単純に変数msg msg2の文字列を数えていますが、msgの「こんにちは」は5文字としっかりカウントできてます。
しかし、msg2の「?」(ほっけと読みます)は1文字にも関わらず2文字としてカウントされてしまってます。これは、?という字がサロゲートペアとして扱われているからだそうです!
サロゲートペア
Unicode(UTF-8)は1文字を2バイトで表現していますが、Unicodeで扱う文字列が増えて65535文字では対応できないという事になったので、一部の文字を4バイトで表現する事で文字数を拡張したそうです。
lengthプロパティはサロゲートペアである文字列を識別できないので、4バイトの文字=2文字として文字数をカウントしていたので、msg2の結果が2となっています。
対策
var msg2 = "?"; var num = msg2.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g).length -1; console.log(num); // 1 console.log(msg2.length - num); //1参考
https://qiita.com/wingsys/items/81d46451d8b93ab065de
https://qiita.com/sounisi5011/items/aa2d747322aad4850fe7
https://jsprimer.net/basic/string-unicode/
- 投稿日:2020-09-19T10:49:09+09:00
d3.jsでobjectの上に格子状の枠を作成する
目的
完成イメージのように、ステータスを表示するダッシュボード画面上に四角いオブジェクトがあり、格子状の枠を作って色を塗りつぶしたり、テキストを入れるのがゴールです。使っているのはJavaScriptのライブラリd3.jsです。
ポイントは2つ。
- foreignObject1要素で、オブジェクト内部にtableを追加し、格子状の枠を作る。
- clipPath2要素で、角を丸くする。初めは座標位置を指定してpathやareaで力技で描画することを考えたのですが、テキストのセンタリングやオブジェクトの拡大・縮小で破綻するので止めて、Webで色々調べつつまとめてみました。
本記事は、d3.jsでコードを書いたことがある人を前提に書いています。
完成イメージ
完成版HTML
最初に完成形を貼っておきます。
grid.html<html> <head> <script src="https://d3js.org/d3.v6.min.js"></script> <style> /* おまじない */ table { border-collapse: collapse; border-spacing: 0; } </style> </head> <body> <div id="svg-area"></div> <script> 'use strict'; let data = [ {t:'RED',c:'#ff0000'}, {t:'GREEN',c:'#00ff00'}, {t:'BLUE',c:'#0000ff'} ]; let width = 1000; let height = 500; let gridSize = 300; let gap = 8; let svg = d3.select("#svg-area").append("svg") .attr("width",width) .attr("height",height); /* 角Rをつけた四角い図形でくり抜く */ svg.append("clipPath") .attr("id","clip") .selectAll("rect") .data(data) .enter() .append("rect") .attr("width", gridSize-gap) .attr("height", gridSize) .attr("x", (d,i) => i*gridSize) .attr("y", 0) .attr("rx", 16) .attr("ry", 16); let g = svg.append("g") .attr("width",width) .attr("height",height); let cards = g.selectAll("rect").data(data); /* ベースとなる四角 */ cards.enter().append("rect") .attr("id",(d,i) => "card-"+i) .attr("x", (d,i) => i*gridSize) .attr("y", 0) .attr("width", gridSize-gap) .attr("height", gridSize) .attr("clip-path", "url(#clip)") .style("fill", "#000000"); /* tableタグのためのforeignObject要素 */ let table = cards.enter().append("foreignObject") .attr("x", (d,i) => i*gridSize) .attr("y", 0) .attr("width", gridSize-gap) .attr("height",gridSize) .attr("clip-path", "url(#clip)") .append("xhtml:table") .attr("width", gridSize-gap) .attr("height",gridSize) .attr("border",1) .attr("frame","void") .attr("bordercolor","#ffffff"); /* 通常のtableタグと同様に表の内部を作る */ let tr1 = table.append("tr"); let tr2 = table.append("tr"); let tr3 = table.append("tr"); tr1.append("td") .attr("height","33%") .attr("width","50%") .attr("bgcolor", (d) => d.c); tr1.append("td") .attr("height","33%") .attr("width","50%") .style("text-align", "center") .append("font") .attr("color", "#ffffff") .text((d) => d.t); tr2.append("td") .attr("height","33%"); tr2.append("td") .attr("height","33%"); tr3.append("td") .attr("colspan",2); </script> </body> </html>ヘッダ
細かく見ていきます。
ヘッダはシンプルにd3.jsライブラリを読み込むだけです。なお、styleタグの記述がないと枠にスペースが出来てしまいます。3
<html> <head> <script src="https://d3js.org/d3.v6.min.js"></script> <style> /* おまじない */ table { border-collapse: collapse; border-spacing: 0; } </style> </head>ボディ
こちらもシンプルで、div要素のみ。
<body> <div id="svg-area"></div> <script> ... </script> </body>オブジェクトを作る
次に、オブジェクトを実際に作っていきます。まずはベースとなる四角いオブジェクトを作成します。
'use strict'; let data = [ {t:'RED',c:'#ff0000'}, {t:'GREEN',c:'#00ff00'}, {t:'BLUE',c:'#0000ff'} ]; let width = 1000; let height = 500; let gridSize = 300; let gap = 8; let svg = d3.select("#svg-area").append("svg") .attr("width",width) .attr("height",height); let g = svg.append("g") .attr("width",width) .attr("height",height); let cards = g.selectAll("rect").data(data); /* ベースとなる四角 */ cards.enter().append("rect") .attr("id",(d,i) => "card-"+i) .attr("x", (d,i) => i*gridSize) .attr("y", 0) .attr("width", gridSize-gap) .attr("height", gridSize) // .attr("clip-path", "url(#clip)") .style("fill", "#000000");格子状の枠を作る
内部にtableタグを作成することで枠を作ります。foreignObjectを使ってそれを重ねるイメージです。分かりやすいように以下のような2x2の表を作るとします。
<table> <tr> <td></td> <td></td> </tr> <tr> <td></td> <td></td> </tr> </table>以下のようなコードになります。
/* tableタグのためのforeignObject要素 */ let table = cards.enter().append("foreignObject") .attr("x", (d,i) => i*gridSize) .attr("y", 0) .attr("width", gridSize-gap) .attr("height",gridSize) // .attr("clip-path", "url(#clip)") .append("xhtml:table") .attr("width", gridSize-gap) .attr("height",gridSize) .attr("border",1) .attr("frame","void") .attr("bordercolor","#ffffff"); /* 完成版HTMLと違います */ let tr1 = table.append("tr"); let tr2 = table.append("tr"); tr1.append("td") .attr("height","50%") .attr("width","50%"); tr1.append("td") .attr("height","50%") .attr("width","50%"); tr2.append("td") .attr("height","50%") .attr("width","50%"); tr2.append("td") .attr("height","50%") .attr("width","50%");するとこんな感じで格子状の枠が作れました。完成イメージにするには、3段にする、背景色を指定する、カラムを結合する、テキストを加えるなど、通常のtableと同じような作業をやります。
角を丸くする
最後に角を丸くするには、clipPathを使います。描画部分を窓枠のようにマスクするイメージで、Web検索すれば色々出てきます。
以下のコードを追加し、上記解説のclip-path属性のコメントアウトを外します。
/* 角Rをつけた四角い図形でくり抜く */ svg.append("clipPath") .attr("id","clip") .selectAll("rect") .data(data) .enter() .append("rect") .attr("width", gridSize-gap) .attr("height", gridSize) .attr("x", (d,i) => i*gridSize) .attr("y", 0) .attr("rx", 16) .attr("ry", 16);まとめ
いかがでしたでしょうか?完成イメージのような図形でも、自由度の高いd3.jsライブラリを使えば割と簡単に描くことが出来るのがいいところです。
<foreignObject> - SVG: Scalable Vector Graphics | MDN (https://developer.mozilla.org/ja/docs/Web/SVG/Element/foreignObject) ↩
<clipPath> - SVG: Scalable Vector Graphics | MDN (https://developer.mozilla.org/ja/docs/Web/SVG/Element/clipPath) ↩
テーブルのセルの隙間をリセットするCSS (https://qiita.com/macer_fkm/items/bac56f0f863a19cfd674) ↩
- 投稿日:2020-09-19T10:42:41+09:00
【JavaScript】【DOM操作】appnedChild( )でHTML要素を追加
書き方
[親要素].appendChild(追加要素);サンプルコード
以下のようなリストがあるとします。
example.html<ul id="menuList"> <li>coffee</li> <li>tea</li> </ul>↑に
<li>greenTea</li>
を以下のコードで追加します。example.js// 親要素 var menuList = document.getElementById('menuList'); // 追加する要素を作成 var li = document.createElement('li'); li.innerHTML = 'greenTea'; // 末尾に追加 menuList.appendChild(li);■結果
親要素の最後の子要素として追加されます。example.html<ul id="menuList"> <li>coffee</li> <li>tea</li> <li>greenTea</li> </ul>参照
- 投稿日:2020-09-19T10:42:41+09:00
【JavaScript】【DOM操作】appendChild( )でHTML要素を追加
書き方
[親要素].appendChild(追加要素);サンプルコード
以下のようなリストがあるとします。
example.html<ul id="menuList"> <li>coffee</li> <li>tea</li> </ul>↑に
<li>greenTea</li>
を以下のコードで追加します。example.js// 親要素 var menuList = document.getElementById('menuList'); // 追加する要素を作成 var li = document.createElement('li'); li.innerHTML = 'greenTea'; // 末尾に追加 menuList.appendChild(li);■結果
親要素の最後の子要素として追加されます。example.html<ul id="menuList"> <li>coffee</li> <li>tea</li> <li>greenTea</li> </ul>参照
- 投稿日:2020-09-19T09:46:10+09:00
【悲報】React.useCallback() を使いこなせない
初めに
みなさん React.useCallback() を使いこなせていますでしょうか。もちろん、僕は使いこなせていません(泣
この記事は、皆さんの助言をいただきながら、みんなで React.useCallback() を、ひいてはその親兄弟親戚一同である React.useMemo() や React.memo() も使いこなせるようになろうではないか、というものです。
といいながらも、だらだらと拙文をお読みいただくのもあれなので、最初に現時点での個人的な結論から書きたいと思います。結論 (効果音)
メモ化の機能は、値をキャッシュとして保持することにより、処理を高速化したいときに用いる。
ただし、値をキャッシュから読み出すべきか、そうではなく新ためて元のリソースから値を読み出すべきか、という判断がきちんとできないのなら、使用するべきではない。そうであるとすると、各プログラマ単位では、メモ化の機能を使用しなけらばならないケースはそう多くはないのではないか。
具体例としては、「値をファイルから読み込む処理があるが、毎回ファイルから読み込むと処理時間がかかるので、ファイルが更新されていなければ値をキャッシュから読み込む」というようなものが考えられる。よくある処理なので問題はないであろうが、もし「ファイルが更新されているかどうか」というチェックの処理時間が、ファイルから値を読み込む処理時間よりも長いなら、キャッシュ処理はしない方がよいことになる。
この例が示すように、キャッシュ処理は安易に導入すべきものではなく、慎重に設計や実験をしたうえで導入すべきものなのである。
また、React.useCallback() については、関数内関数が毎回作成されることによる弊害を防止するために使えるが、その弊害が「本当に弊害である」ということをきちんと理解したうえで使用するべきであり、「処理が高速化されるかもしれないから」などという安易な理解のみで使用するべきではない。React.useCallback() による処理の高速化とは
「関数を定義する」、「関数を実行する」
いきなりですが次のソースを見てください。
function sample() { // 一連の処理 }一般的には「関数を定義した」といわれるものです。「関数を定義した」というだけではいかにも抽象的ですので、ここでその意味をきちんと理解してしまいましょう。
次のソースを見てください。function sample() { // 一連の処理 }()こうすると、「関数を定義し、実行した」とうことになります。
()
を付けただけでこうなります。次のように書くのと同じです。function sample() { // 一連の処理 } sample()「関数を定義する」と「関数を実行する」とは、それぞれ「一連の処理を定義する」、「一連の処理を実行する」である、という概念がきっちり区別できたら次に進みましょう。できるようになるまではけして進んではいけません。
どうしても理解できない方は、一人で悩まずに、お友達のエンジニアと一緒に議論しましょう。アロー関数
いわゆるアロー関数を使った方が「関数の定義」の概念をつかみやすくなります。
const sample = () => { // 一連の処理 }「一連の処理を変数に代入する」とか「一連の処理に名前を付ける」とか、理解の仕方はいろいろ考えられるところですが、すでにみたように、けして「一連の処理を実行する」ものではない、ということは理解しておきましょう。「関数を実行」したいのなら次のようにすべきことなります。
const sample = () => { // 一連の処理 }()モジュールレベルでローカルな関数
ここまでの sample() 関数は、モジュールレベルでローカルな関数であることとを想定しています。しかしながら、モジュールという概念は曖昧なものであり、特に JavaScript では、単に名前空間の解決をするためだけにモジュールのロードタイミングを決定しているにすぎず、内部では HTML 内に
<Script>
タグがいくつかある状態になっているというだけの話です。
そこで、この記事では、モジュールという概念を使用せずに、すぐに実際にコードを実行できる HTML ファイル形式のソースをいちいち掲載します。sample1.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) const sample = () => { // 一連の処理 let result = "H" result += "E" result += "L" result += "L" result += "O" return result } const elm = html` <h1>TEST</h1> <p>${sample()}</p> <p>${sample()}</p> ` ReactDOM.render(elm, document.getElementById("App")) </script>React の記事なので React をロードしているのは当然として、いちいちトランスパイルしなくてもいいように htm というライブラリもロードしています。JSX 記法とほとんど同じことをストリングテンプレートで実現できる優れたライブラリです。
さて、上のソースでは、sample() がモジュールレベル(モジュールという概念は捨てましたが、グローバルレベルというのもなんかピンと来ないのでとりあえずそうしておきます)でローカルな関数であることが分かると思います。
ここで理解しておくべきことは、モジュールレベルでローカルな関数は、モジュールがロードされたときに定義される(モジュールという概念は捨てたのであれですがもう繰り返しません)、ということです。
次のソースがエラーになることは誰でも理解できると思います。sample2.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) const elm = html` <h1>TEST</h1> <p>${sample()}</p> <!-- エラー --> <p>${sample()}</p> ` const sample = () => { // 一連の処理 let result = "H" result += "E" result += "L" result += "L" result += "O" return result } ReactDOM.render(elm, document.getElementById("App")) </script>しかし、次のソースがエラーにならないことを理解できない方がたまにおられます。そういう方は、もう一度この記事の最初からやり直してください。理解できている方は次に進みましょう。
途中で挫折して欲しくありませんので、理解できているふりだけはしないでくださいね。理解できない方はお友達のエンジニアと(以下同文)sample3.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) const render = () => { const elm = html` <h1>TEST</h1> <p>${sample()}</p> <!-- OK! --> <p>${sample()}</p> ` ReactDOM.render(elm, document.getElementById("App")) } const sample = () => { // 一連の処理 let result = "H" result += "E" result += "L" result += "L" result += "O" return result } render() </script>なお、最近のほとんどのブラウザでは、dynamic import() 関数が使えますが、それを使用して同じモジュールを何度もロードしても、モジュールレベルの関数が定義されるのは、最初のロードのときだけです(のはずです。以前実験したことがありそれから変わってないと思いますが・・・助長になるのでここでは確認実験しません、すみません)。
関数内関数
では、sample() 関数を関数内に移動してみましょう。
ただ、その前に、そろそろ React を使い始めましょうか。sample4.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) const sample = () => { // 一連の処理 let result = "H" result += "E" result += "L" result += "L" result += "O" return result } const Wrapper = props => { return html`<p>${sample()}</p>` } const elm = html` <h1>TEST</h1> <${Wrapper} /> <${Wrapper} /> ` ReactDOM.render(elm, document.getElementById("App")) </script>React 使い始めただけで混乱しないでくださいね。
関数型コンポーネントは文字通りただの関数です。それでは、関数内に関数を移動してみましょう。sample5.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) const Wrapper = props => { const sample = () => { // 一連の処理 let result = "H" result += "E" result += "L" result += "L" result += "O" return result } return html`<p>${sample()}</p>` } const elm = html` <h1>TEST</h1> <${Wrapper} /> <${Wrapper} /> ` ReactDOM.render(elm, document.getElementById("App")) </script>さて、ここまで長かったですが、ここで議論したいのは、「このように関数を関数内に移動したということだけが、React.useCallback() を使う理由になりうるか」ということです。
確かに、関数内関数は関数が実行されるたびに定義されますので、モジュールレベルの関数に比べ処理が増えることは間違いありません。どのくらい増えるのか、有意なほど増えるかということは別にしても、モジュールレベルで定義できるならそうした方がよいのでしょう。関数の定義場所が変わることによる名前空間の混乱(ローカル変数の参照の仕方が変わってしまうことも含む)など、それはそれでデメリットもありますのでそれとの兼ね合いになるでしょうか。<Button onClick={e => {一連の処理}} />いまさら、このような書き方も全部だめですやめてください、いわれたら辛すぎますよね。
しかし、ここでの主題はそれではありません。真の主題は「React.useCallback() を使うことにより、モジュールレベル関数と同じほどのメリットを得られるのか」ということです。
ちなみに、React.useCallback() を使ったソースが次です。sample6.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) const Wrapper = props => { const sample = React.useCallback(() => { // 一連の処理 let result = "H" result += "E" result += "L" result += "L" result += "O" return result }, []) return html`<p>${sample()}</p>` } const elm = html` <h1>TEST</h1> <${Wrapper} /> <${Wrapper} /> ` ReactDOM.render(elm, document.getElementById("App")) </script>個人的には「美しくない」と思います。理由はソースを書いて1年くらいしたら何のために React.useCallback() を使用しているのか忘れそうだからです。結構そういう感覚って大事じゃありません?とはいっても、その点はここではまったく別の問題です。すみません。
さて、そもそも、関数が実行されるたびに関数を定義する、という処理って有意に気にすべきほどの時間を食う処理なのでしょうか。
これを解明するためには、V8 エンジンあたりの解析を行うべきなのでしょうが、やりません。面倒ですので想像だけします。想像できるストーリーとしては大きく分けて次の3つがあります。
- 関数が実行されるたびに関数定義がなされ、関数定義の具体的処理としての関数内関数の解析もそのたびに行われる。
- 関数が実行されるたびに関数定義がなされるが、関数定義の具体的処理としての関数内関数の解析は1度目の関数実行時にだけ行われ、2度目以降は、その関数定義に名前が付与される(あるいは実体化される)だけ。
- 関数内関数の解析もモジュールがロードされたときに1度だけ実行され、関数の実行時には、その関数定義に名前が付与される(あるいは実体化される)だけ。
もし 1 ならば React.useCallback() を使う意味がありそうです。一方、2 あるいは 3 ならば React.useCallback() を使う意味はなさそうです。「関数定義に名前が付与される(あるいは実体化される)」という処理だけなら気にするほどのコストはかからないはずだからです。それどころか、もし、2 や 3 のように、「関数の実行時には、その関数定義に名前が付与される(あるいは実体化される)だけ」であるとしたら、かえって React.useCallback() を使うことで処理が増加する可能性すらあります。
JavaScript が内部で実際にどのように処理を行うのかについては識者のコメントを待ちたいと思いますが、僕は 1 である可能性は低い気がしています。「一連の処理」自体が実行時に変化するわけではないからです。
ちなみに、Pascal という言語でも関数内関数が使え、Pascal 言語はコンパイル言語ですからロードという概念はないのですが、当然コンパイル時に関数定義の解析がなされますから 3 のタイプといえるでしょう。
とりあえず、今のところは、この議論については、これ以上深まりそうにもないので、「おそらくこの点に関しては React.useCallback() を使う理由にはならない」ということにして、次に進みたいと思います。React.useCallback() による高速化とは、関数内関数化により発生する余計なレンダリングを防ぐことである
こちらは議論ではなく、厳然たる事実ですのできちんと勉強して理解しましょう。理解するためのポイントは2つあり、「React.memo()」と「シャロー比較」です。早速それぞれを見てみましょう。
React.memo() とは
コンポネントが更新されると、その子コンポーネントも更新されます。次のソースを実行しボタンをクリックしてみてください。クリックするたびに、コンソール(ブラウザの F12 を押すと表示されるはず)にログが追加されるはずです。
sample7.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) const MrChin = props => { // 一連の処理 let result = props.m1 result += props.m2 result += props.m3 result += props.m4 result += props.m5 console.log("Updated!: " + Date()) return html` <p>${result}</p> ` } const Wrapper = props => { const redrawMe = React.useState()[1] return html` <p> <button onClick=${() => { redrawMe(new Date().getTime()) }}> Click! </button> <${MrChin} m1="H" m2="e" m3="l" m4="l" m5="o" /> </p> ` } const elm = html` <h1>Test</h1> <${Wrapper}/> ` ReactDOM.render(elm, document.getElementById("App")) </script>ここで、MrChin コンポーネント内の「一連の処理」が、10秒くらい時間がかかる処理であったとします。ユーザーは親コンポーネントが更新されるたびに MrChin が更新されて 10秒待たされることになります。
これを回避するための機能が React.memo() です。次のソースを実行すると、React.memo() により、いくらボタンをクリックしてもコンソールにログは出力されなくなります。
React.memo() は、props に変化がない限り、更新を防止します。sample8.html では props が変化しえないため、2度と MrChin が更新されることはありません。sample8.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) const MrChin = React.memo(props => { // 一連の処理 let result = props.m1 result += props.m2 result += props.m3 result += props.m4 result += props.m5 console.log("Updated!: " + Date()) return html` <p>${result}</p> ` }) const Wrapper = props => { const redrawMe = React.useState()[1] return html` <p> <button onClick=${() => { redrawMe(new Date().getTime()) }}> Click! </button> <${MrChin} m1="H" m2="e" m3="l" m4="l" m5="o" /> </p> ` } const elm = html` <h1>Test</h1> <${Wrapper}/> ` ReactDOM.render(elm, document.getElementById("App")) </script>シャロー比較とは
ここまで読み進んでこられた方であれば、これについては十分な知識を持っている方が多いと思われるため、軽めにしておきます。
シャロー比較は、JavaScript の値の比較の方法のひとつです。オブジェクト型などの変数を代入する場合の代入の仕方には、いわゆるシャローコピーとディープコピーとがありますが、変数の比較の際にも同じような問題が生じるのです。
あるいは、C 言語などでポインタの知識をお持ちの方であれば、ポインタ同士を比較するがシャロー比較で、ポインタが指しているその先のメモリの内容まで比較するのがディープ比較(というのでしょうか?)といえば理解しやすいかもしれません。
そして、React.memo() での props が変化したかどうかのチェックは、このシャロー比較で行われます。関数とシャロー比較
前述のように、モジュールレベルの関数は、ロード時に定義される(実体化される)だけのため、シャロー比較すると、毎回「同じ値だよ」と判断されることになります。
モジュールレベルの関数を使用したソースです。React.memo() がきちんと働いている(ログが追加されない)ことが分かるかと思います。sample9.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) const MrChin = React.memo(props => { // 一連の処理 let result = props.m1 result += props.m2 result += props.m3 result += props.m4 result += props.m5 console.log("Updated!: " + Date()) return html` <p>${result + " " + props.country()}</p> ` }) const Wrapper = props => { const redrawMe = React.useState()[1] return html` <p> <button onClick=${() => { redrawMe(new Date().getTime()) }}> Click! </button> <${MrChin} m1="H" m2="e" m3="l" m4="l" m5="o" country=${sample} /> </p> ` } const sample = () => { return "Japan" } const elm = html` <h1>Test</h1> <${Wrapper}/> ` ReactDOM.render(elm, document.getElementById("App")) </script>一方、関数内関数は、関数が実行されるたびに定義される(実体化される)ので、シャロー比較すると、毎回「違う値だよ」と判断されることになります。
関数内関数を使用したソースです。React.memo() が機能しません(ログが追加されてしまう)。sample10.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) const MrChin = React.memo(props => { // 一連の処理 let result = props.m1 result += props.m2 result += props.m3 result += props.m4 result += props.m5 console.log("Updated!: " + Date()) return html` <p>${result + " " + props.country()}</p> ` }) const Wrapper = props => { const redrawMe = React.useState()[1] const sample = () => { return "Japan" } return html` <p> <button onClick=${() => { redrawMe(new Date().getTime()) }}> Click! </button> <${MrChin} m1="H" m2="e" m3="l" m4="l" m5="o" country=${sample} /> </p> ` } const elm = html` <h1>Test</h1> <${Wrapper}/> ` ReactDOM.render(elm, document.getElementById("App")) </script>この関数内関数の対シャロー比較問題を解決するのが React.useCallback() です。次のソースです。見事に解決し、ログが追加されなくなりました。
sample11.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) const MrChin = React.memo(props => { // 一連の処理 let result = props.m1 result += props.m2 result += props.m3 result += props.m4 result += props.m5 console.log("Updated!: " + Date()) return html` <p>${result + " " + props.country()}</p> ` }) const Wrapper = props => { const redrawMe = React.useState()[1] const sample = React.useCallback(() => { return "Japan" }, []) return html` <p> <button onClick=${() => { redrawMe(new Date().getTime()) }}> Click! </button> <${MrChin} m1="H" m2="e" m3="l" m4="l" m5="o" country=${sample} /> </p> ` } const elm = html` <h1>Test</h1> <${Wrapper}/> ` ReactDOM.render(elm, document.getElementById("App")) </script>このように、React.useCallback() は、関数内関数がシャロー比較において「違う値だよ」と判断されてしまう問題を解決します。この点については異論がありません。
シャロー比較は、わりといろいろな場面で使われます。例えば、React.useState() の戻り値である、setState() の引数での同一性チェックや、React.useEffect の「依存リスト引数」での同一性チェックなどです。しっかりマスターしましょう。ここまでは理解した。でも、useCallback() の「依存リスト引数」ってどうよ
React.useCallback() の使いどころを理解できたとしても、「依存リスト引数」についてきちんと理解していなければ、使えるはずなどありません。「依存リスト引数」は React.useCallback の2番目の引数です。
React.useCallback(関数, 依存リスト)「依存している」って何なのか
多くの文献を読ませていただきましたが、多くは「依存している変数等を指定してあげる」くらいの記述しかありませんでした。
これだけでは、僕ごときの知能ではまるで理解ができません。
関数内部で使用している変数をとりあえず全部並べておけ的な文献もありましたのでとりあえず実験してみることにしました。
まずは、モジュールレベルの変数を利用した場合に、「依存リスト引数」として渡す必要があるのかを確認します。ソースです。sample12.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) let now = "?" const Clicker = props => { return html` <p> <button onClick=${() => { now = " " + Date() props.updater() }}> Click! </button> <p>${now}</p> </p> ` } const Wrapper1 = props => { const redrawMe = React.useState()[1] const updater = () => { redrawMe(new Date().getTime()) now = "今は " + now console.log(now) } return html` <${Clicker} updater=${updater} /> ` } const Wrapper2 = props => { const redrawMe = React.useState()[1] const updater = React.useCallback(() => { redrawMe(new Date().getTime()) now = "今は " + now console.log(now) }, []) return html` <${Clicker} updater=${updater} /> ` } const elm = html` <h1>Test</h1> <${Wrapper1}/> <${Wrapper2}/> ` ReactDOM.render(elm, document.getElementById("App")) </script>Wrapper1 では、関数内関数をそのまま使用し、Wrapper2 では React.useCallback() を使用しましたが、結果は同じでした。
すなわち、モジュールレベルの変数を内部で使用している場合には必ず「依存リスト引数」にそれを指定せよ、とまではいえないということになります。
次に、関数内関数の引数を利用した場合に、「依存リスト引数」として渡す必要があるのかを確認します。ソースです。sample13.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) let now = "?" const Clicker = props => { return html` <p> <button onClick=${() => { now = " " + Date() props.updater() }}> Click! </button> <p>${now}</p> </p> ` } const Wrapper1 = props => { const redrawMe = React.useState()[1] const updater = (n) => { redrawMe(new Date().getTime()) n = "今は " + n console.log(n) } return html` <${Clicker} updater=${updater} /> ` } const Wrapper2 = props => { const redrawMe = React.useState(now)[1] const updater = React.useCallback((n) => { redrawMe(new Date().getTime()) n = "今は " + n console.log(n) }, []) return html` <${Clicker} updater=${updater} /> ` } const elm = html` <h1>Test</h1> <${Wrapper1}/> <${Wrapper2}/> ` ReactDOM.render(elm, document.getElementById("App")) </script>やはり、Wrapper1 では、関数内関数をそのまま使用し、Wrapper2 では React.useCallback() を使用しましたが、結果は同じでした。
すなわち、引数を必ず「依存リスト引数」にも指定せよ、とまではいえないということになります。
では次に、関数内のローカル変数を利用した場合に、「依存リスト引数」として渡す必要があるのかを確認します。ソースです。sample14.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) let now = ["?"] const Clicker = props => { return html` <p> <button onClick=${() => { props.setter(" " + Date()) props.updater() }}> Click! </button> <p>${props.getter()}</p> </p> ` } const Wrapper1 = props => { const redrawMe = React.useState()[1] const localNow = now const getter = () => { return localNow[0] } const setter = v => { localNow[0] = v } const updater = () => { redrawMe(new Date().getTime()) localNow[0] = "今は " + localNow[0] console.log(localNow[0]) } return html` <${Clicker} getter=${getter} setter=${setter} updater=${updater} /> ` } const Wrapper2 = props => { const redrawMe = React.useState()[1] const localNow = now const getter = React.useCallback(() => { return localNow[0] }, []) const setter = React.useCallback(v => { localNow[0] = v }, []) const updater = React.useCallback(() => { redrawMe(new Date().getTime()) localNow[0] = "今は " + localNow[0] console.log(localNow[0]) }, []) return html` <${Clicker} getter=${getter} setter=${setter} updater=${updater} /> ` } const elm = html` <h1>Test</h1> <${Wrapper1}/> <${Wrapper2}/> ` ReactDOM.render(elm, document.getElementById("App")) </script>やはり、Wrapper1 では、関数内関数をそのまま使用し、Wrapper2 では React.useCallback() を使用しましたが、結果は同じでした。
すなわち、関数内のローカル変数を内部で使用している場合には必ず「依存リスト引数」にそれを指定せよ、とまではいえないということになります。
ここで、ニヤリとされた方はかなり熟練のプログラマーです。まだ気づけないルーキープログラマも、sample14.html と次の sample15.html とを比較すれば。薄々何かを感じ始めるかもしれません。sample15.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) const Clicker = props => { return html` <p> <button onClick=${() => { props.setter(" " + Date()) props.updater() }}> Click! </button> <p>${props.getter()}</p> </p> ` } const Wrapper1 = props => { let now = "?" const redrawMe = React.useState()[1] const getter = () => { return now } const setter = v => { now = v } const updater = () => { redrawMe(new Date().getTime()) now = "今は " + now console.log(now) } return html` <${Clicker} getter=${getter} setter=${setter} updater=${updater} /> ` } const Wrapper2 = props => { let now = "?" const redrawMe = React.useState()[1] const getter = React.useCallback(() => { return now }, []) const setter = React.useCallback(v => { now = v }, []) const updater = React.useCallback(() => { redrawMe(new Date().getTime()) now = "今は " + now console.log(now) }, []) return html` <${Clicker} getter=${getter} setter=${setter} updater=${updater} /> ` } const elm = html` <h1>Test</h1> <${Wrapper1}/> <${Wrapper2}/> ` ReactDOM.render(elm, document.getElementById("App")) </script>みなさん、ついてこれてますでしょうか。
Wrrapper1 は動作しません。関数内のローカル変数は、関数が実行されるたびに、すなわち、更新処理が発生するたびに初期化されますから、ずっと?
のままです。
なぜこのようなことになったのでしょうか。sample14.html と sample.15.html を比較しましょう。
そうです、sample14.html では、ローカル変数が、モジュールレベルの変数への参照なのでうまくいったのです。ここでもやはりシャローやディープといったものが関係してくるのです。
すなわち。ここでは、ローカル変数localNow
に代入されるモジュールレベルの変数now
は配列型です。配列型やオブジェクト型などの代入はシャローコピーされます。参照渡しともいいます。
C 言語をやっている方なら、配列型やオブジェクト型のようなメモリを多く消費する型の変数をポインタで処理するのと同じ、と考えれば理解できるはずです。
スクリプト言語ではポインタを使えない(ポインタの概念は捨てたほうが言語として素敵と考えている)ものが多いですが、シャローとか参照渡しとかいう概念が残るなら、その基礎にあるポインタの概念も知っておいた方が理解度アップには有利な気がします。
ちなみに、props 引数はオブジェクト型ですから参照渡しのようにも思われます。しかしながら、JSX 記法からは明らかですが、一度バラされますから、元の props との同一性がなく、うまくいきません。sample16.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) let now = {now: "?"} const Clicker = props => { return html` <p> <button onClick=${() => { props.now = " " + Date() props.updater() }}> Click! </button> <p>${props.now}</p> </p> ` } const Wrapper1 = props => { const redrawMe = React.useState()[1] const updater = () => { redrawMe(new Date().getTime()) props.now = "今は " + props.now console.log(props.now) } return html` <${Clicker} ...${props} updater=${updater} /> ` } const Wrapper2 = props => { const redrawMe = React.useState()[1] const updater = React.useCallback(() => { redrawMe(new Date().getTime()) props.now = "今は " + props.now console.log(props.now) }, []) return html` <${Clicker} ...${props} updater=${updater} /> ` } const elm = html` <h1>Test</h1> <${Wrapper1} ...${now} /> <${Wrapper2} ...${now} /> ` ReactDOM.render(elm, document.getElementById("App")) </script>そして、次のソースのように、JSX 記法を使わなくても、やはりうまくいきませんので、記法の問題ではなく、React 内部で props のディープコピー的な処理を行っているのだろうと思います。
sample17.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) let now = {now: "?"} const Clicker = props => { return React.createElement('p', {}, [ React.createElement('button', {'onClick': () => { props.now = " " + Date() props.updater() }}, [ 'Click!' ]), React.createElement('p', {}, [props.now]) ]) } const Wrapper1 = props => { const redrawMe = React.useState()[1] const updater = () => { redrawMe(new Date().getTime()) props.now = "今は " + props.now console.log(props.now) } props.updater = updater return React.createElement(Clicker, props) } const Wrapper2 = props => { const redrawMe = React.useState()[1] const updater = React.useCallback(() => { redrawMe(new Date().getTime()) props.now = "今は " + props.now console.log(props.now) }, []) props.updater = updater return React.createElement(Clicker, props) } const elm = React.createElement(React.Fragment, {}, [ React.createElement('h1', {}, ['Test']), React.createElement(Wrapper1, now), React.createElement(Wrapper2, now), ]) ReactDOM.render(elm, document.getElementById("App")) </script>なお、props のメンバをオブジェクト型にすれば、それはもちろん参照渡しになりますのでうまく動きます。
sample18.html<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) let now = {now: "?"} const Clicker = props => { return html` <p> <button onClick=${() => { props.now.now = " " + Date() props.updater() }}> Click! </button> <p>${props.now.now}</p> </p> ` } const Wrapper1 = props => { const redrawMe = React.useState()[1] const updater = () => { redrawMe(new Date().getTime()) props.now.now = "今は " + props.now.now console.log(props.now.now) } return html` <${Clicker} now=${props.now} updater=${updater} /> ` } const Wrapper2 = props => { const redrawMe = React.useState()[1] const updater = React.useCallback(() => { redrawMe(new Date().getTime()) props.now.now = "今は " + props.now.now console.log(props.now.now) }, []) return html` <${Clicker} now=${props.now} updater=${updater} /> ` } const elm = html` <h1>Test</h1> <${Wrapper1} now=${now} /> <${Wrapper2} now=${now} /> ` ReactDOM.render(elm, document.getElementById("App")) </script>sample18.html により、props を関数内関数の内部で使用している場合には必ず「依存リスト引数」にそれを指定せよ、とまではいえないことも明らかになりました。
ところで、驚くべき sample15.html の Wrapper2 へ少し話を戻しましょう。これ、きちんと動作しています!
あたかも 関数ローカルな変数localNow
が関数の外側に追い出されて、モジュールレベルの変数やクラスのメンバ変数になったかのようです。なぜこうなったのかについては自習をしてください。ここではこれ以上深追いしません。
深追いしない理由は、これが React.useCallback() の正しい使い方であるという自信を僕は今のところ持てないからです。識者の意見をお待ちしております。で、「依存リスト引数」はいつ使うの?
さて、ここまで、「依存リスト引数」を使う例は出てきていません。
ただ、少なくとも、関数内で使う変数のスコープのみと関係性があるものではないことは確認できました。
ここで、「いや、関数内のローカル変数の型と、それがシャローコピーかそうでないかにより、依存リストに加えるべきかどうかが決まるのではないか」という意見はあると思います。確かにその通りなんですが、それが分かったところで実戦で役に立つとは思えないのですよね。「面倒だから全部入れとけ」派の台頭を防げない気がします。
あらためて基本に立ち返ってみましょう。そもそも僕らは何のために React.useCallBack() を使おうとしているのでしたっけ?ここでは React.memo() を働かせるためですよね。つまり、props に「思いがけない変動」(関数内関数の弊害による変動)が生じないようにするためでした。
「依存リスト引数」は、「思いがけない変動」を「想定された変動」にしようというものですが、果たして設計として正しい方向性なのでしょうか?
「想定された変動」なら、props を操作するのが正当なやりかたなのではないでしょうか?
つまり、僕らは、「関数内関数で生じた弊害」に対応するために React.useCallback() を使い始めましたが、そうしたら今度は、「React.useCallback() による弊害」が現れたので、それに対応するために「依存リスト引数」を使おうとしていて、「props を操作するのが正当だと思うけど仕方ないよね」と無理やり納得しようとしているだけなのではないか、という疑念が浮かんでくるのです。React.useCallback() における「依存リスト引数」についてのまとめ
結局、React.useCallback() における「依存リスト引数」については、その使いどころがよくわからないままです。
個人的には、「React.useCallback() における「依存リスト引数」には、使いどころはなく、全部[]
でいい。[]
以外が必要となるようなアルゴリズム自体の方を見直せ。」という結論でよい気がしています。React.useMemo() における「依存リスト引数」
とはいっても、「依存リスト引数」は、React.useCallback() がその基本形である React.useMemo() から引き継いだものですから、React.useMemo() における「依存リスト引数」についても見ておきましょう。
※ React.memo() と React.useMemo()、全然別ものなのに名前が似ていて紛らわしいですねぇ。僕は、カンファレンスとかのビデオとかを見て React の開発陣を尊敬しているのですが、このネーミングセンスだけはいただけないですね。「メモって単語いけてない?」みたいなのりで付けただけの感じがいただけないです。将来的にはどちらかの関数名が変更されるんじゃないでしょうか。
メモ化
React.useMemo() は、「値」をメモ化するものです。
React.useMemo(「値」を返す関数, 依存リスト)もしかしたら、「メモ化」という用語を聞きなれない方がいらっしゃるかもしれませんが、「キャッシング処理」とか「キャッシュする」などと同じと考えてよいと思います。
「値」をキャッシュする、このような処理は、経験豊かなプログラマなら何度か書いたことがある処理でありさほど難しいものではありませんよね。
なぜ、「メモ化」なんていう大袈裟?な専門用語を持ち出すのかというと、おそらく、関数内で、関数の外側の変数の存在を意識しない形でキャッシング処理を行うのは特別なものである、と考えているからでしょう。関数型言語由来かもしれません。
ようは、キャッシュの持ち方が独特なだけで、特別なものではありません。キャッシュしたくなるのはどんな時?
キャッシュ処理を行いたくなる時って、どんなときでしょうか。
「値」をキャッシュに保持することにより、処理を高速化したいときですよね。
例えば、「値をファイルから読み込む処理があるが、毎回ファイルから読み込むと処理時間がかかるので、ファイルが更新されていなければ値をキャッシュから読み込む」というようなものが考えらます。
そして、「ファイルが更新されてい」るかどうか、という判断こそが「依存リスト引数」にあたるものです。とってもわかりやすいですね。
気を付けるべきところは、「ファイルが更新されているかどうか」というチェックの処理時間が、ファイルから値を読み込む処理時間よりも長いなら、キャッシュ処理はしない方がよいことになることです。
メモ化においても、値をメモから読み出せば足りるか、そうではなく新ためて元のリソースから読み出すべきか、という判断がきちんとできないのなら、使用するべきではありません。
すなわち、キャッシュ処理やメモ化は、安易に導入すべきものではなく、慎重に設計や実験をしたうえで導入すべきものなのです。
最後に、メモ化なんてさほど大袈裟なものではない、ということは示すためにも、できるだけ実践的なサンプルソースを掲載いたします。
url が変化したときだけ fetch し直すという、ありがちな処理を React.memo() と、 React Suspense を用いて作成してみました。<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Test</title> </head> <body> <div id="App"></div> </body> <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script> <script crossorigin src="https://unpkg.com/htm"></script> <script> // htm is JSX-like syntax in plain JavaScript - no transpiler necessary. // https://github.com/developit/htm const html = htm.bind(React.createElement) // Render-as-You-Fetch // https://reactjs.org/docs/concurrent-mode-suspense.html#approach-3-render-as-you-fetch-using-suspense const wrapPromise = promise => { let status = "pending"; let result; const suspender = promise.then( r => { status = "success"; result = r; }, e => { status = "error"; result = e; } ); return { read() { if (status === "pending") { throw suspender; } else if (status === "error") { throw result; } else if (status === "success") { return result; } } }; } // 指定されたミリ秒待つ関数 const sleep = msec => new Promise(resolve => setTimeout(resolve, msec)) const WordPressRestApiInner = props => { const text = props.resource.read() return html`<p>${text}</p>` } const WordPressRestApi = props => { const getText = async (url) => { try { const res = await fetch(url, {mode: 'cors', credentials: 'include'}, ) await sleep(500) // 効果を分かりやすくするためにあえて 500ms ウエィト const json = await res.json() return json.content.rendered } catch (e) { return e.message } } // url が変化したときだけ fetch し直す。React.useMemo() の威力です。 const resource = React.useMemo(() => wrapPromise(getText("https://demo.wp-api.org/wp-json/wp/v2/pages/" + props.url)) , [props.url] ) return html` <${React.Suspense} fallback=${html`<p>Now Loading...</p>`}> <${WordPressRestApiInner} resource=${resource}/> <//> ` } const ClickCounter = props => { const redrawMe = React.useState()[1] const refSelect = React.useRef({value: "2"}) // ここではお手軽な uncontroled component を採用しました return html` <p> <select name="url" style=${{marginRight:"1rem"}} ref=${refSelect}> <option value="2">Page 2</option> <option value="7">Page 7</option> </select> <button onClick=${() => redrawMe(new Date().getTime())}> Reload Page </button> <${WordPressRestApi} url=${refSelect.current.value}/> </p> ` } const elm = html` <h1>Test</h1> <${ClickCounter}/> ` ReactDOM.render(elm, document.getElementById("App")) </script>このサンプルを作成して思ったのですが、React.useMemo() を使いこなせば、React.memo() は「いらない子」になっていく気がしました。React.memo() が「いらない子」なら、ほぼそのためだけに存在するといってよいっぽい React.useCallback() もだんだん「いらない子」に近づいてくような・・・
React.useCallback() アゲイン
キャッシュ処理における更新チェック、と考えれば、React.useCallback() を React.memo() とは無関係に、React.useMemo() の単なるショートハンドとして使うときには、「依存リスト引数」を使うこともあるのかな?
いや、でもそもそも単なるショートハンドとしての使い道すらなさそうな。だって、更新チェックが必要なら、関数を生成する過程も必要ですよね。次のソースように。React.memo(() => { // 何らかの、関数生成に影響を与える一連の処理、スタート : : // 何らかの、関数生成に影響を与える一連の処理、ここまで return 生成した関数 }, [関数生成の依存リスト])闇は深そうです。
最後に
いかがでしたでしょうか。
この記事が少しでも React プログラマーの役に立てれば幸せです。
識者のコメントをお待ちしております。
- 投稿日:2020-09-19T09:11:00+09:00
作って理解JavaScript:JOKE開発記その8 - 配列とアロー関数
今回のスコープ
また前回から二か月近く経ってますが8月は暑すぎてほとんど開発してませんでした。
さて前回のあとがき通りに今回のネタは配列メイン、ついでにアロー関数です。
- ステップ11:アロー関数
- ステップ12:配列
- リテラルと添え字形式アクセス
- for-of
- 各種メソッド
ステップ11:アロー関数
ステップ11段階のコードは以下にあります。
https://github.com/junjis0203/joke/tree/step0011前回も書きましたが、アロー関数での「thisを束縛しない」動作は実装していません。これはアロー関数を実装した(使いたかった)理由が「コールバック関数を短く書きたかったから」であるためです(thisを束縛しない動作を実装するモチベーションがない)
引数処理(カッコ問題)
アロー関数の仕様は以下に書かれています。
http://www.ecma-international.org/ecma-262/6.0/#sec-arrow-function-definitions引数に対応する
ArrowParameters
。一つ目がa => a * 2
みたいなカッコなしで、二つ目が(a, b) => a + b
みたいなカッコありに対応しています。ArrowParameters : BindingIdentifier CoverParenthesizedExpressionAndArrowParameterList
CoverParenthesizedExpressionAndArrowParameterList
どこかで見たなと思ったら開発記その3で言及していました。Syntax PrimaryExpression : 略 CoverParenthesizedExpressionAndArrowParameterList CoverParenthesizedExpressionAndArrowParameterList : ( Expression ) ( ) ( ... BindingIdentifier ) ( Expression , ... BindingIdentifier ) Supplemental Syntax When processing the production PrimaryExpression : CoverParenthesizedExpressionAndArrowParameterList the interpretation of CoverParenthesizedExpressionAndArrowParameterList is refined using the following grammar: ParenthesizedExpression : ( Expression )このときはまだよくわかっていなかったので「つまり
( Expression )
として処理してしまっていいんやな」というように実装しました。さて今回の
ArrowParameters
についてもSupplemental Syntaxとして以下のように書かれています。When the production ArrowParameters : CoverParenthesizedExpressionAndArrowParameterList is recognized the following grammar is used to refine the interpretation of CoverParenthesizedExpressionAndArrowParameterList : ArrowFormalParameters : ( StrictFormalParameters )というわけで
( StrictFormalParameters )
と処理するようにしてみましたが、うまくいきませんでした。
正確に言うとアロー関数自体は動くものの、(1 + 2)
のようなカッコつき演算がこける1ようになりました。テスト大事ですね。問題は、
(
を読んだ時点では以降にあるのがカッコつき演算なのかアロー関数なのかわからないという点です。カッコ問題の解決方法
「カッコつき演算なのかアロー関数なのかわからない」問題には以下のように対処しました。
- refineしない
CoverParenthesizedExpressionAndArrowParameterList
としてまず読み込む)
の後に=>
がある場合はアロー関数として処理する(後述))
の後に=>
がなければカッコつき演算として処理する(実装的にはバックトラックした後、既存の演算文法解析処理に流す)コンマの処理
CoverParenthesizedExpressionAndArrowParameterList
のBNFを再掲(残余引数は省略)CoverParenthesizedExpressionAndArrowParameterList : ( Expression ) ( )(。´・ω・)?
あれ?複数引数扱うBNFなくない?それではここで
Expression
のBNFを見てみましょう。Expression : AssignmentExpression Expression , AssignmentExpressionなんと
Expression
はコンマ演算子を含んでいました。
つまり、「コンマ演算に対応することにより、アロー関数の引数を分けるコンマも対応される」という仕組みでした。これを考えた人は頭がいいというのかコリジョンを解決するために編み出したのかが気になるところです。「コンマ演算」を「複数引数」として扱う
読み込んだ
CoverParenthesizedExpressionAndArrowParameterList
を「アロー関数の引数」として扱うにはそれをStrictFormalParameters
に直さないといけません。仕様ではここら辺にやり方が書かれていますが2、以下の変換を行うようにしました。
- 「式」の「変数参照」は「変数宣言」に置き換える
- 「式」の「代入演算」は「デフォルト値あり変数宣言」に置き換える
処理コードはこちら。コメントに「作者の心情」が吐露されてますね(笑)
parser.js抜粋function ArrowParameter(scanner) { function convertNode(node) { let clone; switch(node.type) { case Node.IDENTIFIER_REFERENCE: clone = {...node, type: Node.IDENTIFIER}; break; case Node.ASSIGNMENT: clone = { type: Node.INITIALIZE, identifier: node.left.identifier, initializer: node.right }; break; } return clone; } if (checkCharToken(scanner.token, '(')) { /* Specification says CoverParenthesizedExpressionAndArrowParameterList is recognized as StrictFormalParameters. But can't refine because of ill implementation ... */ const list = CoverParenthesizedExpressionAndArrowParameterList(scanner); // expand const params = []; let curr = list.expr; if (curr) { while (curr.type == Node.COMMA) { params.push(convertNode(curr.left)); curr = curr.right; } params.push(convertNode(curr)); } const node = { params } return node; } else { // 省略 } }ステップ12:配列
ステップ12段階のコードは以下にあります。
https://github.com/junjis0203/joke/tree/step0012配列はステップ12を作ってる最中もいろいろと実装が変わりましたが最終的に以下のようになりました。
- 配列はただのオブジェクトである(
JokeObject
クラスのインスタンスである)constructor
としてJokeArray
を持つJokeArray
はJokeFunction
クラスのインスタンスであるJavaScriptの
Array
はビルトインオブジェクトであり、ビルトインクラスではありません。
ということは知っていたつもりだったのですが、初め「JokeFunction
クラスを継承したJokeArray
クラスを作り、配列はJokeArray
クラスのインスタンス」としていたのでステップ10までで作ったオブジェクトとクラスの仕組みの上で動かすためにおかしな実装をしていました。リテラル
さてまずはリテラルで書いた配列を扱えるようにしました。なお、
new Array()
で配列を作るのもやればできると思いますが自分が普段そういう書き方をしていないので対応していません。前回、lengthはgetter/setter使えばできるなとgetter/setterを実装したわけですが結局使っていません。
arr.length = 0
みたいにされたときは現在の要素をクリアするのが正しい動作だと思いますが使わないので・・・for-of
for-ofはいろいろと難関でした。
構文解析的な課題
まず構文解析的な課題です。前述したアロー関数カッコ問題と同じような感じですが、forがあった時点ではC言語風のfor文が書かれているのかfor-of文が書かれているのかはわかりません。なお、
of
はECMAScriptでは予約語ではないのですがめんどくさいのでJOKEでは予約語としています。
http://www.ecma-international.org/ecma-262/6.0/#sec-iteration-statementsIterationStatement : for ( Expression ; Expression ; Expression ) Statement for ( LexicalDeclaration Expression ; Expression ) Statement for ( LeftHandSideExpression of AssignmentExpression ) Statement for ( ForDeclaration of AssignmentExpression ) Statementいろいろと手抜きして以下のようにしました。
- for-ofではconstで新しいスコープが作られるもののみをサポート。つまり、上の3番目のBNFはサポートしない。
- とりあえずLexicalDeclarationとして読み込んでみて駄目ならfor-ofとして読んでみる。なお2番目のBNFで「セミコロン足りなくね?」と思われた方は開発記その5をご参照ください。
実装コードはこちら。コメントに作者の心情が(ry
そろそろ汎用の「BNF1を試す」→「駄目ならBNF2を試す」という仕組みを作った方がいい気がしますね・・・parser.js抜粋const state = scanner.saveState(); try { decr = LexicalDeclaration(scanner); } catch (e) { if (e instanceof SyntaxError) { // try for-of // TODO: too ad-hoc scanner.restoreState(state); type = Node.FOR_OF; decr = ForDeclaration(scanner); if (decr && checkKeywordToken(scanner.token, 'of')) { scanner.next(); } else { throw e; } } else { throw e; } }実行方法的な課題(仕様編)
次に実行的な課題です。for-ofをどのように実行すればいいかは次の個所に書かれています。
http://www.ecma-international.org/ecma-262/6.0/#sec-for-in-and-for-of-statements-runtime-semantics-labelledevaluation要約すると
- ofの右側にある式からiteratorを取得する
- iteratorから次を取り出す
- 次がないならループ終了する
- 次があるならその値をセットして本文を実行する
初めは独自解釈(よくわからなかったとも言う)で実装したのですが、「for-ofって配列以外にも使えるのかな」ということは疑問に思っており、たまたまSetを使う機会があって
Set
もfor-ofで回せることを知りました。
というわけでもう少し調べてみるとiteratorのインターフェースがちゃんと定義されていることがわかりました(ぉぃ
http://www.ecma-international.org/ecma-262/6.0/#sec-iteration
- for-ofはオブジェクトの
@@iterator
メソッドを呼び出してiteratorを取得する- iteratorはnextメソッドが呼び出されると
IteratorResult
オブジェクトを返すIteratorResult
のdone
プロパティはiteratorに次があるかを示し、value
プロパティが次の値であるというわけでそれを実装しました。やや長いのでコードを貼るのは止めておきます。
実行方法的な課題(実装編)
ここまでは仕様的な実行方法の話、ここからは
@@iterator
を「どう実装するか」の話です。
配列の各種メソッドを実装するために、Rubyとかのように「ネイティブ(言語処理系を書くために使われている言語)」で処理を記述できるようにしようと思ってはいたのですが一足早くその機会が訪れました。まず以下の
JokeNativeMethod
クラスを定義しました。object.js抜粋export class JokeNativeMethod extends JokeObject { constructor(method) { super(); this.method = method; } call(context, thisValue, ...args) { return this.method(context, thisValue, ...args); } }この
JokeNativeMethod
を使って「処理」をラップします。本当はただの関数にしたかったのですが「this」を保持する場所が必要だったのでこのようになりました。builtin/array.js抜粋const Array_iterator = new JokeNativeMethod( (context, thisValue) => { return new JokeArrayIterator(thisValue); } );定義した「ネイティブメソッド」は
prototype
に設定しておきます。builtin/array.js抜粋export const JokeArray = new JokeFunction(); { const prototype = JokeArray.getProperty('prototype'); prototype.setProperty('length', 0); prototype.setProperty(Symbol.iterator, Array_iterator);「ネイティブメソッド」の呼び出しはベタに条件分岐で書いています。
vm.js抜粋export function callFunction(context, func, args) { const thisValue = context.thisValue ? context.thisValue : func.thisValue; if (func instanceof JokeNativeMethod) { return func.call(context, thisValue, ...args); } // これ以降、「アセンブル」されたプログラムの呼び出し処理
context
(変数スコープなど)を引き回しているのはどうにもダサいのですがいい方法が思いつかないのでこのようになっています。また、this
とthisValue
をよく間違えて例外が起こるのが問題ですね(笑)各種メソッド
というわけで予定よりも早く「ネイティブメソッド」のAPIができたので、後は淡々と配列のメソッドを定義していきました。ただし全部ではなく自分が使いそうなもののみです。
- push, pop, unshift, shift
- reverse, sort, splice
- includes, indexOf, join, slice, toString
- filter, find, map, reduce
あっ、もう一ネタありましたね。今度は「ネイティブメソッドからコールバックで渡された関数を呼ぶ方法」です。
builtin/array.js抜粋const Array_filter = new JokeNativeMethod( (context, thisValue, callback, thisArg) => { const length = thisValue.getProperty('length'); const callbackContext = { ...context, thisValue: thisArg ? thisArg : thisValue }; const filtered = JokeArray.newInstance(); for (let i = 0; i < length; i++) { const elem = thisValue.getProperty(i); // ↓これ if (callback.call(callbackContext, elem, i, thisValue)) { filtered.invoke('push', context, elem); } } return filtered; } );
call
されたら単純に自分を引数にしてcallFunction
を呼び出します。
callFunction
が定義されているのはvm.js
なのでこの依存関係はやや微妙です。object.js抜粋export class JokeFunction extends JokeObject { call(context, ...args) { return callFunction(context, this, args); }最後に、「
sort
って比較関数渡されない場合はa < b
みたいにやればいいのかな」と考えていましたが全然違いました。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/sortcompareFunction (比較関数) が与えられなかった場合、 undefined 以外のすべての配列要素は文字列に変換され、文字列が UTF-16 コード単位順でソートされます。例えば、 "banana" は "cherry" の前に来ます。数値のソートでは、 9 が 80 の前に来ますが、数値は文字列に変換されるため、 Unicode 順で "80" が "9" の前に来ます。 undefined の要素はすべて、配列の末尾に並べられます。
マジか。これって常識なんですかね。
なお、JOKEでは比較関数渡されない場合の動作は未実装(ていうか落ちます)です。あとがき
以上、今回はアロー関数と配列を実装しました。思ってた通りのこともあれば思ってた(想像していた)仕様と違う個所も多くありました。
ともかくこれで配列、前回でオブジェクトを実装したので次は残余引数だったり分割代入だったりを実装していく予定です。
それではまた一か月後(?)に。
- 投稿日:2020-09-19T08:58:43+09:00
【Vue.js】propsをHTMLの属性値に渡すときはv-bindを使う
propsをHTMLの内容に渡すとき
子コンポーネント側では特に注意点はありません
<template> <p>{{ text }}</p> </template>propsをHTMLの属性値に渡すとき
子コンポーネント側でv-bindを使う
<template> <p :class="text">テキスト</p> </template>Twitterアカウント
Twitterも更新していますので、よかったらフォローお願いします!
Twitterアカウントはこちら
- 投稿日:2020-09-19T08:34:54+09:00
Markdown でリンクを貼り付けるときに自分が使っている方法
おはようございます。
今回は「Markdown でリンクを貼り付けるときに自分が使っている方法」を紹介しようと思います。
もっといい方法があるよ、という方がいらっしゃいましたら、コメント欄で是非共有いただければと思います。まえがき
Markdown でリンクを張り付けるとき、サイトのタイトルと URL を取ってこないといけないですよね。
Chrome の場合だと、 URL はCtrl+L
でアドレスバーにフォーカスを当ててからのCtrl+C
でコピーすれば楽なのですが、タイトルを取るときに私は少々てこずりました。
ブログ記事などでブログのタイトルになっている場合は、そのタイトルをコピペしたらいいのですが、そうでない場合・・・
タブを右クリックしても何も出てこなくて困りました?からの仕方なく手打ちするとか?私がどうやっているか
くだらないかもしれませんが、開発者ツールで中身見たら全部わかるだろう、と気づきました。
F12
を押下し、Console
タブを選択して次のコマンドを入力すれば、タイトルが取得できます。console.log(document.title);さらに、 Markdown でリンクを貼る際は
[リンク名](URL)
と記載するので、次のようにすればもっと簡単になります。
- ECMAScript 6 の場合
console.log(`[${document.title}](${document.URL})`);
- ECMAScript 5 の場合
console.log('[' + document.title + '](' + document.URL + ')');よろしければ使ってみてください。
- 投稿日:2020-09-19T03:15:00+09:00
JavaScriptでオブジェクトの配列に入っている要素をHTMLに一つずつ表示する
まず、オブジェクトの配列を用意する
var array = [{id:1, name:"Ramen"}, {id:2, name:"Somen"}];HTMLを書く
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <!-- ここのgetDataの部分にjsからデータを表示する --> <div id="getData"></div> <script src="code.js"></script> </body> </html>オブジェクトの配列を表示するためのJavaScriptのコード
// 配列の長さの文だけループを回す for (var i = 0; i < array.length; i++) { // まず、idを表示する // Htmlのdivの部分を指定する var getData = document.getElementById('getData'); // 表示したいデータを指定する var arrayId = document.createTextNode(array[i].id); // 要素を指定し、その要素の子要素としてデータを表示する getData.appendChild(arrayId); // 次に、nameを表示する var arrayName = document.createTextNode(array[i].name); getData.appendChild(arrayName); }最終的なJavaScriptのコード
window.onload = function () { // オブジェクトの配列を用意 var array = [{ id: 1, name: "Ramen" }, { id: 2, name: "Somen" }]; // 配列の長さの文だけループを回す for (var i = 0; i < array.length; i++) { // まず、idを表示する // Htmlのdivの部分を指定する var getData = document.getElementById('getData'); // 表示したいデータを指定する var arrayId = document.createTextNode(array[i].id); // 要素を指定し、その要素の子要素としてデータを表示する getData.appendChild(arrayId); // 次に、nameを表示する var arrayName = document.createTextNode(array[i].name); getData.appendChild(arrayName); } }最終的に
Htmlを開くとこのようにJavaScriptからのデータを表示することができます。
- 投稿日:2020-09-19T02:07:52+09:00
『暗号技術入門 第3版(著:結城 浩)』のクイズをJavaScriptで試す
結城 浩さんの『暗号技術入門』を読んで、
「せっかくなら勉強中のJavaScriptで解こう!」と思ったので、備忘を兼ねて投稿します。P.26 クイズ① シーザー暗号
問題内容
下記の通りです。
問題文シーザー暗号化(※)された暗号文『PELCGBTENCUL』を解読してください。鍵は不明です。 ※「abcde」を「bcdef」のようにズラしてメッセージを暗号化する方法シーザー暗号の詳細はこちらをご参照ください。
問題を解いたコード
下記に解いたコードを載せます。やり方としては、
- 「a」〜「z」のアルファベットを文字列として格納した配列を作る
- 問題文で与えられた暗号文に存在する全文字で、「この文字はアルファベット配列の要素でいうと何番目になるか」を調べて、新しく配列を作る(もし暗号文が 'abc' だったら、[0, 1, 2]の配列を作る)
- 作成した配列の全要素に対して、「a」〜「z」の全アルファベット26回分、1つずつ後ろにずらす処理をする。(0回目:[0, 1, 2] 1回目:[1, 2, 3] ...というイメージ)
- 1つずつ後ろにずらす処理をする度に、数字の配列を元のアルファベットに戻す([0, 1, 2]なら abc、[1, 2, 3]ならbcd)
- アルファベットに戻すたびに、関数から返す配列
decryptedArray
に追加する- 「アルファベットに戻したもの」の配列を
return
するreturn
したresults
を1つずつ取り出して、コンソールに出力するcaesar.jsfunction convertTextToIndex(cryptedText) { let splitted = cryptedText.split('') return splitted.map(val => alphabets.indexOf(val)) } function bruteForceAttack(indexArray) { let decrypedArray = [] for (let i = 0; i < alphabets.length; i++) { // 手順3 const shiftedIndexArray = indexArray.map(value => { let shiftedIndex = value + i if (shiftedIndex > alphabets.length-1) return shiftedIndex - alphabets.length return shiftedIndex }) // 手順4,5 decrypedArray.push(shiftedIndexArray.map(num => alphabets[num]).join('')) } // 手順6 return decrypedArray } // 手順1 const alphabets = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; const cryptedText = 'pelcgbtencul'; // 手順2 const indexArray = convertTextToIndex(cryptedText); const results = bruteForceAttack(indexArray); // 手順7 results.forEach(result => console.log(result.toUpperCase()));分かりにくかったらすみません。
理解度が少し上がったら、もっと分かりやすく書き直します。
おかしな実装等あればビシバシご指摘いただけると嬉しいです、
よろしくお願いします。
- 投稿日:2020-09-19T02:00:45+09:00
既存のVue.jsプロジェクトをVue 3へ以降したときに必要だった修正まとめ
はじめに
この記事では、「既存プロジェクトをとにかくVue3へ移行して元通り動くようにする」が目的です。
「Composition APIで書き換える」といったVue 3の新機能への移行を紹介するものではありません。
Vue CLI
移行元のプロジェクトはかなり古いバージョンのvue-cliでセットアップされたものでした。
これを頑張ってバージョンアップしていくのはしんどいので、
新しいバージョンのVue CLIでプロジェクトを作り直して、そこに既存資材を移行しました。$ sudo npm install -g @vue/cli $ vue create project-nameVue 3が選べるのは、Vue CLI 4.5以上となります。
main.js
必ず変更を要するのが、アプリの初期化部分です。
beforeimport Vue from 'vue' import ... new Vue({ el: '#app', components: { App }, router })afterimport { createApp } from 'vue' import ... const app = createApp(App) app.use(router) app.mount('#app')この後の項目で順次置き換えていきますが、
import Vue from 'vue'
のようにVueオブジェクトをインポートして使うことはなくなります。設定ファイル
これは古めのVue CLIから移行する場合のみですが、webpack周りの設定ファイルが表からはなくなります。
カスタマイズが必要な場合は、
vue.config.js
を作成して、そこに記述しましょう。vue.config.jsmodule.exports = { ... }プラグイン
Vue.js向けのプラグインをそのまま使おうとした場合に動かなくなる可能性があります。
長くなったので、こちらの記事に切り出しました。
『既存のVue.jsプラグインがVue 3で使えない場合の対応』
vuex
Vuex 4に置き換えます。 ※ 現時点でベータです
$ yarn add vuex@^4beforeimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ ... }) new Vue({ el: '#app', store, ... })afterimport { createApp } from 'vue' import { createStore } from 'vuex' const app = createApp(...) const store = createStore({ ... }) app.use(store)vue-router
vue-routerも4に置き換えます。
$ yarn add vue-rouuter@^4import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [...] }) ... app.use(router)router.match
無くなりました。
resolveというメソッドが同等のようです。beforeconst matchedRoute = this.$router.match({ name: 'Index' })afterconst matchedRoute = this.$router.resolve({ name: 'Index' })router.currentRoute
value
に潜る必要があります。beforeconst currentRoute = router.currentRouteafterconst currentRoute = router.currentRoute.valueVue.set / Vue.delete
Vue.jsは新しいプロパティの追加を検知できないので、その対処として
Vue.set()
が必要となる場合がありました。before// これだと変更が検知されないので、 // this.awesomeObject.newKey = newValue // こう↓ Vue.set(this.awesomeObject, 'newKey', newValue)Vue3では、新しいプロパティが検知されるようになりました。
なので、単に使うのを辞めるだけです。
beforethis.awesomeObject.newKey = newValue
Vue.delete
も同様です。Vue.observable
Vue.observable
はありません。今回はComposition API使わないと言ったものの、
Vue.observable
頼りだった部分の書き換えに必要でした。beforeimport Vue from 'vue' class AwesomeClass { count = 0 position = { x: 0, y: 0 } constructor () { Vue.observable(this) // インスタンス丸ごとリアクティブにしちゃえ } }afterimport { ref, reactive } from 'vue' class AwesomeClass { // プリミティブ向け count = ref(0) xxx () { count.value = 1 console.log(this.count.value) // -> 1 } // Object向け position = reactive({ x: 0, y: 0 }) yyy () { this.position.x = 1 console.log(this.position.x) // -> 1 } }おわりに
おわりです
- 投稿日:2020-09-19T02:00:45+09:00
既存のVue.jsプロジェクトをVue 3へ移行したときに必要だった修正まとめ
はじめに
この記事では、「既存プロジェクトをとにかくVue3へ移行して元通り動くようにする」が目的です。
「Composition APIで書き換える」といったVue 3の新機能への移行を紹介するものではありません。
Vue CLI
移行元のプロジェクトはかなり古いバージョンのvue-cliでセットアップされたものでした。
これを頑張ってバージョンアップしていくのはしんどいので、
新しいバージョンのVue CLIでプロジェクトを作り直して、そこに既存資材を移行しました。$ sudo npm install -g @vue/cli $ vue create project-nameVue 3が選べるのは、Vue CLI 4.5以上となります。
main.js
必ず変更を要するのが、アプリの初期化部分です。
beforeimport Vue from 'vue' import ... new Vue({ el: '#app', components: { App }, router })afterimport { createApp } from 'vue' import ... const app = createApp(App) app.use(router) app.mount('#app')この後の項目で順次置き換えていきますが、
import Vue from 'vue'
のようにVueオブジェクトをインポートして使うことはなくなります。設定ファイル
これは古めのVue CLIから移行する場合のみですが、webpack周りの設定ファイルが表からはなくなります。
カスタマイズが必要な場合は、
vue.config.js
を作成して、そこに記述しましょう。vue.config.jsmodule.exports = { ... }プラグイン
Vue.js向けのプラグインをそのまま使おうとした場合に動かなくなる可能性があります。
長くなったので、こちらの記事に切り出しました。
『既存のVue.jsプラグインがVue 3で使えない場合の対応』
vuex
Vuex 4に置き換えます。 ※ 現時点でベータです
$ yarn add vuex@^4beforeimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ ... }) new Vue({ el: '#app', store, ... })afterimport { createApp } from 'vue' import { createStore } from 'vuex' const app = createApp(...) const store = createStore({ ... }) app.use(store)vue-router
vue-routerも4に置き換えます。
$ yarn add vue-rouuter@^4import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [...] }) ... app.use(router)router.match
無くなりました。
resolveというメソッドが同等のようです。beforeconst matchedRoute = this.$router.match({ name: 'Index' })afterconst matchedRoute = this.$router.resolve({ name: 'Index' })router.currentRoute
value
に潜る必要があります。beforeconst currentRoute = router.currentRouteafterconst currentRoute = router.currentRoute.valueVue.set / Vue.delete
Vue.jsは新しいプロパティの追加を検知できないので、その対処として
Vue.set()
が必要となる場合がありました。before// これだと変更が検知されないので、 // this.awesomeObject.newKey = newValue // こう↓ Vue.set(this.awesomeObject, 'newKey', newValue)Vue3では、新しいプロパティが検知されるようになりました。
なので、単に使うのを辞めるだけです。
beforethis.awesomeObject.newKey = newValue
Vue.delete
も同様です。Vue.observable
Vue.observable
はありません。今回はComposition API使わないと言ったものの、
Vue.observable
頼りだった部分の書き換えに必要でした。beforeimport Vue from 'vue' class AwesomeClass { count = 0 position = { x: 0, y: 0 } constructor () { Vue.observable(this) // インスタンス丸ごとリアクティブにしちゃえ } }afterimport { ref, reactive } from 'vue' class AwesomeClass { // プリミティブ向け count = ref(0) xxx () { count.value = 1 console.log(this.count.value) // -> 1 } // Object向け position = reactive({ x: 0, y: 0 }) yyy () { this.position.x = 1 console.log(this.position.x) // -> 1 } }おわりに
おわりです
- 投稿日:2020-09-19T02:00:45+09:00
既存のVue.jsプロジェクトをVue 3へ以降したときに必要だった対応まとめ
はじめに
この記事では、「既存プロジェクトをとにかくVue3へ移行して元通り動くようにする」が目的です。
「Composition APIで書き換える」といったVue 3の新機能への移行を紹介するものではありません。
Vue CLI
移行元のプロジェクトはかなり古いバージョンのvue-cliでセットアップされたものでした。
これを頑張ってバージョンアップしていくのはしんどいので、
新しいバージョンのVue CLIでプロジェクトを作り直して、そこに既存資材を移行しました。$ sudo npm install -g @vue/cli $ vue create project-nameVue 3が選べるのは、Vue CLI 4.5以上となります。
main.js
必ず変更を要するのが、アプリの初期化部分です。
beforeimport Vue from 'vue' import ... new Vue({ el: '#app', components: { App }, router })afterimport { createApp } from 'vue' import ... const app = createApp(App) app.use(router) app.mount('#app')この後の項目で順次置き換えていきますが、
import Vue from 'vue'
のようにVueオブジェクトをインポートして使うことはなくなります。設定ファイル
これは古めのVue CLIから移行する場合のみですが、webpack周りの設定ファイルが表からはなくなります。
カスタマイズが必要な場合は、
vue.config.js
を作成して、そこに記述しましょう。vue.config.jsmodule.exports = { ... }プラグイン
Vue.js向けのプラグインをそのまま使おうとした場合に動かなくなる可能性があります。
長くなったので、こちらの記事に切り出しました。
『既存のVue.jsプラグインがVue 3で使えない場合の対応』
vuex
Vuex 4に置き換えます。 ※ 現時点でベータです
$ yarn add vuex@^4beforeimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ ... }) new Vue({ el: '#app', store, ... })afterimport { createApp } from 'vue' import { createStore } from 'vuex' const app = createApp(...) const store = createStore({ ... }) app.use(store)vue-router
vue-routerも4に置き換えます。
$ yarn add vue-rouuter@^4import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [...] }) ... app.use(router)router.match
無くなりました。
resolveというメソッドが同等のようです。beforeconst matchedRoute = this.$router.match({ name: 'Index' })afterconst matchedRoute = this.$router.resolve({ name: 'Index' })router.currentRoute
value
に潜る必要があります。beforeconst currentRoute = router.currentRouteafterconst currentRoute = router.currentRoute.valueVue.set / Vue.delete
Vue.jsは新しいプロパティの追加を検知できないので、その対処として
Vue.set()
が必要となる場合がありました。before// これだと変更が検知されないので、 // this.awesomeObject.newKey = newValue // こう↓ Vue.set(this.awesomeObject, 'newKey', newValue)Vue3では、新しいプロパティが検知されるようになりました。
なので、単に使うのを辞めるだけです。
beforethis.awesomeObject.newKey = newValue
Vue.delete
も同様です。Vue.observable
Vue.observable
はありません。今回はComposition API使わないと言ったものの、
Vue.observable
頼りだった部分の書き換えに必要でした。beforeimport Vue from 'vue' class AwesomeClass { count = 0 position = { x: 0, y: 0 } constructor () { Vue.observable(this) // インスタンス丸ごとリアクティブにしちゃえ } }afterimport { ref, reactive } from 'vue' class AwesomeClass { // プリミティブ向け count = ref(0) xxx () { count.value = 1 console.log(this.count.value) // -> 1 } // Object向け position = reactive({ x: 0, y: 0 }) yyy () { this.position.x = 1 console.log(this.position.x) // -> 1 } }おわりに
おわりです
- 投稿日:2020-09-19T00:40:01+09:00
既存のVue.jsプラグインがVue 3で使えない場合の対応
目次:
- ケース1: this.$xxx() 系のプラグインで起きる問題
- ケース2: <div v-xxx>系のプラグインで起きる問題
- 補足1: Vue.jsとVue 3両方に対応させる
- 補足2: プラグインの修正ってどうやるの?ケース1: this.$xxx() 系のプラグインで起きる問題
Vueインスタンスメソッドに機能が拡張されるタイプのプラグインで起きるのは、
プラグインのインストール時、Vueのprototypeの拡張に失敗しているという事象です。Uncaught TypeError: Object.defineProperty called on non-object または Uncaught TypeError: Cannot set property '$awesomeVuePlugin' of undefineあたりのエラーが出ていないでしょうか?
プラグインがどのように注入されるか
先にVueのプラグインのインストールについて少し説明させてください。
プラグインは
Vue.use()
に渡されますが、ここに渡されるオブジェクトにはinstall
というメソッドが実装されている必要があります。
install
メソッドの第一引数には、Vueオブジェクトが渡されるので、それに対しdirectiveやmixin、prototypeなどを拡張することができます。export default { install (Vue) { // Vueのインスタンスメソッドとしてプラグインの機能を使えるようにする Vue.prototype.$awesomeVuePlugin = awesomeVuePlugin } }Vue 3での変化
Vue 3では
Vue
オブジェクトは扱わなくなります。
初期化時は代わりにcreateApp
というAPIで、アプリオブジェクトを生成します。旧Vue.jsimport Vue from 'vue' Vue.use(プラグイン) new Vue({ ... })Vue3import { createApp } from 'vue' const app = createApp(...) app.use(プラグイン) // appオブジェクトに対してuseしますそして
use()
したときに、プラグインのinstall
メソッドに渡される引数もVue
ではなくそのアプリオブジェクトになります。このappはprototypeを持ちませんので、アサインしようとして
$xxx of undefined
となってしまいます。export default { install (app) { console.log(app.prototype) // -> undefined app.prototype.$xxx = ... // -> Error } }解決策: globalPropertiesを使う
Vue 3では、app.config.globalPropertiesを拡張するのがその代替となります。
beforeexport default { install (Vue) { Vue.prototype.$awesomeVuePlugin = awesomeVuePlugin } }afterexport default { install (app) { app.config.globalProperties.$awesomeVuePlugin = awesomeVuePlugin } }ケース2: <div v-xxx>系のプラグインで起きる問題
次に、ディレクティブが拡張されるタイプのプラグインです。
エラーなどもなくただ機能が動作しない、といった事象が起きていませんか?
Vue 3では、カスタムディレクティブで用いられるメソッドの名前が変わったことが原因と思われます。
解決策: 新しいメソッド名に修正する
Vue.js Vue 3 bind beforeMount inserted mounted - beforeUpdate update - componentUpdated updated - beforeUnmount unbind unmounted Ref: https://v3.vuejs.org/guide/migration/custom-directives.html#_3-x-syntax
無くなったupdate
はupdated
を使ってください。beforeapp.directive('xxx', { inserted (el, binding, vnode) { el.style.background = binding.value } unbind () {} })afterapp.directive('xxx', { mounted (el, binding, vnode) { el.style.background = binding.value } unmounted () {} })補足1: Vue.jsとVue 3両方に対応させる
アプリオブジェクトも
Vue
オブジェクトもversion
というプロパティから使われているVueのバージョンを取得可能です。これを利用して実装を分岐しましょう。
export default { install (app) { console.log(app.version) // -> '3.0.0-rc.12' const isVue3 = app.version.startsWith('3') // インスタンスメソッドの拡張 const prototype = isVue3 ? app.config.globalProperties : app.prototype prototype.$awesomeVuePlugin = awesomeVuePlugin // ディレクティブの拡張 app.directive('xxx', { [isVue3 ? 'mounted' : 'inserted'] (el, binding, vnode) { el.style.background = binding.value } [isVue3 ? 'unmounted' : 'unbind'] () {} }) } }補足2: プラグインの修正ってどうやるの?
本題とは逸れますが、そもそもインストールしてきたプラグインをどのように修正すればよいか。
- プラグインのGitHubリポジトリを、自分のアカウントにForkする
- 該当箇所を見つけて修正する
- 本家リポジトリにプルリクを出す
- 本家がアップデートされるまでは
yarn add GitHubユーザー名/awesome-vue-plugin
で自分がForkしたものをインストールする
- 投稿日:2020-09-19T00:22:01+09:00
Googleフォームを外部からAjaxで非同期通信しようとした話(個人開発)
環境
- MySQL5.7
- PHP7.3.16
- jQuery 3.5.1
- レンタルサーバー(ComposerやSSHは非対応)
おことわり
自社サービスと書いていますが個人開発です。
URLはこちら:https://vjct.jp/作ろうとした理由
外部サイトで提供されているとあるGoogleフォーム(以下「Googleフォーム」)の利用者が多く、
そのサービスの二次利用を行うことで自社サービスの利用者を増やす目的で実装。やりたかったこと
- 自社サービスのFormにデータを入力してもらい、送信する
- 自社サービスの送信完了のページで自動的にGoogleフォームにデータを送信
- Googleフォームに送信完了後に画面遷移させずポップアップで成功可否を通知
参考にした記事
- https://qiita.com/taichikanaya_1989/items/5d26179013c57e866312
- https://qiita.com/checche/items/3218ea26359214b51d73
- https://qiita.com/samuraibrass/items/d6afda6bd8f3306cee4f
実装した内容(完成品)
下記のJavaScriptは送信完了ページに実装
JavaScript$(function(event) { var checkVal = "下記の内容で送信します。よろしいですか?"; var draftResponse = '[[[null,426573786,["' + $("#gfsd01").val() + '"],0]'; draftResponse += ',[null,1261006949,["' + $("#gfsd09").val() + '"],0]'; draftResponse += ',[null,450203369,["' + $("#gfsd06").val() + "-" + $("#gfsd07").val() + "-" + $("#gfsd08").val() + '"],0]'; draftResponse += ',[null,1010494053,["' + $("#gfsd02").val() + ":" + $("#gfsd03").val() + '"],0]'; draftResponse += ',[null,203043324,["' + $("#gfsd04").val() + ":" + $("#gfsd05").val() + '"],0]'; draftResponse += ',[null,1540217995,[""],0],[null,701384676,[""],0],[null,2064647146,[""],0],[null,1285455202,[""],0],[null,586354013,[""],0]],null,"-7493555121856808308"]'; var entryData = 'entry.1285455202=&entry.1540217995=' + $("#gfsd10").val() + '&entry.2064647146=&entry.586354013=' + $("#gfsd99").val() + '&entry.701384676=' + $("#gfsd11").val() + '&draftResponse=' + draftResponse + "&pageHistory=0,1&fbzx=-7493555121856808308"; var serializeData = encodeURI(entryData); if (confirm(checkVal)) { $.ajax({ url: "https://docs.google.com/forms/u/0/d/e/***/formResponse", data: serializeData, type: "POST", dataType: "xml", statusCode: { 0: function() { alert("送信が完了しました。"); }, 200: function() { alert("送信に失敗しました。"); } } }); event.preventDefault(); } } else { return false; } });補足
draftResponse
に入れるデータは1ページ目の内容で、多次元配列になっている。- 2ページ目のデータ部分は外だしのため空白のままで大丈夫。
- 2ページ目のデータはそのまま
entry.番号=データ
で問題ない。pageHistory
は複数ページの場合は必須っぽい。fbzx
の用途は不明だが、draftResponse
の最後のデータと合わせる。躓いたところ①
まず、前述の参考にした記事は「送信ボタンが押されたとき」の実装で、
event.preventDefault();
のevent
が、submit
だったりclick
のfunctionから取っており
自動送信の場合はどこからevent
判定を取れば良いのか分からなかった。解決内容
最初のfunctionでイベントがキャンセル出来ることが確認できたので、
$(function(event) {});
で問題なさそう。
躓いたところ②
データのシリアライズについて、参考記事は
form.serialize()
や直接data: {}
などをしており、
実際にどのようなデータを送れば受け取ってくれるか不明瞭だった。解決内容
実際の送信内容はGoogle Chromeのデバッグ機能で確認したところパーセントエンコーディングされたURIだったので、
必要なデータをencodeURI()
でエンコードしてあげれば問題なかった。
躓いたところ③
そのGoogleフォームは2ページに分かれており、参考にした記事はすべて1ページで完結していた。
そのため、記事の内容だけを鵜呑みに実装したところ、2ページ分のデータを送信しているにもかかわらず、
Googleフォーム側で1ページ目のみのデータが受け取られ2ページ目のデータは切り捨てられていた。解決内容
二番目の参考記事で
ちなみに以下の内容はリクエストに含まなくても良い
fvv: 1
draftResponse: [null,null,"90993149820********"]
pageHistory: 0
fbzx: 90993149820********と書かれていた内容を鵜吞みにしてしまっていたため解決に時間がかかったが、
実際にはpageHistory=0,1
をデータ内に含めることで2ページ目の内容も受け取ってもらえた。
おそらくpageHistory=0
の0
はページ番号で、pageHistory
が無い場合はpageHistory=0
と見做されるのではないかと思われます。
fbzx
については用途不明ですが念のため送っています。
まとめ
実装自体は思ったほど難しくないが、コンソールにエラーが吐き出されるので若干心配になる(データ自体は送れている)。
先人の知恵はたくさんあるが、鵜呑みにはしすぎないよう気を付けた方がいいと実感した。