Vue源码学习:响应式原理

83 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

学习Vue内容的时候总是觉得自己掌握了,实际使用起来又发现自己有很多不会的地方。内容零零散散,七零八落,学过了很容易又忘记,面试的时候无法给面试官表述清楚。学会可能并不是掌握这个技能的终点,更重要的是能清晰的传达给别人,面试的时候就能完整体现。故此,想做一个重学Vue的系列,逐个核心模块击破,脑子里能串成一张图,方便记忆。

响应式概述

响应式是Vue的数据驱动核心模块,深入学习后就能理解Vue的一些响应式API设计背景,可以去避免去写一些不利于优化的代码。

Vue 2.x 响应式

Vue2的响应式使用的核心API是 Object.defineProperty,该API可以对单个属性进行拦截,代码如下

Object.defineProperty(obj, 'name', {
        get() {
            console.log('get');
            return this.name
        },
        set(newVal) {
            console.log('set');
            this.name = newVal
        }
})

当obj对象的name属性被读取或者修改时就会触发get、set,我们就可以在get/set发生时做一些特殊操作。比如name被读取时做一个读取点收集,修改时更新读取源(视图)。

为了方便使用,我们稍加封装一下

const reactive = (obj, key, val) => {
    Object.defineProperty(obj, key, {
        get() {
            console.log('get', key);
            return val
        },
        set(newVal) {
            console.log('set', key);
            val = newVal
        }
    })
}

这样就封装完成一个可拦截单个属性的reactive的函数,实现对象拦截时,需要遍历对象将逐个属性传入该函数,如

const Observer = (data) => {
    if (!data || typeof data !== 'object') return
    if (Array.isArray(data)) {
        // arrat
    } else {
        // object
        for (const key in data) reactive(data, key, data[key])
    }
}

实现一个Dep类,用于收集管理依赖,当触发get时收集,触发set是通知更新

class Dep {
    constructor() {
        this.deps = []
    }
    addDep(dep) {
        this.deps.push(dep)
    }
    notify() {
        this.deps.forEach(w => {
            w.run()
        })
    }
}

在实现一个Watcher类,用于管理更新函数,如让dom更新等操作

class Watcher {
    constructor(vm, updater) {
        this.vm = vm
        this.getter = updater
​
        this.get()
    }
    get() {
        Dep.target = this
        this.getter()
        Dep.target = null
    }
    run() {
        this.getter()
    }
}

将实现的两个类使用到reactive函数中

const reactive = (obj, key, val) => {
    const dep = new Dep()
​
    Observer(val)
    Object.defineProperty(obj, key, {
        get() {
            console.log('get', key);
            // update 视图更新函数
            new Watcher(this, update)
            Dep.target && dep.addDep(Dep.target)
            return val
        },
        set(newVal) {
            console.log('set', key);
            Observer(newVal)
            val = newVal
            dep.notify()
        }
    })
}

总结

Vue2响应式使用了Object.defineProperty,该APi只能对当个属性进行拦截,对象做响应式时需要对整个对象进行遍历递归,因此初始化响应式时需要耗费较多的性能,并且还存在诸多弊端,如无法灵活新增对象属性,对数组的支持度不够等。