منذ ظهور async
/ await
، نشرت Typescript العديد من المقالات التي تمدّ نهج التطوير هذا ( hackernoon ، blog.bitsrc.io ، habr.com ). نستخدمها منذ البداية من جانب العميل (عندما كانت ES6 Generators تدعم أقل من 50٪ من المتصفحات). والآن أريد أن أشارك تجربتي ، لأن التنفيذ الموازي ليس كل ما سيكون من الجيد معرفته على هذا الطريق.
أنا لا أحب المادة الأخيرة: قد يكون هناك شيء غير مفهوم. يرجع جزئيا إلى حقيقة أنني لا أستطيع توفير رمز الملكية - فقط لتحديد النهج العام. لذلك:
- لا تتردد في إغلاق علامة التبويب دون قراءة
- إذا كنت تديره ، فاطلب التفاصيل غير الواضحة
- سأقبل بكل سرور النصيحة والنقد من الأكثر إصرارًا واكتشافًا دقيقًا حتى الآن.
قائمة التقنيات الأساسية:
- تتم كتابة المشروع في المقام الأول في Typescript باستخدام عدة مكتبات Javascript. المكتبة الرئيسية هي ExtJS. يعتبر رد فعل React أقل شأناً ، ولكنه مناسب تمامًا لمنتج مؤسسة بواجهة غنية: العديد من المكونات الجاهزة ، طاولات مصممة جيدًا خارج الصندوق ، نظام بيئي غني من المنتجات ذات الصلة لتبسيط التطوير.
- خادم متعدد مؤشرات الترابط غير متزامن.
- يتم استخدام RPC عبر Websocket كوسيلة نقل بين العميل والخادم. يشبه التنفيذ .NET WCF.
- أي كائن هو خدمة.
- يمكن أن ينتقل أي كائن من حيث القيمة والمرجع.
- تشبه واجهة طلب البيانات GraphQL من Facebook ، فقط على Typescript.
- اتصال ثنائي الاتجاه: يمكن بدء تهيئة تحديث البيانات من العميل ومن الخادم.
- تتم كتابة التعليمات البرمجية غير المتزامنة بالتتابع من خلال استخدام وظائف
async
/ await
الخاصة بـ Typesrcipt. - يتم إنشاء واجهة برمجة تطبيقات الخادم في Typescript: إذا تم تغييرها ، فستظهر البنية فورًا في حالة حدوث خطأ.
ما هو الإخراج
سوف أخبرك عن كيفية تعاملنا مع هذا وما فعلناه من أجل التنفيذ الآمن وغير التنافسي للرمز غير المتزامن: مصممو أنواع الحروف لدينا الذين يقومون بتنفيذ وظائف قوائم الانتظار. من الأساسيات إلى حل حالة السباق والصعوبات الأخرى التي تنشأ أثناء عملية التطوير.
كيف يتم تنظيم البيانات الواردة من الخادم
يقوم الخادم بإرجاع كائن أصل يحتوي على بيانات (كائنات أخرى ، مجموعات كائنات ، صفوف إلخ) في خصائصه في شكل رسم بياني. هذا يرجع ، في جملة أمور ، إلى التطبيق نفسه:
- فهو يجعل تحليل البيانات / ML رسم بياني موجه للعقد المعالج.
- كل عقدة في المقابل يمكن أن تحتوي على الرسم البياني الخاص بها
- تحتوي الرسوم البيانية على تبعيات: يمكن "توارث" العقد ، ويتم إنشاء عقد جديدة من خلال "الفئة" الخاصة بها.
لكن هيكل الاستعلام في شكل رسم بياني يمكن تطبيقه في أي تطبيق تقريبًا ، كما يشير GraphQL ، حسب علمي ، إلى ذلك في مواصفاته.
مثال بنية البيانات:
كيف يتلقى العميل البيانات
الأمر بسيط: عندما تطلب خاصية لكائن من نوع غير قياسي ، تقوم RPC بإرجاع Promise
:
let Nodes = Parent.Nodes;
تزامن دون "رد الاتصال الجحيم".
لتنظيم رمز غير متزامن "متسلسل" ، يتم استخدام وظيفة Typescript async
/ await
:
async function ShowNodes(parent: IParent): Promise<void> {
ليس من المنطقي التركيز عليها بالتفصيل ، فهناك بالفعل مواد مفصلة كافية على المحور. ظهرت في Typescript مرة أخرى في عام 2016. لقد استخدمنا هذه الطريقة منذ ظهورها في فرع الميزات في مستودع Typescript ، ولهذا السبب حصلنا بالفعل على المطبات ونعمل الآن بكل سرور. لبعض الوقت الآن ، وفي الإنتاج.
باختصار ، جوهر أولئك الذين ليسوا على دراية بالموضوع:
بمجرد إضافة الكلمة الأساسية غير async
إلى الوظيفة ، ستُرجع Promise<_>
تلقائيًا Promise<_>
. ميزات هذه الوظائف:
- التعبيرات بداخل وظائف
async
مع await
(والتي ترجع Promise
) ستوقف تنفيذ الوظيفة وتستمر بعد حل Promise
المتوقع. - في حالة حدوث استثناء في وظيفة
async
، سيتم رفض Promise
إرجاعه مع هذا الاستثناء. - عند التحويل البرمجي في كود Javascript ، سيكون هناك مولدات لمعيار ES6 (
function*
بدلاً من async function
متزامنة yield
بدلاً من await
) أو رمز مخيف مع switch
ES5 (جهاز الحالة). await
هي الكلمة التي تنتظر نتيجة الوعد. في وقت الاجتماع ، أثناء تنفيذ التعليمات البرمجية ، ShowNodes
وظيفة ShowNodes
، وأثناء انتظار البيانات ، قد تنفذ Javascript بعض التعليمات البرمجية الأخرى.
في التعليمة البرمجية أعلاه ، تحتوي المجموعة على طريقة forEachParallel
التي تستدعي رد اتصال غير متزامن لكل عقدة على التوازي. في الوقت نفسه ، await
حتى await
Nodes.forEachParallel
جميع عمليات الاسترجاعات. داخل التطبيق - Promise.all
:
export async function forEachParallel<T>(items: IItemArray<T>, callbackfn: (value: T, index: int, items: IItemArray<T>) => Promise<void | any>, thisArg?: any): Promise<void> { let xCount = items ? await items.Count : 0; if (!xCount) return; let xActions = new Array<Promise<void | any>>(xCount); for (let i = 0; i < xCount; i++) { let xItem = items.Item(i); xActions[i] = ExecuteCallback(xItem, callbackfn, i, items, thisArg); } await Promise.all(xActions); } /** item callbackfn */ async function ExecuteCallback<T>(item: Promise<T> | T, callbackfn: (value: T, index: int, items: IItemArray<T>) => Promise<void | any>, index: int, items: IItemArray<T>, thisArg?: any): Promise<void> { let xItem = await item; await callbackfn.call(thisArg, xItem, index, items); }
هذا هو السكر النحوي: يجب استخدام هذه الطرق ليس فقط لمجموعاتها ، ولكن أيضًا لمصفوفات Javascript القياسية.
تبدو وظيفة ShowNodes
غير مثالية للغاية: عندما نطلب كيانًا آخر ، ShowNodes
كل مرة. الراحة هي أنه يمكن كتابة مثل هذا الرمز بسرعة ، لذلك هذا النهج جيد للنماذج الأولية السريعة. في الإصدار النهائي ، تحتاج إلى استخدام لغة الاستعلام لتقليل عدد المكالمات إلى الخادم.
لغة الاستعلام
هناك العديد من الوظائف التي يتم استخدامها "لبناء" طلب البيانات من الخادم. إنهم "يخبرون" الخادم أي عقد من الرسم البياني للبيانات لإرجاعه في الاستجابة:
selectAsync<T extends IItem>(item: T, properties: () => any[]): Promise<T>; selectAsyncAll<T extends ICollection>(items: T[], properties: () => any[]): Promise<T[]>; select<T>(item: T, properties: () => any[]): T; selectAll<T>(items: T[], properties: () => any[]): T[];
الآن ، دعونا نلقي نظرة على تطبيق هذه الوظائف لطلب البيانات المضمنة اللازمة بمكالمة واحدة إلى الخادم:
async function ShowNodes(parentPoint: IParent): Promise<void> {
مثال على استعلام أكثر تعقيدًا بقليل من المعلومات المضمنة بعمق:
تساعد لغة الاستعلام في تجنب الطلبات غير الضرورية إلى الخادم. لكن الرمز ليس مثاليًا على الإطلاق ، وسيحتوي بالتأكيد على العديد من الطلبات التنافسية ونتيجة لذلك ، شرط السباق.
حالة السباق والحلول
نظرًا للاشتراك في أحداث الخادم وكتابة التعليمات البرمجية مع عدد كبير من الطلبات غير المتزامنة ، قد تحدث حالة سباق عند FuncOne
وظيفة FuncOne
async
، في انتظار Promise
. في هذا الوقت ، قد يأتي حدث خادم (أو من إجراء المستخدم التالي) ، وبعد تنفيذه بطريقة تنافسية ، قم بتغيير النموذج على العميل. ثم FuncOne
بعد حل الوعد ، يمكن أن يتحول ، على سبيل المثال ، إلى الموارد المحذوفة بالفعل.
تخيل مثل هذا الموقف المبسط: IParent
كائن IParent
على IParent
خادم IParent
.
Parent.OnSynchronize.AddListener(async function(): Promise<void> {
يتم استدعاؤه عند تحديث قائمة العقد INodes
على الخادم. بعد ذلك ، في السيناريو التالي ، تكون حالة السباق ممكنة:
- نتسبب في الإزالة غير المتزامنة للعقدة من العميل ، في انتظار الانتهاء لحذف كائن العميل
async function OnClickRemoveNode(node: INode): Promise<void> { let removedOnServer: boolean = await Parent.RemoveNode(node);
- من خلال
Parent.OnSynchronize
، يحدث تحديث حدث قائمة العقدة. Parent.OnSynchronize
معالجة Parent.OnSynchronize
وحذف كائن العميل.- يستمر تنفيذ
async OnClickRemoveNode()
بعد await
الأول ويتم إجراء محاولة لحذف كائن عميل محذوف بالفعل.
يمكنك إجراء التحقق من وجود كائن عميل في OnClickRemoveNode
. هذا مثال مبسط وفيه إجراء فحص مماثل أمر طبيعي. ولكن ماذا لو كانت سلسلة الاتصال أكثر تعقيدًا؟ لذلك ، يعد استخدام أسلوب مشابه بعد كل await
ممارسة سيئة:
- رمز حتى المتضخمة معقدة لدعم وتوسيع نطاق.
- لا تعمل التعليمات البرمجية بالشكل
OnClickRemoveNode
الحذف في OnClickRemoveNode
، ويحدث الحذف الفعلي لكائن العميل في مكان آخر. يجب ألا يكون هناك انتهاك للتسلسل المحدد من قبل المطور ، وإلا فلن يكون هناك أخطاء الانحدار. - هذا لا يمكن الاعتماد عليه بدرجة كافية: إذا نسيت إجراء فحص في مكان ما ، فسيحدث خطأ. يكمن الخطر أولاً في أن الفحص المنسية قد لا يؤدي إلى حدوث خطأ محليًا وفي بيئة اختبار ، وسيحدث ذلك للمستخدمين الذين لديهم تأخير أطول في الشبكة.
- وإذا كان يمكن تدمير وحدة تحكم التي تنتمي إلى هؤلاء المعالجات؟ بعد كل
await
للتحقق من تدميرها؟
يطرح سؤال آخر: ماذا لو كان هناك الكثير من الأساليب التنافسية المماثلة؟ تخيل أن هناك المزيد:
- إضافة عقدة
- تحديث العقدة
- إضافة / إزالة الروابط
- طريقة تحويل العقدة المتعددة
- السلوك المعقد للتطبيق: نقوم بتغيير حالة عقدة واحدة ويبدأ الخادم في تحديث العقد التي تعتمد عليها.
مطلوب تنفيذ معماري ، والذي يلغي من حيث المبدأ إمكانية حدوث أخطاء بسبب حالة السباق ، وإجراءات المستخدم المتوازية ، إلخ. يتمثل الحل الصحيح للقضاء على التغيير المتزامن للنموذج من العميل أو الخادم في تنفيذ قسم مهم مع قائمة انتظار مكالمة. ستكون أدوات تزيين الحروف مفيدة هنا لوضع علامات توضيحية على وظائف التحكم غير المتزامن التنافسية هذه.
نوجز المتطلبات والميزات الرئيسية لمثل هذه الديكورات:
- في الداخل ، يجب تنفيذ قائمة انتظار المكالمات إلى وظائف غير متزامنة. اعتمادًا على نوع الديكور ، قد يتم استدعاء أو رفض مكالمة الوظائف إذا كانت هناك مكالمات أخرى فيها.
- ستتطلب الدالات المحددة سياق تنفيذ لربط قائمة الانتظار. يجب عليك إما إنشاء قائمة انتظار بشكل صريح ، أو القيام بذلك تلقائيًا بناءً على طريقة العرض التي تنتمي إليها وحدة التحكم.
- المعلومات مطلوبة عن إتلاف مثيل وحدة التحكم (على سبيل المثال ، الخاصية
IsDestroyed
). لمنع الديكور من إجراء مكالمات في قائمة الانتظار بعد إتلاف وحدة التحكم. - بالنسبة لوحدة التحكم في العرض ، نضيف وظيفة تطبيق قناع شفاف لاستبعاد الإجراءات في وقت تنفيذ قائمة الانتظار وتشير بصريًا إلى أن العملية قيد التقدم.
- يجب أن تنتهي جميع
Promise.done()
باستدعاء Promise.done()
. في هذه الطريقة ، تحتاج إلى تطبيق معالج الاستثناءات غير المعالجة. شيء مفيد للغاية:
الآن نقدم قائمة تقريبية لمثل هذه الديكورات مع وصف ثم نوضح كيف يمكن تطبيقها.
@Lock @LockQueue @LockBetween @LockDeferred(300)
الأوصاف مجردة تمامًا ، ولكن بمجرد رؤية مثال للاستخدام مع التفسيرات ، يصبح كل شيء أكثر وضوحًا:
class GraphController implements ILockTarget { private View: IView; public GetControllerView(): IView { return this.View; } @Lock private async OnClickRemoveNode(): Promise<void> { ... } @Lock private async OnClickRemoveLink(): Promise<void> { ... } @Lock private async OnClickAddNewNode(): Promise<void> { ... } @LockQueue private async OnServerUpdateNode(): Promise<void> { ... } @LockQueue private async OnServerAddLink(): Promise<void> { ... } @LockQueue private async OnServerAddNode(): Promise<void> { ... } @LockQueue private async OnServerRemoveNode(): Promise<void> { ... } @LockBetween private async OnServerSynchronize(): Promise<void> { ... } @LockQueue private async OnServerUpdateNodeStatus(): Promise<void> { ... } @LockDeferred(300) private async OnSearchFieldChange(): Promise<void> { ... } }
الآن سنقوم بتحليل اثنين من السيناريوهات النموذجية للأخطاء المحتملة والقضاء عليها من قبل الديكور:
- يبدأ المستخدم إجراءً:
OnClickRemoveNode
و OnClickRemoveLink
. من أجل المعالجة المناسبة ، من الضروري عدم وجود معالجات تنفيذ أخرى في قائمة الانتظار (إما عميل أو خادم). خلاف ذلك ، على سبيل المثال ، مثل هذا الخطأ ممكن:
- لا يزال يتم تحديث النموذج على العميل إلى حالة الخادم الحالية
- نبدأ في حذف الكائن قبل اكتمال التحديث (هناك معالج
OnServerSynchronize
قيد OnServerSynchronize
في قائمة الانتظار). ولكن هذا الكائن لم يعد موجودًا بالفعل - لم تكتمل المزامنة الكاملة بعد ولا يزال معروضًا على العميل.
لذلك ، يجب أن ترفض أداة تزيين Lock
جميع الإجراءات التي بدأها المستخدم إذا كان هناك معالجات أخرى في قائمة الانتظار مع نفس سياق قائمة الانتظار. بالنظر إلى أن الخادم غير متزامن ، فهذا مهم بشكل خاص. نعم ، يرسل Websocket الطلبات بشكل متسلسل ، ولكن إذا كسر العميل التسلسل ، فسوف نحصل على خطأ على الخادم.
- نبدأ في إضافة عقدة:
OnClickAddNewNode
. أحداث OnServerAddNode
، تأتي OnServerAddNode
من الخادم.
- تولى
OnClickAddNewNode
قائمة الانتظار (إذا كان هناك شيء ما ، فإن أداة Lock
هذه الطريقة ترفض الدعوة) OnServerAddNode
، OnServerAddNode
، OnServerAddNode
تنفيذه بالتسلسل بعد OnClickAddNewNode
، وليس التنافس معه.
- تحتوي قائمة الانتظار على
OnServerUpdateNode
. افترض أنه أثناء تنفيذ الأول ، يقوم المستخدم بإغلاق GraphController
. ثم يجب عدم إجراء المكالمة الثانية إلى OnServerUpdateNode
تلقائيًا حتى لا تتخذ إجراء على وحدة التحكم المدمرة ، والتي يضمن أن تؤدي إلى حدوث خطأ. لهذا ، ILockTarget
واجهة ILockTarget
على IsDestroyed
- يتحقق IsDestroyed
من العلم دون تنفيذ المعالج التالي من قائمة الانتظار.
الربح: لا حاجة للكتابة if (!this.IsDestroyed())
بعد كل await
. - تبدأ التغييرات في العقد المتعددة. أحداث
OnServerUpdateNode
، تأتي OnServerUpdateNode
من الخادم. سيؤدي تنفيذها التنافسي إلى أخطاء غير قابلة للإنتاج. لكن منذ ذلك الحين LockQueue
تم تمييزها بواسطة LockQueue
LockBetween
و LockBetween
، فسيتم تنفيذها بالتتابع. - تخيل أن العقد قد تحتوي على رسومات بيانية للعقدة متداخلة بداخلها.
GraphController #1
, — GraphController #2
. , GraphController
- , ( — ), .. . :
OnSearchFieldChange
, . - . @LockDeferred(300)
300 : , , 300 . , . :
- , 500 , . —
OnSearchFieldChange
, . OnSearchFieldChange
— , .
- Deadlock:
Handler1
, , await
Handler2
, LockQueue
, Handler2
— Handler1
. - , View . : , — .
, , . :
- -
<Class>
. <Method>
=> <Time>
( ). - .
- .
, , , . ? ? :
class GraphController implements ILockTarget { private View: IView; public GetControllerView(): IView { return this.View; } @Lock private async RunBigDataCalculations(): Promise<void> { await Start(); await UpdateSmth(); await End(); await CleanUp(); } @LockQueue private async OnChangeNodeState(node: INode): Promise<void> { await GetNodeData(node); await UpdateNode(node); } }
:
RunBigDataCalculations
.await Start();
- / ( )
await Start();
, await UpdateSmth();
.
:
RunBigDataCalculations
.OnChangeNodeState
, (.. ).await GetNodeData(node);
- / ( )
await GetNodeData(node);
, await UpdateNode(node);
.
- . :
export interface IQueuedDisposableLockTarget extends ILockTarget { IsDisposing(): boolean; SetDisposing(): void; }
function QueuedDispose(controller: IQueuedDisposableLockTarget): void {
, . QueuedDispose
:
- . .
QueuedDispose
controller
. — ExtJS .
, , .. . , ? , .
, :
vk.com
Telegram