One, foreword

First of all, a brief review of the content of June:

  • Vue2. X source code environment construction
  • Vue2. X Initialization process
  • A single-layer, deep hijacking of an object
  • Single layer, deep hijacking of arrays
  • Implementation of data broker
  • Object, array data change observation
  • Vue data rendering process introduction
  • The template generates the AST syntax tree
  • The AST syntax tree generates the render function
  • The render function generates vNodes
  • Create a real node based on the Vnode
  • The real node replaces the original node
  • Vue2. X dependent collection process analysis
  • Dependency collection and view update process (DEP and Watcher association)
  • Description of the asynchronous update process
  • Dependency collection of arrays
  • Vue lifecycle and Mixin implementation

The beginning of this article, continue Vue2. X source code diff algorithm part;


Second, problems existing in the current version

1. Analysis of initialization and update process

Vue initializes, calling the mountComponent method at mount time

// src/init.js

Vue.prototype.$mount = function (el) {
    const vm = this;
    const opts = vm.$options;
    el = document.querySelector(el); // Get the real element
    vm.$el = el; $el represents the actual element on the current page

    // If there is no render, look at template
    if(! opts.render) {// If there is no template, use element content
      let template = opts.template;
      if(! template) {// Take the entire element tag and compile the template into the render function
        template = el.outerHTML;
      }
      let render = compileToFunction(template);
      opts.render = render;
    }

    mountComponent(vm);
  }
Copy the code

In the mountComponent method, a Watcher is created

// src/lifeCycle.js

export function mountComponent(vm) {

  let updateComponent = () = >{
    vm._update(vm._render());  
  }
  // Call the hook beforeCreate before rendering the view
  callHook(vm, 'beforeCreate');

  // Render watcher: Each component has a Watcher
  new Watcher(vm, updateComponent, () = >{
    // Call the hook created when the view is updated
    callHook(vm, 'created');
  },true)

   // When the view is mounted, call hook: Mounted
   callHook(vm, 'mounted');
}
Copy the code

When the data is updated, the set method is entered

// src/observe/index.js

function defineReactive(obj, key, value) {
  // childOb is the result of a data group observation. Internal New Observe only deals with arrays or object types
  let childOb = observe(value);// Implement deep observation recursively
  let dep = new Dep();  // Add a deP for each attribute
  Object.defineProperty(obj, key, {
    // get (obj, obj, obj, obj)
    // Value looks for value in the upper scope, so the defineReactive function cannot be released and destroyed
    get() {
      if(Dep.target){
        // Dependency collection of object attributes
        dep.depend();
        // Dependency collection of arrays or objects themselves
        if(childOb){  // If childOb has a value, the data is an array or object type
          In the // observe method, new observe adds the DEP property to the array or object itself
          childOb.dep.depend();    // Make the array and object's own DEP remember the current watcher
          if(Array.isArray(value)){// If the current data is an array type
            // The array may continue to be nested in the array, need recursive processing
            dependArray(value)
          }  
        }
      }
      return value;
    },
    set(newValue) { // Make sure the new object is responsive data: if the new value is object, you need to hijack again
      console.log("Modified observed attribute key =" + key + ", newValue = " + JSON.stringify(newValue))
      if (newValue === value) return
      observe(newValue);  // Observe method: If the object is a new Observer, observe deeply
      value = newValue;
      dep.notify(); // Notify all watcher collected in the current DEP to perform view updates in turn}})}Copy the code

At this point, dep.notify() is called to notify the corresponding watcher to call the update method

class Dep {
  constructor(){
    this.id = id++;
    this.subs = [];
  }
  // Make Watcher remember deP and deP remember Watcher
  depend(){
    Dep.target.addDep(this);  
  }
  // let deP remember watcher - be called in watcher
  addSub(watcher){
    this.subs.push(watcher);
  }
  // All the watcher collected in deP executes the update method update in sequence
  notify(){
    this.subs.forEach(watcher= > watcher.update())
  }
}
Copy the code

In the Update method of the Watcher class, queueWatcher is called to cache and de-redo the Watcher

// src/observe/watcher.js

class Watcher {
  constructor(vm, fn, cb, options){
    this.vm = vm;
    this.fn = fn;
    this.cb = cb;
    this.options = options;

    this.id = id++;   // Watcher unique tag
    this.depsId = new Set(a);// The unique ID used to save the deP instance for the current watcher
    this.deps = []; // For the current watcher to save the DEP instance
    this.getter = fn; // fn is the page rendering logic
    this.get();
  }
  addDep(dep){
    let did = dep.id;
    / / dep to check again
    if(!this.depsId.has(did)){
      // Make Watcher remember deP
      this.depsId.add(did);
      this.deps.push(dep);
      // Let deP remember Watcher
      dep.addSub(this); }}get(){
    Dep.target = this;  // Record the watcher to dep.target before triggering the view render
    this.getter();      // Call the page rendering logic
    Dep.target = null;  // Clear the Watcher record after rendering
  }
  update(){
    console.log("watcher-update"."Look up and cache the watcher that needs to be updated")
    queueWatcher(this);
  }
  run(){
    console.log("watcher-run"."Actually perform view update")
    this.get(); }}Copy the code

QueueWatcher method:

// src/observe/scheduler.js

/** * Select * from watcher@param {*} Watcher to update watcher */
export function queueWatcher(watcher) {
  let id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    queue.push(watcher);  // cache watcher
    if(! pending) {// Equivalent to anti-shake
      nextTick(flushschedulerQueue);
      pending = true;     // The first entry is set to true so that the macro task is executed after the microtask has completed}}}/** * Flush the queue: execute all watcher.run and empty the queue; * /
function flushschedulerQueue() {
  BeforeUpdate, perform the life cycle: beforeUpdate
  queue.forEach(watcher= > watcher.run()) // Trigger view updates in turn
  queue = [];       // reset
  has = {};         // reset
  pending = false;  // reset
  // Update is complete
}
Copy the code

Watcher’s run method is called when the flushschedulerQueue method is executed

Run internally calls the Watcher’s get method, which records the current watcher and invokes the getter

Getter is the view update method fn passed in when Watcher initializes,

UpdateComponent view rendering logic

// src/lifeCycle.js

export function mountComponent(vm) {

  let updateComponent = () = >{
    vm._update(vm._render());  
  }
  // Call the hook beforeCreate before rendering the view
  callHook(vm, 'beforeCreate');

  // Render watcher: Each component has a Watcher
  new Watcher(vm, updateComponent, () = >{
    // Call the hook created when the view is updated
    callHook(vm, 'created');
  },true)

   // When the view is mounted, call hook: Mounted
   callHook(vm, 'mounted');
}
Copy the code

Once again, updateComponent->vm._render,

The virtual node is regenerated based on the current latest data and update is called again

// src/lifeCycle.js export function lifeCycleMixin(Vue){ Vue.prototype._update = function (vnode) { const vm = this; $el = patch(vm.$el, vnode); $el = patch(vm. }}Copy the code

Attached is a Vue flow chart:

2. Problem analysis and optimization ideas

The update method regenerates the real DOM with the new virtual node and replaces the original DOM

In the implementation of Vue, a DIff algorithm optimization will be done: reuse the original nodes as much as possible to improve the rendering performance

Therefore, patch method is the key optimization object:

The current patch method only considers the initialization, but also needs to deal with the update operation. The patch method needs to compare the old and new virtual nodes once, and reuse the original nodes as much as possible to improve the rendering performanceCopy the code
  • For the first rendering, real nodes are generated from virtual nodes, replacing the original nodes
  • Update the render to generate a new virtual node and compare it with the old virtual node and render it again

Third, simulate the comparison between new and old virtual nodes

Simulate the comparison of two virtual nodes:

  • Virtual node 1 is generated
  • Virtual node 2 is generated
  • The patch method is invoked to compare the new and old virtual nodes

1. Generate the first virtual node

For the first time, mount the virtual node directly after it is generated

// src/index.js

// 1 to generate the first virtual node
// New Vue hijacks data
let vm1 = new Vue({
    data(){
        return {name:'Brave'}}})// Generate render1 as the render function
let render1 = compileToFunction('<div>{{name}}</div>');// call compileToFunction to generate the render function, which will parse the template and package it into a function
// Call the render function to generate a virtual node
let oldVnode = render1.call(vm1)    // oldVnode: the first virtual node
// Generate a real node from a virtual node
let el1 = createElm(oldVnode);
// Render the actual node to the page
document.body.appendChild(el1);
Copy the code

2. Generate the second virtual node

// src/index.js

// 2 to generate the second virtual node
let vm2 = new Vue({
    data(){
        return {name:'BraveWang'}}})let render2 = compileToFunction('<p>{{name}}</p>');
let newVnode = render2.call(vm2);

El1 is displayed after initialization, el1 is removed after 1 second, and EL2 is displayed
setTimeout(() = >{
    let el2 = createElm(newVnode);
    document.body.removeChild(el1);
    document.body.appendChild(el2);
}, 1000);

export default Vue;
Copy the code

3. Patch method is used to compare new and old virtual nodes

Patch method: Compare the old and new virtual nodes and reuse the original nodes as much as possible to improve rendering performance

Node reuse logic: If the label name and key are the same, the node can be reused

// If the label name is the same, reuse it
// 3, call patch method for comparison
setTimeout(() = >{
    // Compare the differences between the old and new virtual nodes and reuse the original nodes as much as possible to improve rendering performance
    patch(oldVnode,newVnode); 
}, 1000);
Copy the code

4. View the existing and new nodes

let vm = new Vue({
    data(){
        return {name:'Brave'}}})let render = compileToFunction('<div>{{name}}</div>'); /let oldVnode = render.call(vm)
let el = createElm(oldVnode);
document.body.appendChild(el);

// Call the render function again after the data is updated
vm.name = 'BraveWang';
let newVnode = render.call(vm);

setTimeout(() = >{
    patch(oldVnode, newVnode); 
}, 1000);
Copy the code

View the two real nodes generated

Next, the patch method was reformed to realize node comparison and reuse


4. Patch method optimization

1. Current patch method

The current patch method only considers the initialization, so it will directly replace the old node every time

export function patch(el, vnode) {
  // 1, create a real node based on the virtual node
  const elm = createElm(vnode);
  // 2, replace the old node with the real node
  // Find the parent node of the element
  const parentNode = el.parentNode;
  // find the nextSibling of the old node (return null if nextSibling does not exist)
  const nextSibling = el.nextSibling;
  // insert the new node elm before the nextSibling of the old node el
  // Note: If nextSibling is null, insertBefore equals appendChild
  parentNode.insertBefore(elm, nextSibling); 
  // Delete the old node el
  parentNode.removeChild(el);

  return elm;
}
Copy the code

2. Patch transformation method

The two input parameters of the current patch method are: element and virtual node. Virtual nodes are created as real nodes, and element replacement is performed directly to complete data update. Now it is necessary to compare the old and new virtual nodes and reuse the original nodes as much as possible to improve rendering performance. The current patch method of oldVnode and VNode only considers the initialization. Now you also need to support data update situations;Copy the code
export function patch(oldVnode, vnode) {
  const elm = createElm(vnode);
  const parentNode = oldVnode.parentNode;
  parentNode.insertBefore(elm, oldVnode.nextSibling); 
  parentNode.removeChild(oldVnode);
  
  return elm;
}
Copy the code

Question: Initial render OR update render?

Check whether oldvnode. nodeType indicates the nodeType. If it is a real node, new and old virtual nodes need to be compared with non-real nodes, that is, when it is a real DOM, initial rendering logic is carried outCopy the code

Patch method after transformation:

export function patch(oldVnode, vnode) {
  const isRealElement = oldVnode.nodeType;
  if(isRealElement){// Real nodes, old logic
    const elm = createElm(vnode);
    const parentNode = oldVnode.parentNode;;
    parentNode.insertBefore(elm, oldVnode.nextSibling); 
    parentNode.removeChild(oldVnode);
    return elm;
  }else{// Virtual node: do diff algorithm, new and old node comparison
    console.log(oldVnode, vnode)
  }
}
Copy the code

Later, the new and old virtual nodes are compared according to the updated rendering, that is, the logic of diFF algorithm


Five, the end

In this paper, diff algorithm problem analysis and patch method transformation mainly involve the following points:

  • Initialization and update process analysis;
  • Problem analysis and optimization ideas;
  • Comparison simulation between old and new virtual nodes;
  • Patch method modification;

Next, diff algorithm – node alignment


Maintain a log

20210802: Add “iv, Patch method optimization”; Add Vue execution flowchart; Update the title and abstract of the article; 20210806: Adjust the layout and format, modify some typos and ambiguous statements;