最新前端面试知识点总结-2023(3w+字,长篇幅)

2023-前端面试知识点总结

写在最前面,坐标成都,3月16号上家公司离职,目前已经入职新公司。总的来说现阶段前端岗位的招聘比不上以往金三银四那么活跃,零零碎碎的面试机会,加上行内人的内卷行为,我***。
但是积极面试和准备面试的过程中,可以捡起或是加深印象对于以往工作开发中遗忘的前端基础。下面是我根据个人面试过程中整理的一些被问及的前端知识点,记录分享,祝各位早日入坑!
基本是自己键盘敲出来的文字内容,可能存在错别字,欢迎指出错误知识点。

面试题总览

vue2与vue3的区别
重绘重排(回流)
防抖节流
双向绑定原理
react样式实现作用域
组件传参方式
websocket三次握手
http三次握手、浏览器输入一个url到页面显示的过程描述
webpack loader作用
webpack深入(优化、使用)
new 一个对象的过程
js的优化、css的优化、项目界面优化
前端单元模块测试经历
es6与es7 es8
es6的新增特性
this指向分类深入了解
vue2与vue3的响应式原理(es5的defineProperty与es6的proxy的区别对比)
vuex实现数据持久的方法
文件上传组件请求头判断文件类型
垃圾回收机制

async 中使用awiat 与.then的区别-- 阻塞
css 动画样式实现平移 旋转 缩放等
css 阴影 参数讲解
webpack loader使用 基本配置 
vite与webpack
vue2 vue3的区别
typescript 类型(对象key字符串,值number的表示方式)
interface type的区别
typescript 类型有哪些、泛型
map数据格式
函数闭包、内存溢出原因
垃圾回收机制
var let const 区别与实际应用
js作用域
promise
vue组件父子传参
vite冷启动
commonjs 与 esmodule

浏览器http1.0 的缓存
promise原理
执行上下文
link与@import css的区别
js浮点数丢失精度原因与解决方案


flex布局属性
css实现三角形,扇形
线程与进程
http缓存
自适应布局
cdn cdn解决更新缓存问题
递归函数的作用域是否一样? 不一样 因为局部执行上下文是在执行function时才会被创建,每次执行方法时都会创建一个局部执行上下文(执行fun时有一个入栈操作,执行完成后有一个出栈操作,在栈底部时全局执行上下文,这也是为什么基础数据存在栈上,因为栈需要维护js运行时的执行上下文切换)
递归栈溢出解决方案---尾递归
尾调用优化原理
微任务是哪些宏任务是哪些 事件队列
设计模式(观察者模式、发布订阅模式)
target currenttarget区别
点击事件发生的事件过程(捕获,冒泡)
链表
vue的源码(响应式、模板渲染)
js是单线程,怎么执行异步任务的
vue组件中定义一个变量a = 1在页面显示,使用for循环去改变100次变量的值,页面会渲染多少次
执行js同步代码时遇到一个dom的样式修改,浏览器会怎么操作,直接渲染界面还是
vue定义全局组件的方法

javascript相关

一、js 代码的常用优化手段

1、使用事件委托(事件代理):减少事件注册次数和内存占用,提高性能。
2、避免使用全局变量:在 JavaScript 中,访问全局变量会非常慢,因为如果在一个作用域中找不到变量,就会一直向上查找到全局范围。
3、减少 DOM 操作:DOM 操作非常消耗 CPU 和内存,所以最好只有必要时才操作 DOM。
4、对象缓存:如果你需要重复创建同一个对象,那么最好将其缓存下来,避免频繁创建销毁对象带来的性能损耗。
5、使用节流和防抖技术:节流和防抖可以有效降低函数执行的频率。
6、减小作用域链长度:作用域链越长,变量的查找和访问时间也就越长。
7、避免重复计算:例如一些常量、计算结果等,应该缓存起来避免反复计算。
8、function 尾调用优化(例如尾递归)
关于尾递归:
1、只有不再用到外层函数的内部变量(不形成闭包),内层函数的调用帧才会取代外层函数的调用帧
2、在函数内部,尾调用是最后一条语句
3、尾调用的结果作为函数值返回
满足以上三点要求,就会被js引擎自动优化,否则无法识别尾调用优化

尾调用一直存在,尾调用优化是在支持es6的js引擎(如v8)里添加的,尾递归优化只在严格模式下生效。也可以在 es6 module 中使用,因为 es6 module 默认是遵循严格模式的

尾递归优化-解决栈溢出问题:

// 非尾调用 (计算n的阶乘,最多需要保存n个调用记录,复杂度O(n))
function factorial(n) {
    
    
  if (n === 1) return 1;
  return n * factorial(n - 1);
}
// 尾调用 (只保留一个调用记录,复杂度O(1))
function factorial(n, total) {
    
    
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

二、es5 构造函数与继承

// 构造函数继承
// 1.能给父类传参 2.每个新实例引入的构造函数属性是私有的 3.不能继承父类原型上的东西,即不能共用一些公共方法和属性
function A(name) {
    
    
  this.name = name;
  this.age = 1;
  this.say = function () {
    
    
    console.log(this.name, this.age);
  };
}
A.prototype.sayOk = function () {
    
    
  console.log(this.name, "ok");
};
function B(name) {
    
    
  A.call(this, name); // *****
  this.age = 2;
  // this.say = function () {
    
    
  //     console.log(this.name, this.age)
  // }
}

const b = new B("nb");
console.log(b.say());
console.log(b.sayOk()); // 不能继承原型上的属性方法

// 原型继承
// 1.能够继承父类原型上的方法和属性 2.父类不能入参 3.所有子类实例会共享父类的引用类属性
function AA(name) {
    
    
  this.name = name || "AA";
  this.age = 1;
  this.say = function () {
    
    
    console.log(this.name, this.age);
  };
}
AA.prototype.sayOk = function () {
    
    
  console.log(this.name, "ok");
};

function BB() {
    
    
  this.age = 2;
}
BB.prototype = new AA(); //**** child的原型指向parent的实例
BB.prototype.constructor = BB; // 纠正child原型上的构造函数
const bb = new BB("nbb");
bb.say();

// 组合继承(原型加借用构造函数)
// 1.能继承父类的实例属性与方法以及父类原型上的属性与方法 2.父类能入参 3.子类实例引入的构造函数属性是私有的 4.调用两次父类构造函数()
function Parent(name) {
    
    
  this.name = name;
  this.arr = [1];
  this.age = 1;
  this.getAge = function () {
    
    
    console.log(this.age);
  };
}
Parent.prototype.sayOk = function () {
    
    
  console.log(this.name, "say ok");
};

function Child(name) {
    
    
  Parent.call(this, name);
  this.age = 2;
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

Child.prototype.sayWhy = function () {
    
    
  console.log(this.name + " say why");
};

let c1 = new Child("c1");
console.log(c1.__proto__.constructor === Child);

// 原型式继承
// 在已有一个对象的基础上,再创建一个新对象
let obj = {
    
    
  name: "已存在的对象",
  age: "100岁",
  friend: [1, 2, 3],
};
// 本质是一次浅拷贝
//
function object(o) {
    
    
  // 创建一个临时的构造函数
  function F() {
    
    }
  F.prototype = o;
  return new F();
}
// 与原型继承相似 子类实力会共享父类的引用类属性 ;不需要单独创建构造函数
const o1 = object(obj);
o1.friend.push(6);
o1.getNum = function () {
    
    
  console.log(this.friend);
};
const o2 = object(obj);
o2.getNum = function () {
    
    
  console.log(this.friend);
};
o1.getNum();
o2.getNum();

// 寄生式继承
// 在原型式的继承上加一个function包裹,扩展新对象的效果和方法
// 不需要单独创建函数 ;通过该方式给对象扩展 难以复用。
function createAnother(o) {
    
    
  const _o = object(o);
  _o.vue = "ok";
  _o.useVue = function () {
    
    
    console.log("vue is " + this.vue);
  };
  return _o;
}

const o3 = createAnother(obj);
o3.useVue();

// 寄生组合式继承
// 借用父类构造函数、把子类的原型指向通过寄生式继承父类原型的对象
function object(o) {
    
    
  // 创建一个临时的构造函数
  function F() {
    
    }
  F.prototype = o;
  return new F();
}
function createAnother(o) {
    
    
  const _o = object(o);
  return _o;
}
function People(name) {
    
    
  this.name = name;
  this.age = 100;
  this.eat = function () {
    
    
    console.log(this.name + "eat....");
  };
}
People.prototype.bloodColoe = "red";
People.prototype.speek = function (s) {
    
    
  console.log(this.name + "说:" + s);
};

function Man(name) {
    
    
  People.call(this, name);
  this.sex = "男";
}

Man.prototype = createAnother(People.prototype);
// Man.prototype = Object.create(People.prototype)
Man.prototype.constructor = Man;

// new 的过程

//  1.创建一个空对象  2.将空对象的__proto__属性指向构造函数的原型对象 3.将构造函数中的this指向创建的空对象(call apply)并执行构造函数 4.如果构造函数没有返回对象(不包括null)则返回obj(this)
function T() {
    
    
  this.a = 1;
  this.b = 2;
  return {
    
    };
}
const t = new T();
console.log(t);

三、new 一个对象的过程

一、
1、创建一个空的简单 JavaScript 对象(即 {});
2、为步骤 1 新创建的对象添加属性 proto,将该属性链接至构造函数的原型对象;
3、将步骤 1 新创建的对象作为 this 的上下文;
4、如果该函数没有返回对象,则返回 this。
二、
将目标对象(Target)的类型设置为函数(构造函数)
创建一个新的空对象,将其绑定到 Target.prototype 上。
执行 Target 函数,并将 this 设置为新创建的对象。也就是说,这个函数可以在调用过程中使用 this 引用新对象。
如果返回值不为空,则使用该值作为构造函数的结果。否则,返回新创建的对象

四、防抖与节流

防抖和节流是处理 JavaScript 中频繁触发的函数的两种方法,以优化页面性能。

防抖(debounce):
当一个事件被频繁触发时,只有等到一定的时间间隔内没有再次触发该事件,才会真正去执行该事件
实现原理:
使用定时器和闭包来进行实现。每次触发事件都会先清除定时器,只有事件停止触发的时候才会执行定时器中的回调函数

function debounce(fn, delay) {
    
    
  let timer = null;
  return function () {
    
    
    if (timer) clearTimeout(timer);
    const self = this;
    const args = arguments;
    timer = setTimeout(function () {
    
    
      fn.apply(self, args);
    }, delay);
  };
}

节流(throttle):
在一定时间间隔内只会执行一次函数,相当于设置执行的最小时间间隔。
实现原理:
记录上次执行的时间戳 lastTime,在接下来的某个时刻 timestamps 内,如果客户端重复触发事件,则只返回上次调用的结果;只有当时间大于 timestamps 时,才重新计算并返回新的值。

// 1
function throttle(fn, delay) {
    
    
  let lastTime = 0;
  return function () {
    
    
    const currentTime = +new Date();
    const self = this;
    const args = arguments;
    if (currentTime - lastTime > delay) {
    
    
      fn.apply(self, args);
      lastTime = currentTime;
    }
  };
}

// 2
function throttle(fn, delay) {
    
    
  let timer = null;
  return function () {
    
    
    const self = this;
    const args = arguments;
    if (!timer) {
    
    
      timer = setTimeout(() => {
    
    
        fn.apply(self, args);
        clearTimeout(timer);
      }, delay);
    }
  };
}

五、promise/A+规范概述

promise 有三个状态:pending,fulfilled,or rejected;「规范 Promise/A+ 2.1」
new promise 时, 需要传递一个 executor()执行器,执行器立即执行;
executor 接受两个参数,分别是 resolve 和 reject;
promise 的默认状态是 pending;
promise 有一个 value 保存成功状态的值,可以是 undefined/thenable/promise;「规范 Promise/A+ 1.3」
promise 有一个 reason 保存失败状态的值;「规范 Promise/A+ 1.5」
promise 只能从 pending 到 rejected, 或者从 pending 到 fulfilled,状态一旦确认,就不会再改变;
promise 必须有一个 then 方法,then 接收两个参数,分别是 promise 成功的回调 onFulfilled, 和 promise 失败的回调 onRejected;「规范 Promise/A+ 2.2」
如果调用 then 时,promise 已经成功,则执行 onFulfilled,参数是 promise 的 value;
如果调用 then 时,promise 已经失败,那么执行 onRejected, 参数是 promise 的 reason;
如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调 onRejected;
then 的参数 onFulfilled 和 onRejected 可以缺省,如果 onFulfilled 或者 onRejected 不是函数,将其忽略,且依旧可以在下面的 then 中获取到之前返回的值;「规范 Promise/A+ 2.2.1、2.2.1.1、2.2.1.2」
promise 可以 then 多次,每次执行完 promise.then 方法后返回的都是一个“新的 promise";「规范 Promise/A+ 2.2.7」
如果 then 的返回值 x 是一个普通值,那么就会把这个结果作为参数,传递给下一个 then 的成功的回调中;
如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调中;「规范 Promise/A+ 2.2.7.2」
如果 then 的返回值 x 是一个 promise,那么会等这个 promise 执行完,promise 如果成功,就走下一个 then 的成功;如果失败,就走下一个 then 的失败;如果抛出异常,就走下一个 then 的失败;「规范 Promise/A+ 2.2.7.3、2.2.7.4」
如果 then 的返回值 x 和 promise 是同一个引用对象,造成循环引用,则抛出异常,把异常传递给下一个 then 的失败的回调中;「规范 Promise/A+ 2.3.1」
如果 then 的返回值 x 是一个 promise,且 x 同时调用 resolve 函数和 reject 函数,则第一次调用优先,其他所有调用被忽略;「规范 Promise/A+ 2.3.3.3.3」

promise.then()里面如果返回一个非 promise 的值,会在下一个.then()中获取该值;如果返回的是一个 promise,会根据这个 promise 的解析值来判断,如果是 resolve 则会在下一个.then()中获取 resolve 的值,如果是一个 reject 则会在下一个 catch 中获取值,如果没有执行任何操作则不会触发后续的.then 或.catch 操作。promise.resolve() 返回一个成功解析的 promise 对象,promise.reject()返回一个错误解析的 promise 对象

六、实现一个柯里函数封装

函数柯里化,其实就是把多次调用的变量保存在闭包中,每次调用都查看一下变量数和原函数的形参数量是否相等。不相等就继续递归。直到相等为止就处理了

* function的length属性表示该方法的形参个数

function curry(fn) {
    
    
  return function curryFn() {
    
    
    // 1. 一次性将所有参数传完
    // arguments
    if (arguments.length < fn.length) {
    
    
      // 2. 没有一次性传完
      var _arg = Array.from(arguments);
      return function () {
    
    
        // 当这个匿名函数被调用时
        // 看这一传递进来的参数 + 上一次的参数 === fn.length
        return curryFn(...Array.from(arguments).concat(_arg));
      };
    }
    return fn(...arguments);
  };
}

// es6 rest参数 替代arguments(类数组)获取剩余参数,并且本身就是数组类型。箭头函数替代函数声明式或函数表达式、匿名函数等
const _curry = (fn) => {
    
    
    const myCurry = (...args) =>
        fn.length === args.length ?
            fn(...args) :
            (...arg) => myCurry(...args, ...arg)
    return myCurry
}
const _myCurry = _curry(add)

七、事件队列

1.执行全局Script同步代码,形成一个执行栈;

2.在执行代码时当遇到如上 异步任务时便会按上文所描述的将宏任务回调加入宏任务队列,微任务回调加入微任务队列;

3.然而,回调函数放入任务队列后也不是立即执行;会等待执行栈中的同步任务全部执行完清空了栈后引擎才能会去任务队列检查是否有任务,如果有那便会将这些任务加入执行栈,然后执行!

4.执行栈清空后,会先去检查微任务队列是否有任务,逐一将其任务加入执行栈中执行,期间如果又产生了微任务那继续将其加入到微任务队列末尾,并在本周期内执行完,直到微任务队列的任务全部 清空,执行栈也清空后,再去检查宏任务队列是否有任务,取到队列队头的任务放入到执行栈中执行,其他可能又会产生微任务,那当本次执行栈中的任务结果清空后又会去检查微任务队列…

5.引擎会循环执行如上步骤,这就是Event Loop

八、微任务是哪些宏任务是哪些

微任务:postMessage、MutationObserve、promise.then、process.nextTick
红任务:setTimeout、setInterval、setImmediate、I/O

九、执行js代码时,同步任务、微任务、宏任务、dom渲染的执行顺序

1、同步任务 2、微任务 3、dom渲染 4、宏任务

十、js是单线程,怎么实现异步任务

javascript虽然是单线程,但是js运行的依托浏览器是多线程,提供了定时触发器线程、事件触发线程等来实现js的异步逻辑

十一、笔试-事件队列

// 题一
const p1 = new Promise((resolve, reject) => {
    
    
    console.log('p1')
    setTimeout(() => {
    
    
        resolve('success p1')
    }, 0)
})
const p2 = p1.then(() => {
    
    
    console.log('p1-p2')
    return 818
})
const p3 = p2.then(res => {
    
    
    console.log('p3', res)
    // return Promise.resolve('p3 Promise')
    return Promise.resolve(new Promise((resolve, reject) => {
    
    
        resolve('test')
    }))
})
const p4 = p3.then(res => {
    
    
    console.log('p4:', res)
})
setTimeout(() => {
    
    
    console.log('setTimeout 10')
}, 10)
setTimeout(() => {
    
    
    console.log('setTimeout 0')
}, 0)
console.log('just test')
/* 
p1
just test
p1-p2
p3 818
p4: p3 Promise
setTimeout 0
setTimeout 10


*/

// 题二
const myPromise = Promise.resolve('42');
myPromise.then(res => {
    
    
    console.log(res)
    // return Promise.resolve('12').then(res => {
    
    
    //     return Promise.resolve('what')
    // })
    // return new Promise((res, rej) => {})
    return Promise.reject('what')
}).then(res => {
    
    
    console.log(res)
}).then(res => {
    
    
    console.log('1')
}).then(res => {
    
    
    console.log('2')
}).then(res => {
    
    
    console.log('3')
}).then(res => {
    
    
    throw new Error('hhh')
    console.log('4')
}).then(res => {
    
    
    console.log('5')
}, err => {
    
    
    console.log('errr 5')
}).catch(err => {
    
    
    console.log('errrrr')
})

十二、笔试-文件上传通过头文件判断文件类型

通过input标签,type=file,实现上传文件功能,判断文件类型

const allowedType = {
    
    
    'FFD8FFE0': 'jpg',
    '89504E47': 'png',
    '47494638': 'gif',
    "52494646": 'webp'
}
// 通过头文件 判断上传文件格式
function fileType(file) {
    
    
    const fileReader = new FileReader()
    fileReader.readAsArrayBuffer(file)
    return new Promise((resolve, reject) => {
    
    
        fileReader.onload = (e) => {
    
    
            try {
    
    
                let uint8Array = new Uint8Array(e.target.result).slice(0, 4)
                // let key = [...uint8Array].map(s => s.toString(16).toUpperCase().padStart(2, '0')).join('')
                let key = Array.prototype.map.call(uint8Array, s => s.toString(16).toUpperCase().padStart(2, '0')).join('')
                console.log(key, allowedType[key])
                resolve(typeof allowedType[key] === 'string')
            } catch (error) {
    
    
                reject(error)
            }
        }
    })
}

function clickBtn () {
    
    
    const fileInput = document.querySelector('#file')
    console.log(fileInput.files)
    fileType(fileInput.files[0]).then(res => {
    
    
        console.log(res)
    })
}

css相关

一、css的常用优化手段

避免使用 @import:
原因: @import引用外部css会增加页面的请求次数和 css 文件的数量,从而导致页面加载速度变慢。
解决方法: 使用 <link> 标签在 <head> 中引入 CSS。
压缩 CSS 文件:
原因:删除不必要的代码、注释、空格等非必要字符可以减少文件大小,提高文件下载速度。
解决方法: 使用 CSS 压缩工具(例如:UglifyCSS、CSSNano 等)。
减少使用 Float
原因: float 的元素比较容易导致根元素高度塌陷,从而影响其他内容的排布,直接影响页面性能。
解决方法: 使用 Flexbox 或 Grid Layout 替代 Float 布局。
使用 CSS Sprites
原因: 减少了 HTTP 请求,使网页加载更快。
解决方法: 将几个小图片合并成为一张大图,并使用 background-position 属性来显示所需的图像区域

二、link与@import的区别

<link>是 HTML 标签,而@import是 CSS 提供的一种方式;
<link>在页面载入时同时加载,而@import要等价CSS文件完全下载完才会加载;
<link>支持使用 JS 操作 DOM 动态加载和改变样式,而@import不支持

三、响应式页面设计

原生的html元素在没有编写任何css时,本身就是响应式的。响应式主要针对固定宽度或者影响布局的元素进行调整

首先需要启用meta标签 name=“viewport” content属性值设置为width=device-width,initial-scale=1.0。因为在移动端页面默认是对pc端页面进行缩放来展示内容的,用户体验不友好需要放大查看内容,设置device-width后网页的可视区域就会取移动设备屏幕实际的宽度,设置initial-scale=1.0之后就会使用正常的缩放比例,即1:1不缩放

一、容器宽度的响应式
1.使用百分比宽度,使其根据屏幕可视区域自动调整宽度尺寸
2.如果必须使用固定的宽度可以结合@media媒体查询来查询可视区域的宽度,然后根据不同的可视区域宽度设置容器宽度

二、布局的响应式(横向)
例如使用了flex布局
可以使用flex-wrap与flex属性来让flex子元素保持最小宽度并自动折行

如果使用的是grid布局,默认是竖向排列的没有问题,但是如果是多列布局,设置了grid-template-columns: 1fr 1fr 1fr
并且每一列都是浮动宽度也会出现像flex布局那样的情况
解决方式1:
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr))
当容器最后的宽度不足以容纳列的最小宽度时,折行显示
解决方式2:
使用@media根据可视宽度手动控制列数

三、图片的响应式
1.pc端使用百分比宽度设置,但是在移动端时,任然会有显示问题。
移动端时解决方案:
1.结合使用img元素的属性srcset和sizes实现响应式,sizes时通过@media 查询可视区域宽度,当满足条件时通过第二个值来指定图片的宽度,srcset根据第二个参数指定的宽度,来使用第一个参数指定的图片。浏览器会自动根据像素的密度和屏幕的宽度来加载图片

<img src="xx.png" srcset="
    xxx.png 1240w,
    xxx1.png 600w,
    xxx3.png 300w
" sizes="(max-width: 400px) 300px, (max-width: 900px) 600px, 1240px" />

2.使用picture元素结合source元素指定图片的url和media query,在不同可视宽度下加载不同的图片,使用img元素加载保底默认的图片

<picture>
    <source media="(max-width: 400px)" srcset="xxxx.png" />
    <source media="(max-width: 900px)" srcset="xxxx1.png" />
    <source media="(max-width: 1240px)" srcset="xxxx2.png" />
    <img src="xxx.png" />
</picture>

四、字体的响应式
1.使用@media
2.使用浮动单位(rem em vw),vw可能会存在屏幕宽度太小字体也会过小显示,可以结合calc(2rem + 2vw)的方式
3.@media与浮动单位结合

四、css实现三角形与扇形

1、使用border加内容h、w为0来实现三角形
2、使用伪类元素实现
3、使用clip或clip-path,裁剪属性来实现
具体代码如下:

<div class="sector"></div>
<div class="fan"></div>

<div class="triangle-up"></div>
<!-- <div class="triangle-down"></div>

<div class="triangle-left"></div>

<div class="triangle-right"></div> -->
<div class="clip"></div>
.triangle-up {
    
    
    width: 0;
    height: 0;
    border-left: 10px solid transparent;
    border-right: 10px solid transparent;
    border-bottom: 10px solid red;
    /* border-top: 10px solid transparent; */
}


.triangle-down {
    
    
    width: 0;
    height: 0;
    border-left: 10px solid transparent;
    border-right: 10px solid transparent;
    border-top: 10px solid green;
}

.triangle-left {
    
    
    width: 0;
    height: 0;
    border-top: 10px solid transparent;
    border-right: 10px solid brown;
    border-bottom: 10px solid transparent;
}

.triangle-right {
    
    
    width: 0;
    height: 0;
    border-top: 10px solid transparent;
    border-left: 10px solid yellow;
    border-bottom: 10px solid transparent;
}

.sector {
    
    
    width: 100px;
    height: 100px;
    background-color: red;
    border-radius: 50%;
    clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}

.fan {
    
    
    width: 0;
    height: 0;
    border-radius: 100% 100% 0 0;
    border-style: solid;
    border-width: 100px 60px 0 60px;
    border-color: #555555 transparent transparent transparent;
    position: relative;
}

.fan::before {
    
    
    content: "";
    position: absolute;
    left: -20px;
    top: 50%;
    width: 20px;
    height: 20px;
    background-color: #555555;
    border-radius: 50%;
    transform: translate(-50%, -50%);
}

.fan::after {
    
    
    content: "";
    position: absolute;
    right: -20px;
    top: 50%;
    width: 20px;
    height: 20px;
    background-color: #555555;
    border-radius: 50%;
    transform: translate(50%, -50%);
}
.clip{
    
    
    width: 300px;
    height: 300px;
    background-color: red;
    position: absolute;
    clip: rect(0px, 120px, 160px, 20px);
    transform: rotate(45deg);
}

typescript相关

一、typescript 类型(对象key字符串,值number的表示方式)

interface {
    
    
    [key:string]: number
}

二、interface与type的区别

在 TypeScript 中,interface 和 type 都用来描述对象类型或其他类型。它们之间的主要区别是:
interface 可以用于声明合并,即两个同名 interface 会被自动合并为一个类型并集
type 可以用于声明联合类型与类型别名
当需要定义一个联合类型时,应该使用 type 语句调整层次结构。而对于任何需要扩展、合并或实现某种类规范的类型定义,都应该使用 interface

三、&

在 TypeScript 中,& 表示交叉类型(Intersection Types),可以将多个类型合并为一个类型。

例如,有两个接口:User 和 Account,可以使用 & 将它们合并成一个“全新”的接口:

interface User {
    
    
    name: string;
}

interface Account {
    
    
    id: number;
}

type UserInfo = User & Account;

const user: UserInfo = {
    
    
    name: "Tom",
    id: 123456,
}

四、ts中有哪些数据类型

Any: 表示任意类型,即可以是任何类型的值。
Number: 表示数字类型,包括整数和浮点数(支持二进制、八进制和十六进制字面量)。
String: 表示字符串类型,包括单引号和双引号两种形式以及模板字符串(使用反引号 ``)。
Boolean: 表示布尔类型,只有两个值:true 和 false。
Array: 表示数组类型,有两种定义方式:number[] 表示只能存储数字类型的数组,Array<number> 表示同样只能存储数字类型的数组。
Tuple: 表示元组类型,表示已知元素数量和每个元素类型的数组,例如 [string, number] 表示一个由字符串和数字组成的元素序列。
Enum: 表示枚举类型,用于为一组数值赋予友好的名字,例如下方例子中代表星期几的变量名就赋予了相应的英文名称。
Void: 表示undefined 或 null 的类型。通常并不建议直接将其与声明变量使用,但却是一些函数的返回值类型。
Null 和 Undefined: 分别表示空值和未定义的值(这两种类型是所有类型的子类型)
Object: 表示非原始类型,即除 number、string、boolean、symbol、null 和 undefined 这六种类型之外的类型。使用 object 定义相当于其它语言中使用 class 的实例对象。
值得注意的是,在 TypeScript 中,变量的类型定义可以使用小写字符开头,也可以使用大写字符开头。小写字符开头通常被称作 Primitive Type (基本类型),而大写字符开头通常被称作 Object Type(对象类型)。

具体如下:
基础类型:number, boolean, string, null, undefined, symbol, void
对象类型:Object, Function, Array, Tuple, Enum, Class, Interface, Type Alias
以上是 TypeScript 中的数据类型,发现有一些类型是 JavaScript 中没有的,比如 Tuple(元组类型),其他的基础类型都差不多,还有一些特殊类型需要在后续学习过程中慢慢掌握。

五、ts中定义function类型的方式

函数声明:使用function关键字来声明函数

function add(x: number, y: number): number {
    
    
  return x + y;
}

函数表达式:将函数赋值给变量,可以简化明确指出参数类型的定义。

const addFunc = (x: number, y: number): number => {
    
    
  return x + y
}

接口定义函数类型:定义一个接口来描述此函数的签名。

interface Add {
    
    
  (x: number, y: number): number
}
const add: Add = (x, y) => x + y

Type别名定义函数类型:使用type别名为函数类型命名。

type Add = (x: number, y: number) => number
const add: Add = (x, y) => x + y

http相关

一、http基础(超文本传输协议)

http是一种
可扩展的传输协议、
应用层的协议、
client-server协议,
HTTP 本质是无状态的,使用 Cookie 可以创建有状态的会话。
可扩展主要体现在http1.0提出的header标头配置,只要客户端与服务端标头语义一致(例如cookie、缓存之类的)

二、http1.0与http1.1的区别

一、tcp连接区别
http1.0 默认短连接 客户端发起一次请求时会建立tcp连接,在服务端响应此次请求后就关闭tcp连接。每次请求都会建立一次tcp连接;
1.0可以通过设置标头Connection:Keep-Alive来打开保持tcp连接持久化功能,可以配合使用Keep-Alive:max=5,timeout=120,表示此次连接最多为5个请求保持持久状态,或者持久状态保持2分钟
http1.1 默认持久连接 1.1逐渐停止了对keep-alive连接的支持,使用持久连接的改进版来取代它,客户端通过配置标头Connection:close来关闭tcp连接,服务端在报文正确等条件下才会保持持久连接,否则服务端主动断开连接
1.1还有个管道化连接连接
二、缓存方式区别
http1.0使用expires绝对时间
http1.1新增cache-control头文件来设置强缓存,max-age相对时间

三、websocket三次握手(tcp三次握手)

SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的
本质是为了确认客户端与服务端双方的发生与接收能力是否ok

第一次握手:客户端发送SYN包,告诉服务器端请求建立连接。
第二次握手:服务器端收到客户端的SYN包之后(服务端确认客户端的发送能力ok),发回一个SYN-ACK包以示传达确认信息(表示收到了客户端的请求,并同意建立连接)。
第三次握手:客户端收到服务器端的SYN-ACK包之后(客户端确认服务端的发送与接收能力ok),向服务器端发送一个确认应答包ACK(表示确认服务器端已收到自己的请求并同意建立连接,服务端确认客户端的接收能力ok),此时TCP连接成功建立。
在三次握手完成之后,客户端和服务器才能互相发送数据。当连接建立完毕之后,如果需要终止连接则需要进行四次挥手。

四、tcp四次挥手

简述:1、客户端发起关闭连接请求,2、服务端收到关闭请求并返回客户端服务端已经收到关闭连接的请求, 3、但是此时服务端可能还有未完成的报文传输,所以会等服务端处理完后向客户端发起FIN报文, 4、客户端收到服务端的FIN报文后发生一个ACK应答给服务端

五、浏览器缓存(http缓存)-- 主要缓存静态资源

浏览器缓存的优点:减少服务器负担,提高网站性能、加快客户端网页的加载速度、减少多余的网络数据传输、提高服务器的并发性能。

一、http请求时,浏览器获取缓存数据或是重新请求后台大致过程
1.在第一次加载资源时,服务器返回200,浏览器从服务端下载资源文件,并缓存资源文件以及响应头,以供下一次请求数据时判断缓存时效性使用

2.下一次加载资源时,由于强缓存(检验时间过期来判断缓存可用性)的优先级较高,先比较本次请求与上一次服务端返回200请求的时间差,如果没有超过标头里cache-control设置的max-age(相对时间)—http1.1版本支持,则没有过期(命中),可以使用强缓存直接从本地获取资源。如果浏览器不支持http1.1,则使用标头里的expires(绝对时间)来判断是否过期

3.如果缓存资源已经过期,则表示强缓存没有被命中,开始走协商缓存,向服务器发送携带If-None-Match(服务器没有与Etag匹配的资源才会去获取数据并返回200)和If-Modified-Since(在Last-Modified(响应标头里存放的上一次修改时间)之后期间有修改过资源,才会去获取数据并返回200否则返回304,客户端获取本地缓存)的请求

4.服务器收到请求后,优先根据Etag的值去判断被请求的资源是否被修改过,结合条件式请求头If-None-Match,存在Etag值一致的资源(命中)则表示没有修改过返回304,客户端使用本地缓存;不存在一致的Etag说明修改过资源,则返回新资源并且响应头携带新的Etag,返回200状态

5.如果请求头里没有Etag值,则使用条件式请求头If-Modified-Since携带上一次请求的Last-Modified进行对比,在Last-Modified之后资源是否被修改过,没有修改(命中协商缓存)则返回304客户端使用本地缓存;被修改过则返回新的资源并且在响应头里携带新的Last-Modified值,返回200

很多网站的资源后面都加了版本号,这样做的目的是:每次升级了 JS 或 CSS 文件后,为了防止浏览器进行缓存,强制改变版本号,客户端浏览器就会重新下载新的 JS 或 CSS 文件 ,以保证用户能够及时获得网站的最新更新。例如cdn之类的引入数据

二、浏览器资源缓存的位置
1.service worker
2.内存(读取效率高、持续性短)
3.硬盘(容量大、持续性长、读取慢)

六、浏览器本地存储

描述Cookie、LocalStorage、SessionStorage

vue相关

一、vue项目如何进行部署、是否遇到过刷新404问题

  1. vue项目部署
    只需将vue的打包文件放在服务器指定静态文件夹下即可,然后使web容器运行起来即可访问项目。
    一般使用nginx来实现,配置完nginx启动即可
  2. 刷新404问题
    history路由模式的情况下会出现部署的包访问后404的情况,因为history路由在浏览器访问路径时,路径会被包括在 HTTP 请求中,服务器会根据路径重新加载页面。
    vue是单页面spa应用,可以通过打包文件看出只有一个入口页面index.html,在nginx配置中可以看出,在浏览器打开服务器地址时,会默认定向到index.html去
    然而其他路径例如 xx.xx.com/login时 浏览器就会刷新并定向到/login的对应界面,但是由于nignx的location没有相关路径的配置,然后服务器又找不到对应的页面就是抛出一个404给浏览器
    至于hash路由模式不存在这种问题,因为hash路由是根据hash值来判断路由并且hash值(#/login)并不会被包括到http请求中去,所以对服务端完全没有影响也就不会有页面重新加载的过程,
    即使没有配置location /login 也不会出现404的情况

解决方法: history模式下,配置nginx try_files $uri $url/ /index.html。即配置重定向让路由始终定向到index.html页面,路由相关全权交由前端处理

二、vue的template到生成html渲染的过程

我们知道 <template></template> 这个是模板,不是真实的 HTML,浏览器是不认识模板的,所以我们需要把它编译成浏览器认识的原生的HTML这一块的主要流程如下
第一步,通过正则等手段提取template里的标签元素(原生html与非原生html)、变量、属性等,解析成抽象语法树AST
第二步,经过一些处理,如标记静态节点等优化,生成render函数
第三步,通过render函数生成vnode,虚拟dom节点
第四步,再经过patch过程(diff算法-新旧虚拟dom比对),找出需要渲染到视图的vnode
第五步,根据vnode创建真实dom节点(html),渲染到视图中,完成template的渲染

三、vue模板编译过程

模板编译就是生成html过程中的1-3步

第一步,通过正则等手段提取template里的标签元素、变量、属性等,解析成抽象语法树AST(一个对象)
第二步,优化AST,遍历AST并用static属性标记其中的静态节点与静态根节点
第三部,根据AST生成对应的render函数

四、diff算法

diff算法在vue中用来比对生成的新旧vnode,优化页面渲染,比对后只渲染有改变的dom
特点:

  1. 同层级比较,不会跨层级比较。我们都知道虚拟dom与真实dom都是tree结构,同层级vnode比对更合理
  2. 比较的方法是 首尾比对法(react是相邻比对)

五、vue3中设置全局变量

  1. 使用config.globalProperties全局对象(vue2中使用的是Vue.prototype.xxx)
  2. 使用provie/inject 全局注入

六、vuex数据持久化

  1. 使用localStorage或sessionStorage
  2. 使用vue-along插件(实质就是使用的localStorage或sessionStorage原理,只是存取的过程组件帮我们完成了)

七、vue中$router与$route的区别

$router是router的实例对象,可以通过实例对象获取整个路由文件,以及一些实例方法等
$route是当前激活的路由信息对象,一个只读属性对象,不能被监听,可以获取当前激活路由的有关信息

八、vue3的响应式原理, 组件v-model的实现

实现组件的v-model:

// 父组件
<template>
  <Child  v-model="msg"></Child>
</template>
<script setup>
  const msg = ref('xxx')
</script>

// 子组件
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
  <div @click="emit('update:modelValue', 'xxxx')">{
   
   { modelValue }}</div>
</template>

vue3中,父组件通过v-model传入变量值,子组件通过defineProps传入固定的参数"modelValue"获取props中v-model的变量值使用,再通过defineEmits第一参数传入固定的标识"update:modelValue",第二个参数传入改变的值来修改v-model的值来实现v-model的双向绑定
vue2中 父组件通过v-model传入变量值,子组件中通过this.props获取v-model传入的值,并通过$emit触发input等事件来修改v-model的值来实现v-model的双向绑定

vue3的响应式原理:
在普通js执行中是不会有响应式的变化的,需要对其进行一系列处理,大致流程如下
1.创建副作用方法,副作用主要是用于执行改变,比如一个变量值改变,与之业务上相关的其他变量值通过执行副作用来改变
2.创建一个map(weakMap)或者set(weakSet)数据类型来存放需要执行的副作用
3.创建一个收集副作用的方法
4.创建一个执行对应副作用的方法
例如 a,b,c 业务逻辑是 a = b + c,要想达到响应式效果,即b与c改变时a的值也相应做出改变。就需要如下操作

let a = 0, b = 0, c = 0
// 创建副作用
const effect = () => {
    
     a = b + c }
//创建一个set收集副作用
const effects = new Set()
// 创建一个收集副作用的方法
const addEffect = () => {
    
     effects.add(effect) }
addEffect()
// 创建一个执行副作用的方法
const deal = () => {
    
    
  effects.forEach(effect => {
    
     effect() })
}
b = 1
deal()
console.log(a) // 1

如上是一个实现响应式的基本流程,最关键的点就在于实现自动收集副作用以及自动执行副作用,在vue中:
vue2通过es5的Object.defineProperty()方法针对单个属性监听get和set,对数据进行劫持,结合发布订阅模式来实现
vue3中通过es6的proxy API实现针对整个对象监听get和set,代理对象。再使用reflect API反射对被代理对象的属性进行操作
相比较:
proxy API 优势有:
1.直接拦截对象进行监听包括数组,
2.速度快,不需要像Object.defineProperty方法那样定义的时候深度递归复杂对象属性,可以省去for in 、闭包等内容来提升效率(直接绑定整个对象即可)
3.可以检测到代理对象的所有操作行为
缺点: es6语法 对一些低版本的浏览器不兼容
defineProperty优势:es5语法 兼容性好
缺点:
1.只能监听某个属性,不能对全对象进行监听,并且只能监听到get与set,新增与删除时无法监听,需要使用$set,$delete方法来辅助
2.对数组类型的监听,是通过重写更新数组的一些实例方法实现拦截的,有明显的缺陷例如无法监听修改数组的下标
3.因为只能递归处理监听对象的单个属性,所以在数据复杂层次深时,就会有性能问题影响初始化速度

九、vue2与vue3的区别总结

1、声明周期
在声明周期方面,vue3使用setup代替了beforeCreated,created,其他大致相同,只是在使用方式上不同,比如mounted,vue3中使用onMounted声明周期钩子函数

2、碎片化(多根节点)
vue2中template里只能支持单一的根节点,vue3中支持多根节点

3、响应式原理
vue2:Object.defineProperty()
vue3:proxy + reflect

4、支持件组件移动到app根节点之外
vue3提供Teleport组件将组件传送到app根节点之外,例如dialog

5、打包优化
tree-shaking:模块打包webpack,rollup等中的概念,移除js上下文中未使用的代码,主要依赖的esmodule中的import与export语句,用来检测代码模块是否导出、导入、被使用。
vue3中针对tree-shaking的支持,将全局以及内部的api进行了重构,全局只能使用es模块构建的命名导出访问
删除未使用的代码以及依赖,优化打包文件大小

6、编写风格
vue3使用的是组合式api,hooks的味道,vue2使用的是选项式api

7、声明响应式变量方式
vue3通过reactive与ref来声明响应式变量,vue2中通过选项式api中的data方法返回一个对象进行响应式绑定

8、异步组件
vue3提供了完整的Suspense异步组件,通过#default插槽来显示异步内容显示,在异步未加载完成时会默认显示#callback插槽里的内容
vue2只是提出了实验性的Suspense异步组件,去了解过没有使用过

9、事件缓存
vue3在第一次渲染后,绑定的事件会被缓存,不想vue2那样每次渲染都会重新给事件绑定新函数

10、虚拟dom与diff算法优化
vue3的vnode对象新增了patchFlag字段,用来标记动态文本信息、静态节点等,静态节点在渲染时直接引用而不需重新创建。在diff中根据patchFlag字段区分静态节点、以及不同类型的动态节点,一定程度的减少节点本身以及属性的比对。

十、组件传参方式

1.props向下,emit向上
2.使用vuex共享store
3.使用event bus
4.$attrs 和 $listeners 是 Vue 的实例属性。子组件存在插槽时,使用$attrs分发未知的 attribute、与$attrs类似使用,$listeners分发未知的事件
5.$refs可以直接访问子组件的内部数据
6.provide/inject

十一、vue组件中定义一个变量a = 1在页面显示,使用for循环去改变100次变量的值,页面会渲染多少次

由于 Vue 是响应式框架,当 a 的值发生变化时会自动更新 DOM,因此循环100次更改变量的值时,DOM
会重新渲染并更新100次,因此,在这个具体的例子中,页面将渲染100次(错误)

正解:
遍历100次需要看消耗的时长,根据消耗时长来确定渲染次数
因为如果在1ms内改变了响应式变量100次,Vue界面只会重新渲染一次。

Vue使用异步更新队列来处理数据的变化。当数据发生变化时,Vue会将这些变化加入到异步更新队列中。然而,由于JavaScript是单线程的,浏览器的刷新率通常为60Hz(每秒60次),因此在1ms的时间内,浏览器只能进行一次刷新。

当多次更新被添加到异步更新队列中,Vue会在下一个刷新周期之前合并这些更新,以减少不必要的重复渲染。这意味着在1ms内对响应式变量进行多次改变,只有最后一次变化会触发重新渲染。

需要注意的是,Vue会根据具体情况进行性能优化,例如针对频繁变化的大量数据,Vue可能会采用批量更新的方式,以便更有效地处理更新。但总的原则是,在同一刷新周期内,Vue只会执行一次重新渲染。

react相关

一、react样式实现作用域

使用css modules css-in-js(styled-components)
<!-- import styles from ‘./App.module.css’; -->
<!-- styles.样式名使用 -->

二、组件传参方式

1.通过props传递数据
2.通过context共享数据
3.redux
4.eventbus

浏览器相关

一、为什么基本数据类型存放在栈中,复杂数据类型存放在堆中

在javascript中,引擎通过栈空间来维持程序的执行上下文状态,如果所有数据都存放在栈的空间内,就会影响到执行上下文的切换效率,从而导致整个程序的执行效率,所以内存大的数据类型存放在堆上,只在栈上标记一个指向堆上的指针

二、js垃圾回收机制

垃圾回收机制就是清理内存的方式

在js中的内存分配机制(根据数据类型)
基础数据类型存放在栈上,引用数据类型存放在堆上。储存在栈上的基础数据类型值可以直接通过操作清除,而在堆上的数据值,大小不确定需要js引擎的垃圾回收机制进行处理
对于堆上的内存管理,js是自动的,每当我们创建函数、对象、数组的时候会自动的分配相应的内存空间,当对象不再被引用的时候是垃圾,对象不能从根上访问(可达对象)到时也是垃圾

垃圾回收机制的策略
1.标记-清除
2.引用计数

三、重绘与回流(重排)

浏览器渲染过程主要分三步:dom解析、css解析、布局绘制
在dom与css发生改变时引起页面重新渲染时,浏览器会对发生改变的部分进行布局(回流)或绘制(重绘)

重绘指的是元素的样式更改并不影响元素在文档流中的位置,只需要重新将新样式绘制到屏幕上即可,比如更改背景颜色、字体颜色等。重绘不会更改元素的大小以及位置

回流通常指元素的几何尺寸以及位置、布局等发生变更,需要重新计算元素的大小、位置并重新构建渲染树和布局树。比如元素的增删改、wh修改以及位置等,还包括元素样式例如动画等(transform转换不会影响回流),都会导致回流。

以下几个原因会触发页面的重绘与回流:
页面初始化渲染时
浏览器窗口大小变化及缩放
dom元素的增删改
元素位置、大小、内容、样式等任何变化
css伪类(:before :after)
内嵌的iframe内容发生变化

优化方法:
1.避免一次性操作多个dom元素,应将多个操作合并成一个操作、使用documentFragment来优化大量的dom操作
2.避免频繁修改dom元素的样式例如el.style.top,应使用class或styles来完成相关操作只触发一次回流或重绘
3.避免使用table布局,每个单元格的大小取决于内容,单元格大小的变化可能导致整个表格的重新布局。
4.使用flex布局代替传统的布局
5.尽量使用transform代替top、left等定位
6.对执行动画的元素尽量使其脱离文档流,减少对其他元素的影响
7.对需要回流的元素,先将元素隐藏再回流

四、硬件加速(gpu加速)

使用transform或opacity等css属性可以开启GPU加速,用来处理一些动画

五、执行js同步代码时遇到一个dom的样式修改,浏览器会怎么操作

当执行同步的javascript代码时,浏览器会等待javascript代码执行完成后才会更新页面的渲染,如果代码中有对dom样式进行修改,则此更改不会立即更新到浏览器窗口。需要等js代码执行完成后重新绘制和呈现整个文档直接渲染界面来显示更新后的样式。
原因:
在浏览器中js引擎与渲染引擎时互斥的,即在执行js代码时会阻塞渲染引擎(cpu),js中遇到样式修改时浏览器会将该修改保存在渲染队列中,只有当这个js的执行完成之后,渲染引擎才会开始渲染修改后的文档

六、浏览器多线程

渲染引擎线程、js引擎(主线程)、定时触发器线程、事件触发线程(任务队列)、http请求线程、service worker

七、浏览器组成

用户界面- 出标签页以外的其他用户界面内容
浏览器引擎(数据持久层) -用于在用户界面与渲染引擎之间传递数据
渲染引擎(包含网络请求模块、js解析器等)-渲染用户请求的页面内容
渲染引擎是浏览器核心,浏览器内核,谷歌、Edge以及opera使用的blink、以及safari使用的webkit

八、浏览器地址栏输入地址获取数据的过程

待补充

九、浏览器地址栏输入地址获取数据后将html渲染到页面的过程(数据转视图)

一、多进程结构的浏览器
进程是操作系统进行资源分配和调度的基本单元,可以申请和拥有计算机资源,进程是程序的基本执行实体(占用独立的空间)
线程是操作系统能够进行运算调度的最小单位,一个进程中可以并发多个线程,每条线程并行执行不同的任务(线程是资源共享的)

当我们启动某个程序时,就会创建一个进程来执行任务代码,同时会为该进程分配内存空间,该应用程序的状态都保存在该内存空间里,当应用关闭时,该内存就会被回收。进程可以启动更多的进程来执行任务,每个进程分配的内存空间时独立的,如果两个进程间需要传递数据需要通过进程间的通信管道ipc来传递。
很多应用程序都是多进程的结构,进程间是相互独立的,这样是为了避免一个进程卡死影响到整个应用程序。
进程可以将任务分成更多细小的任务,然后通过创建多个线程并行执行不同的的任务,同一进程下的线程之间是可以直接通信和共享数据的

对于以前单线程的浏览器,大致有页面线程来负责页面渲染和展示等,js线程执行js代码,以及其他各种功能的线程。单线程结构会引发很多问题,一是不稳定,一个线程卡死可能会导致整个进程出问题,例如打开多个标签页,其中一个标签页卡死可能会导致整个浏览器无法正常运行。二是不安全,单进程的浏览器之间(线程)是可以共享数据的,js线程就可以随意访问浏览器进程内的所有数据。三是不流畅,一个进程需要负责太多的事情会导致运行效率问题。所以为了解决以上问题实现了多进程浏览器结构

多进程浏览器大致分为:
浏览器进程负责控制除标签页以外的用户界面,包括地址栏、书签、前进后退按钮等以及负责与浏览器其他进程协调工作
网络进程负责发起和接受网络请求
GPU进程负责整个浏览器界面的渲染
渲染器进程用来控制页面内容渲染
浏览器会在默认情况下可能会为每一个标签页创建一个进程(谷歌浏览器启动方式-可以配置进程模型)

正文:
当在地址栏输入地址时,浏览器进程的UI线程会捕捉你的输入内容,如果访问的是网址则ui线程会启动一个网络线程来请求DNS进行域名解析获取IP地址,接着开始连接服务器获取数据。如果输入内容不是网址而是一串关键词,浏览器就会使用默认配置的搜索引擎来查询。
html数据获取成功后浏览器执行渲染过程如下:
1、当网络线程获取到数据后,会通过safeBrowsing来检查站点是否是恶意站点(提示当前连接不安全,浏览器阻止你的访问),safeBrowsing是谷歌浏览器内部的一套站点安全系统,例如查看ip是否在黑名单中。当返回数据准备完毕并且安全校验通过后,网络线程会通知UI线程,表示我已经准备好了该你了。然后UI线程通知渲染进程来渲染页面,浏览器进程通过ipc管道将数据传递给渲染进程正式进入渲染流程。渲染器进程的核心任务就是把html、css、js、image等资源渲染成用户可以交互的web界面。

2、渲染器进程的主线程将html进行解析构造dom数据结构,dom文档对象模型是浏览器对页面在其内部的表现形式,可以通过js与之交互的数据结构和api。html首先经过Tokeniser标记化,通过词法分析将输入的html内容解析成多个标记,根据识别后的标记进行dom树构造,在dom树构造的过程中会创建document对象,以documnet为根节点的dom树不断进行修改,向其中添加各种元素。

3、html中会插入一些额外的资源,比如图片、css、js脚本等。这些资源需要通过网络下载或者从浏览器缓存中直接加载,图片、css资源不会阻塞html的解析,因此它们不会影响dom的生成。但是在html解析过程中遇到script标签,就会停止html解析流程转而去加载解析并执行js代码(因为js中可能存在一些对dom的修改操作,比如document.write在页面加载完成后调用会覆盖整个html页面)

4、解析完成html后得到一个dom tree,但是此时还不知道dom tree每个节点的样式。主线程需要解析css并确定每个dom节点的计算样式,即使没有提供css样式,浏览器有自己默认的样式。在样式解析完成后,此时知道了dom结构以及每个节点的样式。

5、接下来需要知道每个节点需要放在页面哪个位置,也就是节点的坐标以及该节点需要占用多大的区域。这个阶段被称为layout布局,主线程通过遍历dom和计算好的样式来生成layout tree, layout tree上的每一个节点都记录了x,y坐标和边框尺寸。需要注意的是dom tree与layout tree并不是一一对应的,设置了display:none的节点不会出现在layout tree上,而在伪类元素中添加了content值得元素,content里的内容会出现在layout tree上不会出现在dom tree中。这是因为dom tree是通过html解析获得的并不关心样式,而layout tree是根据dom tree与计算好的样式来生成的。layout tree和最后展示在屏幕上的节点是对应的

5、在得到layout tree后,还需要知道页面以什么样的顺序绘制节点,例如样式中的z-index会影响节点绘制的层级关系,如果我们按照dom的层级结构来绘制页面则会导致错误的渲染。所以为了保证屏幕上显示正确的层级主线程遍历layout tree创建一个绘制记录表,用来记录节点绘制的顺序。这个阶段被称为绘制

6、到此渲染进程的解析过程就完成了,接下来就是把这些解析出来的信息转化成像素点显示在屏幕上,这种行为被称为栅格化-栅格化指将图块转换成位图。chrome最早使用的一种很简单的方式,只栅格化用户可是区域的内容当用户滚动页面时再栅格化更多的内容来填充,这样就会导致页面展示延迟。现在chrome使用的一种更复杂的栅格化流程,叫做合成,合成是一种将页面的各个部分分层多个图层,分别对其进行栅格化,并在合成器线程中单独进行合成页面的操作。简单来说就是页面的所有元素按照某种规则进行分图层,并把图层栅格化,然后只需要把可视区的内容组合成一帧展示给用户即可
主线程遍历layout tree生成layer tree,当layer tree生成完毕和绘制顺序确定后主线程将这些信息传递给合成器线程,合成器线程将每个图层栅格化,由于一层可能像页面的整个长度一样大,因此合成器线程将他们切分为许多图块,然后将每个图块发送给栅格化线程,栅格化图块后将他们存储在GPU内存中。

7、当图块栅格化完成后,合成器线程将收集称为“draw quads”的图块信息,记录了图块在内存中的位置和在页面中绘制的位置信息。根据这些信息合成器线程生成一个合成器帧,然后这个合成器帧通过IPC传送给浏览器进程,接着浏览器进程将合成器帧传送到GPU,然后GPU渲染展示到屏幕上。

8、此时就可以在页面上看到内容的展示了。

当滚动页面或者操作一些改变页面的交互时,都会生成一个新的合成器帧,新的帧再传给GPU渲染到屏幕上

总结简述:
浏览器进程中的网络线程请求获取到html数据后,通过IPC将数据传给渲染器进程的主线程,主线程将html解析构造dom树,然后进行样式计算,根据dom和计算好的样式生成layout tree(布局树),通过遍历layout tree生成绘制顺序表,接着遍历layout tree生成layer tree,然后主线程将layer tree和绘制顺序信息一起传给合成器线程,合成器线程按照规则进行分图层,并把图层分为更小的图块传给栅格化线程进行栅格化,栅格化完成后合成器线程会获得栅格化线程传过来的“draw quads”图块信息,根据这些信息合成器线程上合成了一个合成器帧,然后将合成器帧通过IPC传回给浏览器进程,浏览器进程再传给GPU渲染到页面上

重排(回流)与重绘
当我们改变一个元素的尺寸位置属性时,会重新进行计算样式,布局、绘制以及后面所有的流程。这种行为就是重排(回流)
当我们改变一个元素的颜色属性时,不会重新触发布局,但还是会触发样式计算和绘制。这个就是重绘
重排和重绘都会占用主线程,同时js也是运行在主线程,就会出现抢占时间的问题(js引擎与渲染引擎是互斥的)。如果写了一个不断导致重绘重排的动画,浏览器则需要每一帧都运行样式计算布局和绘制的操作,我们知道当页面以每秒60帧的刷新率时(每帧16ms)才不会让用户感觉到页面卡顿。如果在运行动画时还有大量的js任务需要执行,因为样式计算、布局、绘制、js执行都是在主线程执行的,当在一帧的时间内布局和绘制结束后,还有剩余时间js就会拿到主线程的使用权,如果js执行时间过长,就会导致在下一帧开始时js没有及时归还主线程,导致下一帧动画没有按时渲染,就会出现页面动画的卡顿

优化方法:
1.使用requestAnimationFrame()方法
requestAnimationFrame() 方法是由浏览器提供的一种API,用于通过 JavaScript 运行动画。 相较于使用 setTimeout 定时器进行动画操作,requestAnimationFrame的性能更加优化。因为它可以跟随显示器的刷新频率执行,会在每一帧被调用,而不是在固定时间间隔中运行代码
当页面处于非激活状态,即你没有来到当前页面时,requestAnimationFrame 不会被执行。
如果要停止requestAnimationFrame,我们需要使用cancelAnimationFrame方法
2.通过上述的渲染流程可以看出,栅格化的整个流程时不占用主线程的(合成器线程与栅格化线程单独的线程运行),意味着它无需和js抢夺主线程,在反复重绘重排时可能会导致页面掉帧,这是因为有可能js执行阻塞了主线程,在css中有个动画属性-transform(转换),通过该属性实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格化线程中,所以不会受到主线程中js执行的影响。更重要的是通过transform实现的动画由于不需要经过布局绘制样式计算等操作所以节省了很多运算时间。通过将元素分层并使用硬件加速(业界称GPU硬件加速,也可以使用opacity属性达到加速效果)

十、v8引擎执行js代码

javascript动态类型语言,js引擎采用执行时编译来编译js代码(just in time compilation 简称JIT;静态类型语言是采用AOT提前编译)。js引擎的作用就是将js代码编译成机器能够识别的代码。

v8引擎是一个接收javascript代码,编译代码然后执行的c++程序,编译后的代码可以在多种操作系统多种处理器上运行。
主要负责:编译和执行js代码、处理调用栈、内存分配、垃圾回收等

一般的js引擎在编译和执行代码都会用到三个重要的组件----解析器(parser)、解释器(interpreter)、编译器(compiler)
解析器负责将js代码解析成抽象语法树AST、解释器负责ast抽象语法树解释成字节码bytecode,同事解释器也有直接解释执行字节码的能力、编译器负责将字节码编译成运行更高效的机器代码

最新版v8引擎执行js代码的流程(整体是字节码和优化后的机器码共存的架构模式)

1.通过parser解析js代码生成抽象语法树
2.AST通过ignition基准解释器生成bytecode字节码,此时ast就会被清除掉释放内存空间,生成的bytecode直接被解释器执行,同时生成的bytecode将会作为基准执行模型,在不断执行过程中,解释器会收集到很多可以用来优化代码的信息,比如变量的类型以及哪些函数执行频率高。这些信息被发送给编译器
3.v8引擎新的编译器Turbofan根据解释器收集的信息和字节码来编译出经过优化的机器代码。在字节码不断执行过程中可能会生成更多的机器码
4.存在逆向还原成字节码的情况(deoptimization),这是因为js是一个动态语言,会导致解释器(ignition)收集到错误信息,比如一个sum方法如下

function sum(a, b) {
    
    
    return a + b
}

在函数声明时js引擎并不知道参数a,b是什么类型。当多次调用sum方法并且都是传入两个number整型时,sum会被解释器标记为热点函数,解释器将收集到的类型信息和该函数对应的bytecode发送给编译器,编译器生成优化后的机器代码中就假定了sum函数的参数是int类型,之后遇到该函数的调用就直接只用运行更快的机器代码。但是如果此时我们调用sum方法传入两个字符串,机器代码不知道如何处理字符串的参数,此时就需要进行deoptimization回退成bytecode,由解释器来解释执行。所以说编程过程中尽量不要把一个变量类型变来变去的,传入函数的参数也最好保持固定否则会给v8引擎带来一些影响,损失一定性能。

v8引擎处理js代码的一些优化策略:
1.如果函数只声明未调用,不会解析成AST也就不会生成字节码
2.函数只调用一次,bytecode直接被解释执行,不会进行优化编译
3.如果函数被多次调用,可能会被标记为热点函数,可能会被编译成机器代码

注-字节码生成速度比编译成机器码快很多

十一、SSR服务端渲染、CSR客户端渲染与SSG预渲染

待完成

SSR目的:
1、解决首屏加载缓慢
2、优化单页应用(SPA)的SEO搜索引擎

同构:一套代码需要在客户端运行还要在服务端运行

vue:Nuxt.js
react: Next.js
原生node服务搭建ssr,主要在于配置webpack对react与vue语法的识别,例如.jsx、.vue等

注水与脱水操作
renderToString方法

webpack等构建工具

一、Vite整个热更新过程

1.创建websocket服务端和client文件,启动服务
2.通过chokidar监听文件变更
3.当代码改变后,服务端进行判断并推送到客户端
4.客户端根据推送的信息执行更新操作

二、vite了解

vite是一个基于esbuild与rollup,依靠浏览器自身ESM编译功能,实现极致开发体验的新一代构建工具

webpakc构建工具
构建工具一般运行过程(打包): 抓取-编译-构建整个应用的代码,生成一份编译、优化后能良好兼容各个浏览器的生产环境代码
开发环境中的流程也大致一致,先将整个应用构建打包,在把打包后的代码交给dev server运行。随着前端业务复杂化,js代码量呈指数增加,导致打包构建时间越来越长,在dev server中服务器启动以及热更新都到达了性能瓶颈,已经没有什么优化的余地了。
vite就是在这种前景下生产出来的

vite相比较webpack拥有极致的开发体验,在dev server中,webpack,采取的是先将整个应用构建打包,在把打包后的代码交给dev server运行模式。而vite是直接将源码交给浏览器,实现dev server秒开,浏览器需要相关模块再向dev server发起请求,服务器返回相应的模块给浏览器,实现了真正意义的按需加载。

Vite主打的是开发环境的极致体验,生产环境集成Rollup

高度集成,开箱即用;
基于ESM急速热更新,无需打包编译
基于esbuild的依赖预处理
兼容Rollup庞大的插件机制,插件开发更简洁
天然支持TS

三、webpack loader的作用

Webpack loader是一种特殊的模块,用于处理应用中引入的非JS文件,并将它们转换为Webpack可识别的有效模块。用来支持CSS、图片、JSON、XML等资源加载
例如:style-loader和css-loader:style-loader和css-loader常常被一起使用来加载和处理CSS文件。
css-loader: 读取 CSS 文件,然后让 Webpack 将其解析成 JS 的 module export 代码块。
style-loader: 接收 css-loader 模块所导出的 JS 模块,并作为样式标签插入到 DOM 中

四、webpack深入(优化、使用)

待完成

前端单元测试

jest使用

待完成

猜你喜欢

转载自blog.csdn.net/weixin_42508580/article/details/129797148
今日推荐