说一说对 react 的理解
- 思路
- 是什么
- 能干什么 用途和应用场景
- 如何实现的 核心原理
- 效果怎么样 优缺点
- 是什么
- React 是一个用于构建用户界面的 javascript 库
- 能干什么
- 可以通过组件化的方式构建快速响应的大型 web 应用程序
- 如何实现
- 声明式渲染
- 组件化
- 优缺点
- 优点
- 开发团队和社区强大
- 多元化 可以编写多个程序 比如 ios android VR 和命令
- API 比较简单
- 缺点
- 没有官方系统解决方案 选型成本高
- 过于灵活 不容易写出高质量的应用
- 优点
为什么 react 要引入 jsx 或者说 jsx 的作用是什么
- jsx 其实是 react.createElement 的语法糖
- 目的是为了实现
- 声明式渲染
- 让代码结构可以非常清晰 可读性较强
- HTML js css 分离 能实现高内聚低耦合 方便重用和组合
- 不会引入新的概念和语法 会 js 就会写 react
说一下对虚拟节点的理解
- 优点
- 处理了浏览器兼容问题,避免用户真实操作 dom
- 内容经过了 xss 处理 防止了 xss 攻击
- 容易实现跨平台开发
- 更新的时候可以实现差异化更新,避免更新 dom 操作
- 缺点
- 虚拟 DOM 需要消耗额外的内存
- 首次渲染不一定会更快
函数组件和类组件的区别
- 编程思想不同
- 类组件需要创建实例,是基于面向对象的方式编程 而函数式组件不需要创建实例,接收输入,返回输出,是基于函数式编程
- 内存占用
- 类组件需要创建保存实例,会占用一定的内存空间,函数组件不需要实例,可以节约内存占用
- 捕获特性
- 函数组件具有值捕获特性
- 可测试性
- 函数式组件更方便编写单元测试
- 状态
- 类组件有自己的实例,可以定义状态,而且可以修改状态。函数式组件里之前没有状态,但是现在可以通过 useState 来创建
- 生命周期
- 类组件有自己的生命周期,可以在生命周期内编写逻辑,函数组件没有生命周期 但是可以通过 useEffect 来实现相应的效果
- 逻辑复用
- 类组件是通过继承的方式, 函数组件是通过自定义 Hooks 来实现 且函数组件比类组件复用的优先级高
- 跳过更新
- 类组件是通过 shouldComponentUpdate 和 PureCompone 来跳过更新,而函数组件是通过 memo 来跳过更新
- 发展前景
- 函数式必定会成为主流, 因为它可以更好的规避 this,规范和复用,更好的适合时间分片和并发渲染
react 的渲染过程
-
虚拟 dom+ 渲染可中断(fiber)
-
之前遇到的问题 js 任务执行时间过长
- 浏览器刷新频率为 60Hz,大概 16.6 毫秒渲染一次,而 JS 线程和渲染线程是互斥的,所以如果 JS 线程执行任务时间超过 16.6ms 的话,就会导致掉帧,导致卡顿,解决方案就是 React 利用空闲的时间进行更新,不影响渲染进行的渲染
- 把一个耗时任务切分成一个个小任务,分布在每一帧里的方式就叫时间切片
Fiber
- 为什么出现 fiber
- dom 更新都是从根节点开始逐级查阅 react15 之前使用的是队列 没办法中断 如果嵌套层数过多 会影响后续渲染 出现卡顿现象
帧的概念
- 目前大多数设备的屏幕刷新率为 60 次/秒
- 当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿 16.66ms
- 每个帧的开头包括样式计算、布局和绘制
- JavaScript 执行 Javascript 引擎和页面渲染引擎在同一个渲染线程,GUI 渲染和 Javascript 执行两者是互斥的
- 如果某个任务执行时间过长,浏览器会推迟渲染
- 一帧的事件队列
- 输入事件
- Blocking inut events 阻塞输入事件 如 touch wheel
- Non-blocking inut events 非阻塞输入事件 如 click keypress
- javascript 宏任务
- 定时器
- Begin frame
- window.resize
- pre frame events
- scroll
- media query change
- requestAnimationFrame
- requestAnimationFrame callbacks
- Layout
- recalculate style 计算样式
- update Layout 更新布局
- paint
- record
- paint invaildation
- compositing update
- idle peroid 空闲阶段
- 输入事件
什么是 fiber
- 本质还是一个 js 对象 通过 js 对象的不同定义来解释成不同的东西
- 我们可以通过某些调度策略合理分配 CPU 资源,从而提高用户的响应速度
- 通过 Fiber 架构,让自己的协调过程变成可被中断。 适时地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互
- Fiber 是一个执行单元,每次执行完一个执行单元, React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去
- Fiebr 是一个数据结构 React 目前的做法是使用链表,每个虚拟节点内部表示为一个 Fiber
requestAnimationFrame
- requestAnimationFrame 回调函数会在绘制之前执行
- requestAnimationFrame(callback) 会在浏览器每次重绘前执行 callback 回调, 每次 callback 执行的时机都是浏览器刷新下一帧渲染周期的起点上
- requestAnimationFrame(callback) 的回调 callback 回调参数 timestamp 是回调被调用的时间,也就是当前帧的起始时间
- rAfTime performance.timing.navigationStart + performance.now() 约等于 Date.now()
- performance.now()是页面渲染的时间
requestIdleCallback
- requestIdleCallback 使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
- 正常帧任务完成后没超过 16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务
- requestAnimationFrame 的回调会在每一帧确定执行,属于高优先级任务,而 requestIdleCallback 的回调则不一定,属于低优先级任务 如果别的任务用时超过了 16ms 他就不会执行
- callback:回调即空闲时需要执行的任务,该回调函数接收一个 IdleDeadline 对象作为入参。其中 IdleDeadline 对象包含:
- didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用
- timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少
- options:目前 options 只有一个参数
- timeout。表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲
MessageChannel
- 由于 requestIdleCallback 只有 chorme 支持 所以我们要自己模拟一个 requestIdleCallback 出来
- MessageChannel API 允许我们创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据
- MessageChannel 创建了一个通信的管道,这个管道有两个端口,每个端口都可以通过 postMessage 发送数据,而一个端口只要绑定了 onmessage 回调方法,就可以接收从另一个端口传过来的数据
- MessageChannel 是一个宏任务
- postMessage 发送消息 onmessage 接收消息
Fiber 执行阶段
- 每次渲染有两个阶段:Reconciliation(协调 render 阶段)和 Commit(提交阶段)
- 协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更 React 称之为副作用(Effect)
- 提交阶段: 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行了。这个阶段必须同步执行,不能被打断
- render 算法 二叉树的反序
- 从顶点开始遍历
- 如果有第一个儿子,先遍历第一个儿子
- 如果没有第一个儿子,标志着此节点遍历完成
- 如果有弟弟遍历弟弟
- 如果有没有下一个弟弟,返回父节点标识完成父节点遍历,如果有叔叔遍历叔叔
- 没有父节点遍历结束
- 先儿子,后弟弟,再叔叔,辈份越小越优先
- 什么时候一个节点遍历完成? 没有子节点,或者所有子节点都遍历完成了
- 没爹了就表示全部遍历完成了
- Commit 过程
- 给每一个 Fiber 执行单元 加一些标识 形成链表 firstEffect 标识子级第一个副作用 nextEffect 标识下一个副作用 lastEffect 标识子级最后一个副作用
fiber 类型
- 根 fiber 整个 fiber 树最顶部的一个节点节点
- 元素节点 fiber div span a 等元素节点的 fiber
- 文本类型 fiber 文字类型
- 类组件 fiber 类组件
- 函数组件 fiber 函数组件
fiber 流程
- 从根节点开始渲染和调度 有两个阶段
- render 阶段
- 也叫 diff 阶段 对比新旧的虚拟 DOM 进行增量 更新 或者删除
- 这个阶段比较花费时间, 因此我们需要对任务进行拆分,拆分的最小维度是一个虚拟 dom 节点 此阶段可以暂停 因为用的是链表
- 这个阶段的成果有两个 1. 根据虚拟 dom 树构建出 fiber 树 2. 收集 effectList(副作用依赖) 发生变化了的虚拟 dom 节点队列
- commit 阶段
- 进行 dom 更新创建 此阶段不能暂停
双缓冲机制
- fiber 树的对比 diff 是通过对比新旧树 如果每次更新都生成一个新的 fiber 树 随着更新次数的增加 树的数量也就越多 性能就会越差 所以有了双缓冲机制
- 第三次对比的时候 使用的是第一次渲染的树 只是把树上的节点都改成最新的了 不会增加树的数量
流程图
[fiber 更新]](limingshuai.top/assets/pdf/…)
diff 算法
- React17+中 DOM-DIFF 就是根据老的 fiber 树和最新的 fiber 树对比生成新的 fiber 树的过程
- 优化原则
- 只对同级节点进行对比,如果 DOM 节点跨层级移动,则不会复用
- 不同类型的元素会产出不同的结构,会销毁老节点,创建新节点
- 可以通过 key 标识移动的元素
- 单节点
- 多节点
- 如果新的节点有多个节点的话
- 节点有可能更新、删除、新增
- 多节点的时候会经历二轮遍历
- 第一轮主要是处理节点的更新,更新包括属性和类型的更新
- 第二轮处理节点的新增、删除、移动
- 移动原则是尽量少移动,如果必须有一个要动,新地位高的不动,新地位低的动
合成事件
- React 把事件委托到 document 对象上
- 当真实 DOM 元素触发事件,先处理原生事件,然后会冒泡到 document 对象后,再处理 React 事件
- React 事件绑定的时刻是在 reconciliation 阶段,会在原生事件的绑定前执行
- 目的和优势
- 进行浏览器兼容,React 采用的是顶层事件代理机制,能够保证冒泡一致性
- 事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或释放事件对象(React17 中被废弃)
- React17 后的改动
- 事件系统变更
- 更改事件委托
- 更改事件委托节点
- 在 16 版本中,React 都会把事件绑定到页面的 document 元素上,这在多个 React 版本共存的情况下就会虽然某个节点上的函数调用了 event.stopPropagation(),但还是会导致另外一个 React 版本上绑定的事件没有被阻止触发所以在 17 版本中会把事件绑定到 render 函数的节点上
- 去除事件池
- 17 版本中移除了事件对象池,这是因为 React 在旧浏览器中重用了不同事件的事件对象,以提高性能,并将所有事件字段在它们之前设置为 null。在 React 16 及更早版本中,使用者必须调用 event.persist() 才能正确的使用该事件,或者正确读取需要的属性
- 更改事件委托节点
- 更改事件委托
- 事件系统变更
React 的优先级
- 事件优先级
- 连续事件 canplay error 的优先级最高 为 2
- 用户阻塞事件 deag scroll mouseover 等 连续触发,阻塞渲染 优先级为 1
- 离散事件 click keydown 等 优先级为 0
- 更新优先级
- setState 本质上是调用 enqueueSetState,生成一个 update 对象,这时候会计算它的更新优先级,即 update.lane
- 首先找出 Scheduler 中记录的优先级 schedulerPriority,然后计算更新优先级
- 任务优先级
- update 会被一个 React 的更新任务执行
- 任务优先级被用来区分多个更新任务的紧急程度
- 收敛同等优先级的任务调度
- 高优先级任务及时响应
- 调度优先级
- 一旦任务被调度,那么它就会进入 scheduler
- 在 Scheduler 中,这个任务会被包装一下,生成一个属于 Scheduler 自己的 task,这个 task 持有的优先级就是调度优先级
hooks
react18 新特性
并发模式
- Concurrent 模式是一组 React 的新功能,可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整
- 在 Concurrent 模式中,渲染不是阻塞的。它是可中断的
- 更新优先级
- 以前更新没有优先级的概念,优先级高的更新并不能打断之前的更新,需要等前面的更新完成后才能进行
- 用户对不同的操作对交互的执行速度有不同的预期,所以我们可以根据用户的预期赋予更新不同的优先级
- 高优先级 用户输入 窗口缩放和拖拽事件
- 低优先级 数据请求和下载文件
- 高优先级的更新会中断低优先级的更新
- 低优先级更新会基于高优先级的结果重新更新
双缓冲
- 当数据量很大时,绘图可能需要几秒钟甚至更长的时间,而且有时还会出现闪烁现象,为了解决这些问题,可采用双缓冲技术来绘图
- 双缓冲即在内存中创建一个与屏幕绘图区域一致的对象,先将图形绘制到内存中的这个对象上,再一次性将这个对象上的图形拷贝到屏幕上,这样能大大加快绘图的速度
- 对于 IO-bound 的更新 (例如从网络加载代码或数据),并发意味着 React 甚至可以在全部数据到达之前就在内存中开始渲染,然后跳过令人不愉快的空白加载状态
批量更新
- 从队列中加了一个 queueMicrotask(setTimeout)
- 在 React 18 之前,我们只在 React 事件处理程序期间批量更新。默认情况下,Promise、setTimeout、本机事件处理程序或任何其他事件内部的更新不会在 React 中批处理。
- 从 React 18 开始 createRoot,所有更新都将自动批处理,无论它们来自何处。
- 由于所有更新 setTimeout 都是批处理的,所以 React 不会 setState 同步渲染第一次的结果——渲染发生在下一个浏览器滴答声中 在异步事件里 setState 里不会再同步获取最新的值
- 可以使用 ReactDOM.flushSync 强制更新
suspense 异步加载组件
- Suspense 让你的组件在渲染之前进行等待,并在等待时显示 fallback 的内容
- Suspense 内的组件子树比组件树的其他部分拥有更低的优先级
- 执行过程
- 在 render 函数中我们可以使用异步请求数据
- react 会从我们缓存中读取这个缓存
- 如果有缓存了,直接进行正常的 render
- 如果没有缓存,那么会抛出一个 promise 异常
- 当这个 promise 完成后(比发请求数据完成),react 会继续回到原来的 render 中,把数据 render 出来
- 完全同步写法,没有任何异步 callback 之类的东西
- React 提供了一个内置函数 componentDidCatch,如果 render() 函数抛出错误,则会触发该函数
- ErrorBoundary(错误边界)是一个组件,该组件会捕获到渲染期间(render)子组件发生的错误,并有能力阻止错误继续传播
SuspenseList
- SuspenseList 通过编排向用户显示这些组件的顺序,来帮助协调许多可以挂起的组件
- revealOrder (forwards, backwards, together) 定义了 SuspenseList 子组件应该显示的顺序
- together 在所有的子组件都准备好了的时候显示它们,而不是一个接着一个显示
- forwards 从前往后显示
- backwards 从后往前显示
- tail (collapsed, hidden) 指定如何显示 SuspenseList 中未加载的项目
- 默认情况下,SuspenseList 将显示列表中的所有 fallback
- collapsed 仅显示列表中下一个 fallback
- hidden 未加载的项目不显示任何信息
startTransition
- startTransition 是一个接受回调的函数。我们用它来告诉 React 需要推迟的 state
- 允许组件将速度较慢的数据获取更新推迟到随后渲染,以便能够立即渲染更重要的更新
useDeferredValue
- 返回一个延迟响应的值
- 在 useDeferredValue 内部会调用 useState 并触发一次更新,但此更新的优先级很低
useTransition
- useTransition 允许组件在切换到下一个界面之前等待内容加载,从而避免不必要的加载状态
- 它还允许组件将速度较慢的数据获取更新推迟到随后渲染,以便能够立即渲染更重要的更新
- useTransition hook 返回两个值的数组
- startTransition 是一个接受回调的函数。我们用它来告诉 React 需要推迟的 state
- isPending 是一个布尔值。这是 React 通知我们是否正在等待过渡的完成的方式
- 如果某个 state 更新导致组件挂起,则该 state 更新应包装在 transition 中
SSR 水合
- 之前的 SSR 请求是串行的 上一个请求没返回 下一个是不会返回的
- 18 之后 就算上一个请求没有返回 下一个也可以直接返回
reactDOM.render
核心方法 createDOM
- 根据传入的虚拟节点类型 来进行不同的节点操作
- 文本类型
- 创建文本
- 元素节点类型
- 创建元素节点
- 函数类型
- 看看有没 isReactComponent 这个变量 有的话就是类组件 没有的话就是函数组件
- 执行函数得到真实的虚拟节点 重新执行 createDOM
- 如果是类组件的话 new 一个类的实例 执行 willMount 生命周期 如果 ref 存在 就把实例挂载 ref.current 上 实例.render 执行 生成真正的虚拟 dom 重新执行 createDOM
- 看看有没 isReactComponent 这个变量 有的话就是类组件 没有的话就是函数组件
- 文档碎片类型
- 创建一个文档碎片
- ref 类型
- ref 是 class 类的父级对象 ref 对象内的 type 指向的就是 class 实例 所以要 type.render 执行 得到真正的虚拟 dom
- 元素类型
- 创建一个真实 DOM 节点
- provide 类型
- 根据 type 的 context 生成 context
- 让 context.currentValue 等于 props.value
- 生成虚拟节点
- 重新执行 createDOM
- context 类型
- 获取 provide 的 context
- 用 content 的 currentValue 生成虚拟 dom
- 重新执行 createDOM
- memo 节点
- 让 type 的 type 执行 生成一个虚拟节点
- 重新执行 createDOM
- 文本类型
- 更新 props 属性
- 合成事件
- 给 dom 上创建一个唯一对象 store
- 让 store 对象的 eventType 等于用户自定义的方法和事件
- 如果 document 上没有这个 eventType 属性 则给 document 上挂载这个属性的合成事件 dispatchEvent
- 获取事件名称和事件源
- 定义事件方法
- 开启记录批量更新
- 生成合成事件 createSyntheticEvent
- 循环 event 达到每一项 并改变 this
- 把原生事件挂载到合成事件上
- 默认阻止冒泡和默认事件
- 重新 prevDefault 事件 兼容 ie
- 重写 stopPropagation 事件 兼容 ie
- 获取事件源 进行事件委托
- 模拟事件冒泡
- 结束批量更新操作
- 合成事件
- 如果该节点下存在子节点 则递归调度子节点
- 把最终生成的真实 Dom 挂载到 root 上
JSX 渲染
- createElement 方法的语法糖
- 根据入参内容 如果大于 3 就要循环处理子节点
- 将单独拿到的每个节点进行处理 如果是字符串或者数字 要转成对象 type 是 REACT_TEXT 统一代码结构 方便之后的 DIFF 移动
- 生成虚拟节点(本质上还是一个对象 根据对象不同 key 对应的类型不同 来区分是什么节点)
return {
$$typeof: REACT_ELEMENT,
type,
ref,
key,
props,
};
setState 更新
- 首先是同步异步问题
- 本质上是有个任务队列(链表) 调用 setState 的时候 会给这个队列里增加一个一项 批量更新 统一执行
- 但是 setTimeout 这些事件不属于合成事件内的 所有就会同步执行
- 解决方案 ustable_batchUpdates()方法包裹一层就可以了
- 原理就是打开批量更新 把回调函数执行完了 再把批量更新关上 暴力解法
- 深入原因的话
- 之前的版本对比批量更新的判断只是用了一个变量 isBatchUpdate 来判断是否批量更新
- react17 有了 lane 更新优先级 在判断批量更新的时候会根据优先级来判断 一共 31 的 lane
- react18 有了并发模式 createRoot 代替 render 函数
- 任务变的可中断 就不需要在通过 ustable_batchUpdates 来强制开启批量了
- 每一项的 fiber 节点结构
- 每个 fiber 节点都有个 updateQueue 代表更新队列 本质上是个链表
- 父节点的 child 属性执行子节点 子节点的 return 属性指向父节点
- 函数实例上的 reactInternals 指向父节点
- 调用 setState 时
- 调用 Updater 的 enqueueSetState 方法 把新状态入队
- 通过 reactInternals 获取 fiber
- 计算超时时间 避免低优先级的任务一直执行不了 超过这个事件 就把优先级调到当前事件执行优先级 1
- 根据超时时间和事件优先级 +链表指针 构建一个新的 update 对象
- 让 setState 传入的要更新的值挂载到 update 的 payload 上
- 给实例的 fiber 的 updateQueue 队列中 Push 当前 update 对象
- 开始调度
- 找到根节点 markUpdateLaneFromFiberToRoot
- 创建一个任务 从根节点开始进行更新 ensureRootIsScheduled
- 判断当前根节点正在执行的任务优先级 是否等于最高优先级 进行 setState 拦截 把后面执行的直接 return
- 是的话 就说明当前还在同一个 setState 里 直接 return
- 把真正的渲染任务添加到一个 syncQueue 队列中 scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
- performSyncWorkOnRoot 是真正的渲染任务 走 fiber 的调和提交流程
- 在微任务队列 queueMicrotask 中 执行 syncQueue 中的所有渲染函数 flushSyncCallbackQueue 执行更新队列中的函数 清空队列
- 判断当前根节点正在执行的任务优先级 是否等于最高优先级 进行 setState 拦截 把后面执行的直接 return
- 如果不是并发模式 且非批量更新 直接调用更新函数
- 调用 Updater 的 enqueueSetState 方法 把新状态入队
- performSyncWorkOnRoot 给实例的 state 重新赋值
- performSyncWorkOnRoot 更新视图
- compareTwoVdom 进行 vdom 比较
- 查找老节点的真实节点
- 获取新节点的真实节点
- 替换
- 如果老节点和新节点都不存在
- 直接 return
- 老节点存在 新节点不存在
- 删除老节点 并删除节点相关的 ref 和 children
- 老节点不存在 但是新节点存在
- 创建新节点 判断下一个节点是否存在 如果存在 就在下一个节点前插入 不存在 就插入在老节点之后
- 老节点存在 新节点存在 并且 节点类型不同
- 删除老节点 创建新节点 判断下一个节点是否存在 如果存在 就在下一个节点前插入 不存在 就插入在老节点之后
- 老节点存在 新节点存在 并且节点类型相同
- 对新老节点进行深度对比
- 如果老节点是 memo 节点
- 浅比较两个对象是否相等
- 相等的话 不同更新
- 不相等的话
- 得到老节点
- 从新节点里解构出 function 并执行
- 拿到最新的渲染函数
- 进行新旧节点对比 compareTwoVdom
- 浅比较两个对象是否相等
- 如果老节点是 context 节点
- 获取老节点
- 根据老节点拿到父节点
- 获取新节点的 type 和 props
- 从 type.context 得到 context 对象
- props.children 执行得到最新的渲染函数
- 进行新旧节点对比 compareTwoVdom
- 如果老节点是 provide 节点
- 获取老节点
- 根据老节点拿到父节点
- 获取新节点的 type 和 props
- 让 type 的 context 和 currentValue 等于 props 的 value
- 得到最新的渲染函数
- 进行新旧节点对比 compareTwoVdom
- 如果老节点是文本节点
- 获取老节点赋值给新节点的 dom 属性
- 老节点的 props 和新节点的 props 不全等
- 让新节点的 textContent 等于新节点的 props
- 如果老节点是文档碎片节点
- 获取老节点的真实 DOM 挂载到虚拟节点的 dom 属性上
- 更新子节点
- 如果是函数或者类组件节点
- 先判断 isReactComponent
- 类组件
- 让新虚拟节点的实例等于老虚拟节点实例
- 更新类组件 使用 Update 类的 emitUpdate
- 函数组件
- 获取老节点
- 老节点不存在直接 return
- 解构出 type 和 props
- 生成新的虚拟函数
- 进行新旧节点对比 compareTwoVdom
- 如果是元素节点
- 重新定义(格式化)老的子节点列表
- 重新定义(格式化)新的子节点列表
- 构建一个 map 对象
- 构建一个初始索引
- 循环老节点
- 如果每一项中存在 key 就用 Key 作为索引,没有 key 就用 index
- 分别添加到 map 对象中
- 创建一个 DOM 补丁包 来收集 DOM 操作 patch
- 循环新的子节点列表
- 记录当前索引
- 如果每一项中存在 key 就用 Key 作为索引,没有 key 就用 index
- 找到索引对应的老儿子
- 判断老儿子是否存在
- 重新更新元素
- 如果老节点的索引小于最后的索引 给 patch 中追加对象 类型为移动
- 老节点已经被复用,从 map 中删除
- 将最后的索引标识改为索引标识或者老节点索引中最大的那个值
- 不存在
- 给 patch 中追加对象
- 过滤出来补丁包中需要移动的节点
- 先把要移动的和删除的都删除掉
- dom 补丁包循环
- 解构出 type 类型(移动或者插入)
- 获取老的真实子节点集合
- 如果类型是插入
- 获取新的真实节点
- 从老的子节点集合中找到当前子节点
- 存在就在之前插入
- 不存在就在之后插入
- 如果类型是移动
- 获取新的真实节点
- 从老的子节点集合中找到当前子节点
- 存在就在之前插入
- 不存在就在之后插入
- 如果老节点和新节点都不存在
- compareTwoVdom 进行 vdom 比较
新的生命周期
- getDerivedStateFromProps
- getDerivedStateFromProps(props, state) 这个生命周期的功能实际上就是将传入的 props 映射到 state 上面
- getSnapshotBeforeUpdate
- getSnapshotBeforeUpdate() 被调用于 render 之后,可以读取但无法使用 DOM 的时候。它使您的组件可以在可能更改之前从 DOM 捕获一些信息(例如滚动位置)。此生命周期返回的任何值都将作为参数传递给 componentDidUpdate()
高阶组件
- 高阶组件就是一个函数 传给它一个组件, 返回一个新的组件
- 高阶组件的作用其实就是为了组件之间的代码复用
- 属性代理 基于属性代理:操作组件的 props
- 反向继承 基于反向继承:拦截生命周期、state、渲染过程