范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

解读ahooks源码系列DOM篇(二)

  前言
  本文是 ahooks 源码系列的第二篇,往期文章:https://www.toutiao.com/item/7210967895680057867/
  本文主要解读 useEventTarget、useExternal、useTitle、useFavicon、useFullscreen、useHover 源码实现useEventTarget
  常见表单控件(通过 e.target.value 获取表单值) 的 onChange 跟 value 逻辑封装,支持自定义值转换和重置功能。
  官方文档export interface Options {   initialValue?: T; // 初始值   transformer?: (value: U) => T; // 自定义回调值的转化 } 复制代码基本用法
  import React from "react"; import { useEventTarget } from "ahooks";  export default () => {   const [value, { reset, onChange }] = useEventTarget({ initialValue: "this is initial value" });    return (                           ); }; 复制代码使用场景
  适用于较为简单的表单受控控件(如 input 输入框)管理实现思路监听表单的 onChange 事件,拿到值后更新 value 值支持自定义回调值的转化,对外暴露 value 值、onChange 和 reset 方法核心实现
  这个实现比较简单,这里结尾代码有个as const,它表示强制 TypeScript 将变量或表达式的类型视为不可变的
  具体可以看下这篇文章: 杀手级的 TypeScript 功能:const 断言function useEventTarget(options?: Options) {   const { initialValue, transformer } = options || {};   const [value, setValue] = useState(initialValue);    const transformerRef = useLatest(transformer);    const reset = useCallback(() => setValue(initialValue), []);    const onChange = useCallback((e: EventTarget) => {     const _value = e.target.value;     if (isFunction(transformerRef.current)) {       return setValue(transformerRef.current(_value));     }     // no transformer => U and T should be the same     return setValue(_value as unknown as T);   }, []);    return [     value,     {       onChange,       reset,     },   ] as const; // 将数组变为只读元组,可以确保其内容不会在其声明和函数调用之间发生变化 } 复制代码
  完整源码useExternal
  动态注入 JS 或 CSS 资源,useExternal 可以保证资源全局唯一。
  官方文档基本用法import React from "react"; import { useExternal } from "ahooks";  export default () => {   const status = useExternal("/useExternal/test-external-script.js", {     js: {       async: true,     },   });    return (     <>       

Status: {status}

Response: {status === "ready" ? window.TEST_SCRIPT?.start() : "-"} ); }; 复制代码实现思路   原理:通过 script 标签加载 JS 资源 / 创建 link 标签加载 CSS 资源,再通过创建标签返回的 Element 元素监听 load 和 error 事件 获取加载状态正则判断传入的路径 path 是 JS 还是 CSS加载 CSS/JS:创建 link/script 标签传入 path,支持传入 link/script 标签支持的属性,添加到 head/body 中,并返回 Element 元素与加载状态;这里需判断标签路径匹配是否存在,存在则返回上一次结果,以保证资源全局唯一利用创建标签返回的 Element 元素监听 load 和 error 事件,并在回调中改变加载状态核心实现   主体实现结构:export interface Options { type?: "js" | "css"; js?: Partial; css?: Partial; } const useExternal = (path?: string, options?: Options) => { const [status, setStatus] = useState(path ? "loading" : "unset"); const ref = useRef(); useEffect(() => { if (!path) { setStatus("unset"); return; } const pathname = path.replace(/[|#].*$/, ""); if (options?.type === "css" || (!options?.type && /(^css!|.css$)/.test(pathname))) { const result = loadCss(path, options?.css); } else if (options?.type === "js" || (!options?.type && /(^js!|.js$)/.test(pathname))) { const result = loadScript(path, options?.js); } else { } if (!ref.current) { return; } const handler = (event: Event) => {}; ref.current.addEventListener("load", handler); ref.current.addEventListener("error", handler); return () => { // 移除监听 & 清除操作 }; }, [path]); return status; }; 复制代码   主函数中判断加载 CSS 还是 JS 资源:const pathname = path.replace(/[|#].*$/, ""); if (options?.type === "css" || (!options?.type && /(^css!|.css$)/.test(pathname))) { const result = loadCss(path, options?.css); // 加载 css 资源并返回结果 ref.current = result.ref; // 返回创建 link 标签返回的 Element 元素,用于后续绑定监听 load 和 error事件 setStatus(result.status); // 设置加载状态 } else if (options?.type === "js" || (!options?.type && /(^js!|.js$)/.test(pathname))) { const result = loadScript(path, options?.js); ref.current = result.ref; setStatus(result.status); } else { // do nothing console.error( "Cannot infer the type of external resource, and please provide a type ("js" | "css"). " + "Refer to the https://ahooks.js.org/hooks/dom/use-external/#options", ); } 复制代码   loadCss 方法:   往 HTML 标签上添加任意以 "data-" 为前缀来设置我们需要的自定义属性,可以进行一些数据的存放const loadCss = (path: string, props = {}): loadResult => { const css = document.querySelector(`link[href="${path}"]`); // 不存在则创建 if (!css) { const newCss = document.createElement("link"); newCss.rel = "stylesheet"; newCss.href = path; // 设置 link 标签支持的属性 Object.keys(props).forEach((key) => { newCss[key] = props[key]; }); // IE9+ const isLegacyIECss = "hideFocus" in newCss; // use preload in IE Edge (to detect load errors) if (isLegacyIECss && newCss.relList) { newCss.rel = "preload"; newCss.as = "style"; } // 设置自定义属性[data-status]为loading状态 newCss.setAttribute("data-status", "loading"); // 添加到 head 标签 document.head.appendChild(newCss); // 标签路径匹配存在则直接返回现有结果,保证全局资源全局唯一 return { ref: newCss, status: "loading", }; } // 如果标签存在则直接返回,并取 data-status 中的值 return { ref: css, status: (css.getAttribute("data-status") as Status) || "ready", }; } 复制代码   loadScript 方法的实现也类似:const loadScript = (path: string, props = {}): loadResult => { const script = document.querySelector(`script[src="${path}"]`); if (!script) { const newScript = document.createElement("script"); newScript.src = path; // 设置 script 标签支持的属性 Object.keys(props).forEach((key) => { newScript[key] = props[key]; }); newScript.setAttribute("data-status", "loading"); // 添加到 body 标签 document.body.appendChild(newScript); return { ref: newScript, status: "loading", }; } return { ref: script, status: (script.getAttribute("data-status") as Status) || "ready", }; }; 复制代码   前面获取到 Element 元素后,监听 Element 的 load 和 error 事件,判断其加载状态并更新状态const handler = (event: Event) => { const targetStatus = event.type === "load" ? "ready" : "error"; ref.current?.setAttribute("data-status", targetStatus); setStatus(targetStatus); }; ref.current.addEventListener("load", handler); ref.current.addEventListener("error", handler); 复制代码   完整源码useTitle   用于设置页面标题。   官方文档基本用法import React from "react"; import { useTitle } from "ahooks"; export default () => { useTitle("Page Title"); return (

Set title of the page. ); }; 复制代码使用场景   当进入某页面需要改浏览器 Tab 中展示的标题时核心实现   这个实现比较简单const DEFAULT_OPTIONS: Options = { restoreOnUnmount: false, // 组件卸载时,是否恢复上一个页面标题 }; function useTitle(title: string, options: Options = DEFAULT_OPTIONS) { const titleRef = useRef(isBrowser ? document.title : ""); useEffect(() => { document.title = title; }, [title]); useUnmount(() => { if (options.restoreOnUnmount) { // 组件卸载时,恢复上一个页面标题 document.title = titleRef.current; } }); } 复制代码   如果项目中我们自己实现的话,有个需要注意的地方,不要把document.title = title;写在外层,要写在 useEffect 里面,具体见该文:检测意外的副作用   完整源码useFavicon   设置页面的 favicon。   官方文档   favicon 指显示在浏览器收藏夹、地址栏和标签标题前面的个性化图标基本用法import React, { useState } from "react"; import { useFavicon } from "ahooks"; export const DEFAULT_FAVICON_URL = "https://ahooks.js.org/simple-logo.svg"; export const GOOGLE_FAVICON_URL = "https://www.google.com/favicon.ico"; export default () => { const [url, setUrl] = useState(DEFAULT_FAVICON_URL); useFavicon(url); return ( <>

Current Favicon: {url} ); }; 复制代码使用场景   当需要改浏览器 Tab 中展示的图标 icon 时核心实现   原理:通过 link 标签设置 favicon   更多 favicon 知识可见: 详细介绍 HTML favicon 尺寸 格式 制作等相关知识   源代码仅支持图标四种类型:const ImgTypeMap = { SVG: "image/svg+xml", ICO: "image/x-icon", GIF: "image/gif", PNG: "image/png", }; type ImgTypes = keyof typeof ImgTypeMap; 复制代码const useFavicon = (href: string) => { useEffect(() => { if (!href) return; const cutUrl = href.split("."); // 取出文件后缀 const imgSuffix = cutUrl[cutUrl.length - 1].toLocaleUpperCase() as ImgTypes; const link: HTMLLinkElement = document.querySelector("link[rel*="icon"]") || document.createElement("link"); link.type = ImgTypeMap[imgSuffix]; // 指定被链接资源的地址 link.href = href; // rel 属性用于指定当前文档与被链接文档的关系,直接使用 rel=icon 就可以,源码下方的 `shortcut icon` 是一种过时的用法 link.rel = "shortcut icon"; document.getElementsByTagName("head")[0].appendChild(link); }, [href]); }; 复制代码   完整源码useFullscreen   管理 DOM 全屏的 Hook。   官方文档基本用法import React, { useRef } from "react"; import { useFullscreen } from "ahooks"; export default () => { const ref = useRef(null); const [isFullscreen, { enterFullscreen, exitFullscreen, toggleFullscreen }] = useFullscreen(ref); return ( {isFullscreen ? "Fullscreen" : "Not fullscreen"} ); }; 复制代码原生全屏 APIElement.requestFullscreen():用于发出异步请求使元素进入全屏模式Document.exitFullscreen():用于让当前文档退出全屏模式。调用这个方法会让文档回退到上一个调用 Element.requestFullscreen()方法进入全屏模式之前的状态[已过时不建议使用]:Document.fullscreen:只读属性报告文档当前是否以全屏模式显示内容Document.fullscreenElement:返回当前文档中正在以全屏模式显示的 Element 节点,如果没有使用全屏模式,则返回 nullDocument.fullscreenEnabled:返回一个布尔值,表明浏览器是否支持全屏模式。全屏模式只在那些不包含窗口化的插件的页面中可用fullscreenchange:元素过渡到或过渡到全屏模式时触发的全屏更改事件的事件fullscreenerror:在 Element 过渡到或退出全屏模式发生错误后处理事件screenfull 库   useFullscreen 内部主要是依赖 screenfull 这个库进行实现的。   screenfull 对各种浏览器全屏的 API 进行封装,兼容性好。   下面是该库的 API:.request(element, options?):使元素或者页面切换到全屏.exit():退出全屏.toggle(element, options?):在全屏和非全屏之间切换.on(event, function):添加一个监听器,监听全屏切换或者错误事件。event 支持 change 或者 error.off(event, function):移除之前注册的事件监听.isFullscreen:判断是否为全屏.isEnabled:判断当前环境是否支持全屏.element:返回该元素是否是全屏模式展示,否则返回 undefined实现思路   看看 useFullscreen 的导出值:return [ state, { enterFullscreen: useMemoizedFn(enterFullscreen), exitFullscreen: useMemoizedFn(exitFullscreen), toggleFullscreen: useMemoizedFn(toggleFullscreen), isEnabled: screenfull.isEnabled, }, ] as const; 复制代码   那么实现的方向就比较简单了:内部封装并暴露 toggleFullscreen、enterFullscreen、exitFullscreen 方法,暴露内部是否全屏的状态,还有是否支持全屏的状态通过 screenfull 库监听change事件,在change事件里面改变全屏状态与处理执行回调核心实现   三个方法的实现:// 进入全屏方法 const enterFullscreen = () => { const el = getTargetElement(target); if (!el) { return; } if (screenfull.isEnabled) { try { screenfull.request(el); screenfull.on("change", onChange); } catch (error) { console.error(error); } } }; // 退出全屏方法 const exitFullscreen = () => { const el = getTargetElement(target); if (screenfull.isEnabled && screenfull.element === el) { screenfull.exit(); } }; const toggleFullscreen = () => { if (state) { exitFullscreen(); } else { enterFullscreen(); } }; 复制代码   onChange 方法const onChange = () => { if (screenfull.isEnabled) { const el = getTargetElement(target); // screenfull.element:当前元素以全屏模式显示 if (!screenfull.element) { // 退出全屏 onExitRef.current?.(); setState(false); screenfull.off("change", onChange); // 卸载 change 事件 } else { // 全屏模式展示 const isFullscreen = screenfull.element === el; // 判断当前全屏元素是否为目标元素 if (isFullscreen) { onEnterRef.current?.(); } else { onExitRef.current?.(); } setState(isFullscreen); } } }; 复制代码   上方onChange以及exitFullscreen执行退出全屏前有行需要判断的代码注意下,具体原因可以看下修复 useFullScreen 当全屏后,子元素重复全屏和退出全屏操作后父元素也会退出全屏// 判断当前全屏元素是否为目标元素,支持对多个元素同时全屏 const isFullscreen = screenfull.element === el; 复制代码   screenfull.element 的实现:element: { enumerable: true, get: () => document[nativeAPI.fullscreenElement] ?? undefined, }, 复制代码   完整源码useHover   监听 DOM 元素是否有鼠标悬停。   官方文档基本用法import React, { useRef } from "react"; import { useHover } from "ahooks"; export default () => { const ref = useRef(null); const isHovering = useHover(ref); return {isHovering ? "hover" : "leaveHover"}; }; 复制代码鼠标监听事件mouseenter:第一次移动到触发事件元素中的激活区域时触发mouseleave:在定点设备(通常是鼠标)的指针移出某个元素时被触发   扩展下几个鼠标事件的区别:mouseenter:当鼠标移入某元素时触发。mouseleave:当鼠标移出某元素时触发。mouseover:当鼠标移入某元素时触发,移入和移出其子元素时也会触发。mouseout:当鼠标移出某元素时触发,移入和移出其子元素时也会触发。mousemove:鼠标在某元素上移动时触发,即使在其子元素上也会触发。核心实现   原理是监听 mouseenter 触发 onEnter 回调,切换状态为 true;监听 mouseleave 触发 onLeave回调,切换状态为 false。   完整实现:export interface Options { onEnter?: () => void; onLeave?: () => void; onChange?: (isHovering: boolean) => void; } export default (target: BasicTarget, options?: Options): boolean => { const { onEnter, onLeave, onChange } = options || {}; // useBoolean:优雅的管理 boolean 状态的 Hook const [state, { setTrue, setFalse }] = useBoolean(false); // 监听 mouseenter 判断有鼠标进入目标元素 useEventListener( "mouseenter", () => { onEnter?.(); setTrue(); onChange?.(true); }, { target, }, ); // 监听 mouseleave 判断有鼠标是否移出目标元素 useEventListener( "mouseleave", () => { onLeave?.(); setFalse(); onChange?.(false); }, { target, }, ); return state; }; 复制代码   完整源码


不去香山人挤人!北京自驾赏红叶银杏目的地推荐!不能错过的秋色秋天一定要住北平。天堂是什么样子,我不晓得,但是从我的生活经验去判断,北平之秋便是天堂。老舍一场雨,便入了深秋,北京也终于迎来了它最美的时节。此时,在京城的某处,最美秋色已隆重登场我游览过的山西旅游景区(2)悬空寺千年不倒的建造玄机悬空寺,顾名思义,就是悬在空中的寺庙。听起来都有点玄乎,这样的寺庙是怎样建成的呢?下面我先简单介绍一下悬空寺的布局,再来说说这座悬在空中1500多年仍然巍然屹立的寺庙的建造玄机。悬位于泰山曲阜水泊梁山的旅游三角中心,知道这县吗?我在头条搞创作第二期今天跟大家分享的这县,位于鲁中偏西,东邻新泰,西连汶上,南与兖州交界,东南与曲阜泗水接壤,北以大汶河为界与岱岳区肥城相望。境内地势东高西低,东部多为低山丘陵,西临沂新增2家国家4A级旅游景区10月27日,山东省文化和旅游厅发布了关于确定11家旅游景区为国家4A级旅游景区的公告,其中,临沂市沂南县朱家林旅游区和沂水县沂蒙花开旅游区成功获评!近年来,临沂市文化和旅游局聚焦赏红叶大熊猫国家公园广元青川唐家河秋韵十足大熊猫国家公园广元青川唐家河秋韵十足。张蓉摄大熊猫国家公园广元青川唐家河秋韵十足。张蓉摄大熊猫国家公园广元青川唐家河秋韵十足。张蓉摄大熊猫国家公园广元青川唐家河秋韵十足。张蓉摄大熊物易云通1024程序员节猿来你在这里你知道吗?2的10次方等于1024是专属于程序员的浪漫今天是一年一度的程序员节!作为一家以科技赋能产业发展的公司物易云通程序员们一直都在默默奋斗辛苦付出10月24日下午物易云通举办最佳观赏季!河南5个赏红叶地图出炉,看看哪里的深秋最美?霜降来了,秋日艳阳,天气也慢慢转凉,又到了美丽的深秋时节,漫山遍野的红叶,色彩斑斓,如霞似火,层林尽染。河南景区里的红叶林也染红了天,鸡爪槭三角枫五角枫万山红叶到了最佳观赏期,带你深秋红叶满山古村落留住乡愁来源人民网河北频道人民网石家庄10月27日电(赵明妍)金秋时节,山间红叶烂漫绚丽多彩,随风摇曳的树叶由黄及橙由橙而褐由褐而红,极目远眺,犹如律动的彩色海洋王硇村就屹立于这群山怀抱之6大日本枫叶品种枫红全靠鸡爪槭山红叶糖枫打造打卡挑战局秋季来日本赏枫时,你能判别各枫叶品种吗?有如火般深红的,也有橘中带黄的,以下将带你认识6个日本常见的枫叶种类,从可以幻变成唯美枫红的鸡爪槭山红叶跟糖枫到形状独特,如银杏般还去栖霞山看红叶?南京枫叶种类最齐全的地方在这,最新攻略来了说起秋天去南京,哪里适合打卡呢?南京本地人会如数家珍地说,去栖霞山看枫叶啊,去朝天宫清凉山和古惠济寺看银杏,池杉湖大泉湖,燕雀湖的水杉也很美!确实,每到秋天,满城的银杏,枫叶和梧桐绵阳,达州和宜宾能得到国家的重视,靠的是什么?四川达州位于川渝鄂陕结合部,四川的最东边,曾经也是三线基地,普铁时代也是成都重庆后西南铁路第三大交通枢纽,人口众多,和南充一样都是劳务输出大市,拉动GDP的核心,和东北的区别就是这
继贴牌生意后,南极人又开始频繁买牌?继贴牌生意之后,魔幻的南极人又开启了疯狂买牌之路。近日,南极电商发布公告称,拟以4500万元收购贝拉维拉服饰股份有限公司100的股权。成立于1996年的贝拉维拉一直深耕中端女装市场纯电紧凑型SUV全新家族式设计思皓爱跑S将于9月21日预售爱卡汽车国内新车原创日前,爱卡汽车从官方获悉,思皓爱跑S将在9月21日世界制造业大会上正式开启预售,新车定位为一款纯电动紧凑型SUV,采用最新的家族式设计。外观方面,思皓爱跑S运用为什么不建议女性天天喝牛奶?提醒喝牛奶牢记4点,更有营养很多养生的人会每天早上起床喝杯牛奶,牛奶是如今普遍的养生饮品,牛奶含有丰富营养成分,能够及时给身体补充营养,失眠多梦的人睡觉前喝一杯牛奶能够提升睡眠质量,牛奶其中含有丰富的微量元素上市及破发我说的不是某只股票,而是新的苹果14手机。9月16日,苹果14新机正式上市开卖,由于苹果手机一直都是非常火热的,每年新机开卖都会迎来一阵哄抢,新机开卖往往都是黄牛先买,最后加价卖消电商插上云翅膀嵩县爱你潮我看张莉娜文稻菽千重浪,喜色眉尖上。嵩州大地处处都是丰收的景象。9月10日,盛装亮相的嵩县电子商务运营中心,迎来了第四届嵩县爱你网红PK暨金秋有约巧手好物直播带货大赛。在这场乡味十足的iPhone14正式开售!配件选得眼花缭乱?不妨看看PANDAER今天早上一打开微博,iPhone14系列上手的图文和视频几乎占据了主页。即使平心而论iPhone14系列这代升级不大,但新手机到手后的那种愉悦感还是很真实的。不过在此也要提醒各位喜3款新电动车来袭,电池可用5年以上,续航超过百公里,主打代步近年来,电动车制造水平不断升级进步,而行业也由此涌现出众多新车型,不过从新发布的这些电动车来看,很多电动车存在续航短或电池不耐用的问题。对此,有用户提出电动车行业都有哪些电池耐用且Pandas基础之文件的读取和写入1。文件的读取Pandas的文件输入有赖于xlrdxlwtopenpyxl这三个第三方的库,如果读取失败可尝试先安装这几个库在数据分析的过程中对于数据的读取和写入是必不可少的,这里东博会,广西各市都签了哪些大单?9月16日,第19届中国东盟博览会(下简称东博会)签约仪式在南宁举行,现场共签订投资合作项目267个,总投资4130亿元人民币。东博会期间,广西各市签了哪些大单呢?图为客商在第19中国钢材市场新一轮大熊市正默默地向我们走来查询及时钢材价格,请点击众钢网专注钢材价格数据这个标题是不是有点耸人听闻哗众取宠?首先要声明一点,我们对钢材市场的预判均是以事实为依据,以客观数据为基准,说了几句大实话而已。有人问执业律师年收入过百万?实习律师一个月3000元,不含社保我师兄执业第五年,创收就超过了一百万!可可是广州某法学院的学生,毕业后的从业方向,第一是考公,成为公安检察院和法院中的一员第二是成为律师,西装革履,穿着高跟鞋出入写字楼和办公场所。