一个由DOM引起的面试题和延伸
1. 如何修改页面内容
页面上有个空无序列表,用<ul></ul>表示,要往列表里插入3个<li>,每个列表项的文本内容是列表项的插入顺序,取值1,2,3。
解答:可以为<ul>节点加上id或者class属性。
<!-- 一般解答 -->
<ul id="list"></ul>
<script>
var container = document.getElementById('list')
item.innerText = i + 1
container.appendChild(item)
</script>
// 一般解答
var container = document.getElementById('list')
var html = []
for (var i = 0; i < 3; i++) {
html.push('<li>' + (i + 1) + '</li>')
}
container.innerHTML = html.join('')
上面的两份代码只能说满足了需求,但是如果做到了以下几点,会有加分:
- 变量命名:节点类的变量,加上
nd前缀,会更加容易辨识,当然,也有同学习惯借用jquery中的$,关于变量命名的更多内容可以去阅读《可读代码的艺术》; - 选择符命名:给 CSS 用和 JS 用的选择符分开,给 JS 用的选择符建议加上
js-或J-前缀,提高可读性,还有没有其他好处,请思考; - 容错能力:应该对节点的存在性做检查,这样代码才能更健壮,实际工作中,很可能你的这段代码会把其他功能搞砸,因为单个地方 JS 报错是可能导致后续代码不执行的,为啥要这样做?不理解的同学可以去看看防御性编程;
- 最小作用域原则:应该把代码段包在声明即执行的函数表达式(IIFE)里,不产生全局变量,也避免变量名冲突的风险,这是维护遗留代码必须谨记的。
针对第一份代码进行修改:
(() => {
var ndContainer = document.getElementById('js-list')
if (!ndContainer) {
return;
}
for (var i = 0; i < 3; i++) {
var ndItem = document.createElement('li')
ndItem.innerText = i + 1
ndContainer.appendChild(ndItem)
}
})();
2. 绑定事件?
问题:要当每个
<li>被单击的时候alert里面的内容,该怎么做?
// ...
for (let i = 0; i < 3; i++) {
const ndItem = document.createElement('li')
ndItem.innerText = i + 1
ndItem.addEventListener('click', function () {
alert(i) // 用alert(this.innerText)可以吗?this指向正确吗
})
ndContainer.appendChild(ndItem)
}
因为 EventListener 里面默认的 this 指向当前节点,比较喜欢使用箭头函数的同学则需要格外注意,因为箭头函数会强制改变函数的执行上下文。
3. 当数据量变大,用事件委托
如果要插入的
<li>是300个,应该用事件委托解决
使用事件委托能有效的减少事件注册的数量,并且在子节点动态增减是无需修改代码的。
(() => {
var ndContainer = document.getElementById('js-list')
if (!ndContainer) {
return
}
for (let i = 0; i < 300; i++) {
const ndItem = document.createElement('li')
ndItem.innerText = i + 1
ndContainer.appendChild(ndItem)
}
ndContainer.addEventListener('click', function (e) {
const target = e.target
if (target.tagName === 'LI') {
alert(target.innerHTML)
}
})
})()
如果要在 <ul> 中插入 30000 个 <li>,会有什么问题?代码需要怎么改进?几乎可以肯定,页面体验不再流畅,甚至会出现明显的卡顿感,该怎么解决?
出现卡顿感的主要原因是每次循环都会修改 DOM 结构,外加大循环执行时间过长,浏览器的渲染帧率(FPS)过低。而实际上,包含 30000 个 <li> 的长列表,用户不会立即看到全部,大部分甚至根本都不会看,那部分都没有渲染的必要,好在现代浏览器提供了 requestAnimationFrame API 来解决非常耗时的代码段对渲染的阻塞问题。
该技术在 React 和 Angular 里面都有使用,如果你理解了 requestAnimationFrame 的原理,就很容易理解最新的 React Fiber 算法。
综合上面的分析,可以从减少 DOM 操作次数、缩短循环时间两个方面减少主线程阻塞的时间。减少 DOM 操作次数的良方是 DocumentFragment;
而缩短循环时间则需要考虑使用分治的思想把 30000 个 <li> 分批次插入到页面中,每次插入的时机是在页面重新渲染之前。由于 requestAnimationFrame 并不是所有的浏览器都支持,Paul Irish 给出了对应的 polyfill
完整代码:
(() => {
const ndContainer = document.getElementById('js-list')
if (!ndContainer) {
return
}
const total = 30000
const batchSize = 4 // 每批插入的节点次数,越大越卡
const batchCount = total / batchSize // 处理次数
let batchDone = 0 // 处理完成个数
function appendItems() {
const fragment = document.createDocumentFragment()
for (let i = 0; i < batchSize; i++) {
const ndItem = document.createElement('li')
ndItem.innerText = (batchDone * batchSize) + i + 1
fragment.appendChild(ndItem)
}
// 每次批处理只修改1次DOM
ndContainer.appendChild(fragment)
batchDone += 1
doBatchAppend()
}
function doBatchAppend() {
if (batchDone < batchCount) {
window.requestAnimationFrame
}
}
doBatchAppend()
ndContainer.addEventListener('click', function (e) {
const target = e.target
if (target.tagName === 'li') {
alert(target.innerHTML)
}
})
})()