設(shè)計(jì)一個(gè)簡單mvvm例子(vue2.x)

1. 引言

學(xué)習(xí)vue有段時(shí)間了,mvvm在vue中是個(gè)典型應(yīng)用,最近參考了參考網(wǎng)上一些資料,整理了一下,也加入了自己的理解,實(shí)現(xiàn)一個(gè)簡單版的demo,也方便有些面試的同學(xué)遇到設(shè)計(jì)一個(gè)mvvm的面試題。

2. 邏輯結(jié)構(gòu)

mvvm的設(shè)計(jì)模式是“發(fā)布與訂閱者”模式(observe/watcher),主要步驟有三步:

  • observe來劫持并監(jiān)聽所有的屬性(也就是vue中的data)
  • 給每一個(gè)需要監(jiān)聽的屬性,綁定一個(gè)訂閱者(watcher)
  • 當(dāng)observe監(jiān)聽到屬性變化時(shí),通知watcher去更新視圖

ok,步驟講完了,接下來就開始實(shí)現(xiàn)每一步

3. observe

observe的主要功能:

  • 劫持和監(jiān)聽數(shù)據(jù)
  • 當(dāng)數(shù)據(jù)更新時(shí),觸發(fā)通知(后面的dep會講,這里跳過)
    那么observe劫持和監(jiān)聽數(shù)據(jù)的呢?用Object.defineProperty來實(shí)現(xiàn)

先看一個(gè)例子:

function observe (obj) {
   var keys = Object.keys(obj)
   keys.forEach(function(key){
     var val = void 0;
     Object.defineProperty(obj, key, {
        enumerable : true, 
        configurable : true,
        get : function () {
           console.log('這個(gè)屬性是', key)
           return val;
         },
        set : function ( newValue ) {
           val = newValue
           console.log('屬性' + key + '已經(jīng)被監(jiān)聽了,此時(shí)的值是:' + newValue)
         }
     })
   })
}
var book = {page : 300}
observe(book)
var b = book.page   // 后臺會打印 這個(gè)屬性是page
book.page = 400    // 后臺會打印  屬性page已經(jīng)被監(jiān)聽,此時(shí)的值是400

在這里我們就實(shí)現(xiàn)了屬性的監(jiān)聽,上述例子中,我們用Object.defineProperty重寫了set和get函數(shù),使得屬性的值變化時(shí)可以被我們監(jiān)聽到(如果不了解Object.defineProperty的,可以查閱Object.defineProperty

ok,原理我們清楚了,接下來就開始寫observe了

//data對象,key和val分別是data的鍵值對
function defineReactive(data, key, val){
    observe(val)  //遞歸調(diào)用data中的子對象 
    Object.defineProperty(data, key, {
        enumerable : true,  //可枚舉,可在for in 和 Object.keys中得到
        configurable : true,
        get : function () {
            return val;
        },
        set : function ( newValue ) {
            if(val === newValue){
                return;
            }
            val = newValue
            console.log('屬性' + key + '已經(jīng)被監(jiān)聽了,此時(shí)的值是:' + newValue)
        }
    })
}
//觀察者,用來監(jiān)聽數(shù)據(jù)
function observe (obj) {
    if(!obj || typeof obj !== 'object'){
        return;
    }
    Object.keys(obj).forEach(function(key){
        defineReactive(obj, key, obj[key])
    })
}
  • defineReactive函數(shù)的三個(gè)參數(shù),分別是要注冊的對象,對象的key,以及對象的值
  • defineReactive中調(diào)用observe,目的是遞歸調(diào)用所有的屬性

3. watcher

observe寫完了,接下來我們就要看watcher了,因?yàn)槊總€(gè)屬性都綁定一個(gè)watcher,所以可能會有很多的watcher,因此我們需要一個(gè)調(diào)度中心(暫時(shí)定義為Dep),來統(tǒng)一指揮watcher

Dep的主要功能:

  • 將每個(gè)watcher都push進(jìn)去
  • 當(dāng)接收到observe的屬性更新通知時(shí),通知對應(yīng)的watcher來更新視圖

接下來上代碼

//訂閱器,用來收集訂閱者,并且通知訂閱者更新函數(shù)
function Dep(){
    this.subs = []
}
Dep.prototype = {
    addSub : function (sub){
        this.subs.push(sub)
    },
    notify : function (){
        this.subs.forEach(function(sub){
            sub.update()
        })
    }
}

Dep已經(jīng)定義好了,接下來我們需要改一下observe,將dep加進(jìn)去,這樣我們就實(shí)現(xiàn)了在get函數(shù)中將屬性注冊一個(gè)watcher再push進(jìn)dep中,并且set函數(shù)中數(shù)據(jù)更新時(shí)通dep,dep會再通知watcher去更新視圖

function defineReactive(data, key, val){
    observe(val)  //遞歸調(diào)用data中的子對象 
    var dep = new Dep();
    Object.defineProperty(data, key, {
        enumerable : true,  //可枚舉,可在for in 和 Object.keys中得到
        configurable : true,
        get : function () {
            // 這里目的是定義一個(gè)flag,用來判斷什么時(shí)候需要push一個(gè)sub
            //因?yàn)椴荒苊看握{(diào)用屬性都push一個(gè)sub,只有在第一次時(shí)才需要push
            if(Dep.target){   
                dep.addSub(Dep.target)
            }
            return val;
        },
        set : function ( newValue ) {
            if(val === newValue){
                return;
            }
            val = newValue
            console.log('屬性' + key + '已經(jīng)被監(jiān)聽了,此時(shí)的值是:' + newValue)
            dep.notify()
        }
    })
}

到這里Dep調(diào)度中心就完成了,接下來我們實(shí)現(xiàn)watcher

watcher的主要功能:

  • observe中g(shù)et函數(shù)只是定義了一個(gè)watcher,但是觸發(fā)這個(gè)get函數(shù)需要在這里,這樣就完成了注冊
  • 接到dep的更新通知后,調(diào)用更新函數(shù)

ok,先實(shí)現(xiàn)代碼:

function Watcher (vm, exp, cb) {
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.value = this.get()
}
Watcher.prototype = {
    update : function(){
        this.run()
    },
    run : function (){
        var value = this.vm.data[this.exp]
        var oldVal = this.value
        if (value !== oldVal) {
            this.value = value
            this.cb.call(this.vm, value, oldVal)
        }
    },
    get:function(){
        Dep.target = this // 這里設(shè)置一下target,下一行代碼會直接調(diào)用屬性的get函數(shù),會用到target
        var value = this.vm.data[this.exp]
        Dep.target = null 
        return value
    }
}
  • Watcher的三個(gè)參數(shù),分別是vue,要訂閱的屬性,以及回調(diào)函數(shù)(觸發(fā)更新時(shí)調(diào)用的函數(shù))
  • this.value = this.get()這行代碼就是初始化就去獲取這個(gè)屬性值,這樣就會調(diào)用observe中的get函數(shù),然后將watcher加入到Dep中去。
  • Dep.target = this 就是上文中提到的只有target有值時(shí)才會將watcher加入到Dep中

ok,到這里最簡易版本的mvvm已經(jīng)完成了
然后我們定義一個(gè)vue:

// data 是所有的屬性,el是綁定的元素節(jié)點(diǎn)(#app),exp是綁定的屬性
function dVue (data, el, exp) {
    this.data = data;
    observe(data);
    el.innerHTML = this.data[exp];  // 初始化模板數(shù)據(jù)的值
    new Watcher(this, exp, function (value) {
        el.innerHTML = value;
    });
    return this;
}

在html中調(diào)用

<body>
    <h1 id="app">{{math}}</h1>
</body>
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/index.js"></script>
<script type="text/javascript">
    var ele = document.querySelector('#app');
    var dVue = new dVue({
        math : '1'
    }, ele, 'math');
 
    setInterval(function () {
        dVue.data.math = Math.random() * 100
    }, 1000);
 
</script>

OK,接下來我們完善一下,實(shí)現(xiàn)vue中的{{ }}綁定


4. compile

compile主要功能:

  • 獲取模板,并且解析模板,將數(shù)據(jù)替換模板,完成初始化視圖
  • 給模板中綁定的屬性,new初始化一個(gè)watcher(之前是在dVue函數(shù)中完成的,現(xiàn)在移到這里)
 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('節(jié)點(diǎn)不存在')
        }
    },
    nodeToFragment : function(el){
        //創(chuàng)建一個(gè)虛擬的文檔片段,用來操作dom節(jié)點(diǎn),因?yàn)檫@個(gè)片段是存在于內(nèi)存中
        //所以相對于直接操作dom,性能會更好一點(diǎn)
        var fragment = document.createDocumentFragment()
        var child = el.firstChild
        while(child){
            fragment.appendChild(child)
            child = el.firstChild
        }

        return fragment
    },
    compileElement :function(el){
        var childNodes = el.childNodes
        var self = this;
        Array.prototype.slice.call(childNodes).forEach(function(node){
            var reg = /\{\{\s*(.*?)\s*\}\}/
            var text = node.textContent;
            //判斷該節(jié)點(diǎn)是否含有{{ }}這個(gè)指令
            if(self.isTextNode(node) && reg.test(text)){
                self.compileText(node, reg.exec(text)[1])
            }
            if(node.childNodes && node.childNodes.length){
                self.compileElement(node)
            }
        })
    },
    compileText :function(node, exp){
        var self = this
        var initText = this.vm[exp]
        this.updateText(node, initText)  //初始化視圖
        new Watcher(this.vm, exp, function(value){
            self.updateText(node, value)
        })
    },
    updateText : function(node, value){
        node.textContent = typeof value == 'undefined' ? '' : value
    },
    isTextNode : function(node){
        return node.nodeType == 3
    }
 }
  • nodeToFragment是在內(nèi)存建立一個(gè)虛擬的節(jié)點(diǎn),然后將模板賦值給它,再繼續(xù)操作模板,這樣可以提升性能,參考文檔nodeToFragment
  • compileElement這個(gè)函數(shù),解析模板,找到{{ }}指令的文本節(jié)點(diǎn),然后運(yùn)行核心函數(shù)compileText,解析文本節(jié)點(diǎn)
  • compileText這個(gè)函數(shù)中,做了兩件事,第一件事是初始化視圖,也就是調(diào)用updateText函數(shù),第二件事就是給這個(gè)文本節(jié)點(diǎn)綁定一個(gè)watcher,用于訂閱該屬性,當(dāng)屬性值改變時(shí),會調(diào)用里面的回調(diào)函數(shù)

到這里compile就完成了,這樣我們需要把dVue重新修改一下

function dVue (options) {
    var self = this
    this.data = options.data
    this.vm = this
    // 將data上的屬性掛載到vue上,this.data.a === this.a
    Object.keys(this.data).forEach((key)=>{
        this.proxyKeys(key)
    })
    //重寫所有的data屬性的set和get方法,用于劫持監(jiān)聽數(shù)據(jù)
    observe(this.data)
    //編譯模板,得到綁定的節(jié)點(diǎn),初始化視圖,并且給該節(jié)點(diǎn)所綁定的屬性注冊一個(gè)watcher
    new Compile(options.el, this)
    return this
}
//代理一下屬性,這樣的話 dVue.name = dVue.data.name ,不用每次都帶著data了
//相當(dāng)于把data的所有屬性都注冊到了dVue上
dVue.prototype = {
    proxyKeys : function(key){
        var self = this
        Object.defineProperty(this, key, {
            enumerable : false,
            configurable : true,
            get : function(){
                return self.data[key]
            },
            set :function(val){
                self.data[key] = val
            }
        })
    }
}

到這里就基本結(jié)束了,我們在html中調(diào)用一下

<html>
<head>
    <title>實(shí)現(xiàn)一個(gè)簡單的mvvm</title>
</head>
<body>
<div id = "app">
    <div>{{name}}</div>
    <div>{{age}}</div>
    <div>{{like}}</div>
</div>

<script type="text/javascript" src="./observe.js"></script>
<script type="text/javascript" src="./watcher.js"></script>
<script type="text/javascript" src="./compile.js"></script>
<script type="text/javascript" src="./dVue.js"></script>
<script>
    let dVueInit = new dVue({
        el : '#app',
        data:{
            name : 'ding',
            age : 14,
            like : '讀書'
        }
    })
</script>
</body>
</html>

到此結(jié)束!

參考文章:
https://www.cnblogs.com/libin-1/p/6893712.html
https://github.com/canfoo/self-vue/tree/master/v2

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容