Vue 主要包括三大部分:响应式、模板编译和虚拟 DOM。
MVVM 模型
MVVM
是 Model-View-ViewModel 的简写。MVVM 用数据驱动视图。传统组件(如 asp、jsp、php)只是静态渲染,更新时还要依赖于操作 DOM。传统组件遵循 MVC 模型,即模型(Model)-视图(View)-控制器(Controller)。MVC 模式下,后端使用数据(Model部分)填充 HTML 模板,前端(View部分)访问不同的路由时展示不同的视图,这些视图都是后端已经渲染好的 HTML 文档。
而 MVVM
不再直接操作 DOM,而是操作数据,数据的变化会引起页面的变化。MVVM 更专注于数据(业务逻辑)。在前端页面中,把 Model 用纯 JavaScript 对象表示,View 负责显示,两者做到了最大限度的分离。把 Model 和 View 关联起来的就是 ViewModel。View Model 负责把 Model 的数据同步到 View 显示出来,还负责把 View 的修改同步回 Model。
在 Vue 中,View 部分相当于我们书写的 JSX;而 Model 部分相当于 Vue 当中的 data
;View Model
在 Vue 中体现在事件(methods)、计算(computed)等部分。
监听数据变化
在 Vue 中,数据的变化会引起视图的更新。如何监听数据的变化?
Vue 在初始化数据时会给 data 中的属性使用 Object.defineProperty 方法重新定义属性,把属性转化成 getter/setter。例如:
function updateView(){
// 这个函数就相当于页面更新的函数
console.log('页面更新!');
}
function observer(target){
if(typeof target !== 'object' || target === null) {
return target; // 不是对象就直接返回,这也是该函数递归的出口
}
for(let key in target){ // 如果传入的是一个对象
defineReactive(target, key, target[key]);
}
}
function defineReactive(target, key, value){
observer(value); // value 也可能是一个对象,这时就要递归遍历
Object.defineProperty(target, key, {
get(){
return value;
},
set(newVal){
// 当值改变后都会调用 updateView 函数
if(newVal !== value){
// 改变后的新值可能是一个对象,就要递归遍历这个对象
// 让这个对象的所有属性都是可监听的
observer(newVal);
value = newVal;
updateView();
}
}
});
}
测试如下:
var obj = {
count: 1,
person: {
name: 'Ming'
}
}
observer(obj);
obj.count = 2; // 页面更新!
obj.person.name = 'Li'; // 页面更新!
需要注意的是,对监听的对象新增或者删除属性,是监听不到的,updateView 不会执行,例如:
obj.x = 123;
delete obj.count;
深度监听使用了递归,计算量很大。为了优化这些缺陷,Vue3.0 使用 Proxy
取代了 defineProperty 方法来实现数据监听。
如何监听数组?
上面的代码中使用 defineProperty 实现了对象数据的监听,在 Vue 中监听数组变化是扩展数组原型的方式实现的。在数组中,push
、pop
、shift
、unshift
、splice
、reverse
等方法会改变原数组。这些方法在改变原数组后需要触发更新,就可以这样实现:
const oldAryProperty = Array.prototype;
// 这样做不会影响原数组的原型上的方法。
// aryProto.__proto__ === oldAryProperty
const aryProto = Object.create(oldAryProperty);
['pop','push','shift','unshift','splice','reverse'].forEach(method => {
aryProto[method] = function(){
updateView();
oldAryProperty[method].call(this, ...arguments);
}
});
function observer(target){
if(Array.isArray(target)){
// 是数组时,就改变数组实例的原型
// 调用数组方法时,就会首先查找 aryProto 中的方法
// 找不到时才到 原数组的原型上查找
target.__proto__ = aryProto;
return target;
}
// ...
}
Vue3 中的响应式
defineProperty
有缺陷,深度监听时是一次性把对象递归遍历完成,如果 data 层级太深,递归计算次数会多,消耗时间。Vue3 使用 ES6 中的 Proxy
代替了 defineProperty
来实现响应式。
Vue3.0 中可以使用 reactive
函数将一个普通对象包装成可监听的数据。它的大致代码如下:
const isObject = (target) => typeof target === 'object' && target !== null;
const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key);
function get(target, key, receiver) {
// result 是取到的值
const result = Reflect.get(target, key, receiver);
console.log('对这个对象取值', target, key);
// 如果获取的值还是一个对象,那就也进行监听
if (isObject(result)) {
return reactive(result);
}
return result;
}
function set(target, key, value, receiver) {
// receiver 是 proxy 实例本身
// hadKey 用于判断 key 是不是 target 上的属性
const hadKey = hasOwn(target, key);
const oldValue = target[key];
// 设置新的值
const result = Reflect.set(target, key, value, receiver);
if (!hadKey) {
console.log('新增操作', target, key);
} else if (value !== oldValue) {
// 修改操作,老值与新值不相等时,才更新
console.log('更新操作', target, key);
}
// 之没有变化,就什么都不操作
return result;
}
function deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
console.log('delete property', key);
return result;
}
function reactive(target) {
// 创建一个响应式对象,目标对象可能不一定是数组或对象,可能还有 set map
return createReactiveObject(target, {
get,
set,
deleteProperty
});
}
function createReactiveObject(target, baseHeadler) {
if (!isObject(target)) { // 不是对象直接返回即可
return target;
}
const observed = new Proxy(target, baseHeadler);
return observed;
}
使用 Proxy
不仅在更新时可以监听到数据变化,在删除和新增时也能监听到,而 defineProperty
是不行的。而且 Proxy
不是一次性递归遍历对象,而是在取值时才做递归。使用 Proxy 实现数据的监听更加优雅。在上面代码中,还使用了 Reflect
,被称为“反射”,它也是 ES6 中的 API,它与 Proxy
的能力一一对应。Reflect
的出现使得 JavaScript 这门语音更加规范化和函数式,它内部定义了许多可以代替 Object 上的工具函数的方法。关于 Proxy
和 Reflect
的更多用法可以参考 MDN:
Proxy
很好用,但是好的东西总是兼容性不太行。IE
浏览器是不支持的,而且这个 API 不能 polyfill。
computed 和 watch
在 Vue 中,初始化数据时,会给 data 中的属性使用 Object.defineProperty 重新定义所有属性,这个方法可以让数据的获取或设置都增加一个拦截的功能,我们可以在获取(getter)或者更新(setter)的时候增加一些逻辑,这些逻辑称为“依赖收集”。当数据变化时,可以通知收集的依赖去更新,而这些收集的东西被称为 watcher
。比如在页面初始渲染的时候,会对数据取值,取值的时候,会收集一些依赖(watcher),把这些依赖先存起来,当数据变化时通知对应的 watcher 去更新视图(Vue 中调用 notify
函数通知)。 在 Vue 中,可以使用 computed
和 watch
属性添加依赖收集逻辑,这两个属性都可以观察和响应 Vue 实例上的数据变动。相比于 computed
,watch
更有通用性,它可以在数据变化时执行异步或开销较大的操作。如果监听的数据是对象,watch
中可以传入 deep: true
配置项,表示深度监听。相比于 computed
,watch
不具备缓存能力。computed
在数据没有发生变化时不会重新计算。
watch: {
'obj': {
// 当数据变化时就会调用 handler 函数
handler(val, oldVal){
// ...
},
deep: true
}
}
如下图所示: