[FE301] React 基礎(Class component 版)先別急著學 React


Posted by s103071049 on 2021-08-31

目標:用 jquery 實現一個簡單的 todoList

環境設置

  1. 引入 js
  2. 引入 jquery

切板

  1. 輸入窗 + 畫面置中
  2. 篩選區塊
  3. todo 區塊

功能

  1. 新增 todo
  2. 標示完成與否 (hint:事件代理 / jquery 不傳參數是 get、傳參數是 set)
  3. filter (hint:取資料將他顯示或隱藏 / className)
  4. 刪除
$(document).ready(function() {
  // 新增功能
  $('.add-todo').click(function() {
    const text = $('input[name=todo]').val()
    if (text) {
      $('.list').append(
        ` <div class="list-item">
            <div class="list-item__state">X</div>
            <div class="list-item__content">${text}</div>
            <div class="list-item__action">
              <button class="list-item__delete">刪除</button>
              <button class="list-item__mark">標示成已完成</button>
            </div>
          </div>`
      )
    } else {
      alert('請輸入文字')
    }
    $('input[name=todo]').val('')
  })
  // 標記功能
  $('.list').on('click', '.list-item__mark', function(e) {
    const $item = $(e.target)
    const state = $item.parent().parent().find('.list-item__state').text()
    if (state === 'X') {
      $item.parent().parent().find('.list-item__state').text('O')
      $item.parent().parent().addClass('completed')
      $item.text('標示成未完成')
    } else {
      $item.parent().parent().find('.list-item__state').text('X')
      $item.parent().parent().removeClass('completed')
      $item.text('標示成完成')
    }
  })
  // filter 功能
  $('.filters__all').click(function() {
    $('.list-item').show()
  })

  $('.filters__completed').click(function() {
    $('.list-item').hide()
    $('.list-item.completed').show()
  })
  // 刪除功能
  $('.list').on('click', '.list-item__delete', function(e) {
    $(e.target).parent().parent().remove()
  })
})

把 todo list 給存起來

想要重新整理後,todo 存在瀏覽器的 local storage 裡面。
麻煩點:處理狀態同步

let todos = []
let id = 0

// todos 放到 localStorage
function setData() {
  window.localStorage.setItem('todoapp', JSON.stringify(todos))
}
$(document).ready(function() {
  // 剛啟動要把東西從 todos 拿出來
  todoData = window.localStorage.getItem('todoapp')
  if (todoData) {
    todos = JSON.parse(todoData)
    for (let i = 0; i < todos.length; i++) {
      $('.list').append(
      `
      <div class="list__item ${todos[i].isCompleted ? 'completed' : ''}" data-id="${todos[i].id}">
        <div class="isCompleted">${todos[i].isCompleted ? 'O' : 'X'}</div>
        <div class="list__content">${todos[i].content}</div>
        <div class="list__btn">
          <button class="list__delete">刪除</button>
          <button class="list__mark">${todos[i].isCompleted ? '標示完成' : '標示成未完成'}</button>
        </div>
      </div>
      `
      )
    }
    if (todos.length > 0) {
      id = todos[todos.length -1 ].id + 1
    }
  }

  // 新增
  $('.addTodo').on('click', () => {
    const content = $('input[name=content]').val()
    if (content) {
      const innerHtml = 
      `
      <div class="list__item" data-id="${id}">
        <div class="isCompleted">X</div>
        <div class="list__content">${content}</div>
        <div class="list__btn">
          <button class="list__delete">刪除</button>
          <button class="list__mark">標示成未完成</button>
        </div>
      </div>
      `
      $('.list').append(innerHtml)
      $('input[name=content]').val('')
      todos.push({
        id,
        content,
        isCompleted: false
      })
      id ++
      setData()
    } else {
      alert('請輸入 todos')
    }
  })

  // 刪除
  $('.list').on('click', '.list__delete', (e) => {
    $(e.target).parent().parent().remove()
    // todos 要找到刪除的那個東西,很困難,但可以藉由 id 的幫忙
    const id = $(e.target).parent().parent().attr('data-id')
    todos = todos.filter(todo => todo.id !== Number(id))
    setData()
  })
  // 標示
  $('.list').on('click', '.list__mark', (e) => {
    //const text = $(e.target).text()
    if ($(e.target).parent().parent().find('.isCompleted').text() === 'X') {
      $(e.target).text('標示完成')
      $(e.target).parent().parent().find('.isCompleted').text('O')
      $(e.target).parent().parent().addClass('completed')
    } else {
      $(e.target).text('標示成未完成')
      $(e.target).parent().parent().find('.isCompleted').text('X')
      $(e.target).parent().parent().removeClass('completed')
    }
    //map() 方法會建立一個新的陣列,其內容為原陣列的每一個元素經由回呼函式運算後所回傳的結果之集合。
    const id = $(e.target).parent().parent().attr('data-id')
    todos = todos.map(todo => {
      if (todo.id !== Number(id)) {
        return todo
      }
      return {
        ...todo,
        isCompleted: !todo.isCompleted
      }
    })
    setData()
  })
  // filter
  // filter 1 show all
  $('.filter').on('click', '.filter__all', (e) => {
    $('.list__item').show()
  })
  // filter 2 show completed (需要練習)
  $('.filter').on('click', '.filter__completed', (e) => {
    $('.list__item').hide()
    $('.list__item.completed').show() // 透過 completed 這個 class 去達到
  })
})

問題點

  1. code 重複 => 必須在每次資料變動 1) 去操控 todos 2) 改 state
  2. 每次操作都要進行 一、畫面處理 二、資料處理 三、資料保存

因為資料與畫面是分開的,所以某個步驟沒有處理好時,就會造成畫面與資料不同步。
解決方法:把每次都當作第一次。舉例:刪除資料別管畫面,直接改變資料,改變資料完後一樣 call setData() 將東西存入,再啟動將東西從 todo 拿出來,依據目前的狀態 render 出頁面。

因為畫面永遠是從 state 產生,所以可以保證結果永遠正確。

1. add todo, todos: [...]
2. 清空畫面,render
3. 標示為已完成, todos: [...]
4. 清空畫面,render

把每次當作第一次:來寫一個不一樣的 todo list!

狀態讀出來、跑 render。

優:不用想狀態怎麼改變,換句話說解決狀態改變的問題。state 與 UI 為一對一關係,UI 永遠是從狀態產生出來不會有不同步的問題。

缺:效能差。為了保證狀態與畫面對應,改變任何一列,都會把所有東西清空再重新 render 一遍。

因為每次操作都要做 一、更新畫面 二、更新資料 三、儲存資料,所以在儲存資料就可以順便做更新畫面。所以只要改變 todos call setData( )

let todos = []
let id = 0

function setData() {
  render(todos) // 將現在的 todos 傳給他
  window.localStorage.setItem('todoapp', JSON.stringify(todos))  
}
//
function render(todoList) {
  console.log('render', todoList)
  $('.list').empty() // 開始前先將畫面清空
  for (let i = 0; i < todoList.length; i++) { // 根據 todoList 最新的資料,將畫面 render 出來
    $('.list').append(// 已經決定好所有的狀態,所以可以將每次都當成第一次
      ` 
      <div class="list__item ${todoList[i].isCompleted ? 'completed' : ''}" data-id="${todoList[i].id}">
        <div class="isCompleted">${todoList[i].isCompleted ? 'O' : 'X'}</div>
        <div>${todoList[i].content}</div>
        <div class="list__btn">
          <button class="list__mark">${todoList[i].isCompleted ? '未完成' : '已完成'}</button>
          <button class="list__delete">刪除</button>
        </div>
      </div>
    `)
  }
}
$(document).ready(() => {
  // 將東西從 localStorage 拿出來
  const todoData = window.localStorage.getItem('todoapp')
  if (todoData) {
    todos = JSON.parse(todoData)
    render(todos)
    if (todos.length > 0) {
      id = todos[todos.length - 1].id + 1
    }
  }
  // 新增
  $('.addTodo').on('click', () => {
    const content = $('input[name = content]').val()
    if (content) {
     todos.push({
        id,
        content,
        isCompleted: false
     })
     setData() // 有最新的 todos
     $('input[name = content]').val('')
     id++

    } else {
      alert('請輸入 todo ')
    }
  })
  // 刪除 (容易寫錯)
  /* 因為要指定刪除的那個結點,所以 e 要從 list__delete 出發*/
  $('.list').on('click', '.list__delete',(e) => {
    id = $(e.target).parent().parent().attr('data-id')
    todos = todos.filter(todo => // filter 的語法,裡面是條件,放 { } 會出錯
      todo.id !== Number(id)
    )
    setData()
  })
  // mark
  $('.list').on('click', '.list__mark' , (e) => {
    const mark = $(e.target).parent().parent().find('.isCompleted').text()
    id = $(e.target).parent().parent().attr('data-id')
    todos = todos.map(todo => { //  arr.map(function callback( currentValue[, index[, array]])
      if (todo.id !== Number(id)) {
        return todo
      }
      return {
        ...todo,
        isCompleted: !todo.isCompleted
      }
    })
    setData()
  })
  // filter
  // 1. show all
  $('.filter__all').on('click', (e) => {
    $('.list__item').show()
  })
  // 2. show completed
  $('.filter__completed').on('click', (e) => {
    $('.list__item').hide()
    $('.list__item.completed').show()
  })
})

其實這就是 React

React 裡面會有個 state,在上面就是 todos = [ ],他永遠會對應到一個 UI,當改變 state,會進行 re-render。

核心概念:想改變畫面,不會直接改變畫面(操控 dom 上的東西),只會改變 state。










Related Posts

關於 React 小書:dangerouslySetInnerHTML & style

關於 React 小書:dangerouslySetInnerHTML & style

1 Even or Odd 8 kyu

1 Even or Odd 8 kyu

給每個Python專案一個乾淨獨立的環境

給每個Python專案一個乾淨獨立的環境


Comments