使用proxy實現一個雙向綁定

前言

上一篇文章說了ES6中的Proxy,如今就來利用proxy一步步實現一個模擬vue的雙向綁定。javascript

目錄

如何實現

  • 在學習vue的時候,vue是經過劫持數據的變化,監聽到數據變化時改變前端視圖。
  • 那麼要實現雙向綁定,必然須要一個監聽數據的方法。如文章標題所示,這裏使用的proxy實現數據的監聽。
  • 當監聽到數據變化時,須要一個watcher響應並調用更新數據的compile方法去更新前端視圖。
  • 在vue中v-model做爲綁定的入口。當咱們監聽到前端input輸入信息並綁定了數據項的時候,須要先告知watcher,由watcher改變監聽器的數據。
  • 大概的雙向綁定的原理爲:

1.實現一個observer(數據監聽器)

利用proxy實現一個數據監聽器很簡單,由於proxy是監聽整個對象的變化的,因此能夠這樣寫:前端

class VM {
        constructor(options, elementId) {
            this.data = options.data || {}; // 監聽的數據對象
            this.el = document.querySelector(elementId);
            this.init(); // 初始化
        }
        
        // 初始化
        init() {
            this.observer();
        }
        
        // 監聽數據變化方法
        observer() {
            const handler = {
                get: (target, propkey) => {
                    console.log(`監聽到${propkey}被取啦,值爲:${target[propkey]}`);
                    return target[propkey];
                },
                set: (target, propkey, value) => {
                    if(target[propkey] !== value){
                        console.log(`監聽到${propkey}變化啦,值變爲:${value}`);
                    }
                    return true;
                }
            };
            this.data = new Proxy(this.data, handler);
        }
    }
    
    // 測試一下
    const vm = new VM({
        data: {
            name: 'defaultName',
            test: 'defaultTest',
        },
    }, '#app');
    
    vm.data.name = 'changeName'; // 監聽到name變化啦,值變爲:changeName
    vm.data.test = 'changeTest'; // 監聽到test變化啦,值變爲:changeTest
    
    vm.data.name; // 監聽到name被取啦,值爲:changeName 
    vm.data.test; // 監聽到test被取啦,值爲:changeTest
複製代碼

這樣,數據監聽器已經基本實現了,可是如今這樣只能監聽到數據的變化,不能改變前端的視圖信息。如今須要實現一個更改前端信息的方法,在VM類中添加方法changeElementDatavue

// 改變前端數據
    changeElementData(value) {
        this.el.innerHTML = value;
    }
複製代碼

在監聽到數據變化時調用changeElementData改變前端數據,handlerset方法中調用方法java

set(target, propkey, value) {
        this.changeElementData(value);
        return true;
    }
複製代碼

在init中設置一個定時器更改數據node

init() {
        this.observer();
        
        setTimeout(() => {
            console.log('change data !!');
            this.data.name = 'hello world';
        }, 1000)
    }
複製代碼

已經能夠看到監聽到的信息改變到前端了,可是! bash

這樣寫死的綁定數據顯然是沒有意義,如今實現的邏輯大概以下面的圖 app

2.實現數據動態更新到前端

上面實現了一個簡單的數據綁定展現,可是隻能綁定一個指定的節點去改變此節點的數據綁定。這樣顯然是不能知足的,咱們知道vue中是以{{key}}這樣的形式去綁定展現的數據的,並且vue中是監聽指定的節點的全部子節點的。所以對象中須要在VIEWOBSERVER之間添加一個監聽層WATCHER。當監聽到數據發生變化時,經過WATCHER去改變VIEW,如圖: post

根據這個流程,下一步咱們須要作的是:

  1. 監聽整個綁定的element的全部節點並匹配全部節點中的全部{{text}}模板
  2. 監聽到數據變化時,告知watcher須要數據改變了,替換前端模板

在VM類的構造器中添加三個參數學習

constructor() {
        this.fragment = null; // 文檔片斷
        this.matchModuleReg = new RegExp('\{\{\s*.*?\s*\}\}', 'gi'); // 匹配全部{{}}模版
        this.nodeArr = []; // 全部帶有模板的前端結點
    }
複製代碼

新建一個方法遍歷el中的全部節點,並存放到fragment測試

/** * 建立一個文檔片斷 */
    createDocumentFragment() {
        let fragment = document.createDocumentFragment();
        let child = this.el.firstChild;
        // 循環添加到文檔片斷中
        while (child) {
            this.fragment.appendChild(child);
            child = this.el.firstChild;
        }
        this.fragment = fragment;
    }
複製代碼

匹配{{}}的數據並替換模版

/**
     *  匹配模板
     *  @param { string } key 觸發更新的key
     *  @param { documentElement } fragment 結點
     */
    matchElementModule(key, fragment) {
        const childNodes = fragment || this.fragment.childNodes;
        [].slice.call(childNodes).forEach((node) => {
            if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
                node.defaultContent = node.textContent; // 將初始化的前端內容保存到節點的defaultContent中
                this.changeData(node);
                this.nodeArr.push(node); // 保存帶有模板的結點
            }

            // 遞歸遍歷子節點
            if(node.childNodes && node.childNodes.length) {
                this.matchElementModule(key, node.childNodes);
            }
        })
    }
    
    /**
     * 改變視圖數據
     * @param { documentElement } node
     */
    changeData(node) {
        const matchArr = node.defaultContent.match(this.matchModuleReg); // 獲取全部須要匹配的模板
        let tmpStr = node.defaultContent;
        for(const key of matchArr) {
            tmpStr = tmpStr.replace(key, this.data[key.replace(/\{\{|\}\}|\s*/g, '')] || '');
        }
        node.textContent = tmpStr;
    }
複製代碼

實現watcher,數據變化是觸發此watcher更新前端

watcher(key) {
        for(const node of this.nodeArr) {
            this.changeData(node);
        }
    }
複製代碼

initproxyset方法中執行新增的方法

init() {
        this.observer();
        this.createDocumentFragment(this.el); // 將綁定的節點都放入文檔片斷中
        for (const key of Object.keys(this.data)) {
            this.matchElementModule(key);
        }
        this.el.appendChild(this.fragment); // 將初始化的數據輸出到前端
    }
    
    set: () => {
        if(target[propkey] !== value) {
            target[propkey] = value;
            this.watcher(propkey);
        }
        return true;
    }
複製代碼

測試一下:

3.實現數據雙向綁定

如今咱們的程序已經能夠經過改變data動態地改變前端的展現了,接下來須要實現的是一個相似VUEv-model綁定input的方法,經過input輸入動態地將輸入的信息輸出到對應的前端模板上。大概的流程圖以下:

一個簡單的實現流程大概以下:

  1. 獲取全部帶有v-model的input結點
  2. 監聽輸入的信息並設置到對應的data中

在constructor中添加

constructor() {
        this.modelObj = {};
    }
    
複製代碼

在VM類中新增方法

// 綁定 y-model
    bindModelData(key, node) {
        if (this.data[key]) {
            node.addEventListener('input', (e) => {
                this.data[key] = e.target.value;
            }, false);
        }
    }
    
    // 設置 y-model 值
    setModelData(key, node) {
        node.value = this.data[key];
    }

    // 檢查y-model屬性
    checkAttribute(node) {
        return node.getAttribute('y-model');
    }
複製代碼

watcher中執行setModelData方法,matchElementModule中執行bindModelData方法。
修改後的matchElementModulewatcher方法以下

matchElementModule(key, fragment) {
        const childNodes = fragment || this.fragment.childNodes;
        [].slice.call(childNodes).forEach((node) => {

            // 監聽全部帶有y-model的結點
            if (node.getAttribute && this.checkAttribute(node)) {
                const tmpAttribute = this.checkAttribute(node);
                if(!this.modelObj[tmpAttribute]) {
                    this.modelObj[tmpAttribute] = [];
                };
                this.modelObj[tmpAttribute].push(node);
                this.setModelData(tmpAttribute, node);
                this.bindModelData(tmpAttribute, node);
            }

            // 保存全部帶有{{}}模版的結點
            if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
                node.defaultContent = node.textContent; // 將初始化的前端內容保存到節點的defaultContent中
                this.changeData(node);
                this.nodeArr.push(node); // 保存帶有模板的結點
            }

            // 遞歸遍歷子節點
            if(node.childNodes && node.childNodes.length) {
                this.matchElementModule(key, node.childNodes);
            }
        })
    }
    
    watcher(key) {
        if (this.modelObj[key]) {
            this.modelObj[key].forEach(node => {
                this.setModelData(key, node);
            })
        }
        for(const node of this.nodeArr) {
            this.changeData(node);
        }
    }
複製代碼

來看一下是否已經成功綁定了,寫一下測試代碼:

成功!!

最終的代碼以下:

class VM {
            constructor(options, elementId) {
                this.data = options.data || {}; // 監聽的數據對象
                this.el = document.querySelector(elementId);
                this.fragment = null; // 文檔片斷
                this.matchModuleReg = new RegExp('\{\{\s*.*?\s*\}\}', 'gi'); // 匹配全部{{}}模版
                this.nodeArr = []; // 全部帶有模板的前端結點
                this.modelObj = {}; // 綁定y-model的對象
                this.init(); // 初始化
            }

            // 初始化
            init() {
                this.observer();
                this.createDocumentFragment();
                for (const key of Object.keys(this.data)) {
                    this.matchElementModule(key);
                }
                this.el.appendChild(this.fragment);
            }

            // 監聽數據變化方法
            observer() {
                const handler = {
                    get: (target, propkey) => {
                        return target[propkey];
                    },
                    set: (target, propkey, value) => {
                        if(target[propkey] !== value) {
                            target[propkey] = value;
                            this.watcher(propkey);
                        }
                        return true;
                    }
                };
                this.data = new Proxy(this.data, handler);
            }

            /** * 建立一個文檔片斷 */
             createDocumentFragment() {
                let documentFragment = document.createDocumentFragment();
                let child = this.el.firstChild;
                // 循環向文檔片斷添加節點
                while (child) {
                    documentFragment.appendChild(child);
                    
                    child = this.el.firstChild;
                }
                this.fragment = documentFragment;
            }

            /** * 匹配模板 * @param { string } key 觸發更新的key * @param { documentElement } fragment 結點 */
            matchElementModule(key, fragment) {
                const childNodes = fragment || this.fragment.childNodes;
                [].slice.call(childNodes).forEach((node) => {

                    // 監聽全部帶有y-model的結點
                    if (node.getAttribute && this.checkAttribute(node)) {
                        const tmpAttribute = this.checkAttribute(node);
                        if(!this.modelObj[tmpAttribute]) {
                            this.modelObj[tmpAttribute] = [];
                        };
                        this.modelObj[tmpAttribute].push(node);
                        this.setModelData(tmpAttribute, node);
                        this.bindModelData(tmpAttribute, node);
                    }

                    // 保存全部帶有{{}}模版的結點
                    if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
                        node.defaultContent = node.textContent; // 將初始化的前端內容保存到節點的defaultContent中
                        this.changeData(node);
                        this.nodeArr.push(node); // 保存帶有模板的結點
                    }

                    // 遞歸遍歷子節點
                    if(node.childNodes && node.childNodes.length) {
                        this.matchElementModule(key, node.childNodes);
                    }
                })
            }
            
            /** * 改變視圖數據 * @param { documentElement } node */
            changeData(node) {
                const matchArr = node.defaultContent.match(this.matchModuleReg); // 獲取全部須要匹配的模板
                let tmpStr = node.defaultContent;
                for(const key of matchArr) {
                    tmpStr = tmpStr.replace(key, this.data[key.replace(/\{\{|\}\}|\s*/g, '')] || '');
                }
                node.textContent = tmpStr;
            }

            watcher(key) {
                if (this.modelObj[key]) {
                    this.modelObj[key].forEach(node => {
                        this.setModelData(key, node);
                    })
                }
                for(const node of this.nodeArr) {
                    this.changeData(node);
                }
            }

            // 綁定 y-model
            bindModelData(key, node) {
                if (this.data[key]) {
                    node.addEventListener('input', (e) => {
                        this.data[key] = e.target.value;
                    }, false);
                }
            }
            
            // 設置 y-model 值
            setModelData(key, node) {
                node.value = this.data[key];
            }

            // 檢查y-model屬性
            checkAttribute(node) {
                return node.getAttribute('y-model');
            }
        }
複製代碼

最後

本節咱們使用Proxy,從監聽器開始,到觀察者一步步實現了一個模仿VUE的雙向綁定,代碼中也許會有不少寫的不嚴謹的地方,如發現錯誤麻煩大佬們指出~~

相關文章
相關標籤/搜索