0%

操作 Dom 的成本很高 不要轻易去操作 Dom 这句话从开始入门就听说,那么这里说的成本是指什么?
由此引出今天的问题

首先我们要清楚几个概念

什么是 DOM?

  • DOM 全称 Document Object Model 文档对象模型
  • 它是为 HTML(XML)提供的 API
  • HTML 是一种标记语言 HTML 在 DOM 模型标准中被视为对象
  • DOM 只提供编程接口却无法实际操作 HTML 里面的内容
  • 在浏览器中 前端工程师可以通过脚本语言(js)通过 DOM 去操作 HTML 内容
    (不只 js 能调用 DOM 这个 API Python 也可以)
  • ps:也存在 CSSOM:CSS Object Model 浏览器将 CSS 代码解析成树形的数据结构与 DOM 是两个独立的数据机构

浏览器渲染过程

讨论 DOM 操作成本 首先要了解下该成本的来源 那么就离不开浏览器渲染
浏览器渲染前需要先构建 DOM 和 CSS 树 因此我们需要尽快将 HTML 和 CSS 都提供给浏览器

这里只讨论浏览器拿到 HTML 之后开始解析 渲染

之前的一些另开一篇

  1. 解析 HTML 构建 DOM 树 (这里遇到外链 会发起请求)
  2. 解析 CSS 生成 CSS 规则树
  3. 合并 DOM 树和 CSS 规则 生成 render(渲染)树
  4. 布局 render 树(Layout/reflow)负责各元素的尺寸,位置的计算
  5. 绘制 render 树(paint)绘制页面像素信息
  6. 浏览器会将各层的信息发送给 GPU GPU 将各层合成 (composite) 显示在屏幕上
    构建 DOM 树
    HTML 标记转换成文档对象模型 (DOM)
    DOM 树构建过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点
    构建 CSSOM 树
    CSS 标记转换成 CSS 对象模型 (CSSOM)
    在最终计算各个 2 节点的样式时 浏览器都会先从该节点的普遍属性(比如全局样式)开始 再去应用该节点的具体属性

每个浏览器都有自己的默认样式表因此很多时候这颗 CSSOM 树只是对这张默认样式表的部分替换

DOM 和 CSSOM 都要经过
Bytes→characters→tokens→nodes→objectmodel 这个过程
DOM 和 CSSOM 是独立的数据结构
此处需要一张图片

生成 render(渲染)树 由此 浏览器中会解析并生成两个内部数据结构
  • Dom 树表示页面结构
  • DOM 树和 CSSOM 合并生成 render 树(渲染树),渲染树表示 Dom 节点在页面中如何显示(宽高 位置等)
1
在dom树中每一个需要显示的节点在渲染树种至少存在一个对应的节点 渲染树中的节点被称之为“帧”或者“盒” 符合css模型的定义 一旦Dom树和渲染树构建完成  浏览器就开始 显示(绘制paint)页面元素

简单描述下 render 的过程

1
2
3
DOM树从根节点开始遍历可见节点
设置了类似 display:none (则该节点不可见) 在render过程中是被跳过的
visibility:hidden; opacity:0 这种仍旧占据空间的节点不会被跳过render 保存各个节点的样式信息及其余节点的从属关系
1
2
3
Layout布局
有了各个节点的信息属性 但不知道各个节点的确切位置和大小 所以要通过布局将样式信息和属性转换为实际可视窗口的相对大小和位置
(DOM 树捕获文档标记的属性和关系,但并未告诉我们元素在渲染后呈现的外观。那是 CSSOM 的责任)
1
2
Paint绘制
最后只要将确定好位置大小的各节点通过GPU渲染到屏幕的实际像素

TIPS:

  • 在上述渲染过程中 前三点可能要多次执行 比如 js 脚本去操作 DOM 更改 CSS 样式 浏览器又要重新构建 DOM CSSOM 树 重新 render 重新 layout paint
  • 因为 layout 在 paint 之前 因此每次 layput 重新布局(reflow 回流)后都要重新触发 paint 渲染 这时又要去消耗 GPU
  • paint 不一定会触发 layout 比如改个颜色改个背景(repaint 重绘)
  • 图片下载完也会重新触发 Layout 和 paint
何时触发 reflow(重排)和 repaint(重绘)

reflow (重排):当 dom 树的变化影响了元素的集合属性 =》 意味着元素的内容,结构 位置或者尺寸发生了变化,同样其他元素的集合属性和位置也会因此受到影响,浏览器会使渲染树(render 树)中受到影响的部分失效 需要重新计算样式和渲染树,这个过程称为重排(reflow)

repaint (重绘): 意味着元素发生的改变只你影响了节点的一些样式(背景色 边框颜色 文字元素等)只需要应用新样式绘制这个元素就可以了 (完成重排后 浏览器会重新绘制受影响的部分到屏幕中 这个过程叫做重绘)

并不是所有的 dom 辩护都会影响几何属性 例如 改变元素的背景色不会影响 宽和高 这种情况下 只会发生一次重绘(不需要重排)因为元素的布局没有改变

重排一定会引起浏览器的重绘 重绘则不一定伴随重排

重排的成本开销要高于重绘一个节点的重排往往导致子节点以及同级节点的重排

触发重排的情况

当页面布局的几何属性改变时就需要重排 下列情况会导致重排

1
2
3
4
5
6
7
8
9
页面第一次渲染(初始化)

DOM树变化(如:增删节点)
元素位置改变
元素尺寸改变(外边距 内边距 边框厚度 宽度 高度等)
Render树变化(如:padding改变)
浏览器窗口resize
获取元素的某些属性:
当滚动条出现时,会触发整个页面的重排

由于每次重排都会产生计算消耗,大多数浏览器通过队列化修改并批量执行来优化重排的过程

但是 我们经常会不知不觉强制刷新队列并要求计划任务立即执行
获取布局信息的操作会到最后队列刷新 比如

1
2
3
4
5
6
7
8
offsetTop , offsetLeft , offsetWidth , offsetHeight

scrollTop , scrollLeft , scrollWidth , scrollHeight

clientTop , clientLeft , clientWidth , clientHeight

getComputedStyle() ( currentStyle in IE )

当获取以上的属性和方法时 浏览器为了获取最新的布局信息 不得不立即触发重排以返回正确的值

最小化重绘和重排

重绘和重排代价很昂贵 因此一个号的提高程序响应熟读的策略就是减少此类操作的发生

优化方式
  1. 合并多次对样式属性的操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
思考
var el = document.getElementById('mydiv');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';

即使有浏览器有重排机制优化 但最坏的情况也是进行三次重排

修改后

var el = document.getElementById('mydiv');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';
或者

var el = document.getElementById('mydiv');
el.className = 'active';

  1. 批量修改 dom
    当需要对 dom 元素进行一系列的操作时候 可以通过以下的步骤来减少重绘和重排的次数

* 使元素脱离文本流
* 操作元素
* 操作完成后 将元素带回文档中
这样儿 只有第一步和第三部触发两次重排

有三种方式可以实现上面的步骤

1. 隐藏元素(display:none)操作元素 重新展示

1
2
3
4
5
var ul = document.getElementById('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

2. 使用文档片段(document fragment)在当前 DOM 之外构建一个子树,再把它拷贝回文档

1
2
3
 var fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
document.getElementById('mylist').appendChild(fragment);

3. 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素

1
2
3
4
var old = document.getElementById('mylist');
var clone = old.cloneNode(true);
appendDataToElement(clone, data);
old.parentNode.replaceChild(clone, old);

总结:推荐尽可能的使用文档片段(第二个方案),因为它们所产生的 DOM 遍历和重排次数最少。唯一潜在的问题是文档片段未被充分利用,很多人可能并不熟悉这项技术。

  1. 缓存布局信息
1
浏览器获取元素的offsetLeft等属性值时会导致重排 将需要获取的保护局信息的属性值 赋值给变量 然后再操作变量
  1. 定位
1
将需要多次重排的元素,position 属性设置为 absolute 或 fixed,这样元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

操作 DOM 具体的成本,说到底是造成浏览器重排和重绘,从而消耗 GPU 资源

s

all 方法和 HTTP 动词方法

针对不同的请求,Express 提供了 use 方法的一些别名。比如,上面代码也可以用别名的形式来写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var express = require("express");
var http = require("http");
var app = express();

app.all("*", function(request, response, next) {
response.writeHead(200, { "Content-Type": "text/plain" });
next();
});

app.get("/", function(request, response) {
response.end("Welcome to the homepage!");
});

app.get("/about", function(request, response) {
response.end("Welcome to the about page!");
});

app.get("*", function(request, response) {
response.end("404!");
});

http.createServer(app).listen(1337);

除了 get 方法以外,Express 还提供 post、put、delete 方法,即 HTTP 动词都是 Express 的方法,express 允许模式匹配

set 方法

用于指定变量的值

1
2
3
app.set('views',_dirname+'/views')
app.set("view engine", "jade");

response 对象

response.redirect () 允许网址的重定向
response.redirect("/hello/anime");

response.sendFile () 用于发送文件
response.sendFile("/path/to/anime.mp4")

response.render () 用于渲染网页模版

1
2
3
app.get("/", function(request, response) {
response.render("index", { message: "Hello World" });
});

使用 render 方法,将 message 变量传入 index 模版,渲染成 HTML 网页

request 对象

request.ip: 用于获取 HTTP 请求的 IP 地址
request.files 用于获取上传的文件

搭建 HTTPS 服务器

使用 express 搭建 https 加密服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var fs = require('fs');
var options = {
key: fs.readFileSync('E:/ssl/myserver.key'),
cert: fs.readFileSync('E:/ssl/myserver.crt'),
passphrase: '1234'
};

var https = require('https');
var express = require('express');
var app = express();

app.get('/', function(req, res){
res.send('Hello World Expressjs');
});

var server = https.createServer(options, app);
server.listen(8084);

浏览器渲染原理

1
当我们在浏览器地址输入url时,浏览器会发送请求到服务器,服务器将请求的html文档发送回浏览器,浏览器将文档下载下来后 便开始从上到下解析,解析完成后 会生成dom,如果页面中有css 会根据css的内容 形成cssdom 然后 dom和css会生成一个渲染树 最后浏览器会根据渲染树的内容计算出各个节点在页面中的确切大小和位置,并将其绘制在浏览器上

1
在解析html的过程中 有时候解析会被中断,这是因为javascript会阻塞dom的解析 当解析过程中遇到script标签的时候 便会停止解析过程 抓转而去处理脚本 如果脚本是内联的 浏览器会先去执行这段内联的脚本,如果脚本是外链的  那么先去加载脚本 然后执行 在处理完脚本之后  浏览器便继续解析html文档

如何计算 DomContentLoaded 加载时间

当文档中没有脚本时 浏览器解析完成文档便能触发 DomContentLoaded 事件 如果文档包含脚本 则脚本会阻塞文档的解析 而脚本需要等位于前面的 css 加载完才能执行 在任何情况下 DomContentLoaded 的触发不需要等待图片等其他资源加载完成

1
2
3
4
5
6
7
8
9
DOMContentLoaded不同的浏览器对其支持不同,所以在实现的时候我们需要做不同浏览器的兼容。

1)支持DOMContentLoaded事件的,就使用DOMContentLoaded事件;

2)IE6、IE7不支持DOMContentLoaded,但它支持onreadystatechange事件,该事件的目的是提供与文档或元素的加载状态有关的信息。

1) 更低的ie还有个特有的方法doScroll, 通过间隔调用:document.documentElement.doScroll("left");
可以检测DOM是否加载完成。 当页面未加载完成时,该方法会报错,直到doScroll不再报错时,就代表DOM加载完成了。该方法更接近DOMContentLoaded的实现。

如何计算 load 加载时间

页面上所有的资源(图片,音频,视频等)被加载以后才会触发 load 事件,简单来说,页面的 load 事件会在 DOMContentLoaded 被触发之后才触发。

1
2
3
4
window.onload = function(){

}

我们为什么一再强调将 CSS 放在头部 将 js 放在尾部

因为浏览器生成 Dom 树的时候是一行一行读 html 代码的 script 标签放在最后面就不会影响前面的页面渲染,那么问题来了
既然 Dom 树完全生成好页面才能渲染出来 浏览器又必须读完全部的 html 才能生成完成的 dom 树 script 标签放不放在底部是不是也一样 因为 dom 树的生成需要整个文档解析完成

chrome 页面渲染过程中 会有 firstpaint 的概念,现代浏览器为了更好的用户体验,渲染引擎将尝试尽快在屏幕上显示的内容 他不会等到所有的 html 解析完成才开始构建和布局 dom 树 部分的内容被解析并展示 也就是说 浏览器能够渲染不完整的 dom 树和 cssdom 尽快的减少白屏时间

假如我们将 js 放在 header js 将会阻塞解析 dom dom 的内容会影响到 firstpaint 导致 firstpaint 延后 所以说我们会将 js 放在后面 以减少 firstpaint 时间但是不会减少 DomContentLoaded 被触发的时间

  1. 去除 input [type=’number’] 时的右侧上下箭头

    / 在 chrome 下:/

     input::-webkit-outer-spin-button,
     input::-webkit-inner-spin-button{
         -webkit-appearance: none !important;
         margin: 0;
         padding-left:5px;
     }
    
     /*Firefox下:*/
     input[type="number"]{-moz-appearance:textfield;}
  1. 判断小数不能大于两位

       var hopePriceLength = hopePrice.toString().split(".")[1].length;
         if(hopePriceLength>2){
             notify('请输入正数,最多两位小数','error');
             $(`#${mid}jp-hope-price`).focus();
             return false;
         }
阅读全文 »

近期在研究异步编程的我对于 setTimeout 之类的东西异常敏感。在 SegmentFault 上看到了一个问题《关于 SetTimeout 时间设为 0 时》:提问者读了一篇文章,原文解释 setTimeout 延迟时间为 0 时会发生的事情,提问者提出了几个文章中的几个疑点。读了那篇文章之后发现原文的作者对于 setTimeout 的理解和自己的认知有点出入,于是编写了相关测试的代码以求答案。最终编写了这篇文章。

本文内容如下:

起因
单线程的 JavaScript
setTimeout 背后意味着什么

阅读全文 »

面向对象的 Javascript

不同于传统面向对象语言中的类式继承 js 通过 原型委托 的方式实现对象与对象之间的继承

编程语言分为 :
静态类型语言:编译时已确定变量的类型
优点:在编译时就能发现类型不匹配的错误,编译器可以针对不同的数据类型对程序做一些优化工作 提高程序之心速度
缺点:程序员依照契约来编写程序
动态类型语言:要到程序运行的时候 待变量被赋予某个值之后 才会具有某种类型
优点:代码数量少
缺点:程序在运行期间有可能发生跟类型相关的错误

Javascript 是一门典型的动态类型语言

多态:给不同的对象发送同一条消息的时候 这些对象会根据这个消息分别给出不同的反馈

多态:
多种形态 在面向对象语言中,接口的多种不同的实现方式即为多态
多态指同一个实体同时具有多种形式
思想:把做什么 和 谁去做 分开
同一个函数 传入不同的参数 可以实现不同的结果

js 的多态是与生俱来的
它作为一门动态类型语言 他在编译时没有类型检查的类型

多态的好处:你不必再向对象询问‘你是什么类型’而后根据得到的答案 调用对象的某个行为 你只管调用该行为就是了

最根本的作用就是通过把过程话的条件分支语句转化为对象的多态性 从而消除这些条件分支语句

面向对象编程的优点
将行为分布在各个对象中,并且让这些对象各自负责自己的行为

当我们对一些函数发出 调用的指令时 这些函数会返回不同的结果
这也是多态的一些体现

封装:封装的目的是将信息隐藏

封装数据
封装实现
封装类型
封装变化
把系统中不变的和变的分离开 只替换变化的 如果变化的也是封装好的 就好替换多了 保证程序的稳定性和可扩展性

隐藏数据 隐藏实现细节 设计细节以及隐藏对象的类型
其他对象或者用户不关心他的具体实现 封装使对象之前的耦合变得松散 对象之间只暴露 APi 接口来通信

原型模式和基于原型继承的 Javascript 对象系统

使用克隆的原型模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Plane = function(){
this.blood=100;
this.ss = 11;
this.tt =2;
}

var plane = new Plane();

Object.create = Object.create ||function(value){

var F = function(){};
F.prototype= value;
return new F();
}

var clonePlane = Object.create(plane)
console.log('clonePlane',clonePlane)
console.log('clonePlane',clonePlane.blood) 100
console.log('clonePlane',clonePlane.ss) 11
console.log('clonePlane',clonePlane.tt) 2


阅读全文 »