# 依赖收集

首先我要知道,什么是依赖,以及收集依赖的目的是什么,即我们究竟要收集谁,收集了之后可以做什么。

收集谁?换句话说就是当属性发生变化时,我们需要通知谁。

我们需要通知到使用数据的地方,它可以是个模板,也可以是用户自定义的 watch 等多个地方和不同的类型,因此需要我们封装一个可以集中处理这些情况的类。然后,我们在收集依赖阶段只收集这个封装好的类的实例进来,当数据发生变化时,也只通知它一个,然后由它负责通知其它地方。在 Vue 中这个类就是 Watcher

之前我们也说过在 getter 中收集依赖,在 setter 中派发更新,因此我们来回顾下收集依赖的代码:

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()
      }
      // #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()
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

这里我们可以看到首先执行了这串代码 const dep = new Dep(),然后在定义的 getter 函数中先判断了 Dep.target 存在后,则调用 dep.depend() 函数来进行依赖收集,同时还对 childOb 进行判断及后续处理,因此我们首先需要高清以下几个问题:

  • 什么是 Dep?
  • Dep.target 又是什么?
  • dep.depend() 方法内部是如何实现依赖收集的

# Dep

首先让我们来搞懂什么是 Dep,它是定义在 observe 目录中的 dep.js 文件中。其代码如下所示:

let uid = 0

/**
 * 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 = []
  }

  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()
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

从上面我们可以知道,Dep 类实际上是专门用来管理 Watcher(依赖)的类,其首先定义了一个静态属性 target,其值是全局唯一的 Watcher,因为同一时间只有一个 Watcher 会被计算,然后其自身属性 id 相当于 Dep 的逐渐,会在实例化的时候自增。同时自身属性 subs 是存储各种 Watcher 的数组,如 render Watcheruser Watchercomputed Watcher 等。

Dep 的其它方法就是添加和移除各种各样的 Watcher,以及执行 Watcher 中的方法,因此我们来看 Watcher 即依赖究竟是怎样实现的。

# Watcher

Watcher 是定义在 observe/watcher.js 文件中的类,其代码如下所示:

let uid = 0

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
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
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    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 {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
  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)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  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
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

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

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225

从依赖收集角度来讲,我们在构造函数中主要需要关注以下四个属性:

this.deps = []             // 旧dep列表
this.newDeps = []          // 新dep列表
this.depIds = new Set()    // 旧dep id集合
this.newDepIds = new Set() // 新dep id集合
1
2
3
4

这里使用的 Set 集合,是在判断当前环境如果不支持原生方法,则使用自己实现的方法,其定义在 core/util/env.js 中:

let _Set
/* istanbul ignore if */ // $flow-disable-line
if (typeof Set !== 'undefined' && isNative(Set)) {
  // use native Set when available.
  _Set = Set
} else {
  // a non-standard Set polyfill that only works with primitive keys.
  _Set = class Set implements SimpleSet {
    set: Object;
    constructor () {
      this.set = Object.create(null)
    }
    has (key: string | number) {
      return this.set[key] === true
    }
    add (key: string | number) {
      this.set[key] = true
    }
    clear () {
      this.set = Object.create(null)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

我们在实例化 Watcher 的时候,会初始化一系列属性,其中其中重要的一点是获取 getter 函数,通过调用该函数就可以获取需要监听的数值。我们可以通过一下经典的使用方式来理解:

vm.$watch('a.b.c', function(newVal, oldVal){
	// do something
})
1
2
3

当调用上述方法 api 方法的时候,在构造函数中,会进行如下赋值:

// 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
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这是当监听的如果是一个表达式的时候,则通过 parsePath 函数读取字符串的 keyPath 来获取监听数据的值的,其定义在 core/util/lang.js 中:

/**
 * Parse simple path.
 */
const bailRE = new RegExp(`[^${unicodeLetters}.$_\\d]`)
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这里并不复杂,先讲 keyPath 分割成数组,然后循环数组一层一层去读取数据,从而最后返回的就是想要的数据。

在构造函数最后,判断如果不是 computed Watcher(只有 computed Watcherlazy 属性才为 true),那么会立即调用 this.get() 函数,接下来我们就分析 this.get() 方法的实现。

get () {
  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 {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

我们可以看到,首先调用 pustTarget(this) 方法,该方法定义在 dep.js 文件中

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
1
2
3
4
5
6
7
8
9
10

该方法的作用主要是将当前 Watcher 实例添加到 targetStack 中,然后把 Dep.target 设置为当前的 Watcher 实例。 所以 Dep.target 实际上就是一个 Watcher 实例。

然后会调用 this.getter 函数来进行求值。

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

我们这里拿计算属性来作为示例:

export default {
  data () {
    return {
      count: 1
    }
  },
  computed: {
    counter () {
      return this.count + 1
    }
  }
}

// value = this.getter.call(vm, vm)
// 相当于
value = counter()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

当我们调用方法时,就会调用对象属性的数据进行访问,从而触发数据对象上的 getter,每个 getter 属性上都有一个 dep, 在触发 getter 的时候,这时存在当前唯一的 Dep.target, 从而会调用 dep.depend() 来收集依赖,即会执行以下方法:

depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
1
2
3
4
5

# addDep 和 cleanupDeps

由之前调用的 pushTarget 可知,Dep.target 已经被赋值为当前 Watcher,从而可以执行 addDep 方法:

 /**
   * Add a dependency to this directive.
   */
  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)
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13

addDep方法首先判断了当前 dep 是否已经在新 dep ids 集合中,不在则更新 dep ids 集合以及新 dep 数组,随后又判断了当前 dep 是否在旧 dep id 集合中,不在则说明没有将当前 Watcher 订阅到当前数据负责依赖管理 dep 中的 subs(用于后续派发更新), 因此会调用 dep.addSub(this) 方法,把当前 Watcher 实例添加到 subs 数组中。

# addDep 运行示例说明

为了加深对 addDep 的理解,我们通过以下示例说明

<template>
  <p>first:{{msg}}</p>
  <p>second:{{msg}}</p>
</template>
<script>
export default {
  name: 'App',
  data () {
    return {
      msg: 'hello world'
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 当初次渲染该组件的时候,会实例化 render Watcher,此时 Dep.target 上的 Watcher 实例就是 render Watcher

     updateComponent = () => {
     	vm._update(vm._render(), hydrating)
     }
    
    // we set this to vm._watcher inside the watcher's constructor
    // since the watcher's initial patch may call $forceUpdate (e.g. inside child
    // component's mounted hook), which relies on vm._watcher being already defined
    new Watcher(vm, updateComponent, noop, {
    	before () {
    		if (vm._isMounted && !vm._isDestroyed) {
    		callHook(vm, 'beforeUpdate')
    		}
    	}
    }, true /* isRenderWatcher */)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  • 当第一次读取页面的 msg 变量时,会触发 getter 从而调用 dep.depend 方法进行依赖收集,在该方法内部调用 addDep 方法,这时 depsdepIdsnewDepsnewDepIds 被初始化为空数组或者空的集合。当执行完 addDep 方法后,会将当前的 dep 添加到 newDeps 数组中,其 id 被添加到 newDepIds 中,最后会讲当前 Watcher 实例添加到 dep 的订阅数组 subs中。

    // 实例化 Dep
    const dep = { id: 0, subs: [] }
    // 第一次执行 addDep 之后
    // 此时的 Watcher 实例
    const watcher = {
    	newDeps: [dep],
    	newDepIds: [0],
    }
    
    dep = { id: 0, subs: [watcher] }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • 当第二次读取 msg 变量的时候,会再次出发 getter 进行依赖收集,由于实例化的 dep 对于 getter 是定义在 defineReactive 函数中的闭包变量,因此两个触发的 getter 中是同一个 dep 实例,此时调用 addDep 方法,会判断 newDepIds 集合中已经存在 dep.id1 的,因此会直接跳过依赖收集。

# popTarget

依赖收集完成之后,会执行以下几个逻辑,首先是:

if (this.deep) {
  traverse(value)
}
1
2
3

这时当我们设置了 deep 属性的时候,会通过调用 traverse(value) 方法,递归来触发所有子项的 getter,从而对每个嵌套的属性都会进行依赖收集,这个将在后面介绍 $watcher API 的时候详细说明。

然后会执行 popTarget() 方法,其定义在 core/observe/dep.js 文件中,如下:

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

这里实际上将 targetStack 出栈,然后将 Dep.target 设置为最后一项,即恢复到上一个状态,因为当前的数据依赖已经收集完成,对应的 Dep.target 也需变化。那么我们为什么要进行这样的操作呢?

这样的目的主要是为了保持正常的依赖关系,如以下代码:

当渲染组件时,会先渲染子组件,当子组件渲染完成之后,父组件才能渲染完毕,其渲染时的钩子函数如下:

parent beforeMount()
child beforeMount()
child mounted()
parent mounted()
1
2
3
4

parent beforeMount 执行时,会执行父组件的 render watcher 进行实例化,然后调用 this.get() 方法,这时的 Dep.target 为父组件的 render watchertargetStack 的伪代码如下:

// 伪代码,实际为 Watcher 实例
Dep.target = 'parent render watcher'
targetStack = ['parent render watcher']
1
2
3

当子组件的 beforeMount 开始执行时,会对子组件的 render watcher 进行实例化,然后调用 this.get() 方法,这时的 Dep.target 为子组件的 render Watcher 。其伪代码如下所示:

// 伪代码,实际为 Watcher 实例
Dep.target = 'child render watcher'
targetStack = ['parent render watcher', 'child render watcher']
1
2
3

当执行子组件的 mounted 方法时,代表 this.getter() 调用完毕,之后会执行 popTarget 方法,执行出栈操作,此时栈数组和 Dep.target 都会发生变化

// 伪代码,实际为 Watcher 实例
Dep.target = 'parent render watcher'
targetStack = ['parent render watcher']
1
2
3

这样就保证了父子组件正确的依赖关系。

# cleanupDeps

最后我们会执行 this.cleanupDeps() 方法,这里主要是进行了依赖清空操作,那么我们为什么要进行清空依赖呢?具体又是怎样清空依赖的?

首先我们可以看到 Watcher 类中其代码实现如下:

export default Watcher {
  // 精简代码
  constructor () {
    this.deps = []              // 旧dep列表
    this.newDeps = []           // 新dep列表
    this.depIds = new Set()     // 旧dep id集合
    this.newDepIds = new Set()  // 新dep id集合
  }
  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
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# cleanupDeps 示例

为了更好的说明其作用,我们举例来说明,假如我们有以下组件:

<template>
  <div id="app">
    <p v-if="count < 1">{{msg}}</p>
    <p v-else>{{age}}</p>
    <button @click="changeCount">Add Count</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      msg: 'hello world',
      age: 12,
      count: 0
    };
  },
  methods: {
    changeCount() {
      this.count++
    }
  }
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

当组件初次进行渲染的时候,render Watcher 实例上面的 newDeps 数组里面有两个 dep 实例,分别是 msgcount 属性触发 getter 的时候被收集,这里由于 v-if/v-else 的原因,并不会触发 age 属性的 getter。执行 addDep 之后的伪代码如下所示:

// 伪代码
this.deps = []
this.depIds = new Set()
this.newDepIds = new Set([1,3])
this.newDeps = [
  { id: 1, subs: [new Watcher()] },
  { id: 2, subs: [new Watcher()] }
]
1
2
3
4
5
6
7
8

当我们点击 Add Count 之后,会执行 this.count++ ,触发 countsetter 函数,从而会触发组件更新,由于此时 count < 1 判断不再成立,因此会触发 agegetter ,在进行依赖收集 addDep 之后,会发生如下变化:

// 伪代码
this.deps = [
  { id: 1, subs: [new Watcher()] },
  { id: 2, subs: [new Watcher()] }
]
this.newDeps = [
  { id: 1, subs: [new Watcher()] },
  { id: 3, subs: [new Watcher()] }
]
this.depIds = new Set([1, 2])
this.newDepIds = new Set([1, 3])
1
2
3
4
5
6
7
8
9
10
11

在调用 this.get() 函数的最后会执行 cleanupDeps 函数,这个方法首先会遍历 this.deps 旧的依赖列表,当发现其中的某个 dep.id 不存在于 newDepIds 集合中,那么就会调用 dep.removeSub(this) 方法将依赖进行移除,现在这里的 this 代表 render watcher。调用完这个方法之后,后续我们再次修改 msg 属性的值的时候就不会在触发组件渲染了。之后会将 depsnewDeps 以及 depIdsnewDepIds 的值进行更换,然后清空 newDepsnewDepIds 的值,如下最终伪代码:

this.depIds = new Set([1, 3])
this.deps = [
  { id: 1, subs: [new Watcher()] },
  { id: 3, subs: [new Watcher()] }
]
this.newDeps = []
this.newDepIds = new Set()
1
2
3
4
5
6
7