The article original intention

We know that Vue2.0 uses Object.defineProperty to hijack the get and set methods of objects. And MVVM uses observer mode, this time to see hand in hand exactly where the hijacking is taking place? Where exactly is the observer created?

For fear of children’s shoes that you can’t understand, you can have a look firstSimulate the MVVM article implementing VUE, this article will reveal its content in the corresponding Observe, Dep, Watcher, defineRactive source, for further explanation

Listen to Vue source code analysis for a long time. These two smallpox a few hours time to visit the vue source, the feeling is semantic written well! The details are indeed more, and indeed around.

This article is a bit long, but for those of you who have sat through it, I’m sure you’ve already started looking at the source code for any of the NPM libraries yourself

introduce

  • Version: the 2.6.10
  • Download source address: github.com/vuejs/vue/t…
  • Type: the runtime

The order of this viewing

Before you read: how do I find the file entry of an NPM package when it is referenced? The entry point is the file that “mian” points to in package.json

Let’s start with package.json and take a look at the mechanics of MVVM implementation for the pure reader, pure novice

The specific order is as follows:

  • package.json
  • scripts\config.js
  • src\platforms\web\entry-runtime.js
  • src\platforms\web\runtime\index.js
  • src\core\index.js
  • src\core\instance\index.js
  • src\core\instance\init.js
  • SRC \core\instance\state.js – the most important file, most of the source code is here
  • src\core\observer\dep.js
  • src\core\observer\watcher.js

package.json

."main": "dist/vue.runtime.common.js"."scripts": {
      "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"."dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev"."dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm"."dev:test": "karma start test/unit/karma.dev.config.js"."dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",}Copy the code

The rollup file is vue.runtime.common.js. The rollup file is scripts/config.js

scripts/config.js

.const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.dev.js'),
    format: 'cjs'.env: 'development',
    banner
  },
  'web-runtime-cjs-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.prod.js'),
    format: 'cjs'.env: 'production',
    banner
  },
  ...
}
Copy the code

If I skip the rest, This section you can see our vue.runtime.com mon. Js a dev version of mon * * vue.runtime.com. Dev. Js and a prod version * * * * vue.runtime.com mon. Prod. Js * *.

OK, so let’s look at the dev version and see that its entry is ‘web/ entry-Runtime.js’

src\platforms\web\entry-runtime.js

/* @flow */

import Vue from './runtime/index'

export default Vue
Copy the code

/runtime/index to see what Vue is./runtime/index

src\platforms\web\runtime\index.js


import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'. Vue.prototype.$mount =function ( //mount the entry to the render functionel? : string | Element, hydrating? : boolean) :Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating) // Hang in the component's entry function to create the watcher internally}...export default Vue
Copy the code

Another Vue – -0 from somewhere else. Keep looking

src\core\index.js

import Vue from './instance/index'.Copy the code

Found no? He’s fucking for nothing! Keep looking!

src\core\instance\index.js

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '.. /util/index'

function Vue (options) {
  if(process.env.NODE_ENV ! = ='production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)} initMixin(Vue) # init stateMixin(Vue)/ / $data, $props, $watch with
eventsMixin(Vue) $on,$once,$off,$emit, etc
lifecycleMixin(Vue) // $update,$forceUpdate,$destroy
renderMixin(Vue) // $nextTick,_render Internal _render function for the mountComponent mentioned above

export default Vue
Copy the code

OK, I finally see a Vue who doesn’t pay. Vue declares here that he is a function. Then there is the inhuman mixing of Vue’s prototype chain.

  • We see that the _init function is executed internally in Vue. This is equivalent to your code that runs immediately after new Vue({el:’#app’}).
  • {el:’#app’} == options in init(options)
  • Next, _init, which is the Vue that initMix bestow

src\core\instance\init.js

.export function initMixin (Vue: Class<Component>) {... 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')... }...Copy the code

Pick the key paragraph, here we see the familiar life cycle beforeCreate,created. So this is where they were triggered! There’s also a lot of initialization

Of course, keep your eye on our target defineProperty and go into the initState function

SRC \core\instance\state.js – the most important file, most of the source code is here

.export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } } ...Copy the code

Observe the observe object controls the default data null {} object when opts. Data is null. Non-null, implemented in initData in the same file

With the file

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  ...
  // observe data
  observe(data, true /* asRootData */)}Copy the code

Observe data with Observe in initData.

With the file

export function observe (value: any, asRootData: ? boolean) :Observer | void {... ob =new Observer(value)
  ...
  return ob
}
Copy the code

Observe the implementation of Observe, which is actually the object of the Observer.

With the file

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

  constructor (value: any) {
    this.dep = new Dep()
    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])
    }
  }
}
Copy the code

Here, the essence is everywhere. First, every OB has a DEP.

  • If data is an array, all its arrayMethods are listened on, and arrayMethods include

‘push’, ‘pop’, ‘shift’ and ‘unshift’, ‘splice’, ‘sort of’ reverse ‘.

  • Perform the walk method if data is an object. Inside the walk method is listening for the first key of the data.

DefineReactive with file

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  const dep = new Dep()
  ...
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      if (Dep.target) {
        ...
        dep.depend()
        ...
      }
      return value
    },
    set: function reactiveSetter (newVal) {... dep.notify() } }) }Copy the code

This is where we find the expected hijacking of the setter and getter for the corresponding key value passed in to the data by defineProperty.

  • When our data data.a.b.c changes, we go to the setter and execute Watcher’s update method to get the latest state of the data (implemented in DEP.notify). The vm.$watch(‘ A.B.C ‘,cb) and the data bound by instructions and interpolation syntax in the template are all based on Watcher.
  • The dependency collection dep.depend() is done in get.
  • Update notification dep.notify() in set
  • Next, how does Dep implement dependency collection and

src\core\observer\dep.js

export default 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)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
Copy the code

Dep.target is a Watcher type. For objects, dependencies are collected in getters and triggered in setters. Where are the dependencies stored? Here the Dep class manages the dependencies, and for each key value of the response data object, there is an array to store the dependencies.

src\core\observer\watcher.js

export default class Watcher {
  cb: Function;
  deps: Array<Dep>;

  constructor () {
    this.cb = cb
    this.deps = []
    this.newDeps = []
    this.depIds = new Set(a)this.newDepIds = new Set()}/** * Add a dependency to this directive. */
  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)}}}/** * Clean up for dependency collection. */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)}}}/** * Subscriber interface. * Will be called when a dependency changes. */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}/** * Scheduler job interface. * Will be called by the scheduler. */
  run () {
      this.cb.call(this.vm, value, oldValue)
  }
}

Copy the code

The addDep and update methods inside watcher actually trigger dep.addSub and the dependencies that are collected

At the end

So far, see Vue to MVVM initialization process, of course, some of the middle lifecycle initialization, event initialization are mentioned, interested partners can continue to in-depth oh.