Vue Binding Principle from Source Code and Implementation of a demo

This article covers source version 2.6.9

Preparation

down a Vue source, start with package.json, find the code we need
1. scripts in package.json, "build": "node scripts/build.js"
2. scripts/build.js line26 builds (builds), where builds are defined as 11-line let builds = require('./config').getAllBuilds(), which is probably the content of the packaged code. Another build is a function defined below. His code is as follows:

function build (builds) {
  let built = 0
  const total = builds.length
  const next = () => {
    buildEntry(builds[built]).then(() => {
      built++
      if (built < total) {
        next()
      }
    }).catch(logError)
  }

  next()
}

It is said that buildEntry is a function that uses rollup to package. It defines a next function to serialize multiple memory-eating packaging operations to reduce instantaneous memory consumption. This is a commonly used optimization method.
3. Following the logic of getAllBuilds() in scripts/config.js, touch const aliases = require('./alias') of line 28, then open scripts/alias.js, and see the vue: resolve('src/platforms/web/entry-runtime-with-compiler') in it. Finally, it is a little open-minded, and then find src/core/import instance/instance according to the layer by layer. function Vue(){} in x.js. This is the end of the preparation.

What happened to new Vue()

Just one line, this._init(options), which is defined on Vue.prototype in function initMixin().

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

Note that initState(vm) and vm.$mount(vm.$option.el) are important.

1, initState(vm)

export function initState (vm: Component) {
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  }
}

Literally, it initializes props, methods, data. Because the purpose is to see data bind in two directions, it goes directly into initData().

1.1, proxy

In initData(), the keys in data are traversed to determine whether they are renamed with props and methods, and then a layer of proxy is set up for them.

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

That's why we can get the value of this.data.name directly from this.name.
About the property descriptor that Object.defineProperty() can set, where

  • Configurable controls whether configurable and deletable can be delete d. Configuration refers to whether the description of this property can be modified by Object.defineProperty. It is true that if you change the configurable of an attribute to false through defineProperty, it is impossible to change it back.
  • enumerable controls enumerability. After assigning false, Object.keys() is invisible.
  • There are also value s, writable, get, set, which are better understood and will not be repeated.

1.2,new Observe()

After traversing keys, you call observation with data as a parameter, and the main content of observation is ob = new Observer(value), then look at the Observer class. (There's a sense of threading and cocooning)

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

The function def defines attributes on objects. Then determine whether the incoming data is an object or an array.

1.2.1,Array.isArray(value)

If value is an array, the hasProto custom function is used to determine whether _proto__ exists in the current environment. If it does not exist, it can be used directly and manually.
Realize that the function is the same, just look at what protoAugment (value, arrayMethod) did.

function protoAugment (target, src: Object) {
  target.__proto__ = src
}

target is naturally the array of our observations, and src is the definition of arrayMethods as follows

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

Look at some of the methodsToPatch items in the code. Are you familiar with them?

See ob.dep.notify() on the fourth line from the bottom, with the official annotation notify change.
That is to say, arrayMethods is an object that inherits the array prototype and handles several specific methods. Then, when new Observe(value) is an array, let value inherit the arrayMethods. Then, when this array calls a specific method, it calls the dep attribute on the current Observer class. Notfy method for subsequent operations.
After defining these, we recursively call observe for each item in the array.

1.2.2,walk & defineReactive

Then, for the object, walk is called directly, and then non-inherited attributes in the object are traversed, and defineReactive is called for each item.

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          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()
    }
  })
}

The main code for defineReactive is various judgments of recursion and Object.defineProperty(), which is also a key part of bidirectional binding, from data to DOM.
The definition of get includes if (Dep. target) {dep. depend ()} and set includes dep.notify(). Next, look at Dep's method.

1.3 Dep

Dep is defined as

export default class Dep {
  static target: ?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)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Let's look at dep. depend (), which is called in get. Dep. target is not empty. With this as the parameter, Dep. target. addDep is called. target is the static attribute of Dep class. The type is Watcher. The method addDep is defined as follows.

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)
    }
  }
}

You can see that addDep has the function of de-duplicating dep, and then you can call dep.addSub(this) to push the current Dep. target into subs.
That is to say, there is an observer in the data, and then there is a DEP in the observer, and there is an array of watcher s in the dep. Collection depends on a dragon.

As for dep.notify() called in set, it traverses the watcher array and calls the update method of each item. The core code of the update method is to call the run method of watcher. The core of the run method is this.cb.call(this.vm, value, oldValue). The problem arises again. This CB is a reference for new Watcher, but as seen step by step from initState, we first define an Observe, then define the get and set of each attribute, collect dependencies when get, and notify changes when set. But we didn't see where we actually triggered the get we set up, and what was Deep. target mentioned earlier?

2,vm.$mount(vm.$option.el)

This method is also called when new Vue is mentioned earlier, $mount is a method defined on the Vue prototype in the previous process of finding the Vue entry file.

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

Then we look for mountComponent, which is actually found in the call of this function.

mountComponent() {
  // Other logic
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
}

Looking at Watcher's constructor, you can call your own get method, which is defined as follows

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 "${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
}

Push Target (this) to set Dep's static attribute target, then call this.getter.call(vm, vm) to do virtual DOM-related operations, and trigger the getter to set the attribute on the data object. Finally, popTarget() sets Dep.target to null.
Dep.target's function is to collect dependencies only when initialization occurs, otherwise every time a value is taken to collect dependencies, the card gets stuck.

Last

Following the source code to sort out the logic, the understanding of Vue is more in-depth, and then look at the Vue official website in the description of the principle of response, it is also more clear.

This article is only about the implementation logic in the red box on the right, about the virtual DOM on the left, I really did not understand it for the time being. Based on the above logic, I try to write a simple version of Vue myself. -> portal Especially not to say that Vue was just a self-written project at the beginning. It's always right to try more.
If you are also interested in the Vue implementation principle, you might as well go down to a source code to explore for yourself.

Tags: Javascript Vue Attribute JSON

Posted on Sun, 01 Sep 2019 06:13:45 -0700 by nonexist