使用React 实现一个简单的待办事项列表 | 青训营

85 阅读5分钟

使用React实现Todolist

创建React项目

由于是第一次使用React做项目,所以就从最开始进行记录

  • npm install react
  • npm install react-dom
  • npm create vite@latest
    • project name: todolist
    • Select a frameworl: React
    • Select a variant: TypeScript + SWC
  • cd ./todolist
  • npm install
  • npm run dev
  • 打开http://127.0.0.1:5173/

一些细节

javaScript中通常onchange只会在模糊时触发,或者像当停止改变某些东西的时候(在输入时使用);onInput一般在每次更改输入中的值时使用

image.png

App.jsx

最开始在App.jsx中实现整个todolist案例,随着项目代码的增加,将项目的组件进行封装,分别包括NewItemForm.jsx,TodoList.jsx,TodoItem.jsx

封装组件

1 App.jsx

import './styles.css'
import { useState, useEffect } from 'react'
import { NewTodoForm } from './NewTodoForm.jsx'
import { TodoList } from './TodoList.jsx'
export default function App() {
  const [todos, setTodos] = useState(() => {
    const localValue = localStorage.getItem('ITEMS')
    if (localValue == null) return []

    return JSON.parse(localValue)
  })

  useEffect(() => {
    localStorage.setItem('ITEMS', JSON.stringify(todos))
  }, [todos])

  function addTodo(title) {
    setTodos((currentTodos) => {
      return [...currentTodos, { id: crypto.randomUUID(), title, completed: false }]
    })
  }

  function toggleTodo(id, completed) {
    setTodos((currentTodos) => {
      return currentTodos.map((todo) => {
        if (todo.id === id) {
          // todo.completed = completed
          return { ...todo, completed }
        }
        return todo
      })
    })
  }

  function deleteTodo(id) {
    setTodos((currentTodos) => {
      return currentTodos.filter((todo) => todo.id !== id)
    })
  }
  return (
    <>
      <div className="todo">
        <NewTodoForm onSubmit={addTodo} />
        <h2 className="header">Todo List</h2>
        <TodoList todos={todos} toggleTodo={toggleTodo} deleteTodo={deleteTodo} />
      </div>
    </>
  )
}

2 NewTodoForm.jsx

该.jsx文件主要是展现Form表单及其功能

import { useState } from 'react'

export function NewTodoForm({ onSubmit }) {
  const [newItem, setNewItem] = useState('')

  function handleSubmit(e) {
    e.preventDefault()
    if (newItem === '') return

    onSubmit(newItem)

    setNewItem('')
  }
  return (
    <form onSubmit={handleSubmit} className="new-item-form">
      <div className="form-row">
        <label htmlFor="item" className="new-item-label">
          New Item
        </label>
      </div>
      <div className="add-box">
        <input
          value={newItem}
          onChange={(e) => {
            setNewItem(e.target.value)
          }}
          type="text"
          id="item"
        />
        <button className="btn">Add</button>
      </div>
    </form>
  )
}

3 TodoList.jsx

该文件封装的是Todo List中的任务列表,同时将li任务列表还进行了封装--TodoItem.jsx

import { TodoItem } from './TodoItem.jsx'
export function TodoList({ todos, toggleTodo, deleteTodo }) {
  return (
    <ul className="list">
      {todos.length === 0 && 'No Todos'}
      {todos.map((todo) => {
        // return <TodoItem {...todos} id={todo.id} completed={todo.completed} title={todo.title} key={todo.id}></TodoItem>
        return <TodoItem {...todo} key={todo.id} toggleTodo={toggleTodo} deleteTodo={deleteTodo}></TodoItem>
      })}
    </ul>
  )
}

4 TodoItem.jsx

该.jsx文件主要实现每个任务的展示及其操作,通过父文件传递过来的函数进行选中、删除操作

export function TodoItem({ completed, title, id, toggleTodo, deleteTodo }) {
  return (
    <li key={id}>
      <label>
        <input
          type="checkbox"
          checked={completed}
          onChange={(e) => {
            toggleTodo(id, e.target.checked)
          }}
        />
        {title}
      </label>
      <button className="btn btn-danger" onClick={() => deleteTodo(id)}>
        删除
      </button>
      {/* <button className="btn btn-danger">Delete</button> */}
    </li>
  )
}

回顾使用Vue实现Todolist

todolist的页面
image.png
image.png

清除已完成任务后
image.png

添加任务
image.png

删除全部任务
image.png
该任务基于vue3.0实现,通过路由实现未完成、已完成、全部任务的不同页面及其跳转; 在全部任务页面使用计算属性动态显示已完成任务数量、全部任务数量,以及通过计算属性获取vuex中定义的todolist的数据;
以下展示部分主要代码:

// 全部任务
<template>
  <div>
    <div>
      <h2>全部任务</h2>
    </div>
    <div>
      <nav-header @add="add"></nav-header>
      <nav-main :list="list" @del="del"></nav-main>
      <nav-footer :list="list" @clear="clear" @clearall="clearall"></nav-footer>
    </div>
  </div>
</template>

<script>
import { defineComponent, ref, computed } from 'vue'
import NavMain from '../components/navMain/navMain.vue'
import NavHeader from '../components/navHeader/navHeader.vue'
import NavFooter from '../components/navFooter/navFooter.vue'
import { useStore } from 'vuex'

export default defineComponent({
  name: 'Home',
  props: {},
  components: {
    NavHeader,
    NavMain,
    NavFooter,
  },
  setup() {
    let store = useStore()
    let list = computed(() => {
      return store.state.list
    })

    let value = ref('')
    // 添加任务
    let add = (val) => {
      value.value = val
      // 先判断是否是重复任务
      let flag = true
      list.value.map((item) => {
        if (item.title === value.value) {
          // 有重复任务
          flag = false
          alert('任务已存在')
        }
      })
      // 没有重复的任务
      if (flag) {
        // 调用mutaiton
        store.commit('addTodo', {
          title: value.value,
          complete: false,
        })
      }
    }
    let del = (val) => {
      // 调用删除的mutation
      store.commit('delTodo', val)
    }
    let clear = (val) => {
      store.commit('clear', val)
    }
    let clearall = (val) => {
      store.commit('clearall', val)
    }
    return {
      add,
      del,
      clear,
      clearall,
      value,
      list,
    }
  },
})
</script>

<style scoped lang="scss"></style>

在全部任务页面定义三个子组件:navHeader,navMain,navFooter,通过这样的方式囊括vue3.0的知识点,包括:父子组件之间传参(添加任务、删除已完成任务、按需删除任务)

// navHeader.vue
<template>
  <div>
    <input placeholder="请输入任务名称" v-model="value" />
    <button @click="enter">添加</button>
  </div>
</template>

<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
  name: 'navheader',
  props: {},
  components: {},
  setup(props, ctx) {
    let value = ref('')
    let enter = () => {
      ctx.emit('add', value.value)
      // console.log(value.value)
      value.value = ''
    }
    return {
      value,
      enter,
    }
  },
})
</script>

<style lang="scss" scoped>
input {
  margin: 10px;
}
</style>
// navMain.vue
<template>
  <div v-if="list.length > 0">
    <div v-for="(item, index) in list" :key="index">
      <div class="item">
        <input type="checkbox" v-model="item.complete" />
        {{ item.title }}
        <button class="del" @click="del(item, index)">删除</button>
      </div>
    </div>
  </div>
  <div v-else>暂无任务</div>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'navmain',
  props: {
    list: {
      type: Array,
      required: true,
    },
  },
  emits: ['del'],
  components: {},
  setup(props, ctx) {
    // 删除任务
    let del = (item, index) => {
      ctx.emit('del', index)
    }
    return {
      del,
    }
  },
})
</script>

<style lang="scss" scoped>
.item {
  height: 35px;
  line-height: 35px;
  position: relative;
  cursor: pointer;
  width: 240px;
  margin: auto;
  .del {
    position: absolute;
    right: 10px;
    top: 6px;
    display: none;
    z-index: 9999;
  }
  &:hover {
    background-color: #ddd;
    button {
      display: block;
    }
  }
}
</style>
// navFooter.vue
<template>
  <div class="navfooter-container">
    <div>已完成{{ isComplete }} / 全部{{ list.length }}</div>
    <div v-if="isComplete > 0" class="del_btn">
      <button class="del" @click="clear">清除已完成</button>
      <button class="delall" @click="clearall">清楚全部</button>
    </div>
  </div>
</template>

<script>
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
  name: 'navfooter',
  props: {
    list: {
      type: Array,
      required: true,
    },
  },
  components: {},
  setup(props, ctx) {
    let isComplete = computed(() => {
      // 过滤已完成的任务
      let arr = props.list.filter((item) => {
        return item.complete
      })
      return arr.length
    })
    // 清除已完成
    let clear = () => {
      // 过滤未完成的任务
      let arr = props.list.filter((item) => {
        return item.complete === false
      })
      ctx.emit('clear', arr)
    }
    // 清除全部
    let clearall = () => {
      // 过滤未完成的任务
      let arr = props.list
      console.log(arr)
      ctx.emit('clearall', arr)
    }
    return {
      isComplete,
      clear,
      clearall,
    }
  },
})
</script>

<style scoped lang="scss">
.navfooter-container {
  display: flex;
  width: 500px;
  margin: 0 auto;
  justify-content: center;
  align-items: center;
  .del_btn {
    margin-left: 50px;
  }
}
</style>
// store.js
import { createStore } from 'vuex'

export default createStore({
  // state定义所需要的状态
  state: {
    list: [
      {
        title: '吃饭',
        complete: false,
      },
      {
        title: '学习前端',
        complete: false,
      },
      {
        title: '行测回顾',
        complete: false,
      },
    ],
  },
  getters: {},
  // mutations是用于同步修改state,都是方法
  // 第一个参数state 第二个参数是需要修改的值
  mutations: {
    setName(state, payload) {
      // state.name = payload
      state.name = payload
    },
    addTodo(state, payload) {
      state.list.push(payload)
    },
    // 删除任务 splice(下标, 个数)方法
    delTodo(state, payload) {
      state.list.splice(payload, 1)
    },
    // 清除已完成
    clear(state, payload) {
      // 把过滤之后的数组传进来
      state.list = payload
    },
    clearall(state, payload) {
      // 把过滤之后的数组传进来
      state.list = []
    },
  },
  // 异步提交mutation
  // 第一个参数是store,第二个参数是修改的值
  actions: {
    // asyncSetName(store, params) {
    //   setTimeout(() => {
    //     // commit是提交mutation,调用mutation的方法
    //     store.commit('setName', params)
    //   }, 2000)
    // },
  },
  // 模块化
  modules: {},
})
// router.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

// 路由的配置数组
// path: 路由路径 必须以/开头
// component: 对应的路由组件
// name: 路由名字
const routes = [
  {
    path: '/',
    name: 'start',
  },
  {
    path: '/start',
    name: 'start',
    component: () => import('../views/StartView.vue'),
  },
  {
    path: '/home',
    name: 'home',
    component: HomeView,
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('../views/AboutView.vue'),
  },
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
})

export default router

总结

在通过使用Vue和React分别实现Todolist案例的过程中,我更加深刻理解了Vue以及React的区别和优缺点。
Vue和React的区别:

  • React的思路是HTML in JavaScript,通过javascript生成HTML,所以设计的jsx语法,还有通过JS来操作CSS等;Vue则是把HTML、CSS、JavaScript组合到一起,用各自的处理方式,Vue有单文件组件,可以把HTML、CSS、JS写到一个文件中。可以了解到,Vue更适合从三组件过渡到框架的使用,但是React更注重对JavaScript的使用;

  • React的生命周期与Vue的生命周期不同:

  • Diff算法不同:Diff算法是一种对比算法,主要是对比旧的虚拟DOM和新的虚拟DOmain,找出发生更改的节点,只更新更改的节点,而不更新未发生变化的节点,从而准确的更新DOM,减少操作真是DOM的次数,提高性能。

    • 1、vue节点对比,如果节点元素类型相同,但className不同,认为是不同类型的元素,会进行删除重建,但react则会认为是同类型的节点,只会修改节点属性。
    • 2、vue的列表比对采用的是首尾指针法,而react采用的是从左到右依次比对的方式,当一个集合只是把最后一个节点移动到了第一个,react会把前面的节点依次移动,而vue只会把最后一个节点移动到最后一个,从这点上来说vue的对比方式更加高效。
  • 响应式原理不同

    • React主要是通过setState()方法更新状态,状态更新之后,组件也会重新渲染
    • Vue会遍历data数据对象,使用Object.definedProperty()将每个属性都转换为getter和setter,每个Vue组件实例都有一个对应的watcher实例,在组件初次渲染的时候会记录组件用到了哪些数据。当数据发生改变的时候,会触发setter方法,并通知所有依赖这个数据的watcher实例调用update方法去触发组件的compile渲染方法,进行渲染数据。
  • 封装程度不同

    • vue封装成都更高,内置多个指令和数据双向绑定,react封装程度比较低,适合扩展