Vue-MVVM 源码

1.runtime&&compiler 的区别

我们在使用 vue-cli 或者看 vue 源码的时候,通常会看到两种不同的包,我们首先看一下源码生成的 dist 目录
主要分为两种:runtime(运行时)和 compiler(编译时)
我们来说一下 runtime 和 compiler 的区别
在我们编写代码使用了 template 模板时,我们就需要使用 compiler,将模板解析成 render 函数,那么这个编译过程会发生在运行时,所以需要带有编译器的版本。很显然,这个编译过程对性能会有一定损耗,而我们 runtime 没有> compiler,包体积会减少大约 30%,而且已经编译成 render 函数,不需要再依赖 compiler
而我们在线上版本,一般都是使用了 vue-loader 进行了编译,所以上线就是 runtime

2.src/core vue 核心代码

如下图,这是 src/core 里的目录结构
img
components:模板编译的代码
global-api:最上层的文件接口
instance:声明周期
observer:数据收据收集与订阅
util:常用工具方法类
vdom:虚拟 dom

3.MVVM 的实现

什么是 MVVM

MVVM 是一种架构模式,也被称为 model-view-binder。它由微软架构师 Ken Cooper 和 Ted Peters 开发,通过利用 WPF(微软 .NET 图形系统)和 Silverlight(WPF 的互联网应用派生品)的特性来简化用户界面的事> 件驱动程序设计。微软的 WPF 和 Silverlight 架构师之一 John Gossman 于 2005 年在他的博客上发表了 MVVM。

  • Model:模型、数据
  • View:视图、模板(视图和模型是分离的)
  • ViewModel:连接 Model 和 View,也称为数据驱动视图,视图修改驱动数据变化

接下来,我们看一下,vue 中的 MVVM 是如何实现的,我们先看核心目录结构

img

先解释一下这几个文件主要的作用:

  • array.js:监听数组的处理
  • dep.js:维护依赖
  • scheduler.js:批处理
  • traverse.js:解析对象
  • watcher.js:联系 dep 和 Directive
  • index.js:数据监听

我们先看一下 vue MVVM 的流程图,便于理解
img

observer

首先看一下我们 index.js,这个文件就是 observer,主要就是监听数据变化,为每个数据对象绑定 get,set 方法等

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;

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

看上面的图,我们来说一下具体的流程

  • 首先,这段代码只是定义了一个 observer 类,我们看一下类里有什么
  • value:绑定的数据
  • dep:依赖,一会我们具体说
  • vmCount 根元素 vm 的数量
  • constructor:里面判断了传入的 value 是否是数组,如果数组,则会判断 value 上是否有proto,如果有,直接将 array.js 里封装的改变数组方法绑定到原型,如果没有,创建并绑定,并且执行 observeArray 方法。如果不是数组,则执行 walk 方法,接下来我们一步一步看 observer 具体做了什么
  • walk:接收对象,遍历属性,循环执行 defineReactive,这个时候我们应该也能猜到,defineReactive 这个方法就要开始绑定设置数据的 set 以及 get 了
  • observeArray:循环深层遍历,保证嵌套的数据都会被监听,并且执行 observer 方法

我们在这里插一下,先把处理数组说一下

//--------index.js
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
//上面提到的两个方法protoAugment、copyAugment
function protoAugment (target, src: Object) {
  target.__proto__ = src;
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i];
    def(target, key, src[key]);
  }
}
//---------array.js
import { def } from '../util/index'
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
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;
  })
})
//---------util/lang.js
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

array.js 就是将可以改变数组的方法,通过 Object.defineProperty 进行监听
我们可以看到,observer 一进来执行了 Object.getOwnPropertyNames,里面传入了 arrayMethods,arrayMethods 就是数组的原型,我们又接着调用 def 方法,监听操作数组的方法,def 方法里,value 就是传入的 mutator 方法,最后调用 dep 的 notify 方法,notify 方法看上面图我们知道里面会去更新 watcher,一会说到 watcher,然后返回 result,result 就是数组原型的方法,vue 做了缓存,返回的 result 也被挂在带了 Object 原型上,便于以后对数据的操作

接着,我们看一下这个很重要的方法 observe

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

我们可以看到,observe 方法主要就是执行了 new Observer,将实例化 Observer 并传入 value,方法里也判断了ob存不存在,避免循环创建,浪费内存

我们再看一下上面 walk 方法里循环执行的 defineReactive 方法

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

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  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
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

可以看到 defineReactive 这个方法里做了数据的监听,使用的就是 Object.defineProperty,因为 Object.defineProperty 不能监听数组变化,所以我们上面有了 array.js 处理数组,在 vue3 里,使用 proxy 代替了> Object.defineProperty,兼容 IE11 及以上,之后 vue3.0 新特性会讲到为什么使用 proxy,我们说说 defineReactive 方法里属性的作用

  • obj:数据对象
  • key:对象属性值
  • val:可以是一个函数,数据被访问时,会触发该函数
  • customSetter:函数,如果是上线环境,可以在函数里执行响应的操作
  • shallow:一般都为 true,不然不会执行 observe 方法

    在 observer 这个文件里,还导出了 set 和 del 方法,这个我们这里不做解释,主要就是给 Vue 提供的方法,Vue 可以删除或者更新原型上的属性,我们还可以看到,方法里引入了 Dep,并且实例了 Dep,get 的时候执行了 dep.depend 方法,set 的时候执行了 dep.notify 方法,我们可以参照上面的图,会知道,notify 会触发 Dep,Dep 里会执行 update 更新 Directive,而触发 get 的时候,执行 depend 方法,我们紧接着阑槛 dep 里做了什么

dep.js

dep 就比较简单了,我们先来看它的源码

let uid = 0
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.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

先简单说一些变量的作用,也很容易看得出来

  • uid:number,从 0 开始,绑定到依赖的 id 属性上作为唯一标识
  • target:静态方法,Dep 原型可以直接调用,存放 watcher
  • subs:存放依赖,是一个数组
  • addSub:将 watcher push 到 subs 数组里来,watcher 哪里来,一会会看到,watcher 会通知到 Dep 的 target
  • removeSub:移除依赖
  • depend:observer 监听的 get 触发,用于通知 dep,该去添加 watcher 了
  • notify:observer 监听到数据变化执行的方法,里面会执行 watcher 的 update 方法

    从上面的来看,我们大概清楚了 dep 是干啥的,至于 Dep.target=null,就可以知道同一时间,只能有一个 watcher 被执行,上面的 if 条件不符合,那么在什么地方修改了 target 呢,那一定是 pushTarget 和 popTarget 这两个方法了,我们可以看到这两个方法被导出,而我们发现,在 watcher 里使用了这两个方法

watcher.js

在 watcher 里,定义了很多属性,其中包括重要的几个属性:

  • vm:组件实例
  • lazy:是否是懒加载
  • deps:数组,存放依赖
  • getter:真正触发 vm 的 update 方法

我们可以在 watcher 的 constructor 里看到这样一行代码

this.value = this.lazy
      ? undefined
      : this.get()

lazy 是个修饰符,我们先不管,我们看接下来的 get 方法

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
  }

我们可以看到,get 方法中首先执行 pushTarget,将 Dep.target 值设置为当前 watcher
紧接着,使用了 try…catch…finally,会根据 deep 来判断是否触发当前数据子属性的 getter,这里先不看,我们一会看 traverse.js
然后执行了 popTarget 方法,这个方法就是删除 Dep.target,还执行了 cleanupDeps,这个方法就是清空依赖,因为我们已经实现了对当前 watcher 的收集
我们发现上面执行了 this.getter.call,这里的 getter 就是 lifecycle.js 里的 updateComponent 方法,执行了挂载在原型上的_update 和_render 方法
经过上面的流程,我们发现最后又回到了 observer 的 get 方法里,再列举一下代码

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
}

我们发现又执行 dep.depend 方法, 而且调用了 Dep.target.addDep(this),由于 Dep.target 指向的是 Watcher,我们再看 watcher 里的 addDep 方法

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

我们发现,最后还是调用了 dep.addSub 方法,这样又回到了 dep 里,dep 里的 addSub,将 watcher 添加到了数组中,最终 data 的对应 watcher 都会被放到 deps 里,这样就完成了收集
我们结合之前看到的 dep,当数据变化时,set 方法会触发 notify 方法,notify 方法里调用了 update 方法,我们知道 subs 里存放的是 watcher,那么我们也就知道 watcher 的 update 会触发

update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

这里我找了资料,因为没有仔细去看 lazy 和 sync 具体的用处,借鉴于别人的知识,知道 lazy 和 computed 有关,this.sync 跟 watch 有关,如果 sync 是 true,那么要求 watcher 更新要在当前 Tick 一并执行,不需要放到下一个 Tick
我们直接看 queueWatcher 方法,queueWatcher 方法在 scheduler.js 中,接下来我们看 scheduler.js

scheduler.js

紧接着上面的,我们先看一下 queueWatcher 做了什么

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

queueWatcher 首先根据 id,避免重复的执行的 watcher,然后 push 到一个 queue 数组里,最后调用了 nextTick 方法,然后传入了 flushSchedulerQueue 方法,我们紧接着看 flushSchedulerQueue 干了什么

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true;
  let watcher, id;
  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();
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break;
      }
    }
  }
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice();
  resetSchedulerState();
  callActivatedHooks(activatedQueue);
  callUpdatedHooks(updatedQueue);
  if (devtools && config.devtools) {
    devtools.emit('flush');
  }
}

我们发现,这个方法主要就是排序,根据 id 从大到小,因为我们前面也看了,id 是一个 number 从 0 开始,接着使用 for 循环,执行 watcher.run 方法,我们再回到 watcher.js 里,看一下这个 run 方法

//watcher.js
run () {
  if (this.active) {
    const value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

我们发现,又回到了 get 方法,也就是要更新 dom,这篇文章里先不说 dom 的更新,将会在之后的 vue 章节里具体说明
遗留问题:nextTick,将会在之后的章节里说一下,nextTick 就是在下次 DOM 更新循环结束之后执行延迟回调

0