手动实现vue2、vue3响应式原理(Object.defineProperty/proxy)

前言

        “响应式”是指当数据改变后,Vue会通知到使用当前数据的代码,并且更新视图数据。接下来,简单的实现一个vue的响应式。

第一步:实现一个对象的响应式

// 对象的响应式
const info = {
    name: "chuan",
    age: 20
}

// 假如 infoChange() 函数是info对象被修改时而触发的函数
function infoChange() {
    console.log(info.name)
}

const infoProxy = new Proxy(info, {
    get: function (target, key, reciver) {
        return Reflect.get(target, key, reciver) // Reflect对象的用法
    },
    set: function (target, key, value, reciver) {
        infoChange()
        Reflect.set(target, key, value, reciver)
    }
})

infoProxy.name = "hahaha"

        这个时候,当我们手动去修改infoProxy(info的代理对象)时,便会触发infoChange这个函数,那当有其他函数或者其他对象时,我们该怎么办呢?

        接下来我们就要实现函数里的依赖收集。

第二步:封装依赖收集的函数

        假使会有多个用到同一个响应式对象的函数,我们只需要把这些函数放到一个数组里,当响应式对象被修改时,全部执行,那么有了这个思路就好办了。

// 对象的响应式
const info = {
    name: "chuan",
    age: 20
}

class Depend {
    constructor() {
        this.reactiveFns = [] // 存放所有含有响应式依赖的函数
    }
    // 需要一个方法往存放函数的数组加值
    addDependFns(reactiveFn) {
        this.reactiveFns.push(reactiveFn)
    }

    // 需要一个方法区执行这里面的依赖函数
    notify() {
        this.reactiveFns.forEach(reactiveFn => {
            reactiveFn()
        })
    }
}

const depend = new Depend();

//这儿需要封装一个函数,将含有依赖的函数添加进Depend里
function watchFn(fn) {
    depend.addDependFns(fn)
}

// 假如 infoChange() 函数是info对象被修改时而触发的函数
function infoChange() {
    console.log(info.name, '-------1')
}
function infoChange2() {
    console.log(info.name, '-------2');
}
watchFn(infoChange)
watchFn(infoChange2)

const infoProxy = new Proxy(info, {
    get: function (target, key, reciver) {
        return Reflect.get(target, key, reciver) // Reflect对象的用法
    },
    set: function (target, key, value, reciver) {
        Reflect.set(target, key, value, reciver)
        // 当被修改时,那么将通知Depend里面的函数全部取出执行;
        depend.notify()
    }
})

infoProxy.name = "hahaha"

        这样,所有使用了这个依赖的函数在响应式对象发生改变的时候,便为执行。为什么会用一个Depend类呢?其实就是为了方便后面,每一个对象的属性会对应一个 Depend类,收集属于自己的依赖,互不干扰。

第三步:依赖收集的管理 

        手动实现vue2、vue3响应式原理(Object.defineProperty/proxy)_第1张图片

         如上图,不同对象的每个属性分别会用一个Depend进行管理,那么当我某个对象的某个属性进行修改时,那么才能执行相关的所有函数。

        这时候需要引出 map / weekMap 的使用。通过 map 的对象属性将相应的Depend存入。再通过 weekMap 通过对象作为属性存入对象的map信息。下面举个简单例子:

let objNameDepend = new Depend()
let objAgeDepend = new Depend()

let obj = {
    name: "hahaha",
    age: 20
}
const objMap = new Map();
objMap.set("name", objNameDepend)
objMap.set("age", objAgeDepend)

console.log(objMap.get("name"), objMap.get("age"));

        通过这样,我们可以通过每个对象的属性存在Map中,当需要用的时候再取出来使用,接下来我们再封装一个获取对应Depend的函数:

const targetMap = new WeakMap()
function getDepend(targer, key) {
    // 根据target对象获取map的过程
    let objectMap = targetMap(target)
    if (!objectMap) {
        objectMap = new Map()
        targetMap.set(target, objectMap)
    }

    // 再根据key获取depend对象
    let depend = objectMap.get(key)
    if (!depend) { // 那有可能是没有创建Depeng的
        depend = new Depend()
        objectMap.set(key, depend)
    }

    return depend
}

        现在能够获取到每个对象每个属性自己的depend了,那么就简单了,我们只需要要往对应的depend里面加入依赖函数就可以了。那什么时候去收集这个依赖函数呢,其实就在proxy里面的get方法里,我们每次获取一次值,就会进入到get,那么在这个时候往里面添加依赖就好了。

        这个时候,我们又的对收集依赖的函数 watchFn 做修改,我们知道传入的函数里面含有这个响应式对象,那当传入的这个函数传进去后立即执行,是不是就可以触发这个对象的 get 方法,通过这点,我们对 watchFn  进行改造。

// 改变 watchFn 函数 
let globalFn = null; // 声明一个全局变量,是为了让 get 触发的时候,拿到我们传入的这个函数
function watchFn(fn) {
    globalFn = fn
    fn()
    globalFn = null // 执行完后清除这个值,防止后面调用出现问题
}

我们先看下完整代码,并且执行:

// 对象的响应式
const info = {
    name: "chuan",
    age: 20
}

class Depend {
    constructor() {
        this.reactiveFns = [] // 存放所有含有响应式依赖的函数
    }
    // 需要一个方法往存放函数的数组加值
    addDependFns(reactiveFn) {
        this.reactiveFns.push(reactiveFn)
    }

    // 需要一个方法区执行这里面的依赖函数
    notify() {
        this.reactiveFns.forEach(reactiveFn => {
            reactiveFn()
        })
    }
}

const depend = new Depend();

const targetMap = new WeakMap()
function getDepend(targer, key) {
    // 根据target对象获取map的过程
    let objectMap = targetMap(target)
    if (!objectMap) {
        objectMap = new Map()
        targetMap.set(target, objectMap)
    }

    // 再根据key获取depend对象
    let depend = objectMap.get(key)
    if (!depend) { // 那有可能是没有创建Depeng的
        depend = new Depend()
        objectMap.set(key, depend)
    }
}

//这儿需要封装一个函数,将含有依赖的函数添加进Depend里
let globalFn = null;
function watchFn(fn) {
    globalFn = fn
    fn()
    globalFn = null
}

// 假如 infoChange() 函数是info对象被修改时而触发的函数
function infoChange() {
    console.log(info.name, '-------1')
}
function infoChange2() {
    console.log(info.name, '-------2');
}
watchFn(infoChange)
watchFn(infoChange2)

const infoProxy = new Proxy(info, {
    get: function (target, key, reciver) {
        const depend = getDepend(target, key)
        depend.addDependFns(globalFn)
        return Reflect.get(target, key, reciver) // Reflect对象的用法
    },
    set: function (target, key, value, reciver) {
        Reflect.set(target, key, value, reciver)
        // 当被修改时,那么将通知Depend里面的函数全部取出执行;
        depend.notify()
    }
})

infoProxy.name = "hahaha"

        执行后发现,依然没有问题,那么接下来我们来看看哪些地方可以做优化。

第四步:优化

        手动实现vue2、vue3响应式原理(Object.defineProperty/proxy)_第2张图片

        在这里我们可以看到,每次都是通过手动去添加这个方法,那么其实这个事情可以交给depend去处理,所以接下来改造一下:

let globalFn = null; // 将这个存放收集依赖的函数放在顶端

// 对象的响应式
const info = {
    name: "chuan",
    age: 20
}

class Depend {
    constructor() {
        this.reactiveFns = [] // 存放所有含有响应式依赖的函数
    }
    // 每次收集依赖的时候调用这个函数即可
    addDepend() {
        if (globalFn) {
            this.reactiveFns.push(globalFn)
        }
    }

    // 需要一个方法区执行这里面的依赖函数
    notify() {
        this.reactiveFns.forEach(reactiveFn => {
            reactiveFn()
        })
    }
}

const infoProxy = new Proxy(info, {
    get: function (target, key, reciver) {
        const depend = getDepend(target, key)
        depend.addDepend()  // 直接调用方法即可
        return Reflect.get(target, key, reciver) // Reflect对象的用法
    },
    set: function (target, key, value, reciver) {
        Reflect.set(target, key, value, reciver)
        // 当被修改时,那么将通知Depend里面的函数全部取出执行;
        depend.notify()
    }
})

         我们可以看到现在只有info这个对象是响应式,那么如果有其他函数怎么办?这时候应该封装成一个函数:

function reactive(obj) {
    const objProxy = new Proxy(obj, {
        get: function (target, key, reciver) {
            const depend = getDepend(target, key)
            depend.addDepend()
            return Reflect.get(target, key, reciver) // Reflect对象的用法
        },
        set: function (target, key, value, reciver) {
            Reflect.set(target, key, value, reciver)
            // 当被修改时,那么将通知Depend里面的函数全部取出执行;
            depend.notify()
        }
    })
    return objProxy
}

         我们通过传入一个对象,返回回来一个代理对象(proxy),在函数内部完成响应式即可,下面我们看看完整的响应式代码:

let globalFn = null;
// 对象的响应式
const info = {
    name: "chuan",
    age: 20
}

class Depend {
    constructor() {
        this.reactiveFns = [] // 存放所有含有响应式依赖的函数
    }
    // 需要一个方法往存放函数的数组加值
    addDepend() {
        if (globalFn) {
            this.reactiveFns.push(globalFn)
        }
    }

    // 需要一个方法区执行这里面的依赖函数
    notify() {
        this.reactiveFns.forEach(reactiveFn => {
            reactiveFn()
        })
    }
}

const depend = new Depend();

const targetMap = new WeakMap()
function getDepend(targer, key) {
    // 根据target对象获取map的过程
    let objectMap = targetMap(target)
    if (!objectMap) {
        objectMap = new Map()
        targetMap.set(target, objectMap)
    }

    // 再根据key获取depend对象
    let depend = objectMap.get(key)
    if (!depend) { // 那有可能是没有创建Depeng的
        depend = new Depend()
        objectMap.set(key, depend)
    }
}

//这儿需要封装一个函数,将含有依赖的函数添加进Depend里
function watchFn(fn) {
    globalFn = fn
    fn()
    globalFn = null
}

// 假如 infoChange() 函数是info对象被修改时而触发的函数
function infoChange() {
    console.log(info.name, '-------1')
}
function infoChange2() {
    console.log(info.name, '-------2');
}
watchFn(infoChange)
watchFn(infoChange2)

function reactive(obj) {
    const objProxy = new Proxy(obj, {
        get: function (target, key, reciver) {
            const depend = getDepend(target, key)
            depend.addDepend()
            return Reflect.get(target, key, reciver) // Reflect对象的用法
        },
        set: function (target, key, value, reciver) {
            Reflect.set(target, key, value, reciver)
            // 当被修改时,那么将通知Depend里面的函数全部取出执行;
            depend.notify()
        }
    })
    return objProxy
}

const infoProxy = reactive(info)
infoProxy.name = "hahaha"

const obj = {
    message: 'yixiaochuan'
}
watchFn(() => {
    console.log("obj被修改了");
})
const objProxy = reactive(obj)
objProxy.message = "hahahah"

        看看打印的结果:

        确实使我们想要的结果,那么响应式我们就实现了!

        那么Vue2中采用的Object.defineproperty是相同思路,只需要改变写法就可以了

function reactive(target) {
    Object.keys(target).forEach(key => {
        let value = target[key]
        Object.defineProperty(target, key, {
            get: function () {
                const depend = getDepend(target, key)
                depend.addDepend()
                return value
            },
            set: function (newValue) {
                value = newValue
                // 当被修改时,那么将通知Depend里面的函数全部取出执行;
                depend.notify()
            }
        })
    })
    return target
}

总结:

        自己手写一遍响应式原理,加深自己的印象,也可以更加了解响应式到底是什么。

你可能感兴趣的:(Vue,vue,javascript)