跳到主要内容

dom

节点层级

基础

层级结构
  • 通过 DOM 表示 HTML 的一个层级结构;
  • html 中每个元素对应层级结构中的一个节点;
  • DOM 总共有 12 种类型节点;
固定节点
  • 每个文档具有一个 document 根节点;
  • document 根节点具有唯一的 html 节点,称作 documentElement;

Node 类型

Node
  • 所有类型节点继承 Node 类型;
节点属性
  • node.nodeType 属性获取节点类型;
  • node.nodeName 属性存储节点对应元素标签名;
  • node.nodeValue 属性获取节点值;
if (someNode.nodeType == Node.ELEMENT_NODE) {
console.log("Node is an element.");
}
// 节点类型常量
Node.ELEMENT_NODE(1);
Node.ATTRIBUTE_NODE(2);
Node.TEXT_NODE(3);
Node.CDATA_SECTION_NODE(4);
Node.ENTITY_REFERENCE_NODE(5);
Node.ENTITY_NODE(6);
Node.PROCESSING_INSTRUCTION_NODE(7);
Node.COMMENT_NODE(8);
Node.DOCUMENT_NODE(9);
Node.DOCUMENT_TYPE_NODE(10);
Node.DOCUMENT_FRAGMENT_NODE(11);
Node.NOTATION_NODE(12);

if (someNode.nodeType == 1) {
const value = someNode.nodeName;
}
节点关系
  • node.childNodes 属性存储子节点数组;
  • node.parentNode 属性存储父节点;
  • node.previousSibling 属性表示前一个同级节点,第一个其值为 null;
  • node.nextSibling 属性表示后一个同级节点,最后一个其值为 null;
  • node.fistChild 属性表示第一个子节点;
  • node.lastChild 属性表示最后一个子节点;
  • hasChildNodes():boolean 方法判断是否具有子节点;
const firstChild = someNode.childNodes[0]; // 使用索引访问
const secondChild = someNode.childNodes.item(1); // 使用 item() 访问
const count = someNode.childNodes.length; // 获取子节点数组长度

const parentNode = node.parentNode;
const previousNode = node.previousSibling;
const nextNode = node.nextSibling;
const fistChild = node.fistChild;
const lastChild = node.lastChild;

if (node.hasChildNodes()) {
const childNodes = node.childNodes;
}
操作节点
  • appendChild():添加;
  • insertBefore():插入;
  • replaceChild():替换;
  • removeChild():移除;
  • cloneNode():克隆;
// 末尾添加子节点
const returnedNode = someNode.appendChild(newNode);
// 插入到指定节点的前一个位置
const returnedNode = someNode.insertBefore(newNode, null); // 插入到末尾
const returnedNode = someNode.insertBefore(newNode, someNode.firstChild); // 插入到开头
// 替换节点
const returnedNode = someNode.replaceChild(newNode, someNode.firstChild); // 替换第一个节点
// 移除节点
let formerFirstChild = someNode.removeChild(someNode.firstChild); // 移除第一个节点
// 复制节点
const shallowList = myList.cloneNode(); // 浅复制, 仅复制节点
const deepList = myList.cloneNode(true); // 深复制, 复制节点及其子树

Document 类型

Document 类型
  • 表示整个 HTML 界面;
  • nodeType 为 9;
  • nodeName 为 #document
子节点属性
  • documentElement 属性恒指向 html 标签;
  • body 属性恒指向 body 标签;
const html = document.documentElement;
const body = document.body;
文档信息属性
  • title 属性表示 title 标签,只读;
  • URL 属性表示完整 URL,只读;
  • domain 属性表示域名,读写,但只能为 URL 的字串;
const originalTitle = document.title;
const url = document.URL;
const domain = document.domain;

Element 类型

Element 类型
  • nodeType 为 1;
  • nodeName 为标签名(全大写);
HTMLElement 类型
  • 属性可读写;
  • 具有一堆类型;
<div id="myDiv" class="bd" title="Body text" lang="en" dir="ltr"></div>;
let div = document.getElementById("myDiv");
console.log(div.id); // "myDiv"
console.log(div.className); // "bd"
div.id = "someOtherId";
div.className = "ft";
操作属性
  • getAttribute():获取属性对应字符串;
  • setAttribute():设置属性;
  • removeAttribute():移除属性;
const div = document.getElementById("myDiv");
console.log(div.getAttribute("class")); // "bd"
console.log(div.className); // "bd"
div.setAttribute("id", "someOtherId");
div.removeAttribute("class");
attributes 属性
  • 一般不使用 attributes 属性;
  • attributes 属性指向 NamedNodeMap 示例,存储 Attr 节点;
  • nodeName 为属性名,nodeValue 为属性值;
// 获取 id 属性节点
const id = element.attributes.getNamedItem("id");
const id = element.attributes["id"];
// 设置 id 属性
id.nodeValue = "someOtherId";
// 删除 id 属性节点
const oldAttr = element.attributes.removeNamedItem("id");
// 迭代属性节点
for (let i = 0, len = element.attributes.length; i < len; ++i) {
const attribute = element.attributes[i];
console.log(`${attribute.nodeName}="${attribute.nodeValue}"`);
}
创建元素
const div = document.createElement("div");
div.id = "myNewDiv";

Text 类型

Text 类型
  • nodeType 为 3;
  • nodeName 为 #text
  • nodeValue 为节点中的文本;
操作文本
  • length 属性获取字符数量;
  • appendData(text) 添加文本;
  • deleteData(offset,count) 从 offset 开始删除 count 个字符;
  • insertData(offset,text,在 offset 插入 text;
  • replaceData(offset,count,text) 用 text 替换 [offset,offset + count) 的文本;
  • splitText(offset) 在 offset 拆分文本,第一个包含 offset;
  • substringData(offset,count) 提取 [offset,offset + count) 的文本;
创建文本节点
  • createTextNode()
const textNode = document.createTextNode("<strong>Hello</strong> world!");
文本节点数量
  • 文本内容的每个元素规定最多只能有一个文本节点;
  • 但可以存在多个文本节点;
合并和拆分文本节点
let element = document.createElement("div");
let textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
let anotherTextNode = document.createTextNode("Yippee!");
element.appendChild(anotherTextNode);
document.body.appendChild(element);
console.log(element.childNodes.length); // 2
element.normalize();
console.log(element.childNodes.length); // 1
console.log(element.firstChild.nodeValue); // "Hello world!Yippee!"
// 在 offset 位置拆分, 第一个包含 offset
let newNode = element.firstChild.splitText(5);
console.log(element.firstChild.nodeValue); // "Hello"
console.log(newNode.nodeValue); // " world!Yippee!!"
console.log(element.childNodes.length); // 2

Comment 类型

Comment 类型
  • nodeType 为 8;
  • nodeName 为 #comment
  • nodeValue 为注释内容;
与 Text 类型的关系
  • 两者继承于同一个基类;
  • 除 splitText() 之外;
  • Comment 类型和 Text 类型别无二致;

CDATASection 类型

CDATASection 类型
  • nodeType 为 8;
  • nodeName 为 #cdata-section
  • nodeValue 为 CDATA 区块的内容;
与 Text 类型的关系
  • 两者继承于同一个基类;
  • Comment 类型和 Text 类型别无二致;

DocumentType 类型

DocumentType 类型
  • nodeType 为 10;
  • nodeName 为文档类型的名称;
  • parentNode 值为 Document 对象;

DocumentFragment 类型

DocumentFragment 类型
  • nodeType 为 11;
  • nodeName 为 #document-fragment

Attr 类型

Attr 类型
  • nodeType 为 2;
  • nodeName 为属性名;
  • nodeValue 为属性值;
  • 一般不使用,推荐使用 getAttribute(),removeAttribute()和 setAttribute()方法

DOM 编程

动态脚本

  • 通过动态创建 script 标签;
let script = document.createElement("script");
script.src = "foo.js";
document.body.appendChild(script);

动态样式

  • 通过动态创建 link 标签;
let link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = "styles.css";
let head = document.getElementsByTagName("head")[0];
head.appendChild(link);

操作表格

table API
  • caption:指向 caption 元素的指针(如果存在);
  • tBodies:包含 tbody 元素的 HTMLCollection;
  • tFoot:指向 tfoot 元素(如果存在);
  • tHead:指向 thead 元素(如果存在);
  • rows:包含表示所有行的 HTMLCollection;
  • createTHead():创建 thead 元素,放到表格中,返回引用;
  • createTFoot():创建 tfoot 元素,放到表格中,返回引用;
  • createCaption():创建 caption 元素,放到表格中,返回引用;
  • deleteTHead():删除 thead 元素;
  • deleteTFoot():删除 tfoot 元素;
  • deleteCaption():删除 caption 元素;
  • deleteRow(pos):删除给定位置的行;
  • insertRow(pos):在行集合中给定位置插入一行;
tbody API
  • rows:包含 tbody 元素中所有行的 HTMLCollection;
  • deleteRow(pos):删除给定位置的行;
  • insertRow(pos):在行集合中给定位置插入一行,返回该行的引用;
tr API
  • cells:包含 tr 元素所有表元的 HTMLCollection;
  • deleteCell(pos):删除给定位置的表元;
  • insertCell(pos):在表元集合给定位置插入一个表元,返回该表元的引用;

NodeList 对象

  • NodeList 实时反应 DOM 结构;
  • 下列代码会永久循环;
let divs = document.getElementsByTagName("div");
for (let i = 0; i < divs.length; ++i) {
let div = document.createElement("div");
document.body.appendChild(div);
}

MutationObserver 接口

基础

MutationObserver 接口
  • DOM 变化时异步执行回调;
创建实例
let observer = new MutationObserver(() => console.log("DOM was mutated!"));
操作 DOM
  • 回调函数参数;
    • mutationRecords 包含按顺序入队的 MutationRecord 实例的数组;
    • mutationObserver 为 observer 实例;
  • observe() 观察具体 DOM;
    • 第一个参数为观察标签,body 标签发生变化变执行 MutationObserver 回调
    • 第二个参数为 MutationObserverInit 对象;
  • disconnect() 取消 observer();
    • disconnect() 并不会停止 observer,之后可以重新观察;
let observer = new MutationObserver((mutationRecords, mutationObserver) =>
console.log(mutationRecords)
); // 创建示例

observer.observe(document.body, { attributes: true }); // 观察 body

let observer = new MutationObserver(() =>
console.log("<body> attributes changed")
);
observer.observe(document.body, { attributes: true });
document.body.className = "foo";
setTimeout(() => {
observer.disconnect();
}, 0); // 通过 setTImeout(), 保证回调函数先执行一次

// 复用 MutationObserver
let observer = new MutationObserver((mutationRecords) =>
console.log(mutationRecords.map((x) => x.target))
); // 通过 target 属性辨别不同观察对象
observer.observe(childA, { attributes: true });
observer.observe(childB, { attributes: true });
observer.disconnect(); // 停止所有观察对象
MutationRecord 属性
  • target:目标节点;
  • type:变化类型;
  • oldValue:变化前的值;
  • attributeName:修改后的属性;
  • addedNodes:添加的节点及其子节点;
  • removedNodes:删除的节点及其子节点;
  • previousSibling:变化节点的前一个同级节点;
  • nextSibling:变化节点的后一个同级节点;

MutationObserverInit

MutationObserverInit 对象
  • 控制 MutationObserver 的观察范围;
// attributes 默认为 false, 表示是否观察属性变化
observer.observe(document.body, { attributes: true });
// attributeFilter 默认为所有属性, 表示观察属性范围
observer.observe(document.body, { attributeFilter: ["foo"] });
// attributeOldValue 默认为 false, 表示是否保存变化前的值
observer.observe(document.body, { attributeOldValue: true });
// characterData 默认为 false, 表示否是观察文字节点修改
observer.observe(document.body.firstChild, { characterData: true });
// characterDataOldValue 默认为 false, 表示是否保存文字节点变化前的值
observer.observe(document.body.firstChild, { characterDataOldValue: true });
// childList 默认为 false, 表示是否观察子节点变化
observer.observe(document.body, { childList: true });
// subtree 默认为 false, 表示是否观察子树变化
observer.observe(document.body, { attributes: true, subtree: true });

元素查询和元素遍历

元素查询

机制
查询单个元素
  • querySelector();
    • 通过 css 选择符匹配 DOM 元素;
    • 返回第一个满足条件的 DOM 元素;
    • 未找到返回 null;
// 取得<body>元素
let body = document.querySelector("body");
// 取得 ID 为"myDiv"的元素
let myDiv = document.querySelector("#myDiv");
// 取得类名为"selected"的第一个元素
let selected = document.querySelector(".selected");
// 取得类名为"button"的图片
let img = document.body.querySelector("img.button");
查询所有元素
  • querySelectorAll():返回所有元素的 NodeList (ArrayLike);
// 取得 ID 为"myDiv"的<div>元素中的所有<em>元素
let ems = document.getElementById("myDiv").querySelectorAll("em");
// 取得所有类名中包含"selected"的元素
let selecteds = document.querySelectorAll(".selected");
// 取得所有是<p>元素子元素的<strong>元素
let strongs = document.querySelectorAll("p strong");
匹配元素
  • matches():匹配成功返回 true,反之返回 false;
if (document.body.matches("body.page1")) {
// true
}

元素遍历

dom 元素遍历相关属性
  • childElementCount:子元素数量 (不包含文本节点);
  • firstElementChild:第一个 Element 类型的子元素;
  • lastElementChild:最后一个 Element 类型的子元素;
  • previousElementSibling:前一个 Element 类型的同级元素;
  • nextElementSibling:后一个 Element 类型的同级元素;

html5

css 类拓展

getElementsByClassName()
  • 返回对应 className 的所有节点 NodeList (ArrayLike);
let allCurrentUsernames = document.getElementsByClassName("username current");
classList 属性
  • remove():删除;
  • add():添加;
  • toggle():切换;
  • contain():检测;
  • for of:迭代;
// 删除"disabled"类
div.classList.remove("disabled");
// 添加"current"类
div.classList.add("current");
// 切换"user"类
div.classList.toggle("user");
// 检测类名
if (div.classList.contains("bd") && !div.classList.contains("disabled")){
// 执行操作
}
// 迭代类名
for (let class of div.classList){
doStuff(class);
}

焦点管理

document.activeElement 属性
  • 始终为当前拥有焦点的 DOM 元素;
let button = document.getElementById("myButton");
button.focus();
console.log(document.activeElement === button); // true
document.hasFocus()
  • 检测 document 是否具有焦点;
let button = document.getElementById("myButton");
button.focus();
console.log(document.hasFocus()); // true

滚动

  • scrollIntoView();
// 顶部对齐
document.forms[0].scrollIntoView(); // alignToTop 默认 true
document.forms[0].scrollIntoView(true);
document.forms[0].scrollIntoView({ block: "start" }); // block 默认为 start
// 底部对齐
document.forms[0].scrollIntoView(false);
document.forms[0].scrollIntoView({ block: "end" });
// 平滑滚动
document.forms[0].scrollIntoView({ behavior: "smooth", block: "start" }); // behavior 默认为 auto

HTMLDocument 拓展

readyState 属性
// 表示文档是否加载完成, 有 loading 和 complete 两种值
if (document.readyState == "complete") {
// 执行操作
}
compatMode 属性
// 表示 html 渲染模式
if (document.compatMode == "CSS1Compat") {
console.log("Standards mode");
} else {
console.log("Quirks mode");
}
head 属性
// 指向 <head> 元素
let head = document.head;

字符集属性

console.log(document.characterSet); // "UTF-16"
document.characterSet = "UTF-8";

自定义数据属性

语法格式
// 使用 data- 前缀
// 通过 data- 后面的字符串访问
<div id="myDiv" data-appId="12345" data-myname="Nicholas"></div>

插入标记

innerHTML 属性
// 读取时 DOM 子树的 HTML 字符串
// 写入时字符串作为 DOM 子树解析
div.innerHTML = "Hello & welcome, <b>\"reader\"!</b>"
// 等效于下列代码
<div id="content">Hello &amp; welcome, <b>&quot;reader&quot;!</b></div>
outerHTML 属性
// 读取时返回调用元素及其所有后代元素的 HTML 字符串
// 写入时字符串作为 DOM 子树解析, 并替换调用元素
div.outerHTML = "<p>This is a paragraph.</p>";
// 等效于下列代码
let p = document.createElement("p");
p.appendChild(document.createTextNode("This is a paragraph."));
div.parentNode.replaceChild(p, div);
insertAdjacentHTML()与 insertAdjacentText()
// 作为前一个同胞节点插入
element.insertAdjacentHTML("beforebegin", "<p>Hello world!</p>");
element.insertAdjacentText("beforebegin", "Hello world!");
// 作为第一个子节点插入
element.insertAdjacentHTML("afterbegin", "<p>Hello world!</p>");
element.insertAdjacentText("afterbegin", "Hello world!");
// 作为最后一个子节点插入
element.insertAdjacentHTML("beforeend", "<p>Hello world!</p>");
element.insertAdjacentText("beforeend", "Hello world!");
// 作为下一个同胞节点插入
element.insertAdjacentHTML("afterend", "<p>Hello world!</p>");
element.insertAdjacentText("afterend", "Hello world!");
内存问题
  • 若存在移除 DOM 元素的引用;
  • 移除 DOM 元素不会垃圾回收;
安全问题
  • innerHTML 会导致 XSS 攻击;

样式

存取元素样式

style 属性
// css 属性 js 都有对应的 style 属性
let myDiv = document.getElementById("myDiv");
// 设置背景颜色
myDiv.style.backgroundColor = "red";
// 修改大小
myDiv.style.width = "100px";
myDiv.style.height = "200px";
操作 style 属性
// 读写对应的 css 代码
myDiv.style.cssText = "width: 25px; height: 100px; background-color: green";
// 获取 css 属性值
value = myDiv.style.getPropertyValue(prop);
// 删除 css 属性
myDiv.style.removeProperty("border");
// 迭代 css 属性
for (let i = 0, len = myDiv.style.length; i < len; i++) {
console.log(myDiv.style[i]); // 或者用 myDiv.style.item(i)
}

操作样式表

操作样式表
// 遍历样式表
let sheet = null;
for (let i = 0, len = document.styleSheets.length; i < len; i++) {
sheet = document.styleSheets[i];
console.log(sheet.href);
}
操作 css 规则
// 操作 css 规则
let sheet = document.styleSheets[0];
let rules = sheet.cssRules || sheet.rules; // 取得规则集合
let rule = rules[0]; // 取得第一条规则
console.log(rule.selectorText); // "div.box"
console.log(rule.style.cssText); // 完整的 CSS 代码
console.log(rule.style.backgroundColor); // "blue"
console.log(rule.style.width); // "100px"
console.log(rule.style.height); // "200px"
// 创建 css 规则
sheet.insertRule("body { background-color: silver }", 0); // 使用 DOM 方法
// 删除 css 规则
sheet.deleteRule(0);

getComputedStyle

  • 获得指定元素最终使用的 css;
    • 第一个参数为指定元素;
    • 第二个参数可选,为指定伪元素;
const box = document.getElementById("box");
const style = window.getComputedStyle(box, "after");

const height = style.getPropertyValue("height");
const width = style.getPropertyValue("width");

元素尺寸

元素尺寸

偏移尺寸
  • 元素占用的视觉空间,只读属性;
  • 可视 content + padding + border;
  • ltrb 为对于 border 到父元素的偏移值;
const left = element.offsetLeft;
const top = element.offsetTop;
const height = element.offsetHeight;
const width = element.offsetWidth;
客户端尺寸
  • 元素可见内容空间,只读属性;
  • 可视 content + padding;
  • ltrb 为对应 border 宽度;
const height = element.clientHeight;
const width = element.clientWidth;
滚动尺寸
  • 元素总可见内容空间,只读属性;
  • 总 content + padding;
  • ltrb 为可见内容控件的偏移量;
const left = element.scrollLeft;
const top = element.scrollTop;
const height = element.scrollHeight;
const width = element.scrollWidth;
客户端存储和滚动尺寸的关系
  • 没有滚动条时,两者相等;
  • 具有滚动条时,滚动偏移量 + 客户端尺寸 = 滚动尺寸;
getBoundingClientRect
  • 返回 DOMRect 对象;
  • 提供元素位置和尺寸;
const box = document.getElementById("box");
const rect = box.getBoundingClientRect();

console.log(rect.x); // 元素左边界相对于视口的 x 坐标
console.log(rect.y); // 元素上边界相对于视口的 y 坐标
console.log(rect.width); // 元素的宽度
console.log(rect.height); // 元素的高度
console.log(rect.top); // 元素上边界相对于视口顶部的距离
console.log(rect.right); // 元素右边界相对于视口左侧的距离
console.log(rect.bottom); // 元素下边界相对于视口顶部的距离
console.log(rect.left); // 元素左边界相对于视口左侧的距离

getBoundingClientRect

遍历

NodeIterator
// 深度优先遍历
let iterator = document.createNodeIterator(root);
let node = iterator.nextNode(); // 下一个节点
while (node !== null) {
node = iterator.nextNode();
}
node = iterator.previousNode();
while (node !== null) {
node = iterator.previousNode(); // 上一个节点
}
TreeWalker
// 深度优先遍历, NodeIterator 的高级版
let iterator = document.createTreeWalker(root);
let node = iterator.nextNode(); // 下一个节点
node = iterator.previousNode(); // 上一个节点
// 以下 API 提供定位能力
node = iterator.parentNode(); // 遍历至当前节点的父节点
node = iterator.firstChild(); // 遍历至当前节点的第一个子节点
node = iterator.lastChild(); // 遍历至当前节点的最后一个子节点
node = iterator.nextSibling(); // 遍历至当前节点的上一个同级节点
node = iterator.previousSibling(); // 遍历至当前节点的下一个同级节点

范围

DOM 范围

// 创建 DOM 范围对象
let range = document.createRange();

简单选择

range1.selectNode(p1); // 整个节点
range2.selectNodeContents(p1); // 子节点

IntersectionObserver

  • 异步监听多个目标元素与视口的交叉状态;
  • entry 属性;
    • target:观测元素;
    • intersectionRatio:交叉比例 (0-1);
    • isIntersecting:是否相交;
    • intersectionRect:交叉区域的位置和尺寸信息;
  • options;
    • root:观察器的根元素,默认为 viewpoint;
    • rootMargin:root 的外边距;
    • threshold:一组交叉比例的阈值,超过一个阈值触发 callback;
// 创建
const observer = new IntersectionObserver(callback, options);
const callback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// 元素进入视口
} else {
// 元素离开视口
}
});
};
const options = {
// 可选配置
};

// 监听
const target = document.querySelector("#targetElement");
observer.observe(target);

最佳实践

判断元素是否在可视区域

基本原理
  • 使用 getBoundingClientRect() API;
    • 判断 top/left/bottom/right 和 window.innerHeight/ window.innerWidth 的关系;
  • 使用 IntersectionObserver() API;
部分包含
  • getBoundingClientRect();
    • top > 0 且小于 window.innerHeight,left 同理
  • IntersectionObserver();
    • intersectionRatio 为 (0,1);
完全包含
  • getBoundingClientRect();
    • top > 0 且 bottom < window.innerHeight,left/right 同理
  • IntersectionObserver();
    • intersectionRatio 为 1;

上拉加载和下拉刷新

上拉加载
  • 上拉加载即当前页面触底的动作;
  • 判断出发上拉加载的标签的位置;
  • 设置一个触发上拉加载的阈值 distance;
  • 如果其 scrollTop + clientHeight >= scrollHeight - distance,触发对应操作;
if (scrollTop + clientHeight >= scrollHeight - distance) {
console.log("开始加载数据");
}
下拉刷新
  • 页面处于顶部,用户进行下拉刷新;
  • 监听 touchstart,touchmove 和 touchend 事件;
  • touchstart 记录 y 轴起始位置;
  • touchmove 记录 y 轴移动方向;
    • 如果向下移动,element 使用 translateY 移动相同距离;
  • touchend 触发下拉刷新事件,element translateY 设置为 0;

大数据显示

直接渲染
  • 不推荐;
setTimeout 分页渲染
  • 首页将数据分为若干页;
  • 使用 setTimeout 逐页渲染;
const render = (page) => {
if (page >= totalPage) return;
setTimeout(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i];
const div = document.createElement("div");
div.className = "sunshine";
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`;
container.appendChild(div);
}
render(page + 1);
}, 0);
};
render(page);
requestAnimationFrame
  • 即将 setTimeout 替换为 requestAnimationFrame;
df + requestAnimationFrame
  • 文档碎片 + requestAnimationFrame;
    • 减少重排和重绘;
    • 在内存中进行运算,减少对真实 DOM 的操作;
懒加载
  • 首先进行分页,在页尾放置一个空节点;
  • 使用 getBoundingClientRect 或 IntersectionObserver 观察空节点是否在视图内;
  • 若出现在视图内,在渲染下一页内容;
虚拟列表
  • 见虚拟列表;

虚拟列表

虚拟列表
  • 根据可视区域和列表内容;
  • 监听用户滚动事件,动态截取部分列表内容渲染到页面上;
实现机制
  • 固定可视区高度 showHeight 和列表子项高度 itemHeight;
  • 计算起始位置和终止位置索引;
    • 计算索引时可设置一个缓存值,避免滑动效果卡顿;
    • 计算可视区列表起始索引 start;
      • start = Math.floor(scrollTop/itemHeight) - buffer;
    • 计算可视区列表终止索引 end;
      • end = start + Math.ceil(showHeight/itemHeight) + buffer;
  • 监听滚动事件,根据起始和终止索引,渲染部分列表内容;
    • 列表元素使用 absolute 布局;
    • 根据 startIndex 设置 top;
    • 对应回调函数使用节流机制,避免频繁渲染;

实现机制