[Vue]二百行代碼實現數據雙向綁定

前言

vue

圖片來源: 剖析Vue實現原理 - 如何實現雙向綁定mvvmjavascript

數據掛載

<div id="app">
     <input type="text" v-model="text">
     <input type="text" v-model="man.name">
     <div v-html="text"></div>
     <div>{{man.name}}{{man.age}}{{text}}</div>
</div>
複製代碼
var app = new Vue({
    el: "#app",
    data: {
        man: {
            name: '小白',
            age: 20
        },
        text: 'hello world',
    }
})
複製代碼

上面代碼,也許對於你再熟悉不過了html

基於這樣的形式,咱們須要對數據進行掛載,將data的數據掛載到對應的DOM上前端

首先建立一個類來接收對象參數optionsvue

class Vue {
    constructor(options) {
        this.$el = options.el;
        this.$data  = options.data;
        if(this.$el) {
            new Compile(this.$el,this) ////模板解析
        }
    }
}
複製代碼

Compile類,用於模板解析,它的工做內容主要爲如下幾點java

class Compile{
    constructor(el,vm) {
        // 建立文檔碎片,接收el的裏面全部子元素
        // 解析子元素中存在v-開頭的屬性及文本節點中存在{{}}標識
        // 將vm中$data對應的數據掛載上去
    }
}
複製代碼

基礎代碼:node

class Compile {
    constructor(el,vm) {
        this.el = this.isElementNode(el)?el:document.querySelector(el);
        this.vm = vm;
        let fragment = this.node2fragment(this.el);
        this.compile(fragment)
    }
    isDirective(attrName) {
        return attrName.startsWith('v-'); //判斷屬性中是否存在v-字段 返回 布爾值
    }
    compileElement(node) {
        let attributes = node.attributes;
        [...attributes].forEach(attr => {
            let {name,value} = attr
            if(this.isDirective(name)) {
                let [,directive] = name.split('-')
                CompileUtil[directive](node,value,this.vm);
            }
        })
    }
    compileText(node) {
        let content = node.textContent;
        let reg = /\{\{(.+?)}\}/;
        if(reg.test(content)) {
            CompileUtil['text'](node,content,this.vm);
        }
    }
    compile(fragment) {
       let childNodes = fragment.childNodes;
       [...childNodes].forEach(child => {
           if(this.isElementNode(child)) {
            this.compileElement(child);
            this.compile(child);
           }else{
            this.compileText(child);
           }
       })
       document.body.appendChild(fragment);
    }
    node2fragment(nodes) { 
        let fragment = document.createDocumentFragment(),firstChild;
        while(firstChild = nodes.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment
    }
    isElementNode(node) {
        return node.nodeType === 1;
    }
}

CompileUtil = {
    getValue(vm,expr) {
        // 解析表達式值 獲取vm.$data內對應的數據
        let value = expr.split('.').reduce((data,current) => {
            return data[current]
        },vm.$data)
        return value
    },
    model(node,expr,vm) { 
       let data = this.getValue(vm,expr);
       this.updater['modeUpdater'](node,data);
    },
    html(node,expr,vm){
        let data = this.getValue(vm,expr);
        this.updater['htmlUpdater'](node,data);
    },
    text(node,expr,vm){ 
        let content = expr.replace(/\{\{(.+?)}\}/g, (...args) => {
            return this.getValue(vm,args[1]);
        })
        console.log(content)
        this.updater['textUpdater'](node,content);
    },
    updater:{
        modeUpdater(node,value){
            node.value = value;
        },
        textUpdater(node,value){
            node.textContent = value;
        },
        htmlUpdater(node,value){
            node.innerHTML = value;
        }
    }
}
複製代碼

數據劫持

上面已經完成了對模板的數據解析,接下來再對數據的變動進行監聽,實現雙向數據綁定git

class Vue {
    constructor(options) {
        this.$el = options.el;
        this.$data  = options.data;
        if(this.$el) {
            new Compile(this.$el,this);
            new Observer(this.$data); //新增 數據劫持
        }
    }
}
複製代碼

Observer類,用於監聽數據,它的工做內容主要爲如下幾點github

class Observer{
    constructor(el,vm) {
     	// 利用Object.defineProperty監聽全部屬性
        // 遞歸循環監聽全部傳入的對象
    }
}
複製代碼

基礎代碼:閉包

class Observer {
    constructor(data) {
      this.observer(data);
    }
    observer(data) {
        if(!data||typeof data !== 'object') return
        for(let key in data) {
            this.defineReactive(data,key,data[key]);
        }
    }
    defineReactive (obj,key,value) {
        this.observer(value);
        Object.defineProperty(obj,key,{
            get: () => {
                return value;
            },
            set: (newValue) => {
                if(newValue !== value) {
                    this.observer(newValue);
                    value = newValue;
                }
            }
        })

    }
}
複製代碼

發佈訂閱

將監聽到的數據變動,實時的更替上去架構

首先咱們須要一個Watcher類,它的工做內容以下

class Watcher {
   // 存儲當前觀察屬性對象的數據
   // 當前觀察屬性對象數據變動時,更新數據
}
複製代碼

基礎代碼:

class Watcher {
    /* vm 對象實例 expr 須要監聽的對象表達式 cb 更新數據的回調函數 */
    constructor(vm,expr,cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        this.oldValue = this.get();
    }
    get() {
        let value = CompileUtil.getValue(this.vm,this.expr);
        return value;
    }
    update() {
        let newValue = CompileUtil.getValue(this.vm,this.expr);
        if(this.oldValue !== newValue) {
            this.cb(newValue)
        }
    }
}
複製代碼

再來一個發佈訂閱Dep的類

class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    notify() {
        this.subs.forEach(sub => {
            sub.update()
        })
    }
}
複製代碼

接下來,讓咱們把WatcherDep類關聯起來

CompileUtilmodeltext方法中分別新建Watcher實例

Watcher在接收到Dep的廣播時,須要一個對應的回調函數,更新數據

CompileUtil = {
    getValue(vm,expr) {
        // 解析表達式值 獲取vm.$data內對應的數據
        let value = expr.split('.').reduce((data,current) => {
            return data[current]
        },vm.$data)
        return value
    },
    ...
    model(node,expr,vm) { 
       let data = this.getValue(vm,expr);
       //新增 觀察者
       new Watcher(vm,expr,(newValue) => {
            this.updater['modeUpdater'](node,newValue);
       })
       this.updater['modeUpdater'](node,data);
    },
    html(node,expr,vm){
        let data = this.getValue(vm,expr);
         //新增 觀察者
        new Watcher(vm,expr,(newValue) => {
            this.updater['htmlUpdater'](node,newValue);
        })
        this.updater['htmlUpdater'](node,data);
    },
    ...
    text(node,expr,vm){ 
        let content = expr.replace(/\{\{(.+?)}\}/g, (...args) => {
             /* 新增 觀察者 匹配多個{{}}字段 */
            new Watcher(vm,args[1],() => {
                this.updater['textUpdater'](node,this.getContentValue(vm,expr));
            })
            return this.getValue(vm,args[1]);
        })
        this.updater['textUpdater'](node,content);
    },
}
複製代碼

實例化一個Watcher的同時會調用this.get()方法,this.get()在取值時,會觸發被監聽對象的getter

class Watcher {
    ...
    get() {
        // 在Dep設置一個全局屬性
        Dep.target = this;
        // 取值會觸發被監聽對象的getter函數
        let value = CompileUtil.getValue(this.vm,this.expr);
        Dep.target = null;
        return value;
    }
	...
}
複製代碼

來到Observer中,此時在get函數中,咱們就能夠將Watcher實例放進Dep的容器subs

這裏dep,利用了閉包的特性,每次廣播不會通知全部用戶,提升了性能

class Observer {
    ...
    defineReactive (obj,key,value) {
        this.observer(value);
        let dep = new Dep()
        Object.defineProperty(obj,key,{
            get: () => {
                //新增 訂閱
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set: (newValue) => {
                if(newValue !== value) {
                    console.log('監聽',newValue)
                    this.observer(newValue);
                    value = newValue;
                    //廣播
                    dep.notify();
                }
            }
        })
    }
}
複製代碼

此時,WatcherDep已造成關聯,一旦被監聽的對象數據發生變動,就會觸發Depnotify廣播功能,進而觸發Watcherupdate方法執行回調函數!

測試:

setTimeout(function(){
    app.$data.test = "123"
},3000)
複製代碼

結果:

測試

到這裏,咱們已經完成了最核心的部分,數據驅動視圖,可是衆所周知,v-model是能夠視圖驅動數據的,因而咱們再增長一個監聽事件

CompileUtil = {
    ...
	setValue(vm,expr,value) {
    	//迭代屬性賦值
        expr.split('.').reduce((data,current,index,arr) => {
            if(index == arr.length - 1){
                data[current] = value
            }
            return data[current]
        },vm.$data)
    },
    model(node,expr,vm) { 
       let data = this.getValue(vm,expr);
       new Watcher(vm,expr,(newValue) => {
            this.updater['modeUpdater'](node,newValue);
       })
        //事件監聽
       node.addEventListener('input', el => {
          let value = el.target.value;
          console.log(value)
          this.setValue(vm,expr,value)
       })
       this.updater['modeUpdater'](node,data);
    },
        ...
}
複製代碼

效果以下:

最後爲Vue實例添加一個屬性代理的方法,使訪問vm的屬性代理爲訪問vm._data的屬性

class Vue {
    constructor(options) {
        ...
        this.$data  = options.data;
        Object.keys(this.$data).forEach(key => {
            this.proxyKeys(key);
        })
      	...
    }
    proxyKeys(key) {
        console.log(key)
        Object.defineProperty(this,key,{
            enumerable: true,
            configurable: false,
            get: () => {
                return this.$data[key];
            },
            set: (newValue) => {
                console.log('newValue',newValue)
                this.$data[key] = newValue;
            }
        })
    }
}
複製代碼

大功告成! 源碼:github.com/luojinxu520…

結束

目前,Vue 的反應系統是使用 Object.defineProperty 的 getter 和 setter。 可是,Vue 3 將使用 ES2015 Proxy 做爲其觀察者機制。 這消除了之前存在的警告,使速度加倍,並節省了一半的內存開銷。 爲了繼續支持 IE11,Vue 3 將發佈一個支持舊觀察者機制和新 Proxy 版本的構建。

參考 :

一、 剖析Vue實現原理 - 如何實現雙向綁定mvvm

二、珠峯架構前端技術公開課

相關文章
相關標籤/搜索