vue2响应式原理(2)-- 收集依赖、派发更新

84 阅读2分钟

Object.defineProperty get收集依赖

src/core/observer/index.ts

export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean
) {
  const dep = new Dep()
 
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        if (__DEV__) {
         //...
        } else {
          dep.depend()  
        }
       // ...
      }
      return isRef(value) && !shallow ? value.value : value
    },
    set: function reactiveSetter(newVal) {
     // ...
    }
  })

  return dep
}

首先创建一个和key一一对应的dep, 一个key就对应着一个Dep;

下面代码中, Dep.target是全局唯一的watcher,在同一时间只有一个watcher被计算,如果存在这样一个watcher,调用dep.depend;

那么响应式数据的getter如何被触发呢?

watcher.get 触发响应式数据的gettter

代码中要用到响应式数据的地方有很多,如果模板中用到了,那就将模板抽象为一个渲染watcher,如果是用户自己写的watch监听,那就抽象为一个计算watcher;

在依赖收集阶段只收集这个Watcher类的实例,通知也通知它,它再去通知其他地方

src/core/observe/watcher.ts

export default class Watcher implements DepTarget {
  vm?: Component | null
  expression: string
  cb: Function
  id: number
  deps: Array<Dep>
  //本次求值所收集的Dep实例列表, 在每次求值完成之后都会被赋值给deps,然后被清空
  newDeps: Array<Dep>
  //上次求值所收集的Dep实例Id列表,用来避免两次求值之间的重复收集以及去除废弃的观察者
  depIds: SimpleSet
  //本次求值所收集的Dep实例Id列表,用来避免本次求值的重复收集,
  //在每次求值完成之后都会被赋值给depIds,然后被清空
  newDepIds: SimpleSet
  
  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    if ((this.vm = vm) && isRenderWatcher) {
      vm._watcher = this
    }
    
    this.cb = cb
    this.id = ++uid // uid for batching
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = __DEV__ ? expOrFn.toString() : ''
    // parse expression for getter
    if (isFunction(expOrFn)) {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy ? undefined : this.get()
  }

  get() {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e: any) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
// ...
}

src/core/observe/dep.ts

Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = []

export function pushTarget(target?: DepTarget | null) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

watcher.getter这个函数可能是生成DOM的渲染函数,也可能是watch选项中的回调函数

在开始vue渲染页面的时候,是通过渲染watcher来实现的,实例化渲染Watcher的时候,在构造器函数constructor中会调用this.get(),首先pushTarget(this),这里的this就是渲染watcher,把 Dep.target 赋值为当前的渲染 watcher 并压入targetStack中,执行 value = this.getter.call(vm, vm),暂时记住,这个getter函数执行就会读取模板中的响应式数据,也就会触发响应式数据的getter函数收集依赖

Dep 管理依赖Watcher

src/core/observe/dep.ts

export default class Dep {
  static target?: DepTarget | null
  id: number
  subs: Array<DepTarget>

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

  addSub(sub: DepTarget) {
    this.subs.push(sub)
  }
   
  depend(info?: DebuggerEventExtraInfo) {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
}

注意Dep的静态属性target ,全局唯一的依赖watcher,属性subs也是一个watcher数组;

根据上面的分析,Dep.target现在被赋值为渲染watcher, depend方法中,如果dep.target存在,就调用watcher的addDep方法 通过 pushTarget使Dep.target等于目前正在被收集的watcher,同时把watcher压入栈中,popTarget为了把Dep.target还原为上一个watcher,同时让watcher出栈,这样做使的嵌套的数据对象可以收集到准确的watcher

Watcher 被收集的依赖

watcher.addDep 收集依赖watcher

export default class Watcher implements DepTarget {
  vm?: Component | null
  expression: string
  cb: Function
  id: number
  deps: Array<Dep>
  //本次求值所收集的Dep实例列表, 在每次求值完成之后都会被赋值给deps,然后被清空
  newDeps: Array<Dep>
  //上次求值所收集的Dep实例Id列表,用来避免两次求值之间的重复收集以及去除废弃的观察者
  depIds: SimpleSet
  //本次求值所收集的Dep实例Id列表,用来避免本次求值的重复收集,
  //在每次求值完成之后都会被赋值给depIds,然后被清空
  newDepIds: SimpleSet
  
  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    if ((this.vm = vm) && isRenderWatcher) {
      vm._watcher = this
    }
    
    this.cb = cb
    this.id = ++uid // uid for batching
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = __DEV__ ? expOrFn.toString() : ''
    // parse expression for getter
    if (isFunction(expOrFn)) {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy ? undefined : this.get()
  }
  
  //...
  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)
      }
    }
  }

// ...
}

addDep:每个响应式对象的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this),watcher的addDep方法,收集dep和dep.id,同时dep.addSub也把当前watcher收集到subs数组中为了后面的触发更新,其中的逻辑判断,是为了dep不被重复收集;

watcher.get()最后调用了this.cleanupDeps(),为什么还要清除dep呢?

watcher.cleanupDeps 清除依赖

export default class Watcher implements DepTarget {
 // ...
  cleanupDeps() {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp: any = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

// ...
}

cleanupDeps:在执行this.get()收集完依赖的最后,会执行this.cleanupDeps(),遍历 deps,移除对 dep.subs 数组中 Wathcer 的订阅,然后把 newDepIds 和 depIds 交换,newDeps 和 deps 交换,并把 newDepIds 和 newDeps 清空。

比如在v-if这种情况下,如果为false了,v-if下的模板中的用到的响应式数据变化,也不应该触发渲染watcher更新,所以需要把以前的dep清空,重新收集

Object.defineProperty set触发更新

src/core/observer/index.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
      }
     
      childOb = !shallow && observe(newVal, false, mock)
      if (__DEV__) {
      // ...
      } else {
        dep.notify()
      }
    }
  })
  return dep
}

判断hasChanged(value, newVal),如果新旧值一样,就什么都不做;

observe(newVal, false, mock)如果newValue是对象,将返回Observer实例给childOb,同时将newValue也变为响应式

dep.notify 通知更新

如果shallow为false,也就是说要深度响应式,就调用observe(newVal, false, mock),如果newval是对象,它里面的属性也会变成响应式, 接下来就是dep.notify()通知所有的依赖

src/core/observe/dep.ts

export default class Dep {
 // ...
  notify(info?: DebuggerEventExtraInfo) {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

watcher.update

遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcher 的 update 方法

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

先按一般情况分析,直接走到 queueWatcher(this)

queueWatcher 把watcher装一起等更新

src/core/observe/schduler.ts

const queue: Array<Watcher> = []
let has: { [key: number]: true | undefined | null } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0

export function queueWatcher(watcher: Watcher) {
  const id = watcher.id
  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
    nextTick(flushSchedulerQueue)
  }
}

可以看到,每一次数据更新都把watcher往queue数组里面放,if (has[id] != null)不让watcher重复添加,例如:如果用户在data中定义了两个啊a,b两个响应式属性,都在模板中用到,然后同时更新a,b两个属性,这两个响应式数据的依赖watcher都是同一个渲染watcher,没必要收集两次到queue数组中,收集一次就好了

if (!flushing)条件,flushSchedulerQueue没有执行前,watcher队列queue没有正在刷新,就直接添加,else中表示正在刷新,这时数据又变化了,要往queue中添加相应依赖watcher,就需要判断此时queue中watcher执行到第几个了,并通过watcher.id从后往前找到位置插进去在本轮循环中立即执行

至于nextTick方法,暂时先知道在nextTick中调用flushSchedulerQueue方法

src/core/observe/schduler.ts

flushSchedulerQueue 给watcher排个序

const sortCompareFn = (a: Watcher, b: Watcher): number => {
  if (a.post) {
    if (!b.post) return 1
  } else if (b.post) {
    return -1
  }
  return a.id - b.id
}

function flushSchedulerQueue() {
  currentFlushTimestamp = getNow()
  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(sortCompareFn)

  // 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]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
  }
}

queue.sort(sortCompareFn)按watcher的id排序:

1.组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。

2.用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。

3.如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。

watcher.run 更新页面或执行回调

export default class Watcher implements DepTarget {
 // ...
  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) {
          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)
        }
      }
    }
  }

// ...
}

如果是渲染watcher,const value = this.get(),当this.get()执行就会更新页面,如果是用户写的计算watcher, this.cb.call(this.vm, value, oldValue),this.cb就是watch中的回调,并且抛出最新值value和上一次的值oldValue