Created
June 23, 2020 05:53
-
-
Save sontd-0882/e4b2f480697b02f523042186d2d7da4f to your computer and use it in GitHub Desktop.
Revisions
-
sontd-0882 created this gist
Jun 23, 2020 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,140 @@ # Vanilla JS Event Delegation Hôm nay chúng ta sẽ cùng thảo luận 1 tí về việc _delegate event_ trong javascript (vanilla JS). Chắc mọi người đã từng nghe qua khái niệm bubbling, đơn giản thôi: > When an element in the DOM is clicked, the event bubbles all the way up to the parent element (the document and then the window). This allows you to listen for events on the parent item and still detect clicks that happen inside it. Hiểu nôm na là khi một sự kiện xảy ra trên DOM, các hàm handlers sẽ được gọi trên nó, sau đó đến parent element, đến document và cả window.  _(Ảnh: https://javascript.info/bubbling-and-capturing)_ __"Phần lớn"__ các events là bubble. Một số không bubble ví dụ event `focus`. # The concept Như vậy cách tiếp cận ở đây là thay vì gắn listener vào DOM cụ thể, bạn sẽ gắn listener đó vào một parent element (ví dụ `document` hoặc `window`). Tất cả các event cùng loại đó sẽ có thể xảy ra bên trong parent element đó đều sẽ được bubble up, nhưng đừng lo, bạn hoàn toàn có thể lọc những event match với element mà bạn muốn handle là xong. Ví dụ bạn muốn thực thi 1 đoạn logic khi click vào `button.click-me`, cách truyền thống mà bạn sẽ thực hiện: ```js document.querySelector('.click-me').addEventListener(function(evenet) { /* Phần code của event handler sẽ nằm đây */ }); ``` Trong phần code trên thậm chí bạn còn phải check nếu `querySelector` trả về `null` nữa. Còn với __event delegation__ code của bạn sẽ như sau: ```js document.addEventListener('click', function (event) { /* Kiểm tra nếu element vừa bị click có phải là .click-me không */ /* Nếu không thì return */ if (!event.target.matches('.click-me')) return; /* Phần code của event handler sẽ nằm đây */ }); ``` Trông nó hơi bựa đúng không, handler không gắn lên element mà lại gắn cho 1 element khác, sẽ hơi bất tiện trong việc quản lý. Đó chính xác là một điểm yếu, nhưng cũng sẽ là thế mạnh trong 1 số trường hợp khác. Cụ thể khi bạn muốn gắn event listener vào nhiều hơn 1 element, có class name là `click-me`. Với jQuery thì mọi chuyện sẽ rất đơn giản: ```js $('.click-me').click(function(event) { // Do things... }); ``` Tuy nhiên, với vanilla JS thì `addEventListener()` không hoạt động như bạn mong đợi, nó chỉ có thể gắn vào 1 element cụ thể. Và nếu code như này tất nhiên sẽ _không_ thành công. ```js document.querySelectorAll('.click-me') .addEventListener('click', function (event) { // Do stuff... }, false); ``` Và nếu dùng vòng lặp để gán vào từng element cũng tạch nốt, vì biến `i` trong vanilla JS không `scoped` trong vòng lặp. Như này cũng tạch luôn ```js var elements = document.querySelectorAll('.click-me'); for (var i = 0; i < elements.length; i++) { elements[i].addEventListener('click', function (event) { // Do stuff... }, false); } ``` Lúc này _event delegation_ lại là lựa chọn tối ưu. Nghe mọi click hanldler đều gắn vào `document` có vẻ hơi đáng quan ngại, không biết nó có làm giảm performance không. Nhưng thực tế, performance sẽ được cải thiện vì sẽ không phải cấp phát nhiều vùng nhớ cho nhiều event handler, performance còn cao hơn cả việc có hàng tá event linsteners trên từng element. Nếu bạn cần lắng nghe sự kiện click trên nhiều phần tử và làm nhiều việc khác nhau cho từng phần tử, bạn có thể chọn _event delegation_ để tối ưu hoá performance. Vậy còn lợi ích nào khác không? còn # Dynamically rendered elements Nếu bạn gắn event handler vào một element cụ thể vào lúc DOM loads thì khi sinh ra moojt DOM mới sau thời điểm đó, bạn cũng sẽ phải chạy lại đoạn code gắn event handler đó vào DOM mới, mà bất cẩn thì có thể bạn sẽ làm những DOM cũ gắn nhiều event handler giống nhau nữa. Ví dụ trên xuất hiện nhiều trong các trang web ngày nay với sự phổ biến của ajax. Do đó, hãy gắn sự kiện vào parent element hoặc `document` để giải quyết vấn đề. # Modular functions Có thể bạn sẽ tự hỏi làm thế nào để tránh việc lộn xộn logic khi có quá nhiều element cần handler bên trong 1 handler chung như vậy? Bạn hãy move từng khối logic sang từng hàm handler khác nhau, và pass `event` object vào. ```js var showModal = function(event) { if (!event.target.matches('.modal-open')) return; // Run your code to open a modal }; var hideModal = function(event) { if (!event.target.matches('.close')) return; // Run your code to close a modal }; var handleShowMe = function(event) { if (!event.target.matches('.show-me')) return; // The code you want to run goes here... }; var handleSaves = function(event) { if (!event.target.matches('.save')) return; // The code you want to run goes here... }; document.addEventListener('click', function(event) { showModal(event); hideModal(event); handleShowMe(event); handleSaves(event); }); ``` Với ý tưởng trên, bạn có thể dùng duy nhất 1 event listener cho toàn bộ trang, và cũng tránh được việc phải `if...else` lộn xộn bên trong nó. Mỗi logic nó nằm trong từng hàm riêng của nó, khá dễ dàng để thêm, hay bớt, thậm chí sửa từng logic. # Conclusion Không có một chiếc áo nào mặc vừa cho tất cả mọi người, cũng như không có giải pháp nào là tốt và tối ưu toàn diện, mà nó chỉ có thể phù hợp với hoàn cảnh sử dụng của bạn hay không. Cách tiếp cận này khá ổn tuy nhiên với các single page application mình không khuyến khích lắm vì độ phứt tạp của nó cao hơn rất nhiều, và object document sẽ không bị khởi tạo lại cho tới khi bạn tải lại trang. Do đó nếu muốn áp dụng _event delegation_ bạn sẽ phải quản lý thật tốt việc `addEventListener` và `removeEventListener` nếu không bạn sẽ gặp phải những bug không đáng có đấy. Xin chào và hẹn gặp lại trong các bài viết sau nhé. This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,137 @@ # Đặt vấn đề Thỉnh thoảng, bạn sẽ lâm vào tình cảnh buộc phải tạo ra những component cực nặng trong việc create và render, nguyên nhân chủ yếu là nó phải thực hiện nhiều logic quá phức tạp. Tôi (tác giả) cũng không ngoại lệ. Tôi đã làm ra một trang sử dụng có sử dụng [StoryBlok](https://www.storyblok.com/developers?utm_source=newsletter&utm_medium=logo&utm_campaign=vuedose), họ có một tính năng cực kỳ ngon đó là tạo ra một `rich-text field` để những người quản lý nội dung có thể sử dụng để định dạng văn bản như text, list, images, quote blocks, in đậm, in nghiêng... Khi bạn get nội dung của `rich-text` từ StoryBlok API sẽ thấy nó có cấu trúc riêng của nó. Để render data đó thành HTML, bạn phải gọi hàm `richTextResolver.render(content)` từ `storyblok-js-client`. Ví dụ nếu gói gọn chức năng này vào một component `RichText.vue` thì về cơ bản sẽ như này: ```html <template> <div v-html="contentHtml"></div> </template> <script> export default { props: ["content"], computed: { contentHtml() { return this.$storyapi.richTextResolver.render(content); } } }; </script> ``` Code trông khá đơn giản nhỉ, có vẻ không có gì lạ lẫm lắm, nhưng lại có 1 điều khá bất ngờ. Có vẻ như việc render tỏ ra rất nặng nề, dễ nhận thấy nhất là khi render nhiều components như thế này với một lượng nội dung kha khá. Bây giờ tưởng tượng tình huống như sau: Bạn có 1 list các `rich-text` component ở trên page và một dropdown để filter hiển thị theo tiêu chí nào. Khi thay đổi trên dropdown filter, bạn fetch lại tất cả nội dung và cái list sẽ được render lại. Đây là lúc bạn nhận rõ sự nặng nề của `richTextResolver.render`, cái dropdown bị lag khi đóng lại sau khi bạn select. Nguyên nhân là mặc định JavaScript thực thi các lệnh trên main thread, which is UI-blocking. Vấn đề đã ngộ, Vậy có cách cứu chữa nào ko? # Giải quyết vấn đề Dễ thôi: Sử dụng Web Worker cho việc render rich-text. Web Workers chạy trong một Thread riêng biệt, quan trọng hơn là chúng ko `UI-blocking`, rất phù hợp cho case của chúng ta. > Note: Tôi không đi sâu vào Web Workers, bạn có thể xem thêm [documents](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) của nó. Hãy luôn nhớ rằng web workers có context riêng của nó, và mặc định chúng ta không thể access vào các contexts khác bên ngoài, nhưng chúng ta lại cần access `storyblok-js-client` module. Để làm được điều này, Webpack có 1 giải pháp là `worker-loader`. Đầu tiên, cài đặt trước: ```sh npm install -D worker-loader # or yarn add worker-loader ``` Nếu bạn dùng Nuxt.js, chúng ta sẽ config nó trong `nuxt.config.js` như sau: ```js build: { extend(config, { isDev, isClient }) { config.module.rules.push({ test: /\.worker\.js$/, use: { loader: "worker-loader" } }); } } ``` Với cấu hình này, tất cả các file `*.worker.js` đều sẽ được xử lý và load bởi `worker-loader`. Bây giờ thử tạo `render-html.worker.js` ```js import StoryblokClient from "storyblok-js-client"; let storyClient = new StoryblokClient({}); // When the parent theard requires it, render the HTML self.addEventListener("message", ({ data }) => { const result = storyClient.richTextResolver.render(data); self.postMessage(result); }); ``` Đó là một cách làm cơ bản của worker. Bạn cần phải lắng nghe sự kiện `message` - cách mà worker giao tiếp với Vue.js app. Từ đó bạn có thể lấy `data` từ event, render nó bằng `storyblok-js-client` và gửi về lại kết quả `self.postMessage`. Bây giờ cần sửa component `RichText.vue` một xíu để sử dụng worker. ```html <template> <div v-html="contentHtml"></div> </template> <script> import Worker from "./render-html.worker.js"; const worker = new Worker(); export default { props: ["content"], data() { return { contentHtml: "" }; }, mounted() { // Update the state with the processed HTML content worker.onmessage = ({ data }) => { this.contentHtml = data; }; // Call the worker to render the content worker.postMessage(this.content); } }; </script> ``` # Kết quả Hẳn là bạn tò mò về kết quả hiệu năng sẽ được cải thiện bao nhiêu sau khi sử dụng worker, chắc chắn rồi. Chúng ta sẽ thử đo đạc 1 tí để performace thêm ý nghĩa. Thật ra bạn nên đọc trước bài này: [learn and undestand how to meassure performance in Vue.js components](https://vuedose.tips/tips/measure-runtime-performance-in-vue-js-apps) để đảm bản là bạn hiểu rõ hơn về việc test bên dưới. Tôi đã mmeassure component bằng `medium-size` (6x throttle) trên máy mac của tôi. Kết quả là: Thời gian render nhanh hơn __`20.65x`__ lần và thời gian patch nhanh hơn __`1.39x`__, con số cũng ấn tượng nhỉ.   > Nếu bạn chưa biết `render` và `patch` nghĩa là gì thì [bài viết này](https://vuedose.tips/tips/measure-runtime-performance-in-vue-js-apps) có giải thích qua. Và đó là tip của hôm nay. See you next month =)) --- Bài viết được lược dịch từ [https://vuedose.tips](https://vuedose.tips/tips/use-web-workers-in-your-vuejs-component-for-max-performance), cảm ơn bạn đã đọc bài viết.