customRef 与 ref

ref() 我们已经很熟悉了,就是用来定义响应式数据的,其底层原理还是通过 Object.defineprotpty 中的 get 实现收集依赖( trackRefValue 函数收集),通过 set 实现分发依赖通知更新( triggerRefValue 函数分发 )。我们看看 ref 的源码就知道了

javascript 复制代码
class RefImpl {
  private _value: any;    // 用来存储响应值
  private _rawValue: any;    // 用来存储原始值
  public dep?: Dep = undefined;    // 用来收集分发依赖
  public readonly __v_isRef = true;    //是否只读,暂不考虑
 
  // 接收 new RefImpl() 传递过来的 rawValue 和 shallow  
  constructor(value, public readonly __v_isShallow: boolean) {
    // 判断是否需要深层响应,如果不用,直接返回 Value 值,如果需要深层响应,则调用 toRaw 函数解除 value 的响应式,将其转化为原始值,以保证后续的深层响应
    this._rawValue = __v_isShallow ? value : toRaw(value);
 
    // 判断是否需要深层响应,如果不用,则直接返回Value,不做响应式处理。如果需要深层响应,则调用 reactive 函数进行深层响应
    this._value = __v_isShallow ? value : reactive(value);
  }
 
  get value() {
    // 收集依赖
    trackRefValue(this);
 
    // 返回响应式数据
    return this._value;
  }
 
  set value(newVal) {
 
    // 将 newVal 转化为原始值,并于初始原始值比较,若不同,则准备更新数据,渲染页面,分发依赖
    if (hasChanged(toRaw(newVal), this._rawValue)) {
 
      //判断是否需要深层响应,如果不用,直接返回 newVal 值,如果需要深层响应,则调用 toRaw 函数解除 newVal 的响应式,将其转化为原始值,以保证后续的深层响应
      this._rawValue = this.__v_isShallow ? newVal : toRaw(newVal);
 
      // 判断是否需要深层响应,如果不用,则直接返回Value,不做响应式处理。如果需要深层响应,则调用 reactive 函数进行深层响应
      this._value = this.__v_isShallow ? newVal : reactive(newVal);
 
      // 分发依赖,通知更新
      triggerRefValue(this);
    }
  }
}

具体的关于 ref 的使用以及更深层的理解请参考之前的文章 -- ref 函数

那么这个 customRef 函数是用来干啥的呢?

customRef

概念:创建一个自定义的 ref 函数,在其内部显式声明对其依赖追踪和更新触发的控制方式。

前面一句好理解,创建一个自定义的 ref ,其类型是一个函数,函数体内部的逻辑内容自定义。

后面一句就有点绕了,显式声明对其依赖追踪和更新触发的控制方式该怎么理解呢?

我们看看 ref 就知道了,当我们调用 ref 之后,读取数据时,Vue 底层就会自动去在 get 中收集依赖。修改数据时,会自动在 set 中分发依赖。这是不需要我们关心的,我们只需要调用 ref 函数就可以实现了。

但是 customRef 并没有按照 ref 的逻辑去实现,customRef 的处理是:既然你都自定义了,那你就自定义完整一点,依赖收集和分发工作你也自己做了,别去麻烦 Vue 底层在给你适配转化一次。

用法:customRef() 预期接收一个工厂函数作为参数,这个工厂函数接受 track 和 trigger 两个函数作为参数,并返回一个带有 get 和 set 方法的对象。

按照官网的例子我们一点点实现优化:创建一个自定义 ref ,实现防抖。具体效果就是我在 input 框中输入值,延时展示值。

第一步:不延迟,直接同步展示,v-model 双向绑定数据,插值语法展示数据,setup 定义数据

javascript 复制代码
<template>
	<input type="text" v-model="keyword">
	<h3>{{keyword}}</h3>
</template>

<script>
	import {ref} from 'vue'

	export default {
		name:'Demo',
		setup(){
			let keyword = ref('hello') //使用Vue准备好的内置ref
			
			return {
				keyword
			}
		}
	}
</script>

展示效果:

第二步:定义自己的 ref 函数,并且使用它

javascript 复制代码
setup(){
  // let keyword = ref('hello') //使用Vue准备好的内置ref

  // 定义自己的 ref 函数,接收值,并 return 出具体的值,否则返回undefined
  function myRef(value) {
    console.log(value);
    return value
  }

  let keyword = myRef('hello')  // 使用自定义的 ref 函数

  return {
    keyword
  }
}

此时我们发现,当我们使用自定义 ref 函数 时,因为我们并没有对这个数据进行响应式处理,所以页面数据并没有同步更新,这个时候我们就需要用到 customRef 来实现内部的逻辑。

第三步:调用 customRef 实现内部逻辑,按照 customRef() 的使用方法,完善 myRef()

这是 vscode 插件的提示语法,可以看到 customRef() 的完整用法。所以,我们完善一下 myRef()

javascript 复制代码
function myRef(value) {
  return customRef((track,trigger) => {
    return {
      get() {
        // ...
      },
      set() {
        // ...
      }
    }
  })
}

到了这一步是不是就很眼熟了,这不就是 ref() 函数里面的响应式么,取值调 get,修改调 set。按照想法实现一下

javascript 复制代码
function myRef(value) {
  return customRef((track,trigger) => {
    return {
      get() {
        return value
      },
      set(newValue) {
        value = newValue
      }
    }
  })
}

虽然数据发生了变更,但是页面并没有同步更新

这是因为数据只是发生了变更,但是并没有实现依赖追踪和触发更新,这个时候,我们在看看 ref() 的源码。

javascript 复制代码
get value() {
    // 收集依赖
    trackRefValue(this);
 
    // 返回响应式数据
    return this._value;
  }
 
  set value(newVal) {
 
    // 判断逻辑 
    ......
    
    // 更新数据
    this._value = this.__v_isShallow ? newVal : reactive(newVal);
 
      // 分发依赖,通知更新
      triggerRefValue(this);
      
  }

在 ref() 中,在get 中收集依赖,在 set 中分发依赖,按这个模式,我们在 customRef() 中的 get 和 set 中也应该收集或分发。而 customRef 接收的工厂函数接收 track 和 trigger 两个函数作为参数,这两个函数其实就是对应的 ref() 中的 trackRefValue() 和 triggerRefValue() ,于是完善后的代码就成了这样。

javascript 复制代码
function myRef(value) {
  return customRef((track,trigger) => {
    return {
      get() {
        track()    // 先收集依赖,告诉Vue 这个 value 值是需要被追踪的
        return value    // 然后返回被追踪的值,此时Vue底层已经对 value 实现了追踪
      },
      set(newValue) {
        value = newValue    // 先设置值,因为 value 被追踪,所以数据改变时,Vue底层是能监听到
        trigger()    // 然后分发依赖,告诉 Vue 需要更新界面
      }
    }
  })
}

实现的效果

到了这里,其实我们就完成了与第一步同样的效果:不延迟,直接同步展示。

剩下的就是实现防抖了。当数据改变时,我们通过 setTimeout 我们可以实现延迟 500ms 展示值。

javascript 复制代码
set(newValue) {
  setTimeout(() => {
    value = newValue;
    trigger();
  }, 500);
},

但是我们发现,当过快输入时,值出现了诡异的变动,会突然卡一下,这是因为,每次改变数据时,都会开启一个定时器,但是定时器却并没有清除,这就导致累计了多个定时器才会出现这种情况。

按照标准防抖的流程,那就是在一定的时间内只执行一次,如果此时重复触发,则重新开始计时。代码改进之后展示

javascript 复制代码
function myRef(value) {
  return customRef((track, trigger) => {
    let timer    // 定义变量,接收定时器
    return {
      get() {
        track();
        return value;
      },
      set(newValue) {
        clearTimeout(timer);    // 每次开启定时器之前先清除之前的定时器,防止出现错误

        timer = setTimeout(() => {
          value = newValue;
          trigger();
        }, 500);
      },
    };
  });
}

连续快速点击效果:只有在最后一次点击完成,且定时器延迟触发之后,才会展示改变后的值

慢速点击效果:每次点击都等待定时器执行完毕之后再触发下一次动作

到了这里,其实我们就完成了对依赖项跟踪和更新触发进行显式控制。可以看到,track() 应该在 get() 方法中调用,而 trigger() 应该在 set() 中调用。但是其实我们完全实控制了 track()、trigger() 的使用,包括但不限于在哪使用,是否需要使用等。

问题点

当你将 customRef 作为 prop 传递时,它可能会影响父组件和子组件之间的关系,尤其是在响应式系统的依赖追踪和更新通知方面。

案例代码

javascript 复制代码
// 自定义 ref,没有调用 track()
function useCustomRef(value) {
  return customRef((track, trigger) => ({
    get() {
      track()
      return value;
    },
    set(newValue) {
      value = newValue;
      trigger(); // 触发更新
    }
  }));
}

// 父组件
export default {
  setup() {
    const customValue = useCustomRef('Hello');
    return { customValue };
  },
  template: '<ChildComponent :propValue="customValue" />'
};

// 子组件
export default {
  props: {
    propValue: {
      type: Object,
      required: true
    }
  },
  watch: {
    propValue(newValue) {
      console.log('Prop value updated:', newValue);
    }
  },
  template: '<div>{{ propValue }}</div>'
};

1. 依赖追踪不完整

在Vue 响应式系统中 ,Vue会自动进行依赖追踪。 当父组件传递一个 ref 或响应式对象作为 prop 给子组件时,Vue 会追踪这个 prop 的依赖。

但是,customRef 可以自定义依赖追踪逻辑。如果你在 customRef 的 get 方法中没有正确调用 track(),Vue 就无法知道子组件在依赖这个 prop。这意味着,当父组件更新这个 prop 时,子组件可能无法感知到这个变化,因为依赖关系没有被正确建立。

  1. 更新通知的不一致

当你在 customRef 的 set 方法中没有正确调用 trigger(),即使 prop 在父组件中被更新,子组件也不会收到更新通知。这会导致子组件的数据与父组件不同步,从而产生 UI 不一致的问题。

3. 异步逻辑导致的延迟

如果 customRef 中包含异步逻辑(例如防抖或节流),这种延迟处理可能会导致子组件在接收 prop 时得到的是过时的数据。这在需要子组件立即响应父组件更新的场景下,可能引发状态不同步的问题。

在上面的例子中,debouncedRef 可能导致子组件在 prop 变更后并未立即更新,而是延迟更新,可能引发父子组件数据状态不同步的问题。

总结

1、customRef的作用: 创建一个自定义的 ref 函数,在其内部显式声明对其依赖追踪和更新触发的控制方式。

2、customRef 接收的工厂函数接收 track 和 trigger 两个函数作为参数,这两个函数其实就是对应的 ref() 中的 trackRefValue() 和 triggerRefValue() ,并返回一个带有 get 和 set 方法的对象。

3、一般来说,track() 应该在 get() 方法中调用,而 trigger() 应该在 set() 中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。

4、当 customRef 作为 prop 传递时,可能会影响父组件和子组件之间的关系,

  • 依赖追踪不完整
  • 更新通知的不一致
  • 异步逻辑导致的延迟
相关推荐
事圆则缓7 分钟前
Flutter 状态管理框架对比(三):Riverpod 如何连起购物车与异步价格
开发语言·javascript·flutter
linux_cfan8 分钟前
videojs v10 源代码系列解读:30 · `SerialRunner` 与 `ConcurrentRunner`
前端
liangshanbo12158 分钟前
面试题:页面需要发起大量 Ajax / HTTP 请求时,如何优化请求性能?
前端·http·ajax
lyz24685938 分钟前
vue3页面因参数未声明而报错的常犯错误
前端·javascript·vue.js
摸爬滚打的小李42 分钟前
流程图同步AI生成
前端
Hilaku1 小时前
我重写了整个项目,但没人感谢我!
前端·javascript·程序员
zach1 小时前
Vue/React SPA 打包部署后,子路由刷新 401 未登录问题彻底解决
前端·nginx·next.js
旋生万物2 小时前
素数螺旋映射 $z_n=n^{1+i}$ 的角分布统计检验与零模型对比
大数据·前端·人工智能·算法·云原生·螺旋生成论·螺旋相位
亿元程序员2 小时前
游戏引擎都没用!纯AI又上线了一款蚂蚁搬家小游戏!
前端
福兮说2 小时前
JS 正则的六个坑:带 g 的 test() 一真一假、空匹配死循环、replace 里的 $
开发语言·前端·javascript·正则表达式