一、是什么
Real DOM,真实 DOM
,意思为文档对象模型,是一个结构化文本的抽象,在页面渲染出的每一个结点都是一个真实 DOM
结构,如下:
Virtual Dom,本质上是以 JavaScript 对象形式存在的对 DOM 的描述
创建虚拟 DOM 目的就是为了更好将虚拟的节点渲染到页面视图中,虚拟 DOM 对象的节点与真实 DOM 的属性一一照应
在 React 中,JSX 是其一大特性,可以让你在 JS 中通过使用 XML 的方式去直接声明界面的 DOM 结构
// 创建 h1 标签,右边千万不能加引号 const vDom = <h1>Hello World</h1>; // 找到 <div id="root"></div> 节点 const root = document.getElementById("root"); // 把创建的 h1 标签渲染到 root 节点上 ReactDOM.render(vDom, root);
上述中,ReactDOM.render()
用于将你创建好的虚拟 DOM
节点插入到某个真实节点上,并渲染到页面上
JSX
实际是一种语法糖,在使用过程中会被 babel
进行编译转化成 JS
代码,上述 VDOM
转化为如下:
const vDom = React.createElement( 'h1', { className: 'hClass', id: 'hId' }, 'hello world' )
可以看到,JSX 就是为了简化直接调用 React.createElement() 方法:
- 第一个参数是标签名,例如 h1、span、table…
- 第二个参数是个对象,里面存着标签的一些属性,例如 id、class 等
- 第三个参数是节点中的文本
通过 console.log(VDOM),则能够得到虚拟 VDOM 消息
所以可以得到,JSX
通过 babel
的方式转化成 React.createElement
执行,返回值是一个对象,也就是虚拟 DOM
二、区别
两者的区别如下:
- 虚拟 DOM 不会进行排版与重绘操作,而真实 DOM 会频繁重排与重绘
- 虚拟 DOM 的总损耗是“虚拟 DOM 增删改+真实 DOM 差异增删改+排版与重绘”,真实 DOM 的总损耗是“真实 DOM 完全增删改+排版与重绘”
拿以前文章 (opens new window)举过的例子:
传统的原生 api
或 jQuery
去操作 DOM
时,浏览器会从构建 DOM
树开始从头到尾执行一遍流程
当你在一次操作时,需要更新 10 个 DOM
节点,浏览器没这么智能,收到第一个更新 DOM
请求后,并不知道后续还有 9 次更新操作,因此会马上执行流程,最终执行 10 次流程
而通过 VNode
,同样更新 10 个 DOM
节点,虚拟 DOM
不会立即操作 DOM
,而是将这 10 次更新的 diff
内容保存到本地的一个 js
对象中,最终将这个 js
对象一次性 attach
到 DOM
树上,避免大量的无谓计算
三、优缺点
真实 DOM
的优势:
- 易用
缺点:
- 效率低,解析速度慢,内存占用量过高
- 性能差:频繁操作真实 DOM,易于导致重绘与回流
使用虚拟 DOM
的优势如下:
简单方便:如果使用手动操作真实 DOM 来完成页面,繁琐又容易出错,在大规模应用下维护起来也很困难
性能方面:使用 Virtual DOM,能够有效避免真实 DOM 数频繁更新,减少多次引起重绘与回流,提高性能
跨平台:React 借助虚拟 DOM,带来了跨平台的能力,一套代码多端运行
缺点:
- 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化
- 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,速度比正常稍慢
参考文献
- https://juejin.cn/post/6844904052971536391(opens new window)
- https://www.html.cn/qa/other/22832.html‘
12、关于setState同步和异步的问题
参考链接: https://blog.csdn.net/qq_46658751/article/details/123872815
https://www.jianshu.com/p/4a43f4557876
什么时候同步什么时候异步
在 React 中,如果是由 React 引发的事件处理(比如通过 onClick 引发的事件处理),调用 setState
不会同步更新 this.state
,除此之外的 setState
调用会同步更新 this.state
所谓的除此之外,指的是绕过 React,通过 addEventListener
直接添加的事件处理函数,还有通过 setTimeout/setInterval
产生的异步任务
原因
在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdate 判断是直接更新 this.state 还是放到队列中回头再说,而且 isBatchingUpdate 默认是 false,也就是表示 setState 会同步更新 this.state,但是,有一个函数 batchedUpdate,这个函数会把 isBatchingUpdate 修改为 true,而当 React 在调用事件处理函数之前就会调用这个 batchedUpdates,造成的后果,就是由 React 控制的事件处理过程 setState 不会同步更新 this.state
注意
setState 的 异步 并不是说内部由异步代码实现,其实本身执行的过程和代码是同步的,只是合成事件和钩子函数的调用在更新之前,导致在合成事件和钩子函数中没法里吗拿到更新后的值,形成了所谓的"异步"
但是可以通过 setState(partialState, callback) 中的 callback 拿到更新后的结果
(1). setState(stateChange, [callback])------对象式的setState 1.stateChange为状态改变对象(该对象可以体现出状态的更改) 2.callback是可选的回调函数, 它在状态更新完毕、界面也更新后(render调用后)才被调用 (2). setState(updater, [callback])------函数式的setState 1.updater为返回stateChange对象的函数。 2.updater可以接收到state和props。 4.callback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用。
总结:
1.对象式的setState是函数式的setState的简写方式(语法糖)
2.使用原则:
(1).如果新状态不依赖于原状态 ===> 使用对象方式
(2).如果新状态依赖于原状态 ===> 使用函数方式
(3).如果需要在setState()执行后获取最新的状态数据,
要在第二个callback函数中读取
13、 说说react的事件机制?
使用React中并没有注意到的React事件与浏览器原生事件之间的区别,现在发现这篇文章浅显易懂,所有copy过来,首先得感谢作者。
react的事件是合成事件((Synethic event),不是原生事件
<button onClick={this.handleClick}></button> <input value={this.state.name} onChange={this.handleChange} />
合成事件与原生事件的区别
\1. 写法不同,合适事件是驼峰写法,而原生事件是全部小写
\2. 执行时机不同,合适事件全部委托到document上,而原生事件绑定到DOM元素本身
\3. 合成事件中可以是任何类型,比如this.handleClick这个函数,而原生事件中只能是字符串
react事件执行大致流程如下图
我们来看个例子深入了解下,如果我们需要实现一个组件,这个组件点击按钮会显示一个二维码,点击二维码之外的区域可以隐藏二维码,但是点击二维码本身却不会关闭,代码如下:
//代码来源于《深入React技术栈》2.1.4节 class QrCode extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); this.handleClickQr = this.handleClickQr.bind(this); this.state = { active: false, }; } componentDidMount() { document.body.addEventListener('click', e => { this.setState({ active: false, }); }); } componentWillUnmount() { document.body.removeEventListener('click'); } handleClick() { this.setState({ active: !this.state.active, }); } handleClickQr(e) { e.stopPropagation(); } render() { return ( <div className="qr-wrapper"> <button className="qr" onClick={this.handleClick}>二维码</button> <div className="code" style={{ display: this.state.active ? 'block' : 'none' }} onClick={this.handleClickQr} > <img src="qr.jpg" alt="qr" /> </div> </div> ); } }
上面代码从感官上感觉确实可以实现要求的组件,但事实上我们运行上述代码可以发现,点击二维码本身也会导致二维码的隐藏,现在就有意思了,我们来仔细分析一下。
其实React事件并没有原生的绑定在真实的DOM上,而是使用了行为委托方式实现事件机制。
如上图所示,在JavaScript中,事件的触发实质上是要经过三个阶段:事件捕获、目标对象本身的事件处理和事件冒泡,假设在div中触发了click事件,实际上首先经历捕获阶段会由父级元素将事件一直传递到事件发生的元素,执行完目标事件本身的处理事件后,然后经历冒泡阶段,将事件从子元素向父元素冒泡。正因为事件在DOM的传递经历这样一个过程,从而为行为委托提供了可能。通俗地讲,行为委托的实质就是将子元素事件的处理委托给父级元素处理。React会将所有的事件都绑定在最外层(document),使用统一的事件监听,并在冒泡阶段处理事件,当挂载或者卸载组件时,只需要在通过的在统一的事件监听位置增加或者删除对象,因此可以提高效率。
并且React并没有使用原生的浏览器事件,而是在基于Virtual DOM的基础上实现了合成事件(SyntheticEvent),事件处理程序接收到的是SyntheticEvent的实例。SyntheticEvent完全符合W3C的标准,因此在事件层次上具有浏览器兼容性,与原生的浏览器事件一样拥有同样的接口,可以通过stopPropagation()和preventDefault()相应的中断。如果需要访问当原生的事件对象,可以通过引用nativeEvent获得。
上图为大致的React事件机制的流程图,React中的事件机制分为两个阶段:事件注册和事件触发:
1.事件注册
React在组件加载(mount)和更新(update)时,其中的ReactDOMComponent会对传入的事件属性进行处理,对相关事件进行注册和存储。document中注册的事件不处理具体的事件,仅对事件进行分发。ReactBrowserEventEmitter作为事件注册入口,担负着事件注册和事件触发。注册事件的回调函数由EventPluginHub来统一管理,根据事件的类型(type)和组件标识(_rootNodeID)为key唯一标识事件并进行存储。
2.事件执行
事件执行时,document上绑定事件ReactEventListener.dispatchEvent会对事件进行分发,根据之前存储的类型(type)和组件标识(_rootNodeID)找到触发事件的组件。ReactEventEmitter利用EventPluginHub中注入(inject)的plugins(例如:SimpleEventPlugin、EnterLeaveEventPlugin)会将原生的DOM事件转化成合成的事件,然后批量执行存储的回调函,回调函数的执行分为两步,第一步是将所有的合成事件放到事件队列里面,第二步是逐个执行。需要注意的是,浏览器原生会为每个事件的每个listener创建一个事件对象,可以从这个事件对象获取到事件的引用。这会造成高额的内存分配,React在启动时就会为每种对象分配内存池,用到某一个事件对象时就可以从这个内存池进行复用,节省内存。
再回到我们刚开始的问题,现在看起来就很没有很费解了,之所以会出现上面的问题是因为我们混用了React的事件机制和DOM原生的事件机制,认为通过:
handleClickQr(e) { e.stopPropagation(); }
就能阻止原生的事件传播,其实在事件委托的情形下是不能实现这一点的。当然解决的办法也不复杂,不要将React事件和DOM原生事件混用。
componentDidMount() { document.body.addEventListener('click', e => { this.setState({ active: false, }); }); document.querySelector('.code').addEventListener('click', e => { e.stopPropagation(); }) } componentWillUnmount() { document.body.removeEventListener('click'); document.querySelector('.qr').removeEventListener('click'); }
或者通过事件原件对象中的target
进行判断:
componentDidMount() { document.body.addEventListener('click', e => { if (e.target && e.target.matches('div.code')) { return; } this.setState({ active: false, }); }); }
都可以解决异常关闭的问题。