├── .coveralls.yml ├── .dawn ├── pipe.yml └── rc.yml ├── .eslintrc ├── .gitignore ├── .npmignore ├── .npmrc ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── assets ├── benchmark-mobx.html ├── benchmark-mota-old.html ├── benchmark-mota.html ├── benchmark-normal.html ├── benchmark-redux.html └── develop.html ├── design ├── logo.jpg ├── logo.png └── logo.psd ├── docs └── logo.jpg ├── examples ├── benchmark-mobx.tsx ├── benchmark-mota-old.tsx ├── benchmark-mota.tsx ├── benchmark-normal.tsx ├── benchmark-redux.tsx ├── data.ts ├── develop.tsx ├── fix-require-error.ts ├── package-lock.json ├── package.json └── tsconfig.json ├── markdowns ├── autorun.md ├── binding.md ├── hook_model.md ├── mapping.md ├── model.md ├── quick.md ├── typescript.md ├── use_model.md └── watch.md ├── package.json ├── rollup.config.dev.js ├── rollup.config.js ├── scripts └── info.js ├── server.yml ├── src ├── batch.ts ├── hooks.ts ├── index.ts ├── info.ts ├── input.ts ├── observer.ts ├── server.ts ├── sync.ts └── util.ts ├── test ├── helpers │ └── renderer.ts ├── hooks.test.tsx ├── input.test.tsx ├── observer.test.tsx └── tsconfig.json ├── tsconfig.dev.json └── tsconfig.json /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: jjHvZXVmGrgNYcbISEfnWpfNY64jLj9ux -------------------------------------------------------------------------------- /.dawn/pipe.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - name: pkginfo 3 | 4 | dev: 5 | - name: clean 6 | target: 7 | - ./dist/ 8 | - ./types/ 9 | - name: copy 10 | files: 11 | ./dist/(0).html: ./assets/*.html 12 | - name: shell 13 | async: true 14 | script: 15 | - rollup -w -c ./rollup.config.dev.js 16 | - name: server 17 | public: ./dist/ 18 | port: 6008 19 | 20 | build: 21 | - name: clean 22 | target: 23 | - ./dist/ 24 | - ./types/ 25 | - name: ./scripts/info.js 26 | - name: file-header 27 | files: ./src/**/*.ts 28 | text: 29 | - 'Copyright (c) 2015-present Houfeng' 30 | - '@homepage https://github.com/Houfeng/mota' 31 | - '@author Houfeng ' 32 | - name: shell 33 | script: 34 | - rollup -c 35 | - name: copy 36 | files: 37 | ./docs/logo.jpg: ./design/logo.jpg 38 | 39 | test: 40 | - name: shell 41 | script: 42 | - eslint --fix ./src/**/*.ts 43 | - name: unit 44 | env: browser_typescript 45 | extension: .ts 46 | files: ./test/**/*.test.tsx 47 | 48 | publish: 49 | - name: version 50 | - name: shell 51 | script: 52 | - dn test 53 | - dn build 54 | - npm pu --registry=https://registry.npmjs.org/ 55 | - name: submitter 56 | -------------------------------------------------------------------------------- /.dawn/rc.yml: -------------------------------------------------------------------------------- 1 | server: https://query.aliyun.com/rest/alidocs.ds?conf= 2 | registry: https://registry.npmmirror.com -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": [ 4 | "@typescript-eslint", 5 | "prettier" 6 | ], 7 | "env": { 8 | "browser": true, 9 | "node": true 10 | }, 11 | "globals": { 12 | "Set": "readonly" 13 | }, 14 | "extends": [ 15 | "eslint:recommended", 16 | "plugin:@typescript-eslint/recommended" 17 | ], 18 | "rules": { 19 | "@typescript-eslint/no-explicit-any": 0, 20 | "@typescript-eslint/explicit-function-return-type": 0, 21 | "@typescript-eslint/no-non-null-assertion": 0, 22 | "@typescript-eslint/ban-ts-comment": 0, 23 | "prettier/prettier": "error" 24 | } 25 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules/ 4 | .nyc_output/ 5 | coverage/ 6 | debug/ 7 | dist/ 8 | types/ 9 | logs/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules/ 4 | .nyc_output/ 5 | coverage/ 6 | debug/ 7 | src/ 8 | assets/ 9 | test/ 10 | examples/ 11 | example/ 12 | tslint.json 13 | tsconfig.json 14 | jsconfig.json 15 | .eslintrc.yml 16 | server.yml 17 | .dawn/ 18 | *.js.map 19 | docs/ 20 | rollup.config.js 21 | rollup.config.dev.js 22 | design/ 23 | scripts/ 24 | markdowns/ 25 | .coveralls.yml 26 | .travis.yml 27 | doczilla.yml 28 | CODE_OF_CONDUCT.md 29 | CONTRIBUTING.md 30 | ISSUE_TEMPLATE.md -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com 2 | package-lock=false 3 | save-exact=true 4 | legacy-peer-deps=true 5 | engine-strict=false 6 | ignore-scripts=true -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10.10.0" -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "debug/test", 9 | "type": "node", 10 | "request": "launch", 11 | "program": "${workspaceRoot}/node_modules/.bin/mocha", 12 | "args": [ 13 | "-r", 14 | "${workspaceRoot}/node_modules/dn-middleware-unit/lib/setups/browser_typescript", 15 | "${relativeFile}" 16 | ], 17 | "runtimeArgs": [ 18 | "--nolazy" 19 | ], 20 | "cwd": "${workspaceRoot}", 21 | "sourceMaps": true, 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Feng", 4 | "immer", 5 | "lcov", 6 | "lifecycles", 7 | "reactivable", 8 | "Reactiver", 9 | "reduxjs" 10 | ] 11 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at admin@xhou.net. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | fork -> codeing -> push -> pr 2 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 系统及版本 2 | 3 | ## 浏览器及版本 4 | 5 | ## React 版本 6 | 7 | ## ReactDOM 版本 8 | 9 | ## 问题描述 10 | 11 | ## 重现步骤 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present Houfeng (admin@xhou.net) 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](http://houfeng.net/mota/logo.jpg) 2 | 3 |
4 | 5 | [![npm](https://img.shields.io/npm/l/mota.svg)](LICENSE.md) 6 | [![NPM Version](https://img.shields.io/npm/v/mota.svg)](https://www.npmjs.com/package/mota) 7 | [![Coverage Status](https://coveralls.io/repos/github/Houfeng/mota/badge.svg?branch=master)](https://coveralls.io/github/Houfeng/mota?branch=master) 8 | [![npm](https://img.shields.io/npm/dt/mota.svg)](https://www.npmjs.com/package/mota) 9 | 10 | 11 |
12 | 13 | # Overview 14 | 15 | Mota is a "lightweight and responsive" state management library, which is less than 5KB. Developers can use it to write "almost framework independent pure js/ts business models", and then use Mota to simply let react components respond to model changes. 16 | 17 | # Install 18 | 19 | Install through NPM as follows 20 | ```sh 21 | $ npm install mota --save 22 | ``` 23 | 24 | # Usage 25 | 26 | **Example 1**: Hello Mota 27 | 28 | ```jsx 29 | import { observable, observer } from "mota"; 30 | 31 | const model = observable({ count: 0 }); 32 | const add = ()=>model.count++; 33 | 34 | const View1 = observer(() => { 35 | return
{model.count}
; 36 | }); 37 | 38 | const View2 = observer(() => { 39 | return
40 | {model.count} 41 |
; 43 | }); 44 | ``` 45 | 46 | **Example 2**: Using useObservable 47 | 48 | ```jsx 49 | import { observer, useObservable } from "mota"; 50 | 51 | const View = observer(() => { 52 | const model = useObservable({ count: 0 }); 53 | return
54 | {model.count} 55 |
; 57 | }); 58 | ``` 59 | 60 | **Example 3**: Using useComputed 61 | 62 | ```jsx 63 | import { observer, observable, useComputed } from "mota"; 64 | 65 | const user = observable({ 66 | firstName: 'Feng', 67 | lastName: 'Hou' 68 | }); 69 | 70 | const View = observer(() => { 71 | // The fullName will be cached and responsive 72 | const fullName = useComputed(()=>{ 73 | return `${user.firstName} ${user.lastName}`; 74 | }); 75 | return
{fullName}
; 76 | }); 77 | ``` 78 | 79 | 80 | **Example 4**: Using useAutoRun 81 | 82 | ```jsx 83 | import { observer, observable, useAutoRun } from "mota"; 84 | 85 | const model = observable({ count: 0 }); 86 | 87 | const View = observer(() => { 88 | // When the count changes, 89 | // it will be automatically re executed and printed 'count: n' 90 | useAutoRun(()=>{ 91 | console.log('count:', model.count); 92 | }); 93 | return
{model.count}
; 94 | }); 95 | ``` 96 | 97 | **Example 5**: Using useWatch 98 | 99 | ```jsx 100 | import { observer, observable, useWatch } from "mota"; 101 | 102 | const model = observable({ count: 0 }); 103 | 104 | const View = observer(() => { 105 | // When the result of the evaluation function changes, 106 | // the processing function is re executed. 107 | useWatch(()=>model.count%10, (oldValue, newValue)=>{ 108 | console.log(`old: ${oldValue}, new: ${newValue}`); 109 | }); 110 | return
{model.count}
; 111 | }); 112 | ``` 113 | 114 | **Example 6**: Using in class components 115 | 116 | ```jsx 117 | import { observer, observable, autorun, watch } from "mota"; 118 | 119 | const model = observable({ count: 0 }); 120 | 121 | @observer 122 | class View extends React.Component { 123 | add = ()=> model.count++; 124 | 125 | componentDidMount(){ 126 | this.destroyAutoRun = autorun(()=>{ 127 | console.log('autorun count: ', model.count); 128 | }); 129 | this.destroyWatch = watch(()=> model.count%10, ()=>{ 130 | console.log('autorun count: ', model.count); 131 | }); 132 | } 133 | 134 | componentWillUnmount(){ 135 | this.destroyAutoRun(); 136 | this.destroyWatch(); 137 | } 138 | 139 | @computed get message(){ 140 | return `count: ${model.count}`; 141 | } 142 | 143 | render() { 144 | return
145 | {this.message} 146 |
; 148 | } 149 | } 150 | ``` 151 | 152 | **Example 7**: Using multiple instances in class components 153 | 154 | ```jsx 155 | import { observer, observable, autorun, watch } from "mota"; 156 | 157 | @observable 158 | class Model { 159 | count = 0; 160 | add () { 161 | this.count++; 162 | } 163 | @computed get message(){ 164 | return `count: ${model.count}`; 165 | } 166 | } 167 | 168 | @observer 169 | class View extends React.Component { 170 | model = new Model(); 171 | render() { 172 | return
173 | {this.model.message} 174 |
; 176 | } 177 | } 178 | ``` -------------------------------------------------------------------------------- /assets/benchmark-mobx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Mota Benchmark 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /assets/benchmark-mota-old.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Mota Benchmark 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /assets/benchmark-mota.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Mota Benchmark 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /assets/benchmark-normal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Mota Benchmark 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /assets/benchmark-redux.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Mota Benchmark 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /assets/develop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Mota Develop 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /design/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/mota/fc55dbd5fce548b11f296aaa39af2e6752bcb59e/design/logo.jpg -------------------------------------------------------------------------------- /design/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/mota/fc55dbd5fce548b11f296aaa39af2e6752bcb59e/design/logo.png -------------------------------------------------------------------------------- /design/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/mota/fc55dbd5fce548b11f296aaa39af2e6752bcb59e/design/logo.psd -------------------------------------------------------------------------------- /docs/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Houfeng/mota/fc55dbd5fce548b11f296aaa39af2e6752bcb59e/docs/logo.jpg -------------------------------------------------------------------------------- /examples/benchmark-mobx.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode, useEffect } from 'react'; 2 | 3 | import { createRoot } from 'react-dom/client'; 4 | import { list } from './data'; 5 | import { observable } from "mobx"; 6 | import { observer } from "mobx-react"; 7 | 8 | let renderCount = 0; 9 | 10 | const markRender = () => { 11 | renderCount++; 12 | if (renderCount >= list.length) { 13 | console.timeEnd('time'); 14 | } 15 | } 16 | 17 | 18 | const model = observable({ 19 | counter: { count: 0 } 20 | }); 21 | 22 | const increment = () => { 23 | console.time('time'); 24 | renderCount = 0; 25 | model.counter.count++; 26 | model.counter.count++; 27 | } 28 | 29 | const itemStyle = { 30 | padding: 4, margin: 2, background: '#eee', display: 'inline-block', 31 | } 32 | 33 | const Item = observer(function Item() { 34 | const { count } = model.counter; 35 | useEffect(() => markRender(), [count]); 36 | return ( 37 | {count} 38 | ) 39 | }); 40 | 41 | const App = () => { 42 | return ( 43 | 44 | 45 |
46 | {list.map((_, index) => )} 47 |
48 |
49 | ) 50 | } 51 | 52 | const root = createRoot(document.getElementById('root')) 53 | root.render(); 54 | -------------------------------------------------------------------------------- /examples/benchmark-mota-old.tsx: -------------------------------------------------------------------------------- 1 | import "./fix-require-error"; 2 | 3 | import { ObserveConfig, observable, useModel } from "mota"; 4 | import React, { StrictMode, useEffect } from 'react'; 5 | 6 | import { createRoot } from 'react-dom/client'; 7 | import { list } from './data'; 8 | 9 | ObserveConfig.maxDependencies = Number.MAX_SAFE_INTEGER; 10 | ObserveConfig.maxHandlers = Number.MAX_SAFE_INTEGER; 11 | 12 | let renderCount = 0; 13 | 14 | const markRender = () => { 15 | renderCount++; 16 | if (renderCount >= list.length) { 17 | console.timeEnd('time'); 18 | } 19 | } 20 | 21 | const model = observable({ 22 | __displayName: 'model', 23 | count: 0, 24 | }); 25 | 26 | const increment = () => { 27 | console.time('time'); 28 | renderCount = 0; 29 | model.count++; 30 | } 31 | 32 | const itemStyle = { 33 | padding: 4, margin: 2, background: '#eee', display: 'inline-block', 34 | } 35 | 36 | const Item = (function Item() { 37 | const { count } = useModel(model); 38 | useEffect(() => markRender(), [count]); 39 | return ( 40 | {count} 41 | ) 42 | }); 43 | 44 | const App = () => { 45 | return ( 46 | 47 | 48 |
49 | {list.map((_, index) => )} 50 |
51 |
52 | ) 53 | } 54 | 55 | const root = createRoot(document.getElementById('root')) 56 | root.render(); -------------------------------------------------------------------------------- /examples/benchmark-mota.tsx: -------------------------------------------------------------------------------- 1 | import { ObserveConfig, observable, observer } from "../src"; 2 | import React, { Fragment, useEffect } from 'react'; 3 | 4 | import { createRoot } from 'react-dom/client'; 5 | import { list } from './data'; 6 | 7 | ObserveConfig.maxListeners = Number.MAX_SAFE_INTEGER; 8 | 9 | let renderCount = 0; 10 | 11 | const markRender = () => { 12 | renderCount++; 13 | if (renderCount >= list.length) { 14 | console.timeEnd('time'); 15 | } 16 | } 17 | 18 | const model = observable({ 19 | count: 0, 20 | }); 21 | 22 | const increment = () => { 23 | console.time('time'); 24 | renderCount = 0; 25 | model.count++; 26 | } 27 | 28 | const itemStyle = { 29 | padding: 4, margin: 2, background: '#eee', display: 'inline-block', 30 | } 31 | 32 | const Item = observer(function Item() { 33 | const { count } = model; 34 | useEffect(() => markRender(), [count]); 35 | return ( 36 | {count} 37 | ) 38 | }); 39 | 40 | const App = () => { 41 | return ( 42 | 43 | 44 |
45 | {list.map((_, index) => )} 46 |
47 |
48 | ) 49 | } 50 | 51 | const root = createRoot(document.getElementById('root')) 52 | root.render(); 53 | 54 | //@ts-ignore 55 | window.model = model; -------------------------------------------------------------------------------- /examples/benchmark-normal.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode, createContext, useCallback, useContext, useEffect, useState } from 'react'; 2 | 3 | import { createRoot } from 'react-dom/client'; 4 | import { list } from './data'; 5 | import { observable } from '../src'; 6 | 7 | let renderCount = 0; 8 | 9 | const markRender = () => { 10 | renderCount++; 11 | if (renderCount >= list.length) { 12 | console.timeEnd('time' ); 13 | } 14 | } 15 | 16 | const itemStyle = { 17 | padding: 4, margin: 2, background: '#eee', display: 'inline-block', 18 | } 19 | 20 | const context = createContext(0); 21 | 22 | export const model = observable({ 23 | count: 0, 24 | }); 25 | 26 | const Item = (() => { 27 | const count = useContext(context); 28 | useEffect(() => markRender(), [count]); 29 | model.count; 30 | return ( 31 | {count} 32 | ) 33 | }); 34 | 35 | const App = () => { 36 | const [count, setCount] = useState(0); 37 | const increment = useCallback(() => { 38 | console.time('time'); 39 | renderCount = 0; 40 | setCount(count + 1); 41 | }, [count, setCount]); 42 | return ( 43 | 44 | 45 | 46 |
47 | {list.map((_, index) => )} 48 |
49 |
50 |
51 | ) 52 | } 53 | 54 | const root = createRoot(document.getElementById('root')) 55 | root.render(); 56 | -------------------------------------------------------------------------------- /examples/benchmark-redux.tsx: -------------------------------------------------------------------------------- 1 | import { Provider, useDispatch, useSelector } from 'react-redux' 2 | import React, { Fragment, StrictMode, useCallback, useEffect } from 'react' 3 | import { configureStore, createSlice } from '@reduxjs/toolkit' 4 | 5 | import { createRoot } from 'react-dom/client' 6 | import { list } from './data'; 7 | 8 | let renderCount = 0; 9 | 10 | const markRender = () => { 11 | renderCount++; 12 | if (renderCount >= list.length) { 13 | console.timeEnd('time'); 14 | } 15 | } 16 | 17 | const counterSlice = createSlice({ 18 | name: 'counter', 19 | initialState: { 20 | value: 0, 21 | }, 22 | reducers: { 23 | increment: (state) => { 24 | console.time('time'); 25 | renderCount = 0; 26 | state.value += 1 27 | } 28 | }, 29 | }); 30 | 31 | const store = configureStore({ 32 | reducer: { 33 | counter: counterSlice.reducer 34 | }, 35 | }); 36 | 37 | 38 | const itemStyle = { 39 | padding: 4, margin: 2, background: '#eee', display: 'inline-block', 40 | } 41 | 42 | const Item = () => { 43 | const count = useSelector((state: any) => { 44 | return state.counter?.value; 45 | }) 46 | useEffect(() => markRender(), [count]); 47 | return ( 48 | {count} 49 | ) 50 | }; 51 | 52 | const App = () => { 53 | const dispatch = useDispatch(); 54 | const increment = useCallback(() => { 55 | dispatch(counterSlice.actions.increment()); 56 | }, [dispatch]); 57 | return ( 58 | 59 | 60 |
61 | {list.map((_, index) => )} 62 |
63 |
64 | ) 65 | } 66 | 67 | const root = createRoot(document.getElementById('root')) 68 | root.render( 69 | 70 | 71 | 72 | 73 | 74 | ); -------------------------------------------------------------------------------- /examples/data.ts: -------------------------------------------------------------------------------- 1 | export const list = new Array(20000).fill({}); -------------------------------------------------------------------------------- /examples/develop.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, memo, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'; 2 | import { computed, observable, observer, takeDependencies, useObservable, useWatch } from "../src"; 3 | 4 | import { createRoot } from 'react-dom/client'; 5 | 6 | const model = observable({ 7 | __displayName: 'model', 8 | error: null, 9 | name: 'test', 10 | num: 1, 11 | add() { 12 | this.num += 1; 13 | } 14 | }) 15 | 16 | @observable 17 | class User { 18 | firstName = "Feng"; 19 | lastName = "Hou"; 20 | age = 1; 21 | @computed 22 | get fullName() { 23 | return `${this.firstName} ${this.lastName}: ${this.age}`; 24 | } 25 | } 26 | 27 | const UserView = observer(function UserView() { 28 | const user = useObservable(() => new User()); 29 | return ( 30 |
31 | {user.fullName} 32 |
33 |
34 | ) 35 | }); 36 | 37 | export const Demo1 = observer(function Demo1() { 38 | takeDependencies("Demo1 依赖"); 39 | useWatch(() => model.num > 100, () => { 40 | console.log("num:", model.num); 41 | }); 42 | console.log('demo1 model.name', model.name); 43 | return ( 44 |
45 |

Demo1

46 |
47 | model.name = event.target.value} 50 | /> 51 |
{model.num}
52 |
{model.error?.message || 'NONE'}
53 |
54 |
55 | ) 56 | }); 57 | 58 | export const Demo2 = observer(function Demo2() { 59 | //@ts-ignore 60 | //const name = useDeferredValue(model.name, { timeoutMs: 1000 }); 61 | console.log('demo2 model.num', model.num); 62 | return ( 63 |
64 |

Demo2

65 |
name: {model.name}
66 |
model.add()}>num: {model.num}
67 |
68 | ) 69 | }); 70 | 71 | export const Demo4 = observer(function Demo4() { 72 | console.log('Render'); 73 | const store = useMemo(() => { 74 | console.log('useMemo callback'); 75 | return { value: 0 }; 76 | }, []) 77 | useSyncExternalStore(() => { 78 | console.log('useSyncExternalStore subscribe callback'); 79 | return () => { 80 | console.log('useSyncExternalStore unsubscribe callback'); 81 | } 82 | }, () => { 83 | console.log('useSyncExternalStore snapshot callback'); 84 | return store; 85 | }) 86 | if (store.value === 0) { 87 | console.log('useSyncExternalStore first'); 88 | store.value = 1; 89 | } 90 | useEffect(() => { 91 | console.log('useEffect mount callback'); 92 | return () => { 93 | console.log('useEffect unmount callback'); 94 | } 95 | }, []) 96 | const ref = useRef(0); 97 | if (ref.current === 0) { 98 | console.log('ref first'); 99 | ref.current = 1; 100 | } 101 | console.log('ref', ref.current); 102 | return ( 103 |
104 |

Demo4

105 |
model.num++}>count: {model.num}
106 |
107 | ) 108 | }); 109 | 110 | @observer 111 | export class Demo3 extends React.Component { 112 | state = { name: 'Demo3' }; 113 | render(): React.ReactNode { 114 | console.log('demo3 model.num', model.num); 115 | return ( 116 |
117 |

Demo3

118 |
name: {model.name}
119 |
model.add()}>num: {model.num}
120 | 121 |
122 | ) 123 | } 124 | } 125 | 126 | export const Demo3_1 = memo(observer(function Demo3_1() { 127 | return
128 |

Demo3_1

129 | {model.num} 130 |
131 | })); 132 | 133 | const App = () => { 134 | return ( 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | ) 143 | } 144 | 145 | const root = createRoot(document.getElementById('root')) 146 | root.render(); 147 | 148 | //@ts-ignore 149 | window.model = model; -------------------------------------------------------------------------------- /examples/fix-require-error.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | window.require = (name: string) => { 3 | if (name === 'react') name = 'React'; 4 | if (name === 'react-dom') name = 'ReactDOM'; 5 | // @ts-ignore 6 | return window[name]; 7 | }; 8 | // @ts-ignore 9 | window.global = window; -------------------------------------------------------------------------------- /examples/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mota-examples", 3 | "version": "0.1.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "mota-examples", 9 | "version": "0.1.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@reduxjs/toolkit": "^1.8.2", 13 | "mobx-react": "^7.5.0", 14 | "mota": "^7.3.8", 15 | "react-redux": "^8.0.2", 16 | "redux": "^4.2.0" 17 | } 18 | }, 19 | "node_modules/@babel/runtime": { 20 | "version": "7.18.3", 21 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.3.tgz", 22 | "integrity": "sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==", 23 | "dependencies": { 24 | "regenerator-runtime": "^0.13.4" 25 | }, 26 | "engines": { 27 | "node": ">=6.9.0" 28 | } 29 | }, 30 | "node_modules/@reduxjs/toolkit": { 31 | "version": "1.8.2", 32 | "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.2.tgz", 33 | "integrity": "sha512-CtPw5TkN1pHRigMFCOS/0qg3b/yfPV5qGCsltVnIz7bx4PKTJlGHYfIxm97qskLknMzuGfjExaYdXJ77QTL0vg==", 34 | "dependencies": { 35 | "immer": "^9.0.7", 36 | "redux": "^4.1.2", 37 | "redux-thunk": "^2.4.1", 38 | "reselect": "^4.1.5" 39 | }, 40 | "peerDependencies": { 41 | "react": "^16.9.0 || ^17.0.0 || ^18", 42 | "react-redux": "^7.2.1 || ^8.0.0-beta" 43 | }, 44 | "peerDependenciesMeta": { 45 | "react": { 46 | "optional": true 47 | }, 48 | "react-redux": { 49 | "optional": true 50 | } 51 | } 52 | }, 53 | "node_modules/@types/hoist-non-react-statics": { 54 | "version": "3.3.1", 55 | "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", 56 | "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", 57 | "dependencies": { 58 | "@types/react": "*", 59 | "hoist-non-react-statics": "^3.3.0" 60 | } 61 | }, 62 | "node_modules/@types/prop-types": { 63 | "version": "15.7.5", 64 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", 65 | "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" 66 | }, 67 | "node_modules/@types/react": { 68 | "version": "18.0.12", 69 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.12.tgz", 70 | "integrity": "sha512-duF1OTASSBQtcigUvhuiTB1Ya3OvSy+xORCiEf20H0P0lzx+/KeVsA99U5UjLXSbyo1DRJDlLKqTeM1ngosqtg==", 71 | "dependencies": { 72 | "@types/prop-types": "*", 73 | "@types/scheduler": "*", 74 | "csstype": "^3.0.2" 75 | } 76 | }, 77 | "node_modules/@types/react-dom": { 78 | "version": "18.0.5", 79 | "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.5.tgz", 80 | "integrity": "sha512-OWPWTUrY/NIrjsAPkAk1wW9LZeIjSvkXRhclsFO8CZcZGCOg2G0YZy4ft+rOyYxy8B7ui5iZzi9OkDebZ7/QSA==", 81 | "peer": true, 82 | "dependencies": { 83 | "@types/react": "*" 84 | } 85 | }, 86 | "node_modules/@types/scheduler": { 87 | "version": "0.16.2", 88 | "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", 89 | "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" 90 | }, 91 | "node_modules/@types/use-sync-external-store": { 92 | "version": "0.0.3", 93 | "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", 94 | "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" 95 | }, 96 | "node_modules/csstype": { 97 | "version": "3.1.0", 98 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", 99 | "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" 100 | }, 101 | "node_modules/hoist-non-react-statics": { 102 | "version": "3.3.2", 103 | "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", 104 | "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", 105 | "dependencies": { 106 | "react-is": "^16.7.0" 107 | } 108 | }, 109 | "node_modules/hoist-non-react-statics/node_modules/react-is": { 110 | "version": "16.13.1", 111 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", 112 | "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" 113 | }, 114 | "node_modules/immer": { 115 | "version": "9.0.15", 116 | "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz", 117 | "integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==", 118 | "funding": { 119 | "type": "opencollective", 120 | "url": "https://opencollective.com/immer" 121 | } 122 | }, 123 | "node_modules/js-tokens": { 124 | "version": "4.0.0", 125 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 126 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 127 | "peer": true 128 | }, 129 | "node_modules/loose-envify": { 130 | "version": "1.4.0", 131 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 132 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 133 | "peer": true, 134 | "dependencies": { 135 | "js-tokens": "^3.0.0 || ^4.0.0" 136 | }, 137 | "bin": { 138 | "loose-envify": "cli.js" 139 | } 140 | }, 141 | "node_modules/mobx": { 142 | "version": "6.6.0", 143 | "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.6.0.tgz", 144 | "integrity": "sha512-MNTKevLH/6DShLZcmSL351+JgiJPO56A4GUpoiDQ3/yZ0mAtclNLdHK9q4BcQhibx8/JSDupfTpbX2NZPemlRg==", 145 | "peer": true, 146 | "funding": { 147 | "type": "opencollective", 148 | "url": "https://opencollective.com/mobx" 149 | } 150 | }, 151 | "node_modules/mobx-react": { 152 | "version": "7.5.0", 153 | "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-7.5.0.tgz", 154 | "integrity": "sha512-riHu0XZJA6f64L1iXZoAaDjVt6suYoy8I2HIfuz2tX3O4FFaAe4lVA2CoObttmUQTTFPM7j3Df6T4re0cHkghQ==", 155 | "dependencies": { 156 | "mobx-react-lite": "^3.4.0" 157 | }, 158 | "funding": { 159 | "type": "opencollective", 160 | "url": "https://opencollective.com/mobx" 161 | }, 162 | "peerDependencies": { 163 | "mobx": "^6.1.0", 164 | "react": "^16.8.0 || ^17 || ^18" 165 | }, 166 | "peerDependenciesMeta": { 167 | "react-dom": { 168 | "optional": true 169 | }, 170 | "react-native": { 171 | "optional": true 172 | } 173 | } 174 | }, 175 | "node_modules/mobx-react-lite": { 176 | "version": "3.4.0", 177 | "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.4.0.tgz", 178 | "integrity": "sha512-bRuZp3C0itgLKHu/VNxi66DN/XVkQG7xtoBVWxpvC5FhAqbOCP21+nPhULjnzEqd7xBMybp6KwytdUpZKEgpIQ==", 179 | "funding": { 180 | "type": "opencollective", 181 | "url": "https://opencollective.com/mobx" 182 | }, 183 | "peerDependencies": { 184 | "mobx": "^6.1.0", 185 | "react": "^16.8.0 || ^17 || ^18" 186 | }, 187 | "peerDependenciesMeta": { 188 | "react-dom": { 189 | "optional": true 190 | }, 191 | "react-native": { 192 | "optional": true 193 | } 194 | } 195 | }, 196 | "node_modules/mota": { 197 | "version": "7.3.8", 198 | "resolved": "https://registry.npmjs.org/mota/-/mota-7.3.8.tgz", 199 | "integrity": "sha512-/6VegPGrnoHAXtS6ZLERNFuo8ikYJBkop2IMKFCcISysA1nn1y0HaffWG0u6C/0Fh2pN9AMT0PeIIlQkQin3ug==", 200 | "dependencies": { 201 | "ntils": "^5.2.0", 202 | "ober": "^6.5.0", 203 | "tslib": "*" 204 | }, 205 | "peerDependencies": { 206 | "@types/react": "*", 207 | "@types/react-dom": "*", 208 | "react": "*", 209 | "react-dom": "*" 210 | } 211 | }, 212 | "node_modules/ntils": { 213 | "version": "5.2.4", 214 | "resolved": "https://registry.npmjs.org/ntils/-/ntils-5.2.4.tgz", 215 | "integrity": "sha512-VUdpPYb0bvVpxkzFYx2o3I0VWIYtz+8VGlA55dlSTb7eInvWrazV+InQAM3aNfWkdLZL4LFay7eitUYGuZKhjQ==" 216 | }, 217 | "node_modules/ober": { 218 | "version": "6.5.0", 219 | "resolved": "https://registry.npmjs.org/ober/-/ober-6.5.0.tgz", 220 | "integrity": "sha512-N3HFtlJzmlplcK+BATNCU2TB81APqh0sktbgqDlkvi51rdLm7SCuicM2mzV6lRa2jxx3VKAtbmSuTGqZRwuw6w==", 221 | "dependencies": { 222 | "tslib": "*" 223 | } 224 | }, 225 | "node_modules/react": { 226 | "version": "18.1.0", 227 | "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", 228 | "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", 229 | "peer": true, 230 | "dependencies": { 231 | "loose-envify": "^1.1.0" 232 | }, 233 | "engines": { 234 | "node": ">=0.10.0" 235 | } 236 | }, 237 | "node_modules/react-dom": { 238 | "version": "18.1.0", 239 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", 240 | "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", 241 | "peer": true, 242 | "dependencies": { 243 | "loose-envify": "^1.1.0", 244 | "scheduler": "^0.22.0" 245 | }, 246 | "peerDependencies": { 247 | "react": "^18.1.0" 248 | } 249 | }, 250 | "node_modules/react-is": { 251 | "version": "18.2.0", 252 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", 253 | "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" 254 | }, 255 | "node_modules/react-redux": { 256 | "version": "8.0.2", 257 | "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.2.tgz", 258 | "integrity": "sha512-nBwiscMw3NoP59NFCXFf02f8xdo+vSHT/uZ1ldDwF7XaTpzm+Phk97VT4urYBl5TYAPNVaFm12UHAEyzkpNzRA==", 259 | "dependencies": { 260 | "@babel/runtime": "^7.12.1", 261 | "@types/hoist-non-react-statics": "^3.3.1", 262 | "@types/use-sync-external-store": "^0.0.3", 263 | "hoist-non-react-statics": "^3.3.2", 264 | "react-is": "^18.0.0", 265 | "use-sync-external-store": "^1.0.0" 266 | }, 267 | "peerDependencies": { 268 | "@types/react": "^16.8 || ^17.0 || ^18.0", 269 | "@types/react-dom": "^16.8 || ^17.0 || ^18.0", 270 | "react": "^16.8 || ^17.0 || ^18.0", 271 | "react-dom": "^16.8 || ^17.0 || ^18.0", 272 | "react-native": ">=0.59", 273 | "redux": "^4" 274 | }, 275 | "peerDependenciesMeta": { 276 | "@types/react": { 277 | "optional": true 278 | }, 279 | "@types/react-dom": { 280 | "optional": true 281 | }, 282 | "react-dom": { 283 | "optional": true 284 | }, 285 | "react-native": { 286 | "optional": true 287 | }, 288 | "redux": { 289 | "optional": true 290 | } 291 | } 292 | }, 293 | "node_modules/redux": { 294 | "version": "4.2.0", 295 | "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz", 296 | "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", 297 | "dependencies": { 298 | "@babel/runtime": "^7.9.2" 299 | } 300 | }, 301 | "node_modules/redux-thunk": { 302 | "version": "2.4.1", 303 | "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", 304 | "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", 305 | "peerDependencies": { 306 | "redux": "^4" 307 | } 308 | }, 309 | "node_modules/regenerator-runtime": { 310 | "version": "0.13.9", 311 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", 312 | "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" 313 | }, 314 | "node_modules/reselect": { 315 | "version": "4.1.6", 316 | "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz", 317 | "integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ==" 318 | }, 319 | "node_modules/scheduler": { 320 | "version": "0.22.0", 321 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", 322 | "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", 323 | "peer": true, 324 | "dependencies": { 325 | "loose-envify": "^1.1.0" 326 | } 327 | }, 328 | "node_modules/tslib": { 329 | "version": "2.4.0", 330 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", 331 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" 332 | }, 333 | "node_modules/use-sync-external-store": { 334 | "version": "1.2.0", 335 | "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", 336 | "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", 337 | "peerDependencies": { 338 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0" 339 | } 340 | } 341 | }, 342 | "dependencies": { 343 | "@babel/runtime": { 344 | "version": "7.18.3", 345 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.3.tgz", 346 | "integrity": "sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==", 347 | "requires": { 348 | "regenerator-runtime": "^0.13.4" 349 | } 350 | }, 351 | "@reduxjs/toolkit": { 352 | "version": "1.8.2", 353 | "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.2.tgz", 354 | "integrity": "sha512-CtPw5TkN1pHRigMFCOS/0qg3b/yfPV5qGCsltVnIz7bx4PKTJlGHYfIxm97qskLknMzuGfjExaYdXJ77QTL0vg==", 355 | "requires": { 356 | "immer": "^9.0.7", 357 | "redux": "^4.1.2", 358 | "redux-thunk": "^2.4.1", 359 | "reselect": "^4.1.5" 360 | } 361 | }, 362 | "@types/hoist-non-react-statics": { 363 | "version": "3.3.1", 364 | "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", 365 | "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", 366 | "requires": { 367 | "@types/react": "*", 368 | "hoist-non-react-statics": "^3.3.0" 369 | } 370 | }, 371 | "@types/prop-types": { 372 | "version": "15.7.5", 373 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", 374 | "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" 375 | }, 376 | "@types/react": { 377 | "version": "18.0.12", 378 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.12.tgz", 379 | "integrity": "sha512-duF1OTASSBQtcigUvhuiTB1Ya3OvSy+xORCiEf20H0P0lzx+/KeVsA99U5UjLXSbyo1DRJDlLKqTeM1ngosqtg==", 380 | "requires": { 381 | "@types/prop-types": "*", 382 | "@types/scheduler": "*", 383 | "csstype": "^3.0.2" 384 | } 385 | }, 386 | "@types/react-dom": { 387 | "version": "18.0.5", 388 | "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.5.tgz", 389 | "integrity": "sha512-OWPWTUrY/NIrjsAPkAk1wW9LZeIjSvkXRhclsFO8CZcZGCOg2G0YZy4ft+rOyYxy8B7ui5iZzi9OkDebZ7/QSA==", 390 | "peer": true, 391 | "requires": { 392 | "@types/react": "*" 393 | } 394 | }, 395 | "@types/scheduler": { 396 | "version": "0.16.2", 397 | "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", 398 | "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" 399 | }, 400 | "@types/use-sync-external-store": { 401 | "version": "0.0.3", 402 | "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", 403 | "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" 404 | }, 405 | "csstype": { 406 | "version": "3.1.0", 407 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", 408 | "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" 409 | }, 410 | "hoist-non-react-statics": { 411 | "version": "3.3.2", 412 | "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", 413 | "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", 414 | "requires": { 415 | "react-is": "^16.7.0" 416 | }, 417 | "dependencies": { 418 | "react-is": { 419 | "version": "16.13.1", 420 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", 421 | "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" 422 | } 423 | } 424 | }, 425 | "immer": { 426 | "version": "9.0.15", 427 | "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz", 428 | "integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==" 429 | }, 430 | "js-tokens": { 431 | "version": "4.0.0", 432 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 433 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 434 | "peer": true 435 | }, 436 | "loose-envify": { 437 | "version": "1.4.0", 438 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 439 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 440 | "peer": true, 441 | "requires": { 442 | "js-tokens": "^3.0.0 || ^4.0.0" 443 | } 444 | }, 445 | "mobx": { 446 | "version": "6.6.0", 447 | "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.6.0.tgz", 448 | "integrity": "sha512-MNTKevLH/6DShLZcmSL351+JgiJPO56A4GUpoiDQ3/yZ0mAtclNLdHK9q4BcQhibx8/JSDupfTpbX2NZPemlRg==", 449 | "peer": true 450 | }, 451 | "mobx-react": { 452 | "version": "7.5.0", 453 | "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-7.5.0.tgz", 454 | "integrity": "sha512-riHu0XZJA6f64L1iXZoAaDjVt6suYoy8I2HIfuz2tX3O4FFaAe4lVA2CoObttmUQTTFPM7j3Df6T4re0cHkghQ==", 455 | "requires": { 456 | "mobx-react-lite": "^3.4.0" 457 | } 458 | }, 459 | "mobx-react-lite": { 460 | "version": "3.4.0", 461 | "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.4.0.tgz", 462 | "integrity": "sha512-bRuZp3C0itgLKHu/VNxi66DN/XVkQG7xtoBVWxpvC5FhAqbOCP21+nPhULjnzEqd7xBMybp6KwytdUpZKEgpIQ==", 463 | "requires": {} 464 | }, 465 | "mota": { 466 | "version": "7.3.8", 467 | "resolved": "https://registry.npmjs.org/mota/-/mota-7.3.8.tgz", 468 | "integrity": "sha512-/6VegPGrnoHAXtS6ZLERNFuo8ikYJBkop2IMKFCcISysA1nn1y0HaffWG0u6C/0Fh2pN9AMT0PeIIlQkQin3ug==", 469 | "requires": { 470 | "ntils": "^5.2.0", 471 | "ober": "^6.5.0", 472 | "tslib": "*" 473 | } 474 | }, 475 | "ntils": { 476 | "version": "5.2.4", 477 | "resolved": "https://registry.npmjs.org/ntils/-/ntils-5.2.4.tgz", 478 | "integrity": "sha512-VUdpPYb0bvVpxkzFYx2o3I0VWIYtz+8VGlA55dlSTb7eInvWrazV+InQAM3aNfWkdLZL4LFay7eitUYGuZKhjQ==" 479 | }, 480 | "ober": { 481 | "version": "6.5.0", 482 | "resolved": "https://registry.npmjs.org/ober/-/ober-6.5.0.tgz", 483 | "integrity": "sha512-N3HFtlJzmlplcK+BATNCU2TB81APqh0sktbgqDlkvi51rdLm7SCuicM2mzV6lRa2jxx3VKAtbmSuTGqZRwuw6w==", 484 | "requires": { 485 | "tslib": "*" 486 | } 487 | }, 488 | "react": { 489 | "version": "18.1.0", 490 | "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", 491 | "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", 492 | "peer": true, 493 | "requires": { 494 | "loose-envify": "^1.1.0" 495 | } 496 | }, 497 | "react-dom": { 498 | "version": "18.1.0", 499 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", 500 | "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", 501 | "peer": true, 502 | "requires": { 503 | "loose-envify": "^1.1.0", 504 | "scheduler": "^0.22.0" 505 | } 506 | }, 507 | "react-is": { 508 | "version": "18.2.0", 509 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", 510 | "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" 511 | }, 512 | "react-redux": { 513 | "version": "8.0.2", 514 | "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.2.tgz", 515 | "integrity": "sha512-nBwiscMw3NoP59NFCXFf02f8xdo+vSHT/uZ1ldDwF7XaTpzm+Phk97VT4urYBl5TYAPNVaFm12UHAEyzkpNzRA==", 516 | "requires": { 517 | "@babel/runtime": "^7.12.1", 518 | "@types/hoist-non-react-statics": "^3.3.1", 519 | "@types/use-sync-external-store": "^0.0.3", 520 | "hoist-non-react-statics": "^3.3.2", 521 | "react-is": "^18.0.0", 522 | "use-sync-external-store": "^1.0.0" 523 | } 524 | }, 525 | "redux": { 526 | "version": "4.2.0", 527 | "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz", 528 | "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", 529 | "requires": { 530 | "@babel/runtime": "^7.9.2" 531 | } 532 | }, 533 | "redux-thunk": { 534 | "version": "2.4.1", 535 | "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", 536 | "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", 537 | "requires": {} 538 | }, 539 | "regenerator-runtime": { 540 | "version": "0.13.9", 541 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", 542 | "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" 543 | }, 544 | "reselect": { 545 | "version": "4.1.6", 546 | "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz", 547 | "integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ==" 548 | }, 549 | "scheduler": { 550 | "version": "0.22.0", 551 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", 552 | "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", 553 | "peer": true, 554 | "requires": { 555 | "loose-envify": "^1.1.0" 556 | } 557 | }, 558 | "tslib": { 559 | "version": "2.4.0", 560 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", 561 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" 562 | }, 563 | "use-sync-external-store": { 564 | "version": "1.2.0", 565 | "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", 566 | "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", 567 | "requires": {} 568 | } 569 | } 570 | } 571 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mota-examples", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Houfeng", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@reduxjs/toolkit": "^1.8.2", 13 | "mobx-react": "^7.5.0", 14 | "mota": "^7.3.8", 15 | "react-redux": "^8.0.2", 16 | "redux": "^4.2.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "declarationDir": "./types/", 5 | "sourceMap": false, 6 | "allowJs": false, 7 | "declaration": true, 8 | "noImplicitAny": true, 9 | "module": "ESNext", 10 | "target": "es5", 11 | "removeComments": true, 12 | "jsx": "react", 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "moduleResolution": "node", 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "importHelpers": true, 19 | "esModuleInterop": true, 20 | "skipLibCheck": true, 21 | "skipDefaultLibCheck": true, 22 | "lib": [ 23 | "dom", 24 | "es2017" 25 | ] 26 | }, 27 | "include": [ 28 | "./**/*.tsx" 29 | ] 30 | } -------------------------------------------------------------------------------- /markdowns/autorun.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: guide 3 | name: autorun 4 | title: 自执行函数 5 | index: 4 6 | --- 7 | 8 | # 自执行函数 9 | 10 | Mota 中提供了一个 `autorun` 函数,可用于装饰 React 组件的成员方法,被装饰的「成员方法」将会在组件挂载后自动执行一次,Mota 将「收集方法中依赖的模型数据」,在依赖的模型数据发生变化时会「自动重新执行」对应的组件方法。 11 | 12 | 示例 13 | 14 | ```js 15 | import { Component } from 'react'; 16 | import { model, autorun } from 'mota'; 17 | import DemoModel from './models/demo'; 18 | 19 | @model(DemoModel) 20 | export default Demo extends Component { 21 | 22 | @autorun 23 | test() { 24 | console.log(this.model.name); 25 | } 26 | 27 | } 28 | ``` 29 | 30 | 上边的示例代码中,组件在被挂载后将会自动执行 `test` 方法,同时 mota 会发现方法中依赖了 `model.name`,那么,在 `model.name` 发生变化时,就会重新执行 `test` 方法。 -------------------------------------------------------------------------------- /markdowns/binding.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: guide 3 | name: binding 4 | title: 数据绑定 5 | index: 6 6 | --- 7 | 8 | # 数据绑定 9 | 10 | 11 | ### 基本用法 12 | 13 | 不要惊诧,就是「双向绑定」。Mota 不排斥「双向绑定」,使用 Mota 能够实现类似 `ng` 或 `vue` 的绑定效果。还是前边小节中的模型,我们来稍微改动一下组件的代码 14 | 15 | ```js 16 | import { model,binding } from 'mota'; 17 | import React from 'react'; 18 | import ReactDOM from 'react-dom'; 19 | import User from './models/user'; 20 | 21 | @model(User) 22 | @binding 23 | class App extends React.Component { 24 | render(){ 25 | const { fullName, firstName, popup } = this.model; 26 | return
27 |

{fullName}

28 |

29 | 30 | 31 |

32 |
; 33 | } 34 | } 35 | ReactDOM.render(, mountNode); 36 | ``` 37 | 38 | ~~其中的「关键」就是 `@binding`,使用 `@binding` 后~~ (>=1.2.0 的版本将会自动处理,不必显示的启用),组件便具备了「双向绑定」的能力,在 `jsx` 中便可以通过名为 `data-bind` 的自定义 `attribute` 进行绑定了,`data-bind` 的值是一个「绑定表达式字符串」,绑定表达式执行的 `scope` 是 `model` 而不是 `this`,也就是只能与 `模型的成员` 进行绑定。 39 | 40 | 会有一种情况是当要绑定的数据是一个循环变量时,「绑定表达式」写起会较麻烦也稍显长,比如 41 | 42 | ```js 43 | @model(userModel) 44 | @binding 45 | class App extends React.Component { 46 | render(){ 47 | const { userList } = this.model; 48 | return
    49 | {userList.map((user,index)=>( 50 |
  • 51 | 52 | {user.name} 53 |
  • 54 | ))} 55 |
; 56 | } 57 | } 58 | ``` 59 | 60 | 因为「绑定表达式」的执行 `scope` 默认是 `this.model`,以及「表达式是个字符串」,看一下 `userList[${index}].selected` 这并不友好,为此 Mota 还提供了一个名为 `data-scope` 的 `attribute`,通过它能改变要绑定的 `scope`,参考如下示例 61 | 62 | ```js 63 | @model(userModel) 64 | @binding 65 | class App extends React.Component { 66 | render(){ 67 | const { userList } = this.model; 68 | return
    69 | {userList.map(user=>( 70 |
  • 71 | 72 | {user.name} 73 |
  • 74 | ))} 75 |
; 76 | } 77 | } 78 | ``` 79 | 80 | 通过 `data-scope` 将 `input` 的绑定上下文对象声明为当前循环变量 `user`,这样就可以用 `data-bind` 直接绑定到对应 `user` 的属性上了。 81 | 82 | ### 原生表单控件 83 | 84 | 所有的原生表单控件,比如「普通 input、checkbox、radio、textarea、select」都可以直接进行绑定。其中,「普通 input 和 textrea」比较简单,将一个字符类型的模型数据与控件绑定就行了,而对于「checkbox 和 radio」 有多种不同的绑定形式。 85 | 86 | 将「checkbox 或 radio」绑定到一个 `boolean` 值,此时会将 checkbox 或 radio 的 `checked` 属性和模型数据建立绑定,`checked` 反应了 `boolean` 变量的值,参考如下示例 87 | 88 | ```js 89 | @model({ selected:false }) 90 | @binding 91 | class App extends React.Component { 92 | render(){ 93 | return
94 | 95 | 96 |
; 97 | } 98 | } 99 | ``` 100 | 101 | 如上示例通过 `this.model.selected` 就能拿到当前 checkbox 或 radio 的选中状态。 102 | 103 | 104 | 将 checkbox 绑定到一个「数组」,通常是多个 checkbox 绑定同一个数组变量上,此时和数据建立绑定的是 checkbox 的 value,数据中会包含当前选中的 checkbox 的 value,如下 105 | 106 | ```js 107 | @model({ selected:[] }) 108 | @binding 109 | class App extends React.Component { 110 | render(){ 111 | return
112 | 113 | 114 |
; 115 | } 116 | } 117 | ``` 118 | 119 | 如上示例,通过 `this.selected` 就能知道当前有哪些 checkbox 被选中了,并拿到所有选中的 value 120 | 121 | 122 | 将多个 radio 绑定我到一个「字符类型的变量」,此时和数据建立绑定的是 raido 的 value,因为 radio 是单选的,所以对应的数据是当前选中的 radio 的 value,如下 123 | 124 | ```js 125 | @model({ selected:'' }) 126 | @binding 127 | class App extends React.Component { 128 | render(){ 129 | return
130 | 131 | 132 |
; 133 | } 134 | } 135 | ``` 136 | 通过 `this.model.selected` 就能拿到当前选中的 radio 的 `value` 137 | 138 | 139 | ### 自定义组件 140 | 141 | 但是对于一些「组件库」中的「部分表单组件」不能直接绑定,因为 Mota 并没有什么依据可以判断这是一个什么组件。所以 Mota 提供了一个名为 `bindable` 的函数,用将任意组件包装成「可绑定组件」。 142 | 143 | bindable 有两种个参数,用于分别指定「原始组件」和「包装选项」 144 | 145 | ```js 146 | //可以这样 147 | const MyComponent = bindable(opts, Component); 148 | //也可这样 149 | const MyCompoent = bindable(Component, opts); 150 | ``` 151 | 152 | 关建是 `bindable` 需要的 `opts`,通过 `opts` 我们可以造诉 Mota 如何绑定这个组件,`opts` 中有两个重要的成员,它的结构如下 153 | 154 | ```js 155 | { 156 | value: ['value 对应的属性名'], 157 | event: ['value 改变的事件名'] 158 | } 159 | ``` 160 | 161 | 所以,我们可以这样包装一个自定义文本输入框 162 | 163 | ```js 164 | const MyInput = bindable(Input,{ 165 | value: ['value'], 166 | event: ['onChange'] 167 | }); 168 | ``` 169 | 170 | 对这种「value 不需要转换,`change` 能通过 `event` 或 `event.target.value` 拿到值」的组件,通过如上的代码就能完成包装了。 171 | 172 | 对于有 `onChange` 和 `value` 的这类文本输入组件,因为 opts 的默认值就是 173 | 174 | ```js 175 | { 176 | value: ['value'], 177 | event: ['onChange'] 178 | } 179 | ``` 180 | 181 | 所以,可以更简单,这样就行, 182 | ```js 183 | const MyInput = bindable(Input); 184 | ``` 185 | 186 | 而对于 checkbox 和 radio 来讲,如上边讲到的它「根据不同的数据型有不同的绑定形式」,这就需要指定处理函数了,如下 187 | 188 | ```js 189 | const radioOpts = { 190 | prop: ['checked', (ctx, props) => { 191 | const mValue = ctx.getValue(); 192 | if (typeof mValue == 'boolean') { 193 | return !!mValue; 194 | } else { 195 | return mValue == props.value; 196 | } 197 | }], 198 | event: ['onChange', (ctx, event) => { 199 | const { value, checked } = event.target; 200 | const mValue = ctx.getValue(); 201 | if (typeof mValue == 'boolean') { 202 | ctx.setValue(checked); 203 | } else if (checked) ctx.setValue(value); 204 | }] 205 | }; 206 | ``` 207 | 208 | 通过 `prop` 的第二个值,能指定「属性处理函数」,`event` 的第二个值能指取「事件处理函数」,处理函数的 `ctx` 是个特殊的对象 209 | 210 | - `ctx.getValue` 能获取「当前绑定的模型数据」 211 | - `ctx.setValue` 能设置「当前绑定的模型数据」 212 | 213 | 上边是 `radio` 的配置,首先,在「属性处理函数」中通过绑定的「模型数据的类型」决定 `checked` 最终的状态是什么,并在函数中返回。再次,在「事件处理函数」中通过绑定的「模型数据的类型」决定将什么值回写到模型中。 214 | 215 | 通过「属性处理函数」和「事件处理函数」几乎就能将任意的自定义组件转换为「可绑定组件」了。 216 | 217 | 另外,对于常见的 `CheckBox` 和 `Radio` 类型的组件 Mota 也提供了内建的 `opts` 配置支持,如果一个自定义组件拥有和「原生 checkbox 一致的属性和事件模型」,那边可以直接用简单的方式去包装,如下 218 | 219 | ```js 220 | const MyCheckBox = bindable('checkbox',CheckBox); 221 | const MyRadio = bindable('radio',Radio); 222 | ``` 223 | 224 | 好了,关于绑定就这些了。 -------------------------------------------------------------------------------- /markdowns/hook_model.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: guide 3 | name: hook_model 4 | title: 面向 Hook 的模型 5 | index: 8 6 | --- 7 | 8 | # 面向 Hook 的模型 9 | 10 | ### 一些说明 11 | 12 | 针对 Mota 的 `useModel` 的模型本质上和针对 `@model` 的模型并无本质区别,用以往的风格编写的「模型类」或「单例的普通 Object」,除了能用于 `@model` 也是能用于 `useModel` 的。 13 | 14 | 既然用了 `Hook API`,是不是可在编写模型也避免再写「类」或「单例的 Object」?为此 `useModel` 还提供了用 `ES Module` 作为模型的支持,如用其他风格的模型一样,`ES Module` 风格的模型也不需要引用额外的依赖,仅用 `ES` 原生语法即可。 15 | 16 | ### 用 ES Module 编写模型 17 | 18 | 通过 `ES Module` 直接作为 `model` 的优点时「简单、直接」,同时由于为了保证「可响应」不被破坏,需要一点点约束,就是「必须导出一个 state 对象」,如下 19 | 20 | ```js 21 | export const state = { 22 | name: 'test' 23 | } 24 | 25 | export function setName(name){ 26 | state.name = name 27 | } 28 | ``` 29 | 30 | 上边的代码是一个最简单的可当作 `model` 的 `ES Module`,一个包含「state 和一组件函数」的 `ES Module` 就是一个可被 `useModel` 使用的 `model`,参考如下代码 31 | 32 | ```js 33 | import * as demo from './models/demo'; 34 | 35 | function App(){ 36 | const { name } = useModel(demo); 37 | return
{name}
; 38 | } 39 | ``` 40 | 41 | 注意:必须 export 一个 state 的约束只针对于 `ES Module`,用 `class` 或 `object` 的风格编写的 `model` 无任何约束。 -------------------------------------------------------------------------------- /markdowns/mapping.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: guide 3 | name: mapping 4 | title: 属性映射 5 | index: 3 6 | --- 7 | 8 | # 属性映射 9 | 10 | 在 React 中通常会将应用折分为多个组件重用它们,并在用时传递给它「属性」,Mota 提供了将「组件属性」映射到「模型数据」的能力,基于 `model` 编程会让「视图层」的编写更为方例,专注于 UI 的呈现,如下 11 | 12 | ```js 13 | @model({ value: 'demo' }) 14 | @mapping(['value']) 15 | class Demo extends React.Component { 16 | render () { 17 | return
{this.model.value}
; 18 | } 19 | } 20 | ``` 21 | 22 | 上边的代码通过 `mapping` 将 `Demo` 这个组件的 `value` 属性映射到了 `model.value` 上,在组件的属性 `value` 发生变化时,会自动同步到 `model.value` 中。 23 | 24 | 通过一个 `map` 进行映射,还可以让「组件属性」和「模型的成员」使用不同名称,如下: 25 | 26 | ```js 27 | @model({ value: 'demo' }) 28 | @mapping({ content: 'value' }) 29 | class Demo extends React.Component { 30 | render () { 31 | return
{this.model.value}
; 32 | } 33 | } 34 | ``` 35 | 36 | 上边的代码,将组件 demo 的 `content` 属性映射到了 `model.value` 上,那么这个组件就可以这样使用了 37 | 38 | ```js 39 | function App(){ 40 | return ; 41 | } 42 | ``` 43 | 44 | `Demo` 组件的 `content` 属性,将自动被赋值给 `model.value`,如果没有 `mapping`,通常我们就需要在 `componentDidMount` 和 `componentWillReceiveProps` 之类的生命周函数去处理。其实,`mapping` 就像是一个语法糖,使用它将不再需要手动处理 prop->model 的更新了。 -------------------------------------------------------------------------------- /markdowns/model.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: guide 3 | name: model 4 | title: 编写业务模型 5 | index: 2 6 | --- 7 | 8 | # 编写业务模型 9 | 10 | 在你编写模型之前,先放下 React 也放下 Mota,就用单纯的 JavaScript 去编写你的业务模型,或有一个或多个类、或就是几个 Object,依它们应有的、自然的关系去抽像就行了,业务模型不依赖于 UI、也不依赖于某个框架,它易于测试,你可以针对它做单元测试。它易于重用,你可以将它用在合适的地方。最后, Mota 只是出场把它关联到 React。 11 | 12 | 在 Mota 中「模型」可以是由一个 `class` 或普通的的 `Object`,整个「业务模型层」会由多个 `class` 和多个 `Object` 组成,而编写模型所需要的知识就是 JavaScript 固有的编程的知识。 13 | 14 | 如下示例通过编写一个名为 `User` 的 `class` 创建了一个「用户模型」 15 | 16 | ```js 17 | export default class User { 18 | firstName = 'Jack'; 19 | lastName = 'Hou'; 20 | get fullName(){ 21 | reutrn `${this.firstName} ${this.lastName}`; 22 | } 23 | } 24 | ``` 25 | 26 | 也可以是一个 `Object`,通常这个模型需要是「单例」时,可采用这种方式,如下 27 | 28 | ```js 29 | export default { 30 | firstName: 'Jack', 31 | lastName: 'Hou', 32 | get fullName(){ 33 | reutrn `${this.firstName} ${this.lastName}`; 34 | } 35 | }; 36 | ``` 37 | 38 | 在「业务模型」编写完成后,可以通过 `@model` 将某个「类」或「类的实例」关联到指定组件,关联后便可以在组件中使用 `this.model` 访问「模型的成员变量或方法」了,Mota 还会自动「收集组件依赖」,在组件「依赖的模型数据」发生变化时,自动响应变化并「驱动组件重新渲染」,如下 39 | 40 | ```js 41 | import { model,binding } from 'mota'; 42 | import React from 'react'; 43 | import ReactDOM from 'react-dom'; 44 | import User from './models/user'; 45 | 46 | @model(User) 47 | class App extends React.Component { 48 | 49 | onChange(field,event){ 50 | this.model[field] = event.target.value; 51 | } 52 | 53 | render(){ 54 | return
55 |

{this.model.fullName}

56 |

57 | 58 |
59 | 60 |

61 |
; 62 | } 63 | } 64 | 65 | ReactDOM.render(, mountNode); 66 | ``` 67 | 68 | 值得注意的是,在使用 `@model` 时如果传入的是一个 `class` 最终每个组件实例都会自动创建一个 `独立的实例`,这样带来的好处是「当一个页面中有同一个组件的多个实例时,不会相互影响」。 -------------------------------------------------------------------------------- /markdowns/quick.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: guide 3 | name: quick 4 | title: 快速开始 5 | index: 1 6 | --- 7 | 8 | # Mota 9 | 10 | ## 简述 11 | 12 | React 是一个「视图层」的 UI 框架,以常见的 MVC 来讲 React 仅是 View,而我们在编写应用时,通常还需要关注更加重要的 model,对于 React 来讲,我们常常需要一个「状态管理」库。然而,目前大多数针对 React 的状态管理库都是「强依赖」过多的侵入本应该独立的业务模型中,导致「业务逻辑」对应的代码并不能轻易在其它地方重用,往往这些框架还具有「强排它性」,但是「业务模型」应该是没有过多依赖,应该是无关框架的,它应该随时可以被用在任何合适的 JavaScript 环境中,使用 mota 你可以用原生的普通的 JavaScript 代码编写你的「业务模型」,并让你的「业务模型」在不同框架、不同运行环境下重用更为容易。 13 | 14 | Mota 是一个响应式的 React 应用状态管理库,基于 Mota 你可以用单纯无依赖的 JavaScript 为应用编写「业务模型」,并轻易的将「业务模型」关联到 React 应用中。 15 | 16 | ## 示例 17 | 18 | [在线 TodoList 示例](http://houfeng.net/dn-template-mota/example/) 19 | ([示例源码](https://github.com/Houfeng/dn-template-mota)) 20 | 21 | 22 | 23 | ## 安装 24 | 25 | 通过 npm 安装,如下 26 | ```sh 27 | $ npm i mota --save 28 | ``` 29 | 30 | 或通过 `dawn` 脚手脚加创建工程,如下 31 | 32 | ```sh 33 | $ mkdir your_path 34 | $ cd your_path 35 | $ dn init -t mota 36 | $ dn dev 37 | ``` 38 | 39 | 需要先安装 dawn([Dawn 安装及使用文档](https://alibaba.github.io/dawn/docs/)) 40 | 41 | ## 结构 42 | 43 | 一个 `mota` 工程的通常结构如下 44 | 45 | ```sh 46 | . 47 | ├── README.md 48 | ├── package.json 49 | └── src 50 | ├── assets 51 | │   ├── common.less 52 | │   ├── favicon.ico 53 | │   └── index.html 54 | ├── components 55 | │   ├── todoApp.js 56 | │   └── todoItem.js 57 | ├── index.js 58 | └── models 59 | ├── TodoItem.js 60 | ├── TodoList.js 61 | └── index.js 62 | ``` 63 | 64 | ## 文档 65 | - [快速开始](http://houfeng.net/mota/#!/zh/guide/quick) 66 | - [编写业务模型](http://houfeng.net/mota/#!/zh/guide/model) 67 | - [将组件属性映射到模型](http://houfeng.net/mota/#!/zh/guide/mapping) 68 | - [自执行函数](http://houfeng.net/mota/#!/zh/guide/autorun) 69 | - [监听模型变化](http://houfeng.net/mota/#!/zh/guide/watch) 70 | - [将模型数据与表单绑定](http://houfeng.net/mota/#!/zh/guide/binding) 71 | 72 | ## 链接 73 | - [版本发布日志](https://github.com/Houfeng/mota/releases) 74 | - [MIT 开源协议](https://tldrlegal.com/license/mit-license) -------------------------------------------------------------------------------- /markdowns/typescript.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: guide 3 | name: typescript 4 | title: 在 TS 中使用 5 | index: 8 6 | --- 7 | 8 | # 在 TS 中使用 Mota 9 | 10 | Mota 的 `Package` 中自带了「类型定义文件」,无论使用 `Class + Decorator` 风格的 API 或使用 `Hooks` 风格的 API,都能愉快的使用 TypeScript,下边有两个小提示。 11 | 12 | ### 提示一:使用 @model 13 | 14 | 在通过 `@model` 为组件关联了一个 `model` 后,需要声明 `this.model` 的类型,参考如下代码 15 | 16 | ```js 17 | import * as React from "react"; 18 | import { model, watch, mapping } from "mota"; 19 | import { DemoModel } from "./DemoModel"; 20 | 21 | @model(DemoModel) 22 | export class Demo extends React.Component { 23 | 24 | //需要声明 model 的类型 25 | model: DemoModel; 26 | 27 | render() { 28 | //便能让 this.model 具备完整的类型提示了 29 | const { name } = this.model; 30 | return
31 | {name} 32 |
; 33 | } 34 | } 35 | ``` 36 | 37 | ### 提示一:使用 useModel 38 | 39 | 完整的 `useModel` 的定义为 `useModel(model:T)=>T`,但使用 `useModel` 时一般不需做特别的声明,默认情况下 `TS` 就能完成类型推导 40 | 41 | ```js 42 | import * as React from "react"; 43 | import { useModel } from "mota"; 44 | import { DemoModel } from "./DemoModel"; 45 | 46 | export function Demo { 47 | const { name } = useModel(DemoModel); 48 | return
49 | {name} 50 |
; 51 | } 52 | ``` -------------------------------------------------------------------------------- /markdowns/use_model.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: guide 3 | name: use_model 4 | title: 使用 Hook API 5 | index: 7 6 | --- 7 | 8 | # 使用 Hook API 9 | 10 | ### 简单介绍 11 | 在 React 发布包含 `Hooks` 的 `alpah` 版后,Mota 也在 `next` 版本中新增支持了 Hook 风格的 API,随着 React `v16.8` 版本的发布带来了稳定版的 `Hooks` 支持。 12 | 13 | 目前,Mota 已经在稳定版中,提供了 `Hook API` 的支持,利用 React 的 `Hooks` 可以让你在不编写类的情况下使用 `state` 和 React 的其他功能。而使用 Mota 极少的 `Hook API` 将给应用带来 Hook 风格可响应的全局状态管理支持。 14 | 15 | ### 基本用法 16 | 17 | ```js 18 | import React from 'react'; 19 | import { render } from 'react-dom'; 20 | 21 | function App(){ 22 | //通过 useModel 拿到一个可响应的 model 23 | const model = useModel({ count:0 }); 24 | //定义累加按钮事件 25 | const onClick = useCallback(()=>model.count++); 26 | //-- 27 | return
28 |
{model.count}
29 | 30 |
; 31 | } 32 | 33 | render(, document.getElementId('root')); 34 | ``` 35 | 36 | 仅有一个新增 API `useModel`,通过 `useModel` 可在一个 `Function Component` 中使用 `model`,如同在 `Class Component` 中的 `@model`,此时的 `model` 依然是可响应的,执行时会对组件进行「依赖收集」,当操作 `model` 的成员时(比如 `model.count=1` 的赋值操作),Mota 会自动发现组件依赖的数据发生了变化并通知组件进行更新。 37 | 38 | ### 进阶说明 39 | 40 | 在基本用法中提到了一个关键词「依赖收集」,通过 `useModel` 拿到了可响应的 `model`,默认情况下只有被组件依赖的模型数据发生了变化组件才会更新,比如下边的示例代码中,只有在 `model.a` 发生变化时,组件才会重新渲染。 41 | 42 | 43 | ```js 44 | function Demo(){ 45 | const model = useModel({ a:0, b:1 }); 46 | ... 47 | return
{model.a}
48 | } 49 | ``` 50 | 51 | 实际开发过程中有时「组件依赖了模型上的某个对象,但希望这个对象的子成员发生变化时,组件也要重新渲染」 52 | 53 | ```js 54 | function Demo(){ 55 | const { info } = useModel({ info: { name: 'test'} }); 56 | ... 57 | return 58 | } 59 | ``` 60 | 61 | 因为对于 `Demo` 来说只依赖了 `info`,而 `info` 的引用是一直没有变化的,所以在 `info.name` 发生变化时 `Demo` 并不会重新渲染。那这样 `Info` 组件会一直显示旧的数据。 62 | 63 | 如何处理这个问题? 64 | 65 | 一个方法是让 `Info` 也通过 `useModel` 有自已的 `model`,那 `Info` 的依赖会被独立解析,比如 66 | 67 | ```js 68 | function Info(props){ 69 | const info = useModel(props.data); 70 | return
{info.name}
71 | } 72 | ``` 73 | 74 | 这个虽然 `Demo` 不会重新渲染,但 Mota 会发现 `Info` 依赖了 `info.name`,但发现数据变化时,`Info` 会自动更新。 75 | 76 | 还有一个方法是,在更新 `info.name` 时换一个写法 77 | 78 | ```js 79 | //通常的直接给 name 赋值 80 | model.info.name = 'test'; 81 | //如下的给 info 赋值的写法,会让 Demo 发现 info 的变化 82 | model.info = {...model.info, name: 'test'}; 83 | ``` 84 | 85 | 除了上述的两个方法,还有一个方法就是通过 `useModel` 的第二个参数显示的声明额外的依赖,第二个参数可是一个数组,数组中是显式声明的依赖,格式为子成员的路径,如下 86 | 87 | ```js 88 | function Demo(){ 89 | const { info } = useModel({ 90 | info: { name: 'test'} 91 | }, ['info.name']); 92 | ... 93 | return 94 | } 95 | ``` 96 | 97 | 但有时时模型数据是一个数组,我们无法直接指定每个子元素的路径,这时第二个参数还可以是一个函数,函数的参数是「变化的模型数据的路径」,可参函数中返回 `boolean` 值决定是否需要更新组件,如下 98 | 99 | ```js 100 | function Demo(){ 101 | const { info } = useModel({ 102 | info: [{name: 'test1'}] 103 | }, p=> p.endsWith('.name')); 104 | ... 105 | return 106 | } 107 | ``` 108 | 109 | 示例中通过用 `endsWith` 路径是不是 `*.name` 结尾的决定要不要更新,当然也可以用更多的判断方法决定要不要更新。 -------------------------------------------------------------------------------- /markdowns/watch.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: guide 3 | name: watch 4 | title: 监听模型变化 5 | index: 5 6 | --- 7 | 8 | # 监听模型变化 9 | 10 | 11 | Mota 中提供了一个 `watch` 函数,可用于装饰 React 组件的成员方法,`watch` 可以指定要观察的「模型数据」,在模型数据发变化时,就会自动执行「被装饰的组件方法」,`watch` 还可以像 `autorun` 一样自动执行一次,但它和 `autorun` 还是不尽相同,主要有如下区别 12 | 13 | - `autorun` 会自动收集依赖,而 `watch` 不会关心组件方法中有何依赖,需要手动指定依赖的模型数据 14 | - `watch` 默认不会「自动执行」,需显式的指定「立即执行参数为 true」,才会自动执行首次。 15 | - `autorun` 依赖的是「模型数据」本身,而 `watch` 依赖的是「计算函数」每次的「计算结果」 16 | 17 | 示例 18 | 19 | ```js 20 | import { Component } from 'react'; 21 | import { model, autorun } from 'mota'; 22 | import DemoModel from './models/demo'; 23 | 24 | @model(DemoModel) 25 | export default Demo extends Component { 26 | 27 | @watch(model=>model.name) 28 | test() { 29 | console.log('name 发生了变化'); 30 | } 31 | 32 | } 33 | ``` 34 | 35 | 上边的代码,通过 `watch` 装饰了 `test` 方法,并指定了观察的模型数据 `model.name`,那么每当 `model.name` 发生变化时,都会打印 `name 发生了变化`. 36 | 37 | `watch` 是否重新执行,取决于 `watch` 的作为第一个参数传给它的「计算函数」的计算结果,每当依赖的模型数据发生变化时 `watch` 都会重执行计算函数,当计算结果有变化时,才会执行被装饰的「组件方法」,示例 38 | 39 | ```js 40 | export default Demo extends Component { 41 | 42 | @watch(model=>model.name+model.age) 43 | test() { 44 | console.log('name 发生变化'); 45 | } 46 | 47 | } 48 | ``` 49 | 50 | 有时,我们希望 `watch` 能首先自动执行一次,那么可通过向第二个参数传一个 `true` 声明这个 `watch` 要自动执行一次。 51 | 52 | ```js 53 | export default Demo extends Component { 54 | 55 | @watch(model=>model.name,true) 56 | test() { 57 | console.log('name 发生变化'); 58 | } 59 | 60 | } 61 | ``` 62 | 63 | 上边的 `test` 方法,将会在「组件挂载之后自动执行」,之后在 `model.name` 发生变化时也将自动重新执行。 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mota", 3 | "version": "8.2.4", 4 | "description": "An extremely lightweight and responsive state management library", 5 | "module": "./dist/mota-es.js", 6 | "main": "./dist/mota-cjs.js", 7 | "types": "./types/index.d.ts", 8 | "scripts": { 9 | "config": "dn config registry https://registry.npmjs.com/", 10 | "test": "npm run config && dn test && dn build -e prod", 11 | "coveralls": "nyc report --reporter=text-lcov | coveralls" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Houfeng/mota.git" 16 | }, 17 | "author": { 18 | "name": "Houfeng", 19 | "email": "houzhanfeng@gmail.com", 20 | "url": "http://houfeng.net" 21 | }, 22 | "license": "MIT", 23 | "dependencies": { 24 | "ober": "8.3.2", 25 | "tslib": "*" 26 | }, 27 | "peerDependencies": { 28 | "react": "*" 29 | }, 30 | "peerDependenciesMeta": { 31 | "react-dom": { 32 | "optional": true 33 | }, 34 | "react-native": { 35 | "optional": true 36 | } 37 | }, 38 | "devDependencies": { 39 | "@types/mocha": "9.1.1", 40 | "@types/react": "*", 41 | "@types/react-dom": "*", 42 | "@typescript-eslint/eslint-plugin": "5.20.0", 43 | "@typescript-eslint/parser": "5.20.0", 44 | "coveralls": "^3.0.0", 45 | "dawn": "^1.8.0", 46 | "dn-middleware-browser-sync": "^0.1.0", 47 | "dn-middleware-clean": "^1.0.2", 48 | "dn-middleware-copy": "^0.2.7", 49 | "dn-middleware-file-header": "^1.0.3", 50 | "dn-middleware-server": "^1.0.15", 51 | "dn-middleware-shell": "^1.1.0", 52 | "dn-middleware-submitter": "^1.0.1", 53 | "dn-middleware-typescript": "^2.0.8", 54 | "dn-middleware-unit": "0.3.1", 55 | "dn-middleware-version": "^1.0.4", 56 | "eslint": "8.13.0", 57 | "eslint-plugin-prettier": "4.0.0", 58 | "nyc": "^11.4.1", 59 | "prettier": "2.6.2", 60 | "react": "*", 61 | "react-dom": "*", 62 | "rollup": "^2.2.0", 63 | "rollup-plugin-alias": "2.2.0", 64 | "rollup-plugin-cleanup": "^3.2.1", 65 | "rollup-plugin-commonjs": "^10.1.0", 66 | "rollup-plugin-inject-process-env": "1.3.1", 67 | "rollup-plugin-node-resolve": "^5.2.0", 68 | "rollup-plugin-replace": "2.2.0", 69 | "rollup-plugin-sourcemaps": "0.6.3", 70 | "rollup-plugin-terser": "^5.3.0", 71 | "rollup-plugin-typescript2": "0.31.2", 72 | "rollup-plugin-uglify": "^6.0.4", 73 | "typescript": "4.6.3" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs'; 2 | import injectProcessEnv from 'rollup-plugin-inject-process-env'; 3 | import path from 'path'; 4 | import resolve from 'rollup-plugin-node-resolve'; 5 | import sourcemaps from 'rollup-plugin-sourcemaps'; 6 | import typescript from 'rollup-plugin-typescript2'; 7 | 8 | const externals = { 9 | 'react': 'React', 10 | 'react-dom': 'ReactDOM', 11 | } 12 | 13 | const createConf = (page) => { 14 | return { 15 | input: `./examples/${page}.tsx`, 16 | output: [ 17 | { 18 | file: `./dist/js/${page}.js`, 19 | format: 'iife', 20 | sourcemap: true, 21 | name: page.split('-').join('_'), 22 | globals: externals, 23 | } 24 | ], 25 | external: Object.keys(externals), 26 | plugins: [ 27 | resolve(), 28 | commonjs({ 29 | namedExports: { 30 | 'examples/node_modules/react-is/index.js': [ 31 | 'isValidElementType', 32 | 'isContextConsumer', 33 | ], 34 | 'examples/node_modules/use-sync-external-store/shim/with-selector.js': [ 35 | 'useSyncExternalStoreWithSelector' 36 | ], 37 | 'examples/node_modules/use-sync-external-store/shim/index.js': [ 38 | 'useSyncExternalStore' 39 | ] 40 | } 41 | }), 42 | typescript({ 43 | useTsconfigDeclarationDir: true, 44 | tsconfig: path.resolve(__dirname, './tsconfig.dev.json') 45 | }), 46 | sourcemaps(), 47 | injectProcessEnv({ 48 | NODE_ENV: 'production', 49 | OBER_CONFIG: { mode: 'property' }, 50 | }), 51 | ] 52 | }; 53 | }; 54 | 55 | export default [ 56 | createConf('develop'), 57 | createConf('benchmark-mota'), 58 | createConf('benchmark-mota-old'), 59 | createConf('benchmark-redux'), 60 | createConf('benchmark-mobx'), 61 | createConf('benchmark-normal'), 62 | ]; -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import cleanup from 'rollup-plugin-cleanup'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | import typescript from 'rollup-plugin-typescript2'; 5 | 6 | const externals = { 7 | 'react': 'React', 8 | 'react-dom': 'ReactDOM', 9 | } 10 | 11 | const createConf = ({ name, input, min } = {}) => { 12 | const suffix = min ? '.min' : ''; 13 | return { 14 | input: input, 15 | output: [ 16 | { 17 | file: `./dist/${name}-es${suffix}.js`, 18 | format: 'es' 19 | }, 20 | { 21 | file: `./dist/${name}-cjs${suffix}.js`, 22 | format: 'cjs' 23 | }, 24 | { 25 | file: `./dist/${name}-umd${suffix}.js`, 26 | format: 'umd', 27 | name: 'Mota', 28 | globals: externals, 29 | }, 30 | { 31 | file: `./dist/${name}-iife${suffix}.js`, 32 | format: 'iife', 33 | name: 'Mota', 34 | globals: externals, 35 | } 36 | ], 37 | external: Object.keys(externals), 38 | plugins: [ 39 | resolve(), 40 | min && terser(), 41 | cleanup({ comments: 'none' }), 42 | typescript({ 43 | useTsconfigDeclarationDir: true, 44 | }), 45 | ].filter(Boolean) 46 | }; 47 | }; 48 | 49 | const normalOptions = { name: 'mota', input: './src/index.ts' }; 50 | const serverOptions = { name: 'mota-server', input: './src/server.ts' }; 51 | 52 | export default [ 53 | createConf({ ...normalOptions }), 54 | createConf({ ...normalOptions, min: true }), 55 | createConf({ ...serverOptions }), 56 | createConf({ ...serverOptions, min: true }), 57 | ]; -------------------------------------------------------------------------------- /scripts/info.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return async (next, ctx) => { 3 | const { name,version } = ctx.project; 4 | await ctx.utils.writeFile('./src/info.ts', ` 5 | export const name = "${name}"; 6 | export const version = "${version}"; 7 | `); 8 | next(); 9 | } 10 | } -------------------------------------------------------------------------------- /server.yml: -------------------------------------------------------------------------------- 1 | proxy: 2 | rules: 3 | ^/api(.*): 'https://www.aliyun.com/' -------------------------------------------------------------------------------- /src/batch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present Houfeng 3 | * @homepage https://github.com/Houfeng/mota 4 | * @author Houfeng 5 | */ 6 | 7 | export { unstable_batchedUpdates } from "react-dom"; 8 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present Houfeng 3 | * @homepage https://github.com/Houfeng/mota 4 | * @author Houfeng 5 | */ 6 | 7 | import { DependencyList, useCallback, useLayoutEffect, useMemo } from "react"; 8 | import { autorun, computable, observable, watch } from "ober"; 9 | 10 | /** 11 | * 在函数组件中即时创建一个可观察对象 12 | * @param value 对象或创建对象的函数 13 | * @param deps 指定的依赖发生变化时,将重新创建 14 | * @returns 可观察对象 15 | */ 16 | export function useObservable( 17 | value: T | (() => T), 18 | deps: DependencyList = [] 19 | ): T { 20 | return useMemo(() => { 21 | return observable(value instanceof Function ? value() : value); 22 | }, deps); 23 | } 24 | 25 | /** 26 | * 创建一个观察器,每当用到的数据发生变化时,将重新计算 27 | * @param selector 计算函数,需返回一个值,将对新旧值进行浅对比,决定是否调用执行函数 28 | * @param handler 执行函数,由 selector 的计算结果决定是否重新执行 29 | * @param immed 是否立即执行一次 handler 30 | */ 31 | export function useWatch( 32 | selector: () => T, 33 | handler: (newValue?: T, oldValue?: T) => void, 34 | immed = false 35 | ) { 36 | return useLayoutEffect(() => watch(selector, handler, immed), [immed]); 37 | } 38 | 39 | /** 40 | * 启动一个自执行函数,当函数中用到的数据发生变化时它将自动重新执行 41 | * @param handler 将执行的函数 42 | */ 43 | export function useAutoRun(handler: () => void) { 44 | return useLayoutEffect(() => autorun(handler), []); 45 | } 46 | 47 | /** 48 | * 创建一个具备缓存能力的计算结果, 49 | * 计算函数用到的数据发生变化时将重新计算并驱动组件更新,否则将使用缓存 50 | * @param fn 计算函数 51 | * @returns 计算结果 52 | */ 53 | export function useComputed(fn: () => T): T { 54 | const compute = useCallback(computable(fn, { bind: false }), []); 55 | useLayoutEffect(() => { 56 | compute.subscribe!(); 57 | return () => compute.unsubscribe!(); 58 | }); 59 | return compute(); 60 | } 61 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present Houfeng 3 | * @homepage https://github.com/Houfeng/mota 4 | * @author Houfeng 5 | */ 6 | 7 | import { ObserveConfig, nextTick } from "ober"; 8 | 9 | import { unstable_batchedUpdates } from "./batch"; 10 | import { name } from "./info"; 11 | import { AnyFunction } from "./util"; 12 | 13 | export { 14 | observable, 15 | action, 16 | bind, 17 | computed, 18 | track, 19 | untrack, 20 | autorun, 21 | watch, 22 | nextTick, 23 | ObserveConfig, 24 | ObserveSpy, 25 | takeDependencies, 26 | isProxy, 27 | type ObserveMode, 28 | } from "ober"; 29 | 30 | export { version } from "./info"; 31 | export { sync } from "./sync"; 32 | 33 | export { observer } from "./observer"; 34 | export { useObservable, useWatch, useAutoRun, useComputed } from "./hooks"; 35 | 36 | export function setBatchHandler(fn: AnyFunction) { 37 | nextTick.batch = fn; 38 | } 39 | 40 | setBatchHandler(unstable_batchedUpdates); 41 | ObserveConfig.logPrefix = name.toLocaleUpperCase(); 42 | -------------------------------------------------------------------------------- /src/info.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present Houfeng 3 | * @homepage https://github.com/Houfeng/mota 4 | * @author Houfeng 5 | */ 6 | 7 | export const name = "mota"; 8 | export const version = "8.2.4"; 9 | -------------------------------------------------------------------------------- /src/input.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present Houfeng 3 | * @homepage https://github.com/Houfeng/mota 4 | * @author Houfeng 5 | */ 6 | 7 | import { AnyFunction, inBrowser } from "./util"; 8 | 9 | import { ChangeEvent } from "react"; 10 | 11 | let timer: ReturnType | null; 12 | let composing = false; 13 | let inputting = false; 14 | let value: any = null; 15 | 16 | function bind(name: any, handler: AnyFunction) { 17 | if (!inBrowser()) return; 18 | document.addEventListener(name, handler, true); 19 | } 20 | 21 | bind("compositionUpdate", () => { 22 | composing = true; 23 | }); 24 | 25 | bind("compositionEnd", () => { 26 | composing = false; 27 | }); 28 | 29 | bind("input", (event: ChangeEvent) => { 30 | inputting = true; 31 | value = event.target.value as string; 32 | if (timer) clearTimeout(timer); 33 | timer = setTimeout(() => { 34 | inputting = false; 35 | timer = null; 36 | }, 0); 37 | }); 38 | 39 | export function isSyncInput(updateValue: any) { 40 | return (inputting || composing) && value === updateValue; 41 | } 42 | -------------------------------------------------------------------------------- /src/observer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present Houfeng 3 | * @homepage https://github.com/Houfeng/mota 4 | * @author Houfeng 5 | */ 6 | 7 | import { 8 | Component, 9 | ReactNode, 10 | useLayoutEffect, 11 | useMemo, 12 | useState, 13 | } from "react"; 14 | import { 15 | ComponentClass, 16 | ComponentType, 17 | FunctionComponent, 18 | isClassComponent, 19 | } from "./util"; 20 | import { 21 | ObserveData, 22 | ReactiveFunction, 23 | define, 24 | getOwnValue, 25 | nextTick, 26 | reactivable, 27 | } from "ober"; 28 | 29 | import { inSyncHandler } from "./sync"; 30 | import { isSyncInput } from "./input"; 31 | 32 | function createReactiver( 33 | render: (...args: any[]) => ReactNode, 34 | requestUpdate: () => void, 35 | bind = true 36 | ) { 37 | const update = (info?: ObserveData) => { 38 | return inSyncHandler() || isSyncInput(info?.value) 39 | ? requestUpdate() 40 | : nextTick(requestUpdate); 41 | }; 42 | return reactivable(render, { bind, update, batch: false }); 43 | } 44 | 45 | function getDisplayName( 46 | target: ComponentClass | FunctionComponent, 47 | defaultName: string 48 | ) { 49 | return target.displayName || target.name || defaultName; 50 | } 51 | 52 | type ObserverComponent = Component & { __reactiver__: ReactiveFunction }; 53 | 54 | function wrapClassComponent(Component: T): T { 55 | // 8.1.10 之前的版本是通过 extends 传入 Component 的 mixin 方式 56 | // 如果上层应用不编译将一个 Native Class 传入, 57 | // 此处 Wrapper 被 ts 编译后 Function 在 extends 时 58 | // _supper.apply 将引发 invoked without 'new' 错误,所有 TS/Babel 类代码均有此问题 59 | // 所以,在 8.1.10 后的版本采用「直接修改原型」的方式实现 60 | const proto = Component.prototype as Component; 61 | const { render, componentWillUnmount } = proto; 62 | proto.render = function (this: ObserverComponent) { 63 | // 如果是实被子类调用直接执行原 render 64 | if (this.constructor !== Component) return render?.call(this); 65 | if (!this.__reactiver__) { 66 | let tick = 0; 67 | this.__reactiver__ = createReactiver( 68 | () => render?.call(this), 69 | () => this.setState({ __tick__: ++tick }) 70 | ); 71 | } 72 | return this.__reactiver__(); 73 | }; 74 | proto.componentWillUnmount = function (this: ObserverComponent) { 75 | this.__reactiver__!.unsubscribe!(); 76 | componentWillUnmount?.call(this); 77 | }; 78 | Component.displayName = getDisplayName(Component, "Component"); 79 | return Component; 80 | } 81 | 82 | function wrapFunctionComponent(FC: T): T { 83 | const Wrapper = (...args: any[]) => { 84 | const setState = useState([])[1]; 85 | const reactiver = useMemo(() => { 86 | return createReactiver(FC, () => setState([]), false); 87 | }, []); 88 | useLayoutEffect(() => { 89 | reactiver.subscribe!(); 90 | return reactiver.unsubscribe; 91 | }, [reactiver]); 92 | return reactiver(...args); 93 | }; 94 | Object.setPrototypeOf(Wrapper, FC); 95 | Wrapper.displayName = getDisplayName(FC, "FC"); 96 | return Wrapper as T; 97 | } 98 | 99 | /** 100 | * 将一个组件转换为可响应组件 (不能是 memo/forwardRef/lazy 后的组件) 101 | * @param target 原类组件或函数组件 102 | * @returns 具有响应能力的组件 103 | */ 104 | export function observer(target: T) { 105 | if (!target || getOwnValue(target, "__observer__")) return target; 106 | const Wrapper = isClassComponent(target) 107 | ? wrapClassComponent(target) 108 | : wrapFunctionComponent(target); 109 | target.__observer__ = true; 110 | define(target, "__observer__", true); 111 | return Wrapper as T & { displayName?: string }; 112 | } 113 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present Houfeng 3 | * @homepage https://github.com/Houfeng/mota 4 | * @author Houfeng 5 | */ 6 | 7 | import { ObserveConfig } from "ober"; 8 | import { name } from "./info"; 9 | 10 | export { 11 | observable, 12 | action, 13 | bind, 14 | computed, 15 | track, 16 | untrack, 17 | autorun, 18 | watch, 19 | nextTick, 20 | ObserveConfig, 21 | ObserveSpy, 22 | isProxy, 23 | type ObserveMode, 24 | } from "ober"; 25 | 26 | export { version } from "./info"; 27 | export { sync } from "./sync"; 28 | 29 | ObserveConfig.logPrefix = name.toLocaleUpperCase(); 30 | -------------------------------------------------------------------------------- /src/sync.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present Houfeng 3 | * @homepage https://github.com/Houfeng/mota 4 | * @author Houfeng 5 | */ 6 | 7 | import { AnyFunction } from "./util"; 8 | 9 | let syncing = false; 10 | 11 | export function inSyncHandler() { 12 | return syncing; 13 | } 14 | 15 | export function sync(handler: T): ReturnType { 16 | const originState = syncing; 17 | syncing = true; 18 | try { 19 | const result = handler(); 20 | syncing = originState; 21 | return result; 22 | } catch (err) { 23 | syncing = originState; 24 | throw err; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present Houfeng 3 | * @homepage https://github.com/Houfeng/mota 4 | * @author Houfeng 5 | */ 6 | 7 | import { Component, ReactNode, ExoticComponent } from "react"; 8 | 9 | export const inBrowser = () => typeof document !== "undefined"; 10 | export const hasRequire = () => typeof require === "function"; 11 | 12 | export type AnyFunction = (...args: any[]) => any; 13 | 14 | export type Impossible = { [P in keyof T]?: never }; 15 | 16 | export type ComponentClass = { 17 | new (...args: any[]): Component; 18 | displayName?: string; 19 | }; 20 | 21 | export type FunctionComponent = ((...args: any[]) => ReactNode) & { 22 | displayName?: string; 23 | } & Impossible; 24 | 25 | export type ComponentType = (ComponentClass | FunctionComponent) & { 26 | __observer__?: boolean; 27 | }; 28 | 29 | export function isClassComponent( 30 | target: ComponentType 31 | ): target is ComponentClass { 32 | return target && !!target.prototype?.render; 33 | } 34 | -------------------------------------------------------------------------------- /test/helpers/renderer.ts: -------------------------------------------------------------------------------- 1 | import { Root, createRoot } from "react-dom/client"; 2 | 3 | import { ReactNode } from "react"; 4 | 5 | const container = document.querySelector('.root'); 6 | let root: Root; 7 | 8 | export const $ = (selector: string): T => { 9 | return container?.querySelector(selector) as T; 10 | } 11 | 12 | export function render(element: ReactNode) { 13 | if (!container) return; 14 | if (root) root.unmount(); 15 | root = createRoot(container); 16 | return root.render(element); 17 | } -------------------------------------------------------------------------------- /test/hooks.test.tsx: -------------------------------------------------------------------------------- 1 | import { $, render } from './helpers/renderer'; 2 | import { observable, observer, useAutoRun, useObservable, useWatch } from '../src/'; 3 | 4 | import React from 'react'; 5 | import assert from 'assert'; 6 | 7 | @observable 8 | class DemoModel { 9 | name = 'demo'; 10 | value = 0; 11 | } 12 | 13 | describe('hooks', () => { 14 | 15 | it('useWatch: 观察模型变化', (done) => { 16 | const demo = new DemoModel(); 17 | let runCount = 0; 18 | const DemoView = observer((props: { model: DemoModel }) => { 19 | useWatch(() => demo.value, () => { 20 | runCount++ 21 | }); 22 | const { value } = props.model; 23 | return
{value}
; 24 | }); 25 | render(); 26 | setTimeout(() => { 27 | assert.strictEqual($("#value").innerHTML, '0'); 28 | demo.value = 1; 29 | setTimeout(() => { 30 | assert.strictEqual($("#value").innerHTML, '1'); 31 | assert.strictEqual(runCount, 1); 32 | done(); 33 | }, 100); 34 | }, 100); 35 | }); 36 | 37 | it('useWatch: 观察模型变化 & 立即自动执行一次', (done) => { 38 | const demo = new DemoModel(); 39 | let runCount = 0; 40 | const DemoView = observer((props: { model: DemoModel }) => { 41 | useWatch(() => demo.value, () => { 42 | runCount++ 43 | }, true); 44 | const { value } = props.model; 45 | return
{value}
; 46 | }); 47 | render(); 48 | setTimeout(() => { 49 | assert.strictEqual($("#value").innerHTML, '0'); 50 | demo.value = 1; 51 | setTimeout(() => { 52 | assert.strictEqual($("#value").innerHTML, '1'); 53 | assert.strictEqual(runCount, 2); 54 | done(); 55 | }, 100); 56 | }, 100); 57 | }); 58 | 59 | it('useAutoRun: 模型变化自动执行', (done) => { 60 | const demo = new DemoModel(); 61 | let runCount = 0; 62 | const DemoView = observer((props: { model: DemoModel }) => { 63 | useAutoRun(() => { 64 | console.log(' Print:', demo.value); 65 | runCount++; 66 | }); 67 | const { value } = props.model; 68 | return
{value}
; 69 | }); 70 | render(); 71 | setTimeout(() => { 72 | assert.strictEqual($("#value").innerHTML, '0'); 73 | demo.value = 1; 74 | setTimeout(() => { 75 | assert.strictEqual($("#value").innerHTML, '1'); 76 | assert.strictEqual(runCount, 2); 77 | done(); 78 | }, 100); 79 | }, 100); 80 | }); 81 | 82 | it('useObservable: 在函数组件中声明可观察对象', (done) => { 83 | let model: { value: number }; 84 | const DemoView = observer(() => { 85 | model = useObservable({ value: 0 }); 86 | return
{model.value}
; 87 | }); 88 | render(); 89 | setTimeout(() => { 90 | assert.strictEqual($("#value").innerHTML, '0'); 91 | model.value = 1; 92 | setTimeout(() => { 93 | assert.strictEqual($("#value").innerHTML, '1'); 94 | done(); 95 | }, 100); 96 | }, 100); 97 | }); 98 | 99 | }); -------------------------------------------------------------------------------- /test/input.test.tsx: -------------------------------------------------------------------------------- 1 | import { $, render } from './helpers/renderer'; 2 | import { observable, observer } from '../src/'; 3 | 4 | import React from 'react'; 5 | import assert from 'assert'; 6 | 7 | @observable 8 | class DemoModel { 9 | value = '0'; 10 | } 11 | 12 | describe('input', () => { 13 | 14 | it('Input: 输入处理', (done) => { 15 | const demo = new DemoModel(); 16 | const DemoView = observer(({ model }: { model: DemoModel }) => { 17 | return ( 18 |
19 | model.value = event.target.value} 23 | /> 24 |
25 | ); 26 | }); 27 | render(); 28 | setTimeout(() => { 29 | assert.strictEqual( 30 | $("#value").value, '0' 31 | ); 32 | window.eval(` 33 | const input = document.querySelector("#value"); 34 | input.value = "1"; 35 | const event = new Event('input',{bubbles:true,cancelable:true}); 36 | input.dispatchEvent(event); 37 | `); 38 | setTimeout(() => { 39 | assert.strictEqual( 40 | $("#value").value, '1' 41 | ); 42 | done(); 43 | }, 100); 44 | }, 100); 45 | }); 46 | 47 | }); -------------------------------------------------------------------------------- /test/observer.test.tsx: -------------------------------------------------------------------------------- 1 | import { $, render } from './helpers/renderer'; 2 | import React, { Component } from 'react'; 3 | import { observable, observer } from '../src/'; 4 | 5 | import assert from 'assert'; 6 | 7 | @observable 8 | class DemoModel { 9 | name = 'demo'; 10 | value = 0; 11 | } 12 | 13 | function Label(props: { value: string | number }) { 14 | return
{props.value}
15 | } 16 | 17 | describe('observer', () => { 18 | 19 | it('类组件: 响应模型变化并合并多次更新', (done) => { 20 | const demo = new DemoModel(); 21 | let renderCount = 0; 22 | @observer 23 | class DemoView extends Component<{ model: DemoModel }> { 24 | render() { 25 | renderCount++; 26 | const { value } = this.props.model; 27 | return