我们掌握了react的基本语法,我们尝试实现一个todos的案例.
素材
- 官网: https://todomvc.com
- 模板下载: https://github.com/tastejs/todomvc-app-template
- 文档可能和代码不一致,以随堂代码为准:
- 随堂代码: https://gitee.com/bufanxy/react-todo-mvc
功能实现
修改解构
- 实现新增
- 实现列表展示
- 实现是否完成状态切换
- 实现item left
- 全选/取消全选
- 注意setState异步问题
- 双击编辑
- 注意取消还原的问题
- 删除
- 状态过滤
- 清除完成

代码部分:
class Todos extends React.Component {constructor(props){super(props);this.state = {newTodoValue : '',todoList: [],itemLeft: 0 , // 未完成剩余数量allCheckState: false, // 全选状态preEditValue: '',// 备份编辑前的内容fitlerState: 'all', //all, active, completed}this.handleNewTodo = this.handleNewTodo.bind(this);this.handleNewTodoEnter = this.handleNewTodoEnter.bind(this);this.regetItemLeft = this.regetItemLeft.bind(this);this.handleCheckAll = this.handleCheckAll.bind(this);this.handleClearCompleted = this.handleClearCompleted.bind(this);}// 处理新增表单valuehandleNewTodo(e){this.setState({newTodoValue: e.target.value})}// 处理新增事件handleNewTodoEnter(e){// 内容为空 不处理if(!e.target.value) return;if(e.keyCode === 13){// 复制todoList临时变量var todoList = [...this.state.todoList];// 构建todo对象var todo = {id: new Date().getTime(),isEdit: false,text: e.target.value,isDone: false,}todoList.push(todo);//更新到todoListhis.setState({todoList});// 重新计算剩余数量this.regetItemLeft();// 清除新增输入框内容this.setState({newTodoValue: ''})}// esc 取消if(e.keyCode === 27){this.setState({newTodoValue: ''})}}// 处理全选handleCheckAll(){if(this.state.todoList.length == 0) return;// 注意: setState是异步的,必须在第二个回调才能获取最新状态// # https://react.docschina.org/docs/state-and-lifecycle.htmlthis.setState(state=>({allCheckState: !state.allCheckState}),()=>{// 更改每一项var todoList = [...this.state.todoList];todoList.map(item=>{item.isDone = this.state.allCheckState;return item;})console.log('todoList',todoList)//this.setState(state=>({todoList}))// // 更新剩余数量this.regetItemLeft();})}// 某一行的checkbox被修改handleCheckOne(id,e){var todoList = [...this.state.todoList];// 找到目标元素下标var index = todoList.findIndex(item=>item.id == id);todoList[index].isDone = !todoList[index].isDone;// 更新this.setState({todoList})// 计算剩余未完成数量this.regetItemLeft();}// 编辑行数据变化handleEditChange(id,e){var todoList = [...this.state.todoList];var index = todoList.findIndex(item=>item.id == id);todoList[index].text = e.target.value;this.setState({todoList})}//双击编辑handleEdit(id,e){var todoList = [...this.state.todoList];// 排他 只能有一个处于编辑状态var beforeEditIndex = todoList.findIndex(item=>item.isEdit);if(beforeEditIndex>-1){todoList[beforeEditIndex].isEdit = false;}// 找到目标元素下标var index = todoList.findIndex(item=>item.id == id);todoList[index].isEdit = true;this.setState({todoList},()=>{// 通过dom关系找到目标元素e.target.parentNode.nextElementSibling.focus();})// 备份旧内容 用于esc取消还原this.setState({preEditValue: todoList[index].text})}// 编辑行确定handleOkOrCancel(id,e){var todoList = [...this.state.todoList];// 找到目标元素下标var index = todoList.findIndex(item=>item.id == id);// 确定是esc还是enterif(e.keyCode === 13){todoList[index].isEdit = false;this.setState({todoList,preEditValue: ''})}else if(e.keyCode === 27){// 还原todoList[index].text = this.state.preEditValue;todoList[index].isEdit = false;this.setState({todoList,preEditValue: ''})}}// 计算剩余regetItemLeft(){// 如果没有数据if(this.state.todoList.length == 0){// 处理全选this.setState({allCheckState: false})return;}// 设置剩余长度var itemLefts = this.state.todoList.filter(item=>!item.isDone);this.setState({itemLeft: itemLefts.length})// 响应全选this.setState({allCheckState: itemLefts.length==0})}// 删除removeItem(id){var todoList = [...this.state.todoList];// 找到目标元素下标var index = todoList.findIndex(item=>item.id == id);todoList.splice(index,1);this.setState({todoList})}// 改变filterStatechangeFilterState(state){this.setState({fitlerState: state})}// 清除所有完成的handleClearCompleted(){var todoList = [...this.state.todoList];todoList = todoList.filter(item=>!item.isDone);this.setState({todoList})}render() {return (<section className="todoapp"><header className="header"><h1>todos</h1><inputclassName="new-todo"placeholder="What needs to be done?"autoFocusvalue = {this.state.newTodoValue}onChange = {this.handleNewTodo}onKeyUp= {this.handleNewTodoEnter}/></header>{/* This section should be hidden by default and shown when there are todos */}<section className="main"><input id="toggle-all" className="toggle-all" type="checkbox" checked={this.state.allCheckState} onChange={this.handleCheckAll}/><label htmlFor="toggle-all">Mark all as complete</label><ul className="todo-list">{/* These are here just to show the structure of the list items */}{/* List items should get the class `editing` when editing and `completed` when marked as completed */}{this.state.todoList.map(item=>{// 待办if(this.state.fitlerState == 'active'){if(item.isDone) return null;}// 已完成if(this.state.fitlerState == 'completed'){if(!item.isDone) return null;}return(<li className={item.isDone?'completed':''} onDoubleClick={e=>this.handleEdit(item.id,e)} key={item.id}><div className="view" style={{display: item.isEdit?'none':'block'}}><input className="toggle" type="checkbox" checked={item.isDone} onChange={e=>this.handleCheckOne(item.id,e)}/><label>{item.text}</label><button className="destroy" onClick={e=>this.removeItem(item.id)}></button></div><input className="edit" onKeyUp={e=>this.handleOkOrCancel(item.id,e)} style={{display: !item.isEdit?'none':'block'}} value={item.text} onChange={e=>this.handleEditChange(item.id,e)} /></li>)})}</ul></section>{/* This footer should be hidden by default and shown when there are todos */}<footer className="footer">{/* This should be `0 items left` by default */}<span className="todo-count"><strong>{this.state.itemLeft}</strong> item left</span>{/* Remove this if you don't implement routing */}<ul className="filters"><li onClick={e=>this.changeFilterState('all')}><a className={this.state.fitlerState=='all'?'selected':''} href="#/">All</a></li><li onClick={e=>this.changeFilterState('active')}><a className={this.state.fitlerState=='active'?'selected':''} href="#/active">Active</a></li><li onClick={e=>this.changeFilterState('completed')}><a className={this.state.fitlerState=='completed'?'selected':''} href="#/completed">Completed</a></li></ul>{/* Hidden if no completed items are left ↓ */}<button className="clear-completed" onClick={this.handleClearCompleted}>Clear completed</button></footer></section>);}}ReactDOM.render(<Todos/>,document.getElementById('app'));
封装组件
拆分组件
把功能拆分为 <Todos /> <TodoItem /> <FooterBar /> 三个组件.
其实todos 这个例子并不适合我们这样组件化,因为组件和组件之间存在大量的数据联动,我们在处理某个组件的变量的同时需要同时考虑其他组件的变量,这个不仅增加了代码量,而且提高了代码逻辑的复杂度.那这里为什么要封装组件呢?
但通过这个案例,可充分体现react的设计思路:
- 提高代码复用性
- 体现react组件开发的优势
- 体会并理解react 单向数据流 , 自上而下 的state 思想
- 关于state:
状态提升:
通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。让我们看看它是如何运作的。
任何可变数据应当只有一个相对应的唯一“数据源”。通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠自上而下的数据流,而不是尝试在不同组件间同步 state。
// 单个todo组件class TodoItem extends React.Component {constructor(props){super(props);}// 处理一个变化handleCheckOne(id){this.props.handleCheckOne(id)}// 双击编辑handleEdit(id,e){// 通过dom关系找到目标元素var target = e.target.parentNode.nextElementSibling;this.props.handleEdit(id,target);}handleEditChange(id,e){var value = e.target.value;this.props.handleEditInputChange(id,value);}handleOkOrCancel(id,e){this.props.handleEditOkOrCancel(id,e);}render(){return(<li className={this.props.isDone?'completed':''}><div className="view" style={{display: this.props.isEdit?'none':'block'}}><input className="toggle" type="checkbox" checked={this.props.isDone} onChange={e=>this.handleCheckOne(this.props.id,e)}/><label onDoubleClick={e=>this.handleEdit(this.props.id,e)}>{this.props.text}</label><button className="destroy" onClick={e=>this.props.removeItem(this.props.id)}></button></div><input className="edit" onKeyUp={e=>this.handleOkOrCancel(this.props.id,e)} style={{display: !this.props.isEdit?'none':'block'}} value={this.props.text} onChange={e=>this.handleEditChange(this.props.id,e)} /></li>)}}class FooterBar extends React.Component {constructor(props){super(props);this.changeFilterState=this.changeFilterState.bind(this);this.handleClearCompleted=this.handleClearCompleted.bind(this);}changeFilterState(state){this.props.changeFilterState(state);}handleClearCompleted(){this.props.handleClearCompleted();}render(){return(<footer className="footer">{/* This should be `0 items left` by default */}<span className="todo-count"><strong>{this.props.itemLeft||0}</strong> item left</span>{/* Remove this if you don't implement routing */}<ul className="filters"><li onClick={e=>this.changeFilterState('all')}><a className={this.props.fitlerState=='all'?'selected':''} href="#/">All</a></li><li onClick={e=>this.changeFilterState('active')}><a className={this.props.fitlerState=='active'?'selected':''} href="#/active">Active</a></li><li onClick={e=>this.changeFilterState('completed')}><a className={this.props.fitlerState=='completed'?'selected':''} href="#/completed">Completed</a></li></ul>{/* Hidden if no completed items are left ↓ */}<button className="clear-completed" onClick={this.handleClearCompleted}>Clear completed</button></footer>)}}class Todos extends React.Component {constructor(props){super(props);this.state = {newTodoValue : '',todoList: [],itemLeft: 0 , // 未完成剩余数量allCheckState: false, // 全选状态preEditValue: '',// 备份编辑前的内容fitlerState: 'all', //all, active, completed}this.handleNewTodo = this.handleNewTodo.bind(this);this.handleNewTodoEnter = this.handleNewTodoEnter.bind(this);this.handleCheckOne = this.handleCheckOne.bind(this);this.handleCheckAll = this.handleCheckAll.bind(this);this.handleEdit = this.handleEdit.bind(this);this.handleEditChange = this.handleEditChange.bind(this);this.handleOkOrCancel = this.handleOkOrCancel.bind(this);this.removeItem = this.removeItem.bind(this);this.changeFilterState = this.changeFilterState.bind(this);this.handleClearCompleted = this.handleClearCompleted.bind(this);}// 响应新增handleNewTodo(e){this.setState({newTodoValue: e.target.value})}// 处理新增handleNewTodoEnter(e){// enter or escif(e.keyCode === 13){var todo = {id: new Date().getTime(),text: e.target.value,isEdit: false,isDone: false}var todoList = [...this.state.todoList];todoList.push(todo);// 添加新增 并修改表单内容// 因为setState是异步的this.setState({todoList,newTodoValue: ''},()=>{this.regetItemLeft();})}else if(e.keyCode === 27){this.setState({newTodoValue: ''})}}// 当修改是否选中状态handleCheckOne(id){console.log('id',id)var todoList = [...this.state.todoList];var todo = todoList.find(item=>item.id == id);todo.isDone = !todo.isDone;// 修改statethis.setState({...todoList})this.regetItemLeft();}// 全选handleCheckAll(){if(this.state.todoList.length == 0) return;// 注意: setState是异步的,必须在第二个回调才能获取最新状态// # https://react.docschina.org/docs/state-and-lifecycle.htmlthis.setState(state=>({allCheckState: !state.allCheckState}),()=>{// 更改每一项var todoList = [...this.state.todoList];todoList.map(item=>{item.isDone = this.state.allCheckState;return item;})//this.setState(state=>({todoList}),()=>{// 更新剩余数量this.regetItemLeft();})})}// 计算剩余regetItemLeft(){// 如果没有数据if(this.state.todoList.length == 0){// 处理全选this.setState({allCheckState: false})return;}// 设置剩余长度var itemLefts = this.state.todoList.filter(item=>!item.isDone);this.setState({itemLeft: itemLefts.length})console.log(itemLefts.length)// 响应全选this.setState({allCheckState: itemLefts.length==0})}//双击编辑handleEdit(id,target){var todoList = [...this.state.todoList];// 排他 只能有一个处于编辑状态var beforeEditIndex = todoList.findIndex(item=>item.isEdit);if(beforeEditIndex>-1){todoList[beforeEditIndex].isEdit = false;}// 找到目标元素下标var index = todoList.findIndex(item=>item.id == id);todoList[index].isEdit = true;this.setState({todoList},()=>{target.focus();})// 备份旧内容 用于esc取消还原this.setState({preEditValue: todoList[index].text})}// 编辑行数据变化handleEditChange(id,value){var todoList = [...this.state.todoList];var index = todoList.findIndex(item=>item.id == id);todoList[index].text = value;this.setState({todoList})}// 编辑行确定handleOkOrCancel(id,e){var todoList = [...this.state.todoList];// 找到目标元素下标var index = todoList.findIndex(item=>item.id == id);// 确定是esc还是enterif(e.keyCode === 13){todoList[index].isEdit = false;this.setState({todoList,preEditValue: ''})}else if(e.keyCode === 27){// 还原todoList[index].text = this.state.preEditValue;todoList[index].isEdit = false;this.setState({todoList,preEditValue: ''})}}removeItem(id){var todoList = [...this.state.todoList];// 找到目标元素下标var index = todoList.findIndex(item=>item.id == id);todoList.splice(index,1);this.setState({todoList})}// 改变filterStatechangeFilterState(state){this.setState({fitlerState: state})}// 清除所有完成的handleClearCompleted(){var todoList = [...this.state.todoList];todoList = todoList.filter(item=>!item.isDone);this.setState({todoList})}render() {return (<section className="todoapp"><header className="header"><h1>todos</h1><inputclassName="new-todo"placeholder="What needs to be done?"autoFocusvalue = {this.state.newTodoValue}onChange = {this.handleNewTodo}onKeyUp= {this.handleNewTodoEnter}/></header>{/* This section should be hidden by default and shown when there are todos */}<section className="main"><input id="toggle-all" className="toggle-all" type="checkbox" checked={this.state.allCheckState} onChange={this.handleCheckAll}/><label htmlFor="toggle-all">Mark all as complete</label><ul className="todo-list">{this.state.todoList.map(item=>{// 待办if(this.state.fitlerState == 'active'){if(item.isDone) return null;}// 已完成if(this.state.fitlerState == 'completed'){if(!item.isDone) return null;}return(/*把整个item对象传入组件*/<TodoItem key={item.id} {...item}handleEdit={this.handleEdit}handleCheckOne={this.handleCheckOne}handleEditInputChange={this.handleEditChange}handleEditOkOrCancel={this.handleOkOrCancel}removeItem={this.removeItem}/>)})}</ul></section><FooterBar fitlerState={this.state.fitlerState} itemLeft={this.state.itemLeft} changeFilterState={this.changeFilterState} handleClearCompleted={this.handleClearCompleted}/></section>);}}ReactDOM.render(<Todos/>,document.getElementById('app'));
什么时候封装组件?
至于什么时候需要组件封装,这个需要我们在以后的开发过程中根据实际情况来体会.这里我个人总结的两个原则:
