当前位置: 首页 > article >正文

customRef 与 ref

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

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 定义数据

<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 函数,并且使用它

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()

function myRef(value) {
  return customRef((track,trigger) => {
    return {
      get() {
        // ...
      },
      set() {
        // ...
      }
    }
  })
}

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

function myRef(value) {
  return customRef((track,trigger) => {
    return {
      get() {
        return value
      },
      set(newValue) {
        value = newValue
      }
    }
  })
}

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

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

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() ,于是完善后的代码就成了这样。

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 展示值。

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

 

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

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

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 传递时,它可能会影响父组件和子组件之间的关系,尤其是在响应式系统的依赖追踪和更新通知方面。

案例代码

// 自定义 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 时,子组件可能无法感知到这个变化,因为依赖关系没有被正确建立。

2. 更新通知的不一致

当你在 customRefset 方法中没有正确调用 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 传递时,可能会影响父组件和子组件之间的关系,

  • 依赖追踪不完整
  • 更新通知的不一致
  • 异步逻辑导致的延迟

http://www.kler.cn/a/284344.html

相关文章:

  • 2023年MathorCup数学建模B题城市轨道交通列车时刻表优化问题解题全过程文档加程序
  • 大数据新视界 -- 大数据大厂之 Impala 存储格式转换:从原理到实践,开启大数据性能优化星际之旅(下)(20/30)
  • 【HarmonyOS NEXT】一次开发多端部署(以轮播图、Tab栏、列表为例,配合栅格布局与媒体查询,进行 UI 的一多开发)
  • Jmeter性能测试 -3数据驱动实战
  • HelloMeme 上手即用教程
  • 如何用C#和Aspose.PDF实现PDF转Word工具
  • docker仓库的工作原理
  • Apache CloudStack Official Document 翻译节选(十)
  • 零基础转行学网络安全怎么样?
  • sheng的学习笔记-AI-基于分歧的方法
  • 高性价比百元学生党蓝牙耳机怎么选?2024四款年度耳机推荐揭秘!
  • redis作为缓存,mysql的数据如何与redis同步
  • 力扣52-最大子序和(java详细题解)
  • AI产品经理系列:如何应对AI时代?
  • 设置 Nginx、MySQL 日志轮询
  • Java-树形图工具类TreeUtil
  • 网通产品硬件设计工程师:百兆超薄网络隔离变压器您知道的有多少呢?
  • 【PyCharm激活码】2024年最新pycharm专业版激活码+安装教程!
  • 【Android】使用 ADB 查看 Android 设备的 CPU 使用率
  • 项目技巧二
  • R语言股价跳跃点识别:隐马尔可夫hmm和 GARCH-Jump对sp500金融时间序列分析
  • “添加”业务功能开发
  • Qt 杨帆起航
  • 【分布式定时任务】XXL-JOB_2.4.1部署与实战
  • 解决Element-ui中Table表格里的show-overflow-tooltip不兼容safari浏览器问题
  • vue-admin-template pan版使用方法