├── JavaScript之原型链.md ├── Koa上下文Context的数据劫持.md ├── Promise ├── README.md └── myPromise.js ├── README.md ├── React源码解析 之 Fiber的渲染(1).md ├── React源码解析 之Fiber的渲染(2)beginWork.md ├── React源码解析之 Fiber结构的创建.md ├── 一次弄懂Event Loop(彻底解决此类面试问题).md ├── 从源码上理解express中间件.md ├── 从零开始React服务器渲染(SSR)同构😏(基于Koa).md ├── 原来正则表达式我记得这么少.md ├── 如何创建一个可靠稳定的Web服务器.md ├── 打破思维桎梏:探索业务和技术的交汇,开拓个人职业道路 ├── 浏览器地址栏里输入URL后的全过程.md ├── 浏览器渲染原理(处理重排和重绘).md └── 骚年,koa和webpack了解一下.md /JavaScript之原型链.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | > 距离上次写文章,时隔一年。此次的目的,整理文章,强化记忆,为以后巩固留下笔记。 4 | 5 | ## 构造函数 6 | 7 | 构造函数与其他函数的唯一区别,就在于他们调用的方式不同。 8 | 9 | 任何函数只要通过`new`操作符来调用,那他就可以称之为**构造函数**。 10 | 11 | 下面来创建一个构造函数: 12 | 13 | ```js 14 | 15 | function Animal() { 16 | } 17 | 18 | var animal = new Animal(); 19 | 20 | console.log(animal.name); // undefined 21 | 22 | ``` 23 | 24 | 上述代码中,我们通过`new`操作符创建了一个实例对象animal。 25 | 26 | ```js 27 | Animal.prototype.name = 'jerry'; 28 | 29 | var animal1 = new Animal(); 30 | var animal2 = new Animal(); 31 | console.log(animal1.name,animal2.name);// jerry,jerry 32 | ``` 33 | ## 原型 34 | 35 | 什么是原型? 36 | 37 | 我们创建的每一个函数,都有一个`prototype`属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。 38 | 39 | 简单来说,就是通过构造函数而创建的那个对象实例的`原型对象`。 40 | 41 | 42 | ![](https://user-gold-cdn.xitu.io/2020/2/23/17070eb7c472f4d4?w=1000&h=434&f=png&s=290082) 43 | 44 | ## __ proto __ 45 | 46 | ```js 47 | 48 | Animal.prototype.name = 'jerry'; 49 | 50 | console.log(animal.name); // 'jerry'; 51 | 52 | ``` 53 | 上述代码中,我们对`prototype`进行赋值,打印`animal`得到`jerry`。 54 | 55 | 那么`animal`和`Animal`是什么关系呢? 56 | 57 | ```js 58 | console.log(animal.__proto__ === Animal.prototype)// true 59 | ``` 60 | 从而我们可以得知下图关系 61 | 62 | 63 | ![](https://user-gold-cdn.xitu.io/2020/2/23/17070f23099965de?w=1000&h=440&f=png&s=311110) 64 | 65 | 为什么我们没有给animal.name 进行赋值,也能得到值呢? 66 | 67 | ```js 68 | 69 | Animal.prototype.name = 'jerry'; 70 | 71 | animal.name = 'tom'; 72 | 73 | console.log(animal.name); // 'tom'; 74 | 75 | delete animal.name; 76 | 77 | console.log(animal.name);//jerry; 78 | 79 | ``` 80 | 81 | 当我们给`animal.name`进行赋值,得到tom,当我们删除了`animal.name`,得到了jerry。 82 | 83 | 它们之间是不是还含有更深的关联关系呢。 84 | 85 | ```js 86 | 87 | Animal.prototype.name = 'jerry'; 88 | Object.prototype.name = 'person'; 89 | animal.name = 'tom'; 90 | 91 | console.log(animal.name); // 'tom'; 92 | 93 | delete animal.name; 94 | 95 | console.log(animal.name);//jerry; 96 | 97 | delete Animal.prototype.name; 98 | 99 | console.log(animal.name);//person; 100 | 101 | ``` 102 | 103 | ## 原型的原型 104 | 105 | ```js 106 | console.log(animal.__proto__.__proto__ === Object.prototype);//true 107 | ``` 108 | 由此我们可以得知为什么当没有对animal进行赋值时,同样可以获取到值。 109 | 这里通过一层一层的隐式原型去查找`animal.name`返回,否则返回`undefined`。 110 | 111 | ![](https://user-gold-cdn.xitu.io/2020/2/23/1707105ddb7989fe?w=1000&h=676&f=png&s=968140) 112 | 113 | ## constructor 114 | 115 | 现在我们已经知道Animal的原型是Animal的原型对象,那我们可以从原型对像中得到其构造函数吗? 116 | 117 | ```js 118 | 119 | console.log(Animal.prototype.constructor === Animal);//true 120 | 121 | console.log(Object.prototype.constructor === Object) //true 122 | 123 | ``` 124 | 由于构造函数都可以通过new得到多个实例对象,所以只能通过`constructor`得到构造函数,无法获取其他实例对象,因此我们来补充其关系图。 125 | 126 | ![](https://user-gold-cdn.xitu.io/2020/2/23/170710b200930544?w=1000&h=684&f=png&s=644692) 127 | 128 | ## 原型链 129 | 130 | 此时我们已经知道了Object的原型是Object.prototype的原型对象,那么,Object.prototype的隐式原型__proto__是什么? 131 | ```js 132 | Object.prototype.__proto__ === null //true 133 | ``` 134 | 现在我们已经可以总结出原型链的具体关系了。 135 | 136 | ![](https://user-gold-cdn.xitu.io/2020/2/23/170711108f36461e?w=1000&h=892&f=png&s=1363113) 137 | 138 | 上图的红色线条所关联的链路就是**原型链** 139 | 140 | ## 相关资料 141 | 《JavaScript高级程序设计》 142 | 143 | [《JavaScript深入之从原型到原型链 144 | 》](https://github.com/mqyqingfeng/Blog/issues/2) 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /Koa上下文Context的数据劫持.md: -------------------------------------------------------------------------------- 1 | 这几天我一直在思索,关于`Koa`我可以分享什么? 2 | 分享`Koa`中间件实现原理? 3 | 上一篇的《[从源码上理解express中间件](https://juejin.im/post/5c10b5176fb9a049af6d1e1e)》已经解释的比较完善了,`Koa`中间件实现也比较相似,只不过,是把中间件函数抽离出来为[`koa-compose`](https://github.com/koajs/compose/blob/master/index.js),循环遍历的时候将函数外边套了`tryCatch`,里面加了`Promise.resolve(fn)`而已。 4 | 5 | 因此,这篇文章主要分享下Koa的数据劫持。 6 | 7 | 如: 8 | 以下访问器和 Request 别名等效。 9 | ```javascript 10 | 11 | ctx.header 12 | ctx.headers 13 | ctx.method 14 | ctx.method= 15 | ctx.url 16 | ctx.url= 17 | ctx.originalUrl 18 | ctx.origin 19 | ctx.href 20 | ctx.path 21 | ctx.path= 22 | ctx.query 23 | ctx.query= 24 | ctx.querystring 25 | ctx.querystring= 26 | ctx.host 27 | ctx.hostname 28 | ctx.fresh 29 | ctx.stale 30 | ctx.socket 31 | ctx.protocol 32 | ctx.secure 33 | ctx.ip 34 | ctx.ips 35 | ctx.subdomains 36 | ctx.is() 37 | ctx.accepts() 38 | ctx.acceptsEncodings() 39 | ctx.acceptsCharsets() 40 | ctx.acceptsLanguages() 41 | ctx.get() 42 | 43 | ``` 44 | 45 | 46 | ## 上下文Context的创建 47 | 48 | ### `Koa`的源码主要分为四个部分: 49 | * `application`。 50 | * `context`。 51 | * `request`。 52 | * `response`。 53 | 54 | `application`是继承自`Node`核心模块`events`,通过实例化`Application`,得到`Koa`。`application`在`constructor`的时候会通过`Object.create()`创建一个新对象,*带着指定的原型对象和属性*。 55 | 点击这里查看[MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create#Parameters)。 56 | ```javascript 57 | constructor() { 58 | super(); 59 | 60 | this.proxy = false; 61 | //中间件数组 62 | this.middleware = []; 63 | this.subdomainOffset = 2; 64 | //设置环境变量,默认development 65 | this.env = process.env.NODE_ENV || 'development'; 66 | //使用现有的对象来提供新创建的对象的__proto__,即this.context.__proto__ === context //true 67 | this.context = Object.create(context); 68 | this.request = Object.create(request); 69 | this.response = Object.create(response); 70 | if (util.inspect.custom) { 71 | this[util.inspect.custom] = this.inspect; 72 | } 73 | } 74 | ``` 75 | 76 | 当用户执行`app.listen`时,调用callback函数,中间件函数的执行和调用`createContext()`。 77 | 78 | ```javascript 79 | //启动Koa服务器 80 | listen(...args) { 81 | debug('listen'); 82 | const server = http.createServer(this.callback()); 83 | return server.listen(...args); 84 | } 85 | 86 | //callback 87 | callback() { 88 | //处理Koa的中间件。 89 | const fn = compose(this.middleware); 90 | 91 | if (!this.listenerCount('error')) this.on('error', this.onerror); 92 | 93 | const handleRequest = (req, res) => { 94 | const ctx = this.createContext(req, res); 95 | return this.handleRequest(ctx, fn); 96 | }; 97 | 98 | return handleRequest; 99 | } 100 | //创建context 101 | createContext(req, res) { 102 | const context = Object.create(this.context); 103 | const request = context.request = Object.create(this.request); 104 | const response = context.response = Object.create(this.response); 105 | //设置context的app、req、res、res、ctx 106 | context.app = request.app = response.app = this; 107 | context.req = request.req = response.req = req; 108 | context.res = request.res = response.res = res; 109 | request.ctx = response.ctx = context; 110 | request.response = response; 111 | response.request = request; 112 | context.originalUrl = request.originalUrl = req.url; 113 | context.state = {}; 114 | return context; 115 | } 116 | ``` 117 | 118 | ## 上下文Context 119 | 120 | `Context`属性代理一些参数主要是通过[`delegates`](https://github.com/tj/node-delegates)模块实现的,这里主要是以讲述`delegates`为主。 121 | 122 | ```javascript 123 | //创建context的原型 124 | const proto = module.exports = { 125 | ... 126 | } 127 | 128 | /** 129 | * Response delegation. 130 | */ 131 | 132 | delegate(proto, 'response') 133 | .method('attachment') 134 | .method('redirect') 135 | .method('remove') 136 | .method('vary') 137 | .method('set') 138 | .method('append') 139 | .method('flushHeaders') 140 | .access('status') 141 | .access('message') 142 | .access('body') 143 | .access('length') 144 | .access('type') 145 | .access('lastModified') 146 | .access('etag') 147 | .getter('headerSent') 148 | .getter('writable'); 149 | 150 | ``` 151 | 152 | `Koa`的`context`主要应用了`delegates`的三个方法,分别是`method`、`access`、`getter`方法。 153 | 154 | 初始化`Context`的时候,会在`createContext`中将`response`和`request`赋值给`ctx`,因此`context`含有`request`和`response`的`key`。 155 | 156 | ```javascript 157 | //设置context的app、req、res、res、ctx 158 | context.app = request.app = response.app = this; 159 | context.req = request.req = response.req = req; 160 | context.res = request.res = response.res = res; 161 | request.ctx = response.ctx = context; 162 | request.response = response; 163 | response.request = request; 164 | 165 | ``` 166 | 167 | 在创建`context`原型`proto`时候会调用`delegator`,将`response`和`request`的`key`传递进去,再依次链式调用`method`,`access`,`getter`,将`request`和`response`中需要代理的属性依次传入。 168 | 169 | 如:当用户通过调用`ctx.set()`时, 在此之前,在`delegator`中调用了`method`方法,已经将`set`传递进去,`proto[name]`可以理解为`ctx['set']`,赋值给`proto[name]`一个函数,由于是`ctx`调用`set`,所以当前函数`this`的指向是`ctx`。 170 | 171 | ```javascript 172 | /** 173 | * 委托方法的名字 174 | * 175 | * @param {String} name 176 | * @return {Delegator} self 177 | * @api public 178 | */ 179 | Delegator.prototype.method = function(name){ 180 | // proto原型 181 | var proto = this.proto; 182 | //target 为delegate的第二个参数,这里是response | request 183 | var target = this.target; 184 | this.methods.push(name); 185 | 186 | proto[name] = function(){ 187 | return this[target][name].apply(this[target], arguments); 188 | }; 189 | 190 | return this; 191 | }; 192 | 193 | ``` 194 | 195 | 而这个函数实际上就是通过将`ctx.response.set`通过`apply`进行调用,然后`return`出去的值。 196 | 197 | 198 | ```javascript 199 | /** 200 | * Delegator accessor `name`. 201 | * 202 | * @param {String} name 203 | * @return {Delegator} self 204 | * @api public 205 | */ 206 | 207 | Delegator.prototype.access = function(name){ 208 | return this.getter(name).setter(name); 209 | }; 210 | 211 | /** 212 | * Delegator getter `name`. 213 | * 214 | * @param {String} name 215 | * @return {Delegator} self 216 | * @api public 217 | */ 218 | 219 | Delegator.prototype.getter = function(name){ 220 | var proto = this.proto; 221 | var target = this.target; 222 | this.getters.push(name); 223 | 224 | proto.__defineGetter__(name, function(){ 225 | return this[target][name]; 226 | }); 227 | 228 | return this; 229 | }; 230 | 231 | /** 232 | * Delegator setter `name`. 233 | * 234 | * @param {String} name 235 | * @return {Delegator} self 236 | * @api public 237 | */ 238 | 239 | Delegator.prototype.setter = function(name){ 240 | var proto = this.proto; 241 | var target = this.target; 242 | this.setters.push(name); 243 | 244 | proto.__defineSetter__(name, function(val){ 245 | return this[target][name] = val; 246 | }); 247 | 248 | return this; 249 | }; 250 | 251 | ``` 252 | 253 | 而`access`方法是`setter`、`getting`方法的连续调用,通过设置`Object.__defineGetter__`和`Object.__defineSetter__`来进行数据劫持的。 254 | 255 | 由于[MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__defineSetter__)并不推荐使用这种方法,因此这里使用`Object.defineProperty()`重新写`getter`和`setter`方法。 256 | 257 | ```javascript 258 | //getter 259 | Delegator.prototype.getter = function(name){ 260 | var proto = this.proto; 261 | var target = this.target; 262 | this.setters.push(name); 263 | 264 | Object.defineProperty(proto, name, { 265 | get: function() { 266 | return this[target][name]; 267 | } 268 | }); 269 | 270 | return this; 271 | }; 272 | 273 | //setter 274 | Delegator.prototype.setter = function(name){ 275 | var proto = this.proto; 276 | var target = this.target; 277 | this.setters.push(name); 278 | 279 | Object.defineProperty(proto, name, { 280 | set: function(val) { 281 | return this[target][name] = val; 282 | } 283 | }); 284 | 285 | return this; 286 | }; 287 | 288 | ``` 289 | 290 | ## 最后 291 | 292 | Koa的数据劫持主要是靠`Object.__defineSetter__`和`Object.__defineSetter__`的应用,不过说起来,`Koa`整体的设计模式还是很值得学习的。 293 | 294 | 295 | 296 | -------------------------------------------------------------------------------- /Promise/README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 相信大家经常使用`Promise`,或者使用`Generator`、`asnyc/await`等异步解决方案,网上的`Promise`原理也遍地开花。 3 | 一直以来想抽出时间也写一写`Promise`实现,但是平常工作也是忙的不可开交,正好元旦放了3天假期,休息了2天半,抽出半天时间来看一看`Promise`。 4 | ## 如何使用Promise 5 | ``` 6 | new Promise((resolve, reject) => { 7 | setTimeout(() => { 8 | resolve(1); 9 | }, 1000) 10 | }).then((data) => { 11 | console.log(data); 12 | return new Promise((res) => { 13 | setTimeout(() => { 14 | res(2); 15 | },1000) 16 | }) 17 | 18 | }).then((res) => { 19 | console.log(res); 20 | }) 21 | 22 | ``` 23 | ## 术语 24 | 25 | * `Promise`是一个包含了兼容`promise`规范`then`方法的对象或函数。 26 | * `thenable` 是一个包含了`then`方法的对象或函数。 27 | * `value` 是任何`Javascript`值。 (包括 `undefined`, `thenable`, `Promise`等)。 28 | * `exception` 是由`throw`表达式抛出来的值。 29 | * `reason` 是一个用于描述`Promise`被拒绝原因的值。 30 | 31 | **由于`Promise/A+`规范并不包括`catch`、`race`、`all`等方法的实现,所以这里也不会去详细解释。** 32 | 33 | ## 要求 34 | 35 | 一个`Promise`必须处在其中之一的状态:`pending`, `fulfilled` 或 `rejected`。 36 | 如果是`pending`状态,则`promise`: 37 | 38 | 可以转换到`fulfilled`或`rejected`状态。 39 | 如果是`fulfilled`状态,则`promise`: 40 | 41 | * 不能转换成任何其它状态。 42 | * 必须有一个值,且这个值不能被改变。 43 | 44 | 如果是`rejected`状态,则`promise`可以: 45 | 46 | * 不能转换成任何其它状态。 47 | * 必须有一个原因,且这个值不能被改变。 48 | 49 | ```javascript 50 | function MyPromise(callback) { 51 | 52 | let that = this; 53 | //定义初始状态 54 | //Promise状态 55 | that.status = 'pending'; 56 | //value 57 | that.value = 'undefined'; 58 | //reason 是一个用于描述Promise被拒绝原因的值。 59 | that.reason = 'undefined'; 60 | 61 | //定义resolve 62 | function resolve(value) { 63 | //当status为pending时,定义Javascript值,定义其状态为fulfilled 64 | if(that.status === 'pending') { 65 | that.value = value; 66 | that.status = 'resolved'; 67 | } 68 | } 69 | 70 | //定义reject 71 | function reject(reason) { 72 | //当status为pending时,定义reason值,定义其状态为rejected 73 | if(that.status === 'pending') { 74 | that.reason = reason; 75 | that.status = 'rejected'; 76 | } 77 | } 78 | 79 | //捕获callback是否报错 80 | try { 81 | callback(resolve, reject); 82 | } catch (error) { 83 | reject(error); 84 | } 85 | } 86 | ``` 87 | `Promise`对象有一个`then`方法,用来注册在这个`Promise`状态确定后的回调,`then`方法接受两个参数: 88 | `Promise.then(onFulfilled,onRejected)`。 89 | `我们把then函数写在原型上`。 90 | 91 | ```javascript 92 | MyPromise.prototype.then = function(onFulfilled, onRejected) { 93 | let that = this; 94 | 95 | if(that.status === 'resolved') { 96 | onFulfilled(that.value); 97 | } 98 | 99 | if(that.status === 'rejected') { 100 | onRejected(that.reason); 101 | } 102 | } 103 | ``` 104 | 上述代码只是实现了`Promise的`最基本逻辑,如果直接调用`then`是可以执行的,但是并不支持异步,而`Promise`最大的特点就是解决`callback`异步回调地狱的问题。 105 | 所以我们来改造下。 106 | ```javascript 107 | function MyPromise(callback) { 108 | 109 | let that = this; 110 | //定义初始状态 111 | //Promise状态 112 | that.status = 'pending'; 113 | //value 114 | that.value = 'undefined'; 115 | //reason 是一个用于描述Promise被拒绝原因的值。 116 | that.reason = 'undefined'; 117 | //用来解决异步问题的数组 118 | that.onFullfilledArray = []; 119 | that.onRejectedArray = []; 120 | 121 | //定义resolve 122 | function resolve(value) { 123 | //当status为pending时,定义Javascript值,定义其状态为fulfilled 124 | if(that.status === 'pending') { 125 | that.value = value; 126 | that.status = 'resolved'; 127 | that.onFullfilledArray.forEach((func) => { 128 | func(that.value); 129 | }); 130 | } 131 | } 132 | 133 | //定义reject 134 | function reject(reason) { 135 | //当status为pending时,定义reason值,定义其状态为rejected 136 | if(that.status === 'pending') { 137 | that.reason = reason; 138 | that.status = 'rejected'; 139 | that.onRejectedArray.forEach((func) => { 140 | func(that.reason); 141 | }); 142 | } 143 | } 144 | 145 | //捕获callback是否报错 146 | try { 147 | callback(resolve, reject); 148 | } catch (error) { 149 | reject(error); 150 | } 151 | } 152 | ``` 153 | 154 | `then`函数的改造 155 | ```javascript 156 | MyPromise.prototype.then = function(onFulfilled, onRejected) { 157 | let that = this; 158 | //需要修改下,解决异步问题,即当Promise调用resolve之后再调用then执行onFulfilled(that.value)。 159 | //用两个数组保存下onFulfilledArray 160 | if(that.status === 'pending') { 161 | that.onFullfilledArray.push((value) => { 162 | onFulfilled(value); 163 | }); 164 | 165 | that.onRejectedArray.push((reason) => { 166 | onRejected(reason); 167 | }); 168 | } 169 | 170 | if(that.status === 'resolved') { 171 | onFulfilled(that.value); 172 | } 173 | 174 | if(that.status === 'rejected') { 175 | onRejected(that.reason); 176 | } 177 | } 178 | ``` 179 | 由于`Promise/A+`规范规定一个`Promise`必须处在其中之一的状态:`pending`, `fulfilled` 或 `rejected`,所以在用户使用`Promise`时,写的是异步代码的话,那么此时`Promise`一定是处于`pending`状态,反之正常调用。 180 | 因此,初始化`Promise`时,定义两个数组为`onFullfilledArray`,`onRejectedArray`,用来保存`then`函数的两个回调函数`onFulfilled`和`onRejected`。同时我们在`then`函数中判断`status`是否是`pending`,然后将`onFulfilled`和`onRejected`分别传入对应数组中。当用户调用`resolve`或`reject`时,更改状态,遍历数组,执行`onFulfilled`或者`onRejected`,从而异步调用。 181 | 182 | ## then函数的链式调用 183 | 在`Promise/A+`规范中: 184 | 185 | **对于一个promise,它的then方法可以调用多次** 186 | * 当`promise` `fulfilled`后,所有`onFulfilled`都必须按照其注册顺序执行。 187 | * 当`promise` `rejected`后,所有`OnRejected`都必须按照其注册顺序执行。 188 | 189 | **then 必须返回一个promise** 190 | 191 | * 如果`onFulfilled` 或 `onRejected` 返回了值`x`, 则执行`Promise` 解析流程`[[Resolve]](promise2, x)`。 192 | * 如果`onFulfilled` 或 `onRejected`抛出了异常`e`, 则`promise2`应当以`e`为`reason`被拒绝。 193 | * 如果 `onFulfilled` 不是一个函数且`promise1`已经`fulfilled`,则`promise2`必须以`promise1`的值`fulfilled`。 194 | * 如果`OnReject` 不是一个函数且`promise1`已经`rejected`, 则`promise2`必须以相同的`reason`被拒绝。 195 | 196 | 197 | ```js 198 | MyPromise.prototype.then = function(onFulfilled, onRejected) { 199 | let that = this; 200 | let promise2; 201 | 202 | // 根据标准,如果then的参数不是function,则我们需要忽略它 203 | onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(f) {}; 204 | onRejected = typeof onRejected === 'function' ? onRejected : function(r) {}; 205 | //需要修改下,解决异步问题,即当Promise调用resolve之后再调用then执行onFulfilled(that.value)。 206 | //用两个数组保存下onFulfilledArray 207 | if(that.status === 'pending') { 208 | 209 | return promise2 = new Promise(function(resolve, reject) { 210 | that.onFullfilledArray.push((value) => { 211 | try { 212 | let x = onFulfilled(that.value); 213 | //判断onFulfilled是否是一个Promise,如果是,那么就直接把MyPromise中的resolve和reject传给then; 214 | //返回值是一个Promise对象,直接取它的结果做为promise2的结果 215 | if(x instanceof MyPromise) { 216 | x.then(resolve, reject); 217 | } 218 | //否则,以它的返回值做为promise2的结果 219 | resolve(x); 220 | } catch (error) { 221 | reject(error); 222 | } 223 | }); 224 | 225 | that.onRejectedArray.push((value) => { 226 | try { 227 | let x = onRejected(that.value); 228 | //判断onRejected是否是一个Promise,如果是,那么就直接把MyPromise中的resolve和reject传给then; 229 | //返回值是一个Promise对象,直接取它的结果做为promise2的结果 230 | if(x instanceof MyPromise) { 231 | x.then(resolve, reject); 232 | } 233 | //否则,以它的返回值做为promise2的结果 234 | resolve(x); 235 | } catch (error) { 236 | reject(error); 237 | } 238 | }); 239 | 240 | }) 241 | } 242 | 243 | if(that.status === 'fulfilled') { 244 | return promise2 = new MyPromise(function(resolve, reject) { 245 | try { 246 | let x = onFulfilled(that.value); 247 | //判断onFulfilled是否是一个Promise,如果是,那么就直接把MyPromise中的resolve和reject传给then; 248 | //返回值是一个Promise对象,直接取它的结果做为promise2的结果 249 | if(x instanceof MyPromise) { 250 | x.then(resolve, reject); 251 | } 252 | //否则,以它的返回值做为promise2的结果 253 | resolve(x); 254 | } catch (error) { 255 | reject(error); 256 | } 257 | 258 | }) 259 | } 260 | 261 | if(that.status === 'rejected') { 262 | return new MyPromise(function(resolve, reject) { 263 | try { 264 | let x = onRejected(that.value); 265 | //判断onRejected是否是一个Promise,如果是,那么就直接把MyPromise中的resolve和reject传给then; 266 | //返回值是一个Promise对象,直接取它的结果做为promise2的结果 267 | if(x instanceof MyPromise) { 268 | x.then(resolve, reject); 269 | } 270 | //否则,以它的返回值做为promise2的结果 271 | resolve(x); 272 | } catch (error) { 273 | reject(error); 274 | } 275 | 276 | }) 277 | 278 | } 279 | } 280 | 281 | ``` 282 | 283 | 在调用`then`时,判断`onFulfilled`和`onRejected`是否是一个函数,如果不是,返回一个匿名函数,同时必须返回各参数的值,用来解决链式调用时`Promise`值的穿透问题。 284 | 例如: 285 | ```js 286 | new MyPromise(resolve=>resolve(8)) 287 | .then() 288 | .then() 289 | .then(function foo(value) { 290 | alert(value) 291 | }) 292 | ``` 293 | 294 | 所以我们把这块改成这样: 295 | 296 | ```js 297 | onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(f) { return f}; 298 | onRejected = typeof onRejected === 'function' ? onRejected : function(r) {throw r}; 299 | ``` 300 | 301 | 但是这样每次判断都需要重新写这个`x`和`MyPromise`的关系,所以,我们需要将这块代码给抽象出来,这块代码在`Promise/A+`规范中叫做`resolvePromise`。 302 | 303 | ```js 304 | function resolvePromise(promise, x, resolve, reject) { 305 | let then,thenCalledOrThrow = false 306 | //如果promise 和 x 指向相同的值, 使用 TypeError做为原因将promise拒绝。 307 | if (promise === x) { 308 | return reject(new TypeError('Chaining cycle detected for promise!')) 309 | } 310 | 311 | //判断x是否是一个Promise,如果是,那么就直接把MyPromise中的resolve和reject传给then; 312 | //返回值是一个Promise对象,直接取它的结果做为promise2的结果 313 | if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) { 314 | try { 315 | then = x.then 316 | if (typeof then === 'function') { // typeof 317 | 318 | //x.then(resolve, reject); 319 | then.call(x, function rs(y) { 320 | 321 | if (thenCalledOrThrow) return 322 | 323 | thenCalledOrThrow = true 324 | 325 | return resolvePromise(promise, y, resolve, reject) 326 | 327 | }, function rj(r) { 328 | 329 | if (thenCalledOrThrow) return 330 | 331 | thenCalledOrThrow = true 332 | 333 | return reject(r) 334 | 335 | }) 336 | } else { 337 | 338 | return resolve(x) 339 | } 340 | } catch(e) { 341 | if (thenCalledOrThrow) return 342 | 343 | thenCalledOrThrow = true 344 | 345 | return reject(e) 346 | } 347 | } else { 348 | 349 | return resolve(x) 350 | } 351 | 352 | } 353 | ``` 354 | 355 | `then`函数最后修改为: 356 | 357 | ```js 358 | MyPromise.prototype.then = function(onFulfilled, onRejected) { 359 | let that = this; 360 | let promise2; 361 | // 根据标准,如果then的参数不是function,则我们需要忽略它 362 | onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(f) { return f}; 363 | onRejected = typeof onRejected === 'function' ? onRejected : function(r) {throw r}; 364 | //需要修改下,解决异步问题,即当Promise调用resolve之后再调用then执行onFulfilled(that.value)。 365 | //用两个数组保存下onFulfilledArray 366 | if(that.status === 'pending') { 367 | return promise2 = new Promise(function(resolve, reject) { 368 | that.onFullfilledArray.push((value) => { 369 | try { 370 | let x = onFulfilled(value); 371 | resolvePromise(promise2, x, resolve, reject) 372 | } catch(e) { 373 | return reject(e) 374 | } 375 | }); 376 | 377 | that.onRejectedArray.push((value) => { 378 | try { 379 | let x = onRejected(value); 380 | resolvePromise(promise2, x, resolve, reject) 381 | } catch(e) { 382 | return reject(e) 383 | } 384 | }); 385 | }) 386 | } 387 | 388 | if(that.status === 'fulfilled') { 389 | return promise2 = new MyPromise(function(resolve, reject) { 390 | try { 391 | let x = onFulfilled(that.value); 392 | //处理then的多种情况 393 | resolvePromise(promise2, x, resolve, reject) 394 | } catch (error) { 395 | reject(error); 396 | } 397 | }) 398 | } 399 | 400 | if(that.status === 'rejected') { 401 | return new MyPromise(function(resolve, reject) { 402 | try { 403 | let x = onRejected(that.value); 404 | //处理then的多种情况 405 | resolvePromise(promise2, x, resolve, reject); 406 | } catch (error) { 407 | reject(error) 408 | } 409 | 410 | }) 411 | 412 | } 413 | } 414 | ``` 415 | 416 | 测试一下: 417 | ```js 418 | new MyPromise((resolve, reject) => { 419 | setTimeout(() => { 420 | resolve(1); 421 | }, 1000) 422 | }).then((data) => { 423 | console.log(data); 424 | return new MyPromise((res) => { 425 | setTimeout(() => { 426 | res(2); 427 | },1000) 428 | }) 429 | }).then((res) => { 430 | console.log(res); 431 | }) 432 | //1 433 | //2 434 | ``` 435 | 436 | ## 最后 437 | 438 | [Promise GITHUB地址](https://github.com/baiyuze/notes/blob/master/Promise/myPromise.js)。 439 | 参考资料: 440 | 441 | [《实现一个完美符合Promise/A+规范的Promise》](https://juejin.im/post/5b3994eff265da596e4cf325) 442 | [《Promise/A+规范》](https://segmentfault.com/a/1190000002452115) 443 | [《Promise3》](https://github.com/xieranmaya/Promise3/blob/master/Promise3.js) 444 | [《剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类》](https://github.com/xieranmaya/blog/issues/3) 445 | 446 | 447 | 448 | 449 | -------------------------------------------------------------------------------- /Promise/myPromise.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // 1. 术语 4 | // promise是一个包含了兼容promise规范then方法的对象或函数, 5 | // thenable 是一个包含了then方法的对象或函数。 6 | // value 是任何Javascript值。 (包括 undefined, thenable, promise等). 7 | // exception 是由throw表达式抛出来的值。 8 | // reason 是一个用于描述Promise被拒绝原因的值。 9 | 10 | // 要求 11 | // 2 Promise状态 12 | // 一个Promise必须处在其中之一的状态:pending, fulfilled 或 rejected. 13 | 14 | // 如果是pending状态,则promise: 15 | 16 | // 可以转换到fulfilled或rejected状态。 17 | // 如果是fulfilled状态,则promise: 18 | 19 | // 不能转换成任何其它状态。 20 | // 必须有一个值,且这个值不能被改变。 21 | // 如果是rejected状态,则promise可以: 22 | 23 | // 不能转换成任何其它状态。 24 | // 必须有一个原因,且这个值不能被改变。 25 | // ”值不能被改变”指的是其identity不能被改变,而不是指其成员内容不能被改变。 26 | // x 是then中的回调函数的返回值。可能的值为Promise,value, 和undefined。 27 | function resolvePromise(promise, x, resolve, reject) { 28 | let then,thenCalledOrThrow = false 29 | //如果promise 和 x 指向相同的值, 使用 TypeError做为原因将promise拒绝。 30 | if (promise === x) { 31 | return reject(new TypeError('Chaining cycle detected for promise!')) 32 | } 33 | 34 | //判断x是否是一个Promise,如果是,那么就直接把MyPromise中的resolve和reject传给then; 35 | //返回值是一个Promise对象,直接取它的结果做为promise2的结果 36 | if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) { 37 | try { 38 | then = x.then 39 | if (typeof then === 'function') { // typeof 40 | 41 | //x.then(resolve, reject); 42 | then.call(x, function rs(y) { 43 | 44 | if (thenCalledOrThrow) return 45 | 46 | thenCalledOrThrow = true 47 | 48 | return resolvePromise(promise, y, resolve, reject) 49 | 50 | }, function rj(r) { 51 | 52 | if (thenCalledOrThrow) return 53 | 54 | thenCalledOrThrow = true 55 | 56 | return reject(r) 57 | 58 | }) 59 | } else { 60 | 61 | return resolve(x) 62 | } 63 | } catch(e) { 64 | if (thenCalledOrThrow) return 65 | 66 | thenCalledOrThrow = true 67 | 68 | return reject(e) 69 | } 70 | } else { 71 | 72 | return resolve(x) 73 | } 74 | 75 | } 76 | 77 | function MyPromise(callback) { 78 | 79 | let that = this; 80 | //定义初始状态 81 | //Promise状态 82 | that.status = 'pending'; 83 | //value 84 | that.value = '1'; 85 | //reason 是一个用于描述Promise被拒绝原因的值。 86 | that.reason = 'undefined'; 87 | //用来解决异步问题的数组 88 | that.onFullfilledArray = []; 89 | that.onRejectedArray = []; 90 | 91 | //定义resolve 92 | function resolve(value) { 93 | //当status为pending时,定义Javascript值,定义其状态为fulfilled 94 | if(that.status === 'pending') { 95 | that.value = value; 96 | that.status = 'fulfilled'; 97 | that.onFullfilledArray.forEach((func) => { 98 | func(that.value); 99 | }); 100 | } 101 | } 102 | 103 | //定义reject 104 | function reject(reason) { 105 | //当status为pending时,定义reason值,定义其状态为rejected 106 | if(that.status === 'pending') { 107 | that.reason = reason; 108 | that.status = 'rejected'; 109 | that.onRejectedArray.forEach((func) => { 110 | func(that.reason); 111 | }); 112 | } 113 | } 114 | 115 | //捕获callback是否报错 116 | try { 117 | callback(resolve, reject); 118 | } catch (error) { 119 | 120 | reject(error); 121 | } 122 | } 123 | 124 | // 定义then 125 | // 一个promise必须有一个then方法,then方法接受两个参数: 126 | // promise.then(onFulfilled,onRejected) 127 | //观察者模式解决异步调用问题 128 | 129 | // 如果onFulfilled是一个函数: 130 | // 它必须在promise fulfilled后调用, 且promise的value为其第一个参数。 131 | // 它不能在promise fulfilled前调用。 132 | // 不能被多次调用。 133 | 134 | // 如果onRejected是一个函数, 135 | // 它必须在promise rejected后调用, 且promise的reason为其第一个参数。 136 | // 它不能在promise rejected前调用。 137 | // 不能被多次调用。 138 | MyPromise.prototype.then = function(onFulfilled, onRejected) { 139 | let that = this; 140 | let promise2; 141 | 142 | // 根据标准,如果then的参数不是function,则我们需要忽略它 143 | onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(f) { return f}; 144 | onRejected = typeof onRejected === 'function' ? onRejected : function(r) {throw r}; 145 | //需要修改下,解决异步问题,即当Promise调用resolve之后再调用then执行onFulfilled(that.value)。 146 | //用两个数组保存下onFulfilledArray 147 | if(that.status === 'pending') { 148 | 149 | return promise2 = new Promise(function(resolve, reject) { 150 | that.onFullfilledArray.push((value) => { 151 | try { 152 | let x = onFulfilled(value); 153 | resolvePromise(promise2, x, resolve, reject) 154 | } catch(e) { 155 | return reject(e) 156 | } 157 | }); 158 | 159 | // that.onFullfilledArray.push((reason) => { 160 | // onFulfilled(reason); 161 | // }); 162 | 163 | // that.onRejectedArray.push((reason) => { 164 | // onRejected(reason); 165 | // }); 166 | 167 | that.onRejectedArray.push((value) => { 168 | try { 169 | let x = onRejected(value); 170 | resolvePromise(promise2, x, resolve, reject) 171 | } catch(e) { 172 | return reject(e) 173 | } 174 | }); 175 | 176 | }) 177 | 178 | 179 | } 180 | 181 | if(that.status === 'fulfilled') { 182 | return promise2 = new MyPromise(function(resolve, reject) { 183 | try { 184 | let x = onFulfilled(that.value); 185 | //处理then的多种情况 186 | resolvePromise(promise2, x, resolve, reject) 187 | } catch (error) { 188 | reject(error); 189 | } 190 | 191 | // try { 192 | // let x = onFulfilled(that.value); 193 | // //判断onFulfilled是否是一个Promise,如果是,那么就直接把MyPromise中的resolve和reject传给then; 194 | // //返回值是一个Promise对象,直接取它的结果做为promise2的结果 195 | // if(x instanceof MyPromise) { 196 | // x.then(resolve, reject); 197 | // } 198 | // //否则,以它的返回值做为promise2的结果 199 | // resolve(x); 200 | // } catch (error) { 201 | // reject(error); 202 | // } 203 | 204 | }) 205 | } 206 | 207 | if(that.status === 'rejected') { 208 | return new MyPromise(function(resolve, reject) { 209 | try { 210 | let x = onRejected(that.value); 211 | //处理then的多种情况 212 | resolvePromise(promise2, x, resolve, reject); 213 | } catch (error) { 214 | reject(error) 215 | } 216 | 217 | }) 218 | 219 | } 220 | } 221 | 222 | module.exports = MyPromise; 223 | 224 | // 如果没有异步,此时status状态肯定为三种状态之一,一般为resolved,反之,为pending。 225 | 226 | // 测试 227 | // new MyPromise((resolve, reject) => { 228 | // setTimeout(() => { 229 | // resolve(1); 230 | // }, 1000) 231 | // }).then((data) => { 232 | // console.log(data); 233 | // return new MyPromise((res) => { 234 | // setTimeout(() => { 235 | // res(2); 236 | // },1000) 237 | // }) 238 | // }).then((res) => { 239 | // console.log(res); 240 | // }) 241 | 242 | new MyPromise(resolve=>resolve(8)) 243 | .then() 244 | .then() 245 | .then(function foo(value) { 246 | console.log(value) 247 | }) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 博客 2 | 3 | ## 写博客的初衷 4 | 5 | 自从上班以来,陆陆续续发生很多事,成长了很多。我一直也没有记录笔记的习惯,总觉得写着用处不大,有需要用到的东西,直接搜索就能解决问题。但是,到18年的6月份的求职,我才发现,一份好的简历也许能为自己瑞色不少,也就想着自己也做一些开源项目吧。 6 | 7 | ## 开源项目 8 | 9 | 10 | 自7月中旬开始,我往`github`正式提交了第一份前端开发模板[`egg-dva`](https://github.com/baiyuze),后面又陆续提交了[`mobile-app-tamplate`](https://github.com/baiyuze/mobile-app-tamplate)、[`pc-app-tamplate`](https://github.com/baiyuze/pc-app-tamplate)、[`egg-ssr-mobile-react`](https://github.com/baiyuze/egg-ssr-mobile-react)、[`easywebpack-template-pc`](https://github.com/baiyuze/easywebpack-template-pc)、[`pc-react-tamplate`](https://github.com/baiyuze/pc-react-tamplate),后来提的模板越来越多,就有了整合这些模板的心思,把他们做成一个脚手架,直接在命令行里选择某种模板,拉下来就能开发。 11 | 后来由于一些事耽误了,也就搁置了。 12 | 13 | ## 博客 14 | 15 | 写第一篇博客是在掘金,分享了第一篇文章[骚年,koa和webpack了解一下.md](https://github.com/baiyuze/notes/blob/master/骚年,koa和webpack了解一下.md),看着一个个点赞,自己心中还是小有成就的,毕竟每写一篇文章,就得花费自己好几天的时间去寻找答案,归纳总结,才能汇总成一篇文章。可能一篇文章只是寥寥的近3000字,也是付出了极大的心血。 16 | 17 | 第二篇文章是讲服务器的,[如何创建一个可靠稳定的Web服务器](https://github.com/baiyuze/notes/blob/master/如何创建一个可靠稳定的Web服务器.md),这篇文章花费了5个晚上,每个晚上都到2点多,自己抱着《NodeJs深入浅出》一遍一遍的看,涉及建立服务器,就和进程挂钩,之前对于进程也是一知半解,通过这次学习,极大了解服务器的机制,短短的一篇文章2000多字,写的也只是我学习内容的1/5。 18 | 19 | 这里汇总下文章。 20 | 21 | ### NODE相关 22 | 23 | * [骚年,koa和webpack了解一下](https://github.com/baiyuze/notes/blob/master/骚年,koa和webpack了解一下.md) 24 | * [如何创建一个可靠稳定的Web服务器](https://github.com/baiyuze/notes/blob/master/如何创建一个可靠稳定的Web服务器.md) 25 | * [从源码上理解express中间件](https://github.com/baiyuze/notes/blob/master/从源码上理解express中间件.md) 26 | * [Koa上下文Context的数据劫持](https://github.com/baiyuze/notes/blob/master/Koa上下文Context的数据劫持.md) 27 | 28 | ### 浏览器相关 29 | 30 | * [浏览器地址栏里输入URL后的全过程](https://github.com/baiyuze/notes/blob/master/浏览器地址栏里输入URL后的全过程.md) 31 | * [浏览器渲染原理(处理重排和重绘)](https://github.com/baiyuze/notes/blob/master/浏览器渲染原理(处理重排和重绘).md) 32 | 33 | ### 前端相关 34 | 35 | * [Promise/A+规范](https://github.com/baiyuze/notes/tree/master/Promise) 36 | * [一次弄懂Event Loop(彻底解决此类面试问题)](https://github.com/baiyuze/notes/blob/master/%E4%B8%80%E6%AC%A1%E5%BC%84%E6%87%82Event%20Loop%EF%BC%88%E5%BD%BB%E5%BA%95%E8%A7%A3%E5%86%B3%E6%AD%A4%E7%B1%BB%E9%9D%A2%E8%AF%95%E9%97%AE%E9%A2%98%EF%BC%89.md) 37 | * [原来我正则表达式会的这么少](https://github.com/baiyuze/notes/blob/master/%E5%8E%9F%E6%9D%A5%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%88%91%E8%AE%B0%E5%BE%97%E8%BF%99%E4%B9%88%E5%B0%91.md) 38 | * [从零开始React服务器渲染(SSR)同构😏(基于Koa)](https://github.com/baiyuze/notes/blob/master/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8BReact%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%B8%B2%E6%9F%93%EF%BC%88SSR%EF%BC%89%E5%90%8C%E6%9E%84%F0%9F%98%8F%EF%BC%88%E5%9F%BA%E4%BA%8EKoa%EF%BC%89.md) 39 | 40 | ## 写在最后 41 | 42 | 从写文章到现在也过去了大概一个月半的时间,每周至少输出一篇文章,从一开始只是为了写文章而写文章,到现在为了提升个人能力而写文章,我想我应该会坚持下去的。 43 | 44 | 愿我出走半生,归来仍是少年! 45 | 46 | 2019年,记录点点滴滴。 47 | 48 | -------------------------------------------------------------------------------- /React源码解析 之 Fiber的渲染(1).md: -------------------------------------------------------------------------------- 1 | 2 | 3 | #### React源码系列 4 | - [React源码解析之 Fiber结构的创建](https://juejin.cn/post/6951585684679294989) 5 | - [React源码解析 之 Fiber的渲染(1)](https://juejin.cn/post/6951585684679294989) 6 | > 通过上一篇《[React Fiber结构的创建](https://juejin.cn/post/6950692243485229086)》,我们得到`fiberRoot`的结构,这一篇,主要讲述如何将得到的`fiberRoot`渲染到视图。 7 | 8 | 9 | ## 如何生成workInProgress结构 10 | 11 | 上篇通过查看调用栈生成了了一个`fiberRoot`,从调用`updateContainer`函数为入口函数,开始执行解析`fiberRoot`结构。 12 | ```js 13 | updateContainer(children, fiberRoot, parentComponent, callback); 14 | ``` 15 | 解释一下,此时的`children`为未实例化的`ReactNodeList`,`parentComponent`为`React.render`中的`container(div#root)`。 16 | 17 | 18 | `updateContainer`处理以下几件事。 19 | 20 | ### 一、 获取当前优先级`lane`(车道) 21 | ```js 22 | const current = container.current;//container - fiber root 23 | // 获取优先级车道lane 24 | const lane = requestUpdateLane(current); 25 | ``` 26 | 此时`lane`为1。 27 | 28 | 在`react v17`版本后,以`lane`为优先级,`lane`值越小,优先级越高,`lane`为二进制存储,一共`31`位,每个位为一个车道。点击[这里](https://github.com/facebook/react/blob/933880b4544a83ce54c8a47f348effe725a58843/packages/react-reconciler/src/ReactFiberLane.old.js)查看源码。 29 | ```js 30 | export const TotalLanes = 31; 31 | 32 | export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000; 33 | export const NoLane: Lane = /* */ 0b0000000000000000000000000000000; 34 | 35 | export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001; 36 | 37 | // 输入框的值 38 | const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010; 39 | export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100; 40 | 41 | export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000; 42 | export const DefaultLane: Lanes = /* */ 0b0000000000000000000000000010000; 43 | 44 | ``` 45 | 在`requestUpdateLane`中在初始化渲染时,`lane`为1。 46 | ```js 47 | export function requestUpdateLane(fiber: Fiber): Lane { 48 | // Special cases 49 | const mode = fiber.mode; 50 | if ((mode & ConcurrentMode) === NoMode) { 51 | // 0b0000000000000000000000000000001; 52 | // 优先级为1车道为1 53 | return (SyncLane: Lane); 54 | // .... 55 | ``` 56 | 57 | 在react源码中,有一个文件,单独存放了不同种类型的优先级的`lane`的二进制值。 58 | 关于`lane`,后续会有单独文章进行解释其含义,以及使用`lane`进行权限设计的应用,此时我们只需要知道这个地方获取了`lane`作为`fiber`的优先级。 59 | 60 | ### 二、创建context。 61 | 在初始化时,通过`getContextForSubtree`函数,判断是否存在`parentComponent`,此时`parentComponet`为root的父元素,此时为null。 62 | 否则返回`parentContext`。 63 | ```js 64 | function getContextForSubtree( 65 | parentComponent: ?React$Component, 66 | ): Object { 67 | if (!parentComponent) { 68 | return emptyContextObject; 69 | } 70 | 71 | const fiber = getInstance(parentComponent); 72 | const parentContext = findCurrentUnmaskedContext(fiber); 73 | 74 | if (fiber.tag === ClassComponent) { 75 | const Component = fiber.type; 76 | if (isLegacyContextProvider(Component)) { 77 | return processChildContext(fiber, Component, parentContext); 78 | } 79 | } 80 | 81 | return parentContext; 82 | } 83 | ``` 84 | 85 | ### 三、创建update 86 | 创建update对象用来进行排队更新`enqueueUpdate`。 87 | 88 | ```js 89 | // 创建update对象 90 | const update = createUpdate(eventTime, lane); 91 | // ... 92 | export function createUpdate(eventTime: number, lane: Lane): Update<*> { 93 | const update: Update<*> = { 94 | eventTime, 95 | lane, 96 | tag: UpdateState,// tag为 0|1|2|3 97 | payload: null, 98 | callback: null, 99 | 100 | next: null, 101 | }; 102 | return update; 103 | } 104 | // ... 105 | update.payload = { element }; 106 | callback = callback === undefined ? null : callback; 107 | update.callback = callback; 108 | } 109 | // 队列更新 110 | enqueueUpdate(current, update, lane); 111 | ``` 112 | `enqueueUpdate`主要用来添加一个共享队列`sharedQueue`,该队列可以和`workInProgress`和`FiberRoot`进行共享队列。 113 | ```js 114 | export function enqueueUpdate( 115 | fiber: Fiber,//fiberRoot 116 | update: Update, 117 | lane: Lane, 118 | ) { 119 | const updateQueue = fiber.updateQueue; 120 | if (updateQueue === null) { 121 | // Only occurs if the fiber has been unmounted. 122 | return; 123 | } 124 | // 当前队列和缓存队列共享一个持久化队列 125 | const sharedQueue: SharedQueue = (updateQueue: any).shared; 126 | // 比较fiber lane和lane,相同时更新 127 | // render初始化时不执行 128 | if (isInterleavedUpdate(fiber, lane)) { 129 | const interleaved = sharedQueue.interleaved;//交错更新 130 | if (interleaved === null) { 131 | // 如果是第一次更新,创建双向链表 132 | update.next = update; 133 | //在当前渲染结束时,将显示此队列的交错更新 134 | //被转移到挂起队列。 135 | pushInterleavedQueue(sharedQueue); 136 | } else { 137 | // interleaved.next -> update.next update - interleaved.next; 138 | // interleaved.next = update 139 | // update.next = interleaved.next = update 140 | update.next = interleaved.next; 141 | interleaved.next = update; 142 | } 143 | sharedQueue.interleaved = update; 144 | } else { 145 | const pending = sharedQueue.pending; 146 | if (pending === null) { 147 | //这是第一次更新。创建单向链表。 148 | update.next = update; 149 | } else { 150 | // 定义双向列表 151 | update.next = pending.next; 152 | pending.next = update; 153 | } 154 | sharedQueue.pending = update; 155 | } 156 | } 157 | ``` 158 | ### 四、调用`scheduleUpdateOnFiber` 159 | 160 | 处理`Fiber` `schedule`更新,在这里我们只看该次render函数会执行的逻辑,页面初始化时,此时只是先生成了`fiberRoot`,并未生成`workInProgressRoot`,此时`workInProgressRoot`为`null`,因此,会走一下逻辑,调用`performSyncWorkOnRoot`。 161 | ```js 162 | export function scheduleUpdateOnFiber( 163 | fiber: Fiber, 164 | lane: Lane,//当前更新lane 165 | eventTime: number, 166 | ): FiberRoot | null { 167 | ... 168 | //标记root有挂起的更新。 169 | markRootUpdated(root, lane, eventTime); 170 | if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) { 171 | ... 172 | } 173 | // root为fiber,workInfoProGressRoot为null,为false 174 | if (root === workInProgressRoot) { 175 | ... 176 | } 177 | 178 | if (lane === SyncLane) { 179 | if ( 180 | // 检查我们是否在unbatchedUpdates 181 | (executionContext & LegacyUnbatchedContext) !== NoContext && 182 | // 检查我们是否还没有渲染 183 | (executionContext & (RenderContext | CommitContext)) === NoContext 184 | ) { 185 | // 在根目录上注册挂起的交互,以避免丢失跟踪的交互数据。 186 | schedulePendingInteractions(root, lane); 187 | performSyncWorkOnRoot(root); 188 | } else { 189 | ensureRootIsScheduled(root, eventTime); 190 | schedulePendingInteractions(root, lane); 191 | if ( 192 | executionContext === NoContext && 193 | (fiber.mode & ConcurrentMode) === NoMode 194 | ) { 195 | //重置 196 | resetRenderTimer(); 197 | flushSyncCallbackQueue(); 198 | } 199 | } 200 | } else { 201 | //... 202 | ensureRootIsScheduled(root, eventTime); 203 | schedulePendingInteractions(root, lane); 204 | } 205 | 206 | return root; 207 | } 208 | ``` 209 | 210 | `performSyncWorkOnRoot`会刷新`Effects`和同步渲染`Fiber`。 211 | 212 | ```js 213 | //... 214 | if ( 215 | root === workInProgressRoot && 216 | areLanesExpired(root, workInProgressRootRenderLanes) 217 | ) { 218 | lanes = workInProgressRootRenderLanes; 219 | exitStatus = renderRootSync(root, lanes); 220 | } else { 221 | lanes = getNextLanes(root, NoLanes); 222 | // 同步渲染root 223 | exitStatus = renderRootSync(root, lanes); 224 | } 225 | //... 226 | ``` 227 | 228 | `renderRootSync`函数,首先会创建一个`workInProgress`的双向链表树,此树通过`alternate`和fiberRoot进行关联 229 | ```js 230 | ... 231 | if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) { 232 | // 创建workInProgress的tree 233 | prepareFreshStack(root, lanes); 234 | // 开始处理挂起的交互,并将有交互的,绑定到store中即root.memoizedInteractions中 235 | startWorkOnPendingInteractions(root, lanes); 236 | } 237 | ... 238 | //创建workInProgress和workInProgressRoot。 239 | function prepareFreshStack(root: FiberRoot, lanes: Lanes) { 240 | root.finishedWork = null; 241 | root.finishedLanes = NoLanes; 242 | 243 | const timeoutHandle = root.timeoutHandle; 244 | if (timeoutHandle !== noTimeout) { 245 | // The root previous suspended and scheduled a timeout to commit a fallback 246 | // state. Now that we have additional work, cancel the timeout. 247 | root.timeoutHandle = noTimeout; 248 | // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above 249 | cancelTimeout(timeoutHandle); 250 | } 251 | 252 | if (workInProgress !== null) { 253 | let interruptedWork = workInProgress.return; 254 | while (interruptedWork !== null) { 255 | unwindInterruptedWork(interruptedWork, workInProgressRootRenderLanes); 256 | interruptedWork = interruptedWork.return; 257 | } 258 | } 259 | workInProgressRoot = root; 260 | workInProgress = createWorkInProgress(root.current, null);//根据当前节点创建workInProgess 261 | workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes; 262 | workInProgressRootExitStatus = RootIncomplete; 263 | workInProgressRootFatalError = null; 264 | workInProgressRootSkippedLanes = NoLanes; 265 | workInProgressRootUpdatedLanes = NoLanes; 266 | workInProgressRootPingedLanes = NoLanes; 267 | 268 | ... 269 | } 270 | ``` 271 | 执行后续代码,调用`workLoopSync`函数,开始处理`workInProgress`双向链表,进入`beginWork`阶段。 272 | ```js 273 | do { 274 | try { 275 | workLoopSync();//同步更新 276 | break; 277 | } catch (thrownValue) { 278 | handleError(root, thrownValue); 279 | } 280 | } while (true); 281 | 282 | 283 | ... 284 | ... 285 | 286 | function workLoopSync() { 287 | // Already timed out, so perform work without checking if we need to yield. 288 | while (workInProgress !== null) { 289 | performUnitOfWork(workInProgress);//执行工作单元 290 | } 291 | } 292 | ··· 293 | ``` 294 | 此时workInProgress结构已经生成,它的结构和左侧结构一致,我这边省略画了,通过中间的alternate进行连接。 295 | 图如下: 296 | 297 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fbae5fe0f8b74a8db171e9837330a1cc~tplv-k3u1fbpfcp-watermark.image) 298 | 299 | #### 函数调用栈如下: 300 | 301 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6d5d2a431ad84e96b8198236e147affd~tplv-k3u1fbpfcp-watermark.image) 302 | ## 下一篇:beginWork阶段。 303 | 304 | 305 | 306 | -------------------------------------------------------------------------------- /React源码解析 之Fiber的渲染(2)beginWork.md: -------------------------------------------------------------------------------- 1 | 2 | ## beginWork 阶段 3 | ### React源码系列 4 | - [React源码解析之 Fiber结构的创建](https://juejin.cn/post/6951585684679294989) 5 | - [React源码解析 之 Fiber的渲染(1)](https://juejin.cn/post/6951585684679294989) 6 | > * 上一篇结尾,我们得到了一个`workInProgress`结构的双向链表树,它的`alternate`指向`FiberRootNode.current`。 7 | > * 这一篇,主要介绍`beginWork`阶段。 8 | 9 | ### beginWork阶段 10 | 从调用的代码中来看,`beginWork`最终返回的是一个`Next`,此`next`最后赋值给`WorkInProgress`,然后继续循环调用,最终当`workInProgress === null`,时,结束循环。 11 | 12 | ```js 13 | // 同步 14 | function workLoopSync() { 15 | // Already timed out, so perform work without checking if we need to yield. 16 | while (workInProgress !== null) { 17 | performUnitOfWork(workInProgress); 18 | } 19 | } 20 | 21 | ... 22 | //执行任务单元 23 | function performUnitOfWork(unitOfWork: Fiber): void { 24 | 25 | const current = unitOfWork.alternate; 26 | setCurrentDebugFiberInDEV(unitOfWork); 27 | 28 | let next; 29 | if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) { 30 | startProfilerTimer(unitOfWork); 31 | next = beginWork(current, unitOfWork, subtreeRenderLanes); 32 | stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); 33 | } else { 34 | next = beginWork(current, unitOfWork, subtreeRenderLanes); 35 | } 36 | 37 | resetCurrentDebugFiberInDEV(); 38 | unitOfWork.memoizedProps = unitOfWork.pendingProps; 39 | if (next === null) { 40 | // If this doesn't spawn new work, complete the current work. 41 | completeUnitOfWork(unitOfWork); 42 | } else { 43 | workInProgress = next; 44 | } 45 | 46 | ReactCurrentOwner.current = null; 47 | } 48 | ``` 49 | 对于beginWork函数来说,需要三个参数。 50 | 51 | - `current`:当前Fiber,即`workInProgress.alterNate` 52 | - `workInProgress`:当前工作任务单元 53 | - `renderLanes`:优先级相关 54 | 55 | 在一开始调用beginWork时,就判断current是否为null,区分组件是否是首次渲染,首次渲染情况下current为null,也就是处于mount。 56 | 57 | 所以`beginWork`,一共有两个阶段,一个是`mount`阶段,一个`update`阶段,`update`可复用`current`阶段,就不需要重新创建`workInProgress.child`。 58 | 59 | ```js 60 | 61 | function beginWork( 62 | current: Fiber | null, 63 | workInProgress: Fiber, 64 | renderLanes: Lanes, 65 | ): Fiber | null { 66 | if(current !== null) { 67 | const oldProps = current.memoizedProps 68 | const newProps = workInProgress.pendingProps 69 | if ( 70 | oldProps === newProps && 71 | !hasLegacyContextChanged() && 72 | (updateExpirationTime === NoWork || 73 | updateExpirationTime > renderExpirationTime) 74 | ) { 75 | ... 76 | } 77 | ... 78 | } 79 | ``` 80 | 然后定义属性`oldProps`和`newProps`,进行判断,判断`oldProps === newProps` && 81 | `!hasLegacyContextChanged()`,当判断为`true`的时候。设置`didReceiveUpdate`为`true`。 82 | 83 | 否则,调用`bailoutOnAlreadyFinishedWork`函数,复用组件,返回`workInProgress.chilren`。 84 | 85 | ```js 86 | 87 | function bailoutOnAlreadyFinishedWork( 88 | current: Fiber | null, 89 | workInProgress: Fiber, 90 | renderLanes: Lanes, 91 | ): Fiber | null { 92 | if (current !== null) { 93 | // Reuse previous dependencies 94 | workInProgress.dependencies = current.dependencies; 95 | } 96 | 97 | if (enableProfilerTimer) { 98 | // Don't update "base" render times for bailouts. 99 | stopProfilerTimerIfRunning(workInProgress); 100 | } 101 | 102 | markSkippedUpdateLanes(workInProgress.lanes); 103 | if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { 104 | if (enableLazyContextPropagation && current !== null) { 105 | lazilyPropagateParentContextChanges(current, workInProgress, renderLanes); 106 | if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { 107 | return null; 108 | } 109 | } else { 110 | return null; 111 | } 112 | } 113 | cloneChildFibers(current, workInProgress); 114 | return workInProgress.child; 115 | } 116 | ``` 117 | 118 | 根据`workInProgress`的`tag`来判断当前组件处于什么状态,调用函数,返回`workInProgress.child`。 119 | ```js 120 | export const FunctionComponent = 0; 121 | export const ClassComponent = 1; 122 | export const IndeterminateComponent = 2; // Before we know whether it is function or class //未判断是否是类组件,还是函数组件 123 | export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.可以嵌套在另外节点中 124 | export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.子树。可能是其他渲染器的入口点。 125 | export const HostComponent = 5; 126 | export const HostText = 6; 127 | export const Fragment = 7; 128 | export const Mode = 8; 129 | export const ContextConsumer = 9; 130 | export const ContextProvider = 10; 131 | export const ForwardRef = 11; 132 | export const Profiler = 12; 133 | export const SuspenseComponent = 13; 134 | export const MemoComponent = 14; 135 | export const SimpleMemoComponent = 15; 136 | export const LazyComponent = 16; 137 | export const IncompleteClassComponent = 17; 138 | export const DehydratedFragment = 18; 139 | export const SuspenseListComponent = 19; 140 | export const ScopeComponent = 21; 141 | export const OffscreenComponent = 22; 142 | export const LegacyHiddenComponent = 23; 143 | export const CacheComponent = 24; 144 | 145 | ``` 146 | 大概有二十四中类型,这里主要介绍其中的几种。 147 | 148 | // 根据tag不同,创建不同的子Fiber节点 149 | ```js 150 | switch (workInProgress.tag) { 151 | case IndeterminateComponent: 152 | // ... 153 | case LazyComponent: 154 | // ... 155 | case FunctionComponent: 156 | // ... 157 | case ClassComponent: 158 | // ... 159 | case HostRoot: 160 | // ... 161 | case HostComponent: 162 | // ...省略 163 | case HostText: 164 | // ... 165 | // ...省略其他类型 166 | } 167 | } 168 | ``` 169 | #### HostRoot 170 | 171 | 172 | ```js 173 | case HostRoot://3 rootFiber 174 | return updateHostRoot(current, workInProgress, renderLanes); 175 | ``` 176 | 这是在初始化,第一次调用时的tag,此tag为3,即`rootFiber`节点。调用`updateHostRoot`函数。 177 | ```js 178 | function updateHostRoot(current, workInProgress, renderLanes) { 179 | //...省略 180 | const updateQueue = workInProgress.updateQueue; 181 | const nextProps = workInProgress.pendingProps; 182 | const prevState = workInProgress.memoizedState; 183 | const prevChildren = prevState.element; 184 | cloneUpdateQueue(current, workInProgress);//克隆,赋值 185 | // 这个方法的主要作用是处理updateQueue里面的update,执行并获得最新的state,最后获取effect放置到Fiber对象上 186 | processUpdateQueue(workInProgress, nextProps, null, renderLanes); 187 | const nextState = workInProgress.memoizedState; 188 | 189 | const root: FiberRoot = workInProgress.stateNode; 190 | //...省略 191 | const nextChildren = nextState.element; 192 | if (nextChildren === prevChildren) {//如果children状态没发生改变,就不处理 193 | resetHydrationState(); 194 | return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); 195 | } 196 | // hydrate 为true时执行 197 | if (root.hydrate && enterHydrationState(workInProgress)) { 198 | //...省略 199 | } else { 200 | // root.进入diff算法 201 | reconcileChildren(current, workInProgress, nextChildren, renderLanes); 202 | resetHydrationState();//重置 203 | } 204 | return workInProgress.child; 205 | } 206 | ``` 207 | `updateHostRoot`函数,在开始执行时,初始化了`nextProps`、`prevState`、`prevChildren`,值,并放入`reconcileChildren`处理,最后返回`workInProgress.child`。 208 | ```js 209 | export function reconcileChildren( 210 | current: Fiber | null, 211 | workInProgress: Fiber, 212 | nextChildren: any, 213 | renderLanes: Lanes 214 | ) { 215 | if (current === null) { 216 | // 对于mount的组件 217 | workInProgress.child = mountChildFibers( 218 | workInProgress, 219 | null, 220 | nextChildren, 221 | renderLanes, 222 | ); 223 | } else { 224 | // 对于update的组件 225 | workInProgress.child = reconcileChildFibers( 226 | workInProgress, 227 | current.child, 228 | nextChildren, 229 | renderLanes, 230 | ); 231 | } 232 | } 233 | ``` 234 | 在上面时,有介绍过`current === null`的情况,即组件有两个状态,一个是`mount`状态,一个是`update`状态。 235 | 236 | 1. `mount`时,创建新的子`fiber`节点。 237 | 2.`update`时,与上一次的`fiber`节点,进行对比,也就是所谓的`Diff`算法。这里就不展开说了,后续会有单独文章介绍`React`的`Diff`算法。 238 | 239 | #### IndeterminateComponent 240 | 241 | `IndeterminateComponent`从字面理解,这是个不确定组件,调用`mountIndeterminateComponent`函数。 242 | 243 | 而什么时候才会调用`mountIndeterminateComponent`函数呢? 244 | 245 | 一般来说,在`FunctionComponent`在第一次识别的时候,会把tag设为`IndeterminateComponent`。 246 | 在之后再进行调用,区分是具体是`classComponent`还是`FunctionComponent`。 247 | 248 | 所以,在执行该函数的时候,先试判断了`current`是否存在,如果存在,清空该`current`和`workInProgress`的引用`alternate`。 249 | 250 | ```js 251 | 252 | function mountIndeterminateComponent( 253 | _current, 254 | workInProgress, 255 | Component, 256 | renderLanes, 257 | ) { 258 | if (_current !== null) { 259 | _current.alternate = null; 260 | workInProgress.alternate = null; 261 | 262 | workInProgress.flags |= Placement; 263 | } 264 | 265 | const props = workInProgress.pendingProps; 266 | let context; 267 | if (!disableLegacyContext) { 268 | const unmaskedContext = getUnmaskedContext( 269 | workInProgress, 270 | Component, 271 | false, 272 | ); 273 | context = getMaskedContext(workInProgress, unmaskedContext); 274 | } 275 | 276 | prepareToReadContext(workInProgress, renderLanes); 277 | let value; 278 | if(__DEV__) 279 | // ... 280 | } else { 281 | value = renderWithHooks( 282 | null, 283 | workInProgress, 284 | Component, 285 | props, 286 | context, 287 | renderLanes, 288 | ); 289 | } 290 | workInProgress.flags |= PerformedWork; 291 | //判断 那个是类组件,哪个是函数组件 292 | if ( 293 | !disableModulePatternComponents && 294 | typeof value === 'object' && 295 | value !== null && 296 | typeof value.render === 'function' && 297 | value.$$typeof === undefined 298 | ) { 299 | 300 | // Proceed under the assumption that this is a class instance 301 | workInProgress.tag = ClassComponent; 302 | 303 | // Throw out any hooks that were used. 304 | workInProgress.memoizedState = null; 305 | workInProgress.updateQueue = null; 306 | // 是否含有context 307 | let hasContext = false; 308 | if (isLegacyContextProvider(Component)) { 309 | hasContext = true; 310 | pushLegacyContextProvider(workInProgress); 311 | } else { 312 | hasContext = false; 313 | } 314 | // 判断是否含有state 315 | workInProgress.memoizedState = 316 | value.state !== null && value.state !== undefined ? value.state : null; 317 | //初始化updateQueue 318 | initializeUpdateQueue(workInProgress); 319 | const getDerivedStateFromProps = Component.getDerivedStateFromProps; 320 | if (typeof getDerivedStateFromProps === 'function') { 321 | applyDerivedStateFromProps( 322 | workInProgress, 323 | Component, 324 | getDerivedStateFromProps, 325 | props, 326 | ); 327 | } 328 | 329 | adoptClassInstance(workInProgress, value); 330 | // 实例化clas组件,并调用componentWillUpdate和UNSAFE_componentWillMount 331 | mountClassInstance(workInProgress, Component, props, renderLanes); 332 | return finishClassComponent( 333 | null, 334 | workInProgress, 335 | Component, 336 | true, 337 | hasContext, 338 | renderLanes, 339 | ); 340 | } else { 341 | 342 | workInProgress.tag = FunctionComponent; 343 | // 处理workInProgress 344 | reconcileChildren(null, workInProgress, value, renderLanes); 345 | 346 | return workInProgress.child; 347 | } 348 | } 349 | 350 | ``` 351 | 对于value变量,该变量,直接调用`renderWithHooks`函数,然后在函数内调用`Component(props, secondArg);` 352 | ```js 353 | value = renderWithHooks( 354 | null, 355 | workInProgress, 356 | Component, 357 | props, 358 | context, 359 | renderLanes, 360 | ); 361 | ``` 362 | 363 | ```js 364 | let children = Component(props, secondArg); 365 | ``` 366 | 对于`Component`函数,该函数是`workInProgress.type`函数。也是在一开始判断`tag`时,所传入的`type`。 367 | ```js 368 | // 不确定组件 369 | case IncompleteClassComponent: {//17 370 | const Component = workInProgress.type; 371 | const unresolvedProps = workInProgress.pendingProps; 372 | const resolvedProps = 373 | workInProgress.elementType === Component 374 | ? unresolvedProps 375 | : resolveDefaultProps(Component, unresolvedProps); 376 | return mountIncompleteClassComponent( 377 | current, 378 | workInProgress, 379 | Component, 380 | resolvedProps, 381 | renderLanes, 382 | ); 383 | } 384 | ``` 385 | 当判断FunctionComponent和ClassComponent处理value函数后,返回`workInProgress.child`. 386 | ```js 387 | reconcileChildren(null, workInProgress, value, renderLanes); 388 | return workInProgress.child; 389 | ``` 390 | #### FunctionComponent 391 | ```js 392 | // 函数式组件 393 | case FunctionComponent: { 394 | child = updateFunctionComponent( 395 | null, 396 | workInProgress, 397 | Component, 398 | resolvedProps, 399 | renderLanes, 400 | ); 401 | return child; 402 | ``` 403 | 对于`FunctionComponet`类型,`updateFunctionComponent`处理就比较简单了,先是处理`context`,创建`nextChildren`,同时处理`current !== null && !didReceiveUpdate`,判断是否是`update`阶段。 404 | 405 | 调用`bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes)`,返回`workInProgress.child`; 406 | 407 | ```js 408 | 409 | function updateFunctionComponent( 410 | current, 411 | workInProgress, 412 | Component, 413 | nextProps: any, 414 | renderLanes, 415 | ) { 416 | // 初始化Context 417 | let context; 418 | if (!disableLegacyContext) { 419 | const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); 420 | context = getMaskedContext(workInProgress, unmaskedContext); 421 | } 422 | 423 | let nextChildren; 424 | //处理context 425 | prepareToReadContext(workInProgress, renderLanes); 426 | //... 427 | //处理函数式组件 428 | nextChildren = renderWithHooks( 429 | current, 430 | workInProgress, 431 | Component, 432 | nextProps, 433 | context, 434 | renderLanes, 435 | ); 436 | // update状态,current != null ,并且didReceiveUpdate= false 437 | if (current !== null && !didReceiveUpdate) { 438 | bailoutHooks(current, workInProgress, renderLanes); 439 | return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); 440 | } 441 | 442 | //... 443 | workInProgress.flags |= PerformedWork; 444 | reconcileChildren(current, workInProgress, nextChildren, renderLanes); 445 | return workInProgress.child; 446 | } 447 | ``` 448 | 449 | 450 | #### ClassComponent 451 | 452 | ```js 453 | case ClassComponent: {//1类组件 454 | const Component = workInProgress.type; 455 | const unresolvedProps = workInProgress.pendingProps; 456 | const resolvedProps = 457 | workInProgress.elementType === Component 458 | ? unresolvedProps 459 | // const props = Object.assign({}, baseProps); 460 | // 如果该组件设置了defaultProps,会先合并defaultProps 461 | : resolveDefaultProps(Component, unresolvedProps);//合并默认props,assign props 462 | return updateClassComponent( 463 | current, 464 | workInProgress, 465 | Component, 466 | resolvedProps, 467 | renderLanes, 468 | ); 469 | } 470 | ``` 471 | 472 | 对于`ClassComponent`组件,先是获取resulvedProps,创建类组件`updateClassComponent`。 473 | 474 | ```js 475 | function updateClassComponent( 476 | current: Fiber | null, 477 | workInProgress: Fiber, 478 | Component: any, 479 | nextProps: any, 480 | renderLanes: Lanes, 481 | ) { 482 | //判断是否含有context 483 | let hasContext; 484 | if (isLegacyContextProvider(Component)) { 485 | hasContext = true; 486 | pushLegacyContextProvider(workInProgress); 487 | } else { 488 | hasContext = false; 489 | } 490 | prepareToReadContext(workInProgress, renderLanes); 491 | // 获取instance 492 | const instance = workInProgress.stateNode; 493 | let shouldUpdate; 494 | if (instance === null) { 495 | if (current !== null) { 496 | //没有实例的类组件只有在挂起时才会挂载 497 | current.alternate = null; 498 | workInProgress.alternate = null; 499 | workInProgress.flags |= Placement; 500 | } 501 | // 初始化的情况下需要实例化 502 | constructClassInstance(workInProgress, Component, nextProps); 503 | // 调用生命周期beginWork阶段 504 | mountClassInstance(workInProgress, Component, nextProps, renderLanes); 505 | shouldUpdate = true; 506 | } else if (current === null) { 507 | // 在已经instance实例化的基础上,处理context,props,和state 508 | shouldUpdate = resumeMountClassInstance( 509 | workInProgress, 510 | Component, 511 | nextProps, 512 | renderLanes, 513 | ); 514 | } else { 515 | shouldUpdate = updateClassInstance( 516 | current, 517 | workInProgress, 518 | Component, 519 | nextProps, 520 | renderLanes, 521 | ); 522 | } 523 | 524 | const nextUnitOfWork = finishClassComponent( 525 | current, 526 | workInProgress, 527 | Component, 528 | shouldUpdate, 529 | hasContext, 530 | renderLanes, 531 | ); 532 | 533 | return nextUnitOfWork; 534 | } 535 | ``` 536 | 537 | 判断`instance === null`不存在时,说明组件并未实例话,`workInProgress.stateNode`为null,调用`constructClassInstance`,进行实例化组件,如下: 538 | 539 | ```js 540 | constructClassInstance() { 541 | //... 542 | // 实例化类组件和状态 543 | const instance = new ctor(props, context); 544 | const state = (workInProgress.memoizedState = 545 | instance.state !== null && instance.state !== undefined 546 | ? instance.state 547 | : null);//传入state 548 | // 设置实例instance到workInProgress.stateNode,并且在instance上挂载workInProgress 549 | //并将instace赋值workInProgress.stateNode。 550 | adoptClassInstance(workInProgress, instance);//采用类实例 551 | //... 552 | } 553 | ``` 554 | 通过判断`current=== null`,判断该组件处于`mount`阶段。 555 | 556 | 通过调用`resumeMountClassInstance`函数,执行生命周期。 557 | 558 | 559 | 调用`componentWillReceiveProps` 函数和`UNSAFE_componentWillReceiveProps`函数,最后返回`false`。 560 | 561 | 562 | 设置`shouldUpdate = false` 563 | 564 | 当处于`update`状态时,调用`updateClassInstance`函数,处理`props`,`state`,和`context`。 565 | ```js 566 | function updateClassInstance( 567 | current: Fiber, 568 | workInProgress: Fiber, 569 | ctor: any, 570 | newProps: any, 571 | renderLanes: Lanes, 572 | ): boolean { 573 | const instance = workInProgress.stateNode; 574 | 575 | cloneUpdateQueue(current, workInProgress); 576 | // 处理props 577 | const unresolvedOldProps = workInProgress.memoizedProps; 578 | const oldProps = 579 | workInProgress.type === workInProgress.elementType 580 | ? unresolvedOldProps 581 | : resolveDefaultProps(workInProgress.type, unresolvedOldProps); 582 | instance.props = oldProps; 583 | const unresolvedNewProps = workInProgress.pendingProps; 584 | // 处理context 585 | const oldContext = instance.context; 586 | const contextType = ctor.contextType; 587 | let nextContext = emptyContextObject; 588 | if (typeof contextType === 'object' && contextType !== null) { 589 | nextContext = readContext(contextType); 590 | } else if (!disableLegacyContext) { 591 | const nextUnmaskedContext = getUnmaskedContext(workInProgress, ctor, true); 592 | nextContext = getMaskedContext(workInProgress, nextUnmaskedContext); 593 | } 594 | //调用生命周期 595 | const getDerivedStateFromProps = ctor.getDerivedStateFromProps; 596 | const hasNewLifecycles = 597 | typeof getDerivedStateFromProps === 'function' || 598 | typeof instance.getSnapshotBeforeUpdate === 'function'; 599 | 600 | if ( 601 | !hasNewLifecycles && 602 | (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || 603 | typeof instance.componentWillReceiveProps === 'function') 604 | ) { 605 | if ( 606 | unresolvedOldProps !== unresolvedNewProps || 607 | oldContext !== nextContext 608 | ) { 609 | // 调用ComponentWillReceiveProps生命周期 610 | callComponentWillReceiveProps( 611 | workInProgress, 612 | instance, 613 | newProps, 614 | nextContext, 615 | ); 616 | } 617 | } 618 | 619 | //... 620 | const oldState = workInProgress.memoizedState; 621 | let newState = (instance.state = oldState); 622 | processUpdateQueue(workInProgress, newProps, instance, renderLanes); 623 | newState = workInProgress.memoizedState; 624 | // .... 625 | // 调用生命周期返回getDerivedStateFromProps,静态类 626 | if (typeof getDerivedStateFromProps === 'function') { 627 | // 该函数返回partialState。 628 | applyDerivedStateFromProps( 629 | workInProgress, 630 | ctor, 631 | getDerivedStateFromProps, 632 | newProps, 633 | ); 634 | newState = workInProgress.memoizedState; 635 | } 636 | 637 | // 判断是否需要更新 638 | const shouldUpdate = 639 | checkHasForceUpdateAfterProcessing() || 640 | checkShouldComponentUpdate( 641 | workInProgress, 642 | ctor, 643 | oldProps, 644 | newProps, 645 | oldState, 646 | newState, 647 | nextContext, 648 | ) || 649 | (enableLazyContextPropagation && 650 | current !== null && 651 | current.dependencies !== null && 652 | checkIfContextChanged(current.dependencies)); 653 | 654 | if (shouldUpdate) { 655 | // 调用生命周期,componentWillUpdate、和即将失效的componentWillUpdate和兼容版本UNSAFE_componentWillUpdate 656 | if ( 657 | !hasNewLifecycles && 658 | (typeof instance.UNSAFE_componentWillUpdate === 'function' || 659 | typeof instance.componentWillUpdate === 'function') 660 | ) { 661 | if (typeof instance.componentWillUpdate === 'function') { 662 | instance.componentWillUpdate(newProps, newState, nextContext); 663 | } 664 | if (typeof instance.UNSAFE_componentWillUpdate === 'function') { 665 | instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext); 666 | } 667 | } 668 | // ... 669 | } else { 670 | 671 | //... 672 | //赋值新旧Props和state 673 | workInProgress.memoizedProps = newProps; 674 | workInProgress.memoizedState = newState; 675 | } 676 | // if shouldComponentUpdate returns false. 677 | instance.props = newProps; 678 | instance.state = newState; 679 | instance.context = nextContext; 680 | 681 | return shouldUpdate; 682 | } 683 | ``` 684 | 处理新旧`props`和`state`和`context`,调用生命周期,最后将赋值新的值给`instance`。 685 | 686 | ```js 687 | instance.props = newProps; 688 | instance.state = newState; 689 | instance.context = nextContext; 690 | ``` 691 | 692 | 最后调用`finishClassComponent`函数。`finishClassComponent`函数判断`!shouldUpdate && !didCaptureError`,该值如果为true,调用`bailoutOnAlreadyFinishedWork`函数,克隆`cloneChildFibers`,返回`workInProgress.child`。 693 | ```js 694 | export function cloneChildFibers( 695 | current: Fiber | null, 696 | workInProgress: Fiber, 697 | ): void { 698 | 699 | if (workInProgress.child === null) { 700 | return; 701 | } 702 | // 复用 703 | let currentChild = workInProgress.child; 704 | let newChild = createWorkInProgress(currentChild, currentChild.pendingProps); 705 | workInProgress.child = newChild; 706 | 707 | newChild.return = workInProgress; 708 | while (currentChild.sibling !== null) { 709 | currentChild = currentChild.sibling; 710 | newChild = newChild.sibling = createWorkInProgress( 711 | currentChild, 712 | currentChild.pendingProps, 713 | ); 714 | newChild.return = workInProgress; 715 | } 716 | newChild.sibling = null; 717 | } 718 | ``` 719 | 720 | ### 下一篇 completeWork阶段。 721 | 722 | [React技术揭秘](https://react.iamkasong.com/process/beginWork.html) 723 | 724 | [React源码解析](https://react.jokcy.me/book/flow/begin-work.html) 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | -------------------------------------------------------------------------------- /React源码解析之 Fiber结构的创建.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### React源码系列 4 | - [React源码解析之 Fiber结构的创建](https://juejin.cn/post/6951585684679294989) 5 | - [React源码解析 之 Fiber的渲染(1)](https://juejin.cn/post/6951585684679294989) 6 | ## React Fiber的创建 7 | 当前React版本基于V17.0.2版本,本篇主要介绍fiber结构的创建。 8 | 9 | #### 一、开始之前 10 | > 个人理解,如有不对,请指出。 11 | 12 | > 首先需要配置好React的debugger开发环境,入口在这里[:github](https://github.com/baiyuze/debug-react) 13 | 14 | 执行`npm run i`,安装依赖,`npm start`运行环境。 15 | 16 | #### 二、从React.render开始 17 | 18 | 通过在项目入口处调用React.render,打上Debug,查看React调用栈。 19 | ```jsx 20 | const root = document.getElementById('root'); 21 | ReactDOM.render( 22 | 23 | 24 | , 25 | root 26 | ); 27 | ``` 28 | 29 | 在`Reac`t调用`render`之后,在传入基础的配置后,调用`legacyRenderSubtreeIntoContainer`。 30 | ```jsx 31 | export function render( 32 | element: React$Element, 33 | container: Container, 34 | callback: ?Function, 35 | ) { 36 | // 删除一些环境代码 37 | // ... 38 | return legacyRenderSubtreeIntoContainer( 39 | null, 40 | element, 41 | container, 42 | false, 43 | callback, 44 | ); 45 | } 46 | ``` 47 | 48 | `legacyRenderSubtreeIntoContainer`一共做了两件事情,一个是生成了`fiberRoot`,一个是调用`updateContainer`。 49 | 50 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5339a4c530324144aec8a11c51e1359e~tplv-k3u1fbpfcp-watermark.image) 51 | 52 | 进入`legacyCreateRootFromDOMContainer`函数,查看如何生成fiberRoot。 53 | 在函数内部,调用了`createLegacyRoot`,在这里区分了下,是否使用`hydrate`,如下: 54 | ```jsx 55 | return createLegacyRoot( 56 | container, 57 | shouldHydrate 58 | ? { 59 | hydrate: true, 60 | } 61 | : undefined, 62 | ); 63 | ``` 64 | 65 | 对于`createLegacyRoot`来说,是用来实例化`ReactDOMLegacyRoot`函数的,通过后续调用,终于进入到`root`的生成,调用`createRootImpl`函数,实例化`root`。 66 | 67 | 进入`createFiberRoot`函数,初始化`FiberRootNode`。 68 | ```js 69 | function FiberRootNode(containerInfo, tag, hydrate) { 70 | this.tag = tag; // 类型 71 | this.containerInfo = containerInfo; // container 72 | this.pendingChildren = null; 73 | this.current = null; 74 | this.pingCache = null; 75 | this.finishedWork = null; 76 | this.timeoutHandle = noTimeout; 77 | this.context = null; 78 | this.pendingContext = null; 79 | this.hydrate = hydrate; 80 | this.callbackNode = null; 81 | this.callbackPriority = NoLanePriority; 82 | this.eventTimes = createLaneMap(NoLanes); 83 | this.expirationTimes = createLaneMap(NoTimestamp); 84 | 85 | this.pendingLanes = NoLanes; 86 | this.suspendedLanes = NoLanes; 87 | this.pingedLanes = NoLanes; 88 | this.mutableReadLanes = NoLanes; 89 | this.finishedLanes = NoLanes; 90 | 91 | this.entangledLanes = NoLanes; 92 | this.entanglements = createLaneMap(NoLanes); 93 | 94 | // .... 95 | 96 | 97 | } 98 | ``` 99 | 这里的tag,有以下几种类型。 100 | ```js 101 | export type RootTag = 0 | 1; 102 | ``` 103 | 上述的结构是fiberRootNode节点。 104 | 105 | `rootTag` 等于**0** 时,代表`legacy`渲染模式,等于1时,代表`Concurrent mode`渲染,也就是说,传统我们使用`React.render`进行渲染,当调用`React.createRoot`时,进入`Concurrent mode`渲染模式,即**并行渲染**。 106 | 107 | 现在我们一起看看`fiber`的结构。 108 | ```js 109 | const uninitializedFiber = createHostRootFiber(tag, strictModeLevelOverride); 110 | root.current = uninitializedFiber; 111 | uninitializedFiber.stateNode = root; 112 | ``` 113 | `uninitializedFiber`为创建的`FiberNode`的创建的实例。 114 | 115 | ```js 116 | const createFiber = function( 117 | tag: WorkTag, 118 | pendingProps: mixed, 119 | key: null | string, 120 | mode: TypeOfMode, 121 | ): Fiber { 122 | // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors 123 | return new FiberNode(tag, pendingProps, key, mode); 124 | }; 125 | ``` 126 | 通过基础的创建,生成`FiberNode`结构,如下 127 | ```js 128 | 129 | function FiberNode( 130 | tag: WorkTag, 131 | pendingProps: mixed, 132 | key: null | string, 133 | mode: TypeOfMode, 134 | ) { 135 | // Instance 136 | this.tag = tag;//组件类型 137 | this.key = key;//key属性 138 | this.elementType = null;//元素类型,类函数,显示类,div显示div 139 | this.type = null;//func或者class 140 | this.stateNode = null;//dom节点 141 | 142 | // Fiber 143 | this.return = null;//指向父节点 144 | this.child = null;//指向子节点 145 | this.sibling = null;//兄弟节点 146 | this.index = 0;// 147 | 148 | this.ref = null; 149 | 150 | this.pendingProps = pendingProps;//等待中的属性pendingProps 151 | this.memoizedProps = null; //记忆属性,一般存放props 152 | this.updateQueue = null;//更新队列 153 | this.memoizedState = null;// 一般存放state 154 | this.dependencies = null; 155 | 156 | this.mode = mode; 157 | 158 | // Effects相关 159 | this.flags = NoFlags; 160 | this.subtreeFlags = NoFlags; 161 | this.deletions = null; 162 | 163 | this.lanes = NoLanes; 164 | this.childLanes = NoLanes; 165 | 166 | this.alternate = null;//指向workInProgress 167 | } 168 | ``` 169 | 170 | 171 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/72a68e717cbf4da4be2b32a208f834f8~tplv-k3u1fbpfcp-watermark.image) 172 | `FiberNode`基本显示如上,`elementType`和`type`的基础类型为`function、class`。 173 | 174 | 通过对比`fiberRootNode`结构,和下面的代码,生成最终的`FiberNode` 结构。 175 | 176 | ```js 177 | render() { 178 | const { name, count } = this.state; 179 | return ( 180 |
181 |
186 | ); 187 | } 188 | ``` 189 | ```js 190 | ReactDOM.render( 191 | 192 | 193 | , 194 | root 195 | ); 196 | ``` 197 | 198 | 通过最后执行,生成`fiberRoot`链表结构。 199 | 200 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/514f6a846c3c40ff81ec87850e598eaf~tplv-k3u1fbpfcp-watermark.image) 201 | 202 | 203 | 最后,调用`unbatchedUpdates`,进行渲染。 204 | 205 | 进入`updateContainer`函数。 206 | ```jsx 207 | unbatchedUpdates(() => { 208 | // 更新container 209 | updateContainer(children, fiberRoot, parentComponent, callback); 210 | }); 211 | ``` 212 | 213 | #### 三、结束 214 | 215 | 下一篇:`fiberRoot`的渲染 216 | 217 | - 阶段一:生成`workInProgress`。 218 | - 阶段二:调用`workLoopSync`,执行`beginWork`。 219 | - 阶段三:执行`completeWork`。 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | -------------------------------------------------------------------------------- /一次弄懂Event Loop(彻底解决此类面试问题).md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | `Event Loop`即事件循环,是指浏览器或`Node`的一种解决`javaScript`单线程运行时不会阻塞的一种机制,也就是我们经常使用**异步**的原理。 3 | ## 为啥要弄懂Event Loop 4 | * 是要增加自己技术的深度,也就是懂得`JavaScript`的运行机制。 5 | 6 | * 现在在前端领域各种技术层出不穷,掌握底层原理,可以让自己以不变,应万变。 7 | 8 | * 应对各大互联网公司的面试,懂其原理,题目任其发挥。 9 | 10 | ## 堆,栈、队列 11 | 12 | 13 | ![](https://user-gold-cdn.xitu.io/2019/1/17/16859c984806c78d?w=294&h=271&f=png&s=7649) 14 | ### 堆(Heap) 15 | 16 | **堆**是一种数据结构,是利用完全二叉树维护的一组数据,**堆**分为两种,一种为最大**堆**,一种为**最小堆**,将根节点**最大**的**堆**叫做**最大堆**或**大根堆**,根节点**最小**的**堆**叫做**最小堆**或**小根堆**。 17 | **堆**是**线性数据结构**,相当于**一维数组**,有唯一后继。 18 | 19 | 如最大堆 20 | 21 | ![](https://user-gold-cdn.xitu.io/2019/1/17/16859dbb5b9c7ca1?w=190&h=161&f=png&s=10625) 22 | 23 | ### 栈(Stack) 24 | **栈**在计算机科学中是限定仅在**表尾**进行**插入**或**删除**操作的线性表。 **栈**是一种数据结构,它按照**后进先出**的原则存储数据,**先进入**的数据被压入**栈底**,**最后的数据**在**栈顶**,需要读数据的时候从**栈顶**开始**弹出数据**。 25 | **栈**是只能在**某一端插入**和**删除**的**特殊线性表**。 26 | 27 | 28 | ![](https://user-gold-cdn.xitu.io/2019/1/17/16859ed4f6143043?w=616&h=282&f=png&s=111894) 29 | 30 | ### 队列(Queue) 31 | 特殊之处在于它只允许在表的前端(`front`)进行**删除**操作,而在表的后端(`rear`)进行**插入**操作,和**栈**一样,**队列**是一种操作受限制的线性表。 32 | 进行**插入**操作的端称为**队尾**,进行**删除**操作的端称为**队头**。 队列中没有元素时,称为**空队列**。 33 | 34 | **队列**的数据元素又称为**队列元素**。在队列中插入一个队列元素称为**入队**,从**队列**中**删除**一个队列元素称为**出队**。因为队列**只允许**在一端**插入**,在另一端**删除**,所以只有**最早**进入**队列**的元素**才能最先从队列中**删除,故队列又称为**先进先出**(`FIFO—first in first out`) 35 | 36 | 37 | ![](https://user-gold-cdn.xitu.io/2019/1/17/16859f2f4f5da2a8?w=554&h=270&f=png&s=75189) 38 | 39 | ## Event Loop 40 | 41 | 在`JavaScript`中,任务被分为两种,一种宏任务(`MacroTask`)也叫`Task`,一种叫微任务(`MicroTask`)。 42 | ### MacroTask(宏任务) 43 | 44 | * `script`全部代码、`setTimeout`、`setInterval`、`setImmediate`(浏览器暂时不支持,只有IE10支持,具体可见[`MDN`](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/setImmediate))、`I/O`、`UI Rendering`。 45 | 46 | ### MicroTask(微任务) 47 | 48 | * `Process.nextTick(Node独有)`、`Promise`、`Object.observe(废弃)`、`MutationObserver`(具体使用方式查看[这里](http://javascript.ruanyifeng.com/dom/mutationobserver.html)) 49 | 50 | ## 浏览器中的Event Loop 51 | 52 | `Javascript` 有一个 `main thread` 主线程和 `call-stack` 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。 53 | 54 | ### JS调用栈 55 | JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。 56 | 57 | ### 同步任务和异步任务 58 | `Javascript`单线程任务被分为**同步任务**和**异步任务**,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。 59 | 60 | ![](https://user-gold-cdn.xitu.io/2019/1/18/1685f03d7f88792b?w=636&h=518&f=png&s=51005) 61 | 任务队列`Task Queue`,即队列,是一种先进先出的一种数据结构。 62 | 63 | 64 | ![](https://user-gold-cdn.xitu.io/2019/1/18/1685f037d48da0de?w=800&h=669&f=png&s=278649) 65 | 66 | ### 事件循环的进程模型 67 | 68 | * 选择当前要执行的任务队列,选择任务队列中最先进入的任务,如果任务队列为空即`null`,则执行跳转到微任务(`MicroTask`)的执行步骤。 69 | * 将事件循环中的任务设置为已选择任务。 70 | * 执行任务。 71 | * 将事件循环中当前运行任务设置为null。 72 | * 将已经运行完成的任务从任务队列中删除。 73 | * microtasks步骤:进入microtask检查点。 74 | * 更新界面渲染。 75 | * 返回第一步。 76 | 77 | ### 执行进入microtask检查点时,用户代理会执行以下步骤: 78 | 79 | * 设置microtask检查点标志为true。 80 | * 当事件循环`microtask`执行不为空时:选择一个最先进入的`microtask`队列的`microtask`,将事件循环的`microtask`设置为已选择的`microtask`,运行`microtask`,将已经执行完成的`microtask`为`null`,移出`microtask`中的`microtask`。 81 | * 清理IndexDB事务 82 | * 设置进入microtask检查点的标志为false。 83 | 84 | 上述可能不太好理解,下图是我做的一张图片。 85 | 86 | ![](https://user-gold-cdn.xitu.io/2019/1/18/1686078c7a2f63e5?w=1011&h=589&f=gif&s=264204) 87 | 88 | 89 | 执行栈在执行完**同步任务**后,查看**执行栈**是否为空,如果执行栈为空,就会去执行`Task`(宏任务),每次**宏任务**执行完毕后,检查**微任务**(`microTask`)队列是否为空,如果不为空的话,会按照**先入先**出的规则全部执行完**微任务**(`microTask`)后,设置**微任务**(`microTask`)队列为`null`,然后再执行**宏任务**,如此循环。 90 | 91 | ## 举个例子 92 | ```js 93 | console.log('script start'); 94 | 95 | setTimeout(function() { 96 | console.log('setTimeout'); 97 | }, 0); 98 | 99 | Promise.resolve().then(function() { 100 | console.log('promise1'); 101 | }).then(function() { 102 | console.log('promise2'); 103 | }); 104 | console.log('script end'); 105 | ``` 106 | 107 | 首先我们划分几个分类: 108 | ### 第一次执行: 109 | ```js 110 | Tasks:run script、 setTimeout callback 111 | 112 | Microtasks:Promise then 113 | 114 | JS stack: script 115 | Log: script start、script end。 116 | ``` 117 | 执行同步代码,将宏任务(`Tasks`)和微任务(`Microtasks`)划分到各自队列中。 118 | ### 第二次执行: 119 | ```js 120 | Tasks:run script、 setTimeout callback 121 | 122 | Microtasks:Promise2 then 123 | 124 | JS stack: Promise2 callback 125 | Log: script start、script end、promise1、promise2 126 | ``` 127 | 执行宏任务后,检测到微任务(`Microtasks`)队列中不为空,执行`Promise1`,执行完成`Promise1`后,调用`Promise2.then`,放入微任务(`Microtasks`)队列中,再执行`Promise2.then`。 128 | 129 | ### 第三次执行: 130 | ```js 131 | Tasks:setTimeout callback 132 | 133 | Microtasks: 134 | 135 | JS stack: setTimeout callback 136 | Log: script start、script end、promise1、promise2、setTimeout 137 | ``` 138 | 当微任务(`Microtasks`)队列中为空时,执行宏任务(`Tasks`),执行`setTimeout callback`,打印日志。 139 | ### 第四次执行: 140 | 141 | ```js 142 | Tasks:setTimeout callback 143 | 144 | Microtasks: 145 | 146 | JS stack: 147 | Log: script start、script end、promise1、promise2、setTimeout 148 | ``` 149 | 清空**Tasks**队列和`JS stack`。 150 | 151 | 以上执行帧动画可以查看[Tasks, microtasks, queues and schedules](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) 152 | 或许这张图也更好理解些。 153 | 154 | 155 | ![](https://user-gold-cdn.xitu.io/2019/1/18/16860ae5ad02f993?w=611&h=341&f=gif&s=722979) 156 | ## 再举个例子 157 | ```js 158 | console.log('script start') 159 | 160 | async function async1() { 161 | await async2() 162 | console.log('async1 end') 163 | } 164 | async function async2() { 165 | console.log('async2 end') 166 | } 167 | async1() 168 | 169 | setTimeout(function() { 170 | console.log('setTimeout') 171 | }, 0) 172 | 173 | new Promise(resolve => { 174 | console.log('Promise') 175 | resolve() 176 | }) 177 | .then(function() { 178 | console.log('promise1') 179 | }) 180 | .then(function() { 181 | console.log('promise2') 182 | }) 183 | 184 | console.log('script end') 185 | ``` 186 | 这里需要先理解`async/await`。 187 | 188 | `async/await` 在底层转换成了 `promise` 和 `then` 回调函数。 189 | 也就是说,这是 `promise` 的语法糖。 190 | 每次我们使用 `await`, 解释器都创建一个 `promise` 对象,然后把剩下的 `async` 函数中的操作放到 `then` 回调函数中。 191 | `async/await` 的实现,离不开 `Promise`。从字面意思来理解,`async` 是“异步”的简写,而 `await` 是 `async wait` 的简写可以认为是等待异步方法执行完成。 192 | 193 | ### **关于73以下版本和73版本的区别** 194 | * 在73版本以下,先执行`promise1`和`promise2`,再执行`async1`。 195 | * 在73版本,先执行`async1`再执行`promise1`和`promise2`。 196 | 197 | **主要原因是因为在谷歌(金丝雀)73版本中更改了规范,如下图所示:** 198 | 199 | ![](https://user-gold-cdn.xitu.io/2019/1/21/1686eb29a6a19658?w=668&h=243&f=png&s=24331) 200 | * 区别在于`RESOLVE(thenable)`和之间的区别`Promise.resolve(thenable)`。 201 | 202 | ### **在73以下版本中** 203 | * 首先,传递给 `await` 的值被包裹在一个 `Promise` 中。然后,处理程序附加到这个包装的 `Promise`,以便在 `Promise` 变为 `fulfilled` 后恢复该函数,并且暂停执行异步函数,一旦 `promise` 变为 `fulfilled`,恢复异步函数的执行。 204 | * 每个 `await` 引擎必须创建两个额外的 Promise(即使右侧已经是一个 `Promise`)并且它需要至少三个 `microtask` 队列 `ticks`(`tick`为系统的相对时间单位,也被称为系统的时基,来源于定时器的周期性中断(输出脉冲),一次中断表示一个`tick`,也被称做一个“时钟滴答”、时标。)。 205 | 206 | 207 | ### **引用贺老师知乎上的一个例子** 208 | ```js 209 | async function f() { 210 | await p 211 | console.log('ok') 212 | } 213 | ``` 214 | 简化理解为: 215 | ```js 216 | 217 | function f() { 218 | return RESOLVE(p).then(() => { 219 | console.log('ok') 220 | }) 221 | } 222 | ``` 223 | * 如果 `RESOLVE(p)` 对于 `p` 为 `promise` 直接返回 `p` 的话,那么 `p `的 `then` 方法就会被马上调用,其回调就立即进入 `job` 队列。 224 | * 而如果 `RESOLVE(p)` 严格按照标准,应该是产生一个新的 `promise`,尽管该 `promise `确定会 `resolve` 为 `p`,但这个过程本身是异步的,也就是现在进入 `job` 队列的是新 `promise` 的 `resolve `过程,所以该 `promise` 的 `then` 不会被立即调用,而要等到当前 `job` 队列执行到前述 `resolve` 过程才会被调用,然后其回调(也就是继续 `await` 之后的语句)才加入 `job` 队列,所以时序上就晚了。 225 | 226 | ### **谷歌(金丝雀)73版本中** 227 | * 使用对`PromiseResolve`的调用来更改`await`的语义,以减少在公共`awaitPromise`情况下的转换次数。 228 | * 如果传递给 `await` 的值已经是一个 `Promise`,那么这种优化避免了再次创建 `Promise` 包装器,在这种情况下,我们从最少三个 `microtick` 到只有一个 `microtick`。 229 | 230 | ### **详细过程:** 231 | 232 | **73以下版本** 233 | * 首先,打印`script start`,调用`async1()`时,返回一个`Promise`,所以打印出来`async2 end`。 234 | * 每个 `await`,会新产生一个`promise`,但这个过程本身是异步的,所以该`await`后面不会立即调用。 235 | * 继续执行同步代码,打印`Promise`和`script end`,将`then`函数放入**微任务**队列中等待执行。 236 | * 同步执行完成之后,检查**微任务**队列是否为`null`,然后按照先入先出规则,依次执行。 237 | * 然后先执行打印`promise1`,此时`then`的回调函数返回`undefinde`,此时又有`then`的链式调用,又放入**微任务**队列中,再次打印`promise2`。 238 | * 再回到`await`的位置执行返回的 `Promise` 的 `resolve` 函数,这又会把 `resolve` 丢到微任务队列中,打印`async1 end`。 239 | * 当**微任务**队列为空时,执行宏任务,打印`setTimeout`。 240 | 241 | **谷歌(金丝雀73版本)** 242 | 243 | * 如果传递给 `await` 的值已经是一个 `Promise`,那么这种优化避免了再次创建 `Promise` 包装器,在这种情况下,我们从最少三个 `microtick` 到只有一个 `microtick`。 244 | * 引擎不再需要为 `await` 创造 `throwaway Promise` - 在绝大部分时间。 245 | * 现在 `promise` 指向了同一个 `Promise`,所以这个步骤什么也不需要做。然后引擎继续像以前一样,创建 `throwaway Promise`,安排 `PromiseReactionJob` 在 `microtask` 队列的下一个 `tick` 上恢复异步函数,暂停执行该函数,然后返回给调用者。 246 | 247 | 具体详情查看([这里](https://v8.js.cn/blog/fast-async/))。 248 | 249 | ## NodeJS的Event Loop 250 | 251 | 252 | ![](https://user-gold-cdn.xitu.io/2019/1/18/16860f35d3a3e50d?w=543&h=223&f=png&s=93018) 253 | 254 | `Node`中的`Event Loop`是基于`libuv`实现的,而`libuv`是 `Node` 的新跨平台抽象层,libuv使用异步,事件驱动的编程方式,核心是提供`i/o`的事件循环和异步回调。libuv的`API`包含有时间,非阻塞的网络,异步文件操作,子进程等等。 255 | `Event Loop`就是在`libuv`中实现的。 256 | 257 | 258 | ![](https://user-gold-cdn.xitu.io/2019/1/18/16860f8f8f7f053d?w=745&h=442&f=png&s=43869) 259 | 260 | ### `Node`的`Event loop`一共分为6个阶段,每个细节具体如下: 261 | * `timers`: 执行`setTimeout`和`setInterval`中到期的`callback`。 262 | * `pending callback`: 上一轮循环中少数的`callback`会放在这一阶段执行。 263 | * `idle, prepare`: 仅在内部使用。 264 | * `poll`: 最重要的阶段,执行`pending callback`,在适当的情况下回阻塞在这个阶段。 265 | * `check`: 执行`setImmediate`(`setImmediate()`是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行`setImmediate`指定的回调函数)的`callback`。 266 | * `close callbacks`: 执行`close`事件的`callback`,例如`socket.on('close'[,fn])`或者`http.server.on('close, fn)`。 267 | 268 | 具体细节如下: 269 | ### timers 270 | 执行`setTimeout`和`setInterval`中到期的`callback`,执行这两者回调需要设置一个毫秒数,理论上来说,应该是时间一到就立即执行callback回调,但是由于`system`的调度可能会延时,达不到预期时间。 271 | 以下是官网文档解释的例子: 272 | 273 | ```js 274 | const fs = require('fs'); 275 | 276 | function someAsyncOperation(callback) { 277 | // Assume this takes 95ms to complete 278 | fs.readFile('/path/to/file', callback); 279 | } 280 | 281 | const timeoutScheduled = Date.now(); 282 | 283 | setTimeout(() => { 284 | const delay = Date.now() - timeoutScheduled; 285 | 286 | console.log(`${delay}ms have passed since I was scheduled`); 287 | }, 100); 288 | 289 | 290 | // do someAsyncOperation which takes 95 ms to complete 291 | someAsyncOperation(() => { 292 | const startCallback = Date.now(); 293 | 294 | // do something that will take 10ms... 295 | while (Date.now() - startCallback < 10) { 296 | // do nothing 297 | } 298 | }); 299 | ``` 300 | 301 | 当进入事件循环时,它有一个空队列(`fs.readFile()`尚未完成),因此定时器将等待剩余毫秒数,当到达95ms时,`fs.readFile()`完成读取文件并且其完成需要10毫秒的回调被添加到轮询队列并执行。 302 | 当回调结束时,队列中不再有回调,因此事件循环将看到已达到最快定时器的**阈值**,然后回到**timers阶段**以执行定时器的回调。 303 | 304 | 在此示例中,您将看到正在调度的计时器与正在执行的回调之间的总延迟将为105毫秒。 305 | 306 | **以下是我测试时间:** 307 | ![](https://user-gold-cdn.xitu.io/2019/1/19/16864b8177c25eaf?w=724&h=430&f=png&s=79911) 308 | 309 | ### pending callbacks 310 | 311 | 此阶段执行某些系统操作(例如TCP错误类型)的回调。 例如,如果`TCP socket ECONNREFUSED`在尝试connect时receives,则某些* nix系统希望等待报告错误。 312 | 这将在`pending callbacks`阶段执行。 313 | 314 | ### poll 315 | **该poll阶段有两个主要功能:** 316 | 317 | * 执行`I/O`回调。 318 | * 处理轮询队列中的事件。 319 | 320 | **当事件循环进入`poll`阶段并且在`timers`中没有可以执行定时器时,将发生以下两种情况之一** 321 | 322 | * 如果`poll`队列不为空,则事件循环将遍历其同步执行它们的`callback`队列,直到队列为空,或者达到`system-dependent`(系统相关限制)。 323 | 324 | **如果`poll`队列为空,则会发生以下两种情况之一** 325 | 326 | * 如果有`setImmediate()`回调需要执行,则会立即停止执行`poll`阶段并进入执行`check`阶段以执行回调。 327 | 328 | * 如果没有`setImmediate()`回到需要执行,poll阶段将等待`callback`被添加到队列中,然后立即执行。 329 | 330 | **当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。** 331 | 332 | 333 | ### check 334 | 335 | **此阶段允许人员在poll阶段完成后立即执行回调。** 336 | 如果`poll`阶段闲置并且`script`已排队`setImmediate()`,则事件循环到达check阶段执行而不是继续等待。 337 | 338 | `setImmediate()`实际上是一个特殊的计时器,它在事件循环的一个单独阶段运行。它使用`libuv API`来调度在`poll`阶段完成后执行的回调。 339 | 340 | 通常,当代码被执行时,事件循环最终将达到`poll`阶段,它将等待传入连接,请求等。 341 | 但是,如果已经调度了回调`setImmediate()`,并且轮询阶段变为空闲,则它将结束并且到达`check`阶段,而不是等待`poll`事件。 342 | 343 | ```js 344 | console.log('start') 345 | setTimeout(() => { 346 | console.log('timer1') 347 | Promise.resolve().then(function() { 348 | console.log('promise1') 349 | }) 350 | }, 0) 351 | setTimeout(() => { 352 | console.log('timer2') 353 | Promise.resolve().then(function() { 354 | console.log('promise2') 355 | }) 356 | }, 0) 357 | Promise.resolve().then(function() { 358 | console.log('promise3') 359 | }) 360 | console.log('end') 361 | ``` 362 | 如果`node`版本为`v11.x`, 363 | 其结果与浏览器一致。 364 | ```js 365 | start 366 | end 367 | promise3 368 | timer1 369 | timer2 370 | promise1 371 | promise2 372 | ``` 373 | 具体详情可以查看《[又被node的eventloop坑了,这次是node的锅](https://juejin.im/post/5c3e8d90f265da614274218a)》。 374 | 375 | 如果v10版本上述结果存在两种情况: 376 | * 如果time2定时器已经在执行队列中了,那么执行结果与上面结果相同。 377 | * 如果time2定时器没有在执行对列中,执行结果为 378 | ```js 379 | start 380 | end 381 | promise3 382 | timer1 383 | promise1 384 | timer2 385 | ``` 386 | 具体情况可以参考`poll`阶段的两种情况。 387 | 388 | 从下图可能更好理解: 389 | 390 | ![](https://user-gold-cdn.xitu.io/2019/1/19/1686530bcd4e456a?w=598&h=333&f=gif&s=467665) 391 | 392 | ## setImmediate() 的setTimeout()的区别 393 | 394 | **`setImmediate`和`setTimeout()`是相似的,但根据它们被调用的时间以不同的方式表现。** 395 | 396 | * `setImmediate()`设计用于在当前`poll`阶段完成后check阶段执行脚本 。 397 | * `setTimeout()` 安排在经过最小(ms)后运行的脚本,在`timers`阶段执行。 398 | ### 举个例子 399 | ```js 400 | setTimeout(() => { 401 | console.log('timeout'); 402 | }, 0); 403 | 404 | setImmediate(() => { 405 | console.log('immediate'); 406 | }); 407 | ``` 408 | **执行定时器的顺序将根据调用它们的上下文而有所不同。 如果从主模块中调用两者,那么时间将受到进程性能的限制。** 409 | 410 | **其结果也不一致** 411 | 412 | **如果在`I / O`周期内移动两个调用,则始终首先执行立即回调:** 413 | ```js 414 | const fs = require('fs'); 415 | 416 | fs.readFile(__filename, () => { 417 | setTimeout(() => { 418 | console.log('timeout'); 419 | }, 0); 420 | setImmediate(() => { 421 | console.log('immediate'); 422 | }); 423 | }); 424 | ``` 425 | 426 | 其结果可以确定一定是`immediate => timeout`。 427 | 主要原因是在`I/O阶段`读取文件后,事件循环会先进入`poll`阶段,发现有`setImmediate`需要执行,会立即进入`check`阶段执行`setImmediate`的回调。 428 | 429 | 然后再进入`timers`阶段,执行`setTimeout`,打印`timeout`。 430 | 431 | ```js 432 | ┌───────────────────────────┐ 433 | ┌─>│ timers │ 434 | │ └─────────────┬─────────────┘ 435 | │ ┌─────────────┴─────────────┐ 436 | │ │ pending callbacks │ 437 | │ └─────────────┬─────────────┘ 438 | │ ┌─────────────┴─────────────┐ 439 | │ │ idle, prepare │ 440 | │ └─────────────┬─────────────┘ ┌───────────────┐ 441 | │ ┌─────────────┴─────────────┐ │ incoming: │ 442 | │ │ poll │<─────┤ connections, │ 443 | │ └─────────────┬─────────────┘ │ data, etc. │ 444 | │ ┌─────────────┴─────────────┐ └───────────────┘ 445 | │ │ check │ 446 | │ └─────────────┬─────────────┘ 447 | │ ┌─────────────┴─────────────┐ 448 | └──┤ close callbacks │ 449 | └───────────────────────────┘ 450 | ``` 451 | 452 | ## Process.nextTick() 453 | **`process.nextTick()`虽然它是异步API的一部分,但未在图中显示。这是因为`process.nextTick()`从技术上讲,它不是事件循环的一部分。** 454 | 455 | * `process.nextTick()`方法将 `callback` 添加到`next tick`队列。 一旦当前事件轮询队列的任务全部完成,在`next tick`队列中的所有`callbacks`会被依次调用。 456 | 457 | **换种理解方式:** 458 | * 当每个阶段完成后,如果存在 `nextTick` 队列,就会清空队列中的所有回调函数,并且优先于其他 `microtask` 执行。 459 | 460 | 461 | ### 例子 462 | ```js 463 | let bar; 464 | 465 | setTimeout(() => { 466 | console.log('setTimeout'); 467 | }, 0) 468 | 469 | setImmediate(() => { 470 | console.log('setImmediate'); 471 | }) 472 | function someAsyncApiCall(callback) { 473 | process.nextTick(callback); 474 | } 475 | 476 | someAsyncApiCall(() => { 477 | console.log('bar', bar); // 1 478 | }); 479 | 480 | bar = 1; 481 | ``` 482 | 在NodeV10中上述代码执行可能有两种答案,一种为: 483 | ``` 484 | bar 1 485 | setTimeout 486 | setImmediate 487 | ``` 488 | 另一种为: 489 | 490 | ``` 491 | bar 1 492 | setImmediate 493 | setTimeout 494 | ``` 495 | 无论哪种,始终都是先执行`process.nextTick(callback)`,打印`bar 1`。 496 | 497 | 498 | 499 | ## 最后 500 | 501 | 感谢@Dante_Hu提出这个问题`await`的问题,文章已经修正。 502 | 503 | ### **关于await问题参考了以下文章:**. 504 | 505 | 《[promise, async, await, execution order](https://github.com/xianshenglu/blog/issues/60)》 506 | 《[Normative: Reduce the number of ticks in async/await](https://github.com/tc39/ecma262/pull/1250)》 507 | 《[async/await 在chrome 环境和 node 环境的 执行结果不一致,求解?](https://www.zhihu.com/question/268007969)》 508 | 《[更快的异步函数和 Promise](https://v8.js.cn/blog/fast-async/)》 509 | ### 其他内容参考了: 510 | 511 | 《[JS浏览器事件循环机制](https://segmentfault.com/a/1190000015559210)》 512 | 《[什么是浏览器的事件循环(Event Loop)?](https://segmentfault.com/a/1190000010622146)》 513 | 《[一篇文章教会你Event loop——浏览器和Node](https://segmentfault.com/a/1190000013861128)》 514 | 《[不要混淆nodejs和浏览器中的event loop](https://cnodejs.org/topic/5a9108d78d6e16e56bb80882)》 515 | 《[浏览器与Node的事件循环(Event Loop)有何区别?](https://juejin.im/post/5c337ae06fb9a049bc4cd218)》 516 | 《[Tasks, microtasks, queues and schedules](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/)》 517 | 《[前端面试之道](https://juejin.im/book/5bdc715fe51d454e755f75ef/section/5be04a8e6fb9a04a072fd2cd#heading-3)》 518 | 《[Node.js介绍5-libuv的基本概念](https://www.jianshu.com/p/8e0ad01c41dc)》 519 | 《[The Node.js Event Loop, Timers, and process.nextTick()](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/)》 520 | 《[node官网](http://nodejs.cn/)》 521 | -------------------------------------------------------------------------------- /从源码上理解express中间件.md: -------------------------------------------------------------------------------- 1 | **本篇文章从express源码上来理解中间件,同时可以对express有更深层的理解** 2 | 3 | ## 前言 4 | 中间件函数可以执行哪些任务? 5 | > * 执行任何代码。 6 | > * 对请求和响应对象进行更改。 7 | > * 结束请求/响应循环。 8 | > * 调用堆栈中的下一个中间件函数。 9 | 10 | 我们从一个`app.use`开始,逐步分析到下一个中间件函数的执行。 11 | 12 | ## 初始化服务器 13 | 14 | 首先从[github](https://github.com/expressjs/express)上下载`express`源码。 15 | 16 | 建立一个文件`test.js`文件,引入根目录的`index.js`文件,实例化`express`,启动服务器。 17 | 18 | ```javascript 19 | 20 | let express = require('../index.js'); 21 | let app = express() 22 | 23 | function middlewareA(req, res, next) { 24 | console.log('A1'); 25 | next(); 26 | console.log('A2'); 27 | } 28 | 29 | function middlewareB(req, res, next) { 30 | console.log('B1'); 31 | next(); 32 | console.log('B2'); 33 | } 34 | 35 | function middlewareC(req, res, next) { 36 | console.log('C1'); 37 | next(); 38 | console.log('C2'); 39 | } 40 | 41 | app.use(middlewareA); 42 | app.use(middlewareB); 43 | app.use(middlewareC); 44 | 45 | app.listen(8888, () => { 46 | console.log("服务器已经启动访问http://127.0.0.1:8888"); 47 | }) 48 | 49 | ``` 50 | 51 | 启动服务器,通过访问`http://127.0.0.1:8888`服务,打开终端,看看终端日志执行顺序。 52 | 53 | 54 | ![](https://user-gold-cdn.xitu.io/2018/12/14/167ad30126227a1d?w=1424&h=244&f=png&s=80240) 55 | 56 | 从日志我们可以看出,每次`next()`之后,都会按照顺序依次调用下中间件函数,然后按照执行顺序依次打印`A1,B1,C1`,此时中间件已经调用完成,再依次打印`C2,B2,A2`。 57 | 58 | ## 目录结构 59 | ```json 60 | --lib 61 | |__ middleware 62 | |__ init.js 63 | |__ query.js 64 | |__ router 65 | |__ index.js 66 | |__ layer.js 67 | |__ route.js 68 | |__ application.js 69 | |__ express.js 70 | |__ request.js 71 | |__ response.js 72 | |__ utils.js 73 | |__ view.js 74 | 75 | ``` 76 | 77 | 通过实例化的express,我们可以看到,`index.js`文件实际上是暴露出`lib/express`的文件。 78 | 79 | ## 实例化express 80 | 81 | `express`,通过`mixin`继承appLication,同时初始化application。 82 | 83 | ```javascript 84 | 85 | function createApplication() { 86 | var app = function(req, res, next) { 87 | app.handle(req, res, next); 88 | }; 89 | 90 | mixin(app, EventEmitter.prototype, false); 91 | mixin(app, proto, false); 92 | 93 | // expose the prototype that will get set on requests 94 | app.request = Object.create(req, { 95 | app: { configurable: true, enumerable: true, writable: true, value: app } 96 | }) 97 | 98 | // expose the prototype that will get set on responses 99 | app.response = Object.create(res, { 100 | app: { configurable: true, enumerable: true, writable: true, value: app } 101 | }) 102 | 103 | app.init(); 104 | return app; 105 | } 106 | 107 | ``` 108 | 109 | 而mixin是`merge-descriptors`npm模块。*Merge objects using descriptors.*。 110 | 111 | 打开`application.js`文件,发现`express`的实例化源自`var app = exports = module.exports = {}`。 112 | 进一步搜索`app.use`,找到`app.use`,而`app.use`又只是向应用程序路由器添加中间件的`Proxy`。 113 | 114 | ```javascript 115 | 116 | /** 117 | * Proxy `Router#use()` to add middleware to the app router. 118 | * See Router#use() documentation for details. 119 | * 120 | * If the _fn_ parameter is an express app, then it will be 121 | * mounted at the _route_ specified. 122 | * 123 | * @public 124 | */ 125 | 126 | app.use = function use(fn) { 127 | var offset = 0; 128 | var path = '/'; 129 | 130 | // 默认path 为 '/' 131 | // app.use([fn]) 132 | //判断app.use传进来的是否是函数 133 | if (typeof fn !== 'function') { 134 | var arg = fn; 135 | 136 | while (Array.isArray(arg) && arg.length !== 0) { 137 | arg = arg[0]; 138 | } 139 | 140 | // 第一个参数是路径 141 | //取出第一个参数,将第一个参数赋值给path。 142 | if (typeof arg !== 'function') { 143 | offset = 1; 144 | path = fn; 145 | } 146 | } 147 | //slice.call(arguments,offset),通过slice转为数据,slice可以改变具有length的类数组。 148 | //arguments是一个类数组对象。 149 | 150 | //处理多种中间件使用方式。 151 | // app.use(r1, r2); 152 | // app.use('/', [r1, r2]); 153 | // app.use(mw1, [mw2, r1, r2], subApp); 154 | 155 | var fns = flatten(slice.call(arguments, offset));//[funtion] 156 | 157 | //抛出错误 158 | if (fns.length === 0) { 159 | throw new TypeError('app.use() requires a middleware function') 160 | } 161 | 162 | //设置router 163 | this.lazyrouter(); 164 | var router = this._router; 165 | 166 | fns.forEach(function (fn) { 167 | // 处理不是express的APP应用的情况,直接调用route.use。 168 | if (!fn || !fn.handle || !fn.set) { 169 | //path default to '/' 170 | return router.use(path, fn); 171 | } 172 | 173 | debug('.use app under %s', path); 174 | fn.mountpath = path; 175 | fn.parent = this; 176 | router.use(path, function mounted_app(req, res, next) { 177 | var orig = req.app; 178 | fn.handle(req, res, function (err) { 179 | setPrototypeOf(req, orig.request) 180 | setPrototypeOf(res, orig.response) 181 | next(err); 182 | }); 183 | }); 184 | 185 | // app mounted 触发emit 186 | fn.emit('mount', this); 187 | }, this); 188 | 189 | return this; 190 | }; 191 | 192 | 193 | ``` 194 | 195 | 196 | 定义默认参数`offer`和`path`。然后处理`fn`形参不同类型的情况。将不同类型的中间件使用方式的形参转为扁平化数组,赋值给`fns`。 197 | 198 | `forEach`遍历fns,判断如果`fn`、`fn.handle`、`fn.set`参数不存在,return出去`router.use(path, fn)`。 199 | 200 | 否则继续执行`router.use`。 201 | 202 | ## 调用`handle`函数,执行中间件。 203 | 204 | 代码如下: 205 | 206 | ```javascript 207 | /** 208 | *将一个req, res对分派到应用程序中。中间件执行开始。 209 | *如果没有提供回调,则默认错误处理程序将作出响应 210 | *在堆栈中冒泡出现错误时。 211 | */ 212 | app.handle = function handle(req, res, callback) { 213 | var router = this._router; 214 | 215 | // 最后报错处理error。 216 | var done = callback || finalhandler(req, res, { 217 | env: this.get('env'), 218 | onerror: logerror.bind(this) 219 | }); 220 | 221 | // no routes 222 | if (!router) { 223 | debug('no routes defined on app'); 224 | done(); 225 | return; 226 | } 227 | 228 | router.handle(req, res, done); 229 | }; 230 | 231 | ``` 232 | 233 | ## 惰性添加Router。 234 | 235 | 从上述代码中可以知道,`app.use`的作用实际上是将各种应用函数传递给`router`的一个中间层代理。 236 | 237 | 而且,在app.use中有调用`this.lazyrouter()`函数,惰性的添加默认`router`。 238 | 239 | ```javascript 240 | app.lazyrouter = function lazyrouter() { 241 | 242 | if (!this._router) { 243 | this._router = new Router({ 244 | caseSensitive: this.enabled('case sensitive routing'), 245 | strict: this.enabled('strict routing') 246 | }); 247 | 248 | this._router.use(query(this.get('query parser fn'))); 249 | //初始化router 250 | this._router.use(middleware.init(this)); 251 | } 252 | }; 253 | 254 | ``` 255 | 256 | 这里对`Router`进行了实例化,同时设置基本的`option`,`caseSensitive` 是否区分大小写,`strict` 是否设置严格模式。 257 | 258 | ![](https://user-gold-cdn.xitu.io/2018/12/17/167bb4e1bf9f33ee?w=1824&h=852&f=png&s=357246) 259 | 260 | 261 | `Router`初始化如下: 262 | 263 | ``` 264 | /** 265 | * 用给定的“选项”初始化一个新的“路由器”。 266 | * 267 | * @param {Object} [options] [{ caseSensitive: false, strict: false }] 268 | * @return {Router} which is an callable function 269 | * @public 270 | */ 271 | 272 | var proto = module.exports = function(options) { 273 | 274 | var opts = options || {}; 275 | 276 | function router(req, res, next) { 277 | router.handle(req, res, next); 278 | } 279 | 280 | // 混合路由器类函数 281 | setPrototypeOf(router, proto) 282 | 283 | router.params = {}; 284 | router._params = []; 285 | router.caseSensitive = opts.caseSensitive; 286 | router.mergeParams = opts.mergeParams; 287 | router.strict = opts.strict; 288 | router.stack = []; 289 | 290 | return router; 291 | }; 292 | 293 | ``` 294 | 295 | 调用`app.use`时,参数都会传递给`router.use`,因此,打开`router/index.js`文件,查找`router.use`。 296 | 297 | ```javascript 298 | 299 | /** 300 | *使用给定的中间件函数,具有可选路径,默认为“/”。 301 | * Use(如' .all ')将用于任何http方法,但不会添加 302 | *这些方法的处理程序,所以选项请求不会考虑“。use” 303 | *函数,即使它们可以响应。 304 | *另一个区别是_route_ path被剥离,不可见 305 | *到处理程序函数。这个特性的主要作用是安装 306 | *无论“前缀”是什么,处理程序都可以在不更改任何代码的情况下操作 307 | *路径名。 308 | * 309 | * @public 310 | */ 311 | 312 | proto.use = function use(fn) { 313 | var offset = 0; 314 | var path = '/'; 315 | 316 | // 默认路径 '/' 317 | // 消除歧义 router.use([fn]) 318 | // 判断是否是函数 319 | if (typeof fn !== 'function') { 320 | var arg = fn; 321 | while (Array.isArray(arg) && arg.length !== 0) { 322 | arg = arg[0]; 323 | } 324 | // 第一个参数是函数 325 | if (typeof arg !== 'function') { 326 | offset = 1; 327 | path = fn; 328 | } 329 | } 330 | 331 | //将arguments转为数组,然后扁平化多维数组 332 | var callbacks = flatten(slice.call(arguments, offset)); 333 | 334 | //如果callbacks内没有传递函数,抛错 335 | if (callbacks.length === 0) { 336 | throw new TypeError('Router.use() requires a middleware function') 337 | } 338 | 339 | //循环callbacks数组 340 | for (var i = 0; i < callbacks.length; i++) { 341 | var fn = callbacks[i]; 342 | 343 | if (typeof fn !== 'function') { 344 | throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn)) 345 | } 346 | 347 | //解析下query和expressInit的含义 348 | // 添加中间件 349 | //匿名 anonymous 函数 350 | debug('use %o %s', path, fn.name || '') 351 | 352 | var layer = new Layer(path, { 353 | sensitive: this.caseSensitive, //敏感区分大小写 //默认为false 354 | strict: false, //严格 355 | end: false //结束 356 | }, fn); 357 | 358 | layer.route = undefined; 359 | this.stack.push(layer); 360 | } 361 | 362 | return this; 363 | } 364 | 365 | ``` 366 | 367 | `router.use`的主要作用就是将从`app.use`中传递过来的函数,通过`Layer`实例化的处理,添加一些**处理错误**、**处理请求**的方法,以便后续调用处理。同时将传递过来的`path`,通过`path-to-regexp`模块把路径转为正则表达式(`this.regexp`),调用`this.regexp.exec(path)`,将参数提取出来。 368 | 369 | `Layer`代码较多,这里不贴代码了,可以参考[express/lib/router/layer.js](https://github.com/expressjs/express/blob/master/lib/router/layer.js)。 370 | 371 | ## 处理中间件。 372 | 373 | 处理中间件就是将放入`this,stack`的`new Layout([options],fn)`,拿出来依次执行。 374 | 375 | ```javascript 376 | proto.handle = function handle(req, res, out) { 377 | var self = this; 378 | 379 | debug('dispatching %s %s', req.method, req.url); 380 | 381 | var idx = 0; 382 | //获取协议与URL地址 383 | var protohost = getProtohost(req.url) || '' 384 | var removed = ''; 385 | //是否添加斜杠 386 | var slashAdded = false; 387 | var paramcalled = {}; 388 | 389 | //存储选项请求的选项 390 | //仅在选项请求时使用 391 | var options = []; 392 | 393 | // 中间件和路由 394 | var stack = self.stack; 395 | // 管理inter-router变量 396 | //req.params 请求参数 397 | var parentParams = req.params; 398 | var parentUrl = req.baseUrl || ''; 399 | var done = restore(out, req, 'baseUrl', 'next', 'params'); 400 | 401 | // 设置下一层 402 | req.next = next; 403 | 404 | // 对于选项请求,如果没有其他响应,则使用默认响应 405 | if (req.method === 'OPTIONS') { 406 | done = wrap(done, function(old, err) { 407 | if (err || options.length === 0) return old(err); 408 | sendOptionsResponse(res, options, old); 409 | }); 410 | } 411 | 412 | // 设置基本的req值 413 | req.baseUrl = parentUrl; 414 | req.originalUrl = req.originalUrl || req.url; 415 | 416 | next(); 417 | 418 | function next(err) { 419 | var layerError = err === 'route' 420 | ? null 421 | : err; 422 | 423 | //是否添加斜线 默认false 424 | if (slashAdded) { 425 | req.url = req.url.substr(1); 426 | slashAdded = false; 427 | } 428 | 429 | // 恢复改变req.url 430 | if (removed.length !== 0) { 431 | req.baseUrl = parentUrl; 432 | req.url = protohost + removed + req.url.substr(protohost.length); 433 | removed = ''; 434 | } 435 | 436 | // 出口路由器信号 437 | if (layerError === 'router') { 438 | setImmediate(done, null) 439 | return 440 | } 441 | 442 | // 不再匹配图层 443 | if (idx >= stack.length) { 444 | setImmediate(done, layerError); 445 | return; 446 | } 447 | 448 | // 获取路径pathname 449 | var path = getPathname(req); 450 | 451 | if (path == null) { 452 | return done(layerError); 453 | } 454 | 455 | // 找到下一个匹配层 456 | var layer; 457 | var match; 458 | var route; 459 | 460 | while (match !== true && idx < stack.length) { 461 | layer = stack[idx++]; 462 | //try layer.match(path) catch err 463 | //搜索 path matchLayer有两种状态一种是boolean,一种是string。 464 | match = matchLayer(layer, path); 465 | route = layer.route; 466 | 467 | if (typeof match !== 'boolean') { 468 | layerError = layerError || match; 469 | } 470 | 471 | if (match !== true) { 472 | continue; 473 | } 474 | 475 | if (!route) { 476 | //正常处理非路由处理程序 477 | continue; 478 | } 479 | 480 | if (layerError) { 481 | // routes do not match with a pending error 482 | match = false; 483 | continue; 484 | } 485 | 486 | var method = req.method; 487 | var has_method = route._handles_method(method); 488 | 489 | // build up automatic options response 490 | if (!has_method && method === 'OPTIONS') { 491 | appendMethods(options, route._options()); 492 | } 493 | 494 | // don't even bother matching route 495 | if (!has_method && method !== 'HEAD') { 496 | match = false; 497 | continue; 498 | } 499 | } 500 | 501 | // no match 502 | if (match !== true) { 503 | return done(layerError); 504 | } 505 | 506 | //重新赋值router。 507 | if (route) { 508 | req.route = route; 509 | } 510 | 511 | // 合并参数 512 | req.params = self.mergeParams 513 | ? mergeParams(layer.params, parentParams) 514 | : layer.params; 515 | 516 | var layerPath = layer.path; 517 | 518 | // 处理参数 519 | self.process_params(layer, paramcalled, req, res, function (err) { 520 | if (err) { 521 | return next(layerError || err); 522 | } 523 | 524 | if (route) { 525 | return layer.handle_request(req, res, next); 526 | } 527 | 528 | // 处理req.url和layerPath,同时对layer中的请求error和handle_error加tryCatch处理。 529 | trim_prefix(layer, layerError, layerPath, path); 530 | }); 531 | } 532 | 533 | ``` 534 | 535 | 执行`proto.handle`中间件也就的`while`循环中的一些核心代码,每次调用`app.use`中的回调函数中的`next()`都会让`idx`加一,将`stack[idx++];`赋值给`layer`,调用一开始说到的`layer.handle_request`,然后调用`trim_prefix(layer, layerError, layerPath, path)`,添加一些报错处理。 536 | 537 | `trim_prefix`函数如下: 538 | 539 | ```javascript 540 | 541 | function trim_prefix(layer, layerError, layerPath, path) { 542 | if (layerPath.length !== 0) { 543 | // Validate path breaks on a path separator 544 | var c = path[layerPath.length] 545 | if (c && c !== '/' && c !== '.') return next(layerError) 546 | 547 | // //删除url中与路由匹配的部分 548 | // middleware (.use stuff) needs to have the path stripped 549 | debug('trim prefix (%s) from url %s', layerPath, req.url); 550 | removed = layerPath; 551 | req.url = protohost + req.url.substr(protohost.length + removed.length); 552 | 553 | // Ensure leading slash 554 | if (!protohost && req.url[0] !== '/') { 555 | req.url = '/' + req.url; 556 | slashAdded = true; 557 | } 558 | 559 | // 设置 base URL (no trailing slash) 560 | req.baseUrl = parentUrl + (removed[removed.length - 1] === '/' 561 | ? removed.substring(0, removed.length - 1) 562 | : removed); 563 | } 564 | 565 | debug('%s %s : %s', layer.name, layerPath, req.originalUrl); 566 | 567 | if (layerError) { 568 | layer.handle_error(layerError, req, res, next); 569 | } else { 570 | layer.handle_request(req, res, next); 571 | } 572 | } 573 | }; 574 | 575 | ``` 576 | 577 | ## 总结 578 | 579 | 以上就是通过`app.use`调用之后,一步步执行中间件函数`router.handle`。 580 | 581 | `next`核心代码很简单,但是需要考虑的场景却是很多,通过这次源码阅读,更能进一步的理解express的核心功能。 582 | 583 | 虽说平常做项目用到`express`框架很少,或者可以说基本不用,一般都是用`Koa`或者`Egg`,可以说基本上些规模的场景的项目用的都是`Egg`。 584 | 585 | 但是不可否认得是,`express`框架还是一款非常经典的框架。 586 | 587 | 588 | **以上代码纯属个人理解,如有不合适的地方,望在评论区留言。** 589 | 590 | 591 | 592 | 593 | 594 | 595 | -------------------------------------------------------------------------------- /从零开始React服务器渲染(SSR)同构😏(基于Koa).md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 自前端框架(`React`,`Vue`,`Angelar`)出现以来,每个框架携带不同理念,分为三大阵营,以前使用`JQuery`的时代已经成为过去,以前每个页面就是一个`HTML`,引入相对应的`JS`、`CSS`,同时在`HTML`中书写`DOM`。正因为是这样,每次用户访问进来,由于`HTML`中有`DOM`的存在,给用户的感觉响应其实并不是很慢。 4 | 5 | 但是自从使用了框架之后,无论是多少个页面,就是单独一个单页面,即`SPA`。`HTML`中所有的`DOM`元素,必须在客户端下载完`js`之后,通过调用执行`React.render()`才能够进行渲染,所以就有了很多网站上,一进来很长时间的`loading`动画。 6 | 7 | 为了解决这一并不是很友好的问题,社区上提出了很多方案,例如`预渲染`、`SSR`、`同构`。 8 | 9 | 当然这篇文章主要讲述的是从零开始搭建一个**React服务器渲染同构**。 10 | 11 | 12 | ![](https://user-gold-cdn.xitu.io/2019/2/14/168eb1c4e355f319?w=720&h=508&f=png&s=253917) 13 | 14 | ## 选择方案 15 | 16 | ### 方案一 使用社区精选框架Next.js 17 | 18 | `Next.js` 是一个轻量级的 `React` 服务端渲染应用框架。有兴趣的可以去[`Next.js`](http://nextjs.frontendx.cn/)官网学习下。 19 | 20 | ### 方案二 同构 21 | 22 | 关于同构有两种方案: 23 | 24 | ### 通过babel转义node端代码和React代码后执行 25 | 26 | ``` 27 | let app = express(); 28 | app.get('/todo', (req, res) => { 29 | let html = renderToString( 30 | 31 | 32 | 33 | ) 34 | res.send( indexPage(html) ) 35 | } 36 | }) 37 | 38 | ``` 39 | 在这里有两个问题需要处理: 40 | * `Node`不支持前端的`import`语法,需要引入`babel`支持。 41 | * `Node`不能解析标签语法。 42 | 43 | 所以执行`Node`时,需要使用`babel`来进行转义,如果出现错误了,也无从查起,个人并不推荐这样做。 44 | 45 | 所以这里采用第二种方案 46 | ### webpack进行编译处理 47 | 48 | 使用`webpack`打包两份代码,一份用于`Node`进行服务器渲染,一份用于浏览器进行渲染。 49 | 50 | 下面具体详细说明下。 51 | 52 | ## 搭建Node服务器 53 | 54 | 由于使用习惯,经常使用`Egg`框架,而`Koa`是`Egg`的底层框架,因此,这里我们采用`Koa`框架进行服务搭建。 55 | 56 | 搭建最基本的一个`Node`服务。 57 | ```js 58 | const Koa = require('koa'); 59 | const app = new Koa(); 60 | 61 | app.listen(3000, () => { 62 | console.log("服务器已启动,请访问http://127.0.0.1:3000") 63 | }); 64 | ``` 65 | 66 | ## 配置webpack 67 | 众所周知,`React`代码需要经过打包编译才能执行的,而服务端和客户端运行的代码只有一部分相同,甚至有些代码根本不需要将代码打包,这时就需要将客户端代码和服务端运行的代码分开,也就有了两份`webpack`配置 68 | 69 | `webpack` 将同一份代码,通过不同的`webpack`配置,分别为`serverConfig`和`clientConfig`,打包为两份代码。 70 | 71 | ### serverConfig和clientConfig配置 72 | 73 | 通过[webpack](https://www.webpackjs.com/configuration/target/)文档我们可以知道,webpack不仅可以编译web端代码还可以编译其他内容。 74 | 75 | 76 | ![](https://user-gold-cdn.xitu.io/2019/2/14/168eb551d6f7c6ce?w=1408&h=920&f=png&s=247131) 77 | 78 | 这里我们将`target`设为`node`。 79 | 80 | 配置入口文件和出口位置: 81 | ```js 82 | const serverConfig = { 83 | target: 'node', 84 | entry: { 85 | page1: './web/render/serverRouter.js', 86 | }, 87 | resolve, 88 | output: { 89 | filename: '[name].js', 90 | path: path.resolve(__dirname, './app/build'), 91 | libraryTarget: 'commonjs' 92 | } 93 | } 94 | 95 | ``` 96 | **注意⚠** 97 | 98 | 服务端配置需要配置`libraryTarget`,设置`commonjs`或者`umd`,用于服务端进行`require`引用,不然`require`值为`{}`。 99 | 100 | 在这里客户端和服务端配置没有什么区别,无需配置`target`(默认`web`环境),其他入门文件和输出文件不一致。 101 | ```js 102 | const clientConfig = { 103 | entry: { 104 | page1: './web/render/clientRouter.js' 105 | }, 106 | output: { 107 | filename: '[name].js', 108 | path: path.resolve(__dirname, './public') 109 | } 110 | } 111 | ``` 112 | ### 配置babel 113 | 由于打包的是`React`代码,因此还需要配置`babel`。 114 | 新建`.babelrc`文件。 115 | 116 | ```js 117 | { 118 | "presets": ["@babel/preset-react", 119 | ["@babel/preset-env",{ 120 | "targets": { 121 | "browsers": [ 122 | "ie >= 9", 123 | "ff >= 30", 124 | "chrome >= 34", 125 | "safari >= 7", 126 | "opera >= 23", 127 | "bb >= 10" 128 | ] 129 | } 130 | }] 131 | ], 132 | "plugins": [ 133 | [ 134 | "import", 135 | { "libraryName": "antd", "style": true } 136 | ] 137 | ] 138 | } 139 | ``` 140 | 141 | 这份配置由服务端和客户端共用,用来处理`React`和转义为`ES5`和浏览器兼容问题。 142 | ### 处理服务端引用问题 143 | 144 | 服务端使用`CommonJS`规范,而且服务端代码也并不需要构建,因此,对于node_modules中的依赖并不需要打包,所以借助`webpack`第三方模块`webpack-node-externals`来进行处理,经过这样的处理,两份构建过的文件大小已经相差甚远了。 145 | 146 | ### 处理css 147 | 148 | 服务端和客户端的区别,可能就在于一个默认处理,一个需要将`CSS`单独提取出为一个文件,和处理`CSS`前缀。 149 | 150 | 服务端配置 151 | ```js 152 | { 153 | test: /\.(css|less)$/, 154 | use: [ 155 | { 156 | loader: 'css-loader', 157 | options: { 158 | importLoaders: 1 159 | } 160 | }, 161 | { 162 | loader: 'less-loader', 163 | } 164 | ] 165 | } 166 | ``` 167 | 168 | 客户端配置 169 | 170 | ```js 171 | { 172 | test: /\.(css|less)$/, 173 | use: [ 174 | { 175 | loader: MiniCssExtractPlugin.loader, 176 | }, 177 | { 178 | loader: 'css-loader' 179 | }, 180 | { 181 | loader: 'postcss-loader', 182 | options: { 183 | plugins: [ 184 | require('precss'), 185 | require('autoprefixer') 186 | ], 187 | } 188 | }, 189 | { 190 | loader: 'less-loader', 191 | options: { 192 | javascriptEnabled: true, 193 | // modifyVars: theme //antd默认主题样式 194 | } 195 | } 196 | ], 197 | } 198 | ``` 199 | 200 | ## SSR 中客户端渲染与服务器端渲染路由代码的差异 201 | 202 | 实现 `React` 的 `SSR` 架构,我们需要让相同的代码在客户端和服务端各自执行一遍,但是这里各自执行一遍,并不包括路由端的代码,造成这种原因主要是因为客户端是通过地址栏来渲染不同的组件的,而服务端是通过请求路径来进行组件渲染的。 203 | 因此,在客户端我们采用`BrowserRouter`来配置路由,在服务端采用`StaticRouter`来配置路由。 204 | 205 | ### 客户端配置 206 | 207 | ```js 208 | import React from 'react'; 209 | import ReactDOM from 'react-dom'; 210 | import { BrowserRouter } from "react-router-dom"; 211 | import Router from '../router'; 212 | 213 | function ClientRender() { 214 | return ( 215 | 216 | 217 | 218 | ) 219 | } 220 | 221 | ``` 222 | 223 | ### 服务端配置 224 | 225 | ```javascript 226 | import React from 'react'; 227 | import { StaticRouter } from 'react-router' 228 | import Router from '../router.js'; 229 | 230 | function ServerRender(req, initStore) { 231 | 232 | return (props, context) => { 233 | return ( 234 | 235 | 236 | 237 | ) 238 | } 239 | } 240 | 241 | export default ServerRender; 242 | 243 | ``` 244 | 245 | ## 再次配置Node进行服务器渲染 246 | 上面配置的服务器,只是简单启动个服务,没有深入进行配置。 247 | ### 引入ReactDOMServer 248 | 249 | 250 | ```js 251 | 252 | const Koa = require('koa'); 253 | const app = new Koa(); 254 | const path = require('path'); 255 | const React = require('react'); 256 | const ReactDOMServer = require('react-dom/server'); 257 | const koaStatic = require('koa-static'); 258 | const router = new KoaRouter(); 259 | 260 | const routerManagement = require('./app/router'); 261 | const manifest = require('./public/manifest.json'); 262 | /** 263 | * 处理链接 264 | * @param {*要进行服务器渲染的文件名默认是build文件夹下的文件} fileName 265 | */ 266 | function handleLink(fileName, req, defineParams) { 267 | let obj = {}; 268 | fileName = fileName.indexOf('.') !== -1 ? fileName.split('.')[0] : fileName; 269 | 270 | try { 271 | obj.script = ``; 272 | } catch (error) { 273 | console.error(new Error(error)); 274 | } 275 | try { 276 | obj.link = ``; 277 | 278 | } catch (error) { 279 | console.error(new Error(error)); 280 | } 281 | //服务器渲染 282 | const dom = require(path.join(process.cwd(),`app/build/${fileName}.js`)).default; 283 | let element = React.createElement(dom(req, defineParams)); 284 | obj.html = ReactDOMServer.renderToString(element); 285 | 286 | return obj; 287 | } 288 | 289 | /** 290 | * 设置静态资源 291 | */ 292 | app.use(koaStatic(path.resolve(__dirname, './public'), { 293 | maxage: 0, //浏览器缓存max-age(以毫秒为单位) 294 | hidden: false, //允许传输隐藏文件 295 | index: 'index.html', // 默认文件名,默认为'index.html' 296 | defer: false, //如果为true,则使用后return next(),允许任何下游中间件首先响应。 297 | gzip: true, //当客户端支持gzip时,如果存在扩展名为.gz的请求文件,请尝试自动提供文件的gzip压缩版本。默认为true。 298 | })); 299 | 300 | /** 301 | * 处理响应 302 | * 303 | * **/ 304 | app.use((ctx) => { 305 | let obj = handleLink('page1', ctx.req, {}); 306 | ctx.body = ` 307 | 308 | 309 | 310 | 311 | 312 | 313 | koa-React服务器渲染 314 | ${obj.link} 315 | 316 | 317 | 318 |
319 | ${obj.html} 320 |
321 | 322 | ${obj.script} 323 | 324 | ` 325 | }) 326 | 327 | app.listen(3000, () => { 328 | console.log("服务器已启动,请访问http://127.0.0.1:3000") 329 | }); 330 | ``` 331 | 这里涉及一个`manifest`文件,这个文件是`webpack`插件`webpack-manifest-plugin`生成的,里面包含编译后的地址和文件。大概结构是这样: 332 | ```js 333 | { 334 | "page1.css": "page1.css", 335 | "page1.js": "page1.js" 336 | } 337 | ``` 338 | 我们把他引入到`clientConfig`中,添加如下配置: 339 | ```js 340 | ... 341 | plugins: [ 342 | // 提取样式,生成单独文件 343 | new MiniCssExtractPlugin({ 344 | filename: `[name].css`, 345 | chunkFilename: `[name].chunk.css` 346 | }), 347 | new ManifestPlugin() 348 | ] 349 | ``` 350 | 在上述服务端代码中,我们对于`ServerRender.js`进行了柯里化处理,这样做的目的在于,我们在`ServerRender`中,使用了服务端可以识别的`StaticRouter`,并配置了`location`参数,而`location`需要参数`URL`。 351 | 因此,我们需要在`renderToString`中传递`req`,以让服务端能够正确解析React组件。 352 | 353 | ```js 354 | let element = React.createElement(dom(req, defineParams)); 355 | obj.html = ReactDOMServer.renderToString(element); 356 | ``` 357 | 358 | 通过`handleLink`的解析,我们可以得到一个`obj`,包含三个参数,`link`(`css`链接),`script`(`JS`链接)和`html`(生成`Dom`元素)。 359 | 360 | 通过`ctx.body`渲染`html`。 361 | 362 | ### renderToString() 363 | 364 | 将 `React` 元素渲染到其初始 `HTML` 中。 该函数应该只在服务器上使用。 `React` 将返回一个 `HTML` 字符串。 您可以使用此方法在服务器上生成 `HTML` ,并在初始请求时发送标记,以加快网页加载速度,并允许搜索引擎抓取你的网页以实现 `SEO` 目的。 365 | 366 | 如果在已经具有此服务器渲染标记的节点上调用 `ReactDOM.hydrate()` ,`React` 将保留它,并且只附加事件处理程序,从而使您拥有非常高性能的第一次加载体验。 367 | 368 | ### renderToStaticMarkup() 369 | 370 | 类似于 `renderToString` ,除了这不会创建 `React` 在内部使用的额外`DOM`属性,如 `data-reactroot`。 如果你想使用`React` 作为一个简单的静态页面生成器,这很有用,因为剥离额外的属性可以节省一些字节。 371 | 372 | 但是如果这种方法是在浏览访问之后,会全部替换掉服务端渲染的内容,因此会造成页面闪烁,所以并不推荐使用该方法。 373 | 374 | ### renderToNodeStream() 375 | 376 | 将 `React` 元素渲染到其最初的 `HTML` 中。返回一个 可读的 流(`stream`) ,即输出 `HTML` 字符串。这个 流(`stream`) 输出的 `HTML` 完全等同于 `ReactDOMServer.renderToString` 将返回的内容。 377 | 378 | 我们也可以使用上述`renderToNodeSteam`将其改造下: 379 | 380 | ``` 381 | let element = React.createElement(dom(req, defineParams)); 382 | 383 | ctx.res.write(' 384 | 385 | 386 | 387 | 388 | 389 | koa-React服务器渲染 390 |
'); 391 | 392 | // 把组件渲染成流,并且给Response 393 | const stream = ReactDOMServer.renderToNodeStream(element); 394 | stream.pipe(ctx.res, { end: 'false' }); 395 | 396 | // 当React渲染结束后,发送剩余的HTML部分给浏览器 397 | stream.on('end', () => { 398 | ctx.res.end('
'); 399 | }); 400 | ``` 401 | 402 | ### renderToStaticNodeStream() 403 | 404 | 类似于 `renderToNodeStream` ,除了这不会创建 `React` 在内部使用的额外`DOM`属性,如 `data-reactroot` 。 如果你想使用 `React` 作为一个简单的静态页面生成器,这很有用,因为剥离额外的属性可以节省一些字节。 405 | 406 | 这个 流(`stream`) 输出的 `HTML` 完全等同于 `ReactDOMServer.renderToStaticMarkup` 将返回的内容。 407 | 408 | 409 | ## 添加状态管理redux 410 | 411 | 以上开发一个静态网站,或者一个相对于比较简单的项目已经`OK`了,但是对于复杂的项目,这些还远远不够,这里,我们再给它加上全局状态管理`Redux`。 412 | 413 | 服务器渲染中其顺序是同步的,因此,要想在渲染时出现首屏数据渲染,必须得提前准备好数据。 414 | 415 | * 提前获取数据 416 | * 初始化store 417 | * 根据路由显示组件 418 | * 结合数据和组件生成 HTML,一次性返回 419 | 420 | 对于客户端来说添加`redux`和常规的`redux`并无太大差别,只是对于`store`添加了一个初始的`window.__INIT_STORE__`。 421 | 422 | ```js 423 | let initStore = window.__INIT_STORE__; 424 | let store = configStore(initStore); 425 | 426 | function ClientRender() { 427 | return ( 428 | 429 | 430 | 431 | 432 | 433 | 434 | ) 435 | } 436 | ``` 437 | 438 | 而对于服务端来说在初始数据获取完成之后,可以采用`Promise.all()`来进行并发请求,当请求结束时,将数据填充到`script`标签内,命名为`window.__INIT_STORE__`。 439 | 440 | ```html 441 | `` 442 | ``` 443 | 444 | 然后将服务端的`store`重新配置下。 445 | 446 | ```js 447 | function ServerRender(req, initStore) { 448 | let store = CreateStore(JSON.parse(initStore.store)); 449 | 450 | return (props, context) => { 451 | return ( 452 | 453 | 454 | 455 | 456 | 457 | ) 458 | } 459 | } 460 | ``` 461 | 462 | ![](https://user-gold-cdn.xitu.io/2019/2/15/168f0106bef8b999?w=640&h=400&f=gif&s=2282198) 463 | 464 | ## 整理Koa 465 | 466 | 考虑后面开发的便利性,添加如下功能: 467 | * Router功能 468 | * HTML模板 469 | ### 添加Koa-Router 470 | 471 | ```js 472 | /** 473 | * 注册路由 474 | */ 475 | const router = new KoaRouter(); 476 | const routerManagement = require('./app/router'); 477 | ... 478 | routerManagement(router); 479 | app.use(router.routes()).use(router.allowedMethods()); 480 | 481 | ``` 482 | 483 | 为了保证开发时,接口规整,这里将所有的路由都提到一个新的文件中进行书写。并保证如以下格式: 484 | 485 | ```js 486 | /** 487 | * 488 | * @param {router 实例化对象} router 489 | */ 490 | 491 | const home = require('./controller/home'); 492 | 493 | module.exports = (router) => { 494 | router.get('/',home.renderHtml); 495 | router.get('/page2',home.renderHtml); 496 | router.get('/favicon.ico',home.favicon); 497 | router.get('/test',home.test); 498 | } 499 | 500 | ``` 501 | 502 | ### 处理模板 503 | 504 | 将`html`放入代码中,给人感觉并不是很友好,因此,这里同样引入了服务模板`koa-nunjucks-2`。 505 | 506 | 同时在其上在套一层中间件,以便传递参数和处理各种静态资源链接。 507 | 508 | ```js 509 | ... 510 | const koaNunjucks = require('koa-nunjucks-2'); 511 | ... 512 | /** 513 | * 服务器渲染,渲染HTML,渲染模板 514 | * @param {*} ctx 515 | */ 516 | function renderServer(ctx) { 517 | return (fileName, defineParams) => { 518 | let obj = handleLink(fileName, ctx.req, defineParams); 519 | // 处理自定义参数 520 | defineParams = String(defineParams) === "[object Object]" ? defineParams : {}; 521 | obj = Object.assign(obj, defineParams); 522 | ctx.render('index', obj); 523 | } 524 | } 525 | 526 | ... 527 | 528 | /** 529 | * 模板渲染 530 | */ 531 | app.use(koaNunjucks({ 532 | ext: 'html', 533 | path: path.join(process.cwd(), 'app/view'), 534 | nunjucksConfig: { 535 | trimBlocks: true 536 | } 537 | })); 538 | 539 | /** 540 | * 渲染Html 541 | */ 542 | app.use(async (ctx, next) => { 543 | ctx.renderServer = renderServer(ctx); 544 | await next(); 545 | }); 546 | ``` 547 | 548 | 在用户访问该服务器时,通过调用`renderServer`函数,处理链接,执行到最后,调用`ctx.render`完成渲染。 549 | 550 | ```js 551 | 552 | /** 553 | * 渲染react页面 554 | */ 555 | 556 | exports.renderHtml = async (ctx) => { 557 | let initState = ctx.query.state ? JSON.parse(ctx.query.state) : null; 558 | ctx.renderServer("page1", {store: JSON.stringify(initState ? initState : { counter: 1 }) }); 559 | } 560 | exports.favicon = (ctx) => { 561 | ctx.body = null; 562 | } 563 | 564 | exports.test = (ctx) => { 565 | ctx.body = { 566 | data: `测试数据` 567 | } 568 | } 569 | 570 | ``` 571 | 关于`koa-nunjucks-2`中,在渲染`HTML`时,会将有`< >`进行安全处理,因此,我们还需对我们传入的数据进行过滤处理。 572 | 573 | ```html 574 | 575 | 576 | 577 | 578 | 579 | 580 | koa-React服务器渲染 581 | {{ link | safe }} 582 | 583 | 584 | 585 |
586 | {{ html | safe }} 587 |
588 | 589 | 592 | {{ script | safe }} 593 | 594 | ``` 595 | ## 文档结构 596 | 597 | ```js 598 | ├── README.md 599 | ├── app //node端业务代码 600 | │ ├── build 601 | │ │ ├── page1.js 602 | │ │ └── page2.js 603 | │ ├── controller 604 | │ │ └── home.js 605 | │ ├── router.js 606 | │ └── view 607 | │ └── index.html 608 | ├── index.js 609 | ├── package.json 610 | ├── public //前端静态资源 611 | │ ├── manifest.json 612 | │ ├── page1.css 613 | │ ├── page1.js 614 | │ ├── page2.css 615 | │ └── page2.js 616 | ├── web //前端源码 617 | │ ├── action //redux -action 618 | │ │ └── count.js 619 | │ ├── components //组件 620 | │ │ └── layout 621 | │ │ └── index.jsx 622 | │ ├── pages //主页面 623 | │ │ ├── page 624 | │ │ │ ├── index.jsx 625 | │ │ │ └── index.less 626 | │ │ └── page2 627 | │ │ ├── index.jsx 628 | │ │ └── index.less 629 | │ ├── reducer //redux -reducer 630 | │ │ ├── counter.js 631 | │ │ └── index.js 632 | │ ├── render //webpack入口文件 633 | │ │ ├── clientRouter.js 634 | │ │ └── serverRouter.js 635 | │ ├── router.js //前端路由 636 | │ └── store //store 637 | │ └── index.js 638 | └── webpack.config.js 639 | ``` 640 | ## 最后 641 | 642 | 目前这个架构目前只能手动启动`Koa`服务和启动`webpack`。 643 | 644 | 如果需要将Koa和webpack跑在一块,这里就涉及另外一个话题了,在这里可以查看我一开始写的文章。 645 | 646 | 《[骚年,Koa和Webpack了解一下?](https://juejin.im/post/5c01f46c51882516d725ee51)》 647 | 648 | 如果需要了解一个完整的服务器需要哪些功能,可以了解我早期的文章。 649 | 650 | 《[如何创建一个可靠稳定的Web服务器](https://juejin.im/post/5c0cf55c51882530544f22e2)》 651 | 652 | 最后`GITHUB`地址如下: 653 | 654 | [基于koa的react服务器渲染](https://github.com/baiyuze/koa-react-ssr-render) 655 | 656 | 参考资料: 657 | 658 | 《[React中文文档](http://react.html.cn/docs/react-dom-server.html)》 659 | 《[Webpack中文文档](https://www.webpackjs.com/configuration/output/#output-librarytarget)》 660 | 《[React 中同构(SSR)原理脉络梳理](https://juejin.im/post/5bc7ea48e51d450e46289eab)》 661 | 《[Redux](https://www.redux.org.cn/docs/basics/Reducers.html)》 662 | -------------------------------------------------------------------------------- /原来正则表达式我记得这么少.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 记得上一次系统的学习正则表达式,还是刚学前端的事,现在过去那么久了,现在有必要将正则给补一补,也许这一次会有不同的感悟。 4 | 5 | ## 正则的速查表 6 | 7 | | 字符 | 详细 | 8 | | :-----: | :--------------------------------------- | 9 | | \ | 将下一个字符标记为一个特殊字符、或一个原义字符、或一个向后引用、或一个八进制转义符。例如,“`n`"匹配字符"`n`"。"`\n`"匹配一个换行符。串行"`\\`"匹配"`\`"而"`\(`"则匹配"`(`"。 | 10 | | ^ | 匹配输入字符串的开始位置。如果设置了`RegExp`对象的`Multiline`属性,`^`也匹配“`\n`”或“`\r`”之后的位置。 | 11 | | $ | 匹配输入字符串的结束位置。如果设置了`RegExp`对象的`Multiline`属性,`$`也匹配“`\n`”或“`\r`”之前的位置。 | 12 | |* | 匹配前面的子表达式零次或多次。例如,`zo`*能匹配“`z`"以及"`zoo`"。*等价于`{0,}`。 | 13 | | + | 匹配前面的子表达式一次或多次。例如,“`zo+`"能匹配"`zo`"以及"`zoo`",但不能匹配"`z`"。`+`等价于{`1,`}。 | 14 | | ? | 匹配前面的子表达式零次或一次。例如,“`do(es)?`”可以匹配"`does`"或"`does`"中的"`do`"。`?`等价于`{0,1}`。 | 15 | | {n} | `n`是一个非负整数。匹配确定的`n`次。例如,“`o{2}`”不能匹配"`Bob`"中的"`o`",但是能匹配"`food`"中的两个`o`。 | 16 | | {n,} | `n`是一个非负整数。至少匹配n次。例如,"`o{2,}`"不能匹配"`Bob`"中的"`o`",但能匹配"`foooood`"中的所有`o`。"`o{1,}`"等价于"`o+`"。"`o{0,}`"则等价于"`o*`"。 | 17 | | {n,m} | `m`和`n`均为非负整数,其中`n<=m`。最少匹配n次且最多匹配`m`次。例如,"`o{1,3}`"将匹配"`fooooood`"中的前三个`o`。"`o{0,1}`"等价于"`o?`"。请注意在逗号和两个数之间不能有空格。 | 18 | | ? | 当该字符紧跟在任何一个其他限制符`(*,+,?,{n},{n,},{n,m})`后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串"`oooo`","`o+?`"将匹配单个"`o`",而"`o+`"将匹配所有"`o`"。 | 19 | | . | 匹配除“`\n`”之外的任何单个字符。要匹配包括“`\n`”在内的任何字符,请使用像“`(.\|\n)`”的模式。 | 20 | | (pattern) | 匹配`pattern`并获取这一匹配。所获取的匹配可以从产生的`Matches`集合得到,在`VBScript`中使用`SubMatches`集合,在`JScript`中则使用`$0…$9`属性。要匹配圆括号字符,请使用“\(”或“\)”。 | 21 | | (?:pattern) | 匹配`pattern`但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用或字符“`(|)`”来组合一个模式的各个部分是很有用。例如“`industr(?:y|ies)`”就是一个比“`industry|industries`”更简略的表达式。| 22 | | (?=pattern) | 正向肯定预查,在任何匹配`pattern`的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,“`Windows(?=95|98|NT|2000)`”能匹配“`Windows2000`”中的“`Windows`”,但不能匹配“`Windows3.1`”中的“`Windows`”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。 | 23 | | (?!pattern) |正向否定预查,在任何不匹配`pattern`的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如“`Windows(?!95|98|NT|2000)`”能匹配“`Windows3.1`”中的“`Windows`”,但不能匹配“`Windows2000`”中的“`Windows`”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始| 24 | |(?<=pattern)| 反向肯定预查,与正向肯定预查类拟,只是方向相反。例如,“`(?<=95|98|NT|2000)Windows`”能匹配“`2000Windows`”中的“`Windows`”,但不能匹配“`3.1Windows`”中的“`Windows`”。 | 25 | | (? { 116 | return val.slice(1).toUpperCase(); 117 | }); 118 | //"getElementById" 119 | ``` 120 | 121 | ### 匹配二进制数字 122 | * 方法一 123 | ```js 124 | var str = "10101111"; 125 | var reg = /^[01]+$/g; 126 | console.log(reg.test(str)); 127 | 128 | 129 | ``` 130 | * 方法二 131 | ```js 132 | var str = "81"; 133 | var reg = /^(?!0)\d+$/; 134 | console.log(reg.test(str)); 135 | ``` 136 | * 方法三 137 | 138 | ```js 139 | var str = "0101212312"; 140 | var reg = /^[^0]\d+$/g; 141 | console.log(reg.test(str)); 142 | //false 143 | ``` 144 | ### 分割数字每三个以一个逗号划分 145 | ```js 146 | var str = "12345678901"; 147 | function numSplit(str){ 148 | var re = /(\d)(?=(\d{3})+$)/g; 149 | //(\d{3})+$ 的意思是连续匹配 3 个数字,且最后一次匹配以 3 个数字结尾。 150 | //要找到所有的单个字符,这些字符的后面跟随的字符的个数必须是3的倍数,并在符合条件的单个字符后面添加, 151 | return str.replace(re,'$1,'); 152 | } 153 | console.log(numSplit(str));//12,345,678,901 154 | ``` 155 | 156 | ### 如何获取一个字符串中的数字字符,并按数组形式输出 157 | ```js 158 | var str="dgfhfgh254bhku289fgdhdy675"; 159 | var re=/(\d+)/g; 160 | console.log(str.match(re)); 161 | ``` 162 | 163 | ### 求一串字符串中出现次数最多的字符和其出现的次数 164 | 165 | ```js 166 | var str = "qjvj58h7vv9n57v55v5jj"; 167 | var n = -1; 168 | while ((new RegExp("(.)(.*?\\1){"+(++n)+"}")).test(str)); 169 | console.log("出现次数最多的字符是 "+RegExp.$1+",出现次数 "+n); 170 | ``` 171 | 172 | **解释一下** 173 | * `.`代表任意一个字符。 174 | * `(.)`选择任意一个字符进行复制。 175 | * `.*` 代表任意一个字符后面有0个或者多个字符。 176 | * `(.)(.*\\1)`是否存在一个或多个字符与它相同。 177 | * `(.)(.*?\\1)`表示是非贪婪模式。 178 | * `\1`匹配和正则表达式中的括号(计算左括号)中出现相同内容的内容,数字代表匹配第几个括号。 179 | * `\\1`代表第一个圆括号里面的内容是否相同。 180 | 181 | ### 压缩字符串(例如:abcbc压缩后依然是abcbc,而xxxyyyyyyz压缩后就是3x6yz) 182 | 183 | ```js 184 | var str = "xxxyyyyyyz"; 185 | str = str.replace(/(.)\1+/ig,function(s,a){return s.length+a;}); 186 | console.log(str); 187 | ``` 188 | * `i`不区分大小写 189 | 190 | ### 判断是否含有连续字符 191 | ```js 192 | var str = 'a2s3s2d2d3dsfas'; 193 | var reg = /(\w)\1/ig 194 | console.log(reg.test(str)); 195 | ``` 196 | 197 | ### 匹配一个字符串中的正浮点数 198 | 199 | ```js 200 | var reg = /(0.\d+)|(\d+.\d+)/; 201 | console.log(reg.test('0')); // false 202 | console.log(reg.test('0.5')); // true 203 | console.log(reg.test('d0.5')); // true 204 | console.log(reg.test('d0.5s')); // true 205 | console.log(reg.test('d0.a5s')); // false 206 | 207 | ``` 208 | ## 最后 209 | **如果有地方不合理的,麻烦提出来一下** 210 | ### 参考文章: 211 | 212 | 《[js正则表达式常见面试题](https://www.cnblogs.com/dxzg/p/8279919.html)》 213 | 《[【大家一起来思考】近段时间整理的...](https://bbs.csdn.net/topics/392083813?page=1)》 214 | 《[js正则表达式常见面试题](https://www.cnblogs.com/dxzg/p/8279919.html)》 215 | 《[MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions)》 216 | -------------------------------------------------------------------------------- /如何创建一个可靠稳定的Web服务器.md: -------------------------------------------------------------------------------- 1 | > 延续上篇文章[骚年,Koa和Webpack了解一下?](https://juejin.im/post/5c01f46c51882516d725ee51) 2 | 3 | **本篇文章主要讲述的是如何通过Node创建一个稳定的web服务器,如果你看到这里想起了pm2等工具,那么你可以先抛弃pm2,进来看看,如果有哪些不合适的地方,恳请您指出。** 4 | 5 | ## 创建一个稳定的web服务器需要解决什么问题。 6 | 7 | > * 如何利用多核CPU资源。 8 | > * 多个工作进程的存活状态管理。 9 | > * 工作进程的平滑重启。 10 | > * 进程错误处理。 11 | > * 工作进程限量重启。 12 | 13 | ## 如何利用多核CPU资源 14 | 15 | #### 利用多核CPU资源有多种解决办法。 16 | 17 | 18 | * 通过在单机上部署多个Node服务,然后监听不同端口,通过一台Nginx负载均衡。 19 | 20 | > 这种做法一般用于多台机器,在服务器集群时,采用这种做法,这里我们不采用。 21 | 22 | 23 | * 通过单机启动一个master进程,然后fork多个子进程,master进程发送句柄给子进程后,关闭监听端口,让子进程来处理请求。 24 | > 这种做法也是Node单机集群普遍的做法。 25 | 26 | 27 | 28 | 所幸的是,Node在v0.8版本新增的cluster模块,让我们不必使用[child_process](http://nodejs.cn/api/child_process.html)一步一步的去处理Node集群这么多细节。 29 | 30 | **所以本篇文章讲述的是基于cluster模块解决上述的问题。** 31 | 32 | 33 | **首先创建一个Web服务器**,Node端采用的是**Koa**框架。没有使用过的可以先去看下 ===> [传送门](https://koa.bootcss.com/) 34 | 35 | 下面的代码是创建一个基本的web服务需要的配置,看过上篇文章的可以先直接过滤这块代码,直接看后面。 36 | 37 | ```javascript 38 | const Koa = require('koa'); 39 | const app = new Koa(); 40 | const koaNunjucks = require('koa-nunjucks-2'); 41 | const koaStatic = require('koa-static'); 42 | const KoaRouter = require('koa-router'); 43 | const router = new KoaRouter(); 44 | const path = require('path'); 45 | const colors = require('colors'); 46 | const compress = require('koa-compress'); 47 | const AngelLogger = require('../angel-logger') 48 | const cluster = require('cluster'); 49 | const http = require('http'); 50 | 51 | class AngelConfig { 52 | constructor(options) { 53 | this.config = require(options.configUrl); 54 | this.app = app; 55 | this.router = require(options.routerUrl); 56 | this.setDefaultConfig(); 57 | this.setServerConfig(); 58 | 59 | } 60 | 61 | setDefaultConfig() { 62 | //静态文件根目录 63 | this.config.root = this.config.root ? this.config.root : path.join(process.cwd(), 'app/static'); 64 | //默认静态配置 65 | this.config.static = this.config.static ? this.config.static : {}; 66 | } 67 | 68 | setServerConfig() { 69 | this.port = this.config.listen.port; 70 | 71 | //cookie签名验证 72 | this.app.keys = this.config.keys ? this.config.keys : this.app.keys; 73 | 74 | } 75 | } 76 | 77 | //启动服务器 78 | class AngelServer extends AngelConfig { 79 | constructor(options) { 80 | super(options); 81 | this.startService(); 82 | } 83 | 84 | startService() { 85 | //开启gzip压缩 86 | this.app.use(compress(this.config.compress)); 87 | 88 | //模板语法 89 | this.app.use(koaNunjucks({ 90 | ext: 'html', 91 | path: path.join(process.cwd(), 'app/views'), 92 | nunjucksConfig: { 93 | trimBlocks: true 94 | } 95 | })); 96 | this.app.use(async (ctx, next) => { 97 | ctx.logger = new AngelLogger().logger; 98 | await next(); 99 | }) 100 | 101 | //访问日志 102 | this.app.use(async (ctx, next) => { 103 | await next(); 104 | // console.log(ctx.logger,'loggerloggerlogger'); 105 | const rt = ctx.response.get('X-Response-Time'); 106 | ctx.logger.info(`angel ${ctx.method}`.green,` ${ctx.url} - `,`${rt}`.green); 107 | }); 108 | 109 | // 响应时间 110 | this.app.use(async (ctx, next) => { 111 | const start = Date.now(); 112 | await next(); 113 | const ms = Date.now() - start; 114 | ctx.set('X-Response-Time', `${ms}ms`); 115 | }); 116 | 117 | this.app.use(router.routes()) 118 | .use(router.allowedMethods()); 119 | 120 | // 静态资源 121 | this.app.use(koaStatic(this.config.root, this.config.static)); 122 | 123 | // 启动服务器 124 | this.server = this.app.listen(this.port, () => { 125 | console.log(`当前服务器已经启动,请访问`,`http://127.0.0.1:${this.port}`.green); 126 | this.router({ 127 | router, 128 | config: this.config, 129 | app: this.app 130 | }); 131 | }); 132 | } 133 | } 134 | 135 | module.exports = AngelServer; 136 | 137 | ``` 138 | 139 | **在启动服务器之后,将`this.app.listen`赋值给`this.server`,后面会用到。** 140 | 141 | 一般我们做**单机集群**时,我们`fork`的进程数量是机器的CPU数量。当然更多也不限定,只是一般不推荐。 142 | 143 | ```javascript 144 | const cluster = require('cluster'); 145 | const { cpus } = require('os'); 146 | const AngelServer = require('../server/index.js'); 147 | const path = require('path'); 148 | let cpusNum = cpus().length; 149 | 150 | //超时 151 | let timeout = null; 152 | 153 | //重启次数 154 | let limit = 10; 155 | // 时间 156 | let during = 60000; 157 | let restart = []; 158 | 159 | //master进程 160 | if(cluster.isMaster) { 161 | //fork多个工作进程 162 | for(let i = 0; i < cpusNum; i++) { 163 | creatServer(); 164 | } 165 | 166 | } else { 167 | //worker进程 168 | let angelServer = new AngelServer({ 169 | routerUrl: path.join(process.cwd(), 'app/router.js'),//路由地址 170 | configUrl: path.join(process.cwd(), 'config/config.default.js') 171 | //默认读取config/config.default.js 172 | }); 173 | } 174 | 175 | // master.js 176 | //创建服务进程 177 | function creatServer() { 178 | let worker = cluster.fork(); 179 | console.log(`工作进程已经重启pid: ${worker.process.pid}`); 180 | } 181 | 182 | ``` 183 | 使用进程的方式,其实就是通过`cluster.isMaster`和`cluster.isWorker`来进行判断的。 184 | 185 | 主从进程代码写在一块可能也不太好理解。这种写法也是Node官方的写法,当然也有更加清晰的写法,借助[`cluster.setupMaster`](http://nodejs.cn/api/cluster.html#cluster_cluster_setupmaster_settings)实现,这里不去详细解释。 186 | 187 | 通过Node执行代码,看看究竟发生了什么。 188 | 189 | 190 | ![](https://user-gold-cdn.xitu.io/2018/12/9/1679253db1777ae4?w=1454&h=482&f=png&s=319810) 191 | 192 | 首先判断`cluster.isMaster`是否存在,然后循环调用`createServer()`,**fork**4个工作进程。打印工作进程**pid**。 193 | 194 | `cluster`启动时,它会在内部启动**TCP**服务,在`cluster.fork()`子进程时,将这个**TCP**服务端`socket`的文件描述符发送给工作进程。如果工作进程中存在`listen()`监听网络端口的调用,它将拿到该文件的文件描述符,通过**SO_REUSEADDR**端口重用,从而实现多个子进程共享端口。 195 | 196 | 197 | ## 进程管理、平滑重启、和错误处理。 198 | 199 | 一般来说,master进程比较稳定,工作进程并不是太稳定。 200 | 201 | 因为工作进程处理的是业务逻辑,因此,我们需要给工作进程添加**自动重启**的功能,也就是如果子进程因为业务中不可控的原因报错了,而且阻塞了,此时,我们应该停止该进程接收任何请求,然后**优雅**的关闭该工作进程。 202 | 203 | ```javascript 204 | //超时 205 | let timeout = null; 206 | 207 | //重启次数 208 | let limit = 10; 209 | // 时间 210 | let during = 60000; 211 | let restart = []; 212 | 213 | if(cluster.isMaster) { 214 | //fork多个工作进程 215 | for(let i = 0; i < cpusNum; i++) { 216 | creatServer(); 217 | } 218 | 219 | } else { 220 | //worker 221 | let angelServer = new AngelServer({ 222 | routerUrl: path.join(process.cwd(), 'app/router.js'),//路由地址 223 | configUrl: path.join(process.cwd(), 'config/config.default.js') //默认读取config/config.default.js 224 | }); 225 | 226 | //服务器优雅退出 227 | angelServer.app.on('error', err => { 228 | //发送一个自杀信号 229 | process.send({ act: 'suicide' }); 230 | cluster.worker.disconnect(); 231 | angelServer.server.close(() => { 232 | //所有已有连接断开后,退出进程 233 | process.exit(1); 234 | }); 235 | //5秒后退出进程 236 | timeout = setTimeout(() => { 237 | process.exit(1); 238 | },5000); 239 | }); 240 | } 241 | 242 | // master.js 243 | //创建服务进程 244 | function creatServer() { 245 | 246 | let worker = cluster.fork(); 247 | console.log(`工作进程已经重启pid: ${worker.process.pid}`); 248 | //监听message事件,监听自杀信号,如果有子进程发送自杀信号,则立即重启进程。 249 | //平滑重启 重启在前,自杀在后。 250 | worker.on('message', (msg) => { 251 | //msg为自杀信号,则重启进程 252 | if(msg.act == 'suicide') { 253 | creatServer(); 254 | } 255 | }); 256 | 257 | //清理定时器。 258 | worker.on('disconnect', () => { 259 | clearTimeout(timeout); 260 | }); 261 | 262 | } 263 | 264 | ``` 265 | 266 | 我们在实例化`AngelServer`后,得到`angelServer`,通过拿到`angelServer.app`拿到`Koa`的实例,从而监听Koa的`error`事件。 267 | 268 | 当监听到错误发生时,发送一个自杀信号`process.send({ act: 'suicide' })`。 269 | 调用`cluster.worker.disconnect()`方法,调用此方法会关闭所有的server,并等待这些server的 'close'事件执行,然后关闭IPC管道。 270 | 271 | 调用`angelServer.server.close()`方法,当所有连接都关闭后,通往该工作进程的IPC管道将会关闭,允许工作进程优雅地死掉。 272 | 273 | 如果5s的时间还没有退出进程,此时,5s后将强制关闭该进程。 274 | 275 | Koa的`app.listen`是`http.createServer(app.callback()).listen();`的语法糖,因此可以调用close方法。 276 | 277 | **worker**监听`message`,如果是该信号,此时先重启新的进程。 278 | 同时监听`disconnect`事件,清理定时器。 279 | 280 | 正常来说,我们应该监听`process`的`uncaughtException`事件,*如果 Javascript 未捕获的异常,沿着代码调用路径反向传递回事件循环,会触发 'uncaughtException' 事件。* 281 | 282 | 但是`Koa`已经在[middleware](https://github.com/chenshenhai/koajs-design-note/blob/master/note/chapter01/06.md)外边加了`tryCatch`。因此在uncaughtException捕获不到。 283 | 284 | 在这里,还得特别感谢下[大深海](https://github.com/chenshenhai)老哥,深夜里,在群里给我指点迷津。 285 | 286 | ## 限量重启 287 | 288 | 通过自杀信号告知主进程可以使新连接总是有进程服务,但是依然还是有极端的情况。 289 | 工作进程不能无限制的被频繁重启。 290 | 291 | 因此在单位时间规定只能重启多少次,超过限制就触发giveup事件。 292 | 293 | ```javascript 294 | //检查启动次数是否太过频繁,超过一定次数,重新启动。 295 | function isRestartNum() { 296 | 297 | //记录重启的时间 298 | let time = Date.now(); 299 | let length = restart.push(time); 300 | if(length > limit) { 301 | //取出最后10个 302 | restart = restart.slice(limit * -1); 303 | } 304 | //1分钟重启的次数是否太过频繁 305 | return restart.length >= limit && restart[restart.length - 1] - restart[0] < during; 306 | } 307 | 308 | ``` 309 | 310 | 同时将createServer修改成 311 | 312 | ```javascript 313 | // master.js 314 | //创建服务进程 315 | function creatServer() { 316 | //检查启动是否太过频繁 317 | if(isRestartNum()) { 318 | process.emit('giveup', length, during); 319 | return; 320 | } 321 | let worker = cluster.fork(); 322 | console.log(`工作进程已经重启pid: ${worker.process.pid}`); 323 | //监听message事件,监听自杀信号,如果有子进程发送自杀信号,则立即重启进程。 324 | //平滑重启 重启在前,自杀在后。 325 | worker.on('message', (msg) => { 326 | //msg为自杀信号,则重启进程 327 | if(msg.act == 'suicide') { 328 | creatServer(); 329 | } 330 | }); 331 | //清理定时器。 332 | worker.on('disconnect', () => { 333 | clearTimeout(timeout); 334 | }); 335 | 336 | } 337 | 338 | ``` 339 | 340 | ## 更改负载均衡策略 341 | 342 | 默认的是操作系统抢占式,就是在一堆工作进程中,闲着的进程对到来的请求进行争抢,谁抢到谁服务。 343 | 344 | 对于是否繁忙是由CPU和I/O决定的,但是影响抢占的是CPU。 345 | 346 | 对于不同的业务,会有的I/O繁忙,但CPU空闲的情况,这时会造成负载不均衡的情况。 347 | 因此我们使用node的另一种策略,名为轮叫制度。 348 | 349 | ```javascript 350 | cluster.schedulingPolicy = cluster.SCHED_RR; 351 | ``` 352 | 353 | ## 最后 354 | 355 | 当然创建一个稳定的web服务还需要注意很多地方,比如优化处理进程之间的通信,数据共享等等。 356 | 357 | 本片文章只是给大家一个参考,如果有哪些地方写的不合适的地方,恳请您指出。 358 | 359 | 完整代码请见[github](https://github.com/baiyuze/version-control-system/blob/master/lib/angel-cluster/master.js)。 360 | 361 | 参考资料:**深入浅出nodejs** 362 | 363 | 364 | 365 | 366 | 367 | -------------------------------------------------------------------------------- /打破思维桎梏:探索业务和技术的交汇,开拓个人职业道路: -------------------------------------------------------------------------------- 1 | --- 2 | theme: fancy 3 | --- 4 | 5 | ## 打破思维桎梏 6 | 7 | ### 一、写在之前 8 | > 本篇文章是自工作到现在的一个思考总结,主要目的是为了阐述个人的观点以及如何保持竞争力,也会从多方面,多维度去展开。本文叙述的重点将从业务、技术(架构)和管理三方面来阐述,当然也有可能说的并不准确,仅代表个人观点。 9 | > 10 | > 欢迎大家进行讨论和指出文章中不足之处,大家共同学习进步。 11 | 12 | ### 二、调整思维模式 13 | 14 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4f7aaba79c104d34a6e05509aa76225e~tplv-k3u1fbpfcp-watermark.image?) 15 | 16 | 自2022年年初至今,整个行业形势严峻,各企业业务收缩,裁员优化,市场人才饱和,求职困难,竞争激烈。在此背景下,公司对员工能力要求更高,“前端已死”论调加剧了前端等技术人员的焦虑,今年的求职旺季并不热闹,许多人选择稳守岗位,幸存者庆幸自己未成为被裁员者。 17 | 18 | **站在公司角度,哪些能力的员工面临的裁员风险最小?** 19 | 20 | 我认为主要分为**技术**、**业务**和**管理**三方面。 21 | 22 | 如果团队从事有价值或具有广阔市场前景的业务,拥有上述三个条件中的任何一个就足够。 23 | 24 | 从个人角度看,技术、业务和管理三者相辅相成,至少在初期阶段,技术和业务是密切相关的。 25 | 26 | 有人可能认为,作为技术人员,只需编写优秀的代码,不必关心业务。 27 | 28 | 尽管如此,在开发过程中仅满足需求而不关注业务可能并非最佳策略。 29 | 30 | 在许多情况下,后端开发人员比前端开发人员更深入地理解业务。 31 | 32 | 他们需要充分了解业务才能进行数据库和架构设计,满足现有和未来功能扩展需求。而前端开发人员似乎只需完成产品需求文档,无需了解整个业务逻辑。 33 | 34 | 然而,这样做真的好吗? 35 | 36 | 事实上,后端开发人员更有可能担任部门或项目主管。 37 | 38 | 这可能与前端技术的兴起时间较晚有关,但也可能是因为后端开发人员对业务理解更深刻。 39 | 40 | 对公司来说,始终关心的不是开发团队的技术实力,而是如何满足市场需求,技术的深度都是附加的。 41 | 42 | 技术部门是公司的固定成本,投入多少取决于产品带来的利润以及保证产品稳定性和用户体验的需求。 43 | 44 | 我们也很容易沉迷于技术,探究深层原理,提高编程能力,这会提升我们在团队中的地位,让我们成为技术大牛,承担更复杂的功能开发。 45 | 46 | 但换个角度思考,作为技术大牛,承担更复杂任务的同时,我们是否也成为了一种资源?既然如此,为何不成为分配资源的角色呢? 47 | 48 | ### 三、突破思维边界 49 | 50 | ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f69f979e2f35444da7a5249aa3eb604a~tplv-k3u1fbpfcp-watermark.image?) 51 | 52 | 突破思维边界,个人理解为**多维度思考**,这种思考方式有助于开阔视野、提高创新能力和自我提升等方面。 53 | 54 | **具备多维度思考能力后,你所扮演的角色将不仅限于开发者,而是站在更高的维度来思考问题。** 55 | 56 | 举个简单例子: 57 | 58 | 当产品经理提出新需求时,通常会有需求评审会议。在会议过程中,如果你作为普通开发者,你可能会考虑功能如何实现、模块间是否合理、是否需要抽象或其他人的配合。 59 | 60 | 甚至可能觉得功能难实现,而抵触需求,要求产品简化功能。 61 | 62 | 这时,作为开发者的我们变得被动,节奏受产品经理控制。 63 | 64 | 遇到善解人意的产品经理会替开发者着想;若产品经理强势,我们会更加被动。 65 | 66 | 如果换个角度呢? 67 | 68 | 在充分了解业务需求、市场需求及站在用户角度思考后,我们便具备主动性。 69 | 70 | 在会议上,我们可以抓住重点,大胆提出想法,归纳总结,排除不合理需求,从而占据主动地位。这样一来,在会议上不仅具备发言权,还能潜移默化地提升个人威望和竞争力。 71 | 72 | #### 个人品牌与影响力 73 | 74 | 75 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d1a17690bb54439bb62abaf50776eb59~tplv-k3u1fbpfcp-watermark.image?) 76 | 77 | 想象一下,在一个公司工作的开发人员,他们的项目是开发一个出行平台,为用户提供一站式的出行解决方案。作为这个团队的一员,你可能会专注于自己的任务,例如前端设计、后端开发或者数据处理等。 78 | 79 | 然而,如果你能够运用多维度思考,站在不同角色的立场,理解他们的需求和痛点,你的影响力和竞争力将大大提升。 80 | 81 | 实际上,我们所学到的跨领域知识和沟通技巧也能潜移默化的提升竞争力。 82 | 83 | 当具备了多维度思考的能力,将更容易适应不同的行业和岗位,拓宽职业道路。 84 | 85 | 这种能力会在面对市场变化和行业竞争时,具备更强的抗风险能力。 86 | 87 | **通过多维度思考,能够更好地发现和把握个人发展的机会,这将有助于在职业生涯中找到自己的核心竞争力,从而在激烈的竞争中脱颖而出。** 88 | 89 | 在这个过程中,将会逐渐建立起自己的个人品牌和专业声誉,这将对我们的职业发展产生长远的积极影响。 90 | 91 | 总之,多维度思考不仅可以帮助你为公司创造价值,还可以提升个人的职业竞争力和价值,通过不断地学习和实践,能够在职场上取得更大的成功,实现个人和团队的持续发展。 92 | 93 | 所以,从个人的角度来看,多维度思考是一种投资自己的方式,它将使你在未来获得更好的回报。 94 | 95 | ### 四、理解三者之间的关系 96 | 97 | **技术、业务、管理,这三种能力,我认为他们是相辅相成的,技术是为了更好的服务于业务,管理是为了更好的发展业务,一切的前提就是以业务为基础的。** 98 | 99 | 可能会有人反驳这一观点,没错,这个观点是站在整个公司产品层面去考虑的,而我们作为普通开发者,如何去平衡三者之间的关系呢? 100 | 101 | 102 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d0a14a61ac64462da134371cbbe9dbad~tplv-k3u1fbpfcp-watermark.image?) 103 | 104 | #### 1、业务能力 105 | 106 | **业务能力是业务架构的关键组成,表示企业执行业务活动的能力,是对完成某一业务目标的一些列业务活动的抽象与封装。** 107 | 108 | 在此背景下,业务能力不仅仅是对公司业务的熟悉程度,而是一种基于日常工作中抽象、封装和总结的业务架构能力。 109 | 110 | 这种能力可以帮助我们迅速熟悉各种业务需求,深入了解公司产品、需求及发展方向,并为之提供针对性的业务理论指导。 111 | 112 | 作为技术人员,我们需要将自己的技术能力与现有业务模型相结合,构建一套独特的方法论。 113 | 114 | 在IT行业中,业务能力可以从以下几个方面来总结:产品背景、业务流程、市场需求、问题分析与解决方案以及创新。 115 | 116 | 在实际工作中,要构建自己的业务模型体系,我们需要对以下问题心中有数: 117 | 118 | **1、为什么要这么做?** 119 | 120 | **2、能带来什么价值?** 121 | 122 | **3、我该如何做?** 123 | 124 | 我们都见过在会议讨论中,总有一个人能够直击问题要害并给出解决方案,或者对整个会议进行总结。虽然表面上我们可能不以为然,但内心可能都希望那个人是自己。 125 | 126 | 拥有一定的业务能力不仅有助于在当前行业的提升,还可推广到其他行业。 127 | 128 | 这是一种专业化的业务架构能力。 129 | 130 | 因此,我认为提升个人业务能力是提升个人影响力的关键环节,通过提高个人影响力,进一步提升个人竞争力。 131 | 132 | #### 2、技术能力 133 | 134 | * 在国内,我们可以爱好编程,为爱发电,去为技术贡献自己的一份力。 135 | 136 | * 但是我们仍需记得,在工作中,业务永远比技术重要,技术只是手段,不是目的,技术是服务于业务的。 137 | 138 | * 技术的难度对于公司来言没有任何意义,公司关注的永远只是解决需求,带来盈利。 139 | 140 | * 技术要带来盈利,无论是间接的,还是直接的。 141 | 142 | **选择比努力更重要。** 143 | 144 | 曾了解过一些公司的晋升体系,对于晋升者来说,前期的一些努力是必要的,有的人选择了优质的业务,有的人通过私下去整合轮子,发布到社区,提升个人影响力。 145 | 146 | 对于选择了业务并充分运用个人技术与架构能力的人来说,他们在产品中所带来的价值越高,主导性越强,产品盈利越好,晋升成功率通常也越高。 147 | 148 | 毕竟,资本往往是短视的,即便是在国内的科研机构也是如此。[知乎例子](https://zhuanlan.zhihu.com/p/48289828) 149 | 150 | 因此,我们需要在追求技术进步的同时,关注业务需求和市场变化,以确保我们在职业生涯中做出明智的选择和投入。 151 | 152 | ------------- 153 | 154 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/97fd63d766604526aa008fa8ffe3e963~tplv-k3u1fbpfcp-watermark.image?) 155 | 前面说了这么多,其实明确说下来就一点,一定要有一个清晰的认知。 156 | 157 | 该技术能力,我其实表达的是技术架构能力,对于一个新公司,或者接触到新业务时,需要重新构建自己的业务模型,分析当下和未来业务的发展方向,充分利用个人的技术能力,提前为业务布局。 158 | 159 | 在这里,我并不会多说如何去针对业务进行技术架构分析。而是将具体业务细化,然后分析业务,落实到技术分析中,从而布局整个技术架构。 160 | 161 | 通常考虑进行架构设计时,往往都是以哪个技术深,技术新,有可玩性而进行设计;如果站在业务侧,需要既满足业务所需的基础上进行扩展,保证产品的稳定性,才是关键所在。 162 | 163 | 合理的利用个人架构能力,能让我们在接触新业务时迅速构建自己的业务模型,分析当前和未来业务的发展方向,并充分利用个人技术能力为业务提前布局。 164 | 165 | * **全面了解业务需求** 166 | > 与业务团队密切沟通,确保充分理解业务需求、目标和痛点。深入了解用户需求和市场趋势,以便在技术架构设计时能够紧密结合业务需求。 167 | 168 | * **设计灵活、可扩展的架构** 169 | > 在技术架构设计时,要考虑到业务的可持续发展和变化。设计一个灵活、可扩展的架构,以便在业务发展和变化时能够快速调整和适应。 170 | 171 | * **考虑性能与稳定性** 172 | > 在技术架构设计中,不仅要关注技术的深度和新颖性,还要关注产品的性能和稳定性。确保架构能够在满足业务需求的同时,提供良好的用户体验和系统稳定性。 173 | 174 | * **具备敏捷开发的思维** 175 | > 在技术架构设计过程中,要具备敏捷开发的思维,能够快速响应业务变化和市场需求,迅速迭代和优化产品,以便及时满足业务需求。 176 | 177 | 综上所述,技术架构设计应该以业务需求为核心,结合业务模型、技术深度和可玩性,以及考虑性能、稳定性和敏捷开发的思维,才能为公司创造最大的价值。 178 | 179 | 以上都是站在如何提升竞争力的基础上去思考的。 180 | 181 | #### 3、管理能力 182 | 183 | **在这里,我所强调的管理能力是为了让我们在日常工作中对职业规划有更全面的思考。** 184 | 185 | 我们不应该仅限于**业务**、**技术**或**管理**方面的单一发展,而应该从各个方面来综合考虑。 186 | 187 | 有些人可能认为他们未来应该朝技术专家方向发展,而不需要考虑管理问题。 188 | 189 | 虽然个人能力的突出也是一种职业规划方向,但我们此次讨论的重点是如何拓展职业道路以提高个人竞争力。 190 | 191 | 我们可以通过归纳和总结工作中遇到的问题,提炼出一套适合自己的方法论,并在工作中充分运用。很多时候,只有在实际执行过程中,我们才能发现问题并解决问题。 192 | 193 | 在这里,我并没有详细讨论管理需要具备哪些能力,这个部分仅仅是为了强调管理能力的重要性。最终,提升管理能力也是为了促进业务的更好发展。 194 | 195 | 从个人角度来看,具备管理能力可以从整个团队层面来考虑问题,拓宽视野角度,有着充分的业务理解,协同团队成员,以目标导向,提升个人执行力,进而实施。 196 | 197 | ### 五、语言组织能力 198 | 199 | 200 | ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/08bc8c86415248ab8a4c2acd62f164f2~tplv-k3u1fbpfcp-watermark.image?) 201 | 202 | 我认为当个人能力突出时,如果有着良好的沟通能力,也是极大加分的。 203 | 204 | #### 归纳总结 205 | 206 | * 确定核心要点 207 | * 提炼关键信息 208 | * 提供支持性论据 209 | * 结构清晰 210 | 211 | 此外,在一场会议中,能够从众多纷繁的信息中,根据自己的理解归纳和提取关键信息,并总结出结论或论据,这不仅便于我们个人迅速地理解与记忆,还有利于日后查阅。 212 | 213 | 清晰的结构不仅便于我们自己再次查阅,也有助于他人更快地理解相关内容。此外,这样的会议总结还能促进跨部门或团队间的沟通与协作,使得信息传递更加高效,提高整个组织的工作效率。 214 | 215 | 当然,最重要是,在不断地总结中,加强个人归纳总结的能力,为个人表达能力打下基础。 216 | 217 | #### 表达能力 218 | 219 | 良好的表达能力,我认为在整篇文章中应当是最重要的一点。即使能力再强,如果无法清晰地表达出来,或者表达的意思难以理解,明明自己已经理解了,但说出来的话却失去了原意。 220 | 221 | 在日常工作中,我相信很多人都会遇到这样的人:在描述一个问题时,虽然说了很多,但大家仍然不明白他想表达什么。或者,本可以用一句话简洁地描述的事情,却说了一大堆无关紧要的话。 222 | 223 | 因此,在日常沟通协作中,准确而简洁地描述重点变得尤为重要。 224 | 225 | 举个例子: 226 | 227 | 在企业招聘中,招聘要求通常会包括**沟通能力良好**或**较强的沟通能力**等。 228 | 229 | 以技术面试为例,面试官往往会提出一些与源代码相关的问题,面试者需要回答这些问题以证明自己的能力。 230 | 231 | 源代码问题的答案通常都是简单的,例如`Vue 2`和`Vue 3`的核心原理,或者`React`的**异步调度引擎**等。 232 | 233 | 这些问题的难度在于源代码的封装、抽象以及设计思想和解决问题的思路。 234 | 235 | 因此,我们可以借鉴《**金字塔原理**》这本书,通过结构化的思考和组织方式来回答问题。首先提出问题的**结论**和**关键点**,然后通过**分层逻辑**和**支持性细节**来展开解释。 236 | 237 | 只有我们自己的思维结构足够清晰,听众才能更好地理解。 238 | 239 | 这种能力不仅可以运用在面试中,还可以广泛应用到生活的各个方面。 240 | 241 | ## 最后 242 | 243 | 本篇文章,自从去年下半年以来,我一直在构思如何编写,却始终没有找到合适的切入点。 244 | 245 | 距离我上次在掘金发布文章已有两年,当时的我也没想到,这次我不再分享技术内容,而是分享关于思考和感悟的内容。 246 | 247 | 我认为这样的分享非常有必要,因为它是对我多年工作经验的一次认知总结。 248 | 249 | 将自己的思考用文字表达出来,不仅能加深理解,还能锻炼写作能力,总是有收获的😆。 250 | 251 | 希望大家阅读完本文后能有所启发。 252 | 253 | 欢迎大家一起讨论。 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | -------------------------------------------------------------------------------- /浏览器地址栏里输入URL后的全过程.md: -------------------------------------------------------------------------------- 1 | ## 什么是URL 2 | `URL`是**统一资源定位符**(Uniform Resource Locator),是资源标识最常见的形式。`URL`描述了一台特定服务器上某资源的特定位置。它们可以明确说明如何从一个精确、固定的位置获取资源。 3 | 4 | `URL`说明了协议、服务器和本地资源。 5 | 6 | 而浏览器都是基于`HTTP`协议,而`HTTP`是个应用层的协议。`HTTP`无需操心网络通信的具体细节都交给了`TCP/IP`。 7 | **`TCP`**: 8 | * 无差错的数据传输。 9 | * 按序传输(数据总是按照发送的顺序到达)。 10 | * 未分段的数据流(可以在任意时刻将数据发送出去)。 11 | 12 | HTTP协议位于TCP的上层。HTTP使用TCP来传输其报文数据。 13 | 14 | ![HTTP](https://user-gold-cdn.xitu.io/2019/1/7/168276f60bb83475?w=800&h=296&f=png&s=197061) 15 | 16 | ## 解析URL 17 | 当用户输入一个完整的`URL`之后,浏览器就开始解析`URL`的构成,以便于查找资源地址,大多数URL的语法通用格式如下: 18 | ```http 19 | ://:@:/;?# 20 | ``` 21 | 基本上没有哪个`URL`包含了所有这些组件。`URL`最重要的3个部分是方案`scheme`,主机`host`和路径`path`。 22 | 如果`URL`中不包含`port`,浏览器会默认使用`80`端口进行访问。 23 | 24 | ## DNS域名解析 25 | 26 | ### 什么是DNS? 27 | `DNS`( Domain Name System)是“域名系统”的英文缩写,DNS是应用层协议,事实上他是为其他应用层协议工作的,包括不限于HTTP和SMTP以及FTP,用于将用户提供的主机名解析为ip地址。 28 | 29 | 30 | DNS 查询的过程如下图所示。 31 | ![](https://user-gold-cdn.xitu.io/2019/1/7/16827de892e1666e?w=720&h=391&f=png&s=251669) 32 | 33 | * 在浏览器中输入`www.qq.com` 域名,操作系统会先检查自己本地的`hosts`文件是否有这个网址映射关系,如果有,就先调用这个`IP`地址映射,完成域名解析。 34 | 35 | * 如果`hosts`里没有这个域名的映射,则查找本地`DNS`解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。 36 | 37 | * 如果`hosts`与本地`DNS`解析器缓存都没有相应的网址映射关系,首先会找`TCP/ip`参数中设置的首选`DNS`服务器,在此我们叫它本地`DNS`服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。 38 | 39 | * 如果要查询的域名,不由本地`DNS`服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个`IP`地址映射,完成域名解析,此解析不具有权威性。 40 | 41 | * 如果本地`DNS`服务器本地区域文件与缓存解析都失效,则根据本地`DNS`服务器的设置(是否设置转发器)进行查询,如果未用**转发模式**,本地`DNS`就把请求发至[13台根`DNS`](https://baike.baidu.com/item/%E6%A0%B9%E5%9F%9F%E5%90%8D%E6%9C%8D%E5%8A%A1%E5%99%A8/5907519?fr=aladdin),根`DNS`服务器收到请求后会判断这个域名(`.com`)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个`IP`。本地`DNS`服务器收到`IP`信息后,将会联系负责`.com`域的这台服务器。这台负责`.com`域的服务器收到请求后,如果自己无法解析,它就会找一个管理`.com`域的下一级`DNS`服务器地址(`http://qq.com`)给本地`DNS`服务器。当本地`DNS`服务器收到这个地址后,就会找`http://qq.com`域服务器,重复上面的动作,进行查询,直至找到`www.qq.com`主机。 42 | 43 | * 如果用的是转发模式,此`DNS`服务器就会把请求转发至上一级`DNS`服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根`DNS`或把转请求转至上上级,以此循环。不管是本地`DNS`服务器用是是转发,还是根提示,最后都是把结果返回给本地`DNS`服务器,由此`DNS`服务器再返回给客户机。 44 | 45 | **从客户端到本地`DNS`服务器是属于递归查询,而DNS服务器之间就是的交互查询就是迭代查询。** 46 | 47 | ## 建立TCP连接 48 | ### `TCP`根据不同的当前状态(常规或加星)所发送的内容: 49 | | 常规状态| 说 明 | 发 送 | 加 星 状 态 | 发 送 | 50 | | :----: | :----: | :----: |:----: | :----: | :----: | 51 | | CLOSED | 关闭 | RST, ACK | | | | 52 | | LISTEN | 监听连接请求(被动打开) | | | | | 53 | | SYN_SENT | 已发出SYN (主动打开) | SYN |SYN_SENT* |SYN, FIN | 54 | | SYN_RCVD | 已经发出和收到SYN;等待ACK | SYN, ACK |SYN_RCVD* | SYN, FIN, ACK | 55 | | ESTABLISHED | 连接已经建立(数据传输) | ACK |ESTABLISHED* |SYN, ACK | 56 | | CLOSE_WAIT | 收到FIN,等待应用程序关闭 | ACK |CLOSE_WAIT* |SYN, FIN | 57 | | FIN_WAIT_1 | 已经关闭,发出FIN;等待ACK和FIN | FIN, ACK |FIN_WAIT_1 |SYN, FIN, ACK | 58 | | CLOSING | 两端同时关闭;等待ACK | FIN, ACK |CLOSING* |SYN, FIN, ACK | 59 | | LAST_ACK | 收到FIN已经关闭;等待ACK | FIN, ACK |LAST_ACK* |SYN, FIN, ACK | 60 | | FIN_WAIT_2 | 已经关闭;等待FIN | ACK | | | 61 | | TIME_WAIT | 主动关闭后长达2 M S L的等待状态 | ACK | | | 62 | 63 | `TCP`中定义了7个**扩展状态**,这些扩展状态都称为加星状态。它们分别是:`SYN_SENT*`、 64 | `SYN_RCVD*`、`ESTABLISHED *`、`CLOSE_WAIT *`、`LAST_ACK *`、`FIN_WAIT_1 *`和`CLOSING *`。 65 | 66 | ### `TCP`输入的处理顺序 67 | 68 | `TCP`协议收到报文段时,对其中所携带的各种控制信息 ( `SYN`、`FIN`、`ACK`、`URG`和`RST` 69 | 标志,还可能有数据和选项 )的处理顺序不是随意的,也不是各种实现可以自行决定的。 70 | 71 | 72 | ![](https://user-gold-cdn.xitu.io/2019/1/8/1682b7906b0a395e?w=639&h=704&f=png&s=115649) 73 | 74 | 从 `CLOSED`状态到`SYN_SENT`状态的变迁就标明发送了一个`SYN`报文段。在图中则没有采用这种标记方法,而是在每个状态框中标出处于该状态时要发送的报文段类型。例如,当处于`SYN_RECV`状态时,要发出一个带有 `SYN` 75 | 的报文段,其中还包括对所收到`SYN`的确认( `ACK` )。而当处于`CLOSE_WAIT`状态时,要发出 76 | 对所收到`FIN`的确认( `ACK` )。 77 | 78 | 我们之所以要这样做是因为,在`TCP`协议中我们经常需要处理可能造成多次状态变迁的 79 | 报文段。于是在处理一个报文段时,重要的是处理完报文段后连接所处的最终状态,因为它决定了应答的内容。而如果不使用`TCP`协议,每收到一个报文段通常至多只引起一次状态 80 | 变迁,只有在收到`SYN/ACK`报文段时才是例外。 81 | 82 | ## 三次握手 83 | 84 | 85 | ![](https://user-gold-cdn.xitu.io/2019/1/8/1682cd9c90411515?w=651&h=461&f=png&s=105824) 86 | 87 | 客户端和服务器之间建立连接需要经过**三次确认**的阶段,我们称之为`TCP`的三次握手。 88 | 89 | ### 第一次 90 | > 客户端发送一个`syn`报文,设置发送序号为`X`,客户端进入`SYN_SENT`状态,等待服务器回应。 91 | 92 | ### 第二次 93 | > 服务端收到`syn`报文,但是服务端必须确定客户端的`syn(ack= X + 1)`, 因此服务端也要发送一个`syn=Y`给客户端进行确认,表示服务端已经收到客户端的请求。 94 | 服务端需要发送`ack+syn`给客户端,此时服务器进入`SYN_RECV`状态。 95 | 96 | ### 第三次 97 | 客户端收到服务器的`syn+ack`包,向服务器发送确认包`ack(ack=Y+1)`,此包发送完毕,客户端和服务器进入`ESTABLISHED`(`TCP`连接成功)状态,完成三次握手。 98 | 99 | ### 举个例子 100 | 比如你走在路上,发现前面有你的朋友向你走过来,你向你朋友挥手(**第一次握手**)。 101 | 102 | 你朋友看见了你向他打招呼,但是你朋友因为距离你太远,并不确定是否是跟他打招呼的,因此,你朋友用手指了下自己,并向你示意(**第二次握手**)。 103 | 104 | 你看见了你朋友的表情和动作,你需要给你朋友一个肯定,此时,你点头示意(**第三次握手**)。 105 | 106 | 此时你和你朋友互相聊天(`TCP已连接`)。 107 | 108 | ## 四次挥手 109 | 110 | ![](https://user-gold-cdn.xitu.io/2019/1/8/1682d2cb52fd0bdc?w=1324&h=904&f=png&s=399522) 111 | 112 | 由于`TCP`连接是全双工的,因此每个方向都必须单独进行关闭。 113 | 114 | 这原则是当一方完成它的数据发送任务后就能发送一个`FIN`来终止这个方向的连接。收到一个 `FIN`只意味着这一方向上没有数据流动,一个`TCP`连接在收到一个`FIN`后仍能发送数据。 115 | 116 | 首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。 117 | 118 | * `TCP`客户端发送一个`FIN`,用来关闭客户到服务器的数据传送。 119 | * 服务器收到这个`FIN`,它发回一个`ACK`,确认序号为收到的序号加1。和`SYN`一样,一个`FIN`将占用一个序号。 120 | * 服务器关闭客户端的连接,发送一个`FIN`给客户端。 121 | * 客户端发回`ACK`报文确认,并将确认序号设置为收到序号加1。 122 | 123 | ## 挥手例子 124 | 125 | 你和你朋友聊天,聊着聊着,突然想起来女朋友钥匙丢了,你是回家开门的,现在耽误了半小时了,吓得冷汗都出来了,一想起榴莲。。。 126 | 127 | 这个时候,你赶紧跟你朋友说了情况,你说你马上得回去了,下次再聊(**第一次挥手**)。 128 | 129 | 你朋友听了,觉得也是得赶紧回去,就跟你说你赶紧回去吧。(**第二次挥手**)。 130 | 131 | 然后,你朋友走了,并向你挥手道别(**第三次挥手**)。 132 | 133 | 你看见你朋友跟你道别,你同样也跟你朋友道别(**第四次挥手**)。 134 | 135 | 回去之后,你就需要玩玩你的榴莲了。 136 | 137 | ## 页面渲染 138 | 139 | 浏览器渲染页面。(下一篇更新) 140 | 141 | ## 最后 142 | 143 | 参考文章: 144 | 145 | 1、`HTTP`权威指南。 146 | 147 | 2、[DNS原理及其解析过程【精彩剖析】](http://blog.51cto.com/369369/812889)。 148 | 149 | 3、`TCP-IP`详解卷 150 | 151 | -------------------------------------------------------------------------------- /浏览器渲染原理(处理重排和重绘).md: -------------------------------------------------------------------------------- 1 | 2 | ![](https://user-gold-cdn.xitu.io/2019/1/10/168368f7cf2c6293?w=512&h=512&f=png&s=44571) 3 | > 继续上篇《[浏览器地址栏里输入URL后的全过程](https://juejin.im/post/5c354b656fb9a049e553ce68)》 4 | 5 | ## 前言 6 | 7 | 为什么要了解浏览器的渲染原理?了解浏览器的渲染原理有什么好处?我们做前端开发为什么非要了解浏览器的原理?直接把网页做出来,什么需求,直接一把梭,撸完收工不好吗。 8 | 9 | 但是经常会有人会问,什么是**重排**和**重绘**? 10 | 11 | **重排**也叫**回流**(`Reflow`),**重绘**(`Repaint`),会影响到浏览器的性能,给用户的感觉就是网页访问慢,或者网页会卡顿,不流畅,从而使网页访问量下降。 12 | 13 | 所以,想要尽可能的避免**重排**和**重绘**,就需要了解浏览器的**渲染原理**。 14 | 15 | ## 浏览器工作流程 16 | 17 | ![](https://user-gold-cdn.xitu.io/2019/1/10/16836d0b7afebcdd?w=768&h=250&f=png&s=187861) 18 | 19 | 上图我们可以看出,浏览器会解析三个模块: 20 | * `HTML`,`SVG`,`XHTML`,解析生成`DOM`树。 21 | * `CSS`解析生成`CSS`规则树。 22 | * `JavaScript`用来操作`DOM API`和`CSSOM API`,生成`DOM Tree`和`CSSOM API`。 23 | 24 | 解析完成后,浏览器会通过已经解析好的`DOM Tree` 和 `CSS`规则树来构造 `Rendering` `Tree`。 25 | 26 | * `Rendering Tree` 渲染树并不等同于`DOM`树,因为一些像`Header`或`display:none`的东西就没必要放在渲染树中了。 27 | 28 | * `CSS` 的 `Rule Tree`主要是为了完成匹配并把`CSS Rule`附加上`Rendering`。 29 | * `Tree`上的每个`Element`。也就是`DOM`结点,即`Frame`。然后,计算每个`Frame`(也就是每个`Element`)的位置,这又叫`layout`和`reflow`过程。 30 | * 最后通过调用操作系统`Native GUI`的`API`绘制。 31 | 32 | ## 不同内核的浏览器渲染 33 | 34 | 35 | ![](https://user-gold-cdn.xitu.io/2019/1/10/16836ec4fcc37d74?w=624&h=289&f=png&s=45038) 36 | 上图是`webkit`内核的渲染流程,和总体渲染流程差不多,要构建`HTML`的`DOM Tree`,和`CSS`规则树,然后合并生成`Render Tree`,最后渲染。 37 | 38 | 39 | ![](https://user-gold-cdn.xitu.io/2019/1/10/16836f26e5a17b6a?w=624&h=290&f=png&s=112605) 40 | 这个是`Mozilla`的`Gecko`渲染引擎。 41 | 总体看来渲染流程差不多,只不过在生成渲染树或者`Frame`树时,两者叫法不一致,`webkit`称之为`Layout`,`Gecko`叫做`Reflow`。 42 | 43 | ## 渲染顺序 44 | 45 | 46 | ![](https://user-gold-cdn.xitu.io/2019/1/10/16836f8fa40fd40d?w=665&h=284&f=png&s=80451) 47 | * 当浏览器拿到一个网页后,首先浏览器会先解析`HTML`,如果遇到了外链的`css`,会一下载`css`,一边解析`HTML`。 48 | * 当`css`下载完成后,会继续解析`css`,生成`css Rules tree`,不会影响到`HTML`的解析。 49 | * 当遇到`