@[TOC]


Learning goals

  1. What is reactive
  2. VueHow do we know we’re up to date
  3. Analog data responsiveness
  4. By readingVue2Source code, understand Vue two-way data binding principle, can pull with the interviewer
  5. What is theWatcher
  6. How many types of Watcher are there in VUE
  7. What is theDep

The learning process

Data responsiveness simply means that the target (view) updates automatically when the dependency changes.

So, to understand data responsiveness, let’s first try to make the data change automatically.

1. Let a number change

Simulated data responsiveness, where target changes when data dependencies change. Such as:

let c = a+b;
Copy the code

Here we call C target, a,b is a dependency of C, because C is derived from A and B.

So how do we get c to change when A changes or b changes?

1.1 The first thing THAT comes to my mind is to put C in a function. When a or B changes, we can call that function and let C remake it.

Let’s test my idea.

"use strict";

let a = 1
let b = 2
let c = null;

function fn(){
    	c =a+b;
}
console.log('1',a,b,c)         // 1- 1 2 null
// c is null

// Call the function to initialize c
fn()

console.log('2 -,a,b,c)         // 2
// At this point c=3

// Dependencies change
a = 9
console.log('3',a,b,c)         // 3
// Here a changes, but c does not

fn()
console.log('4',a,b,c)         // 4
// c=11 after fn is called
Copy the code

Obviously at this point, calling target manually does update, but it feels weird to call target manually all the time. Is there any way to make it call automatically?


Here we have to solve two problems:

1. How to know automatic data changes 2. How to automatically call a specific functionCopy the code

1.2 The way I came up with is from the little Red BookObject.defineProperty(), the use ofObject.defineProperty()thegetterAs well assetterInterception feature, let us test it.

"use strict";

function say() {
console.log('hello')}function defineReactive(obj, key, val) {
return Object.defineProperty(obj, key, {
   get() {
       console.log('get->', key)
       return val
   },

   set(newVal) {
       if (newVal === val) return;
       console.log(`set ${key} from ${val} to ${newVal}`)

       // When the data changes, we call the function
       say()
       val = newVal
   }
})
}

let source = {}
defineReactive(source, 'a'.1)
console.log(source.a)
source.a = 99
console.log(source.a)
Copy the code

Results:

As you can see, our get and set on A have been identified, and our say function has been called successfully.

So how do we reproduce the example where c is equal to a plus b? As follows:

"use strict";
function defineReactive(obj, key, val) {
return Object.defineProperty(obj, key, {
   get() {
       console.log('get->', key)
       return val
   },

   set(newVal) {
       if (newVal === val) return;
       console.log(`set ${key} from ${val} to ${newVal}`)
       // When the data changes, we call the function
       fn()
       val = newVal
   }
})
}

let source = {}
defineReactive(source, 'a'.1)
defineReactive(source, 'b'.2)

let c;
function fn(){
c = source.a + source.b;
console.log('c 'automatically changes to'. ',c)
}

// initialize c
fn()

source.a = 99



Copy the code

We find that fn is automatically called, but the value of c is still 3. How should we solve this problem?

After checking, it was found that there was a problem in set (fn was called first, so val had not had time to change).

set(newVal) {
    if (newVal === val) return;
    console.log(`set ${key} from ${val} to ${newVal}`)
    // When the data changes, we call the function
    val = newVal
    fn()
}
Copy the code

The above problem is solved.

2. Make an object change

Because the object operation is more complex, so we first implement the interception of the object operation, such as object acquisition and setting I know.

2.1 Intercepting the acquisition and setting of object properties

Add observe to redefine an object’s property traversal (similar to defining a data variable)

"use strict";
/** * Where defineReactive is actually a closure, the outer side references variables inside the function, causing these temporary variables to persist */
function defineReactive(obj, key, val){
    Observe that val is an object. The value in the object has no response
    observe(val)
    // Use getter setters to intercept data
    Object.defineProperty(obj,key, {
        get(){
            console.log('get', key)
            return val
        },
        set(newVal){
            if( newVal ! == val){console.log(`set ${val} -> ${newVal}`)
                val = newVal
            }
        }
    })
}

// 2. Observe an object and make its properties responsive
function observe(obj){
    // We want to pass in an Object
    
    if( typeofobj ! = ='object' || typeof(obj) == null) {return ;
    }
    Object.keys(obj).forEach(key= >{
        defineReactive(obj, key, obj[key])
    })
}

let o = { a: 1.b: 'hello'.c: {age:9}}
observe(o)

o.a
o.a = 2
o.b
o.b = 'world'
Copy the code

2.2 Let object properties change

To simplify the program, let’s look at only one layer of objects

"use strict";
const { log } = console;

let target = null;
let data = { a: 1.b:2 }
let c, d;

// Depending on collection, each Object key has a Dep instance
class Dep{
    constructor(){
        this.deps = []
    }
    depend(){
        target && !this.deps.includes(target) && this.deps.push(target)
    }
    notify() {
        this.deps.forEach(dep= >dep() )
    }
}

Object.keys(data).forEach(key= >{
    let v = data[key]
    const dep = new Dep()

    Object.defineProperty(data, key, {
        get(){
            dep.depend()
            return v;
        },
        set(nv){
            v = nv
            dep.notify()
        }
    })
})

function watcher(fn) {
    target = fn
    target()
    target = null
}

watcher(() = >{
    c = data.a + data.b
})

watcher(() = >{
    d = data.a - data.b
})

log('c=',c)
log('d=',d)
data.a = 99
log('c=',c)
log('d=',d)

/** c= 3 d= -1 c= 101 d= 97 */
Copy the code
  • To summarize the process:

Intercept data with defineProperty for each key in data object, collect Dep dependencies in GET, and notify data updates in set.

In fact, the dependency collection is to add the Watcher instance to the DEPS queue, and execute the function in the queue when it receives an update notification to achieve the effect of automatic data update.

3. Read source code

While reading the source code, let’s first take a look at the official website’s interpretation of the data responsiveness for ease of entry.

The granularity of Watcher is component, that is, each component corresponds to a Watcher.

So what is Watcher? What is Dep? What does an Observer do? Let’s go to the source code to find the answer.

The test code

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
    <script src='.. /dist/vue.js'></script>
</head>
<body>
    <div id="app">
        {{a}}
    </div>

    <script>
        const app = new Vue({
            el:'#app'.data: {
                a: 1
            },
            mounted(){
                setInterval(() = >{
                    this.a ++
                }, 3000)}})</script>
</body>
</html>
Copy the code

Trace call stack

A flow chart drawn by myself

talk is cheap, show me the code

In particular, to simplify the process, all source code display, have been deleted

src\core\instance\index.js

An entry file

function Vue (options) {
  /** * initializes */
  this._init(options)
}
/** * The following uses the vue. prototype mount method to mix in other methods */
initMixin(Vue)    
/** * initMixin provides an __init method to Vue. Initialize the lifecycle initLifecycle -> initEvents -> initRender -> callHook(vm, 'beforeCreate') -> initInJections -> initState -> initProvide -> callHook(vm, 'created') -> if (vm.$options.el) { vm.$mount(vm.$options.el) } */

stateMixin(Vue)  
/** stateMixin $data -> $props -> $set -> $delete -> $watch */

eventsMixin(Vue)   
/** eventsMixin $on $once $off $emit */

lifecycleMixin(Vue) 
/** lifecycleMixin * _update(), $forceUpdate, $destroy * */
renderMixin(Vue)
/** * $nextTick, _render, $vnode * * */     

export default Vue

Copy the code

src\core\instance\init.js

/ * *

* initMixin

With this method, Vue is provided with an __init method to initialize the lifecycle

initLifecycle -> initEvents -> initRender

-> callHook(vm, ‘beforeCreate’) -> initInJections

-> initState -> initProvide

-> callHook(vm, ‘created’)

-> if (vm.$options.el) {

vm.
m o u n t ( v m . mount(vm.
options.el)

}

* /

export function initMixin (Vue: Class<Component>) {

    
    $reject $delete $reject $delete $reject $reject $delete $reject $reject $delete $reject $reject $delete $reject $reject $delete $reject $reject $delete $reject
    initState(vm)

    /** * creates the hook function */
    callHook(vm, 'created')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
Copy the code

src\core\instance\state.js\initState

Data is preprocessed

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  /** * state * props -> methods -> data -> computed -> watch */
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) { initData(vm) }
  if (opts.computed) initComputed(vm, opts.computed)
}

Copy the code

src\core\instance\state.js\initData

Observe data, getter data,setter interception data

function initData (vm: Component) {
  let data = vm.$options.data
  // proxy data on instance
  /**
   * 数据代理
   */
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
      /** * @proxy */
      proxy(vm, `_data`, key)

  }
  /** * @reactive operation */
  observe(data, true /* asRootData */)}Copy the code

“src\core\instance\state.js\proxy`

const sharedPropertyDefinition = {
  enumerable: true.configurable: true.get: noop,
  set: noop
}


/** * proxy(vm, '_data', key) 168 lines */
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code

src\core\observer\index.js\observe

Creating an Observer Instance

export function observe (value: any, asRootData: ? boolean) :Observer | void {
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  /** * @observer */
  let ob: Observer | void
  ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
  
}
Copy the code

SRC \ core \ observer \ index js \ the observer

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value

    /** * @why declare deP in Observer * create deP instance * Add/remove property in Object * change method in array */
    this.dep = new Dep()
    this.vmCount = 0

    /** * Sets an __ob__ attribute referencing the current Observer instance */

    /** * export function def (obj: Object, key: string, val: any, enumerable? : boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !! enumerable, writable: true, configurable: true }) } */
    def(value, '__ob__'.this)

    /** * Type check */
    / / array
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      /** * if the elements in the array are still objects, we need to do reactive processing */
      this.observeArray(value)

    } else {
      // is an object
      this.walk(value)
    }
  }

  /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value  type is Object. */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /** * Observe a list of Array items. */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
Copy the code

SRC \ core, the observer, dep. Js \ dep class

Depend on the collection

Subs is a Watcher queue

/** * A DEP is an Observable that can have multiple * directives subscribing to it. * /
export default class Dep {
  statictarget: ? Watcher; id: number; subs:Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    Dep.target && Dep.target.addDep(this)
  }

  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Copy the code

SRC \ core \ observer \ watcher js \ watcher

/** * The observer parses the expression, collects dependencies, and triggers a callback when the expression value changes. * This is used for the $watch() API and directives. * /
export default class Watcher {
  constructor () {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)

    this.expression = process.env.NODE_ENV ! = ='production'
      ? expOrFn.toString()
      : ' '
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop process.env.NODE_ENV ! = ='production' && warn(
          `Failed watching path: "${expOrFn}"` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /** * Evaluate the getter, and re-collect dependencies. */
  get () {
    pushTarget(this)... popTarget()this.cleanupDeps()
    return value
  }

  /** * Add a dependency to this directive. */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)}}}/** * Clean up for dependency collection. */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)}}}/** * Subscriber interface. * Will be called when a dependency changes. */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}/** * Scheduler job interface. * Will be called by the scheduler. */
  run () {
    if (this.active) {
      const value = this.get()
      if( value ! = =this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "The ${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /** * Depend on all deps collected by this watcher. */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /** * Remove self from all dependencies' subscriber list. */
  teardown () {
    if (this.active) {
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)}let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)}this.active = false}}}Copy the code

Learning outcomes

The declaration of data, unless otherwise specified, refers to the predefined data object that was defined when the component was defined.

1. What is reactive

Take C =a+b to show that when a or B changes, C will also change, which is the essence of the data response.

2. VueHow do I know when the data is updated?

Vue2. X redefines user-defined data using Object.defineProperty before mounting it to the component. When the component retrieves or updates the data, it fires getters or setters to let the component know that the user “acted on” the data.

3. What isWatcher?

A component in Vue corresponds to a Watcher, which we call a “subscriber.”

It is used to subscribe to data changes and perform operations (such as update views).

4. What isDep

Each key of the component data object corresponds to a Dep instance.

Dep is an observable class that Watcher can subscribe to once it is instantiated.

Vue does dependency collection in the Dep.

Dep has a class attribute, Target, that holds the Watcher subscriber, which is key to the dependency collection.

  1. Dep.depend () is called when the data property is got.
  2. In dep.depend, if dep. target exists, the corresponding Watcher is told to add the dependency
  3. When data’s property key is set (that is, when it is updated), the corresponding dep.notify() is called. Notify calls all Watcher subscribers to the property for uOdate updates

5. What isObserver?

An Observer instance is created when the component calls Observe (data), which redefines data with objce. property and then mounts it to the component.

We call it “observer”, because he converts data into a responsive data by observing data. Some people joke that data has since gone through the “baptism of socialism” and has become a mature successor of socialism. It is worth noting that for each Observer instance, there is also a unique Dep instance corresponding to it.

6. How many different Watchers are there in Vue?

Since Watcher is for updates, we can think about how many scenarios in Vue need to be updated.

Such as:

  • Data changes → View using data changes
  • Data changes → Computational properties using data change → views using computational properties change
  • Data change → the developer actively registers the watch callback function execution

Three scenarios, corresponding to three types of Watcher:

  • Component Watcher, i.e., render-watcher

  • User-defined computed attributes correspond to computed-watcher

  • Watcher (watch-API or Watch property) for user-defined listening properties

Interview question: Please talk about your understanding of the data responsiveness principle

First of all, we can take a look at C =a+ B. In this equation, C is our target data, and both A and B are dependent on C. When a or B changes, C will automatically update, which is the data response formula I understand.

In Vue, Dep, Watcher and Observer classes are mainly related to data responsiveness.

When we create the component, vue observes the user’s predefined data, creates an Observer instance, and redefines the data Object with the getter and setter of Object.defineProperty() so that all operations performed on the data are performed. Can be detected by the component.

And then when we get the Watcher instance, we call the get function in Watcher, and the get function pushes the currently triggered dependency into the targetStack, and then fires that dependency, the getter for obj. Key that we defined earlier, If dep. target is not empty depend is called for dependency collection.

When we change the data again, the triggered setter for obj.key calls the notify function of the corresponding Dep instance. Notify traverses the subscriber queue and calls all the update functions subscribed to the Keydependent Watcher to update the data. So as to achieve the purpose of data response.

May I ask the interviewer whether my understanding is wrong?