likes
comments
collection
share

书接上回 手写vue中的reactive之后 再手写ref 了解源码之后你会选择ref还是reactive呢?

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

用js手撕ref 了解原理之后你是选择ref还是reactive呢?

哈喽哈喽,我是你们的金樽清酒。在上篇文章用js之后。我们埋下了一个伏笔。那就是如何用js打造一个ref出来。相信很多小伙伴通常会在选择ref还是reactive中间犯难,或许你看完源码自己打造一个之后就会有更好的答案。找到自己内心的那个答案是吧。做出选择那肯定是要有对比的咯,比如这个好看一点,那个高一点,肯定是各有优势才会让你为难?好了,那今天我们的重点就是手写ref。

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

知识拓展 set和get的作用

我们知道,对象字面量里面是可以放函数的。但是访问该对象里面的属性的时候是不是也要调用呢?

 let obj = {
            name: "li",
             value() {
                return 28
            },
            // set value(newVal) {
            //     console.log(newVal, "--------")
            // }
        }
        console.log(obj.name)
        console.log(obj.value())

比如我们打印obj里面的value的时候是不是要obj.value(),value要调用才能执行,这样的话会很麻烦,不像访问name一样,直接obj.name。

书接上回  手写vue中的reactive之后  再手写ref   了解源码之后你会选择ref还是reactive呢?

但是有没有一种方法,将value也能像name一样访问,不需要调用呢?诶,还真有,那就是在前面加get。get可以将属性与函数绑定,当属性被访问时,对应函数会执行。当然还有set。set也是将属性与函数绑定,但是它接受一个参数,当属性发生变化时对应函数被执行。我们可以看看下面的代码。

  let obj = {
            name: "li",
            get value() {
                return 28
            },
            set value(newVal) {
                console.log(newVal, "--------")
            }
        }
        obj.value = 18
        console.log(obj.name)
        console.log(obj.value)

书接上回  手写vue中的reactive之后  再手写ref   了解源码之后你会选择ref还是reactive呢? 你看这样我们就可以像访问name一样访问value,对应的函数会自动执行。

打造ref的理念

想一想为什么我要做一个这样的拓展呢?我们在用到ref的时候是不是后面都得加一个value呢?诶,是不是明白了什么。对,什么是响应式,那就是得有一个场所对数据进行处理。像我们前面reactive的理念是用proxy作为这个场所作为代理。那在ref里面value就是这个场所。所以为了方便我们通常加上get,set不用再调用,所以没错,value就是对象里面的方法。

import { reactive } from './reactive.js'
import { track, trigger } from './effect.js'


export function ref(val) {  // 将原始类型数据变成响应式 引用类型也可以
  return createRef(val)
}

function createRef(val) {
  // 判断val是否已经是响应式
  if (val.__v_isRef) {
    return val
  }

  // 将val变为响应式
  return new RefImpl(val)
}


 // const age = ref({n: 18})
class RefImpl {  
  constructor(val) {
    this.__v_isRef = true // 给每一个被ref操作过的属性值都添加标记
    this._value = convert(val)
  }

  get value() {
    // 为this对象做依赖收集
    track(this, 'value')
    return this._value
  }

  set value(newVal) {
    // console.log(newVal);
    
    if (newVal !== this._value) {
      this._value = convert(newVal)
      trigger(this, 'value') // 触发掉 'value' 上的所有副作用函数
    }
  }

}

function convert(val) {
  if (typeof val !== 'object' || val === null) {  // 不是对象
    return val
  } else {
    return reactive(val)
  }
}

看到上面的代码,我们是如何打造ref的呢? 其实,和打造reactive一样,三个核心理念

  • 将数据代理
  • 收集依赖
  • 触发依赖
  • 不知道核心理念的可以去看我的手写reactive。

那我们知道,reactive的将对象代理是通过proxy完成的,它只能代理对象,而我们ref是将原始数据类型转化为响应式的。那js有没有像proxy一样的功能但是接受原始数据类型呢?答案是没有。所以我们需要自己构造一个函数来完成这个功能。如代码21行到50行。然后在createref实例化这个对象就是将数据代理为响应式,最后在ref里面返回createref,所以最后的ref其实是一个实例化的对象。然后我们的依赖收集就在get value里面。依赖触发就在set value里面。收集依赖,和触发依赖的函数在上篇打造reactive里面effrct.js里面。这里我直接引用了。

在44-55行,我们判断了参数的类型,如果是原始数据类型,用value可以完成代理,如果是引用数据类型,还是直接用reactive的源码。哈哈哈哈哈哈,所以你可以看到这里我们直接引用了reactivef方法。

所以说,ref里面是内置了一个reactive的,功能会比reactive更强,因为reactive是通过proxy代理的,而proxy是只能接受对象作为参数,所以有一定的局限性。ref就通过写一个构造函数来弥补这个缺陷,但是你看由于ref是一个实例化对,我们要访问值必须访问value属性,所以ref要.value一下。再看一下为什么我要补充一下set和get呢?不然我们的ref就要这么写了。.value()。

代码总汇和效果

文件路径

书接上回  手写vue中的reactive之后  再手写ref   了解源码之后你会选择ref还是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 { effect } from './effect.js';
        import { ref } from './ref.js'
        const state = ref({
            name: "li",
            age: 18
        })
        const college = ref("东华理工大学")
        effect(
            () => {
                console.log(`${state.value.name}今年${state.value.age}岁了,在${college.value}`);
            },
            { lazy: false }
        )
        setInterval(() => {
            state.value.age = state.value.age + 1
            console.log(state.value)
        }, 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() // 将该属性上的所有的副作用函数全部触发
    });
}
import { reactive } from './reactive.js'
import { track, trigger } from './effect.js'


export function ref(val) {  // 将原始类型数据变成响应式 引用类型也可以
    return createRef(val)
}

function createRef(val) {
    // 判断val是否已经是响应式
    if (val.__v_isRef) {
        return val
    }

    // 将val变为响应式
    return new RefImpl(val)
}


// const age = ref({n: 18})
class RefImpl {
    constructor(val) {
        this.__v_isRef = true // 给每一个被ref操作过的属性值都添加标记
        this._value = convert(val)
    }

    get value() {
        // 为this对象做依赖收集
        track(this, 'value')
        return this._value
    }

    set value(newVal) {
        // console.log(newVal);

        if (newVal !== this._value) {
            this._value = convert(newVal)
            trigger(this, 'value') // 触发掉 'value' 上的所有副作用函数
        }
    }

}

function convert(val) {
    if (typeof val !== 'object' || val === null) {  // 不是对象
        return val
    } else {
        return reactive(val)
    }
}





// function RefImpl(val) {
//   this.__v_isRef = true
//   this._value = val
// }
// RefImpl.prototype.value = function () {

// }

书接上回  手写vue中的reactive之后  再手写ref   了解源码之后你会选择ref还是reactive呢? 看,我们自己打造的ref就生效了。

总结

ref的设计理念其实和reactive一样。但是由于proxy只能代理对象,所以我们只能在自己打造一个构造函数来进行代理。你看我们都写出来了代理原始数据类型的构造函数。没准什么时候能手写proxy,哈哈哈哈。ref里面内置了一个reactive,所以它的功能更加强大。但是由于它返回的是一个是实例对象,我们要访问里面的value属性才能得到值,所以ref在使用的时候要加一个.value。正因为用了get和set,不然访问函数对象还要调用,形如.value()。会更加的复杂。好了希望我的文章能够帮到你,可以在评论区多交流一下哦。我来给你们解答。

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