一些有趣的前端知识

浏览器的渲染机制

1. 解析 HTML 结构

浏览器首先从顶部到下方解析 HTML 文档,按照顺序识别标签。解析过程中会生成 DOM(Document Object Model) 树,描述页面结构。

2. CSSOM 树的构建

当解析到 <link> 标签或 <style> 标签时,浏览器会下载并解析 CSS,生成 CSSOM(CSS Object Model) 树。DOM 树CSSOM 树结合后生成渲染树,决定元素的视觉呈现。

3. 加载和渲染顺序

通常情况下,渲染顺序是自上而下的,遵循文档的结构和样式优先级。

4. 阻塞资源

在 HTML 渲染过程中,以下资源会暂时“阻塞”渲染,直到它们加载完成:

  • CSS 文件:浏览器会等待 CSS 文件加载完成,确保正确应用样式后再进行页面渲染。
    • 因为 @import 会导致 CSS 文件的串行加载(即一个文件加载完后才开始下一个),相比 <link> 更慢。因此,通常推荐将 CSS 文件通过 <link> 引入,特别是在网站性能要求较高的情况下。
  • JavaScript 文件:默认情况下,<script> 标签会阻塞 HTML 渲染,直到脚本加载并执行完成。
    • script标签中defer和async的区别
      • script:会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。
      • defer:浏览器指示脚本在文档被解析后执行,script 被异步加载后并不会立刻执行,而是等待文档被解析完毕后执行。
      • async:同样是异步加载脚本,区别是脚本加载完毕后立即执行,这导致async 属性下的脚本是乱序的,对于 script 有先后依赖关系的情况,并不适用
      • 在这里插入图片描述

5. 渲染和绘制

当浏览器构建好 DOM 和 CSSOM 后,会开始生成渲染树,对内容进行布局和绘制。渲染树确定元素的尺寸、位置、样式属性等,之后会绘制在屏幕上。

详细版可以看这里

优化SEO

  • 合理的titledescriptionkeywords
  • 语义化的HTML代码,img带上alt
  • 重要内容不要用js输出

html语义化

建议直接移步这篇博客

CSS的BFC

直接移步移步这篇博客

一个计算其子元素将如何定位,以及和其他元素的关系和相互作用的渲染区域

其触发方式

  • 根元素 (当前文档中 html 标签就是一个BFC);
  • float 的值不为 none 的其他属性值;
  • overflow 的值不为 visible的其他属性值(有 hidden,auto,scroll);
  • display 的值为 inline-block / table-cell / table-caption / flex / inline-flex;
  • position 的值为 absolute 或 fixed;

特性:

边距合并:BFC 内部的元素的边距不会与外部元素的边距合并。即使在同一块上下文中,边距也不会相互影响。

清除浮动:BFC 可以包含内部的浮动元素,从而确保父元素能够正确计算高度,避免高度塌陷的问题。

不与浮动区域重叠BFC区域不会与浮动盒子发生重叠。

JS的 Symbol类型

  • 可以用来表示一个独一无二的变量防止命名冲突。

  • 还可以利用 symbol 不会被常规的方法遍历到,所以可以用来模拟私有变量。

  • 主要用来提供遍历接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const range ={
    form:1,
    to:5,
    *[Symbol.iterator](){ // 迭代器
    for(let v = this.form;v<=to;v++){
    yield v;
    }
    }
    }
  • Symbol.for() 可以在全局访问 symbol

事件流

包括事件捕获阶段,目标阶段,冒泡阶段,一般利用的都是冒泡阶段,冒泡阶段是事件从触发的元素一直向父节点传播

添加事件的方式有

  • 直接在html上绑定事件,element.onclick = func()
  • addEventListener注册事件,removeEnventListener移除事件,其中的参数需要指向同一个函数

事件代理

事件代理,俗地来讲,就是把一个元素响应事件(clickkeydown……)的函数委托到另一个元素

前面讲到,事件流的都会经过三个阶段: 捕获阶段 -> 目标阶段 -> 冒泡阶段,而事件委托就是在冒泡阶段完成

事件委托,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,而不是目标元素

原型继承

原型:是 JavaScript 中对象的一种特殊属性,它允许对象共享属性和方法,从而实现继承。在 JavaScript 中,每个对象都有一个原型对象,当访问对象的属性或方法时,如果当前对象中没有找到,JavaScript 引擎就会沿着原型链向上查找,直到找到该属性或方法或达到 null 为止。

原型链: 是 JavaScript 中实现继承和共享属性的机制,它依赖于对象的 __proto__ 指向上一级对象的原型,从而形成一个链式的继承结构。

构造函数:一个空对象obj被创建,F.prototype的值被链接到obj[[prototype]]中,之后运行构造函数,之后返回obj

原型链中的继承:如果是一个构造函数中,想要添加函数,那么应该将函数挂载到该构造函数的prototype上,这样共享内存和方法,提高效率。

this指针:函数中,有调用者就指向调用者,没有就指向window,箭头函数都指向window,事件里的指向元素本身

call,apply,bind:

  • call、apply、bind 的共同点都是为了解决改变 this 的指向。作用都是相同的,只是传参的方式不同。

  • call() 和 apply() 是立即执行的,而 bind() 是返回一个函数。

  • call() 可以传递多个参数,第一个参数和 apply() 一样,是用来替换的对象,后面是参数列表。

  • apply() 最多只能有两个参数 —— 新this 对象和一个参数数组 argArray

  • bind() 和其他两个方法的作用也是一致的,只是该方法会返回一个函数,并且可以通过bind() 实现 柯里化。

Promise

使用:

1
const a = new Promise((reso,reje)=>{reso(res);reje(res);}).then(fA1,fB1).then(fA2,fB2)

实现:

可以看这篇文章,里面有详细的说明,我这里总结一下

首先是一些零碎的点,设置了三种状态,pending,fulfilled,rejected来实现状态转化

为了实现异步,reso(res)会将thenfA1存储,reje(res)会将thenfB1存储,如果promise中的函数执行完毕,就调用其中的fA1(res)/fB1(res),这样完成了异步

为了实现then返回一个新的promisepromise中会有一个处理函数,处理函数的作用是为了将then中的函数fAfB的结果res使用promisereso/reje处理

Promise中包含了两个主要函数,一个是初始化函数,将执行结果保存在回调数组中,并在执行完毕后调用then的处理函数,另一个是then函数,负责将then中的处理函数放入回调数组中

闭包

什么是闭包:闭包是指一个函数可以记住周围的变量,并访问他们。

每个函数都是闭包的,他们在创建的时候会记录周围的变量

  • 闭包的特性
    • 保存:闭包将局部变量的生命周期拉长,不在随着函数调用结束而回收。
    • 保护:不会成为全局变量、也不会收到外部影响。
    • 可以构建私有变量
  • 闭包的应用
    • 模块
    • 私有属性
    • 高阶函数、有状态的函数
  • 闭包的缺点
    • 内存泄漏

缓存

第一次加载资源:直接请求

下一次加载资源:判断是否有缓存,查看缓存是否过期,没过期就使用缓存,过期就开始协商缓存,通过以下两个字段

  • Last-Modified是服务器响应请求时,返回该资源文件在服务器最后被修改的时间;If-Modified-Since则是客户端再次发起该请求时,携带上次请求返回的Last-Modified值,通过此字段值告诉服务器该资源上次请求返回的最后被修改时间.服务器通过修改事件判断这个资源是否修改,如果没有修改,则返回304,修改则返回200

  • Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),;If-None-Match是客户端再次发起该请求时,携带上次请求返回的唯一标识Etag值,通过此字段值告诉服务器该资源上次请求返回的唯一标识值.同上理

  • Etag / If-None-Match优先级高于Last-Modified / If-Modified-Since,同时存在则只有Etag / If-None-Match生效。为每个服务器上 Etag 的值都不一样,因此在考虑负载平衡时,最好不要设置 Etag 属性。

刷新

  • 点击刷新按钮或者按 F5或者地址栏回车: 使用缓存

  • 用户按 Ctrl+F5(强制刷新): 不适用强缓存,协商缓存使用

  • F12强制刷新缓存:不使用缓存

instanceof typeof

后者判断值类型,函数,是否是引用类型

前者判断一个对象是否是某个构造函数的实例或某个类的实例,会顺着原型链寻找

箭头函数

箭头函数不会创建自身的this,只会从上下文中寻找并继承this,箭头函数的this在定义的时候就已经确认了,之后不会改变。同时箭头函数无法作为构造函数使用,没有自身的prototype,也没有arguments。

事件循环

详细的可以看这篇博客

js是单线程的

JS引擎之所以是单线程,是由于JavaScript最初是作为浏览器脚本语言开发的,并且JavaScript需要操作DOM等浏览器的API,如果多个线程同时进行DOM更新等操作则可能会出现各种问题(如竞态条件、数据难以同步、复杂的锁逻辑等),因此将JS引擎设计成单线程的形式就可以避免这些问题。

柯里化

柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)

可以通过这种方式实现只使用部分函数参数

拓展运算符

用处多,可以用来拓展对象和数组,可以将类数组转化为数组x

weakmap

其key是弱引用,不会阻止垃圾回收

DOM&BOM

DOM 主要关注网页内容和结构的操作,允许开发者通过 JavaScript 动态修改网页的显示内容。

BOM 主要关注与浏览器相关的功能,允许开发者与浏览器窗口和环境进行交互。

变量提升

  • 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
  • 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行

迭代器

1
2
3
4
5
6
7
8
9
[Symbol.iterator](){
// 要么重写next
// next()
}

*[Symbol.iterator](){
// 要么写生成器generator
/// yield
}

call,apply,bind

call可以接收多个参数,apply接收两个参数,第二个参数是个数组或者类数组

1
2
3
4
5
6
7
Function.prototype.myCall=function(context,...args){
context = context || window;
context.fn = this;
const res = context.fn(...args);
delete context.fn;
return res;
}

VUE原理

  • 响应式原理

    proxy,getter,setter,effect

  • 虚拟dom

    vnode,diff

Vue渲染时机

当有数据发生变化的时候,Vue会开启一个队列,将对应的订阅者watcher放入一个队列中中,等待下一个事件tick,多次变化只会被推入一次

Vue生命周期

创建前后:将data,props等数据初始化

挂载前后:构建虚拟dom,并挂载到dom上

更新前后:更新视图,在OnbeforeUpdate中的多次修改视图都会仅仅更新一次

React 节流和防抖

img

React在重新渲染的时候会将所有函数重新挂载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function useDebounce(fn, delay=500, dep = []) {
const { current } = useRef({ fn, timer: null });
useEffect(() => {
current.fn = fn;
}, [fn]);
return useCallback(function (...args) {
if (current.timer) {
clearTimeout(current.timer);
}
current.timer = setTimeout(() => {
current.fn(...args);
}, delay);
}, dep);
}

function useThrottle(fn, limit=500, dep = []) {
const { current } = useRef({ fn, timer: null });
useEffect(() => {
current.fn = fn;
}, [fn]);
return useCallback(
(...args) => {
if (!current.timer) {
current.timer = setTimeout(() => {
current.timer = null;
}, limit);
current.fn(...args);
}
},
[dep]
);
}

本地存储

同源策略

存储大小不同 4k 5M

有效时间不同

React的响应式原理

可以看这篇文章更加详细的解释了react的架构

image-20241111211104515

react有在更新时有三个阶段:

  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler

  • Reconciler(协调器)—— 负责找出变化的组件

  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上

在协调器中,react使用了一种新的响应式diff基础:fiber

这个可以帮助react在执行diff操作的时候,可以被中断,防止阻塞渲染进程。这个的原理是改变了虚拟dom树的存储结构,原本是之后子节点,现在是包含了下一个子节点和兄弟节点和父节点

可以看这篇文章

Vue响应式原理

在分析”响应式”之前, 我们应首先明确: 不进行任何逻辑封装的前提下, JS 是指令式的, 即对 X 的操作就局限于 X, 并不会影响到 Y 或 Z. 原生 DOM 操作也沿袭了指令式风格. 因此在为网站添加动态性, 也就是维护数据 -渲染-> DOM这一关系时, 数据变动指令其后必须跟随渲染指令

出于代码的简洁性, 让数据变动摆脱掉渲染这条”小尾巴”, 前端开发引入了响应式的概念. 具体表现为通过使用 React 或 Vue 等来自动维护数据DOM之间的渲染逻辑. 当数据发生改变, React 或 Vue 将响应变化并依据改变后的结果执行渲染

此时我们可以将数据分到依赖组(dependencies), DOM分到结果组(results), 渲染逻辑从依赖组中获取数据, 经过执行后得到结果组. 那么泛化来说, 依赖组中可以放入任意数据; 结果组中也可以不局限于 DOM; 依赖和结果之间的绑定逻辑也可以自由指定, 且当依赖发生改变时自动执行, 来对结果进行更新, 维护依赖和结果之间的对应关系. 这就实现了响应式

更进一步, 当依赖组内部对象被读取(get), 这就表明某一逻辑依赖了该对象, 此时应追踪(track)该逻辑; 当依赖组内的对象被赋值(set), 此时应根据被赋值对象的追踪情况, 触发(trigger)所有依赖该被赋值对象的逻辑. 这时我们就实现了一个响应式系统