# DailyLog **Repository Path**: lzy_8920/daily-log ## Basic Information - **Project Name**: DailyLog - **Description**: 用于每天的记录 复盘等 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2022-08-31 - **Last Updated**: 2023-10-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 2022.8.29-2022.9.2 原代码 ```JS componentWillReceiveProps(nextProps) { // 根据父组件传过来的参数 改变数组 最后增加一个更新完成的标识 } ``` 第一版代码 ```JS import 。。。 const ARR1 = [ { text: '苹果', id: 1 }, { text: '香蕉', id: 2 }, { text: '橘子', id: 4 }, { text: '葡萄', id: 3 }, ]; const ARR2 = [ { id: 1, text: '苹果', img: `苹果.png` }, { id: 2, text: '大香蕉', img: `香蕉.png` }, { id: 4, text: '大橘子', img: `橘子.png` }, { id: 3, text: '大葡萄', img: `葡萄.png` }, ]; const ARR3 = [ { id: 1, text: '香蕉', img: `香蕉.png` }, { id: 2, text: '大苹果', img: `苹果.png` }, { id: 4, text: '大橘子', img: `橘子.png` }, { id: 3, text: '大葡萄', img: `葡萄.png` }, ]; export default class 组件 extends Component { state = { extraTag: [], arr: ARR1, 是否更新完成: true, }; // tag是啥 tab栏的每个小项 // abtest 一个ab测 命中则使用新的常量arr // isFresh 当前页面的tag是否更新完成 // 为什么不写在didmount里 因为data中的某些信息在didmount时传不过来 // showType属性 在didmount时也有可能传不过来 用于展示某些个tag项 例如该属性若为124,就展示 苹果、香蕉、橘子 // 在写第一版代码时 原本是想先写好ARR这个常量 在receivewprops后决定使用哪组常量 最后通过函数更新出最终展示的 // 但是发现 1:每次修改信息 即每次data更新 都会setState 而是还要把(是否更新完成)属性重置 所以个人认为(是否更新完成)设置的就 没啥意义了 // 2:更新方法是同步方法 不管我setState与否都需要执行 这样就导致我需要把更新方法变成异步 // 3: 这个生命周期最好是纯函数 不要存储啥状态 // 4: 这个可以用getDerivedStateFromProps代替 但是写不了回调函数 componentWillReceiveProps(nextProps) { const { 是否更新完成 } = this.state; const { data, abtest } = nextProps; if (abtest === 2) { this.setState({ arr: 一个条件 ? ARR1 : ARR2, 是否更新完成: true, }); } settimeout(()=>{ const { showType = [] } = data || {}; if (是否更新完成 && showType && showType.length > 0) { 根据showType更新一个参数 } },0) } ``` 第二版代码 ```JS const ARR = [ { id: 1, text: '苹果', img: `` }, { id: 2, text: '香蕉', img: `` }, { id: 4, text: '橘子', img: `` }, { id: 3, text: '葡萄', img: `` }, ] _render = () => { const { state } = this.state; const { props } = this.props; if (abtest === 2) { // 我在渲染的时候根据条件去选择图片 const pic = [ './img/苹果.png', './img/香蕉.png', './img/橘子.png', './img/葡萄.png', ]; const text = [ '苹果', '香蕉', '橘子', '葡萄', ]; // 我不关心在willreceiveprops更新后剩下了几个数据 我只需要在渲染的时候 根据现在已存在数据的编号 给对应的对象加上对应的图片 arr.map((item, index) => { item.img = pic[+item.id - 1]; item.text = text[+item.id - 1]; if (!条件 && +item.id === 1) { item.img = './img/香蕉.png'; } if (!条件 && +item.id === 2) { item.img = './img/苹果.png'; } }); } }; ``` 第三版代码 ```JS _render = () => { const { state } = this.state; const { props } = this.props; if (abtest === 2) { const text = [ '苹果', '香蕉', '橘子', '葡萄', ]; arr.map((item, index) => { item.text = text[+item.id - 1]; // 之前在map的时候 id1 id2赋值了两次 用switch只赋值1次 switch (+item.id) { case 1: item.img = 条件 ? './img/苹果.png' : './img/香蕉.png'; break; case 2: item.img = 条件 ?'./img/香蕉.png':'./img/苹果.png'; break; case 3: item.borderImg ='./img/橘子.png'; break; case 4: item.borderImg = './img/葡萄.png'; break; default: break; } }); } }; ``` ## 2022.9.5-2022.9.9 第一次使用react Hook + ts ```JS import { FC, useState } from 'react'; import { Modal } from 'antd-mobile-v2'; import { CommonUtil } from 'utils'; import styles from './index.less'; type Props = { history: any; fn: Function; fn: Function; data: { // {[],[]} aa: String, bb: String, cc: boolean, }[]; } const SelectModalNew: FC = (props) => { const [info, setInfo] = useState(data); return ( ) } export default SelectModalNew; const Head = ({ }): JSX.Element => { return (
xxxx
); }; const AddressBookMemebers = ({ }): JSX.Element => { return (
); }; const Buttons = ({ }): JSX.Element => ( ); ``` - 没有this 更不用担心this的指向问题 - 不需要再每个组件里从props里拿属性了 注意: 1. useState()的默认值最好不要是表达式,如xxxx( ),这样会在每次初始化的时候都执行。 2. useState传递函数时,要使用( ) => fn ,这里不是箭头函数。 ```JS const [fn, setFn] = useState(() => () => { console.log('asd'); }) ``` ## 2022.9.13-2022.9.16 关于静态资源加载 - 一个页面可能有多种静态资源文件,如骨架屏、头图、音频等资源文件 - 要预加载静态资源文件,避免出现进入页面很久音频才加载完成 - 当有多种预加载文件时,确认加载的先后顺序,避免出现音频加载完成,图片才开始加载的情况 关于音频文件的使用 - 部分浏览器现已不支持进入页面自动播放声音,会报错提示需要与页面交互才可播放。 - 目前大多的解决方法是给audio加上muted静音标签,在页面中展示一个供交互的图标,点击再play音频。 - 音频媒体提供了很多加载事件供开发者参考使用,如加载开始:loadstart,加载完成:loadedmetadata。 - 可根据这些事件与其他图片资源加载事件打出日志的前后比较加载顺序。 ## 2022.9.19-2022.9.23 1. 字符串截取函数slice、substring、substr - 警告:尽管 String.prototype.substr(…) 没有严格被废弃 (as in “removed from the Web standards”), 但它被认作是遗留的函数并且可以的话应该避免使用。它并非JavaScript核心语言的一部分,未来将可能会被移除掉。如果可以的话,使用 substring() 替代它. - substring返回一个索引和另一个索引之间的字符串 ```JS str.substring(indexStart, \[indexEnd\]) // 左闭右开 ``` - substr返回从指定位置开始的字符串中指定字符数的字符 ```JS str.substr(start, \[length\]) ``` - slice返回一个索引和另一个索引之间的字符串 ```JS str.slice(beginIndex\[, endIndex\]) ``` - 如果遇到负值,则加上字符串长度在计算 2. URL.createObjectURL()在大多数浏览器中的高版本不在兼容,可以使用FileReader.readAsDataURL() 3. ```JS onClick={() => {this._jjj()}} onClick={this._jjj} // 前者在传入props时每次都会生成一个新对象 // 后者有具体的引用 不会每次都生成新的对象 // 建议用后者 ``` ## 2022.9.26-2022.9.30 本周分享:react-lottie https://gitee.com/lzy_8920/react-lottie/blob/master/Lottie.md ## 2022.10.8-2022.10.14 《一个数字滚动的动画》 第一版 - 区分int和float,每个数字对应渲染一个0-9的div(3个map渲染) - 数字的位移量通过id,id.style.transform = `translateY(-${数字高度 * (9 - +当前数字)}rem)`; 第二版 - 无需区分数字类型,遇到小数点不渲染(一个map渲染) 第三版 - 获取位移量位移量的函数不在render中调用,在didmount时根据数组的长度设置一个state存入位移量 注意点: - 浏览器不会重新渲染在同一动画帧中发生变化的东西。但是它们合并更改并呈现最终结果。 - 由于上述原因需要在把setstate位移量的函数写在定时器中,过一帧动画后在setstate。并不是因为setstate变成了同步。 - 可以使用`requestAnimationFrame`代替定时器,但是可能会有浏览器兼容问题。参考:https://muffinman.io/blog/react-rerender-in-component-did-mount/。 - 该组件可以写成纯函数组件,因外部传入的值不变。extends React.PureComponent ## 2022.10.17-2022.10.21 ## 2022.10.24-2022.10.28 - 解决eslint报错Require statement not part of import statement ```JS .eslintrc下 "rules": { "@typescript-eslint/no-var-requires": 0 } ``` - img不能写伪元素 - fontsize最小是12 - 组件优化方案: 1. 业务抽离:之前写的组件内容不仅包括ui展示、交互还包括各种业务逻辑,有些业务逻辑写在了组件内部,有些写在了使用组件的组件里即组件外部,导致查找问题困难。所以可以把组件拆分为基础组件和业务组件,基础组件只负责展示及交互,其内容在业务组件中计算好以props的形式传过去,类似于MVC。 2. 高阶组件:对于某些组件中可能存在两种功能相似但交互和样式不同的组件,大多数props相同,根据state变化,可以采用高阶组件的方式优化。 ## 2022.10.31-2022.11.04 ### 1. 循环请求 - 需求背景:渲染组件时,需要连续请求多次同样的接口,并按顺序拿到返回值保存到数组。 - 第一版改动遇到的坑:useEffect里根据需要的长度去map请求数据的函数,相当于for循环联系执行请求,请求是异步的,会导致在返回结果后存入数组时的存入顺序会有问题。其次,由于请求是异步的,而本次需把每次请求的结果做为基础供为下次请求使用,所以这里就需要加入async await,导致函数复杂。 - 第二版改动:解决方法是函数使用递归写法,首先定义一个存放内容的全局变量arr,和一个计数的全局变量currIndex。每次请求首先把结果push进arr,其次currIdex++,然后进入下次递归。递归结束条件就是currIdex到达一定值,最后setState。 ### 2. 组件优化 - 需求背景:在一个组件中根据条件渲染两个组件,两个组件内容相似,需要将该组件的state尽量删除,组件尽量变成纯组件。 - 高阶组件:接收两个参数,第一个参数是一个组件,用于最后返回并渲染,第二个参数可以是原组件接收的props或者其他。 - useMemo:缓存上次计算结果,根据参数变化执行函数更改计算结果。 ## 2022.11.07-2022.11.11 #### useMemo的注意事项 1. 不要在循环中使用,取而代之的是为循环项建立新的组件,在组件内部使用。 ```JS function ReportList({ items }) { return (
{items.map(item => { // 错误 const data = useMemo(() => calculateReport(item), [item]); return (
); })}
); } // 正确 function ReportList({ items }) { return (
{items.map(item => )}
); } function Report({ item }) { const data = useMemo(() => calculateReport(item), [item]); return (
); } ``` 2. 在useMemo中接收的第二个参数,也就是依赖项中存在外部建立的常量时,应该把该常量改为在useMemo中使用。因为每次创建组件时,都会新建常量,进而每次useMemo都会重新计算。 ## 2022.11.14-2022.11.18 1. useMemo和useCallback的区别 - useMemo计算结果是return回来的值,用于缓存计算结果的值。 - useCallback计算结果是其第一个参数-函数,主要用于缓存函数。因函数式组件每次任何一个state变化时整个组件都会被刷新,但一些函数时没有必要被重建或者刷新的,此时应缓存。 注:上两个钩子滥用的话也会造成性能浪费,最最主要的目的还是要减少render。 ## 2022.11.21-2022.11.25 1. 解决业务中的异步问题 场景: 原逻辑中,在页面a通过setState更改状态展示展示页面b,并请求接口,上述两个操作都为异步执行。 需求: 要求先请求接口,根据接口返回的值来决定是否更新状态,也就是根据接口值决定是否展示页面b。 解决方案1: 把setState作为请求接口的回调函数传入,根据接口返回值判断是否执行该回调。弊端是若请求的接口在较深的层级中,期间就需要传递多次callback函数。 解决方案2: 自定义监听事件:顾名思义监听事情发生的动作即可。弊端是首先先要绑定元素,有些事件发生的过程往往较为复杂,绑定的dom难以确定,其次对浏览器有兼容性问题,正常使用dispatchEvent分发事件,有些浏览器的版本需要使用fireEvent。 ## 2022.12.05-2022.12.09 1. offsetTop 返回当前元素相对于其offsetParent元素顶部的距离 2. scrollTop 页面滚动的距离 3. document.scrollingElement.scrollTop设置页面卷曲的高度 4. 问题查找:页面监听scroll,重复setState一个字段,该字段用于在render中控制一个组件的渲染,本次需求需要增加一个渲染的组件,但是是通过函数渲染。首次看问题的是以为scroll重复渲染导致函数重复执行,需要加防抖处理,但是无效。之后发现是因为不能载render中直接调用函数。 ## 2022.12.12-2022.12.16 与useRef相关的两个API - 一个组件公开了原始的DOM输入元素,这使得父组件可以在它上面调用其私有方法。然而,这也允许其父组件做其他事情——例如,改变它的CSS样式。但是在一些的情况下,希望限制使用其他的功能,这时可以使用useImperativeHandle ```JS const MyInput = forwardRef((props, ref) => { const realInputRef = useRef(null); useImperativeHandle(ref, () => ({ // Only expose focus and nothing else focus() { realInputRef.current.focus(); }, })); return ; }); ``` - 有这样一个场景,给列表添加一个新元素,然后滚动到新元素。但是由于某些原因,只能滚动到添加之前的最后一个元素。 ```JS setTodos([ ...todos, newTodo]); listRef.current.lastChild.scrollIntoView(); ``` 这是因为setTodos不会立刻更新DOM,则先执行scrollIntoView。要解决这个问题,可以使用flushSync,并将状态更新包装到flushSync调用中。 ```JS flushSync(() => { setTodos([ ...todos, newTodo]); }); listRef.current.lastChild.scrollIntoView(); ``` ## 2022.12.19-2022.12.23 1. 问题排查 在父组件render中调用子组件方法报错。 原因:父子组件生命周期执行顺序是父willMount-》父render-〉子1willMount-》子1render-〉子2willMount-》子2render-〉子2DidMount-》子1DidMount-〉父DidMount。所以子组件未render完成时,在父组件的render中调用自组件的方法会报错。(vue中同理) 2. 自定义hooks 即自己定义的hook,自定义hook专注的就是逻辑复用,共享一个有状态逻辑的方法,是指对相同逻辑功能的封装。 ```JS function useFormatList(list) { return useMemo(() => list.map(item => { return item.toUpperCase() }), []) } // 对数组每一项转大写 ``` 3. memo和useMemo memo是react的api,接收两个参数,第一个是react的组件,react会浅比较其中的props,当父组件渲染引起子组件渲染,如果props没变化时,子组件不会重复渲染,但是props中如果有对象或者函数时,react只会比较其引用地址。第二个参数就是一个用来比较前后props的一个函数,如果前后相等则返回true。 useMemo是react的hook,最终会返回一个值,其第一个参数是用来返回值的函数,第二个参数是用来监控函数中某些值的变化,如果不变直接返回上次的计算结果。 所以memo适合结合useMemo和useCallBack一起使用。 ## 2022.12.26-2022.12.30 ### 1. 泛型工具类型 参考:https://www.cnblogs.com/chenxian123m/p/16557210.html 1. partail 2. Readonly 3. pick 4. emit 5. Record 6. Exclude ### 2. 解决useState的闭包问题 ```JS const [count,setCount] = useState(0) useEffect(() => { setCount(1) setTimeout(() => { console.log("setTimeout count", count); // 0 }, 1000); }, []); ``` 解决方法:使用useRef ```JS const [count, setCount] = useState(0) const countRef = useRef(0); const fnfn = () => { const newCount = count + 1 setCount(count => count + 1) countRef.current = newCount console.log(countRef.current); // 1 } ``` 参考:https://juejin.cn/post/7119839372593070094#heading-6 ## 2023.01.03-2023.01.06 1. **遇到问题**:编写一个函数返回一个input框,当成组件写在render中时,在输入框中输入会失焦,当render函数用不会。 - 当组件用,上层组件重新 render后,里面组件会百分百被重创建,就也就是失焦的状态;每次重新渲染时,该组件都会被重新定义。 - 当 render 函数用,就是对当前组件的 JSX 补充,调度粒度还是当前组件,里面的东西不会重新创建。 2. unit_title=decodeURI(unit_title); // 解码方式unscape换为decodeURI,将中文参数获取 3. useEffect清除函数的执行时机 - 没有第二个参数: 每次渲染完毕之后执行 - 有第二个参数,是空数组:第一次渲染完毕后执行(组件初始化) - 有第二个参数,不是空数组:每次渲染后,如果数组中的变量发生变化,就会执行。 4. **遇到问题**:定义定时器 ```JS // 全局定义 const App = () => { let timer; useEffect(() => { timer = setInterval(() => { console.log('触发了'); }, 1000); },[]); const clearTimer = () => { clearInterval(timer); } return ( <> ) } ``` - 如果该组件中有state变化导致这个组件重新render时,点击停止按钮,定时器仍然会不断在控制台打印,定时器清楚事件无效。 - 因为重新渲染后,这里的timer以及clearTimer方法都会被重新创建,timer已经不是定时器变量了 解决办法:使用useRef - 因为useRef的返回值在组件每次render后都是同一个,所以他可以用来保存一些组件整个生命周期都不需要变化的值,最常见的就是定时器的清除场景。 5. 生成m到n的随机数 - [n,m]:Math.floor(Math.random() * (m + 1 - n) + n) - (n,m):Math.floor(Math.random() * (m - n) + n) - (n,m]:Math.floor(Math.random() * (m +1- n) + n-1) ## 2023.01.09-2023.01.13 1. useEffect中的竞态问题 随着用户频繁的交互导致段时间内多次请求同一个接口,因为接口返回的时间不一样,如果不加处理,导致页面上显示的是返回最慢的接口数据。 解决办法: ```JS useEffect(() => { let isSubscribe = true; fetchData(args).then(ret => { if (isSubscribe) { setData(ret); } }) return () => { isSubscribe = false; } }, [args]); ``` 2. useLayoutEffect注册回调,layout之后paint之前,同步处理注册函数(阻塞线程,慎用) 3. effect的执行流程 1. 首次App函数执行 2. layout effect 3. effect 4. 触发re-render,函数组件再次执行 5. 对上次的layout effect进行清除 6. 本次layout effect 7. 对上次effect进行清除 8. 本次effect ## 2023.01.29-2023.02.03 1. 在本周需求进行交互以及样式的ab测改造时,发现代码结构嵌套复杂。在编写需求之前需要先梳理组件的DOM结构,并进行拍平优化。有些组件最初的逻辑结构并不嵌套复杂,但随着业务需求逐渐变形。所以对于一些容易变形的组件,可以提供一个不变化的基础模版提供参考。 ```JS // a组件嵌套b、c组件 // => , , ``` 2. 关于headless ui - 目前业务相关组件ab测较多、交互复杂、样式有相似点但细节差距较大,而最开始编写组件的时候并没有考虑过 将组件的状态及交互逻辑和ui展示层解藕。 - 例如现在大多的组件,都是直接渲染,对组件的功能通过props设置。那么每当出现业务化定制场景的需求,或者功能扩展时,都需要向下兼容组件。 - 对于 Headless UI 组件,我们要做到第一件事,就是分析和抽离组件的状态以及交互逻辑。对于counter组件,它的状态大概如下 图片 - 优点:最大化代码复用、极大的ui自定义空间 - 缺点:开发者需要了解业务以及较强的组件抽象设计能力 ## 2023.02.06-2023.02.10 ab测modal 设计背景:目前落地页、结果页根据业务的不同存在很多ab测,而这些ab测目前是使用props的形式一层一层传入其子组件。 方案:编写一个ab测的集合类,用于存放ab测的值,并提供一个保存修改值的方法。 遇到问题:原本想对公共属性进行抽取,但对比各个ab测数据,公共属性较少,且相同属性含义不用,并且为了连拼提示注释,故对每个 ab测的属性进行单独编写属性类型和注释。 ## 2023.02.13-2023.02.17 abTestModal的数据优化 1. abTestModal中属性的可扩展性 目前大多ab测中返回的属性比较单一,例如xxAbtest只返回了一个版本号,所以最初在对属性赋值的时候,直接使用该ab测的版本号。但如果该属性以后进行了其他属性的扩充,即变成了对象属性,对原来的属性就需要全部重新定义。 2. ab测数据的处理 在目前的业务代码中,需要对返回的ab测进行数据处理才能使用,本次为了做到视图和数据分离,将把ab测数据的处理移至abTestModal中。对于面向对象编程时,对象的属性一般都有set get方法;暴露一个方法,根据现有的属性,返回对应的值,也是常用的写法。 准备浏览器原理的分享 对于一名前端开发来讲,需要具备准确评估web开发可行性的能力,并要能站在用户体验角度考虑页面性能。 所以我个人打算调研一下浏览器的渲染机制、JS的执行机制等。 ## 2023.02.20-2023.02.24 https://gitee.com/lzy_8920/react-router ## 2023.02.27-2023.03.03 ### 关于useCallback的使用场景 - useCallBack的本质工作不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址,所以不论是否使用useCallBack都无法阻止组件render时函数的重新创建 - 使用场景:**在往子组件传入了一个函数并且子组件被React.memo缓存了的时候使用** - React.memo检测的是props中数据的栈地址是否改变。而**父组件重新构建的时候,会重新构建父组件中的所有函数**(旧函数销毁,新函数创建,等于更新了函数地址),新的函数地址传入到子组件中被props检测到栈地址更新。也就引发了子组件的重新渲染。 - 那么如何才能让子组件不进行重新渲染呢? - 使用useCallBack包一下需要传入子组件的那个函数。那样的话,父组件重新渲染,子组件中的函数就会因为被useCallBack保护而返回旧的函数地址,子组件就不会检测成地址变化,也就不会重选渲染 ### 检测一个元素是否可见 - 过去,要检测一个元素是否可见或者两个元素是否相交并不容易,大多使用getBoundingClientRect + throttle方法,事件监听和调用 [`Element.getBoundingClientRect()`](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect) 都是在主线程上运行,因此频繁触发、调用可能会造成性能问题。 下面这些情况都需要用到相交检测: - 图片懒加载——当图片滚动到可见时才进行加载 - 内容无限滚动——也就是用户滚动到接近内容底部时直接加载更多,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉 - 检测广告的曝光情况——为了计算广告收益,需要知道广告元素的曝光情况 - 在用户看见某个区域时执行任务或播放动画 **Intersection Observer API** 会注册一个回调函数,每当被监视的元素进入或者退出另外一个元素时 (或者 [viewport](https://developer.mozilla.org/zh-CN/docs/Glossary/Viewport) ),或者两个元素的相交部分大小发生变化时,该回调方法会被触发执行。 ```JS const callback = entries => { // ... }; const options = { root: document.querySelector('#scroll'), // 目标元素所在的容器节点,如果不指定根节点,默认文档为根节点 rootMargin: '0px', // 围绕根元素的边距,类似于css的margin属性 threshold: [0] // 相交的比例,既可以是一个数字也可以是一个数组。取值在0-1之间 }; const observer = new IntersectionObserver(callback, options); const ele = document.querySelector('.img'); observer.observe(ele); ``` - entry.boundingClientRect 目标元素的区域信息,getBoundingClientRect()的返回值 - entry.intersectionRatio 目标元素的可见比率 - entry.intersectionRect 目标元素与根元素交叉的区域信息 - entry.isIntersecting 是否进入可视区域 - entry.rootBounds 根元素的矩形区域信息 - entry.target 被观察的目标,是一个DOM节点 - entry.time 可见性发生变化的时间,相交发生时距离页面打开时的毫秒数.精度为微秒 ## 2023.03.06-2023.03.10 #### 1. 控制组件的参数抽象到props - 通过modal抽离的属性,可以通过在组件中引入modal使用,这样做的好处可以减少组件自身的属性。 - 但并不是所有的组件都会引入modal,这就会导致其他地方引入组件的时候,传入的属性无法控制组件。 - 在编写storybook时,也无法控制modal的属性展示出组件所有的状况。 - 所以在定义组件时,尽量把所需的属性通过props引入。 ## 2023.03.13-2023.03.17 react router v6用法 原理以及源码解析 https://gitee.com/lzy_8920/react-router ## 2023.03.20-2023.03.24 WebSocket - WebSocket是一种网络通信协议,属于应用层协议,基于TCP协议实现,用于在客户端和服务器之间进行双向通信。 - WebSocket对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。 - 服务端 ```JS const ws = require("ws"); // 加载ws模块; // 启动websocket服务器 const wsServer = new ws.Server({ host: "127.0.0.1", port: 8888, }); console.log('WebSocket sever is listening at port localhost:8888'); wsServer.on('connection', function (ws) { console.log('clinet connected', ws); }) wsServer.on('message', function (message) { console.log('request message', message); }) wsServer.on('close', function () { console.log("request close"); }) wsServer.on('error', function (err) { console.log("request error", err); }) ``` - 客户端 ```JS const ws = new WebSocket('ws://127.0.0.1:8888/') ws.onopen=()=>{ console.log('服务器已连接') } ws.onmessage=(msg)=>{ console.log('来自服务器端的数据:'+msg.data) //监听接受来自服务端的信息 } ws.onclose=()=>{ console.log('服务器关闭') } function sendserver(){ ws.send('nihao你好啊') //向服务端发送信息 } ``` ## 2023.04.03-2023.04.07 ### 1. useState与useRef #### useState ```JS import React, { useState, useEffect } from 'react'; export default function Demo() { const [like, setLike] = useState(0); useEffect(() => {}); const handleClick = () => { setLike(like + 1); }; const getLikeValue = () => { setTimeout(() => { alert(like); }, 2000); }; return (
); } ``` - 此时当我点击 `获得like值` 按钮,因为定时器的原因并不会立即执行`alert`,此时我再次点击 `+` 修改like。 - 当两秒过后,你会发现页面上展示的最新的like值,而 `alert` 弹出的 like 停留到了1。 - 原因:每一次渲染函数内部都拥有自己独立的`props`和`state`, 当在jsx中调用代码中的state进行渲染时,每一次渲染都会获得各自渲染作用域内的`props`和`state`。 #### useRef - useRef会在所有的render中保持对返回值的唯一引用。因为所有对ref的赋值和取值拿到的都是最终的状态,并不会因为不同的render中存在不同的隔离。 - 总结来说,`useRef`返回值的改变并不会造成页面更新。而且`useRef`类似于react中的全局变量, 并且不存在不同次render中的`state/props`的作用域隔离机制。这就是`useRef`和`useState`这两个hook的主要区别。 - 个人理解,useRef可代替class组件中的this ### 2. 关于开发思路 举例:一个弹窗有a、b、c三个按钮,点击后关闭弹窗,提交表单时不再拉起。 ​ 如果点击系统返回关闭弹窗,提交表单时再次拉起。 - 正向:点击abc时,定义标识,提单时根据 “点击过abc” 不再拉起 - 反向:点击系统返回时,定义标识,提单时根据 “非--点击过系统返回” 不再拉起,但是如果其他事件触发了系统返回,该弹窗就会一直拉起。 - 所以尽量采用正向思考。 ## 2023.04.10-2023.04.14 1. echarts -- tooltip -- formatter 提示框浮层内容格式器,支持字符串和回调函数 - 字符串模版 ``` formatter: '{b0}: {c0}
{b1}: {c1}' ``` - 回调函数 ```JS (params: Object|Array, ticket: string, callback: (ticket: string, html: string)) => string | HTMLElement | HTMLElement[] ``` 2. esModule引入css和直接引入css - 在webpack中的css-loader,为解析的css类名配置contenthash,如果此时页面中是直接引入css(import 'css'),页面中将获取不到对应的类名。 3. amis相关属性 - Unique:下拉框中使用 指定当前表单项不可重复 - labelField:默认渲染选项组,会获取每一项中的`label`变量作为展示文本,如果你的选中项中没有`label`字段,想自定义该字段,则可以设置`labelField` - valueField: 默认渲染选项组,会获取每一项中的`value`变量作为表单项值,如果你的选中项中没有`value`字段,想自定义该字段,则可以设置`valueField` ## 2023.04.17-2023.04.21 1. amis中的数据链 - 首先会先尝试在当前组件的数据域中寻找变量,当成功找到变量时,通过数据映射完成渲染,停止寻找过程 - 当在当前数据域中没有找到变量时,则向上寻找,在父组件的数据域中 - 只有少数几个容器组件会创建新的数据域,除了最顶层的 Page,还有 CRUD、Dialog、IFrame、Form、Service 等 2. initApi返回的数据 - initApi返回的数据会被自动保存到页面中,保存的位置取决于返回数据的格式 - 如果返回的是对象,那么可以通过 `this.data` 或 `this.formData` 访问 - 如果返回的是数组,那么可以通过 `this.items` 访问 - 如果返回的数据需要进行格式转换,可以通过在 `initApi` 中指定 `adaptor` 函数来进行处理 3. 同步更新内部表单项 - `canAccessSuperData`可以获取父级数据域值,但是为了效率,在父级数据域变化的时候,默认 combo 内部是不会进行同步的 - 配置`"strictMode": false` - 配置`syncFields`字符串数组,数组项是需要同步的变量名 4. list - "actions": [ ] 可将表单默认的提交按钮去掉 - multiple: true, // 是否支持多选 - multiLine: true, // 是否支持多行文本输入 - clearValueOnHidden: true, // 当组件被隐藏时是否清空其值 - columnsTogglable:false 表示不允许用户隐藏表格的某些列 5. 文档整理 c端 b端 ## 2023.04.23-2023.04.28 1. 分析包体积 - webpack-bundle-analyzer 是 webpack 的插件,这个插件可以读取输出文件夹(dist)中的 stats.json 文件,把该文件可视化展现,生成代码分析报告 - ```JS const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin module.exports={ plugins: [ new BundleAnalyzerPlugin() // 使用默认配置 // 默认配置的具体配置项 // new BundleAnalyzerPlugin({ // analyzerMode: 'server', // analyzerHost: '127.0.0.1', // analyzerPort: '8888', // reportFilename: 'report.html', // defaultSizes: 'parsed', // openAnalyzer: true, // generateStatsFile: false, // statsFilename: 'stats.json', // statsOptions: null, // excludeAssets: null, // logLevel: info // }) ] ``` 2. 您可以按照以下步骤将`export PATH=$PATH:/Users/yuanbao/.npm-global/bin`添加到`zshrc`文件中 - open ~/.zshrc - export PATH=$PATH:/Users/yuanbao/.npm-global/bin - source ~/.zshrc 使新的`zshrc`配置文件立即生效 - npm bin -g 这个命令会返回全局安装目录的路径 3. 将本地库上传至私库 - 本地发布可以使用工具库 Verdaccio,搭建本地私有仓库 npm install -g verdaccio - 启动本地私有库服务器 verdaccio - 添加用户 npm adduser --registry https://xxxxxxx - 部署 npm publish --registry=http://localhost:4873 - 引入 npm install helloworld --registry=http://localhost:4873 - 注:更改版本号、更改name 4. ### Pre & Post Scripts、Life Cycle Scripts(前后脚本、生命周期脚本) - 前后脚本 ```JS { "scripts": { "precompress": "{{ executes BEFORE the `compress` script }}", // 在compress之前执行 "compress": "{{ run command to compress files }}", // 运行压缩文件的命令 "postcompress": "{{ executes AFTER `compress` script }}" // 在compress之后执行 } } ``` - 生命周期脚本 ```JS 有一些特殊的生命周期脚本只在特定情况下发生 除了pre、post脚本之外,还会出现这些脚本 prepare 在包被打包之前运行,即在npm publish 和npm pack npm install在没有任何参数的情况下在本地运行 prepublish(预发布 已弃用) prepublishOnly(仅预发布) 在包准备和打包之前运行,仅在npm publish prepack(预包装) 在打包 tarball 之前运行(在“ npm pack”、“ npm publish”以及安装 git 依赖项时) postpack dependencies ``` 在打包react-lottie之前修改这些无用的生命周期脚本 仅需要执行npm publish ## 2023.05.04-2023.05.06 1. amis中的input-table外的属性控制其内的属性 - input-table是amis中的输入表格组件 - 场景:属性1控制table显示,属性2控制table列的显示 - 如果先选择属性2,后选择属性1展示table,再改变属性2想控制table列的显示将无效 - 所以需要先展示table 2. amis中的input-table内的属性控制其行内的属性 - 场景:属性3控制控制该行中属性4的显示 - 再input-table中该控制无效 ## 2023.05.08-2023.05.12 1. amis中使用三元表达式 - '${isAbTest|isMatch:"1":"是":"否"}' - `${isAbTest}`:使用`$`符号将`isAbTest`包裹起来,表示从当前上下文中获取`isAbTest`的值。 - `|`:条件运算符,表示执行条件判断。 - `isMatch`:条件判断函数的名称。 - `:"是":"否"`:三元表达式的条件语句。如果`isMatch`函数返回`true`,则显示`是`;否则,显示`否`。 2. html2canvas - `html2canvas` 库来截取指定元素 `ele` 的屏幕截图并转换为 PNG 格式的图片,然后将图片的 `src` 属性设置为生成的图片的 dataURL,最终渲染到一个 `` 标签上 - 使用:传入两个参数 1、`ele`:要截取的 DOM 元素 2、一个配置对象{`useCORS`:是否允许跨域,`backgroundColor`:指定背景色 ... } 3. 页面加载过程 - html加载 - 初始化init接口 - 加载访问页资源 - 请求访问页接口 - 渲染页面 4. bug记录 - props传入 ...otherProps: 例如在otherProps中拆分出aaa属性,需要在组件的props中传入aaa属性。 ## 2023.05.15-2023.05.19 1. 页面渲染阶段详解 | 可交互 | 可交互--》html加载完 | html加载完成--》首个渲染 | 首个渲染---》首屏 | | ----------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | html请求回来,对应domInteracive domInteractive - fetchStart | 浏览器构建解析dom,加载cdn资源 domContentLoadEventEnd - domInteractive | 登录页接口请求完成,并开始setState渲染,对应ab和查询信息接口请求 | 绑定页面资源,渲染头图,加载mp3资源 首屏以最后一个可视范围大图渲染完毕为准 | 2. 小程序与h5的关联 - 登录授权小程序 生成userId_A - 登录h5 生成userId_B - 删除其一的登录或者授权态 仅代表当前环境的登录态丢失 - 小程序、h5通过手机号关联 3. amis中adaptor、requestAdaptor需要return ```JS adaptor: (payload) => { const { data } = payload || {}; 。。。 console.log('payload', payload); return ({ ...payload }); }, ``` 4. 需求背景:批量提交,选中时页面停止刷新 ```JS api: { url: '/api/请求', method: 'get', sendOn: '!window.flag', // 根据设置的flag 停止请求 达到不刷新页面的目的 adaptor: payload => ({ xxxxxxx }), }, interval: 30000, // 自动刷新时间 stopAutoRefreshWhenModalIsOpen: true, // 有弹窗时不刷新 headerToolbar: [ 'bulkActions', ], bulkActions: [ { label: '批量', actionType: 'ajax', api: { url: '/api请求', method: 'post', data: { 选中列表: '${items|split|pick:id|toInt}', // 从 items 数据中提取 id 属性的值,并将其转换为整数类型 }, adaptor: (payload) => { if (xxxxxxxxxxx) { window.flag = false; } else { window.flag = true; } return ({ ...payload }); }, }, confirmText: '确定?', }, ], ``` ## 2023.05.22-2023.05.26 1. html2canvas兼容性问题 - 在苹果ios13、微信环境下html2canvas不生效 - ```JS // src/dom/document-cloner.ts // line 96 if (documentClone. fonts && documentClone.fonts.ready) { // await documentClone.fonts.ready; } ``` - 该代码不生效,因为当前并未使用字体,删除即可。 1. 天机表单更改使用antd编写 - 虽然amis框架对于b端系统快速实现表单增删改查功能比较迅速,但对于一些特殊的需求实现起来却是很困难。 - 例如组件里交互过于固定、事件绑定困难、无法使用生命周期、调试繁琐。 - 所以后续新增的页面以及功能打算全部使用react hook + antd 的形式实现。 - 对于原有的页面维护可以在有时间的情况下进行代码梳理以及改造。 ## 2023.05.29-2023.06.02 1. amis - combo中设置默认项 ```JS { type: 'combo', multiple: true, multiLine: true, noBorder: true, name: 'xxxx', label: false, required: true, clearValueOnHidden: true, mode: 'horizontal', disabledOn: '!this.isEdit', addable: false, // 不可添加 removable: false, // 不可删除 value: [{}, {}, {}], // 设置几个空对象 controls:[ ] } ``` 2. antd - 增减行 ```JS {(fields, { add, remove }) => ( {fields.map(({ key, name, ...restField }, index) => ( ))} )} ``` - 该add方法添加数据的数据结构可能有问题,例:原数据为数组,添加的可能对象 3. 返回销毁页面 并刷新数据 - history.push('/data/templateconf/categoryTemplate?isDestroy=1'); - ```JS const history = useHistory(); const location = useLocation(); const queryParams = new URLSearchParams(location.search); const paramValue = queryParams.get('isDestroy'); // 其他页面返回需要刷新数据 useEffect(() => { wxIdSelectData(); formInit(); }, [paramValue]); ``` ## 2023.06.05-2023.06.09 1. amis - `match` 模糊匹配后面的参数 - `equals` 精准匹配 2. react并发模式 React 并发的基本原理是重构渲染过程,使 **在渲染下一个视图时,当前视图保持响应性** - 在幕后,这将通过在`requestIdleCallback()`调用中包装组件渲染来实现,让应用在渲染过程中保持响应性。 - ```JS 阻塞模式 function renderBlocking(Component) { for (let Child of Component) { renderBlocking(Child); } } 改写如下 并发模式 function renderConcurrent(Component) { // 如果状态已经过时,打断渲染进程 if (isCancelled) return; for (let Child of Component) { // 等待浏览器不忙(没有要处理的输入) requestIdleCallback(() => renderConcurrent(Child)); } } ``` ## 2023.06.12-2023.06.16 1. BFF - 核心矛盾 - 多端开发UI、功能存在差异 - 功能投放场景功能逻辑的差异较大 - BFF应用模式 - 后端bff:针对业务场景把存在于多个服务的数据抽离到一个接口中供前端使用(目前采用) - 前端bff:前端自主查询和使用数据,从而达到降低跨团队协作的成本,提升BFF研发效率的效果。 2. React hook **useTransition(版本18)** ```JS function TabContainer() { const [isPending, startTransition] = useTransition(); const [tab, setTab] = useState('about'); function selectTab(nextTab) { startTransition(() => { setTab(nextTab); }); } // ... } ``` - 返回参数 isPending 用于告诉是否存在pending的转换 startTransition函数用于更新状态 - 通过useTransition,使UI在重新渲染过程中保持响应。例如,如果用户单击某个选项卡,但随后改变主意并单击另一个选项卡,则无需等待第一次重新渲染完成即可执行此操作。 - 防止不需要的Suspense加载指示器。使用Suspense隐藏整个选项卡容器以显示加载指示符会导致用户体验不好。使用useTransition可以改为在选项卡按钮中指示显示挂起状态。也就是避免fallback的调用。 ## 2023.06.26-2023.06.30 1. amis 1.1 触发其他组件的动作 - 通过配置`componentId`来触发指定组件的动作 - 动作表:submit,reset,clear,validate,reload,setValue,static,nonstatic ```JS "body": [ { "type": "button", "label": "选中选项卡2", "level": "primary", "onEvent": { "click": { "actions": [ { "actionType": "changeActiveKey", "componentId": "tabs-change-receiver", "args": { "activeKey": 2 } } ] } } }, { "id": "tabs-change-receiver", // 设置id "type": "tabs", "tabs": [ { "title": "选项卡1", "body": "选项卡内容1" }, { "title": "选项卡2", "body": "选项卡内容2" }, ] } ] ``` 1.2 combo下的内容默认为一行,如果需要换行需要设置mutline:true 1.3 设置了value 且设置了clearValueOnHidden的项 在隐藏的时候会上传value值 2. react api 2.1 createRoot使用两种方法返回一个对象:render和unmount - ###### `root.render(reactNode)` - ###### `root.unmount()` - 调用root.unmount来销毁React根中的渲染树。 - 调用root.unmount将卸载根中的所有组件,并从根DOM节点“分离”React,包括删除树中的任何事件处理程序或状态。 ## 2023.07.03-2023.07.07 1. amis 1.1 前端一次性加载 如果你的数据并不是很大,而且后端不方便做分页和条件过滤操作,那么通过配置`loadDataOnce`实现前端一次性加载并支持分页和条件过滤操作 1.2 clearValueOnHidden踩坑 clearValueOnHidden是在配置项被隐藏时清除该字段 ```JS { // A type: 'select', label: 'aaaa', name: 'category', visibleOn: 'this.ab !==1', required: true, clearable: true, options: OPTIONS, disabledOn: '!this.isEdit', clearValueOnHidden: true, }, { // B type: 'select', label: 'bbbb', name: 'category', visibleOn: 'this.ab ===1', required: true, options: OPTIONS, disabledOn: '!this.isEdit', clearValueOnHidden: true, }, ``` 如果此时ab为1,配置A被隐藏,触发clearValueOnHidden,category字段被删除,配置B也无法显示,所以此时不能配置clearValueOnHidden。 1.3 联动查询 需求背景:点击按钮筛选列表 ``` controls:[ { type: 'action', actionType: 'reload', label: '全部', target: `目标crud?查询参数1=xxx&查询参数2=xxx&查询参数3=xxx`, ] ``` 2. css方案 2.1 css in js - emotion \- 好处: 更灵活了, \- 坏处: 代码更复杂了,没有提示。 - Styled-component 2.2 css module ```JS { // 定义一下,使用 xxx.module.(less|css) test: /.module.(less|css)$/, include: [path.resolve(__dirname, '../src')], use: [ // 在开发环境中,用 'style-loader', 方便做热更新。 // 生产环境下,放在单独的文件里。 !isDev ? "style-loader" : MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { importLoaders: 2, // 开启 css modules modules: { localIdentName: '[path][name]__[local]--[hash:base64:4]' } } }, "postcss-loader", "less-loader" ] }, // 配置过多使用oneOf ``` 2.3 utility css - Tailwindcss ## 2023.07.10-2023.07.14 1. amis 1.1 链接校验 ```JS { type: 'text', label: '跳转链接', placeholder: '请输入跳转链接', name: 'jumpLink', visibleOn: '+this.xxxxx === 1', clearable: true, required: true, validations: { isUrl: true, urlValidations: true, maxLength: 200, }, validationErrors: { isUrl: '链接格式不正确', urlValidations: '链接不能有多个问号', maxmum: '最大200字符', }, disabledOn: 'this.id && !this.isEdit', }, ``` ## 2023.07.17-2023.07.21 1. 判断对象数组中,某指定项是否和其他指定项同时存在 ```JS // 判断a和b、c是否同时存在 _checkCustomerGrow = (pageData) => a const { config } = pageData || {}; const { list } = config || {}; // 筛选出b、c const allEle = list.filter(item =>item.name === 'b' || item.name === 'c'); // 判断是否存在a const hasA = list.some(item => item.name === 'a'); // 存在a且abc都存在 即 a和b、c是否同时存在 return hasA && allEle.length >= 2; } ``` 2. 数组中去除指定的某几项 ```JS const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // 去除值为 2, 4, 6 的元素 const removedItems = [2, 4, 6]; const newArr = arr.filter(item => !removedItems.includes(item)); console.log(newArr); // Output: [1, 3, 5, 7, 8, 9, 10] ``` 3. 字符串去重 ```JS const str = 'abbcdddeeff'; const uniqueStr = [...new Set(str)].join(''); console.log(uniqueStr); ``` ## 2023.07.24-2023.07.28 1. eslint ```JS { "root": true, // 该属性设置为 true 表示 ESLint 将在找到此配置文件后停止继续查找父级目录中的配置文件 "env": { // 该属性用于指定代码运行的环境 "browser": true, "node": true, "es6": true }, "globals": { // 该属性定义了全局变量 "__DEV__": "readonly", "__PUB__": "readonly", "__TEST__": "readonly", "__VERSION__": "readonly" }, "overrides": [ // 该属性允许对特定文件进行覆盖配置。它包含了两个子对象,分别用于 JavaScript 和 TypeScript 文件的配置。 { "files": [ "*.js", "*.jsx", "*.ts", "*.tsx" ], // 使用 @babel/eslint-parser 解析器,继承了 "airbnb" 规则集 "parser": "@babel/eslint-parser", "extends": "airbnb", "rules": { ........... } }, { "files": [ "*.ts", "*.tsx" ], // 使用 @typescript-eslint/parser 解析器,继承了 "@typescript-eslint/recommended" 规则集 "parser": "@typescript-eslint/parser", "plugins": [ // 这个属性指定了要使用的 ESLint 插件 "@typescript-eslint" ], "extends": [ // 这个属性指定了要继承的 ESLint 规则集 "plugin:@typescript-eslint/recommended" ], "rules": { "@typescript-eslint/no-empty-function": "off", // 关闭了空函数的检查,允许函数体为空 "@typescript-eslint/no-var-requires": "off", // 关闭了 require 语句的检查,允许使用 require "@typescript-eslint/ban-ts-comment": "off" // 关闭了 TypeScript 注释的检查,允许使用 // @ts-ignore 注释来忽略类型检查。 } } ] } ``` ## 2023.07.31-2023.08.04 1. 防止e报any隐式类型 ```JS const handleChange: ChangeEventHandler = (e) => { setInputValue(e.target.value); handleFocus(e); }; ``` 2. 浏览器提供的 5 种 Observer - IntersectionObserver:监听元素和可视区相交部分的比例,在达到某个阈值时触发回调 - MutationObserver:监听对元素属性的修改、对其子节点的增删改 - ResizeObserver:监听大小的改变,当width、height被修改时触发回调 - PerformanceObserver:监听performance数据的行为,记录了就会触发回调,我们可以在回调里上报这些数据 - ReportingObserver:监听过时的api、浏览器干预等报告的打印,在回调里上报,这些是错误监听无法监听到但对了解网页运行情况有用的数据 ## 2023.08.07-2023.08.11 npm、yarn、pnpm 1.06 https://gitee.com/lzy_8920/review/blob/master/node.md ## 2023.08.14-2023.08.18 ## 2023.08.21-2023.08.25 ## 2023.08.28-2023.09.01 ## 2023.09.04-2023.09.08 1. ```js const event = new Event('xxxxx'); document.dispatchEvent(event); document.addEventListener('xxxxx', this._xxxx); ``` ## 2023.09.18-2023.09.22 1. 页面同步逻辑 除props state外 表单1中的this 需要同步到表单2 中的this 2. z-index 在iOS设备上,fixed元素的z-index可能不起作用。这是因为iOS对于fixed元素的位置和z-index的处理方式和其他浏览器不同。iOS会将fixed元素渲染在单独的图层中,而其他元素则渲染在不同的图层中,因此如果设置了z-index可能不会生效。 解决这个问题的方法是在需要覆盖fixed元素的元素上也设置一个fixed定位,并设置一个更高的z-index。或者采用transform和z-index结合的方式来实现覆盖fixed元素的效果 ```JS .parent { position: relative; } .overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); z-index: 999; transform: translateZ(0); } ``` 3. 弹窗内调用拉起弹窗方法使用表单内的 ## 2023.10.09-2023.10.13 1. 职责链模式优化异步任务(https://zhuanlan.zhihu.com/p/612391198) - 页面中会有一系列的异步任务需要串行处理,每个任务的代码是散落在各处的、耦合的。目标将这些任务集中管理。 - 如 任务1-弹窗 任务2-弹窗 任务3-自动滚动 任务4-自动点击 - 首先通过抽象类约束单个任务 ```ts enum TaskStatus { Init, Done, } export abstract class TaskHandler { static status = TaskStatus.Init; constructor(props) { this.setBaseContext(props?.baseContext); } printLog = true; baseContext: any = null; // 共享上下文 nextHandler: TaskHandler = null; // 下一个任务的引用 // 命中当前任务的条件, 子类必须重写 isHit = () => { console.error('子类必须实现isHit方法'); return true; }; // 当前任务的核心实现, 子类必须重写 hitHandler = () => { console.error('子类必须实现hitHandler方法'); }; next = (param?) => { if (TaskHandler.status === TaskStatus.Done) return; if (this.nextHandler === null) { TaskHandler.status = TaskStatus.Done; } else { this.nextHandler.leaveHander.call(this.nextHandler, param); } }; leaveHander = () => { if (this.baseContext === null) { throw new Error('====开头的任务必须传入baseContext====='); } if (this.isHit()) { this.print('taskManager hit任务:', this); this.hitHandler(); return; } this.print('taskManager jump任务:', this); this.next(); }; currentTaskDone?: () => void; // 部分任务的结束可能依赖外部触发 setNextHandler = (handler: TaskHandler) => { this.nextHandler = handler; this.nextHandler.setBaseContext(this.baseContext); return this.nextHandler; }; setBaseContext = (context?) => { if (context) { this.baseContext = context; } }; print = (...params) => { if (this.printLog) { console.log(...params); } }; start = this.leaveHander; } ``` - 需要明确每个任务的触发时机,结束时机,并抽象到一个类中 ```ts import { TaskHandler } from './index'; export default class P1AbManager extends TaskHandler { constructor(props) { super(props); if (props.isHit) this.isHit = props.isHit; this.props = props; } props:any; hitHandler = () => { const { xxxx } = this.baseContext; if (xxxxx) { xxxxx } if (xxxxx) { this.next(); return; } }; // 依赖外部触发 currentTaskDone = () => { this.next(); }; } ```