从谷歌一行代码学到的姿势
网上很流行的一行代码,据说是谷歌工程师写的,它的作用是给页面所有元素增加一个随机颜色的外边框。
[].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16)})
运行效果如下图:
这个代码虽然只有一行,但是包含的知识点不少,网上有很多解析。我也说下自己的理解,然后最后推荐在实务中使用TreeWalker对象进行遍历。
我的理解其中主要包含如下4个知识点:
1. [].forEach.call
2. $$("*")
3. a.style.outline
4. (~~(Math.random()*(1<<24))).toString(16)
1 [].forEach.call
1.1 [].forEach
forEach是数组遍历的一个方法,接收一个函数参数用来处理每一个遍历的元素,常规的使用姿势是:
let arr = [3, 5, 8];
arr.forEach((item) => {
console.log(item);
})
// 控制台输出:
// 3
// 5
// 8
那么下面的写法:
[].forEach
只是为了得到 forEach 这个方法,这个方法是定义都在Array.prototype上的方法,[] 表示空数组,可以访问到数组原型对象上的方法。
得到 forEach 这个方法后,就可以通过 call 发起调用。
1.2 call
call函数用来调用一个函数,和普通调用不同,call调用可以修改函数内this指向。
常规调用函数的姿势:
let object1 = {
id: 1,
printId() {
console.log(this.id)
}
}
object1.printId();
// 控制台输出:
// 1
因为是正常调用,方法内的this指向object1对象,所以上例输出1。
使用call调用printId方法,并传入另外一个对象object2:
let object2 = {
id: 2
}
object1.printId.call(object2);
// 控制台输出:
// 2
这里使用call调用object1.printId函数,传入了object2对象,那么printId函数内的this就是指向object2这个对象,所以结果输出2。
1.3 综合分析
综合来看:
[].forEach.call( $$("*"), function(a){} )
这行代码的意思就是遍历如下对象:
$$("*")
然后用如下方法处理每个元素:
function(a){}
其中,a就是遍历的的每一个元素。
那么
$$("*")
指什么呢?我们接着往后看。
2 $$("*")
这个写法用来获取页面所有元素,相当于
document.querySelectorAll('*')
只是
$$("*")
只能在浏览器开发控制台内使用,这个是浏览器开发控制台提供出来的预定义API,至于为什么,大家可以参考底部的参考文章。
3 a.style.outline
设置元素边框,估计很多人都知道,但是设置外边框就比较少人了解了,外边框的效果和边框类似,唯一不同的点是外边框盒子模型的算式,仅仅做装饰使用。
<style type="text/css">
#swiper {
width: 100px;
height: 100px;
outline: 10px solid;
}
style>
<div id="swiper">div>
运行效果:
div元素实际的宽高还是100 * 100,如果把outline改成border,那么div元素的实际宽高就是120 * 120,因为要加上border的宽度。
外边框设置的最大作用就是:
可以设置元素边框效果,但是不影响页面布局。
4 (~~(Math.random()*(1<<24))).toString(16)
这个代码从结果是得到一个16进制的颜色值,但是为什么能得到呢?
16进制的颜色值:81f262
4.1 Math.random()
这个容易理解,就是随机 [0, 1) 的小数。
4.2 1<<24
这个表示1左移24位,二进制表示如下所示:
1 0000 0000 0000 0000 0000 0000
十进制就是表示:
2^24
那么
Math.random() * (1<<24)
就会得到如下范围的一个随机浮点数:
[0, 2^24)
4.3 两次按位取反
因为Math.random()得到是一个小数,所以两次按位取反就是为了过滤掉小数部分,最后得到整数。
所以
(~~(Math.random()*(1<<24)))
就会得到如下范围的一个随机整数:
[0, 2^24)
4.4 转成字符串toString(16)
最后就是把上面得到的数字转成16进制,我们知道toString()是用来把相关的对象转成字符串的,它可以接收一个进制参数,转成不同的进制,默认是转成10进制。
对象.toString(2); // 转成2进制
对象.toString(8); // 转成8进制
对象.toString(10); // 转成10进制
对象.toString(16); // 转成16进制
上面的得到的随机整数用二进制表示就是:
0000 0000 0000 0000 0000 0000
到
1111 1111 1111 1111 1111 1111
那么2进制转成16进制,是不是就是每4位转一个?
最终是不是就得到一个6个长度的16进制数了?
这个字符串加上#是不是就是16进制的颜色值了?
形如:
#ac83ce
#b74384
等等...
实务应用
虽然上面的代码简短,并且知识含量也很高,但是在实务中如果要遍历元素,我并不建议使用这样的方式。
主要原因是两个:
1. $$("*") 只在开发控制台可以用,正常项目代码中不能用。
2. 选中所有元素再遍历,性能低。
如果实务中要遍历元素,建议是用 TreeWalker。querySelectorAll是一次性获取所有元素然后遍历,TreeWalker是迭代器的方式,性能上 TreeWalker 更优,另外 TreeWalker 还支持各种过滤。
参考如下示例:
// 实例化 TreeWalker 对象
let walker = document.createTreeWalker(
document.documentElement,
NodeFilter.SHOW_ELEMENT
);
// 遍历
let node = walker.nextNode();
while (node !== null) {
node.style.outline = "1px solid #" + (~~(Math.random() * (1 << 24))).toString(16);
node = walker.nextNode();
}
虽然代码更多,当时性能更好,并且支持各种过滤等,功能也更加强大。
如果大家有学到新姿势,麻烦帮忙点个赞,谢谢。欢迎大家留言讨论。
参考资料
JavaScript中的$$(*)代表什么和$选择器的由来:ourjs.com/detail/54ab…
querySelectorAll vs NodeIterator vs TreeWalker:stackoverflow.com/questions/6…
作者:晴空闲云
来源:https://juejin.cn/post/7034777643014684703
现在实现倒计时都这么卷了吗?
但是在校准时间的过程中,为了快速追赶落后的时间,时间跳动太快了,导致体验不太好,体感上感觉这时间不准呀,因此我再在那基础上再优化了一版
为求实现一版超准确!超平稳!性能极好!体验极佳的倒计时
旧版的功能实现代码
const totalDuration = 10 * 1000;
let requestRef = null;
let startTime;
let prevEndTime;
let prevTime;
let currentCount = totalDuration;
let endTime;
let timeDifferance = 0; // 每1s倒计时偏差值,单位ms
let interval = 1000;
let nextTime = interval;
setInterval(() => {
let n = 0;
while (n++ < 1000000000);
}, 0);
const animate = (timestamp) => {
if (prevTime !== undefined) {
const deltaTime = timestamp - prevTime;
if (deltaTime >= nextTime) {
prevTime = timestamp;
prevEndTime = endTime;
endTime = new Date().getTime();
currentCount = currentCount - 1000;
console.log("currentCount: ", currentCount / 1000);
timeDifferance = endTime - startTime - (totalDuration - currentCount);
console.log(timeDifferance);
nextTime = interval - timeDifferance;
// 慢太多了,就立刻执行下一个循环
if (nextTime < 0) {
nextTime = 0;
}
console.log(`执行下一次渲染的时间是:${nextTime}ms`);
if (currentCount <= 0) {
currentCount = 0;
cancelAnimationFrame(requestRef);
console.log(`累计偏差值: ${endTime - startTime - totalDuration}ms`);
return;
}
}
} else {
startTime = new Date().getTime();
prevTime = timestamp;
endTime = new Date().getTime();
}
requestRef = requestAnimationFrame(animate);
};
requestRef = requestAnimationFrame(animate);
然后有个细小的问题在于这段代码
// 慢太多了,就立刻执行下一个循环
if (nextTime < 0) {
nextTime = 0;
}
问题在于,假如遇到线程阻塞的情况,出现了倒计时落后情况严重,假设3s
,我这里设置下一个循环是0s
,然后现在倒计时当前15s
,就会看到快速倒计时到12s
,产品同学说你这倒计时还怎么加速了呀
这倒计时加速像极了职业生涯结束在加速倒计时一样,瑟瑟发抖的我立刻赶紧修复一下
其实很简单,就是把这个临近值0
设置接近每次循环的时间数即可,那么其实是看不出来每次是有在稍微加速/减速的,这里每次循环的时间数是1s
,那么我们可以将上面这段代码修改下,把以前立刻就追赶描述的操作,放缓一下追赶的脚步,以此优化用户体验
例如以前追赶2s
在3s~4s
内立刻追赶上,那么波动是很明显的,但是如果把2s
的落后秒数,平躺到接下来要倒计时的1min
里,每次大概追赶30ms
,那是看不出来滴
// 慢到一定临界点,比正常循环的时间数稍微慢点,再执行下一个循环
if (nextTime < 900) {
nextTime = 900;
}
这里我设置落后太多时,每秒追赶100ms
,假如落后2s
,20s
后就能追赶回来啦,而且看不出明显波动,时间又是被校验准确的,得到了产品同学的好评!
虽然修改很小,但是也是反复思考得到的~如果对时间要求比较严格,而且倒计时时间范围比较小,来不及把差距平摊到这么大的时间段,可建议让后端同学定时推送最新的倒计时给前端来校验时间准确性,这就万无一失啦
结语
以上是我使用requestAnimationFrame实现倒计时功能反复雕琢的心得,希望能对大家有帮助~如果能获得一个小小的赞作为鼓励会十分感激!!
作者:一只凤梨
链接:https://juejin.cn/post/7026735190634414087
收起阅读 »
中高级前端不一定了解的setTimeout | 网易实践小总结
setTimeout的创建和执行
我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。
首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了保证setTimeout能够在规定的时间内执行,setTimeout创建的任务不会被添加到消息队列里,与此同时,浏览器还维护了另外一个队列叫做延迟消息队列,该队列就是用来存放延迟任务,setTimeout创建的任务会被存放于此,同时它会被记住创建时间,延迟执行时间。
然后我们看下具体例子:
setTimeout(function showName() { console.log('showName') }, 1000)
setTimeout(function showName() { console.log('showName1') }, 1000)
console.log('martincai')
以上例子执行是这样:
- 1.从消息队列中取出宏任务进行执行(首次任务直接执行)
- 2.执行setTimeout,此时会创建一个延迟任务,延迟任务的回调函数是showName,发起时间是当前的时间,延迟时间是第二个参数1000ms,然后该延迟任务会被推入到延迟任务队列
- 3.执行console.log('martincai')代码
- 4.从延迟队列里去筛选所有已过期任务(当前时间 >= 发起时间 + 延迟时间),然后依次执行
所以我们可以看到showName和showName1同时执行,原因是两个延迟任务都已经过期
循环源码:
void MainTherad(){
for(;;){
// 执行消息队列中的任务
Task task = task_queue.takeTask();
ProcessTask(task);
// 执行延迟队列中的任务
ProcessDelayTask()
if(!keep_running) // 如果设置了退出标志,那么直接退出线程循环
break;
}
}
删除延迟任务
clearTimeout是windows下提供的原生方法,用于删除特定的setTimeout的延迟任务,我们在定义setTimeout的时候返回值就是当前任务的唯一id值,那么clearTimeout就会拿着id在延迟消息队列里查找对应的任务,将其踢出队列即可
setTimeout的几个注意点:
- setTimeout会受到消息队列里的宏任务的执行时间影响,上面我们可以看到延迟消息队列的任务会在消息队列的弹出的当前任务执行完之后再执行,所以当前任务的执行时间会阻碍到setTimeout的延迟任务的执行时间
function showName() {
setTimeout(function show() {
console.log('show')
}, 0)
for (let i = 0; i <= 5000; i++) {}
}
showName()
这里去执行一遍可以发现setTimeout并不是在0ms左右执行,中间会有明显的延迟,因为setTimeout在执行的时候首先会将任务放入到延迟消息队列里,等到showName执行完之后,才会去延迟队列里去查找已过期的任务,这里setTimeout任务会被showName耽误
- setTimeout嵌套下会有4ms的延迟
Chrome会把嵌套5层以上的setTimeout后当作阻塞方法,在第6次调用setTimeout的时候会自动将延时器更改为至少4ms的延迟时间
- 未激活的页面的setTimeout更改为至少1000ms
当前tab页面不在active状态的时候,setTimeout的延迟至少会被更改1000ms,这样做是为了减少性能消耗和电量消耗
- 延迟时间有最大值
目前Chrome、Firefox等主流浏览器都是用32bit去存储延时时间,所以最大值是2的31次方 - 1
setTimeout(() => {
console.log(1)
}, 2 ** 31)
以上代码会立即执行
作者:我在曾经眺望彼岸
链接:https://juejin.cn/post/7032091028609990692
收起阅读 »
你可以永远相信debugger,但是不能永远相信console.log
总结放前面:
console.log在打印引用数据类型的时候表现和我们的预期不相符合是因为console.log打印的是引用数据类型的一个快照,因为浏览器或者我们异步代码的原因在快照之后修改了对应的内存空间的值,所以等我们展开打印浏览器通过指针重新访问内存空间的时候会获得最新的值导致展开和不展开的表现不一致。
不知道各位大佬有没有遇到过这样的情况,我在代码里面console.log()了一个数组,然后打开浏览器控制台,看着是空的就像这样[],结果我点展开它里面又有值了,但是在代码打印的位置使用length或者获取数组里面的值都是不行的,🤯 就像下面这样:
let arr = [];
const setFun = () => {
return new Promise((reslove) => {
let arr1 = [1, 2, 3];
setTimeout(() => {
reslove(arr1);
}, 2000)
});
}
const getFun = async () => {
let result = await setFun();
result.forEach((item) => {
arr.push(item);
})
}
getFun();
console.log(arr);
或者说我在某处代码console.log()一个对象,明明控制台打印对象的某一个key是1,但是我展开这个对象里面的key居然是2,我在代码里面获取的也是2,就像下面这样:
不知道各位大佬遇到这样的情况是怎么个想法,反正我第一次遇到的时候我还以为是我的谷歌浏览器出问题了,擦💦我甚至都想卸载重装一波。后来动了动🧠,觉得可能是代码执行顺序的原因,所以我就在代码里面打了断点看了一下,在执行console.log()的时候arr的确是一个空的对象,对arr数组的操作是在console.log()执行之后才进行的。
所以说这到底是为什么呐?
其实这个还是和js的引用数据类型还有console.log()的设计有关系。我们都知道引用数据类型大体上可以说是由两部分组成:指针和内容,指针保存的内容就是一个内存地址的指向,指针一般都是基本数据类型保存在栈内存,内容就包含着这个引用数据类型的实际值一般保存在堆内存。😍 而console.log呐打印的时候只是打印了这个引用数据类型的一个快照,快照中的指针和内容都是照相的时候的内容,在console.log()之后,修改了这个引用数据类型,或者说在这之前修改的操作在一个异步的内容里面,当我们去看打印的时候,这个引用数据类型的内容可能就被修改了,但是因为快照的原因我们看到的还是以前的值。
然后当我们展开的时候,浏览器会利用指针去内存重新读取内容,因为快找的指针是没有发生变化的,所以就看到了改变之后内存,这就是为什么我们展开和不展开看到的结果是不一样的原因了。当然造成这样的原因不一定都是因为我们代码在异步里面操作这个引用数据类型。
还有就是浏览器在进行I/O的时候异步会提升性能,所有这就是为什么有时候我们写的同步代码依然会出现不一致的情况,就像我第二个图一样。
下面就验证一下我上面的想法,当我把上面的代码修改一下,直接替换:
let arr = [];
const setFun = () => {
return new Promise((reslove) => {
let arr1 = [1, 2, 3];
setTimeout(() => {
reslove(arr1);
}, 2000)
});
}
const getFun = async () => {
let result = await setFun();
arr = result; // 修改部分
}
getFun();
console.log(arr);
那么我们看到的结果就和上面不一样了,这个展开的表现是和不展开是一样的。
相信各位大佬也知道是啥原因了,因为这次直接替换,修改的是指针的指向并没有修改之前引用数据饿类型的内存空间,所以当我们展开的时候快照中指针保存的地址还是空的,这样我们看到的和看之前的想法就对应上了。
注:该问题只存在于打印引用数据类型,基本数据类型不会出现。
作者:江湖不渡i
来源:https://juejin.cn/post/7032504319584780325
12 个救命的 CSS 技巧
✨12 个救命的 CSS 技巧✨
1. 使用 Shape-outside 在浮动图像周围弯曲文本
它是一个允许设置形状的 CSS 属性。它还有助于定义文本流动的区域。css代码:
.any-shape {
width: 300px;
float: left;
shape-outside: circle(50%);
}
2. 魔法组合
这个小组合实际上可以防止你在 HTML 中遇到的大多数布局错误的问题。我们确实不希望水平滑块或绝对定位的项目做他们想做的事情,也不希望到处都是随机的边距和填充。所以这是你们的魔法组合。
* {
padding: 0;
margin: 0;
max-width: 100%;
overflow-x: hidden;
position: relative;
display: block;
}
有时“display:block”没有用,但在大多数情况下,你会将 <a>
和 <span>
视为与其他块一样的块。所以,在大多数情况下,它实际上会帮助你!
3. 拆分 HTML 和 CSS
这更像是一种“工作流程”类型的技巧。我建议你在开发时创建不同的 CSS 文件,最后才合并它们。例如,一个用于桌面,一个用于移动等。最后,你必须合并它们,因为这将有助于最大限度地减少您网站的 HTTP 请求数量。
同样的原则也适用于 HTML。如果你不是在 Gatsby 等 SPA 环境中进行开发,那么 PHP 可用于包含 HTML 代码片段。例如,你希望在单独的文件中保留一个“/modules”文件夹,该文件夹将包含导航栏、页脚等。因此,如果需要进行任何更改,你不必在每个页面上都对其进行编辑。模块化越多,结果就越好。
4. ::首字母
它将样式应用于块级元素的第一个字母。因此,我们可以从印刷或纸质杂志中引入我们熟悉的效果。如果没有这个伪元素,我们将不得不创建许多跨度来实现这种效果。例如:
这是如何做到的?代码如下:
p.intro:first-letter {
font-size: 100px;
display: block;
float: left;
line-height: .5;
margin: 15px 15px 10px 0 ;
}
5. 四大核心属性
CSS 动画提供了一种相对简单的方法来在大量属性之间平滑过渡。良好的动画界面依赖于流畅流畅的体验。为了在我们的动画时间线中保持良好的性能,我们必须将我们的动画属性限制为以下四个核心:
缩放 - transform:scale(2)
旋转 - transform:rotate(180deg)
位置 – transform:translateX(50rem)
不透明度 - opacity: 0.5
边框半径、高度/宽度或边距等动画属性会影响浏览器布局方法,而背景、颜色或框阴影的动画会影响浏览器绘制方法。所有这些都会大大降低您的 FPS (FramesPerSecond)。您可以使用这些属性来产生一些有趣的效果,但应谨慎使用它们以保持良好的性能。
6. 使用变量保持一致
保持一致性的一个好方法是使用 CSS 变量或预处理器变量来预定义动画时间。
:root{ timing-base: 1000;}
在不定义单元的情况下设置基线动画或过渡持续时间为我们提供了在 calc() 函数中调用此持续时间的灵活性。此持续时间可能与我们的基本 CSS 变量不同,但它始终是对该数字的简单修改,并将始终保持一致的体验。
7. 圆锥梯度
有没有想过是否可以只使用 CSS 创建饼图?好消息是,您实际上可以!这可以使用 conic-gradient 函数来完成。此函数创建一个由渐变组成的图像,其中设置的颜色过渡围绕中心点旋转。您可以使用以下代码行执行此操作:
.piechart {
background: conic-gradient(rgb(255, 132, 45) 0% 25%, rgb(166, 195, 209) 25% 56%, #ffb50d 56% 100%);
border-radius: 50%;
width: 300px;
height: 300px;
}
8. 更改文本选择颜色
要更改文本选择颜色,我们使用 ::selection。它是一个伪元素,在浏览器级别覆盖以使用您选择的颜色替换文本突出显示颜色。使用光标选择内容后即可看到效果。
::selection {
background-color: #f3b70f;
}
9. 悬停效果
悬停效果通常用于按钮、文本链接、站点的块部分、图标等。如果您想在有人将鼠标悬停在其上时更改颜色,只需使用相同的 CSS,但要添加 :hover到它并更改样式。这是您的方法;
.m h2{
font-size:36px;
color:#000;
font-weight:800;
}
.m h2:hover{
color:#f00;
}
当有人将鼠标悬停在 h2 标签上时,这会将您的 h2 标签的颜色从黑色更改为红色。它非常有用,因为如果您不想更改它,则不必再次声明字体大小或粗细。它只会更改您指定的任何属性。
10.投影
添加此属性可为透明图像带来更好的阴影效果。您可以使用给定的代码行执行此操作。
.img-wrapper img{
width: 100% ;
height: 100% ;
object-fit: cover ;
filter: drop-shadow(30px 10px 4px #757575);
}
11. 使用放置项居中 Div
居中 div 元素是我们必须执行的最可怕的任务之一。但不要害怕我的朋友,你可以用几行 CSS 将任何 div 居中。只是不要忘记设置display:grid; 对于父元素,然后使用如下所示的 place-items 属性。
main{
width: 100% ;
height: 80vh ;
display: grid ;
place-items: center center;
}
12. 使用 Flexbox 居中 Div
我们已经使用地点项目将项目居中。但是现在我们解决了一个经典问题,使用 flexbox 将 div 居中。为此,让我们看一下下面的示例:
<div>
<div></div>
</div>
.center {
display: flex;
align-items: center;
justify-content: center;
}
.center div {
width: 100px;
height: 100px;
border-radius: 50%;
background: #b8b7cd;
}
首先,我们需要确保父容器持有圆,即 flex-container。在它里面,我们有一个简单的 div 来制作我们的圆圈。我们需要使用以下与 flexbox 相关的重要属性:
display: flex; 这确保父容器具有 flexbox 布局。
align-items: center; 这可确保 flex 子项与横轴的中心对齐。
justify-content: center; 这确保 flex 子项与主轴的中心对齐。
之后,我们就有了常用的圆形 CSS 代码。现在这个圆是垂直和水平居中的,试试吧!
作者:海拥
来源:https://juejin.cn/post/7024372412632268813
vscode调试入门——不要只会console了!什么是launch.json?
前言
记得我还是一个小菜鸡的时候,就有人问过我,都用什么调试,我红着脸说到,我只会用console调试。羞愧的我想再继续掌握一下vscode调试的方法,可惜当时没有找到很好的教程,加上相关基础较差,只能是一知半解。如今进化为大菜鸡的我,总结一下基础的vscode调试
基本调试
可以说vscode对js代码的调试非常友好了,它内置了node的debugger插件,如果是要用vscdoe调试python,c++等还是要后续安装插件的
基本的调试方法很简单,写一段简单的代码
然后在调试项里找到这个小三角箭头
然后就可以进入node的调试界面
怎么样!是不是很简单就达到了我想要的效果,比单纯一个个console出来要更好
深入一下
上边的方法虽然很简单,但是只适用比较简单的情况,对于大多数调试场景,去创建launch配置文件是更好的,因为它允许配置和保存调试设置细节
launch.json
当你刚开始创建还没有launch.json的时候 vscoed会自动帮你自动检测你的debug环境,开始debug
但如果失败了,会让你进行选择
然后他会在你当前工作区下,给你创建个.vscode文件夹,里面有我们要的launch.json文件。简单说我们可以通过这个文件可配置的debug
假如你的launch.json文件是这样的(搞懂意思就好)
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": ["<node_internals>/**"],
}
]
}
其中
type属性是指你这次debug的类型 我这里介绍常用的两类 node和chrome 下边都会说到
request 指的是请求配置类型,氛围launch和attach
name 指的就是你这一条调试配置的name,会出现在start绿箭头,选择具体方式的时候
这几个是必用的
有更多的会在之后涉及,了解更多可以看这里的文档
attach还是launch
这是两个核心的debugging模式,用不同方式去处理工作流
简单来说 launch会在 你调试的工具 也就是我们用的vscode 启动另外的应用,这很适合你习惯于用浏览器的方法
attach 意为附加 会在你的开发者工具上附加调试程序
chrome debugger
除了用上述的type为node的调试方法,我们也可以用调用的chrome的工作台去调试
这里要安装一个插件 debugger for chrome 我在之前的文章当你买了新的mac 曾经提到过
当安装好之后
你就可以在我们的launch.json里添加配置啦!
假如我们添加一个这样的配置
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome",
"url": "http://localhost:8080",
"file": "${workspaceFolder}/index.html"
},
file 值得就是打开的文件 workspaceFolder是我们当前的工作区
假如我们的index.html是这样的并打上断点
可以进入我们的chrome调试页面
总结
基本的调试入门方法就是这样啦,其实还有更深层的内容,我会继续学习,完善这篇文章
作者:douxpang
链接:https://juejin.cn/post/6956832271236071431
收起阅读 »
前端架构师的 git 功力,你有几成火候?
分支管理策略
git 分支强大的同时也非常灵活,如果没有一个好的分支管理策略,团队人员随意合并推送,就会造成分支混乱,各种覆盖,冲突,丢失等问题。
目前最流行的分支管理策略,也称工作流(Workflow),主要包含三种:
- Git Flow
- GitHub Flow
- GitLab Flow
我司前端团队结合实际情况,制定出自己的一套分支管理策略。
我们将分支分为 4 个大类:
- dev-*
- develop
- staging
- release
dev-*
是一组开发分支的统称,包括个人分支,模块分支,修复分支等,团队开发人员在这组分支上进行开发。
开发前,先通过 merge
合并 develop 分支的最新代码;开发完成后,必须通过 cherry-pick
合并回 develop
分支。
develop
是一个单独分支,对应开发环境,保留最新的完整的开发代码。它只接受 cherry-pick
的合并,不允许使用 merge。
staging
分支对应测试环境。当 develop 分支有更新并且准备发布测试时,staging 要通过 rebase
合并 develop 分支,然后将最新代码发布到测试服务器,供测试人员测试。
测试发现问题后,再走 dev-* -> develop -> staging 的流程,直到测试通过。
release
则表示生产环境。release 分支的最新提交永远与线上生产环境代码保持同步,也就是说,release 分支是随时可发布的。
当 staging 测试通过后,release
分支通过 rebase
合并 staging 分支,然后将最新代码发布到生产服务器。
总结下合并规则:
- develop -> (merge) -> dev-*
- dev-* -> (cherry-pick) -> develop
- develop -> (rebase) -> staging
- staging -> (rebase) -> release
为什么合并到 develop 必须用 cherry-pick?
使用 merge 合并,如果有冲突,会产生分叉;dev-*
分支多而杂,直接 merge 到 develop 会产生错综复杂的分叉,难以理清提交进度。
而 cherry-pick 只将需要的 commit 合并到 develop 分支上,且不会产生分叉,使 git 提交图谱(git graph)永远保持一条直线。
再有,模块开发分支完成后,需要将多个 commit 合为一个 commit,再合并到 develop 分支,避免了多余的 commit,这也是不用 merge 的原因之一。
为什么合并到 staging/release 必须用 rebase?
rebase 译为变基,合并同样不会产生分叉。当 develop 更新了许多功能,要合并到 staging 测试,不可能用 cherry-pick 一个一个把 commit 合并过去。因此要通过 rebase 一次性合并过去,并且保证了 staging 与 develop 完全同步。
release 也一样,测试通过后,用 rebase 一次性将 staging 合并过去,同样保证了 staging 与 release 完全同步。
commit 规范与提交验证
commit 规范是指 git commit 时填写的描述信息,要符合统一规范。
试想,如果团队成员的 commit 是随意填写的,在协作开发和 review 代码时,其他人根本不知道这个 commit 是完成了什么功能,或是修复了什么 Bug,很难把控进度。
为了直观的看出 commit 的更新内容,开发者社区诞生了一种规范,将 commit 按照功能划分,加一些固定前缀,比如 fix:
,feat:
,用来标记这个 commit 主要做了什么事情。
目前主流的前缀包括以下部分:
build
:表示构建,发布版本可用这个ci
:更新 CI/CD 等自动化配置chore
:杂项,其他更改docs
:更新文档feat
:常用,表示新增功能fix
:常用:表示修复 bugperf
:性能优化refactor
:重构revert
:代码回滚style
:样式更改test
:单元测试更改
这些前缀每次提交都要写,刚开始很多人还是记不住的。这里推荐一个非常好用的工具,可以自动生成前缀。地址在这里
首先全局安装:
npm install -g commitizen cz-conventional-changelog
创建 ~/.czrc
文件,写入如下内容:
{ "path": "cz-conventional-changelog" }
现在可以用 git cz
命令来代替 git commit
命令,效果如下:
然后上下箭选择前缀,根据提示即可方便的创建符合规范的提交。
有了规范之后,光靠人的自觉遵守是不行的,还要在流程上对提交信息进行校验。
这个时候,我们要用到一个新东西 —— git hook
,也就是 git 钩子。
git hook 的作用是在 git 动作发生前后触发自定义脚本。这些动作包括提交,合并,推送等,我们可以利用这些钩子在 git 流程的各个环节实现自己的业务逻辑。
git hook 分为客户端 hook 和服务端 hook。
客户端 hook 主要有四个:
pre-commit
:提交信息前运行,可检查暂存区的代码prepare-commit-msg
:不常用commit-msg
:非常重要,检查提交信息就用这个钩子post-commit
:提交完成后运行
服务端 hook 包括:
pre-receive
:非常重要,推送前的各种检查都在这post-receive
:不常用update
:不常用
大多数团队是在客户端做校验,所以我们用 commit-msg
钩子在客户端对 commit 信息做校验。
幸运的是,不需要我们手动去写校验逻辑,社区有成熟的方案:husky + commitlint
husky 是创建 git 客户端钩子的神器,commitlint 是校验 commit 信息是否符合上述规范。两者配合,可以阻止创建不符合 commit 规范的提交,从源头保证提交的规范。
husky + commitlint 的具体使用方法请看这里
误操作的撤回方案
开发中频繁使用 git 拉取推送代码,难免会有误操作。这个时候不要慌,git 支持绝大多数场景的撤回方案,我们来总结一下。
撤回主要是两个命令:reset
和 revert
git reset
reset 命令的原理是根据 commitId
来恢复版本。因为每次提交都会生成一个 commitId,所以说 reset 可以帮你恢复到历史的任何一个版本。
这里的版本和提交是一个意思,一个 commitId 就是一个版本
reset 命令格式如下:
$ git reset [option] [commitId]
比如,要撤回到某一次提交,命令是这样:
$ git reset --hard cc7b5be
上面的命令,commitId 是如何获取的?很简单,用 git log
命令查看提交记录,可以看到 commitId 值,这个值很长,我们取前 7 位即可。
这里的 option 用的是 --hard
,其实共有 3 个值,具体含义如下:
--hard
:撤销 commit,撤销 add,删除工作区改动代码--mixed
:默认参数。撤销 commit,撤销 add,还原工作区改动代码--soft
:撤销 commit,不撤销 add,还原工作区改动代码
这里要格外注意 --hard
,使用这个参数恢复会删除工作区代码。也就是说,如果你的项目中有未提交的代码,使用该参数会直接删除掉,不可恢复,慎重啊!
除了使用 commitId 恢复,git reset 还提供了恢复到上一次提交的快捷方式:
$ git reset --soft HEAD^
HEAD^
表示上一个提交,可多次使用。
其实平日开发中最多的误操作是这样:刚刚提交完,突然发现了问题,比如提交信息没写好,或者代码更改有遗漏,这时需要撤回到上次提交,修改代码,然后重新提交。
这个流程大致是这样的:
# 1. 回退到上次提交
$ git reset HEAD^
# 2. 修改代码...
...
# 3. 加入暂存
$ git add .
# 4. 重新提交
$ git commit -m 'fix: ***'
针对这个流程,git 还提供了一个更便捷的方法:
$ git commit --amend
这个命令会直接修改当前的提交信息。如果代码有更改,先执行 git add
,然后再执行这个命令,比上述的流程更快捷更方便。
reset 还有一个非常重要的特性,就是真正的后退一个版本。
什么意思呢?比如说当前提交,你已经推送到了远程仓库;现在你用 reset 撤回了一次提交,此时本地 git 仓库要落后于远程仓库一个版本。此时你再 push,远程仓库会拒绝,要求你先 pull。
如果你需要远程仓库也后退版本,就需要 -f
参数,强制推送,这时本地代码会覆盖远程代码。
注意,-f
参数非常危险!如果你对 git 原理和命令行不是非常熟悉,切记不要用这个参数。
那撤回上一个版本的代码,怎么同步到远程更安全呢?
方案就是下面要说的第二个命令:git revert
git revert
revert 与 reset 的作用一样,都是恢复版本,但是它们两的实现方式不同。
简单来说,reset 直接恢复到上一个提交,工作区代码自然也是上一个提交的代码;而 revert 是新增一个提交,但是这个提交是使用上一个提交的代码。
因此,它们两恢复后的代码是一致的,区别是一个新增提交(revert),一个回退提交(reset)。
正因为 revert 永远是在新增提交,因此本地仓库版本永远不可能落后于远程仓库,可以直接推送到远程仓库,故而解决了 reset 后推送需要加 -f
参数的问题,提高了安全性。
说完了原理,我们再看一下使用方法:
$ git revert -n [commitId]
掌握了原理使用就很简单,只要一个 commitId 就可以了。
Tag 与生产环境
git 支持对于历史的某个提交,打一个 tag 标签,常用于标识重要的版本更新。
目前普遍的做法是,用 tag 来表示生产环境的版本。当最新的提交通过测试,准备发布之时,我们就可以创建一个 tag,表示要发布的生产环境版本。
比如我要发一个 v1.2.4
的版本:
$ git tag -a v1.2.4 -m "my version 1.2.4"
然后可以查看:
$ git show v1.2.4
> tag v1.2.4
Tagger: ruims <2218466341@qq.com>
Date: Sun Sep 26 10:24:30 2021 +0800
my version 1.2.4
最后用 git push 将 tag 推到远程:
$ git push origin v1.2.4
这里注意:tag 和在哪个分支创建是没有关系的,tag 只是提交的别名。因此 commit 的能力 tag 均可使用,比如上面说的 git reset
,git revert
命令。
当生产环境出问题,需要版本回退时,可以这样:
$ git revert [pre-tag]
# 若上一个版本是 v1.2.3,则:
$ git revert v1.2.3
在频繁更新,commit 数量庞大的仓库里,用 tag 标识版本显然更清爽,可读性更佳。
再换一个角度思考 tag 的用处。
上面分支管理策略的部分说过,release 分支与生产环境代码同步。在 CI/CD(下面会讲到)持续部署的流程中,我们是监听 release 分支的推送然后触发自动构建。
那是不是也可以监听 tag 推送再触发自动构建,这样版本更新的直观性是不是更好?
诸多用处,还待大家思考。
永久杜绝 443 Timeout
我们团队内部的代码仓库是 GitHub,众所周知的原因,GitHub 拉取和推送的速度非常慢,甚至直接报错:443 Timeout。
我们开始的方案是,全员开启 VPN。虽然大多时候速度不错,但是确实有偶尔的一个小时,甚至一天,代码死活推不上去,严重影响开发进度。
后来突然想到,速度慢超时是因为被墙,比如 GitHub 首页打不开。再究其根源,被墙的是访问网站时的 http 或 https 协议,那么其他协议是不是就不会有墙的情况?
想到就做。我们发现 GitHub 除了默认的 https
协议,还支持 ssh
协议。于是准备尝试一下使用 ssh 协议克隆代码。
用 ssh 协议比较麻烦的一点,是要配置免密登录,否则每次 pull/push 时都要输入账号密码。
GitHub 配置 SSH 的官方文档在这里
英文吃力的同学,可以看这里
总之,生成公钥后,打开 GitHub 首页,点 Account -> Settings -> SSH and GPG keys -> Add SSH key,然后将公钥粘贴进去即可。
现在,我们用 ssh 协议克隆代码,例子如下:
$ git clone git@github.com:[organi-name]/[project-name]
发现瞬间克隆下来了!再测几次 pull/push,速度飞起!
不管你用哪个代码管理平台,如果遇到 443 Timeout 问题,请试试 ssh 协议!
hook 实现部署?
利用 git hook 实现部署,应该是 hook 的高级应用了。
现在有很多工具,比如 GitHub,GitLab,都提供了持续集成功能,也就是监听某一分支推送,然后触发自动构建,并自动部署。
其实,不管这些工具有多少花样,核心的功能(监听和构建)还是由 git 提供。只不过在核心功能上做了与自家平台更好的融合。
我们今天就抛开这些工具,追本溯源,使用纯 git 实现一个 react 项目的自动部署。掌握了这套核心逻辑,其他任何平台的持续部署也就没那么神秘了。
由于这一部分内容较多,所以单独拆出去一篇文章,地址如下:
终极应用: CI/CD
上面的一些地方也提到了持续集成,持续部署这些字眼,现在,千呼万唤始出来,主角正式登场了!
可以这么说,上面写到的所有规范规则,都是为了更好的设计和实现这个主角 ——— CI/CD。
首先了解一下,什么是 CI/CD ?
核心概念,CI(Continuous Integration)译为持续集成,CD 包括两部分,持续交付(Continuous Delivery)和持续部署(Continuous Deployment)
从全局看,CI/CD 是一种通过自动化流程来频繁向客户交付应用的方法。这个流程贯穿了应用的集成,测试,交付和部署的整个生命周期,统称为 “CI/CD 管道”。
虽然都是像流水线一样自动化的管道,但是 CI 和 CD 各有分工。
持续集成是频繁地将代码集成到主干分支。当新代码提交,会自动执行构建、测试,测试通过则自动合并到主干分支,实现了产品快速迭代的同时保持高质量。
持续交付是频繁地将软件的新版本,交付给质量团队或者用户,以供评审。评审通过则可以发布生产环境。持续交付要求代码(某个分支的最新提交)是随时可发布的状态。
持续部署是代码通过评审后,自动部署到生产环境。持续部署要求代码(某个分支的最新提交)是随时可部署的。
持续部署与持续交付的唯一区别,就是部署到生产环境这一步,是否是自动化。
部署自动化,看似是小小的一步,但是在实践过程中你会发现,这反而是 CI/CD 流水线中最难落实的一环。
为什么?首先,从持续集成到持续交付,这些个环节都是由开发团队实施的。我们通过团队内部协作,产出了新版本的待发布的应用。
然而将应用部署到服务器,这是运维团队的工作。我们要实现部署,就要与运维团队沟通,然而开发同学不了解服务器,运维同学不了解代码,沟通起来困难重重。
再有,运维是手动部署,我们要实现自动部署,就要有服务器权限,与服务器交互。这也是个大问题,因为运维团队一定会顾虑安全问题,因而推动起来节节受阻。
目前社区成熟的 CI/CD 方案有很多,比如老牌的 jenkins,react 使用的 circleci,还有我认为最好用的GitHub Action等,我们可以将这些方案接入到自己的系统当中。
这篇文章篇幅已经很长了,就到这里结束吧。接下来我会基于 GitHub Action 单独出一篇详细的 react 前端项目 CI/CD 实践,记得关注我的专栏哦。
作者:杨成功
链接:https://juejin.cn/post/7024043015794589727
收起阅读 »
先睹为快即将到来的HTML6
HTML,超文本标记语言,是一种用于创建网页的标准标记语言。自从引入 HTML 以来,它就一直用于构建互联网。与 JavaScript 和 CSS 一起,HTML 构成前端开发的三剑客。
尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核心。随着 HTML5 的普及,在 2014 年,这种标记语言发生了很多变化,变得更加友好,浏览器对新标准的支持热度也越来越高。而HTML并不止于此,还在不断发生变化,并且可能会获得一些特性来证明对 HTML6 的命名更改是合理的。
支持原生模式
该元素<dialog>
将随 HTML6 一起提供。它被认为等同于用 JavaScript 开发的模态,并且已经标准化,但只有少数浏览器完全支持。但这种现象会改变,很快它将在所有浏览器中得到支持。
这个元素在其默认格式下,只会将光标显示在它所在的位置上,但可以使用 JavaScript 打开模式。
<dialog>
<form method="dialog">
<input type="submit" value="确定" />
<input type="submit" value="取消" />
</form>
</dialog>
在默认形式下,该元素创建一个灰色背景,其下方是非交互式内容。
可以在 <dialog>
其中的表单上使用一种方法,该方法将发送值并将其传递回自身 <dialog>
。
总的来说,这个标签在用户交互和改进的界面中变得有益。
可以通过更改 <dialog>
标签的 open
属性以控制打开和关闭。
<dialog open>
<p>组件内容</p>
</dialog>
没有 JavaScript 的单页应用程序
FutureClaw 杂志主编 Bobby Mozumder 建议:
将锚元素链接到
JSON/XML
、API 端点,让浏览器在内部将数据加载到新的数据结构中,然后浏览器将DOM
元素替换为根据需要加载的任何数据。初始数据(以及标准错误响应)可以放在标题装置中,如果需要,可以稍后替换。
据他介绍,这是单页应用程序网页设计模式,可以提高响应速度和加载时间,因为不需要加载 JavaScript。
自由调整图像大小
HTML6 爱好者相信即将到来的更新将允许浏览器调整图像大小以获得更好的观看体验。
每个浏览器都难以呈现相对于设备和屏幕尺寸的最佳图像尺寸,不幸的是,srce
标签 img
在处理这个问题时不是很有效。
这个问题可以通过一个新标签 <srcset>
来解决,它使浏览器在多个图像之间进行选择的工作变得更加容易。
专用库
将可用库引入 HTML6 绝对是提高开发效率的重要一步。
微格式
很多时候,需要在互联网上定义一般信息,而这些一般信息可以是任何公开的信息,例如电话号码、姓名、地址等。微格式是能够定义一般数据的标准。微格式可以增强设计者的能力,并可以减少搜索引擎推断公共信息所需的努力。
自定义菜单
尽管标签<ul>
、<ol>
非常有用,但在某些情况下仍有一些不足之处。可以处理交互元素的标签将是一个不错的选择。
这就是创建标签 <menu>
的驱动力,它可以处理按钮驱动的列表元素。
<menu type="toolbar">
<li><button>个人信息</button></li>
<li><button>系统设置</button></li>
<li><button>账号注销</button></li>
</menu>
因此 <menu>
,除了能够像普通列表一样运行之外,还可以增强 HTML 列表的功能。
增强身份验证
虽然HTML5在安全性方面还不错,浏览器和网络技术也提供了合理的保护。毫无疑问,在身份验证和安全领域还有很多事情可以做。如密钥可以异地存储;这将防止不受欢迎的人访问并支持身份验证。使用嵌入式密钥而不是 cookie,使数字签名更好等。
集成摄像头
HTML6 允许以更好的方式使用设备上的相机和媒体。将能够控制相机、它的效果、模式、全景图像、HDR 和其他属性。
总结
没有什么是完美的,HTML 也不是完美的,所以 HTML 规范可以做很多事情来使它更好。应该对一些有用的规范进行标准化,以增强 HTML 的能力。小的变化已经开始推出。如增强蓝牙支持、p2p 文件传输、恶意软件保护、云存储集成,下一个 HTML 版本可以考虑一下。
作者:天行无忌
链接:https://juejin.cn/post/7032874253573685261
收起阅读 »
先睹为快即将到来的HTML6
尽管许多新技术使网站创建过程变得更简单、更高效,但 HTML 始终是核心。随着 HTML5 的普及,在 2014 年,这种标记语言发生了很多变化,变得更加友好,浏览器对新标准的支持热度也越来越高。而HTML并不止于此,还在不断发生变化,并且可能会获得一些特性来证明对 HTML6 的命名更改是合理的。
支持原生模式
该元素<dialog>
将随 HTML6 一起提供。它被认为等同于用 JavaScript 开发的模态,并且已经标准化,但只有少数浏览器完全支持。但这种现象会改变,很快它将在所有浏览器中得到支持。
这个元素在其默认格式下,只会将光标显示在它所在的位置上,但可以使用 JavaScript 打开模式。
<dialog>
<form method="dialog">
<input type="submit" value="确定" />
<input type="submit" value="取消" />
</form>
</dialog>
在默认形式下,该元素创建一个灰色背景,其下方是非交互式内容。
可以在 <dialog>
其中的表单上使用一种方法,该方法将发送值并将其传递回自身 <dialog>
。
总的来说,这个标签在用户交互和改进的界面中变得有益。
可以通过更改 <dialog>
标签的 open
属性以控制打开和关闭。
<dialog open>
<p>组件内容</p>
</dialog>
没有 JavaScript 的单页应用程序
FutureClaw 杂志主编 Bobby Mozumder 建议:
将锚元素链接到
JSON/XML
、API 端点,让浏览器在内部将数据加载到新的数据结构中,然后浏览器将DOM
元素替换为根据需要加载的任何数据。初始数据(以及标准错误响应)可以放在标题装置中,如果需要,可以稍后替换。
据他介绍,这是单页应用程序网页设计模式,可以提高响应速度和加载时间,因为不需要加载 JavaScript。
自由调整图像大小
HTML6 爱好者相信即将到来的更新将允许浏览器调整图像大小以获得更好的观看体验。
每个浏览器都难以呈现相对于设备和屏幕尺寸的最佳图像尺寸,不幸的是,srce
标签 img
在处理这个问题时不是很有效。
这个问题可以通过一个新标签 <srcset>
来解决,它使浏览器在多个图像之间进行选择的工作变得更加容易。
专用库
将可用库引入 HTML6 绝对是提高开发效率的重要一步。
微格式
很多时候,需要在互联网上定义一般信息,而这些一般信息可以是任何公开的信息,例如电话号码、姓名、地址等。微格式是能够定义一般数据的标准。微格式可以增强设计者的能力,并可以减少搜索引擎推断公共信息所需的努力。
自定义菜单
尽管标签<ul>
、<ol>
非常有用,但在某些情况下仍有一些不足之处。可以处理交互元素的标签将是一个不错的选择。
这就是创建标签 <menu>
的驱动力,它可以处理按钮驱动的列表元素。
<menu type="toolbar">
<li><button>个人信息</button></li>
<li><button>系统设置</button></li>
<li><button>账号注销</button></li>
</menu>
因此 <menu>
,除了能够像普通列表一样运行之外,还可以增强 HTML 列表的功能。
增强身份验证
虽然HTML5在安全性方面还不错,浏览器和网络技术也提供了合理的保护。毫无疑问,在身份验证和安全领域还有很多事情可以做。如密钥可以异地存储;这将防止不受欢迎的人访问并支持身份验证。使用嵌入式密钥而不是 cookie,使数字签名更好等。
集成摄像头
HTML6 允许以更好的方式使用设备上的相机和媒体。将能够控制相机、它的效果、模式、全景图像、HDR 和其他属性。
总结
没有什么是完美的,HTML 也不是完美的,所以 HTML 规范可以做很多事情来使它更好。应该对一些有用的规范进行标准化,以增强 HTML 的能力。小的变化已经开始推出。如增强蓝牙支持、p2p 文件传输、恶意软件保护、云存储集成,下一个 HTML 版本可以考虑一下。
作者:天行无忌
来源:https://juejin.cn/post/7032874253573685261
是时候封装一个DOM库了
由于原始的DOM提供的API过长,不方便记忆,
于是我采用对象风格的形式封装了一个DOM库->源代码链接
这里对新封装的API进行总结
同样,用增删改查进行划分,我们先提供一个全局的window.dom对象
增
创建节点
create (string){
const container = document.createElement("template")//template可以容纳任意元素
container.innerHTML = string.trim();//去除字符串两边空格
return container.content.firstChild;//用template,里面的元素必须这样获取
}
首先,如果用原始的DOM API,我们想要创建一个div,div里面含有一个文本'hi',需要分为两步
document.createElement('div')
div.innerText = 'hi'
而这里,只需要一步就能完成dom.create('
hi
')
它可以直接创建多标签的嵌套,如
create('你好')
为什么能这样写?
因为我们用
innerHTML
直接把字符串写进了HTML里,字符串直接变成了HTML里面的内容为什么使用template?
因为template可以容纳任意元素,如果使用div,div不能直接容纳标签,但template就可以
新增哥哥
before(node,node2){
node.parentNode.insertBefore(node2,node);
}
这个比较简单,找到爸爸节点,然后使用```JavaScript,新增一个node2即可
新增弟弟
after(node,node2){
node.parentNode.insertBefore(node2,node.nextSibling); //把node2插到node下一个节点的前面,即使node的下一个节点为空,也能插入
}
由于原始的DOM只有insertBefore,并没有insertAfter,所以要实现这个功能我们需要一个曲线救国的方法:
node.nextSibling 表示node节点的下一个节点,
而想在node的后面插入一个节点,就等于说在node的下一个节点前插入一个新节点node2即可
如上代码就是实现了这个操作,而即使node的下一个节点为空,也能成功插入
新增儿子
append(parent,node){
parent.appendChild(node)
}
找到爸爸节点,用appendChild即可
新增爸爸
wrap(node,parent){
dom.before(node,parent)
dom.append(parent,node)
}
思路如图:
分为两步走:
先把新增的爸爸节点,放到老节点的前面
再把老节点放入新增的爸爸节点的里面
这样就可以使新的爸爸节点包裹住老节点
使用示例:
const newDiv = dom.create('')
dom.wrap(test, newDiv)
删
删节点
remove(node){
node.parentNode.removeChild(node)
return node
}
找到爸爸节点,removeChild即可
删除所有子节点
empty(node){
const array = []
let x = node.firstChild
while (x) {
array.push(dom.remove(node.firstChild))
x = node.firstChild//x指向下一个节点
}
return array
}
其实一开始的思路,是用for循环
for(let i = 0;i<childNodes.length;i++){
dom.remove(childNodes[i])
}
但这样的思路有一个问题:childNodes.length是会随着删除而变化的
所以我们需要改变思路,用while循环:
先找到该节点的第一个儿子赋值为x
当x是存在的,我们就把它移除,并放入数组里面(用于获取删除的节点的引用)
再把x赋值给它的下一个节点(当第一个儿子被删除后,下一个儿子就变成了第一个儿子)
反复操作,直到所有子节点被删完
改
读写属性
attr(node,name,value){
if(arguments.length === 3){
node.setAttribute(name,value)
}else if(arguments.length === 2){
return node.getAttribute(name)
}
}
这里运用重载,实现两种不同的功能:
当输入的参数是3个时,就写属性
当如数的参数是2个时,读属性
使用示例:
//写:
//给 test 添加属性
dom.attr(test,'title','Hi,I am Wang')
//添加之后:test
//读:
const title = dom.attr(test,'title')
console.log(`title:${title}`)
//打印出:title:Hi,Hi,I am Wang
读写文本内容
text(node,string){
if(arguments.length === 2){
if('innerText' in node){
node.innerText = string
}else{
node.textContent = string
}
}else if(arguments.length === 1){
if('innerText' in node){
return node.innerText
}else{
return node.textContent
}
}
}
为什么这里需要适配,innerText与textContent?
因为虽然现在绝大多数浏览器都支持两种,但还是有非常旧的IE只支持innerText,所以这里是为了适配所有浏览器
同时与读写属性思路相同:
当输入的参数是2个时,就在节点里写文本
当输入的参数是1个时,就读文本内容
读写HTML的内容
html(node,string){
if(arguments.length === 2){
node.innerHTML = string
}else if(arguments.length === 1){
return node.innerHTML
}
}
同样,2参数写内容,1参数读内容
修改Style
style(node,name,value){
if(arguments.length === 3){
//dom.style(div,'color','red')
node.style[name] = value
}else if(arguments.length === 2){
if(typeof name === 'string'){
//dom.style(div,'color')
return node.style[name]
}else if(name instanceof Object){
//dom.style(div,{color:'red'})
const Object = name
for(let key in Object){
//key:border/color
//node.style.border = ...
//node.style.color = ...
node.style[key] = Object[key]
}
}
}
}
思路:
首先判断输入的参数,如果为3个如:
dom.style(div,'color','red')
就更改它的style
如果输入参数为2个时,先判断输入name的值的类型
如果是字符串,如
dom.style(div,'color')
,就返回style的属性如果是对象,如
dom.style(div,{border:'1px solid red',color:'blue'})
,就更改它的style
增删查class
class:{
add(node,className){
node.classList.add(className)
},
remove(node,className){
node.classList.remove(className)
},
has(node,className){
return node.classList.contains(className)
}
}
注:查找一个元素的classList里是否有某一个class, 用的是contains
添加事件监听
on(node,eventName,fn){
node.addEventListener(eventName,fn)
}
使用示例:
const fn = ()=>{
console.log('点击了')
}
dom.on(test,'click',fn)
这样当点击id为test的div时,就会打印出'点击了'
删除事件监听
off(node,eventName,fn){
node.removeEventListener(eventName,fn)
}
查
获取单个或多个标签
find(selector,scope){
return (scope || document).querySelectorAll(selector)
}
可以在指定区域或者全局的document里找
使用示例:
在document中查询:
const testDiv = dom.find('#test')[0]
console.log(testDiv)
在指定范围内查询:
<div>
<div id="test"><span>test1span>
<p class="red">段落标签p>
div>
<div id="test2">
<p class="red">段落标签p>
div>
div>
我只想找test2里面的red,应该怎么做
const test2 = dom.find('#test2')[0]
console.log(dom.find('.red',test2)[0])
注意:末尾的[0]别忘记写
获取父元素
parent(node){
return node.parentNode
}
获取子元素
children(node){
return node.children
}
获取兄弟姐妹元素
siblings(node){
return Array.from(node.parentNode.children).filter(n=>n!==node) //伪数组变数组再过滤本身
}
找到爸爸节点,然后过滤掉自己本身
获取弟弟
next(node){
let x = node.nextSibling
while(x && x.nodeType === 3){
x = x.nextSibling
}
return x
}
为什么这里需要while(x && x.nodeType === 3)
?
因为我们不想获取文本节点(空格回车等)
所以当读到文本节点时,自动再去读取下一个节点,直到读到的内容不是文本节点为止
获取哥哥
previous(node){
let x = node.previousSibling
while(x && x.nodeType === 3){
x = x.previousSibling
}
return x
}
与上面思路相同
遍历所有节点
each(nodeList,fn){
for(let i=0;i<nodeList.length;i++){
fn.call(null,nodeList[i])
}
}
注:null用于填充this的位置
使用示例:
利用fn可以更改所有节点的style
const t = dom.find('#travel')[0]
dom.each(dom.children(t),(n)=>dom.style(n,'color','red'))
遍历每个节点,把每个节点的style都更改
用于获取排行老几
index(node){
const list = dom.children(node.parentNode)
let i;
for(i=0;i<list.length;i++){
if(list[i]===node){
break
}
}
return i
}
思路:
获取爸爸节点的所有儿子
设置一个变量i
如果i等于想要查询的node
退出循环,返回i值
作者:PrayWang
来源:https://juejin.cn/post/7038171258617331719
这一次,彻底搞懂 async...await
先上结论:
执行 async 函数,返回的都是 Promise 对象
Promise.then() 对应 await
Promise.catch() 对应 try...catch
执行 async 函数,返回的都是 Promise 对象
先看下面两个函数:
async function test1() {
return 1;
}
async function test2() {
return Promise.resolve(2);
}
const res1 = test1();
const res2 = test2();
console.log('res1', res1);
console.log('res1', res2);
test1 和 test2 两个函数前面都加了 async,说明这两个都是异步函数,并且如果一个函数前加了 async,那么这个函数的返回值就是一个 Promise(不论这个函数返回的是什么,都会被 JS 引擎包装成 Promise 对象)。
输出结果如下图:
Promise.then() 对应 await
1.直接在一个 await 后面加 promise 对象
看下面的代码:
async function test3() {
const p3 = Promise.resolve(3);
p3.then(data => {
console.log('data', data);
});
const data = await p3;
console.log('data', data);
}
test3();
输出结果如下图:
可以看到输出是相同的,这就说明了 Promise 的 then() 方法对应 await。
2.直接在一个 await 后面加一个基本数据类型的值
看下面的例子:
async function test4() {
const data4 = await 4; // await Promise.resolve(4)
console.log('data4', data4);
}
test4();
输出结果如下图:
可以看到输出的是 4,上面的 await 4 就相当于 await Promise.resolve(4),又因为 await 相当于 then(),所以输出的就是 4。
3.直接在一个 await 后面加一个异步函数
看下面的例子:
async function test1() {
return 1;
}
async function test5() {
const data5 = await test1();
console.log('data5', data5);
}
test5();
输出结果如下图:
可以看到输出的是 1,首先 test5() 执行,然后执行 test1(),test1 返回数字 1,相当于返回 Promise.resolve(1),await 又相当于 then(),所以输出 1。
:::tip 提示 开发中最常用的就是第三种,await 后面跟一个异步函数,所以一定要掌握! :::
Promise.catch() 对应 try...catch
看下面的例子:
async function test6() {
const p6 = Promise.reject(6);
const data6 = await p6;
console.log('data6', data6);
}
test6();
输出结果如下图:
可以看到没有捕获到错误,那应该怎么做呢?没错,可以使用 try...catch。 看下面的例子:
async function test6() {
const p6 = Promise.reject(6);
try {
const data6 = await p6;
console.log('data6', data6);
} catch (e) {
console.log('e', e); // 顺利捕获错误
}
}
test6();
输出结果如下图:
可以看到已经成功捕获到错误了!
作者:ShiYan_Chen
来源:https://juejin.cn/post/7038152028664627230
会话过期后token刷新,重新请求接口(订阅发布模式)
前言
❝ 最近,我们老大让小白搞一下登录模块,登录模块说简单也简单,复杂也复杂。本章主要讲一下,会话过期后,token 刷新的一系列的事。 ❞
需求
在一个页面内,当请求失败并且返回 302 后,判断是接口过期还是登录过期,如果是接口过期,则去请求新的token,然后拿新的token去再次发起请求.
思路
当初,想了一个黑科技(为了偷懒),就是拿到新的token后,直接强制刷新页面,这样一个页面内的接口就自动刷新啦~(方便是方便,用户体验却不好)
目前,想到了重新请求接口时,可以配合订阅发布模式来提高用户体验
响应拦截
首先我们发起一个请求 axios({url:'/test',data:xxx}).then(res=>{})
拦截到302后,我们进入到刷新token逻辑
响应拦截代码
axios.interceptors.response.use(
function (response) {
if (response.status == 200) {
return response;
}
},
(err) => {
//刷新token
let res = err.response || {};
if (res.data.meta?.statusCode == 302) {
return refeshToken(res);
} else {
return err;
}
}
);
我们后台的数据格式是根据statusCode来判断过期(你们可以根据自己的实际情况判断),接着进入refrshToken
方法~
刷新token方法
//避免其他接口同时请求(只请求一次token接口)
let isRefreshToken = false;
const refeshToken = (response) => {
if (!isRefreshToken) {
isRefreshToken = true;
axios({
//获取新token接口
url: `/api/refreshToken`,
})
.then((res) => {
const { data = '', meta = {} } = res.data;
if (meta.statusCode === 200) {
isRefreshToken = false;
//发布 消息
retryOldRequest.trigger(data);
} else {
history.push('/user/login');
}
})
.catch((err) => {
history.push('/user/login');
});
}
//收集订阅者 并把成功后的数据返回原接口
return retryOldRequest.listen(response);
};
看到这,有的小伙伴就有点奇怪retryOldRequest
这个又是什么?没错,这就是我们男二 订阅发布模式队列。
订阅发布模式
大家如果还不了解订阅发布模式,可以点击看一下,里面有大神写的通俗易懂的例子(觉得学到的话,可以顺便帮点赞哦~)。
把失败的接口当订阅者,成功拿到新的token后再发布(重新请求接口)。
以下便是订阅发布模式代码
const retryOldRequest = {
//维护失败请求的response
requestQuery: [],
//添加订阅者
listen(response) {
return new Promise((resolve) => {
this.requestQuery.push((newToken) => {
let config = response.config || {};
//Authorization是传给后台的身份令牌
config.headers['Authorization'] = newToken;
resolve(axios(config));
});
});
},
//发布消息
trigger(newToken) {
this.requestQuery.forEach((fn) => {
fn(newToken);
});
this.requestQuery = [];
},
};
大家可以先不用关注订阅者的逻辑,只需要知道订阅者是每次请求失败后的接口(reponse)就好了。
每次进入refeshToken
方法,我们失败的接口都会触发retryOldRequest.listen
去订阅,而我们的requestQuery
则是保存这些订阅者的队列。
注意
:我们订阅者队列requestQuery
是保存待发布的方法。而在成功获取新token后,retryOldRequest.trigger
就会去发布这些消息(新token)给订阅者(触发订阅队列的方法)。
而订阅者(response
)里面有config配置,我们拿到新的token后(发布后),修改config里面的请求头Autorzation.而借助Promise我们可以更好的拿到新token请求回来的接口数据,一旦请求到数据,我们可以原封不动的返回给原来的接口/test
了(因为我们在响应拦截那里返回的是refreshToken
,而refreshToken
又返回的是订阅者retryOldRequest.listen
返回的数据,而Listiner又返回Promise的数据,Promise又在成功请求后resolve出去)。
看到这,小伙伴们是不是觉得有点绕了~
而在真实开发中,我们的逻辑还含有登录过期(与请求过期区分开来)。我们是根据当前时间 - 过去时间 < expiresTime
(epiresTime:登录后返回的有效时间)来判断是请求过期还是登录过期的。 以下是完整逻辑
以下是完整代码
const retryOldRequest = {
//维护失败请求的response
requestQuery: [],
//添加订阅者
listen(response) {
return new Promise((resolve) => {
this.requestQuery.push((newToken) => {
let config = response.config || {};
config.headers['Authorization'] = newToken;
resolve(axios(config));
});
});
},
//发布消息
trigger(newToken) {
this.requestQuery.forEach((fn) => {
fn(newToken);
});
this.requestQuery = [];
},
};
/**
* sessionExpiredTips
* 会话过期:
* 刷新token失败,得重新登录
* 用户未授权,页面跳转到登录页面
* 接口过期 => 刷新token
* 登录过期 => 重新登录
* expiresTime => 在本业务中返回18000ms == 5h
* ****/
//避免其他接口同时请求
let isRefreshToken = false;
let timer = null;
const refeshToken = (response) => {
//登录后拿到的有效期
let userExpir = localStorage.getItem('expiresTime');
//当前时间
let nowTime = Math.floor(new Date().getTime() / 1000);
//最后请求的时间
let lastResTime = localStorage.getItem('lastResponseTime') || nowTime;
let token = localStorage.getItem('token');
if (token && nowTime - lastResTime < userExpir) {
if (!isRefreshToken) {
isRefreshToken = true;
axios({
url: `/api/refreshToken`,
})
.then((res) => {
const { data = '', meta = {} } = res.data;
isRefreshToken = false;
if (meta.statusCode === 200) {
localStorage.getItem('token', data);
localStorage.getItem('lastResponseTime', Math.floor(new Date().getTime() / 1000)
);
//发布 消息
retryOldRequest.trigger(data);
} else {
//去登录
}
})
.catch((err) => {
isRefreshToken = false;
//去登录
});
}
//收集订阅者 并把成功后的数据返回原接口
return retryOldRequest.listen(response);
} else {
//节流:避免重复运行
//去登录
}
};
// http response 响应拦截
axios.interceptors.response.use(
function (response) {
if (response.status == 200) {
//记录最后操作时间
localStorage.getItem('lastResponseTime', Math.floor(new Date().getTime() / 1000));
return response;
}
},
(err) => {
let res = err.response || {};
if (res.data.meta?.statusCode == 302) {
return refeshToken(res);
} else {
// 非302 报的错误;
return err;
}
}
);
以上便是我们这边的业务,如果写的不好请大佬多担待~~
如果有好方案的小伙伴也可以在评论区内互相讨论~
作者:用户3797421129853
来源:https://juejin.cn/post/7037787299202990093
通过 Performance 证明,网页的渲染是一个宏任务
网页的渲染是一个宏任务。 这是我下的一个结论。
别着急反驳,后面我会给出证据。
我们先来聊下什么是调试:
调试是通过工具获取运行过程中的某一时刻或某一段时间的各方面的数据,帮助开发者理清逻辑、分析性能、排查问题等。 JS 的各种运行环境都会提供调试器,除此以外我们也会自己做一些埋点上报来做调试和统计。
我们最常用的调试工具是 JS Debugger,它支持断点,可以在某处断住,查看当前上下文的变量、调用栈等,这对于理清逻辑很有帮助。
但是性能分析的调试工具却不能这样做,不能用断住的方式实时查看,因为会影响数据的真实性。所以这类工具都是通过录制一段时间的数据,然后作事后的统计和分析的方式,常用的是 Chrome Devtools 里的 Performance 工具。(甚至为了避免浏览器插件的影响,还要用无痕模式来运行网页)点击录制按钮 record 开始录制(如果想录制从页面加载开始的数据,就点击 reload 按钮),Performance 会记录下录制时间内各方面的数据。
有哪些数据呢?
网页的运行是有多个线程的,主线程负责通过 Event Loop 的方式来不断的执行 JS 和渲染,也有一些别的线程,比如合成渲染图层的线程,Web Worker 的线程等,渲染的每一帧会绘制到界面上。
网页是这样运行的,那记录的自然也都是这些数据:Performance 会记录网页的每个线程的数据,其中最重要的是主线程,也就是图中的 Main,这部分记录着 Event Loop 的执行过程,记录着 JS 执行的调用栈和页面渲染的流程。
看到图中标出的一个个小灰块了么,那就是一个个 Task,也就是宏任务。Event Loop 就是循环执行宏任务。每个 Task 都有自己的调用栈,可以看到函数的执行路径,耗时等信息。图中宽度代表了耗时,可以直观的通过块的宽窄来分析性能。
执行完宏任务会执行所有的微任务,在图中也可以清晰的看到:点击每一个块可以看到代码的位置,可以定位到对应代码,这样就可以分析出哪块代码性能不好。
这些是 Main 线程的执行逻辑,也就是通过 Event Loop 来不断执行 JS 和渲染。
当然,还有其他线程,比如光栅化线程,也就是负责把渲染出的图层合并成一帧的线程:总之,就像 Debugger 面前,JS 的执行过程没有秘密一样,在 Performance 面前,网页各线程的执行过程也没有秘密。
说了这么多,就是为了讲清楚调试工具和 Performance 都是干啥的,它记录了哪些信息。
我们想知道渲染是不是一个宏任务,自然可以通过 Performance 来轻易的分析出来。
我们继续看 Main 线程的 Event Loop 执行过程:你会看到一个个很小的灰块,也就是一个个 Task,每隔一段时间都会执行,点击它,就会看到其实他做的就是渲染,包括计算布局,更新渲染树,合并图层、渲染等。
这说明了什么,不就说明了渲染是一个宏任务么。
所以,我们得到了结论:渲染是一个宏任务,通过 Event Loop 来做一帧帧的渲染。
通过 Performance 调试工具,我们可以看到 Main 线程 Event Loop 的细节,看到 JS 执行和渲染的详细过程。
有时你可能会看到有的 Task 部分被标红了,还警告说这是 Long Task。因为渲染和 JS 执行都是在同一个 Event Loop 内做的,那如果有执行时间过长的 Task,自然会导致渲染被延后,也就是掉帧,用户感受到的就是页面的卡顿。
避免 Long Task,这是网页性能优化的一个重点。这也是为什么 React 使用了 Fiber 架构的可打断的组件树渲染,替代掉了之前的递归渲染整个组件树的方式,就是为了不产生 Long Task。
总结
本文目的为了证明渲染是不是一个宏任务,但其实更重要的是想讲清楚调试工具的意义。
调试工具可以分析程序运行过程中某一刻或某一段时间的各方面的数据,有两种方式:一种是 Debugger 那种断点的方式,可以看到上下文变量的值、调用栈,可以帮助理清逻辑、定位问题。而性能分析工具则是另一种方式,通过录制一段时间内的各种数据,做事后的分析和统计,这样能保证数据的真实性。
网页的性能分析工具 Performance 可以记录网页执行过程中的各个线程的执行情况,主要是主线程的 Event Loop 的执行过程,包括 JS 执行、渲染等。
通过 Performance,我们可以轻易的得出“渲染是一个宏任务”的结论。
就像在 Debugger 面前,JS 执行过程没有秘密一样。在 Performance 面前,网页的执行过程也同样没有秘密。
作者:zxg_神说要有光
来源:https://juejin.cn/post/7037839989018722340
技术选型,Vue和React的对比
1. MVVM和MVC
Vue是MVVM,React是MVC。
MVVM(Model-View-ViewModel)是在MVC(Model View Controller)的基础上,VM抽离Controller中展示的业务逻辑,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现。
也就是说MVVM实现的是业务逻辑组件的重用,使开发更高效,结构更清晰,增加代码的复用性。
可以理解为MVVM是MVC的升级版。
虽然React不算一个完整的MVC框架,可以认为是MVC中的V(View),但是Vue的MVVM还是更面向未来一些。
2. 数据绑定
vue是双向绑定,react是单向绑定。
单向绑定的优点是相应的可以带来单向数据流,这样做的好处是所有状态变化都可以被记录、跟踪,状态变化通过手动调用通知,源头易追溯,没有“暗箱操作”。同时组件数据只有唯一的入口和出口,使得程序更直观更容易理解,有利于项目的可维护性。
但是Vue虽然是双向绑定,但是也是单向数据流,它的双向绑定只是一个语法糖,想看正经的双向绑定可以去看下Dva。
单向绑定的缺点则是代码量会相应的上升,数据的流转过程变长,从而出现很多类似的重复代码。同时由于对应用状态独立管理的严格要求(单一的全局store),在处理局部状态较多的场景时(如用户输入交互较多的“富表单型”应用),会显得冗余。
双向绑定可以在表单交互较多的场景下,会简化大量业务无关的代码。
我认为Vue的设计方案好一些,全局性数据流使用单向,局部性数据流使用双向。
3. 数据更新
3.1 React 更新流程
React 推崇 Immutable(不可变),通过重新render去发现和更新自身。
3.2 Vue 更新流程
Vue通过收集数据依赖去发现更新。
Vue很吸引人的就是它的响应式更新,Vue首次渲染触发data的getter,从而触发依赖收集,为对应的数据创建watcher,当数据发生更改的时候,setter被触发,然后通知各个watcher在下个tick的时候更新数据。
所以说,如果data中某些数据没有在模板template 中使用的话,更新这些数据的时候,是不会触发更新的。这样的设计非常好,没有在模版上用到的变量,当它的值发生变化时,不更新视图,相当于内置了React的shouldComponentUpdate。
3.3 更新比较
获取数据更新的手段和更新的粒度不一样
Vue通过依赖收集,当数据更新时 ,Vue明确知道是哪些数据更新了,每个组件都有自己的渲渲染watcher,掌管当前组件的视图更新,所以可以精确地更新对应的组件,所以更新的粒度是组件级别的。
React会递归地把所有的子组件重新render一下,不管是不是更新的数据,此时,都是新的。然后通过 diff 算法 来决定更新哪部分的视图。所以,React 的更新粒度是一个整体。
对更新数据是否需要渲染页面的处理不一样
只有依赖收集的数据发生更新,Vue 才会去重新渲染页面
只要数据有更新(setState,useState 等手段触发更新),都会去重新渲染页面 (可以使用shouldComponentUpdate/ PureComponent 改善)
Vue的文档里有一描述说,Vue是细粒度数据响应机制,所以说数据更新这一块,我认为Vue的设计方案好一些。
4. 性能对比
借用尤大大的一段话:
模板在性能这块吊打 tsx,在 IDE 支持抹平了的前提下用 tsx 本质上是在为了开发者的偏好牺牲用户体验的性能(性能没遇到瓶颈就无所谓) 这边自己不维护框架的人吐槽吐槽我也能理解,毕竟作为使用者只需要考虑自己爽不爽。作为维护者,Vue 的已有的用户习惯、生态和历史包袱摆在那里,能激进的程度是有限的,Vue 3 的大部分设计都是戴着镣铐跳舞,需要做很多折衷。如果真要激进还不如开个新项目,或者没人用的玩票项目,想怎么设计都可以。 组件泛型的问题也有不少人提出了,这个目前确实不行,但不表示以后不会有。 最后实话实说,所有前端里面像这个问题下面的类型体操运动员们毕竟是少数,绝大部分有 intellisense + 类型校验就满足需求了。真的对类型特别特别较真的用 React 也没什么不好,无非就是性能差点。
为什么模板性能吊打TSX?
tsx和vue template其实都是一样的模版语言,tsx最终也会被编译成createElement,模板被编译成render函数,所以本质上两者都有compile-time和runtime,但tsx的特殊性在于它本身是在ts语义下的,过于灵活导致优化无从下手。但是vue的模板得益于自身本来就是DSL,有自己的文法和语义,所以vue在模板的compile-time做了巨多的优化,比如提升不变的vnode,以及blocktree配合patchflag靶向更新,这些优化在最终的runtime上会把性能拉开不少。
DSL: 一种为特定领域设计的,具有受限表达性的编程语言。
所以说Vue的性能是优于React的。
5. React Hooks和Vue Hooks
其实 React Hook 的限制非常多,比如官方文档中就专门有一个章节介绍它的限制:
不要在循环,条件或嵌套函数中调用 Hook
确保总是在你的 React 函数的最顶层调用他们。
遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
而 Vue 带来的不同在于:
与 React Hooks 相同级别的逻辑组合功能,但有一些重要的区别。 与 React Hook 不同,setup函数仅被调用一次,这在性能上比较占优。
对调用顺序没什么要求,每次渲染中不会反复调用 Hook 函数,产生的的 GC 压力较小。
不必考虑几乎总是需要 useCallback 的问题,以防止传递函数prop给子组件的引用变化,导致无必要的重新渲染。
React Hook 有臭名昭著的闭包陷阱问题,如果用户忘记传递正确的依赖项数组,useEffect 和 useMemo 可能会捕获过时的变量,这不受此问题的影响。 Vue 的自动依赖关系跟踪确保观察者和计算值始终正确无误。
不得不提一句,React Hook 里的「依赖」是需要你去手动声明的,而且官方提供了一个 eslint 插件,这个插件虽然大部分时候挺有用的,但是有时候也特别烦人,需要你手动加一行丑陋的注释去关闭它。
我们认可 React Hooks 的创造力,这也是 Vue-Composition-Api 的主要灵感来源。上面提到的问题确实存在于 React Hook 的设计中,我们注意到 Vue 的响应式模型恰好完美的解决了这些问题。
--- 来自ssh
Vue的组合式API刚出来的时候确实一看好像React Hooks,我也对它的.value进行了吐槽,
但是总体来说还是更偏向于Vue Hooks。
6. 写法
React的思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等,所以说React的写法感觉相对自由一些,逻辑正确老子想怎么写怎么写,对于我来说,我确实更偏向于React的写法。
Vue则是把html,css,js组合到一起,就像 Web 开发多年的传统开发方式一样, vue-loader会解析文件,提取每个语言块用各自的处理方式,vue有单文件组件(SFC),可以把html、css、js写到一个文件中,html提供了模板引擎来处理。Vue感觉是给你搭了一个框架,告诉你什么地方该写什么东西,你只要按照他的要求向里面填内容就可以了,没有React那么自由,但是上手难度简单了许多。而且因为SFC,一个组件的代码会看起来很长,维护起来很头痛。
7. 理念及设计
Vue 和 React 的核心差异,以及核心差异对后续设计产生的“不可逆”影响。
Vue 和 React 在 API 设计风格和哲学理念(甚至作者个人魅力)上的不同。
Vue 和 React 在工程化预编译构建阶段,AOT 和 JIT 优化的本质差异和设计。
这个层次的比较确实对我难度确实大,我也懒得去copy,下面是Lucas大佬的分析,可以去看一下,时空隧道。
作者:黑色的枫
来源:https://juejin.cn/post/7037365650251055134
微前端-从了解到动手搭建
前言
微前端是 2016 年thoughtWorks提出的概念,它将微服务的理念应用于浏览器端,即将前端应用由单体应用转变成多个小型前端应用聚合的应用。各个小型前端应用可以独立运行、独立开发、独立部署。
为什么出现?
与微服务出现的原因相似,随着前端业务越来越复杂,前端的代码和业务逻辑也愈发难以维护,尤其对于中后台系统,很容易出现巨石应用,微前端由此应运而生,其根本目的就是解决巨石应用的项目复杂,系统庞大,开发人员众多,难以维护的问题。
微前端 vs 巨石应用
微前端 | 巨石应用 | |
---|---|---|
可维护性 | 拆分为框架应用、微应用、微模块后,每个业务页面都对应一个单独的仓库,应用风险性降低。 | 所有页面都在一个仓库,经常会出现动一处则动全身,随着系统增大维护成本会逐渐升高。 |
开发效率 | 结合发布、回滚、团队协作三个方面来看,单个仓库只关心一个业务页面,可以更方便快速迭代。 | 团队多人协作时,发布排队;回滚有可能会把其他人发布的代码同时回滚掉;多分支开发时发布前沟通增加成本。 |
代码复用 | 所有页面都分开维护,使用公用代码成本较大,不过共用代码抽离为npm包使用可以减小成本。 | 一个仓库中很容易抽离公用的部分,但是要注意动一处就会动全身的结果。 |
架构方案
基座模式是当前比较常见的微前端架构设计。
首先以容器应用作为整个项目的主应用,负责子应用的注册,聚合,提供子运行环境、管理生命周期等。子应用就是各个独立部署、独立开发的单元。
应用注册表拥有每个应用及对应的入口。在前端领域里,入口的直接表现形式可以是路由,又或者对应的应用映射。
目前可以实现微前端架构的方案有如下:
HTTP后端路由转发(nginx)
✅ 简单高效快速,同时不需要前端做额外的工作。
❌ 体验并不好,相当于mpa页面,路由到每个应用需要重新刷新
iframe
✅ 前端最简单的应用方式,直接嵌入,门槛最低,改动最小
❌ iframe都会遇到的一些典型问题:UI 不同步,DOM 结构不共享(比如iframe中的弹框),跨域通信等
各个业务独立打到npm包中
✅ 门槛低,易上手
❌ 模块修改后需要重新部署发布,太麻烦。
组合式应用路由分发(基座模式)
✅ 纯前端改造,体验良好,各个业务相互独立
❌ 需要设计和开发,有一定成本,同时需要兼顾子页面和基座的变量污染,样式互相影响等问题
web component
✅ 是一项标准,目前它包含三项主要技术,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。应该是微前端的最终态
❌ 比较新,兼容性较差
微前端页面形态
微前端基座框架需要解决的问题
路由分发
作为微前端的基座应用,是整个应用的入口,负责承载当前子应用的展示和对其他路由子应用的转发,对于当前子应用的展示,一般是由以下几步构成:
远程拉取子应用内容
将子应用的 js 和 css 抽离,采用eval来运行 js,并将 css 和 html 内容append到基座应用中留给子应用的展示区域
当子应用切换走时,同步卸载这些内容
对于路由分发而言,以采用react-router开发的基座SPA应用来举例,主要是下面这个流程:
当浏览器的路径变化后,react-router会监听hashchange或者popstate事件,从而获取到路由切换的时机。
最先接收到这个变化的是基座的router,通过查询注册信息可以获取到转发到那个子应用,经过一些逻辑处理后,采用修改hash方法或者pushState方法来路由信息推送给子应用的路由,子应用可以是手动监听hashchange或者popstate事件接收,或者采用react-router接管路由,后面的逻辑就由子应用自己控制。
应用隔离
应用隔离问题主要分为主应用和子应用,子应用和子应用之间的JavaScript执行环境隔离,CSS样式隔离,
CSS
当主应用和子应用同屏渲染时,就可能会有一些样式会相互污染,如果要彻底隔离CSS污染,可以采用CSS Module 或者命名空间的方式,给每个子应用模块以特定前缀,即可保证不会互相干扰,可以采用webpack的postcss插件,在打包时添加特定的前缀。
而对于子应用与子应用之间的CSS隔离就非常简单,在每次应用加载时,将该应用所有的link和style 内容进行标记。在应用卸载后,同步卸载页面上对应的link和style即可。
JavaScript隔离
每当子应用的JavaScript被加载并运行时,它的核心实际上是对全局对象Window的修改以及一些全局事件的改变,例如jQuery这个js运行后,会在Window上挂载一个window.$对象,对于其他库React,Vue也不例外。为此,需要在加载和卸载每个子应用的同时,尽可能消除这种冲突和影响,最普遍的做法是采用沙箱机制(SandBox)。
沙箱机制的核心是让局部的JavaScript运行时,对外部对象的访问和修改处在可控的范围内,即无论内部怎么运行,都不会影响外部的对象,需要结合 with 关键字和window.Proxy对象来实现浏览器端的沙箱。
消息通信
应用间通信有很多种方式,当然,要让多个分离的子应用之间要做到通信,本质上仍离不开中间媒介或者说全局对象。所以对于消息订阅(pub/sub)模式的通信机制是非常适用的,在基座应用中会定义事件中心Event,每个子应用分别来注册事件,当被触发事件时再有事件中心统一分发,这就构成了基本的通信机制。
当然,如果基座和子应用采用的是React或者是Vue,是可以结合Redux和Vuex来一起使用,实现应用之间的通信。
搭一个看看?
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。也是支付宝内部广泛使用的微前端框架。
那么我们就使用 qiankun 从头搭一个demo出来体验一下
基座
基座我们使用react,自行使用 create-react-app 创建一个react项目即可。
npm install qiankun -s
在基座中需要调用 registerMicroApps 注册子应用,然后调用start启动
因此在 index.js 中插入如下代码
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'vueApp',
entry: '//localhost:8080',
container: '#container',
activeRule: '/app-vue',
},
]);
// 启动 qiankun
start();
修改App.js
加入一些 antd 元素,让demo像样一些
同时,由于qiankun根据路由来加载不同微应用,我们也安装 react-router-dom
npm install react-router-dom
安装完之后修改 App.js 如下:
import { useState } from 'react';
import { Layout, Menu } from 'antd';
import { PieChartOutlined } from '@ant-design/icons';
import { Link } from 'react-router-dom'
import './App.css';
const { Header, Content, Footer, Sider } = Layout;
const App = () => {
const [collapsed, setCollapsed] = useState(false);
const onCollapse = collapsed => {
setCollapsed(collapsed);
};
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse}>
<div className="logo" />
<Menu theme="dark" defaultSelectedKeys={['1']} mode="inline">
<Menu.Item key="1" icon={<PieChartOutlined />}>
<Link to="/app-vue">Vue应用</Link>
</Menu.Item>
</Menu>
</Sider>
<Layout className="site-layout">
<Header className="site-layout-background" style={{ padding: 0 }} />
<Content style={{ margin: '16px' }}>
<div id="container" className="site-layout-background" style={{ minHeight: 360 }}></div>
</Content>
<Footer style={{ textAlign: 'center' }}>This Project ©2021 Created by DiDi</Footer>
</Layout>
</Layout>
);
}
export default App;
记得修改 index.js,把 App 组件用 react-router-dom 的 BrowserRouter 包一层,让 BrowserRouter 作为顶层组件才可以跳转
至此,基座搭好了
子页面
尝试使用vue作为子页面,来体现微前端的技术隔离性。
使用vue-cli创建vue2.x项目
修改main.js如下:
import Vue from "vue/dist/vue.js";
import App from "./App.vue";
import router from "./router";
Vue.config.productionTip = false;
// window.__POWERED_BY_QIANKUN__ 为true 说明在 qiankun 架构中
// 修改webpack的publicPath,将子应用资源加载的公共基础路径设为 qiankun 包装后的路径
// 这个 __INJECTED_PUBLIC_PATH_BY_QIANKUN__ 的实际地址是子应用的服务器地址,子应用的应用资源都在他本身的实际服务器上
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
router,
render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}
// 独立运行时 直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// 应用需要导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
export async function bootstrap() {
console.log("[vue] vue app bootstraped");
}
export async function mount(props) {
console.log("[vue] props from main framework", props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = "";
instance = null;
}
router.js配置如下:
import Vue from "vue/dist/vue.js";
import VueRouter from "vue-router";
Vue.use(VueRouter);
const routes = [
{
path: "/test",
name: "Test",
component: () => import("./components/Test.vue"),
},
{
path: "/hello",
name: "Hello",
component: () => import("./components/Hello.vue"),
},
];
const router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? "/app-vue/" : "/",
mode: "history",
routes,
});
export default router;
根目录下新建vue.config.js 用来配置webpack,内容如下:
const { name } = require("./package");
module.exports = {
devServer: {
// 跨域
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
// 把微应用打包成 umd 库格式
libraryTarget: "umd",
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
启动
基座和子应用分别启动,可以看到,子应用已经加载到了主应用中:
作者:visa
来源:https://juejin.cn/post/7037386845751083021
实现一个逐步递增的数字动画
背景
可视化大屏项目使用最多的组件就是数字组件,展示数据的一个变化,为了提高视觉效果,需要给数字增加一个滚动效果,实现一个数字到另一个数字逐步递增的滚动动画。
先上一个思维导图:
一、实现类似滚轮的效果,容器固定,数字向上滚动
先列举所有的可能的值形成一个纵向的列表,然后固定一个容器,匀速更改数字的偏移值。
下面来介绍一下这种方案的实现,元素值从0到9一共十个值,每个数字占纵向列表的10%,所以纵向偏移值依次为为0% —> -90%
实现:
<ul>
<li>
<span>0123456789</span>
</li>
</ul>
ul{
margin-top: 200px;
}
ul li{
margin:0 auto;
width: 20px;
height: 30px;
text-align: center;
border:2px solid rgba(221,221,221,1);
border-radius:4px;
}
ul li span{
position: absolute;
color: #fff;
top: 30%;
left: 50%;
transform: translate(-50%,0);
transition: transform 500ms ease-in-out;
writing-mode: vertical-rl;
text-orientation: upright;
letter-spacing: 17px;
}
let spanDom = document.querySelector('span')
let start = 0
setInterval(() =>{
start++
if(start>9){
start = 0
}
spanDom.style.transform = `translate(-50%,-${start*10}%)`
}, 1000)
上述代码存在一个问题,当我们从9到0的时候,容器偏移从-90%直接到了0%。 但是由于设定了固定的过渡动画时间,就会出现一个向反方向滚动的情况,为了解决这个问题,可以参考无缝滚动的思路
在9后面复制一份0,
当纵向列表滚动到9的时候,继续滚动到复制的0
滚动到复制的0的时候,把列表的偏移位置改为0,并且控制动画时间为0
<ul>
<li>
<span>01234567890</span>
</li>
</ul>
let spanDom = document.querySelector('span')
let start = 0
var timer = setInterval(fn, 1000);
function fn() {
start++
clearInterval(timer)
timer = setInterval(fn,start >10 ? 0 : 1000);
if(start>10){
spanDom.style.transition = `none`
start = 0
}else{
spanDom.style.transition = `transform 500ms ease-in-out`
}
spanDom.style.transform = `translate(-50%,-${start/11*100}%)`
}
利用两个元素实现滚动
仔细看动图的效果,事实上在在视口只有两个元素,一个值之前的值,一个为当前的值,滚动偏移值只需设置translateY(-100%)
具体思路:
声明两个变量,分别存放之前的值
prev
,以及变化后的值cur
;声明一个变量play
作为这两个值的滚动动画的开关
使用
useEffect
监听监听传入的值:如果是有效的数字,那么把没有变化前的值赋值给prev
,把当前传入的值赋值给cur
,并且设置paly
为true
开启滚动动画
下面是调整后的代码结构:
<div className={styles.slider}>
{[prev, cur].map((item, index) => (
<span key={index} className={`${styles['slider-text']} ${playing && styles['slider-ani']} ${(prev === 0 && cur === 0 && index ===0) && styles['slider-hide']}`}>
{item}
</span>
))}
</div>
const { value} = props
const [prev, setPrev] = useState(0)
const [cur, setCur] = useState(0)
const [playing, setPlaying] = useState(false)
const play = (pre, current) => {
setPrev(pre)
setCur(current)
setPlaying(false)
setTimeout(() => {
setPlaying(true)
}, 20)
}
useEffect(() => {
if (!Number.isNaN(value)) {
play(cur, value)
} else {
setPrev(value)
setCur(value)
}
}, [value])
.slider {
display: flex;
flex-direction: column;
height: 36px;
margin-top: 24%;
overflow: hidden;
text-align: left;
}
.slider-text {
display: block;
height: 100%;
transform: translateY(0%);
}
.slider-ani {
transform: translateY(-100%);
transition: transform 1s ease;
}
.slider-hide {
opacity: 0;
}
实现多个滚轮的向上滚动的数字组件
利用H5的requestAnimationFrame()API实现数字逐步递增的动画效果
实现一个数字的逐渐递增的滚动动画,并且要在指定时间内完成。要看到流畅的动画效果,就需要在更新元素状态时以一定的频率进行,JS动画都是通过在很短的时间内不停的渲染/绘制元素做到的,所以计时器一直都是Javascript动画的核心技术,关键就是刷新的间隔时间,刷新时间需要尽量短,这样动画效果才能显得更加流畅,不卡顿;同时刷新间隔又不能太短,需要确保浏览器有能力渲染动画 。
大多数电脑显示器的刷新频率是 60Hz,即每秒重绘 60次。因此平滑动画的最佳循环间隔是通常是 1000ms/60,约等于16.6ms
计时器对比
与 setTimeout 和 setInterval 不同,requestAnimationFrame 不需要程序员自己设置时间间隔。setTimeout 和 setInterval 的问题是精确度低。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器 UI 线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。
requestAnimationFrame 采用系统时间间隔,它会要求浏览器根据自己的频率进行一次重绘,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。
requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。
requestAnimationFrame 对于隐藏或不可见元素,将不会进行重绘或回流,就意味着使用更少的 CPU、GPU 和内存使用量。
requestAnimationFrame 是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。
requestAnimationFrame实现滚动动画思路
- 动画开始,记录开始动画的时间
startTimeRef.current
const startTimeRef = useRef(Date.now());
const [t, setT] = useState(Date.now());
- 之后每一帧动画,记录从开始动画经过了多长时间,计算出当前帧的所到达的数字应该是多少,即
currentValue
useEffect(() => {
const rafFunc = () => {
const now = Date.now();
const t = now - startTimeRef.current;
if (t >= period) {
setT(period);
} else {
setT(t);
requestAnimationFrame(rafFunc);
}
};
let raf;
if (autoScroll) {
raf = requestAnimationFrame(rafFunc);
startTimeRef.current = Date.now();
} else {
raf && cancelAnimationFrame(raf);
}
return () => raf && cancelAnimationFrame(raf);
}, [period, autoScroll]);
const currentValue = useMemo(() => ((to - from) / period) * t + from, [t, period, from, to]);
- 针对当前每个数字位上的数字进行比较,如果有变化,进行偏移量的变化,偏移量体现在当前数字位上的数字与下一位数字之间的差值,这个变化每一帧都串起来形成了滚动动画
成果展示
作者:我就是胖虎
链接:https://juejin.cn/post/7025913017627836452
收起阅读 »
前端金额格式化处理
前端项目中,金额格式化展示是很常见的需求,在此整理了一些通用的处理方式,如 toLocaleString();正则匹配;slice()循环截取等等;也解决了小数点精度问题
以此为例:12341234.246
=> ¥ 12,341,234.25
方式一:采用浏览器自带的Number.prototype.toLocaleString()处理整数部分,小数部分直接用Number.prototype.toFixed()四舍五入处理
// v1.0
const formatMoney = (money, symbol = "", decimals = 2) => {
if (!(money && money > 0)) {
return 0.0;
}
let arr = money.toFixed(decimals).toString().split(".");
let first = parseInt(arr[0]).toLocaleString();
let result = [first, arr[1]].join(".");
return `${symbol} ${money.toFixed(decimals)}`;
};
formatMoney(12341234.246); // 12,341,234.25
formatMoney(12341234.246, "¥", 1); // ¥ 12,341,234.2
2021.11.9 更改记录 我之前写复杂了,经过评论区[黄景圣]的指点,优化如下:
// v2.0 简化函数
const formatMoney = (money, symbol = "", decimals = 2) =>
`${symbol} ${parseFloat(money.toFixed(decimals)).toLocaleString()}`;
formatMoney(12341234.246, "¥", 2) // ¥ 12,341,234.25
// 或者只用toLocaleString()处理
const format = (money, decimals = 2) =>
money.toLocaleString("zh", {
style: "currency",
currency: "CNY",
maximumFractionDigits: decimals,
useGrouping: true, // false-没有千位分隔符;true-有千位分隔符
});
format(12341234.246); // ¥12,341,234.25
2021.11.10 更改记录 经过评论区[你摸摸我这料子]的提示,解决了 toFixed() 精度失效的问题,具体可查看前端小数展示精度处理
// 测试数据如下:
formatMoney(12.035); // 12.04 正常四舍五入
formatMoney(12.045); // 12.04 异常,应该为12.05,没有四舍五入
// v3.0 解决toFixed()问题
const formatToFixed = (money, decimals = 2) => {
return (
Math.round((parseFloat(money) + Number.EPSILON) * Math.pow(10, decimals)) /
Math.pow(10, decimals)
).toFixed(decimals);
};
const formatMoney = (money, symbol = "", decimals = 2) =>
`${symbol}${parseFloat(formatToFixed(money, decimals)).toLocaleString()}`;
formatMoney(12341234.035, '¥'); // ¥12,341,234.04
formatMoney(12341234.045, '¥'); // ¥12,341,234.05
2021.11.17 更改记录 通过评论区[Ryan_zhang]的提醒,解决了保留四位小数显示的问题
// v4.0 只更改了formatMoney函数,其他的不变
const formatMoney = (money, symbol = "", decimals = 2) =>
`${symbol}${parseFloat(formatToFixed(money, decimals)).toLocaleString(
"zh",
{
maximumFractionDigits: decimals,
useGrouping: true,
}
)}`;
formatMoney(12341234.12335, "¥", 4); // ¥12,341,234.1234
formatMoney(12341234.12345, "¥", 4); // ¥12,341,234.1235
方式二:使用正则表达式处理整数部分,小数部分同上所示。有个《JS 正则迷你书》介绍正则表达式挺好的,在 2.4.2 章就讲了“数字的千位分隔符表示法”,介绍的很详细,推荐看看。
\b
:单词边界,具体就是 \w 与 \W 之间的位置,也包括 \w 与 ^ 之间的位置,和 \w 与 $ 之间的位置\B
:\b 的反面的意思,非单词边界(?=p)
:其中 p 是一个子模式,即 p 前面的位置,或者说,该位置后面的字符要匹配 p
/**
* @params {Number} money 金额
* @params {Number} decimals 保留小数点后位数
* @params {String} symbol 前置符号
*/
const formatMoney = (money, symbol = "", decimals = 2) => {
let result = money
.toFixed(decimals)
.replace(/\B(?=(\d{3})+\b)/g, ",")
.replace(/^/, `${symbol}`);
return result;
};
formatMoney(12341234.246, "$", 2); // $12,341,234.25
// v2.0 解决toFixed()问题
const formatMoneyNew = (money, symbol = "", decimals = 2) =>
formatToFixed(money, decimals)
.replace(/\B(?=(\d{3})+\b)/g, ",")
.replace(/^/, `${symbol}`);
formatMoneyNew(12341234.035, "¥", 2); // ¥12,341,234.04
formatMoneyNew(12341234.045, "¥", 2); // ¥12,341,234.05
方式三:循环字符串,通过 slice 截取实现
- substring(start, end):包含 start,不包含 end
- substr(start, length):包含 start,长度为 length
- slice(start, end):可操作数组和字符串;包含 start,不包含 end
- splice(start, length, items):只能针对数组;增删改都可以
const formatMoney = (money, symbol = "", decimals = 2) => {
// 改造前
// let arr = money.toFixed(decimals).toString().split(".");
// 改造后
let arr = formatToFixed(money, decimals).toString().split(".");
let num = arr[0];
let first = "";
su;
while (num.length > 3) {
first = "," + num.slice(-3) + first;
num = num.slice(0, num.length - 3);
}
if (num) {
first = num + first;
}
return `${symbol} ${[first, arr[1]].join(".")}`;
};
formatMoney(12341234.246, "$", 2); // $ 12,341,234.25
formatMoney(12341234.035, "¥", 2); // ¥ 12,341,234.04
formatMoney(12341234.045, "¥", 2); // ¥ 12,341,234.05
2021.11.24 更改记录 通过评论区[SpriteBoy]和[maxxx]的提醒,采用Intl内置的NumberFormat试试
方式四:Intl.NumberFormat,用法和toLocaleString()挺相似的
const formatMoney = (money, decimals = 2) => {
return new Intl.NumberFormat("zh-CN", {
style: "currency", // 货币形式
currency: "CNY", // "CNY"是人民币
currencyDisplay: "symbol", // 默认“symbol”,中文中代表“¥”符号
// useGrouping: true, // 是否使用分组分隔符,如千位分隔符或千/万/亿分隔符,默认为true
// minimumIntegerDigits: 1, // 使用的整数数字的最小数目.可能的值是从1到21,默认值是1
// minimumFractionDigits: 2, // 使用的小数位数的最小数目.可能的值是从 0 到 20
maximumFractionDigits: decimals, // 使用的小数位数的最大数目。可能的值是从 0 到 20
}).format(money);
};
console.log(formatMoney(12341234.2, 2)); // ¥12,341,234.20
console.log(formatMoney(12341234.246, 1)); // ¥12,341,234.2
console.log(formatMoney(12341234.035, 2)); // ¥12,341,234.04
console.log(formatMoney(12341234.045, 2)); // ¥12,341,234.05
console.log(formatMoney(12341234.12335, 4)); // ¥12,341,234.1234
console.log(formatMoney(12341234.12345, 4)); // ¥12,341,234.1235
作者:时光足迹
链接:https://juejin.cn/post/7028086399601475591
收起阅读 »
清空数组的几个方式
1. 前言
前两天在工作当中遇到一个问题,在vue3
中使用reactive
生成的响应式数组如何清空,当然我一般清空都是这么写:
let array = [1,2,3];
array = [];
不过这么用在reactive
代理的方式中还是有点问题,比如这样:
let array = reactive([1,2,3]);
watch(()=>[...array],()=>{
console.log(array);
},)
array = reactive([]);
很显然,因为丢失了对原来响应式
对象的引用,这样就直接失去了监听
。
2. 清空数据的几种方式
当然,作为一名十年代码经验常年摸鱼的我,立马就给出了几个解决方案。
2.1 使用ref()
使用ref
,这是最简便的方法:
const array = ref([1,2,3]);
watch(array,()=>{
console.log(array.value);
},)
array.value = [];
2.2 使用slice
slice
顾名思义,就是对数组进行切片
,然后返回一个新数组
,感觉和go
语言的切片
有点类似。当然用过react
的小伙伴应该经常用slice
,清空一个数组只需要这样写:
const array = ref([1,2,3]);
watch(array,()=>{
console.log(array.value);
},)
array.value = array.value.slice(0,0);
不过需要注意要使用ref
。
2.3 length赋值为0
个人比较喜欢这种,直接将length
赋值为0
:
const array = ref([1,2,3]);
watch(array,()=>{
console.log(array.value);
},{
deep:true
})
array.value.length = 0;
而且,这种只会触发一次,但是需要注意watch
要开启deep
:
不过,这种方式,使用reactive
会更加方便,也不用开启deep
:
const array = reactive([1,2,3]);
watch(()=>[...array],()=>{
console.log(array);
})
array.length = 0;
2.4 使用splice
副作用
函数splice
也是一种方案,这种情况同时也可以使用reactive
:
const array = reactive([1,2,3]);
watch(()=>[...array],()=>{
console.log(array);
},)
array.splice(0,array.length)
不过要注意,watch
会触发多次:
当然也可以使用ref
,但是注意这种情况下,需要开启deep
:
const array = ref([1,2,3]);
watch(array,()=>{
console.log(array.value);
},{
deep:true
})
array.value.splice(0,array.value.length)
但是可以看到ref
也和reactive
一样,会触发多次。
3. 总结
以上是我个人工作中
的对于清空数组
的总结,但是可以看到splice
还是有点特殊的,会触发多次,不过为什么会产生这种差异还有待研究。
作者:RadiumAg
链接:https://juejin.cn/post/7028086044285206564
收起阅读 »
手写一个 ts-node 来深入理解它的原理
当我们用 Typesript 来写 Node.js 的代码,写完代码之后要用 tsc 作编译,之后再用 Node.js 来跑,这样比较麻烦,所以我们会用 ts-node 来直接跑 ts 代码,省去了编译阶段。
有没有觉得很神奇,ts-node 怎么做到的直接跑 ts 代码的?
其实原理并不难,今天我们来实现一个 ts-node 吧。
相关基础
实现 ts-node 需要 3 方面的基础知识:
- require hook
- repl 模块、vm 模块
- ts compiler api
我们先学下这些基础
require hook
Node.js 当 require 一个 js 模块的时候,内部会分别调用 Module.load、 Module._extensions[‘.js’],Module._compile 这三个方法,然后才是执行。
同理,ts 模块、json 模块等也是一样的流程,那么我们只需要修改 Module._extensions[扩展名] 的方法,就能达到 hook 的目的:
require.extensions['.ts'] = function(module, filename) {
// 修改代码
module._compile(修改后的代码, filename);
}
比如上面我们注册了 ts 的处理函数,这样当处理 ts 模块时就会调用这个方法,所以我们在这里面做编译就可以了,这就是 ts-node 能够直接执行 ts 的原理。
repl 模块
Node.js 提供了 repl 模块可以创建 Read、Evaluate、Print、Loop 的命令行交互环境,就是那种一问一答的方式。ts-node 也支持 repl 的模式,可以直接写 ts 代码然后执行,原理就是基于 repl 模块做的扩展。
repl 的 api 是这样的: 通过 start 方法来创建一个 repl 的交互,可以指定提示符 prompt,可以自己实现 eval 的处理逻辑:
const repl = require('repl');
const r = repl.start({
prompt: '- . - > ',
eval: myEval
});
function myEval(cmd, context, filename, callback) {
// 对输入的命令做处理
callback(null, 处理后的内容);
}
repl 的执行时有一个上下文的,在这里就是 r.context,我们在这个上下文里执行代码要使用 vm 模块:
const vm = require('vm');
const res = vm.runInContext(要执行的代码, r.context);
这两个模块结合,就可以实现一问一答的命令行交互,而且 ts 的编译也可以放在 eval 的时候做,这样就实现了直接执行 ts 代码。
ts compiler api
ts 的编译我们主要是使用 tsc 的命令行工具,但其实它同样也提供了编译的 api,叫做 ts compiler api。我们做工具的时候就需要直接调用 compiler api 来做编译。
转换 ts 代码为 js 代码的 api 是这个:
const { outputText } = ts.transpileModule(ts代码, {
compilerOptions: {
strict: false,
sourceMap: false,
// 其他编译选项
}
});
当然,ts 也提供了类型检查的 api,因为参数比较多,我们后面一篇文章再做展开,这里只了解 transpileModule 的 api 就够了。
了解了 require hook、repl 和 vm、ts compiler api 这三方面的知识之后,ts-node 的实现原理就呼之欲出了,接下来我们就来实现一下。
实现 ts-node
直接执行的模式
我们可以使用 ts-node + 某个 ts 文件,来直接执行这个 ts 文件,它的原理就是修改了 require hook,也就是 Module._extensions['.ts']
来实现的。
在 require hook 里面做 ts 的编译,然后后面直接执行编译后的 js,这样就能达到直接执行 ts 文件的效果。
所以我们重写 Module._extensions['.ts']
方法,在里面读取文件内容,然后调用 ts.transpileModule 来把 ts 转成 js,之后调用 Module._compile 来处理编译后的 js。
这样,我们就可以直接执行 ts 模块了,具体的模块路径是通过命令行参数执行的,可以用 process.argv 来取。
const path = require('path');
const ts = require('typescript');
const fs = require('fs');
const filePath = process.argv[2];
require.extensions['.ts'] = function(module, filename) {
const fileFullPath = path.resolve(__dirname, filename);
const content = fs.readFileSync(fileFullPath, 'utf-8');
const { outputText } = ts.transpileModule(content, {
compilerOptions: require('./tsconfig.json')
});
module._compile(outputText, filename);
}
require(filePath);
我们准备一个这样的 ts 文件 test.ts:
const a = 1;
const b = 2;
function add(a: number, b: number): number {
return a + b;
}
console.log(add(a, b));
然后用这个工具 hook.js 来跑:
可以看到,成功的执行了 ts,这就是 ts-node 的原理。
当然,细节的逻辑还有很多,但是最主要的原理就是 require hook + ts compiler api。
repl 模式
ts-node 支持启动一个 repl 的环境,交互式的输入 ts 代码然后执行,它的原理就是基于 Node.js 提供的 repl 模块做的扩展,在自定义的 eval 函数里面做了 ts 的编译,然后使用 vm.runInContext 的 api 在 repl 的上下文中执行 js 代码。
我们也启动一个 repl 的环境,设置提示符和自定义的 eval 实现。
const repl = require('repl');
const r = repl.start({
prompt: '- . - > ',
eval: myEval
});
function myEval(cmd, context, filename, callback) {
}
eval 的实现就是编译 ts 代码为 js,然后用 vm.runInContext 来执行编译后的 js 代码,执行的 context 指定为 repl 的 context:
function myEval(cmd, context, filename, callback) {
const { outputText } = ts.transpileModule(cmd, {
compilerOptions: {
strict: false,
sourceMap: false
}
});
const res = vm.runInContext(outputText, r.context);
callback(null, res);
}
同时,我们还可以对 repl 的 context 做一些扩展,比如注入一个 who 的环境变量:
Object.defineProperty(r.context, 'who', {
configurable: false,
enumerable: true,
value: '神说要有光'
});
我们来测试下效果:
可以看到,执行后启动了一个 repl 环境,提示符修改成了 -.- >,可以直接执行 ts 代码,还可以访问全局变量 who。
这就是 ts-node 的 repl 模式的大概原理: repl + vm + ts compiler api。
全部代码如下:
const repl = require('repl');
const ts = require('typescript');
const vm = require('vm');
const r = repl.start({
prompt: '- . - > ',
eval: myEval
});
Object.defineProperty(r.context, 'who', {
configurable: false,
enumerable: true,
value: '神说要有光'
});
function myEval(cmd, context, filename, callback) {
const { outputText } = ts.transpileModule(cmd, {
compilerOptions: {
strict: false,
sourceMap: false
}
});
const res = vm.runInContext(outputText, r.context);
callback(null, res);
}
总结
ts-node 可以直接执行 ts 代码,不需要手动编译,为了深入理解它,我们我们实现了一个简易 ts-node,支持了直接执行和 repl 模式。
直接执行的原理是通过 require hook,也就是 Module._extensions[ext] 里通过 ts compiler api 对代码做转换,之后再执行,这样的效果就是可以直接执行 ts 代码。
repl 的原理是基于 Node.js 的 repl 模块做的扩展,可以定制提示符、上下文、eval 逻辑等,我们在 eval 里用 ts compiler api 做了编译,然后通过 vm.runInContext 在 repl 的 context 中执行编译后的 js。这样的效果就是可以在 repl 里直接执行 ts 代码。
当然,完整的 ts-node 还有很多细节,但是大概的原理我们已经懂了,而且还学到了 require hook、repl 和 vm 模块、 ts compiler api 等知识。
题外话
其实 ts-node 的原理是应一个同学的要求写的,大家有想读的 nodejs 工具的源码也可以告诉我呀(可以加我微信),无偿提供源码带读 + 简易实现的服务,不过会做一些筛选。
作者:zxg_神说要有光
来源:https://juejin.cn/post/7036688014206042143
为什么我不用 Typescript
前言
我算是久仰 Typescript 的大名了,因而之前就想学习,但是一直没有抽出时间来看看它。直到最近有一天我在知乎上被邀请回答了 一个问题 —— 一个我以为的中学生问怎么样提升他的开源仓库。我点进去,先是被惊艳到了;然后发现,他用的是 Typescript。我顿时感觉我似乎落后了,于是鼓起劲,开始学起了 Typescript。
但是我学了下,再用了下,发现它没有像被吹的那么神。虽说是 Javascript 的超集,也的确有些地方挺好的,但是还是不足够改变我使用 Javascript 编程。就像虽然有 Deno,但是我还是用 Node.js 一样。
所以,我就写这篇文章,说下我个人感觉 Typescript 的缺点、为何它的优点无法打动我用它替代 Javascript,以及跟推荐我使用 Typescript 的大家讲一下我不用 Typescript 的逻辑。
各位想骂我心里骂骂就好了,我今天过个生日也不容易。
缺陷
1. 语法丑陋,代码臃肿
我写两段相同的代码,大家感受下:
// js
const multiply = (i, j) => i * j
// ts
function multiply(i: number, j: number) {
return i + j
}
你看这类型注释,把好好的一段代码弄得这么乱……反正我看这样的 Typescript,花的反应时间一定比看上面的 Javascript 代码长。——不过也有可能是我比较熟悉 Javascript 吧。
复杂一点的东西也是一个道理(Apollo GraphQL 的代码):
// js
import React from 'react';
import ApolloClient from 'apollo-client';
let apolloContext;
export function getApolloContext() {
if (!apolloContext) {
apolloContext = React.createContext({});
}
return apolloContext;
}
export function resetApolloContext() {
apolloContext = React.createContext({});
}
铁定要比这个好得多:
// ts
import React from 'react';
import ApolloClient from 'apollo-client';
export interface ApolloContextValue {
client?: ApolloClient<object>;
renderPromises?: Record<any, any>;
}
let apolloContext: React.Context<ApolloContextValue>;
export function getApolloContext() {
if (!apolloContext) {
apolloContext = React.createContext<ApolloContextValue>({});
}
return apolloContext;
}
export function resetApolloContext() {
apolloContext = React.createContext<ApolloContextValue>({});
}
甚至有人提了个 issue 就是抱怨 Type 让它变得难用。
这么看,实在是为了这个假静态类型语言牺牲太多了,毕竟代码可读性还是很重要的。——之所以说它是假静态语言,是因为在真正的静态类型语言中,如 C 和 C++,不同的变量类型在内存中的存储方式不同,而在 Typescript 中不是这样。
比如,缺了这个可读性,debug 会变得更难。你是不是没有注意到我上面 multiply
的 Typescript 代码其实有 bug——应该是 *
而不是 +
。
2. 麻烦
浏览器不能直接执行 Typescript,所以 Typescript 必须要被编译成 Javascript 才能执行,要花一段时间;项目越大,花的时间越长,所以 Deno
才要停用它。并且,使用 Typescript 要安装新的依赖,虽然的确不麻烦,但是不用 Typescript,就不用再多装一个依赖了是不是。
其实还有一点,但是放不上台面来讲,因为这是我自己的问题。
我一直不大喜欢给添花样的东西,比如 pug
、typescript
、deno
等;虽然 scss
啥的我觉得还是不错的——没有它我就写不出 @knowscount/vue-lib 这个仓库。
3. 文件体积会变大
随随便便就能猜到,我写那么多额外的类型注释、代码变得那么臃肿肯定会让 Typescript 文件比用 Javascript 编写的文件更大。作为一个用 “tab 会让文件体积更小” 作为论据的 tab 党,我当然讨厌 Typescript 啦哈哈哈哈。
我理解在编译过后都是一样的,但是反正……我还是不爽。而且正是由于 TypeScript 会被编译到JavaScript 中,所以才会出现无论你的类型设计得多么仔细,还是有可能让不同的值类型潜入 JavaScript 变量中的问题。这是不可避免的,因为 JavaScript 仍然是没有类型的。
4. 报错使我老花
单纯吐槽一句,为什么它的报错那么丑,我就拼错了一个单词他给我又臭又长报一大段,还没有颜色。。
为何无法打动我
在讲为什么 Typescript 的优点无法打动我之前,我先来讲一讲 Typescript 有哪些优点吧:
- 大厂的产品
- 大厂在用
- 可以用未来的特性
- 降低出 bug 的可能性
- 面对对象编程(OOP)
对于它是微软的产品,我不能多说啥,毕竟我用 Visual Studio Code 用得很香;但是大厂在用这个论点,就不一样了。
有个逻辑谬误叫做「reductio ad absurdum」,也就是「归谬法」。什么意思呢:
大厂用 Typescript,所以我要用 Typescript。
大厂几百万改个 logo,我就借几百万改个 logo,因为大厂是大厂,肯定做得对。
这就很荒谬。
的确,大公司会采用 Typescript,必定有他的道理。但是,同样的论证思路也可以用于 Flow
、Angular
、Vue
、Ember
、jQuery
、Bootstrap
等等等等,几乎所有流行的库都是如此,那么它们一定都适合你吗?
关于它可以让你提前接触到未来的特性……大哥,babel
不香吗?
最后就是 OOP 以及降低出 bug 的可能性(Typesafe)。OOP 是 Typescript 的核心部分,而现在 OOP 已经不吃香了……例如 Ilya Suzdalnitski 就说过它是「万亿美元的灾难」。
至于为什么这么说,无非就两点——面向对象代码难以重构,也难以进行单元测试。重构时的抓狂不提,单元测试的重要性,大家都清楚吧。
而在 Javascript 这种非 OOP 语言里头,函数可以独立于对象存在。不用为了包含这些函数而去发明一些奇怪的概念真是一种解脱。
总之,TypeScript 的所谓优点(更好的错误处理、类型推理)都不是最佳方案。你还是得写测试,还是得好好命名你的函数和变量。个人觉得单单像 Typescript 一样添加一个接口或类型不能解决任何这些问题。
正好一千五百字。
作者:TurpinHero
链接:https://juejin.cn/post/6961012856573657095
收起阅读 »
我是如何把vue项目启动时间从70s优化到7秒的
可怕的启动时间
公司的产品是一个比较大的后台管理系统,而且使用的是webpack3的vue模板项目,单次项目启动时间达到了70s左右
启动个项目都够吃一碗豆腐脑了,可是没有豆腐脑怎么办,那就优化启动时间吧!
考虑到升级webpack版本的风险还是比较大的,出了一点问题都得找我,想想还是先别冒险,稳妥为主,所以我选择了通过插件来优化构建时间。
通过查阅资料,提升webpack的构建时间有以下几个方向:
- 多进程处理文件,同一时间处理多个文件
- 预编译资源模块,比如把长时间不变的库提取出来做预编译,构建的时候直接取编译结果就好
- 缓存,未修改的模块直接拿到处理结果,不必编译
- 减少构建搜索和处理的文件数量
针对以上几种优化方向,给出以下几种优化方案。
多进程构建
happypack
happypack 的作用就是将文件解析任务分解成多个子进程并发执行。
子进程处理完任务后再将结果发送给主进程。所以可以大大提升 Webpack 的项目构件速度。
查看happypack的github,发现作者已经不再维护该插件,并且作者推荐使用webpack官方的多进程插件thread-loader,所以我放弃了happypacy,选择了thread-loader。
thread-loader
thread-loader
是官方维护的多进程loader,功能类似于happypack,也是通过开启子任务来并行解析文件,从而提高构建速度。
把这个loader放在其他loader前面。不过该loader是有限制的。示例:
- loader无法发出文件。
- loader不能使用自定义加载器API。
- loader无法访问网页包选项。
每个worker都是一个单独的node.js进程,其开销约为600毫秒。还有进程间通信的开销。在小型项目中使用thread-loader
可能并不能优化项目的构建速度,反而会拖慢构建速度,所以使用该loader时需要明确项目构建构成中真正耗时的过程。
我的项目中我主要是用该loader用来解析vue和js文件,作用于vue-loader
和babel-loader
,如下代码:
const threadLoader = {
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1,
}
}
module.exports = {
module:{
rules: [
{
test: /\.vue$/,
use: [
threadLoader, // vue-loader前使用该loader
{
loader: 'vue-loader',
options: vueLoaderConfig
}
],
},
{
test: /\.js$/,
use: [
threadLoader, // babel-loader前使用该loader
{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
]
}
]
}
}
配置了thread-loader
后,重新构建试试,如下图所示,大概缩短了10秒的构建时间,还不错。
利用缓存提升二次构建的速度
虽然使用了多进程构建项目使构建时间缩短了10秒,但是一分钟的构建时间依然让人无法接受,这种挤牙膏似的优化方式多少让人有点不爽,有没有比较爽的方法来进一步缩短构建时间呢?
答案是有的,使用缓存。
缓存,不难理解就是第一次构建的时候将构建的结果缓存起来,当第二构建时,查看对应缓存是否修改,如果没有修改,直接使用缓存,由此,我们可以想象,当项目的变化不大时,大部分缓存都是可复用的,拿构建的速度岂不是会有质的飞跃。
cache-loader
说到缓存,当然百度一查,最先出现的就是cache-loader
,github搜索下官方文档,得到如下结果:
该loader会缓存其他loader的处理结果,把该loader放到其他loader的前面,同时该loader保存和读取缓存文件也会有开销,所以建议在开销较大的loader前使用该loader。
文档很简单,考虑到项目中的vue-loader
,babel-loader
,css-loader
会有比较大的开销,所以为这些loader加上缓存,那么接下来就把cache-loader
加到项目中吧:
const cacheLoader = {
loader: 'cache-loader'
}
const threadLoader = {
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1,
}
}
module.exports = {
module:{
rules: [
{
test: /\.vue$/,
use: [
cacheLoader,
threadLoader, // vue-loader前使用该loader
{
loader: 'vue-loader',
options: vueLoaderConfig
}
],
},
{
test: /\.js$/,
use: [
cacheLoader,
threadLoader, // babel-loader前使用该loader
{
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
]
}
]
}
}
在util.js
文件中,该文件主要是生成css相关的webpack配置,找到generateLoaders
函数,修改如下:
const cacheLoader = {
loader: 'cache-loader'
}
function generateLoaders(loader, loaderOptions) {
// 在css-loader前增加cache-loader
const loaders = options.usePostCSS ? [cacheLoader, cssLoader, postcssLoader] : [cacheLoader, cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader',
// 添加这句配置解决element-ui的图标路径问题
publicPath: '../../'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
如上配置完成后,再次启动项目,可以发现,现在的启动时间没什么变化,然后我们二次启动项目,可以发现现在的启动时间来到了30s左右,前面我们已经说过了,cache-loader
缓存只有在二次启动的时候才会生效。
虽然项目启动时间优化了一半还多,但是我们的欲望是无限大的,30秒的时间离我们的预期还是有点差距的,继续优化!
hard-source-webpack-plugin
HardSourceWebpackPlugin
是一个webpack插件,为模块提供中间缓存步骤。为了查看结果,您需要使用此插件运行webpack两次:第一次构建将花费正常的时间。第二次建设将大大加快。
话不多说,直接配置到项目中:
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
//...
plugins: [
new HardSourceWebpackPlugin()
]
}
二次构建时,我们会发现构建时间来到了个位数,只有短短的7秒钟。
在二次构建中,我发现了一个现象,构建的进度会从10% 一下跳到 80%,甚至是一瞬间就完成了中间构建过程。这正验证了该插件为模块提供中间缓存的说法。
为模块提供中间缓存,我的理解是cache-loader缓存的是对应loader的处理结果 ,而这个插件甚至可以缓存整个项目全部的处理结果,直接引用最终输出的缓存文件,从而大大提高构建速度。
其他优化方法
babel-loader开启缓存
babel-loader
自带缓存功能,开启cacheDirectory
配置项即可,官网的说法是,开启缓存会提高大约两倍的转换时间。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
...
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 开启缓存
}
}
]
}
]
}
}
uglifyjs-webpack-plugin开启多进程压缩
uglifyjs-webpack-plugin
或是其他的代码压缩工具都提供了多进程压缩代码的功能,开启可加速代码压缩。
动态polyfill
建议查看该篇文章
总结
至此,我们完成了项目构建时间从70s到7s的优化过程,文中主要使用:

一步步的将项目优化到几乎立马启动,哎,看来这下摸鱼的时间又少了,加油干吧,打工人!
作者:进击的小超人
链接:https://juejin.cn/post/6979879230297341989
收起阅读 »
从零到一编写 IOC 容器
前言
本文的编写主要是最近在使用 midway 编写后端应用,midway 的 IOC 控制反转能力跟我们平时常写的前端应用,例如 react、vue 这些单应用还是有蛮大区别的,所以促使我想一探究竟,这种类 Spring IOC 容器是如何用 JavaScript 来实现的。为方便读者阅读,本文的组织结构依次为 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器。阅读完本文后,我希望你能有这样的感悟:元数据(metadata)和 装饰器(Decorator) 本是 ES 中两个独立的部分,但是结合它们, 竟然能实现 控制反转 这样的能力。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。
辛苦整理良久,还望手动点赞鼓励~ 博客 github地址为:github.com/fengshi123/… ,汇总了作者的所有博客,欢迎关注及 star ~
一、TS 装饰器
1、类装饰器
(1)类型声明
type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
参数:
target: 类的构造器。
返回:
如果类装饰器返回了一个值,它将会被用来代替原有的类构造器的声明。因此,类装饰器适合用于继承一个现有类并添加一些属性和方法。例如我们可以添加一个 toString 方法给所有的类来覆盖它原有的 toString 方法,以及增加一个新的属性 school,如下所示
type Consturctor = { new (...args: any[]): any };
function School<T extends Consturctor>(BaseClass: T) {
// 新构造器继承原有的构造器,并且返回
return class extends BaseClass {
// 新增属性 school
public school = 'qinghua'
// 重写方法 toString
toString() {
return JSON.stringify(this);
}
};
}
@School
class Student {
public name = 'tom';
public age = 14;
}
console.log(new Student().toString())
// {"name":"tom","age":14,"school":"qinghua"}
但是存在一个问题:装饰器并没有类型保护,这意味着在类装饰器的构造函数中新增的属性,通过原有的类实例将报无法找到的错误,如下所示
type Consturctor = { new (...args: any[]): any };
function School<T extends Consturctor>(BaseClass: T){
return class extends BaseClass {
// 新增属性 school
public school = 'qinghua'
};
}
@School
class Student{
getSchool() {
return this.school; // Property 'school' does not exist on type 'Student'
}
}
new Student().school // Property 'school' does not exist on type 'Student'
这是 一个TypeScript的已知的缺陷。 目前我们能做的可以额外提供一个类用于提供类型信息,如下所示
type Consturctor = { new (...args: any[]): any };
function School<T extends Consturctor>(BaseClass: T){
return class extends BaseClass {
// 新增属性 school
public school = 'qinghua'
};
}
// 新增一个类用于提供类型信息
class Base {
school: string;
}
@School
class Student extends Base{
getSchool() {
return this.school;
}
}
new Student().school)
2、属性装饰器
(1)类型声明
type PropertyDecorator = (
target: Object,
propertyKey: string | symbol
) => void;
复制代码
参数:
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
propertyKey: 属性的名称。
返回:
返回的结果将被忽略。
我们可以通过属性装饰器给属性添加对应的验证判断,如下所示
function NameObserve(target: Object, property: string): void {
console.log('target:', target)
console.log('property:', property)
let _property = Symbol(property)
Object.defineProperty(target, property, {
set(val){
if(val.length > 4){
throw new Error('名称不能超过4位!')
}
this[_property] = val;
},
get: function() {
return this[_property];
}
})
}
class Student {
@NameObserve
public name: string; // target: Student {} key: 'name'
}
const stu = new Student();
stu.name = 'jack'
console.log(stu.name); // jack
// stu.name = 'jack1'; // Error: 名称不能超过4位!
export default Student;
3、方法装饰器
(1)类型声明:
type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
参数:
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链;
propertyKey: 属性的名称;
descriptor: 属性的描述器;
返回: 如果返回了值,它会被用于替代属性的描述器。
方法装饰器不同于属性装饰器的地方在于 descriptor 参数。 通过这个参数我们可以修改方法原本的实现,添加一些共用逻辑。 例如我们可以给一些方法添加打印输入与输出的能力
function logger(target: Object, property: string,
descriptor: PropertyDescriptor): PropertyDescriptor | void {
const origin = descriptor.value;
console.log(descriptor)
descriptor.value = function(...args: number[]){
console.log('params:', ...args)
const result = origin.call(this, ...args);
console.log('result:', result);
return result;
}
}
class Person {
@logger
add(x: number, y: number){
return x + y;
}
}
const person = new Person();
const result = person.add(1, 2);
console.log('查看 result:', result) // 3
4、访问器装饰器
访问器装饰器总体上讲和方法装饰器很接近,唯一的区别在于描述器中有的 key 不同: 方法装饰器的描述器的 key 为:
value
writable
enumerable
configurable
访问器装饰器的描述器的key为:
get
set
enumerable
configurable
例如,我们可以对访问器进行统一更改:
function descDecorator(target: Object, property: string,
descriptor: PropertyDescriptor): PropertyDescriptor | void {
const originalSet = descriptor.set;
const originalGet = descriptor.get;
descriptor.set = function(value: any){
return originalSet.call(this, value)
}
descriptor.get = function(): string{
return 'name:' + originalGet.call(this)
}
}
class Person {
private _name = 'tom';
@descDecorator
set name(value: string){
this._name = value;
}
get name(){
return this._name;
}
}
const person = new Person();
person.name = ('tom');
console.log('查看:', person.name) // name:'tom'
5、参数装饰器
类型声明:
type ParameterDecorator = (
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) => void;
参数:
target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链。
propertyKey: 属性的名称(注意是方法的名称,而不是参数的名称)。
parameterIndex: 参数在方法中所处的位置的下标。
返回:
返回的值将会被忽略。
单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。
function ParamDecorator(target: Object, property: string,
paramIndex: number): void {
console.log(property);
console.log(paramIndex);
}
class Person {
private name: string;
public setNmae(@ParamDecorator school: string, name: string){ // setNmae 0
this.name = school + '_' + name
}
}
6、执行时机
装饰器只在解释执行时应用一次,如下所示,这里的代码会在终端中打印 apply decorator,即便我们其实并没有使用类 A。
function f(C) {
console.log('apply decorator')
return C
}
@f
class A {}
// output: apply decorator
7、执行顺序
不同类型的装饰器的执行顺序是明确定义的:
实例成员:参数装饰器 -> 方法/访问器/属性 装饰器
静态成员:参数装饰器 -> 方法/访问器/属性 装饰器
构造器:参数装饰器
类装饰器
示例如下所示
function f(key: string): any {
console.log("evaluate: ", key);
return function () {
console.log("call: ", key);
};
}
@f("Class Decorator")
class C {
@f("Static Property")
static prop?: number;
@f("Static Method")
static method(@f("Static Method Parameter") foo:any) {}
constructor(@f("Constructor Parameter") foo:any) {}
@f("Instance Method")
method(@f("Instance Method Parameter") foo:any) {}
@f("Instance Property")
prop?: number;
}
/* 输出顺序如下
evaluate: Instance Method
evaluate: Instance Method Parameter
call: Instance Method Parameter
call: Instance Method
evaluate: Instance Property
call: Instance Property
evaluate: Static Property
call: Static Property
evaluate: Static Method
evaluate: Static Method Parameter
call: Static Method Parameter
call: Static Method
evaluate: Class Decorator
evaluate: Constructor Parameter
call: Constructor Parameter
call: Class Decorator
*/
我们从上注意到执行实例属性 prop 晚于实例方法 method 然而执行静态属性 static prop 早于静态方法static method。 这是因为对于属性/方法/访问器装饰器而言,执行顺序取决于声明它们的顺序。 然而,同一方法中不同参数的装饰器的执行顺序是相反的, 最后一个参数的装饰器会最先被执行。
function f(key: string): any {
console.log("evaluate: ", key);
return function () {
console.log("call: ", key);
};
}
class C {
method(
@f("Parameter Foo") foo,
@f("Parameter Bar") bar
) {}
}
/* 输出顺序如下
evaluate: Parameter Foo
evaluate: Parameter Bar
call: Parameter Bar
call: Parameter Foo
*/
8、多个装饰器组合
我们可以对同一目标应用多个装饰器。它们的组合顺序为:
求值外层装饰器
求值内层装饰器
调用内层装饰器
调用外层装饰器
如下示例所示
function f(key: string) {
console.log("evaluate: ", key);
return function () {
console.log("call: ", key);
};
}
class C {
@f("Outer Method")
@f("Inner Method")
method() {}
}
/* 输出顺序如下
evaluate: Outer Method
evaluate: Inner Method
call: Inner Method
call: Outer Method
*/
二、Reflect Metadata
1、背景
在 ES6 的规范当中,ES6 支持元编程,核心是因为提供了对 Proxy 和 Reflect 对象的支持。简单来说这个 API 的作用就是可以实现对变量操作的函数化,也就是反射。然而 ES6 的 Reflect 规范里面还缺失一个规范,那就是 Reflect Metadata。这会造成什么样的情境呢? 由于 JS/TS 现有的 装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定 this 等等功能。但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上... 这就限制了 JS 中元编程的能力。 此时 Relfect Metadata 就派上用场了,可以通过装饰器来给类添加一些自定义的信息。然后通过反射将这些信息提取出来(当然你也可以通过反射来添加这些信息)。 综合一下, JS 中对 Reflect Metadata 的诉求,简单概括就是:
其他 C#、Java、Pythone 语言已经有的高级功能,我 JS 也应该要有(诸如C# 和 Java 之类的语言支持将元数据添加到类型的属性或注释,以及用于读取元数据的反射API,而目前 JS 缺少这种能力)
许多用例(组合/依赖注入,运行时类型断言,反射/镜像,测试)都希望能够以一致的方式向类中添加其他元数据。
为了使各种工具和库能够推理出元数据,需要一种标准一致的方法;
元数据不仅可以用在对象上,也可以通过相关捕获器用在 Proxy 上;
对开发人员来说,定义新的元数据生成装饰器应该简洁易用;
2、使用
TypeScript 在 1.5+ 的版本已经支持 reflect-metadata,但是我们在使用的时候还需要额外进行安装,如下所示
npm i reflect-metadata --save
在 tsconfig.json 里配置选项 emitDecoratorMetadata: true
关于 reflect-metadata 的基本使用 api 可以阅读 reflect-metadata 文档,其包含常见的增删改查基本功能,我们来看下其基本的使用示例,其中 Reflect.metadata 当作 Decorator 使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,如:
import "reflect-metadata";
@Reflect.metadata('classMetaData', 'A')
class SomeClass {
@Reflect.metadata('methodMetaData', 'B')
public someMethod(): string {
return 'hello someMethod';
}
}
console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B
当然跟我们平时看到的 IOC 不同,我们进一步结合装饰器,如下所示,与前面的功能是一样的
import "reflect-metadata";
function classDecorator(): ClassDecorator {
return target => {
// 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
Reflect.defineMetadata('classMetaData', 'A', target);
};
}
function methodDecorator(): MethodDecorator {
return (target, key, descriptor) => {
// 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
Reflect.defineMetadata('methodMetaData', 'B', target, key);
};
}
@classDecorator()
class SomeClass {
@methodDecorator()
someMethod() {}
}
console.log(Reflect.getMetadata('classMetaData', SomeClass)); // 'A'
console.log(Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod')); // 'B'
3、design:类型元数据
在 TS 中的 reflect-metadata 的功能是经过增强的,其添加 "design:type"、"design:paramtypes" 和 "design:returntype" 这 3 个类型相关的元数据
design:type 表示被装饰的对象是什么类型, 比如是字符串、数字、还是函数等;
design:paramtypes 表示被装饰对象的参数类型, 是一个表示类型的数组, 如果不是函数, 则没有该 key;
design:returntype 表示被装饰对象的返回值属性, 比如字符串、数字或函数等;
示例如下所示
import "reflect-metadata";
@Reflect.metadata('type', 'class')
class A {
constructor(
public name: string,
public age: number
) { }
@Reflect.metadata(undefined, undefined)
method(name: string, age: number):boolean {
return true
}
}
const t1 = Reflect.getMetadata('design:type', A.prototype, 'method')
const t2 = Reflect.getMetadata('design:paramtypes', A.prototype, 'method')
const t3 = Reflect.getMetadata('design:returntype', A.prototype, 'method')
console.log(t1) // [Function: Function]
console.log(...t2) // [Function: String] [Function: Number]
console.log(t3) // [Function: Boolean]
三、IOC 容器实现
1、源码解读
我们可以从 github 上克隆 midway 仓库的代码到本地,然后进行代码阅读以及 debug 调试。本篇博文我们主要想探究下 midway 的依赖注入合控制反转是如何实现的,其主要源码存在于两个目录:packages/core 和 packages/decorator,其中 packages/core 包含依赖注入的核心实现,加载对象的class,同步、异步创建对象实例化,对象的属性绑定等。 IOC 容器就像是一个对象池,管理着每个对象实例的信息(Class Definition),所以用户无需关心什么时候创建,当用户希望拿到对象的实例 (Object Instance) 时,可以直接拿到依赖对象的实例,容器会 自动将所有依赖的对象都自动实例化。packages/core 中主要有以下几种,分别处理不同的逻辑:
AppliationContext 基础容器,提供了基础的增加定义和根据定义获取对象实例的能力;
MidwayContainer 用的最多的容器,做了上层封装,通过 bind 函数能够方便的生成类定义,midway 从此类开始扩展;
RequestContainer 用于请求链路上的容器,会自动销毁对象并依赖另一个容器创建实例;
packages/decorator 包含装饰器 provide.ts、inject.ts 的实现,在midwayjs中是有一个装饰器管理类DecoratorManager, 用来管理 midwayjs 的所有装饰器:
@provide() 的作用是简化绑定,能被 IOC 容器自动扫描,并绑定定义到容器上,对应的逻辑是绑定对象定义;
@inject() 的作用是将容器中的定义实例化成一个对象,并且绑定到属性中,这样,在调用的时候就可以访问到该属性。
2、简单实现
2.1、装饰器 Provider
实现装饰器 Provider 类,作用为将对应类注册到 IOC 容器中。
import 'reflect-metadata'
import * as camelcase from 'camelcase'
import { class_key } from './constant'
// Provider 装饰的类,表示要注册到 IOC 容器中
export function Provider (identifier?: string, args?: Array<any>) {
return function (target: any) {
// 类注册的唯一标识符
identifier = identifier ?? camelcase(target.name)
Reflect.defineMetadata(class_key, {
id: identifier, // 唯一标识符
args: args || [] // 实例化所需参数
}, target)
return target
}
}
2.2、装饰器 Inject
实现装饰器 Inject 类,作用为将对应的类注入到对应的地方。
import 'reflect-metadata'
import { props_key } from './constant'
export function Inject () {
return function (target: any, targetKey: string) {
// 注入对象
const annotationTarget = target.constructor
let props = {}
// 同一个类,多个属性注入类
if (Reflect.hasOwnMetadata(props_key, annotationTarget)) {
props = Reflect.getMetadata(props_key, annotationTarget)
}
//@ts-ignore
props[targetKey] = {
value: targetKey
}
Reflect.defineMetadata(props_key, props, annotationTarget)
}
}
2.3、管理容器 Container
管理容器 Container 的实现,用于绑定实例信息并且在对应的地方获取它们。
import 'reflect-metadata'
import { props_key } from './constant'
export class Container {
bindMap = new Map()
// 绑定类信息
bind(identifier: string, registerClass: any, constructorArgs: any[]) {
this.bindMap.set(identifier, {registerClass, constructorArgs})
}
// 获取实例,将实例绑定到需要注入的对象上
get<T>(identifier: string): T {
const target = this.bindMap.get(identifier)
if (target) {
const { registerClass, constructorArgs } = target
// 等价于 const instance = new registerClass([...constructorArgs])
const instance = Reflect.construct(registerClass, constructorArgs)
const props = Reflect.getMetadata(props_key, registerClass)
for (let prop in props) {
const identifier = props[prop].value
// 递归进行实例化获取 injected object
instance[prop] = this.get(identifier)
}
return instance
}
}
}
2.4、加载类文件 load
启动时扫描所有文件,获取文件导出的所有类,然后根据元数据进行绑定。
import * as fs from 'fs'
import { resolve } from 'path'
import { class_key } from './constant'
// 启动时扫描所有文件,获取定义的类,根据元数据进行绑定
export function load(container: any, path: string) {
const list = fs.readdirSync(path)
for (const file of list) {
if (/\.ts$/.test(file)) {
const exports = require(resolve(path, file))
for (const m in exports) {
const module = exports[m]
if (typeof module === 'function') {
const metadata = Reflect.getMetadata(class_key, module)
// register
if (metadata) {
container.bind(metadata.id, module, metadata.args)
}
}
}
}
}
}
2.5、示例类
三个示例类如下所示
// class A
import { Provider } from "../provide";
import { Inject } from "../inject";
import B from './classB'
import C from './classC'
@Provider('a')
export default class A {
@Inject()
private b: B
@Inject()
c: C
print () {
this.c.print()
}
}
// class B
import { Provider } from '../provide'
@Provider('b', [10])
export default class B {
n: number
constructor (n: number) {
this.n = n
}
}
// class C
import { Provider } from '../provide'
@Provider()
export default class C {
print () {
console.log('hello')
}
}
2.6、初始化
我们能从以下示例结果中看到,我们已经实现了一个基本的 IOC 容器能力。
import { Container } from './container'
import { load } from './load'
import { class_path } from './constant'
const init = function () {
const container = new Container()
// 通过加载,会先执行装饰器(设置元数据),
// 再由 container 统一管理元数据中,供后续使用
load(container, class_path)
const a:any = container.get('a') // A { b: B { n: 10 }, c: C {} }
console.log(a);
a.c.print() // hello
}
init()
总结
本文的依次从 TS 装饰器、Reflect Metadata、IOC 容器源码简单解读、以及自定义实现 IOC 容器四个部分由零到一编写自定义 IOC 容器,希望对你有所启发。本文的所有演示实例都已经上传到 github 仓库 ioc-container ,读者可以克隆下来进行调试运行。
作者:我是你的超级英雄
来源:https://juejin.cn/post/7036895697865555982
手写迷你版Vue
手写迷你版Vue
Vue响应式设计思路
Vue响应式主要包含:
数据响应式
监听数据变化,并在视图中更新
Vue2使用
Object.defineProperty
实现数据劫持Vu3使用
Proxy
实现数据劫持模板引擎
提供描述视图的模板语法
插值表达式
{{}}
指令
v-bind
,v-on
,v-model
,v-for
,v-if
渲染
将模板转换为html
解析模板,生成
vdom
,把vdom
渲染为普通dom
数据响应式原理
数据变化时能自动更新视图,就是数据响应式 Vue2使用Object.defineProperty
实现数据变化的检测
原理解析
new Vue()
⾸先执⾏初始化,对data
执⾏响应化处理,这个过程发⽣在Observer
中同时对模板执⾏编译,找到其中动态绑定的数据,从
data
中获取并初始化视图,这个过程发⽣在
Compile
中
同时定义⼀个更新函数和
Watcher实例
,将来对应数据变化时,Watcher会调⽤更新函数由于
data
的某个key
在⼀个视图中可能出现多次,所以每个key
都需要⼀个管家Dep来管理多个
Watcher
将来
data
中数据⼀旦发⽣变化,会⾸先找到对应的Dep
,通知所有Watcher
执⾏更新函数
一些关键类说明
CVue
:自定义Vue类 Observer
:执⾏数据响应化(分辨数据是对象还是数组) Compile
:编译模板,初始化视图,收集依赖(更新函数、 watcher创建) Watcher
:执⾏更新函数(更新dom) Dep
:管理多个Watcher实例,批量更新
涉及关键方法说明
observe
: 遍历vm.data
的所有属性,对其所有属性做响应式,会做简易判断,创建Observer实例
进行真正响应式处理
html页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>cvue</title>
<script src="./cvue.js"></script>
</head>
<body>
<div id="app">
<p>{{ count }}</p>
</div>
<script>
const app = new CVue({
el: '#app',
data: {
count: 0
}
})
setInterval(() => {
app.count +=1
}, 1000);
</script>
</body>
</html>
CVue
创建基本CVue构造函数:
执⾏初始化,对
data
执⾏响应化处理
// 自定义Vue类
class CVue {
constructor(options) {
this.$options = options
this.$data = options.data
// 响应化处理
observe(this.$data)
}
}
// 数据响应式, 修改对象的getter,setter
function defineReactive(obj, key, val) {
// 递归处理,处理val是嵌套对象情况
observe(val)
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
if(val !== newVal) {
console.log(`set ${key}:${newVal}, old is ${val}`)
val = newVal
// 继续进行响应式处理,处理newVal是对象情况
observe(val)
}
}
})
}
// 遍历obj,对其所有属性做响应式
function observe(obj) {
// 只处理对象类型的
if(typeof obj !== 'object' || obj == null) {
return
}
// 实例化Observe实例
new Observe(obj)
}
// 根据传入value的类型做相应的响应式处理
class Observe {
constructor(obj) {
if(Array.isArray(obj)) {
// TODO
} else {
// 对象
this.walk(obj)
}
}
walk(obj) {
// 遍历obj所有属性,调用defineReactive进行响应化
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
}
为vm.$data做代理
方便实例上设置和获取数据
例如
原本应该是
vm.$data.count
vm.$data.count = 233
代理之后后,可以使用如下方式
vm.count
vm.count = 233
给vm.$data做代理
class CVue {
constructor(options) {
// 省略
// 响应化处理
observe(this.$data)
// 代理data上属性到实例上
proxy(this)
}
}
// 把CVue实例上data对象的属性到代理到实例上
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
// 实现 vm.count 取值
return vm.$data[key]
},
set(newVal) {
// 实现 vm.count = 123赋值
vm.$data[key] = newVal
}
})
})
}
编译
初始化视图
根据节点类型进行编译
class CVue {
constructor(options) {
// 省略。。
// 2 代理data上属性到实例上
proxy(this)
// 3 编译
new Compile(this, this.$options.el)
}
}
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
constructor(vm, el) {
this.$vm = vm
this.$el = document.querySelector(el)
if(this.$el) {
this.complie(this.$el)
}
}
// 编译
complie(el) {
// 取出所有子节点
const childNodes = el.childNodes
// 遍历节点,进行初始化视图
Array.from(childNodes).forEach(node => {
if(this.isElement(node)) {
// TODO
console.log(`编译元素 ${node.nodeName}`)
} else if(this.isInterpolation(node)) {
console.log(`编译插值文本 ${node.nodeName}`)
}
// 递归编译,处理嵌套情况
if(node.childNodes) {
this.complie(node)
}
})
}
// 是元素节点
isElement(node) {
return node.nodeType === 1
}
// 是插值表达式
isInterpolation(node) {
return node.nodeType === 3
&& /\{\{(.*)\}\}/.test(node.textContent)
}
}
编译插值表达式
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
complie(el) {
Array.from(childNodes).forEach(node => {
if(this.isElement(node)) {
console.log(`编译元素 ${node.nodeName}`)
} else if(this.isInterpolation(node)) {
// console.log(`编译插值文本 ${node.textContent}`)
this.complieText(node)
}
// 省略
})
}
// 是插值表达式
isInterpolation(node) {
return node.nodeType === 3
&& /\{\{(.*)\}\}/.test(node.textContent)
}
// 编译插值
complieText(node) {
// RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
// 相等于{{ count }}中的count
const exp = String(RegExp.$1).trim()
node.textContent = this.$vm[exp]
}
}
编译元素节点和指令
需要取出指令和指令绑定值 使用数据更新视图
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
complie(el) {
Array.from(childNodes).forEach(node => {
if(this.isElement(node)) {
console.log(`编译元素 ${node.nodeName}`)
this.complieElement(node)
}
// 省略
})
}
// 是元素节点
isElement(node) {
return node.nodeType === 1
}
// 编译元素
complieElement(node) {
// 取出元素上属性
const attrs = node.attributes
Array.from(attrs).forEach(attr => {
// c-text="count"中c-text是attr.name,count是attr.value
const { name: attrName, value: exp } = attr
if(this.isDirective(attrName)) {
// 取出指令
const dir = attrName.substring(2)
this[dir] && this[dir](node, exp)
}
})
}
// 是指令
isDirective(attrName) {
return attrName.startsWith('')
}
// 处理c-text文本指令
text(node, exp) {
node.textContent = this.$vm[exp]
}
// 处理c-html指令
html(node, exp) {
node.innerHTML = this.$vm[exp]
}
}
以上完成初次渲染,但是数据变化后,不会触发页面更新
依赖收集
视图中会⽤到data中某key,这称为依赖。 同⼀个key可能出现多次,每次出现都需要收集(⽤⼀个Watcher来维护维护他们的关系),此过程称为依赖收集。 多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知。
data中的key和dep是一对一关系
视图中key出现和Watcher关系,key出现一次就对应一个Watcher
dep和Watcher是一对多关系
实现思路
在
defineReactive
中为每个key
定义一个Dep实例
编译阶段,初始化视图时读取key, 会创建
Watcher实例
由于读取过程中会触发key的
getter
方法,便可以把Watcher实例
存储到key对应的Dep实例
中当key更新时,触发setter方法,取出对应的
Dep实例
,Dep实例
调用notiy
方法通知所有Watcher更新
定义Watcher类
监听器,数据变化更新对应节点视图
// 创建Watcher监听器,负责更新视图
class Watcher {
// vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
constructor(vm, key, updateFn) {
this.$vm = vm
this.$key = key
this.$updateFn = updateFn
}
update() {
// 调用更新函数,获取最新值传递进去
this.$updateFn.call(this.$vm, this.$vm[this.$key])
}
}
修改Compile类中的更新函数,创建Watcher实例
class Complie {
// 省略。。。
// 编译插值
complieText(node) {
// RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
// 相等于{{ count }}中的count
const exp = String(RegExp.$1).trim()
// node.textContent = this.$vm[exp]
this.update(node, exp, 'text')
}
// 处理c-text文本指令
text(node, exp) {
// node.textContent = this.$vm[exp]
this.update(node, exp, 'text')
}
// 处理c-html指令
html(node, exp) {
// node.innerHTML = this.$vm[exp]
this.update(node, exp, 'html')
}
// 更新函数
update(node, exp, dir) {
const fn = this[`${dir}Updater`]
fn && fn(node, this.$vm[exp])
// 创建监听器
new Watcher(this.$vm, exp, function(newVal) {
fn && fn(node, newVal)
})
}
// 文本更新器
textUpdater(node, value) {
node.textContent = value
}
// html更新器
htmlUpdater(node, value) {
node.innerHTML = value
}
}
定义Dep类
data的一个属性对应一个Dep实例
管理多个
Watcher
实例,通知所有Watcher
实例更新
// 创建订阅器,每个Dep实例对应data中的一个属性
class Dep {
constructor() {
this.deps = []
}
// 添加Watcher实例
addDep(dep) {
this.deps.push(dep)
}
notify() {
// 通知所有Wather更新视图
this.deps.forEach(dep => dep.update())
}
}
创建Watcher时触发getter
class Watcher {
// vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
constructor(vm, key, updateFn) {
// 省略
// 把Wather实例临时挂载在Dep.target上
Dep.target = this
// 获取一次属性,触发getter, 从Dep.target上获取Wather实例存放到Dep实例中
this.$vm[key]
// 添加后,重置Dep.target
Dep.target = null
}
}
defineReactive中作依赖收集,创建Dep实例
function defineReactive(obj, key, val) {
// 递归处理,处理val是嵌套对象情况
observe(val)
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if(val !== newVal) {
val = newVal
// 继续进行响应式处理,处理newVal是对象情况
observe(val)
// 更新视图
dep.notify()
}
}
})
}
监听事件指令@xxx
在创建vue实例时,需要缓存
methods
到vue实例上编译阶段取出methods挂载到Compile实例上
编译元素时
识别出
v-on
指令时,进行事件的绑定识别出
@
属性时,进行事件绑定事件绑定:通过指令或者属性获取对应的函数,给元素新增事件监听,使用
bind
修改监听函数的this指向为组件实例
// 自定义Vue类
class CVue {
constructor(options) {
this.$methods = options.methods
}
}
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
constructor(vm, el) {
this.$vm = vm
this.$el = document.querySelector(el)
this.$methods = vm.$methods
}
// 编译元素
complieElement(node) {
// 取出元素上属性
const attrs = node.attributes
Array.from(attrs).forEach(attr => {
// c-text="count"中c-text是attr.name,count是attr.value
const { name: attrName, value: exp } = attr
if(this.isDirective(attrName)) {
// 省略。。。
if(this.isEventListener(attrName)) {
// v-on:click, subStr(5)即可截取到click
const eventType = attrName.substring(5)
this.bindEvent(eventType, node, exp)
}
} else if(this.isEventListener(attrName)) {
// @click, subStr(1)即可截取到click
const eventType = attrName.substring(1)
this.bindEvent(eventType, node, exp)
}
})
}
// 是事件监听
isEventListener(attrName) {
return attrName.startsWith('@') || attrName.startsWith('c-on')
}
// 绑定事件
bindEvent(eventType, node, exp) {
// 取出表达式对应函数
const method = this.$methods[exp]
// 增加监听并修改this指向当前组件实例
node.addEventListener(eventType, method.bind(this.$vm))
}
}
v-model双向绑定
实现v-model
绑定input
元素时的双向绑定功能
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
// 省略...
// 处理c-model指令
model(node, exp) {
// 渲染视图
this.update(node, exp, 'model')
// 监听input变化
node.addEventListener('input', (e) => {
const { value } = e.target
// 更新数据,相当于this.username = 'mio'
this.$vm[exp] = value
})
}
// model更新器
modelUpdater(node, value) {
node.value = value
}
}
数组响应式
获取数组原型
数组原型创建对象作为数组拦截器
重写数组的7个方法
// 数组响应式
// 获取数组原型, 后面修改7个方法
const originProto = Array.prototype
// 创建对象做备份,修改响应式都是在备份的上进行,不影响原始数组方法
const arrayProto = Object.create(originProto)
// 拦截数组方法,在变更时发出通知
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
// 在备份的原型上做修改
arrayProto[method] = function() {
// 调用原始操作
originProto[method].apply(this, arguments)
// 发出变更通知
console.log(`method:${method} value:${Array.from(arguments)}`)
}
})
class Observe {
constructor(obj) {
if(Array.isArray(obj)) {
// 修改数组原型为自定义的
obj.__proto__ = arrayProto
this.observeArray(obj)
} else {
// 对象
this.walk(obj)
}
}
observeArray(items) {
// 如果数组内部元素时对象,继续做响应化处理
items.forEach(item => observe(item))
}
}
作者:LastStarDust
来源:https://juejin.cn/post/7036291383153393701
LRU缓存-keep-alive实现原理
前言
相信大部分同学在日常需求开发中或多或少的会有需要一个组件状态被持久化、不被重新渲染的场景,熟悉 vue 的同学一定会想到 keep-alive
这个内置组件。
那么什么是 keep-alive
呢?
keep-alive
是 Vue.js 的一个 内置组件。它能够将不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实 DOM 中,也不会出现在父组件链中。简单的说,keep-alive用于保存组件的渲染状态,避免组件反复创建和渲染,有效提升系统性能。 keep-alive
的 max
属性,用于限制可以缓存多少组件实例,一旦这个数字达到了上限,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉,而这里所运用到的缓存机制就是 LRU 算法
LRU 缓存淘汰算法
LRU( least recently used)根据数据的历史记录来淘汰数据,重点在于保护最近被访问/使用过的数据,淘汰现阶段最久未被访问的数据
LRU的主体思想在于:如果数据最近被访问过,那么将来被访问的几率也更高
新数据插入到链表尾部;
每当缓存命中(即缓存数据被访问),则将数据移到链表尾部
当链表满的时候,将链表头部的数据丢弃。
实现LRU的数据结构
经典的 LRU 一般都使用
hashMap
+双向链表
。考虑可能需要频繁删除一个元素,并将这个元素的前一个节点指向下一个节点,所以使用双链接最合适。并且它是按照结点最近被使用的时间顺序来存储的。 如果一个结点被访问了, 我们有理由相信它在接下来的一段时间被访问的概率要大于其它结点。
不过既然已经在 js 里都已经使用 Map
了,何不直接取用现成的迭代器获取下一个结点的 key 值(keys().next(
)
)
// ./LRU.ts
export class LRUCache {
capacity: number; // 容量
cache: Map; // 缓存
constructor(capacity: number) {
this.capacity = capacity;
this.cache = new Map();
}
get(key: number): number {
if (this.cache.has(key)) {
let temp = this.cache.get(key) as number;
//访问到的 key 若在缓存中,将其提前
this.cache.delete(key);
this.cache.set(key, temp);
return temp;
}
return -1;
}
put(key: number, value: number): void {
if (this.cache.has(key)) {
this.cache.delete(key);
//存在则删除,if 结束再提前
} else if (this.cache.size >= this.capacity) {
// 超过缓存长度,淘汰最近没使用的
this.cache.delete(this.cache.keys().next().value);
console.log(`refresh: key:${key} , value:${value}`)
}
this.cache.set(key, value);
}
toString(){
console.log('capacity',this.capacity)
console.table(this.cache)
}
}
// ./index.ts
import {LRUCache} from './lru'
const list = new LRUCache(4)
list.put(2,2) // 入 2,剩余容量3
list.put(3,3) // 入 3,剩余容量2
list.put(4,4) // 入 4,剩余容量1
list.put(5,5) // 入 5,已满 从头至尾 2-3-4-5
list.put(4,4) // 入4,已存在 ——> 置队尾 2-3-5-4
list.put(1,1) // 入1,不存在 ——> 删除队首 插入1 3-5-4-1
list.get(3) // 获取3,刷新3——> 置队尾 5-4-1-3
list.toString()
// ./index.ts
import {LRUCache} from './lru'
const list = new LRUCache(4)
list.put(2,2) // 入 2,剩余容量3
list.put(3,3) // 入 3,剩余容量2
list.put(4,4) // 入 4,剩余容量1
list.put(5,5) // 入 5,已满 从头至尾 2-3-4-5
list.put(4,4) // 入4,已存在 ——> 置队尾 2-3-5-4
list.put(1,1) // 入1,不存在 ——> 删除队首 插入1 3-5-4-1
list.get(3) // 获取3,刷新3——> 置队尾 5-4-1-3
list.toString()
结果如下:
vue 中 Keep-Alive
原理
使用 LRU 缓存机制进行缓存,max 限制缓存表的最大容量
根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例
根据组件 ID 和 tag 生成缓存 Key ,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)
获取节点名称,或者根据节点 cid 等信息拼出当前 组件名称
获取 keep-alive 包裹着的第一个子组件对象及其组件名
源码分析
初始化 keepAlive 组件
const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number],
},
setup(props: KeepAliveProps, { slots }: SetupContext) {
// 初始化数据
const cache: Cache = new Map();
const keys: Keys = new Set();
let current: VNode | null = null;
// 当 props 上的 include 或者 exclude 变化时移除缓存
watch(
() => [props.include, props.exclude],
([include, exclude]) => {
include && pruneCache((name) => matches(include, name));
exclude && pruneCache((name) => !matches(exclude, name));
},
{ flush: "post", deep: true }
);
// 缓存组件的子树 subTree
let pendingCacheKey: CacheKey | null = null;
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree));
}
};
// KeepAlive 组件的设计,本质上就是空间换时间。
// 在 KeepAlive 组件内部,
// 当组件渲染挂载和更新前都会缓存组件的渲染子树 subTree
onMounted(cacheSubtree);
onUpdated(cacheSubtree);
onBeforeUnmount(() => {
// 卸载缓存表里的所有组件和其中的子树...
}
return ()=>{
// 返回 keepAlive 实例
}
}
}
return ()=>{
// 省略部分代码,以下是缓存逻辑
pendingCacheKey = null
const children = slots.default()
let vnode = children[0]
const comp = vnode.type as Component
const name = getName(comp)
const { include, exclude, max } = props
// key 值是 KeepAlive 子节点创建时添加的,作为缓存节点的唯一标识
const key = vnode.key == null ? comp : vnode.key
// 通过 key 值获取缓存节点
const cachedVNode = cache.get(key)
if (cachedVNode) {
// 缓存存在,则使用缓存装载数据
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
if (vnode.transition) {
// 递归更新子树上的 transition hooks
setTransitionHooks(vnode, vnode.transition!)
}
// 阻止 vNode 节点作为新节点被挂载
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// 刷新key的优先级
keys.delete(key)
keys.add(key)
} else {
keys.add(key)
// 属性配置 max 值,删除最久不用的 key ,这很符合 LRU 的思想
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
// 避免 vNode 被卸载
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode
return vnode;
}
将组件移出缓存表
// 遍历缓存表
function pruneCache(filter?: (name: string) => boolean) {
cache.forEach((vnode, key) => {
const name = getComponentName(vnode.type as ConcreteComponent);
if (name && (!filter || !filter(name))) {
// !filter(name) 即 name 在 includes 或不在 excludes 中
pruneCacheEntry(key);
}
});
}
// 依据 key 值从缓存表中移除对应组件
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key) as VNode;
if (!current || cached.type !== current.type) {
/* 当前没有处在 activated 状态的组件
* 或者当前处在 activated 组件不是要删除的 key 时
* 卸载这个组件
*/
unmount(cached); // unmount方法里同样包含了 resetShapeFlag
} else if (current) {
// 当前组件在未来应该不再被 keepAlive 缓存
// 虽然仍在 keepAlive 的容量中但是需要刷新当前组件的优先级
resetShapeFlag(current);
// resetShapeFlag
}
cache.delete(key);
keys.delete(key);
}
function resetShapeFlag(vnode: VNode) {
let shapeFlag = vnode.shapeFlag; // shapeFlag 是 VNode 的标识
// ... 清除组件的 shapeFlag
}
keep-alive案例
本部分将使用 vue 3.x 的新特性来模拟 keep-alive
的具体应用场景
在 index.vue 里我们引入了 CountUp 、timer 和 ColorRandom 三个带有状态的组件 在容量为 2 的
中包裹了一个动态组件
// index.vue
<script setup>
import { ref } from "vue"
import CountUp from '../components/CountUp.vue'
import ColorRandom from '../components/ColorRandom.vue'
import Timer from '../components/Timer.vue'
const tabs = ref([ // 组件列表
{
title: "ColorPicker",
comp: ColorRandom,
},
{
title: "timer1",
comp: Timer,
},
{
title: "timer2",
comp: Timer,
},
{
title: "CountUp",
comp: CountUp,
},
])
const currentTab = ref(tabs.value[0]) // tab 默认展示第一个组件
const tabSwitch = (tab) => {
currentTab.value = tab
}
script>
<template>
<div id="main-page">keep-alive demo belowdiv>
<div class="tab-group">
<button
v-for="tab in tabs"
:key="tab"
:class="['tab-button', { active: currentTab === tab }]"
@click="tabSwitch(tab)"
>
{{ tab.title }}
button>
div>
<keep-alive max="2">
<component
v-if="currentTab"
:is="currentTab.comp"
:key="currentTab.title"
:name="currentTab.title"
/>
keep-alive>
template>
缓存状态
缓存流程如下:
可以看到被包裹在 keep-alive
的动态组件缓存了前一个组件的状态。
通过观察 vue devtools 里节点的变化,可以看到此时 keepAlive 中包含了 ColorRandom
和 Timer
两个组件,当前展示的组件会处在 activated 的状态,而其他被缓存的组件则处在 inactivated
的状态
如果我们注释了两个 keep-alive
会发现不管怎么切换组件,都只会重新渲染,并不会保留前次的状态
移除组件
移除流程如下:
为了验证组件是否在切换tab时能被成功卸载,在每个组件的 onUnmounted
中加上了 log
onUnmounted(()=>{
console.log(`${props.name} 组件被卸载`)
})
当缓存数据长度小于等于 max ,切换组件并不会卸载其他组件,就像上面在 vue devtools 里展示的一样,只会触发组件的
activated
和deactivated
两个生命周期若此时缓存数据长度大于 max ,则会从缓存列表中删除优先级较低的,优先被淘汰的组件,对应的可以看到该组件
umounted
生命周期触发。
性能优化
使用 KeepAlive 后,被 KeepAlive 包裹的组件在经过第一次渲染后,的 vnode 以及 DOM 都会被缓存起来,然后再下一次再次渲染该组件的时候,直接从缓存中拿到对应的 vnode 和 DOM,然后渲染,并不需要再走一次组件初始化,render 和 patch 等一系列流程,减少了 script 的执行时间,性能更好。
总结
Vue 内部将 DOM 节点抽象成了一个个的 VNode 节点,keep-alive 组件的缓存也是基于 VNode 节点的而不是直接存储 DOM 结构。它将满足条件( include 与 exclude )的组件在 cache 对象中缓存起来,在需要重新渲染的时候再将 vnode 节点从 cache 对象中取出并渲染。
具体缓存过程如下:
声明有序集合 keys 作为缓存容器,存入组件的唯一 key 值
在缓存容器 keys 中,越靠前的 key 值意味着被访问的越少也越优先被淘汰
渲染函数执行时,若命中缓存时,则从 keys 中删除当前命中的 key,并往 keys 末尾追加 key 值,刷新该 key 的优先级
未命中缓存时,则 keys 追加缓存数据 key 值,若此时缓存数据长度大于 max 最大值,则删除最旧的数据
当触发 beforeMount/update 生命周期,缓存当前 activated 组件的子树的数据
参考
作者:政采云前端团队
来源:https://juejin.cn/post/7036483610920091656
统一路由,让小程序跳转更智能
我们在小程序开发及运营过程中,不可避免的需要进行页面之间的跳转。如果使用小程序自带的路由功能来实现这个功能,是非常简单的,如:
// 根据不同的场景选择 navigateTo、redirectTo、switchTab 等
wx.navigateTo({
url: "pages/somepage?id=1",
success: function (res) {},
});
但这里面存在几个问题:
- 需要代码里面写死或者运营人员维护小程序页面的长长的具体路径,这显然是很不友好的
- 需要知道页面是否为 tabbar 页面(switchTab)
- 如果某个页面在 tabbar 和非 tabbar 页面之间发生了变化,或路径因为重构、主包瘦身等各种原因发生变化,原来的代码就会报错导致无法运行
- navigateBack 不支持传参
为了解决以上问题,我们在项目中实现了一套基于命令别名(cmd)的统一路由跳转方式(以下称为统一路由),很好解决了遇到的实际问题,统一路由特点如下:
- 页面别名声明使用注释方式,不侵入业务代码
- 页面可以存在多个别名,方便新老版本页面的流量切换
- 路由内自动判断是否 tabbar 页面,自行处理跳转及传参,业务代码无需关心
- 支持纯 js api 的页面跳转及需要用户点击的任意类型跳转(如联系客服、打开小程序等等)
- 对于页面栈中存在相同页面时,可以自动返回并根据参数是否相同决定是否需要刷新页面,可有效减少页面栈层级,规避小程序 10 层限制
实现思路
step1. 资源描述约定
小程序内的跳转类操作存在以下几种
- js api 直接可以操作的内部页面间跳转(wx.navigateTo、wx.navigateBack、wx.redirectTo、wx.reLaunch、wx.switchTab)
- js api 直接可以操作的打开微信原生功能的跳转(扫码、拨打电话等)
- 需要借助点击操作的跳转(如打开小程序及客服等需要 open-type 配合的场景 )
针对这三类操作,我们使用常见的 URL(统一资源定位系统)方式描述不同的待跳转资源
- 内部页面
https://host?cmd=${pagename}¶m1=a // 打开普通页面并传参,标准的H5容器也算在普通页面内
- 微信原生 API
https://host?cmd=nativeAPI&API=makePhoneCall&phoneNumber=123456 // 拨打电话
https://host?cmd=nativeAPI&API=scanCode&callback=scanCallback // 扫码并执行回调
- 需要借助按钮 open-type 的微信原生能力
https://host?cmd=nativeButtonAPI&openType=contact // 在线客服
- 打开另一个小程序
https://host?cmd=miniProgram&appId=wx637bb****&path=pages/order/index&version=trial&uid=${uid}
小程序跳转需要携带更多的参数,所以做了cmd的区分,这里实际会解析成 nativeButtonAPI 运行
step2. 在页面内定义需要的数据
在每个页面的顶部添加注释,注意 cmd 不能重复,支持多个 cmd。为了方便后续解析,我们的注释大体上遵循 JSDoc 注释规范
// pages/detail/index.tsx
/**
* @cmd detail, newdetail
* @description 详情
* @param skuid {number} skuid
*/
step3. 在编译阶段扫描并生成配置文件
根据入口文件的页面定义,匹配出需要的注释部分,使用 doctrine 解析需要的数据,解析后的数据如下:
// config/router.config.ts
export default {
index: {
description: "首页", // 页面描述
path: "/pages/index/index", // 真实路径
isTabbar: true, // 是否tabbar页面
ensureLogin: false, // 是否需要强制登录
},
detail: {
description: "详情",
path: "/pages/detail/index",
isTabbar: false,
ensureLogin: true,
},
};
这里顺便可以使用 param 等生成详细的页面名称及入参文档,提供给其他研发或运营同学使用。
step4. 资源描述解析为标准数据
根据上面的资源描述约定及扫描得到的配置文件,我们可以将其转换为方便在小程序内解析的数据定义,基本格式如下
{
origin: 'https://host?cmd=detail&skuid=1', // 原始数据
parsed: {
type: 'PAGE', // 类型,PAGE,NATIVE_API,NATIVE_BUTTON_API,UNKNOW
data: {
path: 'pages/detail/index', // 实际的页面路径,如果type是PAGE则会解析出此字段
action: undefined, // 动作,scanCode,makePhoneCall,openType,miniprogram ……。如果type是NATIVE_API,NATIVE_BUTTON_API,则会解析出此字段
params: {
skuid: '1' // 需要携带的参数
}
}
}
}
step5. 根据标准数据执行对应逻辑
由于我们的项目使用的是 Taro 框架,以下伪代码都是以 Taro 为例。
// utils/router.ts
// 用于解析原始链接为标准数据
const parseURL = (origin) => {
// balabala,一顿操作格式化成上文的数据
const data = {
...
};
return data;
};
// 执行除 NATIVE_BUTTON_API 之外的跳转
const routeURL = (origin) => {
const parsedData = parseURL(origin)
const {parsed: {type, data}} = parsedData
switch(type){
case 'PAGE':
...
break;
case 'NATIVE_API':
...
break;
case 'UNKNOW':
...
break;
}
};
export default {
parseURL,
routeURL,
};
对于需要点击的类型,我们需要借助 UI 组件实现
// components/router.tsx
import router from "/utils/router";
import { Button } from "@tarojs/components";
import Taro, { Component, eventCenter } from "@tarojs/taro";
export default class Router extends Component {
componentWillMount() {
const { path } = this.props;
const data = router.parseURL(path);
const { parsed, origin } = data;
const openType =
(parsed &&
parsed.data &&
parsed.data.params &&
parsed.data.params.openType) ||
false;
this.setState({
parsed,
openType,
});
}
// 点击事件
async handleClick(parsed, origin) {
// 点击执行动作
let {
type,
data: { action, params },
} = parsed;
if (!type) {
return;
}
// 内部页面
if (["PAGE", "CMD_UNKNOW"].includes(type)) {
console.log(`CMD_NATIVE_PAGE 参数:`, origin, options);
router.routeURL(origin);
return;
}
// 拨打电话、扫码等原生API
if (["NATIVE_API"].includes(type) && action) {
if (action === "makePhoneCall") {
let { phoneNumber = "" } = params;
if (!phoneNumber || phoneNumber.replace(/\s/g, "") == "") {
Taro.showToast({
icon: "none",
title: "未查询到号码,无法呼叫哦~",
});
return;
}
}
let res = await Taro[action]({ ...params });
// 扫码事件,需要在扫码完成后发送全局广播,业务内自行处理
if (action === "scanCode" && params.callback) {
let eventName = `${params.callback}_event`;
eventCenter.trigger(eventName, res);
}
}
// 打开小程序
if (
["NATIVE_BUTTON_API"].includes(type) &&
["miniprogram"].includes(action)
) {
await Taro.navigateToMiniProgram({
...params,
});
}
}
render() {
const { parsed, openType, origin } = this.state;
return (
<Button
onClick={this.handleClick.bind(this, parsed, origin)}
hoverClass="none"
openType={openType}
>
{this.props.children}
</Button>
);
}
}
在具体业务中使用
// pages/index/index.tsx
import router from "/utils/router";
import Router from "/components/router";
// js方式直接跳转
router.routeURL('https://host?cmd=detail&skuid=1')
// UI组件方式
...
render(){
return <Router path='https://host?cmd=detail&skuid=1'></Router>
}
...
当然这里面可以附加你自己需要的功能,比如:增加跳转方式控制、数据处理、埋点、加锁防连续点击,相对来说并不复杂。甚至你还可以顺手实现一下上面提到的 navigateBack 传参。
结语
上文的思考及实现过程比较简单,纯属抛砖引玉,欢迎大家交流互动。
作者:胖纳特
链接:https://juejin.cn/post/6930899487250448398
收起阅读 »
如何美化checkbox
前言
对于前端开发人员,checkbox应该是经常见到的东西。利用checkbox的checked属性,我们可以做出很多精彩的效果,之前还用checkbox来做动画暂停。前几天还看到外国大佬使用 checkbok做游戏:http://www.bryanbraun.com/2021/09/21/… ,真的是佩服的五体投地,不过对于我这种菜鸡选手,还是只能实现一些简单的东西。对于下面的这个switch按钮,大家应该非常熟悉了,同样的在这个效果上还衍生出了各种华丽花哨的效果,例如暗黑模式的切换。一生万,掌握了一,万!还不是手到擒来。
推荐大家看看codepen上的这个仓库:文章封面的效果,也是从这里录制的!
tql
codepen.io/oliviale/pe…
标签
这里使用for将label和input捆绑
<input type="checkbox" id="toggle" />
<label for="toggle"></label>
同时设置input不可见
input {
display: none;
}
美化label
遇到checkbox的美化问题,基本上都是考虑用美化labl替代美化input。
设置背景颜色,宽高,以及圆角
.switch {
display: inline-block;
display:relative;
width: 40px;
height: 20px;
background-color: rgba(0, 0, 0, 0.25);
border-radius: 20px;
}
最终的效果如下:
切换的圆
在label上会有一个圆,一开始是在左边的,效果如下,其实这个只需要利用伪元素+positon定位,就可以实现了。
这是postion:absolute,同时将位置定位在top1px,left1px。同时设置圆角。
.switch:after {
content: "";
position: absolute;
width: 18px;
height: 18px;
border-radius: 18px;
background-color: white;
top: 1px;
left: 1px;
transition: all 0.3s;
}
checked+小球右移动
这里点击之后圆会跑到右边,这里有两种实现方案
1.仍然通过定位
当checkbox处于checked状态,会设置top,left,bottom,right。这里将top,left设置为auto是必须的,这种的好处就是,不需要考虑label的宽度。
input[type="checkbox"]:checked + .switch:after {
top: auto;
left: auto;
bottom: 1px ;
right: 1px ;
}
当然知道label的宽度可以直接,设置top和left
top: 1px;
left: 21px;
2.translateX
*transform: translateX(20px)*
美化切换后的label
加上背景色
input[type="checkbox"]:checked + .switch {
background-color: #7983ff;
}
效果:
后记
看上去本文是一篇介绍一个checkbox美化的效果,其实是一篇告诉你如何美化checkbox的文章,最终的思想就是依赖for的捆绑效果,美化label来达到最终的效果。
作者:半夏的故事
链接:https://juejin.cn/post/7035650204829220877
收起阅读 »
Metaverse 已经到来:5 家公司正在构建我们的虚拟现实未来
如果你相信 Facebook,未来就是一个虚拟现实的“元宇宙”。
这家上个月更名为 Meta的科技巨头计划今年投资100 亿美元来开发支持增强现实和虚拟现实的产品——机械手、高科技 VR 眼镜和复杂的软件应用程序,仅举几例。分析师预计该公司至少要花费 500 亿美元来实现其对虚拟现实未来的承诺。
但 Meta 远非唯一的玩家。事实上,六家其他公司已经在构建将成为下一代虚拟交互的硬件和软件——华尔街认为这是一个价值 1 万亿美元的市场。这些公司包括谷歌、微软、苹果、Valve 和其他开发工作和通信产品的公司。随着投资者涌入市场,规模较小的初创公司可能会加入他们的行列。
“元宇宙是真实的,华尔街正在寻找赢家,”韦德布什分析师丹艾夫斯在一份报告中说。
在 Facebook 试图在元领域打上烙印时,这些公司的产品将不得不与之抗衡。
谷歌
Google Cardboard 可能是历史上最成功的 VR 项目。2014 年,当时世界上最大的科技公司要求数百万人用一块硬纸板将智能手机绑在脸上。谷歌表示,它出货了“数千万”可折叠耳机,谷歌 Cardboard应用程序的下载量超过 1.6 亿次。这不是最高分辨率或高科技的体验,但该策略帮助向数百万学生和有抱负的开发人员介绍了虚拟现实。
它还帮助谷歌摆脱了之前的增强现实实验Glass。今天,增强现实眼镜作为企业业务的工具进行销售,但当它推出时,谷歌的期望值很高。字面意思是:谷歌创始人谢尔盖·布林 (Sergey Brin) 从飞机上跳下时宣布了 1,500 美元的产品。
玻璃本质上是智能手机的内脏,在非处方眼镜框架上安装了一个小型摄像头。该项目失败了,但在催生了无数模因之前就失败了。
微软
微软于 2015 年发布了Hololens混合现实眼镜。一年后,微软并没有用营销炒作充斥消费者市场,而是悄悄推出了 Hololens 作为工业制造工具,面向选定的企业集团。价值 3,000 美元的商业套件附带专业版 Windows,具有额外的安全功能和软件以帮助应用程序开发。第二次迭代于 2019 年首次亮相,价格稍贵,但拥有更好的相机和镜头卡口,可实现更精确的操作,并提供更广泛的软件功能,包括工业应用。
目前Hololens用户包括像肯沃斯,三得利和丰田,它使用耳机,以加快培养和汽车修理重量级人物,根据微软。
苹果
如果你相信的传言,苹果一直在释放的风口浪尖AR眼镜多年。这家 iPhone 制造商于 2017 年在 iOS 11 中发布了ARKit,这是为 Apple 设备创建增强现实应用程序的开发者框架。 据科技网站The Information报道,Apple 在 2019 年举行了一次 1000 人的会议,讨论了 iPhone 上的 AR 和两个潜力未来的产品,N421 智能眼镜和 N301 VR 耳机。分析师现在推测,苹果正准备在 2022 年及以后发布 AR 产品。
阀门
Valve 的Index耳机可以说是市场上最强大的消费虚拟现实产品。高分辨率屏幕流畅,控制器在虚拟现实和游戏环境中提供无与伦比的控制。该索引还与 Value 的Steam视频游戏市场集成,这意味着该设备已经堆满了兼容的内容。
它也很贵而且很笨拙。完整的 Index VR 套件的价格接近 1,000 美元,要正常运行,耳机需要多条电缆和传感器。Valve 继续创新和试验沉浸式虚拟现实耳机。分析师预计,这家总部位于贝尔维尤的游戏公司将很快发布一款独立耳机,与 Facebook 的Oculus Quest 2展开竞争。
魔法飞跃
尽管虚拟现实的想法部分受到科幻小说的启发,但 Big Tech 对 AR 和 VR 未来的现代愿景直接受到 Magic Leap 的启发。该公司成立于 2010 年,2014 年从谷歌和芯片制造商高通等公司筹集了超过 5 亿美元 。2015 年,该公司发布了一段令人惊叹的视频,旨在展示该产品的技术。但是怀疑论者质疑这项技术,最终的产品遭到了抨击。
最初的 Magic Leap 耳机是在设计和广告等创意协作行业销售的。
Magic Leap于 2018 年推出了一款精致的 AR 设备,筹集了更多资金,并计划在 2022 年初发布Magic Leap 2。该公司还计划瞄准国防、医疗保健和工业制造。
收起阅读 »跨域问题及常见解决方法
1.出现跨域问题是因为浏览器的同源策列限制,下面是MDN文档对浏览器同源策略的描述,简单来说就是:
同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)
浏览器的同源策略
同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
同源的定义
如果两个 URL 的 protocol、port (en-US) (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。
2.四种常见解决跨域的方法:
一,CORS:
跨域资源共享,它允许浏览器向非同源服务器,发出XMLHttpRequest请求。它对一般请求和非一般请求的处理方式不同: 1、一般跨域请求(对服务器没有要求):只需服务器端设置Access-Control-Allow-Origin 2、非一般跨域请求(比如要请求时要携带cookie):前后端都需要设置。
一般跨域请求服务器设置代码: (1)Node.JS
const http = require('http');
const server = http.createServer();
const qs = require('querystring');
server.on('request', function(req, res) {
var postData = '';
// 数据块接收中
req.addListener('data', function(chunk) {
postData += chunk;
});
// 数据接收完毕
req.addListener('end', function() {
postData = qs.parse(postData);
// 跨域后台设置
res.writeHead(200, {
'Access-Control-Allow-Credentials': 'true', // 后端允许发送Cookie
'Access-Control-Allow-Origin': 'http://www.example.com', // 允许访问的域(协议+域名+端口)
});
res.end(JSON.stringify(postData));
});
});
server.listen('8080');
console.log('running at port 8080...');
复制代码
(2)PHP
<?php
header("Access-Control-Allow-Origin:*");
复制代码
如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
前端请求携带cookie代码:
(1)原生JavaScript
const xhr = new XMLHttpRequest();
// 前端设置是否带cookie
xhr.withCredentials = true;
};
复制代码
(2)axios
axios.defaults.withCredentials = true
复制代码
二,JSONP
JSONP 只支持get请求,不支持post请求。 核心思想:网页通过添加一个<scriot>
标签,向服务器请求 JSON 数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。
原生JavaScript代码:
<script src="http://example.php?callback=getData"></script>
// 向服务器发出请求,请求参数callback是下面定义的函数名字
// 处理服务器返回回调函数的数据
<script type="text/javascript">
function getData(res)
{
console.log(res.data)
}
</script>
复制代码
三,设置document.domain
因为浏览器是通过document.domain属性来检查两个页面是否同源,因此只要通过设置相同的document.domain,两个页面就可以共享Cookie(此方案仅限主域相同,子域不同的跨域应用场景。)
// 两个页面都设置
document.domain = 'test.com';
复制代码
四,跨文档通信 API:window.postMessage()
调用postMessage方法实现父窗口向子窗口发消息(子窗口同样可以通过该方法发送消息给父窗口)
var openWindow = window.open('http://test2.com', 'title');
// 父窗口向子窗口发消息(第一个参数代表发送的内容,第二个参数代表接收消息窗口的url)
openWindow.postMessage('Nice to meet you!', 'http://test2.com');
//调用message事件,监听对方发送的消息
// 监听 message 消息
window.addEventListener('message', function (e) {
console.log(e.source); // e.source 发送消息的窗口
console.log(e.origin); // e.origin 消息发向的网址
console.log(e.data); // e.data 发送的消息
},false);
作者:玩具大兵
来源:https://juejin.cn/post/7035562059152490526
TypeScript 原始类型、函数、接口、类、泛型 基础总结
原始数据类型
原始数据类型包括:
Boolean
String
Number
Null
undefined
类型声明是TS非常重要的一个特点,通过类型声明可以指定TS中变量、参数、形参的类型。
Boolean 类型
let boolean: boolean = true
boolean = false
boolean = null
// bollean = 123 报错不可以将数字 123 赋值给 boolean类型的变量Number 类型
//ES6 Number 类型 新增支持2进制和8进制
let num: number = 123
num = 0b1111String 类型
let str1: string = 'hello TS'
let sre2: string = `模板字符串也支持使用 ${str1}`]Null 和 Undefined
let n: null = null
let u: undefined = undefined
n = undefined
u = null
// undefined 和 null 是所有类型的子类型 所以可以赋值给number类型的变量
let num: number = 123
num = undefined
num = null
any 类型
any 表示队变量没有任何显示,编译器失去了对 TS 的检测功能与 JS 无异(不建议使用)。
let notSure: any = 4
// any类型可以随意赋值
notSure = `任意模板字符串`
notSure = true
notSure = null
// 当 notSure 为any 类型时,在any类型上访问任何属性和调用方法都是允许的, 很有可能出现错误
notSure.name // 现在调用name属性是允许的,但很明显我们定义的notSure没有name这个属性,下面的调用sayName方法也是如此
notSure.sayName()
array 类型
// 数组类型,可以指定数组的类型和使用数组的方法和属性
let arrOfNumbers: number[] = [1, 2, 3]
console.log(arrOfNumbers.length);
arrOfNumbers.push(4)
tuple 元组类型
// 元组类型 元组就是固定长度,类型的数组
// 类型和长度必须一致
let u: [string, number] = ['12', 12]
// let U: [string, number] = ['12', 12, true] 报错信息为:不能将类型“[string, number, boolean]”分配给类型“[string, number]”。源具有 3 个元素,但目标仅允许 2 个。
// 也可以使用数组的方法,如下所示push一个值给元组u
u.push(33)
Interface 接口
对对象的形状(shape)进行描述
Duck Typing(鸭子类型)
interface Person {
// readonly id 表示只读属性的id不可以修改
readonly id: number;
name: string;
age: number;
// weight? 表示可选属性,可以选用也可以不选用
weight?: number
}
let host: Person = {
id: 1,
name: 'host',
age: 20,
weight: 70
}
//host.id = 2 报错 提示信息为:无法分配到 "id" ,因为它是只读属性
function 函数类型
// 方式一:函数声明的写法 z 为可选参数 ,
function add1 (x: number, y: number, z?: number): number {
if (typeof z === 'number') {
return x + y + z
} else {
return x + y
}
}
// 需要注意的是:可选参数必须置于所有必选参数之后,否则会报错
add1(1, 2, 3)
// 方式二:函数表达式
const add2 = (x: number, y: number, z?: number): number => {
if (typeof z === 'number') {
return x + y + z
} else {
return x + y
}
}
// 使用interface接口 描述函数类型
interface ISum {
(x: number, y: number, z?: number): number
}
let add3: ISum = add1
值的注意的是:可选参数必须置于所有必选参数之后,否则会报错,如下图展示的错误案例所示:·
类型推论
当定义变量时没有指定类型,编译器会自动推论第一次赋的值为默认类型
let s = 'str'
// s = 12 本句将会报错,提示为:不能将类型“number”分配给类型“string”
联合类型
使用|
分隔可选类型
let StringOrNumber: string | number
StringOrNumber = 123
StringOrNumber = '111'
类型断言
使用 as
关键字进行类型断言
function getLength (rod: number | string): number {
const str = rod as string
//这里我们可以用 as 关键字,告诉typescript 编译器,你没法判断我的代码,但是我本人很清楚,这里我就把它看作是一个 string,你可以给他用 string 的方法。
if (str.length) {
return str.length
} else {
const num = rod as number
return num.toString().length
}
}
类型守卫
// 4.类型守卫 type guard typescript 在不同的条件分支里面,智能的缩小了范围
function getLength2 (rod: number | string): number {
if (typeof rod === 'string') {
return rod.length
} else {
// else 里面的rod 会自动默认为number类型
return rod.toString().length
}
}
Class 类
面向对象编程的三大特点:
封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,
继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性。
多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。
话不多少,看代码:
class Animal {
readonly name: string
constructor(name: string) {
this.name = name
console.log(this.run())
}
// private run ():私有的 protected run () 受保护的
run () {
return `${this.name} is running`
}
}
const animal = new Animal('elephant')
// console.log(animal.name)
animal.run() //elephant is running
// 继承
class Dog extends Animal {
age: number
constructor(name, age) {
super(name)
console.log(this.name)
this.age = age
}
bark () {
console.log(`这只在叫的狗狗叫${this.name},它今年${this.age}岁了`)
}
}
const dog = new Dog('旺财', 5)
dog.run() // 旺财 is running
dog.bark() // 这只在叫的狗狗叫旺财,它今年5岁了
// 多态
class Cat extends Animal {
static catAge = 2
constructor(name) {
super(name)
console.log(this.name) // 布丁
}
run () {
return 'Meow,' + super.run()
}
}
const cat = new Cat('布丁')
console.log(cat.run()) // Meow,布丁 is running
console.log(Cat.catAge) // 2
class中还提供了readonly关键字,readonly为只读属性,在调用的时候不能修改。如下所示:
类成员修饰符
public修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是public的。
private修饰的属性或方法是私有的,不能在声明它的类的外部访问。
上述示例代码中,在父类Animal的 run 方法身上加上private修饰符之后就会产生如下图的报错信息:
protected 修饰的属性或方法是受保护的,它和private类似,区别在于它在子类中也是可以访问的。
上述示例代码中,在父类Animal的 run 方法身上加上protected修饰符之后就会产生如下图的报错信息:
接口和类
类可以使用 implements来实现接口。
// interface可以用来抽象验证类的方法和方法
interface Person {
Speak (trigger: boolean): void;
}
interface Teenagers {
Young (sge: number): void
}
// 接口之间的继承
interface PersonAndTeenagers extends Teenagers {
Speak (trigger: boolean): void;
}
// implements 实现接口
class Boy implements Person {
Speak (mouth: boolean) { }
}
// class Girl implements Person, Teenagers 和 class Girl implements PersonAndTeenagers 作用相同
class Girl implements PersonAndTeenagers {
Speak (mouth: boolean) { }
Young (sge: number) { }
}
enum枚举
枚举成员会被赋值为从 0
开始递增的数字,同时也会对枚举值到枚举名进行反向映射:
enum Color {
red = 'red',
blue = 'blue',
yellow = 'yellow',
green = 'green'
}
// 常量枚举
const enum Color {
red = 'red',
blue = 'blue',
yellow = 'yellow',
green = 'green'
}
console.log(Color.red) // 0
// 反向映射
console.log(Color[0]) // red
const value = 'red'
if (value === Color.red) {
console.log('Go Go Go ')
常量枚举经过编译后形成的js文件如下:
非常量枚举经过编译器编译之后的js文件如下:
Generics 泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
约束泛型
类与泛型
接口与泛
示例代码如下:
function echo (arg) {
return arg
}
let result1 = echo(123) // 参数传递123后result1 的类型为any
// 泛型
function echo2<T> (arg: T): T {
return arg
}
let result2 = echo2(123) // 加上泛型之后 参数传递 123后result2的类型为number
function swap<T, U> (tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]]
}
console.log(swap(['hero', 123]))//[ 123, 'hero' ]
// 约束泛型
interface IWithLength {
length: number
}
function echoWithLength<T extends IWithLength> (arg: T): T {
console.log(arg.length)
return arg
}
const str = echoWithLength('123')
const obj = echoWithLength({ length: 3, name: 'Tom' })
const arr = echoWithLength([1, 2, 3, 4])
// 类与泛型
class Queue<T> {
private data = []
push (item: T) {
return this.data.push(item)
}
pop (): T {
return this.data.shift()
}
}
const queue = new Queue<number>()
queue.push(1)
console.log(queue.pop().toFixed())// 1
// 接口与泛型
interface KeyPair<T, U> {
key: T
value: U
}
let kp1: KeyPair<string, number> = { key: 'str', value: 123 }
let kp2: KeyPair<number, string> = { key: 123, value: 'str' }
let arr2: Array<string> = ['1', '2'] // 使用 Array<string> 等价于 interface Array<T>
类型别名 type-alias
类型别名,就是给类型起一个别名,让它可以更方便的被重用。
let sum: (x: number, y: string) => number
const result1 = sum(1, '2')
// 将(x: number, y: string) => number类型取一个别名 为 PlusType
type PlusType = (x: number, y: string) => number
let sum2: PlusType
const result2 = sum2(2, '2')
type StrOrNum = string | number
let result3: StrOrNum = 123
result3 = '123'
字面量
let Name: 'name' = 'name'
// Name = '123' //报错信息:不能将类型“"123"”分配给类型“"name"”
let age: 19 = 19
type Directions = 'Up' | 'Down' | 'Left' | 'Right'
let up: Directions = 'Up'
交叉类型
// 交叉类型 使用 ‘&’ 符号进行类型的扩展
interface IName {
name: string
}
type IPerson = IName & { age: number }
let person: IPerson = { name: 'Tom', age: 19 }
内置类型
全局对象
// global objects 全局对象
const a: Array<string> = ['123', '456']
const time = new Date()
time.getTime()
const reg = /abc/ // 此时reg为RegExp类型
reg.test('abc')
build-in object 内置对象
Math.pow(2, 2) //返回 2 的 2次幂。
console.log(Math.pow(2, 2)) // 4DOM and BOM
// document 对象,返回的是一个 HTMLElement
let body: HTMLElement = document.body
// document 上面的query 方法,返回的是一个 nodeList 类型
let allLis = document.querySelectorAll('li')
//当然添加事件也是很重要的一部分,document 上面有 addEventListener 方法,注意这个回调函数,因为类型推断,这里面的 e 事件对象也自动获得了类型,这里是个 mouseEvent 类型,因为点击是一个鼠标事件,现在我们可以方便的使用 e 上面的方法和属性。
document.addEventListener('click', (e) => {
e.preventDefault()
})utility types实用类型
interface IPerson2 {
name: string
age: number
}
let viking: IPerson2 = { name: 'viking', age: 20 }
// partial,它可以把传入的类型都变成可选
type IPartial = Partial<IPerson>
let viking2: IPartial = {} // Partial 将IPerson 中的类型变成了可选类型 所以 viking2 可以等于一个空对象
// Omit,它返回的类型可以忽略传入类型的某个属性
type IOmit = Omit<IPerson2, 'name'> // 忽略name属性
let viking3: IOmit = { age: 20 }
如果加上name属性将会报错:不能将类型“{ age: number; name: string; }”分配给类型“IOmit”。对象文字可以只指定已知属性,并且“name”不在类型“IOmit”中。
作者:不一213
来源:https://juejin.cn/post/7035563509882552334
神奇的交叉观察器 - IntersectionObserver
1. 背景
网页开发时,不管是在移动端,还是PC端,都有个很重要的概念,叫做动态懒加载,适用于一些图片资源(或者数据)特别多的场景中,这个时候,我们常常需要了解某个元素是否进入了“视口”(viewport),即用户能不能看到它。
传统的实现方法是,监听到scroll
事件或者使用setInterval
来判断,调用目标元素的getBoundingClientRect()
方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll
事件触发频率高,计算量很大,如果不做防抖节流的话,很容易造成性能问题,而setInterval
由于其有间歇期,也会出现体验问题。
所以在几年前,Chrome率先提供了一个新的API
,就是IntersectionObserver
,它可以用来自动监听元素是否进入了设备的可视区域之内,而不需要频繁的计算来做这个判断。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做”交叉观察器”。
2. 兼容性
由于这个api问世已经很多年了,所以对浏览器的支持性还是不错的,完全可以上生产环境,点击这里
可以看看当前浏览器对于IntersectionObserver
的支持性:
3. 用法
该API
的调用非常简单:
const io = new IntersectionObserver(callback, option);
上面代码中,IntersectionObserver
是浏览器原生提供的构造函数,接受两个参数:
callback
:可见性发现变化时的回调函数option
:配置对象(可选)。
构造函数的返回值是一个观察器实例。实例一共有4个方法:
observe
:开始监听特定元素unobserve
:停止监听特定元素disconnect
:关闭监听工作takeRecords
:返回所有观察目标的对象数组
3.1 observe
该方法需要接收一个target参数,值是Element类型,用来指定被监听的目标元素
// 获取元素
const target = document.getElementById("dom");
// 开始观察
io.observe(target);
3.2 unobserve
该方法需要接收一个target参数,值是Element类型,用来指定停止监听的目标元素
// 获取元素
const target = document.getElementById("dom");
// 停止观察
io.unobserve(target);
3.3 disconnect
该方法不需要接收参数,用来关闭观察器
// 关闭观察器
io.disconnect();
3.4 takeRecords
该方法不需要接收参数,返回所有被观察的对象,返回值是一个数组
// 获取被观察元素
const observerList = io.takeRecords();
注意:
observe
方法的参数是一个 DOM 节点,如果需要观察多个节点,就要多次调用这个方法:
// 开始观察多个元素
io.observe(domA);
io.observe(domB);
io.observe(domC);
4. callback 参数
目标元素的可见性变化时,就会调用观察器的回调函数callback
。
callback
一般会触发两次。一次是目标元素刚刚进入视口,另一次是完全离开视口。
const io = new IntersectionObserver((changes, observer) => {
console.log(changes);
console.log(observer);
});
上面代码中,callback
函数的参数接收两个参数changes
和observer
:
changes
:这是一个数组,每个成员都是一个被观察对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,那么changes
数组里面就会打印出两个元素,如果只观察一个元素,我们打印changes[0]
就能获取到被观察对象observer
: 这是一个对象,返回我们在实例中传入的第二个参数option(如果没传,则返回默认值)
5. IntersectionObserverEntry 对象
上面提到的changes数组中的每一项都是一个IntersectionObserverEntry 对象(下文简称io对象),对象提供目标元素的信息,一共有八个属性,我们打印这个对象:
// 创建实例
const io = new IntersectionObserver(changes => {
changes.forEach(change => {
console.log(change);
});
});
// 获取元素
const target = document.getElementById("dom");
// 开始监听
io.observe(target);
运行上面代码,并且改变dom的可见性,这时控制台可以看到一个对象:
每个属性的含义如下:
boundingClientRect
:目标元素的矩形区域的信息intersectionRatio
:目标元素的可见比例,即intersectionRect
占boundingClientRect
的比例,完全可见时为1
,完全不可见时小于等于0
intersectionRect
:目标元素与视口(或根元素)的交叉区域的信息isIntersecting
: 布尔值,目标元素与交集观察者的根节点是否相交isVisible
: 布尔值,目标元素是否可见(该属性还在试验阶段,不建议在生产环境中使用)rootBounds
:根元素的矩形区域的信息,getBoundingClientRect()
方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
target
:被观察的目标元素,是一个 DOM 节点对象time
:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
6. 应用
- 预加载(滚动加载,翻页加载,无限加载)
- 懒加载(后加载、惰性加载)
- 其它
7. 注意点
IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。
规格写明,IntersectionObserver
的实现,应该采用requestIdleCallback()
,即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。
8. 参考链接
作者:三年没洗澡
来源:https://juejin.cn/post/7035490578015977480
js打包时间缩短90%,bundleless生产环境实践总结
最近尝试将bundleless的构建结果直接用到了线上生产环境,因为bundleless只会编译代码,不会打包,因此构建速度极快,同比bundle模式时间缩短了90%以上。得益于大部分浏览器都已经支持了http2和浏览器的es module,对于我们没有强兼容场景的中后台系统,将bundleless构建结果应用于线上是有可能的。本文主要介绍一下,本人在使用bundleless构建工具实践中遇到的问题。
起源
结合snowpack实践
snowpack的Streaming Imports
性能比较
总结
附录snowpack和vite的对比
本文原文来自我的博客: github.com/fortheallli…
一、起源
1.1 从http2谈起
以前因为http1.x不支持多路服用, HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制.因此我们需要做的就是将同域的一些静态资源比如js等,做一个资源合并,将多次请求不同的js文件,合并成单次请求一个合并后的大js文件。这就是webpack等bundle工具的由来。
而从http2开始,实现了TCP链接的多路复用,因此同域名下不再有请求并发数的限制,我们可以同时请求同域名的多个资源,这个并发数可以很大,比如并发10,50,100个请求同时去请求同一个服务下的多个资源。
因为http2实现了多路复用,因此一定程度上,将多个静态文件打包到一起,从而减少请求次数,就不是必须的
主流浏览器对http2的支持情况如下:
除了IE以外,大部分浏览器对http2的支持性都很好,因为我的项目不需要兼容IE,同时也不需要兼容低版本浏览器,不需要考虑不支持http2的场景。(这也是我们能将不bundle的代码用于线上生产环境的前提之一)
1.2 浏览器esm
对于es modules,我们并不陌生,什么是es modules也不是本文的重点,一些流行的打包构建工具比如babel、webpack等早就支持es modules。
我们来看一个最简单的es modules的写法:
//main.js
import a from 'a.js'
console.log(a)
//a.js
export let a = 1
上述的es modules就是我们经常在项目中使用的es modules,这种es modules,在支持es6的浏览器中是可以直接使用的。
我们来举一个例子,直接在浏览器中使用es modules
<html lang="en">
<body>
<div id="container">my name is {name}</div>
<script type="module">
import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js'
new Vue({
el: '#container',
data:{
name: 'Bob'
}
})
</script>
</body>
</html>
上述的代码中我们直接可以运行,我们根据script的type="module"可以判断浏览器支不支持es modules,如果不支持,该script里面的内容就不会运行。
首先我们来看主流浏览器对于ES modules的支持情况:
从上图可以看出来,主流的Edge, Chrome, Safari, and Firefox (+60)等浏览器都已经开始支持es modules。
同样的因为我们的中后台项目不需要强兼容,因此不需要兼容不支持esm的浏览器(这也是我们能将不bundle的代码用于线上生产环境的前提之二)。
1.3 小结
浏览器对于http2和esm的支持,使得我们可以减少模块的合并,以及减少对于js模块化的处理。
如果浏览器支持http2,那么一定程度上,我们不需要合并静态资源
如果浏览器支持esm,那么我们就不需要通过构建工具去维护复杂的模块依赖和加载关系。
这两点正是webpack等打包工具在bundle的时候所做的事情。浏览器对于http2和esm的支持使得我们减少bundle代码的场景。
二、结合snowpack实践
我们比较了snowpack和vite,最后选择采用了snowpack(选型的原因以及snowpack和vite的对比看最后附录),本章节讲讲如何结合snowpack构建工具,构建出不打包形式的线上代码。
2.1 snowpack的基础用法
我们的中后台项目是react和typescript编写的,我们可以直接使用snowpack相应的模版:
npx create-snowpack-app myproject --template @snowpack/app-template-react-typescript
snowpack构建工具内置了tsc,可以处理tsx等后缀的文件。上述就完成了项目初始化。
2.2 前端路由处理
前端路由我们直接使用react-router或者vue-router等,需要注意的时,如果是在开发环境,那么必须要指定在snowpack.config.mjs配置文件,在刷新时让匹配到前端路由:
snowpack.config.mjs
...
routes: [{ match: 'routes', src: '.*', dest: '/index.html' }],
...
类似的配置跟webpack devserver等一样,使其在后端路由404的时候,获取前端静态文件,从而执行前端路由匹配。
2.3 css、jpg等模块的处理
在snowpack中同样也自带了对css和image等文件的处理。
css
以sass为例,
snowpack.config.mjs
plugins: [
'@snowpack/plugin-sass',
{
/* see options below */
},
],
只需要在配置中增加一个sass插件就能让snowpack支持sass文件,此外,snowpack也同样支持css module。.module.css或者.module.scss命名的文件就默认开启了css module。此外,css最后的结果都是通过编译成js模块,通过动态创建style标签,插入到body中的。
//index.module.css文件
.container{
padding: 20px;
}
snowpack构建处理后的css.proxy.js文件为:
export let code = "._container_24xje_1 {\n padding: 20px;\n}";
let json = {"container":"_container_24xje_1"};
export default json;
// [snowpack] add styles to the page (skip if no document exists)
if (typeof document !== 'undefined') {
const styleEl = document.createElement("style");
const codeEl = document.createTextNode(code);
styleEl.type = 'text/css';
styleEl.appendChild(codeEl);
document.head.appendChild(styleEl);
}
上述的例子中我们可以看到。最后css的构建结果是一段js代码。在body中动态插入了style标签,就可以让原始的css样式在系统中生效。
jpg,png,svg等
如果处理的是图片类型,那么snowpack同样会将图片编译成js.
//logo.svg.proxy.js
export default "../dist/assets/logo.svg";
snowpack没有对图片做任何的处理,只是把图片的地址,包含到了一个js模块文件导出地址中。值得注意的是在浏览器es module中,import 动作类似一个get请求,import from可以是一个图片地址,浏览器es module自身可以处理图片等形式。因此在.js文件结尾的模块中,export 的可以是一个图片。
snowpack3.5.0以下的版本在使用css module的时候会丢失hash,需要升级到最新版本。
2.4 按需加载处理
snowpack默认是不打包的。只对每一个文件都做一些简单的模块处理(将非js模块转化成js模块)和语法处理,因此天然支持按需加载,snowpack支持React.lazy的写法,在react的项目中,只要正常使用React.Lazy就能实现按需加载。
2.5 文件hash处理
在最后构建完成后,在发布构建结果的时候,为了处理缓存,常见的就是跟静态文件增加hash,snowpack也提供了插件机制,插件会处理snowpack构建前的所有文件的内容,做为content转入到插件中,经过插件的处理转换后得到新的content.
可以通过snowpack-files-hash插件来实现给文件增加hash。
2.6 公用esm模块托管
snowpack对于项目构建的bundleless的代码可以直接跑在线上,在bundless的构建结果中,我们想进一步减少构建结果文件大小。以bundleless的方式构建的代码,默认在处理三方npm包依赖的时候,虽然不会打包,snowpack对项目中node_modules中的依赖重新编译成esm形式,然后放在一个新的静态目录下。因此最后构建的代码包含了两个部分:
项目本身的代码,将node_modules中的依赖处理成esm后的静态文件。
其中node_modules中的依赖处理成esm后的静态文件,可以以cdn或者其他服务形式来托管。这样我们每次都不需要在构建的时候处理node_modules中的依赖。在项目本身的代码中,如果引用了npm包,只需要将其指向一个cdn地址即可。这样处理后的,构建的代码就变成:
只有项目本身的代码(项目中对于三方插件的引入,直接使用三方插件的cdn地址)。
进一步想,如果我们使用了托管所有npm包(es module形式)的cdn地址之后,那么在本地开发或者线上构建的过程中,我们甚至不需要去维护本地的node_modules目录,以及yarn-lock或者package-lock文件。我们需要做的,仅仅是一个map文件进行版本管理。保存项目中的npm包名和该包相对应的cdn地址。
比如:
//config.map.json
{
"react": "https://cdn.skypack.dev/react@17.0.2",
"react-dom": "https://cdn.skypack.dev/react-dom@17.0.2",
}
通过这个map文件,不管是在开发还是线上,只要把:
import React from 'react'
替换成
import React from "https://cdn.skypack.dev/react@17.0.2"
就能让代码在开发环境或者生产环境中跑起来。如此简化之后,我们不论在开发环境还是生产环境都不需要在本地维护node_modules相关的文件,进一步可以减少打包时间。同时包管理也更加清晰,仅仅是一个简单的json文件,一对固定意义的key/value,简单纯粹。
我们提到了一个托管了的npm包的有es module形式的cdn服务,上述以skypack为例,这对比托管了npm包cjs形式的cdn服务unpkg,两者的区别就是,unpkg所托管的npm包,大部分是cjs形式的,cjs形式的npm包,是不能直接用于浏览器的esm中的。skypack所做的事情就是将大部分npm包从cjs形式转化成esm的形式,然后存储和托管esm形式的结果。
三、snowpack的Streaming Imports
在2.7中我们提到了在dev开发环境使用了skypack,那么本地不需要node_modules,甚至不需要yarn-lock和package-lock等文件,只需要一个json文件,简单的、纯粹的,只有一对固定意义的key/value。在snowpack3.x就提供了这么一个功能,称之为Streaming Imports。
3.1 snowpack和skypack
在snowpack3.x在dev环境支持skypack:
// snowpack.config.mjs
export default {
packageOptions: {
source: 'remote',
},
};
如此,在dev的webserver过程中,就是直接下载skypack中相应的esm形式的npm包,放在最后的结果中,而不需要在本地做一个cjs到esm的转换。这样做有几点好处:
速度快: 不需要npm install一个npm包,然后在对其进行build转化成esm,Streaming Imports可以直接从一个cdn地址直接下载esm形式的依赖
安全:业务代码中不需要处理公共npm包cjs到esm的转化,业务代码和三方依赖分离,三方依赖交给skypack处理
3.2 依赖控制
Streaming Imports自身也实现了一套简单的依赖管理,有点类似go mod。是通过一个叫snowpack.deps.json文件来实现的。跟我们在2.7中提到的一样,如果使用托管cdn,那么本地的pack-lock和yarn-lock,甚至node_modules是不需要存在的,只需要一个简单纯粹的json文件,而snowpack中就是通过snowpack.deps.json来实现包的依赖管理的。
我们安装一个npm包时,我们以安装ramda为例:
npx snowpack ramda
在snowpack.deps.json中会生成:
{
"dependencies": {
"ramda": "^0.27.1",
},
"lock": {
"ramda#^0.27.1": "ramda@v0.27.1-3ePaNsppsnXYRcaNcaWn",
}
}
安装过程的命令行如下所示:
从上图可以看出来,通过npx snowpack安装的依赖是从skypack cdn直接请求的。
特别的,如果项目需要支持typescript,那么我们需要将相应的npm包的声明文件types下载到本地,skypack同样也支持声明文件的下载,只需要在snowpack的配置文件中增加:
// snowpack.config.mjs
export default {
packageOptions: {
source: 'remote',
types:true //增加type=true
},
};
snowpack会把types文件下载到本地的.snowpack目录下,因此在tsc编译的时候需要指定types的查找路径,在tsconfig.json中增加:
//tsconfig.json
"paths": {
"*":[".snowpack/types/*"]
},
3.3 build环境
snowpack的Streaming Imports,在dev可以正常工作,dev的webserver中在请求npm包的时候会将请求代理到skypack,但是在build环境的时候,还是需要其他处理的,在我们的项目中,在build的时候可以用一个插件snowpack-plugin-skypack-replacer,将build后的代码引入npm包的时候,指向skypack。
build后的线上代码举例如下:
import * as __SNOWPACK_ENV__ from '../_snowpack/env.271340c8a413.js';
import.meta.env = __SNOWPACK_ENV__;
import ReactDOM from "https://cdn.skypack.dev/react-dom@^17.0.2";
import App from "./App.e1841499eb35.js";
import React from "https://cdn.skypack.dev/react@^17.0.2";
import "./index.css.proxy.9c7da16f4b6e.js";
const start = async () => {
await ReactDOM.render(/* @__PURE__ */ React.createElement(App, null), document.getElementById("root"));
};
start();
if (undefined /* [snowpack] import.meta.hot */ ) {
undefined /* [snowpack] import.meta.hot */ .accept();
}
从上述可以看出,build之后的代码,通过插件将:
import React from 'react'
//替换成了
import React from "https://cdn.skypack.dev/react@^17.0.2";
四、性能比较
4.1 lighthouse对比
简单的使用lighthouse来对比bundleless和bundle两种不同构建方式网页的性能。
bundleless的前端简单性能测试:
bundle的前端性能测试:
对比发现,这里两个网站都是同一套代码,相同的部署环境,一套是构建的时候是bundleless,利用浏览器的esm,另一个是传统的bundle模式,发现性能上并没有明显的区别,至少bundleless简单的性能测试方面没有明显差距。
4.2构建时间对比
bundleless构建用于线上,主要是减少了构建的时间,我们传统的bundle的代码,一次编译打包等可能需要几分钟甚至十几分钟。在我的项目中,bundleless的构建只需要4秒。
同一个项目,用webpack构建bundle的情况下需要60秒左右。
4.3构建产物体积对比
bundleless构建出的产物,一般来说也只有bundle情况下的1/10.这里不一一举例。
五、总结
在没有强兼容性的场景,特别是中后台系统,bundleless的代码直接跑在线上,是一种可以尝试的方案,上线的时间会缩短90%,不过也有一些问题需要解决,首先需要保证托管esm资源的CDN服务的稳定性,且要保障被托管的esm资源在浏览器运行不会出现异常。我们运行了一些常见的npm包,发现并没有异常情况,不过后续需要更多的测试。
六、附录:snowpack和vite的对比
6.1 相同点
snowpack和vite都是bundleless的构建工具,都利用了浏览器的es module来减少对静态文件的打包,从而减少热更新的时间,从而提高开发体验。原理都是将本地安装的依赖重新编译成esm形式,然后放在本地服务的静态目录下。snowpack和vite有很多相似点
在dev环境都将本地的依赖进行二次处理,对于本地node_module目录下的npm包,通过其他构建工具转换成esm。然后将所有转换后的esm文件放在本地服务的静态目录下
都支持css、png等等静态文件,不需要安装其他插件。特别对于css,都默认支持css module
默认都支持jsx,tsx,ts等扩展名的文件
框架无关,都支持react、vue等主流前端框架,不过vite对于vue的支持性是最好的。
6.2 不同点
dev构建: snowpack和vite其实大同小异,在dev环境都可以将本地node_modules中npm包,通过esinstall等编译到本地server的静态目录。不同的是在dev环境
snowpack是通过rollup来将node_modules的包,重新进行esm形式的编译
vite则是通过esbuild来将node_modules的包,重新进行esm形式的编译
因此dev开发环境来看,vite的速度要相对快一些,因为一个npm包只会重新编译一次,因此dev环境速度影响不大,只是在初始化项目冷启动的时候时间有一些误差,此外snowpack支持Streaming Imports,可以在dev环境直接用托管在cdn上的esm形式的npm包,因此dev环境性能差别不大。
build构建:
在生产环境build的时候,vite是不支持unbundle的,在bundle模式下,vite选择采用的是rollup,通过rollup来打包出线上环境运行的静态文件。vite官方支持且仅支持rollup,这样一定程度上可以保持一致性,但是不容易解耦,从而结合非rollup构建工具来打包。而snowpack默认就是unbundle的,这种unbundle的默认形式,对构建工具就没有要求,对于线上环境,即可以使用rollup,也可以使用webpack,甚至可以选择不打包,直接使用unbundle。
可以用两个表格来总结如上的结论:
dev开发环境:
产品 | dev环境构建工具 |
---|---|
snowpack | rollup(或者使用Streaming imports) |
vite | esbuild |
build生产环境:
产品 | build构建工具 |
---|---|
snowpack | 1.unbundle(esbuild) 2.rollup 3.webpack... |
vite | rollup(且不支持unbundle) |
6.3 snowpack支持Streaming Imports
Streaming Imports是一个新特性,他允许用户,不管是生产环境还是开发环境,都不需要在本地使用npm/yarn来维护一个lock文件,从而下载应用中所使用的npm包到本地的node_module目录下。通过使用Streaming Imports,可以维护一个map文件,该map文件中的key是包名,value直接指向托管该npm包esm文件形式的cdn服务器的地址。
6.4 vite的一些优点
vite相对于snowpack有一下几个优点,不过个人以为都不算特别有用的一些优点。
多页面支持,除了根目录的root/index.html外,还支持其他根目录以外的页面,比如nest/index.html
对于css预处理器支持更好(这点个人没发现)
支持css代码的code-splitting
优化了异步请求多个chunk文件(不分场景可以同步进行,从而一定程度下减少请求总时间)
6.5 总结
如果想在生产环境使用unbundle,那么vite是不行的,vite对于线上环境的build,是必须要打包的。vite优化的只是开发环境。而snowpack默认就是unbundle的,因此可以作为前提在生产环境使用unbundle.此外,snowpack的Streaming Imports提供了一套完整的本地map的包管理,不需要将npm包安装到本地,很方便我们在线上和线下使用cdn托管公共库。
作者:yuxiaoliang
来源:https://juejin.cn/post/7034484346874986533
重新审视前端模块的调用, 执行和加载之间的关系
在进入正题之前, 让我们先回顾下前端模块从无到有的一个简短历史
如果你有一定的工作经验, 并且经历过 jQuery 那样的年代, 应该了解早期的前端模块, 只是 window 上的一个局部变量.
在最初的时候前端工程师为了分享自己的代码, 往往会通过 window 来建立联系, 这种古老的做法至今还被很多人使用, 因为简单. 例如我们编写了一个脚本, 通常我们并不认为这是个模块, 但我们会习惯于将这个脚本包装成一个对象. 例如
window.myModule = {
getName(name){
return `hello ${name}`
}
}
当其他人加载这个脚本后, 就可以便捷的通过 window.myModule 来调用 getName 方法.
早期的 JavaScript 脚本主要用于开发一些简单的表单和网页的交互功能, 那个年代的前端工程师数量也极少, 通过 window 来实现模块化并没有什么太大的问题.
直到 ajax 的出现, 将 web 逐步推动到了富客户端的阶段, 随着 spa 的兴起, 前端工程师发现使用 window 模块化代码越来越难以维护, 主要原因有 2 个
- 大量的模块加载污染了 window, 导致各种命名冲突和意外覆盖, 这些问题还很难定位.
- 模块和模块之间的交互越来越多, 为了保证调用顺序, 需要人为保障 script 标签的加载顺序
为了解决这个问题, 类似 require seajs 这样的模块 loader 被创造出来, 通过模块 loader, 大大缓解了上述的两个问题.
但前端技术和互联网发展的速度远超我们的想象, 随着网页越来越像一个真实的客户端, 这对前端的工程能力提出了极大的挑战, 仅靠单纯的脚本开发已经难以满足项目的需要, 于是 gulp 等用于前端工程管理的脚手架开始进入我们的视野, 不过在这个阶段, 模块 loader 和前端工程流之间尚未有机的结合.
直到 nodejs 问世, 前端拥有了自己的包管理工具 npm, 在此基础上 Webpack 进一步推动了前端工程流和模块之间的整合, 随后前端模块化的进程开始稳固下来, 一直保持至今.
从这个历史上去回顾, 前端模块化的整个进程包括 es6 关于 module 的标准都是一直围绕这个一个核心命题存在的.
无论是 require 还是 Webpack 在这个核心命题上并没有区别, 即前端模块遵循
加载 → 调用 → 执行 这样的一个逻辑关系. 因为模块必须先加载才能调用并执行, 模块加载器和构建工具就必须管理和分析应用中所有模块的依赖关系, 从而确定哪些模块可以拆分哪些可以合并, 以及模块的加载顺序.
但是随着时间的推移, 前端应用的模块越来越多, 应用越来越庞大, 我们的本地的 node_modules 几百兆起步, Webpack 虽然做了很多优化, 但是 rebuild 的时间在大型应用面前依然显得很慢.
今年 2 月份, Webpack 5 发布了他们的模块拆解方案, 模块联邦, 这个插件解决了 Webpack 构建的模块无法在多个工程中复用的问题.
早些时间 yarn 2.0 采用共享 node_moudles 的方法来解决本地模块大量冗余导致的性能问题.
包括 nodejs 作者在 deno 中放弃了 npm 改用网络化加载模块的方式等等.
可以看到社区已经意识到了先行的前端模块化机制再次面临瓶颈, 无论是性能还是维护成本都面临诸多挑战, 各个团队都在想办法开辟一个新的方向.
不过这些努力依然没有超越先行模块化机制中的核心命题, 即模块必须先加载, 后调用执行.
只要这个核心命题不变, 模块的依赖问题依然是无解的. 为此我们尝试提出了一种新的思路
模块为什么不能先调用, 后加载执行呢?
如果 A 模块调用 B 模块, 但并不需要 B 模块立即就绪, 这就意味着, 模块加载器可以不关心模块的依赖关系, 而致力于只解决模块加载的效率和性能问题.
同时对于构建工具来说, 如果 A 模块的执行并不基于 B 模块立即就绪这件事, 那么构建工具可以放心的将 A 和 B 模块拆成两个文件, 如果模块有很多, 就可以利用 http2 的并行加载能力, 大大提升模块的加载性能.
在我们的设想中, 一种新的模块加载方式是这样的
// remoteModule.js 这是一个发布到 cdn 的远程模块, 内部代码是这样
widnow.rdeco.create({
name:'remote-module',
exports:{
getName(name, next){
next(`hello ${name}`)
}
}
})
让我们先不加载这个模块, 而是直接先执行调用端的代码例如这样
window.rdeco 可以理解成类似 Webpack runtime 一样的存在, 不过 rdeco 是一个独立的库, 其功能远不止于此
// localModule.js 这个是本地的模块
window.rdeco.inject('remote-module').getName('world').then(fullName=>{
console.log(fullName)
})
然后我们在 html 中先加载 localModule.js 后加载 remoteModule.js
<scirpt src="localModule.js"></script>
<scirpt src="remoteModule.js"></script>
正常理解, localModule.js 加载完之后会试图去调用 remote-module 的 getName 方法, 但此时 remoteModule 尚未加载, 按照先行的模块化机制, 这种调用会抛出异常. 为了避免这个问题
模块构建工具需要分析两个文件的代码, 从而发现 localModule.js 依赖 remoteModule.js, 然后保存这个依赖顺序, 同时通知模块加载器, 为了让代码正常执行, 必须先加载 remoteModule.js.
但如果模块可以先调用后加载, 那么这个复杂的过程就可以完全避免. 目前我们实现了这一机制, 可以看下这个 demo: codesandbox.io/s/tender-ar…
你可试着先点击 Call remote module's getName method
按钮,
此时文案不会变化只是显示 hello, 但代码并不会抛出异常, 然后你再点击 Load remote module
按钮, 开始加载 remoteModule, 等待加载完成, getName 才会真实执行, 此时文案变成了 hello world
作者:掘金泥石流
链接:https://juejin.cn/post/7034412398261993479
收起阅读 »
CSS实现随机不规则圆角头像
前言
最近真是彻底爱上了 CSS
,我又又又被 CSS
惊艳到了,明明是简单的属性,为啥大佬们稍微一组合,就能形成如此好看的效果啊。本文
给大家带来的是随机不规则圆角头像效果,我们可以把这个效果用于一些人物的展示页面
学习本文章,你可以学到:
border-radius
实现椭圆效果border-radius
实现不规则圆角头像animation-delay
设置负值- 实现随机不规则圆角
📃 预备知识
🎨 border-radius
border-radius
可以设置外边框的圆角。比如我们经常使用的 border-radius: 50%
可以得到一个圆形头像。
但 border-radius
就只能实现圆形效果吗?当然不是,当使用一个半径是确定圆形,两个半径时则会确定椭圆形。
光说不练假把式,接下来一起试试
- 设置
border-radius: 30% 70%
,就可以得到椭圆效果
上面的设置都是针对于四个方向的,也可以只设置一个方向的圆角
- 设置 border-top-left-radius:
30% 70%
从上图其实可以得出,两个值分别设置水平半径和垂直半径的半径,为了更准确我们验证一下
但为啥设置的圆角与 border-radius: 30% 70%
设置有这么大的差距。别急,下面慢慢道来。
- 设置
border-radius: 30%/70%
,/ 前后的值分别为水平半径和垂直半径
border-radius: 30%/70%
相当于给四个方向都设置 30%/70%,而border-radius: 30% 70%
是给左上右下设置30%
,左下右上设置70%
- 设置四个方向为四种椭圆角:
border-radius: 40% 60% 60% 40% / 60% 30% 70% 40%
,就可以实现简单的不规则圆角效果,小改改的头像是不是看起来舒服了好多。
💞 animation-delay
animation-delay
: 可以定义动画播放的延迟时间。
但如果给 animation-delay
设置负值会发生什么那?
MDN 中指出: 定义一个负值会让动画立即开始。但是动画会从它的动画序列中某位置开始。例如,如果设定值为 -1s ,动画会从它的动画序列的第 1 秒位置处立即开始。
那个,乍看上去,我好像懂了,又好像没懂,咱们还是来自己试一下吧。
- 创建
div
块,宽高都为0
,背景设置为#000
- 添加
keyframe
动画,100%
状态宽高都扩展为1000px
@keyframes extend {
0% {
width: 0;
height: 0;
}
100% {
width: 1000px;
height: 1000px;
}
}
- 给
div
添加animation
和animation-delay
/* 设置 paused 可以使动画暂停 */
animation: extend 10s linear paused;
animation-delay: -3s;
当我打开浏览器时,浏览器出现 300*300
的黑色块,修改 animation-delay
为 -4s
,浏览器出现 400*400
的黑块。我们使用 linear
匀速作为动画播放函数,10s
后 div 会变为 1000px
,设置 -3s
起始为 300px
,-4s
起始为 400px
。
这样一对比,我们来把 MDN
的描述翻译一下:
+ animation-delay
设置负值的动画会立即执行
+ 动画起始位置是动画中的一阶段,比如上述案例,定义 10s
的动画,设置 -3s
动画就从 3s
开始执行
🌊 radius 配合 delay 实现
有了上面基础知识的配合,不规则圆角的实现就变得很简单了。
设置 keyframe
,keyframe
的开始与结束为两种不规则圆角,再使用 :nth-child
进行自然随机设置 animation-delay
的负值延迟时间,就可以得到一组风格各异的不规则圆角效果
自然随机的算法非常有意思,效果开创者为了更好、更自然的随机性,选取序列为 2n+1 3n+2 5n+3 7n+4 11+5 ...
- 设置
keyframe
动画
@keyframes morph {
0% {
border-radius: 40% 60% 60% 40% / 60% 30% 70% 40%;
transform: rotate(-5deg);
}
100% {
border-radius: 40% 60%;
transform: rotate(5deg);
}
}
- 自然随机设置每个头像的
delay
.avatar:nth-child(n) {
animation-delay: -3.5s;
}
.avatar:nth-child(2n + 1) {
animation-delay: -1s;
}
.avatar:nth-child(3n + 2) {
animation-delay: -2s;
}
.avatar:nth-child(5n + 3) {
animation-delay: -3s;
}
.avatar:nth-child(7n + 5) {
animation-delay: -4s;
}
.avatar:nth-child(11n + 7) {
animation-delay: -5s;
}
当当当当~~~ 效果就实现了! 看着下面这些风格各异的小改改,瞬间心情舒畅了好多。
不规则圆角头像的功能实现了,但总感觉缺点什么?如果头像能有点动态效果就更好了。
例如 hover
时,头像圆角会发生变化,用户的体验会更好。
我首先的想法还是在上面的代码基础上面更改,但由于 @keyframe
定义好了终点时的状态,能变化的效果并不多,而且看起来很单调,显得很呆 🤣。
那有没有好的实现方案那?有,最终我找到了张鑫旭大佬的实现方案,大佬还是大佬啊。
🌟 radius 配合 transition 实现
参考博客: “蝉原则”与CSS3随机多背景随机圆角等效果
- 按照自然随机给每个头像赋予不同的不规则圆角
/* 举两个例子 */
.list:hover {
border-radius: 95% 70% 100% 80%;
transform: rotate(-2deg);
}
.list:nth-child(2n+1) {
border-radius: 59% 52% 56% 59%;
transform: rotate(-6deg);
}
- 设置
hover
时新的不规则圆角
.list:nth-child(2n+1):hover {
border-radius: 51% 67% 56% 64%;
transform: rotate(-4deg);
}
.list:nth-child(3n+2):hover {
border-radius: 69% 64% 53% 70%;
transform: rotate(0deg);
}
- 给
list
元素配置transition
完成上面的步骤,我们就可以得到更灵动的小改改头像了。
但这种实现方法相比较于
radius
配合animation-delay
实现具备一定的难点,需要设计多种好看的不规则圆角效果
🛕 源码仓库
传送门: 随机不规则圆角
作者:战场小包
链接:https://juejin.cn/post/7034396555738251301
收起阅读 »
使用 Promise 时的5个常见错误,你占了几个!
Promise 提供了一种优雅的方法来处理 JS 中的异步操作。这也是避免“回调地狱”的解决方案。然而,并没有多少开发人员了解其中的内容。因此,许多人在实践中往往会犯错误。
在本文中,介绍一下使用 promise 时的五个常见错误,希望大家能够避免这些错误。
1.避免 Promise 地狱
通常,Promise是用来避免回调地狱。但滥用它们也会导致 Promise是地狱。
userLogin('user').then(function(user){
getArticle(user).then(function(articles){
showArticle(articles).then(function(){
//Your code goes here...
});
});
});
在上面的例子中,我们对 userLogin
、getararticle
和 showararticle
嵌套了三个promise。这样复杂性将按代码行比例增长,它可能变得不可读。
为了避免这种情况,我们需要解除代码的嵌套,从第一个 then
中返回 getArticle
,然后在第二个 then
中处理它。
userLogin('user')
.then(getArticle)
.then(showArticle)
.then(function(){
//Your code goes here...
});
2. 在 Promise 中使用 try/catch
块
通常情况下,我们使用 try/catch
块来处理错误。然而,不建议在 Promise
对象中使用try/catch
。
这是因为如果有任何错误,Promise对象会在 catch
内自动处理。
ew Promise((resolve, reject) => {
try {
const data = doThis();
// do something
resolve();
} catch (e) {
reject(e);
}
})
.then(data => console.log(data))
.catch(error => console.log(error));
在上面的例子中,我们在Promise 内使用了 try/catch
块。
但是,Promise本身会在其作用域内捕捉所有的错误(甚至是打字错误),而不需要 try/catch
块。它确保在执行过程中抛出的所有异常都被获取并转换为被拒绝的 Promise。
new Promise((resolve, reject) => {
const data = doThis();
// do something
resolve()
})
.then(data => console.log(data))
.catch(error => console.log(error));
**注意:**在 Promise 块中使用 .catch()
块是至关重要的。否则,你的测试案例可能会失败,而且应用程序在生产阶段可能会崩溃。
3. 在 Promise 块内使用异步函数
Async/Await
是一种更高级的语法,用于处理同步代码中的多个Promise。当我们在一个函数声明前使用 async
关键字时,它会返回一个 Promise,我们可以使用 await
关键字来停止代码,直到我们正在等待的Promise解决或拒绝。
但是,当你把一个 Async 函数放在一个 Promise 块里面时,会有一些副作用。
假设我们想在Promise 块中做一个异步操作,所以使用了 async
关键字,但,不巧的是我们的代码抛出了一个错误。
这样,即使使用 catch()
块或在 try/catch
块内等待你的Promise,我们也不能立即处理这个错误。请看下面的例子。
// 此代码无法处理错误
new Promise(async () => {
throw new Error('message');
}).catch(e => console.log(e.message));
(async () => {
try {
await new Promise(async () => {
throw new Error('message');
});
} catch (e) {
console.log(e.message);
}
})();
当我在Promise块内遇到 async
函数时,我试图将 async
逻辑保持在 Promise 块之外,以保持其同步性。10次中有9次都能成功。
然而,在某些情况下,可能需要一个 async
函数。在这种情况下,也别无选择,只能用try/catch
块来手动管理。
new Promise(async (resolve, reject) => {
try {
throw new Error('message');
} catch (error) {
reject(error);
}
}).catch(e => console.log(e.message));
//using async/await
(async () => {
try {
await new Promise(async (resolve, reject) => {
try {
throw new Error('message');
} catch (error) {
reject(error);
}
});
} catch (e) {
console.log(e.message);
}
})();
4.在创建 Promise 后立即执行 Promise 块
至于下面的代码片断,如果我们把代码片断放在调用HTTP请求的地方,它就会被立即执行。
const myPromise = new Promise(resolve => {
// code to make HTTP request
resolve(result);
});
原因是这段代码被包裹在一个Promise构造函数中。然而,有些人可能会认为只有在执行myPromise
的then
方法之后才被触发。
然而,真相并非如此。相反,当一个Promise被创建时,回调被立即执行。
这意味着在建立 myPromise
之后到达下面一行时,HTTP请求很可能已经在运行,或者至少处于调度状态。
Promises 总是急于执行过程。
但是,如果希望以后再执行 Promises,应该怎么做?如果现在不想发出HTTP请求怎么办?是否有什么神奇的机制内置于 Promises 中,使我们能够做到这一点?
答案就是使用函数。函数是一种耗时的机制。只有当开发者明确地用 ()
来调用它们时,它们才会执行。简单地定义一个函数还不能让我们得到什么。所以,让 Promise 变得懒惰的最有效方法是将其包裹在一个函数中!
const createMyPromise = () => new Promise(resolve => {
// HTTP request
resolve(result);
});
对于HTTP请求,Promise 构造函数和回调函数只有在函数被执行时才会被调用。所以现在我们有一个懒惰的Promise,只有在我们需要的时候才会执行。
5. 不一定使用 Promise.all() 方法
如果你已经工作多年,应该已经知道我在说什么了。如果有许多彼此不相关的 Promise,我们可以同时处理它们。
Promise 是并发的,但如你一个一个地等待它们,会太费时间,Promise.all()
可以节省很多时间。
记住,Promise.all() 是我们的朋友
const { promisify } = require('util');
const sleep = promisify(setTimeout);
async function f1() {
await sleep(1000);
}
async function f2() {
await sleep(2000);
}
async function f3() {
await sleep(3000);
}
(async () => {
console.time('sequential');
await f1();
await f2();
await f3();
console.timeEnd('sequential');
})();
上述代码的执行时间约为 6
秒。但如果我们用 Promise.all()
代替它,将减少执行时间。
(async () => {
console.time('concurrent');
await Promise.all([f1(), f2(), f3()]);
console.timeEnd('concurrent');
})();
总结
在这篇文章中,我们讨论了使用 Promise 时常犯的五个错误。然而,可能还有很多简单的问题需要仔细解决。
作者:前端小智
链接:https://juejin.cn/post/7034661345148534815
收起阅读 »
没想到吧!这个可可爱爱的游戏居然是用 ECharts 实现的!
前言
echarts
是一个很强大的图表库,除了我们常见的图表功能,echarts
有一个自定义图形的功能,这个功能可以让我们很简单地在画布上绘制一些非常规的图形,基于此,我们来玩一些花哨的。
下面我们来一步步实现他。
1 在坐标系中画一只会动的小鸟
首先实例化一个echart容器,再从网上找一个像素小鸟的图片,将散点图的散点形状,用自定义图片的方式改为小鸟。
const myChart = echarts.init(document.getElementById('main'));
option = {
series: [
{
name: 'bird',
type: 'scatter',
symbolSize: 50,
symbol: 'image://bird.png',
data: [
[50, 80]
],
animation: false
},
]
};
myChart.setOption(option);
要让小鸟动起来,就需要给一个向右的速度和向下的加速度,并在每一帧的场景中刷新小鸟的位置。而小鸟向上飞的动作,则可以靠角度的旋转来实现,向上飞的触发条件设置为空格事件。
option = {
series: [
{
xAxis: {
show: false,
type: 'value',
min: 0,
max: 200,
},
yAxis: {
show: false,
min: 0,
max: 100
},
name: 'bird',
type: 'scatter',
symbolSize: 50,
symbol: 'image://bird.png',
data: [
[50, 80]
],
animation: false
},
]
};
// 设置速度和加速度
let a = 0.05;
let vh = 0;
let vw = 0.5
timer = setInterval(() => {
// 小鸟位置和仰角调整
vh = vh - a;
option.series[0].data[0][1] += vh;
option.series[0].data[0][0] += vw;
option.series[0].symbolRotate = option.series[0].symbolRotate ? option.series[0].symbolRotate - 5 : 0;
// 坐标系范围调整
option.xAxis.min += vw;
option.xAxis.max += vw;
myChart.setOption(option);
}, 25);
效果如下
2 用自定义图形绘制障碍物
echarts自定义系列,渲染逻辑由开发者通过renderItem
函数实现。该函数接收两个参数params和api,params包含了当前数据信息和坐标系的信息,api是一些开发者可调用的方法集合,常用的方法有:
api.value(...),意思是取出
dataItem
中的数值。例如api.value(0)
表示取出当前dataItem
中第一个维度的数值。
api.coord(...),意思是进行坐标转换计算。例如
var point = api.coord([api.value(0), api.value(1)])
表示dataItem
中的数值转换成坐标系上的点。
api.size(...), 可以得到坐标系上一段数值范围对应的长度。
api.style(...),可以获取到
series.itemStyle
中定义的样式信息。
灵活使用上述api,就可以将用户传入的Data数据转换为自己想要的坐标系上的像素位置。
renderItem
函数返回一个echarts
中的graphic
类,可以多种图形组合成你需要的形状,graphic类型。对于我们游戏中的障碍物只需要使用矩形即可绘制出来,我们使用到下面两个类。
type: group, 组合类,可以将多个图形类组合成一个图形,子类放在children中。
type: rect, 矩形类,通过定义矩形左上角坐标点,和矩形宽高确定图形。
// 数据项定义为[x坐标,下方水管上侧y坐标, 上方水管下侧y坐标]
data: [
[150, 50, 80],
...
]
renderItem: function (params, api) {
// 获取每个水管主体矩形的起始坐标点
let start1 = api.coord([api.value(0) - 10, api.value(1)]);
let start2 = api.coord([api.value(0) - 10, 100]);
// 获取两个水管头矩形的起始坐标点
let startHead1 = api.coord([api.value(0) - 12, api.value(1)]);
let startHead2 = api.coord([api.value(0) - 12, api.value(2) + 8])
// 水管头矩形的宽高
let headSize = api.size([24, 8])
// 水管头矩形的宽高
let rect = api.size([20, api.value(1)]);
let rect2 = api.size([20, 100 - api.value(2)]);
// 坐标系配置
const common = {
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height
}
// 水管形状
const rectShape = echarts.graphic.clipRectByRect(
{
x: start1[0],
y: start1[1],
width: rect[0],
height: rect[1]
},common
);
const rectShape2 = echarts.graphic.clipRectByRect(
{
x: start2[0],
y: start2[1],
width: rect2[0],
height: rect2[1]
},
common
)
// 水管头形状
const rectHeadShape = echarts.graphic.clipRectByRect(
{
x: startHead1[0],
y: startHead1[1],
width: headSize[0],
height: headSize[1]
},common
);
const rectHeadShape2 = echarts.graphic.clipRectByRect(
{
x: startHead2[0],
y: startHead2[1],
width: headSize[0],
height: headSize[1]
},common
);
// 返回一个group类,由四个矩形组成
return {
type: 'group',
children: [{
type: 'rect',
shape: rectShape,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
}, {
type: 'rect',
shape: rectShape2,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
},
{
type: 'rect',
shape: rectHeadShape,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
},
{
type: 'rect',
shape: rectHeadShape2,
style: {
...api.style(),
lineWidth: 1,
stroke: '#000'
}
}]
};
},
颜色定义, 我们为了让水管具有光泽使用了echarts
的线性渐变色对象。
itemStyle: {
// 渐变色对象
color: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [{
offset: 0, color: '#ddf38c' // 0% 处的颜色
}, {
offset: 1, color: '#587d2a' // 100% 处的颜色
}],
global: false // 缺省为 false
},
borderWidth: 3
},
另外,用一个for循环一次性随机出多个柱子的数据
function initObstacleData() {
// 添加minHeight防止空隙太小
let minHeight = 20;
let start = 150;
obstacleData = [];
for (let index = 0; index < 50; index++) {
const height = Math.random() * 30 + minHeight;
const obstacleStart = Math.random() * (90 - minHeight);
obstacleData.push(
[
start + 50 * index,
obstacleStart,
obstacleStart + height > 100 ? 100 : obstacleStart + height
]
)
}
}
再将背景用游戏图片填充,我们就将整个游戏场景,绘制完成:
3 进行碰撞检测
由于飞行轨迹和障碍物数据都很简单,所以我们可以将碰撞逻辑简化为小鸟图片的正方形中,我们判断右上和右下角是否进入了自定义图形的范围内。
对于特定坐标下的碰撞范围,因为柱子固定每格50坐标值一个,宽度也是固定的,所以,可碰撞的横坐标范围就可以简化为 (x / 50 % 1) < 0.6
在特定范围内,依据Math.floor(x / 50)
获取到对应的数据,即可判断出两个边角坐标是否和柱子区域有重叠了。在动画帧中判断,如果重叠了,就停止动画播放,游戏结束。
// centerCoord为散点坐标点
function judgeCollision(centerCoord) {
if (centerCoord[1] < 0 || centerCoord[1] > 100) {
return false;
}
let coordList = [
[centerCoord[0] + 15, centerCoord[1] + 1],
[centerCoord[0] + 15, centerCoord[1] - 1],
]
for (let i = 0; i < 2; i++) {
const coord = coordList[i];
const index = coord[0] / 50;
if (index % 1 < 0.6 && obstacleData[Math.floor(index) - 3]) {
if (obstacleData[Math.floor(index) - 3][1] > coord[1] || obstacleData[Math.floor(index) - 3][2] < coord[1]) {
return false;
}
}
}
return false
}
function initAnimation() {
// 动画设置
timer = setInterval(() => {
// 小鸟速度和仰角调整
vh = vh - a;
option.series[0].data[0][1] += vh;
option.series[0].data[0][0] += vw;
option.series[0].symbolRotate = option.series[0].symbolRotate ? option.series[0].symbolRotate - 5 : 0;
// 坐标系范围调整
option.xAxis.min += vw;
option.xAxis.max += vw;
// 碰撞判断
const result = judgeCollision(option.series[0].data[0])
if(result) { // 产生碰撞后结束动画
endAnimation();
}
myChart.setOption(option);
}, 25);
}
总结
echarts
提供了强大的图形绘制自定义能力,要使用好这种能力,一定要理解好数据坐标点和像素坐标点之间的转换逻辑,这是将数据具象到画布上的重要一步。
运用好这个功能,再也不怕产品提出奇奇怪怪的图表需求。
作者:DevUI团队
链接:https://juejin.cn/post/7034290086111871007
收起阅读 »
学会了axios封装,世界都是你的
项目中对axios进行二次封装
随着前端技术的发展,网络请求这一块,越来越多的程序猿选择使用axios来实现网络请求。但是单纯的axios插件并不能满足我们日常的使用,因此我们使用时,需要根据项目实际的情况来对axios进行二次封装。
接下来就我对axios的二次封装详细的说说,主要包括请求之前、返回响应以及使用等。
「1、请求之前」
一般的接口都会有鉴权认证(token)之类的,因此在接口的请求头里面,我们需要带上token值以通过服务器的鉴权认证。但是如果每次请求的时候再去添加,不仅会大大的加大工作量,而且很容易出错。好在axios提供了拦截器机制,我们在请求的拦截器中可以添加token。
// 请求拦截
axios.interceptors.request.use((config) => {
//....省略代码
config.headers.x_access_token = token
return config
}, function (error) {
return Promise.reject(error)
})
当然请求拦截器中,除了处理添加token以外,还可以进行一些其他的处理,具体的根据实际需求进行处理。
「2、响应之后」
请求接口,并不是每一次请求都会成功。那么当接口请求失败的时候,我们又怎么处理呢?每次请求的时候处理?封装axios统一处理?我想一个稍微追求代码质量的码农,应该都会选择封装axios进行统一处理吧。axios不仅提供了请求的拦截器,其也提供了响应的拦截器。在此处,可以获取到服务器返回的状态码,然后根据状态码进行相对应的操作。
// 响应拦截
axios.interceptors.response.use(function (response) {
if (response.data.code === 401 ) {//用户token失效
//清空用户信息
sessionStorage.user = ''
sessionStorage.token = ''
window.location.href = '/';//返回登录页
return Promise.reject(msg)//接口Promise返回错误状态,错误信息msg可有后端返回,也可以我们自己定义一个码--信息的关系。
}
if(response.status!==200||response.data.code!==200){//接口请求失败,具体根据实际情况判断
message.error(msg);//提示错误信息
return Promise.reject(msg)//接口Promise返回错误状态
}
return response
}, function (error) {
if (axios.isCancel(error)) {
requestList.length = 0
// store.dispatch('changeGlobalState', {loading: false})
throw new axios.Cancel('cancel request')
} else {
message.error('网络请求失败,请重试')
}
return Promise.reject(error)
})
当然响应拦截器同请求拦截器一样,还可以进行一些其他的处理,具体的根据实际需求进行处理。
「3、使用axios」
axios使用的时候一般有三种方式:
执行get请求
axios.get('url',{
params:{},//接口参数
}).then(function(res){
console.log(res);//处理成功的函数 相当于success
}).catch(function(error){
console.log(error)//错误处理 相当于error
})
执行post请求
axios.post('url',{
data:xxx//参数
},{
headers:xxxx,//请求头信息
}).then(function(res){
console.log(res);//处理成功的函数 相当于success
}).catch(function(error){
console.log(error)//错误处理 相当于error
})
axios API 通过相关配置传递给axios完成请求
axios({
method:'delete',
url:'xxx',
cache:false,
params:{id:123},
headers:xxx,
})
//------------------------------------------//
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'monkey',
lastName: 'soft'
}
});
直接使用api的方式虽然简单,但是不同请求参数的名字不一样,在实际开发过程中很容易写错或者忽略,容易为开发造成不必要的时间损失。
前面两种方式虽然没有参数不一致的问题,但是使用的时候,太过于麻烦。那么怎么办呢?
前面两种虽然使用过于麻烦,但是仔细观察,是可以发现有一定的相似点,我们便可以基于这些相似点二次封装,形成适合我们使用的一个请求函数。直接上代码:
/*
*url:请求的url
*params:请求的参数
*config:请求时的header信息
*method:请求方法
*/
const request = function ({ url, params, config, method }) {
// 如果是get请求 需要拼接参数
let str = ''
if (method === 'get' && params) {
Object.keys(params).forEach(item => {
str += `${item}=${params[item]}&`
})
}
return new Promise((resolve, reject) => {
axios[method](str ? (url + '?' + str.substring(0, str.length - 1)) : url, params, Object.assign({}, config)).then(response => {
resolve(response.data)
}, err => {
if (err.Cancel) {
} else {
reject(err)
}
}).catch(err => {
reject(err)
})
})
}
这样我们需要接口请求的时候,直接调用该函数就好了。不管什么方式请求,传参方式都一样。
作者:monkeysoft
来源:https://juejin.cn/post/6847009771606769677
大话WEB前端性能优化基本套路
前言
前端性能优化这是一个老生常谈的话题,但是还是有很多人没有真正的重视起来,或者说还没有产生这种意识。
当用户打开页面,首屏加载速度越慢,流失用户的概率就越大,在体验产品的时候性能和交互对用户的影响是最直接的,推广拉新是一门艺术,用户的留存是一门技术,拉进来留住用户,产品体验很关键,这里我以 美柚的页面为例子,用实例展开说明前端优化的基本套路(适合新手上车)。
WEB性能优化套路
基础套路1:减少资源体积
css
压缩
响应头GZIP
js
压缩
响应头GZIP
html
输出压缩
响应头GZIP
hhh
图片
压缩
使用Webp格式
cookie
注意cookie体积,合理设置过期时间
基础套路2:控制请求数
js
合并
css
合并
图片
合并
事实上
base64(常用图标:如logo等)
hhh
接口
数量控制
异步ajax
合理使用缓存机制
浏览器缓存
js编码
Require.JS 按需加载
异步加载js
lazyload图片
基础套路3:静态资源CDN
请求走CDN
html
p_w_picpath
js
css
综合套路
图片地址独立域名
与业务不同域名可以减少请求头里不必要的cookie传输
提高渲染速度
js放到页面底部,body标签底部
css放到页面顶部,head标签里
代码
代码优化:css/js/html
预加载,如:分页预加载,快滚动到底部的时候以前加载下一页数据
拓展资料
性能辅助工具
谷歌 PageSpeed Insights(网页载入速度检测工具,需要×××)
看完上面的套路介绍
可能有人会说:我在前端界混了这么多年,这些我都知道,只不过我不想去做
我答: 知道做不到,等于不知道
也可能有人会说:压缩合并等这些操作好繁琐,因为懒,所以不做
我答: 现在前端构建工具都很强大,如:grunt、gulp、webpack,支持各种插件操作,还不知道就说明你OUT了
因为我主要负责后端相关工作,前端并不是我擅长的,但是平时也喜欢关注前端前沿技术,这里以我的视角和开发经验梳理出基本套路。
套路点到为止,具体实施可以通过拓展资料进行深入了解,如有疑义或者补充请留言怼。
感谢你的支持,我会继续努力!~
作者: SFLYQ
来源:https://blog.51cto.com/sflyq/1947541
收起阅读 »WEB加载动画之彩条起伏动画
介绍
本期将带给大家一个简单的创意加载效果——彩条起伏加载。顾名思义,我们会通过scss来完成,将会制作做7个不同颜色的矩形,按不同的延迟不断的递减然后再反弹,循环往复。寓意是希望各位同学像这个加载动画一样,生活过的多姿多彩。
接下来,我们先来一睹为快吧:
感觉如何,其实这个动画的实现方案有很多,今天就用障眼法去实现它,希望给你打开书写css的新思路。
正文
1.彩条绘制
<div id="app">
<div class="loading">
<span>l</span>
<span>o</span>
<span>a</span>
<span>d</span>
<span>i</span>
<span>n</span>
<span>g</span>
</div>
</div>
结构非常的简单,我们将会在div#app让div.loading居中显示,然后在loading中平分各个距离,渲染不同的颜色。
@import url("https://fonts.googleapis.com/css?family=Baloo+Bhaijaan&display=swap");
#app{
width: 100%;
height: 100vh;
background-color: #fff;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.loading{
width: 350px;
height: 120px;
display: flex;
overflow: hidden;
span{
flex:1;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-family: cursive;
font-weight: bold;
text-transform: uppercase;
font-family: "Baloo Bhaijaan", cursive;
color: white;
font-size: 48px;
position: relative;
box-sizing: border-box;
padding-top: 50px;
@for $i from 1 through 7 {
&:nth-child(#{$i}) {
background: linear-gradient(180deg, hsl($i * 20 , 60%, 50%) 0, hsl($i * 20 , 60%, 90%) 100%);
box-shadow: inset 0 15px 30px hsl($i * 20 , 60%, 50%);
text-shadow: 12px 12px 12px hsl($i * 20 , 60%, 30%);
border-left: 1px solid hsl($i * 20 , 60%, 80%);;
border-right: 1px solid hsl($i * 20 , 60%, 60%);;
}
}
}
}
为了,美观我们还引入了谷歌的一个字体,居中显示是在div#app用了弹性布局。
#app{
display: flex;
justify-content: center;
align-items: center;
}
这三句话目的就是完成元素在上下左右居中。
另外,我们用scss的一大好处就是体现了出来,遍历十分的方便,即**@for
i就可以拿到下标,还可以参与运算,我们的颜色值就是通过他配合hsl色盘(HSL即色相、饱和度、亮度)去完成的。当然,色盘有360度,我们只取一部分形成清新的渐变,如果整个色盘都平分的话这几个色值出入还是太大会感觉很脏。我们发现文字被设置了padding-top: 50px,原因就是一会要完成起伏的动画,上面的部分最先消失,我们为了保证这些字母能显示时间更长一些,就往下移了一些距离。
2.起伏动画
一开始我们说过这个要用障眼法去实现,所以我们这里不改变span的高度或者裁切他。
.loading{
span{
//...
&::after{
content: "";
display: block;
box-sizing: border-box;
position: absolute;
height: 100%;
top: 0;
left: -1px;
right: -1px;
background: linear-gradient(180deg, white 0, rgb(249, 249, 249) 100%);
animation: shorten 2.1s infinite ease-out;
}
@for $i from 1 through 7 {
// ...
&::after{
animation-delay: #{ $i * 0.08s};
}
}
}
}
}
@keyframes shorten {
12% { height: 10px; }
}
看了刚才的scss代码可以发现,我们其实是通过一个绝对定位的伪类去遮挡了他,做了一个障眼法让人感觉他高度改变了,其实不然。
至于动画,就更容易了就只有一句,就是在初期某个阶段让他变化高度到10px,也就是遮挡块变小了,显示的高度就就多了,然后缓缓增大至整块,来完成起伏效果。另外,我依然通过遍历在其伪类中,给他们不同的延迟显得更有层次感。
讲到这里,我们的这个案例就书写完成了
结语
本次通过一个做加载创意动画的案例,向各位同学讲到了css如何弹性居中,scss的遍历,hsl色盘改变色值的方便之处以及障眼法的一种方式,希望大家会喜欢,多多支持哦~
作者:jsmask
链接:https://juejin.cn/post/7034304330878418980
收起阅读 »
学会这招,轻松优化webpack构建性能
webpack
webpack
本质上是一个静态资源打包工具,静态资源打包是指 webpack
会将文件及其通过 import
、require
等方式引入的各项资源,处理成一个资源依赖关系图,也称为 chunk
,这些资源包括 js
,css
,jpg
, 等等。
然后将这个 chunk
内的资源分别进行处理 ,如 less
编译成 css
,es6
编译成 es5
,等等。这个处理过程就是打包,最终将这些处理后的文件输出,输出的文件集合便称为 bundle
。
bundle 分析
学会优化 webpack
构建性能等优化,我们需要先学会如何分析 bundle
,通过对产出的分析,才能有针对性的对过程进行优化。
webpack
官方提供了一个非常好用的 bundle
可视化分析工具:webpack-bundle-analyzer
。这个工具会将 bundle
处理一个可视化页面,呈现出资源的依赖关系和体积大小等信息。
这个工具的使用方式也很简单,这需要在通过 npm install webpack-bundle-analyzer
或 yarn install webpack-bundle-analyzer
安装这个插件,然后在 webpack
配置文件的 plugins
配置项中加上这一行代码:
plugins: [
new BundleAnalyzerPlugin(),
]
复制代码
运行 webpack
打包后,会自动在 http://127.0.0.1:8888/
打开一个可视化页面:
优化小妙招
接下来我们将会结合对 bundle
的分析,进行一些优化操作。
在讲解如何优化之前,我们需要明确 chunk
和 bundle
的关系:chunk
是一组依赖关系的集合,它不单单指一个文件,可以包含一个或多个文件。而 bundle
是 webpack
打包的输出结果,它可以包含一个或多个 chunk
。而 webpack
打包执行时会以一个个 chunk
进行处理,前端在加载 webpack
打包的资源时,也往往是以一个 chunk
为单位加载的(无论它是一个或多个文件)。
splitChunks
从可视化界面中我们可以看到,经过 webpack
打包后我们得到一个 app.bundle.js
,这是个 bundle
中包含了我们项目的所有代码以及从 node_modules
中引入的依赖,而这个 bundle
中包含了项目内的所以依赖关系,因此这个 bundle
也是我们项目中唯一一个 chunk
。
那么我们在加载页面时,便是加载这一整个 chunk
,即需要在页面初始时加载全部的代码。
而 splitChunks
,是由 webpack
提供的插件,通过它能够允许我们自由的配置 chunk
的生成策略,当然也包括允许我们将一个巨大的 chunk
拆分成多个 chunk
。
在使用 splitChunks
之前我们先介绍一个重要的配置属性 cacheGroups
(如果需要,可以在官方文档 splitChunks 中了解更多):
cacheGroups
配置提取 chunk
的方案。里面每一项代表一个提取 chunk
的方案。下面是 cacheGroups
每项中特有的选项:
test
选项:用来匹配要提取的 chunk
的资源路径或名称,值是正则或函数。name
选项:生成的 chunk
名称。chunks
选项,决定要提取那些内容。priority
选项:方案的优先级,值越大表示提取 chunk
时优先采用此方案,默认值为0。enforce
选项:true
/false
。为true
时,chunk
的大小和数量限制。接下来我们便通过实际的配置操作,将 node_modules 的内容提取成单独的 chunk
,下面是我们的配置代码:
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
enforce: true,
priority: -3,
},
},
},
},
配置完成后,重新运行 webpack
,可以看到,node_modules
相关的依赖关系被提取成一个单独的 chunk
vendors.bundle.js
,最终我们得到了两个 chunk
:vendors.bundle.js
,app.bundle.js
那么通过这样的chunk
提取,有什么好处呢?
node_modules
下往往是在项目中不会的变化的第三方依赖,我们将这些固定不变的提取成单独的chunk
处理,webpack
便可以将这个chunk
进行一定的缓存策略,而不需要每次都做过多的处理,减少了性能消耗。- 网页加载资源时不需要一次性加载太多的资源,可以通过不同
chunk
分批次加载,从而减少首屏加载的时间。
除了这里介绍的对 node_modules
的处理外,在实际的项目中也可以根据需要对更多的资源采取这样的提取chunk
策略。
、
externals + CDN
通过对 bundle
的分析,我们不难发现:在我们输出的 bundle
中 react.development.js
、 react-dom.development.js
以及 react-router.js
这三个文件特别的显眼。这表示这几个文件的体积在我们总的输出文件中占的比例特别大,那么有什么方法可以解决这些碍眼的家伙呢?
当然有! 下面将要介绍的 external
+ CDN
策略,便可以很好的帮助我们做到这点。
external
是 webpack
的一个重要的配置项,顾名思义,它可以帮助我们将某些资源在 webpack
打包时将其剔除,不参与到资源的打包中。
external
是一个有多项 key-value
组成的对象,它的的每一项属性表示不需要经过 webpack
打包的资源,key
表示的是我们需要排除在外的依赖名称,value
则告诉 webpack
,需要从 window
对象的哪个属性获取到这些被排除在外的依赖。
下面的代码就是将react
、react-dom
、react-router-dom
三个依赖不进行 webpack
打包的配置,它告诉 webpack
,不将 react
、react-dom
和react-router-dom
打包进最终的输出中,需要用到这些依赖时从 window
对象下的 React
、ReactDOM
和 ReactRouterDOM
属性获取。
externals: {
react: 'React',
'react-dom': 'ReactDOM',
'react-router-dom': 'ReactRouterDOM',
},
复制代码
那么这些被剔除的依赖,为什么可以从 window
对象获取到呢?答案就是 CDN
!
我们将这些剔除的依赖,通过 script
标签引入对应的 CDN
资源( CDN
即 内容分发网络,我们可以将这些静态资源存储到 CDN
网络中,以便更快的获取资源)。
这需要我们将引入这些资源的script
标签加在入口 HTML
文件中,这些加载进来的js文件,会将资源挂载在对应的 window
属性 React
、ReactDOM
和 ReactRouterDOM
上。
<script src="https://cdn.staticfile.org/react/0.0.0-0c756fb-f7f79fd/cjs/react.development.js"></script>
<script src="https://cdn.staticfile.org/react-dom/0.0.0-0c756fb-f7f79fd/cjs/react-dom.development.js"></script>
<script src="https://cdn.staticfile.org/react-router-dom/0.0.0-experimental-ffd8c7d0/react-router-dom.development.js"></script>
接下来看下通过 external
+ CDN
策略处理后,我们最终输出的bundle:
react.development.js
、 react-dom.development.js
以及 react-router.js
这三个文件消失了!
作者:Promise
链接:https://juejin.cn/post/7034181462106570759
前端面试js高频手写大全(下)
8. 手写call, apply, bind
手写call
Function.prototype.myCall=function(context=window){ // 函数的方法,所以写在Fuction原型对象上
if(typeof this !=="function"){ // 这里if其实没必要,会自动抛出错误
throw new Error("不是函数")
}
const obj=context||window //这里可用ES6方法,为参数添加默认值,js严格模式全局作用域this为undefined
obj.fn=this //this为调用的上下文,this此处为函数,将这个函数作为obj的方法
const arg=[...arguments].slice(1) //第一个为obj所以删除,伪数组转为数组
res=obj.fn(...arg)
delete obj.fn // 不删除会导致context属性越来越多
return res
}
//用法:f.call(obj,arg1)
function f(a,b){
console.log(a+b)
console.log(this.name)
}
let obj={
name:1
}
f.myCall(obj,1,2) //否则this指向window
obj.greet.call({name: 'Spike'}) //打出来的是 Spike
手写apply(arguments[this, [参数1,参数2.....] ])
Function.prototype.myApply=function(context){ // 箭头函数从不具有参数对象!!!!!这里不能写成箭头函数
let obj=context||window
obj.fn=this
const arg=arguments[1]||[] //若有参数,得到的是数组
let res=obj.fn(...arg)
delete obj.fn
return res
}
function f(a,b){
console.log(a,b)
console.log(this.name)
}
let obj={
name:'张三'
}
f.myApply(obj,[1,2]) //arguments[1]
手写bind
this.value = 2
var foo = {
value: 1
};
var bar = function(name, age, school){
console.log(name) // 'An'
console.log(age) // 22
console.log(school) // '家里蹲大学'
}
var result = bar.bind(foo, 'An') //预置了部分参数'An'
result(22, '家里蹲大学') //这个参数会和预置的参数合并到一起放入bar中
简单版本
Function.prototype.bind = function(context, ...outerArgs) {
var fn = this;
return function(...innerArgs) { //返回了一个函数,...rest为实际调用时传入的参数
return fn.apply(context,[...outerArgs, ...innerArgs]); //返回改变了this的函数,
//参数合并
}
}
new失败的原因:
例:
// 声明一个上下文
let thovino = {
name: 'thovino'
}
// 声明一个构造函数
let eat = function (food) {
this.food = food
console.log(`${this.name} eat ${this.food}`)
}
eat.prototype.sayFuncName = function () {
console.log('func name : eat')
}
// bind一下
let thovinoEat = eat.bind(thovino)
let instance = new thovinoEat('orange') //实际上orange放到了thovino里面
console.log('instance:', instance) // {}
生成的实例是个空对象
在new
操作符执行时,我们的thovinoEat
函数可以看作是这样:
function thovinoEat (...innerArgs) {
eat.call(thovino, ...outerArgs, ...innerArgs)
}
在new操作符进行到第三步的操作thovinoEat.call(obj, ...args)
时,这里的obj
是new操作符自己创建的那个简单空对象{}
,但它其实并没有替换掉thovinoEat
函数内部的那个上下文对象thovino
。这已经超出了call
的能力范围,因为这个时候要替换的已经不是thovinoEat
函数内部的this
指向,而应该是thovino
对象。
换句话说,我们希望的是new
操作符将eat
内的this
指向操作符自己创建的那个空对象。但是实际上指向了thovino
,new
操作符的第三步动作并没有成功!
可new可继承版本
Function.prototype.bind = function (context, ...outerArgs) {
let that = this;
function res (...innerArgs) {
if (this instanceof res) {
// new操作符执行时
// 这里的this在new操作符第三步操作时,会指向new自身创建的那个简单空对象{}
that.call(this, ...outerArgs, ...innerArgs)
} else {
// 普通bind
that.call(context, ...outerArgs, ...innerArgs)
}
}
res.prototype = this.prototype //!!!
return res
}
9. 手动实现new
new的过程文字描述:
创建一个空对象 obj;
将空对象的隐式原型(proto)指向构造函数的prototype。
使用 call 改变 this 的指向
如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。
function Person(name,age){
this.name=name
this.age=age
}
Person.prototype.sayHi=function(){
console.log('Hi!我是'+this.name)
}
let p1=new Person('张三',18)
////手动实现new
function create(){
let obj={}
//获取构造函数
let fn=[].shift.call(arguments) //将arguments对象提出来转化为数组,arguments并不是数组而是对象 !!!这种方法删除了arguments数组的第一个元素,!!这里的空数组里面填不填元素都没关系,不影响arguments的结果 或者let arg = [].slice.call(arguments,1)
obj.__proto__=fn.prototype
let res=fn.apply(obj,arguments) //改变this指向,为实例添加方法和属性
//确保返回的是一个对象(万一fn不是构造函数)
return typeof res==='object'?res:obj
}
let p2=create(Person,'李四',19)
p2.sayHi()
细节:
[].shift.call(arguments) 也可写成:
let arg=[...arguments]
let fn=arg.shift() //使得arguments能调用数组方法,第一个参数为构造函数
obj.__proto__=fn.prototype
//改变this指向,为实例添加方法和属性
let res=fn.apply(obj,arg)
10. 手写promise(常考promise.all, promise.race)
// Promise/A+ 规范规定的三种状态
const STATUS = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}
class MyPromise {
// 构造函数接收一个执行回调
constructor(executor) {
this._status = STATUS.PENDING // Promise初始状态
this._value = undefined // then回调的值
this._resolveQueue = [] // resolve时触发的成功队列
this._rejectQueue = [] // reject时触发的失败队列
// 使用箭头函数固定this(resolve函数在executor中触发,不然找不到this)
const resolve = value => {
const run = () => {
// Promise/A+ 规范规定的Promise状态只能从pending触发,变成fulfilled
if (this._status === STATUS.PENDING) {
this._status = STATUS.FULFILLED // 更改状态
this._value = value // 储存当前值,用于then回调
// 执行resolve回调
while (this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(value)
}
}
}
//把resolve执行回调的操作封装成一个函数,放进setTimeout里,以实现promise异步调用的特性(规范上是微任务,这里是宏任务)
setTimeout(run)
}
// 同 resolve
const reject = value => {
const run = () => {
if (this._status === STATUS.PENDING) {
this._status = STATUS.REJECTED
this._value = value
while (this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(value)
}
}
}
setTimeout(run)
}
// new Promise()时立即执行executor,并传入resolve和reject
executor(resolve, reject)
}
// then方法,接收一个成功的回调和一个失败的回调
function then(onFulfilled, onRejected) {
// 根据规范,如果then的参数不是function,则忽略它, 让值继续往下传递,链式调用继续往下执行
typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
typeof onRejected !== 'function' ? onRejected = error => error : null
// then 返回一个新的promise
return new MyPromise((resolve, reject) => {
const resolveFn = value => {
try {
const x = onFulfilled(value)
// 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
}
}
const rejectFn = error => {
try {
const x = onRejected(error)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
switch (this._status) {
case STATUS.PENDING:
this._resolveQueue.push(resolveFn)
this._rejectQueue.push(rejectFn)
break;
case STATUS.FULFILLED:
resolveFn(this._value)
break;
case STATUS.REJECTED:
rejectFn(this._value)
break;
}
})
}
catch (rejectFn) {
return this.then(undefined, rejectFn)
}
// promise.finally方法
finally(callback) {
return this.then(value => MyPromise.resolve(callback()).then(() => value), error => {
MyPromise.resolve(callback()).then(() => error)
})
}
// 静态resolve方法
static resolve(value) {
return value instanceof MyPromise ? value : new MyPromise(resolve => resolve(value))
}
// 静态reject方法
static reject(error) {
return new MyPromise((resolve, reject) => reject(error))
}
// 静态all方法
static all(promiseArr) {
let count = 0
let result = []
return new MyPromise((resolve, reject) => {
if (!promiseArr.length) {
return resolve(result)
}
promiseArr.forEach((p, i) => {
MyPromise.resolve(p).then(value => {
count++
result[i] = value
if (count === promiseArr.length) {
resolve(result)
}
}, error => {
reject(error)
})
})
})
}
// 静态race方法
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
promiseArr.forEach(p => {
MyPromise.resolve(p).then(value => {
resolve(value)
}, error => {
reject(error)
})
})
})
}
}
11. 手写原生AJAX
步骤
创建 XMLHttpRequest 实例
发出 HTTP 请求
服务器返回 XML 格式的字符串
JS 解析 XML,并更新局部页面
不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。
了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。
version 1.0:
myButton.addEventListener('click', function () {
ajax()
})
function ajax() {
let xhr = new XMLHttpRequest() //实例化,以调用方法
xhr.open('get', 'https://www.google.com') //参数2,url。参数三:异步
xhr.onreadystatechange = () => { //每当 readyState 属性改变时,就会调用该函数。
if (xhr.readyState === 4) { //XMLHttpRequest 代理当前所处状态。
if (xhr.status >= 200 && xhr.status < 300) { //200-300请求成功
let string = request.responseText
//JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象
let object = JSON.parse(string)
}
}
}
request.send() //用于实际发出 HTTP 请求。不带参数为GET请求
}
promise实现
function ajax(url) {
const p = new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest()
xhr.open('get', url)
xhr.onreadystatechange = () => {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status <= 300) {
resolve(JSON.parse(xhr.responseText))
} else {
reject('请求出错')
}
}
}
xhr.send() //发送hppt请求
})
return p
}
let url = '/data.json'
ajax(url).then(res => console.log(res))
.catch(reason => console.log(reason))
12. 手写节流防抖函数
函数节流与函数防抖都是为了限制函数的执行频次,是一种性能优化的方案,比如应用于window对象的resize、scroll事件,拖拽时的mousemove事件,文字输入、自动完成的keyup事件。
节流:连续触发事件但是在 n 秒中只执行一次函数
例:(连续不断动都需要调用时用,设一时间间隔),像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多。
防抖:指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
例:(连续不断触发时不调用,触发完后过一段时间调用),像仿百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。
防抖的实现:
function debounce(fn, delay) {
if(typeof fn!=='function') {
throw new TypeError('fn不是函数')
}
let timer; // 维护一个 timer
return function () {
var _this = this; // 取debounce执行作用域的this(原函数挂载到的对象)
var args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args);
}, delay);
};
}
// 调用
input1.addEventListener('keyup', debounce(() => {
console.log(input1.value)
}), 600)
节流的实现:
function throttle(fn, delay) {
let timer;
return function () {
var _this = this;
var args = arguments;
if (timer) {
return;
}
timer = setTimeout(function () {
fn.apply(_this, args); // 这里args接收的是外边返回的函数的参数,不能用arguments
// fn.apply(_this, arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。
timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
}, delay)
}
}
div1.addEventListener('drag', throttle((e) => {
console.log(e.offsetX, e.offsetY)
}, 100))
13. 手写Promise加载图片
function getData(url) {
return new Promise((resolve, reject) => {
$.ajax({
url,
success(data) {
resolve(data)
},
error(err) {
reject(err)
}
})
})
}
const url1 = './data1.json'
const url2 = './data2.json'
const url3 = './data3.json'
getData(url1).then(data1 => {
console.log(data1)
return getData(url2)
}).then(data2 => {
console.log(data2)
return getData(url3)
}).then(data3 =>
console.log(data3)
).catch(err =>
console.error(err)
)
14. 函数实现一秒钟输出一个数
(!!!这个题这两天字节校招面试被问到了,问var打印的什么,改为let为什么可以?
有没有其他方法实现?我自己博客里都写了不用let的写法第二种方法,居然给忘了~~~白学了)
ES6:用let块级作用域的原理实现
for(let i=0;i<=10;i++){ //用var打印的都是11
setTimeout(()=>{
console.log(i);
},1000*i)
}
不用let的写法: 原理是用立即执行函数创造一个块级作用域
for(var i = 1; i <= 10; i++){
(function (i) {
setTimeout(function () {
console.log(i);
}, 1000 * i)
})(i);
}
15. 创建10个标签,点击的时候弹出来对应的序号?
var a
for(let i=0;i<10;i++){
a=document.createElement('a')
a.innerHTML=i+'<br>'
a.addEventListener('click',function(e){
console.log(this) //this为当前点击的<a>
e.preventDefault() //如果调用这个方法,默认事件行为将不再触发。
//例如,在执行这个方法后,如果点击一个链接(a标签),浏览器不会跳转到新的 URL 去了。我们可以用 event.isDefaultPrevented() 来确定这个方法是否(在那个事件对象上)被调用过了。
alert(i)
})
const d=document.querySelector('div')
d.appendChild(a) //append向一个已存在的元素追加该元素。
}
16. 实现事件订阅发布(eventBus)
实现EventBus类,有 on off once trigger功能,分别对应绑定事件监听器,解绑,执行一次后解除事件绑定,触发事件监听器。 这个题目面字节和快手都问到了,最近忙,答案会在后续更新
class EventBus {
on(eventName, listener) {}
off(eventName, listener) {}
once(eventName, listener) {}
trigger(eventName) {}
}
const e = new EventBus();
// fn1 fn2
e.on('e1', fn1)
e.once('e1', fn2)
e.trigger('e1') // fn1() fn2()
e.trigger('e1') // fn1()
e.off('e1', fn1)
e.trigger('e1') // null
实现:
//声明类
class EventBus {
constructor() {
this.eventList = {} //创建对象收集事件
}
//发布事件
$on(eventName, fn) {
//判断是否发布过事件名称? 添加发布 : 创建并添加发布
this.eventList[eventName]
? this.eventList[eventName].push(fn)
: (this.eventList[eventName] = [fn])
}
//订阅事件
$emit(eventName) {
if (!eventName) throw new Error('请传入事件名')
//获取订阅传参
const data = [...arguments].slice(1)
if (this.eventList[eventName]) {
this.eventList[eventName].forEach((i) => {
try {
i(...data) //轮询事件
} catch (e) {
console.error(e + 'eventName:' + eventName) //收集执行时的报错
}
})
}
}
//执行一次
$once(eventName, fn) {
const _this = this
function onceHandle() {
fn.apply(null, arguments)
_this.$off(eventName, onceHandle) //执行成功后取消监听
}
this.$on(eventName, onceHandle)
}
//取消订阅
$off(eventName, fn) {
//不传入参数时取消全部订阅
if (!arguments.length) {
return (this.eventList = {})
}
//eventName传入的是数组时,取消多个订阅
if (Array.isArray(eventName)) {
return eventName.forEach((event) => {
this.$off(event, fn)
})
}
//不传入fn时取消事件名下的所有队列
if (arguments.length === 1 || !fn) {
this.eventList[eventName] = []
}
//取消事件名下的fn
this.eventList[eventName] = this.eventList[eventName].filter(
(f) => f !== fn
)
}
}
const event = new EventBus()
let b = function (v1, v2, v3) {
console.log('b', v1, v2, v3)
}
let a = function () {
console.log('a')
}
event.$once('test', a)
event.$on('test', b)
event.$emit('test', 1, 2, 3, 45, 123)
event.$off(['test'], b)
event.$emit('test', 1, 2, 3, 45, 123)
参考:
数组扁平化 https://juejin.im/post/5c971ee16fb9a070ce31b64e#heading-3
函数柯里化 https://juejin.im/post/6844903882208837645
节流防抖 https://www.jianshu.com/p/c8b...
事件订阅发布实现 https://heznb.com/archives/js...
浅拷贝深拷贝 https://segmentfault.com/a/11...
收起阅读 »作者:晚起的虫儿
如何写 CSS 重置(RESET)样式?
很长一段时间,我都使用Eric Meyer著名的CSS Reset。这是CSS的一个坚实的块,但是在这一点上它有点长。它已经十多年没有更新了,从那时起发生了很多变化!
最近,我一直在使用我自己的自定义CSS重置。它包括我发现的所有小技巧,以改善用户体验和CSS创作体验。
像其他CSS重置一样,在设计/化妆品方面,它是不赞成的。您可以将此重置用于任何项目,无论您想要哪种美学。
在本教程中,我们将介绍我的自定义 CSS 重置。我们将深入研究每个规则,您将了解它的作用以及您可能想要使用它的原因!
CSS 重置
事不宜迟,这里是:
/*
1. Use a more-intuitive box-sizing model.
*/
*, *::before, *::after {
box-sizing: border-box;
}
/*
2. Remove default margin
*/
* {
margin: 0;
}
/*
3. Allow percentage-based heights in the application
*/
html, body {
height: 100%;
}
/*
Typographic tweaks!
4. Add accessible line-height
5. Improve text rendering
*/
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/*
6. Improve media defaults
*/
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
/*
7. Remove built-in form typography styles
*/
input, button, textarea, select {
font: inherit;
}
/*
8. Avoid text overflows
*/
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
/*
9. Create a root stacking context
*/
#root, #__next {
isolation: isolate;
}
它相对较短,但是这个小样式表中包含了很多东西。让我们开始吧!
从历史上看,CSS重置的主要目标是确保浏览器之间的一致性,并撤消所有默认样式,从而创建一个空白的石板。我的CSS重置并没有真正做这些事情。
如今,浏览器在布局或间距方面没有巨大的差异。总的来说,浏览器忠实地实现了CSS规范,并且事情的行为符合您的预期。因此,它不再是必要的了。
我也不认为有必要剥离所有浏览器默认值。例如,我可能确实想要设置标签!我总是可以在各个项目风格中做出不同的设计决策,但我认为剥离常识性默认值是没有意义的。
<em>``font-style: italic
我的CSS重置可能不符合"CSS重置"的经典定义,但我正在采取这种创造性的自由。
CSS盒子模型
测验!通过可见的粉红色边框进行测量,假设未应用其他 CSS,则在以下方案中元素的宽度是多少?.box
<style>
.parent {
width: 200px;
}
.box {
width: 100%;
border: 2px solid hotpink;
padding: 20px;
}
</style>
<div>
<div></div>
</div>
我们的元素有.因为它的父级是200px宽,所以100%将解析为200px。.box``width: 100%
但是它在哪里应用200px宽度? 默认情况下,它会将该大小应用于内容框。
如果您不熟悉,"内容框"是框模型中实际保存内容的矩形,位于边框和填充内:
该声明会将 的内容框设置为 200px。填充将添加额外的40px(每侧20px)。边框添加最后一个 4px(每侧 2px)。当我们进行数学计算时,可见的粉红色矩形将是244px宽。width: 100%``.box
当我们尝试将一个 244px 的框塞入一个 200px 宽的父级中时,它会溢出:
这种行为很奇怪,对吧?幸运的是,我们可以通过设置以下规则来更改它:
*, *::before, *::after {
box-sizing: border-box;
}
应用此规则后,百分比将基于边框进行解析。在上面的示例中,我们的粉红色框将为 200px,内部内容框将缩小到 156px(200px - 40px - 4px)。
在我看来,这是一个必须的规则。 它使CSS更适合使用。
我们使用通配符选择器 () 将其应用于所有元素和伪元素。与普遍的看法相反,这对性能来说并不坏。*
我在网上看到了一些建议,可以代替这样做:
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
删除默认间距
* {
margin: 0;
}
浏览器围绕保证金做出常识性的假设。例如,默认情况下,将包含比段落更多的边距。h1
这些假设在文字处理文档的上下文中是合理的,但对于现代 Web 应用程序而言,它们可能不准确。
Margin是一个棘手的魔鬼,而且我经常发现自己希望元素默认情况下没有任何元素。所以我决定全部删除它。🔥
如果/当我确实想为特定标签添加一些边距时,我可以在我的自定义项目样式中执行此操作。通配符选择器 () 具有极低的特异性,因此很容易覆盖此规则。*
基于百分比的高度
html, body {
height: 100%;
}
你有没有试过在CSS中使用基于百分比的高度,却发现它似乎没有效果?
下面是一个示例:
元素有,但元素根本不会增长!main``height: 100%
这不起作用,因为在 Flow 布局(CSS 中的主要布局模式)中,并且操作的原则根本不同。元素的宽度是根据其父级计算的,但元素的高度是根据其子元素计算的。height``width
这是一个复杂的主题,远远超出了本文的范围。我计划写一篇关于它的博客文章,但与此同时,你可以在我的CSS课程中了解它,CSS for JavaScript Developers。
作为一个快速演示,在这里我们看到,当我们应用此规则时,我们的元素可以增长:main
如果你使用的是像 React 这样的 JS 框架,你可能还希望向这个规则添加第三个选择器:框架使用的根级元素。
例如,在我的 Next.js 项目中,我按如下方式更新规则:
html, body, #__next {
height: 100%;
}
为什么不使用vh?
您可能想知道:为什么要在基于百分比的高度上大惊小怪?为什么不改用该装置呢?
vh
问题是该单元在移动设备上无法正常工作; 将占用超过100%的屏幕空间,因为移动浏览器在浏览器UI来来去去的地方做那件事。
vh``100vh
将来,新的CSS单元将解决这个问题。在此之前,我继续使用基于百分比的高度。
调整行高
body {
line-height: 1.5;
}
line-height
控制段落中每行文本之间的垂直间距。默认值因浏览器而异,但往往在 1.2 左右。
此无单位数字是基于字体大小的比率。它的功能就像设备一样。如果为 1.2,则每行将比元素的字体大小大 20%。em``line-height
问题是:对于那些有阅读障碍的人来说,这些行挤得太紧,使得阅读起来更加困难。WCAG标准规定行高应至少为1.5。
现在,这个数字确实倾向于在标题和其他具有大类型的元素上产生相当大的行:
您可能希望在标题上覆盖此值。我的理解是,WCAG标准适用于"正文"文本,而不是标题。
使用"计算"实现更智能的线高
我一直在尝试一种管理行高的替代方法。在这里:
* {
line-height: calc(1em + 0.5rem);
}
这是一个非常高级的小片段,它超出了这篇博客文章的范围,但这里有一个快速的解释。
字体平滑,抗锯齿
body {
-webkit-font-smoothing: antialiased;
}
好吧,所以这个有点争议。
在 MacOS 电脑上,浏览器将默认使用"子像素抗锯齿"。这是一种旨在通过利用每个像素内的 R/G/B 灯使文本更易于阅读的技术。
过去,这被视为可访问性的胜利,因为它提高了文本对比度。您可能已经读过一篇流行的博客文章停止"修复"字体平滑,该帖子主张反对切换到"抗锯齿"。
问题是:那篇文章写于2012年,在高DPI"视网膜"显示时代之前。今天的像素要小得多,肉眼看不见。
像素 LED 的物理排列也发生了变化。如果你在显微镜下看一台现代显示器,你不会再看到R/G/B线的有序网格了。
在2018年发布的MacOS Mojave中 ,Apple在整个操作系统中禁用了子像素抗锯齿。我猜他们意识到它在现代硬件上弊大于利。
令人困惑的是,像Chrome和Safari这样的MacOS浏览器仍然默认使用子像素抗锯齿。我们需要通过设置为 来显式关闭它。-webkit-font-smoothing``antialiased
区别如下:
MacOS 是唯一使用子像素抗锯齿的操作系统,因此此规则对 Windows、Linux 或移动设备没有影响。如果您使用的是 MacOS 电脑,则可以尝试实时渲染:
合理的媒体默认值
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
所以这里有一件奇怪的事情:图像被认为是"内联"元素。这意味着它们应该在段落的中间使用,例如 或 。<em>``<strong>
这与我大多数时候使用图像的方式不符。通常,我对待图像的方式与处理段落或标题或侧边栏的方式相同;它们是布局元素。
但是,如果我们尝试在布局中使用内联元素,则会发生奇怪的事情。如果您曾经有过一个神秘的4px间隙,不是边距,填充或边框,那么它可能是浏览器添加的"内联魔术空间"。line-height
通过默认设置所有图像,我们回避了整个类别的时髦问题。display: block
我也设置了.这样做是为了防止大图像溢出,如果它们放置在不够宽而无法容纳它们的容器中。max-width: 100%
大多数块级元素会自动增大/缩小以适应其父元素,但媒体元素是特殊的:它们被称为替换元素,并且它们不遵循相同的规则。<img>
如果图像的"本机"大小为 800×600,则该元素的宽度也将为 800px,即使我们将其放入 500px 宽的父级中也是如此。<img>
此规则将防止该图像超出其容器,这对我来说更像是更明智的默认行为。
继承窗体控件的字体
input, button, textarea, select {
font: inherit;
}
如果我们想避免这种自动缩放行为,输入的字体大小需要至少为1rem / 16px。以下是解决此问题的一种方法:
CSS
input, button, textarea, select {
font-size: 1rem;
}
这解决了自动变焦问题,但它是创可贴。让我们解决根本原因:表单输入不应该有自己的印刷风格!
CSS
input, button, textarea, select {
font: inherit;
}
font
是一种很少使用的速记,它设置了一堆与字体相关的属性,如 、 和 。通过将其设置为 ,我们指示这些元素与其周围环境中的排版相匹配。font-size``font-weight``font-family``inherit
只要我们不为正文文本选择令人讨厌的小字体大小,就可以同时解决我们所有的问题。🎉
自动换行
CSS
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
在 CSS 中,如果没有足够的空间来容纳一行上的所有字符,文本将自动换行。
默认情况下,该算法将寻找"软包装"机会;这些是算法可以拆分的字符。在英语中,唯一的软包装机会是空格和连字符,但这因语言而异。
如果某行没有任何软换行机会,并且它不合适,则会导致文本溢出:
这可能会导致一些令人讨厌的布局问题。在这里,它添加了一个水平滚动条。在其他情况下,它可能会导致文本与其他元素重叠,或滑到图像/视频后面。
该属性允许我们调整换行算法,并允许它在找不到软换行机会时使用硬换行:overflow-wrap
这两种解决方案都不完美,但至少硬包装不会弄乱布局!
感谢Sophie Alpert提出类似的规则!她建议将其应用于所有元素,这可能是一个好主意,但不是我个人测试过的东西。
您也可以尝试添加属性:hyphens
p {
overflow-wrap: break-word;
hyphens: auto;
}
hyphens: auto
使用连字符(在支持连字符的语言中)来指示硬换行。这也使得硬包装更加普遍。
如果您有非常窄的文本列,这可能是值得的,但它也可能有点分散注意力。我选择不将其包含在重置中,但值得尝试!
根堆叠上下文
#root, #__next {
isolation: isolate;
}
最后一个是可选的。通常只有当你使用像 React 这样的 JS 框架时才需要它。
正如我们在"到底是什么,z-index??"中看到的那样,该属性允许我们创建新的堆叠上下文,而无需设置 .isolation``z-index
这是有益的,因为它允许我们保证某些高优先级元素(模式,下拉列表,工具提示)将始终显示在应用程序中的其他元素之上。没有奇怪的堆叠上下文错误,没有z指数军备竞赛。
您应该调整选择器以匹配您的框架。我们希望选择在其中呈现应用程序的顶级元素。例如,create-react-app 使用 一个 ,因此正确的选择器是 。<div id="root">``#root
最终成品
下面再次以精简的复制友好格式进行 CSS 重置:
/*
Josh's Custom CSS Reset
https://www.joshwcomeau.com/css/custom-css-reset/
*/
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html, body {
height: 100%;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
#root, #__next {
isolation: isolate;
}
```
```
/*
Josh's Custom CSS Reset
https://www.joshwcomeau.com/css/custom-css-reset/
*/
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html, body {
height: 100%;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
#root, #__next {
isolation: isolate;
}
```
`
作者:非优秀程序员
链接:https://juejin.cn/post/7034308682825351176
收起阅读 »
前端面试js高频手写大全(上)
介绍
在前端面试中,手撕代码显然是不可避免的,并且占很大的一部分比重。
一般来说,如果代码写的好,即使理论知识答得不够清楚,也能有大概率通过面试。并且其实很多手写往往背后就考察了你对相关理论的认识。
编程题主要分为这几种类型:
* 算法题
* 涉及js原理的题以及ajax请求
* 业务场景题: 实现一个具有某种功能的组件
* 其他(进阶,对计算机综合知识的考察,考的相对较少):实现订阅发布者模式;分别用面向对象编程,面向过程编程,函数式编程实现把大象放进冰箱等等
其中前两种类型所占比重最大。
算法题建议养成每天刷一道leetcode的习惯,重点刷数据结构(栈,链表,队列,树),动态规划,DFS,BFS
本文主要涵盖了第二种类型的各种重点手写。
建议优先掌握:
instanceof (考察对原型链的理解)
new (对创建对象实例过程的理解)
call&apply&bind (对this指向的理解)
手写promise (对异步的理解)
手写原生ajax (对ajax原理和http请求方式的理解,重点是get和post请求的实现)
事件订阅发布 (高频考点)
其他:数组,字符串的api的实现,难度相对较低。只要了解数组,字符串的常用方法的用法,现场就能写出来个大概。(ps:笔者认为数组的reduce方法比较难,这块有余力可以单独看一些,即使面试没让你实现reduce,写其他题时用上它也是很加分的)
话不多说,直接开始
1. 手写instanceof
instanceof作用:
判断一个实例是否是其父类或者祖先类型的实例。
instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype查找失败,返回 false
let myInstanceof = (target,origin) => {
while(target) {
if(target.__proto__===origin.prototype) {
return true
}
target = target.__proto__
}
return false
}
let a = [1,2,3]
console.log(myInstanceof(a,Array)); // true
console.log(myInstanceof(a,Object)); // true
2. 实现数组的map方法
数组的map() 方法会返回一个新的数组,这个新数组中的每个元素对应原数组中的对应位置元素调用一次提供的函数后的返回值。
用法:
const a = [1, 2, 3, 4];
const b = array1.map(x => x * 2);
console.log(b); // Array [2, 4, 6, 8]
实现前,我们先看一下map方法的参数有哪些
map方法有两个参数,一个是操作数组元素的方法fn,一个是this指向(可选),其中使用fn时可以获取三个参数,实现时记得不要漏掉,这样才算完整实现嘛
原生实现:
// 实现
Array.prototype.myMap = function(fn, thisValue) {
let res = []
thisValue = thisValue||[]
let arr = this
for(let i=0; i<arr.length; i++) {
res.push(fn.call(thisValue, arr[i],i,arr)) // 参数分别为this指向,当前数组项,当前索引,当前数组
}
return res
}
// 使用
const a = [1,2,3];
const b = a.myMap((a,index)=> {
return a+1;
}
)
console.log(b) // 输出 [2, 3, 4]
3. reduce实现数组的map方法
利用数组内置的reduce方法实现map方法,考察对reduce原理的掌握
Array.prototype.myMap = function(fn,thisValue){
var res = [];
thisValue = thisValue||[];
this.reduce(function(pre,cur,index,arr){
return res.push(fn.call(thisValue,cur,index,arr));
},[]);
return res;
}
var arr = [2,3,1,5];
arr.myMap(function(item,index,arr){
console.log(item,index,arr);
})
4. 手写数组的reduce方法
reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终为一个值,是ES5中新增的又一个数组逐项处理方法
参数:
callback(一个在数组中每一项上调用的函数,接受四个函数:)
previousValue(上一次调用回调函数时的返回值,或者初始值)
currentValue(当前正在处理的数组元素)
currentIndex(当前正在处理的数组元素下标)
array(调用reduce()方法的数组)
initialValue(可选的初始值。作为第一次调用回调函数时传给previousValue的值)
function reduce(arr, cb, initialValue){
var num = initValue == undefined? num = arr[0]: initValue;
var i = initValue == undefined? 1: 0
for (i; i< arr.length; i++){
num = cb(num,arr[i],i)
}
return num
}
function fn(result, currentValue, index){
return result + currentValue
}
var arr = [2,3,4,5]
var b = reduce(arr, fn,10)
var c = reduce(arr, fn)
console.log(b) // 24
5. 数组扁平化
数组扁平化就是把多维数组转化成一维数组
1. es6提供的新方法 flat(depth)
let a = [1,[2,3]];
a.flat(); // [1,2,3]
a.flat(1); //[1,2,3]
其实还有一种更简单的办法,无需知道数组的维度,直接将目标数组变成1维数组。 depth的值设置为Infinity。
let a = [1,[2,3,[4,[5]]]];
a.flat(Infinity); // [1,2,3,4,5] a是4维数组
2. 利用cancat
function flatten(arr) {
var res = [];
for (let i = 0, length = arr.length; i < length; i++) {
if (Array.isArray(arr[i])) {
res = res.concat(flatten(arr[i])); //concat 并不会改变原数组
//res.push(...flatten(arr[i])); //或者用扩展运算符
} else {
res.push(arr[i]);
}
}
return res;
}
let arr1 = [1, 2,[3,1],[2,3,4,[2,3,4]]]
flatten(arr1); //[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
补充:指定deep的flat
只需每次递归时将当前deep-1,若大于0,则可以继续展开
function flat(arr, deep) {
let res = []
for(let i in arr) {
if(Array.isArray(arr[i])&&deep) {
res = res.concat(flat(arr[i],deep-1))
} else {
res.push(arr[i])
}
}
return res
}
console.log(flat([12,[1,2,3],3,[2,4,[4,[3,4],2]]],1));
6. 函数柯里化
用的这里的方法 https://juejin.im/post/684490...
柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。
当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?
有两种思路:
通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数
在调用柯里化工具函数时,手动指定所需的参数个数
将这两点结合一下,实现一个简单 curry 函数:
/**
* 将函数柯里化
* @param fn 待柯里化的原函数
* @param len 所需的参数个数,默认为原函数的形参个数
*/
function curry(fn,len = fn.length) {
return _curry.call(this,fn,len)
}
/**
* 中转函数
* @param fn 待柯里化的原函数
* @param len 所需的参数个数
* @param args 已接收的参数列表
*/
function _curry(fn,len,...args) {
return function (...params) {
let _args = [...args,...params];
if(_args.length >= len){
return fn.apply(this,_args);
}else{
return _curry.call(this,fn,len,..._args)
}
}
}
我们来验证一下:
let _fn = curry(function(a,b,c,d,e){
console.log(a,b,c,d,e)
});
_fn(1,2,3,4,5); // print: 1,2,3,4,5
_fn(1)(2)(3,4,5); // print: 1,2,3,4,5
_fn(1,2)(3,4)(5); // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5
我们常用的工具库 lodash 也提供了 curry 方法,并且增加了非常好玩的 placeholder 功能,通过占位符的方式来改变传入参数的顺序。
比如说,我们传入一个占位符,本次调用传递的参数略过占位符, 占位符所在的位置由下次调用的参数来填充,比如这样:
直接看一下官网的例子:
接下来我们来思考,如何实现占位符的功能。
对于 lodash 的 curry 函数来说,curry 函数挂载在 lodash 对象上,所以将 lodash 对象当做默认占位符来使用。
而我们的自己实现的 curry 函数,本身并没有挂载在任何对象上,所以将 curry 函数当做默认占位符
使用占位符,目的是改变参数传递的顺序,所以在 curry 函数实现中,每次需要记录是否使用了占位符,并且记录占位符所代表的参数位置。
直接上代码:
/**
* @param fn 待柯里化的函数
* @param length 需要的参数个数,默认为函数的形参个数
* @param holder 占位符,默认当前柯里化函数
* @return {Function} 柯里化后的函数
*/
function curry(fn,length = fn.length,holder = curry){
return _curry.call(this,fn,length,holder,[],[])
}
/**
* 中转函数
* @param fn 柯里化的原函数
* @param length 原函数需要的参数个数
* @param holder 接收的占位符
* @param args 已接收的参数列表
* @param holders 已接收的占位符位置列表
* @return {Function} 继续柯里化的函数 或 最终结果
*/
function _curry(fn,length,holder,args,holders){
return function(..._args){
//将参数复制一份,避免多次操作同一函数导致参数混乱
let params = args.slice();
//将占位符位置列表复制一份,新增加的占位符增加至此
let _holders = holders.slice();
//循环入参,追加参数 或 替换占位符
_args.forEach((arg,i)=>{
//真实参数 之前存在占位符 将占位符替换为真实参数
if (arg !== holder && holders.length) {
let index = holders.shift();
_holders.splice(_holders.indexOf(index),1);
params[index] = arg;
}
//真实参数 之前不存在占位符 将参数追加到参数列表中
else if(arg !== holder && !holders.length){
params.push(arg);
}
//传入的是占位符,之前不存在占位符 记录占位符的位置
else if(arg === holder && !holders.length){
params.push(arg);
_holders.push(params.length - 1);
}
//传入的是占位符,之前存在占位符 删除原占位符位置
else if(arg === holder && holders.length){
holders.shift();
}
});
// params 中前 length 条记录中不包含占位符,执行函数
if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){
return fn.apply(this,params);
}else{
return _curry.call(this,fn,length,holder,params,_holders)
}
}
}
验证一下:;
let fn = function(a, b, c, d, e) {
console.log([a, b, c, d, e]);
}
let _ = {}; // 定义占位符
let _fn = curry(fn,5,_); // 将函数柯里化,指定所需的参数个数,指定所需的占位符
_fn(1, 2, 3, 4, 5); // print: 1,2,3,4,5
_fn(_, 2, 3, 4, 5)(1); // print: 1,2,3,4,5
_fn(1, _, 3, 4, 5)(2); // print: 1,2,3,4,5
_fn(1, _, 3)(_, 4,_)(2)(5); // print: 1,2,3,4,5
_fn(1, _, _, 4)(_, 3)(2)(5); // print: 1,2,3,4,5
_fn(_, 2)(_, _, 4)(1)(3)(5); // print: 1,2,3,4,5
至此,我们已经完整实现了一个 curry 函数~~
7. 浅拷贝和深拷贝的实现
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
浅拷贝和深拷贝的区别:
浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,如果其中一个对象改变了引用类型的属性,就会影响到另一个对象。
深拷贝:将一个对象从内存中完整的复制一份出来,从堆内存中开辟一个新区域存放。这样更改拷贝值就不影响旧的对象
浅拷贝实现:
方法一:
function shallowCopy(target, origin){
for(let item in origin) target[item] = origin[item];
return target;
}
其他方法(内置api):
Object.assign
var obj={a:1,b:[1,2,3],c:function(){console.log('i am c')}}
var tar={};
Object.assign(tar,obj);
当然这个方法只适合于对象类型,如果是数组可以使用slice和concat方法
Array.prototype.slice
var arr=[1,2,[3,4]];
var newArr=arr.slice(0);
Array.prototype.concat
var arr=[1,2,[3,4]];
var newArr=arr.concat();
测试同上(assign用对象测试、slice concat用数组测试),结合浅拷贝深拷贝的概念来理解效果更佳
深拷贝实现:
方法一:
转为json格式再解析
const a = JSON.parse(JSON.stringify(b))
方法二:
// 实现深拷贝 递归
function deepCopy(newObj,oldObj){
for(var k in oldObj){
let item=oldObj[k]
// 判断是数组、对象、简单类型?
if(item instanceof Array){
newObj[k]=[]
deepCopy(newObj[k],item)
}else if(item instanceof Object){
newObj[k]={}
deepCopy(newObj[k],item)
}else{ //简单数据类型,直接赋值
newObj[k]=item
}
}
}
(未完待续……)作者:晚起的虫儿
太震撼了!我把七大JS排序算法做成了可视化!!!太好玩了!
前言
大家好,我是林三心。写这篇文章是有原因的,偶然我看到了一个Java的50种排序算法的可视化
的视频,但是此视频却没给出具体的实现教程,于是我心里就想着,我可以用JavaScript + canvas
去实现这个酷炫的效果。每种排序算法的动画效果基本都不一样哦。例如冒泡排序
是这样的
实现思路
想实现的效果
从封面可以看到,无论是哪种算法,一开始都是第一张图,而最终目的是要变成第二张图的效果
极坐标
讲实现思路之前,我先给大家复习一下高中的一个知识——极坐标。哈哈,不知道还有几个人记得他呢?
- O:极点,也就是原点
- ρ:极径
- θ:极径与X轴夹角
x = ρ * cosθ
,因为x / ρ = cosθ
y = ρ * sinθ
,因为y / ρ = sinθ
那我们想实现的结果,又跟极坐标有何关系呢?其实是有关系的,比如我现在有一个排序好的数组,他具有37个元素,那我们可以把这37个元素
转化为极坐标中的37个点
,怎么转呢?
const arr = [
0, 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
]
我们可以这么转:
元素对应的索引index * 10 -> 角度θ
(为什么要乘10呢,因为要凑够360°嘛)元素对应的值arr[index] -> 极径ρ
按照上面的规则来转的话,那我们就可以在极坐标上得到这37个点(在canvas中Y轴是由上往下的,下面这个图也是按canvas的,但是Y轴我还是画成正常方向,所以这个图其实是反的,但是是有原因的哈):
(0 -> θ = 00°,ρ = 0) (1 -> θ = 10°,ρ = 1) (2 -> θ = 20°,ρ = 2) (3 -> θ = 30°,ρ = 3)
(4 -> θ = 40°,ρ = 4) (5 -> θ = 50°,ρ = 5) (6 -> θ = 60°,ρ = 6) (7 -> θ = 70°,ρ = 7)
(8 -> θ = 80°,ρ = 8) (9 -> θ = 90°,ρ = 9) (10 -> θ = 100°,ρ = 10) (11 -> θ = 110°,ρ = 11)
(12 -> θ = 120°,ρ = 12) (13 -> θ = 130°,ρ = 13) (14 -> θ = 140°,ρ = 14) (15 -> θ = 150°,ρ = 15)
(16 -> θ = 160°,ρ = 16) (17 -> θ = 170°,ρ = 17) (18 -> θ = 180°,ρ = 18) (19 -> θ = 190°,ρ = 19)
(20 -> θ = 200°,ρ = 20) (21 -> θ = 210°,ρ = 21) (22 -> θ = 220°,ρ = 22) (23 -> θ = 230°,ρ = 23)
(24 -> θ = 240°,ρ = 24) (25 -> θ = 250°,ρ = 25) (26 -> θ = 260°,ρ = 26) (27 -> θ = 270°,ρ = 27)
(28 -> θ = 280°,ρ = 28) (29 -> θ = 290°,ρ = 29) (30 -> θ = 300°,ρ = 30) (31 -> θ = 310°,ρ = 31)
(32 -> θ = 320°,ρ = 32) (33 -> θ = 330°,ρ = 33) (34 -> θ = 340°,ρ = 34) (35 -> θ = 350°,ρ = 35)
(36 -> θ = 360°,ρ = 36)
有没有发现,跟咱们想实现的最终效果的轨迹很像呢?
随机打散
那说完最终的效果,咱们来下想想如何一开始先把数组的各个元素打散在极坐标上呢?其实很简单,咱们可以先把生成一个乱序的数组,比如
const arr = [
25, 8, 32, 1, 19, 14, 0, 29, 17,
6, 7, 26, 3, 30, 31, 16, 28, 15,
24, 10, 21, 2, 9, 4, 35, 5, 36,
33, 11, 27, 34, 22, 13, 18, 23, 12, 20
]
然后还是用上面那个规则,去转换极坐标
元素对应的索引index * 10 -> 角度θ
(为什么要乘10呢,因为要凑够360°嘛)元素对应的值arr[index] -> 极径ρ
那么我们可以的到这37个点,自然就可以实现打散的效果
(25 -> θ = 00°,ρ = 25) (8 -> θ = 10°,ρ = 8) (32 -> θ = 20°,ρ = 32) (1 -> θ = 30°,ρ = 1)
(19 -> θ = 40°,ρ = 19) (14 -> θ = 50°,ρ = 14) (0 -> θ = 60°,ρ = 0) (29 -> θ = 70°,ρ = 29)
(17 -> θ = 80°,ρ = 17) (6 -> θ = 90°,ρ = 6) (7 -> θ = 100°,ρ = 7) (26 -> θ = 110°,ρ = 26)
(3 -> θ = 120°,ρ = 3) (30 -> θ = 130°,ρ = 30) (31 -> θ = 140°,ρ = 31) (16 -> θ = 150°,ρ = 16)
(28 -> θ = 160°,ρ = 28) (15 -> θ = 170°,ρ = 15) (24 -> θ = 180°,ρ = 24) (10 -> θ = 190°,ρ = 10)
(21 -> θ = 200°,ρ = 21) (2 -> θ = 210°,ρ = 2) (9 -> θ = 220°,ρ = 9) (4 -> θ = 230°,ρ = 4)
(35 -> θ = 240°,ρ = 35) (5 -> θ = 250°,ρ = 5) (36 -> θ = 260°,ρ = 36) (33 -> θ = 270°,ρ = 33)
(11 -> θ = 280°,ρ = 11) (27 -> θ = 290°,ρ = 27) (34 -> θ = 300°,ρ = 34) (22 -> θ = 310°,ρ = 22)
(13 -> θ = 320°,ρ = 13) (18 -> θ = 330°,ρ = 18) (23 -> θ = 340°,ρ = 23) (12 -> θ = 350°,ρ = 12)
(20 -> θ = 360°,ρ = 20)
实现效果
综上所述,咱们想实现效果,也就有了思路
- 1、先生成一个
乱序数组
- 2、用canvas画布画出此
乱序数组
所有元素对应的极坐标对应的点
- 3、对
乱序数组
进行排序
- 4、排序过程中
不断清空画布
,并重画
数组所有元素对应的极坐标对应的点 - 5、直到排序完成,终止画布操作
开搞!!!
咱们,做事情一定要有条有理才行,还记得上面说的步骤吗?
- 1、先生成一个
乱序数组
- 2、用canvas画布画出此
乱序数组
所有元素对应的极坐标对应的点
- 3、对
乱序数组
进行排序
- 4、排序过程中
不断清空画布
,并重画
数组所有元素对应的极坐标对应的点 - 5、直到排序完成,终止画布操作
咱们就按照这个步骤,来一步一步实现效果,兄弟们,冲啊!!!
生成乱序数组
咱们上面举的例子是37个元素,但是37个肯定是太少了,咱们搞多点吧,我搞了这么一个数组nums:我先生成一个0 - 179
的有序数组,然后打乱,并塞进数组nums中,此操作我执行4次。为什么是0 - 179
,因为0 - 179
刚好有180个数字
身位一个程序员,我肯定不可能自己手打这么多元素的啦。。来。。上代码
let nums = []
for (let i = 0; i < 4; i++) {
// 生成一个 0 - 179的有序数组
const arr = [...Array(180).keys()] // Array.keys()可以学一下,很有用
const res = []
while (arr.length) {
// 打乱
const randomIndex = Math.random() * arr.length - 1
res.push(arr.splice(randomIndex, 1)[0])
}
nums = [...nums, ...res]
}
经过上面操作,也就是我的nums中拥有4 * 180 = 720
个元素,nums中的元素都是0 - 179
范围内的
canvas画乱序数组
画canvas之前,肯定要现在html页面上,编写一个canvas的节点,这里我宽度设置1000,高度也是1000,并且背景颜色是黑色
<canvas id="canvas" width="1000" height="1000" style="background: #000;"></canvas>
上面看到了,极点(原点)是在坐标正中间的,但是canvas的初始原点是在画布的左上角,我们需要把canvas的原点移动到画布的正中间,那正中间的坐标是多少呢?还记得咱们宽高都是1000吗?那画布中心点坐标不就是(500, 500)
,咱们可以使用canvas的ctx.translate(500, 500)
来移动中心点位置。因为咱们画的点都是白色的,所以咱们顺便把ctx.fillStyle
设置为white
有一点注意了哈,canvas里的Y轴是自上向下的,与常规的Y轴的相反的。
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'white' // 设置画画的颜色
ctx.translate(500, 500) // 移动中心点到(500, 500)
那到底该怎么画点呢?按照之前的,其实光计算出角度θ
和极径ρ
是不够的,因为canvas画板不认这两个东西啊。。那canvas认啥呢,他只认(x, y)
,所以咱们只要通过角度θ
和极径ρ
去算出(x, y)
,就好了,还记得前面极坐标的公式吗
x = ρ * cosθ
,因为x / ρ = cosθ
y = ρ * sinθ
,因为y / ρ = sinθ
由于咱们是要铺散点是要铺出一个圆形来,那么一个圆形的角度是0° - 360°
,但是我们不要360°,咱们只要0° - 359°
,因为0°和360°
是同一个直线。咱们一个直线上有一个度数就够了。所以咱们要求出0° - 359°
每个角度所对应的cosθ和sinθ
(这里咱们只算整数角度,不算小数角度)
const CosandSin = []
for (let i = 0; i < 360; i++) {
const jiaodu = i / 180 * Math.PI
CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
}
这时候又有新问题了,咱们一个圆上的整数角度只有0° - 359°
这360个整数角
,但是nums
中有720个元素
啊,那怎么分配画布呢?很简单啊,一个角度上画2个元素,那不就刚好 2 * 360 = 720
行,咱们废话不多说,开始画初始散点吧。咱们也知道咱们需要画720个点,对于这种多个相同的东西,咱们要多多使用面向对象
这种编程思想
// 单个长方形构造函数
function Rect(x, y, width, height) {
this.x = x // 坐标x
this.y = y // 坐标y
this.width = width // 长方形的宽
this.height = height // 长方形的高
}
// 单个长方形的渲染函数
Rect.prototype.draw = function () {
ctx.beginPath() // 开始画一个
ctx.fillRect(this.x, this.y, this.width, this.height) // 画一个
ctx.closePath() // 结束画一个
}
const CosandSin = []
for (let i = 0; i < 360; i++) {
const jiaodu = i / 180 * Math.PI
CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
}
function drawAll(arr) {
const rects = [] // 用来存储720个长方形
for (let i = 0; i < arr.length; i++) {
const num = arr[i]
const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
const x = num * cos // x = ρ * cosθ
const y = num * sin // y = ρ * sinθ
rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
}
rects.forEach(rect => rect.draw()) // 遍历渲染
}
drawAll(nums) // 执行渲染函数
来页面中看看效果吧。此时就完成了初始的散点渲染
边排序边重画
其实很简单,就是排序一次,就清空画布,然后重新执行上面的渲染函数drawAll
就行了。由于性能原因,我先把drawAll
封装成一个Promise函数
function drawAll(arr) {
return new Promise((resolve) => {
setTimeout(() => {
ctx.clearRect(-500, -500, 1000, 1000) // 清空画布
const rects = [] // 用来存储720个长方形
for (let i = 0; i < arr.length; i++) {
const num = arr[i]
const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
const x = num * cos // x = ρ * cosθ
const y = num * sin // y = ρ * sinθ
rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
}
rects.forEach(rect => rect.draw()) // 遍历渲染
resolve('draw success')
}, 10)
})
}
然后咱们拿一个排序算法例子来讲一讲,就拿个冒泡排序
来讲吧
async function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { //相邻元素两两对比
var temp = arr[j + 1]; //元素交换
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
await drawAll(arr) // 一边排序一边重新画
}
return arr;
}
然后在页面里放一个按钮,用来执行开始排序
<button id="btn">开始排序</button>
document.getElementById('btn').onclick = function () {
bubbleSort(nums)
}
效果如下,是不是很开心哈哈哈!!!
完整代码
这是完整代码
<canvas id="canvas" width="1000" height="1000" style="background: #000;"></canvas>
<button id="btn">开始排序</button>
复制代码
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'white' // 设置画画的颜色
ctx.translate(500, 500) // 移动中心点到(500, 500)
let nums = []
for (let i = 0; i < 4; i++) {
// 生成一个 0 - 180的有序数组
const arr = [...Array(180).keys()]
const res = []
while (arr.length) {
// 打乱
const randomIndex = Math.random() * arr.length - 1
res.push(arr.splice(randomIndex, 1)[0])
}
nums = [...nums, ...res]
}
// 单个长方形构造函数
function Rect(x, y, width, height) {
this.x = x // 坐标x
this.y = y // 坐标y
this.width = width // 长方形的宽
this.height = height // 长方形的高
}
// 单个长方形的渲染函数
Rect.prototype.draw = function () {
ctx.beginPath() // 开始画一个
ctx.fillRect(this.x, this.y, this.width, this.height) // 画一个
ctx.closePath() // 结束画一个
}
const CosandSin = []
for (let i = 0; i < 360; i++) {
const jiaodu = i / 180 * Math.PI
CosandSin.push({ cos: Math.cos(jiaodu), sin: Math.sin(jiaodu) })
}
function drawAll(arr) {
return new Promise((resolve) => {
setTimeout(() => {
ctx.clearRect(-500, -500, 1000, 1000) // 清空画布
const rects = [] // 用来存储720个长方形
for (let i = 0; i < arr.length; i++) {
const num = arr[i]
const { cos, sin } = CosandSin[Math.floor(i / 2)] // 一个角画两个
const x = num * cos // x = ρ * cosθ
const y = num * sin // y = ρ * sinθ
rects.push(new Rect(x, y, 5, 3)) // 收集所有长方形
}
rects.forEach(rect => rect.draw()) // 遍历渲染
resolve('draw success')
}, 10)
})
}
drawAll(nums) // 执行渲染函数
async function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { //相邻元素两两对比
var temp = arr[j + 1]; //元素交换
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
await drawAll(arr) // 一边排序一边重新画
}
return arr;
}
document.getElementById('btn').onclick = function () {
bubbleSort(nums) // 点击执行
}
正片开始!!!
首先说明,哈哈
- 我是算法渣渣
- 每种算法排序,动画都不一样
- drawAll放在不同地方也可能有不同效果
冒泡排序
async function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { //相邻元素两两对比
var temp = arr[j + 1]; //元素交换
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
await drawAll(arr) // 一边排序一边重新画
}
return arr;
}
document.getElementById('btn').onclick = function () {
bubbleSort(nums) // 点击执行
}
选择排序
async function selectionSort(arr) {
var len = arr.length;
var minIndex, temp;
for (var i = 0; i < len - 1; i++) {
minIndex = i;
for (var j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) { //寻找最小的数
minIndex = j; //将最小数的索引保存
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
await drawAll(arr)
}
return arr;
}
document.getElementById('btn').onclick = function () {
selectionSort(nums)
}
插入排序
async function insertionSort(arr) {
if (Object.prototype.toString.call(arr).slice(8, -1) === 'Array') {
for (var i = 1; i < arr.length; i++) {
var key = arr[i];
var j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
await drawAll(arr)
}
return arr;
} else {
return 'arr is not an Array!';
}
}
document.getElementById('btn').onclick = function () {
insertionSort(nums)
}
堆排序
async function heapSort(array) {
if (Object.prototype.toString.call(array).slice(8, -1) === 'Array') {
//建堆
var heapSize = array.length, temp;
for (var i = Math.floor(heapSize / 2) - 1; i >= 0; i--) {
heapify(array, i, heapSize);
await drawAll(array)
}
//堆排序
for (var j = heapSize - 1; j >= 1; j--) {
temp = array[0];
array[0] = array[j];
array[j] = temp;
heapify(array, 0, --heapSize);
await drawAll(array)
}
return array;
} else {
return 'array is not an Array!';
}
}
function heapify(arr, x, len) {
if (Object.prototype.toString.call(arr).slice(8, -1) === 'Array' && typeof x === 'number') {
var l = 2 * x + 1, r = 2 * x + 2, largest = x, temp;
if (l < len && arr[l] > arr[largest]) {
largest = l;
}
if (r < len && arr[r] > arr[largest]) {
largest = r;
}
if (largest != x) {
temp = arr[x];
arr[x] = arr[largest];
arr[largest] = temp;
heapify(arr, largest, len);
}
} else {
return 'arr is not an Array or x is not a number!';
}
}
document.getElementById('btn').onclick = function () {
heapSort(nums)
}
快速排序
async function quickSort(array, left, right) {
drawAll(nums)
if (Object.prototype.toString.call(array).slice(8, -1) === 'Array' && typeof left === 'number' && typeof right === 'number') {
if (left < right) {
var x = array[right], i = left - 1, temp;
for (var j = left; j <= right; j++) {
if (array[j] <= x) {
i++;
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
await drawAll(nums)
await quickSort(array, left, i - 1);
await quickSort(array, i + 1, right);
await drawAll(nums)
}
return array;
} else {
return 'array is not an Array or left or right is not a number!';
}
}
document.getElementById('btn').onclick = function () {
quickSort(nums, 0, nums.length - 1)
}
基数排序
async function radixSort(arr, maxDigit) {
var mod = 10;
var dev = 1;
var counter = [];
for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
for (var j = 0; j < arr.length; j++) {
var bucket = parseInt((arr[j] % mod) / dev);
if (counter[bucket] == null) {
counter[bucket] = [];
}
counter[bucket].push(arr[j]);
}
var pos = 0;
for (var j = 0; j < counter.length; j++) {
var value = null;
if (counter[j] != null) {
while ((value = counter[j].shift()) != null) {
arr[pos++] = value;
await drawAll(arr)
}
}
}
}
return arr;
}
document.getElementById('btn').onclick = function () {
radixSort(nums, 3)
}
希尔排序
async function shellSort(arr) {
var len = arr.length,
temp,
gap = 1;
while (gap < len / 5) { //动态定义间隔序列
gap = gap * 5 + 1;
}
for (gap; gap > 0; gap = Math.floor(gap / 5)) {
for (var i = gap; i < len; i++) {
temp = arr[i];
for (var j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
arr[j + gap] = arr[j];
}
arr[j + gap] = temp;
await drawAll(arr)
}
}
return arr;
}
document.getElementById('btn').onclick = function () {
shellSort(nums)
}
参考
- 排序算法参考:十大经典排序算法总结(JavaScript描述)
总结
如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼
作者:Sunshine_Lin
来源:https://juejin.cn/post/7004454008634998821
JavaScript复制内容到剪贴板
最近一个活动页面中有一个小需求,用户点击或者长按就可以复制内容到剪贴板,记录一下实现过程和遇到的坑。
常见方法
查了一下万能的Google,现在常见的方法主要是以下两种:
- 第三方库:clipboard.js
- 原生方法:document.execCommand()
分别来看看这两种方法是如何使用的。
clipboard.js
这是clipboard的官网:clipboardjs.com/,看起来就是这么的简单。
引用
直接引用: <script src="dist/clipboard.min.js"></script>
包: npm install clipboard --save
,然后 import Clipboard from 'clipboard';
使用
从输入框复制
现在页面上有一个 <input>
标签,我们需要复制其中的内容,我们可以这样做:
<input id="demoInput" value="hello world">
<button class="btn" data-clipboard-target="#demoInput">点我复制</button>
import Clipboard from 'clipboard';
const btnCopy = new Clipboard('btn');
注意到,在 <button>
标签中添加了一个 data-clipboard-target
属性,它的值是需要复制的 <input>
的 id
,顾名思义是从整个标签中复制内容。
直接复制
有的时候,我们并不希望从 <input>
中复制内容,仅仅是直接从变量中取值。如果在 Vue
中我们可以这样做:
<button class="btn" :data-clipboard-text="copyValue">点我复制</button>
import Clipboard from 'clipboard';
const btnCopy = new Clipboard('btn');
this.copyValue = 'hello world';
事件
有的时候我们需要在复制后做一些事情,这时候就需要回调函数的支持。
在处理函数中加入以下代码:
// 复制成功后执行的回调函数
clipboard.on('success', function(e) {
console.info('Action:', e.action); // 动作名称,比如:Action: copy
console.info('Text:', e.text); // 内容,比如:Text:hello word
console.info('Trigger:', e.trigger); // 触发元素:比如:<button :data-clipboard-text="copyValue">点我复制</button>
e.clearSelection(); // 清除选中内容
});
// 复制失败后执行的回调函数
clipboard.on('error', function(e) {
console.error('Action:', e.action);
console.error('Trigger:', e.trigger);
});
小结
文档中还提到,如果在单页面中使用 clipboard
,为了使得生命周期管理更加的优雅,在使用完之后记得 btn.destroy()
销毁一下。
clipboard
使用起来是不是很简单。但是,就为了一个 copy
功能就使用额外的第三方库是不是不够优雅,这时候该怎么办?那就用原生方法实现呗。
document.execCommand()方法
先看看这个方法在 MDN
上是怎么定义的:
which allows one to run commands to manipulate the contents of the editable region.
意思就是可以允许运行命令来操作可编辑区域的内容,注意,是可编辑区域。
定义
bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
方法返回一个 Boolean
值,表示操作是否成功。
aCommandName
:表示命令名称,比如:copy
,cut
等(更多命令见命令);aShowDefaultUI
:是否展示用户界面,一般情况下都是false
;aValueArgument
:有些命令需要额外的参数,一般用不到;
兼容性
这个方法在之前的兼容性其实是不太好的,但是好在现在已经基本兼容所有主流浏览器了,在移动端也可以使用。
使用
从输入框复制
现在页面上有一个 <input>
标签,我们想要复制其中的内容,我们可以这样做:
<input id="demoInput" value="hello world">
<button id="btn">点我复制</button>
const btn = document.querySelector('#btn');
btn.addEventListener('click', () => {
const input = document.querySelector('#demoInput');
input.select();
if (document.execCommand('copy')) {
document.execCommand('copy');
console.log('复制成功');
}
})
其它地方复制
有的时候页面上并没有 <input>
标签,我们可能需要从一个 <div>
中复制内容,或者直接复制变量。
还记得在 execCommand()
方法的定义中提到,它只能操作可编辑区域,也就是意味着除了 <input>
、<textarea>
这样的输入域以外,是无法使用这个方法的。
这时候我们需要曲线救国。
<button id="btn">点我复制</button>
const btn = document.querySelector('#btn');
btn.addEventListener('click',() => {
const input = document.createElement('input');
document.body.appendChild(input);
input.setAttribute('value', '听说你想复制我');
input.select();
if (document.execCommand('copy')) {
document.execCommand('copy');
console.log('复制成功');
}
document.body.removeChild(input);
})
算是曲线救国成功了吧。在使用这个方法时,遇到了几个坑。
遇到的坑
在Chrome下调试的时候,这个方法时完美运行的。然后到了移动端调试的时候,坑就出来了。
对,没错,就是你,ios。。。
点击复制时屏幕下方会出现白屏抖动,仔细看是拉起键盘又瞬间收起
知道了抖动是由于什么产生的就比较好解决了。既然是拉起键盘,那就是聚焦到了输入域,那只要让输入域不可输入就好了,在代码中添加
input.setAttribute('readonly', 'readonly');
使这个<input>
是只读的,就不会拉起键盘了。
无法复制
这个问题是由于
input.select()
在ios下并没有选中全部内容,我们需要使用另一个方法来选中内容,这个方法就是input.setSelectionRange(0, input.value.length);
。
完整代码如下:
const btn = document.querySelector('#btn');
btn.addEventListener('click',() => {
const input = document.createElement('input');
input.setAttribute('readonly', 'readonly');
input.setAttribute('value', 'hello world');
document.body.appendChild(input);
input.setSelectionRange(0, 9999);
if (document.execCommand('copy')) {
document.execCommand('copy');
console.log('复制成功');
}
document.body.removeChild(input);
})
作者:axuebin
链接:https://juejin.cn/post/6844903567480848391
收起阅读 »
前端vue面霸修炼手册!!
一、对MVVM的理解
MVVM全称是Model-View-ViewModel
Model 代表数据模型,数据和业务逻辑都在Model层中定义;泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。View 代表UI视图,负责数据的展示;视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;
Vue是以数据为驱动的,Vue自身将DOM和数据进行绑定,一旦创建绑定,DOM和数据将保持同步,每当数据发生变化,DOM会跟着变化。 ViewModel是Vue的核心,它是Vue的一个实例。Vue实例时作用域某个HTML元素上的这个HTML元素可以是body,也可以是某个id所指代的元素。
二、vue常见指令
v-textv-text 主要用来更新 textContent,可以等同于 JS 的 text 属性。
<span v-text="name"></span>
或
<span插值表达式{{name}}</span>
v-html等同于 JS 的 innerHtml 属性
<div v-html="content"></div>
v-cloak用来保持在元素上直到关联实例结束时进行编译 解决闪烁问题
<div id="app" v-cloak>
<div>
{{msg}}
</div>
</div>
<script type="text/javascript">
new Vue({
el:'#app',
data:{
msg:'hello world'
}
})
</script>
正常在页面加载时会闪烁,先显示:
<div>
{{msg}}
</div>
编译后才显示:
<div>
hello world!
</div>
可以用 v-cloak 指令解决插值表达式闪烁问题,v-cloak 在 css 中用属性选择器设置为 display: none;
v-oncev-once 关联的实例,只会渲染一次。之后的重新渲染,实例极其所有的子节点将被视为静态内容跳过,这可以用于优化更新性能
<span v-once>This will never change:{{msg}}</span> //单个元素
<div v-once>//有子元素
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<my-component v-once:comment="msg"></my-component> //组件
<ul>
<li v-for="i in list">{{i}}</li>
</ul>
上面的例子中,msg,list 即使产生改变,也不会重新渲染。
v-ifv-if 可以实现条件渲染,Vue 会根据表达式的值的真假条件来渲染元素
<a v-if="true">show</a>
v-elsev-else 是搭配 v-if 使用的,它必须紧跟在 v-if 或者 v-else-if 后面,否则不起作用
<a v-if="true">show</a>
<a v-else>hide</a>
v-else-ifv-else-if 充当 v-if 的 else-if 块, 可以链式的使用多次。可以更加方便的实现 switch 语句。
<div v-if="type==='A'">
A
</div>
<div v-else-if="type==='B'">
B
</div>
<div v-else-if="type==='C'">
C
</div>
<div v-else>
Not A,B,C
</div>
v-show也是用于根据条件展示元素。和 v-if 不同的是,如果 v-if 的值是 false,则这个元素被销毁,不在 dom 中。但是 v-show 的元素会始终被渲染并保存在 dom 中,它只是简单的切换 css 的 dispaly 属性。
<span v-show="true">hello world</span >
注意:v-if 有更高的切换开销 v-show 有更高的初始渲染开销。因此,如果要非常频繁的切换, 则使用 v-show 较好;如果在运行时条件不太可能改变,则 v-if 较好
v-for用 v-for 指令根据遍历数组来进行渲染
<div v-for="(item,index) in items"></div> //使用in,index是一个可选参数,表示当前项的索引
v-bindv-bind 用来动态的绑定一个或者多个特性。没有参数时,可以绑定到一个包含键值对的对象。常用于动态绑定 class 和 style。以及 href 等。简写为一个冒号【 :】
<div id="app">
<div :class="{'is-active':isActive, 'text-danger':hasError}"></div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
isActive: true,
hasError: false
}
})
</script>
编译后
<div class = "is-active"></div>
v-model用于在表单上创建双向数据绑定
<div id="app">
<input v-model="name">
<p>hello {{name}}</p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
name:'小明'
}
})
</script>
model 修饰符有
.lazy(在 change 事件再同步) > v-model.lazy .number(自动将用户的输入值转化为数值类型) > v-model.number .trim(自动过滤用户输入的首尾空格) > v-model.trim
v-onv-on 主要用来监听 dom 事件,以便执行一些代码块。表达式可以是一个方法名。 简写为:【 @ 】
<div id="app">
<button @click="consoleLog"></button>
</div>
<script>
var app = new Vue({
el: '#app',
methods:{
consoleLog:function (event) {
console.log(1)
}
}
})
</script>
事件修饰符
.stop 阻止事件继续传播 .prevent 事件不再重载页面 .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理 .self 只当在 event.target 是当前元素自身时触发处理函数 .once 事件将只会触发一次 .passive 告诉浏览器你不想阻止事件的默认行为
三 、v-if 和 v-show 有什么区别?
共同点:v-if 和 v-show 都能实现元素的显示隐藏
区别:
v-show 只是简单的控制元素的 display 属性 而 v-if 才是条件渲染(条件为真,元素将会被渲染,条件为假,元素会被销毁) 2. v-show 有更高的首次渲染开销,而 v-if 的首次渲染开销要小的多 3. v-if 有更高的切换开销,v-show 切换开销小 4. v-if 有配套的 v-else-if 和 v-else,而 v-show 没有 5. v-if 可以搭配 template 使用,而 v-show 不行
四、如何让CSS只在当前组件中起作用?
将组件样式加上 scoped
<style scoped>
...
</style>
五、 keep-alive的作用是什么?
keep-alive包裹动态组件时,会缓存不活动的组件实例, 主要用于保留组件状态或避免重新渲染。
六、在Vue中使用插件的步骤
采用ES6的 import … from …
语法 或 CommonJSd的 require()
方法引入插件 2、使用全局方法 Vue.use( plugin )
使用插件,可以传入一个选项对象 Vue.use(MyPlugin, { someOption: true })
七、Vue 生命周期
八、Vue 组件间通信有哪几种方式
Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信
九、computed 和 watch 的区别和运用的场景
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作
运用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch
十、vue-router 路由模式有几种
Hash: 使用 URL 的 hash 值来作为路由。支持所有浏览器。
History: 以来 HTML5 History API 和服务器配置。参考官网中 HTML5 History 模式
Abstract: 支持所有 javascript 运行模式。如果发现没有浏览器的 API,路由会自动强制进入这个模式。
十一、SPA 单页面的理解,它的优缺点分别是什么
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS 一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转 取而代之的是利用路由机制实现 HTML 内容的变换, UI 与用户的交互,避免页面的重新加载。
优点:
1、用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
2、基于上面一点,SPA 相对对服务器压力小
3、前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理
缺点:
1、初次加载耗时多:为实现单页 Web 应用功能及显示效果, 需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载
2、前进后退路由管理:由于单页应用在一个页面中显示所有的内容, 所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理
3、SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势
不想加班,你就背会这 10 条 JS 技巧
为了让自己写的代码更优雅且高效,特意向大佬请教了这 10 条 JS 技巧
1. 数组分割
const listChunk = (list = [], chunkSize = 1) => {
const result = [];
const tmp = [...list];
if (!Array.isArray(list) || !Number.isInteger(chunkSize) || chunkSize <= 0) {
return result;
};
while (tmp.length) {
result.push(tmp.splice(0, chunkSize));
};
return result;
};
listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
// [['a'], ['b'], ['c'], ['d'], ['e'], ['f'], ['g']]
listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 3);
// [['a', 'b', 'c'], ['d', 'e', 'f'], ['g']]
listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], 0);
// []
listChunk(['a', 'b', 'c', 'd', 'e', 'f', 'g'], -1);
// []
2. 求数组元素交集
const listIntersection = (firstList, ...args) => {
if (!Array.isArray(firstList) || !args.length) {
return firstList;
}
return firstList.filter(item => args.every(list => list.includes(item)));
};
listIntersection([1, 2], [3, 4]);
// []
listIntersection([2, 2], [3, 4]);
// []
listIntersection([3, 2], [3, 4]);
// [3]
listIntersection([3, 4], [3, 4]);
// [3, 4]
3. 按下标重新组合数组
const zip = (firstList, ...args) => {
if (!Array.isArray(firstList) || !args.length) {
return firstList
};
return firstList.map((value, index) => {
const newArgs = args.map(arg => arg[index]).filter(arg => arg !== undefined);
const newList = [value, ...newArgs];
return newList;
});
};
zip(['a', 'b'], [1, 2], [true, false]);
// [['a', 1, true], ['b', 2, false]]
zip(['a', 'b', 'c'], [1, 2], [true, false]);
// [['a', 1, true], ['b', 2, false], ['c']]
4. 按下标组合数组为对象
const zipObject = (keys, values = {}) => {
const emptyObject = Object.create({});
if (!Array.isArray(keys)) {
return emptyObject;
};
return keys.reduce((acc, cur, index) => {
acc[cur] = values[index];
return acc;
}, emptyObject);
};
zipObject(['a', 'b'], [1, 2])
// { a: 1, b: 2 }
zipObject(['a', 'b'])
// { a: undefined, b: undefined }
5. 检查对象属性的值
const checkValue = (obj = {}, objRule = {}) => {
const isObject = obj => {
return Object.prototype.toString.call(obj) === '[object Object]';
};
if (!isObject(obj) || !isObject(objRule)) {
return false;
}
return Object.keys(objRule).every(key => objRule[key](obj[key]));
};
const object = { a: 1, b: 2 };
checkValue(object, {
b: n => n > 1,
})
// true
checkValue(object, {
b: n => n > 2,
})
// false
6. 获取对象属性
const get = (obj, path, defaultValue) => {
if (!path) {
return;
};
const pathGroup = Array.isArray(path) ? path : path.match(/([^[.\]])+/g);
return pathGroup.reduce((prevObj, curKey) => prevObj && prevObj[curKey], obj) || defaultValue;
};
const obj1 = { a: { b: 2 } }
const obj2 = { a: [{ bar: { c: 3 } }] }
get(obj1, 'a.b')
// 2
get(obj2, 'a[0].bar.c')
// 3
get(obj2, ['a', '0', 'bar', 'c'])
// 2
get(obj1, 'a.bar.c', 'default')
// default
get(obj1, 'a.bar.c', 'default')
// default
7. 将特殊符号转成字体符号
const escape = str => {
const isString = str => {
return Object.prototype.toString.call(str) === '[string Object]';
};
if (!isString(str)) {
return str;
}
return (str.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\//g, '/')
.replace(/\\/g, '\')
.replace(/`/g, '`'));
};
8. 利用注释创建一个事件监听器
class EventEmitter {
#eventTarget;
constructor(content = '') {
const comment = document.createComment(content);
document.documentElement.appendChild(comment);
this.#eventTarget = comment;
}
on(type, listener) {
this.#eventTarget.addEventListener(type, listener);
}
off(type, listener) {
this.#eventTarget.removeEventListener(type, listener);
}
once(type, listener) {
this.#eventTarget.addEventListener(type, listener, { once: true });
}
emit(type, detail) {
const dispatchEvent = new CustomEvent(type, { detail });
this.#eventTarget.dispatchEvent(dispatchEvent);
}
};
const emmiter = new EventEmitter();
emmiter.on('biy', () => {
console.log('hello world');
});
emmiter.emit('biu');
// hello world
9. 生成随机的字符串
const genRandomStr = (len = 1) => {
let result = '';
for (let i = 0; i < len; ++i) {
result += Math.random().toString(36).substr(2)
}
return result.substr(0, len);
}
genRandomStr(3)
// u2d
genRandomStr()
// y
genRandomStr(10)
// qdueun65jb
10. 判断是否是指定的哈希值
const isHash = (type = '', str = '') => {
const isString = str => {
return Object.prototype.toString.call(str) === '[string Object]';
};
if (!isString(type) || !isString(str)) {
return str;
};
const algorithms = {
md5: 32,
md4: 32,
sha1: 40,
sha256: 64,
sha384: 96,
sha512: 128,
ripemd128: 32,
ripemd160: 40,
tiger128: 32,
tiger160: 40,
tiger192: 48,
crc32: 8,
crc32b: 8,
};
const hash = new RegExp(`^[a-fA-F0-9]{${algorithms[type]}}$`);
return hash.test(str);
};
isHash('md5', 'd94f3f016ae679c3008de268209132f2');
// true
isHash('md5', 'q94375dj93458w34');
// false
isHash('sha1', '3ca25ae354e192b26879f651a51d92aa8a34d8d3');
// true
isHash('sha1', 'KYT0bf1c35032a71a14c2f719e5a14c1');
// false
后记
如果你喜欢探讨技术,或者对本文有任何的意见或建议,非常欢迎加鱼头微信好友一起探讨,当然,鱼头也非常希望能跟你一起聊生活,聊爱好,谈天说地。
全文完
作者:酸菜鱼+黄焖鸡
来源:https://blog.51cto.com/u_15291238/4538068
收起阅读 »尤大亲自解释vue3源码中为什么不使用?.可选链式操作符?
阅读本文🦀
1.什么是可选链式操作符号
2.为什么vue3源码中不使用可选链式操作符
什么是可选链式操作符号❓
可选链操作符( ?.
)允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?.
操作符的功能类似于 .
链式操作符,不同之处在于,在引用为空(nullish ) (null
或者 undefined
) 的情况下不会引起错误,该表达式短路返回值是 undefined
。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined
。
当尝试访问可能不存在的对象属性时,可选链操作符将会使表达式更短、更简明。在探索一个对象的内容时,如果不能确定哪些属性必定存在,可选链操作符也是很有帮助的。
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah'
}
};
const dogName = adventurer.dog?.name;
console.log(dogName);
// expected output: undefined
console.log(adventurer.someNonExistentMethod?.());
// expected output: undefined
短路效应
如果 ?.
左边部分不存在,就会立即停止运算(“短路效应”)。
所以,如果后面有任何函数调用或者副作用,它们均不会执行。
let user = null;
let x = 0;
user?.sayHi(x++);
// 没有 "sayHi",因此代码执行没有触达 x++ alert(x); // 0,值没有增加
Vue3源码中为什么不采用这么方便的操作符
看看这样是不是代码更简洁了,但是为什么这个PR没有被合并呢
来自尤大的亲自解释
(我们有意避免在代码库中使用可选链,因为我们的目标是 ES2016,而 TS 会将其转换为更加冗长的内容)
从尤大的话中我们可以得知由于Vu3打包后的代码是基于ES2016的,虽然我们在编写代码时看起来代码比较简洁了,实际打包之后反而更冗余了,这样会增大包的体积,影响Vu3的加载速度。由此可见一个优秀的前端框架真的要考虑的东西很多,语法也会考虑周到~✨
作者:速冻鱼
链接:https://juejin.cn/post/7033167068895641637
收起阅读 »
想知道一个20k级别前端在项目中是怎么使用LocalStorage的吗?
前言
大家好,我是林三心,用最通俗的话,讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天就给大家唠一下嗑,讲一下,怎么样使用localStorage、sessionStorage
,才能更规范,更高大上,更能让人眼前一亮。
用处
在平时的开发中,localStorage、sessionStorage
的用途是非常的多的,在我们的开发中发挥着非常重要的作用:
- 1、登录完成后
token
的存储 - 2、用户部分信息的存储,比如
昵称、头像、简介
- 3、一些项目通用参数的存储,例如
某个id、某个参数params
- 4、项目状态管理的持久化,例如
vuex的持久化、redux的持久化
- 5、项目整体的切换状态存储,例如
主题颜色、icon风格、语言标识
- 6、等等、、、、、、、、、、、、、、、、、、、、、、、、、、
普通使用
那么,相信我们各位平时使用都是这样的(拿localStorage
举例)
1、基础变量
// 当我们存基本变量时
localStorage.setItem('基本变量', '这是一个基本变量')
// 当我们取值时
localStorage.getItem('基本变量')
// 当我们删除时
localStorage.removeItem('基本变量')
2、引用变量
// 当我们存引用变量时
localStorage.setItem('引用变量', JSON.stringify(data))
// 当我们取值时
const data = JSON.parse(localStorage.getItem('引用变量'))
// 当我们删除时
localStorage.removeItem('引用变量')
3、清空
localStorage.clear()
暴露出什么问题?
1、命名过于简单
- 1、比如我们存用户信息会使用
user
作为 key 来存储 - 2、存储主题的时候用
theme
作为 key 来存储 - 3、存储令牌时使用
token
作为 key 来存储
其实这是很有问题的,咱们都知道,同源的两个项目,它们的localStorage
是互通的。
我举个例子吧比如我现在有两个项目,它们在同源https://www.sunshine.com
下,这两个项目都需要往localStorage
中存储一个 key 为name
的值,那么这就会造成两个项目的name
互相顶替的现象,也就是互相污染现象
:
2、时效性
咱们都知道localStorage、sessionStorage
这两个的生命周期分别是
- localStorage:除非手动清除,否则一直存在
- sessionStorage:生命结束于当前标签页的关闭或浏览器的关闭
其实平时普通的使用时没什么问题的,但是给某些指定缓存加上特定的时效性,是非常重要的!比如某一天:
- 后端:”兄弟,你一登录我就把token给你“
- 前端:”好呀,那你应该会顺便判断token过期没吧?“
- 后端:”不行哦,放在你前端判断过期呗“
- 前端:”行吧。。。。。“
那这时候,因为需要在前端判断过期,所以咱们就得给token
设置一个时效性,或者是1天
,或者是7天
3、隐秘性
其实这个好理解,你们想想,当咱们把咱们想缓存的东西,存在localStorage、sessionStorage
中,在开发过程中,确实有利于咱们的开发,咱们想看的时候也是一目了然,点击Application
就可以看到。
但是,一旦产品上线了,用户也是可以看到缓存中的东西的,而咱们肯定是会想:有些东西可以让用户看到,但是有些东西我不想让你看到
或者咱们在做状态管理持久化
时,需要把数据先存在localStorage
中,这个时候就很有必要对缓存进行加密了。
解决方案
1、命名规范
我个人的看法是项目名 + 当前环境 + 项目版本 + 缓存key
,如果大家有其他规则的,可以评论区告诉林三心,让林三心学学
2、expire定时
思路:设置缓存key
时,将value
包装成一个对象,对象中有相应的时效时段
,当下一次想获取缓存值时,判断有无超时,不超时就获取value
,超时就删除这个缓存
3、crypto加密
加密很简单,直接使用crypto-js
进行对数据的加密,使用这个库里的encrypt、decrypyt
进行加密、解密
实践
其实实践的话比较简单啦,无非就是四步
- 1、与团队商讨一下
key
的格式 - 2、与团队商讨一下
expire
的长短 - 3、与团队商讨一下使用哪个库来对缓存进行加密(个人建议
crypto-js
) - 4、代码实施(不难,我这里就不写了)
结语
有人可能觉得没必要,但是严格要求自己其实是很有必要的,平时严格要求自己,才能做到每到一个公司都能更好的做到向下兼容难度。
如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼,我会定时直播模拟面试,简历指导,答疑解惑
作者:Sunshine_Lin
链接:https://juejin.cn/post/7033749571939336228
收起阅读 »
巧用渐变实现高级感拉满的背景光动画
实现
这个效果想利用 CSS 完全复制是比较困难的。CSS 模拟出来的光效阴影相对会 Low 一点,只能说是尽量还原。
其实每组光都基本是一样的,所以我们只需要实现其中一组,就几乎能实现了整个效果。
观察这个效果:
它的核心其实就是角向渐变 -- conic-gradient()
,利用角向渐变,我们可以大致实现这样一个效果:
<div></div>
div {
width: 1000px;
height: 600px;
background:
conic-gradient(
from -45deg at 400px 300px,
hsla(170deg, 100%, 70%, .7),
transparent 50%,
transparent),
linear-gradient(-45deg, #060d5e, #002268);
}
看看效果:
有点那意思了。当然,仔细观察,渐变的颜色并非是由一种颜色到透明就结束了,而是颜色 A -- 透明 -- 颜色 B,这样,光源的另一半并非就不会那么生硬,改造后的 CSS 代码:
div {
width: 1000px;
height: 600px;
background:
conic-gradient(
from -45deg at 400px 300px,
hsla(170deg, 100%, 70%, .7),
transparent 50%,
hsla(219deg, 90%, 80%, .5) 100%),
linear-gradient(-45deg, #060d5e, #002268);
}
我们在角向渐变的最后多加了一种颜色,得到观感更好的一种效果:
emm,到这里,我们会发现,仅仅是角向渐变 conic-gradient()
是不够的,它无法模拟出光源阴影的效果,所以必须再借助其他属性实现光源阴影的效果。
这里,我们会很自然的想到 box-shadow
。这里有个技巧,利用多重 box-shadow
, 实现 Neon 灯的效果。
我们再加个 div,通过它实现光源阴影:
<div class="shadow"></div>
.shadow {
width: 200px;
height: 200px;
background: #fff;
box-shadow:
0px 0 .5px hsla(170deg, 95%, 80%, 1),
0px 0 1px hsla(170deg, 91%, 80%, .95),
0px 0 2px hsla(171deg, 91%, 80%, .95),
0px 0 3px hsla(171deg, 91%, 80%, .95),
0px 0 4px hsla(171deg, 91%, 82%, .9),
0px 0 5px hsla(172deg, 91%, 82%, .9),
0px 0 10px hsla(173deg, 91%, 84%, .9),
0px 0 20px hsla(174deg, 91%, 86%, .85),
0px 0 40px hsla(175deg, 91%, 86%, .85),
0px 0 60px hsla(175deg, 91%, 86%, .85);
}
OK,光是有了,但问题是我们只需要一侧的光,怎么办呢?裁剪的方式很多,这里,我介绍一种利用 clip-path
进行对元素任意空间进行裁切的方法:
.shadow {
width: 200px;
height: 200px;
background: #fff;
box-shadow: .....;
clip-path: polygon(-100% 100%, 200% 100%, 200% 500%, -100% 500%);
}
原理是这样的:
这样,我们就得到了一侧的光:
这里,其实 CSS 也是有办法实现单侧阴影的(你所不知道的 CSS 阴影技巧与细节),但是实际效果并不好,最终采取了上述的方案。
接下来,就是利用定位、旋转等方式,将上述单侧光和角向渐变重叠起来,我们就可以得到这样的效果:
这会,已经挺像了。接下来要做的就是让整个图案,动起来。这里技巧也挺多的,核心还是利用了 CSS @Property,实现了角向渐变的动画,并且让光动画和角向渐变重叠起来。
我们需要利用 CSS @Property 对代码渐变进行改造,核心代码如下:
<div class="wrap">
<div class="shadow"></div>
</div>
@property --xPoint {
syntax: '<length>';
inherits: false;
initial-value: 400px;
}
@property --yPoint {
syntax: '<length>';
inherits: false;
initial-value: 300px;
}
.wrap {
position: relative;
margin: auto;
width: 1000px;
height: 600px;
background:
conic-gradient(
from -45deg at var(--xPoint) var(--yPoint),
hsla(170deg, 100%, 70%, .7),
transparent 50%,
hsla(219deg, 90%, 80%, .5) 100%),
linear-gradient(-45deg, #060d5e, #002268);
animation: pointMove 2.5s infinite alternate linear;
}
.shadow {
position: absolute;
top: -300px;
left: -330px;
width: 430px;
height: 300px;
background: #fff;
transform-origin: 100% 100%;
transform: rotate(225deg);
clip-path: polygon(-100% 100%, 200% 100%, 200% 500%, -100% 500%);
box-shadow: ... 此处省略大量阴影代码;
animation: scale 2.5s infinite alternate linear;
}
@keyframes scale {
50%,
100% {
transform: rotate(225deg) scale(0);
}
}
@keyframes pointMove {
100% {
--xPoint: 100px;
--yPoint: 0;
}
}
这样,我们就实现了完整的一处光的动画:
我们重新梳理一下,实现这样一个动画的步骤:
- 利用角向渐变
conic-gradient
搭出基本框架,并且,这里也利用了多重渐变,角向渐变的背后是深色背景色; - 利用多重
box-shadow
实现光及阴影的效果(又称为 Neon 效果) - 利用
clip-path
对元素进行任意区域的裁剪 - 利用 CSS @Property 实现渐变的动画效果
剩下的工作,就是重复上述的步骤,补充其他渐变和光源,调试动画,最终,我们就可以得到这样一个简单的模拟效果:
由于原效果是 .mp4
,无法拿到其中的准确颜色,无法拿到阴影的参数,其中颜色是直接用的色板取色,阴影则比较随意的模拟了下,如果有源文件,准确参数,可以模拟的更逼真。
完整的代码你可以戳这里:CodePen -- iPhone 13 Pro Gradient
作者:chokcoco
链接:https://juejin.cn/post/7033952765151805453
收起阅读 »
vite对浏览器的请求做了什么
工作原理:
type="module"
浏览器中ES Module原生native支持。 如果浏览器支持type="module"
,我i们可以使用es6模块化的方式编写。浏览器会把我们需要导入的文件再发一次http请求,再发到服务器上。 开发阶段不需要打包- 第三方依赖预打包
- 启动一个开发服务器处理资源请求
type="module"
浏览器中ES Module原生native支持。 如果浏览器支持type="module"
,我i们可以使用es6模块化的方式编写。浏览器会把我们需要导入的文件再发一次http请求,再发到服务器上。 开发阶段不需要打包一图详解vite原理:
浏览器做的什么事啊
宿主文件index.html
<script type="module" src="/src/main.js"></script>
浏览器获取到宿主文件中的资源后,发现还要再去请求main.js文件。会再向服务端发送一次main.js的资源请求。
main.js
在main中,可以发现,浏览器又再次发起对vue.js?v=d253a66c
、App.vue?t=1637479953836
两个文件的资源请求。
服务器会将App.vue中的内容进行编译然后返回给浏览器,下图可以看出logo图片和文字都被编译成_hoisted_
的静态节点。
从请求头中,也可以看出sfc文件已经变成浏览器可以识别的js文件(app.vue文件中要存在script内容才会编译成js)。对于浏览器来说,执行的就是一段js代码。
其他裸模块
如果vue依赖中还存在其他依赖的话,浏览器依旧会再次发起资源请求,获取相应资源。
了解一下预打包
对于第三方依赖(裸模块)的加载,vite对其提前做好打包工作,将其放到node_modules/.vite下。当启动项目的时候,直接从该路径下下载文件。
通过上图,可以看到再裸模块的引入时,路径发生了改变。
服务器做的什么事啊
总结一句话:服务器把特殊后缀名的文件进行处理返回给前端展示。
我们可以模拟vite的devServe,使用koa中间件启动一个本地服务。
// 引入依赖
const Koa = require('koa')
const app = new Koa()
const fs = require('fs')
const path = require('path')
const compilerSfc = require('@vue/compiler-sfc')
const compilerDom = require('@vue/compiler-dom')
app.use(async (ctx) => {
const { url, query } = ctx.request
// 处理请求资源代码都写这
})
zaiz都h这z都he在
app.listen(3001, () => {
console.log('dyVite start!!')
})
请求首页index.html
if (url === '/') {
const p = path.join(__dirname, './index.html') // 绝对路径
// 首页
ctx.type = 'text/html'
ctx.body = fs.readFileSync(p, 'utf8')
}
看到上面这张图,就知道我们的宿主文件已经请求成功了。只是浏览器又给服务端发送的一个main.js文件的请求。这时,我们还需要判断处理一下main.js文件。
请求以.js结尾的文件
我们处理上述情况后,emmmm。。。发现main中还是存在好多其他资源请求。
基础js文件
main文件:
console.log(1)
处理main:
else if (url.endsWith('.js')) {
// 响应js请求
const p = path.join(__dirname, url)
ctx.type = 'text/javascript'
ctx.body = rewriteImport(fs.readFileSync(p, 'utf8')) // 处理依赖函数
}
对main中的依赖进行处理
你以为main里面就一个输出吗?太天真了。这样的还能处理吗?
main文件:
import { createApp, h } from 'vue'
createApp({ render: () => h('div', 'helllo dyVite!') }).mount('#app')
emmm。。。应该可以!
我们可以将main中导入的地址变成相对地址。
在裸模块路径添加上/@modules/
。再去识别/@modules/
的文件即(裸模块文件)。
// 把能读出来的文件地址变成相对地址
// 正则替换 重写导入 变成相对地址
// import { createApp } from 'vue' => import { createApp } from '/@modules/vue'
function rewriteImport(content) {
return content.replace(/ from ['|"](.*)['|"]/g, function (s0, s1) {
// s0匹配字符串,s1分组内容
// 是否是相对路径
if (s1.startsWith('./') || s1.startsWith('/') || s1.startsWith('../')) {
// 直接返回
return s0
} else {
return ` from '/@modules/${s1}'`
}
})
}
对于第三方依赖,vite内部是使用预打包请求自己服务器/node_modules/.vite/
下的内部资源。
我们可以简单化一点,将拿到的依赖名去客户端下的node_modules下拿相应的资源。
else if (url.startsWith('/@modules/')) {
// 裸模块的加载
const moduleName = url.replace('/@modules/', '')
const pre的地址
const module = require(prefix + '/package.json').module
const filePath = path.join(prefix, module) // 拿到文件加载的地址
// 读取相关依赖
const ret = fs.readFileSync(filePath, 'utf8')
ctx.type = 'text/javascript'
ctx.body = rewriteImport(ret) //依赖内部可能还存在依赖,需要递归
}
在main中进行render时,会报下图错误:
我们加载的文件都是服务端执行的库,内部可能会产生node环境的代码,需要判断一下环境变量。如果开发时,会输出一些警告信息,但是在前端是没有的。所以我们需要mock一下,告诉浏览器我们当前的环境。
给html加上process环境变量。
<script>
window.process = { env: { NODE_ENV: 'dev' } }
</script>
此时main文件算是加载出来了。
但是这远远打不到我们的目的啊!
我们需要的是可以编译vue文件的服务器啊!
处理.vue
文件
main.js文件:
import { createApp, h } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
在vue文件中,它是模块化加载的。
我们需要在处理vue文件的时候,对.vue
后面携带的参数做处理。
在此,我们简化只考虑template和sfc情况。
else if (url.indexOf('.vue') > -1) {
// 处理vue文件 App.vue?vue&type=style&index=0&lang.css
// 读取vue内容
const p = path.join(__dirname, url.split('?')[0])
// compilerSfc解析sfc 获得ast
const ret = compilerSfc.parse(fs.readFileSync(p, 'utf8'))
// App.vue?type=template
// 如果请求没有query.type 说明是sfc
if (!query.type) {
// 处理内部的script
const scriptContent = ret.descriptor.script.content
// 将默认导出配置对象转为常量
const script = scriptContent.replace(
'export default ',
'const __script = ',
)
ctx.type = 'text/javascript'
ctx.body = `
${rewriteImport(script)}
// template解析转换为单独请求一个资源
import {render as __render} from '${url}?type=template'
__script.render = __render
export default __script
`
} else if (query.type === 'template') {
const tpl = ret.descriptor.template.content
// 编译包含render模块
const render = compilerDom.compile(tpl, { mode: 'module' }).code
ctx.type = 'text/javascript'
ctx.body = rewriteImport(render)
}
}
处理图片路径
直接从客户端读取返回。
else if (url.endsWith('.png')) {
ctx.body = fs.readFileSync('src' + url)
}
作者:ClyingDeng
链接:https://juejin.cn/post/7033713960784248868
收起阅读 »