深入Vue内部原理(一):响应式原理

Vue 主要包括三大部分:响应式、模板编译和虚拟 DOM。

MVVM 模型

MVVM 是 Model-View-ViewModel 的简写。MVVM 用数据驱动视图。传统组件(如 asp、jsp、php)只是静态渲染,更新时还要依赖于操作 DOM。传统组件遵循 MVC 模型,即模型(Model)-视图(View)-控制器(Controller)。MVC 模式下,后端使用数据(Model部分)填充 HTML 模板,前端(View部分)访问不同的路由时展示不同的视图,这些视图都是后端已经渲染好的 HTML 文档。
MVC
MVVM 不再直接操作 DOM,而是操作数据,数据的变化会引起页面的变化。MVVM 更专注于数据(业务逻辑)。在前端页面中,把 Model 用纯 JavaScript 对象表示,View 负责显示,两者做到了最大限度的分离。把 Model 和 View 关联起来的就是 ViewModel。View Model 负责把 Model 的数据同步到 View 显示出来,还负责把 View 的修改同步回 Model。
MVVM
在 Vue 中,View 部分相当于我们书写的 JSX;而 Model 部分相当于 Vue 当中的 dataView 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 中监听数组变化是扩展数组原型的方式实现的。在数组中,pushpopshiftunshiftsplicereverse 等方法会改变原数组。这些方法在改变原数组后需要触发更新,就可以这样实现:

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 上的工具函数的方法。关于 ProxyReflect 的更多用法可以参考 MDN:

Proxy
Reflect

Proxy 很好用,但是好的东西总是兼容性不太行。IE 浏览器是不支持的,而且这个 API 不能 polyfill。

computed 和 watch

在 Vue 中,初始化数据时,会给 data 中的属性使用 Object.defineProperty 重新定义所有属性,这个方法可以让数据的获取或设置都增加一个拦截的功能,我们可以在获取(getter)或者更新(setter)的时候增加一些逻辑,这些逻辑称为“依赖收集”。当数据变化时,可以通知收集的依赖去更新,而这些收集的东西被称为 watcher。比如在页面初始渲染的时候,会对数据取值,取值的时候,会收集一些依赖(watcher),把这些依赖先存起来,当数据变化时通知对应的 watcher 去更新视图(Vue 中调用 notify 函数通知)。 在 Vue 中,可以使用 computedwatch 属性添加依赖收集逻辑,这两个属性都可以观察和响应 Vue 实例上的数据变动。相比于 computedwatch 更有通用性,它可以在数据变化时执行异步或开销较大的操作。如果监听的数据是对象,watch 中可以传入 deep: true 配置项,表示深度监听。相比于 computedwatch 不具备缓存能力。computed 在数据没有发生变化时不会重新计算。

watch: {
    'obj': {
        // 当数据变化时就会调用 handler 函数
        handler(val, oldVal){
            // ...
        },
        deep: true
    }
}

如下图所示:
响应式原理

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值