Vue源码分析之响应式对象(一)

558 阅读5分钟

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

前言

我们知道Vue是典型的实现双向数据绑定的MVVM框架,数据变化会更新视图,视图变化会更新数据。视图改变数据我们可以很简单想象到应该是用到了input事件,那数据怎么改变视图呢??

官方文档提到Vue最独特的特性之一就是其非侵入性的响应式系统。当修改普通的Javascript对象时,视图会随之更新。这让Vue的状态管理非常简单,让开发者更容易专注于数据模型。那到底Vue是怎么来实现这个响应式呢??

定义响应式对象

官方文档已经说了Vue2实现响应式的核心是使用Object.defineProperty,把数据对象中的property转化为getter/setter。所以我们可以简单的理解:如果对象拥有getter/setter,就可以称之为响应式对象。getter用来依赖收集,setter用来派发更新。

Object.defineProperty(obj, prop, descriptor)方法可以直接在对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

这里重点关注的就是descriptor参数,它可以用来设置get和set。

定义响应式对象的函数为defineReactive,在src/core/observer/index.js中

/**
* Define a reactive property on an Object.
*/
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
  }
  
  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, {
    ...
  })
}

该函数接收5个参数:

  • obj:要处理的对象
  • key:该对象的key值
  • val:要处理的value值
  • customSetter:定义的辅助函数
  • shallow:判断是否浅复制来处理对象,即是否将对象的子对象也处理为响应式对象

接下来具体看下这个函数的实现:

Dep

首先实例化一个Dep类

const dep = new Dep()

Dep类定义在src/core/observer/dep.js

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 aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

这段代码写的很清晰,熟悉设计模式的都看的出来这段代码与发布订阅模式/观察者模式非常相似。

它定义了一个subs数组,用来收集所有Watcher类型的订阅者(收集所有的依赖);当调用notify方法时,会依次去执行subs数组(会先根据id从小到大排序)中的每一项,即各个Watcher的update方法。

这里的Watcher对象就是订阅者(观察者)。

depend方法需要配合Watcher来看,后边会说,它负责向Dep中添加Watcher。

具体看一下Dep.target:

Dep.target

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

Dep.target是Dep上的静态数据属性。注释说得很清楚,Dep.target代表当前正在执行的watcher,全局唯一。默认会设置成null,代表没有依赖需要收集,如果Dep.target存在说明有依赖需要被收集。

Object.getOwnPropertyDescriptor

接下来调用Object.getOwnPropertyDescriptor来获取obj对象的key属性对应的属性描述符。如果configurable属性为false,则该对象无法设置为响应式对象。

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
  return
}

这里也说明了为啥在性能优化的时候推荐使用Object.freeze来冻结那些无需被依赖收集的对象,因为设置了Object.freeze是无法被设置为响应式的,它内部的所有属性的configurable都是false。

image.png

预处理getter/setter

如果属性已经定义了getter/setter,则先存下来。

const getter = property && property.get
const setter = property && property.set

预处理val

if ((!getter || setter) && arguments.length === 2) {
  val = obj[key]
}

一开始不太理解这个if语句。如果给defineReactive只传了两个参数,则把obj[key]赋值给val,这个可以理解。但是为什么要加(!getter || setter)呢??

上网搜了下,原来!getter是为了解决这个issue。所以当一个对象的某个属性本身具有getter拦截函数,是不会取obj[key]的值,则val为undefined。

那为啥还要判断是否具有setter呢??继续向下看。

childOb

如果shallow为false,说明要对obj深层处理,调用observe函数。observe函数从注释里看到是用来创建一个Observer实例,具体实现后边专门来看,这里大概知道一下Observer类的作用就是为对象添加getter/setter,用来依赖收集和派发更新(Observer内部也是调用defineRective方法)。

let childOb = !shallow && observe(val)

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  ...
}

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

  constructor (value: any) {
    ...
    
    if (Array.isArray(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])
    }
  }
}

Object.defineProperty

接下来就是创建响应式对象的重点了,使用Object.defineProperty函数来对对象的属性值设置getter/setter

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter() {},
  set: function reactiveSetter() {}
});

getter函数

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
},
  • 先判断属性是否本身设置了getter函数,如果设置了直接调用,否则就直接取属性对应的value值。

  • 接下来依赖收集,需要联合Watcher来理解。判断Dep.target是否为空,如果不为空,则把当前依赖添加到dep中。

  • 如果子对象也是响应式的,则也需要依赖收集添加到子dep中。同时判断属性值是否为数组,如果是则调用dependArray进行处理。

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

setter函数

set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
  • 首先拿到原来的value值,然后进行新旧值的判断,如果新旧值相同则直接返回。这里有个newVal !== newVal && value !== value判断,一开始没想明白,怎么会有这种场景??后来一想,哦,NaN!!!

  • 接下来判断属性是否自带setter,如果设置了则执行,否则直接用新值覆盖

  • 如果shadow是false,说明要深度监听,则调用observe将newVal变成响应式。

  • 最后调用dep.notify来通知所有的依赖(subs数组中的watchers)进行更新。

为何要判断(!getter || setter)

看完了整个defineReactive函数,再回到中途遇到的那个问题,为啥对setter也进行判断??

if ((!getter || setter) && arguments.length === 2) {
  val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
  ...
  
  set: function reactiveSetter(newVal){
    ...
    
    childOb = !shadow && observe(val)
  }
})

如果没有setter的判断,当数据对象属性具有getter时,会把val置为undefined。则在后边的深度观测中传给observe的是undefined,即不会被深度监听。但是经过defineProperty后会重新执行setter,并且重新执行深度观测(!shadow && observe(newVal))。这会导致前后不一,一个数据对象定义阶段是非响应式的,修改后又变成响应式的了。

总结

  • Vue2利用Object.defineProperty给数据添加getter和setter来实现响应式对象,getter用来依赖收集,setter用来派发更新。

  • getter依赖收集的核心是Dep。Dep用来管理Watcher,Watcher用来订阅Dep,Dep和Watcher是观察者模式(发布-订阅)的一种体现。