面试问我reactive和ref的区别,我没有深入了解,面试官说我不知道reactive,我怒了,一怒之下,我写了这个文章。
无论是 reactive
还是 ref
,它们都能将普通的 JavaScript 数据转换为 Vue 的响应式数据,使得当数据发生变化时,相关的视图会自动更新。那为什么reactive只能处理引用类型?ref能黑白通吃。
看过源码可能不理解,干脆手写一个普通版本的reactive试试。
手写过程
创建reactive函数
reactive的作用是把数据变成响应式,所以在创建reactive函数时需要传递第一个参数
target(其他参数后面提到)。
js
export function reactive(target){ //target是引用类型
return createReactiveObject(target,reactiveMap,mutableHandlers)
}
为了保持代码的优雅,将逻辑放在createReactiveObject函数里面。
创建响应式函数createReactiveObject
在这个函数里面:
- 首先就得判断数据是否是引用类型,还得注意null使用typeof会判断为object。
- 对象是否已经被代理成为响应式对象。
- 执行代理操作。
- 存储代理过的响应式对象。
- export const reactiveMap = new WeakMap() 。
WeakMap
是一种特殊的 Map 数据结构,它的键只能是对象,并且不会阻止被引用的对象被垃圾回收。这个特性使得 WeakMap 在存储对象的同时不会造成内存泄漏。源码使用也是该数据结构。
- export const reactiveMap = new WeakMap() 。
使用export是为了将reactiveMap作为第二个参数
传递到createReactiveObject,更加优雅。
js
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处理成响应式) Proxy不支持原始类型
const proxy = new Proxy(target,proxyHandlers) //第二个参数的作用:当target被读取值,设置值等操作会触发的函数
// 往proxyMap 增加 proxy,把已经代理过的对象缓存起来
proxyMap.set(target,proxy)
return proxy
}
代理操作
创建mutableHandlers函数
当我们把数据变成响应式,这个时候我们还得考虑一下reactive涉及到的副作用函数(effect),数据更新,副作用函数也会更新
mutableHandlers函数涉及到第三个参数
。当target被v8引擎读取,操作后会触发该函数。
js
export const mutableHandlers = {
get, 读取使用的target
set 更新target
}
const get = createGetter();
const set = createSetter();
get
在get里面进行依赖收集,当数据没有被使用时不会被收集,get只读取使用的属性,依赖的收集后面提到。
js
function createGetter(){
return function get(target,key,receiver){
console.log('读取');
const res = Reflect.get(target,key,receiver)
// 依赖收集
// 在set之前收集这个属性究竟还有哪些地方用到了(副作用函数的收集)
track(target,key)
return res
}
}
set
在set里面更新数据并触发副作用函数
js
function createSetter(){
return function set(target,key,value,receiver) {
console.log('更新',key,value);
const res = Reflect.set(target,key,value,receiver) // target[key] = value
//记录此时是哪个key值变更,再去通知依赖该值的函数生效 更新浏览器的视图
//触发被修改的属性身上添加副(附加 watch,computed)作用函数 触发收集(被修改的key在哪些地方用到了)发布订阅
trigger(target,key)
return res
}
}
副作用函数
effect函数如下
js
effect(
()=>{
console.log(state.count+ 'jin' + state.name);
},
{
lazy:false,
scheduler:()=>{ //当调度器函数执行的时候,副作用函数不再触发
console.log('调度器')
}
})
副作用函数收集器
创建一个effect副作用函数收集器
js
let activeEffect = null //得是一个副作用函数
export function effect(fn,options ={}){
const effectFn = ()=>{
try {
activeEffect = effectFn //保证activeEffect是一个函数
return fn()
} finally {
activeEffect = null
}
}
if (!options.lazy){ //false才会触发
effectFn()
}
effectFn.scheduler = options.scheduler //调度器函数
return effectFn
}
effect
函数用来创建一个副作用函数。该函数接收两个参数:fn
和 options
,其中 fn
是副作用函数本身,options
是一个可选的配置对象,用来指定一些额外的选项。
在函数内部,首先定义了一个名为 effectFn
的函数,它的作用是执行副作用函数。在 effectFn
中,首先将当前函数赋值给全局变量 activeEffect
,以便在后续的响应式数据访问中记录(收集)当前正在执行的副作用函数。然后执行副作用函数 fn()
并返回其结果。最后,将 activeEffect
重置为 null
,以便在下一次副作用函数执行时重新记录当前函数。
track收集依赖响应式数据函数
这里就是实现get操作里面的依赖收集(副作用函数收集),将初次读取到的数据收集,它在 targetMap
中创建或更新一个映射关系,将属性 key
与副作用函数 activeEffect
关联起来。
js
const targetMap = new WeakMap();
export function track(target,key){
let depsMap = targetMap.get(target)
if(!depsMap){ //初次读取到值 收集effect
depsMap = new Map()
targetMap.set(target,depsMap )
}
//是否做过依赖收集
let deps = depsMap.get(key);
if(!deps){ //还未添加过effect,说明创建的desMap为空
deps = new Set()
}
if(!deps.has(activeEffect) && activeEffect){
//存入一个effect函数
deps.add(activeEffect)
}
// track函数更新 depsMap中属性key 的依赖集合,将最新的 deps设置为属性 key的值
depsMap.set(key,deps)
}
trigger触发依赖响应式数据函数
当我们将所有副作用函数收集后,响应式数据的更新就要触发依赖响应式数据的函数。 更新数据后,在set里面就会触发该函数,通过找到更新的数据的key,将所有依赖该数据的副作用函数执行并更新。
js
export function trigger(target,key){
//将targetMap里面的每个副作用函数赋给depsMap
const depsMap = targetMap.get(target)
if(!depsMap){ //当前对象中所有的key都没有副作用函数(从来没有使用过)
return
}
const deps = depsMap.get(key)
if(!deps){ //这个属性没有依赖
return
}
deps.forEach(effectFn=>{
//判断调度器是否存在
if(effectFn.scheduler){
effectFn.scheduler()
}else{
effectFn()//将该属性上的所有的副作用函数全部触发
}
//即使有调度器函数,副作用函数也会被执行
effectFn()
})
}
在响应式函数createReactiveObject
里面target被操作就会触发代理操作,执行mutableHandlers
函数的get和set,将每个变更后的target和它的key传给trigger,以此实现将全部副作用函数触发。
最后
假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!