[vue源码笔记]vue3.0-Proxy变化监听

986 阅读6分钟

前言:前文[笔记]vue2.x-Object变化监听已基本介绍了在vue2.x版本中vue实现响应式或者数据驱动的原理,接下来就看一下在3.0版本带来了哪些变化

侦听数据变化的方法

通过前文分析我们直到在vue2.0版本中使用Object.defineProperty来监听对象属性的读写,但是它有一些难以克服的缺陷:

  1. 无法监听对象属性的增删
  2. 需要遍历对象的所有属性进行设置,对于嵌套层级较深的对象性能较差
  3. 对数组的监听存在缺陷 基于以上认识我们需要找出一些更好的解决方案,正好js的新特性Proxy出现在了我们的视野,它能很好得解决Object.defineProperty在此场景中的缺陷
var obj = {name: 'jack'}
var person = new Proxy(obj, {
    get(target, key) {
        console.log('读取')
        return Reflect.get(target, key)
    },
    set(target, key, value) {
        console.log('更新')
        return Reflect.set(target, key, value)
    },
    deleteProperty(target, key) {
        console.log('删除')
        return Reflect.deleteProperty(target, key)
    },
    defineProperty(target, key, descriptor) {
        console.log('设置')
        return Reflect.defineProperty(target, key, descriptor)
    }
})

person.name // '读取'
person.name // '更新'
delete.name // '删除'
Object.defineProperty(person, 'age', 18) // '设置'
person.tall = 185 // '更新'

以上示例只是Proxy提供功能的冰山一角,更多的内容可以自行google,不过已知上面的特性就足够我们理解vue3.0响应式/数据驱动的重构

响应化数据

如果要实现数据变更驱动视图更新必须首先能监听到数据的变更,vue3.0相对于2.x版本在响应化数据方面有很大变化,2.x版本只能对引用类型的数据进行监听,但在3.0版本这方面做了提升

响应化引用数据类型

function createReactiveObject(target) {
    const handlers = {
        get(target, key, receiver) {
            console.log('读取')
        },
        set(target, key, value) {
            console.log('写入')
        },
        has(target, key) {
            console.log('查找')
        },
        deleteProperty(target, key) {
            console.log('删除')
        },
    }
    const proxy = new Proxy(target, handlers)
    return proxy
}
function reactive (target) {
    return createReactiveObject(target)
}

响应化基础数据类型

function isRef(r) {
  return r && r.__v_isRef === true
}
function ref(value) {
    return createRef(value)
}
class RefImpl {
    constructor(rawValue) {
        this._value = rawValue // 初始值
        this.__v_isRef = true // 标识为一个ref类型对象
    }
    get value() {
        console.log('ref读取')
    }
    set value(newValue) {
        console.log('ref写入')
    }
}
function createRef(rawValue) {
    // 如果已经处理过了,直接返回
    if (isRef(rawValue) {
        return rawValue
    }
    return new RefImpl(rawValue)
}

上面我们基于Proxy实现了对引用数据类型的响应化,以及扩展实现了对基础数据类型的响应化 对基础数据类型实际上我们是基于改变量创建了一个对象实例,该对象实例包含get valueset value,要使用该变量的时候需要访问其value属性

依赖收集

同2.x版本,前面我们初步实现了对数据的读写监听,当订阅者读了数据就要记录订阅者和数据之间的依赖关系以实现数据变更后通知到订阅者进行更新

const targetMap = new WeakMap() // 保存所有的依赖
function track(target, type, key) {
    if (!activeEffect) return
    let depsMap = targetMap.get(target)
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
        depsMap.set(key, (dep = new Set()))
    }
    if (!dep.has(activeEffect)) {
        dep.add(activeEffect)
        activeEffect.deps.push(dep)
    }
}

3.0版本实现的依赖收集实际上是在全局扩展了WeakMap类型的变量targetMap用以保存所有的订阅者-依赖对应关系 具体为:

targetMap = {
    target: {
        key: [effect]
    }
}

target表示响应数据 key表示响应数据的key effect表示订阅者 当数据被引用后将触发Proxy get,在get中执行track进行依赖收集

变更通知

现在已经实现了数据响应,并且对数据发生引用后触发依赖收集将订阅者和依赖之间建立了联系,接下来就要实现当数据发生变更通知到订阅者进行更新

function trigger(target, type, key, newValue) {
    const effects = new Set()
    const depsMap = targetMap.get(target)
    if (!depsMap) return
    function add(effectsToAdd) {
        effectsToAdd.forEach(effect => {
            if (effect !== activeEffect) {
                effects.add(effect)
            }
        })
    }
    if (!depsMap.get(key)) return
    add(depsMap.get(key))
    function run(effect) {
        effect()
    }
    effects.forEach(run)
}

找到数据对应的订阅者,逐一通知执行

订阅者

订阅者的画像:

  1. 订阅者要包含deps列表,用以保存依赖项
  2. 订阅者在执行之前要把activeEffect置为自己,以便依赖收集
let uid = 0
function cleanup(effect) {
    const deps = effect.deps
    for (let i = 0; i < deps.length; i++) {
        deps[i].delete(effect)
    }
    deps.length = 0
}
function createReactiveEffect(fn) {
    const effect = function reactiveEffect() {
        activeEffect = effect
        cleanup(effect) // 每次执行订阅者方法之前都先将所有的依赖关系清空,重新进行依赖收集
        try {
            fn() // 订阅者方法会引用数据,从而触发被引用数据的getter进行依赖收集
        }
        finally {
            activeEffect = null // 完成依赖收集后将activeEffect置为null
        }
    }
    effect.id = uid++
    effect.deps = []
    return effect
}

具体过程参照代码注释

完整代码

在放完整代码前先来一个整个流程得总结

  1. 使用Proxy响应化数据,如果数据为基础数据类型,则创建辅助对象RefImpl,后续要访问其value属性
  2. 基于订阅者方法fn创建effect函数,在此函数中订阅者方法fn执行前将首先清理订阅者的依赖,同时将activeEffect置为当前effect函数,等待依赖收集
  3. effect函数执行,订阅者引用数据,将触发数据对应的getter,执行trace函数,在该函数中进行依赖收集,依赖收集其实就是建立依赖数据与订阅者之间的关系图谱具体看trace实现
  4. 当数据被重新赋值发生变更将触发setter,执行trigger函数通知数据对应的所有订阅者进行更新
  5. 重复步骤3进行订阅者和依赖的依赖更新 完整代码如下:
window.activeEffect = null
function createReactiveObject(target) {
    const handlers = {
        get(target, key, receiver) {
            console.log('读取,开始依赖收集')
            track(target, 'get', key)
            return Reflect.get(target, key)
        },
        set(target, key, value) {
            Reflect.set(target, key, value)
            console.log('写入,开始变更通知')
            trigger(target, 'set', key, value)
        },
    }
    const proxy = new Proxy(target, handlers)
    return proxy;
}
function reactive (target) {
    return createReactiveObject(target)
}

// 响应化基础类型
function isRef(r) {
  return r && r.__v_isRef === true
}
function ref(value) {
    return createRef(value)
}
class RefImpl {
    constructor(rawValue) {
        this._value = rawValue // 初始值
        this.__v_isRef = true // 标识为一个ref类型对象
    }
    get value() {
        console.log('ref读取')
        track(this, 'get', 'value')
        return this._value
    }
    set value(newValue) {
        this._value = newValue
        console.log('ref写入')
        trigger(this, 'set', 'value')
    }
}
function createRef(rawValue) {
    // 如果已经处理过了,直接返回
    if (isRef(rawValue)) {
        return rawValue
    }
    return new RefImpl(rawValue)
}

// 依赖收集
const targetMap = new WeakMap() // 保存所有的依赖
function track(target, type, key) {
    if (!activeEffect) return
    let depsMap = targetMap.get(target)
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
        depsMap.set(key, (dep = new Set()))
    }
    if (!dep.has(activeEffect)) {
        dep.add(activeEffect)
        activeEffect.deps.push(dep)
    }
}

// 变更通知
function trigger(target, type, key, newValue) {
    const effects = new Set()
    const depsMap = targetMap.get(target)
    if (!depsMap) return
    function add(effectsToAdd) {
        effectsToAdd.forEach(effect => {
            if (effect !== activeEffect) {
                effects.add(effect)
            }
        })
    }
    if (!depsMap.get(key)) return
    add(depsMap.get(key))
    function run(effect) {
        effect()
    }
    effects.forEach(run)
}

// 订阅者
let uid = 0
function cleanup(effect) {
    const deps = effect.deps
    for (let i = 0; i < deps.length; i++) {
        deps[i].delete(effect)
    }
    deps.length = 0
}
function createReactiveEffect(fn) {
    const effect = function reactiveEffect() {
        activeEffect = effect
        cleanup(effect) // 每次执行订阅者方法之前都先将所有的依赖关系清空,重新进行依赖收集
        try {
            fn() // 订阅者方法会引用数据,从而触发被引用数据的getter进行依赖收集
        }
        finally {
            activeEffect = null // 完成依赖收集后将activeEffect置为null
        }
    }
    effect.id = uid++
    effect.deps = []
    return effect
}

分别测试一下:

// 引用类型
var obj = {name: 'jack'}
var person = reactive(obj)
function fn() {
    console.log('fn called with ' + person.name) 
}
var consoleEffect = createReactiveEffect(fn)
// 修改obj.name
person.name = 'tom'

// 基础类型
var count = ref(1)
var fn1 = function () {
    console.log('fn1 called with ' + count.value)
}
var countEffect = createReactiveEffect(fn1)
countEffect()
// 修改count的value
count.value = 2

总结&思考

vue3.0整个数据驱动的实现过程见上一小节开头 结合上一篇笔记遗留的问题:

  1. 在一轮变更中订阅者依赖的多个属性都发生了变更或者依赖的某个属性发生了多次变更怎么解决订阅者多次update的问题
  2. 如果在一个订阅者的getter函数中嵌套另一个订阅者,此时的依赖收集过程是怎样的

在本篇依旧没有得到解决,将在后面的篇幅集中解决这些问题,同时对比3.0和2.x有一些细节的差别也很值得注意:

  1. 2.x中数据(被依赖者)需要维护订阅者信息但是数据本身(属性)并没有存储数据的能力所以引入了Dep类同数据属性一一对应用于维护订阅者信息;但在3.0中则使用了全局的targetMapWeakmap对象对所有数据(被依赖者)和订阅者之间的关系做了统一的管理,targetMap以数据的代理作为key以由数据所有属性作为key的对象为值,在这个对象的每一个属性中以Set数据结构保存订阅者;描述半天还不如几行代码:
targetMap = {
    target/*proxy*/: {
        key: [effect]
    }
}
  1. 在2.x和3.0版本中避免依赖更新过程中出现循环触发通知订阅者的情况分别使用了不同的处理方式;在2.0中引入新生代依赖和老生代依赖的概念,在执行订阅者方法依赖更新的时候不修改老生代依赖避免出现循环触发变更通知订阅者,待订阅者方法执行完毕依赖更新完成后再进行一次依赖整理将新生代依赖变更为老生代依赖;在3.0中则在变更通知方法中执行订阅者更新前首先确定要通知的订阅者列表然后根据此列表进行逐一通知,而订阅者effect方法在执行订阅者方法fn前首先执行cleanup清空该订阅者所有的依赖关系然后执行订阅者方法fn进行依赖更新

后续文章将着重最近两篇遗留的问题进行讨论,将引出vue变更通知后订阅者的更新策略