Ref 和React 堪称Vue 响应式API 的孪生宝石。
但这也引出了一个问题:为什么是这两个?如何选择?
我最近在开发过程中遇到了这个问题。我没有时间阅读Vue 文档,因此我查找了前4 个“ref reactive”主题来寻找答案。
从广义上讲, ref 工作得很好,但没有清楚地解释它为什么工作。列出几个场景对比,主要是基于我对Vue文档的理解和使用经验。 这显然还不够。我们可以知道发生了什么,但没有人能告诉我们原因。
当时时间很紧,所以我只能随机选择一个ref并推送代码。
然而,“自然是夺不走的,人心是有疑虑的!”如果我们不尝试更深入地理解它,说不定有一天我们又会掉进坑里,我们不能只是加班加点,慢慢地爬出来。长期解决方案。 请利用这种先进的杀戮能力并研究它。
当我遇到问题追求速度的时候,我只能直接而嚣张地寻找答案。现在事情已经平静下来了,是时候回到老派了,先阅读官网的文档,然后使用调试器分析源代码。
我仔细阅读了Vue官方文档关于响应式基础知识和详细响应式系统三遍,并在本地运行了内部演示。
看完这两篇文章,最大的好处就是我现在可以自己写参考代码和计算代码了。这是Vue 文档威力的第一次展示。竖起大拇指。
等一下,回到你的初衷:“引用还是反应?”本文档的标准答案是:
这是因为reactive() API有一些限制。
仅支持有限的值类型,即对象类型;不支持基本类型(数字、字符串、布尔值、未定义、空)。 您无法替换整个对象。如果不更换,响应将无效。 这不适合分解操作,分解后响应将会失败。
因此,建议使用ref() 作为声明反应式状态的主要API。
Vue官方文档《响应式基础》
显然这个答案并不能解决我的困惑。
如果Reactive不支持基本的数据类型,那有什么难的呢?只要包裹一层ref这样的对象,写成({value})就可以了。说到无法替换整个对象,reactive返回的Proxy本身就是一个reactive对象,所以替换ref返回的RefImpl实际上是行不通的。引用已经被替换了,但是我们不能禁用响应吗?与2类似,拆除不友好。综上所述,如果基于这种差异,我们必须将一个响应式功能强行放入两个API 中,那么Vue 就不可能达到目前全球排名前二的前端框架的地位。
这个问题没有办法解决,我来解决。
要解开铃铛,你需要系上它。唯一破译的答案必须在Vue 的源代码实现中找到。不过,源代码与短文不同,它不是像流水一样从上到下流动,而是像网络一样来回流动,所以直接阅读它会让你怀疑人生。
首先,为什么不打印ref 和reactive return 日志来感受一下它们有什么区别?
对象数据类型
基本数据类型
参考
RefImpl {_value: 代理(对象)}
RefImpl {_value: 值}
反应性的
代理(对象)
值,无响应
如果该值是对象数据类型,则ref API 返回RefImpl。这里,_value是对象代理Proxy,与反应式API返回值相同。
如果该值是基本数据类型,则ref 返回RefImpl。这里,_value是对应的基本数据类型,与reactive返回的值相同。请注意,reactive 返回的值当前是无响应的。
此时,你可以说“ref底层是reactive的实现,如果reactive更强大,就用reactive”或者“reactive是ref的子集,如果ref很大,就用ref”,有些朋友可能会想得出这样的结论那。 其实很多前端朋友都这么说,网上也有一些文章说同样的话,但总体来说赞成ref的声音比较高。
再说一次,我仍然不相信Vue 没有两个反应式API 能够为广大前端开发爱好者找到一些东西,就是因为这个原因。
基本型没什么好说的。反应式使用对象代理,并不是为了将基本类型包装在对象层中而设计的。如果你想使用reactive,只需手动将其包装成一个对象即可。使用反应式({count})。
可以看到需要扩展对象类型。从日志输出中可以看到,ref返回的RefImpl除了_value之外,还多了四个值,指向代理对象Proxy:dep、__v_isRef、__v_isShallow、_rawValue。这些是做什么用的?
要回答这些问题,您需要调试器源代码。
研究过源码的朋友都知道,如果只是看源码的话,很可能就要从头到尾看一遍。 我的做法是,“为了避免在大量代码中迷失,首先想出一个想法,明确方向,设定一个目标。通过设定目标,你可以达到事半功倍的效果。” ‘ 你可以得到它。” 而且,不要只阅读静态源代码,要有想法,在运行和调试时理解和想象它们。 “读”万卷书,不如“行”万里路。
为了理解这个想法,让我们继续上面的内容,首先看看我们如何从详细的反应式系统创建引用和计算代码实现。
让我们先举一个同时使用ref 和Computed 的小栗子。
单击上方按钮将显示已累加1 的值,单击下方按钮将显示上述累加值的2 倍的值。
好消息是,Vue 官网介绍了直接从html 导入vue.global.js csn 代码的方法,可以直接下载到本地导入。 这非常有用。那么此时,Vue源码分析就像放在口袋里一样简单。
这个例子就不多说了。继续查看代码。
注:为了节省空间,我直接去掉了样式标签。有关源代码详细信息,请参阅diyVueV0RefCompulated.html。
上面的例子中,需要完成两个特征点:
如何更新DOM,以便在单击按钮时出现新数据? countA0Ref 数据更改后,如何监控countA1CompulatedRef 并重新调用compute 中的函数来更新值?问题1官方文档没有提到这一点。我在vue.global.js源码中搜索了关键字mount和render,并尝试了几次。 我首先使用app.unmount() 和app.mount() 发现错误。然后我尝试了context.reload() 并发现它不起作用。然后我尝试了_instance.update()。我真的很幸运。
如何通过直接访问代码来获取_instance我就不详细说了。运行日志或调试器以查看更多信息。
问题2 深入探讨了响应式系统,因此必须运行文档中的示例以进一步加深您的理解和想象力。
Refs 很容易实现,只需包装一层对象并通过覆盖set 和get value 属性来监听它们即可。
这里有趣的是第三行。这意味着refObj 不需要在模板中尾随花哨的.value ,但在JS 代码中需要。你知道为什么吗?
当Vue 将模板转换为DOM 操作时,当遇到__v_isRef 为true 的对象时,它会自动添加一个可爱的.value 尾部。这相当于一些语法糖,给程序员枯燥的生活增添了一点甜蜜。
countA1CompatedRef 是如何重新计算的?
在提出问题之前,请先考虑一下。如果我要做的话,我会怎么做? 第一个想法是找到目标函数的源并重新触发执行,就像在调试器的堆栈中一样,但我到底该怎么做呢?函数执行还需要上下文参数,但是更改如何找到并重新分配给定的值?价值?
这是Vue 响应式API 的最后一步。在运行函数时主动记录副作用,因为它们在调用时很难发现。全局变量activeEffect用于记录当前执行的副作用函数(即计算函数对应的函数)。副作用函数执行完毕后,activeEffect被清零。
调用refObj.value时拦截判断当前activeEffect是否非空。如果activeEffect不为空,则表示activeEffect的副作用函数访问当前的refObj.value并记录activeEffect。
当重新分配refObj.value 时,访问阻塞期间记录的所有副作用函数都会再次执行,完成响应跟踪触发。
这应该可以帮助您理解为什么计算函数的输入函数中没有参数。事实上,这主要是为了方便直接执行副作用函数,而不是传递外部记录函数的上下文。使用参数时,建议直接在函数内传递“闭包”。
如果您细心的话,您会注意到有一个您以前从未见过的queueFlush() 函数。这里,副作用并不是在设定值截距内立即触发,而是在下一个微任务中执行。执行集重复数据删除过滤可防止副作用函数多次执行并浪费性能。
有关计算实现的初步参考和完整源代码,请参阅diy-vue.v0.global.js。
请运行并检查。
看到这里,我认为Vue 中的响应性原则应该已经很完整了。 再次回到原来的问题。 “ref还是reactive?”上面只描述了ref的实现,还没有反应。 但按理说,两者都是响应式API,它们的实现应该是相似的。
让我们使用小栗子继续我们的逐步方法,并分别定义ref(count) 和re。
active({count}),绑定到 <div> 标签展示,并且点击 count++。 点击断点 debugger 走一遍。
理解了 ref 和 computed 实现,再 debugger Vue 源码,会事半功倍,不用费多大劲就能猜出个七七八八,关键也就 3 点:
reactive 使用对象代理 Proxy 来监听属性访问变化,最大的优势是所有属性访问都能监听到,哪怕是深层对象属性变化,ref 对于基本数据类型只能监听到 value 属性变化,如果是对象数据类型,则直接调用 reactive 创建对象代理赋值给 value 属性。这也是大家所说的 ref 底层也是 reactive 实现的。reactive 因为用的是对象代理,不方便在原对象上记录额外的副作用记录信息,所以存了个全局变量 const targetMap = new WeakMap() ,数据结构是官方文档中提到的 WeakMap<target, Map<key, Set<effect>>>。ref 因为自身包了一层对象,所以副作用直接可以存储在该对象的 dep 属性里面,数据结构是 Set<effect>。如果 ref 参数是对象,如 const countObjRef = ref({count: 0}) ,即支持直接 countObjRef.value 访问修改(将会被 countObjRef 的 value 属性的 set 和 get 监听),也支持 countObjRef.value.count++(将会被 countObjRef.value 的对象代理 Proxy 监听)。 这点将非常关键,是回答 “ref 还是reactive?” 问题的突破口。其他逻辑不再赘述了,上 ref 和 reactive 实现代码
整个 ref、reactive 和 computed 完全源码详见 diy-vue.v1.global.js。
回到最初的问题,“ref 还是 reactive?”
与其说“ref 还是 reactive?”,不如说 “基础数据类型监听还是对象数据类型监听?” 这才是我一直苦苦寻找的“为什么 Vue 非要整俩响应式 API”的根源。 ref 因为内部做了两者兼顾,自然而然成了 Vue 官方推荐写法。
对这个问题,我的答案是:
如果响应数据是基本数据类型,建议使用 ref,即使多了个迷人的 .value 小尾巴,也还行。
如果是对象类型且仅内部修改这种简单场景,特别是深层对象,建议使用 reactive,毕竟 refObj.value.xxx 显然没有 reactiveObj.xxx 用的爽,使用 ref 本身也只是个壳,里面用的就是reactive,杀鸡焉用牛刀。
如果是对象类型且有重置清空这种复杂场景(常见表单和筛选项),那必须使用 ref 了,因为 reactiveObj 被重新赋值后,对象代理 Proxy 被清掉,此时已再无监听可能,最终导致响应失效。 但是对 refObj.value 重新赋值响应依旧有效,因为此时 refObj 对 value 属性的 set 和 get 监听依旧在。
当然了,如果你嫌混用麻烦且不想纠结这些技术细节,使用 ref 是相对较好的选择,一方面不会出问题,另外因为 ref 是 Vue 官网推荐写法,在多人协作开发情况下,随大流能提升代码可读性,有效降低学习成本和协作运维成本。
作者:盛书强链接:https://juejin.cn/post/7313549609179447306
原创文章,作者:小条,如若转载,请注明出处:https://www.sudun.com/ask/84996.html