Vue virtual DOM understanding - in-depth and easy to understand

Preface

Part of the content of this article comes from Huo Chunyang's understanding of the book "Vue.js Design and Implementation". Interested friends can buy and read it by themselves. You can clearly feel the author's deep understanding and intention of Vue. It is rich in very comprehensive Vue knowledge points. I highly recommend it to everyone.

Virtual DOM understanding

Let me first draw a conclusion, which is very helpful for understanding this article: Vue is a reserved systemruntime + compile time The framework of the architecture. When compiling, users can provide HTML strings, which we compile intodata objects and then hand them over to the runtime for processing; at runtime, according to The provideddata object is rendered into the page. It doesn’t matter if you don’t quite understand it here. The following will help you understand it step by step.

What is virtual DOM

The one mentioned aboveData Object is actually the so-called virtual DOM. It is a data object with a JS tree structure. It is easy to understand To put it plainly, virtual DOM is a common JS object, and this JS object is a description of the real DOM. It doesn’t matter if you don’t understand it well. Take a look at the following example.

For example, we have an HTML string like this:

const html = `
	<div id="app">
    <span>Hello World</span>
  </div>
`

Assume that there is an application called Compiler as a compiler, and its role is to convert an HTML string into a tree-shaped data structure.

const obj = Compiler(html)
// obj 结果如下
obj = {
    
    
  tag: "div",
  props: {
    
    
    id: 'app'
  }
  children: [
    {
    
    tag: "span", children: "Hello World"}
  ]
}

The converted tree-shaped data structure obj can be regarded as a virtual DOM, in which tag is used to describe the tag name; props is an object used to describe tag attributes, events, etc.; children is used to describe the child nodes of the tag. That is, it can be an array, representing the child node, or it can be a string, representing the child node as a text node. Now we can feel more deeply that the virtual DOM is a description of the real DOM.

In fact, you can completely design the structure of the virtual DOM yourself, such as using tagName to describe the tag name.

Convert virtual DOM to real DOM

We mentioned above that Vue is a framework ofruntime + compile time architecture. Let’s understand it again. Our process above is performed at compile time, compiling HTML strings into JS data objects, which is virtual DOM. Then at runtime, we render the elements into the page based on this JS data object (virtual DOM), which is to convert it into a real DOM. So how is the virtual DOM converted into the real DOM? In fact, it is implemented through the renderer. The renderer is a very important role. Usually when we use Vue.js, we rely on the renderer to perform work. Let's briefly understand the renderer.

We can write a simple version of the renderer to render the virtual DOM into the page. For example, we have a virtual DOM like this:

  • tag: description tag, indicating rendering a <button> tag
  • props: describes properties or events, indicating that we add a click event to <button>
  • children: used to describe child nodes, here it means that the text in button is "button" --> <button>button</button>
const vnode = {
    
    
  tag: "button",
  props: {
    
    "onClick": () => alert("Hello World")},
  children: "按钮"
}

Next we need a renderer to convert the above virtual DOM into a real DOM:

function renderer (vnode, container){
    
    
  // 获取标签名, 并创建一个DOM元素
  const curEl = document.createElement(vnode.tag)

  // 遍历属性, 将事件或属性添加到DOM元素上
  for (const key in vnode.props) {
    
    
    // on开头说明是事件, 则为DOM元素添加事件
    if(/^on/.test(key)) 
      curEl.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
  }

  // 如果子节点为stirng类型, 说明是文本子节点
  if (typeof vnode.children === "string") 
    curEl.appendChild(document.createTextNode(vnode.children))
  // 如果子节点为数组类型, 递归调用渲染函数
  else if (Array.isArray(vnode.children)) 
    vnode.children.forEach(child => renderer(child, curEl));

  // 将元素添加到挂载点下
  container.appendChild(curEl)  
}

Next, we pass in the virtual DOM just now, mount it under the body, and run the code in the browser. We will get a button on the page. Click the button and a pop-up "Hello World" will appear.

renderer(vnode, document.body)

Of course, the actual renderer will be more complicated internally, here we just made a simple implementation. For a more detailed implementation, we can view the source code of Vue.

The nature of components

We have a preliminary understanding of virtual DOM and renderers, so what are components? What is the relationship between components and virtual DOM? How does the renderer render components?

In fact, in addition to describing the real DOM, virtual DOM can also be used to describe components. However, the component is not a real DOM element after all, so how should we describe it? Before talking about this problem, I I want to raise a question, what is the essence of a component? Essence: A component is an encapsulation of a set of DOM elements, and this set of DOM elements is what the component needs. Rendered content. If so, we can define a function to represent the component.

For example, we define vnode in the previous section into a component and represent it with a function; the return value of the function is the content to be rendered, which is the virtual DOM:

function MyComponent() {
    
    
  return {
    
    
    tag: "button",
    props: {
    
    "onClick": () => alert("Hello World")},
    children: "按钮"
  }
}

Now that we have figured out the essence of the component, we can use virtual DOM to describe the component like a tag, except that the tag attribute no longer stores the tag name, but the component function.

const vnode = {
    
    
  tag: MyComponent,
}

Of course, we also need to make the renderer support the component in order to render, so we need to make some modifications to the renderer function in the previous book as follows:

function renderer(vnode, container) {
    
    
  // 说明描述的是标签
  if (typeof vnode.tag === "string") mountElement(vnode, container)
  // 说明描述的时组件
  else if(typeof vnode.tag === "function") mountComponent(vnode, container)
}

Let’s first change the name of the renderer function above to mountElement so that the renderer can process tags, as follows:

function mountElement (vnode, container){
    
    
  // 获取标签名, 并创建一个DOM元素
  const curEl = document.createElement(vnode.tag)

  // 遍历属性, 将事件或属性添加到DOM元素上
  for (const key in vnode.props) {
    
    
    // on开头说明是事件, 则为DOM元素添加事件
    if(/^on/.test(key)) 
      curEl.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
  }

  // 如果子节点为stirng类型, 说明是文本子节点
  if (typeof vnode.children === "string") 
    curEl.appendChild(document.createTextNode(vnode.children))
  // 如果子节点为数组类型, 递归调用渲染函数
  else if (Array.isArray(vnode.children)) 
    vnode.children.forEach(child => renderer(child, curEl));

  // 将元素添加到挂载点下
  container.appendChild(curEl)  
}

Then implement a mountComponent function to let the renderer process the component:

function mountComponent (vnode, container) {
    
    
  // 获取到组件返回的虚拟DOM
  const subtree = vnode.tag()
  // 递归调用renderer渲染组件返回的虚拟DOM
  renderer(subtree, container)
}

Passing in vNode and body as the mount point and running it in the browser can also achieve the above effect.

renderer(vNode, document.body)

In this way, we describe the component through the virtual DOM and convert it into a real rendering into the page. But must the component be a function? Friends who have studied react must know that there are function components and class components in react. So we can also use a JS object to express components:

const MyComponent = {
    
    
  render() {
    
    
    return {
    
    
      tag: "button",
      props: {
    
    "onClick": () => alert("Hello World")},
      children: "按钮"
    }
  }
}

Of course, the renderer renderer and mountComponent functions need to be modified to support object components:

// 渲染器的修改
function renderer(vnode, container) {
    
    
  const type = vnode.tag 
  if (typeof type === "string") mountElement(vnode, container)
  else if (
    typeof type === 'function' ||
    Object.prototype.toString.call(type) === '[object Object]'
  )
    mountComponent(vnode, container)
}
// mountComponent函数的修改
function mountComponent (vnode, container) {
    
    
  const type = vnode.tag 
  let subtree
  // 获取到组件返回的虚拟DOM
  if (typeof type === "function") subtree = vnode.tag()
  else if (typeof type === "object") subtree = vnode.tag.render()

  // 递归调用renderer渲染组件返回的虚拟DOM
  renderer(subtree, container)
}

We only made a small modification to meet the need to use objects to express components. At this point, we have completed the operation of converting virtual DOM components into real DOM, and support function expression components and Object expression componentTwo ways.

How templates work

We knew earlier how virtual DOM is rendered into real DOM, so let’s discuss how templates work? This is also another very important role in Vue: Compiler. Let's recall again, mentioned at the beginning of the articleruntime + compile time. We already know that at runtime, virtual DOM is rendered into a real DOM through a renderer; and at compile time, HTML strings are compiled into virtual DOM.

So how do templates in Vue work? It is through the compiler. The compiler, like the renderer, is just a program. The function of the compiler is to compile the template into a rendering function. For the compiler, a template is an ordinary string. It will analyze the string and generate a rendering function with the same function (that is, the h function. If you don’t know the h function, you can go online to read the information and learn about it briefly. How to use it).

Let’s take a .vue file as an example. There is a Vue file as shown below:

<template>
  <div @click="handler">
    按钮
  </div>
</template>

<script>
export default {
    
    
  data() {
    
    /* ... */},
  methods: {
    
    
    handler: () => {
    
    /* ... */}
  }
}
</script>

The template tag contains the content of the template. The compiler will compile the template content into a rendering function and add it to the script tag. The above code is processed by the compiler, and the final code running in the browser is as follows:

<script>
export default {
    
    
  data() {
    
    /* ... */},
  methods: {
    
    
    handler: () => {
    
    /* ... */}
  },
  render() {
    
    
    return h('div', {
    
     onClick: handler }, '按钮')
  }
}
</script>

So whether we handwrite the rendering function ourselves or use a template, the final rendered content is generated through the rendering function, so the template can be regarded as a syntactic sugar for the handwritten rendering function.

The implementation of components depends on the renderer, and the compilation of templates depends on the compiler. The compiler will compile the template content into a rendering function. The return value of the rendering function is the virtual DOM. The renderer will render the real DOM based on the returned virtual DOM. This process is how templates work, and it is also the process of Vue rendering to the page.

Guess you like

Origin blog.csdn.net/m0_71485750/article/details/133813669