深入浅出Vue源码 - 响应式原理

72 阅读2分钟

简单学习下Vue从初始化到实现数据响应式的实现过程,这是vue的响应式核心所在,过程中会附上核心源码。

1.new Vue()初始化

src/core/instance/index.js

function Vue (options) {
  ...
  this._init(options)
}
initMixin(Vue)
...
  1. new Vue()初始化是通过构造函数function Vue进行的
  2. 执行构造函数的过程中调用_init()方法进行初始化
  3. _init方法是在initMixin方法中定义的

2. 初始化 data

2.1 initMixin

src/core/instance/init.js

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ...
    initState(vm)
    ...
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
  1. Vue的原型对象上定义了一个_init方法
  2. 执行初始化data函数initState

2.2 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)
  }
}
  1. 初始化 props methods data computed watch
  2. 初始化props时,会将props中的属性挂在到vm上,这就是为什么this.propsKey可以访问到props上的值
  3. 初始化methods上的方法,最终得到vm[methodsKey] = methods[key]
  4. 初始化data时,如果存在data属性则对其执行initData进行初始化,如果不存在data属性,则直接把其作为一个{}进行响应式处理
  5. 初始化computed,这里是处理初始化组件配置中的computed属性
  6. 初始化watch,这里是处理初始化组件配置中的watch属性
  7. 在初始化时会对props methods data computed watch进行判断重复的处理,判重的优先级为props > methods > data > computed

2.3 initData

src/core/instance/state.js

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  ...
  // proxy data on instance
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    ...
    proxy(vm, `_data`, key)
    ...
  }
  // observe data
  observe(data, true /* asRootData */)
}
  1. 这里会对data的不同类型进行处理,类型为函数时,需要执函数getData获取其返回值对象;类型为对象时,就直接赋值即可;处理完成之后挂载在vm._data
  2. 通过proxy函数进行代理,使得可以使用this.[dataKey]的形式访问到data属性
  3. 最后是通过observe函数,对数据进行响应式处理

3.数据响应式处理

3.1 observe

src/core/observer/index.js

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)
  }
  ...
  return ob
}
  1. observe会先判断该值是否为基本数据类型或者虚拟DOM对象,如果是则说明该值不需要进行处理,直接返回即可
  2. 判断是否已经进行过响应式处理,如果已经进行响应式处理则返回value.__ob__
  3. 这里的__ob__属性是在new Observer的时候添加上去的
  4. 以上都不满足,则直接调用new Observer创建响应式对象

3.2 Observer 类

src/core/observer/index.js

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

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 () {
      ...
      dep.depend()
      if (childOb) {
        childOb.dep.depend()
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      ...
      dep.notify()
    }
  })
}
  1. Observer函数初始化时,并且给属性加上__ob__属性,同时也用来判断是否已经进行过响应式处理
  2. 判断该数据是否为数组,数组和对象的处理思路不同
  3. value为数组时,由于数据是挂载在data下面的,而data属性是一个对象,所以依赖的收集依旧在set中进行
  4. 由于Object.defineProperty无法捕获到数组中值的变化,所以数组的依赖更新需要单独处理,详见3.3 数组响应式处理
  5. value为对象时,执行walk方法,循环对象获取每一个值,对其执行defineReactive方法,此方法中中使用Object.defineProperty对数据进行拦截,并且在set中收集依赖,在get中通知依赖更新
  6. defineReactive中则是Object.defineProperty对数据的具体拦截,并且使用了de.depend()进行依赖收集,使用dep.notify()进行依赖更新。

3.3 数组响应式处理

Observer 类中

if (hasProto) {
  protoAugment(value, arrayMethods)
} else {
  copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)

// 将方法添加到 __proto__ 上
function protoAugment (target, src: Object, keys: any) {
  target.__proto__ = src
}

// 将数组方法添加到 value 上
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])
  }
}

// src/core/observer/array.js
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__
    ...
    ob.dep.notify()
    return result
  })
})
  1. methodsToPatch中定义了需要拦截的方法
  2. 先进行判断是否支持 __proto__,如果支持,函数protoAugment会将拦截器函数赋值在value的__proto__上,当对数组的操作时,拦截器会对这些方法进行拦截从而进行依赖更新
  3. 如果不支持,则循环这几个方法,函数copyAugment会将其通过Object.defineProperty加入到value上,这样当数组通过这些方法进行操作时,就能通过这些方法进行依赖更新了

4. 依赖的收集与更新

4.1 依赖收集 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 () {
    ...
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
  1. 依赖的收集与更新其实是通过Dep类实现的
  2. 根据定义可以看出,收集的依赖其实就是一个Watcher实例
  3. 初始化Dep,并且在原型上定义相关的方法,subs用来存放收集的依赖数组;depend收集依赖;notify负责依赖更新
  4. notify执行更新时,相当于把存放的依赖数组subs循环,然后调用Watcherupdate方法

4.2 监听器 Watcher 更新

src/core/observer/watcher.js

export default class Watcher {
  ...
  update () {
    ...
    queueWatcher(this)
  }
}
  1. update方法中会执行queueWatch函数,此函数会将需要更新的watcher放入到watcher队列中
  2. 执行队列会按照浏览器事件循环的机制进行执行,完成更新操作