What is change detection

Vue.js automatically generates the DOM from the state and outputs it to the page for display, a process called rendering. The rendering process of vue.js is declarative, and we use templates to describe the mapping between state and DOM.

Typically, the internal state of the application is constantly changing at run time, requiring constant re-rendering. How do you determine what’s happening in the state?

Change detection is designed to solve this problem, and it comes in two types: push and pull.

Change detection in Angular and React is a “pull,” which means that when a state changes, it doesn’t know which state has changed, only that it might. It sends a signal to the framework and, upon receiving the signal, it performs a violence comparison to find out which DOM nodes need to be rerendered. This is the process of dirty checking in Angular; React uses the virtual DOM.

Vue. Js change detection is “push”. Vue.js immediately knows when a state has changed, and to some extent knows which states have changed. As a result, it knows more information and can make more fine-grained updates.

More granular updating means that if you have a state with multiple dependencies, each representing a specific DOM node, all dependencies in that state are notified to update the DOM when that state changes. Comparatively speaking, “pull” granularity is the coarsest.

But it comes at a cost, because the finer the granularity, the more dependencies each state is bound to, and the more memory overhead dependency tracing becomes. Thus, starting with vue.js 2.0, it introduced the virtual DOM and adjusted the granularity to medium granularity, that is, the dependency bound to a state is no longer a concrete DOM node, but a component. This state change is notified to the component, which is then compared internally using the virtual DOM. This can greatly reduce the number of dependencies and thus the memory consumed by dependency tracing.

The arbitrary granularity of vue.js is essentially due to change detection. Because push type change detection can be fine-tuned at will.

How to track change

Object.defineproperty and proxies in ES6

Observer

The Observer class is attached to each detected object. Once attached, an Observer converts all properties of an object into getters/setters. To collect the properties’ dependencies and notify them when the properties change

import Dep from './Dep';

export class Observer {

    constructor(value) {
        this.value = value;
        if (!Array.isArray(value)) {
            this.walk(value); }}walk(obj) {
        const keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }
}

function defineReactive(data, key, val) {
    if (typeof val === 'object') {
        new Observer(val);
    }
    let dep = new Dep();
    Object.defineProperty(data, key, {
        enumerable: true.configurable: true.get() {
            dep.depend();// Collect dependencies
            return val
        },
        set(newVal) {
            if (val === newVal) {
                return
            }
            val = newVal;
            dep.notify();// Trigger dependencies}})}Copy the code

Dep

It is used to collect dependencies, delete dependencies, send messages to dependencies, and so on.

import { Watcher } from "./Watcher";

export  class Dep {
    target; //target: ? Watcher;
    constructor() {
        this.subs = [];
    }

    addSub(sub) {
        this.subs.push(sub);
    }

    removeSub(sub) {
        remove(this.subs, sub);
    }
  
    depend(){
        if(this.target instanceof Watcher){
            this.addSub(this.target); }}notify(){
        const subs=this.subs.slice();
        for (let i = 0; i < subs.length; i++) { subs[i].update(); }}}function remove(arr, item) {
    if (arr.length) {
        const index = arr.findIndex(item);
        if (index > -1) {
            this.subs.splice(index, 1); }}}Copy the code

Watcher

Watcher is a mediator role that notifies it when data changes, and then notifies the rest of the world.

import { Dep } from "./Dep";

export class Watcher {
    constructor(vm, expOrFn, cb) {
        this.vm = vm;// VM refers to the current Vue instance
        this.getter = parsePath(expOrFn);
        this.cb = cb;
        this.value = this.get();// Reads the value in vm.$data and fires the getter on the property
    }

    get() {
        // Watcher sets itself to a globally unique location, in this case dep.target
        Dep.target = this;
        // Read the data and trigger the getter for the data. So the Observer collects dependencies, collecting this Watcher into the Dep, which is a dependency collection.
        let value = this.getter.call(this.vm, this.vm);
        // Clear the dep.target
        Dep.target = null;
        // Return the value of the data read
        return value
    }

    update() {
        // After data changes, Dep circulates notifications to dependencies in turn. After receiving notifications, the old data is retrieved first
        const oldValue = this.value;
        // Then get the latest value
        this.value = this.get();
        // Pass the old and new values to the callback function
        this.cb.call(this.vm, this.value, oldValue); }}const bailRE = /[^\w.$]/
export function parsePath(path) {
    if (bailRE.tetx(path)) {
        return
    }
    const segments = path.split('. ')
    return function (obj) {
        for (let i = 0; i < segments.length; i++) {
            if(! obj) {return; }
            obj = obj[segments[i]]
        }
        returnobj; }}Copy the code

Review summary

Change detection detects changes in data. Be able to detect and notify when data changes.

Object can track changes by converting a property to a getter/setter via Object.defineProperty. Getters fire when data is read, setters fire when data is modified.

We need to collect in the getter which dependencies use the data. When a setter is fired, it notifies the dependent data collected in the getter that has changed.

Collecting dependencies requires finding a place to store dependencies for dependencies, so we created a Dep that collects dependencies, removes dependencies, sends messages to dependencies, and so on.

The so-called dependency is actually Watcher. Only getters triggered by the Watcher collect dependencies, and whichever Watcher fires the getter is collected into the Dep. When the data changes, we loop through the dependency list, notifying all the Watchers.

Watcher works by setting itself to a globally unique location (such as window.target) and then reading data. Because the data is read, the getter for that data is triggered. The Watcher that is currently reading is then read from the globally unique location in the getter and collected into the Dep. In this way, Watcher can actively subscribe to any data changes.

In addition, we create an Observer class that converts all data (including children) in an object to reactive, that is, it detects changes to all data (including children) in the object.

Since JavaScript did not provide metaprogramming capabilities prior to ES6, new attributes and deleted attributes on objects cannot be traced.

The relationship between Data, Observer, Dep and Watcher

Data is converted into getters/setters via observers to track changes.

When the outside world reads data through the Watcher, the getter is triggered to add the Watcher to the dependency.

When data changes, setters are triggered to send notifications to dependencies (Watcher) in the Dep.

When Watcher receives a notification, it sends a notification to the outside world. The change notification may trigger a view update or a callback function of the user.

References:

  • Blog posts

  • Vue. Js object change detection section