-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 399 KB
/
content.json
1
{"pages":[{"title":"About","text":"这里是ZhangLK的个人网站,始于2019-05-27。 湿货多和啰里啰嗦是本站的两大特点。 很惭愧,只做了一点微小的工作,谢谢大家。","link":"/about/index.html"}],"posts":[{"title":"序","text":"这样的日子适合留下一些东西。 今天是小程序正式上线的日子,这是我从头到尾跟过的第一个项目,值得纪念。 本篇是继个人博客从学校服务器上迁出的开篇之作,也是经历WordPress之后使用的第二个博客框架——VuePress 用markdown来写作是一件奇妙的事情。 祝考研成功。 2019年5月28日 桂林","link":"/2019/05/28/article/Prologue/"},{"title":"前端菜鸟的面试记录","text":"春招转正失利,已提桶跑路,现将之前面过的公司进行记录。 愿互联网行业挺过寒冬,春暖花开,早日实现共同富裕! 共勉之 2021春招(前端岗位)大中厂 深信服(简历挂) CVTE视源 (二面挂) 涂鸦智能(笔试挂) 步步高(简历挂) 顺丰科技(简历挂) 珍爱网(一面挂) 锐捷网络(笔试挂) 富士康(offer,工资太低,宿舍太差) 小公司 睿联技术(笔试挂) 店匠(二面挂) 赢和信息(offer,转正挂,垃圾公司尽早倒闭) 外包 软通动力(offer,外包没敢去) 中软国际(推掉了) 腾讯外包(笔试挂) 中国电信海南分公司研发中心(offer,人力外包,没编制) 2021年10月社招(前端岗位) 云创捷为(小程序外包,二面挂) 云迈网络(直播行业,二面挂) 有好软件(餐饮SaaS,一面挂) 微克科技(智能可穿戴,一面挂) 财盈通科技(电商外包,一面挂) 酷宅科技(物联网解决方案,笔试挂) 乾坤物联科技(UWB解决方案,offer) 十方融海(在线教育,一面挂) 科比特航空(无人机,offer) 华为OD (不是普通外包,笔试挂) 字节外包(头条,一面挂) 2023年6月社招(前端岗位) 微购科技(微购相册,一面挂) 云瑞科技(青椒云) 未完待续 2022年1月1日 深圳","link":"/2022/01/01/article/interview/"},{"title":"简述「in桂工」小程序","text":"本篇是小程序1.X答辩的逐字稿。 大家好,我是来自XXXX的XXX,我们这次进行答辩的项目是“in桂工”小程序。下面开始我的答辩: 我们项目的目标是打造“一站式”校园服务平台 首先,让我们看看大家目前普遍使用的校园服务平台。 不仅有教务处的网站,还有各种App,甚至还有各个平台的公众号。当我们想完成一些简单的操作,比如查成绩时,我们常常会感到困惑。我们究竟该选择哪个平台? 那么说回到我们的项目“in桂工”,顾名思义,就是“在桂工”。那么,在桂工,我们可以学在桂工、住在桂工、玩在桂工。我们希望的是,能够打造一个一站式的平台。 那么,经过一段时间的努力,我们取得了一些微小进展,现在拿来给大家分享一下。 首先我们看到的这个主界面是我们的首页,从上而下分别是,可以滚动的banner,我们可以在这里展示相关的活动和推荐。下面是根据大家的点击量生成的快捷功能。在往下我们还可以看到我们自己一卡通的余额和最近的交易记录。右边还可以看到最近的考试时间。依次往下,我们可以看到我们今天的课表。 在屏幕的底部,我们可以看到四个底栏的选项。他们分别是:显示所有功能的页面、实时显示通知的信息流页面、以及个人的相关设置。 既然我们要做到一站式的服务,那么我们肯定要给大家提供相关的功能。以下是一些我们相关功能的展示。像大家日常都会使用到的课程表、成绩查询、考试地点查询、以及我们大家都很在意的体测查询。 那么,可能大家会有疑问,既然大家都有的功能,我们为什么要使用in桂工呢?我们能够有什么独到之处呢? 当我们仔细观察这个界面,当你点击“查看详情”,就可以看到这们课程的详细信息。仔细观察,你就可以发现我们十分贴心的提供了你在年级里的排名,还帮你算好了及格率,让你对自己的学习掌握的更加透彻。 甚至,我们还提供了这门课程的成绩分布,让你了如指掌。 当然,类似的细节还有很多,因为时间的限制我只能举例说明。总之,精心而美好,功能强大而又充满了人性化的瞬间,每一处都恰到好处。 所以,这是我们的功能部分,截止到目前为止,我们有将近30多种以上的功能,分别囊括了教务处、体育部、财务处、图书馆和学工处。 当然可能有些我在这里没有列出,我们以此希望能够正在构建真正一站式的平台。 当然,我们构建的是一个平台,那么我们肯定要提供相关的开放能力。 目前我们已经提供支持了第三方应用上线的功能,通过我们的后台可以进行互不干扰统一的管理,当然我们也自己研发了开发工具,上传和修改应用将更加方便快捷。 如果有对我们平台感兴趣的同学,我们在这个网址里提供了相关的文档,可以随时查阅。同时,我们也欢迎大家可以把自己做好的应用上传到我们的平台上,工作人员审核过后,就可以上架平台。 接下来是实机演示环节 这就是我们今天所带来的项目,那么这个版本在今天,就会正式的对外发布。 希望大家能够喜欢,也欢迎大家随时向我们提出意见,我们会及时回复,谢谢大家。再见 2019年7月5日 桂林","link":"/2019/07/05/article/wxapp-2019/"},{"title":"祝考研成功","text":"但行好事,莫问前程。 祝考研成功。 2020年12月25日 桂林","link":"/2020/12/25/essay/exam2021/"},{"title":"结束","text":"从暑假的艰难起步,到九月份的强化,再到十月份的闭关修炼,再到冬天的冲刺。 是的,就这样结束了。一切都是那么真实,历历在目。肖四依然是舍友之间的谈资,我的高数预测卷其实还没写完。 “如何理解考研结束不是终点,而是新生活,新奋斗的起点?” 都过去了。 昨天晚上从图书馆出来之前,特地环顾了一下一眼看不到头的六楼自习室。我知道,这可能是我最后一次再这样出现在这里了。所有人好像都知道自己要做什么,一切仿佛跟之前没用什么区别。不曾换过的闭馆音乐,不曾开过的空调。图书馆似乎在用它特别的温柔向我挥手。 或许明天就会有新的人坐我的位置,开始新的学习。 “幸福不会从天而降,梦想不会自动成真。” 有些东西只有经历过才能真正理解。 这一年终于要过去了,有失有得,但并不后悔。 最后,祝考研上岸。 2020年12月29日 桂林","link":"/2020/12/29/essay/exam2022/"},{"title":"浅谈JavaScript的 var, const, let","text":"基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值。引用数据类型的值是保存在内存中的对象,JS不允许直接访问内存中的位置,所以在操作的时候操作的是对象的引用;因此是引用数据类型是按照引用访问的。 基本数据类型和引用数据类型基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值。引用数据类型的值是保存在内存中的对象,JS不允许直接访问内存中的位置,所以在操作的时候操作的是对象的引用;因此是引用数据类型是按照引用访问的。 复制变量值复制基本类型的值 12var num1 = 5;var num2 = num1; num1和num2中的5是完全独立的,互不影响 复制引用类型 12345var obj1 = new Object();var obj2 = obj1;obj1.name = 'lucyStar';console.log(obj2.name);// lucyStar 我们可以看到,obj1保存了一个对象的实例,这个值被复制到 Obj2中。复制操作完成后,两个变量实际引用的是同一个对象,改变了其中一个,会影响另外一个值 传递参数参数传递就跟把函数外部的值复制给函数内部的参数; 基本类型传参 1234567891011function addTen(num) { num+=10; return num;}const count = 20;const result = addTen(count);console.log(count);// 20,没有变化console.log(result);// 30 引用类型传参 123456789function setName(obj) { obj.name = 'luckyStar'; obj = new Object(); obj.name = 'litterStar'}const person = new Object();setName(person);console.log(person.name);// luckyStar 在函数内部修改了参数的值,但是原始的引用仍然保持未变。实际上,在函数内部重写 obj时,这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕之后立即销毁。 变量提升(hoisting) 为了更好地解释声明提升,下面例子中使用 var 而不是使用 ES6新增的let和const(它们不存在声明提升) 下面的代码输出什么 1234a = 2;var a;console.log(a);// 2 可能有人会认为是 undefined, 因为 var a 声明在 a = 2之后,会被重新赋值为 undefined。但他实际上的输出结果是 2 下面的代码输出什么 12console.log(a);var a = 2; 可能有人会认为,由于变量 a 在使用前没有先进行声明,因此会抛出 ReferenceError异常。但实际它的输出是 undefined。 引擎会在解释JavaScript代码之前首先会对其进行编译。编译阶段中一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来。 所以正确的思考思路是:包含变量和函数在内的所有声明都会在任何代码被执行前首先被处理。 当你看到 var a = 2时,可能会被认为这是一个声明。但是 JavaScript实际上会将其看成两个声明:var a 和 a = 2; 第一个声明是在编译阶段进行的。第二个声明会被留在原地等待执行阶段。 所以第一个例子中的代码会以如下的形式进行处理 1234var a;a = 2;console.log(a); 其中第一部分是编译,第二部分是执行。 第二个例子会按照以下流程进行处理 1234var a;console.log(a);a = 2; 注意:只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。 函数声明和变量声明都会被提升,但是函数会首先被提升,然后才是变量 12345678910111213141516171819202122foo(); // 1var foo;function foo(){ console.log(1);}foo = function() { console.log(2);}// 上面代码会按照以下流程进行处理// 函数声明会提升到变量前面function foo(){ console.log(1);}var foo;foo(); // 1foo = function() { console.log(2);} 虽然重复的 var声明会被忽略掉,但是出现在后面的函数声明还是会覆盖之前的 123456789101112foo(); // 3function foo(){ console.log(1);}var foo = function() { console.log(2);}function foo() { console.log(3);} 思考一下下面的代码输出什么 123456789var name = 'Tom';(function() { if (typeof name == 'undefined') { var name = 'Jack'; console.log('Goodbye ' + name); } else { console.log('Hello ' + name); }})(); 答案是 Goodbye Jack。 改成下面这样应该会更容易理解一些 123456789101112// 去掉下面这行也是一样的,因为会优先访问函数作用域内部的变量// var name = 'Tom';(function() { var name; // 注意这行 if (typeof name == 'undefined') { var name = 'Jack'; console.log('Goodbye ' + name); } else { console.log('Hello ' + name); }})(); 立即执行函数的中的变量 name 的定义被提升到了顶部,并在初始化赋值之前是 undefined,所以 typeof name == 'undefined' var,let,const我们先来看看,var,let,const 声明变量的位置 可以看到 let和const声明的变量在块级作用域中,不存在变量提升。 1234567// var 的情况console.log(a); // 输出undefinedvar a = 2;// let 的情况console.log(b); // 报错ReferenceErrorlet b = 2; let 声明的变量可以被修改。 要注意暂时性死区(TDZ)总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ) 12345function foo(x = y, y = 2) { return [x, y];}foo(); // 报错 因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”。 const声明的变量是常量; const 实际保证的,并不是变量的值不变,而是变量指向的那个内存地址所保存的数据不得改动。 对于基本数据类型(数值。字符串。布尔值)。值就保存在变量指向的那个内存地址,因此等同于常量。但对于引用数据类型主要是对象和数组)。变量指向的内存地址,保存的只是一个指向实际数据的指针。 const 只能保证这个指针是固定的(即使总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,那就完全不能控制了。因此,将一个对象声明为常量必须非常小心。 1234567const foo = {};// 为 foo 添加一个属性,可以成功foo.prop = 123;foo.prop // 123// 将 foo 指向另一个对象,就会报错foo = {}; // TypeError: "foo" is read-only 参考 JavaScript高级程序设计(第三版) 你不知道的JavaScript(上) ECMAScript 6 入门 - let 和 const 命令","link":"/2022/06/01/frontEnd/JSvarletconst/"},{"title":"理解addLoadEvent函数","text":"阅读理解 在JS中 onload事件是 HTML DOM Event对象的一个属性,又叫事件句柄(Event Handlers),它会在页面或图像 加载完成后(注意是加载完成后)立即发生。 window.onload = func的作用就是在页面加载完成后将 func函数绑定到 onload事件上并执行。如果页面加载完成之后,只需要执行一个函数 func,那么只用 window.onload = func也就可以了,但是如果需要执行两个甚至多个函数呢? 直接调用两次onload不就行了: 12window.onload = firstfunction;window.onload = secondfunction; 这么做的话,只有 secondfunction会被绑定,因为前面的值被后面的值覆盖了。那么该怎么办? 将两个函数合并到一个函数当中不就行了,匿名函数发挥作用的时候到了: 1234window.onload = function() { fristfunction; secondfunction;} 不过,它也只能绑定两个函数。还好,大神们早已解决了这个问题。西蒙·威利森 (Simon Willison)——jQuery框架的开发者之一编写了下面的 addLoadEvent函数: 1234567891011function addLoadEvent(func) { var oldonload = window.onload;//将现有的事件处理函数的值存入变量中 if (typeof window.onload != 'function') { window.onload = func;//如果这个事件处理函数没有绑定任何函数,就把新函数添加给它 } else { window.onload = function() { oldonload(); func();//如果已经绑定了函数,就把新函数追加到现有指令的末尾 } }} 然后,不管页面加载完成后要执行多少个函数,只要调用这个函数就可以了: 1234addLoadEvent(firstfunction);addLoadEvent(secondfunction);addLoadEvent(thirdfunction);... 附:相关概念 支持onload事件的 HTML 标签有 <body>, <frame>, <frameset>, <iframe>, <img>, <link>, <script>支持该事件的 JavaScript 对象有 image(图像), layer, window(整个页面) 事件句柄(Event Handlers),可以在某个事件发生时通过一个事件句柄对某个元素进行操作。事件是可以被控件识别的操作,如按下确定按钮,选择某个单选按钮或者复选框。每一种控件有自己可以识别的事件,如窗体的加载、单击、双击等事件,编辑框(文本框)的文本改变事件,等等。 HTML DOM Event 对象代表事件的状态,比如事件在其中发生的元素、键盘按键的状态、鼠标的位置、鼠标按钮的状态等。事件通常与函数结合使用,函数不会在事件发生前被执行 (这句很重要)。 参考资料:JavaScript DOM编程艺术 by Jeremy Keith 2021年1月28日 海口","link":"/2021/01/28/frontEnd/addLoadEvent/"},{"title":"浅谈JS的数组拍平","text":"一、什么是数组扁平化 扁平化,顾名思义就是减少复杂性装饰,使其事物本身更简洁、简单,突出主题。 数组扁平化,对着上面意思套也知道了,就是将一个复杂的嵌套多层的数组,一层一层的转化为层级较少或者只有一层的数组。 一段代码总结 Array.prototype.flat() 特性 注:数组拍平方法 Array.prototype.flat() 也叫数组扁平化、数组拉平、数组降维。本文统一叫:数组拍平 12345678910111213141516171819202122const animals = [" ", [" ", " "], [" ", [" ", [" "]], " "]];// 不传参数时,默认“拉平”一层animals.flat();// [" ", " ", " ", " ", [" ", [" "]], " "]// 传入一个整数参数,整数即“拉平”的层数animals.flat(2);// [" ", " ", " ", " ", " ", [" "], " "]// Infinity 关键字作为参数时,无论多少层嵌套,都会转为一维数组animals.flat(Infinity);// [" ", " ", " ", " ", " ", " ", " "]// 传入 <=0 的整数将返回原数组,不“拉平”animals.flat(0);animals.flat(-10);// [" ", [" ", " "], [" ", [" ", [" "]], " "]];// 如果原数组有空位,flat()方法会跳过空位。[" ", " ", " ", " ",,].flat();// [" ", " ", " ", " "] Array.prototype.flat() 特性总结 Array.prototype.flat() 用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。 不传参数时,默认“拉平”一层,可以传入一个整数,表示想要“拉平”的层数。 传入 <=0 的整数将返回原数组,不“拉平” Infinity 关键字作为参数时,无论多少层嵌套,都会转为一维数组 如果原数组有空位,Array.prototype.flat() 会跳过空位。 面试官 N 连问第一问:实现一个简单的数组拍平 flat 函数首先,我们将花一点篇幅来探讨如何实现一个简单的数组拍平 flat 函数,详细介绍多种实现的方案,然后再尝试接住面试官的连环追问。 实现思路如何实现呢,思路非常简单:实现一个有数组拍平功能的 flat 函数, 我们要做的就是在数组中找到是数组类型的元素,然后将他们展开 。这就是实现数组拍平 flat 方法的关键思路。 有了思路,我们就需要解决实现这个思路需要克服的困难: 第一个要解决的就是遍历数组的每一个元素; 第二个要解决的就是判断元素是否是数组; 第三个要解决的就是将数组的元素展开一层; 遍历数组的方案遍历数组并取得数组元素的方法非常之多, 包括且不限于下面几种 : for 循环 for...of for...in forEach() entries() keys() values() reduce() map() 123456789101112131415161718192021222324252627282930313233343536const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "弹铁蛋同学" }];// 遍历数组的方法有太多,本文只枚举常用的几种// for 循环for (let i = 0; i < arr.length; i++) { console.log(arr[i]);}// for...offor (let value of arr) { console.log(value);}// for...infor (let i in arr) { console.log(arr[i]);}// forEach 循环arr.forEach(value => { console.log(value);});// entries()for (let [index, value] of arr.entries()) { console.log(value);}// keys()for (let index of arr.keys()) { console.log(arr[index]);}// values()for (let value of arr.values()) { console.log(value);}// reduce()arr.reduce((pre, cur) => { console.log(cur);}, []);// map()arr.map(value => console.log(value)); 只要是能够遍历数组取到数组中每一个元素的方法,都是一种可行的解决方案。 判断元素是数组的方案 instanceof constructor Object.prototype.toString isArray 123456789const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "弹铁蛋同学" }];arr instanceof Array// truearr.constructor === Array// trueObject.prototype.toString.call(arr) === '[object Array]'// trueArray.isArray(arr)// true 说明 : instanceof 操作符是假定只有一种全局环境,如果网页中包含多个框架,多个全局环境,如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。(所以在这种情况下会不准确) typeof 操作符对数组取类型将返回 object 因为 constructor 可以被重写,所以不能确保一定是数组。 javascript const str = 'abc'; str.constructor = Array; str.constructor === Array // true 将数组的元素展开一层的方案 扩展运算符 + concat concat() 方法用于合并两个或多个数组,在拼接的过程中加上扩展运算符会展开一层数组。详细见下面的代码。 concat +apply 主要是利用 apply 在绑定作用域时,传入的第二个参数是一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。也就是在调用 apply 函数的过程中,会将传入的数组一个一个的传入到要执行的函数中,也就是相当对数组进行了一层的展开。 toString + split 不推荐使用 toString + split 方法,因为操作字符串是和危险的事情,在上一面文章中我做了一个操作字符串的案例还被许多小伙伴们批评了。如果数组中的元素所有都是数字的话,toString +split 是可行的,并且是一步搞定。 12345678910111213const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "弹铁蛋同学" }];// 扩展运算符 + concat[].concat(...arr)// [1, 2, 3, 4, 1, 2, 3, [1, 2, 3, [1, 2, 3]], 5, "string", { name: "弹铁蛋同学" }];// concat + apply[].concat.apply([], arr);// [1, 2, 3, 4, 1, 2, 3, [1, 2, 3, [1, 2, 3]], 5, "string", { name: "弹铁蛋同学" }];// toString + splitconst arr2 =[1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]]]arr2.toString().split(',').map(v=>parseInt(v))// [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3] 总结完要解决的三大困难,那我们就可以非常轻松的实现一版数组拍平 flat 函数了。 1234567891011121314151617const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "弹铁蛋同学" }];// concat + 递归function flat(arr) { let arrResult = []; arr.forEach(item => { if (Array.isArray(item)) { arrResult = arrResult.concat(arguments.callee(item))); // 递归 // 或者用扩展运算符 // arrResult.push(...arguments.callee(item)); } else { arrResult.push(item); } }); return arrResult;}flat(arr)// [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { name: "弹铁蛋同学" }]; 到这里,恭喜你成功得到了面试官对你手撕代码能力的基本认可 。但是面试官往往会不止于此,将继续考察面试者的各种能力。 第二问:用 reduce 实现 flat 函数我见过很多的面试官都很喜欢点名道姓的要面试者直接用 reduce 去实现 flat 函数。想知道为什么?文章后半篇我们考虑数组空位的情况的时候就知道为啥了。其实思路也是一样的。 12345678910111213const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "弹铁蛋同学" }]// 首先使用 reduce 展开一层arr.reduce((pre, cur) => pre.concat(cur), []);// [1, 2, 3, 4, 1, 2, 3, [1, 2, 3, [1, 2, 3]], 5, "string", { name: "弹铁蛋同学" }];// 用 reduce 展开一层 + 递归const flat = arr => { return arr.reduce((pre, cur) => { return pre.concat(Array.isArray(cur) ? flat(cur) : cur); }, []);};// [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { name: "弹铁蛋同学" }]; 第三问:使用栈的思想实现 flat 函数123456789101112131415161718// 栈思想function flat(arr) { const result = []; const stack = [].concat(arr); // 将数组元素拷贝至栈,直接赋值会改变原数组 //如果栈不为空,则循环遍历 while (stack.length !== 0) { const val = stack.pop(); if (Array.isArray(val)) { stack.push(...val); //如果是数组再次入栈,并且展开了一层 } else { result.unshift(val); //如果不是数组就将其取出来放入结果数组中 } } return result;}const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "弹铁蛋同学" }]flat(arr)// [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { name: "弹铁蛋同学" }]; 第四问:通过传入整数参数控制“拉平”层数12345678910111213// reduce + 递归function flat(arr, num = 1) { return num > 0 ? arr.reduce( (pre, cur) => pre.concat(Array.isArray(cur) ? flat(cur, num - 1) : cur), [] ) : arr.slice();}const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "弹铁蛋同学" }]flat(arr, Infinity);// [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { name: "弹铁蛋同学" }]; 第五问:使用 Generator 实现 flat 函数123456789101112131415function* flat(arr, num) { if (num === undefined) num = 1; for (const item of arr) { if (Array.isArray(item) && num > 0) { // num > 0 yield* flat(item, num - 1); } else { yield item; } }}const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "弹铁蛋同学" }]// 调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。// 也就是遍历器对象(Iterator Object)。所以我们要用一次扩展运算符得到结果[...flat(arr, Infinity)] // [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { name: "弹铁蛋同学" }]; 第六问:实现在原型链上重写 flat 函数123456789101112131415161718Array.prototype.fakeFlat = function(num = 1) { if (!Number(num) || Number(num) < 0) { return this; } let arr = this.concat(); // 获得调用 fakeFlat 函数的数组 while (num > 0) { if (arr.some(x => Array.isArray(x))) { arr = [].concat.apply([], arr); // 数组中还有数组元素的话并且 num > 0,继续展开一层数组 } else { break; // 数组中没有数组元素并且不管 num 是否依旧大于 0,停止循环。 } num--; } return arr;};const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "弹铁蛋同学" }]arr.fakeFlat(Infinity)// [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { name: "弹铁蛋同学" }]; 第七问:考虑数组空位的情况由最开始我们总结的 flat 特性知道,flat 函数执行是会跳过空位的。ES5 大多数数组方法对空位的处理都会选择跳过空位包括:forEach(), filter(), reduce(), every() 和 some()都会跳过空位。 所以我们可以利用上面几种方法来实现 flat 跳过空位的特性 123456789101112131415161718192021222324252627282930313233343536// reduce + 递归Array.prototype.fakeFlat = function(num = 1) { if (!Number(num) || Number(num) < 0) { return this; } let arr = [].concat(this); return num > 0 ? arr.reduce( (pre, cur) => pre.concat(Array.isArray(cur) ? cur.fakeFlat(--num) : cur), [] ) : arr.slice();};const arr = [1, [3, 4], , ,];arr.fakeFlat()// [1, 3, 4]// foEach + 递归Array.prototype.fakeFlat = function(num = 1) { if (!Number(num) || Number(num) < 0) { return this; } let arr = []; this.forEach(item => { if (Array.isArray(item)) { arr = arr.concat(item.fakeFlat(--num)); } else { arr.push(item); } }); return arr;};const arr = [1, [3, 4], , ,];arr.fakeFlat()// [1, 3, 4] 扩展阅读:由于空位的处理规则非常不统一,所以建议避免出现空位。ES5 对空位的处理,就非常不一致,大多数情况下会忽略空位。 forEach(), filter(), reduce(), every() 和 some()都会跳过空位。 map()会跳过空位,但会保留这个值。 join()和 toString()会将空位视为 undefined,而 undefined和 null会被处理成空字符串。 ES6 明确将空位转为 undefined。 entries()、keys()、values()、find()和 findIndex()会将空位处理成 undefined。 for...of循环会遍历空位。 fill()会将空位视为正常的数组位置。 copyWithin()会连空位一起拷贝。 扩展运算符(...)也会将空位转为 undefined。 Array.from方法会将数组的空位,转为 undefined。 总结面试官现场考察一道写代码的题目,其实不仅仅是写代码,在写代码的过程中会遇到各种各样的知识点和代码的边界情况。虽然大多数情况下,面试官不会那么变态,就 flat 实现去连续追问面试者,并且手撕好几个版本,但面试官会要求在你写的那版代码的基础上再写出一个更完美的版本是常有的事情。只有我们沉下心来把基础打扎实,不管面试官如何追问,我们都能自如的应对。flat 的实现绝对不会只有文中列出的这几个版本,敲出自己的代码是最好的进步,在评论区或者在 issue 中写出你自己的版本吧! 查看原文","link":"/2022/06/10/frontEnd/JSflatArray/"},{"title":"JavaScript 异步代码的几个推荐做法","text":"今天给大家来推荐几个写好 JavaScript 异步代码的推荐做法,每种场景都有一个对应的 eslint 规则,大家可以选择去配置一下。 no-async-promise-executor不建议将 async 函数传递给 new Promise 的构造函数。 12345// ❌new Promise(async (resolve, reject) => {});// ✅new Promise((resolve, reject) => {}); 首先,你在 Promise 的构造函数里去使用 async ,那么包装个 Promise 可能就是没啥必要的。另外,如果 async 函数抛出了异常,新构造的 promise 实例并不会 reject ,那么这个错误就捕获不到了。 no-await-in-loop不建议在循环里使用 await,有这种写法通常意味着程序没有充分利用 JavaScript 的事件驱动。 1234// ❌for (const url of urls) { const response = await fetch(url);} 建议将这些异步任务改为并发执行,这可以大大提升代码的执行效率。 123456789// ✅const responses = [];for (const url of urls) { const response = fetch(url); responses.push(response);}await Promise.all(responses); no-promise-executor-return不建议在 Promise 构造函数中返回值,Promise 构造函数中返回的值是没法用的,并且返回值也不会影响到 Promise 的状态。 12345// ❌new Promise((resolve, reject) => { return result;}); 正常的做法是将返回值传递给 resolve,如果出错了就传给 reject。 1234// ✅new Promise((resolve, reject) => { resolve(result);}); require-atomic-updates不建议将赋值操作和 await 组合使用,这可能会导致条件竞争。 看看下面的代码,你觉得 totalPosts 最终的值是多少? 123456789101112131415// ❌let totalPosts = 0;async function getPosts(userId) { const users = [{ id: 1, posts: 5 }, { id: 2, posts: 3 }]; await sleep(Math.random() * 1000); return users.find((user) => user.id === userId).posts;}async function addPosts(userId) { totalPosts += await getPosts(userId);}await Promise.all([addPosts(1), addPosts(2)]);console.log('Post count:', totalPosts); totalPosts 会打印 3 或 5,并不会打印 8,你可以在浏览器里自己试一下。 问题在于读取 totalPosts 和更新 totalPosts 之间有一个时间间隔。这会导致竞争条件,当值在单独的函数调用中更新时,更新不会反映在当前函数范围中。因此,两个函数都会将它们的结果添加到 totalPosts 的初始值0。 避免竞争条件正确的做法: 12345678910111213141516// ✅let totalPosts = 0;async function getPosts(userId) { const users = [{ id: 1, posts: 5 }, { id: 2, posts: 3 }]; await sleep(Math.random() * 1000); return users.find((user) => user.id === userId).posts;}async function addPosts(userId) { const posts = await getPosts(userId); totalPosts += posts; // variable is read and immediately updated}await Promise.all([addPosts(1), addPosts(2)]);console.log('Post count:', totalPosts); max-nested-callbacks防止回调地狱,避免大量的深度嵌套: 12345678910111213141516171819/* eslint max-nested-callbacks: ["error", 3] */// ❌async1((err, result1) => { async2(result1, (err, result2) => { async3(result2, (err, result3) => { async4(result3, (err, result4) => { console.log(result4); }); }); });});// ✅const result1 = await asyncPromise1();const result2 = await asyncPromise2(result1);const result3 = await asyncPromise3(result2);const result4 = await asyncPromise4(result3);console.log(result4); 回调地狱让代码难以阅读和维护,建议将回调都重构为 Promise 并使用现代的 async/await 语法。 no-return-await返回异步结果时不一定要写 await ,如果你要等待一个 Promise,然后又要立刻返回它,这可能是不必要的。 1234// ❌async () => { return await getUser(userId);} 从一个 async 函数返回的所有值都包含在一个 Promise 中,你可以直接返回这个 Promise。 1234// ✅async () => { return getUser(userId);} 当然,也有个例外,如果外面有 try...catch 包裹,删除 await 就捕获不到异常了,在这种情况下,建议明确一下意图,把结果分配给不同行的变量。 123456789101112131415161718// 👎async () => { try { return await getUser(userId); } catch (error) { // Handle getUser error }}// 👍async () => { try { const user = await getUser(userId); return user; } catch (error) { // Handle getUser error }} prefer-promise-reject-errors建议在 reject Promise 时强制使用 Error 对象,这样可以更方便的追踪错误堆栈。 12345// ❌Promise.reject('An error occurred');// ✅Promise.reject(new Error('An error occurred')); node/handle-callback-err强制在 Node.js 的异步回调里进行异常处理。 1234567891011121314// ❌function callback(err, data) { console.log(data);}// ✅function callback(err, data) { if (err) { console.log(err); return; } console.log(data);} 在 Node.js 中,通常将异常作为第一个参数传递给回调函数。忘记处理这些异常可能会导致你的应用程序出现不可预知的问题。 如果函数的第一个参数命名为 err 时才会触发这个规则,你也可以去 .eslintrc 文件里自定义异常参数名。 node/no-sync不建议在存在异步替代方案的 Node.js 核心 API 中使用同步方法。 12345// ❌const file = fs.readFileSync(path);// ✅const file = await fs.readFile(path); 在 Node.js 中对 I/O 操作使用同步方法会阻塞事件循环。大多数场景下,执行 I/O 操作时使用异步方法是更好的选择。 @typescript-eslint/await-thenable不建议 await 非 Promise 函数或值。 12345678910111213// ❌function getValue() { return someValue;}await getValue();// ✅async function getValue() { return someValue;}await getValue(); @typescript-eslint/no-floating-promises建议 Promise 附加异常处理的代码。 12345678// ❌myPromise() .then(() => {});// ✅myPromise() .then(() => {}) .catch(() => {}); 养成个好的习惯,永远做好异常处理! @typescript-eslint/no-misused-promises不建议将 Promise 传递到并非想要处理它们的地方,例如 if 条件。 12345// ❌if (getUserFromDB()) {}// ✅ 👎if (await getUserFromDB()) {} 更推荐抽一个变量出来提高代码的可读性。 123// ✅ 👍const user = await getUserFromDB();if (user) {}","link":"/2022/04/10/frontEnd/asyncAndawait/"},{"title":"常用的前端JavaScript方法封装","text":"一些常用的前端JavaScript方法封装,自用备份。 1、输入一个值,返回其数据类型123function type(para) { return Object.prototype.toString.call(para)} 2、数组去重1234567891011121314151617181920212223function unique1(arr) { return [...new Set(arr)]}function unique2(arr) { var obj = {}; return arr.filter(ele => { if (!obj[ele]) { obj[ele] = true; return true; } })}function unique3(arr) { var result = []; arr.forEach(ele => { if (result.indexOf(ele) == -1) { result.push(ele) } }) return result;} 3、字符串去重1234567891011121314151617String.prototype.unique = function () { var obj = {}, str = '', len = this.length; for (var i = 0; i < len; i++) { if (!obj[this[i]]) { str += this[i]; obj[this[i]] = true; } } return str;}#### //去除连续的字符串 function uniq(str) { return str.replace(/(\\w)\\1+/g, '$1')} 4、深拷贝 浅拷贝12345678910111213141516171819202122232425262728293031323334353637383940414243444546//深克隆(深克隆不考虑函数)function deepClone(obj, result) { var result = result || {}; for (var prop in obj) { if (obj.hasOwnProperty(prop)) { if (typeof obj[prop] == 'object' && obj[prop] !== null) { // 引用值(obj/array)且不为null if (Object.prototype.toString.call(obj[prop]) == '[object Object]') { // 对象 result[prop] = {}; } else { // 数组 result[prop] = []; } deepClone(obj[prop], result[prop]) } else { // 原始值或func result[prop] = obj[prop] } }}return result;}// 深浅克隆是针对引用值function deepClone(target) { if (typeof (target) !== 'object') { return target; } var result; if (Object.prototype.toString.call(target) == '[object Array]') { // 数组 result = [] } else { // 对象 result = {}; } for (var prop in target) { if (target.hasOwnProperty(prop)) { result[prop] = deepClone(target[prop]) } } return result;}// 无法复制函数var o1 = jsON.parse(jsON.stringify(obj1)); 5、reverse底层原理和扩展12345678910// 改变原数组Array.prototype.myReverse = function () { var len = this.length; for (var i = 0; i < len; i++) { var temp = this[i]; this[i] = this[len - 1 - i]; this[len - 1 - i] = temp; } return this;} 6、圣杯模式的继承12345678function inherit(Target, Origin) { function F() {}; F.prototype = Origin.prototype; Target.prototype = new F(); Target.prototype.constructor = Target; // 最终的原型指向 Target.prop.uber = Origin.prototype;} 7、找出字符串中第一次只出现一次的字母12345678910111213141516String.prototype.firstAppear = function () { var obj = {}, len = this.length; for (var i = 0; i < len; i++) { if (obj[this[i]]) { obj[this[i]]++; } else { obj[this[i]] = 1; } } for (var prop in obj) { if (obj[prop] == 1) { return prop; } }} 8、找元素的第n级父元素1234567function parents(ele, n) { while (ele && n) { ele = ele.parentElement ? ele.parentElement : ele.parentNode; n--; } return ele;} 9、 返回元素的第n个兄弟节点1234567891011121314151617181920function retSibling(e, n) { while (e && n) { if (n > 0) { if (e.nextElementSibling) { e = e.nextElementSibling; } else { for (e = e.nextSibling; e && e.nodeType !== 1; e = e.nextSibling); } n--; } else { if (e.previousElementSibling) { e = e.previousElementSibling; } else { for (e = e.previousElementSibling; e && e.nodeType !== 1; e = e.previousElementSibling); } n++; } } return e;} 10、封装mychildren,解决浏览器的兼容问题1234567891011function myChildren(e) { var children = e.childNodes, arr = [], len = children.length; for (var i = 0; i < len; i++) { if (children[i].nodeType === 1) { arr.push(children[i]) } } return arr;} 11、判断元素有没有子元素12345678910function hasChildren(e) { var children = e.childNodes, len = children.length; for (var i = 0; i < len; i++) { if (children[i].nodeType === 1) { return true; } } return false;} 12、我一个元素插入到另一个元素的后面12345678Element.prototype.insertAfter = function (target, elen) { var nextElen = elen.nextElenmentSibling; if (nextElen == null) { this.appendChild(target); } else { this.insertBefore(target, nextElen); }} 13、返回当前的时间(年月日时分秒)123456789101112131415161718192021function getDateTime() { var date = new Date(), year = date.getFullYear(), month = date.getMonth() + 1, day = date.getDate(), hour = date.getHours() + 1, minute = date.getMinutes(), second = date.getSeconds(); month = checkTime(month); day = checkTime(day); hour = checkTime(hour); minute = checkTime(minute); second = checkTime(second); function checkTime(i) { if (i < 10) { i = "0" + i; } return i; } return "" + year + "年" + month + "月" + day + "日" + hour + "时" + minute + "分" + second + "秒"} 14、获得滚动条的滚动距离12345678910111213function getScrollOffset() { if (window.pageXOffset) { return { x: window.pageXOffset, y: window.pageYOffset } } else { return { x: document.body.scrollLeft + document.documentElement.scrollLeft, y: document.body.scrollTop + document.documentElement.scrollTop } }} 15、获得视口的尺寸1234567891011121314151617181920212223function getViewportOffset() { if (window.innerWidth) { return { w: window.innerWidth, h: window.innerHeight } } else { // ie8及其以下 if (document.compatMode === "BackCompat") { // 怪异模式 return { w: document.body.clientWidth, h: document.body.clientHeight } } else { // 标准模式 return { w: document.documentElement.clientWidth, h: document.documentElement.clientHeight } } }} 16、获取任一元素的任意属性123function getStyle(elem, prop) { return window.getComputedStyle ? window.getComputedStyle(elem, null)[prop] : elem.currentStyle[prop]} 17、绑定事件的兼容代码1234567891011function addEvent(elem, type, handle) { if (elem.addEventListener) { //非ie和非ie9 elem.addEventListener(type, handle, false); } else if (elem.attachEvent) { //ie6到ie8 elem.attachEvent('on' + type, function () { handle.call(elem); }) } else { elem['on' + type] = handle; }} 18、解绑事件123456789function removeEvent(elem, type, handle) { if (elem.removeEventListener) { //非ie和非ie9 elem.removeEventListener(type, handle, false); } else if (elem.detachEvent) { //ie6到ie8 elem.detachEvent('on' + type, handle); } else { elem['on' + type] = null; }} 19、取消冒泡的兼容代码1234567function stopBubble(e) { if (e && e.stopPropagation) { e.stopPropagation(); } else { window.event.cancelBubble = true; }} 20、检验字符串是否是回文123456789101112function isPalina(str) { if (Object.prototype.toString.call(str) !== '[object String]') { return false; } var len = str.length; for (var i = 0; i < len / 2; i++) { if (str[i] != str[len - 1 - i]) { return false; } } return true;} 21、检验字符串是否是回文12345function isPalindrome(str) { str = str.replace(/\\W/g, '').toLowerCase(); console.log(str) return (str == str.split('').reverse().join(''))} 22、兼容getElementsByClassName方法123456789101112131415161718Element.prototype.getElementsByClassName = Document.prototype.getElementsByClassName = function (_className) { var allDomArray = document.getElementsByTagName('*'); var lastDomArray = []; function trimSpace(strClass) { var reg = /\\s+/g; return strClass.replace(reg, ' ').trim() } for (var i = 0; i < allDomArray.length; i++) { var classArray = trimSpace(allDomArray[i].className).split(' '); for (var j = 0; j < classArray.length; j++) { if (classArray[j] == _className) { lastDomArray.push(allDomArray[i]); break; } } } return lastDomArray;} 23、运动函数123456789101112131415161718192021222324252627282930function animate(obj, json, callback) { clearInterval(obj.timer); var speed, current; obj.timer = setInterval(function () { var lock = true; for (var prop in json) { if (prop == 'opacity') { current = parseFloat(window.getComputedStyle(obj, null)[prop]) * 100; } else { current = parseInt(window.getComputedStyle(obj, null)[prop]); } speed = (json[prop] - current) / 7; speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed); if (prop == 'opacity') { obj.style[prop] = (current + speed) / 100; } else { obj.style[prop] = current + speed + 'px'; } if (current != json[prop]) { lock = false; } } if (lock) { clearInterval(obj.timer); typeof callback == 'function' ? callback() : ''; } }, 30)} 24、弹性运动1234567891011121314151617function ElasticMovement(obj, target) { clearInterval(obj.timer); var iSpeed = 40, a, u = 0.8; obj.timer = setInterval(function () { a = (target - obj.offsetLeft) / 8; iSpeed = iSpeed + a; iSpeed = iSpeed * u; if (Math.abs(iSpeed) <= 1 && Math.abs(a) <= 1) { console.log('over') clearInterval(obj.timer); obj.style.left = target + 'px'; } else { obj.style.left = obj.offsetLeft + iSpeed + 'px'; } }, 30);} 25、封装自己的forEach方法12345678Array.prototype.myForEach = function (func, obj) { var len = this.length; var _this = arguments[1] ? arguments[1] : window; // var _this=arguments[1]||window; for (var i = 0; i < len; i++) { func.call(_this, this[i], i, this) }} 26、封装自己的filter方法123456789Array.prototype.myFilter = function (func, obj) { var len = this.length; var arr = []; var _this = arguments[1] || window; for (var i = 0; i < len; i++) { func.call(_this, this[i], i, this) && arr.push(this[i]); } return arr;} 27、数组map方法123456789Array.prototype.myMap = function (func) { var arr = []; var len = this.length; var _this = arguments[1] || window; for (var i = 0; i < len; i++) { arr.push(func.call(_this, this[i], i, this)); } return arr;} 28、数组every方法123456789101112Array.prototype.myEvery = function (func) { var flag = true; var len = this.length; var _this = arguments[1] || window; for (var i = 0; i < len; i++) { if (func.apply(_this, [this[i], i, this]) == false) { flag = false; break; } } return flag;} 29、数组reduce方法123456789101112131415161718Array.prototype.myReduce = function (func, initialValue) { var len = this.length, nextValue, i; if (!initialValue) { // 没有传第二个参数 nextValue = this[0]; i = 1; } else { // 传了第二个参数 nextValue = initialValue; i = 0; } for (; i < len; i++) { nextValue = func(nextValue, this[i], i, this); } return nextValue;} 30、获取url中的参数1234567891011121314function getWindonHref() { var sHref = window.location.href; var args = sHref.split('?'); if (args[0] === sHref) { return ''; } var hrefarr = args[1].split('#')[0].split('&'); var obj = {}; for (var i = 0; i < hrefarr.length; i++) { hrefarr[i] = hrefarr[i].split('='); obj[hrefarr[i][0]] = hrefarr[i][1]; } return obj;} 31、数组排序1234567891011121314151617181920212223242526272829303132333435363738394041424344454647// 快排 [left] + min + [right]function quickArr(arr) { if (arr.length <= 1) { return arr; } var left = [], right = []; var pIndex = Math.floor(arr.length / 2); var p = arr.splice(pIndex, 1)[0]; for (var i = 0; i < arr.length; i++) { if (arr[i] <= p) { left.push(arr[i]); } else { right.push(arr[i]); } } // 递归 return quickArr(left).concat([p], quickArr(right));}// 冒泡function bubbleSort(arr) { for (var i = 0; i < arr.length - 1; i++) { for (var j = i + 1; j < arr.length; j++) { if (arr[i] > arr[j]) { var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } } return arr;}function bubbleSort(arr) { var len = arr.length; for (var i = 0; i < len - 1; i++) { for (var j = 0; j < len - 1 - i; j++) { if (arr[j] > arr[j + 1]) { var temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } return arr;} 32、遍历Dom树123456789// 给定页面上的DOM元素,将访问元素本身及其所有后代(不仅仅是它的直接子元素)// 对于每个访问的元素,函数讲元素传递给提供的回调函数function traverse(element, callback) { callback(element); var list = element.children; for (var i = 0; i < list.length; i++) { traverse(list[i], callback); }} 33、原生js封装ajax123456789101112131415161718192021222324252627function ajax(method, url, callback, data, flag) { var xhr; flag = flag || true; method = method.toUpperCase(); if (window.XMLHttpRequest) { xhr = new XMLHttpRequest(); } else { xhr = new ActiveXObject('Microsoft.XMLHttp'); } xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { console.log(2) callback(xhr.responseText); } } if (method == 'GET') { var date = new Date(), timer = date.getTime(); xhr.open('GET', url + '?' + data + '&timer' + timer, flag); xhr.send() } else if (method == 'POST') { xhr.open('POST', url, flag); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send(data); }} 34、异步加载script12345678910111213141516function loadScript(url, callback) { var oscript = document.createElement('script'); if (oscript.readyState) { // ie8及以下版本 oscript.onreadystatechange = function () { if (oscript.readyState === 'complete' || oscript.readyState === 'loaded') { callback(); } } } else { oscript.onload = function () { callback() }; } oscript.src = url; document.body.appendChild(oscript);} 35、cookie管理12345678910111213141516171819var cookie = { set: function (name, value, time) { document.cookie = name + '=' + value + '; max-age=' + time; return this; }, remove: function (name) { return this.setCookie(name, '', -1); }, get: function (name, callback) { var allCookieArr = document.cookie.split('; '); for (var i = 0; i < allCookieArr.length; i++) { var itemCookieArr = allCookieArr[i].split('='); if (itemCookieArr[0] === name) { return itemCookieArr[1] } } return undefined; }} 36、实现bind()方法1234567891011121314Function.prototype.myBind = function (target) { var target = target || window; var _args1 = [].slice.call(arguments, 1); var self = this; var temp = function () {}; var F = function () { var _args2 = [].slice.call(arguments, 0); var parasArr = _args1.concat(_args2); return self.apply(this instanceof temp ? this : target, parasArr) } temp.prototype = self.prototype; F.prototype = new temp(); return F;} 37、实现call()方法1234567891011Function.prototype.myCall = function () { var ctx = arguments[0] || window; ctx.fn = this; var args = []; for (var i = 1; i < arguments.length; i++) { args.push(arguments[i]) } var result = ctx.fn(...args); delete ctx.fn; return result;} 38、实现apply()方法123456789101112Function.prototype.myApply = function () { var ctx = arguments[0] || window; ctx.fn = this; if (!arguments[1]) { var result = ctx.fn(); delete ctx.fn; return result; } var result = ctx.fn(...arguments[1]); delete ctx.fn; return result;} 39、防抖1234567891011function debounce(handle, delay) { var timer = null; return function () { var _self = this, _args = arguments; clearTimeout(timer); timer = setTimeout(function () { handle.apply(_self, _args) }, delay) }} 40、节流12345678910function throttle(handler, wait) { var lastTime = 0; return function (e) { var nowTime = new Date().getTime(); if (nowTime - lastTime > wait) { handler.apply(this, arguments); lastTime = nowTime; } }} 41、requestAnimFrame兼容性方法12345678window.requestAnimFrame = (function () { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); };})(); 42、cancelAnimFrame兼容性方法12345678window.cancelAnimFrame = (function () { return window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || function (id) { window.clearTimeout(id); };})(); 43、jsonp底层方法12345678910111213141516function jsonp(url, callback) { var oscript = document.createElement('script'); if (oscript.readyState) { // ie8及以下版本 oscript.onreadystatechange = function () { if (oscript.readyState === 'complete' || oscript.readyState === 'loaded') { callback(); } } } else { oscript.onload = function () { callback() }; } oscript.src = url; document.body.appendChild(oscript);} 44、获取url上的参数12345678910111213141516function getUrlParam(sUrl, sKey) { var result = {}; sUrl.replace(/(\\w+)=(\\w+)(?=[&|#])/g, function (ele, key, val) { if (!result[key]) { result[key] = val; } else { var temp = result[key]; result[key] = [].concat(temp, val); } }) if (!sKey) { return result; } else { return result[sKey] || ''; }} 45、格式化时间12345678910111213141516171819202122function formatDate(t, str) { var obj = { yyyy: t.getFullYear(), yy: ("" + t.getFullYear()).slice(-2), M: t.getMonth() + 1, MM: ("0" + (t.getMonth() + 1)).slice(-2), d: t.getDate(), dd: ("0" + t.getDate()).slice(-2), H: t.getHours(), HH: ("0" + t.getHours()).slice(-2), h: t.getHours() % 12, hh: ("0" + t.getHours() % 12).slice(-2), m: t.getMinutes(), mm: ("0" + t.getMinutes()).slice(-2), s: t.getSeconds(), ss: ("0" + t.getSeconds()).slice(-2), w: ['日', '一', '二', '三', '四', '五', '六'][t.getDay()] }; return str.replace(/([a-z]+)/ig, function ($1) { return obj[$1] });} 46、验证邮箱的正则表达式1234function isAvailableEmail(sEmail) { var reg = /^([\\w+\\.])+@\\w+([.]\\w+)+$/ return reg.test(sEmail)} 47、函数柯里化12345678910111213141516//是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术function curryIt(fn) { var length = fn.length, args = []; var result = function (arg) { args.push(arg); length--; if (length <= 0) { return fn.apply(this, args); } else { return result; } } return result;} 48、大数相加12345678910111213function sumBigNumber(a, b) { var res = '', //结果 temp = 0; //按位加的结果及进位 a = a.split(''); b = b.split(''); while (a.length || b.length || temp) { //~~按位非 1.类型转换,转换成数字 2.~~undefined==0 temp += ~~a.pop() + ~~b.pop(); res = (temp % 10) + res; temp = temp > 9; } return res.replace(/^0+/, '');} 49、单例模式123456789function getSingle(func) { var result; return function () { if (!result) { result = new func(arguments); } return result; }} 查看原文","link":"/2022/05/12/frontEnd/commonJSmethod/"},{"title":"有了for循环 为什么还要forEach?","text":"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。 在了解这些后就知道 forEach 其实是一个迭代器,他与 for 循环本质上的区别是 forEach 是负责遍历(Array Set Map)可迭代对象的,而 for 循环是一种循环机制,只是能通过它遍历出数组。再来聊聊究竟什么是迭代器,还记得之前提到的 Generator 生成器,当它被调用时就会生成一个迭代器对象(Iterator Object),它有一个 .next()方法,每次调用返回一个对象{value:value,done:Boolean},value返回的是 yield 后的返回值,当 yield 结束,done 变为 true,通过不断调用并依次的迭代访问内部的值。 迭代器是一种特殊对象。ES6规范中它的标志是返回对象的 next() 方法,迭代行为判断在 done 之中。在不暴露内部表示的情况下,迭代器实现了遍历。看代码 1234567let arr = [1, 2, 3, 4] // 可迭代对象let iterator = arrSymbol.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 的实现原理。 1234let arr = [1, 2, 3, 4]for (const item of arr) { console.log(item); // 1 2 3 4} 把调用 next 方法返回对象的 value 值并保存在 item 中,直到 done 为 true 跳出循环,所有可迭代对象可供for…of消费。 再来看看其他可迭代对象: 123456789101112131415161718192021function 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的语法区别 了解了本质区别,在应用过程中,他们到底有什么语法区别呢? forEach 的参数。 forEach 的中断。 forEach 删除自身元素,index不可被重置。 for 循环可以控制循环起点。 forEach 的参数我们真正了解 forEach 的完整传参内容吗?它大概是这样: 1arr.forEach((self,index,arr) =>{},this) self: 数组当前遍历的元素,默认从左往右依次获取数组元素。index: 数组当前元素的索引,第一个元素索引为0,依次类推。arr: 当前遍历的数组。this: 回调函数中this指向。 123456789let arr = [1, 2, 3, 4];let person = { name: 'ZLK'};arr.forEach(function (self, index, arr) { console.log(`当前元素为${self}索引为${index},属于数组${arr}`); console.log(this.name+='666');}, person) 我们可以利用 arr 实现数组去重: 123456let arr1 = [1, 2, 1, 3, 1];let arr2 = [];arr1.forEach(function (self, index, arr) { arr1.indexOf(self) === index ? arr2.push(self) : null;});console.log(arr2); // [1,2,3] forEach 的中断在js中有break return continue 对函数进行中断或跳出循环的操作,我们在 for循环中会用到一些中断行为,对于优化数组遍历查找是很好的,但由于forEach属于迭代器,只能按序依次遍历完成,所以不支持上述的中断行为。 1234567891011121314151617181920212223let 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: 12345678910111213try { 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 并不会报错,但是不会生效 12345678910let 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重置,先看一个简单例子: 12345let arr = [1,2,3,4]arr.forEach((item, index) => { console.log(item); // 1 2 3 4 index++;}); index不会随着函数体内部对它的增减而发生变化。在实际开发中,遍历数组同时删除某项的操作十分常见,在使用forEach删除时要注意。for 循环可以控制循环起点如上文提到的 forEach 的循环起点只能为0不能进行人为干预,而for循环不同: 1234567let arr = [1, 2, 3, 4], i = 1, length = arr.length;for (; i < length; i++) { console.log(arr[i]) // 2 3 4}; 那之前的数组遍历并删除滋生的操作就可以写成 12345678910111213141516let 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倍,forEach 比 map 快20%左右。原因分析 for:for循环没有额外的函数调用栈和上下文,所以它的实现最为简单。 forEach:对于forEach来说,它的函数签名中包含了参数和上下文,所以性能会低于 for 循环。 map:map 最慢的原因是因为 map 会返回一个新的数组,数组的创建和赋值会导致分配内存空间,因此会带来较大的性能开销。如果将map嵌套在一个循环中,便会带来更多不必要的内存消耗。当大家使用迭代器遍历一个数组时,如果不需要返回一个新数组却使用 map 是违背设计初衷的。在我前端合作开发时见过很多人只是为了遍历数组而用 map 的: 123let data = [];let data2 = [1,2,3];data2.map(item=>data.push(item)); 写在最后:这是我面试遇到的一个问题,当时只知道语法区别。并没有从可迭代对象,迭代器,生成器和性能方面,多角度进一步区分两者的异同,我也希望我能把一个简单的问题从多角度展开细讲,让大家正在搞懂搞透彻。 查看原文","link":"/2022/02/22/frontEnd/forAndForeach/"},{"title":"浅谈中型 Web 应用的开发与部署","text":"本文从真实场景出发分析一个中型 Web 应用从立项到上线稳定运行的平稳解决方案,力求既不太空泛以至于看完了仍然找不到落地的点,也尽量不会特别纠结于个别细节导致没有相关使用经历的同学无法感同身受,而是从宏观到方法论,分析整个流程中我们需要用到的工具、方法与规范,给大家提供一个参考。 本文适合具有一定经验的初中级前端开发者,如果有相关问题,也欢迎与我交流。 目录 项目构建的搭建,关键词: webpack 、 react/vue cli , steamer ,组件库 代码的规范约束,关键词: typescript 、 eslint 、prettier 测试与测试部署,关键词: 测试部署方案 、docker 日志查看与脚本错误的监控,关键词: sentry 、 vconsole 、mlogger 版本发布更新,关键词: 发布系统 、灰度发布 访问量实时监控 起步:项目构建的搭建使用 webpack 搭建脚手架目前在一般的项目中,我们都会使用 webpack 作为搭建开发环境的基础,而 react 和 vue 也各自提供了 cli 工具用于开发一个中小型项目,react 提供了 eject 功能让我们可以更加自由的配置 webpack,而 vue 脚手架虽然没有提供类似命令,但是借助 webpack 工具链我们几乎也可以自由定制每一个角落。 不过,这里我的建议是,如果是个人项目或小型项目,我们可以基于 react 或 vue 的脚手架进行更改使用,对于一个具备一定规模的项目团队,建议还是自己维护一套单独的 webpack 构建环境,原因如下: 由于我们一般需要在项目中接入各类司内工具、支持高级API和语法、同时支持 react/vue、构建目录定制化等各类工作,实际上 80% 以上的工作我们都需要在模版之上自行添加,这个时候我们再用脚手架带来的收益已经非常小了,反而还会受制于项目的初始目录结构。 我们在自定义脚手架的 webpack 构建的时候,也需要梳理出一定的目录规范与约束,这样也有利于提高后期脚手架的可维护性和扩展性,一般来说,我们也要对脚手架中的公共部分和项目私有部分进行分离,对于一个具体项目而言,可以不用改动 webpack 的项目公共部分,这样也有利于减少不同项目之间的切换成本,对于我们目前的项目,一般会有如下两个目录: 12345678- project - project.js- config - feature - plugins - rules - script.js - webpack.base.js 对于一个项目,只需更改 project 下的配置。 这里我也推荐一个前同事做的steamer研发体系,在从中也可以找到很多相关参考,最简单的方式,就是直接在steamer-simple 的基础上进行扩展。 定制生成目录生成目录的格式,这里需要单独讲一下。 一般来说,我们生成目录的格式都是要跟发布系统进行结合的,不过也有的时候,我们做项目的时候还没有明确要接入发布系统,或者尚不知道发布系统的格式要求,但是一般情况下我们应当遵循下面的约定: js/css/img 等资源文件和 html 文件分离,前者发布到 CDN,后者发布到 http 服务器。 html 中引入的文件地址,应当是在构建过程中更新的 CDN 地址,而不是相对路径地址。 如果有离线包(offline 能力需要对应的客户端 webview 支持)等,需要单独一个目录。 对于我们目前的项目而言,一般情况下会有三个生成目录: 123- cdn- offline # 需要客户端支持该能力- webserver 如果一开始我们把所有内容生成到一个目录中了,这给我们后期的改动和维护,都带来很大的隐患。 组件库组件库这一部分,适合项目开始变得复杂的情况下进行启动,而不建议一开始进行过渡设计,建设组件库能够通过组件复用降低我们的开发成本,同时,组件库也需要专人维护,保持更新。 开发:代码的规范约束对于 js 文件的代码格式,诸如要不要分号、两个还是四个字符缩进等,一只争议不断,本文也不对此进行讨论,但是对于一个团队的项目集合(而不是单个项目)而言,代码风格的统一,是一个非常有必要而且必须要做的事情。 typescript关于 typescript 的相关文章实在太多了,这里也不对此进行详细的说明,其对代码的可读性、规范约束、降低报错风险等都有很好的改进,对于越复杂的项目其效果越明显。 另外, typescript 入门教程的作者也在我们团队中,这里我想说,如果现在还没有开始使用 typescript,请开始学习并使用 typescript 吧。 eslint 与 prettier除了 typescript 以外,在代码格式方面还建议使用 eslint 和 prettier 来进行代码格式的约束,这里虽然 eslint 和 prettier 两者在某些情景下会有些重叠,但是两者的侧重点不同,eslint 侧重于代码质量的检查并且给出提示,在某种层面上,可以看作是 typescript 的补充,而 prettier 在格式化代码方面更具有优势,并且 prettier 在设计之初就考虑了和 eslint 的集成,所以你可以完全放心的在项目中使用两者,而不用担心出现无法解决的冲突。 另外,eslint 社区中已经形成了很多种最佳实践,而我们团队也设计出了自己的一套 eslint 规则,可以按需取用 p.s. 目前 tslint 后续计划不在维护,转向 eslint 增强,因此我们在项目中可以不再使用 tslint。 以上这几种代码风格方面的约束,适合项目之初即开始约束,这样在中后期会有巨大的时间成本的节省和效率的提升。 协作:使用 git使用 git 进行协作这里其实包括两个点,使用 git 管理项目与自建 gitlab,后者是一个比较基础性的工作,并且实际上难度并不大,我认为每一个公司都可以使用自建的 gitlab 进行版本管理,这个实际上难度并不大,并且可以有效的保护公司的代码财产,如果你所在的公司还没有,那么这也是你的机会。 在具体的使用 git 中,对于git的分支/TAG管理、PR规范、提交文件约束等都应当有一套合理的流程,这里我对几点比较重要的进行说明: 锁定主干与分支开发,我们在日常开发中禁止直接提交主干,而是只能在分支中进行开发,并且通过 MR 的方式提交到主干。 git hooks 检查:我们应该通过 git hooks 进行必要的检查,比如自动化测试、eslint 检查、以及一些项目中必要的检查。 MR 检查与 Code Review,这里建议在 Merge Request 的时候做两件事情,一件是 Code Review,不过这个在某些特殊情况下不具备条件,尤其是团队人力紧张的时候,另外一个则是 MR 的 HOOK 触发检查,这个一般需要借助一些持续集成工具来完成,可以说是我们代码在合并主干之前的最后一个关卡。 测试:测试与测试部署测试是代码开发中重要的一个环节,但实际上对于前端开发来说,前端开发工程师一般较少书写测试用例,也并没有专业的测试开发工程师来辅助工作,不过,一般会有配备系统测试工程师在黑盒的情况下进行冒烟测试和功能测试以及整体链路的验收,俗称“点点点”。而这个时候,前端开发要做的就是把程序代码部署到测试服务器上,同时提供一个尽可能真实的场景供测试进行测试。 在笔者经历的项目中,虽然也使用了单元测试、端对端测试,不过这一部分体系并不十分完备,并且可能也不是大多数前端开发者感兴趣的内容,所以这里主要总结如何进行高效的测试部署与发布对接。 一般来说,我们一般会有一台到多台 Linux 测试机,供测试环境部署使用,对于前端项目而言,一般不需要特殊环境,可以进行 webpack 构建以及有 nginx 进行转发即可。 而测试环境的部署,如果是让我们手动登录去部署,显然是不合理的,如果我们纯粹使用 CI 来完成这件事,则对 CI 工具的能力和项目人员素质有一定要求,并且不具备可视化管理能力,容易出错,这里我建议可以维护一个可视化系统来进行测试环境的部署和管理,其整个环节应该是这样的: 1本地代码 -> gitlab -> 测试系统部署 -> 对接发布系统 这里的测试系统,实际上是从 gitlab 拉取代码,并且本地执行 build 命令(一般是 npm run build)并把构建结果存储在 nginx 可代理的目录即可,出于系统完备性考虑,一般我们会有多台测试机,这里我建议一般拿其中的一台作为构建机,其他的测试机仅提供 nginx 代理能力即可,我们在一台构建机中进行构建,并且将构建结果通过系统命令发送到其他的测试机。 一台构建机可以服务于所有的项目,这里还可能涉及到 webpack、nodejs 版本的维护,我们需要约束各个测试项目构建处在一个相对独立的环境中,我们也可以使用过 Docker 来进行构建,保证隔离。 构建完成后,一般我们借助 Fiddler、Charles、Whistle 等任意一款代理工具,即可以进行测试。 监控:日志查看与脚本错误的监控对于前端项目而言,即使我们已经使用了 typescript、eslint 并且也有了一些测试脚本和系统测试工程师进行的功能测试,我们还是免不了会出现 js 脚本错误,特别是 js 代码的运行环境和设备的多样化,很多错误我们并没有发现,但是产品、运营同学却出现了,或者到了线上在用户设备上出现了。 所以,有两个事情我们必须要做: 日志查看功能(手机端):现在我们写的大多数 TO C 页面都是在手机端进行,查看 console 非常不方便,我们需要一个线上实时查看 console 的工具。 我们需要脚本错误日志统计系统来进行错误统计管理与具体错误查看。 对于第一个功能,进行细分,我们需要做这样几件事情: 嵌入一个 console 和 网络请求查看器,并且只在特殊情况下才能触发(比如连续点击标题十次、或者使用特定交互手势) 在触发查看器的时候,可以将日志完整地进行上传并分析。 同时可以对该用户进行染色,会自动上传并记录该用户一定时间内后续刷新后操作的全部日志。 不过这里并没有完全实现以上三点的开源库推荐,可以在 vconsole 或者 mlogger 的基础上进行适当扩展,完成相关功能。 对于第二个功能,我们需要一个完整的统计分析与管理的错误系统,这个如果自行开发的话,难度会比较大,这里强烈推荐 sentry,可以非常方便的使用 Docker 部署到服务器端,并且拥有非常强大的日志错误分析与处理能力,通过结合 JavaScript 的 sourcemap ,可以给我们的错误定位带来极大的方便。 总之,日志查看与脚本错误监控,是比较重要但是同时容易被忽视的地方,很多时候,我们的系统在线上使用了几个月了,真正有问题反馈了,我们才会考虑加上这些功能,但这个时候通常已经造成了损失。 发布:版本发布更新发布系统,一般作为前端最后环节的系统,通常会和测试部署系统打通(或合二为一),一般的发布系统的必要功能如下: 对于前端的发布,每次只发布有改变的文件,无变动的文件则无需发布。 每次发布先发布 js/css/img 等资源文件,生效之后再发布 html 文件。 发布系统保留线上旧版代码,出问题后可以快速一键回滚。 至于一些其他的日志、报表等辅助性功能,则根据需要满足,这里不再赘述。 灰度发布灰度发布是大型项目在发布时的常见方法,指在发布版本时,初始情况下,只允许小比例(比如1-5%比例的用户使用),若出现问题时,可以快速回滚使用老版本,适用于主链路和访问量较大的页面。 对于前端的灰度,实际上有以下几种方案: 在代码层面进行灰度,即通过 if/else 进行判断,这样无需发布系统关注,也可以自由配置规则、比例与白名单/黑名单。 在入口层面进行灰度,比如 App 内嵌的 H5 则在客户端的对应入口进行回复,这样通常也无需发布系统关注。 通过发布系统,按照比例灰度,比如我们有 10 台 webserver,如果我们先发布 1 台,这样我们的灰度比例为 10%。 访问量实时监控最后一点,我们还需要一个访问量实时监控系统,我们上述有了错误查看与脚本监控系统,但是对于我们的各个页面的访问量、点击率等指标,通常是产品/运营同学比较关心的,同时访问量的波动情况也是项目健康度的一个表征(访问量突然大幅上涨或下跌,一般都有其特定原因),所以我们需要访问量实时监控系统。 而实际上访问量监控系统也有两种不同形态: 对于每一个上报 key,只进行数量上的统计 对于每一个上报 key,可以携带一些信息,对携带信息进行统计分析。 通常情况下,前者的功能是实时或者低延时的,而后者由于需要一部分统计分析,通常可以接受非实时情况(一般每天出前一天的报表)。 这部分内容,需要较强的后端接口稳定性,通常前端需要和对应岗位的同学共建。 总结总结下来,我们一个稳定的前端项目,至少涉及到以下环节: 完善的项目脚手架与代码约束规范 内部 gitlab 可视化管理的测试部署系统 实时日志查看工具 脚本错误统计管理系统 发布管理系统 访问量实时监控系统 如果你所在的团队哪个环节还没有或者不完善,那么这也是你的机会。 查看原文","link":"/2022/03/01/frontEnd/frontEndBuildRoadmap/"},{"title":"重学JS系列","text":"“如何理解考研结束不是终点,而是新生活,新奋斗的起点?” 经过考研的洗礼之后,随手做了几套面试题,感觉自己还有很多不足。所以还是希望有机会能够重新梳理自己知识体系。前端入坑一去不复返,在前进的道路上,希望我们可以共勉之! 那么先从JS开始。 2020年12月30日 桂林","link":"/2020/12/30/frontEnd/reStudyJS-EP0/"},{"title":"写在 Gitee 图床挂了之后","text":"2022年3月25日起,Gitee图床外链开始无法访问。甚至有些仓库直接被禁:当前仓库由于大量外链被屏蔽,暂时无法正常访问…… 起因大早上的刷头条说gitee图床挂了,惊得我立马起床打开电脑去看我的博客,果然如此,图片都打不开了,将图片链接直接在浏览器打开后显示的却是gitee的logo,我擦。。。 想起去年的时候gitee就曾搞过这么一出,也是图片无法外链访问,后来过了几天自己好了,当时我也就没太在意。 毕竟白嫖的,咱也不好骂人不是,还是赶紧想想解决方法吧 经过看到网友们的解决办法,也就是下面的几种: 七牛云 阿里云oss/腾讯云oss github 当然还有其他的我就不一一列举了。 嗯,还是说说我自己的解决方法吧。 之前有了解过七牛云,怎么说呢,你别看什么每个月给你免费10G的流量,但一细看其实是只有http域名才免流量,而https并不是免费的。我之前是用的自己的域名,然后申请的https证书。麻烦的地方就是每隔三个月需要手动的更换一次https证书。如果忘记了更改那图片就访问不了了。 七牛云主要的扣费点还是https的流量费,每个月博客访问量少的话大概10块钱以内吧,具体还是要看你的图片请求量多少来算。 后来就换到了gitee仓库。当时没考虑用github的原因还是 “众所周知” 你懂得。 对于oss来说,个人是感觉太重了。不过花钱买安心,也算是一个不错的选择。 不过这里我需要提醒一点,你购买时花的钱仅仅是容量费,后续的使用是需要花流量费的。而流量费可不是一笔小数目。建议可以体验几个月后再下决心使用。 最后看了一下发现 Github + jsDelivr 实现图床的方法,对于白嫖党来说应该算是一个不错的选择了。 结果配置方法比较简单,这里我就不详细的说了。 我使用的图片上传工具是 PicGo 这里简单讲描述一下: CDN地址:https://fastly.jsdelivr.netpicGo中配置:https://fastly.jsdelivr.net/gh/用户名/仓库名 其他的方案,比如七牛云(这个需要弄域名备案),腾讯云(有免费存储空间,半年期限)。 所以想用免费的,最佳选择还是 github 仓库做图床。 刚刚看到有人评论说 gitee 仓库的图片又可以查看了,可惜呀。。。我已经 删库跑路 了。 尾巴那既然事情已经发生了,无论 Gitee 官方到底是临时还是永久添加了防盗链,我都不建议大家继续使用 Gitee 作为图床(本身它还有 1 M 图片大小的限制)。而是应该使用七牛云、或者腾讯 / 阿里等云服务厂商提供的稳定的对象存储服务。 当然如果你非要拿代码托管平台做图床,建议去薅鹅毛: CODING CODING的使用条款里没有类似的限制,鹅厂八成也不在乎那点带宽,我就拿CODING存了一堆素材跑自己的项目,两年多了还是好好的。不过按照大厂的尿性,我也不知道CODING这个项目会不会有一天突然就被砍了,总之先用着吧……","link":"/2022/03/28/thoughts/gitee-Image-Host/"},{"title":"Hello World","text":"Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. Quick StartCreate a new post1$ hexo new "My New Post" More info: Writing Run server1$ hexo server More info: Server Generate static files1$ hexo generate More info: Generating Deploy to remote sites1$ hexo deploy More info: Deployment","link":"/2017/12/02/thoughts/hello-world/"},{"title":"hexo-theme-test","text":"一个半途而废的Hexo主题开发计划 开发计划之前心血来潮打算自己做一个主题,名字还没想好,先叫 test 吧。不过最近发现了一个很好看的主题,感觉有点懒得折腾自己的主题了,希望能给自己列个计划排期,希望有朝一日可以用上这个主题。(大概率不会完成) 项目预览 当前进度首页 顶部导航 ✓ 文章列表 ✓ 个人介绍 ✓ 底部版权 ✓ 搜索功能 ing 文章页 文章标题 ✓ 文章内容 ✓ 文章作者 ing 文章时间 ing 文章阅读统计 ing 文章目录 ing 分类页 分类列表 ✓ 分类跳转 ing 标签页 标签列表 ✓ 标签跳转 ing 其他功能 渲染模板由ejs换为jsx ing 全局页面响应式 ing 全局数据可配置 ing 全局深色模式 × (伪需求) 暂不考虑 站长统计 评论插件 切换主题色 看板娘 鼠标效果 后续计划当主题基本可用的时候我会上传到 Github有想玩玩的可以去催更 (当然做不做完全看我心情) Why Hexo ?了解详情","link":"/2022/02/12/thoughts/hexo-them-test/"},{"title":"hexo-theme-icarus","text":"我愿称之为 Hexo 最强主题 A simple, delicate, and modern theme for the static site generator Hexo.(来自 官方文档) 一个简单、精致和现代的静态站点生成器 Hexo 的主题。 Hexo 比 Vuepress 好用坑也少,拿来做 blog 真的是棒极了。 icarus 主题,我真的好喜欢你,为了你,我要重新开始写博客! 你他娘的还真是个好主题 奴才:老爷,您博客的主题都白了。王爷:戳了,是 icarus 主题嘛↓,噫嘻嘻嘻嘻嘻嘻~奴才:是↓是↓是↓,是 icarus 主题。奴才:(指主题)哇,好漂亮啊(迫真)。王爷:漂亮得很啊~嘻嘻嘻嘻嘻嘻(梗百科) 这个主题确实够好看,直接让我自己的主题开发计划流产了。 废话不多说直接放上链接 https://github.com/ppoffice/hexo-theme-icarus 中文文档也足够完善,即使是小白用户也能快速上手(可能) 中文文档","link":"/2021/12/12/thoughts/hexo-theme-icarus/"},{"title":"jsDelivr大面积失效,个人站点该怎么办?","text":"jsDelivr的官网还曾经介绍“jsDelivr是唯一有中国ICP许可的公开CDN,并在大陆有非常多的节点”,现在看来,它可能就要逐渐离开了。 jsDelivr 简介jsDelivr曾经是最火的前端静态文件库,也是各个站点喜欢用的静态文件CDN,甚至他们自己也推出了新的服务:esm.run,可以直接在module中使用类似 import crypto-js from 'https://esm.run/crypto-js' 的方式快速导入依赖,同时还可以使用到CDN。 说的有点多,某天一觉醒来发现自己的ISP的DNS服务器直接将 cdn.jsdelivr.net 解析成了 0.0.0.0,然后我就明白了,jsd在我家这里也要开始不能使用了。最近在不少博客之间逛,经常发现有博友出现这种情况,估计jsd正在逐步在全国失效。如果你的DNS把 cdn.jsdelivr.net 解析成 127.0.0.1 或者 0.0.0.0,那么说明你的地区jsd也被污染了。可以通过更换本机DNS解决 你的问题 。 jsd的官网还曾经介绍“jsDelivr是唯一有中国ICP许可的公开CDN,并在大陆有非常多的节点”,现在看来,它可能就要逐渐离开了。 jsDelivr 备选站目前jsDelivr有以下备选站,分别由不同的赞助商提供,目前DNS还没有被污染,使用方法和 cdn.jsdelivr.net 相同 https://fastly.jsdelivr.net/ 由fastly提供 https://gcore.jsdelivr.net/ 由G-Core Labs提供 https://testingcf.jsdelivr.net/ 由CloudFlare提供 其实 cdn.jsdelivr.net 就是由上述几家服务的综合,只不过在特定情况只解析某一个特定服务商。 至于这三个的速度,请自行去类似boce之类的网站上测试(我这里是fastly最好)。建议有在使用jsDelivr的站长尽快更换一下,不然肯定会有访客访问不了的。 只能说DNS污染比cdn前缀要差一些,还是做好替换到别的服务的准备 前端静态文件CDN备选站如果你在寻找前端库的CDN,那么有以下几个CDN站值得一试: https://www.staticfile.org/ – 由七牛云及掘金提供支持 https://www.bootcdn.cn/ – 由极兔云联合BootStrap中文网提供 https://cdn.bytedance.com/ – 字节跳动提供,内容与cdnjs一致 https://www.sourcegcdn.com/ – 由 AHDark 创立,支持npm及GitHub(白名单) https://cdnjs.com/ – 由CloudFlare等提供支持 https://unpkg.com/ – 也是CloudFlare提供支持,仅限npm包 以上站点可能对于一些包的更新不是那么及时,所以jsd如果没有大面积不可用,还是可以作为最好的选择的。 如果你是要加速Github文件 ,那么我目前还没有找到很好的替代,因为jsd真的太方便了。 如果你是要加速自己的=个人=图片等资源 ,那么你 alt+f4 赶快光速离开,因为这种公共静态文件CDN根本不是给你这种为了个人用的好吧,至少在我自己的思考里这样做就是浪费公共资源。(例如在npm官方发包来当图床的,分明就是在污染npm好吧)。 查看原文","link":"/2022/05/18/thoughts/jsdelivr-alt/"},{"title":"S02E00 七天玩转金融科技","text":"深入浅出的金融速成教学事实证明玩不转 此系列文章仅用于培训教学,请勿进行商用。 目录 S02E01 证券市场基础知识 S02E02 证券投资基金基础知识 S02E03 债券基础知识 S02E04 股票业务基础知识 S02E05 会计基础知识 S02E06 估值财务系统 S02E07 投资交易系统介绍 S02E08 注册登记系统介绍 S02E09 归纳与总结 2021年7月5日 长沙","link":"/2021/07/05/finance/financeStudy-EP0/"},{"title":"S02E01 证券市场基础知识","text":"证券市场基础知识 一、金融市场的定义、要素、功能金融市场的定义: 金融市场是进行金融融通的场所,在这里实现借贷资金的集中和分配,并由资金供给与资金需求的对比形成该市场的价格。 金融市场的要素:1、金融市场的交易对象:货币基金2、金融市场的交易主体:个人、企业、政府、金融机构3、金融市场的交易工具:直接金融工具、间接金融工具4、金融市场的交易价格:利率 金融市场的功能:资金筹集功能 (最基本的功能)直接调节功能间接调节功能信息反馈功能产业结构优化功能 二、金融市场的分类:金融市场的划分按交易期限划分:货币市场(短期)、资本市场(长期)按交割期限划分:现货市场、期货市场按政治地理区域划分:国内金融市场、国外金融市场按交易品种划分:股票、基金、债券、回购、外汇、黄金、保险 我国的金融市场主要有:银行间借拆、银行放贷、证券公司股票、保险、外汇、专门性社会基金机构 我国的金融工具主要有:银行票据、股票、基金、债券、国债、汇兑、大额存单、外汇 三、金融机构及其构成:金融机构的定义及特殊性金融机构的定义:凡是专门从事各种金融活动的组织,均称为金融机构金融机构的特殊性:(与一般企业相比)特殊的经营对象与经营内容 (货币的收付、借贷、其他金融业务) 特殊的经营关系与经营原则 (货币资金的借贷或投资关系) 特殊的经营风险影响 (信用风险、挤兑风险、利率风险、汇率风险) 金融机构主要有 中央银行 (中国人民银行)政策性银行 (国家开发银行、进出口银行、农业发展银行) 银行理财公司国有独资银行 (工行、建行、中行、农行、交行、邮储)股份制银行 (招商、中信、华夏)信用合作社 (农村信用合作社、城市信用合作社)保险公司 (财产、人身)在华外资金融机构 (在华代表处、在华营业性分支机构) 其他非银行的金融机构信托公司证券公司基金公司财务公司金融租赁公司邮政储蓄机构 监督机构(一行两会)中国人民银行中国证券监督管理委员会中国银行保险监督管理委员会 商业银行之托管部托管业务部由:运营处、监督稽核处、综合管理处、市场营销处&nbsp;构成 托管人的职责资产保管资产清算资产核算(会计核算、复核审查资产净值)投资运作监督 托管人的作用防止财产被挪用,保障财产的安全监督投资管理人的投资运作,保障持有人的的权益对资产进行会计复核,防止差错,保证准确性真实性 四、金融市场工具金融工具的定义: 在信用活动中产生,能够证明金融交易金融、期限、价格的书面文件。它对于债权债务双方所应承担的义务与享有的权利均有法律约束作用。<br> 金融工具的特征:偿还期、流动性、安全性、收益性 金融工具的分类按交易期限划分:短期金融工具、长期金融工具按融资形式划分:直接金融工具、间接金融工具按权利与义务划分:债权债务类金融工具、所有权类金融工具按是否与信用活动相关:原生金融工具、衍生金融工具 金融工具的种类:短期债券、回购协议、可转让大额定期存单、国债、公司债券、企业债券、股票、基金2021年7月6日 长沙","link":"/2021/07/06/finance/financeStudy-EP1/"},{"title":"S02E02 证券投资基金基础知识","text":"证券投资基金基础知识 一、基金的概念及相关知识1、证券投资基金的定义: 证券投资基金是指通过发售基金份额,将众多投资者的资金集中起来,形成独立资产,由基金托管人托管,基金管理人管理,以投资组合的方法进行证券投资的一种利益共享、风险共担的集合投资方式,代表了一种委托关系。 2、基金市场的参与主体 1、基金投资者:(购买份额、持有人) 2、基金管理人:(基金管理公司、证券公司、保险资管公司) 3、基金托管人:(资产保管、资金清算、会计复核、监督监管) 4、基金登记注册机构:(中国证券登记结算有限责任公司) 5、监管机构和自律机构:(证监会、基金行业自律组织、证券交易所) 3、证券投资基金与股票、债券的区别 4、证券投资基金的常见分类方式1、按基金的设立方式分类 契约型基金 (我国主要是这种) 公司型基金 2、按基金的投资标的分类 股票性基金 债券型基金 货币市场基金 混合基金 3、按基金的运作方式分类 封闭式基金 开放式基金 4、按基金的募集对象分类 公募基金 私募基金 5、特殊类型基金 指数基金 (沪深300) 交易型开放式指数基金(ETF) 上市型开放式基金(LOF) QDII 基金(合格境内机构投资者) 二、基金的认购、申购、赎回及交易1、基金的认购 认购的定义:投资者在基金成立之前向基金公司购买基金金额的行为 认购价格:通常为1元/份,基金份额在合同生效时确认,并且在建仓期后才能赎回 认购份额= (认购金额+募集期利息) / [ 基金面值 * (1+认购费率) ] 2、基金的申购 申购的定义:投资者在基金成立之后向基金公司购买基金份额的行为 申购原则:未知价法、金额申购 申购份额= 申购金额 / [ 基金单位净值 * (1+申购费率) ] 3、基金的赎回 赎回的定义:投资者向基金管理人卖出基金份额的行为 赎回原则:未知价法、份额赎回 赎回金额= 赎回份额 * [ 基金单位净值 * (1-赎回费率) ] 4、基金申赎规则总结 申购原则:未知价法、金额申购 赎回原则:未知价法、份额赎回 “未知价”原则:即基金的申购与赎回价格以受理申请当日收市后计算的基金单位净值为基准进行计算 基金采用金额申购和份额赎回的方式,即申购以金额申请,赎回以份额申请 5、基金的交易 1、上市交易的基金类别:封闭式基金、LOF、ETF 2、基金申赎与基金交易的区别 交易市场不同,基金申赎是一级市场的委托,买卖是二级市场的委托 交易价格不同,基金申赎是按净值结算,买卖是按二级市场的成交价格结算 交易费用不同,基金申赎收取申购、赎回费,买卖只收取佣金 确认时间不同,普通开放式基金的申赎T+2确认,QDII申赎T+3确认,基金买卖的交收时间同A股(T+1清算交收) 申赎行为会引起基金资产的变化,买卖行为不会影响基金资产。 三、基金资产的估值与分红1、基金资产的估值 1、基金估值的目的:为了准确、真实的反映本基金所持有金融资产和所承担金融负债的公允价值,并准确计算出本基金的经营收益,为基金的申购、赎回等交易提供公允的价值基础。 2、基金资产净值的计算:基金资产净值=总资产-总负债 总资产:指基金拥有的所有资产(包括股票、债券、银行存款和其他有价证券等)按照公允价格(可变现净值)计算的资产总额。 总负债:指基金运作及融资时所形成的负债,包括应付给他人的各项费用、应付资金利息等。 基金费用一般包括两大类: 一类是在基金销售过程中发生的由基金投资人自己承担的费用,主要包括认购费、申购费、赎回费和基金转换费。这些费用一般直接在投资人认购、申购、赎回或转换时收取。 另一类是在基金管理过程中发生的费用,主要包括基金管理费、基金托管费、信息披露费等,这些费用由基金承担。 3、基金单位净值的计算:基金单位净值=基金资产净值/基金份额数量 基金单位净值:指计算投资者申购基金份额、赎回资金金额的基础,是评价基金投资业绩的基础指标之一。 基金份额数量:指当时发行在外的基金单位的总量 2、基金分红 利息收益 股利收益 资本利得 其他收入 基金分红的分配方式 1、现金分红方式 2、分红再投资转换为基金份额 四、基金公司运作管理架构1、公募基金产品的生命周期 2、基金公司运作管理部门设置 3、基金公司的投资流程 2021年7月7日 长沙","link":"/2021/07/07/finance/financeStudy-EP2/"},{"title":"S02E03 债券基础知识","text":"债券基础知识培训 一、债券的基本认识1、债券的定义 债券是政府、金融机构、企业等机构直接向社会借债筹措资金时,向投资者发行,并且承诺按规定利率支付利息并按约定条件偿还本金的债权债务凭证。 债券通常又称为固定收益证券,因为这类金融工具能够提供固定数额现金流。 债券市场是债券发行和买卖交易的场所,将需要资金的政府机构或公司与资金盈余的投资者联系起来。 2、债券的参与方 债券发行人:借入资金的经济主体,包括了中央政府、地方政府、金融机构和企业等 债券投资人:出借资金的经济主体 债券承销商:债券承销商负责债券的发行与承销,他们在债券发行人和债券投资人之间起到金融中介的作用 债券登记结算机构: 中央国债登记结算有限责任公司(中债登):银行间市场债券登记托管结算机构 银行间市场清算所股份有限公司(上海清算所):公司信用债券、同业存单、大额存单等货币市场工具登记托管结算中心 中国证券登记结算有限责任公司(中登):交易所全部证券登记结算业务 监管机构: 证券监督管理委员会监管(证监会监管):上市企业发行债券(公司债) 中国人民银行监管(央行监管):短期融资券、中期票据等 国家发展与改革委员会监管(发改委监管):企业债、中小企业集合债券 3、发行债券的意义 对于政府来说:发行债券是为了筹措资金以弥补财政赤字和扩大公共投资。 对于金融机构来说:发行债券的目的主要是为了增加资金以扩大贷款规模;改变本身的资产负债结构。 对公司来说:在自有资金不能完全满足其资金需求时,便需要向外部筹资。通常公司对外筹资的渠道有三个:发行股票、发行债券、向银行等金融机构借款。 发行债券在一定程度上弥补了股票筹资和向银行借款的不足。 4、债券的基本要素 发行者:该债券的债务主体 票面价值:是债券票面标明的货币价值,规定币种 票面利率:名义利率,是债券年利息与债券票面价值的比率,百分数 到期期限:从发行之日起至偿清本息之日止的时间 发行价格:高于面额为溢价,等于面额为平价,低于面额为折价 付息方式:分为一次性付息、分期付息等 发行额度:根据发行人的资金需求、债券种类及市场状况决定 信用评级:测定发行人不履约而造成债券本息不能偿还的可能性 5、债券的特征 偿还性:一般规定有偿还期限,发行人必须按约定条件偿还本金并支付利息 流通性:可按需要和市场的实际情况转让债券,提前收回本金,实现投资收益 收益性:利息收入、买卖差价收入、再投资收益 安全性:收益比较稳定,不随发行者经营收益的变动而变动,风险较小 6、债券的分类 按发行主体可分为 政府债券 国债(弥补国家财政赤字,或者为了一些耗资巨大的建设项目筹措资金) 记账式国债 (由 财政部 面向全社会各类投资者、通过无纸化方式发行、以 电子记账 的方式记录债权并可以上市和流通转让的债券) 储蓄国债(凭证式)(填制 国库券收款凭证 的方式发行,不可流通转让) 储蓄国债(电子式)(以 电子方式 记录债权的方式发行,不可流通转让) 地方政府债券(以当地政府的税收能力作为还本付息的保证) 金融债券(由银行和非银行金融机构发行的债券,具有较高的安全性) 公司债券(公司债券风险与其本身的经营状况直接相关) 按付息方式可分为 零息债券:指债券合约未规定利息支付的债券 付息债券:按合约中规定,在存续期间对持有人定期支付利息 息票累积债券:与附息债券相似,也规定了票面利率,但持有人只能在债券到期时一次性获得本息,存续期间没有利息支付 按计息方式可分为 固定利率债券:有固定的到期日,并在偿还期内有固定的票面利率和不变的面值 浮动利率债券:债券的票面利率不是固定不变的,通常与一些利率进行挂钩,比如上海同业拆借率等等,根据定价日挂钩利率的变动而进行变动 可调利率债券:是指在债券存续期内允许根据一些事先选定的参考利率指数的变化,对利率进行定期调整的债券,调整间隔往往事先设定,包括1个月、6个月、1年、3年、5年等 按偿还期限可分为 长期债券:偿还期限在10年或10年以上的债券 中期债券:偿还期限在1年以上10年以下的债券 短期债券:偿还期限在1年或1年以内的债券,同业存单、短融、超短融 按是否可以赎回可分为 可赎回债券:在债券到期前,发行人可以以事先约定的赎回价格收回的债券 不可赎回债券:不能在债券到期前收回的债券 按是否可以转股可分为 可转换债券:在特定时期内可以按某一固定的比例转换成普通股的债券,它具有债务与权益双重属性,属于一种混合性筹资方式 不可转换债券:不能转换为普通股的债券 7、债券与股票的异同点 二、债券的发行与承销三、债券的交易2021年7月7日 长沙","link":"/2021/07/07/finance/financeStudy-EP3/"},{"title":"S02E07 投资交易系统介绍","text":"投资交易系统介绍培训 1、金融市场 2、基金公司运作 3、基金公司组织架构 4、基金公司系统生态 5、产品管理 6、组合管理 组合分析支持不同维度的指标结果分析查询,支持如组合创建和管理、组合画像深度分析、风险画像风险测度、情景分析压力测试、组合智能配比优化、业绩分析收益归因的前端展示,并可根据金融机构要求将结果进行数据服务输出。 组合决策现针对不同投资业务的投前模拟交易建立投资组合,并可结合金融机构时点持仓、头寸、风险指标进行不同维度的组合试算。同时,可将最终结果给到交易管理完成后续业务处理。 7、运营管理 系统核心打造运营自动化管理,通过结算管理、资金监控管理、一键报送、数据自动风险管控,极大提高运营的工作效率,杜绝人工二次录入导致的人工风险。 8、估值核算 9、监管报送中心 2021年7月12日 长沙","link":"/2021/07/12/finance/financeStudy-EP7/"},{"title":"S02E05 会计基础知识","text":"会计基础知识培训 一、会计的定义会计师以货币为主要的计量单位,反应和监督一个单位经济活动的一种经济管理活动。 二、会计的六要素收入、费用、利润、资产、负债、所有者权益资产的特征: 1、资产应为企业拥有或者控制的资源 2、资产预期会给企业带来经济利益 3、资产是由企业过去的交易或者事项形成的<br> 资产可以分为:流动资产 (货币基金、短期投资、存货和应收及预付款)非流动资产 (长期资产、固定资产、无形资产、其他资产) 负债可以分为:流动负债 (银行借款、应付以及预收款及应交款项)非流动负债 (长期借款、长期应付款及其他长期负债) 所有者权益(股东权益、净资产):投资资本、留存资本、其他综合收益 其他综合权益:直接计入所有者权益的利得和损失(偶然性所得) 收入可以分为:主营业务收入、其他业务收入费用可以分为:计入成本的费用、期间费用利润可以分为:营业利润、营业外收支 三、会计等式和记账方法1、资产=负债+所有者权益2、收入-费用=利润3、资产=负债+所有者权益+(收入-费用)利润4、资产=负债+所有者权益会计要素变动的四种类型: 1、资产方与负债及所有者权益同时等额 增加,双方总额相等。 2、资产方与负债及所有者权益同时等额 减少,双方总额相等。 3、资产方内部项目有增有减,增减金额相等,双方总额不变。 4、负债及所有者权益内部项目有增有减,增减金额相等,双方总额不变。 记账方法按《企业会计准则》规定,所有企业、事业单位一律采用 借贷记账法借贷记账法以“借”和“贷”作为记账符号。以 “有借必有贷,借贷必相等”,作为记账原则。 四、会计的科目资产类、负债类、共同类、所有者权益类、损益类 借贷关系说明: 资产类科目记在借方表示增加,记在贷方表示减少; 负债类科目记在借方表示减少,记在贷方表示增加; 所有者权益记在借方表示减少,记在贷方表示增加; 收入记在借方表示减少,记在贷方表示增加; 费用记在借方表示增加,记在贷方表示减少; 利润记在借方表示减少,记在贷方表示增加; 2021年7月8日 长沙","link":"/2021/07/08/finance/financeStudy-EP5/"},{"title":"S02E08 注册登记系统介绍","text":"TA系统培训 然而源文件已经没了,令人感叹。 2021年7月13日 长沙","link":"/2021/07/13/finance/financeStudy-EP8/"},{"title":"vuepress-theme-reco","text":"这是一个vuepress主题,旨在添加博客所需的分类、TAB墙、分页、评论等能; 主题追求极简,根据 vuepress 的默认主题修改而成,官方的主题配置仍然适用; 你可以打开 午后南杂 来查看效果。 UseBuild 12345npm run build# oryarn build Server 12345npm run dev# oryarn dev Play [email protected] 是基于 [email protected] 的博客主题。 [email protected] 功能比较简单,只适合书写简单的文档,但好在支持主题自定义,而个人又希望能够用它来书写博客,原因就是它足够的简洁,毫无疑问,这也符合很多程序员的观念,也就是在这种情况下,[email protected] 的第一个版本经过一个通宵而产生。 主题开源不久,很多朋友通过各种联系方式,给到很多好的意见和建议,所以我个人也在积极地更新。 因为我是一名前端开发工程师,开发的过程中,总是想着能不能加入一些炫酷的效果,有很多次都是添加上又去掉,反反复复,最后都是被 简洁 的这个原则阻止掉,毕竟,现在我是将它当作一个产品来看待,并不是一个技术或者是技巧的尝试项目。 1.x随着不断有用户过来询问:为什么 [email protected] 不能在 [email protected] 上使用?本来只是打算对 [email protected] 进行简单的bug修复的我,终究还是忍不住,开始了 [email protected] 的开发。又是在一个寂静的凌晨两点半(晚上就是出活快),我默默地开始了。 主题升级的关键也就是 @vuepress/plugin-blog 这款官方插件,它不需要再去麻烦地过滤数据,将分类和标签的相关信息直接存在 $categories 和 $tags 这两个全局变量中。借助于 @vuepress/plugin-blog,分类和标签功能更容易实现,但也有了一些局限。接下来两三天的时间,都是在进行功能的迁移和一些bug的修复。 [email protected] 的开发中,更加深刻地明白了模块化和组件化编程的重要性,如果当初没有把一些功能进行封装,而是直接简单的复制,这次升级也不会这么顺利。模块拆分的越细,使用就会越灵活。 CLI还是衷心地希望能有更多的朋友参与进来,更快地去完善它。接下来时间允许的情况下,我会开源一款自动生成博客的脚手架,略过配置步骤,直接书写优质内容,这也是我后来逐渐形成的一种信念,就是希望能让这款主题,功能越完善,使用越来越简单。 LicenseMIT","link":"/2019/04/09/other/guide/"},{"title":"S02E09 归纳与总结","text":"归纳与总结别看了,学不会的。 一、证券市场概述(一)有价证券定义:有价证券与虚拟资本 有价证券是指标有票面金额,用于证明持有人或该证券指定的特定主体对特定财产拥有所有权或债权的凭证。 1、证券本身没有价值; 2、但由于它代表着一定量的财产权利,持有人可凭该证券直接取得一定量的商品、货币,或是取得利息、股息等收入; 有价证券是虚拟资本的一种形式。所谓虚拟资本,是指以有价证券形式存在,并能给持有者带来一定收益的资本。虚拟资本是 相对独立于实际资本的一种资本存在形式 。通常,虚拟资本的价格总额并不等于所代表的真实资本的账面价格,甚至与真实资本的重置价格也不一定相等,其变化并不完全反映实际资本额的变化。 (二)有价证券分类12广义的有价证券:商品证券、货币证券、资本证券狭义有价证券:资本证券 商品证券是证明持有人拥有商品所有权或使用权的凭证。属于商品证券的有提货单、运货单、仓库栈单等。 货币证券是指本身能使持有人或第三者取得货币索取权的有价证券。货币证券主要包括两大类:一类是商业证券,主要是商业汇票和商业本票;另一类是银行证券,主要是银行汇票、银行本票和支票。 资本证券是指由金融投资或与金融投资有直接联系的活动而产生的证券。持有人有一定的收入请求权。资本证券是有价证券的主要形式。 有价证券按不同标准分类: 1、发行主体不同 :政府证券、政府机构证券(禁止性规定)和公司证券(进一步细分) 我国目前尚不允许除特别行政区以外的各级地方政府发行债券。政府机构证券是由经批准的政府机构发行的证券,我国目前也不允许政府机构发行。 2、依照是否在证券交易所挂牌交易 上市证券 :是指经证券主管机关核准发行,并经证券交易所依法审核同意,允许在证券交易所内公开买卖的证券。 非上市证券 :是指未申请上市或不符合证券交易所上市条件的证券。非上市证券不允许在证券交易所内交易,但可以在其他证券交易市场交易。凭证式国债和普通开放式基金份额属于非上市证券。 3、按照募集方式分类 公募证券 :指发行人向不特定的社会公众投资者公开发行的证券,审核较严格并采取公示制度。 私募证券 :指向少数特定的投资者发行的证券,其审查条件相对宽松,投资者也较少,不采取公示制度。 4、按证券所代表的权利性质 股票、债券和其他。 股票和债券是证券市场两个最基本和最主要的品种。 (三)有价证券的特征 1、收益性 2、流动性 证券具有极高的流动性必须满足三个条件: 很容易变现 变现的交易成本极小 本金保持相对稳定 3、风险性 指实际收益与预期收益的背离,或者说是收益的不确定性。 从整体上说,证券的风险与其收益成正比。 4、期限性 债券一般有明确的还本付息期限。 股票一般没有期限性,可以视为无期证券。 二、证券市场1证券市场的定义:有价证券发行和交易的场所。 (一)证券市场的特征 价值直接交换的场所 :有价证券是价值的一种直接表现形式 财产权利直接交换的场所 :有价证券是财产权利的直接代表 风险直接交换的场所 :转移的不仅是收益权,同时也包含风险 (二)证券市场结构 1、层次结构 按证券进入市场的顺序而形成的结构关系划分,证券市场的构成可分为发行市场和交易市场。 证券发行市场又称“一级市场”或“初级市场”。 证券交易市场又称“二级市场”或“次级市场”。 证券发行市场和流通市场的关系: 证券发行市场和流通市场相互依存、相互制约。证券发行市场是流通市场的基础和前提。流通市场是证券得以持续扩大发行的必要条件。此外,流通市场的交易价格制约和影响着证券的发行价格,是证券发行时需要考虑的重要因素。 证券市场的层次性还体现为区域分布、覆盖公司类型、上市交易制度以及监管要求的多样性。根据所服务和覆盖的上市公司类型,可分为全球性市场、全国性市场、区域性市场等类型; 根据上市公司规模、监管要求等差异,可分为主板市场、二板市场(创业板或高新企业板);根据交易方式,可以分为集中交易市场、柜台市场等。 2、品种结构 3、交易场所结构 按交易活动是否在固定场所进行,证券市场可分为有形市场和无形市场。 有形市场称作“场内市场”,是指有固定场所的证券交易所市场。有形市场的诞生是证券市场走向集中化的重要标志之一。 无形市场称作为“场外市场”,是指没有固定交易场所的市场。目前场内市场与场外市场之间的截然划分已经不复存在,出现了多层次的证券市场结构。 (三)证券市场的基本功能1证券市场被称为国民经济的“晴雨表”。 1、筹资、投资功能 筹资和投资是证券市场基本功能不可分割的两个方面,忽视其中任何一个方面都会导致市场的严重缺陷。 2、定价功能 证券的价格是证券市场上证券供求双方共同作用的结果。 3、资本配置功能 是指通过证券价格引导资本的流动从而实现资本的合理配置的功能。 二、证券市场参与者一、证券发行人 (一)公司(企业) 企业的组织形式可分为独资制、合伙制和公司制。现代股份制公司主要采取股份有限公司和有限责任公司两种形式。 其中,只有股份有限公司才能发行股票。 (二)政府和政府机构 随着国家干预经济理论的兴起,政府(中央政府和地方政府)和中央政府直属机构已成为证券发行的重要主体之一,但政府发行证券的品种仅限于债券。 由于中央政府拥有税收、货币发行等特权。通常情况下,中央政府债券不存在违约风险,因此,这类证券被视为“无风险证券”,相对应的证券收益率被称为“无风险利率”,是金融市场上最重要的价格指标。 中央银行作为证券发行主体,主要涉及两类证券。 第一,类是中央银行股票,第二类是中央银行出于调控货币供给量目的而发行的特殊债券。中国人民银行从2003年起发行中央银行票据。 二、证券投资人 (一)机构投资者 1、政府机构:政府债券、金融债券 作为政府机构,参与证券投资的目的主要是为了调剂资金余缺和进行宏观调控。 中央银行以公开市场操作作为政策手段,通过买卖政府债券或金融债券,影响货币供应量进行宏观调控。 我国国有资产管理部门或其授权部门持有国有股,履行国有资产的保值增值和通过国家控股、参股来支配更多社会资源的职责。 从各国的具体实践看,出于维护金融稳定的需要,政府还可成立或指定专门机构参与证券市场交易,减少非理性的市场震荡。 2、金融机构 (1)证券经营机构 (2)银行业金融机构 根据《中华人民共和国商业银行法》规定,银行业金融机构可用自有资金买卖政府债券和金融债券。除国家另有规定外,在中华人民共和国境内不得从事信托投资和证券经营业务,不得向非自用不动产投资或者向非银行金融机构和企业投资。 《中华人民共和国外资银行管理条例》规定,外商独资银行、中外合资银行可买卖政府债券、金融债券,买卖股票以外的其他外币有价证券。 银行业金融机构因处置贷款质押资产而被动持有的股票,只能单向卖出。《商业银行个人理财业务管理暂行办法》规定,商业银行可以向个人客户提供综合理财服务,向特定目标客户群销售理财计划,接受客户的委托和授权,按照与客户事先约定的投资计划和方式进行投资和资产管理。 (3)保险经营机构 (4) 合格境外机构投资者 (QFII) QFII 制度是一国(地区)在货币没有实现完全可自由兑换、资本项目尚未完全开放的情况下,有限度地引进外资、开放资本市场的一项过渡性的制度。 合格境外机构投资者的境内股票投资,应当遵守中国证监会规定的持股比例限制和国家其他有关规定:单个境外投资者通过合格境外机构投资者持有一家上市公司股票的,持股比例不得超过该公司股份总数的10%;所有境外投资者对单个上市公司A股的持股比例总和,不超过该上市公司股份总数的20%。同时,境外投资者根据《外国投资者对上市公司战略投资管理办法》对上市公司战略投资的,其战略投资的持股不受上述比例限制。 (5) 主权财富基金 :中国投资责任公司被视为中国主权财富基金的发端。07年成立 (6) 其他金融机构 :包括信托投资公司、企业集团财务公司、金融租赁公司等等。 目前尚未批准金融租赁公司从事证券投资业务。 3、企业和事业法人 我国现行的规定是,各类企业可参与股票配售,也可投资于股票二级市场;事业法人可用自有资金和有权自行支配的预算外资金进行证券投资。 4、各类基金:证券投资基金、社保基金、企业年金和社会公益基金 (1)证券投资基金。 (2)社保基金。在大多数国家,社保基金分为两个层次: 其一是国家以社会保障税等形式征收的全国性基金; 其二是由企业定期向员工支付并委托基金公司管理的企业年金。 在我国,社保基金也主要由两部分组成:一部分是 社会保障基金 。另一部分是 社会保险基金 。社保基金的投资范围包括银行存款、国债、证券投资基金、股票、信用等级在投资级以上的企业债、金融债等有价证券。 (3)企业年金:投资的范围和投资的比例。 (4)社会公益基金 (二)个人投资者 个人投资者是指从事证券投资的社会自然人,他们是 证券市场最广泛的投资者 。 (三)投资者的风险特性与投资适当性 不同的投资者对风险的态度各不相同,理论上可以将其区分为风险偏好型、风险中立型和风险回避型三种类型。 实践中,金融机构通常采用客户调查问卷、产品风险评估与充分披露等方法,根据客户分级和资产分级匹配原则,避免误导投资者和错误销售。 投资适当性的要求就是“适合的投资者购买恰当的产品” 。 三、证券市场中介机构 (一)证券公司 证券公司又称证券商,是指依照《公司法》、《证券法》规定并经国务院证券监督管理机构批准经营证券业务的有限责任公司或股份有限公司。 (二)证券服务机构 证券投资咨询机构、财务顾问机构、资信评级机构、资产评估机构、会计师事务所、律师事务所等。 四、自律性组织 (一)证券交易所 (二)证券业协会 中国证券业协会是依法注册的具有独立法人地位的、由经营证券业务的金融机构自愿组成的行业性自律组织,是社会团体法人。中国证券业协会采取会员制的组织形式,协会的权力机构为全体会员组成的会员大会。中国证券业协会的自律管理体现在保护行业共同利益、促进行业共同发展方面,具体表现为:对会员单位的自律管理、对从业人员的自律管理和对代办股份转让系统的自律管理。 (三)证券登记结算机构 中国证券登记结算公司是为证券交易提供集中的登记、托管与结算服务,不以营利为目的的法人。 五、证券监管机构 中华人民共和国证券监督管理委员会及其派出机构。 二、股票市场概述第一节 股票的特征与类型一、股票的定义 (一)股票定义 股票是一种有价证券,它是股份有限公司签发的证明股东所持有股份的凭证。 我国《公司法》规定,股票采用纸面形式或者国务院证券监督管理机构规定的其他形式。 股票应载明的事项主要有:公司名称、公司成立的日期、股票种类、票面金额及代表的股份数、股票的编号。 股票由法定代表人签名,公司盖章。发起人的股票,应当标明“发起人股票”字样。 (二)股票的性质 1、股票是 有价证券 。股票具有有价证券的特征: 第一,虽然股票本身没有价值,但股票是一种代表财产权的有价证券; 第二,股票与它代表的财产权有不可分离的关系。 2、股票是 要式证券 。 股票应具备《公司法》规定的有关内容,如果缺少规定的要件,股票就无法律效力。 3、股票是 证权证券 。证券可分为设权证券和证权证券。 设权证券是指证券所代表的权利本来不存在,而是随着证券的制作而产生,即权利的发生是以证券的制作和存在为条件的。 证权证券是指证券是权利的一种物化的外在形式,它是权利的载体,权利是已经存在的。 4、股票是 资本证券 。 股票是投入股份公司资本份额的证券化,属于资本证券。股票独立于真实资本之外 ,在股票市场进行着独立的价值运动,是一种虚拟资本。 5、股票是 综合权利证券 。 股票不属于物权证券,也不属于债权证券,而是一种综合权利证券。股东权是一种综合权利,股东依法享有资产收益、重大决策、选择管理者等权利。 (三)股票的特征 1、 收益性 :是 最基本的特征 。股票的收益来源可分成两类:一是来自于股份公司。二是来自于股票流通。 2、 风险性 :股票风险的内涵是股票投资收益的不确定性,或者说实际收益与预期收益之间的偏离程度。风险不等于损失。 3、 流动性 :判断流动性强弱的三个方面:市场深度、报价紧密度、价格弹性(恢复能力)。 需要注意的是,由于股票的转让可能受各种条件或法律法规的限制,因此,并非所有股票都具有相同的流动性。通常情况下,大盘股流动性强于小盘股,上市公司股票的流动性强于非上市公司股票,而上市公司股票又可能因市场或监管原因而受到转让限制,从而具有不同程度的流动性。 4、 永久性 :是指股票所载有权利的有效性是始终不变的,因为它是一种无期限的法律凭证。 5、 参与性 :是指股票持有人有权参与公司重大决策的特性。 二、股票的类型 (一)普通股票和优先股。 按股东享有权利的不同,股票可以分为普通股票和优先股票 。 1、普通股票。普通股票是最基本、最常见的一种股票,其持有者享有股东的基本权利和义务。在公司盈利较多时,普通股票股东可获得较高的股利收益,但在公司盈利和剩余财产的分配顺序上列在债权人和优先股票股东之后,故其承担的风险也比较高。与优先股票相比,普通股票是标准的股票,也是风险较大的股票。 2、优先股票。优先股票是一种特殊股票。优先股票的股息率是固定的,其持有者的股东权利受到一定限制。但在公司盈利和剩余财产的分配上比普通股票股东享有优先权。 (二)记名股票和不记名股票 股票按是否记载股东姓名,可分为记名股票和不记名股票。 1、记名股票。 我国《公司法》规定,公司发行的股票可以为记名股票,也可以为无记名股票。股份有限公司向发起人、法人发行的股票,应当为记名股票,并应当记载该发起人、法人的名称或者姓名,不得另立户名或者以代表人姓名记名。公司发行记名股票的,应当置备股东名册,记载下列事项:股东的姓名或者名称及住所、各股东所持股份数、各股东所持股票的编号、各股东取得股份的日期。 记名股票有如下 特点 : (1)股东权利归属于记名股东。 (2)可以一次或分次缴纳出资。 (3)转让相对复杂或受限制(股东以背书方式或者法律、行政法规规定的其他方式转让)。 (4)便于挂失,相对安全。 2、无记名股票。 我国《公司法》规定,发行无记名股票的,公司应当记载其股票数量、编号及发行日期。 无记名股票有如下特点: (1)股东权利归属股票的持有人。 (2)认购股票时要求一次缴纳出资。 (3)转让相对简便(交付转让)。 (4)安全性较差。 (三)有面额股票和无面额股票 1、有面额股票。所谓有面额股票,是指在股票票面上记载一定金额的股票。这一记载的金额也称为 票面金额、票面价值或股票面值 。我国《公司法》规定,股份有限公司的资本划分为股份,每一股的金额相等。 有面额股票有如下 特点 : (1)可以明确表示每一股所代表的股权比例。 (2)为股票发行价格的确定提供依据。我国《公司法》规定股票发行价格可以按票面金额,也可以超过票面金额,但不得低于票面金额。有面额股票的票面金额就是股票发行价格的最低界限。 2、无面额股票。是指在股票票面上不记载股票面额,只注明它在公司总股本中所占比例的股票。无面额股票也称为 比例股票或份额股票 。无面额股票淡化了票面价值的概念,但仍然有内在价值,它与有面额股票的差别仅在表现形式上。也就是说,它们都代表着股东对公司资本总额的投资比例,股东享有同等的股东权利。 目前世界上很多国家(包括中国)的公司法规定不允许发行这种股票。 无面额股票有如下特点: (1)发行或转让价格较灵活; (2)便于股票分割。 第二节 股票的价值与价格一、股票的价值 (一)股票的票面价值 又称面值,即在股票票面上标明的金额。该种股票被称为有面额股票。 如果以面值作为发行价,称为平价发行,此时公司发行股票募集的资金等于股本的总和,也等于面值总和。发行价值高于面值称为溢价发行,募集的资金中等于面值总和的部分计入资本账户,以超过股票票面金额的发行价值发行股份所得的溢价款列为公司资本公积金。 (二)股票的账面价值 股票的账面价值又称股票净值或每股净资产 ,在没有优先股的条件下,每股账面价值等于公司净资产除以发行在外的普通股票的股数。 但是通常情况下,并不等于股票价格。 主要原因有两点: 一是会计价值通常反映的是历史成本或者按某种规则计算的公允价值,并不等于公司资产的实际价格; 二是账面价值并不反映公司的未来发展前景。 (三)股票的清算价值 股票的清算价值是公司清算时每一股份所代表的实际价值 。 (四)股票的内在价值 股票的内在价值即理论价值,也即股票未来收益的现值。股票的内在价值决定股票的市场价格,股票的市场价格总是围绕其内在价值的波动。 二、股票的价格 (一)股票的理论价格 股票及其他有价证券的理论价格就是以一定的必要收益率计算出来的未来收入的现值。 (二)股票的市场价格 股票的市场价值一般是指股票在二级市场上交易的价格。 供求关系是股票价格最直接的影响因素,其他因素都是通过作用于供求关系而影响股票价格的。 (三)影响股票价格的因素 供求关系是最直接的影响因素。根本因素:预期。 分析股价变动的因素,就是要梳理影响供求关系变化的深层次原因。 三、影响股价变动的基本因素 (一)公司经营状况 股价公司的经营现状和未来发展是股票价格的基石。 1、公司治理水平和管理层质量。 对于公司治理情况的分析主要包括公司股东、管理层、员工及其他外部利益相关者之间的关系及其制衡状况,公司董事会、监事会构成及运作等因素。 2、公司竞争力。 最常用的公司竞争力分析框架是所谓的SWOT分析,它提出了4个考察维度,即公司经营中存在的优势、劣势、机会与威胁。 3、行业生命周期。 幼稚期、成长期、成熟期、稳定期4个阶段。 (三)宏观经济与政策因素 宏观经济发展水平和状况是影响股票价格的重要因素。宏观经济影响股票价值的特点是波及范围广、干扰程度深、作用机制复杂和股价波动幅度较大。 1、经济增长。当一国或地区的经济运行势态良好时,大多数企业的经营状况也较良好,它们的股价会上升;反之股价会下降。 2、经济周期循环。社会经济运行经常表现为扩张与收缩的周期性交替,每个周期一般都要经过高涨、衰退、萧条、复苏四个阶段。 股价的变动通常比实际经济的繁荣或衰退领先一步。所以股价水平已成为经济周期变动的灵敏信号或称先导性指标 。 3、货币政策。 中央银行通常采用存款准备金制度、再贴现政策、公开市场业务等货币政策手段调控货币供应量,从而实现发展经济、稳定货币等政策目标。 (1)中央银行提高法定存款准备金率,商业银行可贷资金减少,市场资金趋紧,股价下降;中央银行降低法定存款准备金率,商业银行可贷资金增加,市场资金趋松,股价上升。 (2)中央银行通过采取再贴现政策手段,提高再贴现率,收紧银根、使商业银行得到的中央银行贷款减少,市场资金趋紧;再贴现率又是基准利率,它的提高必定使市场利率随之提高。 (3)中央银行通过公开市场业务大量出售证券,收紧银根,在收回中央银行供应的基础货币的同时又增加证券的供应,使证券价格下降。 4、财政政策。财政政策对股票价格的影响包括: 其一,通过扩大财政赤字、发行国债筹集资金,增加财政支出,刺激经济发展;或是通过增加财政盈余或降低赤字,减少财政支出,抑制经济增长,以此影响股价。 其二,通过调节税率影响企业利润和股息。 其三,干预资本市场各类交易适用的税率,如利息税、资本利得税、印花税等。 其四,国债发行量会改变证券市场的证券供应和资金需求,从而间接影响股价。 第三节 普通股票和优先股票一、普通股票 (一)普通股票股东的权利 普通股票股东的权利: 1、公司重大决策参与权 股东基于股票的持有而享有股东权,这是一种综合权利,其中首要的是可以以股东身份参与股份公司的重大事项决策。 股东大会一般每年定期召开一次年会,当出现董事会认为必要或监事会提议召开、单独或者合计持有公司10%以上股份的股东请求等情形时,也可召开临时股东大会。股东会议由股东按出资比例行使表决权,但公司章程另有规定的除外。股东出席股东大会,所持每股份有一表决权。股东大会作出决议必须经出席会议的股东所持表决权过 半数通过 。股东大会作出修改公司章程、增加或减少注册资本的决议,以及公司合并、分立、解散或者变更公司形式的决议,必须经出席会议的股东所持表决权的2/3以上通过。股东大会选举董事、监事,可以依照公司章程的规定或者股东大会的决议,实行累积投票制。累积投票制是指股东大会选举董事或者监事时,每一股份拥有与应选董事或者监事人数相同的表决权,股东拥有的表决权可以集中使用。股东可以亲自出席股东大会,也可以委托代理人出席。 对于规定的上市公司重大事项,必须经全体股东大会表决通过,并经参加表决的社会公众股股东所持表决权的半数以上通过,方可实施或提出申请 。规定的上市公司重大事项分为5类:增发新股、发行可转债、配股;重大资产重组,购买的资产总价较所购买资产经审计的账面净值溢价达到或超过20%的;股东以其持有的上市公司股权偿还其所欠该公司的债务;对上市公司有重大影响的附属企业到境外上市;在上市公司发展中对社会公众股股东利益有重大影响的相关事项。 2、公司资产收益权和剩余资产分配权 这个权利表现在: (1)普通股股东有权按照实缴的出资比例分配红利,但全体股东约定不按照出资比例分取红利的除外; (2)普通股股东在股份公司解散清算时,有权要求取得公司的剩余资产。 我国有关法律规定,公司缴纳所得税后的利润,在支付普通股票的红利之前,应按如下顺序分配:弥补亏损,提取法定公积金,提取任意公积金。 按我国《公司法》规定,公司财产在分别支付清算费用、职工的工资、社会保险费用和法定补偿金,缴纳所欠税款,清偿公司债务后的剩余财产,按照股东持有的股份比例分配。公司财产在未按照规定清偿前,不得分配给股东。 3.其他权利 主要权利: 第一,股东有权查阅公司章程、股东名册等。(查阅权、建议权和质询权) 第二,股东持有的股份可依法转让。(依法转让权) 第三,公司为增加注册资本发行新股时,股东有权按照实缴的出资比例认购新股。(优先认股权) 优先认股权是指当股份公司为增加公司资本而决定增加发行新的股票时,原普通股股东享有的按其持股比例、以低于市价的某一特定价格优先认购一定数量新发行股票的权利。赋予股东这种权利有两个 主要目的 :一是能保证普通股股东在股份公司中保持原有的持股比例;二是能保护原普通股股东的利益和持股价值。 享有优先认股权的股东可以有 三种选择 : 一是行使权利来认购新发行的普通股票; 二是如果法律允许,可以将该权利转让给他人,从中获得一定的报酬; 三是不行使此权利而听任其过期失效。普通股票股东是否具有优先认股权,取决于认购时间与股权登记日的关系。 二、优先股票 (一)优先股定义 优先股票与普通股票相对应,是指股东享有某些优先权利的股票。 首先,对股份公司而言,发行优先股票的作用在于可以筹集长期稳定的公司股本,又因其股息率固定,可以减轻股息的分派负担。 另外,优先股票股东无表决权,这样可以避免公司经营决策权的改变和分散。 其次,对投资者而言,由于优先股票的股息收益稳定可靠,而且在财产清偿时也先于普通股票股东,因而风险相对较小,不失为一种较安全的投资对象。 (二)优先股票的特征 1、股息率固定。普通股票的股息是不固定的,它取决于股份公司的经营状况和盈利水平。 2、股息分派优先。在股份公司盈利分配顺序上,优先股排在普通股票之前。 3、剩余资产分配优先。当股份公司因解散或破产进行清算时,在对公司剩余资产的分配上,优先股票股东排在债权人之后、普通股票股东之前。 4、一般无表决权。优先股票股东权利是受限制的,最主要的是表决权限制。 (三)优先股票的种类 1、 累积优先股票和非累积优先股票 。 这种分类的依据是优先股股息在当年未能足额分派时,能否在以后年度补发。 所谓累积优先股票,是指历年股息累积发放的优先股票。 非累积优先股票,是指股息当年结清、不能累积发放的优先股票。非累积优先股票的特点是股息分派以每个营业年度为界,当年结清。 2、 参与优先股票和非参与优先股票 。 这种分类的依据是优先股票在公司盈利较多的年份里,除了获得固定的股息以外,能否参与或部分参与本期剩余盈利的分配。 参与优先股票,是指优先股票股东除了按规定分得本期固定股息外,还有权与普通股股东一起参与本期剩余盈利分配的优先股票。 非参与优先股票,是指除了按规定分得本期固定股息外,无权再参与对本期剩余盈利分配的优先股票。非参与优先股票是一般意义上的优先股票,其优先权不是体现在股息多少上,而是在分配顺序上。 3、 可转换优先股票和不可转换优先股票 。 这种分类的 依据是优先股票在一定的条件下能否转换成其他品种 。 可转换优先股票,是指发行后,在一定条件下允许持有者将它转换成其他种类股票的优先股票。在大多数情况下,股份公司的转换股票是由优先股票转换成普通股票,或者由某种优先股票转换成另一种优先股票。 不可转换优先股票,是指发行后不允许其持有者将它转换成其他种类股票的优先股票。不可转换优先股票与转换优先股票不同,它没有给投资者提供改变股票种类的机会。 4、 可赎回优先股票和不可赎回优先股票 。 这种分类的依据是在一定条件下, 该优先股票能否由原发行的股份公司出价赎回 。 股份公司一旦赎回自己的股票,必须在短期内予以注销。 5、 股息率可调整优先股票和股息率固定优先股票 。 这种分类的 依据是股息率是否允许变动 。 第四节 我国的股票类型一、我国的股票类型 (一)按投资主体性质分类 1、国家股 国家股是指有权代表国家投资的部门或机构以国有资产向公司投资形成的股份,包括公司现有国有资产投资形成的股份。 国家股从资金来源上看,主要有三个方面:第一,现有国有企业改组为股份公司时所拥有的净资产;第二,现阶段有权代表国家投资的政府部门向新组建的股份公司的投资;第三,经授权代表国家投资的投资公司、资产经营公司、经济实体性总公司等机构向新组建股份公司的投资。 三、债券市场概述第一节 债券的特征与类型一、债券的定义与特征 (一)债券的定义 债券是一种有价证券,是社会各类经济主体为筹集资金而向债券投资者出具的、承诺按一定利率定期支付利息并到期偿还本金的债权债务凭证。 债券上规定资金借贷的权责关系主要有三点: 第一,所借贷货币的数额; 第二,借款时间; 第三,在借贷时间内应有的补偿或代价是多少(即债券的利息)。 债券包含四个方面的含义: 第一,发行人是借入资金的经济主体; 第二,投资者是出借资金的经济主体; 第三,发行人需要在一定时期付息还本; 第四,债券反映了发行者和投资者之间的债权、债务关系,而且是这一关系的法律凭证。 债券的基本性质 1、债券属于有价证券。首先,债券反映和代表一定的价值。其次,债券与其代表的权利联系在一起。 2、债券是一种虚拟资本。 3、债券是债权的表现。 (二)债券的票面要素 1、债券的票面价值。债券的票面价值是债券票面标明的货币价值,是债券发行人承诺在债券到期日偿还给债券持有人的金额。 债券的票面价值要标明的内容主要有:要标明币种,要确定票面的金额。票面金额大小不同,可以适应不同的投资对象,同时也会产生不同的发行成本。 票面金额定得较小,有利于小额投资者,购买持有者分布面广,但债券本身的印刷及发行工作量大,费用可能较高; 票面金额定得较大,有利于少数大额投资者认购,且印刷费用等也会相应减少,但使小额投资者无法参与。 因此,债券票面金额的确定也要根据债券的发行对象、市场资金供给情况及债券发行费用等因素综合考虑。 2、债券的到期期限。债券到期期限是指债券从发行之日起至偿清本息之日止的时间,也是债券发行人承诺履行合同义务的全部时间。 决定偿还期限的主要因素:资金使用方向、市场利率变化、债券变现能力。 一般来说,当未来市场利率趋于下降时,应发行期限较短的债券; 而当未来市场利率趋于上升时,应发行期限较长的债券,这样有利于降低筹资者的利息负担。 3、债券的票面利率 影响票面利率的因素: 第一,借贷资金市场利率水平。 第二,筹资者的资信。 第三,债券期限长短。 一般来说,期限较长的债券流动性差,风险相对较大,票面利率应该定得高一些;而期限较短的债券流动性强,风险相对较小,票面利率就可以定得低一些。 4、债券发行者名称 这一要素指明了该债券的债务主体。 需要说明的是,以上4个要素虽然是债券票面的基本要素,但它们并非一定在债券票面上印制出来。在许多情况下,债券发行者是以公布条例或公告形式向社会公开宣布某债券的期限与利率。 此外,债券票面上有时还包含一些其他要素,如,附有赎回选择权、附有出售选择权、附有可转换条款、附有交换条款、附有新股认购条款等等。 (三)债券的特征 1、 偿还性 。偿还性是指债券有规定的偿还期限,债务人必须按期向债权人支付利息和偿还本金。这一特征与股票的永久性有很大的区别。 2、 流动性 。是指债券持有人可按需要和市场的实际状况,灵活地转让债券,以提前收回本金和实现投资收益。流动性首先取决于市场为转让所提供的便利程度;其次取决于债券在迅速转变为货币时,是否在以货币计算的价值上蒙受损失。 安全性 。 一般来说,具有高度流动性的债券同时也是较安全的,因为它不仅可以迅速地转换为货币,而且还可以按一个较稳定的价格转换。 债券投资不能收回的两种情况: 第一,债务人不履行债务,即债务人不能按时足额履行约定的利息支付或者偿还本金。 第二,流通市场风险,即债券在市场上转让时因价格下跌而承受损失。 4、 收益性 。 在实际经济活动中,债券收益可以表现为三种形式: 一是利息收入; 二是资本损益,即债权人到期收回的本金与买入债券或中途卖出债券与买入债券之间的价差收入。 三是再投资收益。 二、债券的分类 (一)按发行主体分类 1、政府债券。政府债券的发行主体是政府。中央政府发行的债券称为国债,其主要用途是解决由政府投资的公共设施或重点建设项目的资金需要和弥补国家财政赤字。有些国家把政府担保的债券也划归为政府债券体系,称为政府保证债券。 2、金融债券。发行主体是银行或非银行的金融机构。金融机构一般有雄厚的资金实力,信用度较高,因此,金融债券往往也有良好的信誉。它们发行债券的目的的主要有:筹资用于某种特殊用途;改变本身的资产负债结构。金融债券的期限以中期较为多见。 3、公司债券。是公司依照法定程序发行、约定在一定期限还本付息的有价证券。 (二)按计息与付息方式分类:零息债券、附息债券、息票累积债券 1、零息债券。也称零息票债券,指债券合约未规定利息支付的债券。通常,这类债券以低于面值的价格发行和交易,债券持有人实际上是以买卖(到期赎回)价差的方式取得债券利息。 2、附息债券。债券合约中明确规定,在债券存续期内,对持有人定期支付利息(通常每半年或每年支付一次)。按照计息方式的不同,这类债券还可细分为固定利率债券和浮动利率债券,有些付息债券可以根据合约条款推迟支付定期利率,故称为缓息债券。 3、息票累积债券:与附息债券相似,这类债券也规定了票面利率,但是,债券持有人必须在债券到期时一次性获得还本付息,存续期间没有利息支付。 (三)按债券形态分类 1、实物债券 :实物债券是一种具有标准格式实物券面的债券。在标准格式的债券券面上,一般印有债券面额、债券利率、债券期限、债券发行人全称、还本付息方式等各种债券票面要素。有时债券利率、债券期限等要素也可以通过公告向社会公布,而不在债券券面上注明。 2、凭证式债券 :凭证式债券的形式是债权人认购债券的一种收款凭证,而不是债券发行人制定的标准格式的债券。 特点:可记名、挂失、不能上市流通。可以到原购买网点提前兑取。 3、记账式债券 :记账式债券是没有实物形态的债券,利用证券账户通过电脑系统完成债券发行、交易及兑付的全过程。 特点:可以记名、挂失,安全性较高。发行时间短,发行效率高,交易手续简便,成本低,交易安全。 三、债券与股票的比较 (关注多选题) (一)债券与股票的相同点 1、两者都属于有价证券。 2、两者都是筹措资金的手段。 3、两者的收益率相互影响。 (二)债券与股票的区别 1、二者权利不同:债券是债权凭证。股票则不同,股票是所有权凭证。 2、二者目的不同:发行债券是公司追加资金的需要,它属于公司的负债,不是资本金。发行股票则是股份公司创立和增加资本的需要,筹措的资金列入公司资本。 3、二者期限不同:债券有偿还期,而股票具有永久性。 4、二者收益不同:债券利息固定,而股票的红利股息不固定。 5、二者风险不同:股票风险较大,债券风险相对较小。 因为: 第一,债券利息是公司的固定支出,属于费用范围;股票的股息红利是公司利润的一部分,公司有盈利才能支付,而且支付顺序列在债券利息支付和纳税之后。 第二,倘若公司破产,清理资产有余额偿还时,债券偿付在前,股票偿付在后。 第三,在二级市场上,债券因其利率固定,期限固定,市场价格也较稳定;而股票无固定期限和利率,受各种宏观因素和微观因素的影响,市场价格波动频繁,涨跌幅度较大。 第二节 政府债券一、政府债券概述 (一)政府债券的定义 政府债券的发行主体是政府,它是指政府财政部门或其他代理机构为筹集资金,以政府名义发行的、承诺在一定时期支付利息和到期还本的债务凭证。 中央政府发行的债券称为中央政府债券或者国债,地方政府发行的债券称为地方政府债券;有时也将二者统称为公债。 (二)政府债券的性质: 第一,从形式上看,政府债券是一种有价证券,它具有债券的一般性质。 第二,从功能上看,政府债券最初仅仅是政府弥补赤字的手段,但在现代商品经济条件下,政府债券已成为政府筹集资金、扩大公共事业开支的重要手段,并且随着金融市场的发展,逐渐具备了金融商品和信用工具的职能,成为国家实施宏观经济政策、进行宏观调控的工具。 (三)政府债券的特征 1、安全性高。 在各类债券中,政府债券的信用等级是最高的,通常被称为“金边债券”。投资者购买政府债券,是一种较安全的投资选择。 2、流通性强。 由于政府债券的信用好、竞争力强,市场属性好,所以,许多国家政府债券的二级市场十分发达,一般不仅允许在证券交易所上市交易,还允许在场外市场买卖。 3.收益稳定。 4.免税待遇。 在政府债券与其他证券名义收益率相等的情况下,如果考虑税收因素,持有政府债券的投资者可以获得更多的实际投资收益。 三、证券投资基金第一节 证券投资基金概述一、证券投资基金 (一)证券投资基金的产生与发展 证券投资基金是指通过公开发售基金份额募集资金,由基金托管人托管,由基金管理人管理和运用资金,为基金份额持有人的利益,以资产组合方式进行证券投资的一种利益共享、风险共担的集合投资方式。 各国对证券投资基金的称谓不尽相同,如美国称“共同基金”,英国和我国香港地区称“单位信托基金”,日本和我国台湾地区则称“证券投资信托基金”等。 英国1868年由政府出面组建了海外和殖民地政府信托组织,公开向社会发售受益凭证。 基金起源于英国, 基金产业已经与银行业、证券业、保险业并驾齐驱,成为现代金融体系的四大支柱 。 (二)我国证券投资基金业发展概况 1997年11月,国务院颁布《证券投资基金管理暂行办法》;l998年3月,两只封闭式基金——基金金泰、基金开元设立,分别由国泰基金管理公司和南方基金管理公司管理。2004年6月1日,我国《证券投资基金法》正式实施。 证券投资基金业从此进入崭新的发展阶段,基金数量和规模迅速增长,市场地位日趋重要,呈现出下列特点: 1、基金规模快速增长,开放式基金后来居上,逐渐成为基金设立的主流形式。 2、基金产品差异化日益明显,基金的投资风格也趋于多样化。 3、中国基金业发展迅速,对外开放的步伐加快。 (三)证券投资基金的特点 1、集合投资。基金的特点是将零散的资金汇集起来,交给专业机构投资于各种金融工具,以谋取资产的增值。基金对投资的最低限额要求不高,投资者可以根据自己的经济能力决定购买数量,有些基金甚至不限制投资额大小。 2、分散风险。以科学的投资组合降低风险、提高收益是基金的另一大特点。 3、专业理财。将分散的资金集中起来以信托方式交给专业机构进行投资运作,既是证券投资基金的一个重要特点,也是它的一个重要功能。 二、证券投资基金的分类 (一)按基金的组织形式不同,基金可分为契约型基金和公司型基金。 契约型基金又称为单位信托,是指将投资者、管理人、托管人三者作为基金的当事人,通过签订基金契约的形式发行受益凭证而设立的一种基金。 公司型基金是依据基金公司章程设立,在法律上具有独立法人地位的股份投资公司。 公司型基金在组织形式上与股份有限公司类似,由股东选举董事会,由董事会选聘基金管理公司,基金管理公司负责管理基金的投资业务。 1、公司型基金的特点: (1)基金的设立程序类似于一般的股份公司,基金本身为独立法人机构。但不同于一般股份公司的是,它委托基金管理公司作为专业的财务顾问或管理公司来经营、管理基金资产。 (2)基金的组织结构与一般股份公司类似,设有董事会和持有人大会。基金资产归基金所有。 2、契约型基金与公司型基金的区别: (1)资金的性质不同。契约型基金的资金是通过发行基金份额筹集起来的信托财产;公司型基金的资金是通过发行普通股票筹集的公司法人资本。 (2)投资者的地位不同。契约型基金的投资者既是基金的委托人,又是基金的受益人,即享有基金的受益权。公司型基金的投资者对基金运作的影响比契约型基金的投资者大。 (3)基金的营运依据不同。契约型基金依据基金契约营运基金,公司型基金依据基金公司章程营运基金。 (二)按基金运作方式不同,基金可分为封闭式基金和开放式基金。 封闭式基金是指经核准的基金份额总额在基金合同期限内固定不变,基金份额可以在依法设立的证券交易场所交易,但基金份额持有人不得申请赎回原基金。 决定基金期限长短的因素主要有两个: 一是基金本身投资期限的长短。 二是宏观经济形势。 开放式基金是指基金份额总额不固定,基金份额可以在基金合同约定的时间和场所申购或者赎回的基金。 三、管理人与托管人 (一)基金管理人的概念 基金管理人是负责基金发起设立与经营管理的专业性机构,不仅负责基金的投资管理,而且承担着产品设计、基金营销、基金注册登记、基金估值、会计核算和客户服务等多方面的职责。 我国《证券投资基金法》规定, 基金管理人由依法设立的基金管理公司担任 。基金管理公司通常由证券公司、信托投资公司或其他机构等发起成立,具有独立法人地位。 基金管理人作为受托人,必须履行“诚信义务”。 基金管理人的目标函数是受益人利益的最大化 ,因而,不得出于自身利益的考虑损害基金持有人的利益。 (二)证券投资基金托管人 为充分保障基金投资者的权益,防止基金资产被挪作他用,各国的证券投资信托法规都规定必须由某一托管机构,即基金托管人来对基金管理机构的投资操作进行监督和保管基金资产。 (一)基金托管人的概念 基金托管人又称基金保管人,是根据法律法规的要求,在证券投资基金运作中承担资产保管、交易监督、信息披露、资金清算与会计核算等相应职责的当事人。基金托管人是基金持有人权益的代表,通常由有实力的商业银行或信托投资公司担任。基金托管人与基金管理人签订托管协议,在托管协议规定的范围内履行自己的职责并收取一定的报酬。 (二)基金托管人的条件 基金托管人应该是完全独立于基金管理机构、具有一定的经济实力、实收资本达到一定规模、具有行业信誉的金融机构。 我国《证券投资基金法》规定,基金托管人由依法设立并取得基金托管资格的商业银行担任。 (三)证券投资基金当事人之间的关系 (一)持有人与管理人之间的关系 基金份额持有人与基金管理人之间的关系是委托人、受益人与受托人的关系,也是所有者和经营者之间的关系。 (二)管理人与托管人之间的关系 基金管理人与托管人的关系是相互制衡的关系。基金管理人是基金的组织者和管理者,负责基金资产的经营,是基金运营的核心;托管人由主管机关认可的金融机构担任,负责基金资产的保管,依据基金管理机构的指令处置基金资产并监督管理人的投资运作是否合法合规。这种相互制衡的运行机制,有利于基金信托财产的安全和基金运用的绩效。但是这种机制的作用得以有效发挥的前提是基金托管人与基金管理人必须严格分开,由不具有任何关联关系的不同机构或公司担任,两者在财务上、人事上、法律地位上应该完全独立。 (三)持有人与托管人之间的关系 基金份额持有人与托管人的关系是委托与受托的关系,也就是说,基金份额持有人将基金资产委托给基金托管人保管。 2021年7月14日 长沙","link":"/2021/07/14/finance/financeStudy-EP9/"},{"title":"【深入浅出搞定 React】这一次,真正吃透 React 知识链路与底层逻辑","text":"深入浅出搞定 React这一次,真正吃透 React 知识链路与底层 此系列文章仅用于培训教学,请勿进行商用。 目录 EP01 JSX 代码是如何“摇身一变”成为 DOM 的? EP02 为什么 React 16 要更改组件的生命周期?(上) EP03 为什么 React 16 要更改组件的生命周期?(下) EP04 数据是如何在 React 组件之间流动的?(上) EP05 数据是如何在 React 组件之间流动的?(下) 在接下来的一段时间里,我们将一起深入 React 这个框架领域,完成从“小工”到“专家”的蜕变。 作为一名React 重度用户,与其说我对 React 源码、底层原理及周边生态有着较为深入的探究,不如说我对它们有着浓厚的兴趣。早期,我专注于性能优化和前端工程化,曾将线上大型应用性能提升率做到 40%,并基于 React 打造过团队新基建。此外,我还担任过多年一线前端面试官,积累了丰富的面试经验。 前端生涯至今,我从未停止过挑战自己的能力边界,始终乐于拥抱新的技术和工具,这不止让我保持了很好的职场竞争力,还使我深知新手从入门到精通过程中的痛点和难点。 作为一线开发者,我不认同技术圈时下盛行的“造名词”风气,痛恨故弄玄虚的“语言壁垒”——其实技术本身在多数情况下都是一些简单且有趣的东西,人们越是试图神化它,越容易脱离技术本质。这也是我在这个专栏中秉持和践行的一个原则。 学好 React,到底有多爽? 在过去的几年,“变化”始终是前端框架世界里的一号关键词:前有 jQuery 刚刚式微时各路神仙各显神通,后有 React/Vue/Angular 三分天下,如今又渐渐演变成了 React/Vue 两分天下。 而反观框架本身,你会发现 Vue、React 乃至 Angular 之间不仅写法越来越像,甚至在设计层面也日渐趋同——它们似乎像是约好了一样,在齐刷刷地朝着 WebComponents 标准前进。因此在展望未来的前端框架时,我们有充分的理由相信,属于前端框架的一号关键词终有一日会从“变化”发展为“稳定”或“标准化”。 在这样的趋势下,站在任何主观视角去拉踩任何前端框架的行为都是不合适的。学习者在意的不应是“哪个框架最牛”这样娱乐性的问题,而应该是学习的效用。 那么学习 React,将会带来什么样的效用呢? 利好个人职业生涯:大厂更喜欢 React 若单说岗位数量,我不敢妄言,但在一二线的互联网大厂中,React 的绝对优势凸显无疑。(比如,阿里就统一使用 React 作为底层技术栈,并且在内部紧密共建 React 生态。)国外的一份“2019 年度 JavaScript 趋势报告”中,React 也被评估为综合指数最高的前端框架: 在招聘上,大厂普遍青睐 React 人才,各种高薪职位中不乏“精通 React”“掌握 React”的字眼。作为前端,我们必须认识到这样一个现状:大厂(包括国内、国外)更喜欢 React,当我们立下一个有朝一日进大厂的志愿时,就意味着必须先下定决心搞定 React。 (信息来源:拉勾网) 强化项目实战能力:吃透 React,疑难杂症不在话下 面试时,React 相关的问题往往具备较高的区分度,能够在 React 方面脱颖而出的候选人并不多。很多时候,候选人似乎也确实不理解面试官“为什么要问得这么难”。比如常见的吐槽就有“我不读源码,不研究调用栈,用 React 写业务照样一把梭”这样的说辞。 确实,通过阅读 React 文档以及市面上一些“快速上手”“XX 实战”类型的学习材料,也能胜任一定的业务开发工作,但当业务复杂度攀升,“奇形怪状”的问题就会如雨后春笋般接连冒头。当你对 React 的运行机制不甚了解时,遇到这样的“疑难杂症”,就很容易懵掉。 面试环节的 React 深度考察,正是为了筛选出这些能够真正吃透 React、解决复杂问题的“高级玩家”:对 React 的理解深度,将决定着你所能解决的实战问题复杂度的上限。 普通开发者的“逆袭”机会:一个好的框架,就是最好的老师 这两年,许多中小型公司的前端工程师都面临着这样一个困境:业务含金量不高,老板又不重视,技术专项难以提取,架构机会更是没有……好像永远都没办法破局,难道我这辈子就这样了吗? 当然不是!当环境无法给我们提供优质的成长途径时,不妨自己尝试创造途径,比如: 深挖一个优质的前端框架,吃透其底层原理; 跟框架作者(React 团队)学架构思想、学编码规范、学设计模式。 React 正是一个优秀前端框架的典型 ,它在架构上融合了数据驱动视图、组件化、函数式编程、面向对象、Fiber 等经典设计“哲学”,在底层技术选型上涉及了 JSX、虚拟 DOM 等经典解决方案,在周边生态上至少涵盖了状态管理和前端路由两大领域的最佳实践。此外,它还自建了状态管理机制与事件系统,创造性地在前端框架中引入了 Hooks 思想...... React 十年如一日的稳定输出背后,有太多值得我们去吸收和借鉴的东西。 这个专栏我将带你掌握目前行业里相对前沿且具有代表性的一套东西,也是真正能够在你的职业生涯里沉淀下来、发挥长期效用的“底层技能”。 React 为什么这么难学? 在实际的招聘过程中,我和同事都曾经不止一次地发过这样的感慨:当下要想从社区招到一个符合预期的 React 开发,真的太太太太太太难了。 不知道你有没有观察到一个比较有趣的现象:Vue 知识体系/原理的相关内容百花齐放,但 React 知识体系/原理的相关内容却屈指可数。 市面上以 React 为主题的进阶性内容,大部分是在教会一系列 API 的基础上,描述如何去实战一个具体的项目,即专精于“使用”;而为数不多的源码分析性内容,虽然试图去拆解“原理”,但却往往伴随着细化到逐行代码的知识粒度,对读者的时间、耐力和既有水平(提炼知识、抽象知识的能力)都提出了很高的要求。 这些现象的背后,和 React 令人望而却步的庞大知识体系、精密复杂的底层原理以及长长的知识链路是分不开的。平心而论,学透 React 很难,而我想帮你解决的,也正是这个“难”。 课程设计:串联知识链路,讲透底层逻辑 我分享技术内容有两年多了,一直将“接地气、说人话”作为写作的第一要务,这个专栏更是将“把复杂的问题简单化、把琐碎的问题系统化”作为课程设计的核心原则。它并非平铺直叙的学习笔记,而是一次我与你之间的对话。 我希望做一个能够将学习体验与知识深度中和到最佳状态,切实为你带来学习效用的专栏。为此,专栏在设计层面做了以下几件事情。 设计原则:贴着大厂面试逻辑做大纲,贴着源码讲原理 大厂的 React 面试不是走过场,更不是“造火箭”式的炫技,它是最有“效用导向”的一个学习依据。如果能够将大厂面试的逻辑利用充分,我们将实现面试和应用的双重突破。 贴着源码讲原理,绝不是带着你死磕源码,源码 !== 原理,源码是代码,而原理是逻辑,代码是繁杂冗长的,原理却可以是简洁清晰的。在一些场景下,源码确实能够成为一个不错的教具,但阅读源码不是抵达原理的唯一途径。因此,必要时我会提取对你理解问题有帮助的源码;也会在一些场景下选取其他的教具,确保你能够用正确且高效的姿势抵达知识的重点。 专栏所涉及的原理,可以帮你解决实际工作中的大多数疑难杂症,也可以 Match 上大厂对资深前端工程师的技术深度的要求。 对于体系性较强的知识:创建足够充分的上下文 之前曾经读到过木心关于红楼梦的书评,印象极深:“红楼梦中的诗词像是水中摇曳的水草,美极。若是捞出来看,就干巴巴了。” 同样的道理也适用于 React 的知识链路:一些知识之所以难学,不是因为它有多复杂,而是因为理解它是需要上下文的。你若把它放到正确的上下文里,可能想通这件事也就是一瞬间的工夫;但如果你的学习上下文是断裂的,那么知识点本身自然会变得“干巴巴”,难以下咽。 对于复杂度较高的知识:用现象向原理提问 考虑来学习这门专栏的同学的学习阶段参差不齐,我在讲解复杂原理时,会尽量遵循“先提现象/问题,再挖原理”这个顺序,将困难知识的学习坡度降至最低。专栏中有一些内容的前置知识,我写得比较细,一般也会提前标明这是“先导知识”,如果你是高端玩家,直接跳过即可。 整个专栏的结构规划思路如下。 模块一:基础夯实。这部分内容涉及 React 的基本原理和源码,对大多数人普遍薄弱的、说不清楚的基础知识做深入浅出的讲解,帮你突破一些重点和难点。 模块二:核心原理。这部分内容源于日常开发中的疑难杂症、大厂面试的压轴难题,呈现出框架的底层逻辑和源码设计,我将用最少的篇幅来提取尽量多的信息。如果你想要从事一些高级岗位,或者精通 React,那么这块的内容你肯定避不开,而面试官能够通过这些内容,对候选人的能力做一个评价,甚至是定级。 模块三:周边生态。很多人用过 Redux、听说过 React-Router,但为什么要用它?其背后的工作原理、设计思想又是怎样的?专栏要讲的就是这部分比较有区分度的内容,面向使用过 React 全家桶,或者接触过还不具备熟练使用能力的前端工程师,解决你出了 Bug 却不知如何调试的问题。 模块四:生产实践。对于一个优秀的前端应用来说,性能和设计模式是永恒的主题,性能决定用户体验,设计模式决定研发效率。这部分将结合我团队的实践经验以及当下行业里推崇的最佳实践,为你输出实战观点。对于这些最佳实践,你不仅要知道怎么做,还要理解“为什么这么做”。学完个模块可以强化你的实际应用能力,提升自主研发创新实践的线索和灵感。 讲师寄语 我认为,学习的本质是重复,对于重要的知识,我会翻来覆去地说,想方设法让你记住它。所以,如果你在学习过程中发现某一块知识似曾相识或者早已埋下伏笔,多半意味着你发现了一个重难点,请牢牢抓住它。 为了将晦涩的知识转化为你手里实实在在的生产力,专栏的框架和内容也历经了多次的迭代和重构。每一次的果断推翻早期设想,每一次的重构表达逻辑,都是希望帮你更好地消化吸收这份知识。希望你也能够不吝耐心和智慧,顺利走完整个 React 学习曲线中最难的一段路。 到这里,我的故事就结束了,而你和我的故事才刚刚开始。欢迎在留言区分享你的前端经历,或者写写学习 React 过程中遇到的问题、想要学习的内容,让我们一起写出“我们”的故事,开启 React 奇幻之旅。","link":"/2022/09/20/frontEnd/inDepthAndSimpleReact/reactStudy-EP0/"},{"title":"EP01 JSX 代码是如何“摇身一变”成为 DOM 的?","text":"JSX 代码是如何“摇身一变”成为 DOM 的? 时下虽然接入 JSX 语法的框架越来越多,但与之缘分最深的毫无疑问仍然是 React。2013 年,当 React 带着 JSX 横空出世时,社区曾对 JSX 有过不少的争议,但如今,越来越多的人面对 JSX 都要说上一句“真香”!本课时我们就来一起认识下这个“真香”的 JSX,聊一聊“JSX 代码是如何‘摇身一变’成为 DOM 的”。 关于 JSX 的 3 个“大问题” 在日常的 React 开发工作中,我们已经习惯了使用 JSX 来描述 React 的组件内容。关于 JSX 语法本身,相信每位 React 开发者都不陌生。这里我用一个简单的 React 组件,来帮你迅速地唤醒自己脑海中与 JSX 相关的记忆。下面这个组件中的 render 方法返回值,就是用 JSX 代码来填充的: import React from \"react\"; import ReactDOM from \"react-dom\"; class App extends React.Component { render() { return ( <div className=“App”> <h1 className=“title”>I am the title</h1> <p className=“content”>I am the content</p> </div> ); }} const rootElement = document.getElementById(“root”);ReactDOM.render(<App />, rootElement); 由于本专栏的整体目标是帮助你在 React 这个领域完成从“小工”到“行家”的进阶,此处我无意再去带你反复咀嚼 JSX 的基础语法,而是希望能够引导你去探寻 JSX 背后的故事。针对这“背后的故事”,我总结了 3 个最具代表性和区分度的问题。 在开始正式讲解之前,我希望你能在自己心中尝试回答这 3 个问题: JSX 的本质是什么,它和 JS 之间到底是什么关系? 为什么要用 JSX?不用会有什么后果? JSX 背后的功能模块是什么,这个功能模块都做了哪些事情? 面对以上问题,如果你无法形成清晰且系统的思路,那么很可能是你把 JSX 想得过于简单了。大多数人只是简单地把它理解为模板语法的一种,但事实上,JSX 作为 React 框架的一大特色,它与 React 本身的运作机制之间存在着千丝万缕的联系。 上述 3 个问题的答案,就恰恰隐藏在这层“联系”中,在面试场景下,候选人对这层“联系”吃得透不透,是我们评价其在 React 方面是否“资深”的一个重要依据。 接下来,我就将带你由表及里地起底 JSX 相关的底层原理,帮助你吃透这层“联系”,建立起强大的理论自信。你可以将“能够用自己的话回答上面 3 个问题”来作为本课时的学习目标,待课时结束后,记得回来检验自己的学习成果^_^。 JSX 的本质:JavaScript 的语法扩展 JSX 到底是什么,我们先来看看 React 官网给出的一段定义: JSX 是 JavaScript 的一种语法扩展,它和模板语言很接近,但是它充分具备 JavaScript 的能力。 “语法扩展”这一点在理解上几乎不会产生歧义,不过“它充分具备 JavaScript 的能力”这句,却总让人摸不着头脑,JSX 和 JS 怎么看也不像是一路人啊?这就引出了“JSX 语法是如何在 JavaScript 中生效的”这个问题。 JSX 语法是如何在 JavaScript 中生效的:认识 Babel Facebook 公司给 JSX 的定位是 JavaScript 的“扩展”,而非 JavaScript 的“某个版本”,这就直接决定了浏览器并不会像天然支持 JavaScript 一样地支持 JSX。那么,JSX 的语法是如何在 JavaScript 中生效的呢?React 官网其实早已给过我们线索: JSX 会被编译为 React.createElement(), React.createElement() 将返回一个叫作“React Element”的 JS 对象。 这里提到,JSX 在被编译后,会变成一个针对 React.createElement 的调用,此时你大可不必急于关注 React.createElement 这个 API 到底做了什么(下文会单独讲解)。咱们先来说说这个“编译”是怎么回事:“编译”这个动作,是由 Babel 来完成的。 什么是 Babel 呢? Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。 —— Babel 官网 比如说,ES2015+ 版本推出了一种名为“模板字符串”的新语法,这种语法在一些低版本的浏览器里并不兼容。下面是一段模板字符串的示例代码: var name = \"Guy Fieri\"; var place = \"Flavortown\"; `Hello ${name}, ready for ${place}?`; Babel 就可以帮我们把这段代码转换为大部分低版本浏览器也能够识别的 ES5 代码: var name = \"Guy Fieri\"; var place = \"Flavortown\"; \"Hello \".concat(name, \", ready for \").concat(place, \"?\"); 类似的,Babel 也具备将 JSX 语法转换为 JavaScript 代码的能力。 那么 Babel 具体会将 JSX 处理成什么样子呢?我们不如直接打开 Babel 的 playground 来看一看。这里我仍然键入文章开头示例代码中的JSX 部分: 可以看到,所有的 JSX 标签都被转化成了 React.createElement 调用,这也就意味着,我们写的 JSX 其实写的就是 React.createElement,虽然它看起来有点像 HTML,但也只是“看起来像”而已。JSX 的本质是React.createElement这个 JavaScript 调用的语法糖,这也就完美地呼应上了 React 官方给出的“JSX 充分具备 JavaScript 的能力”这句话。 React 选用 JSX 语法的动机 换个角度想想,既然 JSX 等价于一次 React.createElement 调用,那么 React 官方为什么不直接引导我们用 React.createElement 来创建元素呢? 原因非常简单,我们来看一个相对复杂一些的组件的 JSX 代码和 React.createElement 调用之间的对比。它们各自的形态如下图所示,图中左侧是 JSX 代码,右侧是 React.createElement 调用: 你会发现,在实际功能效果一致的前提下,JSX 代码层次分明、嵌套关系清晰;而 React.createElement 代码则给人一种非常混乱的“杂糅感”,这样的代码不仅读起来不友好,写起来也费劲。 JSX 语法糖允许前端开发者使用我们最为熟悉的类 HTML 标签语法来创建虚拟 DOM,在降低学习成本的同时,也提升了研发效率与研发体验。 读到这里,相信你已经充分理解了“JSX 是 JavaScript 的一种语法扩展,它和模板语言很接近,但是它充分具备 JavaScript 的能力。 ”这一定义背后的深意。那么我们文中反复提及的 React.createElement 又是何方神圣呢?下面我们就深入相关源码来一窥究竟。 JSX 是如何映射为 DOM 的:起底 createElement 源码 在分析开始之前,你可以先尝试阅读我追加进源码中的逐行代码解析,大致理解 createElement 中每一行代码的作用: /** 101. React的创建元素方法 */ export function createElement(type, config, children) { // propName 变量用于储存后面需要用到的元素属性 let propName; // props 变量用于储存元素属性的键值对集合 const props = {}; // key、ref、self、source 均为 React 元素的属性,此处不必深究 let key = null; let ref = null; let self = null; let source = null; // config 对象中存储的是元素的属性 if (config != null) { // 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值 if (hasValidRef(config)) { ref = config.ref; } // 此处将 key 值字符串化 if (hasValidKey(config)) { key = ‘’ + config.key; } self = config.__self === undefined ? null : config.__self; source = config.__source === undefined ? null : config.__source; // 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面 for (propName in config) { if ( // 筛选出可以提进 props 对象里的属性 hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName) ) { props[propName] = config[propName]; } } } // childrenLength 指的是当前元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度 const childrenLength = arguments.length - 2; // 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了 if (childrenLength === 1) { // 直接把这个参数的值赋给props.children props.children = children; // 处理嵌套多个子元素的情况 } else if (childrenLength > 1) { // 声明一个子元素数组 const childArray = Array(childrenLength); // 把子元素推进数组里 for (let i = 0; i < childrenLength; i++) { childArray[i] = arguments[i + 2]; } // 最后把这个数组赋值给props.children props.children = childArray; } // 处理 defaultProps if (type && type.defaultProps) { const defaultProps = type.defaultProps; for (propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } // 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数 return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props, );} 上面是对源码细节的初步展示,接下来我会带你逐步提取源码中的关键知识点和核心思想。 入参解读:创造一个元素需要知道哪些信息 我们先来看看方法的入参: export function createElement(type, config, children) createElement 有 3 个入参,这 3 个入参囊括了 React 创建一个元素所需要知道的全部信息。 type:用于标识节点的类型。它可以是类似“h1”“div”这样的标准 HTML 标签字符串,也可以是 React 组件类型或 React fragment 类型。 config:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中。 children:以对象形式传入,它记录的是组件标签之间嵌套的内容,也就是所谓的“子节点”“子元素”。 如果文字描述使你觉得抽象,下面这个调用示例可以帮你增进对概念的理解: React.createElement(\"ul\", { // 传入属性键值对 className: \"list\" // 从第三个入参开始往后,传入的参数都是 children }, React.createElement(\"li\", { key: \"1\" }, \"1\"), React.createElement(\"li\", { key: \"2\" }, \"2\")); 这个调用对应的 DOM 结构如下: <ul className=\"list\"> <li key=\"1\">1</li> <li key=\"2\">2</li> </ul> 对入参的形式和内容有了大致的把握之后,下面我们继续来讲解 createElement 的函数逻辑。 createElement 函数体拆解 前面你已经阅读过 createElement 源码细化到每一行的解读,这里我想和你探讨的是 createElement在逻辑层面的任务流转。针对这个过程,我为你总结了下面这张流程图: 这个流程图,或许会打破不少同学对 createElement 的幻想。在实际的面试场景下,许多候选人由于缺乏对源码的了解,谈及 createElement 时总会倾向于去夸大它的“工作量”。但其实,相信你也已经发现了,createElement 中并没有十分复杂的涉及算法或真实 DOM 的逻辑,它的每一个步骤几乎都是在格式化数据。 说得更直白点,createElement 就像是开发者和 ReactElement 调用之间的一个“转换器”、一个数据处理层。它可以从开发者处接受相对简单的参数,然后将这些参数按照 ReactElement 的预期做一层格式化,最终通过调用 ReactElement 来实现元素的创建。整个过程如下图所示: 现在看来,createElement 原来只是个“参数中介”。此时我们的注意力自然而然地就聚焦在了 ReactElement 上,接下来我们就乘胜追击,一起去挖一挖 ReactElement 的源码吧! 出参解读:初识虚拟 DOM 上面已经分析过,createElement 执行到最后会 return 一个针对 ReactElement 的调用。这里关于 ReactElement,我依然先给出源码 + 注释形式的解析: const ReactElement = function(type, key, ref, self, source, owner, props) { const element = { // REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement $$typeof: REACT_ELEMENT_TYPE, <span class="hljs-comment">// 内置属性赋值</span> type: type, key: key, ref: ref, props: props, <span class="hljs-comment">// 记录创造该元素的组件</span> _owner: owner, }; // if (DEV) { // 这里是一些针对 DEV 环境下的处理,对于大家理解主要逻辑意义不大,此处我直接省略掉,以免混淆视听 } return element;}; ReactElement 的代码出乎意料的简短,从逻辑上我们可以看出,ReactElement 其实只做了一件事情,那就是“创建”,说得更精确一点,是“组装”:ReactElement 把传入的参数按照一定的规范,“组装”进了 element 对象里,并把它返回给了 React.createElement,最终 React.createElement 又把它交回到了开发者手中。整个过程如下图所示: 如果你想要验证这一点,可以尝试输出我们示例中 App 组件的 JSX 部分: const AppJSX = (<div className=\"App\"> <h1 className=\"title\">I am the title</h1> <p className=\"content\">I am the content</p> </div>) console.log(AppJSX) 你会发现它确实是一个标准的 ReactElement 对象实例,如下图(生产环境下的输出结果)所示: 这个 ReactElement 对象实例,本质上是以 JavaScript 对象形式存在的对 DOM 的描述,也就是老生常谈的“虚拟 DOM”(准确地说,是虚拟 DOM 中的一个节点。关于虚拟 DOM, 我们将在专栏的“模块二:核心原理”中花大量的篇幅来研究它,此处你只需要能够结合源码,形成初步认知即可)。 既然是“虚拟 DOM”,那就意味着和渲染到页面上的真实 DOM 之间还有一些距离,这个“距离”,就是由大家喜闻乐见的ReactDOM.render方法来填补的。 在每一个 React 项目的入口文件中,都少不了对 React.render 函数的调用。下面我简单介绍下 ReactDOM.render 方法的入参规则: ReactDOM.render( // 需要渲染的元素(ReactElement) element, // 元素挂载的目标容器(一个真实DOM) container, // 回调函数,可选参数,可以用来处理渲染结束后的逻辑 [callback] ) ReactDOM.render 方法可以接收 3 个参数,其中第二个参数就是一个真实的 DOM 节点,这个真实的 DOM 节点充当“容器”的角色,React 元素最终会被渲染到这个“容器”里面去。比如,示例中的 App 组件,它对应的 render 调用是这样的: const rootElement = document.getElementById(\"root\"); ReactDOM.render(<App />, rootElement); 注意,这个真实 DOM 一定是确实存在的。比如,在 App 组件对应的 index.html 中,已经提前预置 了 id 为 root 的根节点: <body> <div id=\"root\"></div> </body>","link":"/2022/09/22/frontEnd/inDepthAndSimpleReact/reactStudy-EP1/"},{"title":"S02E04 股票业务基础知识","text":"股票业务基础知识培训 #一、股票定义及相关概念#1、股票的定义 股票是股份有限公司发行的,用以证明投资者的股东身份,并据以获取股息和红利的凭证。 股票实质上代表了股东对股份公司的 所有权 ,一经发行,购买股票的投资者即成为公司的股东,股东凭借股票可以获得公司的股息和 红利 ,参加股东大会并行使自己的 权力 , 同时也承担相应的责任和 风险 。 同一类别的每一份股票所代表的公司所有权是相等的。每个股东所拥有的公司所有权份额的大小,取决于其持有的股票数量占公司总股本的比重。 纸质股票载明的事项 公司名称 公司成立的日期 股票种类、票面金额 代表的股份数量 股票编号 公司法定代表人签名 #2、股票的性质 有价证券 :有价证券是财产价值和财产权利的统一表现形式,股票本身没有价值,但它是一种代表财产权的有价证券。 要式证券 :股票应具备《公司法》规定的有关内容,必须具备规定的要件(公司名称、股票种类、票面金额等),否则股票就无法律效力。 证权证券 :证权证券是指证券是权利的一种物化的外在形式,是权利的载体,股票用来证明股东的权利。 资本证券 :发行股票是股份公司筹措自有资本的手段,因此股票是投入股份公司资本份额的证券化,属于资本证券。 综合权利证券 :当公司股东将出资交给公司后,股东对其出资财产的所有权就转化为股东权,股东依法享有资产收益、重大决策、选择管理者等权利。 #3、股票的特征 收益性 :最基本特征,股票可以为持有人带来收益的特征,其收益包含了股息和红利,以及股票流通而取得的资本利得。 风险性 :投资收益的不确定性,高风险的金融产品。 流动性 :可以通过依法转让而变现的特性,股票持有人不能从公司退股,但股票转让为其提供了变现的渠道。 永久性 :股票所载的权利的有效性是始终不变的,因为它是一种无期限的法律凭证。 参与性 :股票持有人有权参与公司重大决策的特性,有权出席股东大会,选举公司董事会,行使对公司经营决策的参与权等。 #4、股票的分类 按照股东的权利划分 普通股 是指秉持“ 一股一权 ”规则之下收益权与表决权无差别、等比例配置的股票。是最基本的,最常见的一种股票,其持有者享有股东的基本权利和义务。 优先股 是一种特殊股票,其 股息率固定 ,参与公司决策管理等权利受到一定限制,但在公司盈利和剩余财产分配上比普通股东享有 优先权 。 按是否记载股东姓名划分 记名股票 在股票票面和股份公司的股东名册上记载股东姓名的股票。一般股份有限公司向发起人、法人发行股票时,应为记名股票。需置备股东名册, 记载相关事项。 不记名股票 在股票票面和股份公司股东名册上均不记载股东姓名的股票,其与记名股票的差别不是在股东权利等方面,而是在股票的记载方式上。 按是否在股票上标明金额划分 有面额股票 是指在股票票面上记载一定的金额的股票,这一金额也称为票面金额、票面价值或股票面值。 无面额股票 是指在股票票面不记载股票面额,只注明它在公司总股本中所占比例的股票。 按股票的上市地区划分 A股 即人民币普通股,由中国境内公司发行、上市,境内机构和个人以人民币购买交易的股票。 B股 即人民币特种股,是由中国境内注册、上市的公司发行,以人民币标明面值的股票,但以其他货币认购和交易的股票。 H股 经证监会批准,注册地在内地,在香港市场上市,供境外投资者认购交易的股票。 #5、股票的价格 股票的理论价格 股票价格应由其价值决定,但股票本身没有价值,它之所以有价格是因为它代表着收益的价值,所以股票的理论价格就是对未来收益的评定。股票的现值就是未来收益的当前价值,也是人们为了得到股票的未来收益愿意付出的代价。 股票的理论价格为预测股票市场价格的变动趋势提供了重要的依据,也是股票市场价格形成的一个基础性因素。 股票的市场价格 指的是二级市场的交易价格,股票的交易价格是由它的价值决定的,但也受其他因素的影响,其中供求关系是最直接的影响因素。由于影响股票价格的因素复杂多变,所以股票的市场价格呈现出高低起伏的波动性特征。 影响股票市场价格变动的基本因素 #二、股票的发行#1、中国股票市场结构 1、主板市场 主板市场也称为 一板市场 ,是传统意义上的证券市场(通常指股票市场),是一个国家或地区证券发行、上市及交易的主要场所; 中国大陆主板市场的公司在上交所和深交所两个市场进行上市; 主板市场是资本市场中最重要的组成部分,很大程度上能够反映经济发展状况,有“ 国民经济晴雨表 ”之称; 主板市场上市门槛较高,对发行人的营业期限、股本大小、盈利水平、最低市值等方面都具有较高要求。通常能在主板上市的多为大型成熟企业,或处于某个行业的龙头地位,具有较大的资本规模及稳定的盈利能力。 2、中小板市场 中小板市场是深交所为了鼓励自主创新而专门设置的中小型公司聚集板块。在中小板上市的企业,行业地位虽然通常没有主板上市企业那么高,但其中部分企业成长性较强,上市后快速发展。 3、创业板市场 创业板市场又被称为 二板市场 ,是为具有高成长性的中小企业和高科技企业融资服务的资本市场。 创业板市场是不同于主板市场的独特的资本市场,具有前瞻性、高风险、监管要求严格以及明显的高技术产业导向的特点。 与主板市场相比,在创业板市场上市的企业规模较小、上市条件相对较低,中小企业更容易上市募集发展所需资金。 4、科创板市场 科创板是我国首个实行注册制的场内市场,主要服务于符合国家战略、突破关键核心技术、市场认可度高的科技创新企业。 科创板上市企业普遍具有技术新、研发投入规模大、盈利周期长、技术迭代快、盈利能力不稳定以及严重依赖核心项目、核心技术人员、少数供应商等特点,因此企业上市后的持续创新能力、主营业务发展的可持续性、公司收入及盈利水平等仍具有较大不确定性。 科创板重点关注三类企业 : 符合国家战略、突破关键核心技术、市场认可度高的科技创新企业; 属于新一代信息技术、高端装备、新材料、新能源、节能环保以及生物医药等高新技术产业和战略性新兴产业的科技创新企业; 互联网、大数据、云计算、人工智能和制造业深度融合的科技创新企业。 5、新三板市场 新三板即为 全国中小企业股份转让系统 ,主要为创新型、创业型、成长型中小微企业发展服务。 新三板设立创新层和基础层,符合不同标准的挂牌公司分别纳入精选层、创新层和基础层管理。境内符合条件的股份公司均可通过主办券商申请在新三板挂牌,公开转让股份,进行股权融资、债券融资、资产重组等。 新三板构成了小微企业,特别是创新型小微企业直接融资的重要平台,尤其是对于既无法满足银行信贷审核要求,也无法满足A股上市条件的小微企业,新三板融资通道的作用更加凸显,是我国多层次资本市场的重要一环。 #2、股票发行制度股票发行监管制度 审批制 :是一国在股票市场的发展初期,为了维护上市公司稳定和平衡复杂的社会经济关系,采用行政计划的办法分配股票发行的指标和额度,由地方或行业主管部门根据指标推荐企业发行股票的一种发行制度。 核准制 :介于注册制和审批制之间的中间形式。一方面取消了指标额度管理,并引进证券中介机构的责任,判断企业是否达到股票的发行条件;另一方面证券监管机构对股票发行的合规性和适销性条件进行实质性审查,并有权否决股票发行的申请。 注册制 :是在市场化程度较高的成熟股票市场所普遍采用的一种发行制度。证券监管部门公布股票发行的必要条件,只要达到所公布条件要求的企业即可发行股票。2020年3月1日起,我国开始全面推行注册制。 #3、首次公开发行IPO 首次公开发行股票(IPO) ,是指公司首次在证券市场公开发行股票募集资金并上市的行为。 #4、股权再融资 #5、股票退市制度 股票退市是指上市公司股票在证券交易所终止上市交易。退市制度是资本市场重要的基础性制度,有利于健全资本市场功能,增强市场主体活力,实现优胜劣汰,惩戒重大违法行为,保护投资者合法权益。 主动退市 :上市公司通过对上市地位维持成本收益的理性分析,或者为便捷、高效地对公司治理结构、资产结构、人员结构等实施调整,或者为进一步实现公司股票的长期价值,可以依据《证券法》和证券交易所规则实现主动退市。 强制退市 :证交所为维护公开交易股票的总体质量与市场信心,保护投资者合法权益,依照规则要求交投不活跃、股权分布不合理、市值过低而不再适合公开交易的股票应终止交易,特别是对于存在严重违法违规行为的公司,证交所可以依法强制其股票退出市场交易。 #三、股票的交易#1、证券交易原则 根据《证券法》规定,证券交易必须实行公开、公平、公正原则。 公开原则 :又称为信息公开原则,指证券交易是一种面向社会的、公开的交易活动,其核心要求是实现市场信息的公开化,证券交易参与各方应依法及时、真实、准确、完整地向社会发布有关信息。 公平原则 :是指参与交易的各方应当获得平等的机会。它要求证券交易活动中的所有参与者都有平等的法律地位,各自的合法权益都能得到公平保护。 公正原则 :是指应当公正地对待证券交易的参与各方,以及公正地处理证券交易事务。在实践中,公正原则也体现在很多方面,例如,公正地办理证券交易中的各项手续,公正地处理证券交易中的违法违规行为等。 #2、股票竞价与成交 1、竞价原则 证券交易所内的证券交易按“ 价格优先、时间优先 ”原则竞价成交。 价格优先 :较高价格买入申报优先于较低价格买入申报,较低价格卖出申报优先于较高价格卖出申报。 时间优先 :买卖方向、价格相同的,先申报者优于后申报者。 2、竞价方式 目前,我国证券交易所采用两种竞价方式:集合竞价方式和 连续竞价方式 。 集合竞价 :是指对在规定的一段时间内接受的买卖申报一次性集合撮合的竞价方式。 连续竞价 :是指对买卖申报逐笔连续撮合的竞价方式。连续竞价的特点是,每一笔买卖委托输入交易自动撮合系统后,当即判断进行不同的处理:能成交者予以成交,不能成交者等待机会成交,部分成交者则让剩余部分继续等待。 3、交易结算 目前我国证券市场采用的是 法人结算模式 ,主要是指由证券公司以法人名义在证券登记结算机构开立证券交收账户和资金交收账户,其接受客户委托代理的证券交易的清算交收均通过此账户办理。 证券公司与客户之间的证券结算交收 :委托中国结算公司根据成交记录按照业务规则代为办理。证券交收结果等数据由中国结算公司每日传送至证券公司,供其对账和向客户提供余额查询,证券公司根据中国结算公司数据,记录客户清算交收结果。 证券公司与客户之间的资金清算交收 :需要由证券公司与指定商业银行配合完成 2021年7月7日 长沙","link":"/2021/07/07/finance/financeStudy-EP4/"},{"title":"EP02 为什么 React 16 要更改组件的生命周期?(上)","text":"为什么 React 16 要更改组件的生命周期?(上) React 生命周期已经是一个老生常谈的话题了,几乎没有哪一门 React 入门教材会省略对组件生命周期的介绍。然而,入门教材在设计上往往追求的是“简单省事、迅速上手”,这就导致许多同学对于生命周期知识的刻板印象为“背就完了、别想太多”。 “背就完了”这样简单粗暴的学习方式,或许可以帮助你理解“What to do”,到达“How to do”,但却不能帮助你去思考和认知“Why to do”。作为一个专业的 React 开发者,我们必须要求自己在知其然的基础上,知其所以然。 在本课时和下一个课时,我将抱着帮你做到“知其所以然”的目的,以 React 的基本原理为引子,对 React 15、React 16 两个版本的生命周期进行探讨、比对和总结,通过搞清楚一个又一个的“Why”,来帮你建立系统而完善的生命周期知识体系。 生命周期背后的设计思想:把握 React 中的“大方向” 在介绍具体的生命周期之前,我想先带你初步理解 React 框架中的一些关键的设计思想,以便为你后续的学习提供不可或缺的“加速度”。 如果你经常翻阅 React 官网或者 React 官方的一些文章,你会发现“组件”和“虚拟 DOM”这两个词的出镜率是非常高的,它们是 React 基本原理中极为关键的两个概念,也是我们这个小节的学习切入点。 虚拟 DOM:核心算法的基石 通过 01 课时的学习,你已经知晓了虚拟 DOM 节点的基本形态,现在我们需要简单了解下虚拟 DOM 在整个 React 工作流中的作用。 组件在初始化时,会通过调用生命周期中的 render 方法,生成虚拟 DOM,然后再通过调用 ReactDOM.render 方法,实现虚拟 DOM 到真实 DOM 的转换。 当组件更新时,会再次通过调用 render 方法生成新的虚拟 DOM,然后借助 diff(这是一个非常关键的算法,我将在“模块二:核心原理”重点讲解)定位出两次虚拟 DOM 的差异,从而针对发生变化的真实 DOM 作定向更新。 以上就是 React 框架核心算法的大致流程。对于这套关键的工作流来说,“虚拟 DOM”是所有操作的大前提,是核心算法的基石。 组件化:工程化思想在框架中的落地 组件化是一种优秀的软件设计思想,也是 React 团队在研发效能方面所做的一个重要的努力。 在一个 React 项目中,几乎所有的可见/不可见的内容都可以被抽离为各种各样的组件,每个组件既是“封闭”的,也是“开放”的。 所谓“封闭”,主要是针对“渲染工作流”(指从组件数据改变到组件实际更新发生的过程)来说的。在组件自身的渲染工作流中,每个组件都只处理它内部的渲染逻辑。在没有数据流交互的情况下,组件与组件之间可以做到“各自为政”。 而所谓“开放”,则是针对组件间通信来说的。React 允许开发者基于“单向数据流”的原则完成组件间的通信。而组件之间的通信又将改变通信双方/某一方内部的数据,进而对渲染结果构成影响。所以说在数据这个“红娘”的牵线搭桥之下,组件之间又是彼此开放的,是可以相互影响的。 这一“开放”与“封闭”兼具的特性,使得 React 组件既专注又灵活,具备高度的可重用性和可维护性。 生命周期方法的本质:组件的“灵魂”与“躯干” 之前我曾经在社区读过一篇文章,文中将 render 方法形容为 React 组件的“灵魂”。当时我对这句话产生了非常强烈的共鸣,这里我就想以这个曾经打动过我的比喻为引子,帮助你从宏观上建立对 React 生命周期的感性认知。 注意,这里提到的 render 方法,和我们 01 课时所说的 ReactDOM.render 可不是一个东西,它指的是 React 组件内部的这个生命周期方法: class LifeCycle extends React.Component { render() { console.log(“render方法执行”); return ( <div className=“container”> this is content </div> ); }} 前面咱们介绍了虚拟 DOM、组件化,倘若把这两块知识整合一下,你就会发现这两个概念似乎都在围着 render 这个生命周期打转:虚拟 DOM 自然不必多说,它的生成都要仰仗 render;而组件化概念中所提及的“渲染工作流”,这里指的是从组件数据改变到组件实际更新发生的过程,这个过程的实现同样离不开 render。 由此看来,render 方法在整个组件生命周期中确实举足轻重,它担得起“灵魂”这个有分量的比喻。那么如果将 render 方法比作组件的“灵魂”,render 之外的生命周期方法就完全可以理解为是组件的“躯干”。 “躯干”未必总是会做具体的事情(比如说我们可以选择性地省略对 render 之外的任何生命周期方法内容的编写),而“灵魂”却总是充实的(render 函数却坚决不能省略);倘若“躯干”做了点什么,往往都会直接或间接地影响到“灵魂”(因为即便是 render 之外的生命周期逻辑,也大部分是在为 render 层面的效果服务);“躯干”和“灵魂”一起,共同构成了 React 组件完整而不可分割的“生命时间轴”。 拆解 React 生命周期:从 React 15 说起 我发现时下许多资料在讲解 React 生命周期时,喜欢直接拿 React 16 开刀。这样做虽然省事儿,却也模糊掉了新老生命周期变化背后的“Why”(关于两者的差异,我们会在“03 课时”中详细讲解)。这里为了把这个“Why”拎出来,我将首先带你认识 React 15 的生命周期流程。 在 React 15 中,大家需要关注以下几个生命周期方法: constructor() componentWillReceiveProps() shouldComponentUpdate() componentWillMount() componentWillUpdate() componentDidUpdate() componentDidMount() render() componentWillUnmount() 如果你接触 React 足够早,或许会记得还有 getDefaultProps 和 getInitState 这两个方法,它们都是 React.createClass() 模式下初始化数据的方法。由于这种写法在 ES6 普及后已经不常见,这里不再详细展开。 这些生命周期方法是如何彼此串联、相互依存的呢?这里我为你总结了一张大图: 接下来,我就围绕这张大图,分阶段讨论组件生命周期的运作规律。在学习的过程中,下面这个 Demo 可以帮助你具体地验证每个阶段的工作流程: import React from \"react\"; import ReactDOM from \"react-dom\"; // 定义子组件 class LifeCycle extends React.Component { constructor(props) { console.log(\"进入constructor\"); super(props); // state 可以在 constructor 里初始化 this.state = { text: \"子组件的文本\" }; } // 初始化渲染时调用 componentWillMount() { console.log(\"componentWillMount方法执行\"); } // 初始化渲染时调用 componentDidMount() { console.log(\"componentDidMount方法执行\"); } // 父组件修改组件的props时会调用 componentWillReceiveProps(nextProps) { console.log(\"componentWillReceiveProps方法执行\"); } // 组件更新时调用 shouldComponentUpdate(nextProps, nextState) { console.log(\"shouldComponentUpdate方法执行\"); return true; } // 组件更新时调用 componentWillUpdate(nextProps, nextState) { console.log(“componentWillUpdate方法执行”); } // 组件更新后调用 componentDidUpdate(preProps, preState) { console.log(“componentDidUpdate方法执行”); } // 组件卸载时调用 componentWillUnmount() { console.log(“子组件的componentWillUnmount方法执行”); } // 点击按钮,修改子组件文本内容的方法 changeText = () => { this.setState({ text: “修改后的子组件文本” }); }; render() { console.log(“render方法执行”); return ( <div className=“container”> <button onClick={this.changeText} className=“changeText”> 修改子组件文本内容 </button> <p className=“textContent”>{this.state.text}</p> <p className=“fatherContent”>{this.props.text}</p> </div> ); }}// 定义 LifeCycle 组件的父组件class LifeCycleContainer extends React.Component { // state 也可以像这样用属性声明的形式初始化 state = { text: “父组件的文本”, hideChild: false }; // 点击按钮,修改父组件文本的方法 changeText = () => { this.setState({ text: “修改后的父组件文本” }); }; // 点击按钮,隐藏(卸载)LifeCycle 组件的方法 hideChild = () => { this.setState({ hideChild: true }); }; render() { return ( <div className=“fatherContainer”> <button onClick={this.changeText} className=“changeText”> 修改父组件文本内容 </button> <button onClick={this.hideChild} className=“hideChild”> 隐藏子组件 </button> {this.state.hideChild ? null : <LifeCycle text={this.state.text} />} </div> ); }}ReactDOM.render(<LifeCycleContainer />, document.getElementById(“root”)); 该入口文件对应的 index.html 中预置了 id 为 root 的真实 DOM 节点作为根节点,body 标签内容如下: <body> <div id=\"root\"></div> </body> 这个 Demo 渲染到浏览器上大概是这样的: 此处由于我们强调的是对生命周期执行规律的验证,所以样式上从简,你也可以根据自己的喜好添加 CSS 相关的内容。 接下来我们就结合这个 Demo 和开头的生命周期大图,一起来看看挂载、更新、卸载这 3 个阶段,React 组件都经历了哪些事情。 Mounting 阶段:组件的初始化渲染(挂载) 挂载过程在组件的一生中仅会发生一次,在这个过程中,组件被初始化,然后会被渲染到真实 DOM 里,完成所谓的“首次渲染”。 在挂载阶段,一个 React 组件会按照顺序经历如下图所示的生命周期: 首先我们来看 constructor 方法,该方法仅仅在挂载的时候被调用一次,我们可以在该方法中对 this.state 进行初始化: constructor(props) { console.log(\"进入constructor\"); super(props); // state 可以在 constructor 里初始化 this.state = { text: \"子组件的文本\" }; } componentWillMount、componentDidMount 方法同样只会在挂载阶段被调用一次。其中 componentWillMount 会在执行 render 方法前被触发,一些同学习惯在这个方法里做一些初始化的操作,但这些操作往往会伴随一些风险或者说不必要性(这一点大家先建立认知,具体原因将在“03 课时”展开讲解)。 接下来 render 方法被触发。注意 render 在执行过程中并不会去操作真实 DOM(也就是说不会渲染),它的职能是把需要渲染的内容返回出来。真实 DOM 的渲染工作,在挂载阶段是由 ReactDOM.render 来承接的。 componentDidMount 方法在渲染结束后被触发,此时因为真实 DOM 已经挂载到了页面上,我们可以在这个生命周期里执行真实 DOM 相关的操作。此外,类似于异步请求、数据初始化这样的操作也大可以放在这个生命周期来做(侧面印证了 componentWillMount 真的很鸡肋)。 这一整个流程对应的其实就是我们 Demo 页面刚刚打开时,组件完成初始化渲染的过程。下图是 Demo 中的 LifeCycle 组件在挂载过程中控制台的输出,你可以用它来验证挂载过程中生命周期顺序的正确性: Updating 阶段:组件的更新 组件的更新分为两种:一种是由父组件更新触发的更新;另一种是组件自身调用自己的 setState 触发的更新。这两种更新对应的生命周期流程如下图所示: componentWillReceiProps 到底是由什么触发的? 从图中你可以明显看出,父组件触发的更新和组件自身的更新相比,多出了这样一个生命周期方法: componentWillReceiveProps(nextProps) 在这个生命周期方法里,nextProps 表示的是接收到新 props 内容,而现有的 props (相对于 nextProps 的“旧 props”)我们可以通过 this.props 拿到,由此便能够感知到 props 的变化。 写到这里,就不得不在“变化”这个动作上深挖一下了。我在一些社区文章里,包括一些候选人面试时的回答里,都不约而同地见过/听过这样一种说法:componentWillReceiveProps 是在组件的 props 内容发生了变化时被触发的。 这种说法不够严谨。远的不说,就拿咱们上文给出的 Demo 开刀,该界面的控制台输出在初始化完成后是这样的: 注意,我们代码里面,LifeCycleContainer 这个父组件传递给子组件 LifeCycle 的 props 只有一个 text: <LifeCycle text={this.state.text} /> 假如我点击“修改父组件文本内容”这个按钮,父组件的 this.state.text 会发生改变,进而带动子组件的 this.props.text 发生改变。此时一定会触发 componentWillReceiveProps 这个生命周期,这是毋庸置疑的: 但如果我现在对父组件的结构进行一个小小的修改,给它一个和子组件完全无关的 state(this.state.ownText),同时相应地给到一个修改这个 state 的方法(this.changeOwnText),并用一个新的 button 按钮来承接这个触发的动作。 改变后的 LifeCycleContainer 如下所示: // 定义 LifeCycle 组件的父组件 class LifeCycleContainer extends React.Component { // state 也可以像这样用属性声明的形式初始化 state = { text: \"父组件的文本\", // 新增的只与父组件有关的 state ownText: \"仅仅和父组件有关的文本\", hideChild: false }; changeText = () => { this.setState({ text: \"修改后的父组件文本\" }); }; // 修改 ownText 的方法 changeOwnText = () => { this.setState({ ownText: \"修改后的父组件自有文本\" }); }; hideChild = () => { this.setState({ hideChild: true }); }; render() { return ( <div className=\"fatherContainer\"> {/* 新的button按钮 */} <button onClick={this.changeOwnText} className=\"changeText\"> 修改父组件自有文本内容 </button> <button onClick={this.changeText} className=\"changeText\"> 修改父组件文本内容 </button> <button onClick={this.hideChild} className=\"hideChild\"> 隐藏子组件 </button> <p> {this.state.ownText} </p> {this.state.hideChild ? null : <LifeCycle text={this.state.text} />} </div> ); } } 新的界面如下图所示: 可以看到,this.state.ownText 这个状态和子组件完全无关。但是当我点击“修改父组件自有文本内容”这个按钮的时候,componentReceiveProps 仍然被触发了,效果如下图所示: 耳听为虚,眼见为实。面对这样的运行结果,我不由得要带你复习一下 React 官方文档中的这句话: componentReceiveProps 并不是由 props 的变化触发的,而是由父组件的更新触发的,这个结论,请你谨记。 组件自身 setState 触发的更新 this.setState() 调用后导致的更新流程,前面大图中已经有体现,这里我直接沿用上一个 Demo 来做演示。若我们点击上一个 Demo 中的“修改子组件文本内容”这个按钮: 这个动作将会触发子组件 LifeCycle 自身的更新流程,随之被触发的生命周期函数如下图增加的 console 内容所示: 先来说说 componentWillUpdate 和 componentDidUpdate 这一对好基友。 componentWillUpdate 会在 render 前被触发,它和 componentWillMount 类似,允许你在里面做一些不涉及真实 DOM 操作的准备工作;而 componentDidUpdate 则在组件更新完毕后被触发,和 componentDidMount 类似,这个生命周期也经常被用来处理 DOM 操作。此外,我们也常常将 componentDidUpdate 的执行作为子组件更新完毕的标志通知到父组件。 render 与性能:初识 shouldComponentUpdate 这里需要重点提一下 shouldComponentUpdate 这个生命周期方法,它的调用形式如下所示: shouldComponentUpdate(nextProps, nextState) render 方法由于伴随着对虚拟 DOM 的构建和对比,过程可以说相当耗时。而在 React 当中,很多时候我们会不经意间就频繁地调用了 render。为了避免不必要的 render 操作带来的性能开销,React 为我们提供了 shouldComponentUpdate 这个口子。 React 组件会根据 shouldComponentUpdate 的返回值,来决定是否执行该方法之后的生命周期,进而决定是否对组件进行re-render(重渲染)。shouldComponentUpdate 的默认值为 true,也就是说“无条件 re-render”。在实际的开发中,我们往往通过手动往 shouldComponentUpdate 中填充判定逻辑,或者直接在项目中引入 PureComponent 等最佳实践,来实现“有条件的 re-render”。 关于 shouldComponentUpdate 及 PureComponent 对 React 的优化,我们会在后续的性能小节中详细展开。这里你只需要认识到 shouldComponentUpdate 的基本使用及其与 React 性能之间的关联关系即可。 Unmounting 阶段:组件的卸载 组件的销毁阶段本身是比较简单的,只涉及一个生命周期,如下图所示: 对应上文的 Demo 来看,我们点击“隐藏子组件”后就可以把 LifeCycle 从父组件中移除掉,进而实现卸载的效果。整个过程如下图所示: 这个生命周期本身不难理解,我们重点说说怎么触发它。组件销毁的常见原因有以下两个。 组件在父组件中被移除了:这种情况相对比较直观,对应的就是我们上图描述的这个过程。 组件中设置了 key 属性,父组件在 render 的过程中,发现 key 值和上一次不一致,那么这个组件就会被干掉。 在本课时,只要能够理解到 1 就可以了。对于 2 这种情况,你只需要先记住有这样一种现象,这就够了。至于组件里面为什么要设置 key,为什么 key 改变后组件就必须被干掉?要回答这个问题,需要你先理解 React 的“调和过程”,而“调和过程”也会是我们第二模块中重点讲解的一个内容。这里我先把这个知识点点出来,方便你定位我们整个知识体系里的重难点。 总结 在本课时,我们对 React 设计思想中的“虚拟 DOM”和“组件化”这两个关键概念形成了初步的理解,同时也对 React 15 中的生命周期进行了系统的学习和总结。到这里,你已经了解到了 React 生命周期在很长一段“过去”里的形态。 而在 React 16 中,组件的生命周期其实已经发生了一系列的变化。这些变化到底是什么样的,它们背后又蕴含着 React 团队怎样的思量呢? 古人说“以史为镜,可以知兴衰”。在下个课时,我们将一起去“照镜子”,对 React 新旧生命周期进行对比,并探求变化的动机。","link":"/2022/09/23/frontEnd/inDepthAndSimpleReact/reactStudy-EP2/"},{"title":"EP03 为什么 React 16 要更改组件的生命周期?(下)","text":"为什么 React 16 要更改组件的生命周期?(下) 通过对上一个课时的学习,你已经对 React 15 的生命周期有了系统的掌握和理解。本课时,我将在此基础上,对 React 16 以来的生命周期进行剖析。在理解“是什么”的基础上,我将带你对比新旧两个版本生命周期之间的差异,并探寻变化背后的原因。 通过本课时的学习,你将明白 React 团队“动作频频”背后的思量与野心,同时也将对时下大热的 Fiber 架构形成初步的认知。 进化的生命周期方法:React 16 生命周期工作流详解 关于 React 16 以来的生命周期,这个民间开源项目为我们提供了目前公认的比较优秀的流程大图(在下不才,自己动手画了半天仍然自觉无法超越下图,所以这里就直接引用过来辅助讲解)。我们先来看 React 16.3 的大图: 这里之所以特意将版本号精确到了小数点后面一位,是因为在React 16.4之后,React 生命周期在之前版本的基础上又经历了一次微调。不过你先不用着急,在理解 16.3 生命周期的基础上,掌握这个“微调”对你来说将易如反掌。 接下来,我会先把上面这张 React 16.3 生命周期大图中所涉及的内容讲清楚,然后再对 16.4 的改动进行介绍。还是老规矩,这里我先提供一个 Demo,它将辅助你理解新的生命周期。Demo 代码如下: import React from \"react\"; import ReactDOM from \"react-dom\"; // 定义子组件 class LifeCycle extends React.Component { constructor(props) { console.log(\"进入constructor\"); super(props); // state 可以在 constructor 里初始化 this.state = { text: \"子组件的文本\" }; } // 初始化/更新时调用 static getDerivedStateFromProps(props, state) { console.log(\"getDerivedStateFromProps方法执行\"); return { fatherText: props.text } } // 初始化渲染时调用 componentDidMount() { console.log(\"componentDidMount方法执行\"); } // 组件更新时调用 shouldComponentUpdate(prevProps, nextState) { console.log(\"shouldComponentUpdate方法执行\"); return true; } // 组件更新时调用 getSnapshotBeforeUpdate(prevProps, prevState) { console.log(“getSnapshotBeforeUpdate方法执行”); return “haha”; } // 组件更新后调用 componentDidUpdate(preProps, preState, valueFromSnapshot) { console.log(“componentDidUpdate方法执行”); console.log(“从 getSnapshotBeforeUpdate 获取到的值是”, valueFromSnapshot); } // 组件卸载时调用 componentWillUnmount() { console.log(“子组件的componentWillUnmount方法执行”); } // 点击按钮,修改子组件文本内容的方法 changeText = () => { this.setState({ text: “修改后的子组件文本” }); }; render() { console.log(“render方法执行”); return ( <div className=“container”> <button onClick={this.changeText} className=“changeText”> 修改子组件文本内容 </button> <p className=“textContent”>{this.state.text}</p> <p className=“fatherContent”>{this.props.text}</p> </div> ); }}// 定义 LifeCycle 组件的父组件class LifeCycleContainer extends React.Component { // state 也可以像这样用属性声明的形式初始化 state = { text: “父组件的文本”, hideChild: false }; // 点击按钮,修改父组件文本的方法 changeText = () => { this.setState({ text: “修改后的父组件文本” }); }; // 点击按钮,隐藏(卸载)LifeCycle 组件的方法 hideChild = () => { this.setState({ hideChild: true }); }; render() { return ( <div className=“fatherContainer”> <button onClick={this.changeText} className=“changeText”> 修改父组件文本内容 </button> <button onClick={this.hideChild} className=“hideChild”> 隐藏子组件 </button> {this.state.hideChild ? null : <LifeCycle text={this.state.text} />} </div> ); }}ReactDOM.render(<LifeCycleContainer />, document.getElementById(“root”)); React 16 以来的生命周期也可以按照“挂载”“更新”和“卸载”三个阶段来看,所以接下来我们要做的事情仍然是分阶段拆解工作流程。在这个过程中,我将把 React 16 新增的生命周期方法,以及流程上相对于 React 15 产生的一些差异,作为我们学习的重点。对于和 React 15 保持一致的部分,这里不再重复讲解。 Mounting 阶段:组件的初始化渲染(挂载) 为了凸显 16 和 15 两个版本生命周期之间的差异,我将两个流程绘制到了同一张大图里,请看下面这张图: 你现在可以打开开篇我给出的 Demo,将你的 React 版本更新到 16.3,然后运行这个项目,你就可以在控制台看到新的生命周期执行过程了。控制台的输出如图所示: 消失的 componentWillMount,新增的 getDerivedStateFromProps 从上图中不难看出,React 15 生命周期和 React 16.3 生命周期在挂载阶段的主要差异在于,废弃了 componentWillMount,新增了 getDerivedStateFromProps。 注:细心的你可能记得,React 16 对 render 方法也进行了一些改进。React 16 之前,render方法必须返回单个元素,而 React 16 允许我们返回元素数组和字符串。但本课时我们更加侧重讨论的是生命周期升级过程中的“主要矛盾”,也就是“工作流”层面的改变,故对现有方法的迭代细节,以及不在主要工作流里的componentDidCatch 等生命周期不再予以赘述。 一些同学在初次发现这个改变的时候,倾向于认为是 React 16.3 在试图用 getDerivedStateFromProps代替componentWillMount,这种想法是不严谨的。 getDerivedStateFromProps 不是 componentWillMount 的替代品 事实上,componentWillMount 的存在不仅“鸡肋”而且危险,因此它并不值得被“代替”,它就应该被废弃。 为了证明这点,我将在本文后续的“透过现象看本质”环节为大家细数 componentWillMount 的几宗“罪”。 而 getDerivedStateFromProps 这个 API,其设计的初衷不是试图替换掉 componentWillMount,而是试图替换掉 componentWillReceiveProps,因此它有且仅有一个用途:使用 props 来派生/更新 state。 React 团队为了确保 getDerivedStateFromProps 这个生命周期的纯洁性,直接从命名层面约束了它的用途(getDerivedStateFromProps 直译过来就是“从 Props 里派生 State”)。所以,如果你不是出于这个目的来使用 getDerivedStateFromProps,原则上来说都是不符合规范的。 值得一提的是,getDerivedStateFromProps 在更新和挂载两个阶段都会“出镜”(这点不同于仅在更新阶段出现的 componentWillReceiveProps)。这是因为“派生 state”这种诉求不仅在 props 更新时存在,在 props 初始化的时候也是存在的。React 16 以提供特定生命周期的形式,对这类诉求提供了更直接的支持。 由此看来,挂载阶段的生命周期改变,并不是一个简单的“替换”逻辑,而是一个雄心勃勃的“进化”逻辑。 认识 getDerivedStateFromProps 这个新生命周期方法的调用规则如下: static getDerivedStateFromProps(props, state) 在使用层面,你需要把握三个重点。 第一个重点是最特别的一点:getDerivedStateFromProps 是一个静态方法。静态方法不依赖组件实例而存在,因此你在这个方法内部是访问不到 this 的。若你偏要尝试这样做,必定报错,报错形式如下图所示: 第二个重点,该方法可以接收两个参数:props 和 state,它们分别代表当前组件接收到的来自父组件的 props 和当前组件自身的 state。我们可以尝试在 Demo 中输出这两个参数看一看,输出效果如下图所示: 可以看出,挂载阶段输出的 props 正是初始化阶段父组件传进来的 this.props 对象;而 state 是 LifeCycle 组件自身的 state 对象。 第三个重点,getDerivedStateFromProps 需要一个对象格式的返回值。如果你没有指定这个返回值,那么大概率会被 React 警告一番,警告内容如下图所示: getDerivedStateFromProps 的返回值之所以不可或缺,是因为 React 需要用这个返回值来更新(派生)组件的 state。因此当你确实不存在“使用 props 派生 state ”这个需求的时候,最好是直接省略掉这个生命周期方法的编写,否则一定记得给它 return 一个 null。 注意,getDerivedStateFromProps 方法对 state 的更新动作并非“覆盖”式的更新,而是针对某个属性的定向更新。比如这里我们在 getDerivedStateFromProps 里返回的是这样一个对象,对象里面有一个 fatherText 属性用于表示“父组件赋予的文本”: { fatherText: props.text } 该对象并不会替换掉组件原始的这个 state: this.state = { text: \"子组件的文本\" }; 而是仅仅针对 fatherText 这个属性作更新(这里原有的 state 里没有 fatherText,因此直接新增)。更新后,原有属性与新属性是共存的,如下图所示: Updating 阶段:组件的更新 React 15 与 React 16.3 的更新流程对比如下图所示: 注意,咱们前面提到 React 16.4 对生命周期流程进行了“微调”,其实就调在了更新过程的getDerivedStateFromProps 这个生命周期上。先来看一张 React 16.4+ 的生命周期大图(出处仍然是Wojciech Maj 的 react-lifecycle-methods-diagram): React 16.4 的挂载和卸载流程都是与 React 16.3 保持一致的,差异在于更新流程上: 在 React 16.4 中,任何因素触发的组件更新流程(包括由 this.setState 和 forceUpdate 触发的更新流程)都会触发 getDerivedStateFromProps; 而在 v 16.3 版本时,只有父组件的更新会触发该生命周期。 到这里,你已经对 getDerivedStateFromProps 相关的改变有了充分的了解。接下来,我们就基于这层了解,问出生命周期改变背后的第一个“Why”。 改变背后的第一个“Why”:为什么要用 getDerivedStateFromProps 代替 componentWillReceiveProps? 对于 getDerivedStateFromProps 这个 API,React 官方曾经给出过这样的描述: 与 componentDidUpdate 一起,这个新的生命周期涵盖过时componentWillReceiveProps 的所有用例。 在这里,请你细细品味这句话,这句话里蕴含了下面两个关键信息: getDerivedStateFromProps 是作为一个试图代替 componentWillReceiveProps 的 API 而出现的; getDerivedStateFromProps不能完全和 componentWillReceiveProps 画等号,其特性决定了我们曾经在 componentWillReceiveProps 里面做的事情,不能够百分百迁移到getDerivedStateFromProps 里。 接下来我们就展开说说这两点。 关于 getDerivedStateFromProps 是如何代替componentWillReceiveProps 的,在“挂载”环节已经讨论过:getDerivedStateFromProps 可以代替 componentWillReceiveProps 实现基于 props 派生 state。 至于它为何不能完全和 componentWillReceiveProps 画等号,则是因为它过于“专注”了。这一点,单单从getDerivedStateFromProps 这个 API 名字上也能够略窥一二。原则上来说,它能做且只能做这一件事。 乍一看,原来的 API 能做的事情更多,现在的 API 却限制重重,难道是 React 16 的生命周期方法“退化”了? 当然不是。如果你对设计模式有所了解的话,就会知道,一个 API 并非越庞大越复杂才越优秀。或者说得更直接一点,庞大和复杂的 API 往往会带来维护层面的“灾难”。 说回 getDerivedStateFromProps 这个 API,它相对于早期的 componentWillReceiveProps 来说,正是做了“合理的减法”。而做这个减法的决心之强烈,从 getDerivedStateFromProps 直接被定义为 static 方法这件事上就可见一斑—— static 方法内部拿不到组件实例的 this,这就导致你无法在 getDerivedStateFromProps 里面做任何类似于 this.fetch()、不合理的 this.setState(会导致死循环的那种)这类可能会产生副作用的操作。 因此,getDerivedStateFromProps 生命周期替代 componentWillReceiveProps 的背后,是 React 16 在强制推行“只用 getDerivedStateFromProps 来完成 props 到 state 的映射”这一最佳实践。意在确保生命周期函数的行为更加可控可预测,从根源上帮开发者避免不合理的编程方式,避免生命周期的滥用;同时,也是在为新的 Fiber 架构铺路。 到这里,相信你已经对 getDerivedStateFromProps 吃得透透的了。至于什么是 Fiber 架构,这条路该怎么铺,你将在本课时后续的内容中找到答案。现在,我们得回到“更新”这条工作流里来,一起去看看getSnapshotBeforeUpdate 是怎么一回事儿。 消失的 componentWillUpdate 与新增的 getSnapshotBeforeUpdate 咱们先来看看 getSnapshotBeforeUpdate 是什么: getSnapshotBeforeUpdate(prevProps, prevState) { // ... } 这个方法和 getDerivedStateFromProps 颇有几分神似,它们都强调了“我需要一个返回值”这回事。区别在于 getSnapshotBeforeUpdate 的返回值会作为第三个参数给到 componentDidUpdate。它的执行时机是在 render 方法之后,真实 DOM 更新之前。在这个阶段里,我们可以同时获取到更新前的真实 DOM 和更新前后的 state&props 信息。 尽管在实际工作中,需要用到这么多信息的场景并不多,但在对于实现一些特殊的需求来说,没它还真的挺难办。这里我举一个非常有代表性的例子:实现一个内容会发生变化的滚动列表,要求根据滚动列表的内容是否发生变化,来决定是否要记录滚动条的当前位置。 这个需求的前半截要求我们对比更新前后的数据(感知变化),后半截则需要获取真实的 DOM 信息(获取位置),这时用 getSnapshotBeforeUpdate 来解决就再合适不过了。 对于这个生命周期,需要重点把握的是它与 componentDidUpdate 间的通信过程。在 Demo 中我给出了一个使用示例,它将帮助你更加具体地认知这个过程。代码如下: // 组件更新时调用 getSnapshotBeforeUpdate(prevProps, prevState) { console.log(\"getSnapshotBeforeUpdate方法执行\"); return \"haha\"; } // 组件更新后调用componentDidUpdate(prevProps, prevState, valueFromSnapshot) { console.log(“componentDidUpdate方法执行”); console.log(“从 getSnapshotBeforeUpdate 获取到的值是”, valueFromSnapshot);} 现在我们点击 Demo 界面上“修改子组件文本内容”按钮,就可以看到这两个生命周期的通信效果,如下图所示: 值得一提的是,这个生命周期的设计初衷,是为了“与 componentDidUpdate 一起,涵盖过时的 componentWillUpdate 的所有用例”(引用自 React 官网)。getSnapshotBeforeUpdate 要想发挥作用,离不开 componentDidUpdate 的配合。 那么换个角度想想,为什么 componentWillUpdate 就非死不可呢?说到底,还是因为它“挡了 Fiber 的路”。各位莫慌,咱们离真相越来越近了~ Unmounting 阶段:组件的卸载 我们先继续把完整的生命周期流程走完,以下是组件卸载阶段的示意图: 卸载阶段的生命周期与 React 15 完全一致,只涉及 componentWillUnmount 这一个生命周期,此处不再重复讲解。 接下来,就让一切变化背后的”始作俑者“ Fiber 架构来和大家打个招呼吧! 透过现象看本质:React 16 缘何两次求变? Fiber 架构简析 Fiber 是 React 16 对 React 核心算法的一次重写。关于 Fiber,我将在“模块二:核心原理”花大量的篇幅来介绍它的原理和细节。在本课时,你只需要 get 到这一个点:Fiber 会使原本同步的渲染过程变成异步的。 在 React 16 之前,每当我们触发一次组件的更新,React 都会构建一棵新的虚拟 DOM 树,通过与上一次的虚拟 DOM 树进行 diff,实现对 DOM 的定向更新。这个过程,是一个递归的过程。下面这张图形象地展示了这个过程的特征: 如图所示,同步渲染的递归调用栈是非常深的,只有最底层的调用返回了,整个渲染过程才会开始逐层返回。这个漫长且不可打断的更新过程,将会带来用户体验层面的巨大风险:同步渲染一旦开始,便会牢牢抓住主线程不放,直到递归彻底完成。在这个过程中,浏览器没有办法处理任何渲染之外的事情,会进入一种无法处理用户交互的状态。因此若渲染时间稍微长一点,页面就会面临卡顿甚至卡死的风险。 而 React 16 引入的 Fiber 架构,恰好能够解决掉这个风险:Fiber 会将一个大的更新任务拆解为许多个小任务。每当执行完一个小任务时,渲染线程都会把主线程交回去,看看有没有优先级更高的工作要处理,确保不会出现其他任务被“饿死”的情况,进而避免同步渲染带来的卡顿。在这个过程中,渲染线程不再“一去不回头”,而是可以被打断的,这就是所谓的“异步渲染”,它的执行过程如下图所示: 如果你初学 Fiber,对上面的两段描述感到陌生或者说“吃不透”,这都是正常的。在本课时,你大可不必如此苛求自己,只需对“同步渲染”和“异步渲染”这两个概念有一个大致的印象,同时把握住 Fiber 架构下“任务拆解”和“可打断”这两个特性即可。接下来,我们继续往下走,看看“同步”变“异步”这个过程,是如何对生命周期构成影响的。 换个角度看生命周期工作流 Fiber 架构的重要特征就是可以被打断的异步渲染模式。但这个“打断”是有原则的,根据“能否被打断”这一标准,React 16 的生命周期被划分为了 render 和 commit 两个阶段,而 commit 阶段又被细分为了 pre-commit 和 commit。每个阶段所涵盖的生命周期如下图所示: 我们先来看下三个阶段各自有哪些特征(以下特征翻译自上图)。 render 阶段:纯净且没有副作用,可能会被 React 暂停、终止或重新启动。 pre-commit 阶段:可以读取 DOM。 commit 阶段:可以使用 DOM,运行副作用,安排更新。 总的来说,render 阶段在执行过程中允许被打断,而 commit 阶段则总是同步执行的。 为什么这样设计呢?简单来说,由于 render 阶段的操作对用户来说其实是“不可见”的,所以就算打断再重启,对用户来说也是零感知。而 commit 阶段的操作则涉及真实 DOM 的渲染,再狂的框架也不敢在用户眼皮子底下胡乱更改视图,所以这个过程必须用同步渲染来求稳。 细说生命周期“废旧立新”背后的思考 在 Fiber 机制下,render 阶段是允许暂停、终止和重启的。当一个任务执行到一半被打断后,下一次渲染线程抢回主动权时,这个任务被重启的形式是“重复执行一遍整个任务”而非“接着上次执行到的那行代码往下走”。这就导致 render 阶段的生命周期都是有可能被重复执行的。 带着这个结论,我们再来看看 React 16 打算废弃的是哪些生命周期: componentWillMount; componentWillUpdate; componentWillReceiveProps。 这些生命周期的共性,就是它们都处于 render 阶段,都可能重复被执行,而且由于这些 API 常年被滥用,它们在重复执行的过程中都存在着不可小觑的风险。 别的不说,说说我自己在团队 code review 中见过的“骚操作”吧。在“componentWill”开头的生命周期里,你习惯于做的事情可能包括但不限于: setState(); fetch 发起异步请求; 操作真实 DOM。 这些操作的问题(或不必要性)包括但不限于以下 3 点: (1)完全可以转移到其他生命周期(尤其是 componentDidxxx)里去做。 比如在 componentWillMount 里发起异步请求。很多同学因为太年轻,以为这样做就可以让异步请求回来得“早一点”,从而避免首次渲染白屏。 可惜你忘了,异步请求再怎么快也快不过(React 15 下)同步的生命周期。componentWillMount 结束后,render 会迅速地被触发,所以说首次渲染依然会在数据返回之前执行。这样做不仅没有达到你预想的目的,还会导致服务端渲染场景下的冗余请求等额外问题,得不偿失。 (2)在 Fiber 带来的异步渲染机制下,可能会导致非常严重的 Bug。 试想,假如你在 componentWillxxx 里发起了一个付款请求。由于 render 阶段里的生命周期都可以重复执行,在 componentWillxxx 被打断 + 重启多次后,就会发出多个付款请求。 比如说,这件商品单价只要 10 块钱,用户也只点击了一次付款。但实际却可能因为 componentWillxxx 被打断 + 重启多次而多次调用付款接口,最终付了 50 块钱;又或者你可能会习惯在 componentWillReceiveProps 里操作 DOM(比如说删除符合某个特征的元素),那么 componentWillReceiveProps 若是执行了两次,你可能就会一口气删掉两个符合该特征的元素。 结合上面的分析,我们再去思考 getDerivedStateFromProps 为何会在设计层面直接被约束为一个触碰不到 this 的静态方法,其背后的原因也就更加充分了——避免开发者触碰 this,就是在避免各种危险的骚操作。 (3)即使你没有开启异步,React 15 下也有不少人能把自己“玩死”。 比如在 componentWillReceiveProps 和 componentWillUpdate 里滥用 setState 导致重复渲染死循环的,大家都懂哈(邪魅一笑)。 总的来说,React 16 改造生命周期的主要动机是为了配合 Fiber 架构带来的异步渲染机制。在这个改造的过程中,React 团队精益求精,针对生命周期中长期被滥用的部分推行了具有强制性的最佳实践。这一系列的工作做下来,首先是确保了 Fiber 机制下数据和视图的安全性,同时也确保了生命周期方法的行为更加纯粹、可控、可预测。 总结 通过 02 和 03 两个课时的学习,大家已经对 React 15、16 两个版本的生命周期有了深入的掌握,同时对 React 生命周期的一系列的变化以及其背后的原因都有了深刻而健全的理解。 生命周期看似简单,但要想真正吃透,竟然需要挑战这么长的一个知识链路,实在不简单!在使用 React 进行项目开发的 5 年里,我曾不止一次地为各路合作伙伴在生命周期里“为所欲为”而感到痛苦,也曾不止一次地为 React 基础知识结构摇摇欲坠的候选人感到可惜。若你能够耐下心来彻底消化掉这两个课时,相信这世上定能多出一个靠谱的前端! 话说回来,现有的生命周期,虽然已经对方法的最佳实践做了强约束,但是仍然无法覆盖所有的“误操作”,其中最为典型的,就是对 getDerivedStateFromProps 的滥用。关于这点,社区的讨论不是很多,但是 React 团队给出的这篇文章就帮助大家规避“误操作”来说是绰绰有余的。 经过了漫长的两个课时的学习,我们终于征服了生命周期这座小山包。一个组件的一生如何度过,我们已经领教过了。那么,多个组件之间如何“心意相通”呢?在下个课时,将围绕“数据在组件间的流动”展开讲解,探索“心意相通”的艺术。","link":"/2022/09/24/frontEnd/inDepthAndSimpleReact/reactStudy-EP3/"},{"title":"S02E06 估值财务系统","text":"估值财务系统培训 一、基金运营流程 <br> 估值财务系统的功能1、估值:估算价值 对股票、债券、回购、期货等基金购买持有的证券进行价值估算 2、财务:财务管理、核算 自动化会计凭证账务管理 通用财务报表 3、必要的数据处理能力总结:实现证券公司投资后的自动化会计核算,代替人工进行会计核算的基金财务核算系统估值财务基础模块划分估值流程说明估值系统参数基础参数: 基金代码 基金名称 管理人 管理人费率 托管人 托管人费率 成立时间 席位号 股东代码 头寸科目 科目设置:同类基金科目可复用 备付金与保证金 (最低)结算备付金: 保证金: 担保交收与非担保交收: 估值证券参数公共参数 所有基金通用参数,展示节假日信息。 估值方法场内交易市场数据流2021年7月8日 长沙","link":"/2021/07/08/finance/financeStudy-EP6/"},{"title":"EP06 React-Hook 设计动机与工作模式(上)","text":"React-Hook 设计动机与工作模式(上) 从本课时开始,我们将逐步进入 React-Hooks 的世界。 在动笔写 React-Hooks 之前,我发现许多人对这块的知识非常不自信,至少在面试场景下,几乎没有几个人在聊到 React-Hooks 的时候,能像聊 Diff 算法、Fiber 架构一样滔滔不绝、言之有物。后来我仔细反思了一下,认为问题应该出在学习姿势上。 提起 React-Hooks,可能很多人的第一反应,都会是 useState、useEffect、useContext 这些琐碎且繁多的 API。似乎 React-Hooks 就是一坨没有感情的工具性代码,压根没有啥玄妙的东西在里面,那些大厂面试官天天让咱聊 React-Hooks,到底是想听啥呢? 掌握 React-Hooks 的正确姿势 前面我和你聊到过,当我们由浅入深地认知一样新事物的时候,往往需要遵循“Why→What→How”这样的一个认知过程。 在我的读者中,不少人在“What”和“How”这两个环节做得都不错,但是却疏于钻研背后的“Why”。其实这三者是相辅相成、缺一不可的:当我们了解了具体的“What”和“How”之后,往往能够更加具象地回答理论层面“Why”的问题;而我们对“Why”的探索和认知,也必然会反哺到对“What”的理解和对“How”的实践。 这其中,我们尤其不能忽略对“Why”的把控。 React-Hooks 自 React 16.8 以来才真正被推而广之,对我们每一个老 React 开发来说,它都是一个新事物。如果在认知它的过程当中,我们能够遵循“Why→What→How”这样的一个学习法则,并且以此为线索,梳理出属于自己的完整知识链路。那么我相信,面对再刁钻的面试官,你都可以做到心中有数、对答如流。 接下来两个课时,我们就遵循这个学习法则,向 React-Hooks 发起挑战,真正理解它背后的设计动机与工作模式。 React-Hooks 设计动机初探 开篇我们先来聊“Why”。React-Hooks 这个东西比较特别,它是 React 团队在真刀真枪的 React 组件开发实践中,逐渐认知到的一个改进点,这背后其实涉及对类组件和函数组件两种组件形式的思考和侧重。因此,你首先得知道,什么是类组件、什么是函数组件,并完成对这两种组件形式的辨析。 何谓类组件(Class Component) 所谓类组件,就是基于 ES6 Class 这种写法,通过继承 React.Component 得来的 React 组件。以下是一个典型的类组件: class DemoClass extends React.Component { // 初始化类组件的 state state = { text: “” }; // 编写生命周期方法 didMount componentDidMount() { // 省略业务逻辑 } // 编写自定义的实例方法 changeText = (newText) => { // 更新 state this.setState({ text: newText }); }; // 编写生命周期方法 render render() { return ( <div className=“demoClass”> <p>{this.state.text}</p> <button onClick={this.changeText}>点我修改</button> </div> ); }} 何谓函数组件/无状态组件(Function Component/Stateless Component) 函数组件顾名思义,就是以函数的形态存在的 React 组件。早期并没有 React-Hooks 的加持,函数组件内部无法定义和维护 state,因此它还有一个别名叫“无状态组件”。以下是一个典型的函数组件: function DemoFunction(props) { const { text } = props return ( <div className=\"demoFunction\"> <p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p> </div> ); } 函数组件与类组件的对比:无关“优劣”,只谈“不同” 我们先基于上面的两个 Demo,从形态上对两种组件做区分。它们之间肉眼可见的区别就包括但不限于: 类组件需要继承 class,函数组件不需要; 类组件可以访问生命周期方法,函数组件不能; 类组件中可以获取到实例化后的 this,并基于这个 this 做各种各样的事情,而函数组件不可以; 类组件中可以定义并维护 state(状态),而函数组件不可以; ...... 单就我们列出的这几点里面,频繁出现了“类组件可以 xxx,函数组件不可以 xxx”,这是否就意味着类组件比函数组件更好呢? 答案当然是否定的。你可以说,在 React-Hooks 出现之前的世界里,类组件的能力边界明显强于函数组件,但要进一步推导“类组件强于函数组件”,未免显得有些牵强。同理,一些文章中一味鼓吹函数组件轻量优雅上手迅速,不久的将来一定会把类组件干没(类组件:我做错了什么?)之类的,更是不可偏听偏信。 当我们讨论这两种组件形式时,不应怀揣“孰优孰劣”这样的成见,而应该更多地去关注两者的不同,进而把不同的特性与不同的场景做连接,这样才能求得一个全面的、辩证的认知。 重新理解类组件:包裹在面向对象思想下的“重装战舰” 类组件是面向对象编程思想的一种表征。面向对象是一个老生常谈的概念了,当我们应用面向对象的时候,总是会有意或无意地做这样两件事情。 封装:将一类属性和方法,“聚拢”到一个 Class 里去。 继承:新的 Class 可以通过继承现有 Class,实现对某一类属性和方法的复用。 React 类组件也不例外。我们再次审视一下这个典型的类组件 Case: class DemoClass extends React.Component { // 初始化类组件的 state state = { text: “” }; // 编写生命周期方法 didMount componentDidMount() { // 省略业务逻辑 } // 编写自定义的实例方法 changeText = (newText) => { // 更新 state this.setState({ text: newText }); }; // 编写生命周期方法 render render() { return ( <div className=“demoClass”> <p>{this.state.text}</p> <button onClick={this.changeText}>点我修改</button> </div> ); }} 不难看出,React 类组件内部预置了相当多的“现成的东西”等着你去调度/定制,state 和生命周期就是这些“现成东西”中的典型。要想得到这些东西,难度也不大,你只需要轻轻地继承一个 React.Component 即可。 这种感觉就好像是你不费吹灰之力,就拥有了一辆“重装战舰”,该有的枪炮导弹早已配备整齐,就等你操纵控制台上的一堆开关了。 毋庸置疑,类组件给到开发者的东西是足够多的,但“多”就是“好”吗?其实未必。 把一个人塞进重装战舰里,他就一定能操纵这台战舰吗?如果他没有经过严格的训练,不清楚每一个操作点的内涵,那他极有可能会把炮弹打到友军的营地里去。 React 类组件,也有同样的问题——它提供了多少东西,你就需要学多少东西。假如背不住生命周期,你的组件逻辑顺序大概率会变成一团糟。“大而全”的背后,是不可忽视的学习成本。 再想这样一个场景:假如我现在只是需要打死一只蚊子,而不是打掉一个军队。这时候继续开动重装战舰,是不是正应了那句老话——“可以,但没有必要”。这也是类组件的一个不便,它太重了,对于解决许多问题来说,编写一个类组件实在是一个过于复杂的姿势。复杂的姿势必然带来高昂的理解成本,这也是我们所不想看到的。 更要命的是,由于开发者编写的逻辑在封装后是和组件粘在一起的,这就使得类**组件内部的逻辑难以实现拆分和复用。**如果你想要打破这个僵局,则需要进一步学习更加复杂的设计模式(比如高阶组件、Render Props 等),用更高的学习成本来交换一点点编码的灵活度。 这一切的一切,光是想想就让人头秃。所以说,类组件固然强大, 但它绝非万能。 深入理解函数组件:呼应 React 设计思想的“轻巧快艇” 我们再来看这个函数组件的 case: function DemoFunction(props) { const { text } = props return ( <div className=\"demoFunction\"> <p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p> </div> ); } 当然啦,要是你以为函数组件的简单是因为它只能承担渲染这一种任务,那可就太小瞧它了。它同样能够承接相对复杂的交互逻辑,像这样: function DemoFunction(props) { const { text } = props const showAlert = ()=> { alert(我接收到的文本是<span class="hljs-subst">${text}</span>) } return ( <div className=“demoFunction”> <p>{function 组件所接收到的来自外界的文本内容是:[${text}]}</p> <button onClick={showAlert}>点击弹窗</button> </div> );} 相比于类组件,函数组件肉眼可见的特质自然包括轻量、灵活、易于组织和维护、较低的学习成本等。这些要素毫无疑问是重要的,它们也确实驱动着 React 团队做出改变。但是除此之外,还有一个非常容易被大家忽视、也极少有人能真正理解到的知识点,我在这里要着重讲一下。这个知识点缘起于 React 作者 Dan 早期特意为类组件和函数组件写过的一篇非常棒的对比文章,这篇文章很长,但是通篇都在论证这一句话: 函数组件会捕获 render 内部的状态,这是两类组件最大的不同。 初读这篇文章时,我像文中的作者一样,感慨 JS 闭包机制竟能给到我们这么重要的解决问题的灵感。但在反复思考过后的现在,我更希望引导我的读者们去认知到这样一件事情——类组件和函数组件之间,纵有千差万别,但最不能够被我们忽视掉的,是心智模式层面的差异,是面向对象和函数式编程这两套不同的设计思想之间的差异。 说得更具体一点,函数组件更加契合 React 框架的设计理念。何出此言?不要忘了这个赫赫有名的 React 公式: 不夸张地说,React 组件本身的定位就是函数,一个吃进数据、吐出 UI 的函数。作为开发者,我们编写的是声明式的代码,而 React 框架的主要工作,就是及时地把声明式的代码转换为命令式的 DOM 操作,把数据层面的描述映射到用户可见的 UI 变化中去。这就意味着从原则上来讲,React 的数据应该总是紧紧地和渲染绑定在一起的,而类组件做不到这一点。 为什么类组件做不到?这里我摘出上述文章中的 Demo,站在一个新的视角来解读一下“**函数组件会捕获 render 内部的状态,这是两类组件最大的不同”**这个结论。首先我们来看这样一个类组件: class ProfilePage extends React.Component { showMessage = () => { alert('Followed ' + this.props.user); }; handleClick = () => { setTimeout(this.showMessage, 3000); }; render() { return <button onClick={this.handleClick}>Follow</button>; } } 这个组件返回的是一个按钮,交互内容也很简单:点击按钮后,过 3s,界面上会弹出“Followed xxx”的文案。类似于我们在微博上点击“关注某人”之后弹出的“已关注”这样的提醒。 看起来好像没啥毛病,但是如果你在这个在线 Demo中尝试点击基于类组件形式编写的 ProfilePage 按钮后 3s 内把用户切换为 Sophie,你就会看到如下图所示的效果: 图源:https://overreacted.io/how-are-function-components-different-from-classes/ 明明我们是在 Dan 的主页点击的关注,结果却提示了“Followed Sophie”! 这个现象必然让许多人感到困惑:user 的内容是通过 props 下发的,props 作为不可变值,为什么会从 Dan 变成 Sophie 呢? 因为虽然 props 本身是不可变的,但 this 却是可变的,this 上的数据是可以被修改的,this.props 的调用每次都会获取最新的 props,而这正是 React 确保数据实时性的一个重要手段。 多数情况下,在 React 生命周期对执行顺序的调控下,this.props 和 this.state 的变化都能够和预期中的渲染动作保持一致。但在这个案例中,我们通过 setTimeout 将预期中的渲染推迟了 3s,打破了 this.props 和渲染动作之间的这种时机上的关联,进而导致渲染时捕获到的是一个错误的、修改后的 this.props。这就是问题的所在。 但如果我们把 ProfilePage 改造为一个像这样的函数组件: function ProfilePage(props) { const showMessage = () => { alert('Followed ' + props.user); }; const handleClick = () => { setTimeout(showMessage, 3000); }; return ( <button onClick={handleClick}>Follow</button> ); } 事情就会大不一样。 props 会在 ProfilePage 函数执行的一瞬间就被捕获,而 props 本身又是一个不可变值,因此我们可以充分确保从现在开始,在任何时机下读取到的 props,都是最初捕获到的那个 props。当父组件传入新的 props 来尝试重新渲染 ProfilePage 时,本质上是基于新的 props 入参发起了一次全新的函数调用,并不会影响上一次调用对上一个 props 的捕获。这样一来,我们便确保了渲染结果确实能够符合预期。 如果你认真阅读了我前面说过的那些话,相信你现在一定也不仅仅能够充分理解 Dan 所想要表达的“函数组件会捕获 render 内部的状态”这个结论,而是能够更进一步地意识到这样一件事情:函数组件真正地把数据和渲染绑定到了一起。 经过岁月的洗礼,React 团队显然也认识到了,函数组件是一个更加匹配其设计理念、也更有利于逻辑拆分与重用的组件表达形式,接下来便开始“用脚投票”,用实际行动支持开发者编写函数式组件。于是,React-Hooks 便应运而生。 Hooks 的本质:一套能够使函数组件更强大、更灵活的“钩子” React-Hooks 是什么?它是一套能够使函数组件更强大、更灵活的“钩子”。 前面我们已经说过,函数组件比起类组件“少”了很多东西,比如生命周期、对 state 的管理等。这就给函数组件的使用带来了非常多的局限性,导致我们并不能使用函数这种形式,写出一个真正的全功能的组件。 React-Hooks 的出现,就是为了帮助函数组件补齐这些(相对于类组件来说)缺失的能力。 如果说函数组件是一台轻巧的快艇,那么 React-Hooks 就是一个内容丰富的零部件箱。“重装战舰”所预置的那些设备,这个箱子里基本全都有,同时它还不强制你全都要,而是允许你自由地选择和使用你需要的那些能力,然后将这些能力以 Hook(钩子)的形式“钩”进你的组件里,从而定制出一个最适合你的“专属战舰”。 总结 行文至此,关于“Why”的研究已经基本到位,对于“What”的认知也已经初见眉目。虽然本课时并没有贴上哪怕一行 React-Hooks 相关的代码,但我相信,你对 React-Hooks 本质的把握已经超越了非常多的 React 开发者。 在下个课时,我们将会和 React-Hooks 面对面交锋,从编码层面上认知“What”,从实践角度理解“How”。相信在课时的最后,你会对本文所讲解的“Why”有更深刻的理解和感悟。","link":"/2022/12/12/frontEnd/inDepthAndSimpleReact/reactStudy-EP6/"},{"title":"EP04 数据是如何在 React 组件之间流动的?(上)","text":"数据是如何在 React 组件之间流动的?(上) 通过前面 3 个课时的学习,相信你已经对 React 生命周期相关的“Why”“What”和“How”有了系统的理解和掌握。当我们谈论生命周期时,其实谈论的是组件的“内心世界”。但组件和人是一样的,它不仅需要拥有丰富的内心世界,还应该建立健全的“人际关系”,要学会沟通和表达。 从本课时开始,我们将一起探索蕴含在 React 组件中的“沟通与表达”的艺术。我们知道,React 的核心特征是“数据驱动视图”,这个特征在业内有一个非常有名的函数式来表达: 这个表达式有很多的版本,一些版本会把入参里的 data 替换成 state,但它们本质上都指向同一个含义,那就是React 的视图会随着数据的变化而变化。数据这个角色在 React 中的地位可见一斑。 在 React 中,如果说两个组件之间希望能够产生“耦合”(即 A 组件希望能够通过某种方式影响到 B 组件),那么毫无疑问,这两个组件必须先建立数据上的连接,以实现所谓的“组件间通信”。 “组件间通信”的背后是一套环环相扣的 React 数据流解决方案。虽然这套解决方案在业内已经有了比较成熟和稳定的结论,但许多人仍然会因为知识的系统性和整体性不强而吃亏。 在前面三个课时中,我们的学习思路是往纵深处去寻觅:铺陈大量的前置知识,然后一步一步地去询问生命周期背后的“Why”,最终揪出 Fiber 架构这个大 boss(不过学到这里,这个“纵深”我们才只挖到一半,专栏第二模块还有一大波 Fiber 原理等待我们继续寻觅)。 在接下来的第 04 和 05 课时中,我们要做的事情则更倾向于横向的“聚合”:我将用简单易懂的语言,帮你理解当下实践中 React 数据通信的四个大方向,并针对每个方向给出具体的场景和用例。这些知识本身并不难,但摊子却可以铺得非常大,相关的问题在面试中也始终具备较高的区分度。要想扎扎实实掌握,必须耐下心、沉住气,在学习过程中主动地去串联自己的知识链路。 基于 props 的单向数据流 既然 props 是组件的入参,那么组件之间通过修改对方的入参来完成数据通信就是天经地义的事情了。不过,这个“修改”也是有原则的——你必须确保所有操作都在“单向数据流”这个前提下。 所谓单向数据流,指的就是当前组件的 state 以 props 的形式流动时,只能流向组件树中比自己层级更低的组件。 比如在父-子组件这种嵌套关系中,只能由父组件传 props 给子组件,而不能反过来。 听上去虽然限制重重,但用起来却是相当的灵活。基于 props 传参这种形式,我们可以轻松实现父-子通信、子-父通信和兄弟组件通信。 父-子组件通信 原理讲解 这是最常见、也是最好解决的一个通信场景。React 的数据流是单向的,父组件可以直接将 this.props 传入子组件,实现父-子间的通信。这里我给出一个示例。 编码实现 子组件编码内容: function Child(props) { return ( <div className=\"child\"> <p>{`子组件所接收到的来自父组件的文本内容是:[${props.fatherText}]`}</p> </div> ); } 父组件编码内容: class Father extends React.Component { // 初始化父组件的 state state = { text: \"初始化的父组件的文本\" }; // 按钮的监听函数,用于更新 text 值 changeText = () => { this.setState({ text: \"改变后的父组件文本\" }); }; // 渲染父组件 render() { return ( <div className=\"father\"> <button onClick={this.changeText}> 点击修改父组件传入子组件的文本 </button> {/* 引入子组件,并通过 props 下发具体的状态值实现父-子通信 */} <Child fatherText={this.state.text} /> </div> ); } } 视图层验证 我们直接对父组件进行渲染,可以看到大致如下图所示的界面: 通过子组件顺利读取到父组件的 this.props.text,从这一点可以看出,父-子之间的通信是没有问题的。此时假如我们点击父组件中的按钮,父组件的 this.state.text 会发生变化,同时子组件读取到的 props.text 也会跟着发生变化(如下图所示),也就是说,父子组件的数据始终保持一致。 由此我们便充分验证了父-子组件基于 props 实现通信的可行性。 子-父组件通信 原理讲解 考虑到 props 是单向的,子组件并不能直接将自己的数据塞给父组件,但 props 的形式也可以是多样的。假如父组件传递给子组件的是一个绑定了自身上下文的函数,那么子组件在调用该函数时,就可以将想要交给父组件的数据以函数入参的形式给出去,以此来间接地实现数据从子组件到父组件的流动。 编码实现 这里我们只需对父-子通信中的示例稍做修改,就可以完成子-父组件通信的可行性验证。 首先是对子组件的修改。在 Child 中,我们需要增加对状态的维护,以及对 Father 组件传入的函数形式入参的调用。子组件编码内容如下,修改点我已在代码中以注释的形式标出: class Child extends React.Component { // 初始化子组件的 state state = { text: '子组件的文本' } // 子组件的按钮监听函数 changeText = () => { // changeText 中,调用了父组件传入的 changeFatherText 方法 this.props.changeFatherText(this.state.text) } render() { return ( <div className=“child”> {/* 注意这里把修改父组件文本的动作放在了 Child 里 */} <button onClick={this.changeText}> 点击更新父组件的文本 </button> </div> ); }} 在父组件中,我们只需要在 changeText 函数上开一个传参的口子,作为数据通信的入口,然后把 changeText 放在 props 里交给子组件即可。父组件的编码内容如下: class Father extends React.Component { // 初始化父组件的 state state = { text: \"初始化的父组件的文本\" }; // 这个方法会作为 props 传给子组件,用于更新父组件 text 值。newText 正是开放给子组件的数据通信入口 changeText = (newText) => { this.setState({ text: newText }); }; // 渲染父组件 render() { return ( <div className=\"father\"> <p>{`父组件的文本内容是:[${this.state.text}]`}</p> {/* 引入子组件,并通过 props 中下发可传参的函数 实现子-父通信 */} <Child changeFatherText={this.changeText} /> </div> ); } 视图层验证 新的示例渲染后的界面大致如下图所示: 注意,在这个 case 中,我们将具有更新数据能力的按钮转移到了子组件中。 当点击子组件中的按钮时,会调用已经绑定了父组件上下文的 this.props.changeFatherText 方法,同时将子组件的 this.state.text 以函数入参的形式传入,由此便能够间接地用子组件的 state 去更新父组件的 state。 点击按钮后,父组件的文本会按照我们的预期被子组件更新掉,如下图所示: 兄弟组件通信 原理讲解 兄弟组件之间共享了同一个父组件,如下图所示,这是一个非常重要的先决条件。 这个先决条件使得我们可以继续利用父子组件这一层关系,将“兄弟 1 → 兄弟 2”之间的通信,转化为“兄弟 1 → 父组件”(子-父通信)、“父组件 → 兄弟 2”(父-子)通信两个步骤,如下图所示,这样一来就能够巧妙地把“兄弟”之间的新问题化解为“父子”之间的旧问题。 编码实现 接下来我们仍然从编码的角度进行验证。首先新增一个 NewChild 组件作为与 Child 组件同层级的兄弟组件。NewChild 将作为数据的发送方,将数据发送给 Child。在 NewChild 中,我们需要处理 NewChild 和 Father 之间的关系。NewChild 组件编码如下: class NewChild extends React.Component { state = { text: \"来自 newChild 的文本\" }; // NewChild 组件的按钮监听函数 changeText = () => { // changeText 中,调用了父组件传入的 changeFatherText 方法 this.props.changeFatherText(this.state.text); }; render() { return ( <div className=\"child\"> {/* 注意这里把修改父组件文本(同时也是 Child 组件的文本)的动作放在了 NewChild 里 */} <button onClick={this.changeText}>点击更新 Child 组件的文本</button> </div> ); } } 接下来看看 Father 组件。在 Father 组件中,我们通过 text 属性连接 Father 和 Child,通过 changeText 函数来连接 Father 和 NewChild。由此便把 text 属性的渲染工作交给了 Child,把 text 属性的更新工作交给 NewÇhild,以此来实现数据从 NewChild 到 Child 的流动。Father 组件编码如下: class Father extends React.Component { // 初始化父组件的 state state = { text: \"初始化的父组件的文本\" }; // 传给 NewChild 组件按钮的监听函数,用于更新父组件 text 值(这个 text 值同时也是 Child 的 props) changeText = (newText) => { this.setState({ text: newText }); }; // 渲染父组件 render() { return ( <div className=\"father\"> {/* 引入 Child 组件,并通过 props 中下发具体的状态值 实现父-子通信 */} <Child fatherText={this.state.text} /> {/* 引入 NewChild 组件,并通过 props 中下发可传参的函数 实现子-父通信 */} <NewChild changeFatherText={this.changeText} /> </div> ); } } 视图层验证 编码完成之后,界面大致的结构如下图所示: 由于整体结构稍微复杂了一些,这里我把 Father、Child 和 NewChild 在图中的大致范围标一下: 红色所圈范围为 Father 组件,它包括了 Child 和 NewChild; 灰色圈住的按钮是 NewChild 组件的渲染结果,它可以触发数据的改变; 蓝色圈住的文本是 Child 组件的渲染结果,它负责感知和渲染数据。 现在我点击位于 NewChild 组件中的“点击更新 Child 组件的文本”按钮,就可以看到 Child 会跟着发生变化,如下图所示,进而验证方案的可行性。 为什么不推荐用 props 解决其他场景的需求 至此,我们给出了 props 传参这种形式比较适合处理的三种场景。尽管这并不意味着其他场景不能用 props 处理,但如果你试图用简单的 props 传递完成更加复杂的通信需求,往往会得不偿失。这里我给你举一个比较极端的例子: 如上图所示,可以看到这是一个典型的多层嵌套组件结构。A 组件倘若想要和层层相隔的 E 组件实现通信,就必须把 props 经过 B、C、D 一层一层地传递下去。在这个过程中,反反复复的 props 传递不仅会带来庞大的工作量和代码量,还会污染中间无辜的 B、C、D 组件的属性结构。 层层传递的优点是非常简单,用已有知识就能解决,但问题是会浪费很多代码,非常烦琐,中间作为桥梁的组件会引入很多不属于自己的属性。短期来看,写代码的人会很痛苦;长期来看,整个项目的维护成本都会变得非常高昂。因此,层层传递 props 要不得。 那有没有更加灵活的解决方案,能够帮我们处理“任意组件”之间的通信需求呢?答案是不仅有,而且姿势还很多。我先从最朴素的“发布-订阅”模式讲起。 利用“发布-订阅”模式驱动数据流 “发布-订阅”模式可谓是解决通信类问题的“万金油”,在前端世界的应用非常广泛,比如: 前两年爆火的 socket.io 模块,它就是一个典型的跨端发布-订阅模式的实现; 在 Node.js 中,许多原生模块也是以 EventEmitter 为基类实现的; 不过大家最为熟知的,应该还是 Vue.js 中作为常规操作被推而广之的“全局事件总线” EventBus。 这些应用之间虽然名字各不相同,但内核是一致的,也就是我们下面要讲到的“发布-订阅”模型。 理解事件的发布-订阅机制 发布-订阅机制早期最广泛的应用,应该是在浏览器的 DOM 事件中。 相信有过原生 JavaScript 开发经验的同学,对下面这样的用法都不会陌生: target.addEventListener(type, listener, useCapture); 通过调用 addEventListener 方法,我们可以创建一个事件监听器,这个动作就是“订阅”。比如我可以监听 click(点击)事件: el.addEventListener(\"click\", func, false); 这样一来,当 click 事件被触发时,事件会被“发布”出去,进而触发监听这个事件的 func 函数。这就是一个最简单的发布-订阅案例。 使用发布-订阅模式的优点在于,监听事件的位置和触发事件的位置是不受限的,就算相隔十万八千里,只要它们在同一个上下文里,就能够彼此感知。这个特性,太适合用来应对“任意组件通信”这种场景了。 发布-订阅模型 API 设计思路 通过前面的讲解,不难看出发布-订阅模式中有两个关键的动作:事件的监听(订阅)和事件的触发(发布),这两个动作自然而然地对应着两个基本的 API 方法。 on():负责注册事件的监听器,指定事件触发时的回调函数。 emit():负责触发事件,可以通过传参使其在触发的时候携带数据 。 最后,只进不出总是不太合理的,我们还要考虑一个 off() 方法,必要的时候用它来删除用不到的监听器: off():负责监听器的删除。 发布-订阅模型编码实现 “发布-订阅”模式不仅在应用层面十分受欢迎,它更是面试官的心头好。在涉及设计模式的面试中,如果只允许出一道题,那么我相信大多数的面试官都会和我一样,会毫不犹豫地选择考察“发布-订阅模式的实现”。 接下来我就手把手带你来做这道题,写出一个同时拥有 on、emit 和 off 的 EventEmitter。 在写代码之前,先要捋清楚思路。这里我把“实现 EventEmitter”这个大问题,拆解为 3 个具体的小问题,下面我们逐个来解决。 问题一:事件和监听函数的对应关系如何处理? 提到“对应关系”,应该联想到的是“映射”。在 JavaScript 中,处理“映射”我们大部分情况下都是用对象来做的。所以说在全局我们需要设置一个对象,来存储事件和监听函数之间的关系: constructor() { // eventMap 用来存储事件和监听函数之间的关系 this.eventMap= {} } 问题二:如何实现订阅? 所谓“订阅”,也就是注册事件监听函数的过程。这是一个“写”操作,具体来说就是把事件和对应的监听函数写入到 eventMap 里面去: // type 这里就代表事件的名称 on(type, handler) { // hanlder 必须是一个函数,如果不是直接报错 if(!(handler instanceof Function)) { throw new Error(\"哥 你错了 请传一个函数\") } // 判断 type 事件对应的队列是否存在 if(!this.eventMap[type]) { // 若不存在,新建该队列 this.eventMap[type] = [] } // 若存在,直接往队列里推入 handler this.eventMap[type].push(handler) } 问题三:如何实现发布? 订阅操作是一个“写”操作,相应的,发布操作就是一个“读”操作。发布的本质是触发安装在某个事件上的监听函数,我们需要做的就是找到这个事件对应的监听函数队列,将队列中的 handler 依次执行出队: // 别忘了我们前面说过触发时是可以携带数据的,params 就是数据的载体 emit(type, params) { // 假设该事件是有订阅的(对应的事件队列存在) if(this.eventMap[type]) { // 将事件队列里的 handler 依次执行出队 this.eventMap[type].forEach((handler, index)=> { // 注意别忘了读取 params handler(params) }) } } 到这里,最最关键的 on 方法和 emit 方法就实现完毕了。最后我们补充一个 off 方法: off(type, handler) { if(this.eventMap[type]) { this.eventMap[type].splice(this.eventMap[type].indexOf(handler)>>>0,1) } } 接着把这些代码片段拼接进一个 class 里面,一个核心功能完备的 EventEmitter 就完成啦: class myEventEmitter { constructor() { // eventMap 用来存储事件和监听函数之间的关系 this.eventMap = {}; } // type 这里就代表事件的名称 on(type, handler) { // hanlder 必须是一个函数,如果不是直接报错 if (!(handler instanceof Function)) { throw new Error(\"哥 你错了 请传一个函数\"); } // 判断 type 事件对应的队列是否存在 if (!this.eventMap[type]) { // 若不存在,新建该队列 this.eventMap[type] = []; } // 若存在,直接往队列里推入 handler this.eventMap[type].push(handler); } // 别忘了我们前面说过触发时是可以携带数据的,params 就是数据的载体 emit(type, params) { // 假设该事件是有订阅的(对应的事件队列存在) if (this.eventMap[type]) { // 将事件队列里的 handler 依次执行出队 this.eventMap[type].forEach((handler, index) => { // 注意别忘了读取 params handler(params); }); } } off(type, handler) { if (this.eventMap[type]) { this.eventMap[type].splice(this.eventMap[type].indexOf(handler) >>> 0, 1); } } } 下面我们对 myEventEmitter 进行一个简单的测试,创建一个 myEvent 对象作为 myEventEmitter 的实例,然后针对名为 “test” 的事件进行监听和触发: // 实例化 myEventEmitter const myEvent = new myEventEmitter(); // 编写一个简单的 handler const testHandler = function (params) { console.log(`test事件被触发了,testHandler 接收到的入参是${params}`); }; // 监听 test 事件 myEvent.on(\"test\", testHandler); // 在触发 test 事件的同时,传入希望 testHandler 感知的参数 myEvent.emit(\"test\", \"newState\"); 以上代码会输出下面红色矩形框住的部分作为运行结果: 由此可以看出,EventEmitter 的实例已经具备发布-订阅的能力,执行结果符合预期。 现在你可以试想一下,对于任意的两个组件 A 和 B,假如我希望实现双方之间的通信,借助 EventEmitter 来做就很简单了,以数据从 A 流向 B 为例。 我们可以在 B 中编写一个handler(记得将这个 handler 的 this 绑到 B 身上),在这个 handler 中进行以 B 为上下文的 this.setState 操作,然后将这个 handler 作为监听器与某个事件关联起来。比如这样: // 注意这个 myEvent 是提前实例化并挂载到全局的,此处不再重复示范实例化过程 const globalEvent = window.myEvent class B extends React.Component { // 这里省略掉其他业务逻辑 state = { newParams: \"\" }; handler = (params) => { this.setState({ newParams: params }); }; bindHandler = () => { globalEvent.on(\"someEvent\", this.handler); }; render() { return ( <div> <button onClick={this.bindHandler}>点我监听A的动作</button> <div>A传入的内容是[{this.state.newParams}]</div> </div> ); } } 接下来在 A 组件中,只需要直接触发对应的事件,然后将希望携带给 B 的数据作为入参传递给 emit 方法即可。代码如下: class A extends React.Component { // 这里省略掉其他业务逻辑 state = { infoToB: \"哈哈哈哈我来自A\" }; reportToB = () => { // 这里的 infoToB 表示 A 自身状态中需要让 B 感知的那部分数据 globalEvent.emit(\"someEvent\", this.state.infoToB); }; render() { return <button onClick={this.reportToB}>点我把state传递给B</button>; } } 如此一来,便能够实现 A 到 B 的通信了。这里我将 A 与 B 编排为兄弟组件,代码如下: export default function App() { return ( <div className=\"App\"> <B /> <A /> </div> ); } 你也可以在自己的 Demo 里将 A 和 B 定义为更加复杂的嵌套关系,这里我给出的这个 Demo 运行起来会渲染出这样的界面,如下图所示: 依次点击顶部和底部的按钮,就可以实现对 someEvent 这个事件的监听和触发,进而观察到中间这行文本的改变,如下图所示: 由此我们便可以验证到发布-订阅模式驱动 React 数据流的可行性。为了强化你对过程的理解,我将 A 与 B 的通信过程梳理进了一张图里,供你参考: 总结 本课时,我们对 React 数据流管理方案中的前两个大方向进行了学习: 使用基于 Props 的单向数据流串联父子、兄弟组件; 利用“发布-订阅”模式驱动 React 数据在任意组件间流动。 这两个方向下的解决方案,单纯从理解上来看,难度都不高。你需要把重点放在对编码的实现和理解上,尤其是基于“发布-订阅”模式实现的 EventEmitter,多年来一直是面试的大热点,务必要好好把握。 这一课时就讲到这里了,下个课时,我们将继续站在“数据在 React 组件中的流动”这个视角,对 React 中的 Context API,以及第三方数据流管理框架中的“佼佼者” Redux 进行分析,相信一定能够为你带来不一样的理解和收获。","link":"/2022/09/25/frontEnd/inDepthAndSimpleReact/reactStudy-EP4/"},{"title":"EP07 React-Hook 设计动机与工作模式(下)","text":"React-Hook 设计动机与工作模式(下) 经过第 6 课时的学习,相信你已经清楚了 React-Hooks 的来头,并理解了其背后的“设计动机”。本课时我们的任务是构建对 React-Hooks 的整体认知。 在本课时的主体部分,我将通过一系列的编码实例来帮助你认识 useState、useEffect 这两个有代表性的 Hook,这一步意在帮助初学者对 React-Hooks 可以快速上手。在此基础上,我们将重新理解“Why React-Hooks”这个问题。在课时的最后,我将结合自身的开发体验,和你分享当下这个阶段,我所认识到的 Hooks 的局限性。 注:在学习本课时的过程中,请你摒弃“认识的 API 名字越多就越牛”这种错误的学习理念。如果你希望掌握尽可能多的 Hook 的用法,点击这里可以一键进入 React-Hooks API 文档的海洋。对本课时来说,所有涉及对 API 用法的介绍都是 “教具”,仅仅是为后续更深层次的知识讲解作铺垫。 先导知识:从核心 API 看 Hooks 的基本形态 useState():为函数组件引入状态 早期的函数组件相比于类组件,其一大劣势是缺乏定义和维护 state 的能力,而 state(状态)作为 React 组件的灵魂,必然是不可省略的。因此 React-Hooks 在诞生之初,就优先考虑了对 state 的支持。useState 正是这样一个能够为函数组件引入状态的 API。 函数组件,真的很轻 在过去,你可能会为了使用 state,不得不去编写一个类组件(这里我给出一个 Demo,编码如下所示): import React, { Component } from \"react\"; export default class TextButton extends Component { constructor() { super(); this.state = { text: “初始文本” }; } changeText = () => { this.setState(() => { return { text: “修改后的文本” }; }); }; render() { const { text } = this.state; return ( <div className=“textButton”> <p>{text}</p> <button onClick={this.changeText}>点击修改文本</button> </div> ); }} 有了 useState 后,我们就可以直接在函数组件里引入 state。以下是使用 useState 改造过后的 TextButton 组件: import React, { useState } from \"react\"; export default function Button() { const [text, setText] = useState(\"初始文本\"); function changeText() { return setText(\"修改后的文本\"); } return ( <div className=\"textButton\"> <p>{text}</p> <button onClick={changeText}>点击修改文本</button> </div> ); } 上面两套代码实现的界面交互效果完全一样,而函数组件的代码量几乎是类组件代码量的一半! 如果你在第 06 课时曾或多或少地对“类组件太重了”这个观点感到茫然,那么相信眼前这个 Demo 足以让你真真切切地感受到两类组件在复杂度上的差异——同样逻辑的函数组件相比类组件而言,复杂度要低得多得多。 useState 快速上手 从用法上看,useState 返回的是一个数组,数组的第一个元素对应的是我们想要的那个 state 变量,第二个元素对应的是能够修改这个变量的 API。我们可以通过数组解构的语法,将这两个元素取出来,并且按照我们自己的想法命名。一个典型的调用示例如下: const [state, setState] = useState(initialState); 在这个示例中,我们给自己期望的那个状态变量命名为 state,给修改 state 的 API 命名为 setState。useState 中传入的 initialState 正是 state 的初始值。后续我们可以通过调用 setState,来修改 state 的值,像这样: setState(newState) 状态更新后会触发渲染层面的更新,这点和类组件是一致的。 这里需要向初学者强调的一点是:状态和修改状态的 API 名都是可以自定义的。比如在上文的 Demo 中,就分别将其自定义为 text 和 setText: const [text, setText] = useState(\"初始文本\"); “set + 具体变量名”这种命名形式,可以帮助我们快速地将 API 和它对应的状态建立逻辑联系。 当我们在函数组件中调用 React.useState 的时候,实际上是给这个组件关联了一个状态——注意,是“一个状态”而不是“一批状态”。这一点是相对于类组件中的 state 来说的。在类组件中,我们定义的 state 通常是一个这样的对象,如下所示: this.state { text: \"初始文本\", length: 10000, author: [\"xiuyan\", \"cuicui\", \"yisi\"] } 这个对象是“包容万物”的:整个组件的状态都在 state 对象内部做收敛,当我们需要某个具体状态的时候,会通过 this.state.xxx 这样的访问对象属性的形式来读取它。 而在 useState 这个钩子的使用背景下,state 就是单独的一个状态,它可以是任何你需要的 JS 类型。像这样: // 定义为数组 const [author, setAuthor] = useState([\"xiuyan\", \"cuicui\", \"yisi\"]); // 定义为数值const [length, setLength] = useState(100);// 定义为字符串const [text, setText] = useState(“初始文本”) 你还可以定义为布尔值、对象等,都是没问题的。它就像类组件中 state 对象的某一个属性一样,对应着一个单独的状态,允许你存储任意类型的值。 useEffect():允许函数组件执行副作用操作 函数组件相比于类组件来说,最显著的差异就是 state 和生命周期的缺失。useState 为函数组件引入了 state,而 useEffect 则在一定程度上弥补了生命周期的缺席。 useEffect 能够为函数组件引入副作用。过去我们习惯放在 componentDidMount、componentDidUpdate 和 componentWillUnmount 三个生命周期里来做的事,现在可以放在 useEffect 里来做,比如操作 DOM、订阅事件、调用外部 API 获取数据等。 useEffect 和生命周期函数之间的“替换”关系 我们可以通过下面这个例子来理解 useEffect 和生命周期函数之间的替换关系。这里我先给到你一个用 useEffect 编写的函数组件示例: // 注意 hook 在使用之前需要引入 import React, { useState, useEffect } from 'react'; // 定义函数组件 function IncreasingTodoList() { // 创建 count 状态及其对应的状态修改函数 const [count, setCount] = useState(0); // 此处的定位与 componentDidMount 和 componentDidUpdate 相似 useEffect(() => { // 每次 count 增加时,都增加对应的待办项 const todoList = document.getElementById(\"todoList\"); const newItem = document.createElement(\"li\"); newItem.innerHTML = `我是第${count}个待办项`; todoList.append(newItem); }); // 编写 UI 逻辑 return ( <div> <p>当前共计 {count} 个todo Item</p> <ul id=\"todoList\"></ul> <button onClick={() => setCount(count + 1)}>点我增加一个待办项</button> </div> ); } 通过上面这段代码构造出来的界面在刚刚挂载完毕时,就是如下图所示的样子: IncreasingTodoList 是一个只允许增加 item 的 ToDoList(待办事项列表)。按照 useEffect 的设定,每当我们点击“点我增加一个待办项”这个按钮,驱动 count+1 的同时,DOM 结构里也会被追加一个 li 元素。以下是连击按钮三次之后的效果图: 同样的效果,按照注释里的提示,我们也可以通过编写 class 组件来实现: import React from 'react'; // 定义类组件 class IncreasingTodoList extends React.Component { // 初始化 state state = { count: 0 } // 此处调用上个 demo 中 useEffect 中传入的函数 componentDidMount() { this.addTodoItem() } // 此处调用上个 demo 中 useEffect 中传入的函数 componentDidUpdate() { this.addTodoItem() } // 每次 count 增加时,都增加对应的待办项 addTodoItem = () => { const { count } = this.state const todoList = document.getElementById(“todoList”) const newItem = document.createElement(“li”) newItem.innerHTML = 我是第<span class="hljs-subst">${count}</span>个待办项 todoList.append(newItem) } // 定义渲染内容 render() { const { count } = this.state return ( <div> <p>当前共计 {count} 个todo Item</p> <ul id=“todoList”></ul> <button onClick={() => this.setState({ count: this.state.count + 1, }) } > 点我增加一个待办项 </button> </div> ) }} 通过这样一个对比,类组件生命周期和函数组件 useEffect 之间的转换关系可以说是跃然纸上了。 在这里,我提个醒:初学 useEffect 时,我们难免习惯于借助对生命周期的理解来推导对 useEffect 的理解。但长期来看,若是执着于这个学习路径,无疑将阻碍你真正从心智模式的层面拥抱 React-Hooks。 有时候,我们必须学会忘记旧的知识,才能够更好地拥抱新的知识。对于每一个学习 useEffect 的人来说,生命周期到 useEffect 之间的转换关系都不是最重要的,最重要的是在脑海中构建一个“组件有副作用 → 引入 useEffect”这样的条件反射——当你真正抛却类组件带给你的刻板印象、拥抱函数式编程之后,想必你会更加认同“useEffect 是用于为函数组件引入副作用的钩子”这个定义。 useEffect 快速上手 useEffect 可以接收两个参数,分别是回调函数与依赖数组,如下面代码所示: useEffect(callBack, []) useEffect 用什么姿势来调用,本质上取决于你想用它来达成什么样的效果。下面我们就以效果为线索,简单介绍 useEffect 的调用规则。 每一次渲染后都执行的副作用:传入回调函数,不传依赖数组。调用形式如下所示: useEffect(callBack) 仅在挂载阶段执行一次的副作用:传入回调函数,且这个函数的返回值不是一个函数,同时传入一个空数组。调用形式如下所示: useEffect(()=>{ // 这里是业务逻辑 }, []) 仅在挂载阶段和卸载阶段执行的副作用:传入回调函数,且这个函数的返回值是一个函数,同时传入一个空数组。假如回调函数本身记为 A, 返回的函数记为 B,那么将在挂载阶段执行 A,卸载阶段执行 B。调用形式如下所示: useEffect(()=>{ // 这里是 A 的业务逻辑 // 返回一个函数记为 B return ()=>{ }}, []) 这里需要注意,这种调用方式之所以会在卸载阶段去触发 B 函数的逻辑,是由 useEffect 的执行规则决定的:useEffect 回调中返回的函数被称为“清除函数”,当 React 识别到清除函数时,会在调用新的 effect 逻辑之前执行清除函数内部的逻辑。这个规律不会受第二个参数或者其他因素的影响,只要你在 useEffect 回调中返回了一个函数,它就会被作为清除函数来处理。 每一次渲染都触发,且卸载阶段也会被触发的副作用:传入回调函数,且这个函数的返回值是一个函数,同时不传第二个参数。如下所示: useEffect(()=>{ // 这里是 A 的业务逻辑 // 返回一个函数记为 B return ()=>{ }}) 上面这段代码就会使得 React 在每一次渲染都去触发 A 逻辑,并且在下一次 A 逻辑被触发之前去触发 B 逻辑。 其实你只要记住,如果你有一段 effect 逻辑,需要在每次调用它之前对上一次的 effect 进行清理,那么把对应的清理逻辑写进 useEffect 回调的返回函数(上面示例中的 B 函数)里就行了。 根据一定的依赖条件来触发的副作用:传入回调函数,同时传入一个非空的数组,如下所示: useEffect(()=>{ // 这是回调函数的业务逻辑 // 若 xxx 是一个函数,则 xxx 会在组件每次因 num1、num2、num3 的改变而重新渲染时被触发 return xxx }, [num1, num2, num3]) 这里我给出的一个示意数组是 [num1, num2, num3]。首先需要说明,数组中的变量一般都是来源于组件本身的数据(props 或者 state)。若数组不为空,那么 React 就会在新的一次渲染后去对比前后两次的渲染,查看数组内是否有变量发生了更新(只要有一个数组元素变了,就会被认为更新发生了),并在有更新的前提下去触发 useEffect 中定义的副作用逻辑。 Why React-Hooks:Hooks 是如何帮助我们升级工作模式的 在第 06 课时我们已经了解到,函数组件相比类组件来说,有着不少能够利好 React 组件开发的特性,而 React-Hooks 的出现正是为了强化函数组件的能力。现在,基于对 React-Hooks 编码层面的具体认知,想必你对“动机”的理解也已经上了一个台阶。这里我们就趁热打铁,针对“Why React-Hooks”这个问题,做一个加强版的总结。 相信有不少嗅觉敏锐的同学已经感觉到了——没错,这个环节就是手把手教你做“为什么需要 React-Hooks”这道面试题。以“Why xxx”开头的这种面试题,往往都没有标准答案,但会有一些关键的“点”,只要能答出关键的点,就足以证明你思考的方向是正确的,也就意味着这道题能给你加分。这里,我梳理了以下 4 条答题思路: 告别难以理解的 Class; 解决业务逻辑难以拆分的问题; 使状态逻辑复用变得简单可行; 函数组件从设计思想上来看,更加契合 React 的理念。 关于思路 4,我在上个课时已经讲得透透的了,这里我主要是借着代码的东风,把 1、2、3 摊开来给你看一下。 1. 告别难以理解的 Class:把握 Class 的两大“痛点” 坊间总有传言说 Class 是“难以理解”的,这个说法的背后是 this 和生命周期这两个痛点。 先来说说 this,在上个课时,你已经初步感受了一把 this 有多么难以捉摸。但那毕竟是个相对特殊的场景,更为我们所熟悉的,可能还是 React 自定义组件方法中的 this。看看下面这段代码: class Example extends Component { state = { name: '修言', age: '99'; }; changeAge() { // 这里会报错 this.setState({ age: '100' }); } render() { return <button onClick={this.changeAge}>{this.state.name}的年龄是{this.state.age}</button> } } 你先不用关心组件具体的逻辑,就看 changeAge 这个方法:它是 button 按钮的事件监听函数。当我点击 button 按钮时,希望它能够帮我修改状态,但事实是,点击发生后,程序会报错。原因很简单,changeAge 里并不能拿到组件实例的 this,至于为什么拿不到,我们将在第 15课时讲解其背后的原因,现在先不用关心。单就这个现象来说,略有一些 React 开发经验的同学应该都会非常熟悉。 为了解决 this 不符合预期的问题,各路前端也是各显神通,之前用 bind、现在推崇箭头函数。但不管什么招数,本质上都是在用实践层面的约束来解决设计层面的问题。好在现在有了 Hooks,一切都不一样了,我们可以在函数组件里放飞自我(毕竟函数组件是不用关心 this 的)哈哈,解放啦! 至于生命周期,它带来的麻烦主要有以下两个方面: 学习成本 不合理的逻辑规划方式 对于第一点,大家都学过生命周期,都懂。下面着重说说这“不合理的逻辑规划方式”是如何被 Hooks 解决掉的。 2. Hooks 如何实现更好的逻辑拆分 在过去,你是怎么组织自己的业务逻辑的呢?我想多数情况下应该都是先想清楚业务的需要是什么样的,然后将对应的业务逻辑拆到不同的生命周期函数里去——没错,逻辑曾经一度与生命周期耦合在一起。 在这样的前提下,生命周期函数常常做一些奇奇怪怪的事情:比如在 componentDidMount 里获取数据,在 componentDidUpdate 里根据数据的变化去更新 DOM 等。如果说你只用一个生命周期做一件事,那好像也还可以接受,但是往往在一个稍微成规模的 React 项目中,一个生命周期不止做一件事情。下面这段伪代码就很好地诠释了这一点: componentDidMount() { // 1. 这里发起异步调用 // 2. 这里从 props 里获取某个数据,根据这个数据更新 DOM // 3. 这里设置一个订阅 // 4. 这里随便干点别的什么 // …}componentWillUnMount() { // 在这里卸载订阅}componentDidUpdate() { // 1. 在这里根据 DidMount 获取到的异步数据更新 DOM // 2. 这里从 props 里获取某个数据,根据这个数据更新 DOM(和 DidMount 的第2步一样)} 像这样的生命周期函数,它的体积过于庞大,做的事情过于复杂,会给阅读和维护者带来很多麻烦。最重要的是,这些事情之间看上去毫无关联,逻辑就像是被“打散”进生命周期里了一样。比如,设置订阅和卸载订阅的逻辑,虽然它们在逻辑上是有强关联的,但是却只能被分散到不同的生命周期函数里去处理,这无论如何也不能算作是一个非常合理的设计。 而在 Hooks 的帮助下,我们完全可以把这些繁杂的操作按照逻辑上的关联拆分进不同的函数组件里:我们可以有专门管理订阅的函数组件、专门处理 DOM 的函数组件、专门获取数据的函数组件等。Hooks 能够帮助我们实现业务逻辑的聚合,避免复杂的组件和冗余的代码。 3. 状态复用:Hooks 将复杂的问题变简单 过去我们复用状态逻辑,靠的是 HOC(高阶组件)和 Render Props 这些组件设计模式,这是因为 React 在原生层面并没有为我们提供相关的途径。但这些设计模式并非万能,它们在实现逻辑复用的同时,也破坏着组件的结构,其中一个最常见的问题就是“嵌套地狱”现象。 Hooks 可以视作是 React 为解决状态逻辑复用这个问题所提供的一个原生途径。现在我们可以通过自定义 Hook,达到既不破坏组件结构、又能够实现逻辑复用的效果。 要理解上面这两段话,需要你对组件设计模式有基本的理解和应用。如果你读下来觉得一头雾水,也不必心慌。对于组件状态复用这个问题(包括 HOC、Render Props 和自定义 Hook),现在我对你的预期是“知道有这回事就可以了”。如果你实在着急,可以先通过文档中的相关内容简单了解一下。在专栏的第三模块,我会专门把这块知识提出来,放在一个更合适的上下文里给你掰开来讲。 保持清醒:Hooks 并非万能 尽管我们已经说了这么多 Hooks 的“好话”,尽管 React 团队已经用脚投票表明了对函数组件的积极态度,但我们还是要谨记这样一个基本的认知常识:事事无绝对,凡事皆有两面性。更何况 React 仅仅是推崇函数组件,并没有“拉踩”类组件,甚至还官宣了“类组件和函数组件将继续共存”这件事情。这些都在提醒我们,在认识到 Hooks 带来的利好的同时,还需要认识到它的局限性。 关于 Hooks 的局限性,目前社区鲜少有人讨论。这里我想结合团队开发过程当中遇到的一些瓶颈,和你分享实践中的几点感受: Hooks 暂时还不能完全地为函数组件补齐类组件的能力:比如 getSnapshotBeforeUpdate、componentDidCatch 这些生命周期,目前都还是强依赖类组件的。官方虽然立了“会尽早把它们加进来”的 Flag,但是说真的,这个 Flag 真的立了蛮久了……(扶额) “轻量”几乎是函数组件的基因,这可能会使它不能够很好地消化“复杂”:我们有时会在类组件中见到一些方法非常繁多的实例,如果用函数组件来解决相同的问题,业务逻辑的拆分和组织会是一个很大的挑战。我个人的感觉是,从头到尾都在“过于复杂”和“过度拆分”之间摇摆不定,哈哈。耦合和内聚的边界,有时候真的很难把握,函数组件给了我们一定程度的自由,却也对开发者的水平提出了更高的要求。 Hooks 在使用层面有着严格的规则约束:这也是我们下个课时要重点讲的内容。对于如今的 React 开发者来说,如果不能牢记并践行 Hooks 的使用原则,如果对 Hooks 的关键原理没有扎实的把握,很容易把自己的 React 项目搞成大型车祸现场。 总结 在本课时,我们结合编码层面的认知,辩证地探讨了 Hooks 带来的利好与局限性。现在,你对于 React-Hooks 的基本形态和前世今生都已经有了透彻的了解,也真刀真枪地感受到了 Hooks 带来的利好。学习至此,相信你已经建立了对 React-Hooks 的学习自信。 接下来,我们将续上本课时结尾处的“悬念”,向 React-Hooks 的执行规则发问,同时也将进入 React-Hooks 知识链路真正的深水区。","link":"/2022/12/22/frontEnd/inDepthAndSimpleReact/reactStudy-EP7/"},{"title":"EP08 深入 React-Hook 工作机制:“原则”的背后,是“原理”","text":"深入 React-Hook 工作机制:“原则”的背后,是“原理” React 团队面向开发者给出了两条 React-Hooks 的使用原则,原则的内容如下: 只在 React 函数中调用 Hook; 不要在循环、条件或嵌套函数中调用 Hook。 原则 1 无须多言,React-Hooks 本身就是 React 组件的“钩子”,在普通函数里引入意义不大。我相信更多的人在原则 2 上栽过跟头,或者说至今仍然对它半信半疑。其实,原则 2 中强调的所有“不要”,都是在指向同一个目的,那就是要确保 Hooks 在每次渲染时都保持同样的执行顺序。 为什么顺序如此重要?这就要从 Hooks 的实现机制说起了。这里我就以 useState 为例,带你从现象入手,深度探索一番 React-Hooks 的工作原理。 注:本讲 Demo 基于 React 16.8.x 版本进行演示。 从现象看问题:若不保证 Hooks 执行顺序,会带来什么麻烦? 先来看一个小 Demo: import React, { useState } from \"react\"; function PersonalInfoComponent() { // 集中定义变量 let name, age, career, setName, setCareer; // 获取姓名状态 [name, setName] = useState(“修言”); // 获取年龄状态 [age] = useState(“99”); // 获取职业状态 [career, setCareer] = useState(“我是一个前端,爱吃小熊饼干”); // 输出职业信息 console.log(“career”, career); // 编写 UI 逻辑 return ( <div className=“personalInfo”> <p>姓名:{name}</p> <p>年龄:{age}</p> <p>职业:{career}</p> <button onClick={() => { setName(“秀妍”); }} > 修改姓名 </button> </div> );} export default PersonalInfoComponent; 这个 PersonalInfoComponent 组件渲染出来的界面长这样: PersonalInfoComponent 用于对个人信息进行展示,这里展示的内容包括姓名、年龄、职业。出于测试效果需要,PersonalInfoComponent 还允许你点击“修改姓名”按钮修改姓名信息。点击一次后,“修言”会被修改为“秀妍”,如下图所示: 到目前为止,组件的行为都是符合我们的预期的,一切看上去都是那么的和谐。但倘若我对代码做一丝小小的改变,把一部分的 useState 操作放进 if 语句里,事情就会变得大不一样。改动后的代码如下: import React, { useState } from \"react\"; // isMounted 用于记录是否已挂载(是否是首次渲染) let isMounted = false; function PersonalInfoComponent() { // 定义变量的逻辑不变 let name, age, career, setName, setCareer; // 这里追加对 isMounted 的输出,这是一个 debug 性质的操作 console.log(“isMounted is”, isMounted); // 这里追加 if 逻辑:只有在首次渲染(组件还未挂载)时,才获取 name、age 两个状态 if (!isMounted) { // eslint-disable-next-line [name, setName] = useState(“修言”); // eslint-disable-next-line [age] = useState(“99”); <span class="hljs-comment">// if 内部的逻辑执行一次后,就将 isMounted 置为 true(说明已挂载,后续都不再是首次渲染了)</span> isMounted = <span class="hljs-literal">true</span>; } // 对职业信息的获取逻辑不变 [career, setCareer] = useState(“我是一个前端,爱吃小熊饼干”); // 这里追加对 career 的输出,这也是一个 debug 性质的操作 console.log(“career”, career); // UI 逻辑的改动在于,name和age成了可选的展示项,若值为空,则不展示 return ( <div className=“personalInfo”> {name ? <p>姓名:{name}</p> : null} {age ? <p>年龄:{age}</p> : null} <p>职业:{career}</p> <button onClick={() => { setName(“秀妍”); }} > 修改姓名 </button> </div> );}export default PersonalInfoComponent; 修改后的组件在初始渲染的时候,界面与上个版本无异: 注意,你在自己电脑上模仿这段代码的时候,千万不要漏掉 if 语句里面// eslint-disable-next-line这个注释——因为目前大部分的 React 项目都在内部预置了对 React-Hooks-Rule(React-Hooks 使用规则)的强校验,而示例代码中把 Hooks 放进 if 语句的操作作为一种不合规操作,会被直接识别为 Error 级别的错误,进而导致程序报错。这里我们只有将相关代码的 eslint 校验给禁用掉,才能够避免校验性质的报错,从而更直观地看到错误的效果到底是什么样的,进而理解错误的原因。 修改后的组件在初始挂载的时候,实际执行的逻辑内容和上个版本是没有区别的,都涉及对 name、age、career 三个状态的获取和渲染。理论上来说,变化应该发生在我单击“修改姓名”之后触发的二次渲染里:二次渲染时,isMounted 已经被置为 true,if 内部的逻辑会被直接跳过。此时按照代码注释中给出的设计意图,这里我希望在二次渲染时,只获取并展示 career 这一个状态。那么事情是否会如我所愿呢?我们一起来看看单击“修改姓名”按钮后会发生什么: 组件不仅没有像预期中一样发生界面变化,甚至直接报错了。报错信息提醒我们,这是因为“组件渲染的 Hooks 比期望中更少”。 确实,按照现有的逻辑,初始渲染调用了三次 useState,而二次渲染时只会调用一次。但仅仅因为这个,就要报错吗? 按道理来说,二次渲染的时候,只要我获取到的 career 值没有问题,那么渲染就应该是没有问题的(因为二次渲染实际只会渲染 career 这一个状态),React 就没有理由阻止我的渲染动作。啊这……难道是 career 出问题了吗?还好我们预先留了一手 Debug 逻辑,每次渲染的时候都会尝试去输出一次 isMounted 和 career 这两个变量的值。现在我们就赶紧来看看,这两个变量到底是什么情况。 首先我将界面重置回初次挂载的状态,观察控制台的输出,如下图所示: 这里我把关键的 isMounted 和 career 两个变量用红色框框圈了出来:isMounted 值为 false,说明是初次渲染;career 值为“我是一个前端,爱吃小熊饼干”,这也是没有问题的。 接下来单击“修改姓名”按钮后,我们再来看一眼两个变量的内容,如下图所示: 二次渲染时,isMounted 为 true,这个没毛病。但是 career 竟然被修改为了“秀妍”,这也太诡异了?代码里面可不是这么写的。赶紧回头确认一下按钮单击事件的回调内容,代码如下所示: <button onClick={() => { setName(\"秀妍\"); }} > 修改姓名 </button> 确实,代码是没错的,我们调用的是 setName,那么它修改的状态也应该是 name,而不是 career。 那为什么最后发生变化的竟然是 career 呢?年轻人,不如我们一起来看一看 Hooks 的实现机制吧! 从源码调用流程看原理:Hooks 的正常运作,在底层依赖于顺序链表 这里强调“源码流程”而非“源码”,主要有两方面的考虑: React-Hooks 在源码层面和 Fiber 关联十分密切,我们目前仍然处于基础夯实阶段,对 Fiber 机制相关的底层实现暂时没有讨论,盲目啃源码在这个阶段来说没有意义; 原理 !== 源码,阅读源码只是掌握原理的一种手段,在某些场景下,阅读源码确实能够迅速帮我们定位到问题的本质(比如 React.createElement 的源码就可以快速帮我们理解 JSX 转换出来的到底是什么东西);而 React-Hooks 的源码链路相对来说比较长,涉及的关键函数 renderWithHooks 中“脏逻辑”也比较多,整体来说,学习成本比较高,学习效果也难以保证。 综上所述,这里我不会精细地贴出每一行具体的源码,而是针对关键方法做重点分析。同时我也不建议你在对 Fiber 底层实现没有认知的前提下去和 Hooks 源码死磕。对于搞清楚“Hooks 的执行顺序为什么必须一样”这个问题来说,重要的并不是去细抠每一行代码到底都做了什么,而是要搞清楚整个调用链路是什么样的。如果我们能够理解 Hooks 在每个关键环节都做了哪些事情,同时也能理解这些关键环节是如何对最终的渲染结果产生影响的,那么理解 Hooks 的工作机制对于你来说就不在话下了。 以 useState 为例,分析 React-Hooks 的调用链路 首先要说明的是,React-Hooks 的调用链路在首次渲染和更新阶段是不同的,这里我将两个阶段的链路各总结进了两张大图里,我们依次来看。首先是首次渲染的过程,请看下图: 在这个流程中,useState 触发的一系列操作最后会落到 mountState 里面去,所以我们重点需要关注的就是 mountState 做了什么事情。以下我为你提取了 mountState 的源码: // 进入 mounState 逻辑 function mountState(initialState) { // 将新的 hook 对象追加进链表尾部 var hook = mountWorkInProgressHook(); // initialState 可以是一个回调,若是回调,则取回调执行后的值 if (typeof initialState === ‘function’) { // $FlowFixMe: Flow doesn’t like mixed types initialState = initialState(); } // 创建当前 hook 对象的更新队列,这一步主要是为了能够依序保留 dispatch const queue = hook.queue = { last: null, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any), }; // 将 initialState 作为一个“记忆值”存下来 hook.memoizedState = hook.baseState = initialState; // dispatch 是由上下文中一个叫 dispatchAction 的方法创建的,这里不必纠结这个方法具体做了什么 var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue); // 返回目标数组,dispatch 其实就是示例中常常见到的 setXXX 这个函数,想不到吧?哈哈 return [hook.memoizedState, dispatch];} 从这段源码中我们可以看出,mounState 的主要工作是初始化 Hooks。在整段源码中,最需要关注的是 mountWorkInProgressHook 方法,它为我们道出了 Hooks 背后的数据结构组织形式。以下是 mountWorkInProgressHook 方法的源码: function mountWorkInProgressHook() { // 注意,单个 hook 是以对象的形式存在的 var hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null }; if (workInProgressHook === null) { // 这行代码每个 React 版本不太一样,但做的都是同一件事:将 hook 作为链表的头节点处理 firstWorkInProgressHook = workInProgressHook = hook; } else { // 若链表不为空,则将 hook 追加到链表尾部 workInProgressHook = workInProgressHook.next = hook; } // 返回当前的 hook return workInProgressHook; } 到这里可以看出,hook 相关的所有信息收敛在一个 hook 对象里,而 hook 对象之间以单向链表的形式相互串联。 接下来我们再看更新过程的大图: 根据图中高亮部分的提示不难看出,首次渲染和更新渲染的区别,在于调用的是 mountState,还是 updateState。mountState 做了什么,你已经非常清楚了;而 updateState 之后的操作链路,虽然涉及的代码有很多,但其实做的事情很容易理解:按顺序去遍历之前构建好的链表,取出对应的数据信息进行渲染。 我们把 mountState 和 updateState 做的事情放在一起来看:mountState(首次渲染)构建链表并渲染;updateState 依次遍历链表并渲染。 看到这里,你是不是已经大概知道怎么回事儿了?没错,hooks 的渲染是通过“依次遍历”来定位每个 hooks 内容的。如果前后两次读到的链表在顺序上出现差异,那么渲染的结果自然是不可控的。 这个现象有点像我们构建了一个长度确定的数组,数组中的每个坑位都对应着一块确切的信息,后续每次从数组里取值的时候,只能够通过索引(也就是位置)来定位数据。也正因为如此,在许多文章里,都会直截了当地下这样的定义:Hooks 的本质就是数组。但读完这一课时的内容你就会知道,Hooks 的本质其实是链表。 接下来我们把这个已知的结论还原到 PersonalInfoComponent 里去,看看实际项目中,变量到底是怎么发生变化的。 站在底层视角,重现 PersonalInfoComponent 组件的执行过程 我们先来复习一下修改过后的 PersonalInfoComponent 组件代码: import React, { useState } from \"react\"; // isMounted 用于记录是否已挂载(是否是首次渲染) let isMounted = false; function PersonalInfoComponent() { // 定义变量的逻辑不变 let name, age, career, setName, setCareer; // 这里追加对 isMounted 的输出,这是一个 debug 性质的操作 console.log(“isMounted is”, isMounted); // 这里追加 if 逻辑:只有在首次渲染(组件还未挂载)时,才获取 name、age 两个状态 if (!isMounted) { // eslint-disable-next-line [name, setName] = useState(“修言”); // eslint-disable-next-line [age] = useState(“99”); <span class="hljs-comment">// if 内部的逻辑执行一次后,就将 isMounted 置为 true(说明已挂载,后续都不再是首次渲染了)</span> isMounted = <span class="hljs-literal">true</span>; } // 对职业信息的获取逻辑不变 [career, setCareer] = useState(“我是一个前端,爱吃小熊饼干”); // 这里追加对 career 的输出,这也是一个 debug 性质的操作 console.log(“career”, career); // UI 逻辑的改动在于,name 和 age 成了可选的展示项,若值为空,则不展示 return ( <div className=“personalInfo”> {name ? <p>姓名:{name}</p> : null} {age ? <p>年龄:{age}</p> : null} <p>职业:{career}</p> <button onClick={() => { setName(“秀妍”); }} > 修改姓名 </button> </div> );}export default PersonalInfoComponent; 从代码里面,我们可以提取出来的 useState 调用有三个: [name, setName] = useState(\"修言\"); [age] = useState(\"99\"); [career, setCareer] = useState(\"我是一个前端,爱吃小熊饼干\"); 这三个调用在首次渲染的时候都会发生,伴随而来的链表结构如图所示: 当首次渲染结束,进行二次渲染的时候,实际发生的 useState 调用只有一个: useState(\"我是一个前端,爱吃小熊饼干\") 而此时的链表情况如下图所示: 我们再复习一遍更新(二次渲染)的时候会发生什么事情:updateState 会依次遍历链表、读取数据并渲染。注意这个过程就像从数组中依次取值一样,是完全按照顺序(或者说索引)来的。因此 React 不会看你命名的变量名是 career 还是别的什么,它只认你这一次 useState 调用,于是它难免会认为:喔,原来你想要的是第一个位置的 hook 啊。 然后就会有下面这样的效果: 如此一来,career 就自然而然地取到了链表头节点 hook 对象中的“秀妍”这个值。 总结 三个课时学完了,到这里,我们对 React-Hooks 的学习,才终于算是告一段落。 在过去的三个课时里,我们摸排了“动机”,认知了“工作模式”,最后更是结合源码、深挖了一把 React-Hooks 的底层原理。我们所做的这所有的努力,都是为了能够真正吃透 React-Hooks,不仅要确保实践中不出错,还要做到面试时有底气。 接下来,我们就将进入整个专栏真正的“深水区”,逐步切入“虚拟 DOM → Diff 算法 → Fiber 架构”这个知识链路里来。在后续的学习中,我们将延续并且强化这种“刨根问底”的风格,紧贴源码、原理和面试题来向 React 最为核心的部分发起挑战。真正的战斗,才刚刚开始,大家加油~","link":"/2023/01/01/frontEnd/inDepthAndSimpleReact/reactStudy-EP8/"},{"title":"EP09 真正理解虚拟 DOM:React 选它,真的是为了性能吗?","text":"真正理解虚拟 DOM:React 选它,真的是为了性能吗? 在过去的十年里,前端技术日新月异。从最早的纯静态页面,到 jQuery 一统江湖,再到近几年大火的 MVVM 框架——研发模式升级这件事情对于前端来说,好像成了某种常态。其实研发模式不断演进的背后,恰恰蕴含着前端人对 “DOM 操作” 这一核心动作的持续思考和改进。而虚拟 DOM,正是先驱们在这个过程中孕育出的一颗明珠。 在 MVVM 框架这个领域分支,有一道至今仍然非常经典的面试题:“为什么我们需要虚拟 DOM?”。 这个问题比较常见的回答思路是:“DOM 操作是很慢的,而 JS 却可以很快,直接操作 DOM 可能会导致频繁的回流与重绘,JS 不存在这些问题。因此虚拟 DOM 比原生 DOM 更快”。 但真的是这样吗?学完本课时,你心中自会有答案。 快速搞定虚拟 DOM 的两个“大问题” 温故而知新,在一切开始之前,我们先来复习一下虚拟 DOM是什么。 虚拟 DOM(Virtual DOM)本质上是JS 和 DOM 之间的一个映射缓存,它在形态上表现为一个能够描述 DOM 结构及其属性信息的 JS 对象。在第 01 课时,我们探讨了 JSX 和 DOM 之间的转换关系,其中就提到了虚拟 DOM 在 React 中的形态,如下图所示: 就这个示例来说,你需要把握住以下两点: 虚拟 DOM 是 JS 对象 虚拟 DOM 是对真实 DOM 的描述 这样就基本解决了虚拟 DOM“是什么”的问题,接下来我们看看 React 中的虚拟 DOM 大致是如何工作的。虚拟 DOM 在 React 组件的挂载阶段和更新阶段都会作为“关键人物”出镜,其参与的工作流程如下: 挂载阶段,React 将结合 JSX 的描述,构建出虚拟 DOM 树,然后通过 ReactDOM.render 实现虚拟 DOM 到真实 DOM 的映射(触发渲染流水线); 更新阶段,页面的变化在作用于真实 DOM 之前,会先作用于虚拟 DOM,虚拟 DOM 将在 JS 层借助算法先对比出具体有哪些真实 DOM 需要被改变,然后再将这些改变作用于真实 DOM。 OK,现在我们用最短的时间迅速搞定了“What”和“How”两个大问题。或许过程有些粗糙,但这丝毫不影响你吃透本课时的核心内容,也就是虚拟 DOM 背后的“Why”。 “为什么需要虚拟 DOM?”“虚拟 DOM 的优势何在?”“虚拟 DOM 是否伴随更好的性能?” ,要想回答好这无穷无尽的为什么,你千万不要点对点地去看待问题本身。虚拟 DOM 相对于过往的 DOM 操作解决方案来说,是一个新生事物。要想理解一个新生事物存在、发展的合理性,我们必须将其放在一个足够长的、合理的上下文中去讨论。 接下来我要做的事情,就是帮你把这个上下文完全地铺开。当你清楚了虚拟 DOM 在历史长河中的位置后,将能迅速地理解它到底帮助前端开发解决掉了什么问题,彼时,所有的答案都会跃然纸上。 历史长河中的 DOM 操作解决方案 现在,让我们一起来回顾一下,那些没有虚拟 DOM 的苦逼日子。 1. 原生 JS 支配下的“人肉 DOM” 时期 在前端这个工种的萌芽阶段,前端页面“展示”的属性远远强于其“交互”的属性,这就导致 JS 的定位只能是“辅助”:在很长一段时间里,前端工程师们会花费大量的时间去实现静态的 DOM,待一切结束后,再补充少量 JS,实现一些类似于拖拽、隐藏、幻灯片之类的“特效”。 在这个阶段,作为前端开发者来说,虽然我们一无所有,但过得很快乐——简单的业务需求决定了我们不需要去做太多或太复杂的 DOM 操作,原生 JS,足矣。 2.解放生产力的先导阶段:jQuery 时期 时代的浪潮滚滚向前,人们很快就不再满足于简单到有些无聊的交互效果,开始追求更加丰富的用户体验,与之而来的就是大量 DOM 操作需求带来的前端开发工作量的激增。在这个过程中,早期前端们渐渐地明白了一个道理:原生 JS 提供的 DOM API,实在是太太太太太难用了。 为了能够实现高效的开发,jQuery 首先解决的就是“API 不好使”这个问题——它将 DOM API 封装为了相对简单和优雅的形式,同时一口气做掉了跨浏览器的兼容工作,并且提供了链式 API 调用、插件扩展等一系列能力用于进一步解放生产力。最终达到的效果正是我们喜闻乐见的“写得更少,做得更多”。 jQuery 使 DOM 操作变得简单、快速,并且始终确保其形式稳定、可用性稳定。虽然现在看来并不完美,但在当年能够一统江湖,确实当之无愧。 3.民智初启:早期模板引擎方案 jQuery 帮助我们能够以更舒服的姿势操作 DOM,但它并不能从根本上解决 DOM 操作量过大情况下前端侧的压力。 它就好比是一个手持吸尘器,虽然可以帮助我们更加方便快速地清洁某一处的灰尘,但是要想清洁多个位置的灰尘,你仍然需要拿着它四处奔走。这样虽说不必再弯腰擦地板,但还是避免不了跑断腿的结局。 既然“手持吸尘器”满足不了日益膨胀的 DOM 操作需求,那我们想要的到底是什么呢?是一个只需要接收命令,就能够自己跑来跑去、把活干得漂漂亮亮的“扫地机器人”。 而模板引擎方案,正是“扫地机器人”的雏形。 注:由于模板引擎更倾向于点对点解决烦琐 DOM 操作的问题,它在能力和定位上既不能够、也不打算替换掉 jQuery,两者是和谐共存的。因此这里不存在“模板引擎时期”,只有“模板引擎方案”。 怎么理解模板这个概念呢?我们来看一个例子。比如说我现在手里有一套员工数据,数据内容如下: const staff = [ { name: '修言', career: '前端' }, { name: '翠翠', career: '编辑' }, { name: '花花', career: '运营' } ] 现在我想要在前端用表格展示这一堆数据,我就可以遵循模板的语法,把它塞进模板(template)里去。下面就是一个典型的模板语法使用示例: <table> {% staff.forEach(function(person){ %} <tr> <td>{% student.name %}</td> <td>{% student.age %}</td> </tr> {% }); %} </table> 可以看出,模板语法其实就是把 JS 和 HTML 结合在一起的一种规则,而模板引擎做的事情也非常容易理解。 把 staff 这个数据源读进去,塞到预置好的 HTML 模板里,然后把两者融合在一起,吐出一段目标字符串给你。这段字符串的内容,其实就是一份标准的、可用于渲染的 HTML 代码,它将对应一个 DOM 元素。最后,将这个 DOM 元素挂载到页面中去,整个模板的渲染流程也就走完了。 这个过程可以用伪代码来表示,如下所示: // 数据和模板融合出 HTML 代码 var targetDOM = template({data: students}) // 添加到页面中去 document.body.appendChild(targetDOM) 当然,实际的过程会比我们描述的要复杂一些。这里我补充一下模板引擎的实现思路,供感兴趣的同学参考。模板引擎一般需要做下面几件事情: 读取 HTML 模板并解析它,分离出其中的 JS 信息; 将解析出的内容拼接成字符串,动态生成 JS 代码; 运行动态生成的 JS 代码,吐出“目标 HTML”; 将“目标 HTML”赋值给 innerHTML,触发渲染流水线,完成真实 DOM 的渲染。 使用模板引擎方案来渲染数据是非常爽的:每次数据发生变化时,我们都不用关心到底是哪里的数据变了,也不用手动去点对点完成 DOM 的修改。只需要关注的仅仅是数据和数据变化本身,DOM 层面的改变模板引擎会帮我们做掉。 如此看来,模板引擎像极了一个只需要接收命令,就能够把活干得漂漂亮亮的“扫地机器人”!可惜的是,模板引擎出现的契机虽然是为了使用户界面与业务数据相分离,但实际的应用场景基本局限在“实现高效的字符串拼接”这一个点上,因此不能指望它去做太复杂的事情。尤其令人无法接受的是,它在性能上的表现并不尽如人意:由于不够“智能”,它更新 DOM 的方式是将已经渲染出 DOM 整体注销后再整体重渲染,并且不存在更新缓冲这一说。在 DOM 操作频繁的场景下,模板引擎可能会直接导致页面卡死。 注:请注意小标题中“早期”这个限定词——本课时所讨论的“模板引擎”概念,指的是虚拟 DOM 思想推而广之以前,相对原始的一类模板引擎,这类模板引擎曾经主导了一个时代。但时下来看,越来越多的模板引擎正在引入虚拟 DOM,模板引擎最终也将走向现代化。 虽然指望模板引擎实现生产力解放有些天方夜谭,但模板引擎在思想上无疑具备高度的先进性:允许程序员只关心数据而不必关心 DOM 细节的这一操作,和 React 的“数据驱动视图”思想如出一辙,实在是高! 那该怎么办呢? jQuery 救不了加班写 DOM 操作的前端,模板引擎也救不了,那该怎么办呢? 这时候有一批仁人志士,兴许是从模板引擎的设计思想上得到了启发,他们明确了要走“数据驱动视图”这条基本道路,于是便沿着这个思路往下摸索:模板引擎的数据驱动视图方案,核心问题在于对真实 DOM 的修改过于“大刀阔斧”,导致了 DOM 操作的范围过大、频率过高,进而可能会导致糟糕的性能。然后这帮人就想啊:既然操作真实 DOM 对性能损耗这么大,那我操作假的 DOM 不就行了? 沿着这个思路再往下走,就有了我们都爱的虚拟 DOM。 注:出于严谨,还是要解释下。真实历史中的虚拟 DOM 创作过程,到底有没有向模板引擎去学习,这个暂时无从考证。但是按照前端发展的过程来看,模板引擎和虚拟 DOM 确实在思想上存在递进关系,很多场景下,面试官也可能会问及两者的关系。因此在此处,我采取了这样一种表述方式,希望能够帮助你更好地把握住问题的关键所在。 虚拟 DOM 是如何解决问题的 读到这里,你可能对虚拟 DOM 已经有些感觉了。这里我来帮你总结下,同样是将用户界面与数据相分离,模板引擎是这样做的: 而在虚拟 DOM 的加持下,事情变成了这样: 注意图中的“模板”二字加了引号,这是因为虚拟 DOM 在实现上并不总是借助模板。比如 React 就使用了 JSX,前面咱们着重讲过,JSX 本质不是模板,而是一种使用体验和模板相似的 JS 语法糖。 区别就在于多出了一层虚拟 DOM 作为缓冲层。这个缓冲层带来的利好是:当 DOM 操作(渲染更新)比较频繁时,它会先将前后两次的虚拟 DOM 树进行对比,定位出具体需要更新的部分,生成一个“补丁集”,最后只把“补丁”打在需要更新的那部分真实 DOM 上,实现精准的“差量更新”。这个过程对应的虚拟 DOM 工作流如下图所示: 注:图中的 diff 和 patch 其实都是函数名,这些函数取材于一个独立的虚拟 DOM 库。之所以写明了具体流程对应的函数名,是因为我发现面试的时候,很多面试官习惯于用函数名指代过程,但不少人不清楚这个对应关系(尤其是 patch),会非常影响作答。这里提前帮你把这个坑给规避掉。 还需要说明的一点是, 虚拟 DOM 和 Redux 一样,不依附于任何具体的框架。学习虚拟 DOM,实际上可以完全不借助 React;但学习 React,就必须了解虚拟 DOM。如果你对虚拟 DOM 的具体实现过程感兴趣,可以在这个 GitHub 仓库里查看其源码细节。 回到主线剧情上来,差量更新可以确保虚拟 DOM 既能够提供高效的开发体验(开发者只需要关心数据),又能够保持过得去的性能(只更新发生了变化的那部分 DOM),实在是妙啊! React 选用虚拟 DOM,真的是为了更好的性能吗? 读到这里,相信你至少已经 get 到了这样一个点:在整个 DOM 操作的演化过程中,主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物。 虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。 性能问题属于前端领域复杂度比较高的问题。当我们量化性能的时候,往往并不能只追求一个单一的数据,而是需要结合具体的参照物、渲染的阶段、数据的吞吐量等各种要素来作分情况的讨论。 拿前面讲过的模板渲染来举例,我们可以对比一下它和虚拟 DOM 在性能开销上的差异。两者的渲染工作流对比如下图所示: 从图中可以看出,模板渲染的步骤1,和虚拟 DOM 渲染的步骤1、2都属于 JS 范畴的行为,这两者是具备可比性的,我们放在一起来看:动态生成 HTML 字符串的过程本质是对字符串的拼接,对性能的消耗是有限的;而虚拟 DOM 的构建和 diff 过程逻辑则相对复杂,它不可避免地涉及递归、遍历等耗时操作。因此在 JS 行为这个层面,模板渲染胜出。 模板渲染的步骤3,和虚拟 DOM 的步骤3 都属于 DOM 范畴的行为,两者具备可比性,因此我们仍然可以愉快地对比下去:模板渲染是全量更新,而虚拟 DOM 是差量更新。 乍一看好像差量更新一定比全量更新高效,但你需要考虑这样一种情况:数据内容变化非常大(或者说整个发生了改变),促使差量更新计算出来的结果和全量更新极为接近(或者说完全一样)。 在这种情况下,DOM 更新的工作量基本一致,而虚拟 DOM 却伴随着开销更大的 JS 计算,此时会出现的一种现象就是模板渲染和虚拟 DOM 在整体性能上难分伯仲:若两者最终计算出的 DOM 更新内容完全一致,那么虚拟 DOM 大概率不敌模板渲染;但只要两者在最终 DOM 操作量上拉开那么一点点的差距,虚拟 DOM 就将具备战胜模板渲染的底气。因为虚拟 DOM 的劣势主要在于 JS 计算的耗时,而 DOM 操作的能耗和 JS 计算的能耗根本不在一个量级,极少量的 DOM 操作耗费的性能足以支撑大量的 JS 计算。 当然,上面讨论的这种情况相对来说比较极端。在实际的开发中,更加高频的场景是这样的:我每次 setState 的时候只修改少量的数据,比如一个对象中的某几个属性,再比如一个数组中的某几个元素。在这样的场景下,模板渲染和虚拟 DOM 之间 DOM 操作量级的差距就完全拉开了,虚拟 DOM 将在性能上具备绝对的优势。 注意,此处的结论是“在 XXX 场景下,虚拟 DOM 相对于 XXX 具备性能优势”,它是有严格限定条件的。有人不到黄河心不死,可能又要问“那虚拟 DOM 对比 jQuery 呢?”“那虚拟 DOM 对比原生 DOM 呢?”。 我想说的是,性能问题不能一概而论,而且咱都讲到这个份上了,就不要再钻性能这个牛角尖了。jQuery、原生 DOM 在思维模式上来说和虚拟 DOM 截然不同,强行比较意义不大。 前面又是分析又是举例地说了这么多,其实我最终希望你明白的事情只有一件:虚拟 DOM 的价值不在性能,而在别处。因此想要从性能角度来把握虚拟 DOM 的优势,无异于南辕北辙。偏偏在面试场景下,10 个人里面有 9 个都走这条歧路,最后9个人里面自然没有一个能自圆其说,实在让人惋惜。 那么虚拟 DOM 的价值到底是什么呢? 最后我想和你聊聊虚拟 DOM 的价值,这又是一个宏大的、容易说错话的命题。当我们谈及某个事物的价值时,其实就像是在称赞一个美女,不同的人自然有着不同看待美女的视角。此处我无意于给你一个天衣无缝的标准答案(这样的答案想必也不存在),而是希望能够站在“虚拟 DOM 解决了哪些关键问题”这个视角,和你分享一些业内关于虚拟 DOM 的共识。 虚拟 DOM 解决的关键问题有以下两个。 研发体验/研发效率的问题:这一点前面已经反复强调过,DOM 操作模式的每一次革新,背后都是前端对效率和体验的进一步追求。虚拟 DOM 的出现,为数据驱动视图这一思想提供了高度可用的载体,使得前端开发能够基于函数式 UI 的编程方式实现高效的声明式编程。 跨平台的问题:虚拟 DOM 是对真实渲染内容的一层抽象。若没有这一层抽象,那么视图层将和渲染平台紧密耦合在一起,为了描述同样的视图内容,你可能要分别在 Web 端和 Native 端写完全不同的两套甚至多套代码。但现在中间多了一层描述性的虚拟 DOM,它描述的东西可以是真实 DOM,也可以是iOS 界面、安卓界面、小程序......同一套虚拟 DOM,可以对接不同平台的渲染逻辑,从而实现“一次编码,多端运行”,如下图所示。其实说到底,跨平台也是研发提效的一种手段,它在思想上和1是高度呼应的。 在本课时的主线内容之外,虚拟 DOM 还有非常多的亮点值得我们去挖掘,这里我想着重拓展一下的是前面聊到的性能层面的优化。 除了差量更新以外,“批量更新”也是虚拟 DOM 在性能方面所做的一个重要努力:“批量更新”在通用虚拟 DOM 库里是由 batch 函数来处理的。在差量更新速度非常快的情况下(比如极短的时间里多次操作同一个 DOM),用户实际上只能看到最后一次更新的效果。这种场景下,前面几次的更新动作虽然意义不大,但都会触发重渲染流程,带来大量不必要的高耗能操作。 这时就需要请 batch 来帮忙了,batch 的作用是缓冲每次生成的补丁集,它会把收集到的多个补丁集暂存到队列中,再将最终的结果交给渲染函数,最终实现集中化的 DOM 批量更新。 总结 本课时我们首先一起回顾了 DOM 操作解决方案的发展史,从中明确了虚拟 DOM 定位和解决的主要问题,然后对虚拟 DOM 的通用工作流进行了分析。在这个工作流中,有一个过程值得我们格外去注意,那就是“diff”。 diff 指的是对比出两棵虚拟 DOM 树之间差异的过程,不同的框架对 diff 过程有着不同的实现思路。对于 React 框架来说,有特色的、与时俱进的 diff 算法正是它最迷人的地方,也是框架的核心所在。 在接下来的课时,我将从 React15 的 diff 过程切入,对经典的“栈调和”算法一探究竟。","link":"/2023/02/01/frontEnd/inDepthAndSimpleReact/reactStudy-EP9/"},{"title":"EP05 数据是如何在 React 组件之间流动的?(下)","text":"数据是如何在 React 组件之间流动的?(下) 在上个课时,我们掌握了 React 数据流方案中风格相对“朴素”的 Props 单向数据流方案,以及通用性较强的“发布-订阅”模式。在本课时,我们将一起认识 React 天然具备的全局通信方式“Context API”,并对 Redux 的设计思想和编码形态进行初步的探索。 使用 Context API 维护全局状态 Context API 是 React 官方提供的一种组件树全局通信的方式。 在 React 16.3 之前,Context API 由于存在种种局限性,并不被 React 官方提倡使用,开发者更多的是把它作为一个概念来探讨。而从 v 16.3.0 开始,React 对 Context API 进行了改进,新的 Context API 具备更强的可用性。这里我们首先针对 React 16 下 Context API 的形态进行介绍。 图解 Context API 工作流 Context API 有 3 个关键的要素:React.createContext、Provider、Consumer。 我们通过调用 React.createContext,可以创建出一组 Provider。Provider 作为数据的提供方,可以将数据下发给自身组件树中任意层级的 Consumer,这三者之间的关系用一张图来表示: 注意:Cosumer 不仅能够读取到 Provider 下发的数据,还能读取到这些数据后续的更新。这意味着数据在生产者和消费者之间能够及时同步,这对 Context 这种模式来说至关重要。 从编码的角度认识“三要素” React.createContext,作用是创建一个 context 对象。下面是一个简单的用法示范: const AppContext = React.createContext() 注意,在创建的过程中,我们可以选择性地传入一个 defaultValue: const AppContext = React.createContext(defaultValue) 从创建出的 context 对象中,我们可以读取到 Provider 和 Consumer: const { Provider, Consumer } = AppContext Provider,可以理解为“数据的 Provider(提供者)”。 我们使用 Provider 对组件树中的根组件进行包裹,然后传入名为“value”的属性,这个 value 就是后续在组件树中流动的“数据”,它可以被 Consumer 消费。使用示例如下: <Provider value={title: this.state.title, content: this.state.content}> <Title /> <Content /> </Provider> Consumer,顾名思义就是“数据的消费者”,它可以读取 Provider 下发下来的数据。 其特点是需要接收一个函数作为子元素,这个函数需要返回一个组件。像这样: <Consumer> {value => <div>{value.title}</div>} </Consumer> 注意,当 Consumer 没有对应的 Provider 时,value 参数会直接取创建 context 时传递给 createContext 的 defaultValue。 新的 Context API 解决了什么问题 想要知道新的 Context API 解决了什么问题,先要知道过时的 Context API 存在什么问题。 我们先从编码角度认识“过时的”Context API “过时的”是 React 官方对旧的 Context API 的描述,由于个人和团队在实际项目中都并不会考虑去使用旧 Context API 来解决问题,这里我直接引用过时的文档中的 Context API 使用示例: import PropTypes from 'prop-types'; class Button extends React.Component { render() { return ( <button style={{background: this.context.color}}> {this.props.children} </button> ); } } Button.contextTypes = { color: PropTypes.string }; class Message extends React.Component { render() { return ( <div> {this.props.text} <Button>Delete</Button> </div> ); } } class MessageList extends React.Component { getChildContext() { return {color: \"purple\"}; } render() { const children = this.props.messages.map((message) => <Message text={message.text} /> ); return <div>{children}</div>; } } MessageList.childContextTypes = { color: PropTypes.string }; 为了方便你理解,我将上述代码对应的组织结构梳理到一张图里,如下图所示: 借着这张图,我们来理解旧的 Context API 的工作过程: 首先,通过给 MessageList 设置 childContextTypes 和 getChildContext,可以使其承担起 context 的生产者的角色; 然后,MessageList 的组件树内部所有层级的组件都可以通过定义 contextTypes 来成为数据的消费者,进而通过 this.context 访问到 MessageList 提供的数据。 现在回过头来,我们再从编码角度审视一遍“过时的” Context API 的用法。 首先映入眼帘的第一个问题是代码不够优雅:一眼望去,你很难迅速辨别出谁是 Provider、谁是 Consumer。同时这琐碎的属性设置和 API 编写过程,也足够我们写代码的时候“喝一壶了”。总而言之,从编码形态的角度来说,“过时的” Context API 和新 Context API 相去甚远。 不过,这还不是最要命的,最要命的弊端我们从编码层面暂时感知不出来,但是一旦你感知到它,麻烦就大了——前面我们特别提到过,“Cosumer 不仅能够读取到 Provider 下发的数据,还能够读取到这些数据后续的更新”。数据在生产者和消费者之间的及时同步,这一点对于 Context 这种模式来说是至关重要的,但旧的 Conext API 无法保证这一点: 如果组件提供的一个Context发生了变化,而中间父组件的 shouldComponentUpdate 返回 false,那么使用到该值的后代组件不会进行更新。使用了 Context 的组件则完全失控,所以基本上没有办法能够可靠的更新 Context。这篇博客文章很好地解释了为何会出现此类问题,以及你该如何规避它。 ——React 官方 新的 Context API 改进了这一点:即便组件的 shouldComponentUpdate 返回 false,它仍然可以“穿透”组件继续向后代组件进行传播,进而确保了数据生产者和数据消费者之间数据的一致性。再加上更加“好看”的语义化的声明式写法,新版 Context API 终于顺利地摘掉了“试验性 API”的帽子,成了一种确实可行的 React 组件间通信解决方案。 理解了 Context API 的前世今生,接下来我们继续来串联 React 组件间通信的解决方案。 第三方数据流框架“课代表”:初探 Redux 对于简单的跨层级组件通信,我们可以使用发布-订阅模式或者 Context API 来搞定。但是随着应用的复杂度不断提升,需要维护的状态越来越多,组件间的关系也越来越难以处理的时候,我们就需要请出 Redux 来帮忙了。 什么是 Redux 我们先来看一下官方对 Redux 的描述: Redux 是 JavaScript 状态容器,它提供可预测的状态管理。 我们一起品品这句话背后的深意: Redux 是为JavaScript应用而生的,也就是说它不是 React 的专利,React 可以用,Vue 可以用,原生 JavaScript 也可以用; Redux 是一个状态容器,什么是状态容器?这里我举个例子。 假如把一个 React 项目里面的所有组件拉进一个钉钉群,那么 Redux 就充当了这个群里的“群文件”角色,所有的组件都可以把需要在组件树里流动的数据存储在群文件里。当某个数据改变的时候,其他组件都能够通过下载最新的群文件来获取到数据的最新值。这就是“状态容器”的含义——存放公共数据的仓库。 读懂了这个比喻之后,你对 Redux、数据和 React 组件的关系想必已经形成了一个初步的认知。这里我帮你把这层关系总结进一张图里: Redux 是如何帮助 React 管理数据的 Redux 主要由三部分组成:store、reducer 和 action。我们先来看看它们各自代表什么: store 就好比组件群里的“群文件”,它是一个单一的数据源,而且是只读的; action 人如其名,是“动作”的意思,它是对变化的描述。 举个例子,下面这个对象就是一个 action: const action = { type: \"ADD_ITEM\", payload: '<li>text</li>' } reducer 是一个函数,它负责对变化进行分发和处理, 最终将新的数据返回给 store。 store、action 和 reducer 三者紧密配合,便形成了 Redux 独树一帜的工作流: 从上图中,我们首先读出的是数据的流向规律:在 Redux 的整个工作过程中,数据流是严格单向的。这一点一定一定要背下来,面试的时候也一定一定要记得说——不管面试官问的是 Redux 的设计思想还是工作流还是别的什么概念性的知识,开局先放这句话,准没错。 接下来仍然是围绕上图,我们来一起看看 Redux 是如何帮助 React 管理数据流的。对于一个 React 应用来说,视图(View)层面的所有数据(state)都来自 store(再一次诠释了单一数据源的原则)。 如果你想对数据进行修改,只有一种途径:派发 action。action 会被 reducer 读取,进而根据 action 内容的不同对数据进行修改、生成新的 state(状态),这个新的 state 会更新到 store 对象里,进而驱动视图层面做出对应的改变。 对于组件来说,任何组件都可以通过约定的方式从 store 读取到全局的状态,任何组件也都可以通过合理地派发 action 来修改全局的状态。Redux 通过提供一个统一的状态容器,使得数据能够自由而有序地在任意组件之间穿梭,这就是 Redux 实现组件间通信的思路。 从编码的角度理解 Redux 工作流 到这里,你已经了解了 Redux 的设计思想和要素关系。接下来我们将站在编码的角度,探讨 Redux 的工作流,将上文中所提及的各个要素和流程具象化。 1. 使用 createStore 来完成 store 对象的创建 // 引入 redux import { createStore } from 'redux' // 创建 store const store = createStore( reducer, initial_state, applyMiddleware(middleware1, middleware2, ...) ); createStore 方法是一切的开始,它接收三个入参: reducer; 初始状态内容; 指定中间件(这个你先不用管)。 这其中一般来说,只有 reducer 是你不得不传的。下面我们就看看 reducer 的编码形态是什么样的。 2. reducer 的作用是将新的 state 返回给 store 一个 reducer 一定是一个纯函数,它可以有各种各样的内在逻辑,但它最终一定要返回一个 state: const reducer = (state, action) => { // 此处是各种样的 state处理逻辑 return new_state } 当我们基于某个 reducer 去创建 store 的时候,其实就是给这个 store 指定了一套更新规则: // 更新规则全都写在 reducer 里 const store = createStore(reducer) 3. action 的作用是通知 reducer “让改变发生” 如何在浩如烟海的 store 状态库中,准确地命中某个我们希望它发生改变的 state 呢?reducer 内部的逻辑虽然不尽相同,但其本质工作都是“将 action 与和它对应的更新动作对应起来,并处理这个更新”。所以说要想让 state 发生改变,就必须用正确的 action 来驱动这个改变。 前面我们已经介绍过 action 的形态,这里再提点一下。首先,action 是一个大致长这样的对象: const action = { type: \"ADD_ITEM\", payload: '<li>text</li>' } action 对象中允许传入的属性有多个,但只有 type 是必传的。type 是 action 的唯一标识,reducer 正是通过不同的 type 来识别出需要更新的不同的 state,由此才能够实现精准的“定向更新”。 4. 派发 action,靠的是 dispatch action 本身只是一个对象,要想让 reducer 感知到 action,还需要“派发 action”这个动作,这个动作是由 store.dispatch 完成的。这里我简单地示范一下: import { createStore } from 'redux' // 创建 reducer const reducer = (state, action) => { // 此处是各种样的 state处理逻辑 return new_state } // 基于 reducer 创建 state const store = createStore(reducer) // 创建一个 action,这个 action 用 “ADD_ITEM” 来标识 const action = { type: \"ADD_ITEM\", payload: '<li>text</li>' } // 使用 dispatch 派发 action,action 会进入到 reducer 里触发对应的更新 store.dispatch(action) 以上这段代码,是从编码角度对 Redux 主要工作流的概括,这里我同样为你总结了一张对应的流程图: 注意:先别急着死磕。本课时并不要求你掌握 Redux 中涉及的所有概念和原理,只需要你跟着我的思路走,大致理解 Redux 中几个关键角色之间的关系,进而明白 Redux 是如何驱动数据在 React 组件间流动、如何帮助我们实现灵活的组件间通信的,这就够了。关于更多 Redux 的技术细节,我将在专栏的第三个大模块慢慢推敲。 总结 在 04 和 05 课时,我讲解的知识点覆盖面广、跨度大。面试场景下,考察此类问题的目的也主要是对候选人的知识广度进行检验。因此对于这两节的内容,你应抱着梳理“知识地图”的目的去学习,以构建知识体系为第一要务。完成第一要务后,再带着一个完整的上下文,去攻克某个具体的薄弱点。","link":"/2022/09/26/frontEnd/inDepthAndSimpleReact/reactStudy-EP5/"},{"title":"🧑💻JavaScript专项练习 错题本","text":"自家用 1、函数表达式和函数声明以及它们提升的区别 函数声明会提升, 提升是整个函数体提升至当前作用域的顶层; 函数表达式没提升, 提升是提升变量(函数的引用),表达式留在原地; 2、f(a = 100)相当于创建全局变量a再传入函数调用3、原始类型(primitive type)与原始值(primitive value) 原始类型(primitive type)有以下五种类型:Undefined,Null,Boolean,Number,String; 4、浏览器的Response Headers字段5、身份证号的正则表达式6、try…catch…finally的用法和break的用法7、exec()方法和document.write对数组的处理 exec() 方法是一个正则表达式方法; exec() 方法用于检索字符串中的正则表达式的匹配; 数组使用document.write时显示在html中的内容是数组中的元素; 8、JavaScript 中的数字在计算机内存中占8个Byte9、JavaScript 中的内部对象 Navagator:提供有关浏览器的信息; Window:Window对象处于对象层次的最顶层,它提供了处理Navagator窗口的方法和属性; Location:提供了与当前打开的URL一起工作的方法和属性,是一个静态的对象; History:提供了与历史清单有关的信息; Document:包含与文档元素一起工作的对象,它将这些元素封装起来供编程人员使用;","link":"/2022/01/01/frontEnd/nowcoder/Javascript/"},{"title":"面试问题预判","text":"个人面试题库,暂时仅记录一些平时可能答不明白的题目,懂的题目可能不会写。 HTML基础 谈谈你对BFC的理解 BFC(Block Formatting Context)块级格式化上下文,它是一个独立的渲染区域,让处于 BFC 内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响。 内部的盒子会在垂直方向上一个接一个的放置 对于同一BFC内部的两个相邻Box,上下margin会发生重叠 BFC区域不会与float box重叠 BFC可以包含浮动元素(清除浮动) BFC区域不会影响到外部元素 BFC的应用场景(引申) 防止垂直 margin 重叠:当两个相邻元素的margin值很大时,它们之间的间距可能会比我们预期的要小。这时候可以给其中一个元素设置BFC,从而避免margin重叠。 清除浮动:当一个元素浮动之后,它会影响到周围元素的布局,这时候可以给父元素设置BFC,使得父元素包含浮动元素,从而达到清除浮动的效果。 自适应两栏布局:当我们需要实现左侧固定宽度,右侧自适应的两栏布局时,可以将左侧元素设置为浮动,右侧元素设置为BFC,从而实现两栏布局。 防止文字环绕:当我们需要实现文字环绕效果时,可以将文字所在的元素设置为BFC,从而实现文字环绕效果。 CSS CSS选择器权重 !important > 行内样式 > ID选择器 > 类选择器 > 标签选择器 > 通配符选择器 > 继承 > 浏览器默认属性 选择器的权重是由多个级别的规则相加而成的,当权重相同时,后写的规则会覆盖先写的规则。 选择器的权重是由四个级别的规则相加而成的,分别是:行内样式、ID选择器、类选择器和伪类选择器、标签选择器和伪元素选择器。其中,行内样式的权重最高,为1000,ID选择器的权重为100,类选择器和伪类选择器的权重为10,标签选择器和伪元素选择器的权重为1。当权重相同时,后写的规则会覆盖先写的规则。 CSS选择器权重表 选择器 权重 内联样式 1000 ID选择器 100 类选择器、属性选择器、伪类 10 元素选择器、伪元素 1 通配符、子选择器、相邻选择器、后代选择器 0 JavaScript基础 说一下JavaScript的数据类型 基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(ES6新增);引用数据类型:Object、Array、Function、Date、RegExp、Error 基本数据类型和引用数据类型的区别 基本数据类型存储在栈内存中,引用数据类型存储在堆内存中;基本数据类型的值是不可变的,引用数据类型的值是可变的;基本数据类型的比较是值的比较,引用数据类型的比较是引用的比较。 请解释什么是事件循环? 事件循环是一种处理异步操作的机制,它可以让我们在代码执行过程中处理异步任务,比如网络请求、定时器和用户交互等。事件循环的执行顺序是:首先执行同步代码,然后执行异步代码。当 JavaScript 引擎遇到需要等待的操作时,如 I/O 操作、setTimeout 等异步操作,它会将这些操作加入到任务队列中,等待下一次事件循环时执行。 请解释什么是宏任务和微任务? 宏任务和微任务都是异步任务。宏任务包括:script(整体代码)、setTimeout、setInterval、I/O 操作、UI 渲染等;微任务包括:Promise、process.nextTick、Object.observe、MutationObserver 等。在每次事件循环中,宏任务会优先于微任务执行。 谈谈你对JavaScript的事件轮循的理解? JavaScript 是一门单线程语言,它的事件轮询机制是通过一个事件循环来实现的。浏览器通过事件队列来管理这些事件,当事件发生时,会将事件加入到事件队列中,然后等待 JavaScript 引擎执行。事件轮询机制的实现方式是通过一个事件循环来实现的。事件循环会不断地从事件队列中取出事件,然后执行相应的回调函数。 JavaScript 引擎在执行 JavaScript 代码时,会将所有同步任务按照顺序放入执行栈中,然后依次执行。当遇到异步任务时,会将异步任务放入任务队列中,等待 JavaScript 引擎空闲时再去执行。当执行栈中的所有同步任务都执行完毕时,JavaScript 引擎会去查看任务队列中是否有任务需要执行。如果有,则将任务队列中的第一个任务取出来放入执行栈中执行。 谈谈什么是事件循环 事件循环是JavaScript的一个基于并发模型的机制,负责执行代码、收集和处理事件以及执行队列中的子任务。主线程从“任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为事件循环。事件循环执行消息队列中优先级最高的任务,如果没有任务,则执行微任务。 在JavaScript中,Object类的方法作用有哪些? Object.assign():将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。 Object.create():使用指定的原型对象和属性创建一个新对象。 Object.defineProperty():定义一个新属性或修改现有属性,并指定该属性的描述符。 Object.defineProperties():定义一个或多个新属性或修改现有属性,并指定这些属性的描述符。 Object.entries():返回给定对象自身可枚举属性的键值对数组,其排列与使用 for…in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环还枚举原型链中的属性)。 Object.freeze():冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。 Object.fromEntries():把键值对列表转换为一个对象。 Object.getOwnPropertyDescriptor():返回指定对象上一个自有属性对应的属性描述符。 Object.getOwnPropertyDescriptors():返回指定对象上所有自有属性(非继承属性)对应的属性描述符。 Object.getOwnPropertyNames():返回一个数组,它包含了指定对象所有自身属性(非继承属性)的名称(不包含 Symbol 类型的属性)。 4 谈谈你对跨域资源共享的机制的理解,以及跨域的几种解决方法 跨域资源共享(CORS)是一种机制,它允许 Web 应用程序从不同的域访问其资源。跨域的几种方法包括: JSONP:通过动态创建 script 标签,将数据作为回调函数的参数传递。 CORS:通过在服务器端设置响应头,允许指定的域名访问资源。 代理:通过在同一域名下设置代理服务器,将请求转发到目标服务器。 谈谈你对js里面0.1+0.2!=0.3的理解 这是因为在JavaScript中,浮点数运算的精度问题导致的。在计算机运行过程中,需要将数据转化成二进制,然后再进行计算。但是,由于二进制无法精确表示某些十进制小数,所以在进行浮点数运算时会出现精度问题。因此,0.1+0.2的结果并不是0.3,而是0.30000000000000004; 解决这个问题的方法有很多种,其中一种方法是使用toFixed()方法将结果转换为字符串,并指定小数位数。例如:(0.1+0.2).toFixed(1)的结果为”0.3”; 还可以使用BigInt解决这个问题。BigInt是JavaScript中的一个新的数字类型,可以用任意精度表示整数。使用BigInt,即使超出Number的安全整数范围限制,也可以安全地存储和操作大整数。 例如,可以使用BigInt来计算0.1+0.2的结果,如下所示:(0.1n+0.2n)===0.3n。这将返回true,因为BigInt可以精确表示这些数字。 谈谈你对js里面的this的理解 this是JavaScript中的一个关键字,它代表函数执行时所在的上下文对象。this的值取决于函数的调用方式,而不是函数的定义方式。this的值有以下几种情况: 作为对象的方法调用,this的值为调用该方法的对象; 作为普通函数调用,this的值为全局对象(浏览器中为window); 作为构造函数调用,this的值为新创建的对象; 使用call、apply、bind调用,this的值为指定的对象。 双等号,三等号,object.is()的区别 在JavaScript中,双等号(==)和三等号(===)都是用于比较两个值是否相等。 但是,它们之间有一些区别。双等号运算符仅比较值,而三等号运算符不仅比较值,还比较类型。如果类型不同,则返回false。 Object.is()方法与三等号运算符的行为相同,但它特别处理NaN、+0和-0,保证-0和+0不再相同,但Object.is(NaN, NaN)将返回true。 Promise的三种状态 Promise 有三种状态:pending(等待态)、fulfilled(成功态)和 rejected(失败态)。当 Promise 被创建时,它处于 pending 状态。当 Promise 成功执行时,它会进入 fulfilled 状态;当 Promise 执行失败时,它会进入 rejected 状态。 原型链相关 谈谈你对原型链的理解 原型链是由原型对象组成的链式结构,每个对象都有一个原型对象,对象的原型对象也有自己的原型对象,这样就形成了一个原型链。原型链的作用是:当对象访问一个属性时,如果对象本身没有这个属性,则会去原型对象中查找,如果原型对象中也没有这个属性,则会去原型对象的原型对象中查找,以此类推,直到找到这个属性或者找到原型链的尽头。原型链的尽头是Object.prototype,Object.prototype的原型对象为null。 call、apply、bind的区别 call、apply、bind都是用来改变函数的this指向的,call和apply的区别在于传参的方式不同,call是一个一个传参,apply是以数组的形式传参,bind是返回一个新的函数,这个函数的this指向绑定的对象,参数是bind传入的参数和新函数传入的参数的合集。 谈谈你对继承的理解 继承是指子类继承父类的属性和方法,子类可以使用父类的属性和方法,继承的方式有原型链继承、构造函数继承、组合继承、寄生组合继承、ES6的class继承。 谈谈你对constructor的理解 constructor是构造函数,每个函数都有一个constructor属性,指向构造函数本身,当函数作为构造函数使用时,constructor指向构造函数本身,当函数作为普通函数使用时,constructor指向Function。 prototype 和 proto 区别是什么? prototype是函数的一个属性,指向函数的原型对象;proto是对象的一个属性,指向对象的原型对象。原型对象也是一个对象,它也有自己的 proto 属性,指向它的原型对象。这样一直往上追溯,直到最终的原型对象为 null 为止。null 没有 proto 属性,这样就形成了一个原型链。 显式原型和隐式原型分别指什么? 显式原型指的是函数的prototype属性,隐式原型指的是对象的proto属性。显式原型是函数的 prototype 属性,它指向一个对象,这个对象包含了所有实例共享的属性和方法;而隐式原型是每个实例对象都有的一个属性,它指向该对象的原型对象。实例对象的隐式原型的值,为它的构造函数的显式原型的值。 原型链的终点是什么? 原型链的终点是Object.prototype,Object.prototype的原型对象为null。 数组方法 JavaScript中有哪些数组方法 push、pop、shift、unshift、splice、slice、concat、join、reverse、sort、indexOf、lastIndexOf、forEach、map、filter、some、every、reduce、reduceRight、find、findIndex、includes、fill、copyWithin 那么这些方法分别属于ES多少 ES3:push、pop、shift、unshift、splice、slice、concat、join、reverse、sort、indexOf、lastIndexOf、forEach、map、filter、some、every、reduce、reduceRight、find、findIndex、includes、fill、copyWithin; ES5:forEach、map、filter、some、every、reduce、reduceRight、indexOf、lastIndexOf、find、findIndex、includes; ES6:copyWithin、fill、find、findIndex、includes 如何删除数组中的元素 splice、pop、shift;其中splice可以删除任意位置的元素,pop删除最后一个元素,shift删除第一个元素。他们的用法都是传入一个参数,表示要删除的元素的位置,splice还可以传入第二个参数,表示要删除的元素的个数。 浏览器 谈谈浏览器缓存机制 浏览器缓存机制有四种:Memory Cache、Service Worker Cache、HTTP Cache、Push Cache。其中,HTTP 缓存是浏览器缓存机制中最常用的一种,它是根据 HTTP 头信息中的缓存标识来判断是否命中缓存的。HTTP 缓存又分为强缓存和协商缓存两种方式。强缓存是利用 Expires 和 Cache-Control 两个 HTTP 头信息来控制的,而协商缓存则是利用 Last-Modified 和 ETag 两个 HTTP 头信息来控制的。 为什么JSONP只支持get请求,不支持post请求 JSONP只支持get请求,不支持post请求的原因是因为JSONP请求是通过动态创建script标签实现的,所以需要确保被请求的数据源返回的是JSONP格式的数据,而不是普通的JSON格式。因为script标签的src属性是没有跨域的限制的,所以可以通过script标签来实现跨域请求。而由于script标签只支持GET请求,所以JSONP只支持GET请求。 React相关基础 说一下React的生命周期 React的生命周期分为三个阶段:挂载阶段、更新阶段、卸载阶段。挂载阶段包括constructor、getDerivedStateFromProps、render、componentDidMount;更新阶段包括getDerivedStateFromProps、shouldComponentUpdate、render、getSnapshotBeforeUpdate、componentDidUpdate;卸载阶段包括componentWillUnmount。 React的setState是同步还是异步 React的setState是异步的,当调用setState时,React会将要更新的状态放入一个队列中,等到所有的setState都执行完毕后,再统一更新状态,这样可以提高性能。但是有时候我们需要在setState执行完毕后立即获取更新后的状态,这时候可以在setState的第二个参数中传入一个回调函数,这个回调函数会在setState执行完毕后立即执行。 原理 Fiber是什么 Fiber是React16中引入的新的协调机制,它的目的是为了解决React在渲染过程中出现的卡顿问题。在React15中,渲染过程是同步的,一旦开始渲染,就会一直执行到渲染结束,如果渲染的组件树很大,就会导致渲染时间很长,这样就会造成页面卡顿。在React16中,引入了Fiber,将渲染过程分成了多个小任务,每个小任务执行时间很短,这样就不会造成页面卡顿。 谈谈你对虚拟DOM的理解 虚拟DOM是React中的一个概念,它是用一个普通的JS对象来描述一个DOM节点,这个普通的JS对象就是虚拟DOM。虚拟DOM的好处是可以进行批量更新,当数据发生变化时,会生成一个新的虚拟DOM,然后将新的虚拟DOM和旧的虚拟DOM进行比较,找出差异,然后将差异更新到真实的DOM上,这样就可以减少对真实DOM的操作,提高性能。 React的diff和vue的diff有什么区别 React和Vue都使用了Diff算法来提高渲染性能,但是它们的实现方式有所不同。 在React中,Diff算法是在组件树的每个节点上进行的。React会对每个节点进行以下几个方面的比较: 节点类型:如果节点类型不同,则React会销毁旧节点并创建新节点。 节点属性:如果节点属性不同,则React会更新节点属性。 子节点:如果子节点不同,则React会递归地对子节点进行比较。 在Vue中,Diff算法是在虚拟DOM上进行的。Vue会将模板编译成虚拟DOM,并在每次更新时重新渲染虚拟DOM。Vue会对每个虚拟DOM节点进行以下几个方面的比较: 节点类型:如果节点类型不同,则Vue会销毁旧节点并创建新节点。 节点属性:如果节点属性不同,则Vue会更新节点属性。 子节点:如果子节点不同,则Vue会递归地对子节点进行比较。 总的来说,React和Vue的Diff算法都是基于虚拟DOM的,但是它们的实现方式有所不同。React是在组件树上进行比较,而Vue是在虚拟DOM上进行比较。此外,Vue还使用了一些优化技巧,如异步更新和缓存策略等,来进一步提高渲染性能。 Hooks相关 说一下你对Hooks的理解 Hooks是React16.8中引入的新特性,它可以让我们在不编写class的情况下使用state和其他的React特性。Hooks的目的是为了解决React中组件之间状态逻辑复用的问题,它可以让我们在不编写class的情况下使用state和其他的React特性。Hooks的目的是为了解决React中组件之间状态逻辑复用的问题,它可以让我们在不编写class的情况下使用state和其他的React特性。Hooks的目的是为了解决React中组件之间状态逻辑复用的问题,它可以让我们在不编写class的情况下使用state和其他的React特性。 React中有哪些常用的Hooks useState、useEffect、useContext、useReducer、useCallback、useMemo、useRef、useImperativeHandle、useLayoutEffect、useDebugValue Vue相关基础 v-if和v-show有什么区别? v-if和v-show都是Vue框架中的指令,它们的作用都是控制元素的显示和隐藏,区别在于:v-if是创建和删除元素,而v-show只是改变元素中的display样式属性。 一般来说,v-if有更高的切换开销,而v-show有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show较好;如果在运行时条件很少改变,则使用v-if较好。 v-if和v-show都是用来控制元素的渲染。v-if判断是否加载,可以减轻服务器的压力,在需要时加载,但有更高的切换开销;v-show调整DOM元素的CSS的dispaly属性,可以使客户端操作更加流畅,但有更高的初始渲染开销。如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。 原理 Proxy和Object.defineProperty有什么区别? Proxy 和 Object.defineProperty 的区别主要有以下几点 Proxy 可以拦截整个对象,而 Object.defineProperty 只能拦截对象的属性。 Proxy 可以监听一些 Object.defineProperty 监听不到的操作,比如监听数组,监听对象属性的新增、删除等。 Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改。 Proxy 不兼容 IE,而 Object.defineProperty 不兼容 IE8 及以下版本。 Vue的响应式原理是什么? Vue的响应式原理是通过Object.defineProperty()来实现的。Vue会将data中的数据进行递归地遍历,对每个属性都通过Object.defineProperty()来设置getter和setter,当数据发生变化时,会触发setter,从而通知依赖进行更新。 谈谈你对nextTick的理解 nextTick 是 Vue.js 中的一个 API,它的作用是在下次 DOM 更新循环结束之后执行延迟回调。nextTick 本质就是执行延迟回调的钩子,接受一个回调函数作为参数,在下次 DOM 更新循环结束之后执行延迟回调。nextTick 主要用于处理数据动态变化后,DOM还未及时更新的问题,用 nextTick 就可以获取数据更新后最新 DOM 的变化。 Vue.nextTick() 方法的实现原理是,把接收到的回调函数 flushSchedulerQueue() 保存在 callbacks 中,根据 pending 来判断当前是否要执行 timerFunc(),timerFunc() 是根据当前环境判断使用哪种异步方式,按照优先级依次使用 Promise (微),MutationObserver 监视 dom 变动 (微),setImmediate (宏),setTimeout (宏)。 Webpack相关 说一下你对webpack的理解 Webpack是一个模块打包工具,它可以将各种资源,如JS、CSS、图片等都作为模块来处理和使用。 谈谈你对webpack新特性的理解 模块联邦(Module Federation):可以让多个应用程序共享代码,从而提高应用程序的性能和可维护性。 零配置(Zero Configuration):可以让开发人员更快地开始使用 Webpack,而无需进行复杂的配置。 集成优化(Integrated Optimization):可以让开发人员更轻松地优化应用程序的性能。 改进的 Tree Shaking:可以更好地优化应用程序的大小。 改进的缓存:可以更好地缓存应用程序的代码。 webpack的五大模块有哪些 Entry:入口模块,Webpack会从入口模块开始,递归地找出所有的依赖模块。 Output:输出模块,Webpack会将所有的依赖模块打包成一个或多个bundle文件。 Loader:模块转换器,Webpack只能理解JavaScript和JSON文件,而Loader可以让Webpack处理其他类型的文件,并将它们转换为有效的模块,以供应用程序使用,以及被添加到依赖图中。 Plugins:扩展插件,在Webpack构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。 Mode:模式,Webpack提供了三种模式,分别是development、production和none。Mode的作用是设置Webpack的内置优化,不同的模式会有不同的优化策略。 谈谈你对webpack生命周期的理解 初始化(Initialization):在 Webpack 开始编译前执行,用于初始化插件等。 编译(Compilation):在 Webpack 开始编译时执行,用于收集模块依赖等。 构建模块(Module Build):在 Webpack 编译模块时执行,用于处理模块代码等。 完成模块构建(Module Completion):在 Webpack 完成模块构建时执行,用于处理模块构建结果等。 优化(Optimization):在 Webpack 开始优化编译时执行,用于优化编译结果等。 生成资源(Asset Generation):在 Webpack 开始生成资源时执行,用于生成最终的资源文件等。 输出(Output):在 Webpack 输出最终的资源文件前执行,用于处理输出结果等。 其他Gti git fetch和git merge的区别 git fetch:从远程获取最新版本到本地,不会自动merge。 git merge:将本地分支与远程分支合并。 git回滚提交 git reset –hard HEAD^:回滚到上一个版本。 git reset –hard HEAD^^:回滚到上上一个版本。 git reset –hard HEAD~100:回滚到前100个版本。 git reset –hard commit_id:回滚到指定的版本。 git reset –hard:将HEAD指针指向另一个提交,并更改工作目录和暂存区域。 git checkout:检出以前的提交。这不会更改历史记录,但会更改工作目录和暂存区域。 git reset和git revert的区别 git reset:回滚到指定的版本,会删除指定版本之后的所有提交记录,不可恢复。 git revert:回滚到指定的版本,会生成一个新的提交记录,可恢复。","link":"/2023/06/30/frontEnd/nowcoder/interview/"},{"title":"面试问题记录","text":"最近的面试题汇总 微购科技 2023-06-16 为什么Hooks只能在函数组件中使用?(开局放大招,直接白给) React Hooks 是 React 16.8 的新增特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。Hooks 是一些可以让你在函数组件里“钩入” React state 以及生命周期等特性的函数。Hooks 可以让函数组件拥有一些类组件特性,比如定义修改 state、组合生命周期,性能优化等。Hooks 函数只能在函数组件内部使用,不可在函数组件外或者类组件中使用。这是因为 Hooks 是基于函数组件的设计,而类组件已经有了自己的生命周期和 state 管理机制,所以不需要使用 Hooks。 说一下JavaScript的数据类型 基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(ES6新增);引用数据类型:Object、Array、Function、Date、RegExp、Error typeof和instanceof的区别 typeof是一元操作符,放在其单个操作数的前面,操作数可以是任意类型。返回值为表示操作数类型的一个字符串。instanceof是二元操作符,放在两个操作数中间,左边操作数是一个对象,右边操作数是一个函数。如果右边对象是左边对象的实例,则返回true,否则返回false。 追问:instanceof的实现原理 instanceof的实现原理是:遍历左边对象的原型链,如果找到右边对象的原型,则返回true,否则返回false。 追问:1 instanceof Number的结果是什么 1 instanceof Number的结果是false,因为1是基本数据类型,不是对象,所以不是Number的实例。 追问:’1’ instanceof String的结果是什么 ‘1’ instanceof String的结果是false,因为’1’是基本数据类型,不是对象,所以不是String的实例。 引申:new Number(1) instanceof Number的结果是什么 new Number(1) instanceof Number的结果是true,因为new Number(1)是Number的实例。 引申:new String(‘1’) instanceof String的结果是什么 new String(‘1’) instanceof String的结果是true,因为new String(‘1’)是String的实例。 既然’1’不是String的实例,那为什么’1’可以调用String的方法(自动装箱) 因为JavaScript在调用字符串方法时,会自动将基本数据类型转换为对应的包装对象,然后再调用对应的方法,自动转换的过程称为装箱。但是这个过程在instanceof中不会发生,所以’1’ instanceof String的结果是false。 引申:怎么准确判断元素的类型 使用Object.prototype.toString.call()方法,返回一个表示对象的字符串,格式为”[object 类型]”,其中类型就是对象的类型,例如:[object String]、[object Number]、[object Boolean]、[object Undefined]、[object Null]、[object Object]、[object Array]、[object Function]、[object Date]、[object RegExp]、[object Error]、[object Symbol]。 JavaScript中有哪些数组方法 push、pop、shift、unshift、splice、slice、concat、join、reverse、sort、indexOf、lastIndexOf、forEach、map、filter、some、every、reduce、reduceRight、find、findIndex、includes、fill、copyWithin 追问:那么这些方法分别属于ES多少? ES3:push、pop、shift、unshift、splice、slice、concat、join、reverse、sort、indexOf、lastIndexOf、forEach、map、filter、some、every、reduce、reduceRight、find、findIndex、includes、fill、copyWithin; ES5:forEach、map、filter、some、every、reduce、reduceRight、indexOf、lastIndexOf、find、findIndex、includes; ES6:copyWithin、fill、find、findIndex、includes 追问:如果我要生成一个长度为10的随机数数组,只用一行代码,应该怎么做? 1new Array(10).fill(0).map(item => Math.random()) 追问:为什么这里要用fill(0)? 因为当使用 new Array(10) 创建数组时,实际上是创建一个空的数组对象,属性 length = 10。除此以外,这个对象是一个空对象。对象中并没有数组对应的索引键(index key) 因此,如果直接使用 map 方法,会出现以下错误:Uncaught TypeError: Cannot read property ‘map’ of undefined 为了解决这个问题,可以使用 fill() 方法来填充数组,然后再使用 map() 方法。例如:new Array(10).fill(0).map(() => Math.random()) 引申:如果我要生成一个长度为10的随机数数组,只用一行代码,且每个元素的值都不相同,应该怎么做? 1234let arr = new Array(10).fill(0).map(i=>Math.floor(Math.random() * 10));while (new Set(arr).size !== arr.length) { arr = new Array(10).fill(0).map(i=>Math.floor(Math.random() * 10));} 谈谈你对前端设计模式的理解 前端设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。前端常见的设计模式有以下几种 外观模式(Facade Pattern) 代理模式(Proxy Pattern) 工厂模式(Factory Pattern) 单例模式(Singleton Pattern) 策略模式(Strategy Pattern) 观察者模式(Observer Pattern) 装饰器模式(Decorator Pattern) 适配器模式(Adapter Pattern) 模板方法模式(Template Method Pattern) 这些设计模式都有各自的特点和应用场景,可以根据实际需求选择合适的设计模式来解决问题。 引申:谈谈你对原型链的理解 原型链是由原型对象组成的链式结构,每个对象都有一个原型对象,对象的原型对象也有自己的原型对象,这样就形成了一个原型链。原型链的作用是:当对象访问一个属性时,如果对象本身没有这个属性,则会去原型对象中查找,如果原型对象中也没有这个属性,则会去原型对象的原型对象中查找,以此类推,直到找到这个属性或者找到原型链的尽头。原型链的尽头是Object.prototype,Object.prototype的原型对象为null。 瑞云科技 2023-06-21笔试 Object.prototype.proto 返回: undefined Object.prototype._proto_的结果是undefined,因为Object.prototype是所有对象的原型对象,它没有原型对象。 ‘[object Object]’ == {} 返回: true 在JavaScript中,’[object Object]’是通过Object.prototype.toString.call()方法返回的字符串。而{}是一个对象字面量,它是Object的实例。当你使用==运算符比较两个对象时,它们会被转换为原始值。在这种情况下,它们都被转换为字符串’[object Object]’,因此它们相等,返回true。 ‘[object Object]’ === {} 返回: false (引申) 在JavaScript中,’[object Object]’是通过Object.prototype.toString.call()方法返回的字符串。而{}是一个对象字面量,它是Object的实例。当你使用===运算符比较两个对象时,它们不会被转换为原始值,因此它们不相等,返回false。 写一个判断变量是否为数组的表达式 Array.isArray() 写一个电话号码的正则表达式 /^1[3456789]\\d{9}$/ 简述同源策略与跨源资源共享机制 同源策略是浏览器的一种安全策略,它用于限制一个源的文档或脚本如何能与另一个源的资源进行交互。同源策略的限制包括以下几种: Cookie、LocalStorage 和 IndexDB 无法读取 DOM 和 Js对象无法获得 AJAX 请求不能发送 跨源资源共享机制(CORS)是一种机制,它使用额外的HTTP头来告诉浏览器,让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。 简述块格式化上下文(BFC)的作用 块格式化上下文(BFC)是Web页面的可视化CSS渲染的一部分,是布局过程中生成块级盒子的区域,也是浮动元素与其他元素的交互限定区域。BFC的作用有以下几点: 清除浮动 防止同一BFC容器中的相邻元素间的外边距重叠 防止元素被浮动元素覆盖 自适应两栏布局 防止文字环绕 分属于不同的BFC时,可以阻止margin重叠 简述JavaScript的并发模型与事件循环 JavaScript是一门单线程语言,它的并发模型是基于事件循环的。事件循环是一个执行模型,用于等待和发送消息和事件。JavaScript的事件循环包含以下几个步骤: 执行全局Script同步代码,这些同步代码有可能会导致宏任务的执行,例如setTimeout、setInterval、setImmediate、I/O、UI交互事件、postMessage、MessageChannel、setImmediate等。 执行所有的微任务,微任务包括process.nextTick、Promise、Object.observe、MutationObserver等。 执行一个宏任务,执行完毕后再执行微任务,这样一直循环下去,直到所有的任务都执行完毕。 v1, v2的类型分别是什么?为什么? 12345678910function prop<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } function prop2<T>(obj: T, key: keyof T) { return obj[key]; } let o = { p1: 0, p2: "" }; let v1 = prop(o, "p1"); let v2 = prop2(o, "p1"); v1的类型是number,v2的类型是T[keyof T] 两个函数的区别在于第一个函数使用了泛型类型参数K extends keyof T来指定key参数应该是T对象类型的一个键。这意味着函数的返回类型被推断为T中具有该键的属性的类型。在这种情况下,由于键是“p1”,而对象具有一个类型为number的属性“p1”,因此返回类型被推断为number。 第二个函数使用keyof T作为键参数的类型。这意味着函数的返回类型被推断为T中所有可能属性类型的联合。在这种情况下,由于T具有类型为number和string的属性“p1”和“p2”,因此返回类型被推断为number | string。 实现 range 函数,入参类型为数字,输出值与下标相同,长度为该数值的数组。 1234 function range(n) { return new Array(n).fill(0).map((_, i) => i) }console.log(range(5)); // [0, 1, 2, 3, 4] 现给定一个根据偏移量获取字母的方法getSentenceFragment,实现一个获取所有字母的方法getSentence完成以下输入输出。 123456789101112131415161718192021const getSentenceFragment = (offset = 0) => new Promise((resolve, reject) => { const pageSize = 3; const sentence = [...'hello world']; setTimeout(() => resolve({ data: sentence.slice(offset, offset + pageSize), nextPage: offset + pagesize < sentence.length ? offset + pageSize : undefined },500));})const getSentence = () => { let result = []; let nextPage = 0; do { const {data, nextPage: next} = await getSentenceFragment(nextPage); result.push(...data); nextPage = next; } while (nextPage !== undefined); return result;}getSentence().then(console.log) // ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'] 彩讯科技 2023-07-04笔试","link":"/2023/07/01/frontEnd/nowcoder/frontEnd-interview/"},{"title":"S01E02-变量及数据类型","text":"万物起源 变量 概念 什么是变量? 存储值的一个容器或代号 什么是值? 存储的数据 声明变量的几种方式 var(ES3) function(ES3)创建函数(函数名也是变量,只不过存储的值是函数类型的) ES6 新增: let const import ES6 的模块导入 class 创建类 注意:常量声明必须赋值,而且不能重复赋值。 1234const a // Uncaught SyntaxError: Missing initializer in const declarationconst m = 100;m = 200; //=> 报什么错?Uncaught Type: Assignment to constant variable.未捕获的类型错误:分配给常量变量 数据类型值的类型:JS 中的变量是没有类型的,只有值才有。 值的类型可分为: 基本类型(值类型) Null Undefined String Boolean Number Symbol(ES6 中新增加的一个特殊的类型,唯一的值) 引用类型 普通对象 RegExp(正则对象) Date(日期对象) Math(数学对象) Error(错误对象) Function 注意:说到数据的类型都是大写哦,尽管 typeof 会返回小写的 Function: 特殊的引用类型,不用于存储数据 需要注意的知识点这部分内容比较基础,不会全部列出,会介绍一些特殊的,需要注意的。 基本包装类型 3 个特殊的基本类型:String、Number、Boolean 在逻辑上讲,基本类型值是没有属性和方法的,但却有 .length 属性和很多的 API,这是因为 JS 底层会自动将 String、Number、Boolean 类型值包装为一个封装对象。 栗子: 123456var a = new Boolean( false );if (!a) { console.log(1);}console.log(2); 答案是:输出 2! null && undefined 的区别 都代表空或者没有,作为值时小写 null:空对象指针(没有指向任何的内存空间) undefined:未定义 null 一般都是在初始化值时,先手动的先赋值为 null,然后再给他赋具体的值 12var num = null;num = 12; undefined 一般都不是人为手动控制的,大部分都是浏览器自主为空(后面可以赋值也可以不赋值) 1var num; //=>此时变量的值浏览器给分配的就是 undefined 项目中一些细节问题:初始化值时,一般初始化为 null,因为它在内存中是不占空间的。而 0、[]、{} 等是有值的,会在内存中占空间。 undeclared 是一种语法错误。访问未声明的变量, 则会抛出异常, 终止执行。ReferenceError:a is not defined。 特殊的 NaN我们来介绍一个非常特殊的数字: NaN:not a number,不是一个数字 其实,not a number 容易引起误解,因为 NaN 仍然是数字类型,叫无效数值更准确些。 12var a = 2 / "foo"; // NaNtypeof a === "number"; // true isNaN():检测当前的数字是不是无效数字 在重学 JS 系列 - 数据类型转换会对 isNaN 有更为细致的讲解。 NaN 的比较 特殊到自己不等于自己。 isNaN(num) 常作为语句的条件,来检测是否是有效数字 123if(isNaN(num)){}// 条件不可以用 Number(num) == NaN 对象字面量语法需要注意的几点 一般来说,对象的属性名只能是字符串格式的或者数字格式的,不能是其它类型的。 当对象的属性名是数字时,不支持点表示法。 1234567var obj = { name: 'chen', 0: 100,};obj[0] //=>100obj['0'] //=>100obj.0 //=>Uncaught Syntax: Unexpected number 语法错误 当属性名是其他格式时,浏览器会把这个值 toString() 转换为字符串,然后再以这个字符串为key进行存储。 1obj[{}] = 300; //=>先把({}).toString()后的结果作为对象的属性名存储进来 obj['[object Object]']=300 访问对象的属性 点表示法:对象.属性 方括号表示法:对象[“属性”],可以通过变量来访问属性。 不管是哪种写法, 有这个属性名,则可以正常获取到值(哪怕是 null),赋值操作会修改这个属性的值 没有这个属性名,则获取的结果是 undefined,赋值操作会新增加这个属性 栗子 123456789var obj = { name:'chen', age:9};var name = 'chen';obj.name //=>'chen' obj['name'] //=>'chen' obj[name] //=>? 基本类型与引用类型的区别 这是非常常见的、又非常基础的面试题哟! 红宝书中是这样概括的: 存储位置的区别 基本类型的值一般被保存在于栈内存中,引用类型的值是对象,被保存在堆内存中。 包含引用类型的变量的值是一个指向该对象的一个指针,这个指针被保存在栈内存中。 访问方式的区别 基本类型是按值访问的。因为可以操作存储在变量中的实际的值 引用类型是按引用访问的。因为引用类型的值是保存在堆内存中的对象,这块不同于其它语言,JS 不允许直接访问对象的内存空间。在操作对象时,实际上是操作的是对象的引用而不是实际的对象。 复制操作的区别 基本类型复制的是这个值的一个副本,操作两个变量互不影响。 引用类型复制的其实是一个指针(地址的副本),复制操作结束后,两个变量将指向堆中的同一个对象,改变一个,会影响另一个。 为了彻底理解,我们来看一个的栗子: 1234var a = 12;var b = a;b = 13;console.log(a); //=>12 执行过程是这样子的: 首先声明一个变量 a、b(变量提升,值为 undefined),在栈内存中开辟一块内存空间存储 12 执行 var b = a;,复制过程: 复制一份 12 的副本,在栈内存中重新开辟一块内存空间,存储这个副本,然后将这个副本赋值给变量 b 注意:原来的 12 和它的副本没有任何关系,在栈内存中占据不同的内存空间,互不影响 为了验证这句话,我们执行 b = 13,会在栈内存中再开辟一块内存空间,存储 13,将 13 赋值给变量 b,原来的 12 的副本已废弃,修改b的值不会影响a的值 值类型复制的过程,如图所示: 引用类型是如何实现复制的呢?还是上栗子吧。。。 1234var obj1 = {m: 20};var obj2 = obj1;obj2['m'] = 100;console.log(obj1.m); //=>100 上面的代码,一起来分析一下: 首先声明一个变量 obj1、obj2(变量提升,值为 undefined),然后在堆内存中开辟一块内存空间,存储对象的键值对(为这个空间加了一个16进制的地址的标记,就是我们常说的指针),接着将这个地址赋值给变量 obj1 遇见 var obj2 = obj1;,是这样子复制的: 复制一份这个地址的副本,在栈内存中重新开辟一块内存空间,存储起来,然后将这个副本赋值给变量 obj2,这时,obj2 和 obj1 指向堆内存中同一个对象,不管修改谁,其实修改的是一个值,所以最后输出 100. 最后,我们画一个图,来形象的展示一个引用类型复制的过程: 思考题: 12345var obj = { n: 10, m: obj.n * 10};console.log(obj.m); 答案是:在 m: obj.n * 10 行报错:Uncaught TypeError: Cannot read property ‘n’ of undefined,思考一下,为什么? 我们一起来分析一下: 变量提升,obj = undefined 开辟一个新的堆内存(比如地址是 AAAFFF111),把键值对存储到堆内存中-> n: 10-> m: obj.n*10 =>obj.n 此时堆内存信息还没有存储完成,空间的地址还没有给 obj,此时的 obj 是undefined, 访问 obj.n 就是在访问 undefined.n 结束重学 JS 系列 预计 25 篇左右,这是一个旨在帮助大家,其实也是帮助我自己捋顺 JavaScript 底层知识的系列。主要包括变量和类型、执行上下文、作用域及闭包、原型和继承、单线程和异步、JS Web API、渲染和优化几个部分,将重点讲解如执行上下文、作用域、闭包、this、call、apply、bind、原型、继承、Event-loop、宏任务和微任务等比较难懂的部分。让我们一起拥抱整个 JavaScript 吧。 大家或有疑问、或指正、或鼓励、或感谢,尽管留言回复哈!非常欢迎 star 哦! 点击返回博客主页","link":"/2021/01/03/frontEnd/reStudyJS/Part01-%E5%8F%98%E9%87%8F%E5%92%8C%E7%B1%BB%E5%9E%8B/S01E02-%E5%8F%98%E9%87%8F%E5%8F%8A%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/"},{"title":"S01E01-从堆、栈、内存机制开始","text":"JavaScript 中有三种数据结构: 栈(stack) 、堆(heap)、 队列(queue)。它们是我们理解 JavaScript 核心的基础。 本篇将围绕栈(stack) 、堆(heap),以及 JavaScript 的内存机制来展开。队列(queue)将会放在本系列的第四部分-异步和性能来讲解。 栈(stack)栈(stack)有三层含义: 含义一:数据结构栈的第一种含义是表示的是数据的存放方式。 栈存储数据的特点:LIFO规则,即后进先出(Last In, First Out)。数据存储时只能从顶部逐个存入,取出时也只能从顶部逐个取出。顶部是唯一的出口。 借助前端大神的乒乓球盒子的栗子: 如图所示,我们只能从栈顶取出或放入乒乓球,最先放进盒子的总是最后才能取出。 乒乓球的放入/取出,在栈中可称为入栈/出栈。 含义二:函数调用栈(call stack)stack 的第二层含义是代码的一种运行方式。通过栈的方式来管理代码的执行顺序,是栈数据结构的一种实践,遵循LIFO规则。 含义三:内存空间stack 的第三种含义是存放数据的一种内存区域。在 JS 运行时,需要内存空间存放数据。一般来说,内存空间又被分为两种:栈内存(stack)、堆内存(heap)。 栈内存的特点: 一般存放基本类型的值和引用类型的引用地址(指针)。 是有序的 在内存中占据空间小,大小固定 例如,最简单的,声明一个变量a: 1var a = 12 如图所示,会在栈内存中开辟一块空间存储 12,把存储的 12 赋值给 a。 我们需要注意的是: JS 允许直接操作保存在栈内存中的值。因此,基本类型是按值访问的。 堆(heap)堆只有一层含义:内存空间。堆内存的特点: 一般存放引用类型的值 是无序的 引用类型的值没有固定大小,可扩展(一个对象我们可以添加多个属性),占据空间大 为了更好的理解堆内存空间,我们看一个最简单的: 1var obj = { m : 20 } 声明一个变量 obj,会在堆内存中开辟一块新的空间,把对象中的键值对依次存储进来(同时,为这个空间加了一个16 进制的地址的标记),这个地址和这块空间是关联在一起的,如图所示。注意:这个空间地址是被保存在栈内存中的。 我们需要注意的是: JS 不允许直接访问堆内存中的位置。在操作对象时,实际上是操作的是对象的引用。因此,引用类型是按引用访问的。 内存空间管理不管是栈内存,还是堆内存,都是由系统自动分配和自动释放的。了解内存的管理机制,对于提高我们的页面性能尤其重要。 内存的生命周期一般有三步: 分配:当我们声明变量、函数、对象时,系统会自动为它们分配内存 使用:即读/写内存,也就是使用变量、函数等 回收:使用完毕,由垃圾回收机制自动回收不再使用的内存 分配和使用都很好理解。对于内存的释放回收,我们接下来重点看一下。 垃圾回收机制 垃圾回收机制:浏览器会在空闲时,遍历所有的内存空间,发现谁不被占用,就会自主的进行内存回收。 该机制的核心思想就是找到谁不被使用,因此我们可以通过标记清除的算法来标记哪些内存不再被占用。 对于堆内存,我们可以将占用它的变量手动赋值为 null 来标记清除。 对于栈内存,局部环境中,只有当函数执行完成后,函数局部环境声明的变量不再需要时,才会被释放(特殊不销毁的情况:闭包)。全局环境只有当页面关闭时才会解除变量引用。因此,开发者应尽量避免创建全局变量。 垃圾回收算法除了”标记清除”,还有一种”引用计数”,不常用,仅作了解。 内存泄漏由于疏忽或错误造成程序未能释放那些已经不再使用的内存,造成内存的浪费。引起内存泄漏的情况: 在函数内部,不带var声明变量,给 window 添加了属性 12345function foo() { this.a = 'window.a'; b = '全局变量'; }foo(); 当不需要 setInterval 或者 setTimeout 时,定时器没有被 clear,定时器的回调函数以及内部依赖的变量都不能被回收,造成内存泄漏。 闭包可以保存内部状态,使其得不到释放,造成内存泄漏。 没有清理 DOM 元素引用,手动清除为 null 即可 结束重学 JS 系列 预计 25 篇左右,这是一个旨在帮助大家,其实也是帮助我自己捋顺 JavaScript 底层知识的系列。主要包括变量和类型、执行上下文、作用域及闭包、原型和继承、单线程和异步、JS Web API、渲染和优化几个部分,将重点讲解如执行上下文、作用域、闭包、this、call、apply、bind、原型、继承、Event-loop、宏任务和微任务等比较难懂的部分。让我们一起拥抱整个 JavaScript 吧。 大家或有疑问、或指正、或鼓励、或感谢,尽管留言回复哈!非常欢迎 star 哦!","link":"/2021/01/01/frontEnd/reStudyJS/Part01-%E5%8F%98%E9%87%8F%E5%92%8C%E7%B1%BB%E5%9E%8B/S01E01-%E4%BB%8E%E5%A0%86%E3%80%81%E6%A0%88%E3%80%81%E5%86%85%E5%AD%98%E6%9C%BA%E5%88%B6%E5%BC%80%E5%A7%8B/"},{"title":"S01E03-数据类型转换","text":"JavaScript 的数据类型转换,我们只讨论三种情况,分别是: 转Boolean类型 Boolean类型只有两个值:true / false,以下2种情况下会转换为布尔类型: 手动转 Boolean() !(先转为布尔类型,再取反) !!(两次取反,只剩下转布尔类型了) 自动转 在流程控制语句中(如 if 语句),会自动执行 Boolean() 转换。 哪些值可以转换为 false? 规律:在 JS 中转换为 false 的,只有 null、undefined、空字符串、0 和 NaN 这五个值。 1234Boolean(0)=>falseBoolean([])=>true!0=>true!!0=>false 常见笔试题 12[]==false //=>两边都转为数字再比较,true![]==false //=>[]先转为Boolean类型,取反,true 转Number类型 转换规律: 基本类型转换为数字,使用Number() 引用类型转换为数字,先toString()转换为字符串,然后再将字符串Number()转换为数字 以下4种情况下会转换为数字: 隐式转:isNaN()isNaN() 检测机制:首先检测当前的值是不是数字类型的,如果不是会先转换为数字类型的,然后再判断。 123456789101112//=>语法:isNaN([value])isNaN('13') =>falseisNaN('陈陈') =>trueisNaN(true) =>falseisNaN(false) =>falseisNaN(null) =>falseisNaN(undefined) =>trueisNaN({age:9}) =>trueisNaN([12,23]) =>trueisNaN([12]) =>falseisNaN(/^$/) =>trueisNaN(function(){}) =>true 这里有几个引用类型的需要注意一下: 12345678910[对象]({}).toString() ->'[object Object]' ->NaN[数组][].toString() ->''->0[12,23].toString() ->'12,23' ->NaN[12].toString() ->'12' ->12[正则]/^$/.toString() ->'/^$/' ->NaN 显式转:Number() / parseInt() / parseFloat() **Number()**:浏览器自动转换默认的方法 遇见字符串有洁癖:如果字符串中出现任意一个非数字字符,结果则为NaN。 123456789101112131415[字符串] Number('')=>0 Number(' ')=>0 //空格 Number('\\n')=>0 //换行符 Number('\\t')=>0 //制表符 Number('13')=>13 Number('13px')=>NaN Number('13.5')=>13.5 Number('13.5.0')=>NaN[布尔] Number(true) =>1 Number(false) =>0[其它] Number(null)=>0 Number(undefined)=>NaN **parseInt()/parseFloat()**:专门用于将字符串转换为数值。 规则:从字符串的最左边开始查找,遇到非有效字符查找结束。 parseInt:整数部分 parseFloat:小数部分(第二个小数点无效) 12345678[字符串]parseInt('')=>NaN(区别于Number)parseInt('13.5px')=>13parseInt('width:13.5px')=>NaNparseInt('1px3')=>1parseFloat('13.5px')=>13.5parseFloat('5a-1')=>5parseFloat('5e-1')=>0.5 //=> ??? parseInt()支持两个参数,parseInt(’10px’, 2 )输出什么?2 隐式转换:+ - / * 规律:在JS中,+ - * / % 都是数学运算 +号,遇到字符串,开始起拼接作用,没有遇到之前是数学运算 除 + 以外,其它在运算时,如果有非数字类型的值,会先转换为Number类型(自动发生Number()转换),然后再进行运算。 需要注意的细节问题: i++ 遇见数字型字符串,就是单纯的数学运算,已经摒弃掉字符串拼接的规则 我们来看一些栗子: 123456789'3'-1 =>2'3px'-1 =>NaN2+'3px' =>'23px' 字符串拼接5+2+'3px'+1 =>'73px1' var i='3';i=i+1; =>'31'i+=1; =>'31'i++; =>4 //注意 思考题 123456789var num = '10';if (num == 10) { num++;} else if (num == 5) { num--;} else { num = 0;}console.log(num); //=>11 “==”比较,两边多数会转换为Number类型如果“==”两边的数据类型不相同,会首先进行强制类型转换,转换为相同类型再比较。 三种特殊情况: NaN null 和 undefined 12null == undefined //=>truenull === undefined //=>false 对象 == 对象 对象操作的是引用地址,因此判断两个引用地址是否指向同一个对象 12345{name:'xxx'}=={name:'xxx'} //=>false[]==[] //=>falsevar obj1={};var obj2=obj1;obj1==obj2 //=>true 除了上边的三种特殊情况,两边只要不是数字类型的,都转换为数字类型,比如: 12345671==true //=>true1==false //=>false2==true //=>false 规律不要混淆,这里是把true变为数字1[]==true //false 都转换为数字 0==1[]==false //true 都转换为数字 0==0![]==true //false![]==false //true 先算![],把数组转换为布尔取反=>false 转String类型toString()/ String() / toFixed() / join() 等方法 toString()/ String() 除了对象,都是你理解的转换结果,只要是普通对象,最后结果都是’[object Object]’。 栗子: 123456789101 ->'1'NaN ->'NaN'null ->'null'[] ->''[13] ->'13'[12,23] ->'12,23'// 【对象】{name:'xxx'} ->'[object Object]'{} ->'[object Object]' toFixed() 12var n = Math.PI;//=>获取圆周率:n.toFixed(2);//=>'3.14' join()12var ary = [12,23,34];ary.join('+');//=>'12+23+34' alert() / confirm() / prompt() / document.write() 等输出内容的方法这里有一个坑: alert(a++) =>是先执行 alert(a),然后 a 再自增 1 alert(++a) =>是先 a 自增 1,然后再执行 alert(a) 123let a = 1;alert(a++);//=>'1'console.log(a);//=>2 同理: 123let a = 1;console.log(a++);//=>1console.log(a);//=>2 “+”拼接字符串时 规律:当“+”连接的表达式中出现字符串时,开始拼接,前边的是数学运算 121+true //=>2 数学运算'1'+true //=>'1true' 字符串拼接 思考题 12312+true+false+null+undefined+[]+'陈'+null+undefined+[]+true=>'NaN陈nullundefinedtrue' 这里需要注意的是: 引用类型参与’+’运算比如数组、对象(注意:对象要加括号),虽然没有看见字符串,但是当引用类型转换为数字时,首先会转换为字符串,所以'+'起的是字符串拼接的作用。 123[12]+10 //=>'1210' ({})+10 //=>"[object Object]10"[]+10 //=>"10" 特殊情况:{}+任意数据类型 =>根本就不是数学运算,也不是字符串拼接,它是两部分代码=>实际操作的是后边的数据,会执行Number()转换为数字类型 12345678910{}+10 //=>10// {} 代表一个代码块(块级作用域),严格写法:{}; +10;// +10 才是我们的操作// more{}+'' //=>0{}+[] //=>0{}+null //=>0{}+undefined //=>NaN{}+{} //=>"[object Object][object Object] 给对象设置属性名时对象的属性只能是数字或者字符串,如果不是字符串,首先转换为字符串,然后再以这个字符串为 key 存储到对象中。 结束重学 JS 系列 预计 25 篇左右,这是一个旨在帮助大家,其实也是帮助我自己捋顺 JavaScript 底层知识的系列。主要包括变量和类型、执行上下文、作用域及闭包、原型和继承、单线程和异步、JS Web API、渲染和优化几个部分,将重点讲解如执行上下文、作用域、闭包、this、call、apply、bind、原型、继承、Event-loop、宏任务和微任务等比较难懂的部分。让我们一起拥抱整个 JavaScript 吧。","link":"/2021/01/10/frontEnd/reStudyJS/Part01-%E5%8F%98%E9%87%8F%E5%92%8C%E7%B1%BB%E5%9E%8B/S01E03-%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2/"}],"tags":[{"name":"随笔","slug":"随笔","link":"/tags/%E9%9A%8F%E7%AC%94/"},{"name":"面试","slug":"面试","link":"/tags/%E9%9D%A2%E8%AF%95/"},{"name":"找工作","slug":"找工作","link":"/tags/%E6%89%BE%E5%B7%A5%E4%BD%9C/"},{"name":"小程序","slug":"小程序","link":"/tags/%E5%B0%8F%E7%A8%8B%E5%BA%8F/"},{"name":"考研","slug":"考研","link":"/tags/%E8%80%83%E7%A0%94/"},{"name":"JavaScript","slug":"JavaScript","link":"/tags/JavaScript/"},{"name":"DOM编程艺术","slug":"DOM编程艺术","link":"/tags/DOM%E7%BC%96%E7%A8%8B%E8%89%BA%E6%9C%AF/"},{"name":"转载","slug":"转载","link":"/tags/%E8%BD%AC%E8%BD%BD/"},{"name":"重学JS","slug":"重学JS","link":"/tags/%E9%87%8D%E5%AD%A6JS/"},{"name":"随想","slug":"随想","link":"/tags/%E9%9A%8F%E6%83%B3/"},{"name":"blog - 图床","slug":"blog-图床","link":"/tags/blog-%E5%9B%BE%E5%BA%8A/"},{"name":"Hexo","slug":"Hexo","link":"/tags/Hexo/"},{"name":"Hexo主题","slug":"Hexo主题","link":"/tags/Hexo%E4%B8%BB%E9%A2%98/"},{"name":"blog","slug":"blog","link":"/tags/blog/"},{"name":"图床","slug":"图床","link":"/tags/%E5%9B%BE%E5%BA%8A/"},{"name":"金融","slug":"金融","link":"/tags/%E9%87%91%E8%9E%8D/"},{"name":"React","slug":"React","link":"/tags/React/"},{"name":"专项练习","slug":"专项练习","link":"/tags/%E4%B8%93%E9%A1%B9%E7%BB%83%E4%B9%A0/"},{"name":"错题本","slug":"错题本","link":"/tags/%E9%94%99%E9%A2%98%E6%9C%AC/"},{"name":"变量和类型","slug":"变量和类型","link":"/tags/%E5%8F%98%E9%87%8F%E5%92%8C%E7%B1%BB%E5%9E%8B/"}],"categories":[{"name":"article","slug":"article","link":"/categories/article/"},{"name":"essay","slug":"essay","link":"/categories/essay/"},{"name":"frontEnd","slug":"frontEnd","link":"/categories/frontEnd/"},{"name":"Thoughts","slug":"Thoughts","link":"/categories/Thoughts/"},{"name":"FrontEnd","slug":"FrontEnd","link":"/categories/FrontEnd/"},{"name":"JavaScript","slug":"frontEnd/JavaScript","link":"/categories/frontEnd/JavaScript/"},{"name":"Finance","slug":"Finance","link":"/categories/Finance/"},{"name":"frontEnd","slug":"article/frontEnd","link":"/categories/article/frontEnd/"}]}