由 Vue 中三个常见问题引发的深度思考
Posted 前端大全
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了由 Vue 中三个常见问题引发的深度思考相关的知识,希望对你有一定的参考价值。
(给前端大全加星标,提升前端技能)
前言
为什么
data
要写成函数,而不允许写成对象?Vue 中常说的数据劫持到底是什么?
Vue 实例中数组改变
length
或下标直接赋值什么不能更新视图?
Tips
如果你已经掌握了这三个问题的原因和原理;
如果你觉得你不需要掌握原理会用即可;
抖个机灵——请看本文最后一行。
接下来,我们就对这三个问题一一解答。
问题一
为什么 `data` 要写成函数,而不允许写成对象?
想要理解这个问题,我们首先要知道以下三点。
注意要点
第一点无可厚非,data
属性附着于 Vue 实例上。
基本类型赋值
var a = 10;
var b = 10;
var c = a;
console.log(a === b); // true
a ++ ;
console.log(a); // 11
console.log(c); // 10
这段代码分别给 a、b 赋值 10,a 和 b 是全等的。然后用 a 来初始化 c,那么 c 的值也是 10。但 c 中的 10 与 a 中的是完全独立的,该值只是 a 中的值的一个副本,此后, 这两个变量可以参加任何操作而相互不受影响。具体位置如下示意图。
引用类型赋值
var a = {};
var b = {};
var c = a;
console.log(a === b); // false
a.name = 'Marry';
a.say = () => console.log('Hi Marry!');
console.log(c.name); // 'Marry'
console.log(c.say()); // 'Hi Marry!'
至于第三点,大多数有 JS 基础的同学应该都能理解,每个函数都有自己的作用域。
以上是对三个注意点的说明,那么接下来我们就以两个例子解释问题一:为什么 data
要写成函数,而不允许写成对象?
data 为对象示例代码
function MyCompnent() {}
MyCompnent.prototype.data = {
age: 12
};
var JackMa = new MyCompnent();
var PonyMa = new MyCompnent();
console.log(JackMa.data.age === PonyMa.data.age); // true
JackMa.data.age = 13;
console.log('JackMa ' + JackMa.data.age + '岁;' + 'PonyMa ' + PonyMa.data.age + '岁');
// JackMa 13岁;PonyMa 13岁
上面的示例中,我们创建一个构造函数 MyCompnent,它充当的角色相当于 Vue,在他的原型属性上声明一个data
属性,其实也相当于 Vue.$data
。接着声明两个实例,改变其中一个实例,另外一个实例也会跟着改变,这个道理其实和引用类型赋值大同小异。
data 为函数的示例代码
function MyCompnent() {
this.data = this.data();
}
MyCompnent.prototype.data = function() {
return {
age: 12
}
};
var JackMa = new MyCompnent();
var PonyMa = new MyCompnent();
console.log(JackMa.data.age === PonyMa.data.age); // true
JackMa.data = {age: 13};
console.log('JackMa ' + JackMa.data.age + '岁;' + 'PonyMa ' + PonyMa.data.age + '岁');
// JackMa 13岁;PonyMa 12岁
上述代码模拟了 Vue 实例上的data
为函数的时候,如果改变一个实例的data
属性的值,那么不会影响到另外一个实例上的data
的值。
Tips
面试过程中经常会被问到 JS 数据类型问题,如果你只回答基本类型和引用类型可能你只能得到一半分数,但是如果你能把存储位置等要点回答出来并且举例说明,想必是很加分的。
小结
问题二
Vue 中常说的数据劫持到底是什么?
相信大多数用过或者了解 Vue 的同学都听过数据劫持,进一步问为什么可能你也能答出一二,例如 getter、setter 之类。今天我就系统地和你说一下数据劫持之美。首先我们先看一看下图。
上图完整的描述了 Vue 运行的机制,首先数据发生改变,就会经过 Data
处理,然后Dep
会发出通知(notify
),告诉 Watcher
有数据发生了变化,接着 Watcher
会传达给渲染函数跟他说有数据变化了,可以渲染视图了(数据驱动视图),进而渲染函数执行render
方法去更新 VNODE
,也就是我们说的虚拟DOM,最后虚拟DOM根据最优算法,去局部更新需要渲染的视图。这里的 Data
就做了我们今天要说的事——数据劫持。
想要更深入地理解如何劫持,我们就需要看源码实现。
Observer
/**
* Vue中的每一个变量都是由 Observer 构造函数生成的。
* 细心的你可能会发现,你打印出来任何一个Vue上的引用类型属性,后面都有 __ob__: Observer 的字样。
*/
var Observer = function Observer (value) {
this.value = value;
// 这里把发布者 Dep 注册了
this.dep = new Dep();
// ···
// 此处调用 walk
this.walk(value);
};
/**
* 此处会将 obj 里面的每一个值用 defineReactive$$1 处理,而它就是今晚的主角。
*/
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
};
数据劫持
/**
* 这个函数就是数据劫持的根据地,里面为对象重写了 get 和 set 方法以及固有属性 enumerable 等。
*
*/
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
// ···
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// ···
return value
},
set: function reactiveSetter (newVal) {
// ···
// 此处最为关键,这个函数的主要作用就是通过 notify 告诉 Watcher 有数据变化了。
dep.notify();
}
});
}
Dep
/**
* subs 是所有 Watcher 的收集器,类型为数组;notify 实则是调用了每个Watcher的 update方法 。
*/
Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
// ···
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
Watcher
/**
* 更新视图的最直观的方法就是 Watcher 上的 update 方法 , Dep subs 反复调用
* 这里最终都是调用 run 方法。
*/
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
/**
* run 方法内的 cb 方法建立 Watcher 和 VNode 之间的关联关系,从而引发视图的更新。
*/
Watcher.prototype.run = function run () {
// ···
if (this.user) {
// ···
this.cb.call(this.vm, value, oldValue);
// ···
} else {
this.cb.call(this.vm, value, oldValue);
}
// ···
};
小结
至此,我们不但了解了数据劫持的的原理,还知道了谁去劫持,劫持过后做了什么,是谁引发的视图更新等等。是不是对 Vue 的运行机制更明白了一些呢?
问题三
Vue实例中数组改变 `length` 或下标直接赋值什么不能更新视图?
情景再现
上图示例中,为方便调试在 mounted 周期内执行windows.vm = this;
。week
包含周一到周五五个元素,我们尝试改变 week
的 length
为 3 以及给它下标为 4 的元素赋值一个周八
,结果都没有生效。那怎么可以生效呢?请看下图。
主要是因为我们调用了一个数组的内置方法 push
,如果你愿意尝试,你会发现调用数组的 slice
方法是不行的。只要是因为 Vue 提取了数组的可以改变原数组的原生方法,进行了再加工。只有经过 Vue 处理过的方法才有更新视图的能力。下面我将从内置方法和源码的角度给大家说明这个结论。
内置方法
上图中我们展开 week 数组,发现在第一个 __proto__
里面内置了 pop
、push
等多个数组的方法;在第二个 __proto__
不但有上面几种还有更多其他的方法。由此可见,Vue 是对数组的 Api 进行了劫持。
源码解析
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/**
* Intercept mutating methods and emit events
* 此方法主要作用就是遍历数组局部方法,调用的同时去调用 dep 的 notify 通知 Watcher 进而更新视图
*/
methodsToPatch.forEach(function (method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
if (inserted) { ob.observeArray(inserted); }
// 这一步至关重要
ob.dep.notify();
return result
});
});
小结
到这里我们知道了,Vue 劫持了数组可以改变原数组的 Api,使得每次调用都会执行 dep.notify()
方法进而去更新视图。
结束语
分享的时光永远这么短暂,但我还要对你说:工作中经常遇到此类问题,希望我们多问自己一个为什么?去研究它到底是怎么实现的,掌握设计理念,学习设计思想,而不是仅限于知道如何使用。这样自己才会有更多的成长!
【本文作者】
推荐阅读
(点击标题可跳转阅读)
觉得本文对你有帮助?请分享给更多人
关注「前端大全」加星标,提升前端技能
好文章,我在看❤️
以上是关于由 Vue 中三个常见问题引发的深度思考的主要内容,如果未能解决你的问题,请参考以下文章