GVKun编程网logo

【JS】183-『表单开发』一次即通关的 5 个技巧

12

对于【JS】183-『表单开发』一次即通关的5个技巧感兴趣的读者,本文将会是一篇不错的选择,并为您提供关于【JS】1-原型继承的原理解析、【JS】100-浅拷贝与深拷贝、【JS】1004-几张动图教你

对于【JS】183-『表单开发』一次即通关的 5 个技巧感兴趣的读者,本文将会是一篇不错的选择,并为您提供关于【JS】1-原型继承的原理解析、【JS】100-浅拷贝与深拷贝、【JS】1004- 几张动图教你学会 EventLoop、【JS】1027- 几个优雅的 JavaScript 运算符使用技巧的有用信息。

本文目录一览:

【JS】183-『表单开发』一次即通关的 5 个技巧

【JS】183-『表单开发』一次即通关的 5 个技巧

本文由 IMWeb 社区 imweb.io 授权转载自腾讯内部 KM 论坛,原作者:easonruan。点击阅读原文查看 IMWeb 社区更多精彩文章。

笔者目前正在开发一个涉及较多表单的场景的新项目。但由于是新项目进度赶,产品人员紧缺,表单需求往往没有考虑得很周全。

那作为一名前端开发,如何辅助产品尽可能让表单需求一次即通关,减少反复沟通以及提缺陷修缺陷的时间,从而加快项目进度?

以下是笔者在项目中在表单开发方面的一些总结:

以下演示案例为 vue 项目,组件库为 element-ui

1. 重视通用型表单验证

业务场景:

表单中如果涉及手机号码,因为手机号码是特殊场景,我们很容易想到特殊的校验规则 —— 手机号的正则校验。

然而对于一个通用型字段,如标题 title 、描述 desc 等基本的字段,它们实在太普通太一般,导致我们放松了警惕。

导致问题:开发与测试反复在 tapd提缺陷修缺陷,在一堆小问题上浪费了大量时间,工作效率低。

解决方法:

  • 避免用户的输入前后有空格,即 trim

  • 限制最大输入长度,即 max-length

  • 不能包含特殊字符,即 emoji 表情是否能输入等

  
  
  
  1. // form rules

  2. export default {

  3.  title: [

  4.    /**

  5.    * Tips 避免用户的输入前后有空格

  6.    * 可以使用 v-model.trim 指令自动清除用户前后空格,

  7.    * 技术手段能解决的,我们避免提示用户

  8.    **/

  9.    { max: 50, message: ''不能超过50个字'', trigger: ''blur'' },

  10.    {

  11.        pattern: /^[ -~ 一-龥 ＀-。  -   -〗]+$/,

  12.        // - 基本拉丁字母  https://unicode-table.com/cn/blocks/basic-latin/

  13.        // 一-龥 中文 https://unicode-table.com/cn/blocks/cjk-unified-ideographs/

  14.        // ＀-。 半角及全角形式字符 https://unicode-table.com/cn/blocks/halfwidth-and-fullwidth-forms/

  15.        //  -  英文标点 https://unicode-table.com/cn/blocks/general-punctuation/

  16.        //  -〗 中文标点 https://unicode-table.com/cn/blocks/cjk-symbols-and-punctuation/

  17.        message: "不能包含特殊字符",

  18.        trigger: "blur"

  19.    }

  20.  ]

  21. }

2. 避免重复提交

业务场景:当用户快速点击提交按钮,导致问题:页面会重复发请求给后端。

解决方法虽然很简单,但这却是开发最容易忽略的,也是 tapd 上最经常见的缺陷问题。

解决方法一:在业务代码执行完之前不能再次触发

  
  
  
  1. export default {

  2.  methods: {

  3.     onSubmit () {

  4.         // 可以与Loading搭配使用

  5.        if (this.isCommitting) return;

  6.        this.isCommitting = true;


  7.        // 表单验证以及业务请求代码


  8.        this.isCommitting = false;

  9.    },

  10.  }

  11. }

解决方法二:经过评论的提醒,遗漏了利用 debounce防抖实现防二次点击操作。

  
  
  
  1. export default {

  2.    mounted() {

  3.        // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。

  4.        this.debouncedSaveForm = _.debounce(

  5.            this.onSubmit, // 回调函数

  6.            500, // 时间窗口的间隔

  7.            {

  8.                leading: true, // 点击立即执行

  9.                trailing: false // 延迟时间过后不执行

  10.            }

  11.        );

  12.    },

  13.    methods: {

  14.        onSubmit () {

  15.            if (this.isCommitting) return;

  16.            this.isCommitting = true;


  17.            // 表单验证以及业务请求代码


  18.            this.isCommitting = false;

  19.        },

  20.    }

  21. }

3. 表单提交或出错时的 Loading 提示

业务场景:表单提交后没展示 Loading 导致问题:当请求 request 较久时,页面像是卡死了,没任何响应,用户体验很差。

业务场景:遇到错误时没隐藏 Loading 导致问题:当请求 request 出错时,Loading 没关闭,页面流程进行不下去。

解决方案:

  
  
  
  1. export default {

  2.  methods: {

  3.     onSubmit () {


  4.        // 容易忽略一:发送请求前,没展示Loading

  5.        this.$loading.show(''努力加载中...'');

  6.        request(''apiUrl'', data)

  7.          .then(() => {})

  8.          .catch(err => {

  9.            // 容易忽略二:请求出错时,没隐藏Loading

  10.            this.$loading.hide();

  11.          })


  12.    },

  13.  }

  14. }

4. 表单重新打开时,要重置表单数据

业务场景:如果表单是属于弹窗 Dialog 内,部分开发为了代码可复用性,新增和编辑是共用同一个表单代码。

导致问题:用户在编辑某一条数据后,再点击新增,会发现新增表单里面的内容是上一条编辑内容的数据。

解决方案:

  
  
  
  1. export default {

  2.    mounted() {

  3.        // 页面初始化时,先备份表单数据

  4.        this._bak_form = _.cloneDeep(this.form);

  5.    },

  6.    methods:  {

  7.        onOpenDialog(actionType, tableRowIndex, data) {

  8.            if (actionType === "add") {

  9.                // 新增时,需要恢复为默认数据

  10.                this.form = _.cloneDeep(this._bak_form);

  11.                /**

  12.                * Tips

  13.                * 这里不能用解构 this.form = {...this._bak_form},

  14.                * 不然会导致,改了form里面的(object或array类型)数据,同时会影响到_bak_form的数据

  15.                * 这是因为引用数据类型的指针还是指向同一个地址。

  16.                **/

  17.            } else if (actionType === "edit") {

  18.                // 编辑

  19.                this.tableRowIndex = tableRowIndex;

  20.                this.form = _.cloneDeep(data);

  21.            }

  22.            this.actionType = actionType;

  23.            this.visible = true;

  24.        },

  25.    }

  26. }

Tips 避免在关闭窗口时恢复为默认数据

  • 造成问题:恢复为默认数据会触发表单校验规则,因此会有显眼的警告 “XX 不能为空”。

  • 触发原因:与此同时,窗口的 visible 变为 false,假若窗口的隐藏式有过渡效果的话,窗口隐藏需要 500ms,而重置表单是立即生效的,用户是会看到一闪而过的红色警告。

  • 解决方法: 一是避免在关闭窗口时恢复为默认数据 二是使用 resetFields将所有字段值重置为初始值并移除校验结果(但不能解决点编辑后再点新增时,恢复为默认数据)

5. 不小心点击关闭页面时,要提示让用户确认

业务场景:当用户在填写一个长表单时,手误点了关闭页面或者点击去到其他页面。

导致问题:用户花时间填写的表单数据会丢失,用户又要重新填一遍。用户体验大大降低。

解决方法:

  
  
  
  1. export default {

  2.    watch: {

  3.        "visible": value => {

  4.            if (!value) {

  5.                // 当弹窗关闭不涉及表单时,清除事件

  6.                window.onbeforeunload = null;

  7.                return;

  8.            }

  9.            // 当弹窗显示有表单数据时,网页跳转或者关闭时提醒用户

  10.            window.onbeforeunload = e => (e.returnValue = "确定离开当前页面?");

  11.        }

  12.    },

  13. }

总结

最后,我汇总一下上面 5 个技巧点的真实场景 Demo:

https://codepen.io/ryqsky

以上都不是什么新内容,但如果工作中能重视并注意到这些细节问题,就能一次即通关表单开发需求,减少与产品测试反复沟通的时间,加快项目的进度

不然等到产品或测试临下班前发现这些问题时,会出现这样的场景:开发被拖着对着屏幕敲代码修缺陷,测试重新打开缺陷,反复循环 N 次。

N = 上面提到的 5 个技巧点

中华人民共和国消防法第二条是 “消防工作贯彻预防为主,防消结合的方针”,放在需求开发上也是一样道理:** 提前发现提前修复才能避免当代码发生 “火灾” 时的一团乱。

关注我们

IMWeb 团队隶属腾讯公司,是国内最专业的前端团队之一。

我们专注前端领域多年,负责过 QQ 资料、QQ 注册、QQ 群等亿级业务。目前聚焦于在线教育领域,精心打磨 腾讯课堂 及 企鹅辅导 两大产品。

社区官网

http://imweb.io/

加入我们

https://hr.tencent.com/position_detail.php?id=45616

每一个 “在看”,都是对我最大的肯定!

本文分享自微信公众号 - 前端自习课(FE-study)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与 “OSC 源创计划”,欢迎正在阅读的你也加入,一起分享。

【JS】1-原型继承的原理解析

【JS】1-原型继承的原理解析

在JavaScript当中,对象A如果要继承对象B的属性和方法,那么只要将对象B放到对象A的原型链上即可。而某个对象的原型链,就是由该对象开始,通过__proto__属性连接起来的一串对象。__proto__属性是JavaScript对象中的内部属性,任何JavaScript对象,包括我们自己构建的对象,JavaScript的built-in对象,任何函数(在JavaScript当中,函数也是对象)都具有这个属性。如下图就是一个原型链的例子:

上图中,A,B,C分别代表3个对象,蓝色箭头串接起来的所有对象就构成了对象C的原型链,其中C的_proto__属性指向B,B的__proto__属性指向A,A的__proto__属性可能指向更高层的对象,也可能指向null(表示A不继承任何对象的属性和方法)。

如果我们引用了C的某个属性或者方法,那么JavaScript就会顺着C的原型链进行查找,即首先查找对象C本身,看所引用的属性名或者方法名是否存在,如果存在就停止查找直接返回,如果不存在,就通过C的__proto__属性找到原型链中的B对象,继续在B对象中查找,如果B对象中找到所引用的属性名或者方法名,那么就停止查找直接返回,如果B对象中也不存在,就通过对象B的__proto__属性找到原型链中的A对象,继续重复上述查找过程,直到找到所引用的属性或者方法为止(同时也可能查找完对象C的整个原型链也没有找到所引用的属性或者方法,那么该属性或者方法就是undefined的)。

因此,只要能够成功的为某一个对象构造出我们需要的原型链,那么就能让该对象继承我们想要它继承的方法或者属性。而想要成功构造对象的原型链,就还必须理解prototype属性,JavaScript当中已经存在的原型链,以及当我们创建对象时,原型链被构造的过程。

prototype属性

prototype属性存在于JavaScript的任何函数当中,这个属性指向的对象就是所谓的原型对象,在构造原型链时需要原型对象。

JavaScript当中已经存在的原型链

在JavaScript当中存在ObjectFunctionArrayStringBooleanNumberDateErrorRegExp这9个built-in函数,一个built-inMath对象,通过这上述9个built-in函数我们可以创建相应的对象,同时,这9个built-in函数prototype属性所指向的原型对象也是built-in的。下面的图示解释了这几个函数以及各自prototype属性所指向的原型对象之间的关系

(如果此图看不清,可打开 http://p3nqtyvgo.bkt.clouddn.com/20180927_02.png 下载 )

  上面的图示中,黄色方框代表built-in函数对象深绿色方框代表built-in函数prototype属性指向的原型对象,名字都叫xx prototype object浅绿色方框(即Math对象)代表普通对象,蓝色箭头连接非built-in函数对象(无论是普通对象如Math,还是原型对象)的__proto__属性,而土黄色箭头连接函数对象的__proto__属性。

  通过上图可以发现,所有built-in函数对象的原型链最终都指向Function prototype object,所有非函数对象的原型链最终都指向Object prototype object,并且Function prototype object__proto__属性也指向Object prototype objectObject prototype object__proto__属性指向为null

因此,Object prototype object是所有原型链的顶端,通过原型链查找规则可知,所有built-in函数对象同时继承了Object prototype objectFunction prototype object上的属性和方法,而所有非built-in函数对象只继承了Object prototype object上的方法。Function prototype object包含了所有函数共享的属性和方法,而Object prototype object包含了所有对象都共享额属性和方法。

对于上图中原型对象包含的constructor属性,下文当中有解释。

创建对象时,原型链的构造过程

在JavaScript当中创建对象有2中方式,一种是通过定义函数使用new方法来构造,另一种是使用对象字面量的方式,即:

var obj = {
   name: "Jim Green"
};

使用这两种方式创建对象时,对象的原型链构造过程有所不同。

  • 1. 使用函数的方式构造对象

使用函数的方式构造对象分为两步:首先需要定义一个函数作为构造函数,然后使用new方法构造对象。接下来就来看一下这两个步骤会发生什么。

假设我们定义了一个函数名为F,此时JavaScript会为我们做两件事,第一:根据我们定义的函数创建一个函数对象,第二,设置这个函数的__proto__属性和prototype属性。其中__proto__属性指向built-inFunction prototype object,而prototype属性指向一个为函数对象F新创建的原型对象,这个新创建的原型对象通过调用new Object()构造出来,并且为这个新创建的对象添加constructor属性,该属性指向函数对象F。最后的结果如下图所示:

上图中为了方便,没有画出Function prototype objectObject prototype objectconstructor属性。而F prototype object__proto__属性为何指向Object prototype object,下文介绍new操作符时有解释。

当我们使用new方法调用F函数的时候,JavaScript也会为我们做两件事,第一,分配内存作为新创建的对象,第二,将新创建的对象的proto属性指向函数F的原型对象,结果如下图:


上图中,obj就是调用new方法通过函数F创建出来的对象,我们可以看到对象obj的原型链包含了函数F的原型对象,以及Object prototype object,这样,对象obj通过原型链查找规则,就能继承函数F的原型对象,以及Object prototype object上面定义的属性和方法了。并且如果我们想知道一个对象是由哪个方法构建的,只需要访问这个对象的constructor属性即可,上例中,只要我们访问obj.constructor,那么就知道obj是由函数F创建的。同时,由于F prototype object上文中介绍是由new Object函数创建的,根据此处介绍,F prototype object__proto__属性应该指向Object函数的原型对象,即Object prototype object

  • 2. 使用对象字面量定义对象

当使用对象字面量创建对象时,JavaScript会为我们做两件事:

1 分配内存作为新创建的对象。
2 将新创建对象的__proto__属性指向Object prototype object

结果如下图所示:


上图为了简化,同样没有画出Object prototype objectconstructor属性

继承

理解了上面所讲的原理之后,假设目前有一个对象A(这个对象可以是任意的,包括JavaScript built-in的对象,任何函数对象,任何原型对象,以及我们自己new出来的对象),现在想创建一个对象obj,让obj继承A的属性和方法。

通过上面的介绍,我们知道创建对象有两种方式,但是使用对象字面量创建的对象其原型链总是只包含两个对象,一个是其自己,一个是Object prototype object,根本不可能包含对象A,无法达到让对象obj继承对象A属性和方法的效果。因此,只能使用函数的方式创建对象,让对象A包含在新创建对象obj的原型链中即可。

根据上面的讲解,如果是用函数的方式创建对象,那么在调用new方法时,新创建对象的__proto__属性会指向函数的原型对象。因此,只要在调用函数之前,将函数的原型对象换成A,然后再调用new方法,就可以将对象A包含在新创建的对象obj的原型链中,这样通过原型链查找规则,obj就继承了A的属性和方法。假设用来创建对象obj的函数为B,则相关代码为:

B.prototype = A;
B.prototype.constructor = B;
var obj = new B(传入的参数)

上面代码中的B.prototype.constructor = B,是因为对象A中可能没有constructor属性,或者constructor属性不指向B,而为了方便通过访问任何由B函数创建的对象的constructor属性,就可以正确的知道该对象是使用函数B构造出来的。相关图示如下图:

上图中虚线框所包围的B prototype object就是定义函数B时,JavaScript为函数B生成的原型对象,但是该对象被我们用代码替换成了对象A。由于这个被替换的B prototype object没有其他地方再用到,因此会被回收掉。


本文分享自微信公众号 - 前端自习课(FE-study)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

【JS】100-浅拷贝与深拷贝

【JS】100-浅拷贝与深拷贝

作者:浪里行舟

https://github.com/ljianshu/Blog/issues/5


一、数据类型

数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和对象数据类型。

  • 基本数据类型的特点:直接存储在栈(stack)中的数据

  • 引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里

引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

二、浅拷贝与深拷贝

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的

深拷贝和浅拷贝的示意图大致如下:

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

三、赋值和浅拷贝的区别

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

我们先来看两个例子,对比赋值与浅拷贝会对原对象带来哪些改变?

  
  
  
  1. // 对象赋值

  2. var obj1 = {

  3.    ''name'' : ''zhangsan'',

  4.    ''age'' :  ''18'',

  5.    ''language'' : [1,[2,3],[4,5]],

  6. };

  7. var obj2 = obj1;

  8. obj2.name = "lisi";

  9. obj2.language[1] = ["二","三"];

  10. console.log(''obj1'',obj1)

  11. console.log(''obj2'',obj2)

  
  
  
  1. // 浅拷贝

  2. var obj1 = {

  3.    ''name'' : ''zhangsan'',

  4.    ''age'' :  ''18'',

  5.    ''language'' : [1,[2,3],[4,5]],

  6. };

  7. var obj3 = shallowCopy(obj1);

  8. obj3.name = "lisi";

  9. obj3.language[1] = ["二","三"];

  10. function shallowCopy(src) {

  11.    var dst = {};

  12.    for (var prop in src) {

  13.        if (src.hasOwnProperty(prop)) {

  14.            dst[prop] = src[prop];

  15.        }

  16.    }

  17.    return dst;

  18. }

  19. console.log(''obj1'',obj1)

  20. console.log(''obj3'',obj3)

上面例子中,obj1是原始数据,obj2是赋值操作得到,而obj3浅拷贝得到。我们可以很清晰看到对原始数据的影响,具体请看下表:

四、浅拷贝的实现方式

1、Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

  
  
  
  1. var obj = { a: {a: "kobe", b: 39} };

  2. var initalObj = Object.assign({}, obj);

  3. initalObj.a.a = "wade";

  4. console.log(obj.a.a); //wade

注意:当object只有一层的时候,是深拷贝

  
  
  
  1. let obj = {

  2.    username: ''kobe''

  3.    };

  4. let obj2 = Object.assign({},obj);

  5. obj2.username = ''wade'';

  6. console.log(obj);//{username: "kobe"}

2、Array.prototype.concat()
  
  
  
  1. let arr = [1, 3, {

  2.    username: ''kobe''

  3.    }];

  4. let arr2=arr.concat();    

  5. arr2[2].username = ''wade'';

  6. console.log(arr);

修改新对象会改到原对象:

3、Array.prototype.slice()
  
  
  
  1. let arr = [1, 3, {

  2.    username: '' kobe''

  3.    }];

  4. let arr3 = arr.slice();

  5. arr3[2].username = ''wade''

  6. console.log(arr);

同样修改新对象会改到原对象:

关于Array的slice和concat方法的补充说明:Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。

原数组的元素会按照下述规则拷贝:

  • 如果该元素是个对象引用(不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。

  • 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

可能这段话晦涩难懂,我们举个例子,将上面的例子小作修改:

  
  
  
  1. let arr = [1, 3, {

  2.    username: '' kobe''

  3.    }];

  4. let arr3 = arr.slice();

  5. arr3[1] = 2

  6. console.log(arr,arr3);

五、深拷贝的实现方式

1、JSON.parse(JSON.stringify())
  
  
  
  1. let arr = [1, 3, {

  2.    username: '' kobe''

  3. }];

  4. let arr4 = JSON.parse(JSON.stringify(arr));

  5. arr4[2].username = ''duncan'';

  6. console.log(arr, arr4)

原理: 用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

这种方法虽然可以实现数组或对象深拷贝,但不能处理函数。

  
  
  
  1. let arr = [1, 3, {

  2.    username: '' kobe''

  3. },function(){}];

  4. let arr4 = JSON.parse(JSON.stringify(arr));

  5. arr4[2].username = ''duncan'';

  6. console.log(arr, arr4)

这是因为 JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数。

2、手写递归方法

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。

  
  
  
  1.    //定义检测数据类型的功能函数

  2.    function checkedType(target) {

  3.      return Object.prototype.toString.call(target).slice(8, -1)

  4.    }

  5.    //实现深度克隆---对象/数组

  6.    function clone(target) {

  7.      //判断拷贝的数据类型

  8.      //初始化变量result 成为最终克隆的数据

  9.      let result, targetType = checkedType(target)

  10.      if (targetType === ''object'') {

  11.        result = {}

  12.      } else if (targetType === ''Array'') {

  13.        result = []

  14.      } else {

  15.        return target

  16.      }

  17.      //遍历目标数据

  18.      for (let i in target) {

  19.        //获取遍历数据结构的每一项值。

  20.        let value = target[i]

  21.        //判断目标结构里的每一值是否存在对象/数组

  22.        if (checkedType(value) === ''Object'' ||

  23.          checkedType(value) === ''Array'') { //对象/数组里嵌套了对象/数组

  24.          //继续遍历获取到value值

  25.          result[i] = clone(value)

  26.        } else { //获取到value值是基本的数据类型或者是函数。

  27.          result[i] = value;

  28.        }

  29.      }

  30.      return result

  31.    }

3、函数库lodash

该函数库也有提供 _.cloneDeep 用来做 Deep Copy。

  
  
  
  1. var _ = require(''lodash'');

  2. var obj1 = {

  3.    a: 1,

  4.    b: { f: { g: 1 } },

  5.    c: [1, 2, 3]

  6. };

  7. var obj2 = _.cloneDeep(obj1);

  8. console.log(obj1.b.f === obj2.b.f);

  9. // false

参考文章

  • 什么是js深拷贝和浅拷贝及其实现方式

  • JavaScript浅拷贝和深拷贝

  • js 深拷贝 vs 浅拷贝

  • 深拷贝的终极探索(99%的人都不知道)


本文分享自微信公众号 - 前端自习课(FE-study)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

【JS】1004- 几张动图教你学会 EventLoop

【JS】1004- 几张动图教你学会 EventLoop

欧怼怼
https://juejin.cn/post/6969028296893792286


最近在学习Vue源码,刚好学到虚拟DOM的异步更新,这里就涉及到JavaScript中的事件循环Event Loop。之前对这个概念还是比较模糊,大概知道是什么,但一直没有深入学习。刚好借此机会,回过头来学习一下Event Loop

JavaScript是单线程的语言

事件循环Event Loop,这是目前浏览器和NodeJS处理JavaScript代码的一种机制,而这种机制存在的背后,就有因为JavaScript是一门单线程的语言。

单线程和多线程最简单的区别就是:单线程同一个时间只能做一件事情,而多线程同一个时间能做多件事情。

JavaScript之所谓设计为单线程语言,主要是因为它作为浏览器脚本语言,主要的用途就是与用户互动,操作Dom节点。

而在这个情景设定下,假设JavaScript同时有两个进程,一个是操作A节点,一个是删除A节点,这时候浏览器就不知道要以哪个线程为准了。

因此为了避免这类型的问题,JavaScript从一开始就属于单线程语言。

调用栈 Call Stack

JavaScript运行的时候,主线程会形成一个栈,这个栈主要是解释器用来最终函数执行流的一种机制。通常这个栈被称为调用栈Call Stack,或者执行栈(Execution Context Stack)。

调用栈,顾名思义是具有LIFO(后进先出,Last in First Out)的结构。调用栈内存放的是代码执行期间的所有执行上下文。

  • 每调用一个函数,解释器就会把该函数的执行上下文添加到调用栈并开始执行;
  • 正在调用栈中执行的函数,如果还调用了其他函数,那么新函数也会被添加到调用栈,并立即执行;
  • 当前函数执行完毕后,解释器会将其执行上下文清除调用栈,继续执行剩余执行上下文中的剩余代码;
  • 但分配的调用栈空间被占满,会引发”堆栈溢出“的报错。

现在用个小案例来演示一下调用栈。

function a() {
    console.log(''a'');
}

function b() {
    console.log(''b'');
}

function c() {
    console.log(''c'');
    a();
    b();
}

c();

/**
* 输出结果:c a b
*/


执行这段代码的时候,首先调用的是函数c()。因此function c(){}的执行上下文就会被放入调用栈中。

call_stack_1.gif

然后开始执行函数c,执行的第一个语句是console.log(''c'')

因此解释器也会将其放入调用栈中。

call_stack_2.gif

console.log(''c'')方法执行完后,控制台打印了''c'',调用栈就会将其移除。

call_stack_3.gif

接着就是执行a()函数。

解释器就将function a() {}的执行上下文放入调用栈中。

call_stack_4.gif

紧接着就执行a()中的语句——console.log(''a'')

call_stack_5.gif

当函数a执行结束后,调用栈就将执行上下文移除。

然后接着执行c()函数剩下的语句,也就是执行b()函数,因此它的执行上下文就加入调用栈中。

call_stack_6.gif

紧接着就执行b()中的语句——console.log(''b'')

call_stack_7.gif

b()执行完后,调用栈就将其移出。

这时c()也执行结束了,调用栈也将其移出栈。

call_stack_8.gif

这时候,我们这段语句就执行结束了。

任务队列

上面的案例简单的介绍了关于JavaScript单线程的执行方式。

但这其中会存在一些问题,就是如果当一个语句也需要执行很长时间的话,比如请求数据、定时器、读取文件等等,后面的语句就得一直等着前面的语句执行结束后才会开始执行。

显而易见,这是不可取的。

同步任务和异步任务

因此,JavaScript将所有执行任务分为了同步任务和异步任务。

其实我们每个任务都是在做两件事情,就是发起调用得到结果

而同步任务和异步任务最主要的差别就是,同步任务发起调用后,很快就可以得到结果,而异步任务是无法立即得到结果,比如请求接口,每个接口都会有一定的响应时间,根据网速、服务器等等因素决定,再比如定时器,它需要固定时间后才会返回结果。

因此,对于同步任务和异步任务的执行机制也不同。

同步任务的执行,其实就是跟前面那个案例一样,按照代码顺序和调用顺序,支持进入调用栈中并执行,执行结束后就移除调用栈。

而异步任务的执行,首先它依旧会进入调用栈中,然后发起调用,然后解释器会将其响应回调任务放入一个任务队列,紧接着调用栈会将这个任务移除。当主线程清空后,即所有同步任务结束后,解释器会读取任务队列,并依次将已完成的异步任务加入调用栈中并执行。

这里有个重点,就是异步任务不是直接进入任务队列的。

这里举一个简单的例子。

console.log(1);

fetch(''https://jsonplaceholder.typicode.com/todos/1'')
    .then(response => response.json())
    .then(json => console.log(json))

console.log(2);

很显然,fetch()就是一个异步任务。

但执行到console.log(2)之前,其实fetch()已经被调用且发起请求了,但是还未响应数据。而响应数据和处理数据的函数then()此时已经在任务队列中,等候console.log(2)执行结束后,所以同步任务清空后,再进入调用栈执行响应动作。

async.png

宏任务和微任务

前面聊到同步任务和异步任务的时候,提及到了任务队列

在任务队列中,其实还分为宏任务队列(Task Queue)微任务队列(Microtask Queue),对应的里面存放的就是宏任务微任务

首先,宏任务和微任务都是异步任务。

而宏任务和微任务的区别,就是它们执行的顺序,这也是为什么要区分宏任务和微任务。

在同步任务中,任务的执行都是按照代码顺序执行的,而异步任务的执行也是需要按顺序的,队列的属性就是先进先出(FIFO,First in First Out),因此异步任务会按照进入队列的顺序依次执行。

但在一些场景下,如果只按照进入队列的顺序依次执行的话,也会出问题。比如队列先进入一个一小时的定时器,接着再进入一个请求接口函数,而如果根据进入队列的顺序执行的话,请求接口函数可能需要一个小时后才会响应数据。

因此浏览器就会将异步任务分为宏任务和微任务,然后按照事件循环的机制去执行,因此不同的任务会有不同的执行优先级,具体会在事件循环讲到。

任务入队

这里还有一个知识点,就是关于任务入队。

任务进入任务队列,其实会利用到浏览器的其他线程。虽然说JavaScript是单线程语言,但是浏览器不是单线程的。而不同的线程就会对不同的事件进行处理,当对应事件可以执行的时候,对应线程就会将其放入任务队列。

  • js引擎线程:用于解释执行js代码、用户输入、网络请求等;
  • GUI渲染线程:绘制用户界面,与JS主线程互斥(因为js可以操作DOM,进而会影响到GUI的渲染结果);
  • http异步网络请求线程:处理用户的get、post等请求,等返回结果后将回调函数推入到任务队列;
  • 定时触发器线程setIntervalsetTimeout等待时间结束后,会把执行函数推入任务队列中;
  • 浏览器事件处理线程:将 clickmouse等UI交互事件发生后,将要执行的回调函数放入到事件队列中。

这个其实就可以解释了下列代码为什么后面的定时器会比前面的定时器先执行。因为后者的定时器会先被推进宏任务队列,而前者会之后到点了再被推入宏任务队列。

setTimeout(() => {
   console.log(''a'');
}, 10000);

setTimeout(() => {
   console.log(''b'');
}, 10000);

宏任务


浏览器 Node
整体代码(script)
UI交互事件
I/O
setTimeout
setInterval
setImmediate
requestAnimationFrame

微任务


浏览器 Node
process.nextTick
MutationObserver
Promise.then catch finally

事件循环 Event Loop

其实宏任务队列和微任务队列的执行,就是事件循环的一部分了,所以放在这里一起说。

事件循环的具体流程如下:

  1. 从宏任务队列中,按照 入队顺序,找到第一个执行的宏任务,放入调用栈,开始执行;
  2. 执行完 该宏任务下所有同步任务后,即调用栈清空后,该宏任务被推出宏任务队列,然后微任务队列开始按照入队顺序,依次执行其中的微任务, 直至微任务队列清空为止
  3. 当微任务队列清空后,一个事件循环结束;
  4. 接着从宏任务队列中,找到下一个执行的宏任务,开始第二个事件循环,直至宏任务队列清空为止。

这里有几个重点:

  • 当我们第一次执行的时候,解释器会将整体代码 script放入宏任务队列中,因此事件循环是从第一个宏任务开始的;
  • 如果在执行微任务的过程中,产生新的微任务添加到微任务队列中,也需要一起清空;微任务队列没清空之前,是不会执行下一个宏任务的。

接下来,通过一个常见的面试题例子来模拟一下事件循环。

console.log("a");

setTimeout(function () {
    console.log("b");
}, 0);

new Promise((resolve) => {
    console.log("c");
    resolve();
})
    .then(function () {
        console.log("d");
    })
    .then(function () {
        console.log("e");
    });

console.log("f");

/**
* 输出结果:a c f d e b
*/


首先,当代码执行的时候,整体代码script被推入宏任务队列中,并开始执行该宏任务。

task_queque_1.gif

按照代码顺序,首先执行console.log("a")

该函数上下文被推入调用栈,执行完后,即移除调用栈。

task_queque_2.gif

接下来执行setTimeout(),该函数上下文也进入调用栈中。

task_queque_3.gif

因为setTimeout是一个宏任务,因此将其callback函数推入宏任务队列中,然后该函数就被移除调用栈,继续往下执行。

task_queque_4.gif

紧接着是Promise语句,先将其放入调用栈,然后接着往下执行。

task_queque_5.gif

执行console.log("c")resolve(),这里就不多说了。

task_queque_6.gif

接着来到new Promise().then()方法,这是一个微任务,因此将其推入微任务队列中。

task_queque_7.gif

这时new Promise语句已经执行结束了,就被移除调用栈。

接着做执行console.log(''f'')

task_queque_8.gif

这时候,script宏任务已经执行结束了,因此被推出宏任务队列。

紧接着开始清空微任务队列了。首先执行的是Promise then,因此它被推入调用栈中。

task_queque_9.gif

然后开始执行其中的console.log("d")

task_queque_10.gif

执行结束后,检测到后面还有一个then()函数,因此将其推入微任务队列中。

此时第一个then()函数已经执行结束了,就会移除调用栈和微任务队列。

task_queque_11.gif

此时微任务队列还没被清空,因此继续执行下一个微任务。

执行过程跟前面差不多,就不多说了。

task_queque_12.gif

此时微任务队列已经清空了,第一个事件循环已经结束了。

接下来执行下一个宏任务,即setTimeout callback

task_queque_13.gif

执行结束后,它也被移除宏任务队列和调用栈。

这时候微任务队列里面没有任务,因此第二个事件循环也结束了。

宏任务也被清空了,因此这段代码已经执行结束了。

task_queque_14.gif

await

ECMAScript2017中添加了async functionsawait

async关键字是将一个同步函数变成一个异步函数,并将返回值变为promise

await可以放在任何异步的、基于promise的函数之前。在执行过程中,它会暂停代码在该行上,直到promise完成,然后返回结果值。而在暂停的同时,其他正在等待执行的代码就有机会执行了。

下面通过一个例子来体验一下。

async function async1() {
    console.log("a");
    const res = await async2();
    console.log("b");
}

async function async2() {
    console.log("c");
    return 2;
}

console.log("d");

setTimeout(() => {
    console.log("e");
}, 0);

async1().then(res => {
    console.log("f")
})

new Promise((resolve) => {
    console.log("g");
    resolve();
}).then(() => {
    console.log("h");
});

console.log("i");

/**
* 输出结果:d a c g i b h f e
*/


首先,开始执行前,将整体代码script放入宏任务队列中,并开始执行。

第一个执行的是console.log("d")

async_await_1.gif

紧接着是setTimeout,将其回调放入宏任务中,然后继续执行。

async_await_2.gif

紧接着是调用async1()函数,因此将其函数上下文放置到调用栈。

async_await_3.gif

然后开始执行async1中的console.log("a")

async_await_4.gif

接下来就是await关键字语句。

await后面调用的是async2函数,因此我们将其放入调用栈。

async_await_5.gif

然后开始执行async2中的console.log("c"),并return一个值。

执行完成后,async2就被移出调用栈。

async_await_6.gif

这时候,await会阻塞async2的返回值,先跳出async1进行往下执行。

需要注意的是,现在async1中的res变量,还是undefined,没有赋值。

async_await_7.gif

紧接着是执行new Promise

async_await_8.gif

执行console.log("i")

async_await_9.gif

这时,async1外面的同步任务都执行完成了,因此就重新回到前面阻塞的位置,进行往下执行。

async_await_10.gif

这时res成功赋值了async2的结果值,然后往下执行console.log("b")

async_await_11.gif

这时候async1才算是执行结束,紧接着再将其调用的then()函数放入微任务队列中。

async_await_12.gif

这时script宏任务已经全部执行完了,开始准备清空微任务队列了。

第一个被执行的微任务队列是promise then,也就是将执行其中的console.log("h")语句。

async_await_13.gif

执行完Promise then微任务后,紧接着开始执行async1promise then微任务。

async_await_14.gif

这时候微任务队列已经清空了,即开始执行下一个宏任务。

async_await_15.gif

页面渲染

最后来讲将事件循环中的页面更新渲染,这也是Vue中异步更新的逻辑所在。

每次当一次事件循环结束后,即一个宏任务执行完成后以及微任务队列被清空后,浏览器就会进行一次页面更新渲染。

通常我们浏览器页面刷新频率是60fps,也就是意味着120ms要刷新一次,因此我们也要尽量保证一次事件循环控制在120ms之内,这也是我们需要做代码性能优化的一个原因。

接下来还是通过一个案例来看一下。

<!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>Event Loop</title>
</head>
<body>
    <div id="demo"></div>
    
    <script src="./src/render1.js"></script>
    <script src="./src/render2.js"></script>
</body>
</html>

// render1
const demoEl = document.getElementById(''demo'');

console.log(''a'');

setTimeout(() => {
    alert(''渲染完成!'')
    console.log(''b'');
},0)

new Promise(resolve => {
    console.log(''c'');
    resolve()
}).then(() => {
    console.log(''d'');
    alert(''开始渲染!'')
})

console.log(''e'');
demoEl.innerText = ''Hello World!'';

// render2
console.log(''f'');

demoEl.innerText = ''Hi World!'';
alert(''第二次渲染!'');

根据HTML的执行顺序,第一个被执行的JavaScript代码是render1.js,因此解释器将其推入宏任务队列,并开始执行。

render_1.gif

第一个被执行的是console.log("a")

render_2.gif

其次是setTimeout,并将其回调加入宏任务队列中。

render_3.gif

紧接着执行new Promise

render_4.gif

同样,将其then()推入微任务队列中去。

render_5.gif

紧接着执行console.log("e")

render_6.gif

最后,修改DOM节点的文本内容,但是这时候页面还不会更新渲染。

这时候script宏任务也执行结束了。

render_7.gif

紧接着,开始清空微任务队列,执行Promise then

render_8.gif

这时候,alert一个通知,而这个语句结束后,则微任务队列清空,代表第一个事件循环结束,即将要开始渲染页面了。

render_9.gif

当点击关闭alert后,事件循环结束,页面也开始渲染。

render_10.gif

渲染结束后,就开始执行下一个宏任务,即setTimeout callback

render_11.gif

紧接着执行console.log("b")

render_12.gif

这时候宏任务队列已清空了,但是html文件还没执行结束,因此进入render2.js继续执行。

render_13.gif

首先执行console.log(''f'')

render_14.gif

紧接着,再次修改节点的文本信息,此时依旧不会更新页面渲染。

接着执行alert语句,当关闭alert通知后,该宏任务结束,微任务队列也为空,因此该事件循环也结束了,这时候就开始第二次页面更新。

render_15.gif

但如果将所有JavaScript代码使用内嵌方式的话,浏览器会先把两个script丢到宏任务队列中去,因此执行的顺序也会不一样,这里就不一一推导了。

<!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>Event Loop</title>
</head>
<body>
    <div id="demo"></div>

    <script>
        const demoEl = document.getElementById(''demo'');

        console.log(''a'');

        setTimeout(() => {
            alert(''渲染完成!'')
            console.log(''b'');
        },0)

        new Promise(resolve => {
            console.log(''c'');
            resolve()
        }).then(() => {
            console.log(''d'');
            alert(''开始渲染!'')
        })

        console.log(''e'');
        demoEl.innerText = ''Hello World!'';
    </script>

    <script>
        console.log(''f'');

        demoEl.innerText = ''Hi World!'';
        alert(''第二次渲染!'');
    </script>

</body>
</html>

输出:a c e d "开始渲染!" f "第二次渲染!" "渲染完成!" b



1. JavaScript 重温系列(22篇全)
2. ECMAScript 重温系列(10篇全)
3. JavaScript设计模式 重温系列(9篇全)
4.  正则 / 框架 / 算法等 重温系列(16篇全)
5.  Webpack4 入门(上) ||  Webpack4 入门(下)
6.  MobX 入门(上)  ||   MobX 入门(下)
7. 120 +篇原创系列汇总

回复“加群”与大佬们一起交流学习~

点击“阅读原文”查看 120+ 篇原创文章

本文分享自微信公众号 - 前端自习课(FE-study)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

【JS】1027- 几个优雅的 JavaScript 运算符使用技巧

【JS】1027- 几个优雅的 JavaScript 运算符使用技巧

ECMAScript发展进程中,会有很多功能的更新,比如销毁,箭头功能,模块,它们极大的改变JavaScript编写方式,可能有些人喜欢,有些人不喜欢,但像每个新功能一样,我们最终会习惯它们。新版本的ECMAScript引入了三个新的逻辑赋值运算符:空运算符,AND和OR运算符,这些运算符的出现,也是希望让我们的代码更干净简洁,下面分享几个优雅的JavaScript运算符使用技巧

一、可选链接运算符【?.】

可选链接运算符(Optional Chaining Operator) 处于ES2020提案的第4阶段,因此应将其添加到规范中。它改变了访问对象内部属性的方式,尤其是深层嵌套的属性。它也可以作为TypeScript 3.7+中的功能使用。

相信大部分开发前端的的小伙伴们都会遇到null和未定义的属性。JS语言的动态特性使其无法不碰到它们。特别是在处理嵌套对象时,以下代码很常见:

if (data && data.children && data.children[0] && data.children[0].title) {
    // I have a title!

上面的代码用于API响应,我必须解析JSON以确保名称存在。但是,当对象具有可选属性或某些配置对象具有某些值的动态映射时,可能会遇到类似情况,需要检查很多边界条件。

这时候,如果我们使用可选链接运算符,一切就变得更加轻松了。它为我们检查嵌套属性,而不必显式搜索梯形图。我们所要做的就是使用“?” 要检查空值的属性之后的运算符。我们可以随意在表达式中多次使用该运算符,并且如果未定义任何项,它将尽早返回。

对于静态属性用法是:

object?.property

对于动态属性将其更改为:

object?.[expression] 

上面的代码可以简化为:

let title = data?.children?.[0]?.title;

然后,如果我们有:

 let data;
console.log(data?.children?.[0]?.title) // undefined

data  = {children: [{title:''codercao''}]}
console.log(data?.children?.[0]?.title) // codercao

这样写是不是更加简单了呢?由于操作符一旦为空值就会终止,因此也可以使用它来有条件地调用方法或应用条件逻辑

 const conditionalProperty = null;
let index = 0;

console.log(conditionalProperty?.[index++]); // undefined
console.log(index);  // 0

对于方法的调用你可以这样写

object.runsOnlyIfMethodExists?.()

例如下面的parent对象,如果我们直接调用parent.getTitle(),则会报Uncaught TypeError: parent.getTitle is not a function错误,parent.getTitle?.()则会终止不会执行

let parent = {
    name"parent",
    friends: ["p1""p2""p3"],
    getNamefunction() {
      console.log(this.name)
    }
  };
  
  parent.getName?.()   // parent
  parent.getTitle?.()  //不会执行 

与无效合并一起使用

提供了一种方法来处理未定义或为空值和表达提供默认值。我们可以使用??运算符,为表达式提供默认值

console.log(undefined ?? ''codercao''); // codercao

因此,如果属性不存在,则可以将无效的合并运算符与可选链接运算符结合使用以提供默认值。

let title = data?.children?.[0]?.title ?? ''codercao'';
console.log(title); // codercao

二、逻辑空分配(?? =)

expr1 ??= expr2

逻辑空值运算符仅在空值( null 或者 undefined)时才将值分配给expr1,表达方式:

x ??= y

可能看起来等效于:

x = x ?? y;

但事实并非如此!有细微的差别。

空的合并运算符(??)从左到右操作,如果x不为空,则短路。因此,如果x不为 null 或者 undefined,则永远不会对表达式y进行求值。因此,如果y是一个函数,它将根本不会被调用。因此,此逻辑赋值运算符等效于

x ?? (x = y);

三、逻辑或分配(|| =)

此逻辑赋值运算符仅在左侧表达式为 falsy值时才赋值。Falsy值与null有所不同,因为falsy值可以是任何一种值:undefined,null,空字符串(双引号""、单引号’’、反引号``),NaN,0。IE浏览器中的 document.all,也算是一个。

语法

x ||= y

等同于

x || (x = y)

在我们想要保留现有值(如果不存在)的情况下,这很有用,否则我们想为其分配默认值。例如,如果搜索请求中没有数据,我们希望将元素的内部HTML设置为默认值。否则,我们要显示现有列表。这样,我们避免了不必要的更新和任何副作用,例如解析,重新渲染,失去焦点等。我们可以简单地使用此运算符来使用JavaScript更新HTML:

document.getElementById(''search'').innerHTML ||= ''<i>No posts found matching this search.</i>''

四、逻辑与分配(&& =)

可能你已经猜到了,此逻辑赋值运算符仅在左侧为真时才赋值。因此:

x &&= y

等同于

x && (x = y)
最后

本次分享几个优雅的JavaScript运算符使用技巧,重点分享了可选链接运算符的使用,这样可以让我们不需要再编写大量我们例子中代码即可轻松访问嵌套属性。但是IE不支持它,因此,如果需要支持该版本或更旧版本的浏览器,则可能需要添加Babel插件。对于Node.js,需要为此升级到Node 14 LTS版本,因为12.x不支持该版本。

作者:codercao

https://segmentfault.com/a/1190000039885243

1. JavaScript 重温系列(22篇全)
2. ECMAScript 重温系列(10篇全)
3. JavaScript设计模式 重温系列(9篇全)
4.  正则 / 框架 / 算法等 重温系列(16篇全)
5.  Webpack4 入门(上) ||  Webpack4 入门(下)
6.  MobX 入门(上)  ||   MobX 入门(下)
7. 120 +篇原创系列汇总

回复“加群”与大佬们一起交流学习~

点击“阅读原文”查看 120+ 篇原创文章

本文分享自微信公众号 - 前端自习课(FE-study)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

关于【JS】183-『表单开发』一次即通关的 5 个技巧的介绍现已完结,谢谢您的耐心阅读,如果想了解更多关于【JS】1-原型继承的原理解析、【JS】100-浅拷贝与深拷贝、【JS】1004- 几张动图教你学会 EventLoop、【JS】1027- 几个优雅的 JavaScript 运算符使用技巧的相关知识,请在本站寻找。

本文标签: