React实现了一套与浏览器无关的DOM系统,包括元素渲染、节点查询、事件处理等机制。

一、ReactDOM

  自React v0.14开始,官方将与DOM相关的操作从React中剥离,组成单独的react-dom库,从而让React能兼容更多的终端。在引入react-dom库后,就能调用一个全局对象:ReactDOM,虽然在之前的章节中已多次使用该对象,但是都没有给出过多的讲解,本节将对其做重点分析。

  ReactDOM只包含了unmountComponentAtNode()、findDOMNode()、createPortal()和render()等为数不多的几个方法,其中在第2篇中介绍了render()的功能,又在第4篇中提到用unmountComponentAtNode()方法移除DOM中已挂载的组件。接下来会讲解剩余的两个方法。

1)findDOMNode()

  当组件被渲染到页面真实的DOM中后,就能通过findDOMNode()方法得到生成的DOM元素,然后就能完成诸如读值、计算尺寸等操作。

  注意,findDOMNode()只能获取已挂载的组件,并且不能用于函数组件。在组件的生命周期中,它只能存在于componentDidMount()和componentDidUpdate()两个回调函数中,在其它地方调用会抛出一个错误,具体如下所示。

class Btn extends React.Component {
render() {
ReactDOM.findDOMNode(this); //抛出错误
return <button>提交</button>;
}
componentDidMount() {
ReactDOM.findDOMNode(this); //<button>提交</button>
}
}

  在上面的示例中,this指向的是Btn组件实例,在将this传给findDOMNode()方法后,得到了一个<button>元素。有一点要注意,如果组件中的render()返回null或false,那么findDOMNode()只会返回null。

2)createPortal()

  在React v16中,新增了Portal特性,能让组件渲染到父组件以外的DOM节点中。这个特性适用于需要跳出容器的场景,例如创建页面内定制的弹框。

  在React中使用Portal特性,需要调用ReactDOM上的一个新方法:createPortal()。此方法能接收2个参数,第一个是可渲染的React子元素,例如字符串、React元素数组等;第二个是DOM元素,也就是要挂载的容器。关于这个方法的具体使用可参考下面的示例。

class Btn extends React.Component {
render() {
return ReactDOM.createPortal(this.props.children, document.body);
}
}
ReactDOM.render(<Btn><p>按钮</p></Btn>, document.getElementById("container"));

  在上面的render()方法中调用了ReactDOM.createPortal(),使得<p>元素最终挂载到了<body>元素中,而不是id属性为“container”的元素。

二、Refs

  Refs是一种访问方式,通过它可读取render()方法内生成的组件实例和DOM元素,常用来处理元素的焦点、触发动画、集成第三方DOM库等。注意,在组件的生命周期中,要让Refs有效,得将其放在componentDidMount()和componentDidUpdate()两个回调函数中才行。虽然Refs能给某些场景带来便利,但是它破坏了React通过props传递数据的典型数据流,因此要尽量避免使用Refs。

  如果要使用Refs的功能,那么就得设置React元素的ref属性,它的值可以是对象、回调函数和字符串,下面会分别讲解ref属性的这三类值。

1)对象

  此处的对象是React.createRef()方法的返回值,包含一个current属性,而该属性指向的正是要读取的组件实例或DOM元素。下面的示例展示了ref属性和React.createRef()方法的配合过程。

class Btn extends React.Component {
constructor(props) {
super(props);
this.myBtn = React.createRef();
}
render() {
return <button ref={this.myBtn}>提交</button>;
}
componentDidMount() {
let btn = this.myBtn.current;
console.log(btn); //<button>提交</button>
}
}

  首先在组件的构造函数中调用React.createRef();再将返回值赋给this.myBtn,这样就能在组件内部的任意位置使用该对象了;然后让this.myBtn成为<button>元素的ref属性的值;最后在componentDidMount()中就能成功读取到current属性的值,从而完成了一次Refs式的访问。

2)回调函数

  这个回调函数能接收一个参数(如下代码所示),当组件被挂载时,参数的值为组件实例或DOM元素;当组件被卸载时,参数的值为null。

class Btn extends React.Component {
render() {
return (
<button ref={btn => { this.myBtn = btn }}>提交</button>
);
}
componentDidMount() {
let btn = this.myBtn;
console.log(btn); //<button>提交</button>
}
}

  与前一种使用方式最大的不同是解除了对React.createRef()方法的依赖,在回调函数中直接将其参数赋给this.myBtn,就能得到预期的结果,不用再调用一次current属性。

3)字符串

  ref属性的值还可以是字符串,例如下面代码中的"myBtn",通过this.refs.myBtn就能访问到想要的组件实例或DOM元素。

class Btn extends React.Component {
render() {
return <button ref="myBtn">提交</button>;
}
componentDidMount() {
let btn = this.refs.myBtn;
console.log(btn); //<button>提交</button>
}
}

  不过,官方已经不推荐这种写法了,在未来的版本中有可能会被移除,因此建议改用回调函数的方式。

4)使用场景

  ref属性不仅能像之前示例那样应用于DOM元素上,还能在类组件中使用ref属性,如下代码所示。

class Btn extends React.Component {
render() {
return <button>提交</button>;
}
}
class Container extends React.Component {
render() {
return <Btn ref={btn => { this.myBtn = btn }}>提交</Btn>;
}
componentDidMount() {
let btn = this.myBtn;
console.log(btn); //Btn组件的实例
}
}

  Container是Btn的父组件,在其render()方法中,通过回调函数将Btn组件的实例赋给了this.myBtn。

  由于函数组件没有实例,因此不能对其添加ref属性。

5)子组件的DOM元素

  在父组件中,如果要访问子组件的某个DOM元素,那么单靠ref属性是无法实现的,因为ref属性得到的只能是子组件的实例。不过,有一种间接的方式可以实现这个需求,那就是将ref属性和ReactDOM.findDOMNode()配合使用。下面套用上一节使用场景中的Btn和Container两个组件,代码只列出了修改部分,其余都已省略。

class Container extends React.Component {
componentDidMount() {
let btn = this.myBtn;
let dom = ReactDOM.findDOMNode(btn);
console.log(dom); //<button>提交</button>
}
}

  在componentDidMount()方法中,调用了一次ReactDOM.findDOMNode(),从而得到了子组件所拥有的DOM元素。

三、Fragments

  JSX结构有一个限制,那就是在最外层必须用一个元素包裹,即使这是一个冗余的元素,也得加上。例如为一个<ul>元素挂载一组元素集合,如下所示。

class Btns extends React.Component {
render() {
return (
<div>
<li>1</li>
<li>2</li>
<li>3</li>
</div>
);
}
}

  在页面上渲染出的DOM会像下面这样,其中<div>元素在此处是没有作用的。

<ul id="container">
<div>
<li>1</li>
<li>2</li>
<li>3</li>
</div>
</ul>

  为了避免这种无意义的输出,React引入了Fragments,其结构如下代码所示。只需将最外层的<div>元素的开始和结束标签分别改成<>和</>,就不用在DOM中增加额外的元素了。

class Btns extends React.Component {
render() {
return (
<>
<li>1</li>
<li>2</li>
<li>3</li>
</>
);
}
}

1)React.Fragment

  <>和</>最终会被编译成React.Fragment组件的开始和结束标签,也就是说,前者是后者的语法糖。下面的代码和上一个Fragments的示例是等价的。

class Btns extends React.Component {
render() {
return (
<React.Fragment>
<li>1</li>
<li>2</li>
<li>3</li>
</React.Fragment>
);
}
}

  如果要为Fragments添加Keys标识(即为其定义key属性),那么只能用React.Fragment组件包裹子元素。注意,key是React.Fragment组件目前唯一可用的属性。

React躬行记(5)——React和DOM的更多相关文章

  1. React躬行记(3)——组件

    组件(Component)由若干个React元素组成,包含属性.状态和生命周期等部分,满足独立.可复用.高内聚和低耦合等设计原则,每个React应用程序都是由一个个的组件搭建而成,即组成React应用 ...

  2. React躬行记(16)——React源码分析

    React可大致分为三部分:Core.Reconciler和Renderer,在阅读源码之前,首先需要搭建测试环境,为了方便起见,本文直接采用了网友搭建好的环境,React版本是16.8.6,与最新版 ...

  3. React躬行记(13)——React Router

    在网络工程中,路由能保证信息从源地址传输到正确地目的地址,避免在互联网中迷失方向.而前端应用中的路由,其功能与之类似,也是保证信息的准确性,只不过来源变成URL,目的地变成HTML页面. 在传统的前端 ...

  4. React躬行记(8)——样式

    由于React推崇组件模式,因此会要求HTML.CSS和JavaScript混合在一起,虽然这与过去的关注点分离正好相反,但是更有利于组件之间的隔离.React已将HTML用JSX封装,而对CSS只进 ...

  5. React躬行记(2)——JSX

    JSX既不是字符串,也不是HTML,而是一种类似XML,用于描述用户界面的JavaScript扩展语法,如下代码所示.在使用JSX时,为了避免自动插入分号时出现问题,推荐在其最外层用圆括号包裹,并且必 ...

  6. React躬行记(4)——生命周期

    组件的生命周期(Life Cycle)包含三个阶段:挂载(Mounting).更新(Updating)和卸载(Unmounting),在每个阶段都会有相应的回调方法(也叫钩子)可供选择,从而能更好的控 ...

  7. React躬行记(6)——事件

    React在原生事件的基础上,重新设计了一套跨浏览器的合成事件(SyntheticEvent),在事件传播.注册方式.事件对象等多个方面都做了特别的处理. 一.注册事件 合成事件采用声明式的注册方式, ...

  8. React躬行记(7)——表单

    表单元素是一类拥有内部状态的元素,这些状态由其自身维护,通过这类元素可让用户与Web应用进行交互.HTML中的表单元素(例如<input>.<select>和<radio ...

  9. React躬行记(9)——组件通信

    根据组件之间的嵌套关系(即层级关系)可分为4种通信方式:父子.兄弟.跨级和无级. 一.父子通信 在React中,数据是自顶向下单向流动的,而父组件通过props向子组件传递需要的信息是组件之间最常见的 ...

随机推荐

  1. Android官方教程翻译(4)——启动另一个Activity

    Starting Another Activity 启动另一个Activity PREVIOUSNEXT THIS LESSON TEACHES YOU TO 这节课教你 1.   Respond t ...

  2. 用Delphi即时判断当前的网络的连接方式

    判断网络的是否连接或者连接方式,比较重要的一点就是是不是即时判断,如果你明明无连接,判断出来却说你有,连接上了,却说你还未连接,下面要介绍的两个方法中 InternetGetConnectedStat ...

  3. Gradle Android它自己的编译脚本教程的最新举措(提供demo源代码)

    一.前言 Gradle 是以 Groovy 语言为基础,面向Java应用为主.基于DSL(领域特定语言)语法的自己主动化构建工具. 上面这句话我认为写得非常官方,大家仅仅需知道Gradle能够用来an ...

  4. 七easy网络陷阱上当

    网络犯罪可能开始与你或你的家人,因为无论出现什么样的警告信息,或异常体征,你还是做你通常做在互联网上,网络犯罪已经发生不知道.趋势科技收集了你所该避免的七种最常见的网络犯罪陷阱.让你和家人避免成为它们 ...

  5. Leetcode 144 Binary Tree Preorder Traversal 二叉树

    二叉树的基础操作:二叉树的先序遍历(详细请看数据结构和算法,任意本书都有介绍),即根,左子树,右子树,实现方法中还有用栈实现的,这里不介绍了 /** * Definition for binary t ...

  6. python3 小技巧(2)

    原文 http://blog.csdn.net/jclass/article/details/6145078 一. base64 编码和解码任意的二进制字符串到文本字符串(主要用在HTTP EMAIL ...

  7. 初次使用glog

    一.安装配置 1.简单介绍 google 出的一个C++轻量级日志库,支持下面功能: ◆ 參数设置,以命令行參数的方式设置标志參数来控制日志记录行为: ◆ 严重性分级,依据日志严重性分级记录日志: ◆ ...

  8. 2017 JavaScript 开发者的学习图谱

    码云项目推荐 前端框架类 1.项目名称: 基于 Vue.js 的 UI 组件库 iView 项目简介:iView 是一套基于 Vue.js 的 UI 组件库,主要服务于 PC 界面的中后台产品. 特性 ...

  9. php如何判断 0.0/0.00/0.000 是否为空? 测试过用empty函数不行

    if ( (int) $number == 0) echo 'empty'; if ( floatval($number) == 0 ) echo 'empty' 首先,PHP 认为 0.0 是空,同 ...

  10. __declspec的15种用法

    __cdecl和__stdcall都是函数调用规范(还有一个__fastcall),规定了参数出入栈的顺序和方法,如果只用VC编程的话可以不用关心,但是要在C++和Pascal等其他语言通信的时候就要 ...