JavaScript DOM 操作
JavaScript 操作 DOM 的实践层:节点查询、创建、修改、事件挂接与动态脚本加载;DOM 模型本身由 canonical DOM 笔记维护。
#concept
#growing
#javascript
#web
#browser
[!info] related notes
- 所属 MOC: 浏览器 JS MOC, JavaScript MOC
- 上位模型: DOM(文档对象模型)
- 上位主题: 浏览器中的 JavaScript
- 相邻主题: JavaScript BOM, javascript-events, Window 对象
- 渲染关联: 浏览器渲染流程, HTML 中的 JS 脚本
JavaScript DOM 操作
本页聚焦“JavaScript 代码怎样操作 DOM”。DOM 的节点模型、树关系与平台级边界统一维护在 DOM(文档对象模型),这里不再重复把 DOM 当成本页别名。
边界:平台模型 vs JavaScript 使用层
- 想理解
Document、Node、Element的概念关系,先看 DOM(文档对象模型); - 想写
querySelector()、createElement()、append()或动态<script>加载,留在本页; - 想理解事件捕获/冒泡,转到 javascript-events / 前端事件传播;
- 想分析 DOM 变更如何进入 style/layout/paint,转到 浏览器渲染流程。
它解决什么问题
- 让 JavaScript 可以查询页面结构
- 让 JavaScript 可以创建、插入、删除、替换节点
- 让脚本可以读取和修改文本、属性、样式、类名
- 让事件系统有明确的目标节点和传播路径
先抓住几个核心对象
document: 当前页面文档入口Node: 所有 DOM 节点的共同基类Element: 元素节点,最常见的 DOM 操作对象Text: 文本节点DocumentFragment: 批量拼装节点时常用的轻量容器
常见操作分组
查
getElementById()querySelector()/querySelectorAll()children/childNodes
改
textContent: 改纯文本,通常比innerText更直接innerHTML: 改 HTML 片段,方便但要注意 XSSsetAttribute()/classList/style
增删
createElement()append()/appendChild()remove()/removeChild()replaceWith()
动态加载 script 并在完成后执行回调
这是 DOM 操作里很常见的一道面试题,本质是:
- 创建
script元素 - 设置
src - 挂载
onload/onerror - 再插入到文档里
function loadScript(src, callback) {
const script = document.createElement('script')
script.src = src
script.onload = () => {
callback(null)
}
script.onerror = () => {
callback(new Error(`failed to load: ${src}`))
}
document.head.appendChild(script)
}
调用方式:
loadScript('/sdk.js', (error) => {
if (error) {
console.error(error)
return
}
console.log('script loaded')
})
更现代的写法:Promise 包装
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = src
script.onload = () => resolve(script)
script.onerror = () => reject(new Error(`failed to load: ${src}`))
document.head.appendChild(script)
})
}
loadScript('/sdk.js')
.then(() => {
console.log('loaded')
})
.catch((error) => {
console.error(error)
})
加上去重、记忆
const scriptCache = new Map<string, Promise<void>>();
const loadedSet = new Set<string>();
function loadScript(url: string): Promise<void> {
if (loadedSet.has(url)) {
return Promise.resolve();
}
const cached = scriptCache.get(url);
if (cached) {
return cached;
}
const p = new Promise<void>((resolve, reject) => {
const existed = document.querySelector(`script[data-src="${url}"]`) as HTMLScriptElement | null;
if (existed) {
if ((existed as any).__loaded__) {
loadedSet.add(url);
resolve();
return;
}
existed.addEventListener('load', () => {
(existed as any).__loaded__ = true;
loadedSet.add(url);
resolve();
}, { once: true });
existed.addEventListener('error', () => {
scriptCache.delete(url);
reject(new Error(`Failed to load script: ${url}`));
}, { once: true });
return;
}
const script = document.createElement('script');
script.src = url;
script.async = true;
script.setAttribute('data-src', url);
script.onload = () => {
(script as any).__loaded__ = true;
loadedSet.add(url);
resolve();
};
script.onerror = () => {
scriptCache.delete(url);
reject(new Error(`Failed to load script: ${url}`));
};
document.head.appendChild(script);
});
scriptCache.set(url, p);
return p;
}
箭头函数和普通函数在这里怎么替换
如果这里只是把回调交给事件或 Promise,箭头函数和普通匿名函数在功能上是等价的。
function loadScript(src) {
return new Promise(function (resolve, reject) {
const script = document.createElement('script')
script.src = src
script.onload = function () {
resolve(script)
}
script.onerror = function () {
reject(new Error(`failed to load: ${src}`))
}
document.head.appendChild(script)
})
}
loadScript('/sdk.js')
.then(function () {
console.log('loaded')
})
.catch(function (error) {
console.error(error)
})
这里通常不需要 IIFE。
onload/onerror/.then()/.catch()需要的是“先注册,等以后再调用”的回调函数- IIFE 是“定义完立刻执行”
- 如果把这里写成立即执行,就会在绑定阶段直接运行,破坏异步时机
错误示意:
script.onload = (function () {
resolve(script)
})()
上面会立刻执行 resolve(script),而不是等脚本真正加载完成。
面试里要顺手提到的边界
- 要监听加载成功和失败
- 如果脚本依赖执行顺序,要注意不要乱开
async - 如果脚本只该加载一次,要做重复加载保护
- 如果是第三方 SDK,还要考虑超时、幂等和全局变量是否已注入
容易混淆的边界
- DOM 关注文档树;BOM 关注窗口、地址栏、历史记录等浏览器宿主对象,见 JavaScript BOM
- DOM 改动常常会影响布局与绘制,见 浏览器渲染流程
- 事件绑定到 DOM 节点上之后,传播规则看 javascript-events
学习检查
- 为什么
DOM应由 DOM(文档对象模型) 作为 canonical 概念,而本页只保留 JavaScript 使用层? querySelectorAll()返回的集合与 liveHTMLCollection在后续 DOM 变化时有什么行为差异?- 动态加载第三方脚本时,为什么需要同时考虑成功、失败、重复加载和执行顺序?
- 为什么 React/Vue 这类框架减少直接 DOM 操作,却没有让 DOM 本身消失?
- 一个“点击事件为什么从子节点冒泡到父节点”的问题,应该在哪个知识节点继续追?
实践上的一句话
框架不会让 DOM 消失,只是把直接 DOM 操作收敛成更高层的数据更新与渲染过程。