小松的技术博客

六和敬

若今生迷局深陷,射影含沙。便许你来世袖手天下,一幕繁华。 你可愿转身落座,掌间朱砂,共我温酒煮茶。

Vue源码浅析(一)

在当前如日中天的前端领域中,vue无疑是一颗闪亮的明星。在几个月前,vue2.0也携带virtual dom、服务端渲染(ssr)等诸多新特性闪亮登场。紧随前端浪潮,上一个月也在业余时间投身于vue2的学习中,其API相对1.0的变化并不大,因此可以很快的上手,所以进一步针对其源码进行了学习,受益良多。本博文根据自己的理解,简要的拆解一下vue。

要很好的理解Vue,个人认为要深入理解以下几个方面:

  • 观察者系统与依赖收集
  • 生命周期
  • compile与render函数
  • patch

观察者系统

前端MVVM最令人激动的就是双向绑定机制了,框架自动将DOM与数据绑定,使得开发者只需要关注数据,这极大的解放了生产力。双向绑定的实现可以有多种方式,如Angular采取“脏检查”的方式,每次数据变动就去检查DOM,如果DOM的数据是旧的,那么就更新,而Vue是利用Object.defineProperty这个API去实现了一套依赖收集机制,每当数据变动,就非常精准的去更新它的依赖,是非常智能的一种方式,但由于API不支持IE8及IE8以下的浏览器,所以Vue并不支持它们。我之前有一篇博文有介绍Object.defineProperty的使用,对这个API不了解的也可以去看看(点我查看)。

vue通过一个observe的方法将一个普通的数据model转换为一个监控对象:

export function observe (value: any): Observer | void {
  // 如果不是object,返回
  if (!isObject(value)) {
    return
  }
  let ob: Observer | void
  // 已经是一个监控对象了,返会observer
  // 一个监控对象的标志就是含有属性'__ob__',并且属性值是Observer的实例
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    observerState.shouldConvert &&
    !config._isServer &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  return ob
}

上面的关键是通过new Observer(value)产生一个observer,并把它赋值给value.ob, 我们看看Observer的构造:

constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // 数组的处理
    if (Array.isArray(value)) {
      const augment = hasProto
       ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      // 对象处理
      this.walk(value)
    }
}

Observer可以接收的是对象或者数组,我们首先看看是是对象的情况:

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

vue会遍历对象的属性,并调用defineReactive

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
  const dep = new Dep()
  //...
  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      //...
    },
    set: function reactiveSetter (newVal) {
      //...
    }
  })
}

defineReactive主要做了如下工作:

第一步,实例一个依赖收集容器Dep,这里会记录改属性值的所有依赖,在get属性值时进行依赖收集,在set属性值时通知依赖更新;

第二部,调用observe(val),如果属性值为数组或者对象,那么会被转换为监控数组或监控对象,如果是基本值,则直接进入下一步。

第三部,调用Object.defineProperty重新定义obj的当前属性,在get时做依赖收集:

const value = getter ? getter.call(obj) : val
//Dep.target是非常重要的一步,后面在分析watcher时再看,这里先作为一个疑惑点记录下俩
if (Dep.target) {
    // 依赖收集
    dep.depend()
    if (childOb) {
      // 如果是监控数组或监控对象,则相应observer也要收集这个依赖
      // 这样做的目的是我们可能会想给监控对象添加属性,这个时候我们需要调用vue提供的set方法,对添加的属性添加监控,并通知依赖更新
      childOb.dep.depend()
    }
    if (Array.isArray(value)) {
      // 如果是数组,进一步处理
      dependArray(value)
    }
}
return value

在set时就需要去通知依赖更新数据:

const value = getter ? getter.call(obj) : val
// 如果set值与原来的值一样,则直接返回
if (newVal === value) {
    return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()
}
if (setter) {
    setter.call(obj, newVal)
} else {
    val = newVal
}
// 如果新的值是数组或对象,则需要转换为监控数组和监控对象
childOb = observe(newVal)
dep.notify()

至此,属性值为基本值或对象就基本分析完了,但剩下一个较为关键的是数组处理了,数组并不能简单利用Object.defineProterty这接口,并且我们没办法监听如a[3] = 4,这种赋值行为,因此在对待数组时,需要做一些让步,监听数组的'push','pop','shift','unshift','splice','sort','reverse',等行为,并且另外增加Vue.set去取代用subscript赋值的行为。那我们来看看如何实现监控数组吧:

数组的'push','pop'等方法都是定义在Array.prototype上,因此我们要做的事情就是在监控数组的原型链上插入我们改写的对象。

// 数组原型
const arrayProto = Array.prototype
// 我们想要插入原型链的对象
export const arrayMethods = Object.create(arrayProto)

[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // 真正的方法
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    // avoid leaking arguments:
    // http://jsperf.com/closure-with-arguments
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    // 先调用原本的方法
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 得到新添加的成员,并转换为监控对象
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

那我们来看看observe方法中如何使用的:

if (Array.isArray(value)) {
  const augment = hasProto
    ? protoAugment
    : copyAugment
  augment(value, arrayMethods, arrayKeys)
  this.observeArray(value)
} 

如果我们可以使用__proto__,那我们就可以直接将value.__proto__指向arrayMethods,否则的话我们就只能讲arrayMethods方法mixin进value中去,接下来的observeArray方法就比较简单了:

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

还有最后一个细节,假设有如下操作:

var a = [{b: 2}]
observe(a)
var b = a.b;
Vue.set(b, 'c', 123)

调用Vue.set(b, 'c', 123)会通知b对象的各个属性的依赖更新,但是此时数组已经改变了,我们需要通知依赖数组的watcher也更新,因此在defineReactive的get中会对数组有更多的操作:会把依赖数组的依赖也加入到数组元素的依赖中去。

function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

在vue中,我们传入的data对象就会被转换为一个巨大的监控对象,我们可以动过$data拿到它。

var vm = new Vue({
    el: '#app',
    data:{
        a: '普通属性',
        b: {
           b1: '嵌套对象'
        },
        c: ['arr1', 'arr2']
    }
})

console.log(vm.$data)

我们可以打印先打印出来看看其实际结构:

我们可以通过chrome控制台看到其转换后的具体结构,辅助我们理解。

Watcher与依赖收集

当了解了上述的观察者系统后,就让我们来了解Vue中的Watcher以及依赖收集的实现。

在了解watcher之前,我们先了解vue中Dep这个数据结构:

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 () {
    // stablize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep的代码很简单,其本质是维护了一个watcher队列,负责添加watcher,更新watcher,移除watcher,通知watcher更新。在一些其它框架,这部分可能会被整合到Observable中去,而Vue由于使用的方便而将其提取出来了。

现在让我们来了解一下watcher:

constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object = {}
  ) {
    this.vm = vm
    vm._watchers.push(this)
    //...
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
}

watcher主要包含两个点:一个是expOrFn,另一个是cb。expOrFn最终会被转换为getter函数,而cb很好理解,就是更新时执行的回调。

那么getter函数具体有什么作用呢?其实它是用来连接监控属性与watcher的关键。其原理也很简单:作为watcher,如果想要依赖监控属性a,那么它首先要将自己挂到Dep.target这个全局对象中去,然后再去获取a的值,要获取a的属性值,必定走入a的get方法中去,在get方法通过Dep.target拿到watcher,并加入自己的依赖队列中,完成依赖收集过程,将Dep.target置为null。

接下来看具体代码实现,关注Watcher的get方法:

get () {
    pushTarget(this)
    // 奇迹就出现在这一步,这时可以再次回到defineReactive方法中去理解整个依赖机制。
    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
}

Dep.target = null;
// 这个stack在1.x并不存在,暂不清楚为何要加这个stack
var targetStack = [];

function pushTarget (_target) {
  if (Dep.target) { targetStack.push(Dep.target); }
  Dep.target = _target;
}

function popTarget () {
  Dep.target = targetStack.pop();
}

当然,watcher依赖还负责清空依赖,更新依赖、销毁等复杂逻辑,本博文暂不讨论这些细节。

观察者系统与依赖收集可以说是现在诸多前端MVVM框架的根本,这也是我们探索源码的第一步。在下一篇,我会注重分析框架在整个生命周器中的各种行为,敬请期待~

←支付宝← →微信 →
comments powered by Disqus