1. 1. http1.0, 1.1, 2.0, 3.0
  2. 2. 正则表达式$的含义
  3. 3. 操作符?? ?. ...
  4. 4. for forEach for...in for...of map的区别
    1. 4.1. 1. for 循环
    2. 4.2. 2. for...in 循环
    3. 4.3. 3. for...of 循环
    4. 4.4. 4. forEach() 方法
    5. 4.5. 5. map() 方法
  5. 5. 比较js数组中最大值
  6. 6. canvas画动画
  7. 7. 普通函数和箭头函数
  8. 8. css中后代、子、相邻兄弟选择器
  • 2023-11-03
    1. 1. /sockjs-node/info?t=${时间戳}
    2. 2. postinstall干什么的???
    3. 3. 数组扩展运算符
  • 2023-11-22
    1. 1. 事件循环
    2. 2. 如何理解js中的异步?
    3. 3. js中计时器能做到精准计时吗?
    4. 4. 浏览器的渲染原理
      1. 4.1. 1.解析HTML
      2. 4.2. 2、样式计算+ recalculate style
      3. 4.3. 3、布局-Layout
      4. 4.4. 4、分层
      5. 4.5. 5、绘制
      6. 4.6. 6、分块
      7. 4.7. 7.光栅化
      8. 4.8. 8.画
    5. 5. 什么是reflow?
    6. 6. 什么是repaint?
    7. 7. 为什么transform的效率高?
  • 2023-11-27
    1. 1. 1.灰阶滤镜
    2. 2. 2.js实现无限循环轮播图
    3. 3. 3.sticky定位
    4. 4. 4.visibilitychange 离开/进入标签页事件-页面可见度
    5. 5. 5.零宽字符
    6. 6. 6.求和
    7. 7. 7.文字描边text-stroke
    8. 8. 8.js类里创建私有字段
    9. 9. 9.下拉菜单的过渡效果
    10. 10. 10.跨域cors
      1. 10.1. 简单请求
      2. 10.2. 简单请求的交互规范
      3. 10.3. 需要预检的请求
      4. 10.4. 附带身份凭证的请求
      5. 10.5. 补充
    11. 11. 10.解析url中的参数
    12. 12. 11.生成随机颜色
    13. 13. 12.js的atob和btoa函数
    14. 14. 13.判断一个值是否是Promise Like?
    15. 15. 14.手写一个Promise
    16. 16. 15.语义化版本
    17. 17. 16.axios
    18. 18. 17.数据埋点
    19. 19. 18.web性能指标
    20. 20. 19.从视频文件中提取画面帧
    21. 21. 20.ESMoudle的工作原理
    22. 22. 21.defineProperty和proxy
    23. 23. 22.垃圾回收与内存泄露
    24. 24. 23.解析DOM树
    25. 25. 22. 大文件切片上传
    26. 26. 23.单点登录
    27. 27. JWT(token)
    28. 28. 24.LRU缓存置换算法
    29. 29. 25.判断是不是数组
      1. 29.1. 1.Object.prototype.toString.call([])
      2. 29.2. 2.[] instanceof Array
      3. 29.3. 3.Array.isArray([])
      4. 29.4. 4.使用对象的contructor属性
    30. 30. 26.Reflect的本质
    31. 31. 27.img的srcset、size
    32. 32. 28.es2023中的数组纯函数
    33. 33. 29.动态执行js
    34. 34. node模块查找策略
    35. 35. vue状态仓库持久化:vuex、pina
    36. 36. css选择器focus-within、has、 first-letter、selection
    37. 37. 文件上传、文件夹、拖拽上传
    38. 38. 各浏览器内核
    39. 39. vue组建命名规范
    40. 40. 浏览器自动播放策略
    41. 41. web api
    42. 42. 数字字面量
    43. 43. BigInt
    44. 44. 随机数
    45. 45. 数字格式化
    46. 46. 层叠规则
    47. 47. 布尔类型
    48. 48. 赋值运算
    49. 49. js数据类型和typeof
    50. 50. ssr服务端渲染
    51. 51. 作用域
    52. 52. 作用域链
    53. 53. webpack
    54. 54. 原型的作用是什么?
    55. 55. Promise解决了什么问题?
    56. 56. async await
  • vue3小记
    1. 1. 优点特性
    2. 2. setup
    3. 3. ref定义响应式对象
    4. 4. reactive定义对象类型的响应式对象
    5. 5. ref和reactive的对比
    6. 6. toRefs和toRef
    7. 7. computed计算属性
    8. 8. watch监听数据
    9. 9. watchEffect
    10. 10. 标签的ref属性
    11. 11. props属性
    12. 12. 生命周期
    13. 13. key的作用
  • vue3非兼容性改变
    1. 1. 全局API
    2. 2. 模版指令
    3. 3. 组件
    4. 4. 渲染函数
    5. 5. 自定义元素
    6. 6. 其他小改变
    7. 7. 被移除的API
  • vue2小记
    1. 1. vue特点
    2. 2. 模版语法
    3. 3. MVVM
    4. 4. defineProperty
    5. 5. vue中数据代理
    6. 6. 事件监听
    7. 7. scroll和wheel事件
    8. 8. computed计算属性
    9. 9. 侦听器watch
    10. 10. 条件渲染
    11. 11. v-if vs v-show
    12. 12. key的作用
    13. 13. 深入响应式原理
      1. 13.1. 如何追踪变化?
      2. 13.2. 检测变化的注意事项
      3. 13.3. 数组更新检测
      4. 13.4. 异步更新队列
    14. 14. 内置指令
    15. 15. 自定义指令
    16. 16. 组件生命周期
    17. 17. 组件化编程
    18. 18. vue的版本
    19. 19. 混入mixin
    20. 20. 插件
      1. 20.1. 使用插件
    21. 21. 函数式组件
    22. 22. 配置webpack
    23. 23. 组件传参/组件通信
    24. 24. 插槽
    25. 25. vuex
      1. 25.1. 核心概念
    26. 26. 栈Stack和堆Heap的区别?
    27. 27. js原型链
    28. 28. Ajax、xhr、fetch、axios、promise、jQuery的区别?
    29. 29. 内联元素和块级元素?
    30. 30. 浅拷贝和深拷贝?
    31. 31. while循环快还是for循环快?
    32. 32. 页面上隐藏元素方法?
    33. 33. 元素水平垂直居中?
    34. 34. Css元素选择器有哪些?
    35. 35. css可以继承的属性?
    36. 36. css预处理器?
    37. 37. JS组成?
    38. 38. js内置对象有哪些?
    39. 39. 操作数组的方法?
    40. 40. js类型检查方法?
    41. 41. 闭包的理解?
    42. 42. 事件委托?
    43. 43. 基本数据类型和引用数据类型的区别?
    44. 44. 原型链
    45. 45. new关键字的操作
    46. 46. js如何实现继承的?
    47. 47. js设计原理
    48. 48. js中的this指向
    49. 49. script标签的加载和执行
    50. 50. DOMContentLoaded和load事件
    51. 51. setTimeout和setInterval最小执行时间?
    52. 52. ES6和ES5的区别?
    53. 53. ES6新增哪些特性?
    54. 54. ES7新增哪些特性?
    55. 55. 箭头函数
    56. 56. call apply bind三者区别?
    57. 57. 递归遇到的问题?
    58. 58. ajax是什么?
    59. 59. get和post
    60. 60. Promise的内部原理是什么?
    61. 61. Promise和async/await的区别?
    62. 62. 浏览器常用存储方式有哪些?
    63. 63. token存储在哪里?
    64. 64. DOM树和渲染树有什么区别?
    65. 65. 精灵图和base64区别?
    66. 66. SVG
    67. 67. npm的底层环境是什么?
    68. 68. http协议的协议头和请求头有什么?
    69. 69. 浏览器缓存策略
    70. 70. 跨域
    71. 71. 防抖和节流
    72. 72. JSON
    73. 73. 当数据没有请求过来时该怎么做
    74. 74. 无感登录
    75. 75. 前端工程化
      1. 75.1. 模块化和包管理
      2. 75.2. JS工具链
    76. 76. html语义化的理解
    77. 77. html5的新特性
    78. 78. css3新特性
    79. 79. 解决了哪些移动端的兼容问题?
    80. 80. v-for中key的作用
    81. 81. created和mounted请求数据有什么区别
    82. 82. keep-alive
    83. 83. element-ui怎么进行表单验证
    84. 84. 封装axios
    85. 85. vue路由传参方式?
    86. 86. vue的路由hash模式和history模式的区别?
    87. 87. 路由拦截是怎么实现的?
    88. 88. vue的动态路由
    89. 89. 如何避免刷新页面二次加载路由
    90. 90. vuex刷新数据会丢失吗
    91. 91. watch和computed有什么区别?
    92. 92. vuex什么场景使用,属性有哪些?
    93. 93. 资源提示符
    94. 94. vue的双向数据绑定的原理
    95. 95. diff算法和虚拟dom
    96. 96. a链接下载文件
    97. 97. 设计私有属性
    98. 98. vuex是如何实现响应式的?
    99. 99. 如何封装一个组件
    100. 100. 如何搭建一个脚手架?
    101. 101. 如何封装一个可复用的组件?
    102. 102. 首屏优化怎么做?
    103. 103. vue3性能为什么比vue2好?
    104. 104. vue3为什么使用proxy?
    105. 105. 对组件的理解
    106. 106. nuxtjs
    107. 107. 渲染方式
    108. 108. 工作中遇到的难题
    109. 109. will-change
    110. 110. 小程序
    111. 111. 小程序底层
    112. 112. 举例说明行内元素、块级元素和空元素,并解释其特点
    113. 113. 如何实现响应式布局?介绍几种常见的方法和技术
    114. 114. 实现数组去重的方法有哪些?
    115. 115. 请解释前端资源懒加载和预加载的策略
    116. 116. 如何配置Webpack进行项目构建?列举几个常用的Loader和Plugin
    117. 117. 前端性能优化
    118. 118. 组件封装之messageBox
    119. 119. 怎样排查页面加载慢?
    120. 120. js对象的原始值转换
    121. 121. 拖拽元素
    122. 122. 大屏数据如何实现实时更新?
    123. 123. 函数参数和默认值
    124. 124. css属性值的计算过程
    125. 125. 类型转换规则
    126. 126. js运算规则
  • 日常随记

    http1.0, 1.1, 2.0, 3.0

    1、http1.0:

    • 短连接:一个tcp连接只能发送一个请求,接收到响应后立即关闭连接

    2、http1.1

    • 持久连接:一个连接可以处理多个连接而不用重新建立连接
    • 管道传输:同一个连接可同时发送多个请求
    • 队头阻塞:使用请求-应答模式,前一个请求未完成后面的请求必须等待

    3、http2.0

    • 二进制分帧(Binary Framing):将HTTP消息分解为多个帧,极大地降低了延迟
    • 多路复用:在一个TCP连接上并发处理多个请求和响应
    • 首部压缩(Header Compression):减少请求和响应中的冗余首部数据
    • 服务器推送(Server Push):服务器可以在客户端请求之前主动推送资源,提高加载速度

    在HTTP/2中,引入了多路复用(Multiplexing)机制,通过二进制分帧(Binary Framing)技术,将HTTP消息分解为多个独立的帧,每个帧都可以交错发送和接收,不再受到顺序限制。这样一来,即便某一请求的响应由于某种原因被延迟,其它请求也可以继续在同一个TCP连接上并发处理和传输,从而大幅减少了队头阻塞带来的延迟问题。

    然而,HTTP/2并未完全消除队头阻塞,因为在同一TCP流内的帧依然受到排序约束。如果一个关键帧(比如SETTINGS帧或流优先级高的帧)丢失或延迟,依然可能影响到同一TCP连接上的其他帧。不过,HTTP/2通过优先级和流控机制能够在一定程度上减轻这一影响

    4、http3.0

    • 基于QUIC协议:HTTP/3放弃了使用TCP作为传输层协议,转而采用基于UDP的QUIC协议,实现了更快的连接建立和数据传输。
    • 多路复用与无阻塞,QUIC能有效解决HTTP/2中存在的队头阻塞

    正则表达式$的含义

    在JavaScript正则表达式中,$符号有以下几种含义:

    1. $位于正则表达式的末尾,表示匹配输入字符串的结尾位置。
    1
    2
    3
    const pattern = /abc$/;
    console.log(pattern.test("123abc")); // true
    console.log(pattern.test("abcdef")); // false
    1. $有时也可以作为转义字符,用于匹配以特殊字符开头的字符串。例如,\$可以匹配$字符本身。
    1
    2
    3
    const pattern = /\$$/;
    console.log(pattern.test("123$")); // true
    console.log(pattern.test("123"); // false
    1. 捕获组引用:在正则表达式中,$还可以用来引用之前定义的捕获组。例如,正则表达式/(abc)(def)$/中,$2代表的是第二个捕获组的内容,即”def”。这种情况下,$符号实际上是用来引用前面定义的捕获组的,而不是用作锚定符.

    创建正则表达式:
    1、直接量let reg = /abc/
    2、构造函数let reg = new RegExp(‘abc’),第二个参数可选,是表达式的标志,“g”,”i”,”m”或它们的组合

    • \n:换行
    • \t:制表符
    • \r:回车
    • [abc]:匹配括号内的a|b|c|abc|ab|ac|bc
    • [^...]:匹配不在括号内的字符
    • .:匹配换行符或其他终止符
    • \s:匹配空白符
    • \S:非空白符
    • \d:匹配任意数字[0-9]
    • \D:匹配非数字
    • \w:匹配任意单字符[a-zA-Z0-9]
    • \W:匹配任意非单字符[^a-zA-Z0-9]
    • {n,m}:最少n次,最多m次
    • {n,}:最少n次或多次
    • {n}:正好n次
    • ?:匹配0次或1次{0,1}
    • +:匹配1次或多次{1,}
    • *:匹配0次或多次{0,}
    • i:匹配忽略大小写
    • g:全局匹配
    • m:多行模式
    • ^:匹配字符串的开头
    • $:匹配字符串的结尾
    • \b:匹配单词边界
    • \B:非单词边界

    操作符?? ?. ...

    ??:空值合并操作符
    ?? 是一个逻辑操作符,当左侧的操作数为 null 或 undefined 时,它返回其右侧的操作数,否则返回左侧的操作数。

    ?.可选链操作符
    允许你读取位于连接对象链深处的属性的值,而不必显式地验证链中的每个引用是否有效。?. 运算符的功能类似于 . 链式运算符,不同之处在于,在引用为空 (nullish ) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined。

    ??=逻辑空赋值
    逻辑空赋值运算符(x ??= y)仅在 x 是空值(null 或 undefined)时对其赋值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const a = { duration: 50 };

    a.duration ??= 10;
    console.log(a.duration);
    // Expected output: 50

    a.speed ??= 25;
    console.log(a.speed);
    // Expected output: 25

    ...扩展操作符
    可以用于数组或对象,用于将数组或对象的元素/属性展开。

    数组中的扩展操作符:用于将数组元素展开到新的数组或现有数组中
    对象中的扩展操作符:用于将对象的所有可枚举属性复制到新对象。

    for forEach for...in for...of map的区别

    1. for 循环

    1
    2
    3
    for (let i = 0; i < array.length; i++) {
    console.log(array[i]);
    }
    • 用途: 最通用的循环结构,适用于任何需要基于索引遍历数组或其他可迭代对象的情况。
    • 语法: 需要初始化变量、定义终止条件和更新循环变量的步骤。
    • 控制: 支持 break, continue, return 等控制流语句。
    • 适用对象: 数组和其他可通过索引访问元素的数据结构。
    • 性能: 一般情况下,对于简单的循环逻辑,for 循环的性能相对较高。

    2. for...in 循环

    1
    2
    3
    4
    5
    for (let key in object) {
    if (object.hasOwnProperty(key)) {
    console.log(object[key]);
    }
    }
    • 用途: 主要用于遍历对象的可枚举属性(包括自有属性和继承自原型链的属性)。
    • 语法: 直接遍历对象的键,而非值。通常需要配合 hasOwnProperty()Object.prototype.hasOwnProperty.call() 来确保只访问对象自身的属性。
    • 控制: 支持 break, continue, return 等控制流语句。
    • 适用对象: 对象。
    • 注意: 不适用于数组,除非目的是遍历其索引(而不是元素值)。由于可能包含原型链上的属性,使用时需谨慎过滤。

    3. for...of 循环

    1
    2
    3
    for (let value of array) {
    console.log(value);
    }
    • 用途: 遍历可迭代对象(包括数组、Set、Map、字符串、生成器对象等)的
    • 语法: 直接访问每个元素的值,无需关心索引。
    • 控制: 支持 break, continue, return 等控制流语句。
    • 适用对象: 数组、Set、Map、字符串、Generator对象以及任何实现了Symbol.iterator接口的对象。
    • ES6引入: 是ES6引入的新特性,提供了一种更简洁、直观的遍历方式。

    4. forEach() 方法

    1
    2
    3
    array.forEach((value, index, array) => {
    console.log(value);
    });

    forEach(callbackFn, thisArg?),thisArg执行 callbackFn 时用作 this 的值

    • 用途: 遍历数组的所有元素,调用一个给定的回调函数。
    • 语法: 无需管理索引,直接访问值。提供三个参数:当前值、索引和原始数组。
    • 控制: 支持 break, continue, return 等控制流语句中断循环。
    • 适用对象: 仅适用于数组。(Map,Set,NodeList的原型都有forEach方法)
    • 返回值: undefined,无返回值。

    5. map() 方法

    1
    2
    3
    const newArray = array.map((value, index, array) => {
    return value * 2;
    });

    map(callbackFn, thisArg?),thisArg执行 callbackFn 时用作 this 的值

    • 用途: 创建一个新数组,其结果是通过遍历原数组每个元素并应用一个提供的函数得到的。
    • 语法: 提供三个参数:当前值、索引和原始数组。回调函数应返回应用于新数组的值。
    • 控制: 支持 break, continue, return 等控制流语句中断循环。
    • 适用对象: 仅适用于数组。
    • 返回值: 返回一个新的数组,包含经过函数处理后的值。可链式调用

    总结:

    循环结构 用途 控制流 适用对象 特性
    for 通用循环 支持 数组及其他可索引 自定义迭代逻辑,高性能
    for...in 遍历对象属性 支持 对象 访问键,需注意原型链污染
    for...of 遍历可迭代对象的值 支持 可迭代对象 直接访问值,ES6引入
    forEach 遍历数组 不支持 数组 简洁,不支持循环中断
    map 转换数组 不支持 数组 创建新数组,返回转换后元素结果

    比较js数组中最大值

    方法一:使用 Math.max() 与 apply()
    你可以使用 Math.max() 函数与 apply() 方法来找到数组中的最大值。Math.max() 函数接受任意数量的参数,并返回其中的最大值,而 apply() 方法可以用来调用一个函数,其参数以数组(或类似数组对象)的形式传入。

    1
    2
    3
    let array = [1, 2, 3, 4, 5];  
    let max = Math.max.apply(null, array);
    console.log(max); // 输出: 5

    方法二:使用扩展运算符(Spread Operator)
    如果你使用的是ES6或更高版本,你可以使用扩展运算符 … 来代替 apply() 方法。扩展运算符可以将数组的元素展开成单独的参数。

    1
    2
    3
    let array = [1, 2, 3, 4, 5];  
    let max = Math.max(...array);
    console.log(max); // 输出: 5

    方法三:使用 reduce() 方法
    reduce() 方法可以对数组中的每个元素执行一个由你提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

    1
    2
    3
    4
    5
    let array = [1, 2, 3, 4, 5];  
    let max = array.reduce(function(a, b) {
    return Math.max(a, b);
    });
    console.log(max); // 输出: 5

    方法四:使用循环
    如果你不想使用数组方法或扩展运算符,你也可以简单地使用一个循环来遍历数组并找到最大值。

    1
    2
    3
    4
    5
    6
    7
    8
    let array = [1, 2, 3, 4, 5];  
    let max = array[0];
    for (let i = 1; i < array.length; i++) {
    if (array[i] > max) {
    max = array[i];
    }
    }
    console.log(max); // 输出: 5

    数组的reduce函数:接收一个累加器从左到右计算为一个值
    arr.reduce(function(accumulator, currentValue, currentIndex, array){}, initialValue, thisArg)
    参数说明:

    • accumulator(累加器):累积器累积回调函数的返回值;它是上一次调用回调时返回的累积值。
    • currentValue(当前值):数组中正在处理的元素。
    • currentIndex(当前索引):可选参数,数组中正在处理的当前元素的索引。如果提供了initialValue参数,则索引为0,否则索引为1。
    • array(数组):调用reduce()方法的数组。
    • initialValue(初始值):可选参数。作为第一次调用callbackFunction时的第一个参数的值。如果没有提供初始值,则将使用数组中的第一个元素。在没有初始值的空数组上调用reduce将报错.

    此外,reduce()方法还可以接受一个可选的thisArg参数,它用于在执行回调函数时作为this的值。这在某些情况下可能很有用,特别是当回调函数内部需要访问某个对象的属性或方法时。

    canvas画动画

    在HTML5的Canvas上画动画,你需要使用JavaScript的requestAnimationFrame函数,它可以让你创建平滑的动画。以下是一个简单的示例,它会在Canvas上画一个移动的矩形:

    首先,你需要在HTML中创建一个Canvas元素:

    1
    2
    3
    4
    5
    6
    7
    8
    <!DOCTYPE html>  
    <html>
    <body>
    <canvas id="myCanvas" width="500" height="500" style="border:1px solid #d3d3d3;">
    Your browser does not support the HTML5 canvas tag.
    </canvas>
    </body>
    </html>

    然后,你可以使用JavaScript来绘制动画:

    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
    // 获取Canvas元素和绘图上下文  
    let canvas = document.getElementById('myCanvas');
    let ctx = canvas.getContext('2d');

    // 定义矩形的初始位置
    let x = canvas.width / 2;
    let y = canvas.height - 30;
    let speed = 2;

    // 绘制矩形的函数
    function drawRectangle() {
    ctx.clearRect(0, 0, canvas.width, canvas.height); // 清除画布
    ctx.fillStyle = 'green'; // 设置填充颜色
    ctx.fillRect(x, y, 10, 10); // 绘制矩形
    }

    // 更新矩形位置的函数
    function update() {
    x += speed; // 更新矩形的位置
    if (x + 10 > canvas.width || x < 0) { // 如果矩形碰到画布的边界,则反向移动
    speed = -speed;
    }
    drawRectangle(); // 绘制矩形
    requestAnimationFrame(update); // 请求下一帧动画
    }

    // 开始动画
    update();

    这个示例中,drawRectangle函数负责在Canvas上绘制一个绿色的矩形,update函数则负责更新矩形的位置并请求下一帧动画。requestAnimationFrame(update)会在下一次浏览器重绘之前调用update函数,从而创建平滑的动画效果。当矩形碰到画布的边界时,我们改变其移动方向,使其能够来回移动。

    普通函数和箭头函数

    普通函数和箭头函数在JavaScript中有一些关键的区别,这些区别主要体现在它们的定义方式、this指向、参数处理以及功能特性上。

    • 定义方式:箭头函数使用箭头(=>)来定义,语法更为简洁。普通函数则使用function关键字进行定义。箭头函数通常用于定义匿名函数或回调函数。
    • this指向:在普通函数中,this的指向是动态的,它取决于函数的调用方式。在全局环境下,this指向全局对象(在浏览器中是window对象);在对象的方法中,this指向调用该方法的对象;在构造函数中,this指向新创建的对象实例。然而,箭头函数并不绑定自己的this,箭头函数中的this始终指向定义它时的上下文,即父级执行上下文中的this。这使得箭头函数在处理this指向时更为简单和直观。
    • 参数处理:箭头函数在参数处理上有一些特殊的规则。如果箭头函数只有一个参数,可以省略小括号;如果箭头函数体只有一条返回语句,可以省略大括号和return关键字。这些特性使得箭头函数在语法上更加简洁。
    • 功能特性:箭头函数没有内置对象arguments,而每个普通函数调用后都具有一个arguments对象,用来存储实际传递的参数。此外,箭头函数没有原型对象(prototype),不能用作构造函数,也不能通过call()或apply()方法来改变this的指向。这些特性使得箭头函数在某些使用场景下可能不如普通函数灵活。

    css中后代、子、相邻兄弟选择器

    .a .b
    这是一个后代选择器。它选择所有 .b 类的元素,只要它们是 .a 类元素的子元素(直接的或间接的)。

    例如:

    1
    2
    3
    4
    5
    6
    7
    <div class="a">  
    <div class="b">我会被选中</div>
    <p>
    <div class="b">我也会被选中</div>
    </p>
    </div>
    <div class="b">我不会被选中</div>

    .a > .b
    这是一个子选择器。它选择所有 .b 类的元素,这些元素必须是 .a 类元素的直接子元素。

    例如:

    1
    2
    3
    4
    5
    6
    <div class="a">  
    <div class="b">我会被选中</div>
    <p>
    <div class="b">我不会被选中</div>
    </p>
    </div>

    .a + .b
    这是一个相邻兄弟选择器。它选择所有 .b 类的元素,这些元素紧接在 .a 类元素之后,并且它们共享相同的父元素。

    例如:

    1
    2
    3
    4
    5
    <div>  
    <div class="a">我是a</div>
    <div class="b">我会被选中,因为我是a的相邻兄弟</div>
    <div class="b">我不会被选中</div>
    </div>

    所以,当这三个选择器用逗号 , 分隔时,它们会分别选择满足各自条件的元素,并对这些元素应用相同的样式规则。

    .a ~ .b
    是一个通用兄弟选择器.这个选择器会选择所有 .b 类的元素,这些元素是 .a 类的元素之后的所有兄弟元素,并且它们共享相同的父元素

    1
    2
    3
    4
    5
    6
    7
    8
    <div>  
    <div class="a">我是a</div>
    <p>这是一个段落。</p>
    <div class="b">我会被选中,因为我是a之后的兄弟元素。</div>
    <div class="b">我也会被选中,因为我也是a之后的兄弟元素。</div>
    <div>另一个没有类的div元素</div>
    <div class="b">我同样会被选中,即使前面有其他类型的兄弟元素。</div>
    </div>

    2023-11-03

    /sockjs-node/info?t=${时间戳}

    当运行npm run dev启动项目后,在浏览器的network中会自动持续发送/sockjs-node/info?t=${时间戳}请求。

    sockjs-node是什么?
    sockjs-node是一个JavaScript库,提供跨浏览器的API,创建了一个低延迟、全双工的浏览器和web服务器之间的通道。
    在项目运行后,会一直调用该接口。
    SockJS是一个JavaScript库。提供类似于websocket的对象。其作用就是在开发环境下,保证我们在改完代码重写编译后,能够通知浏览器重新加载变更结果。

    这里理解为这个库用来让浏览器和本地之间热更新通信的。

    如果两个地址对接不上,或者更换wifi等都会报错:sockjs-node/info?t= net::ERR_CONNECTION_TIMED_OUT

    需要关闭该请求可以找到node_modules/sockjs-client/dist/sockjs.js ,注释代码的第1603行:

    1
    2
    3
    4
    5
    6
    try {
    // self.xhr.send(payload); 注释掉该行
    } catch (e) {
    self.emit('finish', 0, '');
    self._cleanup(false);
    }

    以上只在本地有效,重新安装依赖或者别的同事都需要重新修改,可添加脚本,当npm install后替换掉node_modules/sockjs-client/dist/sockjs.js该文件

    1.在根目录下新建该文件lib/sockjs.js,拷贝node_modules/sockjs-client/dist/sockjs.js文件并注释掉1603行

    2.在根目录下创建installSockjs.js文件,将node_modules对应文件替换为lib/sockjs.js文件

    3.在package.json的scripts中添加脚本命令:

    1
    2
    3
    4
    "scripts": {
    ...
    "postinstall: node installSockjs.js"
    }

    4.最后npm install即可,但是这样作热更新没有了。

    postinstall干什么的???

    postinstall是安装完某个包后自动执行的脚本。可以在package.json添加postinstall设置执行的脚本命令,帮我们做一些额外的处理,例如复制文件、创建目录、执行脚本、打印信息等。

    例如:将 dist 目录下的所有文件复制到 public 目录下。

    1
    2
    3
    "scripts:" {
    "postinstall": "cp -r dist/* public/"
    }

    数组扩展运算符

    数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。

    2023-11-22

    事件循环

    事件循环:浏览器的核心

    进程:

    何为进程:程序运行需要有他自己专有的内存空间,可以把这块内存空间简单的称为进程

    每个应用至少有一个进程,进程之间相互独立,即使通信也要双方同意。

    何为线程:
    有了进程后,就可以运行程序,运行程序的称为线程。
    一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行代码,这个线程称为主线程。
    如果程序需要同时运行多块代码,主线程会启动更多的线程来运行代码。所以一个进程可以包含多个线程。

    浏览器有哪些进程和线程?

    浏览器是一个多进程多线程的应用程序。

    浏览器内部及其复杂,为了避免互相影响,减少崩溃的几率,启动浏览器后,会启动多个进程。

    浏览器进程、网络进程、渲染进程 等

    浏览器主要进程有:

    浏览器进程:
    负责界面显示、用户交互、子进程管理等,浏览器进程会开启多个线程执行任务。

    网络进程:
    负责加载网络资源。网络进程内部会开启多个线程来执行不同的网络任务。

    渲染进程:
    渲染进程开启后,会启动一个渲染主线程,主线程负责html、css、js代码。

    默认情况下,浏览器为每一个标签页开启一个新的渲染进程,保证不同标签页互不影响。

    事件循环发生在渲染主线程。

    渲染主线程是如何工作的?

    渲染主线程是浏览器最繁忙的线程,需要它处理的任务包括但不限于:

    • 解析html
    • 解析css
    • 计算样式
    • 布局
    • 处理图层
    • 每秒把页面画60次
    • 执行全局js代码
    • 处理事件监听函数
    • 执行定时器的回调函数

    渲染主线程如何调度任务呢?排队

    1、最开始的时候,渲染主线程进入一个无限循环
    2、每一次循环会检查事件队列(消息队列)是否有任务存在,如果有,取出第一个任务在主线程执行,执行完后进入下一个循环;如果没有则进入休眠
    3、其他所有线程(包括其他进程的线程)可以随时向事件队列添加任务,新任务会添加到事件队列的末尾。在添加新任务时如果主线程处在休眠状态则会唤醒以继续循环拿取任务。

    以上整个过程称为事件循环。

    何为异步?

    代码在执行过程中,会遇到无法立即处理的任务,比如:

    • 计时完成后需要执行的任务:setTimeout setInterval
    • 网络通信完成后需要执行的任务:xhr fetch
    • 用户操作后要执行的任务:addEventListener

    如果让渲染主线程等待这些任务的时机到达,就会导致主线程阻塞,造成浏览器卡死。因此浏览器选择异步来解决问题。

    比如:渲染主线程遇到计时器,通知计时线程去计时,结束该任务,继续循环从事件队列拿取任务,计时线程计时结束后将回调函数包装成任务放进事件队列末尾。

    使用异步的方式,渲染主线程永不阻塞。

    如何理解js中的异步?

    js是一门单线程的语言,运行在浏览器的渲染主线程内,渲染主线程只有一个,承担很多任务,比如解析html、css、计算样式、布局、执行js代码等。如果使用同步的方式就有可能阻塞主线程,导致事件队列的其他任务不能执行,造成渲染主线程白白浪费,页面无法更新,给用户造成卡死现象。
    所以浏览器使用异步的方式来避免。具体做法是某些任务发生时,如计时器、网络、事件监听,主线程交给其他线程去处理,自身立即结束任务,执行后续代码。当其他线程执行结束时,就将事先传递回调函数包装成任务加入到事件队列末尾,等待主线程调度执行。
    在这种异步模式下,浏览器永不阻塞,最大限度的保证了单线程的流畅运行。

    js为何会阻碍渲染?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let h1 = document.getElementsByTagName('h1')[0]
    let btn = document.getElementsByTagName('button')[0]
    function delay(duration) {
    let start = Date.now()
    while(Date.now() + start < duration) {}
    }
    btn.onclick = function(e) {
    h1.textContent = 'hahah'
    console.log('onclick')
    delay(3000)
    }

    以上代码点击按钮时,会打印onclick,延时3秒后h1显示‘hahah’,因为delay方法占据主线程运行3秒,h1.textContent绘制需要事件队列排队等待,造成页面阻塞。

    任务有优先级吗?
    任务没有优先级,在队列中先进先出
    但是事件队列是有优先级的。
    根据w3c最新解释:

    • 每个任务都有一个类型,同一个类型的任务必须在同一个队列,不同类型的任务可以分属于不同的队列。再一次事件循环中,浏览器可以根据实际情况从不同的队列取出任务执行。
    • 浏览器必须有一个微队列,微队列中的任务优先于其他任务执行

    随着浏览器的复杂度上升,w3c不再使用宏队列的说法

    在目前chrome的实现中,至少包含以下队列:

    • 延时队列:用于存放计时器的回调任务,优先级【中】
    • 交互队列:用于存放用户操作后的回调任务,优先级【高】
    • 微队列:用户存放最快执行的任务,优先级【最高】

    添加到微队列的方式主要是Promise、MutationObserver、process.nextTick
    例如:

    1
    2
    立即把一个函数添加到微队列
    Promise.resolve().then(函数)

    示例1:

    1
    2
    3
    4
    setTimeout(function() {
    console.log(1) // fn1
    }, 0)
    console.log(2)

    输出: 2、1

    主线程:全局js
    微队列:
    延时队列:fn1
    交互队列:

    解析:主线程执行全局js,遇到setTimeout,计时线程0秒后将fn1放入延时队列-》继续执行全局js-》输出2-》全局js结束,主线程空,微队列空,执行延时队列fn1-》输出1

    示例2:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function delay(duration) {
    let start = Date.now()
    while(Date.now() + start < duration) {}
    }
    setTimeout(() => {
    console.log(1)
    }, 0)
    delay(3000)
    console.log(2)

    输出:等待3秒、2、1

    解析:主线程执行全局js,遇到setTimeout,计时线程0秒后将fn1放入延时队列-》继续执行全局js-》执行delay函数3秒-》输出2-》全局js结束,主线程空,微队列空,执行延时队列fn1-》输出1

    示例3:

    1
    2
    3
    4
    5
    6
    7
    setTimeout(() => {
    console.log(1) // 回调fn1
    }, 0)
    Promise.resolve().then(function() {
    console.log(2) // 回调fn2
    })
    console.log(3)

    输出:3、2、1

    主线程:全局js
    微队列:fn2
    延时队列:fn1
    交互队列:

    解析:
    主线程执行全局js,遇到setTimeout,计时线程0秒后将fn1放入延时队列
    继续全局js执行,遇Promise,立即将fn2添加到微队列
    继续全局js,输出3
    全局js结束,主线程空,微队列不为空,执行fn2
    输出2,fn2结束,继续循环
    微队列空,延迟队列不为空,执行fn1
    输出1,fn1结束,继续循环

    示例4:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function a() {
    console.log(1)
    Promise.resolve().then(function() {
    console.log(2) // fn2
    })
    }
    setTimeout(() => { // fn3
    console.log(3)
    Promise.resolve().then(a) // fn(a)
    }, 0)
    Promise.resolve().then(function() {
    console.log(4) // fn4
    })
    console.log(5)

    输出:5、4、3、1、2

    主线程:全局js
    微队列:fn4->fn(a)->fn2
    延时队列:fn3
    交互队列:

    解析:
    主线程执行全局js,遇到setTimeout,计时线程0秒后将fn3放入延时队列
    继续全局js执行,遇Promise,立即将fn4添加到微队列
    继续全局js,输出5
    全局js结束,主线程空,微队列不为空,执行fn4
    输出4,结束任务,继续循环,微队列空,查询延时队列不为空
    执行fn3,输出3、遇Promise,立即将fn(a)添加到微队列,结束该任务,继续循环
    微队列不空,执行fn(a),输出1,遇Promise,立即将fn2添加到微队列,结束该任务,继续循环
    微队列不空,执行fn2,输出2,结束该任务

    示例5:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function a() {
    console.log(1)
    Promise.resolve().then(function() {
    console.log(2) // fn2
    })
    }
    setTimeout(() => {
    console.log(3) // fn3
    }, 0)
    Promise.resolve().then(a) // fn(a)
    console.log(4)

    输出:4、1、2、3

    主线程:全局js
    微队列:fn(a)->fn2
    延时队列:fn3
    交互队列:

    解析:
    主线程执行全局js,遇到setTimeout,计时线程0秒后将fn3放入延时队列
    继续全局js执行,遇Promise,立即将fn(a)添加到微队列
    继续全局js,输出4
    全局js结束,主线程空,微队列不为空,执行fn(a)
    输出1,遇Promise,立即将fn2添加到微队列,结束该任务,继续循环
    微队列不空,执行fn2,输出2,结束该任务,继续循环
    微队列空,延时队列不空,执行fn3,输出3,结束该任务

    示例6:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    async function asy1() {
    console.log(1)
    await asy2()
    console.log(2)
    }

    const asy2 =async () => {
    await setTimeout(() => {
    Promise.resolve().then(() => {
    console.log(3)
    });
    console.log(4)
    })
    }

    const asy3 = async () => {
    Promise.resolve().then(() => {
    console.log(6)
    });
    }
    asy1()
    console.log(7)
    asy3()

    await函数的作用,等待后面promise完成,将await后面行代码推入到微队列,如果后面没有代码,将函数完成推入到微队列。

    解析:1,7,6,2,4,3
    宏任务:setTimeout
    微任务:asy2完成, 6,2, 3
    运行asy1,打印1,await asy2,0秒后将setTimeout放入宏队列,setTimeout返回一个数字,await完成,将asy2完成推入微队列(后面没有代码)
    继续同步代码,打印7, 执行asy3,将打印6推入微队列
    同步代码执行完毕,事件循环从微队列选取任务执行,asy2完成后asy1中await asy2也完成,将asy2后面代码打印2推入微队列
    继续微队列,打印6, 打印2,微队列执行完毕
    执行宏任务,将打印3推入微队列,同步执行打印4代码
    最后执行微任务打印3

    示例7:

    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
    // h: 5,   微:3, 7, 
    // 4, 2, 1, 6, 8, 3, 7, 5
    async function foo() {
    console.log(1)
    }

    async function bar() {
    console.log(2)
    await foo()
    console.log(3)
    }

    console.log(4)

    setTimeout(function () {
    console.log(5)
    }, 0)

    bar()

    new Promise(function (resolve) {
    console.log(6)
    resolve()
    }).then(function () {
    console.log(7)
    })

    console.log(8)

    阐述js的事件循环:

    事件循环又叫做消息循环,是浏览器的渲染主线程的工作方式。

    在chrome中开启一个无限循环,每次循环从队列中取出第一个任务执行,而其他线程只需要在合适的时机将任务添加到队列末尾。

    在事件循环中,任务通常分为微任务和宏任务。微任务包括Promise.then、async/await、process.nextTick和queueMicrotask等,而宏任务则包括定时器(setTimeout、setInterval、requestAnimationFrame)、事件绑定、ajax回调函数以及Node.js的fs模块等。在执行顺序上,主任务会首先执行,然后是微任务,最后是宏任务。如果在执行微任务的过程中又产生了新的微任务,那么这些新的微任务会在当前轮次的微任务全部执行完毕后才会执行。

    过去把消息队列分为宏队列和微队列,这种说法无法满足复杂的浏览器环境,取而代之的是另一钟灵活的处理方式。

    根据w3c官方解释,每个任务都有一个类型,同一个类型的任务必须在同一个队列,不同任务属于不同的队列。
    不同的任务队列有不同的优先级,在一次事件循环中,浏览器自行决定选取哪一个队列的任务。但浏览器必须有一个微队列,微队列具有最高的优先级,必须优先调度执行。

    js中计时器能做到精准计时吗?

    不能,因为:

    • 计算机硬件没有原子钟,无法做到精准计时
    • 操作系统的计时函数本身就有偏差,js计时器最终调用的是操作系统的函数,所以携带了偏差
    • 按照w3c的标准,浏览器实现计时器,嵌套超过5层,则会带有4毫秒的最小偏差,计时少于4毫秒又带来偏差
    • 受事件循环的影响,计时器的回调函数只能在主线程空闲时执行,因此带来偏差

    单线程是异步产生的原因
    事件循环是异步的实现方式

    浏览器的渲染原理

    当浏览器的网络线程收到HTML文档后,会产生一个渲染任务,并将其传递到渲染主线程的消息队列。
    在事件循环机制下,渲染主线程取出渲染任务,开启渲染流程。

    整个渲染流程分为多个阶段,分别是:HTML解析、样式计算、布局、分层、绘制、分块、光栅化、画。
    每个阶段都有明确的输出,上一个阶段的输出是下一个阶段的输入。
    这样,整个阶段就成为一段严密的流水线。

    HTML字符串-〉解析HTML-〉样式计算-〉布局-〉分层-〉绘制-〉分块-〉光栅化-〉画-〉像素信息
    渲染:将html字符串->像素信息

    1.解析HTML

    **
    解析过程中遇到css,解析css,遇到js解析js。为了提高效率,浏览器在解析前,会启动一个预解析的线程,率先下载HTML中外部的css文件和外部的js文件。
    如果主线程解析到link位置,此时外部的css文件还没有解析下载好,主线程不会等待,继续后续HTML的解析。这是因为下载和解析css是在预解析线程中进行的。这就是css不会阻塞HTML解析的根本原因。
    如果主线程解析到script位置,主线程会停止解析HTML,转而等待js文件下载好,并将全局代码解析完成后,才继续解析HTML。这是因为js代码可能会修改当前的DOM树,所以DOM生成必须暂停。这就是js会阻塞HMTL解析的根本原因。
    第一步完成后,会生成CSSOM树和DOM树,内部样式、外部样式、内联样式、浏览器默认样式都会包含在CSSOM树。
    **

    渲染的第一步是解析HTMl,ParseHTML,产生DOM树(Document Object Model)和CSSOM树(css Object Model)

    css样式包括:内部样式style,外部样式link,内联样式,浏览器默认样式
    js修改style样式,docu.style = ‘’
    js修改<style>和外部样式,document.styleSheets[0].addRule(‘div’, ‘border: 1px solid #f40’)

    解析HTML,遇到css代码怎么办?
    为了提高效率,浏览器会启动一个预解析器率先下载和解析css,css不会阻塞解析HTML。

    2、样式计算+ recalculate style

    **
    主线程遍历整个DOM树,依次为每一个节点计算出最终样式,computed style,
    在这个过程中,很多预设值会变成绝对值,比如red会变成rgb(255, 0, 0); 相对单位会变成绝对单位,比如em会变成px。
    这一步完成后,会得到一颗带样式的DOM树
    **

    getComputedStyle()获取最终样式

    3、布局-Layout

    **
    接下来是布局,布局完成后会得到布局树。
    布局阶段会依次遍历DOM树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置。
    大部分时候,DOM树和布局树不是一一对应的。
    比如dislay:none没有几何信息,因此不会生成到布局树中,又比如使用伪元素选择器,虽然DOM树中不存在这些伪元素节点,但是他们拥有几何信息,所以会生成到布局树。
    **

    DOM树和布局树不是一一对应的。有几何信息的才会在布局树中出现,display:none不出现
    文本内容必须放在行盒中。

    document.body.clientWidth,获取的布局信息

    4、分层

    **
    主线程会使用一套复杂的策略对布局树进行分层。
    分层的好处在于,将来某一层改变后,仅对该层进行后续处理,从而提高效率。

    滚动条、堆叠上下文、transform、opacity等样式都会影响分层结果。will-change会更大程度影响分层。
    **

    5、绘制

    **
    主线程会为每个层单独产生绘制指令集,用于描述每个层的内容该如何画出来。
    完成绘制后,主线程将绘制信息提交给合成线程,后续工作交由合成线程完成。
    **

    6、分块

    合成线程会对每个图层进行分块,将其划分为更小的区域。
    它会从线程池中拿更多线程来完成分块工作。

    分块完成后进入光栅化阶段。

    7.光栅化

    合成线程将块交由GPU进程,完成光栅化,极高速度。
    GPU进程会开启多个线程完成光栅化,并且优先处理靠近视口区域的块。
    光栅化的结果是一块一块的位图。

    8.画

    合成线程计算出每个位图的在屏幕上的位置,交由GPU进程最终呈现。

    渲染进程(在沙盒,隔离硬件,安全):渲染主线程、合成线程

    所以交由GPU进程

    transform发生在合成线程,与渲染主线程无关所以效率高。

    什么是reflow?

    渲染主线程:parse—-computed style—-layout—-layer—-paint
    合成线程:—-tiling分块—-raster光栅化—-draw-〉

    cssom
    dom
    几何信息、布局

    reflow的本质是重新计算布局树。
    当进行了会影响布局树的操作后,会重新计算布局树,引发layout。
    为了避免连续多次的操作导致布局树多次计算,浏览器会合并这些操作。当js代码全部执行完后统一计算。所以改动属性引发的reflow是异步的。

    因此js获取布局属性时,可能是重新布局前的信息,无法获取最新的布局信息,浏览器在反复权衡下,最终决定获取布局信息立即reflow。

    doc.style.width = ‘’
    doc.style.height = ‘’
    doc.style.margin = ‘’
    doc.clientWidth // 立即reflow

    哪些操作触发重排?

    • 更改窗口大小
    • 更改元素的尺寸、位置、内容
    • 更改文字大小
    • 添加或删除可见元素
    • 页面初始化渲染

    什么是repaint?

    repaint本质是重新根据分层信息计算了绘制指令。
    当改动了可见样式就需要重新计算,引发repaint。
    由于元素的布局信息也属于可见样式,所以reflow一定引发repaint。

    哪些操作触发重绘?

    • 修改元素文本颜色和方向
    • 修改元素背景色和图片
    • 修改元素的可见性:如visible
    • 修改元素的边框和阴影属性

    为什么transform的效率高?

    因为transform既不影响布局也不影响绘制,它影响的是渲染流程的最后一个阶段draw。
    由于draw阶段在合成线程,所以transform的变化几乎不影响渲染主线程。反之,渲染主线程也不会影响transform的变化。

    2023-11-27

    1.灰阶滤镜

    filter: grayscale(1) 灰阶滤镜 将对应元素的像素点转换为灰色,0是原图,1是全灰,grayscale是filter滤镜中的一种,
    可以设置在html全局或局部区域上。

    2.js实现无限循环轮播图

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Swiper</title>
    <style>
    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    }
    .container {
    width: 300px;
    height: 200px;
    overflow: hidden;
    border: 1px solid blue;
    position: relative;
    }
    .carousel {
    display: flex;
    height: 100%;
    /* transition: 0.5s; */
    }
    .carousel_item {
    background-color: aquamarine;
    color: white;
    height: 100%;
    width: 300px;
    min-width: 300px;
    text-align: center;
    font-size: 30px;
    padding-top: 60px;
    }
    .indicate {
    position: absolute;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    }
    .dot {
    width: 10px;
    height: 10px;
    margin: 0 10px 0 0;
    border-radius: 50%;
    border: 2px solid blueviolet;
    background-color: white;
    }
    .dot.active {
    background-color: blueviolet;
    }
    </style>
    </head>
    <body>
    <div class="container">
    <div class="carousel">
    <div class="carousel_item" >1</div>
    <div class="carousel_item">2</div>
    <div class="carousel_item">3</div>
    </div>
    <div class="indicate">
    <div class="dot active"></div>
    <div class="dot"></div>
    <div class="dot"></div>
    </div>
    </div>
    <script type="text/javascript">
    // 无限循环原理:
    // 弹性盒水平排列,设置translateX移动加transition过渡动画
    // 克隆第一个轮播图元素到轮播容器末尾,当轮播到最后一个轮播图时即克隆的第一个元素,下标items.length-1
    // 指示器active第一个点,过渡动画结束后,立即translateX移动到第一个轮播位置(取消过渡动画),重设轮播的下标为0,实现无痕无限循环轮播

    let doms = {
    carousel: document.querySelector('.carousel'),
    carouselItems: document.querySelectorAll('.carousel_item'),
    indicators: document.querySelectorAll('.dot')
    }

    function moveTo(index) {
    console.log('moveTo: ' + index)
    doms.carousel.style.transition = '0.5s'
    doms.carousel.style.transform = `translateX(-${index}00%)`
    // 去除当前选中指示器
    let active = document.querySelector('.dot.active')
    active.classList.remove('active')
    // 重新设置指示器
    doms.indicators[index].classList.add('active')
    }
    // 给指示器添加点击事件
    doms.indicators.forEach((item, index) => {
    item.onclick = function () {
    moveTo(index)
    }
    })
    // 在轮播收尾各添加一个克隆项
    const firstClone = doms.carouselItems[0].cloneNode(true)
    doms.carousel.appendChild(firstClone)
    let initialValue = 0
    let currentItems = document.querySelectorAll('.carousel_item')
    let interval = setInterval(() => {
    initialValue++
    // 当轮播到最后一张时,轮播指向克隆的第一张,指示器指向第一个,过渡动画结束后移到第一张轮播图
    if (initialValue >= currentItems.length + 1) {
    doms.carousel.style.transition = '0.5s'
    doms.carousel.style.transform = `translateX(-${initialValue}00%)`
    // 去除当前选中指示器
    let active = document.querySelector('.dot.active')
    if (active) active.classList.remove('active')
    // 重新设置指示器为第一个
    doms.indicators[0].classList.add('active')
    setTimeout(() => {
    initialValue = 0
    doms.carousel.style.transition = 'none'
    doms.carousel.style.transform = `translateX(-${initialValue}00%)`
    }, 500)
    } else {
    moveTo(initialValue)
    }
    }, 3000)
    </script>
    </body>
    </html>

    3.sticky定位

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sticky</title>
    <style>
    dl {
    /* overflow: hidden; */
    }
    dt {
    background-color: aquamarine;
    font-size: 30px;
    height: 40px;
    position: sticky;
    top: 0;
    }
    dd {
    height: 30px;
    line-height: 30px;
    }
    </style>
    </head>
    <body>
    <div>
    <dl>
    <dt>A</dt>
    <dd>ALorem, ipsum.</dd>
    <dd>ANisi, rerum?</dd>
    <dd>ANisi, recusandae.</dd>
    <dd>AIusto, provident!</dd>
    <dd>AFuga, sed!</dd>
    <dd>ANemo, nobis?</dd>
    <dd>AModi, consequatur.</dd>
    <dd>ANemo, nihil!</dd>
    <dd>ARepellat, alias.</dd>
    <dd>AHic, nostrum!</dd>
    <dd>AMinima, ducimus.</dd>
    <dd>ABeatae, a!</dd>
    <dd>AAccusantium, vel?</dd>
    <dd>ACorporis, totam!</dd>
    <dd>AMinus, sapiente.</dd>
    <dd>ARem, tenetur.</dd>
    <dd>ALaboriosam, perferendis.</dd>
    <dd>AQuisquam, iste!</dd>
    <dd>AItaque, nihil?</dd>
    <dd>AQuos, ipsum.</dd>
    <dt>B</dt>
    <dd>BLorem, ipsum.</dd>
    <dd>BNisi, rerum?</dd>
    <dd>BNisi, recusandae.</dd>
    <dd>BIusto, provident!</dd>
    <dd>BFuga, sed!</dd>
    <dd>BNemo, nobis?</dd>
    <dd>BModi, consequatur.</dd>
    <dd>BNemo, nihil!</dd>
    <dd>BRepellat, alias.</dd>
    <dd>BHic, nostrum!</dd>
    <dd>BMinima, ducimus.</dd>
    <dd>BBeatae, a!</dd>
    <dd>BAccusantium, vel?</dd>
    <dd>BCorporis, totam!</dd>
    <dd>BMinus, sapiente.</dd>
    <dd>BRem, tenetur.</dd>
    <dd>BLaboriosam, perferendis.</dd>
    <dd>BQuisquam, iste!</dd>
    <dd>BItaque, nihil?</dd>
    <dd>BQuos, ipsum.</dd>
    <dd>BBeatae, a!</dd>
    <dd>BAccusantium, vel?</dd>
    <dd>BCorporis, totam!</dd>
    <dd>BMinus, sapiente.</dd>
    <dd>BRem, tenetur.</dd>
    <dd>BLaboriosam, perferendis.</dd>
    <dd>BQuisquam, iste!</dd>
    <dd>BItaque, nihil?</dd>
    <dd>BQuos, ipsum.</dd>
    <dt>C</dt>
    <dd>CLorem, ipsum.</dd>
    <dd>CNisi, rerum?</dd>
    <dd>CNisi, recusandae.</dd>
    <dd>CIusto, provident!</dd>
    <dd>CFuga, sed!</dd>
    <dd>CNemo, nobis?</dd>
    <dd>CModi, consequatur.</dd>
    <dd>CNemo, nihil!</dd>
    <dd>CRepellat, alias.</dd>
    <dd>CHic, nostrum!</dd>
    <dd>CMinima, ducimus.</dd>
    <dd>CBeatae, a!</dd>
    <dd>CAccusantium, vel?</dd>
    <dd>CCorporis, totam!</dd>
    <dd>CMinus, sapiente.</dd>
    <dd>CRem, tenetur.</dd>
    <dd>CLaboriosam, perferendis.</dd>
    <dd>CQuisquam, iste!</dd>
    <dd>CItaque, nihil?</dd>
    <dd>CQuos, ipsum.</dd>
    <dd>CMinima, ducimus.</dd>
    <dd>CBeatae, a!</dd>
    <dd>CAccusantium, vel?</dd>
    <dd>CCorporis, totam!</dd>
    <dd>CMinus, sapiente.</dd>
    <dd>CRem, tenetur.</dd>
    <dd>CLaboriosam, perferendis.</dd>
    <dd>CQuisquam, iste!</dd>
    <dd>CItaque, nihil?</dd>
    <dd>CQuos, ipsum.</dd>
    <dd>CMinima, ducimus.</dd>
    <dd>CBeatae, a!</dd>
    <dd>CAccusantium, vel?</dd>
    <dd>CCorporis, totam!</dd>
    <dd>CMinus, sapiente.</dd>
    <dd>CRem, tenetur.</dd>
    <dd>CLaboriosam, perferendis.</dd>
    <dd>CQuisquam, iste!</dd>
    <dd>CItaque, nihil?</dd>
    <dd>CQuos, ipsum.</dd>
    <dd>CMinima, ducimus.</dd>
    <dd>CBeatae, a!</dd>
    <dd>CAccusantium, vel?</dd>
    <dd>CCorporis, totam!</dd>
    <dd>CMinus, sapiente.</dd>
    <dd>CRem, tenetur.</dd>
    <dd>CLaboriosam, perferendis.</dd>
    <dd>CQuisquam, iste!</dd>
    <dd>CItaque, nihil?</dd>
    <dd>CQuos, ipsum.</dd>
    </dl>
    </div>
    </body>
    </html>

    注意,一个 sticky 元素会“固定”在离它最近的一个拥有“滚动机制”的祖先上,如果给其父元素dl设置overflow属性,则该属性失效,如果给body设置overflow属性,则相对于dl元素固定。如果都不设置,则相对于视口。
    positon: sticky;元素根据正常文档流进行定位,然后相对它的最近滚动祖先和包含块,包括 table-related 元素,基于 top、right、bottom 和 left 的值进行偏移。
    该值总是创建一个新的层叠上下文。须指定 top, right, bottom 或 left 四个阈值其中之一,才可使粘性定位生效。否则其行为与相对定位相同。

    4.visibilitychange 离开/进入标签页事件-页面可见度

    1
    2
    3
    document.addEventListener('visibilitychange', function() {
    console.log(document.visibilityState) // hidden, visible
    })

    5.零宽字符

    零宽字符是一种不可打印的Unicode字符,在浏览器等环境不可见,但是真实存在,获取字符串长度时也会占位置,表示某一种控制功能的字符。

    1
    2
    3
    const str1 = '哈哈\u200d\u200d嘿嘿'
    const str2 = '哈哈嘿嘿'
    console.log(str1, str1.length, str1 === str2) // 哈哈‍‍嘿嘿 6 false

    6.求和

    求1开始的前n项的奇数和

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function sum(n) {
    let result = 0
    for(let i=0;i<n;i++) {
    result+=2*i+1
    }
    return result
    }
    sum(1) // 1
    sum(2) // 4
    sum(3) // 9
    sum(4) // 16
    sum(5) // 25
    sum(6) // 36

    // 或者
    function sum(n) {
    return n*n
    }

    求1开始的前n项的偶数和

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function sum(n) {
    let result = 0
    for(let i=0;i<n;i++) {
    result+=2*(i+1)
    }
    return result
    }
    sum(1) // 2 = 1*2
    sum(2) // 6 = 2*3
    sum(3) // 12 = 3*4
    sum(4) // 20 = 4*5
    sum(5) // 30 = 5*6
    sum(6) // 42 = 6*7

    // 或者
    function sum(n) {
    return n*(n+1)
    }

    求1开始的前n项的和

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function sum(n) {
    let result = 0
    for(let i=0;i<n;i++) {
    result+=(i+1)
    }
    return result
    }
    sum(1) // 1 = n*(n+1)/2
    sum(2) // 3
    sum(3) // 6
    sum(4) // 10
    sum(5) // 15
    sum(6) // 21

    // 或者
    function sum(n) {
    return n*(n+1)/2
    }

    求1-n之间的奇数和

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function sum(n) {
    let result = 0
    for(let i=0;i<n;i++) {
    if (i%2 = 1) {
    result+=i
    }
    }
    return result
    }
    sum(1) // 1
    sum(2) // 1
    sum(3) // 4
    sum(4) // 4
    sum(5) // 9
    sum(6) // 9

    7.文字描边text-stroke

    -webkit-text-stroke: 2px #fff;

    8.js类里创建私有字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <script>
    class A {
    constructor() {
    this._abc = 1 // 以前没有私有字段用_来说明是私有字段但是外部可以访问
    }
    }
    let a = new A()
    console.log(a._abc) // 1

    class B {
    #abc; // 定义一个私有字段
    constructor() {
    this.#abc = 1
    }
    #method (){} // 私有方法
    }
    let b = new B()
    console.log(b, b.#abc) // private field '#abc' must be declared in an enclosing class
    </script>

    9.下拉菜单的过渡效果

    css获取焦点设置高度auto展开,但是transition无效,只针对数值有效。

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>select-transition</title>
    <style>
    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    }
    body {
    padding-left: 100px;
    }
    input {
    width: 400px;
    height: 40px;
    }
    .select {
    width: 400px;
    border-right: 1px solid;
    border-left: 1px solid;
    border-bottom: 1px solid;
    margin-top: -1px;
    height: 0;
    overflow: hidden;
    transition: 1s;
    }
    .select > div {
    line-height: 30px;
    padding-left: 20px;
    }
    input:focus ~ .select {
    height: auto
    }
    </style>
    </head>
    <body>
    <input type="text">
    <div class="select">
    <div>Lorem, ipsum.</div>
    <div>Ab, vel?</div>
    <div>Voluptatum, iusto.</div>
    <div>Atque, repudiandae?</div>
    <div>Corporis, voluptas.</div>
    </div>
    </body>
    </html>

    已知select高度情况下可以设置具体值,比如

    1
    2
    3
    input:focus ~ .select {
    height: 300px
    }

    设置max-height值也可以实现效果。

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>select-transition</title>
    <style>
    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    }
    body {
    padding-left: 100px;
    }
    input {
    width: 400px;
    height: 40px;
    }
    .select {
    width: 400px;
    border-right: 1px solid;
    border-left: 1px solid;
    border-bottom: 1px solid;
    margin-top: -1px;
    height: auto;
    overflow: hidden;
    max-height: 0;
    transition: 1s;
    }
    .select > div {
    line-height: 30px;
    padding-left: 20px;
    }
    input:focus ~ .select {
    max-height: 300px;
    }
    </style>
    </head>
    <body>
    <input type="text">
    <div class="select">
    <div>Lorem, ipsum.</div>
    <div>Ab, vel?</div>
    <div>Voluptatum, iusto.</div>
    <div>Atque, repudiandae?</div>
    <div>Corporis, voluptas.</div>
    </div>
    </body>
    </html>

    注意,max-height的值,300px的高度过渡动画为1s,但实际上select真实高度并不一定是300,因此失焦时收起select会有延时

    使用js添加获取/失去焦点事件控制select高度。

    先css设置height为0,获取焦点时,设置高度auto获取实际高度,然后再设置select高度0,再设置获取的实际高度和transition,失焦时隐藏高度

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>select-transition</title>
    <style>
    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    }
    body {
    padding-left: 100px;
    }
    input {
    width: 400px;
    height: 40px;
    }
    .select {
    width: 400px;
    border-right: 1px solid;
    border-left: 1px solid;
    border-bottom: 1px solid;
    margin-top: -1px;
    height: 0;
    overflow: hidden;
    transition: 1s;
    }
    .select > div {
    line-height: 30px;
    padding-left: 20px;
    }
    </style>
    </head>
    <body>
    <input type="text">
    <div class="select">
    <div>Lorem, ipsum.</div>
    <div>Ab, vel?</div>
    <div>Voluptatum, iusto.</div>
    <div>Atque, repudiandae?</div>
    <div>Corporis, voluptas.</div>
    </div>
    <script>
    let input = document.querySelector('input')
    let select = document.querySelector('.select')
    input.onfocus = function() {
    select.style.transition = 'none'
    select.style.height = 'auto'
    // 获取select高度
    const height = select.offsetHeight
    console.log(height)
    select.style.height = 0
    select.offsetHeight // 获取元素几何信息强制浏览器渲染,因为浏览器js设置dom会合并操作所以需要强制渲染
    select.style.transition = '1s'
    select.style.height = height + 'px'
    }
    input.onblur = function() {
    select.style.transition = '1s'
    select.style.height = '0'
    }
    </script>
    </body>
    </html>

    利用css3的scaleY缩放高度。

    注:缩放过程字体会重叠

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>select-transition</title>
    <style>
    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    }
    body {
    padding-left: 100px;
    }
    input {
    width: 400px;
    height: 40px;
    }
    .select {
    width: 400px;
    border-right: 1px solid;
    border-left: 1px solid;
    border-bottom: 1px solid;
    margin-top: -1px;
    height: auto;
    overflow: hidden;
    transition: 1s;
    z-index: -1;
    transform-origin: center top;
    transform: scaleY(0);
    }
    .select > div {
    line-height: 30px;
    padding-left: 20px;
    }
    input:focus ~ .select {
    transform: scaleY(1);
    }
    </style>
    </head>
    <body>
    <input type="text">
    <div class="select">
    <div>Lorem, ipsum.</div>
    <div>Ab, vel?</div>
    <div>Voluptatum, iusto.</div>
    <div>Atque, repudiandae?</div>
    <div>Corporis, voluptas.</div>
    </div>
    </body>
    </html>

    10.跨域cors

    CORS是基于http1.1的一种跨域解决方案,全称跨域资源共享。
    它的总体思路:如果浏览器要跨域访问服务器的资源,需要获取服务器的允许。

    针对不同请求,cors规定里三种不同的交互模式:

    • 简单请求
    • 需要预检的请求
    • 携带身份凭证的请求

    简单请求

    1. 请求方法属于以下的一种:get、post、head

    2. 请求头仅包含安全的字段,常见安全字段如下:

      • Accept
      • Accept-Language
      • Content-Language
      • Content-Type
      • DPR
      • Downlink
      • Save-Data
      • Viewport-Width
      • Width
    3. 请求头如果包含Content-Type,仅限以下值之一:

      • text/plain-文本内容数据
      • multipart/form-data-文件上传
      • application/x-www-form-urlencoded–提交表单

    以上三个条件都满足,浏览器判断为简单请求。

    简单请求的交互规范

    当前浏览器某个跨域请求是简单请求时,会发生以下:

    1. 请求头中会自动添加Origin字段,哪个源地址在跨域请求
    2. 服务器响应头应包含Access-Control-Allow-Origin
      服务器收到响应,如果允许跨域,则在响应头添加Access-Control-Allow-Origin字段,该属性值有:
      • *:允许一切
      • http://my.com: 只允许http://my.com这个源访问

    需要预检的请求

    如果浏览器不认为是简单请求,会按照以下流程进行:

    • 浏览器发送预检请求,访问服务器是否允许
    • 服务器允许访问
    • 浏览器发送真实的请求
    • 服务器完成真实的响应

    例如以下请求,就是需要预检请求

    1
    2
    3
    4
    5
    6
    7
    8
    fetch("http://my.com/api/user", {
    method: "POST",
    header: {
    a:1
    'Content-Type': 'application/json'
    },
    body: ...
    })

    附带身份凭证的请求

    默认情况下,ajax的请求并不会携带cookie,可以配置:

    1
    2
    3
    4
    5
    6
    7
    8
    // xhr
    var xhr = new XMLHttpRequest()
    xhr.withCredentials = true

    // fetch api
    fetch(url, {
    credentials: 'include'
    })

    这样,该跨域的请求就是附带身份凭证的请求。当一个请求附带cookie时,无论是简单请求还是预检请求,都会在请求头中添加cookie字段,
    而服务器响应时需要明确告知客户端服务器允许这样的凭证:Access-Control-Allow-Credentials: true.若服务器没有响应则跨域被拒绝,
    注意:对于附带身份凭证的请求,服务器不得设置Access-Control-Allow-Origin:*。

    补充

    在跨域访问时,js只能拿到一些最基本的响应头,比如:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Progma,如果要访问其他头,需要服务器设置响应头。

    Access-Control-Export-Headers:authorization…,这样浏览器就可以访问authorization响应头了。

    10.解析url中的参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function parseQuery(url) {
    let q = {}
    url.replace(/([^?&=]+)=([^&]+)/g,
    (_, k, v) => (q[k] = v))
    return q
    }
    let a = parseQuery('http://my.com?a=1&b=2')
    let b = parseQuery('a=1&b=2')
    console.log(a, b)

    //g:这两个符号是正则表达式的开始和结束标记,g 标志表示全局搜索,即匹配字符串中所有符合条件的子串,而不是只匹配第一个。

    ([^?&=]+)

    []:字符集,匹配方括号中的任意一个字符。
    ^:在字符集中,表示非,即不匹配后续列出的字符。
    ?&=:列出的字符集,包括问号、等号和与号。
    +:表示匹配一次或多次。
    所以 ([^?&=]+) 匹配一个或多个不是 ?、& 或 = 的字符。这部分通常用于匹配 URL 查询字符串中的参数名。
    =:匹配等号字符,即 URL 查询字符串中用来分隔参数名和参数值的字符。

    ([^&]+)

    [^&]:字符集,匹配除了与号 & 以外的任意字符。
    +:表示匹配一次或多次。
    所以 ([^&]+) 匹配一个或多个不是 & 的字符。这部分通常用于匹配 URL 查询字符串中的参数值。

    11.生成随机颜色

    1
    2
    3
    4
    5
    function randomColor() {
    return '#' + Math.floor(Math.random() * 0xffffff).toString(16).padEnd(6, '0')
    }
    // padEnd() 方法会将当前字符串从末尾开始填充给定的字符串(如果需要会重复填充),直到达到给定的长度。填充是从当前字符串的末尾开始的。
    console.log(randomColor())
    1. 0xffffff 是一个十六进制数,它等于十进制的 16777215
    2. Math.random() 是 JavaScript 中的一个函数,它返回一个大于等于 0 且小于 1 的伪随机数.
    3. Math.floor(Math.random() * 0xffffff),向下取整,确保得到的结果是一个0-16777214的整数
    4. toString(16) 方法将这个随机整数转换为十六进制字符串

    12.js的atob和btoa函数

    btoa() 方法可以将一个二进制字符串(例如,将字符串中的每一个字节都视为一个二进制数据字节)编码为 Base64 编码的 ASCII 字符串.
    atob() 对经过 base-64 编码的字符串进行解码.

    1
    2
    3
    4
    let encodedData = window.btoa("Hello, world"); // 编码
    let decodedData = window.atob(encodedData); // 解码
    console.log(encodedData, decodedData)
    // SGVsbG8sIHdvcmxk Hello, world

    13.判断一个值是否是Promise Like?

    和Promise结构类似
    Promise A+规范:https://promisesaplus.com/
    “promise”是一个具有符合本规范的then方法的对象或函数。

    1
    2
    3
    function isPromiseLike(value) {
    return value !== null && (typeof value === 'object' || typeof value === 'function') && (typeof value.then === 'function')
    }

    14.手写一个Promise

    promise是一个构造函数,带有then方法
    它有三种状态:pending 挂起,任务进行中、fulfilled 完成、rejected 失败
    then方法接收2个参数,onFulfilled 和 onRejected 都是可选参数,它们是异步执行的
    可以多次对同一个promise调用then方法,从而注册多个onFulfilled或onRejected
    then方法必须再次返回一个promise
    Promise(resolve, reject), resolve, reject是函数,改变状态

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    const PENDING = 'pending'
    const FULLFILLED = 'fulfilled'
    const REJECTED = 'rejected'

    // promise是一个构造函数,带有then方法
    // 它有三种状态:pending 挂起,任务进行中、fulfilled 完成、rejected 失败
    class MyPromise {
    #state = PENDING // 私有属性,promise初始状态pending
    #result = undefined // 私有属性,promise结果
    #handler = [] // 保存每次调用then方法的回调onFulfilled, onRejected,resolve,reject
    constructor(executor) {
    const resolve = (data) => {
    // 改变状态
    this.#changeState(FULLFILLED, data)
    }
    const reject = (reason) => {
    // 改变状态
    this.#changeState(REJECTED, reason)
    }
    try {
    executor(resolve, reject)
    } catch (error) {
    reject(error)
    }
    }
    #changeState(state, result) {
    if (this.#state !== PENDING) return // 状态一旦改变不可再更改
    this.#state = state
    this.#result = result
    this.#run() // 异步的时候这里知道什么时候改变状态
    }
    #isPromiseLike(value) {
    return value !== null && (typeof value === 'object' || typeof value === 'function') && (typeof value.then === 'function')
    }
    #runMicroTask(callback) {
    setTimeout(callback, 0)
    }
    #run() {
    // fulfilled、rejected、pending
    if (this.#state === PENDING) {
    return
    }
    // 遍历then方法的回调执行
    while(this.#handler.length) {
    const { onFulfilled, onRejected, resolve, reject } = this.#handler.shift()
    // p.then传递参数 onFulfilled, onRejected:
    // 1.回调不是函数:p.then(null), 直接resolve, rejected
    // 2.回调是函数:函数有执行结果 p.then((res)=> {}, err => {})
    // 3.回调的函数的返回结果是promise
    if (this.#state === FULLFILLED) {
    this.#runMicroTask(() => { // promise运行在微队列中
    // 回调不是函数
    if (typeof onFulfilled !== 'function') {
    resolve(this.#result)
    } else {
    // 回调是函数
    try {
    const data = onFulfilled(this.#result) // 回调的函数返回结果是promise
    if (this.#isPromiseLike(data)) {
    data.then(resolve, reject)
    } else {
    resolve(data)
    }
    } catch (error) {
    reject(error)
    }
    }
    })
    } else { // rejected
    this.#runMicroTask(() => { // promise运行在微队列中
    // 回调不是函数
    if (typeof onRejected !== 'function') {
    reject(this.#result)
    } else {
    // 回调是函数
    try {
    const data = onRejected(this.#result) // 回调的函数返回结果是promise
    if (this.#isPromiseLike(data)) {
    data.then(resolve, reject)
    } else {
    resolve(data)
    }
    } catch (error) {
    reject(error)
    }
    }
    })
    }
    }
    }
    // then方法接收2个参数,onFulfilled 和 onRejected 都是可选参数,它们是异步执行的
    // 可以多次对同一个promise调用then方法,从而注册多个onFulfilled或onRejected
    // then方法必须再次返回一个promise
    then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
    // 如果是同步:状态直接是fulfilled/rejected
    // 异步:pending
    // 可多次调用、可能异步(pending)
    // 保存每次调用then方法的回调onFulfilled, onRejected,resolve,reject,等待完成即状态改变(#changeState)时调用
    this.#handler.push({
    onFulfilled,
    onRejected,
    resolve,
    reject
    })
    this.#run()
    })
    }
    }
    const p = new MyPromise((resolve, reject) => {
    setTimeout(() => {
    reject(123)
    }, 1000)
    })
    // p.then(
    // null,
    // (err) => {
    // console.log('promise-fail1', err)
    // return 456
    // }
    // ).then(
    // (res) => {
    // console.log('promise-success2', res)
    // },
    // (err) => {
    // console.log('promise-fail2', err)
    // }
    // )
    // promise-fail1 123
    // promise-success2 456

    p.then(
    null,
    (err) => {
    console.log('promise-fail1', err)
    throw 456
    }
    ).then(
    (res) => {
    console.log('promise-success2', res)
    },
    (err) => {
    console.log('promise-fail2', err)
    }
    )
    // promise-fail1 123
    // promise-fail2 456

    15.语义化版本

    x.y.z , x、y、z都是数字,只能增加
    x:主版本,breaking update, 不兼容式升级,比如vue2-vue3
    y:次版本,更新兼容以前版本
    z:修订版本,修复bug,优化等

    1.8.2->2.0.0
    1.8.2->1.9.0

    高位版本增加,低位版本要清零

    0.1.0,主版本0表示测试版本,未正式发布
    1.2.3-alpha,alpha预发布版本

    16.axios

    Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

    特性

    从浏览器中创建 XMLHttpRequests
    从 node.js 创建 http 请求
    支持 Promise API
    拦截请求和响应
    转换请求数据和响应数据
    取消请求(CancelToken)
    自动转换 JSON 数据
    客户端支持防御 XSRF

    17.数据埋点

    服务监控:产品(ARMS、神策,sentry免费)

    错误监控
    性能监控
    行为监控

    数据埋点:

    侵入:监控用户行为,关键业务代码处埋点
    非侵入:错误、性能监控,对代码等无侵入,在入口文件收集即可

    如何捕获异常?

    • 全局监控js异常:

      1
      2
      3
      4
      5
      window.addEventListener('error', function(args){
      console.log(args)
      },
      true // 捕获模式
      )
      • 优点:可以监听到全局js错误捕获那些没有被 try…catch 语句捕获的异常。包括同步运行时错误,异步运行时错误,语法错误, 以及资源(如图片或脚本)加载失败
      • 缺点:无法捕获接口的异常,无法捕获静态资源异常
    • 对于promise未被捕获的reject使用 window.addEventListener 监听 unhandledrejection 事件

    1
    2
    3
    4
    window.addEventListener('unhandledrejection', event => {  
    // 处理Promise中未被捕获的reject
    console.error('Promise 异常捕获:', event.reason);
    });
    • 使用 Vue.js 的全局错误处理
    1
    2
    3
    4
    Vue.config.errorHandler = function(error, vm, info) {
    // 处理异常
    console.error('Vue 异常捕获:', err, vm, info);
    }

    18.web性能指标

    评定web应用程序用户体验是否优秀:

    RAIL模型:Response响应时间、Animation动画流畅不丢帧不跳帧、Idle页面运行过程是否给用户留够交互时间、load加载时间

    衡量的关键指标:

    • FCP:首次内容加载经过的时间,从白屏到第一个有意义的内容(可以是文字、图片、按钮或背景色元素,看得见的内容)出现的时间
    • LCP:最大内容绘制时间,屏幕内最大的元素加载出来
    • CLS:累计偏移,前面有内容突然加载出来导致内容往后挤,破坏用户的操作,比如点击按钮时,前面有内容加载导致按钮往后排,点击到别的按钮等
    • TBT:total blocking time总的阻塞时间,从FCP到TTI的时间,TTI表示用户可交互的时间点

    使用Chrome lighthouse工具衡量这些指标

    了解用户真实指标需要收集,第三方库web vitals

    19.从视频文件中提取画面帧

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>提取视频文件中的画面帧</title>
    </head>
    <body>
    <input type="file" name="video" id="">
    <script>
    let file = document.querySelector('input[type=file]')
    file.onchange = async (e) => {
    console.log(e.target.files)
    let file = e.target.files[0]
    // 提取第10秒的帧并img预览
    // const frame = await captureFrame(file, 10)
    // createPreview(frame)
    // console.log(frame)
    // 每隔1秒提取帧展示
    for (let i = 1; i < 10; i++) {
    const frame = await captureFrame(file, 1 * i)
    createPreview(frame)
    }
    }
    function drawVideo(vdo) {
    return new Promise((resolve, reject) => {
    const cvs = document.createElement('canvas')
    const ctx = cvs.getContext('2d')
    cvs.width = vdo.videoWidth
    cvs.height = vdo.videoHeight
    ctx.drawImage(vdo, 0, 0, cvs.width, cvs.height)
    cvs.toBlob((blob) => {
    resolve({
    blob,
    url: URL.createObjectURL(blob)
    })
    })
    })

    }
    function captureFrame(vdoFile, time = 0) {
    return new Promise((resolve, reject) => {
    const vdo = document.createElement('video')
    vdo.currentTime = time
    vdo.muted = true
    vdo.autoplay = true
    vdo.oncanplay = async () => {
    const frame = await drawVideo(vdo)
    resolve(frame)
    }
    vdo.src = URL.createObjectURL(vdoFile)
    })
    }
    function createPreview(frame) {
    const img = document.createElement('img')
    img.src = frame.url
    document.body.append(img)
    }
    </script>
    </body>
    </html>

    从视频文件中提取帧:
    1、拿到文件,开启循环,每隔一秒提取一帧
    2、创建video元素将当前帧和src设置给video并进行播放
    3、创建画布将video元素绘制出来转成blob url
    4、拿到url后创建img元素展示视频帧

    URL.createObjectURL(obj)是Web API中的一个方法,用于创建一个表示指定File对象或Blob对象的URL。这个URL是一个指向对象内容的指针,它允许你立即使用对象而不需要先将其内容全部加载到内存中.
    当你不再需要这个URL时,应该调用URL.revokeObjectURL()方法来释放它占用的资源。如果你不调用,那么Blob URL将会一直存在,直到文档被卸载。这可能会导致内存泄漏,特别是在处理大量或大尺寸的Blob对象时。

    20.ESMoudle的工作原理

    静态导入:

    1
    import A from './A.js'

    动态导入:

    1
    2
    3
    import('./dynmaic.js').then(d => {
    console.log(d.default)
    })

    静态导入解析是在运行之前发生的,动态导入的解析是在运行时发生的。
    比如import A from ‘./A.js’,会先将A.js转换为完整的引入url:***/A.js,再下载,先模块解析,再运行
    动态导入:完整URL-》下载js-》解析-》运行-》然后执行then里面语句

    21.defineProperty和proxy

    defineProperty是对象基本操作(内部函数调用),用来定义属性描述符的,不能暴露给开发者,比如get读取属性会调用has方法,delete obj[k] 调用内部函数delete
    proxy针对所有的操作都可以拦截,也暴露了内部方法。

    22.垃圾回收与内存泄露

    程序运行需要内存空间,可能某一块内存空间不再需要,这块内存空间称为垃圾,回收垃圾的叫垃圾回收器
    内存泄露:一块内存空间不需要但垃圾回收器没法回收的内存就是内存泄露

    垃圾回收机制:标记清除算法、引用计数算法(不用)

    造成内存泄露的因素:一些未声明直接赋值的变量;未清除的定时器;过度的闭包;未清除的引用元素

    23.解析DOM树

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function removeTag(fragment) {
    return (new DOMParser().parseFromString(fragment, 'text/html').body.textContent || '')
    }
    removeTag(`<div class="container">
    <h1>DOM Parser</h1>
    <ul>
    <li>Lorem.</li>
    <li>Maiores.</li>
    <li>Sapiente!</li>
    <li>Doloremque.</li>
    </ul>
    </div>`)

    // output:
    // DOM Parser

    // Lorem.
    // Maiores.
    // Sapiente!
    // Doloremque.

    22. 大文件切片上传

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>大文件上传切片</title>
    </head>
    <body>
    <input type="file" name="file" id="fileUpload">
    <script src="./upload.js"></script>
    </body>
    </html>

    upload.js

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    const uploadFile = document.querySelector("#fileUpload")

    uploadFile.onchange = async e => {
    const file = e.target.files[0]
    console.time("upfile")
    const chunks = await cutFile(file) // 获取分片结果
    console.timeEnd("upfile")
    console.log("切片:", chunks)
    }

    const CHUNK_SIZE = 1024 * 1024 * 5 // 5M
    const THREAD_COUNT = navigator.hardwareConcurrency || 4 // 线程数量(切片运行在主线程可能会造成阻塞,可开启别的线程进行分片工作)
    // 分片
    async function cutFile(file) {
    // 比如 file.size :70M, chunkCount: 14 workerChunkCount: 4
    return new Promise(async (resolve, reject) => {
    const chunkCount = Math.ceil(file.size / CHUNK_SIZE) // 分片数量
    const workerChunkCount = Math.ceil(chunkCount / THREAD_COUNT) // 每个线程处理的分片数量

    console.log("切片数量", chunkCount)
    const result = []
    // 切片运行在主线程
    // for (let i = 0; i < chunkCount; i++) {
    // const chunk = await createChunk(file, i, CHUNK_SIZE)
    // result.push(chunk)
    // }
    // 开启其他线程去分片提高效率
    let finishWorker = 0
    for (let i = 0; i < THREAD_COUNT; i++) {
    const worker = new Worker("./worker.js", {
    type: "module"
    })
    // 每个线程起始切片和结束切片索引
    const startIndex = i * workerChunkCount // 0-4 4-8 8-12 12-14
    let endIndex = startIndex + workerChunkCount
    if (endIndex > chunkCount) {
    endIndex = chunkCount
    }
    // 发送消息
    worker.postMessage({
    file,
    CHUNK_SIZE,
    startIndex,
    endIndex
    })
    // 处理结束接受消息
    worker.onmessage = e => {
    console.log(e.data) // 数组
    // 保证切片顺序正确
    // i: index
    // 0: 0-4
    // 1: 4-8
    // 2: 8-12
    // 3: 12-14
    for (let index = startIndex; index < endIndex; index++) {
    result[index] = e.data[index - startIndex] // index - startIndex: 0 1 2 3
    }
    worker.terminate() // 收到消息后当前线程切片工作完成,结束线程
    finishWorker++
    if (finishWorker === THREAD_COUNT) {
    // 所有线程都完成
    resolve(result)
    }
    }
    }
    })
    }
    // 创建分片 slice截取
    function createChunk(file, index, chunkSize) {
    return new Promise((resolve, reject) => {
    const start = index * chunkSize
    const end = start + chunkSize
    const fileReader = new FileReader()
    fileReader.onload = e => {
    // console.log('fileReader-onload', e.target.result)
    resolve({
    start,
    end,
    index,
    hash: e.target.result
    })
    }
    fileReader.readAsArrayBuffer(file.slice(start, end))
    })
    }

    worker.js

    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
    onmessage = async (e) => {
    const promises = []
    const { file, CHUNK_SIZE, startIndex, endIndex } = e.data
    for (let index = startIndex; index < endIndex; index++) {
    promises.push(createChunk(file, index, CHUNK_SIZE))
    }
    const result = await Promise.all(promises)
    postMessage(result)
    }

    // 创建分片 slice截取
    function createChunk(file, index, chunkSize) {
    return new Promise((resolve, reject) => {
    const start = index * chunkSize
    const end = start + chunkSize
    const fileReader = new FileReader()
    fileReader.onload = e => {
    // console.log('fileReader-onload', e.target.result)
    resolve({
    start,
    end,
    index,
    hash: e.target.result
    })
    }
    fileReader.readAsArrayBuffer(file.slice(start, end))
    })
    }

    不必等到全部切片再上传,可以开启其他线程进行切片,边切片边进行上传

    监听上传进度:

    1
    2
    3
    4
    5
    6
    7
    8
    // 监听进度事件  
    xhr.upload.onprogress = function(event) {
    if (event.lengthComputable) {
    const percentComplete = (event.loaded / event.total) * 100;
    console.log(percentComplete + '% 上传完成');
    // 这里可以更新进度条或其他UI元素
    }
    }

    通过xhr.upload中的progress方法可以实现监控每一个切片上传进度。
    上传暂停的实现也比较简单,通过xhr.abort可以取消当前未完成上传切片的上传,实现上传暂停的效果,恢复上传就跟断点续传类似,先获取已上传的切片列表,然后重新发送未上传的切片。

    切片计算hash以及整个文件hash:在文件传输过程中,通常会使用哈希值来校验文件在传输过程中是否发生损坏或被篡改。

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    const crypto = require('crypto');
    const SparkMD5 = require('spark-md5'); // 按照js-md5

    function calculateChunkHash(file, chunkSize) {
    const fileHash = SparkMD5.ArrayBuffer.hash();
    const chunks = [];
    let offset = 0;

    while (offset < file.size) {
    const chunk = file.slice(offset, offset + chunkSize);
    const reader = new FileReader();

    reader.onload = (event) => {
    const chunkContent = new Uint8Array(event.target.result);
    fileHash.update(chunkContent);
    chunks.push({
    start: offset,
    end: offset + chunkSize,
    hash: crypto.createHash('md5').update(chunkContent).digest('hex') // 单独计算切片文件hash
    });
    offset += chunkSize;

    // 如果已经是最后一个切片,或者文件大小不是切片大小的整数倍
    if (offset >= file.size || offset % chunkSize !== 0) {
    const finalHash = fileHash.end();
    console.log('Final file hash:', finalHash);
    console.log('Chunk hashes:', chunks.map(chunk => chunk.hash));
    // 在这里,你可以将最终的哈希值和每个切片的哈希值发送给服务器
    }
    };

    reader.readAsArrayBuffer(chunk);
    }
    }

    // 使用示例
    const fileInput = document.getElementById('fileInput');
    fileInput.addEventListener('change', async (event) => {
    const file = event.target.files[0];
    if (file) {
    const chunkSize = 1024 * 1024; // 1MB
    calculateChunkHash(file, chunkSize);
    }
    });

    限制切片上传并发请求数量:

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    const MAX_CONCURRENT_REQUESTS = 3;
    const requests = [];

    function fetchWithPool(chunk) {
    // 将请求包装成Promise
    const promise = new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    let formData = new FormData()
    formData.append('chunk', chunk)
    xhr.open('POST', url, data: formData);
    xhr.onload = () => resolve(xhr.responseText);
    xhr.onerror = () => reject(new Error('Network error'));
    xhr.send();
    });

    requests.push(promise);

    // 如果请求数量达到限制,则等待当前请求完成后再发送新的请求
    if (requests.length >= MAX_CONCURRENT_REQUESTS) {
    return promise.then(data => {
    requests.shift();
    return data;
    });
    }

    // 否则直接返回Promise
    return promise;
    }

    // 使用async/await进行请求
    async function fetchWithPoolExample() {
    const chunks = [...];
    const results = [];

    for (const chunk of chunks) {
    const result = await fetchWithPool(chunk);
    results.push(result);
    }

    console.log(results);
    }

    fetchWithPoolExample();

    如何实现断点续传:
    切片上传成功后,保存已上传成功的切片信息,当下次传输相同文件时,遍历切片列表,只选择未上传的切片进行上传。

    如何保存已上传切片的信息:

    • 可以通过locaStorage等方式保存在前端浏览器中,这种方式不依赖于服务端,实现起来也比较方便,缺点在于如果用户清除了本地文件,会导致上传记录丢失
    • 服务端本身知道哪些切片已经上传,因此可以由服务端额外提供一个根据文件hash查询已上传切片的接口,在上传文件前调用该文件的历史上传记录

    23.单点登录

    多条产品线,共享同一套用户系统,抽离用户系统,形成用户中心

    session+cookie模式:
    认证中心登录成功后生成用户信息到session表里(sid:身份信息),然后将该 Session ID 存储在 Cookie 中,并返回给客户的端,客户端再次访问业务系统会带上cookie(包含sid),业务系统去认证中心验证sid是否有效,有效就可以访问业务系统登录后的资源。

    认证中心会将用户信息存储session表里,session 存储在服务器里面,当用户同时在线量比较多时,这些 session 会占据较多的内存,需要在服务端定期的去清理过期的 session。

    token模式:

    用户-认证中心登录成功后返回给客户端一个token(jwt),客户端自己存储。客户端再访问业务系统携带token,业务系统自行验证token是否有效。

    token+refresh模式:

    用户-认证中心登录成功后返回给客户端token+refreshToken, token过期时间短,refreshToken长期有效,客户端访问业务系统携带token,由业务系统进行验证,如果token过期,客户端会用refreshToken去认证中心重新获取新的token,再去访问业务系统

    JWT(token)

    jwt(Json web token)用于在网络间安全的传输信息。用于认证、授权、信息交换等场景。

    JWT由Header(头部)、Payload(负载)、Signature(签名)三部分组成,三者都经过base64转码。

    Header头部包含类型和签名算法;Payload负载包含实体(用户)和其他数据的声明;Signature签名:为了验证数据在传输过程中是否被篡改,对头部和负载使用算法进行签名。

    JWT的优点:

    • 紧凑:非常小巧,可以在URL、HTML表单、HTTP头中使用
    • 自包含:包含有关用户、签发者等必要信息,不需要从服务器多次查询

    JWT的缺点:

    • 无法撤销:JWT一经签发不能被更改和撤销,在过期之前即使被窃取都可使用
    • 增加服务器的负载:因为JWT中信息需要被解码和验证会增加服务器的负载

    JWT通常通过sessionStorage\localStorage\cookies存储在浏览器。

    jwt认证流程:

    1、客户端把账号密码发送给后端接口

    2、服务端核对成功后把用户信息等作为负载,然后把头部和负载分别进行base64编码后进行签名,形成一个jwt

    3、客户端每次发送请求http请求头携带jwt(Authorization)

    4、服务端验证jwt的有效性

    5、验证通过后使用jwt中的用户信息进行操作返回结果

    24.LRU缓存置换算法

    LRU:选择最近最久未使用的置换

    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
    class LRUCache {
    #map;
    #length;
    constructor(length) {
    this.#map = new Map()
    this.#length = length
    }
    get(key) {
    if (!this.$map.get(key)) {
    return
    }
    const value = this.$map.get(key)
    this.#map.delete(key)
    this.#map.set(key, value)
    return value
    }
    set(key, value) {
    if (this.$map.get(key)) {
    this.#map.delete(key)
    }
    this.#map.set(key, value)
    if (this.#map.size > this.#length) {
    const firstkey = this.#map.keys().next().value
    this.#map.delete(firstkey)
    }
    }
    }

    25.判断是不是数组

    1.Object.prototype.toString.call([])

    Object.prototype.toString返回对象的字符串表示,直接使用 Object.prototype.toString 可能会受到对象自身的 toString 方法的覆盖。为了确保调用的是原始的 toString 方法,我们通常会使用 call 或 apply 方法来显式地设置 this 的上下文。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let arr = []
    let obj = {}
    Object.prototype.toString.call(arr) // '[object Array]'
    Object.prototype.toString.call(obj) // '[object Object]'
    // but
    let obj1 = {
    [Symbol.toStringTag]: 'abc'
    }
    Object.prototype.toString.call(obj1) // '[object abc]'

    对象的Symbol.toStringTag属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]或[object Array]中object后面的那个字符串。

    2.[] instanceof Array

    instanceof检测构造函数的prototype属性是否存在实例对象的原型链上

    1
    2
    3
    4
    5
    [] instanceof Array // true
    const Array1 = window.Array
    const Array2 = iframe.contentWindow.Array
    const arr = new Array2()
    arr instanceof Array // false

    如果页面中存在多个框架,那么 instanceof 可能不会正常工作,因为不同框架有不同的全局执行环境,因此会有不同的 Array 构造函数。

    3.Array.isArray([])

    Array.isArray() 检查传递的值是否为 Array。它不检查值的原型链,也不依赖于它所附加的 Array 构造函数。
    对于使用数组字面量语法或 Array 构造函数创建的任何值,它都会返回 true

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 下面的函数调用都返回 true
    Array.isArray([]);
    Array.isArray([1]);
    Array.isArray(new Array());
    Array.isArray(new Array("a", "b", "c", "d"));
    Array.isArray(new Array(3));
    // 鲜为人知的事实:其实 Array.prototype 也是一个数组:
    Array.isArray(Array.prototype);

    // 下面的函数调用都返回 false
    Array.isArray();
    Array.isArray({});
    Array.isArray(null);
    Array.isArray(undefined);
    Array.isArray(17);
    Array.isArray("Array");
    Array.isArray(true);
    Array.isArray(false);
    Array.isArray(new Uint8Array(32));
    // 这不是一个数组,因为它不是使用数组字面量语法或 Array 构造函数创建的
    Array.isArray({ __proto__: Array.prototype });

    当检测 Array 实例时,Array.isArray 优于 instanceof,因为 Array.isArray 能跨领域工作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const iframe = document.createElement("iframe");
    document.body.appendChild(iframe);
    const xArray = window.frames[window.frames.length + 1].Array;
    const arr = new xArray(1, 2, 3); // [1, 2, 3]

    // 正确检查 Array
    Array.isArray(arr); // true
    // arr 的原型是 xArray.prototype,它是一个不同于 Array.prototype 的对象
    arr.__proto__ == xArray.prototype // true
    arr.__proto__ == Array.prototype // false

    arr instanceof Array; // false

    4.使用对象的contructor属性

    每个js对象都有一个constructor属性,表示创建该对象的构造函数的引用。

    1
    2
    3
    4
    5
    let arr = [];  
    let notArr = {};

    console.log(arr.constructor === Array); // 输出:true
    console.log(notArr.constructor === Array); // 输出:false

    26.Reflect的本质

    Reflect: 调用对象的基本方法。

    针对js的对象的基本内部方法有:

    image

    支持对象被作为函数调用的其他基础内部方法:

    image

    这些对象的基本内部方法不对外暴漏,但是有了Reflect可以通过函数方式调用而不使用语法:

    1
    2
    3
    4
    const obj = {}
    // obj.a = '3' 语法
    Reflect.set(obj, 'a', '3')
    // {a: '3'}

    当使用语法obj.a = ‘3’赋值时,执行代码时会先执行一个被封装过后的方法,方法里面调用了内部方法set

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const obj = {
    a: 1,
    b: 2,
    get c() {
    return this.a + this.b
    }
    }
    console.log(obj.c) // 3

    // [Get]]---(propertyKey, Receiver) → any---返回这个对象里 key 值为 propertyKey 的属性的值。如果必须运行一些的 ECMAScript 代码来检索这个属性值,Receiver 就会作为解析代码时的 this 值。

    // 因为obj.c调取get内部方法,Receiver即this传递的是obj,所以没办法更改this的值

    // 但是Reflect可以传递Receiver更改this
    Reflect.get(obj, 'c', {a: 3, b: 4}) // 7
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let obj = {
    a: 1,
    b: 2,
    [Symbol('c')]: 3
    }
    Object.defineProperty(obj, 'd', {
    value: 4,
    enumerable: false
    })
    console.log(Object.keys(obj)) // ['a', 'b']
    // Object.keys方法调用的封装后的方法里面设置过滤掉不可枚举的属性,调用内部方法OwnPropertyKeys:返回一个包含所有自身属性 key 值的 List。
    console.log(Reflect.ownKeys(obj)) //  ['a', 'b', 'd', Symbol(c)]
    // OwnPropertyKeys对应的Reflect方法ownKeys,Reflect.ownKeys直接调用对象的内部方法可获取全部属性

    27.img的srcset、size

    srcset可以设置不同dpr使用的图片尺寸

    图像的尺寸 = css尺寸 * dpr 图片不会模糊

    1
    2
    3
    4
    img {
    width: 150px;
    height: 150px;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    <img srcset="
    https://picsum.photos/id/164/150 1x,
    https://picsum.photos/id/164/300 2x,
    https://picsum.photos/id/164/450 3x,
    https://picsum.photos/id/164/600 4x,
    https://picsum.photos/id/164/750 5x,
    https://picsum.photos/id/164/900 6x,
    ">

    https://picsum.photos/id/164/150“ 1x, dpr为1时,使用150的图片

    sizes: 媒体查询来响应图片大小,多个值用逗号隔开
    例如:sizes=”(max-width: 300px) 50vw, 50vw”,
    表示当视区宽度不大于300像素时候,图片的宽度限制为50vw,其他情况下,使用50vw,那么当视区宽度为200像素时,图片尺寸等于100px*dpr,
    假设dpr为2,那么就匹配最接近的即150w

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <img srcset="
    https://picsum.photos/id/164/150 150w,
    https://picsum.photos/id/164/300 300w
    https://picsum.photos/id/164/600 600w,
    https://picsum.photos/id/164/900 900w,
    https://picsum.photos/id/164/1200 1200w,
    "
    sizes="(max-width: 300px) 50vw,
    (max-width: 600px) 50vw,
    (max-width: 900px) 50vw,
    (max-width: 1200px) 50vw,
    50vw
    ">

    28.es2023中的数组纯函数

    es2023的新数组函数:toSorted、toReversed、toSliced、with(index, value)-arr[index] = value
    这些数组函数不会改变原数组的值只会返回更改后的值

    1
    2
    3
    4
    5
    6
    // let arr = [1, 7, 3]
    // arr.sort((a, b) => a + b)
    // console.log(arr) // [1, 3, 7]
    let arr = [1, 7, 3]
    let sortArr = arr.toSorted((a, b) => a + b)
    console.log(sortArr, arr) // [1, 3, 7] [1, 7, 3]

    29.动态执行js

    1.使用eval: 同步代码,当前作用域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var a = 1
    function exec(code) {
    var a = 2
    eval(code)
    }
    exec('console.log("a", a)')
    console.log('sync')
    // a 2
    // 'sync'
    // eval: 同步代码,当前作用域

    2.setTimeout: 异步,全局作用域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var a = 1
    function exec(code) {
    var a = 2
    setTimeout(code, 0)
    }
    exec('console.log("a", a)')
    console.log('asynchronous')
    // 'asynchronous'
    // a 1

    3.创建script标签加入body: 同步,全局作用域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var a = 1
    function exec(code) {
    var a = 2
    const script = document.createElement('script')
    script.innerHTML = code
    document.body.appendChild(script)
    }
    exec('console.log("a", a)')
    console.log('sync')
    // a 1
    // 'sync'

    4.Function: 同步,全局作用域, 无需创建多余的标签

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ar a = 1
    function exec(code) {
    var a = 2
    let fn = new Function(code) // 创建一个function,函数体为code
    fn()
    }
    exec('console.log("a", a)')
    console.log('sync')
    // a 1
    // 'sync'

    node模块查找策略

    代码使用require、import,根据路径怎么查找文件
    require(‘./a’) require(‘/a’)

    文件查找:

    按照路径查找a,后缀补js查找a.js找不到查找a.json还找不到去查找文件夹

    文件夹查找:

    首先查看a文件夹下面是否有package.json文件,配置”main”: “./t.js”,就去找a/t.js
    如果a文件夹下面没有package.json文件或者没有配置main或者配置了一个不存在的路径,就去查找
    a文件夹下面有没有index.js, index.json文件

    require(‘a’):

    内置模块:

    如果路径直接写名字,首先会去内置模块查找,比如node内置模块http,fs, path等
    如果a在内置模块就使用,不是就去第三方模块查找

    第三方模块:

    从node_modules查找,如果当前node_modules没有a,就去上级目录查找

    vue状态仓库持久化:vuex、pina

    vuex、pina都是存储在内存里,一旦刷新页面就没了

    vuex:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // index.js
    import { createStore } from 'vuex'
    import counter from './counter'
    import persistPligin from "./persistPligin"

    const store = createStore({
    modules: {
    counter
    },
    plugins: [persistPligin]
    })
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // persistPligin.js
    const KEY = 'vuex-store'
    export default function(store) {
    // 保存仓库数据到本地
    console.log(store)
    window.addEventListener('beforeunload', () => {
    localStorage.setItem(KEY, JSON.stringify(store.state))
    })
    // 恢复仓库数据
    try {
    const localState = localStorage.getItem(KEY)
    if (localState) {
    store.replaceState(JSON.parse(localState))
    }
    } catch (error) {
    console.log(error)
    }
    }

    或者使用第三方库比如vuex-persist

    pina:

    1
    2
    3
    4
    5
    // main.js
    import { createPinia } from 'pinia'
    import persistPligin from "./store/persistPligin"
    const pinia = createPinia()
    pinia.use(persistPligin)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // persistPligin.js
    const KEY_PREFIX = 'pina_store_'
    export default function(context) {
    const { store, id } = context
    const KEY = KEY_PREFIX + id
    // 保存仓库数据到本地
    window.addEventListener('beforeunload', () => {
    localStorage.setItem(KEY, JSON.stringify(store.$state))
    })
    // 恢复仓库数据
    try {
    const localState = localStorage.getItem(KEY)
    if (localState) {
    store.$patch(JSON.parse(localState))
    }
    } catch (error) {
    console.log(error)
    }
    }

    也可以使用第三方插件实现持久话存储

    css选择器focus-within、has、 first-letter、selection

    :focus-within CSS 伪类表示当元素或其任意后代元素被聚焦时,将匹配该元素.

    CSS 函数式伪类 :has() 表示一个元素,如果作为参数传递的任何相对选择器在锚定到该元素时,至少匹配一个元素.

    1
    2
    3
    h1:has(+ h2) {
    margin: 0 0 0.25rem 0;
    }

    CSS 伪元素 ::first-letter会选中某 block-level element(块级元素)第一行的第一个字母,并且文字所处的行之前没有其他内容

    ::selection CSS 伪元素应用于文档中被用户高亮的部分(比如使用鼠标或其他选择设备选中的部分)

    文件上传、文件夹、拖拽上传

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>上传文件-支持文件夹-拖拽等</title>
    <style>
    .drop {
    width: 800px;
    height: 300px;
    border: 1px solid black;
    border-radius: 10px;
    }
    </style>
    </head>
    <div class="drop">拖拽</div>
    <!-+ multiple 支持多选文件 -->
    <!-+ webkitdirectory mozdirectory odirectory 文件支持目录选择-->

    <input type="file" id="fileFile" multiple />
    <br>
    <input type="file" id="filepicker" multiple webkitdirectory mozdirectory odirectory />
    <ul id="listing"></ul>
    <script>
    // 选择文件
    document.getElementById("fileFile").addEventListener(
    "change",
    (event) => {
    let output = document.getElementById("listing");
    for (const file of event.target.files) {
    console.log(file)
    let item = document.createElement("li");
    item.textContent = file.name + '----' + file.size;
    output.appendChild(item);
    }
    },
    false,
    );
    // 选择文件夹
    document.getElementById("filepicker").addEventListener(
    "change",
    (event) => {
    let output = document.getElementById("listing");
    for (const file of event.target.files) {
    console.log(file)
    let item = document.createElement("li");
    item.textContent = file.webkitRelativePath;
    output.appendChild(item);
    }
    },
    false,
    );
    // 拖拽
    const drop = document.querySelector('.drop')
    drop.ondragenter = (e) => {
    e.preventDefault()
    }
    drop.ondragover = (e) => {
    e.preventDefault()
    }
    drop.ondrop = (e) => {
    e.preventDefault()
    // 如果只支持拖拽文件
    // const files = e.dataTransfer.files
    // 支持拖拽文件夹
    for (const item of e.dataTransfer.items) {
    const entry = item.webkitGetAsEntry()
    console.log(entry)
    // filesystem: DOMFileSystem: {name: 'http_127.0.0.1_5500:Isolated_AF5320443A64781FE8803A9C403E7049', root: DirectoryEntry}
    // fullPath: "/1.js"
    // isDirectory:false
    // isFile:true
    // name: "1.js"
    if (entry.isDirectory) { // 目录
    const reader = entry.createReader()
    reader.readEntries((entries) => {
    console.log(entries)
    })
    } else { // 文件
    entry.file((f) => {
    console.log(f)
    })
    }
    }
    }

    </script>
    </body>
    </html>

    各浏览器内核

    • Trident:IE
    • Gecko:FireFox
    • Presto:opera前内核
    • Webkit:Chrome,safari
    • Blink:Chrome、opera

    vue组建命名规范

    大驼峰<StarRate>、短横线<star-rate>
    组件命名:

    1
    2
    3
    4
    5
    6
    import StarRate from './StarRate'
    export default {
    components: {
    abc: StarRate // abc是组件真实名称
    }
    }

    组件中的name:

    1
    2
    3
    export default {
    name: 'StartRate'
    }

    这里name的用途:

    • 组件递归引用
    • 调试工具显示
    • 和keep-alive配合

    浏览器自动播放策略

    1、互动后播放

    先尝试自动播放,若发生异常,则引导用户进行互动操作,然后再进行播放

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const vdo = document.querySelector('video')
    const btn = document.querySelector('.play')
    async function play() {
    try {
    await vdo.play()
    btn.style.display = 'none'
    btn.removeEventListener('click', play)
    } catch(e) {
    btn.style.display = 'flex'
    btn.addEventListener('click', play)
    }
    }
    play()

    2、互动后出声

    先静音播放,然后根据是否能自动播放决定是否取消静音:

    1、能自动播放,取消静音
    2、不能自动播放,引导用户进行互动操作后取消静音。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const vdo = document.querySelector('video')
    const btn = document.querySelector('.play')
    function play() {
    vdo.muted = true
    vdo.play()
    const ctx = new AudioContext() // 音频上下文
    const canAutoPlay = ctx.state === 'running' // 音频能够播放
    ctx.close()
    if (canAutoPlay) {
    vdo.muted = false
    btn.style.display = 'none'
    btn.removeEventListener('click', play)
    } else {
    btn.style.display = 'flex'
    btn.addEventListener('click', play)
    }
    }

    web api

    环境: 浏览器、node、微信小程序
    ES: EcmaScript,是一个语言标准,定义语法、标准库,是一个标准文档
    环境API: 浏览器:Web API

    JS:ES + Web API

    数字字面量

    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
    33
    34
    35
    01
    // output: 1
    08
    // output: 8
    09
    // output: 9
    010
    // output: 8 // 数字字面量前面加0,尝试转换8进制,转换不成功转成10进制

    // 八进制0o
    0o8
    // output: caught SyntaxError: Invalid or unexpected token
    0o10
    // output:8
    0o11
    // output:9

    // 十六进制0x
    0x123456
    // output: 1193046
    // 二进制0b
    0b010101
    // output:21

    1e10
    // 10000000000
    3.14e10
    // 31400000000

    11.toString()
    // VM2977:1 Uncaught SyntaxError: Invalid or unexpected token
    // 11.会认为是小数
    11..toString()
    // '11'
    // 第一个点是小数点,第二个点是调用方法的点

    BigInt

    JavaScript中,所有数字都以双精度64位浮点格式表示,这是由IEEE 754-2008标准定义

    BigInt是一种新的数据类型,用于当整数值大于Number 数据类型支持的范围时.不能使用NumberBigInt操作数的组合来执行算术运算.

    JavaScript中的Number 类型只能安全地表示 -9007199254740991 (-(253-1))和 9007199254740991 (253-1)之间的整数。任何超出此范围的整数值都可能丢失精度。

    随机数

    随机16进制颜色

    1
    2
    3
    4
    5
    function randomColor() {
    return '#' + Math.random().toString(16).substring(2, 8).padEnd(6, '0')
    }
    randomColor()
    // '#f371b3'

    随机字符串:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function randomStr(len = 6) {
    // Math.random().toString(36): '0.d88kqf4iy54' 截取0. 后11位
    if (len <= 11) {
    return Math.random().toString(36).substring(2, 2+len).padEnd(len, '0')

    } else {
    return randomStr(11) + randomStr(len + 11)
    }
    }
    randomStr(20)
    // '3oc4csdw5pra4e5zw1tc'

    数字格式化

    1
    2
    3
    4
    5
    const str = '100000000000'
    // 前瞻运算符匹配位置
    const r = str.replace(/\B(?=(\d{3})+$)/g, ',')
    console.log(r)
    // 100,000,000,000

    (?=)前瞻运算符匹配位置
    (\d{3})+ 连续3个数字匹配1-多次
    \B非单词边界,即: ,100,1000,1000, 1前面边界

    层叠规则

    computed style样式计算: 声明、层叠、继承、默认
    visual fomatting model视觉格式化模型

    层叠:解决样式冲突

    • 比较优先级:

      • 作者样式表!important
      • 默认样式表!important
      • 作者样式表
      • 默认样式表
    • 比较特殊性(权重)(四位:?,?,?,?)高位相同比较下一位

      • 第一位:内联样式style为1,否则为0
      • 第二位:元素所属选择器所有的id个数,1个id为1,100个id为100
      • 第三位:元素所属选择器所有的类选择器+属性选择器+伪类选择器的个数
      • 第四位:元素所属选择器所有的元素选择器+伪元素选择器的个数
    • 源次序:样式在源代码里面的次序,后面样式覆盖前面

    布尔类型

    • 布尔类型:ture false
    • 布尔判定:假:null undefined false NaN 0 ‘’
      if (xxx)
      xxx || yyy
      xxx && yyy
      xxx ? xxx : yyy
    • 短路运算
      xxx || yyy 最后一个运算的是啥就是啥 console.log(0 || NaN) // NaN
      xxx && yyy console.log(0 && NaN) // 0
      obj.a.b.c.d.e:
      obj && obj.a && obj.a.b && obj.a.b.c && obj.a.b.c.d && obj.a.b.c.d.e
      obj?.a?.b?.c?.d?.e

    赋值运算

    1
    2
    3
    4
    5
    var a = { n: 1}
    var b = a // {n: 1}
    a.x = a = { n: 2} //
    console.log(a.x) // undefined
    console.log(b.x) // { n: 2}

    a.x = (a = {n : 2})
    a的内存空间存一个x属性, a = {n : 2}, 给a重新开辟一块内存,a = {n: 2},此时b = {n: 1, x: undefined},
    将a = { n: 2}的值赋值给a.x(a.x已经计算过,不再查找), 即原先a和b共同的内存空间{n: 1, x: undefined}将{n:2}赋值给x,即b = {n:1, x: {n:2}}

    js数据类型和typeof

    原始类型:Number、String、Boolean、null、undefined、Symbol、BigInt
    对应typeof:number、string、boolean、object、undefined、symbol、bigint
    对象类型:Object
    对应typeof:object、function

    ssr服务端渲染

    什么是 SSR?​
    Vue.js 是一个用于构建客户端应用的框架。默认情况下,Vue 组件的职责是在浏览器中生成和操作 DOM。然而,Vue 也支持将组件在服务端直接渲染成 HTML 字符串,作为服务端响应返回给浏览器,最后在浏览器端将静态的 HTML“激活”(hydrate) 为能够交互的客户端应用。

    一个由服务端渲染的 Vue.js 应用也可以被认为是“同构的”(Isomorphic) 或“通用的”(Universal),因为应用的大部分代码同时运行在服务端和客户端。

    为什么要用 SSR?​
    与客户端的单页应用 (SPA) 相比,SSR 的优势主要在于:

    • 更快的首屏加载:这一点在慢网速或者运行缓慢的设备上尤为重要。服务端渲染的 HTML 无需等到所有的 JavaScript 都下载并执行完成之后才显示,所以你的用户将会更快地看到完整渲染的页面。除此之外,数据获取过程在首次访问时在服务端完成,相比于从客户端获取,可能有更快的数据库连接。这通常可以带来更高的核心 Web 指标评分、更好的用户体验,而对于那些“首屏加载速度与转化率直接相关”的应用来说,这点可能至关重要。

    • 统一的心智模型:你可以使用相同的语言以及相同的声明式、面向组件的心智模型来开发整个应用,而不需要在后端模板系统和前端框架之间来回切换。

    • 更好的 SEO:搜索引擎爬虫可以直接看到完全渲染的页面。

    使用 SSR 时还有一些权衡之处需要考量:

    • 开发中的限制。浏览器端特定的代码只能在某些生命周期钩子中使用;一些外部库可能需要特殊处理才能在服务端渲染的应用中运行。

    • 更多的与构建配置和部署相关的要求。服务端渲染的应用需要一个能让 Node.js 服务器运行的环境,不像完全静态的 SPA 那样可以部署在任意的静态文件服务器上。

    • 更高的服务端负载。在 Node.js 中渲染一个完整的应用要比仅仅托管静态文件更加占用 CPU 资源,因此如果你预期有高流量,请为相应的服务器负载做好准备,并采用合理的缓存策略。

    在为你的应用使用 SSR 之前,你首先应该问自己是否真的需要它。这主要取决于首屏加载速度对应用的重要程度。例如,如果你正在开发一个内部的管理面板,初始加载时的那额外几百毫秒对你来说并不重要,这种情况下使用 SSR 就没有太多必要了。然而,在内容展示速度极其重要的场景下,SSR 可以尽可能地帮你实现最优的初始加载性能。

    SSR vs. SSG​
    静态站点生成 (Static-Site Generation,缩写为 SSG),也被称为预渲染,是另一种流行的构建快速网站的技术。如果用服务端渲染一个页面所需的数据对每个用户来说都是相同的,那么我们可以只渲染一次,提前在构建过程中完成,而不是每次请求进来都重新渲染页面。预渲染的页面生成后作为静态 HTML 文件被服务器托管。

    SSG 保留了和 SSR 应用相同的性能表现:它带来了优秀的首屏加载性能。同时,它比 SSR 应用的花销更小,也更容易部署,因为它输出的是静态 HTML 和资源文件。这里的关键词是静态:SSG 仅可以用于消费静态数据的页面,即数据在构建期间就是已知的,并且在多次部署期间不会改变。每当数据变化时,都需要重新部署。

    如果你调研 SSR 只是为了优化为数不多的营销页面的 SEO (例如 /、/about 和 /contact 等),那么你可能需要 SSG 而不是 SSR。SSG 也非常适合构建基于内容的网站,比如文档站点或者博客。事实上,你现在正在阅读的这个网站就是使用 VitePress 静态生成的,它是一个由 Vue 驱动的静态站点生成器。

    nuxt.js:一个基于 Vue.js 的服务端渲染应用框架.

    作用域

    作用域规定了如何设置变量,当前执行代码对变量的访问权限
    js采用词法作用域即静态作用域,函数定义的时候就已经确定了作用域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var value = 1
    function get() {
    console.log(value)
    }
    function getFn() {
    var value = 2
    get()
    }
    get() // 1
    getFn() // 1

    变量对象:当前代码段中,所有的变量、函数、形参等组成的对象
    全局变量对象variable object VO函数由于被执行才激活actived object AO

    作用域链

    是变量对象的集合,就是变量对象组成的链式结构
    js中函数存在一个隐式属性[[scopes]],保存当前函数的执行上下文,数据结构式链式的,因此被称为作用域链。
    一系列AO对象组成的一个链式结构

    [[scopes]]在函数声明时产生,在函数调用时更新:在函数调用时,将该函数的AO对象压入unnshift到[[scopes]]中

    作用域链的作用:
    作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,访问到window即终止,作用域链的变量向下访问是禁止的。
    函数内部可以访问函数外部的变量,函数外部不可以访问函数内部的变量

    函数执行完后作用域链就断开销毁

    webpack

    • loader:test属性表示哪些文件被转换 use属性定义在进行转换时使用哪个loader

    • css-loader: 将css资源转换成commonjs模块到js中

    • style-loader:将css-loader生成的js模块的css代码插入到页面的style标签中

    • less-loader/sass-loader/stylus-loader:将less、sass、stylus编译成css

    • 处理图片资源:
      webpack4: file-loader, url-loader
      webpack5: asset/resource, asset/inline, asset/source, asset 资源模块类型
      rules: [{ test: /.png$/, type: ‘asset’}]
      将文件输出到指定资源目录:
      rules: [
      {
      test: /.png$/,

      generator: {

      filename: 'static/image/[hash][ext]'

      }
      }
      ]

    • 每次构建前清理dist文件夹:
      output: { clean: true }

    • eslint负责语法检验,prettier负责代码风格检测
      webpack4使用eslint-laoder,webpack5使用eslint-webpack-plugins
      babel负责将es6语法转换为向后兼容的js语法,能够运行在当前浏览器和旧版本浏览器中.babel-loader
      babel-preset-env: 只转换语法,不转换新的API

    • HtmlWebpackPlugin:生成一个html5文件将打包输出的所有bundle都都在script标签引入

    • webpack-dev-server:监听文件改变,自动打包

    • css处理,提取css文件,css-loader和style-loader将css文件打包到js文件中在style标签引入,会使网站出现闪屏
      mini-css-extract-plugin:将css文件单独提取出来,在html文件中通过link引入。

    1
    2
    3
    4
    5
    rules: [
    {
    test: /\.css$/i,
    use: [MiniCssExtractPlugin.loader, "css-loader"],
    }]
    • css兼容性处理:postcss-loader postcss postcss-preset-env
      在package.json中配置需要浏览器兼容的程度: “browserslint: {…}”

    • 优化和压缩css:css-minimizer-webpack-plugin 配置在optimization: { minimizer: [new CssMinimizerPlugin()]}

    • 启用HMR HotMouduleReplacement,热模块更替,在程序运行中添加、删除、更换某个模块的代码而无需加载整个页面,开发时更改某个模块的代码,webpack会默认将所有模块打包编译,速度很慢。devServer: {hot: true},但是hot: true不支持js热模块替换,js更改整个页面都重新刷新.实际使用框架,vue-loader、react-hot-loader解决热更替

    • oneOf: 每个文件只能被一个loader配置处理. rules: [oneOf[…]]

    • 开启cache缓存,每次打包都要经过eslint检查和babel编译,可以缓存之前的检查和编译结果,提高打包速度。在babel-loaderder和eslint-plugin的配置开启cache缓存

    • Thread-loader多进程,当项目越来越大,打包速度更慢,使用多线程同时处理js文件,配置时将此loader放置在所有loader之前

    • TerserWebpackPlugin压缩js代码

    • Tree-shaking:移除未使用代码减少代码体积

    • 减少babel代码体积:@babel/plugin-transform-runtime

    • image-minimizer-webpack-plugin:本地静态图片压缩,减少代码体积

    • 代码分割:打包时会把所有js文件打包到一个js文件中,体积太大如果渲染首页,就应该只加载首页所需的js
      代码分割:
      分割文件,将打包代码分割成多个js文件
      按需加载,需要哪个文件加载哪个
      配置:optimization.splitChunks

    自定义loader:clean-log-loader

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // clean-log-loader
    // 清理console.log
    module.exports = function(context) {
    return context.replace(/console\.log\(.*\);?/g, "")
    }

    // 使用
    {
    test: /\.js$/,
    use: ['./loaders/clean-log-loader']
    } // 自定义loader

    loader分为:pre前置loader、normal普通loader、inline内联loader、post后置loader,优先级pre>normal>inline>post

    总结:

    1.提升开发体验
    使用 Source Map
    让开发或上线时代码报错能有更加准确的错误提示。
    2.提升 webpack提升打包构建速度

    • 使用 HotModuleReplacement
      让开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而使更新速度更快。
    • 使用 Oneof 让资源文件一旦被某个 loader 处理了,就不会继续遍历了,打包速度更快。
    • 使用 Include/Exclude 排除或只检测菜些文件,处理的文件更少,速度更快。
    • 使用 Cache 对 eslint 和 babel处理的结果进行缓存,让第二次打包速度更快。
    • 使用 Thead 多进程处理 eslint 和 babel任务,速度更快。(需要注意的是,进程启动通信都有开销的,要在比较多代码处理时使用才有效果)

    3.减少代码体积.

    • 使用
      Tree Shaking
      剔除了没有使用的多余代码,让代码体积更小。
    • 使用
      @babel/plugin-transform-runtime 插件对 babel进行处理,让辅助代码从中引入,而不是每个文件都生成辅助代码,从而体积更小。
    • 使用
      Image Minimizer 对项目中图片进行压缩,体积更小,请求速度更快。(需要注意的是,如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。)

    4.优化代码运行性能.

    • 使用 Code split 对代码进行分割成多个js文件,从而使单个文件体积更小,并行加载js速度更快。并通过 import 动态导入语法进行按需加载,从而达到需要使用时才加载该资源,不用时不加载资源。
    • 使用 Preload/ Prefetch
      对代码进行提前加载,等未来需要使用时就能直接使用,从而用户体验更好。
    • 使用 Network cache 能对输出资源文件进行更好的命名,将来好做缓存,从而用户体验更好。
      。使用 Core-js 对js进行兼容性处理,让我们代码能运行在低版本浏览器。
    • 使用 PMA 能让代码离线也能访问,从而提升用户体验。

    原型的作用是什么?

    之所以实现原型,是因为javascript语言要实现面向对象,而原型是实现面向对象的方式之一。

    一个能支持面向对象的语音的特点:能判定一个实例的类型。在javascript中通过原型能知道某个对象所属类型。
    原型的存在避免了对象类型的丢失。

    Promise解决了什么问题?

    Promise出现最重要的是为了统一js中的异步实现方案。

    Promise也无法消除回调,它只不过通过链式调用的方式让回调变得可控

    async await

    • async 表示这是一个async函数, await只能用在async函数里面,不能单独使用
    • async函数返回一个Promise对象,await就是等待这个promise的返回结果后,再继续执行
    • await等待的是一个Promise对象,如果不是Promise对象则用Promise.resolve包装
    • 异步async 调用和普通函数的使用方式一样
    • await 相当于 Promise 的 then ,then指的是成功,不指失败
    • await 和 then 的区别就是:then还需要传回调进去,但 await 可以直接得到值
    • await后面表达式promise的状态resolve才会执行后面代码,reject需要捕获错误,状态没有改变后面代码不会执行
      try…catch 可捕获异常,代替了 Promise的 catch

    vue3小记-2024-01-16

    vue3小记

    优点特性

    体积减少、内存占用减少、渲染速度更快

    更好支持ts

    源码:使用Proxy代替defineProperty实现响应式

    重写虚拟DOM实现Tree-shaking

    setup

    • 不能访问this,是undefined
    • 在beforeCreate钩子之前执行,领先所有钩子
    • 返回值是对象或者函数:
      • 返回值是对象,对象里的方法,属性等都可以在模版中使用
      • 返回值是函数,return () => 'hello', 则返回内容渲染到页面

    setup和options API的关系:

    • setup中的属性、方法可以在data、methods中访问
    • data、methods中的配置不能在setup中使用
    • 两者冲突,setup优先

    ref定义响应式对象

    ref用来定义响应式变量,基本数据类型或者对象类型都可以,返回一个RefImpl对象,简称ref对象或者ref,ref对象的value属性是响应式的。若定义的是对象类型的变量,内部也会调用reactive函数

    let name = ref('zhangsan'),name不是响应式的,name.value才是响应式的,模版可以直接用name

    reactive定义对象类型的响应式对象

    只能定义对象类型的响应式对象,基本类型用ref

    let person = reactive({ name: 'Susan', age: 20}),是深层次结构的响应式对象

    注意:person = reactive({name: 'susan', age: 21})会断开响应式的连接,若需修改整个对象可使用

    person = Object.assign(person, {...})

    ref和reactive的对比

    ref用来定义:基本类型数据、对象类型数据

    reactive用来定义:对象类型数据

    区别:

    • ref对象使用value属性访问
    • reactive重新分配一个对象会失去响应式,可以用Object.assign替换整体

    使用:

    • 基本数据类型必用ref定义
    • 层级不深的响应式对象可以用ref也可以用reactive
    • 层级深的响应式对象用reactive

    toRefs和toRef

    作用:将一个响应式对象的属性转换为ref对象,且依然保持响应式的能力

    区别:toRefs可以批量转换多个属性,toRef单个

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import { reactive, toRefs, toRef } from 'vue'
    let person = reactive({
    name: 'susan',
    age: 23,
    gender: 'woman'
    })
    // 通过toRefs将person对象中的n个属性批量取出,且依然保持响应式的能力
    let {name, age} = toRefs(person)
    // 通过toRef将person对象中的gender属性取出,且依然保持响应式的能力
    let gender = toRef(person, 'gender')

    computed计算属性

    根据已有数据计算出新数据

    只读计算属性、可修改计算属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import {ref, computed} from 'vue'
    let firstName = ref('zhang')
    let lastName = ref('san')
    let readonlyFullName = computed(()=>{
    return firstName.value + '-' + lastName.value
    })
    let writableFullName = computed({
    get() {
    return firstName.value + '-' + lastName.value
    },
    set(value) {
    console.log('value has been updated', value)
    firstName.value = value.split('-')[0]
    lastName.value = value.split('-')[1]
    }
    })
    function changeFullName(){
    fullName.value = 'li-si'
    }

    watch监听数据

    第一个参数:侦听源

    第二个参数:数据变化回调函数

    第三个参数:配置项:deep,immediate…

    侦听源:

    • 函数返回一个值(getter函数)
    • ref对象
    • 响应式对象(reactive)
    • 以上组合的数组

    当侦听ref对象或reactive对象时,newValue和oldValue相等,因为对象的地址一样,除非响应式连接丢失

    当侦听源是响应式对象(reactive定义)时,侦听器会自动启用深层模式且不可关闭

    如果侦听对象的某个属性,其值为基本类型,必须使用函数返回值形式:newValue和oldValue不相等

    如果其值为对象类型,可直接使用watch(obj.b, ()=>{},{deep:true})也可以使用函数式watch(()=>obj.b,()=>{})

    watch也可监听多个源:

    1
    2
    3
    watch([()=>person.name,person.car],(newValue,oldValue)=>{
    console.log('person.car变化了',newValue,oldValue)
    },{deep:true})

    watchEffect

    作用:立即运行一个函数,响应式的追踪其依赖,并在依赖变化时执行函数

    1
    2
    3
    4
    5
    6
    7
    // 当高度大于等于180时,取消监听
    const stopWatch = watchEffect(()=>{
    if (height >= 180) {
    // do something
    stopWatch()
    }
    })

    和watch的对比:

    • 都能监听数据的响应式变化
    • watch需要明确指出数据源
    • watchEffect不用明确指出数据源,根据函数中用到哪些属性就监听哪些数据

    标签的ref属性

    用于注册模版的引用

    • 用在普通元素标签上获取的DOM节点
    • 用在组件标签上获取组件的实例对象

    使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // Child.vue
    <script setup>
    import { ref } from 'vue'
    const a = 1
    const b = ref(2)

    // 像 defineExpose 这样的编译器宏不需要导入
    defineExpose({
    a,
    b
    })
    </script>
    // Parent.vue
    <template>
    <Child ref='c1'/>
    </template>
    <script setup>
    import { ref } from 'vue'
    const c1 = ref()
    cl.value.a // 1
    cl.value.b // 2
    // 如果子组件没有显示暴露则a, b不可访问
    </script>

    props属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <script lang="ts" setup name="Person">
    import {defineProps} from 'vue'
    import {type PersonInter} from '@/types'

    // 第一种写法:仅接收
    // const props = defineProps(['list'])

    // 第二种写法:接收+限制类型
    // defineProps<{list:Persons}>()

    // 第三种写法:接收+限制类型+指定默认值+限制必要性
    let props = withDefaults(defineProps<{list?:Persons}>(),{
    list:()=>[{id:'asdasg01',name:'小猪佩奇',age:18}]
    })
    console.log(props)
    </script>

    生命周期

    分为四个阶段:创建、挂载、更新、销毁

    vue3生命周期:

    创建阶段:setup

    挂载阶段:onBeforeMount、onMounted

    更新阶段:onBeforeUpdate、onUpdated

    销毁阶段:onBeforeUnmount、onUnmounted

    vue3生命周期1

    vue2生命周期:

    创建阶段:beforeCreate、created

    挂载阶段:beforeMount、mounted

    更新阶段:beforeUpdate、updated

    销毁阶段:beforeDetroy、destroyed

    vue2生命周期

    key的作用

    特殊的 key attribute 被作为 Vue 的虚拟 DOM 算法的提示,以保持对节点身份的持续跟踪。这样 Vue 就可以知道何时能够重用和修补现有节点,以及何时需要对它们重新排序或重新创建。

    vue3非兼容性改变

    全局API

    • 全局API更改为使用应用程序实例

      2.x全局API 3.x实例API
      Vue.component app.component
      Vue.directive app.directive
      Vue.config app.config
      Vue.mixin app.mixin
      Vue.use app.use
      Vue.prototype app.config.globalProperties
    • 全局和内部API都已经经过重构,现已支持TreeShaking(摇树优化)

    模版指令

    • 1、v-model在组件上使用已经重新设计,替换掉v-bind.sync

      • 用于自定义组件时,默认的prop属性和事件名已更改:

        • prop:value->modelValue
        • event: input->update:modelValue
      • v-bind的sync修饰符和组件的model属性已移除

      • 新增:组件上支持绑定多个v-model

      • 新增:v-model修饰符可自定义

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        <ChildComponent v-model="pageTitle" />

        <!-+ 是以下的简写: -->

        <ChildComponent
        :modelValue="pageTitle"
        @update:modelValue="pageTitle = $event"
        />
        <!-+ child -->
        defineProps: ['modelValue']

        <!-+ 绑定多个值: -->
        <ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />

        <!-+ 是以下的简写: -->

        <ChildComponent
        :title="pageTitle"
        @update:title="pageTitle = $event"
        :content="pageContent"
        @update:content="pageContent = $event"
        />
        <!-+ child -->
        defineProps: ['title','pageContent']
    • 2、在<template v-for>和v-if等分支使用key发生了变化

      • 新增:在v-if/v-else/v-else-if等分支可以不再使用key,key现在是自动生成的,如果要使用key,必须保证各分支key是唯一的
      • <template v-for>上key不再添加到子组件,直接添加到template上
    • v-ifv-for用在同个元素身上优先级发生了变化

      • 2.x中v-ifv-for作用在同一个元素,v-for优先作用
      • 3.x中v-ifv-for作用在同一个元素,v-if优先作用
    • v-bind="object"现在是顺序敏感的

      • v-bind="object"的绑定顺序影响渲染结果

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        2.x:独立 attribute 覆盖 object 中定义的内容
        <!-+ 模板 -->
        <div id="red" v-bind="{ id: 'blue' }"></div>
        <!-+ 结果 -->
        <div id="red"></div>

        3.x: 绑定顺序影响渲染结果
        <!-+ 模板 -->
        <div id="red" v-bind="{ id: 'blue' }"></div>
        <!-+ 结果 -->
        <div id="blue"></div>

        <!-+ 模板 -->
        <div v-bind="{ id: 'blue' }" id="red"></div>
        <!-+ 结果 -->
        <div id="red"></div>

    • v-on:event.native`事件修饰符移除

      • v-on.native 修饰符已被移除。同时,新增的 emits 选项允许子组件定义真正会被触发的事件。

    组件

    • 函数式组件只能通过纯函数创建
      • 函数式组件应用场景:(1)函数式组件比有状态组件初始化快得多(2)可以返回多个根组件
      • 变化:
        • 3.x中有状态组件性能提升和函数式组件差距几乎不计,同时支持返回多个根节点
        • 函数式组件只能接收props和context(attrs、slots、emit)的普通函数创建
        • 去掉单文件SFC中的template标签的functional属性
        • 去掉函数创建组件的functional:true选项
    • 单文件组件SFC的template标签的functional属性和函数创建的组件的functional:true选项已移除
    • 异步组件只能通过defineAsyncComponent方法进行创建
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // vue异步组件是vue组件延迟加载的技术,特别是在大型应用中,在某种特定情况下或组件本身很大加载需要一定时间。
    // 通过异步组件,可以避免在应用启动时加载全部组件,实现按需加载。
    // 1.使用import()返回promie的js方法
    const AsyncComponent = import('./AsyncComponent.vue')
    // 2.使用组件选项
    const AsyncComponent = {
    component: () => import('./AsyncComponent.vue'), // 返回一个 Promise,解析为要加载的组件。
    loading: LoadingComponent, // 一个组件,在异步组件加载时显示。
    error: ErrorComponent, // 一个组件,在异步组件加载失败时显示。
    delay: 200, // 在显示加载组件之前的延迟时间(以毫秒为单位)。
    timeout: 3000 // 在放弃加载并显示错误组件之前等待的最长时间(以毫秒为单位)。
    }
    • 组件事件通过emits选项声明,定义子组件向父组件触发的事件

    渲染函数

    • 渲染函数API已更改
      • h函数需要全局导入,不作为渲染函数的参数render(h,…)
      • 渲染函数的参数重写,有状态组件和函数式组件表现更加一致
      • VNode具有更扁平化的prop结构
    • $scopeSlots属性已移除,所有插槽都通过$slots作为函数暴露
    • $listeners属性移除整合到$attrs中
    • $attrs现在包含class和style属性
      • $attrs现在包含了所有传递给组件的 attribute,包括classstyle,在vue2中,class和style不在$attrs,被应用到组件的根元素

    自定义元素

    • 自定义元素检测在模版编译时执行而不在运行时
    • is属性限制只能在component标签使用

    其他小改变

    • beforeDestroy、destroy生命周期更改为onBeforeUnmount、onUnmouted
    • 组件的props属性的default工厂函数不再支持访问this上下文
    • 自定义指令的生命周期已更改为与组件生命周期保持一致,且移除binding.expression
    • 监听组件的生命周期由@hook:改为@vue:
    • data选项只能被声明为一个函数
    • mixin的data合并只合并浅层
    • Transition的class命名已更改:v-enter->v-enter-from
    • TransitionGroup不再默认渲染包裹元素
    • 当侦听watch一个数组时,数组被替换才能监听到,数组变更监听不到需要开启deep
    • 没有特殊指令的template元素被渲染成普通元素,并将渲染为原生的 <template> 元素,而不是渲染其内部内容
    • vue2:当挂载一个具有template的应用时,会替换挂载目标,vue3中被渲染为挂载目标的子元素,即innerHTML

    被移除的API

    • 移除keyCode作为v-on修饰符的支持
    • 移除filter过滤器
    • 移除$on,$off,$once
    • 移除$destroy实例方法
    • 移除$children,建议使用模版引用ref
    • 全局的set和delete方法以及实例方法$set,$delete

    ———————————————-这里是分割线———————————————-

    vue2小记

    vue是用于创建用户界面的渐进式框架。

    2016,2.0发布

    2020,3.0发布

    vue特点

    • 采用组件化模式,提高代码复用率,且让代码更好维护
    • 声明式编码,模板语法来声明式地将数据渲染进 DOM 的系统
    • 使用虚拟dom+diff算法,尽量复用dom节点

    模版语法

    • 插值语法:,用于解析标签体内容
    • 指令语法:v-bind, v-if…,用于解析标签属性,解析标签体内容,绑定事件

    V-bind:单向数据绑定,数据只能从data流向页面

    V-model: 双向数据绑定-用于表单元素,数据不仅能从data流向页面,也能从页面流向data

    MVVM

    • m:model模型,对应data中的数据
    • v:view试图负责用户界面,对应模板
    • vm:视图模型,vue实例对象,连接view和model之间的桥梁

    View——-viewmodel: dom listeners——>model

    view<—–viewmodel: data binding———-model

    View:Dom viewmodel: vue. Model: js object

    viewmodel包含model的数据和view的逻辑,包括数据绑定、事件处理等。viewmodel将数据从model映射到view,并处理用户的交互;当model数据发生改变时,viewmodel更新view以反映变化。

    mvvm的核心优势在于它提供了清晰的职责分离和关注点分离。Model只关心数据和业务逻辑;view只负责界面和布局;viewmodel负责将model的数据和view的显示结合起来,并处理用户的交互操作。

    defineProperty

    • value
    • Configurable: 是否可配置的,可删除,默认false
    • writable:是否可修改,默认false
    • enumable:是否可枚举,默认false
    • get
    • set

    vue中数据代理

    1.vue中数据代理:vm._data.message,也可message

    通过vm对象来代理data中的属性的操作-读/写

    2.vue中数据代理的好处:

    更加方便的操作data中的数据

    3.基本原理:

    通过Object.defineProperty()将data中所有属性添加到vm上

    为每一个添加到vm上的属性都添加getter/setter

    在getter/setter内部去操作(读/写)data中对应的属性

    事件监听

    1.绑定监听:

    • v-on:xxx=”fun”
    • @xxx=”fun”
    • @xxx=”func(params)”
    • 默认形参event
    • 隐含属性对象:$event

    2.事件修饰符

    • prevent: @event.prevent阻止事件的默认行为event.preventDefault()
    • stop: @event.stop阻止事件冒泡行为:event.stopPropagation()
    • once: @event.once事件只出发一次
    • capture: @event.capture使用事件的捕获模式,由外到内,默认是冒泡模式,由内向外
    • self:只有event.target是元素自身时才触发
    • passive:立即执行事件的默认行为,无需等待事件回调函数执行完毕,注意:不是所有事件都先执行回调再执行默认行为,比如scroll先滚动再执行回调,但是wheel滚轮事件先执行回调再执行默认行为
    • native:把当前元素当作原生标签使用

    3.按键修饰符:

    按键码:

    • .enter
    • .delete(捕获“删除”和“退格”键)
    • .tab特殊,必须配合keydown事件使用
    • .esc
    • .space
    • .up
    • .down
    • .left
    • .right

    系统修饰健:

    • .ctrl
    • .alt
    • .shift
    • .meta

    .exact 修饰符允许你控制由精确的系统修饰符组合触发的事件。

    1
    2
    3
    4
    5
    6
    7
    8
    <!-+ 即使 Alt 或 Shift 被一同按下时也会触发 -->
    <button v-on:click.ctrl="onClick">A</button>

    <!-+ 有且只有 Ctrl 被按下的时候才触发 -->
    <button v-on:click.ctrl.exact="onCtrlClick">A</button>

    <!-+ 没有任何系统修饰符被按下的时候才触发 -->
    <button v-on:click.exact="onClick">A</button>

    你还可以通过全局 config.keyCodes 对象自定义按键修饰符别名

    1
    2
    // 可以使用 `v-on:keyup.f1`
    Vue.config.keyCodes.f1 = 112

    4、表单修饰符

    • .lazy 修饰符的作用是将v-model从实时监听的状态变为change状态。换句话说,它会使v-model在输入框失去焦点或者按下回车键时才更新数据.只适用于v-model指令
    • .trim 删除内容前后空格
    • .number 输入时数字或转为数字

    scroll和wheel事件

    scroll是页面滚动事件,滚动到底部后不会再触发

    wheel是鼠标滚轮事件,滚动到底部后继续滚轮还会触发

    computed计算属性

    计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。如果你不希望有缓存,请用方法来替代。

    计算属性默认只有 getter,不过在需要时你也可以提供一个 setter:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    computed: {
    fullName: {
    // getter
    get: function () {
    return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
    var names = newValue.split(' ')
    this.firstName = names[0]
    this.lastName = names[names.length + 1]
    }
    }
    }

    现在再运行 vm.fullName = 'John Doe' 时,setter 会被调用,vm.firstNamevm.lastName 也会相应地被更新

    侦听器watch

    当需要在数据变化时执行异步或开销较大的操作时,watch更有用

    条件渲染

    v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染。

    另一个用于根据条件展示元素的选项是 v-show 指令。不同的是带有 v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 CSS property display

    v-if vs v-show

    v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。

    v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

    相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

    一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

    v-ifv-for 一起使用时,v-for 具有比 v-if 更高的优先级.

    key的作用

    key的特殊attribute主要用于vue的虚拟dom算法,在新旧节点比对时标识vnodes。如果不使用key,vue会最大限度的减少使用动态元素且尽可能的尝试就地修改/复用元素。而使用key,会根据key的变化重新排列元素,并移除key不存在的元素。

    深入响应式原理

    如何追踪变化?

    当把一个普通js对象传给data选项时,vue会遍历data中所有属性,并使用Object.defineProperty()方法将属性转换为getter/setter。

    每个组件实例都对应一个watcher实例,它会把组件渲染过程中所接触过的所有属性记录为依赖。之后当依赖项的setter触发时,会通知watcher实例,从而使它关联的组件重新渲染。

    image

    检测变化的注意事项

    对于对象:无法监听到对象属性的添加/删除,可使用Vue.set()/delete()或者实例的vm.$set()/$delete

    对于数组:1.无法监听到通过数组下标修改值,可使用set或splice方法解决; 2.无法监听到数组长度变化:arr.length = 10,可使用splice方法解决;

    Vue.set/delete,注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象:即不能往vm或者vm._data中添加/删除属性

    数组更新检测

    Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:

    • push()
    • pop()
    • shift()
    • unshift()
    • splice()
    • sort()
    • reverse()

    异步更新队列

    vue在更新dom时是异步的。

    当侦听到数据变化时,会开启一个队列,缓冲同一个事件循环队列中所有数据变化。每个组件实例都对应一个watcher实例。当同一个watcher实例被多次触发时,只会推入队列一次。组件数据变更时,vue会在下一次事件循环“tick”中更新组件。

    vue在异步队列中尝试使用原生的Promise.thenMutationObserversetImmdiate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

    MutationObserver: 是Web API的一个接口,允许你监听DOM树的变化,包括元素的增、删、属性的改变等。当这些变化发生时,MutationObserver 会触发一个回调函数,使得开发者能够对这些变化做出响应。MutationObserver是异步触发的,也就是说,dom发生变动时并不会立即出发MutationObserver,而是等当前dom操作完成后才触发。‘

    setImmediate是nodejs的一个函数,把一个函数添加到事件循环的当前迭代的末尾,使得该函数在当前事件循环迭代结束前执行。比setTimeout优先级高,因为它在当前事件循环的末尾执行,而 setTimeout 则要等待一定的延迟时间。不是一个webapi不能在浏览器使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 回调函数,当 DOM 变化时被调用  
    const callback = function(mutationsList, observer) {
    // 遍历所有变化
    for(let mutation of mutationsList) {
    if (mutation.type === 'childList') {
    console.log('子节点已改变:', mutation);
    }
    }
    };

    // 创建 MutationObserver 实例并传入回调函数
    const observer = new MutationObserver(callback);

    // 配置观察选项
    const config = { attributes: false, childList: true, subtree: true };

    // 选择要观察的 DOM 节点
    const targetNode = document.getElementById('some-id');

    // 开始观察目标节点
    observer.observe(targetNode, config);

    // 稍后,当不再需要观察时,可以停止观察
    // observer.disconnect();

    内置指令

    v-text: 更新元素的textContent

    v-html: 更新元素的innerHTML

    v-if:条件为true,才会渲染到页面

    v-else:分支

    v-show:不管条件是否为真,都渲染到页面,通过控制display属性控制显示/隐藏

    v-for:遍历对象/数组

    v-on:绑定事件监听,简写@

    v-bind:绑定解析表达式。可以简写为:

    v-model:双向数据绑定

    v-clock:防止闪现,避免网速慢导致页面呈现未经vue编译的页面<h>{{ xx }}</h>,配合[v-cloak]{display:none}使用

    自定义指令

    全局

    1
    2
    3
    4
    //简写时代表 bind 和 update 时触发相同行为
    Vue.directive('my-dir', funtion(el, binding) {
    el.innerHTML = binding.value.toUpperCase()
    })

    局部指令:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    new Vue({
    ...
    directives: {
    'my-dir': {
    bind(el, binding) {
    ...
    },
    inserted(el, binding) {
    ...
    }
    }
    }
    })
    <h v-my-dir='xxx'></h>

    指令何时被调用?

    1.指令与元素第一次绑定时(bind)

    2.模版被重新解析时(update)

    重要的指令生命周期:

    bind(el, binding, vnode, oldNode): 指令与元素第一次绑定时

    Inserted(el, binding, vnode, oldNode): 被绑定元素插入父节点时调用

    Update(el, binding, vnode, oldNode): 指令所在模版被重新解析时

    组件生命周期

    https://v2.cn.vuejs.org/images/lifecycle.png

    • new Vue()

    • Init event & lifecycle:初始化生命周期、事件

    • beforeCreate:data和methods不可访问

    • 初始化:数据代理、数据监测

    • created:可访问data中数据、methods方法

    • vue开始编译模版,生成虚拟dom(内存中)

    • beforeMount:页面呈现未经vue编译的dom(<h>{{ xx }}</h>),dom不可操作

    • 将内存中虚拟dom生成真实dom并插入到页面

    • mounted:页面呈现经过编译的dom,可操作dom

    数据变化

    • beforeUpate:数据已更新但页面还是旧的

    • 生成新的虚拟dom与旧的虚拟节点比对patch

    • Updated:数据和页面保持同步

    • 调用vm.$destroy方法

    • beforeDestroy:此时data、methods、指令还可用,但操作数据不再更新

    • 移除watchers、子组件、和事件监听(移除自定义事件,原生事件保留)

    • destroyed:组件实例销毁

    组件化编程

    模块:

    向外提供特定功能的js程序,一般就是一个js文件

    作用:复用js,简化js的编写,提高js运行效率

    组件:

    用来实现特定功能的代码集合(html、css、js、images)

    作用:复用编码,简化项目编码,提高运行效率

    模块化:

    当应用中的js都以模块来编写这个应用就是一个模块化的应用。

    组件化:

    当应用中的功能都是多组件的方式来编写这个应用就是一个组件化的应用

    vue的版本

    • vue.js完整版的版本。包含:核心功能+模版解析器
    • vue.runtime.js运行时的版本,包含:核心功能

    单文件组件引入的是运行时的版本,因为运行时版本没有模版解析器,所以不能使用template选项,需要使用render函数接收到的createElement函数来指定渲染内容。

    1
    2
    3
    4
    5
    6
    7
    new Vue({
    render: h =>h('h1', 'hello')
    })
    new Vue({
    el: '#app'
    template: `<app></app>`
    })

    以及使用单文件组件需要vue-template-complier包来解析模版(vue2.x)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    new Vue({
    ...,
    render: h => h(App),
    }).$mount('#app')
    // 等价于
    new Vue({
    el: '#app',
    render: createElement => createElement(App),
    })

    h 作为 createElement 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的。

    createElement 到底返回的不是一个真实的dom,是节点描述信息,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode

    混入mixin

    通过混入来分发组件中的可复用功能。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

    选项合并:

    • 数据对象data在内部会进行递归合并,发生冲突时以组件数据优先
    • 同名钩子函数合并为一个数组,并且混入对象钩子函数在组件自身钩子函数之前调用
    • 值为对象的选项,比如methods、components、directives,将会合并为一个对象,当键名冲突时,使用组件对象的键值对

    注意:Vue.extend() 也使用同样的策略进行合并。

    混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 Vue 实例

    插件

    插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:

    • 添加全局属性或方法
    • 添加全局的资源:指令、过滤器、过渡等
    • 通过全局混入添加一些组件选项,比如vue-router
    • 添加vue实例方法通过把他们添加到Vue.prototype上
    • 一个库,提供自己的api,同时具有以上一个多个功能,如vue-router

    使用插件

    通过Vue.use(xxx), 在new Vue()之前调用

    1
    2
    3
    4
    5
    6
    // 调用 `MyPlugin.install(Vue)`
    Vue.use(MyPlugin)

    new Vue({
    // ...组件选项
    })

    开发插件通过install方法,传入Vue构造函数和可选options选项对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    MyPlugin.install = function(Vue, options) {
    // 添加全局方法
    Vue.myGlobalMethods = function (){
    ...
    }
    // 添加全局资源
    Vue.directive('my-directive', function(el, binding) {
    ...
    })
    // 添加全局混入
    Vue.mixin({
    created() {
    ...
    }
    ...
    })
    // 添加实例方法
    Vue.prototype.$myMthods = function() {}
    }

    函数式组件

    无状态 (没有响应式数据),也没有实例 (没有 this 上下文)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Vue.component('my-component', {
    functional: true,
    // Props 是可选的
    props: {
    // ...
    },
    // 为了弥补缺少的实例
    // 提供第二个参数作为上下文
    render: function (createElement, context) {
    // ...
    }
    })
    // 单文件组件-函数式组件
    <template functional>
    </template>

    配置webpack

    Vue 脚手架隐藏了所有 webpack 相关的配置,若想查看具体的 webpakc 配置, 请执行:vue inspect > output.js

    如果想更改webpack配置,根目录下新建vue.config.js或者package.json中配置”vue”选项

    组件传参/组件通信

    1.父子传参,props传递属性和方法:

    1
    2
    3
    4
    5
    6
    <Child :name="name" :getNameFn="getNameFn" />
    getNameFn() {...}
    // child.vue
    props: ['name', 'getNameFn']
    // 调用父组件传递方法来向父组件传参
    this.getNameFn(params)

    2.父子传参,自定义事件:

    1
    2
    3
    4
    5
    6
    <Child :name="name" @getNameFn="getNameFn" />
    getNameFn() {...}
    // child.vue
    props: ['name']
    // 调用子组件自定义方法传参
    this.$emit('getNameFn', params)

    3.父子传参,父组件监听子组件触发事件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <Child :name="name" ref="child" />
    mounted() {
    this.$refs.child.$on('getNameFn', cb)
    // 注意回调函数this指向
    }
    // child.vue
    props: ['name']
    // 调用子组件自定义方法传参
    this.$emit('getNameFn', params)

    4.父子传参,v-bind.sync,子组件修改props传参同步给父组件(双向绑定):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <Child :name.sync="name"/>
    // 等价于
    <Child v-bind:name.sync="name" />
    // 等价于
    <Child :name="name" @update:name="name = $event"/>
    // child
    props: ['name']
    // 子组件修改props传参同步给父组件
    this.$emit('update:name', name)

    5.父子传参,自定义组件的v-model:

    一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件需要配置model选项:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Vue.component('base-checkbox', {
    model: {
    prop: 'checked',
    event: 'change'
    },
    props: {
    checked: Boolean
    },
    template: `
    <input
    type="checkbox"
    v-bind:checked="checked"
    v-on:change="$emit('change', $event.target.checked)"
    >
    `
    })
    // 使用
    <base-checkbox v-model="lovingVue"></base-checkbox>

    这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 <base-checkbox> 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的 property 将会被更新。

    6.跨组件通信,全局事件总线$bus:

    $on(eventName, listener): 绑定自定义事件监听

    $emit(eventName, data): 分发自定义事件

    $off(eventName): 解绑自定义事件

    $once(eventName, listener): 绑定自定义事件只触发一次

    所有组件实例对象的原型对象指向vue的原型对象

    VueComponent.prototype.__proto__ === Vue.protype

    所有vc组件对象都能访问vue原型对象上的属性和方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    new Vue({
    el: '#app',
    beforeCreate() {
    // 尽量早的执行挂载全局事件总线对象的操作
    Vue.prototype.$bus = this
    }
    })
    // 绑定事件
    this.$bus.$on('eventName', cb)
    // 分发事件
    this.$bus.$emit('eventName', params)
    // 解绑事件
    this.$bus.$off('eventName')

    7.跨组件通信,消息订阅与发布:PubSubJS

    1
    2
    3
    4
    5
    6
    7
    8
    // 安装 npm install -S pubsub-js
    import pubsub from 'pubsub-js'
    // 订阅
    pubsub.subscribe('eventName', cb)
    // 发布
    pubsub.publish('eventName', params)
    // 取消订阅
    pubsub.unsubscribe('eventName')

    8.祖孙组件通信,provide/inject

    provide选项允许祖先组件为其后代组件(不论层次多深)提供一个依赖。
    inject选项用来接收祖先组件provide的依赖。

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    <template>  
    <div>
    <h1>Ancestor Component</h1>
    <GrandchildComponent />
    </div>
    </template>

    <script>
    import GrandchildComponent from './GrandchildComponent.vue';

    export default {
    components: {
    GrandchildComponent
    },
    provide() {
    return {
    foo: 'bar',
    someMethod: this.someMethod
    };
    },
    methods: {
    someMethod() {
    // ... some logic here ...
    }
    }
    }
    </script>

    子组件:
    <template>
    <div>
    <h2>Grandchild Component</h2>
    <p>Provided foo: {{ foo }}</p>
    </div>
    </template>

    <script>
    export default {
    inject: ['foo', 'someMethod'],
    mounted() {
    console.log(this.foo); // 'bar'
    this.someMethod(); // 调用由祖先组件提供的方法
    }
    }
    </script>

    9.祖孙组件通信,$attrs,$listeners

    分别用于在组件之间传递属性和事件。
    $attrs是一个对象,包含了父作用域中不被props接收的绑定的属性(v2中除class和style),通过v-bind=”$attrs传入组件,子组件通过this.$attrs获取
    $listeners是一个对象,包含了父作用域中v-on事件监听器(不包含.native修饰器的),通过 v-on=”$listeners” 传入内部组件,子组件通过this.$listeners获取

    插槽

    • 默认插槽default
    • 具名插槽
    • 作用域插槽

    父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

    默认插槽default:

    1
    2
    3
    4
    5
    6
    7
    8
    // 默认插槽-一个不带 name 的 <slot> 出口会带有隐含的名字“default”。
    <slot />
    // 使用
    <my-com>
    <div>
    this is an default slot
    </div>
    </my-com>

    具名插槽:

    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
    <div class="container">
    <header>
    <slot name="header"></slot>
    </header>
    <main>
    <slot></slot>
    </main>
    <footer>
    <slot name="footer"></slot>
    </footer>
    </div>
    // 使用
    <base-layout>
    <template v-slot:header>
    <h1>Here might be a page title</h1>
    </template>

    <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
    </template>

    <template v-slot:footer>
    <p>Here's some contact info</p>
    </template>
    </base-layout>
    // 注意 v-slot 只能添加在 <template> 上
    <div slot="footer">
    <p>Here's some contact info</p>
    </div>


    作用域插槽:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
     <span>
    <slot v-bind:user="user">
    {{ user.lastName }}
    </slot>
    </span>
    // 使用
    <current-user>
    <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
    </template>
    </current-user>


    // 解构插槽
    <current-user>
    <template v-slot="{user}">
    {{ user.firstName }}
    </template>
    </current-user>

    vuex

    是专门为vuejs应用程序开发的状态管理模式。

    适用于:

    • 多个组件依赖于同一个状态
    • 多个组件的行为变更同一个状态

    每个vuex应用的核心是store仓库。存储应用的状态。

    • vuex的状态存储是响应式的

    • 不能直接更改store中的状态,必须显式的提交comit mutation

    核心概念

    • state: vuex管理的状态对象,唯一数据源;Vuex 的状态存储是响应式的
    • getters:从 store 中的 state 中派生出一些状态,包含返回数据的函数。Getter 接受 state 作为其第一个参数。
    • mutations:改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 **回调函数 (handler)**。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    mutations: {
    increment (state, n) {
    state.count += n
    }
    }
    store.commit('increment', 10)

    // 在组件中使用
    import { mapMutations } from 'vuex'
    export default {
    // ...
    methods: {
    ...mapMutations([
    'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

    // `mapMutations` 也支持载荷:
    'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
    add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
    }
    }

    使用常量替代 Mutation 事件类型

    mutation必须是同步函数

    • actions:类似于mutation,不同的是
      • action提交的是mutation,而不是直接修改状态
      • action可以包含任意异步操作
    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
    actions: {
    increment (context) {
    context.commit('increment')
    },
    incrementAsync ({ commit }) {
    setTimeout(() => {
    commit('increment')
    }, 1000)
    }
    }
    // 分发action
    this.$store.dispatch('increment')

    // 在组件中使用
    import { mapActions } from 'vuex'

    export default {
    // ...
    methods: {
    ...mapActions([
    'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

    // `mapActions` 也支持载荷:
    'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
    add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
    }
    }
    • Module:模块
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const moduleA = {
    state: () => ({ ... }),
    mutations: { ... },
    actions: { ... },
    getters: { ... }
    }

    const moduleB = {
    state: () => ({ ... }),
    mutations: { ... },
    actions: { ... }
    }

    const store = new Vuex.Store({
    modules: {
    a: moduleA,
    b: moduleB
    }
    })

    store.state.a // -> moduleA 的状态
    store.state.b // -> moduleB 的状态

    vuex

    栈Stack和堆Heap的区别?

    • 存储方式:
      • 栈:线性数据结构,采用先进后出的原则。基本数据类型和引用数据类型的值存储在栈中
      • 堆:用于动态分配内存的内存池,采用哈希表的数据结构。引用数据类型(数组、对象、函数)的实际值存储在堆中,栈中存储的是是该对象的引用。
    • 内存管理:
      • 栈:内存管理是自动的,通过栈指针的上移和下移来自动分配内存和释放内存。
      • 堆:堆的内存需要程序员手动释放和分配。js中通过垃圾回收器来回收不再使用的对象的内存
    • 大小:
      • 栈:大小是固定的,在程序编译阶段就能确定
      • 堆:大小是动态变化的,需要在运行阶段根据需求动态调整
    • 生命周期
      • 栈:具有短暂的生命周期,在函数或代码块执行完毕就释放内存
      • 堆:具有较长的生命周期,需要手动释放,否则容易内存泄露

    js原型链

    所有对象都是通过new 函数创建的,包括let obj = {}

    函数是通过new Function创建的对象

    每个函数都有一个属性prototype,叫做原型,prototype也是个对象,所以也叫原型对象。

    原型的作用:

    • 1、存放属性和方法共享给实例对象
    • 2、在js中通过原型实现继承

    每个对象都有一个 __proto__ 属性,指向它构造函数的原型对象。

    原型链:

    对象都有__proto__属性,指向他的构造函数的原型对象,原型对象也是对象,也有__proto__属性,指向它的构造函数的原型,这样一层一层形成的链式结构称为原型链,顶层为null。

    prototype 原型对象:

    • 是函数的一个属性
    • 是一个对象
    • 创建函数后自动拥有的一个属性

    __proto__隐式原型:

    • 对象的一个属性
    • 实例的__proto__属性指向它构造函数的prototype

    constructor:

    • 对象的一个属性
    • 指向创建该实例对象的构造函数
    • 除了 null 原型对象之外,任何对象都会在其 [[Prototype]] 上有一个 constructor 属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function testFun(){}
    var obj = new testFun()
    obj.__proto__ === testFun.prototype // true
    testFun.prototype.__proto__ === Object.prototype // true
    Object.prototype.__proto__ === null // true

    obj.constructor === testFun
    // true
    obj.__proto__ === testFun.prototype
    // true
    obj.__proto__ === obj.constructor.prototype
    // true

    对象有的属性:

    • __proto__
    • constructor

    函数有的属性(也是对象):

    • __proto__(作为对象)
    • constructor(作为对象)
    • prototype(作为函数)

    [[Prototype]]和__proto__:

    __proto__ 属性是 Object.prototype 上一个简单的访问器属性,由 getter 和 setter 函数组成.

    __proto__ 的 getter 函数暴露了一个对象内部的 [[Prototype]] 的值。对于使用对象字面量创建的对象,该值是 Object.prototype。对于使用数组字面量创建的对象,该值是 Array.prototype。对于函数,该值是 Function.prototype.

    __proto__ 的 setter 允许修改一个对象的 [[Prototype]]。提供的值必须是一个对象或 null。提供任何其他值都不会产生任何作用

    Ajax、xhr、fetch、axios、promise、jQuery的区别?

    XHR(XMLHttpRequest):XHR是一种用于发送HTTP请求和接收服务器响应的API,支持异步

    Ajax:Ajax(Asynchronous JavaScript and XML)是一种基于XHR的技术。用于在不刷新页面的情况下实现异步数据交互。

    fetch:是现代浏览器原生提供的API,用于进行网络请求。它返回Promise对象。

    axios:第三方封装库,Axios是一个基于Promise的HTTP客户端,用于浏览器和Node.js

    jQuery:JavaScript库,简化了DOM操作和事件处理,也包括Ajax方法。

    promise:提供了更结构化的异步代码,使得回调地狱(callback hell)问题得以解决,提高了可读性。

    ———————————————-这里是分割线———————————————-

    元素垂直水平居中:父元素设置display: flex;子元素设置margin:auto

    padding和margin区别?padding作用于自身,margin作用于外部元素间距

    vw和百分比?vw是可视窗口的width,百分比相对于包含块计算,元素的位置和尺寸由包含块决定,默认是最近祖先块元素的内容区域,width\top\bottom百分比值由包含块的width决定,height\left\right\margin\padding百分比值由包含块的height决定;

    内联元素和块级元素?

    • 块级元素:block,在正常的流中块级元素前后另起一个新行,从上到下排列;可包含行内元素和其他块级元素;块级元素有:h1-h6, article, div, p, footer, table, audio, video, ul, dd, from, canvas等;可设置width, height, padding, margin值
    • 内联元素:inline,不以新行开始,从左到右排列可包含数据和其他行内元素,行内元素有:b, big, i, small, tt, abbr, acronym, cite, code, dfn, em, kbd, strong, samp, var, a, bdo, br, img, map, object, q, script, span, sub, sup, button, input, label, select, textarea;可设置padding,margin-left/right值,不可设置width, height, margin-top/bottom

    如何让浏览器支持小于12px字体?缩放:transform: scale(0.5); transform-origin:left top;

    浅拷贝和深拷贝?

    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
    // 赋值
    let a = 4
    let b = a
    a = 5
    console.log(a, b) // 5 4
    // 解构赋值:一维数组和对象是深拷贝,多维是浅拷贝
    let arr = [[1,2,3], [4,5,6]]
    let arr1 = [...arr]
    arr1[0].push(7) // 浅拷贝
    // Object.assign(obj1, obj2) // 浅拷贝
    // 深拷贝
    JSON.parse(JSON.stringify(obj)) //不能处理undefined、函数、symbol、Date、NaN等值

    // 递归-深拷贝
    function deepClone(value) {
    if (typeof value !== 'object' || value === null) return value
    let obj = Array.isArray(value) ? [] : {}
    Object.setPrototypeOf(obj, Object.getPrototypeOf(value)) // 原型统一
    for (let key in value) {
    if (value.hasOwnProperty(key)) {
    // obj[key] = value[key] // 浅拷贝
    obj[key] = deepClone(value[key])
    }
    }
    return obj
    }

    while循环快还是for循环快?

    for循环比while循环执行速度快。这是因为for循环在内部执行代码块时自增计数器;而while循环需要手动进行边界检查和变量i的自增操作。

    页面上隐藏元素方法?

    • 不占位
      • 给元素设置hidden属性
      • display: none;
      • width: 0;height: 0;transform: scale(0);但是有些元素设置border宽度会占据空间
      • width: 0;height: 0;overflow: hidden;但是有些元素设置border宽度会占据空间
      • position: absolute; left/margin-left: -100%;脱离文档流
      • position: absolute; z-index: -1;脱离文档流,后面元素覆盖该元素,如果后面元素小可能会覆盖不完全
      • font-size: 0;仅对文本内容有效
    • 占位
      • opacity: 0;
      • filter: opacity(0);
      • visibility: hidden;

    元素水平垂直居中?

    • 定位+margin: 已知被水平垂直居中元素宽高,父相对定位,子绝对定位+top:50%+left:50%+margin: -1/2w 0 0 -1/2h;
    • 定位+transform:未知居中元素宽高,父相对定位,子绝对定位+top:50%+left:50%+transform: translate(-50%, -50%)
    • 定位+margin:auto:未知居中元素宽高,父相对定位,子绝对定位+top/left/right/bottom:0+margin: auto;
    • flex: 父元素设置display:flex;aligin-items:center;justify-content:center;
    • Flex/grid+margin:auto:父元素display:flex/grid;子元素margin:auto;

    Css元素选择器有哪些?

    • 通配符选择器*
    • 元素选择器,p, div
    • id选择器
    • 类选择器
    • 属性选择器: a[class=’test’]
    • 伪类选择器::before,:active
    • 组合选择器:div + p,h1,h2

    css三大特性:继承、层叠、优先级

    css可以继承的属性?

    • 字体系列的属性:font-size,font-family,font-style,font-weight…
    • 文本系列的属性:text-align,color,line-height,letter-spacing,word-spacing…
    • 元素的可见性:visibility, opacity
    • 表格布局的属性:border-spacing
    • 列表的属性:list-style
    • 光标的属性:cursor

    css预处理器?

    css预处理器是一种语言为css添加一些编程的特性,可以在css中使用变量,函数等功能,使css更加直观简洁。

    Sass,less,stylus三种预处理器都有:嵌套,颜色函数,混入,继承,运算符等功能。

    JS组成?

    ECMAscript是国际通过的ecma-262标准化的脚本程序设计语言,js是ecmascript的一种实现

    • 浏览器中javascript组成:ECMAscript + BOM(浏览器对象模型) + DOM(文档对象模型)
    • nodejs中的javascript组成:ECMAscript + NPM(包管理系统)+ Native
    • 小程序中的javascript组成:ECMAscript + 小程序框架 + 小程序API

    js内置对象有哪些?

    String,Number,Boolean,Function,Date,Math,RegExp,Array,Object…

    Math.abs(),sqrt(),min();String.concat(),slice(),split(),new Date().getFullYear()…

    操作数组的方法?

    • 改变数组本身的方法:copyWithin() fill() push() pop() unshift() shift() sort() reverse() splice()
    • 不改变数组本身的方法:concat() join() slice() indexOf() lastIndexOf() toString()…
    • 迭代/遍历方法:遍历数组每个元素前每个回调函数执行一次,执行回调函数前先缓存数据长度。forEach() map() filter() some() every() reduce() reduceRight() entries() keys() values() find() findIndex()

    js类型检查方法?

    • typeof: 可以检测基本数据类型,但不能判断对象,数组和null
    • instanceof:判断构造函数的prototype属性是否在实例对象的原型链上,可以判断引用数据类型,但不能判断基本数据类型;依赖于原型链,一旦原型链被修改结果并不准确
    • constructor:返回创建对象的构造函数的引用,可以判断number,string, boolean, object,function, array,但是constructor可以被修改结果也并不准确
    • Object.prototype.toString.call()返回表示对象的字符串,不依赖于原型链,是一个可靠的类型检查方法,但是对象的Symbol.toStringTag属性的值会改变toString返回字符串的结果。
    • Array.isArray()判断一个对象是否是数组

    闭包的理解?

    闭包是由函数以及声明该函数的词法环境组合而成。该环境包含创建闭包时作用域内任何局部变量。

    闭包的特点:

    1、函数嵌套函数 2、函数内部可以访问外部的参数和变量 3、参数和变量不会被垃圾回收机制回收

    闭包是为了设计私有方法和变量。

    优点:避免全局变量的污染

    缺点:由于保留了作用域链,闭包常驻内存,加大内存消耗,使用不当容易造成内存泄露

    解决方法:退出函数前,把不使用的局部变量删除

    事件委托?

    又叫事件代理,基于事件冒泡的机制将事件监听添加到父元素上而不是给子元素添加。事件逐层冒泡到父元素被捕获。

    事件经历三个阶段:

    1、捕获阶段:window对象自上而下到目标对象传播的阶段

    2、目标阶段:真正的目标对象正在处理事件的阶段

    3、冒泡阶段:目标对象从下到上到window对象传播的阶段

    addEventListener(‘eventName’, callback, true/false); 第三个参数,false默认为冒泡,true事件捕获,阻止冒泡,事件委托失效。

    优点:

    • 减少内存占用,减少事件注册
    • 新增子元素不需要再注册事件

    基本数据类型和引用数据类型的区别?

    基本数据类型:string, number, boolean, null, undefined, symbol, bigint,值保存在栈内存中

    引用数据类型:object, function, array,对象的引用(地址)保存在栈内存中,真实的值保存在堆内存中

    原型链

    每一个对象都有一个私有属性__proto__指向另一个名为原型prototype的对象。原型对象也有自己的原型,层层向上直到一个原型为null,null没有原型,作为原型链的终止。

    每个引用对象都有一个__proto__属性,是一个对象

    每个函数都有一个prototype原型属性,是一个对象

    每个对象的__proto__指向它构造函数的原型prototype

    1
    2
    3
    4
    let obj = {}
    obj.__proto__ === Object.prototype // true
    obj.__proto__.constructor === Object // true
    Object.prototype.constructor === Object // true

    每个构造函数都有一个原型属性prototype,该构造函数的实例对象共享原型对象的方法和属性。

    当访问对象的属性或方法时,从自身开始查找,如果查找不到则从__proto__隐式原型中即构造函数的原型对象查找,原型的原型查找,这样一层一层的向上查找形成的链式结构叫原型链。

    一直往上层查找,直到null还没有找到,返回undefined

    Object.prototype.__proto__ === null

    new关键字的操作

    • 创建一个空的对象{}
    • 将新建对象的原型链连接到构造函数的原型对象上(obj._proto = Fn.prototype)
    • 执行构造函数并将构造函数的this绑定到新建对象上(const _this = Fn.apply(obj, args))
    • 如果构造函数返回一个对象则返回这个对象否则返回新创建的对象;
    1
    2
    3
    4
    5
    6
    function _new(Fn, ...args) {
    let obj = {}
    obj.__proto__ = Fn.prototype
    const _this = Fn.apply(obj, args)
    return _this instanceof Object ? _this : obj
    }

    js如何实现继承的?

    • 1、原型链继承
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function Parent() {
    this.name = 'parent'
    this.play = [1, 2, 3]
    }
    Parent.prototype.getName = function() {
    return this.name
    }
    function Child() {
    this.type = 'child'
    }
    Child.prototype = new Parent() // 继承了Parent,通过原型
    var child1 = new Child()
    child1.play.push(4)
    console.log(child1.play) // [1,2,3,4]
    console.log(child1.getName()) // parent
    var child2 = new Child()
    console.log(child2.play) // [1,2,3,4]
    console.log(child2.getName()) // parent

    原型链继承的方式,如果子类实例修改了原型上的属性那么所有子类实例都会被影响。

    • 2、借用构造函数继承(借用父类的构造函数继承父类的属性)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function Parent() {
    this.name = 'parent'
    this.play = [1, 2, 3]
    }
    function Child() {
    Parent.call(this) // 继承Parent的属性
    this.type = 'child'
    }
    var child1 = new Child()
    child1.play.push(4)
    console.log(child1.play) // [1, 2, 3, 4]
    console.log(child1.name) // parent
    var child2 = new Child()
    console.log(child2.play) // [1, 2, 3]

    这种方式解决了原型链继承中子类实例修改原型上的属性影响所有实例的问题。但是父类的方法,每个子类实例都需要重新创建一份

    • 3、组合继承(原型链+借用构造函数)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function Parent() {
    this.name = 'parent'
    this.play = [1, 2, 3]
    }
    Parent.prototype.getName = function() {
    return this.name
    }
    function Child() {
    Parent.call(this) // 继承Parent的属性
    this.type = 'child'
    }
    Child.prototype = new Parent() // 继承Parent的方法
    Child.prototype.constructor = Child // 修正构造函数指向

    var child1 = new Child()
    child1.play.push(4)
    console.log(child1.play) // [1, 2, 3, 4]
    console.log(child1.getName()) // parent
    var child2 = new Child()
    console.log(child2.play) // [1, 2, 3]
    console.log(child2.getName()) // parent

    这种方式既解决了原型链继承中子类实例修改原型属性影响所有实例的问题,又解决了借用构造函数继承中子类实例无法继承父类的方法的问题。

    • 4、Es6 class通过extends实现继承
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Parent {
    constructor() {
    this.name = 'parent'
    this.play = [1, 2, 3]
    }
    getName() {
    return this.name
    }
    }
    class Child extends Parent {
    constructor() {
    super() // 调用父类的构造函数
    this.type = 'child'
    }
    }
    var child1 = new Child()
    child1.play.push(4)
    console.log(child1.getName()) // Parent
    console.log(child1.play) // [1, 2, 3, 4]
    var child2 = new Child()
    console.log(child2.play) // [1, 2, 3]
    console.log(child2.getName()) // Parent

    这种方式的实现原理其实是基于原型链继承和借用构造函数继承的组合,拥有这两种方式的特点。

    js设计原理

    • 解释型语言:不需要编译,运行时解析和和执行代码
    • 动态类型:变量类型在运行时确定
    • 单线程和异步编程:单线程是异步的原因;事件循环是实现异步的一种方式;回调是处理异步操作的关键
    • 垃圾回收机制:为了防止内存泄露,内置垃圾回收机制。定期寻找不再使用的变量,并释放他们占用的内存。

    js中的this指向

    • 在方法中使用,this表示该方法所属的对象
    • 在全局中使用,this指window对象(浏览器环境)
    • 在函数中使用,this指window对象(浏览器环境)
    • 在函数中使用,严格模式下,this为undefined
    • 在事件监听器中使用,this指接收事件的元素
    • 显式绑定:可以使用callapplybind方法来显式地设置this的值
    • 构造函数调用:new Person()…Person作为构造函数,this指向新创建的实例对象
    • 箭头函数中的this:箭头函数没有this,在定义的时候就确定了,如果外层函数有this,就是外层函数的this,否则就是window
    • 匿名函数中的this:this的值取决于它是如何被调用的。
    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
    33
    34
    // 作为普通函数被调用,this指向全局window对象
    function example() {
    var func = function() {
    console.log(this);
    };
    func(); // this 指向全局对象,通常是 window
    }
    example();

    // 作为某个对象的方法被调用,this指向那个对象
    var obj = {
    prop: 'Hello',
    method: function() {
    (function() {
    console.log(this.prop); // this 指向 obj
    })();
    }
    };

    obj.method(); // 输出 "Hello"

    // 当使用call apply bind显示设置this时,指向设置的对象
    var value = 'global';

    var obj = {
    value: 'local'
    };

    var func = function() {
    console.log(this.value);
    };

    // 使用 call 方法调用匿名函数,并将 this 显式地绑定到 obj
    func.call(obj); // 输出 "local"

    匿名函数中的this值取决于它是如何被调用的,而不是它是否是一个匿名函数

    script标签的加载和执行

    同步脚本:浏览器遇到script标签先暂停页面解析,加载完成后执行脚本,继续页面解析渲染;同步脚本会阻塞页面的解析

    把脚本放在head标签里,会等待脚本下载,执行,然后继续后续解析,浏览器在遇到body标签之前不会渲染页面任何部分,造成页面空白且无法进行交互,体验非常不好。推荐将script标签放在body结束标签的前面。

    浏览器允许并行下载js资源,但是script标签的下载仍要阻塞其他资源的下载,如图片,样式表。

    非阻塞脚本:defer

    当解析到带有defer属性的script标签时,会立即下载此js文件,等到页面解析完成后执行js脚本,只针对外部js脚本,严格按照出现顺序执行。脚本加载不阻塞页面解析

    异步脚本:async

    当解析到带有async属性的script标签时,会立即下载此js文件,下载完成后立即执行,执行完成后继续文档解析,只针对外部脚本,因为加载完成后立即执行所以执行脚本顺序不确定。脚本加载不阻塞页面解析

    动态创建script标签:监听window的load事件执行回调(创建script标签并插入body)

    DOMContentLoaded和load事件

    • DOMContentLoaded:当初始的文档被解析和加载完成后就触发该事件,而无需等待其他资源比如样式表、图像、子框架的完全加载
    • load:当文档和所依赖的资源比如样式表、图像都加载完成后触发该事件
    • 区别:DOMContentLoaded是HTML文档被解析完成后触发,load在HTML文档及其相关资源都加载完后触发

    setTimeout和setInterval最小执行时间?

    根据HTML5标准,setTimeout的最小执行时间是4毫秒,而setInterval的最小执行时间是10毫秒。这意味着,如果你尝试设置一个少于这些时间间隔的延迟,浏览器会自动调整它以达到这个最小时间间隔

    这些间隔受操作系统和浏览器等其他因素的影响。

    ES6和ES5的区别?

    js:ECMAscript + BOM + DOM

    ES5: ECMAscript5,ECMAscript的第五次修订,2009

    ES6: ECMAscript6,ECMAscript的第六次修订,2015

    ES6新增哪些特性?

    • 1、新增let 和const关键字提供块级作用域
    • 2、新增类class关键字实现继承
    • 3、新增模块化:import和export实现模块化
    • 4、新增箭头函数
    • 5、新增函数参数默认值和剩余参数
    • 6、新增对象和数组的解构赋值
    • 7、新增对象和数组的扩展运算符
    • 8、新增模版字符串``
    • 9、新增Promise异步编程方案,使得异步可以像同步操作一样进行链式调用解决回调地狱问题
    • 10、新增Symbol原始数据类型,代表独一无二的值
    • 11、新增字符和数组的方法:如Array.prototype.includesString.prototype.startsWithString.prototype.endsWith等。
    • 12、新增新的集合类型:如map, set, WeakMap, WeakSet
    • 13、迭代器和生成器:引入interato接口和for…of循环,以及Generator函数

    ES7新增哪些特性?

    • 1、新增Array.prototype.includes()方法,这是一个用于数组的方法,允许开发者检查数组中是否存在某个指定的元素。它返回一个布尔值,如果数组中包含给定的元素,则返回true,否则返回false。与indexOf()方法相比,能检测NaN
    • 2、新增**乘方运算符,以前借助于Math.pow方法

    箭头函数

    箭头函数提供了一种更简洁的函数书写方式,是匿名函数。当需要维护this上下文的时候使用箭头函数。

    箭头函数没有this绑定,使用外层作用域的this绑定即外层函数的this对象。

    箭头函数没有this、super、arguments和new.target绑定,不可以作为构造函数。

    不可以使用call\apply\bind改变this的指向。

    call apply bind三者区别?

    都是用来改变this指向和函数的调用,第一个参数都是要调用的函数对象即函数体内的this值。

    1
    2
    3
    4
    Fun.call(obj, arg1, arg2)
    Fun.apply(obj, [arg1, arg2])
    Fun.bind(obj)(arg1, arg2) // bind不能直接调用,返回一个函数后再调用
    Fun.bind(obj, arg1, arg2)()

    call参数传递参数列表,apply传参是数组,bind返回一个函数再调用,传参和call方法一样

    递归遇到的问题?

    递归的本质是函数调用自身来解决问题。

    递归每次调用都会在函数调用栈新增一个层级,函数调用栈是有大小限制的,调用栈层级过深,可能会超出栈的大小限制导致栈溢出错误。如果不设置递归终止条件会导致无限递归陷入死循环消耗资源。

    ajax是什么?

    ajax是基于XHR的一种技术,用来无需刷新页面异步获取数据并更新页面

    xhr是用来发送http请求和接收服务器响应的api,支持异步

    1、创建xhr对象,let xhr = new XmlHttpRequest()

    2、调用xhr对象的open方法与服务器建立连接

    3、调用xhr对象的send方法发送请求给服务器

    4、监听对象的onreadystate change事件监听服务器与客户端的通信状态

    5、接收并处理服务器响应的数据

    6、将数据更新到页面上

    get和post

    get和post是http协议中两种请求方式。在功能和使用上有些区别:

    1、URL可见性:GET请求的参数通过URL传递,在地址栏可见;POST请求的参数在http请求体中,不可见

    2、传输数据的大小限制:GET请求的数据大小是有限制的,取决于URL的最大长度,这个长度由浏览器和服务器决定,通常是2048个字符;POST请求对数据大小没有限制。

    3、安全性:由于GET请求参数在URL中,会被浏览器缓存、记录在历史记录中,或者被服务器、代理服务器、或用户代理记录到日志文件,因此安全性低;POST请求参数不可见,因此安全性高

    4、编码方式:GET请求只支持URL编码,POST请求支持多种编码方式

    5、缓存:GET请求是可以被缓存的,如果相同的请求被发送,浏览器可能从缓存中读取数据而不像服务器发送请求。POST请求不会被缓存

    6、历史记录:GET请求可以被缓存到浏览器历史记录,而post请求不会。

    7、用途:GET请求通常用来查找或获取数据,而post请求用来提交数据,修改或建立新的资源。

    Promise的内部原理是什么?

    Promise的内部实现原理主要依赖于状态机和事件循环机制。

    Promise有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。初始状态为pending,并且状态只能从pending转变为fulfilled或rejected,一旦转变就不能再改变。Promise的构造函数接受一个执行器函数作为参数,这个执行器函数内部包含异步操作。执行器函数有两个参数,分别是resolve和reject。当异步操作成功时,调用resolve函数将Promise状态转变为fulfilled;当异步操作失败时,调用reject函数将Promise状态转变为rejected。

    在Promise内部使用了一个叫做thenable的对象用来管理回调函数。当Promise状态转变为fulfilled或rejected时,会依次执行thenable对象中的回调函数。thenable对象具有一个叫做value的属性,用来存储异步操作的结果。

    Promise还有一个核心方法叫做then,用于注册回调函数。then方法接受两个参数,分别是成功回调函数和失败回调函数。当Promise状态已经是fulfilled时,会直接调用成功回调函数,并将value作为参数传入;当Promise状态已经是rejected时,会直接调用失败回调函数,并将value作为参数传入。如果then方法在Promise状态还是pending时被调用,则会将成功回调函数和失败回调函数分别放入thenable对象的回调数组中,待Promise状态转变时再执行。

    在使用Promise时,可以通过链式调用多个then方法,这样可以方便地处理多个异步操作,并保持代码的可读性。在链式调用中,前一个then方法返回的是一个新的Promise对象,后一个then方法中的回调函数会作为这个新Promise对象的回调函数注册。

    总的来说,Promise内部原理主要是通过状态机来管理异步操作的状态,并使用thenable对象和then方法来处理异步操作的结果。这种机制使得Promise能够方便地处理异步操作,提高代码的可读性和可维护性。

    缺点:

    1. 无法取消Promise:一旦Promise对象被创建并已经开始执行,就无法中途取消。这可能会在一些需要取消异步操作的情况下造成问题。
    2. 错误处理需要显式调用:如果Promise对象内部抛出了错误,但没有设置.catch方法或reject的回调函数,那么这个错误不会被捕获,也不会抛出到外部,这可能会导致一些问题。
    3. 无法获知进展:Promise对象只有三种状态:pending、fulfilled、rejected,无法获知异步操作的进展信息(比如完成了多少百分比)。如果需要获知进展,可能需要使用其他方式(比如使用Progress对象)。

    Promise和async/await的区别?

    Promise和async/await都是JavaScript中用于处理异步操作的解决方案,它们有一些相似之处,但也有一些明显的区别。

    1. 语法差异:Promise使用then()和catch()方法来处理异步操作的结果和错误,而async/await使用更直观的语法,通过async关键字定义异步函数,并在函数内部使用await关键字等待异步操作完成。这使得异步代码看起来更像同步代码,提高了代码的可读性和可维护性。
    2. 错误处理:Promise通过链式调用的catch()方法来捕获错误,而async/await可以使用try-catch语句来捕获错误。这使得错误处理更加直观和方便。
    3. 应用场景:Promise更适用于一连串的异步请求,而async/await更适合串行的异步请求之间的依赖关系比较复杂或需要控制执行顺序的情况。async/await更适合业务逻辑处理较为复杂的场景。
    4. 错误堆栈追踪:使用Promise时,如果发生错误,堆栈追踪会包含Promise内部的异步操作,这可能会使得定位错误变得更加困难。而使用async/await时,错误堆栈追踪会显示在出错的地方,这更方便调试和定位错误。

    ng在一些简单的异步操作中,Promise可能更加简洁和高效;而在需要处理复杂异步逻辑和错误处理的场景中,async/await可能更加适合。

    浏览器常用存储方式有哪些?

    • cookie:由服务器发送并存储在浏览器,并且在同一服务器请求下携带cookies。大小有限通常为4KB,可以通过Expires设置过期时间。通常用来存储用户登录信息、个性化设置等。
    • localStorage:本地持久化存储,以key-value形式存储字符串数据,大小为5M或更大。除非主动删除否则永久化存储。
    • sessionStorage:用于本地存储,只保存在当前浏览器会话中,关闭浏览器窗口或标签就会被删除。适用于临时数据存储。
    • IndexedDB: 基于事务的数据库存储,可以存储大量结构化数据。
    • WebSQL:使用SOL数据库在浏览器中存储数据(兼容性和性能不好已被indexedDB取代)

    浏览器中cookie和sessionStorage/localStorage的区别?

    • 存储大小:cookie存储大小通常为4KB,sessionStorage/localStorage可以达到5M或更大。
    • 存储位置:cookie不设置expires是会话级的存储在浏览器内存中,sessionStorage会话级存储在浏览器内存中,localStorage存储在浏览器中
    • 有效时间:cookie在设置有效期内都可用,即使关闭浏览器;localStorage除非主动删除永久可用;sessionStorage在当前浏览器会话中可用,浏览器窗口或标签关闭就被删除
    • 数据共享:三者都遵循同源共享数据。cookie和localStorage在同源浏览器窗口数据都共享即使不是同一个页面;sessionStorage只在当前会话当前标签/窗口共享数据。
    • 通信方式:cookie参与服务器通信,sessionStorage/localStorage不会被发送到服务器
    • 生成方式:cookie由服务器生成发送到浏览器,sessionStorage/localStorage由前端创建

    token存储在哪里?

    token是验证身份的令牌,用户登录成功后服务器根据凭证加密后得倒的字符串。

    • localstorage:持久化存储数据,每次调用接口发送请求将token当作字段传给后台。将Token存储在Local Storage中可能会面临XSS攻击的风险,特别是当项目中引入了很多第三方JS类库时。
    • sessionStorage:与Local Storage类似,但数据只在当前浏览器会话中有效
    • cookies:由服务器发送给浏览器,每次浏览器向同一服务器发送请求都会携带cookies,将token存储在cookies可用自动发送但是不能跨域。容易收到csrf攻击。

    DOM树和渲染树有什么区别?

    DOM树和HTML标签一一对应的,包括head标签和隐藏元素

    DOM树和CSSOM树生成渲染树,渲染树和DOM树不一一对应,只包含可见元素,比如display:none元素包含在DOM树但是不在渲染树,比如伪元素不在DOM树但是在渲染树。

    精灵图和base64区别?

    精灵图:将多张小图整合到一张大图上,通过定位将小图展示到页面上,多次访问可以减少请求,提高加载速度

    Base64图:将图像数据编码为base64字符串的形式。

    base64优点:

    • 减少http请求次数:图像直接嵌入网页不再发送图片请求,减少http请求次数提高加载速度
    • 避免跨域问题:图片跟随网页下载,避免了跨域

    缺点:

    • 增加css文件大小:如果使用大量base64图片会增加css文件大小,影响加载速度
    • 增加css解析时间:base64编码较长,增加css解析时间,影响网页性能
    • 浏览器兼容性:老版本浏览器不支持base64格式的图片

    SVG

    用于创建和描述基于路径的图形、文本、图像以及其他图形元素
    svg是基于XML语法的图像格式,全称可缩放矢量图。不同于png等基于像素的位图,svg基于矢量数据描述图像内容。

    可无损放大和缩小图片,保持图片的清晰度;svg基于文本可以被编辑和修改;兼容性好,可以同页面元素一样与js和css进行交互。

    包含元素:

    • 路径元素(path):用于创建基于路径的图形,包括,直线、曲线,圆弧等。
    • use元素:用于重复使用一个已定义好的图形元素。
    • symbol元素:用于定义可重用的图形片段,本身并不显示在页面上
    • 矩形元素(rect):用于创建矩形
    • 圆形元素(circle):用于创建一个圆
    • 椭圆元素(ellipse):用于创建椭圆
    • 文本元素
    • 图像元素
    • 渐变元素

    npm的底层环境是什么?

    Node Package Manager,node的包管理和分发工具,已经称为分发node模块的标准,是js 的运行环境。

    npm允许开发者从注册表(一个公共的存储库)中安装、更新和管理nodejs所需的库和工具,这些库和工具称为包

    npm组成:网站、注册表、命令行工具

    http协议的协议头和请求头有什么?

    协议头包含请求头和响应头

    请求头:

    • Accept:客户端告诉服务器支持的数据类型
    • Accept-encoding:客户端告诉服务器支持的压缩格式如gzip
    • Accept-Language:客户端告诉服务器所采用的语言
    • Host:客户端告诉服务器想要访问的主机名。
    • Referer:客户端告诉服务器从那个网页跳转过来
    • If-Modified-Since:客户端告诉服务器缓存最后修改时间
    • cookie:客户端向服务器发送cookie信息
    • user-agent:客户端告诉服务器其所使用的浏览器类型、版本等信息。
    • Connection:是否需要持久连接keep-alive

    响应头:

    • Content-type:告诉客户端响应体的媒体类型
    • Content-length:响应体的长度
    • Cache-control:控制网页缓存
    • Date:消息被发送的时间和日期
    • Server:服务器的名称、版本等信息
    • Set-Cookie:向客户端发送cookie
    • Location:重定向资源的位置

    浏览器缓存策略

    强缓存:本地读取缓存,从内存(会话级)或硬盘读取 Expires cache-control

    协商缓存:强缓存命中失败,向服务器发送请求,协商缓存,if-modified-since/last-modified. If-none-match/etag

    跨域

    浏览器的同源策略:它限制一个源的文档或加载的脚本如何能与另一个源的资源进行交互。它能隔离恶意文档,减少被攻击的媒介。

    如果两个url的协议/域名/端口都相同的话,则这两个url是同源。

    script、link、iframe、img、video、object标签引入的资源可以跨域

    跨域解决方案:

    • JSONP:动态创建script标签并插入head中,浏览器会执行script代码,只能发送get请求
    • CORS:基于http1.1的跨域解决方案,全称跨域资源共享。简单来说就是浏览器要想跨域访问服务器资源必须经过服务器的允许。

    针对不同请求,cors有三种交互模式:简单请求、带预检的请求、携带cookie的请求

    简单请求:请求方式为get、post、head;请求头仅包含安全的字段Accept、Accept-lanuage、content-type等;有content-type字段且值只能为text/plain application/x-www-form-urlencoded multipart/form-data;

    简单请求请求头携带Origin表明发送跨域请求的源地址,服务器收到请求如果允许跨域访问则在响应头添加Access-Control-Allow-Origin字段

    带预检的请求:非简单请求,先发送预检请求访问服务器是否允许,服务器接收请求允许访问,浏览器发送真实的请求,服务器完成真实的响应

    携带cookie的请求:设置withCredentials: true,当一个请求附带cookie时,无论简单还是预检请求都会在请求头添加cookie字段,而服务器需要在响应头明确表示是否允许Access-Control-Allow-Credential:true

    • 代理服务器:客户端向代理服务器发送请求,代理服务器再向目标服务器发送请求,然后将结果返回给客户端
    • WebSocket:在单tcp连接进行双工通信的协议,允许跨域通信
    • window.postMessage:h5的postMessage方法允许跨窗口/跨域通信
    • 第三方库:jQuery、Axios等也提供了跨域请求的支持

    防抖和节流

    防抖:连续触发事件在一段时间内只执行一次,如果时间内再次触发,则重新计时.

    使用场景:用户重复点击按钮,搜索框实时搜索,窗口大小调整

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function debounce(func, wait) {
    let timer
    return function() {
    let context = this
    let args = arguments
    if (timer) timer = null
    timer = setTimeout(() => {
    func.apply(context, args)
    }, wait)
    }
    }

    节流:连续触发函数在一段时间内只执行一次,稀释了函数的执行频率。

    使用场景:鼠标icon定位,页面scorll,窗口调整,抢购和疯狂点击

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function throttle(func, wait) {
    let timer
    return function() {
    let context = this
    let args = arguments
    if (!timer) {
    timer = setTimeout(() => {
    func.apply(context, args)
    timer = null
    }, wait)
    }
    }
    }

    防抖和节流都用于降低函数的执行频率。不同的是防抖在一段时间后操作,在这段时间内再次触发任务,则重新计时;而节流是一段时间内只执行一次任务,若这段时间内再次触发也不会生效。

    JSON

    一种轻量级的数据交换格式。是ecmascript的一个子集,使用文本格式来存储和表示数据。简洁、容易阅读。

    SON的基本语法规则包括:

    • 数据为键值对(key/value pairs)。
    • 数据由逗号分隔。
    • 花括号保存对象。
    • 方括号保存数组。

    JSON的两种基本结构是对象和数组。对象是一个无序的键值对集合,其中每个键值对使用一个冒号分隔,多个键值对之间使用逗号分隔。数组则是一个有序的值列表,其中每个值之间使用逗号分隔。

    当数据没有请求过来时该怎么做

    可以考虑采用一些备选方案,如使用缓存数据、提供占位符或加载默认值等,以保证用户界面的友好性和可用性

    无感登录

    token+refreshToken模式:用户在认证中心登录成功后返回给客户端token+refreshToken,其中token有效期短,refreshToken长期有效,客户端访问业务系统携带token,由业务系统验证通过后返回响应数据,若token过期则返回客户端对应状态码,客户端在响应拦截中拿refreshToken换取新的token保存并替换在本地,用新的token去重新发送业务请求。如果refreshToken过期了,则清除所有token重新进行登录认证。

    方式:1.在响应中拦截,判断过期后调用刷新token接口

    2、设置定时器刷新token接口 3、后端返回过期时间,前端判断token过期调用刷新token接口

    前端工程化

    组织管理:发挥资源的最大效能

    前端工程化:前端开发的管理工具、降低开发成本,提升开发效率

    项目规模越大,团队成员越多,项目越复杂,管理成本就越高,工程化价格越大。

    前端工程化,选择什么样的工具,如何把这些工具组织起来,形成我们的项目结构,让初中级开发者能够在这套系统里完美的进行协作,高效率的进行开发,是非常有价值的一件事。

    一些官方的脚手架工具比如cli, vite是众多方案里现成的解决方案,不一定适合所有项目,一些复杂的项目需要自行开发脚手架。

    开发者和架构师最大的区别是工程化能力的区别。

    模块化和包管理

    问题、标准、实现

    随着项目的复杂,用分解和聚合的思想来管理项目

    模块化:分解和聚合,解决文件的分解(全局污染)和聚合(依赖混乱)

    模块化:

    • 标准: CommonJS(CJS,社区标准-nodejs) AMD CMD UMD Ecmascript Module(ESM,官方标准),常用commonjs、ESM

      • cjs是运行时,运行后才能确定依赖关系const xxx = require('./1.js');ESM是编译时,运行之前就判断依赖关系import xxx from './1.js',ESM不需要运行就能确定依赖关系,方便做优化。

      • 1. CommonJS

        • 设计初衷:CommonJS 主要应用于服务器端,由 Node.js 实现并流行起来,适用于非浏览器环境。
        • 工作原理:CommonJS 是同步的模块加载机制,模块在运行时通过require函数导入,通过module.exportsexports导出。每个模块在执行时有自己的全局作用域,模块加载后立即执行,输出的是导出对象的一个副本。
        • 使用场景:常用于Node.js的服务器端编程,如在express框架中加载中间件和路由模块。
      • 2. Asynchronous Module Definition (AMD):

        • 设计初衷:AMD 是面向浏览器环境设计的,由 RequireJS 首先提出并广泛应用,解决浏览器环境下异步加载模块的需求。
        • 工作原理:AMD 使用异步方式加载模块,模块的加载和执行是分离的。通过define函数定义模块,通过require函数异步加载模块,并在回调函数中使用加载完成的模块。AMD 支持模块的动态加载和依赖管理。
        • 使用场景:适用于浏览器端的复杂单页面应用,特别是需要异步加载和模块按需加载的场景。
      • 3. ECMAScript Modules (ES6 Modules):

        • 设计初衷:ES6 Modules 是 ECMAScript 6 标准中正式引入的模块系统,它既适用于浏览器环境也适用于非浏览器环境。
        • 工作原理:ESM 使用import关键字导入模块,通过export关键字导出模块成员。模块的加载是静态的(静态分析时就可以确定模块依赖),并且默认支持异步加载(尽管现代浏览器也支持同步模块加载)。模块在导入时不会立即执行,而是等到导入它的模块需要时才执行。
        • 使用场景:现代浏览器和Node.js(v14+ 版本后支持原生ESM)中广泛使用。ESM已经成为主流的模块化标准,并逐渐取代CommonJS和AMD成为统一的模块化解决方案。
      • 总结来说,CommonJS 适合于服务器端的同步环境,AMD 主要解决浏览器环境下的异步模块加载问题,而 ES6 Modules 则是新一代标准化的模块系统,旨在为浏览器和服务器两端提供一种统一的、强大的模块化解决方案。

    • 实现:浏览器、node、构建工具

      • 浏览器支持ESM,只支持官方,<script src="./1.js" type="module" >
      • node支持CJS和ESM
      • 构建工具:一般都支持CJS和ESM,vue-cli vite car umijs 脚手架内部内置构建工具:webpack、rollup、esbuilder等,推荐使用ESM

    包管理:

    • 包:package,一系列模块的集合, 函数->文件->包
    • npm:包管理工具,包含:包的属性、registry(包含包的仓库)、cli(command-line interface命令行工具界面),pnpm, yarn, cnpm, bower为了弥补npm的不足,bower支持浏览器环境,其他npm等都是在node环境,常用npm, pnpm, yarn

    框架:约束代码结构,比如vue单文件组件

    库:原本就有自己的结构,调用库的功能

    工程化是前端开发工具,降低开发成功,提高开发效率

    JS工具链

    开发项目除了业务代码外还有别的问题需要解决:

    • 语言问题:html、css(sass/less/stylus等)、js的兼容性和语言增强,兼容性:api兼容(babel、polypill)、语法增强

      • js语言的处理

        • js中常用2个工具Babel,Core-js,Babel负责将最新的ES语法转换为浏览器兼容的语法(比如let, const);而Core-js提供了必要的polyfill,使开发者使用最新的js特性,而不必担心兼容性问题(比如promise, async/await);
        • Babel: 是一个JavaScript编译器,将最新版本es代码转换为向后兼容的js版本,以便在旧版本浏览器运行。这一功能通过各种插件实现,每一种插件负责转换一种语法,为了简化插件的使用,babe提供一组预设presets,presets是插件的集合,可以预先配置好常用的插件方便开发者使用。babel将代码转换为AST抽象语法树,又转换为代码。babel的插件影响抽象语法树从而最终影响转换结果。
        • Core-js:是一个包含javascript标准库polyfill的集合。Polyfill是填充浏览器对ES6+新语法支持不足的工具,为浏览器添加缺失的特性,使开发者可以使用最新的js特性,而不用担心兼容性问题。
      • css语言的处理

        • css语言的问题:语法缺失(循环、判断、拼接)、功能缺失(颜色函数、数学函数、自定义函数)

        • 新语言:在css基础上扩展,sass/less/stylus包含css,新语言->css预编译器->css语言, css语言还需要处理别的问题:厂商前缀(autoprefixer)、代码压缩(cssnano)、代码剪枝(purgecss)等,处理这些的问题的工具称为后处理器,处理完成后生成css代码;

        • Sass/less/stylus–>预编译器–>css–>后处理器–>css

        • PostCSS:是一个使用javascript工具转换css的库,不是一个预处理器,而是用来转换css代码的。主要功能包括:

          • 解析css代码转换为抽象语法树AST

          • 提供API以修改AST

          • 将修改后的AST格式化为新的CSS代码

            postcss本身并不提供任务功能而是依赖于插件来进行css转换,对基于css的AST进行各种操作,包括支持变量和混入、添加浏览器前缀等。postcss使用多种场景包括在css预处理阶段和后处理阶段进行代码转换,也可以结合sass/less/stylus工具使用,以扩展css的功能和兼容性。Tailwind是postcss的一个插件,是个样式库,封装的是css;使用Tailwind时,只需要给组件添加class即可;特点是可定制化程度高和响应式设计。

    • 工程问题

    • 流程问题

    开发和维护的代码于运行时需要的代码不一致,需要进行工程转换,就是构建工具,用来进行工程转换。

    webpack是一种模块打包工具,用于前端资源构建,可以处理js、css、图片等各种文件并将其打包成浏览器可以识别的静态资源。

    html语义化的理解

    • 使内容更加结构化,即使失去样式也能呈现清晰的结构
    • 有利于爬虫和seo
    • 语义化更有可读性,方便团队开发
    • 方便其他设备解析,比如盲人阅读器渲染页面更易于阅读

    html5的新特性

    • 语义化的标签
    • 新增video、audio
    • 画布canvas
    • svg
    • drag & drog 拖拽释放API
    • input类型:date日期、time时间、email、url网址、search搜索框、number、color颜色选择器、month月、week周
    • localStorage/sessionStorage

    css3新特性

    • 边框
    • 背景属性
    • 文本阴影
    • transform
    • transition过渡动画
    • animation动画
    • 渐变
    • 媒体查询
    • 弹性盒
    • 新增选择器:属性、伪类、伪元素选择器

    解决了哪些移动端的兼容问题?

    1、在移动端(特别是 iOS 设备)上,当使用 overflow: scrolloverflow: auto 样式时,确实可能会遇到滑动卡顿的问题。

    原因:IOS safari试图减少绘制和合成操作来进行滚动性能优化。

    解决:使用-webkit-overflow-scrolling: touch启用IOS上滚动性能优化。这个属性告诉浏览器使用原生滚动机制,通常可以提供更流畅的滚动体验。

    1
    2
    3
    4
    .scroll-element {
    overflow: scroll;
    -webkit-overflow-scrolling: touch;
    }

    2、在移动端安卓设备上,placeholder 文字在设置行高 (line-height) 后偏上

    解决:避免给有placeholder属性的input/textarea设置行高(line-height: normal)

    3、移动端字体小于12px时异常显示

    解决:先将元素整体尺寸放大一倍,再用transform:scale(0.5)进行缩小,设置基点transform-origin: 0 0;

    4、ios系统在input框禁用时,disabled:true文本内容不显示

    原因:ios系统在input:disabled时透明度为0.3导致视觉上文本不显示

    解决:

    1
    2
    3
    input:disabled, input[type="button"]:disabled {
    opacity: 1;
    }

    5、IOS端input输入框首字母会大写

    原因:在 iOS 设备上,虚拟键盘默认会开启首字母大写功能

    解决:autocapitalize="off" 告诉键盘不要自动将输入的首字母大写,而 autocorrect="off" 则禁用了自动更正功能

    1
    <input type="text" autocapitalize="off" autocorrect="off"

    6、禁用移动端选中文字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /* 禁止用户选择文本 */  
    * {
    -webkit-touch-callout: none; /* iOS Safari */
    -webkit-user-select: none; /* Safari */
    -khtml-user-select: none; /* Konqueror HTML */
    -moz-user-select: none; /* Firefox */
    -ms-user-select: none; /* Internet Explorer/Edge */
    user-select: none; /* 非前缀版本,目前被大多数浏览器支持 */
    }

    /* 如果需要允许 input 元素内的文本选择,可以单独设置 */
    input {
    -webkit-user-select: auto; /* 允许 input 内的文本选择 */
    }
    ​```

    7、ios识别长串数字为电话

    解决:<meta name="format-dection" content="telphone=no"

    v-for中key的作用

    1、作为元素的唯一标识,避免v-for的“就地更新”策略导致的问题。

    2、更高效的更新虚拟DOM,提高渲染效率

    3、避免带有输入框的DOM产生错误更新造成数据混乱

    created和mounted请求数据有什么区别

    • created:在组件实例被创建后立即调用,此时组件的DOM尚未被挂载。
    • mounted:在组件的DOM挂载完成后调用,此时组件已经可以访问到DOM元素。

    通常,created用于在DOM挂载之前发起数据请求并进行预处理,而mounted则用于在DOM挂载完成后执行依赖于DOM的操作

    keep-alive

    keep-alive 是 Vue.js 中提供的一个内置组件,用于缓存不活动的组件实例,而不是销毁它们。当组件在 <keep-alive> 内被切换时,它的激活和停用状态会被相应触发。

    包裹在动态组件中使用:

    1
    2
    3
    <keep-alive>  
    <component :is="currentComponent"></component>
    </keep-alive>

    特性

    • include(字符串或正则表达式):只有名称匹配的组件会被缓存。
    • exclude(字符串或正则表达式):任何名称匹配的组件都不会被缓存。

    生命周期钩子

    当组件被 <keep-alive> 包裹时,会触发两个额外的生命周期钩子:

    • activated:当组件从缓存中激活时调用。
    • deactivated:当组件被停用(但不是销毁)时调用。

    element-ui怎么进行表单验证

    Form组件设置model属性进行数据绑定,Form-item组件设置prop属性验证字段名,Form 组件上设置 :rules 属性绑定验证规则对象

    表单校验方式:

    • 基础校验:在 el-form-item 中通过 rules 属性进行校验规则的设置或者在form组件添加rules属性绑定表单全部属性检验规则对象
    • 自定义校验:通过 validator 函数来自定义校验规则。validator 是一个函数,它接收三个参数:rule(校验规则)、value(字段值)和 callback(回调函数)。

    封装axios

    封装axios可以简化代码、同一错误处理、请求拦截和响应拦截等。封装步骤:

    • 1、安装下载axios
    1
    npm install axios
    • 2、创建请求文件:在项目的src目录下,创建一个utils文件夹,并在该文件夹下创建一个request.js文件。这个文件将用于封装axios。
    • 3、在request.js中引入依赖,引入axios和其他你可能需要的依赖,如qs(用于序列化post请求的数据)。
    1
    2
    import axios from 'axios';  
    import qs from 'qs';
    • 4、配置axios

    在axios配置默认设置、如baseURL、timeout、请求拦截、响应拦截等

    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
    33
    34
    const service = axios.create({
    baseURL: process.env.VUE_APP_BASE_API, // api的base_url
    timeout: 5000 // 请求超时时间
    })
    // 请求拦截
    service.interceptors.request.use(
    config => {
    // 在发送请求之前做些什么
    // 例如,添加token到请求头
    if (store.getters.token) {
    config.headers['Authorization'] = 'Bearer ' + store.getters.token;
    }
    return config;
    },
    error => {
    // 对请求错误做些什么
    console.log(error); // for debug
    return Promise.reject(error);
    }
    )
    // 响应拦截
    service.interceptors.response.use(
    response => {
    // 对响应数据做点什么
    return response.data;
    },
    error => {
    // 对响应错误做点什么
    console.log('err' + error); // for debug
    return Promise.reject(error);
    }
    )

    export default service
    • 5、创建API管理文件

    创建API文件夹,创建管理对应功能模块的api文件,比如用户模块,user.js

    • 6、在API文件中引入axios
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // user.js
    import request from '@/utils/request.js'

    export function getUserList(params) {
    return request({
    url: '/user/list',
    method: 'get',
    params
    });
    }

    export function createUser(data) {
    return request({
    url: '/user/create',
    method: 'post',
    data: qs.stringify(data)
    });
    }
    • 7、在组件中使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import { getUserList, createUser } from '@/api/user';  

    export default {
    data() {
    return {
    userList: []
    };
    },
    created() {
    getUserList().then(response => {
    this.userList = response;
    });
    },
    methods: {
    createNewUser(newData) {
    createUser(newData).then(response => {
    // 处理响应
    });
    }
    }
    };

    vue路由传参方式?

    • 查询参数query传参:

    http://localhost:8080/user?id=123参数显示在url中,通过$route.query.id获取参数

    • 路由参数:
    1
    2
    3
    4
    5
    {  
    path: '/user/:id',
    component: User
    }
    http://localhost:8080/user/123

    通过$route.params.id访问这个参数

    • 编程式导航传参
    1
    2
    3
    4
    5
    // params
    this.$router.push({ name: 'User', params: { userId: 123 }});
    this.$router.push({ path: `/user/${userId}` });
    // query
    this.$router.push({ path: '/user', query: {userId: 123} });

    查询参数(query parameters)与路由参数(route parameters)不同。路由参数是定义在路由配置中的,并且作为 URL 的一部分出现,但它们不会显示在 URL 中(除非你特别配置它们这样做)

    • 路由组件传参

      • 布尔模式:props属性设置为true,route.params 将会被设置为组件属性

        1
        2
        3
        4
        5
        6
        7
        const User = {
        props: ['id'],
        template: '<div>User {{ id }}</div>'
        }
        routes: [
        { path: '/user/:id', component: User, props: true }
        ]
      • 对象模式:如果 props 是一个对象,它会被按原样设置为组件属性。当 props 是静态的时候有用。

        1
        2
        3
        4
        5
        6
        7
        routes: [
        {
        path: '/user',
        component: User,
        props: { id: 123 }
        }
        ]
      • 函数模式:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        routes: [
        {
        path: '/user',
        component: User,
        props: route => {
        return {
        query: route.query.q,
        name: route.params.name
        }
        }
        }
        ]
      • 全局前置守卫传参: 可以在beforeEach中访问to和from对象处理和修改传入的参数

    vue的路由hash模式和history模式的区别?

    vue的路由hash模式和history模式主要区别体现在URL表现形式、页面刷新行为、后端配置需求等方面。

    • URL表现形式:hash模式下,页面地址栏地址中URL中会带有“#”以及后面的字符,history模式下URL表现正常。
    • 页面刷新行为:hash模式下,地址栏回车页面路由正常导航显示,而history模式下,页面刷新可能会404.这是因为history模式的URL资源在服务器中不存在,需要后端进行相应的配置。
    • 后端配置需求:history模式的URL需要后端进行重写或重定向以便访问不存在的URL资源能正确的返回index.html文件
    • 兼容性:hash模式支持低版本浏览器,history不支持,因为是html5的api

    history.pushState(data, title, url), history.replace(data, title, url)新增/替换当前历史记录,不会触发window.onpopstate = function(){}监听历史栈的改变的事件,history.forward()前进,history.back()后退,history.go()会触发历史展改变的事件;

    window对象提供了onhashchange事件来监听hash值的改变,一旦url中的hash值发生改变,便会触发该事件。

    路由拦截是怎么实现的?

    一般路由分为静态路由和权限路由,静态路由为登录页面,错误处理404页面等不需要鉴权的路由,在白名单内;权限路由为需要鉴定权限即校验是否登录等校验的路由。一般在全局导航守卫进行拦截:

    全局路由拦截:

    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
    import Vue from 'vue'
    import Router from 'router'

    Vue.use(Router)
    const router = new Router({
    // ...路由配置
    })
    // 校验是否登录/认证
    const isValidateLogin = (){}
    const whiteList = ['login', '404']
    // 全局前置守卫
    router.beforeEach((to, from, next) => {
    // 不在白名单内,需要校验
    if (!whiteList.includes(to.name)) {
    if (isValidateLogin()) {
    next()
    } else {
    next({
    name: 'login'
    })
    }
    } else {
    next()
    }
    })

    路由独享的拦截:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const router = new Router({  
    routes: [
    {
    path: '/user/:id',
    name: 'user',
    component: User,
    beforeEnter: (to, from, next) => {
    // 这里的逻辑和全局守卫类似
    const isValidateLogin = (){}

    if (!isValidateLogin) {
    next(false); // 阻止导航
    } else {
    next(); // 确保一定要调用 next()
    }
    }
    }
    // ...其他路由配置
    ]
    });

    组件内的拦截:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    data() {
    return {
    ...
    }
    },
    beforeRouteEnter(to, from, next) {
    // 在路由进入之前被调用
    // 不能使用 this,因为此时组件实例还没被创建
    next(vm => {
    // 通过 `vm` 访问组件实例
    });
    },

    vue的动态路由

    vue的动态路由是根据应用程序的状态或用户的权限动态的添加、删除、修改路由。是由router.addRoutes方法实现的,这个方法运行你在运行时向路由表添加更多的路由。

    首先,安装并引入Vue Router

    1
    npm install vue-router
    1
    2
    3
    import Vue from 'vue'
    import Router from 'router'
    Vue.use(Router)

    创建一个基本的路由配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const baseRoutes = [  
    { path: '/login', component: Login },
    { path: '/404', component: NotFound }
    ]

    const router = new Router({
    routes: baseRoutes
    })
    export default router

    创建权限路由:

    1
    2
    3
    4
    5
    const authRouter = [
    { path: '/admin', component: AdminPanel, meta: { auth: 'AdminPanel'} },
    { path: '/admin/dashboard', component: AdminDashboard, meta: { auth: 'AdminDashboard' } },
    ]
    export default authRouter

    使用addRoutes方法添加动态路由:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import authRouter from './authRouter'
    // 根据用户权限获取权限路由
    function getAuthRoutes() {
    return authRouter.filter(item => store.state.auth[item.meta.auth])
    }
    // 根据用户权限动态添加路由
    function addDynamicRoutes() {
    const authRoutes = getAuthRoutes()
    // 确保总是有一个通配符路由指向404页面
    router.addRoutes([
    ...authRoutes
    { path: '*', redirect: '/404' }
    ]);
    }

    // 全局路由守卫拦截判断是否获取获取用户权限,拿到权限后调用addDynamicRoutes()方法添加动态路由

    注意:

    在Vue.js中,当页面刷新时,Vue Router会重新加载路由配置,并且只会加载在初始化时定义的路由(即在创建router实例时通过routes选项传入的路由)。动态添加的路由在页面刷新后会丢失,因为它们是在运行时通过addRoutes方法添加到路由表中的。

    如果你希望在页面刷新后仍然保留动态添加的路由,你需要将这些路由持久化到某种存储机制中,并在应用启动时从该存储中恢复它们。

    或者全局路由守卫拦截判断是否获取用户权限,拿到权限后动态添加权限路由;用户权限数据存储在store里不做持久化存储,每次刷新重新拿取用户权限数据进而实现重新添加动态路由。

    全局导航守卫:
    判断是否登录:

    • 未登录:在白名单内放行,不在重定向登录页面
    • 已登录:判断是否加载了动态路由
      • 已加载,放行
      • 未加载,判断是否拿到权限数据:
        • 拿到权限数据,根据权限生成权限路由动态添加到路由,设置加载动态路由状态为true,然后放行
        • 未拿到权限数据,获取权限数据并生成路由动态添加,设置加载动态路由状态为true,然后放行

    如何避免刷新页面二次加载路由

    1、window.location.reload()

    2、新创建的router的matcher替换旧的路由matcher

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const createRouter = () => new Router({
    // mode: 'history', // require service support
    scrollBehavior: () => ({ y: 0 }),
    routes: constantRouterMap
    })

    const router = createRouter()

    // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
    export function resetRouter() {
    const newRouter = createRouter()
    // matcher包含了关于路径匹配的所有信息
    router.matcher = newRouter.matcher // reset router
    }

    vuex刷新数据会丢失吗

    会丢失。vuex的数据是保存在运行内存中的,页面刷新会重新加载vue实例,vuex数据会重新进行初始化。

    解决:

    1、把数据保存在浏览器中cookie, sessionStorage, localStorage

    2、页面刷新的时候再次请求数据达到动态更新

    3、页面刷新前保存数据到本地,初始化state数据从本地存储里获取:beforeunload事件在即将卸载页面(即刷新或关闭页面)时触发,beforeunload事件存储vuex数据

    watch和computed有什么区别?

    watch:

    1. watch 是观察的作用,类似于某些数据的监听回调,可以监听data数据和计算属性

    2. 无缓存性,页面重新渲染时,即使值没有改变也执行

    3. 能调用异步函数。

    4. watch是异步的,当被侦听的数据发生变化,并不会立即执行回调,而是等当前dom更新循环后才执行回调函数。可以用$nextTick API 来在 DOM 更新循环结束后立即执行代码。

    computed:

    1. 计算属性,用于计算值等场景

    2. computed的值具有缓存性,computed的值在getter执行后是会缓存的,只有它依赖的属性值改变后,下一次获取computed的值才会重新调用getter来计算。

    3. computed适用于计算比较消耗性能的计算场景

    4. 不能调用异步函数。

    5. 计算属性是同步的。

    如果需要基于已有的数据进行计算并缓存结果,那么应该使用computed。而如果需要监听数据的变化并在变化时执行某些操作,那么应该使用watch

    vuex什么场景使用,属性有哪些?

    vuex用来组件间进行通信。使用场景:用户的个人信息管理模块、购物车模块、订单模块

    vuex属性:

    • state:是应用程序中所有组件的状态集合,存储变量
    • getters:基于state的计算属性,用于逻辑复杂计算
    • mutations:用于提交更改state,同步函数
    • actions:类似事件,可以提交mutations来修改state,可以异步操作
    • modules:将state分割成模块,每个模块都有state、getters、mutations、actions,使得代码更加模块化和清晰易懂。

    资源提示符

    Script标签:

    • defer:边解析HTML文档边下载脚本,解析HTML文档完成后执行脚本;严格按照顺序执行脚本
    • async:边解析HTML文档边下载脚本,脚本下载完成后立即执行脚本,然后继续解析文档;因为是下载完成后立即执行,和脚本下载事件有关,所以脚本执行顺序不一致

    link标签:preload和prefetch都是浏览器优化资源加载的机制

    • preload:允许浏览器在解析HTML文档时,提前加载资源并缓存那些当前页面需要的资源。
    • prefetch:利用浏览器的空闲时间来加载页面将来可能需要的资源的机制。

    Preload和Prefetch的主要区别在于它们加载资源的时机和目的。Preload是在页面渲染过程中提前加载并缓存当前页面需要的资源,以提高页面的渲染速度和性能;而Prefetch则是在浏览器空闲时加载并缓存页面将来可能需要的资源,以提高后续页面的加载速度。这两种机制可以相互结合使用,以最大程度地优化页面的加载速度和性能。

    vue的双向数据绑定的原理

    vue的双向数据绑定是通过数据劫持和发布-订阅模式实现的。劫持和监听所有属性的变动,数据有变动时,触发setter,去通知相关订阅者进行更新。

    实现一个Observer类,递归遍历data,劫持和监听所有属性的变化。数据变动时,触发setter,通知依赖进行更新

    实现一个解析器Compile,解析指令,替换页面中所有变量为数据,初始化渲染页面视图;并对指令的对应节点的绑定相应的更新函数,添加监听数据的订阅者,一旦有变动就进行更新

    实现一个Dep依赖收集起为每个属性设置对应的Dep实例,存储订阅者

    依赖是vue实例、组件中的属性、模版中的表达式等,将依赖抽象为Watcher进行管理,Watcher作为Observer和Compile之间通信的桥梁,可以订阅每个属性的变动,执行绑定的回调函数,从而更新视图。

    整个流程为Data数据通过Observer转换为setter/getter形式来追踪变化,当外界通过Watcher读取数据时,会触发getter从而将watcher添加到依赖中。
    当数据发生变化,会触发setter,从而向Dep中的依赖(Watcher)发送通知。
    Watcher接收到通知后,会向外界发送通知,变化通知到外界可能会触发视图更新,也有可能触发用户某个回调函数。

    diff算法和虚拟dom

    虚拟dom,描述元素与元素之间的关系,是一个js对象

    组件内响应式数据,数据变动时,render函数会重新生成一个新的虚拟dom,新的虚拟dom和旧的虚拟dom进行比对,算出最小量更新,最后反映在真实dom上。

    数据更新->虚拟dom计算变更->操作真实的dom->视图更新

    虚拟DOM 表现为一个 Object对象。并且最少包含标签名 (tag)、属性 (attrs) 和子元素对象 (children) 三个属性

    diff算法就是进行虚拟节点对比,并返回一个patch对象,用来存储两个节点不同的地方,最后用patch记录的消息去局部更新Dom。通俗的讲就是:diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁

    通过diff算法比较 新 旧 两个VDOM,将不同的地方进行修改,相同的地方就地复用,最后再通过render函数渲染页面。

    diff算法:

    key是节点的唯一标识,用来表明新旧节点是否同一个dom

    只比对同一层级,不跨级比较

    只有是同一个虚拟节点才比对,不是直接暴力删除生成并插入新dom

    过程:

    • 调用patch函数,判断oldVnode是不是虚拟节点,不是转换为虚拟节点
    • 判断oldVnode和vnode是否是同一节点(元素选择器相同且key相同),如果不是,则创建新的节点并删除旧的节点
    • 是同一个虚拟节点进行精细比较,调用patchVnode函数判断oldVnode和vnode是否指向同一个对象,是则直接返回
    • 如果oldVnode和vnode都有文本节点且不相同,vnode的文本节点设置为真实dom的文本节点
    • 如果oldVnode有子节点,vnode没有,删除真实dom对应子节点
    • 如果oldVnode没有子节点,vnode有,将vnode的子节点真实化后添加到真实dom
    • 如果oldVnode和vnode都有子节点则执行updateChildren函数比较子节点

    diff比较同级节点:为保证dom顺序和新节点保持一致采用while循环和首尾节点进行比较,给oldVnode和vnode节点数组设置开始结束索引,遍历的过程往中间移动。这样既能实现排序也减小了时间复杂度。两节点比较,有四种比对方式:

    oldStartIndex —> newEndIndex

    oldEndIndex —> newStartIndex

    oldStartIndex —> newStartIndex

    oldEndIndex —> newEndIndex

    如果oldStartIndex和newEndIndex匹配,则将真实dom的第一个元素移动到最右;

    如果oldEndIndex和newStartIndex匹配,则将真实dom的最后一个元素移动到最左;

    匹配过程指针中间移动,直到while循环结束

    如果四种方式都没有匹配,那么:

    • 遍历vnode数组,在oldVnode用key进行查找
    • 如果没有找到,当前节点是新节点,创建对应dom元素并插入到真实dom
    • 如果找到,判断选择器是否相同
      • 如果相同:说明是同一个节点,移动到dom元素的最左边
      • 如果不同,不是一个节点,创建对应的dom元素并插入到真实dom树中
    • 循环结束后,判断oldVnode和vnode是否有剩余:
      • oldVnode有剩余,说明节点被删除,找到dom删除对应节点
      • vnode有剩余,说明节点是新增的,则将剩余子节点真实化后添加到真实dom

    a链接下载文件

    一、a链接下载文件地址,服务器设置响应头Content-Disposition为attachment告诉浏览器该文件应该被视为附件并进行下载,而不是直接在浏览器中打开。

    1
    2
    3
    4
    <a href="/download">点击下载</a>

    // 服务器设置
    res.setHeader('Content-Disposition', `attachment; filename=${fileName}`);

    二、a链接使用HTML的download属性。download属性告诉浏览器该链接是用于下载

    1
    <a href="/path/to/file.ext" download="file.ext">点击下载</a>

    需要注意的是,download属性只在同源文件下载时有效。如果尝试跨域下载文件,或者从第三方平台下载文件(例如微信公众号资源图片),浏览器将不会执行下载操作,而是尝试打开或显示该文件。

    三、通过接口请求文件下载,需要传递token验证权限情况

    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
    // 假设你的服务器端点是 /api/download  
    fetch('/api/download', {
    method: 'GET', // 或者 'POST',取决于你的服务器端点要求
    mode: 'cors', // 允许跨域请求
    credentials: 'include' // 如果需要发送cookies
    })
    .then(response => {
    // 检查响应状态
    if (!response.ok) {
    throw new Error('Network response was not ok');
    }

    // 由于安全限制,浏览器不会直接处理文件下载,因此我们需要创建一个Blob对象
    // 并使用URL.createObjectURL()来生成一个指向该Blob的URL,然后创建一个a标签来下载
    return response.blob();
    })
    .then(blob => {
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'filename.ext'; // 设置下载的文件名
    a.style.display = 'none';
    document.body.appendChild(a);
    a.click();
    window.URL.revokeObjectURL(url); // 释放URL对象
    document.body.removeChild(a); // 清理a标签
    })
    .catch(error => {
    console.error('There was a problem with the fetch operation:', error);
    });

    注意这种下载文件方式,要浏览器先传输文件到内存中,再触发下载到本地,如果文件过大会占用浏览器大量内存,且等待一段时间才触发下载弹出下载保存窗口。

    四、解决需要验证权限的文件下载

    先发送请求服务器返回set-cookie,浏览器保存在本地,再创建a标签下载文件,a标签会自动携带cookie,服务器验证通过后将文件传输直接触发文件下载,从网络IO变为文件IO,不经过浏览器,下载更快

    设计私有属性

    1、通常约定在属性名称前加上一个下划线_

    2、使用Symbol类型来创建一个唯一的、不可变的属性键

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const PRIVATE_ATTR = Symbol('privateAttr');  

    class MyClass {
    constructor() {
    this[PRIVATE_ATTR] = null;
    }

    setPrivateAttr(value) {
    this[PRIVATE_ATTR] = value;
    }

    getPrivateAttr() {
    return this[PRIVATE_ATTR];
    }
    }

    // 使用示例
    const obj = new MyClass();
    obj.setPrivateAttr('hello');
    console.log(obj.getPrivateAttr()); // 输出: hello

    // 尝试直接访问私有属性会失败
    // console.log(obj.PRIVATE_ATTR); // undefined
    // 但是可以通过Object.getOwnPropertySymbols()方法获取

    3、es6中在属性名称前加#

    4、使用对象weakmap:WeakMap允许你存储对象的私有数据,这些数据只能通过WeakMap本身来访问

    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
    const privateData = new WeakMap();  

    class MyClass {
    constructor() {
    privateData.set(this, { privateAttr: null });
    }

    setPrivateAttr(value) {
    const data = privateData.get(this);
    data.privateAttr = value;
    }

    getPrivateAttr() {
    const data = privateData.get(this);
    return data.privateAttr;
    }
    }

    // 使用示例
    const obj = new MyClass();
    obj.setPrivateAttr('hello');
    console.log(obj.getPrivateAttr()); // 输出: hello

    // 尝试直接访问私有属性会失败
    // console.log(obj.privateAttr); // undefined

    5、使用ts的private声明定义变量

    6、使用闭包和弱引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 创建一个只能在类的实例内部访问的属性。
    function MyClass() {
    // 私有属性
    let _privateAttr = null;

    // 公共方法
    this.setPrivateAttr = function(value) {
    _privateAttr = value;
    };

    this.getPrivateAttr = function() {
    return _privateAttr;
    };
    }

    // 使用示例
    const obj = new MyClass();
    obj.setPrivateAttr('hello');
    console.log(obj.getPrivateAttr()); // 输出: hello

    // 尝试直接访问私有属性会失败
    // console.log(obj._privateAttr); // undefined

    vuex是如何实现响应式的?

    在 Vuex 中,state 对象在初始化时被递归地应用了 Object.defineProperty(),将其属性转换为 getter/setter,从而使得当 state 中的数据发生变化时,Vue 能够感知到这些变化并触发相应的更新。

    需要注意的是,Vuex 的 state 中的数据必须是提前定义好的初始化属性,动态添加的属性是无法保证响应式的。如果需要动态添加属性,可以使用 Vue.set(obj, key, value) 方法或者通过创建一个新的对象来替换旧的对象。

    如何封装一个组件

    • 定义组件,包含模版、脚本、样式
    • 注册组件:
      • 全局注册:main.js文件引入组件,Vue.components(‘Child’, Child)
      • 局部注册:父组件引入组件,components选项配置
    • 使用组件:组件模版中直接使用组件<Child />
    • 传递数据:
      • 父向子传值:通过props属性
      • 子与父通信:子组件$emit事件与父组件通信
    • 插槽:可以在组件模版中插入内容

    如何搭建一个脚手架?

    • 安装nodejs、npm、vue-cli npm install -g @vue/cli
    • 命令创建vue项目:vue create my-project
    • 选择预设配置
    • 安装依赖
    • 启动开发服务器

    如何封装一个可复用的组件?

    • 简洁的API提供清晰的props和events作为输入和输出,避免直接操作父组件的状态或子组件的数据;
    • 良好的文档和示例;
    • 无副作用不依赖于外部的全局状态或变量;避免修改全局状态;
    • 良好的错误处理;
    • 组件尽可能独立,不依赖于其他特定组件或库;
    • 随着项目的发展和需求的变化持续进行维护;
    • 考虑扩展性比如提供插槽或mixins使组件更加灵活和可扩展;
    • 提供默认样式组:但也要允许通过CSS变量或自定义类名来覆盖这些样式

    首屏优化怎么做?

    • 使用路由懒加载
    • 非首屏组件使用异步组件
    • 首屏不重要的组件延迟加载
    • 静态资源放cdn上
    • 减少首屏js、css等资源文件的大小
    • 使用服务端渲染
    • 使用精灵图和base64图减少http请求
    • 开启gzip压缩
    • 图片懒加载
    • 做loading动画优化用户体验

    vue3性能为什么比vue2好?

    基于proxy的响应式系统提供了更精细的控制和更好的性能

    vue3对模版编译器和运行时进行了重构和优化,减少了库的体积,提高了加载速度。

    Vue 3 中的编译器增加了更多的编译时优化,包括模板中静态节点的标记、事件处理函数的缓存等:

    • 静态节点标记:vue3在编译阶段就将静态节点提取出来,并在运行时使用,避免了不必要的渲染。
    • 事件处理函数的缓存:vue3的编译器会识别出不依赖于响应式数据的事件处理函数,并将他们缓存起来,以避免每次渲染时都重新创建分配内存

    diff算法优化:

    • 静态节点提升:在编译阶段将静态节点提取出来,在运行时不需要参与diff算法比对,减少了需要比较的节点数量

    vue3为什么使用proxy?

    更全面的数据劫持

    Proxy 可以劫持整个对象,而不仅仅是对象的属性。这意味着无论对象如何变化(例如属性的添加、删除或修改),Proxy 都能捕捉到这些变化,并触发相应的响应。相比之下,Object.defineProperty 只能劫持已经存在的属性,对于新增或删除的属性无法感知。

    数组的支持

    Proxy 对数组的支持更好。在 Vue 2 中,为了实现对数组方法的劫持,不得不通过重写数组原型上的方法来实现。这样做不仅增加了代码的复杂性,而且可能引发一些不易察觉的问题。而 Proxy 可以直接监听数组的变化,无需额外处理。

    对组件的理解

    可复用的vue实例,拥有完整的生命周期,接收特定的输入以产生输出。

    组件的目的就是将UI划分出可独立的、可复用的部分,使代码可维护提高开发效率。

    nuxtjs

    是基于vuejs的框架,关注的是应用的ui渲染。它预设了使用vuejs开发服务端渲染应用所需的各种配置。并提供了nuxt generate命令对基于vuejs的应用生成静态站点的功能。

    优点:

    • 基于服务端ssr渲染:生成的带有html的内容,有利于搜索引擎的搜索,并优化了首屏加载时间
    • 强大的路由管理,开发者可以实现单页应用(SPA)和多页应用(MPA)之间的无缝切换
    • Nuxt.js还支持异步数据加载、中间件、布局、EcmaScript6和EcmaScript7的语法

    watchQuery: 监听参数字符串更改并在更改时执行组件方法 (asyncData, fetch, validate, layout, …),开启该功能,nuxt.config.js设置watchQuery:true

    1
    2
    3
    4
    5
    6
    export default {
    key(route) {
    return route.query.hxss || route.query.deptId
    },
    watchQuery: ['hxss', 'deptId']
    }

    可定时器隔一段时间修改route.query.hxss的值实现无痕刷新

    asyncData:在服务端或路由更新前被调用,该方法会在组件(限页面组件)被加载之前调用。

    1
    2
    3
    asyncData(context) {
    return { project: 'nuxt' } // 不能使用this
    }

    fetch:方法会在渲染页面前填充组件的状态树数据,在每次组件加载前调用(服务端或切换至目标路由之前)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <template>
    <h1>Stars: {{ $store.state.stars }}</h1>
    </template>

    <script>
    export default {
    fetch({ store, params }) { // 参数context
    return axios.get('http://my-api/stars').then(res => {
    store.commit('setStars', res.data)
    })
    }
    }
    </script>

    key属性:key属性赋值到<router-view>,这对于在动态页面和不同路径中进行转换很有用。不同的key会使页面组件重新渲染。

    1
    2
    3
    4
    5
    export default {
    key(route) {
    return route.fullPath
    }
    }

    layout 属性:为页面指定使用哪一个布局文件

    渲染方式

    客户端渲染:用户访问url,请求html文件,前端根据路由动态渲染页面内容,关键链路较长,有一定的白屏时间

    服务端渲染:用户访问url,服务端根据访问路径请求所需数据,拼接成html字符串,返回给前端,前端接收到html已有部分内容

    预渲染:构建阶段生成匹配预渲染路径的html文件(每个需要预渲染的路由都有一个对应的 html),构建处理的html已有部分内容

    服务端渲染:发送请求-服务器请求数据渲染html-包含首屏的html

    预渲染:发送请求-包含首屏的html

    客户端渲染:发送请求-html-js加载并动态渲染

    工作中遇到的难题

    开发app对接高德地图,实现员工当天轨迹图展示,配置好权限和key后,发现真机运行正常,打包到测试机上运行其他页面正常,轨迹图页面白屏(android)。

    android studio真机运行,排查后发现缺少一个高德地图的依赖库(地图依赖多个库,以前引入过部分库),导致页面白屏,下载最新依赖并引入后,仍然存在问题;
    初步排查是离线打包配置及相关文件和最新依赖资源版本不匹配的原因,重新创建android/ios原生工程,进行基础库和应用配置,完成后再次打包地图页面正常。得出结论依赖库版本和工程相关配置及文件相关联,需要同步配置。

    接着根据需求,绘制员工轨迹图,且缩放视野以包含所有给定的坐标点(include-points, markers, polyline),发现android地图标记点气泡消失,经排查发现缩放过程中地图放大再移动到某个标记点后,标记点气泡消失,缩小后气泡又出现;因ios版本正常,确定非api使用问题,社区上报bug并证实是官方bug。

    will-change

    will-change是CSS中的一个属性,用于提前通知浏览器哪些元素的属性即将发生变化,以便浏览器能提前做好对应的优化准备工作,提高浏览器的页面渲染性能。有助于避免元素属性变化时导致页面卡顿失帧等交互体验不佳的问题。此外,当元素的属性变化完成后,应尽快移除will-change属性,以避免不必要的性能损耗。

    小程序

    1. 数据请求封装
      在小程序中,我会使用内置的wx.request API来进行网络通信。封装时,我会创建一个单独的服务模块或类,统一处理所有HTTP请求,便于管理和维护。示例代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      // request.js
      export function request(options) {
      wx.showLoading({ title: '加载中...' });
      return new Promise((resolve, reject) => {
      wx.request({
      ...options,
      success: (res) => {
      wx.hideLoading();
      resolve(res.data);
      },
      fail: (err) => {
      wx.hideLoading();
      reject(err);
      },
      complete: () => {},
      });
      });
      }

      在此基础上,可以增加错误处理、重试机制、缓存策略(如对GET请求结果做本地缓存)以及拦截器的设计,比如在请求前后执行通用操作。

    2. 参数传递方法
      小程序中页面间的参数传递主要有以下方式:

      • wx.navigateTowx.redirectTowx.switchTab等路由跳转API中,可以在options对象中加入params字段,用于传递JSON格式的参数。
      • 也可以在URL后面附加query参数,如wx.navigateTo({url: '../targetPage/index?param1=value1&param2=value2'}),在目标页面的onLoad生命周期函数中通过options.query获取。
    3. 性能优化

      • 使用分包加载,将大的小程序拆分为多个子包,按需加载,提高首屏加载速度。
      • 对于静态资源如图片,使用CDN加速,并合理设置缓存策略,如<image src="" mode="aspectFit" lazy-load/>实现图片懒加载。
      • 减少不必要的网络请求,合并多次请求为一次,合理利用缓存,避免重复获取数据。
      • 优化布局,尽量减少层级过深的嵌套,运用flex布局减少布局计算消耗。
      • 对频繁变动的部分采用虚拟列表等技术优化渲染性能。
    4. 小程序优点

      • 即点即用,无需下载安装,易于推广和分享。
      • 离线缓存能力强,用户体验好,加载速度快。
      • 提供丰富的API接口,如地理位置、支付、授权等,方便构建完整的业务闭环。
      • 与微信生态深度融合,可以便捷地实现微信账号体系登录、微信支付、社交分享等功能。
    5. 小程序缺点

      • 平台兼容性差,无法在非微信环境下运行。
      • 分发渠道相对单一,依赖微信平台,传播受到一定限制。
      • 生态系统相对封闭,与Web标准有一定的差异,代码难以复用到其他平台。
    6. WXML与HTML的差异

      • WXML是微信小程序自定义的一种标记语言,它类似HTML,但提供了微信特有的标签,如wx:if用于条件渲染,wx:for用于列表渲染,数据绑定采用{{ }}语法而非HTML的data-*属性。
      • WXML不支持所有HTML标签,但提供了与小程序功能紧密相关的标签,如<view>代替<div><button>支持开放能力等。
    7. 小程序生命周期

      • onLoad:页面加载时触发,此时可以做一些初始化工作,如从服务器获取数据。
      • onShow:页面显示/切回来时触发,每次打开页面都会调用,可用来处理页面显示相关逻辑。
      • onReady:页面初次渲染完成时触发,此时可以获取到页面的一些DOM信息。
      • onHide:页面从前台切换到后台时触发,可以在此时清除定时器,保存数据等。
      • onUnload:页面卸载时触发,用于清理页面资源。
    8. 登录流程

      • 用户首先点击登录按钮,触发wx.login获取临时登录凭证(code)。
      • 将code传送到服务器,服务器调用微信接口换取用户的唯一标识(openid)和session_key。
      • 若需要用户信息,调用wx.getUserInfo并获取用户同意授权后得到加密的用户信息。
      • 服务器解密用户信息,并结合openid和session_key生成自定义的token,返回给客户端进行后续身份验证和鉴权。
    9. 页面下拉刷新与上拉加载

      • 下拉刷新:在页面json文件或Page构造器的options中设置enablePullDownRefresh为true,然后在Page的onPullDownRefresh方法中发起数据更新请求,请求完成后调用wx.stopPullDownRefresh()结束刷新动画。
      • 上拉加载更多:在Page中监听onReachBottom事件,当页面滚动到底部时触发该事件,执行获取更多数据的逻辑。
    10. 组件化开发

      • 在小程序中,创建自定义组件需在components目录下创建一个文件夹,包含index.wxml、index.wxss、index.js和index.json四个文件。
      • 在index.js中定义组件,通过properties接受外部传入的props,通过setData更新组件内部数据,并在wxml中绑定数据和事件。
      • 外部页面使用自定义组件时,如同普通标签一样引用,并通过属性传递数据给组件。组件内部事件通过this.triggerEvent触发,外部页面通过catchEvent接收。
    11. setData

      • setData方法在数据从逻辑层向视图层传递是异步的,但是更新this.data的值是同步的,视图层需要再次渲染才更新。
      • setData是单向数据绑定的,setData时视图层会响应这些变化,但在视图层(如输入框)的更改不会传递给逻辑层的this.data中。要实现双向绑定在元素的属性前添加model:前缀。

    小程序底层

    小程序的底层实现通常是指其运行环境和架构设计。以微信小程序为例,其底层架构主要特点包括以下几个方面:

    1. 双线程模型

      • 逻辑层(App Service):小程序的业务逻辑层运行在一个独立的JsCore线程中,这个线程并不直接访问或操作DOM,从而保证了更高的安全性。在此环境中,开发者编写的所有业务逻辑、数据处理、网络请求等均在此线程上执行。

      • 渲染层(View Layer):负责界面渲染,基于WebView组件,在不同的WebView线程中执行。每个小程序页面都有对应的WebView实例来渲染界面。由于没有直接暴露DOM API,而是通过一套自定义的视图层描述语言(WXML、WXSS)来构建界面。

    2. 通信机制:逻辑层与渲染层之间通过系统提供的WeixinJsBridge(或称JSBridge)进行通信。逻辑层会将处理后的数据发送给渲染层,触发界面更新;同时,渲染层的用户交互事件会被传递回逻辑层进行处理。

    3. 框架特性:微信小程序框架基于浏览器内核重构,但剥离了DOM和BOM相关的API,提供了自己的API集,如setData方法用于更新视图。此外,还提供了丰富的组件库和API,支持调用微信的原生能力,如获取用户信息、支付、地理位置等功能。

    4. 混合渲染:虽然渲染层基于WebView,但在实现上结合了Web渲染与Native渲染的优势,采取了一种Hybrid模式,部分功能调用原生模块来提升性能和用户体验。

    综上所述,小程序的底层设计兼顾了Web开发的便捷性和原生应用的性能优势,通过定制化的开发环境和框架体系来确保小程序具有轻量化、高性能的特点,并且易于接入特定平台(如微信)提供的丰富服务。随着技术发展,不同小程序框架可能会有不同的优化策略和技术路线,但核心思想基本围绕高效渲染、安全隔离以及灵活调用平台能力等方面展开。
    HTML:

    请解释HTML5的新特性及语义化标签的意义。

    新特性:

    多媒体支持:

    <audio><video> 标签允许直接在网页中嵌入音频和视频内容,无需第三方插件,提供了原生的播放控制功能。
    <canvas> 元素提供了在网页上进行图形绘制的能力,可以创建动态图形和游戏。
    <svg> 支持矢量图形,允许在网页中直接嵌入可缩放的矢量图形内容。

    离线存储:

    应用程序缓存(Application Cache,现已被Service Worker取代)允许Web应用程序在离线状态下依然可用。
    IndexedDB 和 Web Storage(包括localStorage和sessionStorage)提供了持久化数据存储的方式。

    Web Workers:

    允许在后台线程中运行JavaScript,从而不会阻塞UI线程,提高复杂计算任务的性能。

    Web Sockets:

    实现了双向通信,使得服务器和客户端之间能够实时、全双工的数据传输。
    地理定位:

    Geolocation API 可以获取用户的地理位置信息。

    表单控件增强:

    新增了<input type="date"><input type="time"><input type="email"><input type="url"><input type="search">等类型的输入控件,提高了表单数据验证和用户输入体验。

    语义化标签是为了增强HTML文档结构的逻辑性,使内容具有明确的意义,不仅帮助开发者组织文档,而且利于搜索引擎抓取和解析,提升可访问性和用户体验

    举例说明行内元素、块级元素和空元素,并解释其特点

    在HTML中,元素根据其显示方式和行为可以分为行内元素、块级元素和空元素.

    1. 行内元素(Inline Elements)

    行内元素不会开始新的行,它们只会占据其内容本身的宽度和高度。行内元素通常用于表示文本或文本的一部分,例如链接、强调的文本等。

    例子:<span> <a> <strong> <em>

    特点:

    不会独占一行,和其他行内元素并排显示。
    宽度和高度由内容决定,不能设置固定的宽高。
    只能容纳文本或其他行内元素,不能包含块级元素。

    1. 块级元素(Block Elements)

    块级元素会开始新的行,并尽可能占据其父元素的全部宽度。块级元素通常用于布局和结构化内容,例如段落、标题、列表等。

    例子:<div> <p> <h1> <h6> <ul> <li>

    特点:

    独占一行,垂直排列。
    宽度默认为其父元素的100%,高度由内容决定。
    可以设置宽度、高度、内外边距等样式属性。
    可以包含其他块级元素和行内元素。

    1. 空元素(Void Elements)

    空元素是指那些没有内容,只有起始标签的元素。它们通常用于表示那些不需要包含任何文本或其他元素的元素,例如换行符、图像等。

    例子: <br>``<img>``<input>``<hr>

    特点:

    只有起始标签,没有结束标签。
    不能包含任何内容。
    主要用于表示特定的功能或效果,例如换行、插入图像等。

    CSS:

    介绍一下CSS盒模型的不同类型(标准盒模型与IE盒模型)以及如何切换这两种模型。

    标准盒模型(W3C盒模型):

    在标准盒模型中,width和height属性仅包含内容(content)的宽度和高度,即元素的实际内容所占的空间。内边距(padding)、边框(border)和外边距(margin)都不包括在width和height中。这意味着,如果你设置了一个元素的width和height,并且这个元素有内边距或边框,那么元素的实际显示尺寸会比你设定的尺寸大。

    IE盒模型(怪异盒模型):

    在IE盒模型中,width和height属性则包含了内容、内边距和边框的总宽度和总高度。这意味着,如果你设置了一个元素的width和height,这个尺寸就包括了内容、内边距和边框,外边距则仍然不包括在内。因此,在IE盒模型中,元素的实际显示尺寸与你设定的尺寸一致,除非有外边距。

    如何切换这两种模型:

    在CSS3中,可以通过设置元素的box-sizing属性来切换盒模型。box-sizing属性有两个值:content-box和border-box。

    当box-sizing设置为content-box时,使用的是标准盒模型。元素的width和height只包括内容的宽度和高度,内边距和边框会增加元素的实际显示尺寸。
    当box-sizing设置为border-box时,使用的是IE盒模型。元素的width和height包括内容、内边距和边框的总宽度和总高度,元素的实际显示尺寸与你设定的尺寸一致。

    如何实现响应式布局?介绍几种常见的方法和技术

    一套代码适应不同的屏幕尺寸。
    1、百分比布局

    百分比相对于包含块计算,元素的位置和尺寸由包含块决定,默认是最近祖先块元素的内容区域,width\top\bottom百分比值由包含块的width决定,height\left\right\margin\padding百分比值由包含块的height决定

    2、媒体查询布局

    通过@media 媒体查询,可以通过给不同屏幕的大小编写不同的样式来实现响应式的布局。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    .box {
    width: 500px;
    height: 500px;
    background-color: aqua;
    }
    @media screen and (max-width: 1280px) {
    .box {
    width: 400px;
    height: 400px;
    }
    }
    @media screen and (max-width: 980px) {
    .box {
    width: 300px;
    height: 300px;
    }
    }
    @media screen and (max-width: 765px) {
    .box {
    width: 200px;
    height: 200px;
    }
    }

    响应式缺点:如果浏览器大小改变时,需要改变的样式太多,那么多套样式代码会很繁琐。

    3、rem 响应式布局

    rem 是相对于 html 根元素的字体大小的单位。

    我们通过修改 html 中 font-size 的字体大小来控制 rem 的大小。

    1
    html { font-size: 10px;}.box { width: 10rem; height: 20rem;}

    当 html 中 font-size: 10px; 时,此时 1rem = 10px,所以 box 盒子的宽高分别为:100px 和 200px;

    当我们把 html 中 font-size: 20px; 时,此时 1rem = 20px,此时 box 盒子的宽高就为 200px 和 400px;

    在实际的开发中,我们通常会以 750px 的移动端设计稿来开发。1rem = 75px

    我们在代码写完后,统一会把所有 px 单位全部转成 rem 来实现.
    js 动态修改 html 根元素的 font-size 的大小,能适配不同尺寸的屏幕

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <script>
    initPage()
    function initPage() {
    var clientWidth =
    document.documentElement.clientWidth || document.body.clientWidth //获取屏幕可视区宽
    var html = document.getElementsByTagName("html")[0] //获取html根元素
    html.style.fontSize = clientWidth / 10 + "px" //动态设置font-size大小
    }
    window.onresize = initPage
    </script>

    flexible.js 原理就是根据不同的屏幕宽度动态的设置网页中 html 根节点的 font-size

    4、vw 和 vh响应式布局
    vw 和 vh 分别相对的是视图窗口的宽度和视图窗口的高度。
    在宽为 750px 的设计稿下,把 px 转换为 vw,是用 px/7.5 得到对应的 vw 单位,即750px为100vw

    5、flex 弹性布局

    在父元素上,我们经常会用到的有关弹性布局的属性主要有 flex-direction , flex-wrap , justify-content , align-items , align-content ,这几个属性分别从 主轴的方向、是否换行、项目在主轴上的对齐方式、项目在交叉轴上的对齐方式、项目在多根轴线上的对齐方式来规范了项目在父元素中的弹性。

    在子元素上,我们经常会用到的有关弹性布局的属性主要有 order , flex-grow , flex-shrink ,flex-basis , align-self ,这几个属性分别从 项目的排序、项目放大比例、项目缩小比例、项目占据主轴空间、单个项目在交叉轴上的对齐方式来规范了项目自身的弹性。

    实现数组去重的方法有哪些?

    1、使用Set数据结构(ES6):

    1
    2
    let array = [1, 2, 3, 2, 1, 4, 5, 4];
    let uniqueArray = [...new Set(array)];

    2、借助indexOf函数: indexOf() 方法返回数组中第一次出现给定元素的下标,如果不存在则返回 -1.

    1
    2
    3
    4
    5
    6
    7
    let array = [1, 2, 3, 2, 1, 4, 5, 4];
    let uniqueArray = []
    for(let i = 0; i < array.length; i++) {
    if (uniqueArray.indexOf(array[i] === -1)) {
    uniqueArray.push(array[i])
    }
    }

    3、借助filter方法:

    1
    2
    3
    4
    let array = [1, 2, 3, 2, 1, 4, 5, 4];
    let uniqueArray = array.filter((element, index, self) => {
    return self.indexOf(element) === index
    })

    4、利用reduce方法:

    1
    2
    3
    4
    5
    6
    7
    let array = [1, 2, 3, 2, 1, 4, 5, 4];
    let uniqueArray = array.reduce((calcResult, current) => {
    if (!calcResult.includes(current)) {
    calcResult.push(current)
    }
    return calcResult
    }, [])

    性能优化:

    如何减少重绘和回流?请举例说明。

    1.批量修改DOM和样式

    • 尽量将多个DOM操作集中在一起执行,而不是分散在不同的事件或者循环中逐个修改。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 不好的做法(每次循环都会引起回流)
    for (let i = 0; i < 1000; i++) {
    document.getElementById('list').style.height = i + 'px';
    }

    // 好的做法(一次性设置,仅触发一次回流)
    let newHeight;
    for (let i = 0; i < 1000; i++) {
    newHeight = i + 'px';
    }
    document.getElementById('list').style.height = newHeight;

    2.使用CSS类代替内联样式

    • 修改类名而不是直接修改元素样式,可以避免连续的样式变更引起的重绘。
    1
    2
    3
    4
    5
    6
    /* CSS */
    .hidden { display: none; }
    .visible { display: block; }

    //JavaScript
    element.classList.add('hidden'); // 更改类名而非 style.display

    3.避免不必要的布局信息查询

    • 查询布局信息(如offsetTop/Left、scrollWidth/Height等)也会触发回流。在不需要精确值时,考虑延后获取或使用MutationObserver观察变化。

    4.利用层叠上下文(Composite Layers)

    • 如果元素有独立动画,可以通过will-change属性或者3D变换(translateZ(0))创建一个新的层,使得重绘只发生在单独的合成层上,不影响主文档流。

    5.使用CSS3硬件加速

    • 对于动画,使用transform和opacity属性,这两个属性的变化不会触发重排,只会触发重绘,并且可以被GPU硬件加速。

    6.虚拟DOM

    • 使用像React或Vue这样的现代前端框架,它们提供了虚拟DOM机制,只有当实际视图需要更新时才会对比并最小化地更新真实DOM,从而减少回流和重绘的发生。

    7.懒加载和异步更新

    • 对于不在可视区域内的图片或动态内容,延迟加载直到进入视窗再进行渲染,可以有效避免非必要的计算和渲染。

    8.分离读写操作

    • 如果既有读取又有修改DOM的操作,尽量先完成读取,然后集中进行写入,避免读写交错引起的多次回流。
    1. 减少不必要的DOM节点数量
    • 减少冗余DOM节点和深度嵌套结构,特别是对于动态生成的内容,可以显著降低回流发生的频率和范围。

    通过上述方法和原则,可以有效地减少重绘和回流,从而提高页面的运行效率和用户交互体验。

    请解释前端资源懒加载和预加载的策略

    前端资源懒加载(Lazy Loading)和预加载(Preloading)是两种截然不同的资源加载策略,它们分别用于优化网页性能和用户体验:

    懒加载(Lazy Loading)
    懒加载是一种按需加载的技术,主要用于延后加载那些并非页面初始渲染所必需的资源,尤其是对于长页面或包含大量图片、视频等内容的网站尤其有用。具体来说,懒加载的做法包括但不限于:

    • 图片懒加载:当页面加载时,不是一次性加载所有图片,而是仅加载首屏可见区域的图片,其他图片在用户滚动到相应位置时才开始加载,从而减少初次加载的带宽消耗和时间成本。

      • 懒加载通常应用于图片、视频、Ajax请求等领域,通过监听滚动事件、 Intersection Observer API 等方式实现。
        IntersectionObserver API用于监控dom元素是否进入可视区域或者与某个祖先元素的交叉状态
      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
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      let observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
      if (entry.isIntersecting) { // 如果元素进入了可视区域
      console.log('Element is now visible');
      // 在这里可以进行懒加载图片或其他资源的操作
      loadImage(entry.target);
      }
      });
      }, { threshold: 0.5 }); // 设置阈值为0.5,表示当元素50%的部分进入视窗时触发回调

      // 观察目标元素
      observer.observe(document.querySelector('#lazy-image'));
      // 动态加载图片
      function loadImage(imageElement) {
      // 假设我们有一个数据结构存储了需要懒加载图片的真实URL
      const imageSources = {
      'lazy-image-1': 'path/to/image1.jpg',
      // ...其他图片路径
      };

      // 获取该元素对应的图片源
      let src = imageSources[imageElement.id]; // 这里假设元素id对应图片源的键名

      // 确保已经获取到图片源
      if (src) {
      // 创建一个新的Image对象
      let img = new Image();

      // 当图片加载完成时执行的回调函数
      img.onload = function() {
      // 图片加载成功后,将src属性赋给DOM元素
      imageElement.src = src;

      // 可能还需要添加一些额外逻辑,比如显示加载完成后的效果
      imageElement.classList.remove('loading');
      };

      // 图片加载失败时的处理
      img.onerror = function() {
      console.error('Failed to load image:', src);
      };

      // 开始加载图片
      img.src = src;
      } else {
      console.warn('No image source found for element with id:', imageElement.id);
      }
      }

      批量图片懒加载:

      1
      2
      3
      < img class="lazy" data-src="path/to/image1.jpg" alt="Lazy loaded image">
      <!-- 其他需要懒加载的图片...
      -->
      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
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      // 初始化Intersection Observer实例
      let lazyImages = [];
      const observer = new IntersectionObserver(
      (entries, observer) => {
      entries.forEach((entry) => {
      if (entry.isIntersecting) {
      loadAndReplaceImage(entry.target);
      observer.unobserve(entry.target);
      }
      });
      },
      { threshold: 0.5 }
      );

      // 收集所有需要懒加载的图片
      document.querySelectorAll('.lazy').forEach((img) => {
      lazyImages.push(img);
      observer.observe(img);
      });

      // 加载并替换图片的函数
      function loadAndReplaceImage(imgElement) {
      const realSrc = imgElement.dataset.src;

      // 创建新的Image对象
      let newImage = new Image();

      // 图片加载成功
      newImage.onload = function() {
      imgElement.src = realSrc;
      imgElement.classList.remove('lazy'); // 移除懒加载类,可选
      };

      // 图片加载失败
      newImage.onerror = function() {
      console.error('Failed to load image:', realSrc);
      };

      // 开始加载图片
      newImage.src = realSrc;
      }

      // 监听滚动事件,仅当Intersection Observer不支持时才需要
      if ('IntersectionObserver' in window) {
      // 如果浏览器支持Intersection Observer,则无需额外处理滚动事件
      } else {
      // 若不支持Intersection Observer,可以在此处添加滚动事件监听并手动检查图片是否在视口中
      window.addEventListener('scroll', () => {
      // 实现滚动时检查图片位置的逻辑
      // 但请注意,这种方法效率较低,应尽量优先使用Intersection Observer API
      });
      }
    • 动态模块懒加载:在单页应用(SPA)中,根据用户的导航行为,延迟加载未访问路由对应的JavaScript或CSS资源。

    懒加载的优势在于:

    • 减少初始加载时间,提高页面的首屏加载速度。
    • 减轻服务器压力,特别是当同时在线用户数较多时,能够显著降低服务器带宽需求。
    • 改善用户在移动设备上浏览时的数据流量消耗。

    预加载(Preloading)
    预加载则是指在用户尚未明确请求之前,预测并主动加载可能会用到的资源。这可以是即将浏览到的图片、视频,或者是即将跳转的页面所需的资源等。例如:

    • 预加载图片:创建一个新的Image对象,设置其src属性指向待加载的图片地址,浏览器会在后台开始加载,一旦用户需要时,图片已存在于缓存中,可以直接渲染。
    • 预加载资源链接:通过HTML <link rel="preload"> 标签预先告知浏览器加载指定的资源,这些资源可能是CSS样式表、JavaScript文件或其他类型的内容。

    预加载的优点在于:

    • 缩短用户交互时的等待时间,提升用户体验,因为相关资源已经在用户可能需要之前加载完毕。
    • 当用户实际访问时,资源可以直接从缓存中读取,减少了网络延迟。

    综合来看,懒加载和预加载虽然方向相反,但都是为了优化网页性能和用户体验,只是应用场景和目标有所差异。懒加载适用于节省初次加载资源量和时间的情况,而预加载更注重提升后续操作的流畅度和响应性。

    如何配置Webpack进行项目构建?列举几个常用的Loader和Plugin

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    name: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    },
    // Loader配置
    module: {
    rules: [
    // 处理JS文件的Babel Loader
    {
    test: /\.js$/,
    exclude: /node_modules/,
    use: ['babel-loader'],
    },
    // 处理CSS文件的CSS Loader和Style Loader
    {
    test: /\.css$/,
    use: ['style-loader', 'css-loader'],
    },
    // 处理图片的File Loader
    {
    test: /\.(png|jpg|jpeg|gif|svg)$/,
    use: [{
    loader: 'file-loader',
    }]
    }

    Webpack是一个模块打包工具,用于处理前端资源的转换、合并、压缩等工作。配置Webpack进行项目构建主要涉及以下几个步骤:

    1. 创建webpack.config.js配置文件。

    2. 配置入口(entry)和出口(output)。

    3. 添加Loader用于处理不同类型的文件。

    4. 使用Plugin增强Webpack功能。

      以下是配置Webpack的基本结构以及一些常用的Loader和Plugin示例:

    基本配置示例:

    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
    33
    // webpack.config.js
    const path = require('path');

    module.exports = {
    // 入口文件
    entry: './src/index.js',
    // 输出目录和文件名
    output: {
    file options: {
    outputPath: 'images/',
    publicPath: '../images/',
    name: '[name].[hash:8].[ext]',
    },
    }],
    },
    ],
    },
    // Plugin配置
    plugins: [
    // 生成HTML文件的HtmlWebpackPlugin
    new HtmlWebpackPlugin({
    template: './public/index.html',
    filename: 'index.html',
    }),
    // 优化CSS提取的MiniCssExtractPlugin
    new MiniCssExtractPlugin({
    filename: '[name].css',
    chunkFilename: '[id].css',
    }),
    // 代码压缩的UglifyJsPlugin(Webpack 4)或TerserPlugin(Webpack 5)
    new TerserPlugin(),
    ],
    };

    常用Loader:

    • Babel Loader:用于将ES6及以上版本的JavaScript代码转换为向后兼容的JavaScript代码。
    • CSS Loader:将CSS文件作为模块导入,支持CSS Modules和CSS源码转换。
    • Style LoaderMiniCssExtractPlugin:前者将CSS内联到JavaScript中,后者则将CSS抽取到单独的CSS文件中。
    • File LoaderUrl Loader:处理项目中的图片、字体等文件,将其转化为base64编码或输出到指定目录。
    • TS Loader:处理TypeScript文件,将其转换为JavaScript。

    常用Plugin:

    • HtmlWebpackPlugin:根据模板生成HTML文件,并自动注入编译产出的JS/CSS资源。

    • MiniCssExtractPlugin:从JS文件中提取CSS代码,生成单独的CSS文件。

    • CopyWebpackPlugin:复制项目中的静态资源到输出目录。

    • CleanWebpackPlugin:在每次构建前清除输出目录。

    • DefinePlugin:在编译阶段替换全局变量,常用于设置环境变量。

    • UglifyJsPlugin(Webpack 4)或 TerserPlugin(Webpack 5):用于压缩JavaScript代码,优化文件大小。

      根据项目需求,还可以添加更多Loader和Plugin,例如SourceMap相关的插件、热更新插件(HotModuleReplacementPlugin)等。

    异步编程:

    请解释Promise、async/await的区别与联系,并写出对应的错误处理方式。

    Promise、async/await都是JavaScript中用于处理异步操作的重要工具,它们之间存在紧密的联系和区别。

    Promise

    Promise是JavaScript中用于处理异步操作的对象。它有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。一旦Promise的状态从pending变为fulfilled或者rejected,就不会再改变。Promise的主要方法是.then()(用于指定fulfilled时的回调函数)和.catch()(用于指定rejected时的回调函数)。

    Async/Await

    Async/Await是基于Promise的语法糖,使得异步代码看起来像同步代码,易于理解和编写。async函数总是返回一个Promise对象,await只能用在async函数内部,await后面跟的是一个Promise对象,返回的是Promise对象的结果。

    区别

    语法:Promise的语法较为繁琐,需要通过.then()和.catch()来处理异步操作的结果和错误。而async/await的语法更接近于同步代码,更易于理解和编写。
    错误处理:在Promise中,错误需要通过.catch()方法捕获。而在async/await中,错误可以直接通过try/catch捕获。
    联系

    基于Promise:async/await实际上是基于Promise实现的,async函数返回的是一个Promise对象。
    目的相同:无论是Promise还是async/await,它们的主要目的都是为了处理JavaScript中的异步操作,使得代码更易于管理和理解。
    错误处理方式

    Promise的错误处理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let promise = new Promise((resolve, reject) => {  
    // 模拟异步操作
    setTimeout(() => {
    reject(new Error('Promise failed'));
    }, 1000);
    });

    promise
    .then(result => {
    console.log(result);
    })
    .catch(error => {
    console.error('Promise error:', error);
    });

    Async/Await的错误处理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    async function asyncFunction() {  
    try {
    let result = await new Promise((resolve, reject) => {
    // 模拟异步操作
    setTimeout(() => {
    reject(new Error('Async/Await failed'));
    }, 1000);
    });
    console.log(result);
    } catch (error) {
    console.error('Async/Await error:', error);
    }
    }
    asyncFunction();

    在以上两个例子中,无论是使用Promise还是async/await,当异步操作失败时,都会通过.catch()方法或try/catch块捕获并处理错误。

    前端性能优化

    • 代码优化
      • 压缩和合并css和js等资源文件,使用uglifyjs, Terser
      • 避免全局变量,减少查找全局过程消耗时间,补全文件引用路径,减少查询消耗
      • 利用冒泡给父元素绑定事件代理子元素事件处理,事件委托减少事件注册绑定数量,减少内存消耗
      • 优化css选择器避免过于复杂的选择器查询影响性能
      • 更新体积更小的新版本,比如xlsx新版本比旧版本体积小,且支持按需引入import {utils} from 'xlsx,可以tree-shaking;
      • 尽量减少第三方库的使用,比如时间格式化库,手写实现,减少代码体积
      • 小数据量的请求合并到其他接口,减少首屏请求数量
      • 页面响应慢可以添加骨架屏或者loading
    • 图片优化
      • 压缩图片资源,比如使用tinypng工具压缩图片
      • 选用合适的图片格式,比如jpeg, png, webp
        • jpeg:有损压缩格式,高压缩率 优点:保持高质量图片同时文件较小 缺点:压缩损失图片质量
        • png:无损压缩格式 优点:保留原始图像的质量,支持透明通道, 缺点:文件大
        • webp:具有较高的压缩率和图像质量,支持透明通道和动画,缺点:老版本浏览器不支持此格式
      • 使用srcset和sizes属性针对不同屏幕尺寸引入不同图片大小的资源
    • 网络请求优化
      • 减少http请求:合并css和js资源,使用css scriptes和base64图片
      • 使用cdn部署静态资源,利用cdn和缓存和分布式特性
      • 浏览器缓存:设置http header(Cache-control, Etag, Last-Modified)缓存资源减少http请求
    • 渲染优化
      • 减少dom元素布局变动和style变动,减少dom重排和重绘,使用DocumentFragment
      • 使用虚拟列表:大量数据列表使用虚拟列表,只渲染当前视窗的数据,随着滚动,销毁不在视窗的元素并创建进入视窗的元素
      • 对于复杂计算和处理逻辑,使用Web Worker线程处理,避免阻塞主线程
      • 使用css3硬件加速动画性能,比如will-change, transform, requestAnimationFrame(浏览器在下一次重绘之前调用指定的函数来更新动画)
    • 代码拆分和加载
      • 使用webpack将文件拆分成chunk,在首屏按需加载模块
      • 动态import()实现路由懒加载
      • 使用preload和prefetch对关键性资源和后续可能访问资源进行提前加载
      • 异步加载js避免阻塞主线程,defer, async
    • 服务器SSR渲染:提高首屏加载速度,更好的SEO
    • 对于静态页面使用SSG渲染:在构建过程就生成了页面

    杭萧crm通过客户管理,项目管理,办公功能以及数据统计与分析能力,为用户提供了一个全面、高效的工作平台。
    包含工作台,排行榜,待办,我的四个核心板块。
    工作台作为首界面展示了各项业务的核心数据,包含不同项目阶段的统计以及对应的财务指标;通过工作指示、日总结、任务简报等功能模块确保员工掌握自身工作进度和目标;员工分布、员工在线统计等功能配合团队业绩个人业绩使管理者更清晰掌握员工和业务的状态。

    排行榜:结合规章制度按月度,季度,年度等对全集团,分公司,部门以及个人业绩进行多项关键考核指标排名,促进良性竞争,掌控业务进度。

    待办包含全业务流程审批,加强内部协作。

    我的板块包含工作总结,打卡记录等自我管理模块,以及客户模块和通讯录,还详细划分项目各阶段入口,保证用户掌控项目全过程,包括从跟进,有效,投标,中标,合同签订到收款的过程,形成了一套完整的项目管理和客户关系管理的体系。

    组件封装之messageBox

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <template>
    <button @click="showMessageBox('是否确认删除?', {
    confirm: () => { // todo something },
    cancel: () => { // todo something }
    })">删除</button>
    </template>
    <script>
    import { showMessageBox } from './showMessageBox.js'
    export default {
    methods: { showMessageBox } // 或者全局
    }
    </script>

    组件封装思路:用createVnode或者jsx编写组件结构—–用render方法渲染在一个div里——用appendChild方法添加到页面.

    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
    33
    34
    35
    36
    37
    38
    39
    40
    // messageBox.js

    import { createVnode , render } from 'vue'
    export function showMessageBox(content, handler) {
    // js: vnode
    // let messageBox = createVnode('div', {
    // class: 'divcover',
    // [
    // createVnode('div', {
    // class: 'messageContent'
    // }, content)
    // ]
    // })

    // jsx:项目需支持
    let div = document.createElement('div')
    let messageBox = <div class="cover">
    <div class="content">
    <div>{ content }</div>
    <div>
    <button onClick={() => {
    // 移除
    document.body.removeChild(div)
    console.log('取消')
    handler.cancel && handler.cancel()
    }}>取消</button>
    <button onClick={() => {
    // 移除
    document.body.removeChild(div)
    console.log('确定')
    handler.confirm && handler.confirm()
    }}>确定</button>
    </div>
    </div>
    </div>

    // 使用vue创建组件messageBox,用createVnode(messageBox)生成vnode
    render(messageBox, div)
    document.body.appendChild(div)
    }

    怎样排查页面加载慢?

    • 资源加载慢

      • 浏览器开发者工具-性能面板录制-查找netweork,查看哪些文件大,加载慢,当前页面不需要或者点击才需要的资源使用异步加载;按需加载页面资源;webpack工具打包压缩资源;开启gzip数据压缩
    • js执行慢

      • 若js执行慢,则使用console.time或者perfomance.now检测哪段同步代码执行慢
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // console.time()

      let aT = performance.now()
      for(let i=0; i< 10000; i++) {
      let div = document.createElement('div')
      div.innerHTML = i
      }
      // console.timeEnd()
      let bT = performance.now()

      console.log(bT - aT)

      或者在网站index.html加入js代码将window.performance传给接口进行分析用户网站性能

      1
      2
      3
      4
      5
      6
      7
      $.ajax({
      url: 'api/',
      data: window.performance
      })
      ``

      其他分析工具: 开发者工具lighthouse, js库web-vitals
    • 页面绘制慢

      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
      33
      34
      35
      36
      37
      ### 项目难点亮点

      + 移动端
      + 多屏幕适配:利用rem或者px转vw(webpack集成),媒体查询或js解决;
      + 高还原设计稿要求
      + B端公司后台
      + 大文件上传、断点续传
      + 可视化
      + echarts图表,高度定制地图、流程图等
      + websocket:实时消息提醒,在线多人协作
      + webGL:3d模型
      + canvas

      工作亮点:

      + 前端规范定制,并落实到工程化工具:git提交自动eslint代码检查;git提交信息格式检查

      husky+lint-staged:校验eslint

      ```shell
      npm install husky lint-staged --save-dev
      npx husky install // 使git hooks生效

      // 配置package.json
      {
      "husky": {
      "hooks": {
      "pre-commit": "lint-staged"
      }
      },
      "lint-staged": {
      "*.{js,vue}": "npm run lint" // script中配置lint命令
      }
      }

      // 设置precommit运行为lint-staged
      npx husky add .husky/pre-commit "npx lint-staged"

      git提交格式校验:需要安装commitlintrc,并配置commitlintrc.js文件定义你的 commit message 规则

      1
      2
      3
      4
      5
      6
      7
      {  
      "husky": {
      "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
      }
      }
      }
    • 封装组件:pc端一般组件二次封装(列表组件:查询+表格+分页+操作按钮:添加/导出等;添加/编辑表单弹窗组件:弹窗+表单;)

    js对象的原始值转换

    Symbol.toPrimitive是一种特殊的Symbol值,它可以作为对象的属性键,用于定义对象在被转换为原始值时的行为。当一个对象被转换为原始值时,JavaScript引擎会尝试调用对象上的Symbol.toPrimitive方法来确定转换的结果。比如对象{[Symbol.toPrimitive]: () => 1}转换成原始值就是1。

    需要注意的是,Symbol.toPrimitive必须为函数,不然会报错.

    将对象转换成数字:

    将对象转换成数字时,首先会调用Symbol.toPrimitive方法,如果Symbol.toPrimitive不存在或者返回的不是js原始值(以下省略原始值这一条规则),则会调用valueOf方法,如果valueOf不存在,则会调用toString方法,如果toString也不存在,转换就会报错:TypeError: Cannot convert object to primitive value,意思是无法将对象转换成原始值。
    总的来说,调用顺序是:Symbol.toPrimitive -> valueOf -> toString。

    对象的Symbol.toStringTag属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]或[object Array]中object后面的那个字符串。

    1
    2
    3
    4
    5
    let obj = {
    [Symbol.toStringTag]: 'abc' //
    }
    obj.toString() // '[object abc]'
    String(obj) // '[object abc]'
    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    // js中可以使用+号将其他类型转换成number,和Number()的作用一样,为了表达式的简洁,以下将使用+代替Number()

    // 0
    +{[Symbol.toPrimitive]: () => 0}

    // 1
    +{valueOf: () => 1}

    // 2
    +{toString: () => 2} // 没错,toString方法可以返回number、boolean乃至其他类型

    // 0
    // 优先调用Symbol.toPrimitive,所以返回0
    +{
    [Symbol.toPrimitive]: () => 0,
    valueOf: () => 1,
    toString: () => 2,
    }

    // 0
    // 如果返回的原始值不是`number`类型,则会再次进行转换;
    // {[Symbol.toPrimitive]: () => "0"}转换成原始值为字符串"0";
    // 接着再将这个字符串"0"转换成数字0。
    +{[Symbol.toPrimitive]: () => "0"}

    // NaN
    // 空对象中不存在Symbol.toPrimitive方法,会调用valueOf方法;
    // 对象的valueOf默认会返回自身,也就是说没有返回原始值,继续调用toString方法;
    // 对象的toString方法默认会返回"[object " + 对象.constructor.name + "]",在这里将被转换成"[object Object]";
    // 由于"[object Object]"属于原始类型,则js将其转换成number类型,当然它一眼看上去就不是个数字,只能转换成了NaN。
    +{}

    // 报错 TypeError: Cannot convert object to primitive value
    // Object.create(null)创建的对象没有原型链,也就是没有valueOf和toString更没有Symbol.toPrimitive,所以只能转换失败了
    +Object.create(null)

    // 666
    // parseInt和parseFloat如果传入对象,会先将对象转换成字符串,可以参考“将对象转换成字符串”部分内容
    parseInt({
    [Symbol.toPrimitive]: () => "666",
    })

    // 666
    // Math对象中的方法如果传入了对象,先会将对象转换成number,然后才进行计算
    Math.floor({
    [Symbol.toPrimitive]: () => 666.6,
    })

    // 0
    // null对象比较特殊,转换成number是0
    +null

    // NaN
    // undefined对象比较特殊,转换成number是NaN
    +void 0

    将对象转换成字符串:

    一般情况下,我们会使用String()或者xx.toString()将对象转换为字符串,但是他们是有些区别的。String()方法会优先尝试调用对象中的Symbol.toPrimitive方法;如果Symbol.toPrimitive不存在,则会尝试调用对象中的toString方法。
    用String()转换对象成字符串的顺序为:Symbol.toPrimitive -> toString,是的,不会调用valueOf。

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    // "Hello, Symbol.toPrimitive!"
    String({[Symbol.toPrimitive]: () => "Hello, Symbol.toPrimitive!"})

    // "Hello, toString!"
    String({toString: () => "Hello, toString!"})

    // "Hello, Symbol.toPrimitive!"
    String({
    [Symbol.toPrimitive]: () => "Hello, Symbol.toPrimitive!",
    toString: () => "Hello, toString!",
    })

    // "[object Object]"
    // 空对象中不存在Symbol.toPrimitive方法,会调用toString方法;
    // 对象的toString方法默认会返回"[object " + 对象.constructor.name + "]",在这里将被转换成"[object Object]";
    String({})

    // "[object Object]"
    // 由于对象转换成字符串时不会调用valueOf,所以会调用默认的toString方法,可以参考String({})
    String({
    valueOf: () => "Hello, valueOf!",
    })

    // "Hello, Symbol.toPrimitive!"
    // 使用模板字符串转换对象时,规则与String()相同,优先使用Symbol.toPrimitive
    `${{[Symbol.toPrimitive]: () => "Hello, Symbol.toPrimitive!", toString: () => "Hello, toString!"}}`

    // 报错 TypeError: Cannot convert object to primitive value
    // Object.create(null)创建的对象没有原型链,也就是没有toString更没有Symbol.toPrimitive,所以只能转换失败了
    // 顺便一提,`${Object.create(null)}`也会报这个错
    String(Object.create(null))

    // "{}"
    // JSON.stringify会忽略对象中的方法,不受原始值转换规则的约束,所以这里的值为"{}"
    JSON.stringify({
    [Symbol.toPrimitive]: () => {
    return "{a:1}";
    },
    })

    // "null"
    String(null)

    // "undefined"
    // undefined不属于对象,放在这里只是为了方便对比
    // void 0实际上就是undefined
    String(void 0)

    将对象转换成布尔值:

    对象转换成布尔值的规则比较特殊,不论对象里面是否有Symbol.toPrimitive、valueOf或者toString,都为true

    将对象转换成大整数(BigInt):

    对象转换成BigInt的规则和转换成number的规则类似都是按照Symbol.toPrimitive -> valueOf -> toString的顺序

    1
    2
    3
    4
    5
    6
    // 111n
    BigInt({
    [Symbol.toPrimitive]: () => "111",
    valueOf: () => "222",
    toString: () => "333",
    })

    拖拽元素

    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
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>拖拽</title>
    <style>
    .list {
    margin-top: 2rem;
    }
    .item {
    width: 300px;
    height: 60px;
    line-height: 60px;
    background-color: green;
    color: white;
    border-radius: 10px;
    padding-left: 10px;
    margin-bottom: 10px;
    }
    .item.moving {
    background-color: transparent;
    border: 1px solid #eee;
    }
    </style>
    </head>
    <body>
    <div class="list">
    <div draggable="true" class="item">1</div>
    <div draggable="true" class="item">2</div>
    <div draggable="true" class="item">3</div>
    <div draggable="true" class="item">4</div>
    <div draggable="true" class="item">5</div>
    <div draggable="true" class="item">6</div>
    </div>
    <script>
    // 获取列表元素
    const list = document.querySelector('.list');
    let sourceNode = null
    // 拖拽开始
    list.ondragstart = (e) => {
    // 在ondragstart中添加样式
    setTimeout(() => {
    e.target.classList.add('moving');
    }, 0);
    // 记录拖拽开始时的元素
    sourceNode = e.target
    }
    // 拖拽进入
    list.ondragenter = (e) => {
    // 阻止默认行为
    e.preventDefault();
    // 如果目标元素是列表或者拖拽开始时的元素,则直接返回
    if (e.target === list || e.target === sourceNode) return;
    console.log('打印', e.target);
    // 获取列表中子元素的索引
    const children = [...list.children];
    const sourceIndex = children.indexOf(sourceNode);
    const targetIndex = children.indexOf(e.target);
    // 如果源元素索引小于目标元素索引,则目标元素向下移
    if (sourceIndex < targetIndex) {
    console.log('打印', '下移')
    list.insertBefore(sourceNode, e.target.nextElementSibling);
    // 否则,目标元素向上移
    } else {
    console.log('打印', '上移')
    list.insertBefore(sourceNode, e.target);
    }
    }
    // 拖拽移动
    list.ondragover = (e) => {
    // 阻止默认行为
    e.preventDefault();
    }
    // 拖拽结束
    list.ondragend = (e) => {
    // 移除样式
    e.target.classList.remove('moving');
    }
    </script>
    </body>
    </html>

    大屏数据如何实现实时更新?

    • websocket
    • Server-Sent Events(SSE,服务器发送事件)是一种简单的技术,允许服务器通过HTTP连接向客户端推送实时更新。与WebSocket相比,SSE是一种单向通信机制,只允许服务器向客户端发送消息。
    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
    let dataList;
    // 创建一个新的EventSource对象,连接到http://localhost:8080/getList这个地址
    const source = new EventSource('http://localhost:8080/getList')

    // EventSource: 0: connecting 1:open 2: closed

    // 监听source对象打开连接的事件
    source.onopen = (event) => {
    console.log('onopen---', event)
    }
    // 监听source对象接收到消息的事件
    source.onmessage = (event) => {
    console.log('onmessage---', event)
    // 将收到的消息转换为JSON格式,赋值给dataList
    dataList = JSON.parse(event.data)
    console.log('dataList', dataList)
    }
    // 监听source对象发生错误的事件
    source.onerror = (event) => {
    console.log('onerror---', event)
    }
    // 监听source对象关闭连接的事件
    source.onclose = (event) => {
    console.log('onclose---', event)
    }

    函数参数和默认值

    函数参数默认值,传参为undefined才取默认值。
    函数参数和arguments是一一对应的,比如fn(a,b) fn(1,2)。arguments[0]等于a,在函数中,修改a的值,arguments[0]也跟着改变,除非使用严格模式。
    但是如果函数参数设置了默认值,修改a后,arguments[0]还是最初传参的值,不会跟随a改变。

    函数如果设置了默认值,形参的长度计算到默认值前面,即fn(a,b=2,c),fn.length参数长度为1,fn(a,b,c=3),fn.length为2.

    函数参数默认值即可以设置字面量也可以设置表达式,表达式的值只有在调用的时候才计算,即参数为undefined的时候才去计算表达式取默认值。

    函数参数的暂时性死区,fn(a,b=getvalue(a))可以,但是fn(a=getvalue(b), b)会报错,let定义变量在声明前不可使用,暂时性死区.

    css属性值的计算过程

    css属性值的计算过程computed style:

    • 1、确定声明值(找到作者和默认样式表没有冲突的属性直接确定下来属性值)
    • 2、层叠(比较重要性,比较权重,比较源次序)
    • 3、继承(对仍然没有值的属性若可以继承则使用继承,有些属性不能继承)
    • 4、使用默认属性(对仍然没有值的属性直接使用默认值)。从每一个元素css属性没有值到所有css属性都有值就是计算过程。

    类型转换规则

    1、原始转数字

    • true: 1
    • false: 0
    • null: 0
    • undefined: NaN
    • string:
      • 空字符串(含空白字符,\n\r\t等):0
      • 非空字符串,去掉引号, 不是数字就是NaN

    2、所有转bool

    • null:false
    • undefined: false
    • number:
      • 0: false
      • NaN: false
      • 其他: true
    • string
      • 空字符串:false
      • 其他: true
    • 对象/数组:true

    3、原始转字符串

    • null: “null”
    • undefined: “undefined”
    • number: “数字”
    • bool:true-“true” false-“false”

    4、对象转原始
    先调用valueOf()—-得到的是对象?重新调用toString()—-得到的还是对象?报错
    []转原始,[].toString()结果为’’ {}转原始为’[object object]’

    js运算规则

    1、算术运算
    + - * / % ++ --
    转换为原始类型:

    • 转换为数字,然后运算
    • 特殊情况:x+y,x和y有一个是字符串,转换为字符串进行拼接
    • 特殊情况:NaN和任何类型运算得到的还是NaN

    举例:
    null + undefined 转换为数字,0 + NaN ,结果为NaN
    [] + {} :先转换为原始值:’’ + ‘[object object]’, 结果为’[object object]’
    2、比较运算

    < >= <= == != === !===

    • 当运算符为> < >= <=:
      • 转换为原始类型:
        • 转换为数字,进行比较
        • 特殊情况:两端全是字符串,比较字典的顺序
        • 特殊情况:两端存在NaN,一定是false
      • 举例:null > undefined —转为数字: 0 > NaN, 结果为false
      • 举例:null < undefined —转为数字: 0 < NaN, 结果为false
    • 当运算符为==:
      • 两端类型相同,比较值
      • 两端都是原始类型,转换为数字比较
      • 一端是原始类型一端是对象类型,将对象转为原始类型后进行比较
      • 特殊情况,null和undefined只有与自身比较或者相互比较才为true null == null undefined == undefined null == undefined null == 0为false
      • 特殊情况:两端存在NaN,一定是false
    • 当运算符为===:
      • 类型和值必须都相等才为true
      • 特殊情况:两端存在NaN,一定是false
    • 当运算符为!= !==:对相等和全等取反
      3、逻辑运算
      ! && || ?:
      转换为boolean:
    • x&&y: x:false,返回x,x为true,返回y
    • x||y: x:true,返回x,x:false,返回y
      返回最后一次运算