Skip to content

XMLHttpRequest类型

他有2个父类。这几个父类,全都只能new,不能直接调用。

经过getOwnerPropertyDescripter计算,可以发现最顶层父类有这几个属性(基础事件通用绑定和监听相关):

经过仔细观察,会发现这3个类的各个主要属性和方法,均可以被重定义(configurable)或者修改(write)。这给我们把xhr请求对接到其他请求通道提供了可能性。

先open,open之后则能调用setRequestHeader设置请求头。 如果有新增额外的非标准头,那么会发出options预检请求,服务端需要返回access-control-allow-headers的头。

不同的请求content-type:

  • 默认或者text/plain之类的,就是服务端bodyparser直接设置成支持text解析就行,服务端拿到就是个字符串。
  • 如果是// aaa.setRequestHeader('content-type', 'application/x-www-form-urlencoded; chaset=utf-8')。那么服务器要设置支持form类型(他会支持2种from类型)即可,然后服务端能自动把&拼接的字符串解析成对象。
  • aaa.setRequestHeader('Content-Type', 'application/json')。如果是这种,则服务端需要明确返回access-control-allow-headers允许content-type。不然会报错。。然后服务端koa中间件设置支持json解析,就能拿到json。这个content-type非常重要,是因为他主要是用于去告诉服务端怎么解析。

关于responseType

他是让你去标识响应类型的,不会自动识别: https://wangdoc.com/javascript/bom/xmlhttprequest#xmlhttprequestresponsetype 你标记成json,他xhr.response才会解析成json。 关于responseText:只有你没有标记responseType,或者标记成text的时候,你才能读这个字段。否则你去读会报错。因此咱还是乖乖去读xhr.response这个吧或者别设置responseType。 于是我想到:axios的文档里说可以设置resnponseType为json,他就会自动解析json,但是你会发现他源码里面xhr请求部份,就是在用responseText。-----我们最好也这么干---若是json或text则不设置responseType。。

假设他文档里说的resnponseType真的设置了,这样的话,岂不是会导致他无法读取responseText。果然,我们发现他只有非json的时候(可能就是二进制等场景才设置):

可是我们使用axios最后又能拿到对象类型的结果。原因是他又个transformResponse函数对响应做了针对性处理:

XHR2

第一代不支持文件上传、不支持formdata、不支持进度回调、不支持超时时间。第二代可以了。

  • 超时属性和超时回调。

关于请求事件回调

  • onload和addEventListern是两套互不打扰的机制。可以认为onload的一个覆盖式的监听写法,存到了实例某个地方。 而addEventLister则是一个追加式的事件写法。互不影响。
  • onreadyStatechange也跟上面两种onload互不干扰。 1,xhr.readyState状态有01234. 但是0不会触发,当你调用open则触发1,然后send后则触发2,然后数据回来触发3和4. 即使数据一次性回来了,也会先触发3再触发4. 如果responseType设置了json那么当readyState3的时候xhr.response是null(可能因为还需要解析),而纯文本的话在readyState是3的时候就能读responseTExt和response了。
  • 不要用statusText判断完成(毕竟有的服务器没有返回text),一定要用xhr.readyState===4。axios就是用的4.

onnload的疑问

当喜欢fx hx访问fang wenfang wefang wfangfanfaf我new一个XMLHttpRequest之后。然后在xhr.onload = function(){}之后。 当我直接访问xhr.onload就能返回我那个函数,但是当我去xhr.__proto__或者xhr.__proto__上面去找,就无法找到我定一个这个回调函数。 答:原因是他确实是xhr.proto.__proto__上的一个getter/setter. 当你onload=xxx的时候,假设你赋值了一个函数,他那个setter就会把你回调函数设置到xhr这个实例的内部隐藏的监听器变量里面去。 然而你如果通过xhr.__proto.__proto.onload访问的时候,this已经变了,因此那个getter/setter无法返回相应的东西。 我通过Object.getOwnerProepertyDescripter(xhr.proto.proto,'onload')把这个onload属性的定义描述拿出来, 重新defineProperty放到我xhr的myonload属性上,再尝试xhr.myonload就可以了。这说明其实就是个getter/setter机制而已。很简单。

关于form-data表单类型提交文件

其实他也不一定是提交文件。总之是一种分隔符结构的http报文body格式。文件就必须依靠这种格式。普通kv也可用这种格式。 如果是传统网页表单,则form上要这么设置:将enctype设置为multipart/form-data。 如果是现代ajax,则: let fileObj = fileNode.files[0]; // 使用FormData用于伪造form表单提交的数据 let fd = new FormData(); // 添加文件 fd.append(fileNode.name, fileObj); fd.append('mykey', 'myvalue') 然后不要设置content-type(他会自动设置成multipart/form-data; boundary=----WebKitFormBoundaryUc34Ffw4cGXlzs5),send(fd)提交即可。

跨域问题

Access-Control-Expose-Headers:指定哪些http响应头可以被js获取。

ajax请求类型

koa-bodyparser就能解决除了multipart/formdata之外的几个类型。 好像multipart/formdata如果只有表单项,但没有文件的话,bodyparser好像也能支持。

一些xhr的规律

当服务器返回500等错误时,可以拿到error.request.responseURL(其中域名已替换)。但当浏览器网络断开无法联通时,无法拿到error.request.responseURL,只能拿到error.config.url(即请求原始域名的url),因此为了在封禁出错时能上报替换后域名,此处进行兜底替换,以保证上报的域名尽可能准确。通过_replaceblock=1参数来标识此类兜底上报。该标记仅代表axios response拦截器走到了error.config.url兜底逻辑,且调用了replaceBlock函数,从而确保上报的域名符合预期。

更新: 更新: 1、open之后才能调用setRequestHeader。 否则会报错。(注意一旦send完成,xhr就不是open状态了,此时无法调setRequestHeader,见第5条). 2、open之后才能调用send,否则会报错。 3、open可以多次调用。可以在任意时刻多次调用,甚至可以在send之后的readyState===3/2/1的时候多次调用。在这期间调用open时又会触发readyState=1, 会导致无法触发readyState4. 4、xhr send完全结束之后(readyState===4)之后也可以重新调用open,进行下一次请求。(注意必须是send完全结束之后,且state是4之后。也就是说必须onload事件里才能调用,或者onreadstatechange的readystate===4之后才能重新调用open) 5、send不能二次调用。 xhr.send之后(即使send后是http网络错误),xhr都会变成了“未open”。此时不能再次send,需要重新open才能send。 6、setRequestHeader,多次设置同一个头,会在已有的头后面append字符串。例如,两次设置结果:bbb, ccc 7、xhr一旦经历send并且结束后,其之前设置的setRequesetHeader全部会被清空。 此时再次调用open和setRequestHeader将重新设置xhr的header。 (其实本质上是因为xhr每次open调用后,之前setRequestHeader设置的头都会被清空)。 8、onreadystatechange等属性,跟addEventListener('readystatechange')等事件,是两套事件机制。当事件发生后,两套机制各自都会触发。 9、模拟事件onload/onloadend/onprogress的模拟原理。注意:你需要在请求调用 open() 之前添加事件监听。否则 progress 事件将不会被触发。https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest_API/Using_XMLHttpRequest#监测进度 但是如果 lengthComputable 属性的值是 false,那么意味着总字节数是未知并且 total 的值为零。 progress 事件同时存在于下载和上传的传输。下载相关事件在 XMLHttpRequest 对象上被触发,就像上面的例子一样。上传相关事件在 XMLHttpRequest.upload 对象上被触发,像下面这样:

10、使用 loadend 事件可以侦测到所有的三种加载结束条件(abort、load,或 error):需要注意的是,没有方法可以确切的知道 loadend 事件接收到的信息是来自何种条件引起的操作终止;但是你可以在所有传输结束的时候使用这个事件处理。

js主动派发事件:如何通过js主动触发一个事件

分3步,第一步创建事件对象,第二步初始化他,第三步触发他。

js
document.createEvent('MouseEvents') // 有几种类型: UIEvents, MouseEvetns, MustationEvents, HTMLEvents
event.initMouseEvent('click', true, true, xxxxx)
div.dispatchEvent(event)
  • 还可以创建自定义的事件
js
const event = document.createEvent('customEvent')
event.initCustomEvent('myEventName', true, false, '')
div.dispatchEvent(event)

zepto的tap事件的实现原理

zepto会在document上绑定事件监听'touchend',由于touchend肯定是没有延时的,故在touchend触发后,zepto会在此模拟一个'tap'事件。粗略代码如下:

js
    document.addEventListener('touchend', function (e) {
        var event = document.createEvent('MouseEvents')
        event.initMouseEvent('click', true, true, document.defaultView, 0,0,0,0,0,false,false,false,false,false,0,null) // 15个参数里只有前四个比较重要,后面都是给事件处理程序用的
        e.target.dispatchEvent(event)
    })

input事件和change事件的区别

input输入框的input事件会在输入内容变化就触发;而chagne需要在你焦点变化的时候才触发。

创建一个自定义事件的步骤

  1. 创建一个类型的事件对象 event
  2. 调用event自身的初始化方法来初始化这个事件对象
  3. 在特定的dom上dispatchEvent来触发事件

事件继承关系

ProgressEvent-->Event MouseEvent-->UIEvent-->Event CustomEvent --> Event

以MouseEvent为例,你实例化一个事件new MouseEvent('cuiload')。其第一个入参叫做type。 你会发现,这个type就像XMLHttpRequest似的,他有些属性例如xhr.readyState/xhr.onload都是从父类继承下来的getter。 对于继承来的getter/setter,他有个特性是:你通过实例赋值时,依然无法在实例自身创建一个属于实例自身的属性-----因为他赋值那一刻永远都是调用的基类的setter,而父类setter又什么都没做,因此最终还是什么都没做。 例如你要如何访问到MouseEvent创建出来的实例的type属性的源头呢,你可以这么干: Object.getOwnPropertyDescriptors(a.proto.proto.proto).type.get.call(a) 即直接取出源头的getter,然后绑定this到实例,然后call调用那个getter。

再说说xhr的回调时候的事件对象,经观察,响应后的onload、onreadystatechange这些,以及addEventListener这些。 在onload/onloadend/onloadstart里,他都是用了ProgressEvent的实例作为事件对象:

其这个类的内容也就是多了个total、loaded。 然后onreadystate事件,他的事件对象则直接用了Event类的实例:

这玩意就更没什么可说的了。就是判断下是不是这个type就行,其实也不用判断,因为回调过来就肯定是这个事件。 真正的状态数据是从xhr对象里去读。

事件对象的继承关系

任意对象都可能是一个事件对象eventtarget。只要这个对象他继承自EventTarget就算。(所有dom元素都继承自EventTarget) 其实所谓事件对象,就是说他具有触发事件和监听该事件的能力而已。在nodejs里面其实有时候这就叫实现了EventEmiter接口或者继承了EventEmiter的类。 我们说:xhr上可以绑定addEventListener监听onload、onerror等事件,那其实他XMLHttpRequest就是继承了EventTarget。 我们说xhr.upload对象也能监听文件上传进度,而他的继承关系是: XMLHttpRequestUpload-->XMLHttpRequestEventTarget-->EventTarget 底层这个类没什么属性,第二层这个类有:

dispatchEvent是同步还是异步

据说是同步。据说就算是浏览器触发的那种点击,他也是同步的。 注意:我理解这里应该跟我们主动dispatchEvent有个区别在于“事件的发生是不是同步的”。 我理解浏览器收到人的鼠标点击后,并不是立刻触发该eventTarget对象上面的dispatchEvent('click'),而是要把这个任务放到队列等待事件循环-----毕竟你点击界面的时候,人家可能还有别的js任务在执行。然后当时间循环拿到这个点击任务后才触发dispatchEvent,此时所有指向该dom的捕获阶段的所有click监听以及冒泡阶段的所有监听全部都会同步触发。 而我们自己代码中dispatchEvent,由于本来就已经处于当前js线程执行过程中,因此dispatchEvent那一刻就是会立刻执行所有回调。不信的话,你可以在dispatchEvent后面写一句日志,看看他是不是在所有回调执行完,才打印。 于是,我实测了一下,确实如此:

关于addEventListener

click触发的dom事件的事件对象是:PointerEvent类。 他继承了Event。所以他也有eventPhase属性,代表的阶段。0表示不属于任何阶段(事件流结束之后也是0)。 1表示此时是该事件流的捕获阶段所触发的回调事件。2表示此时正在触发目标上的回调函数。3表示冒泡。 其实我感觉eventPhase是2就能判定是否是当前触发该事件的真实目标元素了。 (另一种常见做法是用e.target === e.currentTarget)。这俩属性也是基类Event的。

其第三个参数可以是个options。有参数once用于控制只触发一次,usecapture控制是否在捕获阶段触发,passive表示我回调函数内永远不会preventDefault请浏览器按照默认行为行动即可。

关于xhr里面的事件

当我xhr.dispatchEvent的时候,他onload赋值绑定的事件和addEventListenr绑定的事件回调都可以被触发。

abort规律

1、send之前调用没有用。连abort事件都不会触发。 2、send后立刻调用,才会让浏览器的network面板内看到cancel的效果。而且此时xhr的readyState会来不及触发readyState2,然后就会直接readyState变成了4(但是4的时候其xhr.status是0,各种响应也都没有). 然后会看到收到了abort事件回调,然后会看到xhr.readyState变成了0且各个响应信息全部置空了。 2、send之后,readyState立刻会变成2,在2的时候再调用abort就会产生作用。例如当readyState是2的时候调用abort则不会再触发任何数据接收的readyState3事件,但是还是会触发4. 但4的时候,其status也是啥都没有。那必然也不会触发onload。 3、当readyState是4的时候,你依然可以调用abort,如果readyState是4立刻调用abort,则其他onload事件就无法触发了。且你在readyState4的时候立刻读取xhr.status,xhr,responseTExt,xhr.responseURL也都读不到,因为都置空了。唯一能读到的就是此时readyState是4. 然后如果你Promise.then微任务里面再读取xhr.readyState,那就又变成0了。说明他重置动作是异步进行的。 4、当onload事件触发后,再调用xhr.abort也可以。调用后所有东西会重置,你的status,responseheader就全看不到了。但是你给xhr设置的timeout倒是依然在。 5、abort之后的xhr,处于未open状态。需要重新open才能再次send。

append执行时机

document.head.append的脚本,即使是同步的,他也是等当前线程同步代码执行完毕后再执行。