Vue中的觀察者與發布訂閱


  大家好,今天為大家講解一下 Vue 中的觀察者,及發布和訂閱的實現

  1、首先我們來創建一個觀察者。

  

/**
 * 觀察者
 */
class Watcher{
    constructor(vm,expr,cd){
        this.vm = vm;
        this.expr = expr;
        this.cd = cd;
        
        //存放舊值
        this.oldValue = this.get();
    }
    get(){ //獲取舊值
        Dep.target = this; //先把自己放到 this 上
        //取值時 把這個觀察者和數據關聯起來
        let value = CompileUtil.getVal(this.vm,this.expr);
        Dep.target = null; //不取消 任何值取都會 添加watcher
        return value;
    }
    updata(){
        let newVal = CompileUtil.getVal(this.vm,this.expr); //獲取新值
        if(newVal !== this.oldValue){
            this.cd(newVal)
        }
    }
    
}

 

   接下來我們來創建一個發布和訂閱的構造函數 

/**
 *  (發布訂閱) 觀察者 被觀察者
 */
class Dep{
    constructor(){
        this.subs = [] //存放所有的 watcher
    }
    //訂閱
    addSub(watcher){ //添加 watcher
        this.subs.push(watcher)
    }
    //發布
    notify(){
        this.subs.forEach(watcher=>watcher.updata());
    }
}

 

 

    在觀察者中有這樣的一段代碼

   get(){ //獲取舊值
        Dep.target = this; //先把自己放到 this 上
        //取值時 把這個觀察者和數據關聯起來
        let value = CompileUtil.getVal(this.vm,this.expr);
        Dep.target = null; //不取消 任何值取都會 添加watcher
        return value;
    }

 

    因為 js 是單線程的,所以我們就可以在每一個觀察者獲取舊值的時候,給這個觀察者添加一個 target 屬性,這個屬性就指向它這個觀察者本身。之后就把這個 target 清除。

    

    而在清除之前都發生了什么呢?

    其實就發生了數據劫持,這個在上一篇中講過。

//實現數據劫持
    defineReactive(obj,key,value){
        this.observer(value); //如果傳進來的參數是對象,就回調一下這個函數,就是一個遞歸函數
        
        let dep = new Dep(); //給每一個屬性都添加一個具有發布和訂閱的功能
        
        Object.defineProperty(obj,key,{
            get(){
                //創建watcher時,會獲取到對應的內容 並且把watcher放到了全局上
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set: (newVal)=>{
                if(value !== newVal){
                    this.observer(newVal); //給設置的新值也加上 get set 方法
                    value = newVal;
                    dep.notify(); //執行觀察者更新時的函數
                }
                
            }
        })
    }

 

    在數據劫持中,首先我們 new 了一個具有發布和訂閱的功能實例,給每一個屬性都添加上具有發布和訂閱的功能,

    同時,在給這個屬性重新賦值的時候,我們給這個具有發布和訂閱的功能功能的值,讓它執行它的訂閱函數。

 

  2、好了,現在每一個值都有發布和訂閱功能了,那么這個觀察者又是都給誰添加呢,要知道我們的觀察者還沒有執行過呢 0.0

   

   首先,v-model 是需要綁定的一個觀察者的,因為數值是可以變化的對吧

model(node,expr,vm){ //node是節點  expr是表達式 vm是實例
        // console.log(node)
        let fn = this.updater['modelUpdater'];
        
        //給輸入框加一個觀察者 一旦數據變化,會把新值賦值給輸入框 
        new Watcher(vm,expr,(newVal)=>{ //newVal 是原生方法中自帶的,是重新獲取的新值
            fn(node,newVal);
        });
        
        node.addEventListener('input',(e)=>{
            let value = e.target.value; //獲取用戶輸入的值
            this.setValue(vm,expr,value) //設置v-model的值
        })
        
        let value = this.getVal(vm,expr);
        // console.log(value)
        fn(node,value);
    },

 

 

  

  之后 {{}} 表達式是不是也需要一個啊,因為一個標簽中是可以存在多個表達式的,所以我們要遍歷表達式

  列如:<p> {{ shool.name }}  {{  shool.name  }} </p>

getContentValue(vm,expr){
        //遍歷表達式 將內容 重新替換成一個完整的內容 返還回去
        return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ 
            return this.getVal(vm,args[1]);
        });
    },
    text(node,expr,vm){
        let fn = this.updater['textUpdater']
        //console.log(expr) :{{ school.name }}
        let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ 
            //console.log(args) :["{{ school.name }}", " school.name ", 0, "{{ school.name }}"]
            
            //給每個表達式 都加上觀察者
            new Watcher(vm,args[1],()=>{
                fn(node,this.getContentValue(vm,expr)); //返回一個全的字符串
            })
            
            return this.getVal(vm,args[1]);
        })
        // console.log(content) //
        fn(node,content);
    },

 

  

  v-html 是不是也需要添加一個啊,當然還有好多,基本都是這樣的一種形式,可以仿照者寫

  

html(node,expr,vm,eventName){
        let fn = this.updater['htmlUpdater']
        
        //給輸入框加一個觀察者 一旦數據變化,會把新值賦值給輸入框
        new Watcher(vm,expr,(newVal)=>{ //newVal 是原生方法中自帶的,是重新獲取的新值
            fn(node,newVal);
        });
        
        let value = this.getVal(vm,expr);
        
        fn(node,value);
    },

 

 

  3、好啦,vue的數據雙向綁定就實現啦,下面是完整的代碼,大家可以在理解理解,運行一下試試吧!

  

/**
 *  (發布訂閱) 觀察者 被觀察者
 */
class Dep{
    constructor(){
        this.subs = [] //存放所有的 watcher
    }
    //訂閱
    addSub(watcher){ //添加 watcher
        this.subs.push(watcher)
    }
    //發布
    notify(){
        this.subs.forEach(watcher=>watcher.updata());
    }
}

/**
 * 觀察者
 */
class Watcher{
    constructor(vm,expr,cd){
        this.vm = vm;
        this.expr = expr;
        this.cd = cd;
        
        //存放舊值
        this.oldValue = this.get();
    }
    get(){ //獲取舊值
        Dep.target = this; //先把自己放到 this 上
        //取值時 把這個觀察者和數據關聯起來
        let value = CompileUtil.getVal(this.vm,this.expr);
        Dep.target = null; //不取消 任何值取都會 添加watcher
        return value;
    }
    updata(){
        let newVal = CompileUtil.getVal(this.vm,this.expr); //獲取新值
        if(newVal !== this.oldValue){
            this.cd(newVal)
        }
    }
    
}

/**
 * 數據劫持
 */
class Observer{
    constructor(data) {
        this.observer(data);
    }
    observer(data){
        //如果是對象才觀察
        if(data && typeof data === 'object'){
            
            for (let key in data) { //循環 data 中的所有子項
                this.defineReactive(data,key,data[key]);
            }
        }
    }
    //實現數據劫持
    defineReactive(obj,key,value){
        this.observer(value); //如果傳進來的參數是對象,就回調一下這個函數,就是一個遞歸函數
        
        let dep = new Dep(); //給每一個屬性都添加一個具有發布和訂閱的功能
        
        Object.defineProperty(obj,key,{
            get(){
                //創建watcher時,會獲取到對應的內容 並且把watcher放到了全局上
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set: (newVal)=>{
                if(value !== newVal){
                    this.observer(newVal); //給設置的新值也加上 get set 方法
                    value = newVal;
                    dep.notify(); //執行觀察者更新時的函數
                }
                
            }
        })
    }
}

/**
 * 模板編譯
 */
class Compiler{
    constructor(el,vm) {
        this.vm = vm;
        //判斷el屬性 是不是一個元素 不是就獲取
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        // console.log(this.el);
        
        //把當前的節點元素 獲取到 放到內存中 創建文檔碎片
        let fragment = this.node2fragment(this.el);
        
        //把節點中的內容進行替換
        
        //模板編譯 用數據編譯
        this.compile(fragment);
        
        //把內容在塞到頁面中
        this.el.appendChild(fragment);
    }
    
    //判斷屬性是不是以 v- 開頭
    isDirective(attrName){
        return attrName.startsWith('v-');
        // return /^v-/.test(attrName)
    }
    
    //編譯元素的
    compileElement(node){
        let attributes = node.attributes; //類數組, 獲取所有node節點的屬性和屬性值
        // console.log(attributes)
        attributes = [...attributes]
        // console.log(attributes)
        attributes.forEach(attr=>{ //是一個屬性對象attr
            let {name, value:expr} = attr; //:expr是給value起一個別名叫 expr **school.name
            //判斷是不是vue指令
            if(this.isDirective(name)){  // v-model='asdad' v-on:click='dsa'
                let [,directive] = name.split('-');
                let [directiveName, eventName] = directive.split(':');
                //需要調用不同的指令來處理 *** v-if v-modle v-show v-else
                CompileUtil[directiveName](node,expr,this.vm,eventName);
            }
        })
    }
    
    //編譯文本的
    compileText(node){ //判斷節點中是否包含 {{}}
        let content = node.textContent;
        if(/\{\{.+?\}\}/.test(content)){
            
            CompileUtil['text'](node,content,this.vm);
        }
        
    }
    
    //用來編譯內存中的dom節點 核心方法
    compile(node){ 
        let childNodes = node.childNodes; //獲取node的所有子節點
        [...childNodes].forEach(child=>{
            if(this.isElementNode(child)){ //判斷是不是元素節點
            
                this.compileElement(child); //編譯元素指令
                
                //如果是元素節點的話 需要把自己傳不進去 再去遍歷子元素節點
                this.compile(child);
            }else{ //文本元素
                this.compileText(child); //編譯文本指令
            }
        })
    }
    
    //獲取所有元素,放到內存中
    node2fragment(node){
        //創建一個文檔碎片
        let fragment = document.createDocumentFragment();
        let firstChild;
        
        //將node節點的的第一個節點給firstChild 如果node節點的的第一個節點為空則結束
        while(firstChild = node.firstChild){ 
            //appendChild具有移動性
            fragment.appendChild(firstChild);
        }
        return fragment;
    }
    
    // 是不是元素節點
    isElementNode(node){
        return node.nodeType === 1;
    }
}

CompileUtil = {
    
    //根據表達式獲取對應的數據
    getVal(vm,expr){ 
        // 7. reduce() 方法
         //  函數的參數 (第一參數)1.相加的初始值,2.循環出來的那一項,3.索引 4.循環的數組
         //  (第二個參數)初始值
         //  返回值:總和的結果
         expr = expr.trim()
        return expr.split('.').reduce((data,current)=>{ //[school,name] 
            // console.log(data,current)
            return data[current];
        },vm.$data);
    },
    setValue(vm,expr,value){
        expr.split('.').reduce((data,current,index,arr)=>{ //[school,name]
            if(index === arr.length - 1){ //如果循環到最后一項時執行
                //console.log(data,current)
                //對象賦值是賦值對象的地址 所以更改data會改變相對應的對象
                data[current] = value; //將數據從新賦值 school.name = xxxx
                
            }
            return data[current];
        },vm.$data);
    },
    model(node,expr,vm){ //node是節點  expr是表達式 vm是實例
        // console.log(node)
        let fn = this.updater['modelUpdater'];
        
        //給輸入框加一個觀察者 一旦數據變化,會把新值賦值給輸入框 
        new Watcher(vm,expr,(newVal)=>{ //newVal 是原生方法中自帶的,是重新獲取的新值
            fn(node,newVal);
        });
        
        node.addEventListener('input',(e)=>{
            let value = e.target.value; //獲取用戶輸入的值
            this.setValue(vm,expr,value) //設置v-model的值
        })
        
        let value = this.getVal(vm,expr);
        // console.log(value)
        fn(node,value);
    },
    on(node,expr,vm,eventName){
        node.addEventListener(eventName, (e)=>{ //給node添加事件
            vm[expr].call(vm,e)
        })
    },
    html(node,expr,vm,eventName){
        let fn = this.updater['htmlUpdater']
        
        //給輸入框加一個觀察者 一旦數據變化,會把新值賦值給輸入框
        new Watcher(vm,expr,(newVal)=>{ //newVal 是原生方法中自帶的,是重新獲取的新值
            fn(node,newVal);
        });
        
        let value = this.getVal(vm,expr);
        
        fn(node,value);
    },
    getContentValue(vm,expr){
        //遍歷表達式 將內容 重新替換成一個完整的內容 返還回去
        return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ 
            return this.getVal(vm,args[1]);
        });
    },
    text(node,expr,vm){
        let fn = this.updater['textUpdater']
        //console.log(expr) :{{ school.name }}
        let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ 
            //console.log(args) :["{{ school.name }}", " school.name ", 0, "{{ school.name }}"]
            
            //給每個表達式 都加上觀察者
            new Watcher(vm,args[1],()=>{
                fn(node,this.getContentValue(vm,expr)); //返回一個全的字符串
            })
            
            return this.getVal(vm,args[1]);
        })
        // console.log(content) //
        fn(node,content);
    },
    updater: {
        //把數據插入到value中
        modelUpdater(node,value){ 
            node.value = value;
        },
        htmlUpdater(node,value){
            node.innerHTML = value;
        },
        //處理文本節點
        textUpdater(node,value){
            //textContent 屬性設置或返回指定節點的文本內容,以及它的所有后代。
            node.textContent = value;
        }
    }
}

class Vue{
    constructor(options) {
        this.$el = options.el;
        this.$data = options.data;
        this.computed = options.computed;
        this.methods = options.methods;
        //這個根元素存在則編譯模板
        if(this.$el){
             
            //數據劫持 把 this.$data 數據全部編譯成 用 Object.defineProperty 來定義
            new Observer(this.$data);
            
            //把 vm上的數據獲取操作 都代理到 vm.$data 上
            this.proxyVm(this.$data);
            
            //計算機屬性
            for (let key in this.computed) {
                Object.defineProperty(this.$data,key,{ //有依賴關系 數據
                    get:()=>{
                        return this.computed[key].call(this);
                    }
                })
            };
            
            //函數的儲存
            for (let key in this.methods) {
                
                Object.defineProperty(this,key,{
                    get: ()=>{
                        return this.methods[key]
                    }
                })
            }
            
            //模板編譯
            new Compiler(this.$el,this);
        }
    }
    //給vm.$data 代理
    proxyVm(data){
        for (let key in data) {
            //實現了通過vm.xxx 可以取到vm.$data.xxx
            Object.defineProperty(this,key,{ //this 是 Vue 實例
                get:()=>{ //取vm 的值 等於 取vm.$data的值
                    // console.log(this)
                    return data[key]; // 進行轉換操作
                },
                set: (newVal)=>{ //設置vm 的值 等於 設置vm.$data的值
                    data[key] = newVal
                }
            })
        }
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM