響應(yīng)式數(shù)據(jù)原理---訂閱發(fā)布模式

話不多說來張圖

數(shù)據(jù)劫持Observer

????????所謂數(shù)據(jù)劫持就是給對象的每一個屬性增加get,set方法
1.觀察對象,給對象增加Object.defineProperty
2.vue特點(diǎn)是不能新增不存在的屬性,不存在的屬性沒有g(shù)et和set
3.深度響應(yīng),因?yàn)槊看钨x予一個新對象時會給這個新對象增加defineProperty

// 創(chuàng)建一個Observer構(gòu)造函數(shù)
function Observe(data) {
    let dep = new Dep()
    // 既然要給對象的每一個屬性增加get、set,那就先遍歷一遍對象
    for(let key in data) {
        let val = data[key]
        // 遞歸繼續(xù)向下找,實(shí)現(xiàn)深度的數(shù)據(jù)劫持
        observe(val)
        Object.defineProperty(data, key, {
            configurable: true,
            get() {
                // 當(dāng)獲取值的時候就會自動調(diào)用get方法,于是在數(shù)據(jù)劫持observe修改一下get方法,將watcher添加到訂閱事件中
                // Dep.target && dep.addSub(Dep.target)
                if (Dep.target) {
                    dep.depend()   // 和上面一行代碼的意思是一樣的
                }
                return val
            },
            set(newVal) {
                // 如果設(shè)置的新值和以前的值一樣,就不處理
                if (val === newVal) {
                    return
                }
                val = newVal
                // 當(dāng)設(shè)置完新值后,也需要把新值再去數(shù)據(jù)劫持(不然新值的屬性沒有g(shù)et和set方法)
                observe(newVal)
                // 讓所有watcher的update方法執(zhí)行
                dep.notify()
            }
        })
    }
}

數(shù)據(jù)代理

????????數(shù)據(jù)代理就是讓我們每次取data里面的數(shù)據(jù)時,不用每次都寫一長串,比如mvvm._data.album.name這種,我們可以直接寫成mvvm.album.name這種顯而易見的方式。

for(let key in data) {
    Object.defineProperty(this, key, {
        configurable: true,
        get() {
            return this._data[key]
        },
        set(newVal) {
            this._data[key] = newVal
        }
    })
}

數(shù)據(jù)編譯Compile

????????options中的el參數(shù),為我們指定了需要編譯哪些內(nèi)容,而我們需要做的僅僅是解析出通過v-model、v-text、{{}}等等標(biāo)識和指令,然后獲取綁定數(shù)據(jù)的值,替換掉標(biāo)識的內(nèi)容,并進(jìn)行數(shù)據(jù)的變化監(jiān)聽watcher,當(dāng)再有值發(fā)生變化時,可以及時通知其修改對應(yīng)dom元素。

function Compile(el, vm) {
    // 講el掛載到實(shí)例上方便調(diào)用
    vm.$el = document.querySelector(el)

    // 創(chuàng)建一個新的空白的文檔片段,在el范圍里將內(nèi)容都拿到,當(dāng)然不能一個一個的拿,可以選擇移到內(nèi)存中去,然后放入文檔碎片中,節(jié)省開銷
    // DocumentFragment是DOM節(jié)點(diǎn),它不是DOM樹的一部分,通常的用例是創(chuàng)建文檔片段,講元素附加到文檔片段,然后將文檔片段附加到DOM樹,在DOM樹中,文檔片段將其所有的子元素所代替。因?yàn)槲臋n片段存在于內(nèi)存中,并不在DOM樹中,所以將子元素插入到文檔片段時不會引起頁面回流(對元素位置和幾何上的計算)。因此使用文檔片段通常會帶來更多好的性能。
    let fragment = document.createDocumentFragment()
    while (child = vm.$el.firstChild) {
        // 將el中的內(nèi)容放入到內(nèi)存中
        fragment.appendChild(child)
    }

    // 對el里面的內(nèi)容進(jìn)行替換
    function replace(frag) {
        Array.from(frag.childNodes).forEach(node => {
            let txt = node.textContent
            // 正則匹配{{}}
            let reg = /\{\{(.*?)\}\}/g
            // 如果既是文本節(jié)點(diǎn)又有大括號
            if (node.nodeType === 3 && reg.test(txt)) {
                function replaceTxt() {
                    node.textContent = txt.replace(reg, (matched, placeholder) => {
                 
                        // 我們需要訂閱一個事件,當(dāng)數(shù)據(jù)改變的時候需要重新刷新視圖,這就需要在replace替換的邏輯來進(jìn)行處理
                        // 通過new Watcher 把數(shù)據(jù)訂閱一下,數(shù)據(jù)一變就執(zhí)行改變內(nèi)容的操作
                        // 監(jiān)聽變化,給watcher再添加兩個參數(shù),用來取新的值給回調(diào)函數(shù)
                        new Watcher(vm, placeholder, replaceTxt)
                         return placeholder.split('.').reduce((val, key) => {
                             return val[key]
                         }, vm)
                        // 舉個例子解釋一下上面的代碼
                        // 'album.name'.split('.') => ['album','name'] => ['album','name'].reduce((val,key) => val[key])
                        // 這里vm還是作為初始值傳給val,進(jìn)行第一次調(diào)用,返回的是vm['album'],然后將返回的vm['album']這個對象傳給下一次調(diào)用的val
                        // 最后變成了vm['album']['name'] => '知足'
                    })
                }
                replaceTxt()
            }
          
            // 如果還有子節(jié)點(diǎn),繼續(xù)遞歸replace
            if (node.childNodes && node.childNodes.length) {
                replace(node)
            }
        })
    }

    replace(fragment)
    vm.$el.appendChild(fragment)
}

發(fā)布訂閱Dep、Watcher

????????就像買房的中介一樣,用戶(watcher)去買房,不可能天天去房地產(chǎn)開發(fā)商那邊去問有沒有房源,更多的是找一個中介(dep),然后把我們的需求和聯(lián)系方式告訴中介(dep.depend()),中介一旦有滿足需求的房源,便會打電話來通知我們dep.notify()。
????????我們需要一個訂閱器Dep,它需要有收集需求和聯(lián)系方式的功能,也需要有打電話通知的功能。

function Dep() {
    // 定義一個數(shù)組,用來存放函數(shù)的事件池
    this.subs = []
}
Dep.prototype = {
    // 收集需求和聯(lián)系方式的功能
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    },
    addSub(sub) {
        this.subs.push(sub)
    },
    // 發(fā)通知的功能
    notify() {
        // 綁定的方法,都有一個update方法
        this.subs.forEach(sub => sub.update())
    }
}

????????我們需要一個訂閱者watcher,它包含接受通知的功能,以及建立與Dep關(guān)聯(lián)的功能。

function Watcher(vm, exp, fn) {
    // 將fn放到實(shí)例上
    this.fn = fn
    this.vm = vm
    this.exp = exp

    // 建立關(guān)聯(lián)
    Dep.target = this
    let arr = exp.split('.')
    // 這里取值,會觸發(fā)value的get方法,所以需要在get方法里將聯(lián)系人的方式給中介,代碼47行g(shù)et方法
    let val = vm
    // 取值,獲取到this.album.name,默認(rèn)就會調(diào)用get方法
    arr.forEach(key => {
        val = val[key]
    })
    // 釋放關(guān)聯(lián)
    Dep.target = null
}
Watcher.prototype = {
    // 接受通知的功能,收到消息后,進(jìn)行更新數(shù)據(jù)的操作
    update() {
        // notify的時候值已經(jīng)更改了,再通過vm,exp來獲取新的值
        let arr = this.exp.split('.')
        let val = this.vm
        arr.forEach(key => {
            // 通過get獲取到新的值
            val = val[key]
        })
        // 將每次拿到的新值去替換{{}}的內(nèi)容
        this.fn(val)
    },

    addDep(dep) {
        dep.addSub(this)
    }
}

雙向數(shù)據(jù)綁定

數(shù)據(jù)--------------->Dom
1.通過compile解析指令和數(shù)據(jù),為其添加watcher
2.watcher觸發(fā)對應(yīng)的get方法,使其進(jìn)行依賴收集,把對應(yīng)的watcher進(jìn)行收集
3.當(dāng)數(shù)據(jù)發(fā)送變化的時候,觸發(fā)set方法,使其通知watcher進(jìn)行視圖更新

Dom--------------->數(shù)據(jù)
1.通過compile解析指令和數(shù)據(jù)
2.監(jiān)聽Dom input等更新動作,當(dāng)觸發(fā)dom更新時,在對應(yīng)回調(diào)函數(shù)中更新實(shí)例vm中的數(shù)據(jù)值

// 如果是元素節(jié)點(diǎn)
if (node.nodeType === 1) {
    // 獲取dom上的所有屬性,是個類數(shù)組
    let nodeAttr = node.attributes
    Array.from(nodeAttr).forEach(attr => {
        let name = attr.name      // v-model
        let exp = attr.value      // who
        if (name.includes('v-')) {
            node.value = vm[exp]   // 獲取this.who的值
        }
        // 監(jiān)聽變化
        new Watcher(vm, exp, function (newVal) {
            node.value = newVal
        })
        node.addEventListener('input', e => {
            let newVal = e.target.value
            // 相當(dāng)于給this.who 賦了一個新值,而值的改變會調(diào)用set,set中又會調(diào)用notify,notify中調(diào)用watcher的update方法實(shí)現(xiàn)了更新
            vm[exp] = newVal
        })
    })
}

以上就實(shí)現(xiàn)了一個MVVM模型

完整代碼
Index.html

<head>
    <meta charset="utf-8">
</head>
<body>
    <div id="app">
        <h1>{{song}}</h1>
        <p>《{{album.name}}》是{{singer}}2005年發(fā)行的專輯</p>
        <p>主打歌為{{album.theme}}</p>
        <input v-model="who" type="text">
    </div>

<script src="mvvm.js"></script>
<script>
    let mvvm = new Mvvm({
        el: '#app',
        data: {
            song: '閑魚',
            album: {
                name: '知足專輯',
                theme: '知足主打歌'
            },
            singer: '五月天',
            who: '五月天還是周杰倫'
        }
    })
</script>
</body>

mvvm.js

// 創(chuàng)建一個Mvvm構(gòu)造函數(shù),講options賦一個初始值,防止沒傳,等同于options || {}
function Mvvm(options = {}) {
    // 在vue上將所有的屬性都掛載到了vm.$options 上,所以我們也同樣實(shí)現(xiàn),將所有屬性掛載到了$options
    this.$options = options;
    // this._data這里也和vue一樣
    let data = this._data = this.$options.data;
    // 一、數(shù)據(jù)劫持
    observe(data)
    // 二、數(shù)據(jù)代理
    // 數(shù)據(jù)代理就是讓我們每次取data里面的數(shù)據(jù)時,不用每次都寫一長串,比如mvvm._data.album.name這種,我們可以直接寫成mvvm.album.name這種顯而易見的方式
    for(let key in data) {
        Object.defineProperty(this, key, {
            configurable: true,
            get() {
                return this._data[key]
            },
            set(newVal) {
                this._data[key] = newVal
            }
        })
    }
    // 三、數(shù)據(jù)編譯
    new Compile(options.el, this)
}

// 一、數(shù)據(jù)劫持(所謂數(shù)據(jù)劫持就是給對象增加get,set)
// 為什么要做數(shù)據(jù)劫持?
// 1.觀察對象,給對象增加Object.defineProperty
// 2.vue特點(diǎn)是不能新增不存在的屬性,不存在的屬性沒有g(shù)et和set
// 3.深度響應(yīng),因?yàn)槊看钨x予一個新對象時會給這個新對象增加defineProperty
function observe(data) {
    // 如果不是對象的話就直接return掉,放置遞歸溢出
    if(!data || typeof data !== 'object') return
    return new Observe(data)
}

// 創(chuàng)建一個Observer構(gòu)造函數(shù)
function Observe(data) {
    let dep = new Dep()
    // 既然要給對象的每一個屬性增加get、set,那就先遍歷一遍對象
    for(let key in data) {
        let val = data[key]
        // 遞歸繼續(xù)向下找,實(shí)現(xiàn)深度的數(shù)據(jù)劫持
        observe(val)
        Object.defineProperty(data, key, {
            configurable: true,
            get() {
                // 當(dāng)獲取值的時候就會自動調(diào)用get方法,于是在數(shù)據(jù)劫持observe修改一下get方法,將watcher添加到訂閱事件中
                // Dep.target && dep.addSub(Dep.target)
                if (Dep.target) {
                    dep.depend()   // 和上面一行代碼的意思是一樣的
                }
                return val
            },
            set(newVal) {
                // 如果設(shè)置的新值和以前的值一樣,就不處理
                if (val === newVal) {
                    return
                }
                val = newVal
                // 當(dāng)設(shè)置完新值后,也需要把新值再去數(shù)據(jù)劫持(不然新值的屬性沒有g(shù)et和set方法)
                observe(newVal)
                // 讓所有watcher的update方法執(zhí)行
                dep.notify()
            }
        })
    }
}

// 三、創(chuàng)建Compile構(gòu)造函數(shù)
// options中的el參數(shù),為我們指定了需要編譯哪些內(nèi)容,而我們需要做的僅僅是解析出通過v-model、v-text、{{}}等等標(biāo)識和指令,然后獲取綁定數(shù)據(jù)的值,替換掉標(biāo)識的內(nèi)容,并進(jìn)行數(shù)據(jù)的變化監(jiān)聽watcher,當(dāng)再有值發(fā)生變化時,可以及時通知其修改對應(yīng)dom元素。
function Compile(el, vm) {
    // 講el掛載到實(shí)例上方便調(diào)用
    vm.$el = document.querySelector(el)

    // 創(chuàng)建一個新的空白的文檔片段,在el范圍里將內(nèi)容都拿到,當(dāng)然不能一個一個的拿,可以選擇移到內(nèi)存中去,然后放入文檔碎片中,節(jié)省開銷
    // DocumentFragment是DOM節(jié)點(diǎn),它不是DOM樹的一部分,通常的用例是創(chuàng)建文檔片段,講元素附加到文檔片段,然后將文檔片段附加到DOM樹,在DOM樹中,文檔片段將其所有的子元素所代替。因?yàn)槲臋n片段存在于內(nèi)存中,并不在DOM樹中,所以講子元素插入到文檔片段時不會引起頁面回流(對元素位置和幾何上的計算)。因此使用文檔片段通常會帶來更多好的性能。
    let fragment = document.createDocumentFragment()
    while (child = vm.$el.firstChild) {
        // 將el中的內(nèi)容放入到內(nèi)存中
        fragment.appendChild(child)
    }

    // 對el里面的內(nèi)容進(jìn)行替換
    function replace(frag) {
        Array.from(frag.childNodes).forEach(node => {
            let txt = node.textContent
            // 正則匹配{{}}
            let reg = /\{\{(.*?)\}\}/g
            // 如果既是文本節(jié)點(diǎn)又有大括號
            if (node.nodeType === 3 && reg.test(txt)) {
                function replaceTxt() {
                    node.textContent = txt.replace(reg, (matched, placeholder) => {
                        // 五、數(shù)據(jù)更新視圖
                        // 我們需要訂閱一個事件,當(dāng)數(shù)據(jù)改變的時候需要重新刷新視圖,這就需要在replace替換的邏輯來進(jìn)行處理
                        // 通過new Watcher 把數(shù)據(jù)訂閱一下,數(shù)據(jù)一變就執(zhí)行改變內(nèi)容的操作
                        // 監(jiān)聽變化,給watcher再添加兩個參數(shù),用來取新的值給回調(diào)函數(shù)
                        new Watcher(vm, placeholder, replaceTxt)
                         return placeholder.split('.').reduce((val, key) => {
                             return val[key]
                         }, vm)
                        // 舉個例子解釋一下上面的代碼
                        // 'album.name'.split('.') => ['album','name'] => ['album','name'].reduce((val,key) => val[key])
                        // 這里vm還是作為初始值傳給val,進(jìn)行第一次調(diào)用,返回的是vm['album'],然后將返回的vm['album']這個對象傳給下一次調(diào)用的val
                        // 最后變成了vm['album']['name'] => '知足'
                    })
                }
                replaceTxt()
            }
            // 六、雙向數(shù)據(jù)綁定
            // 如果是元素節(jié)點(diǎn)
            if (node.nodeType === 1) {
                // 獲取dom上的所有屬性,是個類數(shù)組
                let nodeAttr = node.attributes
                Array.from(nodeAttr).forEach(attr => {
                    let name = attr.name      // v-model
                    let exp = attr.value      // who
                    if (name.includes('v-')) {
                        node.value = vm[exp]   // 獲取this.who的值
                    }
                    // 監(jiān)聽變化
                    new Watcher(vm, exp, function (newVal) {
                        node.value = newVal
                    })
                    node.addEventListener('input', e => {
                        let newVal = e.target.value
                        // 相當(dāng)于給this.who 賦了一個新值,而值的改變會調(diào)用set,set中又會調(diào)用notify,notify中調(diào)用watcher的update方法實(shí)現(xiàn)了更新
                        vm[exp] = newVal
                    })
                })
            }
            // 如果還有子節(jié)點(diǎn),繼續(xù)遞歸replace
            if (node.childNodes && node.childNodes.length) {
                replace(node)
            }
        })
    }

    replace(fragment)
    vm.$el.appendChild(fragment)
}

// 四、發(fā)布訂閱
//     就像買房的中介一樣,用戶(watcher)去買房,不可能天天去房地產(chǎn)開發(fā)商那邊去問有沒有房源,更多的是找一個中介(dep),然后把我們的需求和聯(lián)系方式告訴中介(dep.depend()),中介一旦有滿足需求的房源,便會打電話來通知我們dep.notify()

//     我們需要一個訂閱器Dep,它需要有收集需求和聯(lián)系方式的功能,也需要有打電話通知的功能
// 發(fā)布訂閱主要靠的就是數(shù)組關(guān)系,訂閱就是放入函數(shù),發(fā)布就是讓數(shù)組里的函數(shù)執(zhí)行   如[fn1, fn2, fn3]
// 訂閱器
function Dep() {
    // 定義一個數(shù)組,用來存放函數(shù)的事件池
    this.subs = []
}
Dep.prototype = {
    // 收集需求和聯(lián)系方式的功能
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    },
    addSub(sub) {
        this.subs.push(sub)
    },
    // 發(fā)通知的功能
    notify() {
        // 綁定的方法,都有一個update方法
        this.subs.forEach(sub => sub.update())
    }
}

    // 我們需要一個訂閱者watcher,它包含接受通知的功能,以及建立與Dep關(guān)聯(lián)的功能
// 監(jiān)聽函數(shù),通過watcher這個類創(chuàng)建的實(shí)例,都擁有update方法
// 訂閱者
function Watcher(vm, exp, fn) {
    // 將fn放到實(shí)例上
    this.fn = fn
    this.vm = vm
    this.exp = exp

    // 建立關(guān)聯(lián)
    Dep.target = this
    let arr = exp.split('.')
    // 這里取值,會觸發(fā)value的get方法,所以需要在get方法里將聯(lián)系人的方式給中介,代碼47行g(shù)et方法
    let val = vm
    // 取值,獲取到this.album.name,默認(rèn)就會調(diào)用get方法
    arr.forEach(key => {
        val = val[key]
    })
    // 釋放關(guān)聯(lián)
    Dep.target = null
}
Watcher.prototype = {
    // 接受通知的功能,收到消息后,進(jìn)行更新數(shù)據(jù)的操作
    update() {
        // notify的時候值已經(jīng)更改了,再通過vm,exp來獲取新的值
        let arr = this.exp.split('.')
        let val = this.vm
        arr.forEach(key => {
            // 通過get獲取到新的值
            val = val[key]
        })
        // 將每次拿到的新值去替換{{}}的內(nèi)容
        this.fn(val)
    },

    addDep(dep) {
        dep.addSub(this)
    }
}


// 數(shù)據(jù)--------------->Dom
// 1.通過compile解析指令和數(shù)據(jù),為其添加watcher
// 2.watcher觸發(fā)對應(yīng)的get方法,使其進(jìn)行依賴收集,把對應(yīng)的watcher進(jìn)行收集
// 3.當(dāng)數(shù)據(jù)發(fā)送變化的時候,觸發(fā)set方法,使其通知watcher進(jìn)行視圖更新

// Dom--------------->數(shù)據(jù)
// 1.通過compile解析指令和數(shù)據(jù)
// 2.監(jiān)聽Dom input等更新動作,當(dāng)觸發(fā)dom更新時,在對應(yīng)回調(diào)函數(shù)中更新實(shí)例vm中的數(shù)據(jù)值
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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