├── JavaScript基础 ├── ES6中的Class是如何实现的.md ├── javascript按什么传递.md ├── javascript属性.md ├── javascript“作用域与闭包“.md ├── javascript“预解释”.md ├── javascript原型.md ├── javascript中的this.md ├── javascript实现深克隆.md ├── JavaScript基本类型.md ├── JavaScript中的不可变数据.md └── Proxy.md ├── README.md ├── 异步 ├── promise化函数.md ├── 如何实现一个Event.md ├── 异步机制.md └── Event.md ├── Process └── Unix-Process.md ├── IO ├── Buffer.md └── stream.md └── Framework └── V-DOM.md /JavaScript基础/ES6中的Class是如何实现的.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /JavaScript基础/javascript按什么传递.md: -------------------------------------------------------------------------------- 1 | # JavaScript是按引用传递吗? 2 | 3 | --- 4 | #### 1.JavaScript中的基本类型传递 5 | 6 | 一个我们经常遇到的问题:“JS中的值是按值传递,还是按引用传递呢?” 7 | 8 | 由于js中存在**复杂类型**和**基本类型**,对于**基本类型**而言,是按值传递的. 9 | 10 | ```javascript 11 | var a = 1; 12 | function test(x) { 13 | x = 10; 14 | console.log(x); 15 | } 16 | test(a); // 10 17 | console.log(a); // 1 18 | ``` 19 | 虽然在函数`test`中`a`被修改,并没有有影响到 20 | 外部`a`的值,基本类型是按值传递的. 21 | 22 | --- 23 | 24 | #### 2.复杂类型按引用传递? 25 | 我们将外部`a`作为一个对象传入`test`函数. 26 | ```javascript 27 | var a = { 28 | a: 1, 29 | b: 2 30 | }; 31 | function test(x) { 32 | x.a = 10; 33 | console.log(x); 34 | } 35 | test(a); // { a: 10, b: 2 } 36 | console.log(a); // { a: 10, b: 2 } 37 | 38 | ``` 39 | 可以看到,在函数体内被修改的`a`对象也同时影响到了外部的`a`对象,可见复杂类型是按**引用传递的**. 40 | 41 | 可是如果再做一个实验: 42 | ```javascript 43 | var a = { 44 | a: 1, 45 | b: 2 46 | }; 47 | function test(x) { 48 | x = 10; 49 | console.log(x); 50 | } 51 | test(a); // 10 52 | console.log(a); // { a: 1, b: 2 } 53 | ``` 54 | 外部的`a`并没有被修改,如果是按引用传递的话,由于共享同一个堆内存,`a`在外部也会表现为`10`才对. 55 | 此时的复杂类型同时表现出了`按值传递`和`按引用传递`的特性. 56 | 57 | --- 58 | #### 3.按共享传递 59 | 复杂类型之所以会产生这种特性,原因就是在传递过程中,对象`a`先产生了一个`副本a`,这个`副本a`并不是深克隆得到的`副本a`,`副本a`地址同样指向对象`a`指向的堆内存. 60 | 61 | ![](http://omrbgpqyl.bkt.clouddn.com/17-8-31/72507393.jpg) 62 | 63 | 因此在函数体中修改`x=10`只是修改了`副本a`,`a`对象没有变化. 64 | 但是如果修改了`x.a=10`是修改了两者指向的同一堆内存,此时对象`a`也会受到影响. 65 | 66 | 有人讲这种特性叫做**传递引用**,也有一种说法叫做**按共享传递**. 67 | 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 如何通过饿了么 Node.js 面试(解答) 2 | 3 | > *听说饿了么被阿里收购了,饿了么虽然没了,文档还是要更的...* 4 | 5 | 本项目是基于[饿了么node面试](https://github.com/ElemeFE/node-interview)而来,对上述教程中提出的问题进行了解答,本项目是为了记录本人对Node知识点的总结,没有权威性如有错误欢迎指出. 6 | 7 | ## 导读 8 | 9 | 本项目虽然是基于**饿了么Node面试教程**而来,但是在此基础上做了拓展,比如在**JS基础**部分加入了**面向对象** **原型链**等重要知识. 10 | 11 | > 由于原项目包括*知识点*和*常见问题*两个部分,我们会先整理知识点部分的文字,再在后面补充常见问题答案. 12 | 13 | ## [Js 基础问题] 14 | 15 | > 目前还缺少*Node内存*与*ES6*部分. 16 | 17 | * [`[Basic]` 类型判断](/JavaScript基础/JavaScript基本类型.md) 18 | * [`[Basic]` 预解释](/JavaScript基础/javascript“预解释”.md) 19 | * [`[Basic]` 作用域](/JavaScript基础/javascript“作用域与闭包“.md) 20 | * [`[Basic]` 原型](/JavaScript基础/javascript原型.md) 21 | * [`[Basic]` this](/JavaScript基础/javascript中的this.md) 22 | * [`[Basic]` 属性](/JavaScript基础/javascript属性.md) 23 | * [`[Basic]` 传递引用](/JavaScript基础/javascript按什么传递.md) 24 | * [`[Basic]` 深克隆](/JavaScript基础/javascript实现深克隆.md) 25 | * [`[Basic]` 实现不可变数据](/JavaScript基础/JavaScript中的不可变数据.md) 26 | * [`[Basic]` Proxy 与Object.defineProperty 的双向绑定对比](/JavaScript基础/Proxy.md) 27 | 28 | ## 事件/异步 29 | 30 | * [`[Basic]` Promise](https://github.com/xieranmaya/blog/issues/3) 31 | * [`[Basic]` Events (事件机制)](/异步/Event.md) 32 | * [`[Basic]` 实现一个Event](/异步/如何实现一个Event.md) 33 | * [`[Basic]` 阻塞/异步](/异步/异步机制.md) 34 | 35 | ## [IO] 36 | 37 | * [`[Doc]` Buffer详解](/IO/Buffer.md) 38 | * [`[Doc]` Stream (流)](/IO/stream.md) 39 | 40 | ## [进程] 41 | 42 | * [`[Doc]` 类Unix系统中的进程](/Process/Unix-Process.md) 43 | -------------------------------------------------------------------------------- /异步/promise化函数.md: -------------------------------------------------------------------------------- 1 | # 如何将node中默认的回调方式改为Promise 2 | 3 | --- 4 | 5 | ### 1. node默认的异步方式 6 | 7 | 8 | `nodejs`默认的异步方式是回调函数,由于没有提供`Promise`接口导致我们常用的例如`fs`方法无法使用最新的es6+语法. 9 | 10 | 例如我们读取一个文件: 11 | 12 | ```javascript 13 | fs.readFile('package.json', 'utf-8', (err, data) => { 14 | if (err) { 15 | throw err; 16 | } else { 17 | console.log(data); 18 | } 19 | }) 20 | ``` 21 | 22 | ### 2. 解决方案 23 | 24 | 目前通常的解决方案是引入npm包来对当前的函数进行包装,从而使得相关函数拥有更高级的功能. 25 | 26 | 众所周知的 `Bluebird` 拥有 `promisify` 这方法可以将回调函数风格转换为`Promise`方法. 27 | 28 | 比如`mz`模块就是一个使得低版本node拥有es6+标准的工具,当然它不仅仅可以使得回调函数拥有`Promise`能力,其它es6+标准中出现的新的api都可以通过它实现. 29 | 30 | 当然`pify`就是一个专门使得回调函数拥有`Promise`接口的模块了. 31 | 32 | ```javascript 33 | pify(fs.readFile)('package.json', 'utf8').then(data => { 34 | console.log(JSON.parse(data).name); 35 | }).catch(err => { 36 | throw err; 37 | }); 38 | ``` 39 | 40 | 当然如果在`Node v8.0.0`后可以使用`util.promisify(original)`. 41 | 42 | ```javascript 43 | const util = require('util'); 44 | const fs = require('fs'); 45 | 46 | const stat = util.promisify(fs.stat); 47 | stat('.').then((stats) => { 48 | // Do something with `stats` 49 | }).catch((error) => { 50 | // Handle the error. 51 | }); 52 | ``` 53 | 54 | ### 3. 源码实现 55 | 56 | 如果只是简单的实现其实难度并不是很大. 57 | 58 | ```javascript 59 | let promisify = (fn, receiver) => { 60 | return (...args) => { 61 | return new Promise((resolve, reject) => { 62 | fn.apply(receiver, [...args, (err, res) => { 63 | return err ? reject(err) : resolve(res); 64 | }]); 65 | }); 66 | }; 67 | }; 68 | ``` 69 | 例如读取一个项目名称: 70 | 71 | ```javascript 72 | const fs = require('fs'); 73 | 74 | const promisify = (fn, receiver) => { 75 | return (...args) => { 76 | return new Promise((resolve, reject) => { 77 | fn.apply(receiver, [...args, (err, res) => { 78 | return err ? reject(err) : resolve(res); 79 | }]); 80 | }); 81 | }; 82 | }; 83 | 84 | const readFilePromise = promisify(fs.readFile, fs); 85 | 86 | readFilePromise('./package.json', 'utf-8').then(data => { 87 | console.log(JSON.parse(data).name); 88 | }).catch(err => { 89 | throw err; 90 | }) 91 | 92 | ``` 93 | -------------------------------------------------------------------------------- /JavaScript基础/javascript属性.md: -------------------------------------------------------------------------------- 1 | # JavaScript面向对象之属性 2 | 3 | 4 | --- 5 | #### 1.JavaScript中的对象 6 | 7 | 8 | JavaScript中的对象一般分为三类:**内置对象**(Array, Error, Date等), **宿主对象**(对于前端来说指的是浏览器对象,例如window), **自定义对象**(指我们自己创建的对象). 9 | 10 | 因此,我们主要讨论的内容是围绕自定义对象展开的,今天我们就对象的属性进行深入地探究. 11 | 12 | --- 13 | 14 | #### 2.属性的创建 15 | 我们先定义一个对象,然后对其赋值: 16 | ```javascript 17 | var person = {}; 18 | person.name = "Messi"; 19 | 20 | ``` 21 | 以上操作相当于给`person`对象建立了一个`name`属性,且值为`'Messi'`. 22 | 23 | 那么这个赋值的过程具体的原理是什么呢? 24 | 25 | 首先,我们创建了一个'空'对象,之所以我们打上引号,是因为这并不是一个严格意义上的空对象,因为在建立这个对象的过程中,JavaScript已经为这个对象内置了方法和属性,当然是不可见的,在属性的建立过程中就调用了一个隐式的方法`[[put]]`. 26 | 27 | 大概的创建过程是,当属性第一次被创建时,对象调用内部方法`[[put]]`为对象创建一个节点保存属性. 28 | ```flow 29 | st=>start: person 30 | e=>end: persen.name = "Messi" 31 | io1=>inputoutput: [[put]] 32 | 33 | st->io1->e 34 | ``` 35 | 36 | 37 | --- 38 | #### 3.属性的修改 39 | 我们对上例中的代码做一下修改: 40 | ```javascript 41 | var person = {}; 42 | person.name = "Messi"; 43 | person.name = "Bale"; 44 | ``` 45 | 很显然,`name`被创建后,该属就被进行了修改,原属性值`Messi`被修改为`Bale`,那么这个过程又是如何发生的呢? 46 | 47 | 其实对象内部除了隐式的`[[put]]`方法,还有一个`[[set]]`方法,这个方法不同于`[[put]]`在创建属性时调用,而是在同一个属性被再次赋值的时候用于更新属性进行的调用. 48 | 49 | ```flow 50 | st=>start: person.name = "Messi" 51 | e=>end: persen.name = "Bale" 52 | io1=>inputoutput: [[put]] 53 | 54 | st->io1->e 55 | ``` 56 | 57 | --- 58 | #### 4.属性的查询 59 | 判断一个属性或者方法是否在一个对象中,通常有两种方式. 60 | `in`操作符方式: 61 | ```javascript 62 | var person = { 63 | name: "Messi" 64 | }; 65 | console.log("name" in person); //true 66 | ``` 67 | `hasOwnProperty`方法: 68 | 69 | ```javascript 70 | var person = { 71 | name: "Messi" 72 | }; 73 | console.log(person.hasOwnProperty("name")); //true 74 | ``` 75 | 76 | --- 77 | 78 | #### 5.属性的删除 79 | 删除一个属性,最正确的方式是用`delete`方法,一个错误的方式是将该属性赋值为`null`,该方式的错误之处在于赋值`null`相当于调用了[[set]]方法把原属性值更改为了`null`,这个保存属性的节点依然存在,而用`delete`方法便能彻底删除这个节点. 80 | ```javascript 81 | var person = { 82 | name: "Messi" 83 | }; 84 | delete person.name; 85 | console.log("name" in person); //false 86 | ``` 87 | 88 | #### 6.属性的枚举 89 | 90 | 我们通常用`for...in`枚举对象中的属性,它会将属性一一返回. 91 | 在ES5中引入了一个新的方法`Object.key()`,不同之处在于,它可以将结果以数组的形式返回 92 | ```javascript 93 | var person = { 94 | name: "Messi", 95 | age: 29 96 | }; 97 | 98 | for(var pros in person ) { 99 | console.log(pros); // name age 100 | } 101 | 102 | var pros = Object.keys(person); 103 | console.log(pros); //[ 'name', 'age' ] 104 | 105 | ``` 106 | 107 | > 值得注意的是,并非所有的属性都是可枚举的,例如对象自带的属性`length`等等,因此我们可以用`propertyIsEnumerable()`方法来判断一个属性是否可枚举. 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /JavaScript基础/javascript“作用域与闭包“.md: -------------------------------------------------------------------------------- 1 | # JavaScript面向对象之作用域 2 | 3 | --- 4 | #### 1. 为什么要理解作用域 5 | 6 | 原因很简单,JavaScript中最重要的一个概念**闭包**的理解就建立在对**作用域**的理解之上,而一个对象的的构成往往离不开**闭包**以及**作用域**. 7 | 8 | --- 9 | #### 2. 动态作用域or静态作用域? 10 | 首先我们要搞清楚JavaScript的作用域类型,这有助于我们在分析作用域时的判断. 11 | > 静态作用域:静态作用域是指声明的作用域是根据程序正文在编译时就确定的,有时也称为词法作用域。 12 | 13 | > 动态作用域:程序中某个变量所引用的对象是在程序运行时刻根据程序的控制流信息来确定的。 14 | 15 | 大多数现代编程语言都采用的静态作用域,即代码在写出来的时候就已经确定的,并非在执行时再确定,我们可以根据以下代码一探究竟. 16 | ``` 17 | function f() { 18 | console.log(a); 19 | } 20 | function g() { 21 | var a = 7; 22 | f(); 23 | } 24 | g(); // a is not defined 25 | ``` 26 | 这段代码在执行时候会报错,很明显,如果JavaScript采用了动态作用域,`a`在执行时确定的话,那么以上代码相当于这样: 27 | ``` 28 | function g() { 29 | var a = 7; 30 | function f() { 31 | console.log(a); 32 | } 33 | } 34 | g(); //undefind 35 | ``` 36 | 因此,我们可以判断出JavaScript属于静态作用域. 37 | 38 | 39 | --- 40 | #### 3.函数作用域 41 | 大家都知道,函数是JavaScript的一等公民,那么函数是存在自身作用域的,在创建函数之初,函数体内就产生了作用域,为了方便理解,我们引用了《你不知道的JavaScript》书中的代码及图例,他会很清晰地帮助我们理解函数作用域. 42 | ``` 43 | function foo(a) { 44 | var b = a * 2; 45 | function bar(c) { 46 | console.log(a, b, c); 47 | } 48 | bar(b * 3); 49 | } 50 | foo(2); // 2, 4, 12 51 | ``` 52 | 由于JavaScript是采用静态作用域,作用域是在函数创建的时候就确定下来的. 53 | 54 | --- 55 | 56 | #### 4.作用域链 57 | 58 | 那么,我们可以仔细分析一下这个作用域链. 59 | ![](http://omrbgpqyl.bkt.clouddn.com/17-8-25/32385127.jpg) 60 | 我们可以看到`scope chain`通过指向本函数的变量对象,并通过本函数的变量对象与整个父级函数变量对象联系在一起,这就是作用域链. 61 | 62 | 所以说,作用域链与一个执行上下文相关,是**内部上下文所有变量对象(包括父变量对象)**的**列表**,用于变量查询。 63 | 64 | --- 65 | 66 | #### 5. 块级作用域 67 | 68 | 在ES2015之前,JavaScript中实际上是没有语法层面的块级作用域,这就造成了很多意外的产生. 69 | ``` 70 | for (var i = 0; i<3; i++) { 71 | 72 | } 73 | console.log(i); //3 74 | ``` 75 | 如果是在有块级作用域的语言中,`i`是不会被打印出来的,但是在JavaScript中却被打印出来,这就是变量泄露的情况,也就是说看似在块级作用域的变量泄漏到全局作用域中,这也就造成了全局污染. 76 | 77 | 在ES5中,人们为了解决这个问题,一般采用立即执行函数IIFE来模拟块级作用域,但是这种写法不易读也不优雅,因此,在ES2015中引入了`let`,通过`let`可以创建块级作用域. 78 | 79 | > `let`与`var`在使用上基本是类似的,但是`let`有三个主要的特点 80 | 81 | > * 可创建块级作用域 82 | > * 不存在变量提升 83 | > * 存在暂时性死区 84 | 85 | 例如上面的代码如果改用`let`声明,就不存在变量污染全局的情况 86 | ``` 87 | for (let i = 0; i<3; i++) { 88 | 89 | } 90 | console.log(i); //i is not defind 91 | ``` 92 | 至于其它let的具体用法,可以直接参考[《ES6入门教程》](http://es6.ruanyifeng.com/#docs/let). 93 | 94 | --- 95 | 96 | #### 6.什么是闭包 97 | 98 | 我们先简单地描述一下闭包:闭包是一个函数读取其它函数变量的桥梁. 99 | 100 | 我们先从上面这个简单的例子开始 101 | ``` 102 | function foo(a) { 103 | var b = a * 2; 104 | function bar(c) { 105 | console.log(a, b, c); 106 | } 107 | bar(b * 3); 108 | } 109 | foo(2); // 2, 4, 12 110 | ``` 111 | 根据前面所学作用域的概念,函数`f2`将引用函数`f1`的变量`a`并打印,这个嵌套函数中,子函数对父函数中的变量进行了引用,而使得这个引用得以成行的桥梁就是**'闭包'**. 112 | > * 很多讲解闭包的文章都用`return`做实例,值得注意的是,闭包的形成并不一定要有`return`,只要对其它函数变量产生了引用,就会产生闭包,而`return`的作用是方便外部访问. 113 | 114 | 可以看到bar通过作用域链向上寻找到变量,我理解的闭包是一个对象,包含了**函数本身**以及它**引用的上下文环境**,本实例函数的闭包可以用这段代码来示意下: 115 | `{Funtion:bar, bar.variableObject:{c=12, ...}, foo.variableObject:{b=4, ...},window/global.variableObject:{a=2, ...}}` 116 | 117 | > 闭包只是javascript函数作用域产生的附属品 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /JavaScript基础/javascript“预解释”.md: -------------------------------------------------------------------------------- 1 | # JavaScript的"预解释" 2 | 3 | --- 4 | 5 | ### 前言: 6 | 7 | JavaScript的作用域一直是JavaScript比较让人头痛的一部分,也是面试中几乎必考的内容,因此,我们将从更深层次来讲述js作用域。 8 | 9 | --- 10 | 11 | #### 1.从一个实例开始 12 | 13 | 仔细阅读以下JavaScript代码,你觉得运行结果会是什么呢?是 `1` 还是`2`? 14 | ``` javascript 15 | var a= 1; 16 | function f() { 17 | console.log(a); 18 | var a = 2; 19 | } 20 | f(); 21 | ``` 22 | 23 | 答案是undefined. 24 | 25 | 那么到底是什么原因导致了这个让人意外的结果呢?这就要从JavaScript解释阶段说起。 26 | 27 | --- 28 | 29 | #### 2.JavaScript预解释 30 | 31 | 我们可以大致把JavaScript在浏览器中运行的过程分为两个阶段`预解释阶段`(有人说准确的说法是应该是Parser,我们以预解释方便理解) `执行阶段`,在JavaScript引擎对JavaScript代码进行执行之前,需要进行预先处理,然后再对处理后的代码进行执行。 32 | 33 | > 我们平时书写的JavaScript代码并不是JavaScript执行的代码(V8引擎读取一行执行一行这种理解是错误的),它需要预解释后,再由引擎进行执行. 34 | 35 | 具体的解释过程涉及到浏览器内核的技术不属于前端领域,不过我们可以浅显的理解一下V8在处理JavaScript的一般过程: 36 | 37 | 以上例中的`var a = 2; `为例,我们一般人的理解为**声明了一个值为2的变量a**,但是在JavaScript引擎处理时却分为了两个步骤: 38 | >1. 读取`var a`后,在当前作用域中查找是否有相同声明,如果没有就在当前作用域集合中创建一个名为`a`的变量,否则忽略此声明继续进行解析. 39 | 40 | >2. 接下来,V8引擎会处理`a = 2`的赋值操作,首先会询问当前作用域中是否有名为`a`的变量,如果有进行赋值,否则继续向上级作用域询问. 41 | 42 | --- 43 | 44 | #### 3.JavaScript执行环境 45 | 46 | 我们上面提到的所谓javascript预解释正是创建函数的**执行环境**(又称“执行上下文”),只有搞定了javascript的执行环境我们才能搞清楚一段代码在执行过后为什么产生这样的结果。 47 | 48 | 我们用一段伪代码表示创立的**执行环境** 49 | ```javascript 50 | executionContextObj = { 51 | 'scopeChain': { /* 变量对象 + 所有父级执行上下文中的变量对象 */ }, 52 | 'variableObject': { /* 函数参数 / 参数, 内部变量以及函数声明 */ }, 53 | 'this': {} 54 | } 55 | ``` 56 | 作用域链(scopeChain)包括下面提到的变量对象(variableObject)和所有父级执行上下文中的变量对象. 57 | 58 | 变量对象(variableObject)是与执行上下文相关的数据作用域,一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明: 59 | 变量; 60 | 函数声明; 61 | 函数的形参 62 | 63 | 在有了这些基板概念之后我们可以梳理一下js引擎创建执行的过程: 64 | * 创建阶段 65 | * 创建Scope chain 66 | * 创建variableObject 67 | * 设置this 68 | * 执行阶段 69 | * 变量的值、函数的引用 70 | * 执行代码 71 | 72 | 而变量对象的创建细节如下: 73 | 74 | * 根据函数的参数,创建并初始化arguments object 75 | * 扫描函数内部代码,查找函数声明(Function declaration) 76 | * 对于所有找到的函数声明,将函数名和函数引用存入变量对象中 77 | * 如果变量对象中已经有同名的函数,那么就进行覆盖 78 | * 扫描函数内部代码,查找变量声明(Variable declaration) 79 | * 对于所有找到的变量声明,将变量名存入变量对象中,并初始化为"undefined" 80 | * 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性 81 | 82 | 83 | --- 84 | 85 | #### 4.变量提升 86 | 87 | 正是由于以上的处理,产生了大家熟知的JavaScript中的**变量提升**,具体以上代码的执行过程如以下伪代码所示: 88 | ```javascript 89 | // global context 90 | executionContextObj = { 91 | 'scopeChain': { ... }, 92 | 'variableObject': { a: undefined, f: pointer to function f() }, 93 | 'this': {...} 94 | } 95 | ... 96 | }//首先在全局执行环境中声明了变量a以及函数f,此时a虽然被声明,但是尚未赋值 97 | x = 1; 98 | function f() { 99 | executionContextObj { 100 | 'scopeChain': { ... }, 101 | 'variableObject': { 102 | arguments: {}, 103 | a: undefined 104 | }, 105 | 'this': {...} 106 | } 107 | //内部词法环境中声明了变量a,此时a虽然被声明,但是尚未赋值 108 | console.log(a);//此时a需要被被打印出来,在作用域内寻找a变量赋值,于是被赋值undefined 109 | a = 2; 110 | } 111 | ``` 112 | 113 | 我们可以明显看到,`a`变量在预解释阶段已经被赋值`undefined`,在执行阶段js是自上而下单线执行,当`console.log(a)`执行之时,`a=2`还没有被执行,`a`变量的值便是预处理阶段被赋予的`undefined`, 114 | 115 | --- 116 | #### 5.函数声明与函数表达式 117 | 我们看到,在编译器处理阶段,除了被`var`声明的变量会有变量提升这一特性之外,函数也会产生这一特性,但是函数声明与函数表达式两种范式创建的函数却表现出不同的结果. 118 | 119 | 120 | 我们先看一个实例,运行以下代码 121 | ```javascript 122 | f(); 123 | g(); 124 | //函数声明 125 | function f() { 126 | console.log('f'); 127 | } 128 | //函数表达式 129 | var g = function() { 130 | console.log('g'); 131 | }; 132 | 133 | ``` 134 | 135 | `f`成功被打印出来,而`g函数`出现了类型错误,这是什么原因呢? 136 | 137 | 138 | ```javascript 139 | executionContextObj = { 140 | 'scopeChain': { ... }, 141 | 'variableObject': { f: pointer to function f(), g: undefined}, 142 | 'this': {...} 143 | } 144 | 145 | f(); 146 | g(); 147 | //函数声明 148 | function f() { 149 | console.log('f'); 150 | } 151 | //函数表达式 152 | var g = function() { 153 | console.log('g'); 154 | }; 155 | ``` 156 | 我们看到,在预解释阶段函数声明的`f`是被指向了正确的函数得以执行,而函数表达式`g`被赋予`undefined`,`undefined`无法被当作函数执行因此报错`g is not a function`. 157 | 158 | 159 | --- 160 | #### 6.冲突处理 161 | 162 | 通常情况下我们不会将同一变量变量重复声明,但是出现了类似情况后,编译器会如何处理这些冲突呢? 163 | 1. 变量之间冲突 164 | 执行以下函数: 165 | ```javascript 166 | var a = 3; 167 | var a = 4; 168 | console.log(a); 169 | ``` 170 | 结果显而易见,后声明变量值覆盖前者的值 171 | 2. 函数之间冲突 172 | ```javascript 173 | f(); 174 | function f() { 175 | console.log('f'); 176 | } 177 | 178 | function f () { 179 | console.log('g'); 180 | }; 181 | ``` 182 | 结果同变量冲突,后者覆盖前者. 183 | 184 | 3. 函数与变量之间冲突 185 | 186 | ```javascript 187 | console.log(f); 188 | 189 | function f() { 190 | console.log('f'); 191 | } 192 | var f ='g'; 193 | ``` 194 | 结果如下,函数声明将覆盖变量声明. 195 | `[Function: f]` 196 | --- 197 | #### 7.ES6中的let 198 | 199 | 在ES6中出现了两个最新的声明语法`let`与`const`,我们以`let`为例,进行测试看看与`var`的区别. 200 | ```javascript 201 | function f() { 202 | console.log(a); 203 | let a = 2; 204 | } 205 | f(); // ReferenceError: a is not defined 206 | ``` 207 | 这段代码直接报错显示未定义,`let`与`const`拥有类似的特性,阻止了变量提升,当代码执行到`console.log(a)`时,执行换将中`a`还从未被定义,因此产生了错误. 208 | -------------------------------------------------------------------------------- /异步/如何实现一个Event.md: -------------------------------------------------------------------------------- 1 | # 如何实现一个Event 2 | 3 | 4 | --- 5 | ## **前言** 6 | 我们已经通过阅读`Node.js`中`Event`的实现方式对其有一定的了解,那么如何实现一个可以Node与Browser环境通用的`Event`呢? 7 | 8 | > **提前声明:** 我们没有对传入的参数进行及时判断而规避错误,仅仅对核心方法进行了实现,如果要实现一个健壮的程序,你是需要这样做的. 9 | 10 | --- 11 | #### 1.基本构造 12 | 13 | **1.1初始化class** 14 | 我们利用ES6的`class`关键字对`Event`进行初始化,包括`Event`的事件清单和监听者上限. 15 | 16 | 我们选择了`Map`作为储存事件的结构,因为作为键值对的粗存方式`Map`比一般对象更加适合,我们操作起来也更加简洁,可以先看一下Map的[基本用法与特点](http://es6.ruanyifeng.com/#docs/set-map#Map). 17 | 18 | ```javascript 19 | class EventEmeitter { 20 | constructor() { 21 | this._events = this._events || new Map(); 22 | this._maxListeners = this._maxListeners || 10; 23 | } 24 | } 25 | ``` 26 | 27 | **1.2 监听与触发** 28 | 29 | 触发监听函数我们可以用`apply`与`call`两种方法,在少数参数时`call`的性能更好,多个参数时`apply`性能更好,Node的Event就在三个参数一下用`call`否则用`apply`,但是我们不想写的那么复杂,就做了一个简化版. 30 | 31 | ```javascript 32 | EventEmeitter.prototype.emit = function(type, ...args) { 33 | let handler; 34 | handler = this._events.get(type); 35 | if (args.length > 0) { 36 | handler.apply(this, args); 37 | } else { 38 | handler.call(this); 39 | } 40 | return true; 41 | }; 42 | 43 | EventEmeitter.prototype.addListener = function(type, fn) { 44 | if (!this._events.get(type)) { 45 | this._events.set(type, fn); 46 | } 47 | }; 48 | 49 | ``` 50 | 51 | 我们实现了触发事件的`emit`方法和监听事件的`addListener`方法,至此我们就可以进行简单的实践了. 52 | 53 | 54 | ```javascript 55 | const emitter = new EventEmeitter(); 56 | 57 | emitter.addListener('arson', man => { 58 | console.log(`expel ${man}`); 59 | }); 60 | 61 | emitter.emit('arson', 'low-end'); // expel low-end 62 | ``` 63 | 64 | 似乎不错,我们实现了基本的触发/监听,但是如果有多个监听者呢? 65 | ```javascript 66 | emitter.addListener('arson', man => { 67 | console.log(`expel ${man}`); 68 | }); 69 | emitter.addListener('arson', man => { 70 | console.log(`save ${man}`); 71 | }); 72 | 73 | emitter.emit('arson', 'low-end'); // expel low-end 74 | ``` 75 | 是的,只会触发第一个,因此我们需要进行改造. 76 | 77 | 78 | --- 79 | #### 2.升级改造 80 | 81 | **2.1 监听/触发器升级** 82 | 83 | 我们的`addListener`实现方法还不够健全,在绑定第一个监听者之后,我们就无法对后续监听者进行绑定了,因此我们需要将后续监听者与第一个监听者函数放到一个数组里. 84 | 85 | ```javascript 86 | EventEmeitter.prototype.emit = function(type, ...args) { 87 | let handler; 88 | handler = this._events.get(type); 89 | if (Array.isArray(handler)) { 90 | // 如果是一个数组说明有多个监听者,需要依次此触发里面的函数 91 | for (let i = 0; i < handler.length; i++) { 92 | if (args.length > 0) { 93 | handler[i].apply(this, args); 94 | } else { 95 | handler[i].call(this); 96 | } 97 | } 98 | } else { // 单个函数的情况我们直接触发即可 99 | if (args.length > 0) { 100 | handler.apply(this, args); 101 | } else { 102 | handler.call(this); 103 | } 104 | } 105 | 106 | return true; 107 | }; 108 | 109 | EventEmeitter.prototype.addListener = function(type, fn) { 110 | const handler = this._events.get(type); // 获取对应事件名称的函数清单 111 | if (!handler) { 112 | this._events.set(type, fn); 113 | } else if (handler && typeof handler === 'function') { 114 | // 如果handler是函数说明只有一个监听者 115 | this._events.set(type, [handler, fn]); // 多个监听者我们需要用数组储存 116 | } else { 117 | handler.push(fn); // 已经有多个监听者,那么直接往数组里push函数即可 118 | } 119 | }; 120 | ``` 121 | 是的,从此以后可以愉快的触发多个监听者的函数了. 122 | ```javascript 123 | emitter.addListener('arson', man => { 124 | console.log(`expel ${man}`); 125 | }); 126 | emitter.addListener('arson', man => { 127 | console.log(`save ${man}`); 128 | }); 129 | 130 | emitter.addListener('arson', man => { 131 | console.log(`kill ${man}`); 132 | }); 133 | 134 | emitter.emit('arson', 'low-end'); 135 | //expel low-end 136 | //save low-end 137 | //kill low-end 138 | ``` 139 | 140 | **2.2 移除监听** 141 | 142 | 我们会用`removeListener`函数移除监听函数,但是匿名函数是无法移除的. 143 | 144 | 145 | ```javascript 146 | EventEmeitter.prototype.removeListener = function(type, fn) { 147 | const handler = this._events.get(type); // 获取对应事件名称的函数清单 148 | 149 | if (handler && typeof handler === 'function') { 150 | this._events.delete(type, fn); 151 | } else { 152 | let postion; 153 | // 如果handler是数组 154 | for (let i = 0; i < handler.length; i++) { 155 | if (handler[i] === fn) { 156 | postion = i; 157 | } else { 158 | postion = -1; 159 | } 160 | } 161 | // 如果找到匹配的函数,从数组中清除 162 | if (postion !== -1) { 163 | handler.splice(postion, 1); 164 | // 如果清除后只有一个函数,那么取消数组 165 | if (handler.length === 1) { 166 | this._events.set(type, handler[0]); 167 | } 168 | } else { 169 | return this; 170 | } 171 | } 172 | }; 173 | ``` 174 | 175 | --- 176 | #### 3.发现问题 177 | 178 | 179 | 我们已经基本完成了`Event`最重要的几个方法,也完成了升级改造,可以说一个`Event`的骨架是被我们开发出来了,但是它仍然有不足和需要补充的地方. 180 | 181 | > 1. 鲁棒性不足: 我们没有对参数进行充分的判断,没有完善的报错机制. 182 | > 2. 模拟不够充分: 除了`removeAllListeners`这些方法没有实现以外,例如监听时间后会触发`newListener`事件,我们也没有实现,另外最开始的监听者上限我们也没有利用到. 183 | 184 | 索性[Event](https://github.com/Gozala/events/blob/master/events.js)库帮我们实现了完整的特性,整个代码量有300多行,很适合阅读,你可以花十分钟的时间通读一下,见识一下完整的Event实现 185 | -------------------------------------------------------------------------------- /JavaScript基础/javascript原型.md: -------------------------------------------------------------------------------- 1 | # JavaScript面向对象之原型 2 | 3 | --- 4 | 5 | #### 1.原型对象 6 | 绝大部分的函数(少数内建函数除外)都有一个`prototype`属性,这个属性是原型对象用来创建新对象实例,而所有被创建的对象都会共享原型对象,因此这些对象便可以访问原型对象的属性,例如`hasOwnProperty()`方法存在于Obejct原型对象中,它便可以被任何对象当做自己的方法使用. 7 | > `object.hasOwnProperty( propertyName )` 8 | > `hasOwnProperty()`函数的返回值为`Boolean`类型。如果对象`object`具有名称为`propertyName`的属性,则返回`true`,否则返回`false`。 9 | ```javascript 10 | var person = { 11 | name: "Messi", 12 | age: 29, 13 | profession: "football player" 14 | }; 15 | console.log(person.hasOwnProperty("name")); //true 16 | console.log(person.hasOwnProperty("hasOwnProperty")); //false 17 | console.log(Object.prototype.hasOwnProperty("hasOwnProperty")); //true 18 | ``` 19 | 由以上代码可知,`hasOwnProperty()`并不存在于`person`对象中,但是`person`依然可以拥有此方法. 20 | 21 | 很多人此时会好奇,`person`对象是如何找到`Object`对象中的方法的呢?这其中的内部机制是什么? 22 | 这便是我们接下来要说的原型链. 23 | 24 | 25 | --- 26 | 27 | #### 2.`__proto__`与`[[Prototype]]` 28 | 29 | 上一篇我们的示意图中曾经出现过`__proto__`,在ES6之前这个`__proto__`是大部分主流浏览器(IE除外)引擎提供的,还尚属非ECMA标准,在解析一个对象实例的时候为对象实例添加一个`__proto__`属性,此属性指向原型对象,我们便可以通过此属性找到原型对象. 30 | ```javascript 31 | function person(pname, page) { 32 | this.name = pname; 33 | this.age = page; 34 | } 35 | person.prototype.profession = "football player"; 36 | var person1 = new person("Messi", 29); //person1 = {name:"Messi", age: 29, profession: "football player"}; 37 | var person2 = new person("Bale", 28); //person2 = {name:"Bale", age: 28, profession: "football player"}; 38 | console.log(person1.__proto__ === person.prototype); //true 39 | ``` 40 | > `__proto__`除了被主流浏览器支持外,还被Node.js支持,在ES2015进入到规范附录部分,算是被正式纳入了标准. 41 | 42 | 而在标准的语法里,实例对象是通过内置的内部属性`[[Prototype]]`来追踪原型对象的,这个`[[Prototype]]`的指针始终指向原型对象,此属性通常情况下是不可见的,我们需要用`getPrototypeOf()`来读取`[[Prototype]]`属性值(这个值就是原型对象). 43 | 44 | ```javascript 45 | var obj = {}; 46 | console.log(Object.getPrototypeOf(obj) === Object.prototype); //true 47 | ``` 48 | 同时我们也可以用`isPrototypeOf`来检验某个对象是否是另一个对象的原型对象. 49 | 50 | ```javascript 51 | var obj = {}; 52 | console.log(Object.prototype.isPrototypeOf(obj)); //true 53 | ``` 54 | 55 | --- 56 | ### 3.原型链 57 | 58 | 在我们了解了`__proto__`与`[[Prototype]]`之后,就可以相对容易理解原型链了,由于`__proto__`与`[[Prototype]]`功能相似,但是`__proto__`更容易测试方便学习,我们选择`__proto__`来进行原型链的讲解. 59 | 60 | ```javascript 61 | function person(pname, page) { 62 | this.name = pname; 63 | this.age = page; 64 | } 65 | person.prototype.profession = "football player"; 66 | var person1 = new person("Messi", 29); //person1 = {name:"Messi", age: 29, profession: "football player"}; 67 | var person2 = new person("Bale", 28); //person2 = {name:"Bale", age: 28, profession: "football player"}; 68 | person1.hasOwnProperty("name"); 69 | console.log(person1.hasOwnProperty("hasOwnProperty")); //fasle 70 | console.log(person1.__proto__ === person.prototype); //true 71 | console.log(person.prototype.hasOwnProperty("hasOwnProperty")); //false 72 | console.log(person1.__proto__.__proto__ === person.prototype.__proto__); // true 73 | console.log(person.prototype.__proto__.hasOwnProperty("hasOwnProperty")); //true 74 | ``` 75 | 我们可以分析这个例子,看看`person1`对象实例是如何调用`hasOwnProperty()`这个方法的. 76 | 77 | 1. 首先`person1`对象实例中寻找`hasOwnProperty()`方法,`person1.hasOwnProperty("hasOwnProperty")`返回`false`,发现不存在此方法,这时通过`__proto__`找到`person1`的原型对象. 78 | 2. 在`person1`的原型对象`person1.__proto__`即`person.prototype`中寻找`hasOwnProperty()`方法,`person.prototype.hasOwnProperty("hasOwnProperty")`返回`false`,依然没有找到,此时顺着`person.prototype`的`__proto__`找到其原型对象. 79 | 3. 在`person.prototype`原型对象`person.prototype.__proto__`即`Object.prototype`中寻找`hasOwnProperty()`方法,`Object.prototype.hasOwnProperty("hasOwnProperty")`返回`true`,由于`hasOwnProperty()`为`Object.prototype`内置方法,因此`person1`顺利找到此方法并调用. 80 | 81 | 总而言之,实例对象方法调用,是现在实力对象内部找,如果找到则立即返回调用,如果没有找到就顺着`__proto__`向上寻找,如果找到该方法则调用,没有找到会直接报错,这便是**原型链**. 82 | 83 | 如图所示,会更加直观. 84 | 85 | --- 86 | #### 4.ES6中的`__proto__` 87 | 88 | 虽然`__proto__`在最新的ECMA标准中被纳入了规范,但是由于`__proto__`前后的双下划线,说明它本质上是一个内部属性,而不是一个正式的对外的 API. 89 | 标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的`Object.setPrototypeOf()`(写操作)、`Object.getPrototypeOf()`(读操作)代替。 90 | 91 | ```javascript 92 | function person(pname, page) { 93 | this.name = pname; 94 | this.age = page; 95 | } 96 | person.prototype.profession = "football player"; 97 | var person1 = new person("Messi", 29); 98 | 99 | console.log(Object.getPrototypeOf(person1) === person.prototype); //true 100 | Object.setPrototypeOf(person1, {League: "La Liga"}); 101 | console.log(person1.League); //La Liga 102 | 103 | ``` 104 | 以上为具体用法,但是值得注意的是`Object.setPrototypeOf()`在使用中有一个坑,如代码所示: 105 | ```javascript 106 | function person(pname, page) { 107 | this.name = pname; 108 | this.age = page; 109 | } 110 | person.prototype.profession = "football player"; 111 | var person1 = new person("Messi", 29); 112 | var person2 = new person("Bale", 28); 113 | 114 | Object.setPrototypeOf(person1, { League: "La Liga"}); 115 | 116 | console.log(person1.League); //La Liga 117 | console.log(person2.League); //undefind 118 | ``` 119 | 也就是说不同于直接用`person1.__proto__.League = "La Liga";`会使得两个实例同时生效,`Object.setPrototypeOf()`只能生效一个实例对象. 120 | 121 | -------------------------------------------------------------------------------- /Process/Unix-Process.md: -------------------------------------------------------------------------------- 1 | # child_process (子进程) 2 | 3 | --- 4 | 5 | ### 1. 基本概念 6 | 7 | #### **1.1 linux进程** 8 | 对于操作系统而言,进程是分配资源的最小单位. 9 | 10 | 在windows系统下我们通过任务管理器或者服务看到的都是广义上的进程. 11 | ![](http://omrbgpqyl.bkt.clouddn.com/17-11-24/52543983.jpg) 12 | 13 | 在类Unix系统下我们通过`ps`进程查看进程. 14 | ![](http://omrbgpqyl.bkt.clouddn.com/17-11-24/66187398.jpg) 15 | 16 | 具体参数意义如下: 17 | ![](http://omrbgpqyl.bkt.clouddn.com/17-11-24/38089481.jpg) 18 | 19 | 20 | 21 | #### **1.2 进程控制块(PCB)** 22 | 23 | **PCB(process control block)**,进程控制块,其实是一个数据结构描述,通过它人们才能对系统的进程进行管理. 24 | 25 | 一般情况下,PCB中包含以下内容: 26 | 1. 进程标识符(内部,外部) 27 | 2. 处理机的信息(通用寄存器,指令计数器,PSW,用户的栈指针) 28 | 3. 进程调度信息(进程状态,进程的优先级,进程调度所需的其它信息,事件) 29 | 4. 进程控制信息(程序的数据的地址,资源清单,进程同步和通信机制,链接指针) 30 | 31 | Linux的进程控制块为一个由结构`task_struct`所定义的数据结构,在创建一个新进程时,系统在内存中申请一个空的`task_struct`区,并输入相关信息(**信息很多,大概有:进程标识符、进程状态、进程优先级/调度策略等等...**) 32 | 33 | 新建进程流程如下: 34 | ![](http://omrbgpqyl.bkt.clouddn.com/17-11-24/31957772.jpg) 35 | 36 | 37 | #### **1.3 僵尸进程与孤儿进程** 38 | 39 | **僵尸进程**:Linux的进程都是由父进程创建的,当子进程死亡后,子进程一方面会释放占有的资源,一方面会保留一少部分信息交由父进程接管,但是如果父进程不接管已死亡的子进程留下的信息,那么子进程的pid就不会销毁,也就是说一个孩子虽然死了,但是父亲没有拿着户口本、身份证、死亡证明去派出所注销,这个孩子在户籍上就一直是活着的,成了所谓的僵尸。 40 | 如果一个父进程大量子进程死亡都没有接收死亡子进程信息的话,大量pid会被占用,但是pid总数有限,最终导致pid不足。 41 | 42 | **孤儿进程**: 顾名思义,指的是父进程死亡后,由其创建的所有子进程全部成为了没有父进程的孤儿进程,这个时候孤儿进程会被`init`进程接管,你也可以理解为孩子失去双亲后被国家收养了,那个`init`进程就是国家,是创建所有进程的初始进程. 43 | 44 | #### **1.4 IPC进程间通信** 45 | 46 | **进程间通信(IPC,Inter-Process Communication)**,指至少两个进程或线程间传送数据或信号的一些技术或方法. 47 | 48 | IPC是进程管理永远绕不开的话题,常见的通信技术如下: 49 | ![](http://omrbgpqyl.bkt.clouddn.com/17-11-25/46822480.jpg) 50 | 51 | **1.4.1 管道(pipe)** 52 | 53 | 管道实际是用于进程间通信的一段**共享内存**,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。 54 | *一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来*。 55 | 56 | **管道的特点:** 57 | 1. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道; 58 | 2. 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。 59 | 3. 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。 60 | 4. 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。 61 | 62 | **管道的实现机制:** 63 | 管道是由内核管理的一个**缓冲区**,管道的一端连接一个进程的输出。这个进程会向管道中放入信息。同时一头连着一个进程的读取,当缓冲区没有信息时读取进程进入等待状态,当缓冲区信息已满时,输入进程会阻塞等待缓冲区释放,读取的顺序原则是**先进先出**. 64 | 65 | **1.4.2 命名管道(FIFO)** 66 | 命名管道是一种特殊类型的文件,它在系统中以文件形式存在,这样克服了管道的弊端,**他可以允许没有亲缘关系的进程间通信**。 67 | 68 | **1.4.3 消息队列(Message queues)** 69 | MQ(Message queues) 用于在进程间传递数据。MQ 采用**链表** 来实现消息队列,该链表是由系统内核维护,系统中可能有很多的 MQ,每个 MQ 用消息队列描述符(消息队列 ID:qid)来区分,qid 是唯一的,用来区分不同的 MQ。在进行进程间通信时,一个进程将消息加到 MQ 尾端,另一个进程从消息队列中取消息,不一定以先进先出来取消息,也可以按照消息类型字段取消息,这样就实现了进程间的通信。如下 MQ 的模型: 70 | 71 | ![](http://omrbgpqyl.bkt.clouddn.com/17-11-25/36578196.jpg) 72 | 73 | **与管道的比较** 74 | 75 | 1. 消息队列也可以独立于发送和接收进程而存在,而管道随着双方进程的死亡而消失。 76 | 2. 同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。 77 | 3. 接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。 78 | 79 | **1.4.4 共享内存(Share Memory)** 80 | 共享内存是在多个进程之间共享**内存区域**的一种进程间的通信方式,由IPC为进程创建的一个特殊地址范围,此时其他进程可以将同一段共享内存连接到自己的地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是malloc分配的一样。如果一个进程向共享内存中写入了数据,所做的改动将立刻被其他进程看到。 81 | 共享内存是IPC**最快**的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则是需要将数据通过中间机制进行转换。共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅映射到各进程的地址不同而已,因此不需要进行复制,可以直接使用此段空间。 82 | 83 | ![](http://omrbgpqyl.bkt.clouddn.com/17-11-25/22740017.jpg) 84 | 85 | **1.4.5 UNIX域套接字( Unix domain socket )** 86 | 传统的套接字(Socket)是基于TCP/IP协议栈的,需要指定IP地址,从而用来进行不同机器的通信,但是对于同一台机器的通信来说这一套并不高效. 87 | 因此**Unix域套接字**,专门用来处理同一台机器进程通信问题。 88 | ![](http://omrbgpqyl.bkt.clouddn.com/17-11-25/63260591.jpg) 89 | 90 | 91 | ### 2. node的多进程 92 | 93 | #### **2.1 子进程** 94 | 95 | `child_process`模块可以进行子进程的创建. 96 | 97 | **其中有这几个重要方法:** 98 | **1.`spawn`:** spawn方法可以启动一个子进程执行命令,比如通过`shell`压缩文件、转换格式等等一系列操作。 99 | 100 | ```javascript 101 | const { spawn } = require('child_process'); 102 | const ls = spawn('ls', ['-l', '/usr']); 103 | 104 | ls.stdout.on('data', (data) => { 105 | console.log(`stdout: ${data}`); 106 | }); 107 | 108 | ls.stderr.on('data', (data) => { 109 | console.log(`stderr: ${data}`); 110 | }); 111 | 112 | ls.on('close', (code) => { 113 | console.log(`child process exited with code ${code}`); 114 | }); 115 | ``` 116 | 上述代码我们读取了目标目录的文件列表,我们可以通过监听事件来对结果进行进一步操作. 117 | 118 | **2.`exec`:** exec方法可以启动一个子进程执行命令,并缓冲产生的数据,当子进程完成后回调函数可以将其调用. 119 | 120 | 与`spawn`方法相比`exec`多了回调函数,更方便我们进一步操作. 121 | ```javascript 122 | const { exec } = require('child_process'); 123 | 124 | const ls = exec('ls -l', (error, stdout, stderr) => { 125 | if (error) { 126 | console.error(error.stack); 127 | console.log('Error code: ' + error.code); 128 | } 129 | console.log('Child Process STDOUT: ' + stdout); 130 | }); 131 | ``` 132 | 133 | 134 | **3.`execFile`:** execFile方法可以执行一个外部应用,与`exec`类似,除了不衍生一个 shell。 而是,指定的可执行的 file 被直接衍生为一个新进程,这使得它比 `child_process.exec()` 更高效。。 135 | 136 | ```javascript 137 | const { execFile } = require('child_process'); 138 | const child = execFile('node', ['--version'], (error, stdout, stderr) => { 139 | if (error) { 140 | throw error; 141 | } 142 | console.log(stdout); 143 | }); 144 | ``` 145 | 146 | **4.`fork`:** `fork`方法直接创建一个子进程,执行Node脚本,`fork('./child.js')` 相当于 `spawn('node', ['./child.js'])` 。与`spawn`方法不同的是,`fork`会在父进程与子进程之间,建立一个通信管道,用于进程之间的通信。 147 | 148 | ```javascript 149 | const n = child_process.fork('./worker.js'); 150 | n.on('message', function(m) { 151 | console.log('PARENT got message:', m); 152 | }); 153 | n.send({ hello: 'world' }); 154 | ``` 155 | 156 | #### **2.2 进程间通信** 157 | 158 | **2.2.1 node通信原理** 159 | `fork`创建进程之后,会在父子进程之间建立IPC通信,并过message与send()等方法进行通信,这通信方法基于由node的管道技术实现,而这个管道技术不同于上文提到的操作系统的*管道*,node的管道技术由libuv提供,而在不同操作系统下具体实现不同:Windows下由命名管道实现,*nix下由UNIX域套接字实现. 160 | -------------------------------------------------------------------------------- /JavaScript基础/javascript中的this.md: -------------------------------------------------------------------------------- 1 | # JavaScript面向对象之this 2 | 3 | 4 | --- 5 | #### 1.什么决定了`this`的指向 6 | 7 |   `this`一直是JavaScript中十分玄乎的存在,很多人为了避开这个琢磨不透的东西,选择了尽量少得运用`this`,但是不可否认的是,正是因为`this`的存在才使得JavaScript拥有了更加灵活的特性,因此,搞清楚`this`是每一个JavaScript学习者的必修课. 8 | 9 |   `this`之所以让人又爱又恨,正是因为它的指向让人琢磨不透,在进行详细讲解之前,我们要搞清楚一个大前提,`this`的指向不是在编写时确定的,而是在执行时确定的. 10 | ```javascript 11 | obj = { 12 | name: "Messi", 13 | sayName: function () { 14 | console.log(this.name); 15 | } 16 | }; 17 | 18 | obj.sayName(); //"Messi" 19 | 20 | var f = obj.sayName; 21 | f(); //undefind 22 | console.log(f === obj.sayName); //true 23 | ``` 24 |   很明显,虽然`f`与`obj.sayName`是等价的,但是他们所产生的结果却截然不同,归根到底是因为它们调用位置的不同造成的. 25 | 26 |   `f`的调用位置在全局作用域,因此`this`指向`window`对象,而`window`对象并不存在`name`因此会显示出`undefind`,而`obj.sayName`的`this`指向的是`obj`对象,因此会打印出`"Messi"`. 27 | 28 |   我们可以在以下代码中加入`name = "Bale";`来证明以上说法. 29 | 30 | ```javascript 31 | name = "Bale"; 32 | obj = { 33 | name: "Messi", 34 | sayName: function () { 35 | console.log(this.name); 36 | } 37 | }; 38 | 39 | obj.sayName(); //"Messi" 40 | 41 | var f = obj.sayName; 42 | f(); //"Bale" 43 | console.log(f === obj.sayName); //true 44 | ``` 45 |   大家一定会好奇,调用位置是如何决定`obj.sayName`的`this`指向`obj`对象,`f`却指向`window`对象呢,其中遵循什么规则吗? 46 | 47 | --- 48 | #### 2.默认绑定 49 |   `this`一共存在4种绑定规则,默认绑定是其中最常见的,我们可以认为当其他三个绑定规则都没有体现时,就用的是默认的绑定规则. 50 | 51 | ```javascript 52 | name = "Bale"; 53 | 54 | function sayName () { 55 | console.log(this.name); 56 | }; 57 | 58 | sayName(); //"Bale" 59 | ``` 60 |   以上代码可以看成我们第一节例子中的`f`函数,它之所以指向`window`对象,就是运用了`this`**默认绑定**的规则,因为此实例代码中既没有运用`apply`  `bind`等显示绑定,也没有用`new`绑定,不适用于其他绑定规则,因此便是**默认绑定**,此时的`this`指向全局变量,即浏览器端的`window`Node.js中的`global`. 61 | 62 | --- 63 | #### 3.隐式绑定 64 |   当函数被调用的位置存在上下文对象,或者说被某个对象拥有或包含,这时候函数的`f`的`this`被**隐式绑定**到`obj`对象上. 65 | ```javascript 66 | function f() { 67 | console.log( this.name ); 68 | } 69 | var obj = { 70 | name: "Messi", 71 | f: f 72 | }; 73 | obj.f(); // Messi 74 | 75 | ``` 76 | 77 | --- 78 | #### 4.显式绑定 79 | 80 |   除了极少数的宿主函数之外,所有的函数都拥有`call` `apply`方法,而这两个大家既熟悉又陌生的方法可以强制改变`this`的指向,从而实现显式绑定. 81 | 82 | `call` `apply`可以产生对`this`相同的绑定效果,唯一的区别便是他们参数传入的方式不同. 83 | >**call方法**: 84 | **语法**:call([thisObj[,arg1[, arg2[, [,.argN]]]]]) 85 | **定义**:调用一个对象的一个方法,以另一个对象替换当前对象。 86 | **说明**: 87 |   call 方法可以用来代替另一个对象调用一个方法。call 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象。 88 |   如果没有提供 thisObj 参数,那么 Global 对象被用作 thisObj。 89 | 90 | 91 | >**apply方法**: 92 | **语法**:apply([thisObj[,argArray]]) 93 | **定义**:应用某一对象的一个方法,用另一个对象替换当前对象。 94 | **说明**: 95 |   如果 argArray 不是一个有效的数组或者不是 arguments 对象,那么将导致一个 TypeError。 96 | 如果没有提供 argArray 和 thisObj 任何一个参数,那么 Global 对象将被用作 thisObj, 并且无法被传递任何参数。 97 | 98 | 99 |   第一个参数意义都一样。第二个参数:apply传入的是一个参数数组,也就是将多个参数组合成为一个数组传入,而`call`则作为`call`的参数传入(从第二个参数开始)。 100 |   如 `func.call(func1,var1,var2,var3)` 对应的`apply`写法为:`func.apply(func1,[var1,var2,var3])`,同时使用`apply`的好处是可以直接将当前函数的`arguments`对象作为`apply`的第二个参数传入。   101 | 102 | ```javascript 103 | function f() { 104 | console.log( this.name ); 105 | } 106 | var obj = { 107 | name: "Messi", 108 | 109 | }; 110 | f.call(obj); // Messi 111 | f.apply(obj); //Messi 112 | ``` 113 | 我们可以看到,效果是相同的,`call` `apply`的作用都是强制将`f`函数的`this`绑定到`obj`对象上. 114 | 在ES5中有一个与`call` `apply`效果类似的`bind`方法,同样可以达成这种效果, 115 | 116 | >**`Function.prototype.bind()`** 的作用是将当前函数与指定的对象绑定,并返回一个新函数,这个新函数无论以什么样的方式调用,其 this 始终指向绑定的对象。 117 | 118 | ```javascript 119 | function f() { 120 | console.log( this.name ); 121 | } 122 | var obj = { 123 | name: "Messi", 124 | }; 125 | 126 | var obj1 = { 127 | name: "Bale" 128 | }; 129 | 130 | f.bind(obj)(); //Messi ,由于bind将obj绑定到f函数上后返回一个新函数,因此需要再在后面加上括号进行执行,这是bind与apply和call的区别 131 | 132 | ``` 133 | 134 | --- 135 | #### 5.new绑定 136 | 用 new 调用一个构造函数,会创建一个新对象, 在创造这个新对象的过程中,新对象会自动绑定到`Person`对象的`this`上,那么 `this` 自然就指向这个新对象。 137 | 这没有什么悬念,因为 new 本身就是设计来创建新对象的。 138 | ```javascript 139 | function Person(name) { 140 | this.name = name; 141 | console.log(name); 142 | } 143 | 144 | var person1 = new Person('Messi'); //Messi 145 | ``` 146 | 147 | --- 148 | 149 | 150 | #### 6.绑定优先级 151 | 152 | 153 | 通过以上的介绍,我们知道了四种绑定的规则,但是当这些规则同时出现,那么谁的优先级更高呢,这才有助于我们判断`this`的指向. 154 | 通常情况下,按照优先级排序是: 155 | **new绑定 > 显式绑定 >隐式绑定 >默认绑定** 156 | 157 | 我们完全可以通过这个优先级顺序判断`this`的指向问题. 158 | 159 | 160 | --- 161 | 162 | #### 7.ES6箭头函数中的this 163 | 164 | 箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于自己的`this`,它的`this`是捕获其所在上下文的 this 值,作为自己的 this 值,并且由于没有属于自己的`this`,箭头函数是不会被`new`调用的. 165 | 166 | MDN文档中关于箭头函数的实例很清楚的说明了这一点. 167 | 168 | 在 ECMAScript 3/5 中,这个问题可以通过新增一个变量来指向期望的 this 对象,然后将该变量放到闭包中来解决。 169 | ```javascript 170 | function Person() { 171 | var self = this; // 也有人选择使用 `that` 而非 `self`. 172 | // 只要保证一致就好. 173 | self.age = 0; 174 | 175 | setInterval(function growUp() { 176 | // 回调里面的 `self` 变量就指向了期望的那个对象了 177 | self.age++; 178 | }, 1000); 179 | } 180 | ``` 181 | 除此之外,还可以使用 bind 函数,把期望的 this 值传递给 growUp() 函数。 182 | 183 | 箭头函数则会捕获其所在上下文的 this 值,作为自己的 this 值,因此下面的代码将如期运行。 184 | ```javascript 185 | function Person(){ 186 | this.age = 0; 187 | 188 | setInterval(() => { 189 | this.age++; // |this| 正确地指向了 person 对象 190 | }, 1000); 191 | } 192 | 193 | var p = new Person(); 194 | ``` 195 | 196 | 当然,我们用babel转码器,也可以让我们更清楚理解箭头函数的`this` 197 | 198 | ```javascript 199 | // ES6 200 | const obj = { 201 | getArrow() { 202 | return () => { 203 | console.log(this === obj); 204 | }; 205 | } 206 | } 207 | ``` 208 | ```javascript 209 | // ES5,由 Babel 转译 210 | var obj = { 211 | getArrow: function getArrow() { 212 | var _this = this; 213 | return function () { 214 | console.log(_this === obj); 215 | }; 216 | } 217 | }; 218 | ``` -------------------------------------------------------------------------------- /异步/异步机制.md: -------------------------------------------------------------------------------- 1 | # Node中的异步机制 2 | 3 | 4 | **[2018.3.5更新]: 关于event loop机制我发现了社区目前最好的[解读](https://cnodejs.org/topic/5a9108d78d6e16e56bb80882),欢迎大家移步.** 5 | --- 6 | 7 | ### 1. 异步的追根溯源 8 | 9 | #### **1.1 JavaScript中的线程** 10 | 要想追根溯源地了解JavaScript中的异步问题,我们必须深入到js引擎层面来探究这一问题,这就不得不提到线程。 11 | 12 | 众所周知,JavaScript是单线程的,也就是说js语言在解释和执行中只由一个线程负责。 13 | 14 | 但是在实际的场景中,JavaScript虽然是单线程的工作状态,但是如果在浏览器中运行,浏览器内核会自带DOM线程、AJAX线程等,这些线程虽然并不能直接解释和执行js代码,但是可以跟js的主线程进行配合,有效的完成工作。 15 | 16 | 我们可以这样约定,负责解释和执行JavaScript的为主线程,那么除此之外的线程我们成为伪线程。 17 | 18 | #### **1.2 JavaScript中的Runtime** 19 | 20 | 我们在了解JavaScript工作方式之前,必须要弄清楚几个概念: 21 | 22 | 1.2.1 Stack(栈) 23 | **Stack**中是我们正在执行的任务,而其中的单个任务被称为“帧”。 24 | **注意**:这个Stack既不是数据结构中后进先出的Stack,也不是内存区域中Satck的存放方式,而是指函数的执行方式(Call Stack)。 25 | 26 | ```javascript 27 | function f(b){ 28 | var a = 1; 29 | return a+b; 30 | } 31 | 32 | function g(x){ 33 | var y = 2; 34 | return f(y+x); 35 | } 36 | 37 | g(3); 38 | ``` 39 | 以上述代码为例,首先调用 g 时,创建一帧,该帧包含了 g 的参数和局部变量。随后,当 g 调用 f 时,第二帧就会被创建,并且置于第一帧之上,当 f 返回时,其对应的帧就会出栈。同理,当 g 返回时,栈就为空了(先进后出)。 40 | 41 | 1.2.2 Heap(堆) 42 | Heap是内存中的一块区域,通常将对象分配在这里。 43 | 44 | 1.2.3 Queue(队列) 45 | 一个 JavaScript runtime 包含了一个任务队列,该队列是由一系列待处理的任务组成,同时这些任务都有相对应的回调函数。当栈为空时,就会从任务队列中依次取出任务并处理,我们要记住,任务队列实际是一个先进先出的数据结构。 46 | 47 | 这就是以上三个概念的示意图: 48 | ![](http://omrbgpqyl.bkt.clouddn.com/17-6-20/59721062.jpg) 49 | 50 | #### **1.3 JavaScript中的Event Loop** 51 | 52 | 在了解了以上三个概念后,我们就可以更容易搞清楚EventLoop(事件循环)的作用,js主线程产生的Stack(栈)是主要负责处理当前任务,Queue(队列)储存着等待进入Stack(栈)执行的任务队列,那么如何将Queue(队列)中的任务压入Stack(栈)中? 53 | 54 | 负责这一动作的就是**EventLoop(事件循环)**,EventLoop(事件循环)一直循环,每当Stack中为空的时候,Stack会把待执行任务的回调函数压入Stack中执行。 55 | 56 | 我们可以用下列代码示意一下EventLoop 57 | ```javascript 58 | while(queue.waitForMessage()){ 59 | queue.processNextMessage(); 60 | } 61 | ``` 62 | #### **1.4 Node中如何执行异步** 63 | 64 | 我们知道,JavaScript原始语言本身是没有*AJAX*或者*fs.readFile*的,它们分别来自于浏览器的Web Api和Node中的C++模块,由于我们主要讲述Node的异步执行,因此我们可以抛开浏览器的Web Api,当然他们的工作原理也有共通之处. 65 | 66 | 我在[一篇关于事件循环文章](https://blog.risingstack.com/node-js-at-scale-understanding-node-js-event-loop/)中发现了一个很好的实例,可以解释Node中如何执行异步. 67 | 68 | ```javascript 69 | 'use strict' 70 | const express = require('express') 71 | const superagent = require('superagent') 72 | const app = express() 73 | 74 | app.get('/', sendWeatherOfRandomCity) //处理请求,返回城市的天气信息 75 | function sendWeatherOfRandomCity (request, response) { 76 | getWeatherOfRandomCity(request, response) 77 | sayHi() 78 | } 79 | 80 | const CITIES = [ // 储存城市名称的数组 81 | 'london', 82 | 'newyork', 83 | 'paris', 84 | 'budapest', 85 | 'warsaw', 86 | 'rome', 87 | 'madrid', 88 | 'moscow', 89 | 'beijing', 90 | 'capetown', 91 | ] 92 | 93 | function getWeatherOfRandomCity (request, response) { // 处理城市天气信息的函数 94 | const city = CITIES[Math.floor(Math.random() * CITIES.length)] 95 | superagent.get(`wttr.in/${city}`) 96 | .end((err, res) => { 97 | if (err) { 98 | console.log('O snap') 99 | return response.status(500).send('There was an error getting the weather, try looking out the window') 100 | } 101 | const responseText = res.text 102 | response.send(responseText) 103 | console.log('Got the weather') 104 | }) 105 | console.log('Fetching the weather, please be patient') 106 | } 107 | 108 | function sayHi () { 109 | console.log('Hi') 110 | } 111 | 112 | app.listen(3000) 113 | 114 | ``` 115 | 我们最终得到的打印信息是这样的 116 | ``` 117 | Fetching the weather, please be patient 118 | Hi 119 | Got the weather 120 | 121 | ``` 122 | 123 | 我们都知道,`console.log('Got the weather')`由于异步的原因被打印在了最后的位置,但是Node在这个过程中到底具体是发生了什么,才导致这个结果呢? 124 | 125 | 我们不妨分析一下: 126 | 127 | 1. express 为“request”事件注册了一个处理程序,请求 “/” 时会被调用; 128 | 129 | 2. 跳过函数,开始监听 3000 端口; 130 | 131 | 3. 调用栈为空,等待“request”事件触发; 132 | 133 | 4. 请求到来,等待已久的事件触发,express 调用 sendWeatherOfRandomCity; 134 | 135 | 5. sendWeatherOfRandomCity 入栈; 136 | 137 | 6. getWeatherOfRandomCity 被调用并入栈; 138 | 139 | 7. 调用 Math.floor 和 Math.random,入栈、出栈,cities 中的某一个被赋值给 city; 140 | 141 | 8. 传入 'wttr.in/${city}' 调用 superagent.get,为 end 事件设置处理回调; 142 | 143 | 9. 发送 http://wttr.in/${city} http 请求到底层线程,继续向下执行; 144 | 145 | 10. 控制台打印 'Fetching the weather, please be patient',getWeatherOfRandomCity 函数返回; 146 | 147 | 11. 调用 sayHi,控制台打印 'Hi'; 148 | 149 | 12. sendWeatherOfRandomCity 函数返回、出栈,调用栈变空; 150 | 151 | 13. 等待 http://wttr.in/${city} 发送响应; 152 | 153 | 14. 一旦响应返回,end 事件触发,end事件随即进入任务队列中,此时EventLoop发现栈已经空了,而且任务队列中有未处理的任务; 154 | 155 | 15. 因此 .end() 的匿名回调函数调用,带着其闭包内所有变量一起入栈,在栈中执行相关回调函数; 156 | 157 | 16. 最后,调用 response.send(),状态码为 200 或 500,再次发送到底层线程,response stream 不会阻塞代码执行,匿名回调出栈。 158 | 159 | 我们可以看到,我们可以把上述执行过程简化为: 160 | 161 | (1)V8引擎解析JavaScript脚本。 162 | (2)解析后的代码,调用Node API。 163 | (3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。 164 | (4)V8引擎再将结果返回给用户。 165 | 166 | #### **1.5 不止一个任务队列?** 167 | 我们先看一道面试题 168 | ```javascript 169 | setTimeout(function() { 170 | console.log(1) 171 | }, 0); 172 | new Promise(function executor(resolve) { 173 | console.log(2); 174 | for( var i=0 ; i<10000 ; i++ ) { 175 | i == 9999 && resolve(); 176 | } 177 | console.log(3); 178 | }).then(function() { 179 | console.log(4); 180 | }); 181 | console.log(5); 182 | ``` 183 | 如果你的答案也是`2 3 5 4 1 `那么你可以跳过本节了. 184 | 185 | 这道题的难点在于`4`与`1`到底哪个先执行,要判断这个问题,我们就不得不提两个新概念,microtask(小型任务) 与 macrotask(巨型任务),它们各有一个任务队列。 186 | 187 | **Microtask** : 188 | 1. process.nextTick 189 | 2. promise 190 | 3. Object.observe(已废弃) 191 | 192 | **Macrotask**: 193 | 1. setTimeout 194 | 2. setInterval 195 | 3. setImmediate 196 | 4. I/O 197 | 198 | 这两个任务队列有什么区别呢? 199 | 再一次时间循环中,macroktask优先被执行,在macroktask被执行完毕后microtask在同一个循环中接着被执行,直到执行完毕进入下一个循环。 200 | ![](http://omrbgpqyl.bkt.clouddn.com/17-6-20/45931145.jpg) 201 | 202 | 我们可以清楚地看到整个程序的执行过程,那么回到这道面试题,`Promise`显然属于Microtask,是在第一个循环的末尾执行,而`setTimeout`属于Macrotask,是在第二个循环中执行,因此`4`先于`1`被打印出来. 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /JavaScript基础/javascript实现深克隆.md: -------------------------------------------------------------------------------- 1 | # 在JavaScript中如何实现一个深克隆 2 | 3 | --- 4 | 5 | #### 前言 6 | 在要实现一个深克隆之前我们需要了解一下javascript中的基础类型. 7 | 8 | [javascript基础类型](https://github.com/xiaomuzhu/ElemeFE-node-interview/blob/master/JavaScript%E5%9F%BA%E7%A1%80/JavaScript%E5%9F%BA%E6%9C%AC%E7%B1%BB%E5%9E%8B.md) 9 |    10 | > JavaScript原始类型:Undefined、Null、Boolean、Number、String、Symbol 11 | JavaScript引用类型:Object 12 | 13 | --- 14 | 15 | #### 1.浅克隆 16 | 17 |   **浅克隆**之所以被称为**浅克隆**,是因为对象只会被克隆最外部的一层,至于更深层的对象,则依然是通过引用指向同一块堆内存. 18 | 19 | ```javascript 20 | // 浅克隆函数 21 | function shallowClone(o) { 22 | const obj = {}; 23 | for ( let i in o) { 24 | obj[i] = o[i]; 25 | } 26 | return obj; 27 | } 28 | // 被克隆对象 29 | const oldObj = { 30 | a: 1, 31 | b: [ 'e', 'f', 'g' ], 32 | c: { h: { i: 2 } } 33 | }; 34 | 35 | const newObj = shallowClone(oldObj); 36 | console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 } 37 | console.log(oldObj.c.h === newObj.c.h); // true 38 | 39 | ``` 40 | 41 | 我们可以看到,很明显虽然`oldObj.c.h`被克隆了,但是它还与`oldObj.c.h`相等,这表明他们依然指向同一段堆内存,这就造成了如果对`newObj.c.h`进行修改,也会影响`oldObj.c.h`,这就不是一版好的克隆. 42 | 43 | ```javascript 44 | newObj.c.h.i = 'change'; 45 | console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 'change' } 46 | ``` 47 | 我们改变了`newObj.c.h.i `的值,`oldObj.c.h.i`也被改变了,这就是浅克隆的问题所在. 48 | 49 | 当然有一个新的api`Object.assign()`也可以实现浅复制,但是效果跟上面没有差别,所以我们不再细说了. 50 | 51 | #### 2.深克隆的 52 | 53 | ##### 2.1 JSON.parse方法 54 | 55 | 前几年微博上流传着一个传说中最便捷实现深克隆的方法, 56 | JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,这两个方法结合起来就能产生一个便捷的深克隆. 57 | ```javascript 58 | const newObj = JSON.parse(JSON.stringify(oldObj)); 59 | ``` 60 | 61 | 我们依然用上一节的例子进行测试 62 | ```javascript 63 | const oldObj = { 64 | a: 1, 65 | b: [ 'e', 'f', 'g' ], 66 | c: { h: { i: 2 } } 67 | }; 68 | 69 | const newObj = JSON.parse(JSON.stringify(oldObj)); 70 | console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 } 71 | console.log(oldObj.c.h === newObj.c.h); // false 72 | newObj.c.h.i = 'change'; 73 | console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 2 } 74 | 75 | ``` 76 | 果然,这是一个实现深克隆的好方法,但是这个解决办法是不是太过简单了. 77 | 78 | 确实,这个方法虽然可以解决绝大部分是使用场景,但是却有很多坑. 79 | > 1.他无法实现对函数 、RegExp等特殊对象的克隆 80 | 81 | > 2.会抛弃对象的constructor,所有的构造函数会指向Object 82 | 83 | > 3.对象有循环引用,会报错 84 | 85 | 主要的坑就是以上几点,我们一一测试下. 86 | 87 | ```javascript 88 | // 构造函数 89 | function person(pname) { 90 | this.name = pname; 91 | } 92 | 93 | const Messi = new person('Messi'); 94 | 95 | // 函数 96 | function say() { 97 | console.log('hi'); 98 | }; 99 | 100 | const oldObj = { 101 | a: say, 102 | b: new Array(1), 103 | c: new RegExp('ab+c', 'i'), 104 | d: Messi 105 | }; 106 | 107 | const newObj = JSON.parse(JSON.stringify(oldObj)); 108 | 109 | // 无法复制函数 110 | console.log(newObj.a, oldObj.a); // undefined [Function: say] 111 | // 稀疏数组复制错误 112 | console.log(newObj.b[0], oldObj.b[0]); // null undefined 113 | // 无法复制正则对象 114 | console.log(newObj.c, oldObj.c); // {} /ab+c/i 115 | // 构造函数指向错误 116 | console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: Object] [Function: person] 117 | 118 | ``` 119 | 120 | 我们可以看到在对函数、正则对象、稀疏数组等对象克隆时会发生意外,构造函数指向也会发生错误。 121 | 122 | ```javascript 123 | const oldObj = {}; 124 | 125 | oldObj.a = oldObj; 126 | 127 | const newObj = JSON.parse(JSON.stringify(oldObj)); 128 | console.log(newObj.a, oldObj.a); // TypeError: Converting circular structure to JSON 129 | ``` 130 | 对象的循环引用会抛出错误. 131 | 132 | ##### 2.2 构造一个深克隆函数 133 | 134 | 我们知道要想实现一个靠谱的深克隆方法,上一节提到的**序列/反序列**是不可能了,而通常教程里提到的方法也是不靠谱的,他们存在的问题跟上一届序列反序列操作中凸显的问题是一致的. 135 | ![](http://omrbgpqyl.bkt.clouddn.com/18-1-20/43905997.jpg) 136 | *(这个方法也会出现上一节提到的问题)* 137 | 138 | 139 | 140 | 由于要面对不同的对象(正则、数组、Date等)要采用不同的处理方式,我们需要实现一个对象类型判断函数。 141 | 142 | ```javascript 143 | const isType = (obj, type) => { 144 | if (typeof obj !== 'object') return false; 145 | const typeString = Object.prototype.toString.call(obj); 146 | let flag; 147 | switch (type) { 148 | case 'Array': 149 | flag = typeString === '[object Array]'; 150 | break; 151 | case 'Date': 152 | flag = typeString === '[object Date]'; 153 | break; 154 | case 'RegExp': 155 | flag = typeString === '[object RegExp]'; 156 | break; 157 | default: 158 | flag = false; 159 | } 160 | return flag; 161 | }; 162 | ``` 163 | 这样我们就可以对特殊对象进行类型判断了,从而采用针对性的克隆策略. 164 | 165 | ```javascript 166 | const arr = Array.of(3, 4, 5, 2); 167 | 168 | console.log(isType(arr, 'Array')); // true 169 | ``` 170 | 171 | 对于正则对象,我们在处理之前要先补充一点新知识. 172 | 173 | 我们需要通过[正则的扩展](http://es6.ruanyifeng.com/#docs/regex#flags-%E5%B1%9E%E6%80%A7)了解到**flags 属性 **等等,因此我们需要实现一个提取flags的函数. 174 | ```javascript 175 | const getRegExp = re => { 176 | var flags = ''; 177 | if (re.global) flags += 'g'; 178 | if (re.ignoreCase) flags += 'i'; 179 | if (re.multiline) flags += 'm'; 180 | return flags; 181 | }; 182 | ``` 183 | 184 | 做好了这些准备工作,我们就可以进行深克隆的实现了. 185 | 186 | ```javascript 187 | /** 188 | * deep clone 189 | * @param {[type]} parent object 需要进行克隆的对象 190 | * @return {[type]} 深克隆后的对象 191 | */ 192 | const clone = parent => { 193 | // 维护两个储存循环引用的数组 194 | const parents = []; 195 | const children = []; 196 | 197 | const _clone = parent => { 198 | if (parent === null) return null; 199 | if (typeof parent !== 'object') return parent; 200 | 201 | let child, proto; 202 | 203 | if (isType(parent, 'Array')) { 204 | // 对数组做特殊处理 205 | child = []; 206 | } else if (isType(parent, 'RegExp')) { 207 | // 对正则对象做特殊处理 208 | child = new RegExp(parent.source, getRegExp(parent)); 209 | if (parent.lastIndex) child.lastIndex = parent.lastIndex; 210 | } else if (isType(parent, 'Date')) { 211 | // 对Date对象做特殊处理 212 | child = new Date(parent.getTime()); 213 | } else { 214 | // 处理对象原型 215 | proto = Object.getPrototypeOf(parent); 216 | // 利用Object.create切断原型链 217 | child = Object.create(proto); 218 | } 219 | 220 | // 处理循环引用 221 | const index = parents.indexOf(parent); 222 | 223 | if (index != -1) { 224 | // 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象 225 | return children[index]; 226 | } 227 | parents.push(parent); 228 | children.push(child); 229 | 230 | for (let i in parent) { 231 | // 递归 232 | child[i] = _clone(parent[i]); 233 | } 234 | 235 | return child; 236 | }; 237 | return _clone(parent); 238 | }; 239 | ``` 240 | 241 | 我们做一下测试 242 | ```javascript 243 | function person(pname) { 244 | this.name = pname; 245 | } 246 | 247 | const Messi = new person('Messi'); 248 | 249 | function say() { 250 | console.log('hi'); 251 | } 252 | 253 | const oldObj = { 254 | a: say, 255 | c: new RegExp('ab+c', 'i'), 256 | d: Messi, 257 | }; 258 | 259 | oldObj.b = oldObj; 260 | 261 | 262 | const newObj = clone(oldObj); 263 | console.log(newObj.a, oldObj.a); // [Function: say] [Function: say] 264 | console.log(newObj.b, oldObj.b); // { a: [Function: say], c: /ab+c/i, d: person { name: 'Messi' }, b: [Circular] } { a: [Function: say], c: /ab+c/i, d: person { name: 'Messi' }, b: [Circular] } 265 | console.log(newObj.c, oldObj.c); // /ab+c/i /ab+c/i 266 | console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: person] [Function: person] 267 | ``` 268 | 269 | 270 | 当然,我们这个深克隆还不算完美,例如Buffer对象、Promise、Set、Map可能都需要我们做特殊处理,另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间,不过一个基本的深克隆函数我们已经实现了。 271 | -------------------------------------------------------------------------------- /JavaScript基础/JavaScript基本类型.md: -------------------------------------------------------------------------------- 1 | # 你可能真的不懂JavaScript中最基础类型 2 | 3 | --- 4 | 5 | #### 前言 6 |   众所周知,JavaScript是动态弱类型的多范式编程语言,由于设计时的粗糙(当时设计js的初衷就是在浏览器中处理表单这种简单事件)导致JavaScript在许多方面表现出了这样或者那样的问题,其中'类型'便是语法层面最常见的'埋坑'重灾区. 7 |    8 | > JavaScript原始类型:Undefined、Null、Boolean、Number、String、Symbol 9 | 10 | > JavaScript引用类型:Object 11 | 12 | --- 13 | 14 | #### 1.原始类型与引用类型 15 | 16 | ###### 1.1 17 |   **原始类型**又被称为**基本类型**,原始类型保存的变量和值直接保存在**栈内存**(Stack)中,且空间相互独立,通过值来访问,说到这里肯定一同懵逼,不过我们可以通过一个例子来解释. 18 | 19 | ```javascript 20 | var person = 'Messi'; 21 | var person1 = person; 22 | ``` 23 | 上述代码在栈内存的示意图是这样的,可以看到,虽然`person`赋值给了`person1`.但是两个变量并没有指向同一个值,而是`person1`自己单独建立一个内存空间,虽然两个变量的值相等,但却是相互独立的. 24 | 25 | ![](http://omrbgpqyl.bkt.clouddn.com/18-2-20/84377115.jpg) 26 | 27 | ```javascript 28 | var person = 'Messi'; 29 | var person1 = person; 30 | 31 | var person = 1; 32 | 33 | console.log(person); //1 34 | console.log(person1); //'Messi' 35 | 36 | ``` 37 | 上述代码示意图是这样的,`person`的值虽然改变,但是由于`person1`的值是独立储存的,因此不受影响. 38 | 39 | 值得一提的是,虽然原始类型的值是储存在相对独立空间,但是它们之间的比较是**按值**比较的. 40 | 41 | ```javascript 42 | var person = 'Messi'; 43 | var person1 = 'Messi'; 44 | console.log(person === person1); //true 45 | ``` 46 | 47 | 48 | ###### 1.2引用类型 49 | 50 | 剩下的就是引用类型了,即Object 类型,再往下细分,还可以分为:Object 类型、Array 类型、Date 类型、Function 类型 等。 51 | 52 | 与原始类型不同的是,引用类型的内容是保存在**堆内存**中,而**栈内存**(Heap)中会有一个**堆内存地址**,通过这个地址变量被指向堆内存中`Object`真正的值,因此引用类型是按照引用访问的. 53 | 54 | 由于示意图太难画,我从网上找了一个例子,能很清楚的说明引用类型的特质. 55 | 56 | ```javascript 57 | var a = {name:"percy"}; 58 | var b; 59 | b = a; 60 | a.name = "zyj"; 61 | console.log(b.name); // zyj 62 | b.age = 22; 63 | console.log(a.age); // 22 64 | var c = { 65 | name: "zyj", 66 | age: 22 67 | }; 68 | console.log(a === c); //false 69 | 70 | ``` 71 | 72 | 我们可以逐行分析: 73 | 1. `b = a`,如果是原始类型的话,`b`会在栈内自己独自创建一个内存空间保存值,但是引用类型只是`b`的产生一个对内存地址,指向堆内存中的`Object`. 74 | 2.`a.name = "zyj"`,这个操作属于改变了变量的值,在原始类型中会重新建立新的内存空间(可以看上一节的示意图),而引用类型只需要自己在堆内存中更新自己的属性即可. 75 | 3.最后创建了一个新的对象`c`,看似跟`b` `a`一样,但是在堆内存中确实两个相互独立的`Object`,引用类型是按照**引用比较**,由于`a` `c`引用的是不同的`Object`所以得到的结果是`fasle`. 76 | 77 | ![](http://omrbgpqyl.bkt.clouddn.com/18-2-20/34304948.jpg) 78 | --- 79 | #### 2. 类型中的坑 80 | 81 | 2.1 数组中的坑 82 | 数组是JavaScript中最常见的类型之一了,但是在我们实践过程中同样会遇到各种各样的麻烦. 83 | 84 | **稀疏数组**:指的是含有空白或空缺单元的数组 85 | ```javascript 86 | var a = []; 87 | 88 | console.log(a.length); //0 89 | 90 | a[4] = a[5]; 91 | 92 | console.log(a.length); //5 93 | 94 | a.forEach(elem => { 95 | console.log(elem); //undefined 96 | }); 97 | 98 | console.log(a); //[,,,,undefined] 99 | ``` 100 | 这里有几个坑需要注意: 101 | 1. 一开始建立的空数组`a`的长度为0,这可以理解,但是在`a[4] = a[5]`之后出现了问题,`a`的长度居然变成了5,此时`a`数组是`[,,,,undefined]`这种形态. 102 | 2. 我们通过遍历,只得到了`undefined`这一个值,这个`undefind`是由于`a[4] = a[5]`赋值,由于`a[5]`没有定义值为`undefined`被赋给了`a[4]`,可以等价为`a[4] = undefined`. 103 | 104 | **字符串索引** 105 | 106 | ```javascript 107 | var a = []; 108 | a[0] = 'Bale'; 109 | a['age'] = 28; 110 | console.log(a.length); //1 111 | console.log(a['age']); //28 112 | console.log(a); //[ 'Bale', age: 28 ] 113 | ``` 114 | 数组不仅可以通过数字索引,也可以通过字符串索引,但值得注意的是,字符串索引的键值对并不算在数组的长度里. 115 | 116 | 117 | 2.2 数字中的坑 118 | **二进制浮点数** 119 | 120 | JavaScript 中的数字类型是基于“二进制浮点数”实现的,使用的是“双精度”格式,这就带来了一些反常的问题,我们那一道经典面试提来讲解下. 121 | ```javascript 122 | var a = 0.1 + 0.2; 123 | var b = 0.3; 124 | console.log(a === b); //false 125 | ``` 126 | 这是个出人意料的结果,实际上a的值约为`0.30000000000000004`这并不是一个整数值,这就是`二进制浮点数`带来的副作用. 127 | 128 | ```javascript 129 | var a = 0.1 + 0.2; 130 | var b = 0.3; 131 | console.log(a === b); //false 132 | console.log(Number.isInteger(a*10)); //false 133 | console.log(Number.isInteger(b*10)); //true 134 | console.log(a); //0.30000000000000004 135 | ``` 136 | 我们可以用`Number.isInteger()`来判断一个数字是否为整数. 137 | 138 | **NaN** 139 | 140 | ```javascript 141 | var a = 1/new Object(); 142 | console.log(typeof a); //Number 143 | console.log(a); //NaN 144 | console.log(isNaN(a)); //true 145 | ``` 146 | `NaN`属于特殊的`Number`类型,我们可以把它理解为`坏数值`,因为它属于数值计算中的错误,更加特殊的是它自己都不等价于自己`NaN === NaN //false`,我们只能用`isNaN()`来检测一个数字是否为`NaN`. 147 | 148 | 149 | 150 | --- 151 | #### 3.类型转换原理 152 | 153 | **类型转换**指的是将一种类型转换为另一种类型,例如: 154 | ```javascript 155 | var b = 2; 156 | var a = String(b); 157 | console.log(typeof a); //string 158 | ``` 159 | 160 | 当然,**类型转换**分为显式和隐式,但是不管是隐式转换还是显式转换,都会遵循一定的原理,由于JavaScript是一门动态类型的语言,可以随时赋予任意值,但是各种运算符或条件判断中是需要特定类型的,因此JavaScript引擎会在运算时为变量设定类型. 161 | 162 | 这看起来很美好,JavaScript引擎帮我们搞定了`类型`的问题,但是引擎毕竟不是ASI(超级人工智能),它的很多动作会跟我们预期相去甚远,我们可以从一到面试题开始. 163 | 164 | ```javascript 165 | {}+[] //0 166 | ``` 167 | 168 | 169 | 答案是0 170 | 171 | 是什么原因造成了上述结果呢?那么我们得从ECMA-262中提到的转换规则和抽象操作说起,有兴趣的童鞋可以仔细阅读下这浩如烟海的[语言规范](http://ecma-international.org/ecma-262/5.1/),如果没这个耐心还是往下看. 172 | 173 | 这是JavaScript种类型转换可以从**原始类型**转为**引用类型**,同样可以将**引用类型**转为**原始类型**,转为原始类型的抽象操作为`ToPrimitive`,而后续更加细分的操作为:`ToNumber ToString ToBoolean`,这三种抽象操作的转换表如下所示 174 | ![](http://omrbgpqyl.bkt.clouddn.com/17-9-13/15517231.jpg) 175 | 176 | 177 | 如果想应付面试,我觉得这张表就差不多了,但是为了更深入的探究JavaScript引擎是如何处理代码中类型转换问题的,就需要看 ECMA-262详细的规范,从而探究其内部原理,我们从这段内部原理示意代码开始. 178 | ```javascript 179 | // ECMA-262, section 9.1, page 30. Use null/undefined for no hint, 180 | // (1) for number hint, and (2) for string hint. 181 | function ToPrimitive(x, hint) { 182 | // Fast case check. 183 | if (IS_STRING(x)) return x; 184 | // Normal behavior. 185 | if (!IS_SPEC_OBJECT(x)) return x; 186 | if (IS_SYMBOL_WRAPPER(x)) throw MakeTypeError(kSymbolToPrimitive); 187 | if (hint == NO_HINT) hint = (IS_DATE(x)) ? STRING_HINT : NUMBER_HINT; 188 | return (hint == NUMBER_HINT) ? DefaultNumber(x) : DefaultString(x); 189 | } 190 | 191 | // ECMA-262, section 8.6.2.6, page 28. 192 | function DefaultNumber(x) { 193 | if (!IS_SYMBOL_WRAPPER(x)) { 194 | var valueOf = x.valueOf; 195 | if (IS_SPEC_FUNCTION(valueOf)) { 196 | var v = %_CallFunction(x, valueOf); 197 | if (IsPrimitive(v)) return v; 198 | } 199 | 200 | var toString = x.toString; 201 | if (IS_SPEC_FUNCTION(toString)) { 202 | var s = %_CallFunction(x, toString); 203 | if (IsPrimitive(s)) return s; 204 | } 205 | } 206 | throw MakeTypeError(kCannotConvertToPrimitive); 207 | } 208 | 209 | // ECMA-262, section 8.6.2.6, page 28. 210 | function DefaultString(x) { 211 | if (!IS_SYMBOL_WRAPPER(x)) { 212 | var toString = x.toString; 213 | if (IS_SPEC_FUNCTION(toString)) { 214 | var s = %_CallFunction(x, toString); 215 | if (IsPrimitive(s)) return s; 216 | } 217 | 218 | var valueOf = x.valueOf; 219 | if (IS_SPEC_FUNCTION(valueOf)) { 220 | var v = %_CallFunction(x, valueOf); 221 | if (IsPrimitive(v)) return v; 222 | } 223 | } 224 | throw MakeTypeError(kCannotConvertToPrimitive); 225 | } 226 | ``` 227 | 上面代码的逻辑是这样的: 228 | 229 | 1. 如果变量为字符串,直接返回. 230 | 2. 如果`!IS_SPEC_OBJECT(x)`,直接返回. 231 | 3. 如果`IS_SYMBOL_WRAPPER(x)`,则抛出异常. 232 | 4. 否则会根据传入的`hint`来调用`DefaultNumber`和`DefaultString`,比如如果为`Date`对象,会调用`DefaultString`. 233 | 5. `DefaultNumber`:首`先x.valueOf`,如果为`primitive`,则返回`valueOf`后的值,否则继续调用`x.toString`,如果为`primitive`,则返回`toString`后的值,否则抛出异常 234 | 6. `DefaultString`:和`DefaultNumber`正好相反,先调用`toString`,如果不是`primitive`再调用`valueOf`. 235 | 236 | 那讲了实现原理,这个`ToPrimitive`有什么用呢?实际很多操作会调用`ToPrimitive`,比如加、相等或比较操。在进行加操作时会将左右操作数转换为`primitive`,然后进行相加。 237 | 238 | 下面来个实例,({}) + 1(将{}放在括号中是为了内核将其认为一个代码块)会输出啥?可能日常写代码并不会这样写,不过网上出过类似的面试题。 239 | 240 | 加操作只有左右运算符同时为`String或Number`时会执行对应的`%_StringAdd或%NumberAdd`,下面看下`({}) + 1`内部会经过哪些步骤: 241 | 242 | `{}`和`1`首先会调用ToPrimitive 243 | `{}`会走到`DefaultNumber`,首先会调用`valueOf`,返回的是`Object` `{}`,不是primitive类型,从而继续走到`toString`,返回`[object Object]`,是`String`类型 244 | 最后加操作,结果为`[object Object]1` 245 | 再比如有人问你`[] + 1`输出啥时,你可能知道应该怎么去计算了,先对`[]`调用`ToPrimitive`,返回空字符串,最后结果为"1"。 246 | 247 | 248 | --- 249 | 本文主要参考: 250 | 1. [JavaScript 类型的那些事](https://segmentfault.com/a/1190000010352325) 251 | 252 | 253 | 254 | -------------------------------------------------------------------------------- /JavaScript基础/JavaScript中的不可变数据.md: -------------------------------------------------------------------------------- 1 | # JavaScript中的不可变数据结构 2 | 3 | --- 4 | 5 | #### 前言 6 |   我们之前已经提到过,在JavaScript中分为原始类型和引用类型. 7 |    8 | > JavaScript原始类型:Undefined、Null、Boolean、Number、String、Symbol 9 | 10 | > JavaScript引用类型:Object 11 | 12 | 同时引用类型在使用过程中经常会产生副作用. 13 | 14 | ```javascript 15 | const person = {player: {name: 'Messi'}}; 16 | 17 | const person1 = person; 18 | 19 | console.log(person, person1); 20 | 21 | //[ { name: 'Messi' } ] [ { name: 'Messi' } ] 22 | 23 | person.player.name = 'Kane'; 24 | 25 | console.log(person, person1); 26 | //[ { name: 'Kane' } ] [ { name: 'Kane' } ] 27 | ``` 28 | 29 | 我们看到,当修改了`person`中属性后,`person1`的属性值也随之改变,这就是引用类型的副作用. 30 | 31 | 可是绝大多数情况下我们并不希望`person1`的属性值也发生改变,我们应该如何解决这个问题? 32 | 33 | --- 34 | 35 | #### 1.浅复制 36 | 37 |   在ES6中我们可以用`Object.assign` 或者 `...`对引用类型进行浅复制. 38 | 39 | ```javascript 40 | const person = [{name: 'Messi'}]; 41 | const person1 = person.map(item => 42 | ({...item, name: 'Kane'}) 43 | ) 44 | 45 | console.log(person, person1); 46 | // [{name: 'Messi'}] [{name: 'Kane'}] 47 | ``` 48 | 49 | `person`的确被成功复制了,但是之所以我们称它为浅复制,是因为这种复制只能复制一层,在多层嵌套的情况下依然会出现副作用. 50 | 51 | ```javascript 52 | const person = [{name: 'Messi', info: {age: 30}}]; 53 | const person1 = person.map(item => 54 | ({...item, name: 'Kane'}) 55 | ) 56 | 57 | console.log(person[0].info === person1[0].info); // true 58 | 59 | ``` 60 | 上述代码表明当利用浅复制产生新的`person1`后其中嵌套的`info`属性依然与原始的`person`的`info`属性指向同一个堆内存对象,这种情况依然会产生副作用. 61 | 62 | 我们可以发现浅复制虽然可以解决浅层嵌套的问题,但是依然对多层嵌套的引用类型无能为力. 63 | 64 | 65 | 66 | 67 | #### 2.深克隆 68 | 69 | 既然浅复制(克隆)无法解决这个问题,我们自然会想到利用深克隆的方法来实现多层嵌套复制的问题. 70 | 71 | 我们之前已经讨论过如何实现一个深克隆,在此我们不做深究,深克隆毫无疑问可以解决引用类型产生的副作用. 72 | 73 | [如何实现深克隆](https://github.com/xiaomuzhu/ElemeFE-node-interview/blob/master/JavaScript%E5%9F%BA%E7%A1%80/javascript%E5%AE%9E%E7%8E%B0%E6%B7%B1%E5%85%8B%E9%9A%86.md) 74 | 75 | 实现一个在生产环境中可以用的深克隆是非常繁琐的事情,我们不仅要考虑到*正则*、*Symbol*、*Date*等特殊类型,还要考虑到*原型链*和*循环引用*的处理,当然我们可以选择使用成熟的[开源库](https://github.com/lodash/lodash/blob/master/cloneDeep.js)进行深克隆处理. 76 | 77 | 可是问题就在于我们实现一次深克隆的开销太昂贵了,[如何实现深克隆](https://github.com/xiaomuzhu/ElemeFE-node-interview/blob/master/JavaScript%E5%9F%BA%E7%A1%80/javascript%E5%AE%9E%E7%8E%B0%E6%B7%B1%E5%85%8B%E9%9A%86.md)中我们展示了一个勉强可以使用的深克隆函数已经处理了相当多的逻辑,如果我们每使用一次深克隆就需要一次如此昂贵的开销,程序的性能是会大打折扣. 78 | 79 | 80 | 81 | ```javascript 82 | const person = [{name: 'Messi', info: {age: 30}}]; 83 | 84 | for (let i=0; i< 100000;i++) { 85 | person.push({name: 'Messi', info: {age: 30}}); 86 | } 87 | console.time('clone'); 88 | const person1 = person.map(item => 89 | ({...item, name: 'Kane'}) 90 | ) 91 | console.timeEnd('clone'); 92 | console.time('cloneDeep'); 93 | const person2 = lodash.cloneDeep(person) 94 | console.timeEnd('cloneDeep'); 95 | 96 | // clone : 105.520ms 97 | // cloneDeep : 372.839ms 98 | ``` 99 | 100 | 我们可以看到深克隆的的性能相比于浅克隆大打折扣,然而浅克隆不能从根本上杜绝引用类型的副作用,我们需要找到一个兼具性能和效果的方案. 101 | 102 | --- 103 | #### 3. immutable.js 104 | 105 | immutable.js是正是兼顾了使用效果和性能的解决方案,它的解决方法是这样的. 106 | 107 | 因为对**Immutable**对象的任何修改或添加删除操作都会返回一个新的**Immutable**对象**Immutable**实现的原理是**Persistent Data Structur**(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 `deepCopy` 把所有节点都复制一遍带来的性能损耗,**Immutable** 使用了 **Structural Sharing**(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。请看下面动画 108 | 109 | ![](https://segmentfault.com/image?src=http://img.alicdn.com/tps/i2/TB1zzi_KXXXXXctXFXXbrb8OVXX-613-575.gif&objectId=1190000003910357&token=4f994e3bf65c373b010a157dfbab240f) 110 | 111 | 这个方法大大降低了cpu和内存的浪费 112 | 113 | 详细的immutable.js介绍请参考[Immutable 详解及 React 中实践](https://segmentfault.com/a/1190000003910357) 114 | 115 | 但是在使用过程中,immutable.js也存在很多问题. 116 | 117 | 我目前碰到的坑有: 118 | 1. 由于实现了完整的不可变数据,immutable.js的体积过于庞大,尤其在移动端这个情况被凸显出来. 119 | 2. 全新的api+不友好的文档,immutable.js使用的是自己的一套api,因此我们对js原生数组、对象的操作统统需要抛弃重新学习,但是官方文档不友好,很多情况下需要自己去试api. 120 | 3. 调试错误困难,immutable.js自成一体的数据结构,我们无法像读原生js一样读它的数据结构,很多情况下需要`toJS()`转化为原生数据结构再进行调试,这让人很崩溃. 121 | ![](http://omrbgpqyl.bkt.clouddn.com/18-2-21/25345459.jpg) 122 | 123 | 124 | immutable.js在某种程度上来说,更适合于对数据可靠度要求颇高的大型前端应用(需要引入庞大的包、额外的学习成本甚至类型检测工具对付immutable.js与原生js类似的api),中小型的项目引入immutable.js的代价有点高昂了,可是我们有时候不得不利用immutable的特性,那么如何保证性能和效果的情况下减少immutable相关库的体积和提高api友好度呢? 125 | 126 | 127 | --- 128 | #### 4.如何实现更简单的immutable 129 | 130 | 我们的原则已经提到了,要尽可能得减小体积,这就注定了我们不能像immutable.js那样自己定义各种数据结构,而且要减小使用成本,所以要用原生js的方式,而不是自定义数据结构中的api. 131 | 132 | 这个时候需要我们思考如何实现上述要求呢? 133 | 134 | 我们要通过原生js的api来实现immutable,很显然我们需要对引用对象的set、get、delete等一系列操作的特性进行修改,这就需要`defineProperty`或者`Proxy`进行元编程. 135 | 136 | 我们就以`Proxy`为例来进行编码,当然,我们需要事先了解一下`Proxy`的[使用方法](http://es6.ruanyifeng.com/#docs/proxy#Proxy-revocable). 137 | 138 | 我们先定义一个目标对象 139 | ```javascript 140 | const target = {name: 'Messi', age: 29}; 141 | ``` 142 | 143 | 我们如果想每访问一次这个对象的`age`属性,`age`属性的值就增加`1`. 144 | 145 | ```JavaScript 146 | const target = {name: 'Messi', age: 29}; 147 | const handler = { 148 | get: function(target, key, receiver) { 149 | console.log(`getting ${key}!`); 150 | if (key === 'age') { 151 | const age = Reflect.get(target, key, receiver) 152 | Reflect.set(target, key, age+1, receiver); 153 | return age+1 154 | } 155 | return Reflect.get(target, key, receiver); 156 | } 157 | }; 158 | 159 | const a = new Proxy(target, handler); 160 | 161 | console.log(a.age, a.age); 162 | //getting age! 163 | //getting age! 164 | //30 31 165 | ``` 166 | 167 | 是的`Proxy`就像一个代理器,当有人对目标对象进行处理(set、has、get等等操作)的时候它会拦截操作,并用我们提供的代码进行处理,此时`Proxy`相当于一个中介或者叫代理人,当然`Proxy`的名字也说明了这一点,它经常被用于代理模式中,例如字段验证、缓存代理、访问控制等等。 168 | 169 | 我们的目的很简单,就是利用`Proxy`的特性,在外部对目标对象进行修改的时候来进行额外操作保证数据的不可变。 170 | 171 | 在外部对目标对象进行修改的时候,我们可以将被修改的引用的那部分进行拷贝,这样既能保证效率又能保证可靠性. 172 | 173 | 1. 那么如何判断目标对象是否被修改过,最好的方法是维护一个状态 174 | 175 | ```javascript 176 | function createState(target) { 177 | this.modified = false; // 是否被修改 178 | this.target = target; // 目标对象 179 | this.copy = undefined; // 拷贝的对象 180 | } 181 | ``` 182 | 183 | 2. 此时我们就可以通过状态判断来进行不同的操作了 184 | 185 | ```JavaScript 186 | createState.prototype = { 187 | // 对于get操作,如果目标对象没有被修改直接返回原对象,否则返回拷贝对象 188 | get: function(key) { 189 | if (!this.modified) return this.target[key]; 190 | return this.copy[key]; 191 | }, 192 | // 对于set操作,如果目标对象没被修改那么进行修改操作,否则修改拷贝对象 193 | set: function(key, value) { 194 | if (!this.modified) this.markChanged(); 195 | return (this.copy[key] = value); 196 | }, 197 | 198 | // 标记状态为已修改,并拷贝 199 | markChanged: function() { 200 | if (!this.modified) { 201 | this.modified = true; 202 | this.copy = shallowCopy(this.target); 203 | } 204 | }, 205 | }; 206 | 207 | // 拷贝函数 208 | function shallowCopy(value) { 209 | if (Array.isArray(value)) return value.slice(); 210 | if (value.__proto__ === undefined) 211 | return Object.assign(Object.create(null), value); 212 | return Object.assign({}, value); 213 | } 214 | ``` 215 | 3. 最后我们就可以利用构造函数`createState`接受目标对象`state`生成对象`store`,然后我们就可以用`Proxy`代理`store`,`producer`是外部传进来的操作函数,当`producer`对代理对象进行操作的时候我们就可以通过事先设定好的`handler`进行代理操作了. 216 | 217 | ```JavaScript 218 | const PROXY_STATE = Symbol('proxy-state'); 219 | const handler = { 220 | get(target, key) { 221 | if (key === PROXY_STATE) return target; 222 | return target.get(key); 223 | }, 224 | set(target, key, value) { 225 | return target.set(key, value); 226 | }, 227 | }; 228 | 229 | // 接受一个目标对象和一个操作目标对象的函数 230 | function produce(state, producer) { 231 | const store = new createState(state); 232 | const proxy = new Proxy(store, handler); 233 | 234 | producer(proxy); 235 | 236 | const newState = proxy[PROXY_STATE]; 237 | if (newState.modified) return newState.copy; 238 | return newState.target; 239 | } 240 | 241 | ``` 242 | 243 | 4. 我们可以验证一下,我们看到`producer`并没有干扰到之前的目标函数. 244 | ```JavaScript 245 | const baseState = [ 246 | { 247 | todo: 'Learn typescript', 248 | done: true, 249 | }, 250 | { 251 | todo: 'Try immer', 252 | done: false, 253 | }, 254 | ]; 255 | 256 | const nextState = produce(baseState, draftState => { 257 | draftState.push({todo: 'Tweet about it', done: false}); 258 | draftState[1].done = true; 259 | }); 260 | 261 | console.log(baseState, nextState); 262 | /* 263 | [ { todo: 'Learn typescript', done: true }, 264 | { todo: 'Try immer', done: true } ] 265 | 266 | [ { todo: 'Learn typescript', done: true , 267 | { todo: 'Try immer', done: true }, 268 | { todo: 'Tweet about it', done: false } ] 269 | */ 270 | ``` 271 | 272 | 实际上这个实现就是不可变数据库[immer](https://github.com/mweststrate/immer) 273 | 的迷你版,我们阉割了大量的代码才缩小到了60行左右来实现这个基本功能,实际上除了`get/set`操作,这个库本身有`has/getOwnPropertyDescriptor/deleteProperty`等一系列的实现,我们由于篇幅的原因很多代码也十分粗糙,深入了解可以移步完整源码. 274 | 275 | --- 276 | 本文主要参考: 277 | 1. [immer](https://github.com/mweststrate/immer/blob/master/src/proxy.js) 278 | 279 | 280 | 281 | -------------------------------------------------------------------------------- /IO/Buffer.md: -------------------------------------------------------------------------------- 1 | # Node中Buffers详解 2 | 3 | 4 | --- 5 | 6 | #### **前言** 7 | 8 | Buffer 是 Node.js 中用于处理二进制数据的类, 其中与 IO 相关的操作 (网络/文件等) 均基于 Buffer,Buffer类是Node中的一个全局变量,这就意味着你不需要用额外的`require`将模块引入就可以使用它. 9 | 10 | 值得一提的是,在`Node 6.x`之前,我们都是用`new buffer`进行buffer相关的操作,但是随着ES2015的普及和`new buffer`自身安全性的问题,这个api已经被废弃. 11 | 12 | 因此,我们需要重新探究在新的标准下buffer的使用 13 | 14 | 在阅读本文前,你可以先了解一下ES2015中[二进制数组](http://es6.ruanyifeng.com/?search=buffer&x=0&y=0#docs/arraybuffer)的相关知识. 15 | 16 | --- 17 | #### 1.Buffer类的使用 18 | 19 | #### 1.1 创建buffer 20 | 21 | 由于`new buffer`的废弃,用来替换它的Buffer.from()、Buffer.alloc()、和 Buffer.allocUnsafe() 方法应运而生。 22 | 23 | | 方法 | 用途 | 24 | |---|---| 25 | |Buffer.from()|根据已有数据生成一个 Buffer 对象| 26 | |Buffer.alloc()|创建一个初始化后的 Buffer 对象| 27 | |Buffer.allocUnsafe()|创建一个未初始化的 Buffer 对象| 28 | 29 | ```javascript 30 | //创建一个根据数组[1,2,3]生成的buffer 31 | const buffer1 = Buffer.from([1,2,3]); 32 | console.log(buffer1); // 33 | 34 | // 创建一个长度为 10、且用 0x1 填充的 Buffer。 35 | const buffer2 = Buffer.alloc(10, 1); 36 | console.log(buffer2); // 37 | 38 | // 创建一个长度为 10、且未初始化的 Buffer。 39 | const buffer3 = Buffer.allocUnsafe(10); 40 | console.log(buffer3); // 41 | 42 | ``` 43 | 44 | #### 1.2buffer转换 45 | 46 | 我们先设想一个场景,目前有一个`1.txt`文件,内容如下: 47 | ``` 48 | aaa 49 | bbb 50 | ccc 51 | ddd 52 | 53 | ``` 54 | 我们需要读取`1.txt`内容 55 | ```javascript 56 | const fs = require('fs'); 57 | 58 | fs.readFile('1.txt', (err, data) => { 59 | console.log(data); 60 | }); // 61 | 62 | ``` 63 | 此时会发现读取出来的是二进制的类数组,此时我们就需要进行buffer编码转换. 64 | 65 | ```javascript 66 | fs.readFile('1.txt', (err, data) => { 67 | console.log(data.toString()); //转换成字符串 68 | }); 69 | // aaa 70 | // bbb 71 | // ccc 72 | // ddd 73 | ``` 74 | 75 | 以上就是buffer编码转换的应用场景之一,Buffer是可以与字符串进行转换,但是仅限于以下编码格式: 76 | 77 | 78 | 'ascii' - 仅支持 7 位 ASCII 数据。如果设置去掉高位的话,这种编码是非常快的。 79 | 80 | 'utf8' - 多字节编码的 Unicode 字符。许多网页和其他文档格式都使用 UTF-8 。 81 | 82 | 'utf16le' - 2 或 4 个字节,小字节序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。 83 | 84 | 'ucs2' - 'utf16le' 的别名。 85 | 86 | 'base64' - Base64 编码。当从字符串创建 Buffer 时,按照 RFC4648 第 5 章的规定,这种编码也将正确地接受“URL 与文件名安全字母表”。 87 | 88 | 'latin1' - 一种把 Buffer 编码成一字节编码的字符串的方式(由 IANA 定义在 RFC1345 第 63 页,用作 Latin-1 补充块与 C0/C1 控制码)。 89 | 90 | 'binary' - 'latin1' 的别名。 91 | 92 | 'hex' - 将每个字节编码为两个十六进制字符。 93 | 94 | 95 | 同样的,我们可以将字符串转换成`base64`编码 96 | ```javascript 97 | let user = 'Messi'; 98 | let pass = '1987'; 99 | let authstring = user + ':' +pass; 100 | let encoded = Buffer.from(authstring).toString('base64'); 101 | console.log(encoded); //TWVzc2k6MTk4Nw== 102 | ``` 103 | 104 | 105 | 106 | #### 1.3 Buffer拼接 107 | 108 | 说起Buffer拼接,就不得不提其中的一个坑,那就是中文. 109 | 110 | ```javascript 111 | let str = '世界,你好'; 112 | let bf = Buffer.from(str); 113 | console.log(bf.slice(0,8).toString()); //世界,� 114 | ``` 115 | 由于一个汉字所占的字节为3,因此在处理汉字的时候,上例只取了8个字节,所以相当于`世界`占了6个字节,`,`占了一个字节,`你`我们只取了其中一个字节,因此`你`就显示乱码了. 116 | 117 | 中文乱码的问题是node.js中十分常见的坑,在用流读取文件时最容易产生中文乱码的情况. 118 | 119 | 我们读取中文文本文件`2.txt`内容如下: 120 | ``` 121 | 王者农药是最农药的游戏 122 | 123 | ``` 124 | 进行流读取操作: 125 | ```javascript 126 | const fs = require('fs'); 127 | 128 | let rs = fs.createReadStream('./2.txt', { 129 | highWaterMark: 5 //最大限制5个字节读取 130 | }); 131 | let txt = ''; 132 | rs.on('data',(chunk) => { 133 | txt += chunk; 134 | }); 135 | rs.on('end',() => { 136 | console.log(txt); 137 | }); 138 | //王���农���是最���药���游戏 139 | ``` 140 | 这里出现乱码了,是因为`txt += chunk`隐含了一个操作,即 `chunk.toString()`,因为txt是String类型的。因此相当于是`txt += chunk.toString()`. 141 | 142 | 由于每次限定只读取5个字节,所以不少汉字被截断,造成了大量乱码的情况. 143 | 144 | 145 | 一个可行的办法是,先将读取到的多个小`Buffer`拼接成一个大`Buffer`,再对大`Buffer`进行转换,此时就避免了单个转换造成的部分中文字节不完整造成的乱码现象. 146 | 147 | ```javascript 148 | const fs = require('fs'); 149 | 150 | let rs = fs.createReadStream('./2.txt', { 151 | highWaterMark: 5 152 | }); 153 | let buf = []; 154 | let size = 0; 155 | rs.on('data',chunk => { 156 | buf.push(chunk); 157 | size += chunk.length; 158 | }); 159 | rs.on('end',() => { 160 | let buffer1 = Buffer.concat(buf, size); 161 | console.log(buffer1.toString()); //王者农药是最农药的游戏 162 | }); 163 | ``` 164 | 但是如果文件过大,使得大`Buffer`超过了我们的内存,此时需要借助第三方模块`string_decoder`来进行处理,有兴趣的话可以自行探究. 165 | 166 | --- 167 | 168 | #### 3.Buffer底层探究 169 | 170 | --- 171 | 172 | #### 3.1 Buffer的组成 173 | 174 | `Buffer`是Node中很常见的由JavaScript与c++组合而成的模块,c++负责底层和性能的部分,其余则是由js来负责,因此由于有了c++的支持,`Buffer`的性能还是很有保证的. 175 | ![](http://i2.muimg.com/567571/d52d38adb442582c.png) 176 | 177 | 178 | #### 3.2 Buffer与内存 179 | 180 | 我从网络上寻找到了一张图形象地解释了Buffer的内存分配管理体系. 181 | ![](http://i4.buimg.com/567571/226ba23149c08c5e.png) 182 | 183 | 3.2.1 `Buffer.from` 184 | 185 | `Buffer.from`的作用是**根据已有数据生成一个 Buffer 对象**,那么它的内部是如何运作的呢? 186 | 187 | 我们可以看看源码,一探究竟. 188 | ```javascript 189 | Buffer.from = function(value, encodingOrOffset, length) { 190 | if (typeof value === 'number') 191 | throw new TypeError('"value" argument must not be a number'); 192 | 193 | if (value instanceof ArrayBuffer) 194 | return fromArrayBuffer(value, encodingOrOffset, length); 195 | 196 | if (typeof value === 'string') 197 | return fromString(value, encodingOrOffset); 198 | 199 | return fromObject(value); 200 | }; 201 | ``` 202 | **1.`ArrayBuffer`** 203 | 204 | 我们可以清楚地看到,当`value`为ArrayBuffer的实例,将会返回`return fromArrayBuffer(value, encodingOrOffset, length)`. 205 | 206 | `return fromArrayBuffer(value, encodingOrOffset, length)`会通过返回`binding.createFromArrayBuffer(obj, byteOffset, length)`来操作c++模块进行底层处理,源码如下: 207 | 208 | ```javascript 209 | // fromArrayBuffer: 210 | function fromArrayBuffer(obj, byteOffset, length) { 211 | byteOffset >>>= 0; 212 | 213 | if (typeof length === 'undefined') 214 | return binding.createFromArrayBuffer(obj, byteOffset); 215 | 216 | length >>>= 0; 217 | return binding.createFromArrayBuffer(obj, byteOffset, length); 218 | } 219 | // c++ 模块: 220 | void CreateFromArrayBuffer(const FunctionCallbackInfo& args) { 221 | ... 222 | Local ab = args[0].As(); 223 | ... 224 | Local ui = Uint8Array::New(ab, offset, max_length); 225 | ... 226 | args.GetReturnValue().Set(ui); 227 | } 228 | ``` 229 | 如果不懂c++也没关系,你只需要知道这一系列操作是这样的: 230 | 1. `Buffer.from`通过已有的`ArrayBuffer`数据生成了`Buffer` 对象. 231 | 2. 此时的`Buffer`对象并没有重新申请内存而是与`ArrayBuffer`共享了内存. 232 | 233 | 我们可以通过实例来证明这一点: 234 | ```javascript 235 | let a = new ArrayBuffer(8); 236 | let b = new Uint8Array(a); 237 | let c = Buffer.from(a); 238 | 239 | console.log(b); //Uint8Array [ 0, 0, 0, 0, 0, 0, 0, 0 ] 240 | console.log(c); // 241 | 242 | b[0] = 11; 243 | console.log(b); //Uint8Array [ 11, 0, 0, 0, 0, 0, 0, 0 ] 244 | console.log(c); // 245 | ``` 246 | 我们看到,`b`被修改后,`c`也跟着发生了变动,可见内存是共享的. 247 | 248 | **2. `string`** 249 | 250 | 在`value === 'string'`的情况下返回`fromString(value, encodingOrOffset)`,fromString的源码如下: 251 | 252 | ```javascript 253 | function fromString(string, encoding) { 254 | ... 255 | var length = byteLength(string, encoding); 256 | if (length === 0) 257 | return Buffer.alloc(0); 258 | // 进行判断,当字节大于4KB会直接分配内存 259 | if (length >= (Buffer.poolSize >>> 1)) 260 | return binding.createFromString(string, encoding); 261 | // 当小于4KB,会通过pool进行分配 262 | if (length > (poolSize - poolOffset)) 263 | createPool(); 264 | var actual = allocPool.write(string, poolOffset, encoding); 265 | var b = allocPool.slice(poolOffset, poolOffset + actual); 266 | poolOffset += actual; 267 | alignPool(); 268 | return b; 269 | } 270 | ``` 271 | 以上源码相当于对根据字符串创建`Buffer`所需字节进行一个判断,所需大于4KB会直接创建内存,当所需小于4KB会借助pool进行分配,原因是为了合理分配内存,避免内存的浪费. 272 | 273 | **3.`Buffer/TypeArray/Array`** 274 | 在`value`既不是`string`也不是`ArryBuffer`的情况下,会返回`fromObject(value)`,其相关源码如下: 275 | ```javascript 276 | function fromObject(obj) { 277 | // 当为Buffer时 278 | if (obj instanceof Buffer) { 279 | ... 280 | const b = allocate(obj.length); 281 | obj.copy(b, 0, 0, obj.length); 282 | return b; 283 | } 284 | // 当为TypeArray或Array时 285 | if (obj) { 286 | if (obj.buffer instanceof ArrayBuffer || 'length' in obj) { 287 | ... 288 | return fromArrayLike(obj); 289 | } 290 | if (obj.type === 'Buffer' && Array.isArray(obj.data)) { 291 | return fromArrayLike(obj.data); 292 | } 293 | } 294 | 295 | throw new TypeError(kFromErrorMsg); 296 | } 297 | // 进行copy 298 | function fromArrayLike(obj) { 299 | const length = obj.length; 300 | const b = allocate(length); 301 | for (var i = 0; i < length; i++) 302 | b[i] = obj[i] & 255; 303 | return b; 304 | } 305 | 306 | ``` 307 | 与`ArryBuffer`的情况不同,`fromObject(obj)`进行了复制,另创建了新的内存空间,并没有进行内存共享. 308 | 我们看一个实例: 309 | 310 | ```javascript 311 | let a = Buffer.from([1,2,3]); 312 | let b = Buffer.from(a); 313 | console.log(a); // 314 | console.log(b); // 315 | 316 | a[1] = 12; 317 | console.log(a); // 318 | console.log(b); // 319 | ``` 320 | 321 | 明显看出来,`b`并没有受到`a`值改变的影响,因为它另创建了自身的内存空间,所以`a` `b`内存空间相互独立,互不影响. 322 | 323 | 3.2.2 `Buffer.alloc` `Buffer.allocUnSafe` `Buffer.allocUnsafeSlow` 324 | 325 | `Buffer.alloc`:先`createBuffer`申请内存,然后再进行填充,会覆盖旧数据,安全性较好,不借助`Buffer pool`而是直接分配内存. 326 | `Buffer.allocUnSafe`:借助`Buffer pool`分配内存,不会覆盖旧数据,安全性较差 327 | `Buffer.allocUnsafeSlow`:直接分配内存,但是不会对旧数据覆盖,因此安全性也较差. 328 | 329 | 330 | 331 | 332 | 333 | -------------------------------------------------------------------------------- /Framework/V-DOM.md: -------------------------------------------------------------------------------- 1 | # 如何实现一个虚拟DOM 2 | 3 | --- 4 | 5 | ### 1. 虚拟DOM简述 6 | 7 | DOM是什么,我们不需要多说,上古时代的前端工程师们就是靠DOM活着的,当初读着入门的人们眼里js就等于DOM,以至于当Node.js横空出世的时候,某些前端工程师在Node中调用DOM(出自justjavac在知乎上讲得著名案例). 8 | 9 | 现在大家都知道DOM是浏览器环境中才有的东西,是我们的前端开发最终还是离不开DOM的,可是为什么会有虚拟DOM出现,这就不得不提DOM的几个缺点: 10 | 11 | >1.慢: 在现代浏览器运行的DOM本身并不慢,慢的是DOM被操作后产生的副作用(回流、重绘) 12 | 13 | [DOM真的很慢吗?如果真的是,为什么不去改进它?](https://www.zhihu.com/question/30498056) 14 | 15 | > 2.缺乏通用性: DOM是浏览器环境的产物,脱离了浏览器他就不复存在,所以在Node中用DOM是不可以的. 16 | 17 | **虚拟DOM的出现正是用来解决上述问题.** 18 | 19 | 1. 虚拟DOM本质是JavaScript对象,他具有良好的通用性,正是因为有了虚拟DOM,vue和React才可以实现服务端渲染和Rn/Weex等移动技术. 20 | 21 | 2. 也是因为虚拟DOM是JavaScript对象,因此操作起来速度极快,通过diff算法找出差异,最后只需要将差异的部分批量操作DOM,避免我们频繁直接操作DOM而产生性能问题. 22 | 23 | --- 24 | 25 | ### 2. 实现虚拟DOM 26 | 27 | 虚拟DOM是一个思想,不同的库或者框架采用的不同的实现方法,但是本质上都是一样的,就是用js模拟DOM,我们先看一下真实的DOM有哪些属性. 28 | 29 | ![](http://omrbgpqyl.bkt.clouddn.com/18-3-12/85127059.jpg) 30 | 31 | 我把DOM的属性打印了出来,很明显DOM是一个十分**重**的对象,虚拟DOM相对而言就轻量了太多。 32 | 33 | 举个例子: 34 | 35 | 我们在React的JSX中写一段代码 36 | 37 | ```html 38 |
    39 |
  • 1
  • 40 |
  • 2
  • 41 |
42 | ``` 43 | 44 | 实际上在执行过程中是这样的 45 | 46 | ```javascript 47 | React.createElement( 48 | 'ul', 49 | {className: 'todo'}, 50 | ) 51 | ``` 52 | 53 | 我们希望只用`ul` `{className: 'todo'}`等少数标识就能模拟一个DOM。 54 | 55 | ### 2.1 社区的主流实践 56 | 57 | 目前社区主流的`Virtual DOM`实现方法有三种: 58 | 1. 一种是先驱者React实现的虚拟dom,这种方法在React 16.x版本中已经被彻底重写为Fiber架构,因此我们就不过多探究 59 | > Fiber架构是个大话题,借鉴了操作系统的调度方法,React团队花了一年打造,目前依然在改进Fiber。 60 | 2. 一种是[snabbdom](https://github.com/snabbdom/snabbdom)的实现方法,目前许多虚拟DOM的实现都是基于snabbdom,或许是最主流的实现方法,比较著名的就是Vue,它大量借鉴了snabbdom。 61 | 3. 另外一种是高性能的`Virtual DOM`,以Inferno.js为代表,它优化了diff算法和`Virtual DOM`的数据结构,使得性能大幅提高。 62 | 63 | > 社区还有各种各样或复杂或简单的实现方法,其实大同小异,我们就不过多列举了。 64 | 65 | 我们先探究一下社区内最主流的虚拟DOM实现方法。 66 | 67 | ### 2.2 定义虚拟DOM的数据结构 68 | 69 | > 我们的实现是基于snabbdom的简化版,省去了大量的类型判断和兼容性代码,只能作为学习使用。 70 | 71 | 真实DOM是一个对象,自然使用对象作为虚拟DOM的数据结构最为合理。 72 | 73 | ```JavaScript 74 | /** 75 | * 生成 vnode 76 | * @param {String} type 类型,如 'p' 'span'等 77 | * @param {Object} data data,包括属性,事件等等 78 | * @param {Array} children 子 vnode 79 | * @param {String} text 文本 80 | * @param {Element} elm 对应的dom 81 | * @param {String} key 唯一标识 82 | * @return {Object} vnode 83 | */ 84 | function vnode(type, data, children, text, elm, key) { 85 | const element = { 86 | type, data, children, text, elm,key, 87 | } 88 | 89 | return element 90 | } 91 | ``` 92 | 93 | 虚拟DOM的模拟其实很简单,以上几行代码就能模拟一个虚拟DOM了,但是这才是刚刚开始,我们都知道真实DOM是以树的形式呈现的,我们需要一个函数,帮助我们构造虚拟DOM树。 94 | 95 | 我们需要类似于`createElement`的函数,利用`ul`等参数形成虚拟DOM. 96 | 97 | ```javascript 98 | React.createElement( 99 | 'ul', 100 | {className: 'todo'}, 101 | ) 102 | ``` 103 | 104 | 我们实现一个h函数,接收一个`type`代表标签名称(例如‘div’等),接收一个`config`代表属性(例如样式属性等),接收一个`children`代表子节点。 105 | 106 | ```JavaScript 107 | 108 | // 判断是否是number 或者 string 109 | isPrimitive = val => { 110 | return typeof val === 'number' || typeof val === 'string' 111 | } 112 | 113 | function h(type, config, ...children) { 114 | // 建立空对象,作为虚拟dom属性 115 | const props = {} 116 | 117 | // 默认key为null 118 | let key = null 119 | 120 | // 获取 key,对props赋值 121 | if (config) { 122 | if (config.key) { 123 | key = config.key 124 | } 125 | 126 | for (let propName in config) { 127 | if (!config.hasOwnProperty(propName)) { 128 | props[propName] = config[propName] 129 | } 130 | } 131 | } 132 | // 成功转化为虚拟DOM 133 | return vnode( 134 | type, 135 | props, 136 | flattenArray(children).map(c => { 137 | return isPrimitive(c) ? vnode(undefined, undefined, undefined, c, undefined) : c 138 | }), 139 | key, 140 | ) 141 | } 142 | ``` 143 | 144 | ### 2.3 diff算法的可行性 145 | 146 | 现在我们梳理一下逻辑,以React为例,我们平时在书写的是jsx,它类似于HTML和JavaScript的混写,但是真正在执行过程中是事先需要babel将其转换成`React.createElement`的形式,当然我们目前已经实现了一个类似于此方法的`h`函数,但是真正要我们书写的jsx中的代码转换成真实DOM的过程我们还没有涉及,我们仅仅实现了jsx到虚拟DOM的过程,那么接下来我们需要完成剩余的工作。 147 | 148 | 我们都知道虚拟DOM可以以接近最小的代价来局部刷新页面,也就是所谓的**只更改有变化的地方**,这就不得不引入diff算法。 149 | 150 | > diff算法是计算机世界最常用的算法之一,例如unix系统中的diff命令,git中的git diff都是对diff的典型应用,用于比较两个文本、文件、数据的不同。 151 | 152 | 可是普通的diff算法并不适用于虚拟DOM树的对比,因为两棵树的对比时间复杂度为 O(n^3),看到这里大家心里已经凉了,这个时间复杂度其实就意味着diff是极度低效的,如果虚拟DOM采用这个算法,那么这个架构属于画蛇添足之举。 153 | 154 | 所幸的是,我们在实际项目中基本不存在夸层级之间的DOM操作,因此我们只需要对同层级的虚拟DOM节点进行diff即可,我们的时间复杂度仅仅为O(n)。 155 | ![](http://omrbgpqyl.bkt.clouddn.com/18-3-31/55658635.jpg) 156 | 157 | ### 2.4 diff算法的实现 158 | 159 | ```JavaScript 160 | function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { 161 | let oldStartIdx = 0, newStartIdx = 0 162 | let oldEndIdx = oldCh.length - 1 163 | let oldStartVnode = oldCh[0] 164 | let oldEndVnode = oldCh[oldEndIdx] 165 | let newEndIdx = newCh.length - 1 166 | let newStartVnode = newCh[0] 167 | let newEndVnode = newCh[newEndIdx] 168 | let oldKeyToIdx 169 | let idxInOld 170 | let elmToMove 171 | let before 172 | 173 | // 遍历 oldCh 和 newCh 来比较和更新 174 | while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { 175 | // 1⃣️ 首先检查 4 种情况,保证 oldStart/oldEnd/newStart/newEnd 176 | // 这 4 个 vnode 非空,左侧的 vnode 为空就右移下标,右侧的 vnode 为空就左移 下标。 177 | if (oldStartVnode == null) { 178 | oldStartVnode = oldCh[++oldStartIdx] 179 | } else if (oldEndVnode == null) { 180 | oldEndVnode = oldCh[--oldEndIdx] 181 | } else if (newStartVnode == null) { 182 | newStartVnode = newCh[++newStartIdx] 183 | } else if (newEndVnode == null) { 184 | newEndVnode = newCh[--newEndIdx] 185 | } 186 | /** 187 | * 2⃣️ 然后 oldStartVnode/oldEndVnode/newStartVnode/newEndVnode 两两比较, 188 | * 对有相同 vnode 的 4 种情况执行对应的 patch 逻辑。 189 | * - 如果同 start 或同 end 的两个 vnode 是相同的(情况 1 和 2), 190 | * 说明不用移动实际 dom,直接更新 dom 属性/children 即可; 191 | * - 如果 start 和 end 两个 vnode 相同(情况 3 和 4), 192 | * 那说明发生了 vnode 的移动,同理我们也要移动 dom。 193 | */ 194 | // 1. 如果 oldStartVnode 和 newStartVnode 相同(key相同),执行 patch 195 | else if (isSameVnode(oldStartVnode, newStartVnode)) { 196 | // 不需要移动 dom 197 | patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) 198 | oldStartVnode = oldCh[++oldStartIdx] 199 | newStartVnode = newCh[++newStartIdx] 200 | } 201 | // 2. 如果 oldEndVnode 和 newEndVnode 相同,执行 patch 202 | else if (isSameVnode(oldEndVnode, newEndVnode)) { 203 | // 不需要移动 dom 204 | patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) 205 | oldEndVnode = oldCh[--oldEndIdx] 206 | newEndVnode = newCh[--newEndIdx] 207 | } 208 | // 3. 如果 oldStartVnode 和 newEndVnode 相同,执行 patch 209 | else if (isSameVnode(oldStartVnode, newEndVnode)) { 210 | patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) 211 | // 把获得更新后的 (oldStartVnode/newEndVnode) 的 dom 右移,移动到 212 | // oldEndVnode 对应的 dom 的右边。为什么这么右移? 213 | // (1)oldStartVnode 和 newEndVnode 相同,显然是 vnode 右移了。 214 | // (2)若 while 循环刚开始,那移到 oldEndVnode.elm 右边就是最右边,是合理的; 215 | // (3)若循环不是刚开始,因为比较过程是两头向中间,那么两头的 dom 的位置已经是 216 | // 合理的了,移动到 oldEndVnode.elm 右边是正确的位置; 217 | // (4)记住,oldVnode 和 vnode 是相同的才 patch,且 oldVnode 自己对应的 dom 218 | // 总是已经存在的,vnode 的 dom 是不存在的,直接复用 oldVnode 对应的 dom。 219 | api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)) 220 | oldStartVnode = oldCh[++oldStartIdx] 221 | newEndVnode = newCh[--newEndIdx] 222 | } 223 | // 4. 如果 oldEndVnode 和 newStartVnode 相同,执行 patch 224 | else if (isSameVnode(oldEndVnode, newStartVnode)) { 225 | patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) 226 | // 这里是左移更新后的 dom,原因参考上面的右移。 227 | api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) 228 | oldEndVnode = oldCh[--oldEndIdx] 229 | newStartVnode = newCh[++newStartIdx] 230 | } 231 | 232 | // 3⃣️ 最后一种情况:4 个 vnode 都不相同,那么我们就要 233 | // 1. 从 oldCh 数组建立 key --> index 的 map。 234 | // 2. 只处理 newStartVnode (简化逻辑,有循环我们最终还是会处理到所有 vnode), 235 | // 以它的 key 从上面的 map 里拿到 index; 236 | // 3. 如果 index 存在,那么说明有对应的 old vnode,patch 就好了; 237 | // 4. 如果 index 不存在,那么说明 newStartVnode 是全新的 vnode,直接 238 | // 创建对应的 dom 并插入。 239 | else { 240 | // 如果 oldKeyToIdx 不存在,创建 old children 中 vnode 的 key 到 index 的 241 | // 映射,方便我们之后通过 key 去拿下标。 242 | if (oldKeyToIdx === undefined) { 243 | oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) 244 | } 245 | // 尝试通过 newStartVnode 的 key 去拿下标 246 | idxInOld = oldKeyToIdx[newStartVnode.key] 247 | // 下标不存在,说明 newStartVnode 是全新的 vnode。 248 | if (idxInOld == null) { 249 | // 那么为 newStartVnode 创建 dom 并插入到 oldStartVnode.elm 的前面。 250 | api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm) 251 | newStartVnode = newCh[++newStartIdx] 252 | } 253 | // 下标存在,说明 old children 中有相同 key 的 vnode, 254 | else { 255 | elmToMove = oldCh[idxInOld] 256 | // 如果 type 不同,没办法,只能创建新 dom; 257 | if (elmToMove.type !== newStartVnode.type) { 258 | api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm) 259 | } 260 | // type 相同(且key相同),那么说明是相同的 vnode,执行 patch。 261 | else { 262 | patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) 263 | oldCh[idxInOld] = undefined 264 | api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm) 265 | } 266 | newStartVnode = newCh[++newStartIdx] 267 | } 268 | } 269 | } 270 | 271 | // 上面的循环结束后(循环条件有两个),处理可能的未处理到的 vnode。 272 | // 如果是 new vnodes 里有未处理的(oldStartIdx > oldEndIdx 273 | // 说明 old vnodes 先处理完毕) 274 | if (oldStartIdx > oldEndIdx) { 275 | before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm 276 | addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) 277 | } 278 | // 相反,如果 old vnodes 有未处理的,删除 (为处理 vnodes 对应的) 多余的 dom。 279 | else if (newStartIdx > newEndIdx) { 280 | removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) 281 | } 282 | } 283 | ``` -------------------------------------------------------------------------------- /IO/stream.md: -------------------------------------------------------------------------------- 1 | # Node中Stream(流)详解 2 | 3 | --- 4 | 5 | ## 前言 6 | 7 | 流(stream)在 Node.js 中是处理流数据的抽象接口(abstract interface),其中Node.js 中的 `HTTP `请求 和 `process.stdout` 就都是流的实例. 8 | 9 | 可以这么说,`Stream`是Node开发过程中无论如何都无法绕开的知识点,因为基于它的场景很多,我们应该尽可能理解`Stream`并掌握它的一些高级用法. 10 | 11 | Stream 接口分成四类: 12 | 可读数据流接口,用于对外提供数据。 13 | 可写数据流接口,用于写入数据。 14 | 双向数据流接口,用于读取和写入数据。 15 | 转换流接口,可读可写但不保存数据,负责数据转换 16 | 17 | 18 | ```javascript 19 | var Stream = require('stream') 20 | 21 | var Readable = Stream.Readable 22 | var Writable = Stream.Writable 23 | var Duplex = Stream.Duplex 24 | var Transform = Stream.Transform 25 | ``` 26 | 27 | 由于`Stream`基本方法较多,我们在本文中就不做过多介绍,相关的基本用法可以直接阅读[官方文档](http://nodejs.cn/api/stream.html#stream_two_modes). 28 | 29 | --- 30 | 31 | #### 1. Stream 中Readable的运作流程 32 | 33 | Readable有两种模式,分别是`flowing mode`和`paused mode`,这两种模式的不同之处在于:是否需要手动`Readable.prototype.read(n)`,读取缓冲区数据. 34 | 35 | 如何触发这两种模式? 36 | flowing mode: 注册data事件、resume方法、pipe方法 37 | paused mode: pause方法、移除data、unpipe方法 38 | 39 | 40 | ```javascript 41 | // resume触发flowing mode 42 | Readable.prototype.resume = function() { 43 | var state = this._readableState; 44 | if (!state.flowing) { 45 | debug('resume'); 46 | state.flowing = true; 47 | resume(this, state); 48 | } 49 | return this; 50 | } 51 | ``` 52 | 在源码内部全部都是用`resume`方法来住触发flowing mode的(包括resume自身),而调用`resume`触发flowing mode的关键标志是`state.flowing`,源码通过`state.flowing`来判断是否调用`resume`. 53 | 54 | 那么,这个`state.flowing`来自哪里呢? 55 | 56 | 其实`state.flowing`是来自于`_readableState`,`_readableState`同时是`ReadableState`的实例,由于源码量有千行我不便一一贴出来,[相关源码](https://github.com/nodejs/node/blob/master/lib/_stream_readable.js)可以自行查看,我们可以简单介绍下`ReadableState`. 57 | 58 | `ReadableState`其实是一个构造函数,用于记录`Readable`的各种状态和信息,例如读取模式、highWaterMask、编码(默认utf-8)、各种事件、缓冲区、flowing模式等. 59 | 60 | 可以说,Readable内部各种状态转换或者缓存读取等操作,都需要依赖`ReadableState`提供的信息支持. 61 | 62 | 我们都知道可读流有三种状态: 63 | 64 | readable._readableState.flowing = null 65 | readable._readableState.flowing = false 66 | readable._readableState.flowing = true 67 | 68 | 我们调用上述`resume` `pipe`等方法可以改变状态,但是最初始的 ` readable._readableState.flowing = null` 状态就是在`ReadableState`中定义的. 69 | 70 | ```javascript 71 | function ReadableState(options, stream) { 72 | ... 73 | this.flowing = null; 74 | ... 75 | } 76 | ``` 77 | 78 | 此时我们需要看一下Stream整个运行机制的示意图: 79 | 80 | ![](http://i1.piimg.com/567571/82c592bb2211a405.png) 81 | 82 | 我们只对Readable部分进行解读 83 | >* _read:可读数据流的`_read`方法,可以将数据放入可读数据流,真正从外部读取数据的是此方法 84 | >* push:`push` `unshift`等方法将数据压入**读缓存区**. 85 | >* 读缓存区:以数组的形式存在,主要存储着`Buffer`等数据缓存 86 | >* read:`read`方法将缓存中的数据读取,不直接从外部读取数据,而是读取读缓存区内的数据 87 | 88 | 通过`push`等方法将数据压入读缓存区的过程中,数据会以`Buffer`的形式被存在数组里,因此在`read`后会出现`Buffer`的形式. 89 | ```javascript 90 | var Read = require('stream').Readable; 91 | var r = new Read(); 92 | 93 | r.push('hello'); 94 | r.push('world'); 95 | r.push(null); 96 | 97 | console.log(r.read()) // 98 | 99 | ``` 100 | 为了避免以上情况,我们通常会选择如下操作: 101 | ```javascript 102 | console.log(r.read().toString()); //helloworld 103 | ``` 104 | 将数据`chunk`转化为`Buffer`的操作正是在`push` `unshift`等方法内部实现的,源码如下: 105 | ```javascript 106 | Readable.prototype.push = function(chunk, encoding) { 107 | var state = this._readableState; 108 | 109 | if (!state.objectMode && typeof chunk === 'string') { 110 | encoding = encoding || state.defaultEncoding; 111 | if (encoding !== state.encoding) { 112 | chunk = Buffer.from(chunk, encoding); //转化为Buffer 113 | encoding = ''; 114 | } 115 | } 116 | 117 | return readableAddChunk(this, state, chunk, encoding, false); 118 | }; 119 | ``` 120 | 121 | 之后我们就需要讨论`read`方法读取缓存数据的问题,首先我们熟悉一下`read`的用法. 122 | >* `read`方法从系统缓存读取并返回数据。如果读不到数据,则返回null。 123 | 124 | >* 该方法可以接受一个整数作为参数,表示所要读取数据的数量,然后会返回该数量的数据。如果读不到足够数量的数据,返回null。如果不提供这个参数,默认返回系统缓存之中的所有数据。 125 | 126 | >* 只在“暂停态”时,该方法才有必要手动调用。“流动态”时,该方法是自动调用的,直到系统缓存之中的数据被读光。 127 | >* 如果该方法返回一个数据块,那么它就触发了data事件。 128 | 129 | ```javascript 130 | var Read = require('stream').Readable; 131 | var r = new Read(); 132 | 133 | r.push('hello'); 134 | r.push('world'); 135 | r.push(null); 136 | console.log(r.read(1).toString()); //h 137 | ``` 138 | 上述代码表示只读取数量(n)为1的数据,可以初步窥探到`read`的基本用法. 139 | 140 | 现在我们通过阅读部分`read`源码来了解它的运作原理. 141 | ```javascript 142 | Readable.prototype.read = function(n) { 143 | ... 144 | if (n === 0 && //当要读取的数据量为0,或者缓存已经满的时候,只触发Readable事件,不进行read读取 145 | state.needReadable && 146 | (state.length >= state.highWaterMark || state.ended)) { 147 | debug('read: emitReadable', state.length, state.ended); 148 | if (state.length === 0 && state.ended) 149 | endReadable(this); 150 | else 151 | emitReadable(this); 152 | return null; //读不到数据返回null 153 | } 154 | 155 | ... 156 | // 设置读取的数量 157 | n = howMuchToRead(n, state); 158 | 159 | //利用doRead判断是否可以开启可读流, 160 | var doRead = state.needReadable; 161 | 162 | // 如果缓存区为空,亦或者未超过设置的警戒线,则可以开启可读流 163 | if (state.length === 0 || state.length - n < state.highWaterMark) { 164 | doRead = true; 165 | debug('length less than watermark', doRead); 166 | } 167 | 168 | if (state.ended || state.reading) {//但是如果已经结束则停止可读流 169 | doRead = false; 170 | debug('reading or ended', doRead); 171 | } else if (doRead) { 172 | debug('do read'); 173 | state.reading = true; 174 | state.sync = true; 175 | if (state.length === 0) 176 | state.needReadable = true; 177 | this._read(state.highWaterMark); //在可读流开启的情况下用_read方法读取一段警戒线大小的数据 178 | state.sync = false; 179 | if (!state.reading) 180 | n = howMuchToRead(nOrig, state); 181 | } 182 | 183 | var ret; 184 | if (n > 0) 185 | ret = fromList(n, state); //在缓存区的数组内提取数量为n的数据,用于从读缓存区的数据读取 186 | else 187 | ret = null; 188 | 189 | if (ret === null) { 190 | state.needReadable = true; 191 | n = 0; 192 | } else { 193 | state.length -= n; 194 | } 195 | 196 | if (state.length === 0) { 197 | if (!state.ended) 198 | state.needReadable = true; 199 | if (nOrig !== n && state.ended) 200 | endReadable(this); 201 | } 202 | 203 | if (ret !== null) 204 | this.emit('data', ret); //通过事件data将从缓存中读取到的数据交出去 205 | 206 | return ret; 207 | }; 208 | ... 209 | 210 | } 211 | ``` 212 | 我们可以梳理一下`Readable`的工作流程: 213 | 1.paused 模式下 214 | 215 | ```javascript 216 | var Read = require('stream').Readable; 217 | var r = new Read(); 218 | 219 | r.push('hello'); 220 | r.push('world'); 221 | r.push(null); 222 | 223 | console.log(r.read().toString()); 224 | ``` 225 | 以上述代码为例,我们梳理一下`Readbale`的内部流程. 226 | 227 | >* 1.首先通过 `new Read()`来创建一个可读流,此时`readable._readableState.flowing = null`,而且这个可读流其他内部模式也被内部函数` ReadableState(options, stream)`初始化为初始状态并保存. 228 | >* 2. `push`方法将数据转化为`Buffer`类型写入读缓存区内,缓存区以数组的形式存在. 229 | >* 3. 再**手动**调用`read`方法进行对读缓存区的读取,首先判断此可读流是否结束,如果没有结束需要调用`_read`方法读取大小等于警戒线(highWaterMark)的数据,同时利用`fromList`去读取缓存中的数据(读取后就将缓存内的数据清除),随后通过`data`事件将数据交出. 230 | >* 4. `_read`会调用`push`方法,`push`方法如果判定没有读取到结束符的情况下继续向缓存中写入数据,同时调用内部方法` maybeReadMore_`触发`read(0)`. 231 | >* 5. 步骤3 与 步骤4 反复循环直到读取到结束符(null)或者出现错误而终止. 232 | 233 | 234 | 2 . flowing 模式下 235 | 236 | 我们可以分别用`pipe`方法与监听`data`事件的方式实现flowing mode,因为下文中会涉及到`pipe`方法,我们姑且以监听`data`事件为例,讲解内部流程. 237 | ```javascript 238 | var Read = require('stream').Readable; 239 | var r = new Read(); 240 | 241 | r.push('hello '); 242 | r.push('world '); 243 | r.push(null) 244 | 245 | r.on('data', function (chunk) { 246 | console.log(chunk.toString()) 247 | }) 248 | 249 | ``` 250 | >* 1.同paused 模式 251 | >* 2. 监听`data`事件后将会自动调用该可读流的`resume`方法,使流切换至流动模式,`readable._readableState.flowing = flowing`,`resume`内部调用私有函数`_resume`,此函数产生`read`的自动调用. 252 | >* 3. 同paused 模式 253 | >* 4. 同paused 模式 254 | >* 5. 同paused 模式 255 | 256 | 257 | 258 | --- 259 | 260 | #### 2. Stream 中Writeable的运作流程 261 | 262 | 有了前面对Readable的分析,我们理解起Writeable就相对容易了很多,因为很多逻辑是相通的,无非是读入与输出的区别. 263 | 264 | 我们再回到上面我们列举的Stream运行示意图中关于Writeable的部分. 265 | ![](http://i1.piimg.com/567571/b2eb4036eec3a0db.png) 266 | 267 | 268 | 我们再回到上面我们列举的Stream运行示意图中关于Writeable的部分: 269 | > write:利用`write`接收到通过`pipe`传过来的数据`chunk`. 270 | > writeOrBuffer:`writeOrBuffer`方法将数据写入**写缓存区**. 271 | > 写缓存区:以链表的形式存在,主要存储着`Buffer`等数据缓存 272 | > _write:`dowrite`调用 `_write`方法将缓存中的数据输出 273 | 274 | Writeable的相关源码可以访问[这里](https://github.com/nodejs/node/blob/master/lib/_stream_writable.js),我们只贴出少部分关键源码以供参考. 275 | 276 | 跟Readable类似,Writeable也有一个记录、管理Writebale相关状态的`WritableState`对象. 277 | 278 | 我们知道,`write`方法负责将数据写入可写流中,而真正负责管理写入的是`writeOrBuffer`方法,其源码如下. 279 | ```javascript 280 | function writeOrBuffer(stream, state, isBuf, chunk, encoding, cb) { 281 | if (!isBuf) { //对数据进行编码处理 282 | var newChunk = decodeChunk(state, chunk, encoding); 283 | if (chunk !== newChunk) { 284 | encoding = 'buffer'; 285 | chunk = newChunk; 286 | } 287 | } 288 | var len = state.objectMode ? 1 : chunk.length; 289 | 290 | state.length += len; 291 | //判断写入的数据时都超过了设定好的预警线,如果超过通过改变`needDrain`表示缓存区已满,停止写入 292 | var ret = state.length < state.highWaterMark; 293 | if (!ret) 294 | state.needDrain = true; 295 | 296 | if (state.writing || state.corked) { //如果可写流正在写数据,那么将数据写入缓存区 297 | var last = state.lastBufferedRequest; 298 | state.lastBufferedRequest = { chunk, encoding, callback: cb, next: null }; 299 | if (last) { 300 | last.next = state.lastBufferedRequest; 301 | } else { 302 | state.bufferedRequest = state.lastBufferedRequest; 303 | } 304 | state.bufferedRequestCount += 1; 305 | } else { 306 | doWrite(stream, state, false, len, chunk, encoding, cb);//如果不在写数据,那么调用`doWrite`方法 307 | } 308 | 309 | return ret; 310 | } 311 | ``` 312 | 实际上,`doWrite`方法也不是将缓存区数据输出的具体方法,它会调用`_write`,而`_write`会触发回调函数`state.onwrite`, 313 | 314 | ```javascript 315 | function onwrite(stream, er) { 316 | var state = stream._writableState; 317 | var sync = state.sync; 318 | var cb = state.writecb; 319 | 320 | onwriteStateUpdate(state); 321 | 322 | if (er) 323 | onwriteError(stream, state, sync, er, cb); 324 | else { 325 | var finished = needFinish(state); 326 | 327 | if (!finished && //将缓存数据输出 328 | !state.corked && 329 | !state.bufferProcessing && 330 | state.bufferedRequest) { 331 | clearBuffer(stream, state); 332 | } 333 | 334 | if (sync) { 335 | process.nextTick(afterWrite, stream, state, finished, cb); 336 | } else { 337 | afterWrite(stream, state, finished, cb); 338 | } 339 | } 340 | } 341 | ``` 342 | 343 | `doWrite`大致工作机制如下:首先调用`clearBuffer`方法将缓存区的数据依次输出并清空,随后触发`afterWrite`方法,进行判断是否结束可写流或者触发`drain`事件通知继续将数据写入可写流. 344 | 345 | 我们梳理的可写流的大概运作机制如下: 346 | >* 1.首先创建一个可读流,并且通过数` WriteableState`进行初始化. 347 | >* 2. `write`方法通过调用 `writeOrBuffer`管理写入写缓存区的操作,如果符合条件就写入缓存区. 348 | >* 3. 同时`writeOrBuffer`中`doWrite`方法负责将缓存区的数据输出,`doWrite`会调用`_write`方法,而`_write`会触发回调函数`state.onwrite`,`doWrite`首先调用`clearBuffer`方法将缓存区的数据依次输出并清空,随后触发`afterWrite`方法,进行判断是否结束可写流或者触发`drain`事件通知继续将数据写入可写流,再次进入步骤2的流程. 349 | >* 4. 直到触发`end`方法结束该可写流. 350 | -------------------------------------------------------------------------------- /异步/Event.md: -------------------------------------------------------------------------------- 1 | # Node中Events详解 2 | 3 | 4 | --- 5 | ## **前言** 6 | `Events` 是 `Node.js` 中一个非常重要的 `core` 模块, 在 `node` 中有许多重要的 `core API` 都是依赖其建立的. 比如 `Stream` 是基于 `Events` 实现的, 而 `fs, net, http` 等模块都依赖 `Stream`, 所以 `Events` 模块的重要性可见一斑. 7 | 8 | --- 9 | #### 1.基础用法 10 | 11 | **1.1继承** 12 | 虽然你可以通过传统的原型链的方式进行`Events`的继承,但是Node中提供了更标准的方法`util.inherits`用于事件的继承. 13 | 14 | ```javascript 15 | var util = require('util'); 16 | var EventEmitter = require('events').EventEmitter; 17 | 18 | function Music() { 19 | EventEmitter.call(this); 20 | } 21 | 22 | util.inherits(Radio, EventEmitter); 23 | 24 | ``` 25 | 26 | **1.2 监听与触发** 27 | 28 | 通常我们用`on`方法来监听事件,并用`emit`方法触发事件. 29 | 30 | ```javascript 31 | const EventEmitter = require('events').EventEmitter; 32 | const myEmitter = new EventEmitter(); 33 | 34 | const connection = (id) => { 35 | console.log('client id: ' + id); //client id: 6 36 | }; 37 | 38 | myEmitter.on('connection', connection); //监听名为`connection`的事件,并执行connection方法 39 | myEmitter.emit('connection', 6); //触发名为`connection`的事件 40 | 41 | ``` 42 | 43 | 类似的监听方法还有`once`,其作用与`on`类似,只是只触发一次回调. 44 | 45 | ```javascript 46 | const EventEmitter = require('events').EventEmitter; 47 | const myEmitter = new EventEmitter(); 48 | 49 | const connection = (id) => { 50 | console.log('client id: ' + id); //client id: 6 51 | }; 52 | 53 | myEmitter.once('connection', connection); //监听名为`connection`的事件,并执行connection方法 54 | myEmitter.emit('connection', 6); //触发名为`connection`的事件 55 | myEmitter.emit('connection', 7); //触发事件,但不执行回调函数 56 | 57 | ``` 58 | 59 | **1.3 移除监听器** 60 | 61 | 监听器既然可以被创建自然可以被移除`removeListener`方法便是用来移除监听器的回调函数,它接受两个参数,第一个是事件名称,第二个是回调函数名称. 62 | ```javascript 63 | const EventEmitter = require('events').EventEmitter; 64 | const myEmitter = new EventEmitter(); 65 | 66 | const connection = (id) => { 67 | console.log('client id: ' + id); 68 | }; 69 | 70 | myEmitter.once('connection', connection); //监听名为`connection`的事件,并执行connection方法 71 | myEmitter.removeListener('connection', connection); //移除监听器,次后触发的事件全部无效 72 | myEmitter.emit('connection', 7); //无效 73 | myEmitter.emit('connection', 6); //无效 74 | ``` 75 | 如果某事件有多个回调函数,你想一次移除,可以调用`removeAllListeners`方法. 76 | 77 | **1.4 监听新监听器** 78 | `EventEmitter` 实例会在一个监听器被添加到其内部监听器数组之前触发自身的 `'newListener'` 事件。 79 | 80 | 我们可以通过监听这个`'newListener'`事件来追踪新的监听器. 81 | 82 | ```javascript 83 | const myEmitter = new MyEmitter(); 84 | // 只处理一次,所以不会无限循环 85 | myEmitter.once('newListener', (event, listener) => { 86 | if (event === 'event') { 87 | // 在开头插入一个新的监听器 88 | myEmitter.on('event', () => { 89 | console.log('B'); 90 | }); 91 | } 92 | }); 93 | myEmitter.on('event', () => { 94 | console.log('A'); 95 | }); 96 | myEmitter.emit('event'); 97 | // B 98 | // A 99 | ``` 100 | 101 | --- 102 | #### 2.异常管理 103 | 104 | 事件中的异常处理是我们不得不讨论的一个话题,通常我们会采用两种方式来管理我们的异常,一种是通过监听`error`事件的方式,另一种是通过`domain`集中管理,由于`domain`模块即将被废弃我们只讨论第一种方法. 105 | 106 | 107 | ```javascript 108 | const myEmitter = new MyEmitter(); 109 | myEmitter.on('error', (err) => { 110 | console.log('有错误'); 111 | }); 112 | myEmitter.emit('error', new Error('whoops!')); 113 | // 有错误 114 | ``` 115 | 116 | --- 117 | #### 3.Events源码分析 118 | 119 | **3.1 默认的监听器数量限制** 120 | 每个事件默认可以注册最多 `10` 个监听器。 单个 `EventEmitter` 实例的限制可以使用 `emitter.setMaxListeners(n)` 方法改变。 所有 `EventEmitter` 实例的默认值可以使用 `EventEmitter.defaultMaxListeners` 属性改变。 121 | 122 | 那这个方法是如何实现的呢? 123 | 124 | ```javascript 125 | ... 126 | var defaultMaxListeners = 10; 127 | 128 | Object.defineProperty(EventEmitter, 'defaultMaxListeners', { //ES5的defineProperty方法 129 | enumerable: true, //属性可枚举 130 | get: function() { 131 | return defaultMaxListeners; 132 | }, 133 | set: function(arg) { //传入设置的最大监听器数量 134 | // force global console to be compiled. 135 | // see https://github.com/nodejs/node/issues/4467 136 | console; 137 | // check whether the input is a positive number (whose value is zero or 138 | // greater and not a NaN). 139 | if (typeof arg !== 'number' || arg < 0 || arg !== arg) //判断参数是否符合要求 140 | throw new TypeError('"defaultMaxListeners" must be a positive number'); 141 | defaultMaxListeners = arg; 142 | } 143 | }); 144 | ... 145 | ``` 146 | 这是一个很简单的`Object.defineProperty`方法应用案例,对于大多数人理解起来没有太大难度. 147 | 148 | 值得注意的是,一旦修改这个数量,便会影响全部的事件,对于特定的事件我们建议用`emitter.setMaxListeners(n)`修改. 149 | 150 | ```javascript 151 | EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { 152 | if (typeof n !== 'number' || n < 0 || isNaN(n)) 153 | throw new TypeError('"n" argument must be a positive number'); 154 | this._maxListeners = n; 155 | return this; 156 | }; 157 | ``` 158 | 159 | **3.2 事件监听** 160 | 事实上事件监听除了常用`on`方法以外,还有一个同样效果的方法`emitter.addListener(eventName, listener)`,我们可以通过源码窥探一下这两个方法是如何实现的. 161 | 162 | ```javascript 163 | function _addListener(target, type, listener, prepend) { //Events的私有方法,供`addListener` `on`使用 164 | var m; 165 | var events; 166 | var existing; 167 | 168 | if (typeof listener !== 'function') //如果listener不是函数那抛出错误,确保listener可以被执行 169 | throw new TypeError('"listener" argument must be a function'); 170 | 171 | events = target._events; 172 | if (!events) { //保证_events对象已经被创建 173 | events = target._events = Object.create(null); 174 | target._eventsCount = 0; 175 | } else { 176 | // To avoid recursion in the case that type === "newListener"! Before 177 | // adding it to the listeners, first emit "newListener". 178 | if (events.newListener) { //防止递归调用的,这里只有定义了newListener后,才会发送事件newListener 179 | target.emit('newListener', type, 180 | listener.listener ? listener.listener : listener); 181 | 182 | // Re-assign `events` because a newListener handler could have caused the 183 | // this._events to be assigned to a new object 184 | events = target._events; 185 | } 186 | existing = events[type]; 187 | } 188 | 189 | 190 | if (!existing) { //判断事件是否存在 191 | // Optimize the case of one listener. Don't need the extra array object. 192 | existing = events[type] = listener; //如果不存在,直接将listener写入 193 | ++target._eventsCount; 194 | } else { 195 | if (typeof existing === 'function') { //如果是函数那么要转化成为一个数组写入 196 | // Adding the second element, need to change to array. 197 | existing = events[type] = prepend ? [listener, existing] : 198 | [existing, listener]; 199 | } else { 200 | // If we've already got an array, just append. 201 | //如果是数组,直接添加到数组 202 | if (prepend) { 203 | existing.unshift(listener); 204 | } else { 205 | existing.push(listener); 206 | } 207 | } 208 | 209 | // Check for listener leak 210 | if (!existing.warned) { //判断监听数量是否超过最大限制,如果超过报错. 211 | m = $getMaxListeners(target); 212 | if (m && m > 0 && existing.length > m) { 213 | existing.warned = true; 214 | const w = new Error('Possible EventEmitter memory leak detected. ' + 215 | `${existing.length} ${String(type)} listeners ` + 216 | 'added. Use emitter.setMaxListeners() to ' + 217 | 'increase limit'); 218 | w.name = 'MaxListenersExceededWarning'; 219 | w.emitter = target; 220 | w.type = type; 221 | w.count = existing.length; 222 | process.emitWarning(w); 223 | } 224 | } 225 | } 226 | 227 | return target; 228 | } 229 | 230 | EventEmitter.prototype.addListener = function addListener(type, listener) { 231 | return _addListener(this, type, listener, false); //_addListener私有方法供addListener使用 232 | }; 233 | 234 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; //这里可以看出addListener方法与on方法等价 235 | ``` 236 | 237 | **3.3 回调移除方法** 238 | 239 | 240 | ```javascript 241 | EventEmitter.prototype.removeListener = //移除具体时间的回调函数 242 | function removeListener(type, listener) { 243 | var list, events, position, i, originalListener; 244 | 245 | if (typeof listener !== 'function') //检查是否为函数否则报错 246 | throw new TypeError('"listener" argument must be a function'); 247 | // 确保_events存在 248 | events = this._events; 249 | if (!events) 250 | return this; 251 | 252 | list = events[type]; 253 | if (!list) 254 | return this; 255 | 256 | if (list === listener || list.listener === listener) {//如果指定的函数与当前回调相同那么发送removeListener事件删除当前回调函数 257 | if (--this._eventsCount === 0) 258 | this._events = Object.create(null); 259 | else { 260 | delete events[type]; 261 | if (events.removeListener) 262 | this.emit('removeListener', type, list.listener || listener); 263 | } 264 | } else if (typeof list !== 'function') { //如果不是函数的情况下,此时应该是个回调函数组成的数组 265 | position = -1; //设置函数所在的位置为-1.表示先设置不存在于数组中 266 | 267 | for (i = list.length; i-- > 0;) { //在数组列表中查询此回调 268 | if (list[i] === listener || list[i].listener === listener) { 269 | originalListener = list[i].listener; 270 | position = i; 271 | break; 272 | } 273 | } 274 | 275 | if (position < 0) //小于0代表查不到,直接返回 276 | return this; 277 | 278 | if (list.length === 1) { //=1说明已经查到,可以操作删除 279 | list[0] = undefined; 280 | if (--this._eventsCount === 0) { 281 | this._events = Object.create(null); 282 | return this; 283 | } else { 284 | delete events[type]; 285 | } 286 | } else if (position === 0) { 287 | list.shift(); 288 | } else { 289 | spliceOne(list, position); 290 | } 291 | 292 | if (events.removeListener) 293 | this.emit('removeListener', type, originalListener || listener); 294 | } 295 | 296 | return this; 297 | }; 298 | 299 | EventEmitter.prototype.removeAllListeners = 300 | function removeAllListeners(type) { //删除与单一事件相关的所有回调函数 301 | var listeners, events; 302 | 303 | events = this._events; //判断events是否为空 304 | if (!events) 305 | return this; 306 | 307 | // not listening for removeListener, no need to emit 308 | if (!events.removeListener) { // 查看当前是否有removeListener的监听者 309 | if (arguments.length === 0) { 310 | this._events = Object.create(null); 311 | this._eventsCount = 0; 312 | } else if (events[type]) { 313 | if (--this._eventsCount === 0) 314 | this._events = Object.create(null); 315 | else 316 | delete events[type]; 317 | } 318 | return this; 319 | } 320 | 321 | // emit removeListener for all listeners on all events 322 | //如果不指定事件,直接清空所有事件的所有回调函数 323 | if (arguments.length === 0) { 324 | var keys = Object.keys(events); 325 | for (var i = 0, key; i < keys.length; ++i) { 326 | key = keys[i]; 327 | if (key === 'removeListener') continue; //但是唯独不删除removeListener的回调,属于特殊情况 328 | this.removeAllListeners(key); 329 | } 330 | this.removeAllListeners('removeListener'); 331 | this._events = Object.create(null); 332 | this._eventsCount = 0; 333 | return this; 334 | } 335 | 336 | listeners = events[type]; 337 | //如果指定事件,那么删除当前事件下的所有回调 338 | if (typeof listeners === 'function') { 339 | this.removeListener(type, listeners); 340 | } else if (listeners) { 341 | // LIFO order 342 | do { 343 | this.removeListener(type, listeners[listeners.length - 1]); 344 | } while (listeners[0]); 345 | } 346 | 347 | return this; 348 | }; 349 | ``` 350 | 351 | **3.4 事件触发** 352 | 353 | 354 | ```javascript 355 | EventEmitter.prototype.emit = function emit(type) { //触发事件的方法 356 | var er, handler, len, args, i, events, domain; 357 | var needDomainExit = false; 358 | var doError = (type === 'error'); 359 | 360 | events = this._events; 361 | if (events) 362 | doError = (doError && events.error == null); 363 | else if (!doError) 364 | return false; 365 | 366 | domain = this.domain; 367 | 368 | // If there is no 'error' event listener then throw. 369 | // 对error事件做特殊处理 370 | if (doError) { 371 | if (arguments.length > 1) 372 | er = arguments[1]; 373 | if (domain) { 374 | if (!er) 375 | er = new Error('Unhandled "error" event'); 376 | if (typeof er === 'object' && er !== null) { 377 | er.domainEmitter = this; 378 | er.domain = domain; 379 | er.domainThrown = false; 380 | } 381 | domain.emit('error', er); 382 | } else if (er instanceof Error) { 383 | throw er; // Unhandled 'error' event 384 | } else { 385 | // At least give some kind of context to the user 386 | const err = new Error('Unhandled "error" event. (' + er + ')'); 387 | err.context = er; 388 | throw err; 389 | } 390 | return false; 391 | } 392 | //指定触发的事件 393 | handler = events[type]; 394 | //如果没有制定触发事件直接返回 395 | if (!handler) 396 | return false; 397 | 398 | if (domain && this !== process) { //如果domain在使用,那么进入domain处理 399 | domain.enter(); 400 | needDomainExit = true; 401 | } 402 | 403 | var isFn = typeof handler === 'function'; 404 | len = arguments.length; 405 | //Events对于触发事件方法的不定参数进行了性能优化,对三个以下的参数这种情况作了分别作了单个处理,而三个以上则没有进行单个特殊处理,导致参数在3个或者以下的情况下会比3个以上的速度快出了一倍以上 406 | switch (len) { 407 | // fast cases 408 | 409 | case 1: 410 | emitNone(handler, isFn, this); 411 | break; 412 | case 2: 413 | emitOne(handler, isFn, this, arguments[1]); 414 | break; 415 | case 3: 416 | emitTwo(handler, isFn, this, arguments[1], arguments[2]); 417 | break; 418 | case 4: 419 | emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); 420 | break; 421 | // slower 422 | default: 423 | args = new Array(len - 1); 424 | for (i = 1; i < len; i++) 425 | args[i - 1] = arguments[i]; 426 | emitMany(handler, isFn, this, args); 427 | } 428 | 429 | if (needDomainExit) 430 | domain.exit(); 431 | 432 | return true; 433 | }; 434 | ``` 435 | 436 | **3.5 防止死循环** 437 | 438 | 如果我们在同一个时间中继续监听此事件是否会触发死循环? 439 | 440 | ``` 441 | const EventEmitter = require('events') 442 | 443 | let myEventEmitter = new EventEmitter() 444 | 445 | myEventEmitter.on('wtf', function wtf () { 446 | myEventEmitter.on('wtf', wtf) 447 | }) 448 | 449 | myEventEmitter.emit('wtf') 450 | ``` 451 | 我们不妨看一下一下源码: 452 | 453 | ```javascript 454 | ... 455 | function emitMany(handler, isFn, self, args) { 456 | if (isFn) 457 | handler.apply(self, args); 458 | else { 459 | var len = handler.length; 460 | var listeners = arrayClone(handler, len); 461 | for (var i = 0; i < len; ++i) 462 | listeners[i].apply(self, args); 463 | } 464 | } 465 | 466 | ... 467 | function arrayClone(arr, n) { 468 | var copy = new Array(n); 469 | for (var i = 0; i < n; ++i) 470 | copy[i] = arr[i]; 471 | return copy; 472 | } 473 | 474 | ... 475 | ``` 476 | 477 | `handler` 是具体事件监听数组,此时源码使用 `arrayClone` 方法,拷贝出一个副本,我们在执行过程中其实是在执行这个副本,因此再给原监听数组添加新函数的时候,副本是不会增加的,因此避免了死循环. 478 | 479 | 480 | --- 481 | #### 4.一道面试题 482 | 483 | ```javascript 484 | const EventEmitter = require('events'); 485 | 486 | let emitter = new EventEmitter(); 487 | 488 | emitter.on('myEvent', () => { 489 | console.log('hi'); 490 | emitter.emit('myEvent'); 491 | }); 492 | 493 | emitter.emit('myEvent'); 494 | ``` 495 | 以上代码是否会造成死循环? 496 | 497 | 答案是会的,因为在监听器中再次**触发**同一个事件会造成死循环,而且`Events`内部只是用克隆副本的方法避免了**监听**同一事件的死循环,无法避免**触发**事件的循环,因此在使用中要避免这种情况. 498 | 499 | --- 500 | 501 | 502 | 503 | 504 | -------------------------------------------------------------------------------- /JavaScript基础/Proxy.md: -------------------------------------------------------------------------------- 1 | # 实现双向绑定Proxy比defineproperty优劣如何 2 | 3 | --- 4 | 5 | ## **前言** 6 | 7 | **双向绑定**其实已经是一个老掉牙的问题了,只要涉及到MVVM框架就不得不谈的知识点,但它毕竟是Vue的三要素之一. 8 | 9 | ### Vue三要素 10 | 11 | * 响应式: 例如如何监听数据变化,其中的实现方法就是我们提到的双向绑定 12 | * 模板引擎: 如何解析模板 13 | * 渲染: Vue如何将监听到的数据变化和解析后的HTML进行渲染 14 | 15 | 可以实现双向绑定的方法有很多,KnockoutJS基于观察者模式的双向绑定,Ember基于数据模型的双向绑定,Angular基于脏检查的双向绑定,本篇文章我们重点讲面试中常见的基于**数据劫持**的双向绑定。 16 | 17 | 常见的基于数据劫持的双向绑定有两种实现,一个是目前Vue在用的`Object.defineProperty`,另一个是ES2015中新增的`Proxy`,而Vue的作者宣称将在Vue3.0版本后加入`Proxy`从而代替`Object.defineProperty`,通过本文你也可以知道为什么Vue未来会选择`Proxy`。 18 | 19 | > 严格来讲Proxy应该被称为『代理』而非『劫持』,不过由于作用有很多相似之处,我们在下文中就不再做区分,统一叫『劫持』。 20 | 21 | 我们可以通过下图清楚看到以上两种方法在**双向绑定**体系中的关系. 22 | ![](https://user-gold-cdn.xitu.io/2018/5/1/1631801840069098?w=1012&h=234&f=png&s=35400) 23 | 24 | > 基于数据劫持的当然还有已经凉透的`Object.observe`方法,已被废弃。 25 | > **提前声明:** 我们没有对传入的参数进行及时判断而规避错误,仅仅对核心方法进行了实现. 26 | 27 | --- 28 | ## 文章目录 29 | 30 | 1. 基于数据劫持实现的双向绑定的特点 31 | 2. 基于Object.defineProperty双向绑定的特点 32 | 3. 基于Proxy双向绑定的特点 33 | 34 | --- 35 | ### 1.基于数据劫持实现的双向绑定的特点 36 | 37 | #### 1.1 什么是数据劫持 38 | 数据劫持比较好理解,通常我们利用`Object.defineProperty`劫持对象的访问器,在属性值发生变化时我们可以获取变化,从而进行进一步操作。 39 | 40 | ```JavaScript 41 | // 这是将要被劫持的对象 42 | const data = { 43 | name: '', 44 | }; 45 | 46 | function say(name) { 47 | if (name === '古天乐') { 48 | console.log('给大家推荐一款超好玩的游戏'); 49 | } else if (name === '渣渣辉') { 50 | console.log('戏我演过很多,可游戏我只玩贪玩懒月'); 51 | } else { 52 | console.log('来做我的兄弟'); 53 | } 54 | } 55 | 56 | // 遍历对象,对其属性值进行劫持 57 | Object.keys(data).forEach(function(key) { 58 | Object.defineProperty(data, key, { 59 | enumerable: true, 60 | configurable: true, 61 | get: function() { 62 | console.log('get'); 63 | }, 64 | set: function(newVal) { 65 | // 当属性值发生变化时我们可以进行额外操作 66 | console.log(`大家好,我系${newVal}`); 67 | say(newVal); 68 | }, 69 | }); 70 | }); 71 | 72 | data.name = '渣渣辉'; 73 | //大家好,我系渣渣辉 74 | //戏我演过很多,可游戏我只玩贪玩懒月 75 | ``` 76 | 77 | #### 1.2 数据劫持的优势 78 | 目前业界分为两个大的流派,一个是以React为首的单向数据绑定,另一个是以Angular、Vue为主的双向数据绑定。 79 | 80 | > 其实三大框架都是既可以双向绑定也可以单向绑定,比如React可以手动绑定onChange和value实现双向绑定,也可以调用一些双向绑定库,Vue也加入了props这种单向流的api,不过都并非主流卖点。 81 | 82 | 单向或者双向的优劣不在我们的讨论范围,我们需要讨论一下对比其他双向绑定的实现方法,数据劫持的优势所在。 83 | 84 | 1. 无需显示调用: 例如Vue运用数据劫持+发布订阅,直接可以通知变化并驱动视图,上面的例子也是比较简单的实现`data.name = '渣渣辉'`后直接触发变更,而比如Angular的脏检测则需要显示调用`markForCheck`(可以用zone.js避免显示调用,不展开),react需要显示调用`setState`。 85 | 2. 可精确得知变化数据:还是上面的小例子,我们劫持了属性的setter,当属性值改变,我们可以精确获知变化的内容`newVal`,因此在这部分不需要额外的diff操作,否则我们只知道数据发生了变化而不知道具体哪些数据变化了,这个时候需要大量diff来找出变化值,这是额外性能损耗。 86 | 87 | #### 1.3 基于数据劫持双向绑定的实现思路 88 | 89 | **数据劫持**是双向绑定各种方案中比较流行的一种,最著名的实现就是Vue。 90 | 91 | 基于数据劫持的双向绑定离不开`Proxy`与`Object.defineProperty`等方法对对象/对象属性的"劫持",我们要实现一个完整的双向绑定需要以下几个要点。 92 | 93 | 1. 利用`Proxy`或`Object.defineProperty`生成的Observer针对对象/对象的属性进行"劫持",在属性发生变化后通知订阅者 94 | 2. 解析器Compile解析模板中的`Directive`(指令),收集指令所依赖的方法和数据,等待数据变化然后进行渲染 95 | 3. Watcher属于Observer和Compile桥梁,它将接收到的Observer产生的数据变化,并根据Compile提供的指令进行视图渲染,使得数据变化促使视图变化 96 | 97 | ![](https://user-gold-cdn.xitu.io/2018/4/11/162b38ab2d635662?w=711&h=380&f=png&s=32183) 98 | 99 | > 我们看到,虽然Vue运用了数据劫持,但是依然离不开**发布订阅**的模式,之所以在系列2做了[Event Bus的实现](https://juejin.im/post/5ac2fb886fb9a028b86e328c),就是因为我们不管在学习一些框架的原理还是一些流行库(例如Redux、Vuex),基本上都离不开**发布订阅**模式,而*Event*模块则是此模式的经典实现,所以如果不熟悉**发布订阅**模式,建议读一下系列2的文章。 100 | 101 | --- 102 | ### 2.基于Object.defineProperty双向绑定的特点 103 | 104 | 关于`Object.defineProperty`的文章在网络上已经汗牛充栋,我们不想花过多时间在`Object.defineProperty`上面,本节我们主要讲解`Object.defineProperty`的特点,方便接下来与`Proxy`进行对比。 105 | 106 | > 对`Object.defineProperty`还不了解的请阅读[文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) 107 | 108 | 两年前就有人写过基于`Object.defineProperty`实现的[文章](https://segmentfault.com/a/1190000006599500),想深入理解`Object.defineProperty`实现的推荐阅读,本文也做了相关参考。 109 | 110 | > 上面我们推荐的文章为比较完整的实现(400行代码),我们在本节只提供一个极简版(20行)和一个简化版(150行)的实现,读者可以循序渐进地阅读。 111 | 112 | #### 2.1 极简版的双向绑定 113 | 114 | 我们都知道,`Object.defineProperty`的作用就是劫持一个对象的属性,通常我们对属性的`getter`和`setter`方法进行劫持,在对象的属性发生变化时进行特定的操作。 115 | 116 | 我们就对对象`obj`的`text`属性进行劫持,在获取此属性的值时打印`'get val'`,在更改属性值的时候对DOM进行操作,这就是一个极简的双向绑定。 117 | 118 | ```JavaScript 119 | const obj = {}; 120 | Object.defineProperty(obj, 'text', { 121 | get: function() { 122 | console.log('get val');  123 | }, 124 | set: function(newVal) { 125 | console.log('set val:' + newVal); 126 | document.getElementById('input').value = newVal; 127 | document.getElementById('span').innerHTML = newVal; 128 | } 129 | }); 130 | 131 | const input = document.getElementById('input'); 132 | input.addEventListener('keyup', function(e){ 133 | obj.text = e.target.value; 134 | }) 135 | 136 | ``` 137 | 138 |

在线示例 极简版双向绑定 by Iwobi (@xiaomuzhu) on CodePen.

139 | 140 | 141 | #### 2.2 升级改造 142 | 143 | 我们很快会发现,这个所谓的*双向绑定*貌似并没有什么乱用。。。 144 | 145 | 原因如下: 146 | 147 | 1. 我们只监听了一个属性,一个对象不可能只有一个属性,我们需要对对象每个属性进行监听。 148 | 2. 违反开放封闭原则,我们如果了解[开放封闭原则](https://zh.wikipedia.org/zh-hans/%E5%BC%80%E9%97%AD%E5%8E%9F%E5%88%99)的话,上述代码是明显违反此原则,我们每次修改都需要进入方法内部,这是需要坚决杜绝的。 149 | 3. 代码耦合严重,我们的数据、方法和DOM都是耦合在一起的,就是传说中的面条代码。 150 | 151 | 那么如何解决上述问题? 152 | 153 | Vue的操作就是加入了**发布订阅**模式,结合`Object.defineProperty`的劫持能力,实现了可用性很高的双向绑定。 154 | 155 | 首先,我们以**发布订阅**的角度看我们第一部分写的那一坨代码,会发现它的*监听*、*发布*和*订阅*都是写在一起的,我们首先要做的就是解耦。 156 | 157 | 我们先实现一个订阅发布中心,即消息管理员(Dep),它负责储存订阅者和消息的分发,不管是订阅者还是发布者都需要依赖于它。 158 | 159 | ```JavaScript 160 | let uid = 0; 161 | // 用于储存订阅者并发布消息 162 | class Dep { 163 | constructor() { 164 | // 设置id,用于区分新Watcher和只改变属性值后新产生的Watcher 165 | this.id = uid++; 166 | // 储存订阅者的数组 167 | this.subs = []; 168 | } 169 | // 触发target上的Watcher中的addDep方法,参数为dep的实例本身 170 | depend() { 171 | Dep.target.addDep(this); 172 | } 173 | // 添加订阅者 174 | addSub(sub) { 175 | this.subs.push(sub); 176 | } 177 | notify() { 178 | // 通知所有的订阅者(Watcher),触发订阅者的相应逻辑处理 179 | this.subs.forEach(sub => sub.update()); 180 | } 181 | } 182 | // 为Dep类设置一个静态属性,默认为null,工作时指向当前的Watcher 183 | Dep.target = null; 184 | ``` 185 | 186 | 现在我们需要实现监听者(Observer),用于监听属性值的变化。 187 | 188 | ```JavaScript 189 | // 监听者,监听对象属性值的变化 190 | class Observer { 191 | constructor(value) { 192 | this.value = value; 193 | this.walk(value); 194 | } 195 | // 遍历属性值并监听 196 | walk(value) { 197 | Object.keys(value).forEach(key => this.convert(key, value[key])); 198 | } 199 | // 执行监听的具体方法 200 | convert(key, val) { 201 | defineReactive(this.value, key, val); 202 | } 203 | } 204 | 205 | function defineReactive(obj, key, val) { 206 | const dep = new Dep(); 207 | // 给当前属性的值添加监听 208 | let chlidOb = observe(val); 209 | Object.defineProperty(obj, key, { 210 | enumerable: true, 211 | configurable: true, 212 | get: () => { 213 | // 如果Dep类存在target属性,将其添加到dep实例的subs数组中 214 | // target指向一个Watcher实例,每个Watcher都是一个订阅者 215 | // Watcher实例在实例化过程中,会读取data中的某个属性,从而触发当前get方法 216 | if (Dep.target) { 217 | dep.depend(); 218 | } 219 | return val; 220 | }, 221 | set: newVal => { 222 | if (val === newVal) return; 223 | val = newVal; 224 | // 对新值进行监听 225 | chlidOb = observe(newVal); 226 | // 通知所有订阅者,数值被改变了 227 | dep.notify(); 228 | }, 229 | }); 230 | } 231 | 232 | function observe(value) { 233 | // 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听 234 | if (!value || typeof value !== 'object') { 235 | return; 236 | } 237 | return new Observer(value); 238 | } 239 | ``` 240 | 241 | 那么接下来就简单了,我们需要实现一个订阅者(Watcher)。 242 | 243 | ```JavaScript 244 | class Watcher { 245 | constructor(vm, expOrFn, cb) { 246 | this.depIds = {}; // hash储存订阅者的id,避免重复的订阅者 247 | this.vm = vm; // 被订阅的数据一定来自于当前Vue实例 248 | this.cb = cb; // 当数据更新时想要做的事情 249 | this.expOrFn = expOrFn; // 被订阅的数据 250 | this.val = this.get(); // 维护更新之前的数据 251 | } 252 | // 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用 253 | update() { 254 | this.run(); 255 | } 256 | addDep(dep) { 257 | // 如果在depIds的hash中没有当前的id,可以判断是新Watcher,因此可以添加到dep的数组中储存 258 | // 此判断是避免同id的Watcher被多次储存 259 | if (!this.depIds.hasOwnProperty(dep.id)) { 260 | dep.addSub(this); 261 | this.depIds[dep.id] = dep; 262 | } 263 | } 264 | run() { 265 | const val = this.get(); 266 | console.log(val); 267 | if (val !== this.val) { 268 | this.val = val; 269 | this.cb.call(this.vm, val); 270 | } 271 | } 272 | get() { 273 | // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者 274 | Dep.target = this; 275 | const val = this.vm._data[this.expOrFn]; 276 | // 置空,用于下一个Watcher使用 277 | Dep.target = null; 278 | return val; 279 | } 280 | } 281 | ``` 282 | 283 | 那么我们最后完成Vue,将上述方法挂载在Vue上。 284 | ```JavaScript 285 | class Vue { 286 | constructor(options = {}) { 287 | // 简化了$options的处理 288 | this.$options = options; 289 | // 简化了对data的处理 290 | let data = (this._data = this.$options.data); 291 | // 将所有data最外层属性代理到Vue实例上 292 | Object.keys(data).forEach(key => this._proxy(key)); 293 | // 监听数据 294 | observe(data); 295 | } 296 | // 对外暴露调用订阅者的接口,内部主要在指令中使用订阅者 297 | $watch(expOrFn, cb) { 298 | new Watcher(this, expOrFn, cb); 299 | } 300 | _proxy(key) { 301 | Object.defineProperty(this, key, { 302 | configurable: true, 303 | enumerable: true, 304 | get: () => this._data[key], 305 | set: val => { 306 | this._data[key] = val; 307 | }, 308 | }); 309 | } 310 | } 311 | ``` 312 | 313 | 看下效果: 314 | 315 | ![](https://user-gold-cdn.xitu.io/2018/5/1/1631c5aa9c52493e?w=231&h=93&f=gif&s=1025039) 316 | 317 |

在线示例 双向绑定实现---无漏洞版 by Iwobi (@xiaomuzhu) on CodePen.

318 | 319 | 320 | 至此,一个简单的双向绑定算是被我们实现了。 321 | 322 | #### 2.3 Object.defineProperty的缺陷 323 | 324 | 其实我们升级版的双向绑定依然存在漏洞,比如我们将属性值改为数组。 325 | 326 | ```JavaScript 327 | let demo = new Vue({ 328 | data: { 329 | list: [1], 330 | }, 331 | }); 332 | 333 | const list = document.getElementById('list'); 334 | const btn = document.getElementById('btn'); 335 | 336 | btn.addEventListener('click', function() { 337 | demo.list.push(1); 338 | }); 339 | 340 | 341 | const render = arr => { 342 | const fragment = document.createDocumentFragment(); 343 | for (let i = 0; i < arr.length; i++) { 344 | const li = document.createElement('li'); 345 | li.textContent = arr[i]; 346 | fragment.appendChild(li); 347 | } 348 | list.appendChild(fragment); 349 | }; 350 | 351 | // 监听数组,每次数组变化则触发渲染函数,然而...无法监听 352 | demo.$watch('list', list => render(list)); 353 | 354 | setTimeout( 355 | function() { 356 | alert(demo.list); 357 | }, 358 | 5000, 359 | ); 360 | ``` 361 | 362 |

在线示例 双向绑定-数组漏洞 by Iwobi (@xiaomuzhu) on CodePen.

363 | 364 | 365 | 是的,`Object.defineProperty`的第一个缺陷,无法监听数组变化。 366 | 然而[Vue的文档](https://cn.vuejs.org/v2/guide/list.html#%E6%95%B0%E7%BB%84%E6%9B%B4%E6%96%B0%E6%A3%80%E6%B5%8B)提到了Vue是可以检测到数组变化的,但是只有以下八种方法,`vm.items[indexOfItem] = newValue`这种是无法检测的。 367 | 368 | push() 369 | pop() 370 | shift() 371 | unshift() 372 | splice() 373 | sort() 374 | reverse() 375 | 376 | 其实作者在这里用了一些奇技淫巧,把无法监听数组的情况hack掉了,以下是方法示例。 377 | 378 | ```JavaScript 379 | const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; 380 | const arrayAugmentations = []; 381 | 382 | aryMethods.forEach((method)=> { 383 | 384 | // 这里是原生Array的原型方法 385 | let original = Array.prototype[method]; 386 | 387 | // 将push, pop等封装好的方法定义在对象arrayAugmentations的属性上 388 | // 注意:是属性而非原型属性 389 | arrayAugmentations[method] = function () { 390 | console.log('我被改变啦!'); 391 | 392 | // 调用对应的原生方法并返回结果 393 | return original.apply(this, arguments); 394 | }; 395 | 396 | }); 397 | 398 | let list = ['a', 'b', 'c']; 399 | // 将我们要监听的数组的原型指针指向上面定义的空数组对象 400 | // 别忘了这个空数组的属性上定义了我们封装好的push等方法 401 | list.__proto__ = arrayAugmentations; 402 | list.push('d'); // 我被改变啦! 4 403 | 404 | // 这里的list2没有被重新定义原型指针,所以就正常输出 405 | let list2 = ['a', 'b', 'c']; 406 | list2.push('d'); // 4 407 | ``` 408 | 409 | 由于只针对了八种方法进行了hack,所以其他数组的属性也是检测不到的,其中的坑很多,可以阅读上面提到的文档。 410 | 411 | 我们应该注意到在上文中的实现里,我们多次用遍历方法遍历对象的属性,这就引出了`Object.defineProperty`的第二个缺陷,只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择。 412 | 413 | ```JavaScript 414 | Object.keys(value).forEach(key => this.convert(key, value[key])); 415 | ``` 416 | 417 | ### 3.Proxy实现的双向绑定的特点 418 | 419 | Proxy在ES2015规范中被正式发布,它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写,我们可以这样认为,Proxy是`Object.defineProperty`的全方位加强版,具体的文档可以查看[此处](http://es6.ruanyifeng.com/#docs/proxy); 420 | 421 | #### 3.1 Proxy可以直接监听对象而非属性 422 | 423 | 我们还是以上文中用`Object.defineProperty`实现的极简版双向绑定为例,用Proxy进行改写。 424 | 425 | ```JavaScript 426 | const input = document.getElementById('input'); 427 | const p = document.getElementById('p'); 428 | const obj = {}; 429 | 430 | const newObj = new Proxy(obj, { 431 | get: function(target, key, receiver) { 432 | console.log(`getting ${key}!`); 433 | return Reflect.get(target, key, receiver); 434 | }, 435 | set: function(target, key, value, receiver) { 436 | console.log(target, key, value, receiver); 437 | if (key === 'text') { 438 | input.value = value; 439 | p.innerHTML = value; 440 | } 441 | return Reflect.set(target, key, value, receiver); 442 | }, 443 | }); 444 | 445 | input.addEventListener('keyup', function(e) { 446 | newObj.text = e.target.value; 447 | }); 448 | 449 | ``` 450 | 451 |

在线示例 Proxy版 by Iwobi (@xiaomuzhu) on CodePen.

452 | 453 | 454 | 我们可以看到,Proxy直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于`Object.defineProperty`。 455 | 456 | #### 3.2 Proxy可以直接监听数组的变化 457 | 458 | 当我们对数组进行操作(push、shift、splice等)时,会触发对应的方法名称和*length*的变化,我们可以借此进行操作,以上文中`Object.defineProperty`无法生效的列表渲染为例。 459 | 460 | ```js 461 | const list = document.getElementById('list'); 462 | const btn = document.getElementById('btn'); 463 | 464 | // 渲染列表 465 | const Render = { 466 | // 初始化 467 | init: function(arr) { 468 | const fragment = document.createDocumentFragment(); 469 | for (let i = 0; i < arr.length; i++) { 470 | const li = document.createElement('li'); 471 | li.textContent = arr[i]; 472 | fragment.appendChild(li); 473 | } 474 | list.appendChild(fragment); 475 | }, 476 | // 我们只考虑了增加的情况,仅作为示例 477 | change: function(val) { 478 | const li = document.createElement('li'); 479 | li.textContent = val; 480 | list.appendChild(li); 481 | }, 482 | }; 483 | 484 | // 初始数组 485 | const arr = [1, 2, 3, 4]; 486 | 487 | // 监听数组 488 | const newArr = new Proxy(arr, { 489 | get: function(target, key, receiver) { 490 | console.log(key); 491 | return Reflect.get(target, key, receiver); 492 | }, 493 | set: function(target, key, value, receiver) { 494 | console.log(target, key, value, receiver); 495 | if (key !== 'length') { 496 | Render.change(value); 497 | } 498 | return Reflect.set(target, key, value, receiver); 499 | }, 500 | }); 501 | 502 | // 初始化 503 | window.onload = function() { 504 | Render.init(arr); 505 | } 506 | 507 | // push数字 508 | btn.addEventListener('click', function() { 509 | newArr.push(6); 510 | }); 511 | ``` 512 | 513 |

在线示例 Proxy列表渲染 by Iwobi (@xiaomuzhu) on CodePen.

514 | 515 | 516 | 很显然,Proxy不需要那么多hack(即使hack也无法完美实现监听)就可以无压力监听数组的变化,我们都知道,标准永远优先于hack。 517 | 518 | #### 3.3 Proxy的其他优势 519 | 520 | Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是`Object.defineProperty`不具备的。 521 | 522 | Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而`Object.defineProperty`只能遍历对象属性直接修改。 523 | 524 | Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利。 525 | 526 | 当然,Proxy的劣势就是兼容性问题,而且无法用polyfill磨平,因此Vue的作者才声明需要等到下个大版本(3.0)才能用Proxy重写。 527 | --------------------------------------------------------------------------------