{"id":25485813,"url":"https://github.com/zhouyk/femo","last_synced_at":"2025-10-28T12:17:33.505Z","repository":{"id":35014197,"uuid":"187142062","full_name":"ZhouYK/femo","owner":"ZhouYK","description":"react 数据流管理","archived":false,"fork":false,"pushed_at":"2025-07-21T23:13:46.000Z","size":2097,"stargazers_count":10,"open_issues_count":2,"forks_count":4,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-21T16:57:28.517Z","etag":null,"topics":["data-flow","react","react-hooks"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ZhouYK.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-05-17T03:43:58.000Z","updated_at":"2025-05-16T08:08:37.000Z","dependencies_parsed_at":"2024-06-17T03:25:51.430Z","dependency_job_id":"f72b293b-3f28-4b97-b2f8-5be449bdda3e","html_url":"https://github.com/ZhouYK/femo","commit_stats":{"total_commits":440,"total_committers":6,"mean_commits":73.33333333333333,"dds":0.5340909090909092,"last_synced_commit":"3077866b9d1c610270358229608ebc663af8b45d"},"previous_names":[],"tags_count":145,"template":false,"template_full_name":null,"purl":"pkg:github/ZhouYK/femo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ZhouYK%2Ffemo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ZhouYK%2Ffemo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ZhouYK%2Ffemo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ZhouYK%2Ffemo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ZhouYK","download_url":"https://codeload.github.com/ZhouYK/femo/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ZhouYK%2Ffemo/sbom","scorecard":{"id":156957,"data":{"date":"2025-08-11","repo":{"name":"github.com/ZhouYK/femo","commit":"93c3f2453fb1c74c3e2bfd161038c7ec227c0e60"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.3,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":1,"reason":"2 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 1","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Code-Review","score":0,"reason":"Found 0/23 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 8 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":1,"reason":"9 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-gcx4-mw62-g8wm","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-16T12:05:43.177Z","repository_id":35014197,"created_at":"2025-08-16T12:05:43.177Z","updated_at":"2025-08-16T12:05:43.177Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":281435702,"owners_count":26500881,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-28T02:00:06.022Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["data-flow","react","react-hooks"],"created_at":"2025-02-18T18:46:51.656Z","updated_at":"2025-10-28T12:17:33.474Z","avatar_url":"https://github.com/ZhouYK.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ca href=\"https://996.icu\"\u003e\u003cimg src=\"https://img.shields.io/badge/link-996.icu-red.svg\"\u003e\u003c/a\u003e\n[![Build Status](https://travis-ci.com/ZhouYK/femo.svg?branch=master)](https://travis-ci.com/ZhouYK/femo)\n[![codecov](https://codecov.io/gh/ZhouYK/femo/branch/master/graph/badge.svg)](https://codecov.io/gh/ZhouYK/femo)\n[![NPM version](https://img.shields.io/npm/v/femo.svg?style=flat)](https://www.npmjs.com/package/femo)\n[![NPM downloads](http://img.shields.io/npm/dm/femo.svg?style=flat)](https://www.npmjs.com/package/femo)\n![package size](https://img.shields.io/bundlephobia/minzip/femo.svg?style=flat)\n![license](https://img.shields.io/github/license/ZhouYK/glue-redux.svg)\n# femo\n\n*拒绝反直觉，直观地管理数据*\n\n## 当前版本是2.x.x版本，1.x.x版本请点击\u003ca href=\"https://github.com/ZhouYK/femo/tree/v1.15.14\"\u003e查看\u003c/a\u003e    \n## 安装 [![NPM version](https://img.shields.io/npm/v/femo.svg?style=flat)](https://www.npmjs.com/package/femo)\n\n```bash\nnpm i femo\nor\nyarn add femo\n```\n\n---\n## 在react中使用\n\n方式一：先声明定义model，再在组件中使用\n\n```js\n// model.js\nimport { glue } from 'femo';\n\nconst student = glue({\n  name: '',\n  age: 0,\n});\n\nexport default student;\n\n```\n\n```jsx\n// Student组件\nimport { useModel } from 'femo';\nimport model from './model';\n\nconst Student = (props) =\u003e {\n  const [student] = useModel(model);\n  \n  return (\n    \u003csection\u003e\n      \u003csection\u003e{ student.name }\u003c/section\u003e\n      \u003csection\u003e{ student.age }\u003c/section\u003e\n    \u003c/section\u003e  \n  )\n}\nexport default Student;\n```\n\n方式二：直接在组件中声明定义并使用\n\n```jsx\n// Student组件\nimport { useModel } from 'femo';\n\nconst Student = (props) =\u003e {\n  const [student, model] = useModel({\n    name: '',\n    age: 0,\n  });\n  \n  return (\n    \u003csection\u003e\n      \u003csection\u003e{ student.name }\u003c/section\u003e\n      \u003csection\u003e{ student.age }\u003c/section\u003e\n    \u003c/section\u003e  \n  )\n}\nexport default Student;\n```\n\n## 脱离react使用\n\n脱离react后，就不能使用react hooks了\n\n```js\nimport { glue, subscribe } from 'femo';\nconst name = glue('初始名字');\n\nconst unsubscribe = subscribe([name], (nameData) =\u003e { console.log(nameData) });\nname('张胜男');\n// 会打印 张胜男\n\n// 取消监听。调用返回的函数即可\nunsubscribe();\n```\n\n## 核心\n\n数据之间轻耦合，数据本身具有完备的处理能力。\n\n## API\n\n### 核心函数\n\n- \u003ca href=\"#glue\"\u003eglue\u003c/a\u003e\n- \u003ca href=\"#subscribe\"\u003esubscribe\u003c/a\u003e\n- \u003ca href=\"#genRaceQueue\"\u003egenRaceQueue\u003c/a\u003e\n- \u003ca href=\"#genRegister\"\u003egenRegister\u003c/a\u003e\n\n\n### \u003cspan id=\"glue\"\u003eglue\u003c/span\u003e\n\n\u003e 定义数据节点\n\n#### 数据节点定义：\n```js\nimport { glue } from 'femo';\n\nconst name = glue('初始名字');\n\n```\n节点数据可以使任意类型。一旦定义，节点的数据类型就确定了，后续不能改变。数据类型不变性只是用了typescript的类型做约束，请遵守这一约束，让数据更清晰和可预测。\n\n#### 数据更新：\n```js\n  name('张三');\n```\n\n#### 数据获取\n```js\n  name(); // 张三\n```\n\n#### 不同的入参更新数据\n\n同步函数\n```js\n  name((state, data) =\u003e {\n    return '李四';\n  });\n```\n\n异步函数\n```js\n  name(async (state, data) =\u003e {\n    return '王二';\n  });\n```\n当入参是异步函数的时候，数据节点会异步地去更新数据。\n\n### \u003cspan id=\"subscribe\"\u003esubscribe\u003c/span\u003e\n\u003e 订阅数据节点\n\n数据节点被订阅过后，其数据的变化会通知到订阅的回调函数里面。\n```js\nimport { glue, subscribe } from 'femo';\n\nconst name = glue('初始名字');\n\nconst unsubscribe = subscribe([name], (nameData) =\u003e { console.log(nameData) });\nname('张胜男');\n// 会打印 张胜男\n\n// 取消监听。调用返回的函数即可\nunsubscribe();\n```\n\n### genRaceQueue\n\u003e 数据节点更新出现竞争时，需要确保当前的数据正确。\n\n什么是竞争？\n\n常见的，先后发送了两个请求p1和p2，p1和p2都有各自的异步回调处理逻辑。一般情况下，先发出去的请求先回来，后发出去的请求后回来。 这种情况下异步回调的处理逻辑的先后顺序是符合预期的。\n\n但存在另外的情况，p1请求先发送后返回，p2请求后发送先返回。那么异步回调的处理顺序就不再是 p1的异步回调 =\u003e p2的异步回调，而是 p2的异步回调 =\u003e p1的异步回调。这种执行顺序显然是不符合预期的，会导致问题。\n\ngenRaceQueue就是解决这种数据可能不一致的问题的。\n\n```js\nimport { genRaceQueue } from 'femo';\n// 首先创建一个异步队列\nconst raceQueue = genRaceQueue();\n\n// 然后将会出现竞争的异步promise放到同一个异步队列中\n\n// p1请求\nraceQueue.push(someModel(params, async (state, data) =\u003e {\n                                      return await fetchRemote(data);\n                                    }));\n// p2请求\nraceQueue.push(someModel(async (state, data) =\u003e { return await fetchRemote() }));\n\n```\n\u003cstrong\u003e数据节点自身也提供了处理竞争的方法\u003ca href=\"#race\"\u003erace\u003c/a\u003e。很多时候可以通过\u003ca href=\"#race\"\u003erace\u003c/a\u003e方法来简化上面\u003ca href=\"#genRaceQueue\"\u003egenRaceQueue\u003c/a\u003e的使用。\u003c/strong\u003e\n\n\n### genRegister\n\u003e 生成模型注册/消费工具。主要是用于解耦直接 import 模型。\n\n```typescript\nimport { FemoModel } from 'femo';\n\ninterface GlobalModel {\n  name: FemoModel\u003cstring\u003e;\n  age: FemoModel\u003cnumber\u003e;\n  family: FemoModel\u003c{ count: number }\u003e\n}\nconst { register, unregister, pick, useRegister, usePick } = genRegister\u003cGlobalModel\u003e();\n\nconst name = glue('小明');\nconst age = glue(0);\nconst family = glue({\n  count: 3,\n});\n\nregister('name', name);\nregister('age', age);\nregister('family', family);\n\nconst nameModel = pick('name');\nconst ageModel = pick('age');\nconst familyModel = pick('family');\n\nunregister('name');\nunregister('age', age);\nunregister('famliy');\n\n// name === nameModel -\u003e true\n// age === ageModel -\u003e true\n// family === familyModel -\u003e true\n\n// react hook\nuseRegister('name', name);\nuseRegister('age', age);\nuseRegister('family', family);\n\nconst nameModel_1 = usePick('name');\nconst ageModel_1 = usePick('age');\nconst familyModel_1 = usePick('family');\n\n// name === nameModel_1 -\u003e true\n// age === ageModel_1 -\u003e true\n// family === familyModel_1 -\u003e true\n\n\n// 方法详细说明\n/**\n * register 注册 key/model，无返回\n * pick 获取 key 对应的 model\n * unregister 注销 key ；如果传入了 model，则需要 key 和 model 都匹配才会注销\n * useRegister 注册 key/model 的 hook，无返回。如果传入的 key 或者 model 发生变化，会先注销之前的 key，然后再注册 key；组件卸载时会注销 key\n * usePick 获取 key 赌赢的 model 的 hook\n */\n\n```\n\n\n\n### 节点方法\n\n- \u003ca href=\"#watch\"\u003ewatch（原来的relyOn）\u003c/a\u003e\n- \u003ca href=\"#onChange\"\u003eonChange\u003c/a\u003e\n- \u003ca href=\"#silent\"\u003esilent\u003c/a\u003e\n- \u003ca href=\"#race\"\u003erace\u003c/a\u003e\n- \u003ca href=\"#config\"\u003econfig\u003c/a\u003e\n\n### \u003cspan id=\"watch\"\u003ewatch\u003c/span\u003e\n\u003e 声明节点的依赖，并注册回调\n\n适用的场景：多个数据节点的变化都可引起一个数据节点更新，多对一的关系。\n\n```javascript\nconst demo1 = glue(null);\nconst demo2 = glue(null);\nconst demo3 = glue(null);\nconst demo = glue(null);\nconst unsubscribe = demo.watch([demo1, demo2, demo3], (data, state) =\u003e {\n  // data[0] 为 demo1的值\n  // data[1] 为 demo2的值\n  // data[2] 为 demo3的值\n  // state 为 demo的值\n  // 需要返回demo的最新值\n  const newState = { ...state };\n  return newState;\n});\n// 解除依赖\nunsubscribe();\n```\n\n定义节点之间的单向依赖关系。\n\nmodel.watch(models, callback)\n入参返回如下：\n\n| 入参           | 含义                                                                                |\n|:-------------|:----------------------------------------------------------------------------------|\n| models(必填)   | 模型数组，定义依赖的模型。放置的顺序会直接影响取值顺序                                                       |\n| callback(必填) | 回调函数，形如(data, state) =\u003e state。data是模型值的数组，与模型数组一一对应。state 是当前模型的值。回调函数需要返回当前模型的新值 |\n\nwatch处理数据依赖更新是单向的。通常情况下适合处理结构上没有嵌套的彼此独立的模型。\n\n需要注意的是，如果是要处理数据的双向依赖，比如：\n\n```javascript\nconst a = glue('');\nconst b = glue('');\na.watch([b], (list, state) =\u003e {\n  // todo\n});\nb.watch([a], (list, state) =\u003e {\n  // todo\n})\n```\n\n### \u003cspan id=\"#onChange\"\u003eonChange\u003c/span\u003e\n\n节点数据发生变化时会执行通过该方法传入的回调函数\n\n| 入参 | 含义 |\n| :---- | :---- |\n| callback函数(必填) | 节点数据发生变化时会执行的回调 |\n\n```javascript\nconst model = glue('');\nconst unsubscribe = model.onChange((state) =\u003e {\n  console.log(state)\n});\n// 解除变化监听\nunsubscribe();\n\n```\n\n这个方法用于需要节点主动向外发布数据的场景。\n\n### \u003cspan id=\"silent\"\u003esilent\u003c/span\u003e\n\u003e 静默地更新数据节点的内容\n\n该方法和直接使用节点更新内容一样，只是不会进行数据更新的广播，订阅了该数据的回调函数或者组件不会在此次更行中被执行或者重新渲染。\n在需要优化组件渲染频率的时候可以考虑使用它。\n\n### \u003cspan id=\"race\"\u003erace\u003c/span\u003e\n\u003e 处理数据节点更新出现的竞争问题\n\n简化上面\u003ca href=\"#genRaceQueue\"\u003egenRaceQueue\u003c/a\u003e的例子\n```js\n// p1请求\nsomeModel.race(params, async (state, data) =\u003e {\n  return await fetchRemote(data);\n});\n// p2请求\nsomeModel.race(async (state, data) =\u003e { return await fetchRemote() })\n```\n\n### \u003cspan id=\"config\"\u003econfig\u003c/span\u003e\n\u003e 节点配置方法\n\n```js\nsomeModel.config({\n  updatePolicy: 'merge', // 数据更新策略：merge 表示合并，replace 表示替换，默认为 replace\n})\n```\n\n\n## 搭配React\n\n### \u003cspan href=\"#react-hook\"\u003ereact hook\u003c/a\u003e\n\n- \u003ca href=\"#useModel\"\u003euseModel\u003c/a\u003e\n- \u003ca href=\"#useIndividualModel\"\u003e(废弃)~~useIndividualModel~~\u003c/a\u003e 请使用 useModel 代替\n- \u003ca href=\"#useDerivedState\"\u003euseDerivedState\u003c/a\u003e\n- \u003ca href=\"#useDerivedModel\"\u003euseDerivedModel\u003c/a\u003e\n- \u003ca href=\"#useBatchDerivedModel\"\u003euseBatchDerivedModel\u003c/a\u003e\n- \u003ca href=\"#useUpdateEffect\"\u003e(废弃)~~useLight~~\u003c/a\u003e 请使用 useSkipOnce 代替\n- \u003ca href=\"#useUpdateEffect\"\u003e(废弃)~~useSkipOnce~~\u003c/a\u003e 请使用 useUpdateEffect 代替\n- \u003ca href=\"#useUpdateEffect\"\u003euseUpdateEffect\u003c/a\u003e\n- \u003ca href=\"#useLocalService\"\u003euseLocalService\u003c/a\u003e\n\n\n## \u003cspan id=\"useModel\"\u003euseModel\u003c/span\u003e\n\u003e 自定义hook，用于消费节点数据\n\n用react hook的方式订阅并获取数据节点的内容\n\nconst [state, stateModelWithStatus, { service, loading, successful, error }] = useModel(state, service, deps, options);\n\n| 入参                                 | 含义                                                                               |\n|:-----------------------------------|:---------------------------------------------------------------------------------|\n| state(必传)                          | glue定义的模型 或者 S / () =\u003e S                                                        |\n| service(可选)                        | 形如: (state: S, params?: any, index?: number[]) =\u003e S \\ Promise\\\u003cS\u003e                |\n| deps(可选)                           | 依赖数组，如有变化会去执行service更新model数据                                                    |        \n| \u003ca href=\"#options\"\u003eoptions(可选)\u003c/a\u003e | 一些配置                                                                             |\n\n| 返回                   | 含义                                                                                                                                                                                                                                                                                                                                                                                               |\n|:---------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| state                | 数据                                                                                                                                                                                                                                                                                                                                                                                               |\n| stateModelWithStatus | 数据模型，和入参的 model 不一样，stateModelWithStatus 绑定了 loading、successful、error 等状态，即 stateModelWithStatus 进行异步更新时会改变这些状态                                                                                                                                                                                                                                                                                  |\n| status               | 形如 { service, loading, successful, error }。loading、successful、error 都是异步更新的状态；这里的 service 和 入参 service 在主要功能上是等效的，返回的 service 底层也是调用了入参 service。\u003cbr/\u003e 二者的区别在于：\u003cbr/\u003e 1. 返回的 service 入参最多只有一个，并且和作为入参的 service 的第二个参数等同（等同的意思是：二者是同一个，并且该参数最终可使用的地方是在作为入参的 service 里面）；\u003cbr/\u003e 2. 返回的 service 和 state 以及 loading、successful、error 等状态进行了绑定，返回的 service 进行调用调用会影响到这些状态（其中异步的更新会影响所有状态，同步更新只会影响 state） |\n\n```typescript\n\ninterface List {\n  page: number;\n  size: number;\n  list: any[];\n}\n// 定义一个节点\nconst listModel = glue\u003cList\u003e({ page: 1, size: 20, total: 0, list: [] });\n\nconst [query] = useState({\n  pageIndex: 1,\n  pageSize: 20,\n});\n\nconst getList = (state, params, index) =\u003e {\n  console.log('state', state);\n  console.log('params', params);\n  console.log('index', index);\n  // 除了query作为入参来源，还可进行手动传入入参 params\n  // 整合 query 和 params 可以根据场景来，这里做了简单的覆盖合并\n  return get('/api/list', {\n    ...query,\n    ...params,\n  }).then((res) =\u003e res.data);\n};\n\n// 监听 query 变化更新 listData\nconst [listData, listModelWithStatus, { service, loading, successful, error }] = useModel(listModel, getList, [query], {\n  suspense: {\n    key: 'list',\n  },\n});\n\n// 需要手动触发更新 listData\nconst onClick = () =\u003e {\n  service({\n    pageIndex: 2\n  })\n}\n\n```\n\n## (废弃)~~\u003cspan id=\"useIndividualModel\"\u003euseIndividualModel\u003c/span\u003e~~ (请使用 useModel 代替)\n\u003e 和useModel类似，只是不再依赖外部传入model，而是内部生成一个跟随组件生命周期的model。\n\n\n const [state, stateModelWithStatus, { service, loading, successful, error }] = useIndividualModel(initState, service, deps, options)\n\n| 入参                                 | 含义                                                                             |\n|:-----------------------------------|:-------------------------------------------------------------------------------|\n| initState(必传)                      | 可为函数， S / () =\u003e S                                                              |\n| service(可选)                        | 用于更新model的函数，形如 (state: S, params?: any, index?: number[]) =\u003e S / Promise\\\u003cS\u003e; |\n| deps(可选)                           | 依赖数组，更新会驱动service更新model                                                       |\n| \u003ca href=\"#options\"\u003eoptions(可选)\u003c/a\u003e | 一些配置                                                                           |\n\n\n| 返回                   | 含义                                                                                                                                                                                                                                                                                                                                                                                               |\n|:---------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| state                | 数据                                                                                                                                                                                                                                                                                                                                                                                               |\n| stateModelWithStatus | 数据模型，能改变 state 的值。stateModelWithStatus 绑定了 loading、successful、error 等状态，即 stateModelWithStatus 进行异步更新时会改变这些状态                                                                                                                                                                                                                                                            |\n| status               | 形如 { service, loading, successful, error }。loading、successful、error 都是异步更新的状态；这里的 service 和 入参 service 在主要功能上是等效的，返回的 service 底层也是调用了入参 service。\u003cbr/\u003e 二者的区别在于：\u003cbr/\u003e 1. 返回的 service 入参最多只有一个，并且和作为入参的 service 的第二个参数等同（等同的意思是：二者是同一个，并且该参数最终可使用的地方是在作为入参的 service 里面）；\u003cbr/\u003e 2. 返回的 service 和 state 以及 loading、successful、error 等状态进行了绑定，返回的 service 进行调用调用会影响到这些状态（其中异步的更新会影响所有状态，同步更新只会影响 state） |\n\n```typescript\nconst [query] = useState({\n  pageIndex: 1,\n  pageSize: 20,\n});\n\nconst getList = (state, params, index) =\u003e {\n  console.log('state', state);\n  console.log('params', params);\n  console.log('index', index);\n  // 除了query作为入参来源，还可进行手动传入入参 params\n  // 整合 query 和 params 可以根据场景来，这里做了简单的覆盖合并\n  return get('/api/list', {\n    ...query,\n    ...params,\n  }).then((res) =\u003e res.data);\n};\n\n// 监听 query 变化更新 listData\nconst [listData, listModelWithStatus, { service, loading, successful, error }] = useIndividualModel({\n  page: 1,\n  size: 20,\n  list: [],\n}, getList, [query], {\n  suspense: {\n    key: 'list',\n  }\n});\n\n// 需要手动触发更新 listData\nconst onClick = () =\u003e {\n  service({\n    pageIndex: 2\n  })\n}\n\n```\n\n## 处理衍生数据\n\n### 比较逻辑由hook处理，类似useEffect\n### \u003cspan id=\"useDerivedState\"\u003euseDerivedState\u003c/span\u003e\n\u003e 生成衍生数据，并返回model。区别于 useDerivedModel、useBatchDerivedModel，其依赖是个数组，处理更像useEffect\n\n依赖中可以有model，会监听model的变化。\n\nuseDerivedState(initState, callback, deps)\n或者\nuseDerivedState(callback, deps) // 此时callback充当initState，并且承担依赖变化更新model的职责\n\n| 入参        | 含义                                        |\n|:----------|:------------------------------------------|\n| initState | S \\ () =\u003e S                               |\n| callback  | (state: S) =\u003e S。更新model的函数，还可以充当initState |\n| deps      | 依赖数组                                      |\n```javascript\nconst { count } = props;\n\nconst [value, valueModelWithStatus, { loading, successful, error }] = useDerivedState(count, (s: number) =\u003e count, [count]);\n\n// 其实可以简写为\nconst [value, valueModelWithStatus, { loading, successful, error }] = useDerivedState((s: number) =\u003e count, [count]);\n\n```\n\n### 比较逻辑由用户代码处理，类似类组件中的getDerivedStateFromProps\n### \u003cspan id=\"useDerivedModel\"\u003euseDerivedModel\u003c/span\u003e\n\u003e 将依据其他数据产生的衍生数据更新到model中去，统一使用model的数据\n\u003e 和react组件中[getDerivedStateFromProps](https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops) 功能一致。\n\u003e 更具泛用性，不仅限于props，而是一切被依赖的数据都可以通过这个方法来处理衍生数据\n\nuseDerivedModel(initState, source, callback)\n\n| 入参        | 含义                                                                         |\n|:----------|:---------------------------------------------------------------------------|\n| initState | 初始值，形如: S \\ () =\u003e S                                                        |\n| source    | 衍生来源                                                                       |\n| callback  | 形如：(nextSource, prevSource, state: S) =\u003e S，根据前后两次记录的衍生来源，结合当前state，更新model |\n```javascript\n\nconst [value, valueModelWithStatus, {  loading, successful, error }] = useDerivedModel(props.defaultValue ?? 0, props, (nextSource, prevSource, state) =\u003e {\n  if (nextSource !== prevSource) {\n    if ('value' in nextSource) {\n      return nextSource.value;\n    }\n  } \n  return state;\n})\n\n```\n\n### \u003cspan id=\"useBatchDerivedModel\"\u003euseBatchDerivedModel\u003c/span\u003e\n\u003e useDerivedModel只能处理单一的衍生来源，useBatchDerivedModel则可以处理任意多衍生来源\n\nuseBatchDerivedModel(initState, {\n    source: source_1,\n    callback: (nextSource, prevSource, state, )\n})\n\n### \u003cspan id=\"useUpdateEffect\"\u003euseUpdateEffect\u003c/span\u003e\n\u003e ⚠️ 首次挂载并不会执行 callback，首次之后如果 deps 变了就会执行\n\nuseUpdateEffect(callback, deps);\n\n```typescript\n// 如果传入的是空数组依赖，则 callback 永远不会执行\nuseUpdateEffect(() =\u003e {\n  console.log('1');\n}, []);\n\nconst [count, updateCount] = useState(0);\n\n// 组件首次挂载时并不会执行 callback\n// 首次挂载后，后续 count 变化会引起 callback 执行\nuseUpdateEffect(() =\u003e {\n  console.log(count);\n}, [count]);\n\n\n```\n\n### \u003cspan id=\"useLocalService\"\u003euseLocalService\u003c/span\u003e\n\u003e 对 useModel 和 useIndividualModel 返回的 service 进行本地封装（本地是指以组件为单位）\n\n进行本地封装的目的是：拥有本地的异步状态 loading、successful、error 等，数据和请求还是共享的。因为有时我们需要一个请求在多个地方发送，并且这多个地方数据也是共享同一份，但是这些地方又有自己的loading等状态。\n\nconst [localService, { loading, successful, error }] = useLocalService(service, { bubble: false });\n\n| 入参          | 含义                                                                                                                                                                                                  |\n|:------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| service（必选） | 由 useModel 或者 useIndividualModel 返回的 service                                                                                                                                                        |\n| options（可选） | 形如 { bubble: boolean }，目前就一个属性 bubble。bubble 为 false(默认值) 表示只在当前组件产生异步状态的变化（loading、successful、error 等）；bubble 为 true，则表示除了当前组件的异步状态变化之外，传入的 service 所在的 useModel 或者 useIndividualModel 的异步状态也会同步变化 |\n\n| 返回           | 含义                                                      |\n|:-------------|:--------------------------------------------------------|\n| localService | 对入参 service 进行了一层包装。localService 的入参和返回都和传入的 service 一致 |\n| status       | 形如 { loading， successful, error }                       |\n\n```tsx\nconst LoadMore = (props) =\u003e {\n  const { service } = props;\n  \n  const [localService, { loading }] = useLocalService(service);\n  \n  const onClick = () =\u003e {\n    localService();\n  }\n  return (\n    // 这里点击过后，当前组件的 loading 会变化，组件 List 的不会\n    // 列表数据的更新还是在组件 List 中 \n    \u003cButton onClick={onClick} loading={loading}\u003e点击加载更多\u003c/Button\u003e\n  )\n}\n\n\nconst List = () =\u003e {\n  const [query] = useState({\n    pageIndex: 1,\n    pageSize: 20,\n  });\n\n  const getList = (state, params, index) =\u003e {\n    console.log('state', state);\n    console.log('params', params);\n    console.log('index', index);\n    // 除了query作为入参来源，还可进行手动传入入参 params\n    // 整合 query 和 params 可以根据场景来，这里做了简单的覆盖合并\n    return get('/api/list', {\n      ...query,\n      ...params,\n    }).then((res) =\u003e res.data);\n  };\n\n  // 监听 query 变化更新 listData\n  const [listData, listModelWithStatus, { service, loading, successful, error }] = useIndividualModel({\n    page: 1,\n    size: 20,\n    list: [],\n  }, getList, [query], {\n    suspense: {\n      key: 'list',\n    }\n  });\n  return (\n    \u003csection\u003e\n      \u003cTable dataSource={listData} /\u003e\n      \u003cLoadMore service={service} /\u003e\n    \u003c/section\u003e\n  )\n}\n\n```\n\n### \u003cspan href=\"#HOC\"\u003eHOC\u003c/a\u003e\n\n- \u003ca href=\"#Inject\"\u003eInject\u003c/a\u003e\n\n### \u003cspan id=\"Inject\"\u003eInject\u003c/a\u003e\n\nInject会向组件注入一些属性：\n\n| 属性名 | 含义 |\n| :----  | :----  |\n| suspenseKeys | 一组唯一的key。类型为string[]。用于\u003ca href=\"#options\"\u003eoptions\u003c/a\u003e中的suspenseKey，保证suspenseKey的唯一性。 |\n\n## 补充说明\n\n### \u003cspan id='options'\u003eoptions\u003c/a\u003e\n\n#### suspense\n```typescript\nexport interface SuspenseOptions {\n  key: string; // 等同于suspenseKey，唯一，一旦确定就不要变动，否则会有意外\n  persist?: boolean; // 默认false。false：只在第一次渲染时使用suspense能力；true：一直使用suspense能力\n}\n```\n\n#### onChange\n\n形如 (nextState, prevState) =\u003e void \n\n当数据发生变更时向外发布信息。\n\n#### onUpdate\n\n形如 (nextState, prevState) =\u003e void\n\n不管数据有没有变更（nextState 和 prevState 可能一样），只要执行了更新动作都会触发。\n\n\n#### control\n\n\u003e GlueReturn\u003c{ loading: boolean; successful: boolean; error?: any; key?: string; data?: any; }\u003e\n\n\n必须是由glue定义的model。用来控制 useModel 和 useIndividualModel 返回的status，以及在首次组件渲染禁止调用service。\n\n其中key是control的标识，消费control的业务代码可以根据key值来决定是否使用control的数据和状态。\n\n需要说明的是：如果传入了control model，组件首次渲染时不会调用service；control model会一直控制useModel和useIndividualModel\n返回的status，直到调用service进行了一次异步更新(注意是异步更新，同步更新不会解除control model的控制)。\n\n#### autoLoad\nbool 类型，默认为 true；如果设置为 false，service 不会自动执行\n\n```typescript\nconst [state] = useModel(initState, service, deps, {\n  autoLoad: false,\n})\n```\n\n### 循环依赖\n\n一旦发现在模型的调用链中出现了循环，会在那个点终止，在代码层面表现为直接返回。终止点不会执行更新逻辑，终止以前的调用不受影响。\n模型在异步回调函数中的每一次调用都会被视为一次调用链的起始。也就是在说异步回调进行模型调用更新，不会记录之前的调用栈。\n\n\n## 类型支持\n\n⚡️强烈建议使用typescript\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzhouyk%2Ffemo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzhouyk%2Ffemo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzhouyk%2Ffemo/lists"}