问题: 有没有一种高效的方法,给一个Dom元素添加子Dom树?
方法1: 使用document.createElement() -----> dom.appendChild() 一个一个添加,这种方法主要是效率低下,并且每添加一次都要发生回流和重绘,十分影响性能。
方法2: dom.innerHTML() 可以将代表dom子树的一个字符串直接添加的dom上,但是又一个很严重的安全问题,这为XSS跨站脚本攻击提供了便利。
新的解决办法:
1. HTML模板
使用template标签
<template id="foo">
<p> This is p tag in foo div</p>
</template>
这时template标签内的内容不会真的渲染到dom树上,所以也无法通过选择器获取template标签里面的子标签,template里面的内容会自动变成DocumentFragment对象。
<template id="foo">
#documetn-fragment
<p> This is p tag in foo div</p>
</template>
可以通过一下dom.content属性拿到该对象:
const fooEl = document.getElementById('foo')
console.log(fooEl.content);
/* 打印输出:
#documetn-fragment
<p> This is p tag in foo div</p>
*/
可以将通过**.content**拿到的对象进行dom的操作。
奇淫巧技:
可以将script脚本推迟到DocumentFragment的内容被实际添加到DOM树中的时候再执行。方法就是将script标签放到template标签内部。
2. 使用DocumentFragment对象
一下两种方式都创建添加DocumentFragment片段,需要注意的是,为片段添加子元素不会导致回流和重绘。在进行多次dom操作的时候可以有效提升性能。
//const fragment = new DocumentFragment()
const fragment = document.createDocumentFragment() //创建片段的两种方式
for(let color of ['red', 'green', 'pink']){
const pEl = document.createElement('p')
pEl.innerText = `this is ${color}`
fragment.appendChild(pEl) //可以将创建的片段看做是dom元素,但它并不是真正的dom
}
const fooEl = document.getElementById('foo')
fooEl.appendChild(fragment) //将片段挂载到真正的dom上,此时的片段将会被渲染出来
3. 影子DOM
a. 理解影子DOM
通过影子DOM可以将一棵完整的Dom树添加作为节点添加到父DOM树,这样可以实现DOM封装,这意味着css选择符和css样式可以限制在影子DOM子树而不是整个顶级DOM书中。影子DOM与模板template的区别是,影子DOM会实际渲染到DOM树中,而模板不会,但是他们都实现了与主DOM树的一定程度的分离。
<div>
<p>Make me red</p>
</div>
<div>
<p>Make me green</p>
</div>
<div>
<p>Make me pink</p>
</div>
试想一下,有以上三组标签,我们想要将三个不同的p标签渲染成三个不同的颜色,我们应该怎么来做。为每个p标签的style属性写入样式,或者给每个标签定义一个类?这些方法完全可以,但是利用类来定义可能会造成污染,因为这个定义是全局的。这里可以利用影子DOM将对应的属性现在在使用他们的DOM上。
b. 创建影子DOM
并非所有元素都可以创建影子DOM,尝试给无效元素或者已经有了影子DOM的元素添加影子DOM会导致错误。
以下是可以容纳影子DOM的元素:
<article>
<aside>
<blockquote>
<body>
<div>
<footer>
<h1 - h6>
<header>
<main>
<nav>
<p>
<section>
<span>
相关概念:
影子宿主: 容纳影子DOM的元素
影子根: 影子DOM的根节点。
主要方法:
const containerEl = document.getElementById('container')
const conShadow = containerEl.attachShadow({mode: 'open'})
//conShadow 返回的影子实例
//attachShadow({mode: 'open'}) 创建影子实例的方法,参数为一个对象
//这个对象必须具有一个mode属性,属性值为open 或者closed(创建保密影子dom)
c. 使用影子DOM
可以像使用常规DOM一样使用影子DOM。
<body>
<div id="container">
this is container div
</div>
<h1>this is not shadow dom</h1>
</body>
<script>
const containerEl = document.getElementById('container')
for(let color of ['red', 'green', 'pink']){
const divEl = document.createElement('div'), //首先创建一个div标签
shadowEl = divEl.attachShadow({mode: 'open'}) //给创建的div标签添加影子DOM
shadowEl.innerHTML = ` //修改影子DOM里面的内容
<h1>this is ${color}</h1>
<style> //为影子DOM指定局部的样式,不会污染全局
h1{
color: ${color}
}
</style>
`
containerEl.appendChild(divEl) //必须将作为影子宿主的div标签添加到真是的dom元素中去
}
</script>
从结果上看,影子DOM具有局部样式。
d. 合成与影子槽位:
问题:如下,如果影子宿主dom本身内部还有标签时,会出现问题,由于影子DOM优先级要高,所以影子宿主内部的元素不会进行渲染,有时候这可能不是我们想要的结果。
<body>
<div id="container">
this is container div
</div>
</body>
<script>
const containerEl = document.getElementById('container')
const conShadow = containerEl.attachShadow({mode: 'open'})
conShadow.innerHTML = '<h1>This is content in shadow dom</h1>'
</script>
解决办法:
在影子DOM中使用slot标签来指示浏览器在哪里放置原来的HTML。对以上代码做出如下修改
<body>
<div id="container">
this is container div
</div>
</body>
<script>
const containerEl = document.getElementById('container')
const conShadow = containerEl.attachShadow({mode: 'open'})
conShadow.innerHTML = `
<h1>This is content in shadow dom</h1>
<slot></slot> //这里是修改内容,增加了一个slot插槽
`
</script>
浏览器渲染结果:
需要注意的是: 使用插槽相当于做了投射,将影子宿主自己的HTML投射到了影子dom内部,但是被投射的HTML实际上还是影子宿主的字dom树。
e. 使用命名插槽实现多个投射
<body>
<div id="container">
<!--给p标签指定了一个slot属性,并且赋值 -->
<p slot="first">This is the first p tag</p> <!-- -->
<p slot="second">This is the second p tag</p>
</div>
</body>
<script>
const containerEl = document.getElementById('container')
const conShadow = containerEl.attachShadow({mode: 'open'})
conShadow.innerHTML = `
<slot name="first"></slot> //使用name属性告诉应该放哪个p标签
<h1>This is content in shadow dom</h1>
<slot name="second"></slot>
`
</script>
渲染结果:
f 事件重定向
影子DOM上发生的事件会逃出影子DOM并经过事件重定向在影子宿主上被处理,就好像事件就发生在影子宿主本身上一样。通过slot插槽从外部投射进影子DOM的元素上的事件不会发生事件重定向。因为他仅仅就是做了投射,元素本身还是在原来的位置上。