Play an advertisement 🤭 on a Vue3.0 experience, imitation of a NetEase cloud music client

Why write such an article at this time?

It’s 2021, and several months have passed since Vue3.0 was officially released. However, it is not enough for the framework to just be able to use it. In order to understand its relevant principles, we need to know what the difference is between it and previous versions and what the advantages are. So today we re – product 2. X version of responsive source code!

Each of the following stages may have some methods or object instances temporarily unable to understand, it doesn’t matter, when you read the complete process of the article, hand knock again, you will suddenly understand 🤭

The function and relationship of Observer, Dep and Watcher

Those of you who have used Vue more or less know that Utah uses a data hijacking and subscription-publish pattern to achieve responsiveness, which is dependent on the following three objects.

Observer

Observer listeners are used to hijack responsive objects by adding getters and setters, and are used as an intermediate station for Dep and Watcher to collect and update dependent data. Its related source code and analysis are as follows:


// it is called when the vUE is initialized
function initData(vm) {
    let data = vm.$options.data
    // This assumes that data is a function that returns an object
    data = vm._data = data.call(vm, vm)
    const keys = Object.keys(data);
    // Delegate _data to this
    for (let i = 0; i < keys.length; i++) {
        proxy(vm, '_data', keys[i]);
    }
    // If this is the case, the props and methods in option are the same as each other

    observe(data, true)}function observe(value, asRootData) {
    // If it is not an object or a virtual DOM node, no observation is required
    if(! isObject(value) || valueinstanceof VNode) {
        return
    }
    let obj
    // If it is already monitored, there is no need to instantiate it
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        obj = value.__ob__
    } else {
        obj = new Observer(value)
    }

    return obj
}

// Property proxy method
function def(obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable:!!!!! enumerable,writable: true.configurable: true})}class Observer {
    constructor(value) {
        this.value = value
        // Here deP is for array dependency collection
        this.dep = new Dep()

        // The def proxy's __ob__ is important because the array uses the proxy's observer object
        def(value, '__ob__'.this)
        // Extra processing is required if the value is an array, because Object.defineProperty cannot listen for any operations that change the length of the array, as well as methods on the prototype.
        // This is why Utah doesn't handle arrays, instead using $set and proxy variants
        if (Array.isArray(value)) {
            // Proxy array variation method, reassign the prototype
            value.__proto__ = arrayMethods
            this.observeArray(value)
        } else {
            // Otherwise, the object is iterated over, hijacking its properties
            this.walk(value)
        }
    }
    // Listen to the object
    walk(obj) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i])
        }
    }
    Arr [I].xxxx = XXX; arr[I] = XXX; arr[I] = XXX
    observeArray(array) {
        for (let i = 0; i < array.length; i++) {
            observe(array[i])
        }
    }
}




Copy the code

Observe that the observe method value is data or its child object, and then instantiate Obverver to treat the value as an object or an array.

Object to hijack

Object walks through the Object’s keys, passing each key along with the original OBJ to the defineReactive method, which calls Object.defineProperty. The method analysis is as follows:

/** * Each key corresponds to a dep. Obj is the object we need to hijack, such as the initial data, and the object val in the data is manually set. By default, it is not required **/
function defineReactive(obj, key, val) {
    const dep = new Dep()

    // Get the description object of the object attribute
    const property = Object.getOwnPropertyDescriptor(obj, key)
    // If the value of the object has been set previously and is set to an unchangeable value, no listening is required
    if (property && property.configurable === false) {
        return
    }

    // If the property of the original object defines the corresponding GET and set, it should be consistent with them
    const getter = property && property.get
    const setter = property && property.set
    // In the case of non-set, get is not defined, i.e. val is undefined and needs to be given its original value
    if((! getter || setter) &&arguments.length === 2) {
        val = obj[key]
    }
    // Recursively collect dependencies
    let ob = observe(val)
    Object.defineProperty(obj, key, {
        // Can be traversed, can be modified
        enumerable: true.configurable: true.get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val
            // Dep is a dependency collector, as described later. For now, just know that Dep. Target is a Watcher instance
            if (Dep.target) {
                // Rely on collection
                dep.depend()
                / / if val object or array, ob is observe the object, or undefined
                Ob.dep. depend for array method dependency collection
                if (ob) {
                    ob.dep.depend()
                    if (Array.isArray(value)) {
                        Ob.dep. depend depends on the methods of the outermost array
                        // But consider the case if the value is also an array, i.e., a collection of dependencies of multi-dimensional arrays
                        dependArray(value)
                    }
                }
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val
            if (newVal === value) return
            // If object properties cannot be set
            if(getter && ! setter)return
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            // New data is processed recursively and responsively
            ob = observe(newVal)
            // Notify data update
            dep.notify()
        }
    })
}

// Dependency collection for multi-dimensional array variation methods
function dependArray(array) {
    for (let i = 0; i < array.length; i++) {
        const element = array[i];
        // __obj__ is what def does
        element && element.__ob__ && element.__ob__.dep.depend()
        if (Array.isArray(element)) {
            dependArray(element)
        }
    }
}
Copy the code

An array of hijacked

An array is a special case where each value is not handled with Object.defineProperty like an Object, which is why this.a[0] = XXX is invalid. This. A [0]. XXX = XXX is valid because the observe method listens for each value in the array. The operation of adding, deleting, modifying and checking arrays is realized by hijacking the variation methods on their prototypes and calling methods for dependency collection. Its implementation analysis is as follows:

const arrayMethods = Object.create(Array.prototype)
// Array variation method
const methodsToPatch = [
    'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]

// Here is the mutation method for hijacking
methodsToPatch.forEach((method) = > {
    // The original method of the array
    const original = arrayMethods[method]
    def(arrayMethods, method, function (. args) {
        const result = original.apply(this, args)
        Def proxies the current obsever instance to value, which in the case of an array is the array itself
        const ob = this.__ob__
        The value of / / push/unshift/splice
        let inserted
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
                break
        }
        // The new value needs to be processed in a responsive manner
        if (inserted) ob.observeArray(inserted)
        // Change notification
        ob.dep.notify()
        return result
    })
})

Copy the code

summary

At this point, aside from the DEP-related code, you should have some idea of how the Observer hijacks data. ObserveArray, defineReactive, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray, observeArray The object instantiates an Observer instance. It is in this’ recursive ‘way that we listen for the data value we pass in and its children!

Hope you can continue to read, when you finish Watcher and Dep related source code, look back here, there will be a different harvest!

Dep

The dependency collector, which collects the dependencies of reactive objects, instantiates a Dep instance for each reactive object, including its children, in defineReactive for objects and in defineReactive for arrays, which, as mentioned earlier, hijks its mutated methods. The deP of each deP instance is instantiated in an Observer class, and subs of each DEP instance is an array of Watcher instances. When data changes, each Watcher will be notified through DEP. notify.

let depId = 0
class Dep {
    constructor() {
        this.id = depId++
        this.subs = []
    }
    removeSub(sub) {
        // Where sub is watcher
        remove(this.subs, sub)
    }
    addSub(sub) {
    	// Where sub is watcher
        this.subs.push(sub)
    }
    // The watcher instances depend on each other for collection
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)}}notify() {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
        // Update notifications
            subs[i].update()
        }
    }
}


Dep.target = null

function remove(arr, item) {
    if (arr.length) {
        var index = arr.indexOf(item);
        if (index > -1) {
            return arr.splice(index, 1)}}}Copy the code

Watcher

The subscriber, or observer, is responsible for our relevant update operations. There are three types of Watcher: renderWatcher (responsible for component rendering), Watcher(calculated Watcher) and Watcher(userWatcher) for watch property. When a responsive object performs an update operation, The setter method is triggered to call dep.notify of its corresponding dependent collector instance and update the collected Watcher array.

class Watcher {
    dirty
    getter
    cb
    value
    // VM is the vUE instance object
    / / expOrFn for updating method, renderWatcher for our updateComponent update view, userWatcher/computedWatcher for evaluation method
    // cb is a callback, mainly used in userWatcher and our watch property
    constructor(vm, expOrFn, cb, options, isRenderWatcher) {
        this.vm = vm
        if (isRenderWatcher) {
            vm._watcher = this
        }
        vm._watchers.push(this)
        this.id = ++wId
        this.cb = cb
        // Record the last evaluated dependency
        this.deps = []
        // Record the current evaluated dependency
        this.newDeps = []
        this.depIds = new Set(a)this.newDepIds = new Set(a)// Optionslazy is the parameter of the computedWatcher, and user is the parameter of the userWatcher
        if (options) {
            this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.sync }else {
            this.deep = this.user = this.lazy = this.sync = false
        }
        this.dirty = this.lazy
        if (typeof expOrFn === 'function') {
            this.getter = expOrFn
        } else {
            // If it is watch
            this.getter = parsePath(expOrFn)
        }

        // When renderWatcher and userWatchernew watcher are used, the get method is called
        // If computeWatcherThis. lazy is true, the get method will not be called
        this.value = this.lazy ?
            undefined :
            this.get()
    }

    get() {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
            // The getter here is the updateComponent method for renderWatcher, the key for watch, and computed is the method
            value = this.getter.call(vm, vm)
        } catch (error) {

        } finally {
            // This is for watch deep
            if (this.deep) {
                traverse(value)
            }
            popTarget()
            this.cleanupDeps()

        }
        return value
    }

    // Add yourself to the deP
    addDep(dep) {
        const id = dep.id
        // The two DEPs are used to prevent repeated collection of dependencies
        if (!this.newDepIds.has(id)) {
            this.newDepIds.add(id)
            this.newDeps.push(dep)
            if (!this.depIds.has(id)) {
                dep.addSub(this)}}}// Add all existing dependencies
    depend() {
        let i = this.deps.length
        while (i--) {
            this.deps[i].depend()
        }
    }
    update() {
        // For computed data, initializing computedWatcher lazy is true and always is
        // So computedWatcher does not execute run, but triggers its computedWatcher when the value of the data attribute in its calculation method changes, setting dirty to true.
        // Evaluate gets the latest value because dirty is true. (This is a very clever design, and it doesn't compute the value until it's needed.)
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
            this.run()
        } else {
            queueWatcher(this)}}// recalculate the value to calculate the method called by the property
    evaluate() {
    	// For computed properties, the get method calls the computed object method it declared in the Vue instance
        // For example, computed: {name(){return this.first + thi.second}}, get computes the values of the attributes of the two data objects returned by name.
        // Before this, the watcher itself is added to the subs array of the deP of the frist/second responsive data, and changes in the two values trigger changes in the computed property.
        // Also, as you will see later, the watcher id is sorted in the update operation, because the computedWatcher is instantiated before the renderWatcher(which is instantiated at mount time), so its value changes before rendering
        // That's why the page sees updated values for computed attributes.
        this.value = this.get()
        this.dirty = false
    }

    run() {
        const value = this.get()
        if(value ! = =this.value || isObject(value) || this.deep) {
            const oldValue = this.value
            this.value = value
            this.cb.call(this.vm, value, oldValue)
        }
    }

    // Remove useless dependencies
    cleanupDeps() {
        let i = this.deps.length
        while (i--) {
            const dep = this.deps[i]
            // If the current new dependency list does not exist, the old dependency does exist, then the dependency collection needs to be removed
            if (!this.newDepIds.has(dep.id)) {
                dep.removeSub(this)}}// The current dependency list acts as the last dependency list, and then resets the current dependency list
        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}}const targetStack = []

// Depends on the collection array
// dep. target is a Watcher
function pushTarget(target) {
    targetStack.push(target)
    Dep.target = target
}

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

Several links to the above are as follows

  • Depend on the collection

    One of the concepts that I’ve been talking about, you know, deP collects the Watcher, how do you collect the Watcher? In the Observer, the object will determine whether there is a dep. target during the value operation. In the Watcher, the new Watcher will be called when the Watcher is instantiated. When Wacther is instantiated, it calls the get method based on the watcher type. The GET method assigns the current watcher instance to dep. target, thus collecting the dependencies. Next, let’s analyze when Watcher instantiation takes place.

  • When new Watcher is called

    • renderWatcher: be responsible fordataThe update of data to view needs to be updated when every data changes, which must be after the initialization of data, calculation properties and watch, and at the same time must be correctdataEach attribute of thegetterThe dependency collection is triggered by the action (the value operation is done when the view is rendered), so it is instantiated when the component is mounted.
      // Here is a rough implementation of mount and update methods, but it is much more than that
      
      const vm = new Vue({
          data() {
              return {
                  name: 'cn'.age: 24.wife: {
                      name: 'csf'.age: 23}}},computed: {
              husband() {
                  return this.name + this.wife.name
              }
          },
          watch: {
              wife: {
                  handler(val, oldVal) {
                      console.log('watch--->',val.name, oldVal.name)
                  },
                  deep: true.immediate: true}},render() {
              return `
                  <h3>normal key</h3>
                  <p>The ${this.name}</p>
                  <p>The ${JSON.stringify(this.wife)}</p>
                  <h3>computed key</h3>
                  <p>The ${this.husband}</p>
              `;
          }
      }).$mount(null.document.getElementById('root'))
      
      function mountComponent(vm, el, hydrating) {
        vm.$el = el;
        const _updateComponent = function (vm) {
            vm._update(vm._render(), hydrating)
        }
        // That's right, _updateComponent is our view update method
        // Because lazy is not set, watcher will be called directly when instantiated
        // _updateComponent -> _render -> render the page
        new Watcher(vm, _updateComponent, noop, {}, true)
        return vm
      }
      Vue.prototype.$mount = function (el, hydrating) {
              return mountComponent(this, el, hydrating)
          };
    
      Vue.prototype._update = function (node, hydrating) {
          hydrating.innerHTML = node
      }
    Copy the code
    • ComputedWatcher: Responsible for calculating property changes. The Vue instance is instantiated as soon as it is initialized, before renderWatcher, for reasons explained in the previous comment. Let’s see how this is instantiated.
    // initState is called in vue.prototype. _init._init is called when Vue is instantiated
    function initState(vm) {
      // destory
      vm._watchers = [];
      // The parameters here are our data, computed, watch, render.....
      const opts = vm.$options
      if (opts.data) {
      // Initializing data, that is, responding to data, as we'll talk about in a moment
          initData(vm)
      }
      // This is initComputed
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch) {
          // init our watch here
          initWatch(vm, opts.watch)
      }
    }
    
    function initComputed(vm, computed) {
      // Calculate the wacher object of the property
      const watchers = vm._computedWatchers = Object.create(null)
    
      for (const key in computed) {
          const userDef = computed[key]
          // Because compouted has function form or set/get mode
          const getter = typeof userDef === 'function' ? userDef : userDef.get
          // Instantiated here
          watchers[key] = new Watcher(
              vm,
              getter || noop,
              noop,
              // Note that lazy initialization is true
              { lazy: true})if(! (keyin vm)) {
              defineComputed(vm, key, userDef)
          }
      }
    }
    
    function defineComputed(target, key, userDef) {
      // For now, the default is not server-side rendering
      // const shouldCache = ! isServerRendering()
      if (typeof userDef === 'function') {
          sharedPropertyDefinition.get = createComputedGetter(key)
          sharedPropertyDefinition.set = noop
      } else {
          sharedPropertyDefinition.get = createComputedGetter(key)
          sharedPropertyDefinition.set = userDef.set || noop
      }
      
      This.com putedxxx will trigger the following computedGetter to perform the value operation
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    
    
    function createComputedGetter(key) {
        return function computedGetter() {
            const watcher = this._computedWatchers && this._computedWatchers[key]
            if (watcher) {
                // Dirty is true at the beginning of new
                // For the first time, computed values are undefined because dirty is true
                // There is no need to calculate the value at first (waste), only the evaluate will be recalculated.
                if (watcher.dirty) {
                 // Call evaluate and set it to false
                 // Unless the dependent data is changed and dirty is set to true, no fetching is required
                    watcher.evaluate()
                }
                if (Dep.target) {
                    watcher.depend()
                }
                return watcher.value
            } 
        }
    }
    Copy the code
    • UserWatcher: Watch listens for changes. The Vue instance is instantiated as soon as it is initialized, before renderWatcher. This may be a bit convoluted, but you need to look at it a few more times and debug it a few more times. Let’s see how it is instantiated.
      stateMixin(Vue)
      function stateMixin(Vue) {
        // expOrFn is the key of the value we want to listen for, cb is the callback we pass
        Vue.prototype.$watch = function(expOrFn, cb, options) {
            const vm = this
            options = options || {}
            // The user in watcher is for watch
            options.user = true
            // The watcher get method is called immediately
            const watcher = new Watcher(vm, expOrFn, cb, options)
            // If watch is configured to fetch values immediately
            if (options.immediate) {
                try {
                    cb.call(vm, watcher.value, null)}catch (error) {
                }
             }
           }
      }
       
    
      function initWatch(vm, watch) {
           for (const key in watch) {
                // This is a callback defined by user
                const handler = watch[key]
                createWatcher(vm, key, handler)
            }
      }
      function createWatcher(vm, expOrFn, handler, options) {
          // For the case where the watch value is an object
          if(isPlainObject(handler)) {
              options = handler
              handler = handler.handler
          }
          return vm.$watch(expOrFn, handler, options)
      }
      
    
      // Let's review the above
      class Watcher {
        dirty
        getter
        cb
        value
        constructor(vm, expOrFn, cb, options, isRenderWatcher) {
            if (options) {
                this.user = !! options.user }else {
                this.deep = this.user = this.lazy = this.sync = false
            }
            if (typeof expOrFn === 'function') {
                this.getter = expOrFn
            } else {
                // This is the way to go
                this.getter = parsePath(expOrFn)
            }
            // When new watcher is invoked, the get method is called
            this.value = this.lazy ?
                undefined :
                this.get()
        }
    
        get() {
            pushTarget(this)
            let value
            const vm = this.vm
            try {
                // 
                value = this.getter.call(vm, vm)
            } catch (error) {
    
            } finally {
                // This is for watch deep
                // If deep is deep, it needs to recursively traverse all subproperties of the data property of watch
                // Add the current userWatcher to their DEP to make deep changes
                if (this.deep) {
                    traverse(value)
                }
                popTarget()
                this.cleanupDeps()
    
            }
            return value
        }
        
        run() {
            const value = this.get()
            if(value ! = =this.value || isObject(value) || this.deep) {
                const oldValue = this.value
                this.value = value
                // Cb is our callback
                this.cb.call(this.vm, value, oldValue)
            }
          }
      }
      
      function parsePath(path) {
          // select A.B.C from watch
          const segments = path.split('. ');
          // Obj will be assigned to the VM, so return key in the data of watch
          return function (obj) {
            for (let i = 0; i < segments.length; i++) {
              if(! obj) {return }
              obj = obj[segments[i]];
            }
            return obj
          }
      }
    
      // To prevent repeated dependency on collections
      const seenObjects = new Set(a)Deep can be implemented by recursively traversing the values of watch's responsive objects that require deep for dependency collection
      function traverse(val) {
          _traverse(val,seenObjects)
          seenObjects.clear()
      }
    
      function _traverse (val, seen) {
          let i, keys
          const isA = Array.isArray(val)
          if((! isA && ! isObject(val))) {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
  • To summarize

    With the complete Watcher object and several Watcher instances analyzed above, you can get a general idea of when and how Watcher binds to our reactive data to publish -> subscribe.

If you read all the previous sections, you should have a general idea of the process, which is as follows

  • Data hijacking: traversaldataIf the value of the property is not an array, use theObject.definePropertyAdd a propertygetterandsetter, is the array is hijack its mutation method, recursively traverses its child object, traverses the number group, repeat the operation.
  • Watcher instantiation: differentwatcherInstantiate at different stages, rightdataData is selected for dependency collection.
  • Modify responsive dataTrigger:setter, the calldep.notify, traverses the subs array on its corresponding DEP, and is calledwatcher.update.

Update strategy

That’s basically it, but there’s still one more thing that’s going to happen to our update operation, watcher.update.

When we go to watcher.update, instead of calling watcher.run, we update the view asynchronously. If we don’t update the view asynchronously, every time we call this. XXX = XXX, the view will update. If we update a lot of data at one time, or we just want the last result, the transformation in the middle is invalid, is there a lot of disadvantages?

So what Utah did was, Watcher saw that there was a change in the data, and it started a queue to buffer all the changes that happened in the same event cycle. If a watcher is triggered more than once, it will only be pushed to the queue once (removing duplicate data, useless arrays, fetching the last time). Then the actual update is fired in the Tick of the next event loop. For details, see vUE asynchronous update mechanism.

let waiting = false
let has = {}
// Cache the watcher array
const queue = []

// Caches watcher in an update
function queueWatcher(watcher) {
    const id = watcher.id
    if(! has[id]) { has[id] =true
        queue.push(watcher)
        if(! waiting) { waiting =true
            nextTick(flushSchedulerQueue)
        }
    }
}

let index = 0
The flushScheduleQueue function is used to flushScheduleQueue
// It will fetch all the watcher in the queue and perform the corresponding update
function flushSchedulerQueue() {
    flushing = true
    let watcher, id
    // Watcher sort by order
    queue.sort((a, b) = > a.id - b.id)
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
            watcher.before()
        }
        id = watcher.id
        has[id] = null
        watcher.run()
    }
    resetSchedulerState()
}


/ / reset
function resetSchedulerState() {
    index = queue.length = 0
    has = {}
    waiting = flushing = false
}


// Callback is only flushSchedulerQueue
// When we define $nextTick ourselves, we will add it here
const callbacks = []
let pending = false
function nextTick(cb, ctx) {
    callbacks.push(() = > {
        if (cb) {
            try {
                cb.call(ctx)
            } catch (e) {
            }
        }
    })
    // Prevent repeated execution
    if(! pending) { pending =true
        timerFunc()
    }
}

const p = Promise.resolve()
// Here we have default browser support for Promise, whose source code determines a variety of cases
let timerFunc = () = > {
    p.then(flushCallbacks)
}

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

The last

The content is also quite a lot of round, at the beginning may be quite confused, but the whole process related to all go through, in fact, will happen its design beauty. I feel the comments are relatively complete, empty code is more difficult to fully understand, so I suggest the following demo I hand knock, compared to step by step debugging. Dist /vue.js, dist/vue.js, dist/vue.js. Sorting is not easy, if there is something wrong can be pointed out in the comment area, if you feel helpful, I hope to give three even 🤭!

The demo address