خلاصه سریع ↬
React ایده انتشار زمینه را در برنامه‌های ما با API متنی خود، جایگزینی برای حفاری و همگام‌سازی حالت‌ها در بخش‌های مختلف برنامه‌ها، رایج کرد. این مقاله معرفی مختصری از انتشار متن در جاوا اسکریپت ارائه می‌کند و نشان می‌دهد که هیچ جادویی در پشت برخی از مفیدترین APIهای React وجود ندارد.

React با استفاده از API متنی خود، ایده انتشار متن را در برنامه های ما رایج کرد. در دنیای React، زمینه به عنوان جایگزینی برای حفاری و همگام سازی حالت در بخش های مختلف برنامه ها استفاده می شود.

“Context راهی برای انتقال داده ها از طریق درخت کامپوننت بدون نیاز به ارسال props به صورت دستی در هر سطح فراهم می کند.”

– React Docs

می‌توانید زمینه React را به‌عنوان نوعی «کرم‌چاله» تصور کنید که می‌توانید مقادیر را از جایی در بالای درخت مؤلفه‌تان عبور دهید و در مؤلفه‌های فرزندانتان به آن‌ها دسترسی پیدا کنید.

قطعه زیر یک نمونه نسبتا ساده (و تقریباً بی‌فایده) از API زمینه React است، اما نشان می‌دهد که چگونه می‌توانیم از مقادیر تعریف‌شده بالاتر در درخت کامپوننت استفاده کنیم بدون اینکه آنها را به صراحت به مؤلفه‌های فرزند منتقل کنیم.

در قطعه زیر، ما برنامه خود را داریم که دارای یک Color جزء در آن آن مولفه رنگی پیامی را نشان می‌دهد که حاوی پیامی است که در مؤلفه اصلی خود تعریف شده است – برنامه، فقط بدون اینکه مستقیماً به عنوان پشتوانه به مؤلفه منتقل شود، بلکه – با استفاده از «به‌طور جادویی» ظاهر شود. useContext.

import {createContext, useContext} from 'react'

const MyContext = createContext();

function App() {
  return (
    <MyContext.Provider value={color: "red"} >
      <Color/ >
    </MyContext.Provider >
  );
}

function Color() {
  const {color} = useContext(MyContext);

  return <span >Your color is: {color}</span >
}

در حالی که مورد استفاده برای انتشار زمینه هنگام ساختن برنامه های کاربردی رو به روی کاربر با چارچوب UI واضح است، نیاز به یک API مشابه حتی زمانی که اصلاً از چارچوب UI استفاده نمی شود یا حتی زمانی که UI ساخته نمی شود وجود دارد.

چرا باید به این موضوع اهمیت دهیم؟

به نظر من، دو دلیل برای اجرای واقعی آن وجود دارد.

اول، به عنوان یک کاربر یک فریم ورک – بسیار مهم است که بدانیم چگونه کارها را انجام می دهد. ما اغلب به ابزارهایی که استفاده می کنیم به عنوان “جادو” و چیزهایی که فقط کار می کنند نگاه می کنیم. تلاش برای ساختن بخش‌هایی از آن‌ها برای خود، آن را ابهام می‌کند و به شما کمک می‌کند تا ببینید که هیچ جادویی در کار نیست و زیر کاپوت، همه چیز می‌تواند بسیار ساده باشد.

دوم، Context API می تواند هنگام کار بر روی برنامه های غیر UI نیز مفید باشد.

هر زمان که یک برنامه متوسط ​​تا بزرگ بسازیم، با توابعی روبرو می شویم که یکدیگر را فراخوانی می کنند و پشته تماس ممکن است چندین لایه عمیق شود. مجبور به ارسال آرگومان ها به پایین تر می تواند آشفتگی زیادی ایجاد کند – به خصوص اگر از همه این متغیرها در همه سطوح استفاده نکنید. در دنیای React، ما آن را “حفاری پایه” می نامیم.

از طرف دیگر، اگر شما یک نویسنده کتابخانه هستید و به تماس‌هایی که مصرف‌کننده به شما ارسال می‌کند تکیه می‌کنید، ممکن است متغیرهایی در سطوح مختلف زمان اجرا خود داشته باشید و می‌خواهید در پایین‌تر نیز در دسترس باشند. به عنوان مثال، یک چارچوب تست واحد را در نظر بگیرید.

describe('add', () => {
  it('Should add two numbers', () => {
    expect(add(1, 1)).toBe(2);
  });
});

در مثال زیر این ساختار را داریم:

  1. describe فراخوانی می شود و تابع callback ارسال شده به آن را فراخوانی می کند.
  2. در پاسخ به تماس، ما یک it زنگ زدن.

چه کاری می خواهیم انجام شود؟

بیایید اکنون پیاده سازی اساسی را برای چارچوب تست واحد خود بنویسیم. من از روش بسیار ساده لوحانه و خوشبختی استفاده می کنم تا کد را تا حد امکان ساده کنم، اما این، البته، چیزی نیست که در زندگی واقعی از آن استفاده کنید.

function describe(description, callback) {
  callback()
}

function it(text, callback) {
  try {
    callback()
    console.log("✅ " + text)
} catch {
    console.log("🚨 " + text)
  }
}

در مثال بالا، تابع “describe” را داریم که callback آن را فراخوانی می کند. این پاسخ تماس ممکن است شامل تماس‌های مختلفی به «آن» باشد. “آن” به نوبه خود، موفقیت یا عدم موفقیت آزمون را ثبت می کند.

بیایید فرض کنیم که همراه با پیام آزمایشی، می‌خواهیم پیام را از «describe» نیز ثبت کنیم:

describe('calculator: Add', () => {
  it("Should correctly add two numbers", () => {
    expect(add(1, 1)).toBe(2);
  });
});

پیام آزمایشی همراه با توضیحات به کنسول وارد می شود:

"calculator: Add > ✅ Should correctly add two numbers"

برای انجام این کار، باید به نحوی پیغام توضیحات “Hop over” کد کاربر را داشته باشیم و به نوعی راه خود را در اجرای تابع “it” پیدا کنیم.

چه راه حل هایی را می توانیم امتحان کنیم؟

هنگام تلاش برای حل این مشکل، چندین رویکرد وجود دارد که ممکن است امتحان کنیم. من سعی خواهم کرد به چند مورد اشاره کنم و نشان دهم که چرا ممکن است در سناریوی ما مناسب نباشند.

  • استفاده از “این”
    ما می‌توانیم یک کلاس را نمونه‌سازی کنیم و داده‌ها را از طریق “this” منتشر کنیم، اما در اینجا دو مشکل وجود دارد. “این” بسیار دشوار است. همیشه آن‌طور که انتظار می‌رود کار نمی‌کند، مخصوصاً هنگام فاکتورگیری توابع پیکان، که از محدوده واژگانی برای تعیین مقدار فعلی «this» استفاده می‌کنند، به این معنی که مصرف‌کنندگان ما باید از کلمه کلیدی تابع استفاده کنند. در کنار آن، هیچ رابطه ای بین “تست” و “توصیف” وجود ندارد، بنابراین هیچ راه واقعی برای به اشتراک گذاشتن نمونه فعلی وجود ندارد.
  • انتشار یک رویداد
    برای انتشار یک رویداد، به کسی نیاز داریم که آن را بگیرد. اما اگر چندین سوئیت همزمان داشته باشیم چه؟ از آنجایی که ما هیچ رابطه ای بین تماس های آزمایشی و “توضیحات” محترم آنها نداریم، چه چیزی مانع از آن می شود که سایر مجموعه ها نیز رویدادهای خود را دریافت کنند؟
  • ذخیره پیام در یک شی جهانی
    اشیاء جهانی از همان مشکلاتی رنج می برند که یک رویداد را منتشر می کنند، و همچنین، ما دامنه جهانی را آلوده می کنیم. داشتن یک شی سراسری همچنین به این معنی است که مقدار متن ما را می توان از خارج از اجرای تابع ما بررسی و حتی اصلاح کرد، که می تواند بسیار خطرناک باشد.
  • پرتاب خطا
    این می تواند از نظر فنی کار کند: “توصیف” ما می تواند خطاهای ایجاد شده توسط “it” را تشخیص دهد، اما به این معنی است که در اولین شکست، اجرا را متوقف می کنیم و هیچ آزمایش دیگری نمی تواند اجرا شود.
بیشتر بعد از پرش! ادامه مطلب زیر ↓

زمینه برای نجات!

تا به حال، حتما حدس زده اید که من از راه حلی دفاع می کنم که از نظر طراحی تا حدودی شبیه به API زمینه خود React باشد، و فکر می کنم که نمونه آزمایش واحد پایه ما می تواند کاندیدای خوبی برای آزمایش آن باشد.

آناتومی زمینه

بیایید قسمت‌هایی را که زمینه React از آن‌ها تشکیل شده است را بررسی کنیم:

  1. React.createContext – یک زمینه جدید ایجاد می کند، اساساً یک ظرف تخصصی جدید برای ما تعریف می کند.
  2. ارائه دهنده – مقدار بازگشتی createContext. این یک شی با ویژگی “ارائه دهنده” است. ویژگی ارائه دهنده به خودی خود یک مؤلفه است و هنگامی که در برنامه React استفاده می شود، ورودی به “wormhole” ما است.
  3. React.useContext – تابعی که وقتی در درخت React که با یک زمینه پیچیده شده فراخوانی می شود، به عنوان نقطه خروجی از کرم چاله ما عمل می کند و اجازه می دهد تا مقادیر را از آن خارج کنیم.

بیایید نگاهی به زمینه خود React بیندازیم:

اسکرین شات از کدنویسی
شیء زمینه React (پیش نمایش بزرگ)

به نظر می رسد شیء زمینه React بسیار پیچیده است. این شامل یک ارائه دهنده و یک مصرف کننده است که در واقع عناصر React هستند. بیایید این ساختار را در آینده در ذهن داشته باشیم.

با دانستن آنچه که اکنون در مورد زمینه React می دانیم، بیایید فکر کنیم که چگونه بخش های مختلف آن باید با نمونه تست واحد ما تعامل داشته باشند. من قصد دارم یک سناریوی مزخرف بسازم تا بتوانیم اجزای مختلف را در زندگی واقعی تصور کنیم.

const TestContext = createContext()

function describe(description, callback) {
  // <TestContext.Provider value={{description}} >
callback()
  // </TestContext.Provider >
}

function it(text, callback) {
  // const { description } = useContext(TestContext);

  try {
    callback()
    console.log(description + " > ✅ " + text)
  } catch {
    console.log(description+ " > 🚨 " + text)
  }
}

اما واضح است که این کار نمی تواند کار کند. اول، ما نمی توانیم از React Elements در کد وانیلی JS خود استفاده کنیم. دوم، ما نمی توانیم از زمینه React خارج از React استفاده کنیم. درست؟ درست.

بنابراین بیایید آن ساختار را با JS واقعی تطبیق دهیم:

const TestContext = createContext()

function describe(description, callback) {

  TestContext.Provider({description}, () => {
callback()
  });
}

function it(text, callback) {
  const { description } = useContext(TestContext);

  try {
    callback()
    console.log(description + " > ✅ " + text)
  } catch {
    console.log(description+ " > 🚨 " + text)
  }
}

بسیار خوب، پس این شروع به ظاهر بیشتر شبیه جاوا اسکریپت می کند. اینجا چی داریم؟

خوب، بیشتر — به جای مولفه ContextProvider خود، از TextContext.Provider استفاده می کنیم که یک شی را با ارجاع به مقادیر ما می گیرد و useContext() که به عنوان پورتال ما عمل می کند – بنابراین ما می توانیم به کرم چاله خود ضربه بزنیم.

اما آیا این کار می تواند کار کند؟ بیایید تلاش کنیم.

پیش نویس API ما

اکنون که مفهوم کلی نحوه استفاده از زمینه خود را داریم، بیایید با تعریف عملکردهایی که قرار است در معرض نمایش قرار دهیم شروع کنیم. از آنجایی که ما از قبل می دانیم که React Context API چگونه است، می توانیم آن را بر اساس آن قرار دهیم.

function createContext() {
  return {
    Provider,
    Consumer
  }

  function Provider(value, callback) {}

  function Consumer() {}
}

function useContext(ctxRef) {}

ما در حال تعریف دو تابع هستیم، درست مانند React. createContext و useContext. createContext یک ارائه دهنده و یک مصرف کننده را درست مانند زمینه React برمی گرداند و useContext در یک مرجع متنی استفاده می کند.

برخی از مفاهیمی که قبل از غواصی باید از آنها آگاه باشیم

کاری که از اینجا به بعد انجام خواهیم داد بر اساس دو ایده اصلی است که برای توسعه دهندگان جاوا اسکریپت مهم هستند. من قصد ندارم آنها را در اینجا توضیح دهم، اما اگر در مورد این موضوعات احساس ترس می کنید، بیشتر تشویق می شوید که در مورد آنها مطالعه کنید:

  1. بسته شدن جاوا اسکریپت
    از MDN: “آ بسته ترکیبی از یک تابع همراه (محصور) با ارجاع به حالت اطراف آن (the محیط واژگانی). به عبارت دیگر، بسته شدن به شما امکان دسترسی به محدوده عملکرد بیرونی از یک تابع داخلی را می دهد. در جاوا اسکریپت، هر بار که یک تابع ایجاد می شود، در زمان ایجاد تابع، بسته ها ایجاد می شوند.
  2. ماهیت همزمان جاوا اسکریپت در پایه خود، جاوا اسکریپت همزمان و مسدود کننده است. بله، این برنامه دارای وعده‌های همگام‌سازی، تماس‌های برگشتی و همگام‌سازی/انتظار است – و آنها نیاز به مدیریت خاصی دارند، اما در بیشتر موارد، اجازه دهید جاوا اسکریپت را همزمان در نظر بگیریم، زیرا مگر اینکه به آن حوزه‌ها یا پیاده‌سازی‌های بسیار عجیب و غریب مرورگر Edge case دست پیدا کنیم. کد جاوا اسکریپت همزمان است.

این دو ایده به ظاهر نامرتبط چیزی هستند که به زمینه ما اجازه کار می دهند. فرض این است که، اگر مقداری را در داخل تعیین کنیم Provider و با فراخوانی ما تماس بگیرید، مقادیر ما در تمام طول اجرای تابع همزمان ما باقی خواهند ماند و در دسترس خواهند بود. ما فقط به راهی برای دسترسی به آن نیاز داریم. این چیزی است که useContext است برای.

ذخیره ارزش ها در زمینه ما

متن برای انتشار داده ها در پشته تماس ما استفاده می شود، بنابراین اولین کاری که می خواهیم انجام دهیم این است که در واقع اطلاعات را روی آن ذخیره کنیم. بیایید a را تعریف کنیم contextValue متغیر درون ما createContext عملکرد. مقیم در بسته شدن createContext، تضمین می کند که تمام توابع تعریف شده در createContext حتی بعداً به آن دسترسی خواهند داشت.

function createContext() {
  let contextValue = undefined;

  function Provider(value, callback) {}

  function Consumer() {}

  return {
    Provider,
    Consumer
  }
}

اکنون، که ما مقدار ذخیره شده در متن، ما را داریم Provider تابع می تواند مقداری را که می پذیرد روی آن ذخیره کند و Consumer تابع می تواند آن را برگرداند.

function createContext() {
  let contextValue = undefined;

  function Provider(value, callback) {
    contextValue = value;
  }

  function Consumer() {
    return contextValue;
  }

  return {
    Provider,
    Consumer
  }
}

برای دسترسی به داده‌ها از داخل تابع خود، می‌توانیم به سادگی تابع Consumer خود را فراخوانی کنیم، اما فقط برای اینکه رابط ما دقیقاً مانند React کار کند، اجازه دهید useContext نیز به داده‌ها دسترسی داشته باشد.

function useContext(ctxRef) {
  return ctxRef.Consumer();
}

تماس با ما تماس بگیرید

حالا قسمت سرگرم کننده شروع می شود. همانطور که گفته شد، این روش بر ماهیت همزمان جاوا اسکریپت متکی است. این به این معنی است که از زمانی که ما callback خود را اجرا می کنیم، ما دانستنمطمئناً هیچ کد دیگری اجرا نخواهد شد – به این معنی که ما اجرا نمی کنیم واقعا باید از زمینه خود در برابر تغییر در حین اجرای خود محافظت کنیم، اما در عوض، فقط باید بلافاصله پس از اتمام اجرای تماس، آن را پاکسازی کنیم.

function createContext() {
  let contextValue = undefined;

  function Provider(value, callback) {
    contextValue = value;
    callback();
    contextValue = undefined;
  }

  function Consumer() {
    return contextValue;
  }

  return {
    Provider,
    Consumer
  }
}

این تمام چیزی است که در آن وجود دارد. واقعا اگر تابع ما با تابع Provider فراخوانی شود، در تمام طول اجرای آن به مقدار Provider دسترسی خواهد داشت.

اگر یک زمینه تودرتو داشته باشیم چه می شود؟

تودرتوی زمینه ها چیزی است که می تواند اتفاق بیفتد. به عنوان مثال، زمانی که من یک describe در یک describe. در چنین حالتی، زمینه ما هنگام خروج از درونی ترین متن شکسته می شود، زیرا پس از هر بار اجرای فراخوانی، مقدار متن را به حالت تعریف نشده بازنشانی می کنیم، و از آنجایی که هر دو لایه زمینه بسته شدن یکسانی دارند – داخلی ترین ارائه دهنده بازنشانی می شود. مقدار لایه های بالای آن

function Provider(value, callback) {
  contextValue = value;
  callback();
  contextValue = undefined;
}

خوشبختانه رسیدگی به آن بسیار آسان است. هنگام وارد کردن یک متن، تنها کاری که باید انجام دهیم این است که مقدار فعلی آن را در یک متغیر ذخیره کرده و هنگام خروج از متن، آن را به آن برگردانیم:

function Provider(value, callback) {
  let currentValue = contextValue;
  contextValue = value;
  callback();
  contextValue = currentValue;
}

اکنون، هر زمان که از متن خارج شویم، به مقدار قبلی برمی‌گردد، و اگر لایه‌های بیشتری از زمینه در بالا وجود نداشته باشد، به مقدار اولیه – که تعریف نشده است – برمی‌گردیم.

یکی دیگر از ویژگی‌هایی که امروز اجرا نکردیم، مقدار پیش‌فرض برای متن است. در React، می‌توانید متن را با یک مقدار پیش‌فرض مقداردهی کنید که در صورتی که داخل یک زمینه در حال اجرا نباشیم، توسط Consumer/useContext برگردانده می‌شود.

اگر تا اینجا پیش رفته اید، تمام دانش و ابزارهایی را دارید که می توانید آن را به تنهایی پیاده سازی کنید – من دوست دارم ببینم چه چیزی به ذهن شما می رسد.

آیا این در جایی استفاده می شود؟

آره! من در واقع ساختم بسته زمینه در NPM که دقیقاً همین کار را انجام می‌دهد، با برخی اصلاحات و مجموعه‌ای از ویژگی‌های بیشتر – از جمله پشتیبانی کامل از تایپ‌اسکریپت، ادغام زمینه‌های تودرتو، مقادیر بازگشتی از تابع «ارائه‌دهنده»، مقادیر اولیه زمینه، و حتی میان‌افزار ثبت متن.

می توانید کد منبع کامل بسته را در اینجا بررسی کنید: https://github.com/ealush/vest/blob/latest/packages/context/src/context.ts

و در داخل به طور گسترده استفاده می شود چارچوب اعتبارسنجی جلیقهیک چارچوب اعتبار سنجی فرم که از کتابخانه های تست واحد مانند موکا یا جست الهام گرفته شده است. همانطور که مشاهده می شود، Context به عنوان زمان اجرا اصلی Vest عمل می کند اینجا.

امیدوارم از این مقدمه کوتاه برای انتشار متن در جاوا اسکریپت لذت برده باشید و به شما نشان داده باشد که هیچ جادویی در پشت برخی از مفیدترین APIهای React وجود ندارد.

مطالعه بیشتر در مجله Smashing

سرمقاله Smashing
(nl, il)