前言
在面试的过程中,很多面试官都希望你能知道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 这个词的原意是代理,用在这里表示由它来"代理"某些操作,可以译为"代理器"。常用的方法如下,我们就只需要用到get
和set
方法就行了,用于读取和修改。
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
。我们在get
和set
里面用到了我们自己定义的track
和trigger
两个函数,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,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!