ساخت یک autobind decorator

Building an Autobind Decorator

14 مرداد 1399
ساخت یک autobind decorator

حالا که تا حد زیادی با decorator ها آشنا شده اید، بهتر است در مورد نکته ای صحبت کنیم. ما در جلسه قبل با استفاده از تعیین return type برای یک Decorator توانستیم constructor کلاس خود را به طور کامل جایگزین کنیم اما تعیین return type در تمام decorator ها قابل انجام نیست. در واقع تنها در دو نوع از decorator ها می توانیم چیزی را برگردانیم: method decorator ها و accessor decorator ها (مانند setter در جلسه قبل). Decorator های مربوط به پارامترها و خصوصیات نیز از نظر فنی می توانند چیزی را برگردانند اما تایپ اسکریپت اصلا به مقدار برگشتی توجه نمی کند بنابراین از این لحاظ ساپورت نمی شوند.

حالا می خواهم بر اساس همین نکته، کار جالبی انجام بدهیم. اول از همه به فایل index.html رفته و یک button جدید در آن ایجاد کنید:

<body>
  <div id="app"></div>
  <button>Click me</button>
</body>

به فایل app.ts برگردید. من می خواهم زمانی که روی این دکمه کلیک شد، متدی از یک شیء اجرا شود بنابراین باید یک کلاس جدید تعریف کنیم:

class Printer {
  message = 'This works!';

  showMessage() {
    console.log(this.message);
  }
}

این کلاس یک خصوصیت ساده به نام message و یک متد نیز به نام showMessage دارد. در مرحله بعد باید دکمه را از DOM بگیریم:

const p = new Printer();

const button = document.querySelector('button')!;
button.addEventListener('click', p.showMessage);

همانطور که مشاهده می کنید من ابتدا یک نمونه از کلاس خودمان را ساخته ام و بعد از گرفتن دکمه از DOM متد showMessage آن را به صورت event-listener صدا زده ام. اگر ما چنین کاری را انجام بدهیم، بعد از ذخیره کدها و کلیک روی دکمه click me در مرورگر، مقدار undefined برایمان چاپ می شود. چرا؟ این همان مشکل معروف کلیدواژه this است. زمانی که p.showMessage را در یک eventlistener قرار می دهیم، کلیدواژه درون متد showMessage به شیء p اشاره نمی کند بلکه به هدف event (خود دکمه) اشاره خواهد کرد چرا که addEventListener همیشه متد را به هدف رویداد bind (متصل) می کند.

یکی از راه های حل این مشکل استفاده از تابع bind است:

button.addEventListener('click', p.showMessage.bind(p));

با انجام این کار showMessage را به شیء p متصل (bind) می کنیم. تا اینجای کار همه چیز طبق انتظار و بر اساس درس های جاوا اسکریپت بود اما از اینجا به بعد می خواهیم یک decorator خاص بسازیم که با اضافه شدن به متد showMessage آن را به شیء مورد نظرش bind کند تا نیازی نباشد ما کاری انجام بدهیم. برای تعریف این decorator مثل همیشه یک تابع با نام دلخواه خودتان را انتخاب کنید.

همانطور که قبلا یاد گرفتیم، اولین آرگومانی که در این تابع دریافت می کنید Target است که می شود همان prototype شیء ما (یا اگر متد استاتیک باشد، می شود همان constructor). آرگومان دوم نام متد است و آرگومان سوم نیز همان descriptor است:

function Autobind(target: any, methodName: string, descriptor: PropertyDescriptor) {

}

حالا باید درون این decorator کاری کنیم که کلیدواژه this همیشه به شیء ای bind شود که به آن تعلق دارد. چطور؟ ما می توانیم این کار را با کمک descriptor خود انجام دهیم. مثلا اگر یادتان باشد یکی از method descriptor های ما از جلسه قبل به شکل زیر بود:

نمونه ای از method decorator های قبلی
نمونه ای از method decorator های قبلی

اگر به قسمت value نگاه کنید، متوجه می شوید که value در واقع خود تابع است و به آن اشاره می کند! همانطور که می دانید متدها خودشان خصوصیت هایی هستند که مقدارشان یک تابع است، همین! بنابراین می توانیم با استفاده از descriptor.value به خود متد دسترسی پیدا کنیم:

function Autobind(target: any, methodName: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

}

حالا می خواهم یک descriptor جدید را بسازم که بعدا آن را return خواهیم کرد:

function Autobind(target: any, methodName: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  const adjDescriptor: PropertyDescriptor = {

  };
}

همانطور که می بینید نام این descriptor ویرایش شده را adjDescriptor (مخفف adjusted descriptor) گذاشته ام که یک شیء بوده و تایپ آن هم از نوع PropertyDescriptor می باشد. حالا می توانیم داخل آن هر چیزی که داخل یک PropertyDescriptor یافت می شود را به سبک خودمان بنویسیم. همانطور که در تصویر بالا مشاهده کردید، ابتدا configurable و enumerable را مشخص می کنم:

function Autobind(target: any, methodName: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  const adjDescriptor: PropertyDescriptor = {
    configurable: true,
    enumerable: false

  };
}

اما حالا یک Getter اضافه می کنم! با استفاده از این getter می توانیم، هنگامی که کاربری بخواهد به این خصوصیت (که در واقع متد است) دسترسی پیدا کند، کارهای اضافه انجام بدهیم. یعنی این متد به محض صدا زده شدن توسط کاربر اجرا نشود بلکه ما تغییرات کوچکی را در آن اعمال کنیم و سپس بگذاریم اجرا شود. به همین دلیل است که value را به کد بالا اضافه نکرده ام، getter معادل همان value به علاوه کدهای بیشتر است.

function Autobind(target: any, methodName: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  const adjDescriptor: PropertyDescriptor = {
    configurable: true,
    enumerable: false
    get() {
      const boundFn = originalMethod.bind(this);
      return boundFn;
    }
  };
  return adjDescriptor;
}

همانطور که در این کد می بینید، این getter، متد اصلی ما را می گیرد و this را به آن bind می کند. سوال اصلی اینجاست که این this به چه چیزی برمی گردد؟ از آنجایی که this درون getter ما است، هر چیزی که مسئول صدا زدن و اجرا شدن getter باشد، صاحب this خواهد بود. در نهایت هم boundFn را برمی گردانیم و پس از آن در خط آخر adjDescriptor را برمی گردانیم تا جایگزین descriptor قبلی شود. این this دیگر توسط addEventListener جایگزین نخواهد شد و می توانیم با خیال راحت از آن استفاده کنیم.

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

function Autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  const adjDescriptor: PropertyDescriptor = {
    configurable: true,
    enumerable: false,
    get() {
      const boundFn = originalMethod.bind(this);
      return boundFn;
    }
  };
  return adjDescriptor;
}

من نمی توانم دو پارامتر با نام های یکسان داشته باشم بنابراین دومین پارامتر را به صورت 2_ نوشته ام. این کار به تایپ اسکریپت می فهماند که ما نیازی به این دو پارامتر نداریم. آخرین کاری که انجام می دهیم، متصل کردن Autobind به متد showMessage است:

class Printer {
  message = 'This works!';

  @Autobind
  showMessage() {
    console.log(this.message);
  }
}

const p = new Printer();

const button = document.querySelector('button')!;
button.addEventListener('click', p.showMessage);

حالا بدون اینکه مشکلی داشته باشیم کد بالا اجرا خواهد شد.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش جامع تایپ اسکریپت توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

ما را دنبال کنید
اینستاگرام روکسو تلگرام روکسو ایمیل و خبرنامه روکسو