DOM
概念
DOM- Document Object Model 文档对象模型
DOM对象 -> 宿主对象
JavaScript有三种对象:
本地对象 Native Object
- Object, Function, Array, String, Number, Boolean, Error, EvalError, SyntaxError, RangeError, ReferenceError, TypeError, URIError
内置对象 Built-in Object
- Global, Math
- ECMA: isNaN, parseInt, Number, decodeURI, encodeURI
宿主对象 Host Object
- 执行JS脚本的环境提供的对象,浏览器对象,兼容性问题
本地对象和内置对象都是ES的内部对象
浏览器对象window(BOM)和document(DOM w3c标准)
- DOM:通过浏览器提供的这一套方法标识或者操作HTML和XML
- BOM:浏览器对象模型
演变过程:
XML -> XHTML -> HTML
document对象
Web API 接口参考 > document对象
document继承:
document 构造函数 -> HTMLDocument -> HTMLDocument 构造函数 -> Document
获取元素
document对象的方法:
document.getElementById()document.getElementsByTagName()document.getElementsByClassName()document.querySelector()(html5正式引入)document.querySelectorAll()(html5正式引入)
关于querySelector:
- 最多只能选择一个元素
关于querySelector& querySelectorAll
- 性能低
- 不实时(临时缓存)
不实时测试:
var divs = document.getElementsByTagName('div');divs[0].remove(); //[div, div, div]console.log(divs); //[div, div]var divs2 = document.querySelectorAll('div');divs2[0].remove(); //[div, div, div]console.log(divs2); //[div, div, div]
企业书写规范:
标签id值为钩子,对接后端
节点
遍历节点树 即为 元素节点树
节点不是元素,节点包含元素
- 元素节点(相当于DOM元素):节点号 1
- 属性节点:节点号 2
- 文本节点(text): 节点号 3
- 注释节点(comment): 节点号 8
- document节点: 节点号 9
- document fragment节点: 节点号11
document对象的属性:
节点包含元素:
parentNode:父节点childNodes:找子节点集合firstChild:第一个节点lastChild:最后一个节点nextSibling:下一个兄弟节点previousSibling:上一个兄弟节点
元素:
parentElement:父元素,IE9及以下不支持children:子元素,IE7及以下不支持childrenElemntCount = children.length,IE9及以下不支持firstElementChild:第一个子元素,IE9及以下不支持lastElementChild:最后一个子元素,IE9及以下不支持nextElemntSibling:下一个兄弟元素,IE9及以下不支持previousElemntSibling:上一个兄弟元素,IE9及以下不支持
<div class="box" id="box" style="background-color: green;">我是文本节点<!-- 我是注释君 --><h1>我是标题标签</h1><a href="">我是超链接</a><p>我是段落标签</p></div>
查看节点名称:
nodeName
//查看nodeNamevar div = document.getElementsByTagName('div')[0];console.log(document.nodeName); //#documentconsole.log(div.nodeName); //DIVconsole.log(div.nodeName.toLowerCase()); //div
更改节点:
//更改nodeName//无法更改 只读console.log(div.nodeName); //Pdiv.nodeName = 'P';console.log(div.nodeName); //DIV
节点的值:
//nodeValueconsole.log(div.firstChild.nodeValue); //我是文本节点console.log(div.childNodes[1].nodeValue); //我是注释君//元素节点没有nodeValueconsole.log(div.childNodes[3]); //<h1>我是标题标签</h1>console.log(div.childNodes[3].nodeValue); //null//属性节点console.log(div.getAttributeNode('id').nodeValue); //boxconsole.log(div.getAttributeNode('id').value); //box
节点的值可更改:
//试更改文本节点//nodeValue可更改console.log(div.firstChild.nodeValue = '我是假的文本节点'); //我是假的文本节点console.log(div.childNodes[1].nodeValue = '我是假的注释君'); //我是假的注释君console.log(div.getAttributeNode('id').nodeValue = 'box1'); //box1
节点类型:
//元素节点 -> 1//属性节点 -> 2//文本节点 -> 3//注释节点 -> 8//document -> 9//DocumentFragment -> 11
console.log(div.nodeType); //1 元素节点console.log(div.firstChild.nodeType); //3文本节点console.log(div.childNodes[1].nodeType); //8注释节点console.log(div.getAttributeNode('id').nodeType); //2属性节点
封装方法:
找出元素节点的方法
/*** elemChildren()* childNodes -> children**/function elemChildren(node) {var arr = [],children = node.childNodes;for (var i = 0; i < children.length; i++) {var childItem = children[i];//找出元素节点if (childItem.nodeType === 1) {arr.push(childItem);}}return arr;}console.log(elemChildren(div));//[h1, a, p]
案例:
把对象的属性和方法写入到类数组中
/*** elemChildren()* 把对象的属性和方法写入到类数组中*/function elemChildren(node) {var temp = {'length': 0,'push': Array.prototype.push,'splice': Array.prototype.splice}var len = node.childNodes.length;for (var i = 0; i < len; i++) {var childItem = node.childNodes[i];if (childItem.nodeType === 1) {//写法一// //对象方法增加属性// //obj[0] = 1;// //obj[1] = 2;// //obj[2] = 3;// /**// * console.log(obj);// * {0:1, 1:2, 2:3}// * 0: 1,// * 1: 2,// * 2: 3// */// // temp[temp['length']] = childItem;// // temp['length']++;//写法二temp.push(childItem);}}return temp;}console.log(elemChildren(div));//[h1, a, p, push: ƒ, splice: ƒ]
元素属性
attributesgetAttributeNode
//元素属性集合console.log(div.attributes);//NamedNodeMap {0: class, 1: id, 2: style, class: class, id: id, style: style, length: 3}//访问idconsole.log(div.attributes[1]);//id="box"console.log(div.getAttributeNode('id'));//id="box"console.log(div.getAttributeNode('id').nodeValue); //boxconsole.log(div.getAttributeNode('id').value); //boxconsole.log(div.attributes[1].nodeValue); //boxconsole.log(div.attributes[1].value); //box
更改值
//可写console.log(div.attributes[1].nodeValue = 'box2'); //box2
判断有没有子节点的方法
//返回true/falseconsole.log(div.hasChildNodes()); //trueconsole.log(div.hasChildNodes()); //true文本节点也算
DOM结构树
Node:
Document
- HTMLDocument
CharacterData(字符数据)
- Text
- Comment
Element
HTMLElement
- HTMLHead Element
- HTMLBody Element
- HTMLTitle Element
- HTMLParagraph Element
- HTMLInput Element
- HTMLTable Element
- HTMLOther Element
- Attributes
节点创建
document.createElement():创建元素节点document.createTextNode():创建文本节点document.createComment():创建注释节点
//创建元素节点var div = document.createElement('div');div.innerHTML = 123;console.log(div);
//创建文本节点var text = document.createTextNode('woaini');document.body.appendChild(text);
//创建注释节点var comment = document.createComment('woshizhushijun');document.body.appendChild(comment);
节点增加
node.appendChild(node):增加子节点(在Node.prototype)node.insertBefore():指定位置插入子节点
appendChild作用:
- 剪切节点
- 追加节点
var p = document.createElement('p');document.body.appendChild(p);
//c.insertBefore(a,b)//插入:在父级c节点下的子节点b之前插入a节点var div = document.getElementsByTagName('div')[0];var p = document.getElementsByTagName('p')[0];var a = document.createElement('a');a.href = '';div.insertBefore(a, p);
节点删除
node.removeChild(子节点):移除子节点node.remove():销毁节点,释放内存
节点文本
HTMLElement.innerHTML:设置和获取元素的HTMLHTMLElement.innerText
注意点:
- 父节点HTML不要写错
- 在
innerHTML里HTML字符串不要写 - document写法1:
document.body.innerHTML - document写法2:
document.documentElement.innerHTML - 元素内部的所有内容都会被删除掉
问题:设置innerHTML到底发生了什么?
innerHTML= '<h1>123</h1>'<h1>123</h1>解析为HTML文档结构- 用DocumentFragment将这个HTML文档结构变成DOM节点
- 原本父结点上的所有内容都会被替换成这个DOM节点
以上侧面反映了innerHTML的性能并不高效,普通文本尽量避免使用innerHTML
安全问题:
HTML5和现代的新的浏览器都会阻止这种通过innerHTML嵌入script脚本的程序执行
关于Node.textContent
可以赋值更改,不能解析html
关于HTMLElement.innerText
给元素渲染文本节点
低于IE11
textContent&innerText的区别:
textContent:获取所有元素的内容innerText:只会获取给人看的内容innerText:会受到CSS的影响,重新计算样式
总结:
innerHTML存在性能问题textContent没有innerHTML功能那么好,innerHTML可以放html字符串,但有更好的性能不会解析html文档,
关于使用:
- 非html尽量用
textContent, 能避免用innerText - 避免使用
innerHTML可以用createElement
节点替换
node.replaceChild()
//node.replaceChild(new, origin)var div = document.getElementsByTagName('div')[0];var h1 = document.getElementsByTagName('h1')[0];var h2 = document.createElement('h2');div.replaceChild(h2, h1);
文档碎片
document.createDocumentFragment()
var oUl = document.getElementById('list');var oFrag = document.createDocumentFragment('div');for (var i = 0; i < 1000; i++) {var oLi = document.createElement('li');oLi.innerHTML = i + '这时第' + i + '个项目';oLi.className = 'list-item';oFrag.appendChild(oLi);}oUl.appendChild(oFrag);
滚动距离/高度
常规滚动条的距离:
window.pageXOffsetwindow.pageYOffset
IE9/IE8 及以下:
document.body.scrollLeftdocunment.body.scrollTopdocument.documentElement.scrollLeftdocument.documentElement.scrollTop
不常见:
window.scrollXwindow.scrollY
注:
- IE6789b:怪异模式
- IE678/0/FFs:IE678/Opera/FireFox standart 标准模式
- IE9s: IE9 standart 标准模式
- C/Sbs:Chrome/Safari standard 怪异/标准模式
- O/FFb:Opera/FireFox 怪异模式
- O/FFs:Opera/FireFox 标准模式
| Browsers | IE6789b(怪异模式) | IE678/0/FFs | IE9s | C/Sbs | O/FFb | O/FFs |
|---|---|---|---|---|---|---|
| document.documentElement | value:0 | YES | YES | value:0 | value:0 | YES |
| document.body.scrollLeft/Top | YES | value:0 | value:0 | YES | YES | value:0 |
| window.pageXOffset | undefined | undefined | YES | YES | YES | YES |
| window.scrollX/Y | undefined | undefined | undefined | YES | YES | YES |
总结:
做兼容性时 利用document.documentElement和document.body合体
方法封装
/*** 封装兼容IE8&IE9以下的滚动条距离函数* getScrollOffset()*/function getScrollOffset() {if (window.pageXOffset) {return {left: window.pageXOffset,top: window.pageYOffset}} else {return {left: document.body.scrollLeft + document.documentElement.scrollLeft,top: document.body.scrollTop + document.documentElement.scrollTop}}}getScrollOffset().top
怪异&标准
浏览器的怪异模式和标准模式
浏览器兼容模式:向后兼容机制
怪异模式:厂商自己的标准
标准模式:w3c标准
document.compatMode -> BackCompat 怪异模式document.compatMode -> CSS1Compat 标准模式
可视区域
获取浏览器可视区域的尺寸(窗口的尺寸宽高)
常规:
window.innerWidthwindow.innerHeight
IE9/IE9及以下:
标准:
document.documentElement.clientWidthdocunment.documentElement.clientHeight
怪异:
document.body.clientWidthdocument.body.clientHeight
兼容性封装:
/*** 封装兼容IE8&IE9以下的可视区域宽高尺寸的函数* 原理:判断模式是否为怪异*/function getViewportSize() {if (window.innerWidth) {return {width: window.innerWidth,height: window.innerHeight}} else {if (document.compatMode === 'BackCompat') {return {width: document.body.clientWidth,height: document.body.clientHeight}} else {return {width: document.documentElement.clientWidth,height: document.documentElement.clientHeight}}}}getViewportSize().width
滚动距离+可视宽度/高度 = 滚动尺寸
document.body.scrollHeightdocument.body.scrollWidth
兼容性封装:
/*** 封装兼容IE8&IE9以下的滚动尺寸的函数*/function getScrollSize() {if (document.body.scrollWidth) {return {width: document.body.scrollWidth,height: document.body.scrollHeight}} else {return {width: document.documentElement.scrollWidth,height: document.documentElement.scrollHeight}}}getScrollSize().width
getBoundingClientRect的问题
// getBoundingClientRect();var box = document.getElementsByClassName('box')[0];console.log(box.getBoundingClientRect());/*** 缺点:不实时* DOMRect {x: 8, y: 8, width: 5002, height: 2, top: 8, …}* bottom: 10* height: 2* left: 8* right: 5010* top: 8* width: 5002* x: 8* y: 8*/
offsetLeft&offsetTop
情况1:
<style>.box {position: absolute;top: 100px;left: 100px;width: 100px;height: 100px;background-color: green;}</style>
<div class="box"></div>
var box = document.getElementsByClassName('box')[0];//获取盒子到左侧浏览器边框的距离console.log(box.offsetLeft); //100console.log(box.offsetTop); //100
情况2:
在外边再写一个盒子
.parent {position: relative;top: 100px;left: 100px;width: 300px;height: 300px;background-color: #999;}.son {position: absolute;top: 100px;left: 100px;width: 100px;height: 100px;background-color: green;}
<div class="parent"><div class="son"></div></div>
父级上有定位(以父级为参照物)
var box = document.getElementsByClassName('son')[0];//获取盒子到左侧浏览器边框的距离console.log(box.offsetLeft); //100console.log(box.offsetTop); //100
注意:margin塌陷:内侧盒子修改margin会影响到父盒子margin-top且合
并在一起
情况3:
body {margin: 0;}.parent {width: 300px;height: 300px;margin: 100px;background-color: #999;overflow: hidden;}.son {width: 100px;height: 100px;margin: 100px;background-color: green;}
offsetLeft/offsetTop直接找父级盒子
var box = document.getElementsByClassName('son')[0];//获取盒子到左侧浏览器边框的距离console.log(box.offsetLeft); //200console.log(box.offsetTop); //200
offsetParent:返回一个有定位的父级元素
如果parent盒子没有定位的情况打印body元素
console.log(parent.offsetParent);
封装一个方法:
查看盒子到页面边缘的距离
/*** 封装合并子盒子与父盒子到左侧/上侧 到 页面左侧/上侧的 距离*/function getElemDocPosition(el) {//找到有定位的父级盒子var parent = el.offsetParent,//找到当前盒子左侧/上侧到页面左侧/上侧的距离offsetLeft = el.offsetLeft,offsetTop = el.offsetTop;// 如果parent存在while (parent) {// 循环出来的parent是定位元素offsetLeft += parent.offsetLeft;offsetTop += parent.offsetTop;//重新赋值parent,找到外层盒子继续加parent = parent.offsetParent;}return {left: offsetLeft,top: offsetTop}}getElemDocPosition(son);//{left: 230, top: 230}
滚动条
操作滚动条
滚动到多少:跳至目标位置,返回undefined
window.scroll(x, y)window.scrollTo(x, y)
每次滚动多少(累加):应用(小说阅读器自动滚动功能)
window.scrollBy(x, y)
样式操作
DOM间接操作(操作内联样式)CSS
oDiv.style.width = '200px';
如果行间样式/内联样式没有填写属性的情况访问不了
console.log(oDiv.style.width); //''
获取元素
elem.currentStyle&window.getComputedStyle(elem, null)
//查看计算样式//IE8及以下不支持 用 elem.currentStyle//没设置的样式默认值并被getComputedStyle函数查询到//此方法获取属性值比较准确window.getComputedStyle(div, null);window.getComputedStyle(elem, null)[prop];
兼容性封装:
/*** 获取元素属性* 避免使用offsetWidth&offsetHeight* @elem 元素* @prop 属性*/function getStyles(elem, prop) {//检测getComputedStyle是否存在if (window.getComputedStyle) {//存在,打印具体属性值if (prop) {return window.getComputedStyle(elem, null)[prop];}//不存在,打印集合return window.getComputedStyle(elem, null);} else {if (prop) {return elem.currentStyle[prop];} else {return elem.currentStyle;}}}getStyles(div);getStyles(div, 'height');
操作伪元素
div.offsetWidth&div.offsetHeight的另外用法:
如果css 内联样式表样式没有定义的width和height属性且css样式里存在width或height属性时,可以通过div.offsetWidth&div.offsetHeight拿到
元素运动
//JS运动相关var box = document.getElementsByClassName('box')[0];box.onclick = function () {//这里offsetWidth会存在margin,有Bug// var width = this.offsetWidth;//解决方法(企业级写法):用封装的getStyles拿宽高//返回带单位的字符串 如 '100px' 在用parseInt()截取一下var width = parseInt(getStyles(this, 'width'));this.style.width = width + 10 + 'px';}
事件
绑定事件处理程序 / 事件处理函数
绑定事件 = 绑定事件的处理函数
事件 + 事件的反馈 = 前端交互 / 交互体验 (核心价值)
document.getElementsByTagName('div')[0].onclick = function(){this.style.backgroundColor = 'orange';}
如何绑定事件处理函数
写法一:
elem.onclick = function(){};
写法二:
IE9及以下不兼容(w3c标准),且可以给同一事件源绑定多个事件
elem.addEventListener(事件类型,事件处理函数,false);oBtn.addEventListener('click', function(){});
写法三:
兼容IE8以及下的绑定方法
elem.attachEvent(事件类型,事件处理函数);oBtn.attachEvent('onclick', function(){});
封装兼容函数
/*** 封装兼容低版本的事件绑定处理函数* @el 元素* @type 事件类型* @fn 事件处理函数*/function addEvent(el, type, fn) {if (el.addEventListener) {el.addEventListener(type, fn, false);} else if (el.attachEvent) {el.attachEvent('on' + type, function () {fn.call(el);})} else {el['on' + type] = fn;}}
清除事件的方法:
elem.removeEventListener()elem.detachEvent()elem.onclick = null/false
事件冒泡
子级元素向父级元素进行事件冒泡现象
事件捕获
顶层元素开始捕获,直至事件源结束
oBtn.addEventListener('click', function(){},true);
这些事件源没有冒泡/捕获现象:
- focus
- blur
- change
- submit
- reset
- select
事件源对象
存放在事件处理函数的参数里,IE8存放在window里
- 鼠标对象
- 键盘对象
需要兼容性写
appaly.addEventListener('click', function(e){var e = ev || window.event;}, false)
取消冒泡
e.stopPropagation(): w3c标准 继承Event.prototypee.cancelBubble = true:IE写法
兼容性写法封装:
/*** 封装取消冒泡方法*/function cancelBubble(e) {var e = e || window.event;if (e.stopPropagation) {e.stopPropagation();} else {e.cancelBubble = true;}}
取消默认事件
//右键菜单document.oncontextmenu = function(){return false;}
e.preventDefault():w3c方法,IE9不兼容e.returnValue = false:IE9以下
作用之一:取消默认链接跳转防止请求后端数据
事件流
描述从页面中接收事件的顺序 冒泡/捕获
- IE提出事件冒泡流 Event Bubbling
- 网景Netscape提出事件捕获流 Event Capturing
事件冒泡流
有捕获程序也可以用冒泡的处理函数来写
点击事件源 ,如事件源顶层父级有相同事件源 ,优先执行。
事件捕获流
与冒泡行为相反
事件捕获阶段:
触发事件 -> document发出一个事件流 -> DOM节点 -> 目标元素 -> target(默认是不会执行处理函数)
事件冒泡阶段:
目标 -> DOM节点 -> document/window
事件流的三个阶段:
- 事件捕获阶段(默认不执行)
- 处于目标阶段(触发时候) -> 执行事件处理函数
- 事件冒泡阶段
事件代理
多次重复的绑定事件处理函数
事件代理的核心:事件对象和事件源对象
事件源对象 - event.target
写法一:
//事件委托 事件代理//e.target 找出事件源oList.onclick = function (e) {var e = e || window.event,tar = e.target || e.srcElement;console.log(tar);}
写法二:
var oList = document.getElementsByTagName('ul')[0],oLi = oList.getElementsByTagName('li'),oBtn = document.getElementsByTagName('button')[0];//一样可以动态创建被事件源监听oBtn.onclick = function (e) {var li = document.createElement('li');li.innerText = oLi.length + 1;oList.appendChisld(li);}
如何在事件代理的情况下获取下标?
写法一:缺点不能在大量数据下使用
var oList = document.getElementsByTagName('ul')[0],oLi = oList.getElementsByTagName('li'),oBtn = document.getElementsByTagName('button')[0],len = oLi.length,item;oList.onclick = function (e) {var e = e || window.event,tar = e.target || e.srcElement;for(var i = 0; i < len; i++){item = oLi[i];//循环的当前项跟点击的事件源相等if(tar === item){consolo.log(i);}}}
写法二;利用数字IndeOf方法(企业级写法,骚操作)
var oList = document.getElementsByTagName('ul')[0],oLi = oList.getElementsByTagName('li'),oBtn = document.getElementsByTagName('button')[0],len = oLi.length,item;oList.onclick = function (e) {var e = e || window.event,tar = e.target || e.srcElement;//注意:oLi列表是类数组,不能使用数组方法//解决方法:继承Array.prototype//indexOf(数组元素) 这里的元素刚好跟tar事件源的值一样//Array.prototype.indexOf.call(DOM对象集合, 当前事件源);idx = Array.prototype.indexOf.call(oLi, tar);console.log(idx);}
写法三:利用target事件源对象的tagName属性筛选冒泡对象
var oList = document.getElementsByTagName('ul')[0],oLi = oList.getElementsByTagName('li'),oBtn = document.getElementsByTagName('button')[0],len = oLi.length,item;oList.onclick = function (e) {var e = e || window.event,tar = e.target || e.srcElement,tagName = tar.tagName.toLowerCase();if(tagName === 'li'){console.log(tar.innerText);}}
面试问题:
实现:请往ul添加50个li,并且给li添加删除功能,考虑性能
写法一:有createElement,效率不高
var box = document.getElementsByClassName('box')[0],oUl = document.createElement('ul'),oFrag = document.createDocumentFragment(),liNum = 50;for (var i = 0; i < liNum; i++) {var oLi = document.createElement('li');oLi.innerHTML = '第' + (i + 1) + '项 <a href="javascript:;">删除</a>';oLi.className = 'list-item';oFrag.appendChild(oLi);}oUl.appendChild(oFrag);box.appendChild(oUl);
写法二:
var box = document.getElementsByClassName('box')[0],oUl = document.createElement('ul'),liNum = 50,list = '';for (var i = 0; i < liNum; i++) {list += '<li>第' + i + '项 <a href="javascript:;">删除</a></li>';}oUl.innerHTML = list;box.appendChild(oUl);
写法三:
//用模板var oUl = document.createElement('ul'),tpl = document.getElementById('tpl').innerHTML,list = '';for (var i = 0; i < 50; i++) {list += tpl.replace(/{{(.*?)}}/, (i + 1));}oUl.innerHTML = list;
鼠标坐标
坐标系
clientX/Y:鼠标位置相对于当前可视区域的坐标(不包括滚动条的距离)pageX/pageY:鼠标位置相对于当前文档的坐标(包含滚动条的距离), IE9以下不支持screenX/Y:鼠标位置相对于屏幕的坐标X/Y: 同clientX/Y相当 ,fireFox不支持layerX/Y:同pageX/Y相同,IE11以下同clientX/YoffsetX/Y:鼠标位置相对于块元素的坐标(包括边框),safari不包括
分析:
document.onclick = function (e) {var e = e || window.event;console.log(e.clientY, e.pageY);//数字相同 但滚动条下,clientY 数值 < pageY 数值console.log(e.screenY, e.pageY);//256 100 screenY = 屏幕坐标console.log(e.y, e.pageY);//数字相同 但滚动条下,y 数值 < pageY 数值//跟clientY数值一样console.log(e.layerY, e.pageY); IE11以下//数字相同 滚动条下也相同console.log(e.offsetY, e.pageY);//offsetY 鼠标位置相对于块元素的坐标//注意:包含边框 , safari不包括}
兼容性写法:
/*** 封装页面坐标函数pagePos()* @e 元素* @返回值 页面内的x/y坐标*/function pagePos(e) {//获取滚动条距离//使用获取滚动条距离函数var sLeft = getScrollOffset().left,sTop = getScrollOffset().top,//获取文档偏移//documentElement.clientLeft IE8及以下不存在(undefined)cLeft = document.documentElement.clientLeft || 0,cTop = document.documentElement.clientTop || 0;return {//可视区域坐标 + 滚动条距离 - 偏移距离X: e.clientX + sLeft - cLeft,Y: e.clientY + sTop - cTop}}
拖拽
mousedownmouseupmousemove
分析:
//鼠标按下并且移动写法://mousedown: mouseover + mouseup//鼠标的移动和抬起要在鼠标按下事件处理函数内部//Bug: 鼠标永远停留在块元素的左上box.onmousedown = function (e) {//情况1:在box块元素上移动box.onmousemove = function (e) {var e = e || window.event;page = pagePos(e);box.style.left = page.X + 'px';box.style.top = page.Y + 'px';//注:以上代码鼠标移出目标元素块会使元素块停止不动//原因:帧率跟不上}//情况2:解决上述停止不动的问题document.onmousemove = function (e) {var e = e || window.event;page = pagePos(e);box.style.left = page.X + 'px';box.style.top = page.Y + 'px';//注:以上代码鼠标移出目标元素块会使元素块停止不动//原因:帧率跟不上}box.onmouseup = function (e) {//鼠标抬起取消事件处理函数document.onmousemove = null;}}
// 事件拖拽方法box.onmousedown = function (e) {var e = e || window.event;page = pagePos(e);//通过getStyles()拿到盒子x,y位置//盒子边缘到盒子内部鼠标位置 = 页面坐标 - 盒子坐标x = page.X - getStyles(box, 'left');y = page.Y - getStyles(box, 'top');console.log(x, y);document.onmousemove = function (e) {var e = e || window.event;page = pagePos(e);//盒子到页面边缘距离 = 页面坐标 - 盒子边缘到盒子内部鼠标位置box.style.left = page.X - x + 'px';box.style.top = page.Y - y + 'px';box.onmouseup = function (e) {//鼠标抬起取消事件处理函数document.onmousemove = null;}}}
封装拖拽函数:
/*** 封装拖拽函数elemDrag()* @elem 元素*/function elemDrag(elem) {var x,y;addEvent(elem, 'mousedown', function (e) {var e = e || window.event;//盒子边缘到盒子内部鼠标位置 = 页面坐标 - 盒子坐标x = pagePos(e).X - getStyles(elem, 'left');y = pagePos(e).Y - getStyles(elem, 'top');addEvent(document, 'mousemove', mouseMove);addEvent(document, 'mouseup', mouseUp);//去掉冒泡和默认事件cancelBubble(e);preventDefaultEvent(e);})function mouseMove(e) {//盒子到页面边缘距离 = 页面坐标 - 盒子边缘到盒子内部鼠标位置var e = e || window.event;elem.style.left = pagePos(e).X - x + 'px';elem.style.top = pagePos(e).Y - y + 'px';}function mouseUp(e) {var e = e || window.event;removeEvent(document, 'mousemove', mouseMove);removeEvent(document, 'mouseup', mouseUp);}}
鼠标点击抬起顺序问题
var oBtn = document.getElementsByTagName("button")[0];//顺序: mousedown > mouseup > click//即: mousedown + mouseup = clickoBtn.onclick = function () {console.log("click");};oBtn.onmousedown = function () {console.log("mousedown");};oBtn.onmouseup = function () {console.log("mouseup");};
模块:鼠标拖拽功能带单双击事件
/*** 模块:鼠标拖拽功能带单双击事件* 功能一:拖拽* 功能二:点击可以跳转链接** 解决现象:拖拽和点击如何分离* 解决方案:记录时间前后,新增一个点击事件** 解决现象:拖拽和点击同时存在* 解决方案:记录原来位置*/var oLink = document.getElementsByTagName('a')[0];var dragNclick = (function (elem, elemClick) {//记录时间var bTime = 0,eTime = 0,//记录坐标oPos = [];//执行drag(elem);function drag(elem) {var x,y;addEvent(elem, 'mousedown', function (e) {var e = e || window.event;//记录鼠标按下时间戳bTime = new Date().getTime();//记录原来的位置oPos = [getStyles(elem, 'left'), getStyles(elem, 'top')];//盒子边缘到盒子内部鼠标位置 = 页面坐标 - 盒子坐标x = pagePos(e).X - getStyles(elem, 'left');y = pagePos(e).Y - getStyles(elem, 'top');addEvent(document, 'mousemove', mouseMove);addEvent(document, 'mouseup', mouseUp);//去掉冒泡和默认事件cancelBubble(e);preventDefaultEvent(e);})function mouseMove(e) {//盒子到页面边缘距离 = 页面坐标 - 盒子边缘到盒子内部鼠标位置var e = e || window.event;elem.style.left = pagePos(e).X - x + 'px';elem.style.top = pagePos(e).Y - y + 'px';}function mouseUp(e) {var e = e || window.event;//记录鼠标抬起时间戳eTime = new Date().getTime();//结束时间 - 开始时间 < 100ms 为点击事件if (eTime - bTime < 100) {//除了拖拽操作,还能点击链接跳转//执行elemDrag函数传过来的第二个参数elemClick();//把记录好的坐标在点击时显示elem.style.left = oPos[0] + 'px';elem.style.top = oPos[1] + 'px';}removeEvent(document, 'mousemove', mouseMove);removeEvent(document, 'mouseup', mouseUp);}}});//执行函数另外传一函数作为参数dragNclick(oLink, function () {window.open('http://www.baidu.com');});
模块:鼠标拖拽功能带单双击事件(原型)
/*** 模块:鼠标拖拽功能带单双击事件(原型)* 功能一:拖拽* 功能二:点击可以跳转链接** 解决现象:拖拽和点击如何分离* 解决方案:记录时间前后,新增一个点击事件** 解决现象:拖拽和点击同时存在* 解决方案:记录原来位置** 解决现象:this指向问题* 解决方案:call和缓存_self** 解决现象:边缘问题* 解决方案:记录边界坐标根据边缘计算** 解决现象:右键问题* 解决方案:e.button 左中右(0/1/2) IE10及以下不兼容** 解决现象:双击问题* 解决方案:记录双击时间*/var oLink = document.getElementsByTagName('a')[0],oMenu = document.getElementsByTagName('div')[0];Element.prototype.dragNclick = (function (elemClick, menu) {//记录时间var bTime = 0,eTime = 0,//双击开始时间cbTime = 0,//双击结束时间ceTime = 0,//双击次数counter = 0,//计时器t = null,//记录坐标oPos = [],//可视窗口宽度wWidth = getViewportSize().width,//可视窗口高度wHeight = getViewportSize().height,//块元素的宽度eleWidth = getStyles(this, 'width'),//块元素的高度eleHeight = getStyles(this, 'width'),//右键盒子的宽度mWidth = getStyles(menu, 'width'),//右键盒子的高度mHeight = getStyles(menu, 'height');// console.log(this); //adrag.call(this);function drag() {var x,y,_self = this;// console.log(this); //window -> call -> aaddEvent(this, 'mousedown', function (e) {var e = e || window.event,//记录按键编码btnCode = e.button;// console.log(this); //a from drag(AO)//右键if (btnCode === 2) {var mLeft = pagePos(e).X,mTop = pagePos(e).Y;//打开一个菜单if (mLeft <= 0) {mLeft = 0;} else if (mLeft >= wWidth - mWidth) {mLeft = pagePos(e).X - mWidth;}if (mTop <= 0) {mTop = 0;} else if (mTop >= wHeight - mHeight) {mTop = pagePos(e).Y - mHeight;}menu.style.left = mLeft + 'px';menu.style.top = mTop + 'px';menu.style.display = 'block';} else if (btnCode === 0) {//左键//记录鼠标按下时间戳bTime = new Date().getTime();//记录原来的位置oPos = [getStyles(_self, 'left'), getStyles(_self, 'top')];menu.style.display = 'none';//盒子边缘到盒子内部鼠标位置 = 页面坐标 - 盒子坐标x = pagePos(e).X - getStyles(_self, 'left');y = pagePos(e).Y - getStyles(_self, 'top');addEvent(document, 'mousemove', mouseMove);addEvent(document, 'mouseup', mouseUp);//去掉冒泡和默认事件cancelBubble(e);preventDefaultEvent(e);}})//去除默认右键菜单addEvent(document, 'contextmenu', function (e) {var e = e || window.e;preventDefaultEvent(e);})//右键盒子以外区域点击会隐藏该盒子addEvent(document, 'click', function (e) {menu.style.display = 'none';})//取消当前盒子的冒泡行为addEvent(menu, 'click', function (e) {var e = e || window.e;cancelBubble(e);})function mouseMove(e) {var e = e || window.event,//记录边界坐标eleLeft = pagePos(e).X - x,eleTop = pagePos(e).Y - y;//到达边缘的情况//靠左边缘if (eleLeft <= 0) {eleLeft = 0;} else if (eleLeft >= wWidth - eleWidth) {//靠右边缘eleLeft = wWidth - eleWidth - 1;}//到达边缘的情况//靠顶边缘if (eleTop <= 0) {eleTop = 0;} else if (eleTop >= wHeight - eleHeight) {//靠下边缘eleTop = wHeight - eleHeight - 1;}//盒子到页面边缘距离 = 页面坐标 - 盒子边缘到盒子内部鼠标位置_self.style.left = eleLeft + 'px';_self.style.top = eleTop + 'px';}function mouseUp(e) {var e = e || window.event;//记录鼠标抬起时间戳eTime = new Date().getTime();//结束时间 - 开始时间 < 100ms 为点击事件if (eTime - bTime < 100) {//把记录好的坐标在点击时显示_self.style.left = oPos[0] + 'px';_self.style.top = oPos[1] + 'px';counter++;//第一次时 点击第一次if (counter === 1) {cbTime = new Date().getTime();}//第二次时 点击第二次if (counter === 2) {ceTime = new Date().getTime();}//证明双击了if (cbTime && ceTime && (ceTime - cbTime < 200)) {//除了拖拽操作,还能点击链接跳转//执行elemDrag函数传过来的第二个参数elemClick();}t = setTimeout(function () {cbTime = 0;ceTime = 0;counter = 0;clearTimeout(t)}, 500)}removeEvent(document, 'mousemove', mouseMove);removeEvent(document, 'mouseup', mouseUp);}}});//执行函数另外传一函数作为参数oLink.dragNclick(function () {window.open('http://www.baidu.com');}, oMenu);
输入框事件
oninput&onpropertychange
只要在输入框中填写或删除内容,里面显示内容
var content = document.getElementById('content');//HTML5新增接口//IE11支持/IE10支持/IE9支持/IE8不支持content.oninput = function () {console.log(this.value); //返回输入框输入内容}//IE11支持/IE10支持/IE9支持/IE8支持content.onpropertychange = function () {console.log(this.value);}
onchange
获得输入框失去焦点后的文本内容,若不更改值,失去焦点不会变化
- 失去焦点才会触发
- 聚集/失去焦点时,值没有变化不会触发
content.onchange = function () {console.log(this.value);}
onfocus&onblur
//获得焦点content.onfocus = function () {this.className = 'focus';}//失去焦点content.onblur = function () {this.className = '';}
placeholder属性在各个浏览存在不一样的情况,不推荐使用,用onfocus和onblur代替使用
<inputtype="text"id="content"value="请输入关键字"class="search-input"onfocus="if(this.value==='请输入关键字'){ this.value = ''; this.className='search-input has-value';}"onblur="if(this.value === ''){ this.value = '请输入关键字'; this.className = 'search-input';}">
鼠标滑入滑出
onmouseover&onmouseout
存在冒泡现象,影响子元素
mouseenter&mouseleave
只对被绑定元素进行事件触发(只对被绑定元素生效)
存在类似冒泡现象,也影响子元素
适合复杂情况,dom比较复杂,可控性比较强,给某一些元素设置,为了互不影响
键盘事件
keydown&keyup
问题:keydown+keyup =? keypress
答案:不成立
原因:
如果同时执行,执行结果顺序为 keydown > keypress > keyup
document.onkeydown = function () {console.log('keydown');}document.onkeyup = function () {console.log('keyup');}document.onkeypress = function () {console.log('keypress');}
现象:
按住键盘不松开,keydown一直触发多次,直到松开键盘keyup才触发一次
注:
- 键盘事件的构造函数为
KeyboardEvent- 键盘顺位码为
keyCode- ASCII码为
charCode
问题:keydown&keypress有什么区别?
keydown没有charCode但有keyCodekeypress有charCode和keyCode
应用:
//这里keypress可以用但keydown不能用因为keypress底下有charCodedocument.onkeypress = function (e) {var str = String.fromCharCode(e.charCode);console.log(str);}
示例:
//键盘上下左右控制小盒子移动var box = document.getElementsByClassName('box')[0];document.onkeydown = function (e) {var e = e || window.event,code = e.keyCode,boxLeft = getStyles(box, 'left'),boxTop = getStyles(box, 'top');switch (code) {//按左按键往左走case 37:box.style.left = boxLeft - 5 + 'px';break;//按右按键往右走case 39:box.style.left = boxLeft + 5 + 'px';break;//按上按键往上走case 38:box.style.top = boxTop - 5 + 'px';break;//按下按键往下走case 40:box.style.top = boxTop + 5 + 'px';break;default:break;}}
ASCII码表



页面渲染
HTML/CSS/JavaScript组成动态页面
domTree
页面渲染机制:
解析和加载的例子:img先解析完后加载资源
解析 :
- domTree 树型结构 ,解析引擎将html结构转为树形结构
- DOM树满足一个深度优先解析的原则
- DOM树构建是html元素节点的解析
- DOM树不管内部资源的问题
加载:
- 解析的过程伴随着加载的开始
- 先有了解析才会有加载
- 解析和加载异步完成的
注意:
style="display: none"会受解析影响createElement出来的元素也会受解析影响
cssTree
CSS树(样式结构体)也需要构建的,跟dom树构建类型,
- 也满足深度优先
- 会忽略浏览器不能识别的样式
渲染树
渲染树 renderTree(即domTree + cssTree),浏览器会根据它绘制页面,说明绘制之前domTree和cssTree已经构建完毕了
- 渲染树每个节点都有自己的样式
- 不包含隐藏节点(
display: none, head之类不需要绘制的节点) - 包含影响布局的样式都要绘制(
visibility: hidden) - 渲染树上的每一个节点都会被当作一个具有内容填充,边距,边框,位置,大小等其他样式的盒子
回流重绘
当JS对页面的节点操作时,就会产生回流或者重绘
- 回流也叫重排,reflow,一定会引起重绘
- 重绘叫repaint,不一定时回流产生的后续反应
因为节点的尺寸,布局,display:none这些改变的时候,渲染树中的一部分或全部需要重新构建的现象,叫做回流。一个页面至少有一次回流。
上述操作之外的,更背景颜色,字体颜色等就不会引起回流,但引起重绘。
总结:
回流时,浏览器会重新构建受影响部分的渲染树,渲染树一旦被改变或重新构建一定会引起重绘。
回流完成时,浏览器会根据新的渲染树重新绘制回流影响的部分或全部节点,这个重新绘制的过程叫做重绘。
引起回流的因素:
- DOM节点增加,删除
- DOM节点位置变化
- 元素的尺寸,边距,填充文字/图片,边框,宽高
- DOM节点
display显示或隐藏 - 页面的渲染初始化
- 浏览器窗口变化
- 向浏览器请求某些样式信息
offset/scroll/client/width/height/getComputeStyle()/currentStyle
尽可能减少回流,减少DOM操作对性能的消耗
//回流 + 重绘oBoxStyle.width = '200px';//回流 + 重绘oBoxStyle.height = '200px';//回流 + 重绘oBoxStyle.margin = '200px';//重绘oBoxStyle.backgroundColor = 'green';//回流 + 重绘oBoxStyle.border = '5px solid orange';//重绘oBoxStyle.color = '#fff';//回流 + 重绘oBoxStyle.fontSize = '30px';
如何优化:
- 要考虑回流次数的问题
- 要考虑涉及节点数量
浏览器减少回流和重绘的方法:
- 队列策略
时间线
定义:在浏览器加载页面开始的那一刻到页面加载完全结束的这个过程中,按顺序发送的每一件事情的总流程。
- 生成document对象(JS起作用)
- 解析文档,构建DOM树,
document.readyState = 'loading',页面加载第一阶段:加载中 - 遇到link标签会新开线程异步加载css外部资源文件,style开始构建CSS树
- 没有设置异步加载的script标签,阻塞文档解析,等待JS脚本加载并执行完成后,继续解析文档
- 异步加载script,异步加载JS脚本并执行,不阻塞解析文档,不能使用
document.write() - 解析文档遇到img先解析这个节点 ,创建加载线程,异步加载图片资源,不阻塞解析文档
- 文档解析完成且可以交互,
document.readyState = 'interactive',页面加载第二阶段:解析完成 - 文档解析完成后立马defer script JS脚本开始按照顺序执行
- 文档解析完成后立马触发DOMContentLoaded事件,可以监控文档解析完成(不等于文档加载完成)时间,同步程序的脚本执行阶段往事件驱动阶段
- async script加载并执行完毕,img等资源加载完毕,window对象中的onload事件才触发,
document.readyState = 'complete',页面加载第三阶段:页面加载完成
关于onreadystatechange,基于JS引擎监听的:只要文档状态改变了就触发事件处理函数
问题:
window.onload与DOMContentLoaded的区别?答:文档解析完成后立马触发DOMContentLoaded事件,而
window.onload是等到页面加载完成时才触发,所以不用window.onload是因为浪费时间
现代浏览器
构建布局渲染过程
现代浏览器为了更好的用户体验,渲染引擎尝试尽快将页面渲染到屏幕,在解析的过程中已经开始渲染,不是等待完毕再渲染,初次绘制概念,一边解析一边构建一边渲染
渲染引擎
浏览器组成部分
- 用户界面
- 浏览器引擎:让浏览器运行的程序接口集合,主要查询和操作渲染引擎
- 渲染引擎:解析HTML/CSS,将解析的结果渲染到页面的程序
- 网络:进行网络请求的程序
- UI后端:绘制组合选择框及对话框等基本组件的程序
- JS解释器:解析执行JS代码的程序
- 数据存储:浏览器存储相关的程序 cookie/storage
渲染是什么?
用一个特点的软件将模型(一个程序)装华为用户能看到的图像的过程
渲染引擎是什么?
内容具备一套绘制图像方法集合,渲染引擎可以让特定的方法执行,把HTML/CSS代码解析成图像显示再浏览器窗口中
渲染树

JS执行机制
浏览器渲染引擎进程(浏览器内核)是多线程的:
- JS引擎线程(单线程)DOM冲突
- GUI线程(互斥)
- http网络请求程序 webAPIs
- 定时器触发线程
- 浏览器事件处理线程
数据量大会怎样?
- 方法一:SSR服务端计算,前端渲染
- 方法二:webworker
JS运行原理:
JS引擎线程(单线程),同时执行异步执行(事件驱动)
回调队列/事件队列
同步处理情况:
- 同步代码进入Call Stack,执行函数,销毁函数,离开Call Stack
异步处理情况:
- 函数执行(定时器/ajax/处理函数)进入Call Stack,因为属于异步处理函数的一部分,调用相应异步线程
- 在Web APIs里注册一个回调函数,并挂起,等待事件被触发
- 执行完毕后,Call Stack移除异步处理函数,但Web APIs等待事件被触发
- 当等待的事件被触发时,把注册在Web APIs的函数移到Callback Queue中
- 事件循环,把回调队列中的处理函数推入调用栈里(Call Stack)
- 执行函数,销毁函数,离开Call Stack
异步加载
同步/异步加载的三种方法
一般来说,异步加载是 浏览器做的
如何自己实现一个异步加载?
企业级写法:
//企业级写法:匿名空间var utils = {test: function () {console.log("test");},demo: function () {console.log("demo");},};utils.test();
同步加载:
阻塞模式,同步模式,浏览器加载默认同步加载状态如link标签同步加载的同时dom解析
为什么不能浏览器引擎解析时不能异步加载,因为DOM解析的情况不明确,会影响加载的过程,会有报错。也是script标签放最后的原因,等上面的标签都加载完毕才执行JS脚本
异步加载:
浏览器并行加载
- 关于
deferIE8及以下,defer加载完后等待DOM解析完成才执行 - 关于
async,W3C标准,HTML5属性,IE9及以上支持async="async",不等DOM解析完,立马执行
使用说明:
- 不要用文档结构查找更改删除操作的
defer和async除了IE以外都兼容- 按需加载情况用异步加载来做
第三种方法:
var s = document.createElement("script");s.type = "text/javascript";s.async = true;s.src = "utils.js";//执行document.body.appendChild(s);
等待页面全部加载完毕后,进行异步加载操作
//阻塞onload执行function async_load() {var s = document.createElement("script");s.type = "text/javascript";s.async = true;s.src = "utils.js";//执行document.body.appendChild(s);}if (window.attachEvent) {window.attachEvent("onload", async_load);} else {window.addEventListener("load", async_load, false);}
