preface

First, let’s borrow a picture to see the general flow of vUE’s responsive principle:

To understand the reactive principle, you need to know three classes: Observe, Dep, Watcher, Observe create a Dep class for each attribute, while the attribute in data is hijacked. The deP. Sub array is added with the get property, and the deP notify function is executed with the set property to notify all watcher updates.

The figure above illustrates how the component builds the virtual DOM by executing the render function: In the process of building the virtual DOM, the get method of the attribute will be triggered to collect Watcher as a dependency, and then the page will be mounted to the real DOM. When the attribute changes, the set function will be triggered, and the set function will trigger the notify function of the attribute DEP to notify Watcher and generate a new DOM tree. The real DOM is then updated by comparing it with the original DOM tree.

Thinking: how to access data directly inside the data?

The _init() method is used to add attributes to the instance, initialize data, bind data to the instance VM, create an observer instance, listen for data globally, and create a DEP instance for each attribute. Object.defineproperty add get method, set method (reactive principle will focus on analysis)

Data is initialized when instantiated. Let’s take a look at what is done:

function initData (vm) { let data = vm.$options.data data = vm._data = typeof data === 'function' // vm._data ? data() : data || {} const keys = Object.keys(data) let i = keys.length while (i--) { proxy(vm, `_data`, Observe (data, true) // Data is a reference. Function proxy (target, sourceKey, key) {// vm _data MSG object.defineProperty (target, key, { enumerable: true, configurable: true, get: Function () {return this[sourceKey][key] // this is target vm._data.msg}, set: function (val) { this[sourceKey][key] = val } }) }Copy the code

To iterate over data, bind attributes on data directly to vue instances. This. XXX is this.data.xxx

Now, how are these three classes implemented? How does it relate to each other? That’s the responsive principle

Response principle

1. The Observer

When the data attribute is changed, the page will be updated. That is to say, each attribute will correspond to an instance of the Observer mode. Therefore, we use the Observer class to manage all Observer modes of the data attribute. The Dep class acts as the target object and watcher acts as the observer.

The Observer class loops through attributes in data, creating a Dep instance for each attribute, hijacking the watcher via Object.defineProperty, collecting the watcher on get, and notifywatcher of updates on set

class Observer { constructor(value) { this.walk(value) } walk (obj) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; DefineReactive (obj, keys[I])}}} function defineReactive(obj, keys[I]) Const dep = new dep () let val = obj[key] object.defineProperty (obj, key, { enumerable: true, configurable: true, get: Function () {if (dep.target) {// dep.target = watcher // dep.depend() = dep.addSub(dep.target) dep.depend()} return val}, set: Function (newVal) {function (newVal) {// all variables assigned to data are set in render, so dep.subs is null, and watcher update if (newVal! Dep.notify () // Notifying watcher of updates}}})}Copy the code

We see that dep.depend() is collected instead of dep.addSub(dep.target). Why? Leave a question and let’s analyze it

2. Dep

Dep class

Id: The unique identifier of the DEP, one for each attribute

Subs: [watcher], which is used to record all observers of the property

let depId = 0 class Dep { constructor () { this.id = ++depId this.subs = [] } addSub (sub) { this.subs.push(sub) } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; I ++) {subs[I].update() // sub[I].update() If (dep.target) {if (dep.target) {if (dep.target) {if (dep.target) {if (dep.target) { Dep.target.addDep(this) } } } Dep.target = nullCopy the code

3. Watcher

Watcher class

Deps: [deP] maintains an array of depids, all dependent data, if the old DEP ID does not exist in the ids after the new GET collection, then the corresponding ID in the old DEP will be deleted

Getter: Expression or function

Value: The value obtained each time

There are three types of watcher in the source code: global Watcher, custom $watcher, and Conputed Watcher, regardless of computed Watcher

Global watcher

When was global Watcher defined?

New Watcher(vm, updateComponent, noop, {}) in mountComponent

New Watcher(vm, updateComponent, noop, {}) is invoked during vue instance mount ($mount). Get method (vm._update(vm._render())); render method (); Access properties in data during render execution, get hijacked by object.defineProperty’s get method (data hijacking was done during initialization), trigger dependency collection, add global Watcher to subs array

In other words, vue only listens on the data used in Render

Custom watcher

How does a custom Watcher attribute add to its own subs array?

The get method is triggered when the value is instantiated, which further triggers the GET of the property to be collected

Prototype.$mount = function (el) {const vm = this vm.$el = typeof el === 'string'? document.querySelector(el) : document.body const options = vm.$options; if (! Options. render) {// compileToFunctions will need to compile render if there is no render. // vm.options.render = compileToFunctions(vm.options.template)} // Continue mountComponent(vm) // as defined in lifecycle lifecycle.js function mountComponent (vm) { // callHook(vm, 'beforeMount') // Lifecycle let updateComponent = function () {vm._update(vm._render()) // Execute _render() to get vNode, Watcher new Watcher(vm, updateComponent, noop, {}) // noop is an empty function that should pass cb callback, // updateComponent() is used as a callback function, } class constructor {url (vm, expOrFn, cb, options) {// 1. Cb) expOrFn === MSG = expOrFn === MSG = expOrFn === MSG In the global Watcher, ExpOrFn === updateComponent function this.vm = vm this.cb = cb this.expOrFn = expOrFn this.depids = new Set() // used for deP undoing This.val = this.get() // trigger dependency collection} // dep for key, Get () {let value const vm = this.vm dep. target = this // Change to self if (typeof this.exporfn === 'function') {// Url = this.url = this.exporfn value = this.gett. call(VM) // perform updateComponent, $watcher(' MSG ') = this.exporfn // key === MSG console.log(8989,key) value = Vm. _data[key] // This step triggers the attribute get, in defineReactive, } dep.target = null // Clean up watcher this.cleanupDeps() Update () {this.run()} run() {const vm = this.vm const val = this.get() // Each time a property is modified, this.get() is executed again, triggering a new render collection dependency;; New and old dependent DEPs are cleaned up through cleanupDeps if (val! == this.val) { const oldVal = this.val this.val = val this.cb.call(vm, val, }} addDep (dep) {const id = dep.id if (! This.depids. has(id)) { Add (id) dep.addSub(this) // dep add watcher instance to console.log(12, this.depids, Dep)}} // We do not implement it here, but it is useful, if the old DEP ID does not exist in the ids after the new GET collection, // let I = this.deps.length // while (I --) {// const dep = this.deps[I] // if (! this.newDepIds.has(dep.id)) { // dep.removeSub(this) // } // } // let tmp = this.depIds // this.depIds = this.newDepIds // this.newDepIds = tmp // this.newDepIds.clear() // tmp = this.deps // this.deps = this.newDeps // this.newDeps = tmp // this.newDeps.length = 0 } }Copy the code

This is just how Watcher is collected, but how does it trigger updates?

When the value of an attribute is changed, the set method is executed to notify the watcher of the update.

This. Value = get () triggers the render function, recollects dependencies, and displays the update on the page

Customizable watcher, this.get() compares the latest value with the previous value, and executes its own callback if it is inconsistent

Doubt ❓

Add (Watcher) collects dependencies in the Watcher class, instead of adding dependencies directly to the DEP when the property get is added.

What we thought:

This. XXX -> get -> dep.addSub(watcher) dep.target is watcher

Actual:

this.xxx -> get -> dep.depend -> dep.target.addDep(dep) -> dep.addSub(watcher)

Dependencies are collected every time get is triggered.

< div style = “box-color: block; color: block; display: block; display: block; display: block; display: block; display: block; display: block;

  1. Maintain the watcherId array in the DEP. The collected watcherIDS are not collected anymore. It looks fine, but it’s not done

DepId = deP; depId = deP; depId = deP; depId = deP; depId = deP; depId = deP; depId = deP; Dep. AddSub (watcher); depId = depId; depId = depId; depId = depId; depId = depId

Why choose option two? Why collect dependencies in Watcher?

Why collect dependencies in Watcher?

// if = true -> false; // If = true; // If = true; <p v-if="false">{{this.name}}</p>Copy the code

Since it starts true, the this.name property collects watcher, and updates are triggered when this.name changes. What about false? If you do not clean it, it will trigger an update after modification, which will waste performance.


To clean up the watcher in a DEP, we need to know which dePs changed from true to false, so we need to maintain an array of DEPids (where each deP has a unique depId for each attribute in the data). Compare last render with this render, which DEPS are missing, where is the DEP array maintained? The depId array is maintained in watcher because all attributes accessed in render are collected in Watcher, and the callback function that triggers the page rendering is executed in Watcher. Second is to clean up the last render time and this render time, missing the DEP inside watcher


Let’s see if we can do that in the DEP, put a flag on the DEP to show it or not to show it, clear the Watcher?

This. Name does not appear in the render function, so get will not be triggered, and the corresponding deP identifier of this.name will not be changed

Conclusion:

DepId = depId = depId = depId = depId = depId = depId = depId = depId = depId = depId = depId = depId = depId = depId = depId = depId = depId = depId DepId = depId; depId = depId; depId = depId; depId = depId

Simple code

class Observer { constructor(value) { this.walk(value) } walk (obj) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; DefineReactive (obj, keys[I])}}} function defineReactive(obj, keys[I]) Const dep = new dep () let val = obj[key] object.defineProperty (obj, key, { enumerable: true, configurable: true, get: Function () {if (dep.target) {// dep.target = watcher // dep.depend() = dep.addSub(dep.target) dep.depend()} return val}, set: Function (newVal) {function (newVal) {// all variables assigned to data are set in render, so dep.subs is null, and watcher update if (newVal! == val) {val = newVal // DepId = 0 class dep {constructor () {this.id = ++depId this.subs = []  } addSub (sub) { this.subs.push(sub) } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; I ++) {subs[I].update() // sub[I].update() If (dep.target) {if (dep.target) {if (dep.target) {if (dep.target) {if (dep.target) { Prototype.$mount = function (el) {const vm = this  vm.$el = typeof el === 'string' ? document.querySelector(el) : document.body const options = vm.$options; if (! Options. render) {// compileToFunctions will need to compile render if there is no render. // vm.options.render = compileToFunctions(vm.options.template)} // Continue mountComponent(vm) // as defined in lifecycle lifecycle.js function mountComponent (vm) { // callHook(vm, 'beforeMount') // Lifecycle let updateComponent = function () {vm._update(vm._render()) // Execute _render() to get vNode, Watcher new Watcher(vm, updateComponent, noop, {}) // noop is an empty function that should pass cb callback, // updateComponent() is used as a callback function, } class constructor {url (vm, expOrFn, cb, options) {// 1. Cb) expOrFn === MSG = expOrFn === MSG = expOrFn === MSG In the global Watcher, ExpOrFn === updateComponent function this.vm = vm this.cb = cb this.expOrFn = expOrFn this.depids = new Set() // used for deP undoing This.val = this.get() // trigger dependency collection} // dep for key, Get () {let value const vm = this.vm dep. target = this // Change to self if (typeof this.exporfn === 'function') {// Url = this.url = this.exporfn value = this.gett. call(VM) // perform updateComponent, $watcher(' MSG ') = this.exporfn // key === MSG console.log(8989,key) value = Vm. _data[key] // This step triggers the attribute get, in defineReactive, } dep.target = null // Clean up watcher this.cleanupDeps() Update () {this.run()} run() {const vm = this.vm const val = this.get() // Each time a property is modified, this.get() is executed again, triggering a new render collection dependency;; New and old dependent DEPs are cleaned up through cleanupDeps if (val! == this.val) { const oldVal = this.val this.val = val this.cb.call(vm, val, }} addDep (dep) {const id = dep.id if (! This.depids. has(id)) { Add (id) dep.addSub(this) // dep add watcher instance to console.log(12, this.depids, Dep)}} // We do not implement it here, but it is useful, if the old DEP ID does not exist in the ids after the new GET collection, // let I = this.deps.length // while (I --) {// const dep = this.deps[I] // if (! this.newDepIds.has(dep.id)) { // dep.removeSub(this) // } // } // let tmp = this.depIds // this.depIds = this.newDepIds // this.newDepIds = tmp // this.newDepIds.clear() // tmp = this.deps // this.deps = this.newDeps // this.newDeps = tmp // this.newDeps.length = 0 } }Copy the code