Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

preface

I hope this article will help you deepen your understanding of Vue and can confidently say that you are proficient in Ve2 2/3. In addition, I also hope that friends passing by can help me to fill the gap 🤞.

Content mixed usage + principle + use carefully, suggest collection, slowly look.

The difference between

Life cycle changes

Overall, there is little change, but most of the names need + ON, and the function is similar. The Vue3 composite API needs to be introduced first. The Vue2 option API can be called directly, as shown below.

// vue3
<script setup>     
import { onMounted } from 'vue'

onMounted(() = >{... })// Can split different logic into multiple onMounted, still execute sequentially, not overwrite
onMounted(() = >{... })</script>

// vue2
<script>     
   export default {         
      mounted(){... }},</script> 

Copy the code

Common life cycle tables are shown below.

Vue2.x Vue3
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestroy onBeforeUnmount
destroyed onUnmounted

Tips: Setup runs around beforeCreate and Created lifecycle hooks, so it doesn’t need to be explicitly defined.

More than a root node

Vue3 supports multiple node components, also known as fragments.

In Vue2, when writing a page, we need to wrap the component in

, otherwise we will get an error warning.
<template>
  <div>
    <header>.</header>
    <main>.</main>
    <footer>.</footer>
  </div>
</template>
Copy the code

Vue3, we can component contains multiple root nodes, can write one less layer, niceeee!

<template>
  <header>.</header>
  <main>.</main>
  <footer>.</footer>
</template>
Copy the code

Asynchronous components

Vue3 provides Suspense components that allow applications to render content at the bottom of the loop while waiting for asynchronous components, such as loading, for a smoother user experience. To use it, declare it in the template and include two naming slots: default and fallback. Suspense ensures that the default slot is displayed when asynchronous content is finished loading and the fallback slot is used as the loading state.

<tempalte>
   <suspense>
     <template #default>
       <todo-list />
     </template>
     <template #fallback>
       <div>
         Loading...
       </div>
     </template>
   </suspense>
</template>
Copy the code

If you want to invoke asynchronous requests in setup, you need to add the async keyword before setup. Async setup() is used without a suspense boundary.

Solution: Wrap a layer of Suspense components around the current component in the parent page.

Teleport

Vue3 provides a Teleport component to move part of the DOM to a location outside the Vue app. For example, Dialog components are common in projects.

<button @click="dialogVisible = true">Click on the</button>
<teleport to="body">
   <div class="dialog" v-if="dialogVisible">
   </div>
</teleport>
Copy the code

Modular API

Vue2 is an Option API. A logic is scattered in different parts of the file (data, props, computed, watch, lifecycle functions, etc.), which makes the code less readable and requires jumping up and down the file. Vue3’s Composition API solves this problem by writing together the contents of the same logic.

In addition to improving code readability and cohesion, the composite API provides a perfect solution for logical reuse, such as 🌰, as shown in the common mouse coordinate example below.

// main.vue
<template>
  <span>mouse position {{x}} {{y}}</span>
</template>

<script setup>
import { ref } from 'vue'
import useMousePosition from './useMousePosition'

const {x, y} = useMousePosition()

}
</script>
Copy the code
// useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue'

function useMousePosition() {
  let x = ref(0)
  let y = ref(0)
  
  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }
  
  onMounted(() = > {
    window.addEventListener('mousemove', update)
  })
  
  onUnmounted(() = > {
    window.removeEventListener('mousemove', update)
  })
  
  return {
    x,
    y
  }
}
</script>
Copy the code

This solution solves the naming conflict hidden danger of Vue2 Mixin, resulting in unclear dependency relationship and inflexible configuration and use among different components.

Response principle

The principle of Vue2 response is based on Object.defineProperty; Vue3 is based on Proxy.

Object.defineProperty

Basic usage: Directly define new properties or modify existing properties on an object and return the object. Writable and Value do not coexist with getters and setters.

let obj = {}
let name = 'jin line'
Object.defineProperty(obj, 'name', {
  enumerable: true.// enumerable (whether to pass for... In or object.keys ()
  configurable: true.// Configurable (can delete using delete, can set the property again)
  // value: ", // Any type of value, default undefined
  // writable: true, // writable
  get: function() {
    return name
  },
  set: function(value) {
    name = value
  }
})
Copy the code

Carry Vue2 core source code, slightly deleted.

function defineReactive(obj, key, val) {
  // dep with one key
  const dep = new Dep()
  
  // Get the property descriptor for key and return if it is not configurable
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) { return }
  
  // Get getters and setters, and get val values
  const getter = property && property.get
  const setter = property && property.set
  if((! getter || setter) &&arguments.length === 2) { val = obj[key] }
  
  // Make sure all keys in the object are observed
  let childOb = observe(val)
  
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.// get hijacks obj[key] for dependency collection
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if(Dep.target) {
        // Rely on collection
        dep.depend()
        if(childOb) {
          // For nested objects, rely on collection
          childOb.dep.depend()
          // Trigger array responsiveness
          if(Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
    }
    return value
  })
  // set update obj[key]
  set: function reactiveSetter(newVal) {...if(setter) {
      setter.call(obj, newVal)
    } else {
      val = newVal
    }
    // Set the new value to be responsive
    childOb = observe(val)
    // Rely on notification updates
    dep.notify()
  }
}
Copy the code

So why did Vue3 abandon it? There must be some flaws.

Main cause: Cannot listen for new or deleted elements of an object or array. Vue2 scheme: The common array prototype methods push, POP, shift, unshift, splice, sort, reverse hack processing; Vue. Set listens to new object/array properties. Object add/delete response, you can also new a new object, new will merge the new properties and the old object; Delete deeply copies the deleted object to the new object.

DefineOProperty allows you to listen on existing elements of an array, but Vue2 does not provide it because of performance issues.

Proxy

Proxy is a new feature in ES6. It intercepts the behavior of the target object through the second parameter handler. Limitations are eliminated by providing language-wide responsiveness compared to Object.defineProperty. But abandoned on compatibility (below Internet Explorer 11)

limitations

  1. Add and delete objects/arrays.
  2. Monitor.length changes.
  3. WeakMap, Set, WeakMap, WeakSet support.

Basic usage: Create proxies for objects to intercept and customize basic operations.

const handler = {
  get: function(obj, prop) {
    return prop in obj ? obj[prop] : ' '
  },
  set: function() {},... }Copy the code

Move the Vue3 source react. ts file

function createReactiveObject(target, isReadOnly, baseHandlers, collectionHandlers, proxyMap) {...// collectionHandlers: Handles Map, Set, WeakMap, WeakSet
  // baseHandlers: handles arrays and objects
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}
Copy the code

In the basehandlers.ts example, reflect.get is used instead of target[key] because the receiver parameter points this to the object in the getter call instead of the object in the Proxy construct.

// Rely on collection
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {...// Array type
    const targetIsArray = isArray(target)
    if(! isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // Non-array type
    const res = Reflect.get(target, key, receiver);
    
    // The object is called recursively
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}
// Send updates
function createSetter() {
  return function set(target: Target, key: string | symbol, value: unknown, receiver: Object) {
    value = toRaw(value)
    oldValue = target[key]
    // The ref data is dependent on the set value, so it can be assigned to return
    if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true
    }

    // Whether the object has a key There is a key set, but no key add
    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    
    if (target === toRaw(receiver)) {
      if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) }else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
Copy the code

Virtual DOM

Compared with Vue2, patchFlag fields are added to the virtual DOM of Vue3. Let’s see with the help of the Vue3 Template Explorer.

<div id="app">
  <h1>Technology to touch the fish</h1>
  <p>It's a nice day today</p>
  <div>{{name}}</div>
</div>
Copy the code

The rendering function is as follows.

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from "vue"

const _withScopeId = n= > (_pushScopeId("scope-id"),n=n(),_popScopeId(),n)
const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/ _withScopeId(() = > /*#__PURE__*/_createElementVNode("h1".null."Technology", -1 /* HOISTED */))
const _hoisted_3 = /*#__PURE__*/ _withScopeId(() = > /*#__PURE__*/_createElementVNode("p".null."It's a beautiful day.", -1 /* HOISTED */))

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _hoisted_2,
    _hoisted_3,
    _createElementVNode("div".null, _toDisplayString(_ctx.name), 1 /* TEXT */)))}Copy the code

Note that the patchFlag field type is the fourth parameter of the third _createElementVNode, as shown below. 1 indicates that the node is a dynamic text node, so in the diff process, only text alignment is required, and class, style, etc., need not be concerned. In addition, all static nodes are found to be saved as a variable for static promotion, which can be referenced directly during re-rendering without re-creation.

export const enum PatchFlags { 
  TEXT = 1.// Dynamic text content
  CLASS = 1 << 1.// Dynamic class name
  STYLE = 1 << 2.// Dynamic style
  PROPS = 1 << 3.// Dynamic attributes, without class name or style
  FULL_PROPS = 1 << 4.// Has a dynamic key property. When the key changes, a full diff comparison is required
  HYDRATE_EVENTS = 1 << 5.// A node with a listening event
  STABLE_FRAGMENT = 1 << 6.// Fragment that does not change the order of child nodes
  KEYED_FRAGMENT = 1 << 7.// Fragment or partial element node with key attribute
  UNKEYED_FRAGMENT = 1 << 8.// The child node does not have a key fragment
  NEED_PATCH = 1 << 9.// Only non-props comparisons will be made
  DYNAMIC_SLOTS = 1 << 10.// Dynamic slots
  HOISTED = -1.// Static node, the diff phase ignores its children
  BAIL = -2 // means diff should end
}
Copy the code

Event caching

Vue3’s cacheHandler caches our event after the first rendering. Compared to Vue2, you don’t need to pass a new function every time you render. Add a click event.

<div id="app">
  <h1>Technology to touch the fish</h1>
  <p>It's a nice day today</p>
  <div>{{name}}</div>
  <span onCLick="() = > {}"><span>
</div>
Copy the code

The rendering function looks like this

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from "vue"

const _withScopeId = n= > (_pushScopeId("scope-id"),n=n(),_popScopeId(),n)
const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/ _withScopeId(() = > /*#__PURE__*/_createElementVNode("h1".null."Technology", -1 /* HOISTED */))
const _hoisted_3 = /*#__PURE__*/ _withScopeId(() = > /*#__PURE__*/_createElementVNode("p".null."It's a beautiful day.", -1 /* HOISTED */))
const _hoisted_4 = /*#__PURE__*/ _withScopeId(() = > /*#__PURE__*/_createElementVNode("span", { onCLick: "() = > {}"},/*#__PURE__*/_createElementVNode("span"] -1 /* HOISTED */))

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _hoisted_2,
    _hoisted_3,
    _createElementVNode("div".null, _toDisplayString(_ctx.name), 1 /* TEXT */),
    _hoisted_4
  ]))
}
Copy the code

The Diff optimization

Vue3 patchChildren source code Based on the above and the source code, patchFlag helps distinguish static nodes and different types of dynamic nodes in diFF. Reduce the comparison of nodes and their attributes to a certain extent.

function patchChildren(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) {
  // Get old and new child nodes
  const c1 = n1 && n1.children
  const c2 = n2.children
  const prevShapeFlag = n1 ? n1.shapeFlag : 0
  const { patchFlag, shapeFlag } = n2
  
  // Process patchFlag greater than 0
  if(patchFlag > 0) {
    if(patchFlag && PatchFlags.KEYED_FRAGMENT) {
      / / is the key
      patchKeyedChildren()
      return
    } els if(patchFlag && PatchFlags.UNKEYED_FRAGMENT) {
      // There is no key
      patchUnkeyedChildren()
      return}}// The match is a text node (static) : Remove the old node and set the text node
  if(shapeFlag && ShapeFlags.TEXT_CHILDREN) {
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
    }
    if(c2 ! == c1) { hostSetElementText(container, c2as string)
    }
  } else {
    // Match old and new vNodes as arrays, then full comparison; Otherwise, remove all current nodes
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense,...)
      } else {
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)}}else {
      
      if(prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        hostSetElementText(container, ' ')}if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        mountChildren(c2 asVNodeArrayChildren, container,anchor,parentComponent,...) }}}}Copy the code

PatchUnkeyedChildren source code below.

function patchUnkeyedChildren(c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) {
  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++) {
    // Clone a new Vnode if it is already mounted. Otherwise, create a new Vnode
    const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as Vnode)) : normalizeVnode(c2[i])
    patch()
  }
  if(oldLength > newLength) {
    // Remove redundant nodes
    unmountedChildren()
  } else {
    // Create new nodes
    mountChildren()
  }
  
}
Copy the code

PatchKeyedChildren source code is as follows, using the longest increasing sequence algorithm idea.

function patchKeyedChildren(c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) {
  let i = 0;
  const e1 = c1.length - 1
  const e2 = c2.length - 1
  const l2 = c2.length
  
  If the new and old nodes are the same, execute patch to update the difference. Otherwise, break out of the loop
  while(i <= e1 && i <= e2) {
    const n1 = c1[i]
    const n2 = c2[i]
    
    if(isSameVnodeType) {
      patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSvg, optimized)
    } else {
      break
    }
    i++
  }
  
  If the new and old nodes are the same node, execute patch to update the difference. Otherwise, break out of the loop
  while(i <= e1 && i <= e2) {
    const n1 = c1[e1]
    const n2 = c2[e2]
    if(isSameVnodeType) {
      patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSvg, optimized)
    } else {
      break
    }
    e1--
    e2--
  }
  
  // Only nodes need to be added
  if(i > e1) {    
    if(i <= e2) {
      const nextPos = e2 + 1
      const anchor = nextPos < l2 ? c2[nextPos] : parentAnchor
      while(i <= e2) {
        patch(null, c2[i], container, parentAnchor, parentComponent, parentSuspense, isSvg, optimized)
      }
    }
  }
  
  // Only nodes need to be deleted
  else if(i > e2) {
    while(i <= e1) {
      unmount(c1[i], parentComponent, parentSuspense, true)}}// The old and new nodes are not traversed
  // [i ... e1 + 1]: a b [c d e] f g
  // [i ... e2 + 1]: a b [e d c h] f g
  // i = 2, e1 = 4, e2 = 5
  else {
    const s1 = i
    const s2 = i
    {e: 2, d: 3, c: 4, h: 5}
    const keyToNewIndexMap = new Map(a)for (i = s2; i <= e2; i++) {
      const nextChild = (c2[i] = optimized
          ? cloneIfMounted(c2[i] as VNode)
          : normalizeVNode(c2[i]))
      
      if(nextChild.key ! =null) {
        if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
          warn(
            `Duplicate keys found during update:`.JSON.stringify(nextChild.key),
            `Make sure keys are unique.`
          )
        }
        keyToNewIndexMap.set(nextChild.key, i)
      }
    }
  }
  
  let j = 0
  // Record the number of new VNodes to be patched
  let patched = 0
  // Length of the remaining Vnode
  const toBePatched = e2 - s2 + 1
  // Whether to move the identifier
  let moved = false
  let maxNewindexSoFar = 0
  
  // Initialize the correspondence between old and new nodes (for subsequent maximum-incrementing sequence algorithms)
  const newIndexToOldIndexMap = new Array(toBePatched)
  for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
  
  // Traverses the remaining nodes of the old Vnode
  for (i = s1; i <= e1; i++) {
    const prevChild = c1[i]
    
    // Indicates that all new VNodes have been patched. Remove the remaining vNodes
    if (patched >= toBePatched) {
      unmount(prevChild, parentComponent, parentSuspense, true)
      continue
    }
    
    let newIndex
    // If the old Vnode has a key, obtain it from keyToNewIndexMap
    if(prevChild.key ! =null) {
      newIndex = keyToNewIndexMap.get(prevChild.key)
    // If the old Vnode does not have a key, the new Vnode is iterated to obtain the key
    } else {
      for (j = s2; j <= e2; j++) {
        if (newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j] as VNode)){
           newIndex = j
           break}}}// Delete and update the node
   // The new Vnode has no current node
   if (newIndex === undefined) {
     unmount(prevChild, parentComponent, parentSuspense, true)}else {
     // The subscript position of the old Vnode + 1 is stored in the Map of the new Vnode
     // The + 1 processing is used to prevent the first index of the array from being 0, which means that a new node needs to be created
     newIndexToOldIndexMap[newIndex - s2] = i + 1
     
     // If it is not continuously incremented, it means that it needs to move
     if (newIndex >= maxNewIndexSoFar) {
       maxNewIndexSoFar = newIndex
     } else {
       moved = true
     }
     
     patch(prevChild,c2[newIndex],...)
     patched++
   }
  }
  
  NewIndexToOldIndexMap = {0:5, 1:4, 2:3, 3:0}
  // Create or move a node
  const increasingNewIndexSequence = moved
  // Get the longest increment sequence
  ? 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 = extIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
    // 0 Create a Vnode
    if (newIndexToOldIndexMap[i] === 0) {
      patch(null,nextChild,...)
    } else if (moved) {
      // Move the node
      if (j < 0|| i ! == increasingNewIndexSequence[j]) { move(nextChild, container, anchor, MoveType.REORDER) }else {
        j--
      }
    }
  }
}
Copy the code

Packaging optimization

Tree-shaking: Concepts in module packaging webpack, rollup, etc. Remove unreferenced code in the JavaScript context. It relies primarily on import and export statements to detect whether code modules are exported, imported, and used by JavaScript files.

Taking nextTick as an example, in Vue2 the global API is exposed on the Vue instance and cannot be removed by tree-shaking even if it is not used.

import Vue from 'vue'

Vue.nextTick(() = > {
  // Something DOM related
})
Copy the code

Vue3 has been refactored for both global and internal apis with tree-shaking support in mind. As a result, the global API can now only be accessed as named exports of ES module builds.

import { nextTick } from 'vue'

nextTick(() = > {
  // Something DOM related
})
Copy the code

With this change, unused apis in Vue applications will be eliminated from the final bundle for optimal file size, as long as the module binder supports tree-shaking. The global apis affected by this change are as follows.

  • Vue.nextTick
  • Ue. Observable (replaced by ue. Reactive)
  • Vue.version
  • Vue.compile (full build only)
  • Vue.set (build only)
  • Vue.delete (build only)

The internal API also has tags and instructions such as Transition and V-model exported with names. Only when the program is actually used will it be bundled.

According to UVU live, today’s Vue3 is only 22.5 KB packed with all operating functions, which is much lighter than Vue2.

Custom render API

The createApp provided by Vue3 maps template to HTML by default. However, if you want to generate a canvas, you need to customize the render generator using the Custom Renderer API.

// Custom run-time render function
import { createApp } from './runtime-render'
import App from './src/App'

createApp(App).mount('#app')
Copy the code

TypeScript support

Vue3 is rewritten by TS and has better TypeScript support than Vue2.

  • Vue2 Option APIIn option is a simple object, while TS is a type system, object-oriented syntax, not particularly matching.
  • Vue2 needvue-class-componentEnhanced VUE native components are also requiredvue-property-decoratorAdd more decorators combined with Vue features, which are cumbersome to write.

The surrounding

For details about the new syntax of the Vue3 Composition API, see the official migration documentation for links to ~.

  • Vue - cli 4.5.0
  • Vue Router 4.0
  • Vuex 4.0
  • Element plus
  • Vite

reference

Vue3.0 performance optimization to rewrite virtual Dom remember a question to think about: Vue3 is a Virtual Dom Diff array, and Vue 2 is a Virtual Dom Diff array. Vue3 is a Virtual Dom Diff array