1 虚拟DOM的原理是什么

1.1 是什么

  • 虚拟DOM就是虚拟节点。React用JS对象模拟DOM节点,然后将其渲染成真实的DOM节点

1.2 怎么做

  1. 模拟JSX => 虚拟DOM对象

    • 用JSX语法写出来的div其实就是一个虚拟节点

      • 1
        2
        3
        
        <div id={"x"}>
        	<span className={'red'}>hi</span>
        </div>	
        
    • 通过调用React.createElement(),可以将JSX语法转译得到一个虚拟DOM对象

      • 1
        2
        
        React.createElement("div", {id: "x"}, 
        	React.createElement("span", {className: "red"}, "hi"))
        
      •  1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        
        { 
        	tag: 'div', 
        	props: { id: 'x' }, 
        	children: [
        		{
        			tag: 'span',
        			props: {
        				className: 'red'
        			},
        			children: [ 'hi' ]
        		}
        	] 
        }
        
  2. 渲染虚拟DOM => 真实DOM

    •  1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      
      function render(vdom) {
        // 如果是字符串或者数字,创建一个文本节点
        if (typeof vdom === 'string' || typeof vdom === 'number') {
          return document.createTextNode(vdom)
        }
        const { tag, props, children } = vdom
        // 创建真实DOM
        const element = document.createElement(tag)
        // 设置属性
        setProps(element, props)
        // 递归遍历子节点,并获取创建真实DOM,插入到当前节点
        children
          .map(render)
          .forEach(element.appendChild.bind(element))
      
        // 虚拟 DOM 中缓存真实 DOM 节点
        vdom.dom = element
      
        // 返回 DOM 节点
        return element
      }
      
      function setProps // 略
      function setProp // 略
      
    • 如果节点发生变化,并不会直接把新虚拟节点渲染到真实节点,而是先通过diff算法得到一个patch(补丁)再更新到真实节点上

1.3 解决了什么问题

  • DOM操作性能问题,通过虚拟DOM和diff算法减少不必要的DOM操作,保证性能下限
  • DOM操作不方便问题,以前各种DOM API要记,现在只有setState就可以解决DOM更新的问题

1.4 优点

  • 为React带来了跨平台能力,因为虚拟节点的存在,除了可以将其渲染为真实节点,还可以渲染为其他东西
  • 让DOM操作的整体性能更好,能(通过diff)减少不必要的DOM操作

1.5 缺点

  • React为虚拟DOM创造了合成事件,与原生DOM事件不太一样
    • 所有React事件都绑定到根元素,自动实现事件委托
    • 如果混用合成事件和原生DOM事件,有可能会出bug

1.6 如何解决缺点

不用React,用Vue3。。。

2 DOM diff算法是怎样的

2.1 是什么

DOM diff 就是对比两颗虚拟DOM树的算法。当组件变化时,会render出一个新的虚拟DOM,diff算法对比新旧虚拟DOM之后,得到一个patch,然后React用patch来更新真实DOM

2.2 怎么做

  • 先对比根节点

    • 如果根节点的类型变了(div => span),那么就认为整棵树都变了,不再对比子节点,直接删除对应的真实DOM树,根据虚拟DOM树创建新的真实DOM树
    • 如果根节点的类型没变,再对比属性是否发生改变
      • 如果属性没变,就保留,继续进行子节点的diff
      • 如果属性变了,就只更新该节点的属性,不再重新创建新节点
        • 更新style时,如果多个css属性只有一个改变了,那么React只更新改变的
  • 然后对子节点继续做以上操作

  • 举例

    1. 情况1
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    <ul>
      <li>A</li>
      <li>B</li>
    </ul>
    
    <ul>
      <li>A</li>
      <li>B</li>
      <li>C</li>
    </ul>
    
    • React依次对比A-AB-Bnull-C,发现C是新增的,最终会创建真实C节点插入页面
    1. 情况2
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    <ul>
      <li>B</li>
      <li>C</li>
    </ul>
    
    <ul>
      <li>A</li>
      <li>B</li>
      <li>C</li>
    </ul>
    
    • 其实只需要创建 A 文本,保留 B 和 C 即可,但React不是这样的
      • React 对比B-A,会删除 B 文本新建 A 文本;
      • 对比C-B,会删除 C 文本,新建 B 文本;(注意,并不是边对比边删除新建,而是把操作汇总到 patch 里再进行 DOM 操作);
      • 对比null-C,会新建 C 文本。
    • 这也是React需要key的原因:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    <ul>
      <li key="b">B</li>
      <li key="c">C</li>
    </ul>
    
    <ul>
      <li key="a">A</li>
      <li key="b">B</li>
      <li key="c">C</li>
    </ul>
    
    • React 先对比 key 发现 key 只新增了一个,于是保留 b 和 c,新建 a

2.3 双端交叉diff算法(Vue)

  • 将新旧DOM树同一节点的子节点们分别处理为新旧两个数组
  • 头头、尾尾、新头旧尾、旧头新尾、key
  • 失败就按流程继续对比,成功就重新进入循环,直到任意一数组的头指针超过尾指针

Diff算法 | Marvin (canyuegongzi.github.io)

2.4 React DOM diff和Vue DOM diff的区别

  1. React是从左向右遍历对比,Vue是双端交叉对比
  2. Vue整体的diff效率比React更高,
    • 假设有n个子节点,我们只是把最后的子节点移动至第一个
      • 则React需要借助Map进行key搜索找到匹配项,然后复用节点
      • Vue会发现移动,直接复用该节点

3 React有哪些生命周期钩子函数

React lifecycle methods diagram (wojtekmaj.pl)

constructor => getDerivedStateFormProps => shouldComponentUpdate => render => getSnapshotBeforeUpdate => componentDidMount => componentDidUpdate => componentWillUnmount

  • 挂载时调用 constructor,更新时不调用
  • 更新时调用 shouldComponentUpdate 和 getSnapshotBeforeUpdate,挂载时不调用
  • should… 在 render 前调用,getSnapshot… 在 render 后调用
  • 请求放在 componentDidMount 里

4 React如何实现组件间通信

  • 父子组件通信:props + 函数,

    • 父组件给子组件传用props
    • 子组件调用父组件传过来的函数(props.sdata(data)),通过参数传值即可
  • 爷孙组件通信:两层父子通信或者使用 Context.Provider 和 Context.Consumer

  • 任意组件通信:其实就变成了状态管理了

    • Redux

    • Mobx

5 如何理解Redux(DVA)

  • redux是一个状态管理库/容器集中式管理react应用中多个组件共享的状态。

  • 核心概念

    • State 页面或模块需要共享或持久化的数据,只读的
    • Action = type + payload 普通js对象,约定type属性用来描述执行的动作
    • Reducer 纯函数,接收旧的 state 和 action,返回新的 state。
    • Dispatch 将action派发给store
    • Store 将action、state、reducer联系在一起的对象
    • Middleware
  • React-redux核心概念

    • Provider 将store提供给子组件
    • connect()() 将组件与store进行关联,把 redux 的 dispatch 和 state 映射为 react 组件的 props,接收三个参数
    • mapStateToProps 将state合并包装到组件的props属性上
    • mapDispatchToProps 将dispatch合并包装到组件的props属性上,如果不传,组件默认接收dispatch,传的话,写起来可以省略dispatch,更具语义性?
  • 中间件redux-thunk

    • 可以在redux中进行异步操作
  • 中间件redux-promise

  • dva

    • 比redux多了两个概念EffectSubscription
    • Effect被称为副作用,涉及到异步操作都可以写到这里,采用generator相关概念,使得异步操作更加优雅?
    • Subscription订阅数据源,比如路由、键盘输入等

6 什么是高阶组件 HOC?

  • 参数是函数/组件,返回值也是函数/组件的函数。

  • 用法举例说明即可:

    • React.forwardRef

      • 函数组件不支持ref,不能持久化保存对元素的引用
      • 但使用forwardRef包裹一下,第二个参数就成了ref
      • 可以将ref透传(forward)给内部的button
      1
      2
      3
      4
      5
      6
      7
      8
      
      const Button = React.forwardRef((props, ref) => {
      	return (
      		<button ref={ref}>{props.children}</button>
      	)
      })
      
      const ref = React.createRef()
      <Button ref={ref}>Click</Button>
      
    • ReactRedux 的 connect

    • ReactRouter 的 withRouter(class组件),能够赋予组件match、location、history三个属性

参考阅读:「react进阶」一文吃透React高阶组件(HOC) - 掘金 (juejin.cn)

7 React Hooks如何模拟组件声明周期

  • 模拟 componentDidMount => useEffect(()=>{},[]) 依赖为空数组
  • 模拟 componentDidUpdate => useEffect(()=>{}) 不加依赖
  • 模拟 componentWillUnmount => useEffect(()=>{ return()=>{}}, []) 依赖空数组+内部return函数

8 React-router实现原理

聊一聊 React-Router - 知乎 (zhihu.com)

8.1 前端路由原理:监听浏览器URL变化,截获URL地址,然后进行URL路由匹配,从而渲染路由组件

8.2 两种路由实现方式:history和hash

8.2.1 hash

  • 在html5 标准落地之前,都是通过监听 hashchange 事件,当 URL 变化时,进行页面跳转。
  • hash的路由地址会有#
  • 在路由变化(点击跳转或历史跳转)时,触发hashchange事件,匹配到对应的路由规则,通过location.hash改变页面路由,以DOM替换的方式更改页面内容
  • 手动刷新不会向服务器发送请求,也不会触发hashchange事件
  • #后面的数据不会发送到服务器,因此服务器不用担心出现404的问题
  • 兼容性好
  • 锚点定位会失效
  • 不利于SEO

8.2.2 history

  • 通过 html5-mode 实现单页路由的 URL 没有 #
    • history 模式是通过调用 window.history 对象上的一系列方法来实现页面的无刷新跳转。
    • 利用了 HTML5 History Interface 中新增的pushState() 和 replaceState() 方法。
    • 这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会向后端发送请求。
  • 但当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求,此时如果请求路径非根路径,nginx就会出现404的问题
  • 为了避免出现这种情况,所以这个实现需要服务器(nginx)的支持,需要把所有路由都重定向到根页面。
1
2
3
4
5
location / {
  root 路径;
  index index.html index.htm;
  try_files $uri $uri/ /index.html;
}

8.3 Router组件

react-router 的工作方式,是在组件树顶层放一个 Router 组件,然后在组件树中散落着很多 Route 组件(注意比 Router 少一个“r”),顶层的 Router 组件负责分析监听 URL 的变化,在它之下的 Route 组件可以直接读取这些信息。Router 是“提供者”,Route是“消费者”

8.4 Route

Route只是一个具有渲染方法的普通 React 组件,路由匹配成功则渲染该组件。

9 Mixin HOC 与hooks

三者都是对代码逻辑进行复用的方案

  • Mixin:

    • Mixin类似Object.assgin方法,用赋值的方式将mixin对象中的方法都挂载到原对象上

    • React中,只有当使用createClass来创建组件时才能用

    • 存在逻辑与状态被覆盖的问题

  • HOC

    • 高阶组件,即接受一个组件,返回一个新组件
    • 可以处理组件的props、state(反向继承),渲染劫持等
    • 当大量使用HOC时,会出现多层嵌套的问题,影响调试
  • hooks

    • 解决以上问题(多个hooks互不影响、避免嵌套)
    • 使用函数组件代替class组件

10 useState为什么不能在条件或循环中使用

  • 手写useState思路:

    • useState接收初始值,返回state和setState的数组;

    • state主要用于维护状态值,而setState则是改变对应的state和刷新组件的作用;

    • 重新渲染组件可以使用ReactDOM.render(<Demo/>, rootElement)

  • 在组件中,useState维护多个state是有序的(可能是数组,链表),react对useState的标识是用index去记录的,如果在if中使用useState可能会导致顺序出错

  • 确保 Hook 在每一次渲染中都按照同样的顺序被调用

  • 为什么useState是数组结构的形式返回的,能不能以Object的形式返回?

    • 因为useState维护多个state是有序的

11 react工作流程

  • 工作流程:

    • 生成react root节点
    • reconciler 协调生成需要更新的子节点
    • 将节点更新commit 到视图
  • fiber:

    • Fiber是React更新时的最小单元,是一种包含指针的数据结构,从数据结构上看Fiber架构 ≈ 树 + 链表。

    • Fiber单元是从 jsx createElement之后根据ReactElement生成的,相比 ReactElement,Fiber单元具备动态工作能力。

12 class组件与函数组件的异同与好处