Skip to content

首先要知道 Vue2 是 2013 年基于 ES5 开发出来的,我们常说的重渲染就是重新运行render函数

Vue2 的响应式原理是利⽤ ES5 的⼀个 API,Object.defineProperty对数据进⾏劫持结合发布订阅模式的⽅式来实现的

思路

带入作者的角度思考一下,想要达成响应式的特点应该是:属性更新,自动调用依赖的函数进行重新渲染

  1. 使用属性描述符Object.defineProperty监听属性的赋值
  2. 赋值完成后调用依赖该属性的函数,那如何获取依赖的函数呢?看第三点
  3. 由于依赖会调用属性的get方法,所以可以在get方法中「依赖收集
  4. 然后在set方法中执行这些依赖的函数,称为「派发更新

深入了解

初始化

先来看看源码中是怎么初始化data的,它的定义在src/core/instance/state.ts文件中:

ts
export function initState(vm: Component) {
  const opts = vm.$options
  // ...
  if (opts.data) {
    initData(vm)
  } else {
    const ob = observe((vm._data = {}))
    ob && ob.vmCount++
  }
  // ...
}

function initData(vm: Component) {
  let data: any = vm.$options.data
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
  // ...
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    // ...
    proxy(vm, `_data`, key)
  }
  const ob = observe(data) // 响应式处理
  ob && ob.vmCount++
}

data的初始化主要过程是做两件事:

  • 一个是对data函数返回的对象进行遍历,通过proxy函数把每一个vm._data.xxx都代理到vm.xxx
  • 另一个是调用observe方法观察整个data的变化,把data也变成响应式,使得可以通过vm._data.xxx访问到定义在data函数所返回对象中对应的属性

observe

observe函数的功能就是用来监测数据的变化,它的定义在src/core/observer/index.ts文件中:

ts
export function observe(
  value: any,
  shallow?: boolean,
  ssrMockReactivity?: boolean
): Observer | void {
  if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    return value.__ob__
  }
  if (
    shouldObserve &&
    (ssrMockReactivity || !isServerRendering()) &&
    (isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value.__v_skip /* ReactiveFlags.SKIP */ &&
    !isRef(value) &&
    !(value instanceof VNode)
  ) {
    return new Observer(value, shallow, ssrMockReactivity)
  }
}

可以看到,observe函数的最后就是给非VNode的对象类型数据添加一个Observer,可以把observe函数简单理解为Observer类的工厂方法,所以还是要看下Observer这个类的定义:

ts
export class Observer {
  dep: Dep
  vmCount: number // number of vms that have this object as root $data

  constructor(
    public value: any,
    public shallow = false,
    public mock = false
  ) {
    // this.value = value
    this.dep = mock ? mockDep : new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (isArray(value)) {
      if (!mock) {
        if (hasProto) {
          ;(value as any).__proto__ = arrayMethods
        } else {
          for (let i = 0, l = arrayKeys.length; i < l; i++) {
            const key = arrayKeys[i]
            def(value, key, arrayMethods[key])
          }
        }
      }
      if (!shallow) {
        this.observeArray(value)
      }
    } else {
      // 遍历value
      const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i]
        defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
      }
    }
  }
  // ...
}

Observer类的主要作用是给对象的属性添加gettersetter,用于依赖收集派发更新。它的构造函数逻辑很简单,首先实例化Dep对象,这块稍后会介绍,接着通过执行def函数把自身实例添加到数据对象value__ob__属性上,def函数是一个非常简单的Object.defineProperty的封装

ts
export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

这就是我们平时开发中打印data中的对象类型数据时,会发现该对象多了一个__ob__属性的原因

defineReactive

defineReactive的功能就是定义一个响应式对象,给对象动态添加gettersetter,它的定义在src/core/observer/index.ts文件中:

ts
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean
) {
  const dep = new Dep() // 依赖管理器
  // ...
  if ((!getter || setter) && (val === NO_INITIAL_VALUE || arguments.length === 2)) {
    val = obj[key] // 计算出对应key的值
  }

  let childOb = !shallow && observe(val, false, mock) // 递归的转化对象的嵌套属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 依赖收集
    },
    set: function reactiveSetter(newVal) {
      // 派发更新
    }
  })

  return dep
}

defineReactive函数最开始初始化Dep对象的实例,接着拿到obj的属性描述符,然后对子对象递归调用observe方法,这样就保证了无论obj的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改obj中一个嵌套较深的属性,也能触发gettersetter。最后利用Object.defineProperty去给obj的属性key添加gettersetter

依赖收集

什么是依赖了?我们先回顾一下mountComponent的定义:

ts
export function mountComponent(
  vm: Component,
  el: Element | null | undefined,
  hydrating?: boolean
): Component {
  // ...
  let updateComponent
  if (__DEV__ && config.performance && mark) {
    updateComponent = () => {
      // ...
      vm._update(vnode, hydrating)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  // ...
  // render-watcher
  new Watcher(vm, updateComponent, noop, watcherOptions, true /* isRenderWatcher */)
  // ...
}

Watcher

首先说明下这个Watcher类,它会根据传入的参数不同,可以分别实例化出三种不同的Watcher实例,它们分别是user-watchercomputed-watcher以及render-watcher

  • user-watcher:就是开发者自己定义的监听器
  • computed-watcher:也就是计算属性
  • render-watcher:只是用做视图渲染而定义的Watcher实例,也就是上面的mountComponent函数最后所实例化的Watcher

user-watchercomputed-watcher请查看计算属性和监听,本章着重讲解render-watcher的情况,它的定义在src/core/observer/watcher.ts文件中:

ts
export default class Watcher implements DepTarget {
  // ...
  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    // ...
    // 是否是render-watcher
    if ((this.vm = vm) && isRenderWatcher) {
      vm._watcher = this // 当前组件下挂载vm._watcher属性
    }
    // ...
    this.before = options.before // render-watcher特有属性
    this.getter = expOrFn // 第二个参数
    // ...
    this.value = this.lazy ? undefined : this.get() // 实例化为user-watcher和render-watcher时就会执行this.get()方法
  }
  get() {
    pushTarget(this) // 添加
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm) // 执行vm._update(vm._render())
    } catch (e: any) {
      // ...
    } finally {
      // ...
      popTarget() // 移除
      this.cleanupDeps()
    }
    return value
  }
  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) // 将当前watcher收集到dep实例中
      }
    }
  }
}

当执行new Watcher的时候内部会挂载一些属性,然后执行this.get()这个方法,首先会执行一个全局的方法pushTarget(this),传入当前watcher的实例。

我们来看一下pushTarget这个函数,它的定义在src/core/observer/dep.ts文件中:

ts
export default class Dep {
  static target?: DepTarget | null
  // ...
}
Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = [] // 组件从父到子对应的watcher实例集合

export function pushTarget(target?: DepTarget | null) {
  targetStack.push(target) // 添加到集合内
  Dep.target = target // 当前的watcher实例
}

export function popTarget() {
  targetStack.pop() // 移除数组最后一项
  Dep.target = targetStack[targetStack.length - 1] // 赋值为数组最后一项
}

首先会定义一个Dep类的静态属性Dep.targetnull,这是一个全局会用到的属性,保存的是当前组件对应render-watcher的实例;targetStack内存储的是在执行组件化的过程中每个组件对应的render-watcher实例集合,使用的是一个先进后出的形式来管理数组的数据,这里可能有点不太好懂,稍后看到流程图自然就明白了;然后将传入的watcher实例赋值给全局属性Dep.target,再之后的依赖收集过程中就是收集的它

watcherget方法中会执行getter这个方法,它是new Watcher时传入的第二个参数,这个参数就是之前的updateComponent变量,也就是会执行当前组件实例上的vm._update(vm._render(), hydrating)render函数转为VNode,这个时候如果render函数内有使用到data中已经转为了响应式的数据,就会触发get方法进行依赖的收集,补全之前defineReactive依赖收集的逻辑:

ts
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean
) {
  // ..
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      // 之前pushTarget时赋值的当前watcher实例
      if (Dep.target) {
        if (__DEV__) {
          dep.depend({
            target: obj,
            type: TrackOpTypes.GET,
            key
          })
        } else {
          dep.depend() // 收集起来,放入到上面的dep依赖管理器内
        }
        if (childOb) {
          childOb.dep.depend()
          if (isArray(value)) {
            dependArray(value)
          }
        }
      }
      return isRef(value) && !shallow ? value.value : value
    },
    set: function reactiveSetter(newVal) {
      // 派发更新
    }
  })

  return dep
}

这个时候我们知道watcher是个什么东西了,简单理解就是数据和组件之间一个通信工具的封装,当某个数据被组件读取时,就将依赖数据的组件使用 Dep 这个类给收集起来

Dep

如果响应式变量被其他组件使用到,也会将使用它的组件收集起来,例如作为了props传递给了子组件,在dep的数组内就会存在多个render-watcher。我们来看下Dep类这个依赖管理器的定义:

ts
let uid = 0
export default class Dep {
  static target?: DepTarget | null
  id: number
  subs: Array<DepTarget | null>
  _pending = false

  constructor() {
    this.id = uid++
    this.subs = [] // 对象某个key的依赖集合
  }
  // 添加watcher实例到数组内
  addSub(sub: DepTarget) {
    this.subs.push(sub)
  }
  depend(info?: DebuggerEventExtraInfo) {
    // 已经被赋值为了watcher的实例
    if (Dep.target) {
      Dep.target.addDep(this) // 执行watcher的addDep方法
      if (__DEV__ && info && Dep.target.onTrack) {
        Dep.target.onTrack({
          effect: Dep.target,
          ...info
        })
      }
    }
  }
  // ...
}

// ------------------------------

export default class Watcher implements DepTarget {
  // ...
  // 将当前watcher实例添加到dep内
  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类的作用就是管理属性对应的watcher,如添加/删除/通知。至此,依赖收集的过程算是完成了,还是以一张图片加深对过程的理解:

data依赖收集

派发更新

如果只是收集依赖,那其实是没任何意义的,将收集到的依赖在数据发生变化时通知到并引起视图变化,这样才有意义。当我们对响应式数据重新赋值时,就会触发set方法进行派发更新,我们再补全那里的逻辑:

ts
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean
) {
  // ..
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 依赖收集
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (!hasChanged(value, newVal)) {
        return
      }
      if (__DEV__ && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        return
      } else if (!shallow && isRef(value) && !isRef(newVal)) {
        value.value = newVal
        return
      } else {
        val = newVal // 赋值
      }
      childOb = !shallow && observe(newVal, false, mock) // 如果新值是对象也递归包装
      if (__DEV__) {
        dep.notify({
          type: TriggerOpTypes.SET,
          target: obj,
          key,
          newValue: newVal,
          oldValue: value
        })
      } else {
        dep.notify() // 通知更新
      }
    }
  })

  return dep
}

当赋值触发set时,首先会检测新值和旧值是否相同;然后将新值赋值给旧值;如果新值是对象则将它变成响应式的;最后让对应属性的依赖管理器使用dep.notify发出更新视图的通知。我们看下它的实现:

ts
export default class Dep {
  // ...
  notify(info?: DebuggerEventExtraInfo) {
    const subs = this.subs.filter((s) => s) as DepTarget[]
    if (__DEV__ && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      const sub = subs[i]
      if (__DEV__ && info) {
        sub.onTrigger &&
          sub.onTrigger({
            effect: subs[i],
            ...info
          })
      }
      sub.update() // 挨个触发watcher的update方法
    }
  }
}

这里做的事情只有一件,将收集起来的watcher挨个遍历触发update方法:

ts
export default class Watcher implements DepTarget {
  // ...
  update() {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

// -------------------------------------

const queue: Array<Watcher> = []
let has: { [key: number]: true | undefined | null } = {}
export function queueWatcher(watcher: Watcher) {
  const id = watcher.id
  // 如果某个watcher没有被推入队列
  if (has[id] != null) {
    return
  }

  if (watcher === Dep.target && watcher.noRecurse) {
    return
  }

  has[id] = true // 已经推入
  if (!flushing) {
    queue.push(watcher) // 推入到队列
  } else {
    let i = queue.length - 1
    while (i > index && queue[i].id > watcher.id) {
      i--
    }
    queue.splice(i + 1, 0, watcher)
  }
  if (!waiting) {
    waiting = true

    if (__DEV__ && !config.async) {
      flushSchedulerQueue()
      return
    }
    nextTick(flushSchedulerQueue) // 下一个tick更新
  }
}

执行update方法时将当前watcher实例传入到定义的queueWatcher方法内,这个方法的作用是把将要执行更新的watcher收集到一个队列queue之内,保证如果同一个watcher内触发了多次更新,只会更新一次对应的watcher,我们举一个小示例:

js
export default {
  data() {
    return {
      // 都被模板引用了
      num: 0,
      name: 'cc',
      sex: 'man'
    }
  },
  methods: {
    changeNum() {
      // 赋值100次
      for (let i = 0; i < 100; i++) {
        this.num++
      }
    },
    changeInfo() {
      // 一次赋值多个属性的值
      this.name = 'ww'
      this.sex = 'woman'
    }
  }
}

这里的三个响应式属性它们收集都是同一个render-watcher。所以当赋值 100 次的情况出现时,再将当前的render-watcher推入到的队列之后,之后赋值触发的set队列内并不会添加任何render-watcher;当同时赋值多个属性时也是,因为它们收集的都是同一个render-watcher,所以推入到队列一次之后就不会添加了

知识点:Vue还是挺聪明的,通过这个示例能看出来,派发更新通知的粒度是组件级别,至于组件内是哪个属性赋值了,派发更新并不关心,而且怎么高效更新这个视图,那是之后diff比对做的事情。

队列有了,执行nextTick(flushSchedulerQueue)在下一次tick时更新它,这里的nextTick就是我们经常使用的this.$nextTick方法的原始方法,它们作用一致,实现原理请看nextTick。我们来看下参数flushSchedulerQueue是个啥?

ts
function flushSchedulerQueue() {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  queue.sort(sortCompareFn) // watcher 排序

  // 遍历队列
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    // render-watcher独有属性
    if (watcher.before) {
      watcher.before() // 触发 beforeUpdate 钩子
    }
    id = watcher.id
    has[id] = null
    watcher.run() // 真正的更新方法
    // ...
  }
  // ...
}

原来是个函数,在nextTick方法的内部会执行第一个参数。首先会将queue这个队列进行一次排序,依据是每次new Watcher生成的id,以从小到大的顺序。

知识点:watcher的执行顺序是先父后子,然后是从computed-watcheruser-watcher最后render-watcher,这从它们的初始化顺序就能看出

然后就是遍历这个队列,因为是render-watcher,所以是有before属性的,执行传入的before方法触发beforeUpdate钩子。最后执行watcher.run()方法,执行真正的派发更新方法。我们去看下run干了啥:

ts
export default class Watcher implements DepTarget {
  // ...
  run() {
    if (this.active) {
      const value = this.get() // 其实就是重新执行一次get方法
      if (value !== this.value || isObject(value) || this.deep) {
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
}

执行run其实就是重新执行一次this.get()方法,让vm._update(vm._render())再走一遍而已。然后生成新旧VNode,最后进行diff比对以更新视图。

最后我们来说下Vue基于Object.defineProperty响应式系统的一些不足。比如只能监听到数据的变化,所以有时data中要定义一堆的初始值,因为要先加入响应式系统后才能被感知到;还有就是常规 JavaScript 操作对象的方式,并不能监听到增加以及删除。Vue为了解决这个问题,提供了两个 API:setdelete

原理图

Vue响应式原理

通过Object.defineProperty遍历对象的每一个属性,把每一个属性变成一个gettersetter函数,读取属性的时候调用getter,给属性赋值的时候就会调用setter

observe

当运行render函数的时候,发现用到了响应式数据,这时候就会运行getter函数,然后watcher(发布订阅)就会记录下来。当响应式数据发生变化的时候,就会调用setter函数,watcher就会再记录下来这次的变化,然后通知render函数,数据发生了变化,然后就会重新运行render函数,重新生成虚拟 DOM 树

MIT License