vue 响应式原理

vue 版本2.5.17

Object.defineProperty

  Object.defineProperty(obj,prop, descriptor)
// obj 是定义属性的对象,prop 是属性名称,descriptor 是属性描述符,核心点就是 descriptor,vue 主要就是利用 descriptor 的 get 和 set 做响应式处理

依赖收集

const dep = new Dep()
dep.depend()

在 get 方法中创建一个 dep 对象,通过 depend()做依赖收集,Dep 是一个 class,有一个静态属性 target,是一个全局唯一 Watcher,保证同一时间只能有一个全局的 Watcher 被计算,Dep 可以理解为是用来管理 Watcher 的

Watcher

Watcher 是一个 class,定义了一些与 Dep 相关的属性,保存 Wathcer 实例持有的 Dep 实例的数组

过程

知道了在访问数据对象的时候会触发 get 方法收集依赖,那 get 方法是在什么时候触发的呢?这一切需要从 组件mount 开始
在 mountComponent中有一段主要逻辑代码

updateComponent = () => {
  vm._update(vm._render(), htdrating
}
new Watcher(vm, updateComponent, noop, null, true)

在 mount 的时候会实例化一个 渲染watcher,先执行 Watcher 的构造函数,然后调用this.get()方法,进入get 函数,首先会执行:

pushTarget(this)

pushTarget的定义在 dep.js 中,定义如下:

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

实际上就是把 Dep.target复制为当前的渲染 watcher 并压栈(恢复的时候用),接着又执行了:

  value = this.getter.call(vm, vm)

this.getter 对应的就是 updateComponent函数,实际上就是执行

vm._update(vm._render(), hydrating)

会执行 vm._render()方法,这个方法会生成渲染 VNode,并且在这个过程中会访问 vm 上的数据,这个时候就触发了数据对象的 getter,每个对象的 getter 都有一个 dep,在触发 getter 的时候调用 dep.depend()方法,接着调用 Dep.target.addDep(this),即渲染 watcher 的 addDep 方法,定义如下:

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

派发更新

在 defineProperty的 set中有两个关键点,一个是 childOb = !shaoolw && observe(newVal), 将新值变成一个响应式对象;另一个是 dep.notify(),通知所有的订阅者。

过程分析

当对组件响应式数据做了修改时,就会触发 setter,调用 dep.notify(),notify()方法在 dep.js 中,定义如下

nodify () {
  const subs = this.subs.slice()
  for(let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
  }
}

变量所有的 subs,即 Watcher 的实例数组,然后调用每一个 watcher 实例的 update 方法,update 方法定义在 watcher.js 中:

update () {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

这里针对 Watcher的不同状态,会执行不同的逻辑,在渲染中会执行到 queueWatcher,定义在 scheduler.js:

const queue: Array = []
const activatedChildren: Array = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      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) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

同一个 watcher 只会触发一次,不是每次数据改变都会触发 watcher 的回调,而是将 watcher 添加到一个队列,在 nextTick 中执行 flushSchedulerQueue,定义如下:

function flushSchedulerQueue () {
  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.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    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()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

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

首先会依据 id 从小到大排序,主要是确保:1.组件的更新由父到子,2.用户自定义的 watcher 优先于渲染 watcher 执行 3.如果一个组件在父组件的 watcher 执行期间被销毁,那对应的 watcher 不需要执行
然后遍历队列,执行 watcher.run()。有一个细节就是在遍历的时候会堆 queue.length 求值,因为在执行 watcher.run 的时候,可能会有新的 watcher 添加进来,新增的 watcher 会依据 id 添加到队列,在下一次循环中立即执行,状态恢复,情况队列。
接下来看 watcher.run 的逻辑

/**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      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
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

首先调用 get 方法,拿到当前的值,然后判读是否满足新旧值不等,新值是对象类型、deep 模式任何一个条件,则执行 watcher 的回调,对于渲染 watcher 而言,在执行 this.get()方法的时候会调用 getter 方法,即:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

所以这就是当我们修改响应式数据触发组件重新渲染的原因。

  • 问题:数组是怎么收集依赖的?
    回答:在defineProperty 中有一段代码 如下
let childOb = !shallow && observe(val)
if (childOb) {
  childOb.dep.depend()
  if (Array.isArray(value)) {
    dependArray(value)
  }
}

在 Observer的构造函数中有如下代码

if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 */
function dependArray (value: Array) {
  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)
    }
  }
}

如果 val 是数组的话,调用 observe(val)方法将数组变成响应式的,实际上就是给数组添加一个'ob'属性,把该属性值设置为该 Observe的实例,用于标识该数组是一个响应式的数组,并通过数组遍历,将数组里面的每一个元素都变成响应式的,调用 childOb.depend.depend()收集该数组依赖,并且调用 dependArray()遍历收集该数组中每个元素的依赖。在 array.js 中,将改变数组的7个方法做了劫持重写,在调用这些修改数组的方法时,会触发 ob.dep.notify()方法,在之前收集了数组的依赖,所以会在 notify 的时候触发更新,另外对于新增的元素,可以通过ob属性,调用 Observe 的 observeArray 方法,将新增的属性变成响应式的。

你可能感兴趣的:(vue 响应式原理)