paint-brush
Bir Ön Uç Çerçevesi Yazarken Farklı Yaptığım Dört Şeyile@hacker-ss4mpor
Yeni tarih

Bir Ön Uç Çerçevesi Yazarken Farklı Yaptığım Dört Şey

ile 17m2024/08/27
Read on Terminal Reader

Çok uzun; Okumak

Ön uç çerçeveleri bağlamında belki de hiç duymadığınız dört fikir: - HTML şablonlama için nesne değişmezleri. - Yollar aracılığıyla adreslenebilen küresel bir depo. - Tüm mutasyonları işlemek için olaylar ve yanıtlayıcılar. - DOM'u güncellemek için metinsel bir diff algoritması.
featured image - Bir Ön Uç Çerçevesi Yazarken Farklı Yaptığım Dört Şey
undefined HackerNoon profile picture
0-item
1-item

2013'te web uygulamaları geliştirmek için minimalist bir araç seti oluşturmaya koyuldum. Belki de bu süreçten çıkan en iyi şey, 2k satır kodla yazılmış, istemci taraflı, saf JS ön uç çerçevesi olan gotoB'ydi .


Bu makaleyi yazmam, çok başarılı ön yüz çerçevelerinin yazarları tarafından yazılmış ilginç makaleleri okuduktan sonra başladı:


Bu makalelerde beni heyecanlandıran şey, inşa ettikleri şeylerin ardındaki fikirlerin evriminden bahsetmeleri; uygulama, bunları gerçeğe dönüştürmenin sadece bir yolu ve tartışılan tek özellikler, fikirlerin kendisini temsil edecek kadar önemli olanlardır.


Şimdiye kadar, gotoB'den çıkan en ilginç şey, onu inşa etmenin zorluklarıyla yüzleşmenin bir sonucu olarak ortaya çıkan fikirlerdi. İşte burada ele almak istediğim şey bu.


Çerçeveyi sıfırdan oluşturduğum ve hem minimalizmi hem de iç tutarlılığı yakalamaya çalıştığım için, dört problemi, çoğu çerçevenin aynı problemleri çözme biçiminden farklı olduğunu düşündüğüm bir şekilde çözdüm.


Bu dört fikir şimdi sizinle paylaşmak istediklerim. Bunu sizi araçlarımı kullanmaya ikna etmek için yapmıyorum (ama bunu yapmanıza da izin veriyorum!), bunun yerine fikirlerin kendileriyle ilgilenebileceğinizi umuyorum.

Fikir 1: Şablonlama sorununu çözmek için nesne değişmezleri

Herhangi bir web uygulamasının, uygulamanın durumuna bağlı olarak anında işaretleme (HTML) oluşturması gerekir.


Bu, bir örnekle en iyi şekilde açıklanabilir: Çok basit bir yapılacaklar listesi uygulamasında, durum yapılacaklar listesi olabilir: ['Item 1', 'Item 2'] . Bir uygulama yazdığınız için (statik bir sayfanın aksine), yapılacaklar listesi değişebilmelidir.


Durum değiştiğinden, uygulamanızın kullanıcı arayüzünü oluşturan HTML'nin durumla birlikte değişmesi gerekir. Örneğin, yapılacaklarınızı görüntülemek için aşağıdaki HTML'yi kullanabilirsiniz:

 <ul> <li>Item 1</li> <li>Item 2</li> </ul>


Eğer durum değişirse ve üçüncü bir öğe eklenirse, durumunuz artık şu şekilde görünecektir: ['Item 1', 'Item 2', 'Item 3'] ; bu durumda, HTML'niz şu şekilde görünmelidir:

 <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul>


Uygulamanın durumuna göre HTML üretme sorunu genellikle, programlama dili yapılarını (değişkenler, koşullar ve döngüler) sözde HTML'e ekleyen ve ardından gerçek HTML'e genişletilen bir şablon diliyle çözülür.


Örneğin, farklı şablonlama araçlarında bunun yapılabileceği iki yol şöyledir:

 // 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'ye mantık getiren bu sözdizimlerini hiç sevmedim. Şablonlamanın programlama gerektirdiğini fark edip bunun için ayrı bir sözdizimi kullanmaktan kaçınmak istediğimden, nesne değişmezlerini kullanarak HTML'yi js'ye getirmeye karar verdim. Böylece, HTML'imi nesne değişmezleri olarak basitçe modelleyebildim:

 ['ul', [ ['li', 'Item 1'], ['li', 'Item 2'], ['li', 'Item 3'], ]]


Daha sonra listeyi üretmek için yinelemeyi kullanmak isteseydim, basitçe şunu yazabilirdim:

 ['ul', items.map ((item) => ['li', item])]


Ve sonra bu nesneyi HTML'e dönüştürecek bir fonksiyon kullanın. Bu şekilde, tüm şablonlama herhangi bir şablonlama dili veya transpilasyonu olmadan JS'de yapılabilir. HTML'yi temsil eden bu dizileri tanımlamak için liths adını kullanıyorum.


Bildiğim kadarıyla, şablonlamaya bu şekilde yaklaşan başka hiçbir JS framework yok. Biraz araştırma yaptım ve JSON nesnelerinde HTML'yi temsil etmek için neredeyse aynı yapıyı kullanan JSONML'i buldum (ki bunlar JS nesne değişmezleriyle neredeyse aynıdır), ancak bunun etrafında oluşturulmuş bir framework bulamadım.


Mithril ve Hyperapp benim kullandığım yaklaşıma oldukça yakınlar, ancak yine de her bir öğe için fonksiyon çağrıları kullanıyorlar.

 // Mithril m("ul", [ m("li", "Item 1"), m("li", "Item 2") ]) // hyperapp h("ul", [ h("li", "Item 1"), h("li", "Item 2") ])


Nesne değişmezlerini kullanma yaklaşımı HTML için iyi sonuç verdi, bu yüzden bunu CSS'e genişlettim ve artık tüm CSS'imi nesne değişmezleri aracılığıyla oluşturuyorum.


Herhangi bir sebepten dolayı JSX'i derleyemeyeceğiniz veya şablon dili kullanamayacağınız bir ortamdaysanız ve dizeleri birleştirmek istemiyorsanız, bunun yerine bu yaklaşımı kullanabilirsiniz.


Mithril/Hyperapp yaklaşımının benimkinden daha iyi olup olmadığından emin değilim; litleri temsil eden uzun nesne değişmezleri yazarken bazen bir yerde virgülü unuttuğumu ve bunu bulmanın bazen zor olduğunu görüyorum. Bunun dışında gerçekten şikayetim yok. Ve HTML için temsilin hem 1) veri hem de 2) JS'de olması gerçeğini seviyorum. Bu temsil aslında sanal bir DOM olarak işlev görebilir, bunu Fikir #4'e geldiğimizde göreceğiz.


Bonus ayrıntısı: Eğer nesne değişmezlerinden HTML üretmek istiyorsanız, sadece aşağıdaki iki problemi çözmeniz gerekir:

  1. Dizeleri varlıklandırın (yani: özel karakterlerden kaçın).
  2. Hangi etiketleri kapatacağınızı ve hangilerini kapatmayacağınızı bilin.

Fikir 2: Tüm uygulama durumlarını tutmak için yollar aracılığıyla adreslenebilen küresel bir depolama

Bileşenlerden hiç hoşlanmadım. Bir uygulamayı bileşenler etrafında yapılandırmak, bileşene ait verileri bileşenin içine yerleştirmeyi gerektirir. Bu, söz konusu verileri uygulamanın diğer bölümleriyle paylaşmayı zorlaştırır hatta imkansız hale getirir.


Üzerinde çalıştığım her projede, uygulama durumunun bazı kısımlarının birbirinden oldukça uzak bileşenler arasında paylaşılması gerektiğini gördüm. Tipik bir örnek kullanıcı adıdır: buna hesap bölümünde ve ayrıca başlıkta ihtiyacınız olabilir. Peki kullanıcı adı nereye ait?


Bu nedenle, erken bir aşamada basit bir veri nesnesi ( {} ) oluşturmaya ve tüm durumumu oraya doldurmaya karar verdim. Buna depo adını verdim. Depo, uygulamanın tüm parçalarının durumunu tutar ve bu nedenle herhangi bir bileşen tarafından kullanılabilir.


Bu yaklaşım 2013-2015 yıllarında bir bakıma sapkınlık olarak değerlendirilse de o zamandan bu yana yaygınlık kazandı, hatta hakimiyet kazandı.


Hala oldukça yeni olduğunu düşündüğüm şey, mağazanın içindeki herhangi bir değere erişmek için yollar kullanmam. Örneğin, mağaza şuysa:

 { user: { firstName: 'foo' lastName: 'bar' } }


B.get ('user', 'lastName') yazarak (diyelim ki) lastName erişmek için bir yol kullanabilirim. Gördüğünüz gibi, ['user', 'lastName'] 'bar' giden yoldur . B.get , depoya erişen ve fonksiyona geçirdiğiniz yolla belirtilen belirli bir kısmını döndüren bir fonksiyondur.


Yukarıdakilerin aksine, reaktif özelliklere erişmenin standart yolu, bunlara bir JS değişkeni aracılığıyla başvurmaktır. Örneğin:

 // 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');


Ancak bu, firstName ve lastName (veya userStore ) için bu değere ihtiyaç duyduğunuz her yerde bir referans tutmanızı gerektirir. Kullandığım yaklaşım yalnızca mağazaya erişiminizin olmasını gerektirir (bu, küreseldir ve her yerde kullanılabilir) ve onlar için JS değişkenleri tanımlamadan ona ayrıntılı erişim sağlamanıza olanak tanır.


Immutable.js ve Firebase Realtime Database, ayrı nesneler üzerinde çalışıyor olsalar da, benim yaptığım şeye çok daha yakın bir şey yapıyorlar. Ancak, bunları potansiyel olarak, ayrıntılı olarak adreslenebilir olabilecek tek bir yerde her şeyi depolamak için kullanabilirsiniz.

 // 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' });


Verilerimi, yollar aracılığıyla ayrıntılı olarak erişilebilen, küresel olarak erişilebilir bir depoda bulundurmak, son derece yararlı bulduğum bir kalıptır. Ne zaman const [count, setCount] = ... veya buna benzer bir şey yazsam, gereksiz geliyor. count veya setCount bildirip iletmek zorunda kalmadan, buna erişmem gerektiğinde B.get ('count') yapabileceğimi biliyorum.

Fikir 3: Her bir değişim olaylar aracılığıyla ifade edilir

Fikir #2 (yollar aracılığıyla erişilebilen küresel bir depo) verileri bileşenlerden kurtarıyorsa, Fikir #3 kodu bileşenlerden nasıl kurtardığımdır. Bana göre, bu makaledeki en ilginç fikir bu. İşte başlıyor!


Durumumuz, tanımı gereği değişebilir bir veridir (değişmezliği kullananlar için argüman hala geçerlidir: Durumun eski sürümlerinin anlık görüntülerini tutsanız bile, durumun en son sürümünün değişmesini istersiniz). Durumu nasıl değiştiririz?


Etkinliklerle gitmeye karar verdim. Mağazaya giden yollarım zaten vardı, bu yüzden bir etkinlik basitçe bir fiilin ( set , add veya rem gibi) ve bir yolun birleşimi olabilirdi. Yani, user.firstName güncellemek isteseydim, şöyle bir şey yazabilirdim:

 B.call ('set', ['user', 'firstName'], 'Foo')


Bu kesinlikle şunu yazmaktan daha ayrıntılıdır:

 user.firstName = 'Foo';


Ancak bu bana user.firstName bir değişikliğe yanıt verecek kod yazma olanağı verdi. Ve bu önemli fikirdir: Bir kullanıcı arayüzünde, durumun farklı bölümlerine bağımlı farklı bölümler vardır. Örneğin, şu bağımlılıklara sahip olabilirsiniz:

  • Başlık: user ve currentView bağlıdır
  • Hesap bölümü: user bağlıdır
  • Yapılacaklar listesi: items bağlıdır


Karşılaştığım büyük soru şuydu: user değiştiğinde başlığı ve hesap bölümünü nasıl güncellerim, ancak items değiştiğinde güncellemem? Ve updateHeader veya updateAccountSection gibi belirli çağrılar yapmak zorunda kalmadan bu bağımlılıkları nasıl yönetirim? Bu tür belirli çağrılar, "jQuery programlamayı" en sürdürülemez haliyle temsil eder.


Bana daha iyi bir fikir gibi görünen şey şöyle bir şey yapmaktı:

 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 });


Yani, user için bir set olayı çağrılırsa, olay sistemi bu değişiklikle ilgilenen tüm görünümleri (başlık ve hesap bölümü) bilgilendirir, diğer görünümleri (yapılacaklar listesi) ise rahatsız etmez. B.respond yanıtlayıcıları (genellikle "olay dinleyicileri" veya "tepkiler" olarak adlandırılır) kaydetmek için kullandığım işlevdir. Yanıtlayıcıların küresel olduğunu ve herhangi bir bileşene bağlı olmadığını unutmayın; ancak, yalnızca belirli yollardaki set olaylarını dinlerler.


Peki, bir change olayı ilk etapta nasıl çağrılır? Ben bunu şu şekilde yaptım:

 B.respond ('set', '*', function () { // Assume that `path` is the path on which set was called B.call ('change', path); });


Biraz basitleştiriyorum ama gotoB'de işler esasen böyle yürüyor.


Bir olay sistemini basit fonksiyon çağrılarından daha güçlü kılan şey, bir olay çağrısının 0, 1 veya birden fazla kod parçasını yürütebilmesi, bir fonksiyon çağrısının ise her zaman tam olarak bir fonksiyonu çağırmasıdır . Yukarıdaki örnekte, B.call ('set', ['user', 'firstName'], 'Foo'); çağırırsanız, iki kod parçası yürütülür: başlığı değiştiren ve hesap görünümünü değiştiren. firstName güncelleme çağrısının bunu kimin dinlediğini "umursamadığına" dikkat edin. Sadece kendi işini yapar ve yanıtlayanın değişiklikleri almasını sağlar.


Olaylar o kadar güçlüdür ki, deneyimime göre, hesaplanan değerlerin yanı sıra tepkileri de değiştirebilirler. Başka bir deyişle, bir uygulamada gerçekleşmesi gereken herhangi bir değişikliği ifade etmek için kullanılabilirler.


Hesaplanan bir değer bir olay yanıtlayıcısı ile ifade edilebilir. Örneğin, bir fullName hesaplamak istiyorsanız ve bunu depoda kullanmak istemiyorsanız, aşağıdakileri yapabilirsiniz:

 B.respond ('set', 'user', function () { var user = B.get ('user'); var fullName = user.firstName + ' ' + user.lastName; // Do something with `fullName` here. });


Benzer şekilde, tepkiler bir yanıtlayıcı ile ifade edilebilir. Şunu düşünün:

 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 üretmek için dizelerin birleştirilmesinin yarattığı utanç verici durumu bir dakikalığına göz ardı ederseniz, yukarıda gördüğünüz şey bir yanıtlayıcının bir "yan etkiyi" (bu durumda DOM'u güncelleme) yürütmesidir.


(Yan not: Bir web uygulaması bağlamında, yan etkinin iyi bir tanımı ne olabilir? Bana göre, üç şeye indirgenebilir: 1) Uygulamanın durumunda bir güncelleme; 2) DOM'da bir değişiklik; 3) Bir AJAX çağrısı gönderme).


DOM'u güncelleyen ayrı bir yaşam döngüsüne gerçekten gerek olmadığını buldum. GotoB'de, bazı yardımcı işlevlerin yardımıyla DOM'u güncelleyen bazı yanıtlayıcı işlevler vardır. Yani, user değiştiğinde, ona bağlı olan herhangi bir yanıtlayıcı (veya daha doğrusu, DOM'un bir bölümünü güncellemekle görevli yanıtlayıcılara verdiğim isim olduğu için görüntüleme işlevi ) yürütülür ve DOM'u güncelleyen bir yan etki oluşturur.


Olay sistemini, yanıtlayıcı işlevlerini aynı sırayla ve birer birer çalıştırarak öngörülebilir hale getirdim. Asenkron yanıtlayıcılar hala senkron olarak çalışabilir ve onlardan "sonra" gelen yanıtlayıcılar onları bekleyecektir.


DOM'u güncellemeden durumu güncellemeniz gereken daha karmaşık desenler (genellikle performans amaçları için), mset gibi depoyu değiştiren ancak herhangi bir yanıtlayıcıyı tetiklemeyen sessiz fiiller eklenerek eklenebilir. Ayrıca, bir yeniden çizim gerçekleştikten sonra DOM'da bir şey yapmanız gerekiyorsa, bu yanıtlayıcının düşük önceliğe sahip olduğundan ve diğer tüm yanıtlayıcılardan sonra çalıştığından emin olabilirsiniz:

 B.respond ('set', 'date', {priority: -1000}, function () { var datePicker = document.getElementById ('datepicker'); // Do something with the date picker });


Yukarıdaki yaklaşım, fiiller ve yollar kullanan bir olay sistemine ve belirli olay çağrıları tarafından eşleştirilen (yürütülen) bir dizi küresel yanıtlayıcıya sahip olmanın başka bir avantajına sahiptir: her olay çağrısı bir listeye yerleştirilebilir. Daha sonra uygulamanızı hata ayıkladığınızda bu listeyi analiz edebilir ve durumdaki değişiklikleri izleyebilirsiniz.


Bir ön uç bağlamında, olayların ve yanıtlayıcıların izin verdiği şeyler şunlardır:

  • Mağazanın bölümlerini çok az kodla (sadece değişken atamasından biraz daha ayrıntılı) güncellemek.
  • DOM'un parçalarının, DOM'un o parçasının bağlı olduğu mağaza parçalarında bir değişiklik olduğunda otomatik olarak güncellenmesi.
  • DOM'un herhangi bir parçasının ihtiyaç duyulmadığında otomatik olarak güncellenmemesi.
  • DOM'u güncellemekle ilgisi olmayan, yanıtlayıcılar olarak ifade edilen hesaplanmış değerlere ve tepkilere sahip olabilmek.


Deneyimime göre, şunlardan vazgeçebilirsiniz:

  • Yaşam döngüsü yöntemleri veya kancaları.
  • Gözlemlenebilirler.
  • Değişmezlik.
  • Ezberleme.


Aslında hepsi sadece olay çağrıları ve yanıtlayıcılardır, bazı yanıtlayıcılar sadece görünümlerle ilgilenir ve diğerleri diğer işlemlerle ilgilenir. Çerçevenin tüm iç kısımları sadece kullanıcı alanını kullanır.


Eğer gotoB'de bunun nasıl çalıştığını merak ediyorsanız, bu detaylı açıklamayı inceleyebilirsiniz.

Fikir 4: DOM'u güncellemek için bir metin diff algoritması

İki yönlü veri bağlama artık oldukça eskimiş geliyor. Ancak bir zaman makinesiyle 2013'e geri dönerseniz ve durum değiştiğinde DOM'u yeniden çizme sorununu ilk prensiplerden ele alırsanız, kulağa daha makul gelen ne olurdu?

  • HTML değişirse, JS'deki durumunuzu güncelleyin. JS'deki durum değişirse, HTML'yi güncelleyin.
  • JS'deki durum her değiştiğinde, HTML'yi güncelleyin. HTML değişirse, JS'deki durumu güncelleyin ve ardından HTML'yi JS'deki durumla eşleşecek şekilde yeniden güncelleyin.


Aslında, durumdan DOM'a tek yönlü veri akışı olan 2. seçenek daha karmaşık ve verimsiz görünüyor.


Şimdi bunu çok somut hale getirelim: Odaklanmış etkileşimli bir <input> veya <textarea> durumunda, her kullanıcının tuş vuruşuyla DOM'un parçalarını yeniden oluşturmanız gerekir! Tek yönlü veri akışları kullanıyorsanız, girdideki her değişiklik, durumda bir değişikliği tetikler ve bu da <input> tam olarak olması gerektiği gibi olacak şekilde yeniden çizer.


Bu, DOM güncellemeleri için çok yüksek bir çıta belirler: hızlı olmalı ve etkileşimli öğelerle kullanıcı etkileşimini engellememelidir. Bu, üstesinden gelinmesi kolay bir sorun değildir.


Peki, neden state'den DOM'a (JS'den HTML'e) tek yönlü veri kazandı? Çünkü bunun hakkında akıl yürütmek daha kolay. State değişirse, bu değişikliğin nereden geldiği önemli değildir (sunucudan veri getiren bir AJAX geri araması olabilir, bir kullanıcı etkileşimi olabilir, bir zamanlayıcı olabilir). State her zaman aynı şekilde değişir (ya da daha doğrusu mutasyona uğrar ). State'ten gelen değişiklikler her zaman DOM'a akar.


Peki, kullanıcı etkileşimini engellemeyen etkili bir şekilde DOM güncellemeleri nasıl gerçekleştirilir? Bu genellikle işi yapacak en az miktarda DOM güncellemesi yapmaya dayanır. Buna genellikle "farklılaştırma" denir, çünkü eski bir yapıyı (mevcut DOM) alıp yeni bir yapıya (durum güncellendikten sonraki yeni DOM) dönüştürmeniz gereken farklılıkların bir listesini yapıyorsunuz.


Bu sorun üzerinde 2016 civarında çalışmaya başladığımda, React'in ne yaptığını inceleyerek hile yaptım. Bana iki ağacın diff'ini çıkarmak için genelleştirilmiş, doğrusal performanslı bir algoritma olmadığı konusunda önemli bir içgörü verdiler (DOM bir ağaçtır). Ancak, inatçı olsam da, diff'i gerçekleştirmek için yine de genel amaçlı bir algoritma istiyordum. React'in (veya bu konuda hemen hemen her çerçevenin) özellikle sevmediğim yanı, bitişik öğeler için anahtarlar kullanmanız gerektiği konusunda ısrarcı olmalarıdır:

 function MyList() { const items = ['Item 1', 'Item 2', 'Item 3']; return ( <ul> {items.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> ); }


Bana göre key yönergesi gereksizdi, çünkü DOM ile hiçbir ilgisi yoktu; sadece çerçeveye bir ipucuydu.


Sonra bir ağacın düzleştirilmiş versiyonlarında metinsel bir diff algoritması denemeyi düşündüm. Ya her iki ağacı da (sahip olduğum eski DOM parçası ve onu değiştirmek istediğim yeni DOM parçası) düzleştirirsem ve üzerinde bir diff (minimum düzenleme kümesi) hesaplarsam, böylece eskisinden yenisine daha az sayıda adımda geçebilirim?


Bu yüzden Myers algoritmasını aldım, her git diff çalıştırdığınızda kullandığınız algoritmayı, ve onu düzleştirilmiş ağaçlarımda çalıştırdım. Bir örnekle açıklayalım:

 var oldList = ['ul', [ ['li', 'Item 1'], ['li', 'Item 2'], ]]; var newList = ['ul', [ ['li', 'Item 1'], ['li', 'Item 2'], ['li', 'Item 3'], ]];


Gördüğünüz gibi, DOM ile çalışmıyorum, ancak Fikir 1'de gördüğümüz nesne-düzeni gösterimi ile çalışıyorum. Şimdi, listenin sonuna yeni bir <li> eklememiz gerektiğini fark edeceksiniz.


Düzleştirilmiş ağaçlar şu şekilde görünüyor:

 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 "açık etiket", L "gerçek" (bu durumda, biraz metin) ve C "kapalı etiket" anlamına gelir. Her ağacın artık bir dize listesi olduğunu ve artık iç içe geçmiş diziler olmadığını unutmayın. Düzleştirmeyle kastettiğim şey budur.


Bu öğelerin her biri üzerinde bir diff çalıştırdığımda (dizideki her öğeyi bir birim gibi ele alarak), şunu elde ediyorum:

 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'] ];


Muhtemelen çıkardığınız gibi, listenin çoğunu tutuyoruz ve sonuna bir <li> ekliyoruz. Gördüğünüz add girişleri bunlar.


Şimdi üçüncü <li> metnini Item 3 Item 4 değiştirirsek ve üzerinde bir diff çalıştırırsak, şunu elde ederiz:

 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'] ];


Bu yaklaşımın matematiksel olarak ne kadar verimsiz olduğunu bilmiyorum ama pratikte oldukça iyi çalıştı. Sadece aralarında çok fazla fark olan büyük ağaçların diff'ini alırken kötü performans gösteriyor; bu ara sıra olduğunda, diff'i kesmek ve basitçe DOM'un sorunlu kısmını tamamen değiştirmek için 200 ms'lik bir zaman aşımına başvuruyorum. Zaman aşımı kullanmasaydım, diff tamamlanana kadar tüm uygulama bir süre dururdu.


Myers diff'i kullanmanın şanslı bir avantajı, eklemelerden ziyade silmelere öncelik vermesidir: bu, bir öğeyi kaldırmak ve bir öğe eklemek arasında eşit derecede verimli bir seçim varsa, algoritmanın önce bir öğeyi kaldıracağı anlamına gelir. Pratikte, bu, tüm ortadan kaldırılan DOM öğelerini almamı ve daha sonra diff'te ihtiyaç duyduğumda bunları geri dönüştürebilmemi sağlar. Son örnekte, son <li> içeriği Item 3 Item 4 değiştirilerek geri dönüştürülür. Öğeleri geri dönüştürerek (yeni DOM öğeleri oluşturmak yerine), kullanıcının DOM'un sürekli olarak yeniden çizildiğini fark etmediği bir dereceye kadar performansı iyileştiririz.


DOM'a değişiklikler uygulayan bu düzleştirme ve farklılaştırma mekanizmasını uygulamanın ne kadar karmaşık olduğunu merak ediyorsanız, bunu 500 satır ES5 javascript'inde yapmayı başardım ve hatta Internet Explorer 6'da bile çalışıyor. Ancak itiraf etmeliyim ki, yazdığım en zor kod parçasıydı. İnatçı olmanın bir bedeli var.

Çözüm

Sunmak istediğim dört fikir bunlar! Tamamen orijinal değiller ama umarım bazıları için hem yenilikçi hem de ilginç olurlar. Okuduğunuz için teşekkürler!