likes
comments
collection
share

vue2源码分析-依赖收集(渲染、computed、user三种watcher的收集过程)

作者站长头像
站长
· 阅读数 58

简介

前面我们分析了vue2中对象和数组的响应式原理。今天我们再来说说vue2中的依赖收集。

依赖收集我们都知道,就是需要触发响应式数据的get方法,但是在初始化的时候,我们并没有去触发get方法,那每个数据的get方法到底是在什么时候被触发的呢?都有哪些方式触发get呢?

带着这些疑问,我们通过阅读源码的方式来一探究竟。😉

本文vue版本为2.6.14

defineReactive

前面我们说到,依赖收集主要是在defineReactive方法里面的get方法。这里我们重点来看下这个方法。

// src/core/observer/index.js

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  // 实例化 dep,一个 key 一个 dep
  const dep = new Dep()

  // ...

  // 这里很重要,如果不是浅监听,则递归监听
  // 如果值是对象,则又会进行监听
  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 中添加 watcher,也在 watcher 中添加 dep
        dep.depend()
        // 有 childOb 表示当前value是对象
        if (childOb) {
          // 继续进行依赖收集,其实就是将同一个watcher观察者实例放进了两个dep中,一个是正在本身闭包中的dep,另一个是子元素的dep
          childOb.dep.depend()
          // 如果当前value还是数组
          if (Array.isArray(value)) {
            // 将数组每一项都进行依赖收集
            dependArray(value)
          }
        }
      }
      return value
    },
  })
}

为了方便理解我把 dependArray贴出来。

dependArray

遍历数组,对每一项元素进行依赖收集,如果子元素还是数组,则会进行递归处理。

// src/core/observer/index.js

function dependArray (value) {
  for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
    e = value[i];
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      dependArray(e);
    }
  }
}

在这里我们重点关注三个点

第一 const dep = new Dep(),每个key都会有一个dep对象与之对应。

第二 如果有Dep.target 则执行 dep.depend()

第三 如果有childOb即执行childOb.dep.depend()。并且如果是数组的话还会继续执行dependArray

这里为什么要执行childOb.dep.depend(),这个笔者会在扩展部分进行说明。

Dep 到底是个什么东西呢?我们来看看

Dep

Dep主要是用来管理Watcher的。

// src/core/observer/dep.js

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

export default class Dep {
  // 静态属性 target,值为Watcher对象
  static target: ?Watcher;
  id: number;
  // watcher数组
  subs: Array<Watcher>;

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

  // 在 dep 中添加 watcher
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  
  // 在 dep 中删除 watcher
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  // 向 watcher 中添加 dep 又向 dep 中添加 watcher
  depend () {
    // 其实调用的是watcher里面的addDep方法
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  // 派发更新
  // 通知 dep 中的所有 watcher,执行 watcher.update() 方法
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // 排序
      subs.sort((a, b) => a.id - b.id)
    }
    // 遍历 dep 中存储的 watcher,执行 watcher.update()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep.target = null
// 用数组模拟栈数据结构
const targetStack = []

// 在需要进行依赖收集的时候调用,设置 Dep.target = watcher
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

// 依赖收集结束调用,设置 Dep.target为栈顶的那个watcher
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Dep中,主要有如下重点

  1. 有一个静态属性target,类型为Watcher。这是一个非常巧妙的设计,因为在同一时间只能有一个 Watcher 被计算。 Dep.target = 当前正在执行的 watcher
  2. 有一个subs数组,用来存放watcher
  3. depend方法,实际上调用的是watcher里面的addDep方法。在addDep中实际上是一个双向添加的过程。既向 watcher 中添加 dep 又向 dep 中添加 watcher
  4. 因为有父子组件的存在(洋葱模型),所以这里巧妙的设计了一个targetStack栈,调用pushTarget方法的时候将watcher推入栈中并给Dep.target赋值。调用popTarget方法的时候将最上面的watcher弹出,并给Dep.target赋值栈顶watcher

接下来我们再来看看与之关联的Watcher

Watcher

我们知道,在初始化的时候vue会遍历data,重写属性的get、set方法进行数据劫持。但是并没有去触发get、set方法,因此,并没有进行依赖收集和派发更新。

真正的依赖收集其实是在Watcher中。因为Watcher会触发数据的get方法。

因为本文只探讨依赖收集,所以笔者只贴出了Watcher依赖收集相关代码。

// src/core/observer/watcher.js

import {
  warn,
  remove,
  isObject,
  parsePath,
  _Set as Set,
  handleError,
  invokeWithErrorHandling,
  noop
} from '../util/index'

import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'

import type { SimpleSet } from '../util/index'

let uid = 0

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    // computed的lazy会是true,所以第一次他不会计算,而是在需要使用的时候才会计算
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  // 依赖收集
  get () {
    // 将 Dep.target = this
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 执行回调函数,触发依赖收集
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) {
        traverse(value)
      }
      // 将 Dep.target 设置为栈顶的watcher
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  // 这是一个双向添加的过程
  // 添加dep到newDeps,然后又将自身watcher添加到subs数组
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      // watcher 加 dep
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // dep 加 watcher
        dep.addSub(this)
      }
    }
  }

  // 清除依赖收集
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = 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
  }

  // ...
}

在详细分析Watcher之前,我们先要搞清楚,在vue中到底有几种watcher

watcher种类

vue中,总共有三种watcher

渲染watcher

第一种是渲染watcher,每一个组件都会对应一个渲染watcher。在beforeMounte之后mounted之前,会初始化一个watcher

vue2源码分析-依赖收集(渲染、computed、user三种watcher的收集过程)

computed watcher

第二种是computed watcher,它会遍历 computed 中的每个 key,向 computed watchers 列表中新增一个 watcher 实例。

vue2源码分析-依赖收集(渲染、computed、user三种watcher的收集过程)

user watcher

第三种是 user watcher,它会遍历 watch 中的每一个 key,调用 vm.$watch 创建一个 watcher 实例。

vue2源码分析-依赖收集(渲染、computed、user三种watcher的收集过程)

因为渲染watcher才是依赖收集的大头,所以下面笔者以渲染watcher为例,来详细分析下依赖收集的过程。

渲染watcher依赖收集详细过程

每个组件在mount前都会执行 mountComponent 函数,其中有一段比较重要的逻辑,大致如下:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

每一个组件都会有一个渲染watcher与之对应。

当我们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,然后会执行它的 this.get() 方法,进入 get 函数,首先会执行:

pushTarget(this)

实际上就是把 Dep.target 赋值为当前的渲染 watcher 并压栈(因为父子组件绑定是洋葱模型,这里巧妙的使用了栈数据结构)。接着又执行了:

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

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

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

这就是真正的依赖收集,它会先执行 vm._render() 方法,render 方法会生成 VNode,并且在这个过程中会对 vm 上的数据访问,这个时候就触发了数据对象的 get

每个对象值的 get 都持有一个 dep,在触发 get 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)

我们再来看看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)
    }
  }
}

addDep方法会做一些逻辑判断(保证同一数据不会被添加多次),将dep对象添加到当前watchernewDeps数组中,后执行 dep.addSub(this),那么就会执行 this.subs.push(sub),也就是说把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。

这个方法是一个双向添加的过程,既把dep添加到watcher,又把watcher添加到dep

所以在渲染函数执行生成VNode的过程中,会触发所有用到数据的 get,这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了么,其实并没有,在完成依赖收集后,还有几个逻辑要执行,首先是:

if (this.deep) {
  traverse(value)
}

这个是要递归去访问 value,触发它所有子项的 getter,接下来执行:

popTarget()

popTarget 在讲dep的时候也说了,实际上就是把 Dep.target 恢复成上一个状态,因为当前 vm 的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变。

最后执行:

this.cleanupDeps()

在这里我们详细分析下cleanupDeps

cleanupDeps () {
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  let tmp = 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
}

考虑到 vue 是数据驱动的,所以每次数据变化都会重新 render,但是并不会再重新创建watcher(因为渲染watcher只要在第一次挂载的时候会创建),那么 vm._render() 方法又会再次执行,并再次触发数据的 getters。所以 Watcher 在构造函数中会初始化 2 个 Dep 实例数组,newDeps 表示新添加的 Dep 实例数组,而 deps 表示上一次添加的 Dep 实例数组。

在执行 cleanupDeps 函数的时候,会首先遍历 deps,移除对 dep.subs 数组中 Wathcer 的订阅,然后把 newDepIds 和 depIds 交换,newDeps 和 deps 交换,并把 newDepIds 和 newDeps 清空。

那么为什么需要做 deps 订阅的移除呢,在添加 deps 的订阅过程,已经能通过 id 去重避免重复订阅了。

考虑到一种场景,我们的模板会根据 v-if 去渲染不同子模板 a 和 b,当我们满足某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使用的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们一旦改变了条件渲染了 b 模板,又会对 b 使用的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的。

因此 Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除了,所以不会有任何浪费,真的是非常赞叹 Vue 对一些细节上的处理。

computed watcher依赖收集详细过程

// src/core/instance/state.js

function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)

  for (const key in computed) {
    const userDef = computed[key]
    // 获取get方法
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // 一个computed创建一个Watcher
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        // computedWatcherOptions = { lazy: true }
        computedWatcherOptions
      )
    }
  }

  // 在vue实例上没有的的属性执行defineComputed方法
  if (!(key in vm)) {
    defineComputed(vm, key, userDef)
  }
}

可以看到,在我们初始化过程中,会将我们在vue中定义好的computed对象进行遍历。对于每个computed key都会创建一个computed watcher

注意,computed watcherget方法并不是像渲染watcher一样在实例化的时候执行,而是会延迟执行,这个我们在Watcher的构造函数内也能发现。

// src/core/observer/watcher.js

// computed watcher lazy会是true,所以第一次不会计算
this.value = this.lazy ? undefined : this.get()

并在后面会判断,在vue实例上没有该属性的时候会执行defineComputed方法。

接下来我们来看看defineComputed方法。

// src/core/instance/state.js

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 浏览器环境 一定是true,也就是会缓存
  const shouldCache = !isServerRendering()
  // 函数式写法
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    // 对象式写法
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  // 给当前vue实例定义属性和描述
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

因为computed是支持函数写法和对象写法两种方式,所以它这里也进行了判断。

默认情况下,他们都会执行到createComputedGetter方法,也就是给当前computed属性定义get方法。

我们再来看看createComputedGetter方法的逻辑。

// src/core/instance/state.js

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // computed watcher dirty的初始值是true
      if (watcher.dirty) {
        // 真实执行计算属性的get方法
        watcher.evaluate()
      }
      // 依赖收集,在真实渲染的时候才会进来
      // 此时Dep.target是渲染watcher
      if (Dep.target) {
        watcher.depend()
      }
      // 返回值
      return watcher.value
    }
  }
}

这个方法逻辑就是从当前组件实例的watcher数组中取出watcher。因为computed watcherlazy 是 true ,而Watcher中,this.dirty = this.lazy,所以computed watcher dirty的初始值也是true

然后执行evaluate()。这里才是真正的去计算值。所以computed是在真实使用到了的情况下(也就是访问到了计算属性的get方法)才会进行计算。否则并不会计算。

接下来我们来看看evaluate()方法

// src/core/observer/watcher.js

evaluate () {
  // 执行get获取值,并赋值给当前watcher
  this.value = this.get()
  // 将标志位置为false,依赖没发生改变就不需要重新计算。
  this.dirty = false
}

evaluate是专门给计算属性使用的。首先它会执行get方法获取到计算属性的值,并将值赋值给当前的watcher实例。这也是为什么上面会返回return watcher.value。然后将标志位dirty置为false。也就是计算属性的依赖没发生改变的时候就不需要重新计算了。(也就是我们常说的computed会有缓存功能)。

后面会执行 watcher.depend()进行依赖收集。注意,这时候的 Dep.target 是渲染 watcher,所以 this.dep.depend() 相当于渲染 watcher 订阅了这个 computed watcher 的变化。(其实就是将computed watcher的所有dep又全部添加到渲染watcherdeps中。)

// src/core/observer/watcher.js

depend () {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

所以,在computed的依赖发生改变的时候既会通知computed watcher又会通知当前的渲染watcher重新渲染。

当通知computed watcher的时候其实只干了一件事件,就是将标志位又置为true。表示computed在下次使用的时候需要重新计算了。这也是和渲染watcher的另一个区别,在通知更新的时候并不会去重新计算,而只是将标志位置为true

vue2源码分析-依赖收集(渲染、computed、user三种watcher的收集过程)

最后会返回get方法的值。

总的来说就是,一个computed属性对应一个computed watchercomputed watcher的get方法不像渲染watcher一样在实例化的时候执行,而是会延迟,会在真正使用到的时候才会执行。并且在页面渲染的时候,computeddep也会被同步添加到渲染watcher中来。这样computed的依赖发生改变的时候,页面会重新渲染,计算属性也会重新计算。

并且computed也做了计算优化,在依赖数据没发生改变,也就是标志位没重新恢复到true的时候只会返回之前计算好的值,并不会重新计算。

这就是computed watcher的依赖收集逻辑,小伙伴们是否看懂了呢?

user watcher依赖收集详细过程

// src/core/instance/state.js

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    // 数组 遍历
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

这个方法很简单,因为watchhandler可能是数组、对象、字符串、方法,所以在这里进行了初步判断,分别调用createWatcher方法。

// src/core/instance/state.js

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 是对象 就取handler.handler
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  // 字符串其实就是methods里面的方法名,直接从vm上获取
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  // 其实就是调用 vm.$watch
  return vm.$watch(expOrFn, handler, options)
}

createWatcher中,进行了更深一层的判断,分别对对象、字符串形式的写法做了一下适配。最后调用了我们熟悉的$watch方法。

接下里我们再来看看$watch方法。

// src/core/instance/state.js

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  // 因为我们可以直接调用$watch,所以这里又进行了判断
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  // user wathcer的 user 选项会是 true
  options.user = true
  // 实例化watcher,它是有回到函数cb的
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 配置了immediate会立即执行
  if (options.immediate) {
    const info = `callback for immediate watcher "${watcher.expression}"`
    pushTarget()
    invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
    popTarget()
  }
  // 返回取消监听的函数
  return function unwatchFn () {
    watcher.teardown()
  }
}

因为我们可以直接调用$watch,所以这个方法首先会对回调函数再次进行判断。

然后再创建Watcher对象。并且可以发现 user watcher渲染watcher和computed watcher的区别,它是有个回调函数的。

因为user watcherexpOrFn一般会是一个表达式,所以在Watcher实例化的时候会有如下逻辑,会进行转化,转成一个函数。(这是和渲染watcher的第一个区别)

vue2源码分析-依赖收集(渲染、computed、user三种watcher的收集过程)

其实对于user watcher我们并不关心它的get,而是它的回调,因为user watcher就是在依赖数据发生改变的时候能执行回调就可以了。所以我们再看看更新方法。

vue2源码分析-依赖收集(渲染、computed、user三种watcher的收集过程)

可以发现,它的流程和渲染watcher其实是差不多的,就是最后多了一个回调函数执行的操作。(这是和渲染watcher的第二个区别)

然后对于配置了immediate选项的话会立即执行。

最后返回取消监听的函数。

这就是user watcher依赖收集的总体过程,大体上和渲染watcher还是很像的。

总结

依赖收集主要是通过渲染、computed、user三种watcher进行收集的。

  • render()时,也就是通过渲染函数生成虚拟dom时,触发 render watcher 依赖收集。

  • initState方法中,对computed 属性初始化时,触发 computed watcher 依赖收集。

  • initState方法中,对侦听属性初始化时,触发 user watcher 依赖收集(这里就是我们常写的那个watch)。

所以我们也可以得出,定义在data函数里面的属性一定会被响应式处理。但是不一定会被依赖收集。因为只有在被模板或者computed、watch里面使用到了的属性才会触发get方法被依赖收集。

好啦,关于依赖收集,笔者就讲到这里啦。关于派发更新咱们后面见。😃

扩展

为什么有两种dep在收集依赖?

细心的同学会发现除了在defineReactive方法中,为每个属性都创建了一个dep对象外,其实在new Observer()的时候也创建了一个dep对象。

vue2源码分析-依赖收集(渲染、computed、user三种watcher的收集过程)

vue2源码分析-依赖收集(渲染、computed、user三种watcher的收集过程)

那为什么会有两个dep对象呢?

对于defineReactive里面的dep对象,我们很清楚,在obj属性的get方法中收集当前watcher,在触发obj属性的set方法的时候通知watcher更新。

但是对于new Observer()里面的dep对象就有点没弄明白了。为什么这里还会有一个dep对象呢?

其实这个dep是给Vue.set/Vue.delete和调用数组七个原型方法,去派发更新时使用的。

我们知道,defineReactive里面的dep对象其实是一个闭包属性,在这个方法里面能访问到,但是在外部是访问不到的。前面我们分析了Vue.set/Vue.delete的原理和数组修改七个原型方法的原理,他们在最后都会有一个手动派发更新的操作。

vue2源码分析-依赖收集(渲染、computed、user三种watcher的收集过程)

vue2源码分析-依赖收集(渲染、computed、user三种watcher的收集过程)

因为闭包里面的dep对象在外面获取不到,但是这里手动派发更新又需要用到dep,所以就有了new Observer()里面的depVue.set/Vue.delete和数组重写原型方法里面的ob.dep就是new Observer()里面的dep对象。因为他们在依赖收集的时候收集的是同样的watcher,所以用这个dep对象进行派发更新也是可以的。

dep和watcher为什么是双向添加呢?

dep里面添加watcher我们知道,是为了数据修改能方便通知watcher去更新。

但是watcher里面又添加dep是什么意思呢?

其实就是为了方便dep移除subs数组中的watcher。因为我们依赖收集只做了添加,并没有移除,移除其实就是在watcher中判断并处理的。

系列文章

vue2源码分析-data、props、methods、computed属性可以重名吗

vue2源码分析-响应式原理

vue2源码分析-依赖收集

vue2源码分析-派发更新

后记

感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!

转载自:https://juejin.cn/post/7155401509797593102
评论
请登录