转载自:https://zhuanlan.zhihu.com/p/29880992

React v16增加了对Portal的直接支持,今天我们就来聊一聊Portal。

似乎所有说React Portal都直接用Portal这个单词,没听过这词的朋友可能觉得不知所云,其实,Portal可以有一个很形象的翻译——“传送门”。

什么是传送门?

曾经有一款游戏就叫做Portal,玩家手上一杆很厉害很科幻的枪,朝墙上开一枪,就可以开出两个“传送门”,人钻进这个传送门,可以从另一个传送门里走出来,也就是说,两个不同位置的传送门之间形成了对接。

如果还不明白Portal是啥,那就拿范冰冰在《X战警:逆转未来》所演角色的GIF动图来看吧。

你看一个哨兵机器人扑过来攻击一个X战警,范冰冰从一个传送门里神速穿越而来,顺手又甩出两个传送门,让哨兵机器人扑进了一个传送门,从另一个传送门一个踉跄掉了出来,从而救了那个X战警。

现在明白Portal是怎么回事了吧。

为什么React需要传送门?

React Portal之所以叫Portal,因为做的就是和“传送门”一样的事情:render到一个组件里面去,实际改变的是网页上另一处的DOM结构

在React的世界中,一切都是组件(参见《帮助你深入理解React》),用组件可以表示一切界面中发生的逻辑,不过,有些特例处理起来还比较麻烦,比如,某个组件在渲染时,在某种条件下需要显示一个对话框(Dialog),这该怎么做呢?

最直观的做法,就是直接在JSX中把Dialog画出来,像下面代码的样子。

<div class="foo"><div> ... </div>{ needDialog ? <Dialog /> : null }
</div>

问题是,我们写一个Dialog组件,就这么渲染的话,Dialog最终渲染产生的HTML就存在于上面JSX产生的HTML一起了,类似下面这样。

<div class="foo"><div> ... </div><div class="dialog">Dialog Content</div>
</div>

可是问题来了,对于对话框,从用户感知角度,应该是一个独立的组件,通常应该显示在屏幕的最中间,现在Dialog被包在其他组件中,要用CSS的position属性控制Dialog位置,就要求从Dialog往上一直到body没有其他postion是relative的元素干扰,这……有点难为作为通用组件的Dialog,毕竟,谁管得住所有组件不用position呢。

还有一点,Dialog的样式,因为包在其他元素中,各种样式纠缠,CSS样式太容易搞成一坨浆糊了。

看样子这样搞局限很多啊,行不通,有没有其他办法?

有一个其他办法,就是在React组件树的最顶层留一个元素专属于Dialog,然后通过Redux或者其他什么通讯方式给这个Dialog发送信号,让Dialog显示或者不显示。

这种方法看起来还凑合着,但是,就这点事还要动用Redux有点高射炮打蚊子,而且,要控制两个不用位置的组件,好麻烦。

而且,如果我们把Dialog做成一个通用组件,希望里面的内容完全定制,这招就更加麻烦了。

<div class="foo"><div> ... </div>{ needDialog ? <Dialog> <header>Any Header</header><section>Any content</section></Dialog>: null }
</div>

像上面那样,我们既希望在组件的JSX中选择使用Dialog,把Dialog用得像一个普通组件一样,但是又希望Dialog内容显示在另一个地方,就需要Portal上场了。

Portal就是建立一个“传送门”,让Dialog这样的组件在表示层和其他组件没有任何差异,但是渲染的东西却像经过传送门一样出现在另一个地方。

React在v16之前的传送门实现方法

在v16之前,实现“传送门”,要用到两个秘而不宣的React API

  • unstable_renderSubtreeIntoContainer
  • unmountComponentAtNode

第一个unstable_renderSubtreeIntoContainer,都带上前缀unstable了,就知道并不鼓励使用,但是没办法啊,不用也得用,还好React一直没有deprecate这个API,一直挺到v16直接支持portal。这个API的作用就是建立“传送门”,可以把JSX代表的组件结构塞到传送门里面去,让他们在传送门的另一端渲染出来。

第二个unmountComponentAtNode用来清理第一个API的副作用,通常在unmount的时候调用,不调用的话会造成资源泄露的。

一个通用的Dialog组件的实现差不多是这样,注意看renderPortal中的注释。

import React from 'react';
import {unstable_renderSubtreeIntoContainer, unmountComponentAtNode} from 'react-dom';class Dialog extends React.Component {render() {return null;}componentDidMount() {const doc = window.document;this.node = doc.createElement('div');doc.body.appendChild(this.node);this.renderPortal(this.props);}componentDidUpdate() {this.renderPortal(this.props);}componentWillUnmount() {unmountComponentAtNode(this.node);window.document.body.removeChild(this.node);}renderPortal(props) {unstable_renderSubtreeIntoContainer(this, //代表当前组件<div class="dialog">{props.children}</div>, // 塞进传送门的JSXthis.node // 传送门另一端的DOM node);}
}

首先,render函数不要返回有意义的JSX,也就说说这个组件通过正常生命周期什么都不画,要是画了,那画出来的HTML/DOM就直接出现在使用Dialog的位置了,这不是我们想要的。

在componentDidMount里面,利用原生API来在body上创建一个div,这个div的样式绝对不会被其他元素的样式干扰。

然后,无论componentDidMount还是componentDidUpdate,都调用一个renderPortal来往“传送门”里塞东西。

总结,这个Dialog组件做得事情是这样:

  1. 它什么都不给自己画,render返回一个null就够了;
  2. 它做得事情是通过调用renderPortal把要画的东西画在DOM树上另一个角落。

在renderPortal中,利用unstable_renderSubtreeIntoContainer函数往直前创建的div里塞JSX,这里我们用的JSX是这样。

  <div class="dialog">{props.children}</div>

因为是吧children画出来,所以使用Dialog可以加上任意的子组件。

 <Dialog>What ever shit<div>Hello</div><p>World</p></Dialog>

你看,所谓React Portal,就是能够表面上渲染在一个地方,实际上渲染到了另一个地方。

是不是感觉好厉害,不光好厉害,而且像Dialog这样的场景Portal简直就是必不可少。

到了v16,React干脆直接支持Portal,当然,v15还将被使用一段时间,所以大家看了上面的内容也不算浪费时间:-)

React v16的Portal支持

在v16中,使用Portal创建Dialog组件简单多了,不需要牵扯到componentDidMount、componentDidUpdate,也不用调用API清理Portal,关键代码在render中,像下面这样就行。

import React from 'react';
import {createPortal} from 'react-dom';class Dialog extends React.Component {constructor() {super(...arguments);const doc = window.document;this.node = doc.createElement('div');doc.body.appendChild(this.node);}render() {return createPortal(<div class="dialog">{this.props.children}</div>, //塞进传送门的JSXthis.node //传送门的另一端DOM node);}componentWillUnmount() {window.document.body.removeChild(this.node);}
}

v16提供createPortal函数来创建“传送门”,我个人觉得这个函数应该叫renderPortal好一些,因为组件的render函数除了mount时会被调用,update时也会被调用,update时还叫createPortal有点不大合适。

穿越Portal的事件冒泡

v16之前的React Portal实现方法,有一个小小的缺陷,就是Portal是单向的,内容通过Portal传到另一个出口,在那个出口DOM上发生的事件是不会冒泡传送回进入那一端的。

也就是说,这样的代码。

<div onClick={onDialogClick}>   <Dialog>What ever shit</Dialog>
</div>

在Dialog画出的内容上点击,onDialogClick是不会被触发的。

当然,这只是一个小小的缺陷,大部分场景下事件不传过来也没什么大问题。

在v16中,通过Portal渲染出去的DOM,事件是会冒泡从传送门的入口端冒出来的,上面的onDialogClick也就会被调用到了。

传送门:React Portal相关推荐

  1. JZOJ 5906. 【NOIP2018模拟10.15】传送门 (portal)

    Description 8102年,Normalgod在GLaDOS的帮助下,研制出了传送枪.但GLaDOS想把传送枪据为己有,于是把Normalgod扔进了一间实验室.这间实验室是一棵有n个节点的树 ...

  2. 5906. 【NOIP2018模拟10.15】传送门 (portal)

    Description 8102年,Normalgod在GLaDOS的帮助下,研制出了传送枪.但GLaDOS想把传送枪据为己有,于是把Normalgod扔进了一间实验室.这间实验室是一棵有n个节点的树 ...

  3. 传送门(portal)

    Linux operation(Linux运维) #RHEL/CentOS/fedora系列为主 Basis: Kernel: 常见系统调用 Commands: trouble shooting命令详 ...

  4. React TypeScript 从零实现 Popup 组件发布到 npm

    本文转载自掘金<从0到1发布一个Popup组件到npm>,作者「海秋」. 点击下方阅读原文去点个赞吧! 上篇文章[1]中介绍了如何从 0 到 1 搭建一个 React 组件库架子,但为了一 ...

  5. 前端面试题(react)

    说说React中onClick绑定后的工作原理 首先react有自己的事件系统,也是遵循w3c的,这个事件系统的名称叫做合成事件(SyntheticEvent),而其自定义事件系统的动机主要包含以下几 ...

  6. react.lazy 路由懒加载_React lazy/Suspense使用及源码解析

    React v16.6.0已经发布快一年了,为保障项目迭代发布,没有及时更新react版本,最近由于开启了新项目,于是使用新的react版本进行了项目开发.项目工程如何搭建,如何满足兼容性要求,如何规 ...

  7. 把这304道React的面试题刷完,前端面试没有在怕的!

    Core React 什么是 React? React 是一个开源前端 JavaScript 库,用于构建用户界面,尤其是单页应用程序.它用于处理网页和移动应用程序的视图层.React 是由 Face ...

  8. React 面试题 回答

    原文https://github.com/semlinker/reactjs-interview-questions 本项目的面试题来源于 sudheerj/reactjs-interview-que ...

  9. React源码解毒 - render方法解析

    render方法解析 要将react元素渲染到页面当中,分为了两个阶段: render阶段 和 commit阶段. render阶段:由协调层负责的阶段 在这个阶段当中要为每一个react元素构建对应 ...

最新文章

  1. 更改数据库对象所有者
  2. NGUI Label Color Code
  3. ListView和RecyclerView的Adapter封装
  4. golang——net/rpc包学习
  5. JavaBean的详细及引用
  6. 第 10 章 桥接模式
  7. LeetCode 744. Find Smallest Letter Greater Than Target
  8. Netty解决TCP的粘包和分包(二)
  9. Docker系列(七)构建镜像
  10. 24.docker port
  11. 公专网集群对讲系统在城市执法过程中的应用
  12. 百度快速排名算法解密-百度搜索引擎快速排名软件-百度快排模拟点击器软件
  13. 连续8个季度增长超100% 阿里云成长为“亚洲巨象”
  14. 8款高质量小程序推荐:(工具类、电影类、阅读类)
  15. 结算机网络的tracert和route命令
  16. Excel单元格校验
  17. cool edit pro生成2.7khz_0dB音源方法
  18. 你和孩子是好朋友吗?
  19. 【自然语言处理NLP】中文语料整理【情感分析、文本分类、摘要、实体分析】
  20. 最难游戏2计算机,《只狼:影逝二度》勉强排第六?盘点10款史上最难的单机游戏!...

热门文章

  1. c#向服务器发送消息,C#模拟httpwebrequest请求_向服务器模拟cookie发送
  2. 安装泰捷视频、多屏互动体验...电视盒子竟还有这些功能!
  3. html5制作大小写转换,java大小写转换
  4. 记一次支付宝H5证书方式支付、回调、及退款
  5. RPG游戏攻防公式设计探讨
  6. Archlinux使用总结
  7. 有关小米6手机更新REC的一些问题记录
  8. html,css,js实现星座,属相测算小程序
  9. Arrays常用方法(超详解)
  10. 路由器配置loopback具体作用