آموزش ری اکت (React) — کامل و رایگان | از صفر تا صد
مقالهای که پیش رو دارید مروری جامع در مورد React است که گرچه ادعا ندارد همه موارد مربوط به این فریمورک را در خود گنجانده، اما مبانی مقدماتی مورد نیاز برای این که به یک توسعهدهنده خبره React تبدیل شوید را در اختیار شما قرار خواهد داد. این مجموعه مطلب در 8 فصل ارائه شده است که فصل نخست آن شامل مباحث زیر میشود:
- متغیرها
- تابعهای Arrow
- Rest و spread
- تخریب شیء و آرایه
- الفاظ قالبی (template literals)
- کلاسها
- Callback-ها
- promise-ها
- Async/Await
- ماژولهای ES
توجه داشته باشید که آنچه در ادامه آمده است تنها فصل اول این مجموعه مطلب را شامل میشود و برای مطالعه فصول بعدی باید به بخشهای آتی این سری مقالات مراجعه کنید.
مقدمهای بر یک کتابخانه View به نام React
در این بخش React و ماهیت آن را توضیح میدهیم.
React چیست؟
React یک کتابخانه جاوا اسکریپت است که هدف آن سادهسازی مراحل توسعه رابطهای بصری است. React از سوی فیسبوک توسعه یافته است و در سال 2013 انتشاریافته است. در واقع React زیرساخت تشکیل دهنده بسیاری اپلیکیشنهای پر استفاده از قبیل فیسبوک و اینستاگرام را تشکیل میدهد.
هدف اولیه React این بوده است ساخت یک رابط و حالت آن را در هر زمان با تقسیم کردن رابط و حالت آن در هر زمان به کاری آسان تبدیل کند.
پیش از ادامه این مبحث لازم است یادآور شویم که میتوانید ری اکت را با استفاده از مجموعه آموزش ری اکت، مقدماتی تا پیشرفته فرادرس یاد بگیرید.
چرا React چنین محبوب است؟
React به طوری طوفانی دنیای توسعه وب فرانتاند را تسخیر کرده است. برای این وضعیت چند دلیل میتوان برشمرد.
پیچیدگی آن کمتر از جایگزینهای دیگر است
در هنگامی که React انتشار یافت، Ember.js و Angular 1.x گزینههای غالب فریمورکها محسوب میشوند. هر دو این موارد چنان دستکاریهایی در کد میکنند که پورت کردن یک اپلیکیشن موجود به کار دشواری تبدیل میشود.
در مقابل React طوری طراحی شده که ادغام آن در یک پروژه از قبل موجود آسان باشد، چون از ابتدا بدین صورت طراحی شده بود که بتواند در کد از قبل موجود فیسبوک ادغام شود. ضمناً دو فریمورک دیگر بسیار سنگین بودند، در حالی که React به جای مجموعه کامل MVC صرفاً یک لایه View را عرضه میکرد.
زمانبندی عالی
در برههای از زمان نسخه دوم انگولار (Angular 2.x) از سوی گوگل معرفی شد که با نسخههای قبل تطبیق نمییافت و تغییرات زیادی در آن رخ داده بود. حرکت از انگولار 1 به 2 مانند رفتن به یک فریمورک جدید بود. همچنین بهبود سرعت اجرایی که React نوید میداد، موجب شد که اغلب توسعهدهندگان React را انتخاب کنند.
پشتیبانی از سوی فیسبوک
این که یک فریمورک از سوی شرکت بزرگی مانند فیسبوک پشتیبانی شود به معنی پایدار بودن و موفقیت بیشتر آن است. فیسبوک در حال حاضر علاقه زیادی به React دارد، ارزش آن را در اوپنسورس بودن میداند و این به علاوه انرژی همه توسعهدهندگانی که از آن در پروژههایشان استفاده میکنند، موجب موفقیت React شده است.
آیا یادگیری React آسان است؟
گرچه گفتیم که React نسبت به جایگزینهای خود سادهتر است؛ اما استفاده عملی از آن همچنان پیچیده است؛ با این حال، اغلب پیچیدگی React در فناوریهای جانبی که در آن ادغام میشوند مانند Redux و GraphQL است.
React خودش یک API بسیار کوچک دارد. اساساً برای آغاز کار با آن به درک چهار مفهوم زیر نیاز دارید:
- کامپوننتها
- JSX
- حالت
- Props
همه این مفاهیم و موارد بیشتر در این مجموعه مطلب توضیح داده شدهاند.
چگونه React را روی رایانه محیط توسعه خود نصب کنیم؟
React یک کتابخانه است و از این رو شاید استفاده از واژه نصب در مورد آن کمی عجیب باشد. شاید راهاندازی کلمه بهتری باشد؛ در هر حال ما با مفاهیم کار داریم و نه عبارتها. روشهای مختلفی برای راهاندازی React برای استفاده روی اپلیکیشن یا وبسایت وجود دارد.
بارگذاری مستقیم React در صفحه وب
سادهترین روش افزودن فایل جاوا اسکریپت React به صفحه به صورت مستقیم است. این روش در مواردی که اپلیکیشن React با عناصر موجود روی یک صفحه منفرد تعامل خواهد داشت و به صورت مستقیم کل جنبه ناوبری را کنترل نمیکند بهترین گزینه محسوب میشود. در این حالت 2 تگ اسکریپت به تگ body اضافه میکنیم:
1<html>
2 ...
3 <body>
4 ...
5 <script
6 src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.development.js"
7 crossorigin
8 ></script>
9 <script
10 src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"
11 crossorigin
12 ></script>
13 </body>
14</html>
نسخه 16.7.0-alpha.2 در لینکها به جدیدترین نسخه ریاکت در زمان نگارش این مجموعه مطلب یعنی 16.7 آلفا اشاره میکند که در آن قابلیت Hook ارائه شده است. در صورتی که نسخه جدیدتری وجود دارد باید از آن استفاده کنید.
بدین ترتیب React و React DOM را بارگذاری میکنیم. اما شاید از خود بپرسید چرا 2 کتابخانه بارگذاری شده است؟ چون React 100% مستقل از مرورگر عمل میکند و میتواند خارج از آن (برای مثال روی دستگاههای موبایل با استفاده از React Native) نیز فعال باشد. از این رو باید React Dom نیز بارگذاری شود تا پوششهای مناسب مرورگر را اضافه کند.
پس از این که آن تگها اشاره شد میتوانید فایلهای جاوا اسکریپت خود را که از React استفاده میکنند بارگذاری کنید و یا این که کد جاوا اسکریپت را در یک تگ Script به صورت inline درج کنید:
1<script src="app.js"></script>
2
3<!-- or -->
4
5<script>
6 //my app
7</script>
برای استفاده از JSX باید یک مرحله دیگر نیز طی کنید که مرحله بارگذاری Babel است:
1 <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
بدین ترتیب اسکریپتها با نوع MIME خاص text/babel بارگذاری میشوند:
<script src="app.js" type="text/babel"></script>
اینک میتوانید JSX را به فایل app.js خود اضافه کنید:
1const Button = () => {
2 return <button>Click me!</button>
3}
4
5ReactDOM.render(<Button />, document.getElementById('root'))
راه اندازی ریاکت به این روش برای ساخت پروتوتایپ مناسب است، زیرا امکان آغاز سریع بدون نیاز به راهاندازی گردش کار کامل را فراهم میسازد.
چگونه از create-react-app استفاده کنیم؟
create-react-app یک پروژه است که هدف آن ایجاد امکان راهاندازی پروژههای ریاکت در کمترین زمان ممکن است و هر اپلیکیشن create-react-app که بیشتر از یک صفحه داشته باشد، create-react-app این نیاز را رفع میکند.
در ابتدا از npx استفاده میکنیم که یک روش آسان برای دانلود و اجرای دستورهای Node.js بدون نصب آنها است. npx به همراه npm عرضه میشود و اگر npm را روی سیستم نصب ندارید، باید همین الان آن را از آدرس (+) نصب کنید.
اگر مطمئن نیستید که کدام نسخه از npm را نصب کنید، باید دستور npm –v را اجرا کنید تا نیاز بهروزرسانی را بررسی کنید.
زمانی که دستور <npx create-react-app <app-name را اجرا بکنید، npx شروع به دانلود جدیدترین نسخه create-react-app کرده، آن را اجرا و سپس از روی سیستم حذف میکند. این وضعیت عالی است، زیرا شما هرگز نسخه قدیمی از آن روی سیستم خود نخواهید داشت و هر بار که آن را اجرا کنید، جدیدترین و بهترین کد ممکن را دریافت میکنید. با استفاده از دستور زیر آن را اجرا کنید:
npx create-react-app todolist
زمانی که اجرای آن پایان یابد با تصویر زیر مواجه میشوید:
create-react-app یک ساختار فایل در پوشهای که تعیین کردهاید (در این مثال totolist) ایجاد میکند و یک ریپازیتوری Git نیز مقداردهی میکند.
همچنین چند دستور را در فایل اضافه میکند به طوری که میتوانید بیدرنگ اپلیکیشن خود را با رفتن به پوشه مربوطه و اجرای دستور npm start آغاز کنید:
create-react-app علاوه بر npm start، چند دستور دیگر نیز اضافه میکند:
- npm run build – برای ساختن فایلهای اپلیکیشن React در پوشه build، که آماده انتشار روی سرور است.
- npm test – برای اجرای مجموعه تست با استفاده از Jest
- Npm eject – برای خارج شدن از create-react-app
خارج شدن یا eject به حالتی گفته میشود که تشخیص میدهید create-react-app وظیفه خود را انجام داده است، اما میخواهید کارهای بیشتری از آنچه create-react-app مجاز است انجام دهید. از آنجا که create-react-app مجموعهای از قراردادهای مشترک و مقدار کمی از گزینههای اختیاری است، این احتمال وجود دارد که در مواردی به چیز خاصی نیاز داشته باشید که از ظرفیتهای create-react-app فراتر میرود.
بدین ترتیب زمانی که eject میکنید، امکان بهروزرسانی خودکار را از دست میدهید؛ اما میتوانید با استفاده از پیکربندی Balel و Webpack انعطافپذیری بیشتری به دست آورید.
باید بدانید که عمل eject برگشتناپذیر است. شما 2 پوشه در دایرکتوری اپلیکیشن خود به نامهای config و scripts به دست میآورید. این پوشهها شامل پیکربندیها هستند و اینک میتوانید شروع به ویرایش آنها بکنید.
اگر از قبل یک اپلیکیشن ریاکت داشته باشید که با استفاده از نسخههای قدیمی ریاکت ساخته شده است، ابتدا باید نسخه آن را با استفاده از (console.log(React.version در اپلیکیشن خود تست کنید و سپس میتوانید با اجرای دستور yarn add react@16.7 آن را بهروزرسانی کنید. بدین ترتیب yarn به شما هشدار میدهد که باید بهروزرسانی کنید. دقت کنید که عدد نسخه مورد نظرتان باید جدیدترین نسخه باشد.
CodeSandbox
یک روش آسان برای این که ساختار create-react-app را بدون نصب کردن آن به دست بیاورید این است که به آدرس https://codesandbox.io/s بروید و گزینه React را انتخاب کنید.
CodeSandbox روشی عالی برای آغاز یک پروژه ریاکت بدون نیاز به نصب محلی است.
Codepen
Codepen نیز یک گزینه عالی محسوب میشود. شما میتوانید از پروژه استارتر Codepen که از قبل با React پیکربندی شده است و از hook ها نیز پشتیبانی میکند استفاده کنید.
Pen-های Codepen برای پروژههای سریع با یک فایل جاوا اسکریپت عالی هستند؛ در حالی که project-ها برای پروژههای با چندین فایل مانند آنهایی که در اغلب موارد هنگام ساخت اپلیکیشنهای ریاکت استفاده میکنیم مناسب هستند.
نکتهای که باید اشاره کنیم این است که Codepen به دلیل طرز کار درونی خود نیازی به استفاده ساختار import برای ماژولهای معمولی ES ندارد، بلکه میتوان برای مثال useState را با استفاده از دستور زیر ایمپورت کرد:
const { useState } = React
و نه دستور زیر:
import { useState } from 'react'
بخش اول: مفاهیم اساسی جاوا اسکریپت مدرن برای استفاده از React
اینک به فصل اول راهنمای React رسیدهایم. با ما همراه باشید.
آیا قبل از یادگیری عملی React باید چیزی را بیاموزیم؟
اگر قصد دارید ریاکت را بیاموزید ابتدا باید چند چیز را یاد گرفته باشید. برخی فناوریهای پیشنیاز وجود دارند که باید با آنها آشنا باشید و از آن جمله برخی از موارد مرتبط با ویژگیهای جدید جاوا اسکریپت که بارها در React مشاهده خواهید کرد.
برخی اوقات فکر میکنند که یک ویژگی خاص از سوی React ارائه شده است؛ در حالی که آن ویژگی بخشی از ساختار جاوا اسکریپت مدرن است. البته لازم نیست در حال حاضر در این زمینه یک خبره باشید؛ اما هر چه در React جلوتر برویم، به یادگیری این موارد بیشتر نیاز خواهید داشت. در ادامه فهرستی از مواردی که به یادگیری سریعتر شما کمک میکند را ارائه کردهایم.
متغیرها
منظور از متغیر، عبارتی است که به یک شناسه انتساب یافته است و از این رو میتوان در موارد آتی در برنامه آن را مورد ارجاع قرار داده و استفاده کرد. متغیرها در جاوا اسکریپت هیچ نوع مشخصی ندارند. میتوان یک نوع به متغیر نسبت داد و سپس میتوان نوع دیگری برای آن تعیین کرد و هیچ خطا یا مشکلی نیز پیش نمیآید.
به همین دلیل است که جاوا اسکریپت در مواردی یک زبان برنامهنویسی «فاقد نوع» (untyped) نامیده میشود. متغیر باید پیش از استفاده، اعلان شود. 2 روش برای انجام این کار وجود دارد که شامل استفاده از کلیدواژههای var ،let یا const است. تفاوت این سه روش در نوع تعامل بعدی با متغیر است.
استفاده از var
تا ES2015 کلیدواژه var تنها سازه موجود برای تعریف کردن متغیرها بود.
1Var a = 0
اگر فراموش کنید از var استفاده کنید، مقدار مورد نظر خود را به یک متغیر اعلان نشده انتساب میدهید و نتایج این کار بسته به موقعیت متفاوت خواهد بود.
در محیطهای مدرن که در آنها حالت strict فعالشده است، در این مورد با خطا مواجه میشوید. در محیطهای قدیمیتر یا وقتی که حالت strict غیرفعال است، این مورد باعث میشود که متغیر مقداردهی شود و به یک شیء سراسری انتساب مییابد.
اگر متغیری را هنگام اعلان کردن، مقداردهی نکنید، دارای مقدار undefined خواهد بود تا این که مقداری به آن انتساب داده شود.
1var a //typeof a === 'undefined'
شما میتوانید بارها و بارها مقدار جدیدی به متغیر انتساب دهید و مقدار قبلی را باطل کنید:
1var a = 1
2var a = 2
همچنین میتوانید متغیرهای چندگانهای را به یکباره در گزاره واحد اعلان کنید:
1var a = 1، b = 2
دامنه (scope) متغیر بخشی از کد است که متغیر در آن قابل مشاهده است.
متغیری که با var خارج از هر تابع مقداردهی شود، به یک شیء سراسری انتساب مییابد و دارای دامنه سراسری است یعنی در همه جای کد قابل مشاهده است. متغیری که با یک var درون یک تابع مقداردهی شود، به آن تابع انتساب مییابد و به صورت محلی در آن و تنها از درون آن قابل مشاهده است و از این حیث مانند یک پارامتر تابع است.
هر متغیری که در یک تابع با همان نام متغیر سراسری تعریف شود، نسبت به متغیر سراسری تقدم مییابد و به سایه آن تبدیل میشود.
باید دقت کنید که یک بلوک (که به وسیله آکولادها مشخص میشود) دامنه جدیدی تعریف نمیکند. دامنه جدید تنها زمانی که یک تابع ایجاد شود تعریف میشود، زیرا var دارای دامنه بلوکی نیست بلکه دامنه تابعی دارد.
درون یک تابع هر متغیری که در آن تعریف شود، در تمام کد درون تابع قابل مشاهده خواهد بود حتی اگر متغیر در انتهای تابع اعلان شود همچنان میتوان در آغاز تابع به آن ارجاع داد، چون جاوا اسکریپت پیش از اجرای تابع همه متغیرها را به ابتدای آن میبرد (این کار hoisting نامیده میشود.) برای جلوگیری از سردرگمی همواره تابعها را در ابتدای یک تابع اعلان کنید.
استفاده از let
Let ویژگی جدیدی است که در ES2015 معرفی شده است. در واقع let یک نسخه دارای دامنه بلوکی از var محسوب میشود. دامنه آن به بلوک، گزاره یا عبارتی که در آن تعریف میشود و همه بلوکهای درونی که داخل آن بلوک قرار دارند، مربوط میشود.
توسعهدهندگان جاوا اسکریپت مدرن، در غالب موارد صرفاً از let استفاده میکند و استفاده از var را کنار گذاردهاند. تعریف کردن let در خارج از هر تابع، برخلاف var باعث ایجاد یک متغیر سراسری نمیشود.
استفاده از const
متغیرهایی که با var و let اعلان شوند، میتوانند در ادامه در برنامه تغییر یابند و مجدداً مقادیری به آنها انتساب یابد. زمانی که یک const مقداردهی بشود، مقدار آن نمیتواند دیگر تغییر یابد و نمیتوان مقدار دیگری به آن انتساب داد.
1const a = 'test'
بدین ترتیب دیگر نمیتوان مقادیر دیگری به const با نام a انتساب داد. با این حال میتوان a را در صورتی که شیئی باشد که متدهایی برای تغییر محتوای خود داشته باشد، تغییر داد.
Const تغییرناپذیری را تضمین نمیکند و صرفاً این اطمینان را میدهد که ارجاع را نمیتوان تغییر داد. Const مانند let دارای دامنه بلوکی است.
توسعهدهندگان جاوا اسکریپت مدرن برای متغیرهایی که نیازی به انتساب مقادیر مجدد در برنامه نخواهند داشت، همواره از const استفاده میکنند. دلیل این مسئله آن است که همواره باید سعی کنیم از ساختارهایی استفاده کنیم که امکان بروز خطا در ادامه را کاهش دهند.
تابعهای Arrow
تابعهای Arrow در ES6 / ECMAScript 2015 معرفی شدهاند و از زمان معرفی خود، روش نمایش و کارکرد کدهای جاوا اسکریپت را برای همیشه تغییر دادهاند. این تغییر چنان مثبت بوده است که دیگر به ندرت استفاده از کلیدواژه function را در کدهای مدرن میبینید.
این ساختار از نظر بصری ساده و خوشایند است و امکان نوشتن تابعهایی با ساختار کوتاهتر را فراهم ساخته است. بدین ترتیب ساختار زیر:
1const myFunction = function() {
2 //...
3}
به حالت زیر تغییر یافته است:
1const myFunction = () => {
2 //...
3}
اگر بدنه تابع تنها شامل یک گزاره باشد، میتوان پرانتزها را حذف کرد و همه آن را در یک خط نوشت:
1const myFunction = () => doSomething()
پارامترها درون پرانتزها ارسال میشوند:
1const myFunction = (param1، param2) => doSomething(param1، param2)
اگر یک پارامتر داشته باشید و این تنها پارامتر شما باشد، میتوانید پرانتزها را به طور کامل حذف کنید:
1const myFunction = param => doSomething(param)
به لطف این ساختار کوتاه، تابعهای Arrow استفاده از تابعهای کوچک را ترویج دادهاند.
بازگشت ضمنی (Implicit Return)
تابعهای Arrow امکان داشتن مقادیر بازگشتی ضمنی را فراهم ساختهاند. این مقادیر بدون استفاده از کلیدواژه return بازگشت مییابند. طرز کار این بازگشت به این صورت است که گزارههای تکخطی در بدنه تابع به صورت زیر تعریف میشوند:
1const myFunction = () => 'test'
2myFunction() //'test'
مثال دیگر زمانی است که یک شیء بازگشت میدهیم. دقت کنید که باید آکولادها را درون پرانتزها قرار دهید تا با پرانتزهای تشکیل دهنده بدنه تابع اشتباه گرفته نشوند:
1const myFunction = () => ({ value: 'test' })
2myFunction() //{value: 'test'}
طرز کار this در تابعهای Arrow
This مفهومی است که شاید درک آن دشوار باشد، چون بسته به زمینه و همچنین بر اساس حالت جاوا اسکریپت (strict یا غیر آن) متفاوت است.
میبایست این نکته را روشن کنیم، زیرا تابعهای Arrow به طرزی کاملاً متفاوت از تابعهای معمولی عمل میکنند. زمانی که یک متد برای یک شیء تعریف میشود، در تابع معمولی this اشاره به شیء دارد و از این رو میتوان کد زیر را داشت:
1const car = {
2 model: 'Fiesta',
3 manufacturer: 'Ford',
4 fullName: function() {
5 return `${this.manufacturer} ${this.model}`
6 }
7}
بدین ترتیب فراخوانی ()car.fullName مقدار "Ford Fiesta" را بازگشت میدهد.
دامنه this در تابعهای Arrow از زمینه اجرایی به ارث میرسید. یک تابع Arrow به هیچ وجه به this اتصال نمییابد و از این رو مقدار آن در پشته فراخوانی جستجو میشود. بدین ترتیب کدی مانند ()car.fullName کار نخواهد کرد و رشته "undefined undefined" را بازگشت میدهد:
1const car = {
2 model: 'Fiesta',
3 manufacturer: 'Ford',
4 fullName: () => {
5 return `${this.manufacturer} ${this.model}`
6 }
7}
به همین دلیل، تابعهای Arrow به عنوان متدهای شیء مناسب نیستند. همچنین تابعهای Arrow نمیتوانند به عنوان سازنده (constructor) استفاده شوند، زیرا در هنگام مقداردهی اولیه یک خطای TypeError ایجاد میشود.
در این موارد باید از تابعهای معمولی استفاده کرد که زمینه دینامیک مورد نیاز نیست. همچنین این مورد در هنگام مدیرت رویدادها، موجب ایجاد مشکل میشود. شنوندههای رویداد DOM سعی میکنند this را روی عنصر هدف تنظیم کنند و اگر در handler رویداد روی this تکیه کنیم، به یک تابع معمولی نیاز خواهیم داشت:
1const link = document.querySelector('#link')
2link.addEventListener('click', () => {
3 // this === window
4})
5
6const link = document.querySelector('#link')
7link.addEventListener('click', function() {
8 // this === link
9})
Rest و Spread
میتوان یک آرایه، یک شیء یا یک رشته را با استفاده از عملگر spread بسط داد. توضیح خود را با مثالی از آرایه آغاز میکنیم. فرض کنید:
1const a = [1، 2، 3]
شما میتوانید یک آرایه جدید با کد زیر بسازید:
1const c = [...a]
این کد برای شیء نیز به خوبی کار میکند. شیء را با دستور زیر کلون کنید:
1const newObj = { ...oldObj }
با استفاده از رشتهها، عملگر spread یک آرایه با هر یک از کاراکترهای رشته ایجاد میکند:
1const hey = 'hey'
2const arrayized = [...hey] // ['h'، 'e'، 'y']
این عملگر برخی کاربردهای بسیار مفید دارد. مهمترین کاربر توانایی استفاده از یک آرایه به عنوان آرگومان تابع به روشی بسیار ساده است:
1const f = (foo، bar) => {}
2const a = [1، 2]
3f(...a)
در گذشته این کار را میتوانستید با استفاده از (f.apply(null، a انجام دهید اما این روش زیبا و خوانا نیست. عنصر rest زمانی مفید است که میخواهیم با تخریب آرایه (array destructuring):
1const numbers = [1، 2، 3، 4، 5]
2[first، second، ...others] = numbers
و spread عناصر کار کنیم:
1const numbers = [1، 2، 3، 4، 5]
2const sum = (a، b، c، d، e) => a + b + c + d + e
3const sum = sum(...numbers)
ES2018 مشخصات rest را معرفی کرده است که همین مفهوم اما در مورد شیء را داراست.
مشخصات Rest:
1const { first, second, ...others } = {
2 first: 1,
3 second: 2,
4 third: 3,
5 fourth: 4,
6 fifth: 5
7}
8
9first // 1
10second // 2
11others // { third: 3, fourth: 4, fifth: 5 }
مشخصات spread امکان ایجاد یک شیء جدید با ترکیب کردن مشخصات شیء ارسالی پس از عملگر spread را میدهد:
1const items = { first, second, ...others }
2items //{ first: 1, second: 2, third: 3, fourth: 4, fifth: 5 }
تخریب شیء و آرایه
با فرض وجود یک شیء میتوان با استفاده از ساختار تخریب، تنها برخی از مقادیر را استخراج کرد و آنها را در «متغیرهای با نام» قرار داد:
1const person = {
2 firstName: 'Tom',
3 lastName: 'Cruise',
4 actor: true,
5 age: 54 //made up
6}
7
8const { firstName: name, age } = person //name: Tom, age: 54
name و age شامل مقادیر مطلوب ما هستند. همین ساختار در مورد آرایهها نیز کار میکند:
1const a = [1، 2، 3، 4، 5]
2const [first، second] = a
این گزاره با دریافت آیتمها از اندیسهای 0، 1 و 4، سه متغیر جدید از آرایه a ایجاد میکند:
1const [first، second،،، fifth] = a
الفاظ قالبی (Template Literals)
الفاظ قالبی ویژگی جدید ES2015 / ES6 است که امکان کار با رشتهها به روشی کاملاً جدید نسبت به ES5 و قبلتر را فراهم ساخته است.
ساختار آن در نگاه اول بسیار ساده است و کافی است به جای گیومههای تکی یا جفتی از علامت backtick (`) استفاده کرد:
1const a_string = `something`
دقت کنید که این الفاظ کاملاً منحصر به فرد هستند، زیرا ویژگیهای زیادی ارائه میکنند که رشتههای معمولی با گیومه ندارند. از آن جمله:
- ساختاری عالی دارند که امکان تعریف رشتههای چندخطی را میدهد.
- روش آسانی برای میانیابی متغیرها و عبارتها در داخل رشتهها در اختیار ما قرار میدهند.
- امکان ایجاد DSL با استفاده از تگهای قالبی را فراهم میسازند. DSL به معنی «زبان خاص یک حوزه» است و برای مثال در React در کامپوننتهای دارای سبک برای تعریف CSS کامپوننتها استفاده میشوند.)
هر کدام از جزییات فوق را در ادامه بررسی میکنیم.
رشتههای چندخطی
تا قبل از ES6 برای ایجاد یک رشته که در دو یا چند خط گسترش مییابد از کاراکتر \ در انتهای هر خط استفاده میکردیم:
1const string =
2 'first part \
3second part'
بدین ترتیب امکان ایجاد یک رشته 2 خطی پدید میآمد؛ اما تنها در یک خط رندر میشد:
first part second part
برای رندر کردن رشته در چند خط باید به صورت زیر صریحاً از \n در انتهای هر خط استفاده میکردیم:
1const string =
2 'first line\n \
3second line'
یا
1const string = 'first line\n' + 'second line'
الفاظ قالبی امکان تعریف رشتههای چندخطی را سادهتر ساختهاند.
زمانی که یک لفظ قالبی با backtick باز شود، میتوانید برای ایجاد یک خط جدید از اینتر استفاده کنید و دیگر نیازی به کاراکترهای خاص نیست و همچنان که دیده میشود رندر خواهد شد:
1const string = `Hey
2this
3
4string
5is awesome!`
به خاطر داشته باشید که در این حالت فاصله معنیدار است. از این رو در کد زیر:
1const string = `First
2 Second`
قصد داریم یک رشته مانند زیر ایجاد کنیم:
1First
2 Second
یک روش ساده برای اصلاح این مشکل آن است که یک خط خالی داشته باشیم و متد ()trim را درست پس از backtick پایانی قرار دهیم تا هر فاصلهای را پیش از کاراکتر اول حذف کند:
1const string = `
2First
3Second`.trim()
میانیابی
الفاظ قالبی یک روش آسان برای میانیابی متغیرها و عبارتهای درون رشتهها ارائه کردهاند.
این کار با استفاده از ساختار {...}$ ممکن است:
1const var = 'test'
2const string = `something ${var}` //something test
درون {}$ میتوان هر چیزی حتی عبارتها را اضافه کرد:
1const string = `something ${1 + 2 + 3}`
2const string2 = `something ${foo() ? 'x' : 'y'}`
کلاسها
در سال 2015 و با معرفی استاندارد ECMAScript 6 یا ES6، مفهوم کلاس معرفی شد.
جاوا اسکریپت یک روش کاملاً نامتداول برای پیادهسازی وراثت دارد که وراثت پروتوتایپی نامیده میشود. وراثت پروتوتایپی با این که شاید چنان بد نباشد؛ اما برخلاف اغلب پیادهسازیهای وراثت از سوی زبانهای برنامهنویسی محبوب تعریف میشود که مبتنی بر کلاس هستند.
افرادی که با سابقه یادگیری زبانهای برنامهنویسی جاوا یا پایتون یا دیگر زبانها به سراغ جاوا اسکریپت میآیند، برای درک ماهیت وراثت پروتوتایپی با مشکل مواجه میشوند و از این رو کمیته ECMAScript تصمیم گرفت تا این تغییر ظاهری در ساختار (syntactic sugar) را بر مبنای وراثت پروتوتایپی پیاده کند به طوری که شبیه به طرز کارکرد وراثت مبتنی بر کلاس در زبانهای برنامهنویسی دیگر به نظر بیاید.
نکته مهمی که بدانید این است که جاوا اسکریپت در پسزمینه همچنان به صورت قبل عمل میکند و شما میتوانید به طرز معمول به پروتوتایپ یک شیء دسترسی داشته باشید.
تعریف یک کلاس
تعریف کلاس به شکل زیر انجام میشود:
1class Person {
2 constructor(name) {
3 this.name = name
4 }
5
6 hello() {
7 return 'Hello, I am ' + this.name + '.'
8 }
9}
هر کلاس یک شناسه دارد که میتواند برای ایجاد یک شیء جدید با استفاده از ()new ClassIdentifier مورد استفاده قرار گیرد. زمانی که یک شیء مقداردهی میشود، متد سازنده (constructor) با پارامترهای ارسالی فراخوانی میشود.
همچنین یک کلاس بسته به نیاز، متدهای زیادی دارد. در این مورد hello یک متد است و میتواند روی همه اشیای مشتق شده از کلاس فراخوانی شود:
1const flavio = new Person('Flavio')
2flavio.hello()
وراثت کلاس
یک کلاس میتواند کلاس دیگری را بسط دهد و شیءهایی که با استفاده از آن مقداردهی شوند، همه متدهای هر دو کلاس را به ارث میبرند.
اگر کلاسی که به ارث رسیده متدی با همان نام متد کلاس بالاتر در سلسلهمراتب داشته باشد، نزدیکترین متد، تقدم مییابد:
1class Programmer extends Person {
2 hello() {
3 return super.hello() + ' I am a programmer.'
4 }
5}
6
7const flavio = new Programmer('Flavio')
8flavio.hello()
این کد عبارت «.Hello، I am Flavio. I am a programmer» را نمایش میدهد.
در کلاسها اعلان متغیر کلاس به صورت صریح وجود ندارد؛ اما میبایست هر متغیری که در سازنده قرار دارد را مقداردهی کرد. درون یک کلاس میتوان با فراخوانی ()super به کلاس والد ارجاع داد.
متدهای استاتیک
به طور معمول متدها در وهله و نه در خود کلاس تعریف میشوند؛ اما متدهای استاتیک روی خود کلاس اجرا میشوند:
1class Person {
2 static genericHello() {
3 return 'Hello'
4 }
5}
6
7Person.genericHello() //Hello
متدهای خصوصی
جاوا اسکریپت یک روش داخلی برای تعریف متدهای خصوصی (private) یا حفاظتشده (protected) ندارد. البته برخی راهکارها وجود دارند اما معرفی آنها خارج از حیطه این مقاله است.
Getter-ها و Setter-ها
شما میتوانید متدهایی با پیشوند get و set برای ایجاد getter و setter تعریف کنید. این دو متد بر اساس این که بخواهید به یک متغیر دسترسی داشته باشید، یا مقدار آن را تغییر دهید فراخوانی میشوند:
1class Person {
2 constructor(name) {
3 this.name = name
4 }
5
6 set name(value) {
7 this.name = value
8 }
9
10 get name() {
11 return this.name
12 }
13}
اگر تنها یک getter داشته باشیم، مشخصه نمیتواند تعیین شود و هر تلاشی برای انجام این کار نادیده گرفته میشود:
1class Person {
2 constructor(name) {
3 this.name = name
4 }
5
6 get name() {
7 return this.name
8 }
9}
اگر یک setter داشته باشیم، میتوانیم مقدار را تغییر دهیم؛ اما نمیتوانیم از بیرون کلاس به آن دسترسی داشته باشیم:
1class Person {
2 constructor(name) {
3 this.name = name
4 }
5
6 set name(value) {
7 this.name = value
8 }
9}
Callback-ها
رایانهها دارای طراحی ناهمگام (asynchronous) هستند. منظور از ناهمگام این است که کارهای مختلف میتوانند مستقل از گردشکار اصلی برنامه اجرا شوند.
در رایانههای مصرفی امروزی هر برنامه روی بازه زمانی معینی اجرا میشود و سپس اجرای آن متوقف میشود تا برنامههای دیگر بتوانند اجرا شوند. این کار در چرخهای با چنان سرعت صورت میگیرد که امکان این که متوجه آن بشویم وجود ندارد و فکر میکنیم که رایانهها بسیاری از برنامهها را به صورت همزمان اجرا میکنند؛ اما این یک توهم است (البته طرز کار رایانههای چندهستهای متفاوت است).
برنامهها به طور درونی از وقفهها (interrupts) استفاده میکنند. منظور از وقفه سیگنالی است که به سمت پردازنده ارسال میشود و توجه سیستم را جلب میکند.
ما در اینجا قصد نداریم به سازوکار درونی آن اشاره کنیم؛ اما باید به خاطر داشته باشید که ناهمگام بودن برنامهها یک حالت نرمال است و به طور معمول اجرای آنها تا زمانی که نیاز به توجه داشته باشند معلق میشود و رایانه میتواند در این زمان کارهای دیگری انجام دهد. زمانی که یک برنامه منتظر پاسخ از سوی شبکه است، نمیتواند تا زمانی که درخواست پاسخ خود را دریافت میکند، پردازنده را معطل نگه دارد.
به طور معمول زبانهای برنامهنویسی به صورت «همگام» هستند و برخی از آنها در خود زبان یا کتابخانههایش، روشهایی برای مدیریت ناهمگام ارائه میکنند. زبانهای C J،ava ،C# ،PHP ،Go ،Ruby ،Swift و Python همگی به صورت پیشفرض همگام هستند. برخی از آنها از رویدادهای ناهمگام با استفاده از نخها و ایجاد یک پردازش جدید پشتیبانی میکنند.
جاوا اسکریپت نیز به صورت پیشفرض همگام و تک نخی است. این بدان معنی است که کد نمیتواند یک نخ جدید ایجاد کرده و به صورت موازی اجرا شود.
خطوط کد به صورت پشت سر هم یکی پس از دیگری اجرا میشوند. برای نمونه:
1const a = 1
2const b = 2
3const c = a * b
4console.log(c)
5doSomething()
اما جاوا اسکریپت درون مرورگر متولد شده است و وظیفه اصلی آن در ابتدا این بوده است که به اقدامات کاربر مانند onClick ،onMouseOver ،onChange و onSubmit پاسخ دهد. این کار از طریق مدل برنامهنویسی همگام چگونه میسر میشود؟
پاسخ در محیط جاوا اسکریپت است. مرورگر روشی برای انجام این کار ارائه میکند، یعنی مجموعهای از API-ها ارائه کرده است که میتواند این نوع از کارکرد را اجرا کند.
در سالهای اخیر Node.js به معرفی یک محیط بدون انسداد I/O اقدام کرده است که این مفهوم را برای دسترسی به فایلها، فراخوانیهای شبکه و غیره بسط میدهد.
ما نمیتوانیم بدانیم که یک کاربر چه هنگام روی یک دکمه کلیک خواهد کرد، بنابراین تنها میتوانیم یک «دستگیره رویداد» (event handler) برای عمل کلیک تعریف کنیم. این دستگیره رویداد یک تابع میپذیرد که هنگام تحریک شدن رویداد فراخوانی خواهد شد:
1document.getElementById('button').addEventListener('click', () => {
2 //item clicked
3})
این همان callback است.
منظور از callback تابع سادهای است که به صورت یک مقدار به تابع دیگر ارسال میشود و تنها زمانی اجرا خواهد شد که رویداد رخ دهد. ما به این دلیل میتوانیم این کار را انجام دهیم که جاوا اسکریپت «تابعهای درجه اول» (first-class functions) دارد که میتوانند به متغیرها انتساب یابند و به تابعهای دیگر ارسال شوند (تابعهای درجه بالاتر یا higher-order نیز نامیده میشوند).
به طور معمول همه کدهای کلاینت در یک دستگیره رویداد load روی شیء window پوشش مییابد که تابع callback را تنها زمانی که صفحه آماده است فراخوانی میکند:
1window.addEventListener('load', () => {
2 //window loaded
3 //do what you want
4})
Callback-ها همهجا استفاده میشوند و اختصاص به رویدادهای DOM ندارند. یک مثال رایج در استفاده از تایمر چنین است:
1setTimeout(() => {
2 // runs after 2 seconds
3}, 2000)
درخواستهای XHR نیز یک callback میپذیرند. در این مثال یک تابع به یک مشخصه انتساب مییابد که هنگام اتفاق افتادن یک رویداد خاص، فراخوانی میشود:
1const xhr = new XMLHttpRequest()
2xhr.onreadystatechange = () => {
3 if (xhr.readyState === 4) {
4 xhr.status === 200 ? console.log(xhr.responseText) : console.error('error')
5 }
6}
7xhr.open('GET', 'https://yoursite.com')
8xhr.send()
مدیریت خطا در callback-ها
یکی از روشهای رایج برای مدیریت خطا در callback-ها استفاده از چیزی که است Node.js به خدمت گرفته است و آن پارامتر اول در هر تابع callback است که شیء error یا error-first callbacks نامیده میشود.
اگر خطایی وجود نداشته باشد، این شیء nul است. اما اگر خطایی باشد شامل برخی توضیحات در مورد خطا و دیگر اطلاعات خواهد بود:
1fs.readFile('/file.json', (err, data) => {
2 if (err !== null) {
3 //handle error
4 console.log(err)
5 return
6 }
7
8 //no errors, process data
9 console.log(data)
10})
مشکل callback-ها
callback ها برای کاربردهای ساده عالی هستند. با این وجود هر callback یک سطح از تو در تو بودن اضافه میکند و زمانی که تعداد زیادی callback وجود داشته باشد، کد به سرعت پیچیده میشود:
1window.addEventListener('load', () => {
2 document.getElementById('button').addEventListener('click', () => {
3 setTimeout(() => {
4 items.forEach(item => {
5 //your code here
6 })
7 }, 2000)
8 })
9})
این فقط یک کد 4 سطحی است؛ اما سطوح تو در تو بودن بیش از این نیز میتواند باشد که چندان جالب نیست. چگونه میتوان این مشکل را حل کرد؟
جایگزینهای callback
از نسخه ES6 به بعد، جاوا اسکریپت چند ویژگی معرفی کرده است که به ما کمک میکند کدهای ناهمگام بنویسیم و درگیر استفاده از callback نیز نشویم:
- (promises (ES6
- (Async/Await (ES8
promise-ها
promise-ها یکی از روشهای نوشتن کد ناهمگام هستند که در آن لازم نیست نگران وجود callback-های زیاد در کد خود باشیم. با این که این ویژگی سالها است که عرضه شده؛ اما در ES2015 استاندارد و معرفی شدهاند و اینک با معرفی تابعهای async در ES2017 منسوخ شدهاند.
تابعهای Async از API مربوط به promise-ها به عنوان بلوک اصلی سازنده خود استفاده میکنند و از این رو درک آنها حتی در صورتی که در کدهای جدید بخواهید از تابعهای async به جای promise استفاده کنید، ضروری خواهد بود.
توضیح خلاصه طرز کار promise-ها
زمانی که یک promise فراخوانی میشود، کار خود را در حالت معلق (pending) شروع میکند. این بدان معنی است که تابع فراخوانی کننده به اجرای خود ادامه میدهد و در این زمان منتظر promise است تا پردازشش را انجام دهد و بازخوردی به تابع فراخوانی کننده بدهد.
سپس تابع فراخوانی کننده منتظر مقدار بازگشتی از سوی promise در حالت resolved یا در حالت rejected میماند؛ اما میدانیم که جاوا اسکریپت ناهمگام است و از این رو تابع در هنگامی که promise مشغول کار است، به اجرای خود ادامه میدهد.
چرا API جاوا اسکریپت از promise-ها استفاده میکند؟
promise-ها علاوه بر کد اپلیکیشن و کد کتابخانه در API های وب مدرن استاندارد مانند Fetch یا Service Workers نیز استفاده میشوند. این که در جاوا اسکریپت مدرن بخواهید از promise-ها استفاده نکنید نامحتمل است و از این رو باید آنها را کاملاً یاد بگیرید.
ایجاد یک promise
API مربوط به promise یک سازنده promise معرفی کرده است که با استفاده از new promise() مقداردهی میشود:
1let done = true
2
3const isItDoneYet = new Promise((resolve, reject) => {
4 if (done) {
5 const workDone = 'Here is the thing I built'
6 resolve(workDone)
7 } else {
8 const why = 'Still working on something else'
9 reject(why)
10 }
11})
همان طور که میبینید promise ثابت سراسری done را بررسی میکند و اگر true باشد، یک promise به صورت reolved بازگشت مییابد و در غیر این صورت مقدار بازگشتی rejected خواهد بود.
ما با استفاده از resolve و reject میتوانیم یک مقدار را بازگشت دهیم و گرچه در مثال فوق یک رشته بازگشت یافته است؛ اما میتواند یک شیء نیز باشد.
مصرف یک promise
در بخش قبلی شیوه ایجاد یک promise را توضیح دادیم. اینک میخواهیم ببینیم promise چگونه میتواند مصرف شود.
1const isItDoneYet = new Promise()
2//...
3
4const checkIfItsDone = () => {
5 isItDoneYet
6 .then(ok => {
7 console.log(ok)
8 })
9 .catch(err => {
10 console.error(err)
11 })
12}
اجرای تابع ()checkIfItsDone باعث راهاندازی یک promise به نام ()isItDoneYet میشود که با استفاده از یک callback به نام then منتظر resolve شدن میماند و در صورت وجود یک خطا آن را با callback به نام catch مدیریت میکند.
زنجیرهسازی promise-ها
میتوان یک promise را به promise دیگر بازگشت داد و بدین ترتیب زنجیرهای از promise-ها ایجاد کرد. یک مثال عالی از زنجیرهسازی promise-ها در Fetch API ارائه شده است که یک لایه روی API مربوط به XMLHttpRequest ساخته و میتوانیم از آن برای دریافت منابع و صفبندی یک زنجیره از promise-ها برای اجرا در مواد واکشی منابع استفاده کنیم.
Fetch API یک سازوکار مبتنی بر promise است و فراخوانی ()fetch معادل تعریف کردن promise با استفاده از ()new promise است:
مثال
1const status = response => {
2 if (response.status >= 200 && response.status < 300) {
3 return Promise.resolve(response)
4 }
5 return Promise.reject(new Error(response.statusText))
6}
7
8const json = response => response.json()
9
10fetch('/todos.json')
11 .then(status)
12 .then(json)
13 .then(data => {
14 console.log('Request succeeded with JSON response', data)
15 })
16 .catch(error => {
17 console.log('Request failed', error)
18 })
در این مثال ()fetch را برای دریافت لیستی از آیتمهای TODO از فایل todos.json در دامنه root فراخوانی میکنیم و بدین ترتیب زنجیرهای از promise-ها ایجاد میشود.
اجرای ()fetch یک پاسخ بازگشت میدهد که مشخصات زیادی دارد و درون آن موارد زیر را ارجاع میدهیم:
- Status – یک مقدار عددی است که کد وضعیت HTTP را نشان میدهد.
- statusText – یک پیام وضعیت است که در صورت موفق بودن درخواست، به صورت OK خواهد بود.
Response نیز یک متد Jason است که یک promise بازگشت میدهد. این promise با محتوای بدنه پردازش شده و انتقال یافته به JSON به صورت resolve درمیآید.
بنابراین با توجه به این promise-ها اتفاقات زیر رخ میدهد:
promise نخست در زنجیره یک تابع است که به نام ()status تعریف کردهایم و وضعیت پاسخ را بررسی کرده و در صورت عدم موفقیت (کدهای بین 200 تا 299) promise را رد میکند.
این عملیات موجب میشود که زنجیره promise-ها قطع شود و مستقیماً به گزاره ()catch در انتها میرود و متن Request failed به همراه پیام خطا، log میشود.
اما اگر موفق باشد تابع ()json را که تعریف کردیم فرا میخواند. از آنجا که وقتی promise قبلی موفق بود، شیء response را بازگشت داده است، ما آن را به عنوان ورودی promise دوم میگیریم.
در این حالت دادههای JSON که پردازش شده را بازگشت میدهیم به طوری که promise سوم مستقیماً JSON را بازیابی کند:
1.then((data) => {
2 console.log('Request succeeded with JSON response', data)
3})
و آن را در کنسول log میکنیم.
مدیریت خطاها
در مثال فوق در بخش قبلی یک catch داشتیم که به زنجیره promise-ها الحاق میشد. زمانی که هر کدام از بخشهای زنجیره promise-ها از کار میافتد و یک خطا رخ میدهد و یا پاسخ reject دریافت میشود، کنترل به نزدیکترین گزاره ()catch در سمت پایین زنجیره میرسد.
1new Promise((resolve, reject) => {
2 throw new Error('Error')
3}).catch(err => {
4 console.error(err)
5})
6
7// or
8
9new Promise((resolve, reject) => {
10 reject('Error')
11}).catch(err => {
12 console.error(err)
13})
آبشارسازی خطاها
اگر درون ()catch خطایی رخ دهد، میتوانید یک متد ثانویه catch() برای مدیریت آن تعریف کنید و همین طور تا آخر.
1new Promise((resolve, reject) => {
2 throw new Error('Error')
3})
4 .catch(err => {
5 throw new Error('Error')
6 })
7 .catch(err => {
8 console.error(err)
9 })
هماهنگسازی promise-ها با استفاده از ()promise.all
اگر لازم باشد که promise-های مختلف را با هم هماهنگ کنیم، میتوانیم از ()promise.all کمک بگیریم و با تعریف کردن لیستی از همه promise-ها، وقتی همه آنها resolve شدند، چیزی را اجرا کنیم.
مثال:
1const f1 = fetch('/something.json')
2const f2 = fetch('/something2.json')
3
4Promise.all([f1, f2])
5 .then(res => {
6 console.log('Array of results', res)
7 })
8 .catch(err => {
9 console.error(err)
10 })
ساختار انتساب تخریب ES2015 امکان انجام این کار را در اختیار ما قرار میدهد:
1Promise.all([f1, f2]).then(([res1, res2]) => {
2 console.log('Results', res1, res2)
3})
البته شما محدود به استفاده از fetch نیستید و هر promise برای این کار مناسب است.
هماهنگسازی promise-ها با استفاده از ()promise.race
()promise.race به محض این که یکی از promise-ها به صورت resolve شده به آن ارسال شود اجرا میشود و callback الحاقی خود را تنها یک بار با نتیجه promise نخست resolve شده اجرا میکند. مثال:
1const promiseOne = new Promise((resolve, reject) => {
2 setTimeout(resolve, 500, 'one')
3})
4const promiseTwo = new Promise((resolve, reject) => {
5 setTimeout(resolve, 100, 'two')
6})
7
8Promise.race([promiseOne, promiseTwo]).then(result => {
9 console.log(result) // 'two'
10})
Async/Await
جاوا اسکریپت در طی زمان بسیار کوتاهی از callbacks به (promises (ES2015 تکامل یافت و سپس از ES2017 جاوا اسکریپت ناهمگام با معرفی ساختار Async/Await باز هم سادهتر شد.
تابعهای Async ترکیبی از promise-ها و generator-ها هستند و در واقع سطح بالاتری از انتزاع را نسبت به promise-ها ارائه میکنند. یک بار دیگر باید تکرار کنیم که async/await بر مبنای promise ساخته شده است.
چرا async/await معرفی شد؟
این ساختار باعث میشود که سازههای تکراری promise-ها کاهش یابند و محدودیت عدم قطع کردن زنجیره در promise-های زنجیرهای نیز رفع شود.
زمانی که promise-ها در ES2015 معرفی شدند، به منظور رفع مشکل کد ناهمگام عرضه شده بودند و در این کار نیز موفق بودند؛ اما در طی 2 سال که ES2015 و ES2017 را از هم جدا میساخت، مشخص شد که promise-ها نمیتوانند راهحل نهایی باشند.
promise-ها برای رفع مشکل مشهور callbak عرضه شدند؛ اما آنها نیز مشکلات خاص خود را داشتند و باعث ایجاد پیچیدگی بیشتر در ساختار میشدند.
در ادامه مشخص شد که میتوان سازههای بهتری با ساختارهای مناسبتر در اختیار توسعهدهندهها قرار داد و هنگامی که زمان مناسب فرارسید، تابعهای async عرضه شدند.
این تابعها باعث شدند کد طوری به نظر بیاید که گویا همگام است؛ اما ناهمگام بود و در پشتصحنه نیز مسدودسازی نداشت.
طرز کار async/await
یک تابع async در عمل مانند مثال زیر یک promise بازگشت میدهد:
1const doSomethingAsync = () => {
2 return new Promise(resolve => {
3 setTimeout(() => resolve('I did something'), 3000)
4 })
5}
زمانی که بخواهیم این تابع را فراخوانی کنیم یک await در اول آن میگذاریم و کد فراخوانی کننده تا زمانی که promise به صورت resolve شده یا reject شده بازگشت نیافته است متوقف میشود. تنها محدودیت این است که تابع کلاینت باید به صورت async تعریف شده باشد. به مثال زیر توجه کنید:
1const doSomething = async () => {
2 console.log(await doSomethingAsync())
3}
یک مثال ساده
در ادامه مثال سادهای از کاربرد async/await برای اجرای ناهمگام یک تابع را میبینید:
1const doSomethingAsync = () => {
2 return new Promise(resolve => {
3 setTimeout(() => resolve('I did something'), 3000)
4 })
5}
6
7const doSomething = async () => {
8 console.log(await doSomethingAsync())
9}
10
11console.log('Before')
12doSomething()
13console.log('After')
کد فوق مواد زیر را در کنسول مرورگر نمایش میدهد:
Before After I did something //after 3s
Pomise کردن همه چیز
افزودن کلیدواژه async در ابتدای هر تابعی به این معنی است که تابع یک promise بازگشت خواهد داد. حتی اگر این کار به روش صریحی صورت نگیرد در ساز و کار درونی خودش یک promise بازگشت میدهد. به همین دلیل است که کد زیر معتبر است:
1const aFunction = async () => {
2 return 'test'
3}
4
5aFunction().then(alert) // This will alert 'test'
و همانند کد زیر عمل میکند:
1const aFunction = async () => {
2 return Promise.resolve('test')
3}
4
5aFunction().then(alert) // This will alert 'test'
خوانایی کد بسیار افزایش مییابد
همان طور که در مثال فوق شاهد هستید، کد ما بسیار ساده به نظر میرسد. آن را با کدهایی که از promise-های ساده با زنجیرهسازی و تابعهای callback استفاده میکنند مقایسه کنید.
این مثال بسیار سادهای است و مزیتهای اصلی زمانی مشخص میشوند که از کدهای بسیار پیچیده استفاده میکنید. برای نمونه در ادامه یک منبع JSON ارائه شده که آن را با استفاده از promise-ها تجزیه میکنیم:
1const getFirstUserData = () => {
2 return fetch('/users.json') // get users list
3 .then(response => response.json()) // parse JSON
4 .then(users => users[0]) // pick first user
5 .then(user => fetch(`/users/${user.name}`)) // get user data
6 .then(userResponse => response.json()) // parse JSON
7}
8
9getFirstUserData()
و در ادامه همین کارکرد با استفاده از await/async ارائه شده است:
1const getFirstUserData = async () => {
2 const response = await fetch('/users.json') // get users list
3 const users = await response.json() // parse JSON
4 const user = users[0] // pick first user
5 const userResponse = await fetch(`/users/${user.name}`) // get user data
6 const userData = await user.json() // parse JSON
7 return userData
8}
9
10getFirstUserData()
سری کردن تابعهای چندگانه async
تابعهای async میتوانند به سادگی به حالت زنجیری درآیند و ساختار آن بسیار خواناتر از promise-های ساده خواهد بود:
1const promiseToDoSomething = () => {
2 return new Promise(resolve => {
3 setTimeout(() => resolve('I did something'), 10000)
4 })
5}
6
7const watchOverSomeoneDoingSomething = async () => {
8 const something = await promiseToDoSomething()
9 return something + ' and I watched'
10}
11
12const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {
13 const something = await watchOverSomeoneDoingSomething()
14 return something + ' and I watched as well'
15}
16
17watchOverSomeoneWatchingSomeoneDoingSomething().then(res => {
18 console.log(res)
19})
کد فوق عبارت زیر را نمایش میدهد:
I did something and I watched and I watched as well
دیباگ کردن سادهتر
دیباگ کردن promise-ها دشوار است، زیرا دیباگر روی کدهای ناهمگام متوقف نمیشود. Async/await این کار را بسیار سادهتر ساخته است، زیرا از نظر کامپایلر این کد مانند کدهای همگام است.
ماژولهای ES
در حالی که Node.js سالها است که از استاندارد CommonJS استفاده میکند، اما مرورگرها هرگز یک سیستم module نداشتهاند، چون هر تصمیم بزرگی مانند سیستم ماژول باید ابتدا از سوی ECMAScript استانداردسازی و سپس از سوی مرورگر پیادهسازی شود.
این فرایند استانداردسازی در ES6 تکمیل شد و مرورگرها شروع به پیادهسازی این نوعبندی استاندارد کرده و تلاش نمودند همه چیز همراستا بماند و به همان روش سابق کار کند. اینک ماژولهای ES در کروم، سافاری، Edge و فایرفاکس (از نسخه 60) پشتیبانی میشوند.
ماژولها بسیار جالب هستند، زیرا امکان کپسولهسازی همه انواع کارکردها را در اختیار ما قرار میدهند و این کارکرد را به فایلهای دیگر جاوا اسکریپت مانند کتابخانهها نیز اکسپورت میکنند.
ساختار ماژولهای ES
ساختار ایمپورت کردن یک ماژول به صورت زیر است:
import package from 'module-name'
در حالی که CommonJS از ساختار زیر استفاده میکند:
const package = require('module-name')
یک ماژول در واقع یک فایل جاوا اسکریپت قرار دارد که یک یا چند مقدار (شیء، تابع یا متغیر) را با استفاده از کلیدواژه export، اکسپورت میکند. برای نمونه ماژول زیر یک تابع اکسپورت میکند که یک رشته با حروف بزرگ بازگشت میدهد:
uppercase.js
export default str => str.toUpperCase()
در مثال فوق، این ماژول یک اکسپورت منفرد پیشفرض تعریف میکند، به طوری که میتواند یک تابع ناهمگام باشد. در غیر این صورت باید یک نام داشته باشد تا از دیگر اکسپورتها متمایز شود.
اکنون هر ماژول دیگر جاوا اسکریپت میتواند این کارکرد را که از سوی uppercase.js ارائه شده با ایمپورت کردن آن به دست آورد.
یک صفحه HTML میتواند یک ماژول را با استفاده از تگ <script> با خصوصیت ویژه "type="module اضافه کند:
1<script type="module" src="index.js"></script>
لازم به ذکر است که هر اسکریپتی که با "type="module بارگذاری شود در حالت strict خواهد بود. در این مثال ماژول uppercase.js یک اکسپورت پیشفرض تعریف میکند و وقتی آن را ایمپورت کردیم میتوانیم از نام ترجیحی خودمان استفاده کنیم:
1import toUpperCase from './uppercase.js'
و میتوانیم آن را استفاده کنیم:
1toUpperCase('test') //'TEST'
همچنین میتوانیم از یک مسیر مطلق برای ایمپورت کردن ماژول استفاده کنیم تا ماژولهای تعریف شده در دامنه دیگری را نیز مورد ارجاع قرار دهیم:
1import toUpperCase from 'https://flavio-es-modules-example.glitch.me/uppercase.js'
این نیز یک ساختار ایمپورت معتبر است:
1import { foo } from '/uppercase.js'
2import { foo } from '../uppercase.js'
اما این ساختار معتبر نیست:
1import { foo } from 'uppercase.js'
2import { foo } from 'utils/uppercase.js'
این ساختار یا مطلق است و یا یک./ یا / در ابتدای نام خود دارد.
گزینههای ایمپورت/اکسپورت دیگر
ما این مثال را قبلاً دیدیم:
1export default str => str.toUpperCase()
این مثال یک اکسپورت پیشفرض ایجاد میکند. میتوانیم با استفاده از ساختار زیر بیش از یک چیز را اکسپورت کنیم:
1const a = 1
2const b = 2
3const c = 3
4
5export { a، b، c }
ماژول دیگر میتواند همه این موارد را با استفاده از دستور زیر ایمپورت کند:
1import * from 'module'
میتوانیم تنها چند مورد از این اکسپورتها را با بهرهگیری از انتساب تخریبی ایمپورت کنیم:
1import { a } from 'module'
2import { a، b } from 'module'
میتوانیم نام هر ایمپورت را برای سهولت با استفاده از as تغییر دهیم:
1import { a, b as two } from 'module'
میتوانیم ابتدا اکسپورت پیشفرض را ایمپورت کنیم و سپس همه اکسپورتهای غیر پیشفرض را به صورت «با نام» ایمپورت کنیم. برای مثال این کار در ایمپورت react زیر صورت گرفته است:
1import React، { Component } from 'react'
CORS
ماژولها با استفاده از CORS واکشی میشوند. این بدان معنی است که اگر اسکریپتها از یک دامنه دیگر مورد ارجاع قرار گیرند، باید یک هدر CORS معتبر داشته باشند که امکان بارگذاری از سایت دیگر را فراهم سازد.
در مورد مرورگرهایی که از ماژولها پشتیبانی نمیکنند چه میتوان کرد؟ در این موارد میتوان از ترکیبی از "type="module و nomodule استفاده کرد:
1<script type="module" src="module.js"></script>
2<script nomodule src="fallback.js"></script>
ماژولهای ES یکی از بزرگترین ویژگیهای معرفی شده در مرورگرهای مدرن هستند. ماژولها بخشی از ES6 هستند؛ اما مسیری که برای پیادهسازی آنها طی شده بسیار طولانی است. اینک میتوانیم از آنها استفاده کنیم؛ اما باید به خاطر داشته باشیم که داشتن بیش از چند ماژول ممکن است بر روی عملکرد صفحه تأثیر منفی بگذارد، چون یک گام دیگر به مراحلی که مرورگر باید در زمان اجرا به آن بپردازد میافزاید.
با این که ماژولهای ES وارد مرورگرها شدهاند؛ Webpack احتمالاً همچنان یکی از بزرگترین بازیگرها خواهد ماند؛ اما داشتن یک چنین ویژگی که به طور مستقیم در یک زبان ساخته شده است برای یکنواخت سای روش کار ماژولها در سمت کلاینت و Node.js بسیار مفید است.
بخش دوم: مفاهیم پایه ری اکت (React)
در ادامه و در این بخش به بررسی مفاهیم پایه خود React میپردازیم.
- اپلیکیشنهای تکصفحهای
- رویکرد اعلانی ریاکت
- تغییرناپذیری
- محض بودن یا purity
- ترکیببندی
- DOM مجازی
- گردش داده غیر جهتدار
اپلیکیشنهای تکصفحهای
اپلیکیشنهای React به نام اپلیکیشنها با صفحه منفرد نیز نامیده میشوند، اما شاید بپرسید معنی این اصطلاح چیست؟
در گذشته، زمانی که مرورگرها قابلیتهای کمتری نسبت به امروز داشتند و عملکرد جاوا اسکریپت نیز ضعیفتر بود، هر صفحه از یک سرور میآمد. هر بار که شما روی چیزی کلیک میکردید، یک درخواست جدید به سرور ارسال میشد و متعاقباً مرورگر یک صفحه جدید را بارگذاری میکرد. در آن زمان تنها محصولات بسیار نوآورانه به طرز متفاوتی عمل کرده و رویکردهای جدیدی را آزمایش میکردند.
امروزه با رواج فریمورکهای فرانتاند مدرن جاوا اسکریپت مانند React، اپلیکیشن معمولاً به صورت یک اپلیکیشن تکصفحهای ساخته میشود یعنی شما تنها یک کد (HTML، CSS و جاوا اسکریپت)، اپلیکیشن را بارگذاری میکنید و هنگامی که با اپلیکیشن تعامل پیدا میکنید؛ جاوا اسکریپت به طور تدریجی رویدادهای مرورگر را تفسیر میکند و به جای ارسال درخواستهای جدید به سرور و بازگشت یک سند جدید از آن، دادههایی را به صورت JSON از سرور درخواست میکند و یا عملی روی سرور اجرا میشود؛ اما خود صفحه هرگز به طور کامل بارگذاری مجدد نمیشود و در فرایندی شبیه به اپلیکیشن دسکتاپ عمل میکند.
اپلیکیشنهای با صفحه منفرد در جاوا اسکریپت ساخته میشوند، یا بهتر است بگوییم در جاوا اسکریپت کامپایل و در مرورگر اجرا میشوند. فناوری مورد استفاده همواره یکسان بوده است، اما فلسفه و برخی اجزای کلیدی طرز کار اپلیکیشنها فرق کرده است.
نمونههایی از اپلیکیشنهای با صفحه منفرد
برخی نمونههای اپلیکیشنهای تکصفحهای که میتوان مورد اشاره قرار داد، به شرح زیر هستند:
- جیمیل
- گوگل مپ
- فیسبوک
- توییتر
- گوگل درایو
مزایا و معایب اپلیکیشنهای تکصفحهای
عملکرد یک اپلیکیشن تکصفحهای از نظر کاربر بسیار سریعتر به نظر میرسد، چون به جای این که منتظر ارتباط بین سرور-کلاینت بماند و همچنین صبر کند تا مرورگر صفحه را مجدداً رندر کند، میتواند بازخورد آنی داشته باشد. این طراحی در حوزه مسئولیت سازنده اپلیکیشن است؛ اما میتوان از انیمیشنهای گذار و چرخشی یا هر نوع بهبود تجربه کاربری (UX) که قطعاً بهتر از گردش کار سنتی است نیز بهره گرفت.
علاوه بر اینکه این رویکرد موجب افزایش سرعت اپلیکیشنها در نظر کاربر میشود، سرور نیز منابع کمتری مصرف میکند، زیرا میتوان به جای ساخت طرحهای سمت سرور روی ارائه یک API بهینه متمرکز شد. بدین ترتیب رویکرد اپلیکیشن تکصفحهای برای ساخت اپلیکیشنهای موبایل روی API مناسب است چون میتوان به طور کامل از کد سمت سرور موجود به طور مجدد استفاده کرد.
اپلیکیشنهای تکصفحهای به راحتی قابل تبدیل به اپلیکیشنهای وب پیشرونده (progressive) هستند که به نوبه خود امکان کش کردن به صورت محلی و پشتیبانی از تجربه آفلاین سرویسها را فراهم میسازند. همچنین در صورتی که نیاز باشد کاربر آنلاین شود خطایی که ارائه میکنند به صورت بهتری ارائه میشود.
اپلیکیشنهای تکصفحهای در مواردی استفاده میشوند که نیازی به سئو (بهینهسازی موتور جستجو) نباشد. برای نمونه در مورد اپلیکیشنهایی که در پشت فرم login قرار میگیرند از این حالت استفاده میشود.
موتورهای جستجو با این که امور روزمره را بهبود میبخشد؛ اما همچنان در ایندکس کردن سایتهایی که با رویکرد اپلیکیشن تکصفحهای ساخته شدهاند در برابر صفحاتی که به طور سنتی در سمت سرور رندر میشوند با مشکل مواجه هستند. این واقعیت در مورد بلاگها مصداق دارد. اگر قصد دارید در محصولی که میسازید از موتورهای جستجو استفاده کنید هرگز بدون ساخت بخشهای رندر شده در سمت سرور، اقدام به ساخت اپلیکیشنهای تکصفحهای نکنید.
زمانی که به کدنویسی اپلیکیشنهای تکصفحهای میپردازید، باید مقادیر زیادی کدهای جاوا اسکریپت بنویسید. از آنجا که اپلیکیشن میتواند مدتهای زیادی اجرا شود، باید به مشکلات ناشی از نشت حافظه کاملاً دقت داشته باشید. اگر در گذشته صفحههای یک وبسایت عمری به اندازه چند دقیقه داشتند، اپلیکیشنهای تکصفحهای ممکن است برای ساعتها در مرورگر باز بمانند و اگر مشکل نشت حافظه وجود داشته باشد، در طی زمان باعث افزایش مصرف حافظه مرورگر میشود و در صورتی که این مشکل رفع نشود، با تجربه ناخوشایند کند شدن اپلیکیشن مواجه میشوید.
اپلیکیشنهای تکصفحهای در مورد کارهای تیمی بسیار خوب عمل میکنند. توسعهدهندگان بکاند میتوانند روی API متمرکز شوند و توسعهدهندگان فرانتاند نیز میتوانند روی ایجاد بهترین تجربه کاربری با بهرهگیری از API ساخته شده در بکاند تمرکز داشته باشند.
یکی دیگر از مشکلات اپلیکیشنهای تکصفحهای این است که تکیه زیادی روی جاوا اسکریپت دارند. این امر میتواند منجر به کند شدن تجربه کاربری در دستگاههای با توان پایین میشود. همچنین برخی از بازدیدکنندگان ممکن است جاوا اسکریپت را غیرفعال کرده باشند و باید در این حالت نیز قابلیت دسترسی برای امکانات اپلیکیشن را در نظر بگیرید.
مدیریت ناوبری مرورگر
از آنجا که رفتار ناوبری پیشفرض مرورگر در اپلیکیشنهای تکصفحهای بازنویسی میشود، URL-ها باید به صورت دستی مدیریت شوند. بخشی از این اپلیکیشنها، مسیریاب یا روتر (Router) نامیده میشود. برخی فریمورکها مانند Ember این وظیفه را بر عهده دارند و برخی دیگر مانند ریاکت به کتابخانههایی مثل React router برای انجام این کار نیازمند هستند.
مشکل این است که در آغاز توسعه اپلیکیشنهای تکصفحهای، به این وضعیت که دکمههای ناوبری مرورگر چه تأثیری روی عملکرد اپلیکیشن دارند اندیشیده نشده بود. این امر موجب خطای مشهور «از کار افتادن دکمه بازگشت» میشد و در زمان ناوبری درون اپلیکیشن، URL تغییر نمییافت (چون رفتار پیشفرض مرورگر استفاده میشد) و زدن دکمه بازگشت موجب میشد که کاربران به صفحه قبلی که بازدید کرده بودند بروند و این مورد میتوانست وبسایتی باشد که مدتها قبل مورد بازدید قرار گرفته بود.
این مشکل هم اینک با استفاده از History API که از سوی مرورگرها ارائه شده رفع شده است؛ اما در اغلب موارد باید از کتابخانهای مانند React Router استفاده کنید که به طور داخلی از این API بهره میگیرد.
رویکرد اعلانی (Declarative)
زمانی که به شما گفته میشود ریاکت از رویکرد اعلانی استفاده میکند، معنی این گفته برای شما چیست؟ در مقالات زیادی اشاره شده است که ریاکت از یک رویکرد اعلانی برای ساخت رابط کاربری استفاده میکند.
ریاکت باعث شده است که «رویکرد اعلانی» بسیار رایج و مطرح شود و از این رو دنیای فرانتاند به وسیله آن همراه با ریاکت گسترش یافته است. رویکرد اعلانی عملاً مفهوم جدیدی محسوب نمیشود؛ اما روشی که ریاکت برای ساخت رابط کاربری استفاده میکند بسیار اعلانیتر از قالبهای HTML است. به عنوان نمونه:
- با استفاده از ریاکت میتوان رابطهای وب را بدون حتی تماس مستقیم با DOM ساخت.
- میتوان یک سیستم رویداد بدون نیاز به تعامل با رویدادهای DOM واقعی طراحی کرد.
متضاد رویکرد اعلانی یک رویکرد «تکراری» (Iterative) است. مثال رایج برای رویکرد تکراری به گشتن در میان عناصر DOM با استفاده از jQuery یا رویدادهای DOM میتوان اشاره کرد.
رویکرد اعلانی ریاکت این وضعیت را برای ما به حالت انتزاعی درمیآورد. ما صرفاً به ریاکت اعلام میکنیم که چه کامپوننتی باید به چه روشی رندر شود و دیگر هرگز لازم نیست با DOM برای ارجاعات بعدی تعامل داشته باشیم.
تغییرناپذیری (Immutability)
یکی از مفاهیمی که احتمالاً در زمان برنامهنویسی در React با آن مواجه خواهید شد بحث تغییرناپذیری و متضاد آن تغییرپذیری (mutability) است. البته این یک موضوع پر حرف و حدیث است؛ اما هر دیدگاهی که در مورد مفهوم تغییرناپذیری داشته باشید؛ در هر حال ریاکت و غالب بخشهای اکوسیستم آن به نوعی استفاده از این مفهوم را اجبار کردهاند و از این رو باید دستکم درکی از این مفهوم داشته باشید. به همین جهت این مفهوم و نتایج ضمنی آن مهم است.
در برنامهنویسی یک متغیر زمانی تغییرناپذیر نامیده میشود که مقدار آن پس از ایجاد متغیر نتواند عوض شود.
شما تاکنون هنگام دستکاری رشتهها از متغیرهای تغییرناپذیر، احتمالاً بدون این که حتی بدانید، استفاده کردهاید. رشتهها به صورت پیشفرض تغییرناپذیر هستند و هنگامی که آنها را در واقعیت تغییر میدهید یک رشته جدید ایجاد میکنید و آن را به متغیری با همان نام انتساب میدهید.
یک متغیر تغییرناپذیر هرگز نمیتواند تغییر یابد. برای بهروزرسانی مقدار آن میتوانید یک متغیر جدید بسازید. همین مفاهیم در مورد اشیا و آرایهها نیز کاربرد دارند. به جای تغییر دادن آرایهها برای افزودن یک آیتم جدید ما همواره یک آرایه جدید با الحاق آرایه قبلی به علاوه آیتم جدید میسازیم. یک شیء هرگز نمیتواند بهروزرسانی شود؛ بلکه پیش از آن که تغییر یابد کپی میشود.
این مفهوم در بخشهای مختلف ریاکت نیز مصداق دارد. برای نمونه، شما هرگز نباید خصوصیت state یک کامپوننت را به صورت مستقیم تغییر دهید؛ بلکه این کار باید صرفاً از طریق متد ()setState صورت بگیرد. در Redux شما هرگز نباید state را به صورت مستقیم تغییر دهید؛ بلکه این کار از طریق reducer-ها که تابعهای تعریف شده ما هستند تغییر مییابند.
اگر بخواهیم دلایل این مسئله را توضیح دهیم مهمترین موارد به صورت زیر هستند:
- تغییرپذیری میتواند به صورت متمرکز مانند حالت redux انجام یابد که موجب بهبود قابلیتهای دیباگ و کاهش منابع خطا میشود.
- در این حالت کد تمیزتر به نظر میرسد و درک آن آسانتر است. شما هرگز انتظار ندارید که یک تابع مقداری را بدون اطلاع شما تغییر دهد، چون میخواهید «قابلیت پیشبینی» (predictability) داشته باشید. هنگامی که یک تابع شیءها را تغییر ندهد؛ بلکه یک شیء جدید بازگشت دهد، این تابع یک «تابع محض» (pure function) یا در مواردی تابع خالص نیز نامیده میشود.
- کتابخانه میتواند موجب بهینهسازی کد شود، زیرا برای مثال جاوا اسکریپت زمانی که به جای تغییر شیء موجود، ارجاع یک شیء قدیمی را با شیئی کاملاً جدید عوض میکند، بسیار سریعتر میشود. بدین ترتیب بهبود عملکرد رخ میدهد.
محض بودن
در جاوا اسکریپت وقتی یک تابع شیءها را تغییر نمیدهد، بلکه صرفاً یک شیء جدید برگشت میدهد به این تابع، تابع محض گفته میشود. یک تابع یا یک متد برای این که محض نامیده شود، نباید موجب ایجاد عوارض جانبی شود و باید هر چند بار که با ورودیهای یکسان فراخوانی میشود، خروجیهای یکسانی را ارائه دهد. در واقع یک تابع محض یک ورودی میگیرد و یک خروجی بدون تغییر دادن ورودی و یا هیچ چیز دیگر بازگشت میدهد.
خروجی یک تابع محض صرفاً بر اساس آرگومانهایش مشخص میشود. اگر این تابع را 1 میلیون بار نیز فراخوانی کنیم و مجموعه آرگومانهای یکسانی به آن بدهیم، خروجی همواره همان خواهد بود. React از این مفهوم در مورد کامپوننتها استفاده میکند. یک کامپوننت ریاکت زمانی یک کامپوننت محض است که خروجی آن صرفاً به props کامپوننت وابسته باشد. همه کامپوننتهای تابعی کامپوننتهای محض هستند:
1const Button = props => {
2 return <button>{props.message}</button>
3}
کامپوننتهای کلاس در صورتی که خروجیشان تنها به props وابسته باشد، میتوانند کامپوننتهای محض باشند:
1class Button extends React.Component {
2 render() {
3 return <button>{this.props.message}</button>
4 }
5}
ترکیببندی
در برنامهنویسی، ترکیببندی امکان ساخت کارکردهای پیچیدهتر را از طریق ترکیببندی تابعهای کوچک و متمرکز فراهم میکند. برای نمونه میتوان از ()map برای ساخت یک آرایه جدید از مجموعه اولیهای استفاده کرد و سپس نتایج را با استفاده از ()filter، فیلتر کرد:
1const list = ['Apple', 'Orange', 'Egg']
2list.map(item => item[0]).filter(item => item === 'A') //'A'
در React، ترکیببندی امکان داشتن مزیتهای کاملاً جالبی را فراهم کرده است. بدین ترتیب میتوان از کامپوننتهای کوچک و روان استفاده کرده و از آنها برای ترکیببندی کارکردهای پیچیدهتر بهره گرفت.
ایجاد نسخه سفارشی از یک کامپوننت
با گسترش و تخصصی ساختن یک کامپوننت خاص، میتوان یک کامپوننت عمومیتر ساخت:
1const Button = props => {
2 return <button>{props.text}</button>
3}
4
5const SubmitButton = () => {
6 return <Button text="Submit" />
7}
8
9const LoginButton = () => {
10 return <Button text="Login" />
11}
ارسال متدها به عنوان props
برای نمونه یک کامپوننت میتواند روی ردگیری یک رویداد کلیک متمرکز شود و آنچه در عمل رخ میدهد این باشد که وقتی رویداد کلیک رخ میدهد عمل متقابل آن بر عهده کامپوننت کانتینر گذاشته شود:
1const Button = props => {
2 return <button onClick={props.onClickHandler}>{props.text}</button>
3}
4
5const LoginButton = props => {
6 return <Button text="Login" onClickHandler={props.onClickHandler} />
7}
8
9const Container = () => {
10 const onClickHandler = () => {
11 alert('clicked')
12 }
13
14 return <LoginButton onClickHandler={onClickHandler} />
15}
استفاده از فرزندان
خصوصیت props.children امکان تزریق کامپوننتها به درون کامپوننتهای دیگر را فراهم میسازد. این کامپوننت باید props.children را در JSX خود به صورت خروجی ارائه کند:
1const Sidebar = props => {
2 return <aside>{props.children}</aside>
3}
شما میتوانید کامپوننتهای بیشتری را به این ترتیب به روشی کاملاً شفاف جاسازی کنید:
1<Sidebar>
2 <Link title="First link" />
3 <Link title="Second link" />
4</Sidebar>
کامپوننتهای درجه بالا
زمانی که یک کامپوننت، کامپوننت دیگری را به عنوان prop دریافت میکند و یک کامپوننت بازگشت میدهد، این کامپوننت بازگشتی به نام کامپوننت درجه بالا نامیده میشود. این نوع کامپوننتها را در ادامه بیشتر بررسی خواهیم کرد.
DOM مجازی
بسیاری از فریمورکهای موجود که پیش از React به صحنه آمدهاند، به طور مستقیم DOM را در هر تغییر دستکاری میکنند. اگر نمیدانید DOM چیست، باید بگوییم که DOM یا «مدل شیء سند» (Document Object Model) یک بازنمایی درختی از صفحه است که از تگ <html> آغاز میشود و به سمت پایین و همه فرزندان که گره نامیده میشوند، حرکت میکند.
این DOM در حافظه مرورگر نگهداری میشود و مستقیماً با آنچه روی صفحه دیده میشود ارتباط دارد. DOM یک API دارد که میتوان آن را پیمایش کرد و به تک تک گرهها دسترسی داشت و آنها را فیلتر کرد یا تغییر داد.
این API ساختار آشنایی دارد و اگر قبلاً از API ارائه شده از سوی jQuery استفاده نکرده باشید نیز احتمالاً این ساختار را بارها دیدهاید:
document.getElementById(id) document.getElementsByTagName(name) document.createElement(name) parentNode.appendChild(node) element.innerHTML element.style.left element.setAttribute() element.getAttribute() element.addEventListener() window.content window.onload window.dump() window.scrollTo()
React یک کپی از بازنمایی DOM برای آنچه «رندرینگ ریاکت» نامیده میشود نگهداری میکند.
توضیح DOM مجازی
هر بار که DOM تغییر مییابد، مرورگر باید دو عملیات وسیع را اجرا کند: یک عملیات «رسم مجدد» (repaint) که در طی آن محتوا یا عناصر بصری روی عنصری تغییر مییابند به طوری که روی طرحبندی و ترکیببندی آن با عناصر دیگر تأثیر نمیگذارد و عملیات دیگر نیز reflow است که در طی آن طرحبندی بخشی از صفحه تغییر مییابد و یا این که طرح کلی صفحه عوض میشود.
React از DOM مجازی برای کمک به مرورگر جهت استفاده کمتر از منابع در زمان نیاز به اجرای تغییرات روی صفحه استفاده میکند. هنگامی که ()setState روی یک کامپوننت فراخوانی میشود، یک حالت متفاوت از حالت قبلی تعیین میشود و React کامپوننت را به صورت dirty معرفی میکند. دقت کنید که ریاکت تنها زمانی بهروزرسانی میشود که یک کامپوننت، حالت (state) آن را صراحتاً تغییر دهد.
سپس اتفاقهای زیر رخ میدهند:
- ریاکت، DOM مجازی را در رابطه با کامپوننتهایی که به صورت dirty علامتگذاری شدهاند به همراه برخی بررسیهای اضافی مانند ردگیری ()shouldComponentUpdate بهروزرسانی میکند.
- الگوریتم diffing اجرا میشود تا تغییرات هماهنگ شوند.
- DOM واقعی بهروزرسانی میشود.
DOM مجازی با استفاده از تجمیع تغییرات چگونه مفید واقع میشود؟
نکته کلیدی در مورد DOM مجازی این است که اغلب تغییرات تجمیع میشوند و یک بهروزرسانی یکتا روی DOM واقعی اعمال میشود و همه عناصری که باید عوض شوند به یکباره تغییر مییابند. بنابراین مرورگر باید عملیات repaint و reflow را اجرا کند تا تغییرات اجرا شده را به یک باره رندر کند.
گردش داده غیر جهتدار
هنگام کار کردن با ریاکت با اصطلاح گردش داده غیر جهتدار مواجه میشویم. اگر میخواهید معنی دقیق آن را بدانید باید بگوییم که گردش داده غیر جهتدار مفهومی منحصر به فرد در React محسوب نمیشود؛ اما توسعهدهندگان جاوا اسکریپت ممکن است نخستین باری باشد که آن را میشنوند.
این مفهوم به طور کلی بدان معنی است که دادهها، یک روش و واقعاً تنها یک روش برای انتقال به بخشهای دیگر اپلیکیشن دارند. ترجمه تعریف فوق در ریاکت به صورت زیر است:
- حالت (State) به view و سپس کامپوننتهای فرزند ارسال میشود.
- اکشنها به وسیله view تحریک میشوند.
- اکشنها میتوانند state را بهروزرسانی کنند.
- تغییرات state به view و کامپوننتهای فرزند آن ارسال میشوند.
View نتیجه state یک اپلیکیشن است. state تنها هنگامی میتواند تغییر یابد که اکشنی رخ داده باشد. زمانی که اکشن رخ دهد، state بهروزرسانی میشود. به لطف اتصالهای یکطرفه دادهها نمیتوانند در مسیر معکوس گردش یابند، چون اتصال دوطرفه وجود ندارد و این وضعیت چندین مزیت کلیدی دارد:
- امکان بروز خطا کاهش مییابد، چون کنترل بیشتری روی دادههای خود دارید.
- دیباگ کردن آسانتر است زیرا میدانید که چه چیز و از کجا میآید.
- کارایی بالاتری دارد زیرا کتابخانه از قبل میداند که کرانهای هر بخش از سیستم چه هستند.
یک state تحت مالکیت کامپوننت است. هر دادهای که تحت تأثیر این state قرار گیرد، میتواند تنها کامپوننتهای زیرش یعنی فرزندان آن را تحت تأثیر قرار دهد. تغییر دادن state روی کامپوننت هرگز والد آن یا همنیاها و یا دیگر کامپوننتهای اپلیکیشن را تحت تأثیر قرار نمیدهد. فقط فرزندان هستند که تأثیر میپذیرند.
به همین جهت است که state در اغلب موارد در درخت کامپوننتها به سمت بالا حرکت میکند، چون بدین ترتیب میتواند بین کامپوننتهایی که به آن دسترسی دارند به اشتراک گذارده شود.
بخش سوم: مفاهیم عمیق ری اکت
در ادامه و در بخش سوم به بررسی مفاهیم عمیق React میپردازیم که فهرست آنها به شرح زیر است:
- جیاسایکس (JSX)
- کامپوننتها
- حالت (State)
- Props
- کامپوننتهای ارائهای در برابر کانتینری
- State در برابر Props
- انواع Prop
- فرگمان ریاکت (React Fragment)
- رویدادها (Events)
- رویدادهای چرخه عمر (Lifecycle Events)
JSX
JSX یک فناوری معرفی شده از سوی React است.
با این که React بدون استفاده از JSX نیز کاملاً به خوبی عمل میکند؛ اما JSX یک فناوری ایدهآل برای کار با کامپوننتها محسوب میشود و از این رو در ریاکت استفاده زیادی از آن میکنیم. شاید در ابتدا به نظر بیاید که JSX مانند ترکیب کردن HTML و جاوا اسکریپت است؛ اما این حرف صحیح نیست. چون وقتی از JSX استفاده میکنیم، در واقع از یک ساختار اعلانی برای نوشتن UI یک کامپوننت بهره میگیریم. بدین ترتیب مشخص میشود که UI از رشتهها استفاده نمیکند؛ بلکه از جاوا اسکریپت استفاده میکند که کارهای جالب زیادی با آن میتوان انجام داد.
مقدمهای بر JSX
در این بخش یک تگ h1 تعریف میکنیم که شامل یک رشته است:
1const element = <h1>Hello، world!</h1>
این تگ شبیه به ترکیبی از جاوا اسکریپت و HTML است؛ اما در واقع کلاً در جاوا اسکریپت نوشته شده است. دلیل این که بخشی از آن شبیه به HTML به نظر میآید، آن است که دارای «فرم دگرگون یافته» (syntactic sugar) برای تعریف کردن کامپوننتها و قرار دادن آنها درون بخش markup است. درون یک عبارت JSX میتوان به سادگی «خصوصیات» (attributes) را درج کرد:
1const myId = 'test'
2const element = <h1 id={myId}>Hello، world!</h1>
کافی است توجه کنید هر جایی که در نام یک خصوصیت از «خط تیره» (-) استفاده شده است، باید به ساختار «حالت شتری» (camelCase) تبدیل شود و همچنین به دو نکته زیر نیز توجه داشته باشید:
- class به className تبدیل میشود.
- for به htmlFor تبدیل میشود.
چون اینها واژههای رزرو شده در جاوا اسکریپت هستند. در کد JSX زیر، دو کامپوننت درون تگ div پیچیده شدهاند:
1<div>
2 <BlogPostsList />
3 <Sidebar />
4</div>
در JSX تگها همواره باید بسته شوند، چون ساختار آنها در قیاس با HTML شباهت بیشتری به XML دارد. اگر با XHTML آشنا باشید، احتمالاً این ساختار برای شما آشنا خواهد بود؛ اما از زمان معرفی HTML5 این محدودیت چندان رعایت نمیشود. در این مورد از یک تگ self-closing استفاده شده است. اگر دقت کنید، متوجه میشوید که ما دو کامپوننت را درون یک div قرار دادهایم. دلیل این مسئله آن است که تابع ()render تنها یک گره منفرد را میتواند بازگشت دهد و از این رو در این مورد میخواهیم دو همنیا را بازگشت دهد و تنها یک والد داشته باشیم. دقت کنید که میتوان از هر تگ دیگری نیز به جز div استفاده کرد.
Transpile کردن JSX
یک مرورگر نمیتواند فایلهای جاوا اسکریپتی را اجرا کند که شامل کد JSX باشند. این کدها ابتدا باید به کد جاوا اسکریپت معمولی تبدیل شوند. به این کار Transpile کردن گفته میشود. قبلاً گفتیم که استفاده از JSX اختیاری است، زیرا هر خط JSX یک جایگزین متناظر ساده جاوا اسکریپت دارد و هنگام Transpile کردن کد به جاوا اسکریپت از این خصوصیت استفاده میکنیم. برای نمونه در ادامه دو ساختار ارائه شدهاند که معادل هم هستند:
جاوا اسکریپت ساده
1ReactDOM.render(
2 React.DOM.div(
3 { id: 'test' },
4 React.DOM.h1(null, 'A title'),
5 React.DOM.p(null, 'A paragraph')
6 ),
7 document.getElementById('myapp')
8)
کد JSX
1ReactDOM.render(
2 <div id="test">
3 <h1>A title</h1>
4 <p>A paragraph</p>
5 </div>,
6 document.getElementById('myapp')
7)
این مثال کاملاً ساده تنها یک نقطه شروع محسوب میشود؛ اما میتوانید از روی همین نمونه نیز متوجه شوید که کد جاوا اسکریپت تا چه حد پیچیدهتر از JSX است.
در حال حاضر متداولترین روش برای Transpile کردن کدهای JSX استفاده از Babel است که اگر اپلیکیشن خود را از مسیر create-react-app ساخته باشید، گزینه پیشفرض محسوب میشود. از این رو اگر از این گزینه برای ایجاد اپلیکیشن ریاکت استفاده کردهاید، جای هیچ نگرانی نیست، چون همه چیز به طور خودکار اجرا خواهد شد؛ اما اگر از create-react-app استفاده نکردهاید، باید Babel را به صوت دستی نصب و راهاندازی کنید.
جاوا اسکریپت در JSX
JSX هر نوع کد ترکیبی جاوا اسکریپت که برایش ارسال شود را میپذیرد. هر زمان که لازم باشد کدهای جاوا اسکریپت دیگری اضافه کنیم، کافی است آن را درون آکولاد قرار دهیم. برای نمونه در کد زیر شیوه استفاده از یک مقدار ثابت را میبینید که در جای دیگری تعریف شده است:
1const paragraph = 'A paragraph'
2ReactDOM.render(
3 <div id="test">
4 <h1>A title</h1>
5 <p>{paragraph}</p>
6 </div>,
7 document.getElementById('myapp')
8)
این یک مثال ساده است. آکولادها هر نوع کد جاوا اسکریپت را میپذیرند:
1const paragraph = 'A paragraph'
2ReactDOM.render(
3 <table>
4 {rows.map((row, i) => {
5 return <tr>{row.text}</tr>
6 })}
7 </div>,
8 document.getElementById('myapp')
9)
همان طور که میبینید ما کد جاوا اسکریپت را درون JSX قرار دادهایم و خود JSX نیز داخل کد جاوا اسکریپتِ تعریف شده درون JSX قرار دارد.
HTML در JSX
با این که ساختار JSX تا حدود زیادی به HTML شباهت دارد؛ اما در واقع ساختار آن شبیه به XML است. ما در نهایت کد HTML را رندر خواهیم کرد و از این رو لازم است با برخی تفاوتهای روش تعریف چیزها در HTML در برابر تعریف کردن آنها در JSX آشنا باشید.
همه تگها باید بسته شوند
همانند XHTML همه تگها در JSX باید بسته شوند. برای نمونه در صورت استفاده از <br> باید در انتها از تگ بستن <br /> استفاده شود.
از استاندارد جدید camelCase استفاده میشود
در HTML خصوصیات دارای حالت حروف کوچک هستند. برای مثال خصوصیتی مانند onchange داریم. در JSX این تگها به صورت معادل «حالت شتری» نوشته میشوند:
- onchange => onChange
- onclick => onClick
- onsubmit => onSubmit
class به className تبدیل میشود
به دلیل این واقعیت که JSX همان جاوا اسکریپت است و class یک کلمه رزرو شده محسوب میشود، نمیتوان کدی مانند زیر نوشت:
1<p class="description">
بلکه باید از کد زیر استفاده کنید:
1<p className="description">
همین موضوع در مورد for نیز صدق میکند که به صورت htmlFor ترجمه میشود.
CSS در React
JSX روشی جالب برای تعریف CSS عرضه کرده است. اگر تجربه اندکی با استایلهای درونخطی در HTML داشته باشید، در نگاه اول حس میکنید که به 10 تا 15 سال قبل پرتاب شدهاید. در آن زمان استفاده از CSS-های درونخطی امری کاملاً معمول بود. این وضعیت امروز دیگر متداول نیست و صرفاً به عنوان یک راهحل «اصلاح سریع» (Quick Fix) نگریسته میشود.
استایل JSX چنین نیست، قبل از هر چیز به جای پذیرش یک رشته شامل مشخصات CSS، خصوصیت style در JSX، یک شیء را میپذیرد. این بدان معنی است که باید مشخصات را در یک شیء تعریف کنیم:
1var divStyle = {
2 color: 'white'
3}
4ReactDOM.render(<div style={divStyle}>Hello World!</div>, mountNode)
یا به صورت زیر عمل کنیم:
1ReactDOM.render(<div style={{ color: 'white' }}>Hello World!</div>, mountNode)
مقادیر CSS که در JSX مینویسیم، اندکی متفاوت از CSS ساده هستند:
- نام مشخصات کلیدها به صورت «حالت شتری» (camelCased) هستند.
- مقادیر صرفاً به صورت رشته هستند.
- چندتاییها با کاما از هم جدا میشوند.
چرا این وضعیت به CSS / SASS / LESS ساده ترجیح داده میشود؟
CSS یک مسئله حلنشده است. از زمان ظهور آن، دهها ابزار پیرامون آن شکل گرفته و سپس نابود شدهاند. مشکل اصلی در مورد جاوا اسکریپت این است که هیچ نوع دامنهبندی ندارد و به سادگی ممکن است کد CSS بنویسیم که به هیچ ترتیبی پیادهسازی نمیشود. در این موارد، میتوان یک «اصلاح سریع» برای عناصری که تأثیر نپذیرفتهاند، نوشت.
JSX به کامپوننتها اجازه میدهد که استایل خودشان را به طور کامل کپسولهسازی کنند.
آیا این راهحل موقت است؟
استایلهای «درونخطی» (inline) در JSX در موارد زیر مفید هستند:
- نوشتن کوئریهای رسانه
- انیمیشنهای استایل
- ارجاع به شبه کلاسها (مانند hover:)
- ارجاع به شبه عنصرها (مانند first-letter::)
به طور خلاصه موارد ابتدایی را مدیریت میکنند؛ اما یک راهحل نهایی محسوب نمیشوند.
فرمها در JSX
JSX تغییراتی در طرز کار HTML ایجاد میکند و هدف آن این است که کارها برای توسعهدهنده آسانتر شود.
خصوصیتهای value و defaultValue
- خصوصیت value مقدار کنونی فیلد را نگهداری میکند.
- خصوصیت defaultValue مقدار پیشفرضی که هنگام ایجاد فیلد تعیین میشود را نگهداری میکند.
این وضعیت به حل برخی از رفتارهای عجیب در زمان کار با DOM معمولی و بررسی ('input.value و input.getAttribute('value کمک میکند و بین دو مقدار کنونی و مقدار پیشفرض فیلد تمایز قائل میشود. این وضعیت در مورد فیلد textarea نیز صدق میکند:
1<textarea>Some text</textarea>
اما به جای آن از کد زیر استفاده میکنیم:
1<textarea defaultValue={'Some text'} />
در مورد فیلدهای select به جای استفاده از کد زیر:
1<select>
2 <option value="x" selected>
3 ...
4 </option>
5</select>
از این کد استفاده میکنیم:
1<select defaultValue="x">
2 <option value="x">...</option>
3</select>
خصوصیت منسجمتر onChange
ارسال یک تابع به خصوصیت onChange باعث میشود که رویدادهایی روی فیلدهای فرم ثبت کنیم. این وضعیت به طور سازگاری روی فیلدها و حتی فیلدهای ورودی radio ،select و checkbox که یک رویداد onChange را اجرا میکنند، عمل میکند. onChange همچنین در مواردی که یک کاراکتر در فیلد input یا textarea وارد میکنید، فعال میشود.
Auto Escapes در JSX
JSX جهت کاهش ریسکهای همیشه موجود XSS، الزام میکند که از خصوصیت auto escapes در عبارتها استفاده کنیم. این به آن معنی است که وقتی گزارههای HTML را در یک عبارت رشتهای مورد استفاده قرار میدهید، با مشکلاتی مواجه خواهید شد. برای نمونه ما میخواهیم با کد زیر عبارت 2017 © را نمایش دهیم:
1<p>{'© 2017'}</p>
اما مشاهده میکنیم که عبارت copy; 2017& نمایش مییابد، زیرا رشته ما escape شده است. جهت اصلاح این وضعیت یا باید گزارههای HTML را به خارج از عبارت ببریم:
1<p>© 2017</p>
یا این که از یک ثابت استفاده کنیم که بازنمایی یونیکد متناظر با گزاره HTML را نمایش میدهد:
1<p>{'\u00A9 2017'}</p>
فاصله خالی در JSX
برای افزودن فاصله خالی در JSX دو روش وجود دارد:
قاعده 1: فاصله خالی افقی به اندازه 1 فاصله کاهش مییابد
اگر بین عناصر در یک خط، فواصل خالی وجود داشته باشد، همه آنها به یک فاصله خالی کاهش مییابند. بدین ترتیب کد زیر:
1<p>Something becomes this</p>
به صورت زیر درمیآید:
1<p>Something becomes this</p>
قاعده 2: فواصل خالی عمودی حذف میشوند
بدین ترتیب کد زیر:
1<p>
2 Something
3 becomes
4 this
5</p>
به صورت زیر درمیآید:
1<p>Somethingbecomesthis</p>
برای حل این مشکل، باید فاصلههای خالی را صریحاً با افزودن یک عبارت فاصله مانند زیر اضافه کنید:
1<p>
2 Something
3 {' '}becomes
4 {' '}this
5</p>
همچنین میتوانید رشتهای را در یک عبارت فاصله (Space) وارد کنید:
1<p>
2 Something
3 {' becomes '}
4 this
5</p>
افزودن کامنت در JSX
با استفاده از کامنتهای معمول جاوا اسکریپت درون یک عبارت میتوان کامنتهایی به کد JSX اضافه کرد:
1<p>
2 {/* a comment */}
3 {
4 //another comment
5 }
6</p>
توزیع خصوصیتها
یک عملیات متداول در JSX، انتساب مقادیر به خصوصیتها است. بدین ترتیب به جای این که این کار را به صورت دستی انجام دهیم:
1<div>
2 <BlogPost title={data.title} date={data.date} />
3</div>
میتوانیم از کد زیر استفاده کنیم:
1<div>
2 <BlogPost {...data} />
3</div>
مشخصات شیء data به لطف عملگر spread در ES6 به صورت خودکار به عنوان خصوصیتها استفاده میشود.
تعریف حلقه در JSX
اگر مجموعهای از عناصر داشته باشید و بخواهید روی آنها حلقهای اجرا کنید تا بخشی از JSX را تولید کنید، میتوانید یک loop ایجاد کنید و سپس JSX را به آرایه اضافه کنید:
1const elements = [] //..some array
2const items = []
3for (const [index, value] of elements.entries() {
4 items.push(<Element key={index} />)
5}
اینک زمانی که JSX را رندر کنید، میتوانید آرایه items را به سادگی با قرار دادن داخل آکولاد، جاسازی کنید:
1const elements = ['one', 'two', 'three'];
2const items = []
3for (const [index, value] of elements.entries() {
4 items.push(<li key={index}>{value}</li>)
5}
6return (
7 <div>
8 {items}
9 </div>
10)
همین کار را میتوان مستقیماً در JSX با استفاده از map به جای حلقه for اجرا کرد:
1const elements = ['one', 'two', 'three'];
2return (
3 <ul>
4 {elements.map((value, index) => {
5 return <li key={index}>{value}</li>
6 })}
7 </ul>
8)
کامپوننتها
یک کامپوننت در واقع بخش مجزایی از اینترفیس است. برای نمونه در صفحه اصلی یک وبلاگ معمولی میتوان کامپوننت سایدبار را مشاهده کرد و همچنین احتمالاً دارای یک کامپوننت فهرست مطالب بلاگ نیز هست. این کامپوننتها به نوبه خود از کامپوننتهای دیگری تشکیل یافتهاند و از این رو میتوان کامپوننتهایی برای فهرست مطالب وبلاگ داشت که هر مطلب خود یک کامپوننت بوده و هر یک دارای مشخصات خاصی است.
- ریاکت همه چیز را ساده ساخته است، چون در ریاکت همه چیز یک کامپوننت محسوب میشود.
- حتی تگهای ساده HTML نیز خود یک کامپوننت هستند و به صورت پیشفرض اضافه میشوند.
دو خط بعدی کد زیر معادل هم هستند و کار یکسانی انجام میدهد. یکی از آنها با JSX و دیگری بدون آن نوشته شده و <h1>Hello World!</h1> درون یک عنصر با id به نام app تزریق شده است.
1import React from 'react'
2import ReactDOM from 'react-dom'
3ReactDOM.render(<h1>Hello World!</h1>, document.getElementById('app'))
4ReactDOM.render(
5 React.DOM.h1(null, 'Hello World!'),
6 document.getElementById('app')
7)
میبینید که React.DOM یک کامپوننت h1 در اختیار ما قرار داده است. همه تگهای دیگر HTML نیز موجود هستند. آنها را میتوانید با وارد کردن React.DOM در کنسول مرورگر مشاهده کنید. کامپوننتهای داخلی جالب هستند؛ اما چندان به کار نمیآیند. نقطه قوت ریاکت در این است که به ما اجازه میدهد یک UI را با ترکیب کردن کامپوننتهای سفارشی بسازیم.
کامپوننتهای سفارشی
دو روش برای تعریف کردن یک کامپوننت در ریاکت وجود دارد:
کامپوننت تابع
1const BlogPostExcerpt = () => {
2 return (
3 <div>
4 <h1>Title</h1>
5 <p>Description</p>
6 </div>
7 )
8}
کامپوننت کلاس
1import React, { Component } from 'react'
2class BlogPostExcerpt extends Component {
3 render() {
4 return (
5 <div>
6 <h1>Title</h1>
7 <p>Description</p>
8 </div>
9 )
10 }
11}
تا همین اواخر کامپوننتهای کلاس تنها روش ما برای تعریف کردن یک کامپوننت بودند که دارای State باشد و بتواند به متدهای چرخه عمر دسترسی داشته باشد و کارهایی را در زمان رندر اولیه، بهروزرسانی یا حذف کامپوننت انجام دهد.
«قلابها» (Hooks) در ریاکت این وضعیت را تغییر دادهاند و بنابراین کامپوننتهای تابع ما اینک میتوانند بسیار قدرتمندتر از همیشه باشند و در آینده احتمالاً شاهد کامپوننتهای کلاس چندانی نخواهیم بود؛ هر چند کامپوننتهای کلاس هنوز هم روشی معتبر برای ایجاد کامپوننت خواهند بود. ساختار سومی نیز وجود دارد که از دستورهای ES6 بدون کلاس استفاده میکند:
1import React from 'react'
2React.createClass({
3 render() {
4 return (
5 <div>
6 <h1>Title</h1>
7 <p>Description</p>
8 </div>
9 )
10 }
11})
امروزه در کدهای مدرن ES6 دیگر شاهد چنین ساختارهایی نیستیم.
State
در این بخش در مورد مفهوم State در ریاکت توضیح خواهیم داد.
تعیین State پیشفرض برای یک کامپوننت
this.state را در سازنده کامپوننت مقداردهی اولیه میکنیم. برای نمونه کامپوننت BlogPostExcerpt میتواند یک State به صورت clicked داشته باشد:
1class BlogPostExcerpt extends Component {
2 constructor(props) {
3 super(props)
4 this.state = { clicked: false }
5 }
6 render() {
7 return (
8 <div>
9 <h1>Title</h1>
10 <p>Description</p>
11 </div>
12 )
13 }
14}
دسترسی به State
State به نام clicked میتواند با ارجاع به this.state.clicked مورد دسترسی قرار گیرد:
1class BlogPostExcerpt extends Component {
2 constructor(props) {
3 super(props)
4 this.state = { clicked: false }
5 }
6 render() {
7 return (
8 <div>
9 <h1>Title</h1>
10 <p>Description</p>
11 <p>Clicked: {this.state.clicked}</p>
12 </div>
13 )
14 }
15}
تغییرپذیر ساختن State
یک State را هرگز نباید با کد زیر تغییر داد:
1this.state.clicked = true
به جای آن؛ همواره باید از ()setState استفاده کرد و آن را به یک شیء ارسال کرد:
1this.setState({ clicked: true })
این شیء میتواند شامل یک زیرمجموعه یا یک ابرمجموعه (superset) از State باشد. تنها مشخصاتی که ارسال میکنید، تغییر خواهند یافت و آنهایی که نادیده گرفته شوند، در State کنونی باقی میمانند.
چرا همواره باید از ()setState استفاده کنیم؟
دلیل این مسئله آن است که در این متد، ریاکت میداند که State تغییر یافته است. سپس شروع به یک سری از رویدادها میکند که منجر به رندر گیری مجدد از کامپوننت و همچنین بهروزرسانی DOM میشود.
گردش داده غیر جهتدار
یک State همواره تحت مالکیت یک کامپوننت است. هر دادهای که از این State تأثیر بپذیرد، تنها کامپوننتهای زیرش یعنی فرزندانش را تحت تأثیر قرار میدهد.
تغییر دادن State یک کامپوننت هرگز بر والدین آن یا همنیاهایش و یا هر کامپوننت دیگر در اپلیکیشن تأثیر نمیگذارد و صرفاً فرزندانش را تحت تأثیر قرار میدهد.
به همین دلیل است که State در اغلب موارد به درخت کامپوننت منتقل میشود.
انتقال دادن State به درخت
به دلیل وجود قاعده گردش غیر جهتدار داده، اگر دو کامپوننت نیاز به اشتراک State داشته باشند، این State باید به جد مشترک آنها انتقال یابد. در اغلب موارد نزدیکترین جد، بهترین مکان برای مدیریت State است؛ اما این یک قاعده اجباری نیست. State به کامپوننتهایی که به آن نیاز دارند از طریق props ارسال میشود:
1class Converter extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = { currency: '€' }
5 }
6 render() {
7 return (
8 <div>
9 <Display currency={this.state.currency} />
10 <CurrencySwitcher currency={this.state.currency} />
11 </div>
12 )
13 }
14}
در این مرحله State میتواند به وسیله کامپوننت فرزند از طریق ارسال یک تابع تغییردهنده (mutating) به صورت یک prop تغییر یابد:
1class Converter extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = { currency: '€' }
5 }
6 handleChangeCurrency = event => {
7 this.setState({ currency: this.state.currency === '€' ? '$' : '€' })
8 }
9 render() {
10 return (
11 <div>
12 <Display currency={this.state.currency} />
13 <CurrencySwitcher
14 currency={this.state.currency}
15 handleChangeCurrency={this.handleChangeCurrency}
16 />
17 </div>
18 )
19 }
20}
21const CurrencySwitcher = props => {
22 return (
23 <button onClick={props.handleChangeCurrency}>
24 Current currency is {props.currency}. Change it!
25 </button>
26 )
27}
28const Display = props => {
29 return <p>Current currency is {props.currency}.</p>
30}
Props
Props به شیوه دریافت مشخصات از سوی کامپوننتها گفته میشود. اگر از کامپوننت فوقانی شروع کنیم، هر کامپوننت فرزند، prop-های خود را از والد دریافت میکند. در یک کامپوننت تابع، props همه آن چیزی است که ارسال میشود و با افزودن props به عنوان آرگومان تابع قابل دسترسی هستند:
1const BlogPostExcerpt = props => {
2 return (
3 <div>
4 <h1>{props.title}</h1>
5 <p>{props.description}</p>
6 </div>
7 )
8}
در یک کامپوننت کلاس، props به صورت پیشفرض ارسال میشوند. نیازی به افزودن هیچ چیز خاصی نیست و از طریق this.props در یک وهله از کامپوننت در دسترس هستند:
1import React, { Component } from 'react'
2class BlogPostExcerpt extends Component {
3 render() {
4 return (
5 <div>
6 <h1>{this.props.title}</h1>
7 <p>{this.props.description}</p>
8 </div>
9 )
10 }
11}
ارسال props به کامپوننتهای فرزند، روشی عالی برای ارسال مقادیر درون یک اپلیکیشن محسوب میشود. یک کامپوننت یا دادهها را نگهداری میکند، یعنی دارای State است و یا دادهها را از طریق props خود دریافت میکند.
در موارد زیر این وضعیت پیچیده میشود:
- اگر بخواهیم به State یک کامپوننت از یک فرزند دسترسی داشته باشیم که چند سطح پایینتر است. چون همه کامپوننتهای میانی باید پست سر گذاشته شوند، هر چند به دانستن State نیازی نداشته باشند و این موجب پیچیدهتر شدن امور میشود.
- در مواردی که نیاز باشد State یک کامپوننت از یک کامپوننت کاملاً نامرتبط گرفته شود.
مقادیر پیشفرض برای Props
اگر هر مقداری مورد نیاز نباشد، باید در صورتی که در زمان مقداردهی اولیه مقداری تعیین نشده باشد، یک مقدار پیشفرض برای آن تعیین کنیم:
1BlogPostExcerpt.propTypes = {
2 title: PropTypes.string,
3 description: PropTypes.string
4}
5BlogPostExcerpt.defaultProps = {
6 title: '',
7 description: ''
8}
برخی کیت ابزارها مانند ESLint (+) توانایی الزام تعریف کردن defaultProps برای یک کامپوننت با برخی propTypes که صریحاً مورد نیاز نیستند را فراهم ساختهاند.
Props چگونه ارسال میشوند؟
هنگامی که یک کامپوننت مقداردهی اولیه میشود، props به روشی مشابه خصوصیتهای HTML ارسال میشود:
1const desc = 'A description'
2//...
3<BlogPostExcerpt title="A blog post" description={desc} />
ما عنوان را به صورت یک رشته ساده و توضیح را به صورت یک متغیر ارسال میکنیم.
فرزندان (Children)
یک prop خاص وجود دارد که نام آن Children است. این prop شامل مقدار هر چیزی است که در body کامپوننت ارسال میشود. برای نمونه:
1<BlogPostExcerpt title="A blog post" description="{desc}">
2 Something
3</BlogPostExcerpt>
در این حالت، درون BlogPostExcerpt میتوانیم با بررسی this.props.children به Something دسترسی داشته باشیم.
با این که props به کامپوننت امکان میدهد که مشخصاتش را از والدینش دریافت کند و برای نمونه جهت نمایش برخی دادهها دستوراتی دریافت کند؛ اما State به کامپوننت اجازه میدهد که خودش حیات پیدا کند و مستقل از محیط پیرامون خودش باشد.
کامپوننتهای ارائهای در برابر کامپوننتهای کانتینر
در ریاکت، کامپوننتها غالباً به دو دسته بزرگ تقسیم میشوند که شامل «کامپوننتهای ارائهای» (Presentational) و «کامپوننتهای کانتینری» (container) هستند. هر کدام از این انواع دارای مشخصات منحصر به خود هستند.
کامپوننتهای ارائهای غالباً در مورد ایجاد برخی کدهای «نشانهگذاری» (markup) که در خروجی ارائه میشوند مورد استفاده قرار میگیرند. این کامپوننتها هیچ نوع State مگر آنها که برای ارائه استفاده میشوند را مدیریت نمیکنند.
کامپوننتهای کانتینر غالباً به عملیات «بکاند» (Back-end) مرتبط هستند. این کامپوننتها State کامپوننتهای فرعی مختلف را مدیریت میکنند. آنها میتوانند چندین کامپوننت ارائهای را در خود جای دهند. این وضعیت با استفاده از Redux قابل پیادهسازی به صورت اینترفیس است.
برای این که این تمایز را به بیانی سادهتر مطرح کنیم، میتوانیم بگوییم که کامپوننتهای ارائهای به نمای اپلیکیشن مرتبط هستند و کامپوننتهای کانتینر به طرز کار بخشهای مختلف ارتباط دارند.
برای نمونه، کامپوننت زیر از نوع ارائهای است و دادهها را از props خود میگیرد و صرفاً روی نمایش یک عنصر متمرکز است:
1const Users = props => (
2 <ul>
3 {props.users.map(user => (
4 <li>{user}</li>
5 ))}
6 </ul>
7)
از سوی دیگر کامپوننت زیر از نوع کانتینر است و به مدیریت و ذخیرهسازی دادههای خود پرداخته و از کامپوننت ارائهای برای نمایش آن استفاده میکند:
1class UsersContainer extends React.Component {
2 constructor() {
3 this.state = {
4 users: []
5 }
6 }
7 componentDidMount() {
8 axios.get('/users').then(users =>
9 this.setState({ users: users }))
10 )
11 }
12 render() {
13 return <Users users={this.state.users} />
14 }
15}
State در برابر Props
در یک کامپوننت React، منظور از props متغیرهایی هستند که از سوی کامپوننت والد ارسال میشوند. در سوی دیگر state نیز متغیر است، اما مستقیماً از سوی کامپوننت مقداردهی شده و مدیریت میشود.دقت کنید که state میتواند به وسیله props مقداردهی شود.
برای نمونه یک کامپوننت والد ممکن است کامپوننت فرزندش را با فراخوانی کد زیر شامل شود:
1<ChildComponent />
در این وضعیت، والد یک prop را با استفاده از ساختار زیر ارسال میکند:
1<ChildComponent color=green />
درون سازنده ChildComponent میتوانیم به صورت زیر به prop دسترسی داشته باشیم:
1class ChildComponent extends React.Component {
2 constructor(props) {
3 super(props)
4 console.log(props.color)
5 }
6}
و هر متد دیگر در این کلاس میتواند با استفاده از this.props به props اشاره کند. props میتوانند به صورت زیر برای تعیین state درونی بر اساس یک مقدار prop در سازنده مورد استفاده قرار گیرند:
1class ChildComponent extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state.colorName = props.color
5 }
6}
البته یک کامپوننت میتواند state را بدون بررسی props آن نیز مقداردهی کند. این وضعیت فایده چندانی ندارد؛ اما تصور کنید قرار است بر اساس مقدار prop کارهای متفاوتی انجام دهید، احتمالاً تعیین مقدار یک state ، بهترین کار است. prop-ها هرگز نباید در یک کامپوننت فرزند تغییر یابند، بنابراین اگر چیزی در آن باشد که بخواهد متغیری را تغییر دهد، آن متغیر باید به state کامپوننت تعلق داشته باشد.
prop-ها میتوانند برای ایجاد امکان دسترسی کامپوننتهای فرزند به متدهای تعریف شده در کامپوننت والد نیز مورد استفاده قرار گیرند. این یک روش مناسب برای مدیریت متمرکز state در کامپوننت والد است. در این وضعیت دیگر لازم نیست که کامپوننتهای فرزند state خاصی برای خود داشته باشند.
اغلب کامپوننتها صرفاً نوعی اطلاعات را بر مبنای prop-ی که دریافت کردهاند نمایش میدهند و از این رو «بیحالت» (stateless) هستند.
انواع Prop
از آنجا که جاوا اسکریپت زبانی با نوعبندی دینامیک است، در عمل نیازی به داشتن روشی برای الزام تعیین نوع یک متغیر در زمان کامپایل نداریم و اگر از انواع نادرستی استفاده کنیم، صرفاً در زمان اجرا با مشکل مواجه میشویم. در صورتی که انواع ارسالی سازگار اما غیر مورد انتظار باشند، نتایج عجیبی در زمان اجرا به دست خواهد آمد.
در این زمینه Flow و TypeScrip کمک زیادی میکنند؛ اما ریاکت روشی برای کمک مستقیم در مورد انواع props دارد و این کار را حتی پیش از اجرای کد انجام میدهد. ابزارهای ما (ویرایشگرها و linter-ها میتوانند زمانی که مقادیر نادرستی ارسال میکنیم، آن را تشخیص دهند:
1import PropTypes from 'prop-types'
2import React from 'react'
3class BlogPostExcerpt extends Component {
4 render() {
5 return (
6 <div>
7 <h1>{this.props.title}</h1>
8 <p>{this.props.description}</p>
9 </div>
10 )
11 }
12}
13BlogPostExcerpt.propTypes = {
14 title: PropTypes.string,
15 description: PropTypes.string
16}
17export default BlogPostExcerpt
از چه نوعهایی میتوانیم استفاده کنیم؟
در ادامه انواع بنیادینی که میتوانیم بپذیریم را مشاهده میکنید:
- PropTypes.array
- PropTypes.bool
- PropTypes.func
- PropTypes.number
- PropTypes.object
- PropTypes.string
- PropTypes.symbol
ما میتوانیم یکی از دو نوع زیر را بپذیریم:
1PropTypes.oneOfType([
2 PropTypes.string,
3 PropTypes.number
4]),
ما میتوانیم یکی از مقادیر متعدد را بپذیریم:
1PropTypes.oneOf(['Test1'، 'Test2'])،
ما میتوانیم یک وهله از کلاس را بپذیریم:
1PropTypes.instanceOf(Something)
ما میتوانیم هر گره React را بپذیریم:
1PropTypes.node
یا حتی به طور کلی هر نوعی را بپذیریم:
1PropTypes.any
آرایهها ساختار خاصی دارند که میتوانیم برای پذیرش یک آرایه از نوع خاص استفاده کنیم:
1PropTypes.arrayOf(PropTypes.string)
ما میتوانیم مشخصات شیء را با استفاده از کد زیر ترکیب کنیم:
1PropTypes.shape({
2 color: PropTypes.string,
3 fontSize: PropTypes.number
4})
الزام مشخصات
الصاق به هر گزینه PropTypes موجب خواهد شد که ریاکت در موارد فقدان آن مشخصه، یک خطا بازگشت دهد:
1PropTypes.arrayOf(PropTypes.string).isRequired,
2PropTypes.string.isRequired,
فرگمان React
دقت کنید که چگونه مقادیر بازگشتی را در یک div قرار میدهیم. دلیل این امر آن است که یک کامپوننت میتواند صرفاً یک عنصر منفرد بازگشت دهد و اگر بخواهید بیش از یکی داشته باشید، باید آن را درون یک تگ کانتینر دیگر قرار دهید. با این وجود، این وضعیت منجر به وجود یک div غیر ضروری در خروجی میشود. شما میتوانید با استفاده از React.Fragment از این وضعیت اجتناب کنید:
1import React, { Component } from 'react'
2class BlogPostExcerpt extends Component {
3 render() {
4 return (
5 <React.Fragment>
6 <h1>{this.props.title}</h1>
7 <p>{this.props.description}</p>
8 </React.Fragment>
9 )
10 }
11}
ضمناً این وضعیت ساختار خلاصه بسیار زیبایی به صورت </><> دارد که صرفاً در نسخههای اخیر و Babel 7 به بعد پشتیبانی میشود:
1import React, { Component } from 'react'
2class BlogPostExcerpt extends Component {
3 render() {
4 return (
5 <>
6 <h1>{this.props.title}</h1>
7 <p>{this.props.description}</p>
8 </>
9 )
10 }
11}
رویدادها (Events)
ریاکت یک روش آسان برای مدیریت رویدادها ارائه کرده است. بدین ترتیب دیگر باید با addEventListener خداحافظی کنیم. در بخش قبلی در مورد state، مثال زیر را مشاهده کردیم:
1const CurrencySwitcher = props => {
2 return (
3 <button onClick={props.handleChangeCurrency}<