我的 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 :
:classclass 的綁定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>思考 :
-
為什么在使用
v-for時 還要使用:key?Vue在更新使用
v-for渲染的列表元素時,會采用一種 就地復(fù)用 策略,盡可能的嘗試就地修改/復(fù)用相同類型元素。而:key給了每個節(jié)點(diǎn)一個 唯一標(biāo)識,讓虛擬 DOM 中的 Diff 算法正確識別節(jié)點(diǎn),從而重用和重新排序現(xiàn)有元素,從而更加 高效地更新虛擬 DOM。
-
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 }
-
:value給input綁定內(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-all的checked屬性綁定一個的計(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) } },
-
思考:
-
computedvsmethods?computedvswatch?computedvsmethods:計(jì)算屬性是基于它們的響應(yīng)式依賴進(jìn)行緩存的。只有相關(guān)響應(yīng)式依賴發(fā)生改變時,他們才會重新求值。而方法會在每次重新渲染時調(diào)用函數(shù),不占用緩存但是會消耗一定時間。
computedvswatch:雖然計(jì)算屬性在大多數(shù)情況下更合適,但是當(dāng)需要在數(shù)據(jù)變化時執(zhí)行異步或開銷較大的操作時,使用偵聽器更合適。
-
v-model在內(nèi)部為 checkbox 使用的 property`和拋出的事件?checkbox 和 radio 使用
checkedproperty 和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)的
todocomputed: { 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ù)會被刪除。