因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。
我们可以通过以下几种常用方法解决跨域的问题
JSONP
的原理很简单,就是利用 <script>
标签没有跨域限制的漏洞。通过 <script>
标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时。
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script> <script> function jsonp(data) { console.log(data) } </script>
JSONP 使用简单且兼容性不错,但是只限于 get
请求。
在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP,以下是简单实现
function jsonp(url, jsonpCallback, success) { let script = document.createElement('script') script.src = url script.async = true script.type = 'text/javascript' window[jsonpCallback] = function(data) { success && success(data) } document.body.appendChild(script) } jsonp('http://xxx', 'callback', function(value) { console.log(value) })
CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest
来实现。
浏览器会自动进行 CORS
通信,实现 CORS
通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
服务端设置 Access-Control-Allow-Origin
就可以开启 CORS
。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
该方式只能用于二级域名相同的情况下,比如 a.test.com
和 b.test.com
适用于该方式。
只需要给页面添加 document.domain = 'test.com'
表示二级域名都相同就可以实现跨域
这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息
// 发送消息端 window.parent.postMessage('message', 'http://test.com') // 接收消息端 var mc = new MessageChannel() mc.addEventListener('message', event => { var origin = event.origin || event.originalEvent.origin if (origin === 'http://test.com') { console.log('验证通过') } })
事件触发有三个阶段
window
往事件触发处传播,遇到注册的捕获事件会触发window
传播,遇到注册的冒泡事件会触发事件触发一般来说会按照上面的顺序进行,但是也有特例,如果给一个目标节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行。
// 以下会先打印冒泡然后是捕获 node.addEventListener( 'click', event => { console.log('冒泡') }, false ) node.addEventListener( 'click', event => { console.log('捕获 ') }, true )
通常我们使用 addEventListener
注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture
参数来说,该参数默认值为 false
。useCapture
决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性
一般来说,我们只希望事件只触发在目标上,这时候可以使用 stopPropagation
来阻止事件的进一步传播。通常我们认为 stopPropagation
是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation
同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。
node.addEventListener( 'click', event => { event.stopImmediatePropagation() console.log('冒泡') }, false ) // 点击 node 只会执行上面的函数,该函数不会执行 node.addEventListener( 'click', event => { console.log('捕获 ') }, true )
如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上
<ul id="ul"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul> <script> let ul = document.querySelector('##ul') ul.addEventListener('click', event => { console.log(event.target) }) </script>
事件代理的方式相对于直接给目标注册事件来说,有以下优点
V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。
新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。
在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。
老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。
在讲算法前,先来说下什么情况下对象会出现在老生代空间中:
老生代中的空间很复杂,有如下几个空间
enum AllocationSpace { // TODO(v8:7464): Actually map this space's memory as read-only. RO_SPACE, // 不变的对象空间 NEW_SPACE, // 新生代用于 GC 复制算法的空间 OLD_SPACE, // 老生代常驻对象空间 CODE_SPACE, // 老生代代码对象空间 MAP_SPACE, // 老生代 map 对象 LO_SPACE, // 老生代大空间对象 NEW_LO_SPACE, // 新生代大空间对象 FIRST_SPACE = RO_SPACE, LAST_SPACE = NEW_LO_SPACE, FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE, LAST_GROWABLE_PAGED_SPACE = MAP_SPACE };
在老生代中,以下情况会先启动标记清除算法:
在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行。
清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象像一端移动,直到所有对象都移动完成然后清理掉不需要的内存。
Proxy
是 ES6 中新增的功能,可以用来自定义对象中的操作
let p = new Proxy(target, handler); // `target` 代表需要添加代理的对象 // `handler` 用来自定义对象中的操作
可以很方便的使用 Proxy 来实现一个数据绑定和监听
let onWatch = (obj, setBind, getLogger) => { let handler = { get(target, property, receiver) { getLogger(target, property) return Reflect.get(target, property, receiver); }, set(target, property, value, receiver) { setBind(value); return Reflect.set(target, property, value); } }; return new Proxy(obj, handler); }; let obj = { a: 1 } let value let p = onWatch(obj, (v) => { value = v }, (target, property) => { console.log(`Get '${property}' = ${target[property]}`); }) p.a = 2 // bind `value` to `2` p.a // -> Get 'a' = 2
一个函数如果加上 async
,那么该函数就会返回一个 Promise
async function test() { return "1"; } console.log(test()); // -> Promise {<resolved>: "1"}
可以把 async
看成将函数返回值使用 Promise.resolve()
包裹了下。
await
只能在 async
函数中使用
function sleep() { return new Promise(resolve => { setTimeout(() => { console.log('finish') resolve("sleep"); }, 2000); }); } async function test() { let value = await sleep(); console.log("object"); } test()
注
上面代码会先打印 finish
然后再打印 object
。因为 await
会等待 sleep
函数 resolve
,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。
async
和 await
相比直接使用 Promise
来说,优势在于处理 then
的调用链,能够更清晰准确的写出代码。缺点在于滥用 await
可能会导致性能问题,因为 await
会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。
下面来看一个使用 await
的代码。
var a = 0 var b = async () => { a = a + await 10 console.log('2', a) // -> '2' 10 a = (await 10) + a console.log('3', a) // -> '3' 20 } b() a++ console.log('1', a) // -> '1' 1
对于以上代码你可能会有疑惑,这里说明下原理