[超詳細(xì)] vue入門項(xiàng)目 TodoMVC 實(shí)現(xiàn)和思考

我的 github 項(xiàng)目地址:https://github.com/Nicklaus6/todomvc-vue

如果對你有幫助希望可以點(diǎn)個 star 哦 ~

一、項(xiàng)目初始化

1.下載模板

在存放該項(xiàng)目的目錄下執(zhí)行:

git clone  https://github.com/tastejs/todomvc-app-template.git

2. 安裝依賴

進(jìn)入項(xiàng)目目錄

cd todomvc-vue

在項(xiàng)目目錄下安裝依賴

yarn

3. 引入vue

安裝vue

yarn add vue

index.html 中引入 vue

<script src="node_modules/vue/dist/vue.js"></script>

app.js中創(chuàng)建vue對象

(function (Vue) {
    window.app = new Vue({
        el:"#todoapp",
    })
})(Vue)

并在index.html中將其掛載到 DOM 元素 (#todoapp)

<section class="todoapp" id="todoapp">...</section>

完成以上操作后,用瀏覽器打開 index.html ,若界面是以下這樣就說明項(xiàng)目 初始化成功 了。

二、功能實(shí)現(xiàn)和思考

1. 列表數(shù)據(jù)渲染

  • 創(chuàng)建數(shù)據(jù)并加入Vue實(shí)例中的 data 對象
let todos = [
    // 先寫兩條假數(shù)據(jù)測試一下
    { id: 1, content: "阿巴阿巴", completed: true },
    { id: 2, content: "馬卡馬卡", completed: false }
]

window.app = new Vue({
    data () {
      return {
        todos: todos
      }
    },
  })
  • 無數(shù)據(jù)時

    .main.footer 隱藏 : v-if 條件渲染

    <section class="main" v-if="todos.length">...</section>
    
    <section class="footer" v-if="todos.length">...</section>
    

思考:為什么這里使用 v-if 而不是 v-show 呢?他們的區(qū)別是?

共同點(diǎn) : 他們的功能都是 條件渲染 。

不同點(diǎn) : v-show 的原理是修改 css 的display屬性,并沒有操作 dom元素。

? v-if 的原理是根據(jù)條件,動態(tài)地銷毀或添加 dom元素。但 v-if 也是 惰性

? 的,如果初始條件為 false ,則什么都不做,等到條件為 true 了再開始渲染。

因此,v-if 有更高的切換開銷,而 v-show 有更高的初始渲染開銷。如果需要頻繁切換,建議使用 v-show,如果不需要頻繁切換,則可以使用 v-if 。而這里.main.footer并不會頻繁切換狀態(tài),所以使用 v-if。

?

  • 有數(shù)據(jù)時

    • 動態(tài)渲染數(shù)據(jù)列表 : v-for 列表渲染

    • 綁定相應(yīng)狀態(tài)下的 class : :class class 的綁定

    • checkbox 選中狀態(tài)切換 : v-model 雙向數(shù)據(jù)綁定

    • label 內(nèi)容渲染 : Mustache 語法

    <ul class="todo-list">
            <li v-for="(item,index) in todos"
                :key="item.id"
                :class="{completed:item.completed}">
              <div class="view">
                <input class="toggle"
                       type="checkbox"
                       v-model="item.completed">
                <label>{{ item.content }}</label>
                <button class="destroy"></button>
              </div>
              <input class="edit" value="Create a TodoMVC template">
            </li>
    </ul>
    

    思考 :

    1. 為什么在使用 v-for 時 還要使用 :key ?

      Vue在更新使用 v-for 渲染的列表元素時,會采用一種 就地復(fù)用 策略,盡可能的嘗試就地修改/復(fù)用相同類型元素。而 :key 給了每個節(jié)點(diǎn)一個 唯一標(biāo)識,讓虛擬 DOM 中的 Diff 算法正確識別節(jié)點(diǎn),從而重用和重新排序現(xiàn)有元素,從而更加 高效地更新虛擬 DOM。

  1. v-model 的原理?

    v-model 用于表單數(shù)據(jù)的雙向綁定,本質(zhì)上是語法糖。

    它背后做了兩個操作 :

    v-bind 綁定一個 value 屬性,v-on 給當(dāng)前元素綁定 input 事件。

    <input v-model="something"></input>
    以上操作等價(jià)于
    <input :value="something" @input="something = $event.target.value"></input>
    先綁定一個 something 屬性,在通過監(jiān)聽 input 事件,當(dāng)用戶改變輸入框數(shù)據(jù)時,通過設(shè)置當(dāng)前事件的目標(biāo)dom的value,從而實(shí)現(xiàn)雙向數(shù)據(jù)綁定的效果。
    

2. 添加新的 todo

  • 按下回車,輸入內(nèi)容不為空,添加一條 todo :

    • @keyup.enter 監(jiān)聽鍵盤回車事件并在 vue 的 methods中添加相應(yīng)方法
    • 內(nèi)容為空則什么都不做
    • 添加完后輸入框內(nèi)容清空
    <input class="new-todo"
           placeholder="What needs to be done?"
           autofocus
           @keyup.enter="addTodo">
    </input>
    
    methods: {
          addTodo ($event) {
            // 創(chuàng)建 newTodo 對象,獲取數(shù)據(jù)
            const newTodo = {
              id: this.todos.length + 1,
              content: $event.target.value.trim(),
              completed: false
            }
            // 如果內(nèi)容為空,什么都不做
            if (!newTodo.content.length) return
            // 如果內(nèi)容不為空,將 newTodo 加入 todos 中
            this.todos.push(newTodo)
            // 清空輸入框內(nèi)容
            $event.target.value = ''
          }
        }
    

3. 刪除 todo

  • 點(diǎn)擊 .destroy,刪除所在的 todo :

    • @click 監(jiān)聽按鈕點(diǎn)擊事件并在 vue 的 methods中添加相應(yīng)方法

    • 數(shù)組的 splice 方法刪除 todo

      <button class="destroy" @click="destroyTodo(index)"></button>
      
      methods: {
            destroyTodo (index) {
              // 用 splice 方法通過參數(shù) index 來找到要刪除的 todo,刪除一項(xiàng)
              this.todos.splice(index, 1)
            }
      }
      

?

4. 編輯 todo

  • 雙擊 label ,進(jìn)入編輯模式

    • @dblclick 監(jiān)聽 label 雙擊事件

    • :class 給所在的 li綁定 class .editing

    這里設(shè)置一個中間變量 currentEditing (就像一種狀態(tài)),當(dāng)監(jiān)聽到 label 雙擊事件時,currentEditing = item,而當(dāng) item === currentEditing 時,就給所在的 li綁定 class

    <li v-for="(item,index) in todos" :key="item.id"
        :class="{ completed: item.completed , editing: item === currentEditing }">
    </li>
    
    <label @dblclick="currentEditing = item">{{ item.content }}</label>
    
    • 局部自定義指令 directives 讓輸入框自動獲取焦點(diǎn)

      <input class="edit" 
             :value="item.content"
             v-editing-focus="item === currentEditing">
      </input>
      
      directives:{
          // update 在所有組件的 VNode(虛擬節(jié)點(diǎn)) 更新時調(diào)用,但可能發(fā)生在其子 VNode 更新之前。
          update(el,binding){
            // el : 用來操作元素 DOM
            // binding.value : 指令的綁定值 這里即 item === currentEditing
              if(binding.value){
                  el.focus()
              }
          }
        }
      

      ?

  • 輸入內(nèi)容后回車或失焦,將原本的 todo內(nèi)容替換為輸入的內(nèi)容
    • @keyup.enter 監(jiān)聽鍵盤回車事件;@blur 監(jiān)聽失焦事件

      <input class="edit"
             :value="item.content"
             v-editing-focus="item === currentEditing"
             @keyup.enter="saveEditing(item,index,$event)"                                          @blur="saveEditing(item,index,$event)">
      
  • 在vue 的 methods中添加相應(yīng)方法

    saveEditing (item, index, $event) {
            // 將輸入的內(nèi)容保存到 newContent 變量中
            const newContent = $event.target.value.trim()
            // 如果內(nèi)容為空 就刪除 todo 
            if (!newContent) this.destroyTodo(index)
            //將原本容替換為輸入的內(nèi)容
            item.content = newContent
            //通過設(shè)置 currentEditing ,移除掉 .editing ,退出編輯模式。
            this.currentEditing = null
          }
    
  • :valueinput綁定內(nèi)容

    <input class="toggle"
           type="checkbox"
           v-model="item.completed"
           :value="item.content">
    
  • 按下 esc,退出編輯模式
  • @keyup.esc 監(jiān)聽鍵盤回車事件

    <input class="edit"
           :value="item.content"
           v-editing-focus="item === currentEditing"
           @keyup.enter="saveEditing(item,index,$event)"                                          @blur="saveEditing(item,index,$event)"
           @keyup.esc="quitEditing">
    
  • 并在vue 的 methods中添加相應(yīng)方法

    quitEditing () {
            // 通過設(shè)置 currentEditing 移除掉 .editing 退出編輯模式
            this.currentEditing = null
          }
    

5. 標(biāo)記所有任務(wù)完成或者未完成

  • 點(diǎn)擊 .toggle-all ,將所有的 todos 的完成狀態(tài) 和 .toggleAll 的勾選狀態(tài) 綁定

    • @click 監(jiān)聽按鈕點(diǎn)擊事件

      <input id="toggle-all" class="toggle-all" type="checkbox"
             @click="toggleAll">
      
    • 并在 vue 的 methods中添加相應(yīng)方法

      methods: {
            toggleAll ($event) {
              // 獲取 .toggleAll 的勾選狀態(tài)
              let isToggled = $event.target.checked
              // 將所有的 todos 的完成狀態(tài) 和 .toggleAll 的勾選狀態(tài) 綁定
              this.todos.forEach(item => item.completed = isToggled);
            },        
      }
      
  • .toggleAll 的勾選狀態(tài) 和 todos 是否全選綁定

    • .toggle-allchecked 屬性綁定一個的計(jì)算屬性來監(jiān)聽單選框選中情況的改變

      <input id="toggle-all" class="toggle-all" type="checkbox"
             @click="toggleAll"
             :checked="isAllChecked">
      <!-- 也可以寫 v-model="isAllChecked" 因?yàn)?checkbox 使用 checked property 和 change 事件 -->
      
    • 并在 vue 的 computed中添加計(jì)算屬性

      computed: {
            isAllChecked () {
              return !this.todos.find(item => !item.completed)
            }
          },
      

思考:

  1. computed vs methods ? computed vs watch ?

    computed vs methods

    計(jì)算屬性是基于它們的響應(yīng)式依賴進(jìn)行緩存的。只有相關(guān)響應(yīng)式依賴發(fā)生改變時,他們才會重新求值。而方法會在每次重新渲染時調(diào)用函數(shù),不占用緩存但是會消耗一定時間

    computed vs watch

    雖然計(jì)算屬性在大多數(shù)情況下更合適,但是當(dāng)需要在數(shù)據(jù)變化時執(zhí)行異步或開銷較大的操作時,使用偵聽器更合適。

  1. v-model 在內(nèi)部為 checkbox 使用的 property`和拋出的事件?

    checkbox 和 radio 使用 checked property 和 change 事件。什么是 change 事件?其實(shí)就是 HTML 的 onchange 事件。它是元素值被改變(用戶改變,用代碼內(nèi)部改變無效)且表單失焦時觸發(fā)的事件。

    所以此時 v-model的原理是:

    <input type="checkbox" v-model="something"></input>
    以上操作等價(jià)于
    <input type="checkbox" 
           :checked="something" @change="something = $event.target.checked">
    </input>
    

    同理,select 字段將 value 作為 prop 并將 change 作為事件。

    <select name="" id="" v-model="something">
                <option disabled value="">請選擇</option>
                <option>A</option>
                <option>B</option>
    </select>
    以上操作等價(jià)于
    <select name="" id="" :value="something" @change="something = $event.target.value">
                <option disabled value="">請選擇</option>
                <option>A</option>
                <option>B</option>
    </select>
    

6. 計(jì)數(shù)

  • .todo-count顯示未完成的 todo 的數(shù)量

    • 模板語法

       <span class="todo-count"><strong>{{todoCount}}</strong> item left</span>
      
  • 在 vue 中添加計(jì)算屬性

    computed: {
          todoCount () {
            // es6 的 filter 方法
            return this.todos.filter(item => !item.completed).length
          }
        },
    

7. 清除所有完成項(xiàng)

  • 點(diǎn)擊 clear-completed ,清除所有完成項(xiàng)

    • @click 監(jiān)聽按鈕點(diǎn)擊事件并在 vue 中添加相應(yīng)方法

      <button class="clear-completed" @click="clearCompleted">Clear completed</button>
      
      methods: {
            clearCompleted () {
              this.todos = this.todos.filter(item => !item.completed)
            }
          },
      
  • 當(dāng)至少有一項(xiàng)完成項(xiàng)才顯示

    • v-show 切換狀態(tài)

      <button class="clear-completed" 
              @click="clearCompleted"
              v-show="hasCompleted">Clear completed</button>
      
  • 綁定計(jì)算屬性判斷是否至少有一項(xiàng)完成項(xiàng)

    computed: {
          hasCompleted () {
            // 當(dāng)至少有一項(xiàng)完成項(xiàng)才顯示
            return this.todos.filter(item => item.completed > 0).length > 0
          }
        },
    

8. 三種狀態(tài)數(shù)據(jù)過濾

  • 點(diǎn)擊不同的狀態(tài),獲取相應(yīng)的數(shù)據(jù)

    • 在 vue 的 data 中保存過濾狀態(tài),默認(rèn)為 'all'

      data () {
            return {
              todos: todos,
              currentEditing: null,
              filterState: 'all'
            }
          },
      
  • 通過 window.onhashchange 獲取點(diǎn)擊狀態(tài)的路由 hash 保存為當(dāng)前的狀態(tài)值,并且賦給 data 中的過濾狀態(tài)

    // 路由狀態(tài)切換
      // 當(dāng) 一個窗口的 hash (URL 中 # 后面的部分)改變時就會觸發(fā) hashchange 事件
      window.onhashchange = function () {
        // 獲取當(dāng)前點(diǎn)擊狀態(tài)的路由 hash  獲取的 location.hash 是 #/all 這樣的數(shù)據(jù)
        const hash = window.location.hash.substr(2) || 'all'
        // 將路由狀態(tài)賦給 過濾狀態(tài)
        window.app.filterState = hash
      }
      // 頁面第一次進(jìn)來保持狀態(tài)
      window.onhashchange()
    
  • 通過計(jì)算屬性來渲染過濾狀態(tài)下的渲染的數(shù)據(jù)

    定義計(jì)算屬性:根據(jù)過濾狀態(tài)返回相應(yīng)的todo

    computed: {
          filterTodos () {
            switch (this.filterState) {
              case 'active':
                return this.todos.filter(item => !item.completed);
                break
              case 'completed':
                return this.todos.filter(item => item.completed);
                break
              default:
                return this.todos;
                break
            }
          }
        },
    

    修改 todo 的列表循環(huán)

    <li v-for="(item,index) in filterTodos" :key="item.id"
        :class="{
                completed: item.completed,
                editing: item === currentEditing
        }">
    </li>
    
  • 根據(jù)狀態(tài)改變狀態(tài)按鈕的樣式

    • :class 給選中的狀態(tài)綁定 .selected 樣式

      <ul class="filters">
              <li>
                <a :class="{selected:filterState==='all'}" href="#/">All</a>
              </li>
              <li>
                <a href="#/active"
                   :class="{selected:filterState==='active'}">Active</a>
              </li>
              <li>
                <a href="#/completed"
                   :class="{selected:filterState==='completed'}">Completed</a>
              </li>
            </ul>
      

9. 數(shù)據(jù)持久化

  • 在 vue 實(shí)例外部 定義一個數(shù)據(jù)存儲對象, 有以下兩個方法:

    • 獲取本地?cái)?shù)據(jù)
    • 保存數(shù)據(jù)到本地
      let STOREAGE_KEY = "todo-items"
    
      // 定義數(shù)據(jù)存儲對象
      const todoStorage = {
        // 獲取本地?cái)?shù)據(jù) localStorage.getItem("key")
        fetch: function () {
          // 返回獲取的本地?cái)?shù)據(jù)的數(shù)組對象 ,如果為空,則是空數(shù)組 || '[]',
          return JSON.parse(localStorage.getItem(STOREAGE_KEY) || '[]')
        },
        // 保存數(shù)據(jù)到本地 localStorage.setItem("key","value")
        save: function (todos) {
          // 以 JSON 字符串形式存儲 todos 數(shù)據(jù)
          localStorage.setItem(STOREAGE_KEY, JSON.stringify(todos))
        }
      }
    
  • 修改 vue 實(shí)例的 data 中的 todos,以本地?cái)?shù)據(jù)初始化

    data () {
          return {
            todos: todoStorage.fetch(),
            currentEditing: null,
            filterState: 'all'
          }
    
  • 通過 vue 的 watch 監(jiān)聽 todos的變化,一有改變就將數(shù)據(jù)保存到本地

    watch: {
          // 監(jiān)聽 todos 變化
          todos: {
            deep: true, // 監(jiān)聽對象內(nèi)部值的變化
            handler (newTodos) {
              todoStorage.save(newTodos)
            }
          }
        },
    

思考: 為什么使用 localStorage 而不是 sessionStorage?

localStorage 用于 長久 保存整個網(wǎng)站的數(shù)據(jù),關(guān)閉標(biāo)簽頁數(shù)據(jù)也不會消失,保存的數(shù)據(jù)沒有過期時間,直到手動刪除。

localStorage的語法:

  • 保存數(shù)據(jù)

    localStorage.setItem("key","value")
    
  • 讀取數(shù)據(jù)

    let myLocalStorage = localStorage.getItem("key")
    
  • 刪除數(shù)據(jù)

    localStorage.removeItem("key")
    

sessionStorage 用于只想將數(shù)據(jù)保存在 當(dāng)前會話 中時,關(guān)閉標(biāo)簽頁數(shù)據(jù)會被刪除。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容