視圖模板引擎——Vue【雙向綁定】原理剖析

首先我們來了解一下MVC、MVP、MVMM這三大架構(gòu)模式在前端角度上的理解。

MVC分別是 Model(模型)、View(視圖)、Controller(控制器)三個(gè)模塊。View(視圖層)最主要完成前端的數(shù)據(jù)展示,Controller(控制層)是對(duì)數(shù)據(jù)的接收和觸發(fā)事件的接收和傳遞,Model(模型層)則是對(duì)數(shù)據(jù)的儲(chǔ)存和處理,再傳遞給視圖層相應(yīng)或者展示。如下圖所示,整個(gè)過程是單鏈條的傳遞,在前端開發(fā)中多將業(yè)務(wù)邏輯寫在View層,使得View層比較厚,而Controller層比較薄。
缺點(diǎn):
1、 開發(fā)者在代碼中大量調(diào)用相同的 DOM API,處理繁瑣 ,操作冗余,使得代碼難以維護(hù)。

2、大量的DOM 操作使頁面渲染性能降低,加載速度變慢,影響用戶體驗(yàn)。
  
3、 當(dāng) Model 頻繁發(fā)生變化,開發(fā)者需要主動(dòng)更新到View ;當(dāng)用戶的操作導(dǎo)致 Model 發(fā)生變化,開發(fā)者同樣需要將變化的數(shù)據(jù)同步到Model 中,這樣的工作不僅繁瑣,而且很難維護(hù)復(fù)雜多變的數(shù)據(jù)狀態(tài)。

MVP是Model(模型)、View(視圖)、Presenter(表示器)組成。MVP架構(gòu)模式最主要是針對(duì)Android的MVC架構(gòu)模式進(jìn)行改進(jìn)的,MVP與MVC最不同的一點(diǎn)是M與V是不直接關(guān)聯(lián)的也是就Model與View不存在直接關(guān)系,這兩者之間間隔著的是Presenter層,其負(fù)責(zé)調(diào)控View與Model之間的間接交互。


MVVM 架構(gòu)模式最主要是針對(duì)前端和iOS的MVC架構(gòu)模式進(jìn)行改進(jìn)的,減輕Controller層或者View層的壓力,實(shí)現(xiàn)更加清晰化代碼。通過對(duì)ViewModel層的封裝:封裝業(yè)務(wù)邏輯處理,封裝網(wǎng)絡(luò)處理、封裝數(shù)據(jù)緩存等,讓邏輯處理分離出來,并且不需要處理Model數(shù)據(jù),使得Controller層或者View層結(jié)構(gòu)簡(jiǎn)單,條理清晰。MVVM模式的優(yōu)點(diǎn)在于當(dāng)view和viewmodel的雙向綁定,當(dāng)數(shù)據(jù)改變后不需要改修改DOM結(jié)構(gòu)。MVVM的實(shí)現(xiàn)原理:利用Object.defineProperty(),該方法有g(shù)et、set兩個(gè)屬性方法,從而獲取對(duì)象屬性的值,給對(duì)象屬性重新賦值。
在MVVM架構(gòu)下,View 和 Model 之間并沒有直接的聯(lián)系,而是通過ViewModel進(jìn)行交互,Model 和 ViewModel 之間的交互是雙向的, 因此View 數(shù)據(jù)的變化會(huì)同步到Model中,而Model 數(shù)據(jù)的變化也會(huì)立即反應(yīng)到View 上。

視圖變化更新數(shù)據(jù),數(shù)據(jù)變化更新視圖



















Vue是框架嗎?算是吧。

Vue的核心定位并不是一個(gè)框架,設(shè)計(jì)上也沒有完全遵循MVVM模式,Vue的核心功能強(qiáng)調(diào)的是狀態(tài)到界面的映射,對(duì)于代碼的結(jié)構(gòu)組織并不重視, 所以單純只使用其核心功能時(shí),它并不是一個(gè)框架,而更像一個(gè)視圖模板引擎。

Vue的使用方式——漸進(jìn)式,那么漸進(jìn)式是什么意思?

在聲明式渲染(視圖模板引擎)的基礎(chǔ)上,我們可以通過添加組件系統(tǒng)、客戶端路由、大規(guī)模狀態(tài)管理、構(gòu)建工具來構(gòu)建一個(gè)完整的框架。更重要的是,這些功能相互獨(dú)立,你可以在核心功能的基礎(chǔ)上任意選用其他的部件,不一定要全部整合在一起。

那么我們來分析一下Vue的雙向綁定到底是什么?

下面的代碼是使用v-model命令實(shí)現(xiàn)的雙向綁定。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>v-model</title>
</head>
<body>
<div id="app">
    <input type="text" v-model="msg">
    <p>{{msg}}</p>
    <select v-model="leval">
        <option value="1">1</option>
        <option value="2">2</option>
        <option value="3">3</option>
    </select>
    <p>{{leval}}</p>
</div>
</body>
<script src="https://unpkg.com/vue@2.6.10/dist/vue.js"></script>
<script>
    let vm=new Vue({
        el:"#app",
        data:{
            msg:"",
            leval:1
        }

    })
</script>
</html>


那么這樣實(shí)現(xiàn)的原理是什么?

Vue的雙向綁定是由數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式實(shí)現(xiàn)的,那么什么是數(shù)據(jù)劫持?vue是如何進(jìn)行數(shù)據(jù)劫持的?說白了就是通過Object.defineProperty()來劫持對(duì)象屬性的setter和getter操作,在數(shù)據(jù)變動(dòng)時(shí)做你想要做的事情。

我們可以看一下通過控制臺(tái)梳理一個(gè)定義在vue初始化數(shù)據(jù)上的對(duì)象是什么.

    let vm=new Vue({
        el:"#app",
        data:{
            msg:"",
            leval:1,
            name:{
                le:'maomin'
            }
        },
        created(){
            console.log(this.name)
        }

    })




如上圖所示:name對(duì)象有兩個(gè)方法。分別是get、set。那么這兩個(gè)方法就是Vue通過Object.definePreperty()進(jìn)行數(shù)據(jù)劫持得到的。

這個(gè)方法會(huì)直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性, 并返回這個(gè)對(duì)象。

Object.definePreperty(data,key,desriptor)

//data 要在其上定義屬性的對(duì)象;
//key  要定義或修改的屬性的名稱;(這里的類型是String)
//descriptor 將被定義或修改的屬性描述符;(一般為set、get方法的對(duì)象組合)



那么我們給個(gè)關(guān)于Object.definePreperty()方法的例子看一下。

先來一個(gè)這個(gè)。

var person = {
        name:'maomin'
    };
    console.log('這個(gè)帥哥叫'+person.name)//這個(gè)帥哥叫maomin



但是你想在執(zhí)行console.log的同時(shí),加上他的女朋友是M。那么可以使用Object.defineProperty()方法來執(zhí)行。

    var person = {
        name:'maomin'
    }
    var name1 = ''
    Object.defineProperty(person,'name',{
        set:function (val) {
            name1=val
            console.log('這是set方法')
            console.log('他的名字叫'+val)
        },
        get:function () {
            console.log('這是get方法')
           return name1 +'他的女朋友叫M'
        }
    })
    console.log('這個(gè)帥哥叫'+person.name)
    /*
    輸出結(jié)果:
    這是get方法
    這個(gè)帥哥叫他的女朋友叫M
    */


但你會(huì)發(fā)現(xiàn)沒有執(zhí)行set()方法,那么我們這樣試試。

    var person = {}
    var name1 = ''
    Object.defineProperty(person,'name',{
        set:function (val) {
            name1=val
            console.log('這是set方法')
            console.log('他的名字叫'+val)
        },
        get:function () {
            console.log('這是get方法')
           return name1 +'他的女朋友叫M'
        }
    })
    person.name ='maomin' //在Object.defineProperty()后賦初始值
    console.log('這個(gè)帥哥叫'+','+person.name)
    /*
    輸出結(jié)果:
    這是set方法
    他的名字叫maomin
    這是get方法
    這個(gè)帥哥叫maomin,他的女朋友叫M
    */



這樣set方法才會(huì)執(zhí)行。
所以你在針對(duì)一個(gè)對(duì)象修改屬性值的時(shí)候一定要在Object.defineProperty()方法后給屬性賦初始值。

那么講完初步了解數(shù)據(jù)劫持了,那么什么是發(fā)布者—訂閱者設(shè)計(jì)模式呢?

比如說根據(jù)微信訂閱號(hào)的例子來講,如果你是訂閱號(hào)文章的發(fā)表者,那么怎樣才能讓別人實(shí)時(shí)看到你的發(fā)表的文章呢?那就是這個(gè)人已經(jīng)關(guān)注了你的訂閱號(hào)。這樣你發(fā)表了文章,關(guān)注的人才會(huì)看到。所以,我們的思路大體是:

1、初始化發(fā)布者、訂閱者。
2、訂閱者需要注冊(cè)到發(fā)布者,發(fā)布者發(fā)布消息時(shí),依次向訂閱者發(fā)布消息。



我們?cè)谇懊婵吹搅薕bject.defineProperty()中的set()方法當(dāng)屬性變化時(shí)會(huì)觸發(fā)。那么可以利用這個(gè)特性可以把需要更新的方法放在set里面,這樣就實(shí)現(xiàn)數(shù)據(jù)層更新,進(jìn)而頁面層就更新了。頁面層更新,數(shù)據(jù)層更新使用事件監(jiān)聽就可以了。

實(shí)現(xiàn)了雙向綁定,那么我們?cè)趺礃訉?shí)現(xiàn)數(shù)據(jù)劫持監(jiān)聽呢?

這里需要三步:

1.實(shí)現(xiàn)一個(gè)監(jiān)聽器Observer,用來劫持并監(jiān)聽所有屬性,如果有變動(dòng)的,就通知訂閱者。
 
2.實(shí)現(xiàn)一個(gè)訂閱者Watcher,可以收到屬性的變化通知并執(zhí)行相應(yīng)的函數(shù),從而更新視圖。
 
3.實(shí)現(xiàn)一個(gè)解析器Compile,可以掃描和解析每個(gè)節(jié)點(diǎn)的相關(guān)指令,并根據(jù)初始化模板數(shù)據(jù)以及初始化相應(yīng)的訂閱器。




1、observer監(jiān)聽器

下面我們將通過observer()方法進(jìn)行遍歷向下找到所有的屬性,并通過defineReactive()方法進(jìn)行數(shù)據(jù)劫持監(jiān)聽。

//對(duì)所有屬性都要蔣婷,遞歸遍歷所有屬性
function defineReactive(data,key,val) {
    observer(val);  //遞歸遍歷所有的屬性,如果不加的話,遍歷不了book1內(nèi)部的元素
    Object.defineProperty(data,key,{
        // writable: false,      //當(dāng)且僅當(dāng)該屬性的writable為true時(shí),value才能被賦值運(yùn)算符改變。默認(rèn)為 false。當(dāng)定義了一個(gè)屬性的set、get描述符,則JavaScript會(huì)忽略該屬性的value、writable屬性。也就是說這倆對(duì)兒屬于互斥的關(guān)系.
        enumerable:true,         //當(dāng)且僅當(dāng)該屬性的 enumerable 為 true 時(shí),該屬性描述符才能夠被改變,同時(shí)該屬性也能從對(duì)應(yīng)的對(duì)象上被刪除。
        configurable:true,       //當(dāng)且僅當(dāng)該屬性的configurable為true時(shí),該屬性才能夠出現(xiàn)在對(duì)象的枚舉屬性中
        set:function(value) {
            val = value;
            console.log('屬性'+key+'已經(jīng)被監(jiān)聽,現(xiàn)在值為:"'+value.toString()+'"');
        },
        get:function() {
            return val;
        },
    })
}
 
function observer(data) {
    if(!data || typeof data !== 'object') {
        return;
    }
    Object.keys(data).forEach(function(key){
        defineReactive(data,key,data[key]);
    });
}
 
var language = {
    book1: {
        name: ''
    },
    book2: '',
    name2:'',
    name3:''
};
observer(language);
language.book1.name = 'Vue';
language.name2 = 'HTML';
language.name2 = 'js';
language.book2 = 'css';
/*
輸出結(jié)果:
屬性name已經(jīng)被監(jiān)聽了,現(xiàn)在值為:“Vue”
屬性name2已經(jīng)被監(jiān)聽了,現(xiàn)在值為:“HTML”
屬性name2已經(jīng)被監(jiān)聽了,現(xiàn)在值為:“js”
屬性book2已經(jīng)被監(jiān)聽了,現(xiàn)在值為:“css”
*/



我們需要一個(gè)可以容納消息訂閱者的消息訂閱器Dep,訂閱器主要收集消息訂閱者,然后在屬性變化時(shí)執(zhí)行相應(yīng)訂閱者的更新函數(shù),那么消息訂閱器Dep需要有一個(gè)容器,用來存放消息訂閱者.

//對(duì)所有屬性都要蔣婷,遞歸遍歷所有屬性
function defineReactive(data,key,val) {
    observer(val);  //遞歸遍歷所有的屬性,如果不加的話,遍歷不了book1內(nèi)部的元素
    var dep = new Dep();
    Object.defineProperty(data,key,{
        // writable: false,      //當(dāng)且僅當(dāng)該屬性的writable為true時(shí),value才能被賦值運(yùn)算符改變。默認(rèn)為 false。當(dāng)定義了一個(gè)屬性的set、get描述符,則JavaScript會(huì)忽略該屬性的value、writable屬性。也就是說這倆對(duì)兒屬于互斥的關(guān)系.
        enumerable:true,         //當(dāng)且僅當(dāng)該屬性的 enumerable 為 true 時(shí),該屬性描述符才能夠被改變,同時(shí)該屬性也能從對(duì)應(yīng)的對(duì)象上被刪除。
        configurable:true,       //當(dāng)且僅當(dāng)該屬性的configurable為true時(shí),該屬性才能夠出現(xiàn)在對(duì)象的枚舉屬性中
        set:function(value) {
              if (val === value) {
                return;
            }
            val = value;
            console.log('屬性' + key + '已經(jīng)被監(jiān)聽了,現(xiàn)在值為:“' + value.toString() + '”');
            dep.notify(); // 如果數(shù)據(jù)變化,通知所有訂閱者
            // val = value;
            // console.log('屬性'+key+'已經(jīng)被監(jiān)聽,現(xiàn)在值為:"'+value.toString()+'"');
        },
        get:function() {
              if (Dep.target) {    //Watcher初始化觸發(fā)
                dep.addSub(Dep.target); // 在這里添加一個(gè)訂閱者
            }
            return val;
        },
    })
}
 
function observer(data) {
    if(!data || typeof data !== 'object') {
        return;
    }
    Object.keys(data).forEach(function(key){
        defineReactive(data,key,data[key]);
    });
}
 
function Dep() {
    this.subs = [];
}


Dep.prototype = {                        
    addSub:function(sub) {
        this.subs.push(sub);
    },
    notify:function() {
        this.subs.forEach(function(sub) {
            sub.update();        //通知每個(gè)訂閱者檢查更新
        })
    }
}
Dep.target = null;


var language = {
    book1: {
        name: ''
    },
    book2: '',
    name2:'',
    name3:''
};
observer(language);
language.book1.name = 'Vue';
language.name2 = 'HTML';
language.name2 = 'js';
language.book2 = 'css';


為了后面的watcher,我們先將上面所有代碼整體修改一下。得到下面最終的Observer:

function Observer(data) {
    this.data = data;
    this.walk(data);
}
Observer.prototype = {
    walk: function(data) {
        var that = this;
        //遍歷所有的屬性
        Object.keys(data).forEach(function(key) {
            that.defineReactive(data, key, data[key]);
        });
    },
    defineReactive: function(data, key, val) {
        var dep = new Dep();
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            set: function(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                dep.notify(); // 如果數(shù)據(jù)變化,通知所有訂閱者
            },
             get: function () {
                 if (Dep.target) { //是否添加訂閱者,Watcher初始化觸發(fā)
                     dep.addSub(Dep.target); //添加訂閱者
                 }
                 return val;
             }
        });
    }
};

function observe(value, vm) {
    if (!value || typeof value !== 'object') {
        return;
    }
    return new Observer(value);
};

function Dep () {
    this.subs = [];
}

//prototype 屬性使您有能力向?qū)ο筇砑訉傩院头椒?br>//prototype這個(gè)屬性只有函數(shù)對(duì)象才有,具體的說就是構(gòu)造函數(shù)具有.只要你聲明定義了一個(gè)函數(shù)對(duì)象,這個(gè)prototype就會(huì)存在
//對(duì)象實(shí)例是沒有這個(gè)屬性
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();  //通知每個(gè)訂閱者檢查更新
        });
    }
};
Dep.target = null;



2、watcher訂閱者

訂閱者Wahcher在初始化時(shí)要將自己添加到訂閱器Dep中。

我們已經(jīng)知道監(jiān)聽器Observer是在get函數(shù)中執(zhí)行了添加訂閱者的操作的,所以我們只需要在訂閱者Watcher在初始化時(shí)觸發(fā)相對(duì)應(yīng)的get函數(shù)來執(zhí)行添加訂閱者的操作即可。那么怎么觸發(fā)對(duì)應(yīng)的get函數(shù)呢?我們只需要獲取對(duì)應(yīng)的屬性值,就可以通過Object.defineProperty( )觸發(fā)對(duì)應(yīng)的get了。

在這里需要注意一個(gè)細(xì)節(jié),我們只需要在訂閱者初始化時(shí)才執(zhí)行添加訂閱者,所以我們需要一個(gè)判斷,在Dep.target上緩存一下訂閱者,添加成功后去除就行了。

function Watcher(vm, exp, cb) {
    this.cb = cb; //閉包
    this.vm = vm; //指向SelfVue的作用域
    this.exp = exp; //綁定屬性的key值
    this.value = this.get();  // 將自己添加到訂閱器的操作
}

Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.vm.data1[this.exp];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        Dep.target = this;  // 緩存自己
        var value = this.vm.data1[this.exp]  // 強(qiáng)制執(zhí)行監(jiān)聽器里的get函數(shù)
        Dep.target = null;  // 釋放自己
        return value;
    }
};



在這里我們已經(jīng)將上面的Observer已經(jīng)修改,這里不再講述。
3、整合(將訂閱者跟監(jiān)聽器連接起來)

<body>
    <div id="name">{{name}}</div>
</body>



    var ele = document.querySelector('#name');
    var selfVue = new SelfVue({
        name: '你好,maomin'
    }, ele, 'name');

    window.setTimeout(function () {
        selfVue.name = '你也好';
    }, 5000);



以上代碼是展示頁面。

接下來我們將寫創(chuàng)建初始對(duì)象的邏輯代碼:

function SelfVue (data, el, exp) {
    var that = this;
    console.log(this)
    this.data1 = data;
    //遍歷所有data里的屬性
    Object.keys(data).forEach(function(key) {
        that.proxyKeys(key);
    });
   //將Observer和Watcher關(guān)聯(lián)起來
    observe(data);
    console.log(this.data1[exp])
    el.innerHTML = this.data1[exp];  // 初始化模板數(shù)據(jù)的值
    new Watcher(this, exp, function (value) {
        el.innerHTML = value;
    });
    return this;
}

 SelfVue.prototype = {
    proxyKeys: function (key) {
        var that = this;
        console.log(this)
        Object.defineProperty(this, key, {
            enumerable: false,
            configurable: true,
            set: function proxySetter(newVal) {
                that.data1[key] = newVal;
            },
             get: function proxyGetter() {
                 return that.data1[key];
             }
        });
    }
}


這樣一個(gè)超簡(jiǎn)易的Vue 雙向綁定這樣就實(shí)現(xiàn)了,這里都是死的數(shù)據(jù),沒有寫compile指令解析器。

上面的雙向綁定demo中,我們發(fā)現(xiàn)整個(gè)過程都沒有解析dom節(jié)點(diǎn),而是固定某個(gè)節(jié)點(diǎn)進(jìn)行替換數(shù)據(jù)
4、Compile指令解析器

指令解析器作用:

1.解析模板指令,并替換模板數(shù)據(jù),初始化視圖
2.將模板指令對(duì)應(yīng)的節(jié)點(diǎn)綁定對(duì)應(yīng)的更新函數(shù),初始化相應(yīng)的訂閱器

function Compile(el, vm) {
    this.vm = vm;
    this.el = document.querySelector(el);
    this.fragment = null;
    this.init();
}

Compile.prototype = {
    init: function () {
        if (this.el) {
            this.fragment = this.nodeToFragment(this.el);
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
        } else {
            console.log('Dom元素不存在');
        }
    },
    //首先要獲得dom元素, 然后對(duì)含有dom元素上含有指令的節(jié)點(diǎn)進(jìn)行處理
    nodeToFragment: function (el) {
        var fragment = document.createDocumentFragment(); //createdocumentfragment()方法創(chuàng)建了一虛擬的節(jié)點(diǎn)對(duì)象,節(jié)點(diǎn)對(duì)象包含所有屬性和方法。
        var child = el.firstChild;
        while (child) {
            // 將Dom元素移入fragment中
            fragment.appendChild(child);
            child = el.firstChild
        }
        return fragment;
    },
    //接下來需要遍歷所有節(jié)點(diǎn), 對(duì)含有指令的節(jié)點(diǎn)進(jìn)行特殊的處理, 這里我們先處理最簡(jiǎn)單的情況, 只對(duì)帶有 '{{變量}}'這種形式的指令進(jìn)行處理
    compileElement: function (el) {
        var childNodes = el.childNodes; //childNodes屬性返回節(jié)點(diǎn)的子節(jié)點(diǎn)集合,以 NodeList 對(duì)象。
        var that = this;
         //slice() 方法可從已有的數(shù)組中返回選定的元素。
        [].slice.call(childNodes).forEach(function(node) {
            var reg = /\{\{(.*)\}\}/;
            var text = node.textContent;

            if (that.isTextNode(node) && reg.test(text)) {  // 判斷是否是符合這種形式{{}}的指令
             //exec() 方法用于檢索字符串中的正則表達(dá)式的匹配。
             //返回一個(gè)數(shù)組,其中存放匹配的結(jié)果。如果未找到匹配,則返回值為 null。
                that.compileText(node, reg.exec(text)[1]);
            }

            if (node.childNodes && node.childNodes.length) {
                that.compileElement(node);  // 繼續(xù)遞歸遍歷子節(jié)點(diǎn)
            }
        });
    },
    compileText: function(node, exp) {
        var that = this;
        var initText = this.vm[exp];
        this.updateText(node, initText);  // 將初始化的數(shù)據(jù)初始化到視圖中
        new Watcher(this.vm, exp, function (value) { // 生成訂閱器并綁定更新函數(shù)
            that.updateText(node, value);
        });
    },
    updateText: function (node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },
    isTextNode: function(node) {
        return node.nodeType == 3;
    }
}



這時(shí)在創(chuàng)建初始對(duì)象修改


function SelfVue (options) {
    var that = this;
    this.vm = this;
    this.data1 = options.data;
    //遍歷所有data里的屬性
    Object.keys(this.data).forEach(function(key) {
        that.proxyKeys(key);
    });

    observe(this.data);
    new Compile(options.el, this.vm); //加入這段代碼
    return this;
}

SelfVue.prototype = {
    proxyKeys: function (key) {
        var that = this;
        Object.defineProperty(this, key, {
            enumerable: false,
            configurable: true,
            get: function proxyGetter() {
                return that.data1[key];
            },
            set: function proxySetter(newVal) {
                that.data1[key] = newVal;
            }
        });
    }
}



展示頁面中修改:

<body>
    <div id="app">
        <h1>{{title}}</h1>
        <h2>{{name}}</h2>
        <h3>{{content}}</h3>
    </div>
</body>
<script src="js/observer2.js"></script>
<script src="js/Watcher1.js"></script>
<script src="js/compile1.js"></script>
<script src="js/index3.js"></script>
 
 
<script>
    var selfVue = new SelfVue({
        el:'#app',
        data:{
            title:'title1',
            name:'name1',
            content:'content1'
        }
    });
    window.setTimeout(function() {
        selfVue.title = 'title2';
        selfVue.name = 'name2';
        selfVue.content = 'content2'
    },5000);
</script>



最后,一個(gè)基本的Vue數(shù)據(jù)雙向綁定就這樣實(shí)現(xiàn)了。

作者:Vam的金豆之路

主要領(lǐng)域:前端開發(fā)

我的微信:maomin9761

微信公眾號(hào):前端歷劫之路