Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
init commit: replace transducer with most
- Loading branch information
0 parents
commit 499de2a
Showing
26 changed files
with
1,052 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# editorconfig.org | ||
root = true | ||
|
||
[*] | ||
indent_style = space | ||
indent_size = 2 | ||
end_of_line = lf | ||
charset = utf-8 | ||
trim_trailing_whitespace = true | ||
insert_final_newline = true | ||
|
||
[*.md] | ||
trim_trailing_whitespace = false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# Created by https://www.gitignore.io/api/node | ||
|
||
### Node ### | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
##########idea | ||
.idea/ | ||
# Runtime data | ||
pids | ||
*.pid | ||
*.seed | ||
|
||
# Directory for instrumented libs generated by jscoverage/JSCover | ||
lib-cov | ||
|
||
# Coverage directory used by tools like istanbul | ||
coverage | ||
|
||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) | ||
.grunt | ||
|
||
# node-waf configuration | ||
.lock-wscript | ||
|
||
# Compiled binary addons (http://nodejs.org/api/addons.html) | ||
build/Release | ||
|
||
# Dependency directory | ||
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git | ||
node_modules | ||
|
||
examples/todomvc/public/* | ||
!examples/todomvc/public/index.html | ||
|
||
|
||
transdux.js | ||
public |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/** | ||
The MIT License (MIT) | ||
|
||
Copyright (c) 2015 Jichao Ouyang | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
* Transdux | ||
|
||
I'm trying to documenting in more detail, but if you have any question, feel free to | ||
#+ATTR_HTML: title="Join the chat at https://gitter.im/jcouyang/transdux" | ||
[[https://gitter.im/jcouyang/transdux?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge][file:https://badges.gitter.im/Join%20Chat.svg]] | ||
|
||
Circle CI [[https://circleci.com/gh/jcouyang/transdux][https://circleci.com/gh/jcouyang/transdux.svg?style=svg]] | ||
|
||
[[http://blog.oyanglul.us/javascript/react-transdux-the-clojure-approach-of-flux.html][>中文<]] | ||
|
||
#+BEGIN_QUOTE | ||
Finnally a flux like framework don't even need a tutorial, our [[./examples/todomvc][TodoMVC]] will tell you all. | ||
#+END_QUOTE | ||
|
||
Managing React Component state in *Elegant & Functional* way with transducers and channels from ClojureScript | ||
|
||
** Rationale | ||
flux and redux are great and solve state management and communication pretty well. | ||
|
||
*But* they are too complicated, users have to know toomany things about store, dispatcher, actions which they *shouldn't* | ||
|
||
In reality what we have to do is actually just need to *talk* to *whatever* component I like and get my state from one *source of truth*, so the simplest way to do this is: | ||
|
||
For component who has *actions*, only thing it have to define is what can it do. | ||
|
||
For component who want to call other component's action, it directly *dispatch* a message to that component. | ||
|
||
SO, all user have to know is to | ||
- define *actions* for your component | ||
- *dispatch* messages to *any component* you want to talk to | ||
|
||
and leave all the other *dirty works* (stores, states) to our functional transducers, channels and pubsub that user don't really need to care about. | ||
|
||
*The Big Picture* | ||
[[https://www.evernote.com/l/ABe_8eE6o2dGlZMCmNnBap_fXy83GvJe6gcB/image.jpg]] | ||
|
||
We're using Channels/Actors Model from Clojure -- core.async, It's compile to JS by [[http://github.com/jcouyang/conjs][conjs]] | ||
|
||
*Basic Idea* | ||
we composed channels, subs, pubs, and transducers together like tunnels, every message goes through the tunnel will got process by the transducer with action function user provided. | ||
|
||
so, whenever a message is dispatch to input channel, you'll get new state from the corresponding output channel. you don't need to care about how the dispatching really happen in the framework. | ||
|
||
** Install | ||
In your React project | ||
#+BEGIN_SRC sh | ||
npm install transdux --save | ||
#+END_SRC | ||
|
||
** Usage | ||
to wire in transdux, only 4 place you have to pay attention to. | ||
*** 1. wrap you app with Transdux | ||
#+BEGIN_SRC html | ||
<Transdux> | ||
<App/> | ||
</Transdux> | ||
#+END_SRC | ||
*** 2. define what your component can do | ||
#+BEGIN_SRC js | ||
// MainSection.jsx | ||
let actions = { | ||
complete(msg, state){ | ||
return { | ||
todos:state.todos.map(todo=>{ | ||
if(todo.id==msg.id) | ||
todo.completed = !todo.completed | ||
return todo | ||
}) | ||
} | ||
}, | ||
clear(msg,state){ | ||
return { | ||
todos: state.todos.filter(todo=>todo.completed==false) | ||
} | ||
} | ||
} | ||
#+END_SRC | ||
*** for Mixin lover | ||
**** 3. mixin Transdux Mixin and Bind Actions | ||
#+BEGIN_SRC js | ||
// MainSection.jsx | ||
import {TxMixin} from 'transdux' | ||
let MainSection = React.createClass({ | ||
mixins: [TxMixin], | ||
componentDidMount(){ | ||
this.bindActions(actions) | ||
}, | ||
... | ||
}) | ||
|
||
#+END_SRC | ||
|
||
**** 4. mixin and dispatch a message | ||
#+BEGIN_SRC jsx | ||
//TodoItem.jsx | ||
import MainSection from './MainSection' | ||
let TodoItem = React.createClass({ | ||
mixins: [TxMixin], | ||
render(){ | ||
<input className="toggle" | ||
type="checkbox" | ||
checked={todo.completed} | ||
onChange={() => this.dispatch(MainSection, 'complete',{id:todo.id})} /> | ||
|
||
} | ||
}) | ||
#+END_SRC | ||
|
||
*** for ES6 class lover | ||
**** 3. mixin transdux into Class | ||
#+BEGIN_SRC js | ||
// TodoItem.jsx | ||
import {mixin} from 'transdux' | ||
let actions = { | ||
... | ||
} | ||
class TodoItem extends React.Component { | ||
constructor(props){ | ||
super(props); | ||
this.state = {editing:false}; | ||
} | ||
... | ||
} | ||
export default mixin(TodoItem, actions) | ||
|
||
#+END_SRC | ||
|
||
**** 4. dispatch a message | ||
#+BEGIN_SRC jsx | ||
//TodoItem.jsx | ||
import MainSection from './MainSection' | ||
class TodoItem extends React.Component { | ||
... | ||
render(){ | ||
<input className="toggle" | ||
type="checkbox" | ||
checked={todo.completed} | ||
onChange={() => this.dispatch(MainSection, 'complete',{id:todo.id})} /> | ||
|
||
} | ||
... | ||
}) | ||
export default mixin(TodoItem) | ||
#+END_SRC | ||
** Examples | ||
- [[http://oyanglul.us/transdux/todomvc/][todomvc]] | ||
- source: [[./examples]] | ||
|
||
** API | ||
[[./docs/api.org]] | ||
|
||
** Performance | ||
for dispatching *1023 messages* at the same time, here is the Memory Usage and Time elapsed | ||
|
||
tested on /Macbook Pro 13, CPU 2.9GHz Intel Core i5, Mem 16GB 1867MHz DDR3/ | ||
|
||
*** transdux | ||
#+BEGIN_EXAMPLE | ||
Memory Usage Before: { rss: 43307008, heapTotal: 18550784, heapUsed: 11889192 } | ||
Memory Usage After: { rss: 46444544, heapTotal: 30921984, heapUsed: 15307800 } | ||
Elapsed 51ms | ||
#+END_EXAMPLE | ||
|
||
*** setTimeout | ||
#+BEGIN_EXAMPLE | ||
Memory Usage Before: { rss: 45432832, heapTotal: 17518848, heapUsed: 12664416 } | ||
Memory Usage After: { rss: 46772224, heapTotal: 19570688, heapUsed: 10927824 } | ||
Elapsed 7ms | ||
#+END_EXAMPLE | ||
|
||
*** redux | ||
#+BEGIN_EXAMPLE | ||
Memory Usage Before: { rss: 21647360, heapTotal: 9275392, heapUsed: 4559616 } | ||
Memory Usage After: { rss: 22638592, heapTotal: 9275392, heapUsed: 5472112 } | ||
Elapsed 4ms | ||
#+END_EXAMPLE | ||
|
||
Yeah, I know, it's slower then redux, and I'm working on it. | ||
But, it's not bad, it's totally reasonable trade-off a little performance to get writing code which is more composable, reusable, testable and easy to reason about. | ||
|
||
** TODOS | ||
[[./ROADMAP.org]] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
* RoadMap [33%] | ||
|
||
- [X] using Atom managing state | ||
- [-] smoke testing | ||
- [X] benchmark | ||
- [ ] smoke | ||
- [ ] unsubscribe when component unmount | ||
- [ ] improve performance | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
var createStore = require('redux').createStore | ||
var timer = require('./timer') | ||
var time = timer.time | ||
var CYCLE = timer.CYCLE | ||
var immutable = require('immutable') | ||
var initState = [0] | ||
for(var i=0;i<1000;i++) | ||
initState.push(i) | ||
|
||
function counter(state, action) { | ||
state=immutable.fromJS(state||initState) | ||
switch (action.type) { | ||
case 'INCREMENT': | ||
return state.map(function(item){return item+1}).toJS() | ||
} | ||
} | ||
|
||
var store = createStore(counter) | ||
|
||
time(function(done){ | ||
store.subscribe(() => { | ||
done(store.getState()[0]) | ||
}) | ||
for(var i=0;i<CYCLE+1;i++){ | ||
store.dispatch({ type: 'INCREMENT' }); | ||
} | ||
}) | ||
/** | ||
Memory Usage Before: { rss: 21942272, heapTotal: 9275392, heapUsed: 4559784 } | ||
Memory Usage After: { rss: 22929408, heapTotal: 9275392, heapUsed: 5473240 } | ||
Elapsed 4ms | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
var createStore = require('redux').createStore | ||
var timer = require('./timer') | ||
var time = timer.time | ||
var CYCLE = timer.CYCLE | ||
|
||
function counter(state, action) { | ||
state=state||0 | ||
switch (action.type) { | ||
case 'INCREMENT': | ||
return state + 1 | ||
} | ||
} | ||
|
||
var store = createStore(counter) | ||
|
||
time(function(done){ | ||
store.subscribe(() => { | ||
done(store.getState()) | ||
}) | ||
for(var i=0;i<CYCLE+1;i++){ | ||
store.dispatch({ type: 'INCREMENT' }); | ||
} | ||
}) | ||
/** | ||
Memory Usage Before: { rss: 21942272, heapTotal: 9275392, heapUsed: 4559784 } | ||
Memory Usage After: { rss: 22929408, heapTotal: 9275392, heapUsed: 5473240 } | ||
Elapsed 4ms | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
var CYCLE = 1023 | ||
exports.time = function(f) { | ||
console.log('Memory Usage Before:', process.memoryUsage()) | ||
var s = new Date(); | ||
f(function(index){ | ||
if(index==CYCLE){ | ||
console.log('Memory Usage After:', process.memoryUsage()) | ||
console.log("Elapsed "+((new Date()).valueOf()-s.valueOf())+"ms"); | ||
} | ||
|
||
}); | ||
} | ||
exports.CYCLE = CYCLE |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
var conjs = require('con.js'); | ||
var tx = require('../transdux.js') | ||
var txmixin = tx.TxMixin | ||
var timer = require('./timer') | ||
var time = timer.time | ||
var CYCLE = timer.CYCLE | ||
var toJs = conjs.toJs | ||
var toClj = conjs.toClj | ||
var inputChan = conjs.async.chan() | ||
var outputChan = conjs.async.chan() | ||
var context = { | ||
transduxChannel: inputChan, | ||
transduxPublication: conjs.async.pub(inputChan, function(_){return _['action']}), | ||
} | ||
|
||
var initState = [0] | ||
for(var i=0;i<1000;i++) | ||
initState.push(i) | ||
|
||
time(function(done){ | ||
function Target(){ | ||
return { | ||
getInitialState: function(){return initState}, | ||
context:context, | ||
state: [0], | ||
setState: function(f){ | ||
this.state=f(this.state) | ||
done(this.state[0]) | ||
}, | ||
constructor: Target | ||
} | ||
} | ||
var target = new Target() | ||
txmixin.bindActions.call(target, { | ||
increment: function(msg,state){ | ||
return conjs.map(function(m){return msg+1}, state) | ||
} | ||
}, toClj, toJs) | ||
|
||
for(var i=0;i<CYCLE+1;i++){ | ||
txmixin.dispatch.call(target, Target, 'increment', i) | ||
} | ||
}) |
Oops, something went wrong.