专栏名称: 前端外刊评论
最新、最前沿的前端资讯,最有深入、最干前端相关的技术译文。
目录
相关文章推荐
前端早读课  ·  【第3453期】圈复杂度在转转前端质量体系中的应用 ·  17 小时前  
奇舞精选  ·  从 DeepSeek 看25年前端的一个小趋势 ·  昨天  
奇舞精选  ·  从 DeepSeek 看25年前端的一个小趋势 ·  昨天  
前端早读课  ·  【第3452期】React 开发中使用开闭原则 ·  昨天  
51好读  ›  专栏  ›  前端外刊评论

Vue.set 的副作用

前端外刊评论  · 公众号  · 前端  · 2020-03-18 08:30

正文

Vue虽然用挺久了,还是会踩到坑,来看下面这段很简单的🌰: 点击a和b按钮,下面代码会提示什么?
<html>
<head>
<meta charset="utf-8">
<script src="https://cdn.staticfile.org/vue/2.5.17/vue.min.js">script>
head>
<body>
<div id="app">
<p>{{ JSON.stringify(this.testObj) }}p>
<button @click="set('a')">设置testObj属性abutton>
<button @click="set('b')">设置testObj属性bbutton>
div>

<script>
new Vue({
el: '#app',
data: {
testObj: {},
},
watch: {
'testObj.a'() {
alert('a')
},
'testObj.b'() {
alert('b')
},
},
methods: {
set(val) {
Vue.set(this.testObj, val, {});
}
},
})
script>
body>
html>

答案是:

点a的时候alert a,点b的时候alert a,接着alert b。

如果再接着点a,点b,提示什么?

答案是:

点a的时候alert a,点b的时候alert b。

我们把代码做一个很小的改动:把Vue.set的值由对象改为true。这时候点击a和b按钮,下面代码会提示什么?

<html>
<head>
<meta charset="utf-8">
<script src="https://cdn.staticfile.org/vue/2.5.17/vue.min.js">script>
head>
<body>
<div id="app">
<p>{{ JSON.stringify(this.testObj) }}p>
<button @click="set('a')">设置testObj属性abutton>
<button @click="set('b')">设置testObj属性bbutton>
div>

<script>
new Vue({
el: '#app',
data: {
testObj: {},
},
watch: {
'testObj.a'() {
alert('a')
},
'testObj.b'() {
alert('b')
},
},
methods: {
set(val) {
Vue.set(this.testObj, val, true);
}
},
})
script>
body>
html>

答案是:

点a的时候alert a,点b的时候alert b。

如果再接着点a,点b,提示什么?

答案是:

没有提示。

先总结一下发现的现象:用Vue.set为对象o添加属性,如果添加的属性是一个对象,那么o的所有属性会被触发响应。

是不是不明白?且请听我讲解一下。

要回答上面这些问题,我们首先需要理解一下Vue的响应式原理。 从Vue官网这幅图上我们可以看出:当我们访问data里某个数据属性p时,会通过getter将这个属性对应的Watcher加入该属性的依赖列表;当我们修改属性p的值时,通过setter通知p依赖的Watcher触发相应的回调函数,从而让虚拟节点重新渲染。

所以响不响应关键是看依赖列表有没有这个属性的watcher。

为了把依赖列表和实际的数据结构联系起来,我画出了vue响应式的主要数据结构,箭头表示它们之间的包含关系: Vue里的依赖就是一个Dep对象,它内部有一个subs数组,这个数组里每个元素都是一个Watcher,分别对应对象的每个属性。Dep对象里的这个subs数组就是依赖列表。

从图中我们可以看到这个Dep对象来自于__ob__对象的dep属性,这个__ob__对象又是怎么来的呢?这就是我们new Vue对象时候Vue初始化做的工作了。Vue初始化最重要的工作就是让对象的每个属性成为响应式,具体则是通过observe函数对每个属性调用下面的defineReactive来完成的:

/**
* Define a reactive property on an Object.
*/

function defineReactive (
obj,
key,
val,
customSetter,
shallow
)
{
var dep = new Dep();

var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}

// cater for pre-defined getter/setters
var getter = property && property.get;
if (!getter && arguments.length === 2) {
val = obj[key];
}
var setter = property && property.set;

var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var 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) {
var 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();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}

让一个对象成为响应式其实就是给对象的所有属性加上getter和setter(defineReactive做的工作),然后在对象里加__ob__属性(observe做的工作),因为__ob__里包含了对象的依赖列表,所以这个对象就可以响应数据变化。

可以看到defineReactive里也调用了observe,所以让一个对象成为响应式这个动作是递归的。即如果这个对象的属性又是一个对象,那么属性对象也会成为响应式。就是说这个属性对象也会加__ob__然后所有属性加上getter和setter。

刚才说有没有响应看“依赖列表有没有这个属性的watcher”,但是实际上, ob 只存在属性所在的对象上,所以依赖列表是在对象上的依赖列表,通过依赖列表里Watcher的expression关联到对应属性(见图2)。说以准确的说:有没有响应应该是看“对象的依赖列表里有没有属性的watcher”。

注意我们在data里只定义了testObj空对象,testObj并没有任何属性,所以testObj的依赖列表一开始是空的。

但是因为代码有定义Vue对象的watch,初始化代码会对每个watch属性新建watcher,并添加到testObj的依赖队列__ob__.dep.subs里。这里的添加方法非常巧妙:新建watcher时候会一层层访问watch的属性。比如watch 'testObj.a',vue会先访问testObj,再访问testObj.a。因为testObj已经初始化成响应式的,访问testObj时会调用defineReactive里定义的getter,getter又会调用dep.depend()从而把testObj.a对应的watcher加到依赖队列__ob__.dep.subs里。于是新建watcher的同时完成了把watcher自动添加到对应对象的依赖列表这个动作。

小结一下:Vue对象初始化时会给data里对象的所有属性加上getter和setter,添加__ob__属性,并把watch属性对应的watcher放到__ob__.dep.subs依赖列表里。

所以经过初始化,testObj的依赖列表里已经有了属性a和b对应的watcher。

有了以上基础知识我们再来看Vue.set也就是下面的set函数做了些什么。

/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/

function set (target, key, val) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
var ob = (target).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
);
return val
}
if (!ob) {
target[key] = val;
return val
}
defineReactive(ob.value, key, val);
ob.dep.notify();
return val
}

我们关心的主要就最后这两句:defineReactive(ob.value, key, val); 和ob.dep.notify();。

defineReactive的作用就是让一个对象属性成为响应式。ob.dep.notify()则是通知对象依赖列表里面所有的watcher:数据变化了,看看你是不是要做点啥?具体做什么就是图2 Watcher里面的cb。当我们在vue 里面写了 watch: { p: function(oldValue, newValue) {} } 时候我们就是为p的watcher添加了cb。

所以Vue.set实际上就做了这两件事:

  1. 把属性变成响应式 。
  2. 通知对象依赖列表里所有watcher数据发生变化。

那么问题来了,既然依赖列表一直包含a和b的watcher,那应该每次Vue.set时候,a和b的cb都应该被调用,为什么结果不是这样呢?奥妙就藏在下面的watcher的run函数里。

/**
* Scheduler job interface.
* Will be called by the scheduler.
*/

Watcher.prototype.run = function run () {
if (this.active) {
var 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
var 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 \""






请到「今天看啥」查看全文