写点什么

2023 面试官常考的前端面试题

作者:loveX001
  • 2023-02-06
    浙江
  • 本文字数:10598 字

    阅读完需:约 35 分钟

Promise.resolve

Promise.resolve = function(value) {    // 1.如果 value 参数是一个 Promise 对象,则原封不动返回该对象    if(value instanceof Promise) return value;    // 2.如果 value 参数是一个具有 then 方法的对象,则将这个对象转为 Promise 对象,并立即执行它的then方法    if(typeof value === "object" && 'then' in value) {        return new Promise((resolve, reject) => {           value.then(resolve, reject);        });    }    // 3.否则返回一个新的 Promise 对象,状态为 fulfilled    return new Promise(resolve => resolve(value));}
复制代码

Object.assign()

描述Object.assign()方法用于将所有可枚举Object.propertyIsEnumerable() 返回 true)和自有Object.hasOwnProperty() 返回 true)属性的值从一个或多个源对象复制到目标对象。它将返回修改后的目标对象(请注意这个操作是浅拷贝)。


实现


Object.assign = function(target, ...source) {    if(target == null) {        throw new TypeError('Cannot convert undefined or null to object');    }    let res = Object(target);    source.forEach(function(obj) {        if(obj != null) {            // for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)            // hasOwnProperty 方法只考虑对象自身的属性            for(let key in obj) {                if(obj.hasOwnProperty(key)) {                    res[key] = obj[key];                }            }        }    });    return res;}
复制代码

如何防御 XSS 攻击?

可以看到 XSS 危害如此之大, 那么在开发网站时就要做好防御措施,具体措施如下:


  • 可以从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回(不使用服务端渲染)。另一种是对需要插入到 HTML 中的代码做好充分的转义。对于 DOM 型的攻击,主要是前端脚本的不可靠而造成的,对于数据获取渲染和字符串拼接的时候应该对可能出现的恶意代码情况进行判断。

  • 使用 CSP ,CSP 的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击。


  1. CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。

  2. 通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式


  • 对一些敏感信息进行保护,比如 cookie 使用 http-only,使得脚本无法获取。也可以使用验证码,避免脚本伪装成用户执行一些操作。

代码输出结果

console.log('1');
setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') })})process.nextTick(function() { console.log('6');})new Promise(function(resolve) { console.log('7'); resolve();}).then(function() { console.log('8')})
setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') })})
复制代码


输出结果如下:


176824359111012
复制代码


(1)第一轮事件循环流程分析如下:


  • 整体 script 作为第一个宏任务进入主线程,遇到console.log,输出 1。

  • 遇到setTimeout,其回调函数被分发到宏任务 Event Queue 中。暂且记为setTimeout1

  • 遇到process.nextTick(),其回调函数被分发到微任务 Event Queue 中。记为process1

  • 遇到Promisenew Promise直接执行,输出 7。then被分发到微任务 Event Queue 中。记为then1

  • 又遇到了setTimeout,其回调函数被分发到宏任务 Event Queue 中,记为setTimeout2



上表是第一轮事件循环宏任务结束时各 Event Queue 的情况,此时已经输出了 1 和 7。发现了process1then1两个微任务:


  • 执行process1,输出 6。

  • 执行then1,输出 8。


第一轮事件循环正式结束,这一轮的结果是输出 1,7,6,8。


(2)第二轮时间循环从**setTimeout1**宏任务开始:


  • 首先输出 2。接下来遇到了process.nextTick(),同样将其分发到微任务 Event Queue 中,记为process2

  • new Promise立即执行输出 4,then也分发到微任务 Event Queue 中,记为then2



第二轮事件循环宏任务结束,发现有process2then2两个微任务可以执行:


  • 输出 3。

  • 输出 5。


第二轮事件循环结束,第二轮输出 2,4,3,5。


(3)第三轮事件循环开始,此时只剩 setTimeout2 了,执行。


  • 直接输出 9。

  • process.nextTick()分发到微任务 Event Queue 中。记为process3

  • 直接执行new Promise,输出 11。

  • then分发到微任务 Event Queue 中,记为then3



第三轮事件循环宏任务执行结束,执行两个微任务process3then3


  • 输出 10。

  • 输出 12。


第三轮事件循环结束,第三轮输出 9,11,10,12。


整段代码,共进行了三次事件循环,完整的输出为 1,7,6,8,2,4,3,5,9,11,10,12。

call apply bind

题目描述:手写 call apply bind 实现


实现代码如下:


Function.prototype.myCall = function (context, ...args) {  if (!context || context === null) {    context = window;  }  // 创造唯一的key值  作为我们构造的context内部方法名  let fn = Symbol();  context[fn] = this; //this指向调用call的函数  // 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了  return context[fn](...args);};
// apply原理一致 只是第二个参数是传入的数组Function.prototype.myApply = function (context, args) { if (!context || context === null) { context = window; } // 创造唯一的key值 作为我们构造的context内部方法名 let fn = Symbol(); context[fn] = this; // 执行函数并返回结果 return context[fn](...args);};
//bind实现要复杂一点 因为他考虑的情况比较多 还要涉及到参数合并(类似函数柯里化)
Function.prototype.myBind = function (context, ...args) { if (!context || context === null) { context = window; } // 创造唯一的key值 作为我们构造的context内部方法名 let fn = Symbol(); context[fn] = this; let _this = this; // bind情况要复杂一点 const result = function (...innerArgs) { // 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象 // 此时由于new操作符作用 this指向result实例对象 而result又继承自传入的_this 根据原型链知识可得出以下结论 // this.__proto__ === result.prototype //this instanceof result =>true // this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true if (this instanceof _this === true) { // 此时this指向指向result的实例 这时候不需要改变this指向 this[fn] = _this; this[fn](...[...args, ...innerArgs]); //这里使用es6的方法让bind支持参数合并 } else { // 如果只是作为普通函数调用 那就很简单了 直接改变this指向为传入的context context[fn](...[...args, ...innerArgs]); } }; // 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法 // 实现继承的方式: 使用Object.create result.prototype = Object.create(this.prototype); return result;};
//用法如下
// function Person(name, age) {// console.log(name); //'我是参数传进来的name'// console.log(age); //'我是参数传进来的age'// console.log(this); //构造函数this指向实例对象// }// // 构造函数原型的方法// Person.prototype.say = function() {// console.log(123);// }// let obj = {// objName: '我是obj传进来的name',// objAge: '我是obj传进来的age'// }// // 普通函数// function normalFun(name, age) {// console.log(name); //'我是参数传进来的name'// console.log(age); //'我是参数传进来的age'// console.log(this); //普通函数this指向绑定bind的第一个参数 也就是例子中的obj// console.log(this.objName); //'我是obj传进来的name'// console.log(this.objAge); //'我是obj传进来的age'// }
// 先测试作为构造函数调用// let bindFun = Person.myBind(obj, '我是参数传进来的name')// let a = new bindFun('我是参数传进来的age')// a.say() //123
// 再测试作为普通函数调用// let bindFun = normalFun.myBind(obj, '我是参数传进来的name')// bindFun('我是参数传进来的age')
复制代码

介绍 Loader

常用 Loader:


  • file-loader: 加载文件资源,如 字体 / 图片 等,具有移动/复制/命名等功能;

  • url-loader: 通常用于加载图片,可以将小图片直接转换为 Date Url,减少请求;

  • babel-loader: 加载 js / jsx 文件, 将 ES6 / ES7 代码转换成 ES5,抹平兼容性问题;

  • ts-loader: 加载 ts / tsx 文件,编译 TypeScript;

  • style-loader: 将 css 代码以<style>标签的形式插入到 html 中;

  • css-loader: 分析 @import 和 url(),引用 css 文件与对应的资源;

  • postcss-loader: 用于 css 的兼容性处理,具有众多功能,例如 添加前缀,单位转换 等;

  • less-loader / sass-loader: css 预处理器,在 css 中新增了许多语法,提高了开发效率;


编写原则:


  • 单一原则: 每个 Loader 只做一件事;

  • 链式调用: Webpack 会按顺序链式调用每个 Loader;

  • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;


参考 前端进阶面试题详细解答

行内元素有哪些?块级元素有哪些? 空(void)元素有那些?

  • 行内元素有:a b span img input select strong

  • 块级元素有:div ul ol li dl dt dd h1 h2 h3 h4 h5 h6 p


空元素,即没有内容的 HTML 元素。空元素是在开始标签中关闭的,也就是空元素没有闭合标签:


  • 常见的有:<br><hr><img><input><link><meta>

  • 鲜见的有:<area><base><col><colgroup><command><embed><keygen><param><source><track><wbr>

了解 this 嘛,bind,call,apply 具体指什么

它们都是函数的方法


call: Array.prototype.call(this, args1, args2]) apply: Array.prototype.apply(this, [args1, args2]) :ES6 之前用来展开数组调用, foo.appy(null, []),ES6 之后使用 ... 操作符


  • New 绑定 > 显示绑定 > 隐式绑定 > 默认绑定

  • 如果需要使用 bind 的柯里化和 apply 的数组解构,绑定到 null,尽可能使用 Object.create(null) 创建一个 DMZ 对象


四条规则:


  • 默认绑定,没有其他修饰(bind、apply、call),在非严格模式下定义指向全局对象,在严格模式下定义指向 undefined


function foo() {     console.log(this.a); }
var a = 2;foo();
复制代码


  • 隐式绑定:调用位置是否有上下文对象,或者是否被某个对象拥有或者包含,那么隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。而且,对象属性链只有上一层或者说最后一层在调用位置中起作用


function foo() {  console.log(this.a);}
var obj = { a: 2, foo: foo,}
obj.foo(); // 2
复制代码


  • 显示绑定:通过在函数上运行 call 和 apply ,来显示的绑定 this


function foo() {  console.log(this.a);}
var obj = { a: 2};
foo.call(obj);
复制代码


显示绑定之硬绑定


function foo(something) {  console.log(this.a, something);
return this.a + something;}
function bind(fn, obj) { return function() { return fn.apply(obj, arguments); };}
var obj = { a: 2}
var bar = bind(foo, obj);
复制代码


New 绑定,new 调用函数会创建一个全新的对象,并将这个对象绑定到函数调用的 this。


  • New 绑定时,如果是 new 一个硬绑定函数,那么会用 new 新建的对象替换这个硬绑定 this,


function foo(a) {  this.a = a;}
var bar = new foo(2);console.log(bar.a)
复制代码

深/浅拷贝

首先判断数据类型是否为对象,如果是对象(数组|对象),则递归(深/浅拷贝),否则直接拷贝。


function isObject(obj) {    return typeof obj === "object" && obj !== null;}
复制代码


这个函数只能判断 obj 是否是对象,无法判断其具体是数组还是对象。

代码输出结果

var a = 10var obj = {  a: 20,  say: () => {    console.log(this.a)  }}obj.say() 
var anotherObj = { a: 30 } obj.say.apply(anotherObj)
复制代码


输出结果:10 10


我么知道,箭头函数时不绑定 this 的,它的 this 来自原其父级所处的上下文,所以首先会打印全局中的 a 的值 10。后面虽然让 say 方法指向了另外一个对象,但是仍不能改变箭头函数的特性,它的 this 仍然是指向全局的,所以依旧会输出 10。


但是,如果是普通函数,那么就会有完全不一样的结果:


var a = 10  var obj = {    a: 20,    say(){    console.log(this.a)    }  }  obj.say()   var anotherObj={a:30}   obj.say.apply(anotherObj)
复制代码


输出结果:20 30


这时,say 方法中的 this 就会指向他所在的对象,输出其中的 a 的值。

说一下 vue3.0 你了解多少?

 <!-- 响应式原理的改变 Vue3.x 使用Proxy取代 Vue2.x 版本的Object.defineProperty --> <!-- 组件选项声明方式Vue3.x 使用Composition API setup 是Vue3.x新增的一个选项,他    是组件内使用Composition API 的入口 --> <!-- 模板语法变化slot具名插槽语法 自定义指令 v-model 升级 --> <!-- 其它方面的更改Suspense支持Fragment(多个根节点) 和Protal (在dom其他部分渲染组建内容)组件     针对一些特殊的场景做了处理。基于treeshaking优化,提供了更多的内置功能。 -->
复制代码

组件之间的传值有几种方式

1、父传子2、子传父3、eventbus4、ref/$refs5、$parent/$children6、$attrs/$listeners7、依赖注入(provide/inject)
复制代码

说一下前端登录的流程?

初次登录的时候,前端调后调的登录接口,发送用户名和密码,后端收到请求,验证用户名和密码,验证成功,就给前端返回一个 token,和一个用户信息的值,前端拿到 token,将 token 储存到 Vuex 中,然后从 Vuex 中把 token 的值存入浏览器 Cookies 中。把用户信息存到 Vuex 然后再存储到 LocalStroage 中,然后跳转到下一个页面,根据后端接口的要求,只要不登录就不能访问的页面需要在前端每次跳转页面师判断 Cookies 中是否有 token,没有就跳转到登录页,有就跳转到相应的页面,我们应该再每次发送 post/get 请求的时候应该加入 token,常用方法再项目 utils/service.js 中添加全局拦截器,将 token 的值放入请求头中 后端判断请求头中有无 token,有 token,就拿到 token 并验证 token 是否过期,在这里过期会返回无效的 token 然后有个跳回登录页面重新登录并且清除本地用户的信息

instanceof

题目描述:手写 instanceof 操作符实现


实现代码如下:


function myInstanceof(left, right) {  while (true) {    if (left === null) {      return false;    }    if (left.__proto__ === right.prototype) {      return true;    }    left = left.__proto__;  }}
复制代码

实现 LazyMan

题目描述:


实现一个LazyMan,可以按照以下方式调用:LazyMan(“Hank”)输出:Hi! This is Hank!
LazyMan(“Hank”).sleep(10).eat(“dinner”)输出Hi! This is Hank!//等待10秒..Wake up after 10Eat dinner~
LazyMan(“Hank”).eat(“dinner”).eat(“supper”)输出Hi This is Hank!Eat dinner~Eat supper~
LazyMan(“Hank”).eat(“supper”).sleepFirst(5)输出//等待5秒Wake up after 5Hi This is Hank!Eat supper
复制代码


实现代码如下:


class _LazyMan {  constructor(name) {    this.tasks = [];    const task = () => {      console.log(`Hi! This is ${name}`);      this.next();    };    this.tasks.push(task);    setTimeout(() => {      // 把 this.next() 放到调用栈清空之后执行      this.next();    }, 0);  }  next() {    const task = this.tasks.shift(); // 取第一个任务执行    task && task();  }  sleep(time) {    this._sleepWrapper(time, false);    return this; // 链式调用  }  sleepFirst(time) {    this._sleepWrapper(time, true);    return this;  }  _sleepWrapper(time, first) {    const task = () => {      setTimeout(() => {        console.log(`Wake up after ${time}`);        this.next();      }, time * 1000);    };    if (first) {      this.tasks.unshift(task); // 放到任务队列顶部    } else {      this.tasks.push(task); // 放到任务队列尾部    }  }  eat(name) {    const task = () => {      console.log(`Eat ${name}`);      this.next();    };    this.tasks.push(task);    return this;  }}function LazyMan(name) {  return new _LazyMan(name);}
复制代码

instance 如何使用

左边可以是任意值,右边只能是函数


'hello tuture' instanceof String // false
复制代码

一个 tcp 连接能发几个 http 请求?

如果是 HTTP 1.0 版本协议,一般情况下,不支持长连接,因此在每次请求发送完毕之后,TCP 连接即会断开,因此一个 TCP 发送一个 HTTP 请求,但是有一种情况可以将一条 TCP 连接保持在活跃状态,那就是通过 Connection 和 Keep-Alive 首部,在请求头带上 Connection: Keep-Alive,并且可以通过 Keep-Alive 通用首部中指定的,用逗号分隔的选项调节 keep-alive 的行为,如果客户端和服务端都支持,那么其实也可以发送多条,不过此方式也有限制,可以关注《HTTP 权威指南》4.5.5 节对于 Keep-Alive 连接的限制和规则。


而如果是 HTTP 1.1 版本协议,支持了长连接,因此只要 TCP 连接不断开,便可以一直发送 HTTP 请求,持续不断,没有上限; 同样,如果是 HTTP 2.0 版本协议,支持多用复用,一个 TCP 连接是可以并发多个 HTTP 请求的,同样也是支持长连接,因此只要不断开 TCP 的连接,HTTP 请求数也是可以没有上限地持续发送

代码输出结果

setTimeout(function () {  console.log(1);}, 100);
new Promise(function (resolve) { console.log(2); resolve(); console.log(3);}).then(function () { console.log(4); new Promise((resove, reject) => { console.log(5); setTimeout(() => { console.log(6); }, 10); })});console.log(7);console.log(8);
复制代码


输出结果为:


23784561
复制代码


代码执行过程如下:


  1. 首先遇到定时器,将其加入到宏任务队列;

  2. 遇到 Promise,首先执行里面的同步代码,打印出 2,遇到 resolve,将其加入到微任务队列,执行后面同步代码,打印出 3;

  3. 继续执行 script 中的代码,打印出 7 和 8,至此第一轮代码执行完成;

  4. 执行微任务队列中的代码,首先打印出 4,如遇到 Promise,执行其中的同步代码,打印出 5,遇到定时器,将其加入到宏任务队列中,此时宏任务队列中有两个定时器;

  5. 执行宏任务队列中的代码,这里我们需要注意是的第一个定时器的时间为 100ms,第二个定时器的时间为 10ms,所以先执行第二个定时器,打印出 6;

  6. 此时微任务队列为空,继续执行宏任务队列,打印出 1。


做完这道题目,我们就需要格外注意,每个定时器的时间,并不是所有定时器的时间都为 0 哦。

代码输出问题

function Parent() {    this.a = 1;    this.b = [1, 2, this.a];    this.c = { demo: 5 };    this.show = function () {        console.log(this.a , this.b , this.c.demo );    }}
function Child() { this.a = 2; this.change = function () { this.b.push(this.a); this.a = this.b.length; this.c.demo = this.a++; }}
Child.prototype = new Parent();var parent = new Parent();var child1 = new Child();var child2 = new Child();child1.a = 11;child2.a = 12;parent.show();child1.show();child2.show();child1.change();child2.change();parent.show();child1.show();child2.show();
复制代码


输出结果:


parent.show(); // 1  [1,2,1] 5
child1.show(); // 11 [1,2,1] 5child2.show(); // 12 [1,2,1] 5
parent.show(); // 1 [1,2,1] 5
child1.show(); // 5 [1,2,1,11,12] 5
child2.show(); // 6 [1,2,1,11,12] 5
复制代码


这道题目值得神帝,他涉及到的知识点很多,例如 this 的指向、原型、原型链、类的继承、数据类型等。


解析:


  1. parent.show(),可以直接获得所需的值,没啥好说的;

  2. child1.show(),Child的构造函数原本是指向Child的,题目显式将Child类的原型对象指向了Parent类的一个实例,需要注意Child.prototype指向的是Parent的实例parent,而不是指向Parent这个类。

  3. child2.show(),这个也没啥好说的;

  4. parent.show(),parent是一个Parent类的实例,Child.prorotype指向的是Parent类的另一个实例,两者在堆内存中互不影响,所以上述操作不影响parent实例,所以输出结果不变;

  5. child1.show(),child1执行了change()方法后,发生了怎样的变化呢?


  • this.b.push(this.a),由于 this 的动态指向特性,this.b 会指向Child.prototype上的 b 数组,this.a 会指向child1a 属性,所以Child.prototype.b变成了**[1,2,1,11]**;

  • this.a = this.b.length,这条语句中this.athis.b的指向与上一句一致,故结果为child1.a变为 4;

  • this.c.demo = this.a++,由于child1自身属性并没有 c 这个属性,所以此处的this.c会指向Child.prototype.cthis.a值为 4,为原始类型,故赋值操作时会直接赋值,Child.prototype.c.demo的结果为 4,而this.a随后自增为 5(4 + 1 = 5)。


  1. child2执行了change()方法, 而child2child1均是Child类的实例,所以他们的原型链指向同一个原型对象Child.prototype,也就是同一个parent实例,所以child2.change()中所有影响到原型对象的语句都会影响child1的最终输出结果。


  • this.b.push(this.a),由于 this 的动态指向特性,this.b 会指向Child.prototype上的 b 数组,this.a 会指向child2a 属性,所以Child.prototype.b变成了**[1,2,1,11,12]**;

  • this.a = this.b.length,这条语句中this.athis.b的指向与上一句一致,故结果为child2.a变为 5;

  • this.c.demo = this.a++,由于child2自身属性并没有 c 这个属性,所以此处的this.c会指向Child.prototype.c,故执行结果为Child.prototype.c.demo的值变为child2.a的值 5,而child2.a最终自增为 6(5 + 1 = 6)。

进程与线程的概念

从本质上说,进程和线程都是 CPU 工作时间片的一个描述:


  • 进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。

  • 线程是进程中的更小单位,描述了执行一段指令所需的时间。


进程是资源分配的最小单位,线程是 CPU 调度的最小单位。


一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程进程是运行在虚拟内存上的,虚拟内存是用来解决用户对硬件资源的无限需求和有限的硬件资源之间的矛盾的。从操作系统角度来看,虚拟内存即交换文件;从处理器角度看,虚拟内存即虚拟地址空间。


如果程序很多时,内存可能会不够,操作系统为每个进程提供一套独立的虚拟地址空间,从而使得同一块物理内存在不同的进程中可以对应到不同或相同的虚拟地址,变相的增加了程序可以使用的内存。


进程和线程之间的关系有以下四个特点:


(1)进程中的任意一线程执行出错,都会导致整个进程的崩溃。


(2)线程之间共享进程中的数据。


(3)当一个进程关闭之后,操作系统会回收进程所占用的内存, 当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。


(4)进程之间的内容相互隔离。 进程隔离就是为了使操作系统中的进程互不干扰,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入数据到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信的机制了。


Chrome 浏览器的架构图: 从图中可以看出,最新的 Chrome 浏览器包括:


  • 1 个浏览器主进程

  • 1 个 GPU 进程

  • 1 个网络进程

  • 多个渲染进程

  • 多个插件进程


这些进程的功能:


  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU 进程:其实, GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

  • 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。


所以,打开一个网页,最少需要四个进程:1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程。如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。


虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:


  • 更高的资源占用:因为每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),这就意味着浏览器会消耗更多的内存资源。

  • 更复杂的体系架构:浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
2023面试官常考的前端面试题_JavaScript_loveX001_InfoQ写作社区