preface

Recently in-depth study of Vue to achieve responsive part of the source code, I will be a little harvest and thinking down, I hope to see this article to help people. If you have any questions, please point them out. We will make progress together.

What is a responsive system

In a word: Data changes drive view updates. This allows us to write our code with a “data-driven” mindset, focusing more on the business than dom manipulation. In fact, the implementation of Vue responsiveness is a process of change tracking and change application.

Vue responsive principle

Intercept data changes in the way of data hijacking; Trigger view updates as a dependency collection. Use ES5 Object.defineProperty to intercept setters and getters for data; The getter collects the dependency, the setter triggers the dependency update, and the component Render becomes a Watcher callback added to the dependency.

Release subscription

It is implemented by publishing and subscribing design pattern, with Observer as publisher and Watcher as subscriber, without direct interaction, and unified scheduling through Dep. Observer intercepts GET, set; When get triggers DEP to add dependencies, and when set, DEP publishing is scheduled. Adding Watcher triggers a GET for subscription data and adds it to the subscriber queue in the DEP dispatch center.

The UML class diagram below shows the classes that Vue implements responsive functionality and the reference relationships between them.

Contains only partial attribute methods

The classes in the figure above are clearly identified, but an invocation diagram is needed to make the invocation process clearer, as shown below.

In a reactive data object, the hijacked GET /set function for each key closes a Dep scheduling instance. This diagram shows a data flow during a key change.

Part of the source

The subscription/publish model for data changes is clearly shown in the figure above. From the figure, we already know that we can subscribe to a change of data by adding watcher. So, if we just use the render component as a Watcher subscription, the data-driven view rendering will come naturally. That’s exactly what Vue did! The following snippet comes from the vue.prototype. _mount function

callHook(vm, 'beforeMount')
vm._watcher = new Watcher(vm, () => {
    vm._update(vm._render(), hydrating)
}, noop)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
}Copy the code

Some questions to ponder

#person assigns a new object. Are the properties in the new object also reactive?

JS Bin Online debugging

var vm = new Vue({ el: '#app', data: () => ({ person: null }) }) vm.person = {name: 'zs'} setTimeout(() => {// Change name vm.person.name = 'finally zs'}, 3000)Copy the code

Answer: It is responsive. Cause: Vue hijack set, will do observe to value again, source code is as follows.

function reactiveSetter (newVal) { /* ... ChildOb = observe(newVal) dep.notify()} childOb = observe(newVal) dep.notify()Copy the code

When we listen to multiple levels of attributes, the upper level reference changes, will trigger a callback?

Var vm = new Vue({data: () => ({person: {name: 'name '}}), watch: { 'person.name'(val) { console.log('name updated', val) } } }) vm.person = {}Copy the code

Answer: Yes. Reason: Person.name, when passed into Watcher as an expression, is parsed into a function like this

() => {this.vm.person.name}Copy the code

This will fire the Person GET, then the Name GET; So the callback function we configured is not only added to the name dependency, but also to the person dependency.

If a new object is assigned to person, how do old objects and dependencies on old objects get garbage collected?

  • Old object reclamation: Since the only direct reference to the old object is Person on the Vue instance, person switches to the new reference, so the old object is no longer referenced and will be reclaimed.
  • The old object dependency deP, watcher’s dependency still exists; But when run is executed, watcher’s get() is called to get the current value; The new dependency collection is performed in GET, and when the collection is complete, the old dependencies are emptied.

    The specific source code is as follows:
    /**
    * Evaluate the getter, and re-collect dependencies.
    */
    get () {
        pushTarget(this)
        const value = this.getter.call(this.vm, this.vm)
        // "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

Does the callback fire more than once when we change the name multiple times?

Var vm = new Vue({data: () => ({person: {name: 'person '}}), watch: {'person. Name ': (val) {console.log('name updated: '+ val)}}}) vm. Person = {name:' zs} vm. The person. The name = 'invincible'Copy the code

Answer: No, because the watch callback executes asynchronously and is de-duplicated. You can use sync to force run to be executed twice.

Implement a responsive system yourself

Contains only the core functions, the specific source can see here github.com/Zenser/z-vu… Welcome to Star. Implementation function is very basic, focus on understanding, the function is not complete.

Observer

class Observe { constructor(obj) { Object.keys(obj).forEach(prop => { reactive(obj, prop, obj[prop]) }) } } function reactive(obj, {let dep = new dep () Object. DefineProperty (obj, prop, {64x: 64x) {let dep = new dep () Object. true, enumerable: True, get() {// use js single thread, If (dep.target) {dep.addSub(dep.target)} return value}, set(newVal) {value = newVal Trigger subscriber update dep.notify()}}) // Object listener if (Typeof value === 'object' && value! == null) { Object.keys(value).forEach(valueProp => { reactive(value, valueProp) }) } }Copy the code

Dep

class Dep {
    constructor() {
        this.subs = []
    }
    addSub(sub) {
        if (this.subs.indexOf(sub) === -1) {
            this.subs.push(sub)
        }
    }
    notify() {
        this.subs.forEach(sub => {
            const oldVal = sub.value
            sub.cb && sub.cb(sub.get(), oldVal)
        })
    }
}Copy the code

Watcher

class Watcher {
    constructor(data, exp, cb) {
        this.data = data
        this.exp = exp
        this.cb = cb
        this.get()
    }
    get() {
        Dep.target = this
        this.value = (function calcValue(data, prop) {
            for (let i = 0, len = prop.length; i < len; i++ ) {
                data = data[prop[i]]
            }
            return data
        })(this.data, this.exp.split('.'))
        Dep.target = null
        return this.value
    }
}Copy the code

Write in the last

Article and my implementation for reference only, welcome to leave a message correction, we discuss together.

Reference documentation

  • Cn.vuejs.org/v2/guide/re…