1.React前言
前端UI的本质问题是如何将源于服务器端的动态数据和用户的交互行为高效的反映到复杂的用户界面上去。React另辟蹊径,通过引入虚拟DOM,状态,单项数据流等设计理念,形成以组件为核心,用组件搭建UI的开发模式。
React特点可归结于以下4点:
- 声明式的视图层。React的视图层是声明式的,基于视图状态声明视图形式,采用jsx语法来声明视图层,因此可以在视图层中随意使用各种状态数据。
- 简单的更新流程。只需要定义UI的准该,React便会负责把它渲染成最终的UI。当状态数据发生改变时,React也会根据最新的状态渲染出最新的UI。从状态到UI这一单项数据流让React组件的更新流程更加清晰整洁。
- 灵活的渲染实现。React并不是把视图直接渲染成最终的终端界面,而是先把它渲染成虚拟DOM,虚拟DOM只是普通的js对象,你可以结合其他依赖库把这个对象渲染成不同的终端上的UI。例如,使用react-dom在浏览器上渲染,使用Node在服务器端渲染,使用React-Native在手机上渲染。
- 高效的DOM操作。通过虚拟DOM,使得我们不再需要直接操作真实DOM。React通过优化diff算法,只需对比更新前后两颗虚拟DOM树,找到最小有变化的部分,将这个有变化的部分(Patch)加入队列,最终批量的更新这些Patch到真实DOM中。
2.组件state
- 组件的state代表着该组件UI呈现的完整状态集,即组件的任何UI改变都可以从state的变化反映出来
- 组件的state还必须代表着一个组件UI呈现的最小状态集,状态只用于反映组件的UI的变化,没有多余的状态,也不能有中间状态。
更新state:
- 不能直接修改state,需要通过setState()合并状态。
- state的更新时异步的。组件的state并不会立即改变,setState只是把需要修改的状态放入一个队列中,React会优化真正的执行时间,并且可能出于性能原因,可能会将多次setState的修改合并成一次修改。因此不要依赖当前的state,计算下一个state。即便是在真正执行状态修改的时候,依赖的this.state并不能保证是最新的state,因为React可能会把多次state的修改合并成一次,这时的state仍有可能是之前的state。
- state的更新是一个合并的过程,因此只需要传入发生改变的state,而不是完整的state。
- state与不可变对象,当 state中的某个状态发生变化时,应该重新创建这个状态对象,而不是直接修改原来的状态。即创建新的状态对象的关键在于,避免使用直接修改原对象的方法而是使用可以返回一个新对象的方法。
3.组件通信
组件与服务器通信:
- 组件挂载阶段通信,即在componentDIdMount 中与服务器通信,这时候组件已经挂载,真实DOM也已经完后渲染,是调用服务器api最安全的地方。
- 组件更新阶段通信,在v16前,在componentWillReceivePrpos中通信,在v16后,在getDerivedStateFromProps中,但只能是父组件所传的Props发生改变,才会调用该函数。
父子组件通信:
- 父传子,子组件用props来接收父组件的数据
- 子传父,父组件可以通过子组件的props来传递一个回调函数,子组件可以调用这个回调函数来改变父组件的数据。
兄弟组件通信:
- 兄弟组件之间不能够直接相互传送数据,需要通过状态提升到距离离他们最近的共同父组件中,再通过父组件传给兄弟组件。
跨级组件通信:
- 跨级一层层通过props传递非常繁琐且消耗性能,通过context,可数据跨级传递
- 父组件创建context,保存状态到context(通常也会保存dispath以便子组件改变状态),在hooks中,子组件可通过useConext来获取父组件所传递的状态。因此也能够使用context内的回调函数dispath来改变父组件的状态,从而影响兄弟组件,因此该通信方式非常适用于兄弟组件内通信。
4.虚拟DOM,diff算法和性能优化
在传统的前端开发中,我们通过浏览器提供的API直接对DOM执行增删改查的操作。例如通过getElementId查询一个DOM节点,然后用insertBefore在某个节点插入一个新的节点,看似只是一段js代码的执行,但实际上每一次对DOM的直接修改,都会引起浏览器对网页的重新布局和重新渲染,而这个过程是很耗时的。这也就是为什么前端性能优化有一条原则:尽量减少对DOM的操作。
虚拟DOM:
- 虚拟DOM使用js对象来描述DOM元素,它是真实DOM一层抽象。
- 当我们需要操作DOM的时候,只需要操作对应的虚拟DOM,访问js对象比访问真实DOM要快很多。
Diff算法:
React采用声明式的API描述UI结构,每次组件的状态或属性更新,组件的render方法都会返回一个新的虚拟DOM对象,用来描述新的UI结构。如果每次render都直接使用新的虚拟DOM来生成真实DOM结构,那么会带来大量对真实DOM的操作,影响执行效率。
但事实上,React只需要比较两次虚拟DOM结构的变化找出差异部分,更新到真实DOM上,从而减少最终要在真实DOM执行的操作。这个过程 就是React的调和过程,其中最关键的是比较两个树结构的Diff算法
和传统diff算法(树编辑距离)O(n^3)复杂度相比,React基于两个假设,实现了O(n)复杂度
- 如果两个元素类型的不同,那么它们将生成两颗不同的树。
- 为列表的元素设置key属性,用key标识对应的元素在多次render过程中是否发生变化
具体优化分为以下3个:
- tree diff。React只对虚拟DOM树进行分层比较,不考虑节点的跨级比较。
如果发生跨级操作,React会直接删除旧节点,而不是复用旧几点,创造新节点。因此该操作会导致大量重新创造,影响性能,因此React官方推荐尽量避免跨级操作。 - component diff。基于组件的比较,
如果是同一类型的组件,则继续按照diff策略比较虚拟DOM树;
如果不是同一类型的,该组件将会被判断为dirty component,从而替换整个组件下的所有子节点 - element diff。基于同层节点的比较,涉及创建,移动,删除操作
1.对于不使用key的情况,一旦涉及节点不一致,则会直接删除旧节点,创建新节点,使得节点不能够被复用,严重影响性能。
2.对于使用key后,React首先对新的集合进行遍历,通过唯一key来判断老的集合中是否存在相同的节点,如果没有则创建,如果有,则判断是否需要移动操作
因此element diff 就是通过唯一key来进行diff优化,用过复用已有的节点,减少节点的删除和创建操作
总是从根节点开始比较;
若根节点不是同类 React 元素,遍历将立即停止,整个树的所有节点将被卸载和销毁,生成一个新的树代替;
若节点属于同类 DOM 元素,则比较其发生变化的属性进行更新,style 中也只对变更的属性进行更新;
若节点属于同类 React 组件,则传入新的 props 并触发该组件的更新渲染逻辑;
递归入子节点;
5.React Fiber
前因: 在v16前,我们通过render()和setState()进行组件渲染和更新的时候,主要两个阶段
- 调和阶段。React会自定向下通过递归,遍历新数据生成的虚拟DOM,然后通过diff算法,找到需要变更的部分(Patch),放到更新队列里面去
- 渲染阶段。遍历更新队列,通过调用宿主环境的api,实时更新渲染对应的元素。宿主环境,比如DOM,Native,WebGL等。
在调和阶段,由于是递归,所以也被称为Stack Reconciler。但其有一个特点,一旦任务开始进行,就无法中断,那么js将一直占用主线程,一直到等到整颗虚拟DOM树计算完成后,才能把执行权交给渲染引擎,那么这将会导致一些用户交互和动画等其他任务无法立即得到处理,就会产生卡顿,影响用户体验。
因此,之前所带来的问题即js一直占用主线程,且任务无法中断,产生卡顿。
浏览器每一帧所完成的工作:
- 页面是一帧一帧绘制的,FPS到达60,页面是流畅的,小于这个值,就会觉得卡顿
- 每一帧的工作分为6个步骤:
1.处理用户的交互
2.js解析执行
3.帧开始。窗口尺寸变更,页面滚去的处理
4.rAF(requestAnimationFrame)
5.布局
6.绘制
因此任意步骤的时间加长,都有可能导致卡顿
React Fiber: 将一个耗时很长的任务分成若干个小片,每一个小片的运行时间很短,在每一个小片执行完后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。而维护每一个分片的数据结构,就是Fiber。
- 在React Fiber中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没完成,就被另一个更高优先级的更新过程打断,这时候,优先级高的更新任务会优先处理,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来。
- 因此,React Fiber 将一个更新过程分为两个阶段,Reconciliation 和Commit阶段
- 在Reconciliation 阶段,React Fiber会找出需要更新哪些DOM,这个阶段是可以被打断的
- 一旦到Commit阶段,就要一鼓作气的更新完,绝不会被打断 。
6.React Router
前端路由的诞生是为了开发单页面应用
React Router 是一种前端路由的实现方式。通过使用React Router可以让web应用根据不同的URl渲染不同的组件。React Router通过两个Router和Route两个组件完成路由功能。
- Router可以理解成路由器,一个应用只需要一个Router实例。所有路由配置组件Route都定义为Router的子组件。
- 一般使用BrowserRouter和HashRouter来包装Router。
- BrowserRouter使用HTML5的history API,Router的URL即为真实的URL,所以当请求URL时,服务器必须返回正确的页面。因此使用BrowserRouter需要服务器进行配置。
- HashRouter用锚实现,在url后添加了#。其特点是虽然出现在url中,但不会被包裹在http请求中,所以对后端没有任何影响,即便请求的url不存在,也不会找不到页面。
Router会创建一个history对象,history用来跟踪URL,当URL发生更新时,Router的后代组件会重新渲染。
路由配置:
path:每个Route都需要定义一个Path属性,path用来描述pathname或者hash,用于匹配URL
match对象:当URL与Route匹配时,Route会创建一个match对象作为Props的一个属性传递给被渲染的对象。包含4个属性
- params:参数
- isexact?:boolean = false ,当exact缺省或为false时,若当前路径是匹配模式的上级路径而不是全部路径,也会视为匹配。当exact为true时,只在路径完全匹配时视作匹配。
- path:Route的path属性,构建嵌套路由的时候会用到。
- url:URL匹配的部分。
Route有以下3种渲染方式:
- component,匹配到url所要渲染的组件
- render,返回渲染的元素
- children,返回渲染的元素,无论是否匹配成功,children返回的组件都会被渲染。但是如果不匹配,match的值为null。我们因此根绝这个特性,可以当路径不匹配的时候,不是不渲染,而是渲染一个其他的东西告知用户路径不匹配。
switch和exact
当URl有多个Route匹配时,这些Route都会进行渲染操作。
- 用switch包裹Route,使其只让第一个匹配的Route渲染。
- exact作为Route的一个属性,但他存在时,只有当URL和Route完全匹配时才会渲染,常用于home页面,也成根路径。
嵌套路由:在一个Route下再建立几个Route
link组件是Router提供的组件,点击该组件,实现页面跳转,to属性定义了要跳转的页面。
7.React 生命周期
3个阶段,mounting(挂载阶段),updating(更新阶段),unMounting(卸载阶段)。
1.mounting(挂载阶段) 组件挂载到DOM阶段
-
constructor(props: P),
第一步主要是调用super(props)保证props能够传入组件,初始化组件的state已经绑定事件处理方法等工作。 -
static getDerivedStateFromProps,
这是一个静态函数,在组件被挂载在DOM前调用,可以用于更新props来映射state的更新。 -
render() => JSX.Element
这是定义组件必须且唯一必要的方法,基于this.props和this.state返回一个React元素(以jsx描述的UI)。render不负责实际的渲染,他只是返回一个UI的描述。且render是一个纯函数,在这个方法里不能执行任何有副作用的操作,所以不能在render里调用setState,这会改变组件的状态。 -
componentDidMount() => void
在组件被挂载到DOM后调用,且只会被调用一次。一般用于需要依赖挂载的DOM元素的操作,请求数据,订阅监听。也可以在里面调用setState,回引起组件的重新渲染。而他引起的一次刷新会在用户品目更新前触发,从而避免用户看到中间状态。
2.updating(更新阶段) props或者state更新引起组件重新渲染
-
static getDerivedStateFromProps
-
shouldComponentUpdate(nextProps,nextState)
除了第一次渲染和forceUpdate()的情况下,shouldComponentUpdate将决定组件是否继续执行更新过程,当它返回true(默认返回)时,组件会继续更新过程;返回false时,组件更新停止。可通过重写该方法阻止某些变化的重渲染,额外优化性能。一般通过比较nextProps,nextState和当前的props,state比较决定是否继续更新。
但重写shouldComponentUpdate不是React所推荐的方法,未来其有可能被视为提示信息而非命令,返回的false无法拦截更新。因此可以使用pureComponent,其可对props和state进行浅层的对比,从而避免了一些重复渲染。 -
render
-
componentDidUpdate
组件更新完成后调用。常用于操作更新后的DOM及发送请求。
3.unMounting(卸载阶段)
- componentWillUnmount() => void
该方法在组件被卸载前调用,可以执行一些cleanUp的工作,例如取消定时器,清除一些手动创建的DOM。