1 组件 VS. 组件实例 VS. react元素 VS. DOM元素
组件函数是一个函数,组件实例是函数的具体应用,类似于类和实例对象,组件实例是独立的,拥有自己的生命周期,state,props,react执行代码时,运行组件实例时,会调用组件函数,背后会运行react.createElement(),最终返回一个react element,react element是一个JavaScript对象,包含所有的创建DOM元素时需要的信息,具体创建DOM的工作由react DOM完成,根据信息创建具体的DOM 元素,然后浏览器重新绘制页面,展示出来
总结:使用组件函数时,写一个组件实例,返回react element(是一个普通的JavaScript对象,包含所有的DOM信息),ReactDOM创建对应的DOM元素 结合图说明如下:
1.1 补充 :组件实例和组件元素的区别
组件实例是 ,组件元素react element是最终函数返回的结果, 运行后,经过完整的渲染流程后,,调用Tab()组件函数后,返回的结果是react element,但是也可以不经过完整的流程,直接写Tab()调用函数,返回react elemnt 结合图说明如下:
2 渲染机制
2.1 引入
刚才说了从组件函数-->组件实例-->react元素-->>DOM元素-->屏幕的大体过程,那具体是怎么从react element到屏幕的呢
答案是从触发渲染--> 渲染阶段--> 提交阶段--> 浏览器绘制
这里的渲染是指react的一个计算阶段,和DOM操作无关,和屏幕展示更没有关系
2.2 触发渲染:
触发渲染分两种情况,一个是初始渲染,所有的组件都被渲染,一个是状态更新,导致再渲染 再渲染:从整体上来说,触发条件是,state状态更新,但是从单个组件来说,触发的条件是state,props,context更新,
2.3 渲染阶段
渲染阶段的目标是得到实际需要进行的插入、删除、更新的DOM操作list,因为react不负责DOM操作,具体操作由react DOM库执行,所以react需要将这些操作计算出来,传递给react DOM
具体的过程是: 分为两种情况:初次渲染和后续state更新触发的渲染
-
初次渲染:调用所有的组件函数-->生成一个完整的react element tree-->构建一个完整的fiber tree-->(后面:react DOM创建所有的DOM元素-->屏幕绘制页面)
-
state更新触发再渲染 当页面上进行某些操作,导致了状态更新时,为了尽可能的减少DOM的操作,因为DOM的操作是昂贵的,所以我们会先需要计算出有哪些元素是可以复用的,哪些需要更新属性,哪些需要删除和新增元素,所以react需要计算出具体需要进行的DOM操作--一个Effect list链表,
【state更新触发渲染】 ----> 【调用对应的组件函数,生成一个新的局部react element tree】 ----> 【拼到完整的react element tree中(新的完整的react element tree)】 ------> 【根据这个新的react element tree和当前的fiber tree进行差分Diffing和协调Reconciliation :会深度遍历fiber tree,对每一个fiber比较这个fiber和react element tree判断是否需要复用fiber,是否需要更新、复用、删除、新增fiber,并将信息标记到fiber上】 ----> 【遍历完成之后会生成一个完整的workInProgress tree,也得到了一个effect list(需要操作的链表)】 这就是渲染阶段,在提交阶段react DOM会根据这个链表进行Dom操作最后屏幕重新绘制 需要说明的是:状态更新是批量提交,批量更新的,所以多个状态更新,最终会一次性生成一个新的react element tree, 协调过程是异步可中断的,被拆分为Fiber工作单元,支持优先级调度、暂停、丢弃和恢复
由react DOM、react Native或者其他的渲染器完成,根据Effect list链表信息,新增、删除、更新DOM,这个步骤是同步的,一次性更新,不可中断,避免只显示局部结果,保证UI和state是同步的
提交阶段完成后,WorkInProgress tree会成为新的Current Fiber tree
补充:Diffing差分原理
根据type和key来进行区分两个react element是否相同,是否需要删除和新增的,类型相同并且key一样,视为同一个元素,否则视为不同元素,没有key时,仅比较相同位置的type相同时,视为同一元素,否则视为不同元素,确认相同元素后再确认是否需要更新props,确认更新元素还是直接复用,在Fiber上标记为不同的Tag:新增,删除,更新,复用
-key:元素的key属性让react去用于区分不同的组件实例
渲染时
1. 如果Key保持一致--> 元素也会保持在DOM中,用于在列表中使用keys
2. 如果改变key-->DOM元素会被销毁和重建,用于重置状态
背后的原理: 差分核心规则:比较type和key,两者都相同为同一元素,没有key时,仅比较相同位置的type相同时,视为同一元素,否则视为不同元素
key作用:元素的key属性让react去用于区分不同的组件实例,告诉差分算法该元素的唯一性--标识
- 如果Key保持一致--> 元素也会保持在DOM中,在列表中使用key(稳定key):无key:一次新增,两次更新,有key:一次新增
- 如果改变key-->DOM元素会被销毁和重建,用于重置状态
3 渲染逻辑规则:纯组件
3.1 什么是渲染逻辑?
react组件的两种逻辑:渲染逻辑render logic+处理逻辑handler logic
- 渲染逻辑:描述组件的视图结构,就是页面怎么展示,位于组件函数顶层代码中,每次渲染时都会执行
- 处理逻辑:事件处理函数触发时执行,或者是useEffect对应情况时执行,实际代码在事件处理函数或useEffect中,:更新state,发起http请求,读取输入字段,导航到另一个页面
3.2 概念:纯函数和副作用
- 纯函数:不与外界交互的函数,相同输入相同输出,无副作用
- 副作用:函数依赖或修改了作用域外的数据,对外界造成了影响,包括:修改外部变量,http请求,操作DOM,依赖可变数据(Math.random(),new Date()),提醒:副作用不是有害的,只是需要隔离到可控范围,毕竟如果程序需要与外界交互才能产生实际意义
渲染逻辑原则:与纯函数类似,pure,相同输入相同输出(相同JSX),不能有副作用:
- 网络请求
- 启动定时器
- 直接操作DOM
- 修改外部变量或者是对象
- 更新状态或refs-->会导致无限循环
3.3 批量更新状态,更新--渲染是异步的
更新状态特点:批量更新,异步更新
强制同步书信 reactDom.flushSync()
除了状态更新是批处理的,react 18后,自动批处理 事件处理,timeouts,promise,nativeEvents
4 react合成事件--事件传播和事件委托
react中所有的事件都被委托到root DOM container上
在事件处理器中使用事件,使我们获得的是一个合成事件对象,不是浏览器原生的对象-->在不同浏览器上事件一样工作。区别是绝大多数的合成事件对象会冒泡,包括不会在原生浏览器上冒泡的事件,比如focus blur和change,只有scoll事件不会冒泡
合成事件 SyntheticEvent: 对DOM原生事件对象的封装
- 拥有和原生事件对象相同的方法比如e.stopPropagation() e.preventDefault()
- 修复浏览器的差异,让事件在不同的浏览器中一样运行
- 绝大多数事件冒泡(包括focus blur change 除了scoll)
react VS. JS 中事件处理器:
- 阻止默认行为:必须显式调用
e.preventDefault()(返回false无效) - 事件命名:始终使用驼峰式(如
onMouseEnter) - 事件阶段:通过
onClickCapture在捕获阶段处理事件
5 框架 VS. 库: react是一个库而不是框架
famework:
- 轻松自如:构建完整应用所需的一切在框架中
- 别无选择:你只能使用框架的工具和约定(这并不总是坏的) library:
- 自由:你可以(需要)选择不同的第三方的库来构建完整的应用
- 决策疲劳:你需要研究、下载、学习并随时更新多个外部库 react 第三方生态:
react之上的框架:NEXT.js Remix Gatsby React 框架还提供了许多其他功能:服务器端渲染(SSR)、静态网站生成(SSG)、更好的开发体验(DX)等