react在事件处理上具有如下特点:
1.几乎所有的事件代理(delegate)到document,达到性能优化的目的
2.对于每种类型的事件,拥有统一的分发函数dispatchEvent
3.事件对象(event)是合成对象(SyntheticEvent),不是原生的
合成事件与原生事件混用时,需要注意。例如存在如下的业务场景: 点击input框展示日历,点击文档其他部分,日历消失,代码如下:
var React = require('react');
var ReactDOM = require('react-dom');
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
showCalender: false
};
}
componentDidMount() {
document.addEventListener('click', () => {
this.setState({
showCalender: false});
console.log('it is document')
}, false);
}
render() {
return (<div>
<input
type="text"
onClick={
(e) => {
this.setState({
showCalender: true});
console.log('it is button')
e.stopPropagation();
}}
/>
<Calendar isShow={
this.state.showCalender}></Calendar>
</div>);
}
}
上述代码: 在点击input的时候,state状态变成true,展示日历,同时阻止冒泡,但是document上的click事件仍然触发了?到底是什么原因造成的呢?
原因解读: 因为react的事件基本都是委托到document上的,并没有真正绑定到input元素上,所以在react中执行stopPropagation并没有什么用处,document上的事件依然会触发。
解决办法1:input的onClick事件也使用原生事件
解决办法2:在document中进行判断,排除目标元素
虚拟dom需要解决的事情:
1、将页面改变的内容应用到虚拟 DOM 上,而不是直接应用到 DOM 上
2、变化被应用到虚拟 DOM 上时,虚拟 DOM 并不急着去渲染页面,而仅仅是调整虚拟 DOM 的内部状态,这样操作虚拟 DOM 的代价就变得非常轻了
3、在虚拟 DOM 收集到足够的改变时,再把这些变化一次性应用到真实的 DOM 上
抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种 GUI
在传统的diff算法下,对比前后两个节点,如果发现节点改变了,会继续去比较节点的子节点,一层一层去对比。就这样循环递归去进行对比,复杂度就达到了o(n3),n是树的节点数
react diff:
React 通过制定大胆的策略,将 O(n^3) 复杂度的问题转换成 O(n) 复杂度的问题。
react diff 策略:
策略一(tree diff):Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计 (DOM结构发生改变-----直接卸载并重新creat)。
策略二(component diff):拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
策略三(element diff):对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
React 分别对 tree diff、component diff 以及 element diff 进行算法优化
1、tree diff:
对树进行分层比较,两棵树只会对同一层次的节点进行比较。
React 只会对同一层的节点作比较,不会跨层级比较
如果出现了 DOM 节点跨层级的移动操作,React diff 会有怎样的表现呢?
A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。
当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A
注意:在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点
2、component diff:
如果是同类型,按照原来策略 继续比较 virtual DOM tree;
如果不是一个类型,替换整个组件下的所有子节点
对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
3、element diff:
React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)
全新的节点,老的集合里面没有 ,就重新创建 插入;
可以复用之前的,做了移动;
老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。
按照diff 规则,B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。这样比较繁琐,因为都是相同的节点,移动就可以减少操作,所以提出了优化的方法:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,虽然只是小小的改动,性能上却发生了翻天覆地的变化
主要分析新老集合中存在相同节点但位置不同时,对节点进行位置移动的
总结
React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题
React 通过分层求异的策略,对 tree diff 进行算法优化
React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化
React 通过设置唯一 key的策略,对 element diff 进行算法优化
建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升
我们都知道 react 的核心的思想:
内存维护虚拟dom (js 对象),数据变化时(setState),自动更新虚拟 DOM,得到一颗新树,然后 Diff 新老虚拟 DOM 树,找到有变化的部分,得到一个 Change(Patch),将这个 Patch 加入队列,最终批量更新这些 Patch 到 DOM 中
React 会自顶向下通过递归,遍历新数据生成新的 Virtual DOM,然后通过 Diff 算法,找到需要变更的元素(Patch),放到更新队列里面去。
在协调阶段,采用递归的遍历方法 ,称为Stack Reconciler 这种方式:
一旦任务开始进行,就无法中断,那么 js 将一直占用主线程, 一直要等到整棵 Virtual DOM 树计算完成之后,才能把执行权交给渲染引擎,那么这就会导致一些用户交互、动画等任务无法立即得到处理,就会有卡顿,非常的影响用户体验
解决方法:
把渲染更新过程拆分成多个子任务,每次只做一小部分,做完看是否还有剩余时间,如果有继续下一个任务;如果没有,挂起当前任务,将时间控制权交给主线程,等主线程不忙的时候在继续执行。
合作式调度主要就是用来分配任务的,当有更新任务来的时候,不会立马diff,而是把更新推入 Update Queue 中,然后交给 Scheduler 去处理。
两个执行帧之间,主线程通常会有一小段空闲时间,requestIdleCallback可以在这个空闲期(Idle Period)调用空闲期回调(Idle Callback),执行一些任务。
requestIdleCallback方法提供 deadline,即任务执行限制时间,以切分任务,避免长时间执行,阻塞UI渲染而导致掉帧;
1、如何拆分子任务?
2、有剩余时间怎么去调度应该执行哪一个任务?
3、没有剩余时间之前的任务怎么办?
对于上面的问题 ,react 通过 Fiber 来解决
fiber 代表 一种工作单元,需要重新实现一个堆栈帧的调度,可以按照自己的调度算法执行他们,另外这些调度可以自己控制,所以本质上Fiber 也可以理解为一个虚拟的堆栈帧。 Fiber 是一种数据结构(堆栈帧),也可以说是一种解决可中断的调用任务的一种解决方案,它的特性就是时间分片(time slicing)和暂停(supense)。
http://www.ayqy.net/blog/dive-into-react-fiber/