Vue responsive principle of source code analysis

Data responsiveness is a major feature of MVVM framework. In Vue2, object.defineProperty () is used to intercept property access by defining setter/getter of Object property, while in Vue3, Proxy() is used to intercept property access. This results in a different design of data responsiveness in Vue2 and Vue3. Below we analyze the source code Vue2 and Vue3 data response is how to achieve.

Analysis on the principle of data response in Vue2

Below we will only analyze the data responsiveness in the Vue2 source code. The initialization process of Vue is not the subject of this article.

Vue2 data responsive began in SRC/core/instance/state. The js the initData () method

function initData (vm: Component) {
  observe(data, true /* asRootData */)}Copy the code

Data is the data that we put in the choices

We go down to observe () function is defined, is located in the SRC/core/instance/observer/index, js

/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */
function observe (value: any, asRootData: ? boolean) :Observer | void {
  
  let ob: Observer | void

  // Create one when initializing
  ob = new Observer(value)

  return ob
}
Copy the code

Observe () returns an instance of an Observer. There is a new thing called an Observer class. What does an Observer class do?

class Observer {
  value: any;
  dep: Dep;
  vmCount: number;

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__'.this)
   
    // object
    this.walk(value)
  }

  /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value  type is Object. */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}
Copy the code

From the above code we can see that the Observer class accepts a value, which is the data object for which we are going to do reactive processing. There is a dep attribute inside, and a new class dep is created. Let’s look at what the new Observer() does

  1. An instance of Dep is created and copied to the Dep property
  2. Def (value, ‘ob’, this) copies the Observer instance to the __ob__ property of data

In the SRC/core/instance/observer/dep. Js We look at the Dep class do?

class Dep {
  statictarget: ? Watcher; id: number; subs:Array<Watcher>;

  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 () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if(process.env.NODE_ENV ! = ='production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) = > a.id - b.id)
    }
    // All watcher instances inside the loop
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
Copy the code

The Dep class has a Depend () method for adding subscriptions and a notify method for publishing subscriptions. What was released? When do you add a subscription? When do you publish subscriptions? What exactly is the update function? We’ll talk about that later.

Again, what does the call to Observe do?

  1. For each object passed in, an instance of an Observer is obtained, which serves primarily as a response to the object
  2. DefineReactive () is called for each key in the object, responding to each property in the object
function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  // Corresponds to the current key
  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
  const setter = property && property.set
  if((! getter || setter) &&arguments.length === 2) {
    val = obj[key]
  }

  // recursive traversal
  // Each object has an Observer instance corresponding to it
  letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // Rely on collection
      if (Dep.target) {
        // Establish a relationship with the watcher of the current component
        dep.depend()
        if (childOb) {
          // Child OB also has a relationship with the current component watcher
          childOb.dep.depend()
          // If it is an array, all internal items are processed responsively
          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()
      }
      // #7981: for accessor properties without setter
      if(getter && ! setter)return
      if (setter) {
        setter.call(obj, newVal)
      } else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify() } }) }Copy the code

DefineReactive () generates an instance of Dep for each key passed in and starts relying on the collection in object.defineProperty’s get function, notifying changes in set.

What do dependencies collect?

Dep.depend () is executed if dep.target exists, as you can see from the code above, which should be collected.

What is dep.Target?

In SRC/core/instance/lifecycle. Js in another mountComponent method, this method is used to mount components

function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {
  new Watcher(vm, updateComponent, noop, {
    before () {
      if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)}Copy the code

UpdateComponent: updateComponent: updateComponent: updateComponent

class Watcher {
  constructor() {
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () {
    pushTarget(this)}}Copy the code

PushTarget assigns an instance of Watcher to dep. target, so it collects an instance of Watcher when it relies on collection

conclusion

Vue2 response involves three classes, Observer, Dep and Watcher

Observer: Each object generates an Observer instance that holds a DEP attribute for responsive processing at the object level

Dep: Collect dependencies in get of Object.defineProperty and publish subscriptions in set

Watcher: Used to hold component update functions, which are collected as dependencies

Vue3 data responsivity principle analysis

Vue2 uses Object. DefineProperty to intercept Object attributes, whereas Vue3 uses Proxy.

The disadvantages of data responsiveness in Vue2 are:

  1. The responsiveness of arrays requires additional implementation
  2. New or delete attributes cannot be listened on, use vue. set, vue. delete
  3. Data structures such as Map, Set, and Class are not supported

Vue3 uses Proxy() to solve this problem.

Let’s copy Vue3’s responsivity principle and implement one ourselves

Step 1: Make the data responsive

const isObject = v= > typeof v === 'object'

function reactive(obj) {
  if(! isObject(obj)) {return obj
  }
  
  return new Proxy(obj, {
    get(target, key) {
      const res = Reflect.get(target, key)
      return isObject(res) ? reactive(res) : res
    },
    set(target, key, val) {
      const res = Reflect.set(target, key, val)
      return res
    },
    deleteProperty(target, key) {
      const res = Reflect.deleteProperty(target, key)
      return res
    }
  })
}
Copy the code

Step 2: Rely on the collection

Dependency collection in Vue3 is first understood with a diagram

  • Effect (CB): If fn is passed in, the returned function will be reactive, and the internal agent’s data will change, and it will be executed again
  • Track (target, key): Establishes mappings between responsive functions and the targets and keys they access
  • Trigger (arget, key): according to the mapping established by track(), find the corresponding reactive function and execute it

Let’s do it in code

// Save the dependency data structure
const targetMap = new WeakMap(a)// Create side effects
function effect(fn) {
  const e = createReactiveEffect(fn)
  e()
  return e
}

function createReactiveEffect(fn) {
  const effect = function () {
    try {
      effectStack.push(fn)
      return fn()
    } finally {
      effectStack.pop()
    }
  }
  return effect
}

// Dependency collection: Establish mappings between target,key, and FN
function track(target, key){
  const effect = effectStack[effectStack.length - 1]
  if(effect) {
    let depMap = targetMap.get(target)
    if(! depMap) { depMap =new Map()
      targetMap.set(target, depMap)
    }

    let deps = depMap.get(key)
    if(! deps) { deps =new Set()
      deps.set(key, deps)
    }

    deps.add(effect)
  }
}

// Trigger side effects: get relevant FNS by target,key, and execute them
function trigger(target, key){
  const depMap = targetMap.get(target)

  if (depMap) {
    const deps = depMap.get(key)

    if (deps) {
      deps.forEach(dep= > dep())
    }
  }
}
Copy the code

Track () is called in get() above

get(target, key) {
  const res = Reflect.get(target, key)
  track(target, key)
  return isObject(res) ? reactive(res) : res
}
Copy the code

Trigger () is called in the set

set(target, key, val) {
  const res = Reflect.set(target, key, val)
  trigger(target, key)
  return res
}
Copy the code

Combine the above two steps to create a working Vue3 Demo

reactive.js

const isObject = v= > typeof v === 'object'

function reactive(obj) {
  if(! isObject(obj)) {return obj
  }
  
  return new Proxy(obj, {
    // Target is the proxied object
    get(target, key) {
      const res = Reflect.get(target, key)
      track(target, key)
      return isObject(res) ? reactive(res) : res
    },
    set(target, key, val) {
      const res = Reflect.set(target, key, val)
      trigger(target, key)
      return res
    },
    deleteProperty(target, key) {
      const res = Reflect.deleteProperty(target, key)
      console.log('deleteproperty');
      trigger(target, key)
      return res
    }
  })
}

// Temporarily store side effect functions
const effectStack = []

// Create side effects
function effect(fn) {
  const e = createReactiveEffect(fn)
  e()
  return e
}

function createReactiveEffect(fn) {
  const effect = function () {
    try {
      effectStack.push(fn)
      return fn()
    } finally {
      effectStack.pop()
    }
  }
  return effect
}

// Save the dependency data structure
const targetMap = new WeakMap(a)// Dependency collection: Establish mappings between target,key, and FN
function track(target, key){
  const effect = effectStack[effectStack.length - 1]
  if(effect) {
    let depMap = targetMap.get(target)
    if(! depMap) { depMap =new Map()
      targetMap.set(target, depMap)
    }

    let deps = depMap.get(key)
    if(! deps) { deps =new Set()
      deps.set(key, deps)
    }

    deps.add(effect)
  }
}
// Trigger side effects: get relevant FNS by target,key, and execute them
function trigger(target, key){
  const depMap = targetMap.get(target)

  if (depMap) {
    const deps = depMap.get(key)

    if (deps) {
      deps.forEach(dep= > dep())
    }
  }
}
Copy the code
<div id="app">
  <h3>{{title}}</h3>
</div>

<script src="reactive.js"></script>

<script>
const Vue = {
    createApp(options) {
      // Web DOM platform
      const renderer = Vue.createRenderer({
        querySelector(sel) {
          return document.querySelector(sel)
        },
        insert(child, parent, anchor) {
          // Do not pass anchor, equivalent to appendChild
          parent.insertBefore(child, anchor || null)}})return renderer.createApp(options)
    },
    createRenderer({querySelector, insert}) {
      // Return the renderer
      return {
        createApp(options) {
          // The object returned is the app instance
          return {
            mount(selector) {
              const parent = querySelector(selector)

              if(! options.render) { options.render =this.compile(parent.innerHTML)
              }

              / / handle the setup
              if (options.setup) {
                this.setupState = options.setup()
              }
              if (options.data) {
                this.data = options.data()
              }

              this.proxy = new Proxy(this, {
                get(target, key) {
                  // If there is a key in setupState, use it, otherwise use the key in data
                  if (key in target.setupState) {
                    return target.setupState[key]
                  } else {
                    return target.data[key]
                  }
                },
                set(target, key, val) {
                  if (key in target.setupState) {
                    target.setupState[key] = val
                  } else {
                    target.data[key] = val
                  }
                },
              })

              this.update = effect(() = > {
                // Execute render to get the view structure
                const el = options.render.call(this.proxy)
                parent.innerHTML = ' '
                // parent.appendChild(el)
                insert(el, parent)
              })
            },
            compile(template) {
              / / compile:
              // template =》 ast =》 ast => generate render()
              // Pass in template, return render
              return function render() {
                const h3 = document.createElement('h3')
                h3.textContent = this.title
                return h3
              }
            }
          }
        }
      }
    }
  }
</script>

<script>
  const { createApp } = Vue
  const app = createApp({
    setup() {
      const state = reactive({
        title: 'vue3,hello! '
      })

      setTimeout(() = > {
        state.title = 'hello'
      }, 2000);
      return state
    }
  })
  app.mount('#app')
</script>
Copy the code

conclusion

Vue3 uses Proxy() for responsive data, making it more powerful and easier to understand the collection and publishing of dependencies.