为什么使用hooks
class 在组件之间复用状态逻辑很难,由高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。| Hook 从组件中提取状态逻辑, 使得这些逻辑可以单独复用
【拆分】class 组件不好理解, 每个生命周期常常包含一些不相关的逻辑【例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,】| Hook 将组件中相互关联的部分拆分成更小的函数
class组件需要理解各种this的指向,class 不能很好的压缩、
react hooks带来了什么便利
在没有 hooks 之前,我们使用函数定义的组件中,不能使用 React 的 state、各种生命周期钩子类组件的特性。在 React 16.8 之后,推出了新功能: Hooks,通过 hooks 我们可以再函数定义的组件中使用类组件的特性。
好处:
跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
相比而言,类组件的实现更为复杂
不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
时刻需要关注this的指向问题;
代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
状态与 UI 隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。
注意:
避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
不能在useEffect中使用useState,React 会报错提示;
类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存
Hooks的缺点
学习成本 2.初学者在useEffect里面会存在死循环的问题
3.自定义 hook 也是 hook,只能在函数组件的顶层使用,不能在 if 或 for 循环中使用;
diff算法:
目的就是对比两次渲染的结果,找到可复用的部分,剩下的进行删除和新增就可以了
虚拟DOM
虚拟DOM表示真实DOM的JS对象,如何做到快速找到两个虚拟DOM之间存在的差异,来可以最小化的更新视图,就需要vue的diff算法
干前端的都知道DOM操作是性能杀手,因为操作DOM会引起页面的回流或者重绘。那么为什么现在的框架都使用虚拟DOM呢?因为使用虚拟DOM可以提高代码的性能下限,并极大的优化大量操作DOM时产生的性能损耗【因为 VM 并不是真实的操作 DOM,通过 diff 算法可以避免一些不变要的 DOM 操作,从而提高了性能。】
69. react的虚拟DOM是怎么实现的
React 是把真实的 DOM 树转换为 JS 对象树,也就是 Virtual DOM。每次数据更新后,重新计算 VM,并和上一次生成的 VM 树进行对比,对发生变化的部分进行批量更新。除了性能之外,VM 的实现最大的好处在于和其他平台的集成。
React的diff算法:
diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,将更新补丁作用于真实 DOM,以最小成本完成视图更新
具体的流程是这样的:
真实 DOM 与虚拟 DOM 之间存在一个映射关系。这个映射关系依靠初始化时的 JSX 建立完成;
当虚拟 DOM 发生变化后,就会根据差距计算生成 patch,这个 patch 是一个结构化的数据,内容包含了增加、更新、移除等;
最后再根据 patch 去更新真实的 DOM,反馈到用户的界面上。
更新时机:更新发生在setState、Hooks 调用等操作以后。
遍历算法:深度优先遍历算法。因为广度优先遍历可能会导致组件的生命周期时序错乱,而深度优先遍历算法就可以解决这个问题。
优化策略:策略一:忽略DOM树跨层级操作场景,提升比对效率。策略二:如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构【React.memo 可以提高性能的原因。】策略三:同一层级的子节点,可以通过标记 key 的方式进行列表对比
React 16 引入Fiber 设计:使得整个更新过程可以随时暂停恢复
性能优化
useCallback + memo 避免组件不必要重的重复渲染
useMemo 避免组件在每次渲染时都进行高开销的计算
react数据通信
类组件和函数式组件的区别
语法上:函数组件是一个纯函数, 它接收一个props对象返回一个react元素
而类组件需要去继承React.Component并且创建render函数返回react元素
状态管理:因为函数组件是一个纯函数,你不能在组件中使用setState(),这也是为什么把函数组件称作为无状态组件。
生命周期钩子:你不能在函数组件中使用生命周期钩子,原因和不能使用state一样,所有的生命周期钩子都来自于继承的React.Component中。
高阶组件HOC
const EnhancedComponent = higherOrderComponent(WrappedComponent);
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧
高阶组件的参数为一个组件返回一个新的组件
组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件
受控组件
非受控组件,即组件的状态不受React控制的组件,例如下边这个
import React, { Component } from 'react';
import ReactDOM from 'react-dom';class Demo1 extends Component {render() {return (<input />)}
}ReactDOM.render(<Demo1/>, document.getElementById('content'))
在这个最简单的输入框组件里,我们并没有干涉input中的value展示,即用户输入的内容都会展示在上面
受控组件就是组件的状态受React控制。上面提到过,我们把input的value属性和state结合在一起,再绑定onChange事件,实时更新value值就行了。
class Demo1 extends Component {constructor(props) {super(props);this.state = {value: props.value}}handleChange(e) {this.setState({value: e.target.value})}render() {return (<input value={this.state.value} onChange={e => this.handleChange(e)}/>)}
}
JSX
JSX即JavaScript XML。一种在React组件内部构建标签的类XML语法。
class MyComponent extends React.Component {render() {let props = this.props; return (<div className="my-component"><a href={props.url}>{props.name}</a></div>);}
}
优点:
允许使用熟悉的语法来定义 HTML 元素树;
提供更加语义化且移动的标签;
程序结构更容易被直观化;
抽象了 React Element 的创建过程;
可以随时掌控 HTML 标签以及生成这些标签的代码;
是原生的 JavaScript。
React是啥
React是一个简单的javascript UI库,用于构建高效、快速的用户界面。
它是一个轻量级库,因此很受欢迎。它遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效。
它使用虚拟DOM来有效地操作DOM。
它遵循从高阶组件到低阶组件的单向数据流。
常见的hook
状态钩子 (useState): 用于定义组件的 State,类似类定义中 this.state 的功能
生命周期钩子 (useEffect): 类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect),这里可以看做componentDidMount、componentDidUpdate和componentWillUnmount的结合。
useContext: 获取 context 对象,不需要死板的一层一层的传递数据
useCallback: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;
useMemo: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;
useRef: 获取组件的真实节点【常用一点】或者其他的值,比较灵活;[class里面如果把 refs 放到 React 组件中,那么我们获得的就是组件的实例]
useReducer 是useState的复杂版本,和redux类似.
[
简单总结一下:reducer是一个利用action提供的信息,将state从A转换到B的一个纯函数,具有一下几个特点:
语法:(state, action) => newState
Immutable:每次都返回一个newState, 永远不要直接修改state对象
Action:一个常规的Action对象通常有type和payload(可选)组成
type: 本次操作的类型,也是 reducer 条件判断的依据
payload: 提供操作附带的数据信息
]
react-router里的<Link>和<a>标签的区别
对比 标签, Link 避免了不必要的重新渲染。【Link 的 “跳转” 行为只会触发相匹配的对应的页面内容更新,而不会刷新整个页面】
而 a 标签就是普通的超链接了,用于从当前页面跳转到href指向的另一个页面(非锚点情况)。
redux
核心描述
单一数据源:整个应用的全局 state 被存储在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
State 是只读的:唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事情的普通对象。
使用纯函数来执行修改:为了描述 action 如何改变 state tree,你需要编写纯的 reducers。
知识拓展
什么时候应该使用 redux:
在应用的大量地方,都存在大量的状态
应用状态会随着时间的推移而频繁更新
更新该状态的逻辑可能很复杂
中型和大型代码量的应用,很多人协同开发
reducer 是一个函数,接收当前的 state 和一个 action 对象,必要时决定如何更新状态,并返回新状态。reducer 必须符合以下规则:
仅使用 state 和 action 参数计算新的状态值
禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做不可变更新
禁止任何异步逻辑、依赖随机值或导致其他副作用代码
reducer 遵守上述规则的原因:
redux 的目标之一是使代码可预测。当函数的输出仅根据输入参数计算时,更容易理解该代码的工作原理并对其进行测试
如果一个函数依赖于自身之外的变量,或者随机行为,你永远不知道运行它时会发生什么
如果一个函数 mutate 了其他对象,比如它的参数,这可能会意外地改变应用程序的工作方式。这可能是错误的常见来源
不可变更新(Immutability),不能在 Redux 中更改 state 的原因:
会导致bug,例如 UI 未正确更新以显示最新值
更难理解状态更新的原因和方式
编写测试变的困难
打破了正确使用“时间旅行调试”的能力
违背了 Redux 的预期精神和使用模式
自定义hooks和普通函数的区别
官方提供的Hooks只应该在React函数组件/自定义Hooks内调用,而不应该在普通函数调用。
自定义Hooks能够调用诸如useState、useRef等,普通函数则不能。由此可以通过内置的Hooks获得Fiber的访问方式,可以实现在组件级别存储数据的方案等。
自定义Hooks需要以use开头,普通函数则没有这个限制。使用use开头并不是一个语法或者一个强制性的方案,更像是一个约定,就像是GET请求约定语义不携带Body一样,使用use开头的目的就是让React识别出来这是个Hooks,从而检查这些规则约束,通常也会使用ESlint配合eslint-plugin-react-hooks检查这些规则,从而提前避免错误的使用。
如果我们使用Hooks的话,实际上由于可以调用useState、useRef等Hooks,从而获取了对于这个Fiber的访问方法,那么也就相当于我们可以将状态或者说数据存放于当前节点当中,而不是类似于普通函数在全局中共享。当然如果需要全局共享状态的话,状态管理方案是更好的选择,而不是全局变量。
setState是同步的还是异步 未完
https://mp.weixin.qq.com/s/GQQlE5WD9xzExixZhjMKTw
先说结论,首先,同步和异步主要取决于它被调用的环境。
如果 setState 在 React 能够控制的范围被调用,它就是异步的。
比如合成事件处理函数, 生命周期函数, 此时会进行批量更新, 也就是将状态合并后再进行 DOM 更新。
如果 setState 在原生 JavaScript 控制的范围被调用,它就是同步的。
比如原生事件处理函数中, 定时器回调函数中, Ajax 回调函数中, 此时 setState 被调用后会立即更新 DOM 。
为什么会这样呢?
其实,我们看到的所谓的 “异步”,是开启了 “批量更新” 模式的。
批量更新模式可以减少真实 DOM 渲染的次数,所以只要是 React 能够控制的范围,出于性能因素考虑,一定是批量更新模式。批量更新会先合并状态,再一次性做 DOM 更新。
react18之后。
setState都会表现为异步(即批处理)。
setState是同步的还是异步?chatGPT的回答,哈哈哈哈
具体来说,当你在组件中调用 setState 方法时,React 不会立即更新组件的 state,而是将新的 state 加入到队列中,等到合适的时机再去更新组件的 state。这个合适的时机就是 React 的更新机制中所谓的“事务”,React 会将一批 setState 操作合并为一个批次,在合适的时候统一去更新组件的 state。
由于 setState 是异步的,所以如果你需要在 setState 之后获取到最新的 state,可以在 setState 的第二个参数中传入一个回调函数,在回调函数中获取到最新的 state。比如:
jsxCopy codethis.setState({ count: this.state.count + 1 }, () => {
console.log(this.state.count); // 打印最新的 count 值
});
除此之外,React 还提供了另外一种方式来更新组件的 state,即使用函数式的 setState。函数式的 setState 可以接受一个函数作为参数,这个函数可以接受前一个 state 作为参数,返回一个新的 state 值。使用函数式的 setState 可以避免多次 setState 导致的性能问题,比如:
jsxCopy codethis.setState(prevState => ({
count: prevState.count + 1
}));
总之,理解 setState 的异步更新机制对于编写高效的 React 组件非常重要。
react的性能优化
8.4JSX本质是什么
JSX 是一个 JavaScript 的语法扩展,它看起来像是一种模板语言,但它具有 JavaScript 的全部功能。
React.JSX转换成真实DOM过程
JSX通过babel最终转化成React.createElement这种形式。返回虚拟DOM(vnode)
在转化过程中,babel在编译时会判断 JSX 中组件的首字母:
当首字母为小写时,其被认定为原生 DOM 标签,createElement 的第一个变量被编译为字符串
当首字母为大写时,其被认定为自定义组件,createElement 的第一个变量被编译为对象
虚拟DOM会通过ReactDOM.render进行渲染成真实DOM,使用方法如下:
ReactDOM.render(<App />, document.getElementById("root"));
14.1关于react hooks
14.2函数组件的特点
14.4useEffect生命周期模拟
DIdMount || WillUnMount
DidUpdate
fetch...直接放在Fetch组件内部,而不是用useEffect包裹起来也是可以的
但是!
理论上可以,useeffect本身是一个副作用,直接在函数里面进行这样的一个副作用,问题是很大的。因为组件随时都有可能去更新,比如副组件的更新会导致子租件更新,或者内部一些状态的更新导致组件的变化,或者props的变化等等。每一次更新,这些东西(fetch里面)都是要再次执行的,这样肯定不行啊。
所以上面console.log打印的次数是7次,放在useEffect里面只会打印2次。
这个是为什么呢?
useEffect(()=>{})
这样写执行两次:在组件每一次更新的时候都会去执行,第一次更新是初始的渲染(相当于didmount),第二次是setResult更新完了之后渲染(相当于didUpdate)
useEffect(()=>{},[])
这样写只会执行一次,后面不会再执行(相当于didmount)
总结:
class 组件 | Hooks 组件 |
constructor | useState |
getDerivedStateFromProps | useEffect 手动对比 props, 配合 useState 里面 update 函数 |
shouldComponentUpdate | React.memo |
render | 函数本身 |
componentDidMount | useEffect 第二个参数为[] |
componentDidUpdate | useEffect 配合useRef |
componentWillUnmount | useEffect 里面返回的函数 |
componentDidCatch | 无 |
getDerivedStateFromError | 无 |
14.12使用规范
ps: 不能在一起可能被打断的程序后面,如上面所示
14 13 为何Hooks要依赖于调用顺序
如何不按照这样的规则可能发生一些错乱
react-router的理解
react-router可以实现无刷新的条件下:改变浏览器地址栏中的URL地址,切换显示不同的页面,
两种模式:
hash 模式:在url后面加上#,如http://127.0.0.1:5500/home/#/page1
history 模式:允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录
这两种模式对应的组件为:BrowserRouter、HashRouter: 作为最顶层组件包裹其他组
react-router的使用
Route用于路径的匹配,然后进行组件的渲染,
path 属性:用于设置匹配到的路径
component 属性:设置匹配到路径后,渲染的组件
render 属性:设置匹配到路径后,渲染的内容
exact 属性:开启精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件
import { BrowserRouter as Router, Route } from "react-router-dom";export default function App() {return (<Router><main><nav><ul><li>< a href="/">Home</ a></li><li>< a href="/about">About</ a></li><li>< a href="/contact">Contact</ a></li></ul></nav><Route path="/" render={() => <h1>Welcome!</h1>} /></main></Router>);
}
Link、NavLink:
路径的跳转是使用Link组件,最终会被渲染成a元素.其中属性to代替a标题的href属性,
NavLink是在Link基础之上增加了一些样式属性,例如组件被选中时,发生样式变化,则可以设置NavLink的一下属性:
activeStyle:活跃时(匹配时)的样式
activeClassName:活跃时添加的class
<NavLink to="/" exact activeStyle={{color: "red"}}>首页</NavLink>
<NavLink to="/about" activeStyle={{color: "red"}}>关于</NavLink>
<NavLink to="/profile" activeStyle={{color: "red"}}>我的</NavLink>
redirect
用于路由的重定向
switch
swich组件的作用适用于当匹配到第一个组件的时候,后面的组件就不应该继续匹配
<Switch><Route exact path="/" component={Home} /><Route path="/about" component={About} /><Route path="/profile" component={Profile} /><Route path="/:userid" component={User} /><Route component={NoMatch} />
</Switch>
如果不使用switch组件进行包裹,相同 path 的就会被匹配到,然后一起展示。
除了一些路由相关的组件之外,react-router还提供一些hooks,如下:
useHistory
useParams
useLocation
react-router的参数传递
这些路由传递参数主要分成了三种形式:
动态路由的方式
search传递参数
to传入对象
动态路由
动态路由的概念指的是路由中的路径并不会固定
例如将path在Route匹配时写成/detail/:id,那么 /detail/abc、/detail/123都可以匹配到该Route
<NavLink to="/detail/abc123">详情</NavLink><Switch>... 其他Route<Route path="/detail/:id" component={Detail}/><Route component={NoMatch} />
</Switch>
获取参数方式如下:
console.log(props.match.params.xxx)
search传递参数
在跳转的路径中添加了一些query参数;
<NavLink to="/detail2?name=why&age=18">详情2</NavLink><Switch><Route path="/detail2" component={Detail2}/>
</Switch>
获取形式如下:
console.log(props.location.search)
to传入对象
传递方式如下:
<NavLink to={{pathname: "/detail2", query: {name: "kobe", age: 30},state: {height: 1.98, address: "洛杉矶"},search: "?apikey=123"}}>详情2
</NavLink>
获取参数的形式如下
console.log(props.location)
react-router的原理
https://mp.weixin.qq.com/s/35ITObjajBOnievRW4JhCg
https://juejin.cn/post/6886290490640039943
不会导致浏览器向服务器发送请求,就不会刷新页面:
原理【hash】:改变路由: window.location.hash 属性获取和设置 hash 值。 监听路由:onhashchange
window.addEventListener('hashchange',function(e){/* 监听改变 */
})
【hash模式路由就是利用 hashchange 事件监听 URL 的变化,从而进行 DOM 操作来模拟页面跳转】
原理【router】:改变路由:使用 window.history.pushState() 【修改 url 也可以不刷新页面】监听路由:触发 popstate 事件
window.addEventListener('popstate',function(e){/* 监听改变 */
})
React 18的新特性
1.批处理: react18以前批处理只限于 React 原生事件内部的更新。React 18批处理支持处理的操作范围扩大了:Promise,setTimeout,native event handlers 等这些非 React 原生的事件内部的更新也会得到合并:
// Before: only React events were batched.setTimeout(() => {setCount((c) => c + 1);setFlag((f) => !f);// React will render twice, once for each state update (no batching)
}, 1000);// After: updates inside of timeouts, promises,// native event handlers or any other event are batched.setTimeout(() => {setCount((c) => c + 1);setFlag((f) => !f);// React will only re-render once at the end (that's batching!)
}, 1000);
2.Transitions
Transitions 是 React 中一个用于区分高优更新和非高优更新的新概念。
starTransition:用于标记非紧急的更新,用 starTransition 包裹起来就是告诉 React,这部分代码渲染的优先级不高,可以优先处理其它更重要的渲染。
import { startTransition } from "react";// Urgent
setSliderValue(input);// Mark any state updates inside as transitions
startTransition(() => {// Transition: Show the results, non-urgentsetGraphValue(input);
});
useLayoutEffect和useEffect的区别
useEffect 是异步执行的,而useLayoutEffect是同步执行的。
useEffect 的执行时机是浏览器完成渲染之后,而 useLayoutEffect 的执行时机准确来说应该是DOM变更之后,浏览器渲染到用户界面之前】,和 componentDidMount 等价。所以也就是说我们最好把操作 dom 的相关操作放到 useLayouteEffect 中去
建议
除非需要使用同步更新 DOM 布局,否则请使用 useEffect。
避免在 useLayoutEffect 中进行阻塞操作,这可能会导致性能问题。
避免在同一个组件中同时使用 useEffect 和 useLayoutEffect。
useTimeout 实现
function useTimeout(callback, delay) {const memorizeCallback = useRef();useLayoutEffect(() => {memorizeCallback.current = callback;}, [callback]);useEffect(() => {if (delay !== null) {const timer = setTimeout(() => {memorizeCallback.current();}, delay);return () => {clearTimeout(timer);};}}, [delay]);
};
如何使用
// callback 回调函数, delay 延迟时间useTimeout(callback, delay);
紧接着上面一个题目,讲讲hooks的闭包陷阱
https://mp.weixin.qq.com/s/zJn-eY5Z90zjXFomWA116w
https://mp.weixin.qq.com/s/0P7eWSNQNKWroDIlcgHBVw
https://mp.weixin.qq.com/s/uJe7gLjFX5O4lA2ezc656A
hooks 的闭包陷阱是指 useEffect 等 hook 中用到了某个 state,但是没有把它加到 deps 数组里,导致 state 变了,但是执行的函数依然引用着之前的 state。
它的解决方式就是正确设置 deps 数组,把用到的 state 放到 deps 数组里,这样每次 state 变了就能执行最新的函数,引用新的 state。同时要清理上次的定时器、事件监听器等。
我们举了这样一个例子:
import { useEffect, useState } from 'react';function Dong() { const [count,setCount] = useState(0); useEffect(() => { setInterval(() => { setCount(count + 1); }, 500); }, []); useEffect(() => { setInterval(() => { console.log(count); }, 500); }, []); return <div>guang</div>;}export default Dong;
每次打印都是 0 :
解决方式就是把 count 设置到 deps 里,并添加清理函数:
import { useEffect, useState } from 'react';function Dong() { const [count,setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount(count + 1); }, 500); return () => clearInterval(timer); }, [count]); useEffect(() => { const timer = setInterval(() => { console.log(count); }, 500); return () => clearInterval(timer); }, [count]); return <div>guang</div>;}export default Dong;
这样就能解决闭包陷阱:
但是这种解决闭包陷阱的方式用在定时器上不是很合适。
为什么呢?
【因为现在每次 count 变了就会重置定时器,那之前的计时就重新计算,这样就会导致计时不准。
所以,这种把依赖的 state 添加到 deps 里的方式是能解决闭包陷阱,但是定时器不能这样做。
那还有什么方式能解决闭包陷阱呢?】===》useRef
useRef 能解决闭包陷阱的原因是 useEffect 等 hook 里不直接引用 state,而是引用 ref.current,这样后面只要修改了 ref 中的值,这里取出来的就是最新的
封装成个自定义 hook:
function useInterval(fn, time) {const ref = useRef();useLayoutEffect(() => {ref.current = fn;}, [fn]);useEffect(() => {if(time !== null) {setInterval(() => ref.current(), time);return () => {clearTimeout(timer);};}}, [time]);
}// setTimeout
function useTimeout(callback, delay) {const memorizeCallback = useRef();useLayoutEffect(() => {memorizeCallback.current = callback;}, [callback]);useEffect(() => {if (delay !== null) {const timer = setTimeout(() => {memorizeCallback.current();}, delay);return () => {clearTimeout(timer);};}}, [delay]);
};
// 然后组件代码就可以简化了:
function Dong() {const [count, setCount] = useState(0);useInterval(() => {setCount(count + 1);}, 500);useInterval(() => {console.log(count);}, 500);return <div>guang</div>;
}
react hooks中如何捕获异常
-在React项目中,因为事件处理程序总是需要写 try/catch,不胜其烦。
-虽然可以丢给window.onerror或者 window.addEventListener("error")去处理,但是对错误细节的捕获以及错误的补偿是极其不友好的。
-实现: 其基本原理就是利用 useMemo和之前封装的observerHandler
export function useCatch<T extends (...args: any[]) => any>(callback: T, deps: DependencyList, options: CatchOptions =DEFAULT_ERRPR_CATCH_OPTIONS): T { const opt = useMemo( ()=> getOptions(options), [options]);const fn = useMemo((..._args: any[]) => {const proxy = observerHandler(callback, undefined, function (error: Error) {commonErrorHandler(error, opt)});return proxy;}, [callback, deps, opt]) as T;return fn;
}
redux使用的原则
单一数据源:整个应用的全局 state 被存储在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
State 是只读的:唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事情的普通对象。
使用纯函数来执行修改:为了描述 action 如何改变 state tree,你需要编写纯的 reducers。
React的事件代理机制
为什么需要事件代理:【1.节约内存,提升页面性能 2.抹平了浏览器之间的差异,简化了事件逻辑】
将事件都代理到了根节点上,减少了事件监听器的创建,节省了内存。
磨平浏览器差异,开发者无需兼容多种浏览器写法。如想阻止事件传播时需要编写event.stopPropagation() 或 event.cancelBubble = true,在 React 中只需编写event.stopPropagation()即可。
对开发者友好。只需在对应的节点上编写如onClick、onClickCapture等代码即可完成click事件在该节点上冒泡节点、捕获阶段的监听,统一了写法。
如何做的:【所有可监听的原生事件均绑定到了root节点上并由对应的监听器处理】
不同版本的区别:【 v17 之前 React 事件都是绑定在 document 上,v17 之后 React 把事件绑定在应用对应的容器上(根 root 上),将事件绑定在同一容器统一管理, 「有利于微前端的实施和多版本 react 共存,因为微前端都是一个 document 下面多个容器应用,防止微应用的所有事件都直接绑定在原生的 document 元素上,造成一些不可控的情况」】
React事件和原生事件的执行顺序
https://mp.weixin.qq.com/s/lCHeWE5DrbxEYIdG60aIsw
在 React16 中,对 document 的事件委托都委托在冒泡阶段,当事件冒泡到 document 之后触发绑定的回调函数,在回调函数中重新模拟一次 捕获-冒泡 的行为,所以 React 事件中的e.stopPropagation()无法阻止原生事件的捕获和冒泡,因为原生事件的捕获和冒泡已经执行完了。
在 React17 中,对 React 应用根 DOM 容器的事件委托分别在捕获阶段和冒泡阶段。即:
当根容器接收到捕获事件时,先触发一次 React 事件的捕获阶段,然后再执行原生事件的捕获传播。所以 React 事件的捕获阶段调用e.stopPropagation()能阻止原生事件的传播。
当根容器接受到冒泡事件时,会触发一次 React 事件的冒泡阶段,此时原生事件的冒泡传播已经传播到根了,所以 React 事件的冒泡阶段调用e.stopPropagation()不能阻止原生事件向根容器的传播,但是能阻止根容器到页面顶层的传播。
为什么需要合成事件
1.节约内存,提升页面性能 2.抹平了浏览器之间的差异,简化了事件逻辑
React的服务端渲染怎么做?原理是什么【未完】
是什么:【服务端渲染(Server-Side Rendering ,简称SSR),指由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程】
其解决的问题主要有两个:
可以支持SEO,因为SSR可以让搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。【SEO可以让网站有更多的流量和排名,也就是说用户更容易搜索到】
加速首屏加载,解决首屏白屏问题
如何做:
手动搭建一个 SSR 框架
使用成熟的SSR 框架,如 Next.JS
redux中间件16??
??
React.memo()和useMemo
React.memo() 是一个高阶组件,我们可以使用它来包装我们不想重新渲染的组件,除非其中的 props 发生变化
useMemo() 是一个 React Hook,可以让你在重新渲染之间缓存计算的结果。 我们可以使用它来确保计算的结果仅在其依赖项之一发生变化时才重新计算
import { useMemo } from 'react';function TodoList({ todos, tab }) {const visibleTodos = useMemo(() => filterTodos(todos, tab),[todos, tab]);// ...
}
React是否支持给标签设置自定义标签,比如给video标签设置webkit-playsinline
*react 15中无法被渲染
*react16中如果自定义的属性不是 string, number 或者 object,该属性依然会被忽略。
所以目前可以这样添加 webkit-playsinline 属性:
<video width="750" height="500" controls webkit-playsinline="true"><source src="https://media.w3.org/2010/05/sintel/trailer.mp4" type="video/mp4"/>
</video>
另外,还可以通过 setAttribute 进行设置,比如:
const video = document.createElement('video');
video.setAttribute('playsinline', 'true'); // fixes autoplay in webkit (ie. mobile safari)
微前端icestark的原理
什么是微前端
微前端是一种前端架构模式,旨在通过将前端应用程序拆分为更小、更可管理的部分来帮助组织更好地构建和维护大型应用程序。它的核心思想是将单个应用程序分解为多个微型前端,每个微型前端都独立运行、独立部署、独立开发,同时保持与其他微型前端之间的通信和协作。
1.基于浏览器端集成的微前端框架包括 Single-SPA 和 qiankun。
2.基于 Web Component 的微前端框架
3.最早的解决方案是采用iframe的方法【子应用之间使用跳转。但这个方案最大问题是导致页面重新加载和白屏。iframe 如果不去做一些特殊处理,嵌入的页面双滚动条、路由无法同步、页面内部存在弹出遮罩交互等问题都是体验纬度上的硬伤】
基于浏览器端集成的微前端框架包括 Single-SPA 和 qiankun。
single-spa是一个很好的微前端基础框架,而qiankun框架就是基于single-spa来实现的,在single-spa的基础上做了一层封装,也解决了single-spa的一些缺陷。
如何使用single-spa来完成微前端的搭建。
实现原理:首先在基座应用中注册所有App的路由,single-spa保存各子应用的路由映射关系,充当微前端控制器Controler。URL响应时,匹配子应用路由并加载渲染子应用。开始走整套生命周期流程,上图便是对single-spa完整的描述。
Qiankun的优势
基于 single-spa[1] 封装,提供了更加开箱即用的 API。【不需要自己手动的去写调用子应用JS的方法,只需要你传入响应的apps的配置即可,会帮助我们去加载。】
技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
样式隔离,确保微应用之间样式互相不干扰。
JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
icestark: 阿里出的类似于 single-spa 实现,React 技术栈友好
single-spa: 最主流微前端方案,通过监听 url change 事件,在路由变化时匹配到渲染的子应用并进行渲染
qiankun: 基于 single-spa 封装,增加 umi 特色,增加沙箱机制(JS、ShadowDOM 等)
Micro App: 是京东出的一款基于 Web Component 原生组件进行渲染的微前端框架。通过 CustomElement 结合自定义的 ShadowDom,将微前端封装成一个类 WebComponent 组件,从而实现微前端的组件化渲染。(这个比较有意思,可以研究看看)
icestark: 阿里出的类似于 single-spa 实现,React 技术栈友好
EMP: 基于 Webpack5 Module Federation 搭建的微前端方案
Garfish: 字节跳动的轮子,也是 single-spa 理念
其他还有 Luigi OpenComponents piral 等
基于 Web Component 的微前端框架【看不懂】
使用 Web Component 技术来实现微前端架构。它们将前端应用程序拆分成多个 Web Component,然后使用浏览器的原生 Web Component 技术将它们动态地组合在一起。
micro-app并没有沿袭single-spa的思路,而是借鉴了WebComponent的思想,通过CustomElement结合自定义的ShadowDom,将微前端封装成一个类WebComponent组件,从而实现微前端的组件化渲染。
并且由于自定义ShadowDom的隔离特性,micro-app不需要像single-spa和qiankun一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改webpack配置,是目前市面上接入微前端成本最低的方案。
ICESTARK知识梳理
icestark 是一个面向大型系统的微前端解决方案:icestark 在保证一个系统的操作体验基础上,实现各个微应用的独立开发和发版,主应用通过 icestark 管理微应用的注册和渲染,将整个系统彻底解耦。
主应用:
系统整体 Layout 的设计
所有微应用的配置与注册
主应用尽量避免包含具体页面的 UI 代码,如果主应用做了过多的事情会带来以下问题:
主应用样式代码太多,会增加微应用和主应用样式冲突概率
主应用为微应用提供其他能力比如一些全局 API,会破坏微应用的独立性,增加相互的耦合
主应用本质是一个中心化的部分,变更后原则上需要回归所有微应用,因此需要保证职责的简单,越简单的东西越稳定
微应用:
又称子应用,微应用通常是一个单页面应用(SPA),可能包含一个或多个路由页面,一般情况下不存在多个微应用同时运行的场景。有以下特点:
本身是普通的前端应用,负责具体的业务逻辑;
可以独立交付(开发、部署、运行),但是一般会集成到主应用中运行;
如有必要,甚至能集成到不同的主应用中。
微应用的生命周期:
在 icestark 中,微应用是一个 具有生命周期 的前端资源。icestark 中有两个生命周期,分别是:
微应用挂载到主应用
微应用从主应用卸载
工作流程
加载模式:icestark 目前支持三种加载模式,分别是 script、fetch 和 import
[cached:默认 false,切换后是否缓存微应用,可能会引起内存占用过大问题,不建议开启]
工作流程如下所示:执行start() [通过 start 开始劫持路由变化,触发微应用的挂载/卸载。] ->路由劫持->根据当前路由查找匹配应用 ->「【如果是第一次渲染=>加载微应用的资源=>执行渲染】 | 【如果不是第一次加载=>执行渲染】」->路由变化->执行卸载->cache[缓存应用]
勾子函数:icestark 在加载微应用的各个阶段提供钩子函数,方便用户监听并执行相应逻辑【被激活,家在前,加载并执行后,渲染前,卸载前,加载或执行错的回调】
样式污染:
页面运行时同时只会存在一个微应用,因此多个微应用不存在样式相互污染的问题,但是主应用和微应用是同时运行的,因此这两者中间可能会存在一些样式相互污染,针对这个问题,我们目前推荐「通过约定避免微应用与主应用的样式相互污染」的方案
规范:
使用 CSS Modules 方案管理样式
主应用自定义基础组件 prefix
微应用避免产生全局样式
JS 污染
正常书写代码是不会有问题的,针对一些特殊情况我们也总结了一些规范。
规范:
微应用避免改变全局状态#
主应用通过钩子记录并恢复全局状态#
应用间通信
1.通过 @ice/stark-data 这个包可以很简单的实现应用间通信,比如全局切换语言微应用响应的场景。@ice/stark-data 支持状态共享【store】和事件监听【event】响应两种方式。
store
get(key) 从 store 中获取变量
set(key, value) 设置/初始化 store 中的变量
on(key, callback, force) 注册变量监听事件,其中 force 为 boolean 类型,true 则表示初始化注册过程中,会强制执行一次
off(key, callback) 删除已经注册的变量监听事件
event
on(key, callback) 注册回调函数,回调函数的入参通过 emit 注入,如 ...rest
off(key, callback) 删除已经注册的回调函数
emit(key, ...rest) 触发已经注册的函数,支持入参
props
在主应用中通过 props 配置用户信息。
微应用可以通过生命周期函数获取到该数据:
性能优化
prefetch,通过预加载子应用资源,可以提升 非首屏首次加载子应用 的渲染速度。简单用法如下
start({
prefetch: true,
})
cached:icestark 提供微应用切换时缓存的能力:在开启该字段后,icestark 不会清理上个微应用的静态资源,不再重复执行脚本资源,以加快微应用二次加载的执行速度。若需要开启该能力,需配置:
页面懒加载
现代框架大多支持 webpack Dynamic Imports,简单来说,就是只有访问某页面时,页面的脚本和样式资源才会加载。比如在 React 中的用法:
import React from 'react';
import { Switch, Route } from 'react-router-dom';function App () {return (<Switch><Route path="/a" component={React.lazy(() => import('./A'))} ></Route><Route path="/b" component={React.lazy(() => import('./B'))} ></Route>...</Switch>>
}
什么是dangerouslySetInnerHTML?
dangerouslySetInnerHTML 是 React 中一个用于设置组件内容的属性,它的命名中带有 dangerously 一词是因为使用该属性可以导致潜在的安全漏洞,如果不正确使用可能会引起 XSS 攻击等安全问题。
通常情况下,在 React 中设置组件内容需要通过 JSX 语法来完成,例如:
但是在某些情况下,我们需要将 HTML 字符串插入到组件内容中,这时可以使用 dangerouslySetInnerHTML 属性。该属性接受一个对象作为参数,对象中有一个 __html 属性,该属性的值是一个字符串,表示要插入到组件内容中的 HTML 内容。例如:
function MyComponent() {return <div dangerouslySetInnerHTML={{ __html: '<span style="color:red;">Hello, world!</span>' }} />;
}
需要注意的是,使用 dangerouslySetInnerHTML 会导致 React 不再对插入的 HTML 内容进行 XSS 过滤等安全检查,因此需要确保插入的 HTML 内容是可信的,或者通过其他手段对内容进行安全检查和过滤,避免潜在的安全问题。同时,尽可能避免使用 dangerouslySetInnerHTML,除非在确实需要将 HTML 字符串作为组件内容时才使用,因为使用该属性会导致组件内容难以维护和测试。
如何让useEffect支持async...await...
看报错,我们知道 effect function 应该返回一个销毁函数(effect:是指return返回的cleanup函数),如果 useEffect 第一个参数传入 async,返回值则变成了 Promise,会导致 react 在调用销毁函数的时候报错。
useEffect返回的函数的执行时机如下:
首次渲染不会进行清理,会在下一次渲染,清除上一次的副作用。
卸载阶段也会执行清除操作
可以这样使用
如何理解react class的面向对象编程
React是一个基于组件化的库,其设计思想来源于面向对象编程(Object-Oriented Programming, OOP)的概念。在React中,组件就是一个类(class),每一个组件都被视为一个独立的对象(instance)。React的类可以继承其他类,包括React自己的Component类,这是典型的面向对象编程中的继承。
使用React类创建的组件可以通过属性(props)和状态(state)来实现对组件的控制。属性可以被父组件传递给子组件,状态可以在组件内部改变。这种属性和状态的控制方式与面向对象编程中的封装概念类似,可以避免组件之间的耦合性,增加代码的可维护性和可重用性。
此外,React类中的方法也可以看作是面向对象编程中的方法,它们被封装在类中,并且可以通过实例来调用。这种方法调用方式也符合面向对象编程的思想。
因此,可以认为React的类组件是一种基于面向对象编程思想的组件化编程方式。通过这种方式,我们可以更加方便地组织和管理代码,提高代码的可重用性和可维护性。
什么叫面向对象编程
面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它以“对象”作为程序设计的基本单元,将数据和处理数据的方法(即“行为”)封装在一起,以便于代码的组织、复用和维护。
在面向对象编程中,对象是一个具有特定状态和行为的实体,它可以与其他对象进行交互。每个对象都是由一个类(Class)定义的,类是对象的模板,描述了对象具有的属性和行为。通过实例化类,程序可以创建出多个对象,这些对象可以独立地进行操作,也可以相互协作实现更复杂的功能。
面向对象编程通常具有以下几个特点:
封装性(Encapsulation):把数据和方法封装在一个对象内部,对外部对象隐藏具体实现细节,只公开必要的接口,保证了程序的安全性和稳定性。
继承性(Inheritance):允许在一个已有类的基础上,派生出一个新的类,新类继承了原有类的属性和方法,并可以扩展或修改其功能,提高了代码的复用性和扩展性。
多态性(Polymorphism):同一种方法或操作可以适用于多个不同类型的对象,提高了代码的灵活性和可扩展性。
面向对象编程被广泛应用于各种编程语言和领域,如Java、Python、C++等编程语言,以及GUI编程、游戏开发、Web开发等领域。