浅谈数据响应的Vue实现及横纵对比

把数据渲染到页面,并且能够追踪数据的改变并自动更新页面的显示,是现代前端框架做的重要工作之一,下面是一段常见的Vue代码片段。




运行起来可以看到页面由空白变为展示相应字符,中间到底发生了什么?

为了便于理解需要了解的前置知识
  1. 实现于ES5Object.defineProperty (MDN)
/*
 *  obj: 目标对象
 *  prop: 需要操作的目标对象的属性名
 *  descriptor: 描述符  
 *  return value 传入对象
 */
Object.defineProperty(obj, prop, descriptor)

重点关注的描述符对象

  • enumerable,属性是否可枚举,默认 false。
  • configurable,属性是否可以被修改或者删除,默认 false。
  • get,获取属性的方法,值被读取时调用。
  • set,设置属性的方法,值被更改时调用。
  1. 发布订阅模式
    Vue的数据响应系统设计里,采用了发布-订阅设计模式,这是一种经常与观察者模式比较的模式。
浅谈数据响应的Vue实现及横纵对比_第1张图片
观察者模式

观察者模式中有两个角色,首先 观察者(Observers) 告诉 被观察者(Subject) 关注的事件,然后当事件发生后,被观察者(Subject)观察者(Observers) 进行通知。
这种模式对于复杂高的系统可能不太合适,因为两个角色的关系耦合性很高,对于跨组件跨系统的事件的处理增加了复杂度,使代码变得难以维护。

浅谈数据响应的Vue实现及横纵对比_第2张图片
发布-订阅模式

发布-订阅模式模式其实是 观察者模式的应用,有的资料里干脆说是别名,在这个模式里,多了一个中间人的角色,中间人充当了 消息中介

  • 对于 发布者(Publisher),它不需关心是谁在关注哪些事件,只需要在事件发生时通知 消息中介
  • 对于 订阅者(Subscriber) ,同样不需要关注事件在哪里发生,只需要告诉 消息中介 关注哪些事件然后坐等通知即可。

接下来是Vue响应系统的简单示例:

function observer (value) {
    if (!value || (typeof value !== 'object')) {
        return;
    }
    Object.keys(value).forEach((key) => {
        // 将对象的每个属性进行响应式处理
        defineReactive(value, key, value[key]);
    });
}
function cb (val) {
    console.log("视图更新啦~", val);
}

function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            return val;         
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            val = newVal;
            // 在对响应式对象的属性赋值时,触发视图更新
            cb(newVal);
        }
    });
}

class Vue {
    constructor(options) {
        this.$data = options.data;
        observer(this.$data);
    }
}

let o = new Vue({
    data: {
        test: "I am test."
    }
});
o.$data.test = "hello,test.";

以上简单实现了在数据更新后,对视图进行更新的功能。

下面是Vue观察者 Watcher 的简单实现,对应上图叫做 订阅者(Subscriber)

class Watcher {
    constructor () {
        /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
        Dep.target = this;
    }

    /* 更新视图的方法 */
    update () {
        console.log("视图更新啦~");
    }
}

Dep.target = null;

接下来实现Vue订阅者 Dep,我的理解是相当于上图的 消息中介 的角色,一方面收集 订阅Vue称依赖),一方面通知 订阅者 更新视图。

class Dep {
    constructor () {
        /* 用来存放Watcher对象的数组 */
        this.subs = [];
    }

    /* 在subs中添加一个Watcher对象 */
    addSub (sub) {
        this.subs.push(sub);
    }

    /* 通知所有Watcher对象更新视图 */
    notify () {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

function defineReactive (obj, key, val) {
    /* 一个Dep类对象 */
    const dep = new Dep();
    
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            /* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
            dep.addSub(Dep.target);
            return val;         
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            /* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
            dep.notify();
        }
    });
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
        new Watcher();
        /* 在这里模拟render的过程,为了触发test属性的get函数 */
        console.log('render~', this._data.test);
    }
}

之前说到发布-订阅模式有三种角色,现在就剩下 发布者 了。

实际上在一个Vue实例化时,除了会初始化一些属性和方法以外,还会进行模板的渲染,也就是render()的过程,此时会调用getter(),将data的每个属性与相关的 依赖 建立关系,所以我认为data的属性就扮演了 发布者 的角色,每当通过任何方式修改data属性,就会触发对 消息中介 进行通知,再由 消息中介 通知到相应的 订阅者


横向对比 ReactAngular

  • Angular数据响应
    不像Vue的机制,Angular没有进行精准的观察数据的变动,而是在某些用户的操作或者某些关键时间点时,对所有的数据递归检查,对于有变动的数据进行视图更新,检查可能会重复多次,因为单次的变动是可能引发连锁反应的。有时候还需要手动触发检查。
  • React数据响应
    区分于Vuemutable模式,data属性值的修改只会作用域使用该属性的地方,React采用的是immutable设计,state不能直接修改,而是需要通过setState()来实现修改,并且React会重新渲染当前组件,当然,经过一系列优化,不会对未变动的值这么做。

即将到来的 Vue 3.0 数据响应

Vue3.0即将到来,除了到来很多功能上的改进以外,对于项目结构也将降低开源参与者的门槛,会将许多功能按照模块化的思想进行解耦,可能到时源码内的数据响应这块的代码也将更清晰易懂。

此外将采用ES6引入的元编程语言特性Proxy,来重写数据响应,由于目前是不能很好的检测数组和对象的变动,所以很多地方要使用$set来实现数据响应,这个问题将在3.0版彻底解决,但也因此将不支持IE11,因为这也是个不能polyfill的特性。

本文代码参考和来源自剖析 Vue.js 内部运行机制

你可能感兴趣的:(浅谈数据响应的Vue实现及横纵对比)