はじめに
JavaScript では非同期処理の書き方がたくさんあります。
ベストな書き方を知りたくないですか?
過去にどんな書き方がされてきたのか、これからどんな書き方が提案されていくのかをまとめてみました!
※ 本記事で紹介する手法やライブラリは JavaScript の長大な歴史の一部です。
(全部はとてもまとめきれなかった)
※ また、記載しているコードは引用元から抜粋している部分的なコードのため動作を保証するものではありません。
これまでの非同期処理
ES5 以前の世界では...
非同期処理の結果を待って次の処理を行う場合にはコールバック関数を書くことが大半でした。
このコールバック関数には "エラーファーストコールバック" という書き方のルールがあります。
言語仕様には用意されていない処理を言わばイディオム的な書き方で克服していました。
しかし、コールバック関数が連鎖する場合、ネストがどんどん深くなっていて
↓ のようになってしまいます。
function get(file, callback) {
console.log('file: %s...', file);
setTimeout(function () {
console.log('file: %s complete', file);
callback(null, '(' + file + ')');
}, 200 + Math.random() * 100);
}
get('a.txt', function (err, a) {
get('b.txt', function (err, b) {
get('c.txt', function (err, c) {
console.log(a + b + c);
});
});
});
コード: https://qiita.com/LightSpeedC/items/7980a6e790d6cb2d6dad
コールバック地獄と呼ばれるものです。
これを解決するために async.js といったライブラリが使われていました。
※ IE11 では後述の仕様をサポートしていないため、現在もコールバック関数のイディオムを用いたり、 async.js といったライブラリを用いてコードで開発することはあります。
ES2015 の登場
ES5 以前はイディオムとしてコールバック関数を書いていましたが、 Promise が使えるようになることで、統一的なインターフェースで書けるようになりました。
myPromise
.then(handleResolvedA)
.then(handleResolvedB)
.then(handleResolvedC)
.catch(handleRejectedAny);
コード: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise
Generator を使った書き方
ES2015 で追加された仕様は他にもあります。
知る人ぞ知る Generator です。
function
の後の *
(アスタリスク) や、yield
など見慣れない書き方となっていますが、これも JavaScript です。
Generator 自体は同期処理ですが、Promise と組み合わせることでメソッドチェーンを書くことなく非同期処理を同期的に書くことができます。
(後述する async/await と似た書き味があります。)
function sleep(g) {
setTimeout(function() {
g.next('I woke up at ' + new Date());
}, 1000);
}
var g = (function *() {
console.log(yield sleep(g));
console.log(yield sleep(g));
console.log(yield sleep(g));
})();
g.next();
コード: https://qiita.com/hitsujiwool/items/316f3e8a41fb7dc3a119
現在の非同期処理
※ 「現在」とは言ったものの、 前述のコールバック関数のイディオムや Promise を使ったり、組み合わせたりすることはあります。
ES2017 以降
async function も Generator × Promise と同様にメソッドチェーンを書かずに非同期処理を同期的に書くことができます。
Generator × Promise よりも直感的ですね。
async function sequentialStart() {
console.log('==SEQUENTIAL START==')
// 1. これは即時実行される
const slow = await resolveAfter2Seconds()
console.log(slow) // 2. これは 1. の 2 秒後に実行される
const fast = await resolveAfter1Second()
console.log(fast) // 3. これは 1. の 3 秒後に実行される
}
コード: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function
書きやすくなった反面、並列処理でいいはずなのに同期的に書いてしまって、非同期処理が不要に時間を食ってしまうようなコードも書きやすくなりました。
適宜 Promise.all()
などをつかいましょう。
React での非同期処理
このあたりから React を使うプロダクトが増えてきたので、 React での非同期処理の変遷も見ていきます。
コンポーネントのライフサイクルで処理する
クラスコンポーネントであっても関数コンポーネントであっても React のライフサイクルを利用して非同期処理を書いていました。
componentDidMount
や useEffect
で処理するというものです。
↓クラスコンポーネント
class App extends React.Component {
constructor(props) {
super(props);
this.state = { hits: [] };
}
async componentDidMount() {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
this.setState({ hits: result.data });
});
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
↓関数コンポーネント
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
コード: https://www.robinwieruch.de/react-hooks-fetch-data
これを書いているうちに、コンポーネントはどんどん肥大化していき、責務も増えていきます。
Redux の登場
肥大化したコンポーネントを見てうんざりし、データ管理は Redux に任せる時代に突入しました。
Redux の思想は、ロジックはコンポーネントに詰め込むのではなく、中央に集約するというもの。
非同期の場合は、 middleware で処理することが一般的でした。
middleware ライブラリとしては redux-thunk や redux-saga などがあります。
export default function todosReducer(state = initialState, action) {
// omit reducer logic
}
// Thunk function
export async function fetchTodos(dispatch, getState) {
const response = await client.get('/fakeApi/todos')
dispatch({ type: 'todos/todosLoaded', payload: response.todos })
}
コード: https://redux.js.org/tutorials/fundamentals/part-6-async-logic
Redux で非同期処理を管理しようとすると、store が肥大化してコード量も多くなりました。
hooks の登場
hooks が登場したことで、また違う形になります。
データや loading、 error など非同期周りの状態はカスタム hooks で管理するようになりつつあります。
export const useFoo = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const items = useSelector(state=> state.foo.items)
const dispatch = useDispatch<Dispatch<FooAction>>()
const getFoo = useCallback(async () => {
setLoading(true)
try {
const result = await fetch('/getFoo')
setLoading(false)
dispatch({type: 'FOO_SUCCESS', result})
} catch (e) {
setLoading(false)
setError(e.message)
}
}, [loading, error, items])
return [items, getFoo, loading, error]
}
コード: https://qiita.com/Naturalclar/items/6157d0b031bbb00b3c73
hooks 化を後押しするライブラリ
非同期処理の hooks 化を後押しするライブラリが続々でてきています。
React Query や useSWR、Apollo、Redux を使い続けるなら redux-toolkit の RTK Query などもあります。
いずれにしても、非同期処理自体はカスタム hooks に隠蔽し、データ、loading、error などを提供しています。
import { QueryClient, QueryClientProvider, useQuery } from 'react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const { isLoading, error, data } = useQuery('repoData', () =>
fetch('https://api.github.com/repos/tannerlinsley/react-query').then(res =>
res.json()
)
)
if (isLoading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{' '}
<strong>✨ {data.stargazers_count}</strong>{' '}
<strong>🍴 {data.forks_count}</strong>
</div>
)
}
コード: https://react-query.tanstack.com/overview
これからの非同期処理
非同期処理はよりコンポーネントの内部へ入っていきます。
React は現在バージョン17が正式リリースされています。
<Suspense>
は 16.6 から導入されているコンポーネントですが、安定版としてのリリースはまだまだ先になりそうなので未来の話として書きます。
<Suspense>
コンポーネントに非同期処理を持ったコンポーネントを入れることで、コンポーネント自体が非同期処理をしていなくても、今までやっていた事が実現できます。
非同期処理は、もはや React が提供するコンポーネントの内部へと隠蔽されています。
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails() {
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
// Try to read posts, although they might not have loaded yet
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
コード: https://ja.reactjs.org/docs/concurrent-mode-suspense.html
クライアント側のコードとしては async / await を書かなくなる時代が来るかもしれません。
ですが、書かなくていいからと言って分かってなくていいという訳ではないと思います。
Suspense コンポーネントを使う場合であっても非同期処理のことはついて回るはずです。
非同期処理を理解していないと使いこなすことは難しいのではと思います。
終わりに
振り返ってみると、長いコード、難しいコード、読みづらいコードが言語やフレームワーク、ライブラリによって隠蔽されてきたことが分かります。
ビギナーが書きやすくなればなる程、深い理解への道のりは長くなると思います。
今回は JS、非同期、React で切り取ってみましたが、言語、処理形態、フレームワークが違えばそれぞれの歴史があるはずで、それはそれは膨大です。
一人で学ぶには限界があります。
みんなで知識を共有しあって効率的に吸収することが大事ですな〜
参考記事
https://techblog.yahoo.co.jp/programming/js_callback/
https://jsprimer.net/basic/async/