七週七種前端框架四:Vue.js 元件和元件通訊
Vue 例項
一個 Vue 應用是由一個 root vue instance 引導啟動的,而 Vue instance 是這麼建立的:
var vm = new Vue({
// options
})
一個 instance 實際上就是 MVVM 中的一個 VM。 傳入的配置物件中data裡的所有屬性都會被掛載到 instance上,而為了避免命名衝突,Vue 內建方法都會以 $ 開頭的屬性掛載到 instance 上。
instance 從建立到銷燬會經歷如下生命週期:
在初始化的時候大致經過三步:
- 繫結資料監聽,即對 data 的監聽
- 編譯模板
- 插入document或者替換對應dom
資料繫結
Vue 使用的是一種 類 mastache 語法。常用繫結語法分這麼幾類:
- mastache 語法,比如
{{ data }}
{{ data | filter}}
v-bind
繫結屬性,比如v-bind: href
,v-bind:class
v-on
繫結事件, 比如v-on:click
,v-on:submit
其中 v-*
都是 directive
例子:
<div v-bind:class="[classA, isB ? classB : '']">
屬性計算
Vue 支援一個很有意思的屬性計算語法,可以指定一個屬性由其他屬性計算出來,這樣就不用通過 $watch
var vm = new Vue({
el: '#example',
data: {
a: 1
},
computed: {
// a computed getter
b: function () {
// `this` points to the vm instance
return this.a + 1
}
}
})
## 流程控制和列表相關的語法
包括 `v-if`, `v-show`, `v-else`, `v-for`
表單
雙向資料繫結:
## 動畫 動畫的實現方式和 Angular 以及 React 都是一樣的,都是通過新增和刪除 class 來實現的。 # Component<input type="text" v-model="message" placeholder="edit me"> <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
元件的基本用法
Component 的定義包括兩部分:
1 建立component類:
var Profile = Vue.extend({
template: "<div> Lily </div>"
});
2 註冊一個 tagname:
Vue.component("me-profile", Profile);
這樣我們就可以通過 tagname 來使用這麼元件了:
<div id="todo">
<my-profile></my-profile>
<form v-on:submit="add" v-on:submit.prevent>
<input type="text" v-model="input"/>
<input type="submit" value='add' />
</form>
...
</div>
Vue.component("me-profile", Profile);
屬於全域性註冊,如果只是在某一個頁面內使用,可以通過區域性註冊的方式:
var vm = new Vue({
el: "#todo",
components: {
"my-profile": Profile
},
...
}
其中因為我們的 Vue 例項是繫結在 todo
元素上的,所以如果把 my-profile
放在這個元素外面是無效的,只有放在這個裡面才會被 Vue 的這個例項引導初始化。
注意事項:
Vue 建構函式可以傳的引數基本都可以用在 Vue.extend
上,但是對 el
和 data
兩個引數需要注意,為了避免不同例項間共享同一個物件,總是要通過 function
返回一個新的物件比較靠譜:
var MyComponent = Vue.extend({
data: function () {
return { a: 1 }
}
})
因為引數都一樣,其實他們倆就是同一個東西,不過一個是元件,一個是用來引導Vue啟動的。
模板注意事項
因為 Vue 就是原生的DOM,所以有些自定義標籤可能不符合DOM標準,比如想在 table
中自定義一個 tr
,如果直接插入 my-component
不符合規範,所以應該這樣寫:
<table>
<tr is="my-component"></tr>
</table>
Props 傳遞資料
在 Vue 中每個元件都是獨立的,不能也不應該直接訪問父類的data。所以我們通過 props
來向子元件傳遞資料,是不是和 React 的方式很像?
不同於 React,在 Vue 中子元件需要先宣告自己的 props 才行:
var Profile = Vue.extend({
props: ["name"],
template: `
<h2>{{name}}'s Todo List</h2>
<h4>{{name}} is a good girl</h4>
`
});
然後我們可以在使用 Profile 的時候這樣傳遞引數:
<my-profile name='Lily'></my-profile>
這種是通過字面量傳遞引數,所以傳遞的值一定是字串。還有一種方式是動態傳參,通過 v-bind
來傳遞引數,可以雙向繫結資料或者傳非字串引數:
<my-profile v-bind:name=‘input'></my-profile>
v-bind
如果是一個字串,則是繫結父元件的data中對應的欄位,比如上面就是雙向綁定了 input
的值。如果是一個數字則就是綁定了一個數字。
Vue 還可以顯式指定單向還是雙向的資料繫結:
<!-- default, one-way-down binding -->
<child :msg="parentMsg"></child>
<!-- explicit two-way binding -->
<child :msg.sync="parentMsg"></child>
<!-- explicit one-time binding -->
<child :msg.once="parentMsg"></child>
Props 校驗
一個好的元件總是應該先驗證引數是否正確,另外可能還需要設定一些引數的預設值:
var Profile = Vue.extend({
input: {
type: String
}
});
父子元件通訊
上面講到的 props
其實就是父元件向子元件傳遞訊息的一種方式。
在子元件中有一個 this.$parent
和 this.$root
可以用來方法父元件和根例項。不過,現在我們應該避免這麼做。因為元件本身就是為了封裝獨立的邏輯,如果又去直接訪問父元件的資料就破壞了元件的封裝性。
所以我們應該還是應該通過父元件向子元件傳遞 props
的方式來通訊。
當然 props
其實只能做回撥。在 React 中就探討過這個問題,React 的做法就是通過 props
來做,傳一個回撥函式給子元件。其實我不是很喜歡這種把回撥函式傳來傳去的方式,我更喜歡的是事件的方式。Vue 中子元件可以通過通過事件和父元件進行通訊的。向父元件發訊息是通過 this.$dispatch
,而向子元件傳送訊息是通過 this.$boardcast
,這裡都是向所有的父親和孩子傳送訊息,但是一旦執行一個回撥之後就會停止,除非這個回撥函式顯式返回了 true
。
我們把之前的Todo List拆成不同的元件來實現,這樣可以體驗下如何進行元件的雙向通訊,我們拆分出兩個元件,分別是 List
和 Form
。
Form
負責處理使用者輸入,並在提交表單的時候向父元件傳送一個 add
訊息,程式碼如下:
var Form = Vue.extend({
props: {
username: {
type: String,
default: "Unnamed"
}
},
data: function() {
return {
input: "",
};
},
template: `
<h1>{{username}}'s Todo List</h1>
<form v-on:submit="add" v-on:submit.prevent>
<input type="text" v-model="input"/>
<input type="submit" value='add' />
</form>
`,
methods: {
add: function() {
this.$dispatch("add", this.input); //這裡就是向父元件傳送訊息
this.input = "";
}
}
});
List
只負責展示列表和處理使用者勾選操作,它接收到 add
訊息之後會在自己上新增一個條目:
var List = Vue.extend({
template: `
<ul>
<li v-for='todo in list'>
<label v-bind:class="{ done : todo.done }" >
<input type="checkbox" v-model="todo.done"/>
{{todo.title}}
</label>
</li>
</ul>`,
props: {
initList: {
type: Array
}
},
data: function() {
return {
list: []
}
},
events: {
add: function(input) {
if(!input) return false;
this.list.unshift({
title: input,
done: false
});
}
}
});
然後,因為這是兩個元件,當然需要一個 Vue 例項來引導啟動,我們的例項如下:
var vm = new Vue({
el: "#todo",
components: {
"todo-form": Form,
"todo-list": List
},
events: {
add: function(input) {
this.$broadcast("add", input);
}
}
});
注意,其實 Form
和 List
在邏輯上是平級的元件,所以他們沒有父子關係,他們共同都是 vm
的孩子。這裡 vm
接到 Form
的訊息之後會轉發給 List
。
html 程式碼就更簡單了:
<div id="todo">
<todo-form username='Lily'></todo-form>
<todo-list></todo-list>
</div>
Slot
通過 Slot 可以實現把父元件渲染出來的HTML插入到子元件中,目前還不清楚什麼時候會需要這樣做,而且這麼做對子元件的侵入性太大。
動態切換元件
這個功能感覺有點多餘,感覺很多情況下我們應該是通過邏輯程式碼來實現切換,而不是通過Vue內建的動態元件來切換。不過用來實現一個類似 tab 切換的功能還是很方便的。
我們這裡給 Todo List 增加一個 about 頁面。那麼首先我們需要把 vm 改成一個元件,這個元件叫 Todo
,它就是整個 Todo 頁面:
var Todo = Vue.extend({
template: `
<div id="todo">
<todo-form username='Lily'></todo-form>
<todo-list></todo-list>
<slot>not show</slot>
</div>
`,
components: {
"todo-form": Form,
"todo-list": List
},
events: {
add: function(input) {
this.$broadcast("add", input);
}
}
});
其實改動就第一行。
然後我們需要建立一個 About
元件:
var About = Vue.extend({
template: `
<div id="about">
<p>About Todo List V0.1.0</p>
<p>Content here</p>
</div>`
});
接下來是重點了,我們要建立一個例項 vm,這vm要負責切換這兩個頁面:
var vm = new Vue({
el: "body",
data: {
currentView: "todo"
},
components: {
"todo": Todo,
"about": About
}
});
這裡我們定義了一個 currentView
欄位,當然可以是任意名稱,然後通過特殊的 component
標籤來進行元件切換:
<component :is="currentView"></component>
<ul>
<li><label><input type="radio" name='page' value='todo' v-model='currentView'> Home</label></li>
<li><label><input type="radio" name='page' value='about' v-model='currentView'> About</label></li>
</ul>
上面的程式碼有兩處需要注意:
- 通過
component
這個特殊標籤,然後用:is
屬性來進行元件的切換。 radio
通過雙向繫結來修改currentView
欄位,從而實現點選之後就可以進行切換。
資料繫結的實現原理
Vue 把雙向繫結稱作 reactive
,可以翻譯為響應式資料繫結。內部是通過 ES5 定義的 getter
和 setter
方法實現的,所以不支援 IE8 及以下瀏覽器,這種實現方式有兩個容易犯錯的地方:
- 如果在
data
上直接新增和刪除屬性是無法被檢測到的,一般刪除是不會的,但是可能會動態新增,這個時候應該通過vm.$set(“name”, value)
的方式來新增。 - 無法檢測到物件內部的變化,也就是隻能檢測
data
的屬性變化,如果data.a
是一個物件,那麼data.a.b = 1
這種變化是無法被檢測到的。這種情況下應該建立一個新的物件並賦值給data.a
就行了。
非同步更新機制
Vue 對DOM的更新是非同步的! 這個非同步是在一個非同步佇列中進行的,不過這個非同步佇列會在當前的 Event Loop
中執行完,所以如果修改了 Data 立刻去DOM中做查詢操作是不對的,這個時候DOM還沒有更新,正確的做法是這樣做:
vm.msg = 'new message' // change data
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})
或者這樣:
vm.$nextTick(function () {
this.$el.textContent === 'new message' // true
})
花了半天時間才看完元件,下面應該去看一下另一個重點: Directive