vue的MVVM


Vue的相關知識有

  • 字符串模板
  • MVVM
  • 虛擬dom和domdiff,查看下一篇筆記

字符串模板

function render(template, data) {
  const reg = /\{\{(\w+)\}\}/; // 模板字符串正則
  if (reg.test(template)) { // 判斷模板里是否有模板字符串
    const name = reg.exec(template)[1]; // 查找當前模板里第一個模板字符串的字段
    template = template.replace(reg, data[name]); // 將第一個模板字符串渲染
    return render(template, data); // 遞歸的渲染並返回渲染后的結構
  }
  return template; // 如果模板沒有模板字符串直接返回
}
// 使用
let template = '我是{{name}},年齡{{age}},性別{{sex}}';
let data = {
  name: '姓名',
  age: 18
}
render(template, data); // 我是姓名,年齡18,性別undefined

MVVM
MVVM 設計模式,是由 MVC(最早來源於后端)、MVP 等設計模式進化而來

  • M - 數據模型(Model)
  • VM - 視圖模型(ViewModel)
  • V - 視圖層(View)

在 Vue 的 MVVM 設計中,我們主要針對Compile(模板編譯),Observer(數據劫持),Watcher(數據監聽),Dep(發布訂閱)幾個部分來實現,核心邏輯流程可參照下圖:

image.png

數據監聽API

  • vue2.0和vue2.x是用defineProperty
  • vue3.0即將使用proxy

為什么要改用proxy,因為defineProperty無法監控到數組下標的變化,導致直接通過數組的下標給數組設置值,不能實時響應。 為了解決這個問題,defineProperty需要判斷如果是數組,需要重寫他的原型方法,而proxy就不需要

為什么還不上線,因為proxy的兼容性太差

defineProperty監聽

// 監聽普通屬性
function isKey(obj,key){
    return Object.defineProperty(obj,key,{
        get: function() {
            console.log('get :', key);
            return eval(key) || "";
        },
        set: function(newValue) {
            console.log('set :', newValue);
            key = newValue;
        }
    })
}
// 監聽數組屬性
function toNewArray(data,key){
    // 實例具名回調函數
    window.eval("var callback = function "+key+" (args,k){console.log('數組'+k+'發生變化...');}")
    return new NewArray(data[key],callback)  // 注入回調函數
}

class NewArray extends Array{
    constructor(arr,callback){
        if(arguments.length === 1){
            return super()
        }  // 產生中間數組會再進入構造方法
        // let args = arr  // 原數組
        arr.length === 1 ? super(arr[0].toString()) : super(...arr)
        this.callback = callback  // 注入回調具名函數
    }
    push(...args){
        super.push(...args)
        this.callback(this, this.callback.name)  // 切面調用具名回調函數
    }
    pop(){
        super.pop()
        this.callback(this, this.callback.name)
    }
    splice(...args){
        super.splice(...args)
        this.callback(this, this.callback.name)
    }
}

var data = {
    arr:[1,2,3,4],
    name:"pdt"
}
function init(data){
  Object.keys(data).forEach(key => {
     let value = data[key]
     // 如果是obj就遞歸
     if(value是對象){
         init(value)  
     }else if(Array.isArray(value)){
         // 如果value是數組
         data[key] = toNewArray(data,key)
     }else{
         // 如果是普通的值
         isKey(data,key)
     }
  })
}
init(data)

proxy監聽

var data = {
   arr:[1,2,3,4],
   name:"pdt"
}

function init(data){
  Object.keys(data).forEach(key => {
     let value = data[key]
     if(value 是對象){
       data[key] = init(value)
     }
  })
  data = newData(data)
}

init(data)

function newData(data){
    return new Proxy(data, {
        get: function(target, key, receiver) {
            console.log(target, key, receiver)
            return Reflect.get(target, key, receiver);
        },
        set: function(target, key, value, receiver) {
            console.log(target, key, value, receiver);
            return Reflect.set(target, key, value, receiver);
        }
    })
}

defineProperty實現一個Vue

<script>
class MVVM {
    constructor (options) {
        // 一上來 先把可用的東西掛載在實例上
        this.$el = options.el;
        this.$data = options.data;

        // 如果有要編譯的模板就開始編譯
        if(this.$el) {
            // 數據劫持,就是把對象的所有屬性添加 set 和 get 方法
            new Observer(this.$data);
            // 用數據和元素進行編譯
            new Compile(this.$el, this);
        }
    }
}
class Observer {
    constructor (data) {
        this.observe(data);
    }
    observe (data) {
        // 驗證 data
        if(!data || typeof data !== 'object') {
            return;
        }
        // 要對這個 data 數據將原有的屬性改成 set 和 get 的形式
        // 要將數據一一劫持,先獲取到 data 的 key 和 value
        Object.keys(data).forEach(key => {
            // 劫持(實現數據響應式)
            this.defineReactive(data, key, data[key]);
            this.observe(data[key]); // 深度劫持
        });
    }
    defineReactive (object, key, value) { // 響應式
        let _this = this;
        // 每個變化的數據都會對應一個數組,這個數組是存放所有更新的操作
        let dep = new Dep();

        // 獲取某個值被監聽到
        Object.defineProperty(object, key, {
            enumerable: true,
            configurable: true,
            get () { // 當取值時調用的方法
                Dep.target && dep.addSub(Dep.target);
                console.log(dep.subs);
                return value;
            },
            set (newValue) { // 當給 data 屬性中設置的值適合,更改獲取的屬性的值
                if(newValue !== value) {
                    _this.observe(newValue); // 重新賦值如果是對象進行深度劫持
                    value = newValue;
                    dep.notify(); // 通知所有人數據更新了
                }
            }
        });
    }
}

class Dep {
    constructor () {
        // 訂閱的數組
        this.subs = [];
    }
    addSub (watcher) { // 添加訂閱
        this.subs.push(watcher);
    }
    notify () { // 通知
        this.subs.forEach(watcher => watcher.update());
    }
}

// 觀察者的目的就是給需要變化的那個元素增加一個觀察者,當數據變化后執行對應的方法
class Watcher {
    constructor (vm, exp, callback) {
        this.vm = vm;
        this.exp = exp;
        this.callback = callback;
        // 先獲取一下老的值
        this.value = this.get();
    }
    get () { // 獲取實例上老值得方法
        Dep.target = this;
        let value = CompileUtil.getVal(this.vm, this.exp);
        Dep.target = null;
        return value;
    }
    update () {
        let newValue = CompileUtil.getVal(this.vm, this.exp);
        let oldValue = this.value;
        // 用新值和老值進行對比,如果變化,就調用更新方法
        if(newValue !== oldValue) {
            this.callback(newValue); // 如果修改后得新舊值不等就執行回調
        }
    }
}

class Compile {
    constructor (el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        if(this.el) {
            // 如果這個元素能獲取到,我們才開始編譯
            // 1.先把這些真實的 DOM 移動到內存種 fragment
            let fragment = this.nodeToFragment(this.el);
            // 2.編譯 => 提取想要的元素節點 v-model 和文本節點 {{message}}
            this.compile(fragment);
            // 把編譯好的 fragment再塞回到頁面中去
            this.el.appendChild(fragment);
        }
    }

    /* 專門寫一些輔助方法 */
    isElementNode (node) { // 是不是 dom 節點
        return node.nodeType === 1;
    }
    isDirective (name) { // 是不是指令
        return name.includes('v-');
    }

    /* 核心方法 */
    nodeToFragment (el) { // 需要將 el 中的內容全部放到內存中
        // 文檔碎片 內存中的 dom 節點
        let fragment = document.createDocumentFragment();
        let firstChild;
        while(firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment; // 內存中的節點
    }
    compile (fragment) { // 編譯文檔碎片方法
        // 需要遞歸
        let childNodes = fragment.childNodes;
        Array.from(childNodes).forEach(node => {
            if(this.isElementNode(node)) {
                // 是元素節點,還需要繼續深入的檢查
                // console.log('element', node);
                this.compile(node);
                // 這里需要編譯元素
                this.compileElement(node);
            } else {
                // 是文本節點
                // console.log('text', node);
                // 這里需要編譯文本
                this.compileText(node);
            }
        });
    }
    compileElement (node) { // 編譯元素節點
        // 帶 v-model 的
        let attrs = node.attributes; // 取出當前節點的屬性
        Array.from(attrs).forEach(attr => {
            // 判斷屬性名字是不是包含 v-
            let attrName = attr.name;
            if(this.isDirective(attrName)) {
                // 取到對應的值,放在節點中
                let exp = attr.value;
                let [, type] = attrName.split('-');
                // node this.vm.$date exp
                CompileUtil[type](node, this.vm, exp);
            }
        });
    }
    compileText (node) { // 編譯文本節點
        // 帶 {{}} 的
        let exp = node.textContent; // 獲取文本中的內容
        let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} {{c}}
        if(reg.test(exp)) {
            // node this.vm.$date exp
            CompileUtil['text'](node, this.vm, exp);
        }
    }
}

CompileUtil = {
    getVal (vm, exp) { // 獲取實例上對應的數據
        exp = exp.split('.');
        return exp.reduce((prev, next) => {
            return prev[next];
        }, vm.$data);
    },
    setVal (vm, exp, newVal) { // 設置實例上對應的數據
        exp = exp.split('.');
        return exp.reduce((prev, next, currentIndex) => {
            if(currentIndex === exp.length - 1) {
                return prev[next] = newVal;
            }
            return prev[next];
        }, vm.$data);
    },
    getTextVal (vm, exp) { // 獲取編譯文本后的結果
        return exp.replace(/\{\{([^}]+)\}\}/g, (...arg) => {
            return this.getVal(vm, arg[1]);
        });
    },
    text (node, vm, exp) { //文本處理
        let updateFn = this.updater['textUpdater'];
        let value = this.getTextVal(vm, exp);
        exp.replace(/\{\{([^}]+)\}\}/g, (...arg) => {
            new Watcher(vm, arg[1], newValue => {
                // 如果數據變化了,文本節點應該重新獲取依賴的數據更新文本中的內容
                updateFn && updateFn(node, newValue);
            });
        });

        updateFn && updateFn(node, value);
    },
    model (node, vm, exp) { // 輸入框處理
        let updateFn = this.updater['modelUpdater'];
        let value = this.getVal(vm, exp);
        // 這里應該加一個監控,數據變化了,應該調用 watch 的回調
        new Watcher(vm, exp, newValue => {
            updateFn && updateFn(node, newValue);
        });
        // 添加輸入框事件實現雙向綁定
        node.addEventListener('input', e => {
            let newValue = e.target.value;
            this.setVal(vm, exp, newValue);
        });
        // 防止沒有的指令解析時報錯
        updateFn && updateFn(node, value);
    },
    updater: {
        // 文本更新
        textUpdater (node, value) {
            node.textContent = value;
        },
        // 輸入框更新
        modelUpdater (node, value) {
            node.value = value;
        }
    }
};
</script>

// 使用
<div id="app">
   <!-- 雙向數據綁定 靠的是表單 -->
   <input type="text" v-model="message">
   <div>{{message}}</div>
</div>
<script>
let vm = new MVVM({
  el: '#app',
  data: {
	message: 'hello world!'
  }
});
</script>

使用proxy寫一個簡易版的vue

<script type="text/javascript">
class Vue {
    constructor(options) {
        this.$el = document.querySelector(options.el);
        this.$methods = options.methods;
        this._binding = {};
        this._observer(options.data);
        this._compile(this.$el);
    }
    _pushWatcher(watcher) {
        if (!this._binding[watcher.key]) {
            this._binding[watcher.key] = [];
        }
        this._binding[watcher.key].push(watcher);
    }
    /*
     observer的作用是能夠對所有的數據進行監聽操作,通過使用Proxy對象
     中的set方法來監聽,如有發生變動就會拿到最新值通知訂閱者。
    */
    _observer(datas) {
        const me = this;
        const handler = {
            set(target, key, value) {
                const rets = Reflect.set(target, key, value);
                me._binding[key].map(item => {
                    item.update();
                });
                return rets;
            }
        };
        this.$data = new Proxy(datas, handler);
    }
    /*
     指令解析器,對每個元素節點的指令進行掃描和解析,根據指令模板替換數據,以及綁定相對應的更新函數
    */
    _compile(root) {
        const nodes = Array.prototype.slice.call(root.children);
        const data = this.$data;
        nodes.map(node => {
            if (node.children && node.children.length) {
                this._compile(node.children);
            }
            const $input = node.tagName.toLocaleUpperCase() === "INPUT";
            const $textarea = node.tagName.toLocaleUpperCase() === "TEXTAREA";
            const $vmodel = node.hasAttribute('v-model');
            // 如果是input框 或 textarea 的話,並且帶有 v-model 屬性的
            if (($vmodel && $input) || ($vmodel && $textarea)) {
                const key = node.getAttribute('v-model');
                this._pushWatcher(new Watcher(node, 'value', data, key));
                node.addEventListener('input', () => {
                    data[key] = node.value;
                });
            }
            if (node.hasAttribute('v-html')) {
                const key = node.getAttribute('v-html');
                this._pushWatcher(new Watcher(node, 'innerHTML', data, key));
            }
            if (node.hasAttribute('@click')) {
                const methodName = node.getAttribute('@click');
                const method = this.$methods[methodName].bind(data);
                node.addEventListener('click', method);
            }
        });
    }
}
/*
watcher的作用是 鏈接Observer 和 Compile的橋梁,能夠訂閱並收到每個屬性變動的通知,
執行指令綁定的響應的回調函數,從而更新視圖。
*/
class Watcher {
constructor(node, attr, data, key) {
    this.node = node;
    this.attr = attr;
    this.data = data;
    this.key = key;
}
update() {
    this.node[this.attr] = this.data[this.key];
}
}
</script>

// 使用
<div id="app">
<input type="text" v-model='count' />
<input type="button" value="增加" @click="add" />
<input type="button" value="減少" @click="reduce" />
<div v-html="count"></div>
</div>
<script type="text/javascript">
new Vue({
    el: '#app',
    data: {
        count: 0
    },
    methods: {
        add() {
            this.count++;
        },
        reduce() {
            this.count--;
        }
    }
});
</script>

總結上面的代碼
可以看到上面的兩個寫法都需要這么幾個構造函數

  • MVVM.js 把參數傳給Observer和Compile
  • Observer.js 劫持,就是監聽,每個key都需要搭配一個Dep,數據更新就執行Dep.updata
  • Dep.js 就是一個訂閱發布的數組而已
  • Compile.js,解析dom元素,通過屬性【v-xx】創建更新規則,每個規則創建一個watch,watch裝在對應位置的Dep數組里
  • Watch.js,閉包存着所有的dom和對應的無數個更新規則,由Dep調用它的更新

相關鏈接一
相關鏈接三
相關鏈接四
相關鏈接五
相關鏈接六

真正的Vue

  • 不能保留原來的dom,遍歷過程成vDom【生命周期是created】
  • 遍歷虛擬dom數據,生成新的真實的dom,再結合data數據,methods,計算屬性,watch,數據綁定到新的dom上
  • 當數組發生改變,生成新的vDom,對比新舊的vDom,更新到真實dom上【如果使用proxy,就不需要domdiff了】,就是一個真正的vue了

參考資料
vue源碼解讀一
vue源碼解讀二
Vue實現


免責聲明!

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



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