vue2源码分析-依赖收集(渲染、computed、user三种watcher的收集过程)
简介
前面我们分析了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
中,主要有如下重点
- 有一个静态属性
target
,类型为Watcher
。这是一个非常巧妙的设计,因为在同一时间只能有一个Watcher
被计算。Dep.target = 当前正在执行的 watcher
。 - 有一个
subs
数组,用来存放watcher
。 - 有
depend
方法,实际上调用的是watcher
里面的addDep
方法。在addDep
中实际上是一个双向添加的过程。既向watcher
中添加dep
又向dep
中添加watcher
。 - 因为有父子组件的存在(洋葱模型),所以这里巧妙的设计了一个
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
。
computed watcher
第二种是computed watcher
,它会遍历 computed
中的每个 key
,向 computed watchers
列表中新增一个 watcher
实例。
user watcher
第三种是 user watcher
,它会遍历 watch
中的每一个 key
,调用 vm.$watch
创建一个 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
对象添加到当前watcher
的newDeps
数组中,后执行 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 watcher
的get
方法并不是像渲染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 watcher
的 lazy 是 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
又全部添加到渲染watcher
的deps
中。)
// 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
。
最后会返回get
方法的值。
总的来说就是,一个computed
属性对应一个computed watcher
,computed watcher
的get方法不像渲染watcher
一样在实例化的时候执行,而是会延迟,会在真正使用到的时候才会执行。并且在页面渲染的时候,computed
的dep
也会被同步添加到渲染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)
}
}
}
这个方法很简单,因为watch
的handler
可能是数组、对象、字符串、方法,所以在这里进行了初步判断,分别调用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 watcher
的expOrFn
一般会是一个表达式,所以在Watcher
实例化的时候会有如下逻辑,会进行转化,转成一个函数。(这是和渲染watcher
的第一个区别)
其实对于user watcher
我们并不关心它的get
,而是它的回调,因为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
对象。
那为什么会有两个dep
对象呢?
对于defineReactive
里面的dep
对象,我们很清楚,在obj
属性的get
方法中收集当前watcher
,在触发obj
属性的set
方法的时候通知watcher
更新。
但是对于new Observer()
里面的dep
对象就有点没弄明白了。为什么这里还会有一个dep
对象呢?
其实这个dep
是给Vue.set/Vue.delete
和调用数组七个原型方法,去派发更新时使用的。
我们知道,defineReactive
里面的dep
对象其实是一个闭包属性,在这个方法里面能访问到,但是在外部是访问不到的。前面我们分析了Vue.set/Vue.delete
的原理和数组修改七个原型方法的原理,他们在最后都会有一个手动派发更新的操作。
因为闭包里面的dep
对象在外面获取不到,但是这里手动派发更新又需要用到dep
,所以就有了new Observer()
里面的dep
。Vue.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属性可以重名吗
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!
转载自:https://juejin.cn/post/7155401509797593102