likes
comments
collection
share

vue中的reactive是如何实现的? vue源码分析,带你用js手写一个reactive

作者站长头像
站长
· 阅读数 7

一篇文章,带你读懂vue中的reactive是如何打造的,用js手写一个vue中的reactive

哈喽哈喽,大家好。我是你们的金樽清酒。我们知道,vue最核心的就是响应式了。什么是响应式呢?在Vue.js中,"响应式"通常指的是Vue实例中的数据变化会自动触发相关联的DOM更新的特性。Vue通过其响应式系统实现了这一功能。当Vue实例的数据发生变化时,与之相关联的DOM将会自动更新以反映这些变化,而不需要手动操作DOM。

Vue的响应式系统是通过使用ES5的Object.defineProperty或者ES6的Proxy来实现的,它会劫持数据的变化,从而能够追踪到数据的变化,并且在数据发生变化时触发相关联的更新。那么我们今天要做的事情就是手写一个reactive响应式数据源。

reactive设计理念

将对象代理

我们知道ref和reactive的区别在于reactive是将引用类型转化为响应式对象。而ref是将原始类型转化为响应式对象,当然ref也可以将引用类型转化为响应式对象。为什么reactive会有这样的局限呢?从源码的角度出发,因为reactiv是通过ES6的Proxy实现的。而Proxy只能接受引用数据类型(复杂数据类型)(不知道Proxy的小伙伴可以可以去看ES6的官方文档)

知道这个之后,那我们的第一步就是抛出reactive函数,并通过reactive函数里面调用Proxy将数据代理。

import { mutableHandlers } from './baseHandlers.js'

// 保存被代理过的对象
export const reactiveMap = new WeakMap() // new Map() // new WeakMap 对内存的回收更加友好


export function reactive(target) { // 将target变成响应式
    return createReactiveObject(target, reactiveMap, mutableHandlers)
}


export function createReactiveObject(target, proxyMap, proxyHandlers) { // 创建响应式的函数
    // 判断target是不是一个引用类型
    if (typeof target !== 'object' || target === null) {  // 不是对象就不给操作
        return target
    }

    // 该对象是否已经被代理过(已经是响应式对象)
    const existingProxy = proxyMap.get(target)
    if (existingProxy) {
        return existingProxy
    }

    // 执行代理操作(将target处理成响应式)
    const proxy = new Proxy(target, proxyHandlers) // 第二个参数的作用:当target被读取值,设置值,判断值等等操作时会触发的函数

    // 往 proxyMap 增加 proxy, 把已经代理过的对象缓存起来
    proxyMap.set(target, proxy)

    return proxy
}

代码分析:14-16行代码是为了保证我们的Proxy代理的数据target是一个对象,因为Proxy只支持对象作为参数。第18到22行是确定该对象是否被代理过,因为被代理过的数据已经是响应式数据了,再执行一遍代理会浪费性能。在这两步之后,我们就可以new一个Proxy对象,Proxy对象有两个参数,一个是被代理的对象target,另一个就是当target被读取值后触发的各种函数,而我们的响应式reactive就是通过这些函数完成的。第二个参数有很多种函数,我们就把它写在另一个文件里面用一个函数作为它的参数。也就是第一行引用的函数。

import { track, trigger } from './effect.js'
const get = createGetter(); // 创建一个get函数
const set = createSetter(); // 创建一个set函数

function createGetter() {
    return function get(target, key, receiver) {
        console.log('target被读取值');
        const res = Reflect.get(target, key, receiver); // 获取源对象中的键值
        // 这个属性究竟还有哪些地方用到了,(副作用函数的收集,computed,watch...)
        track(target, key)


        return res;
    }
}

function createSetter() {
    return function set(target, key, value, receiver) {
        console.log('target被设置值', key, value);
        const res = Reflect.set(target, key, value, receiver); // 设置源对象中的键值 === target[key] = value


        // 需要记录下来此时是哪一个key的值变更了,再去通知其他依赖该值的函数生效,更新浏览器的视图(响应式)
        // 触发被修改的属性身上的副函数 依赖收集(被修改的key在哪些地方被使用了)发布订阅
        trigger(target, key)
        return res;
    }
}


export const mutableHandlers = {
    get,
    set,
}

在这个文件里面。我们抛出了mutableHandlers()函数。函数里面有两个方法createGetter(),createSetter()。一个用来获取我们需要代理的对象,一个用来重新设置值。这样我们简易的reactive的大致功能就完成了。

我们来验证一下

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script type="module">
        import { reactive } from './reactive.js';
        const state = reactive({
            name: 'li',
            age: 18
        })
        setInterval(() => {
            state.age = state.age + 1
            console.log(state.age)
        }, 2000)
    </script>
</body>

</html>

这是一个html文件,我们用引入自己写的reactive函数。在代码13到16行写一个reactive响应式数据源。 在代码17-20行写了一个定时器,每两秒修改state.age的值。再将state.age打印一下。

vue中的reactive是如何实现的? vue源码分析,带你用js手写一个reactive 你看,我们写的reactive就起作用了。你以为就完成了嘛。不,这还只是一个丐版的reactive,甚至都不能称之为响应式。你想想,我们的reactive是不是有时候会用在watch和computed计算属性里面。那reactive发生改变,watch和computed里面的相应的响应式数据源不是也得发生改变嘛,那怎么再次让watch和computed函数再次执行呢?也就是说,响应式数据是一个地方改变,存在该响应式数据源的地方都得改变,watch和computed里面的逻辑也得重新执行。

副作用收集

什么是副作用收集呢?也叫做转发订阅。就是说谁里面存在这个响应式数据源呢?这个属性还要运用到哪些地方呢?这些东西我们要在get获取到响应式对象的时候执行,注意到我上面的get里面有个外部引入的track()那个就是收集函数。统计一下,比如computed,watch等。为什么要收集呢?因为待会要触发。哈哈哈哈哈哈哈。

副作用触发

既然我们收集了副作用函数之后,我们就要在修改值的时候触发。因为响应式对象值被修改之后,相应的副作用函数也要再次触发,也就是watch,computed函数在该依赖的数据源发生变化时要再次被调用。这些都写在了另一个函数effect.js当中。

const targetMap = new WeakMap()
let activeEffect = null  // 得是一个副作用函数

export function effect(fn, options = {}) { // 也是watch,computed 的核心逻辑
    const effectFn = () => {
        try {
            activeEffect = effectFn
            return fn()
        } finally {
            activeEffect = null
        }
    }
    if (!options.lazy) {
        effectFn()
    }
    return effectFn
}


// 为某个属性添加 effect
export function track(target, key) {
    // targetMap = {  // 存成这样
    //   target: {
    //     key: [effect1, effect2, effect3,...]
    //   },
    //   target2: {
    //     key: [effect1, effect2, effect3,...]
    //   }
    // }

    let depsMap = targetMap.get(target)
    if (!depsMap) { // 初次读取到值 收集effect
        depsMap = new Map()
        targetMap.set(target, depsMap)
    }
    let deps = depsMap.get(key)
    if (!deps) { // 该属性还未添加过effect
        deps = new Set()
    }
    if (!deps.has(activeEffect) && activeEffect) {
        // 存入一个effect函数
        deps.add(activeEffect)
    }
    depsMap.set(key, deps)

}

// 触发某个属性 effect
export function trigger(target, key) {
    const depsMap = targetMap.get(target)
    if (!depsMap) { // 当前对象中所有的key都没有副作用函数(从来没有被使用过)
        return
    }

    const deps = depsMap.get(key)
    if (!deps) { // 这个属性没有依赖
        return
    }

    deps.forEach(effectFn => {
        effectFn() // 将该属性上的所有的副作用函数全部触发
    });
}

代码分析:在21到46行是track()函数,也就是我们的副作用收集。我们用set数据结构来存储我们的副作用函数。49行到63行就是触发我们的副作用函数。两个if,一个当该副作用函数数组为空则返回不执行,当里面没有依赖也不执行。然后循环执行收集作用函数数组里面的函数,使它们全部触发。

这样子呢,我们整个的reactive就完成啦。但是,还没完,我们难道不要用watch和computed来验证一下嘛。诶,别急,看到上面还有一个抛出的effect函数嘛。在代码4-17抛出一个effect函数。里面两个参数fn(),和options = {},当options.lazy为true时,函数不调用。不然最后的返回值为fn,也就是我们的功能函数。其实这个effect就是watch和computed的核心逻辑。你看watch和computed是不是也接受一个这样的方法来执行呢。那我们就可以把effct暂时当成watch和computed来使用。

测试效果

总文件路径

vue中的reactive是如何实现的? vue源码分析,带你用js手写一个reactive

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script type="module">
        import { reactive } from './reactive.js';
        import { effect } from './effect.js';

        const state = reactive({
            name: 'li',
            age: 18
        })

        effect(
            () => {
                console.log(`${state.name}今年${state.age}岁了`);
            },
            { lazy: false }
        )
        setInterval(() => {
            state.age = state.age + 1
        }, 2000)
    </script>
</body>

</html>
import { mutableHandlers } from './baseHandlers.js'

// 保存被代理过的对象
export const reactiveMap = new WeakMap() // new Map() // new WeakMap 对内存的回收更加友好


export function reactive(target) { // 将target变成响应式
    return createReactiveObject(target, reactiveMap, mutableHandlers)
}


export function createReactiveObject(target, proxyMap, proxyHandlers) { // 创建响应式的函数
    // 判断target是不是一个引用类型
    if (typeof target !== 'object' || target === null) {  // 不是对象就不给操作
        return target
    }

    // 该对象是否已经被代理过(已经是响应式对象)
    const existingProxy = proxyMap.get(target)
    if (existingProxy) {
        return existingProxy
    }

    // 执行代理操作(将target处理成响应式)
    const proxy = new Proxy(target, proxyHandlers) // 第二个参数的作用:当target被读取值,设置值,判断值等等操作时会触发的函数

    // 往 proxyMap 增加 proxy, 把已经代理过的对象缓存起来
    proxyMap.set(target, proxy)

    return proxy
}

import { track, trigger } from './effect.js'
const get = createGetter(); // 创建一个get函数
const set = createSetter(); // 创建一个set函数

function createGetter() {
    return function get(target, key, receiver) {
        console.log('target被读取值');
        const res = Reflect.get(target, key, receiver); // 获取源对象中的键值
        // 这个属性究竟还有哪些地方用到了,(副作用函数的收集,computed,watch...)
        track(target, key)


        return res;
    }
}

function createSetter() {
    return function set(target, key, value, receiver) {
        console.log('target被设置值', key, value);
        const res = Reflect.set(target, key, value, receiver); // 设置源对象中的键值 === target[key] = value


        // 需要记录下来此时是哪一个key的值变更了,再去通知其他依赖该值的函数生效,更新浏览器的视图(响应式)
        // 触发被修改的属性身上的副函数 依赖收集(被修改的key在哪些地方被使用了)发布订阅
        trigger(target, key)
        return res;
    }
}


export const mutableHandlers = {
    get,
    set,
}
const targetMap = new WeakMap()
let activeEffect = null  // 得是一个副作用函数

export function effect(fn, options = {}) { // 也是watch,computed 的核心逻辑
    const effectFn = () => {
        try {
            activeEffect = effectFn
            return fn()
        } finally {
            activeEffect = null
        }
    }
    if (!options.lazy) {
        effectFn()
    }
    return effectFn
}


// 为某个属性添加 effect
export function track(target, key) {
    // targetMap = {  // 存成这样
    //   target: {
    //     key: [effect1, effect2, effect3,...]
    //   },
    //   target2: {
    //     key: [effect1, effect2, effect3,...]
    //   }
    // }

    let depsMap = targetMap.get(target)
    if (!depsMap) { // 初次读取到值 收集effect
        depsMap = new Map()
        targetMap.set(target, depsMap)
    }
    let deps = depsMap.get(key)
    if (!deps) { // 该属性还未添加过effect
        deps = new Set()
    }
    if (!deps.has(activeEffect) && activeEffect) {
        // 存入一个effect函数
        deps.add(activeEffect)
    }
    depsMap.set(key, deps)

}

// 触发某个属性 effect
export function trigger(target, key) {
    const depsMap = targetMap.get(target)
    if (!depsMap) { // 当前对象中所有的key都没有副作用函数(从来没有被使用过)
        return
    }

    const deps = depsMap.get(key)
    if (!deps) { // 这个属性没有依赖
        return
    }

    deps.forEach(effectFn => {
        effectFn() // 将该属性上的所有的副作用函数全部触发
    });
}

vue中的reactive是如何实现的? vue源码分析,带你用js手写一个reactive 成功在effect里面执行,说明我们的副作用函数也生效了。成功的完成了响应式。

总结

其实用js写一个reactive就三个设计理念。

  • 1.对象代理。(基于Proxy)。
  • 2.收集副作用函数。在get的时候触发。
  • 3.触发副作用函数。在set的时候触发。 好啦,vue的源码确实有点难度。vue作业的代码确实很优雅,也值得我们去细细的剖析。那我们下次再见,一起再手写ref。欢迎友友们提建议哦。

假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!”