vue源码阅读(七):响应式原理

291 阅读4分钟

前言

vue 中数据是普通的js对象,当数据变化时,视图会进行更新。数据之所以能驱动视图的更新,关键的部分就是它的响应式系统。

初始化数据

  • initState

new Vue()初始化阶段,this._init(options)方法执行的时候,会执行initState(vm),它定义在src/core/instance/state.js中。

export function initState (vm: Component) {
  // ...
  if (opts.data) {
    initData(vm)
  }
  // ...
}

initState的作用主要是初始化props,methods,data,computed,watcher等属性,这里我们重点看看initData

  • initData

function initData (vm: Component) {
  // ...
  observe(data, true /* asRootData */)
}

initData,初始化data用的,在最后有一行observe(data, true)

  • observe

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

observe()是将用户定义的数据data变成响应式的数据。主要是去实例化一个Observe对象实例,用于收集依赖和派发更新。

  • new Observer(value)

export class Observer {
  constructor (value: any) {
    this.value = value
    this.walk(value)
  }

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

Observer中,对对象的每一项执行,defineReactive(obj, keys[i])

  • defineReactive

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  const dep = new Dep() // 依赖管理器
  
  val = obj[key]
  observe(val) // 递归对象

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 收集依赖
    },
    set: function reactiveSetter (newVal) {
      // 更新依赖
    }
  })
}

收集依赖

// mountComponent
export function mountComponent(
  vm: Component, // 组件实例
  el: ?Element, // 挂载的元素
  hydrating?: boolean // 服务端渲染相关
): Component {
  ...
  updateComponent = () => {
    // vm._render(),生成 vnode,在 instance/render.js 中
    // vm._update(),更新 dom
    vm._update(vm._render(), hydrating)
  }
  // watcher 会调用 updateComponent,先生成 vnode ,然后调用 update 更新 dom;
  new Watcher(vm, updateComponent, noop, {
    before() { ... }
  }, true /* isRenderWatcher */)
  return vm
}

// watcher
export default class Watcher {
  constructor(
    vm: Component, // 组件实例
    expOrFn: string | Function, 
    cb: Function, // 当监听的数据变化时,会触发该回调
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    ...
    // expOrFn 是 `updateComponent` 方法
    this.getter = expOrFn
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get() {
    // 添加 target,相当于 watcher
    pushTarget(this)
    ...
    try {
      // 相当于执行 updateComponent()
      value = this.getter.call(vm, vm)
    } catch (e) {
    ... 
  }
}

vuemount 过程中会执行mountComponent,而在mountComponent中会new 一个 Watcher,然后里面执行this.get()方法。这个get()方法中,又执行了pushTarget(this),是将当前的watcher保存到数组中。

Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target // 当前的 watcher 实例
}

pushTarget 的定义在 src/core/observer/dep.js 中。然后get()方法中,执行了

this.getter.call(vm, vm)  // 相当于执行vm._update(vm._render(), hydrating)

首先执行的vm._render()方法中,如果有使用data数据,会触发defineProperty重写的get方法。每个对象值都会持有一个dep,在触发get时,会调用dep.depend()方法。

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        // ...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 派发更新
    }
  })
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  // ...
  constructor () {
    this.id = uid++
    this.subs = [] // 对象 key 值对应的 watcher 集合
  }

  addSub (sub: Watcher) {
    this.subs.push(sub) // 将 watcher 添加到subs 数组中
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this) // 执行 addDep
    }
  }
  // ...
}
  -------------------------------------------------
addDep(dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    // ...
    dep.addSub(this) // 执行 dep.addSub
  }
}

dep.depend()会执行Dep.target.addDep(this)方法。接着执行dep.addSub(this)方法,最后会执行this.subs.push(sub)。也就是说,data对象每个key值对应的watcher,都放入到subs中,后续数据如果有变化,可以通知 watcher 需要进行更新。

派发更新

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    // 收集依赖
  },
  set: function reactiveSetter (newVal) {
    const value = getter ? getter.call(obj) : val
    if (newVal === value || (newVal !== newVal && value !== value)) {
      return
    }
    // ...
    if (setter) {
      setter.call(obj, newVal)
    } else {
      val = newVal
    }
    childOb = !shallow && observe(newVal) 
    dep.notify()
  }
})

当我们对响应的数据做了改变时,会触发set 方法,有两个比较关键的地方是,一个是childOb = !shallow && observe(newVal),它是将新设置的值变成一个响应式的值。另一个是dep.notify(),通知watcher进行更新。notify -> update -> queueWatcher

notify () {
  const subs = this.subs.slice()
  // ...
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}
------------------------------------------------
update() {
  // ...
  queueWatcher(this) // 
}
  • queueWatcher

const queue = []
let has = {}

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    // ...
    queue.push(watcher) // 将要更新的 watcher 添加到 queue 队列
    // ...
    nextTick(flushSchedulerQueue) // 异步更新,相当于 promise.then
  }
}

queueWatcher是将要更新的watcher添加到queue,然后在nextTick后执行flushSchedulerQueue,统一更新这些watcher

  • 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()
  }
  // ...
}

flushSchedulerQueue主要的一些作用是:

  • 队列排序queue.sort((a, b) => a.id - b.id)
    每次更新按着id(new watcher 时生成) 的大小顺序来更新。
    1、组件更新顺序是,先父组件,然后在是子组件。
    2、自定义的watch,优先于渲染watcher,因为自定义watcher先创建的.computed watcher->自定义 watcher -> render watcher,这个顺序是根据它们初始化顺序来的。
    3、如果一个子组件在父组件的watcher执行期间被销毁,那么这个子组件的watcher会被跳过,所以父组件应该先执行。

  • watcher.run()

run() {
  if (this.active) {
    const value = this.get()
    // ...
    if (
      value !== this.value
    ) {
      const oldValue = this.value
      this.value = value
      // ...
      this.cb.call(this.vm, value, oldValue)
      // ...
    }
    // ...
  }
}

run()方法是执行watcher 的回调函数,先通过this.get()获得当前的新值value,使用this.value获取旧值oldValue,然后将这两个值传入到回调函数中,所以,在我们在watch 中可以拿到新值和旧值的原因。同时,在执行this.get()方法时,会再次触发vm._update(vm._render(), hydrating),然后进行diff比对,进行视图的更新。

总结

在组件初始化时,vue会使用defineProperty劫持data 数据的getset 方法。在new Watcher 时,会触发data对象的get方法,进行数据依赖收集。当数据改变时,会触发data对象的set方法,进行数据的更新。