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() 预期接收一个工厂函数作为参数,这个工厂函数接受 tracktrigger 两个函数作为参数,并返回一个带有 getset 方法的对象。

按照官网的例子我们一点点实现优化:创建一个自定义 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 接收的工厂函数接收 tracktrigger 两个函数作为参数,这两个函数其实就是对应的 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 可以自定义依赖追踪逻辑。如果你在 customRefget 方法中没有正确调用 track(),Vue 就无法知道子组件在依赖这个 prop。这意味着,当父组件更新这个 prop 时,子组件可能无法感知到这个变化,因为依赖关系没有被正确建立。

  1. 更新通知的不一致

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

3. 异步逻辑导致的延迟

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

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

总结

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

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

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

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

  • 依赖追踪不完整
  • 更新通知的不一致
  • 异步逻辑导致的延迟
相关推荐
小白小白从不日白12 分钟前
react 基础语法
前端·react.js
岸边的风13 分钟前
前端Excel热成像数据展示及插值算法
前端·算法·excel
不良人龍木木1 小时前
sqlalchemy FastAPI 前端实现数据库增删改查
前端·数据库·fastapi
c1tenj22 小时前
Jedis,SpringDataRedis
前端
Code成立2 小时前
HTML5中IndexedDB前端本地数据库
前端·数据库·html5·indexeddb
Code成立2 小时前
最新HTML5中的文件详解
前端·html·html5
橙子家2 小时前
前端项目通过 Nginx 发布至 Linux,并通过 rewrite 配置访问后端接口
前端
老华带你飞3 小时前
美术|基于java+vue的美术外包管理信息系统(源码+数据库+文档)
java·数据库·vue.js
计算机学姐3 小时前
基于SpringBoot+Vue的瑜伽体验课预约管理系统
java·vue.js·spring boot·后端·mysql·intellij-idea·mybatis