Vue principle analysis (five) : thoroughly understand the virtual Dom to the real Dom generation process

The key to vUE’s ability to make data-driven view changes is its responsive system. Responsive systems can be implemented differently if they differentiate between objects and arrays by data type; Explain the responsive principle, if only to explain the responsive principle, but not from the overall process, not find the position of the responsive principle in the overall process of VUE componentization, it is not good for a deep understanding of the responsive principle. Next, I will start from the overall process and try to stand on the shoulders of giants to explain the implementation principles of objects and arrays respectively.

Object responsivity principle

Object creation of responsive data

  • During the initialization phase of the component, the incoming state is initialized, as followsdataFor example, incoming data is wrapped as responsive data.
Object example: main.js new Vue({// Root component render: h => h(App) }) --------------------------------------------------- app.vue <template> <div>{{info.name}}</div> // Using only the info.name attribute </template>exportDefault {// app componentdata() {
    return {
      info: {
        name: 'cc',
        sex: 'man'// Even if responsive data is not used, there is no dependency collection}}}}Copy the code

It is important to understand that this structure is actually a nested component, but the root component usually has fewer parameters defined.

During the vm._init() initialization after component new Vue(), some internal states, such as props, data, computed, watch, and methods, are initialized when executing initState(VM). Initialize data with this statement:

functionInitData (vm) {// Initialize data... observe(data) // info:{name:'cc',sex:'man'}}Copy the code

Observe turns user-defined data into responsive data. Here’s how it was created:

export function observe(value) {
  if(! IsObject (value)) {// Not an array or an object, byereturn
  }
  return new Observer(value)
}
Copy the code

The observe method is the factory method of the Observer class, so let’s take a look at the definition of the Observer class:

exportClass Observer {constructor(value) {this.value = value this.walk(value)} walk(obj) {const keys = Object.keys(obj)for(leti = 0; i < keys.length; I ++) {defineReactive(obj, keys[I])}Copy the code

When executing a new Observer, first mount the incoming object under the current this, then iterate over each item of the current object, using the defineReactive method and see what it defines:

export functiondefineReactive(obj, key, Observe (val) {const dep = new dep () // Dependency manager val = obj[key] // Calculate the corresponding key Object.defineProperty(obj, key, { enumerable:true,
    configurable: true.get() {... Collect dependencies},set(newVal) { ... Send out updates}})}Copy the code

This method is used to create reactive data using Object.defineProperty. Calculate the value of val based on the obj and key passed in. If val is still an Object, use the observe method to recursively create val. Use Object.defineProperty to make each attribute of the Object responsive during recursion:

.data() {
  return {
    info: {
      name: 'cc',
      sex: 'man'}}} this code will have three response data: info, info.name, info.sexCopy the code

DefineProperty the get method in object.defineProperty collects the current key value from whoever accesses it using the DEP in defineReactive. The set method notifies all dependencies collected in the DEP that the value of the key has been changed

Get and set are defined and not fired. What is dependency? First, let’s use a diagram to help you understand the process of creating responsive data:

Depend on the collection

What is dependency? Let’s look at the previous mountComponent definition:

function mountComponent(vm, el) {
  ...
  const updateComponent = function() {vm._update(vm._render())} new Watcher(vm, updateComponent, noop, {// render Watcher... },true) / /trueFlag, indicating whether to render watcher... }Copy the code

The VNode class can instantiate the user Watcher, compute Watcher, and render Watcher instances, depending on the parameters passed in.

The user (user) watcher

  • This is defined by the user, for example:
new Vue({
  data {
    msg: 'hello Vue! '
  }
  created() {
    this.$watch('msg', cb()) // define user watcher}, watch: {msg() {... } // define user watcher}})Copy the code

Both methods are instantiated internally using the Watcher class, but the parameters are different. The implementation will be explained later in the chapter. We only need to know that this is the user Watcher.

Computing (computed watcher

  • As the name implies, this is one of the types instantiated when defining computed properties:
new Vue({
  data: {
    msg: 'hello'  
  },
  computed() {
    sayHi() {// calculate watcherreturn this.msg + 'vue! '}}})Copy the code

Rendering (render) watcher

  • This is defined for view rendering purposes onlyWatcherInstance, and then component executionvm.$mountIs instantiated at the endWatcherClass, this time to renderwatcherThe collection is the current renderwatcherFor example, let’s look at how it is defined internally:
class Watcher {
  constructor(vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm
    if(isRenderWatcher) {render vm._watcher = this} vm._watcher = this} vm._watcher = this [] this.before = options.before // Render watcher unique attribute this.getter = expOrFn // Second parameter This.get () // instantiation executes this.get()}get() {pushTarget(this) // Add... This.getter. call(this.vm, this.vm) // execute vm._update(vm._render())... PopTarget () // Remove} addDep(dep) {... Dep.addsub (this) // Collect the current watcher into the DEP instance}}Copy the code

PushTarget (this) is a global method that passes in an instance of the current Watcher. Let’s look at where this method is defined:

Dep.target = null const targetStack = [] // Set of watcher instances corresponding to the component from parent to childexport functionPushTarget (_target) {// Addif(dep.target) {targetstack.push (dep.target) // add to set} dep.target = _target // Current watcher instance}export function popTargetDep.target = targetStack[targetstack.length-1] // Assign the value to the last item in the array}Copy the code

Dep. Target is null. This is a global attribute that holds the instance of the current component rendering watcher. TargetStack stores the set of rendered Watcher instances for each component in the componentization process, using an advanced and out form to manage the array’s data. This may be a bit difficult to understand, but we will see the final flow diagram. The passed Watcher instance is then assigned to the global property dep.target, which is collected during subsequent dependency collection.

The watcher get method then executes the getter method, which is the second argument passed to the new Watcher. This is the previous updateComponent variable:

function mountComponent(vm, el) {
  ...
  const updateComponent = function() {vm._update(vm._render())}... }Copy the code

Vm._update (vm._render())) on the current component instance will be executed to convert the render function to VNode. If the render function contains data that has been converted to responsivity, the get method will be triggered to collect dependencies. Logic that relies on collection before completion:

export functiondefineReactive(obj, key, Observe (val) {const dep = new dep () // Dependency manager val = obj[key] // Calculate the corresponding key Object.defineProperty(obj, key, { enumerable:true,
    configurable: true.get() {// triggers dependency collectionif(dep.target) {// The current watcher instance that was previously assigned dep.depend() // collect it and put it into the Dep dependency manager above... }return val
    },
    set(newVal) { ... Send out updates}})}Copy the code

Watcher is a tool that encapsulates the communication between data and components. When a data is read by a component, the component that depends on the data is collected using the Dep class.

The property in the current data example is only one render Watcher because it is not used by other components. But if this property is used by other components, components that use it are also collected, such as props passed to sub-components, and there will be multiple render Watchers in the ARRAY of the DEP. Let’s look at the definition of the dependency manager for the Dep class:

let uid = 0
export default class Dep {
  constructor() {this.id = id++ this.subs = [] // a key dependency set} addSub(sub) {// Add watcher instance to array this.subs.push(sub)}depend() {
    if(dep.target) {// Already assigned to an instance of watcher, dep.target.adddep (this) // Executes watcher's addDep method}}} ---------------------------------------------------------- class Watcher{ ... AddDep (dep) {// Add the current watcher instance to the deP... Dep.addsub (this) // Execute the addSub method of dep}}Copy the code

The purpose of this Dep class is to manage the watcher of the property, such as add/remove/notification. At this point, the process of relying on collection is complete, or a picture to deepen the understanding of the process:

Distributed update

It doesn’t really make sense to just collect dependencies; it makes sense to notify the collected dependencies when the data changes and cause the view to change. Now we reassign the data:

app.vue
exportDefault {// app component... methods: {changeInfo() {
      this.info.name = 'ww'; }}}Copy the code

This will trigger the set method for creating reactive data, and we will complete the logic there:

export functiondefineReactive(obj, key, Observe (val) {const dep = new dep () // Dependency manager val = obj[key] // Calculate the corresponding key Object.defineProperty(obj, key, { enumerable:true,
    configurable: true.get() {... Dependency collection},set(newVal) {// Send updatesif(newVal === val) {// samereturn} val = newVal // Assign observer(newVal) // If the new value is the object also recursively wrapped dep.notify() // notify update}})}Copy the code

When an assignment triggers a set, it first checks that the new and old values cannot be the same; Then assign the new value to the old value; If the new value is an object, make it reactive; Finally, let the dependency manager of the corresponding property notify the view with dep.notify. Let’s look at its implementation:

let uid = 0
class Dep{
  constructor() {
    this.id = uid++
    this.subs = []
  }
  
  notify() {// Notify const subs = this.subs.slice()for(leti = 0, i < subs.length; I ++) {subs[I].update() // Trigger watcher update method in sequence}}}Copy the code

There is only one thing to do here, which is to traverse the collected Watcher one by one to trigger the update method:

class Watcher{
  ...
  update() {
    queueWatcher(this)
  }
}

---------------------------------------------------------
const queue = []
let has = {}

function queueWatcher(watcher) {
  const id = watcher.id
  if(has[id] == null) {// If a watcher is not queued... has[id] =true// Queue.push (watcher) // push to queue}... NextTick (flushSchedulerQueue) // nextTick update}Copy the code

QueueWatcher () : queueWatcher () : queueWatcher () : queueWatcher (); queueWatcher () : queueWatcher (); The corresponding Watcher will only be updated once. Here are two small examples:

export default {
  data() {
    returnNum: 0, name:'cc',
      sex: 'man'
    }
  },
  methods: {
    changeNum() {// Assign 100 timesfor(let i = 0; i < 100; i++) {
        this.num++
      }
    },
    changeInfo() {// Assign multiple attributes at once this.name ='ww'
      this.sex = 'woman'}}}Copy the code

The three reactive properties here are all collected from the same render Watcher. So when 100 assignments occur and the current render Watcher is pushed behind the set queue, no render Watcher will be added to the set queue triggered by subsequent assignments. This is also true when multiple attributes are assigned at the same time, because they all collect the same render Watcher and are not added after being pushed to the queue once.

Vue is clever, as you can see from these two examples. The granularity of sending update notifications is component-level. It does not care which attribute is assigned to the component, and how to update the view efficiently is what diff comparison does later.

NextTick is the original method of the this.$nextTick method, which is often used in the flushSchedulerQueue. What is the argument flushSchedulerQueue?

let index = 0

function flushSchedulerQueue() {
  letWatcher, id queue.sort((a, b) => a.id -b.id) // watcher sortfor(index = 0; index < queue.length; Watcher = queue[index]if(watcher.before) {// Render watcher unique property watcher.before() // trigger beforeUpdate hook} id = watcher.id has[id] = null Watcher.run () // The real update method... }}Copy the code

It was a function, and inside the nextTick method the first argument is executed. The queue will be sorted once, based on the ids generated by each new Watcher, in descending order. The current example just does render, and there is only one render Watcher in the queue, so there is no order. But if you define User watcher and computed Watcher plus Render Watcher, there is a problem of execution order between them.

The order in which watcher is implemented is from parent to child, then from computed watcher to user watcher to render Watcher, as can be seen from their initialization order.

It then iterates through the queue, since it is rendering watcher, all with the before attribute, and executes the passed before method to trigger the beforeUpdate hook. Finally, the watcher.run() method executes the actual dispatch update method. Let’s take a look at what Run did:

class Watcher {
  ...
  run () {  
    if(this.active) {this.getAndInvoke(this.cb)}} getAndInvoke(cb) {// Render Watcher's cb to noop null const Value = this.get() ... User watcher logic}}Copy the code

To execute Run is to execute the getAndInvoke method because it is render Watcher and the cb parameter is noop null. After reading so much, actually… Just re-execute this.get() and let vm._update(vm._render()) run again. The old and new VNodes are then generated, and finally a diff comparison is performed to update the view.

Finally, let’s talk about some shortcomings of vUE’s responsive system based on Object.defineProperty. For example, only changes in data can be monitored, so sometimes a bunch of initial values have to be defined in data, because they can only be sensed after joining a responsive system. There is also the normal JavaScript way of manipulating objects that does not listen for additions and deletions, such as:

export default {
  data() {
    return {
      info: {
        name: 'cc'
      }
    }
  },
  methods: {
    addInfo() {// Add attribute this.info.sex ='man'
    },
    delInfo() {// delete attribute delete info.name}}}Copy the code

The data is assigned, but the view does not change. Vue solves this problem by providing two apis: $set and $delete. How do they work? After the principle of chapter analysis.

Finally, the customary interview q&A is about something funny that happened at work recently. For a list whose data does not change, I define it in an Created hook, which is rarely paired, except this time.

created() {
  this.list = [...]
}
Copy the code

Next to the girl answered:

Girl: Why is this list not in the data? Where is it defined? Me: I defined it in the Created hook. Girl: How do you define being here? Me: Because it's immutable, there's no need to... Well, you can move it to data. Girl: HMM! ? Ok. Whispered: THIS is the first time I've seen it written. I:... I feel like I'm being rejectedCopy the code

The interviewer smiled politely and asked,

  • Variables used in the current component template must be defined indata?

Dui back:

  • dataAll variables in thethisDown, so we can alsothisMount properties, as long as they do not have the same name. And defined indataThe variable ofvueIt wraps it up as responsive data, giving it the ability to change and drive changes to the view. But if this data doesn’t need to drive the view, define it increatedormountedHooks are also possible because reactive wrapping is not performed, which is also a performance boost.

Next: Vue principle analysis (seven) : a comprehensive in-depth understanding of the responsive principle (ii)- array advanced chapter

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.