、MVVM簡介
如果你是第一次學(xué)前端,那么本節(jié)知識一定要了解,什么是MVVM。
MVVM是Model-View-ViewModel的簡寫。它本質(zhì)上就是MVC 的改進(jìn)版。MVVM 就是將其中的View 的狀態(tài)和行為抽象化,讓我們將視圖 UI 和業(yè)務(wù)邏輯分開。當(dāng)然這些事 ViewModel 已經(jīng)幫我們做了,它可以取出 Model 的數(shù)據(jù)同時幫忙處理 View 中由于需要展示內(nèi)容而涉及的業(yè)務(wù)邏輯。MVVM的核心是ViewModel層,負(fù)責(zé)轉(zhuǎn)換Model中的數(shù)據(jù)對象來讓數(shù)據(jù)變得更容易管理和使用。是一種簡化用戶界面的事件驅(qū)動編程方式。
下邊我們來畫張圖來大體了解下MVVM的工作原理圖:
該層向上與視圖層進(jìn)行雙向數(shù)據(jù)綁定
向下與Model層通過接口請求進(jìn)行數(shù)據(jù)交互
(1)View
View是視圖層, 也就是用戶界面。前端主要由HTH L和csS來構(gòu)建, 為了更方便地展現(xiàn)vi eu to del或者Hodel層的數(shù)據(jù), 已經(jīng)產(chǎn)生了各種各樣的前后端模板語言, 比如FreeMarker,Thyme leaf等等, 各大MV VM框架如Vue.js.Angular JS, EJS等也都有自己用來構(gòu)建用戶界面的內(nèi)置模板語言。
(2)Model
Model是指數(shù)據(jù)模型, 泛指后端進(jìn)行的各種業(yè)務(wù)邏輯處理和數(shù)據(jù)操控, 主要圍繞數(shù)據(jù)庫系統(tǒng)展開。這里的難點主要在于需要和前端約定統(tǒng)一的接口規(guī)則
(3)ViewModel
ViewModel是由前端開發(fā)人員組織生成和維護(hù)的視圖數(shù)據(jù)層。在這一層, 前端開發(fā)者對從后端獲取的Model數(shù)據(jù)進(jìn)行轉(zhuǎn)換處理, 做二次封裝, 以生成符合View層使用預(yù)期的視圖數(shù)據(jù)模型。
View Model所封裝出來的數(shù)據(jù)模型包括視圖的狀態(tài)和行為兩部分, 而Model層的數(shù)據(jù)模型是只包含狀態(tài)的
視圖狀態(tài)和行為都封裝在了View Model里。這樣的封裝使得View Model可以完整地去描述View層。由于實現(xiàn)了雙向綁定, View Model的內(nèi)容會實時展現(xiàn)在View層, 這是激動人心的, 因為前端開發(fā)者再也不必低效又麻煩地通過操縱DOM去更新視圖。 MVVM框架已經(jīng)把最臟最累的一塊做好了, 我們開發(fā)者只需要處理和維護(hù)View Model, 更新數(shù)據(jù)視圖就會自動得到相應(yīng)更新,真正實現(xiàn)事件驅(qū)動編程。 View層展現(xiàn)的不是Model層的數(shù)據(jù), 而是ViewModel的數(shù)據(jù), 由ViewModel負(fù)責(zé)與Model層交互, 這就完全解耦了View層和Model層, 這個解耦是至關(guān)重要的, 它是前后端分離方案實施的重要一環(huán)。
2、為什么要使用MVVM
MVVM模式和MVC模式一樣,主要目的是分離視圖(View)和模型(Model),有幾大優(yōu)點
(1) 低耦合。視圖(View)可以獨(dú)立于Model變化和修改,一個ViewModel可以綁定到不同的"View"上,當(dāng)View變化的時候Model可以不變,當(dāng)Model變化的時候View也可以不變。
(2) 可重用性。你可以把一些視圖邏輯放在一個ViewModel里面,讓很多view重用這段視圖邏輯。
(3)獨(dú)立開發(fā)。開發(fā)人員可以專注于業(yè)務(wù)邏輯和數(shù)據(jù)的開發(fā)(ViewModel),設(shè)計人員可以專注于頁面設(shè)計,使用Expression Blend可以很容易設(shè)計界面并生成xaml代碼。
(4)可測試。界面素來是比較難于測試的,測試可以針對ViewModel來寫
3、VUE概述
(1)什么是vue?
Vue是一套用于構(gòu)建用戶界面的漸進(jìn)式框架。與其它大型框架不同的是,Vue 被設(shè)計為可以自底向上逐層應(yīng)用。Vue 的核心庫只關(guān)注視圖層,不僅易于上手,還便于與第三方庫或既有項目整合
這是官網(wǎng)給出的介紹,可能不是那么容易理解。簡單來說,Vue是一個視圖層框架,幫助我們更好的構(gòu)建應(yīng)用。
使用Vue和原生JS一個最顯著的差別就是,Vue不再對DOM直接進(jìn)行操作,而是通過對數(shù)據(jù)的操作,來改變頁面。使用Vue構(gòu)建的頁面,是有一個個的組件組成的,當(dāng)組件中定義的數(shù)據(jù)發(fā)生變化時,組件的顯示也會跟著變化,且此過程無需刷新頁面。
(2)MVVM模式的實現(xiàn)者
Model:模型層, 在這里表示JavaScript對象 View:視圖層, 在這里表示DOM(HTML操作的元素) ViewModel:連接視圖和數(shù)據(jù)的中間件, Vue.js就是MVVM中的View Model層的實現(xiàn)者 在MVVM架構(gòu)中, 是不允許數(shù)據(jù)和視圖直接通信的, 只能通過ViewModel來通信, 而View Model就是定義了一個Observer觀察者
ViewModel能夠觀察到數(shù)據(jù)的變化, 并對視圖對應(yīng)的內(nèi)容進(jìn)行更新 ViewModel能夠監(jiān)聽到視圖的變化, 并能夠通知數(shù)據(jù)發(fā)生改變 至此, 我們就明白了, Vue.js就是一個MV VM的實現(xiàn)者, 他的核心就是實現(xiàn)了DOM監(jiān)聽與數(shù)據(jù)綁定
(3)為什么要使用Vue
易用:熟悉HTML、CSS、JavaScript之后,可快速度上手vue。學(xué)習(xí)曲線平穩(wěn)。
輕量級:Vue.js壓縮后有只有20多kb,超快虛擬DOM
高效:吸取了Angular(模塊化) 和React(虛擬DOM) 的優(yōu)勢, 并擁有自己獨(dú)特的功能
開源:文檔齊全,社區(qū)活躍度高
4、VUE之Hello World!
步驟一:創(chuàng)建空文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
</body>
</html>
步驟二:引入vue.js (本人下載的開發(fā)版的vue.js,跟本html文件放在了同一目錄下,所以直接引用)
<script type="text/javascript" src="vue.js"></script>
步驟三:創(chuàng)建vue實例
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data:{
msg:'Hello World'
}
});
</script>
步驟四:數(shù)據(jù)與頁面元素綁定
<div id="app">
{{msg}}
</div>
完整的html
<!DOCTYPE html>
<html lang="en">
<body>
<div id="app">
{{msg}}
</div>
<script type="text/javascript" src="vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data:{
msg:'Hello World'
}
});
</script>
</body>
</html>
瀏覽器打開:
參數(shù)分析:
el : '#app' -- 綁定元素的ID(元素的掛載位置,值可以是CSS選擇器或者是DOM元素)
data : { msg : 'Hello World' } -- 模型數(shù)據(jù),屬性名:msg 值:Hello World
{{msg}} : 在綁定的元素中使用{{ }}將Vue創(chuàng)建的名為msg的屬性包起來, 即可實現(xiàn)數(shù)據(jù)綁定功能,我們在調(diào)試狀態(tài)下手動修改下msg的值,在不刷新頁面的情況下就會展示我們修改后的值,這就是借助了Vue的數(shù)據(jù)綁定功能實現(xiàn)的。 MV VM模式中要求View Model層就是使用觀察者模式來實現(xiàn)數(shù)據(jù)的監(jiān)聽與綁定, 以做到數(shù)據(jù)與視圖的快速響應(yīng)
下一篇:VUE入門教程(二)之模板語法(指令)
于parseHTML函數(shù)代碼實在過于龐大,我這里就不一次性貼出源代碼了,大家可以前往(https://github.com/vuejs/vue/blob/dev/src/compiler/parser/html-parser.js)查看源代碼。
我們來總結(jié)一下該函數(shù)的主要功能:
1、匹配標(biāo)簽的 "<" 字符
匹配的標(biāo)簽名稱不能是:script、style、textarea
有如下情況:
1、注釋標(biāo)簽 /^<!\--/
2、條件注釋 /^<!\[/
3、html文檔頭部 /^<!DOCTYPE [^>]+>/i
4、標(biāo)簽結(jié)束 /^<\/ 開頭
5、標(biāo)簽開始 /^</ 開頭
然后開始匹配標(biāo)簽的屬性包括w3的標(biāo)準(zhǔn)屬性(id、class)或者自定義的任何屬性,以及vue的指令(v-、:、@)等,直到匹配到 "/>" 標(biāo)簽的結(jié)尾。然后把已匹配的從字符串中刪除,一直 while 循環(huán)匹配。
解析開始標(biāo)簽函數(shù)代碼:
function parseStartTag () {
// 標(biāo)簽的開始 如<div
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1], // 標(biāo)簽名稱
attrs: [], // 標(biāo)簽屬性
start: index // 開始位置
}
// 減去已匹配的長度
advance(start[0].length)
let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
attr.start = index
v
advance(attr[0].length)
attr.end = index
match.attrs.push(attr) // 把匹配到的屬性添加到attrs數(shù)組
}
if (end) { // 標(biāo)簽的結(jié)束符 ">"
match.unarySlash = end[1]
advance(end[0].length) // 減去已匹配的長度
match.end = index // 結(jié)束位置
return match
}
}
}
處理過后結(jié)構(gòu)如下:
接下來就是處理組合屬性,調(diào)用 “handleStartTag” 函數(shù)
function handleStartTag (match) {
const tagName = match.tagName // 標(biāo)簽名稱
const unarySlash = match.unarySlash // 一元標(biāo)簽
if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
// 解析標(biāo)簽結(jié)束
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
// 是否為一元標(biāo)簽
const unary = isUnaryTag(tagName) || !!unarySlash
const l = match.attrs.length
// 標(biāo)簽屬性集合
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
const value = args[3] || args[4] || args[5] || ''
const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' ? options.shouldDecodeNewlinesForHref : options.shouldDecodeNewlines
attrs[i] = {
name: args[1], // 屬性名稱
value: decodeAttr(value, shouldDecodeNewlines) // 屬性值
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
// 開始位置
attrs[i].start = args.start + args[0].match(/^\s*/).length
// 結(jié)束位置
attrs[i].end = args.end
}
}
if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
lastTag = tagName
}
// 調(diào)用start函數(shù)
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
我們簡單說一下最后調(diào)用的start函數(shù)的作用:
1、判斷是否為svg標(biāo)簽,并處理svg在ie下的兼容性問題
2、遍歷標(biāo)簽屬性,驗證其名稱是否有效
3、標(biāo)簽名是否為 style 或者 script ,如果在服務(wù)端會提示warn警告
4、檢查屬性是否存在 v-for、v-if、v-once指令
5、如果是更元素就驗證其合法性,不能是 slot 和 template 標(biāo)簽,不能存在 v-for指令
以上就是界面html模板的開始標(biāo)簽的分析,接下來我們來分析如何匹配結(jié)束標(biāo)簽。
請看:Vue源碼全面解析三十 parseHTML函數(shù)(解析html(二)結(jié)束標(biāo)簽)
如有錯誤,歡迎指正,謝謝。
ue的使用相信大家都很熟練了,使用起來簡單。但是大部分人不知道其內(nèi)部的原理是怎么樣的,今天我們就來一起實現(xiàn)一個簡單的vue。
Object.defineProperty()
實現(xiàn)之前我們得先看一下Object.defineProperty的實現(xiàn),因為vue主要是通過數(shù)據(jù)劫持來實現(xiàn)的,通過get、set來完成數(shù)據(jù)的讀取和更新。
var obj = {name:'wclimb'}
var age = 24
Object.defineProperty(obj,'age',{
enumerable: true, // 可枚舉
configurable: false, // 不能再define
get () {
return age
},
set (newVal) {
console.log('我改變了',age +' -> '+newVal);
age = newVal
}
})
> obj.age
> 24
> obj.age = 25;
> 我改變了 24 -> 25
> 25
從上面可以看到通過get獲取數(shù)據(jù),通過set監(jiān)聽到數(shù)據(jù)變化執(zhí)行相應(yīng)操作,還是不明白的話可以去看看Object.defineProperty文檔。
流程圖
html代碼結(jié)構(gòu)
<div id="wrap">
<p v-html="test"></p>
<input type="text" v-model="form">
<input type="text" v-model="form">
<button @click="changeValue">改變值</button>
{{form}}
</div>
js調(diào)用
JavaScript
new Vue({
el: '#wrap',
data:{
form: '這是form的值',
test: '<strong>我是粗體</strong>',
},
methods:{
changeValue(){
console.log(this.form)
this.form = '值被我改變了,氣不氣?'
}
}
})
Vue結(jié)構(gòu)
class Vue{
constructor(){}
proxyData(){}
observer(){}
compile(){}
compileText(){}
}
class Watcher{
constructor(){}
update(){}
}
Vue constructor 初始化
class Vue{
constructor(options = {}){
this.$el = document.querySelector(options.el);
let data = this.data = options.data;
// 代理data,使其能直接this.xxx的方式訪問data,正常的話需要this.data.xxx
Object.keys(data).forEach((key)=> {
this.proxyData(key);
});
this.methods = obj.methods // 事件方法
this.watcherTask = {}; // 需要監(jiān)聽的任務(wù)列表
this.observer(data); // 初始化劫持監(jiān)聽所有數(shù)據(jù)
this.compile(this.$el); // 解析dom
}
}
上面主要是初始化操作,針對傳過來的數(shù)據(jù)進(jìn)行處理
proxyData 代理data
class Vue{
constructor(options = {}){
......
}
proxyData(key){
let that = this;
Object.defineProperty(that, key, {
configurable: false,
enumerable: true,
get () {
return that.data[key];
},
set (newVal) {
that.data[key] = newVal;
}
});
}
}
上面主要是代理data到最上層,this.xxx的方式直接訪問data
observer 劫持監(jiān)聽
class Vue{
constructor(options = {}){
......
}
proxyData(key){
......
}
observer(data){
let that = this
Object.keys(data).forEach(key=>{
let value = data[key]
this.watcherTask[key] = []
Object.defineProperty(data,key,{
configurable: false,
enumerable: true,
get(){
return value
},
set(newValue){
if(newValue !== value){
value = newValue
that.watcherTask[key].forEach(task => {
task.update()
})
}
}
})
})
}
}
同樣是使用Object.defineProperty來監(jiān)聽數(shù)據(jù),初始化需要訂閱的數(shù)據(jù)。
把需要訂閱的數(shù)據(jù)到push到watcherTask里,等到時候需要更新的時候就可以批量更新數(shù)據(jù)了。下面就是;
遍歷訂閱池,批量更新視圖。
set(newValue){
if(newValue !== value){
value = newValue
// 批量更新視圖
that.watcherTask[key].forEach(task => {
task.update()
})
}
}
compile 解析dom
class Vue{
constructor(options = {}){
......
}
proxyData(key){
......
}
observer(data){
......
}
compile(el){
var nodes = el.childNodes;
for (let i = 0; i 0){
this.compile(node)
}
if(node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')){
node.addEventListener('input',(()=>{
let attrVal = node.getAttribute('v-model')
this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'value'))
node.removeAttribute('v-model')
return () => {
this.data[attrVal] = node.value
}
})())
}
if(node.hasAttribute('v-html')){
let attrVal = node.getAttribute('v-html');
this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'innerHTML'))
node.removeAttribute('v-html')
}
this.compileText(node,'innerHTML')
if(node.hasAttribute('@click')){
let attrVal = node.getAttribute('@click')
node.removeAttribute('@click')
node.addEventListener('click',e => {
this.methods[attrVal] && this.methods[attrVal].bind(this)()
})
}
}
}
},
compileText(node,type){
let reg = /{{(.*)}}/g, txt = node.textContent;
if(reg.test(txt)){
node.textContent = txt.replace(reg,(matched,value)=>{
let tpl = this.watcherTask[value] || []
tpl.push(new Watcher(node,this,value,type))
return value.split('.').reduce((val, key) => {
return this.data[key];
}, this.$el);
})
}
}
}
這里代碼比較多,我們拆分看你就會覺得很簡單了
if(node.hasAttribute('v-html')){
let attrVal = node.getAttribute('v-html');
this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'innerHTML'))
node.removeAttribute('v-html')
}
上面這個首先判斷node節(jié)點上是否有v-html這種指令,如果存在的話,我們就發(fā)布訂閱,怎么發(fā)布訂閱呢?只需要把當(dāng)前需要訂閱的數(shù)據(jù)push到watcherTask里面,然后到時候在設(shè)置值的時候就可以批量更新了,實現(xiàn)雙向數(shù)據(jù)綁定,也就是下面的操作
that.watcherTask[key].forEach(task => {
task.update()
})
然后push的值是一個Watcher的實例,首先他new的時候會先執(zhí)行一次,執(zhí)行的操作就是去把純雙花括號 -> 1,也就是說把我們寫好的模板數(shù)據(jù)更新到模板視圖上。
最后把當(dāng)前元素屬性剔除出去,我們用Vue的時候也是看不到這種指令的,不剔除也不影響
至于Watcher是什么,看下面就知道了
Watcher
class Watcher{
constructor(el,vm,value,type){
this.el = el;
this.vm = vm;
this.value = value;
this.type = type;
this.update()
}
update(){
this.el[this.type] = this.vm.data[this.value]
}
}
之前發(fā)布訂閱之后走了這里面的操作,意思就是把當(dāng)前元素如:node.innerHTML = ‘這是data里面的值’、node.value = ‘這個是表單的數(shù)據(jù)’
那么我們?yōu)槭裁床恢苯尤ジ履兀€需要update做什么,不是多此一舉嗎?
其實update記得嗎?我們在訂閱池里面需要批量更新,就是通過調(diào)用Watcher原型上的update方法。
效果
在線效果地址,大家可以瀏覽器看一下效果,由于本人太懶了,gif效果圖就先不放了,哈哈
*請認(rèn)真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。