我为什么劝你不要用选择器获取DOM

这篇博客灵感也是源于公司的项目,因为我自己之前是写 React 比较多,虽然一直记得使用了框架,开发者就不要原生的操作 DOM,但 React 主流脚手架 CRA 在将虚拟 DOM 转换成真实 DOM 的时候,也用的 document.getElementById

加上 React 写起来就很像原生的 JavaScript,所以可能对这条告诫也没有很大的敬畏之心,直到前几天写公司 Vue 栈项目需求的时候出问题了...

1 问题描述

我在项目里复用了一个叫 <lay-bar-graphs> 的组件,该组件里面用到了 echarts。,大致如下:

<div class="template-bewteen">
    <lay-bar-graphs :num="1"></lay-bar-graphs>
    <lay-bar-graphs :num="2"></lay-bar-graphs>
</div>
复制代码

组件里面大致是这样的(就一个容器 div 和包含一个 echarts 渲染的 div ):

<template>
    <div class="layGraphs-container">
        <div 
        id="layBarGraphs" 
        ref="layBarGraphs" 
        style="width:100%;height:100%;"
        >
        </div>
    </div>
</template>
复制代码

我想要的效果是这样的(内网截图传不到外网,小糊):

IMG_20220126_163840.jpg

但是我得到的却是这样的(看似只有一个 <lay-bar-graphs> 组件成功渲染)

IMG_20220126_163724.jpg

打开浏览器开发者工具一查发现,<lay-bar-graphs> 组件渲染了两次,但 DOM 树结构里面只有一个 <canvas> (我们都知道 echarts 会把元素渲染成 canvas),真是奇了怪~

2 解决问题

2.1 聚焦问题

接着我们意识到问题出在渲染这里,也通过 console 等手段确定,echarts 确实渲染了两次。

用过 echarts 的小伙伴都会知道,echarts 需要获取 DOM,大致如下,具体可以去看官网示例

import * as echarts from 'echarts';//引入
​
var chartDom = document.getElementById('main');//获取DOM
var myChart = echarts.init(chartDom);//初始化
var option = {...};//配置
​
option && myChart.setOption(option);//渲染
复制代码

所以我们马上把问题聚焦到获取 DOM 上,同时也是瞬间反应过来。

CSS 是全局的,Vue 和 React 等库虽然做到了组件化,但只是通过闭包等手段取巧的模拟组件效果,事实上 组件里面的id ,class 仍然是全局的。这也是为什么现在脚手架会使用 less/sass/css module 等做局部样式隔离的原因。

这个时候问题又来了,项目中是使用了 less 的,像这样 <style scoped lang="less">,那为什么获取的 DOM 还是同一个呢?我猜测哈!less 只是将选择器隔离在一个组件内,你复用的时候,还是这个组件,所以选择器选择的结果还是一样的

顺带一提,浏览器自带的 web component 最近经常听到吧,不用框架,浏览器原生的支持组件化,也不需要考虑样式冲突,真不错,期待后续的普及。

2.2 解决方案

在框架中获取 DOM,肯定能想到使用 ref。那为什么使用 ref 就不会有问题呢?我来回顾一下。

ref 标记的元素会成为组件实例的一个属性,组件的每次使用都会创建一个新的实例,这样即使属性名一样,但是他们能够区分谁是谁,这样 echarts 的渲染也就不是同一个元素了。

3 拓展

那么原生 HTML 、JS 是如何复用 echarts 组件的呢?经过和同事以及群里老哥的讨论,我们写了一些代码,大伙可以粘贴自己去试一下~

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    div{
    border: black solid 1px;
      width: 400px;
      height: 400px;
    }
    .bule {
      background-color: #00f;
      width: 3oopx;
      height: 300px;
    }
    .green {
      background-color: #0f0;
      width: 3oopx;
      height: 300px;
    }
  </style>
</head>
<body>
  <div id="t1"></div><div id="t2"></div>   
  <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
  <script>
    function fn (color) {
        let obj = document.createElement('p');
        obj.className = color//这种不起作用,因为echarts已经画完了,你才给个高度,晚了
        obj.style.width = '300px'//需要及时给
        obj.style.height = '300px'
​
        var myChart = echarts.init(obj);
        var option;
​
        option = {
        xAxis: {
            type: 'category',
            data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
        },
        yAxis: {
            type: 'value'
        },
        series: [
            {
            data: [820, 932, 901, 934, 1290, 1330, 1320],
            type: 'line',
            smooth: true
            }
        ]
        };
​
        option && myChart.setOption(option);
        return obj;
    }
​
    document.getElementById('t1').appendChild( fn('green') );
    document.getElementById('t2').appendChild( fn('bule') );
  </script>
</body>
</html>
复制代码

这种方式比较取巧的是,通过 createElement 封装成一个函数,每次执行都新建元素,不存在渲染同一个元素,另外在 Vue 中也可以通过 document.getElementsByClassName 来获取 DOM,因为是一个类数组嘛!复用组件的时候传递一个参数进去,指定渲染哪个 DOM (但肯定用 ref 来的简单)。

Supongo que te gusta

Origin juejin.im/post/7062529233687412767
Recomendado
Clasificación