پیاده سازی reactivity فریمورک ویو در جاواسکریپت

توی این آموزش قصد داریم تا با هم از صفر یک سیستم دیتا reactivity رو در جاواسکریپت پیاده سازی کنیم.چرا reactivity?براتون یه مثال میزنم، کد زیر رو در نظر بگیریدconst price = 100 const fee = 10 const total = () => { return price + fee } render(total()) // 110 // new value price = 300 render(total()) // 310همونطوری که میبینید اگر ما بخوایم بدون استفاده از reactivity یک مقدار رو آپدیت کنیم، باید این کار رو به صورت دستی انجام بدیم و در هر مرحله مثل کد بالا، متد های خودمون رو صدا بزنیماما reactivity به ما کمک میکنه این پروسه رو اتوماتیک کنیم و اجازه بدیم روند برنامه صدا زدن متد ها و آپدیت داده های ما رو به عهده بگیره.خروجی این آموزش به این صورت خواهد بودخروجی نهاییری اکتیویتی در ویو جی استو این آموزش سعی میکنیم تا سیستم شبیه به فریم ورک vue رو پیاده سازی کنیم ( نسخه 2 )ما میخوایم ورودی شبیه به کد زیر داشته باشیم ( درست مثل ویو )const App = Observable({ data() { return { price: 300, fee: 10, } }, computed: { totalSpend() { return this.price + this.fee }, }, methods: { sayHello() { console.log(&quothello world&quot) }, }, })خب برای شروع متد Observable رو تعریف میکنیمconst Observable = (input) => { const deps = {} let effect = null const _app = { ...input.data(), } }از _deps برای نگه داری وابستگی های متغیر ها به متد های computed استفاده میشهاز effect برای نگهداری computed فعال و در حال اجرا استفاده میشهاز _app به عنوان ورودی برنامه که به صورت دیفالت متغیر های داخل آبجکت Data رو داخلش میریزیم، استفاده میشهبرای ری اکتیو کردن داده ها، در جاوااسکریپت API وجود داره به اسمProxy. این API به شما اجازه میده درخواست های ارسالی به آبجکت رو شخصی سازی کنید. ما از این API برای خوندن و نوشتن داده توی متغیر هامون استفاده میکنیمما به 2 تا Proxy نیاز خواهیم داشت. یکی برای گوش کردن به تغییرات دیتا در ابجکت _app و دیگری برای گوش کردن به تغییرات computed هاابتدا با دیتا شروع میکنیم:const Observable = (input) => { const deps = {} let effect = null const _app = { ...input.data(), } // base proxy const _appProxy = new Proxy(_app, { get(target, key) { return target[key] }, set(target, key, value) { target[key] = value return true }, }) return _appProxy }در مرحله بعد Method هامون رو به آبجکت _app اضافه میکنیم// init methods _app.methods = {} Object.keys(input.methods).forEach((key) => { _app.methods[key] = input.methods[key].bind(_app) })تو اینجا از input.methods[key].bind(_app) استفاده شده. برای زمانی هست که وقتی از this استفاده کردیم رفرنس رو به ابجکت _app از دست ندیمتو مرحله بعد computed هامون رو اضافه میکنیم// init computed -- convert to getters Object.keys(input.computed).forEach((key) => { Object.defineProperty(_appProxy, key, { get() { const res = effect.call(_appProxy) return res }, }) })توی اینجا به جای Proxy از DefineProperty علت خاصی نداره فقط میخواستم نشون بدم از هر دو روش میشه این کار رو انجام داد. این کد به ما کمک میکنه، زمانی که کاربر یکی از متد های computed رو صدا زد، ما به اون فاکنشن دسترسی داشته باشیم اضافه کردن reactivity به computed هاما نیاز به مکانیزمی داریم تا بتونیم تشخیص بدیم که computed های ما چه متغیر هایی رو اجرا میکنن و بعد از تشخیص، این computed ها رو به عنوان dependency به متغیر هامون اضافه میکنیم تا در صورت تغییر مقدار هر کدوم از متغیر ها، computed هامون رو دوباره اجرا بکنیمقطعه کد زیر رو نظر بگیریدconst App = Observable({ data() { return { price: 300, } }, computed: { getPrice() { return this.price }, }, }) App.getPrice // 300اگر کد بالا رو اجرا بکنیم، Getter ای که در داخل فاکنشن Obseravable تعریف کردیم اجرا میشه. ما میدونیم که توی جاوااسکریپت در هر لحظه فقط یک دستور میتونه اجرا بشه پس: وقتی که getPrice اجرا میشه، ما باید اون رو به عنوان effect درحال اجرا در نظر بگیریمپس کد بخش computed ما به این صورت تغییر میکنه:// init computed -- convert computeds to getters Object.keys(input.computed).forEach((key) => { Object.defineProperty(_appProxy, key, { get() { effect = input.computed[key] const res = effect.call(_appProxy) effect = null return res }, }) })قبل از اجرا شدن متد computed ما، اون رو در درون متغیر effect قرار میدیم و بعد از اتمام اجرا، اون رو به null تغییر میدیمبه این نکته توجه کنید وقتی که computed ما اجرا میشه effect.call(_appProxy) چون در داخل این متد از this.price استفاده کردیم، این باعث میشه Proxy ما برای دیتا اجرا بشهاز اونجایی که ما effect و متد فعال فعلی رو ذخیره داریم، میتونیم این متد رو به عنوان dependency به متغیر ها اضافه کنیمپس بخش get ما به این صورت تغییر میکنه// base proxy const _appProxy = new Proxy(_app, { get(target, key) { if (!deps[key]) { deps[key] = new Set() } effect && deps[key].add(effect) return target[key] }, // -------------- })ابتدا چک میکنم تا ببینیم متغیر فعلی دارای در داخل ابجکت deps وجود داره یا نه، اگر وجود نداشت اون رو ایجاد میکنیم. علت استفاده از new Set() به این خاطر هست که ما نمیخوایم یک فاکنشن چند بار اجرا بشهبعد از ذخیره deps ها ما همچین ساختاری رو خواهیم داشتconsole.log(deps) /* { price: getPrice() { return this.price; } } */حالا اگر هرکدوم از این دیتا های ما تغییر بکنند، ما dependency های اون را دونه به دونه صدا میزنیمبرای اینکه متوجه بشیم آیا تغییر کردنند یا نه از set در proxy استفاده میکنیم کد ما به این صورت تغییر میکنه:// base proxy const _appProxy = new Proxy(_app, { // ---------------- set(target, key, value) { target[key] = value const _deps = deps[key] if (_deps) { _deps.forEach((effect) => { return effect.call(_appProxy) }) } return true }, })همونطور که میبینیدconst _deps = deps[key] if (_deps) { _deps.forEach((effect) => { return effect.call(_appProxy) }) }همه ی deps ها رو اجرا میکنیم تا مطلع بشن که دیتای ما تغییر کردهاگر تا اینجا دنبال کرده باشید کد ما به این صورت خواهد بودconst Observable = (input) => { const deps = {} let effect = null const _app = { ...input.data(), } // base proxy const _appProxy = new Proxy(_app, { get(target, key) { if (!deps[key]) { deps[key] = new Set() } effect && deps[key].add(effect) return target[key] }, set(target, key, value) { target[key] = value const _deps = deps[key] if (_deps) { _deps.forEach((effect) => { return effect.call(_appProxy) }) } return true }, }) // computed proxy // init methods _app.methods = {} Object.keys(input.methods).forEach((key) => { _app.methods[key] = input.methods[key].bind(_app) }) // init computed -- convert computeds to getters Object.keys(input.computed).forEach((key) => { Object.defineProperty(_appProxy, key, { get() { effect = input.computed[key] const res = effect.call(_appProxy) effect = null return res }, }) }) return _appProxy }خب کار ما اینجا تموم میشه. برای اینکه ببینیم کدی که زدیم کار میکنه یا نه یه پروژه کوچیک با هم انجام میدیمیک ابجکت از کلاس Observable مون میسازیم با ورودی های زیر:const App = Observable({ data() { return { price: 300, fee: 10, } }, computed: { totalSpend() { return this.price + this.fee }, }, methods: { price_up() { this.price++ }, price_down() { this.price-- }, fee_up() { this.fee++ }, fee_down() { this.fee-- }, }, })یک فاکنشن تعریف میکنیم به اسم render وظیفه این فاکنشن اضافه کردن کد html یک جدول به صفحه ماست (مثل گیف پروژه نهایی در بتدای پروژه)const render = () => { const appElem = document.getElementById(&quotapp&quot) appElem = ` <table class=&quottable&quot> <tbody> <tr> <td class=&quotbold&quot><p>product price</p></td> <td class=&quotprice bold&quot >${App.price}$</td> <td> <div class=&quotbtn-group-vertical&quot role=&quotgroup&quot aria-label=&quotFirst group&quot> <button type=&quotbutton&quot class=&quotbtn btn-secondary&quot @click=&quotprice_up&quot>+</button> <button type=&quotbutton&quot class=&quotbtn btn-secondary&quot @click=&quotprice_down&quot>-</button> </div> </td> </tr> <tr> <td class=&quotbold&quot>Fee</td> <td class=&quotprice bold&quot>${App.fee}%</td> <td> <div class=&quotbtn-group-vertical&quot role=&quotgroup&quot aria-label=&quotFirst group&quot> <button type=&quotbutton&quot class=&quotbtn btn-secondary&quot @click=&quotfee_up&quot>+</button> <button type=&quotbutton&quot class=&quotbtn btn-secondary&quot @click=&quotfee_down&quot>-</button> </div> </td> </tr> <tr> <td class=&quotbold&quot>Total</td> <td class=&quotprice bold&quot>${App.totalSpend}$</td> <td></td> </tr> </tbody> </table> ` } render()همونطور که میبینید در داخل جدول از String Template ها استفاده شده و داده هامون رو قراره نشون بده<td class=&quotprice bold&quot>${App.fee}%</td>همچنین مثل فریمورک vue از custom attribute ها استفاده کردیم تا بتونیم به click event گوش کنیم<button @click=&quotfee_up&quot>+</button> <button @click=&quotfee_down&quot>-</button>خب حالا به اونت کلیک روی صفحه گوش میدیم و برسی میکنیم که کاربر کجا کلیک کرده اگر روی المنت هایی که click attribute دارن کلیک کرده باشه، فاکنشن ها رو اجرا میکنیمdocument.addEventListener(&quotclick&quot, (e) => { const customClickAttr = e.target.attributes // get click attribute from list of attributes const clickAttr = customClickAttr.getNamedItem(&quot@click&quot) if (clickAttr) { const methodName = clickAttr.value App.methods[methodName]() render() } })بعد از اینکه دیتامون رو اپدیت کردیم نیاز هست تا صفحه رو هم آپدیت کنیم. به خاطر همین بعد از اجرا متد هامون، متد render رو دوباره صدا میزنیمخب پروژه ما اینجا تموم میشه، برای تمرین اگر دوست داشتید سعی کنید مثل Vue قابلیت Watcher و گوش کردن به همه تغییرات متغیر ها رو پیاده کنید.اگر این آموزش براتون جالب بود میتونید کد های این پروژه روی گیت هاب مشاهده کنید[سورس کد پروژه]همچنین میتونید این مطلب رو در بلاگ شخصی من بخونید https://mediv.vercel.app/blog/data-reactivity-in-javascript HAPPY CODING