Dom事件
Model(特定领域对象的抽象)、Protocal(针对数据格式的约定)
DOM(Document Object Model,文档对象模型)是针对HTML文档和XML文档的一个API。DOM描绘了一个层次化的节点树,允许开发人员添加、移出和修改页面的某一部分,DOM 脱胎于Netscape 及微软公司创始的 DHTML(动态HTML)。但现在它已经成为表现和操作页面标记的真正跨平台、语言中立的方式。
Netscape Navigator 4 和 IE4 分别发布于 1997 年的 6 月和 10 月发布的 DHTML,由于 IE4 和 Netscape Navigator4 分别支持不同的 DHTML,为了统一标准,W3C开始制定 DOM。1998 年10 月 W3C 总结了 IE 和 Navigator4 的规范,制定了 DOMLevel 1即 DOM1,之前 IE 与 Netscape 的规范则被称为 DOMLevel 0 即 DOM0 。
DOM0级事件
btn.onclick = function(){console.log('this is a click event')
}
事件就是用户或浏览器自身执行的某种操作,如click、load、mouseover等,都是事件的名字,而响应某个事件的函数就被称为事件处理程序。
click事件过程
在上述的例子中,click 事件并没有像其他函数一样,必须要调用才可以执行,click 事件并不确定什么时候发生,而当浏览器发现用户点击该按钮时,浏览器就检测btn.onclick是否有值,如果有,就会执行btn.onclick.call(btn,event),此时函数执行,call() 方法接收两个参数,第一个指向调用当前方法的对象,也就是this。
需要注意的是,指定的 this 值并不一定是该函数执行时真正的this值,如果这个函数处于非严格模式下,则指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。
另一个参数则是事件对象 event,该对象也可以通过 arguments[0] 来访问,它包含了事件相关的所有信息,如本例子中,则包含了点击事件的全部信息。可以通过给函数传参来获取事件信息。
btn.onclick = function(e){console.log('this is a click event');console.log(e); // 事件对象
}
但是在 IE 中,在使用 DOM0 级方法添加事件处理程序时,event 是作 window 对象的一个属性而存在的。此时访问事件对象需要通过 window.event。
btn.onclick = function(){console.log(window.event); // IE中事件对象
}
在 DOM0级中,如果想要实现一个对象绑定多个函数,可以这样实现:
function fn1(){// do something
}
function fn2(){// do something
}
btn.onclick = function(e){fn1.call(this.xxx);fn2.call(this.yyy);
}
DOM1级事件
DOM级别1于1998年10月1日成为W3C推荐标准。1级DOM标准中并没有定义事件相关的内容,所以没有所谓的1级DOM事件模型。在2级DOM中除了定义了一些DOM相关的操作之外还定义了一个事件模型 ,这个标准下的事件模型就是我们所说的2级DOM事件模型。
DOM2级事件
W3C 后来将 DOM1 升级为 DOM2,DOM2级规范开始尝试以一种符合逻辑的方式来标准化 DOM事件。DOM0级 可以认为 onclick 是 btn 的一个属性,DOM2级 则将属性升级为队列。
DOM2级 事件定义了两个方法,用于处理指定和删除事件处理程序的操作,addEventListener()和removeEventListener(),所有的 DOM 节点中都包含这两个方法,它们都接收 3 个参数。
- 要处理的事件名;
- 作为事件处理程序的函数;
- 布尔值,true 代表在捕获阶段调用事件处理程序,false 表示在冒泡阶段调用事件处理程序,默认为 false;
btn.addEventListener('click',function(){// do something
})
btn.addEventListener('click',function(){// do something else
})
addEventListener()将事件加入到监听队列中,当浏览器发现用户点击按钮时,click 队列中依次执行匿名函数1、匿名函数2。
function fn1(){// do something
}
function fn1(){// do something else
}
btn.addEventListener('click',fn1)
btn.addEventListener('click',fn2)
如果这样写,click 队列中依次fn1.call(btn,event),fn2.call(btn,event)。
通过addEventListener()添加的事件只能由removeEventListener()来移除,并且removeEventListener()只能移除具名函数,不能移除匿名函数。
IE 中 DOM2级事件
IE8 及之前,实现类似addEventListener()和removeEventListener()的两个方法是attachEvent()和detachEvent(),这两个方法接受相同的两个参数。
- 要处理的事件名;
- 作为事件处理程序的函数;
IE8 之前的只支持事件冒泡,所以通过attachEvent()添加的事件处理程序只能添加到冒泡阶段。
btn.attachEvent('click',fn1)
btn.attachEvent('click',fn2)
当用户点击时,click 队列依次fn1.call(undefined,undefined),fn2.call(undefined,undefined)。
类似的detachEvent()也只能移除具名函数,不能移除匿名函数。
function eventHandler() {console.log('xianzao);
}btn.attachEvent('onClick', eventHandler);
btn.detachEvent('onClick, eventHandler);
兼容处理
if(typeof btn.addEventListener === 'function'){btn.addEventListener('click',fn);
}else if(typeof btn.attachEvent === 'function'){btn.attachEvent('onclick',fn)
}else{btn.onclick=function(){// do something}
}
总结
var btn = document.getElementById('btn');btn.onClick = () => {console.log('我是DOM0级事件处理程序');
}
btn.onClick = null;btn.addEventListener('click', () => {console.log('我是DOM2级事件处理程序');
}, false);
btn.removeEventListener('click', handler, false)btn.attachEvent('onclick', () => {console.log('我是IE事件处理程序')
})
btn.detachEvent('onclicn', handler);
- DOM2级的好处是可以添加多个事件处理程序;DOM0对每个事件只支持一个事件处理程序;
- 通过DOM2添加的匿名函数无法移除,上面写的例子就移除不了,addEventListener和removeEventListener的handler必须同名;
- 作用域:DOM0的handler会在所属元素的作用域内运行,IE的handler会在全局作用域运行,this === window;
- 触发顺序:添加多个事件时,DOM2会按照添加顺序执行,IE会以相反的顺序执行;
- 跨浏览器的事件处理程序
var EventUtil = {// element是当前元素,可以通过getElementById(id)获取// type 是事件类型,一般是click ,也有可能是鼠标、焦点、滚轮事件等等// handle 事件处理函数addHandler: (element, type, handler) => {// 先检测是否存在DOM2级方法,再检测IE的方法,最后是DOM0级方法(一般不会到这)if (element.addEventListener) {// 第三个参数false表示冒泡阶段element.addEventListener(type, handler, false);} else if (element.attachEvent) {element.attachEvent(`on${type}`, handler)} else {element[`on${type}`] = handler;}},removeHandler: (element, type, handler) => {if (element.removeEventListener) {// 第三个参数false表示冒泡阶段element.removeEventListener(type, handler, false);} else if (element.detachEvent) {element.detachEvent(`on${type}`, handler)} else {element[`on${type}`] = null;}}
}// 获取元素
var btn = document.getElementById('btn');
// 定义handler
var handler = function(e) {console.log('我被点击了');
}
// 监听事件
EventUtil.addHandler(btn, 'click', handler);
// 移除事件监听
// EventUtil.removeHandler(button1, 'click', clickEvent);
事件捕获&事件冒泡
事件流描述的是从页面中接收事件的顺序
IE 的事件流是事件冒泡流
而 Netscape Communicator 的事件流是事件捕获流
DOM2级事件规定的事件流包括三个阶段:
- 事件捕获阶段;
- 处于目标阶段;
- 事件冒泡阶段;
首先发生的是事件捕获,为截获事件提供了机会。
然后是实际的目标接收到事件。
最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应
- 当处于目标阶段,没有捕获与冒泡之分,执行顺序会按照addEventListener的添加顺序决定,先添加先执行;
- 使用stopPropagation()取消事件传播时,事件不会被传播给下一个节点,但是,同一节点上的其他listener还是会被执行;如果想要同一层级的listener也不执行,可以使用stopImmediatePropagation();
// list 的捕获
$list.addEventListener('click', (e) => {console.log('list capturing');e.stopPropagation();
}, true)// list 捕获 2
$list.addEventListener('click', (e) => {console.log('list capturing2');
}, true)// list capturing
// list capturing2
- preventDefault()只是阻止默认行为,跟JS的事件传播一点关系都没有;
- 一旦发起了preventDefault(),在之后传递下去的事件里面也会有效果;
测试
如果有以下例子:
<!DOCTYPE html>
<html>
<head> <title>Event Bubbling Example</title>
</head>
<body> <div id="myDiv">Click Me</div>
</body>
</html>
- 事件捕获
最不具体的节点最先收到事件,而最具体的节点最后收到事件。事件捕获实际上是为了在事件到达最终目标
前拦截事件。
如果前面的例子使用事件捕获,则点击元素会以下列顺序触发 click 事件:
- 事件冒泡
在点击页面中的div元素后,click 事件会以如下顺序发生:
div元素,即被点击的元素,最先触发 click 事件。然后,click 事件沿 DOM 树一路向上,在经过的每个节点上依次触发,直至到达 document 对象。
事件对象
DOM0和DOM2的事件处理程序都会自动传入event对象
IE中的event对象取决于指定的事件处理程序的方法。
IE的handler会在全局作用域运行,this === window,所以在IE中会有window.event、event两种情况,只有在事件处理程序期间,event对象才会存在,一旦事件处理程序执行完成,event对象就会被销毁
event对象里需要关心的两个属性:
- target:target永远是被添加了事件的那个元素;
- eventPhase:调用事件处理程序的阶段,有三个值
a 捕获阶段;
b. 处于目标;
c 冒泡阶段;
preventDefault与stopPropagation
preventDefault:比如链接被点击会导航到其href指定的URL,这个就是默认行为;
stopPropagation:立即停止事件在DOM层次中的传播,包括捕获和冒泡事件;
IE中对应的属性:
- srcElement => target
- returnValue => preventDefaukt()
- cancelBubble => stopPropagation()
IE 不支持事件捕获,因而只能取消事件冒泡,但stopPropagation可以同时取消事件捕获和冒泡。
再针对上面不同类型的事件及属性进行区分:
var EventUtil = {// element是当前元素,可以通过getElementById(id)获取// type 是事件类型,一般是click ,也有可能是鼠标、焦点、滚轮事件等等// handle 事件处理函数addHandler: (element, type, handler) => {// 先检测是否存在DOM2级方法,再检测IE的方法,最后是DOM0级方法(一般不会到这)if (element.addEventListener) {// 第三个参数false表示冒泡阶段element.addEventListener(type, handler, false);} else if (element.attachEvent) {element.attachEvent(`on${type}`, handler)} else {element[`on${type}`] = handler;}},removeHandler: (element, type, handler) => {if (element.removeEventListener) {// 第三个参数false表示冒泡阶段element.removeEventListener(type, handler, false);} else if (element.detachEvent) {element.detachEvent(`on${type}`, handler)} else {element[`on${type}`] = null;}},// 获取event对象getEvent: (event) => {return event ? event : window.event},// 获取当前目标getTarget: (event) => {return event.target ? event.target : event.srcElement},// 阻止默认行为preventDefault: (event) => {if (event.preventDefault) {event.preventDefault()} else {event.returnValue = false}},// 停止传播事件stopPropagation: (event) => {if (event,stopPropagation) {event.stopPropagation()} else {event.cancelBubble = true}}
}
事件委托
事件委托:用来解决事件处理程序过多的问题
页面结构如下
<ul id="myLinks"><li id="goSomewhere">Go somewhere</li><li id="doSomething">Do something</li><li id="sayHi">Say hi</li>
</ul>
按照传统的做法,需要像下面这样为它们添加 3 个事 件处理程序。
var item1 = document.getElementById("goSomewhere");
var item2 = document.getElementById("doSomething");
var item3 = document.getElementById("sayHi");
EventUtil.addHandler(item1, "click", function(event){location.href = "http://www.xianzao.com";
});
EventUtil.addHandler(item2, "click", function(event){document.title = "I changed the document's title";
});
EventUtil.addHandler(item3, "click", function(event){alert("hi");
});
如果在一个复杂的 Web 应用程序中,对所有可单击的元素都采用这种方式,那么结果就会有数不 清的代码用于添加事件处理程序。此时,可以利用事件委托技术解决这个问题。使用事件委托,只需在 DOM 树中尽量最高的层次上添加一个事件处理程序,如下面的例子所示
var list = document.getElementById("myLinks");
EventUtil.addHandler(list, "click", function(event) {event = EventUtil.getEvent(event);var target = EventUtil.getTarget(event);switch(target.id) {case "doSomething":document.title = "I changed the document's title";break;case "goSomewhere":location.href = "http://www.wrox.com";break;case "sayHi": 9 alert("hi");break; }
}
子节点的点击事件会冒泡到父节点,并被这个注册事件处理
最适合采用事件委托技术的事件包括 click、mousedown、mouseup、keydown、keyup 和 keypress。 虽然 mouseover 和 mouseout 事件也冒泡,但要适当处理它们并不容易,而且经常需要计算元素的位置。
可以考虑为 document 对象添加一个事件处理程序,用以处理页面上发生的某种特定类型的事件,需要跟踪的事件处理程序越少,移除它们就越容易(移除事件处理程序关乎内存和性能)。
只要是通过 onload 事件处理程序添加的东西,最后都要通过 onunload 事件处理程序将它们移除。
浏览器请求
在浏览器端发送网络请求的常见方式:
- ajax;
- fetch;
- axios;
ajax
Asynchronous JavaScript And XML,翻译过来就是“异步的 Javascript 和 XML”。
ajax是js异步技术的术语,早起相关的api是XHR。
Ajax 是一个技术统称,是一个概念模型,它囊括了很多技术,并不特指某一技术,它很重要的特性之一就是让页面实现局部刷新。
特点:
- 局部刷新页面,无需重载整个页面。
简单来说,Ajax 是一种思想,XMLHttpRequest 只是实现 Ajax 的一种方式。其中 XMLHttpRequest 模块就是实现 Ajax 的一种很好的方式。
手写ajax
利用 XMLHttpRequest 模块实现 Ajax。
1)创建异步对象
let xmlHttp;
if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, SafarixmlHttp = new XMLHttpRequest();
} else {// code for IE6, IE5xmlHttp = new ActiveXObject('Microsoft.XMLHTTP');
}
通过XMLHttpRequest构造函数创建一个异步对象xmlhttp, IE6, IE5 使用ActiveXObject创建,创建的这个异步对象上有很多属性和方法,常用的有:
- onreadystatechange:监听异步对象请求状态码readyState的改变,每当readyState改变时,就会触发onreadystatechange事件;
- readyState:请求状态码
readyState表示异步对象目前的状态,状态码从0到4:
0: 表示请求未初始化,还没有调用 open();
1: 服务器连接已建立,但是还没有调用 send();
2: 请求已接收,正在处理中(通常现在可以从响应中获取内容头);
3: 请求处理中,通常响应中已有部分数据可用了,没有全部完成;
4: 当readyState状态码为4时,表示请求已完成;此阶段确认全部数据都已经解析完毕,可以通过异步对象的属性获取对应数据; - status:http状态码
http状态码表示成功的http状态码有
xmlHttp.status >= 200 && xmlHttp.status < 300 || xmlHttp.status == 304 - responseText:后台返回的字符串形式的响应数据;
- responseXML:后台返回的XML形式的响应数据;
设置请求方式和请求地址
创建异步对象之后,通过open()方法设置ajax请求方式和请求地址 格式:
xmlHttp.open(“GET/POST”,“ajax-get.txt”,true)
第一个参数:请求的类型;GET 还是 POST;
第二个参数:表示请求的文件的地址url;
第三个参数:设置请求方法是不是异步async,true为异步, false为同步。AJAX存在的意义就是发异步请求,所以第三个参数永远传true;
注意:有个问题,就是IE中的缓存问题
在IE浏览器中,如果通过Ajax发送GET请求,那么IE浏览器认为,同一个URL只有一个结果,如果地址没有发生变化,它就会把上一次返回的结果,直接返回。这样我们不能实时的拿到变化后的数据。如果要想我们拿到实时数据,必须保证每次的URL都是不一样的,有两种方式:
- Math.random();
- new Date().getTime();
即在请求地址后面拼接上?t=随机数或者1970.01.01至当前的毫秒数 所以在IE中通过ajax发送get请求时,可以设置请求地址为:
xmlHttp.open("GET","ajax-get.txt?t=" + (new Date().getTime()),true);
//或
xmlHttp.open("GET","ajax-get.txt?t=" + Math.random(),true);
发送请求
直接通过异步对象的send()发送请求
xmlHttp.send();
特别注意的是: 如果发送POST请求,使用setRequestHeader()来添加 HTTP请求头,并在send()方法中传递要发送的数据:
xmlHttp.open("POST","ajax_test.html",true);
xmlHttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xmlHttp.send("fname=Henry&lname=Ford");
通过onreadystatechange监听状态变化
当异步对象的readyState发生改变,会触发onreadystatechange函数,当readyState变成为4时,表示当前状态是请求完毕的状态,同时当http的响应码status为200到300之间(包括200和300)或为304时,表示ajax请求成功;当http状态码不是200到300之间的数也不是304时,表示请求不成功
//4.监听状态变化
xmlHttp.onreadystatechange = () => {// 判断当前状态改变是请求完毕的状态吗if (xmlHttp.readyState === 4) {if (xmlHttp.status >= 200 && xmlHttp.status < 300 || xmlHttp.status == 304) {console.log("成功的接收到服务器返回的数据");}else{console.log("不成功!");} }
}
处理返回的结果
如果成功,可通过异步对象的responseText属性来获取服务器返回的字符串
接下来,我们来封装一个方法ajax()用于发送请求
封装的时候,需要注意:
- URL当中只能出现字母 数字 下划线和ASCII码,不能出现中文,可以使用encodeURIComponent()转码;
- 当我们利用我们的ajax放的发送一个请求到远处服务器时,我们需要等待远程服务器去响应我们的请求,等待远程服务器将响应的结果返回给我们,但是这个响应的速度是不确定的,因为响应的速度是由本地网络和远程服务器的网速等共同决定的,所以我们不可能一直等待服务器的响应。这里需要设置超时时间;
ajax({type: 'GET',url: 'http://localhost:3000/posts',timeout: 1000,success: data => {console.log('success', data);},error: err => {console.log('error', err);},
});
其他
至此,jQuery官方的ajax还是有一定的差异,所以还需要进一步完善
3. 传递多个参数,需要保持传递顺序。解决方案是可以改写成传递的是一个对象;因为对象里面的值,传递的是一个对象就不用考虑先后顺序,里面用的参数通过对象名.属性名的形式获取;
4. 传递请求类型的区分大小写,jQuery官方的是大小写都可以;解决方案是可以使用toLowerCase()或者toUpperCase()将类型转成大写或小写再对比;
5. 我们传递的数据用的名字是obj,jQuery官方用的是data;
const ajax = option => {//type, url, data, timeout, success, error将所有参数换成一个对象{}// 0.将对象转换成字符串//处理objconst objToString = data => {data.t = new Date().getTime();let res = [];for (let key in data) {//需要将key和value转成非中文的形式,因为url不能有中文。使用encodeURIComponent();res.push(encodeURIComponent(key) + ' = ' + encodeURIComponent(data[key]));}return res.join('&');};let str = objToString(option.data || {});// 1.创建一个异步对象xmlHttp;var xmlHttp, timer;if (window.XMLHttpRequest) {xmlHttp = new XMLHttpRequest();} else if (xmlHttp) {// code for IE6, IE5xmlHttp = new ActiveXObject('Microsoft.xmlHttp');}// 2.设置请求方式和请求地址;// 判断请求的类型是POST还是GETif (option.type.toLowerCase() === 'get') {xmlHttp.open(option.type, option.url + '?t=' + str, true);// 3.发送请求;xmlHttp.send();} else {xmlHttp.open(option.type, option.url, true);// 注意:在post请求中,必须在open和send之间添加HTTP请求头:setRequestHeader(header,value);xmlHttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');// 3.发送请求;xmlHttp.send(str);}// 4.监听状态的变化;xmlHttp.onreadystatechange = function () {clearInterval(timer);debugger;if (xmlHttp.readyState === 4) {if ((xmlHttp.status >= 200 && xmlHttp.status < 300) || xmlHttp.status == 304) {// 5.处理返回的结果;option.success(xmlHttp.responseText); //成功后回调;} else {option.error(xmlHttp.responseText); //失败后回调;}}};//判断外界是否传入了超时时间if (option.timeout) {timer = setInterval(function () {xmlHttp.abort(); //中断请求clearInterval(timer);}, option.timeout);}
};
实现Upload的request封装
针对fusion中 Upload 组件,实现自定义请求的封装。
function customRequest(option) {/* coding here */return {abort() {/* coding here */}};
}<Upload request={customRequest}/>
其中,customRequest的实现如下:
/*** clone from https://github.com/react-component/upload/blob/master/src/request.js*/function getError(option, xhr, msg) {msg = msg || `cannot post ${option.action} ${xhr.status}'`;const err = new Error(msg);err.status = xhr.status;err.method = option.method;err.url = option.action;return err;
}function getBody(xhr) {const text = xhr.responseText || xhr.response;if (!text) {return text;}try {return JSON.parse(text);} catch (e) {return text;}
}// option {
// onProgress: (event: { percent: number }): void,
// onError: (event: Error, body?: Object): void,
// onSuccess: (body: Object): void,
// data: Object,
// filename: String,
// file: File,
// withCredentials: Boolean,
// action: String,
// headers: Object,
// method: String
// timeout: Number
// }
export default function upload(option) {const xhr = new XMLHttpRequest();if (option.onProgress && xhr.upload) {xhr.upload.onprogress = function progress(e) {if (e.total > 0) {e.percent = (e.loaded / e.total) * 100;}option.onProgress(e);};}const formData = new FormData();if (option.data) {Object.keys(option.data).forEach(key => {formData.append(key, option.data[key]);});}if (option.file instanceof Blob) {formData.append(option.filename, option.file, option.file.name);} else {formData.append(option.filename, option.file);}xhr.onerror = function error(e) {option.onError(e);};xhr.onload = function onload() {// allow success when 2xx status// see https://github.com/react-component/upload/issues/34if (xhr.status < 200 || xhr.status >= 300) {return option.onError(getError(option, xhr), getBody(xhr));}option.onSuccess(getBody(xhr), xhr);};option.method = option.method || 'POST';xhr.open(option.method, option.action, true);// In Internet Explorer, the timeout property may be set only after calling the open() method and before calling the send() method.// see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeoutconst { timeout } = option;if (typeof timeout === 'number' && timeout > 0) {xhr.timeout = timeout;xhr.ontimeout = () => {const msg = `Upload abort for exceeding time (timeout: ${timeout}ms)`;option.onError(getError(option, xhr, msg), getBody(xhr));};}// Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179if (option.withCredentials && 'withCredentials' in xhr) {xhr.withCredentials = true;}const headers = option.headers || {};// when set headers['X-Requested-With'] = null , can close default XHR header// see https://github.com/react-component/upload/issues/33if (headers['X-Requested-With'] !== null) {xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');}for (const h in headers) {if (headers.hasOwnProperty(h) && headers[h] !== null) {xhr.setRequestHeader(h, headers[h]);}}xhr.send(formData);return {abort() {xhr.abort();},};
}
我们可以发现,ajax只是一种异步请求的方式,并不特指某一种具体的实现方式,但随着使用这种方式实现网络请求时内部又包含请求的情况,就会出现回调地狱,这也是XHR的诟病之一,因此,后来才催生了更加优雅的请求方式。