Vue变化监听-对象篇

270 阅读2分钟

Vue.js最独特的特性之一是看起来并不显眼的响应式系统。数据模型仅仅是普通的JavaScript对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单、直接。不过理解其工作原理同样重要,这样你可以回避一些常见的问题。

1. 背景

我们都知道JavaScript要监听一个对象的变化可以使用Object.definePropertyProxy实现。最初考虑到ES6语法在浏览器的支持度不理想,所以Vue2使用的是Object.defineProperty来实现,由于IE8及以下版本不支持该API,这也是Vue不支持IE8及以下的原因。

不熟悉Object.defineProperty的童鞋可以移步MDN学习。


2. 基础版

  1. 实现一个监听对象属性变化的函数:
function defineReactive(obj, key, value, cb) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            return value;
        },
        set(newVal) {
            if(value === newVal) return;
            value = newVal;
            cb();  // 执行观察者收到更新消息的回调
        }
    })
}
  1. 将对象的每个属性都监听起来
function observe(obj, cb) {
    Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key], cb));
}
  1. 实现一个Vue
class Vue {
    constructor(options) {
        this._data = options.data;
        observe(this._data, options.render);
    }
}

使用:

const app = new Vue({
    el: '#app',
    data: {
        name: 'tom',
        age: 11
    },
    render() {
        console.log('render');
    }
});

app._data.name = 'mike';  // render

可以看到,app这个实例里面的data已经被监听了起来,当我们改变data的时候,会触发相应的回调函数。

但是上面改变data的写法有些累,我们可以写一个代理函数,然后改变数据就可以写成app.name='mike'

function _proxy(data) {
    const that = this;
    Object.keys(data).forEach(key => Object.defineProperty(that, key, {
        configurable: true,
        enumerable: true,
        get() {
            return that._data[key];
        },
        set(newVal) {
            that._data[key] = newVal;
        }
    }))
}

改写一下Vue类:

class Vue {
    constructor(options) {
        this._data = options.data;
        _proxy.call(this, this._data);  // 新增
        observe(this._data, options.render);
    }
}

这样就可以直接写成app.name='mike'了。

_proxy函数实际上是把data里面的属性加在app这个对象上,并监听加上的属性。执行app.name='mike'的时候,就会触发_proxy里面的setset里面实际上是执行app._data.name='mike'

升级版

基础版实际上只能够监听对象的一级属性,想要监听对象更深级的属性,需要使用递归的方法改写代码:

function observe(obj, cb) {
    Object.keys(obj).forEach(key => {
        const value = obj[key];
        // 新增:属性值是对象,就递归监听
        if(Object.prototype.toString.call(value) === '[object Object]') {
            observe(value, cb);
        }
        defineReactive(obj, key, value, cb);
    });
}

function defineReactive(obj, key, value, cb) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            // 收集依赖。。。
            return value;
        },
        set(newVal) {
            if(value === newVal) return;
            value = newVal;
            cb();  // 执行订阅者收到更新消息的回调
        }
    })
}

class Vue {
    constructor(options) {
        this._data = options.data;
        _proxy.call(this, this._data);
        observe(this._data, options.render);
    }
}

const app = new Vue({
    el: '#app',
    data: {
        name: 'tom',
        age: 11,
        otherInfo: {
            sex: 'boy'
        }
    },
    render() {
        console.log('render');
    }
});

function _proxy(data) {
    const that = this;
    Object.keys(data).forEach(key => Object.defineProperty(that, key, {
        configurable: true,
        enumerable: true,
        get() {
            return that._data[key];
        },
        set(newVal) {
            that._data[key] = newVal;
        }
    }))
}

app.otherInfo.sex = 'girl';  // render