This picture explains the source code of vue3 clearly! ! !

2024.07.02

let's take advantage of this classmate's mind map as an opportunity to sort out the [Vue3 framework design principles] for everyone (after reading the design principles, you will gain more from reading the mind map~)

01: Introduction

Before understanding the design of the Vue3 framework, we need to do two things, and these two things are also the main content of today.

  1. We need to synchronize and clarify the concepts of some words, such as: declarative, imperative, runtime, compile time... These words will be frequently involved in the subsequent framework design.
  2. We need to understand some basic concepts about the front-end framework, the design principles of the framework, and the developer experience principles. This will help you solve some inherent doubts and unveil the mystery of Vue.

So are you ready?

let us start!

02: Imperative programming

For current front-end development, there are mainly two programming paradigms:

  1. Imperative Programming
  2. Declarative Programming

These two paradigms are generally discussed in relation to each other.

Imperative

So first let's talk about what imperative is.

Specific examples:

Zhang San's mother asked Zhang San to buy soy sauce.

So what did Zhang San do?

  1. Zhang San picked up the money
  2. open the door
  3. Go downstairs
  4. To the store
  5. Buy soy sauce with money
  6. Back Home

The above process describes in detail what Zhang San did at each step in the process of buying soy sauce. So this kind of method of describing the process in detail can be called imperative.

So what should we do if we put this method into specific code implementation?

Let's look at something like this:

Display "hello world" in the specified div

So if we want to accomplish something like this, how do we achieve it in an imperative way?

We know that the core of imperative is: focus on process.

Therefore, the above things can be implemented imperatively to obtain the following logic and code:

// 1. 获取到指定的 div
const divEle = document.querySelector('#app')
// 2. 为该 div 设置 innerHTML 为 hello world
divEle.innerHTML = 'hello world'
  • 1.
  • 2.
  • 3.
  • 4.

Although the code has only two steps, it clearly describes the process required to complete this task.

So if what we do becomes more complicated, the whole process will become more complicated.

for example:

Display the variable msg for the p tag of the child element of the specified div

If the above functions are completed imperatively, the following logic and code will be obtained:

// 1. 获取到第一层的 div
const divEle = document.querySelector('#app')
// 2. 获取到它的子 div
const subDivEle = divEle.querySelector('div')
// 3. 获取第三层的 p
const subPEle = subDivEle.querySelector('p')
// 4. 定义变量 msg
const msg = 'hello world'
// 5. 为该 p 元素设置 innerHTML 为 hello world
subPEle.innerHTML = msg
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

Through the above examples, I believe everyone can have a basic understanding of the concept of imperative.

Finally, let’s summarize, what is imperative?

Imperative programming is a programming paradigm that focuses on the process and describes the detailed logic and steps to complete a function.

03: Programming paradigm: declarative programming

After understanding the imperative style, let's look at declarative programming.

As for the declarative style, everyone is actually very familiar with it.

For example, the following code is a typical declarative code:

<div>{{ msg }}</div>
  • 1.

Does this code look familiar to you?

Yes, this is the very common double curly brace syntax in Vue. So when we write Vue template syntax, we are actually writing declarative programming.

So what exactly does declarative programming mean?

Let’s take the previous example as an example:

Zhang San's mother asked Zhang San to buy soy sauce.

So what did Zhang San do?

  1. Zhang San picked up the money
  2. open the door
  3. Go downstairs
  4. To the store
  5. Buy soy sauce with money
  6. Back Home

In this example, we say: what Zhang San does is imperative. Then what Zhang San's mother does is declarative.

In such an incident, Zhang San's mother just issued a statement. She didn't care how Zhang San bought the soy sauce, but only cared about the final result.

So, the so-called declarative style refers to a paradigm that does not focus on the process but only on the results.

Similarly, if we express it through code, the following example:

Display the variable msg for the p tag of the child element of the specified div

The following code will be obtained:

<div id="app">
  <div>
    <p>{{ msg }}</p>
  </div>
</div>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

In such code, we don’t care how msg is rendered into the p tag. All we care about is rendering the specified text in the p tag.

Finally, let’s summarize, what is declarative?

Declarative programming is a programming paradigm that focuses on results. It does not care about the detailed logic and steps to complete a function. (Note: This does not mean that declarative programming does not require a process! Declarative programming just hides the process!)

04: Imperative vs. Declarative

After we finish explaining the imperative and declarative programming styles, many students will definitely compare these two programming paradigms.

Is imperative or declarative better?

So if we want to figure out this problem, we first need to figure out what are the criteria for evaluating whether a programming paradigm is good or bad?

Usually, we evaluate a programming paradigm from two aspects:

  1. performance
  2. Maintainability

Then we will analyze the imperative and declarative styles from these two aspects.

performance

Performance has always been a focus of our attention when developing projects. So how do we usually express the performance of a function?

Let’s look at an example:

Set the text of the specified div to "hello world"

So for this requirement, the simplest code is:

div.innerText = "hello world" // 耗时为:1
  • 1.

You won't find a simpler code implementation than this.

Then we compare the time taken for this operation to: 1. (PS: The less time taken, the better the performance)

Then let's look at the declarative code. The declarative code is:

<div>{{ msg }}</div>  <!-- 耗时为:1 + n -->
<!-- 将 msg 修改为 hello world -->
  • 1.
  • 2.

So: It is known that the easiest way to modify text is innerText, so no matter how the declarative code implements text switching, its time must be > 1. We compare it to 1 + n (comparative performance consumption).

Therefore, from the above examples, we can see that: Imperative performance > Declarative performance

Maintainability

Maintainability represents many dimensions, but generally speaking, maintainability means that the code can be easily read, modified, deleted, and added.

To achieve this goal, the simple answer is: the logic of the code must be simple enough for people to understand at a glance.

Now that we have clarified this concept, let's look at the code logic of the imperative and declarative styles in the same business segment:

// 命令式
// 1. 获取到第一层的 div
const divEle = document.querySelector('#app')
// 2. 获取到它的子 div
const subDivEle = divEle.querySelector('div')
// 3. 获取第三层的 p
const subPEle = subDivEle.querySelector('p')
// 4. 定义变量 msg
const msg = 'hello world'
// 5. 为该 p 元素设置 innerHTML 为 hello world
subPEle.innerHTML = msg
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
// 声明式
<div id="app">
  <div>
    <p>{{ msg }}</p>
  </div>
</div>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

For the above code, the declarative code is obviously easier to read, so it is also easier to maintain.

Therefore, from the above examples, we can see that: **Imperative maintainability < declarative maintainability**

Summary

From the above analysis, we can know two points:

  1. Imperative performance > Declarative performance
  2. Imperative maintainability < declarative maintainability

So both sides have their advantages and disadvantages, which paradigm should we use in daily development?

To understand this, we need to understand more knowledge.

05: Development and design principles for enterprise applications

The design principles of enterprise applications are complicated to describe. Why?

Because for different types of enterprises (large companies, small and medium-sized companies, personnel outsourcing, project outsourcing) and different project types (front-end, middle-end, back-end), the corresponding enterprise application design principles may have some differences.

Therefore, the description we make here will discard some subtle differences and only grasp the core points for explanation.

Regardless of the type of enterprise, or the type of project they are developing, the most important points are nothing more than two:

  1. Project Cost
  2. Development Experience

Project Cost

Project cost is very easy to understand. It determines the price a company pays to complete "this thing", and thus directly determines whether the project is profitable (except for the money-burning projects of large companies).

Since project cost is so important, you can think about what determines the project cost?

That’s right! Your development cycle.

The longer the development cycle, the higher the personnel cost will be, which will lead to higher project costs.

From our previous analysis, we can see that the declarative development paradigm is greater than the imperative one in terms of maintainability.

Maintainability determines to a certain extent that it will shorten the project's development cycle and make upgrades easier, thereby saving a lot of development costs.

So this is why Vue is becoming more and more popular.

Development Experience

The core factor that determines the developer's development experience is mainly the difficulty during development and reading, which is called: mental burden.

Mental burden can be used as a criterion to measure the difficulty of development. A high mental burden indicates that the development is more difficult, while a low mental burden indicates that the development is less difficult and the development is more comfortable.

So according to what we said before, the difficulty of declarative development is obviously lower than that of imperative development.

Therefore, in terms of development experience, the declarative development experience is better, which means that the mental burden is lower.

06: Why is it said that the framework design process is actually a process of constant trade-offs?

Vue author You Yuxi said in a speech: The design process of the framework is actually a process of constant trade-offs.

What does this mean?

To understand this, let's clarify the concepts we mentioned before:

  1. Imperative performance > Declarative performance
  2. Imperative maintainability < declarative maintainability
  3. Declarative frameworks are essentially implemented by imperative code
  4. When developing enterprise projects, most people use declarative frameworks.

After we have clarified this issue, let's think about the next question: What are the development and design principles of the framework?

We know that for Vue, when we use it, we use it in a declarative way, but for Vue internally, it is implemented in an imperative way.

So we can understand that Vue encapsulates imperative logic and exposes a declarative interface to the outside world.

Well, in this case, we know that the performance of imperative style > the performance of declarative style. So why does Vue choose the declarative solution?

In fact, the reason is very simple, that is because: imperative maintainability < declarative maintainability.

Display the variable msg for the p tag of the child element of the specified div

Take this example.

For developers, there is no need to focus on the implementation process, only the final result.

As for Vue, all it needs to do is to encapsulate imperative logic while minimizing performance loss! It needs to find a balance between performance and maintainability, so as to find a point with better maintainability and relatively better performance.

Therefore, for Vue, its design principle is: to reduce performance loss as much as possible while ensuring maintainability.

So back to our title: Why is the framework design process actually a process of constant trade-offs?

The answer is obvious, because:

We need to find a balance between maintainability and performance. On the basis of ensuring maintainability, we should minimize the performance loss as much as possible.

Therefore, the design process of the framework is actually a process of constantly making trade-offs between maintainability and performance.

07: What is runtime?

In the source code of Vue 3, there is a folder called runtime-core, which contains the core code logic of the runtime.

Runtime-core exposes a function called render function.

We can use render instead of template to complete DOM rendering:

Some students may not understand what the current code means. It doesn’t matter. It’s not important. We will explain it in detail later.

<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script src="https://unpkg.com/vue@3.2.36/dist/vue.global.js"></script>
</head>

<body>
  <div id="app"></div>
</body>

<script>
  const { render, h } = Vue
  // 生成 VNode
  const vnode = h('div', {
    class: 'test'
  }, 'hello render')

  // 承载的容器
  const container = document.querySelector('#app')

  // 渲染函数
  render(vnode, container)
</script>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

We know that in Vue projects, we can render DOM nodes through tempalte as follows:

<template>
	<div>hello render</div>
</template>
  • 1.
  • 2.
  • 3.

But for the render example, we did not use template, but passed a function called render, which returned something unknown. Why can we render the DOM?

With this question, let's look at:

We know that in the above code, there is a core function: the rendering function render, so what exactly does this render do here?

Let's take a look at this through a code example:

Suppose one day your leader says to you:

I hope based on the following data:

Renders a div like this:

{
 type: 'div',
 props: {
  class: test
 },
 children: 'hello render'
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
<div>hello render</div>
  • 1.

So how would you implement such a requirement? You can think about it here first, try to implement it, and then we will continue to read on..........

Then we implement the following code according to this requirement:

<script>
  const VNode = {
    type: 'div',
    props: {
      class: 'test'
    },
    children: 'hello render'
  }
  // 创建 render 渲染函数
  function render(vnode) {
    // 根据 type 生成 element
    const ele = document.createElement(vnode.type)
    // 把 props 中的 class 赋值给 ele 的 className
    ele.className = vnode.props.class
    // 把 children 赋值给 ele 的 innerText
    ele.innerText = vnode.children
    // 把 ele 作为子节点插入 body 中
    document.body.appendChild(ele)
  }

  render(VNode)
</script>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

In such a code, we successfully rendered the corresponding DOM through a render function. Similar to the previous render example, they both rendered a vnode. Do you think such code is really great?

But after using your render for a while, your boss said: It’s too troublesome to write like this every day. I have to write a complicated vnode every time. Can you let me directly write the HTML tag structure and you do the rendering?

After thinking about it, you say: If that's the case, then the above runtime code can't solve it!

That's right! The "framework" we just wrote is the runtime code framework.

Finally, let's make a summary: at runtime, you can use render to render vnode into a real dom node.

08: What is compile time?

Just now, we made it clear that if we only rely on runtime, there is no way to perform rendering and parsing through the HTML tag structure.

So to achieve this, we need to use another thing, that is, compile time.

The compiler in Vue is more accurately called the compiler. Its code mainly exists in the compiler-core module.

Let's look at the following code:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script src="https://unpkg.com/vue@3.2.36/dist/vue.global.js"></script>
</head>

<body>
  <div id="app"></div>
</body>

<script>
  const { compile, createApp } = Vue

  // 创建一个 html 结构
  const html = `
    <div>hello compiler</div>
  `
  // 利用 compile 函数,生成 render 函数
  const renderFn = compile(html)

  // 创建实例
  const app = createApp({
    // 利用 render 函数进行渲染
    render: renderFn
  })
  // 挂载
  app.mount('#app')
</script>

</html>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.

For the compiler, its main function is to compile the HTML in the template into a render function, and then use the runtime to mount the corresponding DOM through render.

Finally, let's make a summary: when compiling, you can compile the html node into a render function

09: Runtime + Compile time

In the previous two sections, we have learned about runtime and compile time respectively. At the same time, we also know that vue is a runtime + compile time framework!

Vue parses the HTML template through the compiler, generates a render function, and then parses the render through the runtime to mount the real DOM.

Some students may be confused when seeing this. Since the compiler can directly parse the HTML template, why do we need to generate a render function and then render it? Why not just use the compiler for rendering?

That is: Why is Vue designed as a runtime + compile-time framework?

So to sort out this problem, we need to know how DOM rendering is done.

For DOM rendering, it can be divided into two parts:

  1. The first rendering, we can call it mounting
  2. Update rendering, we can call it patching

First Render

So what is the first render?

When the innerHTML of the initial div is empty,

<div id="app"></div>
  • 1.

We render the following nodes in this div:

<ul>
 <li>1</li>
 <li>2</li>
 <li>3</li>
</ul>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

Then such a rendering is the initial rendering. In such a rendering, we will generate a ul tag and three li tags at the same time, and mount them into div.

Update Rendering

Then if the content of the ul tag changes at this time:

<ul>
 <li>3</li>
 <li>1</li>
 <li>2</li>
</ul>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

li - 3 has risen to the first place, so now you can think about: how do we expect the browser to update this rendering?

There are two ways for the browser to update the rendering:

  1. Delete all existing nodes and re-render new nodes
  2. Delete li - 3 at the original position and insert li - 3 at the new position

So which of these two methods do you think is better? Let's analyze it:

  1. First of all, for the first method: its advantage is that it does not require any comparison, and only needs to execute 6 times (3 deletions and 3 re-renderings) of DOM processing.
  2. As for the second method, it is relatively complicated in terms of logic. It needs to be done in two steps:
  1. Compare the differences between the old node and the new node
  2. According to the difference, delete an old node and add a new node

Based on the above analysis, we know that:

  1. The first method: involves more DOM operations
  2. The second method: involves js calculation + a small amount of dom operation

So which of these two methods is faster? Let's experiment:

const length = 10000
  // 增加一万个dom节点,耗时 3.992919921875 ms
  console.time('element')
  for (let i = 0; i < length; i++) {
    const newEle = document.createElement('div')
    document.body.appendChild(newEle)
  }
  console.timeEnd('element')

  // 增加一万个 js 对象,耗时 0.402099609375 ms
  console.time('js')
  const divList = []
  for (let i = 0; i < length; i++) {
    const newEle = {
      type: 'div'
    }
    divList.push(newEle)
  }
  console.timeEnd('js')
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

From the results, we can see that DOM operations take much more time than JS operations, that is, DOM operations are more performance-intensive than JS operations.

So based on this conclusion, let’s go back to the scenario we just talked about:

  1. First of all, for the first method: its advantage is that it does not require any comparison, and only needs to perform 6 times (3 deletions and 3 re-renderings) of DOM processing.
  2. As for the second method, it is relatively complicated in terms of logic. It needs to be done in two steps:

Compare the differences between the old node and the new node

According to the difference, delete an old node and add a new node

According to the conclusion, method 1 consumes more performance than method 2 (i.e., the performance is worse).

After reaching this conclusion, let's go back to the original question: Why is Vue designed as a runtime + compile-time framework?

answer:

  1. For pure runtime: since there is no compiler, we can only provide a complex JS object.
  2. For pure compile time: because of the lack of runtime, it can only analyze the difference operations at compile time. Also, because the runtime is omitted, the speed may be faster. However, this method will lose flexibility (for details, see Chapter 6 Virtual DOM, or click here to view the official example). For example, svelte is a pure compile-time framework, but its actual running speed may not reach the theoretical speed.
  3. Runtime + compile time: For example, vue or react are built in this way, so that they can optimize performance as much as possible while maintaining flexibility, thus achieving a balance.

10: What are the side effects?

In the source code of Vue, there is a concept that is heavily involved, that is, side effects.

So we need to first understand what side effects mean.

Side effects refer to the series of consequences that occur when we perform setter or getter operations on data.

So what does it mean specifically? Let's talk about it separately:

sets

Setter represents the assignment operation. For example, when we execute the following code:

msg = '你好,世界'
  • 1.

At this time, msg triggers a setter behavior.

So if msg is a responsive data, then such a data change will affect the corresponding view change.

Then we can say that the setter behavior of msg triggers a side effect, causing the view to change accordingly.

getter

Getter represents the value operation. For example, when we execute the following code:

element.innerText = msg
  • 1.

At this time, a getter operation is triggered for the variable msg. Such a value-taking operation will also cause the innerText of the element to change.

So we can say: the getter behavior of msg triggers a side effect, causing the innterText of element to change.

Will there be multiple side effects?

Now that we have clarified the basic concept of side effects, let’s think about this: Are there likely to be multiple side effects?

The answer is: Yes.

Let's take a simple example:

<template>
  <div>
    <p>姓名:{{ obj.name }}</p>
    <p>年龄:{{ obj.age }}</p>
  </div>
</template>

<script>
 const obj = ref({
    name: '张三',
    age: 30
  })
  obj.value = {
    name: '李四',
    age: 18
  }
</script>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

In such a code, obj.value triggers a setter behavior, but causes the content of two p tags to change, which means that two side effects occur.

A little section

According to this section, we know:

  1. Side effects refer to a series of consequences caused by setter or getter operations on data.
  2. There may be multiple side effects.

11: Vue 3 framework design overview

According to the previous study, we already know:

  1. What is declarative
  2. What is imperative
  3. What is a runtime
  4. What is compile time
  5. What is runtime + compile time
  6. At the same time, we also know that the design process of the framework itself is a process of constant trade-offs.

After understanding these contents, here is a basic framework design of Vue3:

For vue3, the core can be roughly divided into three modules:

  1. Responsiveness: reactivity
  2. Runtime: runtime
  3. Compiler: compiler

We use the following basic structure to describe the basic relationship between the three:

<template>
	<div>{{ proxyTarget.name }}</div>
</template>

<script>
import { reactive } from 'vue'
export default {
	setup() {
		const target = {
			name: '张三'
		}
		const proxyTarget = reactive(target)
		return {
			proxyTarget
		}
	}
}
</script>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

In the above code:

  1. First, we declare a responsive data using the reactive method.

This method is a method exposed by the reactivity module

It can receive a complex data type as the proxy object (target) of the Proxy (many students may not know what a proxy is now, it doesn’t matter, we will introduce it in detail later, just need to have an impression now)

Returns a proxy object of type Proxy (proxyTarget)

When proxyTarget triggers setter or getter behavior, corresponding side effects will occur

  1. Then, we write a div in the tempalte tag. We know that the HTML written here is not real HTML, we can call it a template, and the content of the template will be compiled by the compiler to generate a render function.
  2. Finally, Vue will use the runtime to execute the render function to render the real DOM.

The above is the operational relationship between reactivity, runtime, and compiler.