حفظ ننسى لي قنبلة


هل سمعت عن memoization ؟ إنه شيء بسيط للغاية ، بالمناسبة - ما عليك سوى تذكير النتيجة التي حصلت عليها من استدعاء الوظيفة الأولى ، واستخدامها بدلاً من الاتصال بها للمرة الثانية - لا تتصل بأشياء حقيقية دون سبب ، ولا تضيع وقتك .


تخطي بعض العمليات المكثفة هي تقنية شائعة للغاية. في كل مرة قد لا تفعل شيئا - لا تفعل ذلك. محاولة استخدام ذاكرة التخزين المؤقت - memcache ، file cache ، local cache - أي ذاكرة التخزين المؤقت! يجب أن يكون بين أنظمة الواجهة الخلفية وجزء أساسي من أي نظام خلفي من الماضي والحاضر.



مذكرة مقابل التخزين المؤقت


المذكرة مثل التخزين المؤقت. مختلفة قليلا فقط. لا ذاكرة التخزين المؤقت ، دعونا نسميها كاش.

قصة قصيرة طويلة ، ولكن الحفظ ليس ذاكرة تخزين مؤقت ، وليس ذاكرة تخزين مؤقت دائمة. قد يكون ذلك على جانب الخادم ، ولكن لا يمكن ، ويجب ألا يكون مخبأ على جانب العميل. يتعلق الأمر بالموارد المتاحة وأنماط الاستخدام وأسباب الاستخدام.


مشكلة - ذاكرة التخزين المؤقت تحتاج إلى "مفتاح ذاكرة التخزين المؤقت"


تقوم ذاكرة التخزين المؤقت بتخزين البيانات وجلبها باستخدام key سلسلة مخبأ. إنها مشكلة بالفعل في إنشاء مفتاح فريد وقابل للاستخدام ، ولكن بعد ذلك يجب عليك إجراء تسلسل وإلغاء تسلسل البيانات لتخزينها مرة أخرى على أساس سلسلة سلسلة ... باختصار - قد لا تكون ذاكرة التخزين المؤقت بنفس السرعة ، كما قد تعتقد. ذاكرة التخزين المؤقت الموزعة خاصة.


لا يحتاج Memoization أي مفتاح ذاكرة التخزين المؤقت


في الوقت نفسه - ليس هناك حاجة إلى مفتاح للحفظ. عادةً * تستخدم الوسائط كما هي ، ولا تحاول إنشاء مفتاح واحد منها ، ولا تستخدم بعض الكائنات المشتركة المتاحة عالمياً لتخزين النتائج ، كما تفعل ذاكرة التخزين المؤقت عادةً.


الفرق بين الحفظ وذاكرة التخزين المؤقت في واجهة API !

عادة * لا يعني دائما. يستخدم Lodash.memoize ، افتراضيًا ، JSON.stringify لتحويل الوسائط التي تم تمريرها إلى ذاكرة تخزين مؤقت سلسلة (هل هناك أي طريقة أخرى؟ لا!). فقط لأنهم سيستخدمون هذا المفتاح للوصول إلى كائن داخلي ، مع الاحتفاظ بقيمة مخبأة. المذكرة السريعة ، "مكتبة المذكرات الأسرع" ، تفعل الشيء نفسه. كلا المكتبات المسماة ليست مكتبات تحفيظ ، ولكن مكتبات تخزين مؤقت.


تجدر الإشارة إلى - JSON.stringify قد يكون 10 مرات أبطأ من وظيفة ، ستعمل مذكرة.

من الواضح أن حل البسطاء للمشكلة هو عدم استخدام مفتاح ذاكرة التخزين المؤقت وعدم الوصول إلى بعض ذاكرة التخزين المؤقت الداخلية باستخدام هذا المفتاح. لذلك - تذكر الحجج الأخيرة التي دعيت بها. مثل memoizerific أو إعادة تحديد القيام به.


من المحتمل أن تكون Memoizerific هي مكتبة التخزين المؤقت العامة الوحيدة التي ترغب في استخدامها.

حجم ذاكرة التخزين المؤقت


الفرق الكبير الثاني بين جميع المكتبات يدور حول حجم ذاكرة التخزين المؤقت وهيكل ذاكرة التخزين المؤقت.


هل سبق لك أن فكرت - لماذا reselect أو memoize-one يحمل واحد فقط ، والنتيجة الأخيرة؟ لا "لا تستخدم مفتاح ذاكرة التخزين المؤقت لتكون قادرة على تخزين أكثر من نتيجة واحدة" ، ولكن لأنه لا توجد أسباب لتخزين أكثر من مجرد نتيجة أخيرة .


... المزيد عن:


  • الموارد المتاحة - سطر ذاكرة التخزين المؤقت واحد ودية للغاية الموارد
  • أنماط الاستخدام - تذكر شيء "في مكان" هو نمط جيد. "في مكان" تحتاج عادة إلى نتيجة واحدة أخيرة.
  • السبب في استخدام -modularity والعزلة وسلامة الذاكرة هي أسباب وجيهة. يعد عدم مشاركة ذاكرة التخزين المؤقت مع بقية التطبيق أكثر أمانًا من حيث تصادم ذاكرة التخزين المؤقت.

نتيجة واحدة؟!


نعم - النتيجة الوحيدة. مع نتيجة واحدة تحفيظ بعض الأشياء الكلاسيكية ، مثل توليد عدد فيبوناتشي المخلوطة ( قد تجد مثالاً في كل مقال عن المذكرة ) لن يكون ممكناً . لكن ، عادة ، أنت تفعل شيئًا آخر - من الذي يحتاج إلى فيبوناتشي على Frontend؟ في الخلفية؟ أمثلة العالم الحقيقي بعيدة كل البعد عن اختبارات تكنولوجيا المعلومات المجردة.


ولكن لا يزال ، هناك مشكلتان كبيرتان حول نوع تحفيظ قيم واحد.


المشكلة 1 - إنها "هشة"


من خلال الإعدادات الافتراضية - يجب أن تتطابق جميع الوسائط ، تكون "===" بالضبط. إذا لم تتطابق حجة واحدة - انتهت اللعبة. حتى لو كان هذا يأتي من فكرة الحفظ - قد لا يكون هذا شيء تريده في الوقت الحاضر. أعني - تريد أن تحفيظ بأكبر قدر ممكن ، وكثير من الأحيان ، قدر الإمكان.


حتى ذاكرة التخزين المؤقت ملكة جمال هو مخبأ مسح الرأس.

هناك اختلاف بسيط بين "في الوقت الحاضر" من "الأمس" - هياكل البيانات غير القابلة للتغيير ، المستخدمة على سبيل المثال في Redux.


 const getSomeDataFromState = memoize(state => compute(state.tasks)); 

تبدو جيدة؟ تبحث الصحيح؟ ومع ذلك ، قد تتغير المسطرة عندما لا تتغير المهام ، وستحتاج فقط إلى مهام للمطابقة.


محددات الهيكل هنا لتوفر اليوم بأقوى محارب - إعادة تحديد - حسب رغبتك. إعادة التحديد ليست مكتبة memoization فقط ، بل تأتي قوتها من شلالات memoization ، أو العدسات (وهي ليست كذلك ، ولكن فكر في المحددات كعدسات بصرية).


 // every time `state` changes, cached value would be rejected const getTasksFromState = createSelector(state => state.tasks); const getSomeDataFromState = createSelector( // `tasks` "without" `state` getTasksFromState, // <---------- // and this operation would be memoized "more often" tasks => compute(state.tasks) ); 

نتيجةً لذلك ، في حالة وجود بيانات غير قابلة للتغيير - يتعين عليك دائمًا "التركيز" أولاً على قطعة البيانات التي تحتاجها حقًا ، ثم - إجراء الحسابات ، أو سيتم رفض ذاكرة التخزين المؤقت الأخرى ، وستتلاشى كل الفكرة الكامنة وراء الحفظ.


هذه في الواقع مشكلة كبيرة ، خاصة بالنسبة للوافدين الجدد ، ولكن ، نظرًا لأن الفكرة وراء هياكل البيانات الثابتة ، لها فائدة كبيرة - إذا لم يتم تغيير شيء ما - فلن تتغير. إذا تم تغيير شيء ما - ربما تم تغييره . هذا يعطينا مقارنة بسرعة فائقة ، ولكن مع بعض السلبيات الخاطئة ، كما في المثال الأول.


الفكرة تدور حول "التركيز" على البيانات التي تعتمد عليها

هناك لحظتان يجب عليّ ذكرهما:


  • lodash.memoize و fast-memoize lodash.memoize بتحويل بياناتك إلى سلسلة لاستخدامها كمفتاح. هذا يعني أنهم 1) ليسوا سريعين 2) غير آمنين 3) يمكن أن ينتجوا إيجابيات خاطئة - بعض البيانات المختلفة يمكن أن يكون لها نفس تمثيل السلسلة . قد يؤدي ذلك إلى تحسين "معدل التخزين المؤقت الساخن" ، ولكن في الواقع شيء سيء للغاية.
  • هناك نهج ES6 Proxy ، حول تتبع جميع القطع المستخدمة من المتغيرات المحددة ، والتحقق من المفاتيح المهمة فقط. على الرغم من أنني شخصياً أرغب في إنشاء عدد لا يحصى من محددات البيانات - قد لا تحب العملية أو تفهمها ، لكن قد ترغب في الحصول على الحفظ الصحيح خارج المربع - ثم استخدام حالة المذكرة.

المشكلة 2 - "خط ذاكرة التخزين المؤقت واحد"


حجم ذاكرة التخزين المؤقت لانهائي هو القاتل. أي ذاكرة التخزين المؤقت غير المنضبط هو قاتل ، طالما أن الذاكرة محدودة للغاية. لذلك - كل أفضل المكتبات هي "خط واحد للذاكرة المؤقتة الطويلة". هذه هي ميزة وقرار التصميم القوي. لقد كتبت للتو مدى صوابه وصدقوني - إنه شيء صحيح حقًا ، لكنه لا يزال يمثل مشكلة. مشكلة كبيرة.


 const tasks = getTasks(state); // let's get some data from state1 (function was defined above) getDataFromTask(tasks[0]); // Yep! equal(getDataFromTask(tasks[0]), getDataFromTask(tasks[0])) // Ok! getDataFromTask(tasks[1]); // a different task? What the heck? // oh! That's another argument? How dare you!? // TLDR -> task[0] in the cache got replaced by task[1] you cannot use getDataFromTask to get data from different tasks 

بمجرد أن يعمل نفس المحدد مع بيانات مصدر مختلفة ، مع وجود أكثر من ذلك - يتم كسر كل شيء. ومن السهل مواجهة المشكلة:


  • طالما كنا نستخدم المحددات للحصول على المهام من الولاية - يمكننا استخدام نفس المحددات للحصول على شيء من المهمة. مكثفة تأتي من API نفسها. لكنه لا يعمل بعد ذلك ، يمكنك تذكير آخر مكالمة فقط ، ولكن يجب عليك العمل مع مصادر بيانات متعددة.
  • تكمن المشكلة نفسها في العديد من مكونات React Components - جميعها متشابهة ، وكلها مختلفة بعض الشيء ، تجلب مهام مختلفة ، وتمسح نتائج بعضها البعض.

هناك 3 حلول ممكنة:


  • في حالة الإعادة - استخدم mapStateToProps factory. سيخلق في الحفظ لكل مثيل.
     const mapStateToProps = () => { const selector = createSelector(...); // ^ you have to define per-instance selectors here // usually that's not possible :) return state => ({ data: selector(data), // a usual mapStateToProps }); } 
  • المتغير الثاني هو نفسه تقريبًا (وأيضًا للإعادة) - إنه يتعلق باستخدام إعادة التحديد . إنها مكتبة معقدة ، والتي يمكن أن تنقذ اليوم من خلال التمييز بين المكونات. يمكن أن يفهم فقط ، أنه تم إجراء المكالمة الجديدة لمكون "آخر" ، وأنه قد يحتفظ بذاكرة التخزين المؤقت للمكون "السابق".


ستساعدك هذه المكتبة على "الاحتفاظ" بذاكرة التخزين المؤقت للمذكرات ، ولكن لا تحذفها. خاصة لأنها تنفذ 5 (خمسة!) استراتيجيات مختلفة لذاكرة التخزين المؤقت لتناسب أي حالة. هذه رائحة سيئة. ماذا لو اخترت الخطأ؟
جميع البيانات التي قمت بحفظها - عليك أن تنساها ، عاجلاً أم آجلاً. النقطة الأساسية هي عدم تذكر آخر استدعاء للوظيفة - النقطة هي أن تنسى ذلك في الوقت المناسب. لم يحن بعد قليل ، وتدمير الحفظ ، وليس بعد فوات الأوان.


حصلت على الفكرة؟ الآن ننسى ذلك! وأين البديل الثالث؟

دعنا نتوقف


إيقاف. استرح. صنع نفسا عميقا. والإجابة على سؤال بسيط واحد - ما هو الهدف؟ ما يتعين علينا القيام به للوصول إلى الهدف؟ ما من شأنه أن ينقذ اليوم؟


نصيحة: أين هذا f *** "ذاكرة التخزين المؤقت" LOCATED!


أين يقع "ذاكرة التخزين المؤقت"؟ نعم - هذا هو السؤال الصحيح. شكرا لسؤالك. والجواب بسيط - إنه يقع في الختام. في بقعة خفية داخل * وظيفة memoized. على سبيل المثال - إليك رمز memoize-one :


 function(fn) { let lastArgs; // the last arguments let lastResult;// the last result <--- THIS IS THE CACHE // the memoized function const memoizedCall = function(...newArgs) { if (isEqual(newArgs, lastArgs)) { return lastResult; } lastResult = resultFn.apply(this, newArgs); lastArgs = newArgs; return lastResult; }; return memoizedCall; } 

ستحصل على memoizedCall ، memoizedCall بالنتيجة الأخيرة في مكان قريب ، داخل إغلاقها المحلي ، ولا يمكن لأي شخص الوصول إليها ، باستثناء memoizedCall. مكان آمن "هذا" هو مكان آمن.


Reselect يفعل نفس الشيء ، والطريقة الوحيدة لإنشاء "شوكة" ، مع ذاكرة تخزين مؤقت أخرى - إنشاء إغلاق مذكرة جديدة.


ولكن السؤال الرئيسي (آخر) - متى (ذاكرة التخزين المؤقت) سيكون "ذهب"؟


TLDR: سيتم "اختفاء" مع دالة ، عندما يتم تناول مثيل الوظيفة بواسطة أداة تجميع البيانات المهملة.

المثال؟ المثال! لذلك - ماذا عن تحفيظ المثال؟ هناك مقال كامل حول هذا الموضوع في وثائق React


باختصار - إذا كنت تستخدم مكونات التفاعل القائمة على الفصل ، فيمكنك القيام بما يلي:


 import memoize from "memoize-one"; class Example extends Component { filter = memoize( // <-- bound to the instance (list, filterText) => list.filter(...); // ^ that is "per instance" memoization // we are creating "own" memoization function // with the "own" lastResult render() { // Calculate the latest filtered list. // If these arguments haven't changed since the last render, // `memoize-one` will reuse the last return value. const filteredList = this.filter(something, somehow); return <ul>{filteredList.map(item => ...}</ul> } } 

لذلك - حيث يتم تخزين "lastResult" ؟ داخل النطاق المحلي للمرشح memoized ، داخل هذه الفئة الطبقة. وعندما يكون "ذهب"؟


هذه المرة سيكون "ذهب" مع مثيل فئة. مرة واحدة حصلت على مكون غير مثبت - ذهب دون أثر. إنه "لكل مثيل" حقيقي ، ويمكنك استخدام this.lastResult للاحتفاظ بنتيجة زمنية ، بنفس تأثير "المذكرة" بالضبط.


ماذا عن React.Hooks


نحن نقترب. السنانير المستردة لديها بعض الأوامر المشبوهة ، والتي ، على الأرجح ، تدور حول الحفظ. مثل - useMemo ، useCallback ، useRef



ولكن السؤال - أين يتم تخزين قيمة memoized هذه المرة؟

باختصار - يقوم بتخزينها في "خطافات" ، داخل جزء خاص من عنصر VDOM يُعرف باسم الألياف المرتبطة بعنصر حالي. داخل بنية بيانات موازية.


ليس هذا كثيرًا - تعمل الخطافات على تغيير طريقة عمل البرنامج ، حيث تقوم بنقل وظيفتك داخل أخرى ، مع بعض المتغيرات في بقعة مخفية داخل إغلاق الوالدين . تُعرف هذه الوظائف بالوظائف المعلقة أو القابلة للاستئناف - coroutines. في JavaScript ، تُعرف عادةً generators أو async functions .


لكن هذا قليلا المدقع. باختصار - useMemo هو تخزين القيمة memoized في هذا. الأمر مختلف قليلاً "هذا".


إذا كنا نريد إنشاء مكتبة تحفيظ أفضل ، فيجب أن نجد "هذا" أفضل.

زينغ!


WeakMaps!


نعم! WeakMaps! لتخزين قيمة المفتاح ، حيث سيكون هذا هو المفتاح ، طالما أن WeakMap لا يقبل أي شيء باستثناء هذا ، أي "الكائنات".


لنقم بإنشاء مثال بسيط:


 const createHiddenSpot = (fn) => { const map = new WeakMap(); // a hidden "closure" const set = (key, value) => (map.set(key, value), value); return (key) => { return map.get(key) || set(key, fn(key)) } } const weakSelect = createHiddenSpot(selector); weakSelect(todos); // create a new entry weakSelect(todos); // return an existing entry weakSelect(todos[0]); // create a new entry weakSelect(todos[1]); // create a new entry weakSelect(todos[0]); // return an existing entry! weakSelect(todos[1]); // return an existing entry!! weakSelect(todos); // return an existing entry!!! 

انها بسيطة بغباء ، و "الحق" تماما. لذلك "متى ستزول"؟


  • ننسى ضعيف تحديد و "خريطة" كاملة ستزول
  • ننسى تودوس [0] وسيتم اختفاء دخولهم الضعيف
  • ننسى تودوس - وسيتم اختفاء البيانات المحفوظة!

من الواضح متى "ذهب" شيء - فقط عندما ينبغي!

سحري - جميع القضايا إعادة اختفت. مشاكل مع المذكرة العدوانية - أيضا goner.


هذا النهج تذكر البيانات حتى حان الوقت لتنسى . إنه أمر لا يصدق ، ولكن أن تتذكر شيئًا أفضل ، عليك أن تنساه بشكل أفضل.


الشيء الوحيد الذي يدوم - إنشاء واجهة برمجة تطبيقات أكثر قوة لهذه الحالة


كاش - هو مخبأ


kashe هي مكتبة تحفيظ قائمة على WeakMap ، والتي يمكن أن توفر يومك.


تعرض هذه المكتبة 4 وظائف


  • kashe أجل الحفظ.
  • box - للحصول على الحفظ المسبق ، لزيادة فرصة التحفيظ.
  • inbox - المذكرة البادئة المتداخلة ، لتقليل تغيير المذكرة
  • fork - إلى شوكة (من الواضح) المذكرة.

kashe (fn) => memoizedFn (... args)


انها في الواقع createHiddenSpot من مثال سابق. سيستخدم الوسيطة الأولى كمفتاح ل WeakMap داخلي.


 const selector = (state, prop) => ({result: state[prop]}); const memoized = kashe(selector); const old = memoized(state, 'x') memoized(state, 'x') === old memoized(state, 'y') === memoized(state, 'y') // ^^ another argument // but old !== memoized(state, 'x') // 'y' wiped 'x' cache in `state` 

الوسيطة الأولى هي مفتاح ، إذا قمت باستدعاء الوظيفة مرة أخرى بنفس المفتاح ، ولكن سيتم استبدال وسيطات مختلفة ، وستظل عبارة عن ذاكرة تخزين مؤقت لسطر ذاكرة التخزين المؤقت. لجعلها تعمل - يجب عليك توفير مفاتيح مختلفة لحالات مختلفة ، كما فعلت مع مثال ضعيف حدد ، لتوفير هذا مختلفة لعقد النتائج. إعادة تحديد التعاقب A لا يزال هو الشيء.
ليست كل الوظائف قابلة للتذكر. يجب أن تكون الوسيطة الأولى كائنًا أو صفيفًا أو دالة. يجب أن يكون قابلاً للاستخدام كمفتاح لـ WeakMap.


box (fn) => memoizedFn2 (box ، ... args)


هذه هي نفس الوظيفة ، تطبق فقط مرتين. مرة واحدة لـ fn ، مرة واحدة لـ memoizedFn ، إضافة مفتاح بادئة إلى الوسيطات. قد تجعل أي وظيفة kashe-memoizable.


انها تعريفي تماما - مهلا وظيفة! سأخزن النتائج في هذا المربع.

 // could not be "kashe" memoized const addTwo = (a,b) => ({ result: a+b }); const bAddTwo = boxed(addTwo); const cacheKey = {}; // any object bAddTwo(cacheKey, 1, 2) === bAddTwo(cacheKey, 1, 2) === { result: 3} 

إذا كنت ستحدد وظيفة "memoized" بالفعل - ستزيد من فرصة "memoization" ، كما هو الحال بالنسبة لكل مثيل "memoization" - فيمكنك إنشاء سلسلة memoization.


 const selectSomethingFromTodo = (state, prop) => ... const selector = kashe(selectSomethingFromTodo); const boxedSelector = kashe(selector); class Component { render () { const result = boxedSelector(this, todos, this.props.todoId); // 1. try to find result in `this` // 2. try to find result in `todos` // 3. store in `todos` // 4. store in `this` // if multiple `this`(components) are reading from `todos` - // selector is not working (they are wiping each other) // but data stored in `this` - exists. ... } } 

البريد الوارد (fn) => memoizedFn2 (box ، ... args)


هذا واحد عكس المربع ، ولكن تفعل الشيء نفسه تقريبا ، قيادة ذاكرة التخزين المؤقت المتداخلة لتخزين البيانات في المربع المقدم. من وجهة نظر واحدة - فهو يقلل من احتمالية الحفظ (لا يوجد تسلسل الحفظ) ، ولكن من جهة أخرى - يزيل تصادم ذاكرة التخزين المؤقت ويساعد على عزل العمليات إذا كان ينبغي ألا يتداخلوا مع بعضهم البعض لأي سبب من الأسباب.


انها التصريح تماما - مهلا! الجميع في الداخل! هنا هو مربع للاستخدام

 const getAndSet = (task, number) => task.value + number; const memoized = kashe(getAndSet); const inboxed = inbox(getAndSet); const doubleBoxed = inbox(memoized); memoized(task, 1) // ok memoized(task, 2) // previous result wiped inboxed(key1, task, 1) // ok inboxed(key2, task, 2) // ok // inbox also override the cache for any underlaying kashe calls doubleBoxed(key1, task, 1) // ok doubleBoxed(key2, task, 2) // ok 

شوكة (kashe-memoized) => kashe-memoized


Fork عبارة عن شوكة حقيقية - فهي تحصل على أي وظيفة kashe-memoized ، وتعيد نفس الشيء ، ولكن مع إدخال ذاكرة تخزين مؤقت داخلية أخرى. تذكر طريقة إعادة تعيين mapStateToProps إلى المصنع؟


 const mapStateToProps = () => { // const selector = createSelector(...); // const selector = fork(realSelector); // just fork existing selector. Or box it, or don't do anything // kashe is more "stable" than reselect. return state => ({ data: selector(data), }); } 

إختار من جديد


وهناك شيء آخر يجب أن تعرفه - يمكن أن يحل كاشي محل إعادة التحديد. حرفيا.


 import { createSelector } from 'kashe/reselect'; 

إنه في الواقع نفس إعادة التحديد ، تم إنشاؤه للتو باستخدام kashe كوظيفة تحفيظ.


Codesandbox


هنا مثال صغير للعب به. كما يمكنك مضاعفة فحص الاختبارات - فهي مدمجة وصوتية.
إذا كنت تريد معرفة المزيد حول التخزين المؤقت والذاكرة - تحقق من كيفية كتابتي أسرع مكتبة للذاكرة قبل عام.


ملحوظة: تجدر الإشارة إلى أن النسخة الأبسط من هذا النهج - المذكرة الضعيفة - تستخدم في المشاعر العاطفية لفترة من الوقت. لا شكاوى. يستخدم nano-memoize أيضًا WeakMaps لحالة وسيطة واحدة.

حصلت على النقطة؟ من شأن اتباع نهج "ضعيف" أكثر أن يساعدك على تذكر شيء ما بشكل أفضل ، ونسيانه بشكل أفضل.


https://github.com/theKashey/kashe


نعم ، حول نسيان شيء ما ، هل يمكنك إلقاء نظرة هنا؟


Source: https://habr.com/ru/post/ar446390/


All Articles