在网页设计中,盒子模型是css技术所使用的一种思维模型。盒子模型是指将网页设计页面中的内容元素看作一个个装了东西的矩形盒子。通过定义一系列与盒子相关的属性,可以极大地丰富和促进各个盒子乃至整个HTML文档的表现效果和布局结构。对于是盒子的元素,如果没有特殊设置,其默认总是占独立的一行,宽度为浏览器窗口的宽度,在其前后的元素不管是不是盒子,都只能排列在它的上面或者下面,即上下累加着进行排列。
两种盒子模型
在了解两种不同的盒模型之前,需要先了解一下为什么会产生两种不同的盒模型。(标准模式和怪异模式)这有一定的历史渊源。
当年,Netscape4(译注:网景公司早期的浏览器)和IE4(微软公司早期的浏览器)实现CSS机制时,并没有遵循W3C提出的标准。Netscape4 提供了糟糕的支持,而IE4 虽然接近标准,但依旧未能完全正确的支持标准。尽管IE 5 修复了IE4 许多的问题(bugs),但是依然延续CSS实现中的其它故障(主要是盒模型(box model)问题)。然而随着标准一致性变得越来越重要,浏览器开发商不得不面临一个艰难的抉择:逐渐遵循W3C的标准是前进的方向。但是改变现有CSS的实现,完整去遵循标准,会使许多网站或多或少受到破坏。如果浏览器突然以正确的方式解析现存的CSS,陈旧的网站显示必然受到影响。
于是,所有的浏览器开始提供两种模式:怪异模式(即兼容模式 Quirks Mode/Compalibility Mode)
服务于旧式规则,严格模式(即标准模式 Standard Mode/Strict Mode)
服务于标准规则。Mac平台的IE浏览器最先实现这两种模式,Mozilla, Safari、Opera和Windows平台的IE6也相继实现了这两种模式。Windows平台的IE5和Netscape4则只提供了怪异模式。
选择使用哪种模式需要一个触发器,而 “DOCTYP切换” 则用于此目的。依照标准,任何一个(X)HTML文档必须拥有一个DOCTYPE(译注:DTD(文档类型定义)是一组机器可读的规则,它们指示(X)HTML文档中允许有什么,不允许有什么,DOCTYPE正是用来告诉浏览器使用哪种DTD,一般放在(X)HTML文档开头声明)用以告诉其他人这个文档的类型风格
- 产生于标准化浪潮以前的网页并没有DOCTYPE声明。因此’没有DOCTYPE’意味着触发怪异模式:既依据旧式的CSS规则渲染网页。
- 相反,如果开发者明确知道包含DOCTYPE,他们应该明白他们想要怎么做。因此大部分的DOCTYPE声明将触发严格模式:即依据标准的CSS规则渲染网页。
- 任何新的或未知的DOCTYPE将触发严格模式。
- 一些页面依据怪异模式而写,但是却包含DOCTYPE。这种情况下各个浏览器依据自己的DOCTYPE规则列表来触发怪异模式。
标准模型的宽高 = 内容(content)的宽高,
IE盒模型的宽高 = 内容(content) + 填充(padding) + 边框(border)的总宽高。
文档流
文档流指的是元素在HTML中的位置顺序决定排布的过程,主要的形式是自上而下(块级元素)一行接一行,每一行从左至右(行内元素)。主要是针对盒子模型来说的。文档流不但是盒子模式定位的基础所在,它也是HTML中默认的网页布局模式
浮动
浮动就是让设置的标签产生漂浮效果,脱离原来在页面本应出现的空间位置,不再占用任何文档流空间。元素设置为浮动以后,会生成一个块级元素,而不论它本身是何种元素。且要指明一个宽度,否则它会尽可能地窄;另外当可供浮动的空间小于浮动元素时,它会跑到下一行,直到拥有足够放下它的空间。
定位
页面布局使用最多的是相对定位和绝对定位。
- 相对定位(relative):这种定位方式下元素不脱离文档流,仍然保持其未定位前的形态并且保留它原来所占空间。偏移时以自身位置的左上角作为参照物,通过left、top、right和bottom四个方向的属性来定义偏移的位置。下面通过比较定位前和定位后的两种状态来分析相对定位的效果。
- 绝对定位(absolute):这种定位方式下元素将脱离文档流,不占据空间,文档流中的后续元素将填补它留下的空间。下面通过比较定位前和定位后的两种状态来分析绝对定位的效果。
- 固定定位(fixd):相对于浏览器窗口进行定位,脱离原来的文档流
DOM节点模型
DOM 是 JavaScript 操作网页的接口,全称为“文档对象模型”(Document Object Model)。它的作用是将网页转为一个 JavaScript 对象,从而可以用脚本进行各种操作(比如增删内容)。浏览器会根据 DOM 模型,将结构化文档(比如 HTML 和 XML)解析成一系列的节点,再由这些节点组成一个树状结构(DOM Tree)。所有的节点和最终的树状结构,都有规范的对外接口。
DOM 只是一个接口规范,可以用各种语言实现。所以严格地说,DOM 不是 JavaScript 语法的一部分,但是 DOM 操作是 JavaScript 最常见的任务,离开了 DOM,JavaScript 就无法控制网页。另一方面,JavaScript 也是最常用于 DOM 操作的语言。后面介绍的就是 JavaScript 对 DOM 标准的实现和用法。
节点
DOM 的最小组成单位叫做节点(node)。文档的树形结构(DOM 树),就是由各种不同类型的节点组成。每个节点可以看作是文档树的一片叶子。节点的类型有七种:
Document
:整个文档树的顶层节点DocumentType
:doctype
标签(比如<!DOCTYPE html>
)Element
:网页的各种HTML标签(比如<body>
、<a>
等)Attribute
:网页元素的属性(比如class="right"
)Text
:标签之间或标签包含的文本Comment
:注释DocumentFragment
:文档的片段
浏览器提供一个原生的节点对象Node
,上面这七种节点都继承了Node
,因此具有一些共同的属性和方法。
节点树
一个文档的所有节点,按照所在的层级,可以抽象成一种树状结构。这种树状结构就是 DOM 树。它有一个顶层节点,下一层都是顶层节点的子节点,然后子节点又有自己的子节点,就这样层层衍生出一个金字塔结构,倒过来就像一棵树。浏览器原生提供document
节点,代表整个文档。
文档的第一层只有一个节点,就是 HTML 网页的第一个标签<html>
,它构成了树结构的根节点(root node),其他 HTML 标签节点都是它的下级节点。除了根节点,其他节点都有三种层级关系:
- 父节点关系(parentNode):直接的那个上级节点
- 子节点关系(childNodes):直接的下级节点
- 同级节点关系(sibling):拥有同一个父节点的节点
DOM 提供操作接口,用来获取这三种关系的节点。比如,子节点接口包括firstChild
(第一个子节点)和lastChild
(最后一个子节点)等属性,同级节点接口包括nextSibling
(紧邻在后的那个同级节点)和previousSibling
(紧邻在前的那个同级节点)属性。
DOM事件模型
基本事件模型
在Web应用程序或Web网站中,可以通过使用者操作或系统的事件,达到相应的响应。而在JavaScript中,事件在未得到W3C标准化之前,各浏览器就有一个事件模型 —— 基本事件模型(Basic Event Model)。例如下面这个点击事件
<!-- HTML --><button>Click Me!</button>// Script
let handler = function () {
console.log(this)
}
document.querySelector('button'). = handler
上面的代码,当用户用鼠标点击按钮时会调用handler()
函数,打印出来的this
就是用户点击的按钮。像这样的做法,被称为传统模型(Traditional Model)或传统注册模型(Traditional Registration Model)。这种事件模型也被称为DOM0级模型。基本事件模型有一个典型的缺点,就是只能注册一个事处处理程序,如果你想注册多个事件处理程序是行不通的。比如:
<!-- HTML --><button>单击我</button>// Script
let handler1 = function () {
console.log('Handler1:', this)
}
let handler2 = function () {
console.log('Handler2', this)
}
document.querySelector('button'). = handler1
document.querySelector('button'). = handler2
当你点击button
按钮时,浏览器控制台只会输出hander2()
函数做的事情,第一个函数handler1()
不起作用。
DOM Level 2模型
DOM level 2模型属于W3C标准模型,现代浏览器都支持该模型。在该事件模型中,一次事件共有三个过程:
- 事件捕获阶段(Capturing Phase):事件从
document
一直向下传播到目标元素,依次检查经历过的节点是否绑定了事处监听函数(事件处理程序),如果有则执行,反之不执行 - 事件处理阶段(Target Phase):事件到达目标元素,触发目标元素的监听函数
- 事件冒泡阶段(Bubbling Phase):事件从目标元素冒泡到
document
,依次检查经过的节点是否绑定了事件监听函数,如果有则执行,反之不执行。
简而言之:事件一开始从文档的根节点流向目标对象(捕获阶段),然后在目标对向上被触发(目标阶段),之后再回溯到文档的根节点(冒泡阶段)。
DOM事件中这三个过程很复杂,但在这篇文章中不做深入阐述,在DOM Level 2事件模型中,要注册事件,必须使用addEventListener()方法。比如下面这个示例:
let handler = function () {
// window的load事件要做的事情...}
window.addEventListener('load',handler, false)
在基本事件模型中提到过,基本事件模型只能注册一个事件,但在DOM Level 2事件模型中可以,比如前面提到的按钮的click
事件,我们可以注册多个事件:
let handler1 = function () {
console.log('handler1:', this)
}let handler2 = function () {
console.log('handler2', this)
}
document.querySelector('button').addEventListener('click', handler1, true)
document.querySelector('button').addEventListener('click', handler2, true)
这个时候点击按钮时,handler1()
和handler2()
两个函数都会被触发。在DOM Level 2事件模型中,如果要移除事件处理程序,可以使用removeEventListener()
方法。
let btn = document.getElementById('btn');
btn.addEventListener('click', handler, false);btn.removeEventListener('click', handler, false);
DOM Level 3事件模型
DOM Level 3事件模型是DOM Level 2的事件模型的升级版,在DOM Level 2事件模型的基础上添加了更多的事件类型:
- UI事件:当用户与页面上的元素交互时触发,如:
load
、scroll
- 焦点事件:当元素获得或失去焦点时触发,如:
blur
、focus
- 鼠标事件:当用户通过鼠标在页面执行操作时触发如:
dbclick
、mouseup
- 滚轮事件:当使用鼠标滚轮或类似设备时触发,如:
mousewheel
- 文本事件:当在文档中输入文本时触发,如:
input
、change
- 键盘事件:当用户通过键盘在页面上执行操作时触发,如:
keydown
、keypress
- 合成事件:当为IME(输入法编辑器)输入字符时触发,如:
compositionstart
- 变动事件:当底层DOM结构发生变化时触发,如:
DOMsubtreeModified
同时DOM3级事件也允许使用者自定义一些事件。在自定义事件称之为自定义事件模型。
自定义事件模型
事件模型的实现从设计模式的角度来看,是一种观察者模式或者也叫发布订阅模式,订阅者订阅一个消息,发布者发布这个消息,订阅者收到消息,这是一种数据流动的方式,使用这个模式的好处是,可以有多个订阅者,一个发布者,发布一条消息,可被多个订阅者收到。比如下面这样的一个事件模型:
(function(global){
class Events {
constructor(){
this.cache = {};
this.onceKeys = [];
}
on(key, fn){
if(!this.cache[key])
this.cache[key] = [];
this.cache[key].push(fn);
}
one(key, fn){
this.cache[key]=[];
this.on(key, fn);
this.onceKeys.push(key);
}
off(key, fn){
if(this.cache[key])
this.cache[key] = fn ? this.cache[key].filter(v=>v !== fn) : [];
}
emit(key, ...args){
if(this.cache[key]){
this.cache[key].forEach(v=>v.apply(null, args))
if(this.onceKeys.includes(key)){
this.cache[key] = [];
this.onceKeys = this.onceKeys.filter(v=>v!==key);
}
}
}
}
global.Events = new Events();
})(this)
这是一个简单的自定义事件型型:
on()
用于绑定事件,参数:事件名称,事件处理函数emit()
用于触发事件,参数:事件名称,传递给事件处理函数的参数off
用于解除绑定的指定事件, 参数:事件名称,要解绑的事件函数one
用于绑定一次性事件,只能触发一次,参数:事件名称,事件处理函数
如果要使用,可以这样:
Events.on('cus', (a, b) => console.log(a+b))
Events.emit('cus', 1, 2); // => 3Events.off('cus');
Events.emit('cus', 1, 2)// 只触发一次Events.one('once', a => console.log(a))
Events.emit('once', 1); // => 1Events.emit('once', 2);