I believe that after reading this article, you and I have the same idea.

The Observer Pattern is used when there is a one-to-many relationship between objects. For example, when an object is modified, dependent objects are automatically notified. The observer model belongs to the behavioral model.

In order to implement the data changes affect the view, VUE uses the observer mode to associate the data with the page rendering. Dependencies are collected through the DEP, and when data changes, the corresponding Watcher is notified to update the view.

How does that work?

1. Create render Watcher and initialize it

We’ve wrapped the ability to update views in a Watcher

export function lifecycleMixin() {
    Vue.prototype._update = function (vnode) {}}export function mountComponent(vm, el) {
    vm.$el = el;
    let updateComponent = () = > {
        // Render the virtual node to the page
        vm._update(vm._render());
    }
    new Watcher(vm, updateComponent, () = > {}, true);
}

Copy the code
class Watcher {
    // vm,updateComponent,()=>{console.log(' updated view ')},true
    constructor(vm,exprOrFn,cb,options){
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        this.cb = cb;
        this.options = options;
        this.id = id++;

        // By default, exprOrFn should execute exprOrFn. What does exprOrFn do? Render (go to vm for value)

        this.getter = exprOrFn; 
        this.deps = []; 
        this.depsId = new Set(a);this.get(); // Default initialization takes a value
    }
    get(){ // The getter method can be called again later when the user updates
        // defineproperty.get, each property can collect its own watcher
        // I want one property to correspond to multiple Watcher, and one watcher to correspond to multiple properties
        pushTarget(this); // Dep.target = watcher
        this.getter(); // the render() method goes to the vm and values vm._update(vm._render).
        popTarget(); // Dep.target = null; If dep. target has a value, this variable is used in the template
    }
    update(){ // Update operations in VUE are asynchronous
       // Every time you update this
       queueWatcher(this); // I want to cache watcher and update it later
    }
    run(){ // Follow up with other functions
        this.get();
    }
    addDep(dep){
        let id = dep.id;
        if(!this.depsId.has(id)){
            this.depsId.add(id);
            this.deps.push(dep);
            dep.addSub(this)}}}Copy the code

2. Store watcher at render time

Call deP’s pushTarget method to store watcher

class Watcher{
    // ...
    get(){
        pushTarget(this);Dep. Target = watcher Stores the current watcher
        this.getter(); popTarget(); }}Copy the code
let id = 0;
class Dep{
    constructor(){
        this.id = id++; }}let stack = [];
export function pushTarget(watcher){
    Dep.target = watcher;
    stack.push(watcher);
}
export function popTarget(){
    stack.pop();
    Dep.target = stack[stack.length-1];
}
export default Dep;
Copy the code

3. Dependency collection of objects

  1. Properties that are used for page rendering in vUE, need to be collected by dependency collection, collection of object renderingwatcher
  2. When I value, I add one to each of the attributesdepProperty to store the renderwatcher(The same Watcher can have multiple DEPs).
    • watchergetThe method callrender.renderWay to govmValue up, value is calleddefinePropertygetMethods.
  3. dep.depend()= > notificationdepstorewatcher= >Dep.target.addDep()= > notificationwatcherstoredepRealize bidirectional storage.
let dep = new Dep();
Object.defineProperty(data, key, {
    get() {
        if(Dep.target){ // If the value is watcher
            dep.depend(); // Let Watcher save dep, and let deP save watcher
        }
        return value
    },
    set(newValue) {
        if (newValue == value) return;
        observe(newValue);
        value = newValue;
        dep.notify(); // Notify render Watcher to update}});Copy the code

Dep implementation

Each DEP has an ID unique identifier, and adding a DEP to watcher can be de-weighted by id. The deP is used to collect relationships, and each attribute is assigned a DEP, which can hold the Watcher.

let id = 0;
class Dep{ // Each attribute is assigned a deP. The deP can store the watcher, and the watcher also stores the DEP
    constructor(){
        this.id = id++;
        this.subs = []; // Use it to store watcher
    }
    depend(){
        // dep. target = Dep; // dep. target = Dep
        if(Dep.target){
            Dep.target.addDep(this); }}addSub(watcher){
        this.subs.push(watcher);
    }
    notify(){
        this.subs.forEach(watcher= >watcher.update());
    }
}
Dep.target = null; / / a

export function pushTarget(watcher) {
    Dep.target = watcher;
}

export function popTarget(){
    Dep.target = null
}


export default Dep
Copy the code

Watcher implementation

Store dePs in watcher (one DEP for each attribute, multiple dePs for each attribute)

import { popTarget, pushTarget } from "./dep";
import { queueWatcher } from "./scheduler";

let id = 0;
class Watcher {
    // vm,updateComponent,()=>{console.log(' updated view ')},true
    constructor(vm,exprOrFn,cb,options){
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        this.cb = cb;
        this.options = options;
        this.id = id++;

        // By default, exprOrFn should execute exprOrFn. What does exprOrFn do? Render (go to vm for value)

        this.getter = exprOrFn; 
        this.deps = []; 
        this.depsId = new Set(a);this.get(); // Default initialization takes a value
    }
    get(){ // The getter method can be called again later when the user updates
        // defineproperty.get, each property can collect its own watcher
        // I want one property to correspond to multiple Watcher, and one watcher to correspond to multiple properties
        pushTarget(this); // Dep.target = watcher
        this.getter(); // the render() method goes to the vm and values vm._update(vm._render).
        popTarget(); // Dep.target = null; If dep. target has a value, the variable is used in the template. For example, IF I'm outside of vue, the vm. XXX value does not need to collect watcher
    }
    update(){ // Update operations in VUE are asynchronous
       // Every time you update this
       queueWatcher(this); // I want to cache watcher and update it later
    }
    run(){ // Follow up with other functions
        this.get();
    }
    addDep(dep){
        let id = dep.id;
        // Watcher did some deweighting when adding dep
        if(!this.depsId.has(id)){
            this.depsId.add(id);
            this.deps.push(dep);
            dep.addSub(this)}}}Copy the code

4. Dependency collection of arrays

  1. To an array of__ob__Add adepFor storagewatcher
  2. Called when the array variation method is calleddep.notify()To notifywatcherupdate
  3. If it is an array nested array, passdependArray()Recursive collectionwatcher
this.dep = new Dep(); // Designed specifically for arrays
if (Array.isArray(value)) {
	value.__proto__ = arrayMethods;
	this.observeArray(value);
} else {
	this.walk(value);
}	

function defineReactive(data, key, value) {
    let childOb = observe(value);
    let dep = new Dep();
    Object.defineProperty(data, key, {
        get() {
            if(Dep.target){
                dep.depend();
                if(childOb){ 
                    childOb.dep.depend(); // Collect array dependencies}}return value
        },
        set(newValue) {
            if (newValue == value) return;
            observe(newValue);
            value = newValue;
            dep.notify();
        }
    })
}


arrayMethods[method] = function (. args) {
    	// ...
        ob.dep.notify()
        return result;
}

Copy the code

Recursively collect array dependencies

if(Dep.target){
    dep.depend();
    if(childOb){
        childOb.dep.depend(); // Collect array dependencies
        if(Array.isArray(value)){ // If the inside is still an array
            dependArray(value);// Keep collecting dependencies}}}function dependArray(value) {
    for (let i = 0; i < value.length; i++) {
        let current = value[i];
        current.__ob__ && current.__ob__.dep.depend();
        if (Array.isArray(current)) {
            dependArray(current)
        }
    }
}
Copy the code

Vue asynchronous update nextTick

It is obviously not reasonable to update the view every time the data is changed. So vue view updates are asynchronous, call update several times, first cache watcher in queue, deduplicate by ID, then update together.

1. Implement the queue mechanism

update(){
    queueWatcher(this);
}
Copy the code
import { nextTick } from ".. /utils";

let queue = [];
let has = {}; // Do the list maintenance to store the watcher
function flushSchedulerQueue(){
    for(let i =0 ; i < queue.length; i++){
        queue[i].run(); // vm.name = 123?
    }
    queue = [];
    has = {};
    pending = false;
}

let pending = false;

// Wait for synchronous code to complete before executing asynchronous logic
export function queueWatcher(watcher) {
// I want to update the page as soon as possible before clearing the macro task
    const id = watcher.id; 
    if (has[id] == null) {
        queue.push(watcher);
        has[id] = true;
        // Start batch update operation (anti-shake)
        if(! pending){ nextTick(flushSchedulerQueue,0);
            pending = true; }}}Copy the code

2. Implementation principle of nextTick

const callbacks = [];
function flushCallbacks() {
  callbacks.forEach((cb) = > cb());
  waiting = false;
}
//nextTick is asynchronous
// Vue2 takes compatibility into account
//1. Support promise, promise.then
MutationObserver flushCallbacks mutationObserver flushCallbacks mutationObserver flushCallbacks
//3.setImmediate
//4.setTimeout
Vue3 no longer considers compatibility issues
let timerFn = () = > {};
if (Promise) {
  timerFn = () = > {
    Promise.resolve().then(flushCallbacks);
  };
} else if (MutationObserver) {
  let textNode = document.createTextNode(1);
  let observe = new MutationObserver(flushCallbacks);
  observe.observe(textNode, {
    characterData: true}); timerFn =() = > {
    textNode.textContent = 3;
  };
} else if (setImmediate) {
  timerFn = () = > {
    setImmediate(flushCallbacks);
  };
} else {
  timerFn = () = > {
    setTimeout(flushCallbacks);
  };
}

let waiting = false;
export function nextTick(cb) {
  callbacks.push(cb);
  if(! waiting) { timerFn(flushCallbacks,0);
    waiting = false; }}Copy the code

summary

At this point, we have implemented the dependency collection principle.

In reactive data, arrays and objects are treated in two different ways. So are the collections that now depend on.

Does it bother you?