React

React

React setState 异步同步

  1. 在 setTimeout、Promise 等原生事件 API 调用中
    • setState 和 useState 是同步执行的,立即执行 render
      • Class Component 能获取到最新值 => this.state => 引用类型
      • Function Component 不能获取到最新值 => 只能得到之前的值 => 闭包
    • 多次执行 setState 和 useState,每一次执行都会调用一次 render
  2. 在 React 合成事件中
    • setState 和 useState 是异步执行的,不会立即执行 render,都不能立即获取到更新后的值
    • 多次执行 setState 和 useState,只会调用一次 render
    • setState 可能会进行 state 合并
      • 传入一个对象,会进行 state 合并 => +1
      • 传入一个函数,则不会进行 state 合并 => +2
    • useState 不会进行 state 合并

React Fiber

在 Fiber 出现之前 React 的问题

  1. 在 Fiber 出现之前对比更新虚拟 DOM 使用递归加循环,这种对比方式有一个问题,就是任务一旦开始则无法中断
  2. 如果应用组件过多,层级较深,那么主线程将会被一直占用
  3. 这会导致一些用户操作或者动画无法立即得到执行,页面会产生卡顿
  4. 总结 => 递归无法中断,执行任务耗时长,JavaScript 是单线程的,比较虚拟 DOM 过程中无法执行其他任务,导致任务延迟页面卡顿

Fiber 架构思路

 

React16版本的架构可以分为三层,调度层,协调层,渲染层。

 

Scheduler调度层
react15的版本中,采用了循环加递归的方式进行了vdom的比对,由于递归使用js自身的执行栈,一旦开始就无法停止。直到任务执行完成。如果dom树的层级较深,就容易出现长期占用js主线程,导致gui渲染线程无法得到工作,造成页面卡顿。
在16的版本中,放弃了js递归方式进行vdom比对,而是采用了循环模拟递归,而且比对的过程是利用浏览器的空闲时间完成的,不会占用主线程,这就解决了vdom对比造成页面卡顿原因。
在window中提供了requestIdCallback的api,它可以利用浏览器空闲的时间执行任务,但是他自设能触发频率不稳定,并且不是所有浏览器都支持他。
react自己实现了任务调度库,叫做Scheduler,如果浏览器支持postMessage,那么他就会采用postMessage来进行调度,不支持再使用setTimoute,setTimeout的缺点是连续调用setTimeout(()=>{},0),最后会发现他的触发频率变成了4ms一次。并且Scheduler还采用了小顶堆算法,实现了任务优先级的概念。
Reconciler协调层
react15的版本中,协调器和渲染器交替执行,找到了差异就更新差异,这也是15无法中断的原因,因为会造成页面渲染不完全。
react16中,则是Scheduler和Reconciler交替工作,Scheduler负责调度,Reconciler负责找出差异,打上标记。等所有差异找完之后,才会交给Renderer统一进行DOM更新。这也是为什么react16可以实现可中断的异步更新的原因。
Renderer渲染层
渲染层工作的时候,是同步的,也就是无法中断的。可中断的异步更新的概念是描述Scheduler和Reconciler,他们是在内存中完成的。而Renderer是无法被中断的。
既然无法被中段,那么就不会出现dom渲染不完全的情况,因为渲染器的工作是一气呵成的,从0到1。

 

Fiber 节点

type Fiber = {
  // 用于标记fiber的WorkTag类型,主要表示当前fiber代表的组件类型如FunctionComponent、ClassComponent等
  tag: WorkTag,
  // ReactElement里面的key
  key: null | string,
  // ReactElement.type,调用`createElement`的第一个参数
  elementType: any,
  // The resolved function/class/ associated with this fiber.
  // 表示当前代表的节点类型
  type: any,
  // 表示当前FiberNode对应的element组件实例
  stateNode: any,

  // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
  return: Fiber | null,
  // 指向自己的第一个子节点
  child: Fiber | null,
  // 指向自己的兄弟结构,兄弟节点的return指向同一个父节点
  sibling: Fiber | null,
  index: number,

  ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject,

  // 当前处理过程中的组件props对象
  pendingProps: any,
  // 上一次渲染完成之后的props
  memoizedProps: any,

  // 该Fiber对应的组件产生的Update会存放在这个队列里面
  updateQueue: UpdateQueue<any> | null,

  // 上一次渲染的时候的state
  memoizedState: any,

  // 一个列表,存放这个Fiber依赖的context
  firstContextDependency: ContextDependency<mixed> | null,

  mode: TypeOfMode,

  // Effect
  // 用来记录Side Effect
  effectTag: SideEffectTag,

  // 单链表用来快速查找下一个side effect
  nextEffect: Fiber | null,

  // 子树中第一个side effect
  firstEffect: Fiber | null,
  // 子树中最后一个side effect
  lastEffect: Fiber | null,

  // 代表任务在未来的哪个时间点应该被完成,之后版本改名为 lanes
  expirationTime: ExpirationTime,

  // 快速确定子树中是否有不在等待的变化
  childExpirationTime: ExpirationTime,

  // fiber的版本池,即记录fiber更新过程,便于恢复
  alternate: Fiber | null,
}

 

双缓存技术介绍

在react中,DOM得更新采用了双缓存技术,双缓存技术致力于更快速得DOM更新。
举个例子,canvas绘制动画的时候,绘制每一帧动画之前就需要清除上一帧得动画,如果当前帧动画计算较长,就会导致出现替换白屏出现。为了解决这个问题,可以现在内存中构建当前帧动画,绘制完毕后直接替换上一帧,这样就不会出现白屏问题,这种在内存中构建并直接替换的技术叫做双缓存。
React使用双缓存技术完成Fiber树的构建和替换,实现DOM的快速更新。
在react中最多同时存在两颗fiber树,当前屏幕显示的是current Fiber,发生更新的时候,react在内存中构建workInporgress fiber。并且在两颗中之间对应的fiber节点有一个alternate指针,通过这个指针,workInporgress的构建就可以最大化的复用current fiber。进而更快速的构建玩workInporgress fiber。

 

useState是怎么实现的

// React Hooks模拟
// 用于保存组件是update还是mount
let isMount = true;
// 保存我们当前正在处理的hook
let workInProgressHook = null;
// React 中每一个组件对应一个fiber节点
const fiber = {
  // 保存组件本身
  stateNode: App,
  // 对于函数式组件,保存的是hooks的链表,对于类组件保存的是state
  // 问: Hooks信息为什么要使用链表存储而不是数组
  memoizedState: null,
}
// 用于调度组件执行
function schedule() {
  // 每次执行组件的时候,需要将workInProgressHook复位,重新指向第一个hook
  workInProgressHook = fiber.memoizedState;
  const app = fiber.stateNode();
  isMount = false;
  return app;
}

/**
 * useState实现
 */

function useState(initialState) {
  let hook;
  if (isMount) {
    // 首次渲染的时候需要初始化一个hook
    hook = {
      // hook上的memoizedState保存的是hook的状态
      memoizedState: initialState,
      // 指向下一个hook
      next: null,
      // 保存hook的更新信息 比如 setNum(n => n+1)中的n => n+1函数,为什么事一个队列,因为我们可以在一次操作中调用多次setNum(n => n+1)函数
      // 比如说在onClick事件中
      queue: {
        pending: null
      }
    }
    // 表示是第一个hook
    // 这就是为什么我们执行多个useState他们之间可以相互对应的原因
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook;
    } else {
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook; // 指针继续向下移动
  } else {
    // 表示已经有了一条链表
    // 此时workInProgressHook指向这个链表的正在执行的hook
    // 获取到我们当前要执行的hook
    hook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
  }
  console.log(hook)
  // 找到计算state依据的基础的state
  let baseState = hook.memoizedState;
  // 说明本次的状态改变有update要执行
  if (hook.queue.pending) {
    let firstUpdate = hook.queue.pending.next;
    // 遍历链表
    do {
      const action = firstUpdate.action; // setState传入的值这里是函数
      // 更新基础的state
      baseState = action(baseState);
      firstUpdate = firstUpdate.next;
    } while (firstUpdate !== hook.queue.pending.next)
    // 状态计算完成之后需要将hook.queue.pending = null
    hook.queue.pending = null;
  }
  hook.memoizedState = baseState;
  return [baseState, dispatchAction.bind(null, hook.queue)]
}
// state的update函数在React中有自己的名字,叫dispatchAction
function dispatchAction(queue, action) {
  // 那么我们如何知道dispatchAction是来更新哪一个state呢?
  // 创建一个数据结构,这个数据结构就记录着一次更新, 也是一个链表的形式
  // hook的链表是一个单向链表,但是update的链表却是一个环形链表。因为React中的update是有优先级的
  const update = {
    action,
    next: null
  }
  // 说明当前hook上还有要触发的更新,说明这个update是我们要触发的第一次更新
  if (queue.pending === null) {
    update.next = update;
  } else {
    // 不是第一次更新
    // queue.pending保存的是hook的最后一个update
    // queue.pending.next则就是这个环的第一个update,所以queue.pending也是一个环形链表
    update.next = queue.pending.next;
    queue.pending.next = update;
  }
  // 移动指针
  queue.pending = update;
  // 组件执行
  schedule();
}
function App() {
  const [num, setNum] = useState(0);
  const [num1, setNum1] = useState(10);
  console.log(isMount);
  console.log(num);
  console.log(num1);
  return {
    onClick() {
      setNum(n => n + 1)
    },
    onFocus() {
      setNum1(n => n + 10)
    }
  }
}
window.app = schedule();

 

React 合成事件

React 所有事件都委托在root 对象上;

• 当真实 DOM 元素触发事件,会冒泡到 root 对象后,再处理 React 事件;

• 在捕获阶段,先注册的先执行,且React合成事件先于原生事件执行;冒泡阶段,先注册的后执行,且原生事件先于React事件执行

合成事件和原生事件区别

 React 合成事件原生事件
命名 小驼峰(onClick) 纯小写(onclick)
事件处理函数 函数 字符串
阻止默认行为 event.preventDefault() return false

虚拟 DOM 的原理是什么?

  1. 虚拟 DOM 就是虚拟节点。React 使用 JS 对象来模拟 DOM 节点,之后将其渲染成真实的 DOM 节点
  2. 使用 JSX 语法写出来的 div 其实就是一个虚拟节点
    <div className="container">
       <span className="red">hi</span>
    </div>
    
  3. 上面的代码其实是调用了 React.createElement 函数,之后生成了一个 JS 对象
    {
      tag: 'div',
      props: {className: 'container'},
      children: [
        {
          tag: 'span',
          props: {
            className: 'red'
          },
          children: [
            'hi'
          ]
        }
      ]
    }
  4. 之后会根据 JS 对象信息即虚拟节点渲染为真实节点
  5. 如果节点发生改变,并不会把新的虚拟节点直接重新渲染为真实节点,而是要先经过 diff 算法得到一个 patch 再更新到真实节点上
  6. 虚拟 DOM 大大提升了 DOM 操作性能问题。通过虚拟 DOM 和 diff 算法减少不必要的 DOM 操作,保证性能。
  7. 之前 DOM 操作并不是很方便,但是现在只需要 setState 即可
  8. 但是 React 为虚拟 DOM 创造了合成事件,和原生 DOM 事件不太一样。所有 React 事件都绑定到了根元素,自动实现事件委托,如果混用合成事件和原生 DOM 事件,可能会出现 bug

React DOM diff 算法

  1. DOM diff 就是对比两颗虚拟 DOM 树的算法
  2. 当组件变化时,会 render 出一个新的虚拟 DOM,diff 算法对比新旧虚拟 DOM 之后,得到一个 patch,然后 React 用 patch 来更新真实 DOM
  3. 首先对比两棵树的根节点然后同时遍历两棵树的子节点,每个节点的对比过程如上
    • 如果根节点类型改变了,比如 div 变为了 p,那么直接认为整颗树都变了,不再对比子节点。此时直接删除对应的真实 DOM 树,创建新的真实 DOM 树
    • 如果根节点类型没有改变,就看看属性变了没有
      1. 属性没变,保留对应的真实节点
      2. 属性变了,就只更新该节点的属性,不重新创建节点 => case: 更新 style 时,如果多个 css 属性只有一个改变了,那么 React 只更新改变的
  4. case1:React 依次对比 A-A,B-B,空-C,发现 C 是新增的,最终会创建真实 C 节点插入页面
    <ul>
        <li>A</li>
        <li>B</li>
    </ul>
    
    // updated
    <ul>
        <li>A</li>
        <li>B</li>
        <li>C</li>
    </ul>
    
  5. case2: React 对比 B-A,删除 B 节点新建 A 节点;对比 C-B,删除 C 节点新建 B 节点(注意:并不是边对比边删除新建,而是把操作汇总到 patch 里在进行 DOM 操作,会进行标记);对比 空-C,新建 C 节点
    <ul>
        <li>B</li>
        <li>C</li>
    </ul>
    
    // updated
    <ul>
        <li>A</li>
        <li>B</li>
        <li>C</li>
    </ul>
    
  6. case2 其实只需要创建 A 节点,保留 B C 节点即可,此时 React 需要你添加 key
    <ul>
        <li key="b">B</li>
        <li key="c">C</li>
    </ul>
    
    // updated
    <ul>
        <li key="a">A</li>
        <li key="b">B</li>
        <li key="c">C</li>
    </ul>
    
  7. 此时 React 先对比 key,发现 key 增加了 a,此时保留 B C,新建 A 节点

Vue Dom diff

Vue 双端交叉对比

  1. 头头对比 => 对比两个数组的头部,如果找到,把新节点 patch 到旧节点,头指针后移
  2. 尾尾对比 => 对比两个数组的尾部,如果找到,把新节点 patch 到旧节点,尾指针前移
  3. 旧尾新头对比 => 交叉对比,旧尾新头,如果找到,把新节点 patch 到旧节点,旧尾指针前移,新头指针后移
  4. 旧头新尾对比 => 交叉对比,旧头新尾,如果找到,把新节点 patch 到旧节点,新尾指针前移,旧头指针后移
  5. 利用 Key 对比 => 用新指针对应节点的 key 去旧数组中寻找对应的节点
    • 没有对应的 key => 创建新节点
    • 有 key 并且是相同的节点 => 把新节点 patch 到旧节点
    • 有 key 但是不是相同的节点 => 创建新节点

React 有哪些声明周期钩子函数?数据请求放在哪个钩子里? 

React Lifecycle
  1. 挂载时调用 constructor,更新时不调用
  2. 更新时调用 shouldComponentUpdate 和 getSnapshotBeforeUpdate,挂载时不调用
  3. shouldComponentUpdate 在 render 前调用,getSnapshotBeforeUpdate 在 render 后调用
  4. 请求放置在 componentDidMount 里

React 如何实现组件间通信

  1. 父子组件通信 => props + 函数
  2. 爷孙组件通信 => 两层父子通信 | Context.Provider 和 Context.Consumer
  3. 任意组件通信 => 状态管理 => Redux | Mobx

如何理解 Redux

  1. Redux 就是一个状态管理库,可以实现任意组件之间的通信,其实就是将信息放置在顶部,如有需要其余组件去顶部获取即可
  2. Redux 核心概念
    1. store => 信息/状态存放的地方
    2. action => 可以更改 state 的唯一途径,根据 type 和 payload 进行更改 state
    3. dispatch => 用于派发事件
    4. reducer => 就是一个函数,传递给 reducer 一个旧的 state + action,他会返回一个新的 state
    5. Middleware => 中间件
  3. Redux 常与 ReactRedux 联合使用,ReactRedux 提供了以下 Api
    1. connect()(Component)
    2. mapStateToProps
    3. mapDispatchToProps
  4. Redux 常用中间件:
    1. redux-thunk => 扩展 redux 可以支持异步 action,如果 action 是一个函数,就直接调用它,否则就进入下一个中间件
    2. redux-promise => 如果 payload 是一个 Promise,就执行个这个 Promise,并在 then 里面去 dispatch

redux,react-redux,redux-saga,dva的区别与联系

【redux】

1、定位:它是将flux和函数式编程思想结合在一起形成的架构;

2、思想:视图与状态是一一对应的;所有的状态,都保存在一个对象里面;

3、API:

store:就是一个数据池,一个应用只有一个;  

state:一个 State 对应一个 View。只要 State 相同,View 就相同。

action:State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。Action 是一个对象。其中的type属性是必须的,表示 Action 的名称。其他属性可以自由设置。

dispatch:它是view发出action的唯一方法;

reducer:view发出action后,state要发生变化,reducer就是改变state的处理层,它接收action和state,通过处理action来返回新的state;

subscribe:监听。监听state,state变化view随之改变;

 

【react-redux】

1、定位:react-redux是为了让redux更好的适用于react而生的一个库,使用这个库,要遵循一些规范;

2、主要内容

UI组件:就像一个纯函数,没有state,不做数据处理,只关注view,长什么样子完全取决于接收了什么参数(props)
容器组件:关注行为派发和数据梳理,把处理好的数据交给UI组件呈现;React-Redux规定,所有的UI组件都由用户提供,容器组件则是由React-Redux自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。

connect:这个方法可以从UI组件生成容器组件;但容器组件的定位是处理数据、响应行为,因此,需要对UI组件添加额外的东西,即mapStateToProps和mapDispatchToProps,也就是在组件外加了一层state;

mapStateToProps:一个函数, 建立一个从(外部的)state对象到(UI组件的)props对象的映射关系。 它返回了一个拥有键值对的对象;

mapDispatchToProps:用来建立UI组件的参数到store.dispatch方法的映射。 它定义了哪些用户的操作应该当作动作,它可以是一个函数,也可以是一个对象。

以上,redux的出现已经可以使react建立起一个大型应用,而且能够很好的管理状态、组织代码,但是有个棘手的问题没有很好地解决,那就是异步;在react-redux中一般是引入middleware中间件来处理,redux-thunk

 

【redux-saga】:

1、定位:react中间件;旨在于更好、更易地解决异步操作(有副作用的action),不需要像在react-redux上还要额外引入redux-thunk;redux-saga相当于在Redux原有数据流中多了一层,对Action进行监听,捕获到监听的Action后可以派生一个新的任务对state进行维护;

2、特点:通过 Generator 函数来创建,可以用同步的方式写异步的代码;

3、API:

Effect: 一个简单的对象,这个对象包含了一些给 middleware 解释执行的信息。所有的Effect 都必须被 yield 才会执行。

put:触发某个action,作用和dispatch相同;

 

【dva】

1、定位:dva 首先是一个基于redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。dva = React-Router + Redux + Redux-saga;

2、核心:

State:一个对象,保存整个应用状态;
View:React 组件构成的视图层;
Action:一个对象,描述事件(包括type、payload)
connect 方法:一个函数,绑定 State 到 View
dispatch 方法:一个函数,发送 Action 到 State

3、model:dva 提供 app.model 这个对象,所有的应用逻辑都定义在它上面。

4、model内容:

namespace:model的命名空间;整个应用的 State,由多个小的 Model 的 State 以 namespace 为 key 合成;
state:该命名空间下的数据池;
effects:副作用处理函数;
reducers:等同于 redux 里的 reducer,接收 action,同步更新 state; subscriptions:订阅信息;

dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装,没有引入任何新概念,全部代码不到 100 行。dva 实现上尽量不创建新语法,而是用依赖库本身的语法,比如 router 的定义还是用 react-router 的 JSX 语法的方式(dynamic config 是性能的考虑层面,之后会支持)。
他最核心的是提供了 app.model 方法,用于把 reducer, initialState, action, saga 封装到一起

什么是高阶组件 HOC

  1. 参数是组件,返回值也是组件的函数
  2. React.forwardRef
    const FancyButton = React.forwardRef((props, ref) => {
      <button ref={ref} className="fancyButton">
        {props.children}
      </button>
    });
    
    // You can now get a ref directly to the DOM button
    const ref = React.createRef();
    <FancyButton ref={ref}>Click me!</FancyButton>
    
  3. ReactRedux 的 connect

useEffrct 原理

// 重新渲染函数
function render() {
    stateIndex = 0;
    effectIndex = 0;
    ReactDOM.render(<App />, document.getElementById('root'));
}

// 存储上一次的依赖值
const preDepsAry = [];
let effectIndex = 0;
function useEffect(callback, depsAry) {
    // 判断callback是否是函数
    if (Object.prototype.toString.call(depsAry) !== '[object Array]') {
        throw new Error('useEffect 函数的第二个参数必须是数组');
    } else {
        // 获取上一次的状态值
        const prevDeps = preDepsAry[effectIndex];
        // 如果存在就去做对比,如果不存在就是第一次执行
        // // 将当前的依赖值和上一次的依赖值做对比, every如果返回true就是没变化,如果false就是有变化
        const hasChanged = prevDeps ? depsAry.every((dep, index) => dep === prevDeps[index]) === false : true;
        // 值如果有变化
        if (hasChanged) {
            callback();
        }
        // 同步依赖值
        preDepsAry[effectIndex] = depsAry;
        // 累加
        effectIndex++;
    }
}

 

posted @ 2023-05-10 08:59  前端菜园子  阅读(35)  评论(0编辑  收藏  举报