1 sequence

Why the different Vue2 response principle?

This happened two days ago when I reviewed the code to my colleagues and found a Vue2 responsive phenomenon beyond my own cognition. It is a very interesting phenomenon. You can refer to the boiling point for an example

The response of Vue2 objects is generally recognized as follows, from the official website:

  • For the object

    NewPro and delete obj. OldPro cannot intercept Vue2’s response. This.$set or vue. set can be used

  • For an array of

    To put it simply: an array response is usually implemented using seven array manipulation methods (taught by Object.defineProperty), such as arr[0] = 1 and arr. Length = 0

However, different Vue2 response type principle will let you re-understand, find that the above statement is not absolutely correct

Next we will go through a simple example to find the answer from the source level, to parse Vue from initialization to update these two processes do, of course, this article is mainly to illustrate the problem, the source code to do a part of the simplification, all out of the words too much.

2 What are the phenomena and internal principles (execution process) of the following examples?

You can look at the code first, then see the answer, of course, you can write an example to try, verify the following


      
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Document</title>
</head>
<body>
  <div id = "app">
    <p>{{ form }}</p>
    <ul>
      <li v-for = "item in arr" :key = "item">{{ item }}</li>
    </ul>
  </div>
  <script src = ".. /.. /dist/vue.js"></script>
  <script>
  const ins = new Vue({
    data () {
      return {
        form: {
          name: 'lyn'
        },
        arr: [1]
      }
    },
    mounted () {
      setTimeout((a)= > {
        this.form.name = 'test'
        this.arr[0] = 11
      }, 2000)
    }
  })
  ins.$mount('#app')
  </script>
</body>
</html>
Copy the code

2.1 Conclusion first

I believe many students will answer that the initial page is complete, the display content is:

Then execute the timing function after the two, and the page is updated to the next:

If that’s your answer, this article is worth reading

Of course the theoretical basis of your answer is correct, but why is it wrong? The reason is simply that the perception of the whole reactive execution process is a bit flawed, at least as I saw it the other day

2.1.1 phenomenon

The initial render result is:

After 2s, execute the timing function, and the page is updated with:

2.1.2 Internal Principles

Once the sample code is loaded into the browser, the Vue initializes, performing various init operations, the most important of which are instantiating the component Watcher, initializing data, collecting dependencies (DEP), associating deP and Watcher, interlaced with the execution of the lifecycle methods. Create, beforeCreate, created, created, beforeMount, mounted, if there is a child component, beforeMount initializes the child component until the mounted execution is complete, and then returns the mounted execution. The problem is not initialization, but later updates.

The timing function is executed after 2s of page rendering

Executed firstthis.form.name = 'test'This is done in two steps

  • First, this.form triggers the getter to get value = {name: ‘lyn’}

  • Then value.name = ‘test’ triggers the setter to update the data, so this.form.name now has a value of test

Notifying Watcher to execute its own update method by firing dep.notify() in the setter, which pushes watcher into a queue, It then calls the nextTick method to register a function that refreshes the queue (essentially executing each Watcher’s run method in the queue array). The nextTick method wraps the refresh queue function around an arrow function and stores it in an array called callbacks. NextTick then executes the timerFunc function

The timerFunc function uses the asynchro mechanism of the browser to register the function refreshing the Callbacks array as an asynchro task. When all the synchronization tasks are completed, the queue will be refreshed. So the asynchronous task is suspended for now

Next to executethis.arr[0] = 11, again in two steps

  • First this.arr triggers the getter to get value = [1]

  • Then, it is gone, because Vue2’s responsive core Object.defineProperty cannot be intercepted because this.arr[0] = 11 is written, but

This. Arr [0] = 11; this. Arr [0] = 11; this

After all synchronization tasks are complete, the system starts to execute the registered asynchronous task

The asynchronous task above, just a bunch of callback functions, ends up doing something very simple (pure), which is executing the watcher.run method, which executes the watcher.get method, which executes the updateComponent method. When you instantiate watcher, you will execute the vm._render function to generate a new VDOM. Note that to generate a new VDOM, you need to read the properties of the vue instance, that is, the properties used in the template. Our examples are this.form and this.arr. Does this make sense?

Although this.arr[0] = 11 does not trigger Vue2’s reactive mechanism, it does change the value of this, so you see something on the page that you didn’t understand before

To generate a new VDOM, updateComponent executes the vm._update method, calls the patch method, compares the old and new VDOM, finds the DOM nodes that have changed, and then updates them

See here is a little bit of understanding, is it also a little different idea? Such as:

Arr [idx] = XXX, I want to update the array elements with this.arr[idx] = XXX, I don’t want to use this.splice, I just need to add a valid operation that triggers the setter. It’s not good to give someone a bad experience.

See here don’t know is straight understand? Still a little meng, you can then look down, find the answer from the source code, the code after simplification, there are detailed notes, after reading, think back to the process, and then come back to see the conclusion, there will be a great harvest.

2.2 Find the answer from the source code

This section looks at the execution of the entire sample code. Once the content is loaded into the browser, the Vue source code looks like this:

  • src/core/instance/index.js

    /** * the Vue constructor performs initialization */
    function Vue (options) {
      this._init(options)
    }
    Copy the code
  • src/core/instance/init.js

    /** * performs various initialization operations, such as: * Most importantly, set the data in response (this part will not expand, otherwise too much) and then execute the instance's $mount method */
    Vue.prototype._init = function (options? : Object) {
      initLifecycle(vm)
      initEvents(vm) 
      initRender(vm)
      callHook(vm, 'beforeCreate')
      initInjections(vm) // resolve injections before data/props
      initState(vm)
      initProvide(vm) // resolve provide after data/props
      callHook(vm, 'created')
      
      if (vm.$options.el) {
        vm.$mount(vm.$options.el)
      }
    }
    Copy the code
  • src/platforms/web/runtime/index.js

    /** * $mount, which executes the mountComponent method */
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ) :Component {
      el = el && inBrowser ? query(el) : undefined
      return mountComponent(this, el, hydrating)
    }
    Copy the code
  • src/platforms/web/entry-runtime-with-compiler.js

    /** */ $mount ($mount, $mount, $mount, $mount, $mount)
    const mount = Vue.prototype.$mount
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ) :Component {
      return mount.call(this, el, hydrating)
    }
    Copy the code
  • src/core/instance/lifecycle.js

    /** * mountComponent method * important points * 1, define component updateComponent method * 2, instantiate component watcher, And pass the updateComponent method to Watcher * * Watcher will then call the get method in its run method, and the get method will execute the updateComponent method and generate a new VDOM, See the watcher section below */
    export function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {
      callHook(vm, 'beforeMount')
    
      let updateComponent = (a)= > {
        // vm._render generates a new VDOM. Vm. _update calls patch to compare the old and new DOM and update the view
        vm._update(vm._render(), hydrating)
      }
    
      // we set this to vm._watcher inside the watcher's constructor
      // since the watcher's initial patch may call $forceUpdate (e.g. inside child
      // component's mounted hook), which relies on vm._watcher being already defined
      new Watcher(vm, updateComponent, noop, {
        before () {
          if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
      hydrating = false
    
      // Invoke the mounted method of the component instance
      if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')}return vm
    }
    
    /** * is responsible for executing various lifecycle methods, such as Mounted */
    export function callHook (vm: Component, hook: string) {
      // handlers = vm.$options.mounted
      const handlers = vm.$options[hook]
      const info = `${hook} hook`
      if (handlers) {
        for (let i = 0, j = handlers.length; i < j; i++) {
          invokeWithErrorHandling(handlers[i], vm, null, vm, info)
        }
      }
    }
    
    /** * Is responsible for executing the patch method, which is divided into first render and second update */
    Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
      const vm: Component = this
      const prevEl = vm.$el
      const prevVnode = vm._vnode
      const restoreActiveInstance = setActiveInstance(vm)
      vm._vnode = vnode
      // Vue.prototype.__patch__ is injected in entry points
      // based on the rendering backend used.
      if(! prevVnode) {// initial render
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
        // updates
        vm.$el = vm.__patch__(prevVnode, vnode)
      }
    }
    Copy the code
  • src/core/util/error.js

    // Execute the declaration cycle method
    export function invokeWithErrorHandling (handler: Function, context: any, args: null | any[], vm: any, info: string) {
      // Where the declaration cycle method is actually implemented
      return args ? handler.apply(context, args) : handler.call(context)
    }
    Copy the code

The Mounted method has been executed and the page has been rendered. The next step is to execute the timing function after 2 seconds to update the data and trigger the view update via reactive interception

/ / setTimeout(() => {this.form.name = 'test' this.arr[0] = 11}, 2000)Copy the code

What is the next step in analyzing the timer registration callback process

  • src/core/observer/index.js

    This. Form = ‘test’; this. Form = ‘test’; ‘lyn’}, value.name = ‘test’ triggers the setter to update the name property, and dep.notify()

    /** * This is the core of the data responsiveness. It intercepts the properties of the sample object, executes get when reading data, and executes set */ when setting data
    export function defineReactive (obj: Object, key: string, val: any, customSetter? :? Function, shallow? : boolean) {
      const dep = new Dep()
    
      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
          }
          // #7981: for accessor properties without setter
          if(getter && ! setter)return
          if (setter) {
            setter.call(obj, newVal)
          } else{ val = newVal } childOb = ! shallow && observe(newVal)// Notify watcher to execute the update method
          dep.notify()
        }
      })
    }
    Copy the code
  • src/core/observer/dep.js

    /** * A DEP is an Observable that can have multiple * cache subscribing to it. Notify watcher of updates * Only notify (notify Watcher of updates) and the constructor */ remain
    export default class Dep {
      statictarget: ? Watcher; id: number; subs:Array<Watcher>;
    
      constructor () {
        this.id = uid++
        this.subs = []
      }
      // Notification Notification executes the update method
      notify () {
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          // This is the watcher update method
          subs[i].update()
        }
      }
    }
    Copy the code
  • src/core/observer/watcher.js

    /** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. * * A component corresponds to a watcher instance (render watcher), instantiated in the mountComponent method, The most important thing to do during synchronization is to push the current watcher instance to a watcher execution queue for later execution. Execute the updateComponent method */ by executing the run method with a promise.resolve ().then()
    export default class Watcher {
      constructor( vm: Component, // updateComponent expOrFn: string | Function, // noop cb: Function, options? :? Object, isRenderWatcher? : boolean ) {this.vm = vm
        // updateComponent
        this.getter = expOrFn
      }
       
      /** * Evaluate the getter, and re-collect dependencies. ** Execute this. Update the view */
      get () {
        // dep. target = watcher instance, where Dep is associated with watcher
        pushTarget(this)
        let value
        // Component instance
        const vm = this.vm
        try {
          // This is the updateComponent method:
          // let updateComponent = () => { vm._update(vm._render(), hydrating) }
          value = this.getter.call(vm, vm)
        } catch (e) {
        } finally {
          // "touch" every property so they are all tracked as
          // dependencies for deep watching
          if (this.deep) {
            traverse(value)
          }
          popTarget()
        }
        return value
      }
    
      /** * Add the watcher instance to the watcher queue */
      update () { 
        queueWatcher(this)}** ** Scheduler job interface. * Will be called by the Scheduler. ** * Get executes updateComponent */, borrowing the browser's asynchrony mechanism (Promise)
      run () {
        const value = this.get()
      }
    }
    Copy the code
  • src/core/observer/scheduler.js

    /** * Push the watcher into the queue array, and then register a return function to execute the watcher's run methods */ in the future [promise.resolve ().then()]
    export function queueWatcher (watcher: Watcher) {
      queue.push(watcher)
      FlushSchedulerQueue {flushSchedulerQueue}
      nextTick(flushSchedulerQueue)
    }
    
    /** * is responsible for making all watchers in the queue execute their own run method. */
    function flushSchedulerQueue () {
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        watcher.run()
      }
    }
    Copy the code
  • src/core/util/next-tick.js

    If you don’t understand macro and micro tasks, you can read this article

    /** * A few important points ** Define the nextTick method, put all the callbacks into a callbacks array, and then execute timerFunc. Resolve (). Then () registers the callback function to refresh the queue just stored, * executes watcher.run(), When updateComponent is triggered, it is crucial to understand that macro tasks, microtasks, and * when all macro tasks are finished, such as the entire setTimeout callback in the example, the registered microtasks, promise.resolve ().then() */, are executed 
    const callbacks =[]
    let pending = false
    
    // nextTick wraps the flushSchedulerQueue with an arrow function and places it in the Callbacks array
    export function nextTick (cb? : Function, ctx? : Object) {
      let _resolve
      callbacks.push((a)= > {
        cb.call(ctx)
      })
      if(! pending) { pending =true
    		// Execute an asynchronous method, preferably Promise
        timerFunc()
      }
    }
    
    
    // Execute a ready-to-use Promise. The Promise callback is responsible for executing the flushCallbacks function
    let timerFunc = (a)= > {
      Promise.resolve().then(flushCallbacks)
    }
    
    Call () => flushSchedulerQueue.call(CTX); run () => flushSchedulerQueue.call(CTX); The get method will eventually call the component's updateComponent method, perform render to regenerate the VNode, perform the * patch process, and finally update the DOM */
    function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }
    Copy the code
  • What is generated after the vm._render function is executed?

    /** * It is important to understand why our view is effectively updated when we generate the vDOM by reading the value of the component instance's property, such as this.form, this.arr. Because vm.arr has indeed been updated to [11] */
    function anonymous() {
    	with(this) {
        return _c(
          'div',
          {attrs: {"id":"app"}},// this.form
            _c('p',[_v(_s(form))]),_v(""),
            _c(
              'ul',
              _l(
                // this.arr
                (arr),
                function(item) {
              	  return _c('li', {key:item},[_v(_s(item))])
            	  }
              ),
              0)])}}Copy the code

3 sublimation?

Read here, in sorting out their way of thinking, and then look back at the conclusion of the front, is there a kind of suddenly enlightened feeling?