用js手撕ref 了解原理之后你是选择ref还是reactive呢?
哈喽哈喽,我是你们的金樽清酒。在上篇文章用js之后。我们埋下了一个伏笔。那就是如何用js打造一个ref出来。相信很多小伙伴通常会在选择ref还是reactive中间犯难,或许你看完源码自己打造一个之后就会有更好的答案。找到自己内心的那个答案是吧。做出选择那肯定是要有对比的咯,比如这个好看一点,那个高一点,肯定是各有优势才会让你为难?好了,那今天我们的重点就是手写ref。
假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!"
知识拓展 set和get的作用
我们知道,对象字面量里面是可以放函数的。但是访问该对象里面的属性的时候是不是也要调用呢?
js
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。
但是有没有一种方法,将value也能像name一样访问,不需要调用呢?诶,还真有,那就是在前面加get。get可以将属性与函数绑定,当属性被访问时,对应函数会执行。当然还有set。set也是将属性与函数绑定,但是它接受一个参数,当属性发生变化时对应函数被执行。我们可以看看下面的代码。
js
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)
你看这样我们就可以像访问name一样访问value,对应的函数会自动执行。
打造ref的理念
想一想为什么我要做一个这样的拓展呢?我们在用到ref的时候是不是后面都得加一个value呢?诶,是不是明白了什么。对,什么是响应式,那就是得有一个场所对数据进行处理。像我们前面reactive的理念是用proxy作为这个场所作为代理。那在ref里面value就是这个场所。所以为了方便我们通常加上get,set不用再调用,所以没错,value就是对象里面的方法。
js
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()。
代码总汇和效果
文件路径
html
<!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>
reactive.js
复制代码
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
}
baseHandlers.js
复制代码
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,
}
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() // 将该属性上的所有的副作用函数全部触发
});
}
ref.js
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 () {
// }
看,我们自己打造的ref就生效了。
总结
ref的设计理念其实和reactive一样。但是由于proxy只能代理对象,所以我们只能在自己打造一个构造函数来进行代理。你看我们都写出来了代理原始数据类型的构造函数。没准什么时候能手写proxy,哈哈哈哈。ref里面内置了一个reactive,所以它的功能更加强大。但是由于它返回的是一个是实例对象,我们要访问里面的value属性才能得到值,所以ref在使用的时候要加一个.value。正因为用了get和set,不然访问函数对象还要调用,形如.value()。会更加的复杂。好了希望我的文章能够帮到你,可以在评论区多交流一下哦。我来给你们解答。
假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!"