[Vue3] Source Code Analysis

first thing to know

Proxy

The Proxy object corresponding to the Proxy API is a native object introduced in ES6, which is used to define custom behaviors of basic operations (such as attribute lookup, assignment, enumeration, function call, etc.). Literally understood, the Proxy object is a proxy of the target object, and any operation on the target object (instantiation, adding/deleting/modifying attributes, etc.) must pass through the proxy. Therefore, we can intercept, filter or modify all operations from the outside world. Based on these characteristics of Proxy, it is often used in:

  • Create a "responsive" object, such as the reactive method in Vue3.0.
  • Creates a JavaScript "sandbox" that can be isolated.

The basic syntax of Proxy is shown in the following code:

const p = new Proxy(target, handler)

Among them, the target parameter indicates the target object to be wrapped by Proxy (it can be any type of object, including native arrays, functions, or even another proxy), the handler parameter indicates the object with functions as attributes, and the functions in each attribute define Proxy p's behavior when performing various operations. Common usage methods are shown in the following code:

let foo = {
    
    
 a: 1,
 b: 2
}
let handler = {
    
    
    get:(obj,key)=>{
    
    
        console.log('get')
        return key in obj ? obj[key] : undefined
    }
}
let p = new Proxy(foo,handler)
console.log(p.a) // 打印1

In the above code, p is the proxy object of foo, and the related operations on the p object will be synchronized to the foo object. At the same time, Proxy also provides another method of generating proxy objects, Proxy.revocable(), as shown in the following code:

const {
    
     proxy,revoke } = Proxy.revocable(target, handler)

The return value of this method is an object whose structure is: {"proxy": proxy, "revoke": revoke}, where: proxy represents the newly generated proxy object itself, and is created in the general way new Proxy(target, handler) The proxy object of is no different, except that it can be revoked. revoke means the revoke method, and the proxy object generated with it can be revoked without adding any parameters when calling, as shown in the following code:

let foo = {
    
    
 a: 1,
 b: 2
}
let handler = {
    
    
    get:(obj,key)=>{
    
    
        console.log('get')
        return key in obj ? obj[key] : undefined
    }
}
let {
    
     proxy,revoke } = Proxy.revocable(foo,handler)
console.log(proxy.a) // 打印1
revoke()
console.log(proxy.a) // 报错信息:Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked

It should be noted that once a proxy object is revoked, it will become almost completely incallable, and any proxyable operation performed on it will throw a TypeError exception. In the above code, we only use the handler of the get operation, that is, we will enter this method when we try to obtain a certain property of the object. In addition, there are nearly 14 handlers in Proxy, which can also be called hooks. They are:

handler.getPrototypeOf()
在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。

handler.setPrototypeOf()
在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时。

handler.isExtensible()
在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时。

handler.preventExtensions()
在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时。

handler.getOwnPropertyDescriptor()
在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时。

handler.defineProperty()
在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {
    
    }) 时。

handler.has()
在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy 时。

handler.get()
在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。

handler.set()
在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。

handler.deleteProperty()
在删除代理对象的某个属性时触发该操作,即使用 delete 运算符,比如在执行 delete proxy.foo 时。

handler.ownKeys()
当执行Object.getOwnPropertyNames(proxy) 和Object.getOwnPropertySymbols(proxy)时触发。

handler.apply()
当代理对象是一个function函数时,调用apply()方法时触发,比如proxy.apply()

handler.construct()
当代理对象是一个function函数时,通过new关键字实例化时触发,比如new proxy()

Combining these handlers, we can implement some restrictive operations on objects, for example: It is forbidden to delete and modify a certain attribute of an object, as shown in the following code:

let foo = {
    
    
    a:1,
    b:2
}
let handler = {
    
    
    set:(obj,key,value,receiver)=>{
    
    
        console.log('set')
        if (key == 'a') throw new Error('can not change property:'+key)
        obj[key] = value
        return true
    },
    deleteProperty:(obj,key)=>{
    
    
        console.log('delete')
        if (key == 'a') throw new Error('can not delete property:'+key)
        delete obj[key]
        return true
    }
}

let p = new Proxy(foo,handler)
// 尝试修改属性a
p.a = 3 // 报错信息:Uncaught Error
// 尝试删除属性a
delete p.a  // 报错信息:Uncaught Error

In the above code, the set method has an additional receiver parameter, which is usually the Proxy itself, namely p. The scenario is that when a piece of code executes obj.name="jen", obj is not a proxy and does not contain a name attribute itself, but it There is a proxy on the prototype chain of the proxy, then the set method in the handler of that proxy will be called, and at this time obj will be passed in as the parameter receiver. Verify the modification of the attribute, as shown in the following code:

let foo = {
    
    
    a:1,
    b:2
}
let handler = {
    
    
    set:(obj,key,value)=>{
    
    
        console.log('set')
        if (typeof(value) !== 'number') throw new Error('can not change property:'+key)
        obj[key] = value
        return true
    }
}
let p = new Proxy(foo,handler)
p.a = 'hello' // 报错信息:Uncaught Error

Proxy can also monitor array changes, as shown in the following code:

let arr = [1]
let handler = {
    
    
    set:(obj,key,value)=>{
    
    
        console.log('set') // 打印set
        return Reflect.set(obj, key, value);
    }
}

let p = new Proxy(arr,handler)
p.push(2) // 改变数组

Reflect.set() is used to modify the value of the array and return a Boolean type, which is also compatible with modifying the scene corresponding to the method on the prototype of the array, which is equivalent to obj[key] = value.

Reflect

ES6 Reflect

Symbol

ES6 Symbol

Maps and Sets

ES6 Set and Map data structures

diff algorithm

In the process of traversing the child vnodes during the vue update process, different patch methods will be used to patch the new and old vnodes. If the corresponding newVnode and oldVnode are found, the real dom nodes inside can be reused. Avoid the performance overhead caused by repeatedly creating elements. After all, the browser creates and manipulates the real dom, and the performance cost is expensive.

patchChildren

From the above, we know that there are vnode types of children, so if there are children, it is necessary to patch each
child vnode and traverse downwards in turn. Then a patchChildren method is needed to patch the subclass vnode in turn.

In vue3.0, there is such a piece of source code in the patchChildren method

if (patchFlag > 0) {
    
    
      if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
    
     
         /* 对于存在key的情况用于diff算法 */
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        return
      } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
    
    
         /* 对于不存在key的情况,直接patch  */
        patchUnkeyedChildren( 
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        return
      }
    }

patchChildren performs real diff or direct patch according to whether the key exists.

Since the diff algorithm exists in the patchChildren method, and the patchChildren method is used in the vnode of Fragment type and element type, this also explains the scope of the diff algorithm.

What exactly does the diff algorithm do (emphasis)?

Before the official diff algorithm, in the process of patchChildren, there is patchKeyedChildren
patchUnkeyedChildren

patchKeyedChildren is the formal process of opening diff, so what is the function of patchUnkeyedChildren? Let's see what patchUnkeyedChildren will do if there is no key.


 c1 = c1 || EMPTY_ARR
    c2 = c2 || EMPTY_ARR
    const oldLength = c1.length
    const newLength = c2.length
    const commonLength = Math.min(oldLength, newLength)
    let i
    for (i = 0; i < commonLength; i++) {
    
     /* 依次遍历新老vnode进行patch */
      const nextChild = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      patch(
        c1[i],
        nextChild,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
    if (oldLength > newLength) {
    
     /* 老vnode 数量大于新的vnode,删除多余的节点 */
      unmountChildren(c1, parentComponent, parentSuspense, true, commonLength)
    } else {
    
     /* /* 老vnode 数量小于于新的vnode,创造新的即诶安 */
      mountChildren(
        c2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized,
        commonLength
      )
    }


We can conclude that for the absence of key

① Compare the length of the old and new children to obtain the minimum value, and then do a new patch for the common part.
② If the number of old nodes is greater than the number of new nodes, remove the extra nodes.
③ If the number of new nodes is greater than the number of old nodes, add new nodes from the new mountChildren.

So what about the existence of a key? The diff algorithm will be used, what does the diff algorithm do?

What exactly does the patchKeyedChildren method do?

Let's first look at some declared variables.

    /*  c1 老的vnode c2 新的vnode  */
    let i = 0              /* 记录索引 */
    const l2 = c2.length   /* 新vnode的数量 */
    let e1 = c1.length - 1 /* 老vnode 最后一个节点的索引 */
    let e2 = l2 - 1        /* 新节点最后一个节点的索引 */


①The first step is to search for (ab) c
(ab) de from the beginning to the end

 /* 从头对比找到有相同的节点 patch ,发现不同,立即跳出*/
    while (i <= e1 && i <= e2) {
    
    
      const n1 = c1[i]
      const n2 = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
        /* 判断key ,type是否相等 */
      if (isSameVNodeType(n1, n2)) {
    
    
        patch(
          n1,
          n2,
          container, 
          parentAnchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      } else {
    
    
        break
      }
      i++
    }

The first step is to find the same vnode from the beginning, and then perform a patch. If it is not the same node, then jump out of the loop immediately.

The specific process is shown in the figure
insert image description here

isSameVNodeType

export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
    
    
  return n1.type === n2.type && n1.key === n2.key
}

②The second step is the same as the previous diff
a (bc)
de (bc) from the end

 /* 如果第一步没有patch完,立即,从后往前开始patch ,如果发现不同立即跳出循环 */
    while (i <= e1 && i <= e2) {
    
    
      const n1 = c1[e1]
      const n2 = (c2[e2] = optimized
        ? cloneIfMounted(c2[e2] as VNode)
        : normalizeVNode(c2[e2]))
      if (isSameVNodeType(n1, n2)) {
    
    
        patch(
          n1,
          n2,
          container,
          parentAnchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      } else {
    
    
        break
      }
      e1--
      e2--
    }

After the first step, if it is found that the patch has not been completed, then immediately proceed to the second step, starting from the tail and traversing forward diff.

If it is found that the nodes are not the same, then jump out of the loop immediately.

The specific process is shown in the figure
insert image description here
③④ mainly for the situation of adding and deleting elements, the premise is that the elements do not move, if there are elements moving, the logic of ⑤ will follow.

③ If the old nodes are all patched and the new nodes are not patched, create a new vnode
(ab)
(ab) c
i = 2, e1 = 1, e2 = 2
(ab)
c (ab)
i = 0, e1 = -1, e2 = 0

/* 如果新的节点大于老的节点数 ,对于剩下的节点全部以新的vnode处理( 这种情况说明已经patch完相同的vnode  ) */
    if (i > e1) {
    
    
      if (i <= e2) {
    
    
        const nextPos = e2 + 1
        const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
        while (i <= e2) {
    
    
          patch( /* 创建新的节点*/
            null,
            (c2[i] = optimized
              ? cloneIfMounted(c2[i] as VNode)
              : normalizeVNode(c2[i])),
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG
          )
          i++
        }
      }
    }


i > e1

If the number of new nodes is greater than the number of old nodes, all remaining nodes will be processed with new vnodes (in this case, the same vnode has been patched), that is, all new vnodes must be created.

The specific logic is shown in the figure
insert image description here
④ If all new nodes are patched and old nodes remain, then uninstall all old nodes
i > e2
(ab) c
(ab)
i = 2, e1 = 2, e2 = 1
a (bc)
( bc)
i = 0, e1 = 0, e2 = -1

else if (i > e2) {
    
    
   while (i <= e1) {
    
    
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
   }
}

For the case where the old node is larger than the new node, uninstall all the excess nodes (in this case, it means that the same vnode has been patched)

The specific logic is shown in the figure
insert image description here
⑤ Uncertain elements (this situation indicates that the same vnode has not been patched), we can follow the logic of ①② and continue to look at the
diff core

In the case of ①②, the nodes that have not been traversed are shown in the figure below.
insert image description here
remaining nodes.
insert image description here

      const s1 = i  //第一步遍历到的index
      const s2 = i 
      const keyToNewIndexMap: Map<string | number, number> = new Map()
      /* 把没有比较过的新的vnode节点,通过map保存 */
      for (i = s2; i <= e2; i++) {
    
    
        if (nextChild.key != null) {
    
    
          keyToNewIndexMap.set(nextChild.key, i)
        }
      }
      let j
      let patched = 0 
      const toBePatched = e2 - s2 + 1 /* 没有经过 path 新的节点的数量 */
      let moved = false /* 证明是否 */
      let maxNewIndexSoFar = 0 
      const newIndexToOldIndexMap = new Array(toBePatched)
       for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
      /* 建立一个数组,每个子元素都是0 [ 0, 0, 0, 0, 0, 0, ] */ 

Traverse all new nodes and store the index and corresponding key in the map keyToNewIndexMap

keyToNewIndexMap stores key -> index map

D : 2
E : 3
C : 4
I : 5

Next, a new pointer j is declared to record the index of the remaining new node.
patched, record the number of new nodes that have been patched in step 5
toBePatched record the number of new nodes that have not been patched before step 5.
moved represents whether it has been moved, and our demo has already moved.

newIndexToOldIndexMap is used to store the array of new node index and old node index.
The index of the newIndexToOldIndexMap array is the index of the new vnode, and the value is the index of the old vnode.

next

 for (i = s1; i <= e1; i++) {
    
     /* 开始遍历老节点 */
        const prevChild = c1[i]
        if (patched >= toBePatched) {
    
     /* 已经patch数量大于等于, */
          /* ① 如果 toBePatched新的节点数量为0 ,那么统一卸载老的节点 */
          unmount(prevChild, parentComponent, parentSuspense, true)
          continue
        }
        let newIndex
         /* ② 如果,老节点的key存在 ,通过key找到对应的index */
        if (prevChild.key != null) {
    
    
          newIndex = keyToNewIndexMap.get(prevChild.key)
        } else {
    
     /*  ③ 如果,老节点的key不存在 */
          for (j = s2; j <= e2; j++) {
    
     /* 遍历剩下的所有新节点 */
            if (
              newIndexToOldIndexMap[j - s2] === 0 && /* newIndexToOldIndexMap[j - s2] === 0 新节点没有被patch */
              isSameVNodeType(prevChild, c2[j] as VNode)
            ) {
    
     /* 如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex  */
              newIndex = j
              break
            }
          }
        }
        if (newIndex === undefined) {
    
     /* ①没有找到与老节点对应的新节点,删除当前节点,卸载所有的节点 */
          unmount(prevChild, parentComponent, parentSuspense, true)
        } else {
    
    
          /* ②把老节点的索引,记录在存放新节点的数组中, */
          newIndexToOldIndexMap[newIndex - s2] = i + 1
          if (newIndex >= maxNewIndexSoFar) {
    
    
            maxNewIndexSoFar = newIndex
          } else {
    
    
            /* 证明有节点已经移动了   */
            moved = true
          }
          /* 找到新的节点进行patch节点 */
          patch(
            prevChild,
            c2[newIndex] as VNode,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
          patched++
        }
 }


This code is the core of the diff algorithm.

Step 1: Find the index corresponding to the new node through the key of the old node: start traversing the old node, judge whether there is a key, if there is a key, find the index of the new node through the keyToNewIndexMap of the new node, if there is no key, then traverse the rest The new node of tries to find the corresponding index.

Step 2: If there is an index to prove that there is a corresponding old node, then directly reuse the old node for patching. If no new node corresponding to the old node is found, delete the current old node.

Step 3: newIndexToOldIndexMap finds the relationship between the corresponding new and old nodes.

So far, we have patched and patched all the old vnodes.

As shown
insert image description here
but the next question.

1 Although all old nodes have been patched. For the nodes that have already moved, how to actually move the dom elements.
2 For the newly added node, (node ​​I in the figure) has not been processed, how should it be processed.

      /*移动老节点创建新节点*/
     /* 根据最长稳定序列移动相对应的节点 */
      const increasingNewIndexSequence = moved
        ? getSequence(newIndexToOldIndexMap)
        : EMPTY_ARR
      j = increasingNewIndexSequence.length - 1
      for (i = toBePatched - 1; i >= 0; i--) {
    
    
        const nextIndex = s2 + i
        const nextChild = c2[nextIndex] as VNode
        const anchor =
          nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
        if (newIndexToOldIndexMap[i] === 0) {
    
     /* 没有老的节点与新的节点对应,则创建一个新的vnode */
          patch(
            null,
            nextChild,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG
          )
        } else if (moved) {
    
    
          if (j < 0 || i !== increasingNewIndexSequence[j]) {
    
     /*如果没有在长*/
            /* 需要移动的vnode */
            move(nextChild, container, anchor, MoveType.REORDER)
          } else {
    
    
            j--
          }    

⑥ The longest stable sequence
is the first to obtain the longest stable sequence through getSequence. For the case of index === 0, that is, the newly added node (I in the figure) needs to remount a new vnode, and then perform unified mobile operation

What is the longest stable sequence?

For the following original sequence
0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15, the longest increasing
subsequence is
0, 2, 6, 9, 11, 15.

Why get the longest stable sequence?

Because we need a sequence as the basic reference sequence, other nodes that are not in the stable sequence are moved.

Summarize

After the above, we roughly know the process of the diff algorithm

  1. Comparing from the beginning to find the same node patch, if it is different, jump out immediately.
  2. If the first step is not patched, immediately start patching from the back to the front, and jump out of the loop immediately if a difference is found.
  3. If the number of new nodes is greater than the number of old nodes, all the remaining nodes will be processed with new vnodes (in this case, it means that the same vnodes have been patched).
  4. For the case where the old node is larger than the new node, uninstall all the excess nodes (in this case, it means that the same vnode has been patched).
  5. Uncertain elements (this situation means that the same vnode has not been patched) are opposite to 3 and 4.

1. Save the new vnode nodes that have not been compared through the map.
Record the number of new nodes that have been patched. The number of new nodes that have
not been patched.
, 0, 0, ] The numbers in it record the index of the old node, and the array index is the index of the new node to start
traversing the old node
① If the number of new nodes in toBePatched is 0, then unload the old node
② If the key of the old node exists , find the corresponding index through the key
③ If the key of the old node does not exist
1 Traverse all the remaining new nodes
2 If you find a new node corresponding to the current old node, then assign the index of the new node to newIndex
④ No match is found The new node corresponding to the old node uninstalls the current old node.
⑤ If a new node corresponding to the old node is found, record the index of the old node in the array storing the new node. 1
If the node has moved, the record has moved
2. Patch the old and new nodes to find the new node and perform the patch node
traversal

If movement occurs
① Find the longest stable sequence according to the newIndexToOldIndexMap new and old node index list
② For newIndexToOldIndexMap -item = 0, prove that there is no old node, and form a new vnode
③ Move the node that has moved.

insert image description here

composition-api

Introduction to Composition API: A set of function-based additional APIs that can flexibly compose component logic. Composition API hopes to solve this problem by exposing the current component properties and available mechanisms as JavaScript functions. The Vue core team describes the component Composition API as "a set of additional, function-based APIs that allow flexible composition of component logic". Code written using the Composition API is more readable, and the scene is not complicated, which makes it easier to read and learn

Options API

In vue2, we will define methods, computed, watch, data and other attributes and methods in a vue file to jointly process page logic. We call this method Options API
insert image description here

Advantages and disadvantages

  • A function often needs to define attributes and methods in different vue configuration items, which are relatively scattered. Small projects are fine, clear and clear, but when the project is large, a method may contain many methods, and you often can't tell which method corresponds to it. which function
  • The regulations are clear, and the same things are placed in the same place; but as the function of the component increases, the relevance will be greatly reduced, and the difficulty of reading and understanding the component will increase;

Composition-API

In order to solve the problems in vue2, in vue3 Composition API, our code is organized according to logical functions, and all APIs defined by a function will be put together (more high cohesion, low coupling, even if The project is very large and has many functions. We can quickly locate all the APIs used by this function.
insert image description here
Composition-API puts all the APIs defined by each functional module in one module, which solves the problem of scattered modules in Vue2. causing problems
insert image description here

  • Composition API organizes code based on logical dependencies, improving readability and maintainability
  • API based on function composition better reuse logic code (reuse logic code through Mixins in vue2 Options API, prone to naming conflicts and unclear relationships)

API introduction

Setup function

When using the setup function, it will accept two parameters: props, context

props: the data passed from the parent component to the child component, context: contains three attributes attrs, slots, emit
(1) attrs: all non-prop attributes;

(2) slots: the slot passed by the parent component

(3) emit: emit is used when we need to emit events inside our components

props: {
    
    
    message: {
    
    
        type: String,
        required: true
        default:'长夜将至'
    }
},
setup(props,context) {
    
    
    // Attribute (非响应式对象)
    console.log(context.attrs)
    // 插槽 (非响应式对象)
    console.log(context.slots)
    // 触发事件 (方法)
    console.log(context.emit)
     //因为setup函数中是没有this这个东西的, 然而当我们需要拿到父组件所传递过来的数据时, setup函数的第一个参数props便起作用了
    console.log(this)// undefined
    console.log(props.message);//长夜将至
    return {
    
    } // 我们可以通过setup的返回值来替代data选项(但是当setup和data选项同时存在时,使用的是setup中的数据),并且这里返回的任何内容都可以用于组件的其余部分
},

  • The setup function is the function before the two hook functions of the lifecycle function beforeCreate and Created
  • When executing setup, the component instance has not yet been created (inside setup(), this will not be a reference to the active instance, that is, it does not point to the vue instance. In order to avoid our wrong use, Vue directly modifies this in the setup function to undefined)

ref

ref is used to add reactive state to data. Since reactive can only pass in parameters of object type, and for basic data types to add responsive state, only ref can be used, and a copy with responsive state is also returned.

<template>
<h1>{
    
    {
    
    newObj}}</h1>
<button @click="change">点击按钮</button>
 </template>
 
 <script>
import {
    
    ref} from 'vue';
export default {
    
    
  name:'App',
  setup(){
    
    
    let obj = {
    
    name : 'alice', age : 12};
    let newObj= ref(obj.name);
    function change(){
    
    
      newObj.value = 'Tom';
      console.log(obj,newObj)
    }
    return {
    
    newObj,change}
  }
}

insert image description here

  • The ref function can only monitor changes of simple types, not complex types, such as objects and arrays
  • The essence of ref is copying, and has no reference relationship with the original data.
  • ref modifying the responsive data will not affect the original data, and the interface will be updated

toRef

toRef is used to create a new ref for a property on the source reactive object, thereby maintaining a reactive connection to its source object property

<template>
<h1>{
    
    {
    
    newObj}}</h1>
<button @click="change">点击按钮</button>
 </template>
 
 <script>

import {
    
    toRef} from 'vue';
export default {
    
    
  name:'App',
  setup(){
    
    
    let obj = {
    
    name : 'alice', age : 12};
    let newObj= toRef(obj, 'name');
    function change(){
    
    
      newObj.value = 'Tom';
      console.log(obj,newObj)
    }
    return {
    
    newObj,change}
  }
}

insert image description here

  • When getting the data value, you need to add .value
  • The ref data after toRef is not a copy of the original data, but a reference, and changing the value of the result data will also change the original data at the same time
  • toRef receives two parameters, the first parameter is which object, and the second parameter is which attribute of the object
  • toRef can only set one data at a time

toRefs

Sometimes, we want to turn multiple properties of an object into responsive data, and require the responsive data to be associated with the original data, and when updating the responsive data without updating the interface, we can use toRefs for batch setting multiple The data is responsive data.

<template>
<h1>{
    
    {
    
    newObj}}</h1>
<button @click="change">点击按钮</button>
 </template>
 
 <script>

import {
    
    toRefs} from 'vue';
export default {
    
    
  name:'App',
  setup(){
    
    
    let obj = {
    
    name : 'alice', age : 12};
    let newObj= toRefs(obj);
    function change(){
    
    
      newObj.name.value = 'Tom';
      newObj.age.value = 18;
      console.log(obj,newObj)
    }
    return {
    
    newObj,change}
  }
}

insert image description here
It can be clearly seen from the above figure that after clicking the button, the original data and responsive data are updated, but the interface does not change.

  • toRefs receives an object as a parameter, it will traverse all the properties on the object, and then call toRef one by one to execute
  • When getting the data value, you need to add .value
  • The ref data after toRefs is not a copy of the original data, but a reference, and changing the value of the result data will also change the original data at the same time

Reactive variables with ref

When using reactive data in setup(), it needs to be obtained through .value, but when it is returned from the property on the object returned in setup() and can be accessed in the template, it will be automatically expanded to an internal value. No need to append .value in the template

<template>
<h1>{
    
    {
    
    count}}</h1>
 </template>
 
 <script>
  import {
    
     ref } from 'vue' // ref函数使任何变量在任何地方起作用
  export default {
    
    
    setup(){
    
    
      const count= ref(0)
      console.log(count)
      console.log(count.value) // 0
      return {
    
    count } 
    }
  }
 </script>

insert image description here

analyze

Directory Structure

vue3
 ├── packages        # 所有包(此目录只保持一部分包)
 │   ├── compiler-core           # 编译核心包 
 │   │   ├── api-extractor.json  # 用于合并.d.ts, api-extractor  API Extractor是一个TypeScript分析工具
 │   │   ├── src                 # 包主要开发目录
 │   │   ├── index.js            # 包入口,导出的都是dist目录的文件
 │   │   ├── LICENSE             # 开源协议文件
 │   │   ├── package.json        
 │   │   ├── README.md           # 包描述文件
 │   │   └── __tests__           # 包测试文件
 ├── scripts                      # 一些工程化的脚本,本文重点
 │   ├── bootstrap.js            # 用于生成最小化的子包
 │   ├── build.js                # 用于打包所有packages下的包
 │   ├── checkYarn.js            # 检查是否是yarn进行安装
 │   ├── dev.js                  # 监听模式开发
 │   ├── release.js              # 用于发布版本
 │   ├── setupJestEnv.ts         # 设置Jest的环境
 │   ├── utils.js                # 公用的函数包
 │   └── verifyCommit.js         # git提交验证message
 ├── test-dts                     # 验证类型声明
 │   ├── component.test-d.ts
 |   ├── .....-d.ts
 ├── api-extractor.json          # 用于合并.d.ts
 ├── CHANGELOG.md                # 版本变更日志
 ├── jest.config.js              # jest测试配置
 ├── LICENSE
 ├── package.json
 ├── README.md
 ├── rollup.config.js            # rollup配置
 ├── tsconfig.json               # ts配置
 └── yarn.lock                   # yarn锁定版本文件

Among them, Vue 3 and the core source code are in the packages, and are built based on RollUp, and the meaning of each directory is as follows:

├── packages              
│   ├── compiler-core    // 核心编译器(平台无关)
│   ├── compiler-dom     // dom编译器
│   ├── compiler-sfc     // vue单文件编译器
│   ├── compiler-ssr     // 服务端渲染编译
│   ├── global.d.ts      // typescript声明文件
│   ├── reactivity       // 响应式模块,可以与任何框架配合使用
│   ├── runtime-core     // 运行时核心实例相关代码(平台无关)
│   ├── runtime-dom      // 运行时dom 关api,属性,事件处理
│   ├── runtime-test     // 运行时测试相关代码
│   ├── server-renderer   // 服务端渲染
│   ├── sfc-playground    // 单文件组件在线调试器
│   ├── shared             // 内部工具库,不对外暴露API
│   ├── size-check          // 简单应用,用来测试代码体积
│   ├── template-explorer  // 用于调试编译器输出的开发工具
│   └── vue                 // 面向公众的完整版本, 包含运行时和编译器
│   └── vue-compat     //针对vue2的兼容版本

Through the above source code structure, you can see that the following modules are quite special:

  • compiler-core
  • compiler-dom
  • runtime-core
  • runtime-dom
    can see that core and dom appear twice respectively, so what is the difference between compiler and runtime?
  • compile: We can understand that when compiling a program, it refers to the period during which the source code we wrote is compiled into an object file. It can be generally regarded as the source code we wrote is converted into the final executable by the build tool During this period of time, it can be understood as some work that we compile the .vue file into a .js file that the browser can recognize.
  • runtime: It can be understood as when the program is running, that is, after the program is compiled, the browser opens the program and runs it until the program is closed.

Responsive principle

Proxy API

see above

Proxy and responsive object reactive

In Vue 3, use the reactive object method as shown in the following code:

import {
    
    ref,reactive} from 'vue'
...
setup(){
    
    
  const name = ref('test')
  const state = reactive({
    
    
    list: []
  })
  return {
    
    name,state}
}
...

In Vue 3, the method ref/reactive for creating responsive objects is often used in the Composition API, which is implemented internally by using the Proxy API, especially with the help of the set method of the handler, which can realize the logic related to two-way data binding. This is a big change for Object.defineProperty() in Vue 2. The main improvements are as follows:

  • Object.defineProperty() can only monitor the modification or change of existing properties, and cannot detect the addition or deletion of object properties (
    in Vue 2, the $set() method is used to solve it), while Proxy can be easily implemented.
  • Object.defineProperty() cannot monitor the change of the responsive data type is an array (mainly the change of the length of the array, which is
    solved in Vue 2 by rewriting related methods of the array and adding hooks), while Proxy can be easily implemented.

It is precisely because of the characteristics of Proxy that the above two capabilities that originally required a very complicated way to use Object.defineProperty() can be easily realized without any configuration in Proxy by using its native features.

How the ref() method works

In the source code of Vue 3, all responsive codes are under vue-next/package/reactivity, and all available methods are exposed in reactivity/src/index.ts. Let's take the commonly used ref() method as an example to see how Vue 3 uses Proxy. The main logic of the ref() method is in reactivity/src/ref.ts, and its code is as follows:

...
// 入口方法
export function ref(value?: unknown) {
    
    
  return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
    
    
  // rawValue表示原始对象,shallow表示是否递归
  // 如果本身已经是ref对象,则直接返回
  if (isRef(rawValue)) {
    
    
    return rawValue
  }
  // 创建一个新的RefImpl对象
  return new RefImpl(rawValue, shallow)
}
...

The second parameter received by the createRef method is shallow, indicating whether it is a recursive monitoring response type, which corresponds to another response type method shallowRef(). In the RefImpl constructor, there is a value attribute, which is returned by the toReactive() method, and the toReactive() method is in the reactivity/src/reactive.ts file, as shown in the following code:

class RefImpl<T> {
    
    
  ...
  constructor(value: T, public readonly _shallow: boolean) {
    
    
    this._rawValue = _shallow ? value : toRaw(value)
    // 如果是非递归,调用toReactive
    this._value = _shallow ? value : toReactive(value)
  }
  ...
}

In reactive.ts, start to actually create a responsive object, as shown in the following code:

export function reactive(target: object) {
    
    
  // 如果是readonly,则直接返回,就不添加响应式了
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    
    
    return target
  }
  return createReactiveObject(
    target,// 原始对象
    false,// 是否readonly
    mutableHandlers,// proxy的handler对象baseHandlers
    mutableCollectionHandlers,// proxy的handler对象collectionHandlers
    reactiveMap// proxy对象映射
  )
}

Among them, the createReactiveObject() method passes two kinds of handlers, namely baseHandlers and collectionHandlers. If the target type is Map, Set, WeakMap, WeakSet, collectionHandlers will be used, and the type is Object. Array will be baseHandlers. If it is a basic object , and will not create a Proxy object, and reactiveMap stores the mapping relationship of all responsive objects to avoid repeated creation of the same object. Let's take a look at the implementation of the createReactiveObject() method, as shown in the following code:

function createReactiveObject(...) {
    
    
  // 如果target不满足typeof val === 'object',则直接返回target
  if (!isObject(target)) {
    
    
    if (__DEV__) {
    
    
      console.warn(`value cannot be made reactive: ${
      
      String(target)}`)
    }
    return target
  }
  // 如果target已经是proxy对象或者只读,则直接返回
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    
    
    return target
  }
  // 如果target已经被创建过Proxy对象,则直接返回这个对象
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    
    
    return existingProxy
  }
  // 只有符合类型的target才能被创建响应式
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    
    
    return target
  }
  // 调用Proxy API创建响应式
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 标记该对象已经创建过响应式
  proxyMap.set(target, proxy)
  return proxy
}

You can see that in the createReactiveObject() method, the following things are mainly done:

  • Prevent read-only and duplicate creation of reactive types.
  • Select different handlers according to different target types.
  • Create a Proxy object.

Finally, new Proxy will be called to create a responsive object. Let's take baseHandlers as an example to see how this handler is implemented. You can see this part of the code in reactivity/src/baseHandlers.ts. It mainly implements these handlers, as follows The code shows:

const get = /*#__PURE__*/ createGetter()
...
export const mutableHandlers: ProxyHandler<object> = {
    
    
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

Take handler.get as an example to see what is done inside it. When we try to read the properties of an object, we will enter the get method. The core code is as follows:

function createGetter(isReadonly = false, shallow = false) {
    
    
  return function get(target: Target, key: string | symbol, receiver: object) {
    
    
    if (key === ReactiveFlags.IS_REACTIVE) {
    
     // 如果访问对象的key是__v_isReactive,则直接返回常量
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
    
    // 如果访问对象的key是__v_isReadonly,则直接返回常量
      return isReadonly
    } else if (// 如果访问对象的key是__v_raw,或者原始对象只读对象等等直接返回target
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
    
    
      return target
    }
    // 如果target是数组类型
    const targetIsArray = isArray(target)
    // 并且访问的key值是数组的原生方法,那么直接返回调用结果
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
    
    
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // 求值
    const res = Reflect.get(target, key, receiver)
    // 判断访问的key是否是Symbol或者不需要响应式的key例如__proto__,__v_isRef,__isVue
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
    
    
      return res
    }
    // 收集响应式,为了后面的effect方法可以检测到
    if (!isReadonly) {
    
    
      track(target, TrackOpTypes.GET, key)
    }
    // 如果是非递归绑定,直接返回结果
    if (shallow) {
    
    
      return res
    }

    // 如果结果已经是响应式的,先判断类型,再返回
    if (isRef(res)) {
    
    
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    // 如果当前key的结果也是一个对象,那么就要递归调用reactive方法对改对象再次执行响应式绑定逻辑
    if (isObject(res)) {
    
    
      return isReadonly ? readonly(res) : reactive(res)
    }
    // 返回结果
    return res
  }
}

The above code is one of the core codes of Vue 3 responsiveness. Its logic is relatively complicated. Readers can understand it according to the comments. In summary, this code mainly does the following things:

  • For the handler.get method, it will eventually return the result corresponding to the key of the current object, namely obj[key], so this code will eventually return the result.
  • For non-responsive keys, read-only keys, etc., the corresponding results are returned directly.
  • For the target of array type, if the key value is a method on the prototype, such as includes, push, pop, etc., use Reflect.get to return it directly.
  • Add a collection and monitoring track to the effect to provide responsive monitoring services.
  • When the result corresponding to the current key is an object, in order to ensure that the set method can be triggered, it is necessary to recursively bind the object recursively, that is, call the reactive() method recursively.

The main function of the handler.get method is to return the result value, so let’s see what the handler.set mainly does. The code is as follows:

function createSetter(shallow = false) {
    
    
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,// 即将被设置的新值
    receiver: object
  ): boolean {
    
    
    // 缓存旧值
    let oldValue = (target as any)[key]
    if (!shallow) {
    
    
      // 新旧值转换原始对象
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      // 如果旧值已经是一个RefImpl对象且新值不是RefImpl对象
      // 例如var v = Vue.reactive({a:1,b:Vue.ref({c:3})})场景的set
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
    
    
        oldValue.value = value // 直接将新值赋给旧址的响应式对象里
        return true
      }
    }
    // 用来判断是否是新增key还是更新key的值
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // 设置set结果,并添加监听effect逻辑
    const result = Reflect.set(target, key, value, receiver)
    // 判断target没有动过,包括在原型上添加或者删除某些项
    if (target === toRaw(receiver)) {
    
    
      if (!hadKey) {
    
    
        trigger(target, TriggerOpTypes.ADD, key, value)// 新增key的触发监听
      } else if (hasChanged(value, oldValue)) {
    
    
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)// 更新key的触发监听
      }
    }
    // 返回set结果 true/false
    return result
  }
}

The core function of the handler.set method is to set the value corresponding to the key, that is, obj[key] = value, and at the same time perform logical judgment and processing on the old and new values, and finally add a trigger to trigger the monitoring track logic to trigger the effect. If readers feel that it is difficult to understand the above source code, the author eliminates some boundaries and compatibility judgments, and sorts out and simplifies the entire process. You can refer to the following easy-to-understand code:

let foo = {
    
    a:{
    
    c:3,d:{
    
    e:4}},b:2}
const isObject = (val)=>{
    
    
    return val !== null && typeof val === 'object'
}
const createProxy = (target)=>{
    
    
    let p = new Proxy(target,{
    
    
        get:(obj,key)=>{
    
    
            let res = obj[key] ? obj[key] : undefined

            // 添加监听
            track(target)
            // 判断类型,避免死循环
            if (isObject(res)) {
    
    
                return createProxy(res)// 循环递归调用
            } else {
    
    
                return res
            }
        },
        set: (obj, key, value)=> {
    
    
          console.log('set')
          
          obj[key] = value;
          // 触发监听
          trigger(target)
          return true
        }
    })

    return p
}

let result = createProxy(foo)

result.a.d.e = 6 // 打印出set

When trying to modify the properties of a multi-level nested object, the get method of the upper level object of the property will be triggered. Using this, Proxy agents can be added to each level of objects, thus realizing multi-level nesting The problem of object attribute modification, on this basis, adding track and trigger logic at the same time, completes the basic responsive process.

Virtual DOM

What is virtual DOM

In a browser, an HTML page is composed of a basic DOM tree. When a part of it changes, it means that a certain DOM node has changed. When a DOM node changes, the corresponding redrawing or rearrangement will be triggered. , when excessive redrawing and rearrangement occurs in a short period of time, it may cause the page to freeze, so changing the DOM has some costs, so how to optimize the number of DOM changes and change the DOM at the right time is Things developers need to be aware of.

Virtual DOM is designed to solve the above browser performance problems. When there are 10 actions to update the DOM in one operation, the virtual DOM will not operate the DOM immediately, but compare it with the original DOM, save the changed content of these 10 updates in the memory, and finally apply it at once. DOM tree, and then perform subsequent operations to avoid a large amount of unnecessary calculations.

Virtual DOM actually uses JavaScript objects to store the information of DOM nodes, turns DOM updates into object modifications, and these modification calculations occur in memory. When the modification is completed, JavaScript is converted into real DOM nodes, and the To the browser, so as to achieve performance improvement. For example, the following section of DOM node, as shown in the following code:

<div id="app">
  <p class="text">Hello</p>
</div>

Convert to a general virtual DOM object structure, as shown in the following code:

{
    
    
  tag: 'div',
  props: {
    
    
    id: 'app'
  },
  chidren: [
    {
    
    
      tag: 'p',
      props: {
    
    
        className: 'text'
      },
      chidren: [
        'Hello'
      ]
    }
  ]
}

The above code is a basic virtual DOM, but it is not the virtual DOM structure used in Vue, because Vue is much more complicated.

Vue 3 Virtual DOM

In Vue, <template>the content we write in the tag belongs to the DOM node, and this part of the content will be converted into the virtual DOM object VNode in Vue. The steps are more complicated, mainly including the following processes:

  • Extract <template>the content and compile it.
  • Get the AST syntax tree and generate the render method.
  • Execute the render method to get the VNode object.
  • VNode converts the real DOM and renders to the page.

The complete process is shown in the figure below:
insert image description here
Let's take a simple demo as an example, and look in the source code of Vue 3 to find out how to proceed step by step. The demo code is as follows:

<div id="app">
  <div>
    {
    
    {
    
    name}}
  </div>
  <p>123</p>
</div>
Vue.createApp({
    
    
  data(){
    
    
    return {
    
    
      name : 'abc'
    }
  }
}).mount("#app")

In the above code, a responsive data name is defined in data, and <template>the interpolation expression { {name}} is used in it, and there is also a static node <p>123</p>.

get <template>content

Calling the createApp() method will enter the createApp() method in the source code packages/runtime-dom/src/index.ts, as shown in the following code:

export const createApp = ((...args) => {
    
    
  const app = ensureRenderer().createApp(...args)
  ...
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    
    
    if (!isFunction(component) && !component.render && !component.template) {
    
    
      // 将#app绑定的HTML内容赋值给template项上
      component.template = container.innerHTML

      // 调用mount方法渲染
    const proxy = mount(container, false, container instanceof SVGElement)
    return proxy
  }
  ...
  return app
}) as CreateAppFunction<Element>

For the root component, <template>the content is composed of the content in the mounted #app element. If the project adopts npm and Vue Cli+Webpack, the front-end engineering method, then the content of the content is mainly constructed <template>by the corresponding loader The file is processed to obtain it at runtime, which is different from the processing method at runtime of the browser.

Generate AST syntax tree

After obtaining <template>, an AST syntax tree is generated according to the content. Abstract Syntax Tree (AST) is an abstract representation of source code syntax structure. It represents the grammatical structure of the programming language in the form of a tree, and each node on the tree represents a structure in the source code. The reason why the grammar is "abstract" is that the grammar here does not represent every detail that appears in the real grammar. For example, nested parentheses are implicit in the tree structure and are not presented in the form of nodes; while conditional jump statements like if-condition-then can be represented by nodes with three branches. As shown in the following code:

while b ≠ 0
  if a > b
a := a − b
  else
b := b − a
return a

If the above code is converted into a syntax tree in a broad sense, as shown in the figure.
insert image description here
For <template>content, most of it is composed of DOM, but there will also be conditional statements such as if-condition-then, such as v-if, v-for instructions, etc. In Vue 3, this part of the logic is in the source code packages\ The baseCompile method in compiler-core\src\compile.ts, the core code is as follows:

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {
     
     }
): CodegenResult {
    
    
  ...
  // 通过template生成ast树结构
  const ast = isString(template) ? baseParse(template, options) : template
  ...
  // 转换
  transform(
    ast,
    ...
  )
  return generate(
    ast,
    extend({
    
    }, options, {
    
    
      prefixIdentifiers
    })
  )
}

The baseCompile method mainly does the following things:

  • Generate an AST object in Vue.
  • Pass the AST object as a parameter to the transform function for transformation.
  • Pass the converted AST object as a parameter to the generate function to generate the render function.

Among them, the baseParse method is used to create an AST object. In Vue 3, the AST object is a RootNode type tree structure. In the source code packages\compiler-core\src\ast.ts, its structure is shown in the following code:

export function createRoot(
  children: TemplateChildNode[],
  loc = locStub
): RootNode {
    
    
  return {
    
    
    type: NodeTypes.ROOT, // 元素类型
    children, // 子元素
    helpers: [],// 帮助函数
    components: [],// 子组件
    directives: [], // 指令
    hoists: [],// 标识静态节点
    imports: [],
    cached: 0, // 缓存标志位
    temps: 0,
    codegenNode: undefined,// 存储生成render函数字符串
    loc // 描述元素在AST树的位置信息
  }
}

Among them, children store the data of descendant element nodes, which constitutes an AST tree structure, and type indicates the type of element NodeType, which is mainly divided into HTML common type and Vue instruction type, etc. The common types are as follows:

ROOT,  // 根元素 0
ELEMENT, // 普通元素 1
TEXT, // 文本元素 2
COMMENT, // 注释元素 3
SIMPLE_EXPRESSION, // 表达式 4
INTERPOLATION, // 插值表达式 {
    
    { }} 5
ATTRIBUTE, // 属性 6
DIRECTIVE, // 指令 7
IF, // if节点 9
JS_CALL_EXPRESSION, // 方法调用 14
...

hoists is an array, used to store some elements that can be statically improved, and the subsequent transform will create static elements and responsive elements separately, which is also the embodiment of optimization in Vue 3, and codegenNode is used to store the final generated render method String, loc indicates the position information of the element in the AST tree.

When generating the AST tree, Vue 3 <template>will use a stack to save the parsed element tags when parsing the content. When it encounters a start tag, it pushes the tag onto the stack, and when it encounters an end tag, it pops the previous tag off the stack. Its function is to save the element tags that have been parsed but not yet parsed. This stack has another function. When parsing to a certain byte point, its parent element can be obtained through stack[stack.length - 1].

The AST syntax tree generated in the demo code is shown in the figure below.
insert image description here

Generate render method string

After getting the AST object, it will enter the transform method. In the source code packages\compiler-core\src\transform.ts, its core code is as follows:

export function transform(root: RootNode, options: TransformOptions) {
    
    
// 数据组装  
const context = createTransformContext(root, options)
  // 转换代码
  traverseNode(root, context)
  // 静态提升
  if (options.hoistStatic) {
    
    
    hoistStatic(root, context)
  }// 服务端渲染
  if (!options.ssr) {
    
    
    createRootCodegen(root, context)
  }
  // 透传元信息
  root.helpers = [...context.helpers.keys()]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = context.imports
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached
  if (__COMPAT__) {
    
    
    root.filters = [...context.filters!]
  }
}

The transform method is mainly to further transform the AST to prepare for the generate function to generate the render method. It mainly does the following things:

  • The traverseNode method will recursively check and parse the attributes of AST element nodes, such as adding corresponding methods and event callbacks to events such as @click in combination with the helpers method, adding dynamic binding to interpolation expressions, instructions, and props, etc.
  • The processing type logic includes static promotion logic, assigning static nodes to hoists, and marking different patchFlags according to different types of nodes, which is convenient for subsequent diff use.
  • Bind and transparently transmit some metadata on AST.

The generate method is mainly to generate the string code of the render method. In the source code packages\compiler-core\src\codegen.ts, its core code is as follows:

export function generate(
  ast: RootNode,
  options: CodegenOptions & {
     
     
    onContextCreated?: (context: CodegenContext) => void
  } = {
     
     }
): CodegenResult {
    
    
  const context = createCodegenContext(ast, options)
  if (options.onContextCreated) options.onContextCreated(context)
  const {
    
    
    mode,
    push,
    prefixIdentifiers,
    indent,
    deindent,
    newline,
    scopeId,
    ssr
  } = context
  ...
  // 缩进处理
  indent()
  deindent()
  // 单独处理component、directive、filters
  genAssets()
  // 处理NodeTypes里的所有类型
  genNode(ast.codegenNode, context)
  ...
  // 返回code字符串
  return {
    
    
    ast,
    code: context.code,
    preamble: isSetupInlined ? preambleContext.code : ``,
    // SourceMapGenerator does have toJSON() method but it's not in the types
    map: context.map ? (context.map as any).toJSON() : undefined
  }
}

The core logic of the generate method is in the genNode method. The logic is to construct different render method strings according to different NodeTypes types. Some types are shown in the following code:

switch (node.type) {
    
    
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:// for关键字元素节点
  genNode(node.codegenNode!, context)
  break
case NodeTypes.TEXT:// 文本元素节点
  genText(node, context)
  break
case NodeTypes.VNODE_CALL:// 核心:VNode混合类型节点(AST语法树节点)
  genVNodeCall(node, context)
  break
case NodeTypes.COMMENT: // 注释元素节点
  genComment(node, context)
  break
case NodeTypes.JS_FUNCTION_EXPRESSION:// 方法调用节点
  genFunctionExpression(node, context)
  break
...

in:

  • The node type NodeTypes.VNODE_CALL corresponds to the genVNodeCall method and the createVNodeCall method in the ast.ts file. The latter is used to return VNodeCall, and the former generates the corresponding VNodeCall part of the render method string, which is the core of the entire render method string.
  • The node type NodeTypes.FOR corresponds to the for keyword element node, and the genNode method is called recursively inside it.
  • The node type NodeTypes.TEXT corresponds to the text element node responsible for the generation of static text.
  • The node type NodeTypes.JS_FUNCTION_EXPRESSION corresponds to the method call node and is responsible for the generation of method expressions.

Finally, after a series of processing, the result of the final render method string is as follows:

(function anonymous(
) {
    
    
const _Vue = Vue
const {
    
     createElementVNode: _createElementVNode } = _Vue

const _hoisted_1 = ["data-a"] // 静态节点
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, "123", -1 /* HOISTED */)// 静态节点

return function render(_ctx, _cache) {
    
    // render方法
  with (_ctx) {
    
    
    const {
    
     toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue // helper方法

    return (_openBlock(), _createElementBlock(_Fragment, null, [
      _createElementVNode("div", {
    
     "data-a": attr }, _toDisplayString(name), 9 /* TEXT, PROPS */, _hoisted_1),
      _hoisted_2
    ], 64 /* STABLE_FRAGMENT */))
  }
}
})

_createElementVNode, _openBlock and other helper methods passed in the previous step. Among them <p>123</p>, this kind of static node without responsive binding will be distinguished separately, while the createElementVNode method will be used for dynamic nodes to create, and finally these two nodes will enter the createElementBlock method to create VNode.

The with keyword is used in the render method, and the function of with is shown in the following code:

const obj = {
    
    
  a:1
}
with(obj){
    
    
  console.log(a) // 打印1
}

Under the package of with(_ctx), the responsive variables we define in data can be used normally, such as calling _toDisplayString(name), where name is the responsive variable.

Get the final VNode object

In the end, this is an executable code that will be assigned to the component Component.render method, and its source code is in packages\runtime-core\src\component.ts, as follows:

...
Component.render = compile(template, finalCompilerOptions)
...
if (installWithProxy) {
    
     // 绑定代理
   installWithProxy(instance)
}
...

The compile method is the entry point of the initial baseCompile method. After the assignment is completed, the proxy needs to be bound and the installWithProxy method executed. The source code is in runtime-core/src/component.ts, as shown below:

export function registerRuntimeCompiler(_compile: any) {
    
    
  compile = _compile
  installWithProxy = i => {
    
    
    if (i.render!._rc) {
    
    
      i.withProxy = new Proxy(i.ctx, RuntimeCompiledPublicInstanceProxyHandlers)
    }
  }
}

This is mainly to add binding to the responsive variable of _ctx in render. When the name in the above render method is used, the call can be monitored through the proxy, so that it will enter the responsive monitoring collection track. When the trigger monitoring is triggered , perform diff.

In the renderComponentRoot method in the runtime-core/src/componentRenderUtils.ts source code, the render method will be executed to obtain the VNode object. The core code is as follows:

export function renderComponentRoot(){
    
    
  // 执行render
  let result = normalizeVNode(render!.call(
        proxyToUse,
        proxyToUse!,
        renderCache,
        props,
        setupState,
        data,
        ctx
      ))
  ...

  return result
}

The final VNode object obtained in the demo code is shown in the figure below.
insert image description here
The above figure is the VNode object obtained after running the render method. You can see the distinction between children and dynamicChildren. The former includes two child nodes corresponding to <div>this <p>and <template>the content defined in it, while the latter only stores dynamic nodes, including Dynamic props are data-a attributes. At the same time, VNode is also a tree structure, which goes down layer by layer through children and dynamicChildren.

The process of obtaining VNode through the render method is also a process of parsing and constructing a series of Vue grammars such as instructions, interpolation expressions, responsive data, slots, etc., and finally generates a structured VNode object, which can be summarized into a flow chart. It is easy for readers to understand, as shown in the figure below.
insert image description here
Another attribute that needs to be paid attention to is patchFlag, which is the flag used when performing VNode diff later. The number 64 means that it is stable and does not need to be changed. Finally, after getting the VNode object, it needs to be converted into a real DOM node. This part of the logic is completed in the diff of the virtual DOM

source

Vue3 source code analysis - directory structure
Vue 3 source code analysis - responsive principle
Vue3 source code analysis - virtual DOM
Vue3 source code analysis (1) - package management
vue3.0 diff algorithm detailed (super detailed)
Composition-API

Guess you like

Origin blog.csdn.net/weixin_44231544/article/details/125852707