Vue2中的数据劫持

170 阅读1分钟

new Vue的Vue都经历了什么,我们new Vue()的构造函数中会执行this.__init(options)函数,该函数上一章中也有查看代码,方便大家观看,再次贴一下代码:

Vue.prototype._init = function (options?: Object) {
    vm._isVue = true
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    // beforeCreate 中获取不到 数据信息,包括data,props,inject等数据信息
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    // created 钩子里才能获取到 data,props,inject等数据信息
    callHook(vm, 'created')
    // 挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
}

initState(vm)中,对data() { return {} }数据进行了数据劫持。下面将对Vue中数据劫持的过程进行个mini写法,以便大家进行理解。

数据劫持流程

observe

首先执行observe函数,observe函数会判断该对象是否已经被劫持过,如果已经劫持过,那么直接返回被劫持的对象,如果没有被劫持过则返回一个Observe对象。

function observe (value) {
    if(value.__ob__) {
        return value.__ob__
    }
    const ob = new Observer(value);
    return ob;
}

Observe

Observe构造函数中会给数据增加__ob__属性,以便observe方法中能够判断数据是否已经被劫持过,然后调用defineReactive函数,进行数据劫持。


class Observer {
    constructor(value) {
        value.__ob__ = this;
        // 简化写法,默认都是对象形式,先省略数组模式
        this.walk(value);
    }

    walk(obj) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i])
        }
    }
}

defineReactive

defineReactive函数中通过Object.defineProperty函数进行数据的set/get方法劫持,当数据被使用时进行依赖收集,数据被修改时进行视图的更新。

function defineReactive (obj, key) {
    const dep = new Dep ();
    let value  = obj[key];
    Object.defineProperty(obj, key , {
        enumerable: true,
        configurable: true,
        get: function () {
            if(Dep.target) {
                dep.depend();
            }
            return value;
        },
        set: function (newVal) {
            value = newVal;
            dep.notify();
        }
    })
}

Dep

Dep用来收集Watcher对象和通知进行视图更新。

class Dep {
    constructor() {
        this.subs = [];
    }
    depend () {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }
    notify () {
        const subs = this.subs.slice();
        for (let i = 0, l = subs.length; i < l; i++) {
            console.log('更新视图啦~~~');
        }
    }
    addSub (sub) {
        this.subs.push(sub)
    }

}

Watcher

addDep方法用于将当前watcher对象收集到Dep对象中。

class Watcher {
    addDep (dep) {
        dep.addSub(this)
    }
    
}

存在的问题是如何解决的

我们知道Object.defineProperty是存在缺陷的,例如不能监听到对象里新增属性的情况;不能直接监听到对象嵌套对象的情况;针对数组push、unshift等修改数组数据的方法监听不到等问题。Vue是如何解决这些问题的?下面会一一解答:

对象中新增数据监听不到

Vue中提供了$set方法,当对象中新增属性时,可以调用该方法保证数据的更新。

嵌套对象问题

Vue中在defineReactive函数中的set/get方法中都调用的observe方法,确保对象中属性为对象这种情况能覆盖监听到。 Vue中

数组中方法问题

Vue中劫持了数组类型的__proto__,重写了[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]这些方法,以便在调用这些方法时视图能正常更新。

视图重复更新

Watcher.addDep方法中,记录已经收集过的dep对象,通过dep.id对比,防止同一个watcher对象重复收集dep对象,在通知watcher对象update视图的方法中同样通过判断watcher对象是否已经存在来防止重复更新视图。

Watcher 示例代码

addDep (dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id)
        this.newDeps.push(dep)
        if (!this.depIds.has(id)) {
            dep.addSub(this)
        }
    }
}
update() {
    queueWatcher(this)
}
    

queueWatcher方法 示例代码

const has = {};
function queueWatcher (watcher) {
    const id = watcher.id
    if (has[id] == null) {
        has[id] = true
        setTimeout(() => {
            console.log('视图更新了~~');
        }, 0)
    }
}

除了上述优化点外,Vue还做了其他优化点,大家可以去Vue源码中查看。

上述mini-vue-data代码已上传到github mock-vue2-data-proxy中,大家有需要可以自行查看。