一姜盈、Vue簡介
1.1 Vue是什么
Vue (讀音 /vju?/迹辐,類似于 view) 是一套用于構(gòu)建用戶界面的漸進式框架呵萨。與其它大型框架不同的是撒犀,Vue 被設(shè)計為可以自底向上逐層應(yīng)用某宪。Vue 的核心庫只關(guān)注視圖層仿村,不僅易于上手,還便于與第三方庫或既有項目整合兴喂。另一方面蔼囊,當與現(xiàn)代化的工具鏈以及各種支持類庫結(jié)合使用時,Vue 也完全能夠為復(fù)雜的單頁應(yīng)用提供驅(qū)動衣迷。
由于筆者水平有限畏鼓,如有不足和不正確的地方,請評論指出壶谒。
1.2 Vue解決了什么問題
- 數(shù)據(jù)的雙向綁定
- 組件化管理
1.3 怎么學(xué)習(xí)Vue
二云矫、 MVVM
2.1 順便摘要下廖雪峰JavaScript教程的一段前端的發(fā)展史
在上個世紀的1989年,歐洲核子研究中心的物理學(xué)家Tim Berners-Lee發(fā)明了超文本標記語言(HyperText Markup Language)汗菜,簡稱HTML让禀,并在1993年成為互聯(lián)網(wǎng)草案挑社。從此,互聯(lián)網(wǎng)開始迅速商業(yè)化巡揍,誕生了一大批商業(yè)網(wǎng)站痛阻。
最早的HTML頁面是完全靜態(tài)的網(wǎng)頁,它們是預(yù)先編寫好的存放在Web服務(wù)器上的html文件腮敌。瀏覽器請求某個URL時录平,Web服務(wù)器把對應(yīng)的html文件扔給瀏覽器,就可以顯示html文件的內(nèi)容了缀皱。
如果要針對不同的用戶顯示不同的頁面斗这,顯然不可能給成千上萬的用戶準備好成千上萬的不同的html文件,所以啤斗,服務(wù)器就需要針對不同的用戶表箭,動態(tài)生成不同的html文件。一個最直接的想法就是利用C钮莲、C++這些編程語言免钻,直接向瀏覽器輸出拼接后的字符串。這種技術(shù)被稱為CGI:Common Gateway Interface崔拥。
很顯然极舔,像新浪首頁這樣的復(fù)雜的HTML是不可能通過拼字符串得到的。于是链瓦,人們又發(fā)現(xiàn)拆魏,其實拼字符串的時候,大多數(shù)字符串都是HTML片段慈俯,是不變的渤刃,變化的只有少數(shù)和用戶相關(guān)的數(shù)據(jù),所以贴膘,又出現(xiàn)了新的創(chuàng)建動態(tài)HTML的方式:ASP卖子、JSP和PHP——分別由微軟、SUN和開源社區(qū)開發(fā)刑峡。
在ASP中洋闽,一個asp文件就是一個HTML,但是突梦,需要替換的變量用特殊的<%=var%>標記出來了诫舅,再配合循環(huán)、條件判斷阳似,創(chuàng)建動態(tài)HTML就比CGI要容易得多骚勘。
但是,一旦瀏覽器顯示了一個HTML頁面,要更新頁面內(nèi)容俏讹,唯一的方法就是重新向服務(wù)器獲取一份新的HTML內(nèi)容当宴。如果瀏覽器想要自己修改HTML頁面的內(nèi)容,就需要等到1995年年底泽疆,JavaScript被引入到瀏覽器户矢。
有了JavaScript后,瀏覽器就可以運行JavaScript殉疼,然后梯浪,對頁面進行一些修改。JavaScript還可以通過修改HTML的DOM結(jié)構(gòu)和CSS來實現(xiàn)一些動畫效果瓢娜,而這些功能沒法通過服務(wù)器完成挂洛,必須在瀏覽器實現(xiàn)。
第一階段眠砾,直接用JavaScript操作DOM節(jié)點虏劲,使用瀏覽器提供的原生API:
var dom = document.getElementById('name');
dom.innerHTML = 'Homer';
dom.style.color = 'red';
第二階段,由于原生API不好用褒颈,還要考慮瀏覽器兼容性柒巫,jQuery橫空出世,以簡潔的API迅速俘獲了前端開發(fā)者的芳心
$('#name').text('Homer').css('color', 'red');
第三階段谷丸,MVC模式堡掏,需要服務(wù)器端配合,JavaScript可以在前端修改服務(wù)器渲染后的數(shù)據(jù)刨疼。
現(xiàn)在泉唁,隨著前端頁面越來越復(fù)雜,用戶對于交互性要求也越來越高币狠,想要寫出Gmail這樣的頁面游两,僅僅用jQuery是遠遠不夠的砾层。MVVM模型應(yīng)運而生漩绵。
MVVM最早由微軟提出來,它借鑒了桌面應(yīng)用程序的MVC思想肛炮,在前端頁面中止吐,把Model用純JavaScript對象表示,View負責(zé)顯示侨糟,兩者做到了最大限度的分離碍扔。
把Model和View關(guān)聯(lián)起來的就是ViewModel。ViewModel負責(zé)把Model的數(shù)據(jù)同步到View顯示出來秕重,還負責(zé)把View的修改同步回Model不同。
其實從jq語法的引入操作DOM結(jié)構(gòu),變的容易的多了。但是如果能直接該表javaScript對象就能導(dǎo)致DOM結(jié)構(gòu)做出對應(yīng)的變化二拐,那該多好呀服鹅,而MVVM就把開發(fā)者從DOM的繁瑣步驟中解脫出來了,而更加關(guān)注Mode的變化百新。
三企软、步步為營
3.1 主流雙向綁定的做法
手動綁定
臟值檢查(angular.js)
數(shù)據(jù)劫持
具體的做法可以參考javascript實現(xiàn)數(shù)據(jù)雙向綁定的三種方式
3.2 簡要概述以上做法:
雙向綁定從本質(zhì)上來說無非兩部分 Model->View 與 View->Model
3.2.1 首先是Model->View的思路
model無非是個Object,或者是如Vue里面是個全局的vm.data
view 在html上無疑是個樹形的標簽結(jié)構(gòu)饭望,所以也就是node這樣結(jié)構(gòu)
最直接的做法遍歷仗哨。
先看下最基本的vue代碼
html
<div id="app">
<input type="text" v-model="input" id="input">
{{text}}
<p>{{input}}</p>
<p id="show"></p>
</div>
可以看到Vue里面綁定數(shù)據(jù)無非兩種,<input type="text" v-model="input" id="input">
其中 v-model
加載<>中铅辞,也就是給標簽增加新的屬性厌漂,和data-
的方式增加屬性一般無二,(PS:順便提及小程序中函數(shù)傳參斟珊,運用就是這樣的方法)桩卵。
So, a:for也罷,v-model也罷倍宾,或者其他各種種種無非是標識符不同而已雏节,萬變不離其中。第二部分就是關(guān)于'{{}}'高职,因為其實在標簽內(nèi)部钩乍,比如<p>{{input}}</p>
可以看到, {{input}}并不作為app的子節(jié)點怔锌,所以當為元素節(jié)點的是寥粹,判斷是否有子節(jié)點,有則再次調(diào)用scan函數(shù)埃元。
所以有了涝涤,第一簡單的方法就是每次改變data數(shù)值的時候,直接再次調(diào)用scan函數(shù)(PS:因為scan方法因為 先遍歷node列表岛杀,再遍歷該節(jié)點的屬性阔拳,所以會是雙層遍歷)
也就是簡單綁定的方法
/**
* 設(shè)置數(shù)據(jù)后掃描
*/
function mvSet(key, value){
data[key] = value;
scan();
}
第二種臟值檢查
直接封裝和執(zhí)行$digest()
或$apply()
/**
* 臟循環(huán)檢測
* @param {[type]} elems [description]
* @return {[type]} [description]
*/
var digest = function(elems) {
/**
* 掃描帶指令的節(jié)點屬性
*/
for (var i = 0, len = elems.length; i < len; i++) {
var elem = elems[i];
for (var j = 0, len1 = elem.attributes.length; j < len1; j++) {
var attr = elem.attributes[j];
if (attr.nodeName.indexOf('q-event') >= 0) {
/**
* 調(diào)用屬性指令
*/
var dataKey = elem.getAttribute('ng-bind') || undefined;
/**
* 進行臟數(shù)據(jù)檢測,如果數(shù)據(jù)改變类嗤,則重新執(zhí)行指令糊肠,否則跳過
*/
if(elem.command[attr.nodeValue] !== data[dataKey]){
command[attr.nodeValue].call(elem, data[dataKey]);
elem.command[attr.nodeValue] = data[dataKey];
}
}
}
}
}
第三種方式 采用Object.defineProperty對數(shù)據(jù)對象做屬性get和set的監(jiān)聽,但是需要注意的是為了保存?zhèn)鬟M來的數(shù)值遗锣,并且避免無效循環(huán)货裹,采用如下方法用于獨立的函數(shù),value來存儲對應(yīng)的對應(yīng)的數(shù)值精偿。
function defineProperty(vm, key, val){
Object.defineProperty(vm, key, {
get: function (){
return val;
},
set: function (newValue){
document.getElementById("show").innerHTML = newValue;
document.getElementById("input").value = newValue;
if(newValue === val){
return;
}
val = newValue;
}
});
}
function observe(data, vm){
Object.keys(data).forEach(function(key){
defineProperty(vm, key, data[key]);
});
3.2.2 View->Model
View到Model無非一些可以改變的標簽弧圆,比如input等赋兵,而view到Model基本的思路都是原生的事件的一些方法。比如如下代碼搔预。
document.getElementById('input').addEventListener('keyup', function (e) {
obj.txt = e.target.value;
});
3.2.3 關(guān)于設(shè)計模式
從Model->View以及后面的從 View->Model相信大家也能看到毡惜,其實這三種綁定方式,最大區(qū)別體現(xiàn)在Model->層斯撮。雖然我們可以通過遍歷的方式對應(yīng)地修改對應(yīng)的標簽的屬性经伙。也能通過我們自己指定的標識符比如’v-model‘, 'ng-text','{{}}',甚至比如采用自己的名稱的前綴比如筆者的'sl-text'等等來采用需要雙向綁定的標簽元素采用列表的統(tǒng)一管理,這樣能減少遍歷次數(shù)勿锅,也可以對于v-model綁定的屬性帕膜,通過列表添加到該標簽,作為其的一個屬性溢十,但是是否還能進一步優(yōu)化垮刹。
引用一張''Header First"設(shè)計模式上的觀察者模式一圖,如下:
在javascript沒有像協(xié)議這樣的語法张弛,不過原理還是一致荒典,改良好的雙向數(shù)據(jù)綁定模型如下代碼。
//第三部分
function Watcher(vm, node, name, nodeType){
Dep.target = this;
this.vm = vm;
this.node = node;
this.name = name;
this.nodeType = nodeType;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function(){
this.get();
if (this.nodeType === 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType === 'input') {
this.node.value = this.value;
}
},
get: function(){
this.value = this.vm[this.name];
}
}
function Dep(){
this.subs = [];
}
Dep.prototype = {
addSub: function(sub){
this.subs.push(sub);
},
notify: function(){
this.subs.forEach(function(sub){
sub.update();
});
}
}
//第二部分
function defineProperty(vm, key, val){
var dep = new Dep();
Object.defineProperty(vm, key, {
get: function (){
if(Dep.target){
dep.addSub(Dep.target);
}
return val;
},
set: function (newValue){
if(newValue === val){
return;
}
val = newValue;
dep.notify();
}
});
}
function observe(data, vm){
//Object.keys(data)返回data的key數(shù)組
Object.keys(data).forEach(function(key){
defineProperty(vm, key, data[key]);
});
}
//第一部分
function compile(node, vm){
if(node.nodeType === 1){
var attr = node.attributes;
for(let i = 0; i<attr.length; i++){
if(attr[i].nodeName === 'v-model'){
let name = attr[i].nodeValue;
node.addEventListener('keyup', function(e){
vm[name] = e.target.value;
});
node.value = vm[name];
node.removeAttribute('v-model');
new Watcher(vm, node, name, "input");
}
}
if (child = node.firstChild) {
compile(child, vm);
}
}
if(node.nodeType === 3){
let reg = /\{\{(.*)\}\}/;
if(reg.test(node.nodeValue)){
let name = RegExp.$1;
name = name.trim();
// node.nodeValue = vm.data[name];
new Watcher(vm, node, name, "text");
}
}
}
function nodeToFragment(node, vm){
var flag = document.createDocumentFragment();
var child;
while(child = node.firstChild){
compile(child, vm);
flag.appendChild(child);
}
return flag;
}
function Vue(options){
var id = options.el;
var data = options.data;
observe(data, this);
var dom = nodeToFragment(document.getElementById(id), this);
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
input: 'hello'
}
});
大體邏輯表現(xiàn)為,首先定義觀察者Watcher吞鸭,并在編譯函數(shù)compile()中對每個節(jié)點添加觀察著Watcher寺董,當接收到分發(fā)者指令時,調(diào)用update方法更新視圖刻剥。接下來定義消息分發(fā)者Dep遮咖,Dep維護觀察者數(shù)組,當值發(fā)生變化時造虏,通知各觀察者調(diào)用update方法御吞。
四、附上源碼
參考文獻和鏈接
官網(wǎng)
剖析Vue原理&實現(xiàn)雙向綁定MVVM
廖雪峰MVVM
談?wù)凧avaScript中的雙向數(shù)據(jù)綁定
【JavaScript學(xué)習(xí)筆記】自己實現(xiàn)雙向綁定
剖析Vue原理&實現(xiàn)雙向綁定MVVM