小白都看的懂的 Vue ---- MVVM源码概览

690 阅读4分钟

文章原意

我们知道Vue2.0原理是利用Object.defineProperty,劫持对象的get与set方法。以及MVVM使用了观察者模式,此次目的就是手把手的看一下到底在哪儿进行的劫持?到底在哪儿创建的观察者?

怕看不懂的童鞋,可先看一下 模拟实现vue的MVVM文章,此文将会手把手展示其内容中对应的Observe,Dep,Watcher,defineRactive的出处,以作进一步讲解

听了很久Vue源码解析。这两天花了几个小时时间参观了一下vue的源码,感受就是语义化写的很好!细节确实比较多,绕也确实绕。

此文有点长,不过耐着性子看完的同学相信已经入门自己动手查看任何一个npm库的源码

介绍

此次观影顺序

读前须知:npm包被引用时,如何找到该包的文件入口呢?其入口的就是 package.json中 "mian"所指向的文件

那我们就从package.json开始,以纯读者角度考虑,纯新手出发,来看一下MVVM实现的机理

具体顺序如下:

  • 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 --- 最重要的文件了,大部分源码精髓都在此处
  • 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",
    }

从上可知vue源码使用rollup打包,入口文件为 vue.runtime.common.js,rollup使用 scripts/config.js 作为配置文件入口,打开 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
  },
  ...
}

其他略过不看,这段可以看到我们的 vue.runtime.common.js 有一个dev版本的** vue.runtime.common.dev.js ** 和一个prod版本** vue.runtime.common.prod.js ** 。

OK,那我们就来看一下dev版本,可以看到他的entry是 'web/entry-runtime.js'

src\platforms\web\entry-runtime.js

/* @flow */

import Vue from './runtime/index'

export default Vue

很简单,就三行,那就进入 ./runtime/index 看看Vue的定义

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方法,执行render函数的入口
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating) //挂在组件的入口函数,内部创建watcher
}
...
export default Vue

又是一个从其他地方骗来的Vue - -0.继续往下找

src\core\index.js

import Vue from './instance/index'
...

发现没?他也白嫖!继续往下找!

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) #混入初始化
stateMixin(Vue) // $data,$props,$watch混入
eventsMixin(Vue) // $on,$once,$off,$emit 等被安上原型链
lifecycleMixin(Vue) // $update,$forceUpdate,$destroy
renderMixin(Vue) // $nextTick,_render 上面提到的 mountComponent 所需的内在_render函数

export default Vue

OK,总算看见不白嫖Vue的了。Vue在此申明,他是一个function。之后对Vue的原型链进行了惨无人道的混入。

  • 我们看到Vue内部执行了_init函数。此处相当于你代码里写的 new Vue({el:'#app'}) 之后立刻运行的代码。
  • {el:'#app'} == _init(options) 中的options
  • 接下来看_init,他是initMix赠与的Vue

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

摘出重点段落,此处看到了 我们熟悉的生命周期 beforeCreate,created。原来他们是在此处被触发的!同时也有很多的初始化

当然,继续关注我们的目标defineProperty,进入initState函数

src\core\instance\state.js --- 最重要的文件了,大部分源码精髓都在此处

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

当opts.data为空时 observe 观察者对象 对data默认值空对象{} 进行管控。非空时,具体实现在同文件中的initData里

同文件

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

initData里最后也是使用observe 对data进行观察。

同文件

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

查看到了observe的实现,他实际就是Observer的对象。

同文件

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

此处,处处是精髓。首先,每一个ob都有一个dep。

  • 若data为数组的话,则会对他的所有arrayMethods进行监听,arrayMethods包括 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' 。
  • 若data是对象的话则执行walk方法。walk方法内部就是对data的第一层key进行了监听。

defineReactive 同文件

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

在此就找到了我们所期望的 defineProperty 对 传入data内 相应key值的setter及getter的劫持。

  • 当我们的数据 data.a.b.c发生变化时,就会走setter,从而执行watcher的update方法得到数据的最新状态(dep.notify内实现)。我们平常用的vm.$watch('a.b.c',cb)和模板中通过指令和插值语法绑定的数据都是基于watcher。
  • get中进行了依赖收集dep.depend()
  • set中进行了更新通知dep.notify()
  • 接下来看一下Dep如何实现依赖收集及

src\core\observer\dep.js

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

Dep.target 是一个watcher类型。对于对象来说,依赖是在getter中收集,在setter中触发执行。依赖存储在哪呢?这里Dep类来管理依赖,对于响应数据对象的每一个key值,都有一个数组来存储依赖。

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

watcher内部的addDep、update方法实际触发的就是dep.addSub和被收集的依赖事件们~

结尾

至此,看到Vue 对MVVM初始化的全过程,当然中间有的生命周期初始化、事件初始化都有提及,有兴趣的小伙伴们可以继续深入哦。