2023.03.14 - 2023.03.24 更新收集面试问题(45道题)获取更多面试问题可以访问github 地址:
https://github.com/pro-collection/interview-question/issuesgitee 地址:
https://gitee.com/yanleweb/interview-question/issues
目录:初级开发者相关问题【共计 1 道题】111.null 和 undefined 的区别,如何让一个属性变为 null?【JavaScript】中级开发者相关问题【共计 20 道题】73.express middleware(中间件) 工作原理是什么??【Nodejs】104.说一说 cookie sessionStorage localStorage 区别?【JavaScript】105.promise.race、promise.all、promise.allSettled 有哪些区别?【JavaScript】106.手写代码实现 promise.race【JavaScript】109.JavaScript 有几种方法判断变量的类型?【JavaScript】110.样式优先级的规则是什么?【CSS】115.Proxy 和 Object.defineProperty 的区别是啥?【JavaScript】117.css 中 三栏布局的实现方案 的实现方案有哪些?【CSS】119.vue 的 keep-alive 的原理是啥?【web框架】125.当使用 new 关键字创建对象时, 会经历哪些步骤?【JavaScript】126.es5 和 es6 使用 new 关键字实例化对象的流程是一样的吗?【JavaScript】127.如何实现可过期的 localstorage 数据?【JavaScript】132.React setState 是同步还是异步的?【web框架】133.react 18 版本中 setState 是同步还是异步的?【web框架】134.【React】合成事件了解多少【web框架】135.【React】绑定事件的原理是什么?【web框架】139.pnpm 和 npm 的区别?【工程化】142.事件循环原理?【JavaScript】143.[vue] 双向数据绑定原理?【web框架】146.nodejs 进程间如何通信?【Nodejs】高级开发者相关问题【共计 22 道题】77.虚拟 dom 原理是啥,手写一个简单的虚拟 dom 实现?【JavaScript】107.手写代码实现 promise.all【JavaScript】108.手写实现 Promise.allSettled【JavaScript】112.CSS 尺寸单位有哪些?【CSS】113.React Router 中 HashRouter 和 BrowserRouter 的区别和原理?【web框架】114.Vue3.0 实现数据双向绑定的方法是什么?【web框架】118.浏览器垃圾回收机制?【浏览器】120.常见的 web 前端网路攻击有哪些?【网络】121.如何防止 跨站脚本攻击(Cross-Site Scripting, XSS)?【网络】122.跨站请求伪造(Cross-Site Request Forgery, CSRF)具体实现步骤是啥, 如何防止?【网络】123.script 标签 defer 和 async 区别?【浏览器】124.Vue 中 $nextTick 作用与原理是啥?【web框架】128.axios的拦截器原理及应用、简单手写核心逻辑?【web框架】129.有什么方法可以保持前后端实时通信?【网络】130.react 遍历渲染节点列表, 为什么要加 key ?【web框架】131.react lazy import 实现懒加载的原理是什么?【web框架】136.如何分析页面加载慢?【工程化】137.【性能】以用户为中心的前端性能指标有哪些?【工程化】138.浏览器渲染进程了解多少?【浏览器】140.pnpm 了解多少?【工程化】141.如何组织 monorepo 工程?【工程化】144.[vue] 是怎么解析template的?【web框架】资深开发者相关问题【共计 2 道题】116.React Diff算法是怎么实现的?【JavaScript】145.实现 JS 沙盒的方式有哪些?【工程化】
null和undefined都是JavaScript中表示缺失或无值的特殊值。
undefined是一个变量没有被赋值时的默认值,或者在访问对象属性或数组元素不存在时返回的值。例如:
而null表示一个变量被明确地赋值为没有值。例如:
要将一个属性的值设置为null,可以像这样:
如果要删除对象的属性并将其值设置为null,可以使用delete操作符:
请注意,尝试访问一个已删除的属性将返回undefined而不是null。
Express middleware 的工作原理是通过拦截 HTTP 请求,对请求进行处理,然后将请求传递给下一个中间件或应用程序的路由处理。在 Express 中,中间件可以是一个或多个函数,每个函数都可以对请求进行操作或响应,从而实现对请求的处理和过滤。
当 Express 应用程序接收到一个 HTTP 请求时,请求将首先被传递给第一个注册的中间件函数。这个中间件函数可以对请求进行操作,例如修改请求的头信息、检查请求是否包含有效的身份验证令牌等等。当这个中间件函数完成操作后,它可以选择将请求传递给下一个中间件函数,或者直接将响应返回给客户端。
如果中间件函数选择将请求传递给下一个中间件函数,它可以调用 next() 函数来将控制权传递给下一个中间件。这个过程可以一直持续到所有中间件函数都被执行完毕,最后将请求传递给应用程序的路由处理函数。
通过使用中间件,开发人员可以将应用程序的功能模块化,从而实现更好的代码组织和可维护性。同时,中间件还可以实现各种功能,例如身份验证、日志记录、错误处理等等,从而为应用程序提供更丰富的功能和更好的用户体验。
Express middleware 的设计模式是基于责任链模式。在责任链模式中,每个对象都有机会处理请求,并将其传递给下一个对象,直到请求被完全处理为止。在 Express 中,每个中间件函数都有机会对请求进行处理,并可以选择将请求传递给下一个中间件函数或应用程序的路由处理函数。
以下是一个简单的示例,演示如何使用 Express middleware 实现身份验证:
在上面的示例中,我们定义了一个名为 authenticate 的中间件函数,它用于验证用户的身份。在这个函数中,我们检查请求头中是否包含有效的身份验证令牌。如果令牌有效,则将控制权传递给下一个中间件函数或路由处理函数。否则,返回 401 错误。
然后,我们通过调用 app.use() 方法来注册这个中间件函数,以便在每个请求中都进行身份验证。最后,我们定义一个名为 /protected 的路由处理函数,它表示受保护的资源。只有在身份验证通过后,才能访问这个路由处理函数。
通过这个简单的示例,我们可以看到如何使用 Express middleware 实现基本的身份验证功能。中间件函数充当责任链中的一个环节,通过对请求进行处理和过滤,为应用程序提供更好的安全性和用户体验。
cookie、sessionStorage和localStorage都是存储在浏览器端的客户端存储方式,用于存储一些客户端数据。
它们之间的区别如下:生命周期
cookie的生命周期由Expires和Max-Age两个属性控制。当设置了Expires属性时,cookie的生命周期为设置的过期时间;当设置了Max-Age属性时,cookie的生命周期为设置的秒数。cookie在浏览器关闭时也会过期。而sessionStorage和localStorage的生命周期则与浏览器窗口相关,当窗口被关闭时,sessionStorage数据也会被清空,而localStorage数据则会一直存在,直到用户手动删除。存储容量
cookie的存储容量限制为4KB,而sessionStorage和localStorage的存储容量则较大,可以达到5MB或更高。数据共享
cookie可以被所有同源窗口(指协议、域名、端口相同)访问,而sessionStorage和localStorage只能被创建它们的窗口访问。传输方式
cookie会随着http请求发送到服务器,而sessionStorage和localStorage不会发送到服务器,只存在于浏览器端。数据类型
cookie只能存储字符串类型的数据,而sessionStorage和localStorage可以存储除了对象以外的数据类型,如数字、布尔值、数组、甚至是其他复杂的数据结构。但是,它们都可以通过JSON.stringify和JSON.parse方法将数据转化为字符串进行存储和读取。
综上所述,这三种存储方式都有各自的优缺点和适用场景。在实际应用中,我们需要根据实际情况选择合适的存储方式。
Promise.race()、Promise.all()、Promise.allSettled() 都是 JavaScript 中的 Promise 相关 API,它们的区别如下:Promise.race()
Promise.race() 接收一个包含多个 Promise 的数组作为参数,返回一个新的 Promise。该 Promise 将会在数组中的任意一个 Promise 状态变为 fulfilled 或 rejected 时被解决,且以第一个解决的 Promise 的结果作为其结果返回。
如果数组中所有 Promise 都被拒绝,则返回的 Promise 将会以最先被拒绝的 Promise 的原因作为其原因拒绝。Promise.all()
Promise.all() 接收一个包含多个 Promise 的数组作为参数,返回一个新的 Promise。该 Promise 将会在数组中所有 Promise 状态均为 fulfilled 时被解决,并且以数组形式返回所有 Promise 的结果。
如果数组中有任何一个 Promise 被拒绝,则返回的 Promise 将会以最先被拒绝的 Promise 的原因作为其原因拒绝。Promise.allSettled()
Promise.allSettled() 接收一个包含多个 Promise 的数组作为参数,返回一个新的 Promise。该 Promise 将会在数组中所有 Promise 状态都被解决时被解决,并且以数组形式返回所有 Promise 的结果。和 Promise.all() 不同,Promise.allSettled() 不会在有 Promise 被拒绝时拒绝该 Promise。
返回的 Promise 的数组中的每个元素都是一个对象,该对象表示原始 Promise 的结果。每个对象都有一个 status 属性,表示原始 Promise 的状态,其值为字符串 'fulfilled' 或 'rejected'。如果 Promise 被解决,对象还会包含一个 value 属性,表示 Promise 的解决值。如果 Promise 被拒绝,对象还会包含一个 reason 属性,表示 Promise 的拒绝原因。
综上所述,Promise.race()、Promise.all() 和 Promise.allSettled() 的主要区别在于它们对多个 Promise 的状态处理方式不同,以及返回的 Promise 所包含的数据类型和结构不同。
下面是手写实现 Promise.race() 方法的代码:
实现原理:
Promise.race() 方法接收一个包含多个 Promise 的数组作为参数,并返回一个新的 Promise。该 Promise 将会在数组中的任意一个 Promise 状态变为 fulfilled 或 rejected 时被解决,且以第一个解决的 Promise 的结果作为其结果返回。
我们可以通过创建一个新的 Promise,然后遍历 Promise 数组并将每个 Promise 包装在一个 Promise.resolve() 中,然后使用 .then() 方法将它们的解决值和拒绝原因分别传递给新的 Promise 的 resolve() 和 reject() 方法。由于 Promise 的状态只能改变一次,所以一旦第一个 Promise 被解决,新的 Promise 的状态也将被解决,并且以第一个解决的 Promise 的结果作为其结果返回。typeof 运算符:可以用于判断基本数据类型(如字符串、数字、布尔值、Undefined 等)和函数类型,但对于对象类型(如数组、日期、正则表达式等)不能准确判断。instanceof 运算符:可以用于判断一个对象是否为某个构造函数的实例,但不能判断基本数据类型。Object.prototype.toString() 方法:可以返回一个对象的具体类型字符串,可以判断所有数据类型,但需要注意的是需要使用 call 或 apply 方法将要判断的对象传递给 toString 方法。Array.isArray() 方法:可以判断一个对象是否为数组类型。constructor 属性:可以返回一个对象的构造函数,但需要注意的是 constructor 属性可以被修改,因此不能保证准确性。
Object.prototype.toString() 方法是用来返回当前对象的类型字符串,其实现方式是返回一个类似 "[object Type]" 的字符串,其中 Type 是当前对象的类型。
通过这种方式,可以精确判断变量的类型,包括基本数据类型和对象类型。
在 CSS 中,当多个选择器应用于同一个元素并设置了相同的属性时,就会出现样式冲突的问题。此时,CSS 会根据一定的规则来决定哪个样式具有更高的优先级,从而确定最终的样式效果。CSS 样式优先级的规则如下:!important:具有最高优先级,用于强制覆盖其它样式。内联样式:直接在 HTML 元素的 style 属性中定义的样式,其优先级高于后面提到的其它选择器。ID 选择器:通过 #id 定义的样式,其优先级高于后面提到的 class 选择器和标签选择器。类选择器、属性选择器、伪类选择器:通过 .class、[attribute. 或 :pseudo 定义的样式,其优先级高于后面提到的标签选择器。标签选择器、伪元素选择器:通过 tagname 或 ::pseudo 定义的样式,优先级最低。
需要注意的是,当出现多个选择器具有相同的优先级时,CSS 会按照样式表中出现的顺序来决定样式的优先级,越后出现的样式会覆盖前面出现的样式。此外,继承自父元素的样式的优先级比上述任何选择器都低。
Proxy 和 Object.defineProperty 是 JavaScript 中两个不同的特性,它们的作用也不完全相同。
Object.defineProperty 允许你在一个对象上定义一个新属性或者修改一个已有属性。通过这个方法你可以精确地定义属性的特征,比如它是否可写、可枚举、可配置等。该方法的使用场景通常是需要在一个对象上创建一个属性,然后控制这个属性的行为。
Proxy 也可以用来代理一个对象,但是相比于 Object.defineProperty,它提供了更加强大的功能。使用 Proxy 可以截获并重定义对象的基本操作,比如访问属性、赋值、函数调用等等。在这些操作被执行之前,可以通过拦截器函数对这些操作进行拦截和修改。因此,通过 Proxy,你可以完全重写一个对象的默认行为。该方法的使用场景通常是需要对一个对象的行为进行定制化,或者需要在对象上添加额外的功能。
总结来说,Object.defineProperty 是用来定义对象的属性,而 Proxy 则是用来代理对象并对其进行操作拦截和修改。两者的应用场景不同,但都可以用来对对象的行为进行定制化。
以下是 Proxy 和 Object.defineProperty 的一些具体应用场景的不同:Object.defineProperty 适用于需要精确地控制对象属性行为的场景,比如控制属性是否可写、可枚举、可配置等。它的应用场景包括但不限于:对象属性访问权限控制;对象属性计算;对象属性懒加载。Proxy 适用于需要代理对象并对其进行操作拦截和修改的场景。它的应用场景包括但不限于:对象属性访问控制;对象属性修改控制;对象属性缓存;对象属性计算;对象属性监听;对象属性校验;对象属性劫持等。
总的来说,Object.defineProperty 主要用于单个对象属性的控制和修改,而 Proxy 则适用于对整个对象或对象的多个属性进行控制和修改。由于 Proxy 的功能更加强大,它在一些高级应用场景中比 Object.defineProperty 更加适用。但是在一些简单场景下,使用 Object.defineProperty 可能更加方便和实用。
三栏布局指的是在一个页面中,将内容分为左、中、右三栏,并使它们具有相应的宽度和高度。实现三栏布局有多种方案,以下是其中的几种:使用浮动
在这个方案中,左栏和右栏使用浮动来实现,中间栏使用 margin 来占据剩余的宽度。使用绝对定位
在这个方案中,左栏和右栏使用绝对定位来实现,中间栏使用左右 padding 来占据剩余的宽度。使用Flexbox布局
在这个方案中,父容器使用Flexbox布局,左、中、右三栏都是Flex项,中间栏使用 flex: 1 来占据剩余的宽度。
这些方案都可以实现三栏布局,但每种方案都有自己的优缺点。在选择方案时,需要考虑浏览器兼容性、性能、可维护性和可扩展性等因素。
是 Vue.js 提供的一个抽象组件,它可以使被包含的组件保留在内存中,而不是每次重新渲染的时候销毁并重建,从而提高了应用的性能。
具体来说, 的实现原理如下:当一个组件被包裹在 组件内时,它会被缓存起来,而不是被销毁。如果这个组件被包裹的父组件从它的视图中移除,那么这个组件不会被销毁,而是被缓存起来。如果这个组件再次被包裹的父组件添加回视图中,那么它会被重新激活,而不是重新创建。
组件通过一个内部的缓存对象来缓存组件实例,这个缓存对象会在组件被包裹在 组件中时创建。当一个被缓存的组件需要被激活时, 组件会从缓存中取出该组件的实例并将其挂载到视图上,从而实现了组件的复用。
需要注意的是,被缓存的组件并不是一直存在于内存中,它们会在一定条件下被销毁,比如缓存的组件数量超过了一定的阈值,或者系统内存占用过高等。
在 JavaScript 中,new 关键字用于创建一个对象实例。当使用 new 关键字创建对象时,会发生以下几个步骤:创建一个空的对象。将这个空对象的 [[Prototype]] 属性设置为构造函数的 prototype 属性。将这个空对象赋值给构造函数内部的 this 关键字,用于初始化属性和方法。如果构造函数返回一个对象,那么返回这个对象;否则,返回第一步创建的对象实例。
以下是一个示例,演示如何使用 new 关键字创建一个对象实例:
在上面的示例中,new Person("John", 30) 会创建一个新的对象实例。在构造函数 Person 中,this.name 和 this.age 会被赋值为 "John" 和 30。最终,new 关键字会返回这个新的对象实例。
需要注意的是,在 JavaScript 中,函数也是对象。因此,我们可以向对象一样定义属性和方法。当我们使用 new 关键字调用一个函数时,这个函数会被视为构造函数,从而创建一个新的对象实例。
ES5 和 ES6 使用 new 关键字实例化对象的流程基本上是一样的,只是在细节上存在一些差异。
在 ES5 中,当使用 new 关键字调用一个函数时,会创建一个新的对象,并将这个新对象的 [[Prototype]] 属性指向构造函数的 prototype 属性。此外,new 关键字还会将构造函数内部的 this 关键字绑定到新创建的对象上,从而允许我们在构造函数内部添加属性和方法。
在 ES6 中,这些基本的流程也是相同的。但是,ES6 引入了类(class)的概念,从而为面向对象编程提供了更加便利的语法。使用类定义一个对象时,需要使用 constructor 方法作为构造函数,而不是普通的函数。类定义的语法糖实际上是对函数的封装,使用 new 关键字创建类的实例时,实际上也是在调用类的 constructor 方法。
在 ES6 中,可以使用类的继承来创建更复杂的对象。当使用 new 关键字创建一个继承自另一个类的类的实例时,会先调用父类的 constructor 方法,再调用子类的 constructor 方法,从而完成对象实例的创建过程。
需要注意的是,虽然 ES6 的类看起来像是其他面向对象语言中的类,但在 JavaScript 中,类仍然是基于原型继承的。在创建一个类的实例时,实际上是在创建一个新对象,并将这个新对象的原型指向类的原型。因此,实例化对象的流程与使用普通函数或类定义的对象的流程基本上是相同的。
要实现可过期的 localStorage 数据,可以结合使用 localStorage 和 Date 对象。
首先,在存储数据时,需要将数据和过期时间一起存储在 localStorage 中。可以使用 JSON 格式来将数据和过期时间打包存储。代码示例如下:
在上面的代码中,setWithExpiry 函数接受三个参数:key 表示存储数据的键名,value 表示要存储的数据,ttl 表示数据的过期时间(单位为毫秒)。在函数内部,我们首先获取当前时间 now,然后将过期时间 expiry 计算为当前时间加上过期时间。最后,我们将数据和过期时间打包成一个对象,并使用 JSON.stringify 方法将其转换为字符串,存储在 localStorage 中。
接下来,我们可以在获取数据时检查数据是否已过期。如果数据已过期,则需要将其从 localStorage 中删除并返回 null。代码示例如下:
在上面的代码中,getWithExpiry 函数接受一个参数 key,表示要获取的数据的键名。在函数内部,我们首先使用 localStorage.getItem 方法获取存储在 localStorage 中的数据,如果不存在则返回 null。然后,我们将存储的字符串数据解析为对象,并获取过期时间。如果当前时间已经超过了过期时间,则需要将数据从 localStorage 中删除并返回 null。否则,我们返回存储的数据。
通过结合使用 localStorage 和 Date 对象,我们可以实现可过期的 localStorage 数据。这种方法简单易用,适用于需要在客户端存储数据,并且需要在一定时间后将其自动删除的场景。
在 React 中,setState 方法有时是异步的,有时是同步的,具体取决于使用方式和环境。
当我们在 React 中调用 setState 方法时,React 会将新状态合并到组件的状态队列中,并在未来的某个时间更新组件的状态。这个更新过程是异步的,即不能保证在 setState 调用后立即更新状态,而是会将状态更新推迟到下一个渲染周期中。这种情况下,setState 方法会返回一个 Promise 对象,但该对象不包含任何有用的信息。
然而,当 setState 方法被直接调用,而不是在事件处理函数、异步代码或生命周期函数中被调用时,更新过程是同步的。在这种情况下,React 会立即更新组件的状态,并在更新后立即执行一次重新渲染。这种情况下,setState 方法不会返回任何信息。
需要注意的是,在 React 中异步更新状态的机制可以提高性能和优化页面渲染速度,因为它可以让 React 在适当的时候批量更新组件状态,从而避免过多的渲染和浏览器的性能问题。如果我们需要在更新状态后立即执行某些操作,可以使用 setState 方法的回调函数或生命周期函数 componentDidUpdate 来实现。例如:
这里的 setState 方法接受一个回调函数作为第二个参数,在状态更新完成后调用该函数并传递更新后的状态作为参数。在这个回调函数中可以执行任何需要在状态更新后立即执行的操作,例如输出调试信息、发送网络请求等。
在 React 18 中,setState 的行为仍然是异步的。React 团队并没有改变 setState 的默认行为,因为这会带来一些潜在的性能问题和不确定性。异步更新状态可以让 React 在适当的时候批量更新组件状态,从而避免过多的渲染和浏览器的性能问题。
但是,React 18 引入了一个新的特性:批量更新(Batching)。当我们在事件处理函数、异步代码或生命周期函数中调用 setState 时,React 会将多个状态更新合并到同一个批次中,从而减少渲染的次数,提高应用的性能。这意味着,即使我们在多个地方调用 setState 方法,React 也会将这些调用合并到同一个更新队列中,并在下一个渲染周期中一次性更新所有状态。
在 React 18 中,我们可以使用新的 startTransition API 来告诉 React,我们正在进行一次可中断的异步操作,并且希望在操作完成后批量更新组件状态。这个 API 的用法如下:
在这个例子中,我们通过 startTransition API 包装 setState 和其他异步操作,告诉 React 我们正在进行一次可中断的异步操作,并且希望在操作完成后批量更新组件状态。这样做可以让我们的应用更加流畅和响应,并且可以提高用户体验。
需要注意的是,startTransition API 并不是必须的,如果我们不使用这个 API,React 仍然会在适当的时候批量更新组件状态。这个 API 只是为了让我们更加精确地控制更新的时机,并在必要时进行优化。
在 React 中,合成事件是一种封装了浏览器原生事件对象的高级事件机制。它是由 React 提供的一种用于处理事件的抽象层,可以让开发者更方便地处理和管理事件。
React 的合成事件机制提供了一些优秀的特性:跨浏览器兼容性:React 的合成事件可以屏蔽浏览器的差异,保证在各种浏览器上运行一致。性能优化:React 的合成事件可以对事件进行池化处理,重用事件对象,避免创建大量的事件对象,从而提高性能。事件委托:React 的合成事件可以实现事件委托机制,将事件处理程序绑定在组件树的根节点上,统一管理和处理组件内部和外部的事件,从而避免多次绑定事件处理程序的问题。支持自定义事件:React 的合成事件可以支持自定义事件,开发者可以自定义组件事件,提供更多的自定义能力。
React 的合成事件机制通过事件冒泡和事件委托来实现。当在组件中触发事件时,React 会将该事件包装成一个合成事件对象,并在组件树中冒泡传递,直到根节点处。在组件树中,React 使用事件委托机制将事件处理程序绑定到根节点上,统一处理所有组件的事件。
在处理合成事件时,React 提供了一些常用的事件处理函数,例如 onClick、onMouseOver、onSubmit 等,可以在组件中直接使用。此外,开发者还可以自定义事件处理函数,通过 on 前缀加上事件名称的方式来绑定自定义事件。例如,我们可以定义一个 onCustomEvent 方法来处理自定义事件:
在这个例子中,我们定义了一个名为 handleCustomEvent 的方法来处理自定义事件,然后在组件中通过 onClick 属性来绑定该方法。当用户点击按钮时,React 会将该事件包装成一个合成事件对象,并调用 handleCustomEvent 方法来处理事件。
在 React 中,绑定事件的原理是基于合成事件(SyntheticEvent)的机制。合成事件是一种由 React 自己实现的事件系统,它是对原生 DOM 事件的封装和优化,提供了一种统一的事件处理机制,可以跨浏览器保持一致的行为。
当我们在 React 组件中使用 onClick 等事件处理函数时,实际上是在使用合成事件。React 使用一种称为“事件委托”的技术,在组件的最外层容器上注册事件监听器,然后根据事件的目标元素和事件类型来触发合适的事件处理函数。这种机制可以大大减少事件监听器的数量,提高事件处理的性能和效率。
在使用合成事件时,React 会将事件处理函数包装成一个合成事件对象(SyntheticEvent),并将其传递给事件处理函数。合成事件对象包含了与原生 DOM 事件相同的属性和方法,例如 target、currentTarget、preventDefault() 等,但是它是由 React 实现的,并不是原生的 DOM 事件对象。因此,我们不能在合成事件对象上调用 stopPropagation() 或 stopImmediatePropagation() 等方法,而应该使用 nativeEvent 属性来访问原生 DOM 事件对象。
绑定事件的实现原理也涉及到 React 的更新机制。当组件的状态或属性发生变化时,React 会对组件进行重新渲染,同时重新注册事件监听器。为了避免不必要的事件处理函数的创建和注册,React 会对事件处理函数进行缓存和复用,只有在事件处理函数发生变化时才会重新创建和注册新的事件处理函数。这种机制可以大大提高组件的性能和效率,尤其是在处理大量事件和频繁更新状态的情况下。
pnpm 和 npm 是两个不同的 JavaScript 包管理工具,它们有以下区别:包的存储方式:npm 将每个包都下载到项目的 node_modules 目录中,而 pnpm 会在全局安装一个存储库,并在项目中创建一个符号链接到该存储库中的每个包。空间占用: 由于 pnpm 使用符号链接,它的空间占用通常比 npm 小,因为它避免了在多个项目中重复存储相同的依赖项。安装速度: 由于 pnpm 在全局安装中共享依赖项,因此安装速度通常比 npm 更快。命令行接口:pnpm 的命令行接口与 npm 不同,但它们都提供了一组相似的命令来管理包。兼容性: 由于 pnpm 的存储方式不同于 npm,因此某些与 npm 兼容的工具可能无法与 pnpm 一起使用。
总的来说,pnpm 与 npm 相比具有更小的空间占用和更快的安装速度,但由于其不同的存储方式可能会导致与某些工具的不兼容。
通过一道题进入浏览器事件循环原理:
可以先试一下,手写出执行结果,然后看完这篇文章以后,在运行一下这段代码,看结果和预期是否一样
单线程意味着所有的任务需要排队,前一个任务结束,才能够执行后一个任务。如果前一个任务耗时很长,后面一个任务不得不一直等着。
javascript的单线程,与它的用途有关。作为浏览器脚本语言,javascript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定javascript同时有两个线程,一个在添加DOM节点,另外一个是删除DOM节点,那浏览器应该应该以哪个为准,如果在增加一个线程进行管理多个线程,虽然解决了问题,但是增加了复杂度,为什么不使用单线程呢,执行有个先后顺序,某个时间只执行单个事件。为了利用多核CPU的计算能力,HTML5提出Web Worker标准,运行javascript创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个标准并没有改变javascript单线程的本质
事件循环这个名字来源于它往往这么实现:
这个模型的优势在于它必须处理完一个消息(run to completion),才会处理下一个消息,使程序可追溯性更强。不像C语言可能随时从一个线程切换到另一个线程。但是缺点也在于此,若同步代码阻塞则会影响用户交互
宏队列,macroTask也叫tasks。包含同步任务,和一些异步任务的回调会依次进入macro task queue中,macroTask包含:script代码块setTimeoutrequestAnimationFrameI/OUI rendering
微队列, microtask,也叫jobs。另外一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包含:Promise.thenMutationObserver
下面是Event Loop的示意图
一段javascript执行的具体流程就是如下:首先执行宏队列中取出第一个,一段script就是相当于一个macrotask,所以他先会执行同步代码,当遇到例如setTimeout的时候,就会把这个异步任务推送到宏队列队尾中。当前macrotask执行完成以后,就会从微队列中取出位于头部的异步任务进行执行,然后微队列中任务的长度减一。然后继续从微队列中取出任务,直到整个队列中没有任务。如果在执行微队列任务的过程中,又产生了microtask,那么会加入整个队列的队尾,也会在当前的周期中执行当微队列的任务为空了,那么就需要执行下一个macrotask,执行完成以后再执行微队列,以此反复。
总结下来就是不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask是否为空,不让过不为空就执行队列中的所有microtask。然后在取下一个task以此循环
调用栈是一个栈结构,函数调用会形成一个栈帧。栈帧:调用栈中每个实体被称为栈帧,帧中包含了当前执行函数的参数和局部变量等上下文信息,函数执行完成后,它的执行上下文会从栈中弹出。 下面是调用栈和任务队列的关系:
分析文章开头的题目,可以通过在题目前面添加debugger,结合chrome的call stack进行分析:
(这里不知道怎么画动图,在晚上找的一张图,小伙伴们有好的工具,求分享); 下面借助三个数组来分析一下这段代码的执行流程,call stack表示调用栈,macroTasks表示宏队列,microTasks表示微队列:首先代码执行之前都是三个队列都是空的:
在前面提到,整个代码块就相当于一个macroTask,所以首先向callStack中压入main(),main相当于整个代码块2. 执行main,输出同步代码结果:
在遇到setTimeout和promise的时候会向macroTasks与microTasks中分别推入3. 此时的三个队列分别是:
当这段代码执行完成以后,会输出:
当main执行完成以后,会取microTasks中的任务,放入callStack中,此时的三个队列为:
当这个promise执行完成后会输出
后面又有一个then,在前面提到如果还有microtask就在微队列队尾中加入这个任务,并且在当前tick执行。所以紧接着输出promise25. 当前的tick也就完成了,最后在从macroTasks取出task,此时三个队列的状态如下:
最后输出的结果就是setTimeout。所谓的事件循环就是从两个队列中不断取出事件,然后执行,反复循环就是事件循环。经过上面的示例,理解起来是不是比较简单
在目前的前端面试中,vue的双向数据绑定已经成为了一个非常容易考到的点,即使不能当场写出来,至少也要能说出原理。本篇文章中我将会仿照vue写一个双向数据绑定的实例,名字就叫myVue吧。结合注释,希望能让大家有所收获。
Vue的双向数据绑定的原理相信大家也都十分了解了,主要是通过Object对象的defineProperty属性,重写data的set和get函数来实现的,这里对原理不做过多描述,主要还是来实现一个实例。为了使代码更加的清晰,这里只会实现最基本的内容,主要实现v-model,v-bind 和v-click三个命令,其他命令也可以自行补充。
添加网上的一张图
页面结构很简单,如下
包含:
我们最后会通过类似于vue的方式来使用我们的双向数据绑定,结合我们的数据结构添加注释
首先我们需要定义一个myVue构造函数:
为了初始化这个构造函数,给它添加一 个_init属性
接下来实现_obverse函数,对data进行处理,重写data的set和get函数
并改造_init函数
接下来我们写一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新
更新_init函数以及_obverse函数
那么如何将view与model进行绑定呢?接下来我们定义一个_compile函数,用来解析我们的指令(v-bind,v-model,v-clickde)等,并在这个过程中对view与model进行绑定。
至此,我们已经实现了一个简单vue的双向绑定功能,包括v-bind, v-model, v-click三个指令。效果如下图
附上全部代码,不到150行
如果喜欢请关注我的Github,给个Star吧,我会定期分享一些JS中的知识,^_^
在 Node.js 中,进程间通信(IPC)可以通过以下几种方式进行:使用子进程模块:可以使用 Node.js 的子进程模块(child_process)来创建子进程,并使用进程间通信机制(如进程间管道)来实现通信。使用共享内存:Node.js 中的共享内存模块(sharedArrayBuffer)可以在多个进程间共享内存,从而实现进程间通信。使用进程间消息传递:Node.js 提供了一个内置的进程间通信机制,可以使用 process.send() 方法在不同的进程之间发送消息。使用进程间的 TCP 通信:可以使用 Node.js 的 net 模块建立 TCP 服务器和客户端,从而在不同的进程之间进行通信。
需要注意的是,不同的进程之间通信可能会导致一些并发问题,例如竞态条件和死锁。因此,在设计进程间通信方案时,需要仔细考虑并发问题,并采取相应的措施来保证并发安全。
用JS模拟DOM结构。DOM变化的对比,放在JS层来做。提升重绘性能。
比如有abc 三个dom, 如果我们要删除b dom, 以前浏览器的做法是 全部删除abc dom , 然后 在添加b dom 。这样做的成本会非常高。
例如下面的一个dom 结构:
这样的dom 结构,可以模拟为下面的JS :
浏览器操作dom 是花销非常大的。执行JS花销要小非常多,所以这就是为什么虚拟dom 出现的一个根本原因。
1、数据生成表格。 2、随便修改一个信息,表格也会跟着修改。
实际上上面的这段代码也是不符合预期的,因为每次使用render 方法,都会全部渲染整个table, 但是并未没有只渲染我们想要的第二行。
遇到的问题:DOM 操作是非常 "昂贵" 的, JS 运行效率高。虚拟dom 的核心就是diff算法,对比出不同的dom数据,定点渲染不同的数据。
下面是手写实现 Promise.all() 方法的代码:
实现原理:
Promise.all() 方法接收一个包含多个 Promise 的数组作为参数,并返回一个新的 Promise。该 Promise 将会在数组中所有 Promise 状态均为 fulfilled 时被解决,并且以数组形式返回所有 Promise 的结果。
我们可以通过创建一个新的 Promise,然后遍历 Promise 数组并将每个 Promise 包装在一个 Promise.resolve() 中,然后使用 .then() 方法将它们的解决值和拒绝原因分别传递给新的 Promise 的 resolve() 和 reject() 方法。我们还需要维护一个计数器和一个结果数组来跟踪所有 Promise 的状态。每当一个 Promise 被解决时,我们将其结果存储在结果数组中,然后将计数器增加 1。当计数器等于 Promise 数组的长度时,说明所有 Promise 均已被解决,此时我们可以使用 resolve() 方法并将结果数组作为参数传递给它。如果有任何一个 Promise 被拒绝,则使用 reject() 方法并将其拒绝原因作为参数传递给它。
需要注意的是,如果 Promise 数组为空,则 Promise.all() 将立即被解决,并返回一个空数组。
Promise.allSettled 方法会接收一个 Promise 数组,并返回一个新的 Promise 对象。该新 Promise 对象会在所有输入的 Promise 都被 resolved 或 rejected 后变为 settled 状态,并且它的值是一个包含所有 Promise 状态的对象数组。
以下是手写实现 Promise.allSettled 方法的代码:
上述代码中,我们首先创建一个新的 Promise 对象,并在其中执行了一个异步操作。然后我们遍历了传入的 Promise 数组,并为每个 Promise 添加了一个 then 方法的回调函数,以便在 Promise 状态发生变化时收集 Promise 的结果。对于每个 Promise,我们都使用 Promise.resolve 方法将其转换为 Promise 对象,以确保我们处理的是 Promise 对象。我们使用一个 finally 方法来在 Promise settled 时更新 settledCount,以确保在所有 Promise settled 后我们只会执行一次 resolve 方法。
最终,我们将所有 Promise 的状态都收集到了 results 数组中,并将该数组作为 Promise 的值解析。这样,我们就实现了 Promise.allSettled 方法的功能。
CSS尺寸设置的单位包括:像素(Pixel,缩写为px):是最常用的单位,表示屏幕上的一个点,可以精确地指定元素的大小。百分比(Percentage,缩写为%):相对于父元素的大小,可以根据父元素的大小来设置子元素的大小。em:相对于当前元素的字体大小,用于设置字体大小时很常用。rem:相对于根元素(即html元素)的字体大小。vh/vw:相对于视口(Viewport)的高度和宽度。cm、mm、in、pt、pc等长度单位:用于打印样式表,不建议在Web开发中使用。自定义单位:可以通过CSS的calc()函数自定义单位,比如使用“1fr”作为网格布局中的单位。
需要注意的是,不同的浏览器可能会有不同的计算方式和默认值,因此在设置尺寸时需要进行充分的测试和兼容性处理。
React Router 是一个流行的第三方库,它允许在 React 应用程序中实现路由功能。React Router 支持两种路由方式:HashRouter 和 BrowserRouter。HashRouter
HashRouter 使用 URL 中的 hash 部分(即 #)来实现路由。在 React 中,可以使用 组件来创建 HashRouter。例如:
在使用 HashRouter 时,URL 中的路径看起来像这样:http://example.com/#/about。HashRouter 不会向服务器发送请求,因为 # 符号后面的内容被浏览器认为是 URL 的一部分,而不是服务器请求的一部分。这意味着在使用 HashRouter 时,React 应用程序可以在客户端上运行,而无需服务器支持。BrowserRouter
BrowserRouter 使用 HTML5 的 history API 来实现路由。在 React 中,可以使用 组件来创建 BrowserRouter。例如:
在使用 BrowserRouter 时,URL 中的路径看起来像这样:http://example.com/about。BrowserRouter 通过 history API 在客户端和服务器之间发送请求,因此需要服务器支持。区别
HashRouter 和 BrowserRouter 的主要区别在于它们如何处理 URL。HashRouter 使用 URL 中的 # 部分来实现路由,而 BrowserRouter 使用 HTML5 的 history API 来实现路由。HashRouter 不需要服务器支持,而 BrowserRouter 需要服务器支持。原理
HashRouter 的原理是通过监听 window.location.hash 的变化来实现路由。当用户点击链接时,React Router 会根据链接的路径渲染相应的组件,并将路径添加到 URL 中的 # 部分。当用户点击浏览器的“后退”按钮时,React Router 会根据上一个 URL 中的 # 部分来渲染相应的组件。
BrowserRouter 的原理是通过 HTML5 的 history API 来实现路由。当用户点击链接时,React Router 会使用 history API 将路径添加到浏览器的历史记录中,并渲染相应的组件。当用户点击浏览器的“后退”
Vue3.0 通过使用 Composition API 中的 reactive 和 ref 函数来实现数据双向绑定。reactive 函数
reactive 函数是 Vue3.0 中用来创建响应式对象的函数。将一个 JavaScript 对象传递给 reactive 函数,它会返回一个新的响应式对象。响应式对象是一个 Proxy 对象,可以在应用程序中使用它来自动追踪数据的变化。
例如,我们可以这样使用 reactive 函数来创建一个响应式对象:
在上面的示例中,我们使用 reactive 函数创建了一个包含一个 message 属性的响应式对象。ref 函数
ref 函数是 Vue3.0 中用来创建一个包含单个值的响应式对象的函数。将一个初始值传递给 ref 函数,它会返回一个新的响应式对象。响应式对象是一个普通对象,它有一个名为 value 的属性,该属性保存了当前值。当 value 属性的值发生改变时,Vue3.0 会自动更新应用程序的界面。
例如,我们可以这样使用 ref 函数来创建一个响应式对象:
在上面的示例中,我们使用 ref 函数创建了一个包含初始值为 0 的响应式对象。双向绑定的实现
Vue3.0 中的双向绑定可以通过在模板中使用 v-model 指令来实现。v-model 指令是 Vue3.0 中用来实现表单元素和组件的双向数据绑定的指令。例如,我们可以这样使用 v-model 指令来实现一个表单输入框的双向绑定:
在上面的示例中,我们在模板中使用 v-model 指令将输入框和 message 响应式对象进行双向绑定。当用户在输入框中输入文本时,message 响应式对象的值会自动更新,当 message 响应式对象的值发生改变时,界面上的文本也会自动更新。
总之,Vue3.0 使用 reactive 和 ref 函数来实现数据双向绑定。使用 reactive 函数可以创建包含多个属性的响应式对象,使用 ref 函数可以创建包含单个值的响应式对象。通过在模板中使用 `v-model
指令可以实现表单元素和组件的双向数据绑定,将表单元素的值绑定到响应式对象的属性上,当响应式对象的属性值变化时,自动更新绑定的表单元素的值。
除了使用 v-model 指令实现双向绑定,Vue3.0 也提供了 watch 函数和 watchEffect 函数来实现响应式数据的监听和副作用函数的执行。这些函数可以用来监听响应式数据的变化,从而执行特定的操作。下面是一个使用 watch 函数监听响应式数据变化的示例:
在上面的示例中,我们使用 watch 函数监听 count 响应式对象的变化,当 count 响应式对象的值发生变化时,会自动调用回调函数,打印出 count 变化前和变化后的值。
另外,Vue3.0 中还提供了 computed 函数用来计算一个响应式对象的值,toRefs 函数用来将一个响应式对象转换为普通的对象,并且在 TypeScript 中使用时可以使用 defineComponent 函数来定义组件的类型,从而提高代码的可读性和可维护性。
浏览器垃圾回收机制是指浏览器在运行时自动回收不再使用的内存空间的过程。以下是浏览器垃圾回收机制的几个方面:标记清除:这是一种最常用的垃圾回收机制。它的工作原理是标记所有当前正在使用的对象,然后清除未标记的对象。这种方法的优点是效率高,缺点是可能会导致内存碎片。引用计数:这种垃圾回收机制会跟踪每个对象被引用的次数,当引用计数为零时,就会回收该对象。这种方法的优点是可以立即回收不再使用的对象,缺点是无法处理循环引用。分代回收:这是一种结合了标记清除和引用计数的垃圾回收机制。它将对象分为几代,然后在不同的代上使用不同的回收策略。新创建的对象会被分配到第一代,随着时间的推移,如果它们仍然存活,它们会被转移到下一代。这种方法的优点是可以更精细地控制回收策略。
浏览器垃圾回收机制可以帮助开发人员避免内存泄漏和减少程序崩溃的风险。不同的浏览器和不同的 JavaScript 引擎实现可能有不同的垃圾回收机制,但它们的基本原理是相似的。
以下是一些常见的 web 前端网络攻击类型:跨站脚本攻击(Cross-Site Scripting, XSS):XSS攻击利用了 Web 应用程序对用户输入的不当处理,以将恶意代码注入到 Web 页面中。当用户访问包含恶意代码的页面时,攻击者可以利用这些代码窃取用户的敏感信息、劫持用户会话等。跨站请求伪造(Cross-Site Request Forgery, CSRF):CSRF攻击利用了用户已经登录了受信任网站的身份,通过在受害者的浏览器中执行恶意代码,将伪造的请求发送到受信任网站上,从而执行某些操作或者获取某些信息。点击劫持(Clickjacking):点击劫持是一种利用透明 iframe 层来覆盖网页上的其他内容,欺骗用户点击不可见的按钮或链接,以执行攻击者所需的操作。HTML 注入攻击:HTML 注入攻击利用了 Web 应用程序对用户输入的不当处理,以将恶意的 HTML 代码插入到 Web 页面中。这种攻击通常被用来修改页面内容、欺骗用户或者实施其他恶意行为。敏感数据泄露(Sensitive Data Leakage):敏感数据泄露可能会发生在 Web 应用程序中,其中攻击者可以通过暴力破解、SQL 注入等攻击方式,获取存储在数据库中的敏感数据(如用户名、密码、信用卡信息等)。带宽滥用(Bandwidth Abuse):带宽滥用是指攻击者利用 Web 应用程序或服务器的漏洞来消耗服务器的资源和带宽,从而使服务器变得缓慢或无法正常工作。HTTP 请求欺骗(HTTP Request Spoofing):HTTP 请求欺骗是一种利用 Web 应用程序对输入的不当处理,以篡改 HTTP 请求的攻击方式。攻击者可以通过伪造 HTTP 请求头信息、修改 HTTP 请求方法等方式,欺骗 Web 应用程序执行攻击者所需的操作。
需要注意的是,这些攻击类型通常会结合使用,攻击者会利用多种攻击方式,以更好地实现攻击目标。
以下是一些防范跨站脚本攻击的常见方法:输入过滤:对于所有输入的数据(如表单数据、URL 参数等),应该进行过滤和验证。特别是对于敏感数据(如密码、信用卡信息等),应该进行严格的验证,防止恶意的脚本注入。可以使用一些开源的输入验证工具,如OWASP ESAPI来过滤恶意输入。对特殊字符进行转义:对于所有输出到页面上的数据,应该对特殊字符进行转义,比如将 等。这可以防止攻击者通过在页面上注入恶意的脚本。CSP(Content Security Policy):CSP是一种浏览器安全机制,可以限制 Web 页面可以加载哪些资源。通过设置合适的 CSP,可以防止恶意脚本的注入。HttpOnly Cookie:通过设置 HttpOnly 标志,可以防止脚本访问 Cookie。这可以防止攻击者窃取用户的身份验证信息。随机化 Session ID:在用户登录后,应该为其分配一个随机化的 Session ID,防止攻击者通过猜测 Session ID 来劫持用户会话。使用安全的编程语言和框架:使用安全的编程语言和框架可以降低跨站脚本攻击的风险。比如使用最新的版本的编程语言和框架,以获得更好的安全性。
需要注意的是,防范跨站脚本攻击需要综合多种方法,单一的措施并不能完全防止攻击。此外,开发人员应该始终关注最新的安全漏洞和攻击技术,及时采取相应的防范措施。
跨站请求伪造(Cross-Site Request Forgery, CSRF)是一种常见的网络攻击方式,攻击者可以利用已登录的用户身份,通过伪造用户的请求,对服务器上的资源进行非法操作。下面是一种常见的 CSRF 攻击方式:用户在浏览器中登录了某个网站,并获取了该网站的 Cookie。攻击者诱导用户访问一个恶意网站,并在该网站上放置了一段恶意代码,用于发起 CSRF 攻击。当用户在恶意网站上执行某个操作时,比如点击某个按钮或链接,恶意代码会自动向目标网站发送一个 HTTP 请求,请求中包含攻击者想要执行的操作和参数,同时也会携带用户的 Cookie。目标网站接收到请求后,会认为这是一个合法的请求,因为它携带了用户的 Cookie。于是服务器会执行攻击者想要的操作,比如删除用户的数据、修改用户的密码等。
为了防止 CSRF 攻击,开发人员可以采取以下措施:随机化 Token:为每个请求生成一个随机化的 Token,将 Token 放入表单中,并在服务器端进行验证。这可以防止攻击者伪造合法的请求。使用 Referer 验证:在服务器端进行 Referer 验证,只允许来自合法来源的请求。这可以防止攻击者在自己的网站上放置恶意代码,进行 CSRF 攻击。使用验证码:在某些敏感操作上,比如修改密码、删除数据等,可以要求用户输入验证码。这可以降低攻击者的成功率,因为攻击者很难获取验证码。
需要注意的是,以上措施并不能完全防止 CSRF 攻击,因为攻击者总是可以通过一些复杂的方法来规避这些防御措施。因此,开发人员需要综合考虑多种防范措施,以提高网站的安全性。
defer 和 async 是用于控制脚本加载和执行的 HTML 标签属性。
defer 和 async 的主要区别在于它们对脚本的加载和执行的影响。defer 属性告诉浏览器立即下载脚本,但延迟执行,等到文档加载完成后再按照它们在页面中出现的顺序依次执行。这意味着脚本不会阻止文档的解析和渲染,并且它们也不会阻止其他脚本的执行。如果多个脚本都使用 defer 属性,则它们将按照它们在页面中出现的顺序依次执行。async 属性告诉浏览器立即下载脚本,但它们不一定按照它们在页面中出现的顺序执行。它们将在下载完成后立即执行。这意味着脚本不会阻止文档的解析和渲染,但可能会阻止其他脚本的执行。如果多个脚本都使用 async 属性,则它们将按照它们下载完成的顺序依次执行。
需要注意的是,当使用 defer 和 async 属性时,浏览器的支持情况可能不同。一些较旧的浏览器可能不支持这些属性,或者仅支持 defer 而不支持 async。因此,为了确保脚本的兼容性,建议在使用 defer 和 async 属性时,同时提供一个备用脚本,并考虑使用特性检测来检查浏览器是否支持这些属性。
$nextTick 是 Vue.js 提供的一个实例方法,用于在 DOM 更新之后执行一些操作。具体来说,它会将回调函数推迟到下次 DOM 更新循环之后执行。
在 Vue 中,数据变化时,Vue 会异步执行视图更新。例如,当一个数据变化时,Vue 会将这个变化包装成一个更新任务,并将其推入更新队列。Vue 会在下一个事件循环周期中遍历这个队列,并依次执行更新任务,最终将视图更新为最新状态。
在某些情况下,我们需要在 DOM 更新之后执行一些操作,例如在 Vue 中更新 DOM 后获取更新后的元素尺寸、在 Vue 组件中调用子组件的方法等等。如果直接在数据变化后立即执行这些操作,可能会遇到一些问题,例如元素尺寸并未更新,子组件尚未完全挂载等等。这时候,就需要使用 $nextTick 方法。
$nextTick 的实现原理是利用了 JavaScript 的事件循环机制。具体来说,当调用 $nextTick 方法时,Vue 会将回调函数推入一个回调队列中。在下一个事件循环周期中,Vue 会遍历这个回调队列,并依次执行其中的回调函数。由于在这个时候 DOM 已经完成了更新,因此可以安全地执行需要在 DOM 更新之后进行的操作。
需要注意的是,$nextTick 是异步执行的,因此不能保证回调函数会立即执行。如果需要等待 $nextTick 的回调函数执行完毕后再继续执行某些操作,可以使用 Promise 或 async/await 来等待异步操作的完成。
Axios 是一个基于 Promise 的 HTTP 客户端库,可以用于浏览器和 Node.js 环境中发送 HTTP 请求。Axios 提供了拦截器机制,可以在请求发送前和响应返回后对请求和响应进行拦截和处理,从而实现一些通用的功能,例如:添加请求头、添加认证信息、显示 loading 状态、错误处理等。
Axios 的拦截器机制主要是通过 interceptors 属性来实现的,该属性包含了 request 和 response 两个对象,分别代表请求拦截器和响应拦截器。每个对象都包含 use 方法,该方法用于注册拦截器回调函数,拦截器回调函数会在请求发送前或响应返回后被调用。
下面是一个示例代码,展示了如何使用 Axios 的拦截器:
在上面的代码中,我们首先通过 import 语句引入了 Axios 库。然后,我们调用
axios.interceptors.request.use 方法注册了一个请求拦截器回调函数,该函数会在发送请求前被调用,可以在该函数中进行一些通用的操作,例如添加请求头、添加认证信息等。接着,我们调用
axios.interceptors.response.use 方法注册了一个响应拦截器回调函数,该函数会在响应返回后被调用,可以在该函数中进行一些通用的操作,例如显示 loading 状态、错误处理等。
最后,我们使用 axios.get 方法发送请求,并通过 then 和 catch 方法处理响应数据和请求错误。在请求发送前和响应返回后,我们注册的拦截器回调函数会被自动调用,可以对请求和响应进行拦截和处理。
Axios 的拦截器机制非常强大,可以用于实现一些通用的功能,例如添加请求头、添加认证信息、显示 loading 状态、错误处理等。在实际开发中,我们经常会使用 Axios 的拦截器来提高代码的复用性和可维护性。
Axios 的拦截器机制是通过 interceptors 属性来实现的,该属性包含了 request 和 response 两个对象,分别代表请求拦截器和响应拦截器。每个对象都包含 use 方法,该方法用于注册拦截器回调函数,拦截器回调函数会在请求发送前或响应返回后被调用。
具体来说,当我们使用 axios 发送请求时,会先调用请求拦截器的回调函数,该函数会在请求发送前被调用,可以在该函数中进行一些通用的操作,例如添加请求头、添加认证信息等。如果请求拦截器返回的不是一个 Promise 对象,则会自动将其封装为一个 Promise 对象。
接着,Axios 会使用 XMLHTTPRequest 对象发送请求,并监听其状态变化事件。当响应返回后,Axios 会调用响应拦截器的回调函数,该函数会在响应返回后被调用,可以在该函数中进行一些通用的操作,例如显示 loading 状态、错误处理等。如果响应拦截器返回的不是一个 Promise 对象,则会自动将其封装为一个 Promise 对象。
需要注意的是,Axios 的拦截器是按照添加顺序依次执行的,也就是说,先添加的拦截器回调函数先执行,后添加的拦截器回调函数后执行。如果一个拦截器回调函数中没有调用 next 方法,则后面的拦截器回调函数将不会被执行。
下面是一个示例代码,展示了如何使用 Axios 的拦截器:
在上面的代码中,我们首先通过 import 语句引入了 Axios 库。然后,我们调用
axios.interceptors.request.use 方法注册了一个请求拦截器回调函数,该函数会在发送请求前被调用,可以在该函数中进行一些通用的操作,例如添加请求头、添加认证信息等。接着,我们调用
axios.interceptors.response.use 方法注册了一个响应拦
下面是一个简单实现 Axios 拦截器核心逻辑的示例代码:
在上面的代码中,我们首先定义了一个 Axios 类,该类包含了请求拦截器和响应拦截器两个属性,分别用于保存注册的拦截器回调函数。然后,我们定义了 useRequestInterceptor 和 useResponseInterceptor 两个方法,用于注册请求拦截器和响应拦截器回调函数。在这两个方法中,我们将回调函数保存到对应的属性中。
接着,我们定义了 request 方法,该方法用于发送请求。在 request 方法中,我们首先执行请求拦截器回调函数,将请求配置传递给回调函数,并将回调函数返回的结果赋值给请求配置。接着,我们使用 fetch 函数发送请求,并将响应保存到 response 变量中。然后,我们执行响应拦截器回调函数,将响应对象传递给回调函数,并将回调函数返回的结果赋值给响应对象。最后,我们返回响应对象。
在最后几行代码中,我们创建了一个 axios 实例,并使用 useRequestInterceptor 方法和 useResponseInterceptor 方法注册了请求拦截器和响应拦截器回调函数。然后,我们调用 request 方法发送请求,并使用 then 方法处理响应数据,使用 catch 方法处理请求错误。
实时通信是一种双向的通信方式,前后端都能实时地获取对方的数据和状态变化,目前主要有以下几种方法可以实现:WebSocket:WebSocket 是一种基于 TCP 协议的双向通信协议,它可以在客户端和服务器之间建立持久性的连接,并且支持服务器主动向客户端推送数据。WebSocket 协议通过 HTTP 协议的 101 状态码进行握手,握手成功后,客户端和服务器之间的通信就不再使用 HTTP 协议,而是使用 WebSocket 协议。WebSocket 协议具有低延迟、高效、实时等优点,适用于实时通信、在线游戏、股票行情等场景。Server-Sent Events(SSE):SSE 是一种基于 HTTP 协议的服务器推送技术,它允许服务器向客户端推送文本数据或事件数据,而无需客户端发起请求。SSE 协议通过 HTTP 的长连接机制实现服务器向客户端的推送,客户端通过 EventSource API 接口接收服务器推送的数据。SSE 协议比较简单,实现也比较容易,适用于需要推送数据而不需要客户端与服务器进行双向通信的场景。长轮询(Long Polling):长轮询是一种基于 HTTP 协议的服务器推送技术,它通过客户端向服务器发送一个长时间的请求,服务器在有数据更新时返回响应,否则将一直等待一段时间后才返回响应。客户端收到响应后立即发起下一次请求。长轮询比较容易实现,适用于需要实时通知客户端数据变化但不需要高实时性的场景。WebRTC:WebRTC 是一种实时通信协议,它基于 P2P 技术,可以在浏览器之间直接建立通信,并实现视频、音频、数据等多媒体的实时传输。WebRTC 协议支持点对点通信,不需要经过服务器转发,因此具有低延迟、高效、实时等优点,适用于实时视频、音频等场景。
总的来说,WebSocket 和 SSE 协议适用于需要服务器主动向客户端推送数据的场景,长轮询适用于需要实时通知客户端数据变化但不需要高实时性的场景,WebRTC 协议适用于实时视频、音频等场景。选择哪种方法要根据具体的业务场景和需求来决定。
在 React 中,当我们使用数组渲染节点列表时,通常需要给每个节点添加一个 key 属性,这是因为 React 需要通过 key 属性来判断是否需要更新某个节点,从而提高渲染性能。
具体来说,React 在进行更新时,会根据 key 属性来判断哪些节点需要更新,哪些节点需要删除,哪些节点需要新增。如果两个节点的 key 值相同,则 React 认为它们是同一个节点,会尝试进行复用,否则会销毁旧节点并创建新节点。如果没有 key 属性,React 无法判断哪些节点是同一个节点,就需要进行全量更新,这会导致性能下降。
另外,添加 key 属性还可以解决一些潜在的问题。例如,当我们通过 map 函数生成节点列表时,如果没有给节点添加 key 属性,当列表中的某个节点发生变化时,可能会导致其他节点的 props 或状态也被重置。如果给每个节点添加了唯一的 key 属性,就可以避免这种问题。
因此,总的来说,为节点列表添加 key 属性可以提高渲染性能,并避免潜在的问题。
React 的 lazy 函数可以实现代码分割,即将代码按需加载,以达到优化页面加载速度的目的。它的原理是基于 JavaScript 的动态 import() 方法实现的。
当我们使用 lazy 函数加载一个组件时,React 会自动将该组件的代码单独打包成一个单独的 JavaScript 文件,并在需要时通过网络请求加载该文件。具体来说,lazy 函数返回的是一个特殊的组件,该组件在加载时会调用传入的函数并异步加载组件代码。一般来说,我们会将异步加载的组件通过 import() 方法引入,例如:
这里的 import() 方法会返回一个 Promise,该 Promise 在组件代码加载完成后会 resolve,然后通过 React 渲染该组件。
需要注意的是,由于异步加载组件的代码是在运行时执行的,而不是在构建时,因此需要使用符合 ECMAScript 标准的动态 import() 方法。此外,在使用 lazy 函数时还需要将组件包裹在 Suspense 组件中,以处理组件加载时的占位符或错误状态。例如:
这里的 fallback 属性指定了组件加载时的占位符,当组件加载完成后会自动替换成真正的组件。
综上所述,React 的 lazy 函数通过使用动态 import() 方法实现了组件代码的按需加载,以达到优化页面加载速度的目的。如何分析页面加载慢以用户为中心的前端性能指标「译」
浏览器是一个多进程的架构,当我们每开一个tab页面,就会开一个新的进程,所以如果一个页面崩溃也不会影响到别的页面。面试的时候经常会问从输入url到页面显示都发生了什么,这次主要说说针对渲染这块而浏览器具体都做了些什么,都有哪些进程?
首先浏览器进程有如下几部分:主进程,第三方插件进程,GPU进程,渲染进程。
而渲染进程又包含了很多线程:js引擎线程,事件触发线程,定时器触发线程,异步http请求线程,GUI渲染线程。
主进程:负责页面的显示与交互,各个页面的管理,创建和销毁其他进程。网络的资源管理和下载。
GPU进程: 最多有一个,3d绘制等。
插件进程: 每种类型的插件对应一个进程。
渲染进程:称为浏览器渲染或浏览器内核,内部是多线程的;主要负责页面渲染,脚本执行,事件处理等。
GUI渲染线程:
js引擎线程:
事件触发线程:
定时器触发线程:
异步http请求进程:
看图能大致了解渲染流程的七七八八,我按照我的理解重新梳理一下:
关于提高页面性能经常听到建议说:把css代码放头部,js代码放底部。还有如果script和link都在头部,应该把script放上面。
css不会阻塞DOM解析,css阻塞DOM渲染:
从这个渲染流程图可以看出,dom解析的时候,也可以进行css的解析
js阻塞DOM解析:
如果“script”和link都在头部,把link放在头部。就会发生阻塞,浏览器会先去下载css样式,再执行js,再执行dom。 因为浏览器不知道js脚本会写些什么,如果有删除dom操作,那提前解析dom就是无用功。不过浏览器也会先“偷看”下html中是否有碰到如link、script和img等标签时,它会帮助我们先行下载里面的资源,不会傻等到解析到那里时才下载。
我们在优化js阻塞的时候经常会用defer和async异步进行js的解析,那这两个有什么区别呢?
在html解析的时候,async异步的解析js,如果js解析完毕,html还没解析完,就会停止html解析,立即执行js; 如果html解析完了就正好,直接执行js。所以还是有可能阻塞html。
在html解析的时候,defer可以异步的支持解析js,等到html解析完成后,才会执行js。必然不会阻塞html。
pnpm,英文里面的意思叫做 performant npm ,意味“高性能的 npm”,官网地址可以参考 pnpm.io/。
pnpm 相比较于 yarn/npm 这两个常用的包管理工具在性能上也有了极大的提升,根据目前官方提供的 benchmark 数据可以看出在一些综合场景下比 npm/yarn 快了大概两倍:
在这篇文章中,将会介绍一些关于 pnpm 在依赖管理方面的优化,在 monorepo 中相比较于 yarn workspace 的应用,以及也会介绍一些 pnpm 目前存在的一些缺陷,包括讨论一下未来 pnpm 会做的一些事情。
这节会通过 pnpm 在依赖管理这一块的一些不同于正常包管理工具的一些优化技巧。
介绍 pnpm 一定离不开的就是关于 pnpm 在安装依赖方面做的一些优化,根据前面的 benchmark 图可以看到其明显的性能提升。
那么 pnpm 是怎么做到如此大的提升的呢?是因为计算机里面一个叫做 Hard link 的机制,hard link 使得用户可以通过不同的路径引用方式去找到某个文件。pnpm 会在全局的 store 目录里存储项目 node_modules 文件的 hard links 。
举个例子,例如项目里面有个 1MB 的依赖 a,在 pnpm 中,看上去这个 a 依赖同时占用了 1MB 的 node_modules 目录以及全局 store 目录 1MB 的空间(加起来是 2MB),但因为 hard link 的机制使得两个目录下相同的 1MB 空间能从两个不同位置进行寻址,因此实际上这个 a 依赖只用占用 1MB 的空间,而不是 2MB。
上一节提到 store 目录用于存储依赖的 hard links,这一节简单介绍一下这个 store 目录。
一般 store 目录默认是设置在 ${os.homedir}/.pnpm-store 这个目录下,具体可以参考 @pnpm/store-path 这个 pnpm 子包中的代码:
当然用户也可以在 .npmrc 设置这个 store 目录位置,不过一般而言 store 目录对于用户来说感知程度是比较小的。
因为这样一个机制,导致每次安装依赖的时候,如果是个相同的依赖,有好多项目都用到这个依赖,那么这个依赖实际上最优情况(即版本相同)只用安装一次。
如果是 npm 或 yarn,那么这个依赖在多个项目中使用,在每次安装的时候都会被重新下载一次。
在使用 pnpm 对项目安装依赖的时候,如果某个依赖在 sotre 目录中存在了话,那么就会直接从 store 目录里面去 hard-link,避免了二次安装带来的时间消耗,如果依赖在 store 目录里面不存在的话,就会去下载一次。
当然这里你可能也会有问题:如果安装了很多很多不同的依赖,那么 store 目录会不会越来越大?
答案是当然会存在,针对这个问题,pnpm 提供了一个命令来解决这个问题: pnpm store | pnpm。
同时该命令提供了一个选项,使用方法为 pnpm store prune ,它提供了一种用于删除一些不被全局项目所引用到的 packages 的功能,例如有个包 axios@1.0.0 被一个项目所引用了,但是某次修改使得项目里这个包被更新到了 1.0.1 ,那么 store 里面的 1.0.0 的 axios 就就成了个不被引用的包,执行 pnpm store prune 就可以在 store 里面删掉它了。
该命令推荐偶尔进行使用,但不要频繁使用,因为可能某天这个不被引用的包又突然被哪个项目引用了,这样就可以不用再去重新下载这个包了。
在 pnpm 官网有一篇很经典的文章,关于介绍 pnpm 项目的 node_modules 结构: Flat node_modules is not the only way | pnpm。
在这篇文章中介绍了 pnpm 目前的 node_modules 的一些文件结构,例如在项目中使用 pnpm 安装了一个叫做 express 的依赖,那么最后会在 node_modules 中形成这样两个目录结构:
其中第一个路径是 nodejs 正常寻找路径会去找的一个目录,如果去查看这个目录下的内容,会发现里面连个 node_modules 文件都没有:
实际上这个文件只是个软连接,它会形成一个到第二个目录的一个软连接(类似于软件的快捷方式),这样 node 在找路径的时候,最终会找到 .pnpm 这个目录下的内容。
其中这个 .pnpm 是个虚拟磁盘目录,然后 express 这个依赖的一些依赖会被平铺到 .pnpm/express@4.17.1/node_modules/ 这个目录下面,这样保证了依赖能够 require 到,同时也不会形成很深的依赖层级。
在保证了 nodejs 能找到依赖路径的基础上,同时也很大程度上保证了依赖能很好的被放在一起。
pnpm 对于不同版本的依赖有着极其严格的区分要求,如果项目中某个依赖实际上依赖的 peerDeps 出现了具体版本上的不同,对于这样的依赖会在虚拟磁盘目录 .pnpm 有一个比较严格的区分,具体可以参考: pnpm.io/how-peers-a… 这篇文章。
综合而言,本质上 pnpm 的 node_modules 结构是个网状 + 平铺的目录结构。这种依赖结构主要基于软连接(即 symlink)的方式来完成。
在前面知道了 pnpm 是通过 hardlink 在全局里面搞个 store 目录来存储 node_modules 依赖里面的 hard link 地址,然后在引用依赖的时候则是通过 symlink 去找到对应虚拟磁盘目录下(.pnpm 目录)的依赖地址。
这两者结合在一起工作之后,假如有一个项目依赖了 bar@1.0.0 和 foo@1.0.0 ,那么最后的 node_modules 结构呈现出来的依赖结构可能会是这样的:
node_modules 中的 bar 和 foo 两个目录会软连接到 .pnpm 这个目录下的真实依赖中,而这些真实依赖则是通过 hard link 存储到全局的 store 目录中。
读到这里,可能有用户会好奇: 像 hard link 和 symlink 这种方式在所有的系统上都是兼容的吗?
实际上 hard link 在主流系统上(Unix/Win)使用都是没有问题的,但是 symlink 即软连接的方式可能会在 windows 存在一些兼容的问题,但是针对这个问题,pnpm 也提供了对应的解决方案:
在 win 系统上使用一个叫做 junctions 的特性来替代软连接,这个方案在 win 上的兼容性要好于 symlink。
或许你也会好奇为啥 pnpm 要使用 hard links 而不是全都用 symlink 来去实现。
实际上存在 store 目录里面的依赖也是可以通过软连接去找到的,nodejs 本身有提供一个叫做 --preserve-symlinks 的参数来支持 symlink,但实际上这个参数实际上对于 symlink 的支持并不好导致作者放弃了该方案从而采用 hard links 的方式:
具体可以参考 github.com/nodejs/node… 该issue 讨论。
pnpm 在 monorepo 场景可以说算得上是个完美的解决方案了,因为其本身的设计机制,导致很多关键或者说致命的问题都得到了相当有效的解决。
对于 monorepo 类型的项目,pnpm 提供了 workspace 来支持,具体可以参考官网文档: pnpm.io/workspaces/…
Monorepo 下被人诟病较多的问题,一般是依赖结构问题。常见的两个问题就是 Phantom dependencies 和 NPM doppelgangers,用 rush 官网 的图片可以很贴切的展示着两个问题:
下面会针对两个问题一一介绍。
Phantom dependencies 被称之为幽灵依赖,解释起来很简单,即某个包没有被安装(package.json 中并没有,但是用户却能够引用到这个包)。
引发这个现象的原因一般是因为 node_modules 结构所导致的,例如使用 yarn 对项目安装依赖,依赖里面有个依赖叫做 foo,foo 这个依赖同时依赖了 bar,yarn 会对安装的 node_modules 做一个扁平化结构的处理(npm v3 之后也是这么做的),会把依赖在 node_modules 下打平,这样相当于 foo 和 bar 出现在同一层级下面。那么根据 nodejs 的寻径原理,用户能 require 到 foo,同样也能 require 到 bar。
那么这里这个 bar 就成了一个幽灵依赖,如果某天某个版本的 foo 依赖不再依赖 bar 或者 foo 的版本发生了变化,那么 require bar 的模块部分就会抛错。
以上其实只是一个简单的例子,但是根据笔者在字节内部见到的一些 monorepo(主要为 lerna + yarn )项目中,这其实是个比较常见的现象,甚至有些包会直接去利用这种残缺的引入方式去减轻包体积。
还有一种场景就是在 lerna + yarn workspace 的项目里面,因为 yarn 中提供了 hoist 机制(即一些底层子项目的依赖会被提升到顶层的 node_modules 中),这种 phantom dependencies 会更多,一些底层的子项目经常会去 require 一些在自己里面没有引入的依赖,而直接去找顶层 node_modules 的依赖(nodejs 这里的寻径是个递归上下的过程)并使用。
而根据前面提到的 pnpm 的 node_modules 依赖结构,这种现象是显然不会发生的,因为被打平的依赖会被放到 .pnpm 这个虚拟磁盘目录下面去,用户通过 require 是根本找不到的。
值得一提的是,pnpm 本身其实也提供了将依赖提升并且按照 yarn 那种形式组织的 node_modules 结构的 Option,作者将其命名为 --shamefully-hoist ,即 "羞耻的 hoist".....
这个问题其实也可以说是 hoist 导致的,这个问题可能会导致有大量的依赖的被重复安装,举个例子:
例如有个 package,下面依赖有 lib_a、lib_b、lib_c、lib_d,其中 a 和 b 依赖 util_e@1.0.0,而 c 和 d 依赖 util_e@2.0.0。
那么早期 npm 的依赖结构应该是这样的:
这样必然会导致很多依赖被重复安装,于是就有了 hoist 和打平依赖的操作:
但是这样也只能提升一个依赖,如果两个依赖都提升了会导致冲突,这样同样会导致一些不同版本的依赖被重复安装多次,这里就会导致使用 npm 和 yarn 的性能损失。
如果是 pnpm 的话,这里因为依赖始终都是存在 store 目录下的 hard links ,一份不同的依赖始终都只会被安装一次,因此这个是能够被彻彻底底的消除的。
前面有提到关于 pnpm 的主要问题在于 symlink(软链接)在一些场景下会存在兼容的问题,可以参考作者在 nodejs 那边开的一个 discussion:github.com/nodejs/node…
在里面作者提到了目前 nodejs 软连接不能适用的一些场景,希望 nodejs 能提供一种 link 方式而不是使用软连接,同时也提到了 pnpm 目前因为软连接而不能使用的场景:Electron 应用无法使用 pnpm部署在 lambda 上的应用无法使用 pnpm
笔者在字节内部使用 pnpm 时也遇到过一些 nodejs 基础库不支持 symlink 的情况导致使用 pnpm 无法正常工作,不过这些库在迭代更新之后也会支持这一特性。
参考文档:pnpm + workspace + changesets 构建你的 monorepo 工程现代 Monorepo 工程技术选型,聊聊我的思考前端工程化之多个项目如何同时高效管理 — monorepo
整体流程图:
参考文档:Vue 编译三部曲:如何将 template 编译成 AST ?Vue 编译三部曲:模型树优化Vue 编译三部曲:最后一曲,render code 生成
React 中的 Diff 算法,是用于比较新旧两个虚拟 DOM 树,找出需要更新的节点并进行更新的算法。React 的 Diff 算法实现基于以下假设:两个不同类型的元素会产生不同的树形结构。对于同一层级的一组子节点,它们可以通过唯一 id 匹配到相同的节点。每个组件都有一个唯一标识符 key。
基于以上假设,React 的 Diff 算法分为两个阶段:O(n) 的遍历,对比新旧两棵树的每一个节点,并记录节点的变更。在这个过程中,React 使用了双端队列(Double-ended queue)作为辅助数据结构,以保证遍历的高效性。O(k) 的反向遍历,根据记录的变更列表对 DOM 进行更新。
在第一阶段中,React 的 Diff 算法会从两棵树的根节点开始,依次对比它们的子节点。如果某个节点在新旧两个树中都存在,那么就将其进行更新。如果新树中有新节点,那么就将其插入到旧树中对应的位置。如果旧树中有节点不存在于新树中,那么就将其从 DOM 树中移除。
在第二阶段中,React 会根据记录的变更列表对 DOM 进行更新。这个过程中,React 会按照更新的优先级进行更新,优先更新需要移动的节点,其次更新需要删除的节点,最后再更新需要插入的节点。
需要注意的是,React 的 Diff 算法并不保证一定找到最优解,但是它保证了在大多数情况下,找到的解都是比较优的。同时,React 的 Diff 算法也具有一定的限制,比如无法跨越组件边界进行优化,这也是 React 中尽量避免多层嵌套组件的原因之一。
React diff算法是一种优化算法,用于比较两个虚拟DOM树的差异,以最小化DOM操作的数量,从而提高渲染性能。 以下是一个简单的实现React diff算法的代码:
微前端已经成为前端领域比较火爆的话题,在技术方面,微前端有一个始终绕不过去的话题就是前端沙箱
Sandboxie(又叫沙箱、沙盘)即是一个虚拟系统程序,允许你在沙盘环境中运行浏览器或其他程序,因此运行所产生的变化可以随后删除。它创造了一个类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。 在网络安全中,沙箱指在隔离环境中,用以测试不受信任的文件或应用程序等行为的工具
简单来说沙箱(sandbox)就是与外界隔绝的一个环境,内外环境互不影响,外界无法修改该环境内任何信息,沙箱内的东西单独属于一个世界。
对于 JavaScript 来说,沙箱并非传统意义上的沙箱,它只是一种语法上的 Hack 写法,沙箱是一种安全机制,把一些不信任的代码运行在沙箱之内,使其不能访问沙箱之外的代码。当需要解析或着执行不可信的 JavaScript 的时候,需要隔离被执行代码的执行环境的时候,需要对执行代码中可访问对象进行限制,通常开始可以把 JavaScript 中处理模块依赖关系的闭包称之为沙箱。
我们大致可以把沙箱的实现总体分为两个部分:构建一个闭包环境模拟原生浏览器对象
我们知道 JavaScript 中,关于作用域(scope),只有全局作用域(global scope)、函数作用域(function scope)以及从 ES6 开始才有的块级作用域(block scope)。如果要将一段代码中的变量、函数等的定义隔离出来,受限于 JavaScript 对作用域的控制,只能将这段代码封装到一个 Function 中,通过使用 function scope 来达到作用域隔离的目的。也因为需要这种使用函数来达到作用域隔离的目的方式,于是就有 IIFE(立即调用函数表达式),这是一个被称为 自执行匿名函数的设计模式
当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问,它拥有独立的词法作用域。不仅避免了外界访问 IIFE 中的变量,而且又不会污染全局作用域,弥补了 JavaScript 在 scope 方面的缺陷。一般常见于写插件和类库时,如 JQuery 当中的沙箱模式
当将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。
模拟原生浏览器对象的目的是为了,防止闭包环境,操作原生对象。篡改污染原生环境;完成模拟浏览器对象之前我们需要先关注几个不常用的 API。
eval 函数可将字符串转换为代码执行,并返回一个或多个值
由于 eval 执行的代码可以访问闭包和全局范围,因此就导致了代码注入的安全问题,因为代码内部可以沿着作用域链往上找,篡改全局变量,这是我们不希望的
Function 构造函数创建一个新的 Function 对象。直接调用这个构造函数可用动态创建函数
语法
new Function ([arg1[, arg2[, ...argN]],] functionBody)
arg1, arg2, ... argN 被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的 JavaScript 标识符的字符串,或者一个用逗号分隔的有效字符串的列表;例如“×”,“theValue”,或“a,b”。
functionBody 一个含有包括函数定义的 JavaScript 语句的字符串。
同样也会遇到和 eval 类似的的安全问题和相对较小的性能问题。
与 eval 不同的是 Function 创建的函数只能在全局作用域中运行。它无法访问局部闭包变量,它们总是被创建于全局环境,因此在运行时它们只能访问全局变量和自己的局部变量,不能访问它们被 Function 构造器创建时所在的作用域的变量;但是,它仍然可以访问全局范围。new Function()是 eval()更好替代方案。它具有卓越的性能和安全性,但仍没有解决访问全局的问题。
with 是 JavaScript 中一个关键字,扩展一个语句的作用域链。它允许半沙盒执行。那什么叫半沙盒?语句将某个对象添加到作用域链的顶部,如果在沙盒中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出 ReferenceError。
究其原理,with在内部使用in运算符。对于块内的每个变量访问,它都在沙盒条件下计算变量。如果条件是 true,它将从沙盒中检索变量。否则,就在全局范围内查找变量。但是 with 语句使程序在查找变量值时,都是先在指定的对象中查找。所以对于那些本来不是这个对象的属性的变量,查找起来会很慢,对于有性能要求的程序不适合(JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。)。with 也会导致数据泄漏(在非严格模式下,会自动在全局作用域创建一个全局变量)
in 运算符能够检测左侧操作数是否为右侧操作数的成员。其中,左侧操作数是一个字符串,或者可以转换为字符串的表达式,右侧操作数是一个对象或数组。
配合 with 用法可以稍微限制沙盒作用域,先从当前的 with 提供对象查找,但是如果查找不到依然还能从上获取,污染或篡改全局环境。
由上部分内容思考,假如可以做到在使用with对于块内的每个变量访问都限制在沙盒条件下计算变量,从沙盒中检索变量。那么是否可以完美的解决JavaScript沙箱机制。
使用 with 再加上 proxy 实现 JavaScript 沙箱
ES6 Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,属于一种“元编程”(meta programming)
我们前面提到with在内部使用in运算符来计算变量,如果条件是 true,它将从沙盒中检索变量。理想状态下没有问题,但也总有些特例独行的存在,比如 Symbol.unscopables。
Symbol.unscopables
Symbol.unscopables 对象的 Symbol.unscopables 属性,指向一个对象。该对象指定了使用 with 关键字时,哪些属性会被 with 环境排除。
由此我们的代码还需要修改如下:
Symbol.unscopables 定义对象的不可作用属性。Unscopeable 属性永远不会从 with 语句中的沙箱对象中检索,而是直接从闭包或全局范围中检索。
以下是 qiankun 的 snapshotSandbox 的源码,这里为了帮助理解做部分精简及注释。
快照沙箱实现来说比较简单,主要用于不支持 Proxy 的低版本浏览器,原理是基于diff来实现的,在子应用激活或者卸载时分别去通过快照的形式记录或还原状态来实现沙箱,snapshotSandbox 会污染全局 window。
qiankun 框架 singular 模式下 proxy 沙箱实现,为了便于理解,这里做了部分代码的精简和注释。
legacySandBox 还是会操作 window 对象,但是他通过激活沙箱时还原子应用的状态,卸载时还原主应用的状态来实现沙箱隔离的,同样会对 window 造成污染,但是性能比快照沙箱好,不用遍历 window 对象。
在 qiankun 的沙箱 proxySandbox 源码里面是对 fakeWindow 这个对象进行了代理,而这个对象是通过 createFakeWindow 方法得到的,这个方法是将 window 的 document、location、top、window 等等属性拷贝一份,给到 fakeWindow。
源码展示:
proxySandbox 由于是拷贝复制了一份 fakeWindow,不会污染全局 window,同时支持多个子应用同时加载。 详细源码请查看:proxySandbox
常见的有:CSS ModulenamespaceDynamic StyleSheetcss in jsShadow DOM 常见的我们这边不再赘述,这里我们重点提一下Shadow DO。
Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中——它以 shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样。
网友评论