Go slow, go far

preface

A lot of people ask me: Why am I still working on Vue3.0? My answer is to go slow and go far. Version 2.0, in my opinion, lasted for a long time, I use it to do a lot of things to come out, it accompany me for a long, long time, I am very familiar with it, but 3.0 it on the old all of a sudden, in the future it go not bottom go to, I want to know all about it in the past, I’d like to know what it is how to serve me, in this way can in 3.0 when I know the reason why it is old, What new changes does 3.0 bring, and will I finally like 3.0

smallTip

After writing the previous Vue2.0 source code Reading Plan (2) — The Responsive Principle, I went to the source code group and asked a question about nextTick, which is probably where a lot of people get stuck reading the source code:

I asked: nextTick is a promise with browser support, and my understanding of the event loop is macro task => micro task => page render => next macro task… How can you get the updated DOM in $nextTick when the page has not been re-rendered?

Final answer: This simple question was complicated and led to a long discussion. We all know that the JS engine thread and the GUI rendering thread are mutually exclusive, so we should understand that updating the DOM is done immediately, but page rendering is not done until the end of this event loop, so during the microtask phase we can get the updated DOM and not care whether the page is rendered or not, which is a different thing, over.

The body of the

Watcher: indicates an observer object. Instances include computed attribute Watcher (computed Watcher), listener Watcher (user Watcher), and render Watcher (render Watcher)

Deep Watcher, Sync Watcher, deep Watcher, Deep Watcher, Sync Watcher The three types of Watcher that this article will analyze are essentially differentiated by the different parameters passed in and the different times when the watcher instance is created.

Let’s take a look at them one by one:

computed watcher

Computed Watcher is generated when initComputed. In my understanding of computed, it is used when multiple attributes affect one attribute. The biggest characteristic of computed results is that they are cached.

initComputed

const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if(process.env.NODE_ENV ! = ='production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}". `,
        vm
      )
    }

    if(! isSSR) {// create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if(! (keyin vm)) {
      defineComputed(vm, key, userDef)
    } else if(process.env.NODE_ENV ! = ='production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
Copy the code

InitComputed first defines the attribute _computedWatchers as a secure empty object on the instance, and then assigns the same definition to watchers using the same initial value object (assigns values from right to left). IsSSR is used to determine whether it is a server rendering environment. Run through the computed attributes passed in to determine the current value of the attribute. If it is a function, get the attribute directly; otherwise, get the get attribute in the attribute value. Notice that the fourth argument is {lazy: true}. The next step is to determine the repeatability of the current property. If the property does not exist in data or props (the property has been propped to the instance in the initProps and initData phases), call the defineComputed method.

defineComputed

function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  constshouldCache = ! isServerRendering()if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else{ sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! = =false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if(process.env.NODE_ENV ! = ='production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`.this)}}Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code

We first define the constant shouldCache, which is true in non-server rendering environments to indicate that it should be cached. UserDef is then judged, since it is possible to have a custom set if the value is an object. In the judgment, get and set properties of sharedPropertyDefinition are reset. The definition of sharedPropertyDefinition is as follows, which is an accessor description object:

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

The end of the code is to proxy the processed sharedPropertyDefinition to the instance property, in the same way that proxy does. Note that the createComputedGetter method is called when you reset get on sharedPropertyDefinition, which we’ll examine in a moment. We now know that the createComputedGetter method is ultimately called when we get a computed property value, using an example from the VUE website.

Use case analysis

var vm = new Vue({
  el: '#demo'.data: {
    firstName: 'Foo'.lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})
Copy the code

We use {{this.fullName}} in the template to call the createComputedGetter method to get computed property values while executing the vm._render function, which is defined as follows:

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
Copy the code

A closed higher-order function, using the watcher instance you created in initComputed originally, one property for one computed Watcher. Here we need to go back to where the watcher instance was generated and look at its constructor:

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object, isRenderWatcher? : boolean) {
  / /...
  this.dirty = this.lazy // for lazy watchers
  / /...
  this.value = this.lazy
      ? undefined
      : this.get()
}  
Copy the code

In this case, watcher. Dirty will be true (this is how the cache of calculated attributes is determined), so if computed Watcher is not evaluated immediately, go to watcher.evaluate() after the instance is created:

evaluate () {
    this.value = this.get()
    this.dirty = false
}
Copy the code

Evaluate internally by calling get and setting this.dirty to false. The next time we evaluate this computed property, instead of doing watcher.evaluate(), we return the value directly. This is why computed has a cache.

So how does this.fullName get reevaluated when computed data items are changed, such as this.firstName or this.lastName in the example above? Let’s first analyze what happens when we call this.get() inside watcher.evaluate() :

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
 }
Copy the code

PushTarget (this) sets a globally unique dependency on dep.target, In addition to this pushing the current computed Watcher in the targetStack, there should be two watcher in the targetStack at this point :[Render Watcher, Computed watcher] (note that we are in the process of generating a VNode through vm.render), pushTarget looks like this:

const targetStack = []

function pushTarget (target: ? Watcher) {
  targetStack.push(target)
  Dep.target = target
}
Copy the code

Then we evaluate this.getter.call(vm, vm). In the example above, the getter is our this.fullName function. Executing this function triggers getter collection dependencies for this.firstName and this.lastName to collect the current computed watcher, So this.firstName and this.lastName are subscribed to this computed watcher, This process also collects their respective DEP instances in the defineReactive closure in the newDeps of a computed Watcher instance.

Go on popTarget () :

function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]}Copy the code

All popTarget() does here is reset dep.target to Render Watcher. Then execute this.cleanupdeps ():

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

Here we just need to pay attention to the line this.deps = this.newDeps, which now contains deP instances of this.firstName and this.lastName (this refers to instances of computed watcher).

Evaluate (); dep.target ();

depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
}
Copy the code

It is simple enough to trigger the Depend methods of two DEP instances on DEPS in computed Watcher in turn:

depend () {
    if (Dep.target) {
      Dep.target.addDep(this)}}Copy the code

Note that dep. target is still render Watcher, addDep:

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

The two DEP instances, firstName and lastName, collect Render Watcher. Watcher.depend () This is a key step, because if firstName and lastName are not used in the template, then the render Watcher is collected from the DEP instances given here. If they were used in the template in the first place, FirstName and lastName are first collected by Render Watcher, addDep is filtered, so watcher.depend() is important.

Watcher.depend () there is a concept of two-way saving, which can be a bit convoluted. Watcher collects DEPs, and deP collects Watcher collections, as shown in my analysis above. The purpose of watcher collecting DEPs is to prevent repeated collection. As can be seen from addDep method, DEP collects Watcher to notify subscription and distribute updates.

When we update firstName or lastName, execute the setter to trigger updates for Render Watcher and computed Watcher. Computed Watcher updates are as follows:

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

Simply set this.dirty = true to true, which is synchronous, whereas render Watcher is asynchronous, and fetching computed property values during render triggers getters, Execute watcher.evaluate() again and continue rendering the interface with the new value.

To summarize, the computed property is essentially a computed Watcher, and when computed is running, the whole idea is to add the current computed Watcher to the data-dependent DEP, Trigger Render Watcher and Computed Watcher to update views when dependent data is updated.

user watcher

User watcher is initialized in the initWatch method:

initWatch

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
Copy the code

Let’s take the example of the official website to analyze:

var vm = new Vue({
  data: {
    a: 1.b: 2.c: 3.d: 4.e: {
      f: {
        g: 5}}},watch: {
    a: function (val, oldVal) {
      console.log('new: %s, old: %s', val, oldVal)
    },
    // The name of the method in the methods option
    b: 'someMethod'.// Deep listening. This callback is called whenever the property of any object being listened on changes, no matter how deeply nested it is
    c: {
      handler: function (val, oldVal) { / *... * / },
      deep: true
    },
    // This callback will be invoked immediately after the listening starts
    d: {
      handler: 'someMethod'.immediate: true
    },
    // Call multiple callbacks
    e: [
      'handle1'.function handle2 (val, oldVal) { / *... * / },
      {
        handler: function handle3 (val, oldVal) { / *... * /}}],// Listen for expressions
    'e.f': function (val, oldVal) { / *... * / }
  }
})
vm.a = 2 // => new: 2, old: 1
Copy the code

InitWatch iterates through the object first and gets the property value, which is probably an array (the e property value in the example), so createWatcher is called.

createWatcher

function createWatcher (
  vm: Component,
  expOrFn: string | Function, handler: any, options? :Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}
Copy the code

The expOrFn url is the url of the url, or the url of the url is the url of the url, or the url of the url is the url of the url, or the url of the url is the url of the url, or the url of the url is the url of the url, or the url of the url. Deep, etc.), and then execute vm.$watch.

$watch

Vue.prototype.$watch = function (
    expOrFn: string | Function, cb: any, options? :Object
  ) :Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}return function unwatchFn () {
      watcher.teardown()
    }
}
Copy the code

We’ll create an instance of watcher. When the instance is generated, we’ll execute this.get. We’ll access the properties we’re listening for internally and trigger the getter for dependency collection. This collects the User Watcher into the DEP where we listen for the data. If the data for our watch changes, it will trigger the setter to issue the update, which will eventually execute Watcher’s run method and the callback cb. If the attribute on the options passed in immediate is true, the callback is executed immediately. Finally, an unwatchFn method is returned, which calls the teardown method to remove the watcher.

This is the basic implementation of User Watcher. Let’s look at the finer divisions: Deep Watcher versus Sync Watcher.

deep watcher

We use the c attribute in the official example above as an example to analyze. When the deep attribute is defined as true, the deep listening will be enabled. When the subdata of the monitored data changes, it will also be monitored. This is how to achieve, the following specific analysis:

get() {
  let value = this.getter.call(vm, vm)
  // ...
  if (this.deep) {
    traverse(value)
  }
}
Copy the code

The above code is the get method in the Watcher class, which is invoked when user Watcher is instantiated and traverse when deep is enabled:

const seenObjects = new Set(a)/** * Recursively traverse an object to evoke all converted * getters, so that every nested property inside the object * is collected as a "deep" dependency. */
export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if((! isA && ! isObject(val)) ||Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}
Copy the code

The logic of traverse is also very simple. It’s essentially a deep recursive traverse of an object, because traversal is a call to a child object that triggers their getter procedures so that dependencies can be collected, subscriptions to user watcher. There’s a small optimization to this function implementation, Child responsive objects are logged to seenObjects with their DEP IDS during traversal to avoid repeated access later.

So after traverse, if we modify any values inside the Watch object, the user Watcher callback will be called.

sync watcher

Sync Watcher syncs when you send out updates:

update () {
  if (this.computed) {
    // ...
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)}}Copy the code

Instead of enabling the asynchronous queue mechanism, the run method is executed directly and the callback function is executed. This property will be set to true only when we need the change of watch data to execute the user Watcher callback function as a synchronization process, which is rarely used.

render watcher

Render Watcher in the last vUE 2.0 source code reading plan (ii) — responsive principle we have analyzed, occurs in the vUE instance mount phase, here is not repeated.

conclusion

Now we have a better understanding of all three types of Watcher. In terms of usage scenarios, the use of calculated properties (multiple affect one) is used when a particular data is calculated depending on other reactive data or even calculated properties. Use the listening attribute (one affects more than one) when observing changes in a value to complete a complex piece of business logic.

Welcome to pay attention to the rabbit rabbit public number long ear rabbit rabbit jinbang front, learn to exchange and share.