这是一篇学习的笔记,很荣幸能听到那么好的课,虽然但是觉得这个课有点难懂,一遍遍 的重复着,慢慢的记笔记,但是正事这个课让我重塑了对React 印象。希望这篇笔记能帮到你们
Vue的体系/原理的相关内容百花齐放
但React 只是体系/原理的相关内容却屈指可数
jsx 的本质是什么,它和js之间到底是什么关系
为什么要用jsx? 不用会用什么后果
Jsx背后的功能模块是什么,这个功能模块都做了哪些事情
Jsx 是javaScript 的一种语法扩展,它和模板语言很接近,但是它充分具备JavaScript的能力。 ------ React官网
Jsx 语法是如何在JavaSript 中生效的,jsx 是一种扩展,浏览器不会天然的支持Jsx:需要使用Babel编译
Babel 是一个工具链,主要用于将ECMAScript 2015+版本的代码转换为向后兼容的JavaScript 语法,以便能够运行在当前的旧版本的浏览器或其他环境中 --bable官网
Jsx 会被编译为React.createElement(),React.createElement()将返回一个叫作 React Element的js对象
// type 标记节点的类型 h1 div
// config 以对象形式传入,组件所有的属性都会以键值对的形式在config对象中className:'app'
// children 以对象的形式传入,它记录的是组件标签之间的嵌套的内容
export function createElement(type,config,children)
createElement 返回的是一个ReactElement 对象实例,这个对象实例本质上是一个以JavaScript形式存在的对DOM 描述的,就是虚拟DOM
ReactDOM.render()将虚拟DOM渲染处理真实DOM
开发者编写JSX代码经过Babel 编译,React.createElement 调用,React.createElement 调用得到虚拟DOM,ReactDOM.render()将虚拟DOM渲染为真实DOM
总结:
开发者==> 编写JSX代码 >经过Babel 编译=>React.createElement调用得到===>ReactElement 就是===> 虚拟DOM =>作为参数传入>ReactDOM.render()===>渲染为真是的DOM
jsx 可以提高程序员的工作效率,增加了代码的可阅读行,使程序员不必使用React提供的 createElement 开发
虚拟DOM:核心算法的基石
组件的初始化阶段:render 方法生成虚拟DOM,经过ReactDOM.render方法得到真实DOM
组件更新阶段:render方法生成新的虚拟DOM,diff算法,定位出两次虚拟DOM的差异, 实现精确更新
组件化,工程化事项在框架中的落地
封闭特点:主要针对“渲染工作流”来说的,每个组件都只处理它内部的渲染逻辑
开发特点:针对组件间通信来说的,React允许开发者基于 “单向数据流” 的原则完成组间的通信,而组件间的通信又将改变通信双方/某一方的数据, 进而对渲染结果造成影响
生命周期方法的本 质:组件的“灵魂”与躯干
将React.Component 的render方法形容为React组件的“灵魂”,从宏观上建立对React生命周期的感性认知
render方法 之外的生命周期方法可以理解为是组件的躯干
React15的生命周期流程
constructor // 构造方法,只在初始化的时候执行一次
conponentWillReceiveProps // 并不是由Props 的变化触发的,而是父组件的更新触发的,很让人费解的名字,不必纠结这个生命周期,因为已经被废除了
shouldComponentUpdata // 组件更新前触发, retrue true则渲染更新UI ,否则不更新
conponentWillMount // 组件将要挂载时触发,只在初始化的时候执行一次
componentDidUpdata // 在组件更新完之后触发
componentDidMount // 在 render 之前触发,允许做一些不涉及真实DOM 的操作,只在初始化的时候执行一次
render() 在执行过程中并不会去操作真实的DOM,它的职能是把需要渲染的内容返回出来
componentwillUnmount 组件卸载时触发
组件的更新:
React组件会根据shouldComponentUpdata来决定是否执行该方法之后的生命周期,进而决定是否对组件进行re-render(重渲染)
组件的卸载
组件在父组件中被移除了
组件中设置了key 属性,父组件在render的过程中,发现key值不一致。那么父组件就知道子组件已经发生了改变,那么react 就会直接卸载这个旧的组件,然后重新渲染新的组件
废弃了componentWillMount,新增了,不是为了替换,而是进化,更新,进步的表现,与componentDidUpdata一起,这个新的生命周期涵盖了过时componentWillReceiveProps的所有用例,官方试图代替componentWillReceiveProps
getDerivedStateFromProps有且仅有一个用途
使用props来派生/更新state
static getDerivedStateFromProps(props,state)在更新和挂载两个阶段都会触发,返回一个对象形式的返回值,必须需要return 一个对象
是一个静态方法,静态方法不依赖组件实例的存在,因此在这个方法的内部,是访问不到this
此方法不是对state 的更新动作,并非“覆盖”式的更新,而是针对某个属性的定向更新
// 比如
return {
testMessage:"这是getDerivedStateFromProps"
}
// 再子组件中的state
constroctor(){
this.state={
test:"子组件文本"
}
}
// 最的state的结果是
console.log(this.state)
{
test:"子组件文本",
testMessage:"这是getDerivedStateFromProps"
}
此方法要求返回一个对象格式的返回值。不然会被警告
这个生命周期的一个常见 bug。 setState 和 forceUpdate 也会触发这个生命周期,所以内部 state 变化后,又会走 getDerivedStateFromProps 方法,并把 state 值更新为传入的 prop。
// 错误
Class ColorPicker extends React.Component {
state = {
color: '#000000'
}
static getDerivedStateFromProps (props, state) {
if (props.color !== state.color) {
return {
color: props.color
}
}
return null
}
... // 选择颜色方法
render () {
.... // 显示颜色和选择颜色操作
}
}
// 正确
Class ColorPicker extends React.Component {
state = {
color: '#000000',
prevPropColor: ''
}
static getDerivedStateFromProps (props, state) {
if (props.color !== state.prevPropColor) {
return {
color: props.color
prevPropColor: props.color
}
}
return null
}
... // 选择颜色方法
render () {
.... // 显示颜色和选择颜色操作
}
}
为什么要用getDerivedStateFromProps 代替componentWillRecerveProps
于componentDidUpdate 一起,这个新的生命周期涵盖过时componentwillReceiveProps 的所有用例
getSnapshotBeforeUpdate(prevProps,prevState){
// ...
}
组件,从概念上类似于javaScript 函数,它接受任意入参(即props)并返回用于描述页面展示内容的React 元素 —props
当组件的state以props的形式流动时,只能流向组件树中比自己层级更低的组件
React 的数据流是单向的,父组件可以直接把this.props 传入子组件,实现父-子之间通信
父组件传递给子组件的是一个绑定了自身上下文的函数,那么子组件在调用改函数时,就可以将想要交给父组件的数据以函数入参的形式给出去
兄弟组件之间通信,是将自个把需要的传递的信息,放在共同的父组件中,这样就把兄弟组件通信转为了两个子组件和父组件之间的通信
发布订阅模式可谓是解决通信类问题的“万金油”
发布订阅模式,早期的应用应该是在浏览器中DOM事件中,监听事件的位置和触发事件的位置是不受限的,非常适合用于任意组件的通信了
// 创建一个事件监听器(这动作就是订阅,当这个DOM事件被触发时,就是发布)
target.addEventListener(type,listener,useCapture)
el.addEventLisstener('click',function,false)
发布订阅机制的优点在于,监听事件的位置和触发事件的位置是不受限制的,只要他们在同一个上下文里,就能感知
发布订阅有两个关键的动作,事件的监听(订阅)和事件的触发(发布)
class MyEventEmitter {
constructor() {
// eventMap 用来存储事件和监听函数之间的关系
this.eventMap = {}
}
// 负责事件注册
on(type, handler) {
//hanlder 必须是一个函数
if (!handler instanceof Function) {
throw new Error("handler 必须是一个函数")
}
// 判断type在对应的队列是否存在
if (!this.eventMap[type]) {
// 如果不存在,新建改队列
this.eventMap[type] = []
}
// 若存在,直接往队列里推入handler
this.eventMap[type].push(handler)
}
// 负责发布事件
emit(type, params) {
// 假设该事件是有订阅(对应的事件队列存在)
if (this.eventMap[type]) {
// 将事件队列里的handler 依次放在执行出队
this.eventMap[type].forEach((handler, index) => {
handler(params)
})
}
}
// 负责删除事件监听, >>> 运算
off(type, handler) {
if (this.eventMap[type]) {
this.eventMap[type].splice(this.eventMap[type].indexOf(handler) >>> 0, 1)
}
}
}
测试
const EventView = () => {
const myEvent = new MyEventEmitter()
const testHandler = function (params) {
console.log("test 事件触发了,testHandler 接受到的入参是", params);
}
// 监听test 事件
myEvent.on('test', testHandler)
// 触发test 事件的同时,传入希望testHandler 感知的参数
myEvent.emit('test', "helle world")
return (
<div>
手写发布订阅模式,数据传输
</div>
)
}
// print: test 事件触发了,testHandler 接受到的入参是 helle world
对于任意两个组件A和B,假如我希望实现双方之间的简单通信,借助EventEmitter 来做就很简单
在App.js中挂载
import { MyEventEmitter } from './globalEvent'
// 挂载全局发布订阅 MyEventEmitter
const myEvent = new MyEventEmitter()
window.myEvent = myEvent
两个简单的A,B 组件中使用myEvent
Context API 是React 官方提供的一种组件树全局通信的方式
在React 16.3 之前,Context API 由于存在种种局限性,并不被官方提倡开发者使用,开发者更多的是把它作为一个概念来探讨,而从V 16.30开始,React 对Context API 进行了改进新的Context API 具备更强的可用性
const AppContext = React.creactContext()
即便组件的shouldCponentUpdata 返回false, 它依然可以“穿透”组件向后代组件进行传播,进而确保了数据生产者和数据消费者之间数据的一致性,其实和Redux的一样,都是有action对象描述修改的内容 ,reducer纯函数修改真正的数据。
Redux 是JavaSCript 状态容器,它提供可预测的状态管理,Redux 和 JavaScript 的关系,证明React 可以用,Vue可以用,原生个JavaScript 也可以使用Redux.
store 是一个单一的数据源,而且是只读的
action 是对变化的描述
const action = {
type : "Add_ITEM",
payload:'- text
'
}
reducer 是一个函数,负责对变化进行分发和处理,最终把新的数据返回给Store
注意:
在Redux 的整个工作过程中,数据流是严格单向的
对一个React视图的所有数据,都来自Store,如果你想对数据进行修改,只有一种途径,派发Action,Action 会被Reducer 读取,进而对Action 的不同内容,进行修改,生成新的state,这个state 会被更新到store里,进而驱动视图层面,作出对应的改变。
Redux 通过提供一个统一的状态容器,使得数据能够有序地在任意组件之间穿梭
inport {creactStore} from "redux"
//创建store
const store = createStore(
reducer, // 必须传
initial_state,
applyMiddleware(midlleware1,middleware2)
)
// reducer
const reducer = (state,action)=>{
return new_state
}
// action
const action = {
type:"ADD_ITEM",
yload:'text '
}
// 更新规则全都写在reducer 里
const store = createStore(reducer)
// 使用dispatch 派发action ,action 会进入到reducer里,触发对应的更新
store.dispatch(action)
React-Hooks 自16.8 以来才真正被推而广之,对我们每一个老React 开发来说,它都是一个新事物,它是React 团队在这刀真枪的React 组件开发实践中逐渐认知到的一个改进点,背后涉及了对类组件和函数组件两种组件形式的思考和侧重。
React-Hooks 在首次渲染和更新阶段是不同的,
useState >resolveDispatcher 获取dispatcher>调用dispatcher.useState==>调用mountState==>返回目标数组(如[state,setState])
useState >resolveDispatcher 获取dispatcher>调用dispatcher.useState==>调用updataState==>updataReducer==>返回目标数组(如[state,useState])
这是不是意味着类组件比函数组件更好呢?答案当然是否定的。
在React-Hooks 出现之前的世界里,类组件的能力边界明显强于函数组件,但要进一步推导类组件强于函数组件,未免有些牵强。
类组件是面向对象编程思想的一种表征,有两个特点
研发模式的不断演进的背后,恰恰蕴含着前端人对“DOM操作”这一核心动作的持续思考和改进。
“DOM操作是很慢的,而JS却可以很快,直接操作DOM可能会导致频繁的回流和重绘,js不存在这些问题。因此虚拟DOM 比原生DOM 更快”?
本质是JS 和DOM 之间的一个映射缓存在形态上的表现。是一个能够描述DOM 结构及其属性信息的JS对象
React 将结合JSX的描述,构建出虚拟DOM树,然后通过ReactDOM.render实现虚拟DOM到真实DOM 的映射(触发渲染流水线)
页面的变化会优先作用于虚拟DOM,虚拟DOM 将在JS 层借助算法先对比出具体有哪些真实的DOM需要被改变,然后将这些改变作用于真实的DOM
原生JS 支配下的"人肉DOM"时期
前端页面“展示”的属性远远强于交互的属性这就导致了JS的定位只能是“辅助”
前端工程师们会花费大量的时间去实现这些静态的DOM,待一切结束后,在补充少量的JS
解放生产力的jQuery 时期
大量的DOM操作需求带来的前端开发工作量的激增,jQuery 首先解决的就是”API 不好使“这个问题
将DOM API封装为相对简单和优雅的形式,同时一口气做掉了跨浏览器的兼容工作,并且提供了链式API调用、插件扩展等一系列能力用于进步解放生产力
民只初启:早期模板引擎方案
jQuery 并不能从根本上解决DOM 操作量过大情况下前端的压力
模板引擎方案更倾向于点对点解决繁琐DOM操作的问题,它在能力和定位上既不能够、也不打算替换掉jQuery,两者是和谐共存的。因此这里不存在“模板引擎时期”。只有模板引擎方案
数据:
const staffs = [
{
name: "修言",
career: "前端"
},
{
name: "翠翠",
career: "编辑"
}
]
使用模板语法
<table>
{staffs.forEach((staffs) => {
<tr>
<td>{% staffs.name %}</td>
<td>{% staffs.career %}</td>
</tr>
})}
</table>
模板渲染流程:
使用模板引擎方案来渲染数据,关注的仅仅是数据和数据变化的本身。在思想上具有了先进性
走“数据驱动视图”这条基本道路
模板引擎的数据驱动视图方案,核心问题在于对于真实的DOM过于大刀阔斧,导致了DOM的操作范围过大,频率过高,进而导致了糟糕的性能,因为魔板引擎会把整个页面都重新渲染。那么操作虚拟DOM 不就行了吗
虚拟DOM
虚拟DOM 和Redux 一样,不依附于任何具体的框架。当进行DOM操作时,会对比前后两次虚拟DOM 树,用过diff 算法,得到补丁集(需要更新的内容),把补丁替换(patch)到真实DOM中,实现精准的的差量更新。
模板渲染:
动态的生成HTM(构建新的真实DOM) ,旧的DOM元素被新的DOM 元素替换
虚拟DOM渲染:
构建新的虚拟DOM树,通过diff 对比出新旧两颗树的差异,差量更新DOM
在数据内容变化非常大(或者说整个发生了改变),促使差量更新计算出来的结果和全量更新极为接近(或者说完全一样)。虚拟DOM的劣势主要在于JS 计算的耗时。但是DOM操作的能耗和JS计算的能耗根本不在一个量级
更新少量的数据,每次setState 的时候只修改少量的数据,模板渲染和虚拟DOM之间的DOM 操作量级的差距就完全来开了。虚拟DOM将在性能上具备绝对的优势。
虚拟DOM 的价值不在性能:
提高了开发体验,研发效率;虚拟DOM的出现为数据驱动视图这一思想提供了高度可用的载体。是前端开发能基于函数式UI编程方式,实现高效的声明式编程的基础。
跨平台:虚拟DOM是对真实渲染的一层抽象 。如果没有这一层抽象,那么视图层将会和渲染平台紧密的偶合在一起。为了描述相同描述同样的视图内容,你可能要在web 端,native 端,iOS界面,安卓界面,小程序界面。写多套完成不同的代码。
除了差量更新,“批量更新”也是虚拟DOM在性能方面所做的重要努力,批量更新在通用虚拟DOM库里是由batch 函数处理的。在差量更新非常快的情况下,用户实际上只能看到最后的一次更新。前面几次的更新
动作虽然意义不大,但是都会触发重渲染流程,带来大量不必要的高耗能操作。这个时候就要请batch帮忙了,batch的作用是缓存每次生成的补丁集,它会把收集到的多个补丁集暂存到队列中,再将最终的结果交给渲染函数,实现集中化的道理批量更新。
Virtual DOM 是一种概念。在这个概念里,UI是一种理想化的,或者说“虚拟的”表现形式被保存于内存中,并通过ReactDOM 等类库使之与“真实的”DOM同步。这一过程叫作调和。即调和就是将虚拟DOM映射到真实DOM的过程,严格来说调和过程不能和Diff 划等号。
调和是使一致的过程
Diff是找不同的过程
Core 、Renderer、Reconciler:
Reconciler(调和器)所做的工作包括组件的挂载、卸载、更新等过程,其中更新过程涉及Diff 算法的调用。如今大众的认知里,但我们讨论调和的时候,其实就是在讨论Diff,这样的认知确实有一些合理性,因为Diff确实是在调和的过程中最具代表的一环
根据Diff算法实现形式的不同,调和算法被划分为了以React15为代表的栈调和以及React16以内的Fiber 调和。
传统的计算方法是通过循环递归进行树节点的一一对比,找出两个树结构的不同。这一过程的复杂度是O(n3)。这对浏览器来说,意味着一场性能的灾难。
Diff 算法性能突破的关键点在于:分层对比
DOM 节点之间的跨层级操作并不多,同层级操作是主流
,React 的Diff 过程直接放弃了跨层级的比较,Diff过程中只针对相同层级的节点作对比。这样一来,只需要将DOM树从上到下的一次遍历,就可以完成对整棵树的对比。这是对降低复杂度量级的最重要设计
需要注意的是,栈调和虽然将传统树对比算法优化为了分层级的对比,但整个算法仍然是以递归的形式运转的,分层递归,还是递归。
如果真的发生了跨出级的操作会发生什么呢。假如B和C同级,把B移动到C下面,那么Diff算法认为是,B消失了,然后在C下面重新创建一个新的B节点。销毁到创建的代价是十分昂贵的,所以在实际开发中,避免跨层级的操作
类型一致的节点才有继续Diff 的必要性
React认为,若两个组件属于同一类型,那么它们将拥有相同的DOM树形结构。
实际上也是,在开发中很难遇到结构完全一致,而类型不一致的概率十分低。所以React 认为只有同类型的组件,才有进一步对比的必要性,入参比较的两个组件类型不一致,那么将直接放弃比较,不继续递归下去。
key 属性的设置,可以帮我们尽可能重用同一层级类的节点
key属性能够帮助维持节点的稳定性。
key 是用来帮助React 识别那些内容被更改、添加、或者移动。key需要写在用数组渲染出来的元素内部,并且需要赋予其一个稳定的值。稳定在这里很重要,因此如果key值发生了变更,React 则会触发UI的重渲染。这是一个非常有用的特性。
key试图解决的是:同一层级下,同一节点的重用问题。key 就像一个记号一样,帮助React “记住”某一个节点,从而在后续的更新中实现对这个节点的追踪。
Diff 算法的源码链路很长,但若真的把源码中的逻辑要点作提取,你消化它们可能也就不过一杯茶的功夫。
把握住 React15下的Diff 过程下,“树递归”这个特征,这就够了
如果你学有余力,可以了解一下React 16 对调和实现
当你入门React 的时候,接触的第一波API 里一定有setState ,数据驱动视图
,而当你项目的数据流乱作一团的时候,始作俑者也往往是setState,工作机制太复杂,文又不说清楚
。
setState是异步批量更新的。
reduce方法里的setState 竟然是同步更新的(这里是肯定句)。
一个setstate触发将会导致一系列的生命周期函数的发生:
setState > shouldComponentUpdata >componenWillUpdata>render>componentDidUpdata
一个完整的更新流程,涉及了包括rerender的多个生命周期函数,rerender本身会涉及对DOM 的操作,它会带来很大的性能开销。
**假如说:**一次setState就触发一个完整的更新流程,那么每一次setState 就触发一次rerender ,那么我们的视图没有触发几次就卡死了。
批量更新:
每来一个setState,就把它塞进一个队列里,等时机成熟,再把多个state 结果合并最后只针对最新的state值走一次更新的流程。
setTimeout中的setState会是同步的,并不是setTimeout改变了setState,而是setTiemout 帮助了setState 逃脱了React 对它的管控,只要是在React 管控下,setState一定是异步的。
源码中isBachingUpdatas 管控批量更新机制:
在isBachingUpdatas 的约束下,setSate 只能是异步的。isBatchingUpdates先不执行更新操作,当时事件执行完毕之后,再把锁解开。
increment = ()=>{
// 进来先锁上,不立即执行setState
inBatchingUpdates = true
this.setState({
count:this.state.count + 1
})
// 执行完函数再放开
inBatchingUpdates = false
}
就像前面所说,setTimeout方法并不是改变setState把异步变成了通过,只是setState在setTimeout 帮助下了,规避了React 的批量更新操作。当setTimeout从中作祟时,isBatchingUpdates不生效,因为isBatchingUpdate是同步执行的,setTimeout是在异步执行的,当同步代码已经执行完之后才执行异步代码,这个时候isBatchingUpdates是false了,这就isBatchingUpdates对setTimeout中执行的代码完全没有约束力。
increment = ()=>{
// 进来先锁上,不立即执行setState
isBatchingUpdates = true
setTimeout(()=>{
this.setState({
count:this.state.count + 1
})
},0)
// 执行完函数再放开
inBatchingUpdates = false
}
setState 的变现会因调用场景的不同而不同
这种差异本质上是由React 的事务机制和批量更新机制的工作方式来决定的。以上内容都是基于React15的解读,React16以来,整个React 核心算法被重写,setState也不避免的被“Fiber化”
我们认为,React 是用javaScript构建的快速响应
的大型Web 应用程式的首选方式。它在FaceBook 和Instagram上表现优秀。
然而随着事件推移和业务复杂度的提升,React曾被人们津津乐道的State Recondiler 也渐渐在体验方面显出疲态。为了进一步贯彻快速响应的原则,React团队在16.x 的版本中将其最为核心的Diff算法整个重写,使其Fiber Reconciler
的全新面貌示人。
单线程的JavaScript与多线程的浏览器。多线程的浏览器出来要处理JavaScript 线程以外,还需要处理各种各样的任务,比如事件线程,定时器,延时器网络请求的等等,其中还要处理DOM的UI 渲染线程,而JavaScript 线程是可以操作DOM的。
如果渲染线程和JavaScript 线程同时在工作,那么渲染结果必然难以预测的,比如渲染线程刚绘制还的画面,可能接下来就被JavaScript 改得面目全非。
所以JavaScript 线程和渲染线程必须是互斥:当其中一个线程在执行时,另一个线程只能挂起等待。具有相似特征的还有事件线程,浏览器的Event Loop机制,决定了事件任务是由一个事件队列来维持的。当事件被触发时,对应的任务不会立刻被执行,而是由事件线程把它添加到任务队列的末尾,等待JavaScript 的同步代码执行完毕后,在空闲的时间里执行出队。
在这种情况下,若JavaScript 线程长时间占用了主线程,那么渲染层面的更新长时间地等待,界面长时间不更新,带给用户的体验就是所谓的卡顿
。当卡顿时更多的用户会点击页面,希望浏览器给出响应,遗憾的是触发事件之后也将难以被响应,因为事件线程也在等待JavaScript的响应。
javaScript 对主线程的超时占用问题:Stack Reconciler是一个同步的递归过程,同步的递归过程意味着不到南墙不回头,一旦更新开始根本停不下来。
栈调和机制下的Diff算法,其实就是树的深度优先遍历的过程,而树的深度优先遍历总是和递归脱离不了关系。调和器会重复父组件调用子组件
的过程,直到最深的一层节点更新完毕,才慢慢向上返回。整个过程的致命性在于它是同步的,不可以被打断。
Stack Reconciler 需要的调和时间会很长,这就意味着javaScript 线程将长时间霸占主线程,进而导致我们上文中所描述的渲染卡顿/卡死、交互长时间无响应等问题。
在计算机科学中,我们有进程和线程之分,而Fiber就是比线程还要纤细的一个过程,也就是所谓的纤程
,纤程
的出现意在对渲染过程实现更加精细的控制
从架构角度来看,Fiber 是对React 核心算法的重写
从编码角度来看,Fiber 是React 内部所定义的一种数据结构
从工作流的角度来看,Fiber 节点保存了组件需要更新的状态和副作用
Fiber 架构的应用目的是实现增量渲染
,实现增量渲染的目的,是为了实现任务的可中断、可恢复,并给不同的任务赋予不同的优先级,最终达成更加顺滑的用法体验。
核心是:可中断,可恢复,于优先级 三层架构
Scheduler 调度更新的优先级
,每一个更新任务都会被赋予一个优先级,更高的优先级任务,会被更快的调度到Reconciler 层。若发现B的优先级高于当前任务A ,那么当前处于Reconciler 层的A任务就会被中断。之前被中断的A任务会被重新推入到Reconciler 层,这便是可恢复。
Reconciler 找不同
,这一层负责对比出新老DOM 之间的变化
Renderer 渲染不同
,这一层负责将变化的部分,应用道视图上
从Reconciler 到Renderer 这一过程是严格同步的。
React 16生命周期分为三个阶段
Render phase
Render 阶段纯净而没有副作用,可能会被React 暂停,终止或者重新启动。
Pre-commit phase
Pre-commit 可以读取DOM
commit phase
可以使用DOM,运行副作用,安排更新
其中Pre-commit、commit
从大阶段上来看,都是commit阶段。在Render阶段,React 主要在内存中做计算,明确DOM的更新点,commit阶段则负责把Render 阶段生成的更新真正的执行。
React15 : render 开始 >停不下来的递归计算(同步)=>commit 提交渲染
React16: render开始 ===> 工作单元|工作单元|工作单元…(异步) ===> commit 提交渲染
React16 的工作单元有着不同的优先级,React 可以根据优先级的高低去实现工作单元的打断和恢复。
由于render阶段是操作对于用户来说是不可见的,所以就算被打断和恢复,对用户来说,也是零感知。但是工作单元的重启,将会伴随着对部分生命周期的重复执行:
因此,尽量不要再willxxxx的生命周期函数中操作
可中断
,可恢复
到底是如何实现的以首次渲染为切入点,差解FIber 架构下的ReactDOM.render 所触发的渲染链路,结合源码理解整个链路中所涉及的初始化,render和commit等阶段
完成FIber 树中基本实体的创建.
render阶段的任务就是完成Fiber 树的构建,它是整个渲染链路中最核心的一环。
Fiber 架构带来的异步渲染时是React16 的亮点,可是ReactDOM.render 触发的首次渲染是一个同步的过程。
那是因为 React有几种启动模式
ReactDOM.render( ,nodeDode)
, React App 的启动模式,部分功能未开启ReactDOM.createBlockingRoot(rootNode).render( )
,作为concurrent 的一个过渡步骤ReactDOM.createRoot(rootNode).render( )
,目前在实验当中,未来稳点之后将作为React的默认开发模式。这个模式,开启了所以的新功能。在这三种模式当中,我们常用的ReactDOM .render 对应的是legacy 模式,它实际触发的仍然是同步的渲染链路。blockong模式可以理解为legecy 和concurrent模式的一种过渡形态,之所以有这个模式是因为React希望提供一个渐进的迁移策略,帮助我们更加顺滑的过渡到concurrent模式。
concurrent模式 是React 的终极目标,也是其创作团队使用Fiber 架构重写核心算法的动机所在
如果想要开启异步渲染,我们需要调用ReactDOM .createRoot方法来启动应用。
平时我们使用的模式就是ReactDOM.render模式,也是create React App 默认的模式
ReactDOM.render(
<App />,
document.getElementById('root')
);
在不同的渲染模式在挂载阶段的差异,不是工作流的差异,其工作流涉及初始化,render,commit这个步骤。本质上来说是mode,mode 属性
决定这个工作流是一气呵成(同步)的还是分片执行(异步)的
那么它还能叫Fiber架构吗
从动机上来看,Fiber架构的设计确实是为了Concurrent 而存在的,但是Fiber架构在React 中并不能够和异步渲染划上严格的等号,它是一种同时兼容了同步渲染与异步
render 阶段可以认为是整个渲染过程链路中最为核心的一环,因为我们反复强调找不同
的过程,恰恰就是在这个阶段发生的。
React15下的栈调和过程是一个递归的过程,在ReactDOM.render 触发的同步模式下它仍然是一个深度优先搜索过程,在这个过程中,biginWork(调用栈里的一个方法) 将创建新的的Fiber 节点。而completeWorkl
则负责将Fiber 节点映射为DOM 节点。
总结:掌握beginWork 的实现原理、理清Fiber 节点的创建链路,最终串联起了Fiber 树的宏观构建过程
completeWork
寻觅Fiber树和DOM树之间的关联
completeWork 的工作内容:负责处理Fiber 节点到DOM节点的映射逻辑,其内部有3个关键动作:
创建DOM 节点(CreateInstance)
将DOM节点插入到DOM 树中(AppendAllChildren)
为DOM 节点设置属性(FinalizeInitialChildren)
创建好的DOM 节点会被赋值给workInProgress 节点的StateNodes 属性
实际就是将子Fiber 节点说对应的DOM 节点,挂载到其父Fiber节点所对应的DOM 节点里去
就React 事件系统来说,它涉及的源码量不算小,相关逻辑也不够内聚,整体理解成本相对较高。幸运的是,无论是面试场景下还是在实际的开发中,React事件相关的问题,都更倾向于考验我们对事件工作流、事件特征等逻辑层面问题的理解,而非对源码细节的把握。
想要理解好React事件机制,就必须对原生DOM事件流有扎实的掌握。