React

395 阅读15分钟

React引入合成事件的目的

React上注册的事件最终会绑定在根dom容器上,而不是React组件对应的Dom
- 避免在Dom上绑定过多的事件处理函数,减少内存消耗。
- React为了避免这类Dom事件的滥用,同时屏蔽底层不同浏览器之间的事件系统差异,实现了合成事件

Fiber

- 存在问题:16版本之前,React在更新页面时,计算节点差异是对dom树进行的递归调用,执行栈会很深,而且整个过程不能中断,这期间主线程会被js运算一直占用(因为浏览器的UI渲染和js的执行被设计为互斥的),如果这个过程超过16ms,就容易出现掉帧、响应延迟、阻塞布局(layout)、动画等问题。为了解决主线程被长时间占用的问题引入了Fiber。
- fiber的思想是把渲染/更新过程拆分成一个个小任务块,通过合理的调度来控制时间。
- react把更新/渲染分为2个阶段:diff和commit
    - diff工作是对比当前实例和上一次实例状态,找出差异。diff本质上是对树进行分层遍历、比较,是可拆分的
    - commit阶段是把所有dom change应用到Dom树,是一系列的dom操作。这些操作虽然可以拆分,但是可能会导致实际的dom状态和维护的内部状态不一致,另外还会影响体验。一般场景下,dom更新的耗时比起diff算不上什么,所以拆分意义也不大。
- fiber怎么拆分任务
    - 按照虚拟dom节点拆,fiber tree是根据vDOM tree构造出来的,树结构是一样的,只是节点包含的信息有差异。
    - fiber节点结构:{stateNode,child,return(父节点),sibling}
- 如何调度任务
    - 每次处理一个任务单元,处理完看时间用完了没,没有继续下一个任务。用完了就挂起,把控制权交还给主线程等下一次requestIdleCallback回调执行。
    
    参考:http://www.ayqy.net/blog/dive-into-react-fiber/#articleHeader13
         https://juejin.cn/post/6943896410987659277

diff算法

diff算法,会对新旧两个虚拟dom树进行分层比较,只对同一层级的节点进行比较,如果发现该节点已经不存在,则删除该节点以及其所有的子节点。这样只需要对树进行一次遍历就能完成对整个dom树的比较。
- 如果类型不同,则直接删除节点,创建新的节点
- 如果类型相同,React会保留Dom节点,比较属性,只更新有改变的属性
- 当递归DOM节点的子元素时,React会同时遍历两个子元素的列表,如果有差异会生成一个mutation
- 如果子元素是数组,那么会比较key属性,如果key相同,react会认为这是相同的元素,不会重新创建,然后继续比较属性
- 构建workInProgress tree的过程就是diff的过程

Hooks原理

react中的Hooks是以链表结构存放的,按顺序取值,如果在条件语句中声明hooks,那么current树的memoizedState缓存的hooks信息,
和当前workInProgress的memoizedState就有可能不一致(currentHook和workInProgressHook不一致),如果涉及读取state等操作就会发生异常。
- fiber数据结构中的memoizedState指向当前hooks队列的首个hook
- hook数据结构中的memoizedState保存了hook信息,不同类型hook保存的内容不同(比如useStat缓存state的值)

Class 和 hooks对比

1. hooks
    - 复用代码逻辑更容易
    - 把功能相同的逻辑放到同一useEffect中,避免将同一个任务的代码拆分在不同的生命周期函数内
2. Class组件,拥有完整的生命周期,比如shouldComponentUpdate,Hooks无法做到如此精细化的控制

类组件和函数组件对比

1. 类组件你是面向对象编程,主打的是继承、生命周期等核心概念。函数组件内核是函数式编程。
2. 在hooks之前,如果需要使用生命周期,首选类组件。
3. 如果需要使用继承,首推类组件。但继承并不是最佳的设计模式,官方更推崇'组合优于继承'的设计概念
4. 性能优化上,类组件主要靠shouldComponentUpdate阻断渲染;函数组件依靠React.Memo()缓存渲染结果来提升性能
5. 不能在函数组件上使用ref属性,因为他们没有实例

React中代码复用的方法

- HOC高阶组件
- render prop,是一种在组件之间使用一个值为函数的prop共享代码的一种技术,该函数返回一个React元素,告知组件需要渲染什么内容
- 自定义Hooks
- 区别:1.HOC可以多层嵌套 2.render prop 更加灵活,子组件可以选择性的对收据进行接收,而HOC只能通过修改包装函数进行处理。3.推荐hooks写法

React性能优化

1. shouldComponentUpdate,阻断渲染来提升性能
2. PureComponent,对类组件的props和state进行浅比较
3. React.memo,对函数组件的props进行浅比较,缓存渲染结果来提升性能。(只能用于函数组件)
4. useMemo,useCallback
5. 列表使用key属性
6. 避免在didMount、didUpdated钩子函数中更新组件State(该函数的调用阶段是在更新完dom之后,绘制浏览器之前,会阻塞浏览器的渲染)
7. 组件的懒加载,通过webpack的动态导入和React.lazy方法

- 浅比较:
    1. 通过Object.is()方法比较,如果相等返回true
    2. 如果a,b中有任何一个为null或者有任何一个不为object,则返回false
    3. 如果a,b包含的键值对个数不相等,返回false
    4. 比较a,b中的键值对,如果b没有没有a的属性或者相同键对应的值不同,就返回false
    5. 以上都不满足,返回true

state和props区别

1. props从父组件接收的属性,不能改变
2. 组件内自己维护的状态,如果组件在某个时刻需要改变属性的值,这时候应该使用state

setState是同步还是异步的

只要进入了react的调度流程就是异步的,
不可控同步=>原生事件,定时器;
可控异步=>合成事件;钩子函数

触发React重新渲染的方法

1. setState方法
2. 父组件重新渲染

useEffect和useLayoutEffect

- useEffect,在绘制之后执行,不会阻塞浏览器更新屏幕,useEffect在大部分场景下比class性能要好
- useLayoutEffect,这个是用在处理dom的时候使用,当useEffect里需要对dom进行操作,并且会改变页面的样式,
就需要用这个,useLayoutEffect的回调会在更新完dom后(插入dom树)立即执行,在浏览器绘制之前完成,会阻塞浏览器的绘制.(防止页面闪烁)
- class的componentDidMount、componentDidUpdated和useLayoutEffect调用阶段是一样的,是同步的,会阻塞浏览器的绘制

useCallBack和useMemo的区别

useCallback,缓存的函数定义
    1. 父组件更新时,通过props传递给子组件的函数也会重新创建,这个时候可以使用useCallback缓存该函数,跳过组件的重新渲染
    2. 函数作为作为某些hook的依赖时,比如另一个包裹在useCallback中的函数依赖它
useMemo,1. 缓存消耗比较大的计算结果 2.跳过组件的重新渲染
react使用Object.is比较每一个依赖和它之前的值
    -Object.is和三等号的区别:
        三等号中,-0和+0是相等的,NaN和NaN不等
        Object.is中-0和+0是不等的,NaN和NaN是相等的
        双等号在比较的时候会进行类型转换

useRef和全局变量的区别

ref一般用来保存一些不用于渲染的信息,比如dom节点,定时器的id
useRef是定义在实例的基础上的,组件被多次使用时,每个ref属于不同的实例;而定义在组件前的全局变量,是被各实例共用的,会相互影响
- 访问dom节点
- 管理一个可变对象,这个对象不会在每次渲染时改变
- 保存定时器的返回值

Context

提供了一种在组件之间传递数据的方法,不需要逐层传递props
谨慎使用,会使得组件的复用性变差

class生命周期

挂载阶段
    1. constructor:一般做两件事,初始化state,给事件方法绑定this
    2. getDerivedStateFromProps
        是一个静态方法,不能在里面使用this,接受两个参数props(新的props)和state(组件当前的state),
        该函数返回一个对象来更新当前state,不需要更新返回null。该钩子函数存在的目的只有一个,让组件在props变化时更新state。
        当接受到新的属性想修改state可以使用
    3. render:根据props和state返回需要渲染的内容
    4. componentDidMount:在组件挂载后,插入dom树后立刻调用,会阻塞浏览器绘制
    5. 
更新阶段
    1. getDerivedStateFromProps
    2. shouldComponentUpdate(nextProps, nextState),默认返回true
    3. render
    4. getSnapshotBeforeUpdate(preProps, preState), render之后,componentDidUpdate之前,
    必须跟componentDidUpdate配合使用,该函数返回值作为componentDidUpdate第三个参数传入,
    一般用来在组件发生更改之前从dom中获取一些信息,比例滚动位置。
    5. componentDidUpdate(preProps, preState, snapshot)
卸载阶段
    1. componentWillUnmount,在组件卸载及销毁之前调用
        - 清除timer
        - 取消在componentDidMount中的订阅
        
//参考:新旧生命周期的理解:https://jacky-summer.github.io/2020/11/30/%E8%B0%88%E8%B0%88%E5%AF%B9-React-%E6%96%B0%E6%97%A7%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E7%9A%84%E7%90%86%E8%A7%A3/

组件通信

- 父组件向子组件通信
    1. 传递props
- 子组件向父组件通信
    1. 传递值为函数的props给子组件,子组件调用该函数
    2. 通过ref获取子组件的实例
- 兄弟组件之间通信
    1. 找到这两个兄弟节点的父节点,状态提升,再用上面的方法通信
- 跨层级通信
    1. 使用Context共享数据
- 发布订阅模式
- 使用状态管理工具,如redux、 mobx

React.lazy实现原理

结合React.lazyimport()方法实现React的懒加载
webpack解析到import方法时候,会对组件进行拆分打包成一个单独的包(bundle),在该组件渲染时,才会被下载到本地。

Redux原理

- 基于发布订阅的状态管理
1. 将应用状态统一放到state中,由store来管理
2. 每次状态的改变都是通过dispatch传递一个action
3. reducer函数接收这个action返回一个新的state去更新store中的state
4. subscribe是store订阅监听函数,每次dispatch后依次执行这些监听函数
5. 可以添加中间件对dispatch函数进行重写
function createStore(reducer){
    let state
    const listeners = []
    function subscribe(callback) {
        listeners.push(callback)
    }
    function getState() {
        return state
    }
    function dispatch (action) {
        state = reducer(state, action)
        listeners.forEach(cal => cal())
    }
    let store = {
        subscribe,
        dispatch,
        getState,
    }
    return store
}

参考:https://cloud.tencent.com/developer/article/1591283

mobx原理

- mobx,是一个响应式编程库,基本原理和vue一致
1. 页面事件(生命周期,点击事件等)触发action执行
2. 通过action来修改状态
3. 状态更新后,computed计算属性也会根据依赖状态重新计算属性值
4. 状态更新后会触发reaction,来进行一些渲染操作
mobx-react:
1. observer方法,该方法重新了react的render函数,当监听到render中依赖属性变化的时候就会重新渲染组件。

react-router

react-router在history库的基础上,实现了url与UI(component)的同步
- HashRouter,通过监听hashchange事件,感知hash变化,保持UI和url的同步
- BrowserRouter,使用html5提供的history API(pushState,replaceState和popstate事件)保持UI和url的同步
- 通过维护的列表,在每次url发生变化的时候,通过配置的路由路径,匹配到对应的component,并且render
优缺点:
- hash的兼容性较好,所以早期大量采用。缺点是:1.搜索引擎对带有hash的页面不友好,2.带有hash的页面难以追踪用户的行为??
- history对搜索引擎友好,方便统计用户行为。缺点是:1.兼容性不如hash,2.需要后端做相应配置,否则直接访问子页面会出现404错误
以上两种模式都是改变url
- Switch,通常用来包裹Route,用于渲染与路径匹配的第一个子Route
参考:https://segmentfault.com/a/1190000016435538

MVC和MVVC

1. MVC模式的意思是软件可以分为三个部分:
- 视图(View): 用户界面
- 控制器(controller): 业务逻辑
- 模型(model): 数据保存
各部分之间的通信是单向的,View传递指令给Controller,Controller完成业务逻辑,要求Model改变状态,Model将新的数据发送到View,用户得到反馈。
2. MVVM
- View代表视图层
- Model代表数据层
- ViewModel是一个同步View和Model的对象,通过数据双向绑定,将View和Model层连接起来,View和Model之间的同步工作完全是自动的,开发者只需要关注业务逻辑,不需要关注数据状态同步的问题。

react和vue的对比

共同的概念:
1.虚拟dom,两者都有虚拟dom,都只会更新那些已经修改的对象,以节省操作dom的时间和资源。
2.基于组件的开发模式,两者都有大量的组件库,提高代码的复用率,提升开发效率
3.专注于视图,两者都之关系渲染视图,像路由、状态管理都由其他库去完成
4.都依赖JavaScript
两者差异:
    - 一个是语法:
         vue保留了html、css、js分离的写法。
         react使用jsx(jsx是一套类似xml的语法,最终会被编译成js)
    - vue是实现了数据双向绑定的mvvm框架,它提供了v-model这样的指令来实现文本框的数据流的双向绑定。
    - react主张函数式编程,数据不可变,单向数据流。当然需要双向绑定的地方可以借助onChange和setState实现。
性能相关:
vue会跟踪每一个组件的依赖关系,当数据变动时,vue的每个组件都精确的知道是否需要重新绘制,不需要手动优化。
而react,在应用状态改变时,组件默认都会重新渲染,所以react中需要使用shouldComponentUpdate来控制组件是否需要重新渲染。
但是当页面中数据特别多的时候,vue中watcher也会特别多,有可能造成页面卡顿,所以一般数据比较多的大型项目会倾向使用react。

框架选型

1.框架手否能满足大部分的应用需求
2.框架是否有丰富的组件库
3.框架的社区支持怎么样,遇到问题的能否快速方便的找到人解答
4.团队成员对各框架的接受程度,驾驭能力

虚拟dom

用js模拟一颗虚拟dom树
优点:
    1.保证性能下限(虚拟dom会检查哪些节点需要更新,尽量复用已有的dom,减少dom的删除和重新创建,
    和原生dom操作比起来,js计算的消耗极少的)
    2.无需手动操作dom,开发者不需要考虑对dom的操作,只需要关注业务层数据的改变
    3.跨平台,虚拟dom本质上是JavaScript对象,可以方便的进行跨平台操作
目的:提升开发效率,包装性能下限
缺点:
    1.无法进行极致的优化
虚拟dom一定性能更好??
    首次渲染和需要对所有节点进行更新时,这时候采用虚拟dom会比直接操作原生dom多一步构建虚拟dom的过程。
    事实上,如果对原生dom操作得当的话,原生dom的性能一定优于虚拟dom。因为框架的虚拟dom要应对任何上层api可能产生的操作,它的实现必须是普适的。

组件封装

1. 确定组件的功能,遵循单一功能的原则
2. 设计组件的接口,props、事件,定义组件接受的属性,还有需要与父组件交互的事件
3. 通过props传递配置项,例如祖逖、样式或者不同的行为交互
3. 编写组件逻辑
4. 样式封装,使用css Modules来封装组件,确保样式的作用限定在组件内部

代码重构

1. 保持结构化的导入顺序,对三方导入和本地导入分组


设计模式

1. 模块模式,是一种创建封装好的对象的方法,可以防止代码全局污染,并可以创建私有变量。前端经常用这种方式组织代码
2. 观察者模式,定义了对象之间的一对多依赖关系,当一个对象的状态变化时,所有依赖它的对象都会得到通知并更新。这个模式常应用于实现事件监听和状态管理,例如vue和react框架,数据和视图绑定机制的本质就是观察者模式的应用。
3. 单例模式,确保一个类只有一个实例,并提供一个全局的访问点,这种模式适合知需要一个实例的场景,比如,全局状态管理或者全局事件总线
    - 创建一个全局的状态管理器,所有的组件都通过这个单例来访问和修改全局状态
    - 实现一个事件总线,用于非亲属组件之间的通信,确保只有一个事件监听和分发的中心
4. 装饰器模式,