# 前端碎片知识集

我与我周旋久, 宁做我     — 殷浩

在前端的学习旅途中,我们时常会像漫游宇宙般穿梭在各种知识星球之间。有时,我们会发现一颗熠熠生辉的星辰,它可能是一种新的技术,一种优化方案,亦或是一段灵感的闪现。

于是,我将自己在前端旅途中搜集到的那些零散而珍贵的知识点,如同星辰般闪耀,融汇于这篇文章之中。这或许是一个小小的知识集合,但在我的成长过程中,它们无疑是重要的里程碑。

# 如何学习一门语言

  • 语言优势和使用场景
  • 基础语法(常量、变量、函数、类、流程控制)
  • 内置库及API
  • 框架及第三方库
  • 开发环境搭建及调试
  • 线上环境部署及监控

# XMLHttpRequest

异步请求中, xhr对象的readyState主要以下几个状态:

  • UNSET - 0 尚未调用open方法
  • OPENED - 1 open方法已调用
  • HEAD_RECEIVED - 2 send方法已调用, header已被接收
  • LOAD - 3 responseText已有部分内容
  • DONE - 4 请求完成
const xhr = new XMLHttpRequest()
xhr.open('POST', 'www.baidu.com', true) // 默认True异步
setRequestHeader("Content-type", "application/json");
xhr.onreadystatechange = function(){
  if(xhr.readyState === 4){
    if(xhr.staus === 200){
      console.log('请求成功!')
    }
  }
}
xhr.send(JSON.stringify({name: 'zs'}))

xhrreadyState状态为DONE的时候, 也就等同于onload事件完成, 所以Xhr2的标准中可以使用onload

xhr.onload = function(){
  if(xhr.staus === 200){
    console.log('请求成功!')
  }
}

# TCP三次握手

第一次握手: 客户端A将标志位SYN置为1,随机产生一个值为seq=J(J的取值范围为=1234567)的数据包到服务器,客户端A进入SYN_SENT状态,等待服务端B确认;

第二次握手: 服务端B收到数据包后由标志位SYN=1知道客户端A请求建立连接,服务端B将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给客户端A以确认连接请求,服务端B进入SYN_RCVD状态。

第三次握手: 客户端A收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务端B,服务端B检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端A和服务端B进入ESTABLISHED状态,完成三次握手,随后客户端A与服务端B之间可以开始传输数据了。

# TCP 四次挥手

第一次挥手: Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。

第二次挥手: Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与- SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

第三次挥手: Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

第四次挥手: Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。

# Margin

  • margin-topmargin-left负值, 元素向上、向左移动

  • margin-right负值, 右侧元素左移, 自身不受影响

  • margin-bottom负值, 下方元素上移, 自身不受影响

  • margin在弹性子元素项中auto值表示用margin去占用剩余空间

# 屏幕响应式适配

如今移动端响应式屏幕方案主要分为rem适配vw适配

# rem适配

  • 将css属性单位从px改为rem

  • 动态获取用户设备的屏幕宽度

将项目的根字体大小设置为:

fontSize = width(真实设备的屏幕宽度) / width(设计稿的屏幕宽度) * fontSize(设计稿中的根字体大小)

根据这个等比例公式, 动态设置设备的根字体大小

document.documentElement.style.fontSize = Math.min(screen.width, document.documentElement.getBoundingClientRect().width) / 750 * 75(设计稿根字体大小) + 'px'

此处的设计稿中的根字体大小可以随意设置任意正数值, 但是必须与postcss-pxtorem这种插件中的配置值一致

// postcss.config.js
module.exports = {
  plugins: {
    'postcss-pxtorem': {
      rootValue: 75,  // 可以随意设置, 与动态值匹配即可
      propList: ['*'],
      minPixelValue: 2
    }
  }
};

# vw适配

vw适配方案将不需要动态的js来计算屏幕宽度, 只需要使用postcss-px-to-viewport插件, 按照设计稿的参数来配置即可

module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      unitToConvert: 'px', // 要转化的单位
      viewportWidth: 750, // UI设计稿的宽度
      unitPrecision: 3, // 转换后的精度,即小数点位数
      propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
      viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
      fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
      selectorBlackList: [], // 指定不转换为视窗单位的类名,
      minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
      mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
      replace: true, // 是否转换后直接更换属性值
      exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
      landscape: false // 是否处理横屏情况
    }
  }
}

# rem + vw 适配

以下例子可以适配设计稿为750px的尺寸

<head>
  <style>
    /* 根字体大小设置为10vw */
    html {
      font-size: 10vw;
    }
  </style>
</head>
// postcss.config.js
module.exports = {
  plugins: {
    'postcss-pxtorem': {
      rootValue: 75,  // 可以随意设置, 与动态值匹配即可
      propList: ['*'],
      minPixelValue: 2
    }
  }
};

# BEM命名规范

BEM是块(block)、元素(element)、修饰符(modifier)的简写

  • 中划线( - ): 仅作为连字符使用, 表示某个块或者某个子元素的多单词之间的连接记号(单词间隔)
  • 双下划线( __ ): 双下划线用来连接块和块的子元素(连接块元素)
  • 双中划线( -- ): 双中划线用来描述一个块或者块的子元素的一种状态(元素状态)

# 作用域分类

JS 总共有 9 种作用域,我们通过调试的方式来分析了下:

  • Global 作用域: 全局作用域,在浏览器环境下就是 window,在 node 环境下是 global
  • Local 作用域:本地作用域,或者叫函数作用域
  • Block 作用域:块级作用域
  • Script 作用域:let、const 声明的全局变量会保存在 Script 作用域,这些变量可以直接访问,但却不能通过 window.xx 访问
  • Module 作用域:es module 模块运行的时候会生成 Module 作用域,而 commonjs 模块运行时严格来说也是函数作用域,因为 node 执行它的时候会包一层函数,算是比较特殊的函数作用域,有 module、exports、require 等变量
  • Catch Block 作用域: catch 语句的作用域可以访问错误对象
  • With Block 作用域:with 语句会把传入的对象的值放到单独的作用域里,这样 with 语句里就可以直接访问了
  • Closure 作用域:函数返回函数的时候,会把用到的外部变量保存在 Closure 作用域里,这样再执行的时候该有的变量都有,这就是闭包。eval 的闭包比较特殊,会把所有变量都保存到 Closure 作用域
  • Eval 作用域:eval 代码声明的变量会保存在 Eval 作用域

# 资源提示符

html标签包含很多资源提示符, 常用async、defer、preload、prefetch

  • script标签async 不阻塞dom解析, 等script加载完成, 立即停止dom解析, 执行script

  • script标签defer不阻塞dom解析, 等script加载完成, 不会立即执行, 而是等到DomContentLoaded事件开始之前执行

  • link标签preload尽快获取并缓存, 当前页面可能会用到

  • link标签prefetch空闲时间获取并缓存, 下个页面可能会用到

# 经典三栏布局

经典的三栏布局常用双飞翼布局圣杯布局

# 双飞翼布局

<style>
  .middle,
  .left,
  .right {
    float: left;
    height: 300px;
  }

  .shuangfeiyi {
    /* 生成BFC, 清除浮动 */
    overflow: hidden;
  }

  .middle {
    width: 100%
  }

  .left {
    width: 200px;
    margin-left: -100%;
  }

  .right {
    width: 300px;
    margin-left: -300px;
  }

  .middle__content {
    margin: 0 300px 0 200px
  }
</style>

<div class="shuangfeiyi">
  <div class="middle">
    <div class="middle__content"></div>
  </div>
  <div class="left"></div>
  <div class="right"></div>
</div>

# 圣杯布局

<style>
  .middle,
  .left,
  .right {
    float: left;
    height: 300px;
  }

  .shengbei {
    /* 生成BFC, 清除浮动 */
    overflow: hidden;
    padding: 0 300px 0 200px;
  }

  .middle {
    width: 100%;
    background-color: antiquewhite;
  }

  .left {
    width: 200px;
    margin-left: -100%;
    position: relative;
    right: 200px;
    background-color: pink;
  }

  .right {
    width: 300px;
    margin-right: -300px;
    background-color: aquamarine;
  }
</style>

<div class="shengbei">
  <div class="middle">中间</div>
  <div class="left">左侧</div>
  <div class="right">右侧</div>
</div>

# React

# React Effect

当我们编写组件时,应该尽量将组件编写为纯函数。

对于组件中的副作用,首先应该明确:

是「用户行为触发的」还是「视图渲染后主动触发的」?

对于前者,将逻辑放在Event handlers中处理。

对于后者,使用useEffect处理。

# React 首行引入

新版本React函数式组件, 不需要每次在引入React库, 是因为babel做了处理, Automatic Runtime会自动注入行首的React引入逻辑

只需要将babel配置为:

"presets": [
    ["@babel/preset-react",{
     "runtime": "automatic"
    }]
],

在代码中就会自动引入

// 会被自动引入
import { jsx as _jsx } from "react/jsx-runtime";

# React 元素隐藏

在React中, 当一个元素为falseundefinednull时, 该元素将不显示。 因此开发时,要注意元素判断条件是否为这几类值, 否则会导致bug

class App {
  render() {
    const { list } = this.state
    return <div className="page-index-box">
      {
       // 该写法, 当length为0, 会显示0, 导致bug
        list.length && list.map(() =>
          <div>
            {list.id} - {list.name}
          </div>
        )
      }
    </div>
  }
}

# React SetState

  • 假如一次事件中触发一次如上 setState ,在 React 底层主要做了那些事呢?

    • 首先,setState 会产生当前更新的优先级(老版本用 expirationTime ,新版本用 lane )。
    • 接下来 React 会从 fiber Root 根部 fiber 向下调和子节点,调和阶段将对比发生更新的地方,更新对比 expirationTime ,找到发生更新的组件,合并 state,然后触发 render 函数,得到新的 UI 视图层,完成 render 阶段。
    • 接下来到 commit 阶段,commit 阶段,替换真实 DOM ,完成此次更新流程。
    • 此时仍然在 commit 阶段,会执行 setState 中 callback 函数,如上的()=>{ console.log(this.state.number) },到此为止完成了一次 setState 全过程。

    更新的流程图如下:

    请记住一个主要任务的先后顺序,这对于弄清渲染过程可能会有帮助: render 阶段 render 函数执行 -> commit 阶段真实 DOM 替换 -> setState 回调函数执行 callback 。

# Node

# Node Babel的配置

  • 安装依赖
pnpm i @babel/core @babel/preset-env @babel/plugin-transform-runtime -D
# @babel/runtime-corejs3会将core-js@作为依赖安装
pnpm i @babel/runtime-corejs3 -S
  • 书写配置文件
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: "current"
        },
        // 关闭 @babel/preset-env 默认的 Polyfill 注入
        useBuiltIns: "usage",
        // 使用core-js@3提供polyfill
        corejs: 3,
        // 是否转换为其他模块, 可以转换成‘commonjs’、'amd'、‘umd’、‘systemjs’
        modules: false
      }
    ]
  ],
  plugins: [
    // 添加 transform-runtime 插件
    [
      '@babel/plugin-transform-runtime',
      { corejs: 3 }
    ]
  ]
}

# Node Esm模块开发

  • Node中使用ESM模块开发, 需要处理常用的内置变量, 例如__dirname或者__filename
import path from 'path'

const __filename = new URL(import.meta.url).pathname
const __dirname = path.dirname(__filename)
  • Node ESM模块中使用json文件应该怎么做?
// 对于 Node 17.5+,你可以使用导入断言
import pkg from './package.json' assert { type: 'json' };
export default {
	external: Object.keys(pkg.dependencies)
};

// 如果版本低于17.5, 通用情况就使用fs读取
import { readFileSync } from 'node:fs';
const packageJson = JSON.parse(
	readFileSync(new URL('./package.json', import.meta.url))
);

# Node Spawn双向通信

const { spawn } = require('child_process');

const svrCodeWatchProcess = spawn('npm', ['run', 'svr:watch'], { shell: process.platform === 'win32' });

// 父进程发送消息给子进程
svrCodeWatchProcess.stdin.write('Hello from parent process!');

// 子进程接收父进程消息
process.stdout.on('data', (data) => {
  console.log('Received message from child process:', data.toString());
});

// 子进程发送消息给父进程
process.stdout.write('Message from child process');

// 父进程接收子进程消息
svrCodeWatchProcess.stdout.on('data', (data) => {
  console.log('Received message from child process:', data.toString());
});

# 微前端

  • 增量开发及迁移
  • 独立构建发布
  • 包容不同的技术栈

# HMR分类

HMR主要分为热重载和热替换, 两者的区别如下:

  • 热重载: 代码变化重新加载整个应用, 会重刷整个页面, 导致状态和数据丢失

  • 热替换: 只会替换修改的模块, 不重刷页面, 状态数据得以保留

# HMR热更新原理

热更新主要是由4个部分组成

  • HMR Server 服务端
  • Compiler 编译器
  • Module 模块
  • HMR Runtime 客户端

首先, 服务端和客户端会建立Websocket连接, 当代码文件发生变动, HMR Server告知HMR Runtime需要检查更新, HMR Runtime 发起一个请求,获取变更的模块信息JSON文件, 从JSON中获取到已经发生变更的模块;

此时, HMR Runtime异步下载需要更新的模块, 一切就绪后, HMR Server通过 WebSocket 通知 HMR Runtime 可以同步应用更新, HMR Runtime 将最新的模块进行替换,将老的模块解绑,最后更新应用 Hash,并开始以更新的模块为起点向上进行冒泡

如果, 在模块中有注册了 HMR module.hot.accept接口, 那么就会调用该接口来实现元素如何替换

# HMR整体流程

Webpack DevServerHMR(Hot Module Replacement)是一种在开发过程中实现模块热替换的机制,它能够在不刷新整个页面的情况下,实时更新已经修改的模块。下面是 Webpack DevServer HMR 的主要流程:

启用 HMR: 在 Webpack 配置中,你需要启用 HMR 功能。这通常通过 webpack-dev-server 提供的 hot 参数来实现。例如:

// webpack.config.js
module.exports = {
  // ...
  devServer: {
    hot: true,
  },
  // ...
};
  • 启动开发服务器: 运行 webpack-dev-server 命令启动开发服务器。开发服务器会将打包后的文件提供给浏览器,并监听文件变化。

  • 构建并打包: Webpack 会将项目的源代码进行构建和打包。每个模块都会生成一个唯一的 ID,并且在构建过程中会生成一些用于 HMR 的额外代码。

  • HMR runtime 注入: Webpack 会在构建过程中,将 HMR runtime(HMR 的运行时)注入到打包后的文件中。HMR runtime 负责在浏览器端与开发服务器进行通信,接收更新,并触发模块的热替换。

  • 浏览器连接到开发服务器: 当你在浏览器中访问开发服务器时,浏览器会与开发服务器建立连接。这个连接使用 WebSocket 协议,因为 WebSocket 具有实时双向通信的能力。

  • 监听文件变化: 开发服务器会监听项目源代码的变化,包括源代码中的 JavaScript 文件、CSS 文件等。

  • 文件更新推送: 当文件发生变化时,Webpack DevServer 会通过 WebSocket 将更新的模块信息推送给浏览器中的 HMR runtime

  • 模块热替换: 在接收到更新的模块信息后,HMR runtime 会根据这些信息进行模块热替换。它会移除旧的模块,加载新的模块,并应用新的变化,从而实现实时更新。这样,你就能在浏览器中看到最新的修改,而不需要刷新整个页面。

  • 应用更新: HMR 完成模块的热替换后,浏览器会将更新应用到页面中,从而实现无刷新的开发体验。

总的来说,Webpack DevServer HMR 的流程可以简化为:文件变化 -> HMR runtime 接收更新 -> 模块热替换 -> 浏览器应用更新。这样开发者就可以在保持应用状态的同时,实时地看到修改后的效果,提高开发效率。

# 逻辑运算规律

逻辑运算有几个常见的规律,其中包括以下几种:

  1. 交换律(Commutative Law):对于逻辑与(AND)和逻辑或(OR)运算,交换律成立:
    • A && B 等价于 B && A
    • A || B 等价于 B || A
  2. 结合律(Associative Law):对于逻辑与(AND)和逻辑或(OR)运算,结合律成立:
    • (A && B) && C 等价于 A && (B && C)
    • (A || B) || C 等价于 A || (B || C)
  3. 分配律(Distributive Law):对于逻辑与(AND)和逻辑或(OR)运算,分配律成立:
    • A && (B || C) 等价于 (A && B) || (A && C)
    • A || (B && C) 等价于 (A || B) && (A || C)
  4. 吸收律(Absorption Law):对于逻辑与(AND)和逻辑或(OR)运算,吸收律成立:
    • A && (A || B) 等价于 A
    • A || (A && B) 等价于 A
  5. 德摩根定律(De Morgan's Laws):逻辑非(NOT)的德摩根定律成立:
    • !(A && B) 等价于 !A || !B
    • !(A || B) 等价于 !A && !B
    • A && B 等价于 !(!A || !B)
    • A || B 等价于 !(!A && !B)

# JS中的访问器属性

在 JavaScript 中,对象的访问器属性可以通过 Object.defineProperty 方法来定义,这是一种常见的方式。但是,还有其他方式可以定义对象的访问器属性。

除了使用 Object.defineProperty,还有以下两种方式来定义对象的访问器属性:

  • 使用 getset 关键字:在 ES6(ECMAScript 2015)及以后的版本中,可以使用 getset 关键字来定义对象的访问器属性。
const obj = {
  firstName: 'John',
  lastName: 'Doe',
  get fullName() {
    return this.firstName + ' ' + this.lastName;
  },
  set fullName(value) {
    const [firstName, lastName] = value.split(' ');
    this.firstName = firstName;
    this.lastName = lastName;
  }
};

console.log(obj.fullName); // 输出:John Doe

obj.fullName = 'Jane Smith';
console.log(obj.firstName); // 输出:Jane
console.log(obj.lastName); // 输出:Smith

在上述示例中,通过在对象字面量中使用 getset 关键字,定义了 fullName 属性的获取和设置逻辑。

  • 使用 class 中的 getter 和 setter 方法:在使用类(class)定义对象时,可以通过 getter 和 setter 方法来定义访问器属性。
class Person {
  constructor(firstName, lastName) {
    this._firstName = firstName;
    this._lastName = lastName;
  }

  get fullName() {
    return this._firstName + ' ' + this._lastName;
  }

  set fullName(value) {
    const [firstName, lastName] = value.split(' ');
    this._firstName = firstName;
    this._lastName = lastName;
  }
}

const person = new Person('John', 'Doe');

console.log(person.fullName); // 输出:John Doe

person.fullName = 'Jane Smith';
console.log(person.fullName); // 输出:Jane Smith

在上述示例中,通过在 class 中定义 getter 和 setter 方法,实现了访问器属性 fullName

总结:除了使用 Object.defineProperty 方法外,JavaScript 中的对象的访问器属性可以通过 getset 关键字以及 class 中的 getter 和 setter 方法来定义。这些方法都可以用来创建和操作对象的访问器属性, 且get或者set后面必须指定属性名。

# 严格模式

类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。

# Iterator 和 Generator

生成器用于创建迭代器, 调用生成器会返回迭代器, 部署了迭代器接口[Symbol.Iterator]函数的就可以通过for...of直接遍历, 在生成器内部, 为了方便使用, 可以是用yield*直接调用迭代器, 等同于多条yield

# 缓存图片

在浏览器中, 可以使用三种方式创建图片

  • innerHTML
  • new Image
  • document.createElement

创建好图片后, 将需要缓存的所有图片, 添加上src属性, 浏览器就会去下载对应的图片。当使用图片时, 直接引用这些图片对象即可

// 缓存图片
const imgList = Array.from({ length: 10 }, (_, index) => {
  const img = new Image()
  img.src = `https://picsum.photos/id/${index}/200/300`
  return img
})

# Vue子组件异步调用

Vue中父组件如果需要调用子组件的异步方法, 或者父组件需要等待子组件某种Ready状态时, 再执行某些事情, 可以按照如下几种方式

# 子组件发送事件

// a.vue
<template>
  <div>
    这是a页面
    <childB ref="childB" @onReady="toPlay"/>
  </div>
</template>
<script>
import childB from './b'
export default {
  methods: {
    toPlay(){
      const { play } = this.$refs.childB
      play()
    }
  },
  components: {
    childB,
  },
}
</script>

// b.vue
<template>
  <div>这是b页面</div>
</template>
<script>
export default {
  beforeCreate(){
    this.init()
  },
  methods: {
    init() {
      setTimeout(() => {
        this.$emit("onReady")
      }, 2000)
    },
    play() {
      console.log('ok')
    },
  },
}
</script>

# 子组件暴露Promise

// a.vue
<template>
  <div>
    这是a页面
    <childB ref="childB" />
  </div>
</template>
<script>
import childB from './b'
export default {
  mounted() {
    const { init, play } = this.$refs.childB
    init().then(play)
  },
  components: {
    childB,
  },
}
</script>

// b.vue
<template>
  <div>这是b页面</div>
</template>
<script>
export default {
  methods: {
    init() {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve()
        }, 2000)
      })
    },
    play() {
      console.log('ok')
    },
  },
}
</script>

# 子组件内部await

// a.vue
<template>
  <div>
    这是a页面
    <childB ref="childB" />
  </div>
</template>
<script>
import childB from './b'

export default {
  mounted() {
    this.$refs.childB.play()
  },
  components: {
    childB,
  },
}
</script>


// b.vue
<template>
  <div>这是b页面</div>
</template>
<script>
const promisArr = (n = 1) =>
  Array.from({ length: n }, (_, id) => {
    let resolve, reject
    const promise = new Promise((e, j) => ((resolve = e), (reject = j)))
    return { id, resolve, reject, promise }
  })

const { id, reject, resolve, promise } = promisArr().pop()
export default {
  created() {
    this.init()
  },
  methods: {
    init() {
      setTimeout(() => {
        resolve('hello')
      }, 2000)
    },
    async play() {
      const res = await promise
      console.log('ok', res)
    }
  }
}
</script>

# 字符串码点和码元

字符编码系统将一个 Unicode码位 (opens new window)编码为一个或者多个码元

JavaScript字符串使用的编码系统是UTF-16, 即表示一个码元, 也就是两个个字节(2^16 = 65535)

并非 Unicode 定义的所有码位都适合单个 UTF-16 来编码

如果字符串包含非 ASCII字符,那么这个字符可能是由两个码元组成, 即四个字节, 这种两个码元组成的称为码点

我们常用的API, 都是使用的单个码元作为单位编码, 例如:length表示码元个数、slice以码元作为单位

为了表示完整的字符串, JS提供了一个码点相关的API

  • String.prototype.codePointAt 读取字符的码点值
  • String.prototype.fromCodePoint 从码点值转化为字符
const str = `🎂`
str[0]  // 输出为�, 会乱码
str.codePointAt(0)  // 127874大于了65535, 因此使用两个码元编码

// 给字符串添加码点长度的属性
Reflect.defineProperty(String.prototype, 'codePointLength', {
  get(){
    const TWO_BYTE = 65535
    let len = 0
    for(let i = 0; i < this.length; ){
      const codePonitValue = this.codePointAt(i)
      i += codePonitValue > TWO_BYTE ? 2 : 1
      len++
    }
    return len
  }
})

# 内存泄露

常见内存泄露的场景如下:

  • 意外的全局变量
  • 遗忘的定时器
  • 使用不当的闭包
  • 已卸载DOM的持久引用

此处第四点, 如果有变量引用一个DOM元素, 后来DOM元素被移除, 就会发生内存泄露

因此, 日常开发中, 记得使用WeakSetWeakMap来存储这种场景下的DOM元素

# Visual Studio Code

# 重构相关

Visual Studio Code征对重构类、快速定位报错行、征对报错行进行处理、修改函数名称等等征对代码重构优化操作的快捷键收录到此

  • 重构:control + shift + R
  • 定位代码警告:F8
  • 快速修复: cmd + .
  • 重命名符号: F2
  • 跳转到符号: shift + cmd + O
  • 跳转到文件: cmd + P
  • 跳转到行: control + G
  • 显示所有符号Symbol: cmd + T
  • 显示命令板: cmd + shift + P
  • 单词切换大写: control + shift + U
  • 单词切换小写: control + shift + I

更多细节阅读Visual Studio Code文档: typescript-refactoring (opens new window)

# 常用调试配置

{
  // 使用 IntelliSense 了解相关属性。
  // 悬停以查看现有属性的描述。
  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "name": "ChromeCanary调试",
      "request": "launch",
      "type": "chrome",
      "url": "http://localhost:8888",
      "webRoot": "${workspaceFolder}",
      "userDataDir": false,
      "runtimeExecutable": "canary",
      "runtimeArgs": [
        "--auto-open-devtools-for-tabs"
        // 无痕模式
        // "--incognito"
      ]
    },
    {
      "name": "ChromeStable调试",
      "request": "launch",
      "type": "chrome",
      "url": "http://localhost:8081",
      "webRoot": "${workspaceFolder}/projects/wds-html/src",
      // "preLaunchTask": "debug", // 添加的配置,在执行之前需要启动项目,这个是启动项目用的任务。
      "sourceMapPathOverrides": {
        // 把调试的文件 sourcemap 到的路径映射到本地的文件
        "webpack:///./*": "${webRoot}/*",
        "webpack:///src/*": "${webRoot}/*",
        "webpack:///./src/*": "${webRoot}/*",
        "webpack:///*": "*",
        "webpack:///./~/*": "${webRoot}/node_modules/*"
      } // 添加的配置,为了找到打包文件和源代码之间的关联,使断点生效。
    },
    {
      "name": "Static调试",
      "request": "launch",
      "type": "chrome",
      "runtimeExecutable": "canary",
      "userDataDir": false,
      "webRoot": "${workspaceFolder}",
      "file": "${workspaceFolder}/code-snippets/htmls/vue滑动卡片跟随.html",
      "pathMapping": {
        // 服务的路径映射到本地的目录
        "/static/js/": "${workspaceFolder}/src/"
      }
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Node调试",
      // 这里配置脚本位置(package.json->main)
      "program": "${file}",
      // 传给program的参数
      // "args": [
      //   "-l https://juejin.cn/book/7070324244772716556/section/7148818133343158286"
      // ],
      "cwd": "${workspaceFolder}",
      "skipFiles": [
        // "<node_internals>/**"
      ],
      // console 要设置为 integratedTerminal,这样日志会输出在 terminal,就和我们手动执行 npm run xxx 是一样的
      // 不然,日志会输出在 debug console。颜色啥的都不一样
      "console": "integratedTerminal"
    },
    {
      "name": "Pnpm调试",
      "request": "launch",
      "cwd": "${workspaceFolder}/projects/webpack-error-test",
      "env": {
        "ENV_VAR": "1123"
      },
      // "envFile":"...",
      "runtimeArgs": ["run-script", "build-dev"],
      "runtimeExecutable": "pnpm",
      "skipFiles": [],
      "type": "node",
      "resolveSourceMapLocations": ["${workspaceFolder}/**"],
      "stopOnEntry": true
    },
    {
      "name": "Typescript调试",
      "program": "${file}",
      "request": "launch",
      "sourceMaps": true,
      // 默认是 node, 从 PATH 的环境变量中查找对应名字的 runtime 启动
      "runtimeExecutable": "esno", // ts-node也可以, 执行效率没有esno快
      "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
      "resolveSourceMapLocations": ["${workspaceFolder}/**"],
      "type": "node",
      // 入口处断住
      "stopOnEntry": true
    },
    {
      // require(child_process).exec('xxx.js')
      "name": "Child进程调试",
      "program": "${file}",
      "request": "launch",
      "skipFiles": ["<node_internals>/**"],
      "type": "node",
      "console": "internalConsole",
      "autoAttachChildProcesses": true
    },
    {
      "name": "Pick进程调试",
      "processId": "${command:PickProcess}",
      "request": "attach",
      "skipFiles": ["<node_internals>/**"],
      "type": "node"
    },
    {
      "name": "Vite调试",
      "type": "chrome",
      "request": "launch",
      "runtimeExecutable": "canary",
      "runtimeArgs": ["--auto-open-devtools-for-tabs"],
      "userDataDir": false,
      "url": "http://localhost:3000",
      // vite 时会有一些热更之类的文件,也会被映射到源码,导致断在某名奇妙的地方
      // 把 webRoot 配置成任意的一个不存在的目录,比如 noExistPath,这样这些文件就不会被错误的映射到源码里了。
      // 算是一种 hack 的处理方式
      "webRoot": "${workspaceFolder}/noExistPath"
    },
    {
      // 可以调试多个项目, 先自己在浏览器打开相应的页面
      "type": "chrome",
      // 连接某个已经在调试模式启动的 url 进行调试
      "request": "attach",
      "name": "Vite 模块联邦",
      "port": 9222,
      // "preLaunchTask": "launch-chrome",
      "webRoot": "${workspaceFolder}/projects/module-federation/",
      "skipFiles": [],
      "sourceMapPathOverrides": {
        "webpack:///./*": "${webRoot}/*",
        "webpack:///src/*": "${webRoot}/*",
        "webpack:///./src/*": "${webRoot}/*",
        "webpack:///*": "*",
        "webpack:///./~/*": "${webRoot}/node_modules/*"
      }
    },
    {
      // nodemon --inspect-brk=9229 x.js
      // nodemon --inspect=9229 x.js
      "name": "Server附加调试",
      "port": 9229,
      "request": "attach",
      "skipFiles": ["<node_internals>/**"],
      "type": "node"
    }
  ]
}

# 快捷键文档

如果需要查询Chrome的所有快捷键, 可以在vscode中按下cmd + k cmd + r组合快捷键

通过Chrome直达Visual Studio Code快捷键官方文档 (opens new window)

# 单点登录三种方式

选用该Session + Cookie方案的系统, 用户登录都会到统一认证中心。 认证中心产生UIDSessionId, 认证中心存储登录信息到数据库中, 并将UID返回到Cookie

当请求子系统接口时, 会将Cookie发送到子系统, 子系统通过查询认证中心UID是否登录, 再进行下一步的处理

优缺点

很容易控制用户在线状态,缺点扩容花费高,子系统扩容会导致认证中心也必须扩容

# Token

选用该Session + Cookie方案的系统, 用户登录依旧到统一认证中心, 认证中心产生Token返回给客户端, 客户端存储到本地用于后续请求

当请求子系统接口时, 会将Token发送到子系统, 子系统能直接验证Token是否有效, 因为子系统和认证中心有一套约定的密钥,因此不需要再次请求认证中心

优缺点

方便子系统扩容,缺点是不容易控制用户状态,如果想注销用户的状态,只能等待Token过期

# AccessToken + RefreshToken

选用该AccessToken + RefreshToken方案的系统, 用户登录依旧到统一认证中心, 认证中心产生短效期的AccessToken(分钟)和长效期RefreshToken(天)返回给客户端, 客户端将两个Token都存储到本地用于后续请求

当请求子系统接口时, 客户端只会将短效期的AccessToken发送到子系统, 子系统能直接验证AccessToken是否有效, 由于短效AccessToken时间短, 因此会经常过期

当短效AccessToken过期时, 客户端需要使用RefreshToken向认证中心发送请求, 获取最新的AccessToken, 再次存储到本地, 供后续请求使用

优缺点

子系统容易扩容,还可以控制用户登出状态, 因为AccessToken时效短, 因此方案更佳

# Jest

# 项目集成

在一个typescript项目中集成Jest的步骤如下

  • 安装依赖
pnpm add jest @types/jest -D
  • 添加BabelTypescript支持
# 如果项目中在使用`jest-cli`,推荐搭配`babel-jest`,它将使用 `Babel` 自动编译 `JS` 代码
pnpm add babel-jest @babel/core @babel/preset-env @babel/preset-typescript -D
  • 添加Babel配置
// 为Node配置Babel后, 就可以随意在Node中使用ESM模块
module.exports = {
  presets: [
    ['@babel/preset-env', {targets: {node: 'current'}}],
    '@babel/preset-typescript',
  ],
};

集成部分请查阅Jest官网: 使用Typescript (opens new window)

# 小技巧

  • 模拟函数 jest.fn
  • 跳过当前用例 it.skip
  • 仅测试当前用例, 后续不测试 it.only
  • 监听所有测试文件 jest --watchAll
  • 测试覆盖率 jest --coverage

# 断言API

====================真实性Truthiness=============
toBeNull                // 是否为Null
toBeUndefined
toBeDefined
toBeFalsy               // 为假 ensure a value is false in a boolean context
toBeTruthy

====================数字Numbers====================
toBeGreaterThan         // 大于
toBeGreaterThanOrEqual  // 大于等于
toBeLessThan            // 小于
toBeLessThanOrEqual     // 小于
toBe                    //
toEqual                 //
toBeCloseTo             // 接近的数,解决js小数的浮点问题
closeTo                 // 浮点数compare float point numbers

====================数组Arrays&迭代iterables=============
toContain               // 对象是否在数组中使用 check an item is in an array
not.arrayContaining     // 不包含该元素 array is not a subset子集 of the received array

====================异常Exceptions=============
toThrow             // 调用时必须抛出异常 a function throws when it is called
not.toThrow         // 不要抛出异常

====================方法Function=============
assertions          // 必须要调用测试 verifies a certain number of assertion are called
hasAssertions       // 至少调用了一个方法 at least one assertion is called
toHaveBeenCalled    // 被调用的

====================对象Object====================
toEqual             // 相等 Object.is
toStrictEqual       // 对象的结构是否完全相等 test the object have the same types&structure
objectContaining    // 预期对象存在接收对象 received object contains properties in expected object
toBeInstanceOf      // 对象是否为类的实例 check the object is an instance of a class
toMatchObject       // 对象匹配另一个对象的子集a js Object matches a subset of the properties of an object

====================字符串String====================
stringMatching      // 匹配字符串\正则 matches string\Regular Expression
toMatch             // 字符串匹配器 a string matches a regular expression

====================更多More====================
toMatchSnapshot     //匹配最近的快照 matches the most recent snapshot

更多查阅Jest官网 (opens new window)

# 钩子函数

:::info BeforeAll

所有的测试用例执行之前执行的方法 :::

:::info AfterAll

所有的测试用例执行完后执行的方法,如果传入的回调函数返回值是 PromiseGeneratorJest 会等待 Promise Resolve 再继续执行 :::

:::info BeforeEach

BeforeEach 在每个测试完成之前都运行一遍 :::

:::info AfterEach

AfterAll 相比,AfterEach 在每个测试完成后都运行一遍 :::

# TDD开发

TDD (Test Driven Development)是一种开发流程, 在进行开发工作之前,编写测试,预先模拟欲测试的场景

  • 书写测试用例
  • 编写源码通过用例
  • 重构源码

# 测试覆盖率

代码覆盖率是一项指标,可以帮助您了解测试了多少源代码。这是一个非常有用的指标,可以帮助您评估测试套件的质量, 即检测测试是否全面

  • 函数覆盖率:已定义的函数中有多少被调用
  • 语句覆盖率:程序中有多少语句已执行
  • 分支覆盖率:控制结构的分支(例如 if 语句)中有多少已执行
  • 条件覆盖率:已经测试了多少布尔子表达式的真值和假值
  • 行覆盖率:已经测试了多少行源代码

这些指标通常表示为实际测试的项目数量、代码中找到的项目以及覆盖率百分比(测试的项目/找到的项目)

jestkarama 都是基于 istanbul 做的覆盖率检测

# Rollup

# 插件顺序

Rollup配置多个plugins的情况下, 插件默认是从前往后执行, 因此项目中的插件必须配置一定的顺序。如果后续的插件依赖前面的插件, 需要注意先后顺序。一般的工程中,都会使用commonjsresolve等等插件, 所以这几个插件的顺序如下

// resolve让项目支持使用node_modules中的模块
import resolve from '@rollup/plugin-node-resolve';
// rollup使用的是es6的模块化, 插件commonjs能够让项目能够支持使用cjs源码的npm库
import commonjs from '@rollup/plugin-commonjs';

export default {
	input: 'main.js',
	plugins: [
		resolve(),
		commonjs()
	],
	output: {
		file: 'bundle.js',
		format: 'cjs'
	}
}

# 常用插件

下面罗列项目中常用的插件及配置, 更多插件请关注官方推荐插件列表 (opens new window)

import path from 'path';
import alias from '@rollup/plugin-alias';
import commonjs from '@rollup/plugin-commonjs';
import postcss from 'rollup-plugin-postcss';
import serve from 'rollup-plugin-serve';
import livereload from 'rollup-plugin-livereload';
import del from 'rollup-plugin-delete';
import babel from '@rollup/plugin-babel';
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import replace from '@rollup/plugin-replace';
import legacy from '@rollup/plugin-legacy';
import json from '@rollup/plugin-json';
import eslint from '@rollup/plugin-eslint';
import image from '@rollup/plugin-image';


export default {

  input: './src/main.js', // 入口文件

  output: {
    file: './dist/bundle.js', // 打包后的存放文件
    //dir: './dist', // 多入口打包必需加上的属性
    format: 'cjs', // 输出格式 amd es6 iife umd cjs
    name: 'bundleName', // 如果iife,umd需要指定一个全局变量
    sourcemap: true, // 是否开启代码回溯
  },

  plugins: [
    replace({
      'process.env.NODE_ENV': JSON.stringify('production'),
      __buildDate__: () => JSON.stringify(new Date()),
      __buildVersion: 15
    }),

    // 支持从node_modules引入其他的包
    nodeResolve(),

    typescript({
      exclude: 'node_modules/**',
      include: 'src/**',
    }),

    // 支持common.js
    // 在 rollup 中引用 commonjs 规范的包。该插件的作用是将 commonjs 模块转成 es6 模块。
    commonjs({
      throwOnError: true,
    }),

    // 支持eslint
    eslint(),

    // 支持加载图片
    image(),

    // es6语法转义
    babel({
      exclude: 'node_modules/**',
      extensions: ['.js', '.jsx'],
      presets: ['@babel/preset-env', '@babel/preset-react'],
    }),

    // 让项目中可以导入json文件
    json(),

    // 支持加载css,添加前缀等
    postcss(),

    // 有时,您会发现旧时代的一段有用的代码片段,在像 npm 这样的新技术出现之前。
    // 这些脚本通常会将自己公开为var someLibrary = ...或window.someLibrary = ...,
    // 期望其他脚本将从全局命名空间获取对库的引用。
    // 将它们转换为模块通常很容易。但何苦呢?您只需添加legacy插件并进行相应配置,它就会自动变成模块。
    legacy({ 'vendor/some-library.js': 'someLibrary' }),

    // 打包前清空目标目录
    del({ targets: 'dist/*', verbose: true }),

    // 压缩js
    terser(),

    // 启动本地服务
    serve({
      contentBase: '', //服务器启动的文件夹,默认是项目根目录,需要在该文件下创建index.html
      port: 8020, //端口号,默认10001
    }),

    // watch目录,当目录中的文件发生变化时,刷新页面
    livereload('dist'),

    // 使用别名
    alias({
      entries: [{ find: '@', replacement: path.join(__dirname, 'src') }],
    }),
  ],
  // 告诉rollup不要将此lodash打包,而作为外部依赖,在使用该库时需要先安装相关依赖
  external: ['react']

};

# 组件按需加载

babel常见的按需加载的包有两个, babel-plugin-componentbabel-plugin-import。该插件在babel做代码转换时,通过读取AST并收集归属于特定的libraryName的有效imported,然后进行命名转换、生成组件和样式的import 代码、移除多余imported

两者大致的区别如下

  • babel-plugin-component 主要用于element-ui组件的按需加载, 与babel-plugin-import同一作者, 核心逻辑相同, 已不再维护
  • babel-plugin-import 兼容了多个组件库, 例如antdantd-mobilelodashmaterial-ui

# CSS Tree Shaking

Babel依靠AST技术完成对Javascript代码的遍历分析。 而在样式的世界中, PostCSS也起到了Babel的作用。

PostCSS提供了一个解析器, 能够将CSS解析成AST, 我们可以通过PostCSS插件对CSS对应的AST进行操作, 实现

Tree Shaking。这里主要记录在Webpack中如何配置CSS Tree Shaking

  • 安装依赖
npm i purgecss-webpack-plugin -D
  • 修改webpack配置文件
const path = require("path");
const glob = require("glob");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { PurgeCSSPlugin } = require("purgecss-webpack-plugin");

const PATHS = {
  src: path.join(__dirname, "src"),
};

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
    path: path.join(__dirname, "dist"),
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: "styles",
          test: /\.css$/,
          chunks: "all",
          enforce: true,
        },
      },
    },
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
    }),
    new PurgeCSSPlugin({
      paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
    }),
  ],
};

# JS Tree Shaking

Webpack中, 如果代码中包含副作用, 可以利用package.jsonsideEffects属性告诉工程化工具, 哪些模块具有副作用, 哪些模块没有副作用并可以被Tree Shaking优化

# 副作用声明

  • 表示全部模块均没有副作用
{
  "name": "my-package",
  "sideEffects": false
}
  • 表示部分模块具有副作用
{
  "name": "my-package",
  "sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}

# 不利于Tree Shaking情况

以下情况都不利于进行Tree Shaking处理

  • 导出一个包含多个属性和方法的对象
export default {
  add(a, b){
    return a + b
  },
  subtract(a, b){
    return a - b
  }
}
  • 导出一个包含多个属性和方法的类
export default class {
  add(a, b){
    return a + b
  }
  subtract(a, b){
    return a - b
  }
}
  • 使用export default方法导出
export default xxx

鉴于上述的情况, 更推荐遵循原子化和颗粒化原则导出

export function add(a, b){
  return a + b
}

export function subtract(a, b){
  return a - b
}

# append 和 appendChild

Element.append 方法在 Element的最后一个子节点之后插入一组 Node 对象或 DOMString 对象。被插入的 DOMString 对象等价为 Text (en-US) 节点

Node.appendChild() 的差异:

  • Element.append()允许追加 DOMString 对象,而 Node.appendChild() 只接受 Node 对象

  • Element.append() 没有返回值,而 Node.appendChild() 返回追加的 Node 对象

  • Element.append() 可以追加多个节点和字符串,而 Node.appendChild() 只能追加一个节点

# LocalStorage 和 SessionStorage

LocalStorageSessionStorage都是Web Storage API的一部分, 用于在浏览器中存储数据, 且存储都是关联到域名的

  • LocalStorage存储的数据没有过期时间, 除非手动清除, 否则一直存在

  • SessionStorage存储的数据在会话结束时会被清除, 会话结束指浏览器关闭或者标签页关闭

而征对于同一个域名, LocalStorageSessionStorage的区别如下

  • LocalStorage存储的数据在不同的Tab页面中是共享的, 即一个Tab页面中存储的数据, 另一个Tab页面可以访问到

  • SessionStorage存储的数据在不同的Tab页面中是不共享的, Tab页无法访问同源的其他Tab页数据

特殊情况

以下两种情况下打开的新Tab页, SessionStorage会被复制到新的Tab页面中

  • window.open打开新的同源Tab

  • <a href="samesite url" rel=”opener” target="_blank">new tab</>打开新的Tab

# 函数参数长度

一个 Function 对象的 length 属性表示函数期望的参数个数,即形参的个数

这个数字不包括剩余参数,只包括在第一个具有默认值的参数之前的参数

相比之下,arguments.length 是局限于函数内部的,它提供了实际传递给函数的参数个数

# ParseInt 和 Math.floor

两者都能获取小数的整数部分, 但是有以下不同点

  • Math.floor 无论正负, 均向下取最接近的整数。 取随机整数[min,max]场景, 只能用该方法

  • ParseInt对于负数, 会向上取整到最接近的整数。 对于正数, 会向下取整到最接近的整数

# Switch-Case非常规写法

本段落来记录Switch...Case的非常规写法

# 无Break语句

该场景下, Switch...Case会从第一个匹配的Case子语句开始, 执行后续所有的Case语句, 而不需要判断是否满足Case条件, 直到遇到Break语句或者Switch结束

应该特别注意连写Case的场景, 等同于多个条件的逻辑或运算

const fruittype = "Apples"
switch (fruittype) {
  case "Oranges":
    console.log("Oranges are $0.59 a pound.");
  case "Apples":
    console.log("Apples are $0.32 a pound.");
  case "Bananas":
    console.log("Bananas are $0.48 a pound.");
  case "Cherries":
    console.log("Cherries are $3.00 a pound.");
  // 逻辑上等同于if(fruittype === "Mangoes" || fruittype === "Papayas")
  case "Mangoes":
  case "Papayas":
    console.log("Mangoes and Papayas are $2.79 a pound.");
    break;
  default:
    console.log("Sorry, we are out of " + fruittype + ".");
}

// Apples are $0.32 a pound.
// Bananas are $0.48 a pound.
// Cherries are $3.00 a pound.
// Mangoes and Papayas are $2.79 a pound.

# 前置Default语句

该场景下, Switch...Case中, Default语句放在开头, 其效果与放最后没有区别, 在没找到匹配的Case语句时, 会执行Default语句

需要注意, Case后面的子语句可以加上块作用域的括号, 来避免变量冲突

const fruittype = "Apples"
switch (fruittype) {
  default: {
    console.log("Sorry, we are out of " + fruittype + ".");
    break;
  }
  case "Oranges": {
    console.log("Oranges are $0.59 a pound.");
    break;
  }
  case "Apples": {
    console.log("Apples are $0.32 a pound.");
    break;
  }
}
// Apples are $0.32 a pound.

# 网页位置DOM属性

网页位置中, 需要注意五类属性, 分别是WindowScreenElementEvent元素Document

# 屏幕尺寸 Screen

  • screen.width 屏幕宽度
  • screen.height 屏幕高度
  • screen.availWidth 屏幕可用宽度
  • screen.availHeight 屏幕可用高度

# 窗口尺寸 Window

  • window.screenTop 窗口顶部距屏幕顶部的距离
  • window.screenLeft 窗口左侧距屏幕左侧的距离
  • window.innerWidth 窗口中可视区域的宽度
  • window.innerHeight 窗口中可视区域的高度(与浏览器是否显示菜单栏等因素有关)
  • window.outerWidth 浏览器窗口本身的宽度(可视区域宽度+浏览器边框宽度)
  • window.outerHeight 浏览器窗口本身的高度(可是区域高度+浏览器菜单栏等高度)

# 元素尺寸 Element

此处使用div来表示某一个元素

client

  • div.clientWidth 在页面上返回元素的可视宽度(内容宽度+内边距)
  • div.clientHeight 在页面上返回元素的可视高度(内容高度+内边距)
  • div.offsetWidth 返回元素的宽度包括边框和填充(内容宽度+内边距+边框宽)
  • div.offsetHeight 返回元素的高度包括边框和填充(内容高度+内边距+边框高)
  • div.scrollWidth 返回元素的整个滚动宽度(包括带滚动条不可见的部分)
  • div.scrollHeight 返回元素的整个滚动高度(包括带滚动条不可见的部分)
  • div.scrollTop 当元素包含竖向滚动条时, 向下滚动后离开视野的区域高度
  • div.scrollLeft 当元素包含横向滚动条时, 向左滚动后离开视野的区域宽度

如何获取网页的绝对位置:

offset

function getElementLeft(element) {
  let actualLeft = element.offsetLeft;
  let current = element.offsetParent;

  while (current !== null) {
    actualLeft += current.offsetLeft;
    current = current.offsetParent;
  }

  return actualLeft;
}

function getElementTop(element) {
  let actualTop = element.offsetTop;
  let current = element.offsetParent;

  while (current !== null) {
    actualTop += current.offsetTop;
    current = current.offsetParent;
  }

  return actualTop;
}

// 方法一: 迭代计算
function getElementAbsoluteByIter(element) {
  return {
    absoluteTop: getElementTop(element),
    absoluteLeft: getElementLeft(element)
  }
}


// 方法二: 借助新的API-getBoundingClientRect
function getElementAbsolute(element) {
  const { top, left } = getBoundingClientRect(element)

  const scrollTop = element.scrollTop
  const scrollLeft = element.scrollLeft

  return {
    absoluteTop: top + scrollTop,
    absoluteLeft: left + scrollLeft
  };
}

如何获取网页的相对位置:

relative

可以使用getBoundingClientRect直接获取当前元素的相对位置

# 事件目标元素尺寸 Event

Event 代表事件的对象,包含了事件的具体信息。此处使用event代表事件对象参数

  • event.pageX 相对整个页面,以页面左上角为坐标原点到事件所在点的水平距离(IE8以上)
  • event.pageY 相对整个页面,以页面左上角为坐标原点到事件所在点的垂直距离(IE8以上)
  • event.clientX 相对可视区域,以可视区域左上角为坐标原点到事件所在点的水平距离
  • event.clientY 相对可视区域,以可视区域左上角为坐标原点到事件所在点的垂直距离
  • event.screenX 相对电脑屏幕,以屏幕左上角为坐标原点到事件所在点的水平距离
  • event.screenY 相对电脑屏幕,以屏幕左上角为坐标原点到事件所在点的垂直距离
  • event.offsetX 相对于自身,以自身的padding左上角为坐标原点到事件所在点的水平距离
  • event.offsetY 相对于自身,以自身的padding左上角为坐标原点到事件所在点的水平距离

# 根元素 Document

假如获取整个文档的尺寸, 究竟是使用document.documentElement还是document.body呢?

这就不得不提到doctype的概念了, doctypedocument type的缩写, 用于告诉浏览器当前文档的类型, 以便浏览器能够正确的渲染页面

在JS中可以使用document.compatMode来获取当前文档的doctype类型, 该属性有两个值

  • BackCompat 表示无doctype声明, 浏览器使用怪异模式渲染页面, 此时使用document.body获取宽高

  • CSS1Compat 表示有doctype声明, 浏览器使用标准模式渲染页面, 此时使用document.documentElement获取宽高

需要注意

  • safari 比较特别,有自己获取scrollTop的函数window.pageYOffset
  • 火狐等等相对标准些的浏览器就省心多了,直接用 document.documentElement.scrollTop

其实, document.body.scrollTopdocument.documentElement.scrollTop两者有个特点,就是同时只会有一个值生效

比如document.body.scrollTop能取到值的时候,document.documentElement.scrollTop就会始终为0;反之亦然

如果要得到网页的真正的scrollTop值

// 方法一
const scrollTop = document.body.scrollTop
  + document.documentElement.scrollTop;
const scrollLeft = document.body.scrollLeft
  + document.documentElement.scrollLeft;

// 方法二
const scrollLeft = Math.max(
  document.documentElement.scrollLeft,
  document.body.scrollLeft
);
const scrollTop = Math.max(
  document.documentElement.scrollTop,
  document.body.scrollTop
);

// 方法三
const scrollTop = document.documentElement.scrollTop
  || window.pageYOffset
  || document.body.scrollTop
  || 0;

# HTMLCollection、NodeList

上述两个属性都能获取元素的节点集合, 但是他们有一些需要注意的差异性

# NodeList

一般而言,NodeList 是一个静态集合,也就意味着随后对文档对象模型的任何改动都不会影响集合的内容。比如 document.querySelectorAll 就会返回一个静态 NodeList

特殊情况

在一些情况下,NodeList 是一个实时集合,也就是说,如果文档中的节点树发生变化,NodeList 也会随之变化。例如,Node.childNodes 是实时的:

# HTMLCollection

DOM 中的 HTMLCollection 是即时更新的(live);当其所包含的文档结构发生改变时,它会自动更新。因此,最好是创建副本(例如,使用 Array.from)后再迭代这个数组以添加、移动或删除 DOM 节点

# 差异性

  • NodeList 是一个静态集合,其不受 DOM 树元素变化的影响;相当于是 DOM 树快照,节点数量和类型的快照,就是对节点增删,NodeList 感觉不到。但是对节点内部内容修改,是可以感觉到的,比如修改 innerHTML

  • HTMLCollection动态绑定的,是一个的动态集合,DOM 树发生变化,HTMLCollection 也会随之变化,节点的增删是敏感的

  • 只有 NodeList 对象有包含属性节点和文本节点

  • HTMLCollection 元素可以通过 nameidindex 索引来获取。NodeList 只能通过 index 索引来获取

  • HTMLCollectionNodeList 本身无法使用数组的方法:poppushjoin 等。除非你把他转为一个数组