为什么Text.splitText()会影响布局?

时间:2017-08-22 13:17:34

标签: javascript html css dom

我们假设我们的页面中有一个段落,只有一个文本块。

<p>laborum beatae est nihil, non hic ab, deserunt repellat quas. Est molestiae ipsum minus nesciunt tempore voluptate laboriosam</p>

DOM-wise,结构是:

HTMLParagraphElement
  Text [laborum beatae est nihil...]

现在我们将它(使用Text.splitText())拆分两次,以分离&#34; deserunt repellat quas。 Est&#34; 片段。结构变为:

HTMLParagraphElement
  Text [laborum beatae est nihil...]
  Text [deserunt repellat quas. Est]
  Text [ molestiae ipsum minus nesciunt...]

虽然这个操作会影响DOM,但它永远不会在元素级别上更改它(Text!== Element),所以我预计不会有视觉上的变化。

然而splitText()也会影响布局,在所有经过测试的浏览器(Chrome 60,Firefox 55,Edge 14 - 所有Windows 10操作系统)中触发重放和重新绘制。当我们调用ParagraphElement.normalize()时,会发生同样的情况,将Text节点的数量减少到1;再次触发重新布局和重绘。

这有一个令人讨厌的副作用,可以在this demo中看到。如果你检查'quas附近的话。 Est&#39;,你看他们实际上改变了位置!

它在Firefox中清晰可见,并且在Chrome中更加微妙(但也可以区分)。令我高兴的是,没有这样的&#34;字舞&#34;发生在Edge。

这种重要的原因显示在[种类]匀场选择引擎的this demo中。这个特殊的版本在Firefox中不起作用(暂不支持caretRangeFromPoint - 唉!),但即使是&#34; point2dom&#34;重新连接到caretPositionFromPoint,突出显示的文字会在那里重新定位 - 在Chrome中也是如此,甚至更糟。再次,它似乎在Edge中运作良好。

所以,事实上,我最感兴趣的是理解原因并找到解决方法。

这里是动画gif,展示了第一个演示如何在Chrome中播放(我只是在间隔中触发点击)

when the words go marching in...

颤抖在这里微妙,但仍然可以在所有的词上观察到。我特别感到困惑的是imolestiae为什么会摇晃,因为周围的字母似乎保持原样。

随着不太常见的字体和更多的文字,它会变得更糟(更糟糕),就像选择演示一样。

切换到font-family:monospace并没有解决这个问题,但让它看起来更糟糕了:

enter image description here

font-kerning切换为none也无济于事。

更新:Blink跟踪器上的问题is registered

2 个答案:

答案 0 :(得分:6)

关于重新布局/重绘它是可以预期的,因为文本节点也是DOM节点......不是DOM元素,但是浏览器必须重新考虑布局,即使你希望它保持不变,它们也是可能不得不搬家。也许是因为字距调整。

现在,为什么拆分文字会导致一些移动?我期望的是因为浏览器分别绘制文本部分。两个相邻的字母通常有一个空格可以根据字母减少字体,例如取“WA”,W的结尾位于A的开头之上,称为kerning(Thx Ismael)米格尔)。当单独绘制文本节点时,每个文本节点必须在下一次启动之前完成,因此它可以在这些字母之间创建更大的空间,因为它可以防止任何字距。

对不起,字母之间的空格确实有一个名字,但我忘了......

.one {
  background-color: #FF9999;
}

.two {
  background-color: #99FF99;
}

body {
  font-size: 40px;
}

div>span {
  border: 1px solid black;
}
<div><span>AW</span> - in the same DOM node.</div>
<div><span><span>A</span><span>W</span></span> - in two different nodes</div>
<div><span><span class="one">A</span><span class="two">W</span></span> - in two different nodes, colored</div>

至于如何防止这种行为,最简单的解决方案是使用等宽字体。这可能并不总是在美学上可行。 Kerning是嵌入在字体文件中的信息,从这些信息中剥离字体似乎是防止闪烁的最强大的方法。另外,当设置为none时,CSS属性font-kerning会有所帮助。

另一种方法是在文本后面或前面添加绝对元素,以模仿将文本的一部分包含到元素中的事实,但这完全取决于最终目标。

进一步了解CSS-Tricks have a nice article about text-rendering,它也有助于字体字距调整。

编辑:在写这个答案时,我忽略了文本是合理的这一事实。虽然我的回答解释了为什么在将文本节点切割成多个文本节点时会发生某种闪烁,但这并不能解释为什么浏览器在计算合理空间时会出现问题。

答案 1 :(得分:4)

tl:dr Version

splitText()可能是触发更改的操作,但更改 实际上是由更新的dom通过文本运行引起的 理由引擎。从text-align: justify;更改为 text-align: left;看看我的意思。

<强>附录

处理转移的单词。至于在关闭辩护时转移的字母,这有点难以量化,但它似乎是一个正在越过的四舍五入的阈值。

完整答案

文本对齐中的注意事项

至少可以说,文本理由的实施很复杂。为了简化, 有三个基本考虑因素:

  1. 精度
  2. 美学
  3. 速度
  4. 任何一方的收益都要求其中一方或双方都有损失。例如, InDesign,有利于美学,使用字间距(正面和负面),字母间距(正面和负面),并考虑段落中的所有线条,以速度为代价找到最令人满意的布局,并允许光学边距对齐准确性的代价。但是因为它只经历了所有这一次并将结果缓存在文件中,所以它可以(相对)非常慢地消失。

    浏览器往往更关心速度,部分原因是它们需要 能够在过时的硬件上快速证明文本,但也要感谢 我们现在享受的互动性,有时需要在会话期间重复证明同一块文本数千次。

    浏览器实施方面的差异

    spec有些模糊 关于辩解的话题,说:

      

    在对文本进行验证时,用户代理会获取其间的剩余空间   一行的内容的末尾和其行框的边缘,并分发它   整个内容的空间,以便内容准确填充行框。   用户代理可以替代地分配负空间,放置更多   线上的内容比在正常间距条件下适合的内容。

      

    对于自动对齐,此规范未定义所有内容   理由机会是,优先次序如何,何时以及如何   多层次的理由机会相互作用。

    因此,每个浏览器都可以随意优化此功能 配合。

    示例

    现代文本对齐引擎比合理的更复杂 在我们这里的空间解释。补充一点,他们不断调整 在主要考虑因素和我写的任何内容之间找到更好的平衡 无论如何,这将在几纳秒内过时,我将使用非常的 旧的(和更简单的)文本对齐算法来演示如何引擎 在这种情况下可能难以一致地呈现。

    假设我们有字符串'Lorem ipsum dolor sit amet, consectetur.'和我们 一行可以容纳35个字符。因此,让我们使用这种理由算法:

    1. 创建数组
    2. 完成字符串,直到找到单词或标点符号的结尾 后跟一个空格或字符串结尾
    3. 当您找到单词的结尾时,请检查单词的长度是否为+ 数组中所有单词的长度加上对齐的数量 机会。如果是这样,请将其从字符串中删除并将其放入数组中。
    4. 当没有更多的单词可以添加到数组时,请采取区别 所需空间和可用空间除以数量 理由机会,并绘制数组,放置数量 每个单词之间的空格。
    5. 使用此算法:

      1. 拆分字符串

        'Lorem ipsum dolor sit amet, consectetur.' => 
            ['Lorem','ipsum','dolor','sit','amet,'] & 'consectetur.'
        
      2. 我们在23个字符的文字宽度和35个字符的可用空间之间添加了3个空格(I&#39; m将空间增加四倍以便强调,以后会很重要)

        -------------------------------------------------------------------------
        |Lorem            ipsum            dolor            sit            amet,|
        |consectetur.                                                           |
        -------------------------------------------------------------------------
        
      3. 这很快,因为我们可以从左到右绘制所有内容而无需回溯,我们也不需要向前看。

        如果我们对此运行textSplit并将其有效地转换为数组:
        ['Lorem ipsum dolor ','sit',' amet, consectetur.']
        所以我们需要修改我们的规则,让我们改变规则2来解决每个问题 数组中的字符串,遵循与之前相同的规则。

        1. 拆分字符串,注意amet之前有一个空格,所以字边界不会抓住它

          `['Lorem ipsum dolor ','sit',' amet, consectetur.']` => 
              ['Lorem','ipsum','dolor','sit',' amet,'] &  'consectetur.'
          
        2. 我们添加了24个字符的文字宽度和35个字符的可用空间 每个单词之间2.75个空格(再次使空间翻两番)。额外的空间 在amet字符串中也是绘制的。

          -------------------------------------------------------------------------
          |Lorem           ipsum           dolor           sit               amet,|
          |consectetur.                                                           |
          -------------------------------------------------------------------------
          
        3. 如果我们将两条线放在一边看,我们可以看出差异。

            -------------------------------------------------------------------------
          a |Lorem            ipsum            dolor            sit            amet,|
          b |Lorem           ipsum           dolor           sit               amet,|
            -------------------------------------------------------------------------
          

          同样,这些都是夸张的,现实生活中的四分之一空间只会是一个 像素或两个。

          我们的规则很简单,所以我们可以很容易地解决这个问题。

          考虑一下你有理由进行调试会有多复杂 必须支持的引擎:

          1. 其他(可调整的)css属性以进行调整
          2. 多种语言及其所有规则
          3. 包含以下内容的字体:
            • 可变字符宽度
            • 字距调整指标
            • 不一定与像素对齐的矢量
          4. 具有自己样式的内联(和内联块)元素
          5. ... on and on
          6. 更不用说,大部分内容实际上是交给GPU实际绘制的。

            无论如何都要说

            请记住,事实上,你正在改变dom并强迫整个dom 阻止重新渲染的结果。考虑到所涉及的因素数量,它非常有用 很难期望两种不同的dom结构始终呈现完全相同的结构。

            附录

            关于那些似乎偶尔会转变的字母,我所说的关于如何处理这些事情的复杂性的大部分内容继续适用于以下内容。

            同样,为了提高速度,在将数字传递给GPU进行渲染之前,数字通常会向下舍入。

            通过提供简化的示例,百分之一的像素没有太大的区别,人眼察觉不到,因此浪费了处理能力。所以你决定四舍五入到最近的像素。

            让我们说字符绘制序列是:

            1. 从容器的开始
            2. 添加偏移量
            3. 在此位置绘制字符,四舍五入到最近的像素
            4. 通过添加角色的未接地宽度来更新偏移。
            5. 宽度为的字符:

              10.2 10.3 10.4 10.2 10.6 11 8.9 9.9 7.6 9.2 9.8 10.4 10.4 11.1 10.5 10.5
              

              从0点开始绘制:

              0    10   21   31   41   52 63  72  82  89  98  108  119  129  140  151
              

              但是如果你到达节点的末尾怎么办?那很简单,只需在下一个绘制位置启动新节点并继续前进?

              宽度为的字符:

              10.2 10.3 10.4 10.2 10.6 11 8.9|9.9 7.6 9.2 9.8 10.4 10.4 11.1 10.5 10.5
              

              从0点开始绘制:

              0    10   21   31   41   52 63  72  82  89  99  108  119  129  140  151
              

              即使基础数字不同,由于四舍五入,除了第11个字符外,每个位置的渲染位置都保持不变。

              它可能不一定是起始位置,同样,这里存在巨大的复杂性。症状确实指向某种舍入阈值。正如我之前所说,当渲染两个不同的dom树时,应该有所不同。