| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> JavaScript知识库 -> 【Vue.js 3.0源码】响应式之内部实现原理(下) -> 正文阅读 |
|
[JavaScript知识库]【Vue.js 3.0源码】响应式之内部实现原理(下) |
自我介绍:大家好,我是吉帅振的网络日志;微信公众号:吉帅振的网络日志;前端开发工程师,工作4年,去过上海、北京,经历创业公司,进过大厂,现在郑州敲代码。 一、前言【Vue.js 3.0源码】响应式之内部实现原理(上) 中说到 Vue.js 3.0 中引入 reactive API,它可以把对象数据变成响应式,收集依赖的 get 函数,接下来我们来分析 reactive API 中需要关注的另一个内容——派发通知的过程。 二、派发通知:set 函数派发通知发生在数据更新的阶段 ,由于我们用 Proxy API 劫持了数据对象,所以当这个响应式对象属性更新的时候就会执行 set 函数。我们来看一下 set 函数的实现,它是执行 createSetter 函数的返回值:
结合上述代码来看,set 函数的实现逻辑很简单,主要就做两件事情, 首先通过 Reflect.set 求值 , 然后通过 trigger 函数派发通知 ,并依据 key 是否存在于 target 上来确定通知类型,即新增还是修改。整个 set 函数最核心的部分就是 执行 trigger 函数派发通知 ,下面我们将重点分析这个过程。我们先来看一下 trigger 函数的实现,为了分析主要流程,这里省略了 trigger 函数中的一些分支逻辑:
trigger 函数的实现也很简单,主要做了四件事情:通过 targetMap 拿到 target 对应的依赖集合 depsMap;创建运行的 effects 集合;根据 key 从 depsMap 中找到对应的 effects 添加到 effects 集合;遍历 effects 执行相关的副作用函数。 所以每次 trigger 函数就是根据 target 和 key ,从 targetMap 中找到相关的所有副作用函数遍历执行一遍。在描述依赖收集和派发通知的过程中,我们都提到了一个词:副作用函数,依赖收集过程中我们把 activeEffect(当前激活副作用函数)作为依赖收集,它又是什么?接下来我们来看一下副作用函数的庐山真面目。 三、副作用函数介绍副作用函数前,我们先回顾一下响应式的原始需求,即我们修改了数据就能自动执行某个函数,举个简单的例子:
可以看到,这里我们定义了响应式对象 counter,然后我们在 logCount 中访问了 counter.num,我们希望通过执行 count 函数修改 counter.num 值的时候,能自动执行 logCount 函数。按我们之前对依赖收集过程的分析,如果这个 logCount 就是 activeEffect 的话,那么就可以实现需求,但显然是做不到的,因为代码在执行到 console.log(counter.num)这一行 的时候,它对自己在 logCount 函数中的运行是一无所知的。 那么该怎么办呢?其实只要我们运行 logCount 函数前,把 logCount 赋值给 activeEffect 就好了,如下:
顺着这个思路,我们可以利用高阶函数的思想,对 logCount 做一层封装,如下:
这里,wrapper 本身也是一个函数,它接受 fn 作为参数,返回一个新的函数 wrapped,然后维护一个全局的 activeEffect,当 wrapped 执行的时候,把 activeEffect 设置为 fn,然后执行 fn 即可。这样当我们执行 wrappedLog 后,再去修改 counter.num,就会自动执行 logCount 函数了。实际上 Vue.js 3.0 就是采用类似的做法,在它内部就有一个 effect 副作用函数,我们来看一下它的实现:
结合上述代码来看,effect 内部通过执行 createReactiveEffect 函数去创建一个新的 effect 函数,为了和外部的 effect 函数区分,我们把它称作 reactiveEffect 函数,并且还给它添加了一些额外属性(我在注释中都有标明)。另外,effect 函数还支持传入一个配置参数以支持更多的 feature,我们这里就不展开了,在后续的章节会详细分析。 接着说,这个 reactiveEffect 函数就是响应式的副作用函数,当执行 trigger 过程派发通知的时候,执行的 effect 就是它。按我们之前的分析,这个 reactiveEffect 函数只需要做两件事情: 把全局的 activeEffect 指向它 , 然后执行被包装的原始函数 fn 即可 。但实际上它的实现要更复杂一些,首先它会判断 effect 的状态是否是 active,这其实是一种控制手段,允许在非 active 状态且非调度执行情况,则直接执行原始函数 fn 并返回,在后续学习完侦听器后你会对它的理解更加深刻。 接着判断 effectStack 中是否包含 effect,如果没有就把 effect 压入栈内。之前我们提到,只要设置 activeEffect = effect 即可,那么这里为什么要设计一个栈的结构呢?其实是考虑到以下这样一个嵌套 effect 的场景:
我们每次执行 effect 函数时,如果仅仅把 reactiveEffect 函数赋值给 activeEffect,那么针对这种嵌套场景,执行完 effect(logCount2) 后,activeEffect 还是 effect(logCount2) 返回的 reactiveEffect 函数,这样后续访问 counter.num 的时候,依赖收集对应的 activeEffect 就不对了,此时我们外部执行 count 函数修改 counter.num 后执行的便不是 logCount 函数,而是 logCount2 函数,最终输出的结果如下:
而我们期望的结果应该如下:
因此针对嵌套 effect 的场景,我们不能简单地赋值 activeEffect,应该考虑到函数的执行本身就是一种入栈出栈操作,因此我们也可以设计一个 effectStack,这样每次进入 reactiveEffect 函数就先把它入栈,然后 activeEffect 指向这个 reactiveEffect 函数,接着在 fn 执行完毕后出栈,再把 activeEffect 指向 effectStack 最后一个元素,也就是外层 effect 函数对应的 reactiveEffect。 这里我们还注意到一个细节,在入栈前会执行 cleanup 函数清空 reactiveEffect 函数对应的依赖 。在执行 track 函数的时候,除了收集当前激活的 effect 作为依赖,还通过 activeEffect.deps.push(dep) 把 dep 作为 activeEffect 的依赖,这样在 cleanup 的时候我们就可以找到 effect 对应的 dep 了,然后把 effect 从这些 dep 中删除。cleanup 函数的代码如下所示:
为什么需要 cleanup 呢?如果遇到这种场景:
结合代码可以知道,这个组件的视图会根据 showMsg 变量的控制显示 msg 或者一个随机数,当我们点击 Switch View 的按钮时,就会修改这个变量值。假设没有 cleanup,在第一次渲染模板的时候,activeEffect 是组件的副作用渲染函数,因为模板 render 的时候访问了 state.msg,所以会执行依赖收集,把副作用渲染函数作为 state.msg 的依赖,我们把它称作 render effect。然后我们点击 Switch View 按钮,视图切换为显示随机数,此时我们再点击 Toggle Msg 按钮,由于修改了 state.msg 就会派发通知,找到了 render effect 并执行,就又触发了组件的重新渲染。 但这个行为实际上并不符合预期,因为当我们点击 Switch View 按钮,视图切换为显示随机数的时候,也会触发组件的重新渲染,但这个时候视图并没有渲染 state.msg,所以对它的改动并不应该影响组件的重新渲染。因此在组件的 render effect 执行之前,如果通过 cleanup 清理依赖,我们就可以删除之前 state.msg 收集的 render effect 依赖。这样当我们修改 state.msg 时,由于已经没有依赖了就不会触发组件的重新渲染,符合预期。 至此,我们从 reactive API 入手了解了整个响应式对象的实现原理。除了 reactive API,Vue.js 3.0 还提供了其他好用的响应式 API,接下来我们一起分析一些常用的。 readonly API 如果用 const 声明一个对象变量,虽然不能直接对这个变量赋值,但我们可以修改它的属。如果我们希望创建只读对象,不能修改它的属性,也不能给这个对象添加和删除属性,让它变成一个真正意义上的只读对象。
显然,想实现上述需求就需要劫持对象,于是 Vue.js 3.0 在 reactive API 的基础上,设计并实现了 readonly API。我们先来看一下 readonly 的实现:
其实 readonly 和 reactive 函数的主要区别,就是执行 createReactiveObject 函数时的参数 isReadonly 不同。我们来看这里的代码,首先 isReadonly 变量为 true,所以在创建过程中会给原始对象 target 打上一个 __v_readonly 的标识。另外还有一个特殊情况,如果 target 已经是一个 reactive 对象,就会把它继续变成一个 readonly 响应式对象。其次就是 baseHandlers 的 collectionHandlers 的区别,我们这里仍然只关心基本数据类型的 Proxy 处理器对象,readonly 函数传入的 baseHandlers 值是 readonlyHandlers。 接下来,我们来看一下其中 readonlyHandlers 的实现:
readonlyHandlers 和 mutableHandlers 的区别主要在 get、set 和 deleteProperty 三个函数上。很显然,作为一个只读的响应式对象,是不允许修改属性以及删除属性的,所以在非生产环境下 set 和 deleteProperty 函数的实现都会报警告,提示用户 target 是 readonly 的。 接下来我们来看一下其中 readonlyGet 的实现,它其实就是 createGetter(true) 的返回值:
可以看到,它和 reactive API 最大的区别就是不做依赖收集了,这一点也非常好理解,因为它的属性不会被修改,所以就不用跟踪它的变化了。到这里,readonly API 就介绍完了,接下来我们分析一下另一个常用的响应式 API:ref。 ref API 通过前面的分析,我们知道 reactive API 对传入的 target 类型有限制,必须是对象或者数组类型,而对于一些基础类型(比如 String、Number、Boolean)是不支持的。但是有时候从需求上来说,可能我只希望把一个字符串变成响应式,却不得不封装成一个对象,这样使用上多少有一些不方便,于是 Vue.js 3.0 设计并实现了 ref API。
我们先来看一下 ref 的实现:
可以看到,函数首先处理了嵌套 ref 的情况,如果传入的 rawValue 也是 ref,那么直接返回。接着对 rawValue 做了一层转换,如果 rawValue 是对象或者数组类型,那么把它转换成一个 reactive 对象。最后定义一个对 value 属性做 getter 和 setter 劫持的对象并返回,get 部分就是执行 track 函数做依赖收集然后返回它的值;set 部分就是设置新值并且执行 trigger 函数派发通知。 四、总结好的,到这里我们这一节的学习也要结束啦,我希望通过这节课的学习,你能搞明白响应式 API 的实现原理,知道什么时候收集依赖,什么时候派发更新,以及副作用函数的作用和设计原理。我还希望你能知道 reactive、readonly、ref 三种 API 的区别和各自的使用场景,这样你就可以在今后的开发中对它们应用自如啦。 |
|
JavaScript知识库 最新文章 |
ES6的相关知识点 |
react 函数式组件 & react其他一些总结 |
Vue基础超详细 |
前端JS也可以连点成线(Vue中运用 AntVG6) |
Vue事件处理的基本使用 |
Vue后台项目的记录 (一) |
前后端分离vue跨域,devServer配置proxy代理 |
TypeScript |
初识vuex |
vue项目安装包指令收集 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/23 13:45:40- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |