2013 में मैंने वेब एप्लिकेशन विकसित करने के लिए उपकरणों का एक न्यूनतम सेट बनाने का लक्ष्य रखा। शायद उस प्रक्रिया से जो सबसे अच्छी चीज़ निकली वह gotoB थी, जो 2k लाइनों के कोड में लिखा गया एक क्लाइंट-साइड, शुद्ध JS फ्रंटएंड फ्रेमवर्क था।
मैं बहुत सफल फ्रंटएंड फ्रेमवर्क के लेखकों द्वारा लिखे गए दिलचस्प लेखों को पढ़ने के बाद इस लेख को लिखने के लिए प्रेरित हुआ:
इन लेखों के बारे में जो बात मुझे उत्साहित करती है, वह यह है कि वे जो कुछ बनाते हैं उसके पीछे के विचारों के विकास के बारे में बात करते हैं; कार्यान्वयन केवल उन्हें वास्तविक बनाने का एक तरीका है, और केवल उन विशेषताओं पर चर्चा की जाती है जो इतनी आवश्यक हैं कि वे स्वयं विचारों का प्रतिनिधित्व करती हैं।
अब तक, gotoB से जो कुछ निकला है उसका सबसे दिलचस्प पहलू यह है कि इसे बनाने की चुनौतियों का सामना करने के परिणामस्वरूप जो विचार विकसित हुए हैं। यही मैं यहाँ बताना चाहता हूँ।
क्योंकि मैंने ढांचे को शुरू से ही बनाया था, और मैं न्यूनतमता और आंतरिक स्थिरता दोनों को प्राप्त करने की कोशिश कर रहा था, मैंने चार समस्याओं को एक ऐसे तरीके से हल किया जो मुझे लगता है कि अधिकांश ढांचे समान समस्याओं को हल करने के तरीके से अलग है।
ये चार विचार हैं जो मैं अब आपके साथ साझा करना चाहता हूँ। मैं ऐसा आपको मेरे उपकरणों का उपयोग करने के लिए मनाने के लिए नहीं कर रहा हूँ (हालाँकि आप ऐसा कर सकते हैं!), बल्कि, उम्मीद करता हूँ कि आप खुद इन विचारों में रुचि ले सकते हैं।
किसी भी वेब एप्लिकेशन को एप्लिकेशन की स्थिति के आधार पर तत्काल मार्कअप (HTML) बनाने की आवश्यकता होती है।
इसे एक उदाहरण से सबसे अच्छे तरीके से समझाया जा सकता है: एक अति-सरल टूडू सूची एप्लिकेशन में, स्थिति टूडू की एक सूची हो सकती है: ['Item 1', 'Item 2']
। क्योंकि आप एक एप्लिकेशन लिख रहे हैं (एक स्थिर पृष्ठ के विपरीत), टूडू की सूची बदलने में सक्षम होना चाहिए।
क्योंकि स्थिति बदलती रहती है, इसलिए आपके एप्लिकेशन का UI बनाने वाले HTML को भी स्थिति के साथ बदलना पड़ता है। उदाहरण के लिए, अपने टूडो को प्रदर्शित करने के लिए, आप निम्न HTML का उपयोग कर सकते हैं:
<ul> <li>Item 1</li> <li>Item 2</li> </ul>
यदि स्थिति बदलती है और तीसरा आइटम जोड़ा जाता है, तो आपकी स्थिति अब इस तरह दिखाई देगी: ['Item 1', 'Item 2', 'Item 3']
; फिर, आपका HTML इस तरह दिखाई देगा:
<ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul>
अनुप्रयोग की स्थिति के आधार पर HTML उत्पन्न करने की समस्या को आमतौर पर टेम्प्लेटिंग भाषा के साथ हल किया जाता है, जो प्रोग्रामिंग भाषा संरचनाओं (चर, सशर्त और लूप) को छद्म HTML में सम्मिलित करता है, जो वास्तविक HTML में विस्तारित हो जाता है।
उदाहरण के लिए, यहां दो तरीके दिए गए हैं जिनसे विभिन्न टेम्प्लेटिंग टूल में ऐसा किया जा सकता है:
// Assume that `todos` is defined and equal to ['Item 1', 'Item 2', 'Item 3'] // Moustache <ul> {{#todos}} <li>{{.}}</li> {{/todos}} </ul> // JSX <ul> {todos.map((item, index) => ( <li key={index}>{item}</li> ))} </ul>
मुझे HTML में तर्क लाने वाले इन वाक्यविन्यासों से कभी लगाव नहीं था। यह महसूस करते हुए कि टेम्प्लेटिंग के लिए प्रोग्रामिंग की आवश्यकता होती है, और इसके लिए अलग से वाक्यविन्यास की आवश्यकता नहीं होती, मैंने ऑब्जेक्ट लिटरल का उपयोग करके HTML को js में लाने का निर्णय लिया। इसलिए, मैं अपने HTML को ऑब्जेक्ट लिटरल के रूप में आसानी से मॉडल कर सकता था:
['ul', [ ['li', 'Item 1'], ['li', 'Item 2'], ['li', 'Item 3'], ]]
यदि मैं सूची बनाने के लिए पुनरावृत्ति का उपयोग करना चाहता हूं, तो मैं बस लिख सकता हूं:
['ul', items.map ((item) => ['li', item])]
और फिर एक फ़ंक्शन का उपयोग करें जो इस ऑब्जेक्ट लिटरल को HTML में परिवर्तित करेगा। इस तरह, सभी टेम्प्लेटिंग JS में की जा सकती है, बिना किसी टेम्प्लेटिंग भाषा या ट्रांसपिलेशन के। मैं HTML का प्रतिनिधित्व करने वाले इन सरणियों का वर्णन करने के लिए लिथ्स नाम का उपयोग करता हूं।
मेरी जानकारी के अनुसार, कोई भी अन्य JS फ्रेमवर्क इस तरह से टेम्प्लेटिंग का दृष्टिकोण नहीं रखता है। मैंने थोड़ी खोजबीन की और JSONML पाया, जो JSON ऑब्जेक्ट्स में HTML को दर्शाने के लिए लगभग समान संरचना का उपयोग करता है (जो JS ऑब्जेक्ट लिटरल के लगभग समान हैं), लेकिन इसके आसपास कोई फ्रेमवर्क नहीं बना है।
मिथ्रिल और हाइपरऐप मेरे द्वारा अपनाए गए दृष्टिकोण के काफी करीब हैं, लेकिन वे अभी भी प्रत्येक तत्व के लिए फ़ंक्शन कॉल का उपयोग करते हैं।
// Mithril m("ul", [ m("li", "Item 1"), m("li", "Item 2") ]) // hyperapp h("ul", [ h("li", "Item 1"), h("li", "Item 2") ])
ऑब्जेक्ट लिटरल का उपयोग करने का तरीका HTML के लिए अच्छा काम करता है, इसलिए मैंने इसे CSS तक विस्तारित किया और अब मैं अपने सभी CSS को ऑब्जेक्ट लिटरल के माध्यम से भी उत्पन्न करता हूं।
यदि किसी कारणवश आप ऐसे वातावरण में हैं जहां आप JSX को ट्रांसपाइल नहीं कर सकते हैं या टेम्पलेटिंग भाषा का उपयोग नहीं कर सकते हैं, और आप स्ट्रिंग्स को संयोजित नहीं करना चाहते हैं, तो आप इसके बजाय इस दृष्टिकोण का उपयोग कर सकते हैं।
मुझे यकीन नहीं है कि मिथ्रिल/हाइपरऐप दृष्टिकोण मेरे से बेहतर है या नहीं; मुझे लगता है कि लिथ्स का प्रतिनिधित्व करने वाले लंबे ऑब्जेक्ट लिटरल लिखते समय, मैं कभी-कभी कहीं कॉमा भूल जाता हूं और कभी-कभी इसे ढूंढना मुश्किल हो सकता है। इसके अलावा, वास्तव में कोई शिकायत नहीं है। और मुझे यह तथ्य पसंद है कि HTML के लिए प्रतिनिधित्व 1) डेटा और 2) JS दोनों में है। यह प्रतिनिधित्व वास्तव में एक वर्चुअल DOM के रूप में कार्य कर सकता है, जैसा कि हम आइडिया #4 पर आने पर देखेंगे।
बोनस विवरण: यदि आप ऑब्जेक्ट लिटरल से HTML उत्पन्न करना चाहते हैं, तो आपको केवल निम्नलिखित दो समस्याओं को हल करना होगा:
मुझे कभी भी घटकों का शौक नहीं रहा। घटकों के इर्द-गिर्द किसी एप्लिकेशन को संरचित करने के लिए घटक से संबंधित डेटा को घटक के अंदर ही रखना आवश्यक है। इससे उस डेटा को एप्लिकेशन के अन्य भागों के साथ साझा करना कठिन या असंभव हो जाता है।
मैंने जिस भी प्रोजेक्ट पर काम किया, मैंने पाया कि मुझे हमेशा एप्लीकेशन स्टेट के कुछ हिस्सों को उन घटकों के बीच साझा करने की आवश्यकता होती है जो एक दूसरे से काफी दूर हैं। एक विशिष्ट उदाहरण उपयोगकर्ता नाम है: आपको खाता अनुभाग में और हेडर में भी इसकी आवश्यकता हो सकती है। तो उपयोगकर्ता नाम कहाँ है?
इसलिए, मैंने पहले ही एक सरल डेटा ऑब्जेक्ट ( {}
) बनाने और अपनी सारी स्थिति को उसमें भरने का फैसला किया। मैंने इसे स्टोर कहा। स्टोर ऐप के सभी हिस्सों की स्थिति रखता है, और इसलिए इसका इस्तेमाल किसी भी घटक द्वारा किया जा सकता है।
2013-2015 में यह दृष्टिकोण कुछ हद तक विधर्मी था, लेकिन तब से इसने व्यापकता और यहां तक कि प्रभुत्व प्राप्त कर लिया है।
मुझे लगता है कि अभी भी काफी नया यह है कि मैं स्टोर के अंदर किसी भी मूल्य तक पहुँचने के लिए पथों का उपयोग करता हूँ। उदाहरण के लिए, यदि स्टोर है:
{ user: { firstName: 'foo' lastName: 'bar' } }
मैं B.get ('user', 'lastName')
लिखकर (मान लीजिए) lastName
तक पहुँचने के लिए एक पथ का उपयोग कर सकता हूँ। जैसा कि आप देख सकते हैं, ['user', 'lastName']
'bar'
का पथ है। B.get
एक फ़ंक्शन है जो स्टोर तक पहुँचता है और इसका एक विशिष्ट भाग लौटाता है, जो आपके द्वारा फ़ंक्शन को दिए गए पथ द्वारा इंगित किया जाता है।
उपरोक्त के विपरीत, प्रतिक्रियाशील गुणों तक पहुँचने का मानक तरीका उन्हें JS चर के माध्यम से संदर्भित करना है। उदाहरण के लिए:
// Svelte let { firstName, lastName } = $props(); firstName = 'foo'; lastName = 'bar'; // Knockout const firstName = ko.observable('foo'); const lastName = ko.observable('bar'); // mobx class UserStore { firstName = 'foo'; lastName = 'bar'; constructor() { makeAutoObservable(this); } } const userStore = new UserStore(); // SolidJS const [firstName, setFirstName] = createSignal('foo'); const [lastName, setLastName] = createSignal('bar');
हालाँकि, इसके लिए आपको firstName
और lastName
(या userStore
) का संदर्भ हर उस जगह रखना होगा जहाँ आपको उस मान की आवश्यकता हो। मैं जिस दृष्टिकोण का उपयोग करता हूँ, उसके लिए आपको केवल स्टोर तक पहुँच की आवश्यकता होती है (जो वैश्विक है और हर जगह उपलब्ध है) और आपको उनके लिए JS चर परिभाषित किए बिना उस तक बारीक पहुँच की अनुमति देता है।
Immutable.js और Firebase Realtime Database मेरे द्वारा किए गए काम के बहुत करीब हैं, हालाँकि वे अलग-अलग ऑब्जेक्ट पर काम कर रहे हैं। लेकिन आप संभावित रूप से उनका उपयोग सब कुछ एक ही स्थान पर संग्रहीत करने के लिए कर सकते हैं जो कि विस्तृत रूप से संबोधित किया जा सकता है।
// Immutable.js let store = Map({ user: Map({ firstName: 'foo', lastName: 'bar' }) }); const firstName = store.getIn(['user', 'firstName']); // 'foo' // Firebase const db = firebase.database(); db.ref('user').set({ firstName: 'foo', lastName: 'bar' }); db.ref('user/firstName').once('value').then(snapshot => { const firstName = snapshot.val(); // 'foo' });
मेरा डेटा वैश्विक रूप से सुलभ स्टोर में होना जिसे पथों के माध्यम से ग्रैन्यूरल रूप से एक्सेस किया जा सकता है, एक ऐसा पैटर्न है जिसे मैंने बेहद उपयोगी पाया है। जब भी मैं const [count, setCount] = ...
या ऐसा कुछ लिखता हूं, तो यह अनावश्यक लगता है। मुझे पता है कि जब भी मुझे उस तक पहुंचने की आवश्यकता होगी, तो मैं बस B.get ('count')
कर सकता हूं, बिना count
या setCount
घोषित किए और पास किए।
अगर आइडिया #2 (पथों के ज़रिए सुलभ एक वैश्विक स्टोर) घटकों से डेटा को मुक्त करता है, तो आइडिया #3 वह तरीका है जिससे मैंने घटकों से कोड को मुक्त किया। मेरे लिए, यह इस लेख का सबसे दिलचस्प विचार है। यहाँ यह है!
हमारी स्थिति वह डेटा है जो परिभाषा के अनुसार परिवर्तनशील है (अपरिवर्तनीयता का उपयोग करने वालों के लिए, तर्क अभी भी कायम है: आप अभी भी चाहते हैं कि स्थिति का नवीनतम संस्करण बदल जाए, भले ही आप स्थिति के पुराने संस्करणों के स्नैपशॉट रखें)। हम स्थिति को कैसे बदल सकते हैं?
मैंने इवेंट के साथ जाने का फैसला किया। मेरे पास स्टोर के लिए पहले से ही पथ थे, इसलिए एक इवेंट केवल एक क्रिया (जैसे set
, add
या rem
) और पथ का संयोजन हो सकता है। इसलिए, अगर मैं user.firstName
अपडेट करना चाहता हूं, तो मैं कुछ इस तरह लिख सकता हूं:
B.call ('set', ['user', 'firstName'], 'Foo')
यह निश्चित रूप से लिखने से अधिक शब्दाडंबरपूर्ण है:
user.firstName = 'Foo';
लेकिन इसने मुझे ऐसा कोड लिखने की अनुमति दी जो user.firstName
में बदलाव का जवाब दे सके। और यह महत्वपूर्ण विचार है: UI में, अलग-अलग भाग होते हैं जो राज्य के अलग-अलग भागों पर निर्भर होते हैं। उदाहरण के लिए, आपके पास ये निर्भरताएँ हो सकती हैं:
user
और currentView
पर निर्भर करता हैuser
पर निर्भर करता हैitems
पर निर्भर करता है
मेरे सामने सबसे बड़ा सवाल यह था: जब user
बदलता है तो मैं हेडर और अकाउंट सेक्शन को कैसे अपडेट करूँ, लेकिन जब items
बदलता है तो नहीं? और मैं इन निर्भरताओं को updateHeader
या updateAccountSection
जैसी विशिष्ट कॉल किए बिना कैसे प्रबंधित करूँ? इस प्रकार की विशिष्ट कॉल "jQuery प्रोग्रामिंग" को इसके सबसे असहनीय रूप में दर्शाती हैं।
मुझे ऐसा कुछ करना बेहतर लगा:
B.respond ('set', [['user'], ['currentView']], function (user, currentView) { // Update the header }); B.respond ('set', ['user'], function (user) { // Update the account section }); B.respond ('set', ['items'], function (items) { // Update the todo list });
इसलिए, यदि user
के लिए कोई set
इवेंट बुलाया जाता है, तो इवेंट सिस्टम उन सभी व्यू को सूचित करेगा जो उस बदलाव (हेडर और अकाउंट सेक्शन) में रुचि रखते हैं, जबकि अन्य व्यू (टूडू लिस्ट) को बिना किसी बाधा के छोड़ देता है। B.respond
वह फ़ंक्शन है जिसका उपयोग मैं उत्तरदाताओं को पंजीकृत करने के लिए करता हूँ (जिन्हें आमतौर पर "ईवेंट श्रोता" या "प्रतिक्रियाएँ" कहा जाता है)। ध्यान दें कि उत्तरदाता वैश्विक हैं और किसी भी घटक से बंधे नहीं हैं; हालाँकि, वे केवल कुछ निश्चित पथों पर set
इवेंट को सुन रहे हैं।
अब, सबसे पहले change
घटना कैसे बुलाई जाती है? मैंने इसे इस तरह किया:
B.respond ('set', '*', function () { // Assume that `path` is the path on which set was called B.call ('change', path); });
मैं इसे थोड़ा सरल कर रहा हूँ, लेकिन gotoB में यह मूलतः इसी प्रकार काम करता है।
इवेंट सिस्टम को केवल फ़ंक्शन कॉल से ज़्यादा शक्तिशाली बनाने वाली बात यह है कि इवेंट कॉल 0, 1 या कोड के कई टुकड़ों को निष्पादित कर सकता है, जबकि फ़ंक्शन कॉल हमेशा सिर्फ़ एक फ़ंक्शन को कॉल करता है। ऊपर दिए गए उदाहरण में, अगर आप B.call ('set', ['user', 'firstName'], 'Foo');
को कॉल करते हैं, तो कोड के दो टुकड़े निष्पादित होते हैं: वह जो हेडर बदलता है और वह जो अकाउंट व्यू बदलता है। ध्यान दें कि अपडेट firstName
के लिए कॉल को इस बात की परवाह नहीं है कि इसे कौन सुन रहा है। यह बस अपना काम करता है और रिस्पॉन्डर को बदलावों को पहचानने देता है।
घटनाएँ इतनी शक्तिशाली होती हैं कि, मेरे अनुभव में, वे गणना किए गए मानों के साथ-साथ प्रतिक्रियाओं को भी प्रतिस्थापित कर सकती हैं। दूसरे शब्दों में, उनका उपयोग किसी भी परिवर्तन को व्यक्त करने के लिए किया जा सकता है जो किसी एप्लिकेशन में होने की आवश्यकता है।
गणना किए गए मान को इवेंट रिस्पॉन्डर के साथ व्यक्त किया जा सकता है। उदाहरण के लिए, यदि आप fullName
गणना करना चाहते हैं और आप इसे स्टोर में उपयोग नहीं करना चाहते हैं, तो आप निम्न कार्य कर सकते हैं:
B.respond ('set', 'user', function () { var user = B.get ('user'); var fullName = user.firstName + ' ' + user.lastName; // Do something with `fullName` here. });
इसी तरह, प्रतिक्रियाएँ उत्तरदाता के साथ भी व्यक्त की जा सकती हैं। इस पर विचार करें:
B.respond ('set', 'user', function () { var user = B.get ('user'); var fullName = user.firstName + ' ' + user.lastName; document.getElementById ('header').innerHTML = '<h1>Hello, ' + fullName + '</h1>'; });
यदि आप HTML उत्पन्न करने के लिए स्ट्रिंग्स के संयोजन को एक मिनट के लिए नजरअंदाज कर दें, तो आप ऊपर जो देखते हैं वह एक रिस्पॉन्डर है जो एक "साइड-इफेक्ट" (इस मामले में, DOM को अपडेट करना) निष्पादित कर रहा है।
(साइड नोट: वेब एप्लिकेशन के संदर्भ में साइड-इफेक्ट की अच्छी परिभाषा क्या होगी? मेरे लिए, यह तीन चीजों पर निर्भर करता है: 1) एप्लिकेशन की स्थिति का अपडेट; 2) DOM में परिवर्तन; 3) AJAX कॉल भेजना)।
मैंने पाया कि DOM को अपडेट करने वाले अलग जीवनचक्र की वास्तव में कोई आवश्यकता नहीं है। gotoB में, कुछ रिस्पॉन्डर फ़ंक्शन हैं जो कुछ हेल्पर फ़ंक्शन की सहायता से DOM को अपडेट करते हैं। इसलिए, जब user
बदलता है, तो कोई भी रिस्पॉन्डर (या अधिक सटीक रूप से, व्यू फ़ंक्शन , क्योंकि यह वह नाम है जो मैं उन रिस्पॉन्डर को देता हूँ जिन्हें DOM के एक हिस्से को अपडेट करने का काम सौंपा जाता है) जो उस पर निर्भर करता है, निष्पादित होगा, जिससे एक साइड इफ़ेक्ट उत्पन्न होगा जो DOM को अपडेट करने में समाप्त होता है।
मैंने इवेंट सिस्टम को पूर्वानुमान योग्य बनाया, क्योंकि इसमें रिस्पॉन्डर फ़ंक्शन को एक ही क्रम में और एक-एक करके चलाया जाता है। एसिंक्रोनस रिस्पॉन्डर अभी भी सिंक्रोनस के रूप में चल सकते हैं, और उनके "बाद" आने वाले रिस्पॉन्डर उनका इंतज़ार करेंगे।
अधिक परिष्कृत पैटर्न, जहाँ आपको DOM को अपडेट किए बिना स्टेट को अपडेट करने की आवश्यकता होती है (आमतौर पर प्रदर्शन उद्देश्यों के लिए) म्यूट क्रियाओं को जोड़कर जोड़ा जा सकता है, जैसे mset , जो स्टोर को संशोधित करता है लेकिन किसी भी रिस्पॉन्डर को ट्रिगर नहीं करता है। साथ ही, यदि आपको रीड्रा होने के बाद DOM पर कुछ करने की आवश्यकता है, तो आप बस यह सुनिश्चित कर सकते हैं कि उस रिस्पॉन्डर की प्राथमिकता कम हो और वह अन्य सभी रिस्पॉन्डर के बाद चले:
B.respond ('set', 'date', {priority: -1000}, function () { var datePicker = document.getElementById ('datepicker'); // Do something with the date picker });
ऊपर बताए गए दृष्टिकोण में, क्रियाओं और पथों का उपयोग करके एक इवेंट सिस्टम और वैश्विक उत्तरदाताओं का एक सेट होना शामिल है जो कुछ इवेंट कॉल द्वारा मेल खाते हैं (निष्पादित होते हैं), इसका एक और लाभ है: प्रत्येक इवेंट कॉल को एक सूची में रखा जा सकता है। फिर आप अपने एप्लिकेशन को डीबग करते समय इस सूची का विश्लेषण कर सकते हैं और स्थिति में परिवर्तनों को ट्रैक कर सकते हैं।
फ्रंटएंड के संदर्भ में, इवेंट और रिस्पॉन्डर निम्नलिखित की अनुमति देते हैं:
ये वो चीजें हैं जिनके बिना वे (मेरे अनुभव में) काम करने की अनुमति देते हैं:
यह सब वास्तव में सिर्फ इवेंट कॉल और रिस्पॉन्डर है, कुछ रिस्पॉन्डर सिर्फ व्यू से संबंधित हैं, और अन्य अन्य ऑपरेशन से संबंधित हैं। फ्रेमवर्क के सभी आंतरिक भाग सिर्फ यूजर स्पेस का उपयोग कर रहे हैं।
यदि आप इस बारे में उत्सुक हैं कि gotoB में यह कैसे काम करता है, तो आप इस विस्तृत विवरण की जांच कर सकते हैं।
दो-तरफ़ा डेटा बाइंडिंग अब काफी पुरानी लगती है। लेकिन अगर आप टाइम मशीन को 2013 में वापस ले जाएं और आप राज्य में बदलाव होने पर DOM को फिर से बनाने की समस्या को पहले सिद्धांतों से हल करें, तो क्या अधिक उचित लगेगा?
वास्तव में, विकल्प 2, जो कि राज्य से DOM तक एकदिशीय डेटा प्रवाह है, अधिक जटिल और साथ ही अकुशल प्रतीत होता है।
आइए अब इसे बहुत ठोस बनाते हैं: एक इंटरैक्टिव <input>
या <textarea>
के मामले में जो फ़ोकस किया गया है, आपको हर उपयोगकर्ता के कीस्ट्रोक के साथ DOM के कुछ हिस्सों को फिर से बनाने की ज़रूरत है! यदि आप यूनिडायरेक्शनल डेटा फ़्लो का उपयोग कर रहे हैं, तो इनपुट में हर बदलाव स्टेट में बदलाव को ट्रिगर करता है, जो फिर <input>
फिर से बनाता है ताकि यह बिल्कुल वैसा ही हो जैसा कि इसे होना चाहिए।
यह DOM अपडेट के लिए बहुत ही उच्च मानक निर्धारित करता है: उन्हें त्वरित होना चाहिए और इंटरैक्टिव तत्वों के साथ उपयोगकर्ता की सहभागिता में बाधा नहीं डालनी चाहिए। इस समस्या से निपटना आसान नहीं है।
अब, स्टेट से DOM (JS से HTML) तक यूनिडायरेक्शनल डेटा क्यों जीता? क्योंकि इसके बारे में तर्क करना आसान है। यदि स्टेट बदलता है, तो इससे कोई फर्क नहीं पड़ता कि यह परिवर्तन कहां से आया (यह सर्वर से डेटा लाने वाला AJAX कॉलबैक हो सकता है, उपयोगकर्ता इंटरैक्शन हो सकता है, टाइमर हो सकता है)। स्टेट हमेशा एक ही तरह से बदलता है (या बल्कि, उत्परिवर्तित होता है )। और स्टेट से होने वाले परिवर्तन हमेशा DOM में प्रवाहित होते हैं।
तो, कोई व्यक्ति DOM अपडेट को कुशल तरीके से कैसे निष्पादित कर सकता है, जिससे उपयोगकर्ता की सहभागिता में बाधा न आए? यह आमतौर पर DOM अपडेट की न्यूनतम मात्रा को निष्पादित करने पर निर्भर करता है, जिससे काम पूरा हो जाएगा। इसे आमतौर पर "डिफिंग" कहा जाता है, क्योंकि आप उन अंतरों की एक सूची बना रहे हैं, जिनकी आपको एक पुरानी संरचना (मौजूदा DOM) लेने और इसे एक नई संरचना (स्टेट अपडेट होने के बाद नया DOM) में बदलने की आवश्यकता है।
जब मैंने 2016 के आसपास इस समस्या पर काम करना शुरू किया, तो मैंने यह देखकर धोखा दिया कि रिएक्ट क्या कर रहा था। उन्होंने मुझे यह महत्वपूर्ण जानकारी दी कि दो पेड़ों (DOM एक पेड़ है) को अलग करने के लिए कोई सामान्यीकृत, रैखिक-प्रदर्शन एल्गोरिथ्म नहीं था। लेकिन, अगर कुछ भी हो, तो मैं अभी भी अंतर करने के लिए एक सामान्य उद्देश्य एल्गोरिथ्म चाहता था। मुझे रिएक्ट (या उस मामले के लिए लगभग किसी भी फ्रेमवर्क) के बारे में जो विशेष रूप से नापसंद है, वह यह है कि आपको सन्निहित तत्वों के लिए कुंजियों का उपयोग करने की आवश्यकता है:
function MyList() { const items = ['Item 1', 'Item 2', 'Item 3']; return ( <ul> {items.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> ); }
मेरे लिए, key
निर्देश अनावश्यक था, क्योंकि इसका DOM से कोई लेना-देना नहीं था; यह केवल फ्रेमवर्क के लिए एक संकेत था।
फिर मैंने एक पेड़ के समतल संस्करणों पर एक पाठ्य अंतर एल्गोरिथ्म की कोशिश करने के बारे में सोचा। क्या होगा अगर मैं दोनों पेड़ों (मेरे पास मौजूद DOM का पुराना हिस्सा और DOM का नया हिस्सा जिसे मैं बदलना चाहता था) को समतल कर दूं और उस पर एक diff
गणना करूं (संपादनों का एक न्यूनतम सेट), ताकि मैं कम संख्या में चरणों में पुराने से नए में जा सकूं?
इसलिए मैंने मायर्स एल्गोरिथ्म लिया, जिसे आप हर बार git diff
चलाते समय इस्तेमाल करते हैं, और इसे अपने फ़्लैटेड पेड़ों पर काम करने के लिए लगाया। आइए एक उदाहरण से समझाते हैं:
var oldList = ['ul', [ ['li', 'Item 1'], ['li', 'Item 2'], ]]; var newList = ['ul', [ ['li', 'Item 1'], ['li', 'Item 2'], ['li', 'Item 3'], ]];
जैसा कि आप देख सकते हैं, मैं DOM के साथ काम नहीं कर रहा हूँ, बल्कि ऑब्जेक्ट लिटरल रिप्रेजेंटेशन के साथ काम कर रहा हूँ जिसे हमने आइडिया 1 में देखा था। अब, आप देखेंगे कि हमें सूची के अंत में एक नया <li>
जोड़ने की आवश्यकता है।
चपटे पेड़ इस तरह दिखते हैं:
var oldFlattened = ['O ul', 'O li', 'L Item 1', 'C li', 'O li', 'L Item 2', 'C li', 'C ul']; var newFlattened = ['O ul', 'O li', 'L Item 1', 'C li', 'O li', 'L Item 2', 'C li', 'O li', 'L Item 3', 'C li', 'C ul'];
O
का मतलब है "ओपन टैग", L
का मतलब है "लिटरल" (इस मामले में, कुछ टेक्स्ट) और C
का मतलब है "क्लोज टैग"। ध्यान दें कि प्रत्येक पेड़ अब स्ट्रिंग्स की एक सूची है, और अब कोई नेस्टेड एरे नहीं हैं। फ़्लैटनिंग से मेरा यही मतलब है।
जब मैं इनमें से प्रत्येक तत्व पर एक diff चलाता हूँ (सरणी में प्रत्येक आइटम को एक इकाई की तरह मानते हुए), तो मुझे यह प्राप्त होता है:
var diff = [ ['keep', 'O ul'] ['keep', 'O li'] ['keep', 'L Item 1'] ['keep', 'C li'] ['keep', 'O li'] ['keep', 'L Item 2'] ['keep', 'C li'] ['add', 'O li'] ['add', 'L Item 3'] ['add', 'C li'] ['keep', 'C ul'] ];
जैसा कि आपने शायद अनुमान लगाया होगा, हम सूची का अधिकांश भाग रख रहे हैं, और इसके अंत में <li>
जोड़ रहे हैं। ये वे add
प्रविष्टियाँ हैं जिन्हें आप देख रहे हैं।
यदि हम अब तीसरे <li>
के पाठ को Item 3
से Item 4
में बदल दें और उस पर एक अंतर चलाएं, तो हमें यह प्राप्त होगा:
var diff = [ ['keep', 'O ul'] ['keep', 'O li'] ['keep', 'L Item 1'] ['keep', 'C li'] ['keep', 'O li'] ['keep', 'L Item 2'] ['keep', 'C li'] ['keep', 'O li'] ['rem', 'L Item 3'] ['add', 'L Item 4'] ['keep', 'C li'] ['keep', 'C ul'] ];
मुझे नहीं पता कि यह तरीका गणितीय रूप से कितना अक्षम है, लेकिन व्यवहार में यह काफी अच्छा काम करता है। यह केवल बड़े पेड़ों को अलग करते समय खराब प्रदर्शन करता है जिनके बीच बहुत अंतर होता है; जब कभी-कभी ऐसा होता है, तो मैं अंतर को बाधित करने के लिए 200ms टाइमआउट का सहारा लेता हूं और बस DOM के आपत्तिजनक हिस्से को पूरी तरह से बदल देता हूं। अगर मैं टाइमआउट का उपयोग नहीं करता, तो पूरा एप्लिकेशन कुछ समय के लिए रुक जाता जब तक कि अंतर पूरा नहीं हो जाता।
मायर्स डिफ का उपयोग करने का एक भाग्यशाली लाभ यह है कि यह प्रविष्टियों पर विलोपन को प्राथमिकता देता है: इसका मतलब है कि यदि किसी आइटम को हटाने और किसी आइटम को जोड़ने के बीच समान रूप से कुशल विकल्प है, तो एल्गोरिथ्म पहले आइटम को हटा देगा। व्यावहारिक रूप से, यह मुझे सभी हटाए गए DOM तत्वों को पकड़ने और उन्हें रीसायकल करने में सक्षम बनाता है यदि मुझे बाद में डिफ में उनकी आवश्यकता होती है। अंतिम उदाहरण में, अंतिम <li>
Item 3
से Item 4
में इसकी सामग्री को बदलकर रीसायकल किया जाता है। तत्वों को रीसायकल करके (नए DOM तत्व बनाने के बजाय) हम प्रदर्शन को उस हद तक बेहतर बनाते हैं जहाँ उपयोगकर्ता को यह एहसास नहीं होता है कि DOM को लगातार फिर से बनाया जा रहा है।
यदि आप सोच रहे हैं कि DOM में परिवर्तन लागू करने वाले इस फ़्लैटनिंग और डिफिंग मैकेनिज्म को लागू करना कितना जटिल है, तो मैं इसे ES5 जावास्क्रिप्ट की 500 लाइनों में करने में कामयाब रहा, और यह इंटरनेट एक्सप्लोरर 6 में भी चलता है। लेकिन, सच कहूँ तो, यह शायद मेरे द्वारा लिखा गया सबसे कठिन कोड था। जिद्दी होने की एक कीमत होती है।
ये वो चार विचार हैं जिन्हें मैं प्रस्तुत करना चाहता था! वे पूरी तरह से मौलिक नहीं हैं, लेकिन मुझे उम्मीद है कि वे कुछ लोगों के लिए नए और दिलचस्प होंगे। पढ़ने के लिए धन्यवाद!