在前端开发过程中,常常遇到各种各样的问题和坑点。尤其是随着技术的不断发展和更新,新的问题也不断涌现。对于初学者而言,这些问题往往让人感到十分困惑和无助。因此,本文将旨在探讨一些前端开发过程中常见的问题和坑点以及解决方法,帮助读者更加深入地了解前端开发,并解决实际工作中遇到的问题。
数据类型
数字
1. 进制转化问题:
/**
* 为什么 010 会是 8
*/
const num1 = 09 // 9
const num2 = 010 // 8
这边是因为 0 开始的数字js会尝试先把它转成八进制的数字。如果你出现大于 8 的数字,他知道不是八进制还给你转十进制。纯粹的八进制应该用 0o ,类似的还有 0b 二进制和 0x 十六进制,但是他们写的不符合转换条件的话会直接报错。
2. 精度丢失问题:
0.1+0.2 // 0.30000000000000004
2.55.toFixed(1) // '2.5'
2.45.toFixed(1) // '2.5'
2.toFixed(1) // Uncaught SyntaxError: Invalid or unexpected token
2..toFixed(1) // '2.0'
js 计算有精度问题呢?大家一定都是知道的, 今天就是来简单解释一下为什么会出现丢失精度的问题。这边其实分两部分,存储和展示。存储的时候 JavaScript 是以 64 位二进制补码的方式来存储。由于改方式是以 2 为底进行表示的,所以执行某些运算时容易出现误差、溢出、无限循环等问题。
图片
我们可以发现本来应该是 11001100 无限循环被截断了,尾号 11001 的时候1被舍去了,然后进了一位 最后存储成了上图的 1101 的样子。所以 0.1 其实存的比 0.1 要大一点点,0.2 也是一样,而 0.3 比实际小一些。所以计算 0.1+0.2 的时候其实是拿二进制计算的,两个都偏大的数字相加 误差被近一步的放大了。
下面这张图可以看到他们真实存下来的数据转成十进制的样子。实际显示的时候会做近似处理,js 会判断一个数字特别像 0.1 它就显示 0.1 了。
图片
toFixed 问题也是一样。
图片
还有就是有时候我们对一个数字使用 .toFixed .toString 会报错。
0.toString() // Uncaught SyntaxError: Invalid or unexpected token
// 我们期待的是它会隐性转换让我们调用 Number 构造函数上的方法,
// 但是程序会以为你在写一个小数,小数还不合规,所以报错了,
// 解决方法就是拿变量装一下,或者 0..toString()
在 JavaScript 中,采用 64 位二进制补码表示数值类型,即双精度浮点数。符号位(S)、指数位(E)和尾数位(M)的比特数分别为 1 位、11 位和 52 位。在使用 IEEE 754 标准表示双精度浮点数时,使用一些特殊的位表示:其中一个隐含位表示数字 1,在正常项中省略,因此一共有 53 位表示有效数字。
- 符号位:在数值类型的二进制补码表示中,第一位表示符号位,0 表示正号,1 表示负号。
- 指数位:在数值类型的二进制补码表示中,指数位用来表示科学计数法的指数部分。在双精度浮点数中,指数部分使用11个位表示,其中 10 个位表示二进制整数,在运算前需要减去 2^n 的形式,剩下一位表示符号,1表示负指数,0表示正指数。可表示 -1023 ~ 1024 之间的范围。
- 尾数位:尾数位用来表示实数的小数部分。在双精度浮点数中,尾数部分使用 52 个位表示。这意味着 JavaScript 浮点数的精度是有限的,并且可能会发生舍入误差。
长度问题:
function fn(a,b,c){
return a+b+c
}
fn.length // 3 一般来说fn的长度是形参的个数 但是形参有默认值就不同
function fn1(a = 1,b,c){
return a+b+c
}
fn1.length // 0
function fn2(a,b=1,c){
return a+b+c
}
fn2.length //1
// 它只会统计首个默认之前的参数
对象排序问题:
a.b=1
a.a=1
a.c=1
a[2]=2
a[12]=2
a[1]=2
// 结果 {1: 2, 2: 2, 12: 2, b: 1, a: 1, c: 1}
// 对象的内部key value的存储顺序是这样的
// 如果属性可以转number,提前上来,按升序排列,其他的字符串属性按添加的先后顺序
赋值中断问题:js 里没有事务的机制,不会恢复到操作之前的状态。如果中途失败了,之前赋值和操作过的数据是保留的,失败后的操作不执行。
异步
定时器不准:这里说的不准还不是说一点小误差。定时器由于渲染主进程阻塞也好,延时任务嵌套过深也好,事件循环优先级被排队到后边也好。这些都可以认为是“误差”,但是如果说你 setIntervel 是 10ms,结果它间隔 n 秒调一次函数,那可不是误差了,可能直接会产生 bug。
这个问题的原因是:用户在使用谷歌浏览器的过程中将窗口最小化或切换到其他应用程序中去,浏览器会将当前标签页和其中的 JavaScript 定时器挂起,这将导致定时器延迟调用。通常情况下,浏览器会尽可能保持定时器的准确性,并在恢复标签页后立即执行延迟的定时器。但是,如果计算机负载过重或其他原因导致 JavaScript 的执行速度变慢,定时器可能会更加延迟。经过测试,新版本的浏览器上基本都是至少 1 秒一次。
图片
详细参考 https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout
竞态问题:异步的竞态问题也是开发中经常遇到的问题。举个例子用户输入搜索就请求相应的商品,用户很快的输入了“手机壳” 3 个字;“手”“手机”“手机壳” 3 个不同参数的请求几乎同时发了出去,异步请求很难保证哪个请求,先回来哪个请求后回来。那加上防抖呢?其实这也不是防抖该应用的场景,弱网环境下请求 5 秒、10 秒返回都说不准,防抖防几秒都不太合适。我根据个人的经验总结了 3 种方式:
1. 链式调用
// 让要执行的异步函数通过一个链式方式调用
export class SequenceQueue {
promise = Promise.resolve();
excute (promise) {
this.promise = this.promise.then(() => promise);
return this.promise;
}
};
2. 设置一个叠加器,每次调用就累加,回调函数内就可以知道当前是不是“最新”。antd 里有一段例子如下
const fetchRef = useRef(0);
const debounceFetcher = useMemo(() => {
const loadOptions = (value: string) => {
fetchRef.current += 1;
const fetchId = fetchRef.current;
setOptions([]);
setFetching(true);
fetchOptions(value).then((newOptions) => {
if (fetchId !== fetchRef.current) {
// for fetch callback order
return;
}
setOptions(newOptions);
setFetching(false);
});
};
return debounce(loadOptions, debounceTimeout);
}, [fetchOptions, debounceTimeout]);
可以进一步封装,将请求封装成 request( url, [option], [queueName] ), 通过外部传入来指定需要竞态的映射名。也就是将上述的叠加器放在一个 Map 里,使用 queueName 做 Map 的 key。
“
如果作为通用的请求中间件封装,处于内存优化考虑,此处可以将 Map 优化成 weakMap。Map 键值对是强引用,如有一个键被引用,那么GC是无法回收对应的值的,weakmap 不存在这样的问题,但要注意 weakMap 只能使用对象做 key。
3. 新请求发出的时候取消老的请求。一般来说请求发出去了是追不回来的。但是 fetch 和原生 ajax 提供了 abort 之类的取消方法。如果你项目的请求是 fetch 或 XMLHttpRequest 就可以用他们自带的方式取消。需要注意的是,如果请求已经被发送到服务器,并且请求体数据已被上传,那么 abort() 方法就无法中止请求。大多数情况项目用的可能是 axios、uni.request 等其他更热门的请求库,这时候我们可以利用 promise.race 来封装一个可以取消的请求,传一个自定义能带取消方法的 promise 进 promise.race 来控制 真正要执行的 promise 函数提前取消。
// 封装
function cancelableRequest(requestPromise) {
const cancelToken = {};
const cancelablePromise = new Promise((resolve, reject) => {
cancelToken.cancel = () => {
reject(new Error('Request was canceled'));
};
Promise.race([requestPromise, cancelToken])
.then(resolve)
.catch(reject);
});
return { promise: cancelablePromise, cancel: cancelToken.cancel };
}
// 使用
const mockApi= () =>
new Promise(resolve => {
setTimeout(() => {
resolve([{ title: 'Post 1' }, { title: 'Post 2' }, { title: 'Post 3' }]);
}, 3000);
});
const { promise, cancel } = cancelableRequest(mockApi());
promise
.then(posts => console.log(posts))
.catch(error => console.error(error.message));
// 取消请求
cancel();
样式
定位:一般来说写 position: fixed 都是想相对窗口定位实现一些弹窗、抽屉或者浮动组件等效果,但是如果父元素中存在 transform 属性的话,固定效果将直接降级变成 position: absolute 的表现。这可能也是大多数UI库选择将 modal、drawer 之类的 fixd 元素都插入在 body 下,和应用本身分离开,可能就是担心有 transform 来影响定位。究其原因是因为包含块的定义:
如果 position 属性为 static、relative 或 sticky,包含块可能由它的最近的祖先块元素(比如说 inline-block, block 或 list-item 元素)的内容区的边缘组成,也可能会建立格式化上下文 (比如说 a table container, flex container, grid container, 或者是 the block container 自身)。
- 如果 position 属性为 absolute ,包含块就是由它的最近的 position 的值不是 static (也就是值为 fixed, absolute, relative 或 sticky )的祖先元素的内边距区的边缘组成。
- 如果 position 属性是 fixed,在连续媒体的情况下 (continuous media) 包含块是 viewport ,在分页媒体 (paged media) 下的情况下包含块是分页区域 (page area)。
- 如果 position 属性是 absolute 或 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:
- transform 或 perspective 的值不是 none
- will-change 的值是 transform 或 perspective
- filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效)。
- contain 的值是 paint(例如:contain: paint;)
- backdrop-filter 的值不是 none(例如:backdrop-filter: blur (10px) ; )
层叠计算:有的时候,如果引入了很多的库,会发现样式会偶发的发生错误。这是因为样式冲突了,那样式的优先级是什么样子的呢?css 全称为 cascader style sheet, 层叠样式表。其层叠的目的就是为了比对样式冲突后的“胜出者”;mdn 里详细的介绍了其比较计算的规则。https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Cascade_layers
比对分以下步骤进行
- 相关性: 为每个元素查找具有选择器匹配的声明块。
- 重要性: 根据它们是正常的还是重要的对规则进行排序。重要的样式是那些设置了 !important 标志的样式。
- 来源: 在上面两个重要性比较中,按作者、用户或用户代理来源对规则进行排序。
- 层级: 在每个六个来源重要性桶中,通过级联层进行排序。正常声明的层次顺序是从第一层创建到最后一层,然后是未分层的正常样式。重要样式的顺序是相反的,未分层的重要样式优先级较低。
- 特定性: 对于来源层的竞争样式,根据特定性对声明进行排序。
- 出现顺序: 当具有优先级的来源层中的两个选择器具有相同的特定性时,具有最高特定性的最后声明的选择器的属性值获胜。通俗的解释一下:规则自上往下去执行,一旦找到了胜出者就不必向下执行了。
- 看选择器选没选中东西,有选中的“胜出”
- 多人胜出,看有没有 !important,有的话胜出
- 还是分不出唯一的胜者的话,继续看来源,按作者、用户或用户代理来源的优先级,其中【作者】就是代码的你,【用户】就是站点用户,【用户代理】就是浏览器默认样式
- ( 相关性+重要性 )*来源 有 6 种情况,这些情况要按分层进行排序。先创建层级的比后创建的高,分层的比未分层优先级高,对于重要样式,这个顺序则被倒转了。所有未进行图层分层的重要样式都会级联在一起,形成一个隐含的层,这个层优先级高于所有未进行过转换的普通样式,但是优先级低于任何分层的重要样式。下面是一张 apple 官网网页的分层图,我们可以清除的看见分层之间的关系
分层就是浏览器画制图画的顺序,浏览器会根据一定的规则划分图层,当然代码也能干预图层的划分比如定位、动画等等,不同图层直接不能相互影响,换句话说一个图层在另一个图层下面的话,尽管 z-index 是 0 也能覆盖 z-index: 100 的元素。
11. 特定性就是 4 位数字( 0,0,0,0 )第一位代表是否是行内样式,后面3位就是 id,class,tag 的个数。统计完特定性的 4 个数字后,从前往后比较大小。比较极端的情况下如果你写了足够多的 class 是可以超过 id 选择器的。例如,以下选择器有 11 个 class 选择器组成:
div.navbar ul ul.dropdown-menu li.active > a.btn-primary:hover span.icon {
/* styles */
}
// 虽然其中没有 id 选择器,但它显然比单个 id 选择器的优先级更高。
12. 全部比对完后还是没有“胜出者”的话,我们就会根据源代码书写的顺序,后来的覆盖先来的。言归正传,所以我们样式如果偶发的出现问题,可能是因为网络原因 javascript 下载下来的时间不确定,从而导致执行后插入 css 文件的顺序不一致,最终呈现出一种偶发的现象
性能
造成性能问题的原因是多种多样的,大体可以分为 3 种,一是网络,二是渲染,三是计算
- 网络优化的手段:
- 压缩和合并资源,减少请求次数(一定程度的节省请求自身的消耗,请求本身就有一些请求头、响应头等固定开销)
- 减小体积:按需打包加载,模块化的同构相同逻辑的代码
- 使用缓存:可以利用浏览器缓存机制,让用户再次访问页面时不必重新加载文件,从而加快页面的载入速度。
- 利用 CDN:可以使用内容分发网络( CDN )来分发资源,缩短用户请求的时延。
- DNS 优化:预解析一下网站内的域名 ip,首次访问的用户能更快的请求到资源
- 合理安排依赖资源的加载模式和加载顺序,例如将 JavaScript 代码放置在页面底部,避免影响页面的首次渲染时间
- 渲染优化的手段:
为了避免频繁的页面重排和重绘,我们应该尽量减少对 DOM 的访问和修改。为了控制元素的样式变化,应该尽可能使用 CSS。这样有助于提高页面性能和用户体验。
懒加载和预加载:懒加载可以减少初始页面渲染时间,当用户需要访问到某个资源时,才去加载这个资源,而预加载可以预先加载即将用到的资源,加快用户访问其他页面时的速度。
虚拟 DOM:使用 Virtual DOM,实现局部修改视图而不是整体重新渲染,减少 DOM 的操作。
优化 JavaScript 代码:采用 JavaScript 模块化、使用面试编程思想,减少页面的 JavaScript 代码,从而减少浏览器的工作量。
多线程:使用延时线程、网络线程、Web Workers 等其他不会阻塞渲染的进程来完成工作。
运行时计算的手段:
优化 JavaScript 代码:采用 JavaScript 模块化、使用面试编程思想,减少页面的 JavaScript 代码,从而减少浏览器的工作量。
算法优化:用更加合理的数据结构设计和算法,以更优的方式完成需求。
事件委托:将事件绑定在父元素上,减少事件的处理次数。
函数节流和防抖:使用函数节流技术避免频繁触发事件处理。
合理的事件注册和解绑
合理的释放不使用的内存
兼容性
一些想当然觉得应该是一致的东西结果不一致,比如前瞻匹配和后瞻匹配的兼容是不一样的。需要兼容IE的话就不能使用后瞻写法
图片
参考文献
https://developer.mozilla.org/zh-CN/docs/Web/CSS/Containing_block
https://caniuse.com/
https://standards.ieee.org/ieee/754/6210/