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

  • 性能低
  • 不实时(临时缓存)

不实时测试:

  1. var divs = document.getElementsByTagName('div');
  2. divs[0].remove(); //[div, div, div]
  3. console.log(divs); //[div, div]
  4. var divs2 = document.querySelectorAll('div');
  5. divs2[0].remove(); //[div, div, div]
  6. 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及以下不支持
  1. <div class="box" id="box" style="background-color: green;">我是文本节点
  2. <!-- 我是注释君 -->
  3. <h1>我是标题标签</h1>
  4. <a href="">我是超链接</a>
  5. <p>我是段落标签</p>
  6. </div>

查看节点名称:

  • nodeName
  1. //查看nodeName
  2. var div = document.getElementsByTagName('div')[0];
  3. console.log(document.nodeName); //#document
  4. console.log(div.nodeName); //DIV
  5. console.log(div.nodeName.toLowerCase()); //div

更改节点:

  1. //更改nodeName
  2. //无法更改 只读
  3. console.log(div.nodeName); //P
  4. div.nodeName = 'P';
  5. console.log(div.nodeName); //DIV

节点的值:

  1. //nodeValue
  2. console.log(div.firstChild.nodeValue); //我是文本节点
  3. console.log(div.childNodes[1].nodeValue); //我是注释君
  4. //元素节点没有nodeValue
  5. console.log(div.childNodes[3]); //<h1>我是标题标签</h1>
  6. console.log(div.childNodes[3].nodeValue); //null
  7. //属性节点
  8. console.log(div.getAttributeNode('id').nodeValue); //box
  9. console.log(div.getAttributeNode('id').value); //box

节点的值可更改:

  1. //试更改文本节点
  2. //nodeValue可更改
  3. console.log(div.firstChild.nodeValue = '我是假的文本节点'); //我是假的文本节点
  4. console.log(div.childNodes[1].nodeValue = '我是假的注释君'); //我是假的注释君
  5. console.log(div.getAttributeNode('id').nodeValue = 'box1'); //box1

节点类型:

  1. //元素节点 -> 1
  2. //属性节点 -> 2
  3. //文本节点 -> 3
  4. //注释节点 -> 8
  5. //document -> 9
  6. //DocumentFragment -> 11
  1. console.log(div.nodeType); //1 元素节点
  2. console.log(div.firstChild.nodeType); //3文本节点
  3. console.log(div.childNodes[1].nodeType); //8注释节点
  4. console.log(div.getAttributeNode('id').nodeType); //2属性节点

封装方法:

找出元素节点的方法

  1. /**
  2. * elemChildren()
  3. * childNodes -> children
  4. *
  5. */
  6. function elemChildren(node) {
  7. var arr = [],
  8. children = node.childNodes;
  9. for (var i = 0; i < children.length; i++) {
  10. var childItem = children[i];
  11. //找出元素节点
  12. if (childItem.nodeType === 1) {
  13. arr.push(childItem);
  14. }
  15. }
  16. return arr;
  17. }
  18. console.log(elemChildren(div));
  19. //[h1, a, p]

案例:

把对象的属性和方法写入到类数组中

  1. /**
  2. * elemChildren()
  3. * 把对象的属性和方法写入到类数组中
  4. */
  5. function elemChildren(node) {
  6. var temp = {
  7. 'length': 0,
  8. 'push': Array.prototype.push,
  9. 'splice': Array.prototype.splice
  10. }
  11. var len = node.childNodes.length;
  12. for (var i = 0; i < len; i++) {
  13. var childItem = node.childNodes[i];
  14. if (childItem.nodeType === 1) {
  15. //写法一
  16. // //对象方法增加属性
  17. // //obj[0] = 1;
  18. // //obj[1] = 2;
  19. // //obj[2] = 3;
  20. // /**
  21. // * console.log(obj);
  22. // * {0:1, 1:2, 2:3}
  23. // * 0: 1,
  24. // * 1: 2,
  25. // * 2: 3
  26. // */
  27. // // temp[temp['length']] = childItem;
  28. // // temp['length']++;
  29. //写法二
  30. temp.push(childItem);
  31. }
  32. }
  33. return temp;
  34. }
  35. console.log(elemChildren(div));
  36. //[h1, a, p, push: ƒ, splice: ƒ]

元素属性

  • attributes
  • getAttributeNode
  1. //元素属性集合
  2. console.log(div.attributes);
  3. //NamedNodeMap {0: class, 1: id, 2: style, class: class, id: id, style: style, length: 3}
  4. //访问id
  5. console.log(div.attributes[1]);
  6. //id="box"
  7. console.log(div.getAttributeNode('id'));
  8. //id="box"
  9. console.log(div.getAttributeNode('id').nodeValue); //box
  10. console.log(div.getAttributeNode('id').value); //box
  11. console.log(div.attributes[1].nodeValue); //box
  12. console.log(div.attributes[1].value); //box

更改值

  1. //可写
  2. console.log(div.attributes[1].nodeValue = 'box2'); //box2

判断有没有子节点的方法

  1. //返回true/false
  2. console.log(div.hasChildNodes()); //true
  3. console.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():创建注释节点
  1. //创建元素节点
  2. var div = document.createElement('div');
  3. div.innerHTML = 123;
  4. console.log(div);
  1. //创建文本节点
  2. var text = document.createTextNode('woaini');
  3. document.body.appendChild(text);
  1. //创建注释节点
  2. var comment = document.createComment('woshizhushijun');
  3. document.body.appendChild(comment);

节点增加

  • node.appendChild(node):增加子节点(在Node.prototype)
  • node.insertBefore():指定位置插入子节点

appendChild作用:

  • 剪切节点
  • 追加节点
  1. var p = document.createElement('p');
  2. document.body.appendChild(p);
  1. //c.insertBefore(a,b)
  2. //插入:在父级c节点下的子节点b之前插入a节点
  3. var div = document.getElementsByTagName('div')[0];
  4. var p = document.getElementsByTagName('p')[0];
  5. var a = document.createElement('a');
  6. a.href = '';
  7. div.insertBefore(a, p);

节点删除

  • node.removeChild(子节点):移除子节点
  • node.remove():销毁节点,释放内存

节点文本

  • HTMLElement.innerHTML :设置和获取元素的HTML
  • HTMLElement.innerText

注意点:

  • 父节点HTML不要写错
  • innerHTML里HTML字符串不要写
  • document写法1:document.body.innerHTML
  • document写法2:document.documentElement.innerHTML
  • 元素内部的所有内容都会被删除掉

问题:设置innerHTML到底发生了什么?

  1. innerHTML= '<h1>123</h1>'
  2. <h1>123</h1>解析为HTML文档结构
  3. 用DocumentFragment将这个HTML文档结构变成DOM节点
  4. 原本父结点上的所有内容都会被替换成这个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()
  1. //node.replaceChild(new, origin)
  2. var div = document.getElementsByTagName('div')[0];
  3. var h1 = document.getElementsByTagName('h1')[0];
  4. var h2 = document.createElement('h2');
  5. div.replaceChild(h2, h1);

文档碎片

  • document.createDocumentFragment()
  1. var oUl = document.getElementById('list');
  2. var oFrag = document.createDocumentFragment('div');
  3. for (var i = 0; i < 1000; i++) {
  4. var oLi = document.createElement('li');
  5. oLi.innerHTML = i + '这时第' + i + '个项目';
  6. oLi.className = 'list-item';
  7. oFrag.appendChild(oLi);
  8. }
  9. oUl.appendChild(oFrag);

滚动距离/高度

常规滚动条的距离:

  • window.pageXOffset
  • window.pageYOffset

IE9/IE8 及以下:

  • document.body.scrollLeft

  • docunment.body.scrollTop

  • document.documentElement.scrollLeft

  • document.documentElement.scrollTop

不常见:

  • window.scrollX
  • window.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.documentElementdocument.body合体

方法封装

  1. /**
  2. * 封装兼容IE8&IE9以下的滚动条距离函数
  3. * getScrollOffset()
  4. */
  5. function getScrollOffset() {
  6. if (window.pageXOffset) {
  7. return {
  8. left: window.pageXOffset,
  9. top: window.pageYOffset
  10. }
  11. } else {
  12. return {
  13. left: document.body.scrollLeft + document.documentElement.scrollLeft,
  14. top: document.body.scrollTop + document.documentElement.scrollTop
  15. }
  16. }
  17. }
  18. getScrollOffset().top

怪异&标准

浏览器的怪异模式和标准模式

浏览器兼容模式:向后兼容机制

怪异模式:厂商自己的标准

标准模式:w3c标准

  1. document.compatMode -> BackCompat 怪异模式
  2. document.compatMode -> CSS1Compat 标准模式

可视区域

获取浏览器可视区域的尺寸(窗口的尺寸宽高)

常规:

  • window.innerWidth
  • window.innerHeight

IE9/IE9及以下:

  • 标准:

    • document.documentElement.clientWidth
    • docunment.documentElement.clientHeight
  • 怪异:

    • document.body.clientWidth
    • document.body.clientHeight

兼容性封装:

  1. /**
  2. * 封装兼容IE8&IE9以下的可视区域宽高尺寸的函数
  3. * 原理:判断模式是否为怪异
  4. */
  5. function getViewportSize() {
  6. if (window.innerWidth) {
  7. return {
  8. width: window.innerWidth,
  9. height: window.innerHeight
  10. }
  11. } else {
  12. if (document.compatMode === 'BackCompat') {
  13. return {
  14. width: document.body.clientWidth,
  15. height: document.body.clientHeight
  16. }
  17. } else {
  18. return {
  19. width: document.documentElement.clientWidth,
  20. height: document.documentElement.clientHeight
  21. }
  22. }
  23. }
  24. }
  25. getViewportSize().width

滚动距离+可视宽度/高度 = 滚动尺寸

  • document.body.scrollHeight
  • document.body.scrollWidth

兼容性封装:

  1. /**
  2. * 封装兼容IE8&IE9以下的滚动尺寸的函数
  3. */
  4. function getScrollSize() {
  5. if (document.body.scrollWidth) {
  6. return {
  7. width: document.body.scrollWidth,
  8. height: document.body.scrollHeight
  9. }
  10. } else {
  11. return {
  12. width: document.documentElement.scrollWidth,
  13. height: document.documentElement.scrollHeight
  14. }
  15. }
  16. }
  17. getScrollSize().width

getBoundingClientRect的问题

  1. // getBoundingClientRect();
  2. var box = document.getElementsByClassName('box')[0];
  3. console.log(box.getBoundingClientRect());
  4. /**
  5. * 缺点:不实时
  6. * DOMRect {x: 8, y: 8, width: 5002, height: 2, top: 8, …}
  7. * bottom: 10
  8. * height: 2
  9. * left: 8
  10. * right: 5010
  11. * top: 8
  12. * width: 5002
  13. * x: 8
  14. * y: 8
  15. */

offsetLeft&offsetTop

情况1:

  1. <style>
  2. .box {
  3. position: absolute;
  4. top: 100px;
  5. left: 100px;
  6. width: 100px;
  7. height: 100px;
  8. background-color: green;
  9. }
  10. </style>
  1. <div class="box"></div>
  1. var box = document.getElementsByClassName('box')[0];
  2. //获取盒子到左侧浏览器边框的距离
  3. console.log(box.offsetLeft); //100
  4. console.log(box.offsetTop); //100

情况2:

在外边再写一个盒子

  1. .parent {
  2. position: relative;
  3. top: 100px;
  4. left: 100px;
  5. width: 300px;
  6. height: 300px;
  7. background-color: #999;
  8. }
  9. .son {
  10. position: absolute;
  11. top: 100px;
  12. left: 100px;
  13. width: 100px;
  14. height: 100px;
  15. background-color: green;
  16. }
  1. <div class="parent">
  2. <div class="son"></div>
  3. </div>

父级上有定位(以父级为参照物)

  1. var box = document.getElementsByClassName('son')[0];
  2. //获取盒子到左侧浏览器边框的距离
  3. console.log(box.offsetLeft); //100
  4. console.log(box.offsetTop); //100

注意:margin塌陷:内侧盒子修改margin会影响到父盒子margin-top且合

并在一起

情况3:

  1. body {
  2. margin: 0;
  3. }
  4. .parent {
  5. width: 300px;
  6. height: 300px;
  7. margin: 100px;
  8. background-color: #999;
  9. overflow: hidden;
  10. }
  11. .son {
  12. width: 100px;
  13. height: 100px;
  14. margin: 100px;
  15. background-color: green;
  16. }

offsetLeft/offsetTop直接找父级盒子

  1. var box = document.getElementsByClassName('son')[0];
  2. //获取盒子到左侧浏览器边框的距离
  3. console.log(box.offsetLeft); //200
  4. console.log(box.offsetTop); //200

offsetParent:返回一个有定位的父级元素

如果parent盒子没有定位的情况打印body元素

  1. console.log(parent.offsetParent);

封装一个方法:

查看盒子到页面边缘的距离

  1. /**
  2. * 封装合并子盒子与父盒子到左侧/上侧 到 页面左侧/上侧的 距离
  3. */
  4. function getElemDocPosition(el) {
  5. //找到有定位的父级盒子
  6. var parent = el.offsetParent,
  7. //找到当前盒子左侧/上侧到页面左侧/上侧的距离
  8. offsetLeft = el.offsetLeft,
  9. offsetTop = el.offsetTop;
  10. // 如果parent存在
  11. while (parent) {
  12. // 循环出来的parent是定位元素
  13. offsetLeft += parent.offsetLeft;
  14. offsetTop += parent.offsetTop;
  15. //重新赋值parent,找到外层盒子继续加
  16. parent = parent.offsetParent;
  17. }
  18. return {
  19. left: offsetLeft,
  20. top: offsetTop
  21. }
  22. }
  23. getElemDocPosition(son);
  24. //{left: 230, top: 230}

滚动条

操作滚动条

滚动到多少:跳至目标位置,返回undefined

  • window.scroll(x, y)
  • window.scrollTo(x, y)

每次滚动多少(累加):应用(小说阅读器自动滚动功能)

  • window.scrollBy(x, y)

样式操作

DOM间接操作(操作内联样式)CSS

  1. oDiv.style.width = '200px';

如果行间样式/内联样式没有填写属性的情况访问不了

  1. console.log(oDiv.style.width); //''

获取元素

elem.currentStyle&window.getComputedStyle(elem, null)

  1. //查看计算样式
  2. //IE8及以下不支持 用 elem.currentStyle
  3. //没设置的样式默认值并被getComputedStyle函数查询到
  4. //此方法获取属性值比较准确
  5. window.getComputedStyle(div, null);
  6. window.getComputedStyle(elem, null)[prop];

兼容性封装:

  1. /**
  2. * 获取元素属性
  3. * 避免使用offsetWidth&offsetHeight
  4. * @elem 元素
  5. * @prop 属性
  6. */
  7. function getStyles(elem, prop) {
  8. //检测getComputedStyle是否存在
  9. if (window.getComputedStyle) {
  10. //存在,打印具体属性值
  11. if (prop) {
  12. return window.getComputedStyle(elem, null)[prop];
  13. }
  14. //不存在,打印集合
  15. return window.getComputedStyle(elem, null);
  16. } else {
  17. if (prop) {
  18. return elem.currentStyle[prop];
  19. } else {
  20. return elem.currentStyle;
  21. }
  22. }
  23. }
  24. getStyles(div);
  25. getStyles(div, 'height');

操作伪元素

div.offsetWidth&div.offsetHeight的另外用法:

如果css 内联样式表样式没有定义的width和height属性且css样式里存在width或height属性时,可以通过div.offsetWidth&div.offsetHeight拿到

元素运动

  1. //JS运动相关
  2. var box = document.getElementsByClassName('box')[0];
  3. box.onclick = function () {
  4. //这里offsetWidth会存在margin,有Bug
  5. // var width = this.offsetWidth;
  6. //解决方法(企业级写法):用封装的getStyles拿宽高
  7. //返回带单位的字符串 如 '100px' 在用parseInt()截取一下
  8. var width = parseInt(getStyles(this, 'width'));
  9. this.style.width = width + 10 + 'px';
  10. }

事件

绑定事件处理程序 / 事件处理函数

绑定事件 = 绑定事件的处理函数

事件 + 事件的反馈 = 前端交互 / 交互体验 (核心价值)

  1. document.getElementsByTagName('div')[0].onclick = function(){
  2. this.style.backgroundColor = 'orange';
  3. }

如何绑定事件处理函数

写法一:

  1. elem.onclick = function(){};

写法二:

IE9及以下不兼容(w3c标准),且可以给同一事件源绑定多个事件

  1. elem.addEventListener(事件类型,事件处理函数,false);
  2. oBtn.addEventListener('click', function(){});

写法三:

兼容IE8以及下的绑定方法

  1. elem.attachEvent(事件类型,事件处理函数);
  2. oBtn.attachEvent('onclick', function(){});

封装兼容函数

  1. /**
  2. * 封装兼容低版本的事件绑定处理函数
  3. * @el 元素
  4. * @type 事件类型
  5. * @fn 事件处理函数
  6. */
  7. function addEvent(el, type, fn) {
  8. if (el.addEventListener) {
  9. el.addEventListener(type, fn, false);
  10. } else if (el.attachEvent) {
  11. el.attachEvent('on' + type, function () {
  12. fn.call(el);
  13. })
  14. } else {
  15. el['on' + type] = fn;
  16. }
  17. }

清除事件的方法:

  • elem.removeEventListener()
  • elem.detachEvent()
  • elem.onclick = null/false

事件冒泡

子级元素向父级元素进行事件冒泡现象

事件捕获

顶层元素开始捕获,直至事件源结束

  1. oBtn.addEventListener('click', function(){},true);

这些事件源没有冒泡/捕获现象:

  • focus
  • blur
  • change
  • submit
  • reset
  • select

事件源对象

存放在事件处理函数的参数里,IE8存放在window里

  • 鼠标对象
  • 键盘对象

需要兼容性写

  1. appaly.addEventListener('click', function(e){
  2. var e = ev || window.event;
  3. }, false)

取消冒泡

  • e.stopPropagation(): w3c标准 继承 Event.prototype
  • e.cancelBubble = true:IE写法

兼容性写法封装:

  1. /**
  2. * 封装取消冒泡方法
  3. */
  4. function cancelBubble(e) {
  5. var e = e || window.event;
  6. if (e.stopPropagation) {
  7. e.stopPropagation();
  8. } else {
  9. e.cancelBubble = true;
  10. }
  11. }

取消默认事件

  1. //右键菜单
  2. document.oncontextmenu = function(){
  3. return false;
  4. }
  • e.preventDefault():w3c方法,IE9不兼容
  • e.returnValue = false:IE9以下

作用之一:取消默认链接跳转防止请求后端数据

事件流

描述从页面中接收事件的顺序 冒泡/捕获

  • IE提出事件冒泡流 Event Bubbling
  • 网景Netscape提出事件捕获流 Event Capturing

事件冒泡流

有捕获程序也可以用冒泡的处理函数来写

点击事件源 ,如事件源顶层父级有相同事件源 ,优先执行。

事件捕获流

与冒泡行为相反

事件捕获阶段:

触发事件 -> document发出一个事件流 -> DOM节点 -> 目标元素 -> target(默认是不会执行处理函数)

事件冒泡阶段:

目标 -> DOM节点 -> document/window

事件流的三个阶段:

  • 事件捕获阶段(默认不执行)
  • 处于目标阶段(触发时候) -> 执行事件处理函数
  • 事件冒泡阶段

事件代理

多次重复的绑定事件处理函数

事件代理的核心:事件对象和事件源对象

事件源对象 - event.target

写法一:

  1. //事件委托 事件代理
  2. //e.target 找出事件源
  3. oList.onclick = function (e) {
  4. var e = e || window.event,
  5. tar = e.target || e.srcElement;
  6. console.log(tar);
  7. }

写法二:

  1. var oList = document.getElementsByTagName('ul')[0],
  2. oLi = oList.getElementsByTagName('li'),
  3. oBtn = document.getElementsByTagName('button')[0];
  4. //一样可以动态创建被事件源监听
  5. oBtn.onclick = function (e) {
  6. var li = document.createElement('li');
  7. li.innerText = oLi.length + 1;
  8. oList.appendChisld(li);
  9. }

如何在事件代理的情况下获取下标?

写法一:缺点不能在大量数据下使用

  1. var oList = document.getElementsByTagName('ul')[0],
  2. oLi = oList.getElementsByTagName('li'),
  3. oBtn = document.getElementsByTagName('button')[0],
  4. len = oLi.length,
  5. item;
  6. oList.onclick = function (e) {
  7. var e = e || window.event,
  8. tar = e.target || e.srcElement;
  9. for(var i = 0; i < len; i++){
  10. item = oLi[i];
  11. //循环的当前项跟点击的事件源相等
  12. if(tar === item){
  13. consolo.log(i);
  14. }
  15. }
  16. }

写法二;利用数字IndeOf方法(企业级写法,骚操作)

  1. var oList = document.getElementsByTagName('ul')[0],
  2. oLi = oList.getElementsByTagName('li'),
  3. oBtn = document.getElementsByTagName('button')[0],
  4. len = oLi.length,
  5. item;
  6. oList.onclick = function (e) {
  7. var e = e || window.event,
  8. tar = e.target || e.srcElement;
  9. //注意:oLi列表是类数组,不能使用数组方法
  10. //解决方法:继承Array.prototype
  11. //indexOf(数组元素) 这里的元素刚好跟tar事件源的值一样
  12. //Array.prototype.indexOf.call(DOM对象集合, 当前事件源);
  13. idx = Array.prototype.indexOf.call(oLi, tar);
  14. console.log(idx);
  15. }

写法三:利用target事件源对象的tagName属性筛选冒泡对象

  1. var oList = document.getElementsByTagName('ul')[0],
  2. oLi = oList.getElementsByTagName('li'),
  3. oBtn = document.getElementsByTagName('button')[0],
  4. len = oLi.length,
  5. item;
  6. oList.onclick = function (e) {
  7. var e = e || window.event,
  8. tar = e.target || e.srcElement,
  9. tagName = tar.tagName.toLowerCase();
  10. if(tagName === 'li'){
  11. console.log(tar.innerText);
  12. }
  13. }

面试问题:

实现:请往ul添加50个li,并且给li添加删除功能,考虑性能

写法一:有createElement,效率不高

  1. var box = document.getElementsByClassName('box')[0],
  2. oUl = document.createElement('ul'),
  3. oFrag = document.createDocumentFragment(),
  4. liNum = 50;
  5. for (var i = 0; i < liNum; i++) {
  6. var oLi = document.createElement('li');
  7. oLi.innerHTML = '第' + (i + 1) + '项 <a href="javascript:;">删除</a>';
  8. oLi.className = 'list-item';
  9. oFrag.appendChild(oLi);
  10. }
  11. oUl.appendChild(oFrag);
  12. box.appendChild(oUl);

写法二:

  1. var box = document.getElementsByClassName('box')[0],
  2. oUl = document.createElement('ul'),
  3. liNum = 50,
  4. list = '';
  5. for (var i = 0; i < liNum; i++) {
  6. list += '<li>第' + i + '项 <a href="javascript:;">删除</a></li>';
  7. }
  8. oUl.innerHTML = list;
  9. box.appendChild(oUl);

写法三:

  1. //用模板
  2. var oUl = document.createElement('ul'),
  3. tpl = document.getElementById('tpl').innerHTML,
  4. list = '';
  5. for (var i = 0; i < 50; i++) {
  6. list += tpl.replace(/{{(.*?)}}/, (i + 1));
  7. }
  8. oUl.innerHTML = list;

鼠标坐标

坐标系

  • clientX/Y :鼠标位置相对于当前可视区域的坐标(不包括滚动条的距离)
  • pageX/pageY:鼠标位置相对于当前文档的坐标(包含滚动条的距离), IE9以下不支持
  • screenX/Y:鼠标位置相对于屏幕的坐标
  • X/Y: 同clientX/Y相当 ,fireFox不支持
  • layerX/Y:同pageX/Y相同,IE11以下同clientX/Y
  • offsetX/Y:鼠标位置相对于块元素的坐标(包括边框),safari不包括

分析:

  1. document.onclick = function (e) {
  2. var e = e || window.event;
  3. console.log(e.clientY, e.pageY);
  4. //数字相同 但滚动条下,clientY 数值 < pageY 数值
  5. console.log(e.screenY, e.pageY);
  6. //256 100 screenY = 屏幕坐标
  7. console.log(e.y, e.pageY);
  8. //数字相同 但滚动条下,y 数值 < pageY 数值
  9. //跟clientY数值一样
  10. console.log(e.layerY, e.pageY); IE11以下
  11. //数字相同 滚动条下也相同
  12. console.log(e.offsetY, e.pageY);
  13. //offsetY 鼠标位置相对于块元素的坐标
  14. //注意:包含边框 , safari不包括
  15. }

兼容性写法:

  1. /**
  2. * 封装页面坐标函数pagePos()
  3. * @e 元素
  4. * @返回值 页面内的x/y坐标
  5. */
  6. function pagePos(e) {
  7. //获取滚动条距离
  8. //使用获取滚动条距离函数
  9. var sLeft = getScrollOffset().left,
  10. sTop = getScrollOffset().top,
  11. //获取文档偏移
  12. //documentElement.clientLeft IE8及以下不存在(undefined)
  13. cLeft = document.documentElement.clientLeft || 0,
  14. cTop = document.documentElement.clientTop || 0;
  15. return {
  16. //可视区域坐标 + 滚动条距离 - 偏移距离
  17. X: e.clientX + sLeft - cLeft,
  18. Y: e.clientY + sTop - cTop
  19. }
  20. }

拖拽

  • mousedown
  • mouseup
  • mousemove

分析:

  1. //鼠标按下并且移动写法:
  2. //mousedown: mouseover + mouseup
  3. //鼠标的移动和抬起要在鼠标按下事件处理函数内部
  4. //Bug: 鼠标永远停留在块元素的左上
  5. box.onmousedown = function (e) {
  6. //情况1:在box块元素上移动
  7. box.onmousemove = function (e) {
  8. var e = e || window.event;
  9. page = pagePos(e);
  10. box.style.left = page.X + 'px';
  11. box.style.top = page.Y + 'px';
  12. //注:以上代码鼠标移出目标元素块会使元素块停止不动
  13. //原因:帧率跟不上
  14. }
  15. //情况2:解决上述停止不动的问题
  16. document.onmousemove = function (e) {
  17. var e = e || window.event;
  18. page = pagePos(e);
  19. box.style.left = page.X + 'px';
  20. box.style.top = page.Y + 'px';
  21. //注:以上代码鼠标移出目标元素块会使元素块停止不动
  22. //原因:帧率跟不上
  23. }
  24. box.onmouseup = function (e) {
  25. //鼠标抬起取消事件处理函数
  26. document.onmousemove = null;
  27. }
  28. }
  1. // 事件拖拽方法
  2. box.onmousedown = function (e) {
  3. var e = e || window.event;
  4. page = pagePos(e);
  5. //通过getStyles()拿到盒子x,y位置
  6. //盒子边缘到盒子内部鼠标位置 = 页面坐标 - 盒子坐标
  7. x = page.X - getStyles(box, 'left');
  8. y = page.Y - getStyles(box, 'top');
  9. console.log(x, y);
  10. document.onmousemove = function (e) {
  11. var e = e || window.event;
  12. page = pagePos(e);
  13. //盒子到页面边缘距离 = 页面坐标 - 盒子边缘到盒子内部鼠标位置
  14. box.style.left = page.X - x + 'px';
  15. box.style.top = page.Y - y + 'px';
  16. box.onmouseup = function (e) {
  17. //鼠标抬起取消事件处理函数
  18. document.onmousemove = null;
  19. }
  20. }
  21. }

封装拖拽函数:

  1. /**
  2. * 封装拖拽函数elemDrag()
  3. * @elem 元素
  4. */
  5. function elemDrag(elem) {
  6. var x,
  7. y;
  8. addEvent(elem, 'mousedown', function (e) {
  9. var e = e || window.event;
  10. //盒子边缘到盒子内部鼠标位置 = 页面坐标 - 盒子坐标
  11. x = pagePos(e).X - getStyles(elem, 'left');
  12. y = pagePos(e).Y - getStyles(elem, 'top');
  13. addEvent(document, 'mousemove', mouseMove);
  14. addEvent(document, 'mouseup', mouseUp);
  15. //去掉冒泡和默认事件
  16. cancelBubble(e);
  17. preventDefaultEvent(e);
  18. })
  19. function mouseMove(e) {
  20. //盒子到页面边缘距离 = 页面坐标 - 盒子边缘到盒子内部鼠标位置
  21. var e = e || window.event;
  22. elem.style.left = pagePos(e).X - x + 'px';
  23. elem.style.top = pagePos(e).Y - y + 'px';
  24. }
  25. function mouseUp(e) {
  26. var e = e || window.event;
  27. removeEvent(document, 'mousemove', mouseMove);
  28. removeEvent(document, 'mouseup', mouseUp);
  29. }
  30. }

鼠标点击抬起顺序问题

  1. var oBtn = document.getElementsByTagName("button")[0];
  2. //顺序: mousedown > mouseup > click
  3. //即: mousedown + mouseup = click
  4. oBtn.onclick = function () {
  5. console.log("click");
  6. };
  7. oBtn.onmousedown = function () {
  8. console.log("mousedown");
  9. };
  10. oBtn.onmouseup = function () {
  11. console.log("mouseup");
  12. };

模块:鼠标拖拽功能带单双击事件

  1. /**
  2. * 模块:鼠标拖拽功能带单双击事件
  3. * 功能一:拖拽
  4. * 功能二:点击可以跳转链接
  5. *
  6. * 解决现象:拖拽和点击如何分离
  7. * 解决方案:记录时间前后,新增一个点击事件
  8. *
  9. * 解决现象:拖拽和点击同时存在
  10. * 解决方案:记录原来位置
  11. */
  12. var oLink = document.getElementsByTagName('a')[0];
  13. var dragNclick = (function (elem, elemClick) {
  14. //记录时间
  15. var bTime = 0,
  16. eTime = 0,
  17. //记录坐标
  18. oPos = [];
  19. //执行
  20. drag(elem);
  21. function drag(elem) {
  22. var x,
  23. y;
  24. addEvent(elem, 'mousedown', function (e) {
  25. var e = e || window.event;
  26. //记录鼠标按下时间戳
  27. bTime = new Date().getTime();
  28. //记录原来的位置
  29. oPos = [getStyles(elem, 'left'), getStyles(elem, 'top')];
  30. //盒子边缘到盒子内部鼠标位置 = 页面坐标 - 盒子坐标
  31. x = pagePos(e).X - getStyles(elem, 'left');
  32. y = pagePos(e).Y - getStyles(elem, 'top');
  33. addEvent(document, 'mousemove', mouseMove);
  34. addEvent(document, 'mouseup', mouseUp);
  35. //去掉冒泡和默认事件
  36. cancelBubble(e);
  37. preventDefaultEvent(e);
  38. })
  39. function mouseMove(e) {
  40. //盒子到页面边缘距离 = 页面坐标 - 盒子边缘到盒子内部鼠标位置
  41. var e = e || window.event;
  42. elem.style.left = pagePos(e).X - x + 'px';
  43. elem.style.top = pagePos(e).Y - y + 'px';
  44. }
  45. function mouseUp(e) {
  46. var e = e || window.event;
  47. //记录鼠标抬起时间戳
  48. eTime = new Date().getTime();
  49. //结束时间 - 开始时间 < 100ms 为点击事件
  50. if (eTime - bTime < 100) {
  51. //除了拖拽操作,还能点击链接跳转
  52. //执行elemDrag函数传过来的第二个参数
  53. elemClick();
  54. //把记录好的坐标在点击时显示
  55. elem.style.left = oPos[0] + 'px';
  56. elem.style.top = oPos[1] + 'px';
  57. }
  58. removeEvent(document, 'mousemove', mouseMove);
  59. removeEvent(document, 'mouseup', mouseUp);
  60. }
  61. }
  62. });
  63. //执行函数另外传一函数作为参数
  64. dragNclick(oLink, function () {
  65. window.open('http://www.baidu.com');
  66. });

模块:鼠标拖拽功能带单双击事件(原型)

  1. /**
  2. * 模块:鼠标拖拽功能带单双击事件(原型)
  3. * 功能一:拖拽
  4. * 功能二:点击可以跳转链接
  5. *
  6. * 解决现象:拖拽和点击如何分离
  7. * 解决方案:记录时间前后,新增一个点击事件
  8. *
  9. * 解决现象:拖拽和点击同时存在
  10. * 解决方案:记录原来位置
  11. *
  12. * 解决现象:this指向问题
  13. * 解决方案:call和缓存_self
  14. *
  15. * 解决现象:边缘问题
  16. * 解决方案:记录边界坐标根据边缘计算
  17. *
  18. * 解决现象:右键问题
  19. * 解决方案:e.button 左中右(0/1/2) IE10及以下不兼容
  20. *
  21. * 解决现象:双击问题
  22. * 解决方案:记录双击时间
  23. */
  24. var oLink = document.getElementsByTagName('a')[0],
  25. oMenu = document.getElementsByTagName('div')[0];
  26. Element.prototype.dragNclick = (function (elemClick, menu) {
  27. //记录时间
  28. var bTime = 0,
  29. eTime = 0,
  30. //双击开始时间
  31. cbTime = 0,
  32. //双击结束时间
  33. ceTime = 0,
  34. //双击次数
  35. counter = 0,
  36. //计时器
  37. t = null,
  38. //记录坐标
  39. oPos = [],
  40. //可视窗口宽度
  41. wWidth = getViewportSize().width,
  42. //可视窗口高度
  43. wHeight = getViewportSize().height,
  44. //块元素的宽度
  45. eleWidth = getStyles(this, 'width'),
  46. //块元素的高度
  47. eleHeight = getStyles(this, 'width'),
  48. //右键盒子的宽度
  49. mWidth = getStyles(menu, 'width'),
  50. //右键盒子的高度
  51. mHeight = getStyles(menu, 'height');
  52. // console.log(this); //a
  53. drag.call(this);
  54. function drag() {
  55. var x,
  56. y,
  57. _self = this;
  58. // console.log(this); //window -> call -> a
  59. addEvent(this, 'mousedown', function (e) {
  60. var e = e || window.event,
  61. //记录按键编码
  62. btnCode = e.button;
  63. // console.log(this); //a from drag(AO)
  64. //右键
  65. if (btnCode === 2) {
  66. var mLeft = pagePos(e).X,
  67. mTop = pagePos(e).Y;
  68. //打开一个菜单
  69. if (mLeft <= 0) {
  70. mLeft = 0;
  71. } else if (mLeft >= wWidth - mWidth) {
  72. mLeft = pagePos(e).X - mWidth;
  73. }
  74. if (mTop <= 0) {
  75. mTop = 0;
  76. } else if (mTop >= wHeight - mHeight) {
  77. mTop = pagePos(e).Y - mHeight;
  78. }
  79. menu.style.left = mLeft + 'px';
  80. menu.style.top = mTop + 'px';
  81. menu.style.display = 'block';
  82. } else if (btnCode === 0) {
  83. //左键
  84. //记录鼠标按下时间戳
  85. bTime = new Date().getTime();
  86. //记录原来的位置
  87. oPos = [getStyles(_self, 'left'), getStyles(_self, 'top')];
  88. menu.style.display = 'none';
  89. //盒子边缘到盒子内部鼠标位置 = 页面坐标 - 盒子坐标
  90. x = pagePos(e).X - getStyles(_self, 'left');
  91. y = pagePos(e).Y - getStyles(_self, 'top');
  92. addEvent(document, 'mousemove', mouseMove);
  93. addEvent(document, 'mouseup', mouseUp);
  94. //去掉冒泡和默认事件
  95. cancelBubble(e);
  96. preventDefaultEvent(e);
  97. }
  98. })
  99. //去除默认右键菜单
  100. addEvent(document, 'contextmenu', function (e) {
  101. var e = e || window.e;
  102. preventDefaultEvent(e);
  103. })
  104. //右键盒子以外区域点击会隐藏该盒子
  105. addEvent(document, 'click', function (e) {
  106. menu.style.display = 'none';
  107. })
  108. //取消当前盒子的冒泡行为
  109. addEvent(menu, 'click', function (e) {
  110. var e = e || window.e;
  111. cancelBubble(e);
  112. })
  113. function mouseMove(e) {
  114. var e = e || window.event,
  115. //记录边界坐标
  116. eleLeft = pagePos(e).X - x,
  117. eleTop = pagePos(e).Y - y;
  118. //到达边缘的情况
  119. //靠左边缘
  120. if (eleLeft <= 0) {
  121. eleLeft = 0;
  122. } else if (eleLeft >= wWidth - eleWidth) {
  123. //靠右边缘
  124. eleLeft = wWidth - eleWidth - 1;
  125. }
  126. //到达边缘的情况
  127. //靠顶边缘
  128. if (eleTop <= 0) {
  129. eleTop = 0;
  130. } else if (eleTop >= wHeight - eleHeight) {
  131. //靠下边缘
  132. eleTop = wHeight - eleHeight - 1;
  133. }
  134. //盒子到页面边缘距离 = 页面坐标 - 盒子边缘到盒子内部鼠标位置
  135. _self.style.left = eleLeft + 'px';
  136. _self.style.top = eleTop + 'px';
  137. }
  138. function mouseUp(e) {
  139. var e = e || window.event;
  140. //记录鼠标抬起时间戳
  141. eTime = new Date().getTime();
  142. //结束时间 - 开始时间 < 100ms 为点击事件
  143. if (eTime - bTime < 100) {
  144. //把记录好的坐标在点击时显示
  145. _self.style.left = oPos[0] + 'px';
  146. _self.style.top = oPos[1] + 'px';
  147. counter++;
  148. //第一次时 点击第一次
  149. if (counter === 1) {
  150. cbTime = new Date().getTime();
  151. }
  152. //第二次时 点击第二次
  153. if (counter === 2) {
  154. ceTime = new Date().getTime();
  155. }
  156. //证明双击了
  157. if (cbTime && ceTime && (ceTime - cbTime < 200)) {
  158. //除了拖拽操作,还能点击链接跳转
  159. //执行elemDrag函数传过来的第二个参数
  160. elemClick();
  161. }
  162. t = setTimeout(function () {
  163. cbTime = 0;
  164. ceTime = 0;
  165. counter = 0;
  166. clearTimeout(t)
  167. }, 500)
  168. }
  169. removeEvent(document, 'mousemove', mouseMove);
  170. removeEvent(document, 'mouseup', mouseUp);
  171. }
  172. }
  173. });
  174. //执行函数另外传一函数作为参数
  175. oLink.dragNclick(function () {
  176. window.open('http://www.baidu.com');
  177. }, oMenu);

输入框事件

oninput&onpropertychange

只要在输入框中填写或删除内容,里面显示内容

  1. var content = document.getElementById('content');
  2. //HTML5新增接口
  3. //IE11支持/IE10支持/IE9支持/IE8不支持
  4. content.oninput = function () {
  5. console.log(this.value); //返回输入框输入内容
  6. }
  7. //IE11支持/IE10支持/IE9支持/IE8支持
  8. content.onpropertychange = function () {
  9. console.log(this.value);
  10. }

onchange

获得输入框失去焦点后的文本内容,若不更改值,失去焦点不会变化

  • 失去焦点才会触发
  • 聚集/失去焦点时,值没有变化不会触发
  1. content.onchange = function () {
  2. console.log(this.value);
  3. }

onfocus&onblur

  1. //获得焦点
  2. content.onfocus = function () {
  3. this.className = 'focus';
  4. }
  5. //失去焦点
  6. content.onblur = function () {
  7. this.className = '';
  8. }

placeholder属性在各个浏览存在不一样的情况,不推荐使用,用onfocusonblur代替使用

  1. <input
  2. type="text"
  3. id="content"
  4. value="请输入关键字"
  5. class="search-input"
  6. onfocus="if(this.value==='请输入关键字'){ this.value = ''; this.className='search-input has-value';}"
  7. onblur="if(this.value === ''){ this.value = '请输入关键字'; this.className = 'search-input';}">

鼠标滑入滑出

onmouseover&onmouseout

存在冒泡现象,影响子元素

mouseenter&mouseleave

只对被绑定元素进行事件触发(只对被绑定元素生效)

存在类似冒泡现象,也影响子元素

适合复杂情况,dom比较复杂,可控性比较强,给某一些元素设置,为了互不影响

键盘事件

keydown&keyup

问题:keydown+keyup =? keypress

答案:不成立

原因:

如果同时执行,执行结果顺序为 keydown > keypress > keyup

  1. document.onkeydown = function () {
  2. console.log('keydown');
  3. }
  4. document.onkeyup = function () {
  5. console.log('keyup');
  6. }
  7. document.onkeypress = function () {
  8. console.log('keypress');
  9. }

现象:

按住键盘不松开,keydown一直触发多次,直到松开键盘keyup才触发一次

注:

  • 键盘事件的构造函数为KeyboardEvent
  • 键盘顺位码为keyCode
  • ASCII码为charCode

问题:keydown&keypress有什么区别?

  • keydown没有charCode但有keyCode
  • keypress有charCode和keyCode

应用:

  1. //这里keypress可以用但keydown不能用因为keypress底下有charCode
  2. document.onkeypress = function (e) {
  3. var str = String.fromCharCode(e.charCode);
  4. console.log(str);
  5. }

示例:

  1. //键盘上下左右控制小盒子移动
  2. var box = document.getElementsByClassName('box')[0];
  3. document.onkeydown = function (e) {
  4. var e = e || window.event,
  5. code = e.keyCode,
  6. boxLeft = getStyles(box, 'left'),
  7. boxTop = getStyles(box, 'top');
  8. switch (code) {
  9. //按左按键往左走
  10. case 37:
  11. box.style.left = boxLeft - 5 + 'px';
  12. break;
  13. //按右按键往右走
  14. case 39:
  15. box.style.left = boxLeft + 5 + 'px';
  16. break;
  17. //按上按键往上走
  18. case 38:
  19. box.style.top = boxTop - 5 + 'px';
  20. break;
  21. //按下按键往下走
  22. case 40:
  23. box.style.top = boxTop + 5 + 'px';
  24. break;
  25. default:
  26. break;
  27. }
  28. }

ASCII码表

dom - 图1

dom - 图2

dom - 图3

页面渲染

HTML/CSS/JavaScript组成动态页面

domTree

页面渲染机制:

解析和加载的例子:img先解析完后加载资源

  • 解析 :

    • domTree 树型结构 ,解析引擎将html结构转为树形结构
    • DOM树满足一个深度优先解析的原则
    • DOM树构建是html元素节点的解析
    • DOM树不管内部资源的问题
  • 加载:

    • 解析的过程伴随着加载的开始
    • 先有了解析才会有加载
    • 解析和加载异步完成的

注意:

  • style="display: none"会受解析影响
  • createElement出来的元素也会受解析影响

cssTree

CSS树(样式结构体)也需要构建的,跟dom树构建类型,

  • 也满足深度优先
  • 会忽略浏览器不能识别的样式

渲染树

渲染树 renderTree(即domTree + cssTree),浏览器会根据它绘制页面,说明绘制之前domTree和cssTree已经构建完毕了

  1. 渲染树每个节点都有自己的样式
  2. 不包含隐藏节点(display: none, head之类不需要绘制的节点)
  3. 包含影响布局的样式都要绘制(visibility: hidden)
  4. 渲染树上的每一个节点都会被当作一个具有内容填充,边距,边框,位置,大小等其他样式的盒子

回流重绘

当JS对页面的节点操作时,就会产生回流或者重绘

  • 回流也叫重排,reflow,一定会引起重绘
  • 重绘叫repaint,不一定时回流产生的后续反应

因为节点的尺寸,布局,display:none这些改变的时候,渲染树中的一部分或全部需要重新构建的现象,叫做回流。一个页面至少有一次回流。

上述操作之外的,更背景颜色,字体颜色等就不会引起回流,但引起重绘。

总结:

回流时,浏览器会重新构建受影响部分的渲染树,渲染树一旦被改变或重新构建一定会引起重绘。

回流完成时,浏览器会根据新的渲染树重新绘制回流影响的部分或全部节点,这个重新绘制的过程叫做重绘。

引起回流的因素:

  • DOM节点增加,删除
  • DOM节点位置变化
  • 元素的尺寸,边距,填充文字/图片,边框,宽高
  • DOM节点display显示或隐藏
  • 页面的渲染初始化
  • 浏览器窗口变化
  • 向浏览器请求某些样式信息offset/scroll/client/width/height/getComputeStyle()/currentStyle

尽可能减少回流,减少DOM操作对性能的消耗

  1. //回流 + 重绘
  2. oBoxStyle.width = '200px';
  3. //回流 + 重绘
  4. oBoxStyle.height = '200px';
  5. //回流 + 重绘
  6. oBoxStyle.margin = '200px';
  7. //重绘
  8. oBoxStyle.backgroundColor = 'green';
  9. //回流 + 重绘
  10. oBoxStyle.border = '5px solid orange';
  11. //重绘
  12. oBoxStyle.color = '#fff';
  13. //回流 + 重绘
  14. oBoxStyle.fontSize = '30px';

如何优化:

  • 要考虑回流次数的问题
  • 要考虑涉及节点数量

浏览器减少回流和重绘的方法:

  • 队列策略

时间线

定义:在浏览器加载页面开始的那一刻到页面加载完全结束的这个过程中,按顺序发送的每一件事情的总流程。

  1. 生成document对象(JS起作用)
  2. 解析文档,构建DOM树,document.readyState = 'loading'页面加载第一阶段:加载中
  3. 遇到link标签会新开线程异步加载css外部资源文件,style开始构建CSS树
  4. 没有设置异步加载的script标签,阻塞文档解析,等待JS脚本加载并执行完成后,继续解析文档
  5. 异步加载script,异步加载JS脚本并执行,不阻塞解析文档,不能使用document.write()
  6. 解析文档遇到img先解析这个节点 ,创建加载线程,异步加载图片资源,不阻塞解析文档
  7. 文档解析完成且可以交互,document.readyState = 'interactive'页面加载第二阶段:解析完成
  8. 文档解析完成后立马defer script JS脚本开始按照顺序执行
  9. 文档解析完成后立马触发DOMContentLoaded事件,可以监控文档解析完成(不等于文档加载完成)时间,同步程序的脚本执行阶段往事件驱动阶段
  10. async script加载并执行完毕,img等资源加载完毕,window对象中的onload事件才触发,document.readyState = 'complete'页面加载第三阶段:页面加载完成

关于onreadystatechange,基于JS引擎监听的:只要文档状态改变了就触发事件处理函数

问题:window.onloadDOMContentLoaded的区别?

答:文档解析完成后立马触发DOMContentLoaded事件,而window.onload是等到页面加载完成时才触发,所以不用window.onload是因为浪费时间

现代浏览器

构建布局渲染过程

现代浏览器为了更好的用户体验,渲染引擎尝试尽快将页面渲染到屏幕,在解析的过程中已经开始渲染,不是等待完毕再渲染,初次绘制概念,一边解析一边构建一边渲染

渲染引擎

浏览器组成部分

  1. 用户界面
  2. 浏览器引擎:让浏览器运行的程序接口集合,主要查询和操作渲染引擎
  3. 渲染引擎:解析HTML/CSS,将解析的结果渲染到页面的程序
  4. 网络:进行网络请求的程序
  5. UI后端:绘制组合选择框及对话框等基本组件的程序
  6. JS解释器:解析执行JS代码的程序
  7. 数据存储:浏览器存储相关的程序 cookie/storage

渲染是什么?

用一个特点的软件将模型(一个程序)装华为用户能看到的图像的过程

渲染引擎是什么?

内容具备一套绘制图像方法集合,渲染引擎可以让特定的方法执行,把HTML/CSS代码解析成图像显示再浏览器窗口中

渲染树

dom - 图4

JS执行机制

浏览器渲染引擎进程(浏览器内核)是多线程的:

  1. JS引擎线程(单线程)DOM冲突
  2. GUI线程(互斥)
  3. http网络请求程序 webAPIs
  4. 定时器触发线程
  5. 浏览器事件处理线程

数据量大会怎样?

  • 方法一: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

异步加载

同步/异步加载的三种方法

一般来说,异步加载是 浏览器做的

如何自己实现一个异步加载?

企业级写法:

  1. //企业级写法:匿名空间
  2. var utils = {
  3. test: function () {
  4. console.log("test");
  5. },
  6. demo: function () {
  7. console.log("demo");
  8. },
  9. };
  10. utils.test();

同步加载:

阻塞模式,同步模式,浏览器加载默认同步加载状态如link标签同步加载的同时dom解析

为什么不能浏览器引擎解析时不能异步加载,因为DOM解析的情况不明确,会影响加载的过程,会有报错。也是script标签放最后的原因,等上面的标签都加载完毕才执行JS脚本

异步加载:

浏览器并行加载

  • 关于defer IE8及以下,defer加载完后等待DOM解析完成才执行
  • 关于async,W3C标准,HTML5属性,IE9及以上支持async="async",不等DOM解析完,立马执行

使用说明:

  • 不要用文档结构查找更改删除操作的
  • deferasync除了IE以外都兼容
  • 按需加载情况用异步加载来做

第三种方法:

  1. var s = document.createElement("script");
  2. s.type = "text/javascript";
  3. s.async = true;
  4. s.src = "utils.js";
  5. //执行
  6. document.body.appendChild(s);

等待页面全部加载完毕后,进行异步加载操作

  1. //阻塞onload执行
  2. function async_load() {
  3. var s = document.createElement("script");
  4. s.type = "text/javascript";
  5. s.async = true;
  6. s.src = "utils.js";
  7. //执行
  8. document.body.appendChild(s);
  9. }
  10. if (window.attachEvent) {
  11. window.attachEvent("onload", async_load);
  12. } else {
  13. window.addEventListener("load", async_load, false);
  14. }