Skip to content
On this page

浏览器缓存有哪几种类型? 强缓存 浏览器中的缓存作用分为两种情况,一种是需要发送HTTP请求,一种是不需要发送。 首先是检查强缓存,这个阶段不需要发送HTTP请求。通过相应的字段来进行检查。 在HTTP/1.0和HTTP/1.1当中,这个字段是不一样的。在早期,也就是HTTP/1.0时期,使用的是Expires,而HTTP/1.1使用的是Cache-Control。让我们首先来看看Expires。 Expires Expires即过期时间,存在于服务端返回的响应头中,告诉浏览器在这个过期时间之前可以直接从缓存里面获取数据,无需再次请求。比如下面这样: Expires: Wed, 16 Feb 2022 08:41:00 GMT 表示资源在2022年2月16号8点41分过期,过期了就得向服务端发请求。 这个方式看上去没什么问题,合情合理,但其实潜藏了一个坑,那就是服务器的时间和浏览器的时间可能并不一致,那服务器返回的这个过期时间可能就是不准确的。因此这种方式很快在后来的HTTP1.1版本中被抛弃了。 Cache-Control 在HTTP1.1中,采用了一个非常关键的字段:Cache-Control。这个字段也是存在于 它和Expires本质的不同在于它并没有采用具体的过期时间点这个方式,而是采用过期时长来控制缓存,对应的字段是max-age。比如这个例子: Cache-Control:max-age=3600 代表这个响应返回后在 3600 秒,也就是一个小时之内可以直接使用缓存。 如果你觉得它只有max-age一个属性的话,那就大错特错了。 它其实可以组合非常多的指令,完成更多场景的缓存判断, 将一些关键的属性列举如下: public: 客户端和代理服务器都可以缓存。因为一个请求可能要经过不同的代理服务器最后才到达目标服务器,那么结果就是不仅仅浏览器可以缓存数据,中间的任何代理节点都可以进行缓存。 private: 这种情况就是只有浏览器能缓存了,中间的代理服务器不能缓存。 no-cache: 跳过当前的强缓存,发送HTTP请求,即直接进入协商缓存阶段。 no-store:非常粗暴,不进行任何形式的缓存。 s-maxage:这和max-age长得比较像,但是区别在于s-maxage是针对代理服务器的缓存时间。 值得注意的是,当Expires和Cache-Control同时存在的时候,Cache-Control会优先考虑。 当然,还存在一种情况,当资源缓存时间超时了,也就是强缓存失效了,接下来怎么办?没错,这样就进入到第二级屏障——协商缓存了。 协商缓存 强缓存失效之后,浏览器在请求头中携带相应的缓存tag来向服务器发请求,由服务器根据这个tag,来决定是否使用缓存,这就是协商缓存。 具体来说,这样的缓存tag分为两种: Last-Modified 和 ETag。这两者各有优劣,并不存在谁对谁有绝对的优势,跟上面强缓存的两个 tag 不一样。 Last-Modified 即最后修改时间。在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。 浏览器接收到后,如果再次请求,会在请求头中携带If-Modified-Since字段,这个字段的值也就是服务器传来的最后修改时间。 服务器拿到请求头中的If-Modified-Since的字段后,其实会和这个服务器中该资源的最后修改时间对比: 如果请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。 否则返回304,告诉浏览器直接用缓存。 ETag ETag 是服务器根据当前文件的内容,给文件生成的唯一标识,只要里面的内容有改动,这个值就会变。服务器通过响应头把这个值给浏览器。 浏览器接收到ETag的值,会在下次请求时,将这个值作为If-None-Match这个字段的内容,并放到请求头中,然后发给服务器。 服务器接收到If-None-Match后,会跟服务器上该资源的ETag进行比对: 如果两者不一样,说明要更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。 否则返回304,告诉浏览器直接用缓存。 两者对比 在精准度上,ETag优于Last-Modified。优于 ETag 是按照内容给资源上标识,因此能准确感知资源的变化。而 Last-Modified 就不一样了,它在一些特殊的情况并不能准确感知资源变化,主要有两种情况: 编辑了资源文件,但是文件内容并没有更改,这样也会造成缓存失效。 Last-Modified 能够感知的单位时间是秒,如果文件在 1 秒内改变了多次,那么这时候的 Last-Modified 并没有体现出修改了。 在性能上,Last-Modified优于ETag,也很简单理解,Last-Modified仅仅只是记录一个时间点,而 Etag需要根据文件的具体内容生成哈希值。 另外,如果两种方式都支持的话,服务器会优先考虑ETag。 chrome 80+ 对 cache 的改动点了解吗? 更新samesite cookie Chrome 86 版本更新- 缓存分区功能 以上两个都是找到的80+之后的cache相关的更新内容,根据题意我认为应该是想问86版本的更新缓存分区问题。 cache 如何做到在不同域的情况下,还可以共享cookie,且不考虑 samesite 和 domain 设置 这个问题应该回答如何跨域访问cookie吧。

如果本地资源里面有一个 10 mb+ 的图片,它又是整个网页的背景图片,你如何优化它,让它不影响网页的渲染速度? 使用渐进式jpeg,提高用户体验 图片放cdn 图片压缩 react 17 和 18 分别做了什么优化 参考 react 17 的合成事件的具体改进点在什么地方 更改事件委托 首先第一个修改点就是更改了事件委托绑定节点,在 16 版本中,React 都会把事件绑定到页面的 document 元素上,这在多个 React 版本共存的情况下就会虽然某个节点上的函数调用了e.stopPropagation(),但还是会导致另外一个 React 版本上绑定的事件没有被阻止触发,所以在 17 版本中会把事件绑定到 render 函数的节点上。 去除事件池 17 版本中移除了event pooling,这是因为 React 在旧浏览器中重用了不同事件的事件对象,以提高性能,并将所有事件字段在它们之前设置为 null。在 React 16 及更早版本中,使用者必须调用 e.persist() 才能正确的使用该事件,或者正确读取需要的属性。 对标浏览器 onScroll 事件不再冒泡,以防止出现常见的混淆。 React 的 onFocus 和 onBlur 事件已在底层切换为原生的 focusin 和 focusout 事件。它们更接近 React 现有行为,有时还会提供额外的信息。 捕获事件(例如,onClickCapture)现在使用的是实际浏览器中的捕获监听器。 参考 react 什么情况下 state 是同步的什么情况下是异步的 假如所有setState是同步的,意味着每执行一次setState时(有可能一个同步代码中,多次setState),都重新vnode diff + dom修改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代码中的多个setState合并成一次组件更新。所以默认是异步的,但是在一些情况下是同步的。 setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同。在源码中,通过 isBatchingUpdates 来判断setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。 异步:在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。 同步:在 React 无法控制的地方,比如原生事件,具体就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新。 一般认为,做异步设计是为了性能优化、减少渲染次数: setState设计为异步,可以显著的提升性能。如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新; 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步。state和props不能保持一致性,会在开发中产生很多的问题; react 的 state 异步的情况下是个宏任务吗? 参考 看见你使用了 MVC 的形式开发组件,依赖注入的原理说下 参考 如果我想让某个类的属性继承其他的类,那么这步的依赖注入应该如何做? 看你了解 js v8,那么隐藏类主要是解决什么问题 减少 JavaScript 中访问属性所花的时间 属性访问不再需要动态字典查找了 为 V8 使用经典的基于类的优化和内联缓存技术创造了条件 参考 知道隐藏类的作用后,我们在编码的时候应该做哪些操作可以提高编码执行效率,至少说两点 在构造函数里初始化所有对象的成员(所以这些实例之后不会改变其隐藏类) function Point(x, y) { this.x = x; this.y = y; }

var p1 = new Point(11, 22); var p2 = new Point(33, 44); // At this point, p1 and p2 have a shared hidden class // 这里的p1和p2拥有共享的隐藏类 p2.z = 55; // warning! p1 and p2 now have different hidden classes! // 注意!这时p1和p2的隐藏类已经不同了!避免隐藏类的派生,就会获得越高的性能 总是以相同的次序初始化对象成员 永远不要delete对象的某个属性 function Point(x, y) { this.x = x; this.y = y; }

for (var i=0; i<1000000; i++) { var p1 = new Point(11, 22); delete p1.x; p1.y++; } 以上例子由于调用了delete,将导致隐藏类产生变化,导致p1.y不能用内联缓存直接获取。 以上程序在使用了delete之后耗时0.339s,在注释掉delete后只需0.05s。 babel 我看你也挺了解的,请说明如何优化直接 import polyfill 整个包的情况。 使用babel-preset-env插件和useBuiltIns属性,@babel/preset-env这个preset,现在提供了useBuiltIns: "entry"和useBuiltIns: "usage"两种polyfill的方式,这两种方式可根据开发者配置的browserslist的目标运行环境,自动引入core-js最小化的polyfill modules组合 使用polyfill.io。使用cdn的方式将所需要的特性以脚本的方式引入项目中,可以根据用户浏览器的ua和连接的参数来动态判断是否需要polyfill。 参考

babel runtime 和 helper 有何区别? babel的runtime,包含两个部分: helpers(定义了一些处理新的语法关键字的帮助函数) regenerator(仅仅是引用regenerator-runtime这个npm包)。 依赖插件:@babel/plugin-transform-runtime。 这个插件主要有两个方面的用途: babel在转码过程中,会加入很多babel自己的helper函数,这些helper函数,在每个文件里可能都会重复存在,transform-runtime插件可以把这些重复的helper函数,转换成公共的、单独的依赖引入,从而节省转码后的文件大小; 开发者在代码中如果使用了新的ES特性,比如Promise、generator函数等,往往需要通过core-js和regenerator-runtime给全局环境注入polyfill。 这种做法,在应用型的开发中,是非常标准的做法。 但是如果在开发一个独立的工具库项目,不确定它将会被其它人用到什么运行环境里面,那么前面那种扩展全局环境的polyfill就不是一个很好的方式。 transform-runtime可以帮助这种项目创建一个沙盒环境,即使在代码里用到了新的ES特性,它能将这些特性对应的全局变量,转换为对core-js和regenerator-runtime非全局变量版本的引用。这其实也应该看作是一种给代码提供polyfill的方式。 官方文档参考 github地址 它们和 preset-env 有何关系? 如果同时开启preset-env和transform-runtime的polyfill功能,会出现: preset-env的polyfill与transform-runtime的polyfill并存的现象。 这样的转码结果肯定是有问题的,这两个属于不同的polyfill做法,有不同的应用场景。 所以这两种polyfill不能同时启用。 在开发应用时,应该通过下面的方式关闭掉transform-runtime对core-js和regenerator的polyfill: [ "@babel/plugin-transform-runtime", { corejs: false, regenerator: false } ] 还有一点,transform-runtime的polyfill,对目标环境是不做判断的,只要它识别到代码里有用到新的ES特性,就会进行替换。 参考 为什么最新的 babel 草案(Array.prototype.at)没在 preset-env 的支持范围 preset-env 的 stage 和上一个问题的关系 babel 的原理 Babel是一个工具链,主要用于旧浏览器或者缓解中将ECMAScript 2015+代码转换为向后兼容版本的 JavaScript;包括:语法转换、源代码转换、Polyfill实现目标缓解缺少的功能等

图片: https://uploader.shimo.im/f/TAElAykkkn9GT9eY.png!thumbnail?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2NzU5OTI5NzIsImZpbGVHVUlEIjoiUktBV1ZRMHIwWHNZbEJrOCIsImlhdCI6MTY3NTk5MjY3MiwiaXNzIjoidXBsb2FkZXJfYWNjZXNzX3Jlc291cmNlIiwidXNlcklkIjoyMjI2ODE0OX0.iQLkjPXLInYorPrJI_RS5-DDKb_34Y47PpV_KWji2Z0

babel-plugin-import 主要做了什么操作? babel-plugin-import 和普遍的 babel 插件一样,会遍历代码的 ast,然后在 ast 上做了一些事情: 收集依赖:找到 importDeclaration,分析出包 和包的依赖 判断是否使用:判断 收集到的包的依赖是否在代码中被使用,如果有使用的,就调用 importMethod 生成新的 import 语句 生成引入代码:根据配置项生成代码和样式的 import 语句 具体参考 import 插件对于当前的 bundle 环境,你认为还有什么价值?

Http 2.x 和 Http 1.x 有啥区别?Http 2.x 有哪些特性? 参考解析 Hpack 做了什么? HTTP2支持一种新的专门进行头部压缩的算法,叫做HPACK。HPACK的开发设计考虑到了被攻击的危险,因此可以安全使用。 HPACK能够防御攻击者攻击,因为它没有像DEFLATE一样使用后向字符串匹配和动态Huffman,它使用了下面这三种方法进行压缩: 静态字典表:对于总是出现请求头的键值(比如::method:GET),会使用静态字典进行压缩。 动态字典表:可能是重复的头部,会存在于动态表里。 Huffman编码:静态的Huffman编码可以对任何字符串进行编码:名称或者是值。这种编码特定地用在HTTP的request/response头中,ASCII码和小写字母的编码会更短。编码最短可能只有5bits长(一个字节8bits),因此最大的原长和压缩长比率为8:5(或者是37.5%的压缩率) 如果 HPACK 需要为键:值格式的头部编码,首先它会去查看静态和动态的字典。如果键:值是完整存在的,就简单把从字典里引用该项。这通常会消耗一个字节或者最多两个字节。当 HPACK 在字典里无法匹配到一个完整的头部,它会尝试去寻找一个有相同键的头部。大多数常用的头部都会列在静态表里,例如:content-encoding, cookie, etag。剩下的可能是重复的头部,会存在于动态表里。如果这个键被找到,在大多数情况下可以被压缩到一个或者两个字节,否则这个键会被原编码或者使用 Huffman 算法进行编码:最少需要两个字节。 具体参考 PSK 有啥缺点? 与ASK类型的调制相比,此PSK的带宽效率要低 这是一个非相干参考信号 通过估计信号的相位状态,可以对二进制信息进行解码。诸如恢复和检测之类的算法非常困难。 QPSK,16-QAM等高级PSK调制对相位差更为敏感。 由于故障可能与时间结合,因此会产生错误的解调,因为解调的参考信号不固定。 这好像是和无限的通信相关的,具体参考 为什么选择 qiankun? 在利用Single SPA或其它微应用框架构建微前端系统中遇到的一些问题,如样式隔离、JS沙箱、资源预加载、JS副作用处理等等这些你需要的能力全部内置到了 qiankun 里面 到目前为止,已经大概有 200+ 的应用,使用 qiankun 来接入自己的微前端体系。qiankun 在蚂蚁内外受过了大量线上系统的考验,所以它是一个值得信赖的生产可用的解决方案。 什么场景需要使用微应用? 响应需求变慢,需求开发时间变长。 交付的效率变差,bug数越来越多。 业务复杂度变高,应用达到3个或3个以上,或者模块达到5个或以上。 团队人数变多,开发至少有5人以上,运维至少2人。 项目需要长期迭代维护,至少一年以上 qiankun 和其他方案相比到底做了什么,有什么优势? qiankun 总体上属于轻量级微前端框架,它的接口设计简约,功能丰富 ······ qiankun 和 single-spa 的区别? 相比于single-spa,qiankun他解决了JS沙盒环境,不需要我们自己去进行处理。在single-spa的开发过程中,我们需要自己手动的去写调用子应用JS的方法(如上面的 createScript方法),而qiankun不需要,乾坤只需要你传入响应的apps的配置即可,会帮助我们去加载。 qiankun基于single-spa,在single-spa上做了改造,使得接入更加方便。 说一下 react 的 fiber。 react15在render阶段的reconcile是不可打断的,这会在进行大量节点的reconcile时可能产生卡顿,因为浏览器所有的时间都交给了js执行,并且js的执行时单线程。为此react16之后就有了scheduler进行时间片的调度,给每个task(工作单元)一定的时间,如果在这个时间内没执行完,也要交出执行权给浏览器进行绘制和重排,所以异步可中断的更新需要一定的数据结构在内存中来保存工作单元的信息,这个数据结构就是Fiber。 那么有了Fiber这种数据结构后,能完成哪些事情呢? 工作单元 任务分解 :Fiber最重要的功能就是作为工作单元,保存原生节点或者组件节点对应信息(包括优先级),这些节点通过指针的形似形成Fiber树 增量渲染:通过jsx对象和current Fiber的对比,生成最小的差异补丁,应用到真实节点上 根据优先级暂停、继续、排列优先级:Fiber节点上保存了优先级,能通过不同节点优先级的对比,达到任务的暂停、继续、排列优先级等能力,也为上层实现批量更新、Suspense提供了基础 保存状态:因为Fiber能保存状态和更新的信息,所以就能实现函数组件的状态更新,也就是hooks 说一下 fiber 的属性 Fiber对象上面保存了包括这个节点的属性、类型、dom等,Fiber通过child、sibling、return(指向父节点)来形成Fiber树,还保存了更新状态时用于计算state的updateQueue,updateQueue是一种链表结构,上面可能存在多个未计算的update,update也是一种数据结构,上面包含了更新的数据、优先级等,除了这些之外,上面还有和副作用有关的信息。 //ReactFiber.old.js function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { //作为静态的数据结构 保存节点的信息 this.tag = tag;//对应组件的类型 this.key = key;//key属性 this.elementType = null;//元素类型 this.type = null;//func或者class this.stateNode = null;//真实dom节点

//作为fiber数架构 连接成fiber树 this.return = null;//指向父节点 this.child = null;//指向child this.sibling = null;//指向兄弟节点 this.index = 0;

this.ref = null;

//用作为工作单元 来计算state this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null;

this.mode = mode;

//effect相关

this.effectTag = NoEffect; this.nextEffect = null; this.firstEffect = null; this.lastEffect = null;

//优先级相关的属性 this.lanes = NoLanes; this.childLanes = NoLanes;

//current和workInProgress的指针 this.alternate = null; } 为什么需要设计可中断的模式? 由于递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。 为什么最后选择了类似 fiber 这样的数据结构? 形成Fiber树,还保存了更新状态时用于计算state的updateQueue,updateQueue是一种链表结构,上面可能存在多个未计算的update,update也是一种数据结构,上面包含了更新的数据、优先级等,除了这些之外,上面还有和副作用有关的信息。 由于 fiber 是可中断的审核节点的更新,那么 fiber 检查完毕某个节点时,我又去修改了此节点,这里 react 会如何处理?

为什么一定要使用可中断更新的模式?其核心需求是什么?就算现在的结构是链表,但是用递归也可以实现的。 时间片调度实现了异步可中断的任务,根据设备性能的不同,时间片的长度也不一样,在每个时间片中,如果任务到了过期时间,就会主动让出线程给高优先级的任务 究竟为什么 v15 递归不行?如果我记录一个全局变量,那么虽然递归无法直接回溯到制定的状态,但是我们依旧可以通过计算得出上一次递归的中间结果。

为什么数据结构必须要是链表,数组呢? 链表中的元素在内存中并不是连续的,链表中的元素除了自身的值,还拥有一个指针属性,指向下一个元素。相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素 schedule 是如何设置对于组件更新、删除状态的优先级,这个调度是如何伴随 fiber 节点的 diff 而产生变化的? 参考 这个优先级节点在源码中还是比较难的,这样吧,你自己去设计一个动态优先级调度的 schedule 你会怎么做?(没答好,没经验) 参考 说一下 react 组件异步加载 参考解析 自己实现一个 react 异步加载组件说一下思路,我就是想看看你对组件理解有多深 参考解析 react 的 jsx 是如何通过 React.createElement 生成最后的数据结构的 参考解析 说一下你常用的 react hook,以及在项目中的优化使用。 参考解析 之前异步加载你提到了 require.ensure。说一下 require.ensure 的原理 https://www.jianshu.com/p/1e838309627f 使用 jsonp 的 require.ensure 只是一种模式,但是已经过时了,还有其他解决思路吗?

webpack loader 是如何进行排序的,这个规则在什么 webpack hook 中出现? 在 webpack 中每个资源都会有个 request,这个 request 类似于在 require('babel-loader?plugins[]=transform-es2015-arrow-functions!!script-loader!./a'); 中的 babel-loader?plugins[]=transform-es2015-arrow-functions!!script-loader!./a,只是它会把每个 module 都 require.resolve 一下成绝对路径。参考 loader-api/this.request。loader 的执行顺序完全是按照 request 的顺序来执行的,先从左到右 pitch,然后再从右到左 transform。所以 loader 的执行顺序关键就是看 request 是如何生成的 acron 解析代码至 ast 后,内部是如何解析模块关系的?

你写过 webpack plugin 吗? 参考 关于 tapable , 你从最基础的角度说它主要是用于做什么,怎么让 webpack 跑起来的? 参考 http2 做了啥,把知道的全部说一下 头部压缩 多路复用 二进制分帧 服务端推送 详情参考 好了,前面只是开胃菜,下面开始考 CSAPP。 说一下 js 代码到 CPU 单个时钟周期指令的执行的流程 参考解析 函数中调用函数,在计算机中是用了 rbp 和 rsp 去做栈帧顶和底的保存,那么如果函数中调用了新的函数,它是怎么保持调用关系的?

你执行一条机器指令时,你在函数中声明的变量和语句都会进入调用栈帧吗? 不会,执行的时候才会 数组在堆内存中的表现形式是怎么样的? JS 数组中内存地址不是连续的。(因为传统语言数组下标取值,就是因为是连续,且每小块大小都相等的内存空间,可以直接对应上内存的地址)而 JS 它因为数组可以存放不同类型的数据,而且也能进行数组下标取值。其实这个时候采用的是哈希映射的方式,获取到对应数组下标的值的 现在的 JS 引擎为了优化 JS 的性能,它会分配一个连续的内存空间给存储了相同数据类型的数组,以达到更好的遍历效果。所以,只要你数组里存的是相同类型的值,在内存中的地址还是连续的。 constarr= [1, 2, 3, 4, 5]; //JS引擎分配连续的内存空间 constarr2= [true, "233", {}, 2]; //JS 分配不同的内存空间 为了进一步优化功能的实现,Javascript中出现了ArrayBuffer,它可以创建连续的内存供编程人员使用。 ArrayBuffer是创建一块连续的内存,不能直接操作,通过视图对分配的内存进行读写操作。显而易见,如果通过ArrayBuffer创建的数组进行遍历操作,速度更快。 字节码解析的时候它是怎么去读取内存的? 这样,我再给你一段代码,你把这段代码的 v8 执行过程说一遍。 functiona() {​letx= [12,3]​console.log(x)​} 其主要核心流程分为编译和执行两步。首先需要将 JavaScript 代码转换为低级中间代码或者机器能够理解的机器代码,然后再执行转换后的代码并输出执行结果 在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC),并且压入到EC Stack中。 FEC中包含三部分内容: 第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO):AO中包含形参、arguments、函数定义和指向函数对象、定义的变量; 第二部分:作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找; 第三部分:this绑定的值 所以当执行到这部分的代码的时候,会在GO中创建一个函数执行上下文a,他的值指向一个内存地址,如果函数a被执行,内存地址中声明了一个变量x,值为数组的内存地址,当log(x)的时候,打印完成后将此函数执行上下文弹出栈。 V8执行JS代码过程 图片: https://uploader.shimo.im/f/4GcmTK88P8WWqz7o.png!thumbnail?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE2NzU5OTI5NzIsImZpbGVHVUlEIjoiUktBV1ZRMHIwWHNZbEJrOCIsImlhdCI6MTY3NTk5MjY3MiwiaXNzIjoidXBsb2FkZXJfYWNjZXNzX3Jlc291cmNlIiwidXNlcklkIjoyMjI2ODE0OX0.iQLkjPXLInYorPrJI_RS5-DDKb_34Y47PpV_KWji2Z0

Blink将源码交给V8引擎,Stream获取到源码并且进行编码转换; Scanner会进行词法分析(lexical analysis),词法分析会将代码转换成tokens; 接下来tokens会被转换成AST树,经过Parser和PreParser: p Parser就是直接将tokens转成AST树架构; PreParser称之为预解析,为什么需要预解析呢? 这是因为并不是所有的JavaScript代码,在一开始时就会被执行。那么对所有的JavaScript代码进行解析,必然会 影响网页的运行效率; 所以V8引擎就实现了Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂 时需要的内容,而对函数的全量解析是在函数被调用时才会进行; 比如我们在一个函数outer内部定义了另外一个函数inner,那么inner函数就会进行预解析;n 生成AST树后,会被Ignition转成字节码(bytecode),之后的过程就是代码的执行过程(后续会详细分析)。 这里在解析成 AST 前引擎会先做什么事? 通过词法分析scanner扫描整个代码,生成tokens流 这里的那些元素是不会进入计算机调用栈帧的?

console.log 这个在内存中(字节码态)是如何读取的? 代码题 看代码写输出,中间有 promise 的问题时,写一下 promise 源码伪代码,以说明前一个 promise 何时被 resolve,才会触发 then(这里我提到了,如果返回值是一个新的promise 以及 thenable 的处理方式)

数组去重,每个数组元素内部内部可能是对象(我一开始用了排序+序列化的手段消除了不一样的对象元素,他看完后,禁止我使用这种方式,由于面试时间也长了,它让我用其他方式说一下思路。我说了递归,并且用 Map 做一个递归节点的记录,以防出现死循环,之后它又问,如果不允许你 Map 呢。。。。这个留给读者自己思考吧) 参考

Released under the MIT License.