Vue3 源码解读系列(七)——侦听器

侦听器

watch

侦听器是当侦听的对象或者函数发生了变化则自动执行某个回调函数。

侦听器的内部设计:侦听响应式数据的变化,内部创建 effect runner,首次执行 runner 做依赖收集,然后在数据发生变化后,以某种调度方式去执行回调函数。

调用侦听器的两种方式:

  1. 通过 Composition API watch

    watch(sourch, callback, options?)
    
  2. 通过 vm.$watch

    vm.$watch(sourch, callback, options?)
    

侦听器主要做了5件事:

  1. 标准化 source
    • 如果 source 是一个数组,则遍历该数组的每一项,判断该项为 refreactivefunction 进行分别的处理
    • 如果 sourceref 对象,则创建一个访问 source.valuegetter 函数
    • 如果 sourcereactive 对象,则创建一个访问 sourcegetter 函数,并设置 deeptrue
    • 如果 source 是一个函数,则会进一步判断第二个参数 cb 是否存在,对于 watch API 来说,cb 是一定存在且是一个回调函数,getter 就是一个简单的对 source 函数封装的函数
    • 如果 source 不满足上述条件,则在非生产环境下报警告,提示 source 类型不合法
  2. 构造 applyCb 回调函数
  3. 创建 scheduler 时序执行函数
    • flushsync 时,表示它是一个同步 watcher,即当数据变化时同步执行回调函数
    • flushpre 时,回调函数通过 queueJob 的方式在组件更新之前执行,如果组件还没挂载,则同步执行确保回调函数在组件挂载之前执行
    • 如果没设置 flush,回调函数通过 queuePostRenderEffect 的方式在组件更新之后执行
  4. 创建 effect 副作用函数,用于依赖收集
  5. 返回侦听器销毁函数
/*** watch 侦听器的实现*/
function watch(source, cb, options) {// 如果传入的 cb 参数不是函数,则警告if ((process.env.NODE_ENV !== 'production') && !isFunction(cb)) {warn(/* ... */)}return doWatch(source, cb, options)
}/*** 侦听函数* doWatch 主要做了 5 件事:* 1、标准化 source* 2、构造 applyCb 回调函数* 3、创建 scheduler 时序执行函数* 4、创建 effect 副作用函数* 5、返回侦听器销毁函数*/
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {// 1、标准化 source/*** source 的标准化根据 source 的类型分类:* · 如果 source 是 ref 对象,则创建一个访问 source.value 的 getter 函数* · 如果 source 是 reactive 对象,则创建一个访问 source 的 getter 函数,并设置 deep 为 true* · 如果 source 是一个函数,则会进一步判断第二个参数 cb 是否存在,对于 watch API 来说,cb 是一定存在且是一个回调函数,getter 就是一个简单的对 source 函数封装的函数* · 如果 source 不合法,则在非生产环境下报警告,提示 source 类型不合法*/// source 不合法时报警告的函数const warnInvalidSource = (s) => {warn(/* ... */)}const instance = currentInstance // 当前组件实例let getter// source 为数组,遍历判断每一个的类型if (isArray(source)) {getter = () => source.map(s => {// 如果是 ref,则返回访问 s.value 的 getterif (isRef(s)) {return s.value}// 如果是 reactive,则返回访问 s 的 getter,并递归执行侦听else if (isReactive(s)) {return traverse(s)}// 如果是函数,则返回访问 s 的返回的响应式对象的 getterelse if (isFunction(s)) {return callWithErrorHandling(s, instance, 2/* WATCH_GETTER */)}// 如果不合法,则在非生产环境下报警告else {(process.env.NODE_ENV !== 'production') && warnInvalidSource(s)}})}// source 为 ref,则返回访问 source.value 的 getterelse if (isRef(source)) {getter = () => source.value}// source 为 reactive,则返回访问 source 的 getter,并且设置 deep:trueelse if (isReactive(source)) {getter = () => sourcedeep = true}// source 为函数,则进一步判断是否存在回调函数 cb/*** 如果存在,则返回访问 source 的返回的响应式对象的 getter* 如果不存在,说明是省略 source,因为 cb 是一定存在的,则执行 watchEffect 的逻辑,source 为依赖的响应式对象*/else if (isFunction(source)) {// getter with cbif (cb) {getter = () => callWithErrorHandling(source, instance, 2/* WATCH_GETTER */)}// getter without cbelse {// watchEffect 的逻辑}}// source 不合法,则在非生产环境下报警告else {getter = NOOP;(process.env.NODE_ENV !== 'production') && warnInvalidSource(source)}// 如果存在 cb,且 deep:true,则递归侦听每一个子属性if (cb && deep) {const baseGetter = gettergetter = () => traverse(baseGetter())}// 2、构造 applyCb 回调函数/*** cb 的三个参数:* 1、newValue - 新值* 2、oldValue - 旧值* 3、onCleanup - 无效回调函数*/let cleanup// 注册无效回调函数const onInvalidate = (fn) => {cleanup = runner.options.onStop = () => {callWithErrorHandling(fn, instance, 4/* WATCH_CLEANUP */)}}let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE/* {} */ // 旧值初始值// applyCb 回调函数	const applyCb = cb ? () => {// 若组件被销毁,则直接返回if (instance && instance.isUnmounted) return// 执行 runner 求得新值const newValue = runner()if (deep || hasChanged(newValue, oldValue)) {// 执行清理函数if (cleanup) {cleanup()}callWithAsyncErrorHandling(cb, instance, 3/* WATCH_CALLBACK */, [newValue,oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, // 第一次更改时传递旧值为 undefinedonInvalidate])// 更新旧值,用于下次比对oldValue = newValue}} : void 0// 3、创建 scheduler 时序执行函数const invoke = (fn) => fn()// scheduler 的作用是根据某种调度方式去执行某种函数,在 watch API 中主要影响的是回调函数的执行方式,而 scheduler 又受到第三个参数 options 的 flush 属性的影响/*** 当 flush 为 sync 时,表示它是一个同步 watcher,即当数据变化时同步执行回调函数* 当 flush 为 pre 时,回调函数通过 queueJob 的方式在组件更新之前执行,如果组件还没挂载,则同步执行确保回调函数在组件挂载之前执行* 如果没设置 flush,回调函数通过 queuePostRenderEffect 的方式在组件更新之后执行*/let scheduler// 同步if (flush === 'sync') {scheduler = invoke}// 进入异步队列,组件更新前执行else if (flush === 'pre') {scheduler = job => {// 如果组件已经挂载或销毁,则在组件更新之前执行if (!instance || instance.isMounted) {queueJob(job)}// 如果组件还没挂载,则同步执行确保回调函数在组件挂载前执行else {job()}}}// 进入异步队列,组件更新后执行else {scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)}// 4、创建 effect 副作用函数const runner = effect(getter, {lazy: true, // 延时执行computed: true, // computed effect 可以优先于普通的 effect 先运行,比如组件渲染的 effectonTrack,onTrigger,scheduler: applyCb ? () => scheduler(applyCb) : scheduler})// 在组件实例中记录这个 effectrecordInstanceBoundEffect(runner)// 存在 cbif (applyCb) {// 配置了 options: { immediate: true },直接执行if (immediate) {applyCb()}// 没有配置 options: { immediate: true },在指定时机执行else {oldValue = runner()}}// 没有 cb 的情况else {runner()}// 5、返回侦听器销毁函数return () => {// 执行 stop 函数让 runner 失活,这样可以停止对数据的侦听stop(runner)if (instance) {// 移除组件 effects 对这个 runner 的引用remove(instance.effects, runner)}}function stop(effect) {if (effect.active) {cleanup(effect)if (effect.options.onStop) {effect.options.onStop()}effect.active = false}}
}
/*** 异步任务队列的设计* 当前 flush 不是 sync 时,把回调函数执行的任务推到一个异步任务队列中执行*/
function queuePostRenderEffect() {const queue = [] // 异步任务队列const postFlushCbs = [] // 异步任务队列任务执行完后执行的回调函数队列const p = Promise.resolve()let isFlushing = false // 是否正在执行任务队列let isFlushPending = false // 是否在等待 nextTick 执行 flushJobs// 添加到下一个 Tick(宏任务执行的周期) 队列function nextTick(fn) {// 通过 promise.then() 实现异步return fn ? p.then(fn) : p}// 添加异步任务到执行队列function queueFlush() {if (!isFlushing && !isFlushPending) {isFlushPending = truenextTick(flushJobs)}}// 添加任务到队列function queueJob(job) {if (!queue.includes(job)) {queue.push(job)queueFlush()}}// 添加回调函数到队列function queuePostFlushCb(cb) {// 如果不是数组,则直接 pushif (!isArray(cb)) {postFlushCbs.push(cb)}// 如果是数组,则拍平后 pushelse {postFlushCbs.push(...cb)}queueFlush()}
}const getId = (job) => (job.id == null ? Infinity : job.id)/*** 执行异步任务*/
function flushJobs(seen) {isFlushPending = falseisFlushing = truelet jobif ((process.env.NODE_ENV !== 'production')) {seen = seen || new Map()}// 异步任务队列 queue 从小到大排序/* 原因:1、保证组件更新的顺序:创建组件的过程是父->子,创建组件副作用函数也是父->子,因此父组件的副作用渲染函数的 effect.id 小于子组件2、确保子组件在父组件的更新过程中被卸载就不更新自身了*/queue.sort((a, b) => getId(a) - getId(b))while ((job = queue.shift()) !== undefined) {if (job === null) continue// 在非生产环境下检测是否有循环更新,即在侦听器的回调函数中更改了依赖响应式对象的值,从而导致死循环if ((process.env.NODE_ENV !== 'production')) {checkRecursiveUpdates(seen, job)}callWithErrorHandling(job, null, 14/* SCHEDULER */)}// 遍历执行异步任务队列flushPostFlushCbs(seen)isFlushing = false// 一些 postFlushCb 执行过程中会再次添加异步任务,递归 flushJobs 直到把它们都执行完毕if (queue.length || postFlushCbs.length) {flushJobs(seen)}
}/*** 遍历执行所有推入到 postFlushCbs 的函数*/
function flushPostFlushCbs(seen) {if (postFlushCbs.length) {// 在 cb 的执行过程中可能会修改 postFlushCbs,因此通过拷贝副本的方式以防止收到其影响const cbs = [...new Set(postFlushCbs)]postFlushCbs.length = 0if ((process.env.NODE_ENV !== 'production')) {seen = seen || new Map()}for (let i = 0; i < cbs.length; i++) {if ((process.env.NODE_ENV !== 'production')) {checkRecursiveUpdates(seen, cbs[i])}}}
}/*** 检测是否存在循环更新*/
const RECURSION_LIMIT = 100 // 循环的最大次数
function checkRecursiveUpdates(seen, fn) {// 第一次添加 fnif (!seen.has(fn)) {seen.set(fn, 1)}// 多次添加 fnelse {const count = seen.get(fn)// 超出限制次数,则报错if (count > RECURSION_LIMIT) {throw new Error(/* 报错内容 */)}// 没有超出限制次数,则计数 +1else {seen.set(fn, count + 1)}}
}

优化:只使用一个变量

从功能上讲,isFlushPending 和 isFlushing 的作用主要有两点:

  1. 在一个 Tick 内可以多次添加任务到队列中,但是任务队列会在 nextTick 后执行
  2. 在任务队列的过程中,也可以添加新的任务到队列中,在当前 Tick 去执行剩余的任务队列

优化思路:

  • 去掉 isFlushPending 变量,默认为 true 可以执行异步任务,执行完毕后再设置为 false,禁止执行异步任务

优化后的代码:

function queueFlush() {if (!isFlushing) {isFlushing = truenextTick(flushJobs)}
}function flushJobs(seen) {let jobif ((process.env.NODE_ENV !== 'production')) {seen = seen || new Map()}queue.sort((a, b) => getld(a) - getld(b))while ((job = queue.shift()) !== undefined) {if (job === null) {continue}if ((process.env.NODE_ENV !== 'production')) {checkRecursiveUpdates(seen, job)}callWithErrorHandling(job, null, 14/* SCHEDULER */)}flushPostFlushCbs(seen)if ((queue.length || postFlushCbs.length)) {flushJobs(seen)}isFlushing = false
}

watchEffect

watchEffect 的作用是注册一个副作用函数,副作用函数内部可以访问到响应式对象,当内部响应式对象变化后再立即执行这个函数。

与 watch 的区别:

  1. 侦听源不同
    • watch 手动设置侦听源
    • watchEffect 自动侦听依赖响应式对象
  2. 是否懒执行
    • watch 是懒执行,即初次不会执行,需要设置 options: { immediate: true } 才会立即执行
    • watchEffect 会立即执行
/*** watchEffect 侦听器*/
function watchEffect(effect, options) {return doWatch(effect, null, options)
}/*** 侦听*/
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {instance = currentlnstancelet getter // source 包装函数if (isFunction(source)) {getter = () => {// 判断组件实例是否销毁if (instance && instance.isUnmounted) return// 执行清理函数if (cleanup) {cleanup()}// 执行 source 函数,传入 onInvalide 作为参数return callWithErrorHandling(source, instance, 3/* WATCH_CALLBACK */, [onInvalidate])}}let cleanup // 无效回调函数const onInvalidate = (fn) => {cleanup = runner.options.onStop = () => {callWithErrorHandling(fn, instance, 4/* WATCH_CLEANUP */)}}let scheduler// 创建 schedulerif (flush === 'sync') {scheduler = invoke} else if (flush === 'pre') {sheduler = job => {if (!instance || instance.isMounted) {queueJob(job)} else {job()}}} else {scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)}// 创建 runnerconst runner = effect(getter, {lazy: true,computed: true,onTrack,onTrigger,scheduler})recordInstanceBoundEffect(runner);// 立即执行 runnerrunner();// 返回销毁函数return () => {stop(runner);if (instance) {remove(instance.effects, runner);}}
}

问题:在组件中创建的自定义 watcher,在组件销毁的时候会被销毁吗,是如何做的呢?

答:会,通过访问对象属性时的回调函数

问题:动态创建的 watcher 会销毁吗?

答:不会,需要手动清除。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/150535.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ESP32C3小飞控调试

ESP32C3小飞控调试 - 1 ESP32C3小飞控板赶在国庆节前发出打样&#xff0c;假期后上班就收到了样板&#xff0c;但是迟迟没有动手调试&#xff0c;这两天终于抽出时间调试了&#xff0c;调试过程还算顺利&#xff0c;基本没有遇到什么大问题&#xff0c;下面记录一下调试过程。…

什么是轻量应用服务器?腾讯云轻量服务器可以干什么?

腾讯云轻量应用服务器&#xff08;TencentCloud Lighthouse&#xff09;是新一代开箱即用、面向轻量应用场景的云服务器产品&#xff0c;轻量应用服务器可用于搭建中小型网站、Web应用、博客、论坛、小程序/小游戏、电商、云盘/图床、云端开发测试和学习环境等轻量级、中低负载…

微信可以注册小号啦,看看怎么操作

微信支持同一手机号绑定两个账号啦&#xff01; 生活号和工作号可以分开啦&#xff5e;实用又简单&#xff01; 详细步骤如下&#xff1a; ①点击微信-我的-设置 ②点击“切换账号” ③点击“添加账号” ④点击“注册新账号” ⑤点击“通过当前微信的手机号辅助注册” ⑥安…

《 机器人基础 》期末试卷(A)

一、填空题&#xff08;30分&#xff0c;每空2分&#xff09; 1. 按照相机的工作方式&#xff0c;机器人常用相机分为1&#xff09;__ 单目摄像头 2&#xff09;__ 双目摄像头 _ 3&#xff09;_深度摄像头_ 三类。 2. 度量地图强调…

建议收藏《Verilog代码规范笔记_华为》

华为verilog编程规范是坊间流传出来华为内部的资料&#xff0c;其贴合实际工作需要&#xff0c;是非常宝贵的资料&#xff0c;希望大家善存。至于其介绍&#xff0c;在此不再赘述&#xff0c;大家可看下图详细了解&#xff0c;感兴趣的可私信移知教育老师领取《Verilog代码规范…

超长圆钢在线直线度检测 告别手工测量时代

圆钢的直线度指的是它的表面形状是否呈现出直线。直线度是圆钢的重要品质要求之一&#xff0c;与其物理性能密切相关。在工业制造中&#xff0c;如果圆钢的直线度不达标&#xff0c;就会影响其后续的加工和使用效果&#xff0c;严重时甚至会造成损失。 超长圆钢的检测&#xff…

4-4有人写了一个函数

#include<stdio.h> int main(){int x,y;for(int i0;i<3;i){printf("输入x:");scanf("%d",&x);if(x<0){y(-1);printf("x%d的结果是y%d\n",x,y);}else if(x>0){y1;printf("x%d的结果是y%d\n",x,y);}else{y0;printf(&…

/main/binary-i386/Packages 404 File not found

在Ubuntu中使用本地源安装软件时提示以下错误&#xff1a; ......正在读取软件包列表... 完成 E: 无法下载 http://192.168.1.1:8000/dists/Jammy/main/binary-i386/Packages 404 File not found [IP: 192.168.1.1 8000] E: 无法下载 http://192.168.1.1:8000/dists/Jammy-upd…

多表查询内连接、外连接、子查询、笛卡尔积

1.1 多表查询&#xff1a;从多张表中进行查询 仅仅使用select * from tb_emp,tb_dept查询语句会显示如下结果&#xff08;笛卡尔积&#xff1a;两个表的所有的组合情况&#xff09;其中很大部分数据是冗余的。 1.2内连接&#xff1a;相当于查询A,B表交集部分的数据&#xff08…

作为电子信息工程的学生,学完stm32后,是应该学fpga还是Linux?

作为电子信息工程的学生&#xff0c;学完stm32后&#xff0c;是应该学fpga还是Linux? 我自己也是电子信息工程专业出身&#xff0c;FPGA和Linux都是我们大三的专业课&#xff0c;都学过基础也做了基础 的项目&#xff0c;我自己的选择是Linux。最近很多小伙伴找我&#xff0c…

预包装食品备案与食品经营许可证两者的关系

在食品行业中&#xff0c;预包装食品备案和食品经营许可证是两个重要的概念。它们之间存在一定的关系&#xff0c;但又不完全相同。本文将详细介绍两者的定义、区别和联系。 一、预包装食品备案 预包装食品备案&#xff0c;是指对预包装食品的生产者或进口商进行备案登记的一种…

css制作瀑布流布局

CSS制作瀑布流布局的步骤如下&#xff1a; HTML结构&#xff1a;使用无序列表ul和列表项li来创建网格布局。 <ul class"grid"><li><img src"image1.jpg"></li><li><img src"image2.jpg"></li><l…

【kafka】使用docker启动kafka

1.环境准备 docker拉取zookeeper镜像 docker pull zookeeper:3.4.14 创建zookeeper容器&#xff0c;默认端口号为2181 docker run -d --name zookeeper -p 2181:2181 zookeeper:3.4.14 拉取kafka镜像 docker pull wurstmeister/kafka:2.12-2.3.1 创键kafka容器&#xff…

OpenCV [c++](图像处理基础示例小程序汇总)

OpenCV [c++](图像处理基础示例小程序汇总) 推荐 原创 NCUTer 2023-04-04 14:18:49 文章标签 Image 图像处理 文章分类 计算机视觉 人工智能 在51CTO的第一篇博文 阅读数1467 一、图像读取与显示 #include<opencv2/opencv.hpp> #include<iostream>using…

git修改commit历史提交时间、作者

1、修改最近的几条记录&#xff0c;进入提交记录列表&#xff0c;修改提交记录模式 git rebase -i HEAD~3 // 修改最近的三条记录&#xff0c;顺序排列按提交时间升序 指令说明&#xff1a; pick&#xff1a;保留该commit&#xff08;缩写:p&#xff09; reword&#xff1a…

迎接“全全闪”时代 星辰天合发布星海架构和星飞产品

11 月 17 日&#xff0c;北京市星辰天合科技股份有限公司&#xff08;简称&#xff1a;XSKY星辰天合&#xff09;在北京首钢园举办了主题为“星星之火”的 XSKY 星海全闪架构暨星飞存储发布会&#xff0c;到场嘉宾共同见证了全新的分布式全闪架构“星海&#xff08;XSEA&#x…

redis非关系型数据库

redis非关系型数据库&#xff0c;缓存型数据库 关系型数据库和非关系型数据库的区别 关系型数据库 关系型数据库是一个结构化的数据库。 记录方式&#xff1a;行和列 行的作用&#xff1a;记录对象的属性 列的作用&#xff1a;声明对象 表与表之间是有关联的&#xff1a…

代码模版-实现form表单输入框和label统一对齐,vue+elementui

文章目录 背景代码 背景 通过 vue elementUI 实现 form 表单中输入框还有 label 统一对齐 代码 el-form 中每个 el-form-item 都是一个单独的小项目&#xff0c;默认 el-form-item 都是换行的&#xff0c;除非加上:inline"true" 属性&#xff0c;就是表示弄成一…

Windows通过ssh连接远程服务器并进入docker容器

Windows操作系统可以使用vscode通过ssh远程连接ubuntu服务器&#xff1a; 首先下载安装vscode&#xff0c;然后安装插件Remote-SSH&#xff1a; 通过ctlshiftP可以打开Remote-SSH&#xff1a;connect to host&#xff1b; 输入ssh Userhostname -p port host和hostname对应的是…