手撕Vue源码之——reactive

前言

在面试的过程中,很多面试官都希望你能知道vue的一些底层原理,所以可能会问你vue中的某个功能是如何实现的,这个时候就是希望你能用原生的js手搓这个功能出来,reactive就在其中。reactive是Vue源码中实现响应式数据的核心之一,它负责将引用类型代理成响应式对象。这是因为Proxy只接受引用类型对象。

思路

从vue已经封装好的reactive来看,它接受一个对象作为参数,然后将这个对象变成响应式的,所谓响应式就是属性的值发生改变后,所有用到这些属性的地方(副作用函数)都会重新执行一遍,所以核心就是两个功能,一是找到所有用到这个属性的副作用函数,二是当属性值发生变更时这些副作用函数会重新执行。

js 复制代码
import { mutableHandlers } from "./baseHandlers.js"

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

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

export function createReactiveObject(target, proxyMap, proxyHandlers) {  // 创建响应式的函数
    // 判断target是不是一个引用类型
    // typeof用于判断除null之外的原始类型
    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
}

这里我们用到了Proxy对象,它的作用就是直接把一个对象变成响应式对象,这里我们还用到了WeakMap,它其实和Map的功能一样,只是性能更加友好一些,它用来存放被代理过的对象,防止对象被重复代理(变成响应式),Proxy里面执行的逻辑用另一个的文件书写,这样可以使代码看起来更加美观。

Proxy

Proxy 可以理解成,在目标对象之前架设一层"拦截",外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来"代理"某些操作,可以译为"代理器"。常用的方法如下,我们就只需要用到getset方法就行了,用于读取和修改。

js 复制代码
import  { track, trigger } from './effect.js'

const get = createGetter()
const set = createSetter()

function createGetter() {
    return function get(target, key, receiver) {
        // target 被代理的原对象,key是原对象中的键,receiver是被代理后的对象
        const res = Reflect.get(target, key, receiver)
        // 这个属性究竟还有哪些地方用到了(副作用函数的收集,computed,watch...)依赖收集
        track(target, key)
        return res
    }
}

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

export const mutableHandlers =  {
    get,
    set,
}

这里我们用到了Reflect来进行读取和修改数据,Reflect是ES6及之后的所有隶属于Object对象上的方法挪到Reflect对象上的一个机制。在reactive中,Reflect主要解决了一些程序报错问题,增强了代码的健壮性。所以Reflect本质上就是Object。我们在getset里面用到了我们自己定义的tracktrigger两个函数,track用于依赖收集,trigger用于依赖的触发,使所有的副作用函数在属性值修改的时候重新执行一遍。

effect

effect 用来跟踪当前正在运行的函数,effect 是一个函数的包裹器 ,在函数被调用之前就启动跟踪, 并能在需要时再次执行它。它接受两个参数,一个回调函数,还有一个对象,对象里面就一个属性lazy,lazy的值为bool,表示是否懒惰,false的话就立即执行,true就不执行。

js 复制代码
effect(
    () => {
        console.log(`${state.name}今年${state.age}岁了`);
    },
    {lazy: false}
)

这里我们手写了一个effect用于测试我们的reactive是否有效,activeEffect用来表示相应的副作用函数,注释掉的targetMap是我们要存成的数据结构,deps用的是Set,目的是去重,同种的副作用函数存一次就够了,在trigger中,用了一个forEach循环,目的是让该属性的所有副作用函数都再执行一次

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

export function effect(fn, options={}) {
    const effectFn = () => {
        try {
            activeEffect = effectFn
            return fn()
        } finally {
            activeEffect = null
        }
    }
    if (!options.lazy) {
        effectFn()
    }
    return effectFn
}


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

    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) {      // 也是watch和computed的核心
    const depsMap = targetMap.get(target)
    if (!depsMap) {    // 当前对象中所有的key都没有副作用函数(从来没有被使用过)
        return
    }
    const deps = depsMap.get(key)
    if (!deps) {    //这个属性没有依赖
        return
    }

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

结语

到这里一个基本的reactive就已经写完了,是不是也没有想象中的那么难,核心就是要理清楚它到底实现了哪些功能,只要思路对,剩下的就是善于去选择和使用js自带的一些数据结构和方法了。

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

相关推荐
好易学·数据结构1 分钟前
可视化图解算法52:数据流中的中位数
数据结构·算法·leetcode·面试·力扣·笔试·牛客
qq_27866728612 分钟前
ros中相机话题在web页面上的显示,尝试js解析sensor_msgs/Image数据
前端·javascript·ros
烛阴20 分钟前
JavaScript并发控制:从Promise到队列系统
前端·javascript
&活在当下&1 小时前
element plus 的树形控件,如何根据后台返回的节点key数组,获取节点key对应的node节点
javascript·vue.js·element plus
$程1 小时前
Vue3 项目国际化实践
前端·vue.js
nbsaas-boot1 小时前
Vue 项目中的组件职责划分评审与组件设计规范制定
前端·vue.js·设计规范
fanged2 小时前
Angular--Hello(TODO)
前端·javascript·angular.js
京东零售技术3 小时前
我在618主场,和3位顶尖技术博士聊了聊
面试
gongzemin3 小时前
前端根据文件流渲染 PDF 和 DOCX 文件
前端·vue.js·express
实习生小黄4 小时前
基于扫描算法获取psd图层轮廓
前端·javascript·算法