Vue principle analysis (eight) : together to understand the headache of diff algorithm

In the previous chapters, we introduced the core concepts such as vUE initialization, virtual Dom generation, virtual Dom conversion to real Dom, deep understanding of responsive and diff algorithm, and analyzed its internal implementation according to the process. All of these are low-level principles. We will further enrich our understanding of VUE by introducing the principles of apis commonly used in daily development, including the following:

Responsive related apis: this.$watch, this.$set, this.$delete

Event-related apis: this.$on, this.$off, this.$once, this.$emit

Lifecycle apis: this.$mount, this.$forceUpdate, this.$destroy

Global API: Vue.extend, vue.nexttick, vue.set, vue.delete, vue.ponent, vue.use, vue.mixin, vue.pile, vue.version, vue.directive, vue.fil ter

This chapter mainly analyzes the attributes of computed and Watch. Those who have been exposed to VUE for a while may have doubts about computed and Watch, and when to use which attributes. Next, from the perspective of internal implementation, we will thoroughly understand the scenarios in which they are respectively applicable.

  • this.$watch

This API is a wrapper around the reactive Watcher class we introduced earlier, namely the user-Watcher of the three types of Watcher. Listener properties are often used as follows:

exportdefault { watch: { name(newName) {... }}}Copy the code

It’s just a wrapper around the this.$watch API:

export default {
  created() {
    this.$watch('name', newName => {... }}})Copy the code

Listen for property initialization

Let’s first look at what the watch property does when initialized:

functionInitState (vm) {vm._watchers = [] // Current instance watcher set const opts = vm.$options// The merged attribute... // Initialize other statesif(opts.watch) {// If you define the watch property initWatch(vm, Opts. Watch) / / perform initialization method}} -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --functionInitWatch (vm, watch) {// Initialization methodfor (const key inWatch) {const handler = watch[key] // The value of each watchif(array.isarray (handler)) {// if the value of this item isArrayfor (leti = 0; i < handler.length; I ++) {createWatcher(vm, key, handler[I]) // Wrap each item with watcher}}else{createWatcher (vm, key, handler) / / not array directly using the watcher}}} -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --function createWatcher (vm, expOrFn, handler, options) {
  if(isPlainObject(handler)) {// If it is an object, the argument shifts options = handler handler = handler.handler}if (typeof handler === 'string'Handler = vm[handler] // get a method within methods}return vm.$watch(expOrFn, handler, options) // package}Copy the code

All of these different uses of the listener attribute are addressed. Examples can be found on the official website: Watch example, not too much introduction here. You can see that the vm.$watch method is called at the end.

How to implement listener properties

So let’s look at the internal implementation of $watch:

Vue.prototype.$watch = function(expOrFn, cb, options = {}) {
  const vm = this
  if(isPlainObject(cb)) {// If CB is an object, when manually creating listener propertiesreturn createWatcher(vm, expOrFn, cb, options)
  }
  
  options.user = true// Exporcher class const watcher = new watcher (VM, expOrFn, cb, options) // instantiating user-watcherif(options.immediate) {// Execute cb.call(vm, watcher.value) // Execute a callback function immediately using the current value} // Watcher. value is the value returned after the instantiationreturn function unwatchFn() {/ / return a function that performs cancel listening watcher. Teardown ()}} -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --export default {
  data() {
    return {
      name: 'cc'}},created() {
    this.unwatch = this.$watch('name', newName => {... }) this.unwatch() // Unlisten}}Copy the code

This.$watch is used internally, but we can also manually call this.$watch to create a listener, so the second parameter cb will appear as an object. Next, set a flag bit, options.user, to true, indicating that this is a user-watcher. After you set the immediate attribute to watch, the value is passed to the callback and the callback function is executed immediately. This is how the watch is implemented. The final return value is a method that can be executed to unlisten on the listening property. Let’s look at how user-Watcher is defined:

Class Watcher {constructor(vm, expOrFn, cb, options) {this.vm = vm vm._watchers. Push (this) // Add to current instance of watchersif(options) { this.deep = !! Options. deep // If this. User =!! Options. user // Whether user-wathcer this.sync =!! Options. sync // Whether to synchronize updates} this.active =true// // send the updated flag bit this.cb = cb // callback functionif (typeof expOrFn === 'function'// expOrFn is a function this.getter = expOrFn}else{this.getter = parsePath(expOrFn) // If it is a string object path, return the closure function}... }}Copy the code

When user-watcher is instantiated internally, watcher is instantiated in this way. Normally we create listener attributes as strings, so let’s first look at what the parsePath method does:

Const bailRE = /[^\w.$]/ // must be in the form of an object path, such as info.namefunction parsePath (path) {
  if (bailRE.test(path)) returnConst segments = path-.split (const segments = path-.split (const segments = path-.split ('. ') // Split by points into arraysreturn function(obj) {// The closure returns a functionfor (let i = 0; i < segments.length; i++) {
      if(! obj)returnObj = obj[segments[I]]return obj
  }
}
Copy the code

The parsePath method finally returns a closure method, and this. Getter in the Watcher class is a function. This.get () will pass this.

class Watcher { constructor(vm, expOrFn, cb, options) { ... This.getter = parsePath(expOrFn) // Return method this.value = this.get() // implement get}get() {pushTarget(this) // Assigns the current user-watcher instance to dep.target, which will be collected when readletValue = this.getter.call(this.vm, this.vm) // Pass the VM instance to the closure for readingif(this.deep) {// if there are deep attributes traverse(value) // popTarget()returnValue // Returns the value read by the closure. The parameter immediate uses the same value}... }Copy the code

Since the initializer has already delegated all the state to this, we can simply read the properties of this, for example:

export default {
  data() {// Initialize data with watch firstreturn {
      info: {
        name: 'cc'}}},created() {
    this.$watch('info.name', newName => {... }}}Copy the code

The info property under this is read first, followed by the Name property under INFO. Notice that we’re using the verb read here, so the dependency collection is done by the get method that wraps the data responsive data, collecting the dependencies into the DEP of the read property, but user-watcher, and the get method returns the value read by the closure.

When the info.name attribute is reassigned, the update process will be distributed. We will specify the differences with render- Watcher separately. The update process will execute the update method in watcher:

class Watcher {
  constructor(vm, expOrFn, cb, options) {
    ...
  }
  
  update() {// Perform dispatch updateif(this.sync) {// If sync is set totrueThis.run ()}else{queueWatcher(this) // otherwise join queue, run()}}run() {
    if(this.active) {this.getAndInvoke(this.cb) // Pass the callback function}} getAndInvoke(cb) {const Value = this.get() // reevaluateif(value ! = = this. Value | | isObject (value) | | this. Deep) {const oldValue = this. The value / / the value of the cache before this. Value = the value / / new valuesif(this.user) {// If user-watcher cb.call(this.vm, value, oldValue) // Pass new and old values}}}} in the callbackCopy the code

In fact, the sync attribute is not explained on the official website, but we can see that the source code is still there. Call (this.vm, value, oldValue) internally passes the new and old values to the callback function.

<template> <div>{{name}}</div> </template>exportDefault {// App componentdata() {
    return {
      name: 'cc'} }, watch: { name(newName, oldName) {... } // assign new and old values to callback},mounted() {
    setTimeout(() => {  
      this.name = 'ww'// Trigger nameset}}}, 1000)Copy the code

Listener attributedeepPrinciple of deep monitoring

Traverse if you have a deep property:

Const seenObjects = new Set() //function traverse (val) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val, seen) {
  letI, keys const isA = array. isArray(val) // whether val is an Arrayif((! isA && ! IsObject (val)) / / if not array and object | | object. IsFrozen (val) / / or has frozen object | | val instanceof VNode) {/ / or VNode instancereturn/ / bye}if(val.__ob__) {// Only objects and arrays have an __ob__ attribute const depId = val.__ob__.dep.id // Manually dependent collector IDif(seen. Has (depId)) {// There has been a collectionreturn} seen. Add (depId)if(isA) {// yes array I = val.lengthwhile(I --) {_traverse(val[I], seen) {_traverse(val[I], seen)elseKeys (val) I = keys.lengthwhile(I --) {_traverse(val[keys[I]], seen) {_traverse(val[keys[I]], seen)Copy the code

In chapter 7, the manual dependency manager collects the dependencies of arrays and objects into the DEP of the Observer class, which is used to perform deep listening.

Watch: The user watcher is created and collected for the data to be observed. When the data changes, the user is notified to the user watcher to pass the new and old values to the user defined callback function. Finally, the realization principle of the three parameters used in the definition of watch is analyzed: sync, immediate and deep. Sync updates the watcher synchronously without adding it to the nextTick queue, immediate executes a callback on the value immediately, and deep recursively collects dependencies on its subvalues.

  • this.$set

The API was analyzed at the end of Chapter 7, and you can see how this.$set implementation works.

  • this.$delete

The API was analyzed at the end of Chapter 7, and you can see how this.$delete works.

  • Computed properties

The calculated property is not an API, but it is the last and most complex instantiation use of the Watcher class, and it is worth analyzing. (VUE version 2.6.10) the main reason is to analyze how the calculation property can be recalculated only when its dependencies change, otherwise the current data is cached. The value of a calculated attribute can be an object, which requires passing in the get and set methods. This is not common, so the analysis here will introduce the common function form. They are almost the same, but can reduce the cognitive burden, focus on the core principles of implementation.

exportDefault {computed: {newName: {// Do not analyze this ~get() {... }, // The internal get attribute is used to calculate the value of the attributeset() {... }}}}Copy the code

Calculate the property initialization

functionInitState (vm) {vm._watchers = [] // Current instance watcher set const opts = vm.$options// The merged attribute... // Initialize other statesif(opts.computed) {// If there is a defined computing property initComputed(VM, opts.computed) // initialize}... } -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --functionInitComputed (VM, computed) {const watchers = vm._computedWatchers = object.create (null) // Create a pure Objectfor(const key inComputed) {const getter = computed[key] // computed each corresponding callback function Watchers [key] = new Watcher(VM, getter, noop, {lazy:true}) // instantiate computed-watcher... }}Copy the code

Calculate the implementation principle of attributes

Here, again, we instantiate each of the computed attributes defined using the Watcher class, but here’s how to instantiate them in terms of computed- Watcher:

class Watcher{
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm
    this._watchers.push(this)
    
    if(options) { this.lazy = !! Options. lazy // Indicates that it is computed} this.dirty = this.lazy // dirty is the flag bit, Value = expOrFn // expOrFn // for computed this.getter = expOrFn // for computed this.value = undefined}}Copy the code

I’m done here. The instantiation is done. The render-watcher and user-watcher methods are not implemented as before. Why is this? We then analyze why this is so, and complete the methods used to initialize computed before:

function initComputed(vm, computed) {
  ...
  
  for(const key inComputed) {const getter = computed[key] // // computed each corresponding callback function...if(! (keyinvm)) { defineComputed(vm, key, getter) } ... Key cannot have the same name as data... Key cannot have the same name as properties in props}}Copy the code

The App component here has already attached the key to the vm prototype when it executes the extend constructor, but it also executed the defineComputed method earlier, so we can see what it does:

function defineComputed(target, key) {
  ...
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: createComputedGetter(key),
    set: noop
  })
}
Copy the code

What this method does is make computed a responsive data and define its GET property, which means that when a page performs a rendering that accesses computed, it fires GET and then executes the createComputedGetter method, so that’s where I’m going to end up, Let’s see how the get method is defined:

functionCreateComputedGetter (key) {// Higher order functionreturn functionConst watcher = this._computedwatchers &&this._computedwatchers [key] const watcher = this._computedwatchers &&this._computedwatchers [key] Get computed-watcher for keyif (watcher) {
      if(watcher.dirty) {// When instantiating watchertrueWatcher.evaluate () // Evaluate the property}if(dep.target) {// The current watcher, so render-watcher watcher.depend() // Collect the current watcher}returnWatcher. value // returns the evaluated value or previously cached value}}} ------------------------------------------------------------------------------------ class Watcher { ...evaluate() {this.value = this.get() // Evaluate attributes to this.dirty =false// Indicates that the calculated property has been calculated, no further calculation is required}depend () {
    letI = this.deps.length // Deps is an array of dePs that calculate the responsive data accessible within the attributewhile(I --) {this.deps[I].depend() // let each deP collect the current render-watcher}}}Copy the code

The variable watcher here is the computed instance of computed before computed, and then the two methods defined by the Watcher class specifically for calculating attributes are executed, During the evaluate method, get of responsive data accessible in computed data is triggered, and they collect the current computed- Watcher as a dependency in their DEP. After the calculation, they set dirty to false, indicating that the calculation has been done.

Then perform Depend to make the computed responsive data subscribe to the current render-watcher, so for computed responsive data, computed-watcher and render- Watcher are collected, When set is triggered by a change in the state of computed, computed needs to be recalculated, then rendered to the view, then rendered to the computed value, and finally rendered to the page.

Ps: The value in the calculated attribute must be responsive data to trigger recalculation.

Notification triggered when responsive data in computed changes:

class Watcher {
  ...
  update() {// When computed responsive data is triggeredsetafterif(this.lazy) {
      this.diray = true// Notification computed needs to be recalculated}... }}Copy the code

Finally, I will use an example combined with a flow chart to help clarify the logic here:

export default {
  data() {
    return {
      manName: "cc",
      womanName: "ww"
    };
  },
  computed: {
    newName() {
      return this.manName + ":" + this.womanName;
    }
  },
  methods: {
    changeName() {
      this.manName = "ss"; }}};Copy the code

Watch concludes: Why do calculated attributes have caching capabilities? Because when the calculated attribute has been calculated, the internal flag bit will indicate that it has been calculated, and the calculated value will be directly read when it is accessed again. Why does the calculated property recalculate if the reactive data in the calculated property changes? Because internal responsive data is collected in computed-watcher, changes notify computed properties to be computed, and the page is notified to be re-rendered, which reads the recalculated value.

We end this chapter with the usual interview question that Vue will be asked

The interviewer smiled politely and asked,

  • Excuse me,computedProperties andwatchIn what scenarios are attributes used?

Dui back:

  • Computed attributes can be used when a value in a template needs to be computed from one or more data, and the function that computed attributes does not take parameters. Listening attribute is mainly to listen to a value change, the new value to logical processing.

Understand the event API principle and its use in the component library

Easy to click a like or follow bai, also easy to find ~

Reference:

Vue. Js source code comprehensive in-depth analysis

Vue.js is easy to understand

Share a component library for everyone, may use up ~ ↓

A library of vUE functional components that you might want to use.