Vue is a data-driven framework where views are updated as data is modified. Data-responsive systems make state management simple and straightforward, reducing contact with DOM elements during development. In-depth study of the principles is necessary to avoid some common problems and make development more efficient.

One, the realization of a simple data response system

Vue implements a data responsive system using observer mode (also known as publish-subscribe mode) plus data hijacking, which uses object.defineProperty to convert data attributes into accessor attributes. Object.defineproperty is a non-shim feature in ES5, so Vue does not support IE8 and earlier browsers. Vue source code for data responsive system implementation is more complex, in the in-depth study of this part of the source code before the implementation of a simpler version is more conducive to subsequent understanding. The code looks like this:

let uid = 0 

// The container constructor
function Dep() {
    // Collect observer container
    this.subs = []
    this.id = uid++
}

Dep.prototype = {
    // Collect the current observer into the container
    addSub: function(sub) {
        this.subs.push(sub)
    },

    // Collect dependencies and call the observer's addDep method
    depend: function() {
        if(Dep.target){
            Dep.target.addDep(this)}},// Iterate over the run method of each observer in the execution container to execute the callback
    notify: function() {
        this.subs.forEach(sub= > {
            sub.run()
        })
    }
}

// Initialize the current observer object to be empty
Dep.target = null

// Data hijacking function
function observe(data) {
    // Prevent repeated hijacking of data
    if(data.__ob__) return
    let keys = Object.keys(data)
    keys.forEach(key= > {
        let val = data[key]
        let dep = new Dep()

        Object.defineProperty(data, key, {
            enumerable: true.configurable: true.get: function() {
                dep.depend()
                return val
            },
            set: function(newValue) {
                if((newValue ! == newValue) || (newValue === val)){return
                } else {
                    val = newValue
                    dep.notify()
                }
            }
        })
    });
    // Define a non-traversal internal attribute on hijacked data
    Object.defineProperty(data, '__ob__', {configurable: true.enumerable: false.value: true.writable: true})}// Observer constructor
function Watcher(data, exp, callback) {
    this.cb = callback
    this.deps = {}
    this.exp = exp
    // Get the function that gets the data
    this.getter = this.parseExp(exp.trim())
    this.data = data
    this.value = this.get()
}

Watcher.prototype = {
    run: function() {
        let value = this.get()
        let oldValue = this.value

        if(value ! == oldValue){this.value = value
            this.cb.call(null, value, oldValue)
        }
    },

    addDep: function(dep) {
        // Prevent the collection of duplicate data
        if(!this.deps.hasOwnProperty(dep.id)){
            dep.addSub(this)
            this.deps[dep.id] = dep
        }
    },

    get: function() {
        // Change the instance object to the current observer object
        Dep.target = this
        // Read the data to trigger the data get method
        let value = this.getter.call(this.data, this.data)
        // Dependencies are collected, and the current observer object is left empty
        Dep.target = null

        return value
    },

    // Get the data value as a string of the form 'a.b'
    parseExp: function(exp) {
        if(/[^\w.$]/.test(exp)) return

        let exps = exp.split('. ')

        return function(obj) {
            return obj[exps[1]]}}}// Monitor function
function $watch(data, exp, cb) {
    observe(data)
    new Watcher(data, exp, cb)
}
Copy the code

First use the monitor function $watch to test it as follows:

let a = {
    b: 100.c: 200
}

const callback = function(newValue, oldValue) {
    console.log('The new value is:${newValue}, the old value is:${oldValue}`)
}

$watch(a, 'a.b', callback)
$watch(a, 'a.c', callback)

a.b = 101 
a.c = 201
Copy the code

Output result:

The new value is:101, the old value is:100The new value is:201, the old value is:200
Copy the code

The logical structure of the above code is shown below:


1. Data hijacking

In the data hijacking function Observe, the object is first checked for the presence of an untraversable attribute __ob__. If it exists, the object has been converted to reactive; If not, add an __ob__ attribute after the data is converted. It then loops through the properties of the object, turning the data properties into accessor properties, each of which references a Dep instance Dep via a closure. When a property is read, the get method is triggered, and dep.depend() is called to collect the dependencies. When a new value is set for the property, the set method is fired, and then dep.notify() is called to trigger the dependency. The observe method only modiates the object property. The get and set of the object property are not triggered.

2. Collect dependencies

Add dependencies to reactive data through the Watcher function. A dependency is a callback function that needs to be triggered when data changes. The Watcher function is instantiated by calling get() to set the instance object to the current observer and then reading the data. The get method of the data is called and the Depend () method of the data deP referenced by the data closure is called. In Depend (), the DEP is passed to the addDep() method of the current observer. In the addDep() method, the dependency collection is first prevented from being repeated, and then the dep.addSub() method is called to add the current observer to the SUBs property of the DEP, completing the dependency collection.

3. Trigger dependencies

Triggered dependencies are the sequential execution of callbacks to observer objects stored in the variable DEP referenced by the data closure when data changes. When the data changes, the set method is called and then the dep.notify() method is executed, which traverses the set dep.subs, executing the run() method for each observer object in the array. The Run () method of the Watcher instance passes the values before and after the data change to the callback function for execution, completing the dependency firing.

4. Existing problems

The above code implements a simple data responsive system, but there are many problems, such as: what if the data type is array? What if the properties of the object are themselves accessor properties? How does deleting an object property trigger a dependency? And so on. Vue author Yuxi Yu said in an interview in 2019:

“I wanted to build a simple framework for myself and try out data change detection with ES5’s Object.defineProperty.”

As time went on, Vue became more and more functional, and it was no longer the framework for practicing. After understanding the most basic implementation ideas, let’s go deep into Vue source code on the implementation of data responsive system.

Second, observe

The observe function hijacks and modifies data to collect dependencies when data is accessed and trigger dependencies when data is changed. The observe function code looks like this:

function observe (value: any, asRootData: ? boolean) :Observer | void {
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
Copy the code

First, ensure that the data type being processed is an object; If a data object has an __ob__ attribute that is an instance of an Observer, the data is already responsive and the attribute is returned. Then check if the following five criteria are met: shouldObserve variable is true, not server-side rendering, data is pure object or array, data is extensible, data is not Vue instance. If all five conditions are met, the data is processed through the function Observer and the same value is returned. That is, observe returns either undefined or an instance of Observer. The constructor method for the Observer function looks like this:

constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0
  def(value, '__ob__'.this)
  if (Array.isArray(value)) {
    const augment = hasProto ? protoAugment : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {
    this.walk(value)
  }
}
Copy the code

The Observer instance object has three attributes: the instance attribute Dep of Dep, the attribute value pointing to hijacked data, and vmCount. An __ob__ attribute is added to the hijacked data, which points to an Observer instance object that has a circular reference to the hijacked data. The observe function processes two types of data: pure objects and arrays. Arrays are objects, but they have a special feature: their indexes are nonresponsive. The observe function treats these two types of data differently.

1. Handle pure objects

If the hijacked data is a pure object, it is processed by the Walk method.

walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}
Copy the code

The walk method calls the defineReactive method for each attribute on the object.

export function defineReactive ( obj: Object, key: string,
  val: any, customSetter?: ?Function, shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  if(! getter &&arguments.length === 2) {
    val = obj[key]
  }
  const setter = property && property.set

  letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      /* eslint-enable no-self-compare */
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify() } }) }Copy the code

(1) Principle of pure object data hijacking

We can see from the defineReactive method that the principle of hijacking data is to convert data attributes into accessor attributes. If the data itself is accessor attributes, the corresponding GET and SET methods are called in the overridden GET and SET methods. Each attribute closure refers to an instance of Dep, with dependencies collected in the get method through dep.depend() and triggered in the set method through dep.notify().

(2) if the data attribute value is pure object or array

What if the property value of pure object data is pure object or array? The first thing to understand is what the childOb value is in the following code.

letchildOb = ! shallow && observe(val)Copy the code

Shallow is the parameter of the defineReactive method, meaning whether to deeply observe the data. If this parameter is not passed, the default is deep observation. The observe method returns undefined when data is not an object, and returns an Observer instance when data is not an object. If the value of childOb is present, it indicates that the attribute value val is a pure object or array, and childOb is an instance of Observer(val). Because of circular references, childOb is equal to val.__ob__. In the case of an object attribute value, it is easier to process the dependency when it is triggered, simply by recursively processing the new value through the Observe method to make it responsive. Collecting dependencies in the GET method is a bit more cumbersome, starting with the following code:

childOb.dep.depend()
Copy the code

That is, execute the following code:

val.__ob__.dep.depend()
Copy the code

As mentioned earlier, the collection of property dependencies is stored in the DEP variable referenced by the closure, so what is the DEP for the __ob__ attribute of each object’s data? Why are dependencies collected again here? In fact, the main reason is that the two DEPs are fired at different times. The DEP referenced by the closure is used when the value of the attribute changes, and the DEP referenced by the object’s __ob__ attribute is used when the object reference changes. The principles of vue. set and vue. delete will be explained in detail below.

2. Handle arrays

There are seven instance methods that change the value of the array itself: push, POP, Shift, unshift, splice, sort, and reverse. In the case of an array, dependencies are collected when the array is read and fired when the user changes the array using these seven methods.

(1) Collect dependencies

Arrays cannot turn indexes into accessor properties, so they cannot use closures for each property to collect and trigger dependencies like pure objects. When processing an array, observe() first adds an __ob__ attribute to the array, pointing to the Observer instance object. Collect dependencies in __ob__.dep. In particular, array collection dependencies have the following code:

if (Array.isArray(value)) {
    dependArray(value)
}
Copy the code

DependArray recursive function code is as follows:

function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
Copy the code

In addition to collecting dependencies from the __ob__. Dep attribute of the array, we also need to recursively collect dependencies from the __ob__. Dep attribute of the array. Why do you do that? This is because of the premise that any change in the array counts as a change in the array, and that depending on the array is equivalent to depending on every element in the array.

{
  a:[ {b: 1},1].2]}Copy the code

As shown in the example above, the __ob__ attribute is added when accessing array A, storing the dependency on A in A.__ob__. dep. When a is operated on by changing the instance method of the array itself, a.__ob__.dep.notify() is called to trigger the dependency; When a value of a is changed by vue.set (), it is converted to an instance method call, and then a.__ob__.dep.notify() is called to trigger the dependency; . However, if you change the value of a[0].b, you cannot trigger a dependency on array A because no dependencies on array A are collected in object A [0]. This violates the premise that any change in the array counts as a change in the array. Therefore, Vue collects the array dependencies into each object contained in the array by recursively calling the dependArray method, so that any change in the array’s value triggers the array’s dependencies.

(2) Trigger dependency

To be able to trigger dependencies when an array is changed by instance methods, Vue overwrites methods that can change the array itself. The following code looks like this:

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = ['push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse']

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (. args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    ob.dep.notify()
    return result
  })
})
Copy the code

The arrayMethods object is modeled as array. prototype, and these seven methods are added after processing. The overridden instance method contains three main functions:

1. Call the native instance method. 2. When adding data via push, unshift, splice, make the newly added data responsive. 3. Trigger dependencies when an array changes.

When an object reference changes, the deP in its own property is triggered. When an array changes, the deP in its own __ob__ property is triggered by notify(). ES6 has added a __proto__ attribute for objects that reads or sets the prototype object of the current object, compatible with IE11. When Vue handles arrays, if the array has a __proto__ attribute, it points that attribute directly to the arrayMethods object, the prototype object that modifies the array. The methods of the arrayMethods object are called when the seven methods that change the array itself are called, thereby triggering dependencies. As shown below:


function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
Copy the code

Vue. Set and Vue. Delete

To summarize the dependency collection and triggering described above:

1. In the case of an object, the properties of the object are converted to accessor properties. When the properties are accessed, the dependencies are collected and stored in the variable DEP referenced by the property closure. 2. If it is an array, add an __ob__ object attribute when reading the array, collecting dependencies in the object’s DEP attribute. Then override the seven instance methods that can change themselves, triggering dependencies stored in __ob__.dep when called.

These two situations lead to the issues mentioned in the list rendering on the official website:

Due to JavaScript limitations, Vue cannot detect the following changes to an array: 1. When you set an item directly using an index, e.g. Vm. items[indexOfItem] = newValue 2. When you change the length of an array, e.g. vm.items.length = newLength

Again, due to JavaScript limitations, Vue cannot detect additions or deletions of object attributes.

Vue provides two global apis, vue.set () and vue.delete (), as well as vm.Delete () two instance methods to solve the above problem.

Vue. Set

Vue. Set () and vm. $set () is called SRC/core/observer/index set method in js.

export function set (target: Array<any> | Object, key: any, val: any) :any {
  if(process.env.NODE_ENV ! = ='production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key intarget && ! (keyin Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if(target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV ! = ='production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if(! ob) { target[key] = valreturn val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
Copy the code

From the above code, we can see that the set method does the following:

1. If target is an array and key is a valid index, check whether to adjust the array size before calling splice to trigger the dependency. 2. If target is a pure object and key is an existing attribute of the object, change the key value directly, and then call the attribute set method to trigger the dependency. If the target is not responsive, add a key attribute to the target. If the target has a key attribute, overwrite the target. 4. If target is reactive and doesn’t have a key attribute of its own, add the value to target via defineReactive and trigger the dependency via target.__ob__.dep.notify().

(2) Vue.delete

Vue. Delete () and vm. $the delete () is called SRC/core/observer/index. The del method in js.

export function del (target: Array<any> | Object, key: any) {
  if(process.env.NODE_ENV ! = ='production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)}if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  if(target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV ! = ='production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  if(! hasOwn(target, key)) {return
  }
  delete target[key]
  if(! ob) {return
  }
  ob.dep.notify()
}
Copy the code

As you can see from the above code, the del method does the following:

1. If target is an array and key is a valid index, call the splice method to delete the array and trigger the dependency. 2. If target is a pure object and the key attribute does not exist, the delete operation is not performed. __ob__.dep.notify() if target is a pure object and the key attribute exists, delete the attribute and trigger the dependency on target.__ob__.dep.notify().

Third, Dep

The main function of the Dep function is to generate an observer container for observed data, whose static property target points to the current observer to be collected. The Dep function is as follows:

export default class Dep {
  constructor () {
    this.id = uid++
    this.subs = []
  }

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

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

  depend () {
    if (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()
    }
  }
}
Dep.target = null
const targetStack = []

export function pushTarget (_target: ? Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}
Copy the code

The instance attribute subs is an array of observers. Depend () collects dependencies and notify() triggers dependencies. One thing to note: The Depend method calls the addDep method of the current observer, which in turn calls the addSub method of the Dep instance to store the dep. target to the subs. Why not just push the current observer dep. target into the subs? There are three main reasons for this: to avoid repeated collection dependencies, to conveniently record the values before and after the observed data changes, and to keep the amount of observed data in the observer object. If only to avoid duplicate collection dependencies, you can use the id of the observer object to delete the duplicate observer object. In addition, the dep. target value is not simply assigned to the current observer, but is implemented by pushTarget, which stores the original observer before assigning. PopTarget restores dep. target to the original observer object.

Fourth, the Watcher

The main function of the Watcher function is to provide dependencies (callbacks) for the data being observed. The observer function observes data by reading data. Data becomes responsive after being processed by the Observe function. During the reading process, the reference of Watcher instance object can be stored, which is the process of collecting dependencies. When the observed data changes, the data iterates through the stored Reference to the Watcher instance to execute the callback function on each Watcher instance. This is how the dependency is triggered.

1, an overview of the

The constructor method for the Watcher function is as follows:

constructor(vm: Component, expOrFn: string | Function, cb: Function, options? :? Object, isRenderWatcher? : boolean ) {this.vm = vm
    if (isRenderWatcher) { vm._watcher = this }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.syncthis.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set(a)this.newDepIds = new Set(a)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()
  }
Copy the code

The Watcher function takes five parameters: the current component instance object, the object being observed, the callback function after the observed data has changed, the option passed, and whether it is a render function identifier. The VM attribute of the observer object points to the instance object of the current component, the _Watcher attribute on the component instance object points to the observer of the render function, and the _Watchers attribute contains all the observer objects on the current instance object. Initialize the five values on the observer object from the value of the passed option options, with the following meanings:

Property deep: indicates whether to deeply observe data. Default is false. Attribute user: Whether the observer is defined by the developer. Default is false. Attribute lazy: Whether the observer is lazily evaluated. It is an observer created internally to implement the attribute evaluation function. The default is false. Property sync: Whether data changes are evaluated synchronously. Default is false. Property before: Hook function called after data changes but before updates are triggered.

The getter method for an observer object is determined based on the type of the expOrFn parameter passed in. If you pass in a function, the getter method is directly equal to the function; If a string is passed in, the getter method is a function that returns the target value. The function of a getter is to read the target data. At the end of the constructor method, the value of the value is determined by whether the lazy attribute is true, which is true only if it is the observer of the evaluated attribute. If you are not the observer evaluating the property, the get() method is called and the value is returned with a value record. The get() method has two main functions: it reads the observed data and returns the data value. When the data is read, the data’s corresponding dep.depend() method is called, and the observer object’s addDep() method is called, followed by the dep.addSub() method, to complete the collection of the current dependency. When observed data changes, the dep.notify() method is called, and then the Update () method of each observer object contained within it is called. In update, the run() method is eventually called if the observer is not evaluating the property. The run() method gets the new value of the data by executing the get() method, and then calls the callback function with the old and new values as arguments.

2. Avoid collecting duplicate dependencies

The Watcher function relies on two sets of properties to avoid collecting duplicate dependencies: newDeps and newDepIds, and deps and depIds. NewDeps and newDepIds are used to avoid collecting duplicate dependencies during a single evaluation. Note When the expOrFn parameter is a function and a value is used multiple times in the function, newDeps and newDepIds are used to avoid repeating the collection of dependencies for that value. NewDeps stores the Dep instance objects collected from the current evaluation. Deps and depIds are designed to avoid collecting duplicate dependencies during multiple evaluations. These two properties are used to avoid collecting dependencies again when the observed data changes and the data is re-read. Deps stores the Dep instance objects collected during the last evaluation. The cleanupDeps() method is called in the finally part of the get() method.

cleanupDeps () {
  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

The cleanupDeps() method has two functions:

1. Remove discarded observers. 2. Assign values to depIds and deps before emptying newDepIds and newDeps, respectively.

Avoid collecting duplicate dependencies in the addDep() method.

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)}}}Copy the code

In the addDep() method, it first checks whether the current ID already exists in newDepIds. If so, it indicates that the dependency has already been collected at the time of this evaluation and does not need to be collected again. If not, add ids to newDepIds and DEp to newDeps. It then determines whether the dependency was collected the last time it was evaluated, and if so, does not need to be collected again. If the dependency was not collected the last time it was evaluated, the dependency is collected.

3. Execute asynchronously

The update() method is called in the process of triggering a dependency, which has three cases: as an observer of evaluated properties, indicating synchronous execution, and default asynchronous execution.

update () {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)}}Copy the code

When the sync option is true, the callback function is called directly by executing the run() method. The sync option, unless explicitly specified, defaults to false, which means that dependency triggering is performed asynchronously by default. The main reason for asynchronous execution is to optimize performance, such as when the data in the template changes, the rendering function is reevaluated to complete the re-rendering. If executed synchronously, rerendering is required every time a value is changed. In complex business scenarios, many data may be modified at the same time, and multiple rendering can cause serious performance problems. If executed asynchronously, the value of the attribute is not reevaluated immediately after each change, but the observer that needs to perform the update operation is placed in a queue, and there are no duplicate observer objects in the queue, thus achieving performance optimization.

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if(! flushing) { queue.push(watcher) }else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) { i-- }
      queue.splice(i + 1.0, watcher)
    }
    if(! waiting) { waiting =true
      nextTick(flushSchedulerQueue)
    }
  }
}
Copy the code

The queueWatcher() method first checks if the observer queue contains an observer to be added and does nothing if it already exists. The flushing variable is a marker of whether the queue is performing an update. If the queue is not performing an update, the observer object is directly stored in the queue. If the queue is performing an update, the execution order of the observers needs to be guaranteed. Waiting has an initial value of false. After an if judgment is executed, nextTick(flushSchedulerQueue) is executed only once. NextTick () is the same as setTimeout(fn, 0). The function of nextTick() is to call flushSchedulerQueue immediately at the start of the next event cycle, so that the observers in the queue can perform the update uniformly.

Five, the summary

Vue data response system is generally divided into two steps: 1. Hijacking data; 2. Associate the data with the callback function to be triggered by data changes. The observe function hijacks data so that it collects dependencies when it is read and triggers dependencies when it is changed. If the data is a pure object, its properties are converted to accessor properties; If the data is an array type, this is done by overriding methods that can change themselves. Dependencies are not triggered by adding or deleting attributes to objects and changing arrays by direct assignment. Instead, use the vue.set () and vue.delete () methods. The Dep function is used to generate containers for hosting dependencies. The collection of dependencies is eventually collected in the subs array property of the instantiation object, and the final operation of the dependency is triggered to iterate over the callback function on the observer object in the subs. The Watcher function mainly associates the observed data with the callback function to be executed after the data changes. To improve performance, avoid duplication of data collection dependencies in this process; Updates are performed asynchronously when data changes.

If you need to reprint, please indicate the source: juejin.cn/post/684490…