阅读 3595

Vue超好玩的新特性:DOM传送门

前言

自从上一篇《Vue超好玩的新特性:在CSS中引入JS变量》大火之后,很多小伙伴纷纷点赞留言,所以我决定将《Vue超好玩的新特性》做成一个系列。

因为目前大多数的 Vue 3 文章重点都在讲 Composition API响应式的优化 ,虽然这些新特性很棒,但大家都把其他的新特性给忽略掉了。或者有的文章只是简单的提了一嘴,然后写个简单的 Demo ,讲解的不够细致也不够深入。

除了响应式的优化Composition API之外还有一些新特性非常的实用。所以记得关注我,带你解锁 Vue 3 各种好玩的新特性。

联想

相信大家或多或少都听说过 传送门 这个词。就算没有听说过,那么至少也应该看过《哆啦A梦》吧!(00后好像基本都没看过)

想一想在《哆啦A梦》中经常出现的道具都有什么?

  • 时光机
  • 竹蜻蜓
  • 任意门

这三个道具经常出现,就证明这仨相比于其他道具来说,具有非常强大的通用性。

新版 Vue 就实现了这三个当中的:任意门( 传送门 )。

传送门

如果单单看到 传送门 这三个字的话,有可能无法联想出这到底是一个怎样的功能。

所以我们就用《哆啦A梦》中的任意门来进行举例:

可以想一想《哆啦A梦》中的任意门通常都有哪些特点?

  • 目的地
  • 受目的地环境的影响
  • 目的地必须特别精确

我们来解释一下:

  • 目的地:任意门的使用方法是"在打开门时要想着目的地,否则将通向无法预知的地区"

  • 受目的地环境的影响:假如你的目的地是一个极寒之地,那么你就会像下图这样:

  • 目的地必须特别精确:据不完全统计,大雄每10次使用任意门,就会有3次打开门撞见静香在洗澡。之所以出现这种情况的原因就是:大雄就是想看 大雄打开门的时候想的目的地是"静香家",而静香家有很多房间,但大雄并没有给任意门一个明确的指示到底去的是哪个房间,于是就直接去了静香所在的房间( 淋浴间 ):

(只是举个例子不要钻牛角尖)

Vue 中的传送门

那么再把这几个特性放在 Vue 的传送门中来看看:

  • 目的地:传送门的使用方法是给一个 CSS 选择器来作为目的地。

  • 受目的地环境的影响:假如目的地写了一个可以继承的那种 CSS 属性( 如:color ),那么被传送过去的 DOM 就会受到这个样式的影响。

  • 目的地必须特别精确:假如你给目的地了一个可以匹配很多元素的选择器( 如: div ),那么只会给你传送到第一个匹配到的 div 里面去( 很有可能不是你想去的那个div )。

可能不少人看到这里还是晕晕乎乎的,这玩意到底有啥用?我写组件不就是为了把 DOM 封装在一起,干嘛要让 DOM 传送到别的地方去呢?

要是想在别的 DOM 元素中引入某个 DOM ,直接引入这个组件不就好了嘛,这不正是组件化的意义所在吗?

传送门的意义

来看这样一个案例:

相信大家应该对轮播图都很熟悉了,在这个轮播图之外有一排按钮,这排按钮可以控制轮播图内部的 面板指示点 的位置。

你可能会觉得这不很简单吗?封装一个组件,里面包含了这一排按钮、轮播图、面板指示灯。完了再定义一个位置变量,点击哪个按钮,变量就会随之变化。

之后 面板指示点 相对于轮播图来进行绝对定位,具体定位在哪个方向就靠这个位置变量来控制了。

那么问题来了,这一排按钮并不一定是在一个固定的位置,有可能在这个轮播图的左边,也有可能在右侧。
甚至说这四个按钮可能在别的 DOM 元素内,距离轮播图很远。

你听了以后觉得这也没啥啊,我把这个拆分成两个组件不就完了。按钮是单独的一个组件,轮播图和 面板指示点 又是另一个组件:

就像上图红框里的那样,这是两个不同的组件,你爱把他俩放哪就放哪,放多远都没关系。

当点击按钮的时候只需要用:emit、eventBus或者Vuex…等一系列你能想到的组件间通信方式进行通信就可以了。
把按钮的值传给轮播图,然后轮播图根据传进来的值来决定 面板指示点 的位置。

这么做完全可以实现,事实上以前我们大家一直都是这样实现类似需求的。

但这种实现有两方面的弊端:

封装性

这四个按钮的四个值似乎只会控制这个 面板指示点 ,它并不会去控制页面上其他的什么元素的上下左右,也就是说它的值只关联到 面板指示点

而且如果要是有好几个轮播图的话,每个轮播图都是靠不同的一组按钮来控制,那么就会存在很多很多的组件间通信。

一方面在 Vuex 中能尽量少定义状态就尽量少定义。另一方面即使没用 Vuex ,用 emit 派发事件,然后再在父组件或祖先组件上监听事件再向下层层传递也显得过于麻烦,其他的一些组件通信方式也没好到哪去。

所以从封装性上来讲,它俩才应该是一个整体:

它们两个才应该被封装到一个组件当中去。

可复用性

如果项目中不止有这一个轮播图,还有一些不需要 面板指示点 的轮播图。而且不只是轮播图,假如还有别的 div 或者什么其他元素也需要一个能上下左右来回切换的 面板指示点 和一排可以控制位置的按钮,那你应该怎么做?

先封装按钮组件、再面板指示点组件、再轮播图组件(轮播组件中引入面板指示组件)、然后再来一个不带面板指示的轮播组件、再来几个需要面板的其他组件……

是不是感觉很麻烦、可复用性差而且耦合程度还高?之所以会出现目前这种情景,正是因为不能把他俩封装成一个组件而导致的:

那么到底是什么原因阻碍了它们两个在一起呢?

答案就是 DOM 结构

DOM 结构

假如把他俩封装到一起去会变成这样:

<template>
  <div>
    <!-- 按钮组件 -->
  	<Btns/>
    
    <!-- 面板组件 -->
    <Points/>
  </div>
</template>

<style>
  .points {
  	position: absolute;
  }
</style>
复制代码

虽说为面板组件设置了绝对定位,但它相对的并不是我们想要让它相对的那个 DOM 元素。想要相对具体某个 DOM 元素定位最好的方式就是成为它的后代元素( 当然还要在祖先元素上设置非 static 定位 )。

但现在 <Points> 被限制在了这个组件中,没办法成为别人的后代元素,所以 DOM 传送门就是为了解决这个需求而诞生的!

听我讲到这里是不是已经有点明白过来 DOM 传送门的意义了呢?
DOM 传送门就是能把组件中的 DOM 元素传送到别的地方去。

它的意义就在于:

  • 从组件角度来看,它俩仍旧是同一个组件,组件内定义的变量以及逻辑依然生效
  • 从 DOM 结构来看,面板控制点的 DOM 结构已经成为了别人的子元素了

用法

首先咱们先根据《今日凌晨Vue3 beta版震撼发布,竟然公开支持脚手架项目》这篇文章来搭建一个支持 vue 3 的一个项目。

当然了上面这种是针对你没有升级 @vue/cli 的这么一种情况,如果你把 @vue/cli 升级到最新版的话直接就可以创建:

看!已经可以选择 Vue 3 的预览版了!

建完项目后要先说明的一点是 传送门 是一个组件,这个组件的名字叫做 Teleport :

然后我们想把 App 组件内的图片送走:

那么我们只需在 img 标签的外侧包裹一个 teleport 组件:

⚠️ teleport 组件必须有一个 to 属性,传递的是一个 css 选择器,相当于传送门的目的地

可以看到 img 目前位于 body 内的最底部,想必底层应该是运用了 dom.appendChild() 这样的语法。

不过令人惊讶的是:img 居然被传送到了 #app 的外面去,要知道 vue 代理的可就是这个 #app:

以前是 new Vue() ,然后 vue 挂载到这个 #app 元素上。虽说现在已经被传送到了外面去,那它还归不归 vue 管呢?来试一下:

可以看到组件里面定义的变量以及逻辑对该 DOM 元素依然生效!

那样式呢?样式也会生效吗?再来试一下:

这可真是太棒了!

不过刚才传的是 DOM 元素,可不可以传组件呢?咱们这就来传一下 <HelloWorld/> :

还是没毛病!

不过当你给 teleport 传入字符串的时候,它的底层会调用 document.querySelector() 这个方法。这个方法只会匹配到第一个符合选择器的元素,所以尽量要使用精确一点的选择器,如:'#id'。

除了 to 属性外,teleport 还支持传入一个 disabled 属性,该属性需要传入一个布尔值。

有时我们需要根据某种条件来动态的判断是否需要传送,但这种情况并不适合用 v-if 来处理,因为如果 v-if 为 false 的话,虽然说不会被传送了,但同时它也不会出现在它原本的位置上了,所以这个时候 disabled 就派上用场了。

从字面意义上来看 disabled 是使无效的意思,它默认是 false,也就是说 不使 teleport 无效。换句话说就是生效的意思,那我们来给它一个 true 值让 teleport 不生效:

可以看到它并没有被传送到 body 里去,而是还待在它原来的位置,而且仔细看的话还会发现这样一段注释:

以此来证明不是你没写 teleport ,而是因为它被禁用了!

灵感

估计随便一猜你们都知道这是哪里来的灵感,学过 React 的小伙伴们似乎会觉得这个 teleport 好像有点似曾相识。

没错,它的灵感就来自 React 的 createPortal,但是 Vue 设计的更符合组件化的直觉,我们来对比一下:

其实 Portal 这个单词才是 传送门 的意思:

当然翻译器并没有翻译成传送门,但是意思其实也差不多啦。关键是这个单词它是一个名词,按道理来讲组件名称应该是名词才对。

据说尤雨溪一开始是把它命名为 Portal 的,但是听说 Chrome 浏览器正在研究一个 <portal> 组件,如果成功了的话可能会在浏览器上部署,为了避免未来可能会发生的冲突,就把它改名为 teleport 这个动词啦!

结语

怎么样,这个新特性是不是既实用又好玩呢?

如果还是觉得意犹未尽的话可以看看往期精彩文章: