├── assets └── change-tree.gif └── README.md /assets/change-tree.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronffy/immer-tutorial/HEAD/assets/change-tree.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Immer 中文文档 2 | 3 | ## 前言 4 | 5 | [Immer](https://github.com/mweststrate/immer) 是 mobx 的作者写的一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对JS不可变数据结构的需求。 6 | 无奈网络上完善的文档实在太少,所以自己写了一份,本篇文章以贴近实战的思路和流程,对 Immer 进行了全面的讲解。 7 | 8 | ## 目录 9 | 10 | - [数据处理存在的问题](#数据处理存在的问题) 11 | - [解决引用类型对象被修改的办法](#解决引用类型对象被修改的办法) 12 | - [immer功能介绍](#immer功能介绍) 13 | - [安装immer](#安装immer) 14 | - [immer如何fix掉那些不爽的问题](#immer如何fix掉那些不爽的问题) 15 | - [概念说明](#概念说明) 16 | - [常用api介绍](#常用api介绍) 17 | - [用immer优化react项目的探索](#用immer优化react项目的探索) 18 | - [抛出需求](#抛出需求) 19 | - [优化setState方法](#优化setState方法) 20 | - [优化reducer](#优化reducer) 21 | - [参考文档](#参考文档) 22 | 23 | ## 数据处理存在的问题 24 | 25 | 先定义一个初始对象,供后面例子使用: 26 | 首先定义一个`currentState`对象,后面的例子使用到变量`currentState`时,如无特殊声明,都是指这个`currentState`对象 27 | ```javascript 28 | let currentState = { 29 | p: { 30 | x: [2], 31 | }, 32 | } 33 | ``` 34 | 35 | 哪些情况会一不小心修改原始对象? 36 | 37 | ```javascript 38 | // Q1 39 | let o1 = currentState; 40 | o1.p = 1; // currentState 被修改了 41 | o1.p.x = 1; // currentState 被修改了 42 | 43 | // Q2 44 | fn(currentState); // currentState 被修改了 45 | function fn(o) { 46 | o.p1 = 1; 47 | return o; 48 | }; 49 | 50 | // Q3 51 | let o3 = { 52 | ...currentState 53 | }; 54 | o3.p.x = 1; // currentState 被修改了 55 | 56 | // Q4 57 | let o4 = currentState; 58 | o4.p.x.push(1); // currentState 被修改了 59 | ``` 60 | 61 | ## 解决引用类型对象被修改的办法 62 | 63 | 1. 深度拷贝,但是深拷贝的成本较高,会影响性能; 64 | 2. [ImmutableJS](https://github.com/facebook/immutable-js),非常棒的一个不可变数据结构的库,可以解决上面的问题,But,跟 Immer 比起来,ImmutableJS 有两个较大的不足: 65 | - 需要使用者学习它的数据结构操作方式,没有 Immer 提供的使用原生对象的操作方式简单、易用; 66 | - 它的操作结果需要通过`toJS`方法才能得到原生对象,这使得在操作一个对象的时候,时刻要主要操作的是原生对象还是 ImmutableJS 的返回结果,稍不注意,就会产生意想不到的 bug。 67 | 68 | 看来目前已知的解决方案,我们都不甚满意,那么 Immer 又有什么高明之处呢? 69 | 70 | ## immer功能介绍 71 | 72 | ### 安装immer 73 | 74 | 欲善其事必先利其器,安装 Immer 是当前第一要务 75 | 76 | ```shell 77 | npm i --save immer 78 | ``` 79 | 80 | ### immer如何fix掉那些不爽的问题 81 | 82 | Fix Q1、Q3 83 | ```js 84 | import produce from 'immer'; 85 | let o1 = produce(currentState, draft => { 86 | draft.p.x = 1; 87 | }) 88 | ``` 89 | 90 | Fix Q2 91 | ```js 92 | import produce from 'immer'; 93 | fn(currentState); // currentState 被修改了 94 | function fn(o) { 95 | return produce(o, draft => { 96 | draft.p1 = 1; 97 | }) 98 | }; 99 | ``` 100 | 101 | Fix Q4 102 | ```js 103 | import produce from 'immer'; 104 | let o4 = produce(currentState, draft => { 105 | draft.p.x.push(1); 106 | }) 107 | ``` 108 | 109 | 是不是使用非常简单,通过小试牛刀,我们简单的了解了 Immer ,下面将对 Immer 的常用 api 分别进行介绍。 110 | 111 | 112 | ### 概念说明 113 | 114 | Immer 涉及概念不多,在此将涉及到的概念先行罗列出来,阅读本文章过程中遇到不明白的概念,可以随时来此处查阅。 115 | 116 | - currentState 117 | 被操作对象的最初状态 118 | 119 | - draftState 120 | 根据 currentState 生成的草稿状态,它是 currentState 的代理,对 draftState 所做的任何修改都将被记录并用于生成 nextState 。在此过程中,currentState 将不受影响 121 | 122 | - nextState 123 | 根据 draftState 生成的最终状态 124 | 125 | - produce 生产 126 | 用来生成 nextState 或 producer 的函数 127 | 128 | - producer 生产者 129 | 通过 produce 生成,用来生产 nextState ,每次执行相同的操作 130 | 131 | - recipe 生产机器 132 | 用来操作 draftState 的函数 133 | 134 | 135 | ### 常用api介绍 136 | 137 | 使用 Immer 前,请确认将`immer`包引入到模块中 138 | 139 | ```js 140 | import produce from 'immer' 141 | ``` 142 | or 143 | ```js 144 | import { produce } from 'immer' 145 | ``` 146 | 147 | 这两种引用方式,produce 是完全相同的 148 | 149 | #### produce 150 | 151 | *备注:出现`PatchListener`先行跳过,后面章节会做介绍* 152 | 153 | ##### 第1种使用方式: 154 | 155 | 语法: 156 | `produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState` 157 | 158 | 例子1: 159 | ```js 160 | let nextState = produce(currentState, (draft) => { 161 | 162 | }) 163 | 164 | currentState === nextState; // true 165 | ``` 166 | 167 | 例子2: 168 | ```js 169 | let currentState = { 170 | a: [], 171 | p: { 172 | x: 1 173 | } 174 | } 175 | 176 | let nextState = produce(currentState, (draft) => { 177 | draft.a.push(2); 178 | }) 179 | 180 | currentState.a === nextState.a; // false 181 | currentState.p === nextState.p; // true 182 | ``` 183 | 184 | 由此可见,对 draftState 的修改都会反应到 nextState 上,而 Immer 使用的结构是共享的,nextState 在结构上又与 currentState 共享未修改的部分,共享效果如图(借用的一篇 Immutable 文章中的动图,侵删): 185 | 186 | ![](./assets/change-tree.gif) 187 | 188 | ##### 自动冻结功能 189 | 190 | Immer 还在内部做了一件很巧妙的事情,那就是通过 produce 生成的 nextState 是被冻结(freeze)的,(Immer 内部使用`Object.freeze`方法,只冻结 nextState 跟 currentState 相比修改的部分),这样,当直接修改 nextState 时,将会报错。 191 | 这使得 nextState 成为了真正的不可变数据。 192 | 193 | 示例: 194 | ```ts 195 | const currentState = { 196 | p: { 197 | x: [2], 198 | }, 199 | }; 200 | const nextState = produce(currentState, draftState => { 201 | draftState.p.x.push(3); 202 | }); 203 | console.log(nextState.p.x); // [2, 3] 204 | nextState.p.x = 4; 205 | console.log(nextState.p.x); // [2, 3] 206 | nextState.p.x.push(5); // 报错 207 | ``` 208 | 209 | ##### 第2种使用方式 210 | 211 | 利用高阶函数的特点,提前生成一个生产者 producer 212 | 213 | 语法: 214 | `produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState` 215 | 216 | 例子: 217 | ```js 218 | let producer = produce((draft) => { 219 | draft.x = 2 220 | }); 221 | let nextState = producer(currentState); 222 | ``` 223 | 224 | 225 | ##### recipe的返回值 226 | 227 | recipe 是否有返回值,nextState 的生成过程是不同的: 228 | recipe 没有返回值时:nextState 是根据 recipe 函数内的 draftState 生成的; 229 | recipe 有返回值时:nextState 是根据 recipe 函数的返回值生成的; 230 | 231 | ```js 232 | let nextState = produce( 233 | currentState, 234 | (draftState) => { 235 | return { 236 | x: 2 237 | } 238 | } 239 | ) 240 | ``` 241 | 242 | 此时,nextState 不再是通过 draftState 生成的了,而是通过 recipe 的返回值生成的。 243 | 244 | ##### recipe中的this 245 | 246 | recipe 函数内部的`this`指向 draftState ,也就是修改`this`与修改 recipe 的参数 draftState ,效果是一样的。 247 | **注意:此处的 recipe 函数不能是箭头函数,如果是箭头函数,`this`就无法指向 draftState 了** 248 | 249 | ```js 250 | produce(currentState, function(draft){ 251 | // 此处,this 指向 draftState 252 | draft === this; // true 253 | }) 254 | ``` 255 | 256 | #### patch补丁功能 257 | 258 | 通过此功能,可以方便进行详细的代码调试和跟踪,可以知道 recipe 内的做的每次修改,还可以实现时间旅行。 259 | 260 | Immer 中,一个 patch 对象是这样的: 261 | ```typescript 262 | interface Patch { 263 | op: "replace" | "remove" | "add" // 一次更改的动作类型 264 | path: (string | number)[] // 此属性指从树根到被更改树杈的路径 265 | value?: any // op为 replace、add 时,才有此属性,表示新的赋值 266 | } 267 | ``` 268 | 269 | 语法: 270 | ```typescript 271 | produce( 272 | currentState, 273 | recipe, 274 | // 通过 patchListener 函数,暴露正向和反向的补丁数组 275 | patchListener: (patches: Patch[], inversePatches: Patch[]) => void 276 | ) 277 | 278 | applyPatches(currentState, changes: (patches | inversePatches)[]): nextState 279 | ``` 280 | 281 | 例子: 282 | 283 | ```js 284 | import produce, { applyPatches } from "immer" 285 | 286 | let state = { 287 | x: 1 288 | } 289 | 290 | let replaces = []; 291 | let inverseReplaces = []; 292 | 293 | state = produce( 294 | state, 295 | draft => { 296 | draft.x = 2; 297 | draft.y = 2; 298 | }, 299 | (patches, inversePatches) => { 300 | replaces = patches.filter(patch => patch.op === 'replace'); 301 | inverseReplaces = inversePatches.filter(patch => patch.op === 'replace'); 302 | } 303 | ) 304 | 305 | state = produce(state, draft => { 306 | draft.x = 3; 307 | }) 308 | console.log('state1', state); // { x: 3, y: 2 } 309 | 310 | state = applyPatches(state, replaces); 311 | console.log('state2', state); // { x: 2, y: 2 } 312 | 313 | state = produce(state, draft => { 314 | draft.x = 4; 315 | }) 316 | console.log('state3', state); // { x: 4, y: 2 } 317 | 318 | state = applyPatches(state, inverseReplaces); 319 | console.log('state4', state); // { x: 1, y: 2 } 320 | ``` 321 | 322 | `state.x`的值4次打印结果分别是:`3、2、4、1`,实现了时间旅行, 323 | 可以分别打印`patches`和`inversePatches`看下, 324 | 325 | `patches`数据如下: 326 | ```js 327 | [ 328 | { 329 | op: "replace", 330 | path: ["x"], 331 | value: 2 332 | }, 333 | { 334 | op: "add", 335 | path: ["y"], 336 | value: 2 337 | }, 338 | ] 339 | ``` 340 | 341 | `inversePatches`数据如下: 342 | ```js 343 | [ 344 | { 345 | op: "replace", 346 | path: ["x"], 347 | value: 1 348 | }, 349 | { 350 | op: "remove", 351 | path: ["y"], 352 | }, 353 | ] 354 | ``` 355 | 356 | 可见,`patchListener`内部对数据操作做了记录,并分别存储为正向操作记录和反向操作记录,供我们使用。 357 | 358 | 至此,Immer 的常用功能和 api 我们就介绍完了。 359 | 360 | 接下来,我们看如何用 Immer ,提高 React 、Redux 项目的开发效率。 361 | 362 | 363 | ## 用immer优化react项目的探索 364 | 365 | 首先定义一个`state`对象,后面的例子使用到变量`state`或访问`this.state`时,如无特殊声明,都是指这个`state`对象 366 | ```js 367 | state = { 368 | members: [ 369 | { 370 | name: 'ronffy', 371 | age: 30 372 | } 373 | ] 374 | } 375 | ``` 376 | 377 | ### 抛出需求 378 | 379 | 就上面定义的`state`,我们先抛一个需求出来,好让后面的讲解有的放矢: 380 | **members 成员中的第1个成员,年龄增加1岁** 381 | 382 | ### 优化setState方法 383 | 384 | #### 错误示例 385 | 386 | ```js 387 | this.state.members[0].age++; 388 | ``` 389 | 只所以有的新手同学会犯这样的错误,很大原因是这样操作实在是太方便了,以至于忘记了操作 State 的规则。 390 | 391 | 下面看下正确的实现方法 392 | 393 | #### setState的第1种实现方法 394 | 395 | ```js 396 | const { members } = this.state; 397 | this.setState({ 398 | members: [ 399 | { 400 | ...members[0], 401 | age: members[0].age + 1, 402 | }, 403 | ...members.slice(1), 404 | ] 405 | }) 406 | ``` 407 | 408 | #### setState的第2种实现方法 409 | 410 | ```js 411 | this.setState(state => { 412 | const { members } = state; 413 | return { 414 | members: [ 415 | { 416 | ...members[0], 417 | age: members[0].age + 1, 418 | }, 419 | ...members.slice(1) 420 | ] 421 | } 422 | }) 423 | ``` 424 | 425 | 以上2种实现方式,就是`setState`的两种使用方法,相比大家都不陌生了,所以就不过多说明了,接下来看下,如果用 Immer 解决,会有怎样的烟火? 426 | 427 | #### 用immer更新state 428 | 429 | ```js 430 | this.setState(produce(draft => { 431 | draft.members[0].age++; 432 | })) 433 | ``` 434 | 435 | 是不是瞬间代码量就少了很多,阅读起来舒服了很多,而且更易于阅读了。 436 | 437 | ### 优化reducer 438 | 439 | #### immer的produce的拓展用法 440 | 441 | 在开始正式探索之前,我们先来看下 produce [第2种使用方式](#第2种使用方式)的拓展用法: 442 | 443 | 例子: 444 | ```js 445 | let obj = {}; 446 | 447 | let producer = produce((draft, arg) => { 448 | obj === arg; // true 449 | }); 450 | let nextState = producer(currentState, obj); 451 | ``` 452 | 453 | 相比 produce 第2种使用方式的例子,多定义了一个`obj`对象,并将其作为 producer 方法的第2个参数传了进去;可以看到, produce 内的 recipe 回调函数的第2个参数与`obj`对象是指向同一块内存。 454 | ok,我们在知道了 produce 的这种拓展用法后,看看能够在 Redux 中发挥什么功效? 455 | 456 | #### 普通reducer怎样解决上面抛出的需求 457 | 458 | ```js 459 | const reducer = (state, action) => { 460 | switch (action.type) { 461 | case 'ADD_AGE': 462 | const { members } = state; 463 | return { 464 | ...state, 465 | members: [ 466 | { 467 | ...members[0], 468 | age: members[0].age + 1, 469 | }, 470 | ...members.slice(1), 471 | ] 472 | } 473 | default: 474 | return state 475 | } 476 | } 477 | ``` 478 | 479 | #### 集合immer,reducer可以怎样写 480 | 481 | ```js 482 | const reducer = (state, action) => produce(state, draft => { 483 | switch (action.type) { 484 | case 'ADD_AGE': 485 | draft.members[0].age++; 486 | } 487 | }) 488 | ``` 489 | 490 | 可以看到,通过 produce ,我们的代码量已经精简了很多; 491 | 不过仔细观察不难发现,利用 produce 能够先制造出 producer 的特点,代码还能更优雅: 492 | 493 | ```js 494 | const reducer = produce((draft, action) => { 495 | switch (action.type) { 496 | case 'ADD_AGE': 497 | draft.members[0].age++; 498 | } 499 | }) 500 | ``` 501 | 502 | 好了,至此,Immer 优化 reducer 的方法也讲解完毕。 503 | 504 | Immer 的使用非常灵活,多多思考,相信你还可以发现 Immer 更多其他的妙用! 505 | 506 | 507 | ## 参考文档 508 | 509 | - [官方文档](https://github.com/mweststrate/immer) 510 | - [Introducing Immer: Immutability the easy way](https://hackernoon.com/introducing-immer-immutability-the-easy-way-9d73d8f71cb3) 511 | --------------------------------------------------------------------------------