共计 33954 个字符,预计需要花费 85 分钟才能阅读完成。
2023.03.14 – 2023.03.24 更新前端面试问题总结(45 道题)– 下部
获取更多面试问题能够拜访
github 地址: https://github.com/pro-collection/interview-question/issues
gitee 地址: 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 沙盒的形式有哪些?【工程化】
121. 如何避免 跨站脚本攻打(Cross-Site Scripting, XSS)?【网络】
以下是一些防备跨站脚本攻打的常见办法:
- 输出过滤:对于所有输出的数据(如表单数据、URL 参数等),应该进行过滤和验证。特地是对于敏感数据(如明码、信用卡信息等),应该进行严格的验证,避免歹意的脚本注入。能够应用一些开源的输出验证工具,如 OWASP ESAPI 来过滤歹意输出。
- 对特殊字符进行本义:对于所有输入到页面上的数据,应该对特殊字符进行本义,比方将 < 本义为 <、将 > 本义为 > 等。这能够避免攻击者通过在页面上注入歹意的脚本。
- CSP(Content Security Policy):CSP 是一种浏览器平安机制,能够限度 Web 页面能够加载哪些资源。通过设置适合的 CSP,能够避免歹意脚本的注入。
- HttpOnly Cookie:通过设置 HttpOnly 标记,能够避免脚本拜访 Cookie。这能够避免攻击者窃取用户的身份验证信息。
- 随机化 Session ID:在用户登录后,应该为其调配一个随机化的 Session ID,避免攻击者通过猜想 Session ID 来劫持用户会话。
- 应用平安的编程语言和框架:应用平安的编程语言和框架能够升高跨站脚本攻打的危险。比方应用最新的版本的编程语言和框架,以取得更好的安全性。
须要留神的是,防备跨站脚本攻打须要综合多种办法,繁多的措施并不能齐全避免攻打。此外,开发人员应该始终关注最新的安全漏洞和攻打技术,及时采取相应的防范措施。
122. 跨站申请伪造(Cross-Site Request Forgery, CSRF)具体实现步骤是啥,如何避免?【网络】
跨站申请伪造(Cross-Site Request Forgery, CSRF)是一种常见的网络攻击形式,攻击者能够利用已登录的用户身份,通过伪造用户的申请,对服务器上的资源进行非法操作。上面是一种常见的 CSRF 攻击方式:
- 用户在浏览器中登录了某个网站,并获取了该网站的 Cookie。
- 攻击者诱导用户拜访一个歹意网站,并在该网站上搁置了一段恶意代码,用于发动 CSRF 攻打。
- 当用户在歹意网站上执行某个操作时,比方点击某个按钮或链接,恶意代码会主动向指标网站发送一个 HTTP 申请,申请中蕴含攻击者想要执行的操作和参数,同时也会携带用户的 Cookie。
- 指标网站接管到申请后,会认为这是一个非法的申请,因为它携带了用户的 Cookie。于是服务器会执行攻击者想要的操作,比方删除用户的数据、批改用户的明码等。
为了避免 CSRF 攻打,开发人员能够采取以下措施:
- 随机化 Token:为每个申请生成一个随机化的 Token,将 Token 放入表单中,并在服务器端进行验证。这能够避免攻击者伪造非法的申请。
- 应用 Referer 验证:在服务器端进行 Referer 验证,只容许来自非法起源的申请。这能够避免攻击者在本人的网站上搁置恶意代码,进行 CSRF 攻打。
- 应用验证码:在某些敏感操作上,比方批改明码、删除数据等,能够要求用户输出验证码。这能够升高攻击者的成功率,因为攻击者很难获取验证码。
须要留神的是,以上措施并不能齐全避免 CSRF 攻打,因为攻击者总是能够通过一些简单的办法来躲避这些进攻措施。因而,开发人员须要综合思考多种防范措施,以进步网站的安全性。
123.script 标签 defer 和 async 区别?【浏览器】
defer
和 async
是用于管制脚本加载和执行的 HTML <script>
标签属性。
defer
和 async
的次要区别在于它们对脚本的加载和执行的影响。
defer
属性通知浏览器立刻下载脚本,但提早执行,等到文档加载实现后再依照它们在页面中呈现的程序顺次执行。这意味着脚本不会阻止文档的解析和渲染,并且它们也不会阻止其余脚本的执行。如果多个脚本都应用defer
属性,则它们将依照它们在页面中呈现的程序顺次执行。async
属性通知浏览器立刻下载脚本,但它们不肯定依照它们在页面中呈现的程序执行。它们将在下载实现后立刻执行。这意味着脚本不会阻止文档的解析和渲染,但可能会阻止其余脚本的执行。如果多个脚本都应用async
属性,则它们将依照它们下载实现的程序顺次执行。
须要留神的是,当应用 defer
和 async
属性时,浏览器的反对状况可能不同。一些较旧的浏览器可能不反对这些属性,或者仅反对 defer
而不反对 async
。因而,为了确保脚本的兼容性,倡议在应用 defer
和 async
属性时,同时提供一个备用脚本,并思考应用个性检测来查看浏览器是否反对这些属性。
124.Vue 中 $nextTick 作用与原理是啥?【web 框架】
$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 来期待异步操作的实现。
128.axios 的拦截器原理及利用、简略手写外围逻辑?【web 框架】
axios 拦截器的应用
Axios 是一个基于 Promise 的 HTTP 客户端库,能够用于浏览器和 Node.js 环境中发送 HTTP 申请。Axios 提供了拦截器机制,能够在申请发送前和响应返回后对申请和响应进行拦挡和解决,从而实现一些通用的性能,例如:增加申请头、增加认证信息、显示 loading 状态、错误处理等。
Axios 的拦截器机制次要是通过 interceptors
属性来实现的,该属性蕴含了 request
和 response
两个对象,别离代表申请拦截器和响应拦截器。每个对象都蕴含 use
办法,该办法用于注册拦截器回调函数,拦截器回调函数会在申请发送前或响应返回后被调用。
上面是一个示例代码,展现了如何应用 Axios 的拦截器:
import axios from 'axios'
// 增加申请拦截器
axios.interceptors.request.use(function (config) {
// 在发送申请之前做些什么
console.log('申请拦截器')
return config
}, function (error) {
// 对申请谬误做些什么
return Promise.reject(error)
})
// 增加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
console.log('响应拦截器')
return response
}, function (error) {
// 对响应谬误做点什么
return Promise.reject(error)
})
// 发送申请
axios.get('/api/user')
.then(function (response) {// 解决响应数据})
.catch(function (error) {// 解决申请谬误})
在下面的代码中,咱们首先通过 import
语句引入了 Axios 库。而后,咱们调用 axios.interceptors.request.use
办法注册了一个申请拦截器回调函数,该函数会在发送申请前被调用,能够在该函数中进行一些通用的操作,例如增加申请头、增加认证信息等。接着,咱们调用 axios.interceptors.response.use
办法注册了一个响应拦截器回调函数,该函数会在响应返回后被调用,能够在该函数中进行一些通用的操作,例如显示 loading 状态、错误处理等。
最初,咱们应用 axios.get
办法发送申请,并通过 then
和 catch
办法解决响应数据和申请谬误。在申请发送前和响应返回后,咱们注册的拦截器回调函数会被主动调用,能够对申请和响应进行拦挡和解决。
Axios 的拦截器机制十分弱小,能够用于实现一些通用的性能,例如增加申请头、增加认证信息、显示 loading 状态、错误处理等。在理论开发中,咱们常常会应用 Axios 的拦截器来进步代码的复用性和可维护性。
axios 拦截器原理
Axios 的拦截器机制是通过 interceptors
属性来实现的,该属性蕴含了 request
和 response
两个对象,别离代表申请拦截器和响应拦截器。每个对象都蕴含 use
办法,该办法用于注册拦截器回调函数,拦截器回调函数会在申请发送前或响应返回后被调用。
具体来说,当咱们应用 axios
发送申请时,会先调用申请拦截器的回调函数,该函数会在申请发送前被调用,能够在该函数中进行一些通用的操作,例如增加申请头、增加认证信息等。如果申请拦截器返回的不是一个 Promise 对象,则会主动将其封装为一个 Promise 对象。
接着,Axios 会应用 XMLHTTPRequest 对象发送申请,并监听其状态变动事件。当响应返回后,Axios 会调用响应拦截器的回调函数,该函数会在响应返回后被调用,能够在该函数中进行一些通用的操作,例如显示 loading 状态、错误处理等。如果响应拦截器返回的不是一个 Promise 对象,则会主动将其封装为一个 Promise 对象。
须要留神的是,Axios 的拦截器是依照增加程序顺次执行的,也就是说,先增加的拦截器回调函数先执行,后增加的拦截器回调函数后执行。如果一个拦截器回调函数中没有调用 next
办法,则前面的拦截器回调函数将不会被执行。
上面是一个示例代码,展现了如何应用 Axios 的拦截器:
import axios from 'axios'
// 增加申请拦截器
axios.interceptors.request.use(function (config) {
// 在发送申请之前做些什么
console.log('申请拦截器')
return config
}, function (error) {
// 对申请谬误做些什么
return Promise.reject(error)
})
// 增加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
console.log('响应拦截器')
return response
}, function (error) {
// 对响应谬误做点什么
return Promise.reject(error)
})
// 发送申请
axios.get('/api/user')
.then(function (response) {// 解决响应数据})
.catch(function (error) {// 解决申请谬误})
在下面的代码中,咱们首先通过 import
语句引入了 Axios 库。而后,咱们调用 axios.interceptors.request.use
办法注册了一个申请拦截器回调函数,该函数会在发送申请前被调用,能够在该函数中进行一些通用的操作,例如增加申请头、增加认证信息等。接着,咱们调用 axios.interceptors.response.use
办法注册了一个响应拦
axios 拦截器外围逻辑代码实现
上面是一个简略实现 Axios 拦截器外围逻辑的示例代码:
class Axios {constructor() {
// 申请拦截器
this.requestInterceptors = []
// 响应拦截器
this.responseInterceptors = []}
// 注册申请拦截器
useRequestInterceptor(callback) {this.requestInterceptors.push(callback)
}
// 注册响应拦截器
useResponseInterceptor(callback) {this.responseInterceptors.push(callback)
}
// 发送申请
async request(config) {
// 执行申请拦截器
for (const interceptor of this.requestInterceptors) {config = await interceptor(config)
}
// 发送申请
const response = await fetch(config.url, {
method: config.method,
headers: config.headers,
body: config.data
})
// 执行响应拦截器
for (const interceptor of this.responseInterceptors) {response = await interceptor(response)
}
return response
}
}
// 创立 Axios 实例
const axios = new Axios()
// 注册申请拦截器
axios.useRequestInterceptor(config => {
// 在申请头中增加认证信息
config.headers['Authorization'] = 'Bearer xxx'
return config
})
// 注册响应拦截器
axios.useResponseInterceptor(response => {
// 解决响应数据
return response.json()})
// 发送申请
axios.request({
url: '/api/user',
method: 'GET'
}).then(data => {
// 解决响应数据
console.log(data)
}).catch(error => {
// 解决申请谬误
console.error(error)
})
在下面的代码中,咱们首先定义了一个 Axios
类,该类蕴含了申请拦截器和响应拦截器两个属性,别离用于保留注册的拦截器回调函数。而后,咱们定义了 useRequestInterceptor
和 useResponseInterceptor
两个办法,用于注册申请拦截器和响应拦截器回调函数。在这两个办法中,咱们将回调函数保留到对应的属性中。
接着,咱们定义了 request
办法,该办法用于发送申请。在 request
办法中,咱们首先执行申请拦截器回调函数,将申请配置传递给回调函数,并将回调函数返回的后果赋值给申请配置。接着,咱们应用 fetch
函数发送申请,并将响应保留到 response
变量中。而后,咱们执行响应拦截器回调函数,将响应对象传递给回调函数,并将回调函数返回的后果赋值给响应对象。最初,咱们返回响应对象。
在最初几行代码中,咱们创立了一个 axios
实例,并应用 useRequestInterceptor
办法和 useResponseInterceptor
办法注册了申请拦截器和响应拦截器回调函数。而后,咱们调用 request
办法发送申请,并应用 then
办法解决响应数据,应用 catch
办法解决申请谬误。
129. 有什么办法能够放弃前后端实时通信?【网络】
实时通信是一种双向的通信形式,前后端都能实时地获取对方的数据和状态变动,目前次要有以下几种办法能够实现:
- 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 协定实用于实时视频、音频等场景。抉择哪种办法要依据具体的业务场景和需要来决定。
130.react 遍历渲染节点列表,为什么要加 key ?【web 框架】
在 React 中,当咱们应用数组渲染节点列表时,通常须要给每个节点增加一个 key
属性,这是因为 React 须要通过 key
属性来判断是否须要更新某个节点,从而进步渲染性能。
具体来说,React 在进行更新时,会依据 key
属性来判断哪些节点须要更新,哪些节点须要删除,哪些节点须要新增。如果两个节点的 key
值雷同,则 React 认为它们是同一个节点,会尝试进行复用,否则会销毁旧节点并创立新节点。如果没有 key
属性,React 无奈判断哪些节点是同一个节点,就须要进行全量更新,这会导致性能降落。
另外,增加 key
属性还能够解决一些潜在的问题。例如,当咱们通过 map
函数生成节点列表时,如果没有给节点增加 key
属性,当列表中的某个节点发生变化时,可能会导致其余节点的 props
或状态也被重置。如果给每个节点增加了惟一的 key
属性,就能够防止这种问题。
因而,总的来说,为节点列表增加 key
属性能够进步渲染性能,并防止潜在的问题。
131.react lazy import 实现懒加载的原理是什么?【web 框架】
React 的 lazy
函数能够实现代码宰割,行将代码按需加载,以达到优化页面加载速度的目标。它的原理是基于 JavaScript 的动静 import()
办法实现的。
当咱们应用 lazy
函数加载一个组件时,React 会主动将该组件的代码独自打包成一个独自的 JavaScript 文件,并在须要时通过网络申请加载该文件。具体来说,lazy
函数返回的是一个非凡的组件,该组件在加载时会调用传入的函数并异步加载组件代码。一般来说,咱们会将异步加载的组件通过 import()
办法引入,例如:
jsCopy codeconst MyComponent = React.lazy(() => import('./MyComponent'));
这里的 import()
办法会返回一个 Promise,该 Promise 在组件代码加载实现后会 resolve,而后通过 React 渲染该组件。
须要留神的是,因为异步加载组件的代码是在运行时执行的,而不是在构建时,因而须要应用合乎 ECMAScript 规范的动静 import()
办法。此外,在应用 lazy
函数时还须要将组件包裹在 Suspense
组件中,以解决组件加载时的占位符或谬误状态。例如:
jsCopy codeimport React, {lazy, Suspense} from 'react';
const MyComponent = lazy(() => import('./MyComponent'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
</div>
);
}
这里的 fallback
属性指定了组件加载时的占位符,当组件加载实现后会主动替换成真正的组件。
综上所述,React 的 lazy
函数通过应用动静 import()
办法实现了组件代码的按需加载,以达到优化页面加载速度的目标。
136. 如何剖析页面加载慢?【工程化】
- 如何剖析页面加载慢
137.【性能】以用户为核心的前端性能指标有哪些?【工程化】
- 以用户为核心的前端性能指标「译」
138. 浏览器渲染过程理解多少?【浏览器】
浏览器过程:
浏览器是一个多过程的架构,当咱们每开一个 tab 页面,就会开一个新的过程,所以如果一个页面解体也不会影响到别的页面。面试的时候常常会问从输出 url 到页面显示都产生了什么,这次次要说说针对渲染这块而浏览器具体都做了些什么,都有哪些过程?
首先浏览器过程有如下几局部:主过程 , 第三方插件过程,GPU 过程,渲染过程。
而渲染过程又蕴含了很多线程:js 引擎线程,事件触发线程,定时器触发线程,异步 http 申请线程,GUI 渲染线程。
主过程:负责页面的显示与交互,各个页面的治理,创立和销毁其余过程。网络的资源管理和下载。
GPU 过程:最多有一个,3d 绘制等。
插件过程:每种类型的插件对应一个过程。
渲染过程:称为浏览器渲染或浏览器内核,外部是多线程的;次要负责页面渲染,脚本执行,事件处理等。
GUI 渲染线程:
1. 负责渲染浏览器界面,解析 html,css,构建 dom 树和 render 树,布局和绘制。2. 当重绘和回流的时候就会执行这个线程
3. GUI 渲染线程和 js 引擎线程互斥,当 js 引擎执行时,GUI 线程就会被挂起(相当于解冻了),GUI 更新会被保留在一个队列中等到 js 引擎闲暇时立刻执行。
js 引擎线程:
1. 也称 js 内核,负责解决 js 脚本程序,例如 v8 引擎
2. 负责解析 js 脚本,运行代码
3. 期待工作队列中的工作,一个 tab 页只有一个 js 过程
4. 因为与 GUI 渲染线程互斥,所以 js 执行过长时间,就会造成页面渲染不连贯,导致页面渲染阻塞
事件触发线程:
1. 归属于浏览器而不是 js 引擎,用了管制事件循环
2. 当 js 引擎执行 settimeout 相似的代码块时,会将对应工作增加到事件线程
3. 当对应的事件合乎触发条件时,会被放到工作队列的队尾,期待 js 引擎线程解决
4. 因为 js 单线程的关系,这些期待解决的事件都须要排队期待 js 引擎解决
定时器触发线程:
1. settimeout 和 setinterval 所在的线程
2. 浏览器定时计数器不是由 js 引擎线程计数的,因而通过独自线程来计时触发定时,计时结束后,增加到事件队列,期待 js 引擎执行。
异步 http 申请过程:
1. 在 XMLHttpRequest 在连贯后是通过浏览器新开一个线程申请。2. 将检测到状态变更时, 如果设置有回调函数, 异步线程就产生状态变更事件, 将这个回调再放入事件队列中。再由 JavaScript 引擎执行
看图能大抵理解渲染流程的七七八八,我依照我的了解从新梳理一下:
1. 构建 DOM 树。因为浏览器无奈了解和间接应用 html 所以须要转换成 dom 树的模式,对 html 进行解析。2. 款式计算,对 css 进行解析。首先把 css 文本转化成浏览器能够了解的构造 --stylesheets,而后对 stylesheets 进行标准化解决,就是将一些属性值转化为渲染引擎更容易了解,标准化的计算值(例如,color 单词模式转化为 rgb,em 单位转化为 px),其次计算 dom 节点的款式属性。3. 布局阶段。a. 首先创立布局:遍历 dom 中所有节点,并增加到布局树中。b. 布局计算:通过 js 和 css,计算 dom 在页面上的地位。c. 最初创立布局树。4. 分层。依据简单的 3d 转换,页面滚动,还有 z -index 属性都会造成独自图层,把图层依照正确顺序排列。生成分层树。5. 图层绘制,栅格化以及图层显示。对每个图层进行独自的绘制,并提交到合成器线程。6. 合成线程将图层分为图块,并在栅格化线程池中将图块转化为位图。7. 合成线程发送绘制图块命令 drawquads 给浏览器过程。8. 浏览器依据 drawquads 音讯生成页面展现进去
css 阻塞,js 阻塞:
对于进步页面性能常常听到倡议说:把 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 的解析,那这两个有什么区别呢?
async:
在 html 解析的时候,async 异步的解析 js,如果 js 解析结束,html 还没解析完,就会进行 html 解析,立刻执行 js;如果 html 解析完了就正好,间接执行 js。所以还是有可能阻塞 html。
defer:
在 html 解析的时候,defer 能够异步的反对解析 js,等到 html 解析实现后,才会执行 js。必然不会阻塞 html。
140.pnpm 理解多少?【工程化】
pnpm,英文外面的意思叫做 performant npm
,象征“高性能的 npm”,官网地址能够参考 pnpm.io/。
pnpm 相比拟于 yarn/npm 这两个罕用的包管理工具在性能上也有了极大的晋升,依据目前官网提供的 benchmark 数据能够看出在一些综合场景下比 npm/yarn 快了大略两倍:
在这篇文章中,将会介绍一些对于 pnpm 在依赖治理方面的优化,在 monorepo 中相比拟于 yarn workspace 的利用,以及也会介绍一些 pnpm 目前存在的一些缺点,包含讨论一下将来 pnpm 会做的一些事件。
依赖治理
这节会通过 pnpm 在依赖治理这一块的一些不同于失常包管理工具的一些优化技巧。
hard link 机制
介绍 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 目录
上一节提到 store 目录用于存储依赖的 hard links,这一节简略介绍一下这个 store 目录。
个别 store 目录默认是设置在 ${os.homedir}/.pnpm-store
这个目录下,具体能够参考 @pnpm/store-path
这个 pnpm 子包中的代码:
const homedir = os.homedir()
if (await canLinkToSubdir(tempFile, homedir)) {await fs.unlink(tempFile)
// If the project is on the drive on which the OS home directory
// then the store is placed in the home directory
return path.join(homedir, relStore, STORE_VERSION)
}
当然用户也能够在 .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 外面删掉它了。
该命令举荐偶然进行应用,但不要频繁应用,因为可能某天这个不被援用的包又忽然被哪个我的项目援用了,这样就能够不必再去从新下载这个包了。
node\_modules 构造
在 pnpm 官网有一篇很经典的文章,对于介绍 pnpm 我的项目的 node\_modules 构造: Flat node\_modules is not the only way | pnpm。
在这篇文章中介绍了 pnpm 目前的 node\_modules 的一些文件构造,例如在我的项目中应用 pnpm 装置了一个叫做 express
的依赖,那么最初会在 node\_modules 中造成这样两个目录构造:
node_modules/express/...
node_modules/.pnpm/express@4.17.1/node_modules/xxx
其中第一个门路是 nodejs 失常寻找门路会去找的一个目录,如果去查看这个目录下的内容,会发现外面连个 node_modules
文件都没有:
▾ express
▸ lib
History.md
index.js
LICENSE
package.json
Readme.md
实际上这个文件只是个软连贯,它会造成一个到第二个目录的一个软连贯(相似于软件的快捷方式),这样 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) 的形式来实现。
symlink 和 hard link 机制
在后面晓得了 pnpm 是通过 hardlink 在全局外面搞个 store 目录来存储 node\_modules 依赖外面的 hard link 地址,而后在援用依赖的时候则是通过 symlink 去找到对应虚构磁盘目录下 (.pnpm 目录) 的依赖地址。
这两者联合在一起工作之后,如果有一个我的项目依赖了 bar@1.0.0
和 foo@1.0.0
,那么最初的 node\_modules 构造出现进去的依赖构造可能会是这样的:
node_modules
└── bar // symlink to .pnpm/bar@1.0.0/node_modules/bar
└── foo // symlink to .pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
│ ├── index.js
│ └── package.json
└── foo@1.0.0
└── node_modules
└── foo -> <store>/foo
├── index.js
└── package.json
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 探讨。
Monorepo 反对
pnpm
在 monorepo 场景能够说算得上是个完满的解决方案了,因为其自身的设计机制,导致很多要害或者说致命的问题都失去了相当无效的解决。
workspace 反对
对于 monorepo 类型的我的项目,pnpm 提供了 workspace 来反对,具体能够参考官网文档: pnpm.io/workspaces/…
痛点解决
Monorepo 下被人诟病较多的问题,个别是依赖构造问题。常见的两个问题就是 Phantom dependencies
和 NPM doppelgangers
,用 rush 官网 的图片能够很贴切的展现着两个问题:
上面会针对两个问题一一介绍。
Phantom dependencies
Phantom dependencies 被称之为幽灵依赖,解释起来很简略,即某个包没有被装置(package.json
中并没有,然而用户却可能援用到这个包)。
引发这个景象的起因个别是因为 node\_modules 构造所导致的,例如应用 yarn 对我的项目装置依赖,依赖外面有个依赖叫做 foo,foo 这个依赖同时依赖了 bar,yarn 会对装置的 node\_modules 做一个扁平化构造的解决(npm v3 之后也是这么做的),会把依赖在 node\_modules 下打平,这样相当于 foo 和 bar 呈现在同一层级上面。那么依据 nodejs 的寻径原理,用户能 require 到 foo,同样也能 require 到 bar。
package.json -> foo(bar 为 foo 依赖)
node_modules
/foo
/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”…..
NPM doppelgangers
这个问题其实也能够说是 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 的依赖构造应该是这样的:
- package
- package.json
- node_modules
- lib_a
- node_modules <- util_e@1.0.0
- lib_b
- node_modules <- util_e@1.0.0
_ lib_c
- node_modules <- util_e@2.0.0
- lib_d
- node_modules <- util_e@2.0.0
这样必然会导致很多依赖被反复装置,于是就有了 hoist 和打平依赖的操作:
- package
- package.json
- node_modules
- util_e@1.0.0
- lib_a
- lib_b
_ lib_c
- node_modules <- util_e@2.0.0
- lib_d
- node_modules <- util_e@2.0.0
然而这样也只能晋升一个依赖,如果两个依赖都晋升了会导致抵触,这样同样会导致一些不同版本的依赖被反复装置屡次,这里就会导致应用 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 无奈失常工作,不过这些库在迭代更新之后也会反对这一个性。
141. 如何组织 monorepo 工程?【工程化】
参考文档:
- pnpm + workspace + changesets 构建你的 monorepo 工程
- 古代 Monorepo 工程技术选型,聊聊我的思考
- 前端工程化之多个我的项目如何同时高效治理 — monorepo
144.[vue] 是怎么解析 template 的?【web 框架】
整体流程图:
参考文档:
- Vue 编译三部曲:如何将 template 编译成 AST ?
- Vue 编译三部曲:模型树优化
- Vue 编译三部曲:最初一曲,render code 生成
资深开发者相干问题【共计 2 道题】
116.React Diff 算法是怎么实现的?【JavaScript】
原理
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 算法的代码:
function diff(oldTree, newTree) {const patches = {};
let index = 0;
walk(oldTree, newTree, index, patches);
return patches;
}
function walk(oldNode, newNode, index, patches) {const currentPatch = [];
if (!newNode) {currentPatch.push({ type: "REMOVE"});
} else if (typeof oldNode === "string" && typeof newNode === "string") {if (oldNode !== newNode) {currentPatch.push({ type: "TEXT", content: newNode});
}
} else if (oldNode.type === newNode.type) {const attrs = diffAttrs(oldNode.props, newNode.props);
if (Object.keys(attrs).length > 0) {currentPatch.push({ type: "ATTRS", attrs});
}
diffChildren(oldNode.children, newNode.children, index, patches, currentPatch);
} else {currentPatch.push({ type: "REPLACE", newNode});
}
if (currentPatch.length > 0) {patches[index] = currentPatch;
}
}
function diffAttrs(oldAttrs, newAttrs) {const attrs = {};
for (const key in oldAttrs) {if (oldAttrs[key] !== newAttrs[key]) {attrs[key] = newAttrs[key];
}
}
for (const key in newAttrs) {if (!oldAttrs.hasOwnProperty(key)) {attrs[key] = newAttrs[key];
}
}
return attrs;
}
function diffChildren(oldChildren, newChildren, index, patches, currentPatch) {const diffs = listDiff(oldChildren, newChildren, "key");
newChildren = diffs.children;
if (diffs.moves.length > 0) {const reorderPatch = { type: "REORDER", moves: diffs.moves};
currentPatch.push(reorderPatch);
}
let lastIndex = index;
oldChildren.forEach((child, i) => {const newChild = newChildren[i];
index = lastIndex + 1;
walk(child, newChild, index, patches);
lastIndex = index;
});
}
function listDiff(oldList, newList, key) {const oldMap = makeKeyIndexAndFree(oldList, key);
const newMap = makeKeyIndexAndFree(newList, key);
const newFree = newMap.free;
const moves = [];
const children = [];
let i = 0;
let item;
let itemIndex;
let freeIndex = 0;
while (i < oldList.length) {item = oldList[i];
itemIndex = oldMap.keyIndex[item[key]];
if (itemIndex === undefined) {moves.push({ index: i, type: "REMOVE"});
} else {children.push(newList[itemIndex]);
if (itemIndex >= freeIndex) {freeIndex = itemIndex + 1;} else {moves.push({ index: itemIndex, type: "INSERT", item: item});
}
}
i++;
}
const remaining = newFree.slice(freeIndex);
remaining.forEach(item => {moves.push({ index: newList.indexOf(item), type: "INSERT", item: item });
});
return {moves, children};
}
function makeKeyIndexAndFree(list, key) {const keyIndex = {};
const free = [];
for (let i = 0; i < list.length; i++) {const item = list[i];
if (item[key] !== undefined) {keyIndex[item[key]] = i;
} else {free.push(item);
}
}
return {keyIndex, free};
}
145. 实现 JS 沙盒的形式有哪些?【工程化】
微前端曾经成为前端畛域比拟火爆的话题,在技术方面,微前端有一个始终绕不过来的话题就是前端沙箱
什么是沙箱
Sandboxie(又叫沙箱、沙盘)即是一个虚构零碎程序,容许你在沙盘环境中运行浏览器或其余程序,因而运行所产生的变动能够随后删除。它发明了一个相似沙盒的独立作业环境,在其外部运行的程序并不能对硬盘产生永久性的影响。在网络安全中,沙箱指在隔离环境中,用以测试不受信赖的文件或应用程序等行为的工具
简略来说沙箱(sandbox)就是与外界断绝的一个环境,内外环境互不影响,外界无奈批改该环境内任何信息,沙箱内的货色独自属于一个世界。
JavaScript 的沙箱
对于 JavaScript 来说,沙箱并非传统意义上的沙箱,它只是一种语法上的 Hack 写法,沙箱是一种平安机制,把一些不信赖的代码运行在沙箱之内,使其不能拜访沙箱之外的代码。当须要解析或着执行不可信的 JavaScript 的时候,须要隔离被执行代码的执行环境的时候,须要对执行代码中可拜访对象进行限度,通常开始能够把 JavaScript 中解决模块依赖关系的闭包称之为沙箱。
JavaScript 沙箱实现
咱们大抵能够把沙箱的实现总体分为两个局部:
- 构建一个闭包环境
- 模仿原生浏览器对象
构建闭包环境
咱们晓得 JavaScript 中,对于作用域(scope), 只有全局作用域(global scope)、函数作用域(function scope)以及从 ES6 开始才有的块级作用域(block scope)。如果要将一段代码中的变量、函数等的定义隔离进去,受限于 JavaScript 对作用域的管制,只能将这段代码封装到一个 Function 中,通过应用 function scope 来达到作用域隔离的目标。也因为须要这种应用函数来达到作用域隔离的目标形式,于是就有 IIFE(立刻调用函数表达式), 这是一个被称为 自执行匿名函数的设计模式
(function foo(){
var a = 1;
console.log(a);
})();
// 无奈从内部拜访变量 name
console.log(a) // 抛出谬误:"Uncaught ReferenceError: a is not defined"
当函数变成立刻执行的函数表达式时,表达式中的变量不能从内部拜访,它领有独立的词法作用域。不仅防止了外界拜访 IIFE 中的变量,而且又不会净化全局作用域,补救了 JavaScript 在 scope 方面的缺点。个别常见于写插件和类库时,如 JQuery 当中的沙箱模式
(function (window) {var jQuery = function (selector, context) {return new jQuery.fn.init(selector, context);
}
jQuery.fn = jQuery.prototype = function () {// 原型上的办法,即所有 jQuery 对象都能够共享的办法和属性}
jQuery.fn.init.prototype = jQuery.fn;
window.jQeury = window.$ = jQuery; // 如果须要在外界裸露一些属性或者办法,能够将这些属性和办法加到 window 全局对象下来
})(window);
当将 IIFE 调配给一个变量,不是存储 IIFE 自身,而是存储 IIFE 执行后返回的后果。
var result = (function () {
var name = "张三";
return name;
})();
console.log(result); // "张三"
原生浏览器对象的模仿
模仿原生浏览器对象的目标是为了,避免闭包环境,操作原生对象。篡改净化原生环境;实现模仿浏览器对象之前咱们须要先关注几个不罕用的 API。
eval
eval 函数可将字符串转换为代码执行,并返回一个或多个值
var b = eval("({name:' 张三 '})")
console.log(b.name);
因为 eval 执行的代码能够拜访闭包和全局范畴,因而就导致了代码注入的平安问题,因为代码外部能够沿着作用域链往上找,篡改全局变量,这是咱们不心愿的
new Function
Function 构造函数创立一个新的 Function 对象。间接调用这个构造函数可用动态创建函数
语法
new Function ([arg1[, arg2[, ...argN]],] functionBody)
arg1, arg2, … argN 被函数应用的参数的名称必须是非法命名的。参数名称是一个无效的 JavaScript 标识符的字符串,或者一个用逗号分隔的无效字符串的列表; 例如“×”,“theValue”,或“a,b”。
functionBody 一个含有包含函数定义的 JavaScript 语句的字符串。
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(1, 2));//3
同样也会遇到和 eval 相似的的平安问题和绝对较小的性能问题。
var a = 1;
function sandbox() {
var a = 2;
return new Function('return a;'); // 这里的 a 指向最下面全局作用域内的 1
}
var f = sandbox();
console.log(f())
与 eval 不同的是 Function 创立的函数只能在全局作用域中运行。它无法访问部分闭包变量,它们总是被创立于全局环境,因而在运行时它们只能拜访全局变量和本人的局部变量,不能拜访它们被 Function 结构器创立时所在的作用域的变量;然而,它依然能够拜访全局范畴。new Function()是 eval()更好代替计划。它具备卓越的性能和安全性,但仍没有解决拜访全局的问题。
with
with 是 JavaScript 中一个关键字, 扩大一个语句的作用域链。它容许半沙盒执行。那什么叫半沙盒?语句将某个对象增加到作用域链的顶部,如果在沙盒中有某个未应用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出 ReferenceError。
function sandbox(o) {with (o){
//a=5;
c=2;
d=3;
console.log(a,b,c,d); // 0,1,2,3 // 每个变量首先被认为是一个局部变量,如果局部变量与 obj 对象的某个属性同名,则这个局部变量会指向 obj 对象属性。}
}
var f = {
a:0,
b:1
}
sandbox(f);
console.log(f);
console.log(c,d); // 2,3 c、d 被泄露到 window 对象上
究其原理,with
在外部应用 in
运算符。对于块内的每个变量拜访,它都在沙盒条件下计算变量。如果条件是 true,它将从沙盒中检索变量。否则,就在全局范畴内查找变量。然而 with 语句使程序在查找变量值时,都是先在指定的对象中查找。所以对于那些原本不是这个对象的属性的变量,查找起来会很慢,对于有性能要求的程序不适宜(JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于可能依据代码的词法进行动态剖析,并预先确定所有变量和函数的定义地位,能力在执行过程中疾速找到标识符。)。with 也会导致数据透露(在非严格模式下,会主动在全局作用域创立一个全局变量)
in 运算符
in 运算符可能检测左侧操作数是否为右侧操作数的成员。其中,左侧操作数是一个字符串,或者能够转换为字符串的表达式,右侧操作数是一个对象或数组。
var o = {
a : 1,
b : function() {}
}
console.log("a" in o); //true
console.log("b" in o); //true
console.log("c" in o); //false
console.log("valueOf" in o); // 返回 true,继承 Object 的原型办法
console.log("constructor" in o); // 返回 true,继承 Object 的原型属性
with + new Function
配合 with 用法能够略微限度沙盒作用域,先从以后的 with 提供对象查找,然而如果查找不到仍然还能从上获取,净化或篡改全局环境。
function sandbox (src) {src = 'with (sandbox) {' + src + '}'
return new Function('sandbox', src)
}
var str = 'let a = 1;window.name=" 张三 ";console.log(a);console.log(b)'
var b = 2
sandbox(str)({});
console.log(window.name);//'张三'
基于 Proxy 实现的沙箱(ProxySandbox)
由上局部内容思考, 如果能够做到在应用 with
对于块内的每个变量拜访都限度在沙盒条件下计算变量,从沙盒中检索变量。那么是否能够完满的解决 JavaScript 沙箱机制。
应用 with 再加上 proxy 实现 JavaScript 沙箱
ES6 Proxy 用于批改某些操作的默认行为,等同于在语言层面做出批改,属于一种“元编程”(meta programming)
function sandbox(code) {code = 'with (sandbox) {' + code + '}'
const fn = new Function('sandbox', code)
return function (sandbox) {
const sandboxProxy = new Proxy(sandbox, {has(target, key) {return true}
})
return fn(sandboxProxy)
}
}
var a = 1;
var code = 'console.log(a)' // TypeError: Cannot read property 'log' of undefined
sandbox(code)({})
咱们后面提到 with
在外部应用 in
运算符来计算变量,如果条件是 true,它将从沙盒中检索变量。现实状态下没有问题,但也总有些特例独行的存在,比方 Symbol.unscopables。
Symbol.unscopables
Symbol.unscopables 对象的 Symbol.unscopables 属性,指向一个对象。该对象指定了应用 with 关键字时,哪些属性会被 with 环境排除。
Array.prototype[Symbol.unscopables]
// {
// copyWithin: true,
// entries: true,
// fill: true,
// find: true,
// findIndex: true,
// keys: true
// }
Object.keys(Array.prototype[Symbol.unscopables])
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'keys']
由此咱们的代码还须要批改如下:
function sandbox(code) {code = 'with (sandbox) {' + code + '}'
const fn = new Function('sandbox', code)
return function (sandbox) {
const sandboxProxy = new Proxy(sandbox, {has(target, key) {return true},
get(target, key) {if (key === Symbol.unscopables) return undefined
return target[key]
}
})
return fn(sandboxProxy)
}
}
var test = {
a: 1,
log(){console.log('11111')
}
}
var code = 'log();console.log(a)' // 1111,TypeError: Cannot read property 'log' of undefined
sandbox(code)(test)
Symbol.unscopables 定义对象的不可作用属性。Unscopeable 属性永远不会从 with 语句中的沙箱对象中检索,而是间接从闭包或全局范畴中检索。
快照沙箱(SnapshotSandbox)
以下是 qiankun 的 snapshotSandbox 的源码,这里为了帮忙了解做局部精简及正文。
function iter(obj, callbackFn) {for (const prop in obj) {if (obj.hasOwnProperty(prop)) {callbackFn(prop);
}
}
}
/**
* 基于 diff 形式实现的沙箱,用于不反对 Proxy 的低版本浏览器
*/
class SnapshotSandbox {constructor(name) {
this.name = name;
this.proxy = window;
this.type = 'Snapshot';
this.sandboxRunning = true;
this.windowSnapshot = {};
this.modifyPropsMap = {};
this.active();}
// 激活
active() {
// 记录以后快照
this.windowSnapshot = {};
iter(window, (prop) => {this.windowSnapshot[prop] = window[prop];
});
// 复原之前的变更
Object.keys(this.modifyPropsMap).forEach((p) => {window[p] = this.modifyPropsMap[p];
});
this.sandboxRunning = true;
}
// 还原
inactive() {this.modifyPropsMap = {};
iter(window, (prop) => {if (window[prop] !== this.windowSnapshot[prop]) {
// 记录变更,复原环境
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
});
this.sandboxRunning = false;
}
}
let sandbox = new SnapshotSandbox();
//test
((window) => {
window.name = '张三'
window.age = 18
console.log(window.name, window.age) // 张三,18
sandbox.inactive() // 还原
console.log(window.name, window.age) // undefined,undefined
sandbox.active() // 激活
console.log(window.name, window.age) // 张三,18
})(sandbox.proxy);
快照沙箱实现来说比较简单,次要用于不反对 Proxy 的低版本浏览器,原理是基于 diff
来实现的, 在子利用激活或者卸载时别离去通过快照的模式记录或还原状态来实现沙箱,snapshotSandbox 会净化全局 window。
legacySandBox
qiankun 框架 singular 模式下 proxy 沙箱实现,为了便于了解,这里做了局部代码的精简和正文。
//legacySandBox
const callableFnCacheMap = new WeakMap();
function isCallable(fn) {if (callableFnCacheMap.has(fn)) {return true;}
const naughtySafari = typeof document.all === 'function' && typeof document.all === 'undefined';
const callable = naughtySafari ? typeof fn === 'function' && typeof fn !== 'undefined' : typeof fn ===
'function';
if (callable) {callableFnCacheMap.set(fn, callable);
}
return callable;
};
function isPropConfigurable(target, prop) {const descriptor = Object.getOwnPropertyDescriptor(target, prop);
return descriptor ? descriptor.configurable : true;
}
function setWindowProp(prop, value, toDelete) {if (value === undefined && toDelete) {delete window[prop];
} else if (isPropConfigurable(window, prop) && typeof prop !== 'symbol') {
Object.defineProperty(window, prop, {
writable: true,
configurable: true
});
window[prop] = value;
}
}
function getTargetValue(target, value) {
/*
仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类。目前没有完满的检测形式,这里通过 prototype 中是否还有可枚举的拓展办法的形式来判断
@warning 这里不要随便替换成别的判断形式,因为可能触发一些 edge case(比方在 lodash.isFunction 在 iframe 上下文中可能因为调用了 top window 对象触发的平安异样)*/
if (isCallable(value) && !isBoundedFunction(value) && !isConstructable(value)) {const boundValue = Function.prototype.bind.call(value, target);
for (const key in value) {boundValue[key] = value[key];
}
if (value.hasOwnProperty('prototype') && !boundValue.hasOwnProperty('prototype')) {
Object.defineProperty(boundValue, 'prototype', {
value: value.prototype,
enumerable: false,
writable: true
});
}
return boundValue;
}
return value;
}
/**
* 基于 Proxy 实现的沙箱
*/
class SingularProxySandbox {
/** 沙箱期间新增的全局变量 */
addedPropsMapInSandbox = new Map();
/** 沙箱期间更新的全局变量 */
modifiedPropsOriginalValueMapInSandbox = new Map();
/** 继续记录更新的 (新增和批改的) 全局变量的 map,用于在任意时刻做 snapshot */
currentUpdatedPropsValueMap = new Map();
name;
proxy;
type = 'LegacyProxy';
sandboxRunning = true;
latestSetProp = null;
active() {if (!this.sandboxRunning) {this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
}
this.sandboxRunning = true;
}
inactive() {// console.log('this.modifiedPropsOriginalValueMapInSandbox', this.modifiedPropsOriginalValueMapInSandbox)
// console.log('this.addedPropsMapInSandbox', this.addedPropsMapInSandbox)
// 删除增加的属性,批改已有的属性
this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true));
this.sandboxRunning = false;
}
constructor(name) {
this.name = name;
const {
addedPropsMapInSandbox,
modifiedPropsOriginalValueMapInSandbox,
currentUpdatedPropsValueMap
} = this;
const rawWindow = window;
//Object.create(null)的形式,传入一个不含有原型链的对象
const fakeWindow = Object.create(null);
const proxy = new Proxy(fakeWindow, {set: (_, p, value) => {if (this.sandboxRunning) {if (!rawWindow.hasOwnProperty(p)) {addedPropsMapInSandbox.set(p, value);
} else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
// 如果以后 window 对象存在该属性,且 record map 中未记录过,则记录该属性初始值
const originalValue = rawWindow[p];
modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
}
currentUpdatedPropsValueMap.set(p, value);
// 必须从新设置 window 对象保障下次 get 时能拿到已更新的数据
rawWindow[p] = value;
this.latestSetProp = p;
return true;
}
// 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的状况下应该疏忽谬误
return true;
},
get(_, p) {
// 防止应用 window.window 或者 window.self 逃离沙箱环境,触发到实在环境
if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {return proxy;}
const value = rawWindow[p];
return getTargetValue(rawWindow, value);
},
has(_, p) { // 返回 boolean
return p in rawWindow;
},
getOwnPropertyDescriptor(_, p) {const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
// 如果属性不作为指标对象的本身属性存在,则不能将其设置为不可配置
if (descriptor && !descriptor.configurable) {descriptor.configurable = true;}
return descriptor;
},
});
this.proxy = proxy;
}
}
let sandbox = new SingularProxySandbox();
((window) => {
window.name = '张三';
window.age = 18;
window.sex = '男';
console.log(window.name, window.age,window.sex) // 张三,18, 男
sandbox.inactive() // 还原
console.log(window.name, window.age,window.sex) // 张三,undefined,undefined
sandbox.active() // 激活
console.log(window.name, window.age,window.sex) // 张三,18, 男
})(sandbox.proxy); //test
legacySandBox 还是会操作 window 对象,然而他通过激活沙箱时还原子利用的状态,卸载时还原主利用的状态来实现沙箱隔离的,同样会对 window 造成净化,然而性能比快照沙箱好,不必遍历 window 对象。
proxySandbox(多例沙箱)
在 qiankun 的沙箱 proxySandbox 源码外面是对 fakeWindow 这个对象进行了代理,而这个对象是通过 createFakeWindow 办法失去的,这个办法是将 window 的 document、location、top、window 等等属性拷贝一份,给到 fakeWindow。
源码展现:
function createFakeWindow(global: Window) {
// map always has the fastest performance in has check scenario
// see https://jsperf.com/array-indexof-vs-set-has/23
const propertiesWithGetter = new Map<PropertyKey, boolean>();
const fakeWindow = {} as FakeWindow;
/*
copy the non-configurable property of global to fakeWindow
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor
> A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object.
*/
Object.getOwnPropertyNames(global)
.filter((p) => {const descriptor = Object.getOwnPropertyDescriptor(global, p);
return !descriptor?.configurable;
})
.forEach((p) => {const descriptor = Object.getOwnPropertyDescriptor(global, p);
if (descriptor) {const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');
/*
make top/self/window property configurable and writable, otherwise it will cause TypeError while get trap return.
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get
> The value reported for a property must be the same as the value of the corresponding target object property if the target object property is a non-writable, non-configurable data property.
*/
if (
p === 'top' ||
p === 'parent' ||
p === 'self' ||
p === 'window' ||
(process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
) {
descriptor.configurable = true;
/*
The descriptor of window.window/window.top/window.self in Safari/FF are accessor descriptors, we need to avoid adding a data descriptor while it was
Example:
Safari/FF: Object.getOwnPropertyDescriptor(window, 'top') -> {get: function, set: undefined, enumerable: true, configurable: false}
Chrome: Object.getOwnPropertyDescriptor(window, 'top') -> {value: Window, writable: false, enumerable: true, configurable: false}
*/
if (!hasGetter) {descriptor.writable = true;}
}
if (hasGetter) propertiesWithGetter.set(p, true);
// freeze the descriptor to avoid being modified by zone.js
// see https://github.com/angular/zone.js/blob/a5fe09b0fac27ac5df1fa746042f96f05ccb6a00/lib/browser/define-property.ts#L71
rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
}
});
return {
fakeWindow,
propertiesWithGetter,
};
}
proxySandbox 因为是拷贝复制了一份 fakeWindow,不会净化全局 window,同时反对多个子利用同时加载。具体源码请查看:proxySandbox
对于 CSS 隔离
常见的有:
- CSS Module
- namespace
- Dynamic StyleSheet
- css in js
- Shadow DOM 常见的咱们这边不再赘述,这里咱们重点提一下 Shadow DO。
Shadow DOM
Shadow DOM 容许将暗藏的 DOM 树附加到惯例的 DOM 树中——它以 shadow root 节点为起始根节点,在这个根节点的下方,能够是任意元素,和一般的 DOM 元素一样。