Hôm nay mình xin giới thiệu một bài viết mới.

Mình đã dành cả buổi mày mò làm một thử một cái thư viện render giống như React hay Vue. Có mấy lần phỏng vấn mình hay hỏi những ứng viên, Virtual DOM là gì? Virtual DOM nằm ở đâu? Làm sao để tương tác với Virtual DOM. Vậy nên hôm nay tự làm một cái thư viện để có bị hỏi ngược lại thì có biết đường mà chém.



Virtual DOM là gì?


Virtual DOM là một object trừu tượng được đồng bộ với DOM thực tế. Đó là một cấu trúc dữ liệu giống như cây biểu thị trạng thái của trang web hoặc ứng dụng. Virtual DOM được tạo và quản lý bởi thư viện hoặc framework JavaScript, và nó được sử dụng để tối ưu quá trình cập nhật DOM thực tế.


Khi trạng thái của trang web hoặc ứng dụng thay đổi, Virtual DOM được cập nhật để phản ánh những thay đổi đó. Sau đó, Virtual DOM được so sánh với phiên bản trước đó của Virtual DOM để xác định số lượng tối thiểu các thay đổi cần thiết để cập nhật DOM thực tế. Những thay đổi này sau đó được áp dụng cho DOM thực tế, dẫn đến quá trình cập nhật hiệu quả và mượt hơn.


Ok let's go!


Xác định cấu trúc của Virtual DOM.

Virtual DOM thường được biểu thị dưới dạng một cấu trúc dữ liệu giống như cây, với các node đại diện cho các phần tử và node. Mỗi node nên có thuộc tính:

  • tagName cho biết loại node (ví dụ: "div", "span", "text").
  • attrs là những thuộc tính của html tag như là style, class, src, href ...
  • children là các node con hay ứng với những thẻ bên trong của nó, chính có children thì cấu trúc này mới có thể mô tả DOM tree một cách hoàn thiện.


export interface VNode {
  tagName: string;
  attrs: Record<string, unknown>;
  children: (VNode | string)[];
};


Function khởi tạo Element Node

function createElement(
  tagName: string, 
  {
    attrs = {},
    children = []
  } : { attrs?: Record<string, unknown>; children?: (VElement | string)[]; } = {}): VElement {
   return {
     tagName,
     attrs,
     children,
  };
};

Trong ví dụ này, hàm createElement nhận các tham số type, attrs, và children. tagName là loại phần tử (ví dụ: "div", "span", "h1" vv), props là một đối tượng chứa các thuộc tính của phần tử (ví dụ: id, class, style), và ...children là một danh sách các phần tử con. Hàm này trả về một đối tượng đại diện cho phần tử, với tagattrs tương ứng.


Nếu bạn làm ReactJs đời đầu chắc không lạ với function này đâu nhỉ. như React.createElement cho phép bạn viết React mà không cần dùng JSX. Nhớ lời tôi "bạn có thể viết React mà không cần dùng JSX, JSX có sau và làm việc code React trực quan hơn".


Tiếp theo là render function

function render(vNode: VElement){
  const { tagName, attrs, children } = vNode;
  let element = document.createElement(tagName);
  // insert all children elements
  children.forEach( child =>  {
  	 if (typeof child === 'string'){
  	 // if the children is a kind of string create a text Node object
  	   element.appendChild(document.createTextNode(child));
  	 } else {
  	 // repeat the process with the children elements
  	   element.appendChild(render(child));
  	 }
  });
  // if it has attributes it adds them to the elementif (Object.keys(attrs).length){
  	for (const [key, value] of Object.entries(attrs)) {
  	  element.setAttribute(key, value as string);
  	}
  }
  	
  return element;
};

Hàm render là quá trình chuyển từ Object sang các thẻ tag Html, Hàm này tạo ra một phần tử DOM thực là một biểu diễn trực quan của phần tử DOM ảo và có thể được chèn vào DOM thực để cập nhật giao diện người dùng.


  • Các thuộc tính tagName, attrs và children của vNode được giải nén để dễ dàng làm việc với chúng.
  • Một phần tử DOM thực mới được tạo ra bằng cách sử dụng document.createElement, với thuộc tính tagName của vNode.
  • Nếu child là một chuỗi, một nút văn bản mới được tạo bằng cách sử dụng document.createTextNode() và được thêm vào element bằng cách sử dụng element.appendChild().
  • Nếu child không phải là một chuỗi, hàm render được gọi đệ quy với child là đối số, và phần tử DOM thực kết quả được thêm vào element bằng cách sử dụng element.appendChild().
  • Nếu đối tượng attrs có bất kỳ thuộc tính nào, chúng được lặp qua bằng cách sử dụng Object.entries(), và mỗi thuộc tính được thêm vào element bằng cách sử dụng element.setAttribute().


Cuối cùng thì mình cần xuất nó lên browser.


Mount

function mountElement(element: Element, domElement: Element | null): Element | null {
  if (!domElement) return null;
  domElement.replaceWith(element);
  return element;
}

Mỗi lần được gọi mountElement() thì phần tử trên browser sẽ được thay thế bằng một phần tử html mới.


Vậy là đầu đủ những hàm cần thiết và bay giờ làm thử một cái app render xem sao


const createVApp = (count: number) =>  createElement('div', {
  attrs: {
    id: 'app',
  },
  children: [
    'The current count is: ' + count,
    createElement('img', {
	  attrs: {
        src: 'https://media.giphy.com/media/cuPm4p4pClZVC/giphy.gif',
	  },
    }),
  ],
});

let count = 0;
const $app = render(createVApp(count));
let $rootEl = mountElement($app, document.getElementById('app'));

setInterval(() => {
  count++;
  const updatedVApp = render(createVApp(count));
  $rootEl = mountElement(updatedVApp, $rootEl);
}, 10000);


Tổng kết

Trên đây là cách xây dựng một thư viện javascript hỗ trợ render với Virtual DOM, rất đơn giản mà giúp mình hiểu thêm vế cách mấy cái bên dưới hoạt động thế nào.

Phần code mình viết còn khá thô sơ và có thể nhìn thấy khá nhiều điểm còn cần cả thiện, như hiện tại đang bị re-render lại mỗi lần update, mà để TODO để update tiếp nhé.


Source

Tham khảo https://dev.to/ycmjason/building-a-simple-virtual-dom-from-scratch-3d05