Vue响应式原理

如何追踪变化

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter,每一个属性都有一个自己对应的Dep订阅器。
在对’#app’里面的子元素进行编译时,会对每一个需要获取key的位置创建一个订阅者watcher
(1)构造时将这个watcher赋值给Dep.newSub,然后读取key,触发get(),在get函数中触发当前key对应的订阅器dep的addSub事件,将这个watcher加入到key的订阅者数组中------订阅key
(2)每次key改变,就会触发set,从而触发dep的notify事件,通知该key的订阅者数组subs所有成员更新最新数据并把新值通过回调函数传回,更新视图,达到追踪变化的效果------发布消息

检测变化的注意事项

受现代 JavaScript 的限制,Vue 无法检测到对象属性的添加或删除。由于 Vue 只在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。

声明响应式属性

由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值

/* 监听器 */
class Observer {
    constructor(data) {
        this.observer(data);
    }
    /* 遍历data设置监听 */
    observer(obj) {
        if (obj && typeof obj == 'object') {
            /* 获取obj所有子属性 */
            let keys = Object.keys(obj);
            /* 遍历子属性,全部设置监听 */
            keys.forEach(key => {
                this.defineRactive(obj, key, obj[key]);
            })
        }
    }
    /* 监听 */
    defineRactive(obj, key, val) {
        /* 若子对象则继续遍历 */
        this.observer(val);
        /* 给每个值设置一个订阅器 */
        let dep = new Dep();
        /* 监听改变函数 */
        Object.defineProperty(obj, key, {
            get() {
                /* 判断是否有新订阅者 */
                Dep.newSub && dep.addSub();
                return val;
            },
            set: (newVal) => {
                /* 相等则没必要更新 */
                if (val !== newVal) {
                    /* 更新val已有的属性的数据,对新加的属性进行监听 */
                    if (typeof val == 'object') {/* 对象或数组 */
                        /* 更新内部内容 */
                        this.updateObj(val, newVal);
                    } else {
                        val = newVal;
                    }
                    /* 发布 */
                    dep.notify();
                }
            }
        })
    }
    /* 对象更新设置子数据更新 */
    updateObj(obj1, obj2) {
        if (obj1 instanceof Object || obj1 instanceof Array) {
            for (let key in obj1) {
                if (obj1[key] instanceof Object || obj1[key] instanceof Array) {
                    this.updateObj(obj1[key], obj2[key]);
                } else {
                    obj1[key] = obj2[key];
                }
            }
        } else {
            obj1 = obj2;
        }
    }
}
/* 订阅器 */
class Dep {
    constructor() {
        /* 初始化订阅者数组 */
        this.subs = [];
    }
    /* 增加订阅者 */
    addSub() {this.subs.push(Dep.newSub);}
    /* 发布 */
    notify() {
        /* 通知所有订阅者更新数据 */
        this.subs.forEach(sub => {
            sub.update();
        })
    }
}
/* 订阅者 */
class Watcher {
    constructor(vm, prop, cb) {
        this.vm = vm;
        this.prop = prop;
        this.cb = cb;
        /* 加入订阅者数组 */
        this.join();
    }
    /* 加入订阅者数组 */
    join() {
        /* 将自己设置为订阅器的新订阅者 */
        Dep.newSub = this;
        /* 读取一次数据触发get,从而间接触发addSub */
        this.oldVal = CompileUtil.getVal(this.vm, this.prop);
        /* 重置,否则会重复添加 */
        Dep.newSub = null;
    }
    /* 更新 */
    update() {
        /* 对比新旧值决定是否更新 */
        let newVal = CompileUtil.getVal(this.vm, this.prop);
        if (this.oldVal !== newVal) {
            this.oldVal = newVal;
            /* 运行回调函数 */
            this.cb(newVal);
        }
    }
}

异步更新队列

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。例如:

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })
    }
  }
})
posted @ 2020-04-01 12:40  aeipyuan  阅读(146)  评论(0编辑  收藏  举报