Make it to make it

いろいろ作ってアウトプットするブログ

Nuxt.js入門

Nuxt.jsはVue.jsアプリケーションを作成するためのフレームワーク

Nuxt.jsを使うことによって、トップノッチなテクノロジーを素早く、簡単に、整理された状態で導入することができる。

Nuxt.jsの主要な3つの機能としては、以下がある。

SSR

JavaScriptアプリケーションの弱点として、SEOとメタタグに対応するのが難しいということが挙げられる。ページロードの前までは、検索エンジンがインデックスするためのコンテンツが表示されないこと、そもそもクローラ自体がJavaScriptをサポートしないことなどの理由からだ。

そこで解決策を提供するのがSSRだ。サーバ側でHTMLレスポンスを作成し、クライアント側またはクローラ側に送信する。

SSRの特長

  • SEO
  • メタタグ
  • パフォーマンス向上

vue-class-componentの導入

一般的なVueコンポーネントの書き方は、オブジェクト志向でclassコンポーネントに慣れている人にとっては特殊な書き方である面が大きいだろう。

vue-class-componentを導入すれば、classスタイルなVueコンポーネントが書ける。

vue-class-componentの特徴

  • methodはclassのメンバメソッドとしてダイレクトに書ける
  • computedプロパティはclassプロパティアクセサーとして書ける
  • 初期のdataはclassプロパティとして宣言できる
  • data, renderやその他のVueライフサイクルフックもclassのメンバメソッドして宣言できるが、Vueインスタンス自体にはできない。カスタムメソッドを作成する際には、当然ながら予約語は避けるべきである
  • その他のオプションに関しては、decoratorファンクションに与える

vue-class-componentの導入手順

必要なパッケージ

  • @babel/plugin-proposal-class-properties
  • @babel/plugin-proposal-decorators

必要な設定ファイル

babel.config.js

  plugins: [
    ['@babel/plugin-proposal-decorators', { legacy: true }],
    ['@babel/plugin-proposal-class-properties', { loose: true }]
  ]

jsconfig.json

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

.eslintrc.js

parserOptionsに設定追加

  parserOptions: {
    ecmaFeatures: {
      legacyDecorators: true
    }
  },

今までとの書き方の対比

object (一般的な書き方)

<template>
  <div id="app">
    <button type="button" @click="onClick">Click!</button>
    {{ message }}
  </div>
</template>

<script>
export default {
  data () {
    return {
      message: 'Hello from function'
    }
  },
  methods: {
    onClick () {
      this.message = 'Goodbye'
    }
  }
}
</script>
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App)
}).$mount('#app')

class-style (classの書き方)

<template>
  <div id="app">
    <button type="button" @click="onClick">Click!</button>
    {{ message }}
  </div>
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component({})

export default class App extends Vue {
  message = 'Hello from class'

  onClick () {
    this.message = 'Goodbye'
  }
}
</script>
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => <App />
}).$mount('#app')

propsの記述方法

記述方法1

<template>
  <div id="app">
    {{ message }}
  </div>
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component({
  props: {
    message: {
      default: 'Hello from default prop'
    }
  }
})

export default class App extends Vue {}
</script>

記述方法2

vue-property-decoratorが必要で、Componentも使える

<template>
  <div id="app">
    {{ message }}
  </div>
</template>

<script>
import Vue from 'vue'
import { Component, Prop } from 'vue-property-decorator'

@Component({})

export default class App extends Vue {
  @Prop({ default: 'Hello from Prop decorator' }) message
}
</script>

2020年を迎えてのJavaScript Tips総ざらい

変数定義(var / let / const

var使うことはもうほとんどないし、今さらだが、重要なポイント。

var: 
  function scoped
  undefined when accessing a variable before it's declared

let: 
  block scoped
  ReferenceError when accessing a variable before it's declared

const:
  block scoped
  ReferenceError when accessing a variable before it's declared
  can't be reassigned

分割代入(Object / Array destructuring)

リネームして分割代入することは有用。

const { f: firstName, l: lastName } = user;
console.log(firstName)
console.log(lastName)

React RouterのLinkコンポーネントにも使われている書き方。

render () {
  const { component: Component, to , replace, ...rest } = this.props
  return <Component {...rest} onPress={this.handlePress}/>
}

ファンクションの引数に値を渡す際に、オブジェクト形式で渡す例。 引数が多い場合などは、順番にとらわれないし、渡す際にもよりリーダブルな書き方である。 ついでにデフォルト値なども設定しておくと便利。

function fetchResources ({ srcUrl, minStars = 0, maxStars = 5, createdDate }) {
  // (...snip)
}

fetchResources({
  srcUrl: 'https://example.com/',
  minStars: 3,
  createdDate: new Date('01/01/2020').getTime(),
});

Promise.all()を使う際にも用いることができる。

function getUserData (user) {
  return Promise.all([
    getProfile(user),
    getRepos(user)
  ]).then(([profile, repos]) => ({
    profile,
    repos
  }))
}

ここで欲しい機能として、必須の引数に対してはどのように処理するかである。 一つの対処法としては、requiredを判断するファンクションを別に用意することだ。

function isRequired (arg) {
  throw new Error(`${arg} is required`)
}

function logUserProfile ({ name = isRequired('name'), job = isRequired('job') }) {
  console.log(`${name}'s job is a ${job}`)
}

logUserProfile({
  name: 'Sungjoon',
  job: 'Web developer'
})

この例の場合は、jobの引数を渡さないとjob is requiredというメッセージが出る。

ショートハンドプロパティとメソッド名(Shorthand properties / Method names)

ファクトリーファンクションのような形式で書く場合など。

function formatMessage (name, id, avatar) {
  return {
    name,
    id,
    avatar,
    timestamp: Date.now(),
    save () {
      // (...snip)
    }
  }
}

コンピューティッドプロパティ名(Computed property names)

オブジェクトのキー・バリューを動的に生成する必要がある場合など。

function objectify (key, value) {
  return {
    [key]: value
  }
}

objectify('name', 'Park') // { name: 'Park' }

テンプレートリテラル(Template literals)

シングルコロンやダブルコロンも使える。

const year = 2020
const word = 'cool'
const greetings = `
  Happy new year ${year}!
  This year's gonna be "${word}" year!!
`
console.log(greetings)
// Happy new year 2020! 
// This year's gonna be "cool" year!! 

コンパイル・ポリフィルどちらになるか?

ESNextの機能を使用しているとき、Babelを通すともちろんES5に対応するようにコンパイルが行われるが、fetchなどは新しく追加された機能(API)であることから、コンパイルは行われない。こういった機能に対してポリフィルを行う必要がある。

コンパイルされるもの

Arrow functions
Async functions
Async generator functions
Block scoping
Block scoped functions
Classes
Class properties
Computed property names
Constants
Decorators
Default parameters
Destructuring
Do expressions
Exponentiation operator
For-of
Function bind
Generators
Modules
Module export extensions
New literals
Object rest/spread
Property method assignment
Property name shorthand
Rest parameters
Spread
Sticky regex
Template literals
Trailing function commas
Type annotations
Unicode regex

ポリフィルが必要なもの

ArrayBuffer
Array.from
Array.of
Array#copyWithin
Array#fill
Array#find
Array#findIndex
Function#name
Map
Math.acosh
Math.hypot
Math.imul
Number.isNaN
Number.isInteger
Object.assign
Object.getOwnPropertyDescriptors
Object.is
Object.entries
Object.values
Object.setPrototypeOf
Promise
Reflect
RegExp#flags
Set
String#codePointAt
String#endsWith
String.fromCodePoint
String#includes
String.raw
String#repeat
String#startsWith
String#padStart
String#padEnd
Symbol
WeakMap
WeakSet

callback, promise, async/await周り

過去にまとめた記事とgithubリポジトリを参照のこと。

mktmkt.hatenablog.com

github.com

プロトタイプチェーンとES6クラス

Classはプロトタイプを用いたいわゆるPseudoclassical Instantiationの糖衣構文だということ。

class Animal {
  constructor(name, energy) {
    this.name = name;
    this.energy = energy;
  }

  eat(amount) {
    console.log(`${this.name} is eating.`);
    this.energy += amount;
  }

  sleep(length) {
    console.log(`${this.name} is sleeping.`);
    this.energy += length;
  }

  play(length) {
    console.log(`${this.name} is playing.`);
    this.energy -= length;
  }

  static nextToEat(animals) {
    const sortedByLeastEnergy = animals.sort((a, b) => a.energy - b.energy);

    return sortedByLeastEnergy[0].name;
  }
}

結局のところはES5である以下の書き方と同じもの。

function Animal(name, energy) {
  this.name = name;
  this.energy = energy;
}

Animal.prototype.eat = function(amount) {
  console.log(`${this.name} is eating.`);
  this.energy += amount;
};

Animal.prototype.sleep = function(length) {
  console.log(`${this.name} is sleeping.`);
  this.energy += length;
};

Animal.prototype.play = function(length) {
  console.log(`${this.name} is playing.`);
  this.energy -= length;
};

Animal.nextToEat = function(animals) {
  const sortedByLeastEnergy = animals.sort((a, b) => a.energy - b.energy);

  return sortedByLeastEnergy[0].name;
};

インスタンスの作成

newキーワードで行う。implicitな処理が行われる。

const leo = new Animal('Leo', 7);

newすることによってfunction prototypeの中にthisという空オブジェクトを作成し、それをリターンするということを裏で行っている。

for-in文は副作用を生む

プロトタイプチェーンとES6クラスで述べた例に被せて、次のような構文を実行してみる。

const leo = new Animal('Leo', 7);

for (const key in leo) {
  console.log(`Key: ${key}. Value: ${leo[key]}`);
}

Class構文での実行結果

Key: name. Value: Leo
Key: energy. Value: 7

Prototypeを利用した構文での実行結果

Key: name. Value: Leo
Key: energy. Value: 7
// 副産物
Key: eat. Value: function (amount) {
  console.log(`${this.name} is eating.`);
  this.energy += amount;
}
Key: sleep. Value: function (length) {
  console.log(`${this.name} is sleeping.`);
  this.energy += length;
}
Key: play. Value: function (length) {
  console.log(`${this.name} is playing.`);
  this.energy -= length;
}

この理由は、for-in文が列挙可能なプロパティ(enumerable property)の中身、すなわち継承先のprototypeの中身までイタレートしてしまうから。

回避するために、obj.hasOwnPropertyメソッドを用いることで防げる。 これによって、prototypeに内にあるプロパティのみに絞ることができる。

for (const key in leo) {
  if (leo.hasOwnProperty(key)) {
    console.log(`Key: ${key}. Value: ${leo[key]}`);
  }
}

もっとも、for-in文自体使わない方がよいが。 ちなみに、for-in文はESLintなどで使用禁止(guard-for-in, no-restricted-syntax)にできる。

モジュール化とCommonJS / ESモジュール

モジュール化のメリット

  • ユーザビリティ(Reusability)
    • コードをブロックとして使い回せる
  • コンポーザビリティ(Composability)
    • パーツを構成して一つのプロダクトを作る要領
  • レバレッジ(Levarage)
    • パーツさえ揃っていればどこでどう作られるかは関係ない
  • アイソレーション(Isolation)
    • 独立性を保って各パーツを分けて扱うことで、互いの依存関係やボトルネックを作らない
  • オーガニゼーション(Organization)
    • 各パーツが明確な境界を保つことで、組織が成り立つ

ESモジュールでのimport

import defaultExport from "module-name"; // default import
import * as name from "module-name"; // universal import
import { export } from "module-name"; // named import
import { export as alias } from "module-name";
import { export1 , export2 } from "module-name";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
var promise = import(module-name);

Tree shaking

CommonJSでは、モジュールをファイル内のどこからでも、コンディショナルに読み込みできるが、ESモジュールではファイル最上部にimport文を書くこと以外を許容しない。

CommonJSの例

  • import時はmodule.exports = ...
  • export時はrequire(...)
if (pastTheFold === true) {
  require('./parallax')
}

ESモジュールの例

  • import時はimport
  • export時はexport

ファイル上部以外でimportしようとすると、下記のエラーが出る。 (ESLintのimport/firstルールで検知可能)

import' and 'export' may only appear at the top level

例外的に、ESモジュールでもダイナミックimportを可能にするproposalがある(現在ステージ3まできている)。

React + React Routerなどで使用する場合、ダイナミックimportをするコンポーネントを自作できる。

tylermcginnis.com

ReactのpropTypesやdefault propsの書き方、メソッドのバインド方法

Reactでの書き方だが、propTypesやdefault propsはclass内にまとめて書いてしまった方が簡潔で見やすい。

Class内に閉じ込めて書く

import React, { Component } from 'react';
import PropTypes from 'prop-types';

class UserInput extends Component {
  static propTypes = {
    username: PropTypes.string,
  };

  static defaultProps = {
    username: 'John Doe',
  };

  constructor(props) {
    super(props);
    const { username } = props;

    this.state = {
      username,
    };

    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(event) {
    this.setState({
      username: event.target.value,
    });
  }

  render() {
    const { username } = this.state;

    return (
      <div>
        <h2>{username}</h2>
      </div>
    );
  }
}

export default UserInput;

メソッドのバインドの仕方によるコンパイルの違い

class Animal {
  eat() {}
  sleep = () => {}
}

// Compiled to

function Animal () {
  this.sleep = function () {}
}

Animal.prototype.eat = function () {}

React hooksで全文表示

案件などでも使えそうな実装だったので、メモがてらブログ記事に残しておく。

例えば、何かしらの記事一覧を取得してきて、3点リーダーで省略した文一覧として表示してから「もっと見る」ボタンで表示するなど、様々な応用例が考えられる。

index.js (エントリーポイント)

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './styles/index.css';

const mountNode = document.getElementById('app');

ReactDOM.render(
  <App
    posts={[
      {
        id: 0,
        img: 'https://picsum.photos/seed/picsum/300/200',
        text:
          'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.',
      },
      {
        id: 1,
        img: 'https://picsum.photos/seed/picsum/300/200',
        text:
          'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Malesuada fames ac turpis egestas sed tempus urna. Id consectetur purus ut faucibus pulvinar elementum integer enim neque. Sit amet risus nullam eget felis eget nunc lobortis.',
      },
      {
        id: 2,
        img: 'https://picsum.photos/seed/picsum/300/200',
        text:
          'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Posuere ac ut consequat semper. At erat pellentesque adipiscing commodo elit at imperdiet. Arcu cursus euismod quis viverra nibh cras pulvinar mattis.',
      },
    ]}
  />,
  mountNode
);

App.js (コンポーネント)

import React, { useState } from 'react';
import PropTypes from 'prop-types';

function App({ posts }) {
  const byteCut = (byte, text) => text.slice(0, byte);
  const [newPosts, setNewPosts] = useState(
    posts.map(post => ({
      id: post.id,
      img: post.img,
      text: post.text,
      excerpt: byteCut(100, post.text),
      showMore: false,
    }))
  );
  const showMore = index => {
    setNewPosts(
      newPosts.map((newPost, i) => ({ ...newPost, showMore: index === i }))
    );
  };

  return (
    <div>
      {newPosts.map((post, index) => (
        <section className="mb-8" key={post.id}>
          <img src={post.img} alt="" />
          <p>{!post.showMore ? `${post.excerpt}...` : post.text}</p>
          {post.showMore || (
            <button type="button" onClick={() => showMore(index)}>
              Open
            </button>
          )}
        </section>
      ))}
    </div>
  );
}

App.propTypes = {
  posts: PropTypes.arrayOf(
    PropTypes.exact({
      id: PropTypes.number,
      img: PropTypes.string,
      text: PropTypes.string,
    })
  ),
};

export default App;

React hooks入門からToDoアプリまで

今回、React hooksによるToDoアプリを作って、従来のclassコンポーネントによる書き方と比べて、そのように書き方が変わったかというところを検証してみた。

結果からいうと、React hooksを導入して書いた方が圧倒的に書きやすかった。 コードがはるかにリーダブルな他に、書いたコードの行数も30行少ないものだった。

github.com

なぜReact hooksが導入されたか

  • thisがわずらわしかった
  • super(props)もわずらわしかった
    • stateをclass fieldとして書くことで解決はしていたものの
  • Lifecycleメソッド内で同じロジックを書くのがわずらわしかった
  • ロジックとUIを分離するのが難しかった
    • Higher Order Comopnents (HOC)
    • Render Props

ToDoアプリ実装例

Class components

まず先ず、stateフィールドをconstructor外でclass fieldとして書く際に、babelプラグインが必要となる。

.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
      }
    ],
    "@babel/preset-react"
  ],
  "plugins": ["@babel/plugin-proposal-class-properties"]
}

従来の書き方だとthisによる結びつきが多く、コードが読みづらい。 stateの展開時やコンポーネント内のメソッドを参照する際にいちいちthisを書かないといけない。 この不便さはReact hooks使用時に気づいた。

TodoLegacy.js

import React, { Component } from 'react';
import PropTypes from 'prop-types';

function TodoItems({ items, checkFunc, isDoneItems }) {
  const generateId = () =>
    Math.random()
      .toString(36)
      .substr(2, 9);

  return (
    <ul className="pl-0 list-none">
      {items.map((item, index) => (
        <li key={`${item}-${generateId}`}>
          <label htmlFor={`${item}-${index}`}>
            <input
              type="checkbox"
              id={`${item}-${index}`}
              onChange={e => checkFunc(e, item, isDoneItems)}
            />
            <span className={isDoneItems && 'line-through'}>{item}</span>
          </label>
        </li>
      ))}
    </ul>
  );
}

TodoItems.propTypes = {
  items: PropTypes.array,
  checkFunc: PropTypes.func,
  isDoneItems: PropTypes.bool,
};

class TodoLegacy extends Component {
  state = {
    inputValue: '',
    todoItems: ['apple', 'banana', 'clementine'],
    doneTodoItems: [],
  };

  changeInputValue = e => {
    this.setState({
      inputValue: e.target.value,
    });
  };

  addItem = e => {
    if (e.keyCode !== 13) return;
    const { todoItems } = this.state;
    this.setState({
      inputValue: '',
      todoItems: [...todoItems, e.target.value],
    });
  };

  checkItem = (e, val, isDoneItems) => {
    if (!e.target.checked) return;
    const { todoItems, doneTodoItems } = this.state;
    const leftTodoItems = [...todoItems].filter(todoItem => todoItem !== val);
    const checkedTodoItems = [...todoItems].find(todoItem => todoItem === val);
    const leftDoneTodoItems = [...doneTodoItems].filter(
      doneTodoItem => doneTodoItem !== val
    );
    const checkedDoneTodoItems = [...doneTodoItems].find(
      doneTodoItem => doneTodoItem === val
    );
    setTimeout(() => {
      if (!isDoneItems) {
        this.setState({
          todoItems: [...leftTodoItems],
          doneTodoItems: [...doneTodoItems, checkedTodoItems],
        });
      } else {
        this.setState({
          todoItems: [...todoItems, checkedDoneTodoItems],
          doneTodoItems: [...leftDoneTodoItems],
        });
      }
    }, 400);
  };

  render() {
    const { inputValue, todoItems, doneTodoItems } = this.state;

    return (
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={this.changeInputValue}
          onKeyDown={e => this.addItem(e)}
        />
        <section>
          <h3>ToDo</h3>
          <TodoItems items={todoItems} checkFunc={this.checkItem} />
        </section>
        {doneTodoItems[0] && (
          <section>
            <h3>Done</h3>
            <TodoItems
              items={doneTodoItems}
              checkFunc={this.checkItem}
              isDoneItems
            />
          </section>
        )}
      </div>
    );
  }
}

export default TodoLegacy;

Functional components with react hooks

React hooksを用いた書き方ではthisを使う必要が一切なかった。thisの縛りから解放されたのに驚いた。 また状態の更新時に従来のように、複数行にかけてthis.setState({ key: 'value' })と書く必要がなく、一つひとつのstateに個別で作ったカスタムのsetterで更新ができるのもよい。

Todo.js

import React, { useState } from 'react';
import PropTypes from 'prop-types';

function TodoItems({ items, checkFunc, isDoneItems }) {
  const generateId = () =>
    Math.random()
      .toString(36)
      .substr(2, 9);

  return (
    <ul className="pl-0 list-none">
      {items.map((item, index) => (
        <li key={`${item}-${generateId}`}>
          <label htmlFor={`${item}-${index}`}>
            <input
              type="checkbox"
              id={`${item}-${index}`}
              onChange={e => checkFunc(e, item, isDoneItems)}
            />
            <span className={isDoneItems && 'line-through'}>{item}</span>
          </label>
        </li>
      ))}
    </ul>
  );
}

TodoItems.propTypes = {
  items: PropTypes.array,
  checkFunc: PropTypes.func,
  isDoneItems: PropTypes.bool,
};

function Todo() {
  const [inputValue, setInputValue] = useState('');
  const [todoItems, setTodoItems] = useState(['apple', 'banana', 'clementine']);
  const [doneTodoItems, setDoneTodoItems] = useState([]);

  const changeInputValue = e => {
    setInputValue(e.target.value);
  };

  const addItem = e => {
    if (e.keyCode !== 13) return;
    setInputValue('');
    setTodoItems([...todoItems, e.target.value]);
  };

  const checkItem = (e, val, isDoneItems) => {
    if (!e.target.checked) return;
    const leftTodoItems = [...todoItems].filter(todoItem => todoItem !== val);
    const checkedTodoItems = [...todoItems].find(todoItem => todoItem === val);
    const leftDoneTodoItems = [...doneTodoItems].filter(
      doneTodoItem => doneTodoItem !== val
    );
    const checkedDoneTodoItems = [...doneTodoItems].find(
      doneTodoItem => doneTodoItem === val
    );
    setTimeout(() => {
      if (!isDoneItems) {
        setTodoItems([...leftTodoItems]);
        setDoneTodoItems([...doneTodoItems, checkedTodoItems]);
      } else {
        setTodoItems([...todoItems, checkedDoneTodoItems]);
        setDoneTodoItems([...leftDoneTodoItems]);
      }
    }, 400);
  };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={changeInputValue}
        onKeyDown={e => addItem(e)}
      />
      <section>
        <h3>ToDo</h3>
        <TodoItems items={todoItems} checkFunc={checkItem} />
      </section>
      {doneTodoItems[0] && (
        <section>
          <h3>Done</h3>
          <TodoItems items={doneTodoItems} checkFunc={checkItem} isDoneItems />
        </section>
      )}
    </div>
  );
}

export default Todo;

async/await再入門

asyncファンクション

asyncファンクションが通常のファンクションと何が違うかは、以下の2つの例を見ればわかる。

function hello(ms) {
  return new Promise(resolve => setTimeout(() => {
    resolve('Hello from the other side!')
  }, ms))
}

console.log(hello(1000))
async function hello(ms) {
  return new Promise(resolve => setTimeout(() => {
    resolve('Hello from the other side!')
  }, ms))
}

console.log(hello(1000))

結果はasyncを付けても付けなくても変わらず、ともにpromiseをreturnする。つまり、promise返すファンクションでは、asyncは何も作用しない。

では、ほとんどのpromiseを返さないファンクションではどうか?

function say(message) {
  return message
}

console.log(say('Greeting!'))
async function say(message) {
  return message
}

console.log(say('Greeting!'))

通常のファンクションは当然ながらGreeting!を返却するが、asyncファンクションはpromiseオブジェクトを返す。

つまり、通常のファクションにasyncを足すと、promiseオブジェクトにwrapされるかたちで返却される。

await演算子

awaitが使われるのはasyncファンクションの中だけである。

awaitの役割は、promiseを渡されたときにそれがresolveするまで処理を止めることだ。これはpromise.then()と似ているが、awaitの場合は別のファンクションを用意して処理結果をハンドルする必要がなく、ただ結果が返されるというシンプルなものだ。

それでは、promiseでないものに対してawaitを記述するとどうなるかというと、non-promiseにも使えるものの、promise.then()のように、他のスクリプトが処理された後に処理される。

document.querySelector('.btn').addEventListener('click', async () => {
  const user = await getUser('mrsung')
  const weather = await getWeather(user.location)

  updateUI({
    user,
    weather
  })
})

エラーハンドリング

async/awaitを記述した際にエラーをどのようにハンドリングするのかというと、try/catchブロックで処理するのが通例である。

document.querySelector('.btn').addEventListener('click', async () => {
  try {
    const user = await getUser('mrsung')
    const weather = await getWeather(user.location)

    updateUI({
      user,
      weather
    })
  } catch (e) {
    showError(e)
  }
})

Promise再入門(3)

Promiseを組み合わせる

JavaScriptはPromiseの組み合わせのために、Promise.all()Promise.race()という2つのビルトイファンクションが用意されている。

Promise.all()

Promise.all()はpromiseからなる配列を組み合わせて、それらの処理結果を含むかたちで一つのpromiseを返す。

const promises = [
  Promise.resolve('All the promises that I made'),
  Promise.resolve('is going to be'),
  Promise.resolve('resolved!')
]
const combinedPromises = Promise.all(promises)

combinedPromises.then(outcomes => {
  for (let i = 0; i < promises.length; i += 1) {
    console.log(outcomes[i])
  }
})

しかしこの例が上手くいくのは各々のPromiseが成功した場合であり、配列の中のpromiseの一つがrejectされると、上記のcombinedPromisesrejectされる。

そこでできるのが下記の方法

  • promise.then()を複数回使用する方法
  • Promise.all()にpromiseを複数回渡す
  • 上記2つのコンビネーションを行う

これらの方法も踏まえて、書いたスクリプトが以下。

github.com