1. 程式人生 > >vue 雙向數據綁定的實現學習(二)- 監聽器的實現

vue 雙向數據綁定的實現學習(二)- 監聽器的實現

view 編譯 代碼 圖片 eve 什麽 als 模板變量 server

廢話:上一篇https://www.cnblogs.com/adouwt/p/9928278.html

提到了vue實現的基本實現原理:Object.defineProperty() -數據劫持 和 發布訂閱者模式(觀察者),下面講的就是數據劫持在代碼中的具體實現。

1.先看如何調用

  技術分享圖片

new一個對象,傳入我們的參數,這個Myvue ,做了啥?

  技術分享圖片

上面看到了在實例化一個Myvue 對象的時候,會執行init方法, init 方法做了兩個事,調用了observer 方法,和 實例化調用了 compile 方法。 到這裏我們就明白了,實例化一個Myvue後,我們要做的就是監聽數據變化和編譯模板 。

上面Object.key() 方法,實例化時傳入的data裏面對應的變量緩存到 Myvue 對象的 $prop上,這樣方便在後續處理數據。怎麽個方便法呢!...

2.observer 的實現

  observer ,模式裏面的角色定位 他是一個發布者,也可以理解為是一個觀察者

function observer (data) {
    if(!data || typeof data !== ‘object‘) {
        return;
    }
    Object.keys(data).forEach(key => {
        // 對每個屬性監聽處理
        defineReactive(data, key, data[key]);
    })
}

  defineReactive

function defineReactive (data,key,value) {
    // 每次訪問/修改屬性的時候 實例化一個調度中心Dep
    var dep = new Dep();
    Object.defineProperty(data,key,{
        get: function() {
           // 添加到watcher 的Dep 調度中心
            if (Dep.target) { // Dep.target 是個什麽鬼? 轉到watcher.js 它是某個訂閱者 watcher
                dep.addSub(Dep.target);  //
這個代碼段的意思: 如果有訂閱者(訪問/修改屬性的時候) 就將這個訂閱者統一放進 Dep 調度中心中 } // console.log(`${key}屬性被訪問了`) return value }, set: function (newValue) { if (value != newValue) { // console.log(`${key}屬性被重置了`) value = newValue dep.notify(); //我這裏有做改動了,通知調度中心的notify方法 } } }) // 遞歸調用,observe 這個value observer(value) }

  Dep: 這裏是所有訂閱者的一個調度中心,它不是直接監聽 發布者的信息,發布者將要發布的信息 發布到 一個中介、調度中心(Dep),由這個Dep 來調度信息給哪個訂閱者(Watcher)

// 統一管理watcher訂閱者的Dep (調度中心)  Dispatch center
function Dep () {
    // 所有的watcher 放進這裏統一管理
    this.subs = []
}
Dep.target = null;
// 通知視圖更新dom的 notify的方法
Dep.prototype.notify   = function () {
    // this.subs 是上面訂閱器watcher 的集合
    this.subs.forEach(sub => {
        // sub 是某個Watcher 具體調用某個Watcher的update 方法
        sub.update()
    })
}

// 添加訂閱者的方法
Dep.prototype.addSub  = function (sub) {
    this.subs.push(sub)
}

3.訂閱器Watcher

// 具體的訂閱器Watcher
// 傳入一個vue 的實例, 監聽的屬性, 以及處理的回調函數
function Watcher (vm,prop,callback) {
    this.vm  =  vm;
    this.$prop = prop;
    this.value = this.get();
    this.callback = callback; // 具體watcher所具有的方法,不同的watcher 不同的回調函數,處理不同的業務邏輯
 }
// 添加watcher 獲得屬性的get 方法,當有屬性訪問/設置 的時候,就產生訂閱者 將這個訂閱者放進調度中心
Watcher.prototype.get = function () {
    Dep.target = this;
    // 獲得屬性值
    const value = this.vm.$data[this.$prop];
    return value
}
// 添加watcher的更新視圖的方法
Watcher.prototype.update = function () {
    // 當屬性值有變化的時候,執行方法,更新試圖
    const value = this.vm.$data[this.$prop];
    const oldValue = this.value;
    // update 執行的時候,先獲取 vm 中data實時更新的屬性值,this.value 是vm data中之前的老值
    if (oldValue != value) {
        // console.log(‘人家通知了,我要改變了‘)
        // 把剛剛獲取的更新值賦給之前vm data 中的值
        this.value =  value 
        // 執行回調函數 具體怎麽處理這個,看實際調用時候 callback 的處理 
        this.callback(this.value)
    }
}

4.模板編譯

(為了直接看到頁面數據變化的效果,在模板編譯的核心數據處理上做了dom 操作,下一篇將講模板編譯的一些細節處理)

// dom模板編譯 vm 就是我們最上面的Myvue 對象
function Compile (vm) {
    this.vm = vm;
    this.$el = vm.el;
    // this.data = vm.data;
    this.fragment = null; // 用作後面模板引擎 創建文檔片段
    this.init()
}

Compile.prototype = {
    // init 方法簡單處理,直接做dom 操作,後面會用詳細的模板引擎的學習
    init: function () {
        let value = this.vm.$data.name // 初始化獲取到的值 放進dom節點中
        document.querySelector(‘.form-control‘).value = value;
        document.querySelector(‘.template‘).textContent  = value
        // 通知訂閱者更新dom
        new Watcher(this.vm,this.vm.$prop, (value) => {
            document.querySelector(‘.form-control‘).value = value;
            document.querySelector(‘.template‘).textContent  = value
        })
        document.querySelector(‘.form-control‘).addEventListener(‘input‘,(e) => {
            let targetValue = e.target.value
            if(value !== targetValue) {
                this.vm.$data.name = e.target.value // 將修改的值 更新到 vm的data中
                document.querySelector(‘.form-control‘).value = targetValue; // 更新dom 節點
                document.querySelector(‘.template‘).textContent  = targetValue
            }
             
        },false)
    }
}

這樣就可以看到 在表單中,數據的雙向綁定了。

技術分享圖片

未完待續,錯誤之處,敬請指出,共同進步!

下一篇 vue 雙向數據綁定的實現學習(三)- 模板編譯

附:演示代碼:

js:

技術分享圖片
function Myvue (options) {
    this.$options = options
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    Object.keys(this.$data).forEach(key => {
        this.$prop = key;
    })
    this.init()
}
Myvue.prototype.init = function () {
    // 監聽數據變化
    observer(this.$data);
            // 獲得值
            // let value = this.$data[this.$prop];
            // 不經過模板編譯直接 通知訂閱者更新dom
            // new Watcher(this,this.$prop,value => {
            //     console.log(`watcher ${this.$prop}的改動,要有動靜了`)
            //     this.$el.textContent = value
            // }) 
    //通知模板編譯來執行頁面上模板變量替換
    new Compile(this)
}

function observer (data) {
    if(!data || typeof data !== ‘object‘) {
        return;
    }
    Object.keys(data).forEach(key => {
        // 對每個屬性監聽處理
        defineReactive(data, key, data[key]);
    })
}

function defineReactive (data,key,value) {
    // 每次訪問/修改屬性的時候 實例化一個調度中心Dep
    var dep = new Dep();
    Object.defineProperty(data,key,{
        get: function() {
           // 添加到watcher 的Dep 調度中心
            if (Dep.target) { // Dep.target 是個什麽鬼? 轉到watcher.js 它是某個訂閱者 watcher
                dep.addSub(Dep.target);  //這個代碼段的意思: 如果有訂閱者(訪問/修改屬性的時候) 就將這個訂閱者統一放進 Dep 調度中心中
            }
            // console.log(`${key}屬性被訪問了`)
            return value
        },
        set: function (newValue) {
            if (value != newValue) {
                // console.log(`${key}屬性被重置了`)
                value = newValue
                dep.notify(); //我這裏有做改動了,通知調度中心的notify方法
            }
        }
    })
    // 遞歸調用,observe 這個value
    observer(value)
}

// 統一管理watcher訂閱者的Dep (調度中心)  Dispatch center
function Dep () {
    // 所有的watcher 放進這裏統一管理
    this.subs = []
}
Dep.target = null;
// 通知視圖更新dom的 notify的方法
Dep.prototype.notify   = function () {
    // this.subs 是上面訂閱器watcher 的集合
    this.subs.forEach(sub => {
        // sub 是某個Watcher 具體調用某個Watcher的update 方法
        sub.update()
    })
}

// 添加訂閱者的方法
Dep.prototype.addSub  = function (sub) {
    this.subs.push(sub)
}

// 具體的訂閱器Watcher
// 傳入一個vue 的示例, 監聽的屬性, 以及處理的回調函數
function Watcher (vm,prop,callback) {
    this.vm  =  vm;
    this.$prop = prop;
    this.value = this.get();
    this.callback = callback; // 具體watcher所具有的方法,不同的watcher 不同的回調函數,處理不同的業務邏輯
 }
// 添加watcher 獲得屬性的get 方法,當有屬性訪問/設置 的時候,就產生訂閱者 將這個訂閱者放進調度中心
Watcher.prototype.get = function () {
    Dep.target = this;
    // 獲得屬性值
    const value = this.vm.$data[this.$prop];
    return value
}
// 添加watcher的更新視圖的方法
Watcher.prototype.update = function () {
    // 當屬性值有變化的時候,執行方法,更新試圖
    const value = this.vm.$data[this.$prop];
    const oldValue = this.value;
    // update 執行的時候,先獲取 vm 中data實時更新的屬性值,this.value 是vm data中之前的老值
    if (oldValue != value) {
        // console.log(‘人家通知了,我要改變了‘)
        // 把剛剛獲取的更新值賦給之前vm data 中的值
        this.value =  value 
        // 執行回調函數 具體怎麽處理這個,看實際調用時候 callback 的處理 
        this.callback(this.value)
    }
}

// dom模板編譯 vm 就是我們最上面的Myvue 對象
function Compile (vm) {
    this.vm = vm;
    this.$el = vm.el;
    // this.data = vm.data;
    this.fragment = null; // 用作後面模板引擎 創建文檔片段
    this.init()
}

Compile.prototype = {
    // init 方法簡單處理,直接做dom 操作,後面會用詳細的模板引擎的學習
    init: function () {
        let value = this.vm.$data.name // 初始化獲取到的值 放進dom節點中
        document.querySelector(‘.form-control‘).value = value;
        document.querySelector(‘.template‘).textContent  = value
        // 通知訂閱者更新dom
        new Watcher(this.vm,this.vm.$prop, (value) => {
            document.querySelector(‘.form-control‘).value = value;
            document.querySelector(‘.template‘).textContent  = value
        })
        document.querySelector(‘.form-control‘).addEventListener(‘input‘,(e) => {
            let targetValue = e.target.value
            if(value !== targetValue) {
                this.vm.$data.name = e.target.value // 將修改的值 更新到 vm的data中
                document.querySelector(‘.form-control‘).value = targetValue; // 更新dom 節點
                document.querySelector(‘.template‘).textContent  = targetValue
            }
             
        },false)
    }
}
View Code

html:

技術分享圖片
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Vue雙向綁定原理及實現</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
        crossorigin="anonymous">
        <style>
            #app {
                margin: 20px auto;
                width: 400px;
                padding: 50px;
                text-align: center;
                border: 2px solid #ddd;
            }
        </style>
    </head>

    <body>
        <div id="app">
            <input class="form-control" v-model="name" type="text">
            <h1 class="template">{{name}}</h1>
        </div>
        <script src="./js/index1.js"></script>
        <script>
            const vm = new Myvue({
                el: "#app",
                data: {
                    name: "vue 雙向數據綁定test1"
                }
            });
        </script>
    </body>
</html>
View Code

vue 雙向數據綁定的實現學習(二)- 監聽器的實現