Skip to content

React & Vue

React

生命周期

组件的生命周期可分成三个状态:

挂载阶段

  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount
ts
// 函数组件
useEffect(() => {
  consol.log("componentDidMount")
}, []);

更新阶段

  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

getSnapshotBeforeUpdate

该周期函数在render后执行,执行之时DOM元素还没有被更新。该方法返回的一个Snapshot值,作为componentDidUpdate第三个参数传入。的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些UI视觉上的状态

ts
// 函数组件
useEffect(() => {
  consol.log("componentDidUpdate")
}, [state]);

卸载阶段

  • componentWillUnmount

通过在 useEffect 中返回一个函数,它便可以清理副作用。

清理规则是:

  • 首次渲染不会进行清理,会在下一次渲染,清除上一次的副作用;
  • 卸载阶段也会执行清除操作。
ts
useEffect(() => {
  function handleClick() {
    alert(`You clicked`)
  }

  document.body.addEventListener("click", handleClick, false);

  return function cleanup() {
    document.body.removeEventListener("click", handleClick, false);
  };
}, []);

组件通信

由于React是单向数据流,主要思想是组件不会改变接收的数据,只会监听数据的变化,当数据发生变化时它们会使用接收到的新值,而不是去修改已有的值

因此,可以看到通信过程中,数据的存储位置都是存放在上级位置中

父组件向子组件传递

props

子组件向父组件传递

props回调函数

ts
const Child = ({callback}: {callback: Function}) => {
  return <div onClick={() => callback(1)}></div>
}

兄弟组件之间的通信

  • 通过使用父组件传递
  • content
ts
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属性的作用是用于判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲染

js
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 一个链表结构

每个节点有三个指针:第一个子节点、下一个兄弟节点、父节点;

当遍历发生中断时,保留当前节点的索引,是可以恢复的,因为每个节点有父节点和兄弟节点的指针。

ts
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。

ts
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规范来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口

js
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属性获取

ts
const handleClick = (e) => console.log(e.nativeEvent);

虽然onClick看似绑定到DOM元素上,但实际并不会把事件函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件去监听,即事件代理。

意义

  • 减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 document 上注册一次
  • 统一规范,解决 ie 事件兼容问题,简化事件逻辑
  • 方便事件统一管理和事务机制

高阶组件HOC

在js中,高阶函数(Higher-order function),至少满足下列一个条件的函数

  • 接受一个或多个函数作为输入
  • 输出一个函数

高阶组件 HOC 的这种实现方式,本质上是一个装饰者设计模式;

所以,高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用.

js
// 高阶组件
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

js
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 即可

js
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 就不会重新渲染,从而提高性能。

js
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 的情况下重新渲染。

js
import React from 'react';

const MyComponent = React.memo(function MyComponent(props) {
  // 你的组件代码
});

// 使用 MyComponent
<MyComponent someProp={someValue} />

如果需要深层次比较,这时候可以给memo第二个参数传递比较函数

js
function arePropsEqual(prevProps, nextProps) {
  // 如果返回 true,那么 React 将不会触发组件的重新渲染;
  return prevProps === nextProps;
}

export default memo(Button, arePropsEqual);

其他

避免使用内联函数

避免在渲染方法中创建函数

jsx
// bad case
<input type="button" onClick={(e) => { this.setState({inputValue: e.target.value}) }} value="Click For Inline Function" />
js
// good case
setNewStateData = (event) => {
  this.setState({
    inputValue: e.target.value
  })
}
...
<input type="button" onClick={this.setNewStateData} value="Click For Inline Function" />

使用 React Fragments 避免额外dom

jsx
<>
  <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 消息)在组件被加载过程中显示。

js
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 将返回上一次的计算结果,避免了不必要的计算。

js
const computedValue = useMemo(() => doExpensiveComputation(a, b), [a, b]);

useCallback

例如,如果你有一个经过优化的子组件,它依赖于 props 的恒等性(即,只有当 props 真正改变时才重新渲染),但props是函数时,父组件的重新渲染会导致重新创建函数,子组件也重新渲染。

useCallback 可以解决这个问题。它接收两个参数:一个函数和一个依赖项数组。它返回一个记忆化版本的函数,只有当依赖项改变时,这个函数才会被重新创建。否则,它会返回上一次的函数。

这意味着,即使父组件重新渲染,只要依赖项没有改变,传递给子组件的回调函数也不会改变。这样,子组件就不会因为父组件的重新渲染而进行不必要的重新渲染。

ts
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 来计算新的状态。

js
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)逻辑,最大限度的重用代码,不用维护两套代码。

注水与脱水

  1. 服务器端(脱水)
js
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>
`);
  1. 客户端(注水)
js
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元素匹配。这个过程被称为“差异化”。

简单实现

  1. 首先通过express启动一个app.js文件,用于监听3000端口的请求,当请求根目录时,返回HTML,如下:
js
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!'))
  1. 然后编写react代码,在app.js中进行应引用
js
import React from 'react'

const Home = () =>{

    return <div>home</div>

}

export default Home
  1. 为了让服务器能够识别JSX,这里需要使用webpack对项目进行打包转换,创建一个配置文件webpack.server.js并进行相关配置,如下:
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']   //对主流浏览器最近两个版本进行兼容
                    }
               }]]
           }
       }]
    }
}
  1. 接着借助react-dom提供了服务端渲染的 renderToString方法,负责把React组件解析成html
js
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!'))
  1. 通过script标签为页面引入客户端执行的react代码,实现浏览器事件绑定:
js
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!'))
  1. 然后再客户端执行以下react代码,新建webpack.client.js作为客户端React代码的webpack配置文件如下:
js
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 处理。

错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

但是不能捕获 异步、事件回调。

jsx
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 组件

jsx
const component = () => {
  <ErrorBoundary fallback={<>oooooops no~</>}>
    <something />
  </ErrorBoundary>
}

但是如果想让一些事件回调、异步也能被捕获,有人在 github issue 提供的hack方法:

在需要的组件内,通过try/catch捕获错误,触发组件更新,在更新回调里抛出错误,让 ErrorBoundary 捕获错误:

jsx
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:

jsx
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)
    })
  })
}

封装成一个包装器:

jsx
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
js
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 操作来模拟页面跳转

jsx
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进行匹配,然后决定是否执行渲染组件

jsx
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:将多个动画组件包裹在其中,一般用于列表中元素的动画
jsx
const {show} = this.state;
<CSSTransition
  in={show}
  timeout={500}
  classNames={'fade'}
  unmountOnExit={true}
>
  <div className={'square'} />
</CSSTransition>
css
.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 Diff

Vue.js 中的 diff 算法主要依靠 Virtual DOM 和深度优先遍历来实现。通过比较新旧 Virtual DOM 树的差异,Vue.js 可以找出需要更新的部分,并最小化 DOM 操作的次数,从而实现高效的视图更新。

  • 判断Vnode和oldVnode是否指向同一个对象,如果是,那么直接return
  • 如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点。
  • 如果oldVnode有子节点而Vnode没有,则删除el的子节点
  • 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el -如果两者都有子节点,则执行updateChildren函数比较子节点,这一步很重要
js
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,返回单个的壳
  • 服务器负载变大,相对于前后端分离务器只需要提供静态资源来说,服务器负载更大

怎么做

项目目录

txt
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)应用程序:

js
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中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用

这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用

js
// 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标记,下次发生变化的时候直接找该地方进行比较