注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

抛弃Vue转入React的六个月,我收获了什么?

对不起,我抛弃了Vue,转入React阵营。不因为其它,就因为在我这边使用React的工资比使用Vue的工资高。 在六月前,我硬背了几百道的React面试题,入职一家使用React的公司,薪资增幅120%; 入职就马上进入开发阶段,完全是从零开始,随着时间的推...
继续阅读 »

对不起,我抛弃了Vue,转入React阵营。不因为其它,就因为在我这边使用React的工资比使用Vue的工资高。


在六月前,我硬背了几百道的React面试题,入职一家使用React的公司,薪资增幅120%;


入职就马上进入开发阶段,完全是从零开始,随着时间的推移,发现React入门也不是网传的那么难。难道是自己天生就适合吃这碗饭…………


到今天已经六个月了,在这里想把这段时间的收获跟掘友们分享一下,请掘友们多多指教,一起进步哈。


另外我是用React16.8来入门,故在开发过程中大多使用函数式组件React Hooks来开发。


重磅提醒,文末有抽奖噢。


一、关于函数式组件的收获


函数式组件可以理解为一个能返回React元素的函数,其接收一个代表组件属性的参数props


在React16.8之前,也就是没有React Hooks之前,函数式组件只作为UI组件,其输出完全由参数props控制,没有自身的状态没有业务逻辑代码,是一个纯函数。函数式组件没有实例,没有生命周期,称为无状态组件。


在React Hooks出现后,可以用Hook赋予函数式组件状态和生命周期,于是函数式组件也可以作为业务组件。


开发过程中,类组件和函数式组件都有使用,经过六个月的开发,感觉还是函数式组件比类组件好用一些,感受最深的是以下两点:



  • 不用去学习class,不用去管烦人的this指向问题;

  • 复用性高,很容易就把共同的抽取出来,写出自定义Hook,来替代高阶组件。


函数式组件和类组件之间有一个非常重要的区别:函数式组件捕获了渲染所使用的值


我是遇到一个BUG才知道有这个区别,在解决这个BUG的过程理解了这个区别的含义。


那个BUG的场景是这样的,一个输入框,输入完内容,点击按钮搜索,搜索时先请求一个接口,获取一个类型,再用类型和输入框值去请求搜索接口。用代码简单描述一下。


import React, { Component } from "react";
import * as API from 'api/list';
class SearchComponent extends Component {
constructor() {
super();
this.state = {
inpValue: ""
};
}

getType () {
const param = {
val:this.state.inpValue
}
return API.getType(param);
}

getList(type){
const param = {
val:this.state.inpValue,
type,
}
return API.getList(param);
}

async handleSearch() {
const res = await this.getType();
const type = res?.data?.type;
const res1 = await this.getList(type);
console.log(res1);
}

render() {
return (
<div>
<input
type="text"
value={this.state.inpValue}
onChange={(e) => {
this.setState({ inpValue: e.target.value });
}}
/>
<button
onClick={() => {
this.handleSearch();
}}
>
搜索
</button>
</div>
);
}
}
export default SearchComponent;

以上代码逻辑看上去都没什么毛病,但是QA给我挑了一个BUG,在输入框输入要搜索的内容后,点击搜索按钮开始搜索,然后很快在输入框中又输入内容,结果搜索接口getList报错。查一下原因,发现是获取类型接口getType和搜索接口getList接受的参数val不一致。


在排查过程中,我非常纳闷,明明两次请求中val都是读取this.state.inpValue的值。当时同事指导我改成函数式组件就可解决这个BUG。


import React, { useState } from "react";
import * as API from 'api/list';
export const SearchComponent = () =>{
const [inpValue,setInpValue] = useState('');

const getType = () =>{
const param = {
val:inpValue
}
return API.getType(param);
}

const getList = (type) =>{
const param = {
val:inpValue,
type:type,
}
return API.getList(param);
}

const handleSearch = async ()=>{
const res = await getType();
const type = res?.data?.type;
const res1 = await getList(type);
console.log(res1);
}

return (
<div>
<input
type="text"
value={inpValue}
onChange={(e) => {
setInpValue(e.target.value);
}}
/>
<button
onClick={() => {
handleSearch();
}}
>
搜索
</button>
</div>
);
}
export default SearchComponent;

改成函数式组件后,再试一下,不报错了,BUG修复了。后面我查阅资料后才知道在函数式组件中的事件的state和props所获取的值是事件触发那一刻页面渲染所用的state和props的值。当点击搜索按钮后,val的值就是那一刻输入框中的值,无论输入框后面的值在怎么改变,不会捕获最新的值。


那为啥类组件中,能获取到最新的state值呢?关键在于类组件中是通过this去获取state的,而this永远是最新的组件实例。


其实在类组件中改一下,也可以解决这个BUG,改动的地方如下所示:


getType (val) {
const param = {
val,
}
return API.getType(param);
}
getList(val,type){
const param = {
val,
type,
}
return API.getList(param);
}
async handleSearch() {
const inpValue = this.state.inpValue;
const res = await this.getType(inpValue);
const type = res?.data?.type;
const res1 = await this.getList(inpValue,type);
console.log(res1);
}

在搜索事件handleSearch触发时,就把输入框的值this.state.inpValue存在inpValue变量中,后续执行的事件用到输入框的值都去inpValue变量取,后续再往输入框输入内容也不会影响到inpValue变量的值,除非再次触发搜索事件handleSearch。这样修改也可以解决这个BUG。


二、关于受控和非受控组件的收获


在React中正确理解受控和非受控组件的概念和作用至关重要,因为太多地方用到了



  • 受控组件


在Vue中有v-model指令可以很轻松把组件和数据关联起来,而在React中没有这种指令,那怎么让组件和数据联系起来,这时候就要用到受控组件的概念。


受控组件,我理解为组件的状态由数据来控制,改变这个数据的方法却不是组件的,这里所说的组件不仅仅是组件,也可以表示一个原生DOM。比如一个input输入框。


input输入框的状态(输入框的值)由数据value控制,改变数据value的方法setValue不是input输入框自身的。


import React, { useState } from "react";
const Input = () =>{
const [value,setValue] = useState('');
return (
<div>
<input
type="text"
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
</div>
);
}
export default Input;

再比如在Ant Design UI中的Form组件自定义表单控件时,要求自定义表单控件接受属性valueonChange,其中value来作为自定义表单控件的值,onChange事件来改变属性value


import React from 'react';
import { Form, Input } from 'antd';

const MyInput = (props) => {
const { value, onChange } = props;

const onNameChange = (e) => {
onChange?.(e.target.value)
}

return (
<Input
type="text"
value={value || ''}
onChange={onNameChange}
/>
)
}
const MyForm = () => {
const onValuesChange = (values) => {
console.log(values);
};

return (
<Form
name="demoFrom"
layout="inline"
onValuesChange={onValuesChange}
>
<Form.Item name="name">
<MyInput />
</Form.Item>
</Form>
)
}

export default MyForm;

我认为受控组件最大的作用是在第三方组件的状态经常莫名奇妙的改变时,用一个父组件将其包裹起来,传入一个要控制的状态和改变状态方法的props,把组件的状态提到父组件来控制,这样当组件的状态改变时就很清晰的知道哪个地方改变了组件的状态。



  • 非受控组件


非受控组件就是组件自身状态完全由自身来控制,是相对某个组件而言,比如input输入框受Input组件控制,而Input组件不受Demo组件控制,那么Input相对Demo组件是非受控组件。


import React, { useState } from "react";
const Input = () =>{
const [value,setValue] = useState('');
return (
<div>
<input
type="text"
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
</div>
);
}
const Demo = () =>{
return (
<Input/>
)
}
export default Demo;

也可以把非受控组件理解为组件的值只能由用户设置,而不能通过代码控制。此外要记住一个非常特殊的DOM元素<input type="file" />,其无法通过代码设置所上传的文件。


三、关于useState的收获


useState可谓是使用频率最高的一个Hook,下面分享一下使用的3个心得。




  • useState可以接收一个函数作为参数,把这个函数称为state初始化函数


    一般场景下useState只要传入一个值就可以作为这个state的初始值,但是如果这个值要通过一定计算才能得出的呢?那么此时就可以传入一个函数,在函数中计算完成后,返回一个初始值。


    import React, { useState } from 'react';
    export default (props) => {
    const { a } = props;
    const [b] = useState(() => {
    return a + 1;
    })
    return (
    <div>{b}</div>
    )
    };



  • state更新如何使用旧的state


    刚开始时,我这么使用的


    const [a , setA] = useState(1);
    const changeA = () =>{
    setA(a + 1);
    }

    后来遇到一个错误,代码如下


    const [a , setA] = useState(1);
    const changeA = () =>{
    setA(a + 1);
    setA(a + 1);
    }

    在函数中连续调用两次setA会发现a还是等于2,并不是等于3。正确要这么调用


    const [a , setA] = useState(1);
    const changeA = () =>{
    setA( a => a+1 );
    setA( a => a+1 );
    }



  • 如何拆分state


    在函数式组件中,只要一个state改变都会引起组件的重新渲染。而在React中只要父组件重新渲染,其子组件都会被重新渲染。


    那么问题就来了,如果把state拆分成多个,当依次改变这些state,则会多次触发组件重新渲染。


    若不拆分state,改变state时,就只会触发一次组件重新渲染。但是要注意,函数式组件不像类组件那样,改变其中state中的一个数据,会自动更新state中对应的数据。要这么处理


    const [data,setData] = useState({a:1,b:2,c:3});
    const changeData = () =>{
    setData(data =>{
    ...data,
    a:2,
    })
    }



当然也不能不拆分state,这样代码复用性会大大降低。我的经验是:




  • 将完全不相关的state拆分成一个个




  • 如果有些state是相互关联的,或是需要一起改变,那么将其合并为一个state




四、关于useMemo和usecallback的收获



  • 对其定义的理解


useCallback(fn,[a, b]);
useMemo(fn, [a, b]);

如上所示,useMemousecallback参数中fn是一个函数,参数中[a,b]ab可以是state或props。


useMemousecallback首次执行时,执行fn后创建一个缓存并返回这个缓存,监听[a,b]中的表示state或props的ab,若值未发生变化直接返回缓存,若值发生变化则重新执行fn再创建一个缓存并返回这个缓存。


useMemousecallback都会返回一个缓存,但是这个缓存各不相同,useMemo返回一个值,这个值可以认为是执行fn返回的。usecallback返回一个函数,这个函数可以认为是fn那么注意了传给useMemofn必须返回一个值



  • 结合React.memo来使用


React.memo包裹一个组件,当组件的props发生改变时才会重新渲染。


若包裹的组件是个函数式组件,在其中拥有useStateuseReduceruseContext的 Hook,当state 或context发生变化时,它仍会重新渲染,不过这些影响不大,使用React.memo的主要目的是控制父组件更新迫使子组件也更新的问题。


props值的类型可以为String、Boolean、Null、undefined、Number、Symbol、Object、Array、Function,简单的来说就是基础类型和引用类型。


两个值相等的基础类型的数据用==比较返回true,那两个值相等的引用类型的数据用==比较返回false,不信试一试以下代码,看是不是都为false


console.log({a:1} == {a:1});
console.log([1] == [1]);
const fn1 = () =>{console.log(1)};
const fn2 = () =>{console.log(1)};
console.log(fn1 == fn2);

正因为如此,当props的值为引用类型时,且这个值是通过函数计算出来的,用useMemousecallback来处理一下,避免计算出来的值相等,但是比较却不相等,导致组件更新。我认为usecallback是专门为处理props值为函数而创建的Hook。


在下面的例子,List组件是一个渲染开销很大的组件,它有两个属性,其中data属性是渲染列表的数据源,是一个数组,onClick属性是一个函数。在Demo组件中引入List组件,用useMome处理data属性值,用useCallback处理onClick属性值,使得List组件是否重新渲染只受Demo组件的data这个state控制。


List组件:


import React from 'react';
const List = (props) => {
const { onClick, data } = props;
//...
return (
<>
{data.map(item => {
return (
<div onClick={() =>{onClick(item)}} key={item.id}>{item.content}</div>
)
})}
</>
)
}
export default React.memo(List);

Demo组件:


import
React,
{ useState, useCallback, useMemo }
from 'react';
import List from './List';

const Demo = () => {
//...
const [data, setData] = useState([]);
const listData = useMemo(() => {
return data.filter(item => {
//...
})
}, [data])

const onClick = useCallback((item) => {
console.log(item)
}, []);

return (
<div>
<List onClick={onClick} data={listData} />
</div>
)
}

export default Demo;

可见useMemousecallback作为一个性能优化手段,可以在一点程度上解决React父组件更新,其子组件也被迫更新的性能问题。



  • 单独使用


假设List组件不用React.memo包裹,还能用useMemousecallback来优化吗?先来看一段代码,也可以这么使用组件。


import React, { useState } from 'react';
import List from './List';

export default function Demo() {
//...
const [data, setData] = useState([]);
const list = () => {
const listData = data.filter(item => {
//...
})
const onClick = (item) => {
console.log(item)
}
return (
<List onClick={onClick} data={listData} />
)
}
return (
<div>
{list()}
</div>
)
}

其中list返回一个React元素,是一个值,那么是不是可以用useMemo来处理一下。


import React, { useState, useMemo } from 'react';
import List from './List';

export default function Demo() {
//...
const [data, setData] = useState([]);
const listMemo = useMemo(() => {
const listData = data.filter(item => {
//...
})
const onClick = (item) => {
console.log(item)
}
return (
<List onClick={onClick} data={listData} />
)
}, [data])
return (
<div>
{listMemo}
</div>
)
}

listMemo这个值(React元素)是否重新生成只受Demo组件的data这个state控制。这样不是相当List组件是否重新渲染只受Demo组件的data这个state控制。



  • 不能滥用



不能认为“不管什么情况,只要用useMemouseCallback处理一下,就能远离性能的问题”



要认识到useMemouseCallback也有一定的计算开销,例如useMemo会缓存一些值,在后续重新渲染,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回缓存的值。这个过程有一定的计算开销。


所以在使用useMemouseCallback前一定要考虑清楚使用场景,不能滥用。


下面总结一下要使用useMemouseCallback的场景:




  • 一个渲染开销很大的组件的属性是一个函数时,用useCallback处理一下这个属性值。




  • 得到一个值,有很高得计算开销,用useMemo处理一下。




  • 一个通过计算得到的引用类型的值,且这个值被赋值给一个渲染开销很大的组件的属性,用useMemo处理一下。




  • 自定义Hook中暴露出的引用类型的值,用useMemo处理一下。




总之一句话使用useMemouseCallback是为了保持引用相等和避免重复成本很高的计算。


五、关于useEffect和useLayoutEffect的收获


useEffect(fn);
useLayoutEffect(fn);

useEffect可谓是使用频率第二高的一个Hook,useLayoutEffect使用频率比较低。下面介绍四个使用心得:



  • 执行的时机


在使用useEffectuseLayoutEffect之前,要搞明白传入这个两个Hook的函数会在什么时候执行。


传入useEffect的函数是在React完成对DOM的更改,浏览器把更改后的DOM渲染出来后执行的


传入useLayoutEffect的函数是在React完成对DOM的更改后,浏览器把更改后的DOM渲染出来之前执行的


所以useEffect不会阻碍页面渲染,useLayoutEffect会阻碍页面渲染,但是如果要在渲染前获取DOM元素的属性做一些修改,useLayoutEffect是一个很好的选择。



  • 只想执行一次


组件初始化和每次更新时都会重新执行传入useEffectuseLayoutEffect的函数。那只想在组件初始化时执行一次呢?相当Vue中的mounted,这样实现:


useEffect(fn,[]);
useLayoutEffect(fn,[]);


  • 用来进行事件监听的坑


上面介绍在useEffect在第二参数传入一个空数组[]相当Vue中的mounted,那么在Vue的mounted中经常会用addEventListener监听事件,然后在beforeDestory中用removeEventListener移除事件监听。那用useEffect实现一下。


useEffect(() => {
window.addEventListener('keypress', handleKeypress, false);
return () => {
window.removeEventListener('keypress', handleKeypress, false);
};
},[])

上面用来监听键盘回事事件,但是你会发现一个很奇怪的现象,有些页面回车后会执行handleKeypress方法,有些页面回车后执行几次handleKeypress方法后,就不再执行了。


这是因为一个useEffect执行前会执行上一个useEffect的传入函数的返回函数,这个返回函数可以用来解除绑定,取消请求,防止引起内存泄露


此外组件卸载时,也会执行useEffect的传入函数的返回函数


示意如下代码所示:


window.addEventListener('keypress', handleKeypress, false); // 运行第一个effect
window.removeEventListener('keypress', handleKeypress, false);// 清除上一个effect
window.addEventListener('keypress', handleKeypress, false); // 运行下一个 effect
window.removeEventListener('keypress', handleKeypress, false); // 清除上一个effect
window.addEventListener('keypress', handleKeypress, false); // 运行下一个effect
window.removeEventListener('keypress', handleKeypress, false); // 清除最后一个effect

所以要解决上面的BUG,只要把useEffect的第二参数去掉即可。这点跟Vue中的mounted不一样。



  • 用来监听某个state或prop


useEffectuseLayoutEffect的第二参数是个数组,称为依赖,当依赖改变时候会执行传入的函数。


比如监听 a 这个state 和 b 这个prop,这样实现:


useEffect(fn,[a,b]);
useLayoutEffect(fn,[a,b]);

但是要注意不要一次性监听过多的state或prop,也就是useEffect的依赖过多,如果过多要去优化它,否则会导致这个useEffect难以维护。


我是这样来优化的:



  • 考虑该依赖是否必要,不必要去掉它。

  • useEffect拆分为更小的单元,每个useEffect依赖于各自的依赖数组。

  • 通过合并依赖中的相关state,将多个state聚合为一个state。


六、结语


以上五点就是我这六个月中印象最深的收获,有踩过的坑,有写被leader吐槽的代码。不过最大的收获还是薪资涨幅120%,哈哈哈,各位掘友要勇于跳出自己的舒适区,才能有更丰厚的收获。


虽然以上的收获在某些掘友们眼里会觉得比较简单,但是对于刚转入React的掘友们这些知识的使用频率非常高,要多多琢磨。如果有错误欢迎在评论中指出,或者掘友有更好的收获也在评论中分享一下。


作者:红尘炼心
链接:https://juejin.cn/post/7018328359742636039

收起阅读 »

转动的CSS“loading”,全都是技巧!

loader-1 这应该是最简单的CSS加载了。在圆圈上有一个红色的圆弧,仔细观察会发现,这个圆弧正好是1/4. 实现逻辑: 一个宽高相等容器,设定border为白色。然后给底边bottom设置红色, 当设定border-radius是50%,那他正好可以...
继续阅读 »

loader-1



这应该是最简单的CSS加载了。在圆圈上有一个红色的圆弧,仔细观察会发现,这个圆弧正好是1/4.


实现逻辑:


一个宽高相等容器,设定border为白色。然后给底边bottom设置红色,

当设定border-radius是50%,那他正好可以变成一个圆。



给这个圆加上旋转的动画。CSS中旋转角度的动画是rotate()我们只要给他设定从0旋转到360即可。(这个动画会在下面多次使用,下文不再赘述)


 @-webkit-keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

完整代码


.loader-1 {
width: 48px;
height: 48px;
border: 5px solid #FFF;
border-bottom-color: #FF3D00;
border-radius: 50%;
display: inline-block;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}

loader-2



观察:外围是一个圈,内部有一个红色的元素在转动。


实现逻辑


一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。



里面的红色是如何实现?这里有两个思路。1;新增一个小div,让他在里面,并且跟loader-1一样,设置一个红色的底边。2:使用::after,思路跟方法1 一致。



加上旋转的动画。


完整代码


.loader-2 {
width: 48px;
height: 48px;
border: 3px solid #FFF;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}

.loader-2:after {
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
border-radius: 50%;
border: 3px solid transparent;
border-bottom-color: #FF3D00;
}

loader-3



观察:内部是一个圆,外围是一个红色的圆弧。


实现逻辑


这个加载效果跟loader-2是一致的,区别就是红色圆弧在内外。


完整代码


.loader-3 {
width: 48px;
height: 48px;
border: 3px solid #FFF;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}

.loader-3:after {
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 56px;
height: 56px;
border-radius: 50%;
border: 3px solid transparent;
border-bottom-color: #FF3D00;
}

loader-4



观察:外围是一个圆,内部有两个圆,这两个圆正好是对称的。


实现逻辑


一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。



里面的红色是如何实现?这里有两个思路。1;新增两个小div,背景颜色设置为红色,然后设置50%圆角,这样看上去就像是两个小点。2:使用::after和::before,思路跟方法1 一致。



加上旋转的动画。


完整代码


.loader-4 {
width: 48px;
height: 48px;
border: 2px solid #FFF;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}

.loader-4:before {
left: auto;
top: auto;
right: 0;
bottom: 0;
content: "";
position: absolute;
background: #FF3D00;
width: 6px;
height: 6px;
transform: translate(150%, 150%);
border-radius: 50%;
}

.loader-4:after {
content: "";
position: absolute;
left: 0;
top: 0;
background: #FF3D00;
width: 6px;
height: 6px;
transform: translate(150%, 150%);
border-radius: 50%;
}

loader-5



观察:一共是三层,最外层白圈,中间红圈,里边白圈。每一圈都有一个一半圆弧的缺口,外圈和最内圈的旋转方向一致。


实现逻辑


一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。



这里的问题是,圈的缺口如何实现,其实很简单,在css中有一个属性值:transparent,利用这个值给边框设置透明,即可实现缺口。



对于内部的红色和白色圆弧,继续使用::after和::before即可。



加上动画,这里有一个反方向旋转的动画(rotationBack)。
这里设置旋转是往负角度,旋转即可反方向旋转。


  @keyframes rotationBack {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}

完整代码


.loader-5 {
width: 48px;
height: 48px;
border-radius: 50%;
display: inline-block;
position: relative;
border: 3px solid;
border-color: #FFF #FFF transparent transparent;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}

.loader-5:before {
width: 32px;
height: 32px;
border-color: #FFF #FFF transparent transparent;
-webkit-animation: rotation 1.5s linear infinite;
animation: rotation 1.5s linear infinite;
}

.loader-5:after, .loader-5:before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
border: 3px solid;
border-color: transparent transparent #FF3D00 #FF3D00;
width: 40px;
height: 40px;
border-radius: 50%;
-webkit-animation: rotationBack 0.5s linear infinite;
animation: rotationBack 0.5s linear infinite;
transform-origin: center center; *
}

loader-6



观察:看上去像是一个时钟,一个圆里面有一根指针。


实现逻辑


一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。



指针是如何实现的:从这里开始不再讨论新增div的情况
其实红色的指针就是一个单纯的宽高不一致的容器。



完整代码


.loader-6 {
width: 48px;
height: 48px;
border: 2px solid #FFF;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}


.loader-6:after {
content: "";
position: absolute;
left: 50%;
top: 0;
background: #FF3D00;
width: 3px;
height: 24px;
transform: translateX(-50%);
}

loader-7



观察:首先确定几个圈,一共两个。当第一个圈还没消失,第二个圈已经出现。最后出现了类似水波的效果。同时要注意的是,这两个两个圈是一样大的,这是因为他们最终消失的地方是一致的。


实现逻辑


首先确定,这两个圈是否在容器上。上面一直时在容器上添加边框,当然这个例子也可以,但是为了实现的简单,我们把这两个圈放在::after和::before中。



加上动画,这里的圈是逐渐放大的,在CSS中scale用来放大缩小元素。同时为了实现波纹逐渐清晰的效果,我们加上透明度。


  @keyframes animloader7 {
0% {
transform: scale(0);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}

完整代码


这里因为两个圈是先后出现的,所以需要一个圈加上delay


.loader-7 {
width: 48px;
height: 48px;
display: inline-block;
position: relative;
}

.loader-7::after, .loader--7::before {
content: "";
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid #FFF;
position: absolute;
left: 0;
top: 0;
-webkit-animation: animloader7 2s linear infinite;
animation: animloader7 2s linear infinite;
}

.loader-7::after {
-webkit-animation-delay: 1s;
animation-delay: 1s;
}
.loader-7::after, .loader-7::before {
content: "";
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid #FFF;
position: absolute;
left: 0;
top: 0;
-webkit-animation: animloader7 2s linear infinite;
animation: animloader7 2s linear infinite;
}

loader-8



观察:一段圆弧加上一个三角形。


实现逻辑


一个宽高相等的容器,加上白色的边,50%的圆角。这样就是外围的圈。



transparent,利用这个值给边框设置透明,即可实现缺口。



在:after上创建箭头。CSS中我们有多种方法实现三角形,其中最简单是使用border,不需要给元素设置宽高,只需要设置border的大小,并且只有一边设置颜色。


border: 10px solid transparent;
border-right-color: #FFF

加上旋转动画。


完整代码


.loader-8 {
width: 48px;
height: 48px;
border: 3px solid #FFF;
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
position: relative;
-webkit-animation: rotation 1s linear infinite;
animation: rotation 1s linear infinite;
}

.loader-8:after {
content: "";
position: absolute;
left: 20px;
top: 31px;
border: 10px solid transparent;
border-right-color: #FFF;
transform: rotate(-40deg);
}



收起阅读 »

有了for循环 为什么还要forEach?

js中那么多循环,for for...in for...of forEach,有些循环感觉上是大同小异今天我们讨论下for循环和forEach的差异。 我们从几个维度展开讨论:for循环和forEach的本质区别。for循环和forEach的语法区别。for循...
继续阅读 »

js中那么多循环,for for...in for...of forEach,有些循环感觉上是大同小异今天我们讨论下for循环和forEach的差异。

我们从几个维度展开讨论:

for循环和forEach的本质区别。

for循环和forEach的语法区别。

for循环和forEach的性能区别。



本质区别



for循环是js提出时就有的循环方法。forEach是ES5提出的,挂载在可迭代对象原型上的方法,例如Array Set Map

forEach是一个迭代器,负责遍历可迭代对象。那么遍历迭代可迭代对象分别是什么呢。

遍历:指的对数据结构的每一个成员进行有规律的且为一次访问的行为。

迭代:迭代是递归的一种特殊形式,是迭代器提供的一种方法,默认情况下是按照一定顺序逐个访问数据结构成员。迭代也是一种遍历行为。

可迭代对象:ES6中引入了 iterable 类型,Array Set Map String arguments NodeList 都属于 iterable,他们特点就是都拥有 [Symbol.iterator] 方法,包含他的对象被认为是可迭代的 iterable


未标题-2.png


在了解这些后就知道 forEach 其实是一个迭代器,他与 for 循环本质上的区别是 forEach 是负责遍历(Array Set Map)可迭代对象的,而 for 循环是一种循环机制,只是能通过它遍历出数组。

再来聊聊究竟什么是迭代器,还记得之前提到的 Generator 生成器,当它被调用时就会生成一个迭代器对象(Iterator Object),它有一个 .next()方法,每次调用返回一个对象{value:value,done:Boolean}value返回的是 yield 后的返回值,当 yield 结束,done 变为 true,通过不断调用并依次的迭代访问内部的值。

迭代器是一种特殊对象。ES6规范中它的标志是返回对象的 next() 方法,迭代行为判断在 done 之中。在不暴露内部表示的情况下,迭代器实现了遍历。看代码


let arr = [1, 2, 3, 4]  // 可迭代对象
let iterator = arr[Symbol.iterator]() // 调用 Symbol.iterator 后生成了迭代器对象
console.log(iterator.next()); // {value: 1, done: false} 访问迭代器对象的next方法
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

我们看到了。只要是可迭代对象,调用内部的 Symbol.iterator 都会提供一个迭代器,并根据迭代器返回的next 方法来访问内部,这也是 for...of 的实现原理。


let arr = [1, 2, 3, 4]
for (const item of arr) {
console.log(item); // 1 2 3 4
}

把调用 next 方法返回对象的 value 值并保存在 item 中,直到 valueundefined 跳出循环,所有可迭代对象可供for...of消费。 再来看看其他可迭代对象:


function num(params) {
console.log(arguments); // Arguments(6) [1, 2, 3, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]
let iterator = arguments[Symbol.iterator]()
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
}
num(1, 2, 3, 4)

let set = new Set('1234')
set.forEach(item => {
console.log(item); // 1 2 3 4
})
let iterator = set[Symbol.iterator]()
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

所以我们也能很直观的看到可迭代对象中的 Symbol.iterator 属性被调用时都能生成迭代器,而 forEach 也是生成一个迭代器,在内部的回调函数中传递出每个元素的值。

(感兴趣的同学可以搜下 forEach 源码, Array Set Map 实例上都挂载着 forEach ,但网上的答案大多数是通过 length 判断长度, 利用for循环机制实现的。但在 Set Map 上使用会报错,所以我认为是调用的迭代器,不断调用 next,传参到回调函数。由于网上没查到答案也不妄下断言了,有答案的人可以评论区给我留言)



for循环和forEach的语法区别



了解了本质区别,在应用过程中,他们到底有什么语法区别呢?



  1. forEach 的参数。

  2. forEach 的中断。

  3. forEach 删除自身元素,index不可被重置。

  4. for 循环可以控制循环起点。


forEach 的参数


我们真正了解 forEach 的完整传参内容吗?它大概是这样:


arr.forEach((self,index,arr) =>{},this)

self: 数组当前遍历的元素,默认从左往右依次获取数组元素。

index: 数组当前元素的索引,第一个元素索引为0,依次类推。

arr: 当前遍历的数组。

this: 回调函数中this指向。


let arr = [1, 2, 3, 4];
let person = {
name: '技术直男星辰'
};
arr.forEach(function (self, index, arr) {
console.log(`当前元素为${self}索引为${index},属于数组${arr}`);
console.log(this.name+='真帅');
}, person)

我们可以利用 arr 实现数组去重:


let arr1 = [1, 2, 1, 3, 1];
let arr2 = [];
arr1.forEach(function (self, index, arr) {
arr.indexOf(self) === index ? arr2.push(self) : null;
});
console.log(arr2); // [1,2,3]

image.png


forEach 的中断


在js中有break return continue 对函数进行中断或跳出循环的操作,我们在 for循环中会用到一些中断行为,对于优化数组遍历查找是很好的,但由于forEach属于迭代器,只能按序依次遍历完成,所以不支持上述的中断行为。


let arr = [1, 2, 3, 4],
i = 0,
length = arr.length;
for (; i < length; i++) {
console.log(arr[i]); //1,2
if (arr[i] === 2) {
break;
};
};

arr.forEach((self,index) => {
console.log(self);
if (self === 2) {
break; //报错
};
});

arr.forEach((self,index) => {
console.log(self);
if (self === 2) {
continue; //报错
};
});

如果我一定要在 forEach 中跳出循环呢?其实是有办法的,借助try/catch


try {
var arr = [1, 2, 3, 4];
arr.forEach(function (item, index) {
//跳出条件
if (item === 3) {
throw new Error("LoopTerminates");
}
//do something
console.log(item);
});
} catch (e) {
if (e.message !== "LoopTerminates") throw e;
};

若遇到 return 并不会报错,但是不会生效


let arr = [1, 2, 3, 4];

function find(array, num) {
array.forEach((self, index) => {
if (self === num) {
return index;
};
});
};
let index = find(arr, 2);// undefined

forEach 删除自身元素,index不可被重置


forEach 中我们无法控制 index 的值,它只会无脑的自增直至大于数组的 length 跳出循环。所以也无法删除自身进行index重置,先看一个简单例子:


let arr = [1,2,3,4]
arr.forEach((item, index) => {
console.log(item); // 1 2 3 4
index++;
});

index不会随着函数体内部对它的增减而发生变化。在实际开发中,遍历数组同时删除某项的操作十分常见,在使用forEach删除时要注意。


for 循环可以控制循环起点


如上文提到的 forEach 的循环起点只能为0不能进行人为干预,而for循环不同:


let arr = [1, 2, 3, 4],
i = 1,
length = arr.length;

for (; i < length; i++) {
console.log(arr[i]) // 2 3 4
};

那之前的数组遍历并删除滋生的操作就可以写成


let arr = [1, 2, 1],
i = 0,
length = arr.length;

for (; i < length; i++) {
// 删除数组中所有的1
if (arr[i] === 1) {
arr.splice(i, 1);
//重置i,否则i会跳一位
i--;
};
};
console.log(arr); // [2]
//等价于
var arr1 = arr.filter(index => index !== 1);
console.log(arr1) // [2]


for循环和forEach的性能区别



在性能对比方面我们加入一个 map 迭代器,它与 filter 一样都是生成新数组。我们对比 for forEach map 的性能在浏览器环境中都是什么样的:

性能比较:for > forEach > map

在chrome 62 和 Node.js v9.1.0环境下:for 循环比 forEach 快1倍,forEachmap 快20%左右。
原因分析

for:for循环没有额外的函数调用栈和上下文,所以它的实现最为简单。

forEach:对于forEach来说,它的函数签名中包含了参数和上下文,所以性能会低于 for 循环。

mapmap 最慢的原因是因为 map 会返回一个新的数组,数组的创建和赋值会导致分配内存空间,因此会带来较大的性能开销。如果将map嵌套在一个循环中,便会带来更多不必要的内存消耗。

当大家使用迭代器遍历一个数组时,如果不需要返回一个新数组却使用 map 是违背设计初衷的。在我前端合作开发时见过很多人只是为了遍历数组而用 map 的:


let data = [];
let data2 = [1,2,3];
data2.map(item=>data.push(item));

写在最后:这是我面试遇到的一个问题,当时只知道语法区别。并没有从可迭代对象迭代器生成器性能方面,多角度进一步区分两者的异同,我也希望我能把一个简单的问题从多角度展开细讲,让大家正在搞懂搞透彻。

收起阅读 »

如何在你的项目中使用新的ES规范

JavaScript 和 ECMAScript 的关系 JavaScript 是一种高级的、编译型的编程语言。而 ECMAScript 是一种规范。 JavaScript 是基于 ECMAScript 规范的脚本语言。ECMAScript(以下简称 ES)在 ...
继续阅读 »

JavaScript 和 ECMAScript 的关系


JavaScript 是一种高级的、编译型的编程语言。而 ECMAScript 是一种规范。


JavaScript 是基于 ECMAScript 规范的脚本语言ECMAScript(以下简称 ES)在 2015 年发布了 ES6(ECMAScript 2015),而且 TC39 委员会决定每年发布一个 ECMAScript 的版本,也就是我们看到的 ES6/7/8/9/11/12


ES11 中两个非常有用的特性


空值合并操作符(??)


Nullish coalescing Operator(空值处理)只有 null 和 undefined 的时候才认为真的是空。
以下为使用方式:


let user = {
u1: 0,
u2: false,
u3: null,
u4: undefined
u5: '',
}
let u1 = user.u1 || '用户1' // 用户1
let u2 = user.u2 || '用户2' // 用户2
let u3 = user.u3 || '用户3' // 用户3
let u4 = user.u4 || '用户4' // 用户4
let u5 = user.u5 || '用户5' // 用户5
// es11语法【只有 null 和 undefined 的时候才认为真的是空】
let u1 = user.u1 ?? '用户1' // 0
let u2 = user.u2 ?? '用户2' // false
let u3 = user.u3 ?? '用户3' // 用户3
let u4 = user.u4 ?? '用户4' // 用户4
let u5 = user.u5 ?? '用户5' // ''

应用的场景:后端返回的数据中 null 和 0 表示的意义可能是不一样的,null 表示为空,展示成 /。0 还是有数值,展示为 0。


let a = 0;
console.log(a ?? '/'); // 0
a = null;
console.log(a ?? '/'); // '/'

Optional chaining(可选链)



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



这个有点类似于 lodash 工具库中的 get 方法


let user = {
age: 18
}
let u1 = user.childer.name // TypeError: Cannot read property 'name' of undefined
// es11 语法
let u1 = user.childer?.name // undefined

浏览器兼容性问题


虽然 ES 新的特性十分好用,但需要注意的是它们的兼容性问题。
比如,可选链目前的兼容性如下:



解决方法就是讲 ES 新特性的语法转换成 ES5 的语法。


使用 Babel 进行转换


Babel 是一个 JavaScript 编译器。它的输入是下一代 JavaScript 语法书写的代码,输出浏览器兼容的 JavaScript 代码。


我们可以通过 Babel 中的转换插件来进行语法转换。比如我们上面两个语法可以通过以下两个插件进行转换。


空值合并操作符。@babel/plugin-proposal-nullish-coalescing-operator


使用:


npm install --save-dev @babel/plugin-proposal-nullish-coalescing-operator

在配置文件中:


{
"plugins": ["@babel/plugin-proposal-nullish-coalescing-operator"]
}

就可以做到以下的转换,输入:


var foo = object.foo ?? "default";

输出:


var _object$foo;

var foo =
(_object$foo = object.foo) !== null && _object$foo !== void 0
? _object$foo
: "default";

同理,可选链操作符可以看 @babel/plugin-proposal-optional-chaining,还有其他的都可以在 @babel/preset-env 目录中找到。


测试


在 Firefox 中,下载比较老的版本。



const foo = null ?? 'default string';
console.log(foo);
// expected output: "default string"

const baz = 0 ?? 42;
console.log(baz);

运行上面的代码,报错:



项目中使用,成功。说明 polyfil 成功了。



总结


JavaScript 是基于 ECMAScript 规范的脚本语言。ECMAScript 规范的发展给前端开发带来了很多的便利,但我们在使用的时候应该使用 Babel 这种 JavaScript 编译器将其转换成浏览器兼容的代码。


作者:Gopal
链接:https://juejin.cn/post/7018174628090609701

收起阅读 »

Vue 3 凉凉了吗 - 10 个灵魂拷问

vue
很多人问我,现在可以用 Vue 3 了吗,Vue 2升级成本高吗,我想借着早早聊的场子把大家经常问的问题,跟大家谈一谈我的看法,我会尽量公平公正,客观正向,但尽然是看法,难免会有一些有争议的地方,或者不认可的地方,你可以留言。我总结了 10 个问题,期望能帮助...
继续阅读 »

很多人问我,现在可以用 Vue 3 了吗,Vue 2升级成本高吗,我想借着早早聊的场子把大家经常问的问题,跟大家谈一谈我的看法,我会尽量公平公正,客观正向,但尽然是看法,难免会有一些有争议的地方,或者不认可的地方,你可以留言。我总结了 10 个问题,期望能帮助你在技术选型中起到一定的帮助:


0、升级 Vue 3 成本大吗


可大可小,如果你使用的Vue推荐的 template 语法,成本是非常小的,改个版本号就已经可以 run 起来了,但前提是它的周边库你也已经升级了,不信你试试,其实周边库的升级成本不能算做 Vue 3 的升级成本,因为就算没有 Vue 3,周边也会不断升级,只是刚好撞到了一起,然后这个锅就让 Vue 一起背了。


关于升级这方面我想也可以借鉴 Ant Design Vue 的渐进式升级姿势,Ant Design Vue 2.x 版本是改了少量的代码,让其在 Vue 3 下运行,然后慢慢的使用Composition API 迭代重构,对于简单的没有破坏性更新的继续在 2.x 下迭代小版本,然后将有大量重构代码,复杂度高的,有破坏性更新的在 3.x 版本上迭代升级。


业务代码的更新升级相较于组件库成本会更加可控,因为组件库一般都会用到一些黑科技或非文档API等等,会让升级成本变高,这也是为什么一些组件库迟迟不兼容 Vue3 的部分原因。


1、Vue 3 稳定了吗?


目前 Vue 3 已经相当稳定,除非你会用到各种黑科技,业务项目不应该有黑科技,如果用到了,千万不要写到简历里,相信我,“黑科技”不但不会加分,还会减分,因为所谓的黑科技,大概率是你写法就不对。不服来辩


2、Vue 3 生态不丰富?


你所谓的生态是指哪些?状态管理?路由管理?国际化?组件库?SSR?常用生态库都已经提供了 Vue 3 版本,而且 Vue 2 版本都在逐渐减少维护时间。


退一步讲,如果还不够,那可是造轮子,刷 KPI 的好机会,不是嘛。


3、Vue 3 的写法不习惯?


Composition API 只是可选项,你依然可以用 Option API,没有什么变化。但我们应该跳出舒适圈,拥抱未来,拥抱更好的东西。


4、Vue 3 好找工作吗?


前端技术风口已经不多了,Vue 3 算一个,遥想当年懂个生命周期、虚拟DOM就可以进大厂的时代,甚是想念。


5、Vue 3 不兼容 IE11?


是的,不兼容,如果公司业务需要兼容IE11,我给的方案是:先统计下你们有多少 IE11 用户,是否还值得投入精力兼容,推动去IE化是需要套路的,数据、成本、收益 PPT形式报告给老板,没有想想的那么难。再透漏下,react 版本的 Antd,也会在下一个大版本中不再兼容 IE 11。


6、Vue、React 如何选择?


还在纠结?工具人用哪个,它都只是工具,哪来的优越感?我用 angular,我骄傲了吗?


摸鱼小能手:哪个熟悉用哪个,哪个干活快用哪个


职场新人:公司用哪个就用哪个


KPI 高手:轮着换,使用 Vue(React) 重构 React(Vue) 项目,加载时间减少 30%,秒开率提升,转化率提升10%,带来收益 2千万/年,这TM得跳着升,没毛病吧


学生:都得学,前端框架还没复杂到二选一的地步


7、升级 Vue 3 带来的收益


性能提升,可维护性提升(主要还是看人),刷 KPI,升职加薪。


尤其是性能提升方面,我会在早早聊 Vue 专场给大家分享 Ant Design Vue 使用 Vue 3 重构,总结的一些经验。


8、何时使用 Vue 3?


别问,问就是现在


9、大厂都在用 React ?


其实并没有,我了解的百度、腾讯、京东、字节、快手、美团等等大厂都是 Vue 重度用户,阿里相对特殊些,只有少数部门在使用 Vue、Angular,之所以使用 React,不是说 Vue 不够好,只是最开始选择了React那些部门做的比较好,后来在 React 基建方面也已经做了很多工作,两套共存,有点浪费资源,仅此而已。至于那些个别团队,自带优越感式的招聘,大家可以忽略了,技术和氛围应该都不咋地。


10、硬广,Ant Design Vue 什么时候兼容 Vue 3?


Ant Design Vue 自 2.0 版本开始,已经全面兼容 Vue 3,目前文档站点默认还是 1.x 版本,是因为就像 Vue 3 一样,2.x 版本目前是 next tag,我们会在 Ant Design Vue 3.0 rc 后切回主站,没错 Ant Design Vue 已经 3.0 alpha 了。


所以 Vue 3 凉了吗,说真的,我也不知道,怎么算凉?从 Github Star、npm 下载量来看,都是呈上升趋势,我个人甚至押宝 Vue 3,已经在 6月份辞职,目前全职开源,押宝 Vue 3 了。


但是个别人有这种感觉,也是可以理解的,可以说 Vue 的成功,yyx 个人运营能力起到了至关重要的作用,react 反而低调了很多,因为 React 主要是为公司服务的,其次才是社区,他们没有运营的压力,也没有太大的动力去做运营。在 Vue 3 前期运营的过程中,或许过度强调了 Composition API,导致有部分人产生了不兼容误解,或许这部分人并不在少数,或许 Vue 应该将 Composition API 放在 3.2、3.3 的小版本上去迭代添加。


当然这都是猜测,10月23日,yyx 亲自为大家解读 Vue 3 及生态现状,这应该可以帮助你进一步做出决策。我也会为大家同步 Ant Design Vue 现状及未来规划,如果顺利,我们也会有新产品发布,但大概率要跳票了,哈哈哈,敬请期待吧。

收起阅读 »

React 中 setState 是一个宏任务还是微任务?

最近有个朋友面试,面试官问了个奇葩的问题,也就是我写在标题上的这个问题。 能问出这个问题,面试官应该对 React 不是很了解,也是可能是看到面试者简历里面有写过自己熟悉 React,面试官想通过这个问题来判断面试者是不是真的熟悉 React 🤣。 面试官的...
继续阅读 »

最近有个朋友面试,面试官问了个奇葩的问题,也就是我写在标题上的这个问题。



能问出这个问题,面试官应该对 React 不是很了解,也是可能是看到面试者简历里面有写过自己熟悉 React,面试官想通过这个问题来判断面试者是不是真的熟悉 React 🤣。


面试官的问法是否正确?


面试官的问题是,setState 是一个宏任务还是微任务,那么在他的认知里,setState 肯定是一个异步操作。为了判断 setState 到底是不是异步操作,可以先做一个实验,通过 CRA 新建一个 React 项目,在项目中,编辑如下代码:


import React from 'react';
import logo from './logo.svg';
import './App.css';

class App extends React.Component {
state = {
count: 1000
}
render() {
return (
<div className="App">
<img
src={logo} alt="logo"
className="App-logo"
onClick={this.handleClick}
/>
<p>我的关注人数:{this.state.count}</p>
</div>
);
}
}

export default App;

页面大概长这样:



上面的 React Logo 绑定了一个点击事件,现在需要实现这个点击事件,在点击 Logo 之后,进行一次 setState 操作,在 set 操作完成时打印一个 log,并且在 set 操作之前,分别添加一个宏任务和微任务。代码如下:


handleClick = () => {
const fans = Math.floor(Math.random() * 10)
setTimeout(() => {
console.log('宏任务触发')
})
Promise.resolve().then(() => {
console.log('微任务触发')
})
this.setState({
count: this.state.count + fans
}, () => {
console.log('新增粉丝数:', fans)
})
}


很明显,在点击 Logo 之后,先完成了 setState 操作,然后再是微任务的触发和宏任务的触发。所以,setState 的执行时机是早于微任务与宏任务的,即使这样也只能说它的执行时机早于 Promise.then,还不能证明它就是同步任务。


handleClick = () => {
const fans = Math.floor(Math.random() * 10)
console.log('开始运行')
this.setState({
count: this.state.count + fans
}, () => {
console.log('新增粉丝数:', fans)
})
console.log('结束运行')
}


这么看,似乎 setState 又是一个异步的操作。主要原因是,在 React 的生命周期以及绑定的事件流中,所有的 setState 操作会先缓存到一个队列中,在整个事件结束后或者 mount 流程结束后,才会取出之前缓存的 setState 队列进行一次计算,触发 state 更新。只要我们跳出 React 的事件流或者生命周期,就能打破 React 对 setState 的掌控。最简单的方法,就是把 setState 放到 setTimeout 的匿名函数中。


handleClick = () => {
setTimeout(() => {
const fans = Math.floor(Math.random() * 10)
console.log('开始运行')
this.setState({
count: this.state.count + fans
}, () => {
console.log('新增粉丝数:', fans)
})
console.log('结束运行')
})
}


由此可见,setState 本质上还是在一个事件循环中,并没有切换到另外宏任务或者微任务中,在运行上是基于同步代码实现,只是行为上看起来像异步。所以,根本不存在面试官的问题。


React 是如何控制 setState 的 ?


前面的案例中,setState 只有在 setTimeout 中才会变得像一个同步方法,这是怎么做到的?


handleClick = () => {
// 正常的操作
this.setState({
count: this.state.count + 1
})
}
handleClick = () => {
// 脱离 React 控制的操作
setTimeout(() => {
this.setState({
count: this.state.count + fans
})
})
}

先回顾之前的代码,在这两个操作中,我们分别在 Performance 中记录一次调用栈,看看两者的调用栈有何区别。


正常操作


脱离 React 控制的操作


在调用栈中,可以看到 Component.setState 方法最终会调用enqueueSetState 方法 ,而 enqueueSetState 方法内部会调用 scheduleUpdateOnFiber 方法,区别就在于正常调用的时候,scheduleUpdateOnFiber 方法内只会调用 ensureRootIsScheduled ,在事件方法结束后,才会调用 flushSyncCallbackQueue 方法。而脱离 React 事件流的时候,scheduleUpdateOnFiberensureRootIsScheduled 调用结束后,会直接调用 flushSyncCallbackQueue 方法,这个方法就是用来更新 state 并重新进行 render。




function scheduleUpdateOnFiber(fiber, lane, eventTime) {
if (lane === SyncLane) {
// 同步操作
ensureRootIsScheduled(root, eventTime);
// 判断当前是否还在 React 事件流中
// 如果不在,直接调用 flushSyncCallbackQueue 更新
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
} else {
// 异步操作
}
}

上述代码可以简单描述这个过程,主要是判断了 executionContext 是否等于 NoContext 来确定当前更新流程是否在 React 事件流中。


众所周知,React 在绑定事件时,会对事件进行合成,统一绑定到 document 上( react@17 有所改变,变成了绑定事件到 render 时指定的那个 DOM 元素),最后由 React 来派发。


所有的事件在触发的时候,都会先调用 batchedEventUpdates$1 这个方法,在这里就会修改 executionContext 的值,React 就知道此时的 setState 在自己的掌控中。


// executionContext 的默认状态
var executionContext = NoContext;
function batchedEventUpdates$1(fn, a) {
var prevExecutionContext = executionContext;
executionContext |= EventContext; // 修改状态
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
// 调用结束后,调用 flushSyncCallbackQueue
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
}
}


所以,不管是直接调用 flushSyncCallbackQueue ,还是推迟调用,这里本质上都是同步的,只是有个先后顺序的问题。


未来会有异步的 setState


如果你有认真看上面的代码,你会发现在 scheduleUpdateOnFiber 方法内,会判断 lane 是否为同步,那么是不是存在异步的情况?


function scheduleUpdateOnFiber(fiber, lane, eventTime) {
if (lane === SyncLane) {
// 同步操作
ensureRootIsScheduled(root, eventTime);
// 判断当前是否还在 React 事件流中
// 如果不在,直接调用 flushSyncCallbackQueue 更新
if (executionContext === NoContext) {
flushSyncCallbackQueue();
}
} else {
// 异步操作
}
}

React 在两年前,升级 fiber 架构的时候,就是为其异步化做准备的。在 React 18 将会正式发布 Concurrent 模式,关于 Concurrent 模式,官方的介绍如下。




什么是 Concurrent 模式?


Concurrent 模式是一组 React 的新功能,可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整。在 Concurrent 模式中,渲染不是阻塞的。它是可中断的。这改善了用户体验。它同时解锁了以前不可能的新功能。




作者:Shenfq
链接:https://juejin.cn/post/6992006476558499853

收起阅读 »

正确介绍自己的项目经验 再也不为面试发愁了

在面试时,经过简单寒暄后,面试官一般先从让候选人自我介绍开始,紧接着就是问候选人简历中所列的项目,让介绍下项目经验。常见的问法是,说下你最近的(或感觉不错的)一个项目。面试中很多人忽视对这一个环节的准备,不仅回答不了面试官的追问,甚至连自己的项目都讲不清楚,说...
继续阅读 »

在面试时,经过简单寒暄后,面试官一般先从让候选人自我介绍开始,紧接着就是问候选人简历中所列的项目,让介绍下项目经验。常见的问法是,说下你最近的(或感觉不错的)一个项目。面试中很多人忽视对这一个环节的准备,不仅回答不了面试官的追问,甚至连自己的项目都讲不清楚,说起来磕磕巴巴,甚至有的人说出的项目经验从时间段或技术等方面和简历上的不匹配,这样无疑会让面试官对面试者的能力产生怀疑。


面试时7份靠能力,3份靠技能,本文将从“前期准备”和“面试技巧”两大层面告诉大家如何准备面试时的项目介绍,当然,这只是一家之言,没有最好的方式,只有更适合的方法,仁者见仁智者见智。


前期分析




  1. 知己知彼百战不殆。如果想打动面试官,那么你就必须得了解他到底想要从你口中了解到什么,哪些信息是他所想要的。


    在面试前准备项目描述时,别害怕,因为面试官什么都不知道,最了解你项目的还是你自己。


    面试官是人,不是神,拿到你的简历的时候,只能根据你所描述的项目去推测你的工作经历,是没法核实你的项目细节的(一般公司会到录用后,通过背景调查的方式来核实)。


    更何况,你做的项目是以月或以年为单位算的,而面试官最多用30分钟来从你的简历上了解你的项目经验,所以你对项目的熟悉程度要远远超过面试官,所以你一点也不用紧张。而面试官想了解更多他想知道的你的工作方式及项目中所负责的内容、所用到的技术栈,就不得不从你的介绍中去深挖技术点,以期了解你对项目及技术的了解的深度。


    首先从气势上就要告诉面试官,这项目就是你参与的,有你所负责的功能模块,让面试官不容置疑。


    心态上准备好了,那么就要分析面试官想要考察什么呢?



    • 表达能力。考察表达及逻辑思维能力,看面试者能不能在几分钟就跟一个完全没有参与过项目的人讲清楚这个项目。

    • 实际工作经验。你在项目中中承担了什么角色,做了什么工作。这些工作中的技术含量及跟同事合作情况如何。另外可能会针对某个项目,不断深入问一些技术上的问题,或者是从侧面问技术类实现,这是为了深入核实你做项目的细节及对技术的理解运用。

    • 解决问题能力。一般都会问项目难点,其实就是想知道当你遇到技术或业务难点,是如何思考并解决问题的。

    • 项目复盘及经验总结能力。哪里是自己觉得做的成功的,哪里觉得没做好的,是否还有继续优化的空间。自己所做的项目对个人能力有哪些提升。




  2. 熟能生巧,对答自如


    首先是需要有个充足的准备,写项目经验一定要写自己熟悉的,因为面试官就会根据你写的项目经验提问。在面试前,就要在脑子里过一遍这个项目,准备好说辞,面试的时候自信点。讲清楚这个项目是满足什么需求的,开发过程中遇到哪些困难,自己怎么解决这些困难的。如果你经过充分准备,面试中也能答的很好,那面试官好感度就会增加,相反,如果面试中说的磕磕绊绊,那么可信度就会低了。




  3. 明确目标,控盘引导


    在面试前,你需要明确自己的面试目的,就是通过面试,争取拿到 Offer 。


    最保守的方式就是在自己介绍项目的时候要么就是以面试官为主导,回答很简单,就是面试官问你一句你答一句。这会让面试官失去想了解你的信心,其次也会让自己错失表现自己,凸显重点思想的机会。做好防守虽然也是一种取胜的方式,但并非上策,很容易丢分。


    讲自己简历中所列的项目一定要很清晰明了有逻辑,埋下后续可能会提问到的技术点,也给面试官留下一个好印象。如果项目经验介绍的好,有逻辑和引导力,那么就会给自己带来以下两点明显的好处:



    1. 给面试官带来的第一印象好,会让面试官感觉该候选人表述能力较强。

    2. 一般来说,面试官会根据候选人介绍的项目背景来提问题,假设面试时会问10个问题,那么至少有5个问题会根据候选人所介绍的项目背景来问,候选人如果说的好,那么就可以很好地引导面试官去问后继问题,这就相当于把提问权完全交由自己控制了。


    如果你的工作经验比面试官还丰富的话,甚至还可以控制整个面试流程,甚至遇到 Nice 的面试官的话会以讨论的方式进行沟通。


    既然面试官无法了解你的底细,那么他们怎么来验证你的项目经验和技术?下面总结了一些常用的提问方式。




面试技巧


内容上要对项目进行以下拆分,思考并进行总结,并试着口语化讲出来。



  1. 项目描述。用通俗易懂且简洁的方式简述项目,阐述整个项目及其背景、规模,不要有太多的技术词汇。

  2. 项目模块。2-3分钟的流程介绍,详细的列出项目实现的功能,各个模块,整个过程,大概思路。

  3. 项目技术栈。说出项目实现的技术栈和架构,能说出项目的不寻常之处,比如采用了某项新技术,采用了某个架框等,简要说明技术选型。

  4. 候选人的角色及责任。说出你在项目中的责任,所涉及的功能模块,运用的技术,碰到的难题、代码细节,技术点、应对措施。

  5. 项目总结,待优化点


方法上可以使用万能的STAR原则


Situation(背景):做这个项目的背景是什么,比如这是个人项目还是团队项目,为什么需要做这个
项目,你的角色是什么,等等。

Target(目标):该项目要达成的目标是什么?为了完成这个目标有哪些困难?

Action(行动):针对所要完成目标,你做了哪些工作?如何克服了其中一些困难?

Result(结果):项目最终结果如何?有哪些成就?有哪些不足之处可以改进?


除了项目所取得的成绩之外,候选人还可以说明自己做完项目的感受,包括项目中哪些环节做的不错,哪些环节有提高的空间,自己在做这个项目中有何收获等。


无论是介绍自己的IT产品开发经历,还是在其他公司的实习项目经历,候选人都可以运用STAR法则来具体说明,轻松表现出自己分析阐述问题的清晰性、条理性和逻辑性。


但面试前如下的一些情况还是需要多加注意的。




  1. 回答很简单。问什么答什么,往往就一句话回答。如果你日常回答别人的问题或者之前面试中出现过类似情况就要有所改善了。这里应该将你知道的说出来,重点突出跟问题相关的思想、框架或技术点等。




  2. 扯闲篇,大忌。说少了太过于简短没有互动不好,自来熟,回答问题没有重点,没有逻辑,乱说一通也是大忌。会让面试官感觉你思路混乱,抓不到重点,只是拿其他方面的东西东拼西凑。




  3. 说的太过流利,也未必就是好事。虽然面试有所准备在面试官看来是好事,但是机械的准备好答案去背诵,主观上给人一种你并没有理解这个问题,只是靠记忆知道答案,后续面试官的问题也会相应的加大难度。这方面改善建议是适当停顿,做思考状,边思考边说,过程中同面试官有个眼神上的互动。




  4. 有的放矢的介绍技术细节。不要一次性过多的介绍技术细节,技术面点到为止,等面试官来问。因为面试官通常都有自己的面试节奏。所以技术点等着问的时候再多聊,可以先事先埋下技术点引导着面试官继续追问。




  5. 主动介绍项目亮点。因为面试官没有义务挖掘你的亮点,所以这就需要自己主动提。遇到不会的问题,就如实说这个技术点不会。或者半懂也可以直接说。甚至可以谈谈自己的见解。把自己了解的说说。




项目准备


一般来说,在面试前,大家应当准备项目描述的说辞,自信些,因为这部分你说了算,流利些,因为你经过充分准备后,可以知道你要说些什么。而且这些是你实际的项目经验(不是学习经验,也不是培训经验),那么一旦让面试官感觉你都说不上来,那么可信度就很低了。


不少人是拘泥于“项目里做了什么业务,以及代码实现的细节”,这就相当于把后继提问权直接交给面试官。下表列出了一些不好的回答方式。


不露痕迹地说出面试官爱听的话


在项目介绍的时候(当然包括后继的面试),面试官其实很想要听一些关键点,只要你说出来,而且回答相关问题比较好,这绝对是加分项。我在面试别人的时候,一旦这些关键点得到确认,我是绝对会在评语上加上一笔的。


下面列些面试官爱听的关键点和对应的说辞。



一旦有低级错误,可能会直接出局


面试过程中有些方面你是绝对不能出错,所以你在准备过程中需要尤其注意如下的因素。下面列了些会导致你直接出局的错误回答。



面试场景题


举一个例子,比如考察候选人是否聪明,star 法则会这样询问:


1.在刚才的项目中,你提到了公司业务发展很快,人手不够,你是如何应对的呢?
2.在你的项目里面解决了什么样的难题
3.在你的项目里面如何做的登录
4.前端的项目如何进行优化,移动端呢?
5.图片加载失败要做啥
6.让你带领一个小团队完成一个项目,你会怎么做?
7.项目的同源处理,跨域相关
8.如果再做这个项目,你会在哪些方面进行改善?
面试中,如果面试官让你描述一个自己比较得意的项目的时候,一定记得要遵循 STAR 法则进行回答。比如


为了整合 xxx 业务(S),我承担 xxx 角色,具体负责 xxx (T)。做了 xxx 事情(A),最后产生了 xxx 结果


然后在描述项目亮点的时候也一样,比如


由于项目 xxx 原因(S),我需要进行 xxx 改进(T),然后进行了 xxx 处理(A),最后产出了 xxx 结果,数据对比为 xxx


整体这样下来,会显得你很有思考力,且具有行动力,可以给企业创造出价值,这也是面试官评定候选人最关键的指标之一。


面试官的套路

面试时所问的问题基本分为两种:具象的问题和开放性的问题。


具象的问题基本都会参考工作经验按照 STAR 法则来进行,主要是了解基本的素养,技术深度和潜力。


开放性的问题基本是考察思维发散能力,考察在某个领域的深度和广度,基本上会结合技术问题来问,或者是结合工作内容来问。


比如:实现某种技术的 n 种方法?某种技术的实现原理?和什么什么相比有哪些优缺点?你对这项技术的思考是什么?


面试者的应对

1.就实际情况做回答,提前准备的时候多发散,多思考,多总结。这一块是可以自己准备的加分项。


2.发散性问题主要是看自己平时积累。首先基础知识要牢固,同时也要了解最新技术动态。面对这类问题切记也不能答非所问而跑题了。


注意:


1.避免拿别人的项目直接用


很多初级阶段的同学们,可能并没有实际的商业项目,或者所做过的项目类型有限,就直接从网上找项目当做自己的项目,直接使用是断不可取的,但是如果你仿造别人的项目自己去尝试着将功能实现,有自己的新得体验,这样在做的过程中也可以对项目中的功能点和技术栈有进一步的了解,不至于在面试的时候,磕磕巴巴,甚至将项目时间都搞错。


2.避免低级错误


很多基础相关的低级错误一定要杜绝,如果被问到熟悉知识点就多答,不熟悉就直接说不熟悉。每个人都有自己擅长的点也有不擅长的。


另外就是可以引导一些话题,不要自说自话。很多人会一直很激进的表达自己,反而显得强势。有的面试者被问到数据库相关内容,他不仅回答数据库,还会把大数据处理技术全部都说出来。其实点到为止最好,面试官感兴趣会继续问,但是你一直主导话题,会减分。


这里要说的是,不要把不是自己做的项目说成是自己做的,自己不是核心负责人说成是负责人,即使你对项目很熟悉了解,像我们一线起来的面试官,问几个问题就很清楚你实际参与了多少了,只是大部分不会明说而已,反而起到反效果。


总结


首先我要劝大家,认真对待每一次面试。既然知道自己要参加面试,就在家自己模拟一下面试。自己提前准备一下自己的项目描述,不要到了面试的时候去打磕巴。但是如果你参加面试的时候实在紧张了,磕巴了不要慌。深呼吸尝试让自己放松,一般面试官也会给些提示帮助你回答的。


两句话,第一,面试前一定要准备,第二,本文给出是的方法,不是教条,大家可以按本文给出的方向结合自己的项目背景做准备,而不是死记硬背本文给出的一些说辞。



作者:Gaby
链接:https://juejin.cn/post/7017732278509453348

收起阅读 »

就因为JSON.stringify,我的年终奖差点打水漂了

产品同学在诉苦:线上用户不能提交表单了,带来了好多客诉,估计会是p0故障,希望尽快解决。 测试同学在纳闷:这个场景测试和预发环境明明验过的,怎么线上就不行了。 后端同学在讲原因:接口缺少了value字段,导致出错了。 就是木有人说问题怎么解决!!! 就是木有人...
继续阅读 »

产品同学在诉苦:线上用户不能提交表单了,带来了好多客诉,估计会是p0故障,希望尽快解决。


测试同学在纳闷:这个场景测试和预发环境明明验过的,怎么线上就不行了。


后端同学在讲原因:接口缺少了value字段,导致出错了。


就是木有人说问题怎么解决!!!


就是木有人说问题怎么解决!!!


就是木有人说问题怎么解决!!!


这样的场景不知道你是不是也似曾相识呢?o(╥﹏╥)o,不管咋说第一要务还是先把线上问题解决掉,减少持续影响,赶紧把交接的代码翻出来,开始了排查过程。


问题原因



如下图:有这样一个动态表单搜集页面,用户选择或者填写了信息之后(各字段非必填情况下也可以直接提交),接着前端把数据发送给后端,结束,看起来没有多复杂的逻辑。



image.png


直接错误原因



非必填情况下,signInfo字段中经过JSON.stringify后的字符串对象缺少value key,导致后端parse之后无法正确读取value值,进而报接口系统异常,用户无法进行下一步动作。



// 异常入参数据,数组字符串中没有value key
{
signInfo: '[{"fieldId":539},{"fieldId":540},{"fieldId":546,"value":"10:30"}]'
}

// 正常入参数据
{
signInfo: '[{"fieldId":539,"value":"银卡"},{"fieldId":540,"value":"2021-03-01"},{"fieldId":546,"value":"10:30"}]'
}



异常数据是如何产生的


// 默认情况下数据是这样的
let signInfo = [
{
fieldId: 539,
value: undefined
},
{
fieldId: 540,
value: undefined
},
{
fieldId: 546,
value: undefined
},
]
// 经过JSON.stringify之后的数据,少了value key,导致后端无法读取value值进行报错
// 具体原因是`undefined`、`任意的函数`以及`symbol值`,出现在`非数组对象`的属性值中时在序列化过程中会被忽略
console.log(JSON.stringify(signInfo))
// '[{"fieldId":539},{"fieldId":540},{"fieldId":546}]'


解决方案



问题的原因找到了,解决方式 (这里只讲前端的解决方案,当然也可以由后端解决) 也很简单,将value值为undefined的项转化为空字符串再提交即可。



方案一:新开一个对象处理


let signInfo = [
{
fieldId: 539,
value: undefined
},
{
fieldId: 540,
value: undefined
},
{
fieldId: 546,
value: undefined
},
]

let newSignInfo = signInfo.map((it) => {
const value = typeof it.value === 'undefined' ? '' : it.value
return {
...it,
value
}
})

console.log(JSON.stringify(newSignInfo))
// '[{"fieldId":539,"value":""},{"fieldId":540,"value":""},{"fieldId":546,"value":""}]'


方案二:利用JSON.stringify第二个参数,直接处理



方案一的缺陷是需要新开一个对象进行一顿操作才能解决,不够优雅



let signInfo = [
{
fieldId: 539,
value: undefined
},
{
fieldId: 540,
value: undefined
},
{
fieldId: 546,
value: undefined
},
]

// 判断到value为undefined,返回空字符串即可
JSON.stringify(signInfo, (key, value) => typeof value === 'undefined' ? '' : value)
// '[{"fieldId":539,"value":""},{"fieldId":540,"value":""},{"fieldId":546,"value":""}]'


故事后续



原本这是一个已经上线有一段时间的页面,为何会突然出现这个问题,之前却没有呢?仔细询问下,原来是中途产品同学提了一个小的优化点,离职的小伙伴感觉点比较小直接就改了代码上线了,未曾想出现了线上问题。



后面针对这件事从产品到测试、到后端、到前端单独做了一个完整的复盘,细节就不再展开说了。


因为从发现问题到解决问题速度较快、影响用户数较少,还未达到问责程度,俺的年终奖可算是保住了o(╥﹏╥)o。


重学JSON.stringify



经过这件事情,我觉得有必要重新审视一下JSON.stringify这个方法,彻底搞清楚转换规则,并尝试手写实现一个JSON.stringify



如果你曾遇到和我一样的问题,欢迎一起来重新学习一次,一定会有不一样的收获噢!


学透JSON.stringify



JSON.stringify()  方法将一个 JavaScript 对象转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。



以下信息来自MDN


语法


JSON.stringify(value[, replacer [, space]])

参数




  • value


    将要序列化成 一个 JSON 字符串的值。




  • replacer 可选



    1. 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;

    2. 如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;

    3. 如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。




  • space 可选



    1. 指定缩进用的空白字符串,用于美化输出(pretty-print);

    2. 如果参数是个数字,它代表有多少的空格;上限为10。

    3. 该值若小于1,则意味着没有空格;

    4. 如果该参数为字符串(当字符串长度超过10个字母,取其前10个字母),该字符串将被作为空格;

    5. 如果该参数没有提供(或者为 null),将没有空格。




返回值


一个表示给定值的JSON字符串。

异常



  • 当在循环引用时会抛出异常TypeError ("cyclic object value")(循环对象值)

  • 当尝试去转换 BigInt 类型的值会抛出TypeError ("BigInt value can't be serialized in JSON")(BigInt值不能JSON序列化).


基本使用


注意



  1. JSON.stringify可以转换对象或者值(平常用的更多的是转换对象)

  2. 可以指定replacer为函数选择性的地替换

  3. 也可以指定replacer为数组,可转换指定的属性


这里仅仅是NDN上关于JSON.stringify其中最基础的说明,咱们先打个码试试这几个特性


// 1. 转换对象
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy' })) // '{"name":"前端胖头鱼","sex":"boy"}'

// 2. 转换普通值
console.log(JSON.stringify('前端胖头鱼')) // "前端胖头鱼"
console.log(JSON.stringify(1)) // "1"
console.log(JSON.stringify(true)) // "true"
console.log(JSON.stringify(null)) // "null"

// 3. 指定replacer函数
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }, (key, value) => {
return typeof value === 'number' ? undefined : value
}))
// '{"name":"前端胖头鱼","sex":"boy"}'

// 4. 指定数组
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }, [ 'name' ]))
// '{"name":"前端胖头鱼"}'

// 5. 指定space(美化输出)
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }))
// '{"name":"前端胖头鱼","sex":"boy","age":100}'
console.log(JSON.stringify({ name: '前端胖头鱼', sex: 'boy', age: 100 }, null , 2))
/*
{
"name": "前端胖头鱼",
"sex": "boy",
"age": 100
}
*/

9大特性要记住



以前仅仅是使用了这个方法,却没有详细了解他的转换规则,居然有9个之多。



特性一



  1. undefined任意的函数以及symbol值,出现在非数组对象的属性值中时在序列化过程中会被忽略

  2. undefined任意的函数以及symbol值出现在数组中时会被转换成 null

  3. undefined任意的函数以及symbol值单独转换时,会返回 undefined


// 1. 对象中存在这三种值会被忽略
console.log(JSON.stringify({
name: '前端胖头鱼',
sex: 'boy',
// 函数会被忽略
showName () {
console.log('前端胖头鱼')
},
// undefined会被忽略
age: undefined,
// Symbol会被忽略
symbolName: Symbol('前端胖头鱼')
}))
// '{"name":"前端胖头鱼","sex":"boy"}'

// 2. 数组中存在着三种值会被转化为null
console.log(JSON.stringify([
'前端胖头鱼',
'boy',
// 函数会被转化为null
function showName () {
console.log('前端胖头鱼')
},
//undefined会被转化为null
undefined,
//Symbol会被转化为null
Symbol('前端胖头鱼')
]))
// '["前端胖头鱼","boy",null,null,null]'

// 3.单独转换会返回undefined
console.log(JSON.stringify(
function showName () {
console.log('前端胖头鱼')
}
)) // undefined
console.log(JSON.stringify(undefined)) // undefined
console.log(JSON.stringify(Symbol('前端胖头鱼'))) // undefined

特性二



布尔值数字字符串的包装对象在序列化过程中会自动转换成对应的原始值。



console.log(JSON.stringify([new Number(1), new String("前端胖头鱼"), new Boolean(false)]))
// '[1,"前端胖头鱼",false]'

特性三



所有以symbol为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。



console.log(JSON.stringify({
name: Symbol('前端胖头鱼'),
}))
// '{}'
console.log(JSON.stringify({
[ Symbol('前端胖头鱼') ]: '前端胖头鱼',
}, (key, value) => {
if (typeof key === 'symbol') {
return value
}
}))
// undefined

特性四



NaN 和 Infinity 格式的数值及 null 都会被当做 null。



console.log(JSON.stringify({
age: NaN,
age2: Infinity,
name: null
}))
// '{"age":null,"age2":null,"name":null}'

特性五



转换值如果有 toJSON() 方法,该方法定义什么值将被序列化。



const toJSONObj = {
name: '前端胖头鱼',
toJSON () {
return 'JSON.stringify'
}
}

console.log(JSON.stringify(toJSONObj))
// "JSON.stringify"

特性六



Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。



const d = new Date()

console.log(d.toJSON()) // 2021-10-05T14:01:23.932Z
console.log(JSON.stringify(d)) // "2021-10-05T14:01:23.932Z"

特性七



对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。



let cyclicObj = {
name: '前端胖头鱼',
}

cyclicObj.obj = cyclicObj

console.log(JSON.stringify(cyclicObj))
// Converting circular structure to JSON

特性八



其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性



let enumerableObj = {}

Object.defineProperties(enumerableObj, {
name: {
value: '前端胖头鱼',
enumerable: true
},
sex: {
value: 'boy',
enumerable: false
},
})

console.log(JSON.stringify(enumerableObj))
// '{"name":"前端胖头鱼"}'

特性九



当尝试去转换 BigInt 类型的值会抛出错误



const alsoHuge = BigInt(9007199254740991)

console.log(JSON.stringify(alsoHuge))
// TypeError: Do not know how to serialize a BigInt

手写一个JSON.stringify



终于重新学完JSON.stringify的众多特性啦!咱们根据这些特性来手写一个简单版本的吧(无replacer函数和space



源码实现


const jsonstringify = (data) => {
// 确认一个对象是否存在循环引用
const isCyclic = (obj) => {
// 使用Set数据类型来存储已经检测过的对象
let stackSet = new Set()
let detected = false

const detect = (obj) => {
// 不是对象类型的话,可以直接跳过
if (obj && typeof obj != 'object') {
return
}
// 当要检查的对象已经存在于stackSet中时,表示存在循环引用
if (stackSet.has(obj)) {
return detected = true
}
// 将当前obj存如stackSet
stackSet.add(obj)

for (let key in obj) {
// 对obj下的属性进行挨个检测
if (obj.hasOwnProperty(key)) {
detect(obj[key])
}
}
// 平级检测完成之后,将当前对象删除,防止误判
/*
例如:对象的属性指向同一引用,如果不删除的话,会被认为是循环引用
let tempObj = {
name: '前端胖头鱼'
}
let obj4 = {
obj1: tempObj,
obj2: tempObj
}
*/
stackSet.delete(obj)
}

detect(obj)

return detected
}

// 特性七:
// 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
if (isCyclic(data)) {
throw new TypeError('Converting circular structure to JSON')
}

// 特性九:
// 当尝试去转换 BigInt 类型的值会抛出错误
if (typeof data === 'bigint') {
throw new TypeError('Do not know how to serialize a BigInt')
}

const type = typeof data
const commonKeys1 = ['undefined', 'function', 'symbol']
const getType = (s) => {
return Object.prototype.toString.call(s).replace(/\[object (.*?)\]/, '$1').toLowerCase()
}

// 非对象
if (type !== 'object' || data === null) {
let result = data
// 特性四:
// NaN 和 Infinity 格式的数值及 null 都会被当做 null。
if ([NaN, Infinity, null].includes(data)) {
result = 'null'
// 特性一:
// `undefined`、`任意的函数`以及`symbol值`被`单独转换`时,会返回 undefined
} else if (commonKeys1.includes(type)) {
// 直接得到undefined,并不是一个字符串'undefined'
return undefined
} else if (type === 'string') {
result = '"' + data + '"'
}

return String(result)
} else if (type === 'object') {
// 特性五:
// 转换值如果有 toJSON() 方法,该方法定义什么值将被序列化
// 特性六:
// Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
if (typeof data.toJSON === 'function') {
return jsonstringify(data.toJSON())
} else if (Array.isArray(data)) {
let result = data.map((it) => {
// 特性一:
// `undefined`、`任意的函数`以及`symbol值`出现在`数组`中时会被转换成 `null`
return commonKeys1.includes(typeof it) ? 'null' : jsonstringify(it)
})

return `[${result}]`.replace(/'/g, '"')
} else {
// 特性二:
// 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
if (['boolean', 'number'].includes(getType(data))) {
return String(data)
} else if (getType(data) === 'string') {
return '"' + data + '"'
} else {
let result = []
// 特性八
// 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性
Object.keys(data).forEach((key) => {
// 特性三:
// 所有以symbol为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
if (typeof key !== 'symbol') {
const value = data[key]
// 特性一
// `undefined`、`任意的函数`以及`symbol值`,出现在`非数组对象`的属性值中时在序列化过程中会被忽略
if (!commonKeys1.includes(typeof value)) {
result.push(`"${key}":${jsonstringify(value)}`)
}
}
})

return `{${result}}`.replace(/'/, '"')
}
}
}
}

测试一把


// 1. 测试一下基本输出
console.log(jsonstringify(undefined)) // undefined
console.log(jsonstringify(() => { })) // undefined
console.log(jsonstringify(Symbol('前端胖头鱼'))) // undefined
console.log(jsonstringify((NaN))) // null
console.log(jsonstringify((Infinity))) // null
console.log(jsonstringify((null))) // null
console.log(jsonstringify({
name: '前端胖头鱼',
toJSON() {
return {
name: '前端胖头鱼2',
sex: 'boy'
}
}
}))
// {"name":"前端胖头鱼2","sex":"boy"}

// 2. 和原生的JSON.stringify转换进行比较
console.log(jsonstringify(null) === JSON.stringify(null));
// true
console.log(jsonstringify(undefined) === JSON.stringify(undefined));
// true
console.log(jsonstringify(false) === JSON.stringify(false));
// true
console.log(jsonstringify(NaN) === JSON.stringify(NaN));
// true
console.log(jsonstringify(Infinity) === JSON.stringify(Infinity));
// true
let str = "前端胖头鱼";
console.log(jsonstringify(str) === JSON.stringify(str));
// true
let reg = new RegExp("\w");
console.log(jsonstringify(reg) === JSON.stringify(reg));
// true
let date = new Date();
console.log(jsonstringify(date) === JSON.stringify(date));
// true
let sym = Symbol('前端胖头鱼');
console.log(jsonstringify(sym) === JSON.stringify(sym));
// true
let array = [1, 2, 3];
console.log(jsonstringify(array) === JSON.stringify(array));
// true
let obj = {
name: '前端胖头鱼',
age: 18,
attr: ['coding', 123],
date: new Date(),
uni: Symbol(2),
sayHi: function () {
console.log("hello world")
},
info: {
age: 16,
intro: {
money: undefined,
job: null
}
},
pakingObj: {
boolean: new Boolean(false),
string: new String('前端胖头鱼'),
number: new Number(1),
}
}
console.log(jsonstringify(obj) === JSON.stringify(obj))
// true
console.log((jsonstringify(obj)))
// {"name":"前端胖头鱼","age":18,"attr":["coding",123],"date":"2021-10-06T14:59:58.306Z","info":{"age":16,"intro":{"job":null}},"pakingObj":{"boolean":false,"string":"前端胖头鱼","number":1}}
console.log(JSON.stringify(obj))
// {"name":"前端胖头鱼","age":18,"attr":["coding",123],"date":"2021-10-06T14:59:58.306Z","info":{"age":16,"intro":{"job":null}},"pakingObj":{"boolean":false,"string":"前端胖头鱼","number":1}}

// 3. 测试可遍历对象
let enumerableObj = {}

Object.defineProperties(enumerableObj, {
name: {
value: '前端胖头鱼',
enumerable: true
},
sex: {
value: 'boy',
enumerable: false
},
})

console.log(jsonstringify(enumerableObj))
// {"name":"前端胖头鱼"}

// 4. 测试循环引用和Bigint

let obj1 = { a: 'aa' }
let obj2 = { name: '前端胖头鱼', a: obj1, b: obj1 }
obj2.obj = obj2

console.log(jsonstringify(obj2))
// TypeError: Converting circular structure to JSON
console.log(jsonStringify(BigInt(1)))
// TypeError: Do not know how to serialize a BigInt

复制代码

通过上面测试可以看出,jsonstringify基本和JSON.stringify表现一致,(也有可能测试用例不够全面,欢迎提出一起学习)



作者:前端胖头鱼
链接:https://juejin.cn/post/7017588385615200270

收起阅读 »

通过命令行玩转Git,需要记住那些命令?

Git 简介 什么是 Git ? Git 是目前世界上最先进的分布式版本控制系统!!!什么?啥意思?不懂,没关系,你只要记住,它很重要,非常重要,程序猿的必备技能即可。 Git 的命令非常非常多,这里强调一下,不要傻傻的去背这些命令,没啥卵用,有些命令可能你这...
继续阅读 »

Git 简介


什么是 Git ? Git 是目前世界上最先进的分布式版本控制系统!!!什么?啥意思?不懂,没关系,你只要记住,它很重要,非常重要,程序猿的必备技能即可。


Git 的命令非常非常多,这里强调一下,不要傻傻的去背这些命令,没啥卵用,有些命令可能你这辈子你都未必使得上。


本章的目的是教你如何通过命令行完成 Git 的日常基操,并会适当介绍命令的意义,加深你的理解。


在介绍这些命令之前,我们先来看一张灰常重要的图:


image.png


图中有四个空间,是 Git 工作流程的精髓所在,分别是:



  • Remote: 远程仓库,即你在 Github 或者 Gitee 等平台上创建的项目仓库;

  • Repository: 本地仓库,你可以认为就是我们拉取项目后生成的 .git 文件夹;

  • Index: 暂存区,事实上它只是一个文件,即 .git 文件夹里面的 index 文件,它保存即将提交到本地仓库的文件列表信息;

  • workspace:  工作区,即你在 VS code 或者 WebStorm 编译器正在编写的代码。


Git 基本命令手册


1.克隆/拉取项目


拉取项目是开始搬砖的第一步,一般创建好项目远程仓库,你就能获取到一个 .git 结尾的地址,或者这个地址可能由公司同事给到你,之后你随便找个目录,通过 Git Bash 输入下面命令即可拉取到项目到本地。


git clone 你的项目地址.git

2.查看远程分支


一般拉取完项目后,我们是处于 master/main 主分支,不要着急就去编写代码,先要查看一下项目的远程分支情况,根据要求看是否需要切换到某个分支进行特定开发。


git branch -r

3.查看本地分支


一般本地创建的分支情况应该是和远程分支一一对应,当然,在未进行发布或者和远程分支建立关联,本地分支并不会影响到远程分支,你可以随意创建、删除和修改。


git branch

4.创建本地分支


git branch dev(分支名称, 可以随便取)

通过上面的命令,你会创建好一个本地分支,当然,该分支是基于你当前所在分支创建的。你可以再次敲命令去查看远程分支和本地分支各自的情况(多敲,很快命令就记住了-^〇^-)。


5.删除本地分支


对远程仓库分支并不会有任何影响


git branch -d dev

6.切换分支


创建好本地分支,我们就可以切换到该分支上。


git checkout dev


创建和切换两个操作也可以一起做: git checkout -b xxx (创建并切换到该本地分支)



7.发布本地分支到远程分支


当我们创建好一个本地分支的时候,这个时候还是不要着急去开始编码,我们要把该分支发布到远程仓库去,让远程仓库也拥有该分支,且让它和你本地分支进行关联,方便我们后续直接进行 git pull 或者 git push 操作。


git push origin dev

发布完本地分支后,你可以同样通过 git branch -r 去查看你远程仓库的分支列表是否多了新的分支。


8.建立本地分支与远程分支的关联


本地分支与远程分支关联这步不是必须,但后续就能很方便的直接使用 git pull 或者 git push 获取或提交该分支的代码,而不用去指定分支。


git push --set-upstream origin dev

如果你不关联两者分支的关系,强行去使用,你可能会遇到图中的情况,大致意思就是让你指定目标分支或者去关联分支情况再进行操作。(Git 的提示信息还是很友好的)


image.png


9.添加文件进暂存区


完成上面的几步,我们就能开始搬砖了。在对代码更改后,要把提交代码到远程仓库,我们就要先把代码添加到暂存区,之后提交到本地仓库,最后才能提交到远程仓库。



  • 把工作区某个文件添加进暂存区,比如 src/main.js 文件,则 git add src/main.js


git add xxx(文件路径)
# 多个
git add xxx(文件路径) xxx(文件路径) xxx(文件路径) ...


  • 把工作区更改的所有文件都添加进暂存区


git add .


这里你可能会在想,为什么要先添加到暂存区,再本地仓库之后才提交到远程仓库呢? 如果你是从 SVN 转过来,可能会稍微有点不了解暂存区,这里涉及暂存区的意义,网上有很多解释,这里就不做过多的解释了。

但你可以简单的怎么理解,假如你在开发中开发了用户的添加功能和文章的添加功能,两个功能都同时开发完了,因为都互不影响,完全独立,你想分成两次提交分别写上对应的 commit 信息说明,这个时候就可以使用到暂存区,先将用户添加功能添加到暂存区,然后 commit 到本地仓库,再进行文章添加功能的提交,最后在一起 push 到远程仓库即可。



10.删除文件出暂存区


当你误把文件添加进暂存区,也不要慌,有添加,就肯定有删除。



  • 把工作区某个文件删除出暂存区


git rm --cached xxx(文件路径)


  • 清空暂存区,暂存区实质是 .git 目录下的 index 文件,将此文件删除,那么就认为暂存区被清空。


rm .git/index


当然,这只是把暂存区中跟踪的文件移除而已,不会改动原文件的内容,原先更改的内容还在。



11.查看工作区与暂存区状态


这个命令用于查看工作区和暂存区的状态,能看到哪些文件被修改了,它修改后是否被暂存了,又或者还没有暂存。这个暂存的过程,专业的叫法是 Git tracked,也就是是否被跟踪。


git status

通过下图,我们能查看到所有改动过的文件,绿色的文件是已经添加进暂存区的,红色的文件则是未添加到暂存区的,而且每个文件前都有对应的操作说明,如果是新文件则是 new file,修改的文件则是 modified,删除的是 deleted, 如果是未添加进暂存区的新文件,则没有。


image.png


12.提交暂存区文件到本地仓库


git commit -m "说明信息"

通过上面的命令,我们就将暂存区的文件提交到本地仓库了,我们可以通过 git status 再次查看暂存区的情况。


image.png


Git 的提示真的是非常友好的。


13.查看提交记录


这个命令可以显示提交过的记录。


git log

image.png


进入提交记录日志列表后,可以按 q 退出。



上面的命令会显示所有的 commit 记录,如果你想要显示最近几条记录,你可以通过 git log -n(n为数字, 可以随意指定) 命令完成。



14.提交本地仓库文件到远程仓库


git push

执行这条命令的前提是进行过第7步骤才行哦。


把本地仓库文件提交到远程仓库后,我们再次查看提交日志。


image.png


15.拉取新代码


git pull

这个命令同样也是建立在第7步骤之前的。


Git 进阶命令手册


记住上面15个命令,Git 的日常基操基本也满足了,当然现在各种编辑器功能强大,基本都集成了 Git 的可视化操作,不用命令行来操作,也完全没有问题。


但是程序猿使用命令行不是一件很酷的事情吗?不为别的,只为装13也是可以搞一搞的,哈哈哈。下面再来讲一些命令,虽然使用频率不高,但是也是很重要的。


1.查看全局的Git配置


git config --global -l
# or
git config --global --list

2.查看全局用户名及邮箱


git config --global user.name
git config --global user.email

3.设置全局用户名及邮箱


git config --global user.name "你自己的用户名"
git config --global user.email "你自己的邮箱"

4.查看局部项目用户名及邮箱


要在当前项目根目录下执行。


git config user.name
git config user.email

5.设置局部项目用户名及邮箱


要在当前项目根目录下执行。


git config user.name "你自己的用户名"
git config user.email "你自己的邮箱"

6.删除远程仓库分支


这个命令对于一些未设有保护的分支来说,是挺危险的操作,要慎重执行。


git push --delete origin dev(分支名称)

7.修改远程仓库分支名称


修改远程仓库分支的名称过程是:先修改本地分支名称 - 删除远程分支 - 重新发布本地分支到远程仓库


# 修改本地分支名称
git branch -m 旧分支名称 新分支名称
# 删除远程分支
git push --delete origin 分支名称
# 发布新分支
git push origin 新分支名称
# 重新建立关联
git push --set-upstream origin 新分支名称

8.合并分支


合并分支是一个比较常见的操作了,当你在一个分支开发完新功能后,很多时候这些分支最终都要合并到 master/main 这个主分支上,这个主分支拥有所有分支的代码,并且它是稳定且在生产环境上跑的,所以在你确定要将分支合并到主分支上之前,一定要确保这个分支代码是没有问题。


# 切换到稳定的目标分支
git checkout master
# 更新最新代码,防止本地仓库对应分支代码不够新而出现问题
git pull
# 合并分支到本地仓库
git merge dev
# 发布合并后的新代码到远程分支
git push

合并后,你可以通过 git log 去查看是否有相关的 commit 记录。


9.将A分支直接合并到B分支


git merge xxx(A分支名称) xxx(B分支名称)

10.合并单个commit


有时候我们只想合并某一个 commit 来满足一些特定需要,这也是可以做到的。


# 切换到稳定的目标分支
git checkout master
# 合并某个commit到本地仓库
git cherry-pick xxx(commitId)
# 发布合并后的新代码到远程分支
git push

commitId 是一个 commit 的唯一标识,你可以通过 git log 来找到它,它是一串很长不重复的字符。


image.png


当然,你也可以去到 Github 或者 Gitee 平台对应项目里面找。



合并单个 commit 你可能会遇到冲突的情况,如:在我执行合并某一个 commit 给我报了一个错,大致意思就是代码冲突了,需要去调整后,才能提交。


image.png


代码冲突是件非常蛋疼的事情,不仅仅合并的时候会发生冲突,比较频繁发生的场景是多人共同开发的时候,因为两人负责同个项目,就有极大的可能会改到相同的文件,相同的代码,这就会造成冲突。一般这个时候 Git 会阻止你的提交,让你先去整改,这时候就需要你非常谨慎细心的去处理,因为一旦粗心,就极有可能干掉同伴的代码或者自己的代码,这会造成很严重的后果。(不要问我怎么知道的,都是血的教训︶︿︶)


解决代码冲突一般我会借助编辑器等工具来完成,就不通过命令行操作了,这样比较方便,不容易出错。我使用的是 VS code 编辑器,在编辑器内冲突的文件一般都会标红,点开文件,会发现里面会有写 <<<< 的符号,被符号包围的内容就是冲突的地方。


image.png


编辑器一般会提供四个选项帮助你快速操作,当然你也可以手动删除修改。



  • Accept Current Change:接收当前更改,也就是冲突部分以旧的为准。

  • Accept Incoming Change:接受传入的更改,也就是冲突部分以新的为准。

  • Accept Both Changes:接受两个更改,也就是新旧都存在,可能会出现重复。

  • Compare Changes:比较变化,会分成两个文件,让你更直观的查看两者的冲突内容。


当你解决完冲突后,你可以保存文件,再执行以下操作,提交代码。


# 添加更改进入暂存区
git add .
# 提交暂存区到本地仓库
git commit -m ""
# 提交本地仓库commt到远程仓库
git push


11.撤销最近的一个commit


git reset --soft HEAD^

这个命令用于撤销本地仓库最近的一个 commit ,撤销后会回到暂存区中,通过 git log 可以查看 commit 记录会减少,但不影响远程仓库。


还有另一个相似的撤销 commit 命令,但它比较危险,撤销后的内容是直接就删除的,不会回到暂存区,要慎重使用!!!


git reset --hard HEAD^

12.查看简洁提交记录


这个命令可以让你更加直观的查看你需要的信息。


git log --pretty=oneline

image.png



其实这些信息都存放在 .git 文件夹中,在 .git\logs\refs\heads 下记录了所有分支的 commit 版本号。



13.备份工作区内容stash


这是一个神奇的命令,特别对项目分支比较多的情况,是非常有用的,他的使用场景大致是:当你正在A分支中开发功能,你临时接到一个紧急的需求,需要优先切换到B分支中去开发,这个时候A分支中已经改动的代码要怎么办呢?这时就能使用 stash 来备份代码了。


git stash

执行命令后,当前工作区会回到最近的一次 commit 状态,并将更改的代码保存在 Git 栈中,这样你就能先切换到B分支中去开发,等B分支功能开发完成,再切换回A分支,通过下面的命令取回原来的更改的代码,继续进行A分支的功能开发了。


git stash pop

是不是非常的 nice!!!


14.查看备份列表或清空备份列表



  • 显示Git栈内的所有备份


git stash list


  • 清空Git栈


git stash clear

15.查看远程仓库地址


git remote -v

image.png


16.更改远程仓库地址


git remote set-url origin xxx(新仓库地址)

更改远程仓库地址有另外的两种方式


# 删除项目的远程仓库地址
git remote rm origin
# 添加项目的远程仓库地址
git remote add origin xxx(新仓库地址)

或者去改配置文件


17.比较工作区和暂存区的所有差异


git diff

这个命令是对比工作区和暂存区的所有差异,但个人感觉不太直观,也可能只是比较少用吧,关于对比我更愿意借助编辑器工具来查看。


image.png


对比工作区单个文件和暂存区的区别:


git diff xxx(文件地址)

18.将工作区中的文件还原成与暂存区的一致


git checkout xxx(文件地址)
# 批量还原
git checkout xxx(文件地址) xxx(文件地址) xxx(文件地址) ...


收起阅读 »

优雅的命名

前言 优秀的代码往往是最通俗易懂的代码,在于它的易于维护。在开发过程中,变量/方法优秀的命名往往有助于理解该变量/方法的用途,起到命名即注释的作用。而糟糕的命名往往会让人摸不着头脑。为了提高代码的可维护性,我们需要更优雅的命名方式。 一、通用规则 1. 有意义...
继续阅读 »

前言


优秀的代码往往是最通俗易懂的代码,在于它的易于维护。在开发过程中,变量/方法优秀的命名往往有助于理解该变量/方法的用途,起到命名即注释的作用。而糟糕的命名往往会让人摸不着头脑。为了提高代码的可维护性,我们需要更优雅的命名方式。


一、通用规则


1. 有意义


起一个有意义的变量名这条相信绝大多数开发者都能做到,即变量名有实际的指代意义,在此不再赘述。


2. 指代具体


命名时需要使其更加具体详尽,可以具体到所在的模块,或者能表达出其逻辑/功能。


/* bad */
.title {}
/* good */
.head-title {}

// bad
const info;
// good
const userInfo;

3. 遵循传统


无论是CSS、JavaScript、还是文件的命名,都有一些约定俗成的惯例和规范,我们只需遵循即可。


4. 约定规范


命名的规则有很多种,没有高低之分,只有相对合适,没有绝对完美的规则。通常为了维持项目的风格统一,通常在一个项目中,相同种类的规则只选取一种。毕竟规范也只是一种工具,运用规范的根本目的是为了更好的开发和维护,太过复杂的规范反而会阻碍正常开发。因之,在项目启动前,在技术栈选取后就应当进行规范的约定,这个过程是团队意见的整合,毕竟规范是要靠团队成员的相互配合。


二、CSS 中的命名


1. 划分原则


CSS中的类名根据其职责可以分为公共类名和自定义类名。其中公共类名又可以分为常见类名(一般是约定俗成)和工具类名。


2. 常见类名


下面整理了一些常见的 css类名 供大家参考:








收起阅读 »

❤️谈谈grid布局(细读必有收获)

grid布局的理念是把网页划分成一个一个网格组合成不同样式的布局,再通过对网格进行内容填充,组成一个网页。通过一下这个案例了解grid的基本概念👇👇 经典九宫格布局: 🚨关键点🚨: 容器: 需通过display:grid设置为grid容器,容器中包含所有i...
继续阅读 »

grid布局的理念是把网页划分成一个一个网格组合成不同样式的布局,再通过对网格进行内容填充,组成一个网页。通过一下这个案例了解grid的基本概念👇👇


经典九宫格布局:
image.png



🚨关键点🚨:

容器: 需通过display:grid设置为grid容器,容器中包含所有item

行: 横向为行,对应颜色块123

行距: 上下两个item的间距为行距

列: 纵向为列,对应颜色块147

列距: 左右两个item的间距为列距

item(子元素): 也就是上图对应的123456789颜色块

边: 每个itme共有 上 下 左 右 四条边



1.1 display


display属性规定是否/如何显示元素。我们需要使用grid布局,就要把容器设置为grid或者inline-grid

grid 设置为块级元素的grid布局

inline-grid 设置为行内元素的grid布局

区别如下:


image.png
代码案例
在线代码入口:👉👉(点击传送)


.grid_container {
display:grid;
/* display:inline-grid; */
grid-template-columns: 100px 100px 100px;
grid-template-rows: 100px 100px 100px;
}

1.2 grid-template-columnsgrid-template-rows


grid-template-columns属性用来定义grid布局的每一列列宽

grid-template-rows属性用来定义grid布局的每一行行高

代码案例1:在线代码入口👉👉(点击传送)

定义一个三行三列,每列列宽100px,每行行高100px


.grid_container {
display:grid;
grid-template-columns: 100px 100px 100px;
grid-template-rows: 100px 100px 100px;
}

代码案例2:在线代码入口👉👉(点击传送)

当拥有很多行和列的时候,普通的写法根本不实在,所以现在引入一个函数repeat()

repeat()函数可设置重复的值,或者重复的一个模式,还是以三行三列100px为例:


.grid_container {
display:grid;
/* 重复一个值 */
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 重复一个模式
grid-template-columns: repeat(3,100px 50px);
grid-template-rows: repeat(3,100px 50px);
*/

}

代码案例3:在线代码入口👉👉(点击传送)

这里以圣杯布局为例:左右固定,中间自适应。在这种情况下固定的列宽或行高已经不能满足实现圣杯布局了,所以这个例子引入两个关键字autofr

auto:自适应属性

fr:fraction 的缩写,意为"片段",可以看做百分比属性,通过以下例子可以帮助理解该关键字

auto为例:


.grid_container {
display:grid;
/* 左右列150px,中间列自适应*/
grid-template-columns: 150px auto 150px;
/* 一行行高 300px*/
grid-template-rows: repeat(1,300px);
}

image.png

fr为例:
左右列占比 2/10 = 20% ,中间列占比 6/10 = 60%, 注意10 = 2+6+2


#grid_container{
display: grid;
grid-template-columns: 2fr 6fr 2fr;
grid-template-rows: repeat(1,300px);
}

image.png

代码案例4:在线代码入口👉👉(点击传送)

当需求是要求每个item子元素的宽高只有100px,但是容器宽度自适应时,我们就无法得知应该设置几行几列的属性了,所以这里再引入一个关键字auto-fill

auto-fill:自动填充

⚠️注意:grid-template-rows需要使用关键字时,容器必须要有固定高度⚠️


#grid_container{
display: grid;
height:500px;
grid-template-columns: repeat(auto-fill,100px);
grid-template-rows: repeat(auto-fill,100px);
}

代码案例5:在线代码入口👉👉(点击传送)

如果grid布局的子元素设置为自适应宽度,但宽度缩小到一定程度时就会出现错误,所以避免出现这种错误,我们必须要有一个最小的宽度,所以这里引入一个函数minmax()

minmax():设置一个长度范围,参数1:最小值,参数2:最大值

例子:最小值500px,最大值6fr


.grid_container {
display:grid;
width:600px;
grid-template-columns: 2fr minmax(500px,6fr) 2fr;
/* 自行屏蔽查看区别 */
/* grid-template-columns: 2fr 6fr 2fr; */
grid-template-rows: repeat(1,300px);
}

1.3grid-template-areas


1.3 grid-template-areas


grid-template-areas:用于划分区域,通过以下案例可以帮助理解

代码案例1:在线代码入口👉👉(点击传送)

1、划分出ai九个区域

2、或者每一行划分一个区域,三行就是a b c三个区域

2、当然可以不划分部分区域,使用(.)点表示不需要划分的区域


.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 划分九个区域 */
grid-template-areas:
'a b c'
'd e f'
'g h i';

/* 划分三个区域 */
/* grid-template-areas:
'a a a'
'b b b'
'c c c'; */
/* 不划分部分区域 */
/* grid-template-areas:
'a . c'
'a . c'
'a . c'; */
}


划分区域的用途会在后面结合其他的属性进行讲解!!



1.4 grid-row-gapgrid-column-gapgrid-gap


grid-row-gap:行间距

grid-column-gap:列间距

grid-gap: 行间距 和 列间距 的简写形式,如:grid-gap: <grid-row-gap> <grid-column-gap>;


代码案例1:在线代码入口👉👉(点击传送)

这里以最简单的九宫格为例


.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
grid-row-gap:10px;
grid-column-gap:20px;
/* 下面语句和上面设置的间距效果相同,自行解除注释对比 */
/* grid-gap:10px 20px; */
}

image.png


1.5 grid-auto-flow


grid-auto-flow:设置grid布局的放置顺序,正常情况下是,从左到右放置每一个item子元素,在特殊情况下我们可以重新改变它的放置顺序,比如从上到下。可选值:从左到右 row、从上到下column、稠密从左到右row dense、稠密从上到下column dense,接下来会一一举例说明;

正常设置grid-auto-flow属性为 rowcolumn会出现以下两种效果,左边为row,右边为column


image.png
image.png


这里还是以九宫格为例,我们将 数字1数字2数字3 方块设置为各占2个单元格时,在grid-auto-flow属性默认等于row就会出现以下一幕
image.png

当我们把代码设置成 稠密型的从左到右row dense时,布局就会被尽可能的填满,不会出现上图存在的空格


image.png

代码如下:在线代码入口👉👉(点击传送)


.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 默认,从左到右 */
grid-auto-flow:row;
/* 稠密型 从左到右 请自行开启屏蔽 */
/* grid-auto-flow:row dense; */
}
.item-1 {
background-color: #B53471;
grid-column-start: 1;
grid-column-end: 3;
}
.item-2 {
background-color: #ffcccc;
grid-column-start: 1;
grid-column-end: 3;
}
.item-3 {
background-color: #ff4d4d;
grid-column-start: 1;
grid-column-end: 3;
}


通过上面的例子可以清楚稠密型其实就是,尽可能填满容器而已,所以column dense例子就不多做解析,在线代码入口👉👉(点击传送)



image.png
image.png


1.6 justify-itemsalign-itemsplace-items


属性说明

justify-items:设置item子元素内容水平位置

align-items:设置item子元素内容垂直位置

place-items:align-itemsjustify-items 两个属性的简写方式,若省略第二个值,则认为第二个值等于第一个值


place-items: <align-items> <justify-items>

属性可选值(三个属性均有以下可选值)


start案例:在线代码入口👉👉(点击传送)

对齐子元素容器的起始边框,justify-items对齐水平的起始边框,align-items对齐垂直的起始边框

image.png

end案例:在线代码入口👉👉(点击传送)

对齐子元素容器的结束边框,justify-items对齐水平的结束边框,align-items对齐垂直的结束边框

image.png

center案例:在线代码入口👉👉(点击传送)

子元素容器内部居中,justify-items水平居中,align-items垂直居中

image.png

stretch案例:在线代码入口👉👉(点击传送)

默认就是这个属性,只要不设置宽度和高度就会把宽高拉伸铺满

image.png


1.7 justify-contentalign-contentplace-content



注意这三个属性和1.6描述的区别在于, justify-itemsalign-itemsplace-items是针对子元素内容的,justify-contentalign-contentplace-content是针对grid容器内容的



属性说明

justify-content:设置grid布局容器内容水平位置

align-content:设置grid布局容器内容垂直位置

place-content:align-contentjustify-content 两个属性的简写方式,若省略第二个值,则认为第二个值等于第一个值


place-content: <align-content> <justify-content>

属性可选值(三个属性均有以下可选值)







































可选值可选值说明
start对齐grid容器的起始边框
end对齐grid容器的结束边框
centergrid容器内部居中
stretchgrid容器内容大小没有指定时,拉伸占据整个grid容器
space-around每个子元素两侧间隔相等,所以子元素之间间隔比容器边框的间隔大一倍
space-between子元素与子元素之间间隔相等,子元素与容器边框没有间隔
space-evenly子元素与子元素之间间隔相等,子元素与容器边框之间也是同样长度的间隔

start案例:在线代码入口👉👉(点击传送)

对齐容器的水平和垂直起始边框,justify-content对齐水平的起始边框,align-content对齐垂直的起始边框


  justify-content:start;
align-content:start;

image.png


end案例:在线代码入口👉👉(点击传送)

对齐容器的水平和垂直结束边框,justify-content对齐水平的结束边框,align-content对齐垂直的结束边框


  justify-content:end;
align-content:end;

image.png




center案例:在线代码入口👉👉(点击传送)

容器内容水平和垂直居中对齐,justify-content容器内容水平居中对齐,align-content容器内容垂直居中对齐


  justify-content:center;
align-content:center;

image.png




stretch案例:在线代码入口👉👉(点击传送)

自动拉伸铺满grid容器,justify-content水平铺满容器,align-content垂直铺满容器


  justify-content:stretch;
align-content:stretch;

image.png




space-around案例:在线代码入口👉👉(点击传送)

每个子元素两侧间隔相等,所以子元素之间间隔比容器边框的间隔大一倍


  justify-content:space-around;
align-content:space-around;

image.png




space-between案例:在线代码入口👉👉(点击传送)

子元素与子元素之间间隔相等,子元素与容器边框没有间隔


  justify-content:space-between;
align-content:space-between;

image.png


space-evenly案例:在线代码入口👉👉(点击传送)

子元素与子元素之间间隔相等,子元素与容器边框之间也是同样长度的间隔


  justify-content:space-evenly;
align-content:space-evenly;

image.png


1.8 grid-auto-columnsgrid-auto-rows


grid-auto-columns:设置多余列的列宽

grid-auto-rows:设置多余行的行高

在某种情况下,我们设置了9宫格布局可能会出现10个item子元素,那正常的前9个子元素都设置有合适的宽高,但是多余出现的第10个如果不进行设置,就会出现不正常的布局,通过以下案例可以帮助理解


image.png

当使用 grid-auto-flow:column;改变默认的放置顺序会出现以下情况
image.png

所以在出现以上情况时,使用grid-auto-columnsgrid-auto-rows解决问题
在线代码入口👉👉(点击传送),自行修改案例代码观察变化。


.grid_container {
grid-auto-columns:100px;
grid-auto-rows:100px;
}

image.png


1.9 grid-templategrid


grid-template属性是grid-template-columnsgrid-template-rowsgrid-template-areas这三个属性的合并简写形式。

grid属性是grid-template-rowsgrid-template-columnsgrid-template-areas、 grid-auto-rowsgrid-auto-columnsgrid-auto-flow这六个属性的合并简写形式。



这两个属性用法比较复杂,后期再考虑重新写一篇文章讲解,有需要的请在评论区留言,留言数多的话,会尽快出新文章



2.0(子元素)grid-column-startgrid-column-endgrid-row-startgrid-row-endgrid-columngrid-row


横纵向网格线始终比横纵向子元素多1,下面通过几个案例帮助理解
image.png


案例1:在线代码入口👉👉(点击传送)

🥇当方块一想占满横向两个方格时,将方块一的grid-column-startgrid-column-end分别设置成13,或者设置grid-column: 1/3

🥈当方块一想占满纵向两个方格时,将方块一的grid-row-startgrid-row-end分别设置成13,或者设置grid-row: 1/3


.item-1 {
background-color: #B53471;
/* 横向 */
/* grid-column-start: 1;
grid-column-end: 3; */
grid-column: 1/3; /*效果相同 */

/* 纵向 */
/* grid-row-start: 1;
grid-row-end: 3; */
grid-row: 1/3; /*效果相同 */
}

image.png

案例2:在线代码入口👉👉(点击传送)

🥇当遇到多个方格进行属性设置时,需要考虑网格线是否被别的元素包含,如下图所示:
image.png

所以在案例1的原有基础上,我们想把方块2的纵向占两个方块,位置放在原方块4原方块7的位置,那么我们就要考虑方块1已经包含过的网格线不能使用。所以设置上边框网格线的的时候就要避开纵向的第2条网格线,这样我们要设置上边框网格线为3,下边框网格线为5


.item-2 {
background-color: #ffcccc;
grid-column: 1/2;
grid-row: 3/5;
}

效果如下:

image.png


2.1 (子元素)justify-selfalign-selfplace-self



其实这一节没啥好讲的,属性justify-itemsalign-itemsplace-items 属性效果一样,只不过前者是统一设置grid容器中的子元素内容位置,后者则是在子元素上单独设置,并且会覆盖统一设置的效果。



justify-self:设置水平位置

align-self:设置垂直位置

place-selfalign-self属性和justify-self属性的合并简写形式。(忽略第二个值,则认为第二个值等于第一个值)

案例1:在线代码入口👉👉(点击传送)

所有子元素内容水平垂直居中,第一个子元素内容对齐垂直方向结束边框align-self: end;,对齐水平方向结束边框justify-self: end;


代码和效果如下:justify-selfalign-self 覆盖了justify-itemsalign-items 设置的居中显示


.grid_container {
justify-items: center;
align-items: center;
}
.item-1 {
justify-self:end;
align-self:end;
background-color: #B53471;
}

image.png


2.1 (子元素)grid-area















属性名属性说明
grid-area指定子元素防止在哪个区域

在上面 1.3 中已经说过如何划分区域了,接下来我们通过 grid-area 属性来了解如何使用区域

案例1:在线代码入口👉👉(点击传送)

将就九宫格中1 2 3 方块替换到 4 5 6方块


.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 划分九个区域 */
grid-template-areas:
'a b c'
'd e f'
'g h i';
}
.item-1 {
background-color: #B53471;
grid-area: d;
}
.item-2 {
background-color: #ffcccc;
grid-area: e;
}
.item-3 {
background-color: #ff4d4d;
grid-area: f;
}

image.png


案例2:在线代码入口👉👉(点击传送)

将九宫格中的方块1 2 3 纵向占满两个单元格,方块4 水平占满3个单元格


.grid_container {
display:grid;
grid-template-columns: repeat(3,100px);
grid-template-rows: repeat(3,100px);
/* 划分三个区域 */
grid-template-areas:
'a b c'
'a b c'
'd d d';
}
.item-1 {
background-color: #B53471;
grid-area: a;
}
.item-2 {
background-color: #ffcccc;
grid-area: b;
}
.item-3 {
background-color: #ff4d4d;
grid-area: c;
}
.item-4 {
background-color: #ffaf40;
grid-area: d;
}

image.png


作者:是舍长
链接:https://juejin.cn/post/7017074528752762911

收起阅读 »

构建大型前端业务项目的一点经验

目前工作中接手的几个项目都是 B端 PC 项目,业务逻辑都比较复杂,并且代码历史较久,在日常的维护中经常会遇到想摊手的技术问题,发现问题、解决问题、避免再次出现同样的问题,既是项目可持续维护的因素之一,也是个人工作经验积累的一个过程 本文可当做 接手前端新项...
继续阅读 »

目前工作中接手的几个项目都是 B端 PC 项目,业务逻辑都比较复杂,并且代码历史较久,在日常的维护中经常会遇到想摊手的技术问题,发现问题、解决问题、避免再次出现同样的问题,既是项目可持续维护的因素之一,也是个人工作经验积累的一个过程



本文可当做 接手前端新项目?这里有些注意点你可能需要留意一下编写可维护的现代化前端项目 的补充



具体、连贯的变量名


在前后端分离的现代化 web开发流程下,相比于以往,前端承担了更多的业务逻辑,尽管存在着 TypeScript等约束工具,但相比于后端语言,js 仍具备相当大的灵活性,这就导致了代码一旦复杂,前端代码的排查会更加麻烦


单一变量的层层传递与到处使用是很常见的事情,变量的命名对于追踪变量有着相当大的影响


所以变量名称必须是具体且有实际意义的,不提倡为了追求变量名的精确性而使得变量名称冗长,但模糊而宽泛的变量名同样不可取,这就要求变量名称即准确又简短,在某些时候,可能很难做到这一点,个人倾向是,实在无法做好权衡的前提下,宁愿选择冗长的变量名也不要选择一个模糊宽泛的


例如,data可以当做是一个变量名,这个变量名用于临时的局部变量没啥问题,毕竟你一眼就能看到这个变量所有的使用范围,但如果变量所持有的数据的作用范围较大(例如跨组件)且具备实际业务意义,那么就不太妙了,data 可以用作任何数据的变量名,当需要追踪 data的时候,在编辑器里搜索 data,发现到处都是 data,并不是一件美好的事情


一旦确定好了变量名后,最好不要再对其进行重命名,例如想将 A组件里的 userData 传递到 B 组件中,B组件最好原模原样地接收这个变量名,而不是将其命名为其他的什么名称


有些时候可能就必须要在传递变量的时候进行重命名,例如第三方组件就接收 data,那么你无法传递 userData,这种情况下当然不得不重命名


避免对变量重命名的目的主要是,为了防止在追踪变量的时候因变量名称改变而产生思维上的重新整理,一个变量被重命名了好几次后,追踪到底再回过头来你可能就忘记了自己当初在追踪什么东西,同时对于搜索功能也不太友好,从一而终的连贯变量名可以让你一次搜索就能从追踪的起始位置跳过中间一大堆逻辑直接到终点


必要的选择器


现代化的前端项目基本都会使用 ReactVue等数据驱动的框架,UI组件一般也都是使用别人封装好的组件库,除非是要写样式,否则html元素选择器基本上都是可有可无的了,但并不是说就不需要了,最起码的一个好处是,让你想在代码里查找页面上的一个元素时,直接根据选择器名就能精准定位了


页面上有个弹窗展示得不太对,你在浏览器页面里看到这个弹窗元素名叫 ant-modal-wrap,是个第三方的组件所以你代码里根本搜不到这个选择器名;页面上有一句文案有点问题,你在浏览器页面里看到这个文案所在的元素是个没有选择器的 div标签,在目前普遍 全站div的浪潮下,光是定位这个文案到底是哪里的就够花费好一阵子了


所以,这里选择器是起到了一个索引的作用,既然是索引,那么同样应该遵守上面变量名相关的规则,即选择器名称应当 即准确又简短


优化应该从一开始就开始


不要提前优化


相信很多人都听过这句话,我曾经将这句话当做是至理名言,但经历的多了之后,目前已经开始有所质疑了


不要提前优化,那么要在什么时候优化?快要 hold 不住的时候才优化?迭代了七八十版的时候再优化?团队人员换了一茬又一茬的时候再优化?


当然是可以的,但是那个时候谁来优化,谁来做这种在业务看来毫无产出甚至是可能优化出 bug的吃力不讨好的事情?


一个函数里塞了数十层的 if...else,函数体的代码量超过千行,看着就应该要被优化的,但是这些代码在这里绵延了数年之久,经过了一批又一批不同程序员的修改,承载了不知多少明面上暗地里的业务逻辑,技术上或许好优化,但谁能保证某处优化不会对业务逻辑造成破坏?


没有提前优化,过程中也没有优化,那就完全是没有任何优化了,因而屎山就诞生了


我认为 不要提前优化 这句话是产生在一个朝九晚五不加班需求少有充足时间做技术优化的语境之下,这种语境下,这句话是没啥问题的,只是大部分情况下,现实情况根本不符合语境,所以这句话就有问题了


该拆的组件、该提取的公共方法、该规划的目录结构、该引入的代码规范……应该从一开始就形成,不要等着需要优化的时候才优化,那个时候已经来不及了


复用(组件、方法)


代码复用是为了提升工作效率,但如果只是为了复用代码而复用,就本末倒置了


通用方法、通用组件鼓励复用,但业务逻辑、业务组件,慎重复用


一个常见的例子是,移动端详情页页面和编辑页可能具有大部分重合的逻辑,但类似这种业务属性很强的组件,除非你确信这个组件将来不出现大的改动,否则不要为了贪图眼前的便利而想当然地进行复用


本来为了区分展示态和编辑态,就已经写了一些条件语句了,日后若是出现了已经复用的逻辑必须要按照业务需求进行拆分,甚至是逻辑完全南辕北辙,初期还好,或许还能抢救一下拆分出来,但到了中后期才发现这个问题很可能已经晚了,掺杂了那么多的业务逻辑,你还敢去做拆分吗?那么这个复用组件的代码量必然要被大量的 if...else 占领,修改任何一个功能点、排查任何问题都要兼顾两套逻辑,对于维护者来说,这会造成相当大的心智负担,对于项目本身来说,维护的代码将会变得更大


业务代码是千变万化的,原本多个场景下相似的逻辑,很可能随着业务的迭代变得毫无关系,在这种场景下,复用不仅不能提高工作效率,反而还会拖后腿


而对于通用方法和通用组件来说,为了更加彻底地解耦,其应当是函数式的,不应当对外部状态产生隐式地修改


通用方法最好是纯函数,相同的输入有相同的输出,入参、出参都应当是明确的,让人一眼就看出来需要哪些入参,又会有哪些出参,而不是直接传入一个大的对象,然后在方法体内去一个个查找所需的对象属性


通用组件不应当自作主张修改外部数据,而应该将产生的变化主动抛出去,让上一层组件来明确决定如何使用这个变化


依据社区而不是从心


为项目选择设计模式、UI组件库、状态管理库等基础功能的时候,应当选取社区内热度更高的而非根据个人的喜好


你所认为很牛x的设计模式、第三方库等,可能是其他人根本就没听过的,或者其他人根本就不认同的,这只会增加团队之间的协作难度


团队合作项目的代码是用来传承的而不是用来炫技的


抛弃惯性思维


待在舒适区,这是人之本能,因为熟悉,所以上一个项目使用的技术栈在下一个项目里也要继续用


但是,真的合适吗?


上一个项目用了 mobx,这个项目里也必须要用吗?上一个项目里将所有的状态数据都放到了状态管理库里,这个项目也要这样做吗?上一个项目没用 TypeScript,这一个也不用吗?


可能是不需要的,可能是需要更换的,当然,并不是说就要跟上一个项目反着来,到底怎样最起码要有一个思考的过程,而不是上一个项目就是这样的,所以这一个也要这样


考虑清楚了再写TODO


有意识做优化是个好习惯,但意识得能落到实处


以我的经验看,在多人协作的、业务敏捷迭代的项目中,大多数 todo是无法完成的


大部分 todo都是基于当时的情况做出的考量,当时这个方法可能只有几行,todo 要做的时候很简单,但是当时没有做,当过了一段时间再想起来这事的时候,发现那个方法已经变成了几百行了,你还敢动吗?


或者换句话说,你还有完成这个 todo的心思吗?人都是懒惰的,你愿意将原本可以用在打游戏刷视频的时间用在完成这个 todo上吗?看到了别人写的 todo,并且也看明白了,但是你愿意帮别人完成这个 todo吗?


该做的事情应当立即完成,或许因为某些原因无法立即完成,所以你想延后再来做,但是一般情况下,后续再来完成的成本必然大于当下,现在都完成不了,凭什么认为以后就能完成?


真的需要做的事情,哪怕会让进度延期,只要你理由充分,其他人不可能也没理由去阻止你


小结


很多时候,一些让你能够写出更好的代码建议,实际上对于业务产出是毫无帮助的,哪怕你不遵守这些建议甚至反着来,也不影响你的产出不影响你的绩效,毕竟产出和绩效跟代码写得好不好并没有直接关系,甚至这些所谓的建议有时候还会影响你快速产出,只要我能拿出一个好的产出拿到一个好的绩效,代码写得糙点烂点又有什么关系?以后的事情以后再说呗,搞不好以后维护的人根本不是我


这种心理或许才是常态,毕竟这更加符合现实的利益


但如果你是一位对技术有追求的人,你真的甘心就如此吗?我认为除了现实的考量之外,还应当为自己写下的代码负责


作者:清夜
链接:https://juejin.cn/post/7016948081321050148

收起阅读 »

npm install之后发生了什么

  下载项目后,执行的第一个命令行一般都是 npm install 。在这个过程中可能一帆风顺,也可能遇到大大小小的报错,有时候花点时间各种搜索能解决,可下次遇到还是一头雾水的上网找各种方案尝试解决报错。   那么,你清楚当你输入 npm instal ,按下...
继续阅读 »

  下载项目后,执行的第一个命令行一般都是 npm install 。在这个过程中可能一帆风顺,也可能遇到大大小小的报错,有时候花点时间各种搜索能解决,可下次遇到还是一头雾水的上网找各种方案尝试解决报错。


  那么,你清楚当你输入 npm instal ,按下 Enter 键之后,究竟发生了什么吗?


正文


一、npm install之后发生了什么


  npm install 大概会经过以下几个流程,下面我们就来简单看一下(原图地址)。


install.jpg



  1. npm install执行后,会检查并获取npm配置,优先级为



项目级别的.npmrc文件 > 用户级别的.npmrc文件 > 全局的.npmrc文件 > npm内置的.npmrc文件



.npmrc 文件就是npm的配置文件。查看npm的所有配置, 包括默认配置,可以通过下面的命令:


npm config ls -l


  1. 然后检查项目中是否有package-lock.json文件。


  从npm 5.x开始,执行npm install时会自动生成一个 package-lock.json 文件。


package-lock.json 文件精确描述了node_modules 目录下所有的包的树状依赖结构,每个包的版本号都是完全精确的。


  因此npm会先检查项目中是否有 package-lock.json 文件,分为两种情况:



  • 如果有,检查package-lock.jsonpackage.json中声明的依赖是否一致



  • 一致:直接使用 package-lock.json 中声明的依赖,从缓存或者网络中加载依赖

  • 不一致:各个版本的npm处理方式如上图



  • 如果没有,根据package.json递归构建依赖树,然后根据依赖树下载完整的依赖资源,在下载时会检查是否有相关的资源缓存



  • 存在:将缓存资源解压到 node_modules

  • 不存在:从远程仓库下载资源包,并校验完整性,并添加到缓存,同时解压到 node_modules



  1. 最终将下载资源包,存放在缓存目录中;解压资源包到当前项目的node_modules目录;并生成 package-lock.json 文件。


  构建依赖树时,不管是直接依赖还是子依赖,都会按照扁平化的原则,优先将其放置在 node_modules 根目录中(最新的npm规范), 在这个过程中,如果遇到相同的模块,会检查已放置在依赖树中的模块是否符合新模块的版本范围,如果符合,则跳过,不符合,则在当前模块的 node_modules 下放置新模块。


二、npm缓存


  在执行 npm installnpm update 命令下载依赖后,除了将依赖包安装在 node_modules 目录下外,还会在本地的缓存目录缓存一份。我们
可以通过以下命令获取缓存位置:


// 获取缓存位置
npm config get cache

// C:\Users\DB\AppData\Roaming\npm-cache
复制代码

  如我的缓存位置在C:\Users\DB\AppData\Roaming\npm-cache下面的_cacache 文件夹中。


  再次安装依赖的时候,会根据 package-lock.json 中存储的 integrity、version、name 信息生成一个唯一的 key,然后拿着key去目录中查找对应的缓存记录,如果有缓存资源,就会找到tar包的hash值,根据 hash 再去找缓存的 tar 包,并把对应的二进制文件解压到相应的项目 node_modules 下面,省去了网络下载资源的开销。


  因此,如果我们可能因为网络原因导致下载的包不完整,这就可能造成删除node_modules重新下载的依旧是问题包,假如删除 node_modules 重新下载问题依旧,此时就需借助命令行清除缓存。


// 清除缓存
npm cache clean --force

复制代码

  不过 _cacache 文件夹中不包含全局安装的包,所以想清除存在问题的包为全局安装包时,需用 npm uninstall -g 解决


三、关于yarn


yarn简介:


  yarn是由Facebook、Google、Exponent 和 Tilde 联合推出了一个新的 JS 包管理工具 ,正如官方文档中写的,Yarn 是为了弥补 npm 的一些缺陷而出现的。


yarn特点:



  • 速度快



  • yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。



  • 安全

    • 在执行代码之前,yarn 会通过算法校验每个安装包的完整性。



  • 可靠

    • 使用详细、简洁的锁文件格式和明确的安装算法,yarn 能够保证在不同系统上无差异的工作。




四、yarn和npm部分命令对比



总结


  无论是使用npm 还是 yarn 来管理你的项目依赖,我们都应该知其然更知其所以然,这样才能在项目中跟海的定位及解决问题,不是吗?




链接:https://juejin.cn/post/7016994983186006024

收起阅读 »

进来聊聊!Vue 和 React 大杂烩!

相信应用层面的知识,大家都比较熟悉了,实际 React 用来实现业务对于熟悉 Vue 的开发人员来说也不是难事,今天我们简单的了解一下 React 和 Vue 。(瞎聊聊) 先来两张源码编译图对比一下: 由于每个步骤能涉及的东西太多,所以本篇就简单聊一下他...
继续阅读 »

相信应用层面的知识,大家都比较熟悉了,实际 React 用来实现业务对于熟悉 Vue 的开发人员来说也不是难事,今天我们简单的了解一下 React 和 Vue 。(瞎聊聊)


先来两张源码编译图对比一下:


image.png


image.png


由于每个步骤能涉及的东西太多,所以本篇就简单聊一下他们的区别以及他在我们项目中实际的应用场景中能够做什么(想到什么聊什么)。


Vue


new Vue


我们知道 Vue 和 React 都是通过替换调指定的 Dom 元素来渲染我们的组件,来看一下:


import Vue from 'vue'
import App from './App.vue'

new Vue({
render: h => h(App),
}).$mount('#app')

先说 Vue 的,new Vue 做了什么?相信读过源码的同学都会知道,他执行了一堆初始化操作 initLifecycle、initEvents、initRender、initInjections、initState、initProvide


具体包括以下操作:选项合并(用户选项、默认选项)、$children$refs$slots$createElement等实例属性和方法初始化、自定义事件处理、数据响应式处理、生命周期钩子调用、可能的挂载。


响应式原理


当一个 Vue 实例被创建时,它将 data 对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。


var data = {
a: 1
}
var vm = new Vue({
data
})
vm.a = 1
data.a // 1

data.a = 2
vm.a // 2

Vue 通过劫持 get 和 set 来实现响应式原理,这也是与 React 最大区别所在,React 只能手动调用 this.setState来将state改变。


我在往期篇幅有具体谈过 Vue 的响应式原理:
深入浅出Vue响应式原理


模板编译 && 视图渲染


当 data 中的数据实现了响应式之后,就开始在模板上做功夫了。


这里有一个很重要的东西叫虚拟 Dom。


所谓虚拟 DOM 就是用 js 来描述一个 DOM 节点,在 Vue 中通过 Vnode 类来描述各种真实 DOM 节点。


在视图渲染之前,把 template 先编译成虚拟 Dom 缓存下来,等数据发生变化需要重新渲染时,通过 diff 算法找出差异对比新旧节点(patch),之后把最终结果替换到真实 Dom 上,最终完成一次视图更新。


了解更多关于 diff 移步至:diff算法


1632467217.jpg


关于编译原理要细聊就有点多了,大致总结一下:



  • 第一步是将 模板字符串 转换成 AST语法树(解析器)

  • 第二步是对 AST 进行静态节点标记,主要用来做虚拟 DOM 的渲染优化(优化器)

  • 第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)


有兴趣请移步至:
Vue 模板编译原理


生命周期


在这些过程中,Vue 会暴露一些钩子函数供我们在适当时机去执行某些操作,这就是生命周期钩子函数。关于 Vue 的生命周期大家应该都熟记于心了,简单过一下:




  • beforeCreate (创建实例前的钩子,此时 data 里的数据还不能用。)




  • created (实例创建完成后的钩子,此时 data 已完成初始化可使用,但 Dom 尚未挂载。)




  • beforeMount (将编译完成的 HTML 挂载到对应虚拟 Dom,此时页面并无内容。)




  • mounted (Dom 已完成挂载,此时可以操作 Dom,此阶段也可以调用接口等操作。)




  • beforeUpdate (更新之前的钩子,当data变化时,会触发beforeUpdate方法。基本上没有什么用处。)




  • updated (更新之后的钩子,当数据变化导致地虚拟DOM重新渲染时会被调用,被调用时,组件DOM已经更新。建议不要在这个钩子函数中操作数据,可能陷入死循环。)




  • beforeDestory (实例销毁前的钩子,此时还可以使用 this,通常在这一步会进行清除计时器等操作)




  • destoryed (实例销毁完成的钩子,调用完成后,Vue实例的所有内容都会解绑定,移出全部事件监听器,同时销毁所有的子实例。)




React


大家可能会比较关心 React 会扯什么(猜的),毕竟 Vue 已经是家喻户晓,加上国内业务使用也是居多,生态圈及各类解决方案也是层出不穷。


ReactDOM.render


ReactDOM.render 是 React 的最基本方法用于将模板转为 HTML 语言,并插入指定的 DOM 节点。


import App from './App.jsx'
import ReactDOM from 'react-dom'

ReactDOM.render(
<App></App>,
document.getElementById('root')
)

render 方法实际是调用了内部的 React.createElement 方法,进而执行 ReactMount._renderSubtreeIntoContainer


还有一个方法 ReactDOM.unmountComponentAtNode() 作用和 ReactDOM.render() 正好相反,他是清空一个渲染目标中的 React 部件或 HTML。


React state


state 是 React 中很重要的东西,说到 state 就不得不提到 setState 这个方法,很多人认为 setState 是异步操作,其实并不是。之所以会有一种异步的表现方式是因为 React 本身的性能机制导致的。因为每次调用 setState 都会触发更新,异步操作是为了提高性能,将多个状态合并一起更新,减少 render 调用。


image.png


如图,setState 接受一个新状态并不会立即执行,而是存入 pending 队列中进行判断。


如果有阅读过源码的同学就会知道他在其中通过判断 isBatchingUpdates (是否是批量更新模式)来进行区分。


如果是,那就会将状态保存到 dirtyComponents (脏组件)。


如果否,那就遍历所有的脏组件,并调用 updateComponent 更新 pending 队列的 state 或 props。执行完后,将
isBatchingUpdates 设置为 true。


假如有如下代码:


for ( let i = 0; i < 100; i++ ) {
this.setState( { count: this.state.count + 1 } );
}
复制代码

若 setState 是同步机制,那么这个组件会被 render 100次,这无疑对性能是毁灭性的。


当然 React 也想到了这个问题并做了处理:



React 会将 setState 的调用合并为一个执行,所以 setState 执行时我们并没有看到 state 马上更新,而是通过回调获取到更新后的数据(有点类似 Vue 中的 nextTick),也就是刚刚上图所叙。



React 渲染流程


对于首次渲染,React 的主要是通过 React.render 接收到的 VNode 转化为 Fiber 树,并根据树的层级关系构建出 Dom 树并渲染。


而二次渲染(更新),Fiber 树已经存在内存中了,所以 React 会计算 Fiber 树中的各个节点差异(diff),并将变化更新渲染。


实际上 Vue 和 React 的 diff 算法都是同层 diff,复杂度都为O(n),但是他们的不同在于 React 首位节点是固定不动的(除了删除),然后依次遍历对比。


Vue 的 diff 在 compile 阶段的 optimize 标记了 static 点,可以减少 diff 次数,而且是双向遍历方法,并且借鉴了开源库 snabbdom。(不论 Vue 还是 React 两者都是各有秋千)


再说回渲染, React 中也存在着和 Vue 一样的 VNode(虚拟 Dom)。


JSX 会被编译转换成 React.createElement 函数的调用,返回值就是 VNode,其作用和 Vue 中的 VNode 基本一致。


关于 Fiber 是一个比较抽象的概念比较难理解,可以理解为他是用来描述有关组件以及输入输出的信息的一个 JavaScript 对象。


了解更多 Fiber:Fiber传送门


小结一下:


React 渲染流程(浅看):


jsx --> createElement 函数 --> 这个函数帮助我们创建 ReactElement 对象(对象树) --> ReactDOM.render 函数 --> 映射到浏览器的真实DOM


生命周期


在渲染过程中暴露出来的钩子就是生命周期钩子函数了,看图:


image.png


我在 Vue 转 React 系列中有提到过 ->传送门


组件的生命周期可分成三个状态:



  • Mounting:已插入真实 DOM

  • Updating:正在被重新渲染

  • Unmounting:已移出真实 DOM


简单过一下生命周期:




  • componentWillMount 在渲染前调用,在客户端也在服务端。




  • componentDidMount : 在第一次渲染后调用,只在客户端。之后组件已经生成了对应的DOM结构,可以通过this.getDOMNode()来进行访问。 如果你想和其他JavaScript框架一起使用,可以在这个方法中调用setTimeout, setInterval或者发送AJAX请求等操作(防止异步操作阻塞UI)。




  • componentWillReceiveProps 在组件接收到一个新的 prop (更新后)时被调用。这个方法在初始化render时不会被调用。




  • shouldComponentUpdate 返回一个布尔值。在组件接收到新的props或者state时被调用。在初始化时或者使用forceUpdate时不被调用,可以在你确认不需要更新组件时使用。




  • componentWillUpdate在组件接收到新的props或者state但还没有render时被调用。在初始化时不会被调用。




  • componentDidUpdate 在组件完成更新后立即调用。在初始化时不会被调用。




  • componentWillUnmount在组件从 DOM 中移除之前立刻被调用。




小结


本文只是涉及内容众多,难免会有遗漏或不周,还请看官轻喷


作者:饼干_
链接:https://juejin.cn/post/7016530148073668621

收起阅读 »

前端必学的flip动画思想

前言 相信大家在用Vue的时候,一定用过他的transition-group组件。在该组件下方可以看到这么一句话 这个看起来很神奇,内部的实现,Vue 使用了一个叫 FLIP 简单的动画队列,使用 transforms 将元素从之前的位置...
继续阅读 »

前言


相信大家在用Vue的时候,一定用过他的transition-group组件。在该组件下方可以看到这么一句话



这个看起来很神奇,内部的实现,Vue 使用了一个叫 FLIP 简单的动画队列,使用 transforms 将元素从之前的位置平滑过渡新的位置。,我们将之前实现的例子和这个技术结合,使我们列表的一切变动都会有动画过渡。



和一个特别特别酷炫的动画效果


dfx1w-f5cnr.gif


下面,跟我一起走进Flip动画的奇妙世界


前置知识


getBoundingClientRect


通过dom.getBoundingClientRect(),可以得到某个元素在屏幕上的矩形区域


const rect = dom.getBoundingClientRect(); // 获取矩形区域
rect.left; // 获取矩形区域的left值
rect.top; // 获取矩形区域的top值

transform


transform是css3提供的属性,含义为变形或变换


css3提供了多种变换方式,包括平移、旋转、倾斜、缩放,还包括更加具有通用性的矩阵变换


所有变换,均不会影响真实布局位置,只是影响最终的视觉效果


animate api


Element 接口的animate()方法是一个创建新Animation的便捷方法,将它应用于元素,然后运行动画。它将返回一个新建的 Animation 对象实例


使用animate api实现动画非常简单,仅需要通过下面的代码即可实现


dom.animate(
[
{ /* 起始css属性 */ },
{ /* 结束css属性 */ },
],
{
duration: 800, // 完成动画的时间
}
);

其他API请看MDN文档


Flip思想


Flip是一种动画思路,专门针对上述场景


它由四个单词组成,分别是:



  • First

  • Last

  • Invert

  • Play


具体过程如下


image.png


在代码实现上,可以遵循以下结构实现动画效果


// ① Frist
record(container); // 记录容器中每个子元素的起始坐标
// 改变元素顺序
change();
// ② Last + ③ Invert + ④ Play
move(container); // 让元素真正实现移动

实现


<!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>Document</title>
</head>
<style>
* {
margin: 0;
padding: 0;
}

.btns {
text-align: center;
}

.btns button {
margin: 0 1em;
outline: none;
border: none;
background: #579ef8;
color: #fff;
padding: 7px 10px;
border-radius: 5px;
cursor: pointer;
}

.btns button:hover {
opacity: 0.8;
}

.container {
width: 500px;
overflow: hidden;
margin: 20px auto;
display: flex;
flex-wrap: wrap;
}

.item {
width: 50px;
height: 50px;
box-sizing: border-box;
text-align: center;
background: #eef5fe;
border: 1px solid #ddebfd;
line-height: 50px;
margin: 5px;
}
</style>

<body>
<div class="btns">
<button id="sort">随机排序</button>
</div>
<div class="container">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
<div class="item">9</div>
<div class="item">10</div>
<div class="item">11</div>
<div class="item">12</div>
<div class="item">13</div>
<div class="item">14</div>
<div class="item">15</div>
<div class="item">16</div>
<div class="item">17</div>
<div class="item">18</div>
<div class="item">19</div>
<div class="item">20</div>
<div class="item">21</div>
<div class="item">22</div>
<div class="item">23</div>
<div class="item">24</div>
<div class="item">25</div>
<div class="item">26</div>
<div class="item">27</div>
<div class="item">28</div>
<div class="item">29</div>
<div class="item">30</div>
<div class="item">31</div>
<div class="item">32</div>
<div class="item">33</div>
<div class="item">34</div>
<div class="item">35</div>
<div class="item">36</div>
<div class="item">37</div>
<div class="item">38</div>
<div class="item">39</div>
<div class="item">40</div>
<div class="item">41</div>
<div class="item">42</div>
<div class="item">43</div>
<div class="item">44</div>
<div class="item">45</div>
<div class="item">46</div>
<div class="item">47</div>
<div class="item">48</div>
<div class="item">49</div>
<div class="item">50</div>
</div>

<script>
const container = document.querySelector('.container')
function change() {
const childrens = [...container.children]
for(let i = 0, l = childrens.length; i < l; i ++) {
const children = childrens[i]
const j = Math.floor(Math.random() * l)
if (i !== j) {
// 获取当前dom的下一个元素
const inextDom = children.nextElementSibling
// 把i插入j之前
container.insertBefore(children, childrens[j])
// 把下标j的元素插入到i元素之前
container.insertBefore(childrens[j], inextDom)
}
}
}
sort.onclick = () => {
record(container)
change()
move(container)
}

function record(container) {
for(let i = 0, len = container.children.length; i < len; i ++) {
const dom = container.children[i]
const rect = dom.getBoundingClientRect()
dom.startX = rect.left
dom.startY = rect.top
}
}

function move(container) {
for(let i = 0, len = container.children.length; i < len; i ++) {
const dom = container.children[i]
const rect = dom.getBoundingClientRect()
const curX = rect.left, curY = rect.top
dom.animate([
{ transform: `translate(${dom.startX - curX}px, ${dom.startY - curY}px)` },
{ transform: `translate(0px, 0px)` }
], { duration: 600 })
}
}
</script>
</body>

</html>

以上就是所有代码了,可以在控制台看到,不会出现style标签,非常的神奇。


结语


FLIP 不光可以做位置变化的动画,对于透明度,大小等等都可以很轻松的实现。


Flip非常有用,可以实现在任何需要动画的地方



链接:https://juejin.cn/post/7016912165789515783

收起阅读 »

什么是 Promise.allSettled() !新手老手都要会?

Promise.allSettled() 方法返回一个在所有给定的 promise 都已经 fulfilled 或 rejected 后的 promise,并带有一个对象数组,每个对象表示对应的 promise 结果。 接着,我们来看看 Promise.all...
继续阅读 »

Promise.allSettled() 方法返回一个在所有给定的 promise 都已经 fulfilledrejected 后的 promise,并带有一个对象数组,每个对象表示对应的 promise 结果。


接着,我们来看看 Promise.allSettled() 是如何工作的。


1. Promise.allSettled()


Promise.allSettled() 可用于并行执行独立的异步操作,并收集这些操作的结果。


该函数接受一个 promise 数组(通常是一个可迭代对象)作为参数:


const statusesPromise = Promise.allSettled(promises);

当所有的输入 promises 都被 fulfilledrejected 时,statusesPromise 会解析为一个具有它们状态的数组




  1. { status: 'fulfilled', value: value } — 如果对应的 promise 已经 fulfilled




  2. 或者 {status: 'rejected', reason: reason} 如果相应的 promise 已经被 rejected




2823245504-60af4dde07f62_fix732.png


在解析所有 promises 之后,可以使用 then 语法提取它们的状态:


statusesPromise.then(statuses => {
statuses; // [{ status: '...', value: '...' }, ...]
});

或者使用 async/await 语法:


const statuses = await statusesPromise;
statuses; // [{ status: '...', value: '...' }, ...]

2. 取水果和蔬菜


在深入研究 Promise.allSettle() 之前,我们先定义两个简单的 helper 函数。


首先,resolveTimeout(value, delay)返回一个 promise ,该 promise 在经过 delay 时间后用 value 来实现


function resolveTimeout(value, delay) {
return new Promise(
resolve => setTimeout(() => resolve(value), delay)
);
}

第二,rejectTimeout(reason, delay) - 返回一个 promise,在经过 delay 时间后拒绝reason


最后,我们使用这些辅助函数来试验 promise.allsettle()


2.1 All promises fulfilled


我们同时访问当地杂货店的蔬菜和水果。访问每个列表是一个异步操作:


const statusesPromise = Promise.allSettled([
resolveTimeout(['potatoes', 'tomatoes'], 1000),
resolveTimeout(['oranges', 'apples'], 1000)
]);
// wait...
const statuses = await statusesPromise;
// after 1 second
console.log(statuses);
// [
// { status: 'fulfilled', value: ['potatoes', 'tomatoes'] },
// { status: 'fulfilled', value: ['oranges', 'apples'] }
// ]

线上事例:codesandbox.io/s/all-resol…


Promise.allSettled([...])返回一个 promise statusesPromise,该 promise 在1秒内解决,就在蔬菜和水果被解决之后,并行地解决。


statusesPromise 解析为一个包含状态的数组。



  1. 数组的第一项包含有蔬菜的已完成状态:status: 'fulfilled', value: ['potatoes', 'tomatoes'] }

  2. 同样的方式,第二项是水果的完成状态: { status: 'fulfilled', value: ['oranges', 'apples'] }


2.2一个 promise 被拒绝


想象一下,在杂货店里已经没有水果了。在这种情况下,我们拒绝水果的 promise。


promise.allsettle() 在这种情况下如何工作?


const statusesPromise = Promise.allSettled([
resolveTimeout(['potatoes', 'tomatoes'], 1000),
rejectTimeout(new Error('Out of fruits!'), 1000)
]);
// wait...
const statuses = await statusesPromise;
// after 1 second
console.log(statuses);
// [
// { status: 'fulfilled', value: ['potatoes', 'tomatoes'] },
// { status: 'rejected', reason: Error('Out of fruits!') }
// ]

线上事例:codesandbox.io/s/one-rejec…


Promise.allSettled([...]) 返回的 promise 在 1 秒后解析为一个状态数组:




  1. 数组的第一项,蔬菜 promise 成功解析:{ status: 'fulfilled', value: ['potatoes', 'tomatoes'] }




  2. 第二项,因为水果 promise 被拒绝,所以是一个拒绝状态: { status: 'rejected', reason: Error('Out of fruits') }




即使输入数组中的第二个 promise 被拒绝,statusesPromise仍然会成功解析一个状态数组。


2.3 所有的 promises 都被 rejected


如果杂货店里的蔬菜和水果都卖光了怎么办?在这种情况下,两个 promise 都会被拒绝。


const statusesPromise = Promise.allSettled([
rejectTimeout(new Error('Out of vegetables!'), 1000),
rejectTimeout(new Error('Out of fruits!'), 1000)
]);
// wait...
const statuses = await statusesPromise;
// after 1 second
console.log(statuses);
// [
// { status: 'rejected', reason: Error('Out of vegetables!') },
// { status: 'rejected', reason: Error('Out of fruits!') }
// ]

线上事例:codesandbox.io/s/all-rejec…


在这种情况下,statusesPromise仍然成功地解析为一个状态数组。然而,该数组包含被拒绝的promise 的状态。


3.总结


Promise.allSettled(promises)可以并行地运行 promise,并将状态(fulfilled 或reject)收集到一个聚合数组中。


Promise.allSettled(...)在你需要执行平行和独立的异步操作并收集所有结果时非常有效,即使某些异步操作可能失败。



链接:https://juejin.cn/post/7016856020395753509

收起阅读 »

说一说Web端侧AI

前言 AI 正在不断拓展前端的技术边界, 算法的加持也为前端研发注入了全新的力量。本文为大家介绍什么是端智能,端智能的应用场景以及 Web 端侧实现 AI 的基本原理概念。 什么是端智能 首先,回顾一个AI应用的开发流程,具体步骤包括了数据的采集与预处理模型的...
继续阅读 »

前言


AI 正在不断拓展前端的技术边界, 算法的加持也为前端研发注入了全新的力量。本文为大家介绍什么是端智能,端智能的应用场景以及 Web 端侧实现 AI 的基本原理概念。


什么是端智能


首先,回顾一个AI应用的开发流程,具体步骤包括了

数据的采集与预处理

模型的选取与训练

模型的评估

模型服务部署


模型训练的中间产物为一个模型文件,通过加载模型文件,部署为可调用的服务,然后就可以调用进行推理预测了。


在传统流程中,模型服务会部署在高性能的服务器上,由客户端发起请求,由服务器端进行推理,将预测结果返回给客户端,而端智能则是在客户端上完成推理的过程。


端智能的应用场景


端智能现在已经有非常多的应用场景,涵盖视觉类的 AR 、互动游戏,推荐类的信息流推荐,触达类的智能Push等,语音类的直播、智能降噪等多个领域。算法逐渐从服务端覆盖到用户实时感知更强的移动终端。


典型应用包括了

AR 应用、游戏。由 AI 提供理解视觉信息的能力,由 AR 根据视觉信息来实现虚实结合的交互,带来更沉浸式的购物、互动体验。比如美颜相机、虚拟试妆,即是通过检测人脸面部的关键点,在特定区域使用 AR 增强、渲染妆容。

互动游戏。飞猪双十一的互动游戏"找一找", 即是一个跑在 h5 页面的图片分类应用,通过摄像头实时捕捉图片,调用分类模型进行分类,当出现游戏设定目标时得分。
端侧重排。通过实时的用户意识识别,对服务器推荐算法下发的feeds流进行重新排列,做出更精准的内容推荐。

智能Push。通过端侧感知用户状态,决策是否需要向用户实施干预,推送Push,选取合适的时机主动触达用户,而非服务器端定时的批量推送,带来更精准的营销,更好的用户体验。


端智能的优势


从普遍的应用场景,可以看到端智能的明显优势,包括了

低延时

实时的计算节省了网络请求的时间。对于高帧率要求的应用,比如美颜相机每秒都要请求服务器,高延迟绝对是用户所不能接受的。而对于高频交互场景,比如游戏,低延时变得更为重要。

低服务成本

本地的计算节省了服务器资源,现在的新手机发布都会强调手机芯片的 AI 计算能力,越来越强的终端性能让更多的端上 AI 应用成为了可能。

保护隐私

数据隐私的话题在今天变得越来越重要。通过在端侧进行模型的推理,用户数据不需要上传到服务器,保证了用户隐私的安全。


端智能的局限


同时,端智能也有一个最明显的局限,就是低算力,虽然端侧的性能越来越强,但是和服务器相比还是相差甚远。为了在有限的资源里做复杂的算法,就需要对硬件平台进行适配,做指令级的优化,让模型能够在终端设备中跑起来,同时,需要对模型进行压缩,在时间和空间上减少消耗。


现在已经有一些比较成熟的端侧推理引擎了,这些框架、引擎都对终端设备做了优化来充分发挥设备的算力。比如Tensorflow LitePytorch mobile、阿里的 MNN、百度飞桨 PaddlePaddle


Web端呢


Web 端同样拥有端侧 AI 的优势与局限,作为在 PC 上用户访问互联网内容和服务的主要手段,在移动端很多APP也会嵌入 Web 页面,但是浏览器内存和存储配额的有限,让 Web 上运行 AI 应用看上去更不可能。


然而在 2015 年的时候就已经出现了一个 ConvNetJS的库,可以在浏览器里用卷积神经网络做分类、回归任务,虽然现在已经不维护了,2018 年的时候涌现了非常多的JS的机器学习、深度学习框架。如 Tensorflow.jsSynapticBrain.jsMindKeras.jsWebDNN 等。


受限于浏览器算力,部分框架如 keras.jsWebDNN框架只支持加载模型进行推理,而不能在浏览器中训练。


此外,一些框架不适用于通用的深度学习任务,它们支持的网络类型有所不同。比如 TensorFlow.jsKeras.jsWebDNN 支持了 DNNCNNRNN。而 ConvNetJS 主要支持 CNN 任务,不支持 RNNBrain.jssynaptic 主要支持 RNN 任务,不支持 CNN 网络中使用的卷积和池化操作。Mind 仅支持基本的 DNN


在选择框架时需要看下是否支持具体需求。


Web端架构


Web端是如何利用有限的算力的呢?


一个典型的 JavaScript 机器学习框架如图所示,从底向上分别是驱动硬件,使用硬件的浏览器接口,各种机器学习框架、图形处理库,最后是我们的应用。


Untitled


CPU vs GPU


Web 浏览器中运行机器学习模型的一个先决条件是通过 GPU 加速获得足够的计算能力。


在机器学习中,尤其是深度网络模型,广泛使用的操作是将大矩阵与向量相乘,再与另一个向量做加法。这种类型的典型操作涉及数千或数百万个浮点操作,而是它们通常是可并行化的。


以一个简单的向量相加为例,将两个向量相加可分为许多较小的运算,即每个索引位置相加。这些较小的操作并不相互依赖。尽管 CPU 对每个单独的加法所需的时间通常更少,随着计算量规模的变大,并发会逐渐显示出优势。


Untitled


WebGPU/WebGL vs WebAssembly


有了硬件之后,需要对硬件进行充分的利用。




  • WebGL


    WebGL 是目前性能最高的 GPU 利用方案,WebGL 为在浏览器中加速 2D3D图形渲染而设计,但可以用于神经网络的并行计算来加速推理过程,实现速度数量级的提升。




  • WebGPU


    随着 Web 应用对可编程 3D 图形、图像处理和 GPU 访问需求的持续增强,为了在 WEB 中引入GPU 加速科学计算性能,W3C2017 年提出了 WebGPU ,作为下一代 WEB 图形的的 API 标准,具有更低的驱动开销,更好的支持多线程、使用 GPU 进行计算。




  • WebAssembly


    当终端设备没有 WebGL 支持或者性能较弱的时候,使用 CPU 的通用计算方案为 WebAssemblyWebAssembly是一种新的编码方式,可以在现代的网络浏览器中运行,它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如C / C ++ 等语言提供一个编译目标,以便它们可以在 Web 上运行。




Tensorflow.js


Tensorflow.js 为例,为了在不同的环境下实现运行,tensorflow 支持了不同的后端,根据设备条件自动选择相应的后端 ,当然也支持手动更改。


Untitled


tf.setBackend('cpu');
console.log(tf.getBackend());

对一些通用模型进行测试,WebGL速度大于普通 CPU 后端计算的 100 倍,WebAssembly 则比普通的 JS CPU 后端快 10-30 倍。


同时, tensorflow 也提供了 tfjs-node 版本,通过 C++CUDA 代码编译的本机编译库驱动 CPUGPU 进行计算,训练速度与 Python 版本的 Keras 相当。不需要切换常用语言,可以直接在 nodejs 服务上添加 AI 模块,而不是再启动一个 python 的服务。


模型压缩


有了框架对于硬件设备的适配,还需要对模型进行压缩,复杂的模型固然有更好的预测精度,但是高额的存储空间、计算资源的消耗,过长的推理速度在大部分移动端场景中还是难以接受的。


模型的复杂度在于模型结构的复杂以及海量的参数。模型文件中通常存储了两部分信息:结构参数,如下图中简化的神经网络所示,每个方块对应一个神经元,每个神经元以及神经元中的连线上都是参数。


模型的推理即从左侧输入,通过与神经元进行计算,再通过连线加上权重传到下一层计算,到最终层得到预测输出。节点越多、连接越多,则计算量越大。


Untitled


模型剪枝


对训练好的模型进行裁剪,是模型压缩的一个常见方式,网络模型中存在着大量冗余的参数,大量神经元激活值趋近于 0,通过对无效节点或者是不那么重要的节点进行裁剪,可以减少模型的冗余。


最简单粗暴的剪枝即 DropOut ,在训练过程中随机对神经元进行丢弃。
大部分的剪枝方法会计算重要性因子,计算神经元节点对于最终的结果的重要性,剪去不那么重要的节点。


模型剪枝的过程是迭代反复进行的,并非剪枝后直接用来推理,通过剪枝后的训练恢复模型的精度,模型的压缩过程是在精度和压缩比例之间的不断权衡,在可接受的精度损失范围内选择最好的压缩效果。


模型量化


为了保证较高的精度,大部分的科学运算都是采用浮点型进行计算,常见的是 32 位浮点型和 64 位浮点型,即 float32double64。量化即是把高精度的数值转化为低精度。


如二值量化(1bit量化)会直接将 Float32/float64 的值映射到 1bit ,存储空间直接压缩 32 倍/ 64 倍,计算时加载所需内存同样也会变小,更小的模型尺寸,带来更低的功耗以及更快的计算速度。除此还有8bit量化、任意bit量化。


知识蒸馏


知识蒸馏则是将深度网络中所学到的知识转移到另一个相对简单的网络中,先训练一个 teacher 网络,然后使用这个 teacher 网络的输出和数据的真实标签去训练 student 网络。


工具


模型压缩的实现较为复杂,如果只是面向应用,大概了解其作用原理即可,可以直接用封装好的工具。


比如 Tensorflow Model Optimization Toolkit 提供了量化功能,其官方对于一些通用模型进行了压缩测试,如下表可以看到,对于 mobilenet 模型,模型大小由 10M+ 压缩到了 3、4M,而模型的精度损失很小。


Untitled


百度的飞桨提供的 PaddleSlim 则提供了上述三种压缩方法。


Untitled


总结


综上,开发一个 Web 端上的 AI 应用,流程即变成了

针对特定场景设计算法、训练模型

对模型进行压缩

转换为推理引擎所需要的格式

加载模型进行推理预测


对于算法而言,通用的深度学习框架已然提供了若干的通用预训练模型,可以直接用来做推理,也可以在其基础上训练自己的数据集。模型的压缩与推理也可以使用现有的工具。


作者:凹凸实验室
链接:https://juejin.cn/post/7013674501116264484

收起阅读 »

可恶,又学到了一点 CSS

昨天在做笔记整理的时候,看到一个面试题,如何实现水平垂直居中,虽然心里有一点点数,但是看到好几种答案,还是决定亲自动手验证一番,这验证一开始就出现了小问题,接着就像捅了个马蜂窝一样,各种疑惑扑面而来,而我又想弄清楚,折腾大半天,终于把问题锁定到了 line-h...
继续阅读 »

昨天在做笔记整理的时候,看到一个面试题,如何实现水平垂直居中,虽然心里有一点点数,但是看到好几种答案,还是决定亲自动手验证一番,这验证一开始就出现了小问题,接着就像捅了个马蜂窝一样,各种疑惑扑面而来,而我又想弄清楚,折腾大半天,终于把问题锁定到了 line-heightvertical-align 身上。


大家现在应该都用 flex 布局,但是毕竟折腾好一会呢,好歹记录一下自己的收获哈哈


1.疑惑代码


    <div>
       <div>我要水平垂直居中</div>
   </div>

.container{
   border: 2px solid black;
   background-color: chartreuse;
   width: 200px;
   height: 200px;
   text-align: center;
   line-height: 200px;
}
.box{
   display: inline-block;
   line-height: normal;
   font-size: 1rem;
   vertical-align: middle;
   background-color: cornflowerblue;
}

你别说,还真居中了:


image-20211004131103654.png


2.一些知识点


2.1 水平垂直对齐


有一些疑惑真的只是自己无知哈哈哈



  • text-align 不仅可以作用在文本,还可以对行内元素和行内块元素有效果,设置水平对齐方式

  • vertical-align 只对行内元素和行内块元素起作用,设置元素的垂直对齐方式


2.2 line-height


说来也蛮搞笑的,我对 line-height 的印象就是,当元素 height 等于 line-height 的时候,元素内部的文本就会垂直居中。但是昨天查资料的时候发现,这里面牵扯到了很多复杂的问题。


简单的和我一起学习一下吧,MDN 上边说的,line-height 可用于多行文本的间距或者是单行文本的高度等


看到这里大家可以去看一下,底下的两篇参考文章,得出以下结论:



  • 没给元素设置高度时,元素高度采用的是 line-height 的高度,这个属性具有继承性。也自带默认值,所以当你给一个没有设置高度的元素设置 line-height:0;,即使里面有文本,它也是会塌陷的。

  • 可以分为好几种盒子,当你设置 line-height 的时候,行内框是不会变化的,改变的是行距,它只由 font-size 的决定。这其实就是上边元素 height 等于 line-height 的时候,元素内文本会垂直居中的原因。

  • 取值为 number 时,line-heightnumber 乘以当前元素的 font-size,取 normal 时一般就是 number 为 1.2


3. 疑惑产生


喜欢东拆拆西拆拆的我发现,上边代码注释掉 vertical-align: middle; 效果并没有变化,依旧垂直居中着,但是将它改成 vertical-align: top; 又起到作用了如下:


image-20211004152748629.png


4.解决


真的,其实写博客之前我都没有理解为什么会出现这种怪异情况,但是写着写着就来灵感了,原来是这样哈哈哈


其实咱们把文字内容加一点,使它成为多行文本,效果就出来了,没注释掉 vertical-align: middle 的效果如下:


image-20211004153753934.png


注释掉了,就是这个样子,没有垂直居中:


image-20211004153832263.png


所以前面的只是巧合,因为是单行文本的原因,平衡上下行距,应该也不叫行距,应该就是为了平衡才会出现垂直居中的效果哦!


链接:https://juejin.cn/post/7015117674422206494
收起阅读 »

Vue中 前端实现生成 PDF 并下载

思路: 通过 html2canvas 将 HTML 页面转换成图片,然后再通过 jspdf 将图片的 base64 生成为 pdf 文件。 1. 安装及引入 // 将页面 html 转换成图片 npm install html2canvas --save ...
继续阅读 »

思路: 通过 html2canvas 将 HTML 页面转换成图片,然后再通过 jspdf 将图片的 base64 生成为 pdf 文件。


1. 安装及引入


// 将页面 html 转换成图片
npm install html2canvas --save
// 将图片生成 pdf
npm install jspdf --save

在项目主文件 main.js 中引入定义好的实现方法并注册


import htmlToPdf from '@/utils/htmlToPdf';
// 使用 Vue.use() 方法就会调用工具方法中的install方法
Vue.use(htmlToPdf);

传送门:Vue中 Vue.use() 原理及使用


2. 封装导出 pdf 文件方法


配置详解


let pdf = new jsPDF('p', 'pt', [pdfX, pdfY]);
第一个参数: l:横向 p:纵向
第二个参数:测量单位("pt","mm", "cm", "m", "in" or "px");
第三个参数:可以是下面格式,默认为“a4”。如需自定义格式,只需将大小作为数字数组传递,如:[592.28, 841.89];
a0 - a10
b0 - b10
c0 - c10
dl
letter
government-letter
legal
junior-legal
ledger
tabloid
credit-card

pdf.addPage() 在PDF文档中添加新页面,默认a4。参数如下:


在这里插入图片描述


pdf.addImage() 将图像添加到PDF。参数如下:


在这里插入图片描述


删除某页 pdf


let targetPage = pdf.internal.getNumberOfPages(); //获取总页
pdf.deletePage(targetPage); // 删除目标页
复制代码

保存 pdf 文档


pdf.save(`测试.pdf`);
复制代码

在这里插入图片描述


封装导出 pdf 文件方法(utils/htmlToPdf.js)


// 导出页面为PDF格式
import html2Canvas from 'html2canvas'
import JsPDF from 'jspdf'
export default{
install (Vue, options) {
Vue.prototype.getPdf = function () {
// 当下载pdf时,若不在页面顶部会造成PDF样式不对,所以先回到页面顶部再下载
let top = document.getElementById('pdfDom');
if (top != null) {
top.scrollIntoView();
top = null;
}
let title = this.exportPDFtitle;
html2Canvas(document.querySelector('#pdfDom'), {
allowTaint: true
}).then(function (canvas) {
// 获取canvas画布的宽高
let contentWidth = canvas.width;
let contentHeight = canvas.height;
// 一页pdf显示html页面生成的canvas高度;
let pageHeight = contentWidth / 841.89 * 592.28;
// 未生成pdf的html页面高度
let leftHeight = contentHeight;
// 页面偏移
let position = 0;
// html页面生成的canvas在pdf中图片的宽高(本例为:横向a4纸[841.89,592.28],纵向需调换尺寸)
let imgWidth = 841.89;
let imgHeight = 841.89 / contentWidth * contentHeight;
let pageData = canvas.toDataURL('image/jpeg', 1.0);
let PDF = new JsPDF('l', 'pt', 'a4');
// 两个高度需要区分: 一个是html页面的实际高度,和生成pdf的页面高度
// 当内容未超过pdf一页显示的范围,无需分页
if (leftHeight < pageHeight) {
PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
} else {
while (leftHeight > 0) {
PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight;
position -= 592.28;
// 避免添加空白页
if (leftHeight > 0) {
PDF.addPage();
}
}
}
PDF.save(title + '.pdf')
})
}
}
}

相关组件中应用


<template>
<div class="wrap" >
<div id="pdfDom" style="padding: 10px;">
<el-table
:data="tableData"
border>
<el-table-column prop="date" label="日期" width="250"></el-table-column>
<el-table-column prop="name" label="姓名" width="250"></el-table-column>
<el-table-column prop="address" label="地址"></el-table-column>
</el-table>
</div>
<button type="button" style="margin-top: 20px;" @click="btnClick">导出PDF</button>
</div>

</template>

<script>
export default {
data() {
return {
exportPDFtitle: "页面导出PDF文件名",
tableData: [
{
date: '2016-05-06',
name: '王小虎',
address: '重庆市九龙坡区火炬大道'
}, {
date: '2016-05-07',
name: '王小虎',
address: '重庆市九龙坡区火炬大道'
},{
date: '2016-05-03',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-02',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-04',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-08',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-06',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-06',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-07',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-08',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-06',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-07',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-06',
name: '王小虎',
address: '南京市江宁区将军大道'
}, {
date: '2016-05-07',
name: '王小虎',
address: '南京市江宁区将军大道'
},, {
date: '2016-05-04',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-08',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-06',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-07',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
},{
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-08',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-06',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-08',
name: '王小虎',
address: '武汉市洪山区文化大道'
}, {
date: '2016-05-06',
name: '王小虎',
address: '武汉市洪山区文化大道'
}, {
date: '2016-05-07',
name: '王小虎',
address: '武汉市洪山区文化大道'
}, {
date: '2016-05-06',
name: '王小虎',
address: '南京市江宁区将军大道'
}, {
date: '2016-05-07',
name: '王小虎',
address: '武汉市洪山区文化大道'
},
]
}
},
methods: {
btnClick(){
this.$nextTick(() => {this.getPdf();})
},
},
}
</script>

效果


在这里插入图片描述


待优化部分

分页时,页面内容被截断(欢迎留言讨论交流);

不同内容,另起一页开始;思路:计算超出内容,占最后一页的高度(设定间距 = 页面高度 - 超出部分高度)。


作者:明天也要努力
链接:https://juejin.cn/post/7016249316834557959

收起阅读 »

你还在为pc端适配而烦恼吗?相信我,看了之后就不烦恼了

作为一名前端开发者,你有没有遇到过这种头痛的事情。每次开发pc端的网页时,不管是官网还是管理后台,UI设计师都是按照1920*1080(16:9)的比例来给你提供设计稿的,导致你画页面的时候。会出现两种情况。第一种按照设计师提供的设计稿比例画页面的话,导致在不...
继续阅读 »

作为一名前端开发者,你有没有遇到过这种头痛的事情。每次开发pc端的网页时,不管是官网还是管理后台,UI设计师都是按照1920*1080(16:9)的比例来给你提供设计稿的,导致你画页面的时候。会出现两种情况。

第一种按照设计师提供的设计稿比例画页面的话,导致在不同比例的屏幕上,就会呈现不同的样式效果,有的过大有的过小,这时候用户就会问你,你是不是写的bug。。。很无语

第二种就是按照设计师提供的比例按自适应的方式来画页面,这样导致页面的尺寸与设计稿不同,并且在不同设备也会有差别,这时候设计师跟用户就会跑过来说,这里是不是少了1px的单位,这里是不是写错了。。。很头疼


今天我就介绍一种可以解决pc端网页适配的方法,大家觉得有用的话,就给我个小小点赞,也可以在评论区留言。


PC实现适配也是用了rem这个css3属性,rem相对于根元素(即html元素)font-size计算值的倍数。这里以PC常见的分辨率1920px和1366px(14寸笔记本)为例说明。为了更好的说明,假设设计师给的设计稿是1920px,我们既要做1920px屏幕,也要给1366px的屏幕做适配。


现在我们随便取1920px设计稿一块区域,假设宽度273px,高度随意。那么在1366px屏幕上宽度应该显示多少呢?


我们将屏幕宽度等比分成100份


//1920分辨率屏幕
avg = 1920 / 100 = 19.20 px

//1366分辨率屏幕
avg = 1366 / 100 = 13.66 px

在1366分辨率屏幕应该显示宽度 = (1366 * 273) / 1920 最后是194.228125px


//1920分辨率屏幕定义根
font-size = 19.20px //即 1rem = 19.20px

//1366分辨率屏幕
font-size = 13.66px //即 1rem = 13.66px

适配代码


html{
font-size:19.20px; /*默认以设计稿为基准*/
}

@media only screen and (max-width: 1366px) {
html{
font-size:13.66px;
}
}
#test{
width:14.21875rem;
}

id为test的盒子在1920屏幕宽度= 14.21875 * 19.20 最后是273


id为test的盒子在1366屏幕宽度= 14.21875 * 13.66 最后是194.228125


这样一来我们就适配了1920px和1366px屏幕。PC一般也就是这两个分辨率占多数,兼容了这两个分辨率屏幕基本就可以了。在说下国内基本没有在兼容IE8的浏览器了。基本都是IE9+,css3属性在IE9+上还是可以使用的。不过建议小伙伴们使用前还是确定下,浏览器兼容


最后在对上面补充点,有的小伙伴可能觉得每次设置宽高前都要手动的转换,实在是太麻烦,不要着急我为大家找了个sass方法。


// PX 转 rem 
@function px2Rem($px, $base-font-size: 19.2px) {
@if (unitless($px)) {
//有无单位
@return ($px / 19.2) * 1rem;
} @else if (unit($px) == em) {
@return $px;
}
@return ($px / $base-font-size) * 1rem;
}

测试下上面的方法


#test{
width:px2Rem(273px)
}
//输出
#test{
width:14.21875rem;
}

大家将屏幕分辨率调整为1920px1366px来查看灰色区域宽度。




内容



@function px2Rem($px, $base-font-size: 19.2px) {

@if (unitless($px)) { //有无单位

@return ($px / 19.2) * 1rem;

} @else if (unit($px) == em) {

@return $px;

}
@return ($px / $base-font-size) * 1rem;
}

*{
margin:0;

padding:0;
}

html{
font-size:19.20px;
}

html,body{
height:100%;
}

body{
font-size:px2Rem(16px);
}

#app{
position:relative;

width:100%;

height:100%;
}

.nav{
position:fixed;

left:0;

top:0;

width:px2Rem(273px);

height:100%;

background-color:#E4E4E4;

transition:all .3s;

z-index:11;
}

.content{

position:absolute;

left:px2Rem(273px);

top:0;

right:0;

bottom:0;

width:auto;

height:100%;

background-color:#CBE9CB;

overflow-y:auto;

z-index:10;
}

p{
font-size:px2Rem(20px);
}

@media only screen and (max-width: 1366px) {
html{
font-size:13.66px;
}
}

效果图


screenshot-waliblog.com-2021.10.05-01_12_14.png


小伙伴们如果有更好的PC适配方案也可给我讲讲,欢迎在下方留言


sass下使用变量


height: calc(100% - #{px2rem(200px)});

链接:https://juejin.cn/post/7015257656193449992

收起阅读 »

你会用ES6,那倒是用啊!

不是标题党,这是一位leader在一次代码评审会对小组成员发出的“怒吼”,原因是在代码评审中发现很多地方还是采用ES5的写法,也不是说用ES5写法不行,会有BUG,只是造成代码量增多,可读性变差而已。 恰好,这位leader有代码洁癖,面对3~5年经验的成员,...
继续阅读 »

不是标题党,这是一位leader在一次代码评审会对小组成员发出的“怒吼”,原因是在代码评审中发现很多地方还是采用ES5的写法,也不是说用ES5写法不行,会有BUG,只是造成代码量增多,可读性变差而已。


恰好,这位leader有代码洁癖,面对3~5年经验的成员,还写这种水平的代码,极为不满,不断对代码进行吐槽。不过对于他的吐槽,我感觉还是有很大收获的,故就把leader的吐槽记录下来,分享给掘友们,觉得有收获点个赞,有错误的或者更好的写法,非常欢迎在评论中留言。


ps:ES5之后的JS语法统称ES6!!!


一、关于取值的吐槽


取值在程序中非常常见,比如从对象obj中取值。


const obj = {
a:1,
b:2,
c:3,
d:4,
e:5,
}

吐槽


const a = obj.a;
const b = obj.b;
const c = obj.c;
const d = obj.d;
const e = obj.e;

或者


const f = obj.a + obj.d;
const g = obj.c + obj.e;

吐槽:“不会用ES6的解构赋值来取值吗?5行代码用1行代码搞定不香吗?直接用对象名加属性名去取值,要是对象名短还好,很长呢?搞得代码中到处都是这个对象名。”


改进


const {a,b,c,d,e} = obj;
const f = a + d;
const g = c + e;

反驳


不是不用ES6的解构赋值,而是服务端返回的数据对象中的属性名不是我想要的,这样取值,不是还得重新创建个遍历赋值。


吐槽


看来你对ES6的解构赋值掌握的还是不够彻底。如果想创建的变量名和对象的属性名不一致,可以这么写:


const {a:a1} = obj;
console.log(a1);// 1

补充


ES6的解构赋值虽然好用。但是要注意解构的对象不能为undefinednull。否则会报错,故要给被解构的对象一个默认值。


const {a,b,c,d,e} = obj || {};

二、关于合并数据的吐槽


比如合并两个数组,合并两个对象。


const a = [1,2,3];
const b = [1,5,6];
const c = a.concat(b);//[1,2,3,1,5,6]

const obj1 = {
a:1,
}
const obj1 = {
b:1,
}
const obj = Object.assgin({}, obj1, obj2);//{a:1,b:1}

吐槽


ES6的扩展运算符是不是忘记了,还有数组的合并不考虑去重吗?


改进


const a = [1,2,3];
const b = [1,5,6];
const c = [...new Set([...a,...b])];//[1,2,3,5,6]

const obj1 = {
a:1,
}
const obj2 = {
b:1,
}
const obj = {...obj1,...obj2};//{a:1,b:1}

三、关于拼接字符串的吐槽


const name = '小明';
const score = 59;
const result = '';
if(score > 60){
result = `${name}的考试成绩及格`;
}else{
result = `${name}的考试成绩不及格`;
}

吐槽


像你们这样用ES6字符串模板,还不如不用,你们根本不清楚在${}中可以做什么操作。在${}中可以放入任意的JavaScript表达式,可以进行运算,以及引用对象属性。


改进


const name = '小明';
const score = 59;
const result = `${name}${score > 60?'的考试成绩及格':'的考试成绩不及格'}`;

四、关于if中判断条件的吐槽


if(
type == 1 ||
type == 2 ||
type == 3 ||
type == 4 ||
){
//...
}

吐槽


ES6中数组实例方法includes会不会使用呢?


改进


const condition = [1,2,3,4];

if( condition.includes(type) ){
//...
}

五、关于列表搜索的吐槽


在项目中,一些没分页的列表的搜索功能由前端来实现,搜索一般分为精确搜索和模糊搜索。搜索也要叫过滤,一般用filter来实现。


const a = [1,2,3,4,5];
const result = a.filter(
item =>{
return item === 3
}
)

吐槽


如果是精确搜索不会用ES6中的find吗?性能优化懂么,find方法中找到符合条件的项,就不会继续遍历数组。


改进


const a = [1,2,3,4,5];
const result = a.find(
item =>{
return item === 3
}
)

六、关于扁平化数组的吐槽


一个部门JSON数据中,属性名是部门id,属性值是个部门成员id数组集合,现在要把有部门的成员id都提取到一个数组集合中。


const deps = {
'采购部':[1,2,3],
'人事部':[5,8,12],
'行政部':[5,14,79],
'运输部':[3,64,105],
}
let member = [];
for (let item in deps){
const value = deps[item];
if(Array.isArray(value)){
member = [...member,...value]
}
}
member = [...new Set(member)]

吐槽


获取对象的全部属性值还要遍历吗?Object.values忘记了吗?还有涉及到数组的扁平化处理,为啥不用ES6提供的flat方法呢,还好这次的数组的深度最多只到2维,还要是遇到4维、5维深度的数组,是不是得循环嵌套循环来扁平化?


改进


const deps = {
'采购部':[1,2,3],
'人事部':[5,8,12],
'行政部':[5,14,79],
'运输部':[3,64,105],
}
let member = Object.values(deps).flat(Infinity);

其中使用Infinity作为flat的参数,使得无需知道被扁平化的数组的维度。


补充


flat方法不支持IE浏览器。


七、关于获取对象属性值的吐槽


const name = obj && obj.name;

吐槽


ES6中的可选链操作符会使用么?


改进


const name = obj?.name;

八、关于添加对象属性的吐槽


当给对象添加属性时,如果属性名是动态变化的,该怎么处理。


let obj = {};
let index = 1;
let key = `topic${index}`;
obj[key] = '话题内容';

吐槽


为何要额外创建一个变量。不知道ES6中的对象属性名是可以用表达式吗?


改进


let obj = {};
let index = 1;
obj[`topic${index}`] = '话题内容';

九、关于输入框非空的判断


在处理输入框相关业务时,往往会判断输入框未输入值的场景。


if(value !== null && value !== undefined && value !== ''){
//...
}

吐槽


ES6中新出的空值合并运算符了解过吗,要写那么多条件吗?


if(value??'' !== ''){
//...
}

十、关于异步函数的吐槽


异步函数很常见,经常是用 Promise 来实现。


const fn1 = () =>{
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 300);
});
}
const fn2 = () =>{
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2);
}, 600);
});
}
const fn = () =>{
fn1().then(res1 =>{
console.log(res1);// 1
fn2().then(res2 =>{
console.log(res2)
})
})
}

吐槽


如果这样调用异步函数,不怕形成地狱回调啊!


改进


const fn = async () =>{
const res1 = await fn1();
const res2 = await fn2();
console.log(res1);// 1
console.log(res2);// 2
}

补充


但是要做并发请求时,还是要用到Promise.all()


const fn = () =>{
Promise.all([fn1(),fn2()]).then(res =>{
console.log(res);// [1,2]
})
}

如果并发请求时,只要其中一个异步函数处理完成,就返回结果,要用到Promise.race()


十一、后续


欢迎来对以上十点leader的吐槽进行反驳,你的反驳如果有道理的,下次代码评审会上,我替你反驳。


此外以上的整理内容有误的地方,欢迎在评论中指正,万分感谢。


如果你还有其它想吐槽的,也非常欢迎在评论中留下你的吐槽。


作者:红尘炼心
链接:https://juejin.cn/post/7016520448204603423

收起阅读 »

国庆假期,整整七天,我使用Flutter终于做出了即时通信!!!?

前言:在这个假期,我完成了一个小Demo,Flutter 与 Springboot 进行websocket的通讯,为啥想要去做这个Demo呢,主要是在各大平台以及google搜索后发现,没有一个详细的例子来教大家进行一对一、一对多的通讯,大多数都是教你怎么连接...
继续阅读 »

前言:在这个假期,我完成了一个小Demo,Flutter 与 Springboot 进行websocket的通讯,为啥想要去做这个Demo呢,主要是在各大平台以及google搜索后发现,没有一个详细的例子来教大家进行一对一、一对多的通讯,大多数都是教你怎么连接,却没有教你怎么去进行下一步的功能实现,于是我利用了五天的假期,踩了无数的坑,终于是完成了它,所以,点个赞吧,不容易啊,兄弟们😭


源码在文章最后,直接运行就完事,服务端我都帮兄弟们架包打好了,运行一下就行,运行方法在文末简单叙述了😎


服务端分析:Springboot WebSocket 即时通讯


先上效果图(我自己搜索这样功能性的问题时,没有效果图基本上都是不想看的):


tt0.top-039531.gif


屏幕截图 2021-10-05 153152.jpg


即时通讯最重要的功能是完成了(发送文字信息)


阅读本文的注意点:


1.需要一点WebSocket的原理知识


2.Flutter使用WebSocket的方式,本文使用 'dart:io' ,大家也可以使用插件


正文:


1.WebSocket的简单原理


很多同学在第一次碰到这个协议时会发现它与HTTP相似,于是就会问,我们已经有了 HTTP 协议,为什么还需要WebSocket?它有什么特殊的地方呢?


其实是因为 HTTP 协议有一个缺陷:通信只能由客户端发起。


img


而WebSocket首先是一个持久化的协议,它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,这个协议非常适合即时通讯或者消息的推送。


img


2.Flutter中怎么使用WebSocket


有两种方式:


1.Flutter自带的 'dart:io'



  • 连接WebSocket服务器


    Future<WebSocket> webSocketFuture =  WebSocket.connect('ws://192.168.13.32:9090'); //connect中放服务端地址



  • 存放WebSocket.connect返回的对象


    static WebSocket _webSocket;



  • 发送消息


    _webSocket.add('发送消息内容');



  • 监听接收消息,调用listen方法


    void onData(dynamic content) {
    print('收到消息:'+content);
    }
    _webSocket.listen(onData, onDone: () {
    print('onDone');
    }, onError: () {
    print('onError');
    }, cancelOnError: true);



  • 例子:


    webSocketFuture.then((WebSocket ws) {
    _webSocket = ws;
    void onData(dynamic content) {
    print('收到新的消息');
    }
    // 调用add方法发送消息
    _webSocket.add('message');
    // 监听接收消息,调用listen方法
    _webSocket.listen(onData, onDone: () {
    print('onDone');
    }, onError: () {
    print('onError');
    }, cancelOnError: true);
    });



  • 关闭WebSocket连接


    _webSocket.close();



2.第三方插件库实现 WebSocket

基本使用步骤也都是:连接 WebSocket 服务器、发送消息、接收消息、关闭 WebSocket 连接。



  • 在项目的 pubspec.yaml 里加入引用:


dependencies:
web_socket_channel: 官网最新版本


  • 导入包:


import 'package:web_socket_channel/io.dart';


  • 连接 WebSocket 服务器:


var channel = IOWebSocketChannel.connect("ws://192.168.13.32:9090");

通过IOWebSocketChannel我们便可以进行各种操作



  • 发送消息:


channel.sink.add("connected!");


  • 监听接收消息:


channel.stream.listen((message) {      print('收到消息:' + message);    });


  • 关闭 WebSocket 连接:


channel.sink.close();

以上就是 Flutter 通过第三方插件库实现 WebSocket 通信功能的基本步骤。


3.联系人界面以及对话界面的UI实现


我的小部件都进行了封装,源码请看文章最后,分析部分只放重要代码




  • 对话框ui处理


    顶部使用appbar,包含一个返回按钮,用户信息以及状态,还有一个设置按钮(没有什么难点就不放代码了)




1633651654(1).png



  • 双方信息处理


1633651683(1).png


这里有个ui处理难点,就是分析信息是谁发出的,是自己还是对方呢


这里我选择在每条信息Json格式的末尾加上一个判断符:messageType,”receiver“代表对方发的信息,”sender“代表是自己发的


ui处理:


ListView.builder(  itemCount: UserMessage.messages.length, //总发送信息的条数  shrinkWrap: true,  padding: const EdgeInsets.only(top: 10, bottom: 10),  physics: const NeverScrollableScrollPhysics(),  itemBuilder: (context, index) {    return Container(      padding: const EdgeInsets.only(          left: 14, right: 14, top: 10, bottom: 10),      child: Align(          alignment:              (UserMessage.messages[index].messageType == "receiver"                  ? Alignment.topLeft                  : Alignment.topRight),          child: Container(              decoration: BoxDecoration(                borderRadius: BorderRadius.circular(20),                color: (UserMessage.messages[index].messageType ==                        "receiver"                    ? Colors.grey.shade200                    : Colors.blue[200]),              ),              padding: const EdgeInsets.all(16),              child: Text(                UserMessage.messages[index].messageContent,                style: TextStyle(fontSize: 15),              ))),    );  },),



  • 单个联系人的ui




1633605454(1).png


一行为一个联系人模块,其内包含用户头像,用户姓名,即时通讯内容,以及上一次对话的时间,点击每行跳转到相对应的聊天框。


封装处理:


class ConversationList extends StatefulWidget {  String name; //用户姓名  String messageText; //即时内容  String imageUrl; //用户头像  String time;//上一次对话时间  bool isMessageRead; //用于处理字体大小  ConversationList(      {Key? key, required this.name,      required this.messageText,      required this.imageUrl,      required this.time,      required this.isMessageRead}) : super(key: key);  @override  _ConversationListState createState() => _ConversationListState();}

详细布局:


return GestureDetector(    onTap: () {      Navigator.push(context, MaterialPageRoute(builder: (context){        return ChatDetailPage(name:widget.name,userImageUrl:widget.imageUrl);      }));    },    child: Container(      padding: const EdgeInsets.only(left: 16, right: 16, top: 10, bottom: 10),      child: Row(        children: <Widget>[          Expanded(            child: Row(              children: <Widget>[                CircleAvatar(                  backgroundImage: AssetImage(widget.imageUrl),                  maxRadius: 30,                ),                const SizedBox(                  width: 16,                ),                Expanded(                  child: Container(                    color: Colors.transparent,                    child: Column(                      crossAxisAlignment: CrossAxisAlignment.start,                      children: <Widget>[                        Text(                          widget.name,                          style: const TextStyle(fontSize: 16),                        ),                        const SizedBox(                          height: 6,                        ),                        Text(                          widget.messageText,                          style: TextStyle(                              fontSize: 13,                              color: Colors.grey.shade600,                              fontWeight: widget.isMessageRead                                  ? FontWeight.bold                                  : FontWeight.normal),                        ),                      ],                    ),                  ),                ),              ],            ),          ),          Text(            widget.time,            style: TextStyle(                fontSize: 12,                fontWeight: widget.isMessageRead                    ? FontWeight.bold                    : FontWeight.normal),          ),        ],      ),    ),  );}



  • 列表实现:


    这里简单使用ListView.builder


    ListView.builder(  itemCount: chatUsers.length,  shrinkWrap: true,  padding: const EdgeInsets.only(top: 16),  physics: const NeverScrollableScrollPhysics(),  itemBuilder: (context, index){    return ConversationList(      name: chatUsers[index].name,      messageText: chatUsers[index].messageText,      imageUrl: chatUsers[index].imageURL,      time: chatUsers[index].time,      isMessageRead: (index == 0 || index == 3)?true:false,    );  },),



ui的难点部分在这里就分析完成了


4.Flutter WebSocket处理


因为是个Demo,封装的信息比较简单




  • 对发送的信息进行封装


    class ChatMessage {  String messageContent;  String messageType;  ChatMessage({required this.messageContent, required this.messageType});}



  • 对基本信息,以及用户信息进行封装,


    class UserMessage{  static int userId = 0;  static String socketUrl = "ws://192.168.10.104:9090/websocket/";  ///将对话暂存在这里  static List<ChatMessage> messages = [  ];}



  • 初始化时连接服务器并且对其进行监听


    当数据返回时,保存到内存中


    //连接wensocket并且监听void connect(String userName) {  Future<WebSocket> futureWebSocket =      WebSocket.connect(UserMessage.socketUrl + "/$userName"); //socket地址  futureWebSocket.then((WebSocket ws) {    _webSocket = ws;    _webSocket.readyState;    // 监听事件    void onData(dynamic content) {      print('收到消息:' + content);      setState(() {        if (content.toString().substring(0, 1) ==            UserMessage.userId.toString()) {          ///自己发送的消息(服务端没有完善)        } else {          UserMessage.messages.add(ChatMessage(              messageContent: content.toString().substring(                    2,                  ),              messageType: "receiver"));        }      });    }    _webSocket.listen(onData,        onError: (a) => print("error"), onDone: () => print("done"));  });}



  • 发送信息处理


    对数据需要将json对象转换为json字符串


    // 向服务器发送消息void sendMessage(dynamic message) {  print(convert.jsonEncode(message));  _webSocket.add(convert.jsonEncode(message);}

    onPressed: () {  var toUser = "0"; //服务端没有完善,这里固定用户id了  if (UserMessage.userId == 0) {    toUser = "1";  } else {    toUser = "0";  }  var message = {    "msg": _controller.text,    "toUser": toUser,    "type": 1  };  ///传递信息  sendMessage(message); //发送信息  setState(() { //更新ui    UserMessage.messages.add(      ChatMessage(          messageContent: _controller.text,          messageType: "sender"),    );    _controller.clear(); //清除输入框内文字  });},



  • 在退出页面时,关闭WebSocket连接


    @overridevoid dispose() {  // TODO: implement dispose  super.dispose();  closeSocket();}



5.该文章项目运行步骤




  • 下载代码压缩包,解压后的目录是这样的:




1633654613(1).png


其中images是图片,lib是源代码,jar包是服务端


第一步:创建一个新项目,源代码是使用了空安全的,也可以创建不是空安全的项目,改一下代码即可


第二步:将images与lib复制进去


第三步:在pubspec.yaml中配置静态图片


assets:  - images/

第四步:运行.jar包


切换到包存放的地址,服务端的端口为9090,如果与各位的端口冲突可以修改服务端代码,或者停止你在使用9090的这个端口


java -jar 包名.jar

查找端口:


netstat -aon|findstr 9090

查看指定 PID 的进程


tasklist|findstr 10021

结束进程


强制(/F参数)杀死 pid 为 10021的所有进程包括子进程(/T参数):


taskkill /T /F /PID 9088 

第五步:在cmd中查找自己的ip,然后在代码的这里修改


1633656694(1).png


第六步:运行后的操作


运行后会先进入登录界面,这里第一只手机输入0 ,第二只手机输入1,因为服务端默认从0开始(给用户分配id),因为没有数据库。


1633656155(1).png


这样进入就可以了,然后就可以像效果图一样开始交流


服务端有问题可以参考这篇文章:Springboot WebSocket 即时通讯



作者:阿Tya
链接:https://juejin.cn/post/7016606314025451557

收起阅读 »

设计模式-工厂方法模式

工厂方法模式(Factory Method)又称为多态性工厂模式,其核心不再像简单工厂模式那样负责所有的子类的创建,而是将具体的创建工作交给子类去完成 在前文已经介绍简单工厂模式 时,写了如下代码: /** * type:角色类型 - 管理员、员工 * n...
继续阅读 »

工厂方法模式(Factory Method)又称为多态性工厂模式,其核心不再像简单工厂模式那样负责所有的子类的创建,而是将具体的创建工作交给子类去完成


在前文已经介绍简单工厂模式 时,写了如下代码:


/**
* type:角色类型 - 管理员、员工
* name:对应角色的名字
*/
const Factory = (type, name) => {
switch (type) {
case "admin": // 创建管理员
return new Admin(name, ["user", "salary", "vacation"]);
case "staff": // 创建员工
return new Staff(name, ["vacation"]);
default: // 健壮性处理
throw new Error("暂不支持该角色的创建");
}
};

虽然其可以让我们在创建实例的时候很爽,不用关心内部具体的实现,通过观察代码,可以发现其存在的问题:



  • 不符合设计原则-开放封闭原则


当每一次新增一个权限角色的时候,对需要对上面的代码进行修改,严重破坏了原有的代码和业务逻辑,与开放封闭原则背离



  • 容易变成面条代码


虽然角色越来越多,那么内部的case也会随之变得越来越多,简单工厂函数的内容也随着变得冗余


理想情况下,我们是希望在新增新的权限角色时,对于老的代码无任何的修改便可以完成新功能的增加


首先看一下工厂方法模式的UML:


image-20211006145801692.png


其相比简单工厂模式,会多了一个工厂方法,Admin类对应多了一个AdminFactory类,现在只需要通过AdminFactory类创建实例即可


接下来看看工厂方法模式如何创建:


class Person {
constructor(name, permission) {
this.name = name;
this.permission = permission;
}
}

/**
* 管理员
*/
class Admin extends Person {
constructor(name, permission) {
super(name, permission);
}
}

/**
* 管理员的工厂方法
*/
class AdminFactory {
static create(name) {
return new Admin(name, ["user", "salary", "vacation"]);
}
}

/**
* 员工
*/
class Staff extends Person {
constructor(name, permission) {
super(name, permission);
}
}

/**
* 员工的工厂方法
*/
class StaffFactory {
static create(name) {
return new Staff(name, ["vacation"]);
}
}

const admin = AdminFactory.create("管理员");
const zs = StaffFactory.create("张三");
const ls = StaffFactory.create("李四");

若是需要创建新的权限角色,只需要创建对应的工厂方法即可,完全符合开放封闭原则,也可以避免面条式代码,具体实例创建都是通过对应工厂方法创建


作者:Nordon
链接:https://juejin.cn/post/7016149646334492679

收起阅读 »

设计模式-适配器模式

适配器模式又称为包装器模式,将一个类的接口转化为用户需要的另外一个接口,主要是为了解决对象之间接口不兼容的问题,比如随着业务迭代升级出现了旧的接口与心的接口不兼容,这个时候不可能强制使用旧接口的用户去升级,而是在中间加一个适配器进行转换,让旧接口的使用者无感使...
继续阅读 »

适配器模式又称为包装器模式,将一个类的接口转化为用户需要的另外一个接口,主要是为了解决对象之间接口不兼容的问题,比如随着业务迭代升级出现了旧的接口与心的接口不兼容,这个时候不可能强制使用旧接口的用户去升级,而是在中间加一个适配器进行转换,让旧接口的使用者无感使用,保证了稳定性,在日常生活中适配器的案例随处可见,比如耳机插口不统一、充电接口不统一等,这个时候就需要一个适配器来解决问题


UML:


image-20211007135111668.png


接下看一下UML对应的代码实现:


class Target {
constructor(type) {
let result;

switch (type) {
case "adapter":
result = new Adapter();
break;
default:
result = null;
}
return result;
}

Request() {
console.log('Target Request');
}
}

class Adaptee {
constructor() {
console.log("Adaptee created");
}

SpecificRequest() {
console.log("Adaptee request");
}
}

class Adapter extends Adaptee {
constructor() {
super();
console.log("Adapter created");
}

Request() {
return this.SpecificRequest();
}
}

function init_Adapter() {
var f = new Target("adapter");
f.Request();
}

init_Adapter();

应用场景


开发中常用的axios,其支持node端和浏览器端,那么在不同端调用axios需要进行不同的处理,而这些对于使用者而言都是无感的,我们在使用的时候都是使用同一套API直接干就完事了,不会在意内部具体做了些什么,这个时候就需要使用适配器来抹平不同端的差异,让使用者用着开心


接下来可以模拟简单实现这个过程


使用axios请求一个接口:


axios({
url: "xxx",
method: "GET",
})
.then((res) => {
console.log("success:", res);
})
.catch((err) => {
console.log("fail:", err);
});

接下来需要手动实现axios函数:


function axios(config) {
let adaptor = getDefaultAdaptor();

// 无论是node端 还是 浏览器端,在使用的时候都只是转入一个config配置对象,返回一个 Promise 对象
return adaptor(config);
}

上文说到因为axios可以在浏览器端和node端使用,getDefaultAdaptor函数就是起到适配器的作用,根据不同的环境分别创建不同的adaptor


/**
* 适配器
*/
function getDefaultAdaptor() {
let adaptor;

if (typeof XMLHttpRequest !== "undefined") {
// 是浏览器环境
adaptor = xhr;
} else if (typeof process !== "undefined") {
// node 环境
adaptor = http;
}

return adaptor;
}

其中xhr和http为两个函数,用于创建具体的请求,至此适配器的使用已经完成,接下来就看看不同端是如何实现的接口请求


浏览器端:


/**
* 浏览器环境
*/
function xhr(config) {
return new Promise((resolve, reject) => {
const req = new XMLHttpRequest();
req.open(config.method, config.url, true);

req.onreadystatechange = function () {
if (req.readyState === 4) {
if (req.status >= 200 && req.status < 300) {
resolve(req.responseText);
} else {
reject("请求失败");
}
}
};

req.send();
});
}

node端:


/**
* node 环境
*/
function http(config) {
const url = require("url");
const http = require("http");
// 将需要的参数 解析出来
const { hostname, port, path } = url.parse(config.url);
return new Promise((resolve, reject) => {
const options = {
hostname,
port,
path,
method: config.method,
};

const req = http.request(options, function (response) {
let chunks = [];

response.on("data", (chunk) => {
chunks.push(chunk);
});

response.on("end", () => {
const res = Buffer.concat(chunks).toString();
resolve(res);
});
});

// 监听请求异常
req.on("error", (err) => {
reject(err);
});

// 请求发送完毕
ren.end();
});
}

作者:Nordon
链接:https://juejin.cn/post/7016215674322157581

收起阅读 »

两个 Node.js 进程如何通信?

两个 Node.js 进程之间如何进行通信呢?这里要分两种场景: 不同电脑上的两个 Node.js 进程间通信 同一台电脑上两个 Node.js 进程间通信 对于第一种场景,通常使用 TCP 或 HTTP 进行通信,而对于第二种场景,又分为两种子场景: ...
继续阅读 »

两个 Node.js 进程之间如何进行通信呢?这里要分两种场景:



  1. 不同电脑上的两个 Node.js 进程间通信

  2. 同一台电脑上两个 Node.js 进程间通信


对于第一种场景,通常使用 TCP 或 HTTP 进行通信,而对于第二种场景,又分为两种子场景:



  1. Node.js 进程和自己创建的 Node.js 子进程通信

  2. Node.js 进程和另外不相关的 Node.js 进程通信


前者可以使用内置的 IPC 通信通道,后者可以使用自定义管道,接下来进行详细介绍:


不同电脑上的两个 Node.js 进程间通信


要想进行通信,首先得搞清楚如何标识网络中的进程?网络层的 ip 地址可以唯一标识网络中的主机,而传输层的协议和端口可以唯一标识主机中的应用程序(进程),这样利用三元组(ip 地址,协议,端口)就可以标识网络的进程了。


使用 TCP 套接字


TCP 套接字(socket)是一种基于 TCP/IP 协议的通信方式,可以让通过网络连接的计算机上的进程进行通信。一个作为 server 另一个作为 client,server.js 代码如下:


const net = require('net')
const server = net.createServer(socket => {
console.log('socket connected')
socket.on('close', () => console.log('socket disconnected'))
socket.on('error', err => console.error(err.message))
socket.on('data', data => {
console.log(`receive: ${data}`)
socket.write(data)
console.log(`send: ${data}`)
})
})
server.listen(8888)

client.js 代码:


const net = require('net')
const client = net.connect(8888, '192.168.10.105')

client.on('connect', () => console.log('connected.'))
client.on('data', data => console.log(`receive: ${data}`))
client.on('end', () => console.log('disconnected.'))
client.on('error', err => console.error(err.message))

setInterval(() => {
const msg = 'hello'
console.log(`send: ${msg}`)
client.write(msg)
}, 3000)

运行效果:


$ node server.js
client connected
receive: hello
send: hello

$ node client.js
connect to server
send: hello
receive: hello

使用 HTTP 协议


因为 HTTP 协议也是基于 TCP 的,所以从通信角度看,这种方式本质上并无区别,只是封装了上层协议。server.js 代码为:


const http = require('http')
http.createServer((req, res) => res.end(req.url)).listen(8888)

client.js 代码:


const http = require('http')
const options = {
hostname: '192.168.10.105',
port: 8888,
path: '/hello',
method: 'GET',
}
const req = http.request(options, res => {
console.log(`statusCode: ${res.statusCode}`)
res.on('data', d => process.stdout.write(d))
})
req.on('error', error => console.error(error))
req.end()

运行效果:


$ node server.js
url /hello

$ node client.js
statusCode: 200
hello

同一台电脑上两个 Node.js 进程间通信


虽然网络 socket 也可用于同一台主机的进程间通讯(通过 loopback 地址 127.0.0.1),但是这种方式需要经过网络协议栈、需要打包拆包、计算校验和、维护序号和应答等,就是为网络通讯设计的,而同一台电脑上的两个进程可以有更高效的通信方式,即 IPC(Inter-Process Communication),在 unix 上具体的实现方式为 unix domain socket,这是服务器端和客户端之间通过本地打开的套接字文件进行通信的一种方法,与 TCP 通信不同,通信时指定本地文件,因此不进行域解析和外部通信,所以比 TCP 快,在同一台主机的传输速度是 TCP 的两倍。


使用内置 IPC 通道


如果是跟自己创建的子进程通信,是非常方便的,child_process 模块中的 fork 方法自带通信机制,无需关注底层细节,例如父进程 parent.js 代码:


const fork = require("child_process").fork
const path = require("path")
const child = fork(path.resolve("child.js"), [], { stdio: "inherit" });
child.on("message", (message) => {
console.log("message from child:", message)
child.send("hi")
})

子进程 child.js 代码:


process.on("message", (message) => {
console.log("message from parent:", message);
})

if (process.send) {
setInterval(() => process.send("hello"), 3000)
}

运行效果如下:


$ node parent.js
message from child: hello
message from parent: hi
message from child: hello
message from parent: hi

使用自定义管道


如果是两个独立的 Node.js 进程,如何建立通信通道呢?在 Windows 上可以使用命名管道(Named PIPE),在 unix 上可以使用 unix domain socket,也是一个作为 server,另外一个作为 client,其中 server.js 代码如下:


const net = require('net')
const fs = require('fs')

const pipeFile = process.platform === 'win32' ? '\\\\.\\pipe\\mypip' : '/tmp/unix.sock'

const server = net.createServer(connection => {
console.log('socket connected.')
connection.on('close', () => console.log('disconnected.'))
connection.on('data', data => {
console.log(`receive: ${data}`)
connection.write(data)
console.log(`send: ${data}`)
})
connection.on('error', err => console.error(err.message))
})

try {
fs.unlinkSync(pipeFile)
} catch (error) {}

server.listen(pipeFile)


client.js 代码如下:


const net = require('net')

const pipeFile = process.platform === 'win32' ? '\\\\.\\pipe\\mypip' : '/tmp/unix.sock'

const client = net.connect(pipeFile)
client.on('connect', () => console.log('connected.'))
client.on('data', data => console.log(`receive: ${data}`))
client.on('end', () => console.log('disconnected.'))
client.on('error', err => console.error(err.message))

setInterval(() => {
const msg = 'hello'
console.log(`send: ${msg}`)
client.write(msg)
}, 3000)


运行效果:


$ node server.js 
socket connected.
receive: hello
send: hello

$ node client.js
connected.
send: hello
receive: hello

作者:乔珂力
链接:https://juejin.cn/post/7016233869565231135

收起阅读 »

还在为后端提供接口慢而发愁?试试这个,自己动手丰衣足食

介绍 本期给大家介绍一个可以随机模拟数据的库——mock.js,它是一个模拟数据生成器,帮助前端开发和原型与后端进程分离,减少一些开发时联调测试成本。它支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等数据的模拟。而且不需要修改既有代码,就可以拦...
继续阅读 »

介绍


本期给大家介绍一个可以随机模拟数据的库——mock.js,它是一个模拟数据生成器,帮助前端开发和原型与后端进程分离,减少一些开发时联调测试成本。它支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等数据的模拟。而且不需要修改既有代码,就可以拦截 Ajax 请求,返回模拟的响应数据。


VID_20210920_203040.gif


看就是这么简单,我们实现可以与后端协定好接口格式和字段,然后可以在前端开发中拦截ajax请求把模拟数据直接传过去,也可以通过在线平台或搭建中端做接口模拟。后面,等后端完成接口开发,就可以直接替换接口就行了,非常提高效率。


他的API有非常丰富,我们今天先来学习一下他常用的API有哪些,看懂后绝大部分数据就直接用了。


正文


1.指定数量


A.固定数量


'name|num': any

Mock.mock({
"star|3": "★",
"num|3": 1,
"arr|3": [{"level":1}],
"obj|3":{"a":1,"b":2,"c":3,"d":4,"e":5},
"bool|3":false
})

例如,输入固定值3则:



  • star:连续返回3个★

  • num:固定返回数值3

  • arr:返回数组内3个对象,如果不够固定值则从第一个值开始重复下去直到填满

  • obj:随机选择3个对象里的属性返回出来,如果不够固定值则有多少返回多少

  • bool:随机true和false


微信截图_20211007110316.png


B. 生成随机数量


'name|min-max': any

Mock.mock({
"star|1-5": "★",
"num|1-5": 1,
"arr|1-5": [{"level":1}],
"obj|1-5":{"a":1,"b":2,"c":3,"d":4,"e":5},
"bool|1-5":false
})

例如,star随机生成1到5个★,如图其后皆为随机1到5个


微信截图_20211007110333.png


2.数值自加


适用场景:ID值等唯一值在列表中的模拟。


'name|+number': number|array

Mock.mock({
"id|+1": 1,
"code|+1": ["a","b","c","d","e"]
})

例如,在模拟下列数据,中将每隔一个数据,id值都会在上一个值得基础上自加5。


微信截图_20211007110534.png


3.随机颜色


mock内置随机颜色的函数 —— @color | @hsl | @rgba | @rgb | @hex


Mock.mock({
"status": 1,
"msg": "success",
"data": {
"list|10": [{
"id|+1": 1,
"color": "@color"
}]
}
})

微信截图_20211007134439.png


4.生成图片


mock内置了生成随机图片的函数 —— @image


Random.image( size?, background?, foreground?, format?, text? )


  • size:尺寸,如100x100

  • background:背景色,如#FFFFFF

  • foreground:前景色,也就是图片中文字颜色,如#333333

  • format:图片类型,如png

  • text:图片中文字


Mock.mock({
"status": 1,
"msg": "success",
"data": {
"list|10": [{
"id|+1": 1,
"icon": "@image(240x160, @color, #FFFFFF, png, icon)"
}]
}
})

微信截图_20211007134055.png


微信截图_20211007134134.png


5.内置常用函数


mock出了随机颜色和图片之外,还内置了很内置函数,接下来我们介绍几个使用最频繁的几个:


Mock.mock({
"status": 1,
"msg": "success",
"data": {
"list|5": [{
"id": "@id",
"name": "@cname",
"title":"@ctitle(3,10)",
"date":"@datetime",
"email":"@email",
"des": "@cparagraph(1,3)",
"province":"@province",
"city":"@city(true)",
"address":"@county(true)",
"link": "@url"
}]
}
})


  • @id:随机生成唯一值

  • @cname:随机生成中文名字

  • @ctitle(min?,max?):随机生成中文标题

  • @date("yyyy-MM-dd HH:mm:ss"):随机生成时间

  • @email:随机生成邮箱地址

  • @cparagraph(min?,max?):随机生成中文段落

  • @province:随机生成省

  • @city(boolean?):随机生成市,默认为false,不传则只显示市,不显示省,传入true,则省市全显示

  • @county(boolean?):随机生成区,默认为false,不传则只显示区,不显示省市,传入true,则省市区全显示

  • @url:随机生成链接


微信截图_20211007140247.png


结语


相信看完以上五条规则,你已经完全可以自由模拟想要的数据了,此时,我们可以通过用node搭建一个中端做个测试地址去处理要模拟的数据。或者用fastmock,这类线上平台去实现,他们同样支持mock API。总之,不管那种方案,都会对团队开发的效率有极大的提升。


作者:jsmask
链接:https://juejin.cn/post/7016203778378432525

收起阅读 »

?十分钟学会打字动画

前言 背景知识:基本的CSS动画,逐帧动画,闪烁效果 有些时候,我们希望一段文本中的字符逐个显现,模拟出一种打字的效果。这个效果在技术类网站中尤为流行,用等宽字体可以营造出一种终端命令行的感觉。如果使用得当,它确实可以让整个网页的设计感提升一个档次。 这篇...
继续阅读 »

前言



背景知识:基本的CSS动画逐帧动画闪烁效果



有些时候,我们希望一段文本中的字符逐个显现,模拟出一种打字的效果。这个效果在技术类网站中尤为流行,用等宽字体可以营造出一种终端命令行的感觉。如果使用得当,它确实可以让整个网页的设计感提升一个档次。


这篇文章我们就用CSS来实现一下这个效果~✨


解决方案


核心思路就是让容器的宽度成为动画的主体:


把所有文本包裹在这个容器中,然后让它的宽度从0开始以步进动画的方式、一个字一个字地扩张到它应有的宽度。


你可能已经察觉到了,这个方法是有局限的:它并不适用于多行文本。


好的,我们开始写代码吧!假设我们需要把这个动画效果应用到h1标题上,并且已经它把设置为等宽字体了,结构代码如下所示


<h1>CSS is awesome!</h1>

🧑1.0版本


我们很容易地给他加上动画,让他宽度从0变化完整的宽度


/*
*1.0版本
*/
@keyframes typing {
0% { width: 0 }
}

h1 {
background: lightgreen;
font: bold 200% Consolas, Monaco, monospace;
width: 8.25em;
animation: typing 8s ;
}

20211007_145558.gif
我们可以看到产生的效果简直就是车祸现场😱,跟我们想要的的打字效果没有一点关系。


👦1.1版本


宝儿你可能已经猜到问题出在哪儿了,我们忘了用white-space:nowrap;来阻止文本折行,因此文本的行数会随着宽度的扩张不断变化。其次,我们忘了加上overflow: hidden;,所以超出宽度的文本没有被裁切掉。我们完善一下代码


/*
*1.1版本
*/
@keyframes typing {
0% { width: 0 }
}

h1 {
background: lightgreen;
font: bold 200% Consolas, Monaco, monospace;
width: 8.25em;
white-space: nowrap;
overflow: hidden;
animation: typing 8s steps(15);
}

20211007_144918.gif



值得注意的是我们这里的逐帧动画是通过设置steps实现的,我们所需要的步进数量是由字符的数量来决定的,这显然是很难维护的,而且对于动态文字来说更是不可能维护的。不过,我们稍后将看到,可以用一小段JavaScript代码来把这件事情自动化。



🕵️‍♂️2.0版本


20211007_150221.gif


/*
*2.0版本
*/
@keyframes typing {
0% { width: 0 }
}

@keyframes caret {
50% { border-right-color: transparent; }
}

h1 {
/*background: lightgreen;*/
font: bold 200% Consolas, Monaco, monospace;
/* width: 8.25em; */
width: 15ch;
white-space: nowrap;
overflow: hidden;
border-right: .05em solid;
animation: typing 8s steps(15),
caret 1s steps(1) infinite;
}

2.0版本我们我们使用了ch单位,这个ch单位是由CSS值与单位(第三版规范引入的一个新单位,表示“0”字形的宽度。它应该是最不为人知的一个新单位,因为在绝大多数场景下,我们并不关心0这个字符显示出来到底有多宽。但对等宽字体来说,这是个例外。在等宽字体中,“0”字形的宽度和其他所有字形的宽度是一样的。因此,如果我们用ch单位来表达这个标题的宽度,那取值实际上就是字符的数量,在本例子中,也就是15。


同时我们添加了闪烁光标,我们用右边框来模拟光标效果,同时给光标设置闪烁的动画,将光标设置为透明即可,要注意的,光标的动画是无限循环的!



这个动画现在的表现相当完美了,不过还不是很易于维护:需要根据每个标题的字数来给它们分别指定不同的宽度样式,而且还需要在每次改变标题内容时同步更新这些宽度样式。显然,这种场景正是JavaScript的用武之地:



🕵️‍♀️2.1终极版本


20211007_150221.gif


/*
*2.1终极版本
*/
@keyframes typing {
0% { width: 0 }
}

@keyframes caret {
50% { border-right-color: transparent; }
}

h1 {
font: bold 200% Consolas, Monaco, monospace;
white-space: nowrap;
overflow: hidden;
border-right: .05em solid;
animation: typing 8s,
caret 1s steps(1) infinite;
}

const doc = document.querySelector('h1')
let len = doc.textContent.length, s = doc.style
s.width = len + 'ch'
s.animationTimingFunction = 'steps('+len+')'

只需短短几行JavaScript代码,我们避免了手动计算字符数量和步进数量,取得两全其美的结果:不仅动画生动逼真,而且代码易于维护!



链接:https://juejin.cn/post/7016226271067635748

收起阅读 »

当面试官问Webpack的时候他想知道什么

前言 在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。 说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和...
继续阅读 »

前言


在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。


说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和五花八门的功能感到陌生。尤其当我们使用诸如umi.js之类的应用框架还帮我们把webpack配置再封装一层的时候,webpack的本质似乎离我们更加遥远和深不可测了。


当面试官问你是否了解webpack的时候,或许你可以说出一串耳熟能详的webpack loaderplugin的名字,甚至还能说出插件和一系列配置做按需加载和打包优化,那你是否了解他的运行机制以及实现原理呢,那我们今天就一起探索webpack的能力边界,尝试了解webpack的一些实现流程和原理,拒做API工程师。


CgqCHl6pSFmAC5UzAAEwx63IBwE024.png


你知道webpack的作用是什么吗?


从官网上的描述我们其实不难理解,webpack的作用其实有以下几点:




  • 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。




  • 编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpackLoader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。




  • 能力扩展。通过webpackPlugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。




说一下模块打包运行原理?


如果面试官问你Webpack是如何把这些模块合并到一起,并且保证其正常工作的,你是否了解呢?


首先我们应该简单了解一下webpack的整个打包流程:



  • 1、读取webpack的配置参数;

  • 2、启动webpack,创建Compiler对象并开始解析项目;

  • 3、从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;

  • 4、对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;

  • 5、整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。


其中文件的解析与构建是一个比较复杂的过程,在webpack源码中主要依赖于compilercompilation两个核心对象实现。


compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。
compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。


而每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过Loader解析完成之后,会通过acorn库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。


最终Webpack打包出来的bundle文件是一个IIFE的执行函数。


// webpack 5 打包的bundle文件内容

(() => { // webpackBootstrap
var __webpack_modules__ = ({
'file-A-path': ((modules) => { // ... })
'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
})

// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};

// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

// Return the exports of the module
return module.exports;
}

// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})

webpack4相比,webpack5打包出来的bundle做了相当的精简。在上面的打包demo中,整个立即执行函数里边只有三个变量和一个函数方法,__webpack_modules__存放了编译后的各个文件模块的JS内容,__webpack_module_cache__ 用来做模块缓存,__webpack_require__Webpack内部实现的一套依赖引入函数。最后一句则是代码运行的起点,从入口文件开始,启动整个项目。


其中值得一提的是__webpack_require__模块引入函数,我们在模块化开发的时候,通常会使用ES Module或者CommonJS规范导出/引入依赖模块,webpack打包编译的时候,会统一替换成自己的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性。


你知道sourceMap是什么吗?


提到sourceMap,很多小伙伴可能会立刻想到Webpack配置里边的devtool参数,以及对应的evaleval-cheap-source-map等等可选值以及它们的含义。除了知道不同参数之间的区别以及性能上的差异外,我们也可以一起了解一下sourceMap的实现方式。


sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug问题会带来非常糟糕的体验,sourceMap可以帮助我们快速定位到源代码的位置,提高我们的开发效率。sourceMap其实并不是Webpack特有的功能,而是Webpack支持sourceMap,像JQuery也支持souceMap


既然是一种源码的映射,那必然就需要有一份映射的文件,来标记混淆代码里对应的源码的位置,通常这份映射文件以.map结尾,里边的数据结构大概长这样:


{
"version" : 3, // Source Map版本
"file": "out.js", // 输出文件(可选)
"sourceRoot": "", // 源文件根目录(可选)
"sources": ["foo.js", "bar.js"], // 源文件列表
"sourcesContent": [null, null], // 源内容列表(可选,和源文件列表顺序一致)
"names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
"mappings": "A,AAAB;;ABCDE;" // 带有编码映射数据的字符串
}

其中mappings数据有如下规则:



  • 生成文件中的一行的每个组用“;”分隔;

  • 每一段用“,”分隔;

  • 每个段由1、4或5个可变长度字段组成;


有了这份映射文件,我们只需要在我们的压缩代码的最末端加上这句注释,即可让sourceMap生效:


//# sourceURL=/path/to/file.js.map

有了这段注释后,浏览器就会通过sourceURL去获取这份映射文件,通过解释器解析后,实现源码和混淆代码之间的映射。因此sourceMap其实也是一项需要浏览器支持的技术。


如果我们仔细查看webpack打包出来的bundle文件,就可以发现在默认的development开发模式下,每个_webpack_modules__文件模块的代码最末端,都会加上//# sourceURL=webpack://file-path?,从而实现对sourceMap的支持。


sourceMap映射表的生成有一套较为复杂的规则,有兴趣的小伙伴可以看看以下文章,帮助理解soucrMap的原理实现:


Source Map的原理探究


Source Maps under the hood – VLQ, Base64 and Yoda


是否写过Loader?简单描述一下编写loader的思路?


从上面的打包代码我们其实可以知道,Webpack最后打包出来的成果是一份Javascript代码,实际上在Webpack内部默认也只能够处理JS模块代码,在打包过程中,会默认把所有遇到的文件都当作 JavaScript代码进行解析,因此当项目存在非JS类型文件时,我们需要先对其进行必要的转换,才能继续执行打包任务,这也是Loader机制存在的意义。


Loader的配置使用我们应该已经非常的熟悉:


// webpack.config.js
module.exports = {
// ...other config
module: {
rules: [
{
test: /^your-regExp$/,
use: [
{
loader: 'loader-name-A',
},
{
loader: 'loader-name-B',
}
]
},
]
}
}

通过配置可以看出,针对每个文件类型,loader是支持以数组的形式配置多个的,因此当Webpack在转换该文件类型的时候,会按顺序链式调用每一个loader,前一个loader返回的内容会作为下一个loader的入参。因此loader的开发需要遵循一些规范,比如返回值必须是标准的JS代码字符串,以保证下一个loader能够正常工作,同时在开发上需要严格遵循“单一职责”,只关心loader的输出以及对应的输出。


loader函数中的this上下文由webpack提供,可以通过this对象提供的相关属性,获取当前loader需要的各种信息数据,事实上,这个this指向了一个叫loaderContextloader-runner特有对象。有兴趣的小伙伴可以自行阅读源码。


module.exports = function(source) {
const content = doSomeThing2JsString(source);

// 如果 loader 配置了 options 对象,那么this.query将指向 options
const options = this.query;

// 可以用作解析其他模块路径的上下文
console.log('this.context');

/*
* this.callback 参数:
* error:Error | null,当 loader 出错时向外抛出一个 error
* content:String | Buffer,经过 loader 编译后需要导出的内容
* sourceMap:为方便调试生成的编译后内容的 source map
* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
*/
this.callback(null, content);
// or return content;
}

更详细的开发文档可以直接查看官网的 Loader API


是否写过Plugin?简单描述一下编写plugin的思路?


如果说Loader负责文件转换,那么Plugin便是负责功能扩展。LoaderPlugin作为Webpack的两个重要组成部分,承担着两部分不同的职责。


上文已经说过,webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务,从而实现自己想要的功能。


既然基于发布订阅模式,那么知道Webpack到底提供了哪些事件钩子供插件开发者使用是非常重要的,上文提到过compilercompilationWebpack两个非常核心的对象,其中compiler暴露了和 Webpack整个生命周期相关的钩子(compiler-hooks),而compilation则暴露了与模块和依赖有关的粒度更小的事件钩子(Compilation Hooks)。


Webpack的事件机制基于webpack自己实现的一套Tapable事件流方案(github


// Tapable的简单使用
const { SyncHook } = require("tapable");

class Car {
constructor() {
// 在this.hooks中定义所有的钩子事件
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}

/* ... */
}


const myCar = new Car();
// 通过调用tap方法即可增加一个消费者,订阅对应的钩子事件了
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());

Plugin的开发和开发Loader一样,需要遵循一些开发上的规范和原则:



  • 插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例;

  • 传给每个插件的 compilercompilation 对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件;

  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住;


了解了以上这些内容,想要开发一个 Webpack Plugin,其实也并不困难。


class MyPlugin {
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);

// do something...
})
}
}

更详细的开发文档可以直接查看官网的 Plugin API


最后


本文也是结合一些优秀的文章和webpack本身的源码,大概地说了几个相对重要的概念和流程,其中的实现细节和设计思路还需要结合源码去阅读和慢慢理解。


Webpack作为一款优秀的打包工具,它改变了传统前端的开发模式,是现代化前端开发的基石。这样一个优秀的开源项目有许多优秀的设计思想和理念可以借鉴,我们自然也不应该仅仅停留在API的使用层面,尝试带着问题阅读源码,理解实现的流程和原理,也能让我们学到更多知识,理解得更加深刻,在项目中才能游刃有余的应用。




链接:https://juejin.cn/post/6943468761575849992

收起阅读 »

面试官,我实现了一个 Chrome Devtools

网页会加载资源、运行 JS、渲染界面、存储数据等,我们开发时怎么看到执行的状态呢? 用调试工具 chrome devtools。它支持 dom 调试、JS debugger、本地存储的展示、运行时间的 profile 等。 Node.js 也是同样,不过它只支...
继续阅读 »

网页会加载资源、运行 JS、渲染界面、存储数据等,我们开发时怎么看到执行的状态呢? 用调试工具 chrome devtools。它支持 dom 调试、JS debugger、本地存储的展示、运行时间的 profile 等。


Node.js 也是同样,不过它只支持 JS debugger 和 profile。我们可以通过 chrome devtools 或者 vscode debugger 等来调试。


这些工具都是远程 attach 到运行的程序上来调试的,之间怎么交互数据呢? 通过 webSocket。而且还制定了 chrome devtools protocol 的协议,规定了有什么能力,如何通信。


这种基于 websocket 的调试协议叫做 chrome devtools protocol
因为功能比较多,所以分了多个域(一般复杂的东西都会分域),包括 DOM、Debugger、Network、Page 等等,分别放不同的调试协议。chrome devtools 就是通过这个协议实现的调试。


新版 chrome(金丝雀版)可以打开设置中的实验特性的 Protocol Monitor 面板。



就可以看到传输的 CDP 数据:



这就是 chrome devtools 的原理。


理解了这个原理有什么用呢?


我们可以重新实现服务端,只要对接了调试协议,那么就能够用 chrome devtools 来调试。


比如 kraken(把 css 渲染到 flutter)是怎么做到用 chrome devtools 调试 dom 和样式的?就是对接了这个协议。


我们可以重新实现客户端,只要对接了这个协议,那就可以用任何工具调试网页/Node.js。


大家用 chrome devtools 可以调试 Node.js 和网页,用 vscode debugger 也可以,用 webstorm debugger 也可以。为什么呢?因为它们都对接了这个协议。


那我们是不是可以对接这个协议实现一个类似 chrome devtools 的调试工具呢?


我们来实验下:


我们启动 chrome,通过 --remote-debugging-port 指定调试端口:


/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary --remote-debugging-port=9222

然后连上它。


这里我们不用直接对接协议,chrome 提供了各种语言的 sdk,调用 api 就行:



我们先连接上 chrome:


const CDP = require('chrome-remote-interface');

async function test() {
let client;
try {
client = await CDP();
const { Page, DOM, Debugger } = client;
//...
} catch(err) {
console.error(err);
}
}
test();

然后打开 baidu.com,等 2s,做个截图:


const CDP = require('chrome-remote-interface');
const fs = require('fs');

async function test() {
let client;
try {
client = await CDP();
const { Page, DOM, Debugger } = client;

await Page.enable();
await Page.navigate({url: 'https://baidu.com'});

await new Promise(resolve => setTimeout(resolve, 2000));

const res = await Page.captureScreenshot();
fs.writeFileSync('./screenshot.jpg', res.data, {
encoding: 'base64'
});
} catch(err) {
console.error(err);
}
}
test();

查看下效果:



这样,我们就跑通了 CDP 的第一段代码。


其余的功能,包括 Network、Debugger、DOM 等等也能实现,我们简单试一下:


await DOM.enable();

const { root } = await DOM.getDocument({
depth: -1
});

depth 为深度,设置为 -1 就是返回整个 dom:



有了这些数据我们是不是可以做 DOM 的浏览呢?


还有 DOM.setAttributeValue 可以设置属性、DOM.getBoxModel 拿到盒模型大小等。


基于这些,我们做个 dom 编辑器没问题吧。


还有网络部分:


await Network.enable();
Network.on('responseReceived', async evt => {
const res = await Network.getResponseBody({
requestId: evt.requestId
});

console.log(evt.response.url);
console.log(res.body);
});

我们通过 responseReceived 事件监听每一个响应,然后再通过 Network.getResponseBody 拿到响应的内容:




基于这些,我们实现 Network 面板的功能没问题吧。


还可以对接 profiler:


await Profiler.start();
await new Promise(resolve => setTimeout(resolve,2000));
const { profile } = await Profiler.stop();


有这些数据,我们就可以通过 canvas 画出个火焰图出来。


理论上来说,chrome devtools 的所有功能我们都能实现。而且,一个网页同时用多个调试工具调试是可以的,因为 websocket 本来就可以有多个客户端。



可能你会说自己实现 chrome devtools 有什么意义?


大家自己做开源前端项目的时候,一般都是写个网易云音乐客户端,因为有现成的数据可以用。那为什么不做个 chrome devtools 呢?也有现成的数据啊,启动浏览器就行,而且这个逼格多高啊。


我们也不用实现完整的 chrome devtools,可以单把网络部分、单把 DOM 部分、单把 debugger 部分实现了,可以做不同的 UI,可以做 chrome devtools 没有的功能和交互。


比如你面试可视化岗位,你说你对接了 chrome devtools protocol 的 profiler 部分,用 canvas 画了个火焰图,会加分很多的。


总结


Chrome 的调试是通过 WebSocket 和调试客户端通信,制定了 Chrome Devtools Protocol 的协议,Node.js 也是,不过协议叫做 V8 debugger protocol。我们可以通过 protocol monitor 的面板看到所有的 CDP 协议请求响应。


可以实现 CDP 服务端,来对接 chrome devtools 的调试功能,调试不同的目标,比如 kraken 渲染引擎。


可以实现 CDP 客户端,来用不同的工具调试,比如 vscode debugger、webstorm debugger 等。


我们也可以通过 sdk 的 api 来和 CDP 服务端对接,拿到数据,实现调试的功能。比如单独实现 DOM 编辑器、Network 查看器、JS Debugger、 Profiler 和火焰图都可以,而且可以做到比 chrome devtools 更强的功能,更好的交互。


当大家想做开源项目没有数据的时候,不妨考虑下做个 CDP 客户端,这不比云音乐项目香么?


作者:zxg_神说要有光
链接:https://juejin.cn/post/7012853971362512933
收起阅读 »

10分钟教你自动化部署前端项目

背景 前几年,作为小白的我,只需要安安心心写前端代码就行,前端代码部署的事情直接交给运维人员去部署,部署到哪台服务器,这些都不需要我们关心。 突然,运维人员离职了,没办法,业务需要又起了一个项目,没有运维人员的我们只能每次本地编译完毕,然后手动同步到服务器目录...
继续阅读 »

背景


前几年,作为小白的我,只需要安安心心写前端代码就行,前端代码部署的事情直接交给运维人员去部署,部署到哪台服务器,这些都不需要我们关心。


突然,运维人员离职了,没办法,业务需要又起了一个项目,没有运维人员的我们只能每次本地编译完毕,然后手动同步到服务器目录,累成个狗了!累就要想办法解决。


GitLab CI


我们的项目中经常有一个.yml文件,有很多人不知道这个文件是干什么用的。


GitLab CI/CD(后简称 GitLab CI)是一套基于 GitLab 的 CI/CD 系统,可以让开发人员通过 .gitlab-ci.yml 在项目中配置 CI/CD 流程,在提交后,系统可以自动/手动地执行任务,完成 CI/CD 操作。


说的通俗一点,就是这个yml文件的作用是我们提交代码到GitLab以后,GitLab CI会自动读取yml文件中的内容,进行对应的操作。gitlab只是代码仓库,想要实现CI/CD,我们需要安装gitlab-runner。gitlab-runner相当于任务执行器。


下载安装


例如我们在测试服务器(9.138)上安装git-runner


首先我们下载安装git-runner


# 下载 
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
# 给予执行权限
sudo chmod +x /usr/local/bin/gitlab-runner
# 创建一个CI用户
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
# 安装并且运行服务
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

注册runner


image.png


gitLab查看runner


image.png


使用注册好的runner


编写.gitlab-ci.yml文件,先了解一些基本概念


1. Pipeline


一次 Pipeline 其实相当于一次构建任务,里面可以包含多个流程,如安装依赖、运行测试、编译、部署测试服务器、部署生产服务器等流程。 任何提交或者 Merge Request 的合并都可以触发 Pipeline,如下图所示:


image.png


2. Stages


Stages 表示构建阶段,说白了就是上面提到的流程。 我们可以在一次 Pipeline 中定义多个 Stages,这些 Stages 会有以下特点:



  • 所有 Stages 会按照顺序运行,即当一个 Stage 完成后,下一个 Stage 才会开始

  • 只有当所有 Stages 完成后,该构建任务 (Pipeline) 才会成功

  • 如果任何一个 Stage 失败,那么后面的 Stages 不会执行,该构建任务 (Pipeline) 失败


因此,Stages 和 Pipeline 的关系就是:


image.png


2. Jobs


Jobs 表示构建工作,表示某个 Stage 里面执行的工作。 我们可以在 Stages 里面定义多个 Jobs,这些 Jobs 会有以下特点:



  • 相同 Stage 中的 Jobs 会并行执行

  • 相同 Stage 中的 Jobs 都执行成功时,该 Stage 才会成功

  • 如果任何一个 Job 失败,那么该 Stage 失败,即该构建任务 (Pipeline) 失败


所以,Jobs 和 Stage 的关系图就是:


image.png


下面我们来写一个简单的.gitlab-ci.yml


image.png


注意


安装git-runner的服务器需要安装git,不然你可能看到一下错误:


image.png


后语


欢迎大家多提意见。项目模板在不断优化,一赞一回!欢迎评论。


链接:https://juejin.cn/post/7012496217003261959

收起阅读 »

让小脚本帮你做哪些枯燥无味的git提交吧

睡前小故事 在某次开会的时候,组长说每次做完一个小需求和修改一个bug都要提交一个commit,下班前要将本地分支推送到远程仓库。没commit的,没push的都扣当月的绩效...,虽然觉得这样很烦躁,但是谁让自己是打工崽呢,又不敢说什么,只能照着做了,还能...
继续阅读 »

睡前小故事



在某次开会的时候,组长说每次做完一个小需求和修改一个bug都要提交一个commit,下班前要将本地分支推送到远程仓库。没commit的,没push的都扣当月的绩效...,虽然觉得这样很烦躁,但是谁让自己是打工崽呢,又不敢说什么,只能照着做了,还能怎么办。之后就是每天不停的git status, git add . ,git commit -m 'xxxxx'...有时bug多的时候,一天输入几十次这样的命令,终于有一天我受不了,实在太没劲了(也许是自己懒吧),git,git,git的,代码还写不写啊。然后就想能不能输一个命令自动完成这几个步骤,接着就诞生了这个小脚本。



脚本的实现



脚本用shell写的



判断有没有安装git


if(!shell.which('git')) {
shell.echo('你还没安装git,请先安装git')
shell.exit(1)
}

判断有没有文件变动


if(shell.exec('git status').stdout.indexOf('working tree clean') !== -1) {
shell.echo('没有变动文件')
shell.exit(1)
}

查看哪些文件变动了


if(shell.exec('git status').code !== 0) {
shell.echo('git status执行出错')
shell.exit(1)
}

添加文件的追踪


if(shell.exec('git add .').code !== 0) {
shell.echo('git add执行出错')
shell.exit(1)
}

提交本地仓库


let intro = process.argv[2]

if(!intro) {
shell.echo('请填写提交信息,格式为feat(xxxx):xxxxx')
shell.exit(1)
}
if(shell.exec(`git commit -m ${intro}`).code !== 0) {
shell.echo('git commit执行出错')
shell.exit(1)
}

推送远程仓库


let BranchName = shell.exec('git rev-parse --abbrev-ref HEAD')
// 因为我7点下班,七点后commit的都会推送到远程
if(new Date().getHours() >= 19) {
if(shell.exec(`git push origin ${BranchName}`).code !== 0) {
shell.echo('推送远程失败')
shell.exit(1)
}
shell.echo('推送远程完成')
}

完整代码


let shell = require('shelljs')

if(!shell.which('git')) {
shell.echo('你还没安装git,请先安装git')
shell.exit(1)
}

shell.echo('查看哪些文件变动')

if(shell.exec('git status').stdout.indexOf('working tree clean') !== -1) {
shell.echo('没有变动文件')
shell.exit(1)
}

if(shell.exec('git status').code !== 0) {
shell.echo('git status执行出错')
shell.exit(1)
}

shell.echo('开始添加新文件追踪')

if(shell.exec('git add .').code !== 0) {
shell.echo('git add执行出错')
shell.exit(1)
}

let intro = process.argv[2]

if(!intro) {
shell.echo('请填写提交信息,格式为feat(xxxx):xxxxx')
shell.exit(1)
}

if(shell.exec(`git commit -m ${intro}`).code !== 0) {
shell.echo('git commit执行出错')
shell.exit(1)
}

let BranchName = shell.exec('git rev-parse --abbrev-ref HEAD')

shell.echo(`代码提交到本地完成,当前分支是${BranchName}`)

if(new Date().getHours() >= 19) {
if(shell.exec(`git push origin ${BranchName}`).code !== 0) {
shell.echo('推送远程失败')
shell.exit(1)
}
shell.echo('推送远程完成')
}

使用


 node git-commit.js(写上你脚本的文件) 'feat(xxx):xxxxx'(你要commit的描述)

效果


没变动


image.png


有变动


image.png


总结


程序猿懒就对。


作者:EasyMoment23
链接:https://juejin.cn/post/7012518849543487495

收起阅读 »

关于compute你不知道的骚操作

compute的的用法 首先你要知道computed的用法,在官网中只是简单的介绍了它的缓存作用,根据双向绑定的数据通过计算返回值,当被依赖的值发生改变的时候就会触发重新计算 很多人 就只知道这个只能用来计算值并不能作为其他的用法,其实根据它依赖双向绑定的数...
继续阅读 »

compute的的用法


首先你要知道computed的用法,在官网中只是简单的介绍了它的缓存作用,根据双向绑定的数据通过计算返回值,当被依赖的值发生改变的时候就会触发重新计算


image.png


很多人 就只知道这个只能用来计算值并不能作为其他的用法,其实根据它依赖双向绑定的数据可以监听多个数据,这句话怎么理解呢?就是我们都知道watch只能监听一个数据对象,但是有时候我们要监听多个对象的时候就要写很多个watch方法,而且会触发很多次,很多时候我们要监听的数据并不是1个,而且我们监听的是异步的,这个时候就可以用computed去解决。如果你看到这里还不明白什么意思,你可以看看下面的例子。


image.png


image.png


这里我监听了2个双向绑定的数据 一个是testData,一个是test3,然而test3 会在挂载之后的下一次更新发生改变,当这个改变发生的时候 会再次触发computed的方法,这个时候就代表我监听到了这个变化了,反过来想我是不是监听到了2个双向绑定的数据,那么结果就是 监听单个数据的变化 可以用watch,监听多个数据变化可以考虑computed,有人觉得我另一个数据没有改变,这也是只是监听了一个数据,我改了下代码。让另一个数据也发生改变。


image.png


image.png
可以看到这里触发了3次,为啥有3次 我解释下,computed本身是处于created 跟mounted的生命周期之间会自己去调用的,这是第一次,mounted里面的$nextTick方法里面test3改变的时候触发了一次,setTimeout里面testData改变的时候 就是第三次了。也就是说它监听的双向绑定数据改变的时候都会触发 ,这样子就可以达到监听多个数据的目的了。
适用的场景有 表单校验的时候,所有数据填完按钮点亮 这个时候就可以监听多个,还有就是多个异步返回的数据同时满足条件的时候可以监听。


有这么一个场景 ,一个列表是个数组会增加,高亮的地方(也就是下标会改变),只要一个改变就执行操作,你们会用什么去监听呢?


最后抛个问题


image.png


作者:Try to do
链接:https://juejin.cn/post/7012543084923715591

收起阅读 »

别再用generator模拟async啦,它还有很酷的用法

前一阵在某个技术群里发现有人在讨论JavaScript的generator,不少人一提generator就会把它跟异步联系在一起,我认为这是一个很深的误解。 generator 究竟跟异步是什么关系?以co为代表的一批早期框架用它来模拟async/await,...
继续阅读 »

前一阵在某个技术群里发现有人在讨论JavaScript的generator,不少人一提generator就会把它跟异步联系在一起,我认为这是一个很深的误解。


generator 究竟跟异步是什么关系?以co为代表的一批早期框架用它来模拟async/await,但是这是否能说明 generator 与异步相关呢?我认为答案是否定的,co中用到的语言特性很多,if、函数、变量……而 generator 只是其中之一罢了。


generator 的确是实现模拟async/await的一个关键语言特性,但是,正确的因果关系是 generator 和 async/await 共用了一个JS的底层设施:函数暂停执行并保留当时执行环境。


在我的观念中,generator 的应用前景远远比 async/await 更为广阔。generator 代表了一种"无穷序列"的抽象,或者说不定长序列的抽象,这个抽象可以为我们带来编程思路上的突破。


在非常前卫的函数式语言Haskell的官网首页,有这样一段代码:


primes = filterPrime [2..]
where filterPrime (p:xs) =
p : filterPrime [x | x <- xs, x `mod` p /= 0]

这是一段 Haskell 程序员引以为傲的代码,它的作用是生成一个质数序列,在多数语言中,都很难复刻这个逻辑结构。其中,最关键的一点就是它应用了延迟计算和无穷列表的概念。


我们试着分析这段 Haskell 代码的逻辑,[2..]表示一个从2开始到无穷的整数序列, filterPrime是一个函数,对这个整数序列做了过滤,函数具体内容则由后面的where指定。所以,能够把整数序列变成质数序列的关键代码就是 filterPrime。那么,它究竟做了什么呢?


这段代码简短得不可思议,首先我们来看参数部分,p:xs 是解构赋值的形参,它表示,把输入中的列表第一个元素赋值为p,剩余部分,赋值为xs。


第一次调用 filterPrime 实参为[2..],此时,p的值就是2,而xs则是无尽列表[3..]


那么,filterPrime 是如何将 p 和 xs 过滤成质数列表的呢?我们来看这段代码:


[x | x <- xs, x `mod\` p /= 0]`

这段大概的意思,可以用一段适合JS程序员理解的伪代码来解释:


xs.filter(x => x % p !== 0)

就是从列表 xs 中,过滤 p 的倍数。当然了,xs 并不是 JavaScript 原生数组,所以它并没有方便的filter方法。


那么,接下来,这个过滤好的数组传递给 filterPrime 递归就很有意思了,此时 xs 中已经被过滤掉了 p 的倍数,剩下的第一个数就必定是质数了,我们继续用 filterPrime 递归过滤其第一个元素的倍数,就可以继续找到下一个质数。


最后,代码p : 表示将 p 拼接到列表的第一个。


那么,在 JavaScript 中,是否能复刻这样的编程思想呢?


答案当然是可以,其关键正是 generator。


首先我们要解决的问题就是[2..],这是一个无尽列表,JavaScript中不支持无尽列表,但是我们可以用 generator 来表示,其代码如下:


function *integerRange(from, to){
for(let i = from; i < to; i++){
yield i;
}
}

接下来,数组的filter并不能够很好地作用于无尽列表,所以我们需要一个针对无尽列表的filter函数,其代码如下:



function *filter(iter, condition) {
for(let v of iter) {
if(condition(v)) {
yield v;
}
}
}

最后是我们的重头戏 filterPrime 啦,只要读懂了Haskell,这算不上困难,实现代码如下:


function* filterPrime(iter) {
let p = iter.next().value;
let rest = iter;

yield p;
for(let v of filterPrime(filter(iter, x => x % p != 0)))
yield v;
}

代码写好了,我们可以用JavaScript中独有的异步能力,来输出这个质数序列看看:


function sleep(d){
return new Promise(resolve => setTimeout(resolve, d));
}
void async function(){
for(let v of filterPrime(integerRange(2, Infinity))){
await sleep(1000);
console.log(v);
}
}();

好啦,虽然语法噪声稍微有点多,但是到此为止我们就实现了跟 Haskell 一样的质数序列算法。


除了无尽列表,generator 也很适合包装一些API,表达“不定项的列表”这样的概念。比如,我们可以对正则表达式的exec做一些包装,使它变成一个 generator。


function* execRegExp(regexp, string) {
let r = null;
while(r = regexp.exec(string)) {
yield r;
}
}

使用的时候,我们就可以用 for...of 结构了。下面代码展示了一个简单的词法分析写法。


let tokens = execRegExp(/let|var|\s+|[a-zA-Z$][0-9a-zA-Z$]*|[1-9][0-9]*|\+|-|\*|\/|;|=/g, "let a = 1 + 2;")
for(let s of tokens){
console.log(s);
}

这样的API设计,是不是比原来更简洁优美呢?


你看 generator 是一个潜力如此之大的语言特性,它为 JavaScripter 们打开了通往"无尽"数学概念的大门。所以,别再想着拿它模拟异步啦,希望看过本文,你能获得一点灵感,把 generator 用到开源项目或者生产中的 API 设计,谢谢观赏。


作者:winter
链接:https://juejin.cn/post/7012596693271052325

收起阅读 »

JavaScript 中有了Object 为什么还需要 Map 呢

众所周知,Map 是用于存储键值对的,而 JavaScript 中对象也是由键值对组成的,那么 Map 存在的意义是什么呢? 别把对象当 Map 1、可能通过原型链访问到未定义的属性 假设现有场景,开发一个网站,需要提供日语、汉语、韩语三种语言,我们可以定义一...
继续阅读 »

众所周知,Map 是用于存储键值对的,而 JavaScript 中对象也是由键值对组成的,那么 Map 存在的意义是什么呢?


别把对象当 Map


1、可能通过原型链访问到未定义的属性


假设现有场景,开发一个网站,需要提供日语、汉语、韩语三种语言,我们可以定义一个字典去管理。


const dictionary = {
'ja': {
'Ninjas for hire': '忍者を雇う',
},
'zh': {
'Ninjas for hire': '忍者出租',
},
'ko': {
'Ninjas for hire': '고용 닌자',
}
}

console.log(dictionary.ja['Ninjas for hire']) // 忍者を雇う
console.log(dictionary.zh['Ninjas for hire']) // 忍者出租
console.log(dictionary.ko['Ninjas for hire']) // 고용 닌자

这样我们就把不同语言的字典管理起来了。但是,当我们试图访问 constroctor 属性,问题就出现了。


console.log(dictionary.ko['constructor']) // ƒ Object() { [native code] }

对于不存在的属性,我们期望得到 undefined,结果却通过原型链访问到了未定义的属性,原型对象的 constructor 属性,指向构造函数。


此处有一个解决办法是把原型设置为 null


Object.setPrototypeOf(dictionary.ko, null)
console.log(dictionary.ko['constructor']) // undefined

2、对象的 Key 只能是字符串


假设需要将对象的 key 映射为 html 节点。我们写如下代码:


/* html部分
<div id="firstElement"></div>
<div id="secondElement"></div>
*/

const firstElement = document.getElementById('firstElement')
const secondElement = document.getElementById('secondElement')

const map = {}

map[firstElement] = {
data: 'firstElement'
}
map[secondElement] = {
data: 'secondElement'
}

console.log(map[firstElement].data) // secondElement
console.log(map[secondElement].data) // secondElement


第一个元素的数据被覆盖了,原因是对象中的 key 只能是字符串类型,当我们没有使用字符串类型时,它会隐式调用 toString() 函数进行转换。于是两个 html 元素都被转为字符串 [object HTMLDivElement]


使用 Map


1、Map 常用操作


Map 可以使用任何 JavaScript 数据类型作为键


function People(name) {
this.name = name
}
const zhangsan = new People('zhangsan')
const xiaoming = new People('xiaoming')
const lihua = new People('lihua')
// 创建 Map
const map = new Map()
// 创建 Map 并进行初始化
const map1 = new Map([
['key1', 'val1'],
['key2', 'val2'],
])
// 设置键值映射关系
map.set(zhangsan, {
region: 'HB'
})
map.set(xiaoming, {
region: 'HN'
})
// 根据 key 获取对应值
console.log(map.get(zhangsan)) // { region: 'HB' }
console.log(map.get(xiaoming)) // { region: 'HN' }
// 获取不存在的 key 得到 undefined
console.log(map.get(lihua)) // undefined
// 通过 has 函数判断指定 key 是否存在
console.log(map.has(lihua)) // false
console.log(map.has(xiaoming)) // true
// map存储映射个数
console.log(map.size) // 2
// delete 删除 key
map.delete(xiaoming)
console.log(map.has(xiaoming)) // false
console.log(map.size) // 1
// clear 清空 map
map.clear()
console.log(map.size) // 0

2、遍历 Map


Map 可以确保遍历的顺序和插入的顺序一致


const zhangsan = { name: 'zhangsan' }
const xiaoming = { name: 'xiaoming' }
const map = new Map()
map.set(zhangsan, { region: 'HB' })
map.set(xiaoming, { region: 'HN' })

for (let item of map) { // = for (let item of map.entries()) {
console.log(item)
}
// 每个键值对返回的是 [key, value] 的数组
// [ { name: 'zhangsan' }, { region: 'HB' } ]
// [ { name: 'xiaoming' }, { region: 'HN' } ]
for (let key of map.keys()) {
console.log(key)
}
// 遍历 key
// { name: 'zhangsan' }
// { name: 'xiaoming' }
for (let key of map.values()) {
console.log(key)
}
// 遍历 value
// { region: 'HB' }
// { region: 'HN' }

3、Map 中判断 key 相等


Map 内部使用 SameValueZero 比较操作。


关于SameValue 和 SameValueZero


SameValue (Object.is()) 和严格相等(===)相比,对于 NaN+0-0 的处理不同


Object.is(NaN, NaN) // true
Object.is(0, -0) // false

SameValueZero 与 SameValue 的区别主要在于 0-0 是否相等。


map.set(NaN, 0)
map.set(0, 0)
console.log(map.has(NaN)) // true
console.log(map.has(-0)) // true

4、Map 序列化


感谢 @一条鱼的心事 的提醒。


Map 无法被序列化,如果试图用 JSON.stringify 获得 MapJSON 的话,只会得到 "{}"


由于 Map 的键可以是任意数据类型,而 JSON 仅允许将字符串作为键,所以一般情况下无法将 Map 转为 JSON


不过可以通过下面的方式去尝试序列化一个 Map


// 初始化 Map(1) {"key1" => "val1"}
const originMap = new Map([['key1', 'val1']])
// 序列化 "[[\"key1\",\"val1\"]]"
const mapStr = JSON.stringify(Array.from(originMap.entries()))
// 反序列化 Map(1) {"key1" => "val1"}
const cloneMap = new Map(JSON.parse(mapStr))

Map 和 Object 的性能差异



  1. 内存占用


不同浏览器的情况不同,但给定固定大小的内存,Map 大约可以比 Object 多存储 50% 的键/值对。



  1. 插入性能


Map 略快,如果涉及大量操作,建议使用 Map



  1. 查找速度


性能差异极小,但如果只包含少量键/值对,则 Object 有时候速度更快。Object 作为数组使用时浏览器会进行优化。如果涉及大量查找操作,选择 Object 会更好一些。



  1. 删除性能


如果代码涉及大量的删除操作,建议选择 Map



作者:我不吃饼干呀
链接:https://juejin.cn/post/7012036506994868255

收起阅读 »

CSS实现瀑布流的两种方式

瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。在手机端进行多图片展示时会经常用到。即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。 那么瀑布流式布局有哪些实现方式呢? c...
继续阅读 »

瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。在手机端进行多图片展示时会经常用到。即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。


那么瀑布流式布局有哪些实现方式呢?


column 多行布局实现瀑布流



column 实现瀑布流主要依赖两个属性。


column-count 属性,是控制屏幕分为多少列。


column-gap 属性,是控制列与列之间的距离。



<!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>瀑布流布局-column</title>
<style>
.box {
margin: 10px;
column-count: 3;
column-gap: 10px;
}
.item {
margin-bottom: 10px;
}
.item img{
width: 100%;
height:100%;
}
</style>
</head>
<body>
<div class="box">
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
</div>
</body>
</html>

展示效果如下


column.png


flex 弹性布局实现瀑布流



flex 实现瀑布流需要将最外层元素设置为 display: flex,使用弹性布局


flex-flow:column wrap 使其纵向排列并且换行换行


设置 height: 100vh 填充屏幕的高度,也可以设置为单位为 px 的高度,来容纳子元素。


每一列的宽度可用 calc 函数来设置,即 width: calc(100%/3 - 20px)。分成等宽的 3 列减掉左右两遍的 margin 距离。



<!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>瀑布流布局-flex</title>
<style>
.box {
display: flex;
flex-flow: column wrap;
height: 100vh;
}
.item {
margin: 10px;
width: calc(100%/3 - 20px);
}
.item img{
width: 100%;
height:100%;
}
</style>
</head>
<body>
<div class="box">
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
<div class="item">
<img src="./imgs/2.jpg" alt="2" />
</div>
<div class="item">
<img src="./imgs/3.jpg" alt="3" />
</div>
<div class="item">
<img src="./imgs/1.jpg" alt="1" />
</div>
</div>
</body>
</html>

展示效果如下


flex.png



链接:https://juejin.cn/post/7011333433318178846

收起阅读 »

学透CSS-:focus-within 仿掘金登录小人动画

兼容性 作为:focus的好兄弟,在兼容性上也还是不错的。主流的浏览器基本都已经支持这个属性。 :focus-within 和 :focus 的区 :focus-within 表示一个元素自身获取焦点,以及子元素获取焦点后的效果。 :focus 表...
继续阅读 »

兼容性


作为:focus的好兄弟,在兼容性上也还是不错的。主流的浏览器基本都已经支持这个属性。
image.png


:focus-within 和 :focus 的区


:focus-within 表示一个元素自身获取焦点,以及子元素获取焦点后的效果。


:focus 表示元素自身获取到焦点后的效果。


示例


定义一个form表单,背景颜色是green。


form{          
padding: 50px;
background-color:green ;
}
<form action="">
<input type="text">
</form>

image.png


定义获取焦点后的效果


form:focus-within{
background-color: aqua;
}
input:focus{
background-color: red;
}

当input标签获取到焦点后,背景颜色变成了red,同时form的背景颜色变成aqea
image.png


应用场景- form表单输入(掘金登录页面)


掘金在登录输入密码的时候,这个小人会挡住自己的眼睛,有很多作者用各种方法实现这个效果,:focu-within有同样可以实现这个效果。
image.png


首先实现登陆前的画面(比较丑)


<div class="login">
<form action="">
<div class="panfish"></div>
<div><label for=""> 账号</label> <input type="text" /></div>
<div><label for=""> 密码</label> <input type="text" /></div>
</form>
</div>

.login {
position: relative;
padding: 2rem;
width: 20rem;
font-size: 1.167rem;
background-color: #fff;
border-radius: 2px;
box-sizing: border-box;
}
.panfish {
background: url(https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/ad7fa76844a2df5c03151ead0ce65ea6.svg);
z-index: 1;
padding-top: 50px;
width: 20rem;
height:50px;
position: absolute;
background-repeat: no-repeat;

top: -60px;
}
input:focus {
background-color: red;
}

image.png


使用:fous-within


form:focus-within > .panfish {
background: url(https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/4f6f6f316cde4398d201cd67e44ddea3.svg);
background-repeat: no-repeat;

}

获取焦点后的效果


image.png


GIF


focuswithin.gif


作者:前端picker
链接:https://juejin.cn/post/7012171045155110942

收起阅读 »

给女友写的,每日自动推送暖心消息

起因是因为刷到一则给女友发的每日提醒消息的沸点,每天自动定时发送消息,感觉很有趣,刚好最近在学习egg,里面有用到定时任务,于是决定尝试一把 egg 实现 环境准备 操作系统:支持 macOS,Linux,Windows 运行环境:建议选择 node LTS ...
继续阅读 »

起因是因为刷到一则给女友发的每日提醒消息的沸点,每天自动定时发送消息,感觉很有趣,刚好最近在学习egg,里面有用到定时任务,于是决定尝试一把 egg 实现


环境准备


操作系统:支持 macOS,Linux,Windows


运行环境:建议选择 node LTS 版本,最低要求 8.x。


创建egg项目和目录结构介绍


快速入门


目录结构


运行


本地开发


$ npm i
$ npm run dev
$ open http://localhost:7001/

部署生产


$ npm start
$ npm stop

控制器


class HomeController extends Controller {
async send() {
const { ctx, app } = this;
ctx.body = app.config;
const result = await ctx.service.sendmsg.sendOut();
ctx.logger.info('主动触发,发送模板消息 结果: %j', result);
ctx.body = result;
ctx.set('Content-Type', 'application/json');
}
}

service服务层


 // 时间处理
const moment = require('moment');
class sendmsg extends Service {
// 发送模板消息给媳妇儿
async sendOut() {
const { ctx, app } = this;
const token = await this.getToken();
const data = await this.getTemplateData();
ctx.logger.info('获取token 结果: %j', token);
// 模板消息接口文档
const users = app.config.weChat.users;
const promise = users.map(id => {
ctx.logger.info('--------------开始发送每日提醒-----------------------------------------------: %j', id);
data.touser = id;
return this.toWechart(token, data);
});
const results = await Promise.all(promise);
ctx.logger.info('--------------结束发送每日提醒->结果-----------------------------------------------: %j', results);
return results;
}
// 通知微信接口
async toWechart(token, data) {
...
}
// 获取token
async getToken() {
...
}
// 组装模板消息数据
async getTemplateData() {
...
}
// 获取天气
async getWeather(city = '深泽') {
...
}
// 获取 下次发工资 还有多少天
getWageDay() {
...
}
// 获取距离 下次结婚纪念日还有多少天
getMarryDay() {
...
}
// 获取 距离 下次生日还有多少天
getbirthday() {
...
}
// 获取 相恋天数
getLoveDay() {
...
}
// 获取 相恋几年了
getLoveYear() {
...
}
// 获取是第几个生日
getBirthYear() {
...
}
// 获取是第几个结婚纪念日
getMarryYear() {
...
}
// 获取 每日一句
async getOneSentence() {
...
}
// 获取时间日期
getDatetime() {
...
}
}

发送模板消息


  async toWechart(token, data) {
// 模板消息接口文档
const url = 'https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=' + token;
const result = await this.ctx.curl(url, {
method: 'POST',
data,
dataType: 'json',
headers: {
'Content-Type': 'application/json',
},
});
return result;
}

获取Access token


  async getToken() {
const { app } = this;
const url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + app.config.weChat.appld + '&secret=' + app.config.weChat.secret;
const result = await this.ctx.curl(url, {
method: 'get',
dataType: 'json',
});
if (result.status === 200) {
return result.data.access_token;
}
}

组装模板消息数据


  async getTemplateData() {
const { app } = this;
// 判断所需 模板
// 发工资模板 getWageDay == 0 wageDay
// 结婚纪念日模板 getMarryDay == 0 marry
// 生日 模板 getbirthday == 0 birthday
// 正常模板 daily

const wageDay = this.getWageDay();
const marry = this.getMarryDay();
const birthday = this.getbirthday();
const data = {
topcolor: '#FF0000',
data: {},
};
// 发工资模板
if (!wageDay) {
data.template_id = app.config.weChat.wageDay;
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
};
} else if (!marry) {
// 结婚纪念日模板
data.template_id = app.config.weChat.marry;
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
anniversary: {
value: this.getMarryYear(),
color: '#ff3399',
},
year: {
value: this.getLoveYear(),
color: '#ff3399',
},
};
} else if (!birthday) {
// 生日模板
data.template_id = app.config.weChat.birthday;
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
individual: {
value: this.getBirthYear(),
color: '#ff3399',
},
};
} else {
// 正常模板
data.template_id = app.config.weChat.daily;
// 获取天气
const getWeather = await this.getWeather();
// 获取每日一句
const message = await this.getOneSentence();
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
love: {
value: this.getLoveDay(),
color: '#ff3399',
},
wage: {
value: wageDay,
color: '#66ff00',
},
birthday: {
value: birthday,
color: '#ff0033',
},
marry: {
value: marry,
color: '#ff0033',
},
wea: {
value: getWeather.wea,
color: '#33ff33',
},
tem: {
value: getWeather.tem,
color: '#0066ff',
},
airLevel: {
value: getWeather.air_level,
color: '#ff0033',
},
tem1: {
value: getWeather.tem1,
color: '#ff0000',
},
tem2: {
value: getWeather.tem2,
color: '#33ff33',
},
win: {
value: getWeather.win,
color: '#3399ff',
},
message: {
value: message,
color: '#8C8C8C',
},
};
}
return data;
}

获取天气


  async getWeather(city = '石家庄') {
const { app } = this;
const url = 'https://www.tianqiapi.com/api?unescape=1&version=v6&appid=' + app.config.weather.appid + '&appsecret=' + app.config.weather.appsecret + '&city=' + city;
const result = await this.ctx.curl(url, {
method: 'get',
dataType: 'json',
});
console.log(result.status);
// "wea": "多云",
// "tem": "27", 实时温度
// "tem1": "27", 高温
// "tem2": "17", 低温
// "win": "西风",
// "air_level": "优",
if (result && result.status === 200) {
return result.data;
}
return {
city,
wea: '未知',
tem: '未知',
tem1: '未知',
tem2: '未知',
win: '未知',
win_speed: '未知',
air_level: '未知',
};
}

获取 下次发工资 还有多少天


  getWageDay() {
const { app } = this;
const wage = app.config.time.wageDay;
// 获取日期 day
// 如果在 wage号之前或等于wage时 那么就用 wage-day
// 如果在 wage号之后 那么就用 wage +(当前月总天数-day)
// 当日 日期day
const day = moment().date();
// 当月总天数
const nowDayTotal = moment().daysInMonth();
// // 下个月总天数
// const nextDayTotal = moment().month(moment().month() + 1).daysInMonth();
let resultDay = 0;
if (day <= wage) {
resultDay = wage - day;
} else {
resultDay = wage + (nowDayTotal - day);
}
return resultDay;
}

获取距离 下次结婚纪念日还有多少天


  getMarryDay() {
const { app } = this;
const marry = app.config.time.marry;
// 获取当前时间戳
const now = moment(moment().format('YYYY-MM-DD')).valueOf();
// 获取纪念日 月-日
const mmdd = moment(marry).format('-MM-DD');
// 获取当年
const y = moment().year();
// 获取今年结婚纪念日时间戳
const nowTimeNumber = moment(y + mmdd).valueOf();
// 判断 今天的结婚纪念日 有没有过,如果已经过去(now>nowTimeNumber),resultMarry日期为明年的结婚纪念日
// 如果还没到,则 结束日期为今年的结婚纪念日
let resultMarry = nowTimeNumber;
if (now > nowTimeNumber) {
// 获取明年纪念日
resultMarry = moment((y + 1) + mmdd).valueOf();
}
return moment(moment(resultMarry).format()).diff(moment(now).format(), 'day');
}

获取 距离 下次生日还有多少天



getbirthday() {
const { app } = this;
const birthday = app.config.time.birthday[moment().year()];
// 获取当前时间戳
const now = moment(moment().format('YYYY-MM-DD')).valueOf();
// 获取纪念日 月-日
const mmdd = moment(birthday).format('-MM-DD');
// 获取当年
const y = moment().year();
// 获取今年生日 时间戳
const nowTimeNumber = moment(y + mmdd).valueOf();
// 判断 生日 有没有过,如果已经过去(now>nowTimeNumber),resultBirthday日期为明年的生日 日期
// 如果还没到,则 结束日期为今年的目标日期
let resultBirthday = nowTimeNumber;
if (now > nowTimeNumber) {
// 获取明年目标日期
resultBirthday = moment(app.config.time.birthday[y + 1]).valueOf();
}
return moment(moment(resultBirthday).format()).diff(moment(now).format(), 'day');
}

获取 相恋天数


  getLoveDay() {
const { app } = this;
const loveDay = app.config.time.love;
return moment(moment().format('YYYY-MM-DD')).diff(loveDay, 'day');
}

获取 相恋几年了


  getLoveYear() {
const { app } = this;
const loveDay = app.config.time.love;
return moment().year() - moment(loveDay).year();
}

获取是第几个生日


  getBirthYear() {
const { app } = this;
const birthYear = app.config.time.birthYear;
return moment().year() - birthYear;
}

获取是第几个结婚纪念日


  getMarryYear() {
const { app } = this;
const marry = app.config.time.marry;
return moment().year() - moment(marry).year();
}

获取 每日一句


  async getOneSentence() {
const url = 'https://v1.hitokoto.cn/';
const result = await this.ctx.curl(url, {
method: 'get',
dataType: 'json',
});
if (result && result.status === 200) {
return result.data.hitokoto;
}
return '今日只有我爱你!';
}

获取时间日期


  getDatetime() {
console.log('moment().weekday()', moment().weekday());
const week = {
1: '星期一',
2: '星期二',
3: '星期三',
4: '星期四',
5: '星期五',
6: '星期六',
0: '星期日',
};
return moment().format('YYYY年MM月DD日 ') + week[moment().weekday()];
}


定时任务和主动触发


定时任务


设置规则 请参考文档


└── app
└── schedule
└── update_cache.js

class UpdateCache extends Subscription {
// 通过 schedule 属性来设置定时任务的执行间隔等配置
static get schedule() {
return {
cron: '0 30 7 * * *', // 每天的7点30分0秒执行
// interval: '1m', // 1 分钟间隔
type: 'all', // 指定所有的 worker 都需要执行
};
}

// subscribe 是真正定时任务执行时被运行的函数
async subscribe() {
const { ctx } = this;
const result = await ctx.service.sendmsg.send();
ctx.logger.info('定时任务执行消息提醒 结果: %j', result);
}
}

日志中 可以查看 定时任务的 执行记录


└── logs
└── serves
└── serves-web

主动发送


请求或浏览器访问 http://localhost:7001/send


image


配置文件说明


└── logs
└── config.default.js

天气秘钥


注册地址


// 天气接口配置
config.weather = {
appid: '*******',
appsecret: '*******',
};

特殊 时间点设置


下方是 birthday生日,因老家都是过阴历生日,不好处理,暂时写死的


// 时间
config.time = {
wageDay: 15, // 工资日
love: '2017-06-09', // 相爱日期
marry: '2021-11-27', // 结婚纪念日
birthday: {
2021: '2021-04-17',
2022: '2022-04-06',
2023: '2023-04-25',
2024: '2024-04-14',
2025: '2025-04-03',
2026: '2026-04-22',
}, // 每年生日 阳历
birthYear: '1995-03-06',
};

微信公众号 配置


因个人只能申请订阅号,而订阅号不支持发送模板消息,所以在此使用的测试的微信公众号,有微信号都可以申请,免注册,扫码登录


无需公众帐号、快速申请接口测试号


直接体验和测试公众平台所有高级接口


申请地址


// 测试 微信公众号
config.weChat = {
appld: '**********',
secret: '**********',
// 用户的openid
users: [
'**********************',
'**********************',
'**********************',
'**********************'
],
daily: '************', // 普通模板
marry: ''************',', // 结婚纪念日模板
wageDay: ''************',', // 工资日模板
birthday: ''************',', // 生日模板
};

微信消息模板


这个需要在 上文提到的 微信公众平台测试账号 单独设置


以下是 我用的模板


正常模板


{{dateTime.DATA}}
今天是 我们相恋的第{{love.DATA}}天
距离上交工资还有{{wage.DATA}}天
距离你的生日还有{{birthday.DATA}}天
距离我们结婚纪念日还有{{marry.DATA}}天
今日天气 {{wea.DATA}}
当前温度 {{tem.DATA}}度
最高温度 {{tem1.DATA}}度
最低温度 {{tem2.DATA}}度
空气质量 {{airLevel.DATA}}
风向 {{win.DATA}}
每日一句
{{message.DATA}}

发工资模板


{{dateTime.DATA}}
老婆大人,今天要发工资了,预计晚九点前会准时上交,记得查收!

生日 模板


{{dateTime.DATA}}
听说今天是你人生当中第 {{individual.DATA}} 个生日?天呐,
我差点忘记!因为岁月没有在你脸上留下任何痕迹。
尽管,日历告诉我:你又涨了一岁,但你还是那个天真可爱的小妖女,生日快乐!

结婚纪念日


{{dateTime.DATA}}
今天是结婚{{anniversary.DATA}}周年纪念日,在一起{{year.DATA}}年了,
经历了风风雨雨,最终依然走在一起,很幸运,很幸福!我们的小家庭要一直幸福下去。

展示效果


d086745c18c811ef17488c004f64cb0.jpg


81c079890d8acd86cf02aeb22c1ff4b.jpg


0fb7faaa548c212ae6c31bf5d9ce816.jpg


作者:iwhao
链接:https://juejin.cn/post/7012171027790692388

收起阅读 »

如何小程序上绘制树状图

前言 现有的移动端图可视化技术有Antv旗下的F2、F6。F2主要专注于数据分析的统计图,而F6专注与各种场景的关系图。两者各有侧重。F6 是一个简单、易用、完备的移动端图可视化引擎,它在高定制能力的基础上,提供了一系列设计优雅、便于使用的图可视化解决方案。能...
继续阅读 »

前言


现有的移动端图可视化技术有Antv旗下的F2、F6。F2主要专注于数据分析的统计图,而F6专注与各种场景的关系图。两者各有侧重。F6 是一个简单、易用、完备的移动端图可视化引擎,它在高定制能力的基础上,提供了一系列设计优雅、便于使用的图可视化解决方案。能帮助开发者搭建属于自己的图可视化、图分析、或图编辑器应用。如果您希望将内容通过流程图、知识图谱、思维导图等形式进行输出,并希望可以方便的实现对图的操控,那么建议您一定要尝试一下F6。



欢迎star和提交issue


github.com/antvis/F6



什么是树图?


树图,表现形式如下,


具体原理可以参考 emr.cs.iit.edu/~reingold/t… ,由根节点不断的派生,形成一个树状结构,是一种可以很好表达层级关系的可视化方法。
举个例子:
Kapture 2021-09-22 at 23.40.34.gif


什么场景下使用































































名称应用
分解树在人口调查中,将人口样本分解为人口统计信息
关键质量特性树将顾客的需求转化为产品的可测量参数和过程特性
决策树或逻辑图绘制出思维过程以便于决策
树干图在产品的设计和开发阶段,用于识别产品的特性
故障树分析识别故障的潜在原因
装配图在制造过程中,描绘产品零部件的装配
方法-方法图解决问题
工作或任务分析识别一项工作或任务的要求
组织图识别管理和汇报间的关联水平
过程决策程序图确定潜在的问题和复杂计划中的对策
需求测量树确定顾客、需求以及对测量产品或服务的测量
原因一原因图或five whys识别问题的根本原因
生产分类结构(WBS)识别项目的所有方面,分解成具体工作包水平

可见树图在实际场景中有很多应用,不论是在日常生活中,还是在生产中都有多种用途。我们最熟悉的脑图(mind map)也是树图的一种形式,


F6中如何绘制


演示示例可以参考f6.antv.vision/zh/docs/exa…
本节代码已经开源,感兴趣可以查看



支付宝中


首先安装


npm install @antv/f6 @antv/f6-alipay -S


index.json


{
"defaultTitle": "紧凑树",
"usingComponents": {
"f6-canvas": "@antv/f6-alipay/es/container/container"
}
}


index.js


import F6 from '@antv/f6';
import TreeGraph from '@antv/f6/dist/extends/graph/treeGraph';

import data from './data.js';

/**
* 紧凑树
*/

Page({
canvas: null,
ctx: null, // 延迟获取的2d context
renderer: '', // mini、mini-native等,F6需要,标记环境
isCanvasInit: false, // canvas是否准备好了
graph: null,

data: {
width: 375,
height: 600,
pixelRatio: 2,
forceMini: false,
},

onLoad() {
// 注册自定义树,节点等
F6.registerGraph('TreeGraph', TreeGraph);

// 同步获取window的宽高
const { windowWidth, windowHeight, pixelRatio } = my.getSystemInfoSync();

this.setData({
width: windowWidth,
height: windowHeight,
pixelRatio,
});
},

/**
* 初始化canvas回调,缓存获得的context
* @param {*} ctx 绘图context
* @param {*} rect 宽高信息
* @param {*} canvas canvas对象,在render为mini时为null
* @param {*} renderer 使用canvas 1.0还是canvas 2.0,mini | mini-native
*/
handleInit(ctx, rect, canvas, renderer) {
this.isCanvasInit = true;
this.ctx = ctx;
this.renderer = renderer;
this.canvas = canvas;
this.updateChart();
},

/**
* canvas派发的事件,转派给graph实例
*/
handleTouch(e) {
this.graph && this.graph.emitEvent(e);
},

updateChart() {
const { width, height, pixelRatio } = this.data;

// 创建F6实例
this.graph = new F6.TreeGraph({
context: this.ctx,
renderer: this.renderer,
width,
height,
pixelRatio,
fitView: true,
modes: {
default: [
{
type: 'collapse-expand',
onChange: function onChange(item, collapsed) {
const model = item.getModel();
model.collapsed = collapsed;
return true;
},
},
'drag-canvas',
'zoom-canvas',
],
},
defaultNode: {
size: 26,
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
},
defaultEdge: {
type: 'cubic-horizontal',
},
layout: {
type: 'compactBox',
direction: 'LR',
getId: function getId(d) {
return d.id;
},
getHeight: function getHeight() {
return 16;
},
getWidth: function getWidth() {
return 16;
},
getVGap: function getVGap() {
return 10;
},
getHGap: function getHGap() {
return 100;
},
},
});

this.graph.node(function(node) {
return {
label: node.id,
labelCfg: {
offset: 10,
position: node.children && node.children.length > 0 ? 'left' : 'right',
},
};
});

this.graph.data(data);
this.graph.render();
this.graph.fitView();
},
});


index.axml


<f6-canvas
width="{{width}}"
height="{{height}}"
forceMini="{{forceMini}}"
pixelRatio="{{pixelRatio}}"
onTouchEvent="handleTouch"
onInit="handleInit"
></f6-canvas>

微信中


首先安装


npm install @antv/f6-wx -S

@antv/f6-wx 由于微信对npm包不是很友好,所以我们封装了 @antv/f6-wx 帮助用户简化操作。


index.json


{
"defaultTitle": "紧凑树",
"usingComponents": {
"f6-canvas": "@antv/f6-wx/canvas/canvas"
}
}


index.wxml


<f6-canvas
width="{{width}}"
height="{{height}}"
forceMini="{{forceMini}}"
pixelRatio="{{pixelRatio}}"
bind:onTouchEvent="handleTouch"
bind:onInit="handleInit"
></f6-canvas>


index.js


import F6 from '@antv/f6-wx';
import TreeGraph from '@antv/f6-wx/extends/graph/treeGraph';

import data from './data.js';
Page({
canvas: null,
ctx: null, // 延迟获取的2d context
renderer: '', // mini、mini-native等,F6需要,标记环境
isCanvasInit: false, // canvas是否准备好了
graph: null,

data: {
width: 375,
height: 600,
pixelRatio: 2,
forceMini: false,
},

onLoad() {
// 注册自定义树,节点等
F6.registerGraph('TreeGraph', TreeGraph);
// 同步获取window的宽高
const { windowWidth, windowHeight, pixelRatio } = wx.getSystemInfoSync();

this.setData({
width: windowWidth * pixelRatio,
height: windowHeight * pixelRatio,
pixelRatio,
});
},

/**
* 初始化canvas回调,缓存获得的context
* @param {*} ctx 绘图context
* @param {*} rect 宽高信息
* @param {*} canvas canvas对象,在render为mini时为null
* @param {*} renderer 使用canvas 1.0还是canvas 2.0,mini | mini-native
*/
handleInit(event) {
const { ctx, rect, canvas, renderer } = event.detail;
this.isCanvasInit = true;
this.ctx = ctx;
this.renderer = renderer;
this.canvas = canvas;
this.updateChart();
},

/**
* canvas派发的事件,转派给graph实例
*/
handleTouch(e) {
this.graph && this.graph.emitEvent(e.detail);
},

updateChart() {
const { width, height, pixelRatio } = this.data;

// 创建F6实例
this.graph = new F6.TreeGraph({
context: this.ctx,
renderer: this.renderer,
width,
height,
pixelRatio,
fitView: true,
modes: {
default: [
{
type: 'collapse-expand', // 点击后展开/收缩
onChange: function onChange(item, collapsed) {
const model = item.getModel();
model.collapsed = collapsed;
return true;
},
},
'drag-canvas',
'zoom-canvas',
],
},
defaultNode: {
size: 26,
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
},
defaultEdge: {
type: 'cubic-horizontal',
},
layout: {
type: 'compactBox',
direction: 'LR',
getId: function getId(d) {
return d.id;
},
getHeight: function getHeight() {
return 16;
},
getWidth: function getWidth() {
return 16;
},
getVGap: function getVGap() {
return 10;
},
getHGap: function getHGap() {
return 100;
},
},
});

this.graph.node(function(node) {
return {
label: node.id,
labelCfg: {
offset: 10,
position: node.children && node.children.length > 0 ? 'left' : 'right',
},
};
});

this.graph.data(data);
this.graph.render();
this.graph.fitView();
},
});



作者:AntCredit
链接:https://juejin.cn/post/7011374414394556452

收起阅读 »

[JS基础回顾] 闭包 又双叒叕来~~~

闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包 MDN的解释闭包是函数和声明该函数的词法环境的组合。 Tips: 词法作用域和词法环境 1,此时函数还没被执行,所以使用的是词法作用域即静态作用域.2, 此时函...
继续阅读 »

闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包




MDN的解释闭包是函数声明该函数的词法环境的组合。




Tips: 词法作用域词法环境 1,此时函数还没被执行,所以使用的是词法作用域即静态作用域.2, 此时函数被执行,此时词法作用域就会变成词法环境(包含静态作用域与动态作用域)



以上的解释 个人感觉还是不够清晰
我这样理解

  1. 闭包就是突破了函数作用域
  2. 闭包就是函数嵌套函数子函数可以访问父函数的变量(也就是所谓的自由变量), 所以,此变量不会被回收.


闭包暴露``函数作用域3种方式:


1) 通过外部函数的参数进行暴露



闭包内 调用外部函数 通过外部函数的参数 暴露 闭包内 自由变量.



function fn() { 
var a = 2;
function innerFn() {
outerFn(a) //通过外部函数的参数进行暴露
}
innerFn();
};
function outerFn(val) {
console.log(val); // 2
}
fn(); // 2

2) 通过外部作用域的变量进行暴露



其中val为全局变量



function fn() { 
var a = 1;
function innerFn() {
val = a; //通过外部作用域的变量进行暴露
}
innerFn();
};

fn();
console.log(val); // 1


3) 通过return直接将整个函数进行暴露


function fn() { 
var a = 1;
function innerFn() {
console.log(a);
}
return innerFn; //通过return直接将整个函数进行暴露
};

let a = fn();
a(); // 1

关于闭包的内存泄露



首先必须声明一点:使用闭包并不一定会造成内存泄露,只有使用闭包不当才可能会造成内存泄露.




为什么闭包可能会造成内存泄露呢?原因就是上面提到的,因为它一般会暴露自身的作用域给外部使用.如果使用不当,就可能导致该内存一直被占用,无法被JS的垃圾回收机制回收.就造成了内存泄露.




注意: 即使闭包里面什么都没有,闭包仍然会隐式地引用它所在作用域里的所用变量. 正因为这个隐藏的特点,闭包经常会发生不易发现的内存泄漏问题.



常见哪些情况使用闭包会造成内存泄露:





    1. 使用定时器未及时清除.因为计时器只有先停止才会被回收.所以决办法很简单,将定时器及时清除,并将造成内存的变量赋值为null(变成空指针)





    1. 相互循环引用.这是经常容易犯的错误,并且也不容易发现.





    1. 闭包引用到全局变量上.因为全局变量是只有当页面被关闭的时候才会被回收.




四 循环和闭包


1) 同步循环打印 正确的值


for (var i=1; i<5; i++) { 
console.log( i );
}
// 1 2 3 4

2) 同步中嵌套异步任务(中的宏任务)循环打印 错误的值



当执行 console 时, 循环已经完成, 同步任务执行完成后,执行宏任务,此时 i 已经是 5.所以打印5个5.



for (var i=1; i<5; i++) { 
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
// 打印出 5 个 5

3) 创造5个独立的函数作用域,但是 i 也全都是对外部作用域的引用 错误的值



它的最终值仍然是5个5.为什么?我们来分析下,它用了一个匿名函数包裹了定时器,并立即执行.在进行for循环时,会创造5个独立的函数作用域(由匿名函数创建的,因为它是闭包函数).但是这5个独立的函数作用域里的i也全都是对外部作用域的引用.即它们访问的都是i的最终值5.这并不是我们想要的,我们要的是5个独立的作用域,并且每个作用域都保存一个"当时"i的值.



for (var i=1; i<5; i++) { 
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}
// 打印出 5 个 5

4) 通过匿名函数创建独立的函数作用域,并且通过 变量 保存独立的 i 值


for (var i=1; i<5; i++) { 
(function () {
var x=i;
console.log(x*1000); // 1000 2000 3000 4000
setTimeout( function timer() {
console.log( x );
}, x*1000 );
})();
}

// 1 2 3 4

5) 通过匿名函数创建独立的函数作用域,并且通过 参数 保存独立的 i 值


for (var i=1; i<5; i++) { 
(function (x) {
console.log(x*1000); // 1000 2000 3000 4000
setTimeout( function timer() {
console.log( x );
}, x*1000 );
})(i);
}

// 1 2 3 4

注意

  • 使用定时器未及时清除.因为计时器只有先停止才会被回收.所以决办法很简单,将定时器及时清除,并将造成内存的变量赋值为null(变成空指针)
  • 闭包引用到全局变量上.因为全局变量是只有当页面被关闭的时候才会被回收.
  • 闭包就是函数嵌套函数子函数可以访问父函数的变量(也就是所谓的自由变量), 所以,此变量不会被回收.


  • 作者:无限循环无限
    链接:https://juejin.cn/post/7011805931201642533

    收起阅读 »

    JS箭头函数 什么时候用 ,什么时候不能用,我总结出了4点

    箭头函数的定义 箭头函数定义包括一个参数列表(零个或多个参数,如果参数个数不是一个的话要用 ( .. ) 包围起来),然后是标识 =>,函数体放在最后。 箭头函数与普通函数的区别 箭头函数 let arrowSum = (a, b) => { ...
    继续阅读 »

    箭头函数的定义



    箭头函数定义包括一个参数列表(零个或多个参数,如果参数个数不是一个的话要用 ( .. ) 包围起来),然后是标识 =>,函数体放在最后。



    箭头函数与普通函数的区别


    箭头函数


    let arrowSum = (a, b) => { 
    return a + b
    }

    普通函数


    let zz = function(a, b){
    return a + b
    }

    箭头函数的用法


    我们打印fn函数的原型,我们会发现箭头函数本身没有this;


    var fn = (a, b) => {
    console.log(this, fn.prototype);
    //window, undefined
    var fn2 = () => {
    console.log(this, '测试');
    // window
    };
    fn2();
    }
    fn()

    箭头函数的arguments
    我们会发现这样写会报语法错误


    var fn = (a) => {
    console.log(a.arguments)
    }
    fn();
    // TypeError:Cannot read property 'arguments' of undefined

    我们换一种情况,我们看代码会发现箭头函数argemnets指向了上一个函数



    箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。




    var z = function(a){
    console.log(arguments);
    bb();
    function bb() {
    console.log(arguments);
    let ac = () => {
    console.log(arguments);
    //arguments 指向第二层函数
    };
    ac();
    }
    }
    z()

    什么时候不能用箭头函数


    1. 通过构造函数调用


    let Foo = () =>  {

    }
    let result = new Foo();
    //TypeError: Foo is not a constructor

    2. 需要使用prototype


    let foo = () =>  {

    }
    console.log(foo.prototype)
    //underfind

    3. 没有super



    连原型都没有,自然也不能通过 super 来访问原型的属性,所以箭头函数也是没有 super 的,不过跟 this、arguments、new.target 一样,这些值由外围最近一层非箭头函数决定



    总结




    • 如果你有一个简单语句的在线函数表达式,其中唯一的语句是return某个计算出的值,而且这个函数内部没有this引用,且没有自身引用(比如递归,事件绑定/解绑定),且不会要求函数执行这些,那么我们可以安全的把它重构为=>箭头函数




    • 如果你的内层函数表达式依赖于它的函数中调用 let self= this 或者.bind(this)来确保适当的this绑定,那么内层函数表达式可以转换为=>箭头函数




    • 如果你的内函数表达式依赖于封装函数像 let args = Array.prototype.slice.call
      (arguments)的词法复制,那么这个内层函数表达式应该可以安全的转换=>箭头函数




    • 所有的其他情况——函数声明,较长的多函数表达式,需要词法名称标识符(比如递归 , 构造函数)的函数,以及任何不符合以上几点特征的函数一般都应该避免=>箭头函数





    关于this arguments 和 super 的词法绑定。这是利用es6的特性来修正一些常见的问题,而不是bug或者错误。


    作者:zz
    链接:https://juejin.cn/post/7011270097721360421
    收起阅读 »

    ?Map和Set巧解力扣算法问题

    问题一:什么是Map和Set? ES6以前,在JavaScript中实现“键/值”式存储可以使用Object来方便高效的完成,也就是使用对象属性作为键,再使用属性来引用值,像下面这样 let student = { name: '啊呜', se...
    继续阅读 »

    问题一:什么是Map和Set?


    ES6以前,在JavaScript中实现“键/值”式存储可以使用Object来方便高效的完成,也就是使用对象属性作为键,再使用属性来引用值,像下面这样


    let student = {
    name: '啊呜',
    sex: 'male',
    age: 18
    }

    但是这种实现并非没有问题,这里的键只能是对象的属性,于是就出现了Map这一新的集合类型,为JavaScript带来了真正的键/值存储机制,我们可以这样初始化映射:


    const map = new Map([
    ['key1','value1'],
    ['key2','value2'],
    ['key3','value3'],
    ])

    ES6还新增了Set这一种新的集合类型,Set在很多方面都像是加强的Map,这是因为它们的大多数API和行为都是共有的。Set集合类型的特点是不能存储重复元素,成员值都是唯一且没有重复的值


    问题二:Map和Set的基本API怎么用?


    Map的API:




    • get() :返回键值对




    • set() :添加键值对,返回实例




    • delete() :删除键值对,返回布尔




    • has() :检查键值对,返回布尔




    • clear() :清除所有成员




    • keys() :返回以键为遍历器的对象




    • values() :返回以值为遍历器的对象




    • entries() :返回以键和值为遍历器的对象




    • forEach() :使用回调函数遍历每个成员




    Set的API:




    • add() :添加值,返回实例




    • delete() :删除值,返回布尔




    • has() :检查值,返回布尔




    • clear() :清除所有成员




    • keys() :返回以属性值为遍历器的对象




    • values() :返回以属性值为遍历器的对象




    • entries() :返回以属性值和属性值为遍历器的对象




    • forEach() :使用回调函数遍历每个成员





    好啦,到这,相信你对JS中的Map和Set有了一定的了解,我们现在尝试使用这两种集合类型,在LeetCode中大显身手~



    LeetCode20:有效的括号



    给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。



    示例1:



    输入: s = "()"

    输出: true



    示例2:



    输入: s = "()[]{}"

    输出: true



    示例 3:



    输入: s = "(]"

    输出: false



    这题的思路是使用栈+Map来解决,直接上代码:


    carbon (2).png


    LeetCode141:环形链表



    给定一个链表,判断链表中是否有环。



    示例:


    circularlinkedlist.png



    输入: head = [3,2,0,-4], pos = 1

    输出: true



    这题我的思路是使用Set来解决,当然还有一种方法,用快慢指针来解决,但是比较难想到,而且比较反人类,我们这里只介绍Set,清晰易懂~


    carbon (3).png



    作者:_啊呜
    链接:https://juejin.cn/post/7011710641807294477
    收起阅读 »

    深入理解 redux 数据流和异步过程管理

    前端框架的数据流 前端框架实现了数据驱动视图变化的功能,我们用 template 或者 jsx 描述好了数据和视图的绑定关系,然后就只需要关心数据的管理了。 数据在组件和组件之间、组件和全局 store 之间传递,叫做前端框架的数据流。 一般来说,除了某部分状...
    继续阅读 »

    前端框架的数据流


    前端框架实现了数据驱动视图变化的功能,我们用 template 或者 jsx 描述好了数据和视图的绑定关系,然后就只需要关心数据的管理了。


    数据在组件和组件之间、组件和全局 store 之间传递,叫做前端框架的数据流。


    一般来说,除了某部分状态数据是只有某个组件关心的,我们会把状态数据放在组件内以外,业务数据、多个组件关心的状态数据都会放在 store 里面。组件从 store 中取数据,当交互的时候去通知 store 改变对应的数据。


    这个 store 不一定是 redux、mobox 这些第三方库,其实 react 内置的 context 也可以作为 store。但是 context 做为 store 有一个问题,任何组件都能从 context 中取出数据来修改,那么当排查问题的时候就特别困难,因为并不知道是哪个组件把数据改坏的,也就是数据流不清晰。


    正是因为这个原因,我们几乎见不到用 context 作为 store,基本都是搭配一个 redux。


    所以为什么 redux 好呢?第一个原因就是数据流清晰,改变数据有统一的入口。



    组件里都是通过 dispatch 一个 action 来触发 store 的修改,而且修改的逻辑都是在 reducer 里面,组件再监听 store 的数据变化,从中取出最新的数据。


    这样数据流动是单向的,清晰的,很容易管理。


    这就像为什么我们在公司里想要什么权限都要走审批流,而不是直接找某人,一样的道理。集中管理流程比较清晰,而且还可以追溯。


    异步过程的管理


    很多情况下改变 store 数据都是一个异步的过程,比如等待网络请求返回数据、定时改变数据、等待某个事件来改变数据等,那这些异步过程的代码放在哪里呢?


    组件?


    放在组件里是可以,但是异步过程怎么跨组件复用?多个异步过程之间怎么做串行、并行等控制?


    所以当异步过程比较多,而且异步过程与异步过程之间也不独立,有串行、并行、甚至更复杂的关系的时候,直接把异步逻辑放组件内不行。


    不放组件内,那放哪呢?


    redux 提供的中间件机制是不是可以用来放这些异步过程呢?


    redux 中间件


    先看下什么是 redux 中间件:


    redux 的流程很简单,就是 dispatch 一个 action 到 store, reducer 来处理 action。那么如果想在到达 store 之前多做一些处理呢?在哪里加?


    改造 dispatch!中间件的原理就是层层包装 dispatch。


    下面是 applyMiddleware 的源码,可以看到 applyMiddleware 就是对 store.dispatch 做了层层包装,最后返回修改了 dispatch 之后的 store。


    function applyMiddleware(middlewares) {
    let dispatch = store.dispatch
    middlewares.forEach(middleware =>
    dispatch = middleware(store)(dispatch)
    )
    return { ...store, dispatch}
    }

    所以说中间件最终返回的函数就是处理 action 的 dispatch:


    function middlewareXxx(store) {
    return function (next) {
    return function (action) {
    // xx
    };
    };
    };
    }

    中间件会包装 dispatch,而 dispatch 就是把 action 传给 store 的,所以中间件自然可以拿到 action、拿到 store,还有被包装的 dispatch,也就是 next。


    比如 redux-thunk 中间件的实现:


    function createThunkMiddleware(extraArgument) {
    return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument);
    }

    return next(action);
    };
    }

    const thunk = createThunkMiddleware();

    它判断了如果 action 是一个函数,就执行该函数,并且把 store.dispath 和 store.getState 传进去,否则传给内层的 dispatch。


    通过 redux-thunk 中间件,我们可以把异步过程通过函数的形式放在 dispatch 的参数里:


    const login = (userName) => (dispatch) => {
    dispatch({ type: 'loginStart' })
    request.post('/api/login', { data: userName }, () => {
    dispatch({ type: 'loginSuccess', payload: userName })
    })
    }
    store.dispatch(login('guang'))

    但是这样解决了组件里的异步过程不好复用、多个异步过程之间不好做并行、串行等控制的问题了么?


    没有,这段逻辑依然是在组件里写,只不过移到了 dispatch 里,也没有提供多个异步过程的管理机制。


    解决这个问题,需要用 redux-saga 或 redux-observable 中间件。


    redux-saga


    redux-saga 并没有改变 action,它会把 action 透传给 store,只是多加了一条异步过程的处理。



    redux-saga 中间件是这样启用的:


    import { createStore, applyMiddleware } from 'redux'
    import createSagaMiddleware from 'redux-saga'
    import rootReducer from './reducer'
    import rootSaga from './sagas'

    const sagaMiddleware = createSagaMiddleware()
    const store = createStore(rootReducer, {}, applyMiddleware(sagaMiddleware))
    sagaMiddleware.run(rootSaga)

    要调用 run 把 saga 的 watcher saga 跑起来:


    watcher saga 里面监听了一些 action,然后调用 worker saga 来处理:


    import { all, takeLatest } from 'redux-saga/effects'

    function* rootSaga() {
    yield all([
    takeLatest('login', login),
    takeLatest('logout', logout)
    ])
    }
    export default rootSaga

    redux-saga 会先把 action 透传给 store,然后判断下该 action 是否是被 taker 监听的:


    function sagaMiddleware({ getState, dispatch }) {
    return function (next) {
    return function (action) {
    const result = next(action);// 把 action 透传给 store

    channel.put(action); //触发 saga 的 action 监听流程

    return result;
    }
    }
    }

    当发现该 action 是被监听的,那么就执行相应的 taker,调用 worker saga 来处理:


    function* login(action) {
    try {
    const loginInfo = yield call(loginService, action.account)
    yield put({ type: 'loginSuccess', loginInfo })
    } catch (error) {
    yield put({ type: 'loginError', error })
    }
    }

    function* logout() {
    yield put({ type: 'logoutSuccess'})
    }

    比如 login 和 logout 会有不同的 worker saga。


    login 会请求 login 接口,然后触发 loginSuccess 或者 loginError 的 action。


    logout 会触发 logoutSuccess 的 action。


    redux saga 的异步过程管理就是这样的:先把 action 透传给 store,然后判断 action 是否是被 taker 监听的,如果是,则调用对应的 worker saga 进行处理。


    redux saga 在 redux 的 action 流程之外,加了一条监听 action 的异步处理的流程。


    其实整个流程还是比较容易理解的。理解成本高一点的就是 generator 的写法了:


    比如下面这段代码:


    function* xxxSaga() {
    while(true) {
    yield take('xxx_action');
    //...
    }
    }

    它就是对每一个监听到的 xxx_action 做同样的处理的意思,相当于 takeEvery:


    function* xxxSaga() {
    yield takeEvery('xxx_action');
    //...
    }

    但是因为有一个 while(true),很多同学就不理解了,这不是死循环了么?


    不是的。generator 执行后返回的是一个 iterator,需要另外一个程序调用 next 方法才会继续执行。所以怎么执行、是否继续执行都是由另一个程序控制的。


    在 redux-saga 里面,控制 worker saga 执行的程序叫做 task。worker saga 只是告诉了 task 应该做什么处理,通过 call、fork、put 这些命令(这些命令叫做 effect)。


    然后 task 会调用不同的实现函数来执行该 worker saga。


    为什么要这样设计呢?直接执行不就行了,为啥要拆成 worker saga 和 task 两部分,这样理解成本不就高了么?


    确实,设计成 generator 的形式会增加理解成本,但是换来的是可测试性。因为各种副作用,比如网络请求、dispatch action 到 store 等等,都变成了 call、put 等 effect,由 task 部分控制执行。那么具体怎么执行的就可以随意的切换了,这样测试的时候只需要模拟传入对应的数据,就可以测试 worker saga 了。


    redux saga 设计成 generator 的形式是一种学习成本和可测试性的权衡。


    还记得 redux-thunk 有啥问题么?多个异步过程之间的并行、串行的复杂关系没法处理。那 redux-saga 是怎么解决的呢?


    redux-saga 提供了 all、race、takeEvery、takeLatest 等 effect 来指定多个异步过程的关系:


    比如 takeEvery 会对多个 action 的每一个做同样的处理,takeLatest 会对多个 action 的最后一个做处理,race 会只返回最快的那个异步过程的结果,等等。


    这些控制多个异步过程之间关系的 effect 正是 redux-thunk 所没有的,也是复杂异步过程的管理必不可少的部分。


    所以 redux-saga 可以做复杂异步过程的管理,而且具有很好的可测试性。


    其实异步过程的管理,最出名的是 rxjs,而 redux-observable 就是基于 rxjs 实现的,它也是一种复杂异步过程管理的方案。


    redux-observable


    redux-observable 用起来和 redux-saga 特别像,比如启用插件的部分:


    const epicMiddleware = createEpicMiddleware();

    const store = createStore(
    rootReducer,
    applyMiddleware(epicMiddleware)
    );

    epicMiddleware.run(rootEpic);

    和 redux saga 的启动流程是一样的,只是不叫 saga 而叫 epic。


    但是对异步过程的处理,redux saga 是自己提供了一些 effect,而 redux-observable 是利用了 rxjs 的 operator:


    import { ajax } from 'rxjs/ajax';

    const fetchUserEpic = (action$, state$) => action$.pipe(
    ofType('FETCH_USER'),
    mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe(
    map(response => ({
    type: 'FETCH_USER_FULFILLED',
    payload: response
    }))
    )
    );

    通过 ofType 来指定监听的 action,处理结束返回 action 传递给 store。


    相比 redux-saga 来说,redux-observable 支持的异步过程的处理更丰富,直接对接了 operator 的生态,是开放的,而 redux-saga 则只是提供了内置的几个 effect 来处理。


    所以做特别复杂的异步流程处理的时候,redux-observable 能够利用 rxjs 的操作符的优势会更明显。


    但是 redux-saga 的优点还有基于 generator 的良好的可测试性,而且大多数场景下,redux-saga 提供的异步过程的处理能力就足够了,所以相对来说,redux-saga 用的更多一些。


    总结


    前端框架实现了数据到视图的绑定,我们只需要关心数据流就可以了。


    相比 context 的混乱的数据流,redux 的 view -> action -> store -> view 的单向数据流更清晰且容易管理。


    前端代码中有很多异步过程,这些异步过程之间可能有串行、并行甚至更复杂的关系,放在组件里并不好管理,可以放在 redux 的中间件里。


    redux 的中间件就是对 dispatch 的层层包装,比如 redux-thunk 就是判断了下 action 是 function 就执行下,否则就是继续 dispatch。


    redux-thunk 并没有提供多个异步过程管理的机制,复杂异步过程的管理还是得用 redux-saga 或者 redux-observable。


    redux-saga 透传了 action 到 store,并且监听 action 执行相应的异步过程。异步过程的描述使用 generator 的形式,好处是可测试性。比如通过 take、takeEvery、takeLatest 来监听 action,然后执行 worker saga。worker saga 可以用 put、call、fork 等 effect 来描述不同的副作用,由 task 负责执行。


    redux-observable 同样监听了 action 执行相应的异步过程,但是是基于 rxjs 的 operator,相比 saga 来说,异步过程的管理功能更强大。


    不管是 redux-saga 通过 generator 来组织异步过程,通过内置 effect 来处理多个异步过程之间的关系,还是 redux-observable 通过 rxjs 的 operator 来组织异步过程和多个异步过程之间的关系。它们都解决了复杂异步过程的处理的问题,可以根据场景的复杂度灵活选用。


    作者:zxg_神说要有光
    链接:https://juejin.cn/post/7011835078594527263

    收起阅读 »

    【JavaScript】async await 更优雅的错误处理

    背景 团队来了新的小伙伴,发现我们的团队代码规范中,要给 async await 添加 try...catch。他感觉很疑惑,假如有很多个(不集中),那不是要加很多个地方?那不是很不优雅? 为什么要错误处理 JavaScript 是一个单线程的语言,假如不加...
    继续阅读 »

    背景


    团队来了新的小伙伴,发现我们的团队代码规范中,要给 async await 添加 try...catch。他感觉很疑惑,假如有很多个(不集中),那不是要加很多个地方?那不是很不优雅?


    为什么要错误处理


    JavaScript 是一个单线程的语言,假如不加 try ...catch ,会导致直接报错无法继续执行。当然不意味着你代码中一定要用 try...catch 包住,使用 try...catch 意味着你知道这个位置代码很可能出现报错,所以你使用了 try...catch 进行捕获处理,并让程序继续执行。


    我理解我们一般在执行 async await 的时候,一般运行在异步的场景下,这种场景一般不应该阻塞流程的进行,所以推荐使用了 try...catch 的处理。


    async await 更优雅的错误处理


    但确实如那位同事所说,加 try...catch 并不是一个很优雅的行为。所以我 Google 了一下,发现 How to write async await without try-catch blocks in Javascript 这篇文章中提到了一种更优雅的方法处理,并封装成了一个库——await-to-js。这个库只有一个 function,我们完全可以将这个函数运用到我们的业务中,如下所示:


    /**
    * @param { Promise } promise
    * @param { Object= } errorExt - Additional Information you can pass to the err object
    * @return { Promise }
    */
    export function to<T, U = Error> (
    promise: Promise<T>,
    errorExt?: object
    ): Promise<[U, undefined] | [null, T]> {
    return promise
    .then<[null, T]>((data: T) => [null, data]) // 执行成功,返回数组第一项为 null。第二个是结果。
    .catch<[U, undefined]>((err: U) => {
    if (errorExt) {
    Object.assign(err, errorExt);
    }

    return [err, undefined]; // 执行失败,返回数组第一项为错误信息,第二项为 undefined
    });
    }

    export default to;

    这里需要有一个前置的知识点:await 是在等待一个 Promise 的返回值


    正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。


    所以我们只需要利用 Promise 的特性,分别在 promise.thenpromise.catch 中返回不同的数组,其中 fulfilled 的时候返回数组第一项为 null,第二个是结果。rejected 的时候,返回数组第一项为错误信息,第二项为 undefined。使用的时候,判断第一项是否为空,即可知道是否有错误,具体使用如下:


    import to from 'await-to-js';
    // If you use CommonJS (i.e NodeJS environment), it should be:
    // const to = require('await-to-js').default;

    async function asyncTaskWithCb(cb) {
    let err, user, savedTask, notification;

    [ err, user ] = await to(UserModel.findById(1));
    if(!user) return cb('No user found');

    [ err, savedTask ] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
    if(err) return cb('Error occurred while saving task');

    if(user.notificationsEnabled) {
    [ err ] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
    if(err) return cb('Error while sending notification');
    }

    if(savedTask.assignedUser.id !== user.id) {
    [ err, notification ] = await to(NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you'));
    if(err) return cb('Error while sending notification');
    }

    cb(null, savedTask);
    }

    小结


    async await 中添加错误处理个人认为是有必要的,但方案不仅仅只有 try...catch。利用 async awaitPromise 的特性,我们可以更加优雅的处理 async await 的错误。


    链接:https://juejin.cn/post/7011299888465969166

    收起阅读 »

    你知道如何批量创建一批邮箱吗?

    1.前期准备 搭建邮件服务器需要一些“基础建设”,包括如下 一台服务器 推荐centos 一个域名 1.1 配置细节 邮件服务器是通过SMTP协议进行通信,为了让服务器能够成功接收邮件,我们需要打开25这个端口,并允许访问25端口。同时如果你需要使用像类似...
    继续阅读 »

    1.前期准备


    搭建邮件服务器需要一些“基础建设”,包括如下



    • 一台服务器 推荐centos

    • 一个域名


    1.1 配置细节


    邮件服务器是通过SMTP协议进行通信,为了让服务器能够成功接收邮件,我们需要打开25这个端口,并允许访问25端口。同时如果你需要使用像类似foxmail这种客户端接发收邮件,还需要支持POP3协议,需要打开110端口。换句话说为了保证邮件服务的正常使用,需要开启25和110这两个端口



    关于 POP3协议(Post Office Protocol 3):协议主要用于支持使用客户端远程管理在服务器上的电子邮件,将电子邮件存储到本地主机



    下图是阿里云服务器配置安全策略组的规则,在其中加入一条访问规则


    image.png


    接下来是域名,需要配置域名解析,配置主机记录


    如下图是域名的解析配置,主要包括几个记录数值




    • MX类:增加 MX 记录,类型选择 MX记录,值可以填写主机名,也可以填写你的公网ip地址也可以是mail.example.com。如果配置的是域名,还需要新增一条A类型的记录,主机记录定义为:mail,具体看下图




    • A类:该配置主要用来支持客户端接收邮件(比如:foxmail)分别添加smtp、imap、pop等配置,记录值为 ip




    配置完如下图所示,可以在列表中看到配置好的,


    image.png


    2 服务器安装


    2.1 Postfix



    关于 postfix:Postfix 是实现 SMTP 协议的软件,也叫做邮件发送服务器,负责对邮件进行转发,具体的转发规则,就需要我们对postfix的配置进行修改



    我使用的是阿里云的服务器,首先我们安装邮件服务`postfix'



    • 安装


    yum install postfix // 服务器安装 


    • 配置


    安装成功之后,修改配置,通过vi /etc/postfix/main.cf 命令行修改以下配置


    myhostname =  email.example.com //  设置系统的主机名

    mydomain = example.com  // 设置域名(我们将让此处设置将成为E-mail地址“@”后面的部分)

    myorigin = $mydomain  // 将发信地址“@”后面的部分设置为域名(非系统主机名)

    inet_interfaces = all  // 接受来自所有网络的请求

    mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain  // 指定发给本地邮件的域名

    home_mailbox = Maildir/  // 指定用户邮箱目录

    # 规定邮件最大尺寸为10M
    message_size_limit = 10485760
    # 规定收件箱最大容量为1G
    mailbox_size_limit = 1073741824
    # SMTP认证
    smtpd_sasl_type = dovecot
    smtpd_sasl_path = private/auth
    smtpd_sasl_auth_enable = yes
    smtpd_sasl_security_options = noanonymous
    smtpd_sasl_local_domain = $myhostname
    smtpd_recipient_restrictions = permit_mynetworks,permit_auth_destination,permit_sasl_authenticated,reject


    下图是postfix中主要的参数
    image.png



    • 启动


    配置完postfix的,启动服务


    postfix check   // 检查配置文件是否正确
    systemctl start postfix //开启postfix服务
    systemctl enable postfix //设置postfix服务开机启动

    完成postfix的配置,接下来我们还需要安装dovecot


    2.2 Dovecot



    关于 Dovecot:是一款能够为Linux系统提供IMAP和POP3电子邮件服务的开源服务程序,安全性极高,配置简单,执行速度快,而且占用的服务器硬件资源也较少。上文提到POP3/IMAP是从邮件服务器中读取邮件时使用的协议




    • 安装


    yum install dovecot // 服务器安装 


    • 配置


    安装成功之后,修改配置,通过vi /etc/dovecot/dovecot.conf 命令行修改以下配置


    protocols = imap pop3 lmtp listen = *, 

    #新添加以下配置 #

    !include conf.d/10-auth.conf

    ssl = no

    disable_plaintext_auth = no

    mail_location = maildir:~/Maildir



    • 启动


    systemctl start dovecot   //开启dovecot服务
    systemctl enable dovecot //置dovecot服务开机启动

    完成以上两个服务的配置,你离成功就近一步了!



    啊乐同学:postfix与dovecot这两个其实有什么区别?



    答:postfix主要做发送邮件使用,而dovecot主要做接收使用,两者结合才能完成一个完整的邮件服务


    3 新建用户


    搭建完邮件服务器之后,我们需要创建用户来完成 邮件的接收和发送



    • 如何创建用户


    useradd tree/ 新增用户
    passwd tree // 设置用户密码


    啊乐同学:如果这样我创建100个邮箱用户,岂不是很浪费时间?



    莫慌,我们写个shell脚本,批量创建就可以解决你这个问题


    创建一个文件,createUser.sh 内容如下


    /bash
    #user.txt 为需要创建的用户的文件passwd.txt为随机生成密码
    USER_FILE=user.txt
    pass_FILE=passwd.txt
    for user in `cat user.txt`
    do
    id $user &> /dev/null #查看用户是否存在
    if [ $? -eq 0 ]
    then
    echo "The $user already exist"
    else
    useradd $user #创建用户
    if [ $? -eq 0 ]
    then
    echo "$user create sucessful"
    PASSWD=$(echo $RANDOM |md5sum |cut -c 1-8) #随机生成数字
    echo $PASSWD |passwd --stdin $user &>/dev/null #修改用户密码
    echo -e "$user\'$PASSWD'\'$(date +%Y%m%d)'" >> $pass_FILE #将用户,密码,日期输入到文件中
    fi
    fi
    done

    前提需要建立一个user.txt 来维护我们要创建的用户,比如


    tree
    shujiang

    脚本会根据我们列出的用户名去批量生成用户


    4.测试邮箱


    搭建好服务以及完成用户的创建,接下来就是测试邮件是否正常接收环节了


    我使用的是foxmail来做验证


    image.png


    这个用户名就是我们上一节创建的用户名称,完成创建之后,我们通过发送邮件来测试是否能够成功接收


    image.png


    还有一种方式就是借助telnet去做测试,这里不做大篇幅介绍。最原始的方式



    阿乐同学:如果我每个新建的邮箱用户,我都得去配置一个客户端去接收邮寄,岂不是很费劲,有没有其他方式?



    有的,换个角度思考,你可以通过配置邮件转发,将所有邮件接收都转发到某一个用户的邮箱中去,你就可以只在该邮箱查阅邮件(我开始怀疑你的动机,是不是搞什么批量注册!)


    具体如下,需要配置下第二节中提到的postfix配置文件,在文件最后添加


    virtual_alias_domains = ensbook.com  mail.ensbook.com
    virtual_alias_maps = hash:/etc/postfix/virtual

    完成配置之后,我查阅网上一些资料,需要配置/etc/postfix/virtual文件,该文件主要用来管理电子邮件转发规则的


    于是我尝试修改/etc/postfix/virtual文件,并添加一下信息


    image.png


    这条规则的含义是:所有邮件发送至 @ensbook.com 转发到 qq邮箱


    发现竟然没有生效,最后是创建一个virtual的用户实现转发接收的。如果你看得出问题,记得在评论区告诉我



    阿乐同学:我接收不到邮箱,又不知道什么问题,如何排查?



    你可以通过tail -n /var/log/maillog查看邮件日志


    image.png


    最后


    通过上文的了解,我们不难看到,一个域名邮件服务器的创建其实很简单,而且技术很老。但是无论老不老,能够解决我们的需求就好。如果你有其他方式实现,欢迎在评论区留言。



    链接:https://juejin.cn/post/7011012089800032293

    收起阅读 »

    JavaScript深浅拷贝的实现

    前置知识 对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况 let a = { age: 1 } let b = a a.age = 2 console.log(b.a...
    继续阅读 »

    前置知识



    • 对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况


        let a = {
    age: 1
    }
    let b = a
    a.age = 2
    console.log(b.age) // 2

    浅拷贝



    • Object.assign : 拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝


        let a = {
    age: 1
    }
    let b = Object.assign({}, a)
    a.age = 2
    console.log(b.age) // 1


    • 通过展开运算符 ... 来实现浅拷贝


        let a = {
    age: 1
    }
    let b = {...a}
    a.age = 2
    console.log(b.age) // 1

    深拷贝



    • JSON.parse(JSON.stringify(object))

      • 会忽略 undefined

      • 会忽略 symbol

      • 不能序列化函数

      • 不能解决循环引用的对象,会报错抛出异常




        let a = {
    age: 1,
    jobs: {
    first: 'FE'
    }
    }
    let b = JSON.parse(JSON.stringify(a))
    a.jobs.first = 'native'
    console.log(b.jobs.first) // FE

    let a = {
    age: undefined,
    sex: Symbol('male'),
    jobs: function() {},
    name: 'yck'
    }
    let b = JSON.parse(JSON.stringify(a))
    console.log(b) // {name: "yck"}


    • 递归


        function isObject(obj) {
    //Object.prototype.toString.call(obj) === '[object Object]'要保留数组形式,用在这里并不合适
    return typeof obj === 'object' && obj != null
    }

    function cloneDeep1(obj){
    if(!isObject(obj)) return obj
    var newObj = Array.isArray(obj)? [] : {}
    for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
    newObj[key] = isObject(obj[key])? cloneDeep1(obj[key]) : obj[key]
    }
    }
    return newObj
    }


    • 问题:递归方法最大的问题在于爆栈,当数据的层次很深是就会栈溢出,例如循环引用



    var a = {
    name: "muyiy",
    a1: undefined,
    a2: null,
    a3: 123,
    book: {title: "You Don't Know JS", price: "45"}
    }
    a.circleRef = a

    // TypeError: Converting circular structure to JSON
    JSON.parse(JSON.stringify(a))

    //Uncaught RangeError: Maximum call stack size exceeded at Object.hasOwnProperty (<anonymous>)
    cloneDeep1(a)



    • 解决方法:循环检测(设置一个数组或者哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回即可)


    //哈希表
    function cloneDeep3(source, hash = new WeakMap()) {

    if (!isObject(source)) return source;
    if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表

    var target = Array.isArray(source) ? [] : {};
    hash.set(source, target); // 新增代码,哈希表设值

    for(var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
    if (isObject(source[key])) {
    target[key] = cloneDeep3(source[key], hash); // 新增代码,传入哈希表
    } else {
    target[key] = source[key];
    }
    }
    }
    return target;
    }

    //数组
    function cloneDeep3(source, uniqueList) {

    if (!isObject(source)) return source;
    if (!uniqueList) uniqueList = []; // 新增代码,初始化数组

    var target = Array.isArray(source) ? [] : {};

    // 数据已经存在,返回保存的数据
    var uniqueData = find(uniqueList, source);
    if (uniqueData) {
    return uniqueData.target;
    };

    // 数据不存在,保存源数据,以及对应的引用
    uniqueList.push({
    source: source,
    target: target
    });

    for(var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
    if (isObject(source[key])) {
    target[key] = cloneDeep3(source[key], uniqueList); // 新增代码,传入数组
    } else {
    target[key] = source[key];
    }
    }
    }
    return target;
    }

    // 新增方法,用于查找
    function find(arr, item) {
    for(var i = 0; i < arr.length; i++) {
    if (arr[i].source === item) {
    return arr[i];
    }
    }
    return null;
    }

    链接:https://juejin.cn/post/7010707434473783309

    收起阅读 »

    为什么 Vue2 this 能够直接获取到 data 和 methods ? 源码揭秘!

    1. 前言 写相对很难的源码,耗费了自己的时间和精力,也没收获多少阅读点赞,其实是一件挺受打击的事情。从阅读量和读者受益方面来看,不能促进作者持续输出文章。 所以转变思路,写一些相对通俗易懂的文章。其实源码也不是想象的那么难,至少有很多看得懂。歌德曾说:读一...
    继续阅读 »

    1. 前言



    写相对很难的源码,耗费了自己的时间和精力,也没收获多少阅读点赞,其实是一件挺受打击的事情。从阅读量和读者受益方面来看,不能促进作者持续输出文章。
    所以转变思路,写一些相对通俗易懂的文章。其实源码也不是想象的那么难,至少有很多看得懂。歌德曾说:读一本好书,就是在和高尚的人谈话。
    同理可得:读源码,也算是和作者的一种学习交流的方式。



    本文源于一次源码共读群里群友的提问,请问,“为什么 data 中的数据可以用 this 直接获取到啊”,当时我翻阅源码做出了解答。想着如果下次有人再次问到,我还需要回答一次。当时打算有空写篇文章告诉读者自己探究原理,于是就有了这篇文章。


    阅读本文,你将学到:


    1. 如何学习调试 vue2 源码
    2. data 中的数据为什么可以用 this 直接获取到
    3. methods 中的方法为什么可以用 this 直接获取到
    4. 学习源码中优秀代码和思想,投入到自己的项目中

    本文不难,用过 Vue 的都看得懂,希望大家动手调试和学会看源码。


    看源码可以大胆猜测,最后小心求证。


    2. 示例:this 能够直接获取到 data 和 methods


    众所周知,这样是可以输出我是若川的。好奇的人就会思考为啥 this 就能直接访问到呢。


    const vm = new Vue({
    data: {
    name: '我是若川',
    },
    methods: {
    sayName(){
    console.log(this.name);
    }
    },
    });
    console.log(vm.name); // 我是若川
    console.log(vm.sayName()); // 我是若川

    那么为什么 this.xxx 能获取到data里的数据,能获取到 methods 方法。


    我们自己构造写的函数,如何做到类似Vue的效果呢。


    function Person(options){

    }

    const p = new Person({
    data: {
    name: '若川'
    },
    methods: {
    sayName(){
    console.log(this.name);
    }
    }
    });

    console.log(p.name);
    // undefined
    console.log(p.sayName());
    // Uncaught TypeError: p.sayName is not a function

    如果是你,你会怎么去实现呢。带着问题,我们来调试 Vue2源码学习。


    3. 准备环境调试源码一探究竟


    可以在本地新建一个文件夹examples,新建文件index.html文件。
    <body></body>中加上如下js


    <script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
    <script>
    const vm = new Vue({
    data: {
    name: '我是若川',
    },
    methods: {
    sayName(){
    console.log(this.name);
    }
    },
    });
    console.log(vm.name);
    console.log(vm.sayName());
    </script>

    再全局安装npm i -g http-server启动服务。


    npm i -g http-server
    cd examples
    http-server .
    // 如果碰到端口被占用,也可以指定端口
    http-server -p 8081 .

    这样就能在http://localhost:8080/打开刚写的index.html页面了。


    对于调试还不是很熟悉的读者,可以看这篇文章《前端容易忽略的 debugger 调试技巧》,截图标注的很详细。



    调试:在 F12 打开调试,source 面板,在例子中const vm = new Vue({打上断点。



    如下图所示


    刷新页面后按F11进入函数,这时断点就走进了 Vue 构造函数。


    3.1 Vue 构造函数


    function Vue (options) {
    if (!(this instanceof Vue)
    ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
    }
    // 初始化
    initMixin(Vue);
    stateMixin(Vue);
    eventsMixin(Vue);
    lifecycleMixin(Vue);
    renderMixin(Vue);

    值得一提的是:if (!(this instanceof Vue)){} 判断是不是用了 new 关键词调用构造函数。
    一般而言,我们平时应该不会考虑写这个。


    当然看源码库也可以自己函数内部调用 new 。但 vue 一般一个项目只需要 new Vue() 一次,所以没必要。


    jQuery 源码的就是内部 new ,对于使用者来说就是无new构造。


    jQuery = function( selector, context ) {
    // 返回new之后的对象
    return new jQuery.fn.init( selector, context );
    };

    因为使用 jQuery 经常要调用。
    其实 jQuery 也是可以 new 的。和不用 new 是一个效果。


    如果不明白 new 操作符的用处,可以看我之前的文章。面试官问:能否模拟实现JS的new操作符



    调试:继续在this._init(options);处打上断点,按F11进入函数。



    3.2 _init 初始化函数


    进入 _init 函数后,这个函数比较长,做了挺多事情,我们猜测跟datamethods相关的实现在initState(vm)函数里。


    // 代码有删减
    function initMixin (Vue) {
    Vue.prototype._init = function (options) {
    var vm = this;
    // a uid
    vm._uid = uid$3++;

    // a flag to avoid this being observed
    vm._isVue = true;
    // merge options
    if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options);
    } else {
    vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
    );
    }

    // expose real self
    vm._self = vm;
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    // 初始化状态
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created');
    };
    }


    调试:接着我们在initState(vm)函数这里打算断点,按F8可以直接跳转到这个断点,然后按F11接着进入initState函数。



    3.3 initState 初始化状态


    从函数名来看,这个函数主要实现功能是:


    初始化 props
    初始化 methods
    监测数据
    初始化 computed
    初始化 watch

    function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    // 有传入 methods,初始化方法
    if (opts.methods) { initMethods(vm, opts.methods); }
    // 有传入 data,初始化 data
    if (opts.data) {
    initData(vm);
    } else {
    observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
    }
    }


    我们重点来看初始化 methods,之后再看初始化 data




    调试:在 initMethods 这句打上断点,同时在initData(vm)处打上断点,看完initMethods函数后,可以直接按F8回到initData(vm)函数。
    继续按F11,先进入initMethods函数。



    3.4 initMethods 初始化方法


    function initMethods (vm, methods) {
    var props = vm.$options.props;
    for (var key in methods) {
    {
    if (typeof methods[key] !== 'function') {
    warn(
    "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
    "Did you reference the function correctly?",
    vm
    );
    }
    if (props && hasOwn(props, key)) {
    warn(
    ("Method \"" + key + "\" has already been defined as a prop."),
    vm
    );
    }
    if ((key in vm) && isReserved(key)) {
    warn(
    "Method \"" + key + "\" conflicts with an existing Vue instance method. " +
    "Avoid defining component methods that start with _ or $."
    );
    }
    }
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
    }
    }

    initMethods函数,主要有一些判断。


    判断 methods 中的每一项是不是函数,如果不是警告。
    判断 methods 中的每一项是不是和 props 冲突了,如果是,警告。
    判断 methods 中的每一项是不是已经在 new Vue实例 vm 上存在,而且是方法名是保留的 _ $ (在JS中一般指内部变量标识)开头,如果是警告。

    除去这些判断,我们可以看出initMethods函数其实就是遍历传入的methods对象,并且使用bind绑定函数的this指向为vm,也就是new Vue的实例对象。


    这就是为什么我们可以通过this直接访问到methods里面的函数的原因


    我们可以把鼠标移上 bind 变量,按alt键,可以看到函数定义的地方,这里是218行,点击跳转到这里看 bind 的实现。


    3.4.1 bind 返回一个函数,修改 this 指向


    function polyfillBind (fn, ctx) {
    function boundFn (a) {
    var l = arguments.length;
    return l
    ? l > 1
    ? fn.apply(ctx, arguments)
    : fn.call(ctx, a)
    : fn.call(ctx)
    }

    boundFn._length = fn.length;
    return boundFn
    }

    function nativeBind (fn, ctx) {
    return fn.bind(ctx)
    }

    var bind = Function.prototype.bind
    ? nativeBind
    : polyfillBind;

    简单来说就是兼容了老版本不支持 原生的bind函数。同时兼容写法,对参数多少做出了判断,使用callapply实现,据说是因为性能问题。


    如果对于call、apply、bind的用法和实现不熟悉,可以查看我在面试官问系列中写的面试官问:能否模拟实现JS的call和apply方法
    面试官问:能否模拟实现JS的bind方法



    调试:看完了initMethods函数,按F8回到上文提到的initData(vm)函数断点处。



    3.5 initData 初始化 data


    initData 函数也是一些判断。主要做了如下事情:


    先给 _data 赋值,以备后用。
    最终获取到的 data 不是对象给出警告。
    遍历 data ,其中每一项:
    如果和 methods 冲突了,报警告。
    如果和 props 冲突了,报警告。
    不是内部私有的保留属性,做一层代理,代理到 _data 上。
    最后监测 data,使之成为响应式的数据。

    function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
    if (!isPlainObject(data)) {
    data = {};
    warn(
    'data functions should return an object:\n' +
    'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
    vm
    );
    }
    // proxy data on instance
    var keys = Object.keys(data);
    var props = vm.$options.props;
    var methods = vm.$options.methods;
    var i = keys.length;
    while (i--) {
    var key = keys[i];
    {
    if (methods && hasOwn(methods, key)) {
    warn(
    ("Method \"" + key + "\" has already been defined as a data property."),
    vm
    );
    }
    }
    if (props && hasOwn(props, key)) {
    warn(
    "The data property \"" + key + "\" is already declared as a prop. " +
    "Use prop default value instead.",
    vm
    );
    } else if (!isReserved(key)) {
    proxy(vm, "_data", key);
    }
    }
    // observe data
    observe(data, true /* asRootData */);
    }

    3.5.1 getData 获取数据


    是函数时调用函数,执行获取到对象。


    function getData (data, vm) {
    // #7573 disable dep collection when invoking data getters
    pushTarget();
    try {
    return data.call(vm, vm)
    } catch (e) {
    handleError(e, vm, "data()");
    return {}
    } finally {
    popTarget();
    }
    }

    3.5.2 proxy 代理


    其实就是用 Object.defineProperty 定义对象


    这里用处是:this.xxx 则是访问的 this._data.xxx


    /**
    * Perform no operation.
    * Stubbing args to make Flow happy without leaving useless transpiled code
    * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
    */
    function noop (a, b, c) {}
    var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
    };

    function proxy (target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
    }

    3.5.3 Object.defineProperty 定义对象属性


    Object.defineProperty 算是一个非常重要的API。还有一个定义多个属性的API:Object.defineProperties(obj, props) (ES5)


    Object.defineProperty 涉及到比较重要的知识点,面试也常考。


    value——当试图获取属性时所返回的值。
    writable——该属性是否可写。
    enumerable——该属性在for in循环中是否会被枚举。
    configurable——该属性是否可被删除。
    set()——该属性的更新操作所调用的函数。
    get()——获取属性值时所调用的函数。

    详细举例见此链接


    3.6 文中出现的一些函数,最后统一解释下


    3.6.1 hasOwn 是否是对象本身拥有的属性


    调试模式下,按alt键,把鼠标移到方法名上,可以看到函数定义的地方。点击可以跳转。


    /**
    * Check whether an object has the property.
    */
    var hasOwnProperty = Object.prototype.hasOwnProperty;
    function hasOwn (obj, key) {
    return hasOwnProperty.call(obj, key)
    }

    hasOwn({ a: undefined }, 'a') // true
    hasOwn({}, 'a') // false
    hasOwn({}, 'hasOwnProperty') // false
    hasOwn({}, 'toString') // false
    // 是自己的本身拥有的属性,不是通过原型链向上查找的。

    3.6.2 isReserved 是否是内部私有保留的字符串$ 和 _ 开头


    /**
    * Check if a string starts with $ or _
    */
    function isReserved (str) {
    var c = (str + '').charCodeAt(0);
    return c === 0x24 || c === 0x5F
    }
    isReserved('_data'); // true
    isReserved('$options'); // true
    isReserved('data'); // false
    isReserved('options'); // false

    4. 最后用60余行代码实现简化版


    function noop (a, b, c) {}
    var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
    };
    function proxy (target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
    }
    function initData(vm){
    const data = vm._data = vm.$options.data;
    const keys = Object.keys(data);
    var i = keys.length;
    while (i--) {
    var key = keys[i];
    proxy(vm, '_data', key);
    }
    }
    function initMethods(vm, methods){
    for (var key in methods) {
    vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm);
    }
    }

    function Person(options){
    let vm = this;
    vm.$options = options;
    var opts = vm.$options;
    if(opts.data){
    initData(vm);
    }
    if(opts.methods){
    initMethods(vm, opts.methods)
    }
    }

    const p = new Person({
    data: {
    name: '若川'
    },
    methods: {
    sayName(){
    console.log(this.name);
    }
    }
    });

    console.log(p.name);
    // 未实现前: undefined
    // '若川'
    console.log(p.sayName());
    // 未实现前:Uncaught TypeError: p.sayName is not a function
    // '若川'

    5. 总结


    本文涉及到的基础知识主要有如下:


    构造函数
    this 指向
    call、bind、apply
    Object.defineProperty
    等等基础知识。

    本文源于解答源码共读群友的疑惑,通过详细的描述了如何调试 Vue 源码,来探寻答案。


    解答文章开头提问:


    通过this直接访问到methods里面的函数的原因是:因为methods里的方法通过 bind 指定了this为 new Vue的实例(vm)。


    通过 this 直接访问到 data 里面的数据的原因是:data里的属性最终会存储到new Vue的实例(vm)上的 _data对象中,访问 this.xxx,是访问Object.defineProperty代理后的 this._data.xxx


    Vue的这种设计,好处在于便于获取。也有不方便的地方,就是propsmethodsdata三者容易产生冲突。


    文章整体难度不大,但非常建议读者朋友们自己动手调试下。调试后,你可能会发现:原来 Vue 源码,也没有想象中的那么难,也能看懂一部分。


    启发:我们工作使用常用的技术和框架或库时,保持好奇心,多思考内部原理。能够做到知其然,知其所以然。就能远超很多人。


    你可能会思考,为什么模板语法中,可以省略this关键词写法呢,内部模板编译时其实是用了with。有余力的读者可以探究这一原理。


    链接:https://juejin.cn/post/7010920884789575711

    收起阅读 »

    webpack-dev-server 从入门到实战

    古有云:“工欲善其事,必先利其器”。作为一个前端开发,搭建一个便捷的开发环境,将会为我们的开发工作带来极大的效率提升。而Webpack作为如今前端工程打包必不可少的工具,很多人却不知道从Webpack 4开始提供的DevServer功能。 让我们一起来学习下吧...
    继续阅读 »

    古有云:“工欲善其事,必先利其器”。作为一个前端开发,搭建一个便捷的开发环境,将会为我们的开发工作带来极大的效率提升。而Webpack作为如今前端工程打包必不可少的工具,很多人却不知道从Webpack 4开始提供的DevServer功能。


    让我们一起来学习下吧!


    1 什么是webpack-dev-server


    DevServerWebpack 3开放的一个实验功能,使用webpack-dev-middleware中间件,提供热更新的开发服务器,旨在帮助开发者在开发阶段快速进行环境搭建。


    最新Webpack 5还支持反向代理、防火墙、Socketgzip压缩等功能。


    2 反向代理配置


    Nginx类似,webpack-dev-server也是通过url正则匹配的方式进行url代理配置,常用配置参考如下代码:


    {
    "/rest/": {
    "target": "http://127.0.0.1:8080",
    "secure": false
    }
    }

    还可以通过用JavaScript定义此配置,把多个条目代理到同一个目标。将代理配置文件设置为proxy.conf.js(代替proxy.conf.json),并指定如下例子中的配置文件。


    module.exports = {
        //...
        devServer: {
            proxy: [
                {
                    context: ['/auth', '/api'],
                    target: 'http://localhost:3000',
                },
            ],
        },
    };

    2.1 基本配置项介绍



    • proxydevServer代理配置

    • /api: 表示需要代理的请求url

    • target:反向代理的地址

    • pathRewrite:请求地址重写,类似NginxRewite功能


    其他写法参考:


    "pathRewrite": {
      "^/old/api": "/new/api"
    }

     // remove path
    pathRewrite: {
    '^/remove/api': ''
    }

    // add base path
    pathRewrite: {
    '^/': '/basepath/'
    }

    // custom rewriting
    pathRewrite: function (path, req) {
    return path.replace('/api', '/base/api');
    }

    // custom rewriting, returning Promise
    pathRewrite: async function (path, req) {
    const should_add_something = await httpRequestToDecideSomething(path);
    if (should_add_something) path += 'something';
    return path;
    }

    2.2 其他配置参考



    • logLevel:日志打印等级,支持['debug', 'info', 'warn', 'error', 'silent']silent不打印日志

    • logProvider: 自定义日志打印中间件

    • secure:是否关闭https安全认证

    • changeOrigin:修改代理请求host

    • protocolRewrite:协议重写,httphttps请求互转

    • cookieDomainRewrite:修改cookieDomain的值

    • headers:给所有请求添加headers配置

    • proxyTimeout:请求超时时间


    2.3 高级代理机制



    • onError:  对请求状态码进行处理


    function onError(err, req, res, target) {
        res.writeHead(500, {
            'Content-Type': 'text/plain',
        });
        res.end('Something went wrong. And we are reporting a custom error message.');
    }


    • onProxyRes: 对代理接口的Response处理,这里常用来获取cookie、重定向等


    function onProxyRes(proxyRes, req, res) {
        proxyRes.headers['x-added'] = 'foobar'; // 添加一个header
        delete proxyRes.headers['x-removed']; // 删除一个header
    }


    • onProxyReq:对代理接口request处理,执行在请求前,常用来设置cookieheader等操作


    function onProxyReq(proxyReq, req, res) {
        // add custom header to request
        proxyReq.setHeader('x-added', 'foobar');
        // or log the req
    }

    3 域名白名单配置


    配置该配置后,只有匹配的host地址才可以访问该服务,常用于开发阶段模拟网络网络防火墙对访问IP进行限制。当该配置项被配置为all时,会跳过host检查,但不建议这样做,因为有DNS攻击的风险。



    1. webpack配置项配置


    module.exports = {
      //...
      devServer: {
        allowedHosts: [
          'host.com',
          'subdomain.host.com',
          'subdomain2.host.com',
          'host2.com',
        ],
      },
    };


    1. cli 启动命令配置


    npx webpack serve --allowed-hosts .host.com --allowed-hosts host2.com

    4 端口配置



    1. webpack配置项配置


    module.exports = {
      //...
      devServer: {
        port: 8080,
      },
    };


    1. cli 启动命令配置


       npx webpack serve --port 8080

    5 Angular 实战 —— 通过webpack devServer代理REST接口到本地服务器


    在Angular框架中,由于对webpack进行了封装,proxy配置文件默认使用的是proxy.config.json。(js格式配置文件需要到angular.json配置文件中修改),这里以proxy.config.json为例。



    1. 代理所有以/rest/开头的接口到127.0.0.1:8080,并且将/rest/请求地址转为/


    {
      "/rest/": {
        "target": "http://127.0.0.1:8080",
        "secure": false,
        "pathRewrite": {
          "/rest/": "/"
        },
        "changeOrigin": true,
        "logLevel": "debug",
        "proxyTimeout": 3000
      }
    }

    访问启动地址测试{{ host地址}}/rest/testApi



    1. 给所有的/rest/接口加上cftk的header


    这个需要使用js格式的proxy配置文件,修改angular.json中的proxyConfig为 proxy.config.js,在proxy.config.js中添加如下内容:


    const PROXY_CONFIG = [
        {
            "target": "http://127.0.0.1:8080",
            "secure": false,
            "pathRewrite": {
                "/rest/": "/"
            },
            "changeOrigin": true,
            "logLevel": "debug",
            "proxyTimeout": 3000,
            "onProxyReq": (request, req, res) => {
                request.setHeader('cftk', 'my cftk');
            }
        },
    ];
    module.exports = PROXY_CONFIG;

    6 webpack-dev-server 与 nginx 的对比



    作者:DevUI团队
    链接:https://juejin.cn/post/7010571347705200671

    收起阅读 »

    JavaScript实现2048小游戏,我终于赢了一把

    效果图 实现思路 编写页面和画布代码。 绘制背景。 绘制好全部卡片。 随机生成一个卡片(2或者4)。 键盘事件监听(上、下、左、右键监听)。 根据键盘的方向,处理数字的移动合并。 加入成功、失败判定。 处理其他收尾工作。 代码实现编写页面代码 <...
    继续阅读 »

    效果图


    在这里插入图片描述


    实现思路



    1. 编写页面和画布代码。

    2. 绘制背景。

    3. 绘制好全部卡片。

    4. 随机生成一个卡片(2或者4)。

    5. 键盘事件监听(上、下、左、右键监听)。

    6. 根据键盘的方向,处理数字的移动合并。

    7. 加入成功、失败判定。

    8. 处理其他收尾工作。


    代码实现

    编写页面代码



    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>2048</title>
    <style>
    #box{
    width:370px;
    height:370px;
    position:absolute;
    margin:0 auto;
    left:0;
    right:0;
    top:1px;
    bottom:0;
    }

    .rebutton{
    position: absolute;
    top:370px;
    left:38%;
    }
    </style>
    </head>
    <body>
    <div id='box'></div>
    <button onclick="restart()" class='rebutton'>重开</button>
    </body>
    <script src="js/util.js"></script>
    <script src="js/2048.js"></script>
    <script type="text/javascript">

    </script>
    </html>

    复制代码

    添加画布


    在2048.js编写代码



    1. 创建函数


    function G2048(){
    this.renderArr=[];//渲染数组
    this.cards=initCardArray();
    //游戏标记
    this.flag='start';
    }
    //初始化数组
    function initCardArray(){
    var cards = new Array();
    for (var i = 0; i < 4; i++) {
    cards[i] = new Array();
    for (var j = 0; j < 4; j++) {
    //cards[i][j]=null;
    }
    }
    return cards;
    }


    1. 初始化和绘制背景代码(在2048.js中编写)


    //初始化
    G2048.prototype.init=function(el,musicObj){
    if(!el) return ;
    this.el=el;
    var canvas = document.createElement('canvas');//创建画布
    canvas.style.cssText="background:white;";
    var W = canvas.width = 370; //设置宽度
    var H = canvas.height = 370;//设置高度

    el.appendChild(canvas);//添加到指定的dom对象中
    this.ctx = canvas.getContext('2d');

    this.draw();
    }
    //绘制入口
    G2048.prototype.draw=function(){
    //创建背景
    this.drawGB();

    //渲染到页面上
    this.render();

    }

    //创建背景
    G2048.prototype.drawGB=function(){
    var bg = new _.Rect({x:0,y:0,width:364,height:364,fill:true,fillStyle:'#428853'});
    this.renderArr.push(bg);
    }

    //渲染图形
    G2048.prototype.render=function(){
    var context=this.ctx;
    this.clearCanvas();
    _.each(this.renderArr,function(item){
    item && item.render(context);
    });
    }
    //清洗画布
    G2048.prototype.clearCanvas=function() {
    this.ctx.clearRect(0,0,parseInt(this.w),parseInt(this.h));
    }


    1. 在页面代码中加入以下 js 代码


    var box = document.getElementById('box');
    g2048.init(box);

    在这里插入图片描述
    运行效果:
    在这里插入图片描述


    绘制好全部卡片



    1. 创建Card


    //定义Card
    function Card(i,j){
    this.i=i;//下标i
    this.j=j;//下标j
    this.x=0;// x坐标
    this.y=0;// y坐标
    this.h=80;//高
    this.w=80;//宽
    this.start=10;//偏移量(固定值)
    this.num=0;//显示数字
    this.merge=false;//当前是否被合并过,如果合并了,则不能继续合并,针对当前轮
    //初始化创建
    this.obj = this.init();
    //创建显示数字对象
    this.numText = this.initNumText();
    }
    //初始创建
    Card.prototype.init=function(){
    return new _.Rect({x:this.x,y:this.y,width:this.w,height:this.h,fill:true});
    }
    //根据i j计算x y坐标
    Card.prototype.cal=function(){
    this.x = this.start + this.j*this.w + (this.j+1)*5;
    this.y = this.start + this.i*this.h + (this.i+1)*5;
    //更新给obj
    this.obj.x=this.x;
    this.obj.y=this.y;
    //设置填充颜色
    this.obj.fillStyle=this.getColor();

    //更新文字的位置
    this.numText.x = this.x+40;
    this.numText.y = this.y+55;
    this.numText.text=this.num;
    }
    //初始化显示数字对象
    Card.prototype.initNumText=function(){
    var font = "34px 思源宋体";
    var fillStyle = "#7D4E33";
    return new _.Text({x:this.x,y:this.y+50,text:this.num,fill:true,textAlign:'center',font:font,fillStyle:fillStyle});
    }
    //获取color
    Card.prototype.getColor=function(){
    var color;
    //根据num设定颜色
    switch (this.num) {
    case 2:
    color = "#EEF4EA";
    break;
    case 4:
    color = "#DEECC8";
    break;
    case 8:
    color = "#AED582";
    break;
    case 16:
    color = "#8EC94B";
    break;
    case 32:
    color = "#6F9430";
    break;
    case 64:
    color = "#4CAE7C";
    break;
    case 128:
    color = "#3CB490";
    break;
    case 256:
    color = "#2D8278";
    break;
    case 512:
    color = "#09611A";
    break;
    case 1024:
    color = "#F2B179";
    break;
    case 2048:
    color = "#DFB900";
    break;

    default://默认颜色
    color = "#5C9775";
    break;
    }

    return color;
    }

    Card.prototype.render=function(context){
    //计算坐标等
    this.cal();
    //执行绘制
    this.obj.render(context);
    //是否绘制文字的处理
    if(this.num!=0){
    this.numText.render(context);
    }
    }

    }


    1. 创建卡片


    	//创建卡片
    G2048.prototype.drawCard=function(){
    var that=this;
    var card;
    for (var i = 0; i < 4; i++) {
    for (var j = 0; j < 4; j++) {
    card = new Card(i,j);
    that.cards[i][j]=card;
    that.renderArr.push(card);
    }
    }
    }


    1. 调用绘制代码


    在这里插入图片描述
    运行效果:
    在这里插入图片描述
    4. 修改一下卡片的默认数字
    在这里插入图片描述


    在这里插入图片描述


    随机生成一个卡片,2或者4




    1. 先把Card中 num 默认改成0

    2. 因为2跟4出现的比例是1:4,所以采用随机出1-5的数字,当是1的时候就表示,当得到2、3、4、5的时候就表示要出现数字2.

    3. 随机获取i,j 就可以得到卡片的位置,割接i,j取到card实例,如果卡片没有数字,就表示可以,否则就递归继续取,取到为止。

    4. 把刚才取到的数字,设置到card实例对象中就好了。



    代码如下:


    //随机创建一个卡片
    G2048.prototype.createRandomNumber=function(){
    var num = 0;
    var index = _.getRandom(1,6);//这样取出来的就是1-5 之间的随机数
    //因为2和4出现的概率是1比4,所以如果index是1,则创建数字4,否则创建数字2(1被随机出来的概率就是1/5,而其他就是4/5 就是1:4的关系)
    console.log('index==='+index)
    if(index==1){
    num = 4;
    }else {
    num = 2;
    }
    //判断如果格子已经满了,则不再获取,退出
    if(this.cardFull()){
    return ;
    }
    //获取随机卡片,不为空的
    var card = this.getRandomCard();
    //给card对象设置数字
    if(card!=null){
    card.num=num;
    }
    }
    //获取随机卡片,不为空的
    G2048.prototype.getRandomCard=function(){
    var i = _.getRandom(0,4);
    var j = _.getRandom(0,4);
    var card = this.cards[i][j];
    if(card.num==0){//如果是空白的卡片,则找到了,直接返回
    return card;
    }
    //没找到空白的,就递归,继续寻找
    return this.getRandomCard();
    }
    //判断格子满了
    G2048.prototype.cardFull=function() {
    var card;
    for (var i = 0; i < 4; i++) {
    for (var j = 0; j < 4; j++) {
    card = this.cards[i][j];
    if(card.num==0){//有一个为空,则没满
    return false;
    }
    }
    }
    return true;
    }

    draw方法中调用,表示打开游戏默认一个数字
    在这里插入图片描述
    运行效果:
    在这里插入图片描述


    加入键盘事件


    同样要在draw方法中调用哦


    	//按键的控制
    G2048.prototype.control=function(){
    var that=this;
    global.addEventListener('keydown',function(e){
    console.log(that.flag)
    if(that.flag!='start') return ;
    var dir;
    switch (e.keyCode){
    case 87://w
    case 38://上
    dir=1;//上移动
    break;
    case 68://d
    case 39://右
    dir=2;//右移动
    break;
    case 83://s
    case 40://下
    dir=3;//下移动
    break;
    case 65://a
    case 37://左
    dir=4;//左移动
    break;
    }
    //卡片移动的方法
    that.moveCard(dir);
    });
    }


    1. 加入移动逻辑处理代码


    //卡片移动的方法
    G2048.prototype.moveCard=function(dir) {
    //将卡片清理一遍,因为每轮移动会设定合并标记,需重置
    this.clearCard();

    if(dir==1){//向上移动
    this.moveCardTop(true);
    }else if(dir==2){//向右移动
    this.moveCardRight(true);
    }else if(dir==3){//向下移动
    this.moveCardBottom(true);
    }else if(dir==4){//向左移动
    this.moveCardLeft(true);
    }
    //移动后要创建新的卡片
    this.createRandomNumber();
    //重绘
    this.render();
    //判断游戏是否结束
    this.gameOverOrNot();
    }

    //将卡片清理一遍,因为每轮移动会设定合并标记,需重置
    G2048.prototype.clearCard=function() {
    var card;
    for (var i = 0; i < 4; i++) {//i从1开始,因为i=0不需要移动
    for (var j = 0; j < 4; j++) {
    card = this.cards[i][j];
    card.merge=false;
    }
    }
    }


    1. 加入上下左右处理逻辑


    //向上移动
    G2048.prototype.moveCardTop=function(bool) {
    var res = false;
    var card;
    for (var i = 1; i < 4; i++) {//i从1开始,因为i=0不需要移动
    for (var j = 0; j < 4; j++) {
    card = this.cards[i][j];
    if(card.num!=0){//只要卡片不为空,要移动
    if(card.moveTop(this.cards,bool)){//向上移动
    res = true;//有一个为移动或者合并了,则res为true
    }
    }
    }
    }
    return res;
    }
    //向右移动
    G2048.prototype.moveCardRight=function(bool) {
    var res = false;
    var card;
    for (var i = 0; i < 4; i++) {
    for (var j = 3; j >=0 ; j--) {//j从COLS-1开始,从最右边开始移动递减
    card = this.cards[i][j];
    if(card.num!=0){//只要卡片不为空,要移动
    if(card.moveRight(this.cards,bool)){//向右移动
    res = true;//有一个为移动或者合并了,则res为true
    }
    }
    }
    }
    return res;
    }

    //向下移动
    G2048.prototype.moveCardBottom=function(bool) {
    var res = false;
    var card;
    for (var i = 3; i >=0; i--) {//i从ROWS-1开始,往下递减移动
    for (var j = 0; j < 4; j++) {
    card = this.cards[i][j];
    if(card.num!=0){//只要卡片不为空,要移动
    if(card.moveBottom(this.cards,bool)){//下移动
    res = true;//有一个为移动或者合并了,则res为true
    }
    }
    }
    }
    return res;
    }

    //向左移动
    G2048.prototype.moveCardLeft=function(bool) {
    var res = false;
    var card;
    for (var i = 0; i < 4; i++) {
    for (var j = 1; j < 4 ; j++) {//j从1开始,从最左边开始移动
    card = this.cards[i][j];
    if(card.num!=0){//只要卡片不为空,要移动
    if(card.moveLeft(this.cards,bool)){//向左移动
    res = true;//有一个为移动或者合并了,则res为true
    }
    }
    }
    }
    return res;
    }


    1. 在Card中加入向上移动的处理逻辑




    1. 从第2行开始移动,因为第一行不需要移动。

    2. 只要卡片的数字不是0,就表示要移动。

    3. 根据 i-1 可以获取到上一个卡片,如果上一个卡片是空,则把当前卡片交换上去,并且递归,因为可能要继续往上移动。

    4. 如果当前卡片与上一个卡片是相同数字的,则要合并。

    5. 以上两种都不是,则不做操作。



    //卡片向上移动
    Card.prototype.moveTop=function(cards,bool) {
    var i=this.i;
    var j=this.j;
    //设定退出条件
    if(i==0){//已经是最上面了
    return false;
    }
    //上面一个卡片
    var prev = cards[i-1][j];
    if(prev.num==0){//上一个卡片是空
    //移动,本质就是设置数字
    if(bool){//bool为true才执行,因为flase只是用来判断能否移动
    prev.num=this.num;
    this.num=0;
    //递归操作(注意这里是要 prev 来 move了)
    prev.moveTop(cards,bool);
    }
    return true;
    }else if(prev.num==this.num && !prev.merge){//合并操作(如果已经合并了,则不运行再次合并,针对当然轮)
    if(bool){////bool为true才执行
    prev.merge=true;
    prev.num=this.num*2;
    this.num=0;
    }
    return true;
    }else {//上一个的num与当前num不同,无法移动,并退出
    return false;
    }
    }

    在这里插入图片描述



    1. 在Card中加入其他3个方向的代码


    //向下移动
    Card.prototype.moveBottom=function(cards,bool) {
    var i=this.i;
    var j=this.j;
    //设定退出条件
    if(i==3){//已经是最下面了
    return false;
    }
    //上面一个卡片
    var prev = cards[i+1][j];
    if(prev.num==0){//上一个卡片是空
    //移动,本质就是设置数字
    if(bool){//bool为true才执行,因为flase只是用来判断能否移动
    prev.num=this.num;
    this.num=0;
    //递归操作(注意这里是要 prev 来 move了)
    prev.moveBottom(cards,bool);
    }
    return true;
    }else if(prev.num==this.num && !prev.merge){//合并操作(如果已经合并了,则不运行再次合并,针对当然轮)
    if(bool){////bool为true才执行
    prev.merge=true;
    prev.num=this.num*2;
    this.num=0;
    }
    return true;
    }else {//上一个的num与当前num不同,无法移动,并退出
    return false;
    }


    }
    //向右移动
    Card.prototype.moveRight=function(cards,bool) {
    var i=this.i;
    var j=this.j;
    //设定退出条件
    if(j==3){//已经是最右边了
    return false;
    }
    //上面一个卡片
    var prev = cards[i][j+1];
    if(prev.num==0){//上一个卡片是空
    //移动,本质就是设置数字
    if(bool){//bool为true才执行,因为flase只是用来判断能否移动
    prev.num=this.num;
    this.num=0;
    //递归操作(注意这里是要 prev 来 move了)
    prev.moveRight(cards,bool);
    }
    return true;
    }else if(prev.num==this.num && !prev.merge){//合并操作(如果已经合并了,则不运行再次合并,针对当然轮)
    if(bool){////bool为true才执行
    prev.merge=true;
    prev.num=this.num*2;
    this.num=0;
    }
    return true;
    }else {//上一个的num与当前num不同,无法移动,并退出
    return false;
    }
    }
    //向左移动
    Card.prototype.moveLeft=function(cards,bool) {
    var i=this.i;
    var j=this.j;
    //设定退出条件
    if(j==0){//已经是最左边了
    return false;
    }
    //上面一个卡片
    var prev = cards[i][j-1];
    if(prev.num==0){//上一个卡片是空
    //移动,本质就是设置数字
    if(bool){//bool为true才执行,因为flase只是用来判断能否移动
    prev.num=this.num;
    this.num=0;
    //递归操作(注意这里是要 prev 来 move了)
    prev.moveLeft(cards,bool);
    }
    return true;
    }else if(prev.num==this.num && !prev.merge){//合并操作(如果已经合并了,则不运行再次合并,针对当然轮)
    if(bool){////bool为true才执行
    prev.merge=true;
    prev.num=this.num*2;
    this.num=0;
    }
    return true;
    }else {//上一个的num与当前num不同,无法移动,并退出
    return false;
    }
    }

    运行效果:
    在这里插入图片描述


    做到这里就基本完成了,加入其他一下辅助的东西就行了,比如重新开始、游戏胜利,游戏结束等,也就不多说了。


    收起阅读 »

    js 实现以鼠标位置为中心滚轮缩放图片

    前言 不知道各位前端小伙伴蓝湖使用的多不多,反正我是经常在用,ui将原型图设计好后上传至蓝湖,前端开发人人员就可以开始静态页面的的编写了。对于页面细节看的不是很清楚可以使用滚轮缩放后再拖拽查看,还是很方便的。于是就花了点时间研究了一下。今天分享给大家。 实现 ...
    继续阅读 »

    前言


    不知道各位前端小伙伴蓝湖使用的多不多,反正我是经常在用,ui将原型图设计好后上传至蓝湖,前端开发人人员就可以开始静态页面的的编写了。对于页面细节看的不是很清楚可以使用滚轮缩放后再拖拽查看,还是很方便的。于是就花了点时间研究了一下。今天分享给大家。


    实现


    HTML


    <div class="container">
    <img id="image" alt="">
    </div>
    <div class="log"></div>

    js


    设置图片宽高且居中展示


    // 获取dom
    const container = document.querySelector('.container');
    const image = document.getElementById('image');
    const log = document.querySelector('.log');
    // 全局变量
    let result,
    x,
    y,
    scale = 1,
    isPointerdown = false, // 按下标识
    point = { x: 0, y: 0 }, // 第一个点坐标
    diff = { x: 0, y: 0 }, // 相对于上一次pointermove移动差值
    lastPointermove = { x: 0, y: 0 }; // 用于计算diff
    // 图片加载完成后再绑定事件
    image.addEventListener('load', function () {
    result = getImgSize(image.naturalWidth, image.naturalHeight, window.innerWidth, window.innerHeight);
    image.style.width = result.width + 'px';
    image.style.height = result.height + 'px';
    x = (window.innerWidth - result.width) * 0.5;
    y = (window.innerHeight - result.height) * 0.5;
    image.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(1)';
    // 拖拽查看
    drag();
    // 滚轮缩放
    wheelZoom();
    });
    image.src = '../images/liya.jpg';
    /**
    * 获取图片缩放尺寸
    * @param {number} naturalWidth
    * @param {number} naturalHeight
    * @param {number} maxWidth
    * @param {number} maxHeight
    * @returns
    */
    function getImgSize(naturalWidth, naturalHeight, maxWidth, maxHeight) {
    const imgRatio = naturalWidth / naturalHeight;
    const maxRatio = maxWidth / maxHeight;
    let width, height;
    // 如果图片实际宽高比例 >= 显示宽高比例
    if (imgRatio >= maxRatio) {
    if (naturalWidth > maxWidth) {
    width = maxWidth;
    height = maxWidth / naturalWidth * naturalHeight;
    } else {
    width = naturalWidth;
    height = naturalHeight;
    }
    } else {
    if (naturalHeight > maxHeight) {
    width = maxHeight / naturalHeight * naturalWidth;
    height = maxHeight;
    } else {
    width = naturalWidth;
    height = naturalHeight;
    }
    }
    return { width: width, height: height }
    }

    拖拽查看图片逻辑


    // 拖拽查看
    function drag() {
    // 绑定 pointerdown
    image.addEventListener('pointerdown', function (e) {
    isPointerdown = true;
    image.setPointerCapture(e.pointerId);
    point = { x: e.clientX, y: e.clientY };
    lastPointermove = { x: e.clientX, y: e.clientY };
    });
    // 绑定 pointermove
    image.addEventListener('pointermove', function (e) {
    if (isPointerdown) {
    const current1 = { x: e.clientX, y: e.clientY };
    diff.x = current1.x - lastPointermove.x;
    diff.y = current1.y - lastPointermove.y;
    lastPointermove = { x: current1.x, y: current1.y };
    x += diff.x;
    y += diff.y;
    image.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(' + scale + ')';
    log.innerHTML = `x = ${x.toFixed(0)}<br>y = ${y.toFixed(0)}<br>scale = ${scale.toFixed(5)}`;
    }
    e.preventDefault();
    });
    // 绑定 pointerup
    image.addEventListener('pointerup', function (e) {
    if (isPointerdown) {
    isPointerdown = false;
    }
    });
    // 绑定 pointercancel
    image.addEventListener('pointercancel', function (e) {
    if (isPointerdown) {
    isPointerdown = false;
    }
    });
    }

    滚轮缩放逻辑


    // 滚轮缩放
    function wheelZoom() {
    container.addEventListener('wheel', function (e) {
    let ratio = 1.1;
    // 缩小
    if (e.deltaY > 0) {
    ratio = 0.9;
    }
    // 目标元素是img说明鼠标在img上,以鼠标位置为缩放中心,否则默认以图片中心点为缩放中心
    if (e.target.tagName === 'IMG') {
    const origin = {
    x: (ratio - 1) * result.width * 0.5,
    y: (ratio - 1) * result.height * 0.5
    };
    // 计算偏移量
    x -= (ratio - 1) * (e.clientX - x) - origin.x;
    y -= (ratio - 1) * (e.clientY - y) - origin.y;
    }
    scale *= ratio;
    image.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(' + scale + ')';
    log.innerHTML = `x = ${x.toFixed(0)}<br>y = ${y.toFixed(0)}<br>scale = ${scale.toFixed(5)}`;
    e.preventDefault();
    });
    }

    Demo:jsdemo.codeman.top/html/wheelZ…



    链接:https://juejin.cn/post/7009892447211749406

    收起阅读 »

    深入浅出虚拟 DOM 和 Diff 算法,及 Vue2 与 Vue3 中的区别

    vue
    因为 Diff 算法,计算的就是虚拟 DOM 的差异,所以先铺垫一点点虚拟 DOM,了解一下其结构,再来一层层揭开 Diff 算法的面纱,深入浅出,助你彻底弄懂 Diff 算法原理 认识虚拟 DOM 虚拟 DOM 简单说就是 用JS对象来模拟 DOM 结构 那...
    继续阅读 »

    因为 Diff 算法,计算的就是虚拟 DOM 的差异,所以先铺垫一点点虚拟 DOM,了解一下其结构,再来一层层揭开 Diff 算法的面纱,深入浅出,助你彻底弄懂 Diff 算法原理


    认识虚拟 DOM


    虚拟 DOM 简单说就是 用JS对象来模拟 DOM 结构


    那它是怎么用 JS 对象模拟 DOM 结构的呢?看个例子


    <template>
    <div id="app" class="container">
    <h1>沐华</h1>
    </div>
    </template>

    上面的模板转在虚拟 DOM 就是下面这样的


    {
    'div',
    props:{ id:'app', class:'container' },
    children: [
    { tag: 'h1', children:'沐华' }
    ]
    }

    这样的 DOM 结构就称之为 虚拟 DOM (Virtual Node),简称 vnode


    它的表达方式就是把每一个标签都转为一个对象,这个对象可以有三个属性:tagpropschildren



    • tag:必选。就是标签。也可以是组件,或者函数

    • props:非必选。就是这个标签上的属性和方法

    • children:非必选。就是这个标签的内容或者子节点,如果是文本节点就是字符串,如果有子节点就是数组。换句话说 如果判断 children 是字符串的话,就表示一定是文本节点,这个节点肯定没有子元素


    为什么要使用虚拟 DOM 呢? 看个图


    image.png


    如图可以看出原生 DOM 有非常多的属性和事件,就算是创建一个空div也要付出不小的代价。而使用虚拟 DOM 来提升性能的点在于 DOM 发生变化的时候,通过 diff 算法和数据改变前的 DOM 对比,计算出需要更改的 DOM,然后只对变化的 DOM 进行操作,而不是更新整个视图


    在 Vue 中是怎么把 DOM 转成上面这样的虚拟 DOM 的呢,有兴趣的可以关注我另一篇文章详细了解一下 Vue 中的模板编译过程和原理


    在 Vue 里虚拟 DOM 的数据更新机制采用的是异步更新队列,就是把变更后的数据变装入一个数据更新的异步队列,就是 patch,用它来做新老 vnode 对比


    认识 Diff 算法


    Diff 算法,在 Vue 里面就是叫做 patch ,它的核心就是参考 Snabbdom,通过新旧虚拟 DOM 对比(即 patch 过程),找出最小变化的地方转为进行 DOM 操作



    扩展

    在 Vue1 里是没有 patch 的,每个依赖都有单独的 Watcher 负责更新,当项目规模变大的时候性能就跟不上了,所以在 Vue2 里为了提升性能,改为每个组件只有一个 Watcher,那我们需要更新的时候,怎么才能精确找到组件里发生变化的位置呢?所以 patch 它来了



    那么它是在什么时候执行的呢?


    在页面首次渲染的时候会调用一次 patch 并创建新的 vnode,不会进行更深层次的比较


    然后是在组件中数据发生变化时,会触发 setter 然后通过 Notify 通知 Watcher,对应的 Watcher 会通知更新并执行更新函数,它会执行 render 函数获取新的虚拟 DOM,然后执行 patch 对比上次渲染结果的老的虚拟 DOM,并计算出最小的变化,然后再去根据这个最小的变化去更新真实的 DOM,也就是视图


    那么它是怎么计算的? 先看个图


    diff.jpg


    比如有上图这样的 DOM 结构,是怎么计算出变化?简单说就是



    • 遍历老的虚拟 DOM

    • 遍历新的虚拟 DOM

    • 然后根据变化,比如上面的改变和新增,再重新排序


    可是这样会有很大问题,假如有1000个节点,就需要计算 1000³ 次,也就是10亿次,这样是无法让人接受的,所以 Vue 或者 React 里使用 Diff 算法的时候都遵循深度优先,同层比较的策略做了一些优化,来计算出最小变化


    Diff 算法的优化


    1. 只比较同一层级,不跨级比较


    如图,Diff 过程只会把同颜色框起来的同一层级的 DOM 进行比较,这样来简化比较次数,这是第一个方面


    diff1.jpg


    2. 比较标签名


    如果同一层级的比较标签名不同,就直接移除老的虚拟 DOM 对应的节点,不继续按这个树状结构做深度比较,这是简化比较次数的第二个方面


    diff2.jpg


    3. 比较 key


    如果标签名相同,key 也相同,就会认为是相同节点,也不继续按这个树状结构做深度比较,比如我们写 v-for 的时候会比较 key,不写 key 就会报错,这也就是因为 Diff 算法需要比较 key


    面试中有一道特别常见的题,就是让你说一下 key 的作用,实际上考查的就是大家对虚拟 DOM 和 patch 细节的掌握程度,能够反应出我们面试者的理解层次,所以这里扩展一下 key


    key 的作用


    比如有一个列表,我们需要在中间插入一个元素,会发生什么变化呢?先看个图


    diff3.jpg


    如图的 li1li2 不会重新渲染,这个没有争议的。而 li3、li4、li5 都会重新渲染


    因为在不使用 key 或者列表的 index 作为 key 的时候,每个元素对应的位置关系都是 index,上图中的结果直接导致我们插入的元素到后面的全部元素,对应的位置关系都发生了变更,所以全部都会执行更新操作,这可不是我们想要的,我们希望的是渲染添加的那一个元素,其他四个元素不做任何变更,也就不要重新渲染


    而在使用唯一 key 的情况下,每个元素对应的位置关系就是 key,来看一下使用唯一 key 值的情况下


    diff4.jpg


    这样如图中的 li3li4 就不会重新渲染,因为元素内容没发生改变,对应的位置关系也没有发生改变。


    这也是为什么 v-for 必须要写 key,而且不建议开发中使用数组的 index 作为 key 的原因


    总结一下:



    • key 的作用主要是为了更高效的更新虚拟 DOM,因为它可以非常精确的找到相同节点,因此 patch 过程会非常高效

    • Vue 在 patch 过程中会判断两个节点是不是相同节点时,key 是一个必要条件。比如渲染列表时,如果不写 key,Vue 在比较的时候,就可能会导致频繁更新元素,使整个 patch 过程比较低效,影响性能

    • 应该避免使用数组下标作为 key,因为 key 值不是唯一的话可能会导致上面图中表示的 bug,使 Vue 无法区分它他,还有比如在使用相同标签元素过渡切换的时候,就会导致只替换其内部属性而不会触发过渡效果

    • 从源码里可以知道,Vue 判断两个节点是否相同时主要判断两者的元素类型和 key 等,如果不设置 key,就可能永远认为这两个是相同节点,只能去做更新操作,就造成大量不必要的 DOM 更新操作,明显是不可取的


    有兴趣的可以去看一下源码:src\core\vdom\patch.js -35行 sameVnode(),下面也有详细介绍


    Diff 算法核心原理——源码


    上面说了Diff 算法,在 Vue 里面就是 patch,铺垫了这么多,下面进入源码里看一下这个神乎其神的 patch 干了啥?


    patch


    其实 patch 就是一个函数,我们先介绍一下源码里的核心流程,再来看一下 patch 的源码,源码里每一行也有注释


    它可以接收四个参数,主要还是前两个



    • oldVnode:老的虚拟 DOM 节点

    • vnode:新的虚拟 DOM 节点

    • hydrating:是不是要和真实 DOM 混合,服务端渲染的话会用到,这里不过多说明

    • removeOnly:transition-group 会用到,这里不过多说明


    主要流程是这样的:



    • vnode 不存在,oldVnode 存在,就删掉 oldVnode

    • vnode 存在,oldVnode 不存在,就创建 vnode

    • 两个都存在的话,通过 sameVnode 函数(后面有详解)对比是不是同一节点

      • 如果是同一节点的话,通过 patchVnode 进行后续对比节点文本变化或子节点变化

      • 如果不是同一节点,就把 vnode 挂载到 oldVnode 的父元素下

        • 如果组件的根节点被替换,就遍历更新父节点,然后删掉旧的节点

        • 如果是服务端渲染就用 hydrating 把 oldVnode 和真实 DOM 混合






    下面看完整的 patch 函数源码,说明我都写在注释里了


    源码地址:src\core\vdom\patch.js -700行


    // 两个判断函数
    function isUndef (v: any): boolean %checks {
    return v === undefined || v === null
    }
    function isDef (v: any): boolean %checks {
    return v !== undefined && v !== null
    }
    return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 如果新的 vnode 不存在,但是 oldVnode 存在
    if (isUndef(vnode)) {
    // 如果 oldVnode 存在,调用 oldVnode 的组件卸载钩子 destroy
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    // 如果 oldVnode 不存在的话,新的 vnode 是肯定存在的,比如首次渲染的时候
    if (isUndef(oldVnode)) {
    isInitialPatch = true
    // 就创建新的 vnode
    createElm(vnode, insertedVnodeQueue)
    } else {
    // 剩下的都是新的 vnode 和 oldVnode 都存在的话

    // 是不是元素节点
    const isRealElement = isDef(oldVnode.nodeType)
    // 是元素节点 && 通过 sameVnode 对比是不是同一个节点 (函数后面有详解)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
    // 如果是 就用 patchVnode 进行后续对比 (函数后面有详解)
    patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
    // 如果不是同一元素节点的话
    if (isRealElement) {
    // const SSR_ATTR = 'data-server-rendered'
    // 如果是元素节点 并且有 'data-server-rendered' 这个属性
    if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
    // 就是服务端渲染的,删掉这个属性
    oldVnode.removeAttribute(SSR_ATTR)
    hydrating = true
    }
    // 这个判断里是服务端渲染的处理逻辑,就是混合
    if (isTrue(hydrating)) {
    if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
    invokeInsertHook(vnode, insertedVnodeQueue, true)
    return oldVnode
    } else if (process.env.NODE_ENV !== 'production') {
    warn('这是一段很长的警告信息')
    }
    }
    // function emptyNodeAt (elm) {
    // return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
    // }
    // 如果不是服务端渲染的,或者混合失败,就创建一个空的注释节点替换 oldVnode
    oldVnode = emptyNodeAt(oldVnode)
    }

    // 拿到 oldVnode 的父节点
    const oldElm = oldVnode.elm
    const parentElm = nodeOps.parentNode(oldElm)

    // 根据新的 vnode 创建一个 DOM 节点,挂载到父节点上
    createElm(
    vnode,
    insertedVnodeQueue,
    oldElm._leaveCb ? null : parentElm,
    nodeOps.nextSibling(oldElm)
    )

    // 如果新的 vnode 的根节点存在,就是说根节点被修改了,就需要遍历更新父节点
    if (isDef(vnode.parent)) {
    let ancestor = vnode.parent
    const patchable = isPatchable(vnode)
    // 递归更新父节点下的元素
    while (ancestor) {
    // 卸载老根节点下的全部组件
    for (let i = 0; i < cbs.destroy.length; ++i) {
    cbs.destroy[i](ancestor)
    }
    // 替换现有元素
    ancestor.elm = vnode.elm
    if (patchable) {
    for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, ancestor)
    }
    const insert = ancestor.data.hook.insert
    if (insert.merged) {
    for (let i = 1; i < insert.fns.length; i++) {
    insert.fns[i]()
    }
    }
    } else {
    registerRef(ancestor)
    }
    // 更新父节点
    ancestor = ancestor.parent
    }
    }
    // 如果旧节点还存在,就删掉旧节点
    if (isDef(parentElm)) {
    removeVnodes([oldVnode], 0, 0)
    } else if (isDef(oldVnode.tag)) {
    // 否则直接卸载 oldVnode
    invokeDestroyHook(oldVnode)
    }
    }
    }
    // 返回更新后的节点
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
    }

    sameVnode


    这个是用来判断是不是同一节点的函数


    这个函数不长,直接看源码吧


    源码地址:src\core\vdom\patch.js -35行


    function sameVnode (a, b) {
    return (
    a.key === b.key && // key 是不是一样
    a.asyncFactory === b.asyncFactory && ( // 是不是异步组件
    (
    a.tag === b.tag && // 标签是不是一样
    a.isComment === b.isComment && // 是不是注释节点
    isDef(a.data) === isDef(b.data) && // 内容数据是不是一样
    sameInputType(a, b) // 判断 input 的 type 是不是一样
    ) || (
    isTrue(a.isAsyncPlaceholder) && // 判断区分异步组件的占位符否存在
    isUndef(b.asyncFactory.error)
    )
    )
    )
    }

    patchVnode


    源码地址:src\core\vdom\patch.js -501行


    这个是在新的 vnode 和 oldVnode 是同一节点的情况下,才会执行的函数,主要是对比节点文本变化或子节点变化


    还是先介绍一下主要流程,再看源码吧,流程是这样的:



    • 如果 oldVnode 和 vnode 的引用地址是一样的,就表示节点没有变化,直接返回

    • 如果 oldVnode 的 isAsyncPlaceholder 存在,就跳过异步组件的检查,直接返回

    • 如果 oldVnode 和 vnode 都是静态节点,并且有一样的 key,并且 vnode 是克隆节点或者 v-once 指令控制的节点时,把 oldVnode.elm 和 oldVnode.child 都复制到 vnode 上,然后返回

    • 如果 vnode 不是文本节点也不是注释的情况下

      • 如果 vnode 和 oldVnode 都有子节点,而且子节点不一样的话,就调用 updateChildren 更新子节点

      • 如果只有 vnode 有子节点,就调用 addVnodes 创建子节点

      • 如果只有 oldVnode 有子节点,就调用 removeVnodes 删除该子节点

      • 如果 vnode 文本为 undefined,就删掉 vnode.elm 文本



    • 如果 vnode 是文本节点但是和 oldVnode 文本内容不一样,就更新文本


      function patchVnode (
    oldVnode, // 老的虚拟 DOM 节点
    vnode, // 新的虚拟 DOM 节点
    insertedVnodeQueue, // 插入节点的队列
    ownerArray, // 节点数组
    index, // 当前节点的下标
    removeOnly // 只有在
    ) {
    // 新老节点引用地址是一样的,直接返回
    // 比如 props 没有改变的时候,子组件就不做渲染,直接复用
    if (oldVnode === vnode) return

    // 新的 vnode 真实的 DOM 元素
    if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm
    // 如果当前节点是注释或 v-if 的,或者是异步函数,就跳过检查异步组件
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
    hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
    vnode.isAsyncPlaceholder = true
    }
    return
    }
    // 当前节点是静态节点的时候,key 也一样,或者有 v-once 的时候,就直接赋值返回
    if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
    }
    // hook 相关的不用管
    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
    }
    // 获取子元素列表
    const oldCh = oldVnode.children
    const ch = vnode.children

    if (isDef(data) && isPatchable(vnode)) {
    // 遍历调用 update 更新 oldVnode 所有属性,比如 class,style,attrs,domProps,events...
    // 这里的 update 钩子函数是 vnode 本身的钩子函数
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    // 这里的 update 钩子函数是我们传过来的函数
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 如果新节点不是文本节点,也就是说有子节点
    if (isUndef(vnode.text)) {
    // 如果新老节点都有子节点
    if (isDef(oldCh) && isDef(ch)) {
    // 如果新老节点的子节点不一样,就执行 updateChildren 函数,对比子节点
    if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
    // 如果新节点有子节点的话,就是说老节点没有子节点

    // 如果老节点文本节点,就是说没有子节点,就清空
    if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
    // 添加子节点
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
    // 如果新节点没有子节点,老节点有子节点,就删除
    removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
    // 如果老节点是文本节点,就清空
    nodeOps.setTextContent(elm, '')
    }
    } else if (oldVnode.text !== vnode.text) {
    // 新老节点都是文本节点,且文本不一样,就更新文本
    nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
    // 执行 postpatch 钩子
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
    }

    updateChildren


    源码地址:src\core\vdom\patch.js -404行


    这个是新的 vnode 和 oldVnode 都有子节点,且子节点不一样的时候进行对比子节点的函数


    这里很关键,很关键!


    比如现在有两个子节点列表对比,对比主要流程如下


    循环遍历两个列表,循环停止条件是:其中一个列表的开始指针 startIdx 和 结束指针 endIdx 重合


    循环内容是:{



    • 新的头和老的头对比

    • 新的尾和老的尾对比

    • 新的头和老的尾对比

    • 新的尾和老的头对比。 这四种对比如图


    diff2.gif


    以上四种只要有一种判断相等,就调用 patchVnode 对比节点文本变化或子节点变化,然后移动对比的下标,继续下一轮循环对比


    如果以上四种情况都没有命中,就不断拿新的开始节点的 key 去老的 children 里找



    • 如果没找到,就创建一个新的节点

    • 如果找到了,再对比标签是不是同一个节点

      • 如果是同一个节点,就调用 patchVnode 进行后续对比,然后把这个节点插入到老的开始前面,并且移动新的开始下标,继续下一轮循环对比

      • 如果不是相同节点,就创建一个新的节点




    }



    • 如果老的 vnode 先遍历完,就添加新的 vnode 没有遍历的节点

    • 如果新的 vnode 先遍历完,就删除老的 vnode 没有遍历的节点


    为什么会有头对尾,尾对头的操作?


    因为可以快速检测出 reverse 操作,加快 Diff 效率


    function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0 // 老 vnode 遍历的下标
    let newStartIdx = 0 // 新 vnode 遍历的下标
    let oldEndIdx = oldCh.length - 1 // 老 vnode 列表长度
    let oldStartVnode = oldCh[0] // 老 vnode 列表第一个子元素
    let oldEndVnode = oldCh[oldEndIdx] // 老 vnode 列表最后一个子元素
    let newEndIdx = newCh.length - 1 // 新 vnode 列表长度
    let newStartVnode = newCh[0] // 新 vnode 列表第一个子元素
    let newEndVnode = newCh[newEndIdx] // 新 vnode 列表最后一个子元素
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    const canMove = !removeOnly

    // 循环,规则是开始指针向右移动,结束指针向左移动移动
    // 当开始和结束的指针重合的时候就结束循环
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
    oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
    oldEndVnode = oldCh[--oldEndIdx]

    // 老开始和新开始对比
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
    // 是同一节点 递归调用 继续对比这两个节点的内容和子节点
    patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
    // 然后把指针后移一位,从前往后依次对比
    // 比如第一次对比两个列表的[0],然后比[1]...,后面同理
    oldStartVnode = oldCh[++oldStartIdx]
    newStartVnode = newCh[++newStartIdx]

    // 老结束和新结束对比
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
    // 然后把指针前移一位,从后往前比
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]

    // 老开始和新结束对比
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
    patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
    canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
    // 老的列表从前往后取值,新的列表从后往前取值,然后对比
    oldStartVnode = oldCh[++oldStartIdx]
    newEndVnode = newCh[--newEndIdx]

    // 老结束和新开始对比
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
    patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
    canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
    // 老的列表从后往前取值,新的列表从前往后取值,然后对比
    oldEndVnode = oldCh[--oldEndIdx]
    newStartVnode = newCh[++newStartIdx]

    // 以上四种情况都没有命中的情况
    } else {
    if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
    // 拿到新开始的 key,在老的 children 里去找有没有某个节点有这个 key
    idxInOld = isDef(newStartVnode.key)
    ? oldKeyToIdx[newStartVnode.key]
    : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

    // 新的 children 里有,可是没有在老的 children 里找到对应的元素
    if (isUndef(idxInOld)) {
    /// 就创建新的元素
    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
    } else {
    // 在老的 children 里找到了对应的元素
    vnodeToMove = oldCh[idxInOld]
    // 判断标签如果是一样的
    if (sameVnode(vnodeToMove, newStartVnode)) {
    // 就把两个相同的节点做一个更新
    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
    oldCh[idxInOld] = undefined
    canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
    } else {
    // 如果标签是不一样的,就创建新的元素
    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
    }
    }
    newStartVnode = newCh[++newStartIdx]
    }
    }
    // oldStartIdx > oldEndIdx 说明老的 vnode 先遍历完
    if (oldStartIdx > oldEndIdx) {
    // 就添加从 newStartIdx 到 newEndIdx 之间的节点
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)

    // 否则就说明新的 vnode 先遍历完
    } else if (newStartIdx > newEndIdx) {
    // 就删除掉老的 vnode 里没有遍历的节点
    removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
    }

    至此,整个 Diff 流程的核心逻辑源码到这就结束了,再来看一下 Vue 3 里做了哪些改变吧


    Vue3 的优化


    本文源码版本是 Vue2 的,在 Vue3 里整个重写了 Diff 算法这一块东西,所以源码的话可以说基本是完全不一样的,但是要做的事还是一样的


    关于 Vue3 的 Diff 完整源码解析还在撰稿中,过几天就发布了,这里先介绍一下相比 Vue2 优化的部分,尤大公布的数据就是 update 性能提升了 1.3~2 倍ssr 性能提升了 2~3 倍,来看看都有哪些优化



    • 事件缓存:将事件缓存,可以理解为变成静态的了

    • 添加静态标记:Vue2 是全量 Diff,Vue3 是静态标记 + 非全量 Diff

    • 静态提升:创建静态节点时保存,后续直接复用

    • 使用最长递增子序列优化了对比流程:Vue2 里在 updateChildren() 函数里对比变更,在 Vue3 里这一块的逻辑主要在 patchKeyedChildren() 函数里,具体看下面


    事件缓存


    比如这样一个有点击事件的按钮


    <button @click="handleClick">按钮</button>

    来看下在 Vue3 被编译后的结果


    export function render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("button", {
    onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
    }, "按钮"))
    }

    注意看,onClick 会先读取缓存,如果缓存没有的话,就把传入的事件存到缓存里,都可以理解为变成静态节点了,优秀吧,而在 Vue2 中就没有缓存,就是动态的


    静态标记


    看一下静态标记是啥?


    源码地址:packages/shared/src/patchFlags.ts


    export const enum PatchFlags {
    TEXT = 1 , // 动态文本节点
    CLASS = 1 << 1, // 2 动态class
    STYLE = 1 << 2, // 4 动态style
    PROPS = 1 << 3, // 8 除去class/style以外的动态属性
    FULL_PROPS = 1 << 4, // 16 有动态key属性的节点,当key改变时,需进行完整的diff比较
    HYDRATE_EVENTS = 1 << 5, // 32 有监听事件的节点
    STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的fragment (一个组件内多个根元素就会用fragment包裹)
    KEYED_FRAGMENT = 1 << 7, // 128 带有key属性的fragment或部分子节点有key
    UNKEYEN_FRAGMENT = 1 << 8, // 256 子节点没有key的fragment
    NEED_PATCH = 1 << 9, // 512 一个节点只会进行非props比较
    DYNAMIC_SLOTS = 1 << 10, // 1024 动态slot
    HOISTED = -1, // 静态节点
    BAIL = -2 // 表示 Diff 过程中不需要优化
    }

    先了解一下静态标记有什么用?看个图


    在什么地方用到的呢?比如下面这样的代码


    <div id="app">
    <div>沐华</div>
    <p>{{ age }}</p>
    </div>

    在 Vue2 中编译的结果是,有兴趣的可以自行安装 vue-template-compiler 自行测试


    with(this){
    return _c(
    'div',
    {attrs:{"id":"app"}},
    [
    _c('div',[_v("沐华")]),
    _c('p',[_v(_s(age))])
    ]
    )
    }

    在 Vue3 中编译的结果是这样的,有兴趣的可以点击这里自行测试


    const _hoisted_1 = { id: "app" }
    const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, "沐华", -1 /* HOISTED */)

    export function render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _hoisted_2,
    _createElementVNode("p", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
    ]))
    }

    看到上面编译结果中的 -11 了吗,这就是静态标记,这是在 Vue2 中没有的,patch 过程中就会判断这个标记来 Diff 优化流程,跳过一些静态节点对比


    静态提升


    其实还是拿上面 Vue2 和 Vue3 静态标记的例子,在 Vue2 里每当触发更新的时候,不管元素是否参与更新,每次都会全部重新创建,就是下面这一堆


    with(this){
    return _c(
    'div',
    {attrs:{"id":"app"}},
    [
    _c('div',[_v("沐华")]),
    _c('p',[_v(_s(age))])
    ]
    )
    }

    而在 Vue3 中会把这个不参与更新的元素保存起来,只创建一次,之后在每次渲染的时候不停地复用,比如上面例子中的这个,静态的创建一次保存起来


    const _hoisted_1 = { id: "app" }
    const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, "沐华", -1 /* HOISTED */)

    然后每次更新 age 的时候,就只创建这个动态的内容,复用上面保存的静态内容


    export function render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _hoisted_2,
    _createElementVNode("p", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
    ]))
    }

    patchKeyedChildren


    在 Vue2 里 updateChildren 会进行



    • 头和头比

    • 尾和尾比

    • 头和尾比

    • 尾和头比

    • 都没有命中的对比


    在 Vue3 里 patchKeyedChildren



    • 头和头比

    • 尾和尾比

    • 基于最长递增子序列进行移动/添加/删除


    看个例子,比如



    • 老的 children:[ a, b, c, d, e, f, g ]

    • 新的 children:[ a, b, f, c, d, e, h, g ]



    1. 先进行头和头比,发现不同就结束循环,得到 [ a, b ]

    2. 再进行尾和尾比,发现不同就结束循环,得到 [ g ]

    3. 再保存没有比较过的节点 [ f, c, d, e, h ],并通过 newIndexToOldIndexMap 拿到在数组里对应的下标,生成数组 [ 5, 2, 3, 4, -1 ]-1 是老数组里没有的就说明是新增

    4. 然后再拿取出数组里的最长递增子序列,也就是 [ 2, 3, 4 ] 对应的节点 [ c, d, e ]

    5. 然后只需要把其他剩余的节点,基于 [ c, d, e ] 的位置进行移动/新增/删除就可以了

    作者:沐华
    链接:https://juejin.cn/post/7010594233253888013

    收起阅读 »