├── .babelrc ├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config ├── rollup.base.config.js ├── rollup.compat.config.js ├── rollup.config.devtools-run.js ├── rollup.config.devtools.js ├── rollup.config.js └── rollup.ie8.config.js ├── package.json ├── prop-types.js ├── server-render ├── index.js ├── jsx.js ├── polyfills.js └── util.js ├── src ├── children.ts ├── clone-element.ts ├── compat.ts ├── component.ts ├── constants.ts ├── create-class.ts ├── create-context.ts ├── create-element.ts ├── create-ref.ts ├── declaration.d.ts ├── devtools-base.ts ├── devtools-run.ts ├── devtools.ts ├── dom │ └── index.ts ├── env.ts ├── find.ts ├── forward-ref.ts ├── ie8.ts ├── options.ts ├── polyfill.ts ├── pure-component.ts ├── render.ts ├── types.ts ├── util.ts ├── vdom │ ├── component.ts │ ├── diff.ts │ └── index.ts ├── vnode.ts └── zreact.ts ├── test ├── browser │ ├── components.js │ ├── context.js │ ├── devtools.js │ ├── keys.js │ ├── lifecycle.js │ ├── performance.js │ ├── refs.js │ ├── render.js │ ├── spec.js │ └── svg.js ├── compat │ ├── component.js │ ├── index.js │ ├── jsx.js │ ├── legacy.js │ └── svg.js ├── fiber │ ├── anu-fiber.html │ ├── index.html │ ├── react-fiber.html │ └── zreact-fiber.html ├── karma.conf.js ├── legacy.js ├── node │ └── index.js ├── polyfills.js ├── react-test.html ├── server.js ├── shared │ ├── exports.js │ └── h.js ├── test.html └── ts │ ├── prect-test.tsx │ └── tsconfig.json ├── tsconfig.json ├── tslint.json ├── types ├── zreact.d.ts └── zreact.d.ts.bak ├── typings.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", 4 | { 5 | "loose": true 6 | } 7 | ], 8 | "stage-0", 9 | "react" 10 | ], 11 | "plugins": [ 12 | ["transform-react-jsx", { "pragma":"h" }] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.json] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | /test/test.js 60 | /build/ 61 | /test/coverage/ 62 | /dist/ 63 | /devtools.js 64 | /devtools.js.map 65 | /devtools-run.js 66 | /devtools-run.js.map 67 | /docs/ 68 | *error.log 69 | .DS_Store 70 | 71 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | addons: 3 | chrome: stable 4 | cache: yarn 5 | node_js: 6 | - "9" 7 | before_script: 8 | - yarn config set registry http://registry.npm.taobao.org/ 9 | script: 10 | - yarn test 11 | after_script: 12 | - yarn coverage 13 | brabches: 14 | only: 15 | - master 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [1.5.9](https://github.com/zeromake/zreact/compare/v1.5.5...v1.5.9) (2018-09-10) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * children diff ([af17346](https://github.com/zeromake/zreact/commit/af17346)) 8 | * children diff ([d8c0d76](https://github.com/zeromake/zreact/commit/d8c0d76)) 9 | * deep context lost ([e798821](https://github.com/zeromake/zreact/commit/e798821)) 10 | * nodes children ([ffe2ae8](https://github.com/zeromake/zreact/commit/ffe2ae8)) 11 | 12 | 13 | 14 | 15 | ## [1.5.5](https://github.com/zeromake/zreact/compare/v1.5.4...v1.5.5) (2018-08-30) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * context use default value ([65c81fd](https://github.com/zeromake/zreact/commit/65c81fd)) 21 | * ref value -> current ([6519979](https://github.com/zeromake/zreact/commit/6519979)) 22 | 23 | 24 | 25 | 26 | ## [1.5.4](https://github.com/zeromake/zreact/compare/1.5.3...v1.5.4) (2018-08-22) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * npm has 1.5.3 ([dd1186a](https://github.com/zeromake/zreact/commit/dd1186a)) 32 | 33 | 34 | 35 | 36 | ## [1.5.3](https://github.com/zeromake/zreact/compare/v1.5.2...1.5.3) (2018-08-22) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * PureComponent types ([aa32ba0](https://github.com/zeromake/zreact/commit/aa32ba0)) 42 | 43 | 44 | ### Features 45 | 46 | * add forwardRef api ([2a48c2d](https://github.com/zeromake/zreact/commit/2a48c2d)) 47 | 48 | 49 | 50 | 51 | ## [1.5.2](https://github.com/zeromake/zreact/compare/v1.5.0...v1.5.2) (2018-08-22) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * add react 16 context test ([cb78c3a](https://github.com/zeromake/zreact/commit/cb78c3a)) 57 | * ci ([03dddd4](https://github.com/zeromake/zreact/commit/03dddd4)) 58 | * context ([25d7891](https://github.com/zeromake/zreact/commit/25d7891)) 59 | * karma ([33632bf](https://github.com/zeromake/zreact/commit/33632bf)) 60 | * karma ([ed59024](https://github.com/zeromake/zreact/commit/ed59024)) 61 | * karma browser ([2c62561](https://github.com/zeromake/zreact/commit/2c62561)) 62 | * karma browser ([7586872](https://github.com/zeromake/zreact/commit/7586872)) 63 | * karma browser ([281ae8a](https://github.com/zeromake/zreact/commit/281ae8a)) 64 | * prop ([c1487ba](https://github.com/zeromake/zreact/commit/c1487ba)) 65 | * registry ([8abfd41](https://github.com/zeromake/zreact/commit/8abfd41)) 66 | 67 | 68 | 69 | 70 | # [1.5.0](https://github.com/zeromake/zreact/compare/v1.4.2...v1.5.0) (2018-04-09) 71 | 72 | 73 | ### Features 74 | 75 | * add getSnapshotBeforeUpdate life method ([35de329](https://github.com/zeromake/zreact/commit/35de329)) 76 | 77 | 78 | 79 | 80 | ## [1.4.2](https://github.com/zeromake/zreact/compare/v1.4.1...v1.4.2) (2018-04-02) 81 | 82 | 83 | ### Bug Fixes 84 | 85 | * getDerivedStateFromProps ([8847533](https://github.com/zeromake/zreact/commit/8847533)) 86 | 87 | 88 | ### Features 89 | 90 | * update test webpack4 ([6c0d967](https://github.com/zeromake/zreact/commit/6c0d967)) 91 | 92 | 93 | 94 | 95 | ## [1.4.1](https://github.com/zeromake/zreact/compare/v1.4.0...v1.4.1) (2018-02-27) 96 | 97 | 98 | ### Bug Fixes 99 | 100 | * move isTextNode on utils ([7da9746](https://github.com/zeromake/zreact/commit/7da9746)) 101 | * void node ([0e70ef3](https://github.com/zeromake/zreact/commit/0e70ef3)) 102 | 103 | 104 | 105 | 106 | # [1.4.0](https://github.com/zeromake/zreact/compare/v1.3.0...v1.4.0) (2018-02-24) 107 | 108 | 109 | ### Bug Fixes 110 | 111 | * context ([e7090e9](https://github.com/zeromake/zreact/commit/e7090e9)) 112 | * context support react 16.3-alpha.0 ([8316a76](https://github.com/zeromake/zreact/commit/8316a76)) 113 | * preact bug ([2d977ef](https://github.com/zeromake/zreact/commit/2d977ef)) 114 | * preact bug ([d33c554](https://github.com/zeromake/zreact/commit/d33c554)) 115 | 116 | 117 | ### Features 118 | 119 | * add context ([dc29e67](https://github.com/zeromake/zreact/commit/dc29e67)) 120 | * devtools void Component support ([20319f4](https://github.com/zeromake/zreact/commit/20319f4)) 121 | * props children change to react ([6bb9bcc](https://github.com/zeromake/zreact/commit/6bb9bcc)) 122 | * react 16 context api update ([c22f811](https://github.com/zeromake/zreact/commit/c22f811)) 123 | 124 | 125 | 126 | 127 | # [1.3.0](https://github.com/zeromake/zreact/compare/v1.2.1...v1.3.0) (2018-02-09) 128 | 129 | 130 | ### Bug Fixes 131 | 132 | * typescript key is number ([6cb8edf](https://github.com/zeromake/zreact/commit/6cb8edf)) 133 | 134 | 135 | ### Features 136 | 137 | * add react 16.3 createRef ([3befb77](https://github.com/zeromake/zreact/commit/3befb77)) 138 | 139 | 140 | 141 | 142 | ## [1.2.1](https://github.com/zeromake/zreact/compare/v1.2.0...v1.2.1) (2018-02-06) 143 | 144 | 145 | ### Bug Fixes 146 | 147 | * init state ([ebbd703](https://github.com/zeromake/zreact/commit/ebbd703)) 148 | 149 | 150 | 151 | 152 | # [1.2.0](https://github.com/zeromake/zreact/compare/v1.1.1...v1.2.0) (2018-02-06) 153 | 154 | 155 | ### Bug Fixes 156 | 157 | * createPortal ([7ce4fe1](https://github.com/zeromake/zreact/commit/7ce4fe1)) 158 | 159 | 160 | ### Features 161 | 162 | * Component add static func getDerivedStateFromProps ([a5b3fd9](https://github.com/zeromake/zreact/commit/a5b3fd9)) 163 | 164 | 165 | 166 | 167 | ## [1.1.1](https://github.com/zeromake/zreact/compare/v1.1.0...v1.1.1) (2018-01-24) 168 | 169 | 170 | ### Bug Fixes 171 | 172 | * compat ([0aa2cb4](https://github.com/zeromake/zreact/commit/0aa2cb4)) 173 | * rollup config ([ab93fd7](https://github.com/zeromake/zreact/commit/ab93fd7)) 174 | 175 | 176 | ### Features 177 | 178 | * vnode add typeof type props ([3b31bc8](https://github.com/zeromake/zreact/commit/3b31bc8)) 179 | 180 | 181 | 182 | 183 | # [1.1.0](https://github.com/zeromake/zreact/compare/v1.0.3...v1.1.0) (2018-01-16) 184 | 185 | 186 | ### Bug Fixes 187 | 188 | * empty children is null ([14f5d82](https://github.com/zeromake/zreact/commit/14f5d82)) 189 | 190 | 191 | ### Features 192 | 193 | * add react function ([2450011](https://github.com/zeromake/zreact/commit/2450011)) 194 | * render null is not dom ([fe1e4d0](https://github.com/zeromake/zreact/commit/fe1e4d0)) 195 | 196 | 197 | 198 | 199 | ## [1.0.3](https://github.com/zeromake/zreact/compare/v1.0.2...v1.0.3) (2018-01-05) 200 | 201 | 202 | ### Bug Fixes 203 | 204 | * types.d.ts ([d03d3c9](https://github.com/zeromake/zreact/commit/d03d3c9)) 205 | 206 | 207 | 208 | 209 | ## [1.0.2](https://github.com/zeromake/zreact/compare/v1.0.1...v1.0.2) (2017-12-25) 210 | 211 | 212 | ### Bug Fixes 213 | 214 | * zreact.d.ts ([035fd5d](https://github.com/zeromake/zreact/commit/035fd5d)) 215 | 216 | 217 | 218 | 219 | ## [1.0.1](https://github.com/zeromake/zreact/compare/v1.0.0...v1.0.1) (2017-12-24) 220 | 221 | 222 | ### Bug Fixes 223 | 224 | * zreact.d.ts ([e43e27e](https://github.com/zeromake/zreact/commit/e43e27e)) 225 | 226 | 227 | 228 | 229 | # [1.0.0](https://github.com/zeromake/zreact/compare/v0.3.3...v1.0.0) (2017-12-23) 230 | 231 | 232 | ### Bug Fixes 233 | 234 | * types.d.ts ([94f8728](https://github.com/zeromake/zreact/commit/94f8728)) 235 | 236 | 237 | 238 | 239 | ## [0.3.3](https://github.com/zeromake/zreact/compare/v0.3.2...v0.3.3) (2017-12-22) 240 | 241 | 242 | ### Bug Fixes 243 | 244 | * diffattributes ([86bb04a](https://github.com/zeromake/zreact/commit/86bb04a)) 245 | 246 | 247 | 248 | 249 | ## [0.3.2](https://github.com/zeromake/zreact/compare/v0.3.1...v0.3.2) (2017-12-22) 250 | 251 | 252 | ### Bug Fixes 253 | 254 | * **devtools:** componentUpdated instanceMap is Element key ([d7a05ab](https://github.com/zeromake/zreact/commit/d7a05ab)) 255 | 256 | 257 | ### Features 258 | 259 | * old dom attr ([9ca8ade](https://github.com/zeromake/zreact/commit/9ca8ade)) 260 | 261 | 262 | 263 | 264 | ## [0.3.1](https://github.com/zeromake/zreact/compare/v0.3.0...v0.3.1) (2017-12-20) 265 | 266 | 267 | ### Bug Fixes 268 | 269 | * **diff/component:** vdom.component not clear ([0f785ef](https://github.com/zeromake/zreact/commit/0f785ef)) 270 | 271 | 272 | ### Features 273 | 274 | * add tools Children ([d184736](https://github.com/zeromake/zreact/commit/d184736)) 275 | 276 | 277 | 278 | 279 | # [0.3.0](https://github.com/zeromake/zreact/compare/v0.2.2...v0.3.0) (2017-12-19) 280 | 281 | 282 | ### Bug Fixes 283 | 284 | * scripts are deprecated ([65e86d0](https://github.com/zeromake/zreact/commit/65e86d0)) 285 | 286 | 287 | ### Features 288 | 289 | * createElement miss null child, vnode.children default is undefined ([fe56a93](https://github.com/zeromake/zreact/commit/fe56a93)) 290 | 291 | 292 | 293 | 294 | ## [0.2.2](https://github.com/zeromake/zreact/compare/v0.2.1...v0.2.2) (2017-12-13) 295 | 296 | 297 | ### Bug Fixes 298 | 299 | * **types:** devtools typescript ([3209256](https://github.com/zeromake/zreact/commit/3209256)) 300 | * add package.json files ([a5400b4](https://github.com/zeromake/zreact/commit/a5400b4)) 301 | 302 | 303 | 304 | 305 | ## [0.2.1](https://github.com/zeromake/zreact/compare/v0.1.3...v0.2.1) (2017-11-02) 306 | 307 | 308 | ### Features 309 | 310 | * add findDOMNode, findVDom ([0f3b088](https://github.com/zeromake/zreact/commit/0f3b088)) 311 | * copy preact d.ts ([7333d93](https://github.com/zeromake/zreact/commit/7333d93)) 312 | 313 | 314 | 315 | 316 | ## [0.1.3](https://github.com/zeromake/zreact/compare/v0.1.2...v0.1.3) (2017-10-07) 317 | 318 | 319 | ### Bug Fixes 320 | 321 | * **types:** ts types error ([6a0d702](https://github.com/zeromake/zreact/commit/6a0d702)) 322 | 323 | 324 | 325 | 326 | ## [0.1.2](https://github.com/zeromake/zreact/compare/v0.1.1...v0.1.2) (2017-10-03) 327 | 328 | 329 | ### Bug Fixes 330 | 331 | * componentWillUnmount triggered ([11fd90f](https://github.com/zeromake/zreact/commit/11fd90f)) 332 | * componentWillUnmount triggered on dom unmout after ([5e9d1a2](https://github.com/zeromake/zreact/commit/5e9d1a2)) 333 | 334 | 335 | ### Features 336 | 337 | * add ci and coverage ([5b57703](https://github.com/zeromake/zreact/commit/5b57703)) 338 | 339 | 340 | 341 | 342 | ## [0.1.1](https://github.com/zeromake/zreact/compare/v0.1.0...v0.1.1) (2017-09-21) 343 | 344 | 345 | ### Bug Fixes 346 | 347 | * **vnode:** vnode change class ([855ebb5](https://github.com/zeromake/zreact/commit/855ebb5)) 348 | * compat renderSubtreeIntoContainer error ([b547a3b](https://github.com/zeromake/zreact/commit/b547a3b)) 349 | 350 | 351 | ### Features 352 | 353 | * add compat ([56a188c](https://github.com/zeromake/zreact/commit/56a188c)) 354 | 355 | 356 | 357 | 358 | # [0.1.0](https://github.com/zeromake/zreact/compare/v0.0.4...v0.1.0) (2017-09-08) 359 | 360 | 361 | ### Bug Fixes 362 | 363 | * del devtools ([6ae3797](https://github.com/zeromake/zreact/commit/6ae3797)) 364 | * update api on d.ts ([cd3bfdb](https://github.com/zeromake/zreact/commit/cd3bfdb)) 365 | * update package and rollup config ([ab19d75](https://github.com/zeromake/zreact/commit/ab19d75)) 366 | * **vdom:** Text key, children set undefined ([f1bd5d3](https://github.com/zeromake/zreact/commit/f1bd5d3)) 367 | 368 | 369 | ### Features 370 | 371 | * add version ([22324be](https://github.com/zeromake/zreact/commit/22324be)) 372 | * add vnode event bind this ([5a79412](https://github.com/zeromake/zreact/commit/5a79412)) 373 | * render api change ([342efaa](https://github.com/zeromake/zreact/commit/342efaa)) 374 | * **component:** add like vue's vm.$emit function ([3a61707](https://github.com/zeromake/zreact/commit/3a61707)) 375 | * **event:** filter event not is component ([07e65f9](https://github.com/zeromake/zreact/commit/07e65f9)) 376 | 377 | 378 | 379 | 380 | ## [0.0.4](https://github.com/zeromake/zreact/compare/v0.0.3...v0.0.4) (2017-08-17) 381 | 382 | 383 | ### Bug Fixes 384 | 385 | * class change on interface ([cb1eb89](https://github.com/zeromake/zreact/commit/cb1eb89)) 386 | * render initDevTools add scheduling ([e8568d6](https://github.com/zeromake/zreact/commit/e8568d6)) 387 | * scheduling change defer ([7bbebb7](https://github.com/zeromake/zreact/commit/7bbebb7)) 388 | * update package; test error; dev_tools init change ([eea0856](https://github.com/zeromake/zreact/commit/eea0856)) 389 | * zreact.d.ts and package update ([c5d7237](https://github.com/zeromake/zreact/commit/c5d7237)) 390 | 391 | 392 | 393 | 394 | ## [0.0.3](https://github.com/zeromake/zreact/compare/v0.0.2...v0.0.3) (2017-08-09) 395 | 396 | 397 | ### Bug Fixes 398 | 399 | * component unmount children not clear ([46639fa](https://github.com/zeromake/zreact/commit/46639fa)) 400 | * server render not document ([e4fcbb5](https://github.com/zeromake/zreact/commit/e4fcbb5)) 401 | 402 | 403 | ### Features 404 | 405 | * add fiber ([9f85010](https://github.com/zeromake/zreact/commit/9f85010)) 406 | * add fiber demo ([59aaf1a](https://github.com/zeromake/zreact/commit/59aaf1a)) 407 | 408 | 409 | 410 | 411 | ## [0.0.2](https://github.com/zeromake/zreact/compare/v0.0.1...v0.0.2) (2017-08-03) 412 | 413 | 414 | ### Bug Fixes 415 | 416 | * add Annotations ([1ca7126](https://github.com/zeromake/zreact/commit/1ca7126)) 417 | * add context on dom patch ([19c0640](https://github.com/zeromake/zreact/commit/19c0640)) 418 | * child.base lost ([ca1fd8c](https://github.com/zeromake/zreact/commit/ca1fd8c)) 419 | * **typescript:** generate ([b1bf9fb](https://github.com/zeromake/zreact/commit/b1bf9fb)) 420 | * **typescript:** package typings ([e62a453](https://github.com/zeromake/zreact/commit/e62a453)) 421 | * delete docs ([4c7b868](https://github.com/zeromake/zreact/commit/4c7b868)) 422 | * package file home add ([5ce4a52](https://github.com/zeromake/zreact/commit/5ce4a52)) 423 | * up Annotations ([6a32ed4](https://github.com/zeromake/zreact/commit/6a32ed4)) 424 | * up Annotations ([2a78b36](https://github.com/zeromake/zreact/commit/2a78b36)) 425 | 426 | 427 | ### Features 428 | 429 | * add typescript type ([ae206d3](https://github.com/zeromake/zreact/commit/ae206d3)) 430 | * **devtools:** add devtools.js ([2bb5585](https://github.com/zeromake/zreact/commit/2bb5585)) 431 | * **docs:** add docs ([8223280](https://github.com/zeromake/zreact/commit/8223280)) 432 | * **tag:** v0.0.1 changlog ([cec803b](https://github.com/zeromake/zreact/commit/cec803b)) 433 | * **vdom:** add VDom context replace dom context ([aac1783](https://github.com/zeromake/zreact/commit/aac1783)) 434 | * build v0.0.2 ([43afdae](https://github.com/zeromake/zreact/commit/43afdae)) 435 | * package cmd ([3e991e3](https://github.com/zeromake/zreact/commit/3e991e3)) 436 | 437 | 438 | 439 | 440 | ## [0.0.1](https://github.com/zeromake/zreact/compare/afddb4c...v0.0.1) (2017-07-24) 441 | 442 | 443 | ### Bug Fixes 444 | 445 | * Component lost code ([b6c187d](https://github.com/zeromake/zreact/commit/b6c187d)) 446 | * **vdom:** domChild attr no clear ([cac0c49](https://github.com/zeromake/zreact/commit/cac0c49)) 447 | * copy code ok ([d482619](https://github.com/zeromake/zreact/commit/d482619)) 448 | * global child,component replace not unmount ([5f94e0e](https://github.com/zeromake/zreact/commit/5f94e0e)) 449 | * ie8 test ([b391d04](https://github.com/zeromake/zreact/commit/b391d04)) 450 | * render no exprot and html test ([133af07](https://github.com/zeromake/zreact/commit/133af07)) 451 | * rollup build ([ac2cae3](https://github.com/zeromake/zreact/commit/ac2cae3)) 452 | * **vdom:** childNodes no sync; class not set svg; capture error ([70a23c8](https://github.com/zeromake/zreact/commit/70a23c8)) 453 | * **vdom:** idiff child no sync dom error ([db306ca](https://github.com/zeromake/zreact/commit/db306ca)) 454 | * **vdom:** isSameNodeType error ([acbe1c2](https://github.com/zeromake/zreact/commit/acbe1c2)) 455 | * **vdom:** render merge null unmount; innerDiffNode children no unmount ([6159bf3](https://github.com/zeromake/zreact/commit/6159bf3)) 456 | * **vdom:** unmount domChild children cause removeNode error ([4ec1c67](https://github.com/zeromake/zreact/commit/4ec1c67)) 457 | 458 | 459 | ### Features 460 | 461 | * **config:** add ts rollup config file ([afddb4c](https://github.com/zeromake/zreact/commit/afddb4c)) 462 | * **src:** add createElement ([dbe3811](https://github.com/zeromake/zreact/commit/dbe3811)) 463 | * add changelog ([69e0231](https://github.com/zeromake/zreact/commit/69e0231)) 464 | * add CHANGELOG.md file ([73939e6](https://github.com/zeromake/zreact/commit/73939e6)) 465 | 466 | 467 | 468 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 zeromake 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Travis Build Status](https://travis-ci.org/zeromake/zreact.svg?branch=master)](https://travis-ci.org/zeromake/zreact) 2 | [![Coverage Status](https://coveralls.io/repos/github/zeromake/zreact/badge.svg?branch=master)](https://coveralls.io/github/zeromake/zreact?branch=master) 3 | 4 | # zreact 5 | 6 | copy to [preact](https://github.com/developit/preact) 7 | 8 | ## Differ from preact 9 | 10 | - source is typescript 11 | - render return (null | undefined | boolean) not is a empty text and not render dom but devtools has vnode 12 | - props.children is empty not is `[]` is a `null` 13 | - add react api: 14 | - PureComponent 15 | - createClass 16 | - createFactory 17 | - createPortal 18 | - findDOMNode 19 | - isValidElement 20 | - unmountComponentAtNode 21 | - unstable_renderSubtreeIntoContainer 22 | - Children 23 | - add react 16 api: 24 | - createRef 25 | - Component.getDerivedStateFromProps 26 | - deprecated: 27 | - Component.prototype.componentWillMount 28 | - Component.prototype.componentWillReceiveProps 29 | - Component.prototype.componentWillUpdate 30 | -------------------------------------------------------------------------------- /config/rollup.base.config.js: -------------------------------------------------------------------------------- 1 | const rollupTypescript = require('rollup-typescript') 2 | const { uglify } = require('rollup-plugin-uglify') 3 | const { minify } = require('uglify-es') 4 | const replace = require('rollup-plugin-replace') 5 | const pkg = require('../package.json') 6 | 7 | const isProduction = process.env.NODE_ENV === 'production' 8 | 9 | const rollupTypescriptPlugin = rollupTypescript({typescript: require('typescript')}) 10 | const replacePlugin = replace({ 11 | VERSION_ENV: JSON.stringify(pkg.version), 12 | ENV: JSON.stringify(process.env.NODE_ENV) 13 | }) 14 | 15 | module.exports = { 16 | plugins: !isProduction ? [ 17 | rollupTypescriptPlugin, 18 | replacePlugin 19 | ] : [ 20 | rollupTypescriptPlugin, 21 | uglify({}, minify), 22 | replacePlugin 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /config/rollup.compat.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('./rollup.base.config') 2 | const isProduction = process.env.NODE_ENV === 'production' 3 | 4 | const config = Object.assign({ 5 | input: 'src/compat.ts', 6 | external: ['zreact', 'prop-types'], 7 | output:[ 8 | { 9 | globals: { 10 | zreact: 'zreact', 11 | 'prop-types': 'prop-types' 12 | }, 13 | name: 'zreact', 14 | format: 'es', 15 | file: 'dist/compat.js', 16 | sourcemap: !isProduction 17 | } 18 | ] 19 | }, baseConfig) 20 | 21 | export default config 22 | -------------------------------------------------------------------------------- /config/rollup.config.devtools-run.js: -------------------------------------------------------------------------------- 1 | import rollupTypescript from 'rollup-typescript' 2 | 3 | // set new typescript 4 | const rollupTypescriptPlugin = rollupTypescript({typescript: require('typescript')}) 5 | export default { 6 | input: 'src/devtools-run.ts', 7 | external: ['zreact'], 8 | output: { 9 | globals: { 10 | zreact: 'zreact' 11 | }, 12 | format: 'iife', 13 | file: 'devtools-run.js', 14 | sourcemap: true 15 | }, 16 | plugins: [ 17 | rollupTypescriptPlugin, 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /config/rollup.config.devtools.js: -------------------------------------------------------------------------------- 1 | import rollupTypescript from 'rollup-typescript' 2 | 3 | // set new typescript 4 | const rollupTypescriptPlugin = rollupTypescript({typescript: require('typescript')}) 5 | export default { 6 | input: 'src/devtools.ts', 7 | external: ['zreact'], 8 | output: { 9 | globals: { 10 | zreact: 'zreact' 11 | }, 12 | format: 'es', 13 | file: 'devtools.js', 14 | sourcemap: true 15 | }, 16 | plugins: [ 17 | rollupTypescriptPlugin, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | const pkg = require('../package.json') 2 | const baseConfig = require('./rollup.base.config') 3 | 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const name = 'zreact' 6 | 7 | const config = Object.assign({ 8 | input: 'src/zreact.ts', 9 | output: isProduction ? [ 10 | { 11 | name, 12 | format: 'umd', 13 | file: pkg["minified:main"], 14 | sourcemap: !isProduction 15 | } 16 | ] : [ 17 | { 18 | name, 19 | format: 'umd', 20 | file: pkg.main, 21 | sourcemap: !isProduction 22 | }, 23 | { 24 | name, 25 | format: 'es', 26 | file: pkg.module, 27 | sourcemap: !isProduction 28 | } 29 | ] 30 | }, baseConfig) 31 | 32 | export default config 33 | -------------------------------------------------------------------------------- /config/rollup.ie8.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('./rollup.base.config') 2 | 3 | const isProduction = process.env.NODE_ENV === 'production' 4 | const config = Object.assign({ 5 | input: 'src/ie8.ts', 6 | output:[ 7 | { 8 | name: 'zreact', 9 | format: 'umd', 10 | file: 'dist/zreact.ie8.js', 11 | sourcemap: !isProduction 12 | } 13 | ] 14 | }, baseConfig) 15 | 16 | export default config 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zreact", 3 | "version": "1.5.9", 4 | "description": "React like,copy by preact", 5 | "main": "dist/zreact.js", 6 | "module": "dist/zreact.esm.js", 7 | "jsnext:main": "dist/zreact.esm.js", 8 | "minified:main": "dist/zreact.min.js", 9 | "typings": "types/zreact.d.ts", 10 | "repository": "https://github.com/zeromake/zreact", 11 | "author": "zeromake ", 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/zeromake/zreact/issues" 15 | }, 16 | "homepage": "https://github.com/zeromake/zreact", 17 | "scripts": { 18 | "size": "node -e \"process.stdout.write('gzip size: ')\" && gzip-size --raw dist/zreact.min.js", 19 | "clean": "rimraf dist devtools.js devtools.js.map devtools-run.js devtools-run.js.map build test/coverage", 20 | "build:all": "npm run clean && npm run build:dev && npm run build && npm run build:ie8 && npm run build:compat && npm run build:devtools && npm run build:devtools-run && npm run size", 21 | "build": "cross-env NODE_ENV=production DEVTOOLS_ENV=production rollup -c config/rollup.config.js", 22 | "build:devtools-run": "rollup -c config/rollup.config.devtools-run.js", 23 | "build:devtools": "rollup -c config/rollup.config.devtools.js", 24 | "build:dev": "cross-env DEVTOOLS_ENV=production NODE_ENV=dev rollup -c config/rollup.config.js", 25 | "build:ie8": "cross-env DEVTOOLS_ENV=production NODE_ENV=dev rollup -c config/rollup.ie8.config.js", 26 | "build:compat": "cross-env DEVTOOLS_ENV=production NODE_ENV=dev rollup -c config/rollup.compat.config.js", 27 | "dev": "cross-env DEVTOOLS_ENV=production NODE_ENV=dev rollup -c config/rollup.config.js -w", 28 | "test": "tslint src/**/*.ts && tsc && npm run test:karma", 29 | "test:karma": "karma start test/karma.conf.js --single-run && npm run ts-coverage-lcov", 30 | "test:karma:watch": "karma start test/karma.conf.js --no-single-run", 31 | "ts-coverage-html": "remap-istanbul -i test/coverage/coverage-final.json -o test/coverage/html -t html -e node_modules,test", 32 | "ts-coverage-lcov": "remap-istanbul -i test/coverage/coverage-final.json -o test/coverage/lcov.info -t lcovonly -e node_modules,test", 33 | "coverage": "cat ./test/coverage/lcov.info | coveralls", 34 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 35 | "prepublishOnly": "npm run build:all", 36 | "docs": "rimraf docs && typedoc --exclude src/devtools-base.ts src/devtools-run.ts --out docs src" 37 | }, 38 | "keywords": [ 39 | "zreact", 40 | "react", 41 | "virtual dom", 42 | "vdom", 43 | "components", 44 | "virtual", 45 | "dom" 46 | ], 47 | "files": [ 48 | "src", 49 | "dist", 50 | "types", 51 | "devtools.js", 52 | "devtools.js.map", 53 | "devtools-run.js", 54 | "devtools-run.js.map", 55 | "prop-types.js", 56 | "typings.json", 57 | "LICENSE", 58 | "README.md", 59 | "package.json" 60 | ], 61 | "devDependencies": { 62 | "babel-core": "^6.26.3", 63 | "babel-loader": "^7.1.5", 64 | "babel-plugin-transform-react-jsx": "^6.24.1", 65 | "babel-preset-env": "^1.7.0", 66 | "babel-preset-react": "^6.24.1", 67 | "babel-preset-stage-0": "^6.24.1", 68 | "chai": "^4.1.2", 69 | "conventional-changelog-cli": "^2.0.5", 70 | "coveralls": "^3.0.2", 71 | "cross-env": "^5.2.0", 72 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 73 | "gzip-size-cli": "^3.0.0", 74 | "istanbul-instrumenter-loader": "^3.0.1", 75 | "karma": "^3.0.0", 76 | "karma-chai-sinon": "^0.1.5", 77 | "karma-chrome-launcher": "^2.2.0", 78 | "karma-coverage-istanbul-reporter": "^2.0.1", 79 | "karma-mocha": "^1.3.0", 80 | "karma-mocha-reporter": "^2.2.5", 81 | "karma-remap-istanbul": "^0.6.0", 82 | "karma-sourcemap-loader": "^0.3.7", 83 | "karma-webpack": "^3.0.0", 84 | "mocha": "^5.2.0", 85 | "prop-types": "^15.6.2", 86 | "remap-istanbul": "^0.12.0", 87 | "rimraf": "^2.6.2", 88 | "rollup": "^0.66.2", 89 | "rollup-plugin-alias": "^1.4.0", 90 | "rollup-plugin-replace": "^2.0.0", 91 | "rollup-plugin-uglify": "^6.0.0", 92 | "rollup-typescript": "^1.2.0", 93 | "rollup-watch": "^4.3.1", 94 | "sinon": "^4.5.0", 95 | "sinon-chai": "^3.2.0", 96 | "ts-loader": "^5.1.1", 97 | "tslint": "^5.9.1", 98 | "typedoc": "^0.12.0", 99 | "typescript": "^3.0.3", 100 | "uglify-es": "^3.3.9", 101 | "webpack": "^4.19.1" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /prop-types.js: -------------------------------------------------------------------------------- 1 | var check = () => { 2 | return check; 3 | }; 4 | check.isRequired = check; 5 | module.exports = { 6 | array: check, 7 | bool: check, 8 | func: check, 9 | number: check, 10 | object: check, 11 | string: check, 12 | symbol: check, 13 | 14 | any: check, 15 | arrayOf: check, 16 | element: check, 17 | instanceOf: check, 18 | node: check, 19 | objectOf: check, 20 | oneOf: check, 21 | oneOfType: check, 22 | shape: check, 23 | exact: check, 24 | }; 25 | -------------------------------------------------------------------------------- /server-render/index.js: -------------------------------------------------------------------------------- 1 | import { objectKeys, encodeEntities, indent, isLargeString, styleObjToCss, assign, getNodeProps } from './util'; 2 | 3 | const SHALLOW = { shallow: true }; 4 | 5 | // components without names, kept as a hash for later comparison to return consistent UnnamedComponentXX names. 6 | const UNNAMED = []; 7 | 8 | const VOID_ELEMENTS = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/; 9 | 10 | 11 | /** Render Preact JSX + Components to an HTML string. 12 | * @name render 13 | * @function 14 | * @param {VNode} vnode JSX VNode to render. 15 | * @param {Object} [context={}] Optionally pass an initial context object through the render path. 16 | * @param {Object} [options={}] Rendering options 17 | * @param {Boolean} [options.shallow=false] If `true`, renders nested Components as HTML elements (``). 18 | * @param {Boolean} [options.xml=false] If `true`, uses self-closing tags for elements without children. 19 | * @param {Boolean} [options.pretty=false] If `true`, adds whitespace for readability 20 | */ 21 | renderToString.render = renderToString; 22 | 23 | 24 | /** Only render elements, leaving Components inline as ``. 25 | * This method is just a convenience alias for `render(vnode, context, { shallow:true })` 26 | * @name shallow 27 | * @function 28 | * @param {VNode} vnode JSX VNode to render. 29 | * @param {Object} [context={}] Optionally pass an initial context object through the render path. 30 | */ 31 | let shallowRender = (vnode, context) => renderToString(vnode, context, SHALLOW); 32 | 33 | 34 | /** The default export is an alias of `render()`. */ 35 | function renderToString(vnode, context, opts, inner, isSvgMode) { 36 | if (vnode==null || typeof vnode==='boolean') { 37 | return ''; 38 | } 39 | 40 | let nodeName = vnode.type, 41 | attributes = vnode.props, 42 | isComponent = false; 43 | context = context || {}; 44 | opts = opts || {}; 45 | 46 | let pretty = opts.pretty, 47 | indentChar = typeof pretty==='string' ? pretty : '\t'; 48 | 49 | // #text nodes 50 | if (typeof vnode!=='object' && !nodeName) { 51 | return encodeEntities(vnode); 52 | } 53 | 54 | // components 55 | if (typeof nodeName==='function') { 56 | isComponent = true; 57 | if (opts.shallow && (inner || opts.renderRootComponent===false)) { 58 | nodeName = getComponentName(nodeName); 59 | } 60 | else { 61 | let props = getNodeProps(vnode), 62 | rendered; 63 | 64 | if (!nodeName.prototype || typeof nodeName.prototype.render!=='function') { 65 | // stateless functional components 66 | rendered = nodeName(props, context); 67 | } 68 | else { 69 | // class-based components 70 | let c = new nodeName(props, context); 71 | // turn off stateful re-rendering: 72 | c._disable = c.__x = true; 73 | c.props = props; 74 | c.context = context; 75 | if (c.componentWillMount) c.componentWillMount(); 76 | rendered = c.render(c.props, c.state, c.context); 77 | 78 | if (c.getChildContext) { 79 | context = assign(assign({}, context), c.getChildContext()); 80 | } 81 | } 82 | 83 | return renderToString(rendered, context, opts, opts.shallowHighOrder!==false); 84 | } 85 | } 86 | 87 | // render JSX to HTML 88 | let s = '', html; 89 | 90 | if (attributes) { 91 | let attrs = objectKeys(attributes); 92 | 93 | // allow sorting lexicographically for more determinism (useful for tests, such as via preact-jsx-chai) 94 | if (opts && opts.sortAttributes===true) attrs.sort(); 95 | 96 | for (let i=0; i]/)) continue; 102 | 103 | if (!(opts && opts.allAttributes) && (name==='key' || name==='ref')) continue; 104 | 105 | if (name==='className') { 106 | if (attributes.class) continue; 107 | name = 'class'; 108 | } 109 | else if (isSvgMode && name.match(/^xlink:?./)) { 110 | name = name.toLowerCase().replace(/^xlink:?/, 'xlink:'); 111 | } 112 | 113 | if (name==='style' && v && typeof v==='object') { 114 | v = styleObjToCss(v); 115 | } 116 | 117 | let hooked = opts.attributeHook && opts.attributeHook(name, v, context, opts, isComponent); 118 | if (hooked || hooked==='') { 119 | s += hooked; 120 | continue; 121 | } 122 | 123 | if (name==='dangerouslySetInnerHTML') { 124 | html = v && v.__html; 125 | } 126 | else if ((v || v===0 || v==='') && typeof v!=='function') { 127 | if (v===true || v==='') { 128 | v = name; 129 | // in non-xml mode, allow boolean attributes 130 | if (!opts || !opts.xml) { 131 | s += ' ' + name; 132 | continue; 133 | } 134 | } 135 | s += ` ${name}="${encodeEntities(v)}"`; 136 | } 137 | } 138 | } 139 | 140 | // account for >1 multiline attribute 141 | let sub = s.replace(/^\n\s*/, ' '); 142 | if (sub!==s && !~sub.indexOf('\n')) s = sub; 143 | else if (pretty && ~s.indexOf('\n')) s += '\n'; 144 | 145 | s = `<${nodeName}${s}>`; 146 | if (String(nodeName).match(/[\s\n\\/='"\0<>]/)) throw s; 147 | 148 | let isVoid = String(nodeName).match(VOID_ELEMENTS); 149 | if (isVoid) s = s.replace(/>$/, ' />'); 150 | 151 | let pieces = []; 152 | if (html) { 153 | // if multiline, indent. 154 | if (pretty && isLargeString(html)) { 155 | html = '\n' + indentChar + indent(html, indentChar); 156 | } 157 | s += html; 158 | } 159 | else if (vnode.props.children) { 160 | let hasLarge = ~s.indexOf('\n'); 161 | let children = vnode.props.children; 162 | if (!Array.isArray(children)) { 163 | children = [children] 164 | } 165 | for (let i=0; i< children.length; i++) { 166 | let child = children[i]; 167 | if (child!=null && child!==false) { 168 | let childSvgMode = nodeName==='svg' ? true : nodeName==='foreignObject' ? false : isSvgMode, 169 | ret = renderToString(child, context, opts, true, childSvgMode); 170 | if (!hasLarge && pretty && isLargeString(ret)) hasLarge = true; 171 | if (ret) pieces.push(ret); 172 | } 173 | } 174 | 175 | if (pretty && hasLarge) { 176 | for (let i=pieces.length; i--; ) { 177 | pieces[i] = '\n' + indentChar + indent(pieces[i], indentChar); 178 | } 179 | } 180 | } 181 | 182 | if (pieces.length) { 183 | s += pieces.join(''); 184 | } 185 | else if (opts && opts.xml) { 186 | return s.substring(0, s.length-1) + ' />'; 187 | } 188 | 189 | if (!isVoid) { 190 | if (pretty && ~s.indexOf('\n')) s += '\n'; 191 | s += ``; 192 | } 193 | 194 | return s; 195 | } 196 | 197 | function getComponentName(component) { 198 | return component.displayName || component!==Function && component.name || getFallbackComponentName(component); 199 | } 200 | 201 | function getFallbackComponentName(component) { 202 | let str = Function.prototype.toString.call(component), 203 | name = (str.match(/^\s*function\s+([^( ]+)/) || '')[1]; 204 | if (!name) { 205 | // search for an existing indexed name for the given component: 206 | let index = -1; 207 | for (let i=UNNAMED.length; i--; ) { 208 | if (UNNAMED[i]===component) { 209 | index = i; 210 | break; 211 | } 212 | } 213 | // not found, create a new indexed name: 214 | if (index<0) { 215 | index = UNNAMED.push(component) - 1; 216 | } 217 | name = `UnnamedComponent${index}`; 218 | } 219 | return name; 220 | } 221 | renderToString.shallowRender = shallowRender; 222 | 223 | // export default renderToString; 224 | 225 | export { 226 | renderToString as render, 227 | renderToString, 228 | shallowRender 229 | }; 230 | -------------------------------------------------------------------------------- /server-render/jsx.js: -------------------------------------------------------------------------------- 1 | import './polyfills'; 2 | import renderToString from './index'; 3 | import { indent, encodeEntities, assign } from './util'; 4 | import prettyFormat from 'pretty-format'; 5 | 6 | 7 | // we have to patch in Array support, Possible issue in npm.im/pretty-format 8 | let preactPlugin = { 9 | test(object) { 10 | return object && typeof object==='object' && 'nodeName' in object && 'attributes' in object && 'children' in object && !('nodeType' in object); 11 | }, 12 | print(val, print, indent) { 13 | return renderToString(val, preactPlugin.context, preactPlugin.opts, true); 14 | } 15 | }; 16 | 17 | 18 | let prettyFormatOpts = { 19 | plugins: [preactPlugin] 20 | }; 21 | 22 | 23 | function attributeHook(name, value, context, opts, isComponent) { 24 | let type = typeof value; 25 | 26 | // Use render-to-string's built-in handling for these properties 27 | if (name==='dangerouslySetInnerHTML') return false; 28 | 29 | // always skip null & undefined values, skip false DOM attributes, skip functions if told to 30 | if (value==null || (type==='function' && !opts.functions)) return ''; 31 | 32 | if (opts.skipFalseAttributes && !isComponent && (value===false || ((name==='class' || name==='style') && value===''))) return ''; 33 | 34 | let indentChar = typeof opts.pretty==='string' ? opts.pretty : '\t'; 35 | if (type!=='string') { 36 | if (type==='function' && !opts.functionNames) { 37 | value = 'Function'; 38 | } 39 | else { 40 | preactPlugin.context = context; 41 | preactPlugin.opts = opts; 42 | value = prettyFormat(value, prettyFormatOpts); 43 | if (~value.indexOf('\n')) { 44 | value = `${indent('\n'+value, indentChar)}\n`; 45 | } 46 | } 47 | return indent(`\n${name}={${value}}`, indentChar); 48 | } 49 | return `\n${indentChar}${name}="${encodeEntities(value)}"`; 50 | } 51 | 52 | 53 | let defaultOpts = { 54 | attributeHook, 55 | jsx: true, 56 | xml: false, 57 | functions: true, 58 | functionNames: true, 59 | skipFalseAttributes: true, 60 | pretty: ' ' 61 | }; 62 | 63 | 64 | export default function renderToJsxString(vnode, context, opts, inner) { 65 | opts = assign(assign({}, defaultOpts), opts || {}); 66 | return renderToString(vnode, context, opts, inner); 67 | } 68 | -------------------------------------------------------------------------------- /server-render/polyfills.js: -------------------------------------------------------------------------------- 1 | if (typeof Symbol!=='function') { 2 | let c = 0; 3 | Symbol = function(s) { // eslint-disable-line 4 | return `@@${s}${++c}`; 5 | }; 6 | Symbol.for = s => `@@${s}`; 7 | } 8 | -------------------------------------------------------------------------------- /server-render/util.js: -------------------------------------------------------------------------------- 1 | // DOM properties that should NOT have "px" added when numeric 2 | export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i; 3 | 4 | export const objectKeys = Object.keys || (obj => { 5 | let keys = []; 6 | for (let i in obj) if (obj.hasOwnProperty(i)) keys.push(i); 7 | return keys; 8 | }); 9 | 10 | export let encodeEntities = s => String(s) 11 | .replace(/&/g, '&') 12 | .replace(//g, '>') 14 | .replace(/"/g, '"'); 15 | 16 | export let indent = (s, char) => String(s).replace(/(\n+)/g, '$1' + (char || '\t')); 17 | 18 | export let isLargeString = (s, length, ignoreLines) => (String(s).length>(length || 40) || (!ignoreLines && String(s).indexOf('\n')!==-1) || String(s).indexOf('<')!==-1); 19 | 20 | const JS_TO_CSS = {}; 21 | 22 | // Convert an Object style to a CSSText string 23 | export function styleObjToCss(s) { 24 | let str = ''; 25 | for (let prop in s) { 26 | let val = s[prop]; 27 | if (val!=null) { 28 | if (str) str += ' '; 29 | // str += jsToCss(prop); 30 | str += JS_TO_CSS[prop] || (JS_TO_CSS[prop] = prop.replace(/([A-Z])/g,'-$1').toLowerCase()); 31 | str += ': '; 32 | str += val; 33 | if (typeof val==='number' && IS_NON_DIMENSIONAL.test(prop)===false) { 34 | str += 'px'; 35 | } 36 | str += ';'; 37 | } 38 | } 39 | return str || undefined; 40 | } 41 | 42 | /** 43 | * Copy all properties from `props` onto `obj`. 44 | * @param {object} obj Object onto which properties should be copied. 45 | * @param {object} props Object from which to copy properties. 46 | * @returns {object} 47 | * @private 48 | */ 49 | export function assign(obj, props) { 50 | for (let i in props) obj[i] = props[i]; 51 | return obj; 52 | } 53 | 54 | /** 55 | * Reconstruct Component-style `props` from a VNode. 56 | * Ensures default/fallback values from `defaultProps`: 57 | * Own-properties of `defaultProps` not present in `vnode.attributes` are added. 58 | * @param {import('preact').VNode} vnode The VNode to get props for 59 | * @returns {object} The props to use for this VNode 60 | */ 61 | export function getNodeProps(vnode) { 62 | let props = vnode.props; 63 | // props.children = vnode.children; 64 | 65 | let defaultProps = vnode.type.defaultProps; 66 | if (defaultProps!==undefined) { 67 | for (let i in defaultProps) { 68 | if (props[i]===undefined) { 69 | props[i] = defaultProps[i]; 70 | } 71 | } 72 | } 73 | 74 | return props; 75 | } 76 | -------------------------------------------------------------------------------- /src/children.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from "./vnode"; 2 | import { childType } from "./types"; 3 | import { isArray } from "./util"; 4 | 5 | declare type Child = childType[] | childType; 6 | declare type ChildCallback = (item?: childType, index?: number, arr?: childType[]) => VNode[]; 7 | 8 | const arrayMap = Array.prototype.map; 9 | const arrayForEach = Array.prototype.forEach; 10 | const arraySlice = Array.prototype.slice; 11 | 12 | const Children = { 13 | map(children: Child, callback: ChildCallback, ctx?: any) { 14 | if (children == null) { 15 | return null; 16 | } 17 | if (!isArray(children)) { 18 | children = [children as childType]; 19 | } 20 | if (ctx && ctx !== children) { 21 | callback = callback.bind(ctx); 22 | } 23 | return arrayMap.call(children, callback); 24 | }, 25 | forEach(children: Child, callback: ChildCallback, ctx?: any) { 26 | if (children == null) { 27 | return null; 28 | } 29 | if (!isArray(children)) { 30 | children = [children as childType]; 31 | } 32 | if (ctx && ctx !== children) { 33 | callback = callback.bind(ctx); 34 | } 35 | return arrayForEach.call(children, callback); 36 | }, 37 | count(children: Child): number { 38 | if (children == null) { 39 | return 0; 40 | } 41 | if (!isArray(children)) { 42 | return 1; 43 | } 44 | return (children as childType[]).length; 45 | }, 46 | only(children: Child): childType { 47 | if (children != null && !isArray(children)) { 48 | return children as childType; 49 | } 50 | throw new TypeError("Children.only() expects only one child."); 51 | }, 52 | toArray(children: Child) { 53 | if (children == null) { 54 | return []; 55 | } else if (!isArray(children)) { 56 | return [children as childType]; 57 | } 58 | return arraySlice.call(children); 59 | }, 60 | }; 61 | 62 | export default Children; 63 | -------------------------------------------------------------------------------- /src/clone-element.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from "./create-element"; 2 | import { VNode } from "./vnode"; 3 | import { extend } from "./util"; 4 | 5 | /** 6 | * 通过VNode对象新建一个自定义的props,children的VNode对象 7 | * @param vnode 旧vnode 8 | * @param props 新的props 9 | * @param children 新的子组件 10 | */ 11 | export function cloneElement(vnode: VNode, props: any, ...newChildren: any[]) { 12 | const { children, ...oldProps } = vnode.props; 13 | const child: any = newChildren.length > 0 ? newChildren : children; 14 | return createElement( 15 | vnode.type, 16 | {...oldProps, ...props}, 17 | child, 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/component.ts: -------------------------------------------------------------------------------- 1 | import { FORCE_RENDER } from "./constants"; 2 | import { renderComponent, enqueueRender } from "./vdom/component"; 3 | import { VNode } from "./vnode"; 4 | // import { enqueueRender } from "./render-queue"; 5 | import { extend } from "./util"; 6 | import { IKeyValue, IRefObject, ComponentContext, IBaseProps, childType } from "./types"; 7 | import { IVDom } from "./vdom/index"; 8 | // import { h } from "./h"; 9 | // import options from "./options"; 10 | 11 | enum ComponentChildType { 12 | COMPONENT = 0, 13 | DOM = 1, 14 | } 15 | 16 | interface IComponentChild { 17 | type: ComponentChildType; 18 | index: number; 19 | } 20 | 21 | /** 22 | * 自定义组件所需继承类 23 | */ 24 | export class Component { 25 | /** 26 | * 默认props 27 | */ 28 | public static defaultProps?: IKeyValue; 29 | 30 | /** 31 | * componentWillReceiveProps react16.3后的替代品 32 | * @param nextProps 将要接受的props 33 | * @param previousState 当前state 34 | * @returns 用于更改当前state为空不更改. 35 | */ 36 | public static getDerivedStateFromProps?(nextProps: IKeyValue, previousState: IKeyValue): IKeyValue | null; 37 | 38 | /** 39 | * 当前组件的状态,可以修改 40 | */ 41 | public state: StateType; 42 | 43 | public isFunctionComponent?: boolean; 44 | 45 | /** 46 | * react render 16 多node支持 47 | */ 48 | public _children?: IComponentChild[]; 49 | 50 | /** 51 | * 被移除时的vdom缓存 52 | */ 53 | public _nextVDom?: IVDom; 54 | 55 | /** 56 | * 上一次的属性 57 | */ 58 | public _prevProps?: PropsType; 59 | 60 | /** 61 | * 上一次的状态 62 | */ 63 | public _prevState?: StateType; 64 | 65 | /** 66 | * 上一次的上下文 67 | */ 68 | public _prevContext?: IKeyValue; 69 | 70 | /** 71 | * 绑定了this的 72 | */ 73 | // public _h?: typeof h; 74 | 75 | /** 76 | * 子组件 77 | */ 78 | public _component?: Component; 79 | 80 | /** 81 | * 父组件 82 | */ 83 | public _parentComponent?: Component; 84 | 85 | /** 86 | * 是否加入更新队列 87 | */ 88 | public _dirty: boolean; 89 | 90 | /** 91 | * render 执行完后的回调队列 92 | */ 93 | public _renderCallbacks?: Array<() => void>; 94 | 95 | /** 96 | * 当前组件的key用于复用 97 | */ 98 | public _key?: string; 99 | 100 | /** 101 | * 是否停用 102 | */ 103 | public _disable?: boolean; 104 | 105 | /** 106 | * 模拟vue.emit用的上下文保存 107 | */ 108 | public _emitComponent?: Component; 109 | 110 | /** 111 | * react标准用于设置component实例 112 | */ 113 | public _ref?: ((component: ComponentContext | null) => void) | IRefObject; 114 | 115 | /** 116 | * 由父级组件传递的状态,不可修改 117 | */ 118 | public props: PropsType; 119 | 120 | /** 121 | * 组件上下文,由父组件传递 122 | */ 123 | public context: IKeyValue; 124 | 125 | /** 126 | * 自定义组件名 127 | */ 128 | public name?: string; 129 | 130 | /** 131 | * 组件挂载后的vdom 132 | */ 133 | public _vdom?: IVDom; 134 | 135 | /** 136 | * 137 | */ 138 | public base?: Node | Element | Text | null; 139 | 140 | constructor(props: PropsType, context: IKeyValue) { 141 | // 初始化为true 142 | this._dirty = true; 143 | this.context = context; 144 | this.props = props; 145 | this.state = {} as StateType; 146 | // const self: any = this; 147 | // this.state = self.state || {} as StateType; 148 | // if (options.eventBind) { 149 | // const self = this; 150 | // this.h = function _(){ 151 | // return h.apply(self, Array.prototype.slice.call(arguments, 0)); 152 | // } as typeof h; 153 | // } 154 | } 155 | /** 156 | * 在一个组件被渲染到 DOM 之前 157 | * 在react16.3弃用: https://github.com/reactjs/rfcs/pull/6 158 | * 原因是因为如果在dom渲染前进行异步的setState可能会造成,第一次的渲染效果不同 159 | * @deprecated 160 | */ 161 | public componentWillMount?(): void; 162 | 163 | /** 164 | * 在一个组件被渲染到 DOM 之后 165 | */ 166 | public componentDidMount?(): void; 167 | 168 | /** 169 | * 在一个组件在 DOM 中被清除之前 170 | */ 171 | public componentWillUnmount?(): void; 172 | 173 | /** 174 | * 在新的 props 被接受之前 175 | * Use static getDerivedStateFromProps() instead 176 | * @param { PropsType } nextProps 177 | * @param { IKeyValue } nextContext 178 | * @deprecated 179 | */ 180 | public componentWillReceiveProps?(nextProps: PropsType, nextContext: IKeyValue): void; 181 | 182 | /** 183 | * 在 render() 之前. 若返回 false,则跳过 render,与 componentWillUpdate 互斥 184 | * @param { PropsType } nextProps 185 | * @param { StateType } nextState 186 | * @param { IKeyValue } nextContext 187 | * @returns { boolean } 188 | */ 189 | public shouldComponentUpdate?(nextProps: PropsType, nextState: StateType, nextContext: IKeyValue): boolean; 190 | 191 | /** 192 | * 在 render() 之前,与 shouldComponentUpdate 互斥 193 | * @param { PropsType } nextProps 194 | * @param { StateType } nextState 195 | * @param { IKeyValue } nextContext 196 | * @deprecated 197 | */ 198 | public componentWillUpdate?(nextProps: PropsType, nextState: StateType, nextContext: IKeyValue): void; 199 | 200 | /** 201 | * 在 render() 之后 202 | * @param { PropsType } previousProps 203 | * @param { StateType } previousState 204 | * @param { IKeyValue } previousContext 205 | */ 206 | public componentDidUpdate?(previousProps: PropsType, previousState: StateType, snapshot: any, previousContext: IKeyValue): void; 207 | 208 | /** 209 | * 获取上下文,会被传递到所有的子组件 210 | */ 211 | public getChildContext?(): IKeyValue; 212 | 213 | /** 214 | * 215 | * @param previousProps 216 | * @param previousState 217 | * @param previousContext 218 | */ 219 | public getSnapshotBeforeUpdate?(previousProps: PropsType, previousState: StateType, previousContext: IKeyValue): any; 220 | 221 | /** 222 | * 设置state并通过enqueueRender异步更新dom 223 | * @param state 对象或方法 224 | * @param callback render执行完后的回调。 225 | */ 226 | public setState(state: ((s: StateType, p: PropsType) => StateType) | StateType, callback?: () => void): void { 227 | const s: StateType = this.state; 228 | if (!this._prevState) { 229 | // 把旧的状态保存起来 230 | this._prevState = extend({}, s); 231 | } 232 | // 把新的state和并到this.state 233 | if (typeof state === "function") { 234 | const newState = state(s, this.props); 235 | if (newState) { 236 | extend(s, newState); 237 | } 238 | } else { 239 | extend(s, state); 240 | } 241 | if (callback) { 242 | // 添加回调 243 | this._renderCallbacks = this._renderCallbacks || []; 244 | this._renderCallbacks.push(callback); 245 | } 246 | // 异步队列更新dom,通过enqueueRender方法可以保证在一个任务栈下多次setState但是只会发生一次render 247 | enqueueRender(this); 248 | } 249 | 250 | /** 251 | * 手动的同步更新dom 252 | * @param callback 回调 253 | */ 254 | public forceUpdate(callback?: () => void) { 255 | if (callback) { 256 | this._renderCallbacks = this._renderCallbacks || []; 257 | this._renderCallbacks.push(callback); 258 | } 259 | // 重新执行render 260 | renderComponent(this, FORCE_RENDER); 261 | } 262 | 263 | /** 264 | * 用来生成VNode的函数,一定要继承后覆盖 265 | * @param props 266 | * @param state 267 | * @param context 268 | */ 269 | public render(props: PropsType, state: StateType, context: IKeyValue): childType { 270 | } 271 | } 272 | 273 | // import { h } from "../h"; 274 | 275 | /** 276 | * 缓存卸载自定义组件对象列表 277 | */ 278 | const components: { 279 | [name: string]: Array>; 280 | } = {}; 281 | 282 | /** 283 | * 缓存卸载后的自定义组件 284 | * @param component 卸载后的组件 285 | */ 286 | export function collectComponent(component: Component) { 287 | const constructor: any = component.constructor; 288 | component._emitComponent = undefined; 289 | // 获取组件名 290 | const name = constructor.name; 291 | // 获取该组件名所属的列表 292 | let list = components[name]; 293 | if (!list) { 294 | list = components[name] = []; 295 | } 296 | // 设置 297 | list.push(component); 298 | } 299 | 300 | /** 301 | * 复用已卸载的组件 302 | * @param Ctor 要创建的组件对象 303 | * @param props 304 | * @param context 305 | */ 306 | export function createComponent( 307 | Ctor: any, 308 | props: IKeyValue, 309 | context: IKeyValue, 310 | component: Component | undefined | void | null, 311 | ): Component { 312 | const list = components[Ctor.name]; 313 | let inst: Component; 314 | // 创建组件实例 315 | if (Ctor.prototype && Ctor.prototype.render) { 316 | // if (newContext) { 317 | inst = new Ctor(props, context); 318 | // Component.call(inst, props, context); 319 | } else { 320 | // 一个方法 321 | inst = new Component(props, context); 322 | // 设置到constructor上 323 | inst.constructor = Ctor; 324 | // render用doRender代替 325 | inst.render = doRender; 326 | inst.isFunctionComponent = true; 327 | } 328 | // 查找之前的卸载缓存 329 | if (list) { 330 | for (let i = list.length; i-- ; ) { 331 | const item = list[i]; 332 | if (item.constructor === Ctor) { 333 | inst._nextVDom = item._nextVDom; 334 | list.splice(i, 1); 335 | break; 336 | } 337 | } 338 | } 339 | return inst; 340 | } 341 | 342 | /** 343 | * 代理render,去除state 344 | * @param props 345 | * @param state 346 | * @param context 347 | */ 348 | function doRender(this: typeof Component, props: IBaseProps, state: IKeyValue, context: IKeyValue) { 349 | return this.constructor(props, context); 350 | } 351 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | // 不进行render 2 | export const NO_RENDER = 0; 3 | // 同步render标记 4 | export const SYNC_RENDER = 1; 5 | // 用于Component.forceUpdate方法更新组件时的标记 6 | export const FORCE_RENDER = 2; 7 | // 异步render标记 8 | export const ASYNC_RENDER = 3; 9 | 10 | // dom的props属性key 11 | // export const ATTR_KEY = "__preactattr_"; 12 | 13 | // 使用number值的style属性 14 | export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i; 15 | -------------------------------------------------------------------------------- /src/create-class.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./component"; 2 | import { extend } from "./util"; 3 | import { IKeyValue } from "./types"; 4 | 5 | /** 6 | * 类似React.createClass, 但未bind(this) 7 | * @param obj 8 | */ 9 | export function createClass(obj: any) { 10 | const cl: any = function(this: any, props: IKeyValue, context: IKeyValue) { 11 | Component.call(this, props, context); 12 | if (obj.getInitialState) { 13 | this.state = obj.getInitialState.call(this, props, context); 14 | } 15 | }; 16 | // 保证后面的实例的constructor指向cl 17 | obj = extend({ constructor: cl }, obj); 18 | if (obj.defaultProps) { 19 | // 获取defaultProps 20 | cl.defaultProps = obj.defaultProps; 21 | } 22 | // prototype链 23 | F.prototype = Component.prototype; 24 | cl.prototype = extend(new F(), obj); 25 | // 组件名 26 | cl.displayName = obj.displayName || "Component"; 27 | return cl; 28 | } 29 | 30 | class F { 31 | } 32 | -------------------------------------------------------------------------------- /src/create-context.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./component"; 2 | 3 | const key = "__global_context_unique_id__"; 4 | let g: any = {}; 5 | if (typeof global !== "undefined") { 6 | g = global; 7 | } else if (typeof window !== "undefined") { 8 | g = window; 9 | } 10 | 11 | function gud() { 12 | return g[key] = (g[key] || 0) + 1; 13 | } 14 | 15 | function createEventEmitter(value: any) { 16 | let handlers: any[] = []; 17 | return { 18 | on(handler: any) { 19 | handlers.push(handler); 20 | }, 21 | off(handler: any) { 22 | handlers = handlers.filter((h: any) => h !== handler); 23 | }, 24 | get() { 25 | return value; 26 | }, 27 | set(newValue: any, changedBits: any) { 28 | value = newValue; 29 | handlers.forEach((handler: any) => handler(value, changedBits)); 30 | }, 31 | }; 32 | } 33 | function objectIs(x: any, y: any) { 34 | if (x === y) { 35 | return x !== 0 || 1 / x === 1 / y; 36 | } else { 37 | return x !== x && y !== y; 38 | } 39 | } 40 | 41 | export function createContext(defaultValue: any, calculateChangedBits?: (a: any, b: any) => number, name?: string) { 42 | // const context = {}; 43 | const contextProp = "__create-react-context-" + gud() + "__"; 44 | class Provider extends Component { 45 | public static displayName: string = name ? name + ".Provider" : "Context.Provider"; 46 | public static getDerivedStateFromProps(nextProps: any, previousState: any): null { 47 | const self: Provider = previousState.self; 48 | const oldValue = self.props.value; 49 | const newValue = nextProps.value; 50 | let changedBits: number; 51 | if (objectIs(oldValue, newValue)) { 52 | changedBits = 0; 53 | } else { 54 | changedBits = typeof calculateChangedBits === "function" 55 | ? calculateChangedBits(oldValue, newValue) : 1073741823; 56 | changedBits |= 0; 57 | if (changedBits) { 58 | self.emitter.set(nextProps.value, changedBits); 59 | } 60 | } 61 | return null; 62 | } 63 | 64 | public emitter = createEventEmitter(this.props.value); 65 | 66 | constructor(p: any, c: any) { 67 | super(p, c); 68 | this.state = {self: this}; 69 | } 70 | 71 | public getChildContext() { 72 | const provider = this.emitter; 73 | return { [contextProp]: provider }; 74 | } 75 | 76 | public render() { 77 | return this.props.children; 78 | } 79 | } 80 | class Consumer extends Component { 81 | public static displayName: string = name ? name + ".Consumer" : "Context.Consumer"; 82 | public emitter: any; 83 | public observedBits: number = 0; 84 | constructor(p: any, c: any) { 85 | super(p, c); 86 | this.updateContext = this.updateContext.bind(this); 87 | if (c && c[contextProp] != null) { 88 | this.emitter = c[contextProp]; 89 | } 90 | this.state = { 91 | value: this.getValue(), 92 | }; 93 | } 94 | public componentDidMount() { 95 | const c = this.context; 96 | if (c && c[contextProp] != null) { 97 | this.emitter = c[contextProp]; 98 | } 99 | if (this.emitter) { 100 | this.emitter.on(this.updateContext); 101 | } 102 | const { observedBits } = this.props; 103 | this.observedBits = 104 | observedBits === undefined 105 | || observedBits === null 106 | ? 1073741823 // Subscribe to all changes by default 107 | : observedBits; 108 | } 109 | public updateContext(val: any, changedBits: number) { 110 | const observedBits: number = this.observedBits | 0; 111 | if ((observedBits & changedBits) !== 0) { 112 | this.setState({ value: val }); 113 | } 114 | } 115 | public componentWillUnmount() { 116 | if (this.emitter) { 117 | this.emitter.off(this.updateContext); 118 | } 119 | } 120 | public getValue() { 121 | if (this.emitter) { 122 | return this.emitter.get(); 123 | } else { 124 | return defaultValue; 125 | } 126 | } 127 | public render() { 128 | return this.props.children(this.state.value); 129 | } 130 | } 131 | return {Provider, Consumer, default: defaultValue}; 132 | } 133 | -------------------------------------------------------------------------------- /src/create-element.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./component"; 2 | import options from "./options"; 3 | import { VNode } from "./vnode"; 4 | import { 5 | IKeyValue, 6 | IBaseProps, 7 | childType, 8 | NodeName, 9 | } from "./types"; 10 | import { 11 | REACT_FRAGMENT_TYPE, 12 | } from "./util"; 13 | import { ForwardRef } from "./forward-ref"; 14 | 15 | function Fragment(props: IBaseProps | undefined): childType { 16 | return (props && props.children) as childType; 17 | } 18 | 19 | /** JSX/hyperscript reviver 20 | * Benchmarks: https://esbench.com/bench/57ee8f8e330ab09900a1a1a0 21 | * 标准JSX转换函数 22 | * @param {string|Component} nodeName 组件{@link Component}或者原生dom组件名 23 | * @param {{key: string => value: string}} attributes 组件属性 24 | * @see http://jasonformat.com/wtf-is-jsx 25 | * @public 26 | */ 27 | export function createElement(this: Component | undefined | void | null, nodeName: NodeName, attributes: IKeyValue | null, ...args: Array) { 28 | // 初始化子元素列表 29 | const stack: Array = []; 30 | let children: childType[] | childType | null = []; 31 | // let i: number; 32 | // let child: any; 33 | // 是否为原生组件 34 | let simple: boolean; 35 | // 上一个子元素是否为原生组件 36 | let lastSimple: boolean = false; 37 | // 把剩余的函数参数全部倒序放入stack 38 | for (let i = args.length; i--; ) { 39 | stack.push(args[i]); 40 | } 41 | // 把元素上属性的children放入栈 42 | if (attributes && attributes.children != null) { 43 | if (!stack.length) { 44 | stack.push(attributes.children); 45 | } 46 | // 删除 47 | delete attributes.children; 48 | } 49 | // 把stack一次一次取出 50 | while (stack.length) { 51 | // let num = 0; 52 | // 取出最后一个 53 | let child: any = stack.pop(); 54 | if (child && child.pop != null) { 55 | // 如果是个数组就倒序放入stack 56 | for (let i = child.length; i-- ; ) { 57 | const item = child[i]; 58 | // 修复多个map时不同map的key相同 59 | // if (typeof item === "object" && item.key) { 60 | // item.key = `L${num}-${item.key}`; 61 | // num ++; 62 | // } 63 | stack.push(item); 64 | } 65 | } else { 66 | // 清空布尔 67 | if (typeof child === "boolean" || child == null || child === "") { 68 | continue; 69 | // child = null; 70 | } 71 | // 判断当前组件是否为自定义组件 72 | simple = typeof nodeName !== "function"; 73 | if (simple) { 74 | // 原生组件的子元素处理 75 | if (typeof child === "number") { 76 | // num to string 77 | child = String(child); 78 | } else if (typeof child !== "string") { 79 | // 不是 null,number,string 的不做处理 80 | // 并且设置标记不是一个字符串 81 | simple = false; 82 | } 83 | } 84 | if (simple && lastSimple) { 85 | // 当前为原生组件且子元素为字符串,并且上一个也是。 86 | // 就把当前子元素加到上一次的后面。 87 | children[children.length - 1] += child; 88 | } else { 89 | // 其它情况直接加入children 90 | children.push(child); 91 | } 92 | /* else if (children === EMPTY_CHILDREN) { 93 | children = [child]; 94 | } */ 95 | // 记录这次的子元素状态 96 | lastSimple = simple; 97 | } 98 | } 99 | const childrenLen = children.length; 100 | if (childrenLen === 0) { 101 | children = null; 102 | } else if (childrenLen === 1) { 103 | children = children[0]; 104 | } 105 | if (nodeName == null) { 106 | } else if (nodeName === REACT_FRAGMENT_TYPE) { 107 | nodeName = Fragment; 108 | } 109 | let props = attributes == null ? {} : attributes; 110 | if ( 111 | (nodeName as typeof ForwardRef).$isForwardRefComponent != null 112 | && (nodeName as typeof ForwardRef).$isForwardRefComponent 113 | ) { 114 | const { ref = null, ...deepProps} = attributes || {}; 115 | deepProps.$$forwardedRef = ref; 116 | props = deepProps; 117 | } 118 | // vnode 钩子 119 | if (children) { 120 | props.children = children; 121 | } else { 122 | props.children = null; 123 | } 124 | const p = new VNode( 125 | // 设置原生组件名字或自定义组件class(function) 126 | nodeName, 127 | props, 128 | ); 129 | if (options.vnode != null) { 130 | options.vnode(p); 131 | } 132 | return p; 133 | } 134 | -------------------------------------------------------------------------------- /src/create-ref.ts: -------------------------------------------------------------------------------- 1 | import { IRefObject } from "./types"; 2 | 3 | // an immutable object with a single mutable value 4 | export function createRef(): IRefObject { 5 | const refObject: IRefObject = { 6 | current: null, 7 | }; 8 | return refObject; 9 | } 10 | -------------------------------------------------------------------------------- /src/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module "prop-types" { 2 | const PropTypes: any; 3 | export default PropTypes; 4 | } 5 | -------------------------------------------------------------------------------- /src/devtools-run.ts: -------------------------------------------------------------------------------- 1 | import { options, findDOMNode, findVDom } from "zreact"; 2 | import { getInitDevTools } from "./devtools-base"; 3 | 4 | const initDevTools = getInitDevTools(options, findDOMNode, findVDom); 5 | 6 | initDevTools(); 7 | -------------------------------------------------------------------------------- /src/devtools.ts: -------------------------------------------------------------------------------- 1 | import { getInitDevTools } from "./devtools-base"; 2 | import { options, findDOMNode, findVDom } from "zreact"; 3 | 4 | export const initDevTools = getInitDevTools(options, findDOMNode, findVDom); 5 | -------------------------------------------------------------------------------- /src/dom/index.ts: -------------------------------------------------------------------------------- 1 | import { IS_NON_DIMENSIONAL } from "../constants"; 2 | import { IVDom } from "../vdom/index"; 3 | import options from "../options"; 4 | import { IKeyValue, ComponentContext, IRefObject } from "../types"; 5 | import { Component } from "../component"; 6 | import { innerHTML } from "../util"; 7 | 8 | enum NAME_NS { 9 | SVG = "http://www.w3.org/2000/svg", 10 | XLINK = "http://www.w3.org/1999/xlink", 11 | } 12 | 13 | /** 14 | * 创建一个原生html组件 15 | * @param nodeName 标签名 16 | * @param isSvg 是否为svg 17 | */ 18 | export function createNode(nodeName: string, isSvg: boolean): HTMLElement { 19 | const node: any = isSvg 20 | ? document.createElementNS(NAME_NS.SVG, nodeName) 21 | : document.createElement(nodeName); 22 | // 设置原始的nodeName到dom上normalizedNodeName 23 | // node.normalizedNodeName = nodeName; 24 | return node; 25 | } 26 | 27 | /** 28 | * 移除dom 29 | * @param node 需要移除的node 30 | */ 31 | export function removeNode(node: Element | Text | Node) { 32 | const parentNode = node.parentNode; 33 | if (parentNode) { 34 | parentNode.removeChild(node); 35 | } 36 | } 37 | 38 | /** 39 | * 通过VNode的props设置真实的dom 40 | * @param node dom节点 41 | * @param name 属性名 42 | * @param old 旧属性值 43 | * @param value 新属性值 44 | * @param isSvg 是否为svg 45 | * @param child VDom原dom上的props,和上下文环境,事件就在其中 46 | */ 47 | export function setAccessor( 48 | vdom: IVDom, 49 | name: string, 50 | old: any, 51 | value: any, 52 | isSvg: boolean, 53 | component?: Component | void | null, 54 | ) { 55 | const node = vdom.base as Element; 56 | if (name === "className") { 57 | // 把className重名为class 58 | name = "class"; 59 | } 60 | if (name === "key") { 61 | // 不对key属性做dom设置 62 | } else if ("ref" === name) { 63 | if (old) { 64 | // 对旧的ref设置null保证原方法里的引用移除 65 | setRef(old, null); 66 | // old(null); 67 | } 68 | if (value) { 69 | // 给新方法设置vdom 70 | let context: ComponentContext | null = null; 71 | if (options.ref) { 72 | if (typeof options.ref === "function") { 73 | context = options.ref(vdom); 74 | } else { 75 | context = vdom.base; 76 | } 77 | } else { 78 | context = vdom; 79 | } 80 | setRef(value, context); 81 | } 82 | } else if ("class" === name && !isSvg) { 83 | // 直接通过className设置class 84 | (node as Element).className = value || ""; 85 | } else if ("style" === name) { 86 | if (!value || typeof value === "string" || typeof old === "string") { 87 | // 对于字符串型的直接设置到style.cssText 88 | (node as HTMLElement).style.cssText = value || ""; 89 | } 90 | if (value && typeof value === "object") { 91 | // 如果是一个对象遍历设置 92 | if (typeof old !== "string") { 93 | for (const i in old) { 94 | if (!(i in value)) { 95 | // 清理旧属性且不在新的里 96 | ((node as HTMLElement).style as any)[i] = ""; 97 | } 98 | } 99 | } 100 | for (const i in value) { 101 | // 设置新属性 102 | ((node as HTMLElement).style as any)[i] = typeof value[i] === "number" 103 | && IS_NON_DIMENSIONAL.test(i) === false ? (value[i] + "px") : value[i]; 104 | } 105 | } 106 | } else if (innerHTML === name) { 107 | if (value) { 108 | // innerHTML 109 | (node as Element).innerHTML = value.__html || ""; 110 | // child.children = []; 111 | // const childNodes = node.childNodes; 112 | // for (let i = 0, len = childNodes.length; i < len ; i++) { 113 | // child.children.push({ 114 | // base: childNodes[i], 115 | // }); 116 | // } 117 | 118 | } 119 | } else if (name[0] === "o" && name[1] === "n") { 120 | // 事件绑定 121 | const oldName = name; 122 | name = name.replace(/Capture$/, ""); 123 | // 判断是否 事件代理(事件委托) 124 | const useCapture = oldName !== name; 125 | // 去除前面的on并转换为小写 126 | name = name.toLowerCase().substring(2); 127 | if (value) { 128 | if (!old) { 129 | // 保证只有一次绑定事件 130 | addEventListener(vdom, name, useCapture, component); 131 | } 132 | } else { 133 | // 移除事件 134 | removeEventListener(vdom, name, useCapture); 135 | } 136 | if (!vdom.listeners) { 137 | // 在上下文中创建存放绑定的方法的对象 138 | vdom.listeners = {}; 139 | } 140 | vdom.listeners[name] = value; 141 | } else if (name !== "list" && name !== "type" && !isSvg && name in node) { 142 | // 安全设置属性 143 | setProperty(node, name, value == null ? "" : value); 144 | if (value == null || value === false) { 145 | (node as Element).removeAttribute(name); 146 | } 147 | } else { 148 | // 设置Attribute 149 | const ns = isSvg && (name !== (name = name.replace(/^xlink\:?/, ""))); 150 | // null || undefined || void 0 || false 151 | if (value == null || value === false) { 152 | if (ns) { 153 | (node as Element).removeAttributeNS( 154 | NAME_NS.XLINK, 155 | name.toLowerCase(), 156 | ); 157 | } else { 158 | (node as Element).removeAttribute(name); 159 | } 160 | } else if (typeof value !== "function") { 161 | if (ns) { 162 | (node as Element).setAttributeNS( 163 | NAME_NS.XLINK, 164 | name.toLowerCase(), 165 | value, 166 | ); 167 | } else { 168 | (node as Element).setAttribute(name, value); 169 | } 170 | } 171 | } 172 | } 173 | 174 | const isIe8 = typeof document !== "undefined" && typeof document.addEventListener !== "function"; 175 | 176 | function setProperty(node: any, name: string, value: string) { 177 | try { 178 | node[name] = value; 179 | } catch (e) { } 180 | } 181 | 182 | export function getPreviousSibling(node: Node): Node| null { 183 | return node.previousSibling; 184 | } 185 | 186 | export function getLastChild(node: Node): Node| null { 187 | return node.lastChild; 188 | } 189 | 190 | /** 191 | * 生成用于绑定事件的方法,保证每次更新props上的事件方法不会重新绑定事件 192 | * @param child 上下文 193 | * @param useCapture 是否冒泡(兼容ie8) 194 | */ 195 | function eventProxy(vdom: IVDom, useCapture: boolean, component?: Component | void | null): (e: Event) => void { 196 | return (e: Event) => { 197 | if (isIe8 && !useCapture) { 198 | // ie8事件默认冒泡所以需要阻止 199 | e.cancelBubble = !useCapture; 200 | } 201 | // 取出对于的props事件 202 | const listener = vdom.listeners && vdom.listeners[e.type]; 203 | // 事件钩子 204 | const event = options.event && options.event(e) || e; 205 | const functionName = (listener as any).name; 206 | if (listener) { 207 | if (options.eventBind && component && listener.call && (component as any)[functionName] === listener) { 208 | // 使用vnode的所属组件实例来做this 209 | return listener.call(component, event); 210 | } 211 | // 直接调用事件 212 | return listener(event); 213 | } 214 | }; 215 | } 216 | /** 217 | * 绑定代理事件 218 | * @param node dom节点 219 | * @param name 事件名 220 | * @param useCapture 是否冒泡 221 | * @param child 上下文 222 | */ 223 | function addEventListener(vdom: IVDom, name: string, useCapture: boolean, component?: Component | void | null) { 224 | // 生成当前事件的代理方法 225 | const eventProxyFun = eventProxy(vdom, useCapture, component); 226 | if (!vdom.eventProxy) { 227 | vdom.eventProxy = {}; 228 | } 229 | // 把事件代理方法挂载到child.event上等待卸载时使用 230 | vdom.eventProxy[name] = eventProxyFun; 231 | const node: any = vdom.base; 232 | if (!isIe8) { 233 | node.addEventListener(name, eventProxyFun, useCapture); 234 | } else { 235 | node.attachEvent("on" + name, eventProxyFun); 236 | } 237 | } 238 | 239 | /** 240 | * 移除事件 241 | * @param node dom节点 242 | * @param name 事件名 243 | * @param useCapture 是否冒泡 244 | * @param child 上下文 245 | */ 246 | function removeEventListener(vdom: IVDom, name: string, useCapture: boolean) { 247 | // 把上下文中的存储的代理事件解绑 248 | const eventProxyFun = vdom.eventProxy && vdom.eventProxy[name]; 249 | if (vdom.eventProxy && eventProxyFun) { 250 | vdom.eventProxy[name] = undefined; 251 | } 252 | const node: any = vdom.base; 253 | if (!isIe8) { 254 | node.removeEventListener(name, eventProxyFun, useCapture); 255 | } else { 256 | node.detachEvent("on" + name, eventProxyFun); 257 | } 258 | } 259 | 260 | /** 261 | * 统一设置ref 262 | */ 263 | export function setRef( 264 | ref: ((c: ComponentContext | null) => void) | IRefObject | undefined, 265 | context: ComponentContext | null, 266 | ): void { 267 | if (ref != null) { 268 | if (typeof ref === "function") { 269 | ref(context); 270 | } else if (typeof ref === "object") { 271 | ref.current = context; 272 | } else { 273 | console.warn("not support ref by: ", ref); 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | export const global: any = (function _() { 2 | let local; 3 | if (typeof global !== "undefined") { 4 | local = global; 5 | } else if (typeof self !== "undefined") { 6 | local = self; 7 | } else { 8 | try { 9 | local = Function("return this")(); 10 | } catch (e) { 11 | throw new Error("global object is unavailable in this environment"); 12 | } 13 | } 14 | return local; 15 | })(); 16 | 17 | export const isBrowser = typeof window !== "undefined"; 18 | -------------------------------------------------------------------------------- /src/find.ts: -------------------------------------------------------------------------------- 1 | import { IVDom } from "./vdom/index"; 2 | import { VNode } from "./vnode"; 3 | 4 | /** 5 | * 获取组件|vdom的dom对象 6 | * @param componentOrVdom 7 | */ 8 | export function findDOMNode(componentOrVdom: any): Element { 9 | return componentOrVdom && componentOrVdom.base; 10 | } 11 | 12 | /** 13 | * 获取组件或|dom的vdom对象 14 | */ 15 | export function findVDom(componentOrDom: any | Node | Element): IVDom | undefined { 16 | if (componentOrDom) { 17 | return (componentOrDom as any)._vdom; 18 | } 19 | } 20 | 21 | export function setVDom(componentOrDom: any | Node | Element, vdom: IVDom | undefined): void { 22 | if (componentOrDom) { 23 | componentOrDom._vdom = vdom; 24 | } 25 | } 26 | 27 | export function findVoidNode(dom: any) { 28 | return dom && dom._voidNode; 29 | } 30 | export function setVoidNode(dom: any, value: any) { 31 | if (dom) { 32 | dom._voidNode = value; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/forward-ref.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./component"; 2 | import { IBaseProps, IKeyValue, RefType } from "./types"; 3 | 4 | interface IForwardRefProps extends IBaseProps { 5 | "$$forwardedRef"?: RefType; 6 | } 7 | 8 | export class ForwardRef extends Component { 9 | public static displayName = "ForwardRef"; 10 | public static $isForwardRefComponent: boolean = true; 11 | public $$render?: (props: any, ref: any) => any; 12 | public render() { 13 | const { $$forwardedRef, ...props } = this.props; 14 | if (this.$$render) { 15 | return this.$$render(props, $$forwardedRef); 16 | } 17 | return null; 18 | } 19 | } 20 | 21 | export function forwardRef(render: (props: any, ref: any) => any) { 22 | class Deep extends ForwardRef { 23 | public static displayName = (render as any).displayName || (render as any).name || "ForwardRef"; 24 | public $$render = render; 25 | } 26 | return Deep; 27 | } 28 | -------------------------------------------------------------------------------- /src/ie8.ts: -------------------------------------------------------------------------------- 1 | import "./polyfill"; 2 | 3 | export * from "./zreact"; 4 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from "./vnode"; 2 | import { Component } from "./component"; 3 | import { IKeyValue, ComponentContext } from "./types"; 4 | import { IVDom } from "./vdom/index"; 5 | 6 | const options: { 7 | /** 8 | * render更新后钩子比componentDidUpdate更后面执行 9 | */ 10 | afterUpdate?: (component: Component) => void; 11 | /** 12 | * dom卸载载前钩子比componentWillUnmount更先执行 13 | */ 14 | beforeUnmount?: (component: Component) => void; 15 | /** 16 | * dom挂载后钩子比componentDidMount更先执行 17 | */ 18 | afterMount?: (component: Component) => void; 19 | /** 20 | * setComponentProps时强制为同步render 21 | */ 22 | syncComponentUpdates?: boolean; 23 | /** 24 | * 自定义异步调度方法,会异步执行传入的方法 25 | */ 26 | debounceRendering?: (render: (...args: any[]) => void) => void; 27 | /** 28 | * vnode实例创建时的钩子 29 | */ 30 | vnode?: (vnode: VNode) => void; 31 | /** 32 | * 事件钩子,可以对event过滤返回的会代替event参数 33 | */ 34 | event?: (event: Event) => any; 35 | /** 36 | * 是否自动对事件方法绑定this为组件,默认为true(preact没有) 37 | */ 38 | eventBind?: boolean; 39 | /** 40 | * ref 默认为vdom.base, 41 | */ 42 | ref?: ((vdom: IVDom) => ComponentContext) | boolean; 43 | } = { 44 | eventBind: true, 45 | }; 46 | 47 | export default options; 48 | -------------------------------------------------------------------------------- /src/polyfill.ts: -------------------------------------------------------------------------------- 1 | if (!String.prototype.trim) { 2 | // trim polyfill 3 | String.prototype.trim = function _() { 4 | return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ""); 5 | }; 6 | } 7 | if (!Function.prototype.bind) { 8 | // bind polyfill 9 | Function.prototype.bind = function __(this: any, oThis) { 10 | if (typeof this !== "function") { 11 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); 12 | } 13 | const aArgs = Array.prototype.slice.call(arguments, 1); 14 | const fToBind = this; 15 | const fNOP: any = function _() {}; 16 | const fBound = function _(this: any) { 17 | return fToBind.apply(this instanceof fNOP ? this : oThis || this, aArgs.concat(Array.prototype.slice.call(arguments))); 18 | }; 19 | 20 | fNOP.prototype = this.prototype; 21 | fBound.prototype = new fNOP(); 22 | 23 | return fBound; 24 | }; 25 | } 26 | if (!Array.prototype.map) { 27 | // map polyfill 28 | Array.prototype.map = function(callback, thisArg) { 29 | let T; 30 | let A; 31 | let k; 32 | if (this == null) { 33 | throw new TypeError(" this is null or not defined"); 34 | } 35 | // 1. 将O赋值为调用map方法的数组. 36 | const O = Object(this); 37 | // 2.将len赋值为数组O的长度. 38 | const len = O.length; 39 | // 3.如果callback不是函数,则抛出TypeError异常. 40 | if (Object.prototype.toString.call(callback) !== "[object Function]") { 41 | throw new TypeError(callback + " is not a function"); 42 | } 43 | // 4. 如果参数thisArg有值,则将T赋值为thisArg;否则T为undefined. 44 | if (thisArg) { 45 | T = thisArg; 46 | } 47 | // 5. 创建新数组A,长度为原数组O长度len 48 | A = new Array(len); 49 | // 6. 将k赋值为0 50 | k = 0; 51 | // 7. 当 k < len 时,执行循环. 52 | while (k < len) { 53 | let kValue; 54 | let mappedValue; 55 | // 遍历O,k为原数组索引 56 | if (k in O) { 57 | // kValue为索引k对应的值. 58 | kValue = O[ k ]; 59 | // 执行callback,this指向T,参数有三个.分别是kValue:值,k:索引,O:原数组. 60 | mappedValue = callback.call(T, kValue, k, O); 61 | // 返回值添加到新数组A中. 62 | A[ k ] = mappedValue; 63 | } 64 | // k自增1 65 | k++; 66 | } 67 | // 8. 返回新数组A 68 | return A; 69 | }; 70 | } 71 | if (typeof (Object as any).assign !== "function") { 72 | (Object as any) = function _(target: any) { 73 | if (target == null) { 74 | throw new TypeError("Cannot convert undefined or null to object"); 75 | } 76 | 77 | target = Object(target); 78 | for (let index = 1; index < arguments.length; index++) { 79 | const source = arguments[index]; 80 | if (source != null) { 81 | for (const key in source) { 82 | if (Object.prototype.hasOwnProperty.call(source, key)) { 83 | target[key] = source[key]; 84 | } 85 | } 86 | } 87 | } 88 | return target; 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /src/pure-component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./component"; 2 | import { IBaseProps, IKeyValue } from "./types"; 3 | 4 | /** 5 | * 简单组件state,props对象只有一层改变使用,超过一层改变就会无法更新 6 | * @constructor 7 | */ 8 | export class PureComponent extends Component { 9 | public isPureReactComponent: boolean = true; 10 | public shouldComponentUpdate(props: PropsType, state: StateType): boolean { 11 | // props,state只要一个不同就返回true 12 | return shallowDiffers(this.props, props) || shallowDiffers(this.state, state); 13 | } 14 | } 15 | 16 | /** 17 | * 判断两对象的属性值不同 18 | * @param a 19 | * @param b 20 | */ 21 | function shallowDiffers(a: IKeyValue, b: IKeyValue): boolean { 22 | for (const i in a) { 23 | if (!(i in b)) { 24 | return true; 25 | } 26 | } 27 | for (const i in b) { 28 | if (a[i] !== b[i]) { 29 | return true; 30 | } 31 | } 32 | return false; 33 | } 34 | -------------------------------------------------------------------------------- /src/render.ts: -------------------------------------------------------------------------------- 1 | import { diff } from "./vdom/diff"; 2 | import { VNode } from "./vnode"; 3 | import { IVDom, buildVDom } from "./vdom/index"; 4 | // import { defer } from "./util"; 5 | import { findVDom } from "./find"; 6 | import { Component } from "./component"; 7 | 8 | /** 9 | * 创建组件到dom上 10 | * @param vnode jsx 11 | * @param parent 挂载的dom元素 12 | * @param merge 原dom元素 13 | * @param domChild 虚拟dom用于挂载原来挂载在dom元素上的属性 14 | */ 15 | export function render(vnode: VNode, parent: Element, dom?: Element): Element | Node | Text | Component { 16 | let vdom: IVDom | undefined = findVDom(dom); 17 | if (!vdom) { 18 | vdom = buildVDom(dom); 19 | } 20 | const newVDom = diff(vdom, vnode, {}, false, parent, false); 21 | return newVDom.base || newVDom.component as Component; 22 | } 23 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from "./vnode"; 2 | import { Component } from "./component"; 3 | import { IVDom } from "./vdom/index"; 4 | 5 | /** 6 | * 通用map 7 | */ 8 | export interface IKeyValue { 9 | [name: string]: any; 10 | } 11 | export interface IBaseProps extends IKeyValue { 12 | ref?: RefType; 13 | children?: childType[]|childType; 14 | key?: string|number; 15 | } 16 | /** 17 | * 函数组件 18 | */ 19 | export type funComponent = (props?: IBaseProps, content?: any) => childType; 20 | /** 21 | * nodeName的类型 22 | */ 23 | export type childType = VNode|string|number|boolean|null|undefined|void; 24 | 25 | export type ComponentContext = Component | Element | Node | HTMLElement | IVDom; 26 | 27 | export interface IRefObject { 28 | current: ComponentContext|null; 29 | } 30 | 31 | export type RefFun = (c: ComponentContext|null) => void; 32 | 33 | export type RefType = IRefObject | RefFun; 34 | 35 | export interface IBaseVNode { 36 | $$typeof: any; 37 | } 38 | 39 | // export interface IReactContext extends IBaseVNode { 40 | // calculateChangedBits: ((a: T, b: T) => number) | null; 41 | // defaultValue: T; 42 | // changedBits: number; 43 | // Provider: IReactProvider; 44 | // Consumer: IReactContext; 45 | // } 46 | // export interface IReactProvider extends IBaseVNode { 47 | // $$typeof: any; 48 | // context: IReactContext; 49 | // } 50 | 51 | export type NodeName = string | typeof Component | funComponent; 52 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { global } from "./env"; 2 | declare const Promise: any; 3 | declare class Object { 4 | public static assign: (...args: any[]) => any; 5 | } 6 | 7 | const canUsePromise = "Promise" in global; 8 | /** 9 | * 异步调度方法,异步的执行传入的方法 10 | */ 11 | export let defer: (fn: () => void) => void; 12 | /* istanbul ignore if */ 13 | if (canUsePromise) { 14 | const promiseDefer = Promise.resolve(); 15 | defer = (fn: () => void) => promiseDefer.then(fn); 16 | } else { 17 | defer = setTimeout; 18 | } 19 | 20 | /** 21 | * Object.assign的兼容 22 | */ 23 | export const extend = Object.assign || /* istanbul ignore next */ function assign_(t: any) { 24 | for (let s, i = 1, n = arguments.length; i < n; i++) { 25 | s = arguments[i]; 26 | for (const p in s) { 27 | if (Object.prototype.hasOwnProperty.call(s, p)) { 28 | t[p] = s[p]; 29 | } 30 | } 31 | } 32 | return t; 33 | }; 34 | 35 | export const hasSymbol = typeof Symbol === "function" && (Symbol as any).for; 36 | export const innerHTML = "dangerouslySetInnerHTML"; 37 | // export const hasOwnProperty = Object.prototype.hasOwnProperty; 38 | export const REACT_ELEMENT_TYPE: symbol | number = hasSymbol ? (Symbol as any).for("react.element") : 0xeac7; 39 | export const REACT_FRAGMENT_TYPE = hasSymbol ? (Symbol as any).for("react.fragment") : 0xeacb; 40 | export const REACT_PROVIDER_TYPE = hasSymbol ? (Symbol as any).for("react.provider") : 0xeacd; 41 | export const REACT_CONTEXT_TYPE = hasSymbol ? (Symbol as any).for("react.context") : 0xeace; 42 | 43 | const toString = Object.prototype.toString; 44 | 45 | export function isArray(obj: any): boolean { 46 | if (Array.isArray) { 47 | return Array.isArray(obj); 48 | } 49 | return toString.call(obj) === "[object Array]"; 50 | } 51 | 52 | /** 53 | * 判断是否为Text节点 54 | * @param node 55 | */ 56 | export function isTextNode(node: Text | any): boolean { 57 | return node && node.nodeType === 3; 58 | // return node.splitText !== undefined; 59 | } 60 | 61 | export function rest(s: any, e: string[]): any { 62 | const t: any = {}; 63 | for (const p_ in s) { 64 | if (Object.prototype.hasOwnProperty.call(s, p_) && e.indexOf(p_) < 0) { 65 | t[p_] = s[p_]; 66 | } 67 | } 68 | if (s != null && typeof (Object as any).getOwnPropertySymbols === "function") { 69 | for (let i: number = 0, p_ = (Object as any).getOwnPropertySymbols(s); i < p_.length; i++) { 70 | if (e.indexOf(p_[i] as any) < 0) { 71 | t[p_[i]] = s[p_[i]]; 72 | } 73 | } 74 | } 75 | return t; 76 | } 77 | -------------------------------------------------------------------------------- /src/vdom/index.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from "../vnode"; 2 | import { Component } from "../component"; 3 | // import { ATTR_KEY } from "../constants"; 4 | import { extend, isTextNode } from "../util"; 5 | import { IKeyValue, IRefObject, ComponentContext, IBaseProps, childType } from "../types"; 6 | import { setVDom } from "../find"; 7 | 8 | /** 9 | * dom节点与vnode是否相同的标签 10 | * @param node 11 | * @param vnode 12 | * @param hydrating 13 | */ 14 | export function isSameNodeType(node: IVDom, vnode: childType, hydrating: boolean): boolean { 15 | if (vnode == null) { 16 | return false; 17 | } 18 | if (typeof vnode === "string" || typeof vnode === "number" || typeof vnode === "boolean") { 19 | // vnode是文本节点,判断dom是否为文本节点 20 | return isTextNode(node.base); 21 | } 22 | if (typeof vnode.type === "string") { 23 | // vnode是原生组件,判断dom非组件的根节点且标签名相同 24 | return !node.componentConstructor && isNamedNode(node, vnode.type); 25 | } 26 | return hydrating || node.componentConstructor === vnode.type; 27 | } 28 | 29 | /** 判断标签名是否相同. 30 | * @param {Element} node 31 | * @param {String} nodeName 32 | */ 33 | export function isNamedNode( 34 | node: IVDom, 35 | nodeName: string, 36 | ): boolean { 37 | return !!(node.normalizedNodeName === nodeName || 38 | ( 39 | node.base && 40 | node.base.nodeName.toLowerCase() === nodeName.toLowerCase() 41 | )); 42 | } 43 | 44 | /** 45 | * 获取当前组件所有地方来的props 46 | * @param vnode 47 | */ 48 | export function getNodeProps(vnode: VNode) { 49 | // jsx上的属性 50 | const props = extend({}, vnode.props); 51 | // props.children = vnode.children; 52 | // 组件类 53 | const nodeName: any = vnode.type; 54 | // 组件默认props 55 | const defaultProps = nodeName.defaultProps; 56 | if (defaultProps !== undefined) { 57 | for (const i in defaultProps) { 58 | if (props[i] === undefined) { 59 | props[i] = defaultProps[i]; 60 | } 61 | } 62 | } 63 | return props; 64 | } 65 | 66 | export interface IEventFun { 67 | [name: string]: (e: Event) => void; 68 | } 69 | 70 | /** 71 | * 真正dom绑定的一些数据 72 | * @constructor 73 | */ 74 | export interface IVDom { 75 | /** 76 | * dom所属的顶级Component 77 | */ 78 | component?: Component; 79 | /** 80 | * 真实dom索引 81 | */ 82 | base: Element| Text | Node | null; 83 | /** 84 | * 每种事件的代理方法存放点, 真实绑定到dom上的方法。 85 | */ 86 | eventProxy?: { [name: string]: ((e: Event) => void) | undefined }; 87 | /** 88 | * dom所属的props 89 | */ 90 | props?: IBaseProps | boolean; 91 | /** 92 | * 通过props设置的事件方法, 通过eventProxy来调用, 保证在不停的props变化时不会一直绑定与解绑。 93 | */ 94 | listeners?: IEventFun; 95 | /** 96 | * dom标签名 97 | */ 98 | normalizedNodeName?: string; 99 | /** 100 | * component类(原型) 101 | */ 102 | componentConstructor?: any; 103 | 104 | parent?: Node|null; 105 | childHandle?: { 106 | replaceChild(pvdom: IVDom): void; 107 | insertChild(pvdom: IVDom): void; 108 | }; 109 | // constructor(base: Element| Text | Node) { 110 | // this.base = base; 111 | // } 112 | // public clear() { 113 | // this.children = undefined; 114 | // this.component = undefined; 115 | // this.eventProxy = undefined; 116 | // this.listeners = undefined; 117 | // this.normalizedNodeName = undefined; 118 | // this.props = undefined; 119 | // this.componentConstructor = undefined; 120 | // } 121 | } 122 | 123 | export function buildVDom(base?: Element|Text|Node): IVDom | undefined { 124 | if (base) { 125 | const vdom = { 126 | base, 127 | }; 128 | try { 129 | setVDom(base, vdom); 130 | } catch (e) { 131 | } 132 | return vdom; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/vnode.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "./component"; 2 | import { IKeyValue, NodeName, IBaseVNode, IBaseProps } from "./types"; 3 | 4 | import { 5 | REACT_ELEMENT_TYPE, 6 | } from "./util"; 7 | 8 | export class VNode implements IBaseVNode { 9 | public $$typeof: symbol | number; 10 | /** 11 | * 组件名 12 | * {string} 为原生组件 13 | * {Component|function} 为自定义组件 14 | */ 15 | public type: NodeName; 16 | /** 17 | * 组件所属的属性 18 | */ 19 | public props: IBaseProps; 20 | /** 21 | * 属性中的key 22 | */ 23 | public key?: string|number; 24 | 25 | /** 26 | * 绑定的组件实例 27 | */ 28 | public component?: Component | undefined | void | null; 29 | public zreactCompatUpgraded?: boolean; 30 | public zreactCompatNormalized?: boolean; 31 | 32 | constructor(nodeName: NodeName, props: IBaseProps) { 33 | this.type = nodeName; 34 | this.props = props; 35 | this.key = props.key; 36 | this.$$typeof = REACT_ELEMENT_TYPE; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/zreact.ts: -------------------------------------------------------------------------------- 1 | import { createElement, createElement as h } from "./create-element"; 2 | import { cloneElement } from "./clone-element"; 3 | import { Component } from "./component"; 4 | import { PureComponent } from "./pure-component"; 5 | import { render } from "./render"; 6 | import options from "./options"; 7 | import { createClass } from "./create-class"; 8 | // import { } from "./rerender"; 9 | // import { IKeyValue } from "./types"; 10 | // import { IVDom } from "./vdom/index"; 11 | import { VNode } from "./vnode"; 12 | import { REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE } from "./util"; 13 | import { createRef } from "./create-ref"; 14 | import { forwardRef } from "./forward-ref"; 15 | import { createContext } from "./create-context"; 16 | import { 17 | findDOMNode, 18 | findVDom, 19 | findVoidNode, 20 | } from "./find"; 21 | import Children from "./children"; 22 | import { unmountComponent, rerender } from "./vdom/component"; 23 | 24 | declare const VERSION_ENV: string; 25 | const version = VERSION_ENV; 26 | 27 | /** 28 | * 判断是否为一个组件对象 29 | * @param element 30 | */ 31 | function isValidElement(element: VNode| any): boolean { 32 | return element && (element instanceof VNode); 33 | } 34 | 35 | function unmountComponentAtNode(dom: any): boolean { 36 | const vdom = findVDom(dom); 37 | if (vdom) { 38 | unmountComponent(vdom.component as any); 39 | return true; 40 | } 41 | return false; 42 | } 43 | 44 | function createPortal(vnode: any, container: HTMLElement): null { 45 | // mountVNode can handle array of vnodes for us 46 | const voidNodes = findVoidNode(container); 47 | const voidNode = voidNodes && voidNodes[0]; 48 | const first = container.firstElementChild; 49 | render(vnode, container, (voidNode || first) as any); 50 | return null; 51 | } 52 | 53 | // export default { 54 | // Children, 55 | // Component, 56 | // createRef, 57 | // PureComponent, 58 | // createElement, 59 | // cloneElement, 60 | // createClass, 61 | // createPortal, 62 | // createContext, 63 | // findDOMNode, 64 | // findVDom, 65 | // isValidElement, 66 | // h, 67 | // options, 68 | // render, 69 | // rerender, 70 | // unmountComponentAtNode, 71 | // version, 72 | // Element: REACT_ELEMENT_TYPE, 73 | // Fragment: REACT_FRAGMENT_TYPE, 74 | // forwardRef, 75 | // }; 76 | 77 | export { 78 | Component, 79 | Children, 80 | createRef, 81 | PureComponent, 82 | cloneElement, 83 | createClass, 84 | createElement, 85 | createPortal, 86 | createContext, 87 | findDOMNode, 88 | findVDom, 89 | isValidElement, 90 | h, 91 | options, 92 | render, 93 | rerender, 94 | unmountComponentAtNode, 95 | version, 96 | REACT_ELEMENT_TYPE as Element, 97 | REACT_FRAGMENT_TYPE as Fragment, 98 | forwardRef, 99 | }; 100 | -------------------------------------------------------------------------------- /test/browser/context.js: -------------------------------------------------------------------------------- 1 | import { h, render, Component, createContext } from 'zreact'; 2 | /** @jsx h */ 3 | 4 | const CHILDREN_MATCHER = sinon.match( v => v==null || Array.isArray(v) && !v.length , '[empty children]'); 5 | 6 | describe('context', () => { 7 | let scratch; 8 | 9 | before( () => { 10 | scratch = document.createElement('div'); 11 | (document.body || document.documentElement).appendChild(scratch); 12 | }); 13 | 14 | beforeEach( () => { 15 | scratch.innerHTML = ''; 16 | }); 17 | 18 | after( () => { 19 | scratch.parentNode.removeChild(scratch); 20 | scratch = null; 21 | }); 22 | 23 | it('should pass context to grandchildren', () => { 24 | const CONTEXT = { a:'a' }; 25 | const PROPS = { b:'b' }; 26 | // let inner; 27 | 28 | class Outer extends Component { 29 | getChildContext() { 30 | return CONTEXT; 31 | } 32 | render(props) { 33 | return
; 34 | } 35 | } 36 | sinon.spy(Outer.prototype, 'getChildContext'); 37 | 38 | class Inner extends Component { 39 | // constructor() { 40 | // super(); 41 | // inner = this; 42 | // } 43 | shouldComponentUpdate() { return true; } 44 | componentWillReceiveProps() {} 45 | componentWillUpdate() {} 46 | componentDidUpdate() {} 47 | render(props, state, context) { 48 | return
{ context && context.a }
; 49 | } 50 | } 51 | sinon.spy(Inner.prototype, 'shouldComponentUpdate'); 52 | sinon.spy(Inner.prototype, 'componentWillReceiveProps'); 53 | sinon.spy(Inner.prototype, 'componentWillUpdate'); 54 | sinon.spy(Inner.prototype, 'componentDidUpdate'); 55 | sinon.spy(Inner.prototype, 'render'); 56 | 57 | const root = render(, scratch, scratch.lastChild); 58 | 59 | expect(Outer.prototype.getChildContext).to.have.been.calledOnce; 60 | 61 | // initial render does not invoke anything but render(): 62 | expect(Inner.prototype.render).to.have.been.calledWith({ children:CHILDREN_MATCHER }, {}, CONTEXT); 63 | 64 | CONTEXT.foo = 'bar'; 65 | render(, scratch, root); 66 | 67 | expect(Outer.prototype.getChildContext).to.have.been.calledTwice; 68 | 69 | let props = { children: CHILDREN_MATCHER, ...PROPS }; 70 | expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(props, {}, CONTEXT); 71 | expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(props, CONTEXT); 72 | expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(props, {}); 73 | expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({ children:CHILDREN_MATCHER }, {}); 74 | expect(Inner.prototype.render).to.have.been.calledWith(props, {}, CONTEXT); 75 | 76 | 77 | /* Future: 78 | * Newly created context objects are *not* currently cloned. 79 | * This test checks that they *are* cloned. 80 | */ 81 | // Inner.prototype.render.resetHistory(); 82 | // CONTEXT.foo = 'baz'; 83 | // inner.forceUpdate(); 84 | // expect(Inner.prototype.render).to.have.been.calledWith(PROPS, {}, { a:'a', foo:'bar' }); 85 | }); 86 | 87 | it('should pass context to direct children', () => { 88 | const CONTEXT = { a:'a' }; 89 | const PROPS = { b:'b' }; 90 | 91 | class Outer extends Component { 92 | getChildContext() { 93 | return CONTEXT; 94 | } 95 | render(props) { 96 | return ; 97 | } 98 | } 99 | sinon.spy(Outer.prototype, 'getChildContext'); 100 | 101 | class Inner extends Component { 102 | shouldComponentUpdate() { return true; } 103 | componentWillReceiveProps() {} 104 | componentWillUpdate() {} 105 | componentDidUpdate() {} 106 | render(props, state, context) { 107 | return
{ context && context.a }
; 108 | } 109 | } 110 | sinon.spy(Inner.prototype, 'shouldComponentUpdate'); 111 | sinon.spy(Inner.prototype, 'componentWillReceiveProps'); 112 | sinon.spy(Inner.prototype, 'componentWillUpdate'); 113 | sinon.spy(Inner.prototype, 'componentDidUpdate'); 114 | sinon.spy(Inner.prototype, 'render'); 115 | 116 | const vdom = render(, scratch, scratch.lastChild); 117 | 118 | expect(Outer.prototype.getChildContext).to.have.been.calledOnce; 119 | 120 | // initial render does not invoke anything but render(): 121 | expect(Inner.prototype.render).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}, CONTEXT); 122 | 123 | CONTEXT.foo = 'bar'; 124 | render(, scratch, vdom); 125 | 126 | expect(Outer.prototype.getChildContext).to.have.been.calledTwice; 127 | 128 | let props = { children: CHILDREN_MATCHER, ...PROPS }; 129 | expect(Inner.prototype.shouldComponentUpdate).to.have.been.calledOnce.and.calledWith(props, {}, CONTEXT); 130 | expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledWith(props, CONTEXT); 131 | expect(Inner.prototype.componentWillUpdate).to.have.been.calledWith(props, {}); 132 | expect(Inner.prototype.componentDidUpdate).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}); 133 | expect(Inner.prototype.render).to.have.been.calledWith(props, {}, CONTEXT); 134 | 135 | // make sure render() could make use of context.a 136 | // expect(Inner.prototype.render).to.have.returned(sinon.match({ children:'a' })); 137 | }); 138 | 139 | it('should preserve existing context properties when creating child contexts', () => { 140 | let outerContext = { outer:true }, 141 | innerContext = { inner:true }; 142 | class Outer extends Component { 143 | getChildContext() { 144 | return { outerContext }; 145 | } 146 | render() { 147 | return
; 148 | } 149 | } 150 | 151 | class Inner extends Component { 152 | getChildContext() { 153 | return { innerContext }; 154 | } 155 | render(props, state, context) { 156 | expect(props.children).to.be.null 157 | expect(state).to.deep.equal({}); 158 | expect(context).to.deep.equal({ outerContext }); 159 | return ; 160 | } 161 | } 162 | 163 | class InnerMost extends Component { 164 | render(props, state, context) { 165 | expect(props.children).to.be.null 166 | expect(state).to.deep.equal({}); 167 | expect(context).to.deep.equal({ outerContext, innerContext }); 168 | return test; 169 | } 170 | } 171 | 172 | sinon.spy(Inner.prototype, 'render'); 173 | sinon.spy(InnerMost.prototype, 'render'); 174 | 175 | render(, scratch); 176 | 177 | // expect(Inner.prototype.render).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}, { outerContext }); 178 | // expect(InnerMost.prototype.render).to.have.been.calledWith({ children: CHILDREN_MATCHER }, {}, { outerContext, innerContext }); 179 | }); 180 | it("react 16 context base test", () => { 181 | const Theme = createContext("red"); 182 | const child = (context) => { 183 | return context 184 | } 185 | let change = null; 186 | class Test extends Component { 187 | constructor(p, c) { 188 | super(p, c); 189 | this.state = { 190 | theme: "black", 191 | } 192 | change = this.change.bind(this); 193 | } 194 | change(value) { 195 | this.state.theme = value; 196 | this.forceUpdate(); 197 | } 198 | render() { 199 | return ( 200 | 201 | 202 | {child} 203 | 204 | 205 | ) 206 | } 207 | } 208 | render(, scratch) 209 | expect(scratch.firstChild.nodeValue).to.equal("black"); 210 | change("test"); 211 | expect(scratch.firstChild.nodeValue).to.equal("test"); 212 | }); 213 | 214 | it("react 16 context test deep", () => { 215 | const Theme = createContext("red"); 216 | const child = (context) => { 217 | return context 218 | } 219 | let change = null; 220 | class Test extends Component { 221 | constructor(p, c) { 222 | super(p, c); 223 | this.state = { 224 | theme: "black", 225 | theme2: "red", 226 | } 227 | change = this.change.bind(this); 228 | } 229 | change(value, value2) { 230 | this.state.theme = value; 231 | this.state.theme2 = value2; 232 | this.forceUpdate(); 233 | } 234 | render() { 235 | return ( 236 | 237 |
238 | 239 | {child} 240 | 241 | 242 | 243 | {child} 244 | 245 | 246 |
247 |
248 | ) 249 | } 250 | } 251 | render(, scratch) 252 | expect(scratch.innerHTML).to.equal("
blackred
"); 253 | change("test", "test1"); 254 | expect(scratch.innerHTML).to.equal("
testtest1
"); 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /test/browser/devtools.js: -------------------------------------------------------------------------------- 1 | import { h, Component, render, options, findDOMNode, findVDom } from 'zreact'; 2 | import { initDevTools } from '../../build/devtools'; 3 | import { unmountComponent } from '../../build/vdom/component'; 4 | 5 | class StatefulComponent extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = {count: 0}; 9 | } 10 | 11 | render() { 12 | return h('span', {}, String(this.state.count)); 13 | } 14 | } 15 | 16 | function FunctionalComponent() { 17 | return h('span', {class: 'functional'}, 'Functional'); 18 | } 19 | 20 | function Label({label}) { 21 | return label; 22 | } 23 | 24 | class MultiChild extends Component { 25 | constructor(props) { 26 | super(props); 27 | this.state = {count: props.initialCount}; 28 | } 29 | 30 | render() { 31 | return h('div', {}, Array(this.state.count).fill('child')); 32 | } 33 | } 34 | 35 | let describe_ = describe; 36 | if (!('name' in Function.prototype)) { 37 | // Skip these tests under Internet Explorer 38 | describe_ = describe.skip; 39 | } 40 | // close devtools auto 41 | global.DEVTOOLS_ENV = "production" 42 | describe_('React Developer Tools integration', () => { 43 | let cleanup; 44 | let container; 45 | let renderer; 46 | 47 | // Maps of DOM node to React*Component-like objects. 48 | // For composite components, there will be two instances for each node, one 49 | // for the composite component (instanceMap) and one for the root child DOM 50 | // component rendered by that component (domInstanceMap) 51 | let instanceMap = new Map(); 52 | let domInstanceMap = new Map(); 53 | 54 | beforeEach(() => { 55 | container = document.createElement('div'); 56 | document.body.appendChild(container); 57 | 58 | const onMount = instance => { 59 | if (instance._renderedChildren) { 60 | domInstanceMap.set(instance.node, instance); 61 | } else { 62 | instanceMap.set(instance.node, instance); 63 | } 64 | }; 65 | 66 | const onUnmount = instance => { 67 | instanceMap.delete(instance.node); 68 | domInstanceMap.delete(instance.node); 69 | }; 70 | 71 | global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { 72 | inject: sinon.spy(_renderer => { 73 | renderer = _renderer; 74 | renderer.Mount._renderNewRootComponent = sinon.stub(); 75 | renderer.Reconciler.mountComponent = sinon.spy(onMount); 76 | renderer.Reconciler.unmountComponent = sinon.spy(onUnmount); 77 | renderer.Reconciler.receiveComponent = sinon.stub(); 78 | }) 79 | }; 80 | cleanup = initDevTools(); 81 | }); 82 | 83 | afterEach(() => { 84 | container.remove(); 85 | cleanup(); 86 | }); 87 | 88 | it('registers preact as a renderer with the React DevTools hook', () => { 89 | expect(global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject).to.be.called; 90 | }); 91 | 92 | // Basic component addition/update/removal tests 93 | it('notifies dev tools about new components', () => { 94 | render(h(StatefulComponent), container); 95 | expect(renderer.Reconciler.mountComponent).to.be.called; 96 | }); 97 | 98 | it('notifies dev tools about component updates', () => { 99 | const node = render(h(StatefulComponent), container); 100 | findVDom(node).component.forceUpdate(); 101 | expect(renderer.Reconciler.receiveComponent).to.be.called; 102 | }); 103 | 104 | it('notifies dev tools when components are removed', () => { 105 | const node = render(h(StatefulComponent), container); 106 | unmountComponent(findVDom(node).component, true); 107 | expect(renderer.Reconciler.unmountComponent).to.be.called; 108 | }); 109 | 110 | // Test properties of DOM components exposed to devtools via 111 | // ReactDOMComponent-like instances 112 | it('exposes the tag name of DOM components', () => { 113 | const node = render(h(StatefulComponent), container); 114 | const domInstance = domInstanceMap.get(node); 115 | expect(domInstance._currentElement.type).to.equal('span'); 116 | }); 117 | 118 | it('exposes DOM component props', () => { 119 | const node = render(h(FunctionalComponent), container); 120 | const domInstance = domInstanceMap.get(node); 121 | expect(domInstance._currentElement.props.class).to.equal('functional'); 122 | }); 123 | 124 | it('exposes text component contents', () => { 125 | const node = render(h(Label, {label: 'Text content'}), container); 126 | const textInstance = domInstanceMap.get(node); 127 | expect(textInstance._stringText).to.equal('Text content'); 128 | }); 129 | 130 | // Test properties of composite components exposed to devtools via 131 | // ReactCompositeComponent-like instances 132 | it('exposes the name of composite component classes', () => { 133 | const node = render(h(StatefulComponent), container); 134 | expect(instanceMap.get(node).getName()).to.equal('StatefulComponent'); 135 | }); 136 | 137 | it('exposes composite component props', () => { 138 | const node = render(h(Label, {label: 'Text content'}), container); 139 | const instance = instanceMap.get(node); 140 | expect(instance._currentElement.props.label).to.equal('Text content'); 141 | }); 142 | 143 | it('exposes composite component state', () => { 144 | const node = render(h(StatefulComponent), container); 145 | const vdom = findVDom(node); 146 | vdom.component.setState({count: 42}); 147 | vdom.component.forceUpdate(); 148 | 149 | expect(instanceMap.get(node).state).to.deep.equal({count: 42}); 150 | }); 151 | 152 | // Test setting state via devtools 153 | it('updates component when setting state from devtools', () => { 154 | const node = render(h(StatefulComponent), container); 155 | 156 | instanceMap.get(node).setState({count: 10}); 157 | instanceMap.get(node).forceUpdate(); 158 | 159 | expect(node.textContent).to.equal('10'); 160 | }); 161 | 162 | // Test that the original instance is exposed via `_instance` so it can 163 | // be accessed conveniently via `$r` in devtools 164 | 165 | // Functional component handling tests 166 | xit('wraps functional components with stateful ones', () => { 167 | const vnode = h(FunctionalComponent); 168 | expect(vnode.nodeName.prototype).to.have.property('render'); 169 | }); 170 | 171 | it('exposes the name of functional components', () => { 172 | const node = render(h(FunctionalComponent), container); 173 | const instance = instanceMap.get(node); 174 | expect(instance.getName()).to.equal('FunctionalComponent'); 175 | }); 176 | 177 | xit('exposes a fallback name if the component has no useful name', () => { 178 | const node = render(h(() => h('div')), container); 179 | const instance = instanceMap.get(node); 180 | expect(instance.getName()).to.equal('(Function.name missing)'); 181 | }); 182 | 183 | // Test handling of DOM children 184 | it('notifies dev tools about DOM children', () => { 185 | const node = render(h(StatefulComponent), container); 186 | const domInstance = domInstanceMap.get(node); 187 | expect(renderer.Reconciler.mountComponent).to.have.been.calledWith(domInstance); 188 | }); 189 | 190 | it('notifies dev tools when a component update adds DOM children', () => { 191 | const node = render(h(MultiChild, {initialCount: 2}), container); 192 | 193 | const vdom = findVDom(node); 194 | vdom.component.setState({count: 4}); 195 | vdom.component.forceUpdate(); 196 | expect(renderer.Reconciler.mountComponent).to.have.been.called; 197 | }); 198 | 199 | it('notifies dev tools when a component update modifies DOM children', () => { 200 | const node = render(h(StatefulComponent), container); 201 | 202 | instanceMap.get(node).setState({count: 10}); 203 | instanceMap.get(node).forceUpdate(); 204 | 205 | const textInstance = domInstanceMap.get(node.childNodes[0]); 206 | expect(textInstance._stringText).to.equal('10'); 207 | }); 208 | 209 | it('notifies dev tools when a component update removes DOM children', () => { 210 | const node = render(h(MultiChild, {initialCount: 1}), container); 211 | const vdom = findVDom(node); 212 | vdom.component.setState({count: 0}); 213 | vdom.component.forceUpdate(); 214 | 215 | expect(renderer.Reconciler.unmountComponent).to.be.called; 216 | }); 217 | 218 | // Root component info 219 | it('exposes root components on the _instancesByReactRootID map', () => { 220 | render(h(StatefulComponent), container); 221 | expect(Object.keys(renderer.Mount._instancesByReactRootID).length).to.equal(1); 222 | }); 223 | 224 | it('notifies dev tools when new root components are mounted', () => { 225 | render(h(StatefulComponent), container); 226 | expect(renderer.Mount._renderNewRootComponent).to.be.called; 227 | }); 228 | 229 | it('removes root components when they are unmounted', () => { 230 | const node = render(h(StatefulComponent), container); 231 | unmountComponent(findVDom(node).component, true); 232 | expect(Object.keys(renderer.Mount._instancesByReactRootID).length).to.equal(0); 233 | }); 234 | 235 | it('counts root components correctly when a root renders a composite child', () => { 236 | function Child() { 237 | return h('main'); 238 | } 239 | function Parent() { 240 | return h(Child); 241 | } 242 | 243 | render(h(Parent), container); 244 | 245 | expect(Object.keys(renderer.Mount._instancesByReactRootID).length).to.equal(1); 246 | }); 247 | 248 | it('counts root components correctly when a native element has a composite child', () => { 249 | function Link() { 250 | return h('a'); 251 | } 252 | function Root() { 253 | return h('div', {}, h(Link)); 254 | } 255 | 256 | render(h(Root), container); 257 | 258 | expect(Object.keys(renderer.Mount._instancesByReactRootID).length).to.equal(1); 259 | }); 260 | }); 261 | -------------------------------------------------------------------------------- /test/browser/keys.js: -------------------------------------------------------------------------------- 1 | import { h, Component, render } from 'zreact'; 2 | /** @jsx h */ 3 | 4 | describe('keys', () => { 5 | let scratch; 6 | 7 | before( () => { 8 | scratch = document.createElement('div'); 9 | (document.body || document.documentElement).appendChild(scratch); 10 | }); 11 | 12 | beforeEach( () => { 13 | scratch.innerHTML = ''; 14 | }); 15 | 16 | after( () => { 17 | scratch.parentNode.removeChild(scratch); 18 | scratch = null; 19 | }); 20 | 21 | // See developit/preact-compat#21 22 | it('should remove orphaned keyed nodes', () => { 23 | const root = render(( 24 |
25 |
1
26 |
  • a
  • 27 |
  • b
  • 28 |
    29 | ), scratch); 30 | 31 | render(( 32 |
    33 |
    2
    34 |
  • b
  • 35 |
  • c
  • 36 |
    37 | ), scratch, root); 38 | 39 | expect(scratch.innerHTML).to.equal('
    2
  • b
  • c
  • '); 40 | }); 41 | 42 | it('should set VNode#key property', () => { 43 | expect(
    ).to.have.property('key').that.is.undefined; 44 | expect(
    ).to.have.property('key').that.is.undefined; 45 | expect(
    ).to.have.property('key', '1'); 46 | }); 47 | 48 | it('should remove keyed nodes (#232)', () => { 49 | class App extends Component { 50 | componentDidMount() { 51 | setTimeout(() => { 52 | this.setState({opened: true,loading: true}) 53 | }, 10); 54 | setTimeout(() => { 55 | this.setState({opened: true,loading: false}) 56 | }, 20); 57 | } 58 | 59 | render({ opened, loading }) { 60 | return ( 61 | 62 |
    This div needs to be here for this to break
    63 | { opened && !loading &&
    {[]}
    } 64 |
    65 | ); 66 | } 67 | } 68 | 69 | class BusyIndicator extends Component { 70 | render({ children, busy }) { 71 | return
    72 | { children && children.length ? children :
    } 73 |
    74 |
    indicator
    75 |
    indicator
    76 |
    indicator
    77 |
    78 |
    ; 79 | } 80 | } 81 | 82 | let root; 83 | 84 | root = render(, scratch, root); 85 | root = render(, scratch, root); 86 | root = render(, scratch, root); 87 | 88 | let html = String(root.innerHTML).replace(/ class=""/g, ''); 89 | expect(html).to.equal('
    This div needs to be here for this to break
    indicator
    indicator
    indicator
    '); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/browser/performance.js: -------------------------------------------------------------------------------- 1 | /*global coverage, ENABLE_PERFORMANCE, NODE_ENV*/ 2 | /*eslint no-console:0*/ 3 | /** @jsx h */ 4 | 5 | let { h, Component, render } = require('zreact'); 6 | 7 | const MULTIPLIER = ENABLE_PERFORMANCE ? (coverage ? 5 : 1) : 999999; 8 | 9 | 10 | let now = typeof performance!=='undefined' && performance.now ? () => performance.now() : () => +new Date(); 11 | 12 | function loop(iter, time) { 13 | let start = now(), 14 | count = 0; 15 | while ( now()-start < time ) { 16 | count++; 17 | iter(); 18 | } 19 | return count; 20 | } 21 | 22 | 23 | function benchmark(iter, callback) { 24 | let a = 0; // eslint-disable-line no-unused-vars 25 | function noop() { 26 | try { a++; } finally { a += Math.random(); } 27 | } 28 | 29 | // warm 30 | for (let i=100; i--; ) noop(), iter(); 31 | 32 | let count = 2, 33 | time = 500, 34 | passes = 0, 35 | noops = loop(noop, time), 36 | iterations = 0; 37 | 38 | function next() { 39 | iterations += loop(iter, time); 40 | setTimeout(++passes===count ? done : next, 10); 41 | } 42 | 43 | function done() { 44 | let ticks = Math.round(noops / iterations * count), 45 | hz = iterations / count / time * 1000, 46 | message = `${hz|0}/s (${ticks} ticks)`; 47 | callback({ iterations, noops, count, time, ticks, hz, message }); 48 | } 49 | 50 | next(); 51 | } 52 | 53 | 54 | describe('performance', function() { 55 | let scratch; 56 | 57 | this.timeout(10000); 58 | 59 | before( () => { 60 | if (coverage) { 61 | console.warn('WARNING: Code coverage is enabled, which dramatically reduces performance. Do not pay attention to these numbers.'); 62 | } 63 | scratch = document.createElement('div'); 64 | (document.body || document.documentElement).appendChild(scratch); 65 | }); 66 | 67 | beforeEach( () => { 68 | scratch.innerHTML = ''; 69 | }); 70 | 71 | after( () => { 72 | scratch.parentNode.removeChild(scratch); 73 | scratch = null; 74 | }); 75 | 76 | it('should rerender without changes fast', done => { 77 | let jsx = ( 78 |
    79 |
    80 |

    a {'b'} c {0} d

    81 | 85 |
    86 |
    87 |
    {}}> 88 | 89 | 90 |
    91 | 92 | 93 |
    94 | 95 | 96 | 97 | 98 | 99 | 100 |
    101 |
    102 |
    103 | ); 104 | 105 | let root; 106 | benchmark( () => { 107 | root = render(jsx, scratch, root); 108 | }, ({ ticks, message }) => { 109 | console.log(`PERF: empty diff: ${message}`); 110 | expect(ticks).to.be.below(150 * MULTIPLIER); 111 | done(); 112 | }); 113 | }); 114 | 115 | it('should rerender repeated trees fast', done => { 116 | class Header extends Component { 117 | render() { 118 | return ( 119 |
    120 |

    a {'b'} c {0} d

    121 | 125 |
    126 | ); 127 | } 128 | } 129 | class Form extends Component { 130 | render() { 131 | return ( 132 |
    {}}> 133 | 134 | 135 |
    136 | 137 | 138 |
    139 | 140 | 141 | ); 142 | } 143 | } 144 | class ButtonBar extends Component { 145 | render() { 146 | return ( 147 | 148 | 149 | 150 | 151 | 152 | 153 | ); 154 | } 155 | } 156 | class Button extends Component { 157 | render(props) { 158 | return 231 | 232 | 233 | 234 | 235 | 236 | 237 |
    238 | ); 239 | }, ({ ticks, message }) => { 240 | console.log(`PERF: large VTree: ${message}`); 241 | expect(ticks).to.be.below(2000 * MULTIPLIER); 242 | done(); 243 | }); 244 | }); 245 | 246 | 247 | it('should mutate styles/properties quickly', done => { 248 | let counter = 0; 249 | const keyLooper = n => c => c % n ? `${c}px` : c; 250 | const get = (obj, i) => obj[i%obj.length]; 251 | const CLASSES = ['foo', 'foo bar', '', 'baz-bat', null]; 252 | const STYLES = []; 253 | const MULTIVALUE = ['0 1px', '0 0 1px 0', '0', '1px', '20px 10px', '7em 5px', '1px 0 5em 2px']; 254 | const STYLEKEYS = [ 255 | ['left', keyLooper(3)], 256 | ['top', keyLooper(2)], 257 | ['margin', c => get(MULTIVALUE, c).replace('1px', c+'px')], 258 | ['padding', c => get(MULTIVALUE, c)], 259 | ['position', c => c%5 ? c%2 ? 'absolute' : 'relative' : null], 260 | ['display', c => c%10 ? c%2 ? 'block' : 'inline' : 'none'], 261 | ['color', c => `rgba(${c%255}, ${255 - c%255}, ${50+c%150}, ${c%50/50})`], 262 | ['border', c => c%5 ? `${c%10}px ${c%2?'solid':'dotted'} ${STYLEKEYS[6][1](c)}` : ''] 263 | ]; 264 | for (let i=0; i<1000; i++) { 265 | let style = {}; 266 | for (let j=0; j ( 274 |
    275 | 276 | 277 |
    278 |

    p1

    279 |

    p2

    280 |

    p3

    281 |

    p4

    282 |
    283 |
    284 | ); 285 | 286 | let root, count=0; 287 | benchmark( () => { 288 | root = render(app(++count), scratch, root); 289 | }, ({ ticks, message }) => { 290 | console.log(`PERF: style/prop mutation: ${message}`); 291 | expect(ticks).to.be.below(350 * MULTIPLIER); 292 | done(); 293 | }); 294 | }); 295 | 296 | it('should hydrate from SSR quickly', done => { 297 | class Header extends Component { 298 | render() { 299 | return ( 300 |
    301 |

    a {'b'} c {0} d

    302 | 306 |
    307 | ); 308 | } 309 | } 310 | class Form extends Component { 311 | render() { 312 | return ( 313 |
    {}}> 314 | 315 | 316 |
    317 | 318 | 319 |
    320 | 321 | 322 | ); 323 | } 324 | } 325 | const ButtonBar = () => ( 326 | 327 | 328 | 329 | 330 | 331 | 332 | ); 333 | class Button extends Component { 334 | handleClick() {} 335 | render(props) { 336 | return