Giter Site home page Giter Site logo

blog's Introduction

高质量文章

手写核心原理系列

算法总结

我的翻译

工作分享

实战经验

面试&读书笔记总结

[java学习]

设计模式

Vue源码解读

深入浅出学习node

前端技巧搜集录

个人成长

培歌行,又名“阳光”,公众号《前端阳光》,一名苦逼的前端开发工程师。

简单经历:

  • 2019年,自学前端
  • 2021年,大学毕业
  • 2020年8月, 入职bigo实习
  • 2021年7月, 入职bigo正职

希望后来人少走一些弯路

  • 记录自己的成长过程
  • 希望让那些与我一样的草根前端看到希望,不要放弃前端的梦想,哪怕自己起点很低,“If you believe, you can”
  • 博客的评论里总有一些人调侃我,称我为大佬,但其实我还远配不上“大佬”这两个字,我自己深知自己的技术水平也只是皮毛而已。

坚持写作的动力是因为能看到自己写出来的文章可以得到大家的认可,这种认可让我有动力坚持将自己知道的知识写出来分享给大家,让我们一起共同成长。😊😊

有点值得开心的就是我大学的舍友以及玩得比较好的隔壁宿舍至今全部单身,这让我倍感慰藉。

联系方式

除了这里,还可以在其他的地方找到我。

公众号《前端阳光》,可加我微信,可加入技术交流群

blog's People

Contributors

sunny-lucking avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

blog's Issues

JS执行机制(读浏览器核心原理)


@[toc]

1.变量提升阶段,代码位置会改变吗?

从概念的字面意义上来看,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面,正如我们所模拟的那样。但,这并不准确。实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中。对,你没听错,一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进入执行阶段。大致流程你可以参考下图:

2.编译阶段和变量提升存在什么关系呢?

为了搞清楚这个问题,我们还是回过头来看上面那段模拟变量提升的代码,为了方便介绍,可以把这段代码分成两部分。

showName()
console.log(myname)
var myname = '极客时间'
function showName() {
    console.log('函数showName被执行');
}

第一部分:变量提升部分的代码

var myname = undefined
function showName() {
    console.log('函数showName被执行');
}

第二部分:执行部分的代码。

showName()
console.log(myname)
myname = '极客时间'

下面我们就可以把 JavaScript 的执行流程细化,如下图所示


从上图可以看出,输入一段代码,经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码

3.什么是执行上下文

执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。

执行上下文中存在一个变量环境的对象(Viriable Environment),该对象中保存了变量提升的内容,比如上面代码中的变量 myname 和函数 showName,都保存在该对象中。

4.环境对象是怎么创建的

showName()
console.log(myname)
var myname = '极客时间'
function showName() {
    console.log('函数showName被执行');
}

我们可以一行一行来分析上述代码:

  • 第 1 行和第 2 行,由于这两行代码不是声明操作,所以 JavaScript 引擎不会做任何处理
  • 第 3 行,由于这行是经过 var 声明的,因此 JavaScript 引擎将在环境对象中创建一个名为 myname 的属性,并使用 undefined 对其初始化;
  • 第 4 行,JavaScript 引擎发现了一个通过 function 定义的函数,所以它将函数定义存储到堆 (HEAP)中,并在环境对象中创建一个 showName 的属性,然后将该属性值指向堆中函数的位置。(这里有没有发现环境对象是保存在栈里的)。

这样就生成了变量环境对象。接下来 JavaScript 引擎执行可执行代码的时候会把声明以外的代码编译为字节码。

5.执行阶段是执行的呢?

JavaScript 引擎开始执行“可执行代码”,按照顺序一行一行地执行。下面我们就来一行一行分析下这个执行过程:

  • 当执行到 showName 函数时,JavaScript 引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以 JavaScript 引擎便开始执行该函数,并输出“函数 showName 被执行”结果。
  • 接下来打印“myname”信息,JavaScript 引擎继续在变量环境对象中查找该对象,由于变量环境存在 myname 变量,并且其值为 undefined,所以这时候就输出 undefined。
  • 接下来执行第 3 行,把“极客时间”赋给 myname 变量,赋值后变量环境中的 myname 属性值改变为“极客时间”

6.什么是函数调用

函数调用就是运行一个函数,具体使用方式是使用函数名称跟着一对小括号。

7.什么是 JavaScript 的调用栈

JavaScript 引擎正是利用栈的这种结构来管理执行上下文的。在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。

为便于你更好地理解调用栈,下面我们再来看段稍微复杂点的示例代码:

var a = 2
function add(b,c){
  return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return  a+result+d
}
addAll(3,6)

在上面这段代码中,你可以看到它是在 addAll 函数中调用了 add 函数,那在整个代码的执行过程中,调用栈是怎么变化的呢?下面我们就一步步地分析在代码的执行过程中,调用栈的状态变化情况。

第一步,创建全局上下文,并将其压入栈底。如下图所示:


从图中你也可以看出,变量 a、函数 add 和 addAll 都保存到了全局上下文的变量环境对象中。全局执行上下文压入到调用栈后,JavaScript 引擎便开始执行全局代码了。首先会执行 a=2 的赋值操作,执行该语句会将全局上下文变量环境中 a 的值设置为 2。设置后的全局上下文的状态如下图所示:


接下来,第二步是调用 addAll 函数。当调用该函数时,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈中,如下图所示:

addAll 函数的执行上下文创建好之后,便进入了函数代码的执行阶段了,这里先执行的是 d=10 的赋值操作,执行语句会将 addAll 函数执行上下文中的 d 由 undefined 变成了 10。然后接着往下执行,第三步,当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈,如下图所示:


当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9,
紧接着 addAll 执行最后一个相加操作后并返回,addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。最终如下图所示:


至此,整个 JavaScript 流程执行结束了。好了,现在你应该知道了调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。

8.为什么ES6之前要设计成不支持块级作用域呢?

因为当初设计这门语言的时候,并没有想到 JavaScript 会火起来,所以只是按照最简单的方式来设计。没有了块级作用域,再把作用域内部的变量统一提升无疑是最快速、最简单的设计,不过这也直接导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以这些变量在整个函数体内部的任何地方都是能被访问的,这也就是 JavaScript 中的变量提升。

9.变量提升所带来什么问题

  1. 变量容易在不被察觉的情况下被覆盖掉

比如我们重新使用 JavaScript 来实现上面那段 C 代码,实现后的 JavaScript 代码如下:

var myname = "极客时间"
function showName(){
  console.log(myname);
  if(0){
   var myname = "极客邦"
  }
  console.log(myname);
}
showName()

执行上面这段代码,打印出来的是 undefined,而并没有像前面 C 代码那样打印出来“极客时间”的字符串

  1. 本应销毁的变量没有被销毁
function foo(){
  for (var i = 0; i < 7; i++) {
  }
  console.log(i); 
}
foo()

如果你使用 C 语言或者其他的大部分语言实现类似代码,在 for 循环结束之后,i 就已经被销毁了,但是在 JavaScript 代码中,i 的值并未被销毁,所以最后打印出来的是 7。这同样也是由变量提升而导致的,在创建执行上下文阶段,变量 i 就已经被提升了,所以当 for 循环结束之后,变量 i 并没有被销毁。

10.ES6 是如何解决变量提升带来的缺陷

为了解决这些问题,ES6 引入了 let 和 const 关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域。

11.JavaScript 是如何支持块级作用域的

在同一段代码中,ES6 是如何做到既要支持变量提升的特性,又要支持块级作用域的呢?

那么接下来,我们就要站在执行上下文的角度来揭开答案。你已经知道 JavaScript 引擎是通过变量环境实现函数级作用域的,那么 ES6 又是如何在函数级作用域的基础之上,实现对块级作用域的支持呢?你可以先看下面这段代码:

function foo(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a)
      console.log(b)
    }
    console.log(b) 
    console.log(c)
    console.log(d)
}   
foo()

当执行上面这段代码的时候,JavaScript 引擎会先对其进行编译并创建执行上下文,然后再按照顺序执行代码,关于如何创建执行上下文我们在前面的文章中已经分析过了,但是现在的情况有点不一样,我们引入了 let 关键字,let 关键字会创建块级作用域,那么 let 关键字是如何影响执行上下文的呢?接下来我们就来一步步分析上面这段代码的执行流程。

第一步是编译并创建执行上下文,下面是我画出来的执行上下文示意图,你可以参考下:


通过上图,我们可以得出以下结论:

  • 函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。
  • 通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。
  • 在函数的作用域块内部,通过 let 声明的变量并没有被存放到词法环境中。

接下来,第二步继续执行代码,当执行到代码块里面时,变量环境中 a 的值已经被设置成了 1,词法环境中 b 的值已经被设置成了 2,这时候函数的执行上下文就如下图所示:

从图中可以看出,当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,比如在作用域外面声明了变量 b,在该作用域块内部也声明了变量 b,当执行到作用域内部时,它们都是独立的存在。

其实,在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。需要注意下,我这里所讲的变量是指通过 let 或者 const 声明的变量。

再接下来,当执行到作用域块中的console.log(a)这行代码时,就需要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。这样一个变量查找过程就完成了,你可以参考下图:


从上图你可以清晰地看出变量查找流程,不过要完整理解查找变量或者查找函数的流程,就涉及到作用域链了,这个我们会在下面继续介绍。当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出,最终执行上下文如下图所示:

通过上面的分析,想必你已经理解了词法环境的结构和工作机制,块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。

12.这道题输出什么

function bar() {
    console.log(myName)
}
function foo() {
    var myName = "极客邦"
    bar()
}
var myName = "极客时间"
foo()

通过前面的学习,想必你已经知道了如何通过执行上下文来分析代码的执行流程了。那么当这段代码执行到 bar 函数内部时,其调用栈的状态图如下所示:

从图中可以看出,全局执行上下文和 foo 函数的执行上下文中都包含变量 myName,那 bar 函数里面 myName 的值到底该选择哪个呢?也许你的第一反应是按照调用栈的顺序来查找变量,查找方式如下:先查找栈顶是否存在 myName 变量,但是这里没有,所以接着往下查找 foo 函数中的变量。在 foo 函数中查找到了 myName 变量,这时候就使用 foo 函数中的 myName。如果按照这种方式来查找变量,那么最终执行 bar 函数打印出来的结果就应该是“极客邦”。但实际情况并非如此,如果你试着执行上述代码,你会发现打印出来的结果是“极客时间”。为什么会是这种情况呢?要解释清楚这个问题,那么你就需要先搞清楚作用域链了。

13.什么是做用域链

要是你卡过我之前这篇文章读李老课程引发的思考之JS从栈、堆、预解析来解释闭包原理-|真 · 奥义|,那么你就应该知道什么是惰性解析和预解析。

惰性解析,指的是在解析阶段,并不会解析函数体,预解析是判断内部函数是否引用了外部函数的变量。我们用惰性解析和预解析完美的解释了闭包的形成原理。

而这两个又和作用域链 有什么关系呢?

看上道题,为什么它在寻找变量的时候不是在调用栈上从上往下找呢?而是看定义?

其实,就是预解析的作用。预解析阶段判断内部函数的外部作用域是谁,然后用outer这个引用来指向它

其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量,比如上面那段代码在查找 myName 变量时,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。为了直观理解,你可以看下面这张图:

从图中可以看出,bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链

14.我们来测试一道题

function bar() {
    var myName = "极客世界"
    let test1 = 100
    if (1) {
        let myName = "Chrome浏览器"
        console.log(test)
    }
}
function foo() {
    var myName = "极客邦"
    let test = 2
    {
        let test = 3
        bar()
    }
}
var myName = "极客时间"
let myAge = 10
let test = 1
foo()

你可以自己先分析下这段代码的执行流程,看看能否分析出来执行结果。

要想得出其执行结果,那接下来我们就得站在作用域链和词法环境的角度来分析下其执行过程。在上面我们已经介绍过了,ES6 是支持块级作用域的,当执行到代码块时,如果代码块中有 let 或者 const 声明的变量,那么变量就会存放到该函数的词法环境中。对于上面这段代码,当执行到 bar 函数内部的 if 语句块时,其调用栈的情况如下图所示:

现在是执行到 bar 函数的 if 语块之内,需要打印出来变量 test,那么就需要查找到 test 变量的值,其查找过程我已经在上图中使用序号 1、2、3、4、5 标记出来了。下面我就来解释下这个过程。首先是在 bar 函数的执行上下文中查找,但因为 bar 函数的执行上下文中没有定义 test 变量,所以根据作用域链,下一步就在 bar 函数的外部作用域中查找,也就是全局作用域。

15.JavaScript 中的 this 是什么

希望你能区分清楚作用域链和 this 是两套不同的系统,它们之间基本没太多联系。在前期明确这点,可以避免你在学习 this 的过程中,和作用域产生一些不必要的关联。

在文章前面中,我们提到执行上下文中包含了变量环境、词法环境、外部环境,但其实还有一个 this 没有提及,具体你可以参考下图:

从图中可以看出,this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。

执行上下文主要分为三种——全局执行上下文、函数执行上下文和 eval 执行上下文,所以对应的 this 也只有这三种——全局执行上下文中的 this、函数中的 this 和 eval 中的 this。

关于this指向问题,网上文章很多,什么谁调用指向谁啊,但是谈及为啥谁调用就指向谁的文章少之又少,有几篇文章有谈到从执行上下文来理解this,执行上下文是函数被执行的时候,所需要的环境就创建了,因此this指向就确定了,然仅仅停留在此层面,更深入的就找不到了。

我依旧很多疑问:默认调用函数,和对象调用函数,执行环境就不一样了吗?

他们调用栈,没啥区别,但是没能深入了解执行上下文,例如查看调用栈中的inner的执行环境是否相同,因此很遗憾。实践止步于此,但学习不止于此,希望日后的学习能解开这个谜题。

三面面试官:运行 npm run xxx 的时候发生了什么?


theme: fancy

事情是这样的,直接开讲

面试官:npm run xxx的时候,发生了什么?讲的越详细越好。

我(心想,简单啊): 首先,DNS 解析,将域名解析成 IP 地址,然后
TCP 连接,TCP 三次握手...

面试官:停停,我问的不是从URL输入到页面展现到底发生什么?,是npm run xxx的时候,发生了什么。

我(尴尬,条件反射地以为是问的八股文):emmmm,我记得 npm run xxx的时候,首先会去项目的package.json文件里找scripts 里找对应的xxx,然后执行 xxx的命令,例如启动vue项目 npm run serve的时候,实际上就是执行了vue-cli-service serve 这条命令。(好险,幸好这点常识我还是懂的)

package.json文件

{
  "name": "h5",
  "version": "1.0.7",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve"
   },
}

面试官:嗯,不错,那 为什么 不直接执行vue-cli-service serve而要执行npm run serve 呢?

我(支支吾吾):emm,因为 npm run serve 比较简短,比较好写。

面试官:你再想想。

我(啊?不对吗,对哦,我想起来了): 因为 直接执行vue-cli-service serve,会报错,因为操作系统中没有存在vue-cli-service这一条指令

面试官: 哦,对对对,不错不错,哟西哟西!

我(嘿嘿,稳了,这次我要30k): 嘻嘻!

面试官:那既然vue-cli-service这条指令不存在操作系统中,为什么执行npm run serve的时候,也就是相当于执行了vue-cli-service serve ,为什么这样它就能成功,而且不报指令不存在的错误呢?

我(啊?要不你还是把我鲨了吧,不想再勉强作回答):不好意思,这个我还没了解过。

面试官:emmm,好吧,没关系,我们做下一道算法题吧:....

....

后面无关此次文章的内容,就省略过了。

面试官:好的,此处面试到此结束,我们会在一周内回复您的面试结果

哔哔哔...(电话挂断)

唉。看来是凉了

为什么执行npm run serve的时候,这样它就能成功,而且不报指令不存在的错误呢?

我赶紧问问了大佬朋友这一过程到底是发生了什么

经过一番讨论,终于找到了答案。

不服输的我,赶紧回拨了面试官的电话号码。

我:喂,面试官,您好,我已经找到答案了,可以麻烦您再听一下吗?

面试官:嗯,可以啊,请讲。

我:我们在安装依赖的时候,是通过npm i xxx 来执行的,例如 npm i @vue/cli-service,npm 在 安装这个依赖的时候,就会node_modules/.bin/ 目录中创建 好vue-cli-service 为名的几个可执行文件了。

.bin 目录,这个目录不是任何一个 npm 包。目录下的文件,表示这是一个个软链接,打开文件可以看到文件顶部写着 #!/bin/sh ,表示这是一个脚本。

由此我们可以知道,当使用 npm run serve 执行 vue-cli-service serve 时,虽然没有安装 vue-cli-service的全局命令,但是 npm 会到 ./node_modules/.bin 中找到 vue-cli-service 文件作为 脚本来执行,则相当于执行了 ./node_modules/.bin/vue-cli-service serve(最后的 serve 作为参数传入)。

面试官:可以啊,真不错,但是我还想继续问问,你说.bin 目录下的文件表示软连接,那这个bin目录下的那些软连接文件是哪里来的呢?它又是怎么知道这条软连接是执行哪里的呢?

我(窃喜,这个我们刚刚也讨论了):我们可以直接在新建的vue项目里面搜索vue-cli-service

可以看到,它存在项目最外层的package-lock.json文件中

从 package-lock.json 中可知,当我们npm i 整个新建的vue项目的时候,npm 将 bin/vue-cli-service.js 作为 bin 声明了。

所以在 npm install 时,npm 读到该配置后,就将该文件软链接到 ./node_modules/.bin 目录下,而 npm 还会自动把node_modules/.bin加入$PATH,这样就可以直接作为命令运行依赖程序和开发依赖程序,不用全局安装了。

假如我们在安装包时,使用 npm install -g xxx 来安装,那么会将其中的 bin 文件加入到全局,比如 create-react-app 和 vue-cli ,在全局安装后,就可以直接使用如 vue-cli projectName 这样的命令来创建项目了。

面试官:搜噶,也就是说,npm i 的时候,npm 就帮我们把这种软连接配置好了,其实这种软连接相当于一种映射,执行npm run xxx 的时候,就会到 node_modules/bin中找对应的映射文件,然后再找到相应的js文件来执行。

我(疯狂点头):嗯嗯,是的,就是这样

面试官:我有点好奇。刚刚看到在node_modules/bin中 有三个vue-cli-service文件。为什么会有三个文件呢?

我:如果我们在 cmd 里运行的时候,windows 一般是调用了 vue-cli-service.cmd,这个文件,这是 windows 下的批处理脚本:

@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0

IF EXIST "%dp0%\node.exe" (
  SET "_prog=%dp0%\node.exe"
) ELSE (
  SET "_prog=node"
  SET PATHEXT=%PATHEXT:;.JS;=;%
)

endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%"  "%dp0%\..\@vue\cli-service\bin\vue-cli-service.js" %*

所以当我们运行vue-cli-service serve这条命令的时候,就相当于运行 node_modules/.bin/vue-cli-service.cmd serve

然后这个脚本会使用 node 去运行 vue-cli-service.js这个 js 文件

由于 node 中可以使用一系列系统相关的 api ,所以在这个 js 中可以做很多事情,例如读取并分析运行这条命令的目录下的文件,根据模板生成文件等。

# unix 系默认的可执行文件,必须输入完整文件名
vue-cli-service

# windows cmd 中默认的可执行文件,当我们不添加后缀名时,自动根据 pathext 查找文件
vue-cli-service.cmd

# Windows PowerShell 中可执行文件,可以跨平台
vue-cli-service.ps1

面试官:原来如此,不错嘛小伙子,短短时间内就掌握清楚了,看来学习能力很强,不错不错,我很看好你,我会催hr尽快回复你的。先这样了,拜拜

我(欣喜若狂,功夫不负有心人啊):好啊,好啊,拜拜

哔哔哔...(电话挂断)

过了三十分钟....

今天是个好日子,心想的事儿都能成,今天是个好日子,打开了家门咱迎春风...(手机铃声响起)。

我:喂,您好。

hr:您好,我是xxx公司的hr,根据你面试的优秀表现,恭喜你获得了我司的offer,经过我最大的努力,我给你争取到了最大的薪资,薪资是月薪3500,您看满意吗?

我:....

哔哔哔....(电话挂断)

tmd,c

总结

  1. 运行 npm run xxx的时候,npm 会先在当前目录的 node_modules/.bin 查找要执行的程序,如果找到则运行;
  2. 没有找到则从全局的 node_modules/.bin 中查找,npm i -g xxx就是安装到到全局目录;
  3. 如果全局目录还是没找到,那么就从 path 环境变量中查找有没有其他同名的可执行程序。

我的github

优质文章都在这里,快来

参考文章

https://blog.51cto.com/u_15077533/4531157
https://juejin.cn/post/6971723285138505765

CSS盒模型之内边距、边框、外边距 十九问 (读CSS权威指南)

本篇文章主要探讨盒模型,以及内边距、边框、外边距的面试题与思考,也希望您能把您的思考和遇到的问题以评论的方式补充下,后期,我将会补充到文章中

第一问:什么是盒模型?

可以说,页面就是由一个个盒模型堆砌起来的,每个HTML元素都可以叫做盒模型,盒模型由外而内包括:外边距(margin)、边框(border)、填充(亦称内边距)(padding)、内容(content)。它在页面中所占的实际宽度是margin + border + paddint + content 的宽度相加。

但是,盒模型有标准盒模型和IE的盒模型。

第二问:两者的区别是什么?

我们先来看两张图:

标准的(W3C)盒模型:

image
IE盒模型:

image

第三问:怎么设置这两种模型呢?

很简单,通过设置 box-sizing:content-box(W3C)/border-box(IE)就可以达到自由切换的效果。

第四问JS怎么获取和设置盒模型的宽高呢,你能想到几种方法

  • 第一种:

dom.style.width/height

这种方法只能获取使用内联样式的元素的宽和高。

  • 第二种:

dom.currentStyle.width/height

这种方法获取的是浏览器渲染以后的元素的宽和高,无论是用何种方式引入的css样式都可以,但只有IE浏览器支持这种写法。

  • 第三种:

window.getComputedStyle(dom).width/height

这种方法获取的也是浏览器渲染以后的元素的宽和高,但这种写法兼容性更好一些。

  • 第四种:

dom.getBoundingClientRect().width/height

这种方法经常使用的场所是,计算一个元素的绝对位置(相对于视窗左上角),它能拿到元素的left、top、width、height 4个属性。

第五问:描述一下下面盒子的大小,颜色什么的(content-box模型)

<html>
<style>
  body{
    background-color: gray;
  }
  div{
    color: blue;
    width: 100px;
    background-color: pink;
    border: 10px solid;
    padding: 20px;
  }
</style>
<body>
  <div></div>
</body>
</html>

有一说一,当时被字节面试官问到这个问题,我是挺蒙蔽的,因为他这里不指考了一点,问题列一下:

  1. 整个盒子的大小
  2. padding的颜色
  3. border的颜色
  4. height为0了,看得见盒子吗?

答案:如图所示

image

  1. 整个盒子的大小 = 0 (因为height为0)
  2. padding的颜色 = pink(继承content的颜色)
  3. border的颜色 = blue(继承color字体的颜色,默认为black)
  4. height为0了,看得见盒子吗? (虽然height为0,但是看得见盒子,因为有border和padding)

这里需要注意

  1. 如果没有写border-style,那么边框的宽度不管设置成多少,都是无效的。
  2. border-color的颜色默认跟字体颜色相同
  3. padding颜色跟背景颜色相同

第六问:当small盒子设置成圆形时,内容会超出圆形吗?为什么

<html>
<style>
  body{
    background-color: gray;
  }
  .big{
    width: 400px;
    height: 400px;
    background-color: pink;
  }
  .small{
    width: 100px;
    height: 100px;
    background-color: red;
    border-radius: 50%;
    overflow-wrap: break-word;
  }
</style>
<body>
  <div class="big">
    <div class="small">
      ddddddddddddddddddddddddddddddddddddddddddd
    </div>
  </div>
</body>
</html>

会超出圆形。原因如图所示,是因为border-radius只是改变视觉上的效果,实际上盒子占据的空间还是不变的。

image

第七问:当元素设置成inline-block会出现什么问题?怎么消除?

这是网易有道的小姐姐面试官的问题,我承认我确实不知道这个问题!

真正意义上的inline-block水平呈现的元素间,换行显示或空格分隔的情况下会有间距,很简单的个例子

我们使用CSS更改非inline-block水平元素为inline-block水平,也会有该问题:

.space a {
    display: inline-block;
    padding: .5em 1em;
    background-color: #cad5eb;
}
<div class="space">
    <a href="##">惆怅</a>
    <a href="##">淡定</a>
    <a href="##">热血</a>
</div>

image

去除inline-block元素间间距的N种方法

  1. 元素间留白间距出现的原因就是标签段之间的空格,因此,去掉HTML中的空格,自然间距就木有了。考虑到代码可读性,显然连成一行的写法是不可取的,我们可以:
<div class="space">
    <a href="##">
    惆怅</a><a href="##">
    淡定</a><a href="##">
    热血</a>
</div>

或者是:

<div class="space">
    <a href="##">惆怅</a
    ><a href="##">淡定</a
    ><a href="##">热血</a>
</div>

或者是借助HTML注释:

<div class="space">
    <a href="##">惆怅</a><!--
    --><a href="##">淡定</a><!--
    --><a href="##">热血</a>
</div>
  1. 使用margin负值
  2. 让闭合标签吃胶囊
  3. 使用font-size:0

详细的可以看看这篇文章
去除inline-block元素间间距的N种方法

第八问:行内元素可以设置padding,margin吗?

  • 第一:行内元素与宽度
    宽度不起作用
span {
  width:200px;
}

没有变化

  • 第二:行内元素与高度
    高度不起作用
span{
  height:200px;
}

没用变化

  • 第三:行内元素与padding,margin
span{
  padding:200px;
}

影响左右,不影响上下 ,span包裹的文字左右位置改变,上下位置不变,但背景色会覆盖上面元素的内容。
如图所示:

image

行内元素(inline-block)的padding左右有效 ,但是由于设置padding上下不占页面空间,无法显示效果,所以无效

第九问:padding:1px2px3px;则等效于什么?

对于我们来说,我们经常写的简写是两个值或者一个值或者四个值,而三个值的,会比较少写,所以当时,我确实有点蒙了,因为我对于这个三个值各代表什么是死记硬背的!后来才发现,原来遵循一条非常简单的规则:

简单来说就是 这四个值,分别代表上、右、下、左。如果没有写下的话,那就下复制上的,同理左复制右的值。

因此, 你应该明白了

  1. 当padding的值只有一个时,就是后面三个都复制了第一个
  2. 当写两个时,就是写了top和right,因此bottom复制top,left复制right
  3. 当写了三个时,就是写了top,right,bottom,因此left复制right。

这么简单的规则,再也不会忘记了吧。

第十问:内边距的百分数值是这么计算的

我们知道,padding可以这么设置

padding:100px

也可以

padding:20%

那当是百分比时是怎么计算的呢?

这不是很简单的问题吗?

就是根据父元素的宽度计算的

第十一问:那为什么不根据自己的宽度呢?而要根据父元素

好家伙,我说怎么问那么简单的问题,原来是为了这个问题做铺垫

这个问题可以这么思考,如果不根据父元素,而是根据本身的宽度的话。那么当padding生效后,本身的宽度不就变大了吗?那么padding不是也要变大吗?这就陷入了死循环(哇塞!)。

或者要是本身没有宽度,那岂不是怎么设置padding都是无效的!!!

第十二问:什么是边距重叠?什么情况下会发生边距重叠?如何解决边距重叠?

边距重叠:两个box如果都设置了边距,那么在垂直方向上,两个box的边距会发生重叠,以绝对值大的那个为最终结果显示在页面上。

边距重叠分为两种:

  1. 同级关系的重叠

同级元素在垂直方向上外边距会出现重叠情况,最后外边距的大小取两者绝对值大的那个**

<section class="fat">
  <style type="text/css">
      .fat {
          background-color: #ccc;
      }
      .fat .child-one {
          width: 100px;
          height: 100px;
          margin-bottom: 30px;
          background-color: #f00;
      }

      .fat .child-two {
          width: 100px;
          height: 100px;
          margin-top: 10px;
          background-color: #345890;
      }
  </style>
  <div class="child-one"></div>
  <div class="child-two"></div>
</section>
  1. 父子关系的边距重叠

父子关系,如果子元素设置了外边距,在没有把父元素变成BFC的情况下,父元素也会产生外边距。

给父元素添加 overflow:hidden 这样父元素就变为 BFC,不会随子元素产生外边距,但是父元素的高会变化

<!-- 
。 -->
<section class="box" id="fat">
  <style type="text/css">
      #fat {
          background-color: #f00;
          overflow: hidden;
      }

      #fat .child {
          margin-top: 10px;
          height: 100px;
          background-color: blue;
      }   
  </style>
  <article class="child"></article>
</section>

第十三问:第二种哪里算是外边距重叠???

实际上,这也是第一种的变形。

header {
  background: goldenrod;
}
h1 {
  margin: 1em;
}
<header>
  <h1>Welcome to ConHugeCo</h1>
</header>

image

可以看到其实是header的margin为0,然后h1的margin为1em,因此header和h1的margin发生了重叠,然后header的margin就取1em和0两个值中最大的值了,所以当然取1em啦。

(网上有说是因为margin的传递性,但我并不同意,因为我实践了一下,发现不管父盒子有没有margin-top,父盒子只会选择两者值中的最大值,跟传递性似乎没啥关系)

第十四问:为什么回出现margin重叠的问题?粗俗点就是问设计者的脑子有问题吗?

这个就是问设计了margin重叠的巧妙用处。

这个曾经有位面试官问过我,我不知道,我请教了他,他说,在flex布局前,要实justify-content: space-evenly的效果,利用浮动布局,然后给每个盒子设置margin-right,margin-left就可以实现了,这样就不用去单独地再去设置第一个盒子的margin-left和最后一个盒子的margin-right,那时候我信了。

后来越想越不对,不是说margin水平方向不会发生重叠问题吗????

但是根据面试官的思路来的话,在垂直方向似乎就讲的通了。

<html>
<style>
  body{
    background-color: gray;
  }
  ul{
    width: 300px;
    height: 170px;
    background-color: blue;
    border: 1px solid;
  }
  li{
    margin-top: 20px;
    margin-bottom: 20px;
    width: 40px;
    height: 30px;
    background-color: orange;
  }
</style>
<body>
  <ul>
    <li></li>
    <li></li>
    <li></li>
  </ul>
</body>
</html>

image

或许你有更好的说法,欢迎下方留言评论补充!!!

那该怎么解决margin边距重叠的问题呢?

解决方法就是生成BFC

第十五问:什么是BFC?

BFC的基本概念–BFC就是“块级格式化上下文”的意思,也有译作“块级格式化范围”。它是 W3C CSS 2.1 规范中的一个概念,它决定了元素如何对其内容进行定位,以及与其他元素的关系和相互作用。通俗的讲,就是一个特殊的块,内部有自己的布局方式,不受外边元素的影响。

第十六问:那么BFC的原理是什么呢?

  1. 内部的Box会在垂直方向上一个接一个的放置
  2. 垂直方向上的距离由margin决定。(完整的说法是:属于同一个BFC的两个相邻Box的margin会发生重叠(塌陷),与方向无关。)
  3. 每个元素的左外边距与包含块的左边界相接触(从左向右),即使浮动元素也是如此。(这说明BFC中子元素不会超出他的包含块,而position为absolute的元素可以超出他的包含块边界)
  4. BFC的区域不会与float的元素区域重叠
  5. 计算BFC的高度时,浮动子元素也参与计算
  6. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面元素,反之亦然

第十七问:BFC由什么条件创立?

  1. float属性不为none
  2. position属性为absolute或fixed
  3. display属性为inline-block、table-cell、table-caption、flex、inline-flex
  4. overflow属性不为visible(- overflow: auto/ hidden;)

总结:pdfo

第十八问: BFC的使用场景有哪些呢

  1. 可以用来自适应布局。
<!-- BFC不与float重叠 -->
    <section id="layout">
        <style media="screen">
          #layout{
            background: red;
          }
          #layout .left{
            float: left;
            width: 100px;
            height: 100px;
            background: #664664;
          }
          #layout .right{
            height: 110px;
            background: #ccc;
            overflow: auto;
          }
        </style>
        <div class="left"></div>
        <div class="right"></div>
        <!-- 利用BFC的这一个原理就可以实现两栏布局,左边定宽,右边自适应。不会相互影响,哪怕高度不相等。 -->
    </section>
  1. 可以清除浮动:(塌陷问题)
<!-- BFC子元素即使是float也会参与计算 -->
<section id="float">
    <style media="screen">
      #float{
        background: #434343;
        overflow: auto;
      }
      #float .float{
        float: left;
        font-size: 30px;
      }
    </style>
    <div class="float">我是浮动元素</div>
</section>
  1. 解决垂直边距重叠:
<section id="margin">
    <style>
        #margin{
            background: pink;
            overflow: hidden;
        }
        #margin>p{
            margin: 5px auto 25px;
            background: red;
        }
        #margin>div>p {
            margin: 5px auto 20px;
            background: red;
        }
    </style>
    <p>1</p>
    <div style="overflow:hidden">
        <p>2</p>
    </div>
    <p>3</p>
    <!-- 这样就会出现第一个p标签的margin-bottom不会和第二个p标签的margin-top重叠,这也是BFC元素的另一个原则,不会影响到外边的box,是一个独立的区域。 -->
</section>

第十九问:清除浮动的方法(最常用的4种)

这时候很多人会想到新建标签clear:both和float 方法,但是这两种方法并不推荐使用!

什么是clear:both

clear:both:本质就是闭合浮动, 就是让父盒子闭合出口和入口,不让子盒子出来

  1. 额外标签法(在最后一个浮动标签后,新加一个标签,给其设置clear:both;)(不推荐)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
    .fahter{
        width: 400px;
        border: 1px solid deeppink;
    }
    .big{
        width: 200px;
        height: 200px;
        background: darkorange;
        float: left;
    }
    .small{
        width: 120px;
        height: 120px;
        background: darkmagenta;
        float: left;
    }
    .footer{
        width: 900px;
        height: 100px;
        background: darkslateblue;
    }
    .clear{
        clear:both;
    }
    </style>
</head>
<body>
    <div class="fahter">
        <div class="big">big</div>
        <div class="small">small</div>
        <div class="clear">额外标签法</div>
    </div>
    <div class="footer"></div>
</body>
</html>

image
如果我们清除了浮动,父元素自动检测子盒子最高的高度,然后与其同高。

优点:通俗易懂,方便

缺点:添加无意义标签,语义化差

不建议使用。

  1. 父级添加overflow属性(父元素添加overflow:hidden)(不推荐)

通过触发BFC方式,实现清除浮动

.fahter{
    width: 400px;
    border: 1px solid deeppink;
    overflow: hidden;
}

优点:代码简洁

缺点:内容增多的时候容易造成不会自动换行导致内容被隐藏掉,无法显示要溢出的元素

不推荐使用

  1. 使用after伪元素清除浮动(推荐使用)
.clearfix:after{/*伪元素是行内元素 正常浏览器清除浮动方法*/
    content: "";
    display: block;
    height: 0;
    clear:both;
    visibility: hidden;
}
.clearfix{
    *zoom: 1;/*ie6清除浮动的方式 *号只有IE6-IE7执行,其他浏览器不执行*/
}
 
<body>
    <div class="fahter clearfix">
        <div class="big">big</div>
        <div class="small">small</div>
        <!--<div class="clear">额外标签法</div>-->
    </div>
    <div class="footer"></div>
</body>

优点:符合闭合浮动**,结构语义化正确

缺点:ie6-7不支持伪元素:after,使用zoom:1触发hasLayout.

推荐使用

  1. 使用before和after双伪元素清除浮动
 .clearfix:after,.clearfix:before{
    content: "";
    display: table;
}
.clearfix:after{
    clear: both;
}
.clearfix{
    *zoom: 1;
}
 
 <div class="fahter clearfix">
        <div class="big">big</div>
        <div class="small">small</div>
    </div>

 <div class="footer"></div>

优点:代码更简洁

缺点:用zoom:1触发hasLayout.

推荐使用

  1. 浮动父元素
img{
  width:50px;
  border:1px solid #8e8e8e;
  float:left;
}
<div style="float:left">
  <img src="images/search.jpg"/>
  <img src="images/tel.jpg"/>
  <img src="images/weixin.png"/>
  <img src="images/nav_left.jpg"/>
</div>

这种方式也不推荐,了解即可。

如果有不对的地方欢迎留言交流与补充
也希望您能把您的思考和遇到的问题以评论的方式补充下,后期,我将会补充到文章中

快来学习下webhook吧!超级简单易学

1. Webhook是啥?

简单而言,webhook就是一个监听的钩子,监听你push你的代码到github仓库之后,发起一个请求。这个请求要请求哪里交给你设置要。

主要流程

  1. git push xxx 本地代码提交至远程github仓库
  2. github仓库收到push后进行回调,发post( Payload url 是来自webhooks的配置)请求
  3. 基于 Payload url 的服务根据传回来的信息进行提取,拉取最新代码并重新构建项目
  4. 可以看到,我们只需把代码提交到github仓库即可,不用再上服务器进行一些列的操作了

2. webhook有什么作用呢?

  1. 目前我发现的我们公司主要是用于 监听到开发者 push代码到仓库之后就 发送消息到企业微信群里。如下图所示:

  1. 完成自动化部署

3.配置webhook超简单

来到你的仓库,点击setting

然后选中左边的webhook,就可以配置了

4.主要配置四部分:

Payload URL 回调服务的地址,就是你想请求的一个接口的地址,请求方式是POST;

Content type 回调请求头,建议JSON格式;

Secret 为了做安全校验,设置后会在请求 header 中增加如下两个属性,用来区分请求的来源,避免暴露的请求被恶意访问;

X-Hub-Signature: ...
X-Hub-Signature-256:...

最后我们选择由哪些事件来触发webhook回调,push event(代码推送事件)、everything(所有事件)、某些特定事件三种。

配置完成后,尝试提交代码下,然后从Recent Deliveries中你会发现有调用webhook的记录。

5.实现自动化部署

我们用node简单地搭建一个服务器

下面讲讲在服务器上我们是怎么接收Gitlab的请求并且执行部署的--

const exec = require('child_process').exec
const express = require('express')
const app = express()

let isLocking = false

app.post('/deploy', function (req, res) {
    let headers = req.headers
    let cmdStr = 'cd ... && git fetch origin && ...'
    if (!isLocking && headers['x-gitlab-token'] === 'xxx') {
        isLocking = true
        exec(cmdStr, function (err, stdout, stderr) {
            if (err) {
                // ...
                console.log('error:' + stderr);
            } else {
                // ...
                console.log(stdout)
                isLocking = false
            }
        })
    }
    // ......
})

app.listen(1234, '0.0.0.0', function () {
    console.log(`listening on port 1234`)
})

可以看到,当我们配置webhook的请求URL是我们的deploy接口时,当webhook监听到我们push代码之后,就会请求deploy接口,然后执行接口里面的逻辑,然后自动化部署就是我们在接口里写好的cmdStr字符串了。

6.实现企业微信机器人报告

企业微信的配置其实更简单,我们先创建一个群组,在群组右键有个添加机器人选项,添加成功后会生成webhook地址。我们只要向这个地址发送POST请求,群组内就会收到推送消息。

消息内容支持文本(text)、markdown(markdown)、图片(image)、图文(news)四种消息类型,而且还支持在群内@群成员,下边以文本格式做示范。

   curl 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=145a516a-dd15-421f-97a3-ba3bf1479369' \
   -H 'Content-Type: application/json' \
   -d '
   {
        "msgtype": "text",
        "text": {
            "content": "你好,我是程序员内点事"
        }
   }'

直接请求 url 发现消息推送成功,说明配置的没问题。

所以我们可以这样实现:

const exec = require('child_process').exec
const express = require('express')
const app = express()

let isLocking = false

app.post('/deploy', function (req, res) {

  try {
     const content = JSON.parse(req.body.payload) ;
     const name = content.pusher.name;
     const message = content.before;
     
     axios.post('https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=145a516a-dd15-421f-97a3-ba3bf1479369',data: {
      "msgtype": "text",
        "text": {
            "content": "你好,我是程序员内点事"
        }
     })
  } catch (error) {
    console.log(error);
  }
})

app.listen(1234, '0.0.0.0', function () {
    console.log(`listening on port 1234`)
})

看,基本就是这样实现啦,够简单吧,(代码只是写个大概,并不完整)。

快去给你的仓库配置下webhook吧。

参考文章

https://juejin.cn/post/6844903780740251662

https://juejin.cn/post/6844903583280791566

https://juejin.cn/post/6969016419564388366

https://juejin.cn/post/7024357272461672478

将这段async/await代码翻译成Promise

如图,这道题,我轻而易举地说出了答案是2,3。原理的话我知道是封装成Promise,但要我翻译成Promise我还是很懵逼啊。 不得不学下怎么翻译。

不得不说,感谢这道题,让我进一步地深入了解async/await,感谢面试官

async/await 的基础使用及原理简介

async/await是es7推出的一套关于异步的终极解决方案,为什么要说他是终极解决方案呢?因为他实在是太好用了,而且写起来还非常的简单。

一:async/await基础语法

// 定义一个异步函数(假设他是一个异步函数)
getJSON(){
    return 'JSON'
}

// 在需要使用上面异步函数的函数前面,加上async声明,声明这是一个异步函数
async testAsync() {
  // 在异步函数前面加上await,函数执行就会等待用await声明的异步函数执行完毕之后,在往下执行
  await getJSON()
  
  ...剩下的代码
}

以上就是async/await最基本的用法。

还需要注意的一点就是使用async/await的时候,是无法捕获错误的,这个时候就要用到我们es5里面一个被大家遗忘了的try/catch,来进行错误的捕获:

async testAsync() {
  try {
     await getJSON()
  } catch(err) {
     console.log(err)
  }
  ...剩下的代码
}
注意:

1.async函数在声明形式上和普通函数没有区别,函数声明式,函数表达式,对象方法,class方法和箭头函数等都可以声明async函数。

2.任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

3.async函数返回的 Promise 对象必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

二:async/await

async这个单词大家应该都比较熟悉,他是英文单词‘异步’的简写,代表的意思也是异步。

async function testAsync() {
    return "hello async";
}

const result = testAsync();
console.log(result);

输出结果:

Promise{<resolved>: "hello async"}

可以看出async函数,返回的是一个Promise对象

await是英文单词‘等待’的意思,代表的意思也是等待,那他等的到底是个什么东西呢?还是一个Promise。

三、async/await和Promise直接的转化

async/await其实是基于Promise的。async函数其实是把Promise包装了一下。

下面是一个async/await的写法:

getConstant() {
   return 1
 }

 async getAsyncConstant() { 
  return 1
 }

 async getPromise() {
  return new Promise((resolved, rejected)=> {
    resolved(1)
  });
 }

 async test() {
  let a = 2
  let c = 1
  await getConstant();
  let d = 3
  await getPromise();
  let d = 4
  await getAsyncConstant();
  return 2
 }

上面的代码其实真正的在解析执行的时候是这样的:

function getConstant() {
   return 1;
}

function getAsyncConstant() {
  return Promise.resolve().then(function () {
   return 1;
  });
}

function getPromise() {
  return Promise.resolve().then(function () {
   return new Promise((resolved, rejected) => {
    resolved(1);
   });
  });
}

  test() {
    return Promise.resolve().then(function () {
       let a = 2;
       let c = 1;
       return getConstant();
     }).then(function () {
       let d = 3;
       return getPromise();
     }).then(function () {
       let d = 4;
       return getAsyncConstant();
     }).then(function () {
       return 2;
     });
 }

通过上面的代码可以看出async/await的本身还是基于Promise的。

因为await本身返回的也是一个Promise,它只是把await后面的代码放到了await返回的Promise的.then后面,以此来实现的。

因此回答题目

     function getJson(){
      return new Promise((reslove,reject) => {
        setTimeout(function(){
          console.log(2)
          reslove(2)
        },2000)
      })
     }
    async function testAsync() {
       await getJson()
       console.log(3)
    }

    testAsync()

自己封装一下,变成啥样?

function getJson(){
    return new Promise((resolve,rej)=>{
        setTimeout(function(){
            console.log(2)
            resolve(2)
          },2000)
    })
 }
function testAsync() {
    return Promise.resolve().then(()=>{
        return getJson()
    }).then(()=>{
        console.log(3)
    })
}

testAsync()

你封装对了吗?

多疑的你可能就要问了。要是我把getJson函数体题改成不是返回Promise呢?如下:

function getJson(){
  setTimeout(function(){
     console.log(2)
  },2000)
 }
  async function testAsync() {
       await getJson()
       console.log(3)
    }
  testAsync()

testAsync()

这样的话输出就是3,2了,但是我们刚刚的封装依旧没问题,如下所示

function getJson(){
    setTimeout(function(){
       console.log(2)
    },2000)
   }
  function testAsync() {
      return Promise.resolve().then(()=>{
          return getJson()
      }).then(()=>{
          console.log(3)
      })
  }
  
  testAsync()

因为return getJson()是在then里执行的,所以会返回promise。所以await等待的依旧是一个promise对象。

有点意思,有点意思。。。

但是还有点问题 ,用await的时候,我们平时都是用来接收异步执行后返回的数据啊,例如

// 这个是模拟简单的用Promise封装ajax
function getJson(){
    return new Promise((resolve,reject)=>{
        setTimeout(function(){
            resolve(99999)
        },3000)
    })
}
    async function testAsync() {
         let data = await getJson()
         console.log(1)
         console.log(data)
      }
  
  testAsync()

那,怎翻译过来呢??且看,

function getJson(){
    return new Promise((resolve,reject)=>{
        setTimeout(function(){
            resolve(99999)
        },3000)
    })
}
  function testAsync() {
        return Promise.resolve().then(()=>{
            return getJson()
        }).then((res)=>{
            let data = res
            console.log(1)
            console.log(data)
        })
         
      }
  
  testAsync()

是不是感觉so easy

关于【原型与继承】十问 (读《红宝书》)

第一问:你知道new操作符的实现原理吗?描述下

通过new创建对象经历4个步骤:

  • 1、创建一个新对象
  • 2、将构造函数的作用域赋给新对象(因此this指向了这个新对象)
  • 3、执行构造函数中的代码(为这个新对象添加属性);
  • 4、返回新对象。
function newFunc (name) {
    var o = {};
    o.__proto__ = Person.prototype;//绑定Person的原型
    Person.call(o, name);
    return o;
}

第二问:请问下面代码输出什么?

function Person(){}
var p1 = new Person()

console.log(p1.constructor)
Person.prototype = {
    name:"小红"
}
var p2 = new Person()
console.log(p2.constructor)

答案:

function Person(){}
var p1 = new Person()

console.log(p1.constructor) // [Function: Person]
Person.prototype = {
    name:"小红"
}
var p2 = new Person()
console.log(p2.constructor)  [Function: Object]

第三问:为什么输出的两个constructor不相同

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则,为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性指向了prototype所在的函数。
上面的体题中

打印p1.constructor时,p1没有constructor属性,于是就往原型链查找,就在Person.prototype上找到了有constructor,而这个constructor指向构造函数Person,因此,打印出[Function: Person]

而到p2时,Person.prototype的值已经被修改成{
name:"小红"
},发现这个值里已经没有constructor,但是prototype上必须有constructor,所以它就自己创建了constructor,并且默认指向Object.,所以打印[Function: Object]

第四问:如果我修改prototype时,仍想constructor依旧指向Person,该怎么做呢?

如果想constructor依旧指向Person,可以在修改prototype的时候,添加上construtor属性

function Person(){}
var p1 = new Person()
console.log(p1.constructor) //[Function: Person]
Person.prototype = {
    constructor: Person,
    name:"小红"
}
var p2 = new Person()
console.log(p2.constructor) //[Function: Person]

但是这种添加constructor属性,会导致它的[[Enumerable]]特性的值被设置为true,所以可以用下面这种方式

function Person(){}
var p1 = new Person()
console.log(p1.constructor)  //[Function: Person]
Person.prototype = {
    name:"小红"
}
Object.defineProperty(Person.prototype,"constructor",{
    enumerable: false,
    value: Person
})

var p2 = new Person()
console.log(p2.constructor)  //[Function: Person]

第五问:你能介绍下原型链继承吗?

// 实现原型链的一种基本模式
function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}

// 继承,用 SuperType 类型的一个实例来重写 SubType 类型的原型对象
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
     return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue());     // true

其中,SubType 继承了 SuperType,而继承是通过创建 SuperType 的实例,并将该实例赋值给 SubType 的原型实现的。

实现的本质是重写子类型的原型对象,代之以一个新类型的实例。子类型的新原型对象中有一个内部属性 Prototype 指向了 SuperType 的原型,还有一个从 SuperType 原型中继承过来的属性 constructor 指向了 SuperType 构造函数。

最终的原型链是这样的:instance 指向 SubType 的原型,SubType 的原型又指向 SuperType 的原型,SuperType 的原型又指向 Object 的原型(所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype)
原型链继承的缺点:

1、在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性,并且会被所有的实例共享。这样理解:在超类型构造函数中定义的引用类型值的实例属性,会在子类型原型上变成原型属性被所有子类型实例所共享
2、在创建子类型的实例时,不能向超类型的构造函数中传递参数

第六问:既然原型链继承有以上缺点,那有没有解决方法呢?

有,就是借用构造函数继承

借用构造函数继承(也称伪造对象或经典继承)

// 在子类型构造函数的内部调用超类型构造函数;使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上
function SuperType(){
    // 定义引用类型值属性
    this.colors = ["red","green","blue"];
}
function SubType(){
    // 继承 SuperType,在这里还可以给超类型构造函数传参
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("purple");
alert(instance1.colors);     // "red,green,blue,purple"

var instance2 = new SubType();
alert(instance2.colors);     // "red,green,blue"

通过使用 apply() 或 call() 方法,我们实际上是在将要创建的 SubType 实例的环境下调用了 SuperType 构造函数。这样一来,就会在新 SubType 对象上执行 SuperType() 函数中定义的所有对象初始化代码。结果 SubType 的每个实例就都会具有自己的 colors 属性的副本了

借用构造函数的优点是解决了原型链实现继承存在的两个问题。
但是一波已平,一波又起

借用构造函数的缺点是方法都在构造函数中定义,因此函数复用就无法实现了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

第六问:既然原型链继承和借用构造函数都有缺点,那该怎么办?

既然两种方法都没有对方的缺点,那就可以把两者方法结合起来,就解决了,这种方法叫做组合继承

组合继承(也称伪经典继承)

将原型链和借用构造函数的技术组合到一块。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性。

function SuperType(name){
    this.name = name;
    this.colors = ["red","green","blue"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name,age){
    // 借用构造函数方式继承属性
    SuperType.call(this,name);
    this.age = age;
}
// 原型链方式继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};
var instance1 = new SubType("luochen",22);
instance1.colors.push("purple");
alert(instance1.colors);      // "red,green,blue,purple"
instance1.sayName();
instance1.sayAge();

var instance2 = new SubType("tom",34);
alert(instance2.colors);      // "red,green,blue"
instance2.sayName();
instance2.sayAge();

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 javascript 中最常用的继承模式。而且,使用 instanceof 操作符和 isPrototype() 方法也能够用于识别基于组合继承创建的对象。

但它也有自己的不足 -- 无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

第七问:怎么还有缺点,再介绍下其他的继承方法?

原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
自定义一个函数来实现原型式继承

function object(o){
            function F(){}
            F.prototype = o;
            return new F();
}

在 object() 函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。实质上,object() 对传入其中的对象执行了一次浅复制。
其实这个方法就是Object.create的简单实现

直接用Object.create实现原型式继承

这个方法接收两个参数:一是用作新对象原型的对象和一个为新对象定义额外属性的对象。在传入一个参数的情况下,此方法与 object() 方法作用一致。在传入第二个参数的情况下,指定的任何属性都会覆盖原型对象上的同名属性。

var person = {
            name: "luochen",
            colors: ["red","green","blue"]
};
var anotherPerson1 = Object.create(person,{
            name: {
                    value: "tom"
            }
});
var anotherPerson2 = Object.create(person,{
            name: {
                    value: "jerry"
            }
});
anotherPerson1.colors.push("purple");
alert(anotherPerson1.name);     // "tom"
alert(anotherPerson2.name);     // "jerry"
alert(anotherPerson1.colors);    // "red,green,blue,purple"
alert(anotherPerson2.colors);    // "red,green,bule,purple";

只是想让一个对象与另一个对象类似的情况下,原型式继承是完全可以胜任的。但是缺点是:包含引用类型值的属性始终都会共享相应的值,这也是原型链继承的缺点

寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回这个对象

function createPerson(original){
    var clone = Object.create(original);   // 通过 Object.create() 函数创建一个新对象
    clone.sayGood = function(){   // 增强这个对象
         alert("hello world!!!");
    };
    return clone; // 返回这个对象
}

这个方式跟工厂模式生产对象很类似。在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。此模式的缺点是做不到函数复用

寄生组合式继承

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型

function SuperType(name){
}

function SubType(name,age){
    SuperType.call(this,name);
    this.age = age;
}
// 创建超类型原型的一个副本
var anotherPrototype = Object.create(SuperType.prototype);
// 重设因重写原型而失去的默认的 constructor 属性
anotherPrototype.constructor = SubType;
// 将新创建的对象赋值给子类型的原型
SubType.prototype = anotherPrototype;

这个例子的高效率体现在它只调用一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要,多余的属性。与此同时,原型链还能保持不变;因此还能够正常使用 instance 操作符和 isPrototype() 方法

手写compose核心原理,再也不怕面试官问我compose原理


[toc]

前言:为什么要学习这个方法

遇到这个方法主要是最近在阅读redux,koa 原理 等多次遇到这个方法,为了更好地理解框架原理,于是深入学习了一下compose的实现。

然后也发现这属于函数式编程的东西,发现函数式编程是进击前端进阶的必经之路,因为像其中的纯函数的概念在redux的reducer中也展示得淋漓尽致​,而保留函数计算结果的**无论是在vue,还是react等其他框架也多处见到。

所以建议​有时间可以去看下函数试编程。

接下来,就让我们学习下其中的compose函数吧​!

compose简介

compose就是执行一系列的任务(函数),比如有以下任务队列

let tasks = [step1, step2, step3, step4]

每一个step都是一个步骤,按照步骤一步一步的执行到结尾,这就是一个compose

compose在函数式编程中是一个很重要的工具函数,在这里实现的compose有三点说明

  • 第一个函数是多元的(接受多个参数),后面的函数都是单元的(接受一个参数)
  • 执行顺序的自右向左的
  • 所有函数的执行都是同步的

还是用一个例子来说,比如有以下几个函数

let init = (...args) => args.reduce((ele1, ele2) => ele1 + ele2, 0)
let step2 = (val) => val + 2
let step3 = (val) => val + 3
let step4 = (val) => val + 4

这几个函数组成一个任务队列

steps = [step4, step3, step2, init]

使用compose组合这个队列并执行

let composeFunc = compose(...steps)

console.log(composeFunc(1, 2, 3))

执行过程

6 -> 6 + 2 = 8 -> 8 + 3 = 11 -> 11 + 4 = 15

所以流程就是从init自右到左依次执行,下一个任务的参数是上一个任务的返回结果,并且任务都是同步的,这样就能保证任务可以按照有序的方向和有序的时间执行。

compose的实现

好了,我们现在已经知道compose是什么东西了,现在就来实现它吧!

最容易理解的实现方式

思路就是使用递归的过程**,不断的检测队列中是否还有任务,如果有任务就执行,并把执行结果往后传递,这里是一个局部的思维,无法预知任务何时结束。直观上最容易理解。

const compose = function(...funcs) {
  let length = funcs.length
  let count = length - 1
  let result
  return function f1 (...arg1) {
    result = funcs[count].apply(this, arg1)
    if (count <= 0) {
      count = length - 1
      return result
    }
    count--
    return f1.call(null, result)
  }
}

删繁就简来看下,去掉args1参数

const compose = function(...funcs) {
  let length = funcs.length
  let count = length - 1
  let result
  return function f1 () {
    result = funcs[count]()
    if (count <= 0) {
      count = length - 1
      return result
    }
    count--
    return f1(result)
  }
}

这就好看很多,我们假设有三个方法,aa,bb,cc

 function aa() {
    console.log(11);
}

function bb() {
    console.log(22);
}
function cc() {
    console.log(33);
    return 33
}

然后传入compose

compose(aa,bb,cc)

此时count = 2,则下面其实是执行cc

result = funcs[count]()

然后count--。再递归执行f1,则下面其实就是执行bb

result = funcs[count]()

这样,就实现了 从funcs数组里从右往左依次拿方法出来调用,再把返回值传递给下一个。

后面的步骤同理​。​

这其实是一种面向过程的**

手写javascript中reduce方法

为什么要手写?其实你要是能够很熟练的使用reduce,我觉得不必手写reduce,只是我觉得熟悉一下reduce内部的实现可以更好地理解后面的内容,况且 也不会太难呀!

 function reduce(arr, cb, initialValue){
        var num = initValue == undefined? num = arr[0]: initValue;
        var i = initValue == undefined? 1: 0
        for (i; i< arr.length; i++){
            num = cb(num,arr[i],i)
        }'
        return num
    }

如代码所示,就是先判断有没有传入初始值,有的话,下面的循环直接 从i = 0开始,否则i=1开始。

如果没有传入初始值,num就取 数组的第一个元素。这也是说明了为什么传入初始值,i就=1,因为第一个都被取出来了,就不能再取一次啦啦啦!

下面使用我们写的reduce方法

 function fn(result, currentValue, index){
        return result + currentValue
    }
    
    var arr = [2,3,4,5]
    var b = reduce(arr, fn,10) 
    var c = reduce(arr, fn)
    console.log(b)   // 24

好了 ,没毛病,既然我们了解了reduce原理,就看看下面的redux中compose的实现

redux中compose的实现

function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg
    }

    if (funcs.length === 1) {
        return funcs[0]
    }
    debugger
    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

很简短,非常的巧妙,但是不是很不好理解。不过没关系。

依旧通过例子来讲解。

function aa() {
    console.log(11);
}

function bb() {
    console.log(22);
}
function cc() {
    console.log(33);
}

假设只有这三个方法,我们怎样才能先执行cc再执行bb,再执行aa呢?
没错,可以直接写

aa(bb(cc()))

就是这样,非常巧妙,不仅完成了执行顺序,还实现了前一个方法执行返回的结果传递给了下一个即将要执行的方法。

而下面这段代码所做的就是将funcs数组[aa,bb,cc],转化成aa(bb(cc()))

funcs.reduce((a, b) => (...args) => a(b(...args)))

怎么办到的?

看看下面的解释:

reduce内部第一次执行返回的结果是 一个方法

(...args) => aa(bb(...args))

我们现在把这个方法简化成dd,即

dd = (...args) => aa(bb(...args))

reduce内部第二次执行的时候,此时的a 是 上一次返回的dd方法,b是cc

所以执行结果是

(...args) => dd(cc(...args))

dd(cc(...args))不就是先执行cc再执行dd吗?而dd就是执行bb再执行aa。

我的天,!这不是俄罗斯套娃吗!没错 redux中的compose的实现原理就是套娃哈哈哈!

参考文章

https://segmentfault.com/a/1190000011447164

最后

文章首发于公众号《前端阳光》,欢迎加入技术交流群。

产品经理:阳光你来实现一下分享出去的URL链接预览图片、标题和描述。

产品经理:阳光啊,给项目添加个端外分享的功能吧!

我:分享一条url链接到其他app平台吗?

产品经理:我看竞品也有这个功能,它们添加了端外分享之后,平台的用户量暴增,是怎么回事呢?研究发现因为分享的url链接到其他平台之后会出现预览图和标题等,我觉得挺好的,我们把它给抄过来吧!

即分享一条url链接到其他app平台。例如飞书上、Twitter、Facebook等平台上。我分享的链接会出现图片,标题,描述等内容。如下所示

你想啊,要是分享的链接展示的图片是一张引人注目的图片,比如黑丝,这不得吸引多少用户点击进来啊。相比一条冷冰冰的url,就像下面这样,谁知道分享的是什么东西,丝毫没有点击的欲望好不好。

sigoyi~~

于是开始一天的捣鼓~~

怎么让链接上有图片,描述,和标题的?

在分享出去的html页面上加上这一段代码,content的值写上你想要展示的内容

<meta property="og:title" content="The Rock" />
<meta property="og:type" content="video.movie" />
<meta property="og:url" content="https://www.imdb.com/title/tt0117500/" />
<meta property="og:image" content="https://ia.media-imdb.com/images/rock.jpg" />

为什么加上这段代码就可以实现分享链接的预览了呢?

 像飞书、企业微信、WhatsApp、Twitter、Facebook 等社交软件,都会根据链接去抓取你给定 URL 的内容,以确定要包含哪些属性来进行共享展示。

而抓取的数据就是我们写的og:tags 来显式定义的属性。

og是什么东东?

Open Graph Protocol(开放图谱协议),简称 OG 协议。它是 Facebook公布的一种网页元信息(Meta Information)标记协议,属于 Meta Tag (Meta 标签)的范畴,是一种为社交分享而生的 Meta 标签用于标准化网页中元数据的使用,使得社交媒体得以以丰富的“图形”对象来表示共享的页面内容

后面其他社交app也纷纷效仿!所以其他app也可以实现这种功能。

当然,推特也在基础之上做了拓展,所以我们需要添加下推特拓展的meta头,让链接预览展示地更加好看

<meta property="twitter:title" content="The Rock" />
<meta property="twitter:type" content="video.movie" />
<meta property="twitter:url" content="https://www.imdb.com/title/tt0117500/" />
<meta property="twitter:image" content="https://ia.media-imdb.com/images/rock.jpg" />

soga,可这仅仅使得该链接的点击率变高了,用户量变高是从何而来的。

因为在分享出去的页面中会有个引导用户点击安装app的按钮。

用户点击该按钮后,会自动判断用户手机里是否已经安装了该app,如果已经安装app,就会打开app,并且自动在app里调起我们指向的页面。

如果用户没有安装app,那么就会自动引导用户到应用商店里去安装app。当用户安装app之后并且登录之后,就会自动调起我们指向的页面

5. 怎么实现这个自动判断并跳转的行为的?

这里用到的是appsflyer提供的onelink

Vue.prototype.$goToOpenApp = function (event) {
  var weburl = window.location.href
  var target = `bigolive://web?url=${encodeURIComponent(weburl)}`;
  var url = `https://bingobingo.onelink.me/115925328?pid=ActivityTemplate&is_retargeting=true&af_dp=${encodeURIComponent(target)}`;

  window.open(url, '_blank');
}

appsflyer是一家源于以色列,提供数据归因统计的服务商,因为与多家平台(包括facebook)有合作关系,所以做境外投放尤其是facebook投放时需要监控下载活跃时会用到他家的服务。这里说的onelink就是经过他们加封装后的deeplink。

6. 什么是OneLink

那么,到底什么是OneLink™呢?

简单来说OneLink可以通过单一链接,自动识别设备系统(安卓/iOS)完成跳转,将用户导向Google Play,App Store,Windows Phone Store,或任意指定的落地页URL。同时,由于与深度链接(deeplink)和延迟深度链接(deferred deeplink)的深度整合,OneLink还可以轻松实现将新老用户导入特定的推广页面,大大提升广告效果。

7. 深度链接和延迟深度链接是什么?

深度链接的表现:当用户安装了app时,点击分享页的打开app按钮,这时会直接打开app,并跳转到对应的调起页面

延迟深度链接的表现 :当用户未安装app时,点击分享页的打开app按钮,OneLink会迅速识别用户设备类型,并将用户带到正确的应用商店。接下来延迟深度链接发挥作用,当新用户下载应用后,AppsFlyer会向设备实时传递包括相关的归因信息,以便App首次打开时自动显示与Campaign信息对应的页面。

顾名思义,延迟深度链接 就是延迟到用户安装完app之后再发挥深度链接的作用。

URL Scheme是实现深度链接Deeplink兼容性最高、也最简单的一项方法,原生App可以先向操作系统注册一个URL,其中Scheme的作用是从不同平台唤醒相应App

URL Scheme的协议样式如下:

Scheme://host:port/path?query

● Scheme:代表Scheme协议名称,可自定义

● host:代表Scheme作用的地址域

● port:代表该路径的端口号

● path:代表的是想要跳转的指定页面(路径)

● query:代表想要传递的参数

8. 既然OneLink这么腻害,如何快速配置并生成一条OneLink呢?

关于配置相关,可以参考这篇文章

https://blog.csdn.net/lizhong2008/article/details/117705767

我们重点关注怎么将onelink引入代码中使用。(一般来说,onelink的配置不是开发来配的)。使用如下所示:

Vue.prototype.$goToOpenApp = function (event) {
  var weburl = window.location.href
  var target = `bigolive://web?url=${encodeURIComponent(weburl)}`;
  var url = `https://bingobingo.onelink.me/115925328?pid=ActivityTemplate&is_retargeting=true&af_dp=${encodeURIComponent(target)}`;

  window.open(url, '_blank');
}

这个就是我们配置好的onelink的

https://bingobingo.onelink.me/115925328?pid=ActivityTemplate&is_retargeting=true&af_dp=${encodeURIComponent(target)}

点击这条链接的 用户将打开app或者去到app商店,然后打开我们写好的target页面,实际上,这里我们写的target就是一个deeplink。

解释一下onelink相关参数:

  1. 1168916328,是每一个 OneLink 都有的自己唯一 OneLink ID
  2. pid和is_retargeting 题提供给运营在appsflyer查看相关数据统计报表的参数
  3. af_dp:把用户深度链接到某应用内活动的路径。

除了这几个,其实还有其他参数:

其中af_ios_url和af_android_url可能会常用到,它们是用于当用户没安装app,并且不希望跳转去应用商店时使用的参数,
就可以跳转到af_ios_url或af_android_url指定的页面。例如:下面这条onelink,当用户没安装app时,并不会跳到应用商店,而是跳转到www.baidu.com页面。

https://bingobingo.onelink.me/115925328?pid=ActivityTemplate&is_retargeting=true&af_dp=${encodeURIComponent(target)}&af_ios_url=${encodeURIComponent(https.www.baidu.com)}&af_android_url=${encodeURIComponent(https.www.baidu.com)}

9. af_ios_url和af_android_url使用场景

以电商为例,假设广告主已经在OneLink模版中选择跳转至对应商店,但在某个新年广告系列推广活动中,广告主希望用户先来到新年促销详情的落地页,以便传递更丰富的信息,再由落地页导向商店。

这个时候,可以通过添加两个简单的参数来覆写,af_ios_url和af_android_url参数。

10. 注意事项

  1. 我们在onelink写的应用内的调起页的链接必须用encodeURIComponent解码一下。

  2. <meta property="og:url" content="https://www.imdb.com/title/tt0117500/" />这条meta,实际上是一个canonical URL

canonical URL会导致facebook在抓取我们分享的链接的预览图、标题、描述的时候,会到这条meta声明的url里查找对应的预览图、标题、描述等。所以当我们分享的链接跟这条meta声明的url不一致时,就会出现显示数据不对的问题。

这条meta的作用其实就是利于seo,所以不考虑seo的话,考虑将这条meta删掉。

canonical标签是一种告诉搜索引擎您要在搜索结果中显示哪个版本的URL的方法。使用canonical标签可以防止由于相同(或非常相似)或“重复”内容出现在多个URL上而引起的问题。

  1. af_dp这个参数里的链接是要带有协议的如:【bigolive(域名)://article?url=/CNT/15664895/news939964.html&newstype=1】

完美。测试通过,成功上线

傍晚6点,收拾东西,准备下班。

产品经理缓缓走过来。

产品经理:阳光啊,你有女朋友吗?

我(啊?心跳加速,难道要给我介绍对象了吗,开心到起飞):我没有呢!

产品经理:啊,好啊,那好啊,那你今晚加下班吧,我还有另一个需求给你。

我:....

tmd c!

参考文献:

  1. 【SEO优化:聊聊页面中rel =“canonical”和og:url标签属性】https://www.jiangweishan.com/article/seo20211118a3.html

  2. 【The Open Graph protocol】https://ogp.me/

  3. 【萨瓦迪卡,OneLink™了解一下?】https://kknews.cc/zh-my/news/3nybylg.html

  4. 【AppLinking快问快答】https://developer.huawei.com/consumer/cn/forum/topic/0201405011252010239?fid=0101271690375130218

  5. 【关于appsflyer的deeplink使用体验】https://zhuanlan.zhihu.com/p/88466945

  6. 【OneLink平台归因、跳转、深度链接】https://blog.csdn.net/lizhong2008/article/details/117705767

  7. 【关于appsflyer的deeplink使用体验】https://zhuanlan.zhihu.com/p/88466945

关于【高级技巧】十问 (读《红宝书》)

第一问:安全类型检测——typeof和instanceof 区别以及缺陷,以及解决方案

这两个方法都可以用来判断变量类型

区别:前者是判断这个变量是什么类型,后者是判断这个变量是不是某种类型,返回的是布尔值

(1)typeof

缺陷:

1.不能判断变量具体的数据类型比如数组、正则、日期、对象,因为都会返回object,不过可以判断function,如果检测对象是正则表达式的时候,在Safari和Chrome中使用typeof的时候会错误的返回"function",其他的浏览器返回的是object.

2.判断null的时候返回的是一个object,这是js的一个缺陷,判断NaN的时候返回是number

(2)instanceof
可以用来检测这个变量是否为某种类型,返回的是布尔值,并且可以判断这个变量是否为某个函数的实例,它检测的是对象的原型


let num = 1
num instanceof Number // false

num = new Number(1)
num instanceof Number // true

明明都是num,而且都是1,只是因为第一个不是对象,是基本类型,所以直接返回false,而第二个是封装成对象,所以true。

这里要严格注意这个问题,有些说法是检测目标的__proto__与构造函数的prototype相同即返回true,这是不严谨的,检测的一定要是对象才行,如:

let num = 1
num.__proto__ === Number.prototype // true
num instanceof Number // false

num = new Number(1)
num.__proto__ === Number.prototype // true
num instanceof Number // true

num.__proto__ === (new Number(1)).__proto__ // true

此外,instanceof还有另外一个缺点:如果一个页面上有多个框架,即有多个全局环境,那么我在a框架里定义一个Array,然后在b框架里去用instanceof去判断,那么该array的原型链上不可能找到b框架里的array,则会判断该array不是一个array。

解决方案:使用Object.prototype.toString.call(value) 方法去调用对象,得到对象的构造函数名。可以解决instanceof的跨框架问题,缺点是对用户自定义的类型,它只会返回[object Object]

第二问:既然提到了instanceof,那手写实现下instanceof吧

// [1,2,3] instanceof Array ---- true

// L instanceof R
// 变量R的原型 存在于 变量L的原型链上
function instance_of(L,R){    
    // 验证如果为基本数据类型,就直接返回false
    const baseType = ['string', 'number','boolean','undefined','symbol']
    if(baseType.includes(typeof(L))) { return false }
    
    let RP  = R.prototype;  //取 R 的显示原型
    L = L.__proto__;       //取 L 的隐式原型
    while(true){           // 无线循环的写法(也可以使 for(;;) )
        if(L === null){    //找到最顶层
            return false;
        }
        if(L === RP){       //严格相等
            return true;
        }
        L = L.__proto__;  //没找到继续向上一层原型链查找
    }
}

第三问:作用域安全的构造函数--当我们new一个构造函数的时候可以获得一个实例,要是我们忘记写new了呢?

例如

function Person(){
    this.name = "小红"
}

p = Person();

这会发生什么问题?,怎么解决

这样直接使用,this会映射到全局对象window上。解决方法可以是:首先确认this对象是正确类型的实例。如果不是,那么会创建新的实例并返回。请看下面的例子

function Person(){
    if(this instanceof Person){
        this.name = "小红"
    }else{
        return  new Person()
    }
}

p = Person();

第四问:谈一下惰性载入函数

在JavaScript代码中,由于浏览器之间行为的差异,多数JavaScript代码包含了大量的if语句,以检查浏览器特性,解决不同浏览器的兼容问题。例如添加事件的函数:

function addEvent (element, type, handler) {
    if (element.addEventListener) {
        element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
        element.attachEvent("on" + type, handler);
    } else {
        element["on" + type] = handler;
    }
}

每次调用addEvent()的时候,都要对浏览器所支持的能力仔细检查。首先检查是否支持addEventListener方法,如果不支持再检查是否支持attachEvent方法,如果还不支持,就用DOM 0级的方法添加事件。在调用addEvent()过程中,每次这个过程都要走一遍。其实,浏览器支持其中的一种方法就会一直支持他,就没有必要再进行其他分支的检测了,也就是说if语句不必每次都执行,代码可以运行的更快一些。解决的方案称之为惰性载入。
所谓惰性载入,就是说函数的if分支只会执行一次,之后调用函数时,直接进入所支持的分支代码。有两种实现惰性载入的方式,第一种事函数在第一次调用时,对函数本身进行二次处理,该函数会被覆盖为符合分支条件的函数,这样对原函数的调用就不用再经过执行的分支了,我们可以用下面的方式使用惰性载入重写addEvent()。


function addEvent (type, element, handler) {
    if (element.addEventListener) {
        addEvent = function (type, element, handler) {
            element.addEventListener(type, handler, false);
        }
    }
    else if(element.attachEvent){
        addEvent = function (type, element, handler) {
            element.attachEvent('on' + type, handler);
        }
    }
    else{
        addEvent = function (type, element, handler) {
            element['on' + type] = handler;
        }
    }
    return addEvent(type, element, handler);
}

在这个惰性载入的addEvent()中,if语句的每个分支都会为addEvent变量赋值,有效覆盖了原函数。最后一步便是调用了新赋函数。下一次调用addEvent()的时候,便会直接调用新赋值的函数,这样就不用再执行if语句了。

第二种实现惰性载入的方式是在声明函数时就指定适当的函数。这样在第一次调用函数时就不会损失性能了,只在代码加载时会损失一点性能。一下就是按照这一思路重写的addEvent()。

var addEvent = (function () {
    if (document.addEventListener) {
        return function (type, element, fun) {
            element.addEventListener(type, fun, false);
        }
    }
    else if (document.attachEvent) {
        return function (type, element, fun) {
            element.attachEvent('on' + type, fun);
        }
    }
    else {
        return function (type, element, fun) {
            element['on' + type] = fun;
        }
    }
})();

这个例子中使用的技巧是创建一个匿名的自执行函数,通过不同的分支以确定应该使用那个函数实现,实际的逻辑都一样,不一样的地方就是使用了函数表达式(使用了var定义函数)和新增了一个匿名函数,另外每个分支都返回一个正确的函数,并立即将其赋值给变量addEvent。

惰性载入函数的优点只执行一次if分支,避免了函数每次执行时候都要执行if分支和不必要的代码,因此提升了代码性能,至于那种方式更合适,就要看您的需求而定了。

第五问:谈一下函数节流

概念:限制一个函数在一定时间内只能执行一次。

主要实现思路 就是通过 setTimeout 定时器,通过设置延时时间,在第一次调用时,创建定时器,先设定一个变量true,写入需要执行的函数。第二次执行这个函数时,会判断变量是否true,是则返回。当第一次的定时器执行完函数最后会设定变量为false。那么下次判断变量时则为false,函数会依次运行。目的在于在一定的时间内,保证多次函数的请求只执行最后一次调用。

函数节流的代码实现

function throttle(fn,wait){
    var timer = null;
    return function(){
        var context = this;
        var args = arguments;
        if(!timer){
            timer = setTimeout(function(){
                fn.apply(context,args);
                timer = null;
            },wait)
        }
    }
}
    
function handle(){
    console.log(Math.random());
}
    
window.addEventListener("mousemove",throttle(handle,1000));

函数节流的应用场景(throttle)

  • DOM 元素的拖拽功能实现(mousemove)
  • 高频点击提交,表单重复提交
  • 搜索联想(keyup)
  • 计算鼠标移动的距离(mousemove)
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,- - 才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次.

第六问:谈一下函数防抖

概念:函数防抖(debounce),就是指触发事件后,在 n 秒内函数只能执行一次,如果触发事件后在 n 秒内又触发了事件,则会重新计算函数延执行时间。

函数防抖的要点,是需要一个 setTimeout 来辅助实现,延迟运行需要执行的代码。如果方法多次触发,则把上次记录的延迟执行代码用 clearTimeout 清掉,重新开始计时。若计时期间事件没有被重新触发,等延迟时间计时完毕,则执行目标代码。

函数防抖的代码实现

function debounce(fn,wait){
    var timer = null;
    return function(){
        if(timer !== null){
            clearTimeout(timer);
        }
        timer = setTimeout(fn,wait);
    }
}
    
function handle(){
    console.log(Math.random());
}
    
window.addEventListener("resize",debounce(handle,1000));

函数防抖的使用场景
函数防抖一般用在什么情况之下呢?一般用在,连续的事件只需触发一次回调的场合。具体有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求;
  • 用户名、手机号、邮箱输入验证;
  • 浏览器窗口大小改变后,只需窗口调整完后,再执行 resize 事件中的代码,防止重复渲染。

目前遇到过的用处就是这些,理解了原理与实现思路,小伙伴可以把它运用在任何需要的场合,提高代码质量。

第七问:谈一下requestAnimationFrame

动画原理
眼前所看到图像正在以每秒60次的频率刷新,由于刷新频率很高,因此你感觉不到它在刷新。而动画本质就是要让人眼看到图像被刷新而引起变化的视觉效果,这个变化要以连贯的、平滑的方式进行过渡。 那怎么样才能做到这种效果呢?

刷新频率为60Hz的屏幕每16.7ms刷新一次,我们在屏幕每次刷新前,将图像的位置向左移动一个像素,即1px。这样一来,屏幕每次刷出来的图像位置都比前一个要差1px,因此你会看到图像在移动;由于我们人眼的视觉停留效应,当前位置的图像停留在大脑的印象还没消失,紧接着图像又被移到了下一个位置,因此你才会看到图像在流畅的移动,这就是视觉效果上形成的动画。

与setTimeout相比较

理解了上面的概念以后,我们不难发现,setTimeout 其实就是通过设置一个间隔时间来不断的改变图像的位置,从而达到动画效果的。但我们会发现,利用seTimeout实现的动画在某些低端机上会出现卡顿、抖动的现象。 这种现象的产生有两个原因:

  • setTimeout的执行时间并不是确定的。在Javascript中, setTimeout 任务被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,因此 setTimeout 的实际执行时间一般要比其设定的时间晚一些。

  • 刷新频率受屏幕分辨率和屏幕尺寸的影响,因此不同设备的屏幕刷新频率可能会不同,而 setTimeout只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。

以上两种情况都会导致setTimeout的执行步调和屏幕的刷新步调不一致,从而引起丢帧现象

requestAnimationFrame:与setTimeout相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

除此之外,requestAnimationFrame还有以下两个优势:

  • CPU节能:使用setTimeout实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费CPU资源。而requestAnimationFrame则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的requestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。

  • 函数节流:在高频率事件(resize,scroll等)中,为了防止在一个刷新间隔内发生多次函数执行,使用requestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个刷新间隔内函数执行多次时没有意义的,因为显示器每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来。

第八问:web计时,你知道该怎么计算首屏,白屏时间吗?

白屏时间
白屏时间指的是浏览器开始显示内容的时间。因此我们只需要知道是浏览器开始显示内容的时间点,即页面白屏结束时间点即可获取到页面的白屏时间。

计算白屏时间
因此,我们通常认为浏览器开始渲染 标签或者解析完 标签的时刻就是页面白屏结束的时间点。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>白屏</title>
  <script type="text/javascript">
    // 不兼容performance.timing 的浏览器,如IE8
    window.pageStartTime = Date.now();
  </script>
  <!-- 页面 CSS 资源 -->
  <link rel="stylesheet" href="common.css">
  <link rel="stylesheet" href="page.css">
  <script type="text/javascript">
    // 白屏时间�结束点
    window.firstPaint = Date.now();
  </script>
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>

因此白屏时间则可以这样计算出:

可使用 Performance API 时:

白屏时间 = firstPaint - performance.timing.navigationStart;

不可使用 Performance API 时:

白屏时间 = firstPaint - pageStartTime; //虽然我们知道这并不准确,毕竟DNS解析,tcp三次握手等都没计算入内。

首屏时间
首屏时间是指用户打开网站开始,到浏览器首屏内容渲染完成的时间。对于用户体验来说,首屏时间是用户对一个网站的重要体验因素。通常一个网站,如果首屏时间在5秒以内是比较优秀的,10秒以内是可以接受的,10秒以上就不可容忍了。超过10秒的首屏时间用户会选择刷新页面或立刻离开。


通常计算首屏的方法有

  • 首屏模块标签标记法

  • 统计首屏内加载最慢的图片的时间

  • 自定义首屏内容计算法

    1、首屏模块标签标记法

首屏模块标签标记法,通常适用于首屏内容不需要通过拉取数据才能生存以及页面不考虑图片等资源加载的情况。我们会在 HTML 文档中对应首屏内容的标签结束位置,使用内联的 JavaScript 代码记录当前时间戳。如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>首屏</title>
  <script type="text/javascript">
    window.pageStartTime = Date.now();
  </script>
  <link rel="stylesheet" href="common.css">
  <link rel="stylesheet" href="page.css">
</head>
<body>
  <!-- 首屏可见模块1 -->
  <div class="module-1"></div>
  <!-- 首屏可见模块2 -->
  <div class="module-2"></div>
  <script type="text/javascript">
    window.firstScreen = Date.now();
  </script>
  <!-- 首屏不可见模块3 -->
  <div class="module-3"></div>
    <!-- 首屏不可见模块4 -->
  <div class="module-4"></div>
</body>
</html>

此时首屏时间等于 firstScreen - performance.timing.navigationStart;

事实上首屏模块标签标记法 在业务中的情况比较少,大多数页面都需要通过接口拉取数据才能完整展示,因此我们会使用 JavaScript 脚本来判断首屏页面内容加载情况。

2、统计首屏内图片完成加载的时间

通常我们首屏内容加载最慢的就是图片资源,因此我们会把首屏内加载最慢的图片的时间当做首屏的时间。

由于浏览器对每个页面的 TCP 连接数有限制,使得并不是所有图片都能立刻开始下载和显示。因此我们在 DOM树 构建完成后将会去遍历首屏内的所有图片标签,并且监听所有图片标签 onload 事件,最终遍历图片标签的加载时间的最大值,并用这个最大值减去 navigationStart 即可获得近似的首屏时间。

此时首屏时间等于 加载最慢的图片的时间点 - performance.timing.navigationStart;
//首屏时间尝试:
//1,获取首屏基线高度
//2,计算出基线dom元素之上的所有图片元素
//3,所有图片onload之后为首屏显示时间
//

function getOffsetTop(ele) {
    var offsetTop = ele.offsetTop;
    if (ele.offsetParent !== null) {
        offsetTop += getOffsetTop(ele.offsetParent);
    }
    return offsetTop;
}

var firstScreenHeight = win.screen.height;
var firstScreenImgs = [];
var isFindLastImg = false;
var allImgLoaded = false;
var t = setInterval(function() {
    var i, img;
    if (isFindLastImg) {
        if (firstScreenImgs.length) {
            for (i = 0; i < firstScreenImgs.length; i++) {
                img = firstScreenImgs[i];
                if (!img.complete) {
                    allImgLoaded = false;
                    break;
                } else {
                    allImgLoaded = true;
                }
            }
        } else {
            allImgLoaded = true;
        }
        if (allImgLoaded) {
            collect.add({
                firstScreenLoaded: startTime - Date.now()
            });
            clearInterval(t);
        }
    } else {
        var imgs = body.querySelector('img');
        for (i = 0; i<imgs.length; i++) {
            img = imgs[i];
            var imgOffsetTop = getOffsetTop(img);
            if (imgOffsetTop > firstScreenHeight) {
                isFindLastImg = true;
                break;
            } else if (imgOffsetTop <= firstScreenHeight && !img.hasPushed) {
                img.hasPushed = 1;
                firstScreenImgs.push(img);
            }
        }
    }
}, 0);
doc.addEventListener('DOMContentLoaded', function() {
    var imgs = body.querySelector('img');
    if (!imgs.length) {
        isFindLastImg = true;
    }
});

win.addEventListener('load', function() {
    allImgLoaded = true;
    isFindLastImg = true;
    if (t) {
        clearInterval(t);
    }
    collect.log(collect.global);
});

解释一下思路,大概就是判断首屏有没有图片,如果没图片就用domready时间,如果有图,分2种情况,图在首屏,图不在首屏,如果在则收集,并判断加载状态,加载完毕之后则首屏完成加载,如果首屏没图,找到首屏下面的图,立刻触发首屏完毕。可以想象这么做前端收集是不准的,但是可以确保最晚不会超过win load,所以应该还算有些意义。。没办法,移动端很多浏览器不支持performance api,所以土办法前端收集,想出这么个黑魔法,在基线插入节点收集也是个办法,但是不友好,而且现在手机屏幕这么多。。

3、自定义模块内容计算法

由于统计首屏内图片完成加载的时间比较复杂。因此我们在业务中通常会通过自定义模块内容,来简化计算首屏时间。如下面的做法:

  • 忽略图片等资源加载情况,只考虑页面主要 DOM
  • 只考虑首屏的主要模块,而不是严格意义首屏线以上的所有内容

实际上用performance.timing来计算首屏加载时间与白屏时间非常简单与精确。不过目前只支持IE10和chrome
贴下其API的使用

                                                   var navigationStart = performance.timing.navigationStart;
//1488984540668
console.log(navigationStart);

//Wed Mar 08 2017 22:49:44 GMT+0800 (**标准时间)
console.log(new Date(new Date(navigationStart)));
复制代码
  redirectStart:到当前页面的重定向开始的时间。但只有在重定向的页面来自同一个域时这个属性才会有值;否则,值为0
  redirectEnd:到当前页面的重定向结束的时间。但只有在重定向的页面来自同一个域时这个属性才会有值;否则,值为0

console.log(performance.timing.redirectStart);//0
console.log(performance.timing.redirectEnd);//0
  fetchStart:开始通过HTTP GET取得页面的时间

console.log(performance.timing.fetchStart);//1488984540668
  domainLookupStart:开始査询当前页面DNS的时间,如果使用了本地缓存或持久连接,则与fetchStart值相等
  domainLookupEnd:査询当前页面DNS结束的时间,如果使用了本地缓存或持久连接,则与fetchStart值相等

console.log(performance.timing.domainLookupStart);//1488984540670
console.log(performance.timing.domainLookupEnd);//1488984540671
  connectStart:浏览器尝试连接服务器的时间
  secureConnectionStart:浏览器尝试以SSL方式连接服务器的时间。不使用SSL方式连接时,这个属性的值为0 
  connectEnd:浏览器成功连接到服务器的时间

console.log(performance.timing.connectStart);//1488984540671
console.log(performance.timing.secureConnectionStart);//0
console.log(performance.timing.connectEnd);//1488984540719
  requestStart:浏览器开始请求页面的时间
  responseStart:浏览器接收到页面第一字节的时间
  responseEnd:浏览器接收到页面所有内容的时间

console.log(performance.timing.requestStart);//1488984540720
console.log(performance.timing.responseStart);//1488984540901
console.log(performance.timing.responseEnd);//1488984540902
  unloadEventStart:前一个页面的unload事件开始的时间。但只有在前一个页面与当前页面来自同一个域时这个属性才会有值;否则,值为0
  unloadEventEnd:前一个页面的unload事件结束的时间。但只有在前一个页面与当前页面来自同一个域时这个属性才会有值;否则,值为0

console.log(performance.timing.unloadEventStart);//1488984540902
console.log(performance.timing.unloadEventEnd);//1488984540903
  domLoading:document.readyState变为"loading"的时间,即开始解析DOM树的时间
  domInteractive:document.readyState变为"interactive"的时间,即完成完成解析DOM树的时间
  domContentLoadedEventStart:发生DOMContentloaded事件的时间,即开始加载网页内资源的时间
  domContentLoadedEventEnd:DOMContentLoaded事件已经发生且执行完所有事件处理程序的时间,网页内资源加载完成的时间
  domComplete:document.readyState变为"complete"的时间,即DOM树解析完成、网页内资源准备就绪的时间

console.log(performance.timing.domLoading);//1488984540905
console.log(performance.timing.domInteractive);//1488984540932
console.log(performance.timing.domContentLoadedEventStart);//1488984540932
console.log(performance.timing.domContentLoadedEventEnd);//1488984540932
console.log(performance.timing.domComplete);//1488984540932
  loadEventStart:发生load事件的时间,也就是load回调函数开始执行的时间 
  loadEventEnd:load事件已经发生且执行完所有事件处理程序的时间

console.log(performance.timing.loadEventStart);//1488984540933
console.log(performance.timing.loadEventEnd);//1488984540933
                                                       

第九问:你知道web Worker吗?

多线程技术在服务端技术中已经发展的很成熟了,而在Web端的应用中却一直是鸡肋
在新的标准中,提供的新的WebWork API,让前端的异步工作变得异常简单。
使用:创建一个Worker对象,指向一个js文件,然后通过Worker对象往js文件发送消息,js文件内部的处理逻辑,处理完毕后,再发送消息回到当前页面,纯异步方式,不影响当前主页面渲染。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <script type="text/javascript">
        //创建线程 work对象
        var work = new Worker("work.js");      //work文件中不要存在跟ui代码
        //发送消息
        work.postMessage("100");
        // 监听消息
        work.onmessage = function(event) {
            alert(event.data);
        };
    </script>
</head>
<body>

</body>
</html>          

work.js

  onmessage = function (event) {
    //从1加到num
    var num = event.data;
    var result = 0;
    for (var i = 1; i <= num; i++) {
        result += i;
    }
    postMessage(result);
}
   

手写Vue核心原理,再也不怕面试官问我Vue原理

1.准备工作

** 我们先利用webpack构建项目:**

  • 初始化项目

    npm init -y

  • 安装webpack

    npm i webpack webpack-cli webpack-dev-server html-webpack-plugin --save

  • 配置webpack

    // webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry:'./src/index.js',// 以src下的index.js 作为入口文件进行打包
    output:{
        filename:'bundle.js',
        path:path.resolve(__dirname,'dist')
    },
    devtool:'source-map', // 调试的时候可以快速找到错误代码
    resolve:{
        // 更改模块查找方方式(默认的是去node_modules里去找)去source文件里去找
        modules:[path.resolve(__dirname,'source'),path.resolve('node_modules')]
    },
    plugins:[
        new HtmlWebpackPlugin({
            template:path.resolve(__dirname,'public/index.html')
        })
    ]
}
  • 配置package.json
    "scripts": {
    "start": "webpack-dev-server",
    "build": "webpack"
  },

2 实现数据监听

2.1 创建构造函数MyVue

并初始化用户传入的参数options,我们先假设用户传入的options是只有data属性和el属性的。

export function initState(vm) {
    let opt = vm.$optios
    if (opt.data){
        initData(vm);
    }
}

function initData(vm) {
    // 获取用户传入的data
    let data = vm.$optios.data
    // 判断是不是函数,我们知道vue,使用data的时候可以data:{}这种形式,也可以data(){return{}}这种形式
    // 然后把把用户传入的打他数据赋值给vm._data
    data = vm._data = typeof data === 'function' ? data.call(vm) : data ||{}

    observe(data)
}

到这里我们实现的是new MyVue的时候,通过_init方法来初始化options, 然后通过initData方法将data挂到vm实例的_data上去了,接下来,我们要对data实现数据监听,上面的代码中observe代码就是用来实现数据监听的。

2.2 实现数据监听

export function observe(data) {
    if (typeof data !== 'object' || data == null){
        return
    }
    return new Observe(data)
}

在这段代码observe方法的代码中,observe()将传入的data先进行判断,如果data是对象,则new 一个Observe对象来使这个data 实现数据监听,我们再看下Observe是怎么实现的

class Observe {
    constructor(data){ // data就是我们定义的data vm._data实例
        // 将用户的数据使用defineProperty定义
        this.walk(data)
    }
    walk(data){
        let keys = Object.keys(data)
        for (let i = 0;i<keys.length;i++){
            let key  = keys[i]; // 所有的key
            let value = data[keys[i]] //所有的value
            defineReactive(data,key,value)
        }
    }
}

可见,Observe 将data传入walk方法里,而在walk方法里对data进行遍历,然后将data的每一个属性和对应的值传入defineReactive,我们不难猜测,这个defineReactive就是将data的每一个属性实现监听。我们再看下defineReactive

export function defineReactive(data,key,value) {
  
    Object.defineProperty(data,key,{
        get(){
            return value
        },
        set(newValue){
            if (newValue === value) return
            value = newValue
            observe(value)
        }
    })
}

可见,这是通过defineProperty,=将每个key进行数据监听了。但是这里有一个问题,就是,这里只能监听一个层级,比如

data = {
  wife:"迪丽热巴"
}

这时没问题的,但是

data = {
  wife:{
    name:"迪丽热码",
    friend:{
      name:"古力娜和"
    }
  }
}

我们只能监听到wife.friend和wife.name是否改变与获取,无法监听到wife.friend.name这个属性的变化,因此,我们需要判断wife.friend是不是对象,然后将这个friend对象进行遍历对它的属性实现监听

2.3 解决多层级监听的问题

因此我们在上面代码的基础上,添加上observe(value)就实现了递归监听

export function defineReactive(data,key,value) {
    // 观察value是不是对象,是的话需要监听它的属性。
    observe(value)

    Object.defineProperty(data,key,{
        get(){
            return value
        },
        set(newValue){
            if (newValue === value) return
            value = newValue
        }
    })
}

基本完成。

但是到这里,还有一个问题,就是我们上面的data都是new MyVue的时候传进去的,因此要是我们再new 完 改变data的某个值,如下面将message改成迪丽热巴对象,此时虽然我们依旧可以监听message,但是message.name是监听不到的

let vm = new MyVue({
    el: '#app',
    data(){
        return{
            message:'大家好',
            wife:{
                name:"angelababy",
                age:28
            }
        }
    }
})
vm._data.message = {
    name:'迪丽热巴',
    age:30
}

2.4 解决data中某个属性变化后无法监听的问题

我们知道 message这个属性已经被我们监听了,所以改变message的时候,会触发set()方法,因此我们只需要将wife再放进observe()中重新实现监听一遍即可,如代码所示

export function defineReactive(data,key,value) {
    // 观察value是不是对象,是的话需要监听它的属性。
    observe(value)

    Object.defineProperty(data,key,{
        get(){
            return value
        },
        set(newValue){
            if (newValue === value) return
            value = newValue
            observe(value)
        }
    })
}

2.5 实现数据代理

我们用过vue的都知道,我们获取data中的属性的时候,都是直接通过this.xxx,获取值的,而我们上面只实现了想要获取值需要通过this._data.xxx,所以这一节来实现是数据代理,即将data中的属性挂载到vm上,我们可以实现一个proxy方法,该方法将传入的数据挂载到vm上,而当我们访问this.xxx的时候,其实是访问了this._data.xxx,这就是代理模式。
增加proxy后代码如下

function proxy(vm,source,key) {
    Object.defineProperty(vm,key,{
        get(){
            return vm[source][key]
        },
        set(newValue){
            return vm[source][key] = newValue
        }
    })
}
function initData(vm) {
    // 获取用户传入的data
    let data = vm.$optios.data
    // 判断是不是函数,我们知道vue,使用data的时候可以data:{}这种形式,也可以data(){return{}}这种形式
    // 然后把把用户传入的打他数据赋值给vm._data
    data = vm._data = typeof data === 'function' ? data.call(vm) : data ||{}

    for (let key in data) {
        proxy(vm,"_data",key)
    }

    observe(data)
}

实现原理非常简单,实际上就是但我们想要获取this.wife时,其实是去获取this._data.wife

至此,我们已经实现了数据监听,但是还有个问题,即Object.defineProperty的问题,也是面试常见的问题,即Object.defineProperty是无法监听数组的变化的

3 重写数组方法

如图所示,我们企图往数组arr中添加值,结果发现新添加进去的值是没办法被监听到的,因此,我们需要改写push等方法

let vm = new MyVue({
    el: '#app',
    data(){
        return{
            message:'大家好',
            wife:{
                name:"angelababy",
                age:28
            },
            arr:[1,2,{name:"赵丽颖"}]
        }
    }
})
vm.arr.push({hah:'dasd'})

基本思路就是之前我们调用push方法时,是从Aarray.prototype寻找这个方法,我们改成用一个空对象{} 继承 Aarray.prototype,然后再给空对象添加push方法

{
    push:function(){}
}

这样,我们调用push的时候,实际上就是调用上面{}中的push

现在,我们先区分出用户传入的Observe中接受监听的data是数组还是对象,如果是数组,则改变数组的原型链,这样才能改变调用push时,是调用我们自己设置的push,
只需要在Observe添加判断是数组还是对象即可。

class Observe {
    constructor(data){ // data就是我们定义的data vm._data实例
        // 将用户的数据使用defineProperty定义
        if (Array.isArray(data)){
            data.__proto__ = arrayMethods
        }else {
            this.walk(data)
        }
    }
    walk(data){
        let keys = Object.keys(data)
        for (let i = 0;i<keys.length;i++){
            let key  = keys[i]; // 所有的key
            let value = data[keys[i]] //所有的value
            defineReactive(data,key,value)
        }
    }
}

其中的arrayMethods则是我们一直说的那个对象{},它里面添加push等方法属性

let oldArrayPrototypeMethods = Array.prototype
// 复制一份 然后改成新的
export let arrayMethods = Object.create(oldArrayPrototypeMethods)

// 修改的方法
let methods = ['push','shift','unshift','pop','reverse','sort','splice']

methods.forEach(method=>{
    arrayMethods[method] = function (...arg) {
        // 不光要返回新的数组方法,还要执行监听
        let res = oldArrayPrototypeMethods[method].apply(this,arg)
        // 实现新增属性的监听
        console.log("我是{}对象中的push,我在这里实现监听");
      
      
        return res
    }
})

实际上这是一种拦截的方法。
接下来,我们就要着手实现新增属性的监听。基本思路,1.获得新增属性,2.实现监听

methods.forEach(method=>{
    arrayMethods[method] = function (...arg) {
        // 不光要返回新的数组方法,还要执行监听
        let res = oldArrayPrototypeMethods[method].apply(this,arg)
        // 实现新增属性的监听
        console.log("我是{}对象中的push,我在这里实现监听");
        // 实现新增属性的监听
        let inserted  // 1.获得新增属性
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = arg
                break
            case 'splice':
                inserted = arg.slice(2)
                break
            default:
                break
        }
        // 实现新增属性的监听
        if (inserted){
            observerArray(inserted)
        }
        return res
    }

})

这里用到了observerArray,我们看一下

export function observerArray(inserted){
    // 循环监听每一个新增的属性
    for(let i =0;i<inserted.length;i++){
        observe(inserted[i])
    }
}

可见,它是将inserted进行遍历,对每一项实现监听。可能这里你会有疑问,为什么要进行遍历,原因是inserted不一定是一个值,也可能是多个,例如[].push(1,2,3)。

现在已经实现了数组方法的拦截,还有个问题没有解决,就是当我们初始化的时候,data里面可能有数组,因此也要把这个数组进行监听

constructor(data){ // data就是我们定义的data vm._data实例
    // 将用户的数据使用defineProperty定义
    if (Array.isArray(data)){
        data.__proto__ = arrayMethods
        observerArray(data)
    }else {
        this.walk(data)
    }
}

现在已经实现了对数据的监听,不过这里还有问题没解决,也是vue2.0中没有解决的问题,就是这里并没有实现对数组的每一项实现了监听
例如,这样是不会监听到的。

let vm = new MyVue({
    el: '#app',
    data(){
        return{
            message:'大家好',
            wife:{
                name:"angelababy",
                age:28
            },
            arr:[1,2,{name:"赵丽颖"}]
        }
    }
})
vm.arr[0] = "我改了"

不仅如此,vm.arr.length = 0,当你这样设置数组长度时,也是无法监听到的。

4.初始化渲染页面

数据初始化之后,接下来,就要把初始化好的数据渲染到页面上去了也就是说当dom中有{{name}}这样的引用时,要把{{name}}替换成data里对应的数据

MyVue.prototype._init = function (options) {
    let vm = this;
    // this.$options表示是Vue中的参数,如若我们细心的话我们发现vue框架的可读属性都是$开头的
    vm.$options = options;

    // MVVM原理 重新初始化数据  data
    initState(vm)

    // 初始化渲染页面
    if (vm.$options.el){
        vm.$mount()
    }
}

$mount的功能很显然就是

  1. 先获得dom树,
  2. 然后替换dom树中的数据,
  3. 然后再把新dom挂载到页面上去

我们看下实现代码

MyVue.prototype.$mount = function () {
    let vm = this
    let el = vm.$options.el
    el = vm.$el = query(el) //获取当前节点

    let updateComponent = () =>{
        console.log("更新和渲染的实现");
        vm._update()
    }
    new Watcher(vm,updateComponent)
}

显然,我们并没有看到上面所说的

  1. 先获得dom树,
  2. 然后替换dom树中的数据,
  3. 然后再把新dom挂载到页面上去,

那肯定是把 这些步骤放在 vm._update()的时候实现了。
我们来看下update代码

// 拿到数据更新视图
MyVue.prototype._update = function () {
    let vm = this
    let el = vm.$el
    // 1. 获取dom树
    let node = document.createDocumentFragment()
    let firstChild
    while (firstChild = el.firstChild){
        node.appendChild(firstChild)
    }
    // 2.然后替换dom树中的数据,
    compiler(node,vm)

    //3.然后再把新dom挂载到页面上去,
    el.appendChild(node) 
}

可见,这三个步骤在update的时候实现了。

而这个update方法的执行需要

    let updateComponent = () =>{
        console.log("更新和渲染的实现");
        vm._update()
    }

这个方法的执行,
显然,这个方法是new Wacther的时候执行的。

let id = 0
class Watcher {
    constructor(vm,exprOrFn,cb = ()=>{},opts){
        this.vm = vm
        this.exprOrFn = exprOrFn
        this.cb = cb
        this.id = id++
        if (typeof exprOrFn === 'function'){
            this.getter = exprOrFn
        }
        this.get()  // 创建一个watcher,默认调用此方法
    }
    get(){
        this.getter()
    }
}
export default Watcher

可见,this.getter就是我们传进去的updateComponent,然后在new Wacther的时候,就自动执行了。

总结下思路,

  1. new Watcher的时候执行了 updateComponent
  2. 执行 updateComponent 的时候执行了 update
  3. 执行update的时候执行了 1. 先获得dom树,2. 然后替换dom树中的数据,3. 然后再把新dom挂载到页面上去

现在我们就已经实现了初始化渲染。即把dom中{{}}表达式换成了data里的数据。

上面用到的compile方法我们还没解释,其实,很简单

const defaultRGE = /\{\{((?:.|\r?\n)+?)\}\}/g

export const util = {
    getValue(vm,exp){
        let keys = exp.split('.')
        return keys.reduce((memo,current)=>{
            memo = memo[current]
            return memo
        },vm)
    },
    compilerText(node,vm){
        node.textContent = node.textContent.replace(defaultRGE,function (...arg) {
           return util.getValue(vm,arg[1])
        })
    }
}

export function compiler(node,vm) {
    // 1 取出子节点、
    let childNodes = node.childNodes
    childNodes = Array.from(childNodes)
    childNodes.forEach(child =>{
        if (child.nodeType === 1 ){
            compiler(child,vm)
        }else if (child.nodeType ===3) {
            util.compilerText(child,vm)
        }
    })
}

5.更新数据渲染页面

我们上一节只实现了 初始化渲染,这一节来实现 数据一旦修改就重新渲染页面。上一节中,我们是通过new Watcher()来初始化页面的,也就是说这个watcher具有重新渲染页面的功能,因此,我们一旦改数据的时候,就再一次让这个watcher执行刷新页面的功能。这里有必要解释下一个watcher对应一个组件,也就是说你new Vue 机会生成一个wacther,因此有多个组件的时候就会生成多个watcher。

现在,我们给每一个data里的属性生成一个对应的dep。
例如:

data:{
  age:18,
  friend:{
    name:"赵丽颖",
    age:12
  }
}

上面中,age,friend,friend.name,friend.age分别对应一个dep。一共四个dep。dep的功能是用来通知上面谈到的watcher执行刷新页面的功能的。

export function defineReactive(data,key,value) {
    // 观察value是不是对象,是的话需要监听它的属性。
    observe(value)
    let dep = new Dep() // 新增代码:一个key对应一个dep
    Object.defineProperty(data,key,{
        get(){
            return value
        },
        set(newValue){
            if (newValue === value) return
            value = newValue
            observe(value)
        }
    })
}

现在有一个问题,就是dep要怎么跟watcher关联起来,我们可以把watcher存储到dep里

let id = 0
class Dep {
    constructor(){
        this.id = id++
        this.subs = []
    }
    addSub(watcher){ //订阅
        this.subs.push(watcher)
    }
}

如代码所示,我们希望执行addSub方法就可以将watcher放到subs里。
那什么时候可以执行addSub呢?

我们在执行compile的时候,也就是将dom里的{{}}表达式换成data里的值的时候,因为要获得data里的值,因此会触发get。这样,我们就可以在get里执行addSub。而watcher是放在全局作用域的,我们可以直接重全局作用域中拿这个watcher放到传入addSub。

好了,现在的问题就是,怎么把watcher放到全局作用域

let id = 0
class Watcher {
    constructor(vm,exprOrFn,cb = ()=>{},opts){
        this.vm = vm
        this.exprOrFn = exprOrFn
        this.cb = cb
        this.id = id++
        this.deps = []
        this.depsId = new Set()
        if (typeof exprOrFn === 'function'){
            this.getter = exprOrFn
        }
        this.get()  // 创建一个watcher,默认调用此方法
    }
    get(){
        pushTarget(this)
        this.getter()
        popTarget()
    }
}
export default Watcher

可见,是通过pushTarget(this)放到全局作用域,再通过popTarget()将它移除。

要知道,wachter和dep是多对多的关系,dep里要保存对应的watcher,watcher也要保存对应的dep
因此,但我们触发get的时候,希望可以同时让当前的watcher保存当前的dep,也让当前的dep保存当前的wacther

export function defineReactive(data,key,value) {
    // 观察value是不是对象,是的话需要监听它的属性。
    observe(value)
    let dep = new Dep()
    Object.defineProperty(data,key,{
        get(){
            if (Dep.target){
                dep.depend() //让dep保存watcher,也让watcher保存这个dep
            }
            return value
        },
        set(newValue){
            if (newValue === value) return
            value = newValue
            observe(value)

        }
    })
}

让我们看下depend方法怎么实现

let id = 0
class Dep {
    constructor(){
        this.id = id++
        this.subs = []
    }
    addSub(watcher){ //订阅
        this.subs.push(watcher)
    }
    depend(){
        if (Dep.target){
            Dep.target.addDep(this)
        }
    }
}
// 保存当前watcher
let stack = []
export function pushTarget(watcher) {
    Dep.target = watcher
    stack.push(watcher)
}
export function popTarget() {
    stack.pop()
    Dep.target = stack[stack.length - 1]
}

export default Dep

可见depend方法又执行了watcher里的addDep,看一下watcher里的addDep。

import {pushTarget , popTarget} from "./dep"
let id = 0
class Watcher {
    constructor(vm,exprOrFn,cb = ()=>{},opts){
        this.vm = vm
        this.exprOrFn = exprOrFn
        this.cb = cb
        this.id = id++
        this.deps = []
        this.depsId = new Set()
        if (typeof exprOrFn === 'function'){
            this.getter = exprOrFn
        }
        this.get()  // 创建一个watcher,默认调用此方法
    }
    get(){
        pushTarget(this)
        this.getter()
        popTarget()
    }
    update(){
        this.get()
    }
    addDep(dep){
        let id = dep.id
        if(this.depsId.has(id)){
            this.depsId.add(id)
            this.deps.push(dep)
        }
        dep.addSub(this)
    }
}
export default Watcher

如此一来,就让dep和watcher实现了双向绑定。
这里代码,你可能会有个疑问,就是为什么是用一个stack数组来保存watcher,这里必须解释下,因为每一个watcher是对应一个组件的,也就是说,当页面中有多个组件的时候,就会有多个watcher,而多个组件的执行是依次执行的,也就是说Dep.target中 只会有 当前被执行的组件所对应的watcher。

例如,有一道面试题:父子组件的执行顺序是什么?

答案:在组件开始生成到结束生成的过程中,如果该组件还包含子组件,则自己开始生成后,要让所有的子组件也开始生成,然后自己就等着,直到所有的子组件生成完毕,自己再结束。“父亲”先开始自己的created,然后“儿子”开始自己的created和mounted,最后“父亲”再执行自己的mounted。

为什么会这样,到这里我们就应该发现了,new Vue的时候是先执行initData,也就是初始化数据,然后执行$mounted,也就是new Watcher。而初始化数据的时候,也要处理components里的数据。处理component里的数据的时候,每处理一个子组件就会new Vue,生成一个子组件。因此是顺序是这样的。也就对应了上面的答案。

  1. 初始化父组件数据-->
  2. 初始化 子组件数据 -->
  3. new 子组件Wacther -->
  4. new 父组件Watcher

好,言归正传,回到我们的项目来,接下来要实现的就是 当有数据更改的时候,我们要重新渲染页面。而我们可以通过set来监听数据是否被更改,因此基本步骤为:

  1. set监听到数据被更改
  2. 让dep执行dep.notify()通知与它相关的watcher
  3. watcher执行update,重新渲染页面
 Object.defineProperty(data,key,{
        get(){
            if (Dep.target){
                dep.depend() //让dep保存watcher,也让watcher保存这个dep
            }
            return value
        },
        set(newValue){
            if (newValue === value) return
            value = newValue
            observe(value)

            // 当设置属性的时候,通知watcher更新
            dep.notify()

        }
    })

dep添加notify方法

class Dep {
    constructor(){
        this.id = id++
        this.subs = []
    }
    addSub(watcher){ //订阅
        this.subs.push(watcher)
    }
    notify(){ //发布
        this.subs.forEach(watcher =>{
            watcher.update()
        })
    }
    depend(){
        if (Dep.target){
            Dep.target.addDep(this)
        }
    }
}

watcher添加update方法

class Watcher {
    constructor(vm,exprOrFn,cb = ()=>{},opts){
        this.vm = vm
        this.exprOrFn = exprOrFn
        this.cb = cb
        this.id = id++
        this.deps = []
        this.depsId = new Set()
        if (typeof exprOrFn === 'function'){
            this.getter = exprOrFn
        }
        this.get()  // 创建一个watcher,默认调用此方法
    }
    get(){
        pushTarget(this)
        this.getter()
        popTarget()
    }
    update(){
        this.get()
    }
    addDep(dep){
        let id = dep.id
        if(this.depsId.has(id)){
            this.depsId.add(id)
            this.deps.push(dep)
        }
        dep.addSub(this)
    }
}
export default Watcher

5. 批量更新防止重复渲染

上面我们是每更改一个数据,就会通知watcher重新渲染页面,显然,要是我们在一个组件里更改多个数据,那么就会多次通知wathcer渲染页面,因此这节我们来实现 批量更新,防止重复渲染。
怎么解决呢?

我们知道,每更新一个数据,就会触发dep.notify。而如果组件里的多个数据都更新的话,就会多次触发dep.notyfy。因为是同一个组件里的数据,因此,这些dep.notify通知的是同一个watcher 执行update。显然,这是没必要的,我们只希望先让所有的数据都修改完,再统一让watcher执行一次update。

该怎么实现呢?我们可以创建一个数组queue,来放置即将渲染页面的watcher。所以,我们先要判断这些dep通知的是不是同一个watcher。不相同的话就放入queue里,相同的就不放。(queue就是个去重数组)。基于此,我们可以更改Watcher里的update方法。

class Watcher {
    constructor(vm,exprOrFn,cb = ()=>{},opts){
        。。。
        this.get()  // 创建一个watcher,默认调用此方法
    }
    get(){
        pushTarget(this)
        this.getter()
        popTarget()
    }
    update(){
        // this.get()
        queueWacther(this) //修改代码
    }
    // 新增代码
    run(){
        this.get()
    }

}
// 新增代码
let has = {}
let queue = []
function queueWacther(watcher) {
    let id = watcher.id
    if(has[id] == null){
        has[id] = true
        queue.push(watcher)
    }
}

这样一来,queue里放置的就是不同的wathcer。

接下,再执行queue里的watcher.run。

let has = {}
let queue = []
// 新增代码
function flushQueue() {
    console.log("执行了flushQueue");
    queue.forEach(watcher=>{
        watcher.run()
    })
    has = []
    queue = []
}
function queueWacther(watcher) {
    let id = watcher.id
    if(has[id] == null){
        has[id] = true
        queue.push(watcher)
    }
}

记得执行完要清空queue队列。

但是有个重要的事情,就是queue里的watcher.run必须要异步执行。

现在就是要异步执行queue里的watcher.run()。
我们可以把重新渲染的动作放到异步队列里(可以通过promise.then放到微任务队列里)。而修改数据是在主线程上的,因此,会先执行完主线程才会执行异步队列里的方法。

let has = {}
let queue = []
function flushQueue() {
    console.log("执行了flushQueue");
    queue.forEach(watcher=>{
        watcher.run()
    })
    has = []
    queue = []
}
function queueWacther(watcher) {
    let id = watcher.id
    if(has[id] == null){
        has[id] = true
        queue.push(watcher)
    }
    nextTick(flushQueue) //新增代码:异步执行flushQueue
}
//新增代码:异步执行flushQueue
function nextTick(flushQueue) {
    Promise.resolve().then(flushQueue)
}

实际上这就已经完成了批量更新和防止重复渲染。

但是为了贴近vue源码,我们更改下nextTick。使用vue的相信都用过nextTick,因此也就是说我们会在其他地方调用nextTick,而且我们是经常这样使用的

this.$nextTick(() => {
    this.msg2 = this.$refs.msgDiv.innerHTML
})

也就是说我们会传进个回调函数,而我们上面写的nextTick参数也是一个回调函数。
那么我们可以把其他地方调用nextTick的回调函数 一起整合进一个callback,然后统一执行callback。

因此,我们做如下更改

// 新增代码
let callbacks = []
function flushCallbacks() {
    callbacks.forEach(cb=>cb())
    callbacks = []
}

function nextTick(flushQueue) {
    callbacks.push(flushQueue) //新增代码

    Promise.resolve().then(flushCallbacks
}

6.实现数组依赖收集

上面我们只是对数组实现了方法的拦截,还没实现数据的更新渲染。
现在要解决连个问题

  1. 在哪里收集依赖
  2. 依赖保存在哪里

实际上依赖收集依旧是在getter里实现的。

因为当我们 获取list:[1,2,3]的时候会触发get,所以可以在getter里收集依赖。

那保存在哪里呢?保存在Observe里,因为在拦截方法中可以获得observe,而在set里也可以获得observe。

class Observe {
    constructor(data){ // data就是我们定义的data vm._data实例
        // 将用户的数据使用defineProperty定义
        // 创建数组专用 的dep
        this.dep = new Dep()
        // 给我们的对象包括我们的数组添加一个属性__ob__ (这个属性即当前的observe)
        Object.defineProperty(data,'__ob__',{
            get:() => this
        })
        if (Array.isArray(data)){
            data.__proto__ = arrayMethods
            observerArray(data)
        }else {
            this.walk(data)
        }
    }
   
}

我们在这里返回了一个Observe对象。

然后我们需要在拦截方法里notify

methods.forEach(method=>{
    arrayMethods[method] = function (...arg) {
        // 不光要返回新的数组方法,还要执行监听
        let res = oldArrayPrototypeMethods[method].apply(this,arg)
        // 实现新增属性的监听
        console.log("我是{}对象中的push,我在这里实现监听");
        // 实现新增属性的监听
        let inserted
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = arg
                break
            case 'splice':
                inserted = arg.slice(2)
                break
            default:
                break
        }
        // 实现新增属性的监听
        if (inserted){
            observerArray(inserted)
        }
        this.__ob__.dep.notify()
        return res
    }
})

现在保存依赖和通知更新的问题都解决了,下一步就是在setter里依赖收集

7.watch的实现

现在在initState中添加初始化watch

export function initState(vm) {
    let opt = vm.$options
    if (opt.data){
        initData(vm);
    }
    if (opt.watch){
        initWathch(vm);
    }
}

我们现在原型对象上实现一个方法

MyVue.prototype.$watch = function (key,handler) {
    let vm = this
    new Watcher(vm,key,handler,{user:true})
    
}

这个方法为我们的watch中的key 单独创建了一个Watch实例,其中handler是回调方法。

我们希望初始化(即 new Watch )的时候,先获得key的oldValue。方便后面和newValue比较是否发生变化。

class Watcher {
    constructor(vm,exprOrFn,cb = ()=>{},opts){
        // 省略其他代码
        if (typeof exprOrFn === 'function'){
            this.getter = exprOrFn
        }else{
            // 现在exprOrFn是我们传进来的key
            this.getter = function () {
                return util.getValue(vm,key)
            }
        }
        this.value = this.get() //获得老值oldValue
        // 创建一个watcher,默认调用此方法
    }
    get(){
        pushTarget(this)
        let value = this.getter()
        popTarget()
        return value
    }

当key的值改变的时候,会触发dep.notify。也就会触发wathcer.update ,然后触发watcher.run

我们在 run中获得新值,然后 将新值与老值进行比较,如果两者不等的话,就触发回调函数

    run(){
        let value = this.get()
        if (this.value !== value){
            this.cb(value,this.value)
        }
    }

ok,现在来继续初始化initWatch

function initWathch(vm) {
    let watch = vm.$options.watch
    for (let key in watch){
        let handler = watch[key]
        createWatch(vm,key,handler)
    }
}
function createWatch(vm,key,handler) {
    return this.$watch(vm,key,handler)
}

可见,其实核心**就是给每个key 生成一个Watcher实例,来监听key的值的变化。

8. computed 实现

想要写computed,必须先知道computed 是有缓存的。

先来初始化computed

function initComputed(vm,computed) {
    let watchers = vm._watcherComputed = Object.create(null)
    for(let key in computed){
        let userDef = computed[key]
        watchers[key] = new Watcher(vm,userDef,()=>{},{lazy:true})
    }
}

可见,是先生成一个__watcherComputed的空对象挂载都vm里,
然后遍历computed,给每个computed 生成一个Watcher实例,一个key对应一个Wacther实例。
然后保存到_watcherComputed里。

现在修改一下watcher,我们每new Watcher的时候就计算好key对应的值。然后保存在Watcher实例里。

现在我们不希望自动调用Watcher里的get方法。当 computed的值改变时,再执行get,也就是computed的所有数据依赖有改变的时候再执行get()。

class Watcher {
    constructor(vm,exprOrFn,cb = ()=>{},opts){
        this.lazy = opts.lazy
        this.dirty = this.lazy
        if (typeof exprOrFn === 'function'){
            this.getter = exprOrFn
        }else{
            // 现在exprOrFn是我们传进来的key
            this.getter = function () {
                return util.getValue(vm,exprOrFn)
            }
        }
        this.value = this.lazy? undefined : this.get() //获得老值oldValue
        // 创建一个watcher,默认调用此方法
    }

当用户取值的时候,我们将key定义到vm上,并且返回value

function createComputedGetter(vm,key) {
    let watcher = vm._watcherComputed[key]
    return function () {
        if (watch) {
            if (watcher.dirty){
                // 页面取值的时候,dirty如果为true,就会调用get方法计算
                watcher.evalValue()
            }
            return watcher.value
        }
    }
}
function initComputed(vm,computed) {
    let watchers = vm._watcherComputed = Object.create(null)
    for(let key in computed){
        let userDef = computed[key]
        watchers[key] = new Watcher(vm,userDef,()=>{},{lazy:true})

        // 当用户取值的时候,我们将key定义到vm上
        Object.defineProperty(vm,key,{
            get:createComputedGetter(vm,key)
        })
    }

evalValue方法的实现非常简单

    evalValue(){
        this.value = this.get()
        this.dirty = false
    }

现在已经成功获得computed返回的值了,

接下来,要实现的是,当computed的依赖列表中,有变化的话,就要把dirty设置为true,重新赋予value新值

当computed里的依赖列表有变化时,就通知watcher.update。需要把dirty改为true。

    update(){
        // this.get()
        // 批量更新, 防止重复渲染
        if (this.lazy){ // 如果是计算属性
            this.dirty = true
        }else{
            queueWacther(this)
        }
    }

现在解决依赖收集的问题

function createComputedGetter(vm,key) {
    let watcher = vm._watcherComputed[key]
    return function () {
        if (watcher) {
            if (watcher.dirty){
                // 页面取值的时候,dirty如果为true,就会调用get方法计算
                watcher.evalValue()
            }
            if (Dep.target){
                watcher.depend()
            }
            return watcher.value
        }
    }
}
    depend(){
        let i = this.deps.length
        while(i--){
            this.deps[i].depend()
        }
    }

源码地址:https://github.com/peigexing/myvue

研究大佬写的倒计时组件(Vue),学到了不少东西


highlight: a11y-dark
theme: jzman

一、前言

入职的第一个需求是跟着一位前端大佬一起完成的一个活动项目。

由于是一起开发,当然不会放过阅读大佬的代码的机会。

因为我的页面中需要使用到倒计时功能,发现大佬的已经写了个现成的倒计时组件,于是直接就拿过来用了。

传个参数就实现了功能的感觉真是太棒了。项目完成后,就膜拜了一下大佬的倒计时组件的代码。真是让我学到了不少。列举如下:

  1. 计时器为什么要用setTimeout而不用setInterval
  2. 为什么不直接将剩余时间-1。
  3. 如何将所需要的时间返回出去(有可能我只需要分钟和秒数,那就只返回分钟和秒数,也有可能我全都要)。
  4. 不确定接口返回的是剩余时间还是截止日期,该怎么同时兼容这两种情况。
  5. 不确定接口返回的时间是秒还是毫秒单位。

好了,你可能不太理解这些问题,但是没关系,看完下面的解释,相信你会豁然开朗。

二、开始手操

1. 先创建一个vue组件

<template>
  <div class="_base-count-down">
  </div>
</template>
<script>

export default {
  data: () => ({
   
  }),
  props: {
    
  },
};
</script>
<style lang='scss' scoped>

</style>

2. 实现基本的倒计时组件

接下来,假设接口获得的是一个剩余时间。

将剩余时间time传入这个倒计时组件,由于time可能是秒为单位的,也有可能是毫秒为单位的,所以我们需要在传入time的是有也传入一个isMilliSecond来告诉倒计时组件这个time是毫秒还是秒为单位的。如下代码中的props所示。

<template>
  <div class="_base-count-down">
  </div>
</template>
<script>

export default {
  data: () => ({
  }),
  props: {
    time: {
      type: [Number, String],
      default: 0
    },
    isMilliSecond: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    duration() {
      const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
      return time;
    }
  },
};
</script>
<style lang='scss' scoped>

</style>

computed中的duration是将time进行转化的结果,不管time是毫秒还是秒,都转化为秒
不知道你注意到了没有:+this.time。为什么要在前面加个‘+’号。这点很值得我们学习,因为接口返回的一串数字有时候是字符串的形式,有时候是数字的形式(不能过分相信后端同学,必须自己做好防范)。所以通过前面加个‘+’号 通通转化为数字。现在的duration就是转化后的time啦!

我们获得duration之后就可以开始倒计时了

<template>
  <div class="_base-count-down">
  </div>
</template>
<script>

export default {
  data: () => ({
  }),
  props: {
    time: {
      type: [Number, String],
      default: 0
    },
    isMilliSecond: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    duration() {
      const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
      return time;
    }
  },
  // 新增代码:
  mounted() {
    this.countDown();
  },
  methods: {
    countDown() {
      this.getTime(this.duration);
    },
  }
};
</script>
<style lang='scss' scoped>

</style>

在这里创建了一个countDown方法,表示开始倒计时的意思,已进入页面就开始执行countdown方法。

countDown方法调用了getTime方法,getTime需要传入duration这个参数,也就是我们获得的剩余时间。

现在来实现一下这个方法。

<template>
  <div class="_base-count-down">
    还剩{{day}}天{{hours}}:{{mins}}:{{seconds}}
  </div>
</template>
<script>

export default {
  data: () => ({
    days: '0',
    hours: '00',
    mins: '00',
    seconds: '00',
    timer: null,
  }),
  props: {
    time: {
      type: [Number, String],
      default: 0
    },
    isMilliSecond: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    duration() {
      const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
      return time;
    }
  },
  mounted() {
    this.countDown();
  },
  methods: {
    countDown() {
      this.getTime(this.duration);
    },
    // 新增代码:
    getTime(duration) {
      this.timer && clearTimeout(this.timer);
      if (duration < 0) {
        return;
      }
      const { dd, hh, mm, ss } = this.durationFormatter(duration);
      this.days = dd || 0;
      this.hours = hh || 0;
      this.mins = mm || 0;
      this.seconds = ss || 0;
      this.timer = setTimeout(() => {
        this.getTime(duration - 1);
      }, 1000);
    }
  }
};
</script>
<style lang='scss' scoped>

</style>

可以看到,getTime的目的就是获得 days,hours,mins,seconds,然后显示到html上,并且通过定时器实时来刷新days,hours,mins,seconds这个几个值。从而实现了倒计时。很简单,有木有?

durationFormatter是一个将duration转化成天数,小时,分钟,秒数的方法,很简单,可以看下它的具体实现。

durationFormatter(time) {
  if (!time) return { ss: 0 };
  let t = time;
  const ss = t % 60;
  t = (t - ss) / 60;
  if (t < 1) return { ss };
  const mm = t % 60;
  t = (t - mm) / 60;
  if (t < 1) return { mm, ss };
  const hh = t % 24;
  t = (t - hh) / 24;
  if (t < 1) return { hh, mm, ss };
  const dd = t;
  return { dd, hh, mm, ss };
},

好了,问题开始来了!!

3. 为什么要用setTimeout来模拟setInterval的行为

这里用setInerval不是更方便吗?

setTimeout(function(){··· }, n); // n毫秒后执行function
setInterval(function(){··· }, n); // 每隔n毫秒执行一次function

可以看看setInterval有什么缺点:

再次强调,定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。

setInterval(function, N)  
//即:每隔N秒把function事件推到消息队列中

上图可见,setInterval每隔100ms往队列中添加一个事件;100ms后,添加T1定时器代码至队列中,主线程中还有任务在执行,所以等待,some event执行结束后执行T1定时器代码;又过了100ms,T2定时器被添加到队列中,主线程还在执行T1代码,所以等待;又过了100ms,理论上又要往队列里推一个定时器代码,但由于此时T2还在队列中,所以T3不会被添加,结果就是此时被跳过;这里我们可以看到,T1定时器执行结束后马上执行了T2代码,所以并没有达到定时器的效果。

综上所述,setInterval有两个缺点:

  1. 使用setInterval时,某些间隔会被跳过;
  2. 可能多个定时器会连续执行;

可以这么理解:每个setTimeout产生的任务会直接push到任务队列中;而setInterval在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)

因而我们一般用setTimeout模拟setInterval,来规避掉上面的缺点。

4. 为什么要clearTimeout(this.timer)

第二问:为什么要有this.timer && clearTimeout(this.timer);这一句?

假设一个场景:

如图所示,在倒计时的父组件中,有两个按钮,点击活动一就会传入活动一的剩余时间,点击活动二,就会传入活动二的时间。

如果此时倒计时组件正在做活动一的倒计时,然后点击活动二,就要会马上传入新的time,这个时候就需要重新计时。当然,这里并不会重新计时,因为组件的mounted只会执行一次。也就是说this.countDown();只会执行一次,也就是说this.getTime(this.duration);只会执行一次,因此duration还是活动一的时间,怎么办呢?watch派上用场了。

我们来监听duration,如果发现duration变化,说明新的时间time传入组件,这时就要重新调用this.countDown()。

代码如下:

<template>
  <div class="_base-count-down">
    还剩{{day}}天{{hours}}:{{mins}}:{{seconds}}
  </div>
</template>
<script>

export default {
  data: () => ({
    days: '0',
    hours: '00',
    mins: '00',
    seconds: '00',
    timer: null,
  }),
  props: {
    time: {
      type: [Number, String],
      default: 0
    },
    isMilliSecond: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    duration() {
      const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
      return time;
    }
  },
  mounted() {
    this.countDown();
  },
  // 新增代码:
  watch: {
    duration() {
      this.countDown();
    }
  },
  methods: {
    countDown() {
      this.getTime(this.duration);
    },
    durationFormatter(){...}
    getTime(duration) {
      this.timer && clearTimeout(this.timer);
      if (duration < 0) {
        return;
      }
      const { dd, hh, mm, ss } = this.durationFormatter(duration);
      this.days = dd || 0;
      this.hours = hh || 0;
      this.mins = mm || 0;
      this.seconds = ss || 0;
      this.timer = setTimeout(() => {
        this.getTime(duration - 1);
      }, 1000);
    }
  }
};
</script>
<style lang='scss' scoped>

</style>

好了,但是并没有解释上面提出的那个问题:为什么要有this.timer && clearTimeout(this.timer);这一句?

这样,假设现在页面显示的是活动一的时间,这时,执行到setTimeout,在一秒后就会把setTimeout里的回调函数放到任务队列中,注意是一秒后哦!这时,然而,在这一秒的开头,我们点击了活动二按钮,这时候的活动二的时间就会传入倒计时组件中,然后触发countDown(),也就调用this.getTime(this.duration);,然后执行到setTimeout,也会一秒后把回调函数放到任务队列中。

这时,任务队列中就会有两个setTimeout的回调函数了。等待一秒过去,两个回调函数相继执行,我们就会看到页面上的时间一下子背减了2,实际上是很快速地进行了两遍减1的操作。

这就是为什么要添加上this.timer && clearTimeout(this.timer);这一句的原因了。就是要把上一个setTimeout清除掉。

5. 使用 diffTime

当你认为这是一个完美的组件的时候,你想把这个组件用到项目上,假设你也确实用了,而且还上线了,确发现出现了个大问题:当页面打开的时候,倒计时开始了,时间是 还剩1天12:25:25,然后有人给你发微信,你马上切换到微信,回复消息后切回浏览器,发现倒计时时间却还是还剩1天12:25:25。你慌了:你写的代码出现bug了!

这是怎么回事?

出于节能的考虑, 部分浏览器在进入后台时(或者失去焦点时), 会将 setTimeout 等定时任务暂停
待用户回到浏览器时, 才会重新激活定时任务

说是暂停, 其实应该说是延迟, 1s 的任务延迟到 2s, 2s 的延迟到 5s, 实际情况因浏览器而异。

原来如此,看来不能每次都只是减1这么简单了(毕竟你把浏览器切到后台之后setTimeout就冷却了,等几秒后切回,然后执行setTimeout,只是减了一秒而已)。

所以我们需要改写一下getTime方法。

<template>
  <div class="_base-count-down">
    还剩{{day}}天{{hours}}:{{mins}}:{{seconds}}
  </div>
</template>
<script>

export default {
  data: () => ({
    days: '0',
    hours: '00',
    mins: '00',
    seconds: '00',
    timer: null,
    curTime: 0,// 新增代码:
  }),
  props: {
    time: {
      type: [Number, String],
      default: 0
    },
    isMilliSecond: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    duration() {
      const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
      return time;
    }
  },
  mounted() {
    this.countDown();
  },
  
  watch: {
    duration() {
      this.countDown();
    }
  },
  methods: {
    countDown() {
      // 新增代码:
      this.curTime = Date.now();
      this.getTime(this.duration);
    },
    durationFormatter(){...}
    getTime(duration) {
      this.timer && clearTimeout(this.timer);
      if (duration < 0) {
        return;
      }
      const { dd, hh, mm, ss } = this.durationFormatter(duration);
      this.days = dd || 0;
      this.hours = hh || 0;
      this.mins = mm || 0;
      this.seconds = ss || 0;
      this.timer = setTimeout(() => {
        // 新增代码:
        const now = Date.now();
        const diffTime = Math.floor((now - this.curTime) / 1000);
        this.curTime = now;
        this.getTime(duration - diffTime);
      }, 1000);
    }
  }
};
</script>
<style lang='scss' scoped>

</style>

可以看到,我们在三个位置添加了新的代码。

首先在data了添加了curTime这个变量,然后在执行countDown的时候给curTime赋值Date.now(),也就是当前的时刻,也就是显示在页面上的那个时刻。

然后看修改的第三处代码。可以看到是将-1改成了-diffTime

now 是 setTimeout的回调函数执行的时候的那个时刻。

因而 diffTime 则 表示 当前这个setTimeout的回调函数执行的时刻距离上 页面上的剩余时间上一次变化的时间段。其实也就是 当前这个setTimeout的回调函数执行的时刻距离上 一个setTimeout的回调函数执行的时刻时间段。

可能你还是不太能理解diffTime。举个例子:

你打开了这个倒计时页面,于是执行了countDown,也就是说要执行getTime这个方法了。也就是会马上执行下列的代码。

this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;

执行完这些代码页面上就会出现剩余时间。

this.curTime = Date.now(); 就记录下了此刻的时间点。

然后一秒后执行setTimeout里的回调函数:

const now = Date.now(); 记录当前这个setTimeout的回调函数执行的时间点。

const diffTime = Math.floor((now - this.curTime) / 1000); 记录当前这个setTimeout的回调函数执行的时间点距离页面上开始 渲染 剩余时间的 这一段时间。其实此时的diffTime就是=1。

然后this.curTime = now; 将curTime的值变成当前这个setTimeout的回调函数执行的时间点。

this.getTime(duration - diffTime); 其实就是this.getTime(duration - 1);

然后又执行getTime,就会重新执行下面的代码,有渲染了新的剩余时间。

this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;

然后一秒后又要执行setTmieout的回调函数,在这一秒还没结束的时候,我们将浏览器切到后台,此时setTimeout冷却了。等5秒后再切回。于是setTmieout的回调函数才得以执行。

这时const now = Date.now(); 记录当前这个setTimeout的回调函数执行的时间点。

而curTime是上一个setTimeout的回调函数执行的时间。

所以const diffTime = Math.floor((now - this.curTime) / 1000);实际上,diffTime的值就是5秒。

因而this.getTime(duration - diffTime); 其实就是this.getTime(duration - 5);

这样就完美解决了因为浏览器切到后台,导致剩余时间不变的问题。

6. 添加新功能:可以传入到期时间。

之前是只能传入剩余时间的,现在希望也支持传入到期时间。

只需要改动一下duration就好了。

  computed: {
    duration() {
      if (this.end) {
        let end = String(this.end).length >= 13 ? +this.end : +this.end * 1000;
        end -= Date.now();
        return end;
      }
      const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
      return time;
    }
  },

判断传入的end的长度是否大于13来判断是秒还是毫秒。轻松!

7. 添加新功能:可以选择要显示的内容,例如只显示秒,或者只显示小时。

只需要改动一下html:

<template>
  <div class="_base-count-down no-rtl">
    <div class="content">
      <slot v-bind="{
        d: days, h: hours, m: mins, s: seconds,
        hh: `00${hours}`.slice(-2),
        mm: `00${mins}`.slice(-2),
        ss: `00${seconds}`.slice(-2),
      }"></slot>
    </div>
  </div>
</template>

很巧妙有没有,只需要用插槽,就把倒计时组件,也就是把子组件的值传递给父组件了。

看看父组件是怎么使用这个组件的。

<base-counter v-slot="timeObj" :time="countDown">
  <div class="count-down">
    <div class="icon"></div>
    {{timeObj.d}}天{{timeObj.hh}}小时{{timeObj.mm}}分钟{{timeObj.ss}}秒
  </div>
</base-counter>

看,如此巧妙又简单。

发现00${hours}.slice(-2) 这种写法也很值得学习。以前在获得到分钟的时候,要手动判断获得的分钟是两位数还是一位数,如果是一位数的话就要在前面手动补上0。就像下面的代码:

var StartMinute = startDate.getMinutes().toString().length >= 2 ? startDate.getMinutes() : '0' + startDate.getHours();

00${hours}.slice(-2) 则不用判断,先补上0再说,然后再从后面往前截取两位。

到此。

一个完美的倒计时组件就完成了。

三、学习总结

  1. 明白了setInterval的缺点以及用setTimeout代替setInterval。
  2. 学到了“+”,操作,不管三七二十一,将接口得到的长串数字转化为数字保平安。
  3. 利用clearTimeout来清除掉之前的计时器,以防止造成影响。
  4. 学会使用v-slot来子传父传值
  5. 学会一个倒计时组件,为了以后方便cv操作。把组件完整代码贴上:

最后

公众号《前端阳光》,回复加群,欢迎加入技术交流群以及内推群。

<template>
  <div class="_base-count-down no-rtl">
    <div class="content">
      <slot v-bind="{
        d: days, h: hours, m: mins, s: seconds,
        hh: `00${hours}`.slice(-2),
        mm: `00${mins}`.slice(-2),
        ss: `00${seconds}`.slice(-2),
      }"></slot>
    </div>
  </div>
</template>
<script>
/* eslint-disable object-curly-newline */

export default {
  data: () => ({
    days: '0',
    hours: '00',
    mins: '00',
    seconds: '00',
    timer: null,
    curTime: 0
  }),
  props: {
    time: {
      type: [Number, String],
      default: 0
    },
    refreshCounter: {
      type: [Number, String],
      default: 0
    },
    end: {
      type: [Number, String],
      default: 0
    },
    isMiniSecond: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    duration() {
      if (this.end) {
        let end = String(this.end).length >= 13 ? +this.end : +this.end * 1000;
        end -= Date.now();
        return end;
      }
      const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
      return time;
    }
  },
  mounted() {
    this.countDown();
  },
  watch: {
    duration() {
      this.countDown();
    },
    refreshCounter() {
      this.countDown();
    }
  },
  methods: {
    durationFormatter(time) {
      if (!time) return { ss: 0 };
      let t = time;
      const ss = t % 60;
      t = (t - ss) / 60;
      if (t < 1) return { ss };
      const mm = t % 60;
      t = (t - mm) / 60;
      if (t < 1) return { mm, ss };
      const hh = t % 24;
      t = (t - hh) / 24;
      if (t < 1) return { hh, mm, ss };
      const dd = t;
      return { dd, hh, mm, ss };
    },
    countDown() {
      // eslint-disable-next-line no-unused-expressions
      this.curTime = Date.now();
      this.getTime(this.duration);
    },
    getTime(time) {
      // eslint-disable-next-line no-unused-expressions
      this.timer && clearTimeout(this.timer);
      if (time < 0) {
        return;
      }
      // eslint-disable-next-line object-curly-newline
      const { dd, hh, mm, ss } = this.durationFormatter(time);
      this.days = dd || 0;
      // this.hours = `00${hh || ''}`.slice(-2);
      // this.mins = `00${mm || ''}`.slice(-2);
      // this.seconds = `00${ss || ''}`.slice(-2);
      this.hours = hh || 0;
      this.mins = mm || 0;
      this.seconds = ss || 0;
      this.timer = setTimeout(() => {
        const now = Date.now();
        const diffTime = Math.floor((now - this.curTime) / 1000);
        const step = diffTime > 1 ? diffTime : 1; // 页面退到后台的时候不会计时,对比时间差,大于1s的重置倒计时
        this.curTime = now;
        this.getTime(time - step);
      }, 1000);
    }
  }
};
</script>
<style lang='scss' scoped>
@import '~@assets/css/common.scss';

._base-count-down {
  color: #fff;
  text-align: left;
  position: relative;
  .content {
    width: auto;
    display: flex;
    align-items: center;
  }
  span {
    display: inline-block;
  }
  .section {
    position: relative;
  }
}
</style>

纯css就能实现可点击切换的轮播图,feel起来很丝滑

前言

轮播图经常会在项目里用到,但是实际上用到的轮播图都是比较简单的,没有复杂的特效,这个时候如果去引入swiper那些库的话,未免就有点杀鸡焉用牛刀了。

所以不如自己手写一个,而今天我要分享的一种写法也是我最近才发现的,发现写起来真的是很丝滑,只纯css就实现了呢!

HTML <label> 标签的 for 属性的用法及作用

for 属性规定 label 与哪个表单元素绑定,label的for属性要与绑定表单元素(input)的ID对应。绑定完成后可以通过点击label触发表单元素的默认属性。通俗的讲就是你绑定完了点lebel就相当于点击表单元素(input)。

<form>
  <label for="male">Male</label>
  <input type="radio" name="sex" id="male" />
  <br />
  <label for="female">Female</label>
  <input type="radio" name="sex" id="female" />
</form>

开始实现吧

预览地址:https://sunny-lucking.github.io/howToBuiMySwiper/myswiper.html

源码地址:https://github.com/Sunny-lucking/howToBuiMySwiper/blob/main/myswiper.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>我的轮播图</title>
  <style>
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
    }

    ul.slides {
      position: relative;
      width: 600px;
      height: 280px;
      list-style: none;
      margin: 0;
      padding: 0;
      background-color: #eee;
    }

    li.slide {
      margin: 0;
      padding: 0;
      width: inherit;
      height: inherit;
      position: absolute;
      top: 0;
      left: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      font-family: Helvetica;
      font-size: 120px;
      color: #fff;
      transition: .5s transform ease-in-out;
    }

    .slide:nth-of-type(1) {
      background-color: #F2E205;
    }

    .slide:nth-of-type(2) {
      background-color: #F25C05;
      left: 100%;
    }
    .slide:nth-of-type(3) {
      background-color: #495F8C;
      left: 200%;
    }
  </style>
</head>

<body>
  <ul class="slides">
    <li class="slide">1</li>
    <li class="slide">2</li>
    <li class="slide">3</li>
  </ul>
</body>

</html>

首先先写了所需要的三个子元素。分别给了三种颜色。

接下来。最外层加上overflow: hidden,让只显示一个slide子元素

ul.slides {
      position: relative;
      width: 600px;
      height: 280px;
      list-style: none;
      margin: 0;
      padding: 0;
      background-color: #eee;
      overflow: hidden;
    }

接下来,加上label和input起到控制切换的效果

html

<body>
  <ul class="slides">
    <input type="radio" id="control-1" name="control" checked>
    <input type="radio" id="control-2" name="control">
    <input type="radio" id="control-3" name="control">
    <li class="slide">1</li>
    <li class="slide">2</li>
    <li class="slide">3</li>
    <div class="controls-visible">
      <label for="control-1"></label>
      <label for="control-2"></label>
      <label for="control-3"></label>
    </div>
  </ul>
</body>

css

input[type="radio"] {
  position: relative;
  z-index: 100;
  display: none;
}

.controls-visible {
  position: absolute;
  width: 100%;
  bottom: 12px;
  text-align: center;
}

.controls-visible label {
  display: inline-block;
  width: 10px;
  height: 10px;
  background-color: #fff;
  border-radius: 50%;
  margin: 0 3px;
  border: 2px solid #fff;
}

.slides input[type="radio"]:nth-of-type(1):checked ~ .controls-visible label:nth-of-type(1) {
  background-color: #333;
}

.slides input[type="radio"]:nth-of-type(2):checked ~ .controls-visible label:nth-of-type(2) {
  background-color: #333;
}

.slides input[type="radio"]:nth-of-type(3):checked ~ .controls-visible label:nth-of-type(3) {
  background-color: #333;
}

这里利用input和label来模拟轮播图的pagination分页功能。label模拟的是圆点,然后吧radio输入框隐藏了。radio放在最前面的目的是为了用了控制后面的slides 和controls的 样式

现在实现点击label切换轮播图的效果

.slides input[type="radio"]:nth-of-type(1):checked ~ .slide {
  transform: translatex(0%);
}

.slides input[type="radio"]:nth-of-type(2):checked ~ .slide {
  transform: translatex(-100%);
}

.slides input[type="radio"]:nth-of-type(3):checked ~ .slide {
  transform: translatex(-200%);
}

可以看到已经非常地简单就实现了点击lebel切换轮播图的效果。

当然,我们要实现一个上下页切换的功能也非常简单

image

我们添加三组navigator,一页页面对应一组

<body>
  <ul class="slides">
    <input type="radio" id="control-1" name="control" checked>
    <input type="radio" id="control-2" name="control">
    <input type="radio" id="control-3" name="control">
    <div class="navigator slide-1">
      <label for="control-3"></label>
      <label for="control-2"></label>
    </div>

    <div class="navigator slide-2">
      <label for="control-1"></label>
      <label for="control-3"></label>
    </div>

    <div class="navigator slide-3">
      <label for="control-2"></label>
      <label for="control-1"></label>
    </div>
    <li class="slide">1</li>
    <li class="slide">2</li>
    <li class="slide">3</li>
    <div class="controls-visible">
      <label for="control-1"></label>
      <label for="control-2"></label>
      <label for="control-3"></label>
    </div>
  </ul>
</body>

我们要把不属于当前的那一页的navigator隐藏掉,所以用display:none,当选中对应的页面的时候,再让它显示出来,所以可以这样实现

    .navigator {
      position: absolute;
      top: 50%;
      transform: translatey(-50%);
      width: 100%;
      z-index: 100;
      padding: 0 20px;
      display: flex;
      justify-content: space-between;
      box-sizing: border-box;
      display: none;
    }

    .navigator {
      font-size: 32px;
      color #333333;
    }

    .slides input[type="radio"]:nth-of-type(1):checked~.navigator:nth-of-type(1) {
      display: flex;
    }

    .slides input[type="radio"]:nth-of-type(2):checked~.navigator:nth-of-type(2) {
      display: flex;
    }

    .slides input[type="radio"]:nth-of-type(3):checked~.navigator:nth-of-type(3) {
      display: flex;
    }

image

可以看到,又轻而易举就实现了点击切换上下页的功能,太腻害了。

面试题汇总

【以下是我一边阅读《红宝书》《ES6入门标准》《高性能javascript》《javascript忍者秘籍》《JavaScript语言精粹》以及查阅网上资料,一边做的总结,欢迎补充与纠正】

一、关于【let const】十问

1.下面哪个会报错,为什么

let a;
const a;

答案:

let a; //不报错
const a; //报错

const 声明的常量不得改变值。这意味着, const 一旦声明常量,就必须立即初始化,不 能留到以后赋值

2. 如何在ES5环境下实现let

对于这个问题,我们可以直接查看babel转换前后的结果,看一下在循环中通过let定义的变量是如何解决变量提升的问题

babel在let定义的变量前加了道下划线,避免在块级作用域外访问到该变量,除了对变量名的转换,我们也可以通过自执行函数来模拟块级作用域

3. 如何在ES5环境下实现const

实现const的关键在于Object.defineProperty()这个API,这个API用于在一个对象上增加或修改属性。通过配置属性描述符,可以精确地控制属性行为。Object.defineProperty() 接收三个参数:

Object.defineProperty(obj, prop, desc)

对于const不可修改的特性,我们通过设置writable属性来实现

4.const的本质是什么

const 实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。 对于简单类型的数据(数值、字符串、布尔值〉而言,值就保存在变量指向的内存地址中,因 此等同于常量。但对于复合类型的数据(主要是对象和数组)而言,变量指向的内存地址保存 的只是一个指针, const 只能保证这个指针是固定的,至于它指向的数据结构是不是可变的, 这完全不能控制。 因此,将一个对象声明为常量时必须非常小心。

5.既然const不能保证 对象属性不被修改,那该怎么解决这个问题呢?

如果真的想将对象冻结,应该使用 Object.freeze 方法。

查看Object.freeze的用法与描述

Object.freeze()方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。

6.但是这样就能保证 对象冻结了吗?

不,因为Object.freeze()只能冻结基本数据类型的属性,若是属性还是引用类型的话,那就冻结不了了,因此,除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

7.既然你提到了Object.freeze,你能说下他的原理 吗

Object.freeze()的功能主要就是使对象属性冻结起来,这个功能跟 上面模拟const 类似,可以用Object.definedProperty()

Object.definedProperty()方法可以定义对象的属性的特性。如可不可以删除、可不可以修改、访问这个属性的时候添油加醋等等。。 所以,我们可以这样写:

8.但是这样就完整了没,你有没有发现问题呢?

emmmmm,的确还有个问题,就是Object.definedProperty()只是设置了 对象的属性,也就是说,要是此时我使用 给对象添加属性,还是可以的,因此还有个问题要解决,就是使对象不能 添加新的属性,而要限制 拓展功能,我想到了Object.seal(),

查看Object.seal介绍:

**Object.seal()**方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。

因此用Object.seal()和Object.definedProperty()就可以实现Object.freeze()

9.ES6之前真的没有块级作用域吗?

可以看下这道题,答案是什么

正确答案:内部是 21,外部是 1;

这个玄妙之处确实就在这个块级作用域 if 里面。

假如我们去掉 if 看题。

这道题估计没人好意思去问了,毫无疑问,输出的 a 都是 21 啊。

实际上,首先,if 里面的 function a(){} 会声明提升,将声明" var a" 移到函数级作用域最前面,将函数定义移到块级作用域最前面,预解析如下:

函数本身是【 定义函数名变量 指针指向 函数内存块】。

函数提升是在块级作用域,但是函数名变量是函数级别的作用域。所以在块级的函数定义(原始代码函数的声明位置)的时候,会将函数名变量同步到函数级作用域,实际上,只有这个时候,在块级作用域外才能访问到函数定义。

预解析如下:

关于此题查看更多

10.下面代码输出情况

可见在全局作用域中 var 声明的变量 会被挂载到 window中,而let 不会 而我在读一本关于es6书的时候 看到这一段话

ES6 将全局方法 parse!nt ()和 parseFloat ()移植到了 Number 对象上面,行为完全 >保持不变 这样做的目的是逐步减少全局性方法,使得语言逐步模块化。

是不是let 也想这样呢?当然这只是个人猜想,没什么理论依旧,您也不必往心里去

补充:后来在书里看到这段,好像符合我的猜想

顶层对象的属性与全局变量相关,被认为是 JavaScript 语言中最大的设计败笔之一。这样 的设计带来了几个很大的问题:首先,无法在编译时就提示变量未声明的错误,只有运行时才 能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的〉:其次,程序员 很容易不知不觉地就创建全局变量(比如打字出错〉:最后,顶层对象的属性是到处都可以读写 的,这非常不利于模块化编程。另一方面, window 对象有实体含义,指的是浏览器的窗口对 象,这样也是不合适的。

ES6 为了改变这一点, 一方面规定,为了保持兼容性, var 命令和 function 命令声明的 全局变量依旧是顶层对象的属性;另一方面规定, let 命令、 const 命令、 class 命令声明的 全局变量不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性 隔离。

二、关于【BOM】十问

第一问:请介绍BOM有哪些对象

第一次被问到时,只知道window和navigator

  1. window:BOM的核心对象是window对象,它表示浏览器的一个实例。
  2. avigator:navigator 对象包含有关访问者浏览器的信息。
<div id="example"></div>
<script>
  txt = "<p>浏览器代号: " + navigator.appCodeName + "</p>";
  txt+= "<p>浏览器名称: " + navigator.appName + "</p>";
  txt+= "<p>浏览器版本: " + navigator.appVersion + "</p>";
  txt+= "<p>启用Cookies: " + navigator.cookieEnabled + "</p>";
  txt+= "<p>硬件平台: " + navigator.platform + "</p>";
  txt+= "<p>用户代理: " + navigator.userAgent + "</p>";
  txt+= "<p>用户代理语言: " + navigator.systemLanguage + "</p>";
  document.getElementById("example").innerHTML=txt;
</script>
  1. window.screen 对象包含有关用户屏幕的信息。
<body>

<h3>你的屏幕:</h3>
<script>
  document.write("总宽度/高度: ");
  document.write(screen.width + "*" + screen.height);
  document.write("<br>");
  document.write("可用宽度/高度: ");
  document.write(screen.availWidth + "*" + screen.availHeight);
  document.write("<br>");
  document.write("色彩深度: ");
  document.write(screen.colorDepth);
  document.write("<br>");
  document.write("色彩分辨率: ");
  document.write(screen.pixelDepth);
</script>

</body>
  1. location: 对象用于获得当前页面的地址 (URL),并把浏览器重定向到新的页面。

一些实例:

location.hostname 返回 web 主机的域名
location.pathname 返回当前页面的路径和文件名
location.port 返回 web 主机的端口 (80  443
location.protocol 返回所使用的 web 协议(http:  https:
location.assign(url)  加载 URL 指定的新的 HTML 文档。 就相当于一个链接,跳转到指定的url,当前页面会转为新页面内容,可以点击后退返回上一个页面。
location.replace(url)  通过加载 URL 指定的文档来替换当前文档 ,这个方法是替换当前窗口页面,前后两个页面共用一个窗口,所以是没有后退返回上一页的
  1. history 对象包含浏览器的历史。
history.go(0);  // go() 里面的参数为0,表示刷新页面
history.go(-1);  // go() 里面的参数表示跳转页面的个数 例如 history.go(-1) 表示后退一个页面
history.go(1);  // go() 里面的参数表示跳转页面的个数 例如 history.go(1) 表示前进一个页面
history.back() //方法加载历史列表中的前一个 URL。
history.forward() //方法加载历史列表中的下一个 URL。

第二问:以下代码都会输出什么呢?

var age = 29;
window.color = "red";

delete window.age;

delete window.color;

alert(window.name)
alert(window.age)
alert(window.color)

答案:

var age = 29;
window.color = "red";
// 在IE<9时抛出错误,在其他所有浏览器中都返回false
delete window.age;
// 在IE<9时抛出错误,在其他所有浏览器中都返回true
delete window.color;

alert(window.name) // ''
alert(window.age) //29
alert(window.color) //undefined

使用var语句添加的window属性有一个名为[[configurable]]的特性,这个特性的值被设置为false,因此这样定义的属性不可以通过delete操作符删除.
window对象中本身就有个name属性,window.name 表示当前window的名称
age没被删除,所以输出29,而color被删除了。

第三问:你知道间接调用和超时调用吗?

javascript是单线程语言,但它允许通过设置超时值setTimeout和间歇时间值setInterval来调度代码在特定的时刻执行。前者是在指定的时间过后执行代码,而后者则是每隔指定的时间就执行一次代码。

  1. 超时调用使用window对象的setTimeout()方法,它接受两个参数:要执行的代码 和 以毫秒表示的时间。第一个参数可以是包含javascript语句的字符串(不推荐使用),也可以是函数。调用setTimeout()之后,该方法会返回一个数值ID,表示超时调用。
 // 推荐
 setTimeout(function(){
      alert("Hello");
  },1000);
  
  // 不推荐
  setTimeout("alert('Hello')",1000);
  1. 间歇调用与超时调用类似,只不过它会按照指定的时间间隔重复执行代码,直至间歇调用被取消或者页面被卸载。设置间歇调用的方法是setInterval(),它会接受的参数与setTimeout()相同:因为在不加干涉的情况下,间歇调用将会一直执行到页面卸载。(ps:建议少用setInterval(),可以用setIimeout()代替)

第四问 你刚刚说建议少用setInterval(),可以用setIimeout()代替,为什么呢?

对于这道题,要有 事件循环机制的只是储备,建议先看看:这一次,彻底弄懂 JavaScript 执行机制(别还不知道什么是宏任务,什么是微任务)

之所以说要替换,是因为setInterval的缺点

再次强调,定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。

setInterval(function, N)  
//即:每隔N秒把function事件推到消息队列中

上图可见,setInterval每隔100ms往队列中添加一个事件;100ms后,添加T1定时器代码至队列中,主线程中还有任务在执行,所以等待,some event执行结束后执行T1定时器代码;又过了100ms,T2定时器被添加到队列中,主线程还在执行T1代码,所以等待;又过了100ms,理论上又要往队列里推一个定时器代码,但由于此时T2还在队列中,所以T3不会被添加,结果就是此时被跳过这里我们还可以看到,T1定时器执行结束后马上执行了T2代码,所以并没有达到定时器的效果

综上所述,setInterval有两个缺点:

  • 使用setInterval时,某些间隔会被跳过;
  • 可能多个定时器会连续执行;
    可以这么理解:每个setTimeout产生的任务会直接push到任务队列中;而setInterval在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)。

因而我们一般用setTimeout模拟setInterval,来规避掉上面的缺点。

第五问:既然如此那该怎么用setTimeout模拟setInterval呢

setTimeout模拟setInterval,也可理解为链式的setTimeout。

setTimeout(function () {
    // 任务
    setTimeout(arguments.callee, interval);
}, interval)

上述函数每次执行的时候都会创建一个新的定时器,第二个setTimeout使用了arguments.callee()获取当前函数的引用,并且为其设置另一个定时器。好处:

  • 在前一个定时器执行完前,不会向队列插入新的定时器(解决缺点一)
  • 保证定时器间隔(解决缺点二)

警告:在严格模式下,第5版 ECMAScript (ES5) 禁止使用 arguments.callee()。当一个函数必须调用自身的时候, 避免使用 arguments.callee(), 通过要么给函数表达式一个名字,要么使用一个函数声明.

第六问:上面既然提到了hash和history,那就谈下两者的区别

hash

即地址栏 URL 中的 # 符号
hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。

history

利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法
它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。
通过history api,我们丢掉了丑陋的#,但是它也有个问题:不怕前进,不怕后退,就怕刷新,f5,(如果后端没有准备的话),因为刷新是实实在在地去请求服务器的。
在hash模式下,前端路由修改的是#中的信息,而浏览器请求时不会将 # 后面的数据发送到后台,所以没有问题。但是在history下,你可以自由的修改path,当刷新时,如果服务器中没有相应的响应或者资源,则会刷新出来404页面。

三、关于【ajax和json】十问

第一问:原生js写一个简单的ajax请求

典型的xhr建立ajax的过程。(涵盖了ajax的大部分内容)

  1. new一个xhr对象。(XMLHttpRequest或者ActiveXObject)
  2. 调用xhr对象的open方法。
  3. send一些数据。
  4. 对服务器的响应过程进行监听,来知道服务器是否正确得做出了响应,接着就可以做一些事情。比如获取服务器响应的内容,在页面上进行呈现。
//1、创建一个ajax对象 
var xhr = new XMLHttpRequest(); 

//3、绑定处理函数,我们写的代码,都在这里面 
xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
        if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
            console.log(xhr.responseText)
        } else {
            console.log("fail")
        }
    }
}

//2、进行初始化 
xhr.open('get','http://js.com/day6/ajax_get.php');


//4、发送请求
xhr.send(null);

注意点:

  1. 如果不需要通过请求头发送数据,必须传入null
  2. 为确保跨浏览器兼容性,建议xhr.onreadystatechange事件处理程序写在xhr.open前面
  3. setRequestHeader必须写在xhr.openxhr.send(null)之间

第二问:readyState各阶段的含义

  1. 未初始化,但是已经创建了XHR实例
  2. 调用了open()函数
  3. 已经调用了send()函数,但还未收到服务器回应
  4. 正在接受服务器返回的数据
  5. 完成响应

第三问:怎么终止请求

再接收到响应之前还可以调用obort()方法来取消异步请求

xhr.abort()

调用这个方法后,XHR对象会停止触发事件,而且也不再允许访问任何与响应有关的对象属性。在终止请求之后,还应该对XHR对象进行解引用操作。由于内存原因,不建议重用XHR对象

第四问:如何利用xhr做请求的进度条

另一个革新是添加了progress事件,这个时间会在浏览器接受新数据期间周期性的触发,而onprogress事件处理程序会接收到一个event对象,其target属性是XHR对象,但包含着两个额外的属性:position和totalSize。其中position表示已经接受的字节数,totleSize表示根据Content-Length响应头部确定的预期字节数。

xhr.onprogress = function (event) {
    var divStatus = document.getElementById("status");
    divStatus.innerHTML = "Received" + event.position + "of" + event.totalSize + "bytes";
};

第五问:谈一下跨域

1、JSONP跨域

jsonp的原理就是利用<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。

2、跨域资源共享(CORS)

  CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

浏览器将CORS跨域请求分为简单请求和非简单请求。

只要同时满足一下两个条件,就属于简单请求

(1)使用下列方法之一:

  • head
  • get
  • post

(2)请求的Heder是

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type: 只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain

不同时满足上面的两个条件,就属于非简单请求。浏览器对这两种的处理,是不一样的。

简单请求

  对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

非简单请求

  非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

预检请求

  预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。请求头信息里面,关键字段是Origin,表示请求来自哪个源。除了Origin字段,"预检"请求的头信息包括两个特殊字段。

3、nodejs中间服务器代理跨域
利用本地服务器(跟前端项目同协议,同域名,同端口)来代理转发,利用的是同源策略是只发生在浏览器,而两个服务端是不会出现跨域问题的。

webpack种配置跨域就是这个原理。

webpack.config.js部分配置:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

4、使用图片ping跨域

 图像Ping跨域请求技术是使用标签。一个网页可以从任何网页中加载图像,不用担心跨域不跨域。这也是在线广告跟踪浏览量的主要方式。也可以动态地创建图像,使用它们的 onload 和 onerror事件 处理程序来确定是否接收到了响应

  动态创建图像经常用于图像Ping:图像Ping是与服务器进行简单、单向的跨域通信的一种方式。 请求的数据是通过査询字符串形式发送的,而响应可以是任意内容,但通常是像素图或204响应。通过图像Ping,浏览器得不到任何具体的数据,但通过侦听load和error事件,它能知道响应是什么时候接收到的

图像Ping有两个主要的缺点,一是只能发送GET请求,二是无法访问服务器的响应文本。因此,图像Ping只能用于浏览器与服务器间的单向通信

第六问:JS跨域方案JSONP与CORS的各自优缺点以及应用场景?

两者优点与缺点大致互补,放在一块介绍:

JSONP的主要优势在于对浏览器的支持较好;虽然目前主流浏览器支持CORS,但IE10以下不支持CORS。

JSONP只能用于获取资源(即只读,类似于GET请求);CORS支持所有类型的HTTP请求,功能完善。(这点JSONP被玩虐,但大部分情况下GET已经能满足需求了)

JSONP的错误处理机制并不完善,我们没办法进行错误处理;而CORS可以通过onerror事件监听错误,并且浏览器控制台会看到报错信息,利于排查。

JSONP只会发一次请求;而对于复杂请求,CORS会发两次请求。

始终觉得安全性这个东西是相对的,没有绝对的安全,也做不到绝对的安全。毕竟JSONP并不是跨域规范,它存在很明显的安全问题:callback参数注入和资源访问授权设置。CORS好歹也算是个跨域规范,在资源访问授权方面进行了限制(Access-Control-Allow-Origin),而且标准浏览器都做了安全限制,比如拒绝手动设置origin字段,相对来说是安全了一点。
但是回过头来看一下,就算是不安全的JSONP,我们依然可以在服务端端进行一些权限的限制,服务端和客户端也都依然可以做一些注入的安全处理,哪怕被攻克,它也只能读一些东西。就算是比较安全的CORS,同样可以在服务端设置出现漏洞或者不在浏览器的跨域限制环境下进行攻击,而且它不仅可以读,还可以写。

应用场景:

如果你需要兼容IE低版本浏览器,无疑,JSONP。

如果你需要对服务端资源进行谢操作,无疑,CORS。

其他情况的话,根据自己的对需求的分析和对两者的理解来吧。

第七问:我要是硬要用CORS方法解决跨域呢?有没办法兼容IE低版本

大部分浏览器都已经实现了CORS(跨域资源共享)的规范,IE低版本浏览器却无法支持这一规范。

在ie8,9中,有XDomainRequest对象可以实现跨域请求。可以和xhr一起使用。这个对象拥有onerror,onload,onprogress,ontimeout四个事件,abort,open,send三个方法,contentType, responseText,timeout三个属性。具体参见XDomainRequest对象。

XDomainRequest对象有很多限制,例如只支持get、post方法、不能自定义请求的header头、不能携带cookie、只支持text/plain类型的内容格式等等。具体参见XDomainRequest对象限制。

因此虽然XdomainRequest作为ie8、9中的一种跨域手段,但是适用的业务场景还是比较局限的。

第八问:json.stringify()与json.parse()的区别

json.stringfy()将对象、数组转换成字符串;json.parse()将字符串转成json对象。

1.parse 用于从一个字符串中解析出json 对象

var str='{"name":"TMD","sex":"男","age":"26"}';

console.log(JSON.parse(str));//{name: "TMD", sex: "男", age: "26"}

2.stringify用于从一个对象解析出字符串

var o={a:1,b:2,c:3};

console.log(JSON.stringify(o));//{"a":1,"b":2,"c":3}

第九问:json.stringify()用于实现深拷贝时有什么缺点呢?

弊端:

1.如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式

2、如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;

3.如果obj里有RegExp(正则表达式的缩写)、Error对象,则序列化的结果将只得到空对象;

4、如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null

5、JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;

6、如果对象中存在循环引用的情况也无法正确实现深拷贝;

第十问:现在我要用json.stringify()用于实现深拷贝,而且对象里有undefined或者函数,Date,该怎么办呢?

查看文档,发现JSON.parse是可以传一个转换结果的函数的

 JSON.parse()方法也可以接收一个函数参数,在每个键值对儿上调用,这个函数被称为还原函数(reviver)。该函数接收两个参数,一个键和一个值,返回一个值

  如果还原函数返回undefined,则表示要从结果中删除相应的键;如果返回其他值,则将该值插入到结果中


一次可以判断 key是不是Date,是的话就转化下

var book = {
    "title": "javascript",
    "date": new Date(2016,9,1)
}
var jsonStr = JSON.stringify(book);
//'{"title":"javascript","date":"2016-09-30T16:00:00.000Z"}''
console.log(jsonStr)

var bookCopy = JSON.parse(jsonStr,function(key,value){
    if(key == 'date'){
        return new Date(value);
    }
    return value;
})
console.log(bookCopy.date.getFullYear());//2016

四、关于【script加载执行】十问

第一问:请说出关于下面使用方式中script的区别

默认

<html>
<head>
  <script type="text/javascript" src="script1.js" ></script>
  <script type="text/javascript" src="script1.js" ></script>
</head>

<body>
</body>
</html>

使用defer

<html>
<head>
  <script type="text/javascript" src="script1.js" defer="defer"></script>
  <script type="text/javascript" src="script2.js" defer="defer"></script>
</head>

<body>
</body>
</html>

使用async

<html>
<head>
  <script type="text/javascript" src="script1.js" defer="async"></script>
  <script type="text/javascript" src="script2.js" defer="async"></script>
</head>

<body>
</body>
</html>

默认方式:浏览器会并行加载script, 但是执行是书写的顺序,如果script1执行未完毕,就不会开始执行script2,尽管script2已经加载完。
而且这种方式会阻碍script标签后面其他元素的渲染,直到script1执行完毕才会渲染后面的dom

defer方式:也叫延迟脚本,使用defer后,该脚本会被马上加载,但是脚本会被延迟到整个页面都解析完再执行,即等浏览器遇到</html>标签后在执行。并且这两个脚本会按顺序执行。

async方式:也叫异步脚本: ,使用async后,该脚本会被马上加载,加载完立即执行,但是不会影响页面的解析,。并且这两个脚本不会按顺序执行。谁先加载完,谁就先执行

第二问:第一种方式中script是并行下载的吗?

是的,大多数浏览器现在已经允许并行下载JavaScript文件。这是个好消息,因为<script>标签在下载外部资源时不会阻塞其他<script>标签。遗憾的是,JavaScript 下载过程仍然会阻塞其他资源的下载,比如样式文件和图片(http连接个数的限制,当然,这个原因通常可以用减少http请求来解决,就是合并JavaScript脚本)。尽管脚本的下载过程不会互相影响,但页面仍然必须等待所有 JavaScript 代码下载并执行完成才能继续。因此,尽管最新的浏览器通过允许并行下载提高了性能,但问题尚未完全解决,脚本阻塞仍然是一个问题。

第三问:为什么script脚本要放在body尾部,而不放在head里。

若在head元素中包含js文件,意味着必须等js代码都被下载,解析,执行后才能开始呈现页面(浏览器在遇到<body>标签时才开始呈现页面)

第四问:那我硬是要放在head呢?怎么解决(除了用defer,async)

动态脚本加载: 时可以用另外一种方式加载脚本,叫做动态脚本加载

文档对象模型(DOM)允许您使用 JavaScript 动态创建 HTML 的几乎全部文档内容。<script>元素与页面其他元素一样,可以非常容易地通过标准 DOM 函数创建:

  通过标准 DOM 函数创建`<script>`元素
  var script = document.createElement ("script");
   script.type = "text/javascript";
   script.src = "script1.js";
   document.getElementsByTagName("head")[0].appendChild(script);

新的<script>元素加载 script1.js 源文件。此文件当元素添加到页面之后立刻开始下载。此技术的重点在于:无论在何处启动下载,文件的下载和运行都不会阻塞其他页面处理过程。您甚至可以将这些代码放在<head>部分而不会对其余部分的页面代码造成影响(除了用于下载文件的 HTTP 连接)。

当文件使用动态脚本节点下载时,返回的代码通常立即执行(除了 Firefox 和 Opera,他们将等待此前的所有动态脚本节点执行完毕)。

XMLHttpRequest脚本注入

另外一种无阻塞加载的脚本方法是使用XMLHttpRequest对象获取脚本并注入页面中。此技术会先创建一个XHR对象,然后用它下载JS文件,最后通过创建动态<script>元素将代码注入页面中。

var xmlhttp;
if (window.XMLHttpRequest)
  {// code for IE7+, Firefox, Chrome, Opera, Safari
  xmlhttp=new XMLHttpRequest();
  }
else
  {// code for IE6, IE5
  xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
  }
xmlhttp.onreadystatechange=function()
  {
  if (xmlhttp.readyState==4 && xmlhttp.status==200)
    {
    document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
    }
  }
xmlhttp.open("GET","test.js",true);
xmlhttp.send();
}

这段代码发送一个GET请求获取test.js文件。事件处理函数onReadyStateChange检查readyState是否为4,同时校验HTTP状态码是否有效(200表示有效响应,304意味着从缓存中读取)。

这种方法主要优点是:你可以下载JS代码但不立即执行。由于代码是在<script>标签之外返回的,因此它下载后不会自动执行,这使得你可以把脚本的执行推行到你准备好的时候。另一个优点是,同样的代码再所有的主流浏览器中无一例外都能正常工作。

这种方法的主要局限性是JS文件必须与所请求的页面处于相同的域,这意味着JS文件不能从CDN下载。因此大型Web应用通常不会采用XHR脚本注入。

当不是引入外部脚本时可以用window.load或$('document').ready来使JavaScript等页面加载完再执行

第五问:那我该怎么知道动态脚本已经加载完毕了呢

我们可以对加载的 JS 对象使用 onload 来判断(js.onload),此方法 Firefox2、Firefox3、Safari3.1+、Opera9.6+ 浏览器都能很好的支持,但 IE6、IE7 却不支持。曲线救国 —— IE6、IE7 我们可以使用 js.onreadystatechange 来跟踪每个状态变化的情况(一般为 loading 、loaded、interactive、complete),当返回状态为 loaded 或 complete 时,则表示加载完成,返回回调函数。

对于 readyState 状态需要一个补充说明:

  1. 在 interactive 状态下,用户可以参与互动。
  2. Opera 其实也支持 js.onreadystatechange,但他的状态和 IE 的有很大差别。
<script>
function include_js(file) {
    var _doc = document.getElementsByTagName('head')[0];
    var js = document.createElement('script');
    js.setAttribute('type', 'text/javascript');
    js.setAttribute('src', file);
    _doc.appendChild(js);
    if (!/*@cc_on!@*/0) { //if not IE
        //Firefox2、Firefox3、Safari3.1+、Opera9.6+ support js.onload
        js.onload = function () {
            alert('Firefox2、Firefox3、Safari3.1+、Opera9.6+ support js.onload');
        }
    } else {
        //IE6、IE7 support js.onreadystatechange
        js.onreadystatechange = function () {
            if (js.readyState == 'loaded' || js.readyState == 'complete') {
                alert('IE6、IE7 support js.onreadystatechange');
            }
        }
    }
    return false;
}

include_js('http://www.planabc.net/wp-includes/js/jquery/jquery.js');
</script>

第六问:动态加载的脚本会按顺序执行吗?怎么解决?

浏览器不保证文件加载的顺序。所有主流浏览器之中,只有 Firefox 和 Opera 保证脚本按照你指定的顺序执行。其他浏览器将按照服务器返回它们的次序下载并运行不同的代码文件。
解决方法:一个一个按顺序加载。加载完1.js,再加载2.js,如代码:

function loadScript(){
	var scriptArr =  Array.prototype.slice.apply(arguments);
	var script = document.createElement('script');
	script.type = 'text/javascript'; 
	
	var rest = scriptArr.slice(1);

	if(rest.length > 0){
		script.onload = script.onreadystatechange = function() { 
			if ( !this.readyState || this.readyState === "loaded" || 
			this.readyState === "complete" ) { 
				loadScript.apply(null, rest); 
				// Handle memory leak in IE 
				script.onload = script.onreadystatechange = null; 
			} 
		}; 	
	}					

	script.src = scriptArr[0];
	document.body.appendChild(script);
}	
loadScript('1.js','2.js','3.js');

第七问:有见过noscript标签吗?知道是干嘛用的吗?

如果浏览器不支持支持脚本,那么它会显示出 noscript 元素中的文本。

  
  <body>
  ...
  ...
  <script type="text/vbscript">
   <!--
   document.write("Hello World!")
   '-->
  </script>
  
  <noscript>Your browser does not support VBScript!</noscript>
  ...
  ...
</body>

总结
减少 JavaScript 对性能的影响有以下几种方法:

  • 将所有的<script>标签放到页面底部,也就是</body>闭合标签之前,这能确保在脚本执行前页面已经完成了渲染。
  • 尽可能地合并脚本。页面中的<script>标签越少,加载也就越快,响应也越迅速。无论是外链脚本还是内嵌脚本都是如此。
  • 采用无阻塞下载 JavaScript 脚本的方法:
  • 使用<script>标签的 defer 属性(仅适用于 IE 和 Firefox 3.5 以上版本);
  • 使用动态创建的<script>元素来下载并执行代码;
  • 使用 XHR 对象下载 JavaScript 代码并注入页面中。
    通过以上策略,可以在很大程度上提高那些需要使用大量 JavaScript 的 Web 网站和应用的实际性能。

该模块后续补充,也欢迎大家补充

看高性能js这本书时,有一段话让我很不解他想表达什么意思,如下,既然是放在body底部了为什么还要动态加载?(我的理解:这描述的应该是懒加载,动态加载,你不需要就可以先不加载
欢迎交流)

五、关于【原型与继承】十问

第一问:你知道new操作符的实现原理吗?描述下

通过new创建对象经历4个步骤:

  • 1、创建一个新对象
  • 2、将构造函数的作用域赋给新对象(因此this指向了这个新对象)
  • 3、执行构造函数中的代码(为这个新对象添加属性);
  • 4、返回新对象。
function newFunc (name) {
    var o = {};
    o.__proto__ = Person.prototype;//绑定Person的原型
    Person.call(o, name);
    return o;
}

第二问:请问下面代码输出什么?

function Person(){}
var p1 = new Person()

console.log(p1.constructor)
Person.prototype = {
    name:"小红"
}
var p2 = new Person()
console.log(p2.constructor)

答案:

function Person(){}
var p1 = new Person()

console.log(p1.constructor) // [Function: Person]
Person.prototype = {
    name:"小红"
}
var p2 = new Person()
console.log(p2.constructor)  [Function: Object]

第三问:为什么输出的两个constructor不相同

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则,为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性指向了prototype所在的函数。
上面的体题中

打印p1.constructor时,p1没有constructor属性,于是就往原型链查找,就在Person.prototype上找到了有constructor,而这个constructor指向构造函数Person,因此,打印出[Function: Person]

而到p2时,Person.prototype的值已经被修改成{
name:"小红"
},发现这个值里已经没有constructor,但是prototype上必须有constructor,所以它就自己创建了constructor,并且默认指向Object.,所以打印[Function: Object]

第四问:如果我修改prototype时,仍想constructor依旧指向Person,该怎么做呢?

如果想constructor依旧指向Person,可以在修改prototype的时候,添加上construtor属性

function Person(){}
var p1 = new Person()
console.log(p1.constructor) //[Function: Person]
Person.prototype = {
    constructor: Person,
    name:"小红"
}
var p2 = new Person()
console.log(p2.constructor) //[Function: Person]

但是这种添加constructor属性,会导致它的[[Enumerable]]特性的值被设置为true,所以可以用下面这种方式

function Person(){}
var p1 = new Person()
console.log(p1.constructor)  //[Function: Person]
Person.prototype = {
    name:"小红"
}
Object.defineProperty(Person.prototype,"constructor",{
    enumerable: false,
    value: Person
})

var p2 = new Person()
console.log(p2.constructor)  //[Function: Person]

第五问:你能介绍下原型链继承吗?

// 实现原型链的一种基本模式
function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}

// 继承,用 SuperType 类型的一个实例来重写 SubType 类型的原型对象
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
     return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue());     // true

其中,SubType 继承了 SuperType,而继承是通过创建 SuperType 的实例,并将该实例赋值给 SubType 的原型实现的。

实现的本质是重写子类型的原型对象,代之以一个新类型的实例。子类型的新原型对象中有一个内部属性 Prototype 指向了 SuperType 的原型,还有一个从 SuperType 原型中继承过来的属性 constructor 指向了 SuperType 构造函数。

最终的原型链是这样的:instance 指向 SubType 的原型,SubType 的原型又指向 SuperType 的原型,SuperType 的原型又指向 Object 的原型(所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype)
原型链继承的缺点:

1、在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性,并且会被所有的实例共享。这样理解:在超类型构造函数中定义的引用类型值的实例属性,会在子类型原型上变成原型属性被所有子类型实例所共享
2、在创建子类型的实例时,不能向超类型的构造函数中传递参数

第六问:既然原型链继承有以上缺点,那有没有解决方法呢?

有,就是借用构造函数继承

借用构造函数继承(也称伪造对象或经典继承)

// 在子类型构造函数的内部调用超类型构造函数;使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上
function SuperType(){
    // 定义引用类型值属性
    this.colors = ["red","green","blue"];
}
function SubType(){
    // 继承 SuperType,在这里还可以给超类型构造函数传参
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("purple");
alert(instance1.colors);     // "red,green,blue,purple"

var instance2 = new SubType();
alert(instance2.colors);     // "red,green,blue"

通过使用 apply() 或 call() 方法,我们实际上是在将要创建的 SubType 实例的环境下调用了 SuperType 构造函数。这样一来,就会在新 SubType 对象上执行 SuperType() 函数中定义的所有对象初始化代码。结果 SubType 的每个实例就都会具有自己的 colors 属性的副本了

借用构造函数的优点是解决了原型链实现继承存在的两个问题。
但是一波已平,一波又起

借用构造函数的缺点是方法都在构造函数中定义,因此函数复用就无法实现了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。

第六问:既然原型链继承和借用构造函数都有缺点,那该怎么办?

既然两种方法都没有对方的缺点,那就可以把两者方法结合起来,就解决了,这种方法叫做组合继承

组合继承(也称伪经典继承)

将原型链和借用构造函数的技术组合到一块。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性。

function SuperType(name){
    this.name = name;
    this.colors = ["red","green","blue"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name,age){
    // 借用构造函数方式继承属性
    SuperType.call(this,name);
    this.age = age;
}
// 原型链方式继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};
var instance1 = new SubType("luochen",22);
instance1.colors.push("purple");
alert(instance1.colors);      // "red,green,blue,purple"
instance1.sayName();
instance1.sayAge();

var instance2 = new SubType("tom",34);
alert(instance2.colors);      // "red,green,blue"
instance2.sayName();
instance2.sayAge();

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 javascript 中最常用的继承模式。而且,使用 instanceof 操作符和 isPrototype() 方法也能够用于识别基于组合继承创建的对象。

但它也有自己的不足 -- 无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

第七问:怎么还有缺点,再介绍下其他的继承方法?

原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
自定义一个函数来实现原型式继承

function object(o){
            function F(){}
            F.prototype = o;
            return new F();
}

在 object() 函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。实质上,object() 对传入其中的对象执行了一次浅复制。
其实这个方法就是Object.create的简单实现

直接用Object.create实现原型式继承

这个方法接收两个参数:一是用作新对象原型的对象和一个为新对象定义额外属性的对象。在传入一个参数的情况下,此方法与 object() 方法作用一致。在传入第二个参数的情况下,指定的任何属性都会覆盖原型对象上的同名属性。

var person = {
            name: "luochen",
            colors: ["red","green","blue"]
};
var anotherPerson1 = Object.create(person,{
            name: {
                    value: "tom"
            }
});
var anotherPerson2 = Object.create(person,{
            name: {
                    value: "jerry"
            }
});
anotherPerson1.colors.push("purple");
alert(anotherPerson1.name);     // "tom"
alert(anotherPerson2.name);     // "jerry"
alert(anotherPerson1.colors);    // "red,green,blue,purple"
alert(anotherPerson2.colors);    // "red,green,bule,purple";

只是想让一个对象与另一个对象类似的情况下,原型式继承是完全可以胜任的。但是缺点是:包含引用类型值的属性始终都会共享相应的值,这也是原型链继承的缺点

寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回这个对象

function createPerson(original){
    var clone = Object.create(original);   // 通过 Object.create() 函数创建一个新对象
    clone.sayGood = function(){   // 增强这个对象
         alert("hello world!!!");
    };
    return clone; // 返回这个对象
}

这个方式跟工厂模式生产对象很类似。在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。此模式的缺点是做不到函数复用

寄生组合式继承

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型

function SuperType(name){
}

function SubType(name,age){
    SuperType.call(this,name);
    this.age = age;
}
// 创建超类型原型的一个副本
var anotherPrototype = Object.create(SuperType.prototype);
// 重设因重写原型而失去的默认的 constructor 属性
anotherPrototype.constructor = SubType;
// 将新创建的对象赋值给子类型的原型
SubType.prototype = anotherPrototype;

这个例子的高效率体现在它只调用一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要,多余的属性。与此同时,原型链还能保持不变;因此还能够正常使用 instance 操作符和 isPrototype() 方法

六、关于【高级技巧】十问

第一问:安全类型检测——typeof和instanceof 区别以及缺陷,以及解决方案

这两个方法都可以用来判断变量类型

区别:前者是判断这个变量是什么类型,后者是判断这个变量是不是某种类型,返回的是布尔值

(1)typeof

缺陷:

1.不能判断变量具体的数据类型比如数组、正则、日期、对象,因为都会返回object,不过可以判断function,如果检测对象是正则表达式的时候,在Safari和Chrome中使用typeof的时候会错误的返回"function",其他的浏览器返回的是object.

2.判断null的时候返回的是一个object,这是js的一个缺陷,判断NaN的时候返回是number

(2)instanceof
可以用来检测这个变量是否为某种类型,返回的是布尔值,并且可以判断这个变量是否为某个函数的实例,它检测的是对象的原型


let num = 1
num instanceof Number // false

num = new Number(1)
num instanceof Number // true

明明都是num,而且都是1,只是因为第一个不是对象,是基本类型,所以直接返回false,而第二个是封装成对象,所以true。

这里要严格注意这个问题,有些说法是检测目标的__proto__与构造函数的prototype相同即返回true,这是不严谨的,检测的一定要是对象才行,如:

let num = 1
num.__proto__ === Number.prototype // true
num instanceof Number // false

num = new Number(1)
num.__proto__ === Number.prototype // true
num instanceof Number // true

num.__proto__ === (new Number(1)).__proto__ // true

此外,instanceof还有另外一个缺点:如果一个页面上有多个框架,即有多个全局环境,那么我在a框架里定义一个Array,然后在b框架里去用instanceof去判断,那么该array的原型链上不可能找到b框架里的array,则会判断该array不是一个array。

解决方案:使用Object.prototype.toString.call(value) 方法去调用对象,得到对象的构造函数名。可以解决instanceof的跨框架问题,缺点是对用户自定义的类型,它只会返回[object Object]

第二问:既然提到了instanceof,那手写实现下instanceof吧

// [1,2,3] instanceof Array ---- true

// L instanceof R
// 变量R的原型 存在于 变量L的原型链上
function instance_of(L,R){    
    // 验证如果为基本数据类型,就直接返回false
    const baseType = ['string', 'number','boolean','undefined','symbol']
    if(baseType.includes(typeof(L))) { return false }
    
    let RP  = R.prototype;  //取 R 的显示原型
    L = L.__proto__;       //取 L 的隐式原型
    while(true){           // 无线循环的写法(也可以使 for(;;) )
        if(L === null){    //找到最顶层
            return false;
        }
        if(L === RP){       //严格相等
            return true;
        }
        L = L.__proto__;  //没找到继续向上一层原型链查找
    }
}

第三问:作用域安全的构造函数--当我们new一个构造函数的时候可以获得一个实例,要是我们忘记写new了呢?

例如

function Person(){
    this.name = "小红"
}

p = Person();

这会发生什么问题?,怎么解决

这样直接使用,this会映射到全局对象window上。解决方法可以是:首先确认this对象是正确类型的实例。如果不是,那么会创建新的实例并返回。请看下面的例子

function Person(){
    if(this instanceof Person){
        this.name = "小红"
    }else{
        return  new Person()
    }
}

p = Person();

第四问:谈一下惰性载入函数

在JavaScript代码中,由于浏览器之间行为的差异,多数JavaScript代码包含了大量的if语句,以检查浏览器特性,解决不同浏览器的兼容问题。例如添加事件的函数:

function addEvent (element, type, handler) {
    if (element.addEventListener) {
        element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
        element.attachEvent("on" + type, handler);
    } else {
        element["on" + type] = handler;
    }
}

每次调用addEvent()的时候,都要对浏览器所支持的能力仔细检查。首先检查是否支持addEventListener方法,如果不支持再检查是否支持attachEvent方法,如果还不支持,就用DOM 0级的方法添加事件。在调用addEvent()过程中,每次这个过程都要走一遍。其实,浏览器支持其中的一种方法就会一直支持他,就没有必要再进行其他分支的检测了,也就是说if语句不必每次都执行,代码可以运行的更快一些。解决的方案称之为惰性载入。
所谓惰性载入,就是说函数的if分支只会执行一次,之后调用函数时,直接进入所支持的分支代码。有两种实现惰性载入的方式,第一种事函数在第一次调用时,对函数本身进行二次处理,该函数会被覆盖为符合分支条件的函数,这样对原函数的调用就不用再经过执行的分支了,我们可以用下面的方式使用惰性载入重写addEvent()。


function addEvent (type, element, handler) {
    if (element.addEventListener) {
        addEvent = function (type, element, handler) {
            element.addEventListener(type, handler, false);
        }
    }
    else if(element.attachEvent){
        addEvent = function (type, element, handler) {
            element.attachEvent('on' + type, handler);
        }
    }
    else{
        addEvent = function (type, element, handler) {
            element['on' + type] = handler;
        }
    }
    return addEvent(type, element, handler);
}

在这个惰性载入的addEvent()中,if语句的每个分支都会为addEvent变量赋值,有效覆盖了原函数。最后一步便是调用了新赋函数。下一次调用addEvent()的时候,便会直接调用新赋值的函数,这样就不用再执行if语句了。

第二种实现惰性载入的方式是在声明函数时就指定适当的函数。这样在第一次调用函数时就不会损失性能了,只在代码加载时会损失一点性能。一下就是按照这一思路重写的addEvent()。

var addEvent = (function () {
    if (document.addEventListener) {
        return function (type, element, fun) {
            element.addEventListener(type, fun, false);
        }
    }
    else if (document.attachEvent) {
        return function (type, element, fun) {
            element.attachEvent('on' + type, fun);
        }
    }
    else {
        return function (type, element, fun) {
            element['on' + type] = fun;
        }
    }
})();

这个例子中使用的技巧是创建一个匿名的自执行函数,通过不同的分支以确定应该使用那个函数实现,实际的逻辑都一样,不一样的地方就是使用了函数表达式(使用了var定义函数)和新增了一个匿名函数,另外每个分支都返回一个正确的函数,并立即将其赋值给变量addEvent。

惰性载入函数的优点只执行一次if分支,避免了函数每次执行时候都要执行if分支和不必要的代码,因此提升了代码性能,至于那种方式更合适,就要看您的需求而定了。

第五问:谈一下函数节流

概念:限制一个函数在一定时间内只能执行一次。

主要实现思路 就是通过 setTimeout 定时器,通过设置延时时间,在第一次调用时,创建定时器,先设定一个变量true,写入需要执行的函数。第二次执行这个函数时,会判断变量是否true,是则返回。当第一次的定时器执行完函数最后会设定变量为false。那么下次判断变量时则为false,函数会依次运行。目的在于在一定的时间内,保证多次函数的请求只执行最后一次调用。

函数节流的代码实现

function throttle(fn,wait){
    var timer = null;
    return function(){
        var context = this;
        var args = arguments;
        if(!timer){
            timer = setTimeout(function(){
                fn.apply(context,args);
                timer = null;
            },wait)
        }
    }
}
    
function handle(){
    console.log(Math.random());
}
    
window.addEventListener("mousemove",throttle(handle,1000));

函数节流的应用场景(throttle)

  • DOM 元素的拖拽功能实现(mousemove)
  • 高频点击提交,表单重复提交
  • 搜索联想(keyup)
  • 计算鼠标移动的距离(mousemove)
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,- - 才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次.

第六问:谈一下函数防抖

概念:函数防抖(debounce),就是指触发事件后,在 n 秒内函数只能执行一次,如果触发事件后在 n 秒内又触发了事件,则会重新计算函数延执行时间。

函数防抖的要点,是需要一个 setTimeout 来辅助实现,延迟运行需要执行的代码。如果方法多次触发,则把上次记录的延迟执行代码用 clearTimeout 清掉,重新开始计时。若计时期间事件没有被重新触发,等延迟时间计时完毕,则执行目标代码。

函数防抖的代码实现

function debounce(fn,wait){
    var timer = null;
    return function(){
        if(timer !== null){
            clearTimeout(timer);
        }
        timer = setTimeout(fn,wait);
    }
}
    
function handle(){
    console.log(Math.random());
}
    
window.addEventListener("resize",debounce(handle,1000));

函数防抖的使用场景
函数防抖一般用在什么情况之下呢?一般用在,连续的事件只需触发一次回调的场合。具体有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求;
  • 用户名、手机号、邮箱输入验证;
  • 浏览器窗口大小改变后,只需窗口调整完后,再执行 resize 事件中的代码,防止重复渲染。

目前遇到过的用处就是这些,理解了原理与实现思路,小伙伴可以把它运用在任何需要的场合,提高代码质量。

第七问:谈一下requestAnimationFrame

动画原理
眼前所看到图像正在以每秒60次的频率刷新,由于刷新频率很高,因此你感觉不到它在刷新。而动画本质就是要让人眼看到图像被刷新而引起变化的视觉效果,这个变化要以连贯的、平滑的方式进行过渡。 那怎么样才能做到这种效果呢?

刷新频率为60Hz的屏幕每16.7ms刷新一次,我们在屏幕每次刷新前,将图像的位置向左移动一个像素,即1px。这样一来,屏幕每次刷出来的图像位置都比前一个要差1px,因此你会看到图像在移动;由于我们人眼的视觉停留效应,当前位置的图像停留在大脑的印象还没消失,紧接着图像又被移到了下一个位置,因此你才会看到图像在流畅的移动,这就是视觉效果上形成的动画。

与setTimeout相比较

理解了上面的概念以后,我们不难发现,setTimeout 其实就是通过设置一个间隔时间来不断的改变图像的位置,从而达到动画效果的。但我们会发现,利用seTimeout实现的动画在某些低端机上会出现卡顿、抖动的现象。 这种现象的产生有两个原因:

  • setTimeout的执行时间并不是确定的。在Javascript中, setTimeout 任务被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,因此 setTimeout 的实际执行时间一般要比其设定的时间晚一些。

  • 刷新频率受屏幕分辨率和屏幕尺寸的影响,因此不同设备的屏幕刷新频率可能会不同,而 setTimeout只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。

以上两种情况都会导致setTimeout的执行步调和屏幕的刷新步调不一致,从而引起丢帧现象

requestAnimationFrame:与setTimeout相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

除此之外,requestAnimationFrame还有以下两个优势:

  • CPU节能:使用setTimeout实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费CPU资源。而requestAnimationFrame则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的requestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。

  • 函数节流:在高频率事件(resize,scroll等)中,为了防止在一个刷新间隔内发生多次函数执行,使用requestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个刷新间隔内函数执行多次时没有意义的,因为显示器每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来。

第八问:web计时,你知道该怎么计算首屏,白屏时间吗?

白屏时间
白屏时间指的是浏览器开始显示内容的时间。因此我们只需要知道是浏览器开始显示内容的时间点,即页面白屏结束时间点即可获取到页面的白屏时间。

计算白屏时间
因此,我们通常认为浏览器开始渲染 标签或者解析完 标签的时刻就是页面白屏结束的时间点。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>白屏</title>
  <script type="text/javascript">
    // 不兼容performance.timing 的浏览器,如IE8
    window.pageStartTime = Date.now();
  </script>
  <!-- 页面 CSS 资源 -->
  <link rel="stylesheet" href="common.css">
  <link rel="stylesheet" href="page.css">
  <script type="text/javascript">
    // 白屏时间�结束点
    window.firstPaint = Date.now();
  </script>
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>

因此白屏时间则可以这样计算出:

可使用 Performance API 时:

白屏时间 = firstPaint - performance.timing.navigationStart;

不可使用 Performance API 时:

白屏时间 = firstPaint - pageStartTime; //虽然我们知道这并不准确,毕竟DNS解析,tcp三次握手等都没计算入内。

首屏时间
首屏时间是指用户打开网站开始,到浏览器首屏内容渲染完成的时间。对于用户体验来说,首屏时间是用户对一个网站的重要体验因素。通常一个网站,如果首屏时间在5秒以内是比较优秀的,10秒以内是可以接受的,10秒以上就不可容忍了。超过10秒的首屏时间用户会选择刷新页面或立刻离开。


通常计算首屏的方法有

  • 首屏模块标签标记法

  • 统计首屏内加载最慢的图片的时间

  • 自定义首屏内容计算法

    1、首屏模块标签标记法

首屏模块标签标记法,通常适用于首屏内容不需要通过拉取数据才能生存以及页面不考虑图片等资源加载的情况。我们会在 HTML 文档中对应首屏内容的标签结束位置,使用内联的 JavaScript 代码记录当前时间戳。如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>首屏</title>
  <script type="text/javascript">
    window.pageStartTime = Date.now();
  </script>
  <link rel="stylesheet" href="common.css">
  <link rel="stylesheet" href="page.css">
</head>
<body>
  <!-- 首屏可见模块1 -->
  <div class="module-1"></div>
  <!-- 首屏可见模块2 -->
  <div class="module-2"></div>
  <script type="text/javascript">
    window.firstScreen = Date.now();
  </script>
  <!-- 首屏不可见模块3 -->
  <div class="module-3"></div>
    <!-- 首屏不可见模块4 -->
  <div class="module-4"></div>
</body>
</html>

此时首屏时间等于 firstScreen - performance.timing.navigationStart;

事实上首屏模块标签标记法 在业务中的情况比较少,大多数页面都需要通过接口拉取数据才能完整展示,因此我们会使用 JavaScript 脚本来判断首屏页面内容加载情况。

2、统计首屏内图片完成加载的时间

通常我们首屏内容加载最慢的就是图片资源,因此我们会把首屏内加载最慢的图片的时间当做首屏的时间。

由于浏览器对每个页面的 TCP 连接数有限制,使得并不是所有图片都能立刻开始下载和显示。因此我们在 DOM树 构建完成后将会去遍历首屏内的所有图片标签,并且监听所有图片标签 onload 事件,最终遍历图片标签的加载时间的最大值,并用这个最大值减去 navigationStart 即可获得近似的首屏时间。

此时首屏时间等于 加载最慢的图片的时间点 - performance.timing.navigationStart;
//首屏时间尝试:
//1,获取首屏基线高度
//2,计算出基线dom元素之上的所有图片元素
//3,所有图片onload之后为首屏显示时间
//

function getOffsetTop(ele) {
    var offsetTop = ele.offsetTop;
    if (ele.offsetParent !== null) {
        offsetTop += getOffsetTop(ele.offsetParent);
    }
    return offsetTop;
}

var firstScreenHeight = win.screen.height;
var firstScreenImgs = [];
var isFindLastImg = false;
var allImgLoaded = false;
var t = setInterval(function() {
    var i, img;
    if (isFindLastImg) {
        if (firstScreenImgs.length) {
            for (i = 0; i < firstScreenImgs.length; i++) {
                img = firstScreenImgs[i];
                if (!img.complete) {
                    allImgLoaded = false;
                    break;
                } else {
                    allImgLoaded = true;
                }
            }
        } else {
            allImgLoaded = true;
        }
        if (allImgLoaded) {
            collect.add({
                firstScreenLoaded: startTime - Date.now()
            });
            clearInterval(t);
        }
    } else {
        var imgs = body.querySelector('img');
        for (i = 0; i<imgs.length; i++) {
            img = imgs[i];
            var imgOffsetTop = getOffsetTop(img);
            if (imgOffsetTop > firstScreenHeight) {
                isFindLastImg = true;
                break;
            } else if (imgOffsetTop <= firstScreenHeight && !img.hasPushed) {
                img.hasPushed = 1;
                firstScreenImgs.push(img);
            }
        }
    }
}, 0);
doc.addEventListener('DOMContentLoaded', function() {
    var imgs = body.querySelector('img');
    if (!imgs.length) {
        isFindLastImg = true;
    }
});

win.addEventListener('load', function() {
    allImgLoaded = true;
    isFindLastImg = true;
    if (t) {
        clearInterval(t);
    }
    collect.log(collect.global);
});

解释一下思路,大概就是判断首屏有没有图片,如果没图片就用domready时间,如果有图,分2种情况,图在首屏,图不在首屏,如果在则收集,并判断加载状态,加载完毕之后则首屏完成加载,如果首屏没图,找到首屏下面的图,立刻触发首屏完毕。可以想象这么做前端收集是不准的,但是可以确保最晚不会超过win load,所以应该还算有些意义。。没办法,移动端很多浏览器不支持performance api,所以土办法前端收集,想出这么个黑魔法,在基线插入节点收集也是个办法,但是不友好,而且现在手机屏幕这么多。。

3、自定义模块内容计算法

由于统计首屏内图片完成加载的时间比较复杂。因此我们在业务中通常会通过自定义模块内容,来简化计算首屏时间。如下面的做法:

  • 忽略图片等资源加载情况,只考虑页面主要 DOM
  • 只考虑首屏的主要模块,而不是严格意义首屏线以上的所有内容

实际上用performance.timing来计算首屏加载时间与白屏时间非常简单与精确。不过目前只支持IE10和chrome
贴下其API的使用

                                                   var navigationStart = performance.timing.navigationStart;
//1488984540668
console.log(navigationStart);

//Wed Mar 08 2017 22:49:44 GMT+0800 (**标准时间)
console.log(new Date(new Date(navigationStart)));
复制代码
  redirectStart:到当前页面的重定向开始的时间。但只有在重定向的页面来自同一个域时这个属性才会有值;否则,值为0
  redirectEnd:到当前页面的重定向结束的时间。但只有在重定向的页面来自同一个域时这个属性才会有值;否则,值为0

console.log(performance.timing.redirectStart);//0
console.log(performance.timing.redirectEnd);//0
  fetchStart:开始通过HTTP GET取得页面的时间

console.log(performance.timing.fetchStart);//1488984540668
  domainLookupStart:开始査询当前页面DNS的时间,如果使用了本地缓存或持久连接,则与fetchStart值相等
  domainLookupEnd:査询当前页面DNS结束的时间,如果使用了本地缓存或持久连接,则与fetchStart值相等

console.log(performance.timing.domainLookupStart);//1488984540670
console.log(performance.timing.domainLookupEnd);//1488984540671
  connectStart:浏览器尝试连接服务器的时间
  secureConnectionStart:浏览器尝试以SSL方式连接服务器的时间。不使用SSL方式连接时,这个属性的值为0 
  connectEnd:浏览器成功连接到服务器的时间

console.log(performance.timing.connectStart);//1488984540671
console.log(performance.timing.secureConnectionStart);//0
console.log(performance.timing.connectEnd);//1488984540719
  requestStart:浏览器开始请求页面的时间
  responseStart:浏览器接收到页面第一字节的时间
  responseEnd:浏览器接收到页面所有内容的时间

console.log(performance.timing.requestStart);//1488984540720
console.log(performance.timing.responseStart);//1488984540901
console.log(performance.timing.responseEnd);//1488984540902
  unloadEventStart:前一个页面的unload事件开始的时间。但只有在前一个页面与当前页面来自同一个域时这个属性才会有值;否则,值为0
  unloadEventEnd:前一个页面的unload事件结束的时间。但只有在前一个页面与当前页面来自同一个域时这个属性才会有值;否则,值为0

console.log(performance.timing.unloadEventStart);//1488984540902
console.log(performance.timing.unloadEventEnd);//1488984540903
  domLoading:document.readyState变为"loading"的时间,即开始解析DOM树的时间
  domInteractive:document.readyState变为"interactive"的时间,即完成完成解析DOM树的时间
  domContentLoadedEventStart:发生DOMContentloaded事件的时间,即开始加载网页内资源的时间
  domContentLoadedEventEnd:DOMContentLoaded事件已经发生且执行完所有事件处理程序的时间,网页内资源加载完成的时间
  domComplete:document.readyState变为"complete"的时间,即DOM树解析完成、网页内资源准备就绪的时间

console.log(performance.timing.domLoading);//1488984540905
console.log(performance.timing.domInteractive);//1488984540932
console.log(performance.timing.domContentLoadedEventStart);//1488984540932
console.log(performance.timing.domContentLoadedEventEnd);//1488984540932
console.log(performance.timing.domComplete);//1488984540932
  loadEventStart:发生load事件的时间,也就是load回调函数开始执行的时间 
  loadEventEnd:load事件已经发生且执行完所有事件处理程序的时间

console.log(performance.timing.loadEventStart);//1488984540933
console.log(performance.timing.loadEventEnd);//1488984540933
                                                       

第九问:你知道web Worker吗?

多线程技术在服务端技术中已经发展的很成熟了,而在Web端的应用中却一直是鸡肋
在新的标准中,提供的新的WebWork API,让前端的异步工作变得异常简单。
使用:创建一个Worker对象,指向一个js文件,然后通过Worker对象往js文件发送消息,js文件内部的处理逻辑,处理完毕后,再发送消息回到当前页面,纯异步方式,不影响当前主页面渲染。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <script type="text/javascript">
        //创建线程 work对象
        var work = new Worker("work.js");      //work文件中不要存在跟ui代码
        //发送消息
        work.postMessage("100");
        // 监听消息
        work.onmessage = function(event) {
            alert(event.data);
        };
    </script>
</head>
<body>

</body>
</html>          

work.js

  onmessage = function (event) {
    //从1加到num
    var num = event.data;
    var result = 0;
    for (var i = 1; i <= num; i++) {
        result += i;
    }
    postMessage(result);
}
   

七、关于【promise】十问

第一问:了解 Promise 吗?

  1. 了解Promise,Promise是一种异步编程的解决方案,有三种状态,pending(进行中)、resolved(已完成)、rejected(已失败)。当Promise的状态由pending转变为resolved或reject时,会执行相应的方法

  2. Promised的特点是只有异步操作的结果,可以决定当前是哪一种状态,任务其他操作都无法改变这个状态,也是“Promise”的名称的由来,同时,状态一旦改变,就无法再次改变状态

第二问:Promise 解决的痛点是什么?

Promise解决的痛点:

1)回调地狱,代码难以维护, 常常第一个的函数的输出是第二个函数的输入这种现象,是为解决异步操作函数里的嵌套回调(callback hell)问题,代码臃肿,可读性差,只能在回调里处理异常

2)promise可以支持多个并发的请求,获取并发请求中的数据

3)promise可以解决可读性的问题,异步的嵌套带来的可读性的问题,它是由异步的运行机制引起的,这样的代码读起来会非常吃力

4)promise可以解决信任问题,对于回调过早、回调过晚或没有调用和回调次数太少或太多,由于promise只能决议一次,决议值只能有一个,决议之后无法改变,任何then中的回调也只会被调用一次,所以这就保证了Promise可以解决信任问题
5)Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清晰地分离了

第三问:Promise 解决的痛点还有其他方法可以解决吗?如果有,请列举。

1)Promise 解决的痛点还有其他方法可以解决,比如setTimeout、事件监听、回调函数、Generator函数,async/await

2)setTimeout:缺点不精确,只是确保在一定时间后加入到任务队列,并不保证立马执行。只有执行引擎栈中的代码执行完毕,主线程才会去读取任务队列

3)事件监听:任务的执行不取决于代码的顺序,而取决于某个事件是否发生

4)Generator函数虽然将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。即如何实现自动化的流程管理

5)async/await

第四问:Promise 如何使用?

1)创造一个Promise实例

2)Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数

3)可用Promise的try和catch方法预防异常

第五问:Promise 的业界实现都有哪些?

1) promise可以支持多个并发的请求,获取并发请求中的数据

2)promise可以解决可读性的问题,异步的嵌套带来的可读性的问题,它是由异步的运行机制引起的,这样的代码读起来会非常吃力

第六问: Promise的问题?解决办法?

promise的问题为:

promise一旦执行,无法中途取消

promise的错误无法在外部被捕捉到,只能在内部进行预判处理

promise的内如何执行,监测起来很难

解决办法

正是因为这些原因,ES7引入了更加灵活多变的async,await来处理异步

第七问:老旧浏览器没有Promise全局对象增么办?

果辛辛苦苦写完代码,测试后发现不兼容IE6、7增么办?难道要推翻用回调函数重写?当然不是这样,轮子早就造好了。

我们可以使用es6-promise-polyfill。es6-promise-polyfill可以使用页面标签直接引入,可以通过es6的import方法引入(如果你是用webpack),在node中可以使用require引入,也可以在Seajs中作为依赖引入。

引入这个polyfill之后,它会在window对象中加入Promise对象。这样我们就可以全局使用Promise了。

第八问:怎么让一个函数无论promise对象成功和失败都能被调用?

笨方法:

在两个回调中分别执行一次函数。

推荐方式:

扩展一个 Promise.finally(),finally方法用于指定不管Promise对象最后状态如何,都会执行的操作,它与done方法的最大区别在于,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。

//添加finally方法
Promise.prototype.finally=function (callback) {
   var p=this.constructor;
   return this.then(//只要是promise对象就可以调用then方法
     value => p.resolve(callback()).then(() => value),
     reason => p.resolve(callback()).then(() => {throw reason})
   );
}

对finally方法的理解:
(1)p.resolve(callback())这句函数callback已经执行
(2)finally方法return的是一个promise对象,所以还可以继续链式调用其他方法
(3)对于Promise.resolve方法

Promise.resolve('foo');
等价于
new Promise(resolve => resolve('foo'));

所以可以通过then方法的回调函数 接受 实例对象返回的参数
比如:

Promise.resolve(function(){console.log(2);}).then(function(cb){cb()}) 

第九问:红灯3秒亮一次,绿灯1秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用Promise实现)三个亮灯函数已经存在:

function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}

解析
红灯3秒亮一次,绿灯1秒亮一次 ,黄灯2秒亮一次,意思就是3秒执行一次red函数,2秒执行一次green函数,1秒执行一次yellow函数,不断交替重复亮灯,意思就是按照这个顺序一直执行这3个函数,这步可以利用递归来实现。
答案:

function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}

var light = function (timmer, cb) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            cb();
            resolve();
        }, timmer);
    });
};

var step = function () {
    Promise.resolve().then(function () {
        return light(3000, red);
    }).then(function () {
        return light(2000, green);
    }).then(function () {
        return light(1000, yellow);
    }).then(function () {
        step();
    });
}

step();

第十问:实现 mergePromise 函数,把传进去的数组按顺序先后执行,并且把返回的数据先后放到数组 data 中。

const timeout = ms => new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve();
    }, ms);
});

const ajax1 = () => timeout(2000).then(() => {
    console.log('1');
    return 1;
});

const ajax2 = () => timeout(1000).then(() => {
    console.log('2');
    return 2;
});

const ajax3 = () => timeout(2000).then(() => {
    console.log('3');
    return 3;
});

const mergePromise = ajaxArray => {
    // 在这里实现你的代码

};

mergePromise([ajax1, ajax2, ajax3]).then(data => {
    console.log('done');
    console.log(data); // data 为 [1, 2, 3]
});

// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]

解析
首先 ajax1 、ajax2、ajax3 都是函数,只是这些函数执行后会返回一个 Promise,按题目的要求我们只要顺序执行这三个函数就好了,然后把结果放到 data 中,但是这些函数里都是异步操作,想要按顺序执行,然后输出 1,2,3并没有那么简单,看个例子。

function A() {
    setTimeout(function () {
        console.log('a');
    }, 3000);
}

function B() {
    setTimeout(function () {
        console.log('b');
    }, 1000);
}

A();
B();

// b
// a

答案

// 保存数组中的函数执行后的结果
var data = [];

// Promise.resolve方法调用时不带参数,直接返回一个resolved状态的 Promise 对象。
var sequence = Promise.resolve();

ajaxArray.forEach(function (item) {
    // 第一次的 then 方法用来执行数组中的每个函数,
    // 第二次的 then 方法接受数组中的函数执行后返回的结果,
    // 并把结果添加到 data 中,然后把 data 返回。
    // 这里对 sequence 的重新赋值,其实是相当于延长了 Promise 链
    sequence = sequence.then(item).then(function (res) {
        data.push(res);
        return data;
    });
})

// 遍历结束后,返回一个 Promise,也就是 sequence, 他的 [[PromiseValue]] 值就是 data,
// 而 data(保存数组中的函数执行后的结果) 也会作为参数,传入下次调用的 then 方法中。
return sequence;

第十一问:封装一个异步加载图片的方法

function loadImageAsync(url) {
    return new Promise(function(resolve,reject) {
        var image = new Image();
        image.onload = function() {
            resolve(image) 
        };
        image.onerror = function() {
            reject(new Error('Could not load image at' + url));
        };
        image.src = url;
     });
}   

第十二问:手写promise

请看我的另一篇文章
一步一步实现自己的Promise

八、关于【事件】十问

第一问:请介绍下事件模型

目前共有三种事件模型,它们分别是:

DOM0 级事件模型、IE 事件模型、DOM2 级事件模型

DOM0 级事件模型 又称原始事件模型,有两种方式,最直观的提下如下代码:

// 方式一
// 将事件直接通过属性绑定在元素上
<button onclick="clickBtn()"></button>
// 方式二
// 获取到页面元素后,通过 onclick 等事件,将触发的方法指定为元素的事件
// 取消该事件可直接设置为 null
var btn = document.getElementById('btn')
btn.onclick = function () {...}
btn.onclick = null

DOM0 级的事件模型,方法较为简单,但是将逻辑和界面耦合在了一起,对之后的维护不是很友好

但也不是没有优点,这种方式兼容所有浏览器

IE 事件模型

IE 事件模型一共有两个阶段:

  • 事件处理阶段:事件在达到目标元素时,触发监听事件
  • 事件冒泡阶段:事件从目标元素冒泡到 document,并且一次检查各个节点是否绑定了监听函数,如果有则执行

绑定和移除事件的 api 分别如下:

// 绑定事件
el.attachEvent(eventType, handler)

// 移除事件
el.detachEvent(eventType, handler)

参数说明:

eventType 是如onclick一样的带有”on“的事件,绑定事件时,handler可以是具名函数,也可以是匿名函数,但是匿名函数无法移除

我们会发现,IE 事件模型与我们平时用的事件绑定方法addEventListener,也就是下面要说的 DOM2 级事件模型有点相似,但是 IE 事件模型仅在 IE 浏览器中有效,不兼容其他浏览器

DOM2 级事件模型

W3C标准模型,也是我们频繁使用的事件模型,除 IE6-8 以外的所有现代浏览器都支持该事件模型
DOM2 级事件模型共有三个阶段:

  1. 事件捕获阶段:事件从 document 向下传播到目标元素,依次检查所有节点是否绑定了监听事件,如果有则执行
  2. 事件处理阶段:事件在达到目标元素时,触发监听事件
  3. 事件冒泡阶段:事件从目标元素冒泡到 document,并且一次检查各个节点是否绑定了监听函数,如果有则执行

第二问:介绍下这三种事件模型的区别

  • Dom0 模型:
  1. this指向: 指向函数中的this指向的是被绑定的元素
  2. 绑定多个同事件类型的事件时,如对同个元素绑定多个click,则后面的会覆盖前面的,最后只有一个会执行
  • IE 模型:
  1. this指向: 指向函数中的this指向的是window
  2. 绑定多个同事件类型的事件时,如对同个元素绑定多个click,则后面的不会覆盖前面的,执行顺序是先执行下面的,从下往上执行
  3. 只有两个参数:第一个参数为 事件类型,第二个为事件执行函数目标阶段,
  4. 只有事件冒泡阶段
  5. 获取目标元素: window.event.srcElement
  • Dom2 模型:
  1. this指向: 指向函数中的this指向的是被绑定的元素
  2. 绑定多个同事件类型的事件时,如对同个元素绑定多个click,则后面的不会覆盖前面的,执行顺序是先执行上面的,从上往下执行
  3. 有三个参数:第一个参数为 事件类型,第二个为事件执行函数,第三个为布尔值,表示是否用事件捕获
  4. 有事件捕获阶段,处于目标阶段,事件冒泡阶段
  5. 获取目标元素: event.target

欢迎补充。。。

第三问:请介绍下事件流?

事件流所描述的就是从页面中接受事件的顺序,事件流分为两种:事件冒泡(主流)和事件捕获. 版本IE(IE8及以下版本)不支持捕获阶段

1、事件冒泡


事件开始时由具体元素接收,然后逐级向上传播到父元素

2、事件捕获

父元素的节点更早接收事件,而具体元素最后接收事件,与事件冒泡相反

第四问 怎么阻止事件冒泡和事件捕获以及默认事件?

  1. 阻止事件冒泡 :使用e.stopPropagation(); IE使用 window.event.cancelBubble = true;
function stopBubble(e){
     <!--如果提供了事件对象,则这是个非IE浏览器-->
    if(e&&e.stopPropagation){
        e.stopPropagation();
    }else{
        <!--我们需要使用IE的方式来取消事件冒泡-->
        window.event.cancelBubble = true;
    }
} 
  1. 阻止事件捕获:与冒泡一样 使用`e.stopPropagation(),IE没有捕获阶段,所以不用
  2. 阻止默认事件:
function stopDefault(e){
    <!--阻止默认行为W3C-->
    if(e&&e.preventDefault()){
        e.preventDefault();
    }else{
        <!--IE中阻止默认行为-->
        windown.event.returnValue = false
    }
}

第六问:事件的委托(代理 Delegated Events)的原理以及优缺点

委托(代理)事件是那些被绑定到父级元素的事件,但是只有当满足一定匹配条件时才会触发。这是靠事件的冒泡机制来实现的,

  1. 优点是:
  • (1)可以大量节省内存占用,减少事件注册,比如在table上代理所有td的click事件就非常棒
  • (2)可以实现当新增子对象时无需再次对其绑定事件,对于动态内容部分尤为合适
  1. 缺点是:
  • 事件代理的应用常用应该仅限于上述需求下,如果把所有事件都用代理就可能会出现事件误判,即本不应用触发事件的被绑上了事件。

例子:

要求:兼容浏览器 考点:事件对象e,IE下事件对应的属性名。 重点是要说到target,currentTarget以及IE下的srcElement和this。

第七问:编写一个自定义事件类,包含on/off/emit/once方法


可能谈到Evnet,customerEvent,document.createEvent

第八问:怎样判断js脚本是否加载完,并在加载完后进行操作

在工作过程中,经常会遇到按需加载的需求,即在脚本加载完成后,返回一个回调函数,在回调函数中进行相关操作,那如何去判断脚本是否加载完成了呢?

可以对加载的js对象使用onload来判断,jsDom.onload

ie6、7不支持js.onload方法,使用js.onreadystatechange来解决
js.onreadystatechange来跟踪每个状态的变化(loading、loaded、interactive、complete),当返回状态为loaded或者complete时,表示加载完成,返回回调函数.

第九问:上面的代码中,script脚本是什么时候开始加载的?

body.appendChild(jsNode);这一步,即添加到文档上后开始加载,这跟image不同,image是 image.src = url后开始加载

第十问 如何判断页面是否加载完成?

  • 方式一:window.onload:

当一个文档完全下载到浏览器中时,才会触发window.onload事件。这意味着页面上的全部元素对js而言都是可以操作的,也就是说页面上的所有元素加载完毕才会执行。这种情况对编写功能性代码非常有利,因为无需考虑加载的次序。

 window.onload=function(){
        dosth//你要做的事情
}
  • 方式二:$(document).ready():

会在DOM完全就绪并可以使用时调用。虽然这也意味着所有元素对脚本而言都是可以访问的,但是,并不意味着所有关联的文件都已经下载完毕。换句话说,当HMTL下载完成并解析为DOM树之后,代码就会执行。

$(document).ready(function(){
  dosth//你要做的事情
})

注意:页面加载完成有两种事件,一是ready,表示文档结构已经加载完成(不包含图片等非文字媒体文件),二是onload,指示页 面包含图片等文件在内的所有元素都加载完成。(可以说:ready 在onload 前加载!!!)

  • 方式三:

用document.onreadystatechange的方法来监听状态改变, 然后用document.readyState == “complete”判断是否加载完成,需要的朋友可以参考下,用document.onreadystatechange的方法来监听状态改变, 然后用

document.readyState == “complete”判断是否加载完成

document.onreadystatechange = function()//当页面加载状态改变的时候执行function
 { 
     if(document.readyState == "complete")
      {   //当页面加载状态为完全结束时进入
        init();   //你要做的操作。
      }
  }
复制代

第十一问 cookie和session的区别:

  • ①存在的位置:
    cookie 存在于客户端,临时文件夹中; session存在于服务器的内存中,一个session域对象为一个用户浏览器服务

  • ②安全性
    cookie是以明文的方式存放在客户端的,安全性低,可以通过一个加密算法进行加密后存放; session存放于服务器的内存中,所以安全性好

  • ③网络传输量
    cookie会传递消息给服务器; session本身存放于服务器,不会有传送流量

  • ④生命周期(以20分钟为例)
    cookie的生命周期是累计的,从创建时,就开始计时,20分钟后,cookie生命周期结束;
    session的生命周期是间隔的,从创建时,开始计时如在20分钟,没有访问session,那么session生命周期被销毁。但是,如果在20分钟内(如在第19分钟时)访问过session,那么,将重新计算session的生命周期。关机会造成session生命周期的结束,但是对cookie没有影响

  • ⑤访问范围
    cookie为多个用户浏览器共享; session为一个用户浏览器独享

手写redux-actions核心原理,再也不怕面试官问我redux-actions核心原理

一、前言

为什么介绍redux-actions呢?

第一次见到主要是接手公司原有的项目,发现有之前的大佬在处理redux的时候引入了它。

发现也确实 使得 在对redux的处理上方便了许多,而我为了更好地使用一个组件或者插件,都会去去尝试阅读源码并写成文章 ,这个也不例外。

发现也确实有意思,推荐大家使用redux的时候也引入redux-actions

在这里就介绍一下其使用方式,并且自己手写实现一个简单的redux-actions

二、介绍

在学习 redux 中,总觉得 action 和 reducer 的代码过于呆板,比如

2.1 创建action

let increment = ()=>({type:"increment"})

2.2 reducer

let reducer = (state,action)=>{
    switch(action.type){
      case "increment":return {count:state.count+1};break;
      case "decrement":return {count:state.count-1};break;
      default:return state;
    }
}

2.3 触发action

dispatch(increment())

综上所示,我们难免会觉得 increment 和 reducer 做一个小 demo 还行,遇到逻辑偏复杂的项目后,项目管理维护就呈现弊端了。所以最后的方式就是将它们独立出来,同时在 reducer 中给与开发者更多的主动权,不能仅停留在数字的增增减减。

redux-actions主要函数有createAction、createActions、handleAction、handleActions、combineActions。

基本上就是只有用到createActionhandleActionshandleAction

所以这里我们就只讨论这三个个。

三、 认识与手写createAction()

3.1 用法

一般创建Action方式:

let increment = ()=>({type:"increment"})
let incrementObj = increment();// { type:"increment"}

使用createAction 创建 action

import { createAction } from 'redux-actions';
const increment = createAction('increment');
let incrementObj = increment();// { type:"increment"}
let objincrement = increment(10);// {type:"increment",paylaod:10}

我们可以看到

let increment = ()=>({type:"increment"})
let incrementObj = increment();// { type:"increment"}

const increment = createAction('increment');
let incrementObj = increment();// { type:"increment"}

是等效的,那为什么不直接用传统方式呢?

不难发现有两点:

  1. 传统方式,需要自己写个函数来返回incrementObj,而利用封装好的createAtion就不用自己写函数
  2. 传统方式,在返回的incrementObj若是有payload需要自己添加上去,这是多么麻烦的事情啊,你看下面的代码,如此的不方便。但是用了createAction返回的increment,我们添加上payload,十分简单,直接传个参数,它就直接把它作为payload的值了。
let increment = ()=>({type:"increment",payload:123})

3.2 原理实现

我们先实现个简单,值传入 type参数的,也就是实现下面这段代码的功能

const increment = createAction('increment');
let incrementObj = increment();// { type:"increment"}

我们发现createAction('increment')()才返回最终的action对象。这不就是个柯里化函数吗?

所以我们可以非常简单的写出来,如下面代码所示,我们把type类型当作action对象的一个属性了

function createAction(type) {
    return () => {
        const action = {
            type
        };
        return action;
    };
}

好了现在,现在实现下面这个功能,也就是有payload的情况

const increment = createAction('increment');
let objincrement = increment(10);// {type:"increment",paylaod:10}

很明显,这个payload是 在createAction('increment')返回的函数的参数,所以我们轻而易举地给action添加上了payload。

function createAction(type) {
    return (payload) => {
        const action = {
            type,
            payload
        };
        return action;
    };
}

但是像第一种情况我们是不传payload的,也就是说返回的action是不希望带有payload的,但是这里我们写成这样就是 默认一定要传入payload的了。

所以我们需要添加个判断,当不传payload的时候,action就不添加payload属性。

function createAction(type) {
    return (payload) => {
        const action = {
            type,
        };
        if(payload !== undefined){
            action.payload = payload
        }
        return action;
    };
}

在实际项目中我更喜欢下面这种写法,但它是等价于上面这种写法的

function createAction(type) {
    return (payload) => {
        const action = {
            type,
            ...payload?{payload}:{}
        };
        return action;
    };
}

其实createAction的参数除了type,还可以传入一个回调函数,这个函数表示对payload的处理。

const increment = createAction('increment');
let objincrement = increment(10);// {type:"increment",paylaod:10}

像上面的代码所示,我们希望的是传入10之后是返回的action中的payload是我们传入的2倍数

const increment = createAction('increment',(t)=> t * 2);
let objincrement = increment(10);// {type:"increment",paylaod:20}

现在,就让我们实现一下。

function createAction(type,payloadCreator) {
    return (payload) => {
        const action = {
            type,
        };
        if(payload !== undefined){
            action.payload = payloadCreator(payload)
        }
        return action;
    };
}

卧槽,太简单了吧! 但是我们又犯了前边同样的错误,就是我们使用createAction的时候,不一定会传入payloadCreator这个回调函数,所以我们还需要判断下

function createAction(type,payloadCreator) {
    return (payload) => {
        const action = {
            type,
        };
        if(payload !== undefined){
            action.payload = payloadCreator?payloadCreator(payload):payload
        }
        return action;
    };
}

卧槽,完美。

接下來看看 redux-action的 handleActions吧

四、认识handleActions

我们先看看传统的reducer是怎么使用的

let reducer = (state,action)=>{
    switch(action.type){
      case "increment":return {count:state.count+1};break;
      case "decrement":return {count:state.count-1};break;
      default:return state;
    }
}

再看看使用了handleActions

const INCREMENT = "increment"
const DECREMENT = "decrement"
var reducer = handleActions({
    [INCREMENT]: (state, action) => ({
      counter: state.counter + action.payload
    }),
    [DECREMENT]: (state, action) => ({
      counter: state.counter - action.payload
    })
},initstate)

这里大家不要被{[DECREMENT]:(){}} 的写法吓住哈,就是把属性写成变量了而已。

我们在控制台 console.log(reducer) 看下结果

最后返回的就是一个 reducer 函数。

这样就实现了 reducer 中功能化的自由,想写什么程序,我们只要写在

{[increment]:(state,action)=>{}} 

这个函数内就行,同时也可以把这些函数独立成一个文件,再引入进来就行

import {increment,decrement}from "./reducers.js"
var initstate = {count:0}
var reducer = createReducer({
    [INCREMENT]: increment,
    [DECREMENT]: decrement
},initstate)

reducers.js

//reducers.js
export let increment = (state,action)=>({counter: state.counter + action.payload})
export let decrement = (state,action)=>({counter: state.counter - action.payload})

可见,

handleactions 可以简化 reducers 的写法 不用那么多 switch 而且可以把函数独立出来,这样reducer就再也不会有一大堆代码了。

本来要讲handleActions的实现了,但是在这之前,我们必须先讲一下handleAction,对,你仔细看,没有s

五、认识与手写实现handleAction

5.1 用法

看下使用方式

const incrementReducer = handleAction(INCREMENT, (state, action) => {
  return {counter: state.counter + action.payload}
}, initialState);

可以看出来,跟handleActions的区别 就是,handleAction生成的reducer是专门来处理一个action的。

5.2 原理实现

如果你看过redux原理的话(如果你没看过的话,推荐你去看下我之前的文章Redux 源码解析系列(一) -- Redux的实现**),相信你应该知道reducer(state,action)返回的结果是一个新的state,然后这个新的state会和旧的state进行对比,如果发现两者不一样的话,就会重新渲染使用了state的组件,并且把新的state赋值给旧的state.

也就是说handleAction()返回一个reducer函数,然后incrementReducer()返回一个新的state。

先实现返回一个reducer函数

function handleAction(type, callback) {
    return (state, action) => {
      
    };
}

接下来应当是执行reducer(state,action)是时候返回state,也就是执行下面返回的这个

(state, action) => {
      
};

而其实就是执行callback(state) 然后返回一个新的 state

function handleAction(type, callback) {
    return (state, action) => {
        
      return callback(state)
    };
}

或许你会有疑问,为什么要这么搞,而不直接像下面这样,就少了一层包含。

function handleAction(state,type, callback) {
    return callback(state)
}

这才是它的巧妙之处。它在handleAction()返回的reducer()时,可不一定会执行callback(state),只有handleAction传入的type跟reducer()中传入的action.type匹配到了才会执行,否则就直接return state。表示没有任何处理

function handleAction(type, callback) {
    return (state, action) => {
        
      return callback(state)
    };
}

因此我们需要多加一层判断

function handleAction(type, callback) {
    return (state, action) => {
        if (action.type !== type) {
            return state;
        }
        return callback(state)
    };
}

多么完美啊!

好了现在我们来实现下handleActions

六、handleActions原理实现

function handleActions(handlers, defaultState) {
    const reducers = Object.keys(handlers).map(type => {
        return handleAction(type, handlers[type]);
    });
    const reducer = reduceReducers(...reducers)
    return (state = defaultState, action) => reducer(state, action)
}

看,就这几行代码,是不是很简单,不过应该不好理解,不过没关系,我依旧将它讲得粗俗易懂。

我们拿上面用到的例子来讲好了

var reducer = handleActions({
    [INCREMENT]: (state, action) => ({
      counter: state.counter + action.payload
    }),
    [DECREMENT]: (state, action) => ({
      counter: state.counter - action.payload
    })
},initstate)
{
    [INCREMENT]: (state, action) => ({
      counter: state.counter + action.payload
    }),
    [DECREMENT]: (state, action) => ({
      counter: state.counter - action.payload
    })

上面这个对象,经过下面的代码之后

const reducers = Object.keys(handlers).map(type => {
        return handleAction(type, handlers[type]);
    });

返回的reducer,其实就是

[
  handleAction(INCREMENT,(state, action) => ({
      counter: state.counter + action.payload
  })),
  handleAction(DECREMENT,(state, action) => ({
      counter: state.counter + action.payload
  })),
]

为什么要变成一个handleAction的数组,

我大概想到了,是想每次dispatch(action)的时候,就要遍历去执行这个数组中的所有handleAction。

那岂不是每个handleAction返回的reducer都要执行? 确实,但是别忘了我们上面讲到的,如果handleAction 判断到 type和action.type 是不会对state进行处理的而是直接返回state

function handleAction(type, callback) {
    return (state, action) => {
        if (action.type !== type) {
            return state;
        }
        return callback(state)
    };
}

没有即使每个 handleAction 都执行了也没关系

那应该怎么遍历执行,用map,forEach? 不,都不对。我们看回源码

function handleActions(handlers, defaultState) {
    const reducers = Object.keys(handlers).map(type => {
        return handleAction(type, handlers[type]);
    });
    const reducer = reduceReducers(...reducers)
    return (state = defaultState, action) => reducer(state, action)
}

使用了

const reducer = reduceReducers(...reducers)

用了reduceReducers这个方法,顾名思义,看这方法名,意思就是用reduce这个来遍历执行reducers这个数组。也就是这个数组。

[
  handleAction(INCREMENT,(state, action) => ({
      counter: state.counter + action.payload
  })),
  handleAction(DECREMENT,(state, action) => ({
      counter: state.counter + action.payload
  })),
]

我们看下reduceReducers的内部原理

function reduceReducers(...args) {
    const reducers = args;
    return (prevState, value) => {
        return reducers.reduce((newState, reducer, index) => {
            return reducer(newState, value);
        }, prevState);
    };
};

我们发现将reducers这个数组放入reduceReducers,然后执行reduceReducers,就会返回

(prevState, value) => {
    return reducers.reduce((newState, reducer, index) => {
        return reducer(newState, value);
    }, prevState);
};

这个方法,也就是说执行这个方法就会 执行

return reducers.reduce((newState, reducer, index) => {
        return reducer(newState, value);
    }, prevState);

也就是会使用reduce遍历执行reducers,为什么要用reduce来遍历呢?

这是因为需要把上一个handleAction执行后返回的state传递给下一个。

这个**有一点我们之间之前讲的关于compose函数的**,感兴趣的话,可以去看一下【前端进阶之认识与手写compose方法】

function handleActions(handlers, defaultState) {
    const reducers = Object.keys(handlers).map(type => {
        return handleAction(type, handlers[type]);
    });
    const reducer = reduceReducers(...reducers)
    return (state = defaultState, action) => reducer(state, action)
}

现在也就是说这里的reducer是reduceReducers(...reducers)返回的结果,也就

reducer = (prevState, value) => {
    return reducers.reduce((newState, reducer, index) => {
        return reducer(newState, value);
    }, prevState);
};

而handleActions返回

(state = defaultState, action) => reducer(state, action)

也就是说handleActions其实是返回这样一个方法。

(state = defaultState, action) => {
    return reducers.reduce((newState, reducer, index) => {
        return reducer(newState, value);
    }, state);
}

好家伙,在handleAction之间利用reduce来传递state,真是个好方法,学到了。

贴一下github 的redux-action的源码地址,感兴趣的朋友可以亲自去阅读一下,毕竟本文是做了简化的 redux-actionshttps://github.com/redux-utilities/redux-actions

最后

文章首发于公众号《前端阳光》,欢迎加入技术交流群。

参考文章

手写ReactHook核心原理,再也不怕面试官问我ReactHook原理

React Hook原理

基本准备工作

利用 creact-react-app 创建一个项目
image

已经把项目放到 github:https://github.com/Sunny-lucking/HowToBuildMyReactHook 可以卑微地要个star吗

手写useState

useState的使用

useState可以在函数组件中,添加state Hook。

调用useState会返回一个state变量,以及更新state变量的方法。useState的参数是state变量的初始值,初始值仅在初次渲染时有效

更新state变量的方法,并不会像this.setState一样,合并state。而是替换state变量。
下面是一个简单的例子, 会在页面上渲染count的值,点击setCount的按钮会更新count的值。

function App(){
    const [count, setCount] = useState(0);
    return (
        <div>
            {count}
            <button
                onClick={() => {
                    setCount(count + 1);
                }}
            >
                增加
            </button>
        </div>
    );
}
ReactDOM.render(
    <App />,
  document.getElementById('root')
);

原理实现

let lastState
function useState(initState) {
    lastState = lastState || initState;
    function setState(newState) {
        lastState = newState
    }
    return [lastState,setState]
}
function App(){
    //。。。
}
ReactDOM.render(
    <App />,
  document.getElementById('root')
);

如代码所示,我们自己创建了一个useState方法

当我们使用这个方法时,如果是第一次使用,则取initState的值,否则就取上一次的值(laststate).

在内部,我们创建了一个setState方法,该方法用于更新state的值

然后返回一个lastSate属性和setState方法。

看似完美,但是我们其实忽略了一个问题:每次执行玩setState都应该重新渲染当前组件的。

所以我们需要在setState里面执行刷新操作

let lastState
function useState(initState) {
    lastState = lastState || initState;
    function setState(newState) {
        lastState = newState
        render()
    }
    return [lastState,setState]
}
function App(){
    const [count, setCount] = useState(0);
    return (
        <div>
            {count}
            <button
                onClick={() => {
                    setCount(count + 1);
                }}
            >
                增加
            </button>
        </div>
    );
}
// 新增方法
function render(){
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()

如代码所示,我们在setState里添加了个render方法。
render方法则会执行

ReactDOM.render(
        <App />,
        document.getElementById('root')
    );

也就是重新渲染啦。

好了,现在是不是已经完整了呢?

不,还有个问题:就说我们这里只是用了一个useState,要是我们使用了很多个呢?难道要声明很多个全局变量吗?

这显然是不行的,所以,我们可以设计一个全局数组来保存这些state

let lastState = []
let stateIndex = 0
function useState(initState) {
    lastState[stateIndex] = lastState[stateIndex] || initState;
    const currentIndex = stateIndex
    function setState(newState) {
        lastState[currentIndex ] = newState
        render()
    }
    return [lastState[stateIndex++],setState]
}

这里的currentIndex是利用了闭包的**,将某个state相应的index记录下来了。

好了,useState方法就到这里基本完成了。是不是so easy!!!

React.memo介绍

看下面的代码!你发现什么问题?

import React ,{useState}from 'react';
import ReactDOM from 'react-dom';
import './index.css';
function Child({data}) {
    console.log("天啊,我怎么被渲染啦,我并不希望啊")
    return (
        <div>child</div>
    )
}
function App(){
    const [count, setCount] = useState(0);
    return (
        <div>
            <Child data={123}></Child>
            <button onClick={() => { setCount(count + 1)}}>
                增加
            </button>
        </div>
    );
}
function render(){
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()

没错,就是尽管我们传个子组件的props是固定的值,当父组件的数据更改时,子组件也被重新渲染了,我们是希望当传给子组件的props改变时,才重新渲染子组件。

所以引入了React.memo。

看看介绍

React.memo() 和 PureComponent 很相似,它帮助我们控制何时重新渲染组件。

组件仅在它的 props 发生改变的时候进行重新渲染。通常来说,在组件树中 React 组件,只要有变化就会走一遍渲染流程。但是通过 PureComponent 和 React.memo(),我们可以仅仅让某些组件进行渲染。

import React ,{useState,memo}from 'react';
import ReactDOM from 'react-dom';
import './index.css';
function Child({data}) {
    console.log("天啊,我怎么被渲染啦,我并不希望啊")
    return (
        <div>child</div>
    )
}
Child = memo(Child)
function App(){
    const [count, setCount] = useState(0);
    return (
        <div>
            <Child data={123}></Child>
            <button onClick={() => { setCount(count + 1)}}>
                增加
            </button>
        </div>
    );
}
function render(){
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()

因此,当Child被memo包装后,就只会当props改变时才会重新渲染了。

当然,由于React.memo并不是react-hook的内容,所以这里并不会取讨论它是怎么实现的。

手写useCallback

useCallback的使用

当我们试图给一个子组件传递一个方法的时候,如下代码所示

import React ,{useState,memo}from 'react';
import ReactDOM from 'react-dom';
function Child({data}) {
    console.log("天啊,我怎么被渲染啦,我并不希望啊")
    return (
        <div>child</div>
    )
}
// eslint-disable-next-line
Child = memo(Child)
function App(){
    const [count, setCount] = useState(0);
    const addClick = ()=>{console.log("addClick")}
    return (
        <div>
            
            <Child data={123} onClick={addClick}></Child>
            <button onClick={() => { setCount(count + 1)}}>
                增加
            </button>
        </div>
    );
}
function render(){
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()

发现我们传了一个addClick方法 是固定的,但是却每一次点击按钮子组件都会重新渲染。

这是因为你看似addClick方法没改变,其实旧的和新的addClick是不一样的,如图所示

image

这时,如果想要,传入的都是同一个方法,就要用到useCallBack。

如代码所示

import React ,{useState,memo,useCallback}from 'react';
import ReactDOM from 'react-dom';
function Child({data}) {
    console.log("天啊,我怎么被渲染啦,我并不希望啊")
    return (
        <div>child</div>
    )
}
// eslint-disable-next-line
Child = memo(Child)
function App(){
    const [count, setCount] = useState(0);
    // eslint-disable-next-line
    const addClick = useCallback(()=>{console.log("addClick")},[])
    return (
        <div>
            
            <Child data={123} onClick={addClick}></Child>
            <button onClick={() => { setCount(count + 1)}}>
                增加
            </button>
        </div>
    );
}
function render(){
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()

useCallback钩子的第一个参数是我们要传递给子组件的方法,第二个参数是一个数组,用于监听数组里的元素变化的时候,才会返回一个新的方法。

原理实现

我们知道useCallback有两个参数,所以可以先写

function useCallback(callback,lastCallbackDependencies){
    
    
}

跟useState一样,我们同样需要用全局变量把callback和dependencies保存下来。

let lastCallback
let lastCallbackDependencies
function useCallback(callback,dependencies){
   
}

首先useCallback会判断我们是否传入了依赖项,如果没有传的话,说明要每一次执行useCallback都返回最新的callback

let lastCallback
let lastCallbackDependencies
function useCallback(callback,dependencies){
    if(lastCallbackDependencies){

    }else{ // 没有传入依赖项
        

    }
    return lastCallback
}

所以当我们没有传入依赖项的时候,实际上可以把它当作第一次执行,因此,要把lastCallback和lastCallbackDependencies重新赋值

let lastCallback
let lastCallbackDependencies
function useCallback(callback,dependencies){
    if(lastCallbackDependencies){

    }else{ // 没有传入依赖项
        
        lastCallback = callback
        lastCallbackDependencies = dependencies
    }
    return lastCallback
}

当有传入依赖项的时候,需要看看新的依赖数组的每一项和来的依赖数组的每一项的值是否相等

let lastCallback
let lastCallbackDependencies
function useCallback(callback,dependencies){
    if(lastCallbackDependencies){
        let changed = !dependencies.every((item,index)=>{
            return item === lastCallbackDependencies[index]
        })
    }else{ // 没有传入依赖项
        
        lastCallback = callback
        lastCallbackDependencies = dependencies
    }
    return lastCallback
}
function Child({data}) {
    console.log("天啊,我怎么被渲染啦,我并不希望啊")
    return (
        <div>child</div>
    )
}

当依赖项有值改变的时候,我们需要对lastCallback和lastCallbackDependencies重新赋值

import React ,{useState,memo}from 'react';
import ReactDOM from 'react-dom';
let lastCallback
// eslint-disable-next-line
let lastCallbackDependencies
function useCallback(callback,dependencies){
    if(lastCallbackDependencies){
        let changed = !dependencies.every((item,index)=>{
            return item === lastCallbackDependencies[index]
        })
        if(changed){
            lastCallback = callback
            lastCallbackDependencies = dependencies
        }
    }else{ // 没有传入依赖项
        
        lastCallback = callback
        lastCallbackDependencies = dependencies
    }
    return lastCallback
}
function Child({data}) {
    console.log("天啊,我怎么被渲染啦,我并不希望啊")
    return (
        <div>child</div>
    )
}
// eslint-disable-next-line
Child = memo(Child)
function App(){
    const [count, setCount] = useState(0);
    // eslint-disable-next-line
    const addClick = useCallback(()=>{console.log("addClick")},[])
    return (
        <div>
            
            <Child data={123} onClick={addClick}></Child>
            <button onClick={() => { setCount(count + 1)}}>
                增加
            </button>
        </div>
    );
}
function render(){
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()

手写useMemo

使用

useMemo和useCallback类似,不过useCallback用于缓存函数,而useMemo用于缓存函数返回值

let data = useMemo(()=> ({number}),[number])

如代码所示,利用useMemo用于缓存函数的返回值number,并且当只有监听元素为[number],也就是说,当number的值发生改变的时候,才会重新执行

()=> ({number})

然后返回新的number

原理

所以,useMemo的原理跟useCallback的差不多,仿写即可。

import React ,{useState,memo,}from 'react';
import ReactDOM from 'react-dom';
let lastMemo
// eslint-disable-next-line
let lastMemoDependencies
function useMemo(callback,dependencies){
    if(lastMemoDependencies){
        let changed = !dependencies.every((item,index)=>{
            return item === lastMemoDependencies[index]
        })
        if(changed){
            lastMemo = callback()
            lastMemoDependencies = dependencies
        }
    }else{ // 没有传入依赖项
        lastMemo = callback()
        lastMemoDependencies = dependencies
    }
    return lastMemo
}
function Child({data}) {
    console.log("天啊,我怎么被渲染啦,我并不希望啊")
    return (
        <div>child</div>
    )
}
// eslint-disable-next-line
Child = memo(Child)
function App(){
    const [count, setCount] = useState(0);
    // eslint-disable-next-line
    const [number, setNumber] = useState(20)
    let data = useMemo(()=> ({number}),[number])
    return (
        <div>
            
            <Child data={data}></Child>
            <button onClick={() => { setCount(count + 1)}}>
                增加
            </button>
        </div>
    );
}
function render(){
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()

手写useReducer

使用

先简单介绍下useReducer。

const [state, dispatch] = useReducer(reducer, initState);

useReducer接收两个参数:

第一个参数:reducer函数,第二个参数:初始化的state。

返回值为最新的state和dispatch函数(用来触发reducer函数,计算对应的state)。

按照官方的说法:对于复杂的state操作逻辑,嵌套的state的对象,推荐使用useReducer。

听起来比较抽象,我们先看一个简单的例子:

// 官方 useReducer Demo
// 第一个参数:应用的初始化
const initialState = {count: 0};

// 第二个参数:state的reducer处理函数
function reducer(state, action) {
    switch (action.type) {
        case 'increment':
          return {count: state.count + 1};
        case 'decrement':
           return {count: state.count - 1};
        default:
            throw new Error();
    }
}

function Counter() {
    // 返回值:最新的state和dispatch函数
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
        <>
            // useReducer会根据dispatch的action,返回最终的state,并触发rerender
            Count: {state.count}
            // dispatch 用来接收一个 action参数「reducer中的action」,用来触发reducer函数,更新最新的状态
            <button onClick={() => dispatch({type: 'increment'})}>+</button>
            <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
    );
}

其实意思可以简单的理解为,当state是基本数据类型的时候,可以用useState,当state是对象的时候,可以用reducer,当然这只是一种简单的想法。大家不必引以为意。具体情况视具体场景分析。

原理

看原理你会发现十分简单,简单到不用我说什么,不到十行代码,不信你直接看代码

import React from 'react';
import ReactDOM from 'react-dom';

let lastState
// useReducer原理
function useReducer(reducer,initialState){
    lastState = lastState || initialState
    function dispatch(action){
        lastState = reducer(lastState,action)
        render()
    }
    return [lastState,dispatch]
}

// 官方 useReducer Demo
// 第一个参数:应用的初始化
const initialState = {count: 0};

// 第二个参数:state的reducer处理函数
function reducer(state, action) {
    switch (action.type) {
        case 'increment':
          return {count: state.count + 1};
        case 'decrement':
           return {count: state.count - 1};
        default:
            throw new Error();
    }
}

function Counter() {
    // 返回值:最新的state和dispatch函数
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
        <>
            {/* // useReducer会根据dispatch的action,返回最终的state,并触发rerender */}
            Count: {state.count}
            {/* // dispatch 用来接收一个 action参数「reducer中的action」,用来触发reducer函数,更新最新的状态 */}
            <button onClick={() => dispatch({type: 'increment'})}>+</button>
            <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
    );
}
function render(){
    ReactDOM.render(
        <Counter />,
        document.getElementById('root')
    );
}
render()

手写useContext

使用

createContext 能够创建一个 React 的 上下文(context),然后订阅了这个上下文的组件中,可以拿到上下文中提供的数据或者其他信息。

基本的使用方法:

const MyContext = React.createContext()

如果要使用创建的上下文,需要通过 Context.Provider 最外层包装组件,并且需要显示的通过 <MyContext.Provider value={{xx:xx}}> 的方式传入 value,指定 context 要对外暴露的信息。

子组件在匹配过程中只会匹配最新的 Provider,也就是说如果有下面三个组件:ContextA.Provider->A->ContexB.Provider->B->C

如果 ContextA 和 ContextB 提供了相同的方法,则 C 组件只会选择 ContextB 提供的方法。

通过 React.createContext 创建出来的上下文,在子组件中可以通过 useContext 这个 Hook 获取 Provider 提供的内容

const {funcName} = useContext(MyContext);

从上面代码可以发现,useContext 需要将 MyContext 这个 Context 实例传入,不是字符串,就是实例本身。

这种用法会存在一个比较尴尬的地方,父子组件不在一个目录中,如何共享 MyContext 这个 Context 实例呢?

一般这种情况下,我会通过 Context Manager 统一管理上下文的实例,然后通过 export 将实例导出,在子组件中在将实例 import 进来。

下面我们看看代码,使用起来非常简单

import React, { useState, useContext } from 'react';
import ReactDOM from 'react-dom';
let AppContext = React.createContext()
function Counter() {
    let { state, setState } = useContext(AppContext)
    return (
        <>
            Count: {state.count}

            <button onClick={() => setState({ number: state.number + 1 })}>+</button>
        </>
    );
}
function App() {
    let [state, setState] = useState({ number: 0 })
    return (
        <AppContext.Provider value={{ state, setState }}>
            <div>
                <h1>{state.number}</h1>
                <Counter></Counter>
            </div>
        </AppContext.Provider>
    )
}
function render() {
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()

要是用过vue的同学,会发现,这个机制有点类似vue 中提供的provide和inject

原理

原理非常简单,由于createContext,Provider 不是ReactHook的内容,
所以这里值需要实现useContext,如代码所示,只需要一行代码

import React, { useState } from 'react';
import ReactDOM from 'react-dom';
let AppContext = React.createContext()
function useContext(context){
    return context._currentValue
}
function Counter() {
    let { state, setState } = useContext(AppContext)
    return (
        <>
            <button onClick={() => setState({ number: state.number + 1 })}>+</button>
        </>
    );
}
function App() {
    let [state, setState] = useState({ number: 0 })
    return (
        <AppContext.Provider value={{ state, setState }}>
            <div>
                <h1>{state.number}</h1>
                <Counter></Counter>
            </div>
        </AppContext.Provider>
    )
}
function render() {
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()

手写useEffect

使用

它跟class组件中的componentDidMount,componentDidUpdate,componentWillUnmount具有相同的用途,只不过被合成了一个api。

import React, { useState, useEffect} from 'react';
import ReactDOM from 'react-dom';

function App() {
    let [number, setNumber] = useState(0)
    useEffect(()=>{
        console.log(number);
    },[number])
    return (

        <div>
            <h1>{number}</h1>
            <button onClick={() => setNumber(number+1)}>+</button>
        </div>
    )
}
function render() {
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()

如代码所示,支持两个参数,第二个参数也是用于监听的。
当监听数组中的元素有变化的时候再执行作为第一个参数的执行函数

原理

原理发现其实和useMemo,useCallback类似,只不过,前面前两个有返回值,而useEffect没有。(当然也有返回值,就是那个执行componentWillUnmount函功能的时候写的返回值,但是这里返回值跟前两个作用不一样,因为你不会写

let xxx = useEffect(()=>{
        console.log(number);
    },[number])

来接收返回值。

所以,忽略返回值,你可以直接看代码,真的很类似,简直可以用一模一样来形容

import React, { useState} from 'react';
import ReactDOM from 'react-dom';
let lastEffectDependencies
function useEffect(callback,dependencies){
    if(lastEffectDependencies){
        let changed = !dependencies.every((item,index)=>{
            return item === lastEffectDependencies[index]
        })
        if(changed){
            callback()
            lastEffectDependencies = dependencies
        }
    }else{ 
        callback()
        lastEffectDependencies = dependencies
    }
}
function App() {
    let [number, setNumber] = useState(0)
    useEffect(()=>{
        console.log(number);
    },[number])
    return (

        <div>
            <h1>{number}</h1>
            <button onClick={() => setNumber(number+1)}>+</button>
        </div>
    )
}
function render() {
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()

你以为这样就结束了,其实还没有,因为第一个参数的执行时机错了,实际上作为第一个参数的函数因为是在浏览器渲染结束后执行的。而这里我们是同步执行的。

所以需要改成异步执行callback

import React, { useState} from 'react';
import ReactDOM from 'react-dom';
let lastEffectDependencies
function useEffect(callback,dependencies){
    if(lastEffectDependencies){
        let changed = !dependencies.every((item,index)=>{
            return item === lastEffectDependencies[index]
        })
        if(changed){
            setTimeout(callback())
            lastEffectDependencies = dependencies
        }
    }else{ 
        setTimeout(callback())
        lastEffectDependencies = dependencies
    }
}
function App() {
    let [number, setNumber] = useState(0)
    useEffect(()=>{
        console.log(number);
    },[number])
    return (

        <div>
            <h1>{number}</h1>
            <button onClick={() => setNumber(number+1)}>+</button>
        </div>
    )
}
function render() {
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()

手写useLayoutEffect

使用

官方解释,这两个hook基本相同,调用时机不同,请全部使用useEffect,除非遇到bug或者不可解决的问题,再考虑使用useLayoutEffect。

原理

原理跟useEffect一样,只是调用时机不同

上面说到useEffect的调用时机是浏览器渲染结束后执行的,而useLayoutEffect是在DOM构建完成,浏览器渲染前执行的。

所以这里需要把宏任务setTimeout改成微任务

import React, { useState} from 'react';
import ReactDOM from 'react-dom';
let lastEffectDependencies
function useLayouyEffect(callback,dependencies){
    if(lastEffectDependencies){
        let changed = !dependencies.every((item,index)=>{
            return item === lastEffectDependencies[index]
        })
        if(changed){
            Promise.resolve().then(callback())
            lastEffectDependencies = dependencies
        }
    }else{ 
        Promise.resolve().then(callback())
        lastEffectDependencies = dependencies
    }
}
function App() {
    let [number, setNumber] = useState(0)
    useLayouyEffect(()=>{
        console.log(number);
    },[number])
    return (

        <div>
            <h1>{number}</h1>
            <button onClick={() => setNumber(number+1)}>+</button>
        </div>
    )
}
function render() {
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()

恭喜你阅读到这里,又变强了有没有
已经把项目放到 github:https://github.com/Sunny-lucking/HowToBuildMyReactHook顺便可以卑微地要个star吗

文章首发于公众号《前端阳光》

【深入探究Node】(2)“模块机制” 有十三问

我尝试用一种自问自答的方式记下笔记,就像面试一样,我自个儿觉得有意思极了,希望你也喜欢

第一问: CommonJS规范是干嘛的

CommonJS规范为JavaScript制定了一个美好的愿景——希望JavaScript能够在任何地方运行。

CommonJS规范的提出,主要是为了弥补当前JavaScript没有标准的缺陷,以达到像Python、Ruby和Java具备开发大型应用的基础能力,而不是停留在小脚本程序的阶段

第二问: 那你知道CommonJs模块包含什么吗?

CommonJS对模块的定义十分简单,主要分为模块引用、模块定义和模块标识3个部分。

1.模块引用

模块引用的示例代码如下:

var math = require('math');

在CommonJS规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中。

2.模块定义

在模块中,上下文提供require()方法来引入外部模块。对应引入的功能,上下文提供了exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性。在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式:

// math.js
exports.add = function () {
  var sum = 0,
    i = 0,
    args = arguments,
    l = args.length;
  while (i < l) {
    sum += args[i++];
  }
  return sum;
};

在另一个文件中,我们通过require()方法引入模块后,就能调用定义的属性或方法了:

// program.js
var math = require('math');
exports.increment = function (val) {
  return math.add(val, 1);
};

模块的定义十分简单,接口也十分简洁。它的意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖。如图所示,每个模块具有独立的空间,它们互不干扰,在引用时也显得干净利落。

CommonJS构建的这套模块导出和引入机制使得用户完全不必考虑变量污染,命名空间等方案与之相比相形见绌。

3.模块标识

模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以..开头的相对路径,或者绝对路径。它可以没有文件名后缀.js。

第三问:上面提到了模块引用,你可以谈谈模块引用的过程吗?

在Node中引入模块,需要经历如下3个步骤。

  • (1) 路径分析
  • (2) 文件定位
  • (3) 编译执行

第四问: Node中所有模块的引用都要经历这些?

非也!

在Node中,模块分为两类:一类是Node提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块。

  • ❑ 核心模块部分在Node源代码的编译过程中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的。

  • ❑ 文件模块则是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。

第五问: 我觉得还不够全面,特别重要的一点就是模块二次引用的时候,你没讲。

确实,模块二次引用跟第一次是不一样的。

与前端浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于,浏览器仅仅缓存文件,而Node缓存的是与前端浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于,浏览器仅仅缓存文件,而Node缓存的是编译和执行之后的对象。

不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查。。

不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查。

第六问:你能谈谈 模块引用中的路径分析吗?

可以,路径分析其实就是 模块标志符分析

模块标识符在Node中主要分为以下几类。

  • ❑ 核心模块,如http、fs、path等。
  • ❑ .或..开始的相对路径文件模块。
  • ❑ 以/开始的绝对路径文件模块。
  • ❑ 非路径形式的文件模块,如自定义的connect模块。

而这几种标志符的分析都是不同的。

●核心模块

核心模块的优先级仅次于缓存加载,它在Node的源代码编译过程中已经编译为二进制代码,其加载过程最快。

如果试图加载一个与核心模块标识符相同的自定义模块,那是不会成功的。如果自己编写了一个http用户模块,想要加载成功,必须选择一个不同的标识符或者换用路径的方式。

●路径形式的文件模块

以.、..和/开始的标识符,这里都被当做文件模块来处理。在分析文件模块时,require()方法会将路径转为真实路径,并以真实路径作为索引,将编译执行后的结果存放到缓存中,以使二次加载时更快。

由于文件模块给Node指明了确切的文件位置,所以在查找过程中可以节约大量时间,其加载速度慢于核心模块。

●自定义模块

自定义模块指的是非核心模块,也不是路径形式的标识符。它是一种特殊的文件模块,可能是一个文件或者包的形式(通常我们npm install 的包就是属于自定义模块,它是被放在node_modules包里的)。这类模块的查找是最费时的,也是所有方式中最慢的一种。

第七问: 为什么说自定义模块的查找是最慢的?

模块路径是Node在定位文件模块的具体文件时制定的查找策略,具体表现为一个路径组成的数组。关于这个路径的生成规则,我们可以手动尝试一番。

  • (1) 创建module_path.js文件,其内容为console.log(module.paths);。
  • (2) 将其放到任意一个目录中然后执行node module_path.js。

在Linux下,你可能得到的是这样一个数组输出:

[ '/home/jackson/research/node_modules',
'/home/jackson/node_modules',
'/home/node_modules',
'/node_modules' ]

而在Windows下,也许是这样:

[ 'c:\\nodejs\\node_modules', 'c:\\node_modules' ]

可以看出,模块路径的生成规则如下所示。

  • ❑ 当前文件目录下的node_modules目录。
  • ❑ 父目录下的node_modules目录。
  • ❑ 父目录的父目录下的node_modules目录。
  • ❑ 沿路径向上逐级递归,直到根目录下的node_modules目录。

它的生成方式与JavaScript的原型链或作用域链的查找方式十分类似。在加载的过程中,Node会逐个尝试模块路径中的路径,直到找到目标文件为止。可以看出,当前文件的路径越深,模块查找耗时会越多,这是自定义模块的加载速度是最慢的原因。

第八问: 假如我使用require("myfile")引用文件模块,那这个模块分析过程是怎样的。

我觉得需要分两种情况讨论,一种是 当查到的myfile是文件时就需要按照文件扩展名分析,一种是查不到是文件,而是目录或者包时,就需要继续按照 目录分析

●文件扩展名分析

require()在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。CommonJS模块规范也允许在标识符中不包含文件扩展名,这种情况下,Node会按.js、.json、.node的次序补足扩展名,依次尝试

在尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在。因为Node是单线程的,所以这里是一个会引起性能问题的地方。小诀窍是:如果是.node和.json文件,在传递给require()的标识符中带上扩展名,会加快一点速度。另一个诀窍是:同步配合缓存,可以大幅度缓解Node单线程中阻塞式调用的缺陷

●目录分析和包

在分析标识符的过程中,require()通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,这在引入自定义模块和逐个模块路径进行查找时经常会出现,此时Node会将目录当做一个包来处理。

在这个过程中,Node对CommonJS包规范进行了一定程度的支持。首先,Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析的步骤。

而如果main属性指定的文件名错误,或者压根没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js、index.json、index.node。

如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常。

第九问:上面提到模块的引入的最后步骤模块编译了,其实文件定位之后是加载文件,然后编译,你能谈谈不同文件是怎么加载的吗

在Node中,每个文件模块都是一个Module对象,,它的定义如下:

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  if (parent && parent.children) {
    parent.children.push(this);
  }

  this.filename = null;
  this.loaded = false;
  this.children = [];
}

编译和执行是引入文件模块的最后一个阶段。定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,其载入方法也有所不同,具体如下所示。

  • .js文件。通过fs模块同步读取文件后编译执行。
  • .node文件。这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件。
  • .json文件。通过fs模块同步读取文件后,用JSON.parse()解析返回结果。
  • ❑ 其余扩展名文件。它们都被当做.js文件载入

每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。

根据不同的文件扩展名,Node会调用不同的读取方式,如.json文件的调用如下:

// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
  var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
  try {
    module.exports = JSON.parse(stripBOM(content));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};

其中,Module._extensions会被赋值给require()的extensions属性,所以通过在代码中访问require.extensions可以知道系统中已有的扩展加载方式。编写如下代码测试一下:

console.log(require.extensions);

得到的执行结果如下:

{ '.js': [Function], '.json': [Function], '.node': [Function] }

如果想对自定义的扩展名进行特殊的加载,可以通过类似require.extensions['.ext']的方式实现。

在确定文件的扩展名之后,Node将调用具体的编译方式来将文件执行后返回给调用者。

第十问:前面谈到分别有.js ,.node, .json的文件模块。我比较感兴趣的是.js,即JavaScript模块的编译,你能谈谈吗?

好的。

回到CommonJS模块规范,我们知道每个模块文件中存在着requireexportsmodule这3个变量,但是它们在模块文件中并没有定义,那么从何而来呢?甚至在Node的API文档中,我们知道每个模块中还有__filename__dirname这两个变量的存在,它们又是从何而来的呢?如果我们把直接定义模块的过程放诸在浏览器端,会存在污染全局变量的情况。

事实上,在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装。在头部添加了(function (exports, require, module, __filename, __dirname) {\n,在尾部添加了\n});。一个正常的JavaScript文件会被包装成如下的样子:

(function (exports, require, module, __filename, __dirname) {
  var math = require('math');
  exports.area = function (radius) {
    return Math.PI * radius * radius;
  };
});

这样每个模块文件之间都进行了作用域隔离。包装之后的代码会通过vm原生模块的runInThisContext()方法执行(类似eval,只是具有明确上下文,不污染全局),返回一个具体的function对象。最后,将当前模块对象的exports属性、require()方法、module(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行。

这就是这些变量并没有定义在每个模块文件中却存在的原因。在执行之后,模块的exports属性被返回给了调用方。exports属性上的任何方法和属性都可以被外部调用到,但是模块中的其余变量或属性则不可直接被调用。

至此,require、exports、module的流程已经完整,这就是Node对CommonJS模块规范的实现。

第十一问:好了,顺便也谈谈C/C++模块和JSON文件的编译吧

好的。

C/C++模块的编译

Node调用process.dlopen()方法进行加载和执行。

实际上,.node的模块文件并不需要编译,因为它是编写C/C++模块之后编译生成的,所以这里只有加载和执行的过程。在执行的过程中,模块的exports对象与.node模块产生联系,然后返回给调用者。

C/C++模块给Node使用者带来的优势主要是执行效率方面的,劣势则是C/C++模块的编写门槛比JavaScript高。

JSON文件的编译

.json文件的编译是3种编译方式中最简单的。Node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法得到对象,然后将它赋给模块对象的exports,以供外部调用。

JSON文件在用作项目的配置文件时比较有用。如果你定义了一个JSON文件作为配置,那就不必调用fs模块去异步读取和解析,直接调用require()引入即可。此外,你还可以享受到模块缓存的便利,并且二次引入时也没有性能影响。

第十二问: 我们经常在面试中遇到除了CommonJS外,其实还遇到AMD,能否介绍下AMD呢?

JavaScript在Node出现之后,比别的编程语言多了一项优势,那就是一些模块可以在前后端实现共用,这是因为很多API在各个宿主环境下都提供。但是在实际情况中,前后端的环境是略有差别的。

前后端JavaScript分别搁置在HTTP的两端,它们扮演的角色并不同。浏览器端的JavaScript需要经历从同一个服务器端分发到多个客户端执行,而服务器端JavaScript则是相同的代码需要多次执行。前者的瓶颈在于带宽,后者的瓶颈则在于CPU和内存等资源。前者需要通过网络加载代码,后者从磁盘中加载,两者的加载速度不在一个数量级上。

纵观Node的模块引入过程,几乎全都是同步的。尽管与Node强调异步的行为有些相反,但它是合理的。但是如果前端模块也采用同步的方式来引入,那将会在用户体验上造成很大的问题。UI在初始化过程中需要花费很多时间来等待脚本加载完成。
鉴于网络的原因,CommonJS为后端JavaScript制定的规范并不完全适合前端的应用场景。经过一段争执之后,AMD规范最终在前端应用场景中胜出。它的全称是Asynchronous Module Definition,即是“异步模块定义”。

AMD规范是CommonJS模块规范的一个延伸,它的模块定义如下:

define(id? , dependencies? , factory);

它的模块id和依赖是可选的,与Node模块相似的地方在于factory的内容就是实际代码的内容。下面的代码定义了一个简单的模块:

define(function() {
  var exports = {};
  exports.sayHello = function() {
    alert('Hello from module: ' + module.id);
  };
  return exports;
});

不同之处在于AMD模块需要用define来明确定义一个模块,而在Node实现中是隐式包装的,它们的目的是进行作用域隔离,仅在需要的时候被引入,避免掉过去那种通过全局变量或者全局命名空间的方式,以免变量污染和不小心被修改。另一个区别则是内容需要通过返回的方式实现导出。

第十三问: 其实除了CommonJS ,AMD外,还有CMD,顺便也介绍下吧

好的。

CMD规范由国内的玉伯提出,与AMD规范的主要区别在于定义模块和依赖引入的部分。AMD需要在声明模块的时候指定所有的依赖,通过形参传递依赖到模块内容中:

define(['dep1', 'dep2'], function (dep1, dep2) {
  return function () {};
});

与AMD模块规范相比,CMD模块更接近于Node对CommonJS规范的定义:

define(factory);

在依赖部分,CMD支持动态引入,示例如下:

define(function(require, exports, module) {
  // The module code goes here
});

require、exports和module通过形参传递给模块,在需要依赖模块时,随时调用require()引入即可。

最后

关注公众号《全栈布偶》,置顶公众号,一起进步,一起飞吧

公众号里回复关键词加群,加入技术交流群
文章点个在看,支持一下把!
点击关注我们

关于 HTML5 LocalStorage 的 5 个不为人知的事实

LocalStorage 是HTML5中一个方便使用的 API,它为 Web 开发人员 提供了一个易于使用的5MB的存储空间。使用 LocalStorage API 真的再简单不过了。不信看下:

//Save a value to localStorage
localStorage.setItem('key', 'value to save');
//OR
localStorage.key = 'string value to save';

//Get the value back out of localStorage
localStorage.getItem('key');
//OR
localStorage.key;

//Clear all localStorage values
localStorage.clear();

localStorage API 非常简单,但是很容易忽略有关它们的一些重要细节。关于这个简单的 API,您可能不知道(或可能已经忘记)以下五件事:

1. Secure (SSL) 页面上的 LocalStorge 值是隔离的

根据草案规范,浏览器根据 协议 + 主机名 + 唯一端口(也称为HTML5 Origin)隔离 LocalStorage 值。主机名实现隔离是我们所预期的,因为我们不希望恶意网站访问我们网站的 LocalStorage 数据。但是协议为什么也隔离(即http和https)?

这种隔离的结果意味着保存到http://htmlui.com上的 LocalStorage 的值不能被从https://htmlui.com的页面访问(反之亦然)。

因此如果您的网站同时提供 HTTP 和 HTTPS 页面,请务必小心。(注意:Firefox 提供了一个专有的GlobalStorage,它没有这种 HTTP/HTTPS 隔离。)

2. SessionStorage 值在某些浏览器重启后仍然存在

SessionStorage 与 LocalStorage 不同,它不是为在用户浏览器中长期保存值而设计的。相反,SessionStorage 中的值会在浏览器会话结束时被销毁,这通常是在浏览器窗口关闭时。

不过有一个例外。

当浏览器提供“恢复会话”功能时,通常旨在帮助用户从浏览器/计算机崩溃中快速恢复,SessionStorage 中的值也将被恢复。因此,虽然它是服务器上的一个新“会话”,但从浏览器的角度来看,它是浏览器重启后单个会话的延续。

这使得 SessionStorage 成为一种理想的存储技术,用于临时“备份”用户表单值、在输入时将输入保存到 SessionStorage 以及在页面加载时恢复(如果存在),以进一步帮助用户从浏览器崩溃或意外页面刷新中恢复(尽管浏览器会自行执行其中的一些操作,尤其是在从崩溃中恢复时)。

3.以“隐身”模式创建的LocalStorage值是隔离的

当您在私人/隐身/安全模式(有时更粗略和准确地称为“se情模式”)下启动浏览器时,它将为 LocalStorage 值创建一个新的临时数据库。这意味着当隐私浏览会话关闭时,保存到 LocalStorage 的任何内容都将被销毁,从而使 LocalStorage 的行为更像 SessionStorage。

此外,由于浏览器的“会话恢复”功能不会重新打开私有模式会话,因此在浏览器窗口关闭后,在 SessionStorage 中创建的任何内容也将丢失。实际上,简而言之,在隐私浏览会话期间放入 Local 或 SessionStorage 的任何数据都会在浏览器窗口关闭(有意或无意)后立即丢失。

4. LocalStorage 配额不能大于 5MB

LocalStorage 不应该是 HTML5 的浏览器内存储的主要形式(IndexDB 才是),但某些应用程序可能需要LocalStorage提供不止5m的内存。有没有办法扩大 LocalStorage 配额?没有,没有的,别想了,你在想peach

但是也有个旁门左道!

从技术上讲,LocalStorage 不会阻止同一主机(使用相同的协议和端口)的子域访问他的 LocalStorage 对象。因此,一些浏览器公开了一种解决方法,即授予“a1.website.com”和“a2.website.com”它们自己的 5MB LocalStorage 配额。并且由于两个站点位于同一来源,因此它们可以访问彼此的值。(安全方面注意:这也意味着共享域上的站点,例如 apphost.com,都共享一个 HTML5 存储对象。请谨慎操作!)

因此,虽然存在技术解决方法,但HTML5 Web 存储规范中特别不赞成它。。

但到目前为止只有 Opera 实现了规范的这一部分。所以现在,5MB 是你的现实限制。

5. LocalStorage 可以填充到旧浏览器(包括 IE)中

啊,旧版浏览器(特指 乐色IE浏览器),是每个 HTML5 派对上的失败者。幸运的是,高级浏览器对 LocalStorage 的支持非常好。它在 IE8+ (!)、Firefox 3.5+ 和 Chrome 4+ 中原生可用。很少有 HTML5 规范能像 Web 存储那样得到广泛且一致的支持。

对于旧版本的 IE,polyfill 支持是可用的,这要归功于一个名为“userData”的 IE-only 功能。在 IE5 中引入 userData 是一种 IE 行为,它会打开 1MB 的本地存储。通过包装 userData API,现代 HTML5 应用程序可以处理 polyfill LocalStorage 一直到 IE6(或 IE5,技术上)。

因此,请享受简单的 LocalStorage API,但要注意可能会造成一些令人困惑的调试的内部工作原理

交流

文章每周持续更新,可以微信搜索 前端阳光 第一时间阅读和催更(比博客早一到两篇哟),另外关注公众号,后台回复加群电子书等可以获取好多福利,你懂的。

手写async await核心原理,再也不怕面试官问我async await原理

前言

async await 语法是 ES7出现的,是基于ES6的 promise和generator实现的

generator函数

在之前我专门讲个generator的使用与原理实现,大家没了解过的可以先看那个手写generator核心原理,再也不怕面试官问我generator原理

这里就不再赘述generator,专门的文章讲专门的内容。

await在等待什么

我们先看看下面这代码,这是async await的最简单使用,await后面返回的是一个Promise对象:

async function getResult() {
    await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1);
            console.log(1);
        }, 1000);
    })
}

getResult()

但不知你有没有想过一个问题,为什么会等到返回的promise的对象的状态为非pending的时候才会继续往下执行,也就是resolve执行之后,才会继续执行,就像下面的代码一样

async function getResult() {
    await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1);
            console.log(1);
        }, 1000);
    })
    console.log(2);
}

getResult()

可以看到运行结果是先打印了1,再打印2了,也就是说明在返回的promise对象没执行resolve()前,就一直在await,等它执行。然后再执行下面的程序,那这个是怎么实现的呢?

原理实现

我们看一下下面的代码,输出顺序是什么?

async function getResult() {
    await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1);
            console.log(1);
        }, 1000);
    })
    

    await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2);
            console.log(2);
        }, 500);
    })

    await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(3);
            console.log(3);
        }, 100);
    })

}

getResult()

没错是 1,2,3.

那用generator函数专门来实现这个效果呢

我一开始这样来实现:

function* getResult(params) {
    
    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1);
            console.log(1);
        }, 1000);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2);
            console.log(2);
        }, 500);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(3);
            console.log(3);
        }, 100);
    })
}
const gen = getResult()

gen.next();
gen.next();
gen.next();

但是发现打印顺序是 3,2,1.明显不对。

这里的问题主要是三个 new Promise几乎是同一时刻执行了。才会出现这种问题,所以需要等第一个promise执行完resolve之再执行下一个,所以要这么实现

function* getResult(params) {

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1);
            console.log(1);
        }, 1000);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2);
            console.log(2);
        }, 500);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(3);
            console.log(3);
        }, 100);
    })
}
const gen = getResult()

gen.next().value.then(() => {
    gen.next().value.then(() => {
        gen.next();
    });
});


可以看到这样就打印正常了。

特别 需要解释下。gen.next().value 就是返回的promise对象,不理解的可以看看文首介绍的那篇generator的 文章。手写generator核心原理,再也不怕面试官问我generator原理

优化

但是呢,总不能有多少个await,就要自己写多少个嵌套吧,所以还是需要封装一个函数,显然,递归实现最简单

function* getResult(params) {

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1);
            console.log(1);
        }, 1000);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2);
            console.log(2);
        }, 500);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(3);
            console.log(3);
        }, 100);
    })
}
const gen = getResult()

function co(g) {
    g.next().value.then(()=>{
        co(g)
    })
}

co(gen)

再来看看打印结果

可以发现成功执行了,但是为什么报错了?

这是因为generator方法会返回四次,最后一次的value是undefined。

而实际上返回第三次就表示已经返回done,代表结束了,所以,我们需要判断是否是已经done了,不再让它继续递归

所以可以改成这样

function* getResult(params) {

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(1);
            console.log(1);
        }, 1000);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(2);
            console.log(2);
        }, 500);
    })

    yield new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(3);
            console.log(3);
        }, 100);
    })
}
const gen = getResult()

function co(g) {
    const nextObj = g.next();
    if (nextObj.done) {
        return;
    }
    nextObj.value.then(()=>{
        co(g)
    })
}

co(gen)

可以看到这样就实现了。

完美,这个co其实也是大名鼎鼎的co函数的简单写法

本篇文章关于async 和 await的原理揭秘就到此为止了,再讲下去就不礼貌了。

更多文章,可以去我的github看看

好文推荐

这是我的github,欢迎大家star:https://github.com/Sunny-lucking/blog

手写react核心原理,再也不怕面试官问我react原理

1. 项目基本准备工作

1.1 创建项目

利用npx create-react-app my_react命令创建项目

文章首发于公众号《前端阳光》,项目已经放到github:https://github.com/Sunny-lucking/howToBuildMyReact
觉得可以的话,给个star鼓励下哈啊哈
有什么不对的或者建议或者疑惑,欢迎指出啊!立志写得通俗易懂

1.2 项目结构

将一些用不到的文件删除后,目录变成这样

此时的index.js

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  "sunny",
  document.getElementById('root')
);

2.创建react.js和react-dom.js文件

我们就可以把需要引入react和react-dom的改成自己创建的文件啦

import React from './react';
import ReactDOM from './react-dom';

ReactDOM.render(
  "sunny",
  document.getElementById('root')
);

3.完成react-dom

我们在index.js文件中

ReactDOM.render(
  "sunny",
  document.getElementById('root')
);

以这样的方式使用ReactDOM,说明他有render这个方法。

所以我们可以这样实现react-dom

// react-dom.js
let ReactDOM = {
    render
}
function render(element,container){
    container.innerHTML = `<span>${element}</span>`
    
}

export default ReactDOM

我们看下运行结果

可喜可贺!万里长城迈出了第一步

好了,现在我们给每一个 元素打上 一个标记 ,这样的话 就可以通过这个标记 辨别出与其他 元素的关系,也可以直接通过这标记找到该元素了。

就像下面这张图一样,是不是就直接看出0.0和0.1的父节点就是0了呢?

// react-dom.js
let ReactDOM = {
    render,
    rootIndex:0
}
function render(element,container){
    container.innerHTML = `<span data-reactid=${ReactDOM.rootIndex}>${element}</span>`
}

export default ReactDOM

如代码所示,我们给每一个元素添加了一个标记data-reactid

运行,发现确实标记成功了,哈哈哈

4. 重构render方法

我们前面的render方法

function render(element,container){
    container.innerHTML = `<span data-reactid=${ReactDOM.rootIndex}>${element}</span>`
}

默认传入的element为字符串, 但是实际情况是有可能是 文本节点,也有可能是DOM节点,也有可能是 自定义组件。

所以我们实现一个createUnit方法,将element传入,让它来判断element是什么类型的节点,。然后再返回一个被判断为某种类型,并且添加了对应的方法和属性的对象 。例如,我们的element是字符串类型,那么就返回一个字符串类型的对象,而这个对象自身有element 属性和getMarkUp方法,这个getMarkUp方法,将element转化成真实的dom

其实你也可以简单地认为 createUnit 方法 就是 为 element 对象添加 一个getMarkUp方法

// react-dom.js
import $ from "jquery"
let ReactDOM = {
    render,
    rootIndex:0
}
function render(element,container){
    let unit = createUnit(element)
    let markUp = unit.getMarkUp();// 用来返回HTML标记
    $(container).html(markUp)
}export default ReactDOM

如代码所示,将element传入createUnit方法,获得的unit是一个对象

{
  _currentElement:element,
  getMarkUp(){
    ...
  }
}


再执行 unit的getMarkUp方法,获得到 真实的dom,然后就可以挂载到container上去啦!

注意,如果传入render的element是字符串"sunny",

import React from './react';
import ReactDOM from './react-dom';

ReactDOM.render(
  "sunny",
  document.getElementById('root')
);

也就是说传入createUnit的element是字符串"sunny",那么返回的unit是

{
	_currentElement:"sunny",
	getMarkUp(){
		
	}
}


那怎么写这个createUnit呢?

5. 实现createUnit方法

我们创建一个新的文件叫做unit.js

在这里插入图片描述

// Unit.js
class Unit{
   
}
class TextUnit extends Unit{
    
}

function createUnit(element){
    if(typeof element === 'string' || typeof element === "number"){
        return new TextUnit(element)
    }
}

export {
    createUnit
}

如代码所示,createUnit判断element是字符串时就 new 一个TextUnit的对象,然后返回出去,这个也就是我们上面讲到的unit对象了。

为什么要 TextUnit 继承 于 Unit呢?

这是因为 element除了字符串 ,也有可能是 原生的标签,列如div,span等,也有可能是我们自定义的组件,所以我们先写 了一个 unit类,这个类实现 这几种element 所共有的属性。 然后 具体的 类 ,例如 TextUnit 直接继承 Unit ,再实现自有的 属性就好了。

6. 实现Unit

new Unit 得到的对象应当是这样的

{
  _currentElement:element,
  getMarkUp(){
    ...
  }
}

也就是说,这是所有的 种类都有的属性,所以我们可以这样实现 Unit

class Unit{
    constructor(element){
        this._currentElement = element
    }
    getMarkUp(){
        throw Error("此方法应该被重写,不能直接被使用")
    }
}

为什么getMarkUp 要 throw Error("此方法应该被重写,不能直接被使用")呢?

学过 java或其他语言的同学应该秒懂,这是因为getMarkUp希望是被子类重写的方法,因为每个子类执行这个方法返回的结果是不一样的。

7. 实现TextUnit

到这一步,我们只要重写getMarkUp方法就好了,不过不要忘记,给每一个元素添加一个 reactid,至于为什么,已经在上面说过了,也放了一张大图了哈。

class TextUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid
        return `<span data-reactid=${reactid}>${this._currentElement}</span>`
    }
}

好了,到这里先看下完整的Unit.js长什么样子吧

// Unit.js
class Unit{
    constructor(element){
        this._currentElement = element
    }
    getMarkUp(){
        throw Error("此方法应该被重写,不能直接被使用")
    }
}
class TextUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid
        return `<span data-reactid=${reactid}>${this._currentElement}</span>`
    }
}

function createUnit(element){
    if(typeof element === 'string' || typeof element === "number"){
        return new TextUnit(element)
    }
}

export {
    createUnit
}

我们在index.js引入 unit测试下

// index.js
import React from './react';
import ReactDOM from './react-dom';

ReactDOM.render(
  "sunny",
  document.getElementById('root')
);
// react-dom.js
import {createUnit} from './unit'
import $ from "jquery"
let ReactDOM = {
    render,
    rootIndex:0
}
function render(element,container){
    let unit = createUnit(element)
    let markUp = unit.getMarkUp(ReactDOM.rootIndex);// 用来返回HTML标记
    $(container).html(markUp)
}

export default ReactDOM

在这里插入图片描述

意料之内的成功!哈哈哈啊

8. 理解React.creacteElement方法

在第一次学习react的时候,我总会带着许多疑问。比如看到下面的代码就会想:为什么我们只是引入了React,但是并没有明显的看到我们在其他地方用,这时我就会想着既然没有用到,那如果删除之后会不会受到影响呢?答案当然是不行的。

import React from 'react';
import ReactDOM from 'react-dom';

let element = (
    <h1 id="title" className="bg" style={{color: 'red'}}>
        hello
        <span>world</span>
    </h1>
)

console.log({type: element.type, props:element.props})

ReactDOM.render(element,document.getElementById('root'));

当我们带着这个问题去研究的时候会发现其实在渲染element的时候调了React.createElement(),所以上面的问题就在这里找到了答案。

如下面代码所示,这就是从jsx语法到React.createElement的转化

<h1 id="title" className="bg" style={{color: 'red'}}>
        hello
        <span>world</span>
</h1>

//上面的这段代码很简单,但是我们都知道react是所谓的虚拟dom,当然不可能就是我们看到的这样。当我们将上面的代码经过babel转译后,我们再看看

React.createElement("h1", {
  id: "title",
  className: "bg",
  style: {
    color: 'red'
  }
}, "hello", React.createElement("span", null, "world"));

document有createElement()方法,React也有createElement()方法,下面就来介绍React的createElement()方法。

var reactElement = ReactElement.createElement(
  	... // 标签名称字符串/ReactClass,
  	... // [元素的属性值对对象],
  	... // [元素的子节点]
)

1、参数:

1)第一个参数:可以是一个html标签名称字符串,也可以是一个ReactClass(必须);

2)第二个参数:元素的属性值对对象(可选),这些属性可以通过this.props.*来调用;

3)第三个参数开始:元素的子节点(可选)。

2、返回值:

一个给定类型的ReactElement元素

我们可以改下我们的index.js

// index.js
import React from './react';
import ReactDOM from './react-dom';

var li1 = React.createElement('li', {onClick:()=>{alert("click")}}, 'First');
var li2 = React.createElement('li', {}, 'Second');
var li3 = React.createElement('li', {}, 'Third');
var ul = React.createElement('ul', {className: 'list'}, li1, li2, li3);
console.log(ul);
ReactDOM.render(ul,document.getElementById('root'))

可以就看下 ul 最终的打印 期待结果
在这里插入图片描述

由此 ,我们只知道了,ReactElement.createElement方法将生产一个给定类型的ReactElement元素,然后这个对象被传入 render方法,然后进行了上面讲到的 createUnit和getMarkUp操作。

9. 实现React.createElement方法

经过上面的讲解,我们大概已经知道React.createElement方法的作用了,现在就来看看是怎么实现的

在这里插入图片描述
我们创建了一个新的文件element.js

// element.js
class Element {
    constructor(type,props){
        this.type = type
        this.props = props
    }

}
function createElement(type,props={},...children){
    props.children = children || [];
    return new Element(type,props)
}

export {
    Element,
    createElement
}

我们 定义了一个 Element 类 ,然后在createElement方法里创建了这个类的对象,
并且return出去了

没错,这个对象就是上面所说的给定类型的ReactElement元素,也就是下面这张图所显示的
在这里插入图片描述

我们应当是这样React.createElement()调用这个方法的,所以我们要把这个方法挂载到react身上。

我们前面还没有实现react.js

其实,很简单,就是返回一个React对象,这个对象有createElement方法

 // react.js
 import {createElement} from "./element"
 const React = {
    createElement
 }
 export default React

10. 实现NativeUnit

上面实现了 createElement返回 给定类型的ReactElement元素 后,就将改元素传入,render方法,因此 就会经过 createUnit方法, createUnit方法判断是属于什么类型的 元素,如下面代码

// Unit.js
import {Element} from "./element" // 新增代码
class Unit{
    constructor(element){
        this._currentElement = element
    }
    getMarkUp(){
        throw Error("此方法应该被重写,不能直接被使用")
    }
}
class TextUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid
        return `<span data-reactid=${reactid}>${this._currentElement}</span>`
    }
}

function createUnit(element){
    if(typeof element === 'string' || typeof element === "number"){
        return new TextUnit(element)
    }
    // 新增代码
    if(element instanceof Element && typeof element.type === "string"){
        return new NativeUnit(element)
    }
}

export {
    createUnit
}

好了,现在我们来实现NativeUnit类,其实主要就是实现NativeUnit的getMarkUp方法

class NativeUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid 
        let {type,props} = this._currentElement;
    }
}

要明确的一点是,NativeUnit 的getMarkUp方法,是要把
在这里插入图片描述
这样一个element 对象转化为 真实的dom的

因此,我们可以这样完善getMarkUp方法

class NativeUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid 
        let {type,props} = this._currentElement;
        let tagStart = `<${type} `
        let childString = ''
        let tagEnd = `</${type}>`
        for(let propName in props){
            if(/^on[A-Z]/.test(propName)){ // 添加绑定事件
                
            }else if(propName === 'style'){ // 如果是一个样式对象

            }else if(propName === 'className'){ // 如果是一个类名

            }else if(propName === 'children'){ // 如果是子元素

            }else { // 其他 自定义的属性 例如 reactid
                tagStart += (` ${propName}=${props[propName]} `)
            }
        }
        return tagStart+'>' + childString +tagEnd
    }
}

这只是 大体上的 一个实现 ,其实就是 把标签 和属性 以及 子元素 拼接成 字符串,然后返回出去。

我们测试下,现在有没有 把ul 渲染出来

// index.js
import React from './react';
import ReactDOM from './react-dom';

var li1 = React.createElement('li', {}, 'First');
var li2 = React.createElement('li', {}, 'Second');
var li3 = React.createElement('li', {}, 'Third');
var ul = React.createElement('ul', {className: 'list'}, li1, li2, li3);
console.log(ul);
ReactDOM.render(ul,document.getElementById('root'))

在这里插入图片描述
发现确实成功渲染出来了,但是 属性和 子元素还没有,这是因为我们 还没实现 具体 的功能。

现在我们来实现事件绑定 功能

class NativeUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid 
        let {type,props} = this._currentElement;
        let tagStart = `<${type} data-reactid="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for(let propName in props){
        	// 新增代码
            if(/^on[A-Z]/.test(propName)){ // 添加绑定事件
                let eventName = propName.slice(2).toLowerCase(); // 获取click
                $(document).delegate(`[data-reactid="${this._reactid}"]`,`${eventName}.${this._reactid}`,props[propName])
            }else if(propName === 'style'){ // 如果是一个样式对象
               
            }else if(propName === 'className'){ // 如果是一个类名
                
            }else if(propName === 'children'){ // 如果是子元素
               
            }else { // 其他 自定义的属性 例如 reactid
                
            }
        }
        return tagStart+'>' + childString +tagEnd
    }
}

在这里,我们是用了事件代理的模式,之所以用事件代理,是因为这些标签元素还没被渲染到页面上,但我们又必须提前绑定事件,所以需要用到事件代理

接下来,实现 样式对象的绑定

class NativeUnit extends Unit{
    getMarkUp(reactid){
        this._reactid = reactid 
        let {type,props} = this._currentElement;
        let tagStart = `<${type} data-reactid="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for(let propName in props){
            if(/^on[A-Z]/.test(propName)){ // 添加绑定事件
                ...
            }else if(propName === 'style'){ // 如果是一个样式对象
                let styleObj = props[propName]
                let styles = Object.entries(styleObj).map(([attr, value]) => {
                    return `${attr.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)}:${value}`;
                }).join(';')
                tagStart += (` style="${styles}" `)
            }else if(propName === 'className'){ // 如果是一个类名
                
            }else if(propName === 'children'){ // 如果是子元素
               
            }else { // 其他 自定义的属性 例如 reactid
              
            }
        }
        return tagStart+'>' + childString +tagEnd
    }
}

这里 其实就是把

{style:{backgroundColor:"red"}}

对象中的 style这个对象 属性拿出来,

然后把backgroundColor 通过正则 变化成background-color

然后再拼接到tagStart中。

接下来再实现className,发现这个也太简单了吧

class NativeUnit extends Unit {
    getMarkUp(reactid) {
        this._reactid = reactid
        let { type, props } = this._currentElement;
        let tagStart = `<${type} data-reactid="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for (let propName in props) {
            if (/^on[A-Z]/.test(propName)) { // 添加绑定事件
              	...
            } else if (propName === 'style') { // 如果是一个样式对象
                ...
            } else if (propName === 'className') { // 如果是一个类名
                tagStart += (` class="${props[propName]}"`)
            } else if (propName === 'children') { // 如果是子元素
               ...
            } else { // 其他 自定义的属性 例如 reactid
                ...
            }
        }
        return tagStart + '>' + childString + tagEnd
    }
}

为什么这么简单呢? 因为只需要把

className: 'list'

中的className变化成 class就可以了。OMG!!

接下来,是时候实现子元素的拼接了哈

class NativeUnit extends Unit {
    getMarkUp(reactid) {
        this._reactid = reactid
        let { type, props } = this._currentElement;
        let tagStart = `<${type} data-reactid="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for (let propName in props) {
            if (/^on[A-Z]/.test(propName)) { // 添加绑定事件
                ...
            } else if (propName === 'style') { // 如果是一个样式对象
                ...
            } else if (propName === 'className') { // 如果是一个类名
                ...
            } else if (propName === 'children') { // 如果是子元素
                let children = props[propName];
                children.forEach((child, index) => {
                    let childUnit = createUnit(child); // 可能是字符串 ,也可能是原生标签,也可能是自定义属性
                    let childMarkUp = childUnit.getMarkUp(`${this._reactid}.${index}`)
                    childString += childMarkUp;
                })
            } else { // 其他 自定义的属性 例如 reactid
                
            }
        }
        return tagStart + '>' + childString + tagEnd
    }
}

发现子元素 ,其实只要进行递归操作,也就是将子元素传进createUnit,把返回的childUnit 通过childMarkUp 方法变成 真实动,再拼接到childString 就好了。 其实想想也挺简单,就类似深拷贝的操作。

好了,接下来就是 其他属性了

class NativeUnit extends Unit {
    getMarkUp(reactid) {
        this._reactid = reactid
        let { type, props } = this._currentElement;
        let tagStart = `<${type} data-reactid="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        for (let propName in props) {
            if (/^on[A-Z]/.test(propName)) { // 添加绑定事件
               ...
            } else if (propName === 'style') { // 如果是一个样式对象
               ...
            } else if (propName === 'className') { // 如果是一个类名
               ...
            } else if (propName === 'children') { // 如果是子元素
                ...
            } else { // 其他 自定义的属性 例如 reactid
                tagStart += (` ${propName}=${props[propName]} `)
            }
        }
        return tagStart + '>' + childString + tagEnd
    }
}

其他属性直接就拼上去就好了哈哈哈

好了。现在我们已经完成了NativeUini的getMarkUp方法。我们来测试一下是否成功了没有吧!
在这里插入图片描述
害,不出所料地成功了。

11. 完成React.Component

接下来我们看看自定义组件是怎么被渲染的,例如下面的Counter组件

// index.js
class Counter extends React.Component{
    constructor(props){
        super(props)
        this.state = {number:0};
    }
    render(){
        let p = React.createElement('p',{style:{color:'red'}},this.state.number);
        let button = React.createElement('button',{},"+")
        return React.createElement('div',{id:'counter'},p,button)
    }
}
let element = React.createElement(Counter,{name:"计时器"})
ReactDOM.render(element,document.getElementById('root'))

我们发现自定义组件好像需要继承React.Component。这是为什么呢?

我之前一直误认为所有的生命周期都是从Component继承过来的,也许有很多小伙伴都和我一样有这样的误解,直到我看了Component源码才恍然大悟,原来我们用的setState和forceUpdate方法是来源于这里

知道这个原因后,我们就可以先简单地实现React.Component了

// component.js
class Component{
    constructor(props){
        this.props = props
    }
}

export {
    Component
}

然后再引入react中即可

 // react.js
 import {createElement} from "./element"
 import {Component} from "./component"
 const React = {
    createElement,
    Component
 }
 export default React

跟 处理NativeUnit一样,先通过createUnit判断element是属于什么类型,如果是自定义组件就 return CompositeUnit

// Unit.js
import { Element } from "./element" // 新增代码
import $ from "jquery"
class Unit {
    constructor(element) {
        this._currentElement = element
    }
    getMarkUp() {
        throw Error("此方法应该被重写,不能直接被使用")
    }
}
class TextUnit extends Unit {
    
}

class NativeUnit extends Unit {
   
}

function createUnit(element) {
    if (typeof element === 'string' || typeof element === "number") {
        return new TextUnit(element)
    }
    if (element instanceof Element && typeof element.type === "string") {
        return new NativeUnit(element)
    }
    // 新增代码
    if(element instanceof Element && typeof element.type === 'function'){
        return new CompositeUnit(element)
    }

}


export {
    createUnit
}

为什么是用 typeof element.type === 'function'来判断 呢? 因为Counter是 一个类,而类在js中的本质就是function

好了,接下来实现一下CompositeUnit类

class CompositeUnit extends Unit{
    getMarkUp(reactid){
      this._reactid = reactid
      let {type:Component,props} = this._currentElement // 实际上,在例子中type === Counter
      let componentInstance = new Component(props);
      let renderElement = componentInstance.render();
      let renderUnit = createUnit(renderElement);
      return renderUnit.getMarkUp(this._reactid)
    }
}

咦,好简短 啊,不过 没那么 简单,但是让 我的三寸不烂之舌来讲解一下,包懂

此时的_currentElement 是:

{
	type:Counter,
	props:{}
}

let {type:Component,props} = this._currentElement // 实际上,在例子中type就是Counter
new Component(props);其实就是new Counter

也就是我们上面例子中写的

class Counter extends React.Component{
    constructor(props){
        super(props)
        this.state = {number:0};
    }
    render(){
        let p = React.createElement('p',{style:{color:'red'}},this.state.number);
        let button = React.createElement('button',{},"+")
        return React.createElement('div',{id:'counter'},p,button)
    }
}
let element = React.createElement(Counter,{name:"计时器"})
ReactDOM.render(element,document.getElementById('root'))

可想而知 ,通过new Counter就获得了Counter的实例

也就是componentInstance ,而每一个Counter的实例都会有render方法,所以执行componentInstance.render()

就获得一个给定类型的ReactElement元素(好熟悉的一句话,对,我们在上面讲到过)。

然后就把这个ReactElement元素对象传给createUnit,获得一个具有getMarkUp的renderUnit 对象,
然后就可以执行renderUnit.getMarkUp(this._reactid)获得真实dom,就可以返回了。

其实,仔细想想,就会发现,在

let renderUnit = createUnit(renderElement);

之前,我们是在处理自定义组件Counter。

而到了

let renderUnit = createUnit(renderElement);

这一步,其实就是在处理NativeUnit。(细思极恐。。)

好了,测试一下在这里插入图片描述
发现确实成功了。

12. 实现 componentWillMount

我们在之前的例子上添加个componentWillMount 生命周期函数吧

// index.js
import React from './react';
import ReactDOM from './react-dom';

class Counter extends React.Component{
    constructor(props){
        super(props)
        this.state = {number:0};
    }
    componentWillMount(){
        console.log("阳光你好,我是componentWillMount");
    }
    render(){
        let p = React.createElement('p',{style:{color:'red'}},this.state.number);
        let button = React.createElement('button',{},"+")
        return React.createElement('div',{id:'counter'},p,button)
    }
}
let element = React.createElement(Counter,{name:"计时器"})
ReactDOM.render(element,document.getElementById('root'))

我们知道componentWillMount 实在组件渲染前执行的,所以我们可以在render之前执行这个生命周期函数

class CompositeUnit extends Unit{
    getMarkUp(reactid){
      this._reactid = reactid
      let {type:Component,props} = this._currentElement // 实际上,在例子中type === Counter
      let componentInstance = new Component(props);
      componentInstance.componentWillMount && componentInstance.componentWillMount() // 添加生命周期函数
      let renderElement = componentInstance.render();
      let renderUnit = createUnit(renderElement);
      return renderUnit.getMarkUp(this._reactid)
    }
}

可能聪明的小伙伴会问,不是说componentWillMount是在组件重新渲染前执行的吗?那组件没挂到页面上应该都是渲染前,所以componentWillMount也可以在return renderUnit.getMarkUp(this._reactid)前执行啊。

其实要回答这个问题,倒不如回答另一个问题:

父组件的componentWillMount和子组件的componentWillMount哪个先执行。

答案是父组件先执行。

这是因为在父组件中会先执行 父组件的componentWillMount ,然后执行componentInstance.render();的时候,会解析子组件,然后又进入子组件的getMarkUp。又执行子组件的componentWillMount 。

若要回答 为什么componentWillMount 要在 render函数执行前执行,只能说,react就是这么设计的哈哈哈

13. 实现componentDidMount

众所周知,componentDidMount是在组件渲染,也就是挂载到页面后才执行的。

所以,我们可以在返回组件的真实dom之前 就监听 一个mounted事件,这个事件执行componentDidMount方法。

class CompositeUnit extends Unit{
    getMarkUp(reactid){
      this._reactid = reactid
      let {type:Component,props} = this._currentElement // 实际上,在例子中type === Counter
      let componentInstance = new Component(props);
      componentInstance.componentWillMount && componentInstance.componentWillMount()
      let renderElement = componentInstance.render();
      let renderUnit = createUnit(renderElement);
      $(document).on("mounted",()=>{
          componentInstance.componentDidMount &&  componentInstance.componentDidMount()
      })
      return renderUnit.getMarkUp(this._reactid)
    }
}

然后 再在 把组件的dom挂载到 页面上后再触发这个 mounted事件

// react-dom.js
import {createUnit} from './unit'
import $ from "jquery"
let ReactDOM = {
    render,
    rootIndex:0
}
function render(element,container){
    let unit = createUnit(element)
    let markUp = unit.getMarkUp(ReactDOM.rootIndex);// 用来返回HTML标记
    $(container).html(markUp)
    $(document).trigger("mounted")
}

export default ReactDOM

由此依赖,就实现了,componentDidMount 生命周期函数,哈哈哈。

测试一下,成功了没有哈
在这里插入图片描述
啊,一如既往的成功,可能好奇的你问我为什么每次测试都成功,那是因为,不成功也被我调试到成功了。

为了下面 实现 setState 功能,我们 修改一下 CompositeUnit 的getMarkUp方法。

class CompositeUnit extends Unit{
    getMarkUp(reactid){
      this._reactid = reactid
      let {type:Component,props} = this._currentElement // 实际上,在例子中type === Counter
      let componentInstance = this._componentInstance = new Component(props); // 把 实例对象 保存到这个 当前的 unit
      componentInstance._currentUnit = this // 把 unit 挂到 实例componentInstance 
      componentInstance.componentWillMount && componentInstance.componentWillMount()
      let renderElement = componentInstance.render();
      let renderUnit = this._renderUnit = createUnit(renderElement); // 把渲染内容对象也挂载到当前 unit
      $(document).on("mounted",()=>{
          componentInstance.componentDidMount &&  componentInstance.componentDidMount()
      })
      return renderUnit.getMarkUp(this._reactid)
    }
}

我们为这个 CompositeUnit 的实例添加了

  1. _componentInstance :用了表示 当前组件的实例 (我们所写的Counter组件)
  2. _renderUnit: 当前组件的render方法返回的react元素对应的unit._currentElement

另外,我们也通过

componentInstance._currentUnit = this // 把 unit 挂到 实例componentInstance 

把当前 的unit 挂载到了 组件实例componentInstance身上。

可见 组件的实例保存了 当前 unit,当前的unit也保存了组件实例

14. 实现setState

我们看下面的例子,每隔一秒钟就number+1

// index.js
import React from './react';
import ReactDOM from './react-dom';
import $ from 'jquery'
class Counter extends React.Component{
    constructor(props){
        super(props)
        this.state = {number:0};
    }
    componentWillMount(){
        console.log("阳光你好,我是componentWillMount");
        $(document).on("mounted",()=>{
            console.log(456);
            
        })
    }
    componentDidMount(){
        setInterval(()=>{
            this.setState({number:this.state.number+1})
        },1000)
    }
    render(){
        
        return this.state.number
    }
}
let element = React.createElement(Counter,{name:"计时器"})
ReactDOM.render(element,document.getElementById('root'))

前面说到,setState方法是从Component组件继承过来的。所以我们给Component组件添加setState方法

// component.js
class Component{
    constructor(props){
        this.props = props
    }
    setState(partialState){
        // 第一个参数是新的元素,第二个参数是新的状态
        this._currentUnit.update(null,partialState)
    }
}

export {
    Component
}

我们发现原来是在setState方法里调用了当前实例的对应的unit的update方法,它传进去了 部分state的值。

看到这里,我们就知道了,我们需要回到 CompositeUnit类添加一个update方法。

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        // 有传新元素的话就更新currentElement为新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 获取新的状态,并且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
    }
    getMarkUp(reactid){
     ...
    }
}

我们首先 更换了_currentElement的值,这里为什么会有 有或者没有nextElement的情况呢?

(主要就是因为,如果 _currentElement 是 字符串或者数字的话,那么它就需要 传nextElement 来替换掉旧的 _currentElement 。而如果不是字符串或者数字的话,是不需要传的。而CompositeUnit 必定是组件的,所以不用传nextElement )。

接着,我们 通过下面这句代码获取了最新的state,并且更新了组件的state

 let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);

获取 最新的 props跟获取state的方式不一样,props是跟_currentElement 绑定在一起的,所以获取最新的props是通过

let nextProps = this._currentElement.props

接下来,我们要先获取新旧的渲染元素,然后拿来比较,怎么获取呢?

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        // 有传新元素的话就更新currentElement为新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 获取新的状态,并且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
        // 下面要进行比较更新
        // 先得到上次渲染的unit
        let preRenderedUnitInstance = this._renderUnit;
        // 通过上次渲染的unit得到上次渲染的元素
        let preRenderElement = preRenderedUnitInstance._currentElement
        // 得到最新的渲染元素
        let nextRenderElement = this._componentInstance.render()

    }
    getMarkUp(reactid){
     	
    }
}

我们先得到上次渲染的unit,再通过上次渲染的unit得到上次渲染的元素preRenderElement ,

再通过this._componentInstance.render()得到下次渲染的元素nextRenderElement 。

接下来就可以进行比较这两个元素了

我们首先会判断要不要进行深度比较。

如果不是进行深度比较就非常简单

直接获取新的渲染unit,然后通过getMarkUp获得要渲染的dom,接着就把当前的组件里的dom元素替换掉

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        // 有传新元素的话就更新currentElement为新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 获取新的状态,并且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
        // 下面要进行比较更新
        // 先得到上次渲染的unit
        let preRenderedUnitInstance = this._renderUnit;
        // 通过上次渲染的unit得到上次渲染的元素
        let preRenderElement = preRenderedUnitInstance._currentElement
        // 得到最新的渲染元素
        let nextRenderElement = this._componentInstance.render()
        // 如果新旧两个元素类型一样,则可以进行深度比较,如果不一样,直接干掉老的元素,新建新的
        if(shouldDeepCompare(preRenderElement,nextRenderElement)){

        }else{
            this._renderUnit = createUnit(nextRenderElement)
            let nextMarkUp = this._renderUnit.getMarkUp(this._reactid)
            $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp)
        }

    }
    getMarkUp(reactid){
     
    }
}

我们先简单地写一下shouldDeepCompare方法,直接return false,来测试一下 非深度比较,是否能够正确执行

function shouldDeepCompare(){
    return false
}
class CompositeUnit extends Unit{
    update(nextElement,partialState){
        // 有传新元素的话就更新currentElement为新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 获取新的状态,并且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
        // 下面要进行比较更新
        // 先得到上次渲染的unit
        let preRenderedUnitInstance = this._renderUnit;
        // 通过上次渲染的unit得到上次渲染的元素
        let preRenderElement = preRenderedUnitInstance._currentElement
        // 得到最新的渲染元素
        let nextRenderElement = this._componentInstance.render()
        // 如果新旧两个元素类型一样,则可以进行深度比较,如果不一样,直接干掉老的元素,新建新的
        if(shouldDeepCompare(preRenderElement,nextRenderElement)){

        }else{
            this._renderUnit = createUnit(nextRenderElement)
            let nextMarkUp = this._renderUnit.getMarkUp(this._reactid)
            $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp)
        }

    }
    getMarkUp(reactid){
     
    }
}

在这里插入图片描述

发现确实成功了。

如果可以进行深度比较呢?

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        // 有传新元素的话就更新currentElement为新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 获取新的状态,并且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
        // 下面要进行比较更新
        // 先得到上次渲染的unit
        let preRenderedUnitInstance = this._renderUnit;
        // 通过上次渲染的unit得到上次渲染的元素
        let preRenderElement = preRenderedUnitInstance._currentElement
        // 得到最新的渲染元素
        let nextRenderElement = this._componentInstance.render()
        // 如果新旧两个元素类型一样,则可以进行深度比较,如果不一样,直接干掉老的元素,新建新的
        if(shouldDeepCompare(preRenderElement,nextRenderElement)){
            // 如果可以进行深度比较,则把更新的nextRenderElement传进去
            preRenderedUnitInstance.update(nextRenderElement)
            
        }else{
            this._renderUnit = createUnit(nextRenderElement)
            let nextMarkUp = this._renderUnit.getMarkUp(this._reactid)
            $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp)
        }

    }
    getMarkUp(reactid){
      
    }
}

如果可以深度,就执行

 preRenderedUnitInstance.update(nextRenderElement)

这是什么意思?

我们当前是在执行渲染Counter的话,那preRenderedUnitInstance 是什么呢?

没错!它是Counter组件 执行render方法 ,再执行createUnit获得的

在这里插入图片描述

这个字符串的 unit

然后调用了这个 unit的 update方法

注意,这里 的unit是字符串的 unit,也就是说是 TextUnit

所以我们需要实现 TextUnit 的update 方法

class TextUnit extends Unit {
    getMarkUp(reactid) {
        this._reactid = reactid
        return `<span data-reactid=${reactid}>${this._currentElement}</span>`
    }
    update(nextElement){
        debugger
        if(this._currentElement !== nextElement){
            this._currentElement = nextElement
             $(`[data-reactid="${this._reactid}"]`).html(nextElement)
        }
    }
}

TextUnit 的update方法非常简单,先判断 渲染内容有没有变化,有的话就 替换点字符串的内容

并把当前unit 的_currentElement 替换成最新的nextElement

我们简单的把shouldDeepCompare 改成 return true,测试一下深度比较

function shouldDeepCompare(){
    return true
}

在这里插入图片描述
一如既往成功

15. 实现shouldComponentUpdate方法

我们知道有个shouldComponentUpdate,用来决定要不要 重渲染 该组件的

shouldComponentUpdate(nextProps, nextState) {
  return nextState.someData !== this.state.someData
}

显然,它要我们传入 两个参数,分别是 组件更新后的nextProps和nextState

而在 还是上面,实现 update的过程中,我们已经得到了nextState 和nextProps

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        。。。
        // 获取新的状态,并且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
        // 下面要进行比较更新
        。。。

    }
    getMarkUp(reactid){
     
    }
}

所以,我们可以在update里执行shouldComponentUpdate方法,来确定要不要重新渲染组件

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        // 有传新元素的话就更新currentElement为新的元素
        this._currentElement = nextElement || this._currentElement; 
        // 获取新的状态,并且更新组件的state
        let nextState = this._componentInstance.state = Object.assign(this._componentInstance.state,partialState);
        // 新的属性对象
        let nextProps = this._currentElement.props
        if(this._componentInstance.shouldComponentUpdate && !this._componentInstance.shouldComponentUpdate(nextProps,nextState)){
            return;
        }
        // 下面要进行比较更新
        // 先得到上次渲染的unit
        let preRenderedUnitInstance = this._renderUnit;
        // 通过上次渲染的unit得到上次渲染的元素
        let preRenderElement = preRenderedUnitInstance._currentElement
        // 得到最新的渲染元素
        let nextRenderElement = this._componentInstance.render()
        // 如果新旧两个元素类型一样,则可以进行深度比较,如果不一样,直接干掉老的元素,新建新的
        if(shouldDeepCompare(preRenderElement,nextRenderElement)){
            // 如果可以进行深度比较,则把更新的工作交给上次渲染出来的那个Element元素对应的unit来处理
            preRenderedUnitInstance.update(nextRenderElement)

        }else{
            this._renderUnit = createUnit(nextRenderElement)
            let nextMarkUp = this._renderUnit.getMarkUp(this._reactid)
            $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp)
        }

    }
    getMarkUp(reactid){
     
    }
}

16. 实现componentDidUpdate生命周期函数

so Easy。

只要在更新后触发这个事件就好了

class CompositeUnit extends Unit{
    update(nextElement,partialState){
        
        if(this._componentInstance.shouldComponentUpdate && !this._componentInstance.shouldComponentUpdate(nextProps,nextState)){
            return;
        }
   
        if(shouldDeepCompare(preRenderElement,nextRenderElement)){
            // 如果可以进行深度比较,则把更新的工作交给上次渲染出来的那个Element元素对应的unit来处理
            preRenderedUnitInstance.update(nextRenderElement)
            this._componentInstance.componentDidUpdate && this._componentInstance.componentDidUpdate()
        }else{
            this._renderUnit = createUnit(nextRenderElement)
            let nextMarkUp = this._renderUnit.getMarkUp(this._reactid)
            $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp)
        }

    }
    getMarkUp(reactid){
     
    }
}

17. 实现shouDeepCompare

判断是否需要深比较极其简单,只需要判断 oldElement 和newElement 是否 都是字符串或者数字,这种类型的就走深比较

接着判断 oldElement 和newElement 是否 都是 Element类型,不是的话就return false,是的 再判断 type是否相同(即判断是否是同个组件,是的话 return true)

其他情况都return false

function shouldDeepCompare(oldElement,newElement){
    if(oldElement != null && newElement != null){
        let oldType = typeof oldElement
        let newType = typeof newElement
        if((oldType === 'string' || oldType === "number")&&(newType === "string" || newType === "number")){
            return true
        }
        if(oldElement instanceof Element && newElement instanceof Element){
            return oldElement.type === newElement.type
        }
    }
    return false
}

文章首发于公众号《前端阳光》,项目已经放到github:https://github.com/Sunny-lucking/howToBuildMyReact
觉得可以的话,给个star鼓励下哈啊哈

UI小姐姐说我用CSS实现毛玻璃效果的样子很帅


theme: fancy

前言

UI小姐姐问我,能不能做出透明加模糊的背景,我当然是二话不说就说可以。

因为我觉得没有什么是css实现不了的。

更何况我要在她面前展现得我很厉害的样子。

开发起来

果不其然,在我打开蓝湖后,发现属性都给我提供好了

于是我立即将这份代码ctr c,然后ctr v,一番丰功伟绩立马就完成了,效果也是杠杠滴。

然后兴高采烈地交付给UI小姐姐查看了。小姐姐也说可以。

出于职业素养,我马上拿起我在pdd上9.9买的iphone13手机(当时也就邀请了我老家整个镇子的人来帮我砍一刀吧)查看效果,哇塞真机效果不错啊

但是等到验收,UI小姐姐就告诉我安卓显示有问题。

然后我马上查看了一下backdrop-filter的兼容性。

果然,又是一个兼容性问题的属性。

只能放弃了,好可惜,本来用一个属性就能完成需求了。

好的,问题不大,印象中还有一个属性也可以实现模糊效果,叫filter来着,试试看怎么用它来实现吧。

建立一个html文件来模拟一下吧

<head>
	<style>
		html,
		body {
			margin: 0;
			width: 100%;
			height: 100%;
			background: url(https://puui.qpic.cn/qqvideo_ori/0/x3311cyuqaf_496_280/0);
			background-size: 100% 100%;
			overflow: hidden;
		}

		.card {
			margin: 100px auto;
			width: 300px;
			height: 300px;
			position: relative;
			border: 1px solid #000;
			color: white;
		}

	</style>

</head>

<body>
	<div class="card">
		123123123123123123123123123123123123123123123123123123123123123123123123123123123123
	</div>
</body>

</html>

如图所示,现在是只有一个框框。

然后我,稍加修饰,用下fitler。

<html lang="en">

	<style>
		html,
		body {
			margin: 0;
			width: 100%;
			height: 100%;
			background: url(https://puui.qpic.cn/qqvideo_ori/0/x3311cyuqaf_496_280/0);
			background-size: 100% 100%;
			overflow: hidden;
		}

		.card {
			margin: 100px auto;
			width: 300px;
			height: 300px;
			position: relative;
			border: 1px solid #000;
			color: white;
			filter: blur(10px);
			background-color: rgba(0,0,0,.3);
		}

	</style>


<body>
	<div class="card">
		123123123123123123123123123123123123123123123123123123123123123123123123123123123123
	</div>
</body>

</html>

模糊了吗?确实模糊了。但是有毛玻璃效果吗?没有,毛都没有。我们看下 用backdrop-filter是什么效果的。

.card {
  margin: 100px auto;
  width: 300px;
  height: 300px;
  position: relative;
  border: 1px solid #000;
  color: white;
  backdrop-filter: blur(10px);
  background-color: rgba(0,0,0,.3);
}

看,这才是毛玻璃的效果好吧?为什么filter就不行了? google一下下。

果然,和我猜测的一样(马后炮)

那该怎么样才能模拟呢?

因为是对图片效果才能模糊,想到一个好方法,就是我们要是能取父盒子的背景图片的一块 做为背景就好了。

例如,在这里我们取索隆的半张脸作为card盒子的背景。

该怎么取?

这就有一个属性要登场啦。

background-attachment:fixed

背景图片相对于视口固定,就算元素有了滚动条,背景图也不随内容移动。

fixed用法如下:

<style>
body{
    background-image: url(img/cartooncat.png);
    background-position: bottom left;
    background-attachment: fixed;
    background-repeat: no-repeat;
    height: 1000px;
}
</style>
</head>
<body>
    <h1>下拉看效果:</h1>
</body>

另一个作用是,它可以近似于取父元素背景图的某一块区域。

也就是我们上面所做的假设 就是我们要是能取父盒子的背景图片的一块 做为背景就好了

<html lang="en">

	<style>
		html,
		body {
			margin: 0;
			width: 100%;
			height: 100%;
			background: url(https://puui.qpic.cn/qqvideo_ori/0/x3311cyuqaf_496_280/0);
			background-size: 100% 100%;
			overflow: hidden;
		}

		.card {
			margin: 200px auto;
			width: 300px;
			height: 300px;
			position: relative;
			border: 1px solid #000;
			color: white;
			backdrop-filter: blur(10px);
			background-color: rgba(0,0,0,.3);
		}

		.card::before {
			content: ' ';
			position: absolute;
			top: 0;
			right: 0;
			bottom: 0;
			left: 0;
			z-index: 0;
			/* filter: blur(10px); */
			background: url(https://puui.qpic.cn/qqvideo_ori/0/x3311cyuqaf_496_280/0) no-repeat center;
			/* background-attachment: fixed; */
			background-size: cover; 
			margin: -20px;
		}

	</style>


<body>
	<div class="card">
		123123123123123123123123123123123123123123123123123123123123123123123123123123123123
	</div>
</body>

</html>

看看没有使用background-attachment: fixed的情况

再看看使用的情况

.card::before {
			content: ' ';
			position: absolute;
			top: 0;
			right: 0;
			bottom: 0;
			left: 0;
			z-index: 0;
			/* filter: blur(10px); */
			background: url(https://puui.qpic.cn/qqvideo_ori/0/x3311cyuqaf_496_280/0) no-repeat center;
			background-attachment: fixed;
			background-size: cover; 
			margin: -20px;
		}

可以看取到的差不多就是那块区域。

再看看模糊效果啦。

可以看到 毛玻璃还原度 已经很明显了,但是呢因为设计稿的背景原本是颜色的,现在换成了图片,我们需要换成颜色的话,就需要再添加一个伪元素来模拟颜色。

.card::after {
			content: ' ';
			position: absolute;
			top: 0;
			right: 0;
			bottom: 0;
			left: 0;
			z-index: 0;
			background-color: rgba(0, 0, 0, 0.2);
		}

看,很完美了。

但是文字被覆盖了,所以我们需要提升文字的层级

<html lang="en">

	<style>
		html,
		body {
			margin: 0;
			width: 100%;
			height: 100%;
			background: url(https://puui.qpic.cn/qqvideo_ori/0/x3311cyuqaf_496_280/0);
			background-size: 100% 100%;
			overflow: hidden;
		}

		.card {
			margin: 100px auto;
			width: 300px;
			height: 300px;
			position: relative;
			border: 1px solid #000;
			color: white;
			backdrop-filter: blur(10px);
			background-color: rgba(0,0,0,.3);
		}

		.text {
			position: relative;
			z-index: 1;
		}

		.card::before {
			content: ' ';
			position: absolute;
			top: 0;
			right: 0;
			bottom: 0;
			left: 0;
			z-index: 0;
			filter: blur(10px);
			background: url(https://puui.qpic.cn/qqvideo_ori/0/x3311cyuqaf_496_280/0) no-repeat center;
			background-attachment: fixed;
			background-size: cover; 
			margin: -20px;
		}

		.card::after {
			content: ' ';
			position: absolute;
			top: 0;
			right: 0;
			bottom: 0;
			left: 0;
			z-index: 0;
			background-color: rgba(0, 0, 0, 0.2);
		}
	</style>


<body>
	<div class="card">
		<div class="text">123123123123123123123123123123123123123123123123123123123123123123123123123123123123</div>
	</div>
</body>

</html>

完美。

然后,我马上跟UI小姐姐说我做好了。验收通过,不知道UI小姐姐爱上我了没有。

结尾

已上大多数内容是来自一个渴望爱情又得不到爱情,敲代码还总报错的小傻瓜的 yy,真实故事与UI小姐姐无关

关于【事件】十问 (读《红宝书》)

第一问:请介绍下事件模型

目前共有三种事件模型,它们分别是:

DOM0 级事件模型、IE 事件模型、DOM2 级事件模型

DOM0 级事件模型 又称原始事件模型,有两种方式,最直观的提下如下代码:

// 方式一
// 将事件直接通过属性绑定在元素上
<button οnclick="clickBtn()"></button>
// 方式二
// 获取到页面元素后,通过 onclick 等事件,将触发的方法指定为元素的事件
// 取消该事件可直接设置为 null
var btn = document.getElementById(‘btn’)
btn.onclick = function () {…}
btn.onclick = null

DOM0 级的事件模型,方法较为简单,但是将逻辑和界面耦合在了一起,对之后的维护不是很友好

但也不是没有优点,这种方式兼容所有浏览器

IE 事件模型 IE 事件模型一共有两个阶段:

事件处理阶段:事件在达到目标元素时,触发监听事件

事件冒泡阶段:事件从目标元素冒泡到 document,并且一次检查各个节点是否绑定了监听函数,如果有则执行 绑定和移除事件的 api 分别如下:

// 绑定事件
el.attachEvent(eventType, handler)
// 移除事件
el.detachEvent(eventType, handler)

参数说明:

eventType 是如onclick一样的带有”on“的事件,绑定事件时,handler可以是具名函数,也可以是匿名函数,但是匿名函数无法移除

我们会发现,IE 事件模型与我们平时用的事件绑定方法addEventListener,也就是下面要说的 DOM2 级事件模型有点相似,但是 IE 事件模型仅在 IE 浏览器中有效,不兼容其他浏览器

DOM2 级事件模型 W3C标准模型,也是我们频繁使用的事件模型,除 IE6-8 以外的所有现代浏览器都支持该事件模型

DOM2 级事件模型共有三个阶段:

事件捕获阶段:事件从 document 向下传播到目标元素,依次检查所有节点是否绑定了监听事件,如果有则执行

事件处理阶段:事件在达到目标元素时,触发监听事件

事件冒泡阶段:事件从目标元素冒泡到 document,并且一次检查各个节点是否绑定了监听函数,如果有则执行

function a() {   ...   }

function b() {      }

input.addEventListener( “click” ,a)

input.removeEventListener( “click” ,a)

第二问:介绍下这三种事件模型的区别

Dom0 模型

  • this指向: 指向函数中的this指向的是 被绑定的元素
  • 绑定多个同事件类型的事件时,如对同个元素绑定多个 click, 则后面的会覆盖前面的,最后只有一个会执行

IE 模型:

  • this指向: 指向函数中的this指向的是** window**
  • 绑定多个同事件类型的事件时,如对同个元素绑定多个 click,则后面的不会覆盖前面的,执行顺序是 先执行下面的,从下往上执行
  • 只有两个参数:第一个参数为 事件类型,第二个为事件执行函数
  • 目标阶段,事件冒泡阶段
  • 获取目标元素: window.event.srcElement
    Dom2 模型
  • this指向: 指向函数中的this指向的是 被绑定的元素
  • 绑定多个同事件类型的事件时,如对同个元素绑定多个 click,则后面的不会覆盖前面的,执行顺序是 先执行上面的,从上往下执行
  • 有三个参数:第一个参数为 事件类型,第二个为事件执行函数,第三个为布尔值,表示是否用事件捕获
  • 有事件捕获阶段,处于目标阶段,事件冒泡阶段
  • 获取目标元素: event.target 欢迎补充。。。

第三问:请介绍下事件流?

事件流所描述的就是从页面中接受事件的顺序,事件流分为两种:事件冒泡(主流)和事件捕获. 版本IE(IE8及以下版本)不支持捕获阶段

1、事件冒泡

事件开始时由具体元素接收,然后逐级向上传播到父元素

2、事件捕获

父元素的节点更早接收事件,而具体元素最后接收事件,与事件冒泡相反

第四问 怎么阻止事件冒泡和事件捕获以及默认事件?

阻止事件冒泡 :使用 e.stopPropagation(); IE使用 window.event.cancelBubble = true;

function stopBubble(e){
     <!--如果提供了事件对象,则这是个非IE浏览器-->
    if(e&&e.stopPropagation){
        e.stopPropagation();
    }else{
        <!--我们需要使用IE的方式来取消事件冒泡-->
        window.event.cancelBubble = true;
    }
}  

阻止事件捕获:与冒泡一样 使用e.stopPropagation(),IE没有捕获阶段,所以不用阻止默认事件:

function stopDefault(e){
    <!--阻止默认行为W3C-->
    if(e&&e.preventDefault()){
        e.preventDefault();
    }else{
        <!--IE中阻止默认行为-->
        windown.event.returnValue = false
    }
}

第六问:事件的委托(代理 Delegated Events)的原理以及优缺点

委托(代理)事件是那些被绑定到父级元素的事件,但是只有当满足一定匹配条件时才会被挪。这是靠事件的冒泡机制来实现的,

优点是:

(1)可以大量节省内存占用,减少事件注册,比如在table上代理所有td的click事件就非常棒

(2)可以实现当新增子对象时无需再次对其绑定事件,对于动态内容部分尤为合适

缺点是:

事件代理的应用常用应该仅限于上述需求下,如果把所有事件都用代理就可能会出现事件误判,即本不应用触发事件的被绑上了事件。

例子:

<ul id="parent">
  <li class="child">one</li>
  <li class="child">two</li>
  <li class="child">three</li>
</ul>
 
<script type="text/javascript">
  //父元素
  var dom= document.getElementById('parent');
 
  //父元素绑定事件,代理子元素的点击事件
  dom.οnclick= function(event) {
    var event= event || window.event;
    var curTarget= event.target || event.srcElement;
 
    if (curTarget.tagName.toLowerCase() == 'li') {
      //事件处理
    }
  }
</script>

要求:兼容浏览器 考点:事件对象e,IE下事件对应的属性名。 重点是要说到target,currentTarget以及IE下的srcElement和this。

第七问:编写一个自定义事件类,包含on/off/emit/once方法

可能谈到Evnet,customerEvent,document.createEvent

第八问:怎样判断js脚本是否加载完,并在加载完后进行操作

在工作过程中,经常会遇到按需加载的需求,即在脚本加载完成后,返回一个回调函数,在回调函数中进行相关操作,那如何去判断脚本是否加载完成了呢?

可以对加载的js对象使用onload来判断,jsDom.onload

ie6、7不支持js.onload方法,使用js.onreadystatechange来解决 js.onreadystatechange来跟踪每个状态的变化(loading、loaded、interactive、complete),当返回状态为loaded或者complete时,表示加载完成,返回回调函数.

function loadJsAsync(url){
    var body = document.getElementsByTagName('body')[0];
    var jsNode = document.createElement('script');

    jsNode.setAttribute('type', 'text/javascript');
    jsNode.setAttribute('src', url);
    body.appendChild(jsNode);

    if (jsNode.onload) {
        jsNode.onload = function() {
            // do something
        }
    } else {
        // ie6, ie7不支持onload的情况
        jsNode.onreadystatechange = function() {
            if(jsNode.readyState == 'loaded' || jsNode.readyState == 'complete') {
                // 异步js加载完毕
                // do something执行操作
            }
        }
    }
}

第九问:上面的代码中,script脚本是什么时候开始加载的?

在 body.appendChild(jsNode);这一步,即添加到文档上后开始加载,这跟image不同,image是 image.src = url后开始加载

第十问 如何判断页面是否加载完成?

方式一:window.onload:

当一个文档完全下载到浏览器中时,才会触发window.onload事件。这意味着页面上的全部元素对js而言都是可以操作的,也就是说页面上的所有元素加载完毕才会执行。这种情况对编写功能性代码非常有利,因为无需考虑加载的次序。

  window.οnlοad=function(){

dosth//你要做的事情

}

方式二$(document).ready()

会在DOM完全就绪并可以使用时调用。虽然这也意味着所有元素对脚本而言都是可以访问的,但是,并不意味着所有关联的文件都已经下载完毕。换句话说,当HMTL下载完成并解析为DOM树之后,代码就会执行。

$(document).ready(function(){

dosth//你要做的事情

})

注意:页面加载完成有两种事件,一是ready,表示文档结构已经加载完成(不包含图片等非文字媒体文件),二是onload,指示页 面包含图片等文件在内的所有元素都加载完成。(可以说:ready 在onload 前加载!!!)

方式三:用document.onreadystatechange的方法来监听状态改变, 然后用document.readyState == “complete”判断是否加载完成,需要的朋友可以参考下,用document.onreadystatechange的方法来监听状态改变, 然后用document.readyState == “complete”判断是否加  载完成 document.onreadystatechange = function()  //当页面加载状态改变的时候执行function

{ 

if(document.readyState == “complete”)

{   //当页面加载状态为完全结束时进入
     init();   //你要做的操作。
  }
}

手写generator核心原理,再也不怕面试官问我generator原理

手写核心generator原理

[toc]

1.generator的使用

generator感觉大多数人不太熟悉,所以有必要科普下使用方法。熟悉使用方法 的伙伴可以直接到第二节。

Generator函数跟普通函数的写法有非常大的区别:

  • 一是,function关键字与函数名之间有一个星号;
  • 二是,函数体内部使用yield语句,定义不同的内部状态(yield在英语里的意思就是“产出”)。

最简单的Generator函数如下:

function* g() {
    yield 'a';
    yield 'b';
    yield 'c';
    return 'ending';
}
g(); // 返回一个对象

g函数呢,有四个阶段,分别是'a','b','c','ending'。

Generator 函数神奇之一:g()并不执行g函数

g()并不会执行g函数,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是迭代器对象(Iterator Object)。

Generator 函数神奇之二:分段执行

先看如下代码:

function* g() {
    yield 'a';
    yield 'b';
    yield 'c';
    return 'ending';
}

var gen = g();
gen.next(); // 返回Object {value: "a", done: false}

gen.next()返回一个非常非常简单的对象{value: "a", done: false},'a'就是g函数执行到第一个yield语句之后得到的值,false表示g函数还没有执行完,只是在这暂停。

如果再写一行代码,还是gen.next();,这时候返回的就是{value: "b", done: false},说明g函数运行到了第二个yield语句,返回的是该yield语句的返回值'b'。返回之后依然是暂停。

再写一行gen.next();返回{value: "c", done: false},再写一行gen.next();,返回{value: "ending", done: true},这样,整个g函数就运行完毕了。

提问:如果再写一行gen.next();呢?

答:返回{value: undefined, done: true},这样没意义。

提问:如果g函数没有return语句呢?

答:那么第三次.next()之后就返回{value: undefined, done: true},这个第三次的next()唯一意义就是证明g函数全部执行完了。

提问:如果g函数的return语句后面依然有yield呢?

答:js的老规定:return语句标志着该函数所有有效语句结束,return下方还有多少语句都是无效,白写。

提问:如果g函数没有yield和return语句呢?

答:第一次调用next就返回{value: undefined, done: true},之后也是{value: undefined, done: true}。

提问:如果只有return语句呢?

答:第一次调用就返回{value: xxx, done: true},其中xxx是return语句的返回值。之后永远是{value: undefined, done: true}。

提问:下面代码会有什么结果?

function* g() {
    var o = 1;
    yield o++;
    yield o++;
    yield o++;

}
var gen = g();

console.log(gen.next()); // 1

var xxx = g();

console.log(gen.next()); // 2
console.log(xxx.next()); // 1
console.log(gen.next()); // 3

答:见上面注释。每个迭代器之间互不干扰,作用域独立。

继续提问:如果第二个yield o++;改成yield;会怎样?

答:那么指针指向这个yield的时候,返回{value: undefined, done: false}。

继续提问:如果第二个yield o++;改成o++;yield;会怎样?

答:那么指针指向这个yield的时候,返回{value: undefined, done: false},因为返回的永远是yield后面的那个表达式的值。

所以现在可以看出,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止。换言之,Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。

总之,每调用一次Generator函数,就返回一个迭代器对象,代表Generator函数的内部指针。以后,每次调用迭代器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield语句后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

所以可以看出,Generator 函数的特点就是:

  • 1、分段执行,可以暂停
  • 2、可以控制阶段和每个阶段的返回值
  • 3、可以知道是否执行到结尾

yield语句

迭代器对象的next方法的运行逻辑如下。

(1)遇到yield语句,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句。

(3)如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。

yield语句与return语句既有相似之处,也有区别。

相似之处在于,都能返回紧跟在语句后面的那个表达式的值。

区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield语句。正常函数只能返回一个值,因为只能执行一次return;Generator函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说Generator生成了一系列的值,这也就是它的名称的来历(在英语中,generator这个词是“生成器”的意思)。

注意:yield语句只能用于function的作用域,如果function的内部还定义了其他的普通函数,则函数内部不允许使用yield语句。

注意:yield语句如果参与运算,必须用括号括起来。

console.log(3 + yield 4); // 语法错误
console.log(3 + (yield 4)); // 打印7

next方法可以有参数

一句话说,next方法参数的作用,是为上一个yield语句赋值。由于yield永远返回undefined,这时候,如果有了next方法的参数,yield就被赋了值,比如下例,原本a变量的值是0,但是有了next的参数,a变量现在等于next的参数,也就是11。

next方法的参数每次覆盖的一定是undefined。next在没有参数的时候,函数体里面写let xx = yield oo;是没意义的,因为xx一定是undefined。

function* g() {
    var o = 1;
    var a = yield o++;
    console.log('a = ' + a);
    var b = yield o++;
}
var gen = g();

console.log(gen.next());
console.log('------');
console.log(gen.next(11));

得到:

首先说,console.log(gen.next());的作用就是输出了{value: 1, done: false},注意var a = yield o++;,由于赋值运算是先计算等号右边,然后赋值给左边,所以目前阶段,只运算了yield o++,并没有赋值。

然后说,console.log(gen.next(11));的作用,首先是执行gen.next(11),得到什么?首先:把第一个yield o++重置为11,然后,赋值给a,再然后,console.log('a = ' + a);,打印a = 11,继续然后,yield o++,得到2,最后打印出来。

从这我们看出了端倪:带参数跟不带参数的区别是,带参数的情况,首先第一步就是将上一个yield语句重置为参数值,然后再照常执行剩下的语句。总之,区别就是先有一步先重置值,接下来其他全都一样。

这个功能有很重要的语法意义,通过next方法的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值。也就是说,可以在Generator函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。

提问:第一个.next()可以有参数么?
答:设这样的参数没任何意义,因为第一个.next()的前面没有yield语句。

for...of循环

for...of循环可以自动遍历Generator函数时生成的Iterator对象,且此时不再需要调用next方法。for...of循环的基本语法是:

for (let v of foo()) {
  console.log(v);
}

其中foo()是迭代器对象,可以把它赋值给变量,然后遍历这个变量。

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

let a = foo();

for (let v of a) {
  console.log(v);
}
// 1 2 3 4 5

上面代码使用for...of循环,依次显示5个yield语句的值。这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

下面是一个利用Generator函数和for...of循环,实现斐波那契数列的例子。

斐波那契数列是什么?它指的是这样一个数列 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144........
这个数列前两项是0和1,从第3项开始,每一项都等于前两项之和。

function* fibonacci() {
  let [prev, curr] = [0, 1];
  for (;;) { // 这里请思考:为什么这个循环不设定结束条件?
    [prev, curr] = [curr, prev + curr];
    yield curr;
  }
}

for (let n of fibonacci()) {
  if (n > 1000) {
    break;
  }
  console.log(n);
}

2.手写generator核心原理

我们从一个简单的例子开始,一步步探究Generator的实现原理:

function* foo() {
  yield 'result1'
  yield 'result2'
  yield 'result3'
}
  
const gen = foo()
console.log(gen.next()) //{value: "result1", done: false}
console.log(gen.next()) //{value: "result2", done: false}
console.log(gen.next()) //{value: "result3", done: false}
console.log(gen.next()) //{value: undefined, done: true}

看到这种整齐的结构,我想起了switch case,也是这么地整齐,所以这两种之间应该存在一种关系。

我们尝试写一个用switch/case来实现下:

function gen$(nextStep) {
    while (1) {
        switch (nextStep) {
            case 0:
                return 'result1';
            case 2:
                return 'result2';
            case 4:
                return 'result3';
            case 6:
                return undefined;
        }
    }
}

如代码所示,我们每次调用gen$然后传对应的参数,就能返回对应的值(也就是原本函数yield后面的值)

但是nextStep应该是一个自动增加的函数,应该不是我们传进去的。所以这里应该用一个闭包来实现

function gen$() {
    var nextStep = 0
    return function () {
        while (1) {
            switch (nextStep) {
                case 0:
                    nextStep = 2;
                    return 'result1';

                case 2:
                    nextStep = 4;
                    return 'result2';

                case 4:
                    nextStep = 6;
                    return 'result3';

                case 6:
                    return undefined
            }
        }
    }
}

现在我们可以通过

var a = gen$()

获得内函数。
这样每次执行

a()

nextStep就会改成下一次执行a()应该对应的值,并且返回相应的result了。

但是generator的底层原理不是用闭包的。而是用一个全局变量,因为这样为了后面的实现方便很多,为了遵循原理,我们改成用全局变量来实现。

先定义一个全局变量

context = {
  prev:0,
  next:0
}
function gen$(context) {
    while (1) {
        switch (context.prev = context.next) {
            case 0:
                context.next = 2;
                return 'result1';

            case 2:
                context.next = 4;
                return 'result2';

            case 4:
                context.next = 6;
                return 'result3';

           case 6:
                    return undefined
        }
    }
}

第一次执行gen$(context),swtich判断的时候,是用prev来判断这一次应该执行那个case,执行case时再改变next的值,next表示下次应该执行哪个case。第二次执行gen$(context)的时候,将next的值赋给prev。

但是直接返回这么一个值是不对的。我们看前面的例子是返回一个对象。那该怎么实现呢?

再把例子搬下来:

function* foo() {
  yield 'result1'
  yield 'result2'
  yield 'result3'
}
  
const gen = foo()
console.log(gen.next()) //{value: "result1", done: false}
console.log(gen.next()) //{value: "result2", done: false}
console.log(gen.next()) //{value: "result3", done: false}
console.log(gen.next()) //{value: undefined, done: true}

我们发现 gen 有next这个方法。所以可以判断出 执行foo返回的应该是一个对象,这个对象有next这个方法。所以我们初步实现foo的转化后的函数。

let foo = function () {
    return {
        next: function () {

        }
    }
}

而每次执行next,就会返回拥有value和done的对象,

所以,可以完善返回值

let foo = function () {
  return {
      next: function () {
          return {
              value,
              done
          }
      }
  }
}

但是我们这里还没定义这value和done啊,该怎么定义呢?

我们先看value的实现。我们在上面实现gen$的时候,就发现它返回的是value了。所以可以在这里获取$gen的返回值作为value。

 let foo = function () {
            return {
                next: function () {
                    value = gen$(context)
                    return {
                        value,
                        done
                    }
                }
            }
        }

那done怎么定义呢?

其实done作为一个全局状态表示generator是否执行结束,因此,我们可以在

context里定义,默认值为false。

var context = {
  next:0,
  prev: 0,
  done: false,

}

所以,每次返回,直接返回context.done就可以了

let foo = function () {
    return {
        next: function () {
            value = gen$(context);
            done = context.done
            return {
                value,
                done
            }
        }
    }
}

那done是怎么改变为true的。我们知道,generator执行到后面,就会返回done:true。我们可以看例子的第四个执行结果

function* foo() {
  yield 'result1'
  yield 'result2'
  yield 'result3'
}
  
const gen = foo()
console.log(gen.next()) //{value: "result1", done: false}
console.log(gen.next()) //{value: "result2", done: false}
console.log(gen.next()) //{value: "result3", done: false}
console.log(gen.next()) //{value: undefined, done: true}

因此,我们需要在最后一次执行gen$的时候改变context.done的值。

思路,给context添加一个stop方法。用来改变自身的done为true。在执行$gen的时时候让context执行stop就好

var context = {
  next:0,
  prev: 0,
  done: false,
  新增代码
  stop: function stop () {
    this.done = true
  }
}
function gen$(context) {
    while (1) {
        switch (context.prev = context.next) {
            case 0:
                context.next = 2;
                return 'result1';

            case 2:
                context.next = 4;
                return 'result2';

            case 4:
                context.next = 6;
                return 'result3';

            case 6:
                新增代码
                context.stop();
                return undefined
        }
    }
}
let foo = function () {
    return {
        next: function () {
            value = gen$(context);
            done = context.done
            return {
                value,
                done
            }
        }
    }
}

这样执行到case为6的时候就会改变done的值了。

实际上这就是generator的大致原理

并不难理解,我们分析一下流程:

我们定义的function*生成器函数被转化为以上代码

转化后的代码分为三大块:

gen$(_context)由yield分割生成器函数代码而来

context对象用于储存函数执行上下文

迭代器法定义next(),用于执行gen$(_context)来跳到下一步

从中我们可以看出,「Generator实现的核心在于上下文的保存,函数并没有真的被挂起,每一次yield,其实都执行了一遍传入的生成器函数,只是在这个过程中间用了一个context对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行,看起来就像函数被挂起了一样」

3.参照源码实现Context类

不过,我们这里的context是个全局对象啊?我们都知道如果是下面这种情况:

function* g() {
    var o = 1;
    yield o++;
    yield o++;
    yield o++;

}
var gen = g();

console.log(gen.next()); // 1

var xxx = g();

console.log(gen.next()); // 2
console.log(xxx.next()); // 1
console.log(gen.next()); // 3

我们发现 每个迭代器之间互不干扰,作用域独立。

也就是说每个迭代器的context是独立的。但是与我们目前实现的一个全局context不一致,这个我是百思不得其解,所以看下源码。

利用babel将下面代码转化一下

function* foo() {
  yield 'result1'
  yield 'result2'
  yield 'result3'
}

我们可以在babel官网上在线转化这段代码,看看ES5环境下是如何实现Generator的:

"use strict";

var _marked =
/*#__PURE__*/
regeneratorRuntime.mark(foo);

function foo() {
  return regeneratorRuntime.wrap(function foo$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 'result1';

        case 2:
          _context.next = 4;
          return 'result2';

        case 4:
          _context.next = 6;
          return 'result3';

        case 6:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

看源码,你可能觉得跟我们实现的有点不一样,实际上结构是基本一样的,基本都是分成那三部分

发现源码是将我们的gen$(context)方法传入了wrap中。

我们看下wrap方法

function wrap(innerFn, outerFn, self) {
  var generator = Object.create(outerFn.prototype);
  var context = new Context([]);
  generator._invoke = makeInvokeMethod(innerFn, self, context);

  return generator;
}

发现它是每生foo()执行一次 ,就会执行一次wrap方法,而在wrap方法里就会new 一个Context对象。这就说明了每个迭代器的context是独立的。

Soga~原来如此~~~~

也就是说如果我们要实现独立context还是 把context改成一个类。

在执行var gen = g();的时候再生成context实例即可:

class Context {
    constructor() {
        this.next = 0
        this.prev = 0
        this.done = false
    }
    top() {
        this.done = true
    }
}
let foo = function () {
    var context = new Context() 新增代码
    return {
        next: function () {
            value = gen$(context);
            done = context.done
            return {
                value,
                done
            }
        }
    }
}

4.参照源码实现参数值的保存

好了,这个独立context问题解决。但是发现哈有一个问题:

function* foo() {
    var a = yield 'result1'
    console.log(a);
    yield 'result2'
    yield 'result3'
}

const gen = foo()
console.log(gen.next().value)
console.log(gen.next(222).value)
console.log(gen.next().value)

我们发现这里用var a 来接收传入的参数。

当我们第一次执行gen.next(),foo内部会执行到yield这里。还没给a赋值

当我们第二次执行gen.next(),foo内部会再第一个yield这里执行。把传入的参数222赋值给a


那原理是怎么实现的呢?我依旧百思不得其解,不得不再看下源码。

将下面代码babel一下

function* foo() {
            var a = yield 'result1'
            console.log(a);
            yield 'result2'
            yield 'result3'
        }
"use strict";

var _marked = /*#__PURE__*/regeneratorRuntime.mark(foo);

function foo() {
  var a; 在这里定义
  return regeneratorRuntime.wrap(function foo$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 'result1';

        case 2:
          a = _context.sent; 在这里赋值
          console.log(a);
          _context.next = 6;
          return 'result2';

        case 6:
          _context.next = 8;
          return 'result3';

        case 8:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

可见。是将我们在generator定义的变量提到foo函数顶部了。作为一个闭包的变量。

因此,居于这个思路,我们可以完善一下我们的代码。

如果我们在nenerator定义了xxx这个变量,那么就会被提升到函数顶部

function gen$(context) {
    var xxx;新增代码
    while (1) {
        switch (context.prev = context.next) {
            case 0:
                context.next = 2;
                return 'result1';

            case 2:
            
                context.next = 4;
                return 'result2';

            case 4:
                context.next = 6;
                return 'result3';

            case 6:
              
                context.stop();
                return undefined
        }
    }
}

如果我们将出传入的参数赋值给这个变量

那么

参数就会作为Context的参数。将传入的参数保存到context中。

let foo = function () {
    var context = new Context(222) //修改代码
    return {
        next: function () {
            value = gen$(context);
            done = context.done
            return {
                value,
                done
            }
        }
    }
}

然后在gen$()执行的时候再赋值给变量

function gen$(context) {
    var xxx;
    while (1) {
        switch (context.prev = context.next) {
            case 0:
                context.next = 2;
                return 'result1';

            case 2:
                xxx = context._send 新增代码
                context.next = 4;
                return 'result2';

            case 4:
                context.next = 6;
                return 'result3';

            case 6:
                
                context.stop();
                return undefined
        }
    }
}

5.完结(撒花环节)

到这里 就 大概完成了,撒花撒花~~~~~~~~~~~~~~

有什么建议或者疑问欢迎下边评论,觉得可以点个赞支持一下,谢谢!

参考文献

理解 ES6 Generator 函数:https://www.jianshu.com/p/e0778b004596

Promise/async/Generator实现原理解析:https://www.cnblogs.com/pingan8787/p/13069433.html

手写axios核心原理,再也不怕面试官问我axios原理

一、axios简介

axios是什么?

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

axios有什么特性?(不得不说面试被问到几次)

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

实际上,axios可以用在浏览器和 node.js 中是因为,它会自动判断当前环境是什么,如果是浏览器,就会基于XMLHttpRequests实现axios。如果是node.js环境,就会基于node内置核心模块http实现axios

简单来说,axios的基本原理就是

  1. axios还是属于 XMLHttpRequest, 因此需要实现一个ajax。或者基于http
  2. 还需要一个promise对象来对结果进行处理

有什么不理解的或者是建议欢迎评论提出.项目已经放到
github:https://github.com/Sunny-lucking/howToBuildMyAxios

二、基本使用方式

axios基本使用方式主要有

  1. axios(config)
  2. axios.method(url, data , config)
// index.html文件
<html>
<script type="text/javascript" src="axios"></script>
<body>
<button class="btn">点我发送请求</button>
<script>
    document.querySelector('.btn').onclick = function() {
        // 分别使用以下方法调用,查看myaxios的效果
        axios.post('/postAxios', {
          name: '小美post'
        }).then(res => {
          console.log('postAxios 成功响应', res);
        })

        axios({
          method: 'post',
          url: '/getAxios'
        }).then(res => {
          console.log('getAxios 成功响应', res);
        })
    }
</script>
</body>
</html>
</html>

三、实现axios和axios.method

从axios(config)的使用上可以看出导出的axios是一个方法。从axios.method(url, data , config)的使用可以看出导出的axios上或者原型上挂有get,post等方法

实际上导出的axios就是一个Axios类中的一个方法。

如代码所以,核心代码是request。我们把request导出,就可以使用axios(config)这种形式来调用axios了。

class Axios {
    constructor() {

    }

    request(config) {
        return new Promise(resolve => {
            const {url = '', method = 'get', data = {}} = config;
            // 发送ajax请求
            const xhr = new XMLHttpRequest();
            xhr.open(method, url, true);
            xhr.onload = function() {
                console.log(xhr.responseText)
                resolve(xhr.responseText);
            }
            xhr.send(data);
        })
    }
}

怎么导出呢?十分简单,new Axios,获得axios实例,再获得实例上的request方法就好了。

// 最终导出axios的方法,即实例的request方法
function CreateAxiosFn() {
    let axios = new Axios();
    let req = axios.request.bind(axios);
    return req;
}

// 得到最后的全局变量axios
let axios = CreateAxiosFn();

点击查看此时的myAxios.js

现在axios实际上就是request方法。

你可能会很疑惑,因为我当初看源码的时候也很疑惑:干嘛不直接写个request方法,然后导出呢?非得这样绕这么大的弯子。别急。后面慢慢就会讲到。

现在一个简单的axios就完成了,我们来引入myAxios.js文件并测试一下可以使用不?

简单的搭建服务器:

//server.js
var express = require('express');
var app = express();

//设置允许跨域访问该服务.
app.all('*', function (req, res, next) {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Content-Type');
    res.header('Access-Control-Allow-Methods', '*');
    res.header('Content-Type', 'application/json;charset=utf-8');
    next();
});

app.get('/getTest', function(request, response){
    data = {
        'FrontEnd':'前端',
        'Sunny':'阳光'
    };
    response.json(data);
});
var server = app.listen(5000, function(){
    console.log("服务器启动");
});
//index.html
<script type="text/javascript" src="./myAxios.js"></script>

<body>
<button class="btn">点我发送请求</button>
<script>
    document.querySelector('.btn').onclick = function() {
        // 分别使用以下方法调用,查看myaxios的效果
        axios({
          method: 'get',
          url: 'http://localhost:5000/getTest'
        }).then(res => {
          console.log('getAxios 成功响应', res);
        })
    }
</script>
</body>

点击按钮,看看是否能成功获得数据。

可喜可贺,成功。

现在我们来实现下axios.method()的形式。

思路。我们可以再Axios.prototype添加这些方法。而这些方法内部调用request方法即可,如代码所示:

// 定义get,post...方法,挂在到Axios原型上
const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];
methodsArr.forEach(met => {
    Axios.prototype[met] = function() {
        console.log('执行'+met+'方法');
        // 处理单个方法
        if (['get', 'delete', 'head', 'options'].includes(met)) { // 2个参数(url[, config])
            return this.request({
                method: met,
                url: arguments[0],
                ...arguments[1] || {}
            })
        } else { // 3个参数(url[,data[,config]])
            return this.request({
                method: met,
                url: arguments[0],
                data: arguments[1] || {},
                ...arguments[2] || {}
            })
        }

    }
})

我们通过遍历methodsArr数组,依次在Axios.prototype添加对应的方法,注意的是'get', 'delete', 'head', 'options'这些方法只接受两个参数。而其他的可接受三个参数,想一下也知道,get不把参数放body的。

但是,你有没有发现,我们只是在Axios的prototype上添加对应的方法,我们导出去的可是request方法啊,那怎么办? 简单,把Axios.prototype上的方法搬运到request上即可。

我们先来实现一个工具方法,实现将b的方法混入a;

const utils = {
  extend(a,b, context) {
    for(let key in b) {
      if (b.hasOwnProperty(key)) {
        if (typeof b[key] === 'function') {
          a[key] = b[key].bind(context);
        } else {
          a[key] = b[key]
        }
      }
      
    }
  }
}

然后我们就可以利用这个方法将Axios.prototype上的方法搬运到request上啦。

我们修改一下之前的CreateAxiosFn方法即可

function CreateAxiosFn() {
  let axios = new Axios();
  
  let req = axios.request.bind(axios);
  增加代码
  utils.extend(req, Axios.prototype, axios)
  
  return req;
}

点击查看此时的myAxios.js

现在来测试一下能不能使用axios.get()这种形式调用axios。

<body>
<button class="btn">点我发送请求</button>
<script>
    document.querySelector('.btn').onclick = function() {

        axios.get('http://localhost:5000/getTest')
            .then(res => {
                 console.log('getAxios 成功响应', res);
            })

    }
</script>
</body>

害,又是意料之中成功。

再完成下一个功能之前,先给上目前myAxios.js的完整代码

class Axios {
    constructor() {

    }

    request(config) {
        return new Promise(resolve => {
            const {url = '', method = 'get', data = {}} = config;
            // 发送ajax请求
            console.log(config);
            const xhr = new XMLHttpRequest();
            xhr.open(method, url, true);
            xhr.onload = function() {
                console.log(xhr.responseText)
                resolve(xhr.responseText);
            }
            xhr.send(data);
        })
    }
}

// 定义get,post...方法,挂在到Axios原型上
const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];
methodsArr.forEach(met => {
    Axios.prototype[met] = function() {
        console.log('执行'+met+'方法');
        // 处理单个方法
        if (['get', 'delete', 'head', 'options'].includes(met)) { // 2个参数(url[, config])
            return this.request({
                method: met,
                url: arguments[0],
                ...arguments[1] || {}
            })
        } else { // 3个参数(url[,data[,config]])
            return this.request({
                method: met,
                url: arguments[0],
                data: arguments[1] || {},
                ...arguments[2] || {}
            })
        }

    }
})


// 工具方法,实现b的方法或属性混入a;
// 方法也要混入进去
const utils = {
  extend(a,b, context) {
    for(let key in b) {
      if (b.hasOwnProperty(key)) {
        if (typeof b[key] === 'function') {
          a[key] = b[key].bind(context);
        } else {
          a[key] = b[key]
        }
      }
      
    }
  }
}


// 最终导出axios的方法-》即实例的request方法
function CreateAxiosFn() {
    let axios = new Axios();

    let req = axios.request.bind(axios);
    // 混入方法, 处理axios的request方法,使之拥有get,post...方法
    utils.extend(req, Axios.prototype, axios)
    return req;
}

// 得到最后的全局变量axios
let axios = CreateAxiosFn();

四、请求和响应拦截器

我们先看下拦截器的使用

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

拦截器是什么意思呢?其实就是在我们发送一个请求的时候会先执行请求拦截器的代码,然后再真正地执行我们发送的请求,这个过程会对config,也就是我们发送请求时传送的参数进行一些操作。

而当接收响应的时候,会先执行响应拦截器的代码,然后再把响应的数据返回来,这个过程会对response,也就是响应的数据进行一系列操作。

怎么实现呢?需要明确的是拦截器也是一个类,管理响应和请求。因此我们先实现拦截器

class InterceptorsManage {
  constructor() {
    this.handlers = [];
  }

  use(fullfield, rejected) {
    this.handlers.push({
      fullfield,
      rejected
    })
  }
}

我们是用这个语句axios.interceptors.response.useaxios.interceptors.request.use,来触发拦截器执行use方法的。

说明axios上有一个响应拦截器和一个请求拦截器。那怎么实现Axios呢?看代码

class Axios {
    constructor() {
        新增代码
        this.interceptors = {
            request: new InterceptorsManage,
            response: new InterceptorsManage
        }
    }

    request(config) {
        return new Promise(resolve => {
            const {url = '', method = 'get', data = {}} = config;
            // 发送ajax请求
            console.log(config);
            const xhr = new XMLHttpRequest();
            xhr.open(method, url, true);
            xhr.onload = function() {
                console.log(xhr.responseText)
                resolve(xhr.responseText);
            };
            xhr.send(data);
        })
    }
}

可见,axios实例上有一个对象interceptors。这个对象有两个拦截器,一个用来处理请求,一个用来处理响应。

所以,我们执行语句axios.interceptors.response.useaxios.interceptors.request.use的时候,实现获取axios实例上的interceptors对象,然后再获取response或request拦截器,再执行对应的拦截器的use方法。

而执行use方法,会把我们传入的回调函数push到拦截器的handlers数组里。

到这里你有没有发现一个问题。这个interceptors对象是Axios上的啊,我们导出的是request方法啊(欸?好熟悉的问题,上面提到过哈哈哈~~~额)。处理方法跟上面处理的方式一样,都是把Axios上的方法和属性搬到request过去,也就是遍历Axios实例上的方法,得以将interceptors对象挂载到request上。

所以只要更改下CreateAxiosFn方法即可。

function CreateAxiosFn() {
  let axios = new Axios();
  
  let req = axios.request.bind(axios);
  // 混入方法, 处理axios的request方法,使之拥有get,post...方法
  utils.extend(req, Axios.prototype, axios)
  新增代码
  utils.extend(req, axios)
  return req;
}

好了,现在request也有了interceptors对象,那么什么时候拿interceptors对象中的handler之前保存的回调函数出来执行。

没错,就是我们发送请求的时候,会先获取request拦截器的handlers的方法来执行。再执行我们发送的请求,然后获取response拦截器的handlers的方法来执行。

因此,我们要修改之前所写的request方法
之前是这样的。

request(config) {
    return new Promise(resolve => {
        const {url = '', method = 'get', data = {}} = config;
        // 发送ajax请求
        console.log(config);
        const xhr = new XMLHttpRequest();
        xhr.open(method, url, true);
        xhr.onload = function() {
            console.log(xhr.responseText)
            resolve(xhr.responseText);
        };
        xhr.send(data);
    })
}

但是现在request里不仅要执行发送ajax请求,还要执行拦截器handlers中的回调函数。所以,最好下就是将执行ajax的请求封装成一个方法

request(config) {
    this.sendAjax(config)
}
sendAjax(config){
    return new Promise(resolve => {
        const {url = '', method = 'get', data = {}} = config;
        // 发送ajax请求
        console.log(config);
        const xhr = new XMLHttpRequest();
        xhr.open(method, url, true);
        xhr.onload = function() {
            console.log(xhr.responseText)
            resolve(xhr.responseText);
        };
        xhr.send(data);
    })
}

好了,现在我们要获得handlers中的回调

request(config) {
    // 拦截器和请求组装队列
    let chain = [this.sendAjax.bind(this), undefined] // 成对出现的,失败回调暂时不处理

    // 请求拦截
    this.interceptors.request.handlers.forEach(interceptor => {
        chain.unshift(interceptor.fullfield, interceptor.rejected)
    })

    // 响应拦截
    this.interceptors.response.handlers.forEach(interceptor => {
        chain.push(interceptor.fullfield, interceptor.rejected)
    })

    // 执行队列,每次执行一对,并给promise赋最新的值
    let promise = Promise.resolve(config);
    while(chain.length > 0) {
        promise = promise.then(chain.shift(), chain.shift())
    }
    return promise;
}

我们先把sendAjax请求和undefined放进了chain数组里,再把请求拦截器的handlers的成对回调放到chain数组头部。再把响应拦截器的handlers的承兑回调反倒chain数组的尾部。

然后再 逐渐取数 chain数组的成对回调执行。

promise = promise.then(chain.shift(), chain.shift())

这一句,实际上就是不断将config从上一个promise传递到下一个promise,期间可能回调config做出一些修改。什么意思?我们结合一个例子来讲解一下

首先拦截器是这样使用的

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

然后执行request的时候。chain数组的数据是这样的

chain = [
  function (config) {
    // 在发送请求之前做些什么
    return config;
  }, 
  
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  }
  this.sendAjax.bind(this), 
  
  undefined,
  
  function (response) {
    // 对响应数据做点什么
    return response;
  }, 
  function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  }
]

首先

执行第一次promise.then(chain.shift(), chain.shift()),即

promise.then(
  function (config) {
    // 在发送请求之前做些什么
    return config;
  }, 
  
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  }
)

一般情况,promise是resolved状态,是执行成功回调的,也就是执行

function (config) {
    // 在发送请求之前做些什么
    return config;
  }, 

而promise.then是要返回一个新的promise对象的。

为了区分,在这里,我会把这个新的promise对象叫做第一个新的promise对象
这个第一个新的promise对象会把

function (config) {
    // 在发送请求之前做些什么
    return config;
  }, 

的执行结果传入resolve函数中

resolve(config)

使得这个返回的第一个新的promise对象的状态为resovled,而且第一个新的promise对象的data为config。

这里需要对Promise的原理足够理解。所以我前一篇文章写的是手写Promise核心原理,再也不怕面试官问我Promise原理,你可以去看看
接下来,再执行

promise.then(
  sendAjax(config)
  ,
  undefined
)

注意:这里的promise是 上面提到的第一个新的promise对象。

而promise.then这个的执行又会返回第二个新的promise对象。

因为这里promise.then中的promise也就是第一个新的promise对象的状态是resolved的,所以会执行sendAjax()。而且会取出第一个新的promise对象的data 作为config转入sendAjax()。

当sendAjax执行完,就会返回一个response。这个response就会保存在第二个新的promise对象的data中。

接下来,再执行

promise.then(
  function (response) {
    // 对响应数据做点什么
    return response;
  }, 
  function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  }
)

同样,会把第二个新的promise对象的data取出来作为response参数传入

function (response) {
    // 对响应数据做点什么
    return response;
  }, 

饭后返回一个promise对象,这个promise对象的data保存了这个函数的执行结果,也就是返回值response。

然后通过return promise;

把这个promise返回了。咦?是怎么取出promise的data的。我们看看我们平常事怎么获得响应数据的

axios.get('http://localhost:5000/getTest')
    .then(res => {
         console.log('getAxios 成功响应', res);
    })

在then里接收响应数据。所以原理跟上面一样,将返回的promise的data作为res参数了。

现在看看我们的myAxios完整代码吧,好有个全面的了解

class InterceptorsManage {
    constructor() {
        this.handlers = [];
    }

    use(fullfield, rejected) {
        this.handlers.push({
            fullfield,
            rejected
        })
    }
}

class Axios {
    constructor() {
        this.interceptors = {
            request: new InterceptorsManage,
            response: new InterceptorsManage
        }
    }

    request(config) {
        // 拦截器和请求组装队列
        let chain = [this.sendAjax.bind(this), undefined] // 成对出现的,失败回调暂时不处理

        // 请求拦截
        this.interceptors.request.handlers.forEach(interceptor => {
            chain.unshift(interceptor.fullfield, interceptor.rejected)
        })

        // 响应拦截
        this.interceptors.response.handlers.forEach(interceptor => {
            chain.push(interceptor.fullfield, interceptor.rejected)
        })

        // 执行队列,每次执行一对,并给promise赋最新的值
        let promise = Promise.resolve(config);
        while(chain.length > 0) {
            promise = promise.then(chain.shift(), chain.shift())
        }
        return promise;
    }
    sendAjax(){
        return new Promise(resolve => {
            const {url = '', method = 'get', data = {}} = config;
            // 发送ajax请求
            console.log(config);
            const xhr = new XMLHttpRequest();
            xhr.open(method, url, true);
            xhr.onload = function() {
                console.log(xhr.responseText)
                resolve(xhr.responseText);
            };
            xhr.send(data);
        })
    }
}

// 定义get,post...方法,挂在到Axios原型上
const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];
methodsArr.forEach(met => {
    Axios.prototype[met] = function() {
        console.log('执行'+met+'方法');
        // 处理单个方法
        if (['get', 'delete', 'head', 'options'].includes(met)) { // 2个参数(url[, config])
            return this.request({
                method: met,
                url: arguments[0],
                ...arguments[1] || {}
            })
        } else { // 3个参数(url[,data[,config]])
            return this.request({
                method: met,
                url: arguments[0],
                data: arguments[1] || {},
                ...arguments[2] || {}
            })
        }

    }
})


// 工具方法,实现b的方法混入a;
// 方法也要混入进去
const utils = {
    extend(a,b, context) {
        for(let key in b) {
            if (b.hasOwnProperty(key)) {
                if (typeof b[key] === 'function') {
                    a[key] = b[key].bind(context);
                } else {
                    a[key] = b[key]
                }
            }

        }
    }
}


// 最终导出axios的方法-》即实例的request方法
function CreateAxiosFn() {
    let axios = new Axios();

    let req = axios.request.bind(axios);
    // 混入方法, 处理axios的request方法,使之拥有get,post...方法
    utils.extend(req, Axios.prototype, axios)
    return req;
}

// 得到最后的全局变量axios
let axios = CreateAxiosFn();

来测试下拦截器功能是否正常

<script type="text/javascript" src="./myAxios.js"></script>

<body>
<button class="btn">点我发送请求</button>
<script>
    // 添加请求拦截器
    axios.interceptors.request.use(function (config) {
        // 在发送请求之前做些什么
        config.method = "get";
        console.log("被我请求拦截器拦截了,哈哈:",config);
        return config;
    }, function (error) {
        // 对请求错误做些什么
        return Promise.reject(error);
    });

    // 添加响应拦截器
    axios.interceptors.response.use(function (response) {
        // 对响应数据做点什么
        console.log("被我响应拦截拦截了,哈哈 ");
        response = {message:"响应数据被我替换了,啊哈哈哈"}
        return response;
    }, function (error) {
        // 对响应错误做点什么
        console.log("错了吗");
        return Promise.reject(error);
    });
    document.querySelector('.btn').onclick = function() {
        // 分别使用以下方法调用,查看myaxios的效果
        axios({
          url: 'http://localhost:5000/getTest'
        }).then(res => {
          console.log('response', res);
        })
    }
</script>
</body>

拦截成功!!!!!全掘金人民向我们发来贺电!!!!

有什么不理解的或者是建议欢迎评论提出

感谢您也恭喜您看到这里,我可以卑微的求个star吗!!!

github:https://github.com/Sunny-lucking/howToBuildMyAxios

算法集合

5. 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

示例 1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:

输入:s = "cbbd"
输出:"bb"

解法:双指针

/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function(s) {
    let max = ''

    for(let i=0; i< s.length; i++) {
        // 分奇偶, 一次遍历,每个字符位置都可能存在奇数或偶数回文
        helper(i, i)
        helper(i, i+1)
    }

    function helper(l, r) {
        // 定义左右双指针
        while(l>=0 && r< s.length && s[l] === s[r]) {
            l--
            r++
        }
        // 拿到回文字符, 注意 上面while满足条件后多执行了一次,所以需要l+1, r+1-1
        const maxStr = s.slice(l + 1, r + 1 - 1);
        // 取最大长度的回文字符
        if (maxStr.length > max.length) max = maxStr
    }
    return max
};

6. Z 字形变换

解法

/**
 * @param {string} s
 * @param {number} numRows
 * @return {string}
 */
var convert = function(s, numRows) {
    if(numRows == 1)
        return s;

    const len = Math.min(s.length, numRows);
    const rows = [];
    for(let i = 0; i< len; i++) rows[i] = "";
    let loc = 0;
    let down = false;

    for(const c of s) {
        rows[loc] += c;
        if(loc == 0 || loc == numRows - 1)
            down = !down;
        loc += down ? 1 : -1;
    }

    let ans = "";
    for(const row of rows) {
        ans += row;
    }
    return ans;
};

14. 最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ""。

示例 1:

输入:strs = ["flower","flow","flight"]
输出:"fl"
示例 2:

输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。

/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function(strs) {
    if(strs.length == 0) 
        return "";
    let ans = strs[0];
    for(let i =1;i<strs.length;i++) {
        let j=0;
        for(;j<ans.length && j < strs[i].length;j++) {
            if(ans[j] != strs[i][j])
                break;
        }
        ans = ans.substr(0, j);
        if(ans === "")
            return ans;
    }
    return ans;
};

15. 三数之和

解法

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function(nums) {
    let ans = [];
    const len = nums.length;
    if(nums == null || len < 3) return ans;
    nums.sort((a, b) => a - b); // 排序
    for (let i = 0; i < len ; i++) {
        if(nums[i] > 0) break; // 如果当前数字大于0,则三数之和一定大于0,所以结束循环
        if(i > 0 && nums[i] == nums[i-1]) continue; // 去重
        let L = i+1;
        let R = len-1;
        while(L < R){
            const sum = nums[i] + nums[L] + nums[R];
            if(sum == 0){
                ans.push([nums[i],nums[L],nums[R]]);
                while (L<R && nums[L] == nums[L+1]) L++; // 去重
                while (L<R && nums[R] == nums[R-1]) R--; // 去重
                L++;
                R--;
            }
            else if (sum < 0) L++;
            else if (sum > 0) R--;
        }
    }        
    return ans;
};

16. 最接近的三数之和

解法

固定一个数,剩下两个数就变成了 双指针 的常规解法。

var threeSumClosest = function(nums, target) {
    let N = nums.length
    let res = Number.MAX_SAFE_INTEGER
    nums.sort((a, b) => a - b)
    for (let i = 0; i < N; i++) {
        let left = i + 1
        let right = N - 1
        while (left < right) {
            let sum = nums[i] + nums[left] + nums[right]
            if (Math.abs(sum - target) < Math.abs(res - target)) {
                res = sum
            }
            if (sum < target) {
                left++
            } else if (sum > target) {
                right--
            } else {
                return sum
            }
        }
    }
    return res
};

17. 电话号码的字母组合

解法

//输入:digits = "23"
//输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
var letterCombinations = (digits) => {
    if (digits.length == 0) return [];
    const res = [];
    const map = {//建立电话号码和字母的映射关系
        2: "abc",
        3: "def",
        4: "ghi",
        5: "jkl",
        6: "mno",
        7: "pqrs",
        8: "tuv",
        9: "wxyz",
    };

    const dfs = (curStr, i) => {//curStr是递归每一层的字符串,i是扫描的指针
        if (i > digits.length - 1) {//边界条件,递归的出口
            res.push(curStr); //其中一个分支的解推入res
            return; //结束递归分支,进入另一个分支
        }
        const letters = map[digits[i]]; //取出数字对应的字母
        for (const l of letters) {
            //进入不同字母的分支
            dfs(curStr + l, i + 1); //参数传入新的字符串,i右移,继续递归
        }
    };
    dfs("", 0); // 递归入口,传入空字符串,i初始为0的位置
    return res;
};

22. 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:

输入:n = 1
输出:["()"]

var generateParenthesis = function (n) {
  const res = [];

  const dfs = (lRemain, rRemain, str) => { // 左右括号所剩的数量,str是当前构建的字符串
    if (str.length == 2 * n) { // 字符串构建完成
      res.push(str);           // 加入解集
      return;                  // 结束当前递归分支
    }
    if (lRemain > 0) {         // 只要左括号有剩,就可以选它,然后继续做选择(递归)
      dfs(lRemain - 1, rRemain, str + "(");
    }
    if (lRemain < rRemain) {   // 右括号比左括号剩的多,才能选右括号
      dfs(lRemain, rRemain - 1, str + ")"); // 然后继续做选择(递归)
    }
  };

  dfs(n, n, ""); // 递归的入口,剩余数量都是n,初始字符串是空串
  return res;
};

31. 下一个排列

解法

function nextPermutation(nums) {
    let i = nums.length - 2;                   // 向左遍历,i从倒数第二开始是为了nums[i+1]要存在
    while (i >= 0 && nums[i] >= nums[i + 1]) { // 寻找第一个小于右邻居的数
        i--;
    }
    if (i >= 0) {                             // 这个数在数组中存在,从它身后挑一个数,和它换
        let j = nums.length - 1;                // 从最后一项,向左遍历
        while (j >= 0 && nums[j] <= nums[i]) {  // 寻找第一个大于 nums[i] 的数
            j--;
        }
        [nums[i], nums[j]] = [nums[j], nums[i]]; // 两数交换,实现变大
    }
    // 如果 i = -1,说明是递减排列,如 3 2 1,没有下一排列,直接翻转为最小排列:1 2 3
    let l = i + 1;           
    let r = nums.length - 1;
    while (l < r) {                            // i 右边的数进行翻转,使得变大的幅度小一些
        [nums[l], nums[r]] = [nums[r], nums[l]];
        l++;
        r--;
    }
}

33. 搜索旋转排序数组

var search = function (nums, target) {
    // 二分法
    let start = 0;
    let end = nums.length - 1;

    while (start <= end) {
        // >> 1 相当于除以2向下取整
        let mid = (start + end) >> 1;

        if (nums[mid] === target) {
            return mid;
        }

        // 如果中间数小于最右边数,则右半段是有序的
        // 如果中间数大于最右边数,则左半段是有序的
        if (nums[mid] < nums[end]) {
            // 判断target是否在(mid, end]之间
            if (nums[mid] < target && target <= nums[end]) {
                // 如果在,则中间数右移即start增大
                start = mid + 1;
            } else {
                // 如果不在,则中间数左移即end减小
                end = mid - 1;
            }
        } else {
            // [start, mid)
            if (nums[start] <= target && target < nums[mid]) {
                end = mid - 1;
            } else {
                start = mid + 1;
            }
        }
    }

    return -1;
};

39. 组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:

输入: candidates = [2], target = 1
输出: []

解法:无非就是选与不选

var combinationSum = function(candidates, target) {
    const ans = [];
    const dfs = (target, combine, idx) => {
        if (idx === candidates.length) {
            return;
        }
        if (target === 0) {
            ans.push(combine);
            return;
        }
        // 直接跳过
        dfs(target, combine, idx + 1);
        // 选择当前数
        if (target - candidates[idx] >= 0) {
            dfs(target - candidates[idx], [...combine, candidates[idx]], idx);
        }
    }

    dfs(target, [], 0);
    return ans;
};

45. 跳跃游戏 II

给你一个非负整数数组 nums ,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

假设你总是可以到达数组的最后一个位置。

示例 1:

输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
     从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:

输入: nums = [2,3,0,1,4]
输出: 2

var jump = function (nums) {
  let farthestPos = 0 // 记录当前能去到的最远的位置,遍历每个点都会求能跳到的最远位置,与它比较,如果把它大就更新它
  let endOfCanReach = 0 
  let steps = 0 
  for (let i = 0; i < nums.length - 1; i++) {
    farthestPos = Math.max(farthestPos, i + nums[i])
    if (i === endOfCanReach) { 
      endOfCanReach = farthestPos // 可抵达区间的右端位置
      steps++
    }
    if (endOfCanReach >= nums.length - 1) { // 一旦新的可抵达区间触碰到nums数组的边界,则直接break,不用对区间的点遍历了
      break
    }
  }
  return steps
};

46. 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。


示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:

输入:nums = [1]
输出:[[1]]

const permute = (nums) => {
    const res = [];
    const used = {};

    function dfs(path) {
        if (path.length == nums.length) { // 个数选够了
            res.push(path.slice()); // 拷贝一份path,加入解集res
            return;                 // 结束当前递归分支
        }
        for (const num of nums) { // for枚举出每个可选的选项
            // if (path.includes(num)) continue; // 别这么写!查找是O(n),增加时间复杂度
            if (used[num]) continue; // 使用过的,跳过
            path.push(num);         // 选择当前的数,加入path
            used[num] = true;       // 记录一下 使用了
            dfs(path);              // 基于选了当前的数,递归
            path.pop();             // 上一句的递归结束,回溯,将最后选的数pop出来
            used[num] = false;      // 撤销这个记录
        }
    }

    dfs([]); // 递归的入口,空path传进去
    return res;
};

48. 旋转图像

var rotate = function(matrix) {
    const n = matrix.length;
    //水平中轴线翻转
    for (let i = 0; i < Math.floor(n / 2); i++) {
        for (let j = 0; j < n; j++) {
            [matrix[i][j], matrix[n - i - 1][j]] = [matrix[n - i - 1][j], matrix[i][j]];
        }
    }
    //主对角线翻转
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < i; j++) {
            [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
        }
    }
};

53. 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:

输入:nums = [1]
输出:1
示例 3:

输入:nums = [5,4,-1,7,8]
输出:23

54. 螺旋矩阵

var spiralOrder = function (matrix) {
    if (matrix.length === 0) return []
    const res = []
    let top = 0, bottom = matrix.length - 1, left = 0, right = matrix[0].length - 1
    while (top < bottom && left < right) {
        for (let i = left; i < right; i++) res.push(matrix[top][i])   // 上层
        for (let i = top; i < bottom; i++) res.push(matrix[i][right]) // 右层
        for (let i = right; i > left; i--) res.push(matrix[bottom][i])// 下层
        for (let i = bottom; i > top; i--) res.push(matrix[i][left])  // 左层
        right--
        top++
        bottom--
        left++  // 四个边界同时收缩,进入内层
    }
    if (top === bottom) // 剩下一行,从左到右依次添加
        for (let i = left; i <= right; i++) res.push(matrix[top][i])
    else if (left === right) // 剩下一列,从上到下依次添加
        for (let i = top; i <= bottom; i++) res.push(matrix[i][left])
    return res
};

55. 跳跃游戏

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

示例 1:

输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:

输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

方法:动态规划

解题思路:

由题目描述,我们需要达到最后一个下标,那么最后一个下标的数字其实是可以不用考虑的。那么我们可以假设只有两个数字(比如 [2,4][2, 4][2,4]),这个时候第一个数字如果是大于等于 111 的数就成立;如果是三个数字的话(比如 [3,0,4][3, 0, 4][3,0,4]),第一个数字大于等于 222 时成立。依此类推,一个数字可以到达的位置必须是这个数字标记的长度值,有:nums[i]>=jnums[i] >= jnums[i]>=j 成立时才可以到达后面第 jjj 个目标。

有了上述前提,假设现在只有最后两位数字,在判断出倒数第 222 位数字可以到达最后一位时,我们只需要看剩下的部分,如果可以到达倒数第 222 位,那么整体就可以到达最后一位数字。

我们记录一个最后一位的下标,然后依次向前寻找满足跳跃条件的下标,并将该下标与记录的下标替换。重复这个过程直到判断了 numsnumsnums 的第一个下标为止,最后判断记录值是否为第一个下标,也就是 000 ,如果不是 000 返回 falsefalsefalse。该过程时间复杂度为 O(n)O(n)O(n),空间复杂度为 O(1)O(1)O(1) 。

/**
 * @param {number[]} nums
 * @return {boolean}
 */
var canJump = function(nums) {
    // 必须到达end下标的数字
    let end = nums.length - 1;

    for (let i = nums.length - 2; i >= 0; i--) {
        if (end - i <= nums[i]) {
            end = i;
        }
    }

    return end == 0;
};

56. 合并区间

合并的策略
原则上要更新prev[0]和prev[1],即左右端:
prev[0] = min(prev[0], cur[0])
prev[1] = max(prev[1], cur[1])
但如果先按区间的左端排升序,就能保证 prev[0] < cur[0]
所以合并只需这条:prev[1] = max(prev[1], cur[1])
易错点
我们是先合并,遇到不重合再推入 prev。 当考察完最后一个区间,后面没区间了,遇不到不重合区间,最后的 prev 没推入 res。 要单独补上。
var merge = function (intervals) {
  let res = [];
  intervals.sort((a, b) => a[0] - b[0]);

  let prev = intervals[0];

  for (let i = 1; i < intervals.length; i++) {
    let cur = intervals[i];
    if (prev[1] >= cur[0]) { // 有重合
      prev[1] = Math.max(cur[1], prev[1]); 
    } else {       // 不重合,prev推入res数组 
      res.push(prev);
      prev = cur;  // 更新 prev
    }
  }

  res.push(prev);
  return res;
};

57. 插入区间

给你一个 无重叠的 ,按照区间起始端点排序的区间列表。

在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。

示例 1:

输入:intervals = [[1,3],[6,9]], newInterval = [2,5]
输出:[[1,5],[6,9]]
示例 2:

输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
输出:[[1,2],[3,10],[12,16]]
解释:这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。
示例 3:

输入:intervals = [], newInterval = [5,7]
输出:[[5,7]]
function insert(intervals, newInterval) {
  const res = [];
  let i = 0;
  const len = intervals.length;

  while (i < len && intervals[i][1] < newInterval[0]) { // 当前遍历的是蓝左边的,不重叠的区间
    res.push(intervals[i]);
    i++;
  }

  while (i < len && intervals[i][0] <= newInterval[1]) { // 当前遍历是有重叠的区间
    newInterval[0] = Math.min(newInterval[0], intervals[i][0]); //左端取较小者,更新给兰区间的左端
    newInterval[1] = Math.max(newInterval[1], intervals[i][1]); //右端取较大者,更新给兰区间的右端
    i++;
  }
  res.push(newInterval); // 循环结束后,兰区间为合并后的区间,推入res

  while (i < len) {                 // 在蓝右边,没重叠的区间
    res.push(intervals[i]);
    i++;
  }
  
  return res;
}

63. 不同路径 II

const uniquePathsWithObstacles = (obstacleGrid) => {
  if (obstacleGrid[0][0] == 1) return 0; // 出发点就被障碍堵住 
  const m = obstacleGrid.length;
  const n = obstacleGrid[0].length;
  // dp数组初始化
  const dp = new Array(m);
  for (let i = 0; i < m; i++) dp[i] = new Array(n);
  // base case
  dp[0][0] = 1;                 // 终点就是出发点
  for (let i = 1; i < m; i++) { // 第一列其余的case
    dp[i][0] = obstacleGrid[i][0] == 1 || dp[i - 1][0] == 0 ? 0 : 1;
  }
  for (let i = 1; i < n; i++) { // 第一行其余的case
    dp[0][i] = obstacleGrid[0][i] == 1 || dp[0][i - 1] == 0 ? 0 : 1;
  }
  // 迭代
  for (let i = 1; i < m; i++) {
    for (let j = 1; j < n; j++) {
      dp[i][j] = obstacleGrid[i][j] == 1 ?
        0 :
        dp[i - 1][j] + dp[i][j - 1];
    }
  }
  return dp[m - 1][n - 1]; // 到达(m-1,n-1)的路径数
};

71. 简化路径

示例 1:

输入:path = "/home/"
输出:"/home"
解释:注意,最后一个目录名后面没有斜杠。 
示例 2:

输入:path = "/../"
输出:"/"
解释:从根目录向上一级是不可行的,因为根目录是你可以到达的最高级。
示例 3:

输入:path = "/home//foo/"
输出:"/home/foo"
解释:在规范路径中,多个连续斜杠需要用一个斜杠替换。
示例 4:

输入:path = "/a/./b/../../c/"
输出:"/c"
var simplifyPath = function(path) {
    const names = path.split("/");
    const stack = [];
    for (const name of names) {
        if (name === "..") {
            if (stack.length) {
                stack.pop();
            } 
        } else if (name.length && name !== ".") {
            stack.push(name);

        }
    }
    
    return "/" + stack.join("/");
};

73. 矩阵置零

思路和算法

我们可以用两个标记数组分别记录每一行和每一列是否有零出现。

具体地,我们首先遍历该数组一次,如果某个元素为 000,那么就将该元素所在的行和列所对应标记数组的位置置为 true\text{true}true。最后我们再次遍历该数组,用标记数组更新原数组即可。

var setZeroes = function(matrix) {
    const m = matrix.length, n = matrix[0].length;
    const row = new Array(m).fill(false);
    const col = new Array(n).fill(false);
    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (matrix[i][j] === 0) {
                row[i] = col[j] = true;
            }
        }
    }
    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (row[i] || col[j]) {
                matrix[i][j] = 0;
            }
        }
    }
};

77. 组合

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]
示例 2:

输入:n = 1, k = 1

const combine = (n, k) => {
  const res = [];

  const helper = (start, path) => { // start是枚举选择的起点 path是当前构建的路径(组合)
    if (path.length == k) {
      res.push(path.slice());       // 拷贝一份path,推入res
      return;                       // 结束当前递归
    }
    for (let i = start; i <= n; i++) { // 枚举出所有选择
      path.push(i);                    // 选择
      helper(i + 1, path);             // 向下继续选择
      path.pop();                      // 撤销选择
    }
  };

  helper(1, []); // 递归的入口,从数字1开始选
  return res;
}

78. 子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:

输入:nums = [0]
输出:[[],[0]]

const subsets = (nums) => {
  const res = [];

  const dfs = (index, list) => {
    if (index == nums.length) { // 指针越界
      res.push(list.slice());   // 加入解集
      return;                   // 结束当前的递归
    }
    list.push(nums[index]); // 选择这个数
    dfs(index + 1, list);   // 基于该选择,继续往下递归,考察下一个数
    list.pop();             // 上面的递归结束,撤销该选择
    dfs(index + 1, list);   // 不选这个数,继续往下递归,考察下一个数
  };

  dfs(0, []);
  return res;
};

120. 三角形最小路径和

const minimumTotal = (triangle) => {
  const h = triangle.length;
  // 初始化dp数组
  const dp = new Array(h);
  for (let i = 0; i < h; i++) {
    dp[i] = new Array(triangle[i].length);
  }

  for (let i = h - 1; i >= 0; i--) { // 自底而上遍历
    for (let j = 0; j < triangle[i].length; j++) { // 同一层的
      if (i == h - 1) {  // base case 最底层
        dp[i][j] = triangle[i][j];  
      } else { // 状态转移方程,上一层由它下面一层计算出
        dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j];
      }
    }
  }
  return dp[0][0];
};

121. 买卖股票的最佳时机

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

示例 1

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0
var maxProfit = function(prices) {
    let result = 0, pre = prices[0];
    for(let i = 1; i < prices.length; i++){
        // 找到前一天的最小价格
        pre = Math.min(pre, prices[i]);
        // 计算如果今天卖出的话,收益是多少,然后和之前的收益做比较,返回最大的收益
        result = Math.max(result, prices[i] - pre);
    }
    return result;
};

128. 最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:

输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9

解题思路

一开始的想法是将数组排序,之后遍历数组,记录每个连续部分的curLen,再根据curLen更新maxLen

/**
 * @param {number[]} nums
 * @return {number}
 */
var longestConsecutive = function(nums) {
    let n = nums.length;
    if(n < 2) return n;
    let maxLen = 1;
    let curLen = 1;
    nums.sort((a, b) => {//排序
        return a - b;
    })
    for(let i = 1; i < n; i++) {
        if(nums[i] === nums[i - 1] + 1) {//记录每个连续部分的curLen
            curLen++;
        }
        else if(nums[i] === nums[i - 1] ) {//遇到相同数字不计入curLen
            continue;
        }
        else {//中断了,开始找新的连续序列
            curLen = 1;
        }
        maxLen = Math.max(maxLen, curLen);//比较更新maxLen
    }
    return maxLen;
};

但是再看官解评论区,才发现题目要求时间复杂度为O(n),而sort排序不符合,那么sort的时间复杂度是啥呢


也就是说,元素个数少的时候采用插入排序,平均时间复杂度为O(n^2);元素个数多的时候采用快排,平均时间复杂度为O(nlogn)

排序的目的是避免把每个数字当作起点寻找最长序列,也就是暴力法,比如[1,2,3],我都已经有序列[1,2,3]了,那么再找[2,3]就是多此一举

不用排序,还有更好的方法吗,用哈希表。哈希表寻找,插入元素的时间复杂度是O(1),建立set的时间复杂度是O(n),并且set已经去重

怎么避免把每个数字当作起点寻找最长序列?如果在哈希表中能找到x - 1,那么显然x不是最长连续序列的起点

因此,先找到能作为起点的数,再找这个数作为起点得到的连续序列长度curLen,根据curLen更新maxLen

/**
 * @param {number[]} nums
 * @return {number}
 */
var longestConsecutive = function(nums) {
    let n = nums.length;
    if(n < 2) return n;
    let maxLen = 0;
    let numsSet = new Set(nums);
    for(num of numsSet) {
        if(!numsSet.has(num - 1)) {
            let start = num;
            let curLen = 1; 
            while(numsSet.has(start + 1)) {
                curLen++;
                start++;
            }
            maxLen = Math.max(curLen, maxLen);
        }
    }
    return maxLen;
};

130. 被围绕的区域

给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。

输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]]
输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。

思路

如果把 X 看作海水,把 O 看作陆地,被海水包围的区域就是岛屿。没被海水包围的陆地,与边界有连通,不是岛屿。题目要把岛屿沉了,变成海水。

判断是否为岛屿比较困难,但找出非岛屿比较简单——凡是与边界有联系的 O,标记为 NO,表示非岛屿。这个找的过程可以用 DFS 或 BFS。

所以非岛屿被标记为 NO,最后将它们恢复为 O,其余的 O,变成X。

const solve = (board) => {
  const m = board.length;
  if (m == 0) return;         // [] 情况的特判
  const n = board[0].length;
  const dfs = (i, j) => {
    if (i < 0 || i == m || j < 0 || j == n) return; // 越界了
    if (board[i][j] == 'O') { // 遇到O,染为NO      
      board[i][j] = 'NO';                    
      dfs(i + 1, j);          // 对四个方向的邻居进行dfs
      dfs(i - 1, j);
      dfs(i, j + 1);
      dfs(i, j - 1);
    }
  };
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (i == 0 || i == m - 1 || j == 0 || j == n - 1) {
        if (board[i][j] == 'O') dfs(i, j); // 从最外层的O,开始DFS
      }
    }
  }
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (board[i][j] === 'NO') board[i][j] = 'O';     // 恢复为O
      else if (board[i][j] === 'O') board[i][j] = 'X'; // O变为X
    }
  }
};

134. 加油站

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

示例 1:

输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
示例 2:

输入: gas = [2,3,4], cost = [3,4,3]
输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。

为什么总加油>=总油耗就一定有解?简单论证两个关键结论 | 附暴力法

var canCompleteCircuit = function (gas, cost) {
    const n = gas.length;
    gas = gas.concat(gas);
    cost = cost.concat(cost);

    for (let i = 0; i < n; i++) { 
        let left = 0;
        let iIsStart = true;
        for (let j = i; j < i + n; j++) {
            left += gas[j] - cost[j];
            if (left < 0) {
                iIsStart = false;
                break;
            }
        }
        if (iIsStart) return i;
    }
    return -1;
};
var canCompleteCircuit = function (gas, cost) {
    let left = 0, start = 0, totalGas = 0, totalCost = 0;
    for (let i = 0; i < gas.length; i++) {
        totalGas += gas[i];
        totalCost += cost[i];
        left += gas[i] - cost[i];
        if (left < 0) {
            start = i + 1;
            left = 0;
        }
    }
    if (totalGas < totalCost) {
        return -1;
    }
    return start;
};

155. 最小栈

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

MinStack() 初始化堆栈对象。
void push(int val) 将元素val推入堆栈。
void pop() 删除堆栈顶部的元素。
int top() 获取堆栈顶部的元素。
int getMin() 获取堆栈中的最小元素。
示例 1:

输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]

输出:
[null,null,null,null,-3,null,0,-2]

解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.getMin();   --> 返回 -2.

解题思路

其实push,pop,top操作js数组就能做,关键是getMin如何实现,删除堆栈顶部元素如何更新getMin

利用辅助栈,每次push元素进堆栈时也push一个值进存储最小值的辅助栈,这个值是辅助栈栈顶和当前元素更小的那个,保证了辅助栈栈顶存储的元素是最小值

删除堆栈顶部元素时也删除辅助栈栈顶元素,保持最小值的同步,这样删除到堆栈最小值时辅助栈中的最小值也被删除完毕,实现了更新getMin

代码

var MinStack = function() {
    this.stack = [];
    this.min = [Infinity];
};

/** 
 * @param {number} val
 * @return {void}
 */
MinStack.prototype.push = function(val) {
    this.stack.push(val);
    this.min.push(Math.min(this.min[this.min.length - 1], val));
};

/**
 * @return {void}
 */
MinStack.prototype.pop = function() {
    this.stack.pop();
    this.min.pop();
};

/**
 * @return {number}
 */
MinStack.prototype.top = function() {
    return this.stack[this.stack.length - 1];
};

/**
 * @return {number}
 */
MinStack.prototype.getMin = function() {
    return this.min[this.min.length - 1];
};

162. 寻找峰值

165. 比较版本号

  1. 切割为数组
  2. 比较大小
  3. 循环判断

给你两个版本号 version1 和 version2 ,请你比较它们。

版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.33 和 0.1 都是有效的版本号。

比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 0 和 1 ,0 < 1 。

返回规则如下:

如果 version1 > version2 返回 1,
如果 version1 < version2 返回 -1,
除此之外返回 0。

示例 1:

输入:version1 = "1.01", version2 = "1.001"
输出:0
解释:忽略前导零,"01" 和 "001" 都表示相同的整数 "1"
示例 2:

输入:version1 = "1.0", version2 = "1.0.0"
输出:0
解释:version1 没有指定下标为 2 的修订号,即视为 "0"
示例 3:

输入:version1 = "0.1", version2 = "1.1"
输出:-1
解释:version1 中下标为 0 的修订号是 "0",version2 中下标为 0 的修订号是 "1" 。0 < 1,所以 version1 < version2
var compareVersion = function(version1, version2) {
    const a = version1.split('.');
    const b = version2.split('.');
    const maxLength = Math.max(a.length, b.length);
    for (let i = 0; i < maxLength; i++) {
        const cur = a[i] || 0;
        const next = b[i] || 0;
        if (a[i] === b[i]) continue;
        if (parseInt(cur) > parseInt(next)) {
            return 1;
        } else if(parseInt(cur) < parseInt(next)) {
            return -1;
        }
    }
    return 0;

};

168. Excel表列名称

给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称。

例如:

A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28 
...
示例 1:

输入:columnNumber = 1
输出:"A"
示例 2:

输入:columnNumber = 28
输出:"AB"
示例 3:

输入:columnNumber = 701
输出:"ZY"
示例 4:

输入:columnNumber = 2147483647
输出:"FXSHRXW"

进制转换的变种

本题本质就是进制转换,10进制转26进制,但有所不同的是正常转换成26进制的余数是0-25, 而本题的余数是1-26(对应A-Z),为了消除差距的这个1,有两种方法:

①让除数减一,那么余数自然就少一,原来余 1 的变成余 0,以此类推(详细见下表)。
核心代码 let remain = (n - 1) % 26;

/**
 * @param {number} n
 * @return {string}
 */
var convertToTitle = function(n) {
    if(n <= 0) return "";

    let res = [];
    while(n) {
        n--; // 通过让 n - 1,使得余数 remain 减少 1 
        let remain = n % 26;
        res.unshift(String.fromCharCode(remain + 65));
        n = Math.floor(n / 26);
    }
    return res.join("");
};

171. Excel 表列序号

给你一个字符串 columnTitle ,表示 Excel 表格中的列名称。返回 该列名称对应的列序号 。

例如:

A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28 
...
 

示例 1:

输入: columnTitle = "A"
输出: 1
示例 2:

输入: columnTitle = "AB"
输出: 28
示例 3:

输入: columnTitle = "ZY"
输出: 701
var titleToNumber = function(columnTitle) {
    let ans = 0;
    for (const c of columnTitle) {
        ans = ans * 26 + (c.charCodeAt() - 64);
    }
    return ans;
};

200. 岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1:

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1
示例 2:

输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3
const numIslands = (grid) => {
    let count = 0
    for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[0].length; j++) {//循环网格
            if (grid[i][j] === '1') {//如果为陆地,count++,
                count++
                turnZero(i, j, grid)
            }
        }
    }
    return count
}
function turnZero(i, j, grid) {//沉没四周的陆地
    if (i < 0 || i >= grid.length || j < 0
        || j >= grid[0].length || grid[i][j] === '0') return //检查坐标的合法性
    grid[i][j] = '0'//让四周的陆地变为海水
    turnZero(i, j + 1, grid)
    turnZero(i, j - 1, grid)
    turnZero(i + 1, j, grid)
    turnZero(i - 1, j, grid)
}

动态规划

称砝码

描述

现有n种砝码,重量互不相等,分别为 m1,m2,m3…mn ;

每种砝码对应的数量为 x1,x2,x3...xn 。现在要用这些砝码去称物体的重量(放在同一侧),问能称出多少种不同的重量。

输入描述:

对于每组测试数据:

  • 第一行:n --- 砝码的种数(范围[1,10])
  • 第二行:m1 m2 m3 ... mn --- 每种砝码的重量(范围[1,2000])
  • 第三行:x1 x2 x3 .... xn --- 每种砝码对应的数量(范围[1,10])

输出描述:

利用给定的砝码可以称出的不同的重量数

示例1

输入:
2
1 2
2 1


输出:
5


说明:
可以表示出0,1,2,3,4五种重量。   

代码

let line1 = readline();let line2 = readline();let line3 = readline();
let m = line2.split(' '); //每种砝码的重量
let x = line3.split(' '); //每种砝码对应的数量范围
let fama = []            //序列化砝码,比如两个1g和一个2g的砝码用[1,1,2]表示
for (let i = 0; i < m.length; i++) {
    for (let j = 0; j < x[i]; j++) {
        fama.push(Number(m[i]))
    }
}
let kind = new Set();    //用set表示加入当前砝码之前能产生的重量种类
kind.add(0);            //set初始化为0
// 当第一个1g砝码放入时,set中要插入原先所有元素+1g后的结构,即{0,0+1},插入后变为{0,1}
// 当第二个1g砝码放入时,set要插入{0+1,1+1},变为{0,1,2}
// 第三个2g砝码放入时,set要插入{0+2,1+2,2+2},变为{0,1,2,3,4}
for (let i = 0; i < fama.length; i++) {
    let arr = [...kind]    //用一个数组来缓存当前种类的砝码的值
    for (let k of arr) {
        kind.add(k + fama[i]);
    }
}
console.log(kind.size)

广度优先

剑指 Offer 32 - II. 从上到下打印二叉树 II

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
    if(!root) {
        return []
    }
    let ret = []
    let q = [root]
    while(q.length) {
        let qLen = q.length
        let level = []
        while(qLen >0) {
            let node = q.shift();
            level.push(node.val)
            if(node.left) {
                q.push(node.left)
            }
            if(node.right) {
                q.push(node.right)
            }
            qLen--
        }
        ret.push(level)
    }
    return ret
    
};

数学

HJ108 求最小公倍数

let line = readline();
let A = line.split(' ').map(Number)[0]
let B = line.split(' ').map(Number)[1]

let data = A * B;
let maxNum = Math.max(A,B);

for(let i = maxNum; i <= data; i++ ) {
    if(i % A === 0 && i %B === 0){
        console.log(i);
        break;
    }
}

HJ60 查找组成一个偶数最接近的两个素数

判断一个数是否为素数的方法

只能给1 和自身整除的数,

function isPrime(num){
    for(let i = 2; i <= Math.sqrt(num); i++){
        if(num % i == 0) return false
    }
    return true
}

题目解析

isPrime(i) && isPrime(n - i) 使用n 和n - 1 的判断方式,组成n两个素数有多组,不过他们的差最小的是最后一组,因此 取两个变量num1 和num2 来保存最后一组输出即可。

let n = parseInt(readline())

function isPrime(num){
    for(let i = 2; i <= Math.sqrt(num); i++){
        if(num % i == 0) return false
    }
    return true
}

let num1, num2
for(let i = 1; i<= n / 2; i++){
   if(isPrime(i) && isPrime(n - i)){
       num1 = i
       num2 = n - i
   }
}
print(num1)
print(num2)

手写webpack核心原理,再也不怕面试官问我webpack原理

手写webpack核心原理

@[toc]

一、核心打包原理

1.1 打包的主要流程如下

  1. 需要读到入口文件里面的内容。
  2. 分析入口文件,递归的去读取模块所依赖的文件内容,生成AST语法树。
  3. 根据AST语法树,生成浏览器能够运行的代码

1.2 具体细节

  1. 获取主模块内容
  2. 分析模块
    • 安装@babel/parser包(转AST)
  3. 对模块内容进行处理
    • 安装@babel/traverse包(遍历AST收集依赖)
    • 安装@babel/core和@babel/preset-env包 (es6转ES5)
  4. 递归所有模块
  5. 生成最终代码

二、基本准备工作

我们先建一个项目

项目目录暂时如下:

已经把项目放到 githubhttps://github.com/Sunny-lucking/howToBuildMyWebpack 可以卑微地要个star吗

我们创建了add.js文件和minus.js文件,然后 在index.js中引入,再将index.js文件引入index.html。

代码如下:

add.js

export default (a,b)=>{
  return a+b;
}

minus.js

export const minus = (a,b)=>{
    return a-b
}

index.js

import add from "./add.js"
import {minus} from "./minus.js";

const sum = add(1,2);
const division = minus(2,1);

console.log(sum);
console.log(division);

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>

现在我们打开index.html。你猜会发生什么???显然会报错,因为浏览器还不能识别import等ES6语法

不过没关系,因为我们本来就是要来解决这些问题的。

三、获取模块内容

好了,现在我们开始根据上面核心打包原理的思路来实践一下,第一步就是 实现获取主模块内容。

我们来创建一个bundle.js文件。

// 获取主入口文件
const fs = require('fs')
const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    console.log(body);
}
getModuleInfo("./src/index.js")

目前项目目录如下

我们来执行一下bundle.js,看看是否成功获得入口文件内容

哇塞,不出所料的成功。一切尽在掌握之中。好了,已经实现第一步了,且让我看看第二步是要干嘛。

哦?是分析模块了

四、分析模块

分析模块的主要任务是 将获取到的模块内容 解析成AST语法树,这个需要用到一个依赖包@babel/parser

npm install @babel/parser

ok,安装完成我们将@babel/parser引入bundle.js,

// 获取主入口文件
const fs = require('fs')
const parser = require('@babel/parser')
const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    // 新增代码
    const ast = parser.parse(body,{
        sourceType:'module' //表示我们要解析的是ES模块
    });
    console.log(ast);
}
getModuleInfo("./src/index.js")

我们去看下@babel/parser的文档:


可见提供了三个API,而我们目前用到的是parse这个API。

它的主要作用是 parses the provided code as an entire ECMAScript program,也就是将我们提供的代码解析成完整的ECMAScript代码的AST。

再看看该API提供的参数


我们暂时用到的是sourceType,也就是用来指明我们要解析的代码是什么模块。

好了,现在我们来执行一下 bundle.js,看看AST是否成功生成。

成功。又是不出所料的成功。

不过,我们需要知道的是,当前我们解析出来的不单单是index.js文件里的内容,它也包括了文件的其他信息。
而它的内容其实是它的属性program里的body里。如图所示

我们可以改成打印ast.program.body看看

// 获取主入口文件
const fs = require('fs')
const parser = require('@babel/parser')
const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示我们要解析的是ES模块
    });
    console.log(ast.program.body);
}
getModuleInfo("./src/index.js"

执行

看,现在打印出来的就是 index.js文件里的内容(也就是我们再index.js里写的代码啦).

五、收集依赖

现在我们需要 遍历AST,将用到的依赖收集起来。什么意思呢?其实就是将用import语句引入的文件路径收集起来。我们将收集起来的路径放到deps里。

前面我们提到过,遍历AST要用到@babel/traverse依赖包

npm install @babel/traverse

现在,我们引入。

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示我们要解析的是ES模块
    });
    
    // 新增代码
    const deps = {}
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file)
            const abspath = './' + path.join(dirname,node.source.value)
            deps[node.source.value] = abspath
        }
    })
    console.log(deps);


}
getModuleInfo("./src/index.js")

我们来看下官方文档对@babel/traverse的描述


好吧,如此简略

不过我们不难看出,第一个参数就是AST。第二个参数就是配置对象

我们看看我们写的代码

traverse(ast,{
    ImportDeclaration({node}){
        const dirname = path.dirname(file)
        const abspath = './' + path.join(dirname,node.source.value)
        deps[node.source.value] = abspath
    }
})

配置对象里,我们配置了ImportDeclaration方法,这是什么意思呢?
我们看看之前打印出来的AST。

ImportDeclaration方法代表的是对type类型为ImportDeclaration的节点的处理。

这里我们获得了该节点中source的value,也就是node.source.value,

这里的value指的是什么意思呢?其实就是import的值,可以看我们的index.js的代码。

import add from "./add.js"
import {minus} from "./minus.js";

const sum = add(1,2);
const division = minus(2,1);

console.log(sum);
console.log(division);

可见,value指的就是import后面的 './add.js' 和 './minus.js'

然后我们将file目录路径跟获得的value值拼接起来保存到deps里,美其名曰:收集依赖。

ok,这个操作就结束了,执行看看收集成功了没?

oh my god。又成功了。

六、ES6转成ES5(AST)

现在我们需要把获得的ES6的AST转化成ES5,前面讲到过,执行这一步需要两个依赖包

npm install @babel/core @babel/preset-env

我们现在将依赖引入并使用

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示我们要解析的是ES模块
    });
    const deps = {}
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file)
            const abspath = "./" + path.join(dirname,node.source.value)
            deps[node.source.value] = abspath
        }
    })
    
    新增代码
    const {code} = babel.transformFromAst(ast,null,{
        presets:["@babel/preset-env"]
    })
    console.log(code);

}
getModuleInfo("./src/index.js")

我们看看官网文档对@babel/core 的transformFromAst的介绍

害,又是一如既往的简略。。。

简单说一下,其实就是将我们传入的AST转化成我们在第三个参数里配置的模块类型。

好了,现在我们来执行一下,看看结果

我的天,一如既往的成功。可见 它将我们写const 转化成var了。

好了,这一步到此结束,咦,你可能会有疑问,上一步的收集依赖在这里怎么没啥关系啊,确实如此。收集依赖是为了下面进行的递归操作。

七、递归获取所有依赖

经过上面的过程,现在我们知道getModuleInfo是用来获取一个模块的内容,不过我们还没把获取的内容return出来,因此,更改下getModuleInfo方法

const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示我们要解析的是ES模块
    });
    const deps = {}
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file)
            const abspath = "./" + path.join(dirname,node.source.value)
            deps[node.source.value] = abspath
        }
    })
    const {code} = babel.transformFromAst(ast,null,{
        presets:["@babel/preset-env"]
    })
    // 新增代码
    const moduleInfo = {file,deps,code}
    return moduleInfo
}

我们返回了一个对象 ,这个对象包括该模块的路径(file)该模块的依赖(deps)该模块转化成es5的代码

该方法只能获取一个模块的的信息,但是我们要怎么获取一个模块里面的依赖模块的信息呢?

没错,看标题,,你应该想到了就算递归。

现在我们来写一个递归方法,递归获取依赖

const parseModules = (file) =>{
    const entry =  getModuleInfo(file)
    const temp = [entry]
    for (let i = 0;i<temp.length;i++){
        const deps = temp[i].deps
        if (deps){
            for (const key in deps){
                if (deps.hasOwnProperty(key)){
                    temp.push(getModuleInfo(deps[key]))
                }
            }
        }
    }
    console.log(temp)
}

讲解下parseModules方法:

  1. 我们首先传入主模块路径
  2. 将获得的模块信息放到temp数组里。
  3. 外面的循坏遍历temp数组,此时的temp数组只有主模块
  4. 循环里面再获得主模块的依赖deps
  5. 遍历deps,通过调用getModuleInfo将获得的依赖模块信息push到temp数组里。

目前bundle.js文件:

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示我们要解析的是ES模块
    });
    const deps = {}
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file)
            const abspath = "./" + path.join(dirname,node.source.value)
            deps[node.source.value] = abspath
        }
    })
    const {code} = babel.transformFromAst(ast,null,{
        presets:["@babel/preset-env"]
    })
    const moduleInfo = {file,deps,code}
    return moduleInfo
}

// 新增代码
const parseModules = (file) =>{
    const entry =  getModuleInfo(file)
    const temp = [entry]
    for (let i = 0;i<temp.length;i++){
        const deps = temp[i].deps
        if (deps){
            for (const key in deps){
                if (deps.hasOwnProperty(key)){
                    temp.push(getModuleInfo(deps[key]))
                }
            }
        }
    }
    console.log(temp)
}
parseModules("./src/index.js")

按照目前我们的项目来说执行完,应当是temp 应当是存放了index.js,add.js,minus.js三个模块。
,执行看看。

牛逼!!!确实如此。

不过现在的temp数组里的对象格式不利于后面的操作,我们希望是以文件的路径为key,{code,deps}为值的形式存储。因此,我们创建一个新的对象depsGraph。

const parseModules = (file) =>{
    const entry =  getModuleInfo(file)
    const temp = [entry] 
    const depsGraph = {} //新增代码
    for (let i = 0;i<temp.length;i++){
        const deps = temp[i].deps
        if (deps){
            for (const key in deps){
                if (deps.hasOwnProperty(key)){
                    temp.push(getModuleInfo(deps[key]))
                }
            }
        }
    }
    // 新增代码
    temp.forEach(moduleInfo=>{
        depsGraph[moduleInfo.file] = {
            deps:moduleInfo.deps,
            code:moduleInfo.code
        }
    })
    console.log(depsGraph)
    return depsGraph
}

ok,现在存储的就是这种格式啦

八、处理两个关键字

我们现在的目的就是要生成一个bundle.js文件,也就是打包后的一个文件。其实思路很简单,就是把index.js的内容和它的依赖模块整合起来。然后把代码写到一个新建的js文件。

我们把这段代码格式化一下

// index.js
"use strict"
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum); console.log(division);
// add.js
"use strict";
Object.defineProperty(exports, "__esModule", {  value: true});
exports["default"] = void 0;
var _default = function _default(a, b) {  return a + b;};
exports["default"] = _default;

但是我们现在是不能执行index.js这段代码的,因为浏览器不会识别执行require和exports。

不能识别是为什么?不就是因为没有定义这require函数,和exports对象。那我们可以自己定义。

我们创建一个函数

const bundle = (file) =>{
    const depsGraph = JSON.stringify(parseModules(file))
    
}

我们将上一步获得的depsGraph保存起来。

现在返回一个整合完整的字符串代码。

怎么返回呢?更改下bundle函数

const bundle = (file) =>{
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function (graph) {
                function require(file) {
                    (function (code) {
                        eval(code)
                    })(graph[file].code)
                }
                require(file)
            })(depsGraph)`
    
}

我们看下返回的这段代码

 (function (graph) {
        function require(file) {
            (function (code) {
                eval(code)
            })(graph[file].code)
        }
        require(file)
    })(depsGraph)

其实就是

  1. 把保存下来的depsGraph,传入一个立即执行函数。
  2. 将主模块路径传入require函数执行
  3. 执行reuire函数的时候,又立即执行一个立即执行函数,这里是把code的值传进去了
  4. 执行eval(code)。也就是执行主模块的code这段代码

我们再来看下code的值

// index.js
"use strict"
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum); console.log(division);

没错执行这段代码的时候,又会用到require函数。此时require的参数为add.js的路径,哎,不是绝对路径,需要转化成绝对路径。因此写一个函数absRequire来转化。怎么实现呢?我们来看下代码

(function (graph) {
    function require(file) {
        function absRequire(relPath) {
            return require(graph[file].deps[relPath])
        }
        (function (require,code) {
            eval(code)
        })(absRequire,graph[file].code)
    }
    require(file)
})(depsGraph)

实际上是实现了一层拦截。

  1. 执行require('./src/index.js')函数
  2. 执行了
(function (require,code) {
    eval(code)
})(absRequire,graph[file].code)
  1. 执行eval,也就是执行了index.js的代码。
  2. 执行过程会执行到require函数。
  3. 这时会调用这个require,也就是我们传入的absRequire
  4. 而执行absRequire就执行了return require(graph[file].deps[relPath])这段代码,也就是执行了外面这个require


在这里return require(graph[file].deps[relPath]),我们已经对路径转化成绝对路径了。因此执行外面的require的时候就是传入绝对路径。

  1. 而执行require("./src/add.js")之后,又会执行eval,也就是执行add.js文件的代码。

是不是有点绕?其实是个递归。

这样就将代码整合起来了,但是有个问题,就是在执行add.js的code时候,会遇到exports这个还没定义的问题。如下所示

// add.js
"use strict";
Object.defineProperty(exports, "__esModule", {  value: true});
exports["default"] = void 0;
var _default = function _default(a, b) {  return a + b;};
exports["default"] = _default;

我们发现 这里它把exports当作一个对象来使用了,但是这个对象还没定义,因此我们可以自己定义一个exports对象。

(function (graph) {
    function require(file) {
        function absRequire(relPath) {
            return require(graph[file].deps[relPath])
        }
        var exports = {}
        (function (require,exports,code) {
            eval(code)
        })(absRequire,exports,graph[file].code)
        return exports
    }
    require(file)
})(depsGraph)

我们增添了一个空对象 exports,执行add.js代码的时候,会往这个空对象上增加一些属性,

// add.js
"use strict";
Object.defineProperty(exports, "__esModule", {  value: true});
exports["default"] = void 0;
var _default = function _default(a, b) {  return a + b;};
exports["default"] = _default;

比如,执行完这段代码后

exports = {
  __esModule:{  value: true}
  default:function _default(a, b) {  return a + b;}
}

然后我们把exports对象return出去。

var _add = _interopRequireDefault(require("./add.js"));

可见,return出去的值,被_interopRequireDefault接收,_interopRequireDefault再返回default这个属性给_add,因此_add = function _default(a, b) { return a + b;}

现在明白了,为什么ES6模块 引入的是一个对象引用了吧,因为exports就是一个对象。

至此,处理;两个关键词的功能就完整了。

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const getModuleInfo = (file)=>{
    const body = fs.readFileSync(file,'utf-8')
    const ast = parser.parse(body,{
        sourceType:'module' //表示我们要解析的是ES模块
    });
    const deps = {}
    traverse(ast,{
        ImportDeclaration({node}){
            const dirname = path.dirname(file)
            const abspath = "./" + path.join(dirname,node.source.value)
            deps[node.source.value] = abspath
        }
    })
    const {code} = babel.transformFromAst(ast,null,{
        presets:["@babel/preset-env"]
    })
    const moduleInfo = {file,deps,code}
    return moduleInfo
}
const parseModules = (file) =>{
    const entry =  getModuleInfo(file)
    const temp = [entry]
    const depsGraph = {}
    for (let i = 0;i<temp.length;i++){
        const deps = temp[i].deps
        if (deps){
            for (const key in deps){
                if (deps.hasOwnProperty(key)){
                    temp.push(getModuleInfo(deps[key]))
                }
            }
        }
    }
    temp.forEach(moduleInfo=>{
        depsGraph[moduleInfo.file] = {
            deps:moduleInfo.deps,
            code:moduleInfo.code
        }
    })
    return depsGraph
}
// 新增代码
const bundle = (file) =>{
    const depsGraph = JSON.stringify(parseModules(file))
    return `(function (graph) {
        function require(file) {
            function absRequire(relPath) {
                return require(graph[file].deps[relPath])
            }
            var exports = {}
            (function (require,exports,code) {
                eval(code)
            })(absRequire,exports,graph[file].code)
            return exports
        }
        require('${file}')
    })(${depsGraph})`

}
const content = bundle('./src/index.js')

console.log(content);

来执行下,看看效果


确实执行成功。接下来,把返回的这段代码写入新创建的文件中

//写入到我们的dist目录下
fs.mkdirSync('./dist');
fs.writeFileSync('./dist/bundle.js',content)

至次,我们的手写webpack核心原理就到此结束了。

我们参观下生成的bundle.js文件

发现其实就是将我们早期收集的所有依赖作为参数传入到立即执行函数当中,然后通过eval来递归地执行每个依赖的code。

现在我们将bundle.js文件引入index.html看看能不能执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d21afmU8-1595668735614)(https://imgkr.cn-bj.ufileos.com/8316bad8-53b8-4586-9317-aa98ac72eb1f.png)]

成功。。。。。惊喜。。

感谢您也恭喜您看到这里,我可以卑微的求个star吗!!!

github:https://github.com/Sunny-lucking/howToBuildMyWebpack

在这里插入图片描述

关于【ajax和json】十问(读《红宝书》)

第一问:原生js写一个简单的ajax请求

典型的xhr建立ajax的过程。(涵盖了ajax的大部分内容)

  1. new一个xhr对象。(XMLHttpRequest或者ActiveXObject)
  2. 调用xhr对象的open方法。
  3. send一些数据。
  4. 对服务器的响应过程进行监听,来知道服务器是否正确得做出了响应,接着就可以做一些事情。比如获取服务器响应的内容,在页面上进行呈现。
//1、创建一个ajax对象 
var xhr = new XMLHttpRequest(); 

//3、绑定处理函数,我们写的代码,都在这里面 
xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
        if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
            console.log(xhr.responseText)
        } else {
            console.log("fail")
        }
    }
}

//2、进行初始化 
xhr.open('get','http://js.com/day6/ajax_get.php');


//4、发送请求
xhr.send(null);

注意点:

  1. 如果不需要通过请求头发送数据,必须传入null
  2. 为确保跨浏览器兼容性,建议xhr.onreadystatechange事件处理程序写在xhr.open前面
  3. setRequestHeader必须写在xhr.openxhr.send(null)之间

第二问:readyState各阶段的含义

  1. 未初始化,但是已经创建了XHR实例
  2. 调用了open()函数
  3. 已经调用了send()函数,但还未收到服务器回应
  4. 正在接受服务器返回的数据
  5. 完成响应

第三问:怎么终止请求

再接收到响应之前还可以调用obort()方法来取消异步请求

xhr.abort()

调用这个方法后,XHR对象会停止触发事件,而且也不再允许访问任何与响应有关的对象属性。在终止请求之后,还应该对XHR对象进行解引用操作。由于内存原因,不建议重用XHR对象

第四问:如何利用xhr做请求的进度条

另一个革新是添加了progress事件,这个时间会在浏览器接受新数据期间周期性的触发,而onprogress事件处理程序会接收到一个event对象,其target属性是XHR对象,但包含着两个额外的属性:position和totalSize。其中position表示已经接受的字节数,totleSize表示根据Content-Length响应头部确定的预期字节数。

xhr.onprogress = function (event) {
    var divStatus = document.getElementById("status");
    divStatus.innerHTML = "Received" + event.position + "of" + event.totalSize + "bytes";
};

第五问:谈一下跨域

1、JSONP跨域

jsonp的原理就是利用<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。

2、跨域资源共享(CORS)

  CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

浏览器将CORS跨域请求分为简单请求和非简单请求。

只要同时满足一下两个条件,就属于简单请求

(1)使用下列方法之一:

  • head
  • get
  • post

(2)请求的Heder是

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type: 只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain

不同时满足上面的两个条件,就属于非简单请求。浏览器对这两种的处理,是不一样的。

简单请求

  对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

非简单请求

  非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

预检请求

  预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。请求头信息里面,关键字段是Origin,表示请求来自哪个源。除了Origin字段,"预检"请求的头信息包括两个特殊字段。

3、nodejs中间服务器代理跨域
利用本地服务器(跟前端项目同协议,同域名,同端口)来代理转发,利用的是同源策略是只发生在浏览器,而两个服务端是不会出现跨域问题的。

webpack种配置跨域就是这个原理。

webpack.config.js部分配置:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

4、使用图片ping跨域

 图像Ping跨域请求技术是使用标签。一个网页可以从任何网页中加载图像,不用担心跨域不跨域。这也是在线广告跟踪浏览量的主要方式。也可以动态地创建图像,使用它们的 onload 和 onerror事件 处理程序来确定是否接收到了响应

  动态创建图像经常用于图像Ping:图像Ping是与服务器进行简单、单向的跨域通信的一种方式。 请求的数据是通过査询字符串形式发送的,而响应可以是任意内容,但通常是像素图或204响应。通过图像Ping,浏览器得不到任何具体的数据,但通过侦听load和error事件,它能知道响应是什么时候接收到的

图像Ping有两个主要的缺点,一是只能发送GET请求,二是无法访问服务器的响应文本。因此,图像Ping只能用于浏览器与服务器间的单向通信

第六问:JS跨域方案JSONP与CORS的各自优缺点以及应用场景?

两者优点与缺点大致互补,放在一块介绍:

JSONP的主要优势在于对浏览器的支持较好;虽然目前主流浏览器支持CORS,但IE10以下不支持CORS。

JSONP只能用于获取资源(即只读,类似于GET请求);CORS支持所有类型的HTTP请求,功能完善。(这点JSONP被玩虐,但大部分情况下GET已经能满足需求了)

JSONP的错误处理机制并不完善,我们没办法进行错误处理;而CORS可以通过onerror事件监听错误,并且浏览器控制台会看到报错信息,利于排查。

JSONP只会发一次请求;而对于复杂请求,CORS会发两次请求。

始终觉得安全性这个东西是相对的,没有绝对的安全,也做不到绝对的安全。毕竟JSONP并不是跨域规范,它存在很明显的安全问题:callback参数注入和资源访问授权设置。CORS好歹也算是个跨域规范,在资源访问授权方面进行了限制(Access-Control-Allow-Origin),而且标准浏览器都做了安全限制,比如拒绝手动设置origin字段,相对来说是安全了一点。
但是回过头来看一下,就算是不安全的JSONP,我们依然可以在服务端端进行一些权限的限制,服务端和客户端也都依然可以做一些注入的安全处理,哪怕被攻克,它也只能读一些东西。就算是比较安全的CORS,同样可以在服务端设置出现漏洞或者不在浏览器的跨域限制环境下进行攻击,而且它不仅可以读,还可以写。

应用场景:

如果你需要兼容IE低版本浏览器,无疑,JSONP。

如果你需要对服务端资源进行谢操作,无疑,CORS。

其他情况的话,根据自己的对需求的分析和对两者的理解来吧。

第七问:我要是硬要用CORS方法解决跨域呢?有没办法兼容IE低版本

大部分浏览器都已经实现了CORS(跨域资源共享)的规范,IE低版本浏览器却无法支持这一规范。

在ie8,9中,有XDomainRequest对象可以实现跨域请求。可以和xhr一起使用。这个对象拥有onerror,onload,onprogress,ontimeout四个事件,abort,open,send三个方法,contentType, responseText,timeout三个属性。具体参见XDomainRequest对象。

XDomainRequest对象有很多限制,例如只支持get、post方法、不能自定义请求的header头、不能携带cookie、只支持text/plain类型的内容格式等等。具体参见XDomainRequest对象限制。

因此虽然XdomainRequest作为ie8、9中的一种跨域手段,但是适用的业务场景还是比较局限的。

第八问:json.stringify()与json.parse()的区别

json.stringfy()将对象、数组转换成字符串;json.parse()将字符串转成json对象。

1.parse 用于从一个字符串中解析出json 对象

var str='{"name":"TMD","sex":"男","age":"26"}';

console.log(JSON.parse(str));//{name: "TMD", sex: "男", age: "26"}

2.stringify用于从一个对象解析出字符串

var o={a:1,b:2,c:3};

console.log(JSON.stringify(o));//{"a":1,"b":2,"c":3}

第九问:json.stringify()用于实现深拷贝时有什么缺点呢?

弊端:

1.如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式

2、如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;

3.如果obj里有RegExp(正则表达式的缩写)、Error对象,则序列化的结果将只得到空对象;

4、如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null

5、JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;

6、如果对象中存在循环引用的情况也无法正确实现深拷贝;

第十问:现在我要用json.stringify()用于实现深拷贝,而且对象里有undefined或者函数,Date,该怎么办呢?

查看文档,发现JSON.parse是可以传一个转换结果的函数的

 JSON.parse()方法也可以接收一个函数参数,在每个键值对儿上调用,这个函数被称为还原函数(reviver)。该函数接收两个参数,一个键和一个值,返回一个值

  如果还原函数返回undefined,则表示要从结果中删除相应的键;如果返回其他值,则将该值插入到结果中


一次可以判断 key是不是Date,是的话就转化下

var book = {
    "title": "javascript",
    "date": new Date(2016,9,1)
}
var jsonStr = JSON.stringify(book);
//'{"title":"javascript","date":"2016-09-30T16:00:00.000Z"}''
console.log(jsonStr)

var bookCopy = JSON.parse(jsonStr,function(key,value){
    if(key == 'date'){
        return new Date(value);
    }
    return value;
})
console.log(bookCopy.date.getFullYear());//2016

手写Promise核心原理,再也不怕面试官问我Promise原理

文章会配合例子来讲解为什么要这么实现,尽我所能讲得粗俗易懂。有什么不理解或建议,欢迎下方评论。项目源码已经放到 github,顺手点个star呗:https://github.com/Sunny-lucking/howToBuildMyPromise

整体流程的介绍

整体流程的介绍

  1. 定义整体结构

  2. 实现Promise构造函数

  3. 实现then方法

  4. 实现catch方法

  5. 实现Promise.resolve

  6. 实现Promise.reject

  7. 实现Promise.all

  8. 实现Promise.race

1. 定义整体结构

先写出构造函数,将Promise向外暴露

/*
自定义Promise函数模块:IIFE
 */

(function (window) {
    /*
    Promise构造函数
    executor:执行器函数
     */
    function Promise(executor) {

    }

    // 向外暴露Promise
    window.Promise = Promise
})()

添加Promise原型对象上的方法

 /*
    Promise原型对象的then
    指定一个成功/失败的回调函数
    返回一个新的promise对象
     */
    Promise.prototype.then = function(onResolved,onRejected){

    }

    /*
    Promise原型对象的.catch
    指定一个失败的回调函数
    返回一个新的promise对象
     */
    Promise.prototype.catch = function(onRejected){

    }

添加Promise函数对象上的方法

    /*
    Promise函数对象的resovle方法
    返回一个指定结果的promise对象
     */
    Promise.resolve = function(value){

    }

    /*
    Promise函数对象的reject方法
    返回一个指定reason的失败状态的promise对象
    */
    Promise.reject = function(value){

    }

    /*
    Promise函数对象的all方法
    返回一个promise对象,只有当所有promise都成功时返回的promise状态才成功
    */
    Promise.all = function(0value){

    }

    /*
    Promise函数对象的race方法
    返回一个promise对象,状态由第一个完成的promise决定
    */
    Promise.race = function(value){

    }

通过上面的注释可以知道。不管是Promise原型对象上的方法还是Promise函数对象上的方法 ,它们的执行结果都将返回一个Promise对象

2. 实现Promise构造函数

我们看看我们是怎么使用Promise的

const promiseA = new Promise( (resolve,reject) => {
    resolve(777);
});

我们传入了一个函数,而且这个函数被立即执行,不仅如此,这个函数还会立即执行resolve和reject。说明构造函数里有resolve和reject方法。因此我们可以初步实现:

/*
Promise构造函数
executor:执行器函数
 */
function Promise(executor) {

    function resovle() {

    }
    function reject() {

    }

    // 立即同步执行executor
    executor(resovle,reject)
}

每个promise都有一个状态可能为pending或resolved,rejected。而且初始状态都为pending。因此需要添加个status来表示当前promise的状态.。并且每个promise有自己的data。

function Promise(executor) {

    var self = self
    新增代码
    self.status = 'pending' // 给promise对象指定status属性,初始值为pending

    self.data = undefined // 给promise对象指定一个存储结果的data

    function resovle() {

    }
    function reject() {

    }

    // 立即同步执行executor
    executor(resovle,reject)
}

此外,当我们这样使用Promise的时候,

// 例1
var promise = new Promise((resovle,reject)=>{
    
})

promise.then(resolve=>{},reject=>{})

这时执行到then,因为我们传入的立即执行函数没有执行resolve或者reject,所以promise的状态还是pending,这时要把then里面的回调函数保存起来,所以需要个callbacks数组

function Promise(executor) {

    var self = self

    self.status = 'pending' // 给promise对象指定status属性,初始值为pending
    self.data = undefined // 给promise对象指定一个存储结果的data
    新增代码
    self.callbacks = []  // 每个元素的结构:{onResolved(){},onRejected(){}}


    function resovle() {

    }
    function reject() {

    }

    // 立即同步执行executor
    executor(resovle,reject)
}

那 then函数是怎么把传入的回调收集起来的。其实很简单,就是判断当前promise是否为pending状态,是的话,就把回调push到callbacks中。

Promise.prototype.then = function(onResolved,onRejected){

    var self = this

    if(self.status === 'pending'){
        // promise当前状态还是pending状态,将回调函数保存起来
        self.callbacks.push({
            onResolved(){onResolved(self.data)},
            onRejected(){onRejected(self.data)}
        })
    }else if(self.status === 'resolved'){
    }else{
    }

}

在上面的例子1的基础上,当我们执行resovle(value)时,如例2

// 例2
var promise = new Promise((resolve,reject)=>{
    setTimeout(function () {
        resolve(1)
    })
})

promise.then(
  value=>{console.log(value)},
  err=>{console.log(err)}
  )

此时代码执行情况是怎么样的呢?

  1. 先执行new Promise里的代码,然后发现个定时器,js线程将定时器交给定时器线程处理,2. 然后继续执行下面的代码,发现是then,而且当前的promise还是pending的状态。就把then里的回调函数放到callbacks中。

  2. 5秒后定时器线程将定时器里的回调函数(也就是宏任务)放到消息队列中,js线程在消息队列里发现了这个宏任务,就把它拿来执行。

  3. 执行这个宏任务,就执行了resolve(1),此时promise的callbacks里的回调被执行。并将当前promise状态改为resolved。然后这个1也会被保存到当前promise对象中

那怎么实现resolve呢?依旧上面的描述,就知道resovle的功能是执行callbacks里的函数,并保存data,并将当前promise状态改为resolved。所以我们可以这么实现

function resolve(value) {
    // 将状态改为resolved
    self.status = 'resolved'
    // 保存value的值
    self.data = value

    // 如果有待执行的callback函数,立即异步执行回调函数onResolved
    if (self.callbacks.length>0){
        self.callbacks.forEach(callbackObj=>{
            callbackObj.onResolved(value)
        })
    }
}

我们还知道,promise的状态只能改变一次,因此当执行resolve的时候要判断是不是promise是不是pending的状态,否则是不能执行的

function resolve(value) {
    // 如果当前状态不是pending,则不执行
    if(this.status !== 'pending'){
        return 
    }
    // 将状态改为resolved
    this.status = 'resolved'
    // 保存value的值
    this.data = value

    // 如果有待执行的callback函数,立即异步执行回调函数onResolved
    if (this.callbacks.length>0){
        setTimeout(()=>{
            this.callbacks.forEach(callbackObj=>{ A
                callbackObj.onResolved(value)
            })
        })
    }
}

异曲同工之妙的是reject方法也是这个道理,因此这里无需赘述

function reject(value) {
    // 如果当前状态不是pending,则不执行
    if(self.status !== 'pending'){
        return
    }
    // 将状态改为rejected
    self.status = 'rejected'
    // 保存value的值
    self.data = value

    // 如果有待执行的callback函数,立即异步执行回调函数onResolved
    if (self.callbacks.length>0){
      self.callbacks.forEach(callbackObj=>{
          callbackObj.onRejected(value)
      })
    }
}

我们又知道,当在执行executor的时候,如果执行异常的话,这个promise的状态会直接执行reject方法。例如

// 例 3
var promise = new Promise((resolve,reject)=>{

    error;执行到这里出错了

    setTimeout(function () {
        resolve(1)
    })
})

要实现这个功能,我们可以在executor外让try catch来捕获

try{
    // 立即同步执行executor
    executor(resolve,reject)
}catch (e) { // 如果执行器抛出异常,promise对象变为rejected状态
    reject(e)
}

好了,现在测试下

 // 例4
 let promise = new Promise((resolve,reject)=>{
        
        setTimeout(function () {
            resolve(1)
            //reject(1)
        },100)
    })

    promise.then(
        value=>{
            console.log("onResolved:",value);
        },
        reason=>{
            console.log("onRejected:",reason);
        }
    )

发现成功。成功输出onResolved:1

3. 实现then方法

我们在上面简单的实现了当前promise为pending状态的情况,如:

Promise.prototype.then = function(onResolved,onRejected){

    var self = this

    if(self.status === 'pending'){
        // promise当前状态还是pending状态,将回调函数保存起来
        self.callbacks.push({
            onResolved(){onResolved(self.data)},
            onRejected(){onRejected(self.data)}
        })
    }else if(self.status === 'resolved'){
    }else{
    }

}

那其他情况呢?

执行到then时,promise可能会是pending状态,此时就要把then里的回调函数保存起来,也可能会是resolved或者rejected状态,此时就不用把回调保存起来,直接执行onResolved或onRejected方法。注意是异步执行。而且是做为微任务的,这里我们简单的用setTimeout来实现就好了。

Promise.prototype.then = function(onResolved,onRejected){

  var self = this

  if(self.status === 'pending'){
      // promise当前状态还是pending状态,将回调函数保存起来
      self.callbacks.push({
          onResolved(){onResolved(self.data)},
          onRejected(){onRejected(self.data)}
      })
  }else if(self.status === 'resolved'){
      setTimeout(()=>{
          onResolved(self.data)
      })
  }else{
      setTimeout(()=>{
          onResolved(self.data)
      })
  }

}

而且我们知道,执行完then是要返回一个新的promise的,而新的promise的状态则由当前then的执行结果来确定。

 Promise.prototype.then = function(onResolved,onRejected){

    var self = this

    return new Promise((resolve,reject)=>{
        if(self.status === 'pending'){
            // promise当前状态还是pending状态,将回调函数保存起来
            self.callbacks.push({
                onResolved(){onResolved(self.data)},
                onRejected(){onRejected(self.data)}
            })
        }else if(self.status === 'resolved'){
            setTimeout(()=>{
                onResolved(self.data)
            })
        }else{
            setTimeout(()=>{
                onResolved(self.data)
            })
        }
    })

}

当 当前的promise状态为resolved的时候,则执行then的时候,会执行第二个判断语句

则当前执行第二个判断语句的时候会出现三种情况

  1. 如果then里的回调函数返回的不是promise,return的新promise的状态是则是resolved,value就是返回的值。例如:
// 例5
let promise = new Promise((resolve,reject)=>{
    resolve(1)
})

promise.then(
    value=>{
        return value //返回的不是promise,是value
    }
)

因此,我们可以这样实现

Promise.prototype.then = function(onResolved,onRejected){

    var self = this

    return new Promise((resolve,reject)=>{
        if(self.status === 'pending'){
            // promise当前状态还是pending状态,将回调函数保存起来
            self.callbacks.push({
                onResolved(){onResolved(self.data)},
                onRejected(){onRejected(self.data)}
            })
        }else if(self.status === 'resolved'){
            修改代码
            setTimeout(()=>{
                const result = onResolved(self.data)
                if (result instanceof Promise){

                } else {
                // 1. 如果回调函数返回的不是promise,return的promise的状态是resolved,value就是返回的值。
                    resolve(result)
                }
            })
        }else{
            setTimeout(()=>{
                onResolved(self.data)
            })
        }
    })

}

简单解释下:

执行onResolved(self.data),其实就是执行例子中的下面这个回调函数

value=>{
        return value //返回的不是promise,是value
    }

那么这个回调函数返回了value。就把value传入resolve函数,resolve函数将当前新的promise的状态改为resolved,同时将value保存到当前新的promise的data中。

  1. 如果回调函数返回的是promise,return的promise的结果就是这个promise的结果,如代码所示,我们返回一个新的promise。如果这个promise执行了resolve,返回的新的promise的状态则是resolved的。否则为rejected
// 例6
let promise = new Promise((resolve,reject)=>{
    resolve(1)
})



promise.then(
    value=>{
        return new Promise((resolve,reject)=>{
            resolve(2)
            //或者
            //reject(error)
        })
    }
)

因此我们可以这样实现

Promise.prototype.then = function(onResolved,onRejected){

    var self = this

    return new Promise((resolve,reject)=>{
        if(self.status === 'pending'){
            // promise当前状态还是pending状态,将回调函数保存起来
            self.callbacks.push({
                onResolved(){onResolved(self.data)},
                onRejected(){onRejected(self.data)}
            })
        }else if(self.status === 'resolved'){
            setTimeout(()=>{
                const result = onResolved(self.data)
                if (result instanceof Promise){
                    // 2. 如果回调函数返回的是promise,return的promise的结果就是这个promise的结果
                    result.then(
                        value => {resolve(value)},
                        reason => {reject(reason)}
                    )
                } else {
                    // 1. 如果回调函数返回的不是promise,return的promise的状态是resolved,value就是返回的值。
                    resolve(result)
                }
            })
        }else{
            setTimeout(()=>{
                onResolved(self.data)
            })
        }
    })

}

在这里说明一下:

result.then(
    value => {resolve(value)},
    reason => {reject(reason)}
)

由于我们在例6中执行了then里的

value=>{
        return new Promise((resolve,reject)=>{
            resolve(2)
            //或者
            //reject(error)
        })
    }

则返回一个promise对象,这个promise对象可能为resolved状态(执行 resolve(2))也可能为rejected状态(执行reject(error))。

将会导致value => {resolve(value)},这个回调函数的执行或者reason => {reject(reason)}的执行。

因此会把即将返回的新的promise的data设置为value或者,reason。会把状态设置为resolved或者rejected。

  1. 如果执行这段代码的时候抛出错误,则返回的promise的状态为rejected,我们可以用try catch来实现
setTimeout(()=>{
    try{
        const result = onResolved(self.data)
        if (result instanceof Promise){
            // 2. 如果回调函数返回的是promise,return的promise的结果就是这个promise的结果
            result.then(
                value => {resolve(value)},
                reason => {reject(reason)}
            )
        } else {
            // 1. 如果回调函数返回的不是promise,return的promise的状态是resolved,value就是返回的值。
            resolve(result)
        }
    }catch (e) {
      //  3.如果执行onResolved的时候抛出错误,则返回的promise的状态为rejected
        reject(e)
    }
})

异曲同工之妙的是当status === 'rejected',道理一样

setTimeout(()=>{
      try{
          const result = onRejected(self.data)
          if (result instanceof Promise){
              // 2. 如果回调函数返回的是promise,return的promise的结果就是这个promise的结果
              result.then(
                  value => {resolve(value)},
                  reason => {reject(reason)}
              )
          } else {
              // 1. 如果回调函数返回的不是promise,return的promise的状态是resolved,value就是返回的值。
              resolve(result)
          }
      }catch (e) {
          //  3.如果执行onResolved的时候抛出错误,则返回的promise的状态为rejected
          reject(e)
      }
``  })

到这里,我们发现当执行resolve的时候,onResolved(self.data)onRejected(self.data)执行时也会跟上面一样的结果,可以说执行回调函数都要做以上判断,因此我们要将

self.callbacks.push({
    onResolved(){onResolved(self.data)},
    onRejected(){onRejected(self.data)}
})

改成

if(self.status === 'pending'){
// promise当前状态还是pending状态,将回调函数保存起来
self.callbacks.push({
    onResolved(){
        try{
            const result = onResolved(self.data)
            if (result instanceof Promise){
                // 2. 如果回调函数返回的是promise,return的promise的结果就是这个promise的结果
                result.then(
                    value => {resolve(value)},
                    reason => {reject(reason)}
                )
            } else {
                // 1. 如果回调函数返回的不是promise,return的promise的状态是resolved,value就是返回的值。
                resolve(result)
            }
        }catch (e) {
            //  3.如果执行onResolved的时候抛出错误,则返回的promise的状态为rejected
            reject(e)
        }
    },

到此,我们发现,相同的代码太多了,因此有必要封装一下

 function handle(callback) {
    try{
        const result = callback(self.data)
        if (result instanceof Promise){
            // 2. 如果回调函数返回的是promise,return的promise的结果就是这个promise的结果
            result.then(
                value => {resolve(value)},
                reason => {reject(reason)}
            )
        } else {
            // 1. 如果回调函数返回的不是promise,return的promise的状态是resolved,value就是返回的值。
            resolve(result)
        }
    }catch (e) {
        //  3.如果执行onResolved的时候抛出错误,则返回的promise的状态为rejected
        reject(e)
    }
}

这样以来就清爽了很多

Promise.prototype.then = function(onResolved,onRejected){

    var self = this

    return new Promise((resolve,reject)=>{
       /*
        调用指定回调函数的处理,根据执行结果。改变return的promise状态
         */
        function handle(callback) {
            try{
                const result = callback(self.data)
                if (result instanceof Promise){
                    // 2. 如果回调函数返回的是promise,return的promise的结果就是这个promise的结果
                    result.then(
                        value => {resolve(value)},
                        reason => {reject(reason)}
                    )
                } else {
                    // 1. 如果回调函数返回的不是promise,return的promise的状态是resolved,value就是返回的值。
                    resolve(result)
                }
            }catch (e) {
                //  3.如果执行onResolved的时候抛出错误,则返回的promise的状态为rejected
                reject(e)
            }
        }
        if(self.status === 'pending'){
            // promise当前状态还是pending状态,将回调函数保存起来
            self.callbacks.push({
                onResolved(){
                    handle(onResolved)
                },
                onRejected(){
                    handle(onRejected)
                }
            })
        }else if(self.status === 'resolved'){
            setTimeout(()=>{
                handle(onResolved)
            })
        }else{ // 当status === 'rejected'
            setTimeout(()=>{
                handle(onRejected)
            })
        }
    })

}

另外,我们还知道,promise会发生值传透,例如

let promsie = new Promise((resolve,reject)=>{
    resolve(1)
})
promsie
  .then(2)
  .then(3)
  .then(value =>console.log(value))

运行结果:1

解释:.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。值传透可以理解为,当传入then的不是函数的时候,这个then是无效的。而实际原理上其实是当then中传入的不算函数,则这个then返回的promise的data,将会保存上一个的promise.data。这就是发生值穿透的原因。而且每一个无效的then所返回的promise的状态都为resolved。

因此,要实现直传透这个特性,我们可以这样实现

添加这两句来判断要不要发生值传透

onResolved = typeof onResolved === 'function'? onResolved: value => value
onRejected = typeof onRejected === 'function'? onRejected: reason => {throw reason}

实际上就是改写,如果传入的不是函数,那就忽略那个传入值,自己再写一个函数。这个函数的执行结果将返回上一个promise的data

Promise.prototype.then = function(onResolved,onRejected){
    onResolved = typeof onResolved === 'function'? onResolved: value => value
    onRejected = typeof onRejected === 'function'? onRejected: reason => {throw reason}
    var self = this

    return new Promise((resolve,reject)=>{

        /*
        调用指定回调函数的处理,根据执行结果。改变return的promise状态
         */
        function handle(callback) {
            try{
                const result = callback(self.data)
                if (result instanceof Promise){
                    // 2. 如果回调函数返回的是promise,return的promise的结果就是这个promise的结果
                    result.then(
                        value => {resolve(value)},
                        reason => {reject(reason)}
                    )
                } else {
                    // 1. 如果回调函数返回的不是promise,return的promise的状态是resolved,value就是返回的值。
                    resolve(result)
                }
            }catch (e) {
                //  3.如果执行onResolved的时候抛出错误,则返回的promise的状态为rejected
                reject(e)
            }
        }
        if(self.status === 'pending'){
            // promise当前状态还是pending状态,将回调函数保存起来
            self.callbacks.push({
                onResolved(){
                    handle(onResolved)
                },
                onRejected(){
                    handle(onRejected)
                }
            })
        }else if(self.status === 'resolved'){
            setTimeout(()=>{
                handle(onResolved)
            })
        }else{ // 当status === 'rejected'
            setTimeout(()=>{
                handle(onRejected)
            })
        }
    })

}

3.实现catch方法

catch方法的作用跟then里的第二歌回调函数一样,因此我们可以这样来实现

Promise.prototype.catch = function(onRejected){
    return this.then(undefined,onRejected)
}

我的天啊,居然这么简单

4. 实现Promise.resolve

我们都知道,Promise.resolve方法可以传三种值

  1. 不是promise
  2. 成功状态的promise
  3. 失败状态的promise
    Promise.resolve(1)
    Promise.resolve(Promise.resolve(1))
    Promise.resolve(Promise.reject(1))

实际上跟实现上面的then时有点像

Promise.resolve = function(value){
  return new Promise((resolve,reject)=>{
      if (value instanceof Promise){
          // 如果value 是promise
          value.then(
              value => {resolve(value)},
              reason => {reject(reason)}
          )
      } else{
          // 如果value不是promise
          resolve(value)
      }

  }

}

5.实现Promise.reject

实现这个比较简单,返回一个状态为rejected的promise就好了

/*
Promise函数对象的reject方法
返回一个指定reason的失败状态的promise对象
*/
Promise.reject = function(reason){
    return new Promise((resolve,reject)=>{
        reject(reason)
    })
}

6.实现Promise.all

我们知道,这个方法会返回一个promise

/*
Promise函数对象的all方法
返回一个promise对象,只有当所有promise都成功时返回的promise状态才成功
*/
Promise.all = function(promises){
    return new Promise((resolve,reject)=>{

    })
}

而这个promise的状态由遍历每个promise产生的结果决定

/*
Promise函数对象的all方法
返回一个promise对象,只有当所有promise都成功时返回的promise状态才成功
*/
Promise.all = function(promises){
    return new Promise((resolve,reject)=>{
        // 遍历promises,获取每个promise的结果
        promises.forEach((p,index)=>{

        })
    })
}

有两种结果:

  1. 遍历到有一个promise是reject状态,则直接返回的promise状态为rejected
Promise.all = function(promises){
    return new Promise((resolve,reject)=>{
        // 遍历promises,获取每个promise的结果
        promises.forEach((p,index)=>{
            p.then(
                value => {

                },
                reason => { //只要有一个失败,return的promise状态就为reject
                    reject(reason)
                }
            )
        })
    })
}
  1. 遍历所有的promise的状态都为resolved,则返回的promise状态为resolved,并且还要每个promise产生的值传递下去
Promise.all = function(promises){
  const values = new Array(promises.length)
  var resolvedCount = 0 //计状态为resolved的promise的数量
  return new Promise((resolve,reject)=>{
      // 遍历promises,获取每个promise的结果
      promises.forEach((p,index)=>{
          p.then(
              value => {
                  // p状态为resolved,将值保存起来
                  values[index] = value
                  resolvedCount++;
                  // 如果全部p都为resolved状态,return的promise状态为resolved
                  if(resolvedCount === promises.length){
                      resolve(values)
                  }
              },
              reason => { //只要有一个失败,return的promise状态就为reject
                  reject(reason)
              }
          )
      })
  })
}

好像可以了,当其实这里还有一个问题,就是all传进去的数组不一定都是promise对象,可能是这样的

all([p,2,3,p])

因此需要把不是promise的数字包装成promise

Promise.all = function(promises){
    const values = new Array(promises.length)
    var resolvedCount = 0 //计状态为resolved的promise的数量
    return new Promise((resolve,reject)=>{
        // 遍历promises,获取每个promise的结果
        promises.forEach((p,index)=>{
            Promise.resolve(p).then(
                value => {
                    // p状态为resolved,将值保存起来
                    values[index] = value
                    resolvedCount++;
                    // 如果全部p都为resolved状态,return的promise状态为resolved
                    if(resolvedCount === promises.length){
                        resolve(values)
                    }
                },
                reason => { //只要有一个失败,return的promise状态就为reject
                    reject(reason)
                }
            )
        })
    })
}

7.实现Promise.race

这个方法的实现要比all简单很多

/*
Promise函数对象的race方法
返回一个promise对象,状态由第一个完成的promise决定
*/
Promise.race = function(promises){
    return new Promise((resolve,reject)=>{
        // 遍历promises,获取每个promise的结果
        promises.forEach((p,index)=>{
            Promise.resolve(p).then(
                value => {
                    // 只要有一个成功,返回的promise的状态九尾resolved
                    resolve(value)

                },
                reason => { //只要有一个失败,return的promise状态就为reject
                    reject(reason)
                }
            )
        })
    })
}

有什么不理解或建议,欢迎下方评论。项目源码已经放到github:https://github.com/Sunny-lucking/howToBuildMyPromise 顺手点个star呗

去做一下promise相关题,看看是不是真的掌握了呢?

之前可能会出现有些题不会做,而当你阅读完这篇文章后,再去做,发现易如反掌。

【深入Node探究】(1)“Node特点与应用场景” 有四问

1、为什么叫Node?

它自身非常简单,通过通信协议来组织很多Node,非常容易通过扩展来达成构建大型网络应用的目的。每一个Node进程都构成这个网络应用中的一个节点,这是它名字所含意义的真谛。

2、你能说说Node的特点吗?

作为后端JavaScript的运行平台,Node保留了前端浏览器JavaScript中那些熟悉的接口,没有改写语言本身的任何特性,依旧基于作用域和原型链,区别在于它将前端中广泛运用的**迁移到了服务器端。下面我们可以看看node相较于其他语言的一些特点:

1. 异步I/O

关于异步I/O,向前端工程师解释起来或许会容易一些,因为发起Ajax调用对于前端工程师而言是再熟悉不过的场景了。下面的代码用于发起一个Ajax请求:

$.post('/url', {title: ’深入浅出Node.js'}, function (data) {
    console.log(’收到响应’);
  });
  console.log(’发送Ajax结束’);

熟悉异步的用户必然知道,“收到响应”是在“发送Ajax结束”之后输出的。在调用$.post()后,后续代码是被立即执行的,而“收到响应”的执行时间是不被预期的。我们只知道它将在这个异步请求结束后执行,但并不知道具体的时间点。异步调用中对于结果值的捕获是符合“Don't call me, I will call you”的原则的,这也是注重结果,不关心过程的一种表现。

2. 事件与回调函数

Node不像Rhino那样受Java的影响很大,而是将前端浏览器中应用广泛且成熟的事件引入后端,配合异步I/O,将事件点暴露给业务逻辑。

相比之下,无论在前端还是后端,事件都是常用的。对于其他语言来说,这种俯拾皆是JavaScript的熟悉感觉是基本不会出现的

3. 单线程

Node保持了JavaScript在浏览器中单线程的特点。而且在Node中,JavaScript与其余线程是无法共享任何状态的。

单线程的最大好处是不用像多线程编程那样处处在意状态的同步问题,这里没有死锁的存在,也没有线程上下文交换所带来的性能上的开销。

同样,单线程也有它自身的弱点,这些弱点是学习Node的过程中必须要面对的。积极面对这些弱点,可以享受到Node带来的好处,也能避免潜在的问题,使其得以高效利用。单线程的弱点具体有以下3方面。

  • ❑ 无法利用多核CPU。
  • ❑ 错误会引起整个应用退出,应用的健壮性值得考验。
  • ❑ 大量计算占用CPU导致无法继续调用异步I/O。

像浏览器中JavaScript与UI共用一个线程一样,JavaScript长时间执行会导致UI的渲染和响应被中断。在Node中,长时间的CPU占用也会导致后续的异步I/O发不出调用,已完成的异步I/O的回调函数也会得不到及时执行。

3. 上面提到单线程不利于计算,无法利用多核cpu,难道没有解决方法吗?

有的。

Node采用了与Web Workers相同的思路来解决单线程中大计算量的问题child_process

子进程的出现,意味着Node可以从容地应对单线程在健壮性和无法利用多核CPU方面的问题。通过将计算分发到各个子进程,可以将大量计算分解掉,然后再通过进程之间的事件消息来传递结果,这可以很好地保持应用模型的简单和低依赖。

4. 那你可以谈谈node的使用场景吗

关于Node,探讨得较多的主要有I/O密集型CPU密集型

I/O密集型

在Node的推广过程中,无数次有人问起Node的应用场景是什么。如果将所有的脚本语言拿到一处来评判,那么从单线程的角度来说,Node处理I/O的能力是值得竖起拇指称赞的。通常,说Node擅长I/O密集型的应用场景基本上是没人反对的。Node面向网络且擅长并行I/O,能够有效地组织起更多的硬件资源,从而提供更多好的服务。

I/O密集的优势主要在于Node利用事件循环的处理能力,而不是启动每一个线程为每一个请求服务,资源占用极少。

CPU密集型

换一个角度,在CPU密集的应用场景中,Node是否能胜任呢?实际上,V8的执行效率是十分高的。单以执行效率来做评判,V8的执行效率是毋庸置疑的。

CPU密集型应用给Node带来的挑战主要是:由于JavaScript单线程的原因,如果有长时间运行的计算(比如大循环),将会导致CPU时间片不能释放,使得后续I/O无法发起。但是适当调整和分解大型运算任务为多个小任务,使得运算能够适时释放,不阻塞I/O调用的发起,这样既可同时享受到并行异步I/O的好处,又能充分利用CPU。

CPU密集不可怕,如何合理调度是诀窍。

最后

文章首发于 公众号《前端阳光》,欢迎来和小伙伴们一起交流技术吧

JS设计**篇(读浏览器核心原理)

一、V8是如何执⾏⼀段JavaScript代码的?

1.V8是怎么执⾏JavaScript代码的呢

其主要核⼼流程分为编译和执⾏两步。⾸先需要将JavaScript代码转换为低级中间代码或者机器能够理解的 机器代码,然后再执⾏转换后的代码并输出执⾏结果。

2.什么是解释执行

解释执⾏,需要先将输⼊的源代码通过解析器编译成中间代码,之后直接使⽤解释器解释执⾏中间 代码,然后直接输出结果。具体流程如下图所⽰:

3.什么是编译执行

编译执⾏。采⽤这种⽅式时,也需要先将源代码转换为中间代码,然后我们的编译器再将中间代码 编译成机器代码。通常编译成的机器代码是以⼆进制⽂件形式存储的,需要执⾏这段程序的时候直接执⾏⼆ 进制⽂件就可以了。还可以使⽤虚拟机将编译后的机器代码保存在内存中,然后直接执⾏内存中的⼆进制代 码。

4.V8作为JavaScript的虚拟机的⼀种,?是解释执⾏,还是编译执⾏呢?

实际上,V8并没有采⽤某种单⼀的技术,⽽是混合编译执⾏和解释执⾏这两种⼿段,我们把这种混合使⽤ 编译器和解释器的技术称为JIT(Just?In?Time)技术。这是⼀种权衡策略,因为这两种⽅法都各⾃有⾃的优缺点,解释执⾏的启动速度快,但是执⾏时的速度慢, ⽽编译执⾏的启动速度慢,但是执⾏时的速度快。你可以参看下⾯完整的V8执⾏JavaScript的流程图:

相信你注意到了,我们在解释器附近画了个监控机器⼈,这是⼀个监控解释器执⾏状态的模块,在解释执⾏ 字节码的过程中,如果发现了某⼀段代码会被重复多次执⾏,那么监控机器⼈就会将这段代码标记为热点代 码。当某段代码被标记为热点代码后,V8就会将这段字节码丢给优化编译器,优化编译器会在后台将字节码编 译为⼆进制代码,然后再对编译后的⼆进制代码执⾏优化操作,优化后的⼆进制机器代码的执⾏效率会得到 ⼤幅提升。如果下⾯再执⾏到这段代码时,那么V8会优先选择优化之后的⼆进制代码,这样代码的执⾏速 度就会⼤幅提升。

理解了这⼀点,我们就可以来深⼊分析V8执⾏⼀段JavaScript代码所经历的主要流程了,这包括了:

  • 初始化基础环境;

  • 解析源码⽣成AST和作⽤域;

  • 依据AST和作⽤域⽣成字节码;

  • 解释执⾏字节码;

  • 监听热点代码;

  • 优化热点代码为⼆进制的机器代码;

  • 反优化⽣成的⼆进制机器代码

二、函数即对象,函数的特点

1.js是一门面向对象的语言吗?

不是的,js是基于对象设计的,但不是面向对象的语言

2.js与面向对象语言在继承上有什么区别?

⾯向对象语⾔是由语⾔本⾝对继承做了充分的⽀持,并提供了⼤量的关键字,如public、protected、friend、interface等,众多的关键字使得⾯向对象语⾔的继承变得异常繁琐和复杂,「⽽JavaScript中实现继承的⽅式却⾮常简单清爽, 只是在对象中添加了⼀个称为原型的属性,把继承的对象通过原型链接起来,就实现了继承,我们把这种继承⽅式称为基于原型链继承

3.V8内部是怎么实现函数可调⽤特性的呢?

在V8内部,我们会为函数对象添加了两个隐藏属性,具体属性如下图所⽰:

也就是说,函数除了可以拥有常⽤类型的属性值之外,还拥有两个隐藏属性,分别是name属性和code属 性。隐藏name属性的值就是函数名称,如果某个函数没有设置函数名,如下⾯这段函数:

(function (){
var test = 1
console.log(test)
})()

该函数对象的默认的name属性值就是anonymous,表⽰该函数对象没有被设置名称。另外⼀个隐藏属性是 code属性,其值表⽰函数代码,以字符串的形式存储在内存中。当执⾏到⼀个函数调⽤语句时,V8便会从 函数对象中取出code属性值,也就是函数代码,然后再解释执⾏这段函数代码

4.function在JavaScript中是一等公民 ,何为一等公民?

一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量

5.什么是闭包?

将外部变量和和函数绑定起来的技术称为闭包

function foo(){
  var number = 1
  function bar(){
    number++
    console.log(number)
  }
   return bar
}
var mybar = foo()
mybar()

观察上段代码可以看到,我们在foo函数中定义了⼀个新的bar函数,并且bar函数引⽤了foo函数中的变量 number,当调⽤foo函数的时候,它会返回bar函数。

三、快属性和慢属性:V8采⽤了哪些策略提升了对象属性的访问速度?

1.什么是对象中的 常规属性和 排序属性

function Foo() {
  this[100] = 'test-100'
  this[1] = 'test-1'
  this["B"] = 'bar-B'
  this[50] = 'test-50'
  this[9] = 'test-9'
  this[8] = 'test-8'
  this[3] = 'test-3'
  this[5] = 'test-5'
  this["A"] = 'bar-A'
  this["C"] = 'bar-C'
}
var bar = new Foo()
for(key in bar){
  console.log(`index:${key} value:${bar[key]}`)
}

在上⾯这段代码中,我们利⽤构造函数Foo创建了⼀个bar对象,在构造函数中,我们给bar对象设置了很多 属性,包括了数字属性和字符串属性,然后我们枚举出来了bar对象中所有的属性,并将其⼀⼀打印出来, 下⾯就是执⾏这段代码所打印出来的结果

index:1 value:test-1
index:3 value:test-3
index:5 value:test-5
index:8 value:test-8
index:9 value:test-9
index:50 value:test-50
index:100 value:test-100
index:B value:bar-B
index:A value:bar-A
index:C value:bar-C

观察这段打印出来的数据,我们发现打印出来的属性顺序并不是我们设置的顺序,我们设置属性的时候是乱 序设置的,⽐如开始先设置100,然后有设置了1,但是输出的内容却⾮常规律,总的来说体现在以下两 点:

  • 设置的数字属性被最先打印出来了,并且按照数字⼤⼩的顺序打印的;

  • 设置的字符串属性依然是按照之前的设置顺序打印的,⽐如我们是按照B、A、C的顺序设置的,打印出来,依然是这个顺序。

之所以出现这样的结果,是因为在ECMAScript规范中定义了 「数字属性应该按照索引值⼤⼩升序排列,字符 串属性根据创建时的顺序升序排列。」在这⾥我们把对象中的数字属性称为 「排序属性」,在V8中被称为 elements,字符串属性就被称为 「常规属性」, 在V8中被称为 properties。在V8内部,为了有效地提升存储和访问这两种属性的性能,分别使⽤了两个 线性数据结构来分别保存排序 属性和常规属性,具体结构如下图所⽰:

在elements对象中,会按照顺序存放排序属性,properties属性则指向了properties对 象,在properties对象中,会按照创建时的顺序保存了常规属性。

2.什么是对象内属性

将不同的属性分别保存到elements属性和properties属性中,⽆疑简化了程序的复杂度,但是在查找元素 时,却多了⼀步操作,⽐如执⾏?bar.B这个语句来查找B的属性值,那么在V8会先查找出properties属性所 指向的对象properties,然后再在properties对象中查找B属性,这种⽅式在查找过程中增加了⼀步操作, 因此会影响到元素的查找效率。基于这个原因,V8采取了⼀个权衡的策略以加快查找属性的效率,这个策略是将部分常规属性直接存储到 对象本⾝,我们把这称为 「对象内属性(in-object?properties)」。对象在内存中的展现形式你可以参看下图:

采⽤对象内属性之后,常规属性就被保存到bar对象本⾝了,这样当再次使⽤bar.B来查找B的属性值时, V8就可以直接从bar对象本⾝去获取该值就可以了,这种⽅式减少查找属性值的步骤,增加了查找效率。不过对象内属性的数量是固定的,默认是10个,如果添加的属性超出了对象分配的空间,则它们将被保存在 常规属性存储中。虽然属性存储多了⼀层间接层,但可以⾃由地扩容。

3.什么是快属性和慢属性

通常,我们将保存在线性数据结构中的属性称之为“快属性”,因为线性数据结构中只需要通过索引即可以 访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除⼤量的属性时,则执⾏效率 会⾮常低,这主要因为会产⽣⼤量时间和内存开销。因此,如果⼀个对象的属性过多时,V8为就会采取另外⼀种存储策略,那就是“慢属性”策略,但慢属性 的对象内部会有独⽴的⾮线性数据结构(词典)作为属性存储容器。所有的属性元信息不再是线性存储的,⽽ 是直接保存在属性字典中。

四、函数表达式:涉及⼤量概念,函数表达式到底该怎么学?

1.函数声明与函数表达式的差异

同样是在定义的函数之前调⽤函数,第⼀段代码就可以正确执⾏,⽽第⼆段代码却报错,这是为什么呢?其主要原因是这两种定义函数的⽅式具有不同语义,不同的语义触发了不同的⾏为。

因为语义不同,所以我们给这两种定义函数的⽅式使⽤了不同的名称,第⼀种称之为 「函数声明」,第⼆种称之 为「函数表达式

2.V8是怎么处理函数声明的?

V8在执⾏JavaScript的过程中,会先对其进⾏编译,然后再执⾏,⽐如下⾯这段代码:

var x = 5
function foo(){
  console.log('Foo')
}

V8执⾏这段代码的流程⼤致如下图所⽰:

在编译阶段,如果解析到函数声明,那么V8会将这个函数声明转换为内存中的函数对象(「函数名放在栈,函数体放在堆」),并将其放到作⽤ 域中。同样,如果解析到了某个变量声明,也会将其放到作⽤域中,但是会将其值设置为undefined,表⽰ 该变量还未被使⽤。

然后在V8执⾏阶段,如果使⽤了某个变量,或者调⽤了某个函数,那么V8便会去作⽤域查找相关内容。

3.什么是变量提升?

因为在执⾏之前,这些变量都被提升到作⽤域中了,所以在执⾏阶段,V8当然就能获取到所有的定义变量 了。我们把这种在编译阶段,将所有的变量提升到作⽤域的过程称为「变量提升

4.表达式和语句的区别是什么

简单地理解,表达式就是表⽰值的式⼦,⽽语句是操作值的式⼦。

⽐如:

x = 5

就是表达式,因为执⾏这段代码,它会返回⼀个值。同样,6 === 5?也是⼀个表达式,因为它会返回 False。

⽽语句则不同了,⽐如你定义了⼀个变量:

var x;

这就是⼀个语句,执⾏该语句时,V8并不会返回任何值给你。同样,当我声明了⼀个函数时,这个函数声明也是⼀个语句,⽐如下⾯这段函数声明:

function foo(){
  return 1
}

当执⾏到这段代码时,V8并没有返回任何的值,它只是解析foo函数,并将函数对象存储到内存中。

这么一来就说明了,语句的执行是 编译阶段,把变量放到作用域,导致变量提升,表达式的执行是在执行阶段,导致作用域中的变量的值的改变。」

5.函数声明是表达式还是语句呢?

function foo(){
  console.log('Foo')
}

执⾏上⾯这段代码,它并没有输出任何内容,所以可以肯定,函数声明并不是⼀个表达式,⽽是⼀个语句。

总的来说,在V8解析JavaScript源码的过程中,如果遇到普通的变量声明,那么便会将其提升到作⽤域中, 并给该变量赋值为undefined,如果遇到的是函数声明,那么V8会在内存中为声明⽣成函数对象,并将该对 象提升到作⽤域中。

我觉得最有意思的是下面这道题

6.为什么⽴即调⽤的函数表达式(IIFE)可以拥有私有作用域

JavaScript中有⼀个圆括号运算符,圆括号⾥⾯可以放⼀个表达式,⽐如下⾯的代码:

(a=3)

括号⾥⾯是⼀个表达式,整个语句也是⼀个表达式,最终输出3。如果在⼩括号⾥⾯放上⼀段函数的定义,如下所⽰:

(function () {
  //statements
})

因为⼩括号之间存放的必须是表达式,所以如果在⼩阔号⾥⾯定义⼀个函数,那么V8就会把这个函数看成 是函数表达式,执⾏时它会返回⼀个函数对象

存放在括号⾥⾯的函数便是⼀个函数表达式,它会返回⼀个函数对象,如果我直接在表达式后⾯加上调⽤的 括号,这就称 ⽴即调⽤函数表达式(IIFE),⽐如下⾯代码:

(function () {
  //statements
})()

因为函数⽴即表达式也是⼀个表达式,所以V8在编译阶段,并不会为该表达式创建函数对象。这样的⼀个 好处就是不会污染环境,函数和函数内部的变量都不会被其他部分的代码访问到。

7. 变量a的值是什么

var a = (function () {
  return 1
})()

因为函数⽴即表达式是⽴即执⾏的,所以将⼀个函数⽴即表达式赋给⼀个变量时,不是存储?IIFE?本 ⾝,⽽是存储?IIFE?执⾏后返回的结果,所以a=1。

五、作⽤域链:V8是如何查找变量的?

1.全局作⽤域和函数作⽤域

全局作⽤域和函数作⽤域类似,也是存放变量和函数的地⽅,但是它们还是有点不⼀样:? 「全局作⽤域是在 V8启动过程中就创建了,且⼀直保存在内存中不会被销毁的,直⾄V8退出。? ⽽函数作⽤域是在执⾏该函数 时创建的,当函数执⾏结束之后,函数作⽤域就随之被销毁掉了。」

2.什么是词法作⽤域

因为JavaScript是基于词法作⽤域的,词法作⽤域就是指,查找作⽤域的顺序是按照函数定义时的位置来决 定的。bar和foo函数的外部代码都是全局代码,所以⽆论你是在bar函数中查找变量,还是在foo函数中查找 变量,其查找顺序都是按照当前函数作⽤域‒>全局作⽤域这个路径来的。

由于我们代码中的foo函数和bar函数都是在全局下⾯定义的,所以在foo函数中使⽤了type,最终打印出来 的值就是全局作⽤域中的type。

3.什么是动态作用域和静态作用域

因为词法作⽤域是根据函数在代码中的位置来确定的,作⽤域是在声明函数时就确 定好的了,所以我们也将词法作⽤域称为静态作⽤域。和静态作⽤域相对的是动态作⽤域,动态作⽤域并不关⼼函数和作⽤域是如何声明以及在何处声明的,只关 ⼼它们从 何处调⽤。换句话说,作⽤域链是基于调⽤栈的,⽽不是基于函数定义的位置的。

六.类型转换:V8是怎么实现1-“2”的?

1.V8是怎么实现1-“2”的

V8会提供了⼀个ToPrimitve⽅法,其作⽤是将a和b转换为原⽣数据类型,其转换流程如下:

  • 先检测该对象中是否存在valueOf⽅法,如果有并返回了原始类型,那么就使⽤该值进⾏强制类型转换;

  • 如果valueOf没有返回原始类型,那么就使⽤toString⽅法的返回值;

  • 如果vauleOf和toString两个⽅法都不返回基本类型值,便会触发⼀个TypeError的错误。

将对象转换为原⽣类型的流程图如下所⽰:

当V8执⾏1+“2”时,因为这是两个原始值相加,原始值相加的时候,如果其中⼀项是字符串,那么V8会默 认将另外⼀个值也转换为字符串,相当于执⾏了下⾯的操作:

Number(1).toString() + "2"

注意」:上面valueOf和toString的调用顺序仅适用于 运算。其他情况可以参考8. {} 和 [] 的 valueOf 和 toString 的结果是什么?.

image

快用上PerformanceObserver获取首屏时间,别再手动计算首屏时间了


theme: smartblue

大家好,我是阳光,今天给大家介绍一个非常好用的浏览器api: PerformanceObserver
我们可以用它来获取首屏、白屏的时间,就不用再麻烦地手动去计算了。

介绍

PerformanceObserver 可用于获取性能相关的数据,例如首帧fp首屏fcp首次有意义的绘制 fmp等等。

构造函数

PerformanceObserver()
创建并返回一个新的 PerformanceObserver 对象。

提供的方法

PerformanceObserver.observe()

当记录的性能指标在指定的 entryTypes 之中时,将调用性能观察器的回调函数。

PerformanceObserver.disconnect()

停止性能观察者回调接收到性能指标。

PerformanceObserver.takeRecords()

返回存储在性能观察器中的性能指标的列表,并将其清空。

重点我们看看observer.observe(options);

options

一个只装了单个键值对的对象,该键值对的键名规定为 entryTypes。entryTypes 的取值要求如下:

entryTypes 的值:一个放字符串的数组,字符串的有效值取值在性能条目类型 中有详细列出。如果其中的某个字符串取的值无效,浏览器会自动忽略它。

另:若未传入 options 实参,或传入的 options 实参为空数组,会抛出 TypeError。

实例

<script>
	const observer = new PerformanceObserver((list) => {
		for(const entry of list.getEntries()){
			console.groupCollapsed(entry.name);
			console.log(entry.entryType);
			console.log(entry.startTime);
			console.log(entry.duration);
			console.groupEnd(entry.name);
		}
	})	
	observer.observe({entryTypes:['longtask','frame','navigation','resource','mark','measure','paint']});
</script>

获取结果

根据打印结果我们可以推测出来:

entryTypes里的值其实就是我们告诉PerformanceObserver,我们想要获取的某一方面的性能值。例如传入paint,就是说我们想要得到fcp和fp。

所以我们看打印,它打印出来了fp和fcp

这里有必要解释一下什么是fp,fcp,fpm

TTFB:Time To First Byte,首字节时间
FP:First Paint,首次绘制,绘制Body
FCP:First Contentful Paint,首次有内容的绘制,第一个dom元素绘制完成
FMP:First Meaningful Paint,首次有意义的绘制
TTI:Time To Interactive,可交互时间,整个内容渲染完成

不懂?看图!

FP仅有一个div根节点
FCP包含页面的基本框架,但没有数据内容
FMP包含页面的所有元素及数据

Wow!恍然大悟!

实际使用

好了,我们在实际项目中怎么取获取呢?可以看看我的实现参考一下下:

  // 使用 PerformanceObserver 监听 fcp
  if (!!PerformanceObserver){
    try {
      const type = 'paint';
      if ((PerformanceObserver.supportedEntryTypes || []).includes(type)) {
        observer = new PerformanceObserver((entryList)=>{
          for(const entry of entryList.getEntriesByName('first-contentful-paint')){
            const { startTime } = entry;
            console.log('[assets-load-monitor] PerformanceObserver fcp:', startTime);
            
            // 上报startTime操作
          }
        });
        observer.observe({
          entryTypes: [type],
        });
        return;
      }
    } catch (e) {
      // ios 不支持这种entryTypes,会报错 https://caniuse.com/?search=PerformancePaintTiming
      console.warn('[assets-load-monitor] PerformanceObserver error:', (e || {}).message ? e.message : e);
    }
  }

这里用了判断是否可以使用PerformanceObserver,不能使用的话,我们是用其他方法的,例如MutationObserver,这个我们我们后面再讲。

参考文章:

https://blog.csdn.net/weixin_40970987/article/details/108121988
https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver/observe

手写flexible.js的原理实现,我终于明白移动端多端适配

前言

今天在看阿里的面试题时,看到这样一道面试题,问flexible.js的原理是什么?

然而我也不知道,但是刚好我又在我公司的项目上遇到过,于是研究一番,遂作此文。

核心原理

简单的一句概括就是: flexible.js帮我们计算出1rem 等于多少px

怎么计算的?

很简单,就是1rem = 屏幕宽度的1/10

var docEl = document.documentElement  // 返回文档的root元素,即html
var rem = docEl.clientWidth / 10
docEl.style.fontSize = rem + 'px'

我们知道rem的大小是根据html节点的font-size的相对值

例如,iphone 6的屏幕宽度为375px,因此1rem === 37.5px。

计算rem干嘛?

那帮我们计算出rem的值有什么鬼用吗?

确实,如果只是单纯的计算出rem的值并没什么用。发挥它的用处是当我们根据设计稿来转化成页面时需要用到。

举个例子,现在有两个手机,一个手机的屏幕宽度是375px,一个是750px,设计稿给我们的宽度是375px,那我们按照设计稿的设计在375px的手机上刚好完美匹配,但是却会发现在750px的手机上页面只有一半,空白了一半。

这就是我们需要解决的问题,即怎么解决移动端尺寸众多的问题,我们的设计稿是固定,怎么办,如果设计稿是弹性的可以随意缩放该多好。

好吧,设计只给一张设计稿,我们只能想其他方法啦。

等比画饼

想想,有办法了,就像本来你在一张大的纸上面了一饼,现在让你在小的纸上在画一次要怎么画,就是所有东西都等比例画小,如果要画到更大的纸上也是一个道理,等比画大,对不对。

现在我们把设计稿分成10等份,设计稿 A = W/10,我们把设备可视区域也就是我们的各种移动端设备的这个画布也分成10份,并赋值给根元素的fontSize,我们都知道rem是根据根元素字体大小计算的,所以我们的1rem也就是设备可视区域/10,现在设计稿上有一块区域宽B,那它是不是等比放到设备可视区域的宽度为 B/A rem。

再啰嗦一下,B在设计稿上占B/A份,那在设备可视区域上也要占B/A份对不对,所以宽是B/A rem。这就是flexible.js能实现设备兼容的原理。下面看代码

// 首先是一个立即执行函数,执行时传入的参数是window和document
(function flexible (window, document) {
  var docEl = document.documentElement  // 返回文档的root元素
  var dpr = window.devicePixelRatio || 1 // 获取设备的dpr,即当前设置下物理像素与虚拟像素的比值

  // adjust body font size 设置默认字体大小,默认的字体大小继承自body
  function setBodyFontSize () {
    if (document.body) {
      document.body.style.fontSize = (12 * dpr) + 'px'
    }
    else {
      document.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
  }
  setBodyFontSize();

  // set 1rem = viewWidth / 10
  function setRemUnit () {
    var rem = docEl.clientWidth / 10
    docEl.style.fontSize = rem + 'px'
  }

  setRemUnit()

  // reset rem unit on page resize
  window.addEventListener('resize', setRemUnit)
  window.addEventListener('pageshow', function (e) {
    if (e.persisted) {
      setRemUnit()
    }
  })

  // detect 0.5px supports  检测是否支持0.5像素,解决1px在高清屏多像素问题,需要css的配合。
  if (dpr >= 2) {
    var fakeBody = document.createElement('body')
    var testElement = document.createElement('div')
    testElement.style.border = '.5px solid transparent'
    fakeBody.appendChild(testElement)
    docEl.appendChild(fakeBody)
    if (testElement.offsetHeight === 1) {
      docEl.classList.add('hairlines')
    }
    docEl.removeChild(fakeBody)
  }
}(window, document))

这就是flexible.js的源码,超级简单吧。

就这几行代码就有12k的star,要是我也早点发现这个方案就好了。那star就是我的了

现在已经实现了将屏幕分为10等份,也就是1rem。

将设计稿分成10等份

根据我们上面画饼的方案,现在也要把设计稿转化为10等分才行。

我看了下我们项目的实现是用到了postcss-pxtorem插件来实现的

因为设计稿给我们的是px单位的,所以我们在开发的时候只能写px,然后这就需要postcss-pxtorem来帮我们将我们写的px转化为rem了。

安装完postcss-pxtorem之后的配合非常简单,只要在.postcssrc.js文件配置如下就好了

module.exports = {
  plugins: {
    'postcss-pxtorem': {
      rootValue: 75,
    }
  }
}

rootValue:75 为啥是75呢,这是因为我们的设计稿的宽度是750px,十分之一就是75px

如果你们的设计稿是375px的,就需要将值改写成37.5

flexible.js升级版

我们公司的使用是在flexible.js的基础上进行了更改,主要是添加了这样一段代码

var metaEl = doc.querySelector('meta[name="viewport"]');
if (metaEl) {
  console.warn('将根据已有的meta标签来设置缩放比例');
  var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
  if (match) {
      scale = parseFloat(match[1]);
      dpr = parseInt(1 / scale);
  }
}

这一串主要是来实现iphone和安卓的设备像素比不一样的问题,例如iphone的一些手机

总结

就这么简单的两步就实现了移动端的适配。

相关参考

好文推荐

这是我的github,欢迎大家star:https://github.com/Sunny-lucking/blog

拜托,css这样实现多行文本“展开收起” 超酷的好吧

前言

2022.02.14的午后,我站在你家门口,再次遇见了你,他又来牵起你的手

无法言语,我是什么,这样傻傻的我怎么守护你

这次我静静哭了选择放弃,我好想好想把记忆折叠起

可惜,记忆不能像之前那个需求一样自由展开与折叠

前段时间接到一个需求,关于文字展开和收起的,走了很多路,踩了很多坑。

在这个夜深人静,想你想到泪流的时候,决定记录分享一下。

需求如下所述:

  1. 未满两行时

  2. 超过两行,少于7行时

未展开

展开

  1. 超过7行时

未展开

展开

就如上面所述,我倒是第一次做这种需求,于是就网上搜索下案例,然后就搜出下面这篇文章:

文章链接:https://juejin.cn/post/6963904955262435336#comment

点赞和评论都挺多的,于是就用他的方案来实现了。

但是后面发现有些问题,其实他的文章后面的评论区也有读者提出来了问题。

其实我觉得问题也不大,于是问哦下设计大佬

显然,就收到了拒绝。

被拒绝是十分正常不过的事情了,不过这比被发好人卡杀伤力少太多了,不信你听听:

"你真的挺好的,人也很优秀,但是配不上我"

这矫情的措辞结构

经历过的人会懂

可能是孤独让情绪变得脆弱,毫无头绪的我,开始寻求网友的帮助。

群里就有大佬提供了这个

https://codepen.io/xboxyan/pen/LYWpWzK

这个跟方法跟 上面介绍的那篇文章的方法差不多

不同点在于这个方法是利用div高度来限制文字显示的行数的。

上面文章里是利用-webkit-line-clamp来限制行数。

然后它的省略号也是在label按钮里模拟出来的。

上面文章里的方法的缺点上面已经说了,那么群里推荐的方法是否也能解决问题呢?

其实不行,本来在pc上看确实是没问题了,但是在安卓和ios看发现不太行。

发现在ios上限制7行的时候,显示除了7.5行,就是多了半行。或者 总有一个手机对不齐(我们要适配各种安卓机和低端ios)

虽说两个方法都有缺点,但是都有优点,于是结合两者的优点就进行了我的方案的实现。

我的方案

<div class="activity-desc-wrapper">
  <input
    type="checkbox"
    class="toggleInput"
    id="toggleInput"
    v-model="isUnFold"
  />
  <div class="activity-desc" ref="descBox" id="descBox">
    <label
      class="btn"
      for="toggleInput"
      v-if="isMoreThan2Line && (!isUnFold || isMoreThan7Line)"
      >{{ isUnFold ? '展开' : '收起' }}</label
    >
    概述文字概述文字概述文字概述文字概
    述文字概述文字概述文字概述文字概述文字概述文字
    <label
      class="btn-no-absolute"
      for="toggleInput"
      v-if="isUnFold && !isMoreThan7Line"
      >收起</label
    >
  </div>
</div>

首先,跟上面那篇文章里介绍的一样,用input来记录当前是展开还是收起状态,

不同点是我用了两个label按钮。

一个label是不用定位的,直接跟在文字的末尾。这种是作为文字超过两行,但是未超过七行,展开的状态。

前面的label则是绝对定位到文字盒子的末尾。作为 文字超过两行未展开,展开后文字超过七行的情况。

可以看下css的实现

.activity-desc-wrapper {
  display: flex;
  .toggleInput {
    display: none;
  }
  .toggleInput:checked + .activity-desc {
    -webkit-line-clamp: 7;
  }

  .activity-desc {
    padding: 0;
    position: relative;
    margin-top: 7px;
    font-size: 24px;
    font-weight: 400;
    color: #8a8f99;
    line-height: 1.2;

    display: -webkit-box;
    overflow: hidden;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;

    .btn {
      position: absolute;
      padding: 0 3px 0 7px;
      font-weight: 400;
      bottom: 0px;
      right: 0;
      line-height: 1.2;
      font-size: 24px;
      color: #939fe1;
      background: #f5f7fa;
      &::before {
        content: '...';
        color: #8a8f99;
        transform: translateX(-100%);
      }
    }
    .btn-no-absolute {
      float: none;
      font-size: 24px;
      font-weight: 400;
      color: #939fe1;
    }
  }
}

我的方案的另一个重点在于判断文字是否超过2行和七行,这个就在于获取文字的实际高度是多少,一开始以为是没办法获取的,只能获取到省略后的文字高度,在随便调试了一下之后,发现scrollHeight属性就可以获取到盒子的实际高度了,

mounted() {
  // 判断文案实际行数
  this.$nextTick(() => {
    const height = this.$refs.descBox.scrollHeight;
    const lineHeight = +window
      .getComputedStyle(this.$refs.descBox)
      .lineHeight.match(/\d+\.*\d+/g)[0];
    this.isMoreThan7Line = height / lineHeight > 7;
    this.isMoreThan2Line = height / lineHeight > 2;
  });
},

完美,收获了一帮小迷妹。

总结

  1. 跟在文字后面的按钮可以不设置定位
  2. 处于文字行数末尾的按钮可以设置绝对定位然后
  3. 行数的判断可以用scrollHeight属性
  4. 利用伪元素来模拟省略号...

回忆起从前,我的心总是默默的等候

你曾经说你 想找一个依靠

等了好几天 等你的留言 却发现是空白一片

站在镜子前 是不是我的样子有点丑

手写Vuex核心原理,再也不怕面试官问我Vuex原理

手写Vuex核心原理

@[toc]

一、核心原理

  1. Vuex本质是一个对象
  2. Vuex对象有两个属性,一个是install方法,一个是Store这个类
  3. install方法的作用是将store这个实例挂载到所有的组件上,注意是同一个store实例。
  4. Store这个类拥有commit,dispatch这些方法,Store类里将用户传入的state包装成data,作为new Vue的参数,从而实现了state 值的响应式。

二、基本准备工作

我们先利用vue-cli建一个项目

删除一些不必要的组建后项目目录暂时如下:

已经把项目放到 githubhttps://github.com/Sunny-lucking/howToBuildMyVuex 可以卑微地要个star吗。有什么不理解的或者是建议欢迎评论提出

我们主要看下App.vue,main.js,store/index.js

代码如下:

App.vue

<template>
  <div id="app">
    123
  </div>
</template>

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

现在我们启动一下项目。看看项目初始化有没有成功。

ok,没毛病,初始化成功。

现在我们决定创建自己的Vuex,于是创建myVuex.js文件

目前目录如下

再将Vuex引入 改成我们的myVuex

//store/index.js
import Vue from 'vue'
import Vuex from './myVuex' //修改代码

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

三、剖析Vuex本质

先抛出个问题,Vue项目中是怎么引入Vuex。

  1. 安装Vuex,再通过import Vuex from 'vuex'引入
  2. 先 var store = new Vuex.Store({...}),再把store作为参数的一个属性值,new Vue({store})
  3. 通过Vue.use(Vuex) 使得每个组件都可以拥有store实例

从这个引入过程我们可以发现什么?

  1. 我们是通过new Vuex.store({})获得一个store实例,也就是说,我们引入的Vuex中有Store这个类作为Vuex对象的一个属性。因为通过import引入的,实质上就是一个导出一个对象的引用

所以我们可以初步假设

Class Store{
  
}

let Vuex = {
  Store
}
  1. 我们还使用了Vue.use(),而Vue.use的一个原则就是执行对象的install这个方法

所以,我们可以再一步 假设Vuex有有install这个方法。

Class Store{
  
}
let install = function(){
  
}

let Vuex = {
  Store,
  install
}

到这里,你能大概地将Vuex写出来吗?

很简单,就是将上面的Vuex对象导出,如下就是myVuex.js

//myVuex.js
class Store{

}
let install = function(){

}

let Vuex = {
    Store,
    install
}

export default Vuex

我们执行下项目,如果没报错,说明我们的假设没毛病。

天啊,没报错。没毛病!

四、分析Vue.use

Vue.use(plugin);

(1)参数

{ Object | Function } plugin

(2)用法

安装Vue.js插件。如果插件是一个对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。调用install方法时,会将Vue作为参数传入。install方法被同一个插件多次调用时,插件也只会被安装一次。

关于如何上开发Vue插件,请看这篇文章,非常简单,不用两分钟就看完:如何开发 Vue 插件?

(3)作用

注册插件,此时只需要调用install方法并将Vue作为参数传入即可。但在细节上有两部分逻辑要处理:

1、插件的类型,可以是install方法,也可以是一个包含install方法的对象。

2、插件只能被安装一次,保证插件列表中不能有重复的插件。

(4)实现

Vue.use = function(plugin){
	const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
	if(installedPlugins.indexOf(plugin)>-1){
		return this;
	}
	<!-- 其他参数 -->
	const args = toArray(arguments,1);
	args.unshift(this);
	if(typeof plugin.install === 'function'){
		plugin.install.apply(plugin,args);
	}else if(typeof plugin === 'function'){
		plugin.apply(null,plugin,args);
	}
	installedPlugins.push(plugin);
	return this;
}

1、在Vue.js上新增了use方法,并接收一个参数plugin。

2、首先判断插件是不是已经别注册过,如果被注册过,则直接终止方法执行,此时只需要使用indexOf方法即可。

3、toArray方法我们在就是将类数组转成真正的数组。使用toArray方法得到arguments。除了第一个参数之外,剩余的所有参数将得到的列表赋值给args,然后将Vue添加到args列表的最前面。这样做的目的是保证install方法被执行时第一个参数是Vue,其余参数是注册插件时传入的参数。

4、由于plugin参数支持对象和函数类型,所以通过判断plugin.install和plugin哪个是函数,即可知用户使用哪种方式祖册的插件,然后执行用户编写的插件并将args作为参数传入。

5、最后,将插件添加到installedPlugins中,保证相同的插件不会反复被注册。(~~让我想起了曾经面试官问我为什么插件不会被重新加载!!!哭唧唧,现在总算明白了)

五、完善install方法

我们前面提到 通过Vue.use(Vuex) 使得每个组件都可以拥有store实例

这是什么意思呢???

来看mian.js

import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false;

new Vue({
  store,
  render: h => h(App)
}).$mount('#app');

我们可以发现这里只是将store ,也就是store/index.js导出的store实例,作为Vue 参数的一部分。

但是这里就是有一个问题咯,这里的Vue 是根组件啊。也就是说目前只有根组件有这个store值,而其他组件是还没有的,所以我们需要让其他组件也拥有这个store。

因此,install方法我们可以这样完善

let install = function(Vue){
    Vue.mixin({
        beforeCreate(){
            if (this.$options && this.$options.store){ // 如果是根组件
                this.$store = this.$options.store
            }else { //如果是子组件
                this.$store = this.$parent && this.$parent.$store
            }
        }
    })
}

解释下代码:

  1. 参数Vue,我们在第四小节分析Vue.use的时候,再执行install的时候,将Vue作为参数传进去。
  2. mixin的作用是将mixin的内容混合到Vue的初始参数options中。相信使用vue的同学应该使用过mixin了。
  3. 为什么是beforeCreate而不是created呢?因为如果是在created操作的话,$options已经初始化好了。
  4. 如果判断当前组件是根组件的话,就将我们传入的store挂在到根组件实例上,属性名为$store
  5. 如果判断当前组件是子组件的话,就将我们根组件的$store也复制给子组件。注意是引用的复制,因此每个组件都拥有了同一个$store挂载在它身上。

这里有个问题,为什么判断当前组件是子组件,就可以直接从父组件拿到$store呢?这让我想起了曾经一个面试官问我的问题:父组件和子组件的执行顺序

A:父beforeCreate-> 父created -> 父beforeMounte -> 子beforeCreate ->子create ->子beforeMount ->子 mounted -> 父mounted

可以得到,在执行子组件的beforeCreate的时候,父组件已经执行完beforeCreate了,那理所当然父组件已经有$store了。

六、实现Vuex的state

    <p>{{this.$store.state.num}}</p>

我们都知道,可以通过这个 语句获得 state的值
但是我们在Store类里还没实现,显然,现在就这样取得话肯定报错。

前面讲过,我们是这样使用Store的

export default new Vuex.Store({
  state: {
    num:0
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

也就是说,我们把这个对象

{
  state: {
    num:0
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
}

当作参数了。

那我们可以直接在Class Store里,获取这个对象

class Store{
    constructor(options){
        this.state = options.state || {}
        
    }
}

那这样是不是可以直接使用了呢?

试一下呗!

//App.vue
<template>
  <div id="app">
    123
    <p>{{this.$store.state.num}}</p>
  </div>
</template>

运行结果:

太赞了吧,怎么会这么简单。。。不敢相信。

哦不,当然没有这么简单,我们忽略了一点,state里的值也是响应式的哦,我们这样可没有实现响应式。

曾经面试官问我Vuex和全局变量比有什么区别。这一点就是注意区别吧

那要怎么实现响应式呢? 我们知道,我们new Vue()的时候,传入的data是响应式的,那我们是不是可以new 一个Vue,然后把state当作data传入呢? 没有错,就是这样。

class Store{

    constructor(options) {
        this.vm = new Vue({
            data:{
                state:options.state
            }
        })
    }

}

现在是实现响应式了,但是我们怎么获得state呢?好像只能通过this.$store.vm.state了?但是跟我们平时用的时候不一样,所以,是需要转化下的。

我们可以给Store类添加一个state属性。这个属性自动触发get接口。

class Store{

    constructor(options) {
        this.vm = new Vue({
            data:{
                state:options.state
            }
        })

    }
    //新增代码
    get state(){
        return this.vm.state
    }


}

这是ES6,的语法,有点类似于Object.defineProperty的get接口


成功实现。

七、实现getter

//myVuex.js
class Store{

    constructor(options) {
        this.vm = new Vue({
            data:{
                state:options.state
            }
        })
        // 新增代码
        let getters = options.getter || {}
        this.getters = {}
        Object.keys(getters).forEach(getterName=>{
            Object.defineProperty(this.getters,getterName,{
                get:()=>{
                    return getters[getterName](this.state)
                }
            })
        })

    }
    get state(){
        return this.vm.state
    }
}

我们把用户传进来的getter保存到getters数组里。

最有意思的是经常会有面试题问:为什么用getter的时候不用写括号。要不是我学到这个手写Vuex,也不会想不明白,原来这个问题就像问我们平时写个变量,为什么不用括号一样。(如{{num}},而不是{{num()}}

原来就是利用了Object.defineProperty的get接口。

ok,现在来试一下,getter可不可以使用。

//store/index.js
import Vue from 'vue'
import Vuex from './myVuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    num:0
  },
  // 新增测试代码
  getter:{
    getNum:(state)=>{
      return state.num
    }
  },
  mutations: {
  },
  actions: {
  },
})
<template>
  <div id="app">
    123
    <p>state:{{this.$store.state.num}}</p>
    <p>getter:{{this.$store.getters.getNum}}</p>
  </div>
</template>

完美。毫无事故。

八、实现mutation

//myVuex.js
class Store{

    constructor(options) {
        this.vm = new Vue({
            data:{
                state:options.state
            }
        })

        let getters = options.getter || {}
        this.getters = {}
        Object.keys(getters).forEach(getterName=>{
            Object.defineProperty(this.getters,getterName,{
                get:()=>{
                    return getters[getterName](this.state)
                }
            })
        })
        //新增代码
        let mutations = options.mutations || {}
        this.mutations = {}
        Object.keys(mutations).forEach(mutationName=>{
            this.mutations[mutationName] = (arg)=> {
                mutations[mutationName](this.state,arg)
            }
        })

    }
    get state(){
        return this.vm.state
    }
}

mutations跟getter一样,还是用mutations对象将用户传入的mutations存储起来。

但是怎么触发呢?回忆一下,我们是怎么触发mutations的。

this.$store.commit('incre',1)

对,是这种形式的。可以看出store对象有commit这个方法。而commit方法触发了mutations对象中的某个对应的方法,因此我们可以给Store类添加commit方法

//myVuex.js
class Store{
    constructor(options) {
        this.vm = new Vue({
            data:{
                state:options.state
            }
        })

        let getters = options.getter || {}
        this.getters = {}
        Object.keys(getters).forEach(getterName=>{
            Object.defineProperty(this.getters,getterName,{
                get:()=>{
                    return getters[getterName](this.state)
                }
            })
        })
        
        let mutations = options.mutations || {}
        this.mutations = {}
        Object.keys(mutations).forEach(mutationName=>{
            this.mutations[mutationName] =  (arg)=> {
                mutations[mutationName](this.state,arg)
            }
        })

    }
    //新增代码
    commit(method,arg){
        this.mutations[method](arg)
    }
    get state(){
        return this.vm.state
    }
}

好了,现在来测试一下。

<template>
  <div id="app">
    123
    <p>state:{{this.$store.state.num}}</p>
    <p>getter:{{this.$store.getters.getNum}}</p>
    <button @click="add">+1</button>
  </div>
</template>
//新增测试代码
<script>
  export default {
      methods:{
          add(){
              this.$store.commit('incre',1)
          }
      }
  }
</script>

store/index.js

//store/index.js
import Vue from 'vue'
import Vuex from './myVuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    num:0
  },
  getter:{
    getNum:(state)=>{
      return state.num
    }
  },
  // 新增测试代码
  mutations: {
    incre(state,arg){
        state.num += arg
    }
  },
  actions: {
  },
})

运行成功。

九、实现actions

当会实现mutations后,那actions的实现也很简单,很类似,不信看代码:

//myVuex.js
class Store{
    constructor(options) {
        this.vm = new Vue({
            data:{
                state:options.state
            }
        })

        let getters = options.getter || {}
        this.getters = {}
        Object.keys(getters).forEach(getterName=>{
            Object.defineProperty(this.getters,getterName,{
                get:()=>{
                    return getters[getterName](this.state)
                }
            })
        })

        let mutations = options.mutations || {}
        this.mutations = {}
        Object.keys(mutations).forEach(mutationName=>{
            this.mutations[mutationName] =  (arg)=> {
                mutations[mutationName](this.state,arg)
            }
        })
        //新增代码
        let actions = options.actions
        this.actions = {}
        Object.keys(actions).forEach(actionName=>{
            this.actions[actionName] = (arg)=>{
                actions[actionName](this,arg)
            }
        })

    }
    // 新增代码
    dispatch(method,arg){
        this.actions[method](arg)
    }
    commit(method,arg){
        console.log(this);
        this.mutations[method](arg)
    }
    get state(){
        return this.vm.state
    }
}

一毛一样,不过有一点需要解释下,就是这里为什么是传this进去。这个this代表的就是store实例本身

这是因为我们使用actions是这样使用的:

  actions: {
    asyncIncre({commit},arg){
        setTimeout(()=>{
          commit('incre',arg)
        },1000)
    }
  },

其实{commit} 就是对this,即store实例的解构

那我们来测试一下。

<template>
  <div id="app">
    123
    <p>state:{{this.$store.state.num}}</p>
    <p>getter:{{this.$store.getters.getNum}}</p>
    <button @click="add">+1</button>
    <button @click="asyncAdd">异步+2</button>
  </div>
</template>

<script>
  export default {
      methods:{
          add(){
              this.$store.commit('incre',1)
          },
          asyncAdd(){
              this.$store.dispatch('asyncIncre',2)
          }
      }
  }
</script>

store/index.js

//store/index.js
import Vue from 'vue'
import Vuex from './myVuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    num:0
  },
  getter:{
    getNum:(state)=>{
      return state.num
    }
  },
  mutations: {
    incre(state,arg){
        state.num += arg
    }
  },
  //新增测试代码
  actions: {
    asyncIncre({commit},arg){
        setTimeout(()=>{
          commit('incre',arg)
        },1000)
    }
  },
})

oh my god,居然出错了,它这里错误 说的是执行到这里发现这里的this为undefined。

不过,不对啊,我们在实现mutation的时候也执行到这里了啊,而且执行成功了啊。

来分析一下:

this.$store.commit('incre',1)

执行这段代码的时候,执行commit的时候,this是谁调用就指向谁,所以this指向$store

this.$store.dispatch('asyncIncre',2)

执行这段代码,就会执行

asyncIncre({commit},arg){
    setTimeout(()=>{
      commit('incre',arg)
    },1000)
}

发现问题了吧?? 谁调用commit??是$store吗?并不是。所以要解决这个问题,我们必须换成箭头函数

//myVuex.js
class Store{
    constructor(options) {
        this.vm = new Vue({
            data:{
                state:options.state
            }
        })

        let getters = options.getter || {}
        this.getters = {}
        Object.keys(getters).forEach(getterName=>{
            Object.defineProperty(this.getters,getterName,{
                get:()=>{
                    return getters[getterName](this.state)
                }
            })
        })

        let mutations = options.mutations || {}
        this.mutations = {}
        Object.keys(mutations).forEach(mutationName=>{
            this.mutations[mutationName] =  (arg)=> {
                mutations[mutationName](this.state,arg)
            }
        })

        let actions = options.actions
        this.actions = {}
        Object.keys(actions).forEach(actionName=>{
            this.actions[actionName] = (arg)=>{
                actions[actionName](this,arg)
            }
        })

    }
    dispatch(method,arg){
        this.actions[method](arg)
    }
    // 修改代码
    commit=(method,arg)=>{
        console.log(method);
        console.log(this.mutations);
        this.mutations[method](arg)
    }
    get state(){
        return this.vm.state
    }
}

再来测试

完美收官!!!!

补充:有群友问到一个问题,我觉得很有意思,就是说直接通过$store.state.xx = ""。可以吗?其实这样赋值也不会有问题,而且state依旧是响应式的。那么为什么用commit来多此一举呢?

vuex能够记录每一次state的变化记录,保存状态快照,实现时间漫游/回滚之类的操作

image

我有想到一件有意思的事情,要是说我们要实现一个最简单的Vuex,其实只实现state不就好了,其他的getter啊,action,commit都不实现。有种轻装上阵的感觉。其实也能实现。

而这样实现后发现其实跟全局变量差不多,只不过state是响应式的。

有什么不理解的或者是建议欢迎评论提出

感谢您也恭喜您看到这里,我可以卑微的求个star吗!!!

github:https://github.com/Sunny-lucking/howToBuildMyWebpack

【深入探究Node】(5)“Buffer与乱码的故事” 有十问

1. 为什么要有Buffer对象?

在Node中,应用需要处理网络协议、操作数据库、处理图片、接收上传文件等,在网络流和文件的操作中,还要处理大量二进制数据,JavaScript自有的字符串远远不能满足这些需求,于是Buffer对象应运而生。

Buffer在文件I/O和网络I/O中运用广泛,尤其在网络传输中,它的性能举足轻重。在应用中,我们通常会操作字符串,但一旦在网络中传输,都需要转换为Buffer,以进行二进制数据传输。在Web应用中,字符串转换到Buffer是时时刻刻发生的,提高字符串到Buffer的转换效率,可以很大程度地提高网络吞吐率。

2. 可以谈谈你所认识的Buffer对象吗?

嗯嗯,好的。

Buffer是一个像Array的对象,但它主要用于操作字节。所以我将会从模块结构对象结构的层面上来认识它。

模块结构

Buffer是一个典型的JavaScript与C++结合的模块,它将性能相关部分用C++实现,将非性能相关的部分用JavaScript实现,如图所示。

【深入探究Node】(4)“内存控制” 有十五问我们提到Buffer所占用的内存不是通过V8分配的,属于堆外内存。由于V8垃圾回收性能的影响,将常用的操作对象用更高效和专有的内存分配回收策略来管理是个不错的思路。由于Buffer太过常见,Node在进程启动时就已经加载了它,并将其放在全局对象(global)上。所以在使用Buffer时,无须通过require()即可直接使用。

Buffer对象结构

Buffer对象类似于数组,它的元素为16进制的两位数,即0到255的数值。示例代码如下所示:


由上面的示例可见,不同编码的字符串占用的元素个数各不相同,上面代码中的中文字在UTF-8编码下占用3个元素,字母和半角标点符号占用1个元素。

Buffer受Array类型的影响很大,可以访问length属性得到长度,也可以通过下标访问元素,在构造对象时也十分相似,代码如下:


上述代码分配了一个长100字节的Buffer对象。可以通过下标访问刚初始化的Buffer的元素,代码如下:

这里会得到一个比较奇怪的结果,它的元素值是一个0到255的随机值。同样,我们也可以通过下标对它进行赋值:

3. 哇塞,原来Buffer对象这么有意思,还可以当成Array来使用,我突发奇想,要是给元素赋值的值是小数而不是整数会怎么样呢?


给元素的赋值如果小于0,就将该值逐次加256直到得到一个0到255之间的整数。如果得到的数值大于255,就逐次减256直到得到0~255区间内的数值如果是小数,舍弃小数部分,只保留整数部分。

4. 我看Buffer对象很像字符串,它两可以互转吗?

可以的。

字符串转Buffer

字符串转Buffer对象主要是通过构造函数完成的:

通过构造函数转换的Buffer对象,存储的只能是一种编码类型。encoding参数不传递时,默认按UTF-8编码进行转码和存储。

Buffer转字符串

实现Buffer向字符串的转换也十分简单,Buffer对象的toString()可以将Buffer对象转换为字符串,代码如下:


比较精巧的是,可以设置encoding(默认为UTF-8)、start、end这3个参数实现整体或局部的转换。如果Buffer对象由多种编码写入,就需要在局部指定不同的编码,才能转换回正常的编码。

5. Buffer应该是常见于输入输入流中,你可以说说怎么使用吗?

Buffer在使用场景中,通常是以一段一段的方式传输。以下是常见的从输入流中读取内容的示例代码:

上面这段代码常见于国外,用于流读取的示范,data事件中获取的chunk对象即是Buffer对象。对于初学者而言,容易将Buffer当做字符串来理解,所以在接受上面的示例时不会觉得有任何异常。

6. 我有时候这样读取数据,然后打印出来,有时候会出现乱码,是什么原因呢?

一旦输入流中有宽字节编码时,问题就会暴露出来。如果你在通过Node开发的网站上看到[插图]乱码符号,那么该问题的起源多半来自于这里。

用多个字节来代表的字符称之为宽字符,而Unicode只是宽字符编码的一种实现,宽字符并不一定是Unicode。

这里潜藏的问题在于如下这句代码:

这句代码里隐藏了toString()操作,它等价于如下的代码:

值得注意的是,外国人的语境通常是指英文环境,在他们的场景下,这个toString()不会造成任何问题。但对于宽字节的中文,却会形成问题。为了重现这个问题,下面我们模拟近似的场景,将文件可读流的每次读取的Buffer长度限制为11,代码如下:


搭配该代码的测试数据为李白的《静夜思》。执行该程序,将会得到以下输出:

7.为什么 “月”、“是”、“望”、“低”4个字没有被正常输出,取而代之的是3个乱码?

产生这个输出结果的原因在于文件可读流在读取时会逐个读取Buffer。

这首诗的原始Buffer应存储为:


由于我们限定了Buffer对象的长度为11,因此只读流需要读取7次才能完成完整的读取,结果是以下几个Buffer对象依次输出:

上文提到的buf.toString()方法默认以UTF-8为编码,中文字在UTF-8下占3个字节。所以第一个Buffer对象在输出时,只能显示3个字符,Buffer中剩下的2个字节(e6 9c)将会以乱码的形式显示。第二个Buffer对象的第一个字节也不能形成文字,只能显示乱码。于是形成一些文字无法正常显示的问题。

在这个示例中我们构造了11这个限制,但是对于任意长度的Buffer而言,宽字节字符串都有可能存在被截断的情况,只不过Buffer的长度越大出现的概率越低而已,但该问题依然不可忽视。

8. so噶!那样的话,那我限制Buffer对象的长度为12,就不会有问题了吧!但是这样每次都要数,很麻烦,有没有简单的方法呢?

有的,我们别忘了可读流还有一个设置编码的方法setEncoding(),示例如下:


该方法的作用是让data事件中传递的不再是一个Buffer对象,而是编码后的字符串。为此,我们继续改进前面诗歌的程序,添加setEncoding()的步骤如下:


重新执行程序,得到输出:


9. 哇塞,真是令人兴奋,Node是如何实现这个输出结果的呢?

事实上,在调用setEncoding()时,可读流对象在内部设置了一个decoder对象。每次data事件都通过该decoder对象进行Buffer到字符串的解码,然后传递给调用者。是故设置编码后,data不再收到原始的Buffer对象。

10. 可是设置decoder后,即使被转码,那也无法改变宽字节字符串被截断的问题啊?

decoder对象来自于string_decoder模块StringDecoder的实例对象。

可以看看 下面的代码:

我将前文提到的前两个Buffer对象写入decoder中。奇怪的地方在于“月”的转码并没有如平常一样在两个部分分开输出。StringDecoder在得到编码后,知道宽字节字符串在UTF-8编码下是以3个字节的方式存储的,所以第一次write()时,只输出前9个字节转码形成的字符,“月”字的前两个字节被保留在StringDecoder实例内部。第二次write()时,会将这2个剩余字节和后续11个字节组合在一起,再次用3的整数倍字节进行转码。于是乱码问题通过这种中间形式被解决了。

关于【let const】十问(读《红宝书》+《ES6标准入门》)

1.下面哪个会报错,为什么

let a;
const a;

答案:

let a; //不报错
const a; //报错

const 声明的常量不得改变值。这意味着, const 一旦声明常量,就必须立即初始化,不 能留到以后赋值

2. 如何在ES5环境下实现let

对于这个问题,我们可以直接查看babel转换前后的结果,看一下在循环中通过let定义的变量是如何解决变量提升的问题

babel在let定义的变量前加了道下划线,避免在块级作用域外访问到该变量,除了对变量名的转换,我们也可以通过自执行函数来模拟块级作用域

3. 如何在ES5环境下实现const

实现const的关键在于Object.defineProperty()这个API,这个API用于在一个对象上增加或修改属性。通过配置属性描述符,可以精确地控制属性行为。Object.defineProperty() 接收三个参数:

Object.defineProperty(obj, prop, desc)

对于const不可修改的特性,我们通过设置writable属性来实现

4.const的本质是什么

const 实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。 对于简单类型的数据(数值、字符串、布尔值〉而言,值就保存在变量指向的内存地址中,因 此等同于常量。但对于复合类型的数据(主要是对象和数组)而言,变量指向的内存地址保存 的只是一个指针, const 只能保证这个指针是固定的,至于它指向的数据结构是不是可变的, 这完全不能控制。 因此,将一个对象声明为常量时必须非常小心。

5.既然const不能保证 对象属性不被修改,那该怎么解决这个问题呢?

如果真的想将对象冻结,应该使用 Object.freeze 方法。

Object.freeze()方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。

6.但是这样就能保证 对象冻结了吗?

不,因为Object.freeze()只能冻结基本数据类型的属性,若是属性还是引用类型的话,那就冻结不了了,因此,除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

7. 既然你提到了Object.freeze,你能说下他的原理 吗

Object.freeze()的功能主要就是使对象属性冻结起来,这个功能跟 上面模拟const 类似,可以用Object.definedProperty()

Object.definedProperty()方法可以定义对象的属性的特性。如可不可以删除、可不可以修改、访问这个属性的时候添油加醋等等。。

所以,我们可以这样写:

8 但是这样就完整了没,你有没有发现问题呢?

emmmmm,的确还有个问题,就是Object.definedProperty()只是设置了 对象的属性,也就是说,要是此时我使用 给对象添加属性,还是可以的,因此还有个问题要解决,就是使对象不能 添加新的属性,而要限制 拓展功能,我想到了Object.seal(),

查看Object.seal介绍:

**Object.seal()**方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。

因此用Object.seal()Object.definedProperty()就可以实现Object.freeze()

9 ES6之前真的没有块级作用域吗?

可以看下这道题,答案是什么

正确答案:内部是 21,外部是 1;

这个玄妙之处确实就在这个块级作用域 if 里面。

假如我们去掉 if 看题。

这道题估计没人好意思去问了,毫无疑问,输出的 a 都是 21 啊。

实际上,首先,if 里面的 function a(){} 会声明提升,将声明" var a" 移到函数级作用域最前面,将函数定义移到块级作用域最前面,预解析如下:

函数本身是【 定义函数名变量 指针指向 函数内存块】。

函数提升是在块级作用域,但是函数名变量是函数级别的作用域。所以在块级的函数定义(原始代码函数的声明位置)的时候,会将函数名变量同步到函数级作用域,实际上,只有这个时候,在块级作用域外才能访问到函数定义。

预解析如下:

关于此题查看更多

10 下面代码输出情况

可见在全局作用域中 var 声明的变量 会被挂载到 window中,而let 不会
而我在读一本关于es6书的时候
看到这一段话

ES6 将全局方法 parse!nt ()和 parseFloat ()移植到了 Number 对象上面,行为完全 >保持不变
这样做的目的是逐步减少全局性方法,使得语言逐步模块化。

是不是let 也想这样呢?当然这只是个人猜想,没什么理论依旧,您也不必往心里去

补充:后来在书里看到这段,好像符合我的猜想

顶层对象的属性与全局变量相关,被认为是 JavaScript 语言中最大的设计败笔之一。这样 的设计带来了几个很大的问题:首先,无法在编译时就提示变量未声明的错误,只有运行时才 能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的〉:其次,程序员 很容易不知不觉地就创建全局变量(比如打字出错〉:最后,顶层对象的属性是到处都可以读写 的,这非常不利于模块化编程。另一方面, window 对象有实体含义,指的是浏览器的窗口对 象,这样也是不合适的。

ES6 为了改变这一点, 一方面规定,为了保持兼容性, var 命令和 function 命令声明的 全局变量依旧是顶层对象的属性;另一方面规定, let 命令、 const 命令、 class 命令声明的 全局变量不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性 隔离。

讨教学习方法

大哥你好,我现在用这个vue跟傻瓜式一样,想去专研一下原理自己看的又很迷,想知道你是用什么方式去学习的,自己买的课程还是自己看的源码,我现在应届生,对现在的状况非常迷茫

BAT大佬推荐使用的HTML5的十个功能

HTML5不是新事物。自从最初发布(2008年1月)以来,我们一直在使用它的一些功能。后来,我再次仔细查看了HTML5功能列表。看到我发现了什么?到目前为止,我还没有真正使用过它!

在本文中,我列出了十个HTML5我过去没用过但现在发现有用的功能。我还创建了一个工作示例流程并托管在Netlify。希望您也觉得它有用。

点击演示实例https://html5-tips.netlify.app/

太好了,让我们开始介绍它们的解释,快速码起来吧。您可以在Twitter上关注我,以了解我将来的文章和工作。

🔥 Details Tag

<details>标签提供随需应变的细节内容给用户。如果需要按需向用户显示内容,请使用此标记。默认情况下,详细内容是关闭的。打开后,它将展开并显示其中的内容。

<summary>标签与<details>一起使用,来为它指定一个可见的标题。

Code

<details>
     <summary>Click Here to get the user details</summary>
         <table>
                <tr>
                    <th>#</th>
                    <th>Name</th>
                    <th>Location</th>
                    <th>Job</th>
                </tr>
                <tr>
                    <td>1</td>
                    <td>Adam</td>
                    <td>Huston</td>
                    <td>UI/UX</td>
                </tr>
          </table>
``  </details>

效果演示

您可以从这里开始查看演示:https : //html5-tips.netlify.app/details/index.html

小提示

在GitHub Readme中使用它来显示需要的详细信息。隐藏大量的文字并仅按需显示它。酷吧?

点击查看例子https://github.com/atapas/notifyme#properties

🔥 Content Editable

contenteditable是可以在元素上设置以使内容可编辑的属性

可以与DIV,P,UL等元素一起使用。您必须像这样指定它:<element contenteditable="true|false">

注意,如果contenteditable未在元素上设置属性,则会从其父级继承该属性。

Code

<h2> Shoppping List(Content Editable) </h2>
 <ul class="content-editable" contenteditable="true">
     <li> 1. Milk </li>
     <li> 2. Bread </li>
     <li> 3. Honey </li>
</ul>

效果演示

您可以从这里开始查看演示https://html5-tips.netlify.app/content-editable/index.html

小提示

可以使span或div元素可编辑,并且可以使用CSS样式向其添加任何丰富的内容。这将比使用input 输入框更好。试一试!

🔥 Map

<map>标签可以帮助定义image mapimage map是其中具有一个或多个可单击区域的任何图像。map标签与<area>标签一起确定可点击区域。可点击区域可以是矩形,圆形或多边形区域中的任意一种。如果您未指定任何形状,它将默认整个图像。

Code

<div>
    <img src="circus.jpg" width="500" height="500" alt="Circus" usemap="#circusmap">

    <map name="circusmap">
        <area shape="rect" coords="67,114,207,254" href="elephant.htm">
        <area shape="rect" coords="222,141,318, 256" href="lion.htm">
        <area shape="rect" coords="343,111,455, 267" href="horse.htm">
        <area shape="rect" coords="35,328,143,500" href="clown.htm">
        <area shape="circle" coords="426,409,100" href="clown.htm">
    </map>
 </div>

效果演示

您可以从这里开始查看演示https://html5-tips.netlify.app/map/index.html

小提示

图像贴图有其自身的缺点,但是您可以将其用于视觉演示。我们可以用全家福照片尝试一下并深入研究个人照片

🔥 Mark Content

使用<mark>标记突出显示任何文本内容。

Code

 <p> Did you know, you can <mark>"Highlight something interesting"</mark> just with an HTML tag? </p>

效果演示

您可以从这里开始查看演示https://html5-tips.netlify.app/mark/index.html

小提示

您始终可以使用CSS更改突出显示颜色,

mark {
  background-color: green;
  color: #FFFFFF;
}

🔥 data-* attribute

data-*属性用于存储页面或应用程序专用的自定义数据。可以在JavaScript代码中使用存储的数据来创建更多的用户体验。

data- *属性由两部分组成:

  • 属性名称不得包含任何大写字母,并且前缀“ data-”后必须至少长一个字符
  • 属性值可以是任何字符串

Code

<h2> Know data attribute </h2>
 <div 
       class="data-attribute" 
       id="data-attr" 
       data-custom-attr="You are just Awesome!"> 
   I have a hidden secret!
  </div>

 <button onclick="reveal()">Reveal</button>
function reveal() {
   let dataDiv = document.getElementById('data-attr');
    let value = dataDiv.dataset['customAttr'];
   document.getElementById('msg').innerHTML = `<mark>${value}</mark>`;
}

注意:要在JavaScript中读取这些属性的值,可以使用getAttribute(),但是规范定义了一种更简单的方法:使用dataset属性。

效果演示

您可以从这里开始查看演示https://html5-tips.netlify.app/data-attribute/index.html

小提示

您可以使用它在页面中存储一些数据,然后使用REST调用将其传递给服务器。

🔥 Output Tag

<output>标签表示运算的结果。通常,此元素定义一个区域,该区域将用于显示某些计算得出的文本。

Code

<form oninput="x.value=parseInt(a.value) * parseInt(b.value)">
   <input type="number" id="a" value="0">
          * <input type="number" id="b" value="0">
                = <output name="x" for="a b"></output>
</form>

效果演示

您可以从这里开始查看演示https://html5-tips.netlify.app/output/index.html

小提示

如果要在客户端JavaScript中执行任何计算,并且希望结果反映在页面上,请使用<output>标记。您不必走动使用可获取元素的额外步骤:getElementById()。

🔥 Datalist

<datalist>标签指定了预先定义的选项列表,并允许用户添加更多。它提供了一项autocomplete功能,使您可以提前输入所需的选项。

Code

<form action="" method="get">
    <label for="fruit">Choose your fruit from the list:</label>
    <input list="fruits" name="fruit" id="fruit">
        <datalist id="fruits">
           <option value="Apple">
           <option value="Orange">
           <option value="Banana">
           <option value="Mango">
           <option value="Avacado">
        </datalist>
     <input type="submit">
 </form>  

效果演示

您可以从这里开始查看演示https://html5-tips.netlify.app/datalist/index.html

小提示

与传统<select>-<option>标签有何不同?Select标记用于从选项中选择一个或多个项目,您需要浏览列表以进行选择。Datalist是具有自动完成支持的高级功能。也就是说Datalist标签不仅可以选择,还可以输入

🔥 Range(Slider)

range具有滑块,范围选择的输入类型

Code

<form method="post">
    <input 
         type="range" 
         name="range" 
         min="0" 
         max="100" 
         step="1" 
         value=""
         onchange="changeValue(event)"/>
 </form>
 <div class="range">
      <output id="output" name="result">  </output>
 </div> 

效果演示

您可以从这里开始查看演示https://html5-tips.netlify.app/range/index.html

小提示

在html5中,没有叫slider的东西

🔥 Meter

使用<meter>标签测量给定范围内的数据。

Code

`<label for="home">/home/atapas</label>
<meter id="home" value="4" min="0" max="10">2 out of 10</meter><br>

<label for="root">/root</label>
<meter id="root" value="0.6">60%</meter><br>`

效果演示

您可以从这里开始查看演示https://html5-tips.netlify.app/meter/index.html

小提示

请勿将<meter>标签用于进度条。我们有<Progress>HTML5的标记。

<label for="file">Downloading progress:</label>
<progress id="file" value="32" max="100"> 32% </progress>

下一步是什么?

好吧,我敢肯定,我留下了一些有用的东西。请提供有关此文章以及您对HTML5的学习的评论。

如果对您有用,请点赞/分享,这样也可以吸引其他人。我对UI / UX充满热情,并喜欢通过文章分享我的知识。

文章首发于 微信公众号《前端阳光》

关于【cookie,web Storage】十问 (读《红宝书》)

第一问:字符串最大容量是5M,那么我如果存储容量溢出了怎么办?

其实这个5M对于不同浏览器来说也是不确定的,不过大体上是一个5M的范围,溢出了怎么办,肯定会发生错误啊。浏览器会报一个名为“QuotaExceededError”的错误,如下图:

第二问:最后一次溢出的字符串是会存储到最大容量停止还是不会存储?

正常情况下,可能不会存储5M的字符串,但是也不能保证浏览器日积月累的情况下,恰巧用户也没清理过缓存,那么当最后容量接近5M的时候,我们再存储一个字符串进去的时候会发生错误,发生错误的字符串是存了一半?还是压根就没存呢?答案是--- 没存。下面是我写的一个demo,最后发现报错的时候刷新浏览器,localStorage的当前容量为发生变化。

第三问:既然存在安全问题,那么localStorage的使用就不是绝对安全的,如何更安全的使用localStorage?

前端的安全性是十分重要的一个话题,因为我们直接与用户打交道,你的程序在前端发生不可预知的错误是一定要避免的。因此这种不安全的API,我们需要找到解决办法,下面是我的一个解决办法(可能不是最优解,但是可行)。

(function(){
  var safeLocalStorage = function(key, value) {
    try{
      localStorage.setItem(key,value);
    }catch(oException){
      if(oException.name == 'QuotaExceededError'){
          console.log('已经超出本地存储限定大小!');
          // 可进行超出限定大小之后的操作,如下面可以先清除记录,再次保存
          localStorage.clear();
          localStorage.setItem(key,value);
      }
    }
  }
  this.safeLocalStorage = safeLocalStorage;
})();

第四问 qq.com可以获得a.qq.com的cookie嘛?

子域名可以访问根域名的Cookie,反之则不行。所以不行

第五问 自己封装localStorage?有过期时间

很遗憾,localstorage原生是不支持设置过期时间的,想要设置的话,就只能自己来封装一层逻辑来实现:

function set(key,value){
  var curtime = new Date().getTime();//获取当前时间
  localStorage.setItem(key,JSON.stringify({val:value,time:curtime}));//转换成json字符串序列
}
function get(key,exp)//exp是设置的过期时间
{
  var val = localStorage.getItem(key);//获取存储的元素
  var dataobj = JSON.parse(val);//解析出json对象
  if(new Date().getTime() - dataobj.time > exp)//如果当前时间-减去存储的元素在创建时候设置的时间 > 过期时间
  {
    console.log("expires");//提示过期
  }
  else{
    console.log("val="+dataobj.val);
  }
}

第六问:localStorage在ios设备上无法重复setItem()

另外,在iPhone/iPad上有时设置setItem()时会出现诡异的QUOTA_EXCEEDED_ERR错误,这时一般在setItem之前,先removeItem()就ok了。

第七问:**面试常考题目--sessionStorage,localStorage和cookie的区别

共同点

都是保存在浏览器端,且同源的

区别

  1. 与服务器的数据交换方式不同

cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。而sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存

  1. 存储大小限制也不同

cookie数据不能超过4k,同时因为每次http请求都会携带cookie,所以cookie只适合保存很小的数据,如会话标识。sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大

  1. 数据有效期不同

sessionStorage:仅在当前浏览器窗口关闭前有效,自然也就不可能持久保持;localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭

Cookie可以设置有效期、路径(path),域(domain)

面试官一波素质三连!对于只是会使用localStorage的同学来说,肯定是不得其解的。其实这也是很多同学准备面试的时候因该考虑的问题,或者说应该主攻的方向(虽然我才毕业,但是自身遇到的问题总结出来希望对大家有帮助)。在学习知识时,懂得使用固然重要,但是如果想熟练掌握一个知识点,必须更加深刻的挖掘才可以。

第八问:cookie和session原理及区别

cookie采用的是客户端的会话状态的一种储存机制。它是服务器在本地机器上存储的小段文本或者是内存中的一段数据,并随每一个请求发送至同一个服务器。

session是一种服务器端的信息管理机制,它把这些文件信息以文件的形式存放在服务器的硬盘空间上(这是默认情况,可以用memcache把这种数据放到内存里面)当客户端向服务器发出请求时,要求服务器端产生一个session时,服务器端会先检查一下,客户端的cookie里面有没有session_id,是否过期。如果有这样的session_id的话,服务器端会根据cookie里的session_id把服务器的session检索出来。如果没有这样的session_id的话,服务器端会重新建立一个。PHPSESSID是一串加了密的字符串,它的生成按照一定的规则来执行。同一客户端启动二次session_start的话,session_id是不一样的。

区别:Cookie保存在客户端浏览器中,而Session保存在服务器上。Cookie机制是通过检查客户身上的“通行证”来确定客户身份的话,那么Session机制就是通过检查服务器上的“客户明细表”来确认客户身份。Session相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。

第九问:session产生的session_id放在cookie里面,如果用户把cookie禁止掉,是不是session也不能用了呢?

禁止掉cookie后,session当然可以用,不过通过其他的方式来获得这个sessionid,比如,可以跟在url的后面,或者以表单的形势提交到服务器端。从而使服务器端了解客户端的状态。

第十问 为什么说session 比cookie更安全?

真正的cookie存在于客户端硬盘上的一个文本文件,如果两者一样的话,只要cookie就好了,让客户端来分提服务器的负担,并且对于用户来说又是透明的。但实际上不是。

session的sessionID是放在cookie里,要想功破session的话,得分两步:

第一要得到sessionID。攻破cookie后,你要得到sessionID,sessionID是要有人登录,或者启动session_start才会有,你不知道什么时候会有人登录。

第二取有效sessionID。sessionID是加密的,第二次session_start的时候,前一次的sessionID就没有用了,session过期时sessionid也会失效,想在短时间内功破加了密的 sessionID很难。session是针对某一次通信而言,会话结束session也就随着消失了。

使session失效的方法:

1.关闭tomcat 2.重启web应用 3.session时间到 4.无效的session

第十一问 cookie和session的区别:

①存在的位置:

cookie 存在于客户端,临时文件夹中; session存在于服务器的内存中,一个session域对象为一个用户浏览器服务

②安全性
cookie是以明文的方式存放在客户端的,安全性低,可以通过一个加密算法进行加密后存放; session存放于服务器的内存中,所以安全性好

③网络传输量
cookie会传递消息给服务器; session本身存放于服务器,不会有传送流量

④生命周期(以20分钟为例)
cookie的生命周期是累计的,从创建时,就开始计时,20分钟后,cookie生命周期结束;

session的生命周期是间隔的,从创建时,开始计时如在20分钟,没有访问session,那么session生命周期被销毁。但是,如果在20分钟内(如在第19分钟时)访问过session,那么,将重新计算session的生命周期。关机会造成session生命周期的结束,但是对cookie没有影响

⑤访问范围
cookie为多个用户浏览器共享; session为一个用户浏览器独享

在bigo前端实习三个月的总结

一、整体认知

1. 团队协作

在家或者在学校的时候,一直都是自己独立地开发项目,前后端都是自己一个人梭哈,怎么写,任凭自己主宰,在这种独自一人开发的模式中,对于团队协作模式可谓是一无所知。
后来,实习期间,经过几次版本的迭代之后,对于团队协作开发模式已经有了整体上的认知。例如一个项目的需求周期是怎样的:

  1. 需求评审
  2. 需求进入排期表
  3. 进入开发
  4. 前后端自测联调
  5. 自测无误后发起提测
  6. 测试提bug(最好没有bug)
  7. 要是有bug就修复
  8. 发布上线

在这些过程中,前端主要发挥什么作用,也有了大致上的了解。

2. 工程化的理解

在实习之前,我对于工程化的概念比较模糊,更多的是局限于 组件化模块化 等。虽然之前平时逛有一些技术博客网站的时候,会看到一些类似 《xx走进工程化xx》《xx自动化xx》,我也迫不及待地点进去了,但对于我来说,确实只是玄学,于是我会马上关闭,然后深呼吸,接着,选择《js的基本类型》《css 选择器的顺序》等文章 津津有味地读起来。

实习期间,进过几次发版之后,了解到,原来上线,是不需要手动把资源上传到服务器的。用名为jenkins 的持续集成/持续发布的工具实现起来要方便许多。

慢慢的刷新了我对工程化的认知,工程化远不止组件与模块,原来它还包括 规范化、 持续集成、自动化构建、自动化部署、自动化测试等等。

工程化是一个很大的话题,希望随着实践经验的积累,能够慢慢地掌握精髓,后面也需要找些书来看看。

3. Git操作

除了写代码,我想我最大的一个难题就是git操作了

在实习前,我会的git命令是这样的

  • git pull
  • git add
  • git commit
  • git push

我的分支是这样的

  • master

在创建分支的过程中,踩了不少次坑,我终于摸出了一条正确的道路,比如 在分支的管理上,我的经历是这样的:

  • 刚开始一个项目对应一个文件一个分支(所有需求都在一个分支里完成)

  • 后来一个需求对应一个文件一个分支(导致分支间的关系不明确)

  • 最后同个文件夹,一个分支对应一个需求(目前来说,还没发现什么问题♪(^∇^*))

除此之外,由于有时是另一个需求还没开发完,就要开发其他需求,所以是好几个分支在同时开发的,这时难就发生了意想不到的事情,例如下面我所遇到的:

更改已经被 push ,但是不是需要的

在分支上开发的过程中,添加了一些错误的文件,或者错误的修改之后,把本地的修改给add,commit,并且push上去了,但是这些修改是不需要的。怎么回退呢?

  1. git本地回退到指定版本后,按以往的提交顺序进行提交时会报错
  2. 这是因为gitlab已经在你提交历史前面了,你无法把push过的再次push进行覆盖,这个时候加个参数–force就行
  3. 加完参数后发现gitlab不允许强行push到保护的分支上,解决方法是让项目的管理员暂时在gitlab上把你要提交的分支设置为不受保护的分支即可

除此之外也了解到有git revert这个方法

正在发开当前分支,但需要切到其他分支

开开心心地在新的分支上开发新的功能,这时候,产品经理说 其他分支有bug或者修改需求,需要赶紧处理下,这样的话,就需要切到目标分支了,那在当前的分支上所作的修改该怎么办呢?

我想到的是commit,但其实还有更好的方法

  1. 使用git stash push –m”message” 保存当前的修改
  2. 切到目标分支修改bug,修改提交后切回原分支
  3. 使用git stash pop 还原

这里需要注意的就是保存当前修改的时候,最好是添加上message,而不是简单的git stash,因为git stash 一旦多了之后,就会记不清是做了什么修改

在错误的分支开发了新功能,新功能还没有在本地进行commit(提交)

  1. 使用git stash push –m”message”保存当前的修改

  2. 切换到需要开发的分支

  3. 使用git stash apply 应用修改

在错误的分支开发了新功能,新功能已经在本地提交了,但是还没有push到远程仓库

  1. git log --oneline 先获取本次commit的hash
  2. git cherry-pick <commit hash> 切到目标分支后将本次commit的修改merge到目标分支
  3. git reset <commit hash> 切回错误分支,回退到之前版本
  4. git checkout -- . 清空修改

二、调试技巧

1. console.table展示数据

在以往打印某个变量,基本都是使用console.log,但是其实还有更好的方法。

在控制台上展示数组或对象,使用console.table比console.log更加直观明了。

console.table([
  {name:"Sunny",age:18,country:'China',job:'engineer'},
  {name:"Luffy",age:16,country:'Japan',job:'Pirate'},
  {name:"Kin",age:36,country:'Italy',job:'doctor'},
])

image.png

当然,有时候,你可能不想输出那么多列,比如,你只想输出name和job,那么你只需要在后面加个依赖数组,表明要输出哪些字段

console.table([
  {name:"Sunny",age:18,country:'China',job:'engineer'},
  {name:"Luffy",age:16,country:'Japan',job:'Pirate'},
  {name:"Kin",age:36,country:'Italy',job:'doctor'},
],['name','job'])

image.png

实际上,除了console.table。还有其他的方法:

  • console.info :与console.log 的作用是几乎完全一样的,也是在控制台中打印信息,只不过打印时的样式可能与 console.log 略有区别
  • console.error:同样和console.log的作用几乎一样,不过会将打印的内容通过显目的红色标注出来并前面带一个 ×
  • console.warn:道理同上,会通过黄色感叹号来高亮打印信息。
  • console.time 和 console.timeEnd:两个方法是结合在一起使用的,他们接受一个相同的参数,输出两句表达式中间的代码的执行时间。
  • console.count:会打印当前的打印内容,并在后面跟上该内容的打印次数。

说了这么多,除了table,其实更多还是使用debugger!

2. copy复制数据

使用copy方法 可以复制控制台 输出的值 到粘贴板,比手动复制要方便一些
需要注意的是,只能在谷歌浏览器上哦

3. 滚动元素到视图

在调试DOM元素的时候,我们已经聚焦到相关的DOM结构上了,但是对应的元素并没有在可视窗口上展示,那么我们可以将其快速滚动到可视窗口。

控制面板 => Elements => 右击选中的DOM节点 => Scroll into view

4. 捕获快照

有时候,需要把实现好的页面交付给产品看一下,这时需要截图,但是如果网页太长,就很不方便了,难道要截很多张吗?当然,可以下载长截图的工具,可以这样,但是没必要,其实有一个长截图的命令:

控制面板 => 审查元素 => command + shift + p => capture full size screenshot

这时你就能看到一张长截图啦

5. 捕获局部快照

当然,有时候,并不想截取那么长,只想截取某个部分,其实也是有对应的命令的

控制面板 => 审查元素 => command + shift + p => capture node screens

三、代码风格

由于实习过程中,是在一个现在的项目上进行迭代,添加新的功能,所以可以从前辈们的代码中学习到一些比较好的代码风格,以下就列出来一些印象比较深刻的。

1. 注重命名

命名一个事件,总是有些困难,因为它很重要,我们希望可以直截了当地从方法名就看出方法的作用。比如将两个数组合并成一个新的数组,并且返回的数组不存在重复的值。

我们会怎样命名,才能体现出它的功能呢?

也许可以这样:

mergeListsIntoUniqueList(arr1,arr2){}

但是,实际上,一个方法只做一件事是最好的,这样耦合性会低一些,方法的复用性也就好一点

我们把这两个方法拆成两个方法,一个负责合并,一个负责去重

mergeList(arr1,arr2)
unique(arr)

2. IF语句简化

看一下下面的代码:

多个if嵌套

if(name === "sunny" || name === "Luffy" || === "Kin"){

}

有个比较优美的写法,就是把这些值写进一个数组来,然后判断name是否在数组里即可

const nameArr = ["sunny","Luffy","Kin"]
if(nameArr.includes(name)){

}

3. && 代替 if

看一下下面的代码:

  function hello(){
    console.log("hi")
  }
  let enableSpeak = false
  if( enableSpeak){
    hello()
  }

其实有个更加优美的写法

  function hello(){
    console.log("hi")
  }
  let enableSpeak = false
 
  enableSpeak && hello()

4. 多个if嵌套

当我们在接收后端返回来的数据的时候,接受到的是一个多层嵌套的对象,而我们要拿到深层处的一个对象属性的值,为了防止报错,我们可能需要利用多层if嵌套,如下代码所示:

if(result.data){
  if(result.data.obj){
    if(result.list.obj.name){
      if(result.list.obj.name.firstname){

      }
    }
  }
}

这样写起来,始终是有些麻烦的。

有个可选链操作符( ?. ),它允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效

if(result.data.obj.name.firstname){
 
}

5. 尽早返回

有下面的代碼:

function handleEvent(event){
  if(event){
    //...
    if(event.target){
      // do some thing real
    }
  }
}

尽早返回使得我们的代码更加易读

function handleEvent(event){
  if(!event || !event.target){
   return;
  }
}

6. 对象参数

有下面所示代码

function createObj(name,sex,age,hobby,job,address){}

当函数的参数比较多时,我们可以将同一类的参数使用对象进行合并,然后将合并后的对象作为参数传入,这样在调用该函数时能够很清楚地理解每个参数的含义,也不用去记住每个参数的位置

function createObj({name,sex,age,hobby,job,address}){}

四、新技术

在实习前,由于专攻vue,对于react属于零基础,由于组里的项目是react,所以就开始走上react的踩坑之路

在写react的时候,会出现一个报错,因此就会引发一些思考

1. 为什么要引入React

在写 React 的时候,你可能会写类似这样的代码:

import React from 'react'
function A(){
  return <h1>前端sunny</h1>
}

要是不写

import React from 'react'

就会报错,很奇怪,下面代码中明明没有使用到react的方法或属性,为什么一定要引入react呢?

后来查资料原来是 babel 会把jsx代码转化成

function A(){
  return React.createElement('h1',null,"前端sunny")
}

2. 为什么要引入super(),能不能不调用?

JavaScript 对this使用的限制,是有原因的。假设有如下的继承:

class Person {
  constructor(name) {
    this.name = name;
  }
}

class Geek extends Person {
  constructor(name) {
    this.sayHello;
    super(name);
  }
  sayHello() {
    alert('Good morning xdm!');
  }
}

如果 JavaScript 允许在调用super之前使用 this,一个月之后,我们需要修改sayHello方法,方法中使用了 name 属性:

  sayHello() {
    alert('Good morning xdm! I am '+ this.name);
  }

就会出现 name 为 undefined的情况了,是不是细思极恐!

3. 为什么调用方法要 bind this

class Geek extends Person {
 
  sayHello() {
    alert('Good morning xdm!');
  }
  render(){
    return (
      <button onClick={this.handleClick}>Click</button>
    )
  }
}

会发现 this是 undefined,第一次遇到这个问题,始终会觉得奇怪,因为在以前写vue的时候,是没啥问题的

vue代码:

<button @click="handleClick">Click</button>

或者

<button @click="this.handleClick">Click</button>

之所以react和vue事件的使用方式有所差别,原因还是内部的实现机制的不同

react将事件通过addEventListener统一注册到 document上,然后会有一个事件池存储了所有的事件,当事件触发的时候,通过dispatchEvent进行事件分发,可以简单的理解为,最终this.handleClick会做为一个回调函数调用

作为回调函数调用通常会出现this丢失的情况,就像下面的代码,最常见不过:

function delaySayHello(){
  let _this = this;
  setTimeout(function(){
    // 使用_this
  })
}

那有哪些方法来处理这个this呢?

1. 直接bind this

写起来顺手,一气呵成。性能不太好,每次 render 都会进行 bind,而且如果有两个元素的事件处理函数式同一个,也还是要进行 bind。

class Geek extends Person {
 
  sayHello() {
    alert('Good morning xdm!');
  }
  render(){
    return (
      <button onClick={this.sayHello.bind(this)}>Click</button>
    )
  }
}

2.constructor里手动bind

因为构造函数只执行一次,那么只会 bind 一次,如果有多个元素都需要调用这个函数,也不需要重复 bind。

没有明显缺点,可能就是代码多了?

class Geek extends Person {
  constructor(props){
    super(props)
    this.sayHello = this.sayHello.bind(this)
  }
  sayHello() {
    alert('Good morning xdm!');
  }
  render(){
    return (
      <button onClick={this.sayHello.bind(this)}>Click</button>
    )
  }
}

3.使用箭头函数

顺手,好看。每次 render 都会重复创建函数,性能会差一点。

class Geek extends Person {
  sayHello() {
    alert('Good morning xdm!');
  }
  render(){
    return (
      <button onClick={(e)=>this.sayHello(e)}>Click</button>
    )
  }
}

4.public class field

顺手,好看。处于试验阶段,如果不愿冒险,最好是在构造函数中绑定 this

class Geek extends Person {
  sayHello = () => {
    alert('Good morning xdm!');
  }
  render(){
    return (
      <button onClick={}>Click</button>
    )
  }
}

4. 手写简单的react

为了更好地熟悉react的实现原理,看一些教学视频和资料,自己尝试着写了一个简单的react

手写简单的react核心原理 :https://juejin.cn/post/6898292945867571207

5. React Hook 的学习

由于在项目的迭代中,新增的组件尽量是需要使用hooks来实现的,所以, 对react hook的学习也是必不可少的。

其中,学到了例如useMemouseCallback等性能优化方法。同样,为了更好地使用,也通过看了些视频和资料,自己尝试着写了hooks中 常用的 hook的实现原理。

手写React Hook核心原理

结尾

学无止境,发现还有很多东西需要去学习,例如react fiber,react-saga,next.js,react 360等,在接下来的日子里,也需要努力学习啊!

欢迎大家留言讨论,祝工作顺利、生活愉快!

关于【BOM】十问 (读《红宝书》)

第一问:请介绍BOM有哪些对象

第一次被问到时,只知道window和navigator

  1. window:BOM的核心对象是window对象,它表示浏览器的一个实例。
  2. avigator:navigator 对象包含有关访问者浏览器的信息。
<div id="example"></div>
<script>
  txt = "<p>浏览器代号: " + navigator.appCodeName + "</p>";
  txt+= "<p>浏览器名称: " + navigator.appName + "</p>";
  txt+= "<p>浏览器版本: " + navigator.appVersion + "</p>";
  txt+= "<p>启用Cookies: " + navigator.cookieEnabled + "</p>";
  txt+= "<p>硬件平台: " + navigator.platform + "</p>";
  txt+= "<p>用户代理: " + navigator.userAgent + "</p>";
  txt+= "<p>用户代理语言: " + navigator.systemLanguage + "</p>";
  document.getElementById("example").innerHTML=txt;
</script>
  1. window.screen 对象包含有关用户屏幕的信息。
<body>

<h3>你的屏幕:</h3>
<script>
  document.write("总宽度/高度: ");
  document.write(screen.width + "*" + screen.height);
  document.write("<br>");
  document.write("可用宽度/高度: ");
  document.write(screen.availWidth + "*" + screen.availHeight);
  document.write("<br>");
  document.write("色彩深度: ");
  document.write(screen.colorDepth);
  document.write("<br>");
  document.write("色彩分辨率: ");
  document.write(screen.pixelDepth);
</script>

</body>
  1. location: 对象用于获得当前页面的地址 (URL),并把浏览器重定向到新的页面。

一些实例:

location.hostname 返回 web 主机的域名
location.pathname 返回当前页面的路径和文件名
location.port 返回 web 主机的端口 (80  443
location.protocol 返回所使用的 web 协议(http:  https:
location.assign(url)  加载 URL 指定的新的 HTML 文档。 就相当于一个链接,跳转到指定的url,当前页面会转为新页面内容,可以点击后退返回上一个页面。
location.replace(url)  通过加载 URL 指定的文档来替换当前文档 ,这个方法是替换当前窗口页面,前后两个页面共用一个窗口,所以是没有后退返回上一页的
  1. history 对象包含浏览器的历史。
history.go(0);  // go() 里面的参数为0,表示刷新页面
history.go(-1);  // go() 里面的参数表示跳转页面的个数 例如 history.go(-1) 表示后退一个页面
history.go(1);  // go() 里面的参数表示跳转页面的个数 例如 history.go(1) 表示前进一个页面
history.back() //方法加载历史列表中的前一个 URL。
history.forward() //方法加载历史列表中的下一个 URL。

第二问:以下代码都会输出什么呢?

var age = 29;
window.color = "red";

delete window.age;

delete window.color;

alert(window.name)
alert(window.age)
alert(window.color)

答案:

var age = 29;
window.color = "red";
// 在IE<9时抛出错误,在其他所有浏览器中都返回false
delete window.age;
// 在IE<9时抛出错误,在其他所有浏览器中都返回true
delete window.color;

alert(window.name) // ''
alert(window.age) //29
alert(window.color) //undefined

使用var语句添加的window属性有一个名为[[configurable]]的特性,这个特性的值被设置为false,因此这样定义的属性不可以通过delete操作符删除.
window对象中本身就有个name属性,window.name 表示当前window的名称
age没被删除,所以输出29,而color被删除了。

第三问:你知道间接调用和超时调用吗?

javascript是单线程语言,但它允许通过设置超时值setTimeout和间歇时间值setInterval来调度代码在特定的时刻执行。前者是在指定的时间过后执行代码,而后者则是每隔指定的时间就执行一次代码。

  1. 超时调用使用window对象的setTimeout()方法,它接受两个参数:要执行的代码 和 以毫秒表示的时间。第一个参数可以是包含javascript语句的字符串(不推荐使用),也可以是函数。调用setTimeout()之后,该方法会返回一个数值ID,表示超时调用。
 // 推荐
 setTimeout(function(){
      alert("Hello");
  },1000);
  
  // 不推荐
  setTimeout("alert('Hello')",1000);
  1. 间歇调用与超时调用类似,只不过它会按照指定的时间间隔重复执行代码,直至间歇调用被取消或者页面被卸载。设置间歇调用的方法是setInterval(),它会接受的参数与setTimeout()相同:因为在不加干涉的情况下,间歇调用将会一直执行到页面卸载。(ps:建议少用setInterval(),可以用setIimeout()代替)

第四问 你刚刚说建议少用setInterval(),可以用setIimeout()代替,为什么呢?

对于这道题,要有 事件循环机制的只是储备,建议先看看:这一次,彻底弄懂 JavaScript 执行机制(别还不知道什么是宏任务,什么是微任务)

之所以说要替换,是因为setInterval的缺点

再次强调,定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。

setInterval(function, N)  
//即:每隔N秒把function事件推到消息队列中

上图可见,setInterval每隔100ms往队列中添加一个事件;100ms后,添加T1定时器代码至队列中,主线程中还有任务在执行,所以等待,some event执行结束后执行T1定时器代码;又过了100ms,T2定时器被添加到队列中,主线程还在执行T1代码,所以等待;又过了100ms,理论上又要往队列里推一个定时器代码,但由于此时T2还在队列中,所以T3不会被添加,结果就是此时被跳过这里我们还可以看到,T1定时器执行结束后马上执行了T2代码,所以并没有达到定时器的效果

综上所述,setInterval有两个缺点:

  • 使用setInterval时,某些间隔会被跳过;
  • 可能多个定时器会连续执行;
    可以这么理解:每个setTimeout产生的任务会直接push到任务队列中;而setInterval在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)。

因而我们一般用setTimeout模拟setInterval,来规避掉上面的缺点。

第五问:既然如此那该怎么用setTimeout模拟setInterval呢

setTimeout模拟setInterval,也可理解为链式的setTimeout。

setTimeout(function () {
    // 任务
    setTimeout(arguments.callee, interval);
}, interval)

上述函数每次执行的时候都会创建一个新的定时器,第二个setTimeout使用了arguments.callee()获取当前函数的引用,并且为其设置另一个定时器。好处:

  • 在前一个定时器执行完前,不会向队列插入新的定时器(解决缺点一)
  • 保证定时器间隔(解决缺点二)

警告:在严格模式下,第5版 ECMAScript (ES5) 禁止使用 arguments.callee()。当一个函数必须调用自身的时候, 避免使用 arguments.callee(), 通过要么给函数表达式一个名字,要么使用一个函数声明.

第六问:上面既然提到了hash和history,那就谈下两者的区别

hash

即地址栏 URL 中的 # 符号
hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。

history

利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法
它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。
通过history api,我们丢掉了丑陋的#,但是它也有个问题:不怕前进,不怕后退,就怕刷新,f5,(如果后端没有准备的话),因为刷新是实实在在地去请求服务器的。
在hash模式下,前端路由修改的是#中的信息,而浏览器请求时不会将 # 后面的数据发送到后台,所以没有问题。但是在history下,你可以自由的修改path,当刷新时,如果服务器中没有相应的响应或者资源,则会刷新出来404页面。

进程、线程与页面渲染的关系 (读浏览器核心原理)

[@toc]

1.仅仅打开了 1 个页面,为什么有 4 个进程

因为打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。

通常情况下会是四个,但是有很多其他情况:

  • 1:如果页面里有iframe的话,iframe也会运行在单独的进程中!

  • 2:如果页面里有插件,同样插件也需要开启一个单独的进程!

  • 3:如果你装了扩展的话,扩展也会占用进程

  • 4:如果2个页面属于同一站点的话,并且从a页面中打开的b页面,那么他们会公用一个渲染进程

下面我们来逐个分析下这几个进程的功能。

「浏览器进程」。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

「渲染进程」。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

「GPU 进程」。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

「网络进程」。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

「插件进程」。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响

2.tcp传送数据时 浏览器端就做渲染处理了么?如果前面数据包丢了 后面数据包先来是要等么?类似的那种实时渲染怎么处理?针对数据包的顺序性?

接收到http响应头中的「content-type」类型时就开始准备渲染进程了,

响应体数据一旦接受到便开始做DOM解析了!

基于http不用担心数据包丢失的问题,因为丢包和重传都是在tcp层解决的。http能保证数据按照顺序接收的!

3.从输入URL到导显示页面之导航流程

  1. 「用户输入URL按下回车键」,浏览器会根据用户输入的信息判断是搜索还是网址,如果是搜索内容,就将搜索内容+默认搜索引擎合成新的URL;如果用户输入的内容符合URL规则,浏览器就会根据URL协议,在这段内容上加上协议合成合法的URL,在继续这个过程前会执行「beforeunload」事件,浏览器导航栏显示loading状态,但是页面还是呈现前一个页面,这是因为要等渲染进程向浏览器进程「确认提交」,浏览器才会更新页面。

  2. 浏览器进程构建请求行信息,会通过进程间通信(IPC)将URL请求发送给网络进程

  3. 网络进程获取到URL,先去本地缓存中查找是否有缓存文件,如果有,拦截请求,直接200返回;否则,进入网络请求过程

  4. 网络进程请求DNS返回域名对应的IP和端口号,如果之前DNS数据缓存服务缓存过当前域名信息,就会直接返回缓存信息;否则,发起请求获取根据域名解析出来的IP和端口号(cdn解析下面单独讲),如果没有端口号,http默认80,https默认443。如果是https请求,还需要建立TLS连接。

  5. Chrome 有个机制,同一个域名同时最多只能建立 6 个TCP 连接,如果在同一个域名下同时有 10 个请求发生,那么其中 4 个请求会进入排队等待状态,直至进行中的请求完成。如果当前请求数量少于6个,会直接建立TCP连接。

  6. TCP三次握手建立连接,http请求加上TCP头部——包括源端口号、目的程序端口号和用于校验数据完整性的序号,向下传输(tcp三次握手,四次挥手细节下面单独讲)

  7. 网络层在数据包上加上IP头部——包括源IP地址和目的IP地址,继续向下传输到底层

  8. 底层通过物理网络传输给目的服务器主机

  9. 目的服务器主机网络层接收到数据包,解析出IP头部,识别出数据部分,将解开的数据包向上传输到传输层

  10. 目的服务器主机传输层获取到数据包,解析出TCP头部,识别端口,将解开的数据包向上传输到应用层

  11. 应用层HTTP解析请求头和请求体,如果需要重定向,HTTP直接返回HTTP响应数据的状态code301或者302,同时在请求头的Location字段中附上重定向地址,浏览器会根据code和Location进行重定向操作;如果不是重定向,首先服务器会根据 请求头中的If-None-Match 的值来判断请求的资源是否被更新,如果没有更新,就返回304状态码,相当于告诉浏览器之前的缓存还可以使用,就不返回新数据了;否则,返回新数据,200的状态码,并且如果想要浏览器缓存数据的话,就在相应头中加入字段:Cache-Control:Max-age=2000响应数据又顺着应用层——传输层——网络层——网络层——传输层——应用层的顺序返回到网络进程

  12. 数据传输完成,TCP四次挥手断开连接。如果,浏览器或者服务器在HTTP头部加上如下信息,TCP就一直保持连接。保持TCP连接可以省下下次需要建立连接的时间,提示资源加载速度Connection:Keep-Alive

  13. 网络进程将获取到的数据包进行解析,根据响应头中的Content-type来判断响应数据的类型,如果是字节流类型(其 Content-Type 的值是 application/octet-stream),就将该请求交给下载管理器,该导航流程结束,不再进行;如果是text/html类型,就把解析出来的响应头数据发送给通知浏览器进程获取到文档准备渲染(「到这里你应该明白第二问是为什么是在接收到content-type的时候开始准备渲染进程的了吧」),

  14. 浏览器进程获取到通知,根据当前页面B是否是从页面A打开的并且和页面A是否是同一个站点(根域名和协议一样就被认为是同一个站点),如果满足上述条件,就复用之前网页的进程,否则,新创建一个单独的渲染进程

  15. 浏览器会发出“提交导航(CommitNavigation时携带响应头等基本信息)”的消息给渲染进程,渲染进程收到消息后,会和网络进程建立传输数据的“管道”,文档数据传输完成后,渲染进程会返回“确认提交”的消息给浏览器进程

  16. 浏览器收到“确认提交”的消息后,会更新浏览器的页面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新web页面,此时的web页面是空白页

4.从输入URL到导显示页面之导航流程

构建 DOM 树」:这是因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树

样式计算

  1. 把 CSS 转换为浏览器能够理解的结构

CSS 样式来源主要有三种:

  1. 通过 link 引用的外部 CSS 文件
  2. <style>标记内的 CSS元素的
  3. style 属性内嵌的 CSS和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。
  1. 转换样式表中的属性值,使其标准化;

  1. 计算出 DOM 树中每个节点的具体样式。

在计算过程中需要遵守 CSS 的继承和层叠两个规则。这个阶段最终输出的内容是每个 DOM 节点的样式,并被保存在 ComputedStyle 的结构内

生成render树

你可能注意到了 DOM 树还含有很多不可见的元素,比如 head 标签,还有使用了 display:none 属性的元素。所以在显示之前,我们还要额外地构建一棵只包含可见元素布局树

布局计算

现在我们有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了

分层

因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)

通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层

  1. 第二点,需要剪裁(clip)的地方也会被创建为图层;
  2. 拥有层叠上下文属性(明确定位属性、透明属性、CSS 滤镜、z-index 等)的元素会创建单独图层;

图层绘制」在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,渲染引擎会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表

栅格化(raster)操作」绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系:

如上图所示,当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,那么接下来合成线程是怎么工作的呢?

合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512,如下图所示:

然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:

合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了。

渲染流水线大总结

好了,我们现在已经分析完了整个渲染流程,不过需要提一下, 通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。相信你还记得,GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。具体形式你可以参考下图:

从图中可以看出,渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。

因此,从 HTML 到 DOM、样式计算、布局、图层、绘制、光栅化、合成和显示。下面我用一张图来总结下这整个渲染流程:

结合上图,一个完整的渲染流程大致可总结为如下:

  1. 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。

  2. 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。

  3. 创建render树,并计算元素的布局信息。

  4. 对布局树进行分层,并生成分层树。

  5. 为每个图层生成绘制列表,并将其提交到合成线程。

  6. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。

  7. 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

5.关于显示

  1. 首先渲染进程里执行图层合成(Layer Compositor),也就是生成图层的操作,具体地讲,渲染进程的合成线程接收到图层的绘制消息时,会通过光栅化线程池将其提交给GPU进程,在GPU进程中执行光栅化操作,执行完成,再将结果返回给渲染进程的合成线程,执行合成图层操作!

  2. 合成的图层会被提交给浏览器进程,浏览器进程里会执行显示合成(Display Compositor),也就是将所有的图层合成为可以显示的页面图片。最终显示器显示的就是浏览器进程中合成的页面图片

6.三次握手

当面试官问你为什么需要有三次握手、三次握手的作用、讲讲三次三次握手的时候,我想很多人会这样回答:首先很多人会先讲下握手的过程:

  1. 第一次握手:客户端给服务器发送一个 SYN 报文。

  2. 第二次握手:服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文。

  3. 第三次握手:客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文。

  4. 服务器收到 ACK 报文之后,三次握手建立完成。

作用是为了确认双方的接收与发送能力是否正常。

这里我顺便解释一下为啥只有三次握手才能确认双方的接受与发送能力是否正常,而两次却不可以:

  1. 第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。

  2. 第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。

  3. 第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

因此,需要三次握手才能确认双方的接收与发送能力是否正常。

这样回答其实也是可以的,但我觉得,这个过程的我们应该要描述的更详细一点,因为三次握手的过程中,双方是由很多状态的改变的,而这些状态,也是面试官可能会问的点。所以我觉得在回答三次握手的时候,我们应该要描述的详细一点,而且描述的详细一点意味着可以扯久一点。加分的描述我觉得应该是这样:

刚开始客户端处于 closed 的状态,服务端处于 listen 状态。然后

  1. 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_Send 状态。

  2. 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。

  3. 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 establised 状态。

  4. 服务器收到 ACK 报文之后,也处于 establised 状态,此时,双方以建立起了链接。

7.四次挥手

刚开始双方都处于 establised 状态,假如是客户端先发起关闭请求,则:

  1. 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。

  2. 第二次握手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。

  3. 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。

  4. 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态

  5. 服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。

这里特别需要主要的就是TIME_WAIT这个状态了,这个是面试的高频考点,就是要理解,为什么客户端发送 ACK 之后不直接关闭,而是要等一阵子才关闭。这其中的原因就是,要确保服务器是否已经收到了我们的 ACK 报文,如果没有收到的话,服务器会重新发 FIN 报文给客户端,客户端再次收到 ACK 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文。至于 TIME_WAIT 持续的时间至少是一个报文的来回时间。一般会设置一个计时,如果过了这个计时没有再次收到 FIN 报文,则代表对方成功就是 ACK 报文,此时处于 CLOSED 状态。

8.DNS解析

(1)浏览器会检查缓存中有没有这个域名对应的解析过的IP地址,如果缓存中有,这个解析过程就结束。

(2).如果用户浏览器缓存中没有数据,浏览器会查找操作系统缓存中是否有这个域名对应的DNS解析结果。

(3)如果没有找到,在本地域名服务器(最近的一台DNS服务器)中查询IP地址

(4)如果没有找到,本地域名服务器会向根域名服务器发送一个请求

(5)如果根域名服务器中也不存在该域名,但判定这个域名属于“com”域,则本地域名服务器会向com顶级域名服务器发送一个请求

(6)如果com顶级域名服务器没有找到该域名,但判定这个域名属于“google.com”域,则本地域名服务器会向google.com域名服务器发送一个请求,以此类推

(7)直到本地域名服务器得到域名对应的IP地址,并将其缓存到本地,供下次查询使用

手写Vue-router核心原理,再也不怕面试官问我Vue-router原理

手写vue-router核心原理

@[toc]

一、核心原理

1.什么是前端路由?

在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI 之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面)。

2.如何实现前端路由?

要实现前端路由,需要解决两个核心:

  1. 如何改变 URL 却不引起页面刷新?

  2. 如何检测 URL 变化了?

下面分别使用 hash 和 history 两种实现方式回答上面的两个核心问题。

hash 实现

hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新

通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种:

  1. 通过浏览器前进后退改变 URL
  2. 通过<a>标签改变 URL
  3. 通过window.location改变URL
history 实现

history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新

history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:

  1. 通过浏览器前进后退改变 URL 时会触发 popstate 事件
  2. 通过pushState/replaceState或<a>标签改变 URL 不会触发 popstate 事件。
  3. 好在我们可以拦截 pushState/replaceState的调用和<a>标签的点击事件来检测 URL 变化
  4. 通过js 调用history的back,go,forward方法课触发该事件

所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。

二、原生js实现前端路由

1.基于 hash 实现

html

<!DOCTYPE html>
<html lang="en">
<body>
<ul>
    <ul>
        <!-- 定义路由 -->
        <li><a href="#/home">home</a></li>
        <li><a href="#/about">about</a></li>

        <!-- 渲染路由对应的 UI -->
        <div id="routeView"></div>
    </ul>
</ul>
</body>
<script>
    let routerView = routeView
    window.addEventListener('hashchange', ()=>{
        let hash = location.hash;
        routerView.innerHTML = hash
    })
    window.addEventListener('DOMContentLoaded', ()=>{
        if(!location.hash){//如果不存在hash值,那么重定向到#/
            location.hash="/"
        }else{//如果存在hash值,那就渲染对应UI
            let hash = location.hash;
            routerView.innerHTML = hash
        }
    })
</script>
</html>

解释下上面代码,其实很简单:

  1. 我们通过a标签的href属性来改变URL的hash值(当然,你触发浏览器的前进后退按钮也可以,或者在控制台输入window.location赋值来改变hash)
  2. 我们监听hashchange事件。一旦事件触发,就改变routerView的内容,若是在vue中,这改变的应当是router-view这个组件的内容
  3. 为何又监听了load事件?这时应为页面第一次加载完不会触发 hashchange,因而用load事件来监听hash值,再将视图渲染成对应的内容。

2.基于 history 实现

<!DOCTYPE html>
<html lang="en">
<body>
<ul>
    <ul>
        <li><a href='/home'>home</a></li>
        <li><a href='/about'>about</a></li>

        <div id="routeView"></div>
    </ul>
</ul>
</body>
<script>
    let routerView = routeView
    window.addEventListener('DOMContentLoaded', onLoad)
    window.addEventListener('popstate', ()=>{
        routerView.innerHTML = location.pathname
    })
    function onLoad () {
        routerView.innerHTML = location.pathname
        var linkList = document.querySelectorAll('a[href]')
        linkList.forEach(el => el.addEventListener('click', function (e) {
            e.preventDefault()
            history.pushState(null, '', el.getAttribute('href'))
            routerView.innerHTML = location.pathname
        }))
    }

</script>
</html>

解释下上面代码,其实也差不多:

  1. 我们通过a标签的href属性来改变URL的path值(当然,你触发浏览器的前进后退按钮也可以,或者在控制台输入history.go,back,forward赋值来触发popState事件)。这里需要注意的就是,当改变path值时,默认会触发页面的跳转,所以需要拦截 <a> 标签点击事件默认行为, 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
  2. 我们监听popState事件。一旦事件触发,就改变routerView的内容。
  3. load事件则是一样的

有个问题:hash模式,也可以用history.go,back,forward来触发hashchange事件吗?

A:也是可以的。因为不管什么模式,浏览器为保存记录都会有一个栈。

三、基于Vue实现VueRouter

我们先利用vue-cli建一个项目

删除一些不必要的组建后项目目录暂时如下:

已经把项目放到 githubhttps://github.com/Sunny-lucking/howToBuildMyVueRouter 可以卑微地要个star吗。有什么不理解或者什么建议,欢迎下方评论

我们主要看下App.vue,About.vue,Home.vue,router/index.js

代码如下:

App.vue

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/home">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
  const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
]
const router = new VueRouter({
  mode:"history",
  routes
})
export default router

Home.vue

<template>
  <div class="home">
    <h1>这是Home组件</h1>
  </div>
</template>

About.vue

<template>
  <div class="about">
    <h1>这是about组件</h1>
  </div>
</template>

现在我们启动一下项目。看看项目初始化有没有成功。

ok,没毛病,初始化成功。

现在我们决定创建自己的VueRouter,于是创建myVueRouter.js文件

目前目录如下

再将VueRouter引入 改成我们的myVueRouter.js

//router/index.js
import Vue from 'vue'
import VueRouter from './myVueRouter' //修改代码
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
  const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
];
const router = new VueRouter({
  mode:"history",
  routes
})
export default router

四、剖析VueRouter本质

先抛出个问题,Vue项目中是怎么引入VueRouter。

  1. 安装VueRouter,再通过import VueRouter from 'vue-router'引入
  2. const router = new VueRouter({...}),再把router作为参数的一个属性值,new Vue({router})
  3. 通过Vue.use(VueRouter) 使得每个组件都可以拥有store实例

从这个引入过程我们可以发现什么?

  1. 我们是通过new VueRouter({...})获得一个router实例,也就是说,我们引入的VueRouter其实是一个类。

所以我们可以初步假设

class VueRouter{
    
}
  1. 我们还使用了Vue.use(),而Vue.use的一个原则就是执行对象的install这个方法

所以,我们可以再一步 假设VueRouter有有install这个方法。

class VueRouter{

}
VueRouter.install = function () {
    
}

到这里,你能大概地将VueRouter写出来吗?

很简单,就是将上面的VueRouter导出,如下就是myVueRouter.js

//myVueRouter.js
class VueRouter{

}
VueRouter.install = function () {
    
}

export default VueRouter

五、分析Vue.use

Vue.use(plugin);

(1)参数

{ Object | Function } plugin

(2)用法

安装Vue.js插件。如果插件是一个对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。调用install方法时,会将Vue作为参数传入。install方法被同一个插件多次调用时,插件也只会被安装一次。

关于如何上开发Vue插件,请看这篇文章,非常简单,不用两分钟就看完:如何开发 Vue 插件?

(3)作用

注册插件,此时只需要调用install方法并将Vue作为参数传入即可。但在细节上有两部分逻辑要处理:

1、插件的类型,可以是install方法,也可以是一个包含install方法的对象。

2、插件只能被安装一次,保证插件列表中不能有重复的插件。

(4)实现

Vue.use = function(plugin){
	const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
	if(installedPlugins.indexOf(plugin)>-1){
		return this;
	}
	<!-- 其他参数 -->
	const args = toArray(arguments,1);
	args.unshift(this);
	if(typeof plugin.install === 'function'){
		plugin.install.apply(plugin,args);
	}else if(typeof plugin === 'function'){
		plugin.apply(null,plugin,args);
	}
	installedPlugins.push(plugin);
	return this;
}

1、在Vue.js上新增了use方法,并接收一个参数plugin。

2、首先判断插件是不是已经别注册过,如果被注册过,则直接终止方法执行,此时只需要使用indexOf方法即可。

3、toArray方法我们在就是将类数组转成真正的数组。使用toArray方法得到arguments。除了第一个参数之外,剩余的所有参数将得到的列表赋值给args,然后将Vue添加到args列表的最前面。这样做的目的是保证install方法被执行时第一个参数是Vue,其余参数是注册插件时传入的参数。

4、由于plugin参数支持对象和函数类型,所以通过判断plugin.install和plugin哪个是函数,即可知用户使用哪种方式祖册的插件,然后执行用户编写的插件并将args作为参数传入。

5、最后,将插件添加到installedPlugins中,保证相同的插件不会反复被注册。(~~让我想起了曾经面试官问我为什么插件不会被重新加载!!!哭唧唧,现在总算明白了)

第三点讲到,我们把Vue作为install的第一个参数,所以我们可以把Vue保存起来

//myVueRouter.js
let Vue = null;
class VueRouter{

}
VueRouter.install = function (v) {
    Vue = v;
};

export default VueRouter

然后再通过传进来的Vue创建两个组件router-link和router-view

//myVueRouter.js
let Vue = null;
class VueRouter{

}
VueRouter.install = function (v) {
    Vue = v;
    console.log(v);

    //新增代码
    Vue.component('router-link',{
        render(h){
            return h('a',{},'首页')
        }
    })
    Vue.component('router-view',{
        render(h){
            return h('h1',{},'首页视图')
        }
    })
};

export default VueRouter

我们执行下项目,如果没报错,说明我们的假设没毛病。

天啊,没报错。没毛病!

六、完善install方法

install 一般是给每个vue实例添加东西的

在这里就是给每个组件添加$route$router

$route$router有什么区别?

A:$router是VueRouter的实例对象,$route是当前路由对象,也就是说$route$router的一个属性
注意每个组件添加的$route是是同一个,$router也是同一个,所有组件共享的。

这是什么意思呢???

来看mian.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,
  render: function (h) { return h(App) }
}).$mount('#app')

我们可以发现这里只是将router ,也就是./router导出的store实例,作为Vue 参数的一部分。

但是这里就是有一个问题咯,这里的Vue 是根组件啊。也就是说目前只有根组件有这个router值,而其他组件是还没有的,所以我们需要让其他组件也拥有这个router。

因此,install方法我们可以这样完善

//myVueRouter.js
let Vue = null;
class VueRouter{

}
VueRouter.install = function (v) {
    Vue = v;
    // 新增代码
    Vue.mixin({
        beforeCreate(){
            if (this.$options && this.$options.router){ // 如果是根组件
                this._root = this; //把当前实例挂载到_root上
                this._router = this.$options.router;
            }else { //如果是子组件
                this._root= this.$parent && this.$parent._root
            }
            Object.defineProperty(this,'$router',{
                get(){
                    return this._root._router
                }
            })
        }
    })

    Vue.component('router-link',{
        render(h){
            return h('a',{},'首页')
        }
    })
    Vue.component('router-view',{
        render(h){
            return h('h1',{},'首页视图')
        }
    })
};

export default VueRouter

解释下代码:

  1. 参数Vue,我们在第四小节分析Vue.use的时候,再执行install的时候,将Vue作为参数传进去。
  2. mixin的作用是将mixin的内容混合到Vue的初始参数options中。相信使用vue的同学应该使用过mixin了。
  3. 为什么是beforeCreate而不是created呢?因为如果是在created操作的话,$options已经初始化好了。
  4. 如果判断当前组件是根组件的话,就将我们传入的router和_root挂在到根组件实例上。
  5. 如果判断当前组件是子组件的话,就将我们_root根组件挂载到子组件。注意是引用的复制,因此每个组件都拥有了同一个_root根组件挂载在它身上。

这里有个问题,为什么判断当前组件是子组件,就可以直接从父组件拿到_root根组件呢?这让我想起了曾经一个面试官问我的问题:父组件和子组件的执行顺序

A:父beforeCreate-> 父created -> 父beforeMounte -> 子beforeCreate ->子create ->子beforeMount ->子 mounted -> 父mounted

可以得到,在执行子组件的beforeCreate的时候,父组件已经执行完beforeCreate了,那理所当然父组件已经有_root了。

然后我们通过

Object.defineProperty(this,'$router',{
  get(){
      return this._root._router
  }
})

$router挂载到组件实例上。

其实这种**也是一种代理的**,我们获取组件的$router,其实返回的是根组件的_root._router

到这里还install还没写完,可能你也发现了,$route还没实现,现在还实现不了,没有完善VueRouter的话,没办法获得当前路径

七、完善VueRouter类

我们先看看我们new VueRouter类时传进了什么东东

//router/index.js
import Vue from 'vue'
import VueRouter from './myVueRouter'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
  const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
];
const router = new VueRouter({
  mode:"history",
  routes
})
export default router

可见,传入了一个为数组的路由表routes,还有一个代表 当前是什么模式的mode。因此我们可以先这样实现VueRouter

class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes || [] //你传递的这个路由是一个数组表
    }
}

先接收了这两个参数。

但是我们直接处理routes是十分不方便的,所以我们先要转换成key:value的格式

//myVueRouter.js
let Vue = null;
class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes || [] //你传递的这个路由是一个数组表
        this.routesMap = this.createMap(this.routes)
        console.log(this.routesMap);
    }
    createMap(routes){
        return routes.reduce((pre,current)=>{
            pre[current.path] = current.component
            return pre;
        },{})
    }
}

通过createMap我们将

const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }

转换成

路由中需要存放当前的路径,来表示当前的路径状态
为了方便管理,可以用一个对象来表示

//myVueRouter.js
let Vue = null;
新增代码
class HistoryRoute {
    constructor(){
        this.current = null
    }
}
class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes || [] //你传递的这个路由是一个数组表
        this.routesMap = this.createMap(this.routes)
        新增代码
        this.history = new HistoryRoute();
        
    }

    createMap(routes){
        return routes.reduce((pre,current)=>{
            pre[current.path] = current.component
            return pre;
        },{})
    }

}

但是我们现在发现这个current也就是 当前路径还是null,所以我们需要进行初始化。

初始化的时候判断是是hash模式还是 history模式。,然后将当前路径的值保存到current里

//myVueRouter.js
let Vue = null;
class HistoryRoute {
    constructor(){
        this.current = null
    }
}
class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes || [] //你传递的这个路由是一个数组表
        this.routesMap = this.createMap(this.routes)
        this.history = new HistoryRoute();
        新增代码
        this.init()

    }
    新增代码
    init(){
        if (this.mode === "hash"){
            // 先判断用户打开时有没有hash值,没有的话跳转到#/
            location.hash? '':location.hash = "/";
            window.addEventListener("load",()=>{
                this.history.current = location.hash.slice(1)
            })
            window.addEventListener("hashchange",()=>{
                this.history.current = location.hash.slice(1)
            })
        } else{
            location.pathname? '':location.pathname = "/";
            window.addEventListener('load',()=>{
                this.history.current = location.pathname
            })
            window.addEventListener("popstate",()=>{
                this.history.current = location.pathname
            })
        }
    }

    createMap(routes){
        return routes.reduce((pre,current)=>{
            pre[current.path] = current.component
            return pre;
        },{})
    }

}

监听事件跟上面原生js实现的时候一致。

八、完善$route

前面那我们讲到,要先实现VueRouter的history.current的时候,才能获得当前的路径,而现在已经实现了,那么就可以着手实现$route了。

很简单,跟实现$router一样

VueRouter.install = function (v) {
    Vue = v;
    Vue.mixin({
        beforeCreate(){
            if (this.$options && this.$options.router){ // 如果是根组件
                this._root = this; //把当前实例挂载到_root上
                this._router = this.$options.router;
            }else { //如果是子组件
                this._root= this.$parent && this.$parent._root
            }
            Object.defineProperty(this,'$router',{
                get(){
                    return this._root._router
                }
            });
             新增代码
            Object.defineProperty(this,'$route',{
                get(){
                    return this._root._router.history.current
                }
            })
        }
    })
    Vue.component('router-link',{
        render(h){
            return h('a',{},'首页')
        }
    })
    Vue.component('router-view',{
        render(h){
            return h('h1',{},'首页视图')
        }
    })
};

九、完善router-view组件

现在我们已经保存了当前路径,也就是说现在我们可以获得当前路径,然后再根据当前路径从路由表中获取对应的组件进行渲染

Vue.component('router-view',{
    render(h){
        let current = this._self._root._router.history.current
        let routeMap = this._self._root._router.routesMap;
        return h(routeMap[current])
    }
})

解释一下:

render函数里的this指向的是一个Proxy代理对象,代理Vue组件,而我们前面讲到每个组件都有一个_root属性指向根组件,根组件上有_router这个路由实例。
所以我们可以从router实例上获得路由表,也可以获得当前路径。
然后再把获得的组件放到h()里进行渲染。

现在已经实现了router-view组件的渲染,但是有一个问题,就是你改变路径,视图是没有重新渲染的,所以需要将_router.history进行响应式化。

Vue.mixin({
    beforeCreate(){
        if (this.$options && this.$options.router){ // 如果是根组件
            this._root = this; //把当前实例挂载到_root上
            this._router = this.$options.router;
            新增代码
            Vue.util.defineReactive(this,"xxx",this._router.history)
        }else { //如果是子组件
            this._root= this.$parent && this.$parent._root
        }
        Object.defineProperty(this,'$router',{
            get(){
                return this._root._router
            }
        });
        Object.defineProperty(this,'$route',{
            get(){
                return this._root._router.history.current
            }
        })
    }
})

我们利用了Vue提供的API:defineReactive,使得this._router.history对象得到监听。

因此当我们第一次渲染router-view这个组件的时候,会获取到this._router.history这个对象,从而就会被监听到获取this._router.history。就会把router-view组件的依赖wacther收集到this._router.history对应的收集器dep中,因此this._router.history每次改变的时候。this._router.history对应的收集器dep就会通知router-view的组件依赖的wacther执行update(),从而使得router-view重新渲染(其实这就是vue响应式的内部原理

好了,现在我们来测试一下,通过改变url上的值,能不能触发router-view的重新渲染

path改成home

可见成功实现了当前路径的监听。。

十、完善router-link组件

我们先看下router-link是怎么使用的。

<router-link to="/home">Home</router-link> 
<router-link to="/about">About</router-link>

也就是说父组件间to这个路径传进去,子组件接收就好
因此我们可以这样实现

Vue.component('router-link',{
    props:{
        to:String
    },
    render(h){
        let mode = this._self._root._router.mode;
        let to = mode === "hash"?"#"+this.to:this.to
        return h('a',{attrs:{href:to}},this.$slots.default)
    }
})

我们把router-link渲染成a标签,当然这时最简单的做法。
通过点击a标签就可以实现url上路径的切换。从而实现视图的重新渲染

ok,到这里完成此次的项目了。

看下VueRouter的完整代码吧

//myVueRouter.js
let Vue = null;
class HistoryRoute {
    constructor(){
        this.current = null
    }
}
class VueRouter{
    constructor(options) {
        this.mode = options.mode || "hash"
        this.routes = options.routes || [] //你传递的这个路由是一个数组表
        this.routesMap = this.createMap(this.routes)
        this.history = new HistoryRoute();
        this.init()

    }
    init(){
        if (this.mode === "hash"){
            // 先判断用户打开时有没有hash值,没有的话跳转到#/
            location.hash? '':location.hash = "/";
            window.addEventListener("load",()=>{
                this.history.current = location.hash.slice(1)
            })
            window.addEventListener("hashchange",()=>{
                this.history.current = location.hash.slice(1)
            })
        } else{
            location.pathname? '':location.pathname = "/";
            window.addEventListener('load',()=>{
                this.history.current = location.pathname
            })
            window.addEventListener("popstate",()=>{
                this.history.current = location.pathname
            })
        }
    }

    createMap(routes){
        return routes.reduce((pre,current)=>{
            pre[current.path] = current.component
            return pre;
        },{})
    }

}
VueRouter.install = function (v) {
    Vue = v;
    Vue.mixin({
        beforeCreate(){
            if (this.$options && this.$options.router){ // 如果是根组件
                this._root = this; //把当前实例挂载到_root上
                this._router = this.$options.router;
                Vue.util.defineReactive(this,"xxx",this._router.history)
            }else { //如果是子组件
                this._root= this.$parent && this.$parent._root
            }
            Object.defineProperty(this,'$router',{
                get(){
                    return this._root._router
                }
            });
            Object.defineProperty(this,'$route',{
                get(){
                    return this._root._router.history.current
                }
            })
        }
    })
    Vue.component('router-link',{
        props:{
            to:String
        },
        render(h){
            let mode = this._self._root._router.mode;
            let to = mode === "hash"?"#"+this.to:this.to
            return h('a',{attrs:{href:to}},this.$slots.default)
        }
    })
    Vue.component('router-view',{
        render(h){
            let current = this._self._root._router.history.current
            let routeMap = this._self._root._router.routesMap;
            return h(routeMap[current])
        }
    })
};

export default VueRouter

现在测试下成功没


|


点击确实视图切换了,成功。

完美收官!!!!

有什么不理解或者什么建议,欢迎下方评论

感谢您也恭喜您看到这里,我可以卑微的求个star吗!!!

github:https://github.com/Sunny-lucking/howToBuildMyVueRouter

参考文献:文章前面一、二节原理部分 摘自:https://blog.csdn.net/qq867263657/article/details/90903491

关于【script加载和执行】十问 (读《高性能JavaScript》)

第一问:请说出关于下面使用方式中script的区别

默认

<html>
<head>
  <script type="text/javascript" src="script1.js" ></script>
  <script type="text/javascript" src="script1.js" ></script>
</head>

<body>
</body>
</html>

使用defer

<html>
<head>
  <script type="text/javascript" src="script1.js" defer="defer"></script>
  <script type="text/javascript" src="script2.js" defer="defer"></script>
</head>

<body>
</body>
</html>

使用async

<html>
<head>
  <script type="text/javascript" src="script1.js" defer="async"></script>
  <script type="text/javascript" src="script2.js" defer="async"></script>
</head>

<body>
</body>
</html>

默认方式:浏览器会并行加载script, 但是执行是书写的顺序,如果script1执行未完毕,就不会开始执行script2,尽管script2已经加载完。

而且这种方式会阻碍script标签后面其他元素的渲染,直到script1执行完毕才会渲染后面的dom

defer方式:也叫延迟脚本,使用defer后,该脚本会被马上加载,但是脚本会被延迟到整个页面都解析完再执行,即等浏览器遇到</html>标签后在执行。并且这两个脚本会按顺序执行。

async方式:也叫异步脚本: ,使用async后,该脚本会被马上加载,加载完立即执行,但是不会影响页面的解析,。并且这两个脚本不会按顺序执行。谁先加载完,谁就先执行

第二问:第一种方式中script是并行下载的吗?

是的,大多数浏览器现在已经允许并行下载JavaScript文件。这是个好消息,因为<script>标签在下载外部资源时不会阻塞其他<script>标签。遗憾的是,JavaScript 下载过程仍然会阻塞其他资源的下载,比如样式文件和图片(http连接个数的限制,当然,这个原因通常可以用减少http请求来解决,就是合并JavaScript脚本)。尽管脚本的下载过程不会互相影响,但页面仍然必须等待所有 JavaScript 代码下载并执行完成才能继续。因此,尽管最新的浏览器通过允许并行下载提高了性能,但问题尚未完全解决,脚本阻塞仍然是一个问题。

第三问:为什么script脚本要放在body尾部,而不放在head里。

若在head元素中包含js文件,意味着必须等js代码都被下载,解析,执行后才能开始呈现页面(浏览器在遇到<body>标签时才开始呈现页面)

第四问:那我硬是要放在head呢?怎么解决(除了用defer,async)

动态脚本加载: 时可以用另外一种方式加载脚本,叫做动态脚本加载

文档对象模型(DOM)允许您使用 JavaScript 动态创建 HTML 的几乎全部文档内容。<script>元素与页面其他元素一样,可以非常容易地通过标准 DOM 函数创建:

 通过标准 DOM 函数创建`<script>`元素
var script = document.createElement ("script");
 script.type = "text/javascript";
 script.src = "script1.js";
 document.getElementsByTagName("head")[0].appendChild(script);

新的<script>元素加载 script1.js 源文件。此文件当元素添加到页面之后立刻开始下载。此技术的重点在于:无论在何处启动下载,文件的下载和运行都不会阻塞其他页面处理过程。您甚至可以将这些代码放在<head>部分而不会对其余部分的页面代码造成影响(除了用于下载文件的 HTTP 连接)。

当文件使用动态脚本节点下载时,返回的代码通常立即执行(除了 Firefox 和 Opera,他们将等待此前的所有动态脚本节点执行完毕)。

XMLHttpRequest脚本注入

另外一种无阻塞加载的脚本方法是使用XMLHttpRequest对象获取脚本并注入页面中。此技术会先创建一个XHR对象,然后用它下载JS文件,最后通过创建动态<script>元素将代码注入页面中。

var xmlhttp;
if (window.XMLHttpRequest){ // code for IE7+, Firefox, Chrome, Opera, Safari
  xmlhttp=new XMLHttpRequest();
}
else{ // code for IE6, IE5
  xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}

xmlhttp.onreadystatechange=function(){
  if (xmlhttp.readyState==4 && xmlhttp.status==200){
    document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
   }
}
xmlhttp.open("GET","test.js",true);
xmlhttp.send();
}

这段代码发送一个GET请求获取test.js文件。事件处理函数onReadyStateChange检查readyState是否为4,同时校验HTTP状态码是否有效(200表示有效响应,304意味着从缓存中读取)。

这种方法主要优点是:你可以下载JS代码但不立即执行。由于代码是在<script>标签之外返回的,因此它下载后不会自动执行,这使得你可以把脚本的执行推行到你准备好的时候。另一个优点是,同样的代码再所有的主流浏览器中无一例外都能正常工作。

这种方法的主要局限性是JS文件必须与所请求的页面处于相同的域,这意味着JS文件不能从CDN下载。因此大型Web应用通常不会采用XHR脚本注入。

当不是引入外部脚本时可以用window.load或$(‘document’).ready来使JavaScript等页面加载完再执行

第五问:那我该怎么知道动态脚本已经加载完毕了呢

我们可以对加载的 JS 对象使用 onload 来判断(js.onload),此方法 Firefox2、Firefox3、Safari3.1+、Opera9.6+ 浏览器都能很好的支持,但 IE6、IE7 却不支持。曲线救国 —— IE6、IE7 我们可以使用 js.onreadystatechange 来跟踪每个状态变化的情况(一般为 loading 、loaded、interactive、complete),当返回状态为 loaded 或 complete 时,则表示加载完成,返回回调函数。

对于 readyState 状态需要一个补充说明:

在 interactive 状态下,用户可以参与互动。

Opera 其实也支持 js.onreadystatechange,但他的状态和 IE 的有很大差别。

<script>
function include_js(file) {
    var _doc = document.getElementsByTagName('head')[0];
    var js = document.createElement('script');
    js.setAttribute('type', 'text/javascript');
    js.setAttribute('src', file);
    _doc.appendChild(js);
    if (!/*@cc_on!@*/0) { //if not IE
        //Firefox2、Firefox3、Safari3.1+、Opera9.6+ support js.onload
        js.onload = function () {
            alert('Firefox2、Firefox3、Safari3.1+、Opera9.6+ support js.onload');
        }
    } else {
        //IE6、IE7 support js.onreadystatechange
        js.onreadystatechange = function () {
            if (js.readyState == 'loaded' || js.readyState == 'complete') {
                alert('IE6、IE7 support js.onreadystatechange');
            }
        }
    }
    return false;
}

include_js('http://www.planabc.net/wp-includes/js/jquery/jquery.js');
</script>

第六问:动态加载的脚本会按顺序执行吗?怎么解决?

浏览器不保证文件加载的顺序。所有主流浏览器之中,只有 Firefox 和 Opera 保证脚本按照你指定的顺序执行。其他浏览器将按照服务器返回它们的次序下载并运行不同的代码文件。

解决方法:一个一个按顺序加载。加载完1.js,再加载2.js,如代码:

function loadScript(){
	var scriptArr =  Array.prototype.slice.apply(arguments);
	var script = document.createElement('script');
	script.type = 'text/javascript'; 
	
	var rest = scriptArr.slice(1);

	if(rest.length > 0){
		script.onload = script.onreadystatechange = function() { 
			if ( !this.readyState || this.readyState === "loaded" || 
			this.readyState === "complete" ) { 
				loadScript.apply(null, rest); 
				// Handle memory leak in IE 
				script.onload = script.onreadystatechange = null; 
			} 
		}; 	
	}					

	script.src = scriptArr[0];
	document.body.appendChild(script);
}	
loadScript('1.js','2.js','3.js');

第七问:有见过noscript标签吗?知道是干嘛用的吗?

如果浏览器不支持支持脚本,那么它会显示出 noscript 元素中的文本。

  <body>
  ...
  ...
  <script type="text/vbscript">
   <!--
   document.write("Hello World!")
   '-->
  </script>
  
  <noscript>Your browser does not support VBScript!</noscript>
  ...
  ...
</body>

总结

减少 JavaScript 对性能的影响有以下几种方法:

  • 将所有的<script>标签放到页面底部,也就是</body>闭合标签之前,这能确保在脚本执行前页面已经完成了渲染。
  • 尽可能地合并脚本。页面中的<script>标签越少,加载也就越快,响应也越迅速。无论是外链脚本还是内嵌脚本都是如此。
  • 采用无阻塞下载 JavaScript 脚本的方法:
  • 使用<script>标签的 defer 属性(仅适用于 IE 和 Firefox 3.5 以上版本);
  • 使用动态创建的<script>元素来下载并执行代码;
  • 使用 XHR 对象下载 JavaScript 代码并注入页面中。
  • 通过以上策略,可以在很大程度上提高那些需要使用大量 JavaScript 的 Web 网站和应用的实际性能。

该模块后续补充,也欢迎大家补充

看高性能js这本书时,有一段话让我很不解他想表达什么意思,如下,既然是放在body底部了为什么还要动态加载?(我的理解:这描述的应该是懒加载,动态加载,你不需要就可以先不加载,
欢迎交流)

【深入探究Node】(4)“内存控制” 有十五问

1. V8是用什么给对象分配内存的呢?

在V8中,所有的JavaScript对象都是通过堆来进行分配的。Node提供了V8中内存使用量的查看方式,执行下面的代码,将得到输出的内存信息:

$ node
> process.memoryUsage();
{ rss: 14958592,
  heapTotal: 7195904,
  heapUsed: 2821496 }

在上述代码中,在memoryUsage()方法返回的3个属性中,heapTotal和heapUsed是V8的堆内存使用情况,前者是已申请到的堆内存,后者是当前使用的量。至于rss为何,我们在后续的内容中会介绍到。图为V8的堆示意图:

当我们在代码中声明变量并赋值时,所使用对象的内存就分配在堆中。如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆的大小超过V8的限制为止。

2. V8为何要限制堆的大小?

表层原因为V8最初为浏览器而设计,不太可能遇到用大量内存的场景。对于网页来说,V8的限制值已经绰绰有余。

深层原因是V8的垃圾回收机制的限制。按官方的说法,以1.5 GB的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至要1秒以上。这是垃圾回收中引起JavaScript线程暂停执行的时间,在这样的时间花销下,应用的性能和响应能力都会直线下降。这样的情况不仅仅后端服务无法接受,前端浏览器也无法接受。因此,在当时的考虑下直接限制堆内存是一个好的选择。

3. 原来如此,那你知道垃圾回收机制的策略是什么吗?

V8的垃圾回收策略主要基于分代式垃圾回收机制。

4. 为什么要分代呢?

因为在实际的应用中,对象的生存周期长短不一,不同的算法只能针对特定情况具有最好的效果。为此,现代的垃圾回收算法中按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同分代的内存施以更高效的算法。

5. 哦,那你谈谈是怎么分代的?

在V8中,主要将内存分为新生代和老生代两代。新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。图为V8的分代示意图。

6. 那 新生代是怎么回收的?

在分代的基础上,新生代中的对象主要通过Scavenge算法进行垃圾回收。是一种采用复制的方式实现的垃圾回收算法。

它将堆内存一分为二,每一部分空间称为semispace。在这两个semispace空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的semispace空间称为From空间,处于闲置状态的空间称为To空间。当我们分配对象时,先是在From空间中进行分配。当开始进行垃圾回收时,会检查From空间中的存活对象,这些存活对象将被复制到To空间中,而非存活对象占用的空间将会被释放。完成复制后,From空间和To空间的角色发生对换。

简而言之,在垃圾回收的过程中,就是通过将存活对象在两个semispace空间之间进行复制。

Scavenge的缺点是只能使用堆内存中的一半,这是由划分空间和复制机制所决定的。但Scavenge由于只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以它在时间效率上有优异的表现。

由于Scavenge是典型的牺牲空间换取时间的算法,所以无法大规模地应用到所有的垃圾回收中。但可以发现,Scavenge非常适合应用在新生代中,因为新生代中对象的生命周期较短,恰恰适合这个算法。

是故,V8的堆内存示意图应当如图所示。

当一个对象经过多次复制依然存活时,它将会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中,采用新的算法进行管理。对象从新生代中移动到老生代中的过程称为晋升

7. 很好奇,一个新生代它是怎么晋升成老生代的。

对象晋升的条件主要有两个,一个是对象是否经历过Scavenge回收,一个是To空间的内存占用比超过限制

在默认情况下,V8的对象分配主要集中在From空间中。对象从From空间中复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收。如果已经经历过了,会将该对象从From空间复制到老生代空间中,如果没有,则复制到To空间中。这个晋升流程如图所示。

另一个判断条件是To空间的内存占用比。当要从From空间复制一个对象到To空间时,如果To空间已经使用了超过25%,则这个对象直接晋升到老生代空间中,这个晋升的判断示意图如图所示。

8. 为什么要设置25%这个这么低的值呢?

设置25%这个限制值的原因是当这次Scavenge回收完成后,这个To空间将变成From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。

9. 新生代的对象晋升后就成老生代了,那老生代为什么不能用Scavenge回收?

对于老生代中的对象,由于存活对象占较大比重,再采用Scavenge的方式会有两个问题:一个是存活对象较多,复制存活对象的效率将会很低;另一个问题依然是浪费一半空间的问题。这两个问题导致应对生命周期较长的对象时Scavenge会显得捉襟见肘。

10. 那老生代的对象该怎么处理?

V8在老生代中主要采用了Mark-SweepMark-Compact相结合的方式进行垃圾回收。

Mark-Sweep是标记清除的意思,它分为标记和清除两个阶段。与Scavenge相比,Mark-Sweep并不将内存空间划分为两半,所以不存在浪费一半空间的行为。与Scavenge复制活着的对象不同,Mark-Sweep在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。可以看出,Scavenge中只复制活着的对象,而Mark-Sweep只清理死亡对象。活对象在新生代中只占较小部分,死对象在老生代中只占较小部分,这是两种回收方式能高效处理的原因。图为Mark-Sweep在老生代空间中标记后的示意图,黑色部分标记为死亡的对象。

11. 那为什么还要标记整理?

Mark-Sweep最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。

为了解决Mark-Sweep的内存碎片问题,Mark-Compact被提出来。Mark-Compact是标记整理的意思,是在Mark-Sweep的基础上演变而来的。它们的差别在于对象在标记为死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。图为Mark-Compact完成标记并移动存活对象后的示意图,白色格子为存活对象,深色格子为死亡对象,浅色格子为存活对象移动后留下的空洞。


完成移动后,就可以直接清除最右边的存活对象后面的内存区域完成回收。

12. 咦!既然标记整理是基于标记清除上演变而来的,也就是它包括了标记清除,这么棒,那就用标记整理好了,干嘛还要说它结合标记清除使用呢?

这里将Mark-Sweep和Mark-Compact结合着介绍不仅仅是因为两种策略是递进关系,在V8的回收策略中两者是结合使用的。表是目前介绍到的3种主要垃圾回收算法的简单对比。

从表中可以看到,在Mark-Sweep和Mark-Compact之间,由于Mark-Compact需要移动对象,所以它的执行速度不可能很快,所以在取舍上,V8主要使用Mark-Sweep,在空间不足以对从新生代中晋升过来的对象进行分配时才使用Mark-Compact。

13. 原来是这样啊,要是垃圾回收算法时间花费很长,岂不是就要卡顿?

垃圾回收的3种基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为“全停顿”(stop-the-world)。在V8的分代式垃圾回收中,一次小垃圾回收只收集新生代,由于新生代默认配置得较小,且其中存活对象通常较少,所以即便它是全停顿的影响也不大。

但V8的老生代通常配置得较大,且存活对象较多,全堆垃圾回收(full垃圾回收)的标记、清理、整理等动作造成的停顿就会比较可怕,需要设法改善。

为了降低全堆垃圾回收带来的停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记(incremental marking),也就是拆分为许多小“步进”,每做完一“步进”就让JavaScript应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。图为增量标记示意图。


V8在经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原本的1/6左右。V8后续还引入了延迟清理(lazy sweeping)与增量式整理(incrementalcompaction),让清理与整理动作也变成增量式的。同时还计划引入并行标记与并行清理,进一步利用多核性能降低每次停顿的时间。

14. 你知道Buffer对象吗?Buffer对象是通过V8分配内存的吗?

知道。他不是。

为何Buffer对象并非通过V8分配?这在于Node并不同于浏览器的应用场景。在浏览器中,JavaScript直接处理字符串即可满足绝大多数的业务需求,而Node则需要处理网络流和文件I/O流,操作字符串远远不能满足传输的性能需求。

关于Buffer的细节 后面再仔细讲解一下。

所以,从这里我们可以知道,Node的内存构成主要由通过V8进行分配的部分和Node自行分配的部分。受V8的垃圾回收限制的主要是V8的堆内存。

15. 可以利用fs.readFile()和fs.writeFile()方法 来 读写大文件吗?

由于V8的内存限制,我们无法通过fs.readFile()fs.writeFile()直接进行大文件的操作,而改用fs.createReadStream()fs.createWriteStream()方法通过流的方式实现对大文件的操作。

下面的代码展示了如何读取一个文件,然后将数据写入到另一个文件的过程:


由于读写模型固定,上述方法有更简洁的方式,具体如下所示:


可读流提供了管道方法pipe(),封装了data事件和写入操作。通过流的方式,上述代码不会受到V8内存限制的影响,有效地提高了程序的健壮性。如果不需要进行字符串层面的操作,则不需要借助V8来处理,可以尝试进行纯粹的Buffer操作,这不会受到V8堆内存的限制。但是这种大片使用内存的情况依然要小心,即使V8不限制堆内存的大小,物理内存依然有限制。

公众号【前端阳光】,可加入技术交流群

【深入探究Node】(3)“异步IO” 有九问

1.为什么要异步I/O?

具体到实处,则可以从用户体验资源分配这两个方面说起。

用户体验

与前端JavaScript在单线程上执行,而且它还与UI渲染共用一个线程 一样。JavaScript在执行的时候UI渲染和响应是处于停滞状态的。那么,在node中,假设此时不使用异步io,那么当一个io在执行的时候,另一个io的执行必须等待前一个io执行完毕才可以。那么速度就会慢很多,需要认识到只有后端能够快速响应资源,才能让前端的体验变好

资源分配

我们首先需要知道计算机在发展过程中将组件进行了抽象,分为I/O设备和计算设备。

如果创建多线程的开销小于并行执行,那么多线程的方式是首选的。多线程的代价在于创建线程和执行期线程上下文切换的开销较大。另外,在复杂的业务中,多线程编程经常面临锁、状态同步等问题,这是多线程被诟病的主要原因。但是多线程在多核CPU上能够有效提升CPU的利用率,这个优势是毋庸置疑的。

单线程顺序执行任务的方式比较符合编程人员按顺序思考的思维方式。它依然是最主流的编程方式,因为它易于表达。但是串行执行的缺点在于性能,任意一个略慢的任务都会导致后续执行代码被阻塞。在计算机资源中,通常I/O与CPU计算之间是可以并行进行的但是同步的编程模型导致的问题是,I/O的进行会让后续任务等待,这造成资源不能被更好地利用

单线程同步编程模型会因阻塞I/O导致硬件资源得不到更优的使用。多线程编程模型也因为编程中的死锁、状态同步等问题让开发人员头疼。

Node在两者之间给出了它的方案:利用单线程,远离多线程死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,以更好地使用CPU。

异步I/O可以算作Node的特色,因为它是首个大规模将异步I/O应用在应用层上的平台,它力求在单线程上将资源分配得更高效。为了弥补单线程无法利用多核CPU的缺点,Node提供了类似前端浏览器中WebWorkers的子进程,该子进程可以通过工作进程高效地利用CPU和I/O。

异步I/O的提出是期望I/O的调用不再阻塞后续运算,将原有等待I/O完成的这段时间分配给其余需要的业务去执行

下图为异步I/O的调用示意图。

2.说到异步IO,我也经常听到非阻塞IO,这两者是一个东西吗?

异步与非阻塞听起来似乎是同一回事。从实际效果而言,异步和非阻塞都达到了我们并行I/O的目的。但是从计算机内核I/O而言,异步/同步和阻塞/非阻塞实际上是两回事。

操作系统内核对于I/O只有两种方式:阻塞与非阻塞。在调用阻塞I/O时,应用程序需要等待I/O完成才返回结果,如图所示。


阻塞I/O的一个特点是调用之后一定要等到系统内核层面完成所有操作后,调用才结束。以读取磁盘上的一段文件为例,系统内核在完成磁盘寻道、读取数据、复制数据到内存中之后,这个调用才结束。

阻塞I/O造成CPU等待I/O,浪费等待时间,CPU的处理能力不能得到充分利用。为了提高性能,内核提供了非阻塞I/O。非阻塞I/O跟阻塞I/O的差别为调用之后会立即返回,如图所示。

这个让我想起直接打印状态为pending的promise对象,也是可以打印出来的,这个就是异步吧,虽然状态还没变为resolved或者rejected,也一样返回了。

非阻塞I/O返回之后,CPU的时间片可以用来处理其他事务,此时的性能提升是明显的。

3.这样的话根本无法返回完整的数据,怎么办?

层期望的数据,而仅仅是当前调用的状态。为了获取完整的数据,应用程序需要重复调用I/O操作来确认是否完成。这种重复调用判断操作是否完成的技术叫做轮询。

4.可以说下什么是轮询技术吗?

任意技术都并非完美的。阻塞I/O造成CPU等待浪费,非阻塞带来的麻烦却是需要轮询去确认是否完全完成数据获取,它会让CPU处理状态判断,是对CPU资源的浪费。

轮询技术主要包括这几种:read、select、poll、epoll

read

它是最原始、性能最低的一种,通过重复调用来检查I/O的状态来完成完整数据的读取。在得到最终数据前,CPU一直耗用在等待上。下图为通过read进行轮询的示意图。

select。

它是在read的基础上改进的一种方案,通过对文件描述符上的事件状态来进行判断。下图为通过select进行轮询的示意图。


select轮询具有一个较弱的限制,那就是由于它采用一个1024长度的数组来存储状态,所以它最多可以同时检查1024个文件描述符。

poll。

该方案较select有所改进,采用链表的方式避免数组长度的限制,其次它能避免不需要的检查。但是当文件描述符较多的时候,它的性能还是十分低下的。下图为通过poll实现轮询的示意图,它与select相似,但性能限制有所改善。

epoll。

该方案是Linux下效率最高的I/O事件通知机制,在进入轮询的时候如果没有检查到I/O事件,将会进行休眠,直到事件发生将它唤醒。它是真实利用了事件通知、执行回调的方式,而不是遍历查询,所以不会浪费CPU,执行效率较高。下图为通过epoll方式实现轮询的示意图。

轮询技术满足了非阻塞I/O确保获取完整数据的需求,但是对于应用程序而言,它仍然只能算是一种同步,因为应用程序仍然需要等待I/O完全返回,依旧花费了很多时间来等待。等待期间,CPU要么用于遍历文件描述符的状态,要么用于休眠等待事件发生。结论是它不够好

5.尽管epoll已经利用了事件来降低CPU的耗用,但是休眠期间CPU几乎是闲置的,对于当前线程而言利用率不够。那么,是否有一种理想的异步I/O呢?

有啊。

我们期望的完美的异步I/O应该是应用程序发起非阻塞调用,无须通过遍历或者事件唤醒等方式轮询,可以直接处理下一个任务,只需在I/O完成后通过信号或回调将数据传递给应用程序即可


幸运的是,在Linux下存在这样一种方式,它原生提供的一种异步I/O方式(AIO)就是通过信号或回调来传递数据的。

但不幸的是,只有Linux下有,而且它还有缺陷——AIO仅支持内核I/O中的O_DIRECT方式读取,导致无法利用系统缓存。

6.现实的异步I/O是怎么实现的?

现实比理想要骨感一些,但是要达成异步I/O的目标,并非难事。前面我们将场景限定在了单线程的状况下,多线程的方式会是另一番风景。通过让部分线程进行阻塞I/O或者非阻塞I/O加轮询技术来完成数据获取,让一个线程进行计算处理,通过线程之间的通信将I/O得到的数据进行传递,这就轻松实现了异步I/O(尽管它是模拟的),示意图如图。

另一个需要强调的地方在于我们时常提到Node是单线程的,这里的单线程仅仅只是JavaScript执行在单线程中罢了。在Node中,无论是*nix还是Windows平台,内部完成I/O任务的另有线程池

7.以上是系统对异步IO的实现,那node中是怎么实现异步IO的

完成整个异步I/O环节的有事件循环观察者线程池请求对象等。

事件循环

首先,我们着重强调一下Node自身的执行模型——事件循环,正是它使得回调函数十分普遍。

在进程启动时,Node便会创建一个类似于while(true)的循环,每执行一次循环体的过程我们称为Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行它们。然后进入下个循环,如果不再有事件处理,就退出进程。流程图如图

观察者

在每个Tick的过程中,如何判断是否有事件需要处理呢?这里必须要引入的概念是观察者。每个事件循环中有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。

这个过程就如同饭馆的厨房,厨房一轮一轮地制作菜肴,但是要具体制作哪些菜肴取决于收银台收到的客人的下单。厨房每做完一轮菜肴,就去问收银台的小妹,接下来有没有要做的菜,如果没有的话,就下班打烊了。

在这个过程中,收银台的小妹就是观察者,她收到的客人点单就是关联的回调函数。当然,如果饭馆经营有方,它可能有多个收银员,就如同事件循环中有多个观察者一样。收到下单就是一个事件,一个观察者里可能有多个事件。

浏览器采用了类似的机制。事件可能来自用户的点击或者加载某些文件时产生,而这些产生的事件都有对应的观察者。在Node中,事件主要来源于网络请求、文件I/O等,这些事件对应的观察者有文件I/O观察者、网络I/O观察者等。观察者将事件进行了分类。

事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等则是事件的生产者,源源不断为Node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。

请求对象

我们可以先看这张图,大致了解一下node中异步io的实现,然后在看下面的分析。

由于下面的讲解中会引用到内部的一些方法,要记住这些方法是很困难的,所以我建议不必深究这些方法是怎么写的,只要能够弄清楚这张图的流程就好

我们将通过解释Windows下异步I/O(利用IOCP实现)的简单例子来探寻从JavaScript代码到系统内核之间都发生了什么。对于一般的(非异步)回调函数,函数由我们自行调用,如下所示:

对于Node中的异步I/O调用而言,回调函数却不由开发者来调用。那么从我们发出调用后,到回调函数被执行,中间发生了什么呢?事实上,从JavaScript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,它叫做请求对象

下面我们以最简单的fs.open()方法来作为例子,探索Node与底层之间是如何执行异步I/O调用以及回调函数究竟是如何被调用执行的:


fs.open()的作用是根据指定路径和参数去打开一个文件,从而得到一个文件描述符,这是后续所有I/O操作的初始操作。从前面的代码中可以看到,JavaScript层面的代码通过调用C++核心模块进行下层的操作。

从JavaScript调用Node的核心模块,核心模块调用C++内建模块,内建模块通过libuv进行系统调用,这是Node里经典的调用方式。这里libuv作为封装层,有两个平台的实现,实质上是调用了uv_fs_open()方法。在uv_fs_open()的调用过程中,我们创建了一个FSReqWrap请求对象。从JavaScript层传入的参数和当前方法都被封装在这个请求对象中,其中我们最为关注的回调函数则被设置在这个对象的oncomplete_sym属性上:

对象包装完毕后,在Windows下,则调用QueueUserWorkItem()方法将这个FSReqWrap对象推入线程池中等待执行,该方法的代码如下所示:

QueueUserWorkItem()方法接受3个参数:第一个参数是将要执行的方法的引用,这里引用的是uv_fs_thread_proc,第二个参数是uv_fs_thread_proc方法运行时所需要的参数;第三个参数是执行的标志。当线程池中有可用线程时,我们会调用uv_fs_thread_proc()方法。

uv_fs_thread_proc()方法会根据传入参数的类型调用相应的底层函数。以uv_fs_open()为例,实际上调用fs__open()方法。

至此,JavaScript调用立即返回,由JavaScript层面发起的异步调用的第一阶段就此结束。JavaScript线程可以继续执行当前任务的后续操作。当前的I/O操作在线程池中等待执行,不管它是否阻塞I/O,都不会影响到JavaScript线程的后续执行,如此就达到了异步的目的。

请求对象是异步I/O过程中的重要中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及I/O操作完毕后的回调处理。

8.So嘎,上面讲得是异步方法的调用,也就是fs.open这个方法的调用,那后面的io操作以及回调函数的执行呢?

简单的回答就是:调用fs.open这个方法之后就会获得一个io读取操作,然后把这个操作放入到线程池,等待有空的线程来执行io的读取操作,然后得到结果,将数据传递给回调函数,再执行,再执行回调。

如下图所示。

下面是详细讲解:

组装好请求对象、送入I/O线程池等待执行,实际上完成了异步I/O的第一部分,回调通知是第二部分。

线程池中的I/O操作调用完毕之后,会将获取的结果储存在req->result属性上,然后调用PostQueuedCompletionStatus()通知IOCP,告知当前对象操作已经完成:


PostQueuedCompletionStatus()方法的作用是向IOCP提交执行状态,并将线程归还线程池。通过PostQueuedCompletionStatus()方法提交的状态,可以通过GetQueuedCompletionStatus()提取。

在这个过程中,我们其实还动用了事件循环的I/O观察者。在每次Tick的执行中,它会调用IOCP相关的GetQueuedCompletionStatus()方法检查线程池中是否有执行完的请求,如果存在,会将请求对象加入到I/O观察者的队列中,然后将其当做事件处理。

I/O观察者回调函数的行为就是取出请求对象的result属性作为参数,取出oncomplete_sym属性作为方法,然后调用执行,以此达到调用JavaScript中传入的回调函数的目的。至此,整个异步I/O的流程完全结束。

事件循环、观察者、请求对象、I/O线程池这四者共同构成了Node异步I/O模型的基本要素。

9.setTimeout()、setInterval()、setImmediate()和process.nextTick()也是异步IO吗?

并不是,这些是异步API。

这一部分也值得略微关注一下。

定时器

setTimeout()和setInterval()与浏览器中的API是一致的,分别用于单次和多次定时执行任务。它们的实现原理与异步I/O比较类似,只是不需要I/O线程池的参与。调用setTimeout()或者setInterval()创建的定时器会被插入到定时器观察者内部的一个红黑树中。每次Tick执行时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,如果超过,就形成一个事件,它的回调函数将立即执行。


定时器的问题在于,它并非精确的(在容忍范围内)。尽管事件循环十分快,但是如果某一次循环占用的时间较多,那么下次循环时,它也许已经超时很久了。譬如通过setTimeout()设定一个任务在10毫秒后执行,但是在9毫秒后,有一个任务占用了5毫秒的CPU时间片,再次轮到定时器执行时,时间就已经过期4毫秒。

process.nextTick()

在未了解process.nextTick()之前,很多人也许为了立即异步执行一个任务,会这样调用setTimeout()来达到所需的效果:


由于事件循环自身的特点,定时器的精确度不够。而事实上,采用定时器需要动用红黑树,创建定时器对象和迭代等操作,而setTimeout(fn, 0)的方式较为浪费性能。实际上,process.nextTick()方法的操作相对较为轻量,具体代码如下:


每次调用process.nextTick()方法,只会将回调函数放入队列中,在下一轮Tick时取出执行。定时器中采用红黑树的操作时间复杂度为O(lg(n)), nextTick()的时间复杂度为O(1)。相较之下,process.nextTick()更高效。

setImmediate()

setImmediate()方法与process.nextTick()方法十分类似,都是将回调函数延迟执行。在Node v0.9.1之前,setImmediate()还没有实现,那时候实现类似的功能主要是通过process.nextTick()来完成,该方法的代码如下所示:


上述代码的输出结果如下:


而用setImmediate()实现时,相关代码如下:


其结果完全一样:

但是两者之间其实是有细微差别的。将它们放在一起时,又会是怎样的优先级呢。示例代码如下:


其执行结果如下:

从结果里可以看到,process.nextTick()中的回调函数执行的优先级要高于setImmediate()。这里的原因在于事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idle观察者,setImmediate()属于check观察者。在每一个轮循环检查中,idle观察者先于I/O观察者,I/O观察者先于check观察者。

在具体实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中。在行为上,process.nextTick()在每轮循环中会将数组中的回调函数全部执行完,而setImmediate()在每轮循环中执行链表中的一个回调函数。如下的示例代码可以佐证:


其执行结果如下:


从执行结果上可以看出,当第一个setImmediate()的回调函数执行后,并没有立即执行第二个,而是进入了下一轮循环,再次按process.nextTick()优先、setImmediate()次后的顺序执行。之所以这样设计,是为了保证每轮循环能够较快地执行结束,防止CPU占用过多而阻塞后续I/O调用的情况。

太卡了,巧妙地优化了跑马灯

前言

上周优化了个跑马灯,原因是跑马灯的长度太长了,每个item的节点比较多,所以即使限制最多只有50个item,也还是很长很长,有多长可以看看下面

怎么优化呢?看看之前的跑马灯

优化前的写法

之前的写法很简单,其实就是让很长很长的class="animate"的div在lottery-person-wrapper中滚动。用的是css 中的animation属性。

用animation虽然好,但是不能控制跑马灯的长度,即我不想让50个item一起滚动,最好是让只需要出现在屏幕中的item滚动就好了。于是就将滚动改成了item为绝对定位,然后利用transform来改变位置,然后利用transition来实现动画的过渡。

优化后的写法

可以看到没有那么多的item节点了,这是怎么办到的呢?

  1. 首先获取lottery-person-wrapper的宽度
this.animationWrapperWidth = this.$refs.animateWrapper.clientWidth;
  1. 然后再让一个item出现在跑马灯中。
mounted() {
  this.$nextTick(() => {
     this.animationWrapperWidth = this.$refs.animateWrapper.clientWidth;
     this.emitItem();
   });
}
  1. 看看emit是怎么写的

首先需要知道

  • swiperUserList是从接口获取到的列表
  • swiperUserListShow是在template中遍历的列表

我们先拿出swiperUserList中的第一个item,然后再把item放入swiperUserList的尾部,让swiperUserList始终保持50个item。

然后,再把这个item深拷贝放入到swiperUserListShow中,为什么要深拷贝是因为,不希望swiperUserListShow的item与swiperUserList中的item出现引用的关系,否则会十分混乱。

给每一个item添加了一个id是为了作为遍历时独一无二的key

接下来则是要获取该item的宽度clientWidth,然后计算出该item的尾部出现的时间endShowTime,以及该item完全走完消失的时间disappearTime

在该item尾部出现的时候,就让下一个item push到swiperUserListShow中,使其出现在跑马灯中,在该item完全跑完消失的时候就让这个item从swiperUserListShow中剔除。

    emitItem() {
      if (!this.isShow) {
        return;
      }
      let swiperUser = this.swiperUserList.shift();
      this.swiperUserList.push(swiperUser);
      this.swiperUserListShow.push(
        Object.assign({}, { ...swiperUser, id: this.swiperId })
      );
      this.swiperId += 1;
      this.$nextTick(() => {
        let elm = this.$refs.swiperUserList[this.swiperUserListShow.length - 1];

        let elmWidth = elm.clientWidth || 0;

        let disappearTime = (elmWidth + this.animationWrapperWidth) / 60;
        let endShowTime = elmWidth / 60;

        let moveItem =
          this.swiperUserListShow[this.swiperUserListShow.length - 1];
        elm.style.transition = `transform ${disappearTime}s linear`;
        elm.style.transform = 'translate(-100%,-50%)';
        // this.clearTimer(moveItem)
        moveItem.endShowTimer = window.setTimeout(() => {
          clearTimeout(moveItem.endShowTimer);
          moveItem.endShowTimer = null;
          this.emitItem();
        }, endShowTime * 1000);

        moveItem.disappearTimer = window.setTimeout(() => {
          clearTimeout(moveItem.disappearTimer);
          moveItem.disappearTimer = null;
          this.swiperUserListShow.shift();
        }, disappearTime * 1000);
      });
    },

基本上就已经实现了。

为什么说是基本?

因为有两个坑。

看看坑

第一个是我们用了setTimeout,在我们将页面切到后台的时候,setTimeout里的代码是挂起的,不会执行,但是页面上的动画还是会继续执行的

elm.style.transition = `transform ${disappearTime}s linear`;
elm.style.transform = 'translate(-100%,-50%)';

所以,为了解决这个bug,需要监听是否切出切入后台,切到后台则清除所有setTimeout和清空swiperUserListShow列表,切回页面,再重新执行emitItem。

mounted() {
  this.$nextTick(() => {
     this.animationWrapperWidth = this.$refs.animateWrapper.clientWidth;
     this.emitItem();
   });
   
   // 处理退出前台,跑马灯还在跑的问题,隐藏就是直接清空展示列表
    document.addEventListener('visibilitychange', () => {
      const isShow = document.visibilityState === 'visible'
      this.handleSwiperListShow(isShow);
    });
}
  methods: {

    // 处理跑马灯展示列表和清除计时器
    handleSwiperListShow(isShow) {
      if (isShow) {
        this.emitItem();
      } else {
        this.swiperUserListShow.forEach((item) => {
          clearTimeout(item.endShowTimer);
          clearTimeout(item.disappearTimer);
        });
        this.swiperUserListShow = [];
      }
    },
  }

第二个坑是我们使用了clientWidth来获取item的宽度,当我们页面中有tab的时候,并且跑马灯在某个tab下,然后当前v-show是激活的是其他tab,则会导致跑马灯被隐藏,则获取不到item的宽度,这时的clientWidth的值为0.导致计算出来的endShowTime的值为0,则会导致疯狂执行settimeout里面的内容

为了解决这个bug则需要在父组件中传入isShow来判断跑马灯这个页面是否被隐藏

  props: {
    isShow: {
      type: Boolean,
      default: false
    },
  }

然后监听isShow

 watch: {

    // 处理tab选项卡隐藏抽奖模块,获取不到item clientWith的问题,隐藏就是直接清空展示列表
    isShow(newVal, oldVal) {
      this.handleSwiperListShow(newVal)
    }
  },

至此,优化过程就到此完美结束了。

其实还有个比较简单的优化方法,但是不适用于我这个场景,但是也分享一下。

就是依然使用css的animation动画属性,然后使用animationEnd的监听事件,

其他优化方案

当监听到结束的时候,利用v-if把当前跑马灯销毁,然后就往swiperUserListShow中push两个item,再生成展示跑马灯,又实现animation动画,这样是一个实现起来十分方便的方案,但是由于同一时刻只有我们push的item数,而且需要跑完才继续展示下两个,会留下一片空白,就有的不连贯的感觉,所以不使用这种方案。

从栈、堆、预解析来解释闭包原理(读浏览器核心原理)

1.下面三段代码会执行结果什么不同

function foo() {
  foo() // 是否存在堆栈溢出错误?
}
foo()
function foo() {
setTimeout(foo, 0) // 是否存在堆栈溢出错误?
}
function foo() {
return Promise.resolve().then(foo)
}
foo()

A:

  • 第一段:V8就会报告 栈溢出的错误
  • 第二段:正确执⾏
  • 第三段:没有栈溢出的错误,却会造成⻚⾯的卡死

2.为什么第一段会栈溢出

由于foo函数内部嵌套调⽤它⾃⼰,所以在调⽤foo函数的时候,它的栈会⼀直向上增⻓,但是由于栈空间在内存中是连续的,所以通常我们都会限制调⽤栈的⼤⼩,如果当函数嵌套层数过深时,过多的执⾏上下⽂堆积在栈中便会导致栈溢出,最终如下图所⽰:

3.为什么第二段会正常

setTimeout的本质是将同步函数调⽤改成异步函数调⽤,这⾥的异步调⽤是将foo封装成事件,并将其添加进 「消息队列」中,然后主线程再按照⼀定规则循环地从消息队列中读取下⼀个任务。

⾸先,主线程会从消息队列中取出需要执⾏的宏任务,假设当前取出的任务就是要执⾏的这段代码,这时候主线程便会进⼊代码的执⾏状态。这时关于主线程、消息队列、调⽤栈的关系如下图所⽰

接下来V8就要执⾏foo函数了,同样执⾏foo函数时,会创建foo函数的执⾏上下⽂,并将其压⼊栈中,最终
效果如下图所⽰:

当V8执⾏执⾏foo函数中的setTimeout时,setTimeout会将foo函数封装成⼀个新的宏任务,并将其添加到消息队列中,在V8执⾏setTimeout函数时的状态图如下所⽰:

等foo函数执⾏结束,V8就会结束当前的宏任务,调⽤栈也会被清空,调⽤栈被清空后状态如下图所⽰

当⼀个宏任务执⾏结束之后,忙碌的主线程依然不会闲下来,它会⼀直重复这个取宏任务、执⾏宏任务的过程。刚才通过setTimeout封装的回调宏任务,也会在某⼀时刻被主线取出并执⾏,这个执⾏过程,就是foo函数的调⽤过程。具体⽰意图如下所⽰:

因为foo函数并不是在当前的⽗函数内部被执⾏的,⽽是封装成了宏任务,并丢进了消息队列中,然后等待
主线程从消息队列中取出该任务,再执⾏该回调函数foo,这样就解决了栈溢出的问题。

4.为什么第三段会卡住页面

理解微任务的执⾏时机,你只需要记住以下两点:

  • ⾸先,如果当前的任务中产⽣了⼀个微任务,通过Promise.resolve()或者Promise.reject()都会触发微任务,触发的微任务不会在当前的函数中被执⾏,所以执⾏微任务时,不会导致栈的⽆限扩张;
  • 其次,和异步调⽤不同,微任务依然会在当前任务执⾏结束之前被执⾏,这也就意味着在当前微任务执⾏结束之前,消息队列中的其他任务是不可能被执⾏的

因此在函数内部触发的微任务,⼀定⽐在函数内部触发的宏任务要优先执⾏。

当执⾏foo函数时,由于foo函数中调⽤了Promise.resolve(),这会触发⼀个微任务,那么此时,V8会将该微任务添加进微任务队列中,退出当前foo函数的执⾏。

然后,V8在准备退出当前的宏任务之前,会检查微任务队列,发现微任务队列中有⼀个微任务,于是先执⾏微任务。由于这个微任务就是调⽤foo函数本⾝,所以在执⾏微任务的过程中,需要继续调⽤foo函数,在执⾏foo函数的过程中,⼜会触发了同样的微任务。

那么这个循环就会⼀直持续下去,当前的宏任务⽆法退出,也就意味着消息队列中其他的宏任务是⽆法被执⾏的,⽐如通过⿏标、键盘所产⽣的事件。这些事件会⼀直保存在消息队列中,⻚⾯⽆法响应这些事件,具体的体现就是⻚⾯的卡死。

不过,由于V8每次执⾏微任务时,都会退出当前foo函数的调⽤栈,所以这段代码是不会造成栈溢出的。

5.为什么使⽤栈结构来管理函数调⽤?

我们都知道,v8执行JavaScript时存在预编译和执行可执行代码两个部分。

我们知道,⼤部分⾼级语⾔都不约⽽同地采⽤栈这种结构来管理函数调⽤,为什么呢?这与函数的特性有关。通常函数有两个主要的特性:

  1. 第⼀个特点是函数 「可以被调⽤」,你可以在⼀个函数中调⽤另外⼀个函数,当函数调⽤发⽣时,执⾏代码的控制权将从⽗函数转移到⼦函数,⼦函数执⾏结束之后,⼜会将代码执⾏控制权返还给⽗函数;
  2. 第⼆个特点是函数 「具有作⽤域机制」,所谓作⽤域机制,是指函数在执⾏的时候可以将定义在函数内部的变量和外部环境隔离,在函数内部定义的变量我们也称为 「临时变量」,临时变量只能在该函数中被访问,外部函数通常⽆权访问,当函数执⾏结束之后,存放在内存中的临时变量也随之被销毁。

6.栈如何管理函数调⽤?

(这个问题即使不理解也不影响下面的,可以跳过,因为我觉得后面闭包的问题,真的有意思,只不过理解这个,能加深堆闭包的理解)

int add(num1,num2){
  int x = num1;
  int y = num2;
  int ret = x + y;
  return ret;
}
int main()
{
  int x = 5;
  int y = 6;
  x = 100;
  int z = add(x+y);
  return z;
}

观察上⾯这段代码,当执⾏到int z = add(x,y)时,当前栈的

状态如下所⽰:

接下来,就要调⽤add函数了,理想状态下,执⾏add函数的过程是下⾯这样的:

当执⾏到add函数时,会先把参数num1和num2压栈,接着我们再把变量x、y、ret的值依次压栈,不过执⾏这⾥,会遇到⼀个问题,那就是当add函数执⾏完成之后,需要将执⾏代码的控制权转交给main函数,这意味着需要将栈的状态恢复到main函数上次执⾏时的状态,我们把这个过程叫 「恢复现场」。那么应该怎么恢复main函数的执⾏现场呢?

其实⽅法很简单,只要在寄存器中保存⼀个永远指向当前栈顶的指针,栈顶指针的作⽤就是告诉你应该往哪个位置添加新元素,这个指针通常存放在esp寄存器中。如果你想往栈中添加⼀个元素,那么你需要先根据esp寄存器找到当前栈顶的位置,然后在栈顶上⽅添加新元素,新元素添加之后,还需要将新元素的地址更新到esp寄存器中。

当add函数执⾏结束时,只需要将栈顶指针向下移动就可以了

这里又有一个问题,那就是add函数执行完毕后,esp指针怎么知道移到下面的哪里呢?

这时又来了一个栈帧指针ebp。ebp指向当前执行函数的初始位置。

在main函数调⽤add函数的时候,main函数的栈顶指针就变成了add函数的栈帧指针,所以需要将main函数的栈顶指针保存到ebp中,当add函数执⾏结束之后,我需要销毁add函数的栈帧,并恢复main函数的栈帧,那么只需要取出main函数的栈顶指针写到esp中即可(main函数的栈顶指针是保存在ebp中的),这就相当于将栈顶指针移动到main函数的区域。

这里调用栈里只有两个函数,因此只需要一个ebp指向main函数顶部,一个esp指向栈顶。那要是两个以上呢? 我刚开始以为是要一个函数就对应一个ebp,然后这个epb指向该函数的顶部。发现不是我所想的这样,v8的处理非常巧妙。他是直接把调用栈中的下一个函数的顶部保存在上一个函数中的顶部。看图

这样,当add函数执行完毕的时候,就把此时的ebp的值赋值给esp,而将main函数顶部内存的值赋值给epb,这样,epb就指向下一个函数的顶部了。

(这个即使不理解也不影响下面的,可以继续看)

7.既然有了栈,为什么还要堆?

使⽤栈有⾮常多的优势:

  1. 栈的结构和⾮常适合函数调⽤过程。
  2. 在栈上分配资源和销毁资源的速度⾮常快,这主要归结于栈空间是连续的,分配空间和销毁空间只需要移动下指针就可以了。

虽然操作速度⾮常快,但是栈也是有缺点的,其中最⼤的缺点也是它的优点所造成的,那就是栈是连续的,所以要想在内存中分配⼀块连续的⼤空间是⾮常难的,因此栈空间是有限的。

因为栈空间是有限的,这就导致我们在编写程序的时候,经常⼀不⼩⼼就会导致栈溢出,⽐如函数循环嵌套层次太多,或者在栈上分配的数据过⼤,都会导致栈溢出,基于栈不⽅便存放⼤的数据,因此我们使⽤了另外⼀种数据结构⽤来保存⼀些⼤数据,这就是 「

和栈空间不同,存放在堆空间中的数据是不要求连续存放的,从堆上分配内存块没有固定模式的,你可以在
任何时候分配和释放它

8.什么是惰性解析

在编译JavaScript代码的过程中,V8并不会⼀次性将所有的JavaScript解析为中间代码,这主要是基于以下
两点:

  1. ⾸先,如果⼀次解析和编译所有的JavaScript代码,过多的代码会增加编译时间,这会严重影响到⾸次执⾏JavaScript代码的速度,让⽤⼾感觉到卡顿。因为有时候⼀个⻚⾯的JavaScript代码都有10多兆,如果要将所有的代码⼀次性解析编译完成,那么会⼤⼤增加⽤⼾的等待时间;
  2. 其次,解析完成的字节码和编译之后的机器代码都会存放在内存中,如果⼀次性解析和编译所有JavaScript代码,那么这些中间代码和机器代码将会⼀直占⽤内存,特别是在⼿机普及的年代,内存是⾮常宝贵的资源。

基于以上的原因,所有主流的JavaScript虚拟机都实现了 惰性解析。所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其⽣成AST和字节码,⽽仅仅⽣成顶层代码的AST和字节码。

就像是我们经常解释变量提升时候的预编译阶段。

9.惰性解析的过程是怎样的呢

我们可以结合下⾯这个例⼦来分析下:

function foo(a,b) {
  var d = 100
  var f = 10
  return d + f + a + b;
}
var a = 1
var c = 4
foo(1, 5)

当把这段代码交给V8处理时,V8会⾄上⽽下解析这段代码,在解析过程中⾸先会遇到foo函数,由于这只是⼀个函数声明语句,V8在这个阶段只需要将该函数转换为函数对象,如下图所⽰:

注意,这⾥只是将该函数声明转换为函数对象,但是并有没有解析和编译函数内部的代码,所以也不会为foo函数的内部代码⽣成抽象语法树。

然后继续往下解析,由于后续的代码都是顶层代码,所以V8会为它们⽣成抽象语法树,最终⽣成的结果如下所⽰:

代码解析完成之后,V8便会按照顺序⾃上⽽下执⾏代码,⾸先会先执⾏“a=1”和“c=4”这两个赋值表达式,接下来执⾏foo函数的调⽤,过程是从foo函数对象中取出函数代码,然后和编译顶层代码⼀样,V8会先编译foo函数的代码,编译时同样需要先将其编译为抽象语法树和字节码,然后再解释执⾏。

好了,上⾯就是惰性解析的⼀个⼤致过程,看上去是不是很简单,不过在V8实现惰性解析的过程中,需要⽀持JavaScript中的闭包特性,这会使得V8的解析过程变得异常复杂。为什么闭包会让V8解析代码的过程变得复杂呢?要解答这个问题,我们先来拆解闭包的特性,然后再来分析为什么闭包影响到了V8的解析流程。

10.闭包有哪三个特性

  1. 可以在JavaScript函数内部定义新的函数;
  2. 内部函数中访问⽗函数中定义的变量;
  3. 因为JavaScript中的函数是⼀等公⺠,所以函数可以作为另外⼀个函数的返回值。

11.那闭包给惰性解析带来什么问题呢

function foo() {
  var d = 20
  return function inner(a, b) {
    const c = a + b + d
    return c
  }
}
const f = foo()

观察上⾯上⾯这段代码,我们在foo函数中定义了inner函数,并返回inner函数,同时在inner函数中访问了
foo函数中的变量d。

我们可以分析下上⾯这段代码的执⾏过程:

  1. 当调⽤foo函数时,foo函数会将它的内部函数inner返回给全局变量f;
  2. 然后foo函数执⾏结束,执⾏上下⽂被V8销毁了;
  3. 虽然foo函数的执⾏上下⽂被销毁了,但是依然存活的inner函数引⽤了foo函数作⽤域中的变量d。

按照通⽤的做法,d已经被v8销毁了,但是由于存活的函数inner依然引⽤了foo函数中的变量d,这样就会带来两个问题:

  1. 当foo执⾏结束时,变量d该不该被销毁?如果不应该被销毁,那么应该采⽤什么策略?
  2. 如果采⽤了惰性解析,那么当执⾏到foo函数时,V8只会解析foo函数,并不会解析内部的inner函数,那么这时候V8就不知道inner函数中是否引⽤了foo函数的变量d。

12.怎么处理闭包带来的问题

在执⾏foo函数的阶段,虽然采取了惰性解析,不会解析和执⾏foo函数中的inner函数,但是V8还是需要判断inner函数是否引⽤了foo函数中的变量,负责处理这个任务的模块叫着预解析器。V8引⼊预解析器,⽐如当解析顶层代码的时候,遇到了⼀个函数,那么预解析器并不会直接跳过该函数,⽽是对该函数做⼀次快速的预解析,其主要⽬的有两个。

第⼀,是判断当前函数是不是存在⼀些语法上的错误,如下⾯这段代码:

function foo(a, b) {
  {/} //语法错误
}
var a = 1
var c = 4
foo(1, 5)

在预解析过程中,预解析器发现了语法错误,那么就会向V8抛出语法错误第⼆,除了检查语法错误之外,预解析器另外的⼀个重要的功能就是检查函数内部是否引⽤了外部变量,如果引⽤了外部的变量,预解析器会将栈中的变量复制到堆中,在下次执⾏到该函数的时候,直接使⽤堆中的引⽤,这样就解决了闭包所带来的问题。

function a(){
  let str = '123';
  function b(){
    console.log(str);
  }
  
}

13. 当调⽤foo函数时,foo函数内部的变量a会分别分配到栈上?还是堆上?

function foo() {
var a = 0
return function inner() {
  return a++
  }
}

变量a同时在栈和堆上,当解析foo函数的时候,预解析有发现内部函数引⽤外部变量a,这时候就会把a复制到堆上,当⽗函数执⾏到a的赋值语句时,会同时修改?栈和堆上的变量a的值,⽗函数销毁的时候也只会销毁栈上的变量a,堆上的变量a保留。最后当内部函数执⾏完后,堆上的变量a就没有再被引⽤,就会被垃圾回收掉。

实际上每个函数都有一个[[scope]]属性,当执行到预解析inner判断有用到外部的d时,就会给[[scope]]属性添加一个对象Closure(foo),如图所示

14.真的要把内部函数return 出去才算闭包吗?

我请教过很多人,大部人说要符合上面所说的三个特性,即要return出去,就算闭包,但是这个从上面闭包的原理看来,预解析阶段,并不关♥ 内部函数有没有被return出去。只是判断内部函数有没有引用内部变量。因此,我深不以为然,于是就实践下。不出所料,看下面代码,并没有return出去,但还是生成了闭包

手写Express核心原理,再也不怕被问Express原理


theme: jzman

一、首先安装express

npm install express

安装express是为了示范。

已经把代码放到github:https://github.com/Sunny-lucking/HowToBuildMyExpress 。可以顺手给个star吗?谢谢大佬们。

二、创建example.js文件

// example.js
const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

如代码所示,执行node example.js就运行起了一个服务器。


如下图所示,现在我们决定创建一个属于我们的express文件,引入的express改成引入我们手写的express。

好了,现在开始实现我们的express吧!

创建myExpress.js文件

const express = require('express')
const app = express()

由 这两句代码,我们可以知道,express得到的是一个方法,然后方法执行后得到了app。而app实际上也是一个函数,至于为什么会是函数,我们下面会揭秘。

我们可以初步实现express如下:

// myExpress.js
function createApplication() {
    let app = function (req,res) {

    }
    return app;
}

module.exports = createApplication;

在上面代码中,发现app有listen方法。

因此我们可以进一步给app添加listen方法:

// myExpress.js
function createApplication() {
    let app = function (req,res) {

    }
    app.listen = function () {

    }
    return app;
}

module.exports = createApplication;

app.listen实现的是创建一个服务器,并且将服务器绑定到某个端口运行起来。

因此可以这样完善listen方法。

// myExpress.js
let http = require('http');
function createApplication() {
    let app = function (req,res) {
        res.end('hahha');
    }
    app.listen = function () {
        let server = http.createServer(app)
        server.listen(...arguments);

    }
    return app;
}

module.exports = createApplication;

这里可能会有同学有所疑问,为什么 http.createServer(app)这里要传入app。

其实我们不传入app,也就是说,让app不是一个方法,也是可以的。

我们可以改成这样。

// myExpress.js
let http = require('http');
function createApplication() {
    let app = {};

    app.listen = function () {
        let server = http.createServer(function (req, res) {
            res.end('hahha')
        })
        server.listen(...arguments);

    }
    return app;
}

module.exports = createApplication;

如代码所示,我们将app改成一个对象,也是没有问题的。

.

实现app.get()方法

app.get方法接受两个参数,路径和回调函数。

// myExpress.js
let http = require('http');
function createApplication() {
    let app = {};
    app.routes = []
    app.get = function (path, handler) {
        let layer = {
            method: 'get',
            path,
            handler
        }
        app.routes.push(layer)
    }
    app.listen = function () {
        let server = http.createServer(function (req, res) {
            
            res.end('hahha')
        })
        server.listen(...arguments);
    }
    return app;
}

module.exports = createApplication;

如上面代码所示,给app添加了route对象,然后get方法执行的时候,将接收到的两个参数:路径和方法,包装成一个对象push到routes里了。

可想而知,当我们在浏览器输入路径的时候,肯定会执行http.createServer里的回调函数。

所以,我们需要在这里 获得浏览器的请求路径。解析得到路径.

然后遍历循环routes,寻找对应的路由,执行回调方法。如下面代码所示。

// myExpress.js
let http = require('http');
const url  = require('url');
function createApplication() {
    let app = {};
    app.routes = []
    app.get = function (path, handler) {
        let layer = {
            method: 'get',
            path,
            handler
        }
        app.routes.push(layer)
    }
    app.listen = function () {
        let server = http.createServer(function (req, res) {
            // 取出layer 
            // 1. 获取请求的方法
            let m = req.method.toLocaleLowerCase();
            let { pathname } = url.parse(req.url, true);
            
            // 2.找到对应的路由,执行回调方法
            for (let i = 0 ; i< app.routes.length; i++){
                let {method,path,handler} = app.routes[i]
                if (method === m && path === pathname ) {
                    handler(req,res);
                }
            }
            res.end('hahha')
        })
        server.listen(...arguments);
    }
    return app;
}

module.exports = createApplication;

运行一下代码。

可见运行成功:

实现post等其他方法。

很简单,我们可以直接复制app.get方法,然后将method的值改成post就好了。

// myExpress.js
let http = require('http');
const url  = require('url');
function createApplication() {
    。。。
    app.get = function (path, handler) {
        let layer = {
            method: 'get',
            path,
            handler
        }
        app.routes.push(layer)
    }
    app.post = function (path, handler) {
        let layer = {
            method: 'post',
            path,
            handler
        }
        app.routes.push(layer)
    }
    。。。
    return app;
}

module.exports = createApplication;

这样是可以实现,但是除了post和get,还有其他方法啊,难道每一个我们都要这样写嘛?,当然不是,有个很简单的方法。

// myExpress.js

function createApplication() {
    ... 
    http.METHODS.forEach(method => {
        method = method.toLocaleLowerCase()
        app[method] = function (path, handler) {
            let layer = {
                method,
                path,
                handler
            }
            app.routes.push(layer)
        }
    });
    ...
}

module.exports = createApplication;

如代码所示,http.METHODS是一个方法数组。如下面所示的数组

["GET","POST","DELETE","PUT"]。

遍历方法数组,就可以实现所有方法了。

测试跑了一下,确实成功。

实现app.all方法

all表示的是匹配所有的方法,

app.all('/user')表示匹配所有路径是/user的路由

app.all('*')表示匹配任何路径 任何方法 的 路由

实现all方法也非常简单,如下代码所示

app.all = function (path, handler){
        let layer = {
            method: "all",
            path,
            handler
        }
        app.routes.push(layer)
    }

然后只需要续改下路由器匹配的逻辑,如下代码所示,只需要修改下判断。

app.listen = function () {
    let server = http.createServer(function (req, res) {
        // 取出layer 
        // 1. 获取请求的方法
        let m = req.method.toLocaleLowerCase();
        let { pathname } = url.parse(req.url, true);

        // 2.找到对应的路由,执行回调方法
        for (let i = 0 ; i< app.routes.length; i++){
            let {method,path,handler} = app.routes[i]
            if ((method === m || method === 'all') && (path === pathname || path === "*")) {
                handler(req,res);
            }
        }
        console.log(app.routes);
        res.end('hahha')
    })
    server.listen(...arguments);
}


可见成功。

中间件app.use的实现

这个方法的实现,跟其他方法差不多,如代码所示。

app.use = function (path, handler) {
    let layer = {
        method: "middle",
        path,
        handler
    }
    app.routes.push(layer)
}

但问题来了,使用中间件的时候,我们会使用next方法,来让程序继续往下执行,那它是怎么执行的。

app.use(function (req, res, next) {
  console.log('Time:', Date.now());
  next();
});

所以我们必须实现next这个方法。

其实可以猜想,next应该就是一个疯狂调用自己的方法。也就是递归

而且每递归一次,就把被push到routes里的handler拿出来执行。

实际上,不管是app.use还说app.all还是app.get。其实都是把layer放进routes里,然后再统一遍历routes来判断该不该执行layer里的handler方法。可以看下next方法的实现。

function next() {
    // 已经迭代完整个数组,还是没有找到匹配的路径
    if (index === app.routes.length) return res.end('Cannot find ')
    let { method, path, handler } = app.routes[index++] // 每次调用next就去下一个layer
    if (method === 'middle') { // 处理中间件
        if (path === '/' || path === pathname || pathname.starWidth(path + '/')) {
            handler(req, res, next)
        } else { // 继续遍历
            next();
        }
    } else { // 处理路由
        if ((method === m || method === 'all') && (path === pathname || path === "*")) {
            handler(req, res);
        } else {
            next();
        }
    }
}

可以看到是递归方法的遍历routes数组。

而且我们可以发现,如果是使用中间件的话,那么只要path是“/”或者前缀匹配,这个中间件就会执行。由于handler会用到参数req和res。所以这个next方法要在 listen里面定义。

如下代码所示:

// myExpress.js
let http = require('http');
const url = require('url');
function createApplication() {
    let app = {};
    app.routes = [];
    let index = 0;

    app.use = function (path, handler) {
        let layer = {
            method: "middle",
            path,
            handler
        }
        app.routes.push(layer)
    }
    app.all = function (path, handler) {
        let layer = {
            method: "all",
            path,
            handler
        }
        app.routes.push(layer)
    }
    http.METHODS.forEach(method => {
        method = method.toLocaleLowerCase()
        app[method] = function (path, handler) {
            let layer = {
                method,
                path,
                handler
            }
            app.routes.push(layer)
        }
    });
    app.listen = function () {
        let server = http.createServer(function (req, res) {
            // 取出layer 
            // 1. 获取请求的方法
            let m = req.method.toLocaleLowerCase();
            let { pathname } = url.parse(req.url, true);

            // 2.找到对应的路由,执行回调方法
            function next() {
                // 已经迭代完整个数组,还是没有找到匹配的路径
                if (index === app.routes.length) return res.end('Cannot find ')
                let { method, path, handler } = app.routes[index++] // 每次调用next就去下一个layer
                if (method === 'middle') { // 处理中间件
                    if (path === '/' || path === pathname || pathname.starWidth(path + '/')) {
                        handler(req, res, next)
                    } else { // 继续遍历
                        next();
                    }
                } else { // 处理路由
                    if ((method === m || method === 'all') && (path === pathname || path === "*")) {
                        handler(req, res);
                    } else {
                        next();
                    }
                }
            }

            next()
            res.end('hahha')
        })
        server.listen(...arguments);
    }
    return app;
}

module.exports = createApplication;

当我们请求路径就会发现中间件确实执行成功。

不过,这里的中间价实现还不够完美。

因为,我们使用中间件的时候,是可以不用传递路由的。例如:

app.use((req,res) => {
  console.log("我是没有路由的中间价");
})

这也是可以使用的,那该怎么实现呢,其实非常简单,判断一下有没有传递路径就好了,没有的话,就给个默认路径“/”,实现代码如下:

app.use = function (path, handler) {
    if(typeof path !== "string") { // 第一个参数不是字符串,说明不是路径,而是方法
        handler = path;
        path = "/"
    }
    let layer = {
        method: "middle",
        path,
        handler
    }
    app.routes.push(layer)
}

看,是不是很巧妙,很容易。

我们试着访问路径“/middle”


咦?第一个中间件没有执行,为什么呢?

对了,使用中间件的时候,最后要执行next(),才能交给下一个中间件或者路由执行。

当我们请求“/middle”路径的时候,可以看到确实请求成功,中间件也成功执行。说明我们的逻辑没有问题。

实际上,中间件已经完成了,但是别忘了,还有个错误中间件?

什么是错误中间件?

错误处理中间件函数的定义方式与其他中间件函数基本相同,差别在于错误处理函数有四个自变量而不是三个,专门具有特征符 (err, req, res, next):

app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

当我们的在执行next()方法的时候,如果抛出了错误,是会直接寻找错误中间件执行的,而不会去执行其他的中间件或者路由。

举个例子:

如图所示,当第一个中间件往next传递参数的时候,表示执行出现了错误。
然后就会跳过其他陆游和中间件和路由,直接执行错误中间件。当然,执行完错误中间件,就会继续执行后面的中间件。

例如:

如图所示,错误中间件的后面那个是会执行的。

那原理该怎么实现呢?

很简单,直接看代码解释,只需在next里多加一层判断即可:

function next(err) {
    // 已经迭代完整个数组,还是没有找到匹配的路径
    if (index === app.routes.length) return res.end('Cannot find ')
    let { method, path, handler } = app.routes[index++] // 每次调用next就去下一个layer
    if( err ){ // 如果有错误,应该寻找中间件执行。
        if(handler.length === 4) { //找到错误中间件
            handler(err,req,res,next)
        }else { // 继续徐州
            next(err) 
        }
    }else {
        if (method === 'middle') { // 处理中间件
            if (path === '/' || path === pathname || pathname.starWidth(path + '/')) {
                handler(req, res, next)
            } else { // 继续遍历
                next();
            }
        } else { // 处理路由
            if ((method === m || method === 'all') && (path === pathname || path === "*")) {
                handler(req, res);
            } else {
                next();
            }
        }
    }
}

看代码可见在next里判断err有没有值,就可以判断需不需要查找错误中间件来执行了。

如图所示,请求/middle路径,成功执行。

到此,express框架的实现就大功告成了。

学习总结

通过这次express手写原理的实现,更加深入地了解了express的使用,发现:

  1. 中间件和路由都是push进一个routes数组里的。
  2. 当执行中间件的时候,会传递next,使得下一个中间件或者路由得以执行
  3. 当执行到路由的时候就不会传递next,也使得routes的遍历提前结束
  4. 当执行完错误中间件后,后面的中间件或者路由还是会执行的。

最后

欢迎 关注公众号《前端阳光》,有更多的手写原理文章,也可以加入技术交流群和内推群,公众号收集了各厂的内推码,快来获取吧!

关于【Promise】十问 (读《ES6入门标准》)

@[TOC]((立下flag)-16 关于【Promise】十问)

第一问:了解 Promise 吗?

  1. 了解Promise,Promise是一种异步编程的解决方案,有三种状态,pending(进行中)、resolved(已完成)、rejected(已失败)。当Promise的状态由pending转变为resolved或reject时,会执行相应的方法

  2. Promised的特点是只有异步操作的结果,可以决定当前是哪一种状态,任务其他操作都无法改变这个状态,也是“Promise”的名称的由来,同时,状态一旦改变,就无法再次改变状态

第二问:Promise 解决的痛点是什么?

Promise解决的痛点:

1)回调地狱,代码难以维护, 常常第一个的函数的输出是第二个函数的输入这种现象,是为解决异步操作函数里的嵌套回调(callback hell)问题,代码臃肿,可读性差,只能在回调里处理异常

2)promise可以支持多个并发的请求,获取并发请求中的数据

3)promise可以解决可读性的问题,异步的嵌套带来的可读性的问题,它是由异步的运行机制引起的,这样的代码读起来会非常吃力

4)promise可以解决信任问题,对于回调过早、回调过晚或没有调用和回调次数太少或太多,由于promise只能决议一次,决议值只能有一个,决议之后无法改变,任何then中的回调也只会被调用一次,所以这就保证了Promise可以解决信任问题
5)Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清晰地分离了

第三问:Promise 解决的痛点还有其他方法可以解决吗?如果有,请列举。

1)Promise 解决的痛点还有其他方法可以解决,比如setTimeout、事件监听、回调函数、Generator函数,async/await

2)setTimeout:缺点不精确,只是确保在一定时间后加入到任务队列,并不保证立马执行。只有执行引擎栈中的代码执行完毕,主线程才会去读取任务队列

3)事件监听:任务的执行不取决于代码的顺序,而取决于某个事件是否发生

4)Generator函数虽然将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。即如何实现自动化的流程管理

5)async/await

第四问:Promise 如何使用?

1)创造一个Promise实例

2)Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数

3)可用Promise的try和catch方法预防异常

第五问:Promise 的业界实现都有哪些?

1) promise可以支持多个并发的请求,获取并发请求中的数据

2)promise可以解决可读性的问题,异步的嵌套带来的可读性的问题,它是由异步的运行机制引起的,这样的代码读起来会非常吃力

第六问: Promise的问题?解决办法?

promise的问题为:

promise一旦执行,无法中途取消

promise的错误无法在外部被捕捉到,只能在内部进行预判处理

promise的内如何执行,监测起来很难

解决办法

正是因为这些原因,ES7引入了更加灵活多变的async,await来处理异步

第七问:老旧浏览器没有Promise全局对象增么办?

果辛辛苦苦写完代码,测试后发现不兼容IE6、7增么办?难道要推翻用回调函数重写?当然不是这样,轮子早就造好了。

我们可以使用es6-promise-polyfill。es6-promise-polyfill可以使用页面标签直接引入,可以通过es6的import方法引入(如果你是用webpack),在node中可以使用require引入,也可以在Seajs中作为依赖引入。

引入这个polyfill之后,它会在window对象中加入Promise对象。这样我们就可以全局使用Promise了。

第八问:怎么让一个函数无论promise对象成功和失败都能被调用?

笨方法:

在两个回调中分别执行一次函数。

推荐方式:

扩展一个 Promise.finally(),finally方法用于指定不管Promise对象最后状态如何,都会执行的操作,它与done方法的最大区别在于,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。

//添加finally方法
Promise.prototype.finally=function (callback) {
   var p=this.constructor;
   return this.then(//只要是promise对象就可以调用then方法
     value => p.resolve(callback()).then(() => value),
     reason => p.resolve(callback()).then(() => {throw reason})
   );
}

对finally方法的理解:
(1)p.resolve(callback())这句函数callback已经执行
(2)finally方法return的是一个promise对象,所以还可以继续链式调用其他方法
(3)对于Promise.resolve方法

Promise.resolve('foo');
等价于
new Promise(resolve => resolve('foo'));

所以可以通过then方法的回调函数 接受 实例对象返回的参数
比如:

Promise.resolve(function(){console.log(2);}).then(function(cb){cb()}) 

第九问:红灯3秒亮一次,绿灯1秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用Promise实现)三个亮灯函数已经存在:

function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}

解析
红灯3秒亮一次,绿灯1秒亮一次 ,黄灯2秒亮一次,意思就是3秒执行一次red函数,2秒执行一次green函数,1秒执行一次yellow函数,不断交替重复亮灯,意思就是按照这个顺序一直执行这3个函数,这步可以利用递归来实现。
答案:

function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}

var light = function (timmer, cb) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            cb();
            resolve();
        }, timmer);
    });
};

var step = function () {
    Promise.resolve().then(function () {
        return light(3000, red);
    }).then(function () {
        return light(2000, green);
    }).then(function () {
        return light(1000, yellow);
    }).then(function () {
        step();
    });
}

step();

第十问:实现 mergePromise 函数,把传进去的数组按顺序先后执行,并且把返回的数据先后放到数组 data 中。

const timeout = ms => new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve();
    }, ms);
});

const ajax1 = () => timeout(2000).then(() => {
    console.log('1');
    return 1;
});

const ajax2 = () => timeout(1000).then(() => {
    console.log('2');
    return 2;
});

const ajax3 = () => timeout(2000).then(() => {
    console.log('3');
    return 3;
});

const mergePromise = ajaxArray => {
    // 在这里实现你的代码

};

mergePromise([ajax1, ajax2, ajax3]).then(data => {
    console.log('done');
    console.log(data); // data 为 [1, 2, 3]
});

// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]

解析
首先 ajax1 、ajax2、ajax3 都是函数,只是这些函数执行后会返回一个 Promise,按题目的要求我们只要顺序执行这三个函数就好了,然后把结果放到 data 中,但是这些函数里都是异步操作,想要按顺序执行,然后输出 1,2,3并没有那么简单,看个例子。

function A() {
    setTimeout(function () {
        console.log('a');
    }, 3000);
}

function B() {
    setTimeout(function () {
        console.log('b');
    }, 1000);
}

A();
B();

// b
// a

答案

// 保存数组中的函数执行后的结果
var data = [];

// Promise.resolve方法调用时不带参数,直接返回一个resolved状态的 Promise 对象。
var sequence = Promise.resolve();

ajaxArray.forEach(function (item) {
    // 第一次的 then 方法用来执行数组中的每个函数,
    // 第二次的 then 方法接受数组中的函数执行后返回的结果,
    // 并把结果添加到 data 中,然后把 data 返回。
    // 这里对 sequence 的重新赋值,其实是相当于延长了 Promise 链
    sequence = sequence.then(item).then(function (res) {
        data.push(res);
        return data;
    });
})

// 遍历结束后,返回一个 Promise,也就是 sequence, 他的 [[PromiseValue]] 值就是 data,
// 而 data(保存数组中的函数执行后的结果) 也会作为参数,传入下次调用的 then 方法中。
return sequence;

第十一问:封装一个异步加载图片的方法

function loadImageAsync(url) {
    return new Promise(function(resolve,reject) {
        var image = new Image();
        image.onload = function() {
            resolve(image) 
        };
        image.onerror = function() {
            reject(new Error('Could not load image at' + url));
        };
        image.src = url;
     });
}   

第十二问:手写promise

请看我的另一篇文章
一步一步实现自己的Promise

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.