源码解析 深入vue响应式原理

深入响应式原理

大家应该都知道响应式原理的核心API是Object.defineProperty。如果不了解请点击:
Object.defineProperty,去MDN了解下这个API的用途。

那这个API是如何做到响应式对象的呢?

接下来我们从源码的角度来进行分析:
我们知道在组件的data中定义的数据,最终都能响应式,看下Vue2.x的源码是如何初始化data的:

响应式对象

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

function initData (vm: Component) {
  let data = vm.$options.data
  // 1.获取到data中定义的对象
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  // 2.验证是否与定义的methods与props冲突;
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
    // 3.proxy代理;
      proxy(vm, `_data`, key)
    }
  }
  // observe data 4.oberve实现data对象响应式;
  observe(data, true /* asRootData */)
}

这里面主要分为如下几步:

  1. 获取到data中的返回对象;
  2. 验证是否与定义的methods与props冲突;
  3. proxy代理;
  4. oberve实现data对象响应式;
    这里的第一步和第二部很容易理解,第四步是实现响应式对象的核心,第三步简单的分析下是为什么,贴上代码:

proxy

proxy(vm, `_data`, key)

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

可以看到,这个地方其实也是利用了Object.defineProperty这个api,定义了存储描述符,让我们在vue内部data属性中定义的key通过代理的方式,可以使用this.key的方式去访问到。简化了我们的代码量。

当我们访问this.key的时候,其实内部返回的就是this._data.key的值。

在vue中我们也可以通过this._data.key的方式访问到我们定义的data,但不建议这么做,_data的意思就是内部属性data。

observe

observe 的功能就是用来监测数据的变化,它是vue2.x 实现响应式的核心,它定义在 src/core/observer/index.js 目录中:

/**
 * 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 {
  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)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

可以看到,这里实例化了一个Observer类,并返回这个实例化后的值,接着看下Observer这个类的源码

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has 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)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

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

这个构造函数首先实例化了Dep对象(这个类跟依赖收集相关,这里不做分析,以后的文章会讲清。)接着执行def函数,把自身的实例添加到数据对象value的__ob__属性上

export function def (obj: Object, key: string, val: any, enumerable?: boolean) {

Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
    })
}

接下来对这个value值做判断,数组的话会调用observeArray方法,否则调用walk方法。而observe最终也会调用observe方法,最终都会执行defineReactive。

/**
 * 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
  }

  // cater for pre-defined getter/setters
  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, {
    enumerable: true,
    configurable: true,
    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
    },
    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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

defineReactive目的就是定义一个响应式对象,首先实例化了一个Dep对象,接着拿到属性描述符,然后对子对象递归调用observe,这样就实现了对象的深度响应式,保证无论访问多深的obj的属性,都使用Object.defineProperty给对应的属性添加了getter和setter。
这个作用是为了访问属性的时候触发getter,收集属性的依赖,改变属性的时候触发setter,派发对应的更新,从而通知页面做更新,接下来我们来聊聊这两个过程。

依赖收集

当访问响应式数据的时候,会触发getter方法,下面我们分析这个过程中,Vue的内部做了哪些事情:

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 创建Dep实例
  const dep = new Dep()

  // 获取obj描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 不可配置则直接返回,不做响应式处理
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 存储预先定义的get方法
  const getter = property && property.get
  ......

  // 深层次对象,则递归执行observe
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 计算值
      const value = getter ? getter.call(obj) : val
      // 如果Dep.target存在,进行依赖收集的操作
      if (Dep.target) {
        // 收集依赖
        dep.depend()
        // childOb存在
        if (childOb) {
          // child收集依赖
          childOb.dep.depend()
          // 如果是数组,数组中的值也需要收集依赖
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    ......
  })
}

上面的代码中,有个Dep的类,我们继续来看:

Dep

let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 * dep是一个可监听对象,可以有多个指令订阅它。
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    // 收集依赖的数组
    this.subs = []
  }

  addSub (sub: Watcher) {
    // 将订阅者Watcher推入依赖收集数组
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    // 在收集依赖数组移除订阅者Watcher
    remove(this.subs, sub)
  }

  depend () {
    // 添加依赖
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    // 派发更新
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
// 正在计算的当前目标观察者。这是全局唯一的,因为在任何时候都只能计算一个观察者。
Dep.target = null
// 存放目标观察者的数组
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

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

  • Dep.target是Dep上的一个静态属性,起始时候是一个null值,
  • pushTarget这个函数,是将当前的目标观察者Watcher推入targetStack栈数组中,并将传入的最新的Watcher,赋值给Dep.target
  • popTarget这个函数,取出targetStack栈数组中的最后一个Watcher,赋值给Dep.target

那么这个Dep.target是什么时候赋值的呢?
我们在之前的文章中有过分析,页面渲染的时候会初始化一个updateComponent方法,随后实例化了一个render Watcher,实例化的过程中,执行了Watcher上的get函数(不清楚这段逻辑可以点击这里),这里我们继续以render Watcher来分析:

/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
  // this指向当前的render Watcher(后续还会分析自定义Watcher与计算属性Watcher)
  pushTarget(this)
  let value
    
  // vm指向当前实例
  const vm = this.vm
  try {
    // 获取当前的值,在取值的过程中,就会触发响应式数据的get函数,而这时的Dep.target就是当前的render Watcher
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    // 与深度监听相关
    if (this.deep) {
      traverse(value)
    }
    // 取值完毕,依赖收集完成,释放当前render Watcher
    popTarget()
    // 清除依赖,后面会分析这段逻辑
    this.cleanupDeps()
  }
  return value
}

执行this.get的时候,会将Dep.target赋值为当前的render Watcher,而执行this.getter,就是执行updateComponent函数,会调用render方法,这个过程中会访问到渲染当前页面的数据,有响应式数据的话,就会进入get方法:

get: function reactiveGetter () {
      ......
      // Dep.target为render Watcher进入逻辑
      if (Dep.target) {
        // 进入逻辑,收集依赖
        dep.depend()
        // 下面逻辑我们之后的文章分析
        if (childOb) {
          // child收集依赖
          childOb.dep.depend()
          // 数组中的值依次收集依赖
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },

接下来执行dep.depend()

// Dep类上的方法
depend () {
  // 通知依赖更新,Dep.target为render Watcher
  if (Dep.target) {
    // 执行Watcher上的addDep方法
    Dep.target.addDep(this)
  }
}
  // Watcher类上的方法
/**
* Add a dependency to this directive.
* 添加一个依赖到这个指令
*/
addDep (dep: Dep) {
  // 这儿id的作用是防止重复订阅
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    // (上面的逻辑先忽略,之后的文章分析)这儿判断如果这个Watcher没有订阅过这个dep
    if (!this.depIds.has(id)) {
      // 将Watcher推入这个dep,也就是Watcher订阅这个dep了
      dep.addSub(this)
    }
  }
}
// Dep类上的方法
addSub (sub: Watcher) {
  // 将这个Watcher推入Dep的subs中
  this.subs.push(sub)
}

通过以上的分析,我们清楚了依赖收集的整个过程了,首先是实例化render Watcher,将当前的Dep.target更改,执行取值方法,获取响应式数据的值,触发响应式数据中的get方法,这样当前的render Watcher就订阅了响应式数据中的dep实例。

派发更新

当改变响应式数据的值的时候,会触发setter方法,下面我们分析整个过程:

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 创建Dep实例
  const dep = new Dep()

  // 获取obj描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 不可配置则直接返回,不做响应式处理
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 存储预先定义的get方法
  const setter = property && property.set
  ......

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    
    ......
    
    set: function reactiveSetter (newVal) {
      // 计算value的值
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 新赋的值与之前的值作对比,如果值相等,直接返回
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      ......
      // setter有定义,调用setter
      if (setter) {
        setter.call(obj, newVal)
      } else {
      // 未定义setter,直接将新值赋给旧值
        val = newVal
      }
      // 这儿的逻辑之后的文章去分析
      childOb = !shallow && observe(newVal)
      // 通知dep去更新
      dep.notify()
    }
  })
}

可以看到,当改变响应式数据的值的时候,set方法会将新值与旧值坐下对比,相等的情况下直接返回,不做任何处理。不相等的情况下再对旧值赋值,同时调用dep的notify方法。在依赖收集的章节我们也分析了Dep类中的一些方法,不清楚可以点击这里,接下来我们去分析dep.notify函数,定义在dep类中:

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  ...

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    // 遍历订阅这个dep的所有Watcher,依次执行Watcher.update函数
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

继续去看Watcher上的update函数:

Watcher.update()

// 定义在Watcher类中
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
  /* istanbul ignore else */
  // 判断是否是计算属性Watcher
  if (this.computed) {
    // A computed property watcher has two modes: lazy and activated.
    // It initializes as lazy by default, and only becomes activated when
    // it is depended on by at least one subscriber, which is typically
    // another computed property or a component's render function.
    if (this.dep.subs.length === 0) {
      // In lazy mode, we don't want to perform computations until necessary,
      // so we simply mark the watcher as dirty. The actual computation is
      // performed just-in-time in this.evaluate() when the computed property
      // is accessed.
      this.dirty = true
    } else {
      // In activated mode, we want to proactively perform the computation
      // but only notify our subscribers when the value has indeed changed.
      this.getAndInvoke(() => {
        this.dep.notify()
      })
    }
  } else if (this.sync) {
    // 自定义Watcher中定义了sync(同步)会进入这个逻辑
    this.run()
  } else {
    // render Watcher进入这个逻辑,我们本次还是分析render Watcher
    queueWatcher(this)
  }
}

继续看queueWatcher函数:

queueWatcher

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let flushing = false
let waiting = false
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 * 将一个观察者推入观察者队列。具有重复id的作业将被跳过,除非它在队列刷新时被推送。
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 如果id没有缓存过,进入逻辑
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      // flush为false的时候,向queue队列push Watcher
      queue.push(watcher)
    } else {
      // 这段逻辑是在flushing过程中如果queue中出现watcher变更的情况会进入
      // if already flushing, splice the watcher based on its id
      // 如果已经刷新,则根据观察者的id拼接观察者
      // if already past its id, it will be run next immediately.
      // 如果已经超过了它的id,它将立即运行。
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    // 刷新队列
    if (!waiting) {
      // wating置为true,保证nextTick执行一次
      waiting = true
      // 执行nextTick
      nextTick(flushSchedulerQueue)
    }
  }
}

从上面的代码可以看出,queueWatcher是将Watcher推入queue数组中进行存储,并执行nextTick,关于nextTick,可以查看我的另一篇分析文章,这个函数就是将放在里面的函数放在下一个tick中去执行,等同步任务全部执行完再触发。

从这里我们也清楚了一个问题,当改变数据触发页面重新渲染,这是个异步的过程。

/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  // flushing赋值为true
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // 刷新之前进行排序,按以下规则
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  // 1. 组件的更新是先父后子
  // 2. 组件的自定义watcher应该在render watcher前面运行(因为自定义watcher创建在先)
  // 3. 如果组件在父组件watcher运行期间被销毁,那么他应该被跳过
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  // queue的长度不要缓存,因为我们在运行现有的watchers的时候,可能会推入新的watchers
  for (index = 0; index < queue.length; index++) {
    // 依次拿到watcher
    watcher = queue[index]
    // 如果watcher有before属性,则执行
    if (watcher.before) {
      // render Watcher的时候,这儿是调用生命周期函数beforeUpdated
      watcher.before()
    }
    id = watcher.id
    // 将has[id]重置
    has[id] = null
    // 执行Watcher.run,下面我们看这个逻辑
    watcher.run()
    // in dev build, check and stop circular updates.
    // 这个地方防止无限循环bug,有一种情况会进入这里
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  // 重置队列状态,flushing,waiting等置为初始状态
  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  // 调用updated生命周期函数
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

watcher.run()与getAndInvoke():

/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
  // this.active为true
  if (this.active) {
    // 执行getAndInvoke
    this.getAndInvoke(this.cb)
  }
}

getAndInvoke (cb: Function) {
  // 调用Watcher.get函数计算value值
  const value = this.get()
  // 判断新值与旧值是否相同,执行传入的cb函数,render Watcher 这儿传入的是个空函数
  if (
    value !== this.value ||
    // Deep watchers and watchers on Object/Arrays should fire even
    // when the value is the same, because the value may
    // have mutated.
    isObject(value) ||
    this.deep
  ) {
    // set new value
    const oldValue = this.value
    this.value = value
    this.dirty = false
    if (this.user) {
      // 自定义watcher进入这个逻辑
      try {
        cb.call(this.vm, value, oldValue)
      } catch (e) {
        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
      }
    } else {
      // computed Watcher 进入这个逻辑
      cb.call(this.vm, value, oldValue)
    }
  }
}

到这里我们清楚了,当我们改变数据值的时候,会遍历数据中实例化的dep收集的依赖,通知Watcher做更新,将所有的Watcher推入一个队列,放在nextTick的时候去执行,所以页面的更新渲染是个异步的过程。

执行的过程中,会对队列中的watchers进行排序,排序完成后遍历queue队列,执行钩子函数beforeUpdated,再依次执行定义的Watcher.get函数,完成queue的遍历后,再执行钩子函数updated。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值