区别:
对于事件名称命名方式,原生事件为全小写,react 事件采用小驼峰;
对于事件函数处理语法,原生事件为字符串,react 事件为函数;
react 事件不能采用 return false 的方式来阻止浏览器的默认行 为,而必须要地明确地调用 preventDefault()来阻止默认行为。
合成事件是 react 模拟原生 DOM 事件所有能力的一个事件对象,其 优点如下:
兼容所有浏览器,更好的跨平台; 将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。 方便 react 统一管理和事务机制。
事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒 泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如 果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到 document 上合成事件才会执行。
React 基于 Virtual DOM 实现了一个 SyntheticEvent 层(合成事件 层),定义的事件处理器会接收到一个合成事件对象的实例,它符合 W3C 标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制, 所有的事件都自动绑定在最外层上。
在 React 底层,主要对合成事件做了两件事:
事件委派:React 会把所有的事件绑定到结构的最外层,使用统一的 事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部 事件监听和处理函数。
自动绑定:React 组件中,每个方法的上下文都会指向该组件的实例, 即自动绑定 this 为当前组件。
这三者是目前 react 解决代码复用的主要方式:
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而 形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组 件的函数。
render props 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术,更具体的说,render prop 是一个用于 告知组件需要渲染什么内容的函数 prop。
通常,render props 和高阶组件只渲染一个子节点。让 Hook 来服 务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个 虚拟滚动条组件或许会有一个 renderltem 属性,或是一个可见的容 器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足 够了,并且能够帮助减少嵌套。
(1)HOC
官方解释∶
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而 形成的设计模式。
简言之,HOC 是一种组件的设计模式,HOC 接受一个组件和额外的参 数(如果需要),返回一个新的组件。HOC 是纯函数,没有副作用。
HOC 的优缺点∶
优点∶ 逻辑服用、不影响被包裹组件的内部逻辑。
缺点∶ hoc 传递给被包裹组件的 props 容易和被包裹后的组件重名, 进而被覆盖
(2)Render props 官方解释∶
"render prop"是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
具有 render prop 的组件接受一个返回 React 元素的函数,将 render 的渲染逻辑注入到组件内部。在这里,"render"的命名可以是任何其 他有效的标识符。
由此可以看到,render props 的优缺点也很明显∶
优点:数据共享、代码复用,将组件内的 state 作为 props 传递给调 用者,将渲染逻辑交给调用者。
缺点:无法在 return 语句外访问数据、嵌套写法不够优雅 (3)Hooks
官方解释∶
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情 况下使用 state 以及其他的 React 特性。通过自定义 hook,可以 复用代码逻辑。
以上可以看出,hook 解决了 hoc 的 prop 覆盖的问题,同时使用的方 式解决了 render props 的嵌套地狱的问题。hook 的优点如下∶
使用直观;
解决 hoc 的 prop 重名问题;
解决 render props 因共享数据 而出现嵌套地狱的问题;
能在 return 之外使用数据的问题。
需要注意的是:hook 只能在组件顶层使用,不可在分支语句中使用。 总结∶
Hoc、render props 和 hook 都是为了解决代码复用的问题,但是 hoc 和 render props 都有特定的使用场景和明显的缺点。hook 是 react16.8 更新的新的 API,让组件逻辑复用更简洁明了,同时也解 决了 hoc 和 render props 的一些缺点。
元素:一个元素 element 是一个普通对象(plain object),描述了对 于一个 DOM 节点或者其他组件 component,你想让它在屏幕上呈现成 什么样子。元素 element 可以在它的属性 props 中包含其他元素(译 注:用于形成元素树)。创建一个 React 元素 element 成本很低。元素 element 创建之后是不可变的。
组件:一个组件 component 可以通过多种方式声明。可以是带有一个 render()方法的类,简单点也可以定义为一个函数。这两种情况下, 它都把属性 props 作为输入,把返回的一棵元素树作为输出。
实例:一个实例 instance 是你在所写的组件类 component class 中 使用关键字 this 所指向的东西(译注:组件实例)。它用来存储本地状 态和响应生命周期事件很有用。
函数式组件(Functional component)根本没有实例 instance。类组 件(Class component)有实例 instance,但是永远也不需要直接创建 一个组件的实例,因为 React 帮我们做了这些。
React.createClass 和 extends Component 的 bai 区别主要在于:
(1)语法区别
createClass 本质上是一个工厂函数,extends 的方式更加接近最新 的 ES6 规范的 class 写法。两种方式在语法上的差别主要体现在方法 的定义和静态属性的声明上。
createClass 方式的方法定义使用逗号,隔开,因为 creatClass 本 质上是一个函数,传递给它的是一个 Object;而 class 的方式定义 方法时务必谨记不要使用逗号隔开,这是 ES6 class 的语法规范。
(2)propType 和 getDefaultProps
React.createClass:通过 proTypes 对象和 getDefaultProps()方法 来设置和获取 props.
React.Component:通过设置两个属性 propTypes 和 defaultProps
(3)状态的区别
React.createClass:通过 getInitialState()方法返回一个包含初 始值的对象
React.Component:通过 constructor 设置初始状态
(4)this 区别 React.createClass:会正确绑定 this
React.Component:由于使用了 ES6,这里会有些微不同,属性并不 会自动绑定到 React 类的实例上。
(5)Mixins
React.createClass:使用 React.createClass 的话,可以在创建组 件时添加一个叫做 mixins 的属性,并将可供混合的类的集合以数组 的形式赋给 mixins。
如果使用 ES6 的方式来创建组件,那么 React mixins 的特性将不 能被使用了。
组件状态的改变可以因为 props 的改变,或者直接通过 setState 方 法改变。组件获得新的状态,然后 React 决定是否应该重新渲染组件。 只要组件的 state 发生变化,React 就会对组件进行重新渲染。这是 因为 React 中的 shouldComponentUpdate 方法默认返回 true,这就 是导致每次更新都重新渲染的原因。
当 React 将要渲染组件时会执行 shouldComponentUpdate 方法来看它 是否返回 true(组件应该更新,也就是重新渲染)。所以需要重写 shouldComponentUpdate 方法让它根据情况返回 true 或者 false 来 告诉 React 什么时候重新渲染什么时候跳过重新渲染。
不可以,render 阶段 DOM 还没有生成,无法获取 DOM。DOM 的获取 需要在 pre-commit 阶段和 commit 阶段:
(1)React 中 setState 后发生了什么
在代码中调用 setState 函数之后,React 会将传入的参数对象与组 件当前的状态合并,然后触发调和过程(Reconciliation)。经过调和 过程,React 会以相对高效的方式根据新的状态构建 React 元素树 并且着手重新渲染整个 UI 界面。
在 React 得到元素树之后,React 会自动计算出新的树与老树的节 点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中, React 能够相对精确地知道哪些位置发生了改变以及应该如何改变, 这就保证了按需更新,而不是全部重新渲染。
如果在短时间内频繁 setState。React 会将 state 的改变压入栈中, 在合适的时机,批量更新 state 和视图,达到提高性能的效果
(2)setState 是同步还是异步的
假如所有 setState 是同步的,意味着每执行一次 setState 时(有可 能一个同步代码中,多次 setState),都重新 vnode diff + dom 修 改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代 码中的多个 setState 合并成一次组件更新。所以默认是异步的,但 是在一些情况下是同步的。
setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而 不同。在源码中,通过 isBatchingUpdates 来判断 setState 是先 存进 state 队列还是直接更新,如果值为 true 则执行异步操作, 为 false 则直接更新。
异步:在 React 可以控制的地方,就为 true,比如在 React 生命 周期事件和合成事件中,都会走合并操作,延迟更新的策略。
同步:在 React 无法控制的地方,比如原生事件,具体就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能 同步更新。
一般认为,做异步设计是为了性能优化、减少渲染次数:
setState设计为异步,可以显著的提升性能。如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新 渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后 进行批量更新;
如果同步更新了 state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步。state 和 props 不能保持一致性,会在开发 中产生很多的问题;
(1)props
props 是一个从外部传进组件的参数,主要作为就是从父组件向子组 件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新 的 props 来重新渲染子组件,否则子组件的 props 以及展现形式不会 改变。
(2)state
state 的主要作用是用于组件保存、控制以及修改自己的状态,它只 能在 constructor 中初始化,它算是组件的私有属性,不可通过外部 访问和修改,只能通过组件内部的 this.setState 来修改,修改 state 属性会导致组件的重新渲染。
(3)区别
props 是传递给组件的(类似于函数的形参),而 state 是在组件 内被组件自己管理的(类似于在一个函数内声明的变量)。
props 是不可修改的,所有 React 组件都必须像纯函数一样保护它 们的 props 不被更改。
state 是在组件中创建的,一般在 constructor 中初始化 state。 state 是多变的、可以修改,每次 setState 都异步更新的。
this.props 是组件之间沟通的一个接口,原则上来讲,它只能从父 组件流向子组件。React 具有浓重的函数式编程的思想。
提到函数式编程就要提一个概念:纯函数。它有几个特点: 给定相同的输入,总是返回相同的输出。 过程没有副作用。 不依赖外部状态。
this.props 就是汲取了纯函数的思想。props 的不可以变性就保证的 相同的输入,页面显示的内容是一样的,并且不会产生副作用
React 为我们提供了 PropTypes 以供验证使用。当我们向 Props 传入 的数据无效(向 Props 传入的数据类型和验证的数据类型不符)就会 在控制台发出警告信息。它可以避免随着应用越来越复杂从而出现的 问题。并且,它还可以让程序变得更易读。
当然,如果项目汇中使用了 TypeScript,那么就可以不用 PropTypes 来校验,而使用 TypeScript 定义接口来校验 props。
2. React 废弃了哪些生命周期?为什么? 被废弃的三个函数都是在 render 之前,因为 fber 的出现,很可能因 为高优先级任务的出现而打断现有任务导致它们会被执行多次。另外 的一个原因则是,React 想约束使用者,好的框架能够让人不得已写 出容易维护和扩展的代码,这一点又是从何谈起,可以从新增加以及 即将废弃的生命周期分析入手
1) componentWillMount
首先这个函数的功能完全可以使用 componentDidMount 和 constructor 来代替,异步获取的数据的情况上面已经说明了,而如果抛去异步获取数据,其余的即是初始化而已,这些功能都可以在 constructor 中执行,除此之外,如果在 willMount 中订阅事件, 但在服务端这并不会执行 willUnMount 事件,也就是说服务端会导 致内存泄漏所以 componentWilIMount 完全可以不使用,但使用者有 时候难免因为各 种各样的情况在 componentWilMount中做一些操作, 那么 React 为了约束开发者,干脆就抛掉了这个 API
2) componentWillReceiveProps
在老版本的 React 中,如果组件自身的某个 state 跟其 props 密 切相关的话,一直都没有一种很优雅的处理方式去更新 state,而是 需要在 componentWilReceiveProps 中判断前后两个 props 是否相 同,如果不同再将新的 props 更新到相应的 state 上去。这样做一 来会破坏 state 数据的单一数据源,导致组件状态变得不可预测, 另一方面也会增加组件的重绘次数。类似的业务需求也有很多,如一 个可以横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的时, 根据传入的某个值,直接定位到某个 Tab。为了解决这些问题,React 引入了第一个新的生命周期:getDerivedStateFromProps。它有以下 的优点∶
●getDSFP 是静态方法,在这里不能使用 this,也就是一个纯函数, 开发者不能写出副作用的代码
●开发者只能通过 prevState 而不是 prevProps 来做对比,保证了 state 和 props 之间的简单关系以及不需要处理第一次渲染时 prevProps 为空的情况 ●基于第一点,将状态变化(setState)和昂贵操作(tabChange) 区分开,更加便于 render 和 commit 阶段操作或者说优化。
3) componentWillUpdate
与 componentWillReceiveProps 类似,许多开发者也会在 componentWillUpdate 中根据 props 的变化去触发一些回调 。 但 不论是 componentWilReceiveProps 还 是 componentWilUpdate,都 有可能在一次更新中被调用多次,也就是说写在这里的回调函数也有 可能会被调用多次,这显然是不可取的。与 componentDidMount 类 似, componentDidUpdate 也不存在这样的问题,一次更新中 componentDidUpdate 只会被调用一次,所以将原先写在 componentWillUpdate 中 的 回 调 迁 移 至 componentDidUpdate 就可以解决这个问题。
另外一种情况则是需要获取 DOM 元素状态,但是由于在 fber 中, render 可打断,可能在 wilMount 中获取到的元素状态很可能与实际 需要的不同,这个通常可以使用第二个新增的生命函数的解决 getSnapshotBeforeUpdate(prevProps, prevState)
4) getSnapshotBeforeUpdate(prevProps, prevState) 返回的值作为 componentDidUpdate 的第三个参数。与 willMount 不 同的是,getSnapshotBeforeUpdate 会在最终确定的 render 执行之 前执行,也就是能保证其获取到的元素状态与 didUpdate 中获取到的 元素状态相同。官方参考代码:
在 getDerivedStateFromProps 中进行处理。
这个生命周期函数是为了替代 componentWillReceiveProps 存在的, 所以在需要使用 componentWillReceiveProps 时,就可以考虑使用 getDerivedStateFromProps 来进行替代。
两者的参数是不相同的,而 getDerivedStateFromProps 是一个静态 函数,也就是这个函数不能通过 this 访问到 class 的属性,也并不 推荐直接访问属性。而是应该通过参数提供的 nextProps 以及 prevState 来进行判断,根据新传入的 props 来映射到 state。
需要注意的是,如果 props 传入的内容不需要影响到你的 state,那 么就需要返回一个 null,这个返回值是必须的,所以尽量将其写到 函数的末尾:
关于 React16 开始应用的新生命周期:
可以看出,React16 自上而下地对生命周期做了另一种维度的解读:
Render 阶段:用于计算一些必要的状态信息。这个阶段可能会被 React 暂停,这一点和 React16 引入的 Fiber 架构(我们后面会重 点讲解)是有关的;
Pre-commit阶段:所谓“commit”,这里指的是“更新真正的 DOM 节 点”这个动作。所谓 Pre-commit,就是说我在这个阶段其实还并没 有去更新真实的 DOM,不过 DOM 信息已经是可以读取的了;
Commit 阶段:在这一步,React 会完成真实 DOM 的更新工作。Commit 阶段,我们可以拿到真实 DOM(包括 refs)。
与此同时,新的生命周期在流程方面,仍然遵循“挂载”、“更新”、 “卸载”这三个广义的划分方式。它们分别对应到:
挂载过程:
constructor
getDerivedStateFromProps
render
componentDidMount
更新过程:
getDerivedStateFromProps shouldComponentUpdate
render
getSnapshotBeforeUpdate componentDidUpdate
卸载过程:
componentWillUnmount
基于 hash 的路由:通过监听 hashchange 事件,感知 hash 的变化
改变 hash 可以直接通过 location.hash=xxx
基于 H5 history 路由:
改变 url 可以通过 history.pushState 和 resplaceState 等,会 将 URL 压入堆栈,同时能够应用 history.go() 等 API
监听 url 的变化可以通过自定义事件触发实现 react-router 实现的思想:
基于 history 库来实现上述不同的客户端路由实现思想,并且能够 保存历史记录等,磨平浏览器差异,上层无感知
通过维护的列表,在每次 URL 发生变化的回收,通过配置的 路由路 径,匹配到对应的 Component,并且 render
从最终渲染的 DOM 来看,这两者都是链接,都是 标签,区别是∶
是 react-router 里实现路由跳转的链接,一般配合
做了 3 件事情:
有 onclick 那就执行 onclick
click 的时候阻止 a 标签默认事件
根据跳转 href(即是 to),用 history (web 前端路由两种方式之一, history & hash)跳转,此时只是链接变了,并没有刷新页面而
标签就是普通的超链接了,用于从当前页面跳转到 href 指向的另一 个页面(非锚点情况)。
a 标签默认事件禁掉之后做了什么才实现了跳转?
React 是视图层框架。Redux 是一个用来管理数据状态和 UI 状态的 JavaScript 应用工具。随着 JavaScript 单页应用(SPA)开发日趋 复杂, JavaScript 需要管理比任何时候都要多的 state(状态), Redux 就是降低管理难度的。(Redux 支持 React、Angular、jQuery 甚至纯 JavaScript)。
在 React 中,UI 以组件的形式来搭建,组件之间可以嵌套组合。但 React 中组件间通信的数据流是单向的,顶层组件可以通过 props 属性向下层组件传递数据,而下层组件不能向上层组件传递数据,兄 弟组件之间同样不能。这样简单的单向数据流支撑起了 React 中的 数据可控性。
当项目越来越大的时候,管理数据的事件或回调函数将越来越多,也 将越来越不好管理。管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就 可能引起对应 model 以及另一个 model 的变化,依次地,可能会引 起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在 什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错 综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、 服务端渲染、路由跳转前请求数据等。state 的管理在大项目中相当 复杂。
Redux 提供了一个叫 store 的统一仓储库,组件通过 dispatch 将 state 直接传入 store,不用通过其他的组件。并且组件通过 subscribe 从 store 获取到 state 的改变。使用了 Redux,所有的 组件都可以从 store 中获取到所需的 state,他们也能从 store 获 取到 state 的改变。这比组件之间互相传递数据清晰明朗的多。
主要解决的问题:
单纯的 Redux 只是一个状态机,是没有 UI 呈现的,react- redux 作 用是将 Redux 的状态机和 React 的 UI 呈现绑定在一起,当你 dispatch action 改变 state 的时候,会自动更新页面。
两者都是存储数据以供后期使用。但是 Redux 状态更改可回溯—— Time travel,数据多了的时候可以很清晰的知道改动在哪里发生, 完整的提供了一套状态管理模式。
随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比 任何时候都要多的 state (状态)。 这些 state 可能包括服务器 响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器 等等。
管理不断变化的 state 非常困难。如果一个 model 的变化会引起另 一个 model 变化,那么当 view 变化时,就可能引起对应 model 以 及另一个 model 的变化,依次地,可能会引起另一个 view 的变化. 直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因, 如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题 或者添加新功能就会变得举步维艰。
如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调 优、服务端渲染、路由跳转前请求数据等等。前端开发者正在经受前 所未有的复杂性,难道就这么放弃了吗?当然不是。
这里的复杂性很大程度上来自于:我们总是将两个难以理清的概念混 淆在一起:变化和异步。 可以称它们为曼妥思和可乐。如果把二者 分开,能做的很好,但混到一起,就变得一团糟。一些库如 React 视 图在视图层禁止异步和直接操作 DOM 来解决这个问题。美中不足的 是,React 依旧把处理 state 中数据的问题留给了你。Redux 就是 为了帮你解决这个问题。
(1)Redux 和 Vuex 区别
Vuex 改进了 Redux 中的 Action 和 Reducer 函数,以 mutations 变化 函数取代 Reducer,无需 switch,只需在对应的 mutation 函数里改 变 state 值即可
Vuex 由于 Vue 自动重新渲染的特性,无需订阅重新渲染函数,只要 生成新的 State 即可
Vuex 数据流的顺序是∶View 调用 store.commit 提交对应的请求到 Store 中对应的 mutation 函数->store 改变(vue 检测到数据变化自 动渲染)
通俗点理解就是,vuex 弱化 dispatch,通过 commit 进行 store 状 态的一次更变;取消了 action 概念,不必传入特定的 action 形式
进行指定变更;弱化 reducer,基于 commit 参数直接对数据进行转 变,使得框架更加简易;
(2)共同思想
单—的数据源
变化可以预测
本质上∶ redux 与 vuex 都是对 mvvm 思想的服务,将数据从视图中 抽离的一种方案。
redux 中间件本质就是一个函数柯里化。redux applyMiddleware Api 源码中每个 middleware 接受 2 个参数, Store 的 getState 函数和 dispatch 函数,分别获得 store 和 action,最终返回一个函数。该 函数会被传入 next 的下一个 middleware 的 dispatch 方法,并返 回一个接收 action 的新函数,这个函数可以直接调用 next (action),或者在其他需要的时刻调用,甚至根本不去调用它。调 用链中最后一个 middleware 会接受真实的 store 的 dispatch 方 法作为 next 参数,并借此结束调用链。所以,middleware 的函数 签名是({ getState,dispatch })=> next => action。
React Hooks 主要解决了以下问题:
1)在组件之间复用状态逻辑很难
React 没有提供将可复用性行为“附加”到组件的途径(例如,把组 件连接到 store)解决此类问题可以使用 render props 和 高阶组件。但是这类方案需要重新组织组件结构,这可能会很麻烦,并且会 使代码难以理解。由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。尽管可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状 态逻辑提供更好的原生途径。
可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试 并复用。Hook 使我们在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。
(2)复杂组件变得难以理解
在组件中,每个生命周期常常包含一些不相关的逻辑。例如,组件常 常在 componentDidMount 和 componentDidUpdate 中获取数据。但 是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如 设置事件监听,而之后需在 componentWillUnmount 中清除。相互关 联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同 一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处 不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与 状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概 念,需要你在不同的文件之间来回切换,使得复用变得更加困难。
为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函 数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你 还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
(3)难以理解的 class
除了代码复用和代码管理会遇到困难外,class 是学习 React 的一 大屏障。我们必须去理解 JavaScript 中 this 的工作方式,这与其 他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法 提案,这些代码非常冗余。大家可以很好地理解 props,state 和自 顶向下的数据流,但对 class 却一筹莫展。即便在有经验的 React 开发者之间,对于函数组件与 class 组件的差异也存在分歧,甚至 还要区分两种组件的使用场景。
为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。从概念上讲,React 组件一直更像是函数。而 Hook 则 拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题 的解决方案,无需学习复杂的函数式或响应式编程技术
React Hooks 的限制主要有两条:
不要在循环、条件或嵌套函数中调用 Hook;
在 React 的函数组件中调用 Hook。
那为什么会有这样的限制呢?Hooks 的设计初衷是为了改进 React 组件的开发模式。在旧有的开发模式下遇到了三个问题。
组件之间难以复用状态逻辑。过去常见的解决方案是高阶组件、 render props 及状态管理框架。
复杂的组件变得难以理解。生命周期函数与业务逻辑耦合太深,导致 关联部分难以拆分。
人和机器都很容易混淆类。常见的有 this 的问题,但在 React 团 队中还有类难以优化的问题,希望在编译优化层面做出一些改进。这三个问题在一定程度上阻碍了 React 的后续发展,所以为了解决这 三个问题,Hooks 基于函数组件开始设计。然而第三个问题决定了 Hooks 只支持函数组件。
那为什么不要在循环、条件或嵌套函数中调用 Hook 呢?因为 Hooks 的设计是基于数组实现。在调用时按顺序加入数组中,如果使用循环、 条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。当 然,实质上 React 的源码里不是数组,是链表。
这些限制会在编码上造成一定程度的心智负担,新手可能会写错,为 了避免这样的情况,可以引入 ESLint 的 Hooks 检查插件进行预防。
实际上,diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树 更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,将 更新补丁作用于真实 DOM,以最小成本完成视图更新
具体的流程如下:
真实的 DOM 首先会映射为虚拟 DOM;
当虚拟 DOM 发生变化后,就会根据差距计算生成 patch,这个 patch 是一个结构化的数据,内容包含了增加、更新、移除等;
根据 patch 去更新真实的 DOM,反馈到用户的界面上。
当把 visbile 的值变为 false 时,就会替换 class 属性为 hidden, 并重写内部的 innerText 为 hidden。这样一个生成补丁、更新差异 的过程统称为 diff 算法。
diff 算法可以总结为三个策略,分别从树、组件及元素三个层面进 行复杂度的优化:
策略一:忽略节点跨层级操作场景,提升比对效率。(基于树进行对 比)
这一策略需要进行树比对,即对树进行分层比较。树比对的处理手法 是非常“暴力”的,即两棵树只对同一层次的节点进行比较,如果发 现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用 于进一步的比较,这就提升了比对效率。
策略二:如果组件的 class 一致,则默认为相似的树结构,否则默 认为不同的树结构。(基于组件进行对比)
在组件比对的过程中: 如果组件是同一类型则进行树比对; 如果不是则直接放入补丁中。
只要父组件类型不同,就会被重新渲染。这也就是为什么 shouldComponentUpdate、PureComponent 及 React.memo 可以提高 性能的原因。
策略三:同一层级的子节点,可以通过标记 key 的方式进行列表对 比。(基于节点进行对比)
元素比对主要发生在同层级中,通过标记节点操作生成补丁。节点操 作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移 动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要 的作用。通过标记 key 的方式,React 可以直接移动 DOM 节点,降 低内耗。
Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除 的辅助标识。在开发过程中,我们需要保证某个元素的 key 在其同 级元素中具有唯一性。
在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素 是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染 此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系。
注意事项:
key 值一定要和具体的元素—一对应;
尽量不要用数组的 index 去作为 key;
不要在 render 的时候用随机数或者其他操作给元素加上不稳定的 key,这样造成的性能开销比不加 key 的情况下更糟糕。
diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后, 更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁。
React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算 法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方 式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组 件及元素,以此提升效率。
树比对:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树 只对同一层次的节点进行比较。
组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接 放入到补丁中。
元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点 操作对应真实的 DOM 剪裁操作。
以上是经典的 React diff 算法内容。自 React 16 起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode 与 FiberTree 进行重构。fiberNode 使用了双链表的结 构,可以直接找到兄弟节点与子节点。整个更新过程由 current 与 workInProgress 两株树双缓冲完成。workInProgress 更新完成后, 再通过修改 current 相关指针指向新节点。
Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力,但 这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因 为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎 都可以使用防抖和节流去提高响应性能。
React 16.x 的三大新特性 Time Slicing、Suspense、 hooks
Time Slicing(解决 CPU 速度问题)使得在执行任务的期间可以随时 暂停,跑去干别的事情,这个特性使得 react 能在性能极其差的机器 跑时,仍然保持有良好的性能
Suspense (解决网络 IO 问题)和 lazy 配合,实现异步加载组件。 能 暂停当前组件的渲染, 当完成某件事以后再继续渲染,解决从 react 出生到现在都存在的「异步副作用」的问题,而且解决得非的优雅, 使用的是 T 异步但是同步的写法,这是最好的解决异步问题的方式
提供了一个内置函数 componentDidCatch,当有错误发生时,可以友 好地展示 fallback 组件; 可以捕捉到它的子元素(包括嵌套子元素) 抛出的异常; 可以复用错误组件。
(1)React16.8
加入 hooks,让 React 函数式组件更加灵活,hooks 之前,React 存 在很多问题:
在组件间复用状态逻辑很难 复杂组件变得难以理解,高阶组件和函数组件的嵌套过深。
class 组件的 this 指向问题
难以记忆的生命周期
hooks 很好的解决了上述问题,hooks 提供了很多方法
useState 返回有状态值,以及更新这个状态值的函数
useEffect 接受包含命令式,可能有副作用代码的函数。
useContext 接受上下文对象(从 React.createContext 返回的值) 并返回当前上下文值,
useReducer useState 的替代方案。接受类型为 (state,action) => newState 的 reducer,并返回与 dispatch 方法配对的当前状态。
useCalLback 返回一个回忆的 memoized 版本,该版本仅在其中一个 输入发生更改时才会更改。纯函数的输入输出确定性 o useMemo 纯 的一个记忆函数 o useRef 返回一个可变的 ref 对象,其 Current 属 性被初始化为传递的参数,返回的 ref 对象在组件的整个生命周期 内保持不变。
useImperativeMethods 自定义使用 ref 时公开给父组件的实例值
useMutationEffect 更新兄弟组件之前,它在 React 执行其 DOM 改变 的同一阶段同步触发
useLayoutEffect DOM 改变后同步触发。使用它来从 DOM 读取布局并 同步重新渲染
(2)React16.9
重命名 Unsafe 的生命周期方法。新的 UNSAFE_前缀将有助于在代码 review 和 debug 期间,使这些有问题的字样更突出废弃
javascrip:形式的 URL。以 javascript:开头的 URL 非常容易遭受攻 击,造成安全漏洞。
废弃"Factory"组件。 工厂组件会导致 React 变大且变慢。 act()也支持异步函数,并且你可以在调用它时使用 await。
使用
(3)React16.13.0
支持在渲染期间调用 setState,但仅适用于同一组件
可检测冲突的样式规则并记录警告
废弃 unstable_createPortal,使用 CreatePortal
将组件堆栈添加到其开发警告中,使开发人员能够隔离 bug 并调试其 程序,这可以清楚地说明问题所在,并更快地定位和修复错误。
这个问题就设计到了数据持久化,主要的实现方式有以下几种
Redux:将页面的数据存储在 redux 中,在重新加载页面时,获取 Redux 中的数据;
data.js:使用 webpack 构建的项目,可以建一个文件,data.js,将 数据保存 data.js 中,跳转页面后获取;
sessionStorge:在进入选择地址页面之前,componentWillUnMount 的时候,将数据存储到 sessionStorage 中,每次进入页面判断 sessionStorage 中有没有存储的那个值,有,则读取渲染数据;没 有,则说明数据是初始化的状态。返回或进入除了选择地址以外的页 面,清掉存储的 sessionStorage,保证下次进入是初始化的数据
history API:History API 的 pushState 函数可以给历史记录关联 一个任意的可序列化 state,所以可以在路由 push 的时候将当前页 面的一些信息存到 state 中,下次返回到这个页面的时候就能从 state 里面取出离开前的数据重新渲染。react-router 直接可以支 持。这个方法适合一些需要临时存储的场景。
本质上来说 JSX 是 React.createElement(component,
props, ...children)方法的语法糖。在 React 17 之前,如果使用了 JSX,其实就是在使用React, babel 会把组件转换
为 CreateElement 形式。在React17之后,就不再需要引入,因 为 babel 已经可以帮我们自动引入react。
Redux 的中间件提供的是位于 action 被发起之后,到达 reducer 之前的扩展点,换而言之,原本 view -→> action -> reducer -> store 的数据流加上中间件后变成了 view -> action -> middleware -> reducer -> store ,在这一环节可以做一些"副作用"的操作,如 异步请求、打印日志等。
applyMiddleware 源码:
从 applyMiddleware 中可以看出∶
redux 中间件接受一个对象作为参数,对象的参数上有两个字段 dispatch 和 getState,分别代表着 Redux Store 上的两个同名函 数。
柯里化函数两端一个是 middewares,一个是 store.dispatch
父组件向子组件通讯: 父组件可以向子组件通过传 props 的方式, 向子组件进行通讯
子组件向父组件通讯: props+回调的方式,父组件向子组件传递 props 进行通讯,此 props 为作用域为父组件自身的函 数,子组件 调用该函数,将子组件想要传递的信息,作为参数,传递到父组件的 作用域中
兄弟组件通信: 找到这两个兄弟节点共同的父节点,结合上面两种方 式由父节点转发信息进行通信
跨层级通信: Context 设计目的是为了共享那些对于一个组件树而 言是“全局”的数据,例如当前认证的用户、主题或首选语言,对于 跨越多层的全局数据通过 Context 通信再适合不过
发布订阅模式: 发布者发布事件,订阅者监听事件并做出反应,我们 可以通过引入event 模块进行通信
全局状态管理工具: 借助 Redux 或者 Mobx 等全局状态管理工具进行 通信,这种工具会维护一个全局状态中心Store,并根据不同的事件产 生新的状态