├── .github └── workflows │ └── main.yml ├── .gitignore ├── README.md ├── docs ├── GMTC │ └── readme.md ├── README.md ├── algorithm │ └── README.md ├── architecture │ └── README.md ├── css3 │ ├── BFC.md │ ├── CSS实现自适应九宫格布局.md │ ├── README.md │ └── 居中布局.md ├── data-structure │ ├── README.md │ ├── assets │ │ └── 数据结构.mindnode │ └── 初识数据结构.md ├── design-patterns │ ├── README.md │ └── 单例模式.md ├── fe-engineering │ └── code-quality.md ├── html5 │ └── README.md ├── hybrid │ ├── README.md │ └── hybrid架构设计.md ├── javascript │ ├── README.md │ ├── this关键字各种使用情况.md │ ├── 前端跨域.md │ ├── 浏览器环境的事件循环.md │ ├── 深度优先搜索以及广度优先搜索.md │ ├── 深拷贝与浅拷贝.md │ └── 防抖与节流.md ├── linux │ └── shell.md ├── nginx │ └── Nginx学习之路.md ├── node │ └── README.md ├── pwa │ └── README.md ├── security │ └── README.md ├── tcpIp │ ├── HTTP缓存.md │ ├── README.md │ ├── TCP三次握手.md │ ├── TCP四次挥手.md │ └── network │ │ ├── Part1: 浏览器生成消息.md │ │ └── 网络是如何链接的.md ├── vue │ ├── README.md │ ├── Vue最佳实践.md │ ├── Vue源码: Vue.use, vm.$delete内部原理.md │ ├── Vue源码: vm.$mount内部原理.md │ ├── Vue源码: vm.$set原理分析.md │ ├── Vue源码: 事件相关的实例方法.md │ ├── Vue源码: 关于vm.$watch内部原理.md │ ├── Vue源码: 关于对Array的数据侦听.md │ ├── Vue源码: 构造函数入口.md │ └── Vue源码: 架构和目录设计.md ├── webpack │ ├── README.md │ └── 一个合格的Webpack4配置工程师素养.md └── yarn │ └── 调试.md ├── images └── readme.md ├── pdf ├── 03-复杂度.pdf ├── 04-动态数组.pdf ├── 05-链表.pdf ├── 06-栈.pdf ├── 07-队列.pdf ├── 08-二叉树.pdf ├── 09-二叉搜索树.pdf ├── 10-平衡二叉搜索树.pdf ├── 11-AVL树.pdf ├── 12-B树.pdf ├── 13-红黑树.pdf ├── 14-集合.pdf ├── 15-映射.pdf ├── 16-哈希表.pdf ├── 17-二叉堆.pdf ├── 18-优先级队列.pdf ├── 19-哈夫曼树.pdf ├── 20-Trie.pdf ├── 21-总结.pdf ├── readme.md └── 趣学算法 01.pdf └── utils └── readme.md /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Run a one-line script 13 | run: echo Hello, world! 14 | - name: Run a multi-line script 15 | run: | 16 | echo Add other actions to build, 17 | echo test, and deploy your project. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blog 2 | 3 | 世界很大, 多踏出一步, 就进步一步。✊!!欢迎技术交流。 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/GMTC/readme.md: -------------------------------------------------------------------------------- 1 | # GMTC 2 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## 目录结构 2 | 3 | - algorithm: 算法 4 | - architecture: 架构 5 | - css3 6 | - data-structure: 数据结构 7 | - design-patterns: 设计模式 8 | - html5 9 | - hybrid: 混合开发 10 | - javascript 11 | - node 12 | - pwa 13 | - securoty: 安全 14 | - tcpip: TCP/IP 15 | - vue 16 | - webpack 17 | -------------------------------------------------------------------------------- /docs/algorithm/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/docs/algorithm/README.md -------------------------------------------------------------------------------- /docs/architecture/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/docs/architecture/README.md -------------------------------------------------------------------------------- /docs/css3/BFC.md: -------------------------------------------------------------------------------- 1 | ## 什么是BFC 2 | 3 | BFC(块格式化上下文): 是Web页面可视化渲染CSS的一部分, 是布局过程中生成块级盒子的区域。也是浮动元素与其他元素的交互限定区域。 4 | 5 | 简单理解就是具备BFC特性的元素, 就像被一个容器所包裹, 容器内的元素在布局上不会影响外面的元素。 6 | 7 | ## BFC常见应用 8 | 9 | ### 解决普通文档流块元素的外边距折叠问题 10 | 11 | ``` 12 | 30 |
31 |
32 |
33 |
34 | ``` 35 | 36 | ![块元素外边距折叠问题](https://user-gold-cdn.xitu.io/2019/2/21/1690bb2a83eb38c9?w=2504&h=1004&f=png&s=209518) 37 | 38 | 可见两个块元素外边距为20px。 39 | 40 | 我们可以使用BFC来解决这个问题,只需要把两个元素置于不同的BFC中进行隔离。 41 | 42 | ``` 43 | 64 | 65 |
66 |
67 |
68 |
69 |
70 |
71 | ``` 72 | 73 | ![BFC](https://user-gold-cdn.xitu.io/2019/2/21/1690bb63f3eebe99?w=2518&h=972&f=png&s=188623) 74 | 75 | 76 | ### BFC清除浮动 77 | 78 | demo演示: 79 | ``` 80 | 95 | 96 |
97 |

98 |
99 | ``` 100 | 可见容器元素内子元素浮动,脱离文档流,容器元素高度只有2px。 101 | 102 | ![BFC清除浮动](https://user-gold-cdn.xitu.io/2019/2/21/1690bb99839f6de1?w=2516&h=966&f=png&s=182465) 103 | 104 | 解决方法: 105 | 106 | ``` 107 | .demo { 108 | border: 1px solid pink; 109 | overflow: hidden; 110 | } 111 | ``` 112 | 113 | 114 | ![BFC清除浮动](https://user-gold-cdn.xitu.io/2019/2/21/1690bbb333737669?w=2514&h=1004&f=png&s=185229) 115 | 116 | 117 | ### 阻止普通文档流元素被浮动元素覆盖 118 | 119 | demo演示: 120 | ``` 121 | 138 | 139 |
140 |
我是一个左侧浮动元素
141 |
我是一个没有设置浮动, 也没有触发BFC的元素
142 |
143 | ``` 144 | 145 | demo2部分区域被浮动元素demo1覆盖, 但是文字没有覆盖, 即文字环绕效果。 146 | ![元素被浮动元素覆盖](https://user-gold-cdn.xitu.io/2019/2/21/1690bbfccb00357d?w=2522&h=980&f=png&s=240810) 147 | 148 | 解决办法就是触发demo2的BFC。 149 | 150 | ``` 151 | .demo2 { 152 | width: 200px; 153 | height: 200px; 154 | background: blue; 155 | overflow: hidden; 156 | } 157 | ``` 158 | 159 | 160 | ![BFC解决元素被浮动元素覆盖问题](https://user-gold-cdn.xitu.io/2019/2/21/1690bc19fc381ff8?w=2558&h=472&f=png&s=141117) 161 | 162 | ### 自适应两栏布局 163 | 164 | 165 | demo演示: 166 | 167 | ``` 168 | 189 | 190 |
191 |
192 | 浮动元素 193 |
194 |
195 | 自适应 196 |
197 |
198 | ``` 199 | 200 | 201 | ![BFC实现两栏布局](https://user-gold-cdn.xitu.io/2019/2/21/1690bcadaf036964?w=2238&h=532&f=png&s=104520) 202 | 203 | ## 如何触发BFC 204 | 205 | 206 | ``` 207 | 1. 根元素或包含根元素的元素 208 | 2. 浮动元素(元素的 float 不是 none) 209 | 3. 绝对定位元素(元素的 position 为 absolute 或 fixed) 210 | 4. 行内块元素(元素的 display 为 inline-block) 211 | 5. 表格单元格(元素的 display为 table-cell,HTML表格单元格默认为该值) 212 | 6. 表格标题(元素的 display 为 table-caption,HTML表格标题默认为该值) 213 | 7. 匿名表格单元格元素(元素的 display为 table、table-row、 table-row-group、table-header-group、table-footer-group(分别是HTML table、row、tbody、thead、tfoot的默认属性)或 inline-table) 214 | 8. overflow 值不为 visible 的块元素 215 | 9. display 值为 flow-root 的元素 216 | 10. contain 值为 layout、content或 strict 的元素 217 | 11. 弹性元素(display为 flex 或 inline-flex元素的直接子元素) 218 | 12. 网格元素(display为 grid 或 inline-grid 元素的直接子元素) 219 | 13. 多列容器(元素的 column-count 或 column-width 不为 auto,包括 column-count 为 1) 220 | 14. column-span 为 all 的元素始终会创建一个新的BFC,即使该元素没有包裹在一个多列容器中(标准变更,Chrome bug)。 221 | 222 | ``` 223 | -------------------------------------------------------------------------------- /docs/css3/CSS实现自适应九宫格布局.md: -------------------------------------------------------------------------------- 1 | # 九宫格布局 2 | 3 | 分析布局特点: 4 | 5 | ``` 6 | 1. 九宫格的容器是宽高相等的容器 7 | 2. 每个小格子也是宽高相等 8 | ``` 9 | 10 | 实现方式也很多, 比如flex, grid, float等, 这里只举一个例子 11 | 12 | ## 使用flex布局 13 | 14 | 15 | ``` 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | CSS实现自shiying九宫格(flex布局) 24 | 67 | 68 | 69 | 70 |
71 | 82 |
83 | 84 | 85 | 86 | 87 | ``` 88 | 89 | [效果图](https://codepen.io/nuohui/pen/rgzVar) 90 | -------------------------------------------------------------------------------- /docs/css3/README.md: -------------------------------------------------------------------------------- 1 | - [居中布局](https://github.com/NuoHui/fe-note/blob/master/docs/css3/%E5%B1%85%E4%B8%AD%E5%B8%83%E5%B1%80.md) 2 | - [BFC](https://github.com/NuoHui/fe-note/blob/master/docs/css3/BFC.md) 3 | - [CSS实现自适应九宫格布局](https://github.com/NuoHui/fe-note/blob/master/docs/css3/CSS%E5%AE%9E%E7%8E%B0%E8%87%AA%E9%80%82%E5%BA%94%E4%B9%9D%E5%AE%AB%E6%A0%BC%E5%B8%83%E5%B1%80.md) 4 | -------------------------------------------------------------------------------- /docs/css3/居中布局.md: -------------------------------------------------------------------------------- 1 | ## 水平居中 2 | 3 | ### 行内元素水平居中 4 | 5 | 方法: 给行内元素父元素使用text-align: center 6 | 7 | ``` 8 | 23 |
24 | 行内元素水平居中 25 |
26 | ``` 27 | 28 | ![行内元素水平居中](https://user-gold-cdn.xitu.io/2019/2/22/16915e0be2388514?w=2062&h=452&f=png&s=82414) 29 | 30 | ### 块级元素水平居中 31 | 32 | 方法: 块级元素使用margin: 0 auto。 33 | 34 | ``` 35 | 51 |
52 |

块级元素水平居中

53 |
54 | ``` 55 | 56 | ![块级元素水平居中](https://user-gold-cdn.xitu.io/2019/2/22/16915e2ef4d11f59?w=2056&h=440&f=png&s=82183) 57 | 58 | ### fit-content + margin: 0 auto 59 | 60 | 若子元素包含float,为了让子元素水平居中,可让父元素宽度设置为fit-content,并且配合margin。 61 | 62 | ``` 63 | 81 |
82 |

块级元素水平居中: 子元素浮动

83 |
84 | ``` 85 | 86 | ![fit-content + margin: 0 auto](https://user-gold-cdn.xitu.io/2019/2/22/16915ebb89f61669?w=2152&h=422&f=png&s=93090) 87 | 88 | 关于fit-content推荐阅读[理解CSS3 max/min-content及fit-content等width值](https://www.zhangxinxu.com/wordpress/2016/05/css3-width-max-contnet-min-content-fit-content/) 89 | 90 | ### flex + justify-content: center 91 | 92 | ``` 93 | 110 |
111 |

flex + justify-content: center

112 |
113 | ``` 114 | 115 | ![flex + justify-content: center](https://user-gold-cdn.xitu.io/2019/2/22/16915eeef8c795ce?w=2160&h=466&f=png&s=90450) 116 | 117 | ### absolute + transform 118 | 119 | ``` 120 | 137 |
138 |

宽度未知: absolute + transform

139 |
140 | ``` 141 | 142 | 143 | ![absolute + transform](https://user-gold-cdn.xitu.io/2019/2/23/16915f71f286116b?w=2214&h=458&f=png&s=91078) 144 | 145 | 关于transform使用推荐阅读[理解SVG transform坐标变换](https://www.zhangxinxu.com/wordpress/2015/10/understand-svg-transform/) 146 | 147 | 148 | ### 已知宽度: absolute + 负值的margin-left 149 | 150 | ``` 151 | 169 |
170 |

宽度已知: absolute + 负值的margin-left

171 |
172 | ``` 173 | 174 | ![已知宽度: absolute + 负值的margin-left](https://user-gold-cdn.xitu.io/2019/2/23/16915f9fe1358b11?w=2348&h=482&f=png&s=102193) 175 | 176 | 177 | ### 宽度已知: absolute + left:0;right:0;margin:0 auto 178 | 179 | ``` 180 | 199 |
200 |

宽度已知: absolute + left:0;right:0;margin:0 auto

201 |
202 | ``` 203 | 204 | ![宽度已知: absolute + left:0;right:0;margin:0 auto](https://user-gold-cdn.xitu.io/2019/2/23/16915fc22309250d?w=2478&h=484&f=png&s=106587) 205 | 206 | ## 垂直居中 207 | 208 | ### 已知父元素高度情况: line-height: height 209 | 210 | 若元素是单行文本, 则可设置 line-height 等于父元素高度。 211 | 212 | ``` 213 | 228 |
229 |

已知父元素高度情况: line-height: height

230 |
231 | ``` 232 | 233 | ![已知父元素高度情况: line-height: height](https://user-gold-cdn.xitu.io/2019/2/23/16915ff8533b51f0?w=2324&h=444&f=png&s=98081) 234 | 235 | 236 | ### 已经父元素高度: 若元素是行内块级元素如img, 可以使用display: inline-block, vertical-align: middle和一个伪元素。 237 | 238 | ``` 239 | 259 |
260 | 261 |
262 | ``` 263 | 264 | 265 | ![若元素是行内块级元素](https://user-gold-cdn.xitu.io/2019/2/23/169160678653841b?w=2002&h=456&f=png&s=151831) 266 | 267 | ### flex + align-items: center 268 | 269 | 270 | ![flex + align-items: center](https://user-gold-cdn.xitu.io/2019/2/23/1691a71b3c611f5b?w=2080&h=920&f=png&s=156952) 271 | 272 | ### absolute + transform 273 | 274 | ``` 275 | 293 |
294 |

absolute + transform

295 |
296 | ``` 297 | 298 | 299 | ![absolute + transform](https://user-gold-cdn.xitu.io/2019/2/23/1691a76dd8d99c60?w=2176&h=500&f=png&s=85653) 300 | 301 | 302 | ### display: table 303 | 304 | ``` 305 | 322 |
323 |

flex

324 |
325 | ``` 326 | 327 | 328 | ![table](https://user-gold-cdn.xitu.io/2019/2/23/1691a7de0e7badb1?w=1976&h=516&f=png&s=71301) 329 | 330 | ## 水平垂直居中 331 | 332 | ### absolute + transform 333 | 334 | ``` 335 | 354 |
355 |

absolute + transform

356 |
357 | ``` 358 | 359 | ![absolute + transform](https://user-gold-cdn.xitu.io/2019/2/23/1691a78d1f34dc04?w=2068&h=454&f=png&s=81334) 360 | 361 | ### flex 362 | 363 | 364 | ![flex](https://user-gold-cdn.xitu.io/2019/2/23/1691a7a1887c781f?w=1978&h=948&f=png&s=145042) 365 | 366 | ### table水平垂直居中 367 | 368 | 方法: inline-block + text-align + table-cell + vertical-align 369 | 370 | ``` 371 | 392 |
393 |

块级元素: table水平垂直居中

394 |
395 |
396 | 行内元素: table水平垂直居中 397 |
398 | ``` 399 | 400 | 401 | ![table水平垂直居中](https://user-gold-cdn.xitu.io/2019/2/23/1691a853e0e4f40c?w=2068&h=438&f=png&s=99208) 402 | 403 | 404 | ### 知道父元素高度,子元素高度 405 | 406 | 方法: 绝对定位方式+四个方向置0 + margin: auto 407 | 408 | ``` 409 | 431 |
432 |

绝对定位方式+四个方向置0

433 |
434 | ``` 435 | 436 | ![绝对定位方式+四个方向置0 + margin: auto](https://user-gold-cdn.xitu.io/2019/2/23/1691a8b05476a307?w=2192&h=664&f=png&s=124397) 437 | -------------------------------------------------------------------------------- /docs/data-structure/README.md: -------------------------------------------------------------------------------- 1 | - [初识数据结构](https://github.com/NuoHui/fe-note/blob/master/docs/data-structure/%E5%88%9D%E8%AF%86%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.md) 2 | -------------------------------------------------------------------------------- /docs/data-structure/assets/数据结构.mindnode: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/docs/data-structure/assets/数据结构.mindnode -------------------------------------------------------------------------------- /docs/data-structure/初识数据结构.md: -------------------------------------------------------------------------------- 1 | # 初识数据结构 2 | 3 | 屏幕快照 2019-05-12 下午11 29 56 4 | 5 | ## 图示常用的数据结构 6 | 7 | - 线性结构 8 | 9 | 屏幕快照 2019-05-12 下午11 12 16 1 10 | 11 | - 树 12 | 13 | 屏幕快照 2019-05-12 下午11 12 22 14 | 15 | - 图 16 | 17 | 屏幕快照 2019-05-12 下午11 12 28 18 | 19 | - 顺序存储 20 | 21 | 屏幕快照 2019-05-12 下午11 24 41 22 | 23 | - 链式存储 24 | 25 | 屏幕快照 2019-05-12 下午11 25 27 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/design-patterns/README.md: -------------------------------------------------------------------------------- 1 | - [单例模式](https://github.com/NuoHui/fe-note/blob/master/docs/design-patterns/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F.md) 2 | -------------------------------------------------------------------------------- /docs/design-patterns/单例模式.md: -------------------------------------------------------------------------------- 1 | # 单例模式 2 | 3 | 定义: 单例模式确保某个类永远只有一个实例, 并且提供一个全局访问的方法。 4 | 5 | ## UML类图 6 | 7 | 8 | ![面向对象](https://user-gold-cdn.xitu.io/2019/5/19/16acbaee7c3388da?w=1140&h=584&f=png&s=362637) 9 | 10 | 如果借助TypeScript来实现的话如下: 11 | 12 | ``` 13 | namespace SingletonPattern { 14 | export class Singleton { 15 | // A variable which stores the singleton object. Initially, 16 | // the variable acts like a placeholder 17 | private static singleton: Singleton; 18 | 19 | // private constructor so that no instance is created 20 | private constructor() { 21 | } 22 | 23 | // This is how we create a singleton object 24 | public static getInstance(): Singleton { 25 | // check if an instance of the class is already created 26 | if (!Singleton.singleton) { 27 | // If not created create an instance of the class 28 | // store the instance in the variable 29 | Singleton.singleton = new Singleton(); 30 | } 31 | // return the singleton object 32 | return Singleton.singleton; 33 | } 34 | } 35 | } 36 | 37 | ``` 38 | 39 | ## JS实现方式 40 | 41 | 当某些对象我们只需要创建一个的时候就可以考虑单例模式。 42 | 43 | ### 一个标准的单例模式 44 | 45 | ``` 46 | function Singleton (name) { 47 | this.instance = null 48 | this.name = name 49 | } 50 | 51 | Singleton.prototype.getName = function () { 52 | console.log(this.name) 53 | } 54 | 55 | Singleton.getInstance = function (name) { 56 | return this.instance || (this.instance = new Singleton(name)) 57 | } 58 | 59 | const s1 = Singleton.getInstance('kobe') 60 | const s2 = Singleton.getInstance('kobe2') 61 | 62 | console.log(s1 === s2) // true 63 | ``` 64 | 65 | 或者 66 | 67 | ``` 68 | function Singleton (name) { 69 | this.name = name 70 | } 71 | 72 | Singleton.prototype.getName = function () { 73 | console.log(this.name) 74 | } 75 | 76 | Singleton.getInstance = (function (name) { 77 | let instance = null 78 | return function (name) { 79 | return instance || (instance = new Singleton(name)) 80 | } 81 | })() 82 | 83 | const s1 = Singleton.getInstance('kobe') 84 | const s2 = Singleton.getInstance('kobe2') 85 | 86 | console.log(s1 === s2) // true 87 | ``` 88 | 89 | 上述的代码有个缺点就是不透明性, 即使用者需要知道通过Singleton.getInstance()方法去获取。这个与我们传统的new Constructor()创建实例方法不同。 90 | 91 | 92 | ### 惰性单例模式 93 | 94 | 我们都知道JS作为一门动态类型语言, 所以我们应该使用JS的方式来实现惰性单例, 充分利用JS的灵活性。 95 | 96 | 惰性单例: 即表示等需要创建实例时候才创建。 97 | 98 | #### 通用的核心代码 99 | 100 | ``` 101 | // fn: 具体执行的创建实例业务代码 102 | function getInstance(fn) { 103 | let instance 104 | return function (...args) { 105 | return instance || (instance = fn.apply(this, args)) 106 | } 107 | } 108 | ``` 109 | 110 | demo需求: 点击按钮永远只会导入一个iframe(指向百度的) 111 | 112 | ``` 113 | 114 | 115 | 116 | 117 | 118 | 119 | 单例模式 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | ``` 128 | 129 | ``` 130 | window.onload = function () { 131 | // fn: 具体执行的创建实例业务代码 132 | function getInstance(fn) { 133 | let instance 134 | return function (...args) { 135 | return instance || (instance = fn.apply(this, args)) 136 | } 137 | } 138 | const createEle = function (tagName) { 139 | const ele = document.createElement(tagName) 140 | ele.style.display = 'none' 141 | document.body.appendChild(ele) 142 | return ele 143 | } 144 | const createSingleIframe = getInstance(createEle) 145 | document.querySelector('#login').onclick = function () { 146 | const iframe = createSingleIframe('iframe') 147 | iframe.src = 'http://www.baidu.com' 148 | iframe.style.display = 'block' 149 | } 150 | } 151 | 152 | ``` 153 | -------------------------------------------------------------------------------- /docs/fe-engineering/code-quality.md: -------------------------------------------------------------------------------- 1 | ## What 2 | 3 | ### 什么是代码本身的质量? 4 | 5 | 代码本身的质量: 包括复杂度, 重复率, 代码风格等。 6 | 7 | - 复杂度: 项目代码量,模块大小,耦合度等 8 | - 重复率: 重复出现的代码区块占比,通常要求在5%以下(借助平台化工具如Sonar) 9 | - 代码风格: 代码风格是否统一(动态语言代码如JS, Python风格不受约束) 10 | 11 | 12 | ### 代码质量下降恶性循环 13 | 14 | 常见的代码质量下降的原因: 15 | 16 | - 破罐破摔: 在烂代码上迭代代码罪恶感比较小 17 | - 传染性: 不在意代码质量, 只关注业务的产出 18 | - 心有余而力不足 19 | 20 | 常见的导致恶性循环的场景: 21 | 22 | - 业务压力太大 23 | 24 | 烂代码产生的常见原因是业务压力大,导致没有时间或意愿讲究代码质量。因为向业务压力妥协而生产烂代码之后,开发效率会随之下降,进而导致业务压力更大,形成一种典型的恶性循环。 25 | 26 | 27 | ![](https://user-gold-cdn.xitu.io/2019/9/15/16d32c7f6d759601?w=1244&h=816&f=png&s=172704) 28 | 29 | - 通过增加人力解决业务压力 30 | 31 | 为了应对业务压力,常见的做法就是向项目中增加人力,但是单纯地增加人力的话,会因为风格不一致、沟通成本上升等原因导致烂代码更多。 32 | 33 | 34 | ![](https://user-gold-cdn.xitu.io/2019/9/15/16d32ce9b5c64da1?w=1326&h=510&f=png&s=140669) 35 | 36 | 那么我们应该如何解决呢? 37 | 38 | 39 | ![](https://user-gold-cdn.xitu.io/2019/9/15/16d32d39b796f928?w=1374&h=672&f=png&s=158241) 40 | 41 | 这是一个长期坚持的过程。 42 | 43 | ### 代码质量管控四个阶段 44 | 45 | - 规范化 46 | 47 | 建立代码规范与Code Review制度 48 | 49 | 1. [airbnb](https://github.com/airbnb/javascript) 50 | 2. [standard](https://github.com/standard/standard) 51 | 3. [node-style-guide](https://github.com/felixge/node-style-guide) 52 | 4. [google javascript style guide](https://google.github.io/styleguide/jsguide.html) 53 | 5. [google html/css style guide](https://google.github.io/styleguide/htmlcssguide.html) 54 | 6. [Vue风格指南](https://cn.vuejs.org/v2/style-guide/) 55 | 56 | 我觉得统一项目目录结构也是规范化的一种(比如我们用脚手架创建项目模板), 一个规范化的目录结构大大降低新人的上手成本。 57 | 58 | - 自动化 59 | 60 | 使用工具(linters)自动检查代码质量。 61 | 62 | 63 | ![](https://user-gold-cdn.xitu.io/2019/9/15/16d32d46d3e89065?w=1254&h=1122&f=png&s=224538) 64 | 65 | - 流程化 66 | 67 | 将代码质量检查与代码流动过程绑定。 68 | 69 | 70 | ![](https://user-gold-cdn.xitu.io/2019/9/15/16d32d4edac16516?w=1644&h=236&f=png&s=87006) 71 | 72 | 质量检查与代码流动绑定后的效果: 73 | 74 | 75 | ![](https://user-gold-cdn.xitu.io/2019/9/15/16d32d53cdbc31c9?w=1666&h=364&f=png&s=140003) 76 | 77 | 78 | ``` 79 | 备注: 80 | 81 | 1. 编辑时候: 通过编辑器插件, 实时查看质量检查 82 | 83 | 2. 可以利用CI(Jekins/Travis)把构建发布过程搬到线上, 先跑代码扫描, 测试代码等, 然后没有错误再进行build, build成功通过ssh推到服务器。 84 | ``` 85 | 86 | - 中心化 87 | 88 | 以团队整体为视角,集中管理代码规范,并实现质量状况透明化。 89 | 90 | 当团队规模越来越大,项目越来越多时,代码质量管控就会面临以下问题: 91 | 92 | 1. 不同项目使用的代码规范不一样 93 | 94 | 2. 部分项目由于放松要求,没有接入质量检查,或者存在大量未修复的缺陷 95 | 96 | 3. 无法从团队整体层面上体现各个项目的质量状况对比 97 | 98 | 为了应对以上问题,需要建设中心化的代码质量管控体系,要点包括: 99 | 100 | 代码规范统一管理。通过脚手架命令垂直管理代码扫描配置规则集, 自动安装,不在本地写规则。一个团队、一类项目、一套规则。 101 | 102 | * * * 103 | 104 | 105 | * [待定] 使用统一的持续集成服务(Jekins/Travis等)。质量检查不通过的项目不能上线。 106 | 107 | * [待定] 建立代码质量评分制度(借助Sonar)。让项目与项目之间能够横向对比,项目自身能够纵向对比,并且进行汇总反馈。 108 | 109 | ## Why 110 | 111 | > 代码质量是团队技术水平和管理水平的直接体现。 112 | 113 | > 看代码的时间远远多于写代码的时间 114 | 115 | ### 目前前端项目出现的问题 116 | 117 | - 书写风格不统一, 阅读体验差 118 | - 维护性差, 复用性差(Code Review互相进步) 119 | - 容易出现低质量代码, 代码返工率高 120 | - git commit不规范 121 | 122 | 123 | ## How 124 | 125 | 通过哪些手段来保证代码质量? 126 | 127 | ### EditorConfig 128 | 129 | [EditorConfig]( https://editorconfig.org/)在多人协作开发项目时候, 支持跨编辑器, IDE来支持维护一致的编码样式(文件格式)。 130 | 131 | VSCode插件EditorConfig for VS Code提供一键生成.editorconfig。 132 | 133 | 查看[实例](https://editorconfig.org/#example-file)。 134 | 135 | ### TypeScript 136 | 137 | - [官网介绍](https://www.typescriptlang.org/ 138 | )。 139 | - [中文awesome-typescript](https://github.com/semlinker/awesome-typescript) 140 | - [TypeScript体系调研报告](https://juejin.im/post/59c46bc86fb9a00a4636f939) 141 | - [2018年度JS趋势报告](https://2018.stateofjs.com/javascript-flavors/overview/) 142 | 143 | ### Git Hooks 144 | 145 | Git能在特定的重要动作发生时触发自定义脚本。 有两组这样的钩子:客户端的和服务器端的。 客户端钩子由诸如提交和合并这样的操作所调用,而服务器端钩子作用于诸如接收被推送的提交这样的联网操作, 我们目前使用的大多数是客户端钩子。 146 | 147 | 通过[husky](https://github.com/typicode/husky)集成[git hooks](https://git-scm.com/book/zh/v2/%E8%87%AA%E5%AE%9A%E4%B9%89-Git-Git-%E9%92%A9%E5%AD%90), 如果对git想有更全面的理解推荐阅读[GIt文档](https://git-scm.com/book/zh/v2)。 148 | 149 | husky会安装一系列的git hook到项目的.git/hook目录中。 150 | 151 | 下面两张图分别对比没有安装husky与安装了husky的git目录区别: 152 | 153 | 154 | ![](https://user-gold-cdn.xitu.io/2019/9/15/16d32d74c11d9f41?w=1314&h=688&f=png&s=332033) 155 | 156 | ![](https://user-gold-cdn.xitu.io/2019/9/15/16d32d7d8776a6d8?w=1286&h=1100&f=png&s=510934) 157 | 158 | 当你用 git init 初始化一个新版本库时,Git 默认会在这个目录中放置一些示例脚本(.sample结尾的文件)。 159 | 160 | #### pre-commit 161 | 162 | 163 | pre-commit 钩子在键入提交信息前运行。 它用于检查即将提交的快照,你可以利用该钩子,来检查代码风格是否一致(运行类似 lint 的程序。 164 | 165 | 166 | - [lint-staged](https://github.com/okonet/lint-staged): 可以获取所有被提交的文件并执行配置好的任务命令,各种lint校验工具可以配置好lint-staged任务中。 167 | - [prettier](https://prettier.io/): 可以配置到lint-staged中, 实现自动格式化编码风格。 168 | - [stylelint](https://github.com/stylelint/stylelint) 169 | - [eslint](https://cn.eslint.org/) 170 | - [tslint](https://github.com/palantir/tslint) 171 | - [eslint-plugin-vue](https://github.com/vuejs/eslint-plugin-vue): Vue.js官方推荐的lint工具 172 | 173 | 174 | 关于[为什么选择prettier, 以及eslint 与prettier区别?](https://zhuanlan.zhihu.com/p/62542268)。 175 | 176 | 关于[prettier配置](https://prettier.io/docs/en/configuration.html)。 177 | 关于[stylelint配置](https://stylelint.io/user-guide/configuration/)。 178 | 关于[eslint配置](https://cn.eslint.org/docs/user-guide/configuring)。 179 | 180 | #### commit-msg 181 | 182 | 183 | - [commitlint](https://github.com/conventional-changelog/commitlint)。 184 | 185 | 186 | commit-msg 可以用来在提交通过前验证项目状态或提交信息, 使用该钩子来核对提交信息是否遵循指定的模板。 187 | 188 | 关于git hooks在package.json配置: 189 | 190 | 191 | ![](https://user-gold-cdn.xitu.io/2019/9/15/16d32da5cdaf3df6?w=754&h=892&f=png&s=181244) 192 | 193 | ### 测试 194 | 195 | #### unittest 196 | 197 | - [Jest](https://jestjs.io/ 198 | ) 199 | - [Mocha](https://mochajs.org/ 200 | ) 201 | 202 | #### e2e 203 | 204 | - [Nightwatch](http://nightwatchjs.org/ 205 | ) 206 | - [Cypress](https://www.cypress.io/ 207 | ) 208 | 209 | 210 | ### CHANGELOG 211 | 212 | 更新日志, [standard-version](https://github.com/conventional-changelog/standard-version)。 213 | 214 | ### Code Review 215 | 216 | * [待定] Review制度,我们目前公司在代码merge时候多人审核才通过。 217 | 218 | 219 | ## 如何快速落地到当前业务 220 | 221 | 222 | ### 前端脚手架(xx-cli) 223 | 224 | 采用中心化集中管理代码扫描配置文件的思路, 把code lint配置文件做成一个npm包发到内网, 然后扩展脚手架命令一键执行下发远程配置文件到本地项目, 并且把新增的package.json依赖打进来, 大家后面再安装新的依赖即可。 225 | 226 | 所谓中心化管理: 所有项目代码配置文件以远程配置文件为准, 如果你本地有同名配置会被删除, 这样方便后续我们更新配置文件比如(增加vw/vh适配), 以及所有业务同步问题。 227 | 228 | ``` 229 | 目前只有基于vue.js项目的lint脚本命令, 后续有别的项目, 考虑通过 230 | 231 | dc-cli lint -- vue 232 | dc-cli lint -- node 233 | 扩展 234 | ``` 235 | 236 | 237 | ### demo演示 238 | 239 | demo演示如何在旧项目中植入代码质量检测? 240 | 由于这部分是在内网演示就不发不出来了。 241 | 242 | 至于脚手架可以参考我之前的demo[easy-cli](https://github.com/NuoHui/easy-cli)。这是比较全的demo。 243 | 244 | 245 | ## Future 246 | 247 | ### Jekins自动化 248 | 249 | 250 | ### [Sonar](https://www.sonarqube.org/) 251 | 252 | [Github:](https://github.com/SonarSource/sonarqube) 253 | 254 | SonarQube 是一款领先的持续代码质量监控平台,开源在github 上,可以轻松配置在内网服务器,实时监控代码,帮助了解提升提升团队项目代码质量。通过插件机制,SonarQube可以继承不同的测试工具,代码分析工具,以及持续集成工具。 255 | 256 | 与持续集成工具(例如 Hudson/Jenkins 等)不同,SonarQube 并不是简单地把不同的代码检查工具结果(例如 FindBugs,PMD 等)直接显示在 Web 页面上,而是通过不同的插件对这些结果进行再加工处理,通过量化的方式度量代码质量的变化,从而可以方便地对不同规模和种类的工程进行代码质量管理。 257 | 258 | 行业内提到"代码质量管理, 自动化质量管理", 一般指的都是通过Sonar来实现。 259 | 260 | 用Sonar能够实现什么? 261 | - 技术债务(sonar根据"规则"扫描出不符合规则的代码) 262 | - 覆盖率(单元测试覆盖率) 263 | - 重复(重复的代码, 有利于提醒封装) 264 | - 结构 265 | - … 266 | 267 | ### sonarjs 268 | 269 | sonar支持多种编程语言, 其中包括JavaScript. 如[sonarjs](https://www.sonarsource.com/products/codeanalyzers/sonarjs.html) 270 | 271 | 272 | ## 最后打个广告,欢迎关注我的公众号 全栈小窝。 273 | -------------------------------------------------------------------------------- /docs/html5/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/docs/html5/README.md -------------------------------------------------------------------------------- /docs/hybrid/README.md: -------------------------------------------------------------------------------- 1 | - [hybrid架构设计](https://github.com/NuoHui/fe-note/blob/master/docs/hybrid/hybrid%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.md) 2 | -------------------------------------------------------------------------------- /docs/hybrid/hybrid架构设计.md: -------------------------------------------------------------------------------- 1 | ## 什么是Hybrid 2 | 3 | Hybrid故名思义: 混合开发, 即App端+前端混合开发模式,Hybrid App 底层依赖 Native 端的 Web 容器(UIWebview 和 WKWebview),上层使用前端 Html5、CSS、Javascript 做业务开发。 4 | 5 | ## Hybrid带来哪些好处? 6 | 7 | 一套好的Hybrid App设计能够即拥有原生App流畅的体验, 又拥有前端H5快速灵活的开发模式, 如跨平台能力, 热更新机制。 8 | 9 | Hybrid解决的痛点: 我觉得最主要的就是快速版本迭代。 10 | 11 | ## Hybrid适合的场景 12 | 13 | - 需求频繁变更的 14 | - 对交互体验,性能要求不是特别高的 15 | 16 | 17 | ## Webview 18 | 19 | 我们都知道Webview是一个App端组件, 我们使用WebView 作为容器直接承载 Web页面。 20 | 21 | 屏幕快照 2019-05-03 上午9 34 11 22 | 23 | 24 | 这里推荐阅读 25 | - [如何设计一个优雅健壮的Android WebView?(上)](https://juejin.im/post/5a94f9d15188257a63113a74) 26 | - [如何设计一个优雅健壮的Android WebView?(下)](https://juejin.im/post/5a94fb046fb9a0635865a2d6) 27 | - [深入剖析 iOS 与 JS 交互](https://zhuanlan.zhihu.com/p/31368159) 28 | 29 | 30 | 31 | 32 | 33 | ## App接入H5的方式 34 | 35 | 把H5接入到App中有两种方式: 36 | 37 | ### 在线H5 38 | 39 | 这是一种非常常见的部署方式, 我们只需要把H5代码单独部署在服务器, 当App通过Webview渲染H5时候, 我们只需要提供对应H5 URL即可, Webview直接load这个URL。 40 | 41 | - 优点 42 | 43 | ``` 44 | 1. 具备独立部署/开发/调试/上线能力 45 | 2. 资源部署在远程服务器, 不会影响App包体积 46 | 3. 接入成本低 47 | ``` 48 | 49 | - 缺点 50 | 51 | ``` 52 | 1. 依赖于网络, 首屏渲染慢, 需要进一步优化页面性能 53 | 2. 不支持离线访问 54 | ``` 55 | - 适用业务场景 56 | 57 | 适用于一些功能性不强,不太需要复杂的功能协议,且不需要离线使用的业务。 58 | 59 | ### App内置包H5 60 | 61 | 这是一种本地化嵌入方式, 服务端会把我们打包后的前端代码资源下发到App端,然后App端解压到本地。 62 | 63 | ss 64 | 65 | 当然这里还需要一套版本管理机制, 只当App端本地资源不是最新的才去拉去服务器的资源。 66 | 67 | 屏幕快照 2019-05-03 下午3 04 17 68 | 69 | 70 | - 优点 71 | 72 | ``` 73 | 1. 首屏加载快, 用户体验好 74 | 2. 不依赖网络, 支持离线访问 75 | ``` 76 | 77 | - 缺点 78 | 79 | ``` 80 | 1. 开发流畅/更新机制复杂, 需要三方协作 81 | 2. 会增加App包的体积 82 | ``` 83 | 84 | ## Schema协议 85 | 86 | ### 什么是Schema协议 87 | 88 | scheme是一种页面内跳转协议,是一种非常好的实现机制,通过定义自己的scheme协议,可以非常方便跳转app中的各个页面。 89 | 90 | ### Scheme链接格式样式 91 | 92 | ``` 93 | [scheme]://[host]/[path]?[query] 94 | ``` 95 | 在前端使用Schema协议 96 | 97 | ``` 98 | 打开叮咚app 99 | ``` 100 | 101 | 102 | ### Schem协议使用场景 103 | 104 | - 通过小程序,利用Scheme协议打开原生app 105 | - H5页面点击锚点,根据锚点具体跳转路径APP端跳转具体的页面 106 | - APP端收到服务器端下发的PUSH通知栏消息,根据消息的点击跳转路径跳转相关页面 107 | - APP根据URL跳转到另外一个APP指定页面 108 | - 通过短信息中的url短链打开原生app 109 | 110 | ## JS Bridge设计 111 | 112 | App与前端的通信方式。 113 | 114 | 屏幕快照 2019-05-04 下午9 19 00 115 | 116 | 117 | ### JS Bridge源码 118 | 119 | 待补充 120 | -------------------------------------------------------------------------------- /docs/javascript/README.md: -------------------------------------------------------------------------------- 1 | - [前端跨域](https://github.com/NuoHui/fe-note/blob/master/docs/javascript/%E5%89%8D%E7%AB%AF%E8%B7%A8%E5%9F%9F.md) 2 | 3 | - [this关键字各种使用情况](https://github.com/NuoHui/fe-note/blob/master/docs/javascript/this%E5%85%B3%E9%94%AE%E5%AD%97%E5%90%84%E7%A7%8D%E4%BD%BF%E7%94%A8%E6%83%85%E5%86%B5.md) 4 | 5 | - [浏览器环境的事件循环](https://github.com/NuoHui/fe-note/blob/master/docs/javascript/%E6%B5%8F%E8%A7%88%E5%99%A8%E7%8E%AF%E5%A2%83%E7%9A%84%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF.md) 6 | 7 | - [节流与防抖](https://github.com/NuoHui/fe-note/blob/master/docs/javascript/%E9%98%B2%E6%8A%96%E4%B8%8E%E8%8A%82%E6%B5%81.md) 8 | 9 | - [浅拷贝与深拷贝](https://github.com/NuoHui/fe-note/blob/master/docs/javascript/%E6%B7%B1%E6%8B%B7%E8%B4%9D%E4%B8%8E%E6%B5%85%E6%8B%B7%E8%B4%9D.md) 10 | -------------------------------------------------------------------------------- /docs/javascript/this关键字各种使用情况.md: -------------------------------------------------------------------------------- 1 | this关键字是js中最复杂的机制之一, 因为它是自动被定义在函数作用域中。但是如果我们真正的搞懂了this的工作机制, 其实没有那么复杂。 2 | 3 | ## 为什么需要使用this? 4 | 5 | ``` 6 | function sayName () { 7 | console.log(this.name) 8 | } 9 | 10 | const xm = { 11 | name: 'xm' 12 | } 13 | 14 | const ml = { 15 | name: 'ml' 16 | } 17 | 18 | sayName.call(xm) // xm 19 | sayName.call(ml) // ml 20 | 21 | ``` 22 | 你会发现, 在不同的上下午对象中可以复用同一个函数, 如果不使用this, 我们只能显示指定对象参数。 23 | 24 | ``` 25 | function sayName (ctx) { 26 | console.log(ctx.name) 27 | } 28 | 29 | const xm = { 30 | name: 'xm' 31 | } 32 | 33 | const ml = { 34 | name: 'ml' 35 | } 36 | 37 | sayName(xm) // xm 38 | sayName(ml) // ml 39 | 40 | ``` 41 | 可见this会隐式传递一个对象引用, 这样可以使函数的设计可以更简洁优雅。 42 | 43 | ## 误解之一: this总是指向自身 44 | 45 | 46 | ``` 47 | function fo (i) { 48 | console.log('fo', i) 49 | console.log(this === window) // true 50 | this.count++ 51 | } 52 | 53 | fo.count = 0 54 | 55 | for (var i = 0; i < 5; i++) { 56 | fo(i) 57 | } 58 | console.log(fo.count, count) // 0, NaN 59 | 60 | ``` 61 | 针对上述问题我们怎么修改可以获得预期效果? 62 | ``` 63 | // 回避this的使用 64 | function fo (i) { 65 | console.log('fo', i) 66 | console.log(this === window) // true 67 | fo.count++ 68 | } 69 | 70 | fo.count = 0 71 | 72 | for (var i = 0; i < 5; i++) { 73 | fo(i) 74 | } 75 | console.log(fo.count) // 5 76 | ``` 77 | 78 | 简而言之, this是在运行时候绑定的, 不是由定义时候指定, this的上下文取决于由谁在哪里调用。 79 | 80 | 81 | ## this的绑定规则 82 | 83 | > 默认绑定 84 | 85 | 最常用的函数调用方式就是独立函数调用, 因此我们可以称为默认绑定。 86 | 默认绑定this指向window。 87 | 88 | ``` 89 | function a () { 90 | console.log(this.name) 91 | } 92 | var name = 'a' 93 | 94 | a() // 默认不带任何修饰的调用 == 普通调用 输出a 95 | a.call(null) // 输出a 96 | a.call(undefined) // 输出a 97 | 98 | ``` 99 | 但是需要注意函数体是严格模式下的默认绑定中this指向undefined。 100 | 101 | ``` 102 | function a () { 103 | "use strict" 104 | console.log(this === undefined, this === null) // true, false 105 | console.log(this.name) // Cannot read property 'name' of undefined 106 | } 107 | var name = 'a' 108 | 109 | a() 110 | a.call(null) 111 | a.call(undefined) 112 | 113 | ``` 114 | 115 | > 隐式绑定 116 | 117 | 规则: 有时候我们需要考虑调用位置是否有上下文对象,或者是否被某个对象所包含。 118 | 119 | ``` 120 | // 先定义再添加引用属性 121 | 122 | function test () { 123 | console.log(this.name) 124 | } 125 | 126 | var obj = { 127 | name: 'kobe', 128 | test: test 129 | } 130 | 131 | obj.test() // 'kobe' 132 | 133 | ``` 134 | 135 | ``` 136 | // 直接作为对象的方法 137 | 138 | var obj = { 139 | name: 'kobe', 140 | test: function () { 141 | console.log(this.name) 142 | } 143 | } 144 | 145 | obj.test() // 'kobe' 146 | 147 | ``` 148 | 149 | 箭头函数的this是指向它的父级执行环境 150 | ``` 151 | // 注意箭头函数的this指向 152 | var name = 'global' 153 | var obj = { 154 | name: 'kobe', 155 | test: () => { 156 | console.log(this === window) // true 157 | console.log(this.name) // global 158 | } 159 | } 160 | 161 | obj.test() 162 | 163 | ``` 164 | 165 | ``` 166 | // 对象属性引用链的调用(只有最顶层与最后一层才会影响调用位置) 167 | 168 | function foo () { 169 | console.log(this.name) 170 | } 171 | var c = { 172 | name: 'c', 173 | foo: foo 174 | } 175 | var b = { 176 | name: 'b', 177 | c: c 178 | } 179 | 180 | var a = { 181 | name: 'a', 182 | b: b 183 | } 184 | 185 | a.b.c.foo() // c 186 | 187 | ``` 188 | 189 | > 隐式丢失 190 | 191 | 一个常见的this绑定出现的问题就是: 被隐式绑定的函数容易丢失绑定对象, 最后应用默认绑定。 192 | 193 | ``` 194 | function a () { 195 | console.log(this.name) 196 | } 197 | 198 | var obj = { 199 | name: 'obj', 200 | a: a 201 | } 202 | 203 | var temp = obj.a 204 | var name = 'global' 205 | temp() // this绑定丢失, 应用默认绑定, 指向全局window => 'global' 206 | obj.a() // 'obj' 207 | 208 | ``` 209 | 210 | 另外需要注意参数传递也是一种隐式赋值。 211 | 212 | ``` 213 | function a () { 214 | console.log(this.name) 215 | } 216 | 217 | var obj = { 218 | name: 'obj', 219 | a: a 220 | } 221 | 222 | var name = 'global' 223 | 224 | function fx (cb) { 225 | cb() 226 | } 227 | 228 | fx(obj.a) // this绑定丢失, 应用默认绑定, 指向全局window => 'global' 229 | 230 | /** 231 | * fx(obj.a) === 等于 var fn = obj.a; fx(fn) 232 | */ 233 | 234 | ``` 235 | 如果你把函数传入语言内置的函数, 结果其实也是一样。 236 | 237 | ``` 238 | var obj = { 239 | name: 'obj', 240 | a: function () { 241 | console.log(this.name) 242 | } 243 | } 244 | var name = 'global' 245 | setTimeout(obj.a, 10) // 传参是隐式赋值 246 | 247 | ``` 248 | 249 | > 显示绑定 250 | 251 | 显示绑定主要通过call(), apply() 252 | 253 | call()与apply()方法第一个参数指的是绑定对象,即它们会把对象绑定到this, 然后调用函数时候指向这个this, 因此通过直接指定this的绑定对象方法叫做显示绑定。 254 | 255 | ``` 256 | function test () { 257 | console.log(this.name) 258 | } 259 | 260 | var o = { 261 | name: 'o' 262 | } 263 | test.call(o) // o 264 | 265 | ``` 266 | 267 | 如果你传入的第一个参数是原始值, 来作为this绑定对象, 那么该原始值会通过new String(), new Boolean(), new Number()转为对象形式, 这个操作又叫做装箱。 268 | 269 | 270 | > 硬绑定 271 | 272 | 显示绑定依然无法解决this绑定丢失问题, 但是我们可以通过一些方法来解决该问题。 273 | 274 | - 显示绑定变种实现 275 | 276 | ``` 277 | function test () { 278 | console.log(this.name) 279 | } 280 | 281 | var o = { 282 | name: 'o', 283 | test: test 284 | }; 285 | // 通过创建一个包裹函数 286 | function bindThis () { 287 | test.call(o) 288 | } 289 | var name = 'global' 290 | setTimeout(bindThis) // o 291 | 292 | ``` 293 | 294 | 通过bind()实现硬绑定, 来解决丢失this绑定问题。 295 | bind()会返回一个硬编码的新函数, 并且把参数设置为this的上下文。 296 | 297 | ``` 298 | function test () { 299 | console.log(this.name) 300 | } 301 | 302 | var o = { 303 | name: 'o', 304 | test: test 305 | }; 306 | 307 | var name = 'global' 308 | setTimeout(o.test.bind(o)) // o 309 | 310 | ``` 311 | 312 | 此外很多第三方库或者JS内置函数都允许传递一个可选参数作为上下文对象。 313 | 其作用就是类似bind, 确保你的回调函数中this指向的是该上下文对象。 314 | 315 | ``` 316 | function test (item) { 317 | console.log(item, this.id) 318 | } 319 | 320 | var obj = { 321 | id: 'id' 322 | }; 323 | 324 | [1,2,3,4].forEach(test, obj) 325 | 326 | ``` 327 | 328 | > new 绑定 329 | 330 | 我们一般创建一个对象实例通过new Constructor() 331 | 332 | new 一个构造函数其实会执行以下过程。 333 | ``` 334 | 1. 创建一个全新的对象 335 | 2. 该新对象执行原型链接, 把该对象绑定到this 336 | 3. 执行函数中代码 337 | 4. 如果没有其他返回, 默认返回该新对象 338 | ``` 339 | 340 | ``` 341 | function Person () { 342 | this.name = '2' 343 | } 344 | 345 | console.log(new Person().name) // 2 346 | 347 | ``` 348 | 349 | > 绑定规则优先级 350 | 351 | 352 | ``` 353 | 1. 默认绑定优先级最低 354 | ``` 355 | 356 | 比较显示绑定与隐式绑定: 显示绑定优先于隐式绑定 357 | 358 | ``` 359 | function test () { 360 | console.log(this.name) 361 | } 362 | 363 | var a = { 364 | name: 'a', 365 | test: test 366 | } 367 | 368 | var b = { 369 | name: 'b', 370 | test: test 371 | } 372 | 373 | // 隐式绑定 374 | a.test() // a 375 | b.test() // b 376 | 377 | // 显示绑定 378 | a.test.call(b) // b 379 | b.test.call(a) // a 380 | 381 | ``` 382 | 383 | 比较new绑定与隐式绑定: new绑定优先级高于隐式绑定 384 | 385 | ``` 386 | function test (thing) { 387 | this.thing = thing 388 | } 389 | 390 | var obj1 = { 391 | test: test 392 | } 393 | 394 | var obj2 = {} 395 | 396 | // 隐式绑定 397 | obj1.test(2) 398 | console.log(obj1.thing) // 2 399 | 400 | // 显示绑定 401 | obj1.test.call(obj2, 3) 402 | console.log(obj2.thing) // 3 403 | 404 | 405 | // 可见new绑定优先级高于隐式绑定, 如果不是的化 test.thing=2 406 | var test = new obj1.test(4) 407 | console.log(obj1.thing) // 2 408 | console.log(test.thing) // 4 409 | 410 | ``` 411 | 412 | 那么最后我们比较new绑定跟硬绑定: new绑定优先与硬绑定 413 | 注意: new绑定不能跟call, apply一起使用。 414 | 415 | 416 | ``` 417 | function test (thing) { 418 | this.thing = thing 419 | } 420 | 421 | var o = {} 422 | 423 | var binds = test.bind(o) 424 | binds(3) 425 | // 硬绑定 426 | console.log(o.thing) // 3 427 | 428 | var thing = new binds(4) 429 | console.log(thing.thing) // 4 430 | console.log(o.thing) // 3, 么有改变说明 new绑定优先级高 431 | 432 | ``` 433 | 434 | 总结: 435 | 436 | ``` 437 | 1. 如果函数是通过new调用(new绑定), 那么this指向新创建的对象 438 | 2. 函数是否通过call, apply, bind等显示绑定,或者硬绑定, 是的化this指向指定的对象 439 | 3. 函数是在某个上下文中调用(隐式绑定), 那么this绑定的是那个上下文对象 440 | 4. 如果以上都不是, 那么默认是默认绑定, this指向全局对象, 如果函数体是严格模式指向undefined 441 | 442 | ``` 443 | 444 | 445 | ## 特殊情况下的绑定 446 | 447 | - 被忽略的this 448 | 449 | 即就是bind, call, apply时候, 把undefined或者null作为绑定对象。 450 | 451 | ``` 452 | var a = 2 453 | function test () { 454 | console.log(this.a) 455 | } 456 | test.call(null) // 2 457 | test.call(undefined) // 2 458 | 459 | ``` 460 | 461 | ``` 462 | function test (a, b) { 463 | console.log(a, b) 464 | } 465 | 466 | test.apply(null, [3,4]) // 3,4 467 | var temp = test.bind(null, 2) 468 | temp(4) // 2, 4 469 | 470 | ``` 471 | - 更安全的this 472 | 473 | 474 | 但是很多时候我们不建议把null或者undefined作为空对象的占位符,因为它们默认会指向全局对象, 容易产生副作用, 最好使用Object.create(null)。 475 | 476 | ``` 477 | function test (a, b) { 478 | console.log(a, b) 479 | } 480 | 481 | test.apply(Object.create(null), [3,4]) // 3,4 482 | var temp = test.bind(Object.create(null), 2) 483 | temp(4) // 2, 4 484 | 485 | ``` 486 | - 间接引用 487 | 488 | 有时候我们可能会无意创建这个函数是的间接引用, 这种情况下, 调用这个函数默认会使用默认规则。 489 | 490 | ``` 491 | // 间接引用最容易在赋值时候发生 492 | 493 | var name = 'global' 494 | function test () { 495 | console.log(this.name) 496 | } 497 | 498 | var o = { 499 | name: 'o', 500 | test: test 501 | } 502 | var a = { 503 | name: 'a' 504 | }; 505 | o.test();// o 506 | (a.test = o.test)() // global 507 | 508 | // 如果是 509 | // a.test = o.test 510 | // a.test() // a 511 | ``` 512 | 513 | ## 面试题 514 | 515 | ``` 516 | var name = 'global' 517 | 518 | var obj = { 519 | name: 'obj', 520 | test: function () { 521 | (() => { 522 | var name = '2' 523 | console.log(this.name) 524 | })() 525 | } 526 | } 527 | 528 | obj.test() // obj 529 | 530 | ``` 531 | 532 | ``` 533 | function foo () { 534 | return () => { 535 | console.log(this.name) 536 | } 537 | } 538 | 539 | var obj1 = { 540 | name: 'obj1' 541 | } 542 | 543 | var obj2 = { 544 | name: 'obj2' 545 | } 546 | 547 | var temp = foo.call(obj1) 548 | temp.call(obj2) // obj1 549 | 550 | ``` 551 | -------------------------------------------------------------------------------- /docs/javascript/前端跨域.md: -------------------------------------------------------------------------------- 1 | ## 前端跨域 2 | 3 | > 什么是跨域? 4 | 5 | 跨域是指从一个域下的文档或者脚本去请求另外一个域下的资源,当然这里是广义上说的跨域。 6 | 7 | 广义上的跨域 8 | 9 | ``` 10 | 资源嵌入: 72 | 73 | 74 | 75 | ``` 76 | 服务端: 77 | 78 | ``` 79 | // app.js 80 | 81 | const http = require('http') 82 | const qs = require('querystring') 83 | 84 | const server = http.createServer() 85 | server.on('request', function (req, res) { 86 | const params = qs.parse(req.url.split('?')[1]) 87 | const fn = params.callback 88 | // jsonp返回设置 89 | res.writeHead(200, { 90 | 'Content-Type': 'text/javascript' 91 | }) 92 | console.log('返回客户端的数据', JSON.stringify(params), typeof(JSON.stringify(params))) 93 | res.write(`${fn}(${JSON.stringify(params)})`) 94 | res.end() 95 | }) 96 | 97 | server.listen('8080', function () { 98 | console.log('Server is running at port 8080...') 99 | }) 100 | 101 | ``` 102 | 结果: 103 | 屏幕快照 2019-05-04 下午11 29 24 104 | 屏幕快照 2019-05-04 下午11 31 03 105 | 106 | - 通过document.domain + iframe跨域 107 | 108 | 适用场景: 主域相同, 子域不同的跨域。如比如 a.test.com 和 b.test.com 适用于该方式。 只需要给页面添加 document.domain ='test.com'。 109 | 110 | 关于一级域名, 二级域名的区分: 111 | 112 | 屏幕快照 2019-05-04 下午11 52 07 113 | 114 | 115 | 116 | 原理: 两个页面都通过JS设置document.domain为相同的基础主域,从而实现跨域 117 | 118 | 119 | 父窗口: 120 | ``` 121 | 122 | 126 | ``` 127 | 128 | 子窗口: 129 | 130 | ``` 131 | 136 | ``` 137 | 138 | - CORS: "跨域资源共享"(Cross-origin resource sharing) 139 | 140 | 以下介绍来源于阮一峰大大的[跨域资源共享 CORS 详解](http://www.ruanyifeng.com/blog/2016/04/cors.html), 推荐阅读下。 141 | 142 | 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。 143 | CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。 144 | 145 | 整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息。 146 | 147 | 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。 148 | 149 | 使用场景: CORS与JSONP的使用目的相同,但是比JSONP更强大。JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。 150 | 151 | 浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。 152 | 153 | 下面模拟一个CORS的非简单请求, 非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。 154 | 155 | 非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。 156 | 157 | 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。 158 | 159 | 直接上代码: 160 | 161 | ``` 162 | // app.js下静态资源index.html 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | jsonp跨域 171 | 172 | 173 | 203 | 204 | 205 | 206 | ``` 207 | 208 | ``` 209 | // app.js 服务器1 210 | const express = require("express"); 211 | const app = express(); 212 | 213 | app.use(express.static(__dirname)); 214 | app.listen(3000); 215 | 216 | ``` 217 | 218 | ``` 219 | // app2.js 服务器2 220 | 221 | const express = require('express') 222 | const app = express() 223 | // 白名单 224 | let whitList = ['http://127.0.0.1:3000'] 225 | app.use(function(req, res, next) { 226 | // 获取客户端请求中的origin字段, CORS请求浏览器会在头部添加Origin字段 227 | // Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。 228 | let origin = req.headers.origin 229 | if (whitList.includes(origin)) { 230 | // 如果Origin指定的域名在许可范围内 231 | // 该字段是必须返回的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。 232 | res.setHeader('Access-Control-Allow-Origin', origin) 233 | // 该字段可选, 如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。 234 | // 它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。 235 | res.setHeader('Access-Control-Allow-Headers', 'name') 236 | // 该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。 237 | // 注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。 238 | res.setHeader('Access-Control-Allow-Methods', 'PUT') 239 | // 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。 240 | // 设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。 241 | res.setHeader('Access-Control-Allow-Credentials', true) 242 | // 该字段可选,用来指定本次预检请求的有效期,单位为秒 243 | // 在有效期间,不用发出另一条预检请求。 244 | res.setHeader('Access-Control-Max-Age', 6) 245 | // 该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段: 246 | // Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。 247 | // 如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。 248 | res.setHeader('Access-Control-Expose-Headers', 'name') 249 | if (req.method === 'OPTIONS') { 250 | res.end() // OPTIONS请求不做任何处理 251 | } 252 | } 253 | next() 254 | }) 255 | app.put('/getData', function(req, res) { 256 | console.log(req.headers, 'from put') 257 | res.header("Content-Type", "application/json; charset=utf-8") 258 | res.setHeader('name', 'jw') //返回一个响应头,后台需设置 259 | res.end('测试CORS跨域') 260 | }) 261 | app.get('/getData', function(req, res) { 262 | console.log(req.headers, 'from get') 263 | res.header("Content-Type", "application/json; charset=utf-8") 264 | res.end('测试CORS跨域') 265 | }) 266 | app.use(express.static(__dirname)) 267 | app.listen(4000) 268 | 269 | ``` 270 | 看结果: 271 | 272 | 11 273 | 333 274 | 屏幕快照 2019-05-05 上午12 58 37 275 | 276 | 277 | - postMessage 278 | 279 | postMessage是html5引入的API,postMessage()方法允许来自不同源的脚本采用异步方式进行有效的通信,可以实现跨文本文档,多窗口,跨域消息传递, 因此可以用来进行跨域通信。 280 | 281 | 更多使用见MDN[postMessage](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage)。 282 | 283 | 284 | > otherWindow.postMessage(message, targetOrigin, [transfer]) 285 | 286 | ``` 287 | - otherWindow: 窗口的一个引用, 比如iframe的contentWindow属性, 执行window.open返回的窗口对象,或者是命名过的或数值索引的window.frames。 288 | 289 | - message: 要发送到其他窗口的数据, 它将会被[结构化克隆算法](https://developer.mozilla.org/en-US/docs/DOM/The_structured_clone_algorithm)序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。 290 | 291 | - targetOrigin: 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。这个机制用来控制消息可以发送到哪些窗口;如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的targetOrigin,而不是*。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。 292 | 293 | - transfer: 是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。 294 | ``` 295 | 296 | 297 | > 接收数据: 监听message事件的发生 298 | 299 | ``` 300 | window.addEventListener("message", receiveMessage, false) 301 | function receiveMessage(event) { 302 | var origin= event.origin; 303 | console.log(event); 304 | } 305 | 306 | ``` 307 | 308 | 重点介绍event对象的四个属性 309 | 310 | ``` 311 | data: 指的是从其他窗口发送过来的消息对象; 312 | type: 指的是发送消息的类型; 313 | source: 指的是发送消息的窗口对象; 314 | origin: 指的是发送消息的窗口的源 315 | ``` 316 | 317 | 318 | > 源码例子分析 319 | 320 | demo: http://127.0.0.1:3000/server1.html向http://127.0.0.1:4000/server2.html发送数据, 后者监听到数据并返回数据进行回应。 321 | 322 | 简单目录结构如下: 323 | 屏幕快照 2019-05-05 下午11 56 40 324 | 325 | 目录test/server1/app1.js 326 | ``` 327 | const express = require("express"); 328 | const app = express(); 329 | 330 | app.use(express.static(__dirname)); 331 | app.listen(3000); 332 | 333 | ``` 334 | 目录test/server1/server1.html 335 | ``` 336 | 337 | 338 | 339 | 340 | 341 | 342 | postMessage跨域 343 | 344 | 345 | 351 | 367 | 368 | 369 | 370 | ``` 371 | 372 | 目录test/server2/app2.js 373 | ``` 374 | const express = require('express') 375 | const app = express() 376 | app.use(express.static(__dirname)) 377 | app.listen(4000) 378 | 379 | ``` 380 | 381 | 目录test/server2/server2.html 382 | ``` 383 | 384 | 385 | 386 | 387 | 388 | 389 | postMessage 390 | 391 | 392 | 401 | 402 | 403 | 404 | ``` 405 | 结果: 406 | 407 | 屏幕快照 2019-05-05 下午11 56 04 408 | 409 | - WebSocket通信 410 | 411 | Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。 412 | 413 | 我们下面模拟一下WebSocket通信: http://127.0.0.1:3000/server1.html向服务器发送数据, 服务器接受到并且返回数据以回应。 414 | 415 | ``` 416 | // app1.js 417 | 418 | const express = require("express"); 419 | const WebSocket = require('ws') 420 | const app = express(); 421 | const wss = new WebSocket.Server({ 422 | port: 3003 423 | }); 424 | wss.on('connection', function connection(ws) { 425 | ws.on('message', function incoming(message) { 426 | console.log('服务器接受到客户端数据: %s', message); 427 | }); 428 | ws.send('我是来自服务器返回的数据'); 429 | }); 430 | app.use(express.static(__dirname)); 431 | app.listen(3000); 432 | 433 | ``` 434 | 435 | ``` 436 | // server1.html 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | WebSocket跨域 445 | 446 | 447 | 459 | 460 | 461 | 462 | ``` 463 | 464 | 结果: 465 | 屏幕快照 2019-05-06 上午12 20 46 466 | 屏幕快照 2019-05-06 上午12 20 58 467 | 468 | 469 | - Node做代理层 470 | 通过两次跨域实现Node代理层。 471 | 472 | ``` 473 | 浏览器index.html => 访问Node代理服务器(server1) => 帮忙把请求转发到真正的服务器server2 474 | 475 | 真正的服务器server2生成响应 => 返回到代理服务器server1 => 代理服务器server1把响应转发给浏览器index.html 476 | ``` 477 | 见代码实现: 478 | 479 | index.html 480 | ``` 481 | 482 | 483 | 484 | 485 | 486 | 487 | Node代理服务器 488 | 489 | 490 | 491 | 507 | 508 | 509 | 510 | ``` 511 | 512 | app1.js 513 | 514 | ``` 515 | // 代理服务器 516 | 517 | const http = require('http') 518 | 519 | const server = http.createServer((request, response) => { 520 | // CORS跨域: 用来与浏览器通信 521 | response.writeHead(200, { 522 | 'Access-Control-Allow-Origin': '*', 523 | 'Access-Control-Allow-Methods': '*', 524 | 'Access-Control-Allow-Headers': 'Content-Type' 525 | }) 526 | // 转发请求给服务器 527 | const proxyReq = http.request({ 528 | host: '127.0.0.1', 529 | port: 4000, 530 | url: '/', 531 | method: request.method, 532 | headers: request.headers 533 | }, (res) => { 534 | let result = '' 535 | res.setEncoding('utf8') 536 | res.on('data', function (chunk) { 537 | result += chunk 538 | }) 539 | res.on('end', function () { 540 | console.log('响应已经没有数据') 541 | // 把响应转发给浏览器 542 | response.end(result) 543 | }) 544 | }) 545 | .end() 546 | }) 547 | 548 | server.listen(3000, () => { 549 | console.log('The proxyServer is running at http://127.0.0.1:3000') 550 | }) 551 | 552 | ``` 553 | 554 | app2.js 555 | 556 | ``` 557 | // 真正的服务器 558 | const http = require('http') 559 | const userInfo = { 560 | name: 'admin', 561 | info: { 562 | type: 'admin' 563 | } 564 | } 565 | const server = http.createServer((req, res) => { 566 | if (req.url === '/') { 567 | res.end(JSON.stringify(userInfo)) 568 | } 569 | }) 570 | 571 | server.listen(4000, () => { 572 | console.log('The server is running at http://127.0.0.1:4000') 573 | }) 574 | 575 | ``` 576 | 577 | 结果: 578 | 屏幕快照 2019-05-06 上午12 59 13 579 | 屏幕快照 2019-05-06 上午12 59 21 580 | 581 | - nginx反向代理 582 | 583 | 最简单以及普遍使用的跨域方式就是通过nginx配置反向代理。 584 | 585 | 只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。 586 | 587 | 588 | 实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。 589 | 590 | nginx.conf修改如下: 591 | ``` 592 | // proxy服务器 593 | server { 594 | listen 81; 595 | server_name www.domain1.com; 596 | location / { 597 | proxy_pass http://www.domain2.com:8080; #反向代理 598 | proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名 599 | index index.html index.htm; 600 | 601 | # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用 602 | add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为* 603 | add_header Access-Control-Allow-Credentials true; 604 | } 605 | } 606 | ``` 607 | -------------------------------------------------------------------------------- /docs/javascript/浏览器环境的事件循环.md: -------------------------------------------------------------------------------- 1 | ## Event Loop 2 | 3 | ![22](https://user-images.githubusercontent.com/42414989/57308541-23c8e400-7119-11e9-9d56-e1956e8584c0.jpg) 4 | 5 | 屏幕快照 2019-05-07 下午10 21 28 6 | 7 | 学习事件循环的工作机制: 8 | 9 | > Call Stack: 调用栈 10 | 11 | 它是一个用于记录函数调用的数据结构(后进先出)。 当我们调用一个函数时候, 就会将其推入到堆栈中, 当一个函数返回时候, 就会将其推出堆栈的顶部。 12 | 如果栈长时间被占用或者堵塞就会导致我们常说的blocking script。 13 | 看下面这段代码: 我们理解Call Stack的执行过程. 14 | 15 | ![1](https://user-images.githubusercontent.com/42414989/57309485-c6ce2d80-711a-11e9-93b4-b9545e74f5b1.gif) 16 | 17 | 以下图一个错误堆栈为例可能更明了: 18 | 19 | ![44](https://user-images.githubusercontent.com/42414989/57309616-f846f900-711a-11e9-9550-ad8e1b374d8e.jpg) 20 | 21 | 有时候我们把一个函数递归调用多次, 会进入一个死循环. 而对于Chrome浏览器来说, 栈的大小是有限制的(16000桢), 于是它会抛出一个Max Stack错误. 22 | 23 | ![66](https://user-images.githubusercontent.com/42414989/57309639-0563e800-711b-11e9-9bfb-ccb4adbf33df.jpg) 24 | 25 | Call Stack相当于是一个容器, 所有任务都是在这里执行, 我们都知道JS是单线程, 因此主线程就是在调用栈里面不断的执行任务。 26 | 27 | 28 | > Heap 29 | 30 | 栈内存中存放的只是该对象的访问地址, 对象是分配在堆里面。 31 | ![00](https://user-images.githubusercontent.com/42414989/57309769-37754a00-711b-11e9-8a0a-45dea021baff.jpg) 32 | 33 | 34 | > Task Queue 35 | 36 | 当主线程在调用栈里面遇到异步任务时候会先去进行事件注册, 然后区分是宏任务还是微任务,如果是宏任务就推入到Task Queue, 是微任务就推入微任务队列。 37 | 38 | 然后不等待其结果返回, 继续往下执行下面的同步代码。 39 | 40 | ``` 41 | // 常见的宏任务 42 | 43 | script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering 44 | ``` 45 | 46 | 47 | > 微任务micro-task 48 | 49 | ``` 50 | process.nextTick, Promise.then(), Object.observe, MutationObserver 51 | ``` 52 | 在微任务中 process.nextTick 优先级高于Promise。 53 | 54 | 屏幕快照 2019-05-07 下午11 10 29 55 | 56 | 现在总结下Event Loop的执行机制 57 | 58 | ``` 59 | 1. scripts整体是一个宏任务,代码依次进入栈中, 被主线程执行。 60 | 2. 同步任务按照顺序从上往下执行 61 | 3. 遇到异步任务, 进行区分, 如果是宏任务就注册事件, 把回调推入Task Queue(宏任务队列),如果是微任务就推入微任务队列。 62 | 4. 不等待异步任务结果返回, 继续往下执行同步代码, 直到同步代码执行完毕。 63 | 5. 当同步代码执行完毕后会先执行微任务队列里面的任务。 64 | 6. 等微任务队列里面都执行完毕后, 再执行宏任务队列中排在第一个的任务。 65 | 7. 在执行宏任务过程中, 如果有遇到微任务就继续依次添加到微任务队列。 66 | 8. 等改次栈为空时候, 又清空执行微任务队列. 再次执行宏任务队列的第一个任务。 67 | 9. 依次类推 68 | 69 | ``` 70 | 71 | ## 通过题目来验证(仅限浏览器环境) 72 | 73 | 74 | ``` 75 | console.log(1) 76 | 77 | setTimeout(() => { 78 | console.log(2) 79 | }, 0) 80 | 81 | Promise.resolve().then(() => { 82 | console.log(3) 83 | }).then(() => { 84 | console.log(4) 85 | }) 86 | 87 | console.log(5) 88 | 89 | // 1 5 3 4 2 90 | 91 | ``` 92 | 93 | 关于Promsie需要知道: 94 | 95 | ``` 96 | Promise的executor是一个同步函数, 立即执行, 因此它会与同步任务一起执行, 而Promise的链式调用then, 每次都会在内部生成一个新的Promise, 然后执行then, 在执行的过程不断向微任务队列添加任务, 因此会直到微任务被清空才会执行宏任务 97 | ``` 98 | 99 | 分析: 100 | 101 | 102 | ``` 103 | 1. 首先整体scripts是一个宏任务 104 | 2. 遇到console.log(1), 同步代码先执行, 输出1 105 | 3. 遇到setTimeout,异步任务, 还是宏任务推入到宏任务队列 macro-task [ console.log(2)] 106 | 4. 遇到promsie.then(), 异步任务, 推入微任务队列 micro-task[console.log(3), console.log(4)] 107 | 5. 遇到console.log(5), 同步任务直接执行, 输入5 108 | 6. 调用栈为空, 开始清空本轮微任务队列, 按照顺序执行, 输入3,4 109 | 7. 清空微任务后, 执行宏任务队列第一个, 输入2 110 | ``` 111 | 112 | 113 | ``` 114 | async function a () { 115 | console.log(5) 116 | await b() 117 | console.log(1) 118 | } 119 | 120 | async function b () { 121 | console.log('2') 122 | } 123 | 124 | a() 125 | 126 | setTimeout(() => { 127 | console.log(4) 128 | }) 129 | console.log(3) 130 | 131 | // 5 2 3 1 4 132 | ``` 133 | 134 | 这里关于async/await需要注意, async/await其实本质还是Promise的封装, 而Promise属性微任务的一种, 提供一种很好理解的记忆方法。在await之前的代码是同步执行, 类似new Promise()里面的代码, await之后的代码是异步代码(微队列)类似Promsie.then。 135 | 所以上面的就很好理解了。 136 | 137 | ``` 138 | console.log(1); 139 | 140 | setTimeout(() => { 141 | console.log(2); 142 | new Promise(resolve => { 143 | console.log(4); 144 | resolve(); 145 | }).then(() => { 146 | console.log(5); 147 | }); 148 | }); 149 | 150 | new Promise(resolve => { 151 | console.log(7); 152 | resolve(); 153 | }).then(() => { 154 | console.log(8); 155 | }); 156 | 157 | setTimeout(() => { 158 | console.log(9); 159 | new Promise(resolve => { 160 | console.log(11); 161 | resolve(); 162 | }).then(() => { 163 | console.log(12); 164 | }); 165 | }); 166 | 167 | // 1 7 168 | // 宏任务: [setTimeout, setTimeout] 169 | // 微任务: [console.log(8)] 170 | 171 | // 1 7 8 172 | // 宏任务: [setTimeout, setTimeout] 173 | // 微任务: [] 174 | 175 | // 1 7 8 2 4 176 | // 宏任务: [setTimeout] 177 | // 微任务: [console.log(5)] 178 | 179 | // 1 7 8 2 4 5 180 | // 宏任务: [setTimeout] 181 | // 微任务: [] 182 | 183 | 184 | // 1 7 8 2 4 5 9 11 185 | // 宏任务: [] 186 | // 微任务: [console.log(12)] 187 | 188 | // 1 7 8 2 4 5 9 11 12 189 | // 宏任务: [] 190 | // 微任务: [] 191 | 192 | ``` 193 | 194 | ``` 195 | function execOrder () { 196 | setTimeout(() => console.log('timeout'), 0) 197 | let promise = new Promise((resolve, reject) => { 198 | console.log('Promise') 199 | resolve() 200 | }) 201 | promise 202 | .then(() => { 203 | console.log('resolved') 204 | }) 205 | console.log('hi') 206 | } 207 | 208 | execOrder() 209 | 210 | // Promise, hi 211 | // 宏任务[console.log('timeout'), ] 212 | // 微任务[console.log('resolved')] 213 | 214 | 215 | // Promise, hi, resolved 216 | // 宏任务[console.log('timeout'), ] 217 | // 微任务[] 218 | 219 | // Promise, hi, resolved, timeout 220 | // 宏任务[] 221 | // 微任务[] 222 | 223 | ``` 224 | 225 | 226 | -------------------------------------------------------------------------------- /docs/javascript/深度优先搜索以及广度优先搜索.md: -------------------------------------------------------------------------------- 1 | ## 以DOM结点树为例 2 | 3 | ``` 4 | 5 | 6 | 7 | 8 | 9 | 10 | 深度优先搜索与广度优先搜索 11 | 17 | 18 | 19 |
20 |
21 |
22 |
a
23 |
24 |
25 |
b
26 |
27 |
c
28 |
29 |
30 |
d
31 |
e
32 |
33 |
34 |
f
35 |
36 |
37 | 38 | 39 | 40 | 41 | ``` 42 | 43 | ### 使用BFS实现目标结点查找, 以及返回结点路径 44 | 45 | 46 | 主要思路就是通过把未搜索的结点添加到一个队列。 47 | ``` 48 | function bfs(source, targetNode) { 49 | const nodeLists = [source]; 50 | for(let i = 0; i < nodeLists.length; i++) { 51 | let curNode = nodeLists[i]; 52 | if(targetNode.innerHTML === curNode.innerHTML) { 53 | const path = []; 54 | return(function getSearchPath(curNode){ 55 | path.unshift(curNode) 56 | if(curNode.parent) { 57 | return getSearchPath(curNode.parent) 58 | } 59 | return path; 60 | })(curNode) 61 | } 62 | if(curNode.children) { 63 | nodeLists.push(...Array.from(curNode.children).map(node => { 64 | node.parent = curNode; 65 | return node; 66 | })) 67 | } 68 | } 69 | return []; 70 | } 71 | 72 | const path = bfs(document.querySelector('.parent'), document.querySelector('.child-2-2')) 73 | console.log(path); 74 | 75 | 76 | ``` 77 | 78 | 79 | ### DFS 80 | 81 | 递归 82 | ``` 83 | function dfs(source, target) { 84 | const nodeLists = []; 85 | const path = []; 86 | let _find = false; 87 | // 等待优化, 不需要全部遍历完 88 | Array.from([source]).every(node => { 89 | const map = data => { 90 | nodeLists.push(data); 91 | !_find && path.push(data); 92 | if(data.innerHTML === target.innerHTML) { 93 | _find = true; 94 | } 95 | data.children && Array.from(data.children).forEach(child => map(child)); 96 | }; 97 | map(node); 98 | }) 99 | return path; 100 | } 101 | 102 | const path = dfs(document.querySelector('.parent'), document.querySelector('.child-2-2')) 103 | console.log(path); 104 | 105 | ``` 106 | -------------------------------------------------------------------------------- /docs/javascript/深拷贝与浅拷贝.md: -------------------------------------------------------------------------------- 1 | # 深拷贝与浅拷贝的区别 2 | 3 | 我们常说的浅拷贝与深拷贝主要是针对Object,Array这样的引用类型, 为什么? 因为引用类型在JS里面存储的内存地址。 4 | 5 | 浅拷贝: 只会复制一层对象的属性 6 | 深拷贝: 递归复制对象所有层级属性 7 | 8 | 因此就会存在一个问题, 如果遇到引用类型, 浅拷贝它复制下来的是对象的指针地址, 导致复制后的对象中的引用类型属性是共享的。 9 | 10 | 下面分别从Array/Object来看看它们有哪些原生的拷贝方法。 11 | 12 | 13 | ## Array的拷贝 14 | 15 | 原生数组支持的拷贝方法有: slice()/concat()/Array.from()/扩展运算符 16 | 17 | ``` 18 | // 只含基本数据类型的拷贝 19 | const a = [1,2,3] 20 | const b = a.slice() 21 | const c = [...a] 22 | const d = [].concat(a) 23 | const e = Array.from(a) 24 | 25 | a[0] = 0 26 | 27 | // [ 0, 2, 3 ] [ 1, 2, 3 ] [ 1, 2, 3 ] [ 1, 2, 3 ] [ 1, 2, 3 ] 28 | console.log(a, b, c, d, e) 29 | 30 | // 包含引用类型的拷贝 31 | const a1 = [1,2,3,{name: 'kobe'}] 32 | const b1 = a1.slice() 33 | const c1 = [...a1] 34 | const d1 = [].concat(a1) 35 | const e1 = Array.from(a1) 36 | 37 | a1[3].name = 'change' 38 | 39 | // [ 1, 2, 3, { name: 'change' } ] [ 1, 2, 3, { name: 'change' } ] [ 1, 2, 3, { name: 'change' } ] [ 1, 2, 3, { name: 'change' } ] [ 1, 2, 3, { name: 'change' } ] 40 | console.log(a1, b1, c1, d1, e1) 41 | 42 | ``` 43 | 44 | 可见这些方法都只是浅拷贝。 45 | 46 | 47 | ## Object的拷贝 48 | 49 | 原生的对象支持拷贝的方法有: Object.assign()/扩展运算符/JSON.parse(JSON.stringify(obj)) 50 | 51 | ``` 52 | // 基本数据类型 53 | 54 | const a = { 55 | name: 'kobe', 56 | age: 10 57 | } 58 | const b = {...a} 59 | const c = Object.assign({},a) 60 | const d = JSON.parse(JSON.stringify(a)) 61 | a.name = 'change' 62 | 63 | // { name: 'change', age: 10 } { name: 'kobe', age: 10 } { name: 'kobe', age: 10 } { name: 'kobe', age: 10 } 64 | console.log(a,b,c,d) 65 | 66 | 67 | 68 | // 引用数据类型 69 | 70 | const a1 = { 71 | info: { 72 | name: 'kobe', 73 | age: 10 74 | } 75 | } 76 | 77 | const b1 = {...a1} 78 | const c1 = Object.assign({}, a1) 79 | const d1 = JSON.parse(JSON.stringify(a1)) 80 | a1.info.name = 'change' 81 | // { info: { name: 'change', age: 10 } } { info: { name: 'change', age: 10 } } { info: { name: 'change', age: 10 } } { info: { name: 'kobe', age: 10 } } 82 | console.log(a1,b1,c1,d1) 83 | 84 | ``` 85 | 86 | 发现Object.assign()/扩展运算符只是浅拷贝。 87 | 88 | 至于JSON.parse(JSON.stringify(obj))好像可以, 但是如果你去查询[MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)文档会发现。 89 | 90 | 91 | ![](https://user-gold-cdn.xitu.io/2019/5/18/16aca5510482f06b?w=1372&h=968&f=png&s=296213) 92 | 93 | ``` 94 | const test = { 95 | a: function () { 96 | console.log('a') 97 | }, 98 | [Symbol('age')]: 10, 99 | name: undefined, 100 | address: null, 101 | time: new Date() 102 | } 103 | 104 | 105 | const clone = JSON.parse(JSON.stringify(test)) 106 | 107 | console.log(clone) // { address: null, time: '2019-05-18T14:24:49.050Z' } 108 | 109 | ``` 110 | 111 | 112 | ## 手写深拷贝 113 | 114 | 可见JS本身不能提供深拷贝机制, 需要自己实现一个。 115 | 116 | ``` 117 | const test = { 118 | info: { 119 | name: 'kobe', 120 | nums: [1,2,3] 121 | } 122 | } 123 | 124 | function isObject (obj) { 125 | return typeof(obj) === 'object' && obj !== null 126 | } 127 | 128 | function deepClone (source) { 129 | const obj = Array.isArray(source) ? [] : {} 130 | for (let key in source) { 131 | obj[key] = isObject(source[key]) ? deepClone(source[key]) : source[key] 132 | } 133 | return obj 134 | } 135 | 136 | const copy = deepClone(test) 137 | test.info.nums.push(4) 138 | 139 | // { info: { name: 'kobe', nums: [ 1, 2, 3, 4 ] } } 140 | // { info: { name: 'kobe', nums: [ 1, 2, 3 ] } } 141 | console.log(test, copy) 142 | 143 | ``` 144 | 145 | ### 环 146 | 147 | 环就是我们常说的循环引用拷。 148 | 149 | ``` 150 | const test = { 151 | info: { 152 | name: 'kobe', 153 | nums: [1,2,3] 154 | } 155 | } 156 | 157 | test.loop = test; 158 | 159 | function isObject (obj) { 160 | return typeof(obj) === 'object' && obj !== null 161 | } 162 | 163 | function deepClone (source) { 164 | const obj = Array.isArray(source) ? [] : {} 165 | for (let key in source) { 166 | obj[key] = isObject(source[key]) ? deepClone(source[key]) : source[key] 167 | } 168 | return obj 169 | } 170 | 171 | const copy = deepClone(test) 172 | test.info.nums.push(4) 173 | 174 | console.log(test, copy) // Maximum call stack size exceeded 175 | 176 | ``` 177 | 178 | 解决思路如下: 通过一个WeakMap来存储拷贝过的对象, 每一次拷贝之前先向WeakMap询问是否拷贝, 有直接返回没有就拷贝。 179 | 180 | 所以我们需要修改下代码: 181 | 182 | ``` 183 | const test = { 184 | info: { 185 | name: 'kobe', 186 | nums: [1,2,3] 187 | } 188 | } 189 | 190 | test.loop = test; 191 | 192 | function isObject (obj) { 193 | return typeof obj === 'object' && obj !== null 194 | } 195 | 196 | function deepClone (source, hash = new WeakMap()) { 197 | if (hash.has(source)) { 198 | return hash.get(source) 199 | } 200 | const obj = Array.isArray(source) ? [] : {} 201 | hash.set(source, obj) 202 | for (let key in source) { 203 | obj[key] = isObject(source[key]) ? deepClone(source[key], hash) : source[key] 204 | } 205 | 206 | return obj 207 | } 208 | 209 | const copy = deepClone(test) 210 | test.info.nums.push(4) 211 | 212 | console.log(test, copy) 213 | ``` 214 | 215 | 216 | ### 特殊对象的拷贝 217 | 218 | 由于JS的对象类型太多, 这里我们只考虑比如Date, 正则, 其他自己扩展 219 | 220 | ``` 221 | const test = { 222 | info: { 223 | name: 'kobe', 224 | nums: [1, 2, 3] 225 | }, 226 | reg: /[0-9]{4}/gi, 227 | Date: new Date() 228 | } 229 | 230 | test.loop = test; 231 | 232 | function isObject(obj) { 233 | return typeof obj === 'object' && obj !== null 234 | } 235 | 236 | function deepClone(source, hash = new WeakMap()) { 237 | let obj 238 | const Constructor = source.constructor 239 | switch (Constructor) { 240 | case Date: 241 | obj = new Constructor(source.getTime()) 242 | break 243 | case RegExp: 244 | obj = new Constructor(source) 245 | break 246 | default: 247 | if (hash.has(source)) { 248 | return hash.get(source) 249 | } 250 | obj = new Constructor() 251 | hash.set(source, obj) 252 | break 253 | } 254 | 255 | for (let key in source) { 256 | obj[key] = isObject(source[key]) ? deepClone(source[key], hash) : source[key] 257 | } 258 | 259 | return obj 260 | } 261 | 262 | const copy = deepClone(test) 263 | test.info.nums.push(4) 264 | 265 | console.log(test, copy) 266 | ``` 267 | -------------------------------------------------------------------------------- /docs/javascript/防抖与节流.md: -------------------------------------------------------------------------------- 1 | ## 防抖(debounce) 2 | 3 | 概念: 当一个事件持续触发时候, 我们不执行其回调, 如果在间隔一定时间内没有触发我们就执行一次, 如果在间隔时间内触发了, 那么重新计时。 4 | 5 | 原理: 维护一个定时器, 如果在delay时间之后触发, 执行定时器回调, 如果在delay时间内触发, 清除定时器, 重新计时。 6 | 7 | 8 | 代码实现: 9 | ``` 10 | 11 | 12 | 13 | 14 | 15 | 16 | Document 17 | 22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | ``` 33 | 34 | ``` 35 | window.onload = function() { 36 | function debounce(fn, delay) { 37 | let timer = null; 38 | return function() { 39 | // 清除之前定时器, 只维护最新的 40 | if (timer !== null) { 41 | clearTimeout(timer); 42 | } 43 | timer = setTimeout(() => { 44 | fn.call(this, arguments); 45 | }, delay); 46 | }; 47 | } 48 | function handleInput() { 49 | console.log("防抖"); 50 | } 51 | 52 | document 53 | .querySelector("#name") 54 | .addEventListener("input", debounce(handleInput, 1000), false); 55 | }; 56 | 57 | ``` 58 | 59 | ## 节流(throttle) 60 | 61 | 概念: 当持续触发某个事件时候, 保证在一定时间内触发一次。 62 | 实现节流有两种方式: 通过定时器和时间计算。 63 | 64 | 65 | 时间计算 66 | ``` 67 | window.onload = function () { 68 | function throttle (fn, delay) { 69 | let pre = Date.now() 70 | return function () { 71 | const ctx = this 72 | const arg = arguments 73 | const now = Date.now() 74 | if (now - pre >= delay) { 75 | fn.apply(ctx, arg) 76 | pre = Date.now() 77 | } 78 | } 79 | } 80 | function onScroll () { 81 | console.log('节流') 82 | } 83 | window.addEventListener('scroll', throttle(onScroll, 2000), false) 84 | } 85 | 86 | ``` 87 | 88 | 定时器 89 | ``` 90 | window.onload = function () { 91 | function throttle (fn, delay) { 92 | let timer = null 93 | return function () { 94 | const ctx = this 95 | const arg = arguments 96 | if (!timer) { 97 | timer = setTimeout(function () { 98 | fn.apply(ctx, arg) 99 | timer = null 100 | }, delay) 101 | } 102 | } 103 | } 104 | function onScroll () { 105 | console.log('节流') 106 | } 107 | window.addEventListener('scroll', throttle(onScroll, 2000), false) 108 | } 109 | 110 | ``` 111 | 112 | 定时器+时间: 实现第一次触发时间就执行一次, 然后每隔delay时间执行一次, 最后一次触发后再执行一次 113 | ``` 114 | window.onload = function () { 115 | function throttle(fn, delay) { 116 | let pre, timer 117 | return function () { 118 | const now = Date.now() 119 | const ctx = this 120 | const arg = arguments 121 | if (pre && now < pre + delay) { 122 | // 在delay期间再次触发 123 | clearTimeout(timer) 124 | timer = setTimeout(function () { 125 | pre = now 126 | fn.apply(ctx, arg) 127 | // console.log('最后一次执行') 128 | }, delay) 129 | } else { 130 | // 第一次会执行 131 | // 超过指定时间delay会执行 132 | pre = now 133 | fn.apply(ctx, arg) 134 | } 135 | } 136 | } 137 | function onScroll () { 138 | console.log('节流') 139 | } 140 | window.addEventListener('scroll', throttle(onScroll, 2000), false) 141 | } 142 | 143 | ``` 144 | ## 两者区别 145 | 146 | 防抖和节流是两个相似的技术,都是为了减少一个函数无用的触发次数,以便提高性能或者说避免资源浪费。 147 | 148 | 节流指的是不管事件触发的多频繁, 在规定事件内会至少执行一次, 使用场景如无限滚动。 149 | 防抖指的是在最后一次事件触发后才会执行一次, 如input框的搜索。 150 | -------------------------------------------------------------------------------- /docs/linux/shell.md: -------------------------------------------------------------------------------- 1 | ## 目录介绍 2 | 3 | ```shell 4 | 5 | ls / # 查看根目录 6 | ls /root # 用户的家目录 7 | ls /home/username # 普通用户的家目录 8 | ls /etc # 配置文件目录 9 | ls /bin # 命名目录 10 | ls /sbin # 管理命令目录 11 | ls /usr/bin /usr/sbin # 系统预装的其他命令 12 | ``` 13 | 14 | ## 帮助命令 15 | 16 | man命令是manual的缩写。 17 | 18 | ```shell 19 | 20 | man ls # 查看ls命令详情 21 | man 空格 man # 查看man命令的第1章节内容 22 | man 7 man # 查看man命名的第7章节内容 23 | ``` 24 | 25 | shell(命令解释器)自带的命名叫做内部命令,其他的是外部命令。 26 | 27 | ```shell 28 | 29 | help cd # 内部命令我们使用help帮助 30 | 31 | ls --help # 外部命令使用help帮助 32 | ``` 33 | 34 | 35 | 36 | 37 | 我们使用type可以区分命名是属于外部命名还是内部命令。 38 | 39 | ```shell 40 | 41 | type cd # cd is a shell builtin 42 | 43 | ``` 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/nginx/Nginx学习之路.md: -------------------------------------------------------------------------------- 1 | ## 配置文件 2 | 3 | 截屏2021-11-28 下午1.00.10 4 | 5 | ```nginx 6 | # 指定启动nginx worker的用户 7 | #user nobody; 8 | 9 | # worker进程数量、与cpu核数有关,建议Max-1 10 | worker_processes 1; 11 | 12 | # 错误日志 路径 级别 debug info notice warn error crit 13 | #error_log logs/error.log; 14 | #error_log logs/error.log notice; 15 | #error_log logs/error.log info; 16 | 17 | # 进程Id号 18 | #pid logs/nginx.pid; 19 | 20 | # 指令块 21 | events { 22 | # 默认使用epoll 23 | use epoll; 24 | # 每个worker进程允许连接的客户端最大连接数 25 | worker_connections 1024; 26 | } 27 | 28 | 29 | http { 30 | # 导入文件 31 | include mime.types; 32 | # 默认类型 33 | default_type application/octet-stream; 34 | 35 | 36 | # 自定义日志格式 37 | #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 38 | # '$status $body_bytes_sent "$http_referer" ' 39 | # '"$http_user_agent" "$http_x_forwarded_for"'; 40 | 41 | # 用户请求日志路径 格式,可以全局配置 42 | #access_log logs/access.log main; 43 | 44 | 45 | # 文件上传 46 | sendfile on; 47 | # TCP_NOPUSH 是 FreeBSD 的一个 socket 选项,对应 Linux 的 TCP_CORK,Nginx 里统一用 tcp_nopush 来控制它,并且只有在启用了 sendfile 之后才生效。启用它之后,数据包会累计到一定大小之后才会发送,减小了额外开销,提高网络效率。 48 | #tcp_nopush on; 49 | 50 | 51 | # keepalive_timeout timeout [header_timeout]; 52 | # 通过配置 keepalive_timeout 可以实现服务器与客户端之间的长连接,用户连接访问服务器可能不止 1 次请求,使用keepalive可以减少系统对TCP连接的建立和销毁的开销。 53 | # keepalive_timeout 65; 服务器端主动发起keepalive连接,并保持 65 秒,如果超过65秒没有发送任何请求,则服务器主动断开连接,TCP从ESTABLISHED 转为 TIME_WAIT 状态。 54 | # 第一个参数:设置keep-alive客户端连接在服务器端保持开启的超时值(默认65s);值为0会禁用keep-alive客户端连接; 55 | # 第二个参数:可选、在响应的header域中设置一个值“Keep-Alive: timeout=time”;通常可以不用设置; 56 | #keepalive_timeout 0; 57 | keepalive_timeout 65; 58 | 59 | 60 | # 开启gzip压缩 61 | #gzip on; 62 | 63 | server { 64 | # 监听端口 65 | listen 8080; 66 | # 域名 IP 67 | server_name localhost; 68 | 69 | #charset koi8-r; 70 | 71 | # 在server虚拟主机定义访问日志 72 | #access_log logs/host.access.log main; 73 | 74 | # 路由匹配 75 | location / { 76 | # nginx指定文件路径的方式有两种,root以及alias、root使用的是相对路径 77 | # 如果location是 ^~ /t/, root /www/root/html/; 一个请求的URI是/t/a.html时,web服务器将会返回服务器上的/www/root/html/t/a.html的文件。 78 | # 如果location是 ^~ /t/, alias /www/root/html/new_t/; 一个请求的URI是/t/a.html时,web服务器将会返回服务器上的/www/root/html/new_t/a.html的文件。注意这里是new_t,因为alias会把location后面配置的路径丢弃掉,把当前匹配到的目录指向到指定的目录。使用alias时,目录名后面一定要加"/"。 79 | root html; 80 | index index.html index.htm; 81 | } 82 | 83 | #error_page 404 /404.html; 84 | 85 | # redirect server error pages to the static page /50x.html 86 | # 87 | error_page 500 502 503 504 /50x.html; 88 | location = /50x.html { 89 | root html; 90 | } 91 | 92 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 93 | # 94 | #location ~ \.php$ { 95 | # proxy_pass http://127.0.0.1; 96 | #} 97 | 98 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 99 | # 100 | #location ~ \.php$ { 101 | # root html; 102 | # fastcgi_pass 127.0.0.1:9000; 103 | # fastcgi_index index.php; 104 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 105 | # include fastcgi_params; 106 | #} 107 | 108 | # deny access to .htaccess files, if Apache's document root 109 | # concurs with nginx's one 110 | # 111 | #location ~ /\.ht { 112 | # deny all; 113 | #} 114 | } 115 | 116 | 117 | # another virtual host using mix of IP-, name-, and port-based configuration 118 | # 119 | #server { 120 | # listen 8000; 121 | # listen somename:8080; 122 | # server_name somename alias another.alias; 123 | 124 | # location / { 125 | # root html; 126 | # index index.html index.htm; 127 | # } 128 | #} 129 | 130 | 131 | # HTTPS server 132 | # 133 | #server { 134 | # listen 443 ssl; 135 | # server_name localhost; 136 | 137 | # ssl_certificate cert.pem; 138 | # ssl_certificate_key cert.key; 139 | 140 | # ssl_session_cache shared:SSL:1m; 141 | # ssl_session_timeout 5m; 142 | 143 | # ssl_ciphers HIGH:!aNULL:!MD5; 144 | # ssl_prefer_server_ciphers on; 145 | 146 | # location / { 147 | # root html; 148 | # index index.html index.htm; 149 | # } 150 | #} 151 | include servers/*; 152 | } 153 | ``` 154 | 155 | 156 | 157 | ## 压缩 158 | 159 | ```nginx 160 | # 开启压缩功能:提高传输效率、节约带宽 161 | gzip on; 162 | # 限制最小压缩:小于1字节文件不会压缩 163 | gzip_min_length: 1; 164 | # 定义压缩的级别、压缩比。文件越大压缩越多,相对cpu使用越多 165 | gzip_comp_level: 3; 166 | # 定义文件压缩类型 167 | gzip_types: text/plain; 168 | ``` 169 | 170 | 查看`nginx`进程信息。 171 | 172 | ```shell 173 | › ps -ef|grep nginx 174 | 501 16988 1 0 1:13PM ?? 0:00.01 nginx: master process /usr/local/opt/nginx/bin/nginx -g daemon off; 175 | 501 17186 16988 0 1:13PM ?? 0:00.00 nginx: worker process 176 | 501 17214 16557 0 1:14PM ttys004 0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox nginx 177 | ``` 178 | 179 | 180 | 181 | ## location匹配规则 182 | 183 | ```nginx 184 | # = 表示精确匹配,只有完全匹配才生效 185 | # location = /uri 186 | 187 | # http://website.com/abcd匹配 188 | # http://website.com/ABCD可能会匹配 ,也可以不匹配,取决于操作系统的文件系统是否大小写敏感(case-sensitive) 189 | # http://website.com/abcd?param1¶m2匹配,忽略 querystring 190 | # http://website.com/abcd/不匹配,带有结尾的/ 191 | # http://website.com/abcde不匹配 192 | server { 193 | server_name website.com; 194 | location = /abcd { 195 | […] 196 | } 197 | } 198 | 199 | 200 | # ~表示区分大小写的正则匹配 201 | # location ~ pattern 202 | # ^/abcd$这个正则表达式表示字符串必须以/开始,以$结束,中间必须是abcd 203 | # http://website.com/abcd匹配(完全匹配) 204 | # http://website.com/ABCD不匹配,大小写敏感 205 | # http://website.com/abcd?param1¶m2匹配 206 | # http://website.com/abcd/不匹配,不能匹配正则表达式 207 | # http://website.com/abcde不匹配,不能匹配正则表达式 208 | server { 209 | server_name website.com; 210 | location ~ ^/abcd$ { 211 | […] 212 | } 213 | } 214 | 215 | # ~* 表示不区分大小写的正则匹配 216 | # http://website.com/ABCD匹配 (大小写不敏感) 217 | location ~* pattern 218 | server { 219 | server_name website.com; 220 | location ~* ^/abcd$ { 221 | […] 222 | } 223 | } 224 | 225 | # 不带任何修饰符 也表示前缀匹配 但是在正则匹配之后 226 | location /uri 227 | 228 | # 通用匹配 任何未匹配到其他的Location的请求都会匹配到 229 | location / 230 | ``` 231 | 232 | 匹配顺序: 233 | 234 | - 首先精确匹配 `=` 235 | - 其次前缀匹配 `^~` 236 | - 其次是按文件中顺序的正则匹配 237 | - 然后匹配不带任何修饰的前缀匹配。 238 | - 最后是交给 `/` 通用匹配 239 | - 当有匹配成功时候,停止匹配,按当前匹配规则处理请求 240 | 241 | ## nginx跨域 242 | 243 | ```nginx 244 | server { 245 | listen 90; 246 | server_name: localhost; 247 | # 允许跨域请求的域 248 | add_header 'Access-Control-Allow-Origin' '*'; 249 | # 允许请求带上cookie 250 | add_header 'Access-Control-Allow-Credentials' 'true'; 251 | # 允许请求的方法 比如GET、POST、PUT、DELETE 252 | add_header 'Access-Control-Allow-Methods' '*'; 253 | # 允许请求的header 254 | add_header 'Access-Control-Allow-Headers' '*'; 255 | location / { 256 | root /home; 257 | index index.html; 258 | } 259 | } 260 | ``` 261 | 262 | 263 | 264 | ## 静态资源防盗链 265 | 266 | ```nginx 267 | location ~* .*\.(gif|jpg|ico|png|css|svg|js)$ { 268 | root /usr/local/nginx/static; 269 | valid_referers none blocked *.gupao.com ; 270 | if ($invalid_referer) { 271 | #rewrite ^/ http://www.youdomain.com/404.jpg; 272 | return 403; 273 | break; 274 | } 275 | access_log off; 276 | } 277 | ``` 278 | 279 | 280 | 281 | ## 日志切割 282 | 283 | 现有日志一般存在`access.log`文件中,但是对于大型项目随着日志内容越来越多,不方便查看,一般我们会对日志进行切割,比如按照天、或者小时为单位。 284 | 285 | 下面为示例脚本:`cut_log.sh` 286 | 287 | ```shell 288 | #!/bin/bash 289 | LOG_PATH="/var/log/nginx/" 290 | RECORD_TIME=$(date -d "yesterday" + %Y-%m-%d+%H:%M) 291 | PID=/var/run/nginx/nginx.pid 292 | mv ${LOG_PATH}/access.log ${LOG_PATH}/access.${RECORD_TIME}.log 293 | mv ${LOG_PATH}/error.log ${LOG_PATH}/error.${RECORD_TIME}.log 294 | # 向Nginx主进程发送信号,用于重新打开日志文件 295 | kill -USR1 `cat $PID` 296 | ``` 297 | 298 | 为`cut_log.sh`添加可执行的权限: 299 | 300 | ```bash 301 | chmod + x cut_log.sh 302 | ``` 303 | 304 | 改为定制切割日志 305 | 306 | - install 定时任务 307 | 308 | ```bash 309 | yum install crontabs 310 | ``` 311 | 312 | - `crontab -e` 编辑添加新任务 313 | 314 | ```bash 315 | */1 **** /usr/local/nginx/sbin/cut_log.sh 316 | ``` 317 | 318 | - 重启定时任务 319 | 320 | ``` 321 | service crond restart # 重启 322 | service crond start # 启动 323 | service crond stop # 关闭 324 | service crond reload # 重载 325 | crontab -e # 编辑任务 326 | crontab -l # 查看任务列表 327 | ``` 328 | 329 | 330 | 331 | ## 常用命令 332 | 333 | ```shell 334 | ./nginx -s stop # 强制关闭Nginx 335 | ./nginx -s quit # 优雅退出 336 | ./nginx -t # 校验Nginx配置文件语法 337 | ./nginx -v 338 | ./nginx -V 339 | ./nginx -h # help 340 | ./nginx -c # 指定配置文件 341 | ./nginx -s reload # 热重载 342 | ``` 343 | 344 | -------------------------------------------------------------------------------- /docs/node/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/docs/node/README.md -------------------------------------------------------------------------------- /docs/pwa/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/docs/pwa/README.md -------------------------------------------------------------------------------- /docs/security/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/docs/security/README.md -------------------------------------------------------------------------------- /docs/tcpIp/HTTP缓存.md: -------------------------------------------------------------------------------- 1 | ## HTTP缓存 2 | 3 | Web缓存我们大致可以分为: 数据库缓存, Redis缓存, 代理服务器缓存, CDN缓存, 浏览器缓存。 4 | 那么距离前端最近的就是浏览器缓存。 5 | 6 | 浏览器缓存又包括: HTTP缓存, Cookie, Web Storage, indexDB等, 我们今天主要学习HTTP缓存。 7 | 8 | ### HTTP缓存术语 9 | 10 | ``` 11 | 1. 缓存命中率: 从缓存中获取数据的请求数 / 总HTTP请求数, 缓存命中率越高, 显然效率越高 12 | 2. 过期: 超过了缓存有效时间, 通常过期内容我们需要再次向服务器发送请求, 进行缓存验证 13 | 3. 验证: 向服务器验证缓存中过期内容是否依然有效, 验证通过, 服务器重新刷新资源的有效时间 14 | 4. 失效: 如果服务器资源更新, 那么缓存中内容即为失效, 需要从缓存中移除 15 | ``` 16 | 17 | 虽然HTML的meta标签可以控制缓存 18 | 19 | ``` 20 | 21 | ``` 22 | 但是代理服务器不解析HTML, 因此我们这里谈的主要指的是HTTP头首部控制缓存 23 | 24 | 25 | ### 与HTTP缓存相关的头部字段 26 | 27 | - 通用首部字段 28 | 29 | 屏幕快照 2019-05-10 上午12 46 02 30 | 31 | > Pragma 32 | 33 | 当Pragma值为no-cache时候, 表示客户端不要对该资源进行缓存, 每一次都要向服务器发送请求, 此外**Pragma的优先级是高于Cache-Control的**。 34 | 35 | > Cache-Control 36 | 37 | 由于Expires时间是相对服务器而言,无法保证和客户端时间统一”的问题。http1.1新增了 Cache-Control 来定义缓存过期时间。注意:若报文中同时出现了 Expires 和 Cache-Control,则以 Cache-Control 为准。也就是说优先级从高到低分别是 Pragma -> Cache-Control -> Expires。 38 | Cache-Control也是一个通用首部字段,这意味着它能分别在请求报文和响应报文中使用。 39 | 40 | ``` 41 | "Cache-Control" ":" cache-directive 42 | ``` 43 | 作为请求首部时,cache-directive 的可选值有: 44 | 45 | 屏幕快照 2019-05-10 上午12 58 07 46 | 47 | 作为响应首部时,cache-directive 的可选值有: 48 | 49 | 屏幕快照 2019-05-10 上午12 58 34 50 | 51 | Cache-Control 允许自由组合可选值,例如: 52 | ``` 53 | Cache-Control: max-age=3600, must-revalidate 54 | ``` 55 | 它意味着该资源是从原服务器上取得的,且其缓存(新鲜度)的有效时间为一小时,在后续一小时内,用户重新访问该资源则无须发送请求。 当然这种组合的方式也会有些限制,比如 no-cache 就不能和 max-age、min-fresh、max-stale 一起搭配使用。 56 | 选择 Cache-Control 的策略(摘自 Google Developers) 57 | 58 | ![33](https://user-images.githubusercontent.com/42414989/57472810-f454db80-72c0-11e9-8942-7e5e833c899a.jpg) 59 | 60 | 61 | - 请求首部字段 62 | 63 | 屏幕快照 2019-05-10 上午12 46 59 64 | 65 | > If-Match: ETag-value 66 | 67 | 告诉服务器如果没有匹配到ETag,或者收到了“*”值而当前并没有该资源实体,则应当返回412(Precondition Failed) 状态码给客户端。否则服务器直接忽略该字段。 68 | 需要注意的是,如果资源是走分布式服务器(比如CDN)存储的情况,需要这些服务器上计算ETag唯一值的算法保持一致,才不会导致明明同一个文件,在服务器A和服务器B上生成的ETag却不一样。 69 | 70 | > If-None-Match: ETag-value 71 | 72 | 示例为 If-None-Match: "5d8c72a5edda8d6a:3239" 告诉服务端如果 ETag 没匹配上需要重发资源数据,否则直接回送304 和响应报头即可。 当前各浏览器均是使用的该请求首部来向服务器传递保存的 ETag 值。 73 | 74 | > If-Modified-Since: Last-Modified-value 75 | 76 | 示例为 If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT 77 | 78 | 该请求首部告诉服务器如果客户端传来的最后修改时间与服务器上的一致,则直接回送304 和响应报头即可。 79 | 当前各浏览器均是使用的该请求首部来向服务器传递保存的 Last-Modified 值。 80 | 81 | > If-Unmodified-Since: Last-Modified-value 82 | 83 | 该值告诉服务器,若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了),则应当返回412(Precondition Failed) 状态码给客户端。 Last-Modified 存在一定问题,如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。 84 | 85 | - 响应首部字段 86 | 87 | 屏幕快照 2019-05-10 上午12 47 43 88 | 89 | > ETag 90 | 91 | 为了解决上述Last-Modified可能存在的不准确的问题,Http1.1还推出了 ETag 实体首部字段。 服务器会通过某种算法,给资源计算得出一个唯一标志符(比如md5标志),在把资源响应给客户端的时候,会在实体首部加上“ETag: 唯一标识符”一起返回给客户端。例如: 92 | ``` 93 | Etag: "5d8c72a5edda8d6a:3239" 94 | ``` 95 | 客户端会保留该 ETag 字段,并在下一次请求时将其一并带过去给服务器。服务器只需要比较客户端传来的ETag跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。 96 | 如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。 97 | 那么客户端是如何把标记在资源上的 ETag 传回给服务器的呢?请求报文中有两个首部字段可以带上 ETag 值: 98 | 99 | - 实体首部字段 100 | 101 | 屏幕快照 2019-05-10 上午12 48 26 102 | 103 | > Expires 104 | 105 | 106 | 有了Pragma来禁用缓存,自然也需要有个东西来启用缓存和定义缓存时间,对http1.0而言,Expires就是做这件事的首部字段。 Expires的值对应一个GMT(格林尼治时间),比如Mon, 22 Jul 2002 11:12:01 GMT来告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求, 如 107 | 108 | ``` 109 | Expires: Fri, 11 Jun 2021 11:33:01 GMT 110 | ``` 111 | 如果Pragma头部和Expires头部同时存在,则起作用的会是Pragma,有兴趣的同学可以自己试一下。 112 | 需要注意的是,响应报文中Expires所定义的缓存时间是相对服务器上的时间而言的,其定义的是资源“失效时刻”,如果客户端上的时间跟服务器上的时间不一致(特别是用户修改了自己电脑的系统时间),那缓存时间可能就没啥意义了。 113 | 114 | 115 | > Last-Modified 116 | 117 | 服务器将资源传递给客户端时,会将资源最后更改的时间以“Last-Modified: GMT”的形式加在实体首部上一起返回给客户端。 118 | 119 | ``` 120 | Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT 121 | ``` 122 | 123 | 客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并带给服务器去做检查,若传递的时间值与服务器上该资源最终修改时间是一致的,则说明该资源没有被修改过,直接返回304状态码,内容为空,这样就节省了传输数据量 。如果两个时间不一致,则服务器会发回该资源并返回200状态码,和第一次请求时类似。这样保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。一个304响应比一个静态资源通常小得多,这样就节省了网络带宽。 124 | 125 | 126 | ### 缓存头部对比 127 | 128 | 屏幕快照 2019-05-10 上午1 07 28 129 | 130 | ### HTTP缓存匹配流程 131 | 132 | HTTP 头信息控制缓存: 大致分为两种:强缓存和协商缓存。强缓存如果命中缓存不需要和服务器端发生交互,而协商缓存不管是否命中都要和服务器端发生交互,强制缓存的优先级高于协商缓存。 133 | 134 | ![22](https://user-images.githubusercontent.com/42414989/57472554-48ab8b80-72c0-11e9-963c-98435b7c192b.jpg) 135 | 136 | 137 | #### 强缓存 138 | 139 | 可以理解为无须验证的缓存策略。对强缓存来说,响应头中有两个字段 Expires/Cache-Control 来表明规则。 140 | 141 | - Expires 142 | 143 | Expires 指缓存过期的时间,超过了这个时间点就代表资源过期。有一个问题是由于使用具体时间,如果时间表示出错或者没有转换到正确的时区都可能造成缓存生命周期出错。并且 Expires 是 HTTP/1.0 的标准,现在更倾向于用 HTTP/1.1 中定义的 Cache-Control。两个同时存在时也是 Cache-Control 的优先级更高。 144 | 145 | - Cache-Control 146 | 147 | Cache-Control 可以由多个字段组合而成。 148 | 149 | #### 协商缓存 150 | 151 | 缓存的资源到期了,并不意味着资源内容发生了改变,如果和服务器上的资源没有差异,实际上没有必要再次请求。客户端和服务器端通过某种验证机制验证当前请求资源是否可以使用缓存。浏览器第一次请求数据之后会将数据和响应头部的缓存标识存储起来。再次请求时会带上存储的头部字段,服务器端验证是否可用。如果返回 304 Not Modified,代表资源没有发生改变可以使用缓存的数据,获取新的过期时间。反之返回 200 就相当于重新请求了一遍资源并替换旧资源。 152 | 153 | - Last-modified/If-Modified-Since 154 | 155 | Last-modified: 服务器端资源的最后修改时间,响应头部会带上这个标识。第一次请求之后,浏览器记录这个时间,再次请求时,请求头部带上 If-Modified-Since 即为之前记录下的时间。服务器端收到带 If-Modified-Since 的请求后会去和资源的最后修改时间对比。若修改过就返回最新资源,状态码 200,若没有修改过则返回 304。 156 | 注意:如果响应头中有 Last-modified 而没有 Expire 或 Cache-Control 时,浏览器会有自己的算法来推算出一个时间缓存该文件多久,不同浏览器得出的时间不一样,所以 Last-modified 要记得配合 Expires/Cache-Control 使用。 157 | 158 | 屏幕快照 2019-05-10 上午1 12 58 159 | 160 | 161 | - Etag/If-None-Match 162 | 163 | 由服务器端上生成的一段 hash 字符串,第一次请求时响应头带上 ETag: abcd,之后的请求中带上 If-None-Match: abcd,服务器检查 ETag,返回 304 或 200。 164 | 165 | 屏幕快照 2019-05-10 上午1 13 19 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /docs/tcpIp/README.md: -------------------------------------------------------------------------------- 1 | - [URL从输入到页面渲染的全流程](https://github.com/NuoHui/fe-note/blob/master/docs/tcpIp/network/%E7%BD%91%E7%BB%9C%E6%98%AF%E5%A6%82%E4%BD%95%E9%93%BE%E6%8E%A5%E7%9A%84.md) 2 | 3 | - [HTTP缓存控制](https://github.com/NuoHui/fe-note/blob/master/docs/tcpIp/HTTP%E7%BC%93%E5%AD%98.md) 4 | 5 | - [TCP三次握手](https://github.com/NuoHui/fe-note/blob/master/docs/tcpIp/TCP%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B.md) 6 | 7 | - [TCP四次挥手](https://github.com/NuoHui/fe-note/blob/master/docs/tcpIp/TCP%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B.md) 8 | -------------------------------------------------------------------------------- /docs/tcpIp/TCP三次握手.md: -------------------------------------------------------------------------------- 1 | ## 三次握手 2 | 3 | 屏幕快照 2019-05-11 上午10 46 32 4 | 5 | 分析上图: 6 | 7 | - 第一次握手 8 | 9 | 客户端发送一个syn包到服务端, 等待服务端回应, 进入到syn-send状态。同时会发送一个Seq=j, Seq是一个序列号, 第一次建立连接时候客户端会随机生成一个ISN(32位长到序列号Initial Sequence Number)作为起始原点, 并把该值告诉服务端, 只要为了后续对字节进行序号确认保证传输的可靠性。 10 | 11 | 12 | - 第二次握手 13 | 14 | 服务器接受到syn包返回(acknowledgment)ack=j+1表示确认接受, 以及一个syn=k包, 进入到syn-received状态 15 | 16 | - 第三次握手 17 | 18 | 客户端接受到ack=j+1, 进入ESTABLISHED状态。然后根据服务端发过来的syn=k, 返回ack+k+1表示确认接受, 等待服务端接受ack回复。至此服务器与客户端可以进行正常数据收发操作。 19 | 20 | 21 | 引用知乎上一张书中图片: 22 | 23 | ![11](https://user-images.githubusercontent.com/42414989/57564295-535b4300-73dc-11e9-8adb-997c8ada272d.jpg) 24 | -------------------------------------------------------------------------------- /docs/tcpIp/TCP四次挥手.md: -------------------------------------------------------------------------------- 1 | ## TCP四次挥手 2 | 3 | 屏幕快照 2019-05-11 上午11 12 58 4 | 5 | - 第一次挥手 6 | 7 | 事实上任何一方都可以先表示断开连接, 这里以客户端为例。首先客户端发送FIN包➕一个Seq=M(客户端的ibn号), 此时表示客户端进入FIN-WAIT-1状态。表示客户端已经没有任何数据需要再发送了。 8 | 9 | - 第二次挥手 10 | 11 | 服务端接受到客户端发过来到FIN M包, 然后向客户端返回ACK=M+1, 此时服务端进入CLOSE-WAIT阶段, 客户端进入FIN-WAIT-2状态。 12 | 13 | - 第三次挥手 14 | 15 | 服务端向客户端发送FIN包➕Seq=N, 请求关闭连接,同时server进入LAST-ACK状态。 16 | 17 | - 第四次挥手 18 | 19 | client收到server发送的FIN(N)包,进入TIME-WAIT状态。向server发送**ACK(N+1)**包,server收到client的ACK(N+1)包以后,进入CLOSE状态;client等待一段时间还没有得到回复后判断server已正式关闭,进入CLOSE状态。 20 | 21 | 最后通过指数一个完整的图来描述: 22 | ![22](https://user-images.githubusercontent.com/42414989/57564488-062ca080-73df-11e9-9aa1-e0f79c805969.jpg) 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/tcpIp/network/Part1: 浏览器生成消息.md: -------------------------------------------------------------------------------- 1 | ## 第一章: 浏览器生成消息 2 | 3 | 屏幕快照 2019-05-03 下午11 41 42 4 | 5 | ### 从输入URL开始 6 | 7 | URL: Uniform Resource Locator,统一资源定位符。 8 | 下面是一些常见的URL类型。 9 | 10 | - HTTP/HTTPS协议 11 | 12 | 屏幕快照 2019-05-03 下午11 45 14 13 | 14 | - FTP协议 15 | 16 | 屏幕快照 2019-05-03 下午11 45 37 17 | 18 | - File协议 19 | 20 | 屏幕快照 2019-05-03 下午11 46 17 21 | 22 | - mailto协议 23 | 24 | 屏幕快照 2019-05-03 下午11 46 44 25 | 26 | ### 浏览器解析URL 27 | 28 | 我们这里因为是要访问Web服务器, 因此以HTTP协议为例子, 当用户输入URL地址后, 浏览器根据URL元素规则去解析URL。 29 | 30 | 屏幕快照 2019-05-03 下午11 49 31 31 | 32 | 实际上http://www.lab.glasscom.com/dir1/file1.html在服务器存储位置是这样的。 33 | 屏幕快照 2019-05-03 下午11 51 08 34 | 35 | #### 关于某些特殊URL的处理情况 36 | 37 | 即省略文件名的情况, 如: 38 | 39 | ``` 40 | http://www.lab.glasscom.com/dir/。 41 | http://www.lab.glasscom.com/ (访问根目录) 42 | http://www.lab.glasscom.com 43 | ``` 44 | 45 | 如果没有具体文件名, 我们就会去访问服务器上设置好的默认文件名, 这个具体设置根据服务器不同而不同, 大多数是index.html或者default.html。因此,像前面这样省略文件名时,服务器就会访问 /dir/index.html 或者 /dir/default.htm。 46 | 47 | 还有一种比较特殊的 48 | 49 | ``` 50 | http://www.lab.glasscom.com/whatisthis 51 | ``` 52 | 这种情况会按照下面的惯例进行处理:如果 Web 服务器上存在名为 whatisthis 的文件,则将 whatisthis 作为文件名来处 理;如果存在名为 whatisthis 的目录,则将 whatisthis 作为目录名来处理。 53 | 54 | ### HTTP 的基本思路 55 | 56 | 通过解析URL, 我们就知道要访问的目标在哪里, 然后浏览器通过HTTP协议来访问Web服务器。 57 | 58 | #### HTTP协议的基本交互 59 | 60 | 屏幕快照 2019-05-04 上午12 01 38 61 | 62 | 请求消息: 63 | ``` 64 | 1. 客户端会向服务器发送请求消息 65 | 2. 请求消息中包含的内容是“对什么”和“进行怎样的操作”两个部分, “对什么”的部分称为 URI, “进行怎样的操作”的部分称为方法。 66 | ``` 67 | 68 | 响应消息: 服务器接受到请求消息, 并根据这些要求来完成自己 的工作,然后将结果存放在响应消息中。 69 | 70 | ##### 常见的HTTP方法 71 | 72 | 屏幕快照 2019-05-04 上午12 05 01 73 | 74 | ### 生成HTTP请求消息 75 | 76 | 浏览器通过对URL解析后, 确定了Web服务器和文件名, 接下来就是根据这些信息来生成HTTP请求消息。 77 | 78 | #### 请求消息有严格的规定格式 79 | 80 | 屏幕快照 2019-05-04 上午12 08 31 81 | 82 | #### 顺便解释下响应消息格式 83 | 84 | 响应消息 85 | 86 | #### 常见的一些HTTP头字段 87 | 88 | 屏幕快照 2019-05-04 上午12 10 56 89 | 屏幕快照 2019-05-04 上午12 11 24 90 | 屏幕快照 2019-05-04 上午12 11 51 91 | 屏幕快照 2019-05-04 上午12 12 29 92 | 屏幕快照 2019-05-04 上午12 13 02 93 | 屏幕快照 2019-05-04 上午12 13 37 94 | 95 | #### 响应状态码 96 | 97 | 屏幕快照 2019-05-04 上午12 14 20 98 | 99 | ### 向 DNS 服务器查询 Web 服务器的 IP 地址 100 | 101 | 生成HTTP请求消息后, 浏览器委托操作系统把报文发送到服务器, 但是在发送之前我们还需要查询服务器域名对应的IP地址。 102 | 103 | #### 关于IP的基础认识 104 | 105 | 互联网和公司内部的局域网都是基于 TCP/IP 的思路来设计的,所以我 们先来了解 TCP/IP 的基本思路。 106 | 107 | 屏幕快照 2019-05-04 上午12 22 02 108 | 109 | 由一些 小的子网,通过路由器连接起来组成一个大的网络。这里的子网可以理解 为用集线器连接起来的几台计算机,我们将它看作一个单位,称为子网。 将子网通过路由器连接起来,就形成了一个网络 。 110 | 111 | 在网络中,所有的设备都会被分配一个地址。这个地址就相当于现实 中某条路上的“×× 号 ×× 室”。其中“号”对应的号码是分配给整个子 网的,而“室”对应的号码是分配给子网中的计算机的,这就是网络中的 地址。“号”对应的号码称为网络号,“室”对应的号码称为主机号,这个 地址的整体称为 IP 地址。 112 | 113 | 一个简单的消息传送的具体过程: 发送者发出的消息首先经过子网中的集线器,转发到距离发送者最近的路由器上。接下来, 路由器会根据消息的目的地判断下一个路由器的位置,然后将消息发送 到下一个路由器,即消息再次经过子网内的集线器被转发到下一个路由 器。前面的过程不断重复,最终消息就被传送到了目的地。 114 | 115 | #### IP地址的内部结构 116 | 117 | 实际的 IP 地址是一串 32 比特的数字,按照 8 比特(1 字节)为一组分成 4 组,分别用十进制表示 然后再用圆点隔开。 118 | 119 | 这就是我们平常经常见到的 IP 地址格式,但仅凭这一 串数字我们无法区分哪部分是网络号,哪部分是主机号。在 IP 地址的规则 中,网络号和主机号连起来总共是 32 比特,但这两部分的具体结构是不固 定的。在组建网络时,用户可以自行决定它们之间的分配关系,因此,我 们还需要另外的附加信息来表示 IP 地址的内部结构。 120 | 121 | 屏幕快照 2019-05-04 上午12 28 25 122 | 123 | 这一附加信息称为子网掩码。子网掩码的格式有多种如下图所示: 124 | 125 | 屏幕快照 2019-05-04 上午12 29 25 126 | 127 | 子网掩码是一 串与 IP 地址长度相同的 32 比特数字,其左边一半都是 1,右边一半都是0。其中,子网掩码为 1 的部分表示网络号,子网掩码为 0 的部分表示主机 号。 128 | 129 | #### 为什么我们需要同时使用域名和IP 130 | 131 | TCP/IP 网络是通过 IP 地址来确定通信对象的,因此不知道 IP 地址就无法将消息发送给对方, 因此,在委托操作系统发送消息时,必须要先查询好对方 的 IP 地址。 132 | 133 | 之所以不在网站里面使用IP地址, 是因为相比 IP 地址来说,网址中还是使用服务器 名称比较好。 134 | 当然也有人说那为什么TCP/IP网络不使用域名来确定通信对象? IP 地址的长度 为 32 比特,也就是 4 字节,相对地,域名最短也要几十个字节,最长甚至 可以达到 255 字节。换句话说,使用 IP 地址只需要处理 4 字节的数字,而 域名则需要处理几十个到 255 个字节的字符,这增加了路由器的负担,传送 数据也会花费更长的时间。 135 | 136 | 因此最后确认人使用域名, 让服务器使用IP, 那么因此我们需要一种机制来实现域名与IP地址的映射查询。 137 | 138 | - Socket 库提供查询 IP 地址的功能 139 | 140 | 向 DNS 服务器发出查询(基于UDF),也就是向 DNS 服务器发送查询消息,并接 收服务器返回的响应消息。 141 | 142 | 向 DNS 服务器发送消息时,我们当然也需要知道 DNS 服 务器的 IP 地址。只不过这个 IP 地址是作为 TCP/IP 的一个设置项目事先设 置好的,不需要再去查询了。不同的操作系统中 TCP/IP 的设置方法也有差 异,Windows 中的设置如图。 143 | 144 | 屏幕快照 2019-05-04 上午12 42 00 145 | 146 | 147 | 对于 DNS 服务器,我们的计算机上 一定有相应的 DNS 客户端,而相当于 DNS 客户端的部分称为 DNS 解析 器,或者简称解析器。通过 DNS 查询 IP 地址的操作称为域名解析,因此 负责执行解析(resolution)这一操作的就叫解析器(resolver)了。 148 | 149 | - 通过解析器向 DNS 服务器发出查询 150 | 151 | 屏幕快照 2019-05-04 上午12 38 48 152 | 153 | 调用解析器后,解析器会向 DNS 服务器发送查询消息,然后 DNS 服 务器会返回响应消息。响应消息中包含查询到的 IP 地址,解析器会取出 IP 地址,并将其写入浏览器指定的内存地址中。接 下来,浏览器在向 Web 服务器发送消息时,只要从该内存地址取出 IP 地 址,并将它与 HTTP 请求消息一起交给操作系统就可以了。 154 | 155 | #### DNS服务器的接力查询 156 | 157 | - DNS 服务器的基本工作 158 | 159 | DNS 服务器的基本工作就是接收来自客户端的查询消 息,然后根据消息的内容返回响应。 160 | 161 | ``` 162 | 来自客户端的查询消息包含以下 3 种信息 163 | 164 | - (a)域名: 服务器、邮件服务器(邮件地址中 @ 后面的部分)的名称 165 | - (b)Class: 在最早设计 DNS 方案时,DNS 在互联网以外的其他网络中的应用也被考虑到了,而 Class 就是用来识别网络的信息。不过,如今除了 166 | 互联网并没有其他的网络了,因此 Class 的值永远是代表互联网的 IN 167 | - 记录类型: 表示域名对应何种类型的记录。例如,当类型为 A 时,表示域名 对应的是 IP 地址;当类型为 MX 时,表示域名对应的是邮件服务 器。对于不同的记录类型,服务器向客户端返回的信息也会不同 168 | ``` 169 | DNS 服务器上事先保存有前面这 3 种信息对应的记录数据, 下图是DNS服务器基本工作流程: 170 | 屏幕快照 2019-05-04 上午12 45 01 171 | 172 | - 域名的层次结构 173 | 174 | 互联网中存在着不计其数的服务器,将这些服务器的信息全部保存在一台 DNS 服务器中是不可能的,因此一定 会出现在 DNS 服务器中找不到要查询的信息的情况。下面来看一看此时 DNS 服务器是如何工作的。 175 | 176 | 其实就是将信息分布保存在多台 DNS 服务器中, 这些 DNS 服务器相互接力配合,从而查找出要查询的信息。 177 | 178 | 首先,DNS 服务器中的所有信息都是按照域名以分层次的结构来保存 的。层次结构这个词听起来可能有点不容易懂,其实就类似于公司中的事 业集团、部门、科室这样的结构。层次结构能够帮助我们更好地管理大量 的信息。 179 | 180 | DNS 中的域名都是用句点来分隔的,比如 www.lab.glasscom.com,这 里的句点代表了不同层次之间的界限,就相当于公司里面的组织结构不用 部、科之类的名称来划分,只是用句点来分隔而已 A。在域名中,越靠右的 位置表示其层级越高,比如 www.lab.glasscom.com 这个域名如果按照公司 里的组织结构来说,大概就是“com 事业集团 glasscom 部 lab 科的 www” 这样。其中,相当于一个层级的部分称为域。因此,com 域的下一层是 glasscom 域,再下一层是 lab 域,再下面才是 www 这个名字。 181 | 182 | - 寻找相应的 DNS 服务器并获取 IP 地址 183 | 184 | 首先,将负责管理下级域的 DNS 服务器的 IP 地址注 册到它们的上级 DNS 服务器中,然后上级 DNS 服务器的 IP 地址再注册到 更上一级的 DNS 服务器中,以此类推。 185 | 186 | 以前面www.lab.glasscom.com为例, 负责管理 lab.glasscom.com 这个域的 DNS 服务器的 IP 地址需要注册到 glasscom.com 域的 DNS 服务器中,而 glasscom.com 域的 DNS 服务器的 IP 地址又需要注册到 com 域的 DNS 服务器中。这样,我们就可以通过上级 DNS 服务器查询出下级 DNS 服务器的 IP 地址,也就可以向下级 DNS 服务器发送查询请求了。 187 | 188 | 在互联网 中,com 和 jp 的上面还有一级域,称为根域。根域不像 com、jp 那样有自 己的名字。根域的 DNS 服务器中保管着 com、jp 等的 DNS 服务器的信息。由于上级 DNS 服务器保管着所有下级 DNS 服务器的信息,所以我们可以从根域开始一路往下顺藤摸瓜找到任意 一个域的 DNS 服务器。 189 | 190 | 实际上,根域 DNS 服务器的相关信息已经包含在 DNS 服务器程序的配置文件中了,因 此只要安装了 DNS 服务器程序,这些信息也就被自动配置好了。 191 | 192 | 屏幕快照 2019-05-04 上午12 51 14 193 | 194 | 屏幕快照 2019-05-04 上午12 51 43 195 | 196 | - 通过缓存加快 DNS 服务器的响应 197 | 198 | 在真实的互联网中,一台 DNS 服务器可以管理多个域的信息, 现实中上级域和下级域有可能共享同一 台 DNS 服务器。在这种情况下,访问上级 DNS 服务器时就可以向下跳过 一级 DNS 服务器,直接返回再下一级 DNS 服务器的相关信息。 199 | 200 | 此外,有时候并不需要从最上级的根域开始查找,因为 DNS 服务器有一 个缓存功能,可以记住之前查询过的域名。如果要查询的域名和相关信息已 经在缓存中,那么就可以直接返回响应,接下来的查询可以从缓存的位置开 始向下进行。相比每次都从根域找起来说,缓存可以减少查询所需的时间。 201 | 202 | 并且,当要查询的域名不存在时,“不存在”这一响应结果也会被缓 存。这样,当下次查询这个不存在的域名时,也可以快速响应。 203 | 204 | DNS 服务器中保存的信息都设置有一个有效期,当缓存中的信息超过有效期后,数 据就会从缓存中删除。而且,在对查询进行响应时,DNS 服务器也会告知客 户端这一响应的结果是来自缓存中还是来自负责管理该域名的 DNS 服务器。 205 | 206 | ### 委托协议栈发送消息 207 | 208 | 知道了 IP 地址之后,就可以委托操作系统内部的协议栈向这个目标 IP 地址,也就是我们要访问的 Web 服务器发送消息了。 209 | 210 | 屏幕快照 2019-05-04 上午12 55 03 211 | 212 | 熟悉网络编程的应该很容易理解套接字。 213 | 简述收发数据的操作分为若干个阶段,可以大致总结为以下 4 个。 214 | 215 | ``` 216 | - 创建套接字(创建套接字阶段) 217 | - 将管道连接到服务器端的套接字上(连接阶段) 218 | - 收发数据(通信阶段) 219 | - 断开管道并删除套接字(断开阶段) 220 | 221 | ``` 222 | 223 | 好了第一章结束了, 第二章详细介绍Tcp/IP传输数据过程。 224 | -------------------------------------------------------------------------------- /docs/tcpIp/network/网络是如何链接的.md: -------------------------------------------------------------------------------- 1 | # URL从输入到页面渲染的全流程 2 | 3 | ## [第一章: 浏览器生成消息](https://github.com/NuoHui/fe-note/blob/master/docs/tcpIp/network/Part1:%20%E6%B5%8F%E8%A7%88%E5%99%A8%E7%94%9F%E6%88%90%E6%B6%88%E6%81%AF.md) 4 | 5 | 浏览器、Web服务器、网址(URL)、HTTP、 HTML、协议、URI、请求消息、解析器、 Socket库、DNS服务器、域名 6 | 7 | ## 第二章: 用电信号传输TCP/IP数据 8 | 9 | TCP/IP、套接字、协议栈、IP地址、端口号、 包、头部、网卡、网卡驱动、MAC地址、 以太网控制器、ICMP、UDP 10 | 11 | ## 第三章: 从网线到网络设备 12 | 13 | 局域网(LAN)、双绞线、串扰、中继式集线器、 MDI、MDI-X、交换式集线器、全双工、半双工、 碰撞、自动协商、路由器、路由表、子网掩码、 默认网关、分片、地址转换、公有地址、私有地址 14 | 15 | ## 第四章: 通过接入网进入互联网内部 16 | 17 | ADSL、FTTH、光纤、接入网、 ADSL Modem集成式路由器、ATM、信元、 正交振幅调制、分离器、DSLAM、 宽带接入服务器、远程接入服务器、PPP、 网络运行中心(NOC)、光纤、 IX(Internet eXchange,互联网交换) 18 | 19 | ## 第五章: 服务器端的局域网中有什么玄机 20 | 21 | 防火墙、包过滤、数据中心、轮询、负载均衡器、 22 | 缓存服务器、代理、代理服务器、内容分发服务、 23 | 重定向 24 | 25 | ## 第六章: 请求到达Web服务器,响应返回浏览器 26 | 27 | 响应消息、多任务、多线程、虚拟目录、CGI、 表单、访问控制、密码、数据格式、MIME 28 | -------------------------------------------------------------------------------- /docs/vue/README.md: -------------------------------------------------------------------------------- 1 | - [Vue最佳实践](https://github.com/NuoHui/fe-note/blob/master/docs/vue/Vue%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.md) 2 | 3 | - [Vue源码: 架构和目录设计](https://github.com/NuoHui/fe-note/blob/master/docs/vue/Vue%E6%BA%90%E7%A0%81:%20%E6%9E%B6%E6%9E%84%E5%92%8C%E7%9B%AE%E5%BD%95%E8%AE%BE%E8%AE%A1.md) 4 | 5 | - [Vue源码: 构造函数的入口](https://github.com/NuoHui/fe-note/blob/master/docs/vue/Vue%E6%BA%90%E7%A0%81:%20%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E5%85%A5%E5%8F%A3.md) 6 | 7 | - [Vue源码: Vue.use(), vm.$delete()内部原理](https://github.com/NuoHui/fe-note/blob/master/docs/vue/Vue%E6%BA%90%E7%A0%81:%20Vue.use%2C%20vm.%24delete%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86.md) 8 | 9 | - [Vue源码: vm.$set()原理分析](https://github.com/NuoHui/fe-note/blob/master/docs/vue/Vue%E6%BA%90%E7%A0%81:%20vm.%24set%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md) 10 | 11 | - [Vue源码: 关于vm.$watch()内部原理](https://github.com/NuoHui/fe-note/blob/master/docs/vue/Vue%E6%BA%90%E7%A0%81:%20%E5%85%B3%E4%BA%8Evm.%24watch%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86.md) 12 | 13 | - [Vue源码: vm.$mount()内部原理](https://github.com/NuoHui/fe-note/blob/master/docs/vue/Vue%E6%BA%90%E7%A0%81:%20vm.%24mount%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86.md) 14 | 15 | - [Vue源码: 事件相关的实例方法](https://github.com/NuoHui/fe-note/blob/master/docs/vue/Vue%E6%BA%90%E7%A0%81:%20%E4%BA%8B%E4%BB%B6%E7%9B%B8%E5%85%B3%E7%9A%84%E5%AE%9E%E4%BE%8B%E6%96%B9%E6%B3%95.md) 16 | 17 | - [Vue源码: 关于对Array的数据侦听](https://github.com/NuoHui/fe-note/blob/master/docs/vue/Vue%E6%BA%90%E7%A0%81:%20%E5%85%B3%E4%BA%8E%E5%AF%B9Array%E7%9A%84%E6%95%B0%E6%8D%AE%E4%BE%A6%E5%90%AC.md) 18 | -------------------------------------------------------------------------------- /docs/vue/Vue最佳实践.md: -------------------------------------------------------------------------------- 1 | # 最佳实践 2 | 3 | 即看到或者用到的关于Vue.js的最佳使用技巧。 4 | 5 | > Object.freeze() 6 | 7 | 我们都知道Vue.js会对data选项里面的数据进行Observer, 以此来达到数据响应式, 但是很明显这个操作是比较浪费性能的, 如果我们明确不需要state不需要是响应式数据(如展示型的长列表, 或者深层次嵌套的大JS对象), 可以使用Object.freeze()来阻止Vue对其进行Observer。 8 | 9 | > 善用计算属性的get/set 10 | 11 | 屏幕快照 2019-05-03 下午9 29 18 12 | 13 | > 善用watch 14 | 15 | ``` 16 | var vm = new Vue({ 17 | data: { 18 | a: 1, 19 | b: 2, 20 | c: 3, 21 | d: 4, 22 | e: { 23 | f: { 24 | g: 5 25 | } 26 | } 27 | }, 28 | watch: { 29 | a: function (val, oldVal) { 30 | console.log('new: %s, old: %s', val, oldVal) 31 | }, 32 | // 方法名 33 | b: 'someMethod', 34 | // 深度 watcher 35 | c: { 36 | handler: function (val, oldVal) { /* ... */ }, 37 | deep: true 38 | }, 39 | // 该回调将会在侦听开始之后被立即调用 40 | d: { 41 | handler: function (val, oldVal) { /* ... */ }, 42 | immediate: true 43 | }, 44 | e: [ 45 | function handle1 (val, oldVal) { /* ... */ }, 46 | function handle2 (val, oldVal) { /* ... */ } 47 | ], 48 | // watch vm.e.f's value: {g: 5} 49 | 'e.f': function (val, oldVal) { /* ... */ } 50 | } 51 | }) 52 | vm.a = 2 // => new: 2, old: 1 53 | ``` 54 | 55 | > 路由ID变了,但组件没变? 56 | 57 | 58 | 屏幕快照 2019-05-03 下午9 32 09 59 | 60 | 61 | > 如何在每一个路由后面添加query 62 | 63 | ``` 64 | const transitionTo = router.history.transitionTo 65 | 66 | router.history.transitionTo = function (location, onComplete, onAbort) { 67 | const query = {a: 'xx'} 68 | location = typeof location === 'object' 69 | ? {...location, query: {...location.query, ...query}} 70 | : {path: location, query} 71 | 72 | transitionTo.call(router.history, location, onComplete, onAbort) 73 | } 74 | ``` 75 | 76 | > 事件监听器如何无副作用传递参数 77 | 78 | 79 | ``` 80 | @click="someMethod" - 没有参数 81 | 82 | @click="someMethod(1, 2, 3)" 没有event对象 83 | 84 | @click="event => {someMethod(event, 1, 2, 3)}" - 有参数&event 85 | 86 | @click="someMethod($event, 1, 2, 3)" - 更优雅 87 | 88 | ``` 89 | 90 | > 不建议在业务代码中使用原生的DOM/BOM API 91 | 92 | ``` 93 | - 无法跨平台 94 | - 不符合设计 95 | ``` 96 | 应该改为使用 97 | 98 | ``` 99 | - 指令 100 | - 插件 101 | ``` 102 | 103 | > 避免隐性的父子组件通信 104 | 105 | 建议使用prop和事件, 避免使用this.$parent, this.$refs等 106 | 107 | > 为组件样式设置作用域 108 | 109 | 可以使用scoped 特性 或者 CSS Modules。 110 | 111 | > Vue官方推荐指南 112 | 113 | [风格指南](https://cn.vuejs.org/v2/style-guide/)。 114 | 115 | 116 | ## 架构参考 117 | 118 | 屏幕快照 2019-05-03 下午9 56 18 119 | 120 | ### 基础设施层 121 | 122 | ``` 123 | init => 自动初始化配置文件 => 脚手架 124 | dev => 启动dev-server, hot-reload, http-proxy 125 | deploy => 编译源码, 静态文件上传CDN, 生成html, 发布上线 126 | ``` 127 | 128 | ### api层 129 | 130 | ``` 131 | 请求数据 132 | Mock数据 133 | 反向校验数据 134 | ``` 135 | ### Util 136 | 137 | 全局工具函数集 138 | 139 | ### service层 140 | 141 | 整合server端返回的数据(如几个api返回的数据), 然后一起发到Store层。 142 | 143 | ### 全局事件机制 event-bus 144 | 145 | 一般只用于处理特殊需求, 如业务存在游客模式与用户模式(登陆), 作为用户模式后的提醒 146 | 147 | ### View层 148 | 149 | ``` 150 | - router(路由) 151 | - styles 152 | --- site 153 | ------ common.scss(存储该App公共的基础色调) 154 | - modules(业务层) 155 | - components(组件) 156 | ``` 157 | 158 | ### 关于store层 159 | 160 | 我们选择抽离Api层。ps: 这一层仅供参考。 161 | 162 | 首先我们之前是直接在页面里面调用后端的api接口, 后面发现这样实现代码耦合度太高, 于是决定抽离出Api层。 163 | 164 | ``` 165 | // 我们是按照业务模块来分层代码, 下面以用户模块为例子 166 | 167 | - modules 168 | --- user 169 | ----- api.js(所有的user模块下需要的接口) 170 | ----- store.js(在actions下面调用api.js下接口) 171 | ----- assets(静态资源) 172 | ----- index.js(入口文件) 173 | ----- user.vue(业务组件) 174 | ``` 175 | 176 | ## 工程化 177 | 178 | 屏幕快照 2019-05-03 下午10 09 36 179 | 180 | 181 | ## 关于静态文件上传CDN 182 | 183 | 可以借鉴如下思路: 184 | 185 | ``` 186 | cdn-loader + cdn-plugin 自动上传 cdn 187 | ``` 188 | 189 | 屏幕快照 2019-05-03 下午10 20 41 190 | 屏幕快照 2019-05-03 下午10 20 47 191 | 192 | 193 | ``` 194 | var loaderUtils = require('loader-utils') 195 | var qcdn = require('@q/qcdn') 196 | 197 | module.exports = function(content) { 198 | this.cacheable && this.cacheable() 199 | var query = loaderUtils.getOptions(this) || {} 200 | 201 | if (query.disable) { 202 | var urlLoader = require('url-loader') 203 | return urlLoader.call(this, content) 204 | } 205 | 206 | var callback = this.async() 207 | var ext = loaderUtils.interpolateName(this, '[ext]', {content: content}) 208 | 209 | qcdn.content(content, ext) 210 | .then(function upload(url) { 211 | callback(null, 'module.exports = ' + JSON.stringify(url)) 212 | }) 213 | .catch(callback) 214 | } 215 | 216 | module.exports.raw = true 217 | 218 | ``` 219 | 220 | ``` 221 | var qcdn = require('@q/qcdn') 222 | 223 | function CdnAssetsHtmlWebpackPlugin (options) {} 224 | 225 | CdnAssetsHtmlWebpackPlugin.prototype.apply = function (compiler) { 226 | compiler.plugin('compilation', function(compilation) { 227 | var extMap = { 228 | script: { 229 | ext: 'js', 230 | src: 'src' 231 | }, 232 | link: { 233 | ext: 'css', 234 | src: 'href' 235 | }, 236 | } 237 | 238 | compilation.plugin('html-webpack-plugin-alter-asset-tags', function(htmlPluginData, callback) { 239 | console.log('> Static file uploading cdn...') 240 | 241 | var bodys = htmlPluginData.body.map(upload('body')) 242 | var heads = htmlPluginData.head.map(upload('head')) 243 | 244 | function upload (type) { 245 | return function (item, i) { 246 | if (!extMap[item.tagName]) return Promise.resolve() 247 | var source = compilation.assets[item.attributes[extMap[item.tagName].src].replace(/^(\/)*/g, '')].source() 248 | return qcdn.content(source, extMap[item.tagName].ext) 249 | .then(function qcdnDone(url) { 250 | htmlPluginData[type][i].attributes[extMap[item.tagName].src] = url 251 | return url 252 | }) 253 | } 254 | } 255 | 256 | Promise.all(heads.concat(bodys)) 257 | .then(function (result) { 258 | console.log('> Static file upload cdn done!') 259 | callback(null, htmlPluginData) 260 | }) 261 | .catch(callback) 262 | }) 263 | }) 264 | } 265 | 266 | module.exports = CdnAssetsHtmlWebpackPlugin 267 | ``` 268 | 269 | 270 | ## 关于权限控制 271 | 272 | - 路由权限 273 | 274 | 路由权限前端主要进行三个步骤处理 275 | 276 | ``` 277 | 拦截路由 278 | 认证权限(权限判断) 279 | 跳转Error页 280 | ``` 281 | 首先权限信息我们一般埋在路由的meta信息里面, 如: 282 | 283 | ``` 284 | const router = new Router({ 285 | mode: 'history', 286 | routes: [ 287 | { 288 | path: '/category', 289 | name: 'category', 290 | component: Category, 291 | meta: { 292 | permission: { 293 | addImpl: true, 294 | deleteImpl: true, 295 | editImpl: true, 296 | itemImpl: true, 297 | getCategoryParentsImpl: true, 298 | listImpl: true, 299 | searchImpl: true 300 | } 301 | } 302 | } 303 | ] 304 | }) 305 | ``` 306 | 然后在路由的钩子里面进行业务判断 307 | 308 | ``` 309 | router.beforeEach(function (to, from, next) { 310 | if (permission[to.meta.permission] === false) { 311 | router.push({name: 'error-authorize'}) 312 | return false 313 | } 314 | next() 315 | }) 316 | ``` 317 | 318 | - api权限 319 | 320 | server端会对api权限做验证,所以前端没做权限认证,直接走全局错误处理逻辑 321 | 322 | ``` 323 | // 请求拦截器家token等 324 | 325 | // 在响应的拦截器全局处理 326 | axios.interceptors.response.use(function (response) { 327 | if (response.data.errno !== 0) { 328 | Bus.$emit('message', { 329 | message: response.data.msg || '服务器发生错误', 330 | type: 'error' 331 | }) 332 | return Promise.reject(response) 333 | } 334 | return response 335 | }, function (error) { 336 | return Promise.reject(error) 337 | }) 338 | ``` 339 | 340 | - 按钮显示权限 341 | 342 | 使用v-if控制是否显示 343 | 344 | ``` 345 | 350 | 删除 351 | 352 | 353 | computed: { 354 | ...mapState({ 355 | permission: state => state.user.permission 356 | }) 357 | } 358 | ``` 359 | -------------------------------------------------------------------------------- /docs/vue/Vue源码: Vue.use, vm.$delete内部原理.md: -------------------------------------------------------------------------------- 1 | ## vm.$delete() 2 | 3 | vm.$delete用法见[官网](https://cn.vuejs.org/v2/api/#vm-delete)。 4 | 5 | ### 为什么需要Vue.delete()? 6 | 7 | 在ES6之前, JS没有提供方法来侦测到一个属性被删除了, 因此如果我们通过delete删除一个属性, Vue是侦测不到的, 因此不会触发数据响应式。 8 | 9 | 10 | 见下面的demo。 11 | ``` 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Vue Demo 20 | 21 | 22 | 23 |
24 | 名字: {{ user.name }} 年纪: {{ user.age }} 25 | 26 |
27 | 45 | 46 | 47 | 48 | ``` 49 | 50 | ### 源码分析内部实现 51 | 52 | 源码位置vue/src/core/instance/state.js的stateMixin方法 53 | 54 | ``` 55 | export function stateMixin (Vue: Class) { 56 | ... 57 | 58 | Vue.prototype.$set = set 59 | Vue.prototype.$delete = del 60 | 61 | ... 62 | 63 | } 64 | ``` 65 | 66 | 然后查看del函数位置, vue/src/core/observer/index.js。 67 | 68 | ``` 69 | /** 70 | * Delete a property and trigger change if necessary. 71 | * target: 将被删除属性的目标对象, 可以是对象/数组 72 | * key: 删除属性 73 | */ 74 | export function del (target: Array | Object, key: any) { 75 | // 非生产环境下, 不允许删除一个原始数据类型, 或者undefined, null 76 | if (process.env.NODE_ENV !== 'production' && 77 | (isUndef(target) || isPrimitive(target)) 78 | ) { 79 | warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`) 80 | } 81 | // 如果target是数组, 并且key是一个合法索引,通过数组的splcie方法删除值, 并且还能触发数据的响应(数组拦截器截取到变化到元素, 通知依赖更新数据) 82 | if (Array.isArray(target) && isValidArrayIndex(key)) { 83 | target.splice(key, 1) 84 | return 85 | } 86 | // 获取ob 87 | const ob = (target: any).__ob__ 88 | // target._isVue: 不允许删除Vue实例对象上的属性 89 | // (ob && ob.vmCount): 不允许删除根数据对象的属性,触发不了响应 90 | if (target._isVue || (ob && ob.vmCount)) { 91 | process.env.NODE_ENV !== 'production' && warn( 92 | 'Avoid deleting properties on a Vue instance or its root $data ' + 93 | '- just set it to null.' 94 | ) 95 | return 96 | } 97 | // 如果属性压根不在对象上, 什么都不做处理 98 | if (!hasOwn(target, key)) { 99 | return 100 | } 101 | // 走到这一步说明, target是对象, 并且key在target上, 直接使用delete删除 102 | delete target[key] 103 | // 如果ob不存在, 说明target本身不是响应式数据, 104 | if (!ob) { 105 | return 106 | } 107 | // 存在ob, 通过ob里面存储的Dep实例的notify方法通知依赖更新 108 | ob.dep.notify() 109 | } 110 | 111 | ``` 112 | 工具函数 113 | 114 | ``` 115 | // 判断是否v是未定义 116 | export function isUndef (v: any): boolean %checks { 117 | return v === undefined || v === null 118 | } 119 | 120 | // 判断v是否是原始数据类型(基本数据类型) 121 | export function isPrimitive (value: any): boolean %checks { 122 | return ( 123 | typeof value === 'string' || 124 | typeof value === 'number' || 125 | // $flow-disable-line 126 | typeof value === 'symbol' || 127 | typeof value === 'boolean' 128 | ) 129 | } 130 | 131 | // 判断对象上是否有属性 132 | const hasOwnProperty = Object.prototype.hasOwnProperty 133 | export function hasOwn (obj: Object | Array<*>, key: string): boolean { 134 | return hasOwnProperty.call(obj, key) 135 | } 136 | 137 | ``` 138 | 139 | 关于__ob__属性, 在很多源码地方我们都会看到类似这样获取ob(Observer实例) 140 | 141 | ``` 142 | const ob = (target: any).__ob__ 143 | ``` 144 | 牢记只要数据被observe过就会打上这个私有属性, 是在Observer类的构造器里面发生的 145 | 146 | ``` 147 | export class Observer { 148 | constructor (value: any) { 149 | this.value = value 150 | // 依赖是存在Observe上的dep属性, 再次通知依赖更新时候我们一般使用__ob__.dep.notify() 151 | this.dep = new Dep() 152 | this.vmCount = 0 153 | // 定义__ob__ 154 | def(value, '__ob__', this) 155 | if (Array.isArray(value)) { 156 | if (hasProto) { 157 | protoAugment(value, arrayMethods) 158 | } else { 159 | copyAugment(value, arrayMethods, arrayKeys) 160 | } 161 | this.observeArray(value) 162 | } else { 163 | this.walk(value) 164 | } 165 | } 166 | ... 167 | 168 | } 169 | ``` 170 | 171 | 172 | ## Vue.use() 173 | 174 | 大家都知道这个方法是用来安装插件的, 是全局api。 175 | 具体使用见[官网](https://cn.vuejs.org/v2/api/#Vue-use)。 176 | 177 | ### 通过Vue.use()源码+Vuex部分源码分析插件的安装过程 178 | 179 | 180 | #### Vue.use()什么时候被绑在Vue原型上 181 | 182 | 源码位置: vue/src/core/index.js 183 | 184 | 185 | ![](https://user-gold-cdn.xitu.io/2019/5/1/16a7156fcccc2052?w=1450&h=1182&f=png&s=259188) 186 | Vue 187 | 188 | #### initGlobalAPI() 189 | 190 | 源码位置: vue/src/core/global-api/index.js 191 | ``` 192 | export function initGlobalAPI (Vue: GlobalAPI) { 193 | ... 194 | // 初始化use() 195 | initUse(Vue) 196 | ... 197 | 198 | } 199 | ``` 200 | 201 | #### initUse() 202 | 203 | 源码位置: vue/src/core/global-api/use.js 204 | 205 | ``` 206 | export function initUse (Vue: GlobalAPI) { 207 | // 这里的Vue是构造器函数. 208 | // 通过以下源码: 209 | // vue-dev/src/core/global-api/index.js initGlobalAPI()中 210 | // vue-dev/src/core/index.js 这里执行了initGlobalAPI() => 初始化一些全局api 211 | // Vue.use(): 安装Vue.js的插件 212 | // 如果插件是一个对象,必须提供 install 方法 213 | // 如果插件是一个函数,它会被作为 install 方法 214 | // install 方法调用时,会将 Vue 作为参数传入 215 | Vue.use = function (plugin: Function | Object) { 216 | // installedPlugins存储install后的插件 217 | const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])) 218 | if (installedPlugins.indexOf(plugin) > -1) { 219 | // 同一个插件只会安装一次 220 | return this 221 | } 222 | // additional parameters 223 | // 除了插件外的其他参数 Vue.use(MyPlugin, { someOption: true }) 224 | const args = toArray(arguments, 1) 225 | // 往args存储Vue构造器, 供插件的install方法使用 226 | args.unshift(this) 227 | // 分情况执行插件的install方法, 把this(Vue), 参数抛回给install方法 228 | // 所以我们常说, install这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象: 229 | if (typeof plugin.install === 'function') { 230 | // plugin是一个对象 231 | plugin.install.apply(plugin, args) 232 | } else if (typeof plugin === 'function') { 233 | // plugin是一个函数 234 | plugin.apply(null, args) 235 | } 236 | // install之后会存储该插件避免重复安装 237 | installedPlugins.push(plugin) 238 | return this 239 | } 240 | } 241 | 242 | ``` 243 | 244 | #### Vuex源码 245 | 246 | 我们都知道开发一个Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象: 247 | 248 | 那么我们首先就是看Vuex的install方法是怎么实现的 249 | 250 | 251 | 源码位置: vuex-dev/src/store.js 252 | 253 | ``` 254 | let Vue // bind on install 255 | 256 | // install: 装载vuex到vue, Vue.use(Vuex)也是执行install方法 257 | // 关于Vue.use()源码. vue-dev/src/core/global-api/use.js 258 | export function install (_Vue) { 259 | if (Vue && _Vue === Vue) { 260 | if (process.env.NODE_ENV !== 'production') { 261 | console.error( 262 | '[vuex] already installed. Vue.use(Vuex) should be called only once.' 263 | ) 264 | } 265 | return 266 | } 267 | // 首次安装插件, 会把局部的Vue缓存到全局的window.Vue. 主要为了避免重复调用Vue.use() 268 | Vue = _Vue 269 | applyMixin(Vue) 270 | } 271 | ``` 272 | 273 | ##### applyMixin() 274 | 275 | 源码位置: vuex/src/mixin.js 276 | 277 | ``` 278 | export default function (Vue) { 279 | const version = Number(Vue.version.split('.')[0]) 280 | 281 | if (version >= 2) { 282 | // 如果是2.x.x以上版本,注入一个全局mixin, 执行vueInit方法 283 | Vue.mixin({ beforeCreate: vuexInit }) 284 | } else { 285 | // override init and inject vuex init procedure 286 | // for 1.x backwards compatibility. 287 | // 重写Vue原型上的_init方法, 注入vueinit方法 _init方法见 vue-dev/src/core/instance/init.js 288 | const _init = Vue.prototype._init // 作为缓存变量 289 | Vue.prototype._init = function (options = {}) { 290 | options.init = options.init 291 | ? [vuexInit].concat(options.init) 292 | : vuexInit 293 | // 重新执行_init 294 | _init.call(this, options) 295 | } 296 | } 297 | 298 | /** 299 | * Vuex init hook, injected into each instances init hooks list. 300 | */ 301 | // 注入store到Vue构造器 302 | function vuexInit () { 303 | // 这里的this. 指的是Vue构造器 304 | /** 305 | * new Vue({ 306 | * ..., 307 | * store, 308 | * route 309 | * }) 310 | */ 311 | // options: 就是new Vue(options) 312 | // 源码见 vue-dev/src/core/instance/init.js initMixin方法 313 | const options = this.$options 314 | // store injection 315 | // store是我们使用new Vuex.Store(options)的实例 316 | // 注入store到Vue构造函数上的$store属性上, 所以我们在Vue组件里面使用this.$store来使用 317 | if (options.store) { 318 | // options.store为真说明是根节点root 319 | this.$store = typeof options.store === 'function' 320 | ? options.store() 321 | : options.store 322 | } else if (options.parent && options.parent.$store) { 323 | // 子组件直接从父组件中获取$store,这样就保证了所有组件都公用了全局的同一份store 324 | this.$store = options.parent.$store 325 | } 326 | } 327 | } 328 | 329 | ``` 330 | 331 | 至于install方法Vuex是如果执行的? 332 | 333 | ``` 334 | export class Store { 335 | constructor (options = {}) { 336 | // 浏览器环境下安装vuex 337 | if (!Vue && typeof window !== 'undefined' && window.Vue) { 338 | install(window.Vue) 339 | } 340 | ... 341 | } 342 | } 343 | ``` 344 | -------------------------------------------------------------------------------- /docs/vue/Vue源码: vm.$mount内部原理.md: -------------------------------------------------------------------------------- 1 | ## 使用场景 2 | 3 | 我们在写 Vue.js 时,不论是用 CDN 的方式还是基于Webpack的SPA,都会有一个根节点,并且创建一个根实例,比如: 4 | 5 | - CDN 6 | 7 | ``` 8 | 9 |
10 | 11 | 16 | 17 | ``` 18 | 19 | - Webpack一般在入口文件 main.js 里创建一个实例: 20 | 21 | ``` 22 | import Vue from 'vue'; 23 | import App from './app.vue'; 24 | 25 | new Vue({ 26 | el: '#app', 27 | render: h => h(App) 28 | }); 29 | 30 | ``` 31 | 32 | 基于Webpack的SPA,它的 html 里一般都只有一个根节点 `
`,其余都是通过 JavaScript 完成,也就是许多的 Vue.js 组件(每个页面也是一个组件)。 33 | 34 | 有了初始化的实例,之后所有的页面,都由 vue-router 帮我们管理,组件也都是用 `import` 导入后局部注册(也有在 main.js 全局注册的),不管哪种方式,组件(或页面)的创建过程我们是无需关心的,只是写好 `.vue` 文件并导入即可。这样的组件使用方式,有几个特点: 35 | 36 | 1. 所有的内容,都是在 `#app` 节点内渲染的; 37 | 2. 组件的模板,是事先定义好的; 38 | 3. 由于组件的特性,注册的组件只能在当前位置渲染。 39 | 40 | 比如你要使用一个组件 ``,渲染时,这个自定义标签就会被替换为组件的内容,而且在哪写的自定义标签,就在哪里被替换。换句话说,常规的组件使用方式,只能在规定的地方渲染组件,这在一些特殊场景下就比较局限了,例如: 41 | 42 | 1. 组件的模板是通过调用接口从服务端获取的,需要动态渲染组件; 43 | 2. 实现类似原生 `window.alert()` 的提示框组件,它的位置是在 `` 下,而非 `
`,并且不会通过常规的组件自定义标签的形式使用,而是像 JS 调用函数一样使用。 44 | 45 | 一般来说,在我们访问页面时,组件就已经渲染就位了,对于场景 1,组件的渲染是异步的,甚至预先不知道模板是什么。对于场景 2,其实并不陌生,在 jQuery 时代,通过操作 DOM,很容易就能实现,你可以沿用这种思路,只是这种做法不那么 Vue,既然使用 Vue.js 了,就应该用 Vue 的思路来解决问题。对于这两种场景,Vue.extend 和 vm.$mount 语法就派上用场了。 46 | 47 | 48 | ## 方法 49 | 50 | 上文我们说到,创建一个 Vue 实例时,都会有一个选项 `el`,来指定实例的根节点,如果不写 `el` 选项,那组件就处于未挂载状态。`Vue.extend` 的作用,就是基于 Vue 构造器,创建一个“子类”,它的参数跟 `new Vue` 的基本一样,但 `data` 要跟组件一样,是个函数,再配合 `$mount` ,就可以让组件渲染,并且挂载到任意指定的节点上,比如 body。 51 | 52 | ``` 53 | 54 | 55 | 56 | 57 | 58 | 59 | Vue Demo 60 | 61 | 62 | 63 | 80 | 81 | 82 | 83 | ``` 84 | 85 | 当然也可以直接手动就挂载 86 | 87 | ``` 88 | // 手动直接挂载 89 | new AlertComponent().$mount('#app') 90 | new AlertComponent({ el: '#app' }); 91 | ``` 92 | 93 | 除了使用extend方法外还可以使用render函数创建Vue实例。 94 | 95 | ``` 96 | 97 | 98 | 99 | 100 | 101 | 102 | Vue Demo 103 | 104 | 105 | 106 | 127 | 128 | 129 | 130 | ``` 131 | 132 | 需要注意的是,我们是用 `$mount` 手动渲染的组件,如果要销毁,也要用 `$destroy` 来手动销毁实例,必要时,也可以用 `removeChild` 把节点从 DOM 中移除。 133 | 134 | 135 | ## $mount源码分析 136 | 137 | ### $mount函数执行位置 138 | 139 | 140 | ![new Vue()](https://user-gold-cdn.xitu.io/2019/3/10/169684ecc93f837e?w=2092&h=1196&f=jpeg&s=927379) 141 | 142 | _init这个私有方法是在执行initMixin时候绑定到Vue原型上的。 143 | 144 | 145 | ![mount](https://user-gold-cdn.xitu.io/2019/3/10/1696850a6f0dad6d?w=2110&h=1118&f=jpeg&s=958661) 146 | 147 | ### $mount函数是如如何把组件挂在到指定元素 148 | 149 | #### $mount函数定义位置 150 | 151 | $mount函数定义位置有两个: 152 | 153 | 第一个是在src/platforms/web/runtime/index.js 154 | 155 | 156 | ![第一个](https://user-gold-cdn.xitu.io/2019/3/11/16968559a7eb9629?w=1778&h=904&f=jpeg&s=783777) 157 | 158 | 这里的$mount是一个public mount method。之所以这么说是因为Vue有很多构建版本, 有些版本会依赖此方法进行有些功能定制, 后续会解释。 159 | 160 | ``` 161 | // public mount method 162 | // el: 可以是一个字符串或者Dom元素 163 | // hydrating 是Virtual DOM 的补丁算法参数 164 | Vue.prototype.$mount = function ( 165 | el?: string | Element, 166 | hydrating?: boolean 167 | ): Component { 168 | // 判断el, 以及宿主环境, 然后通过工具函数query重写el。 169 | el = el && inBrowser ? query(el) : undefined 170 | // 执行真正的挂载并返回 171 | return mountComponent(this, el, hydrating) 172 | } 173 | ``` 174 | src/platforms/web/runtime/index.js 文件是运行时版 Vue 的入口文件,所以这个方法是运行时版本Vue执行的$mount。 175 | 176 | 关于Vue不同构建版本可以看[Vue对不同构建版本的解释](https://cn.vuejs.org/v2/guide/installation.html#%E5%AF%B9%E4%B8%8D%E5%90%8C%E6%9E%84%E5%BB%BA%E7%89%88%E6%9C%AC%E7%9A%84%E8%A7%A3%E9%87%8A)。 177 | 178 | 关于这个作者封装的工具函数query也可以学习下: 179 | 180 | ``` 181 | /** 182 | * Query an element selector if it's not an element already. 183 | */ 184 | export function query (el: string | Element): Element { 185 | if (typeof el === 'string') { 186 | const selected = document.querySelector(el) 187 | if (!selected) { 188 | // 开发环境下给出错误提示 189 | process.env.NODE_ENV !== 'production' && warn( 190 | 'Cannot find element: ' + el 191 | ) 192 | // 没有找到的情况下容错处理 193 | return document.createElement('div') 194 | } 195 | return selected 196 | } else { 197 | return el 198 | } 199 | } 200 | ``` 201 | 202 | 第二个定义 $mount 函数的地方是src/platforms/web/entry-runtime-with-compiler.js 文件,这个文件是完整版Vue(运行时+编译器)的入口文件。 203 | 204 | 关于运行时与编译器不清楚的童鞋可以看官网[运行时 + 编译器 vs. 只包含运行时](https://cn.vuejs.org/v2/guide/installation.html#%E8%BF%90%E8%A1%8C%E6%97%B6-%E7%BC%96%E8%AF%91%E5%99%A8-vs-%E5%8F%AA%E5%8C%85%E5%90%AB%E8%BF%90%E8%A1%8C%E6%97%B6)。 205 | 206 | 207 | ``` 208 | // 缓存运行时候定义的公共$mount方法 209 | const mount = Vue.prototype.$mount 210 | Vue.prototype.$mount = function ( 211 | el?: string | Element, 212 | hydrating?: boolean 213 | ): Component { 214 | // 通过query方法重写el(挂载点: 组件挂载的占位符) 215 | el = el && query(el) 216 | 217 | /* istanbul ignore if */ 218 | // 提示不能把body/html作为挂载点, 开发环境下给出错误提示 219 | // 因为挂载点是会被组件模板自身替换点, 显然body/html不能被替换 220 | if (el === document.body || el === document.documentElement) { 221 | process.env.NODE_ENV !== 'production' && warn( 222 | `Do not mount Vue to or - mount to normal elements instead.` 223 | ) 224 | return this 225 | } 226 | // $options是在new Vue(options)时候_init方法内执行. 227 | // $options可以访问到options的所有属性如data, filter, components, directives等 228 | const options = this.$options 229 | // resolve template/el and convert to render function 230 | 231 | // 如果包含render函数则执行跳出,直接执行运行时版本的$mount方法 232 | if (!options.render) { 233 | // 没有render函数时候优先考虑template属性 234 | let template = options.template 235 | if (template) { 236 | // template存在且template的类型是字符串 237 | if (typeof template === 'string') { 238 | if (template.charAt(0) === '#') { 239 | // template是ID 240 | template = idToTemplate(template) 241 | /* istanbul ignore if */ 242 | if (process.env.NODE_ENV !== 'production' && !template) { 243 | warn( 244 | `Template element not found or is empty: ${options.template}`, 245 | this 246 | ) 247 | } 248 | } 249 | } else if (template.nodeType) { 250 | // template 的类型是元素节点,则使用该元素的 innerHTML 作为模板 251 | template = template.innerHTML 252 | } else { 253 | // 若 template既不是字符串又不是元素节点,那么在开发环境会提示开发者传递的 template 选项无效 254 | if (process.env.NODE_ENV !== 'production') { 255 | warn('invalid template option:' + template, this) 256 | } 257 | return this 258 | } 259 | } else if (el) { 260 | // 如果template选项不存在,那么使用el元素的outerHTML 作为模板内容 261 | template = getOuterHTML(el) 262 | } 263 | // template: 存储着最终用来生成渲染函数的字符串 264 | if (template) { 265 | /* istanbul ignore if */ 266 | if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 267 | mark('compile') 268 | } 269 | // 获取转换后的render函数与staticRenderFns,并挂在$options上 270 | const { render, staticRenderFns } = compileToFunctions(template, { 271 | outputSourceRange: process.env.NODE_ENV !== 'production', 272 | shouldDecodeNewlines, 273 | shouldDecodeNewlinesForHref, 274 | delimiters: options.delimiters, 275 | comments: options.comments 276 | }, this) 277 | options.render = render 278 | options.staticRenderFns = staticRenderFns 279 | 280 | /* istanbul ignore if */ 281 | // 用来统计编译器性能, config是全局配置对象 282 | if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 283 | mark('compile end') 284 | measure(`vue ${this._name} compile`, 'compile', 'compile end') 285 | } 286 | } 287 | } 288 | // 调用之前说的公共mount方法 289 | // 重写$mount方法是为了添加模板编译的功能 290 | return mount.call(this, el, hydrating) 291 | } 292 | ``` 293 | 294 | 关于idToTemplate方法: 通过query获取该ID获取DOM并把该元素的innerHTML 作为模板 295 | 296 | ``` 297 | const idToTemplate = cached(id => { 298 | const el = query(id) 299 | return el && el.innerHTML 300 | }) 301 | 302 | ``` 303 | getOuterHTML方法: 304 | ``` 305 | /** 306 | * Get outerHTML of elements, taking care 307 | * of SVG elements in IE as well. 308 | */ 309 | function getOuterHTML (el: Element): string { 310 | if (el.outerHTML) { 311 | return el.outerHTML 312 | } else { 313 | // fix IE9-11 中 SVG 标签元素是没有 innerHTML 和 outerHTML 这两个属性 314 | const container = document.createElement('div') 315 | container.appendChild(el.cloneNode(true)) 316 | return container.innerHTML 317 | } 318 | } 319 | ``` 320 | 321 | 关于compileToFunctions函数, 在src/platforms/web/entry-runtime-with-compiler.js中可以看到会挂载到Vue上作为一个全局方法。 322 | 323 | 324 | ![Vue.compile( template )](https://user-gold-cdn.xitu.io/2019/3/11/169687db19baea52?w=2048&h=1084&f=jpeg&s=747414) 325 | 326 | 327 | 328 | ### mountComponent方法: 真正执行绑定组件 329 | 330 | mountComponent函数中是出现在src/core/instance/lifecycle.js。 331 | 332 | ``` 333 | export function mountComponent ( 334 | vm: Component, // 组件实例vm 335 | el: ?Element, // 挂载点 336 | hydrating?: boolean 337 | ): Component { 338 | // 在组件实例对象上添加$el属性 339 | // $el的值是组件模板根元素的引用 340 | vm.$el = el 341 | if (!vm.$options.render) { 342 | // 渲染函数不存在, 这时将会创建一个空的vnode对象 343 | vm.$options.render = createEmptyVNode 344 | if (process.env.NODE_ENV !== 'production') { 345 | /* istanbul ignore if */ 346 | if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || 347 | vm.$options.el || el) { 348 | warn( 349 | 'You are using the runtime-only build of Vue where the template ' + 350 | 'compiler is not available. Either pre-compile the templates into ' + 351 | 'render functions, or use the compiler-included build.', 352 | vm 353 | ) 354 | } else { 355 | warn( 356 | 'Failed to mount component: template or render function not defined.', 357 | vm 358 | ) 359 | } 360 | } 361 | } 362 | // 触发 beforeMount 生命周期钩子 363 | callHook(vm, 'beforeMount') 364 | 365 | // vm._render 函数的作用是调用 vm.$options.render 函数并返回生成的虚拟节点(vnode)。template => render => vnode 366 | 367 | // vm._update 函数的作用是把 vm._render 函数生成的虚拟节点渲染成真正的 DOM。 vnode => real dom node 368 | 369 | let updateComponent // 把渲染函数生成的虚拟DOM渲染成真正的DOM 370 | /* istanbul ignore if */ 371 | if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 372 | updateComponent = () => { 373 | const name = vm._name 374 | const id = vm._uid 375 | const startTag = `vue-perf-start:${id}` 376 | const endTag = `vue-perf-end:${id}` 377 | 378 | mark(startTag) 379 | const vnode = vm._render() 380 | mark(endTag) 381 | measure(`vue ${name} render`, startTag, endTag) 382 | 383 | mark(startTag) 384 | vm._update(vnode, hydrating) 385 | mark(endTag) 386 | measure(`vue ${name} patch`, startTag, endTag) 387 | } 388 | } else { 389 | updateComponent = () => { 390 | vm._update(vm._render(), hydrating) 391 | } 392 | } 393 | 394 | // we set this to vm._watcher inside the watcher's constructor 395 | // since the watcher's initial patch may call $forceUpdate (e.g. inside child 396 | // component's mounted hook), which relies on vm._watcher being already defined 397 | // 创建一个Render函数的观察者, 关于watcher后续再讲述. 398 | new Watcher(vm, updateComponent, noop, { 399 | before () { 400 | if (vm._isMounted && !vm._isDestroyed) { 401 | callHook(vm, 'beforeUpdate') 402 | } 403 | } 404 | }, true /* isRenderWatcher */) 405 | hydrating = false 406 | 407 | // manually mounted instance, call mounted on self 408 | // mounted is called for render-created child components in its inserted hook 409 | if (vm.$vnode == null) { 410 | vm._isMounted = true 411 | callHook(vm, 'mounted') 412 | } 413 | return vm 414 | } 415 | ``` 416 | -------------------------------------------------------------------------------- /docs/vue/Vue源码: vm.$set原理分析.md: -------------------------------------------------------------------------------- 1 | ## vm.$set() 2 | 3 | 关于vm.$set()用法可以看[官网](https://cn.vuejs.org/v2/api/#vm-set),这里就不赘述了。 4 | 5 | Vue官网有这么一段 6 | 7 | ``` 8 | 由于 JavaScript 的限制,Vue 无法检测到以下数组变动: 9 | 10 | 当你使用索引直接设置一项时,例如 vm.items[indexOfItem] = newValue 11 | 当你修改数组长度时,例如 vm.items.length = newLength 12 | ``` 13 | 14 | 可能有人会问为什么Vue不去劫持数组索引或者length属性, 而要选择一个hack方案,我觉得理由如下(欢迎斧正): 15 | 16 | ``` 17 | const a = [1,2,3] 18 | console.log(Object.getOwnPropertyDescriptor(a, 'length')) 19 | // { configurable: false, enumerable: false, value: 3, writable: true } 20 | 我们都知道Vue是通过Object.defineProperty()来劫持数据的get/set. 21 | 而数组的length属性的可枚举性与可配置性不支持修改。 22 | ``` 23 | ``` 24 | 至于索引: 25 | const a = [1,2,3] 26 | console.log(Object.getOwnPropertyDescriptor(a, '0')) 27 | // 28 | {configurable: true,enumerable: true,value: 1,writable: true} 29 | 索引是可以劫持get/set的, 但是如果监听索引的话, 如果你push一个元素进来, 那个元素的索引就没有被劫持, 那么就不会是响应式的, 另外我们对数组多半是遍历, 劫持索引的get, 性能不好 30 | ``` 31 | 32 | 33 | ### vm.$set()解决了什么问题? 避免滥用 34 | 35 | 在Vue.js里面只有data中已经存在的属性才会被Observe为响应式数据, 如果你是新增的属性是不会成为响应式数据, 因此Vue提供了一个api(vm.$set)来解决这个问题。 36 | 37 | ``` 38 | 39 | 40 | 41 | 42 | 43 | 44 | Vue Demo 45 | 46 | 47 | 48 |
49 | {{user.name}} 50 | {{user.age}} 51 | 52 |
53 | 71 | 72 | 73 | 74 | ``` 75 | 76 | ### 原理 77 | 78 | #### vm.$set()在new Vue()时候就被注入到Vue的原型上。 79 | 80 | 源码位置: vue/src/core/instance/index.js 81 | ``` 82 | import { initMixin } from './init' 83 | import { stateMixin } from './state' 84 | import { renderMixin } from './render' 85 | import { eventsMixin } from './events' 86 | import { lifecycleMixin } from './lifecycle' 87 | import { warn } from '../util/index' 88 | 89 | function Vue (options) { 90 | if (process.env.NODE_ENV !== 'production' && 91 | !(this instanceof Vue) 92 | ) { 93 | warn('Vue is a constructor and should be called with the `new` keyword') 94 | } 95 | this._init(options) 96 | } 97 | 98 | initMixin(Vue) 99 | // 给原型绑定代理属性$props, $data 100 | // 给Vue原型绑定三个实例方法: vm.$watch,vm.$set,vm.$delete 101 | stateMixin(Vue) 102 | // 给Vue原型绑定事件相关的实例方法: vm.$on, vm.$once ,vm.$off , vm.$emit 103 | eventsMixin(Vue) 104 | // 给Vue原型绑定生命周期相关的实例方法: vm.$forceUpdate, vm.destroy, 以及私有方法_update 105 | lifecycleMixin(Vue) 106 | // 给Vue原型绑定生命周期相关的实例方法: vm.$nextTick, 以及私有方法_render, 以及一堆工具方法 107 | renderMixin(Vue) 108 | 109 | export default Vue 110 | 111 | ``` 112 | 113 | #### stateMixin() 114 | 115 | ``` 116 | ... 117 | Vue.prototype.$set = set 118 | Vue.prototype.$delete = del 119 | ... 120 | ``` 121 | 122 | #### set() 123 | 124 | 源码位置: vue/src/core/observer/index.js 125 | 126 | ``` 127 | export function set (target: Array | Object, key: any, val: any): any { 128 | // 如果 set 函数的第一个参数是 undefined 或 null 或者是原始类型值,那么在非生产环境下会打印警告信息 129 | // 这个api本来就是给对象与数组使用的 130 | if (process.env.NODE_ENV !== 'production' && 131 | (isUndef(target) || isPrimitive(target)) 132 | ) { 133 | warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) 134 | } 135 | if (Array.isArray(target) && isValidArrayIndex(key)) { 136 | // 类似$vm.set(vm.$data.arr, 0, 3) 137 | // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误 138 | target.length = Math.max(target.length, key) 139 | // 利用数组的splice变异方法触发响应式, 这个前面讲过 140 | target.splice(key, 1, val) 141 | return val 142 | } 143 | // target为对象, key在target或者target.prototype上。 144 | // 同时必须不能在 Object.prototype 上 145 | // 直接修改即可, 有兴趣可以看issue: https://github.com/vuejs/vue/issues/6845 146 | if (key in target && !(key in Object.prototype)) { 147 | target[key] = val 148 | return val 149 | } 150 | // 以上都不成立, 即开始给target创建一个全新的属性 151 | // 获取Observer实例 152 | const ob = (target: any).__ob__ 153 | // Vue 实例对象拥有 _isVue 属性, 即不允许给Vue 实例对象添加属性 154 | // 也不允许Vue.set/$set 函数为根数据对象(vm.$data)添加属性 155 | if (target._isVue || (ob && ob.vmCount)) { 156 | process.env.NODE_ENV !== 'production' && warn( 157 | 'Avoid adding reactive properties to a Vue instance or its root $data ' + 158 | 'at runtime - declare it upfront in the data option.' 159 | ) 160 | return val 161 | } 162 | // target本身就不是响应式数据, 直接赋值 163 | if (!ob) { 164 | target[key] = val 165 | return val 166 | } 167 | // 进行响应式处理 168 | defineReactive(ob.value, key, val) 169 | ob.dep.notify() 170 | return val 171 | } 172 | 173 | ``` 174 | 175 | 工具函数 176 | ``` 177 | // 判断给定变量是否是未定义,当变量值为 null时,也会认为其是未定义 178 | export function isUndef (v: any): boolean %checks { 179 | return v === undefined || v === null 180 | } 181 | 182 | // 判断给定变量是否是原始类型值 183 | export function isPrimitive (value: any): boolean %checks { 184 | return ( 185 | typeof value === 'string' || 186 | typeof value === 'number' || 187 | // $flow-disable-line 188 | typeof value === 'symbol' || 189 | typeof value === 'boolean' 190 | ) 191 | } 192 | 193 | // 判断给定变量的值是否是有效的数组索引 194 | export function isValidArrayIndex (val: any): boolean { 195 | const n = parseFloat(String(val)) 196 | return n >= 0 && Math.floor(n) === n && isFinite(val) 197 | } 198 | ``` 199 | 关于(ob && ob.vmCount)。 200 | 201 | ``` 202 | export function observe (value: any, asRootData: ?boolean): Observer | void { 203 | // 省略... 204 | if (asRootData && ob) { 205 | // vue已经被Observer了,并且是根数据对象, vmCount才会++ 206 | ob.vmCount++ 207 | } 208 | return ob 209 | } 210 | ``` 211 | 212 | ##### 在初始化Vue的过程中有 213 | 214 | ``` 215 | export function initState (vm: Component) { 216 | vm._watchers = [] 217 | const opts = vm.$options 218 | if (opts.props) initProps(vm, opts.props) 219 | if (opts.methods) initMethods(vm, opts.methods) 220 | if (opts.data) { 221 | //opts.data为对象属性 222 | initData(vm) 223 | } else { 224 | observe(vm._data = {}, true /* asRootData */) 225 | } 226 | if (opts.computed) initComputed(vm, opts.computed) 227 | if (opts.watch && opts.watch !== nativeWatch) { 228 | initWatch(vm, opts.watch) 229 | } 230 | } 231 | ``` 232 | 233 | ##### initData(vm) 234 | ``` 235 | function initData (vm: Component) { 236 | let data = vm.$options.data 237 | data = vm._data = typeof data === 'function' 238 | ? getData(data, vm) 239 | : data || {} 240 | 241 | // 省略... 242 | 243 | // observe data 244 | observe(data, true /* asRootData */) 245 | } 246 | ``` 247 | -------------------------------------------------------------------------------- /docs/vue/Vue源码: 事件相关的实例方法.md: -------------------------------------------------------------------------------- 1 | ### 事件相关实例方法 2 | 3 | 源码位置: vue-dev/src/core/instance/events.js 4 | 5 | 与事件相关的方法有: 6 | 7 | ``` 8 | vm.$on, vm.$once, vm.$off, vm.$emit 9 | ``` 10 | 11 | #### initEvents() 12 | 13 | 这里其实主要是一个对象存储, key=eventName(事件名), value=[事件回调列表]。 14 | 15 | ``` 16 | export function initEvents (vm: Component) { 17 | // _events是一个对象 18 | // 格式为 {[event(事件名): [事件回调列表]]} 19 | vm._events = Object.create(null) 20 | vm._hasHookEvent = false 21 | // init parent attached events 22 | const listeners = vm.$options._parentListeners 23 | if (listeners) { 24 | updateComponentListeners(vm, listeners) 25 | } 26 | } 27 | ``` 28 | 29 | 至于initEvent()方法是在new Vue()时候, 会执行__init()方法。 30 | 31 | ``` 32 | export function initMixin (Vue: Class) { 33 | Vue.prototype._init = function (options?: Object) { 34 | ... 35 | // 执行各种初始化操作 36 | initLifecycle(vm) 37 | initEvents(vm) 38 | initRender(vm) 39 | callHook(vm, 'beforeCreate') 40 | initInjections(vm) // resolve injections before data/props 41 | initState(vm) 42 | initProvide(vm) // resolve provide after data/props 43 | callHook(vm, 'created') 44 | } 45 | } 46 | ``` 47 | 48 | #### vm.$on() 49 | 50 | 监听当前实例上的自定义事件。事件可以由vm.$emit触发。回调函数会接收所有传入事件触发函数的额外参数。 51 | 使用方法就参见官网[vm.$on()](https://cn.vuejs.org/v2/api/#vm-on)。 52 | 53 | 源码分析: 54 | 55 | ``` 56 | const hookRE = /^hook:/ 57 | Vue.prototype.$on = function (event: string | Array, fn: Function): Component { 58 | const vm: Component = this 59 | // 如果event是数组,循环数组, 每一项调用$on 60 | // 主要是使回调能注册到每项事件名指定的事件列表 61 | if (Array.isArray(event)) { 62 | for (let i = 0, l = event.length; i < l; i++) { 63 | vm.$on(event[i], fn) 64 | } 65 | } else { 66 | // 如果不是数组, 直接往事件列表添加回调 67 | // 如果事件名不存在默认初始化为空列表 68 | (vm._events[event] || (vm._events[event] = [])).push(fn) 69 | // optimize hook:event cost by using a boolean flag marked at registration 70 | // instead of a hash lookup 71 | if (hookRE.test(event)) { 72 | vm._hasHookEvent = true 73 | } 74 | } 75 | return vm 76 | } 77 | ``` 78 | 79 | #### vm.$off() 80 | 81 | 用来移除事件监听器, 具体使用方法见官网[vm.off()](https://cn.vuejs.org/v2/api/#vm-off)。 82 | 83 | 源码分析: 84 | 85 | ``` 86 | Vue.prototype.$off = function (event?: string | Array, fn?: Function): Component { 87 | const vm: Component = this 88 | // all 89 | // 么有参数时候默认移除所有的监听器 90 | // 通过重置_events对象为{} 91 | if (!arguments.length) { 92 | vm._events = Object.create(null) 93 | return vm 94 | } 95 | // array of events 96 | // 如果是数组, 遍历逐个移除对应的eventName的回调 97 | if (Array.isArray(event)) { 98 | for (let i = 0, l = event.length; i < l; i++) { 99 | vm.$off(event[i], fn) 100 | } 101 | return vm 102 | } 103 | // specific event 104 | // cbs: eventName对应的回调 105 | const cbs = vm._events[event] 106 | // 如果没有回调默认不处理直接返回 107 | if (!cbs) { 108 | return vm 109 | } 110 | // 如果没有指定具体的回调, 默认清空对应事件名注册下的所有回调函数 111 | if (!fn) { 112 | vm._events[event] = null 113 | return vm 114 | } 115 | // specific handler 116 | // 走到这里说明有fn, 从事件列表回调数组中遍历匹配, 匹配成功就删除 117 | let cb 118 | let i = cbs.length 119 | // 这里有个小技巧, 从后往前遍历如果匹配中删除监听器, 不会影响前面未遍历到的位置 120 | while (i--) { 121 | cb = cbs[i] 122 | // 如果列表中某一项与fn相同, 或者某一项的fn属性与fn一致, 说明匹配成功 123 | if (cb === fn || cb.fn === fn) { 124 | cbs.splice(i, 1) 125 | break 126 | } 127 | } 128 | return vm 129 | } 130 | 131 | ``` 132 | 133 | #### vm.$once() 134 | 135 | 监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。 136 | 见官网介绍[vm.$once()](https://cn.vuejs.org/v2/api/#vm-once)。 137 | 138 | 源码分析: 139 | 140 | ``` 141 | Vue.prototype.$once = function (event: string, fn: Function): Component { 142 | // fn:是用户指定的回调函数 143 | const vm: Component = this 144 | // on: 是我们拦截器函数 145 | function on () { 146 | // 执行回调时候通过$off()移除事件回调 147 | vm.$off(event, on) 148 | // 当事件触发时候执行指定的回调函数 149 | fn.apply(vm, arguments) 150 | } 151 | // 之所以需要on.fn = fn,是因为我们下面使用$on()时候 152 | // 往事件回调列表推入的是on函数不是用户指定的函数fn. 153 | // 这个时候使用$off()移除时候, 有一行代码 154 | /** 155 | * if (cb === fn || cb.fn === fn) { 156 | cbs.splice(i, 1) 157 | break 158 | } 159 | */ 160 | on.fn = fn 161 | // 通过$on()监听自定义事件 162 | vm.$on(event, on) 163 | return vm 164 | } 165 | ``` 166 | 167 | #### vm.$emit() 168 | 169 | 触发当前实例上的事件。附加参数都会传给监听器回调。 170 | 见官网使用[vm.$emit()](https://cn.vuejs.org/v2/api/#vm-emit)。 171 | 172 | 源码分析: 173 | 174 | ``` 175 | Vue.prototype.$emit = function (event: string): Component { 176 | const vm: Component = this 177 | if (process.env.NODE_ENV !== 'production') { 178 | const lowerCaseEvent = event.toLowerCase() 179 | if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { 180 | tip( 181 | `Event "${lowerCaseEvent}" is emitted in component ` + 182 | `${formatComponentName(vm)} but the handler is registered for "${event}". ` + 183 | `Note that HTML attributes are case-insensitive and you cannot use ` + 184 | `v-on to listen to camelCase events when using in-DOM templates. ` + 185 | `You should probably use "${hyphenate(event)}" instead of "${event}".` 186 | ) 187 | } 188 | } 189 | // 取出事件名对应的回调函数列表 190 | let cbs = vm._events[event] 191 | if (cbs) { 192 | cbs = cbs.length > 1 ? toArray(cbs) : cbs 193 | // args: 为除了第一个函数外所有参数组成的数组 194 | const args = toArray(arguments, 1) 195 | const info = `event handler for "${event}"` 196 | for (let i = 0, l = cbs.length; i < l; i++) { 197 | // cbs[i]: 回调函数 198 | invokeWithErrorHandling(cbs[i], vm, args, vm, info) 199 | } 200 | } 201 | return vm 202 | } 203 | ``` 204 | 205 | ``` 206 | export function invokeWithErrorHandling ( 207 | handler: Function, 208 | context: any, 209 | args: null | any[], 210 | vm: any, 211 | info: string 212 | ) { 213 | let res 214 | try { 215 | // 执行事件对应回调函数 216 | res = args ? handler.apply(context, args) : handler.call(context) 217 | if (res && !res._isVue && isPromise(res) && !res._handled) { 218 | // 处理Promise嵌套回调 219 | res.catch(e => handleError(e, vm, info + ` (Promise/async)`)) 220 | // issue #9511 221 | // avoid catch triggering multiple times when nested calls 222 | res._handled = true 223 | } 224 | } catch (e) { 225 | // 如果出错, 执行错误处理 226 | handleError(e, vm, info) 227 | } 228 | return res 229 | } 230 | ``` 231 | -------------------------------------------------------------------------------- /docs/vue/Vue源码: 关于vm.$watch内部原理.md: -------------------------------------------------------------------------------- 1 | ## vm.$watch()用法 2 | 3 | 关于vm.$watch()详细用法可以见[官网](https://cn.vuejs.org/v2/api/#vm-watch)。 4 | 5 | 大致用法如下: 6 | 7 | ``` 8 | 37 | ``` 38 | 39 | 40 | 41 | ![截图](https://user-gold-cdn.xitu.io/2019/4/23/16a4ae24fdd84027?w=1096&h=1224&f=png&s=172271) 42 | 43 | 可以看到data属性整个a对象被Observe, 只要被Observe就会有一个__ob__标示(即Observe实例), 可以看到__ob__里面有dep,前面讲过依赖(dep)都是存在Observe实例里面, subs存储的就是对应属性的依赖(Watcher)。 44 | 好了回到正文, vm.$watch()在源码内部如果实现的。 45 | 46 | ### 内部实现原理 47 | 48 | 49 | ``` 50 | // 判断是否是对象 51 | export function isPlainObject (obj: any): boolean { 52 | return _toString.call(obj) === '[object Object]' 53 | } 54 | 55 | ``` 56 | 57 | 源码位置: vue/src/core/instance/state.js 58 | 59 | ``` 60 | // $watch 方法允许我们观察数据对象的某个属性,当属性变化时执行回调 61 | // 接受三个参数: expOrFn(要观测的属性), cb, options(可选的配置对象) 62 | // cb即可以是一个回调函数, 也可以是一个纯对象(这个对象要包含handle属性。) 63 | // options: {deep, immediate}, deep指的是深度观测, immediate立即执行回掉 64 | // $watch()本质还是创建一个Watcher实例对象。 65 | 66 | Vue.prototype.$watch = function ( 67 | expOrFn: string | Function, 68 | cb: any, 69 | options?: Object 70 | ): Function { 71 | // vm指向当前Vue实例对象 72 | const vm: Component = this 73 | if (isPlainObject(cb)) { 74 | // 如果cb是一个纯对象 75 | return createWatcher(vm, expOrFn, cb, options) 76 | } 77 | // 获取options 78 | options = options || {} 79 | // 设置user: true, 标示这个是由用户自己创建的。 80 | options.user = true 81 | // 创建一个Watcher实例 82 | const watcher = new Watcher(vm, expOrFn, cb, options) 83 | if (options.immediate) { 84 | // 如果immediate为真, 马上执行一次回调。 85 | try { 86 | // 此时只有新值, 没有旧值, 在上面截图可以看到undefined。 87 | // 至于这个新值为什么通过watcher.value, 看下面我贴的代码 88 | cb.call(vm, watcher.value) 89 | } catch (error) { 90 | handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) 91 | } 92 | } 93 | // 返回一个函数,这个函数的执行会解除当前观察者对属性的观察 94 | return function unwatchFn () { 95 | // 执行teardown() 96 | watcher.teardown() 97 | } 98 | } 99 | ``` 100 | 101 | 102 | #### 关于watcher.js。 103 | 104 | 源码路径: vue/src/core/observer/watcher.js 105 | 106 | ``` 107 | export default class Watcher { 108 | vm: Component; 109 | expression: string; 110 | cb: Function; 111 | id: number; 112 | deep: boolean; 113 | user: boolean; 114 | lazy: boolean; 115 | sync: boolean; 116 | dirty: boolean; 117 | active: boolean; 118 | deps: Array; 119 | newDeps: Array; 120 | depIds: SimpleSet; 121 | newDepIds: SimpleSet; 122 | before: ?Function; 123 | getter: Function; 124 | value: any; 125 | 126 | constructor ( 127 | vm: Component, // 组件实例对象 128 | expOrFn: string | Function, // 要观察的表达式 129 | cb: Function, // 当观察的表达式值变化时候执行的回调 130 | options?: ?Object, // 给当前观察者对象的选项 131 | isRenderWatcher?: boolean // 标识该观察者实例是否是渲染函数的观察者 132 | ) { 133 | // 每一个观察者实例对象都有一个 vm 实例属性,该属性指明了这个观察者是属于哪一个组件的 134 | this.vm = vm 135 | if (isRenderWatcher) { 136 | // 只有在 mountComponent 函数中创建渲染函数观察者时这个参数为真 137 | // 组件实例的 _watcher 属性的值引用着该组件的渲染函数观察者 138 | vm._watcher = this 139 | } 140 | vm._watchers.push(this) 141 | // options 142 | // deep: 当前观察者实例对象是否是深度观测 143 | // 平时在使用 Vue 的 watch 选项或者 vm.$watch 函数去观测某个数据时, 144 | // 可以通过设置 deep 选项的值为 true 来深度观测该数据。 145 | // user: 用来标识当前观察者实例对象是 开发者定义的 还是 内部定义的 146 | // 无论是 Vue 的 watch 选项还是 vm.$watch 函数,他们的实现都是通过实例化 Watcher 类完成的 147 | // sync: 告诉观察者当数据变化时是否同步求值并执行回调 148 | // before: 可以理解为 Watcher 实例的钩子,当数据变化之后,触发更新之前, 149 | // 调用在创建渲染函数的观察者实例对象时传递的 before 选项。 150 | if (options) { 151 | this.deep = !!options.deep 152 | this.user = !!options.user 153 | this.lazy = !!options.lazy 154 | this.sync = !!options.sync 155 | this.before = options.before 156 | } else { 157 | this.deep = this.user = this.lazy = this.sync = false 158 | } 159 | // cb: 回调 160 | this.cb = cb 161 | this.id = ++uid // uid for batching 162 | this.active = true 163 | // 避免收集重复依赖,且移除无用依赖 164 | this.dirty = this.lazy // for lazy watchers 165 | this.deps = [] 166 | this.newDeps = [] 167 | this.depIds = new Set() 168 | this.newDepIds = new Set() 169 | this.expression = process.env.NODE_ENV !== 'production' 170 | ? expOrFn.toString() 171 | : '' 172 | // 检测了 expOrFn 的类型 173 | // this.getter 函数终将会是一个函数 174 | if (typeof expOrFn === 'function') { 175 | this.getter = expOrFn 176 | } else { 177 | this.getter = parsePath(expOrFn) 178 | if (!this.getter) { 179 | this.getter = noop 180 | process.env.NODE_ENV !== 'production' && warn( 181 | `Failed watching path: "${expOrFn}" ` + 182 | 'Watcher only accepts simple dot-delimited paths. ' + 183 | 'For full control, use a function instead.', 184 | vm 185 | ) 186 | } 187 | } 188 | // 求值 189 | this.value = this.lazy 190 | ? undefined 191 | : this.get() 192 | } 193 | 194 | /** 195 | * 求值: 收集依赖 196 | * 求值的目的有两个 197 | * 第一个是能够触发访问器属性的 get 拦截器函数 198 | * 第二个是能够获得被观察目标的值 199 | */ 200 | get () { 201 | // 推送当前Watcher实例到Dep.target 202 | pushTarget(this) 203 | let value 204 | // 缓存vm 205 | const vm = this.vm 206 | try { 207 | // 获取value 208 | value = this.getter.call(vm, vm) 209 | } catch (e) { 210 | if (this.user) { 211 | handleError(e, vm, `getter for watcher "${this.expression}"`) 212 | } else { 213 | throw e 214 | } 215 | } finally { 216 | // "touch" every property so they are all tracked as 217 | // dependencies for deep watching 218 | if (this.deep) { 219 | // 递归地读取被观察属性的所有子属性的值 220 | // 这样被观察属性的所有子属性都将会收集到观察者,从而达到深度观测的目的。 221 | traverse(value) 222 | } 223 | popTarget() 224 | this.cleanupDeps() 225 | } 226 | return value 227 | } 228 | 229 | /** 230 | * 记录自己都订阅过哪些Dep 231 | */ 232 | addDep (dep: Dep) { 233 | const id = dep.id 234 | // newDepIds: 避免在一次求值的过程中收集重复的依赖 235 | if (!this.newDepIds.has(id)) { 236 | this.newDepIds.add(id) // 记录当前watch订阅这个dep 237 | this.newDeps.push(dep) // 记录自己订阅了哪些dep 238 | if (!this.depIds.has(id)) { 239 | // 把自己订阅到dep 240 | dep.addSub(this) 241 | } 242 | } 243 | } 244 | 245 | /** 246 | * Clean up for dependency collection. 247 | */ 248 | cleanupDeps () { 249 | let i = this.deps.length 250 | while (i--) { 251 | const dep = this.deps[i] 252 | if (!this.newDepIds.has(dep.id)) { 253 | dep.removeSub(this) 254 | } 255 | } 256 | //newDepIds 属性和 newDeps 属性被清空 257 | // 并且在被清空之前把值分别赋给了 depIds 属性和 deps 属性 258 | // 这两个属性将会用在下一次求值时避免依赖的重复收集。 259 | let tmp = this.depIds 260 | this.depIds = this.newDepIds 261 | this.newDepIds = tmp 262 | this.newDepIds.clear() 263 | tmp = this.deps 264 | this.deps = this.newDeps 265 | this.newDeps = tmp 266 | this.newDeps.length = 0 267 | } 268 | 269 | /** 270 | * Subscriber interface. 271 | * Will be called when a dependency changes. 272 | */ 273 | update () { 274 | /* istanbul ignore else */ 275 | if (this.lazy) { 276 | this.dirty = true 277 | } else if (this.sync) { 278 | // 指定同步更新 279 | this.run() 280 | } else { 281 | // 异步更新队列 282 | queueWatcher(this) 283 | } 284 | } 285 | 286 | /** 287 | * Scheduler job interface. 288 | * Will be called by the scheduler. 289 | */ 290 | run () { 291 | if (this.active) { 292 | const value = this.get() 293 | // 对比新值 value 和旧值 this.value 是否相等 294 | // 是对象的话即使值不变(引用不变)也需要执行回调 295 | // 深度观测也要执行 296 | if ( 297 | value !== this.value || 298 | // Deep watchers and watchers on Object/Arrays should fire even 299 | // when the value is the same, because the value may 300 | // have mutated. 301 | isObject(value) || 302 | this.deep 303 | ) { 304 | // set new value 305 | const oldValue = this.value 306 | this.value = value 307 | if (this.user) { 308 | // 意味着这个观察者是开发者定义的,所谓开发者定义的是指那些通过 watch 选项或 $watch 函数定义的观察者 309 | try { 310 | this.cb.call(this.vm, value, oldValue) 311 | } catch (e) { 312 | // 回调函数在执行的过程中其行为是不可预知, 出现错误给出提示 313 | handleError(e, this.vm, `callback for watcher "${this.expression}"`) 314 | } 315 | } else { 316 | this.cb.call(this.vm, value, oldValue) 317 | } 318 | } 319 | } 320 | } 321 | 322 | /** 323 | * Evaluate the value of the watcher. 324 | * This only gets called for lazy watchers. 325 | */ 326 | evaluate () { 327 | this.value = this.get() 328 | this.dirty = false 329 | } 330 | 331 | /** 332 | * Depend on all deps collected by this watcher. 333 | */ 334 | depend () { 335 | let i = this.deps.length 336 | while (i--) { 337 | this.deps[i].depend() 338 | } 339 | } 340 | 341 | /** 342 | * 把Watcher实例从从当前正在观测的状态的依赖列表中移除 343 | */ 344 | teardown () { 345 | if (this.active) { 346 | // 该观察者是否激活状态 347 | if (!this.vm._isBeingDestroyed) { 348 | // _isBeingDestroyed一个标识,为真说明该组件实例已经被销毁了,为假说明该组件还没有被销毁 349 | // 将当前观察者实例从组件实例对象的 vm._watchers 数组中移除 350 | remove(this.vm._watchers, this) 351 | } 352 | // 当一个属性与一个观察者建立联系之后,属性的 Dep 实例对象会收集到该观察者对象 353 | let i = this.deps.length 354 | while (i--) { 355 | this.deps[i].removeSub(this) 356 | } 357 | // 非激活状态 358 | this.active = false 359 | } 360 | } 361 | } 362 | ``` 363 | 364 | 365 | 366 | ``` 367 | export const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/ 368 | const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`) 369 | // path为keypath(属性路径) 处理'a.b.c'(即vm.a.b.c) => a[b[c]] 370 | export function parsePath (path: string): any { 371 | if (bailRE.test(path)) { 372 | return 373 | } 374 | const segments = path.split('.') 375 | return function (obj) { 376 | for (let i = 0; i < segments.length; i++) { 377 | if (!obj) return 378 | obj = obj[segments[i]] 379 | } 380 | return obj 381 | } 382 | } 383 | 384 | ``` 385 | -------------------------------------------------------------------------------- /docs/vue/Vue源码: 关于对Array的数据侦听.md: -------------------------------------------------------------------------------- 1 | ## 摘要 2 | 3 | 我们都知道Vue的响应式是通过Object.defineProperty来进行数据劫持。但是那是针对Object类型可以实现, 如果是数组呢? 通过set/get方式是不行的。 4 | 5 | 但是Vue作者使用了一个方式来实现Array类型的监测: 拦截器。 6 | 7 | ### 核心思想 8 | 9 | 通过创建一个拦截器来覆盖数组本身的原型对象Array.prototype。 10 | 11 | 12 | ### 拦截器 13 | 14 | 通过查看Vue源码路径vue/src/core/observer/array.js。 15 | 16 | ``` 17 | /** 18 | * Vue对数组的变化侦测 19 | * 思想: 通过一个拦截器来覆盖Array.prototype。 20 | * 拦截器其实就是一个Object, 它的属性与Array.prototype一样。 只是对数组的变异方法进行了处理。 21 | */ 22 | 23 | function def (obj, key, val, enumerable) { 24 | Object.defineProperty(obj, key, { 25 | value: val, 26 | enumerable: !!enumerable, 27 | writable: true, 28 | configurable: true 29 | }) 30 | } 31 | 32 | // 数组原型对象 33 | const arrayProto = Array.prototype 34 | // 拦截器 35 | const arrayMethods = Object.create(arrayProto) 36 | 37 | // 变异数组方法:执行后会改变原始数组的方法 38 | const methodsToPatch = [ 39 | 'push', 40 | 'pop', 41 | 'shift', 42 | 'unshift', 43 | 'splice', 44 | 'sort', 45 | 'reverse' 46 | ] 47 | 48 | methodsToPatch.forEach(function (method) { 49 | // 缓存原始的数组原型上的方法 50 | const original = arrayProto[method] 51 | // 对每个数组编译方法进行处理(拦截) 52 | def(arrayMethods, method, function mutator (...args) { 53 | // 返回的value还是通过数组原型方法本身执行的结果 54 | const result = original.apply(this, args) 55 | // 每个value在被observer()时候都会打上一个__ob__属性 56 | const ob = this.__ob__ 57 | // 存储调用执行变异数组方法导致数组本身值改变的数组,主要指的是原始数组增加的那部分(需要重新Observer) 58 | let inserted 59 | switch (method) { 60 | case 'push': 61 | case 'unshift': 62 | inserted = args 63 | break 64 | case 'splice': 65 | inserted = args.slice(2) 66 | break 67 | } 68 | // 重新Observe新增加的数组元素 69 | if (inserted) ob.observeArray(inserted) 70 | // 发送变化通知 71 | ob.dep.notify() 72 | return result 73 | }) 74 | }) 75 | 76 | ``` 77 | 78 | ### 关于Vue什么时候对data属性进行Observer 79 | 80 | 如果熟悉Vue源码的童鞋应该很快能找到Vue的入口文件vue/src/core/instance/index.js。 81 | 82 | ``` 83 | function Vue (options) { 84 | if (process.env.NODE_ENV !== 'production' && 85 | !(this instanceof Vue) 86 | ) { 87 | warn('Vue is a constructor and should be called with the `new` keyword') 88 | } 89 | this._init(options) 90 | } 91 | 92 | initMixin(Vue) 93 | // 给原型绑定代理属性$props, $data 94 | // 给Vue原型绑定三个实例方法: vm.$watch,vm.$set,vm.$delete 95 | stateMixin(Vue) 96 | // 给Vue原型绑定事件相关的实例方法: vm.$on, vm.$once ,vm.$off , vm.$emit 97 | eventsMixin(Vue) 98 | // 给Vue原型绑定生命周期相关的实例方法: vm.$forceUpdate, vm.destroy, 以及私有方法_update 99 | lifecycleMixin(Vue) 100 | // 给Vue原型绑定生命周期相关的实例方法: vm.$nextTick, 以及私有方法_render, 以及一堆工具方法 101 | renderMixin(Vue) 102 | 103 | export default Vue 104 | ``` 105 | 106 | #### this.__init__() 107 | 108 | 源码路径: vue/src/core/instance/init.js。 109 | 110 | ``` 111 | 112 | export function initMixin (Vue: Class) { 113 | Vue.prototype._init = function (options?: Object) { 114 | // 当前实例 115 | const vm: Component = this 116 | // a uid 117 | // 实例唯一标识 118 | vm._uid = uid++ 119 | 120 | let startTag, endTag 121 | /* istanbul ignore if */ 122 | // 开发模式, 开启Vue性能检测和支持 performance.mark API 的浏览器上。 123 | if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 124 | startTag = `vue-perf-start:${vm._uid}` 125 | endTag = `vue-perf-end:${vm._uid}` 126 | // 处于组件初始化阶段开始打点 127 | mark(startTag) 128 | } 129 | 130 | // a flag to avoid this being observed 131 | // 标识为一个Vue实例 132 | vm._isVue = true 133 | // merge options 134 | // 把我们传入的optionsMerge到$options 135 | if (options && options._isComponent) { 136 | // optimize internal component instantiation 137 | // since dynamic options merging is pretty slow, and none of the 138 | // internal component options needs special treatment. 139 | initInternalComponent(vm, options) 140 | } else { 141 | vm.$options = mergeOptions( 142 | resolveConstructorOptions(vm.constructor), 143 | options || {}, 144 | vm 145 | ) 146 | } 147 | /* istanbul ignore else */ 148 | if (process.env.NODE_ENV !== 'production') { 149 | initProxy(vm) 150 | } else { 151 | vm._renderProxy = vm 152 | } 153 | // expose real self 154 | vm._self = vm 155 | // 初始化生命周期 156 | initLifecycle(vm) 157 | // 初始化事件中心 158 | initEvents(vm) 159 | initRender(vm) 160 | callHook(vm, 'beforeCreate') 161 | initInjections(vm) // resolve injections before data/props 162 | // 初始化State 163 | initState(vm) 164 | initProvide(vm) // resolve provide after data/props 165 | callHook(vm, 'created') 166 | 167 | /* istanbul ignore if */ 168 | if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 169 | vm._name = formatComponentName(vm, false) 170 | mark(endTag) 171 | measure(`vue ${vm._name} init`, startTag, endTag) 172 | } 173 | // 挂载 174 | if (vm.$options.el) { 175 | vm.$mount(vm.$options.el) 176 | } 177 | } 178 | } 179 | ``` 180 | 181 | #### initState() 182 | 183 | 源码路径:vue/src/core/instance/state.js。 184 | 185 | ``` 186 | export function initState (vm: Component) { 187 | vm._watchers = [] 188 | const opts = vm.$options 189 | if (opts.props) initProps(vm, opts.props) 190 | if (opts.methods) initMethods(vm, opts.methods) 191 | if (opts.data) { 192 | initData(vm) 193 | } else { 194 | observe(vm._data = {}, true /* asRootData */) 195 | } 196 | if (opts.computed) initComputed(vm, opts.computed) 197 | if (opts.watch && opts.watch !== nativeWatch) { 198 | initWatch(vm, opts.watch) 199 | } 200 | } 201 | 202 | ``` 203 | 204 | 这个时候你会发现observe出现了。 205 | 206 | #### observe 207 | 208 | 源码路径: vue/src/core/observer/index.js 209 | 210 | ``` 211 | export function observe (value: any, asRootData: ?boolean): Observer | void { 212 | if (!isObject(value) || value instanceof VNode) { 213 | return 214 | } 215 | let ob: Observer | void 216 | if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { 217 | // value已经是一个响应式数据就不再创建Observe实例, 避免重复侦听 218 | ob = value.__ob__ 219 | } else if ( 220 | shouldObserve && 221 | !isServerRendering() && 222 | (Array.isArray(value) || isPlainObject(value)) && 223 | Object.isExtensible(value) && 224 | !value._isVue 225 | ) { 226 | // 出现目标, 创建一个Observer实例 227 | ob = new Observer(value) 228 | } 229 | if (asRootData && ob) { 230 | ob.vmCount++ 231 | } 232 | return ob 233 | } 234 | ``` 235 | 236 | ### 使用拦截器的时机 237 | 238 | Vue的响应式系统中有个Observe类。源码路径:vue/src/core/observer/index.js。 239 | 240 | 241 | ``` 242 | // can we use __proto__? 243 | export const hasProto = '__proto__' in {} 244 | 245 | const arrayKeys = Object.getOwnPropertyNames(arrayMethods) 246 | 247 | function protoAugment (target, src: Object) { 248 | /* eslint-disable no-proto */ 249 | target.__proto__ = src 250 | /* eslint-enable no-proto */ 251 | } 252 | 253 | function copyAugment (target: Object, src: Object, keys: Array) { 254 | // target: 需要被Observe的对象 255 | // src: 数组代理原型对象 256 | // keys: const arrayKeys = Object.getOwnPropertyNames(arrayMethods) 257 | // keys: 数组代理原型对象上的几个编译方法名 258 | // const methodsToPatch = [ 259 | // 'push', 260 | // 'pop', 261 | // 'shift', 262 | // 'unshift', 263 | // 'splice', 264 | // 'sort', 265 | // 'reverse' 266 | // ] 267 | for (let i = 0, l = keys.length; i < l; i++) { 268 | const key = keys[i] 269 | def(target, key, src[key]) 270 | } 271 | } 272 | 273 | export class Observer { 274 | value: any; 275 | dep: Dep; 276 | vmCount: number; // number of vms that have this object as root $data 277 | 278 | constructor (value: any) { 279 | this.value = value 280 | // 281 | this.dep = new Dep() 282 | this.vmCount = 0 283 | def(value, '__ob__', this) 284 | // 如果是数组 285 | if (Array.isArray(value)) { 286 | if (hasProto) { 287 | // 如果支持__proto__属性(非标属性, 大多数浏览器支持): 直接把原型指向代理原型对象 288 | protoAugment(value, arrayMethods) 289 | } else { 290 | // 不支持就在数组实例上挂载被加工处理过的同名的变异方法(且不可枚举)来进行原型对象方法拦截 291 | // 当你访问一个对象的方法时候, 只有当自身不存在时候才会去原型对象上查找 292 | copyAugment(value, arrayMethods, arrayKeys) 293 | } 294 | this.observeArray(value) 295 | } else { 296 | this.walk(value) 297 | } 298 | } 299 | 300 | /** 301 | * Walk through all properties and convert them into 302 | * getter/setters. This method should only be called when 303 | * value type is Object. 304 | */ 305 | walk (obj: Object) { 306 | const keys = Object.keys(obj) 307 | for (let i = 0; i < keys.length; i++) { 308 | defineReactive(obj, keys[i]) 309 | } 310 | } 311 | 312 | /** 313 | * 遍历数组每一项来进行侦听变化,即每个元素执行一遍Observer() 314 | */ 315 | observeArray (items: Array) { 316 | for (let i = 0, l = items.length; i < l; i++) { 317 | observe(items[i]) 318 | } 319 | } 320 | } 321 | ``` 322 | 323 | ### 如何收集依赖 324 | 325 | Vue里面真正做数据响应式处理的是defineReactive()。 326 | defineReactive方法就是把对象的数据属性转为访问器属性, 即为数据属性设置get/set。 327 | 328 | ``` 329 | function dependArray (value: Array) { 330 | for (let e, i = 0, l = value.length; i < l; i++) { 331 | e = value[i] 332 | e && e.__ob__ && e.__ob__.dep.depend() 333 | if (Array.isArray(e)) { 334 | dependArray(e) 335 | } 336 | } 337 | } 338 | 339 | 340 | export function defineReactive ( 341 | obj: Object, 342 | key: string, 343 | val: any, 344 | customSetter?: ?Function, 345 | shallow?: boolean 346 | ) { 347 | // dep在访问器属性中闭包使用 348 | // 每一个数据字段都通过闭包引用着属于自己的 dep 常量 349 | // 每个字段的Dep对象都被用来收集那些属于对应字段的依赖。 350 | const dep = new Dep() 351 | 352 | // 获取该字段可能已有的属性描述对象 353 | const property = Object.getOwnPropertyDescriptor(obj, key) 354 | // 边界情况处理: 一个不可配置的属性是不能使用也没必要使用 Object.defineProperty 改变其属性定义的。 355 | if (property && property.configurable === false) { 356 | return 357 | } 358 | 359 | // 由于一个对象的属性很可能已经是一个访问器属性了,所以该属性很可能已经存在 get 或 set 方法 360 | // 如果接下来会使用 Object.defineProperty 函数重新定义属性的 setter/getter 361 | // 这会导致属性原有的 set 和 get 方法被覆盖,所以要将属性原有的 setter/getter 缓存 362 | const getter = property && property.get 363 | const setter = property && property.set 364 | // 边界情况处理 365 | if ((!getter || setter) && arguments.length === 2) { 366 | val = obj[key] 367 | } 368 | // 默认就是深度观测,引用子属性的__ob__ 369 | // 为Vue.set 或 Vue.delete 方法提供触发依赖。 370 | let childOb = !shallow && observe(val) 371 | Object.defineProperty(obj, key, { 372 | enumerable: true, 373 | configurable: true, 374 | get: function reactiveGetter () { 375 | // 如果 getter 存在那么直接调用该函数,并以该函数的返回值作为属性的值,保证属性的原有读取操作正常运作 376 | // 如果 getter 不存在则使用 val 作为属性的值 377 | const value = getter ? getter.call(obj) : val 378 | // Dep.target的值是在对Watch实例化时候赋值的 379 | if (Dep.target) { 380 | // 开始收集依赖到dep 381 | dep.depend() 382 | if (childOb) { 383 | childOb.dep.depend() 384 | if (Array.isArray(value)) { 385 | // 调用 dependArray 函数逐个触发数组每个元素的依赖收集 386 | dependArray(value) 387 | } 388 | } 389 | } 390 | // 正确地返回属性值。 391 | return value 392 | }, 393 | set: function reactiveSetter (newVal) { 394 | // 获取原来的值 395 | const value = getter ? getter.call(obj) : val 396 | /* eslint-disable no-self-compare */ 397 | // 比较新旧值是否相等, 考虑NaN情况 398 | if (newVal === value || (newVal !== newVal && value !== value)) { 399 | return 400 | } 401 | /* eslint-enable no-self-compare */ 402 | if (process.env.NODE_ENV !== 'production' && customSetter) { 403 | customSetter() 404 | } 405 | // #7981: for accessor properties without setter 406 | if (getter && !setter) return 407 | // 如果数据之前有setter, 那么应该继续使用该函数来设置属性的值 408 | if (setter) { 409 | setter.call(obj, newVal) 410 | } else { 411 | // 赋新值 412 | val = newVal 413 | } 414 | // 由于属性被设置了新的值,那么假如我们为属性设置的新值是一个数组或者纯对象, 415 | // 那么该数组或纯对象是未被观测的,所以需要对新值进行观测 416 | childOb = !shallow && observe(newVal) 417 | // 通知dep中的watcher更新 418 | dep.notify() 419 | } 420 | }) 421 | } 422 | 423 | ``` 424 | 425 | ### 存储数组依赖的列表 426 | 427 | 我们为什么需要把依赖存在Observer实例上。 428 | 即 429 | ``` 430 | export class Observer { 431 | constructor (value: any) { 432 | ... 433 | this.dep = new Dep() 434 | } 435 | } 436 | ``` 437 | 438 | 首先我们需要在getter里面访问到Observer实例 439 | ``` 440 | // 即上述的 441 | let childOb = !shallow && observe(val) 442 | ... 443 | if (childOb) { 444 | // 调用Observer实例上dep的depend()方法收集依赖 445 | childOb.dep.depend() 446 | if (Array.isArray(value)) { 447 | // 调用 dependArray 函数逐个触发数组每个元素的依赖收集 448 | dependArray(value) 449 | } 450 | } 451 | ``` 452 | 另外我们在前面提到的拦截器中要使用Observer实例。 453 | 454 | ``` 455 | methodsToPatch.forEach(function (method) { 456 | ... 457 | // this表示当前被操作的数据 458 | // 但是__ob__怎么来的? 459 | const ob = this.__ob__ 460 | ... 461 | // 重新Observe新增加的数组元素 462 | if (inserted) ob.observeArray(inserted) 463 | // 发送变化通知 464 | ob.dep.notify() 465 | ... 466 | }) 467 | ``` 468 | 469 | 思考上述的this.__ob__属性来自哪里? 470 | 471 | ``` 472 | export class Observer { 473 | constructor () { 474 | ... 475 | this.dep = new Dep() 476 | // 在vue上新增一个不可枚举的__ob__属性, 这个属性的值就是Observer实例 477 | // 因此我们就可以通过数组数据__ob__获取Observer实例 478 | // 进而获取__ob__上的dep 479 | def(value, '__ob__', this) 480 | ... 481 | } 482 | } 483 | ``` 484 | 485 | 牢记所有的属性一旦被侦测了都会被打上一个__ob__的标记, 即表示是响应式数据。 486 | 487 | #### 关于Array注意事项 488 | 489 | 由于 JavaScript 的限制,Vue 不能检测以下变动的数组: 490 | 491 | - 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue 492 | - 当你修改数组的长度时,例如:vm.items.length = newLength 493 | 494 | 495 | 496 | 解决方法见官网文档: 497 | 498 | [关于Array注意事项](https://cn.vuejs.org/v2/guide/list.html#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9) 499 | 500 | 501 | -------------------------------------------------------------------------------- /docs/vue/Vue源码: 构造函数入口.md: -------------------------------------------------------------------------------- 1 | ## 构造函数的入口 2 | 3 | 一步步找到Vue的构造函数入口。 4 | 5 | ### 执行npm run dev 6 | 7 | 通过查看package.json文件下的scripts命令。 8 | 9 | ``` 10 | "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev" 11 | ``` 12 | 13 | scripts/config.js为打开的对应配置文件, process.env.TARGET为web-full-dev。 14 | 在scripts/config.js找到对应的配置对象 15 | 16 | ``` 17 | const builds = { 18 | // Runtime+compiler development build (Browser) 19 | 'web-full-dev': { 20 | entry: resolve('web/entry-runtime-with-compiler.js'), 21 | dest: resolve('dist/vue.js'), 22 | format: 'umd', 23 | env: 'development', 24 | alias: { he: './entity-decoder' }, 25 | banner 26 | }, 27 | } 28 | ``` 29 | 当然主要生成配置对象是这段代码 30 | 31 | ``` 32 | function genConfig (name) { 33 | // opts为builds里面对应key的基础配置对象 34 | const opts = builds[name] 35 | // config是真正要返回的配置对象 36 | const config = { 37 | input: opts.entry, 38 | external: opts.external, 39 | plugins: [ 40 | flow(), 41 | alias(Object.assign({}, aliases, opts.alias)) 42 | ].concat(opts.plugins || []), 43 | output: { 44 | file: opts.dest, 45 | format: opts.format, 46 | banner: opts.banner, 47 | name: opts.moduleName || 'Vue' 48 | }, 49 | onwarn: (msg, warn) => { 50 | if (!/Circular/.test(msg)) { 51 | warn(msg) 52 | } 53 | } 54 | } 55 | 56 | // built-in vars 57 | const vars = { 58 | __WEEX__: !!opts.weex, 59 | __WEEX_VERSION__: weexVersion, 60 | __VERSION__: version 61 | } 62 | // feature flags 63 | Object.keys(featureFlags).forEach(key => { 64 | vars[`process.env.${key}`] = featureFlags[key] 65 | }) 66 | // build-specific env 67 | // 根据不同的process.env.NODE_ENV加载不同的打包后版本 68 | if (opts.env) { 69 | vars['process.env.NODE_ENV'] = JSON.stringify(opts.env) 70 | } 71 | config.plugins.push(replace(vars)) 72 | 73 | if (opts.transpile !== false) { 74 | config.plugins.push(buble()) 75 | } 76 | 77 | Object.defineProperty(config, '_name', { 78 | enumerable: false, 79 | value: name 80 | }) 81 | 82 | return config 83 | } 84 | 85 | if (process.env.TARGET) { 86 | module.exports = genConfig(process.env.TARGET) 87 | } else { 88 | exports.getBuild = genConfig 89 | exports.getAllBuilds = () => Object.keys(builds).map(genConfig) 90 | } 91 | 92 | ``` 93 | 94 | ### 找到打包入口文件 95 | 96 | 根据配置对象的entry字段: 97 | 98 | ``` 99 | entry: resolve('web/entry-runtime-with-compiler.js') 100 | 101 | ``` 102 | 103 | 以及resolve函数 104 | 105 | ``` 106 | const aliases = require('./alias') 107 | const resolve = p => { 108 | // web/ weex /server 109 | const base = p.split('/')[0] 110 | if (aliases[base]) { 111 | // 拼接完整的入口文件 112 | return path.resolve(aliases[base], p.slice(base.length + 1)) 113 | } else { 114 | return path.resolve(__dirname, '../', p) 115 | } 116 | } 117 | ``` 118 | aliases.js文件 119 | 120 | ``` 121 | const path = require('path') 122 | 123 | const resolve = p => path.resolve(__dirname, '../', p) 124 | 125 | module.exports = { 126 | vue: resolve('src/platforms/web/entry-runtime-with-compiler'), 127 | compiler: resolve('src/compiler'), 128 | core: resolve('src/core'), 129 | shared: resolve('src/shared'), 130 | web: resolve('src/platforms/web'), 131 | weex: resolve('src/platforms/weex'), 132 | server: resolve('src/server'), 133 | sfc: resolve('src/sfc') 134 | } 135 | ``` 136 | 137 | 找到真正的入口文件为: vue-dev/src/platforms/web/entry-runtime-with-compiler.js。 138 | 139 | 在entry-runtime-with-compiler.js文件中发现 140 | ``` 141 | import Vue from './runtime/index' 142 | ``` 143 | 其实这里主要做的是挂载$mount()方法, 可以看我之前写的文章[mount挂载函数](https://juejin.im/post/5c8531995188251bbf2edf82)。 144 | 145 | OK回到继续回到我们之前话题, 在vue-dev/src/platforms/web/runtime/index.js下发现这里还不是真正的Vue构造函数 146 | 147 | ``` 148 | import Vue from './instance/index' 149 | ``` 150 | 不过也马上接近了, 继续查找vue-dev/src/core/instance/index.js, 很明显这里才是真正的构造函数。 151 | 152 | ``` 153 | import { initMixin } from './init' 154 | import { stateMixin } from './state' 155 | import { renderMixin } from './render' 156 | import { eventsMixin } from './events' 157 | import { lifecycleMixin } from './lifecycle' 158 | import { warn } from '../util/index' 159 | 160 | // Vue构造函数 161 | function Vue (options) { 162 | // 提示必须使用new Vue() 163 | if (process.env.NODE_ENV !== 'production' && 164 | !(this instanceof Vue) 165 | ) { 166 | warn('Vue is a constructor and should be called with the `new` keyword') 167 | } 168 | // 执行初始化操作, 一般_前缀方法都是内部方法 169 | // __init()方法是initMixin里绑定的 170 | this._init(options) 171 | } 172 | 173 | // 在Vue原型上挂载方法 174 | initMixin(Vue) 175 | stateMixin(Vue) 176 | eventsMixin(Vue) 177 | lifecycleMixin(Vue) 178 | renderMixin(Vue) 179 | 180 | export default Vue 181 | ``` 182 | 183 | ### initMixin() 184 | 185 | ``` 186 | 187 | export function initMixin (Vue: Class) { 188 | Vue.prototype._init = function (options?: Object) { 189 | // 缓存this 190 | const vm: Component = this 191 | // a uid 192 | vm._uid = uid++ 193 | 194 | // 这里只要是开启config.performance进行性能调试时候一些组件埋点 195 | let startTag, endTag 196 | /* istanbul ignore if */ 197 | if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 198 | startTag = `vue-perf-start:${vm._uid}` 199 | endTag = `vue-perf-end:${vm._uid}` 200 | mark(startTag) 201 | } 202 | 203 | // a flag to avoid this being observed 204 | // 标识一个对象是 Vue 实例, 避免再次被observed 205 | vm._isVue = true 206 | // merge options 207 | // options是new Vue(options)配置对象 208 | // _isComponent是一个内部属性, 用于创建组件 209 | if (options && options._isComponent) { 210 | // optimize internal component instantiation 211 | // since dynamic options merging is pretty slow, and none of the 212 | // internal component options needs special treatment. 213 | initInternalComponent(vm, options) 214 | } else { 215 | // 定义实例属性$options: 用于当前 Vue 实例的初始化选项 216 | vm.$options = mergeOptions( 217 | resolveConstructorOptions(vm.constructor), 218 | options || {}, 219 | vm 220 | ) 221 | } 222 | /* istanbul ignore else */ 223 | if (process.env.NODE_ENV !== 'production') { 224 | initProxy(vm) 225 | } else { 226 | vm._renderProxy = vm 227 | } 228 | // expose real self 229 | // 定义一个内部属性_self 230 | vm._self = vm 231 | // 执行各种初始化操作 232 | initLifecycle(vm) 233 | initEvents(vm) 234 | initRender(vm) 235 | callHook(vm, 'beforeCreate') 236 | initInjections(vm) // resolve injections before data/props 237 | initState(vm) 238 | initProvide(vm) // resolve provide after data/props 239 | callHook(vm, 'created') 240 | 241 | /* istanbul ignore if */ 242 | if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 243 | vm._name = formatComponentName(vm, false) 244 | mark(endTag) 245 | measure(`vue ${vm._name} init`, startTag, endTag) 246 | } 247 | // 执行挂载操作 248 | if (vm.$options.el) { 249 | vm.$mount(vm.$options.el) 250 | } 251 | } 252 | } 253 | ``` 254 | -------------------------------------------------------------------------------- /docs/vue/Vue源码: 架构和目录设计.md: -------------------------------------------------------------------------------- 1 | ## 架构设计 2 | 3 | 屏幕快照 2019-05-02 下午7 13 45 4 | 5 | 6 | 7 | ## 目录结构 8 | 9 | ``` 10 | ├── scripts ------------------------------- 构建相关的脚本/配置文件 11 | │ ├── git-hooks ------------------------- 存放git钩子的目录 12 | │ ├── alias.js -------------------------- 别名配置 13 | │ ├── config.js ------------------------- 生成rollup配置的文件 14 | │ ├── build.js -------------------------- 对 config.js 中所有的rollup配置进行构建 15 | │ ├── ci.sh ----------------------------- 持续集成运行的脚本 16 | │ ├── release.sh ------------------------ 用于自动发布新版本的脚本 17 | ├── dist ---------------------------------- 构建后文件的输出目录 18 | ├── examples ------------------------------ 存放一些使用Vue开发的应用案例 19 | ├── flow ---------------------------------- 类型声明,使用开源项目 [Flow](https://flowtype.org/) 20 | ├── packages ------------------------------ 存放独立发布的包的目录 21 | ├── test ---------------------------------- 包含所有测试文件 22 | ├── src ----------------------------------- 源码 23 | │ ├── compiler -------------------------- 编译器代码的存放目录,将 template 编译为 render 函数 24 | │ ├── core ------------------------------ 存放通用的,与平台无关的代码 25 | │ │ ├── observer ---------------------- 响应系统,包含数据观测的核心代码 26 | │ │ ├── vdom -------------------------- 包含虚拟DOM创建(creation)和打补丁(patching)的代码 27 | │ │ ├── instance ---------------------- 包含Vue构造函数设计相关的代码 28 | │ │ ├── global-api -------------------- 包含给Vue构造函数挂载全局方法(静态方法)或属性的代码 29 | │ │ ├── components -------------------- 包含抽象出来的通用组件 30 | │ ├── server ---------------------------- 包含服务端渲染(server-side rendering)的相关代码 31 | │ ├── platforms ------------------------- 包含平台特有的相关代码,不同平台的不同构建的入口文件也在这里 32 | │ │ ├── web --------------------------- web平台 33 | │ │ │ ├── entry-runtime.js ---------- 运行时构建的入口,不包含模板(template)到render函数的编译器,所以不支持 `template` 选项,我们使用vue默认导出的就是这个运行时的版本。大家使用的时候要注意 34 | │ │ │ ├── entry-runtime-with-compiler.js -- 独立构建版本的入口,它在 entry-runtime 的基础上添加了模板(template)到render函数的编译器 35 | │ │ │ ├── entry-compiler.js --------- vue-template-compiler 包的入口文件 36 | │ │ │ ├── entry-server-renderer.js -- vue-server-renderer 包的入口文件 37 | │ │ │ ├── entry-server-basic-renderer.js -- 输出 packages/vue-server-renderer/basic.js 文件 38 | │ │ ├── weex -------------------------- 混合应用 39 | │ ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包 40 | │ ├── shared ---------------------------- 包含整个代码库通用的代码 41 | ├── package.json -------------------------- 不解释 42 | ├── yarn.lock ----------------------------- yarn 锁定文件 43 | ├── .editorconfig ------------------------- 针对编辑器的编码风格配置文件 44 | ├── .flowconfig --------------------------- flow 的配置文件 45 | ├── .babelrc ------------------------------ babel 配置文件 46 | ├── .eslintrc ----------------------------- eslint 配置文件 47 | ├── .eslintignore ------------------------- eslint 忽略配置 48 | ├── .gitignore ---------------------------- git 忽略配置 49 | 50 | ``` 51 | 52 | ## Vue.js构建版本 53 | 54 | 55 | ``` 56 | 完整版: 构建后文件包括编译器+运行时 57 | 编译器: 负责把模板字符串变异为JS的Render函数 58 | 运行时: 负责创建Vue.js实例, 渲染视图, 使用虚拟DOM算法重新渲染 59 | UMD: 支持通过script标签在浏览器引入 60 | CJS: 用来支持一些低版本打包工具, 因为它们package.json文件的main字段只包含运行时的CJS版本 61 | ESM: 用来支持现代打包工具, 这些打包工具package.json的module字段只包含运行时候的ESM版本 62 | ``` 63 | 64 | 屏幕快照 2019-05-02 下午5 34 44 65 | 66 | 67 | ### 什么时候我们需要使用编译器? 68 | 69 | 编译器: 把template变异为Render函数。 70 | 71 | ``` 72 | // 用到了template就需要编译器 73 | new Vue({ 74 | template: '
' 75 | }) 76 | 77 | // 如果本身就是Render函数不需要编译器 78 | new Vue({ 79 | render (h) { 80 | return h('div', this.hi) 81 | } 82 | }) 83 | ``` 84 | 85 | 我们如果使用vue-loader, 那么*.vue文件模板会在构建时候预编译成JS, 所以打包完成的文件实际上不需要编译器的, 只需要引入运行时版本(体积小)即可。 86 | 87 | 如果确实需要使用完整版只需要在打包工具中配置一个别名。 88 | 89 | ``` 90 | // webpack 91 | 92 | resolve: { 93 | alias: { 94 | 'vue$': 'vue/dist/vue.esm.js', 95 | } 96 | }, 97 | ``` 98 | 99 | ### 关于开发环境与生产环境 100 | 101 | 屏幕快照 2019-05-02 下午6 30 40 102 | 103 | 104 | 至于采用什么版本 105 | 106 | - UMD版本 107 | 开发环境与生产环境的模式都是硬编码, 开发环境使用未压缩的, 生产环境使用压缩的。 108 | 109 | - CJS/ESM 110 | 111 | 这两个版本主要用于打包工具, 因此不提供压缩版本, 需要自行将其压缩。 112 | 此外, 这两个版本都保留原始的process.env.NODE_ENV监测, 根据其值来决定选择什么模式。 113 | 所以我们可以在打包工具中配置这些环境变量。 114 | 115 | 见源码位置: vue-dev/scripts/config.js 116 | 117 | ``` 118 | 119 | const builds = { 120 | // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify 121 | 'web-runtime-cjs-dev': { 122 | entry: resolve('web/entry-runtime.js'), 123 | dest: resolve('dist/vue.runtime.common.dev.js'), 124 | format: 'cjs', 125 | env: 'development', 126 | banner 127 | }, 128 | 'web-runtime-cjs-prod': { 129 | entry: resolve('web/entry-runtime.js'), 130 | dest: resolve('dist/vue.runtime.common.prod.js'), 131 | format: 'cjs', 132 | env: 'production', 133 | banner 134 | }, 135 | // Runtime+compiler CommonJS build (CommonJS) 136 | 'web-full-cjs-dev': { 137 | entry: resolve('web/entry-runtime-with-compiler.js'), 138 | dest: resolve('dist/vue.common.dev.js'), 139 | format: 'cjs', 140 | env: 'development', 141 | alias: { he: './entity-decoder' }, 142 | banner 143 | }, 144 | 'web-full-cjs-prod': { 145 | entry: resolve('web/entry-runtime-with-compiler.js'), 146 | dest: resolve('dist/vue.common.prod.js'), 147 | format: 'cjs', 148 | env: 'production', 149 | alias: { he: './entity-decoder' }, 150 | banner 151 | }, 152 | // Runtime only ES modules build (for bundlers) 153 | 'web-runtime-esm': { 154 | entry: resolve('web/entry-runtime.js'), 155 | dest: resolve('dist/vue.runtime.esm.js'), 156 | format: 'es', 157 | banner 158 | }, 159 | // Runtime+compiler ES modules build (for bundlers) 160 | 'web-full-esm': { 161 | entry: resolve('web/entry-runtime-with-compiler.js'), 162 | dest: resolve('dist/vue.esm.js'), 163 | format: 'es', 164 | alias: { he: './entity-decoder' }, 165 | banner 166 | }, 167 | // Runtime+compiler ES modules build (for direct import in browser) 168 | 'web-full-esm-browser-dev': { 169 | entry: resolve('web/entry-runtime-with-compiler.js'), 170 | dest: resolve('dist/vue.esm.browser.js'), 171 | format: 'es', 172 | transpile: false, 173 | env: 'development', 174 | alias: { he: './entity-decoder' }, 175 | banner 176 | }, 177 | // Runtime+compiler ES modules build (for direct import in browser) 178 | 'web-full-esm-browser-prod': { 179 | entry: resolve('web/entry-runtime-with-compiler.js'), 180 | dest: resolve('dist/vue.esm.browser.min.js'), 181 | format: 'es', 182 | transpile: false, 183 | env: 'production', 184 | alias: { he: './entity-decoder' }, 185 | banner 186 | }, 187 | // runtime-only build (Browser) 188 | 'web-runtime-dev': { 189 | entry: resolve('web/entry-runtime.js'), 190 | dest: resolve('dist/vue.runtime.js'), 191 | format: 'umd', 192 | env: 'development', 193 | banner 194 | }, 195 | // runtime-only production build (Browser) 196 | 'web-runtime-prod': { 197 | entry: resolve('web/entry-runtime.js'), 198 | dest: resolve('dist/vue.runtime.min.js'), 199 | format: 'umd', 200 | env: 'production', 201 | banner 202 | }, 203 | // Runtime+compiler development build (Browser) 204 | 'web-full-dev': { 205 | entry: resolve('web/entry-runtime-with-compiler.js'), 206 | dest: resolve('dist/vue.js'), 207 | format: 'umd', 208 | env: 'development', 209 | alias: { he: './entity-decoder' }, 210 | banner 211 | }, 212 | // Runtime+compiler production build (Browser) 213 | 'web-full-prod': { 214 | entry: resolve('web/entry-runtime-with-compiler.js'), 215 | dest: resolve('dist/vue.min.js'), 216 | format: 'umd', 217 | env: 'production', 218 | alias: { he: './entity-decoder' }, 219 | banner 220 | }, 221 | // Web compiler (CommonJS). 222 | 'web-compiler': { 223 | entry: resolve('web/entry-compiler.js'), 224 | dest: resolve('packages/vue-template-compiler/build.js'), 225 | format: 'cjs', 226 | external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies) 227 | }, 228 | // Web compiler (UMD for in-browser use). 229 | 'web-compiler-browser': { 230 | entry: resolve('web/entry-compiler.js'), 231 | dest: resolve('packages/vue-template-compiler/browser.js'), 232 | format: 'umd', 233 | env: 'development', 234 | moduleName: 'VueTemplateCompiler', 235 | plugins: [node(), cjs()] 236 | }, 237 | // Web server renderer (CommonJS). 238 | 'web-server-renderer-dev': { 239 | entry: resolve('web/entry-server-renderer.js'), 240 | dest: resolve('packages/vue-server-renderer/build.dev.js'), 241 | format: 'cjs', 242 | env: 'development', 243 | external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies) 244 | }, 245 | 'web-server-renderer-prod': { 246 | entry: resolve('web/entry-server-renderer.js'), 247 | dest: resolve('packages/vue-server-renderer/build.prod.js'), 248 | format: 'cjs', 249 | env: 'production', 250 | external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies) 251 | }, 252 | 'web-server-renderer-basic': { 253 | entry: resolve('web/entry-server-basic-renderer.js'), 254 | dest: resolve('packages/vue-server-renderer/basic.js'), 255 | format: 'umd', 256 | env: 'development', 257 | moduleName: 'renderVueComponentToString', 258 | plugins: [node(), cjs()] 259 | }, 260 | 'web-server-renderer-webpack-server-plugin': { 261 | entry: resolve('server/webpack-plugin/server.js'), 262 | dest: resolve('packages/vue-server-renderer/server-plugin.js'), 263 | format: 'cjs', 264 | external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies) 265 | }, 266 | 'web-server-renderer-webpack-client-plugin': { 267 | entry: resolve('server/webpack-plugin/client.js'), 268 | dest: resolve('packages/vue-server-renderer/client-plugin.js'), 269 | format: 'cjs', 270 | external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies) 271 | }, 272 | // Weex runtime factory 273 | 'weex-factory': { 274 | weex: true, 275 | entry: resolve('weex/entry-runtime-factory.js'), 276 | dest: resolve('packages/weex-vue-framework/factory.js'), 277 | format: 'cjs', 278 | plugins: [weexFactoryPlugin] 279 | }, 280 | // Weex runtime framework (CommonJS). 281 | 'weex-framework': { 282 | weex: true, 283 | entry: resolve('weex/entry-framework.js'), 284 | dest: resolve('packages/weex-vue-framework/index.js'), 285 | format: 'cjs' 286 | }, 287 | // Weex compiler (CommonJS). Used by Weex's Webpack loader. 288 | 'weex-compiler': { 289 | weex: true, 290 | entry: resolve('weex/entry-compiler.js'), 291 | dest: resolve('packages/weex-template-compiler/build.js'), 292 | format: 'cjs', 293 | external: Object.keys(require('../packages/weex-template-compiler/package.json').dependencies) 294 | } 295 | } 296 | 297 | 298 | // 生成配置对象 299 | function genConfig (name) { 300 | const opts = builds[name] 301 | const config = { 302 | input: opts.entry, 303 | external: opts.external, 304 | plugins: [ 305 | flow(), 306 | alias(Object.assign({}, aliases, opts.alias)) 307 | ].concat(opts.plugins || []), 308 | output: { 309 | file: opts.dest, 310 | format: opts.format, 311 | banner: opts.banner, 312 | name: opts.moduleName || 'Vue' 313 | }, 314 | onwarn: (msg, warn) => { 315 | if (!/Circular/.test(msg)) { 316 | warn(msg) 317 | } 318 | } 319 | } 320 | 321 | // built-in vars 322 | const vars = { 323 | __WEEX__: !!opts.weex, 324 | __WEEX_VERSION__: weexVersion, 325 | __VERSION__: version 326 | } 327 | // feature flags 328 | Object.keys(featureFlags).forEach(key => { 329 | vars[`process.env.${key}`] = featureFlags[key] 330 | }) 331 | // build-specific env 332 | // 加载开发环境变量 333 | if (opts.env) { 334 | vars['process.env.NODE_ENV'] = JSON.stringify(opts.env) 335 | } 336 | config.plugins.push(replace(vars)) 337 | 338 | if (opts.transpile !== false) { 339 | config.plugins.push(buble()) 340 | } 341 | 342 | Object.defineProperty(config, '_name', { 343 | enumerable: false, 344 | value: name 345 | }) 346 | 347 | return config 348 | } 349 | ``` 350 | 351 | 在webpack中配置环境变量 352 | 353 | ``` 354 | var webpack = require('webpack'); 355 | 356 | module.exports = { 357 | ..., 358 | 359 | plugins: [ 360 | // 配置全局变量的插件 361 | new webpack.DefinePlugin({ 362 | 'NODE_ENV': JSON.stringify('production') 363 | }) 364 | ] 365 | }; 366 | 367 | ``` 368 | 369 | 370 | 371 | -------------------------------------------------------------------------------- /docs/webpack/README.md: -------------------------------------------------------------------------------- 1 | - [一个合格的Webpack4配置工程师素养](https://github.com/NuoHui/fe-note/blob/master/docs/webpack/%E4%B8%80%E4%B8%AA%E5%90%88%E6%A0%BC%E7%9A%84Webpack4%E9%85%8D%E7%BD%AE%E5%B7%A5%E7%A8%8B%E5%B8%88%E7%B4%A0%E5%85%BB.md) 2 | -------------------------------------------------------------------------------- /docs/yarn/调试.md: -------------------------------------------------------------------------------- 1 | ## yarn link 2 | 3 | link 到全局,这样才能本地测试,就像使用正式发布的 npm 包一样调用。直接在当前目录下执行: 4 | 5 | 6 | ```shell 7 | › yarn link 8 | yarn link v1.22.17 9 | success Registered "docx". 10 | info You can now run `yarn link "docx"` in the projects where you want to use this package and it will be used instead. 11 | ``` 12 | 13 | 完成 link 后,yarn 会创建可执行命令到对应路径下,具体的路径可通过 `yarn global bin` 来查看, 14 | 15 | ```shell 16 | › yarn global bin 17 | /usr/local/bin 18 | ``` 19 | 我们也可以通过命名直接看。 20 | ```shell 21 | ll /usr/local/bin | grep docx 22 | 23 | lrwxr-xr-x 1 xx admin 55B Dec 4 15:26 docx -> ../../../Users/xx/.config/yarn/link/docx/bin/docx 24 | ``` 25 | 26 | 我们如果直接执行node shellbang会提示没有权限。 27 | 28 | ```shell 29 | yarn dev 30 | yarn run v1.22.17 31 | $ docx 32 | /bin/sh: /usr/local/bin/docx: Permission denied 33 | error Command failed with exit code 126. 34 | info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. 35 | ``` 36 | 真实原因是被 link 的入口文件其属性为非可执行,请看: 37 | 38 | ```shell 39 | ll /usr/local/bin | grep docx 40 | lrwxr-xr-x 1 xx admin 55B Dec 4 15:26 docx -> ../../../Users/xx/.config/yarn/link/docx/bin/docx 41 | 42 | ll /Users/xuyizong/.config/yarn/link/docx/bin/docx 43 | -rw-r--r--@ 1 xx staff 130B Dec 4 15:08 /Users/xx/.config/yarn/link/docx/bin/docx 44 | 45 | ``` 46 | 这里生成到 `/usr/local/bin` 下的软链是 x 可执行的,但软链对应的实际文件,也就是项目中作为 bin 命令入口的 index.js,其属性不是 `x`。 47 | 48 | 解决办法是将其转一下: 49 | ```shell 50 | $ sudo chmod 0777 /Users/xuyizong/.config/yarn/link/docx/bin/docx 51 | ``` 52 | 再次查看其文件属性已经变化 : 53 | ```shell 54 | ll /Users/xuyizong/.config/yarn/link/docx/bin/docx 55 | -rwxrwxrwx@ 1 xuyizong staff 130B Dec 4 15:08 /Users/xuyizong/.config/yarn/link/docx/bin/docx 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /images/readme.md: -------------------------------------------------------------------------------- 1 | # 图片资源 2 | -------------------------------------------------------------------------------- /pdf/03-复杂度.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/03-复杂度.pdf -------------------------------------------------------------------------------- /pdf/04-动态数组.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/04-动态数组.pdf -------------------------------------------------------------------------------- /pdf/05-链表.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/05-链表.pdf -------------------------------------------------------------------------------- /pdf/06-栈.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/06-栈.pdf -------------------------------------------------------------------------------- /pdf/07-队列.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/07-队列.pdf -------------------------------------------------------------------------------- /pdf/08-二叉树.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/08-二叉树.pdf -------------------------------------------------------------------------------- /pdf/09-二叉搜索树.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/09-二叉搜索树.pdf -------------------------------------------------------------------------------- /pdf/10-平衡二叉搜索树.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/10-平衡二叉搜索树.pdf -------------------------------------------------------------------------------- /pdf/11-AVL树.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/11-AVL树.pdf -------------------------------------------------------------------------------- /pdf/12-B树.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/12-B树.pdf -------------------------------------------------------------------------------- /pdf/13-红黑树.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/13-红黑树.pdf -------------------------------------------------------------------------------- /pdf/14-集合.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/14-集合.pdf -------------------------------------------------------------------------------- /pdf/15-映射.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/15-映射.pdf -------------------------------------------------------------------------------- /pdf/16-哈希表.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/16-哈希表.pdf -------------------------------------------------------------------------------- /pdf/17-二叉堆.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/17-二叉堆.pdf -------------------------------------------------------------------------------- /pdf/18-优先级队列.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/18-优先级队列.pdf -------------------------------------------------------------------------------- /pdf/19-哈夫曼树.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/19-哈夫曼树.pdf -------------------------------------------------------------------------------- /pdf/20-Trie.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/20-Trie.pdf -------------------------------------------------------------------------------- /pdf/21-总结.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/21-总结.pdf -------------------------------------------------------------------------------- /pdf/readme.md: -------------------------------------------------------------------------------- 1 | # 书籍笔记 2 | -------------------------------------------------------------------------------- /pdf/趣学算法 01.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NuoHui/fe-note/c212689953baa051ba1de8f6fe1cdf26e53e9715/pdf/趣学算法 01.pdf -------------------------------------------------------------------------------- /utils/readme.md: -------------------------------------------------------------------------------- 1 | ## utils 2 | --------------------------------------------------------------------------------