React & Vue
React
生命周期
组件的生命周期可分成三个状态:
挂载阶段
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
// 函数组件
useEffect(() => {
consol.log("componentDidMount")
}, []);
更新阶段
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
getSnapshotBeforeUpdate
该周期函数在render后执行,执行之时DOM元素还没有被更新。该方法返回的一个Snapshot值,作为componentDidUpdate第三个参数传入。的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些UI视觉上的状态
// 函数组件
useEffect(() => {
consol.log("componentDidUpdate")
}, [state]);
卸载阶段
- componentWillUnmount
通过在 useEffect 中返回一个函数,它便可以清理副作用。
清理规则是:
- 首次渲染不会进行清理,会在下一次渲染,清除上一次的副作用;
- 卸载阶段也会执行清除操作。
useEffect(() => {
function handleClick() {
alert(`You clicked`)
}
document.body.addEventListener("click", handleClick, false);
return function cleanup() {
document.body.removeEventListener("click", handleClick, false);
};
}, []);
组件通信
由于React是单向数据流,主要思想是组件不会改变接收的数据,只会监听数据的变化,当数据发生变化时它们会使用接收到的新值,而不是去修改已有的值
因此,可以看到通信过程中,数据的存储位置都是存放在上级位置中
父组件向子组件传递
props
子组件向父组件传递
props回调函数
const Child = ({callback}: {callback: Function}) => {
return <div onClick={() => callback(1)}></div>
}
兄弟组件之间的通信
- 通过使用父组件传递
- content
import React, { useContext } from 'react';
// 创建一个 Context 对象
const ThemeContext = React.createContext('light');
const App = () => {
return (
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
const Toolbar = () => {
// 在任何组件中,我们都可以使用 useContext 来读取当前的 theme context
const theme = useContext(ThemeContext);
return (
<div>
<p>The theme is {theme}</p>
</div>
);
}
export default App;
非关系组件传递
全局状态管理 redux
虚拟DOM
即用js对象表示真实DOM。 在写jsx时,最终会被babel编译成js代码,编辑成React.createElement()
优势
- 避免真实 DOM 数频繁更新,减少多次引起重绘与回流,提高性能
- 跨平台:React 借助虚拟 DOM,带来了跨平台的能力,一套代码多端运行
- 简单
diff算法
用新的数据生成一棵新的树,然后通过 Diff 算法,遍历旧的树,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列。
三种策略
tree diff
在一个tree中,只对同一层级的做比较, 而不是跨层级比较节点,这个策略极大地减少了比较的复杂度,
然后做出新增、删除、更新的决策;
component diff
- 组件类型不相同时,直接删除老的,创建新的
- 类型相同时,继续比较属性和子组件;
element diff
当Component Diffing确定两个元素是同一类型时,React会使用Element Diffing来确定哪些属性需要更新。
对于比较列表节点们,通过key可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置,从而实现了性能优化。
key
元素key属性的作用是用于判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲染
insertMovie() {
const newMovies = [000 ,...this.state.numbers];
this.setState({
movies: newMovies
})
}
// 当拥有key的时候,react根据key属性匹配原有树上的子元素以及最新树上的子元素,像上述情况只需要将000元素插入到最前面位置
// 当没有key的时候,所有的li标签都需要进行修改
Fiber
React Fiber是React提出的一种更新机制,使用链表取代了树,使得组件更新的流程可以被中断恢复;它把组件渲染的工作分片,浏览器闲置时执行,到时会主动让出渲染主线程。
一个 React 组件的渲染主要经历两个阶段:
调度阶段(Reconciler):用新的数据生成一棵新的树,然后通过 Diff 算法,遍历旧的树,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列。
渲染阶段(Renderer):遍历更新队列,通过调用宿主环境的 API,实际更新渲染对应的元素。宿主环境如 DOM,Native 等。
React和Vue的响应式差异
react中,调用setState方法后,会自顶向下重新渲染组件,自顶向下的含义是,该组件以及它的子组件全部需要渲染;
而vue使用Object.defineProperty(vue@3迁移到了Proxy)对数据的设置(setter)和获取(getter)做了劫持,也就是说,vue能准确知道视图模版中哪一块用到了这个数据,并且在这个数据修改时,告诉这个视图,你需要重新渲染了。
所以当一个数据改变,react的组件渲染是很消耗性能的——父组件的状态更新了,所有的子组件得跟着一起渲染,它不能像vue一样,精确到当前组件的粒度。
在数据更新时,react生成了一棵更大的虚拟dom树,给第二步的diff带来了很大压力——我们想找到真正变化的部分,这需要花费更长的时间。js占据主线程去做比较,渲染线程便无法做其他工作,用户的交互得不到响应,所以便出现了react fiber。
react fiber没法让比较的时间缩短,但它使得diff的过程被分成一小段一小段的,因为它有了“保存工作进度”的能力。js会比较一部分虚拟dom,然后让渡主线程,给浏览器去做其他工作,然后继续比较,依次往复,等到最后比较完成,一次性更新到视图上。
diff的深度优先-后序遍历
在fiber之前的深度遍历中,整个遍历过程是不能中断的,因为DFS中断后,就找不到父节点了,而无法继续遍历;
因此,引入了一个新的数据结构 FiberNode
FiberNode 一个链表结构
每个节点有三个指针:第一个子节点、下一个兄弟节点、父节点;
当遍历发生中断时,保留当前节点的索引,是可以恢复的,因为每个节点有父节点和兄弟节点的指针。
interface FiberNode {
tag: TypeOfWork, // 标识 fiber 类型
type: 'div', // 和 fiber 相关的组件类型
return: Fiber | null, // 父节点
child: Fiber | null, // 子节点
sibling: Fiber | null, // 同级节点
alternate: Fiber | null, // diff 的变化记录在这个节点上
...
}
requestIdleCallback
requesetIdleCallback是一个属于宏任务的回调,是受屏幕的刷新率去控制,当浏览器闲置时,会调用这个函数。fiber 利用这个特性,讲任务分片,在requesetIdleCallback时执行。
同时,这个函数的兼容行不太好,react自己实现了polyfill。
const workLoop = (deadLine) => {
let shouldYield = false;// 是否该让出线程
while(!shouldYield){
console.log('working')
// 遍历节点等工作
shouldYield = deadLine.timeRemaining()<1;
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop);
Hooks
- 编写hooks为函数式编程,每个功能都包裹在函数中,整体风格更清爽,更优雅
- 通过自定义hook能够更好的封装我们的功能
- 每调用useHook一次都会生成一份独立的状态
事件机制
React基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等
在React中这套事件机制被称之为合成事件
合成事件(SyntheticEvent)
合成事件是 React模拟原生 DOM事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器
根据 W3C规范来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口
const handleClick = (e) => console.log(e);
// SyntheticBaseEvent {_reactName: 'onClick', _targetInst: null, type: 'click', nativeEvent: PointerEvent, target: a.App-link, …}
const button = <button onClick={handleClick}>按钮</button>
如果想要获得原生DOM事件,可以通过e.nativeEvent属性获取
const handleClick = (e) => console.log(e.nativeEvent);
虽然onClick看似绑定到DOM元素上,但实际并不会把事件函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件去监听,即事件代理。
意义
- 减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 document 上注册一次
- 统一规范,解决 ie 事件兼容问题,简化事件逻辑
- 方便事件统一管理和事务机制
高阶组件HOC
在js中,高阶函数(Higher-order function),至少满足下列一个条件的函数
- 接受一个或多个函数作为输入
- 输出一个函数
高阶组件 HOC 的这种实现方式,本质上是一个装饰者设计模式;
所以,高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用.
// 高阶组件
function withExtraProps(WrappedComponent, extraProps) {
return function(props) {
return <WrappedComponent {...props} {...extraProps} />;
};
}
// 原始组件
function MyComponent({ name }) {
return <div>Hello, {name}!</div>;
}
// 使用高阶组件
const EnhancedComponent = withExtraProps(MyComponent, { name: 'React' });
// 在应用中使用
function App() {
return <EnhancedComponent />;
}
export default App;
Context
import React, { useContext } from 'react';
// 创建一个 Context 对象
const ThemeContext = React.createContext('light');
const App = () => {
return (
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
const Toolbar = () => {
// 在任何组件中,我们都可以使用 useContext 来读取当前的 theme context
const theme = useContext(ThemeContext);
return (
<div>
<p>The theme is {theme}</p>
</div>
);
}
export default App;
性能优化
父组件渲染导致子组件渲染,子组件并没有发生任何改变,这时候就可以从避免无谓的渲染,具体实现的方式有如下:
- shouldComponentUpdate
- PureComponent
- React.memo
shouldComponentUpdate
通过shouldComponentUpdate生命周期函数来比对 state和 props,确定是否要重新渲染
默认情况下返回true表示重新渲染,如果不希望组件重新渲染,返回 false 即可
class MyComponent extends React.Component {
state = {
count: 0,
};
shouldComponentUpdate(nextProps, nextState) {
// 只有当 count 值增加时才重新渲染
if (this.state.count < nextState.count) {
return true;
}
return false;
}
render() {
return (
<div>
<p>{this.state.count}</p>
<button onClick={() => this.setState(state => ({ count: state.count + 1 }))}>
Increase
</button>
<button onClick={() => this.setState(state => ({ count: state.count - 1 }))}>
Decrease
</button>
</div>
);
}
}
pureComponent
React.PureComponent 是 React 提供的一个类组件,它实现了 shouldComponentUpdate 方法,对 props 和 state 进行浅比较。如果 props 和 state 没有变化,React.PureComponent 就不会重新渲染,从而提高性能。
import React from 'react';
class MyComponent extends React.PureComponent {
render() {
return <div>{this.props.name}</div>;
}
}
// 使用 MyComponent
<MyComponent name="GitHub Copilot" />
React.memo
React.memo 是一个高阶组件,它类似于 React.PureComponent,但是用于函数组件。它可以防止函数组件在给定相同 props 的情况下重新渲染。
import React from 'react';
const MyComponent = React.memo(function MyComponent(props) {
// 你的组件代码
});
// 使用 MyComponent
<MyComponent someProp={someValue} />
如果需要深层次比较,这时候可以给memo第二个参数传递比较函数
function arePropsEqual(prevProps, nextProps) {
// 如果返回 true,那么 React 将不会触发组件的重新渲染;
return prevProps === nextProps;
}
export default memo(Button, arePropsEqual);
其他
避免使用内联函数
避免在渲染方法中创建函数
// bad case
<input type="button" onClick={(e) => { this.setState({inputValue: e.target.value}) }} value="Click For Inline Function" />
// good case
setNewStateData = (event) => {
this.setState({
inputValue: e.target.value
})
}
...
<input type="button" onClick={this.setNewStateData} value="Click For Inline Function" />
使用 React Fragments 避免额外dom
<>
<div></div>
<div></div>
</>
import React, { Fragment } from 'react';
<Fragment>
<div></div>
<div></div>
</Fragment>
列表渲染使用key
避免删除新建dom,而是移动dom
懒加载和代码分割
使用 React.lazy 和 React.Suspense 可以帮助我们实现组件的懒加载和代码分割,从而减少初始加载时间。
React.lazy 函数让你可以渲染一个动态导入的模块作为常规组件。它会自动加载组件,并在组件准备好渲染时进行渲染。
React.Suspense 则用于包裹懒加载的组件,并提供一个加载指示器(如:spinner 或 loading 消息)在组件被加载过程中显示。
import React, { Suspense } from 'react';
const LazyComponent = React.lazy(() => import(/* webpackChunkName: "johanComponent" */'./LazyComponent'));
function MyComponent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
使用一些Hook
如 useMemo
,像是 vue 的compute
useMemo 是用来记忆化计算结果的。它接收两个参数:一个函数和一个依赖项数组。当依赖项改变时,函数会被重新执行,并返回新的结果。如果依赖项没有改变,useMemo 将返回上一次的计算结果,避免了不必要的计算。
const computedValue = useMemo(() => doExpensiveComputation(a, b), [a, b]);
如 useCallback
例如,如果你有一个经过优化的子组件,它依赖于 props 的恒等性(即,只有当 props 真正改变时才重新渲染),但props是函数时,父组件的重新渲染会导致重新创建函数,子组件也重新渲染。
useCallback 可以解决这个问题。它接收两个参数:一个函数和一个依赖项数组。它返回一个记忆化版本的函数,只有当依赖项改变时,这个函数才会被重新创建。否则,它会返回上一次的函数。
这意味着,即使父组件重新渲染,只要依赖项没有改变,传递给子组件的回调函数也不会改变。这样,子组件就不会因为父组件的重新渲染而进行不必要的重新渲染。
import React, { useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = React.useState(0);
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
return <ChildComponent onClick={increment} />;
}
Immutable
在做react性能优化的时候,为了避免重复渲染,我们会在shouldComponentUpdate()中做对比,当返回true执行render方法
Immutable通过is方法则可以完成对比,而无需像一样通过深度比较的方式比较
Redux
Store:存储应用的整个状态树。它是唯一的。
Action:描述了发生了什么的对象。它是改变状态的唯一途径。
Reducer:根据当前的状态和一个 action 来计算新的状态。
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';
// 这是一个 reducer,根据 action.type 来更新 state
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
// 创建 Redux store 来存储 state
let store = createStore(counter);
// 主组件
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
// Counter 组件可以从 Redux store 读取 state
function Counter() {
// useSelector 允许你从 Redux store 中选择(select)你需要的状态片段。它接收一个函数作为参数,这个函数接收整个 Redux store 的状态作为参数,然后返回你需要的状态片段。
// useDispatch 返回 Redux store 的 dispatch 函数,你可以用它来分发(dispatch)action。
let count = useSelector(state => state);
let dispatch = useDispatch();
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
);
}
SSR
- node server 接收客户端请求,得到当前的req url path,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props、context或者store 形式传入组件,
- 然后基于 react 内置的服务端渲染api renderToString() or renderToNodeStream() 把组件渲染为 html字符串或者 stream 流, 在把最终的 html 进行输出前需要将数据注入到浏览器端(注水)
- server 输出(response)后, 浏览器端可以得到数据(脱水),浏览器开始进行渲染和节点对比,然后执行组件的componentDidMount 完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束。
同构架构
服务端和客户端共用一套代码,先由服务端根据参数等将react渲染成html字符串下发,浏览器渲染完成事件绑定。
其中浏览器完成事件绑定的方法是拉取js文件执行。
所谓同构就是采用一套代码,构建双端(server 和 client)逻辑,最大限度的重用代码,不用维护两套代码。
注水与脱水
- 服务器端(脱水)
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root">${appHtml}</div>
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};
</script>
<script src="/bundle.js"></script>
</body>
</html>
`);
- 客户端(注水)
import React from 'react';
import { hydrate } from 'react-dom';
import App from './App';
const initialState = window.__INITIAL_STATE__;
delete window.__INITIAL_STATE__;
hydrate(<App initialState={initialState} />, document.getElementById('root'));
hydrate的原理
它会尽可能地复用已经存在的DOM节点,而不是从头开始创建新的DOM节点。这种方法被称为“注水”,它在性能上比普通的渲染方法更优,因为它避免了不必要的DOM操作。
基本工作流程:
读取和解析初始状态:首先,React从全局变量(如window.INITIAL_STATE)中读取初始状态。这个状态是在服务器端渲染过程中被注入到HTML中的。
创建虚拟DOM
匹配元素和实际DOM:接下来,React开始遍历这些虚拟DOM,并尝试将它们与已经存在的DOM节点匹配。如果找到匹配的DOM节点,React将复用这个节点,而不是创建新的节点。
添加事件监听器:在匹配过程中,React也会为这些DOM节点添加事件监听器,以便它可以响应用户的交互。
处理差异:如果React发现某个React元素与其对应的DOM节点不匹配,它将更新这个DOM节点,使其与React元素匹配。这个过程被称为“差异化”。
简单实现
- 首先通过express启动一个app.js文件,用于监听3000端口的请求,当请求根目录时,返回HTML,如下:
const express = require('express')
const app = express()
app.get('/', (req,res) => res.send(`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
Hello world
</body>
</html>
`))
app.listen(3000, () => console.log('Exampleapp listening on port 3000!'))
- 然后编写react代码,在app.js中进行应引用
import React from 'react'
const Home = () =>{
return <div>home</div>
}
export default Home
- 为了让服务器能够识别JSX,这里需要使用webpack对项目进行打包转换,创建一个配置文件webpack.server.js并进行相关配置,如下:
const path = require('path') //node的path模块
const nodeExternals = require('webpack-node-externals')
module.exports = {
target:'node',
mode:'development', //开发模式
entry:'./app.js', //入口
output: { //打包出口
filename:'bundle.js', //打包后的文件名
path:path.resolve(__dirname,'build') //存放到根目录的build文件夹
},
externals: [nodeExternals()], //保持node中require的引用方式
module: {
rules: [{ //打包规则
test: /\.js?$/, //对所有js文件进行打包
loader:'babel-loader', //使用babel-loader进行打包
exclude: /node_modules/,//不打包node_modules中的js文件
options: {
presets: ['react','stage-0',['env', {
//loader时额外的打包规则,对react,JSX,ES6进行转换
targets: {
browsers: ['last 2versions'] //对主流浏览器最近两个版本进行兼容
}
}]]
}
}]
}
}
- 接着借助react-dom提供了服务端渲染的 renderToString方法,负责把React组件解析成html
import express from 'express'
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from 'react-dom/server'//引入renderToString方法
import Home from'./src/containers/Home'
const app= express()
const content = renderToString(<Home/>)
app.get('/',(req,res) => res.send(`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
${content}
</body>
</html>
`))
app.listen(3001, () => console.log('Exampleapp listening on port 3001!'))
- 通过script标签为页面引入客户端执行的react代码,实现浏览器事件绑定:
import express from 'express'
import React from 'react'//引入React以支持JSX的语法
import { renderToString } from'react-dom/server'//引入renderToString方法
import Home from './src/containers/Home'
const app = express()
app.use(express.static('public'));
//使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹
const content = renderToString(<Home/>)
app.get('/',(req,res)=>res.send(`
<html>
<head>
<title>ssr demo</title>
</head>
<body>
${content}
<script src="/index.js"></script>
</body>
</html>
`))
app.listen(3001, () =>console.log('Example app listening on port 3001!'))
- 然后再客户端执行以下react代码,新建webpack.client.js作为客户端React代码的webpack配置文件如下:
const path = require('path') //node的path模块
module.exports = {
mode:'development', //开发模式
entry:'./src/client/index.js', //入口
output: { //打包出口
filename:'index.js', //打包后的文件名
path:path.resolve(__dirname,'public') //存放到根目录的build文件夹
},
module: {
rules: [{ //打包规则
test: /\.js?$/, //对所有js文件进行打包
loader:'babel-loader', //使用babel-loader进行打包
exclude: /node_modules/, //不打包node_modules中的js文件
options: {
presets: ['react','stage-0',['env', {
//loader时额外的打包规则,这里对react,JSX进行转换
targets: {
browsers: ['last 2versions'] //对主流浏览器最近两个版本进行兼容
}
}]]
}
}]
}
}
Next.js
- Next.js 是一个基于 React 的开源 JavaScript 框架,它使得服务端渲染(SSR)和静态站点生成(SSG)变得更加简单。以下是你可以提及的一些 Next.js 的特性和优点:
- 服务端渲染(SSR)和静态站点生成(SSG):Next.js 提供了内置的 SSR 和 SSG 支持,使得你可以根据需要选择最适合的渲染策略。
- 文件系统路由:Next.js 通过文件系统自动实现路由。你只需要在 pages 目录下添加文件,就可以自动创建对应的路由。
- API 路由:Next.js 允许你在 pages/api 目录下创建 API 路由,这使得你可以在同一个项目中同时处理前端和后端。
- 热模块替换(HMR):Next.js 支持热模块替换,这意味着你在开发过程中可以实时看到你的更改。
- 优化:Next.js 自动优化你的应用,包括代码分割、懒加载等,以提高性能。
- TypeScript 支持:Next.js 提供了内置的 TypeScript 支持,你无需额外配置即可使用 TypeScript。
- 集成 CSS 和 Sass:Next.js 支持导入 CSS 和 Sass 文件,你也可以使用 CSS-in-JS 库,如 styled-components 或 emotion。
- 插件系统:Next.js 有一个丰富的插件系统,你可以使用插件来扩展 Next.js 的功能。
- 与 Vercel 的无缝集成:Next.js 由 Vercel 开发,因此它与 Vercel 平台有着无缝的集成,使得部署变得非常简单。
错误边界
ErrorBoundary
从 react16 开始,react 引入了 error boundary(错误边界)的概念,我们可以在组件树的任何位置设置 error boundary,用于捕获子组件抛出的错误。当一个组件抛出错误时,react 会沿着组件树向上查找最近的 error boundary,并将错误传递给 boundary 处理。
错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
但是不能捕获 异步、事件回调。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
// 发生错误,更新state,触发显示兜底UI
static getDerivedStateFromError() {
return { hasError: true }
}
// React 提供了 componentDidCatch 方法,用于上报之类的错误处理
componentDidCatch(error, errorInfo) {
log(error, errorInfo)
}
render() {
if (this.state.hasError) {
return this.props.fallback
} else {
return this.props.children
}
}
}
使用 ErrorBoundary 组件
const component = () => {
<ErrorBoundary fallback={<>oooooops no~</>}>
<something />
</ErrorBoundary>
}
但是如果想让一些事件回调、异步也能被捕获,有人在 github issue 提供的hack方法:
在需要的组件内,通过try/catch捕获错误,触发组件更新,在更新回调里抛出错误,让 ErrorBoundary 捕获错误:
const Component = () => {
// create some random state that we'll use to throw errors
const [state, setState] = useState();
const onClick = () => {
try {
// something bad happened
} catch (e) {
// trigger state update, with updater function as an argument
setState(() => {
// re-throw this error within the updater function
// it will be triggered during state update
throw e;
})
}
}
}
封装成hooks:
const useThrowAsyncError = () => {
const [state, setState] = useState();
return (error) => {
setState(() => throw error)
}
}
// 使用:
const Component = () => {
const throwAsyncError = useThrowAsyncError();
useEffect(() => {
fetch('/bla').then().catch((e) => {
// throw async error here!
throwAsyncError(e)
})
})
}
封装成一个包装器:
const useCallbackWithErrorHandling = (callback) => {
const [state, setState] = useState()
return (...args) => {
try {
callback(...args)
} catch(e) {
setState(() => throw e)
}
}
}
// 使用:
const Component = () => {
const onClick = () => {
// do something dangerous here
}
const onClickWithErrorHandler = useCallbackWithErrorHandling(onClick);
return <button onClick={onClickWithErrorHandler}>click me!</button>
}
Router
使用
React Router对应的hash模式和history模式对应的组件为:
- HashRouter
- BrowserRouter
import React from 'react';
import {
BrowserRouter as Router,
// HashRouter as Router
Switch,
Route,
} from "react-router-dom";
import Home from './pages/Home';
import Login from './pages/Login';
import Backend from './pages/Backend';
import Admin from './pages/Admin';
function App() {
return (
<Router>
<Route path="/login" component={Login}/>
<Route path="/backend" component={Backend}/>
<Route path="/admin" component={Admin}/>
<Route path="/" component={Home}/>
</Router>
);
}
export default App;
原理-HashRouter
以hash模式为例子,改变hash值并不会导致浏览器向服务器发送请求,浏览器不发出请求,也就不会刷新页面
hash 值改变,触发全局 window 对象上的 hashchange 事件。所以 hash 模式路由就是利用 hashchange 事件监听 URL 的变化,从而进行 DOM 操作来模拟页面跳转
import React, { Component } from 'react';
import { Provider } from './context'
// 该组件下Api提供给子组件使用
class HashRouter extends Component {
constructor() {
super()
this.state = {
location: {
pathname: window.location.hash.slice(1) || '/'
}
}
}
// url路径变化 改变location
componentDidMount() {
window.location.hash = window.location.hash || '/'
window.addEventListener('hashchange', () => {
this.setState({
location: {
...this.state.location,
pathname: window.location.hash.slice(1) || '/'
}
}, () => console.log(this.state.location))
})
}
render() {
let value = {
location: this.state.location
}
return (
<Provider value={value}>
{
this.props.children
}
</Provider>
);
}
}
export default HashRouter;
原理-Route
Route 组件主要做的是通过BrowserRouter传过来的当前值,通过props传进来的path与context传进来的pathname进行匹配,然后决定是否执行渲染组件
import React, { Component } from 'react';
import { Consumer } from './context'
const { pathToRegexp } = require("path-to-regexp");
class Route extends Component {
render() {
return (
<Consumer>
{
state => {
console.log(state)
let {path, component: Component} = this.props
let pathname = state.location.pathname
let reg = pathToRegexp(path, [], {end: false})
// 判断当前path是否包含pathname
if(pathname.match(reg)) {
return <Component></Component>
}
return null
}
}
</Consumer>
);
}
}
export default Route;
动画
使用 react-transition-group
, 和Vue类似
- CSSTransition:结合 CSS 来完成过渡动画效果
- SwitchTransition:两个组件显示和隐藏切换时,使用该组件
- TransitionGroup:将多个动画组件包裹在其中,一般用于列表中元素的动画
const {show} = this.state;
<CSSTransition
in={show}
timeout={500}
classNames={'fade'}
unmountOnExit={true}
>
<div className={'square'} />
</CSSTransition>
.fade-enter {
opacity: 0;
transform: translateX(100%);
}
.fade-enter-active {
opacity: 1;
transform: translateX(0);
transition: all 500ms;
}
.fade-exit {
opacity: 1;
transform: translateX(0);
}
.fade-exit-active {
opacity: 0;
transform: translateX(-100%);
transition: all 500ms;
}
Vue
Vue的生命周期
Vue diff
tree diff,比较只会在同层级进行, 不会跨层级比较
双指针,比较的过程中,循环从两边向中间收拢
Vue.js 中的 diff 算法主要依靠 Virtual DOM 和深度优先遍历来实现。通过比较新旧 Virtual DOM 树的差异,Vue.js 可以找出需要更新的部分,并最小化 DOM 操作的次数,从而实现高效的视图更新。
- 判断Vnode和oldVnode是否指向同一个对象,如果是,那么直接return
- 如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点。
- 如果oldVnode有子节点而Vnode没有,则删除el的子节点
- 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el -如果两者都有子节点,则执行updateChildren函数比较子节点,这一步很重要
function patchVnode(oldVnode, vnode) {
// some code
if (oldVnode === vnode) {
return;
}
const elm = (vnode.elm = oldVnode.elm);
const oldCh = oldVnode.children;
const ch = vnode.children;
// 非文本节点
if (isUndef(vnode.text)) {
// 新旧节点都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 子节点的同层比较
if (oldCh !== ch)
updateChildren(elm, oldCh, ch);
} else if (isDef(ch)) {
// 仅新元素有子节点
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
addVnodes(elm, null, ch, 0, ch.length - 1, null);
// 仅旧元素有子节点
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 清空文本
nodeOps.setTextContent(elm, "");
}
// 文本节点,更新文本即可
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text);
}
}
Vue的响应式原理
Vue的虚拟DOM
Vue的事件机制
Vue的组件通信
Vue的性能优化
页面加载优化
- 尽量选择提供 ES 模块格式的依赖包,它们对 tree-shaking 更友好。
- 代码分割,异步引入组件
更新优化
- v-once 是一个内置的指令,可以用来渲染依赖运行时数据但无需再更新的内容。它的整个子树都会在未来的更新中被跳过。查看它的 API 参考手册可以了解更多细节。
- v-memo 缓存一个模板的子树。在元素和组件上都可以使用。为了实现缓存,该指令需要传入一个固定长度的依赖值数组进行比较。如果数组里的每个值都与最后一次的渲染相同,那么整个子树的更新将被跳过。
- 传给子组件的 props 尽量保持稳定
Vue3的新特性
Vue3的Composition API
Vue3的Proxy
Vue的SSR
是什么
Server-Side Rendering ,服务端渲染,指由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程
Vue SSR是一个在SPA上进行改良的服务端渲染;
通过Vue SSR渲染的页面,需要在客户端激活才能实现交互;
Vue SSR包含两部分:服务端渲染的首屏,包含交互的SPA;
解决了什么
SEO:搜索引擎优先爬取页面HTML结构,使用ssr时,服务端已经生成了和业务想关联的HTML,有利于seo
首屏呈现渲染:用户无需等待页面所有js加载完成就可以看到页面视图(压力来到了服务器,所以需要权衡哪些用服务端渲染,哪些交给客户端)
缺点:
- 复杂度:整个项目的复杂度
- 库的支持性,代码兼容
- 性能问题 每个请求都是n个实例的创建,不然会污染,消耗会变得很大 缓存 node serve、 nginx判断当前用户有没有过期,如果没过期的话就缓存,用刚刚的结果。 降级:监控cpu、内存占用过多,就spa,返回单个的壳
- 服务器负载变大,相对于前后端分离务器只需要提供静态资源来说,服务器负载更大
怎么做
项目目录
src
├── router
├────── index.js # 路由声明
├── store
├────── index.js # 全局状态
├── main.js # ⽤于创建vue实例
├── entry-client.js # 客户端⼊⼝,⽤于静态内容“激活”
└── entry-server.js # 服务端⼊⼝,⽤于⾸屏内容渲染
- 客户端激活 client-side hydration (注入)
所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,无需将其丢弃再重新创建所有的 DOM 元素,使其变为由 Vue 管理的动态 DOM 的过程,为HTML添加事件处理程序和其他交互功能。
服务端下发的 HTML 在客户端是完全静态的,因为我们没有在浏览器中加载 Vue。
为了使客户端的应用可交互,Vue 需要执行一个激活步骤。在激活过程中,Vue 会创建一个与服务端完全相同的应用实例,然后将每个组件与它应该控制的 DOM 节点相匹配,并添加 DOM 事件监听器。
在 entry-client.js 中,我们用下面这行挂载(mount)应用程序:
app.mount('#app')
- 在开发模式下,如果无法匹配,它将退出混合模式,丢弃现有的 DOM 并从头开始渲染;
- 在生产模式下,此检测会被跳过,以避免性能损耗。
Vue3 -> Vue2
defineProperty -> Proxy
Object.defineProperty,这个 API 有一些缺陷,并不能检测对象属性的添加和删除,数组的变化也不能检测到,所以 Vue.js 内部使用了一些 hack 的手段来解决这些问题,但是这些 hack 也导致了一些副作用,比如性能不好、代码不好维护等。
Vue3 使用 Proxy 来替代 Object.defineProperty,Proxy 是 ES6 中新增的特性,它可以直接监听对象而非属性,所以不需要像 Object.defineProperty 那样遍历对象的属性,Proxy 可以直接拦截对象的读取、写入、删除等操作,具体可以参考 MDN 文档。
tree shaking
Tree shaking是基于ES6模板语法(import与exports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量
Vue3 中的模块都是通过 ES Module 的方式来导出的,这样就可以让打包工具来分析模块的依赖关系,实现 tree shaking。Vue2 中单例模式的组件,比如 router、store 等,都是通过 this.$router、this.$store 来访问的,这样打包工具就无法分析出依赖关系,所以无法实现 tree shaking。
静态提升
Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用
这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用
// Vue2
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode("span", null, "你好"),
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
// Vue3
const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "你好", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_hoisted_1,
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
diff算法优化
关于这个静态标记,其作用是为了会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方进行比较