自己實現 一個 Vue框架,包含了Vue的核心原理


Vue工作機制

  1. vue工作機制
  2. Vue響應式的原理
  3. 依賴收集與追蹤
  4. 編譯compile

 

html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
</head>

<body>
    <div id="app">
        <!-- 插值綁定 -->
        <p>{{name}}</p>
        <!-- 指令系統 -->
        <p k-text="name"></p>
        <p>{{age}}</p>
        <p>{{doubleAge}}</p>
        <!-- 雙向綁定 -->
        <input type="text" k-model="name">
        <!-- 事件處理 -->
        <button @click="changeName">呵呵</button>
        <!-- html內容解析 -->
        <div k-html="html"></div>
    </div>
    <script src='./compile.js'></script>
    <script src='./kvue.js'></script>
    <script>
    new KVue({   
        el: '#app',
        data: {    
            name: "I am test.",
                age: 17,
                html: '<button>這是一個按鈕</button>'  
        },
        created() {    
            console.log('開始啦');    
            setTimeout(() => {     
                this.name = '我是異步事件'   
            }, 1500)  
        },
        methods: {    
            changeName() {     
                this.name = '哈嘍,哈哈哈哈';     
                this.age = 20;     
                this.id = 'xx' ;    
                console.log(1, this)   ;
            }  
        } 
    })
    </script>
</body>

</html>

kvue.js

/*
 * @Author: liguowei01
 * @Date:   2019-12-31 11:17:12
 * @Last Modified by:   liguowei01
 * @Last Modified time: 2020-01-02 17:55:53
 */

// 用法: new KVue({data:{...}})
class KVue {
    constructor(options) {
        this.$options = options;
        //數據的響應化
        this.$data = options.data;
        this.observe(this.$data); //觀察數據

        //模擬一下watcher創建
        // new Watcher();        //實例一
        // this.$data.test;
        // new Watcher();        //實例二    實例二不等於實例一
        // this.$data.foo.bar;
        new Compile(options.el, this);
        //生命周期函數
        //created
        if (options.created) {
            //options.created();    //本來是這樣執行,下面的調用call()方法,為函數指定執行作用域
            options.created.call(this); //這樣就可以在created函數中用this了。
        }
    }
    observe(obj) {
            //檢驗數據類型必須是對象
            if (!obj || typeof obj !== 'object') {
                return;
            }
            //遍歷該對象
            Object.keys(obj).forEach(key => {
                this.defineReactive(obj, key, obj[key]);
                //代理配置項 data 中的屬性到vue實例上
                this.proxyData(key);
            })
        }
        //數據響應化(數據劫持)
    defineReactive(obj, key, val) {
        this.observe(val); //遞歸解決數據的嵌套
        const dep = new Dep();
        Object.defineProperty(obj, key, {
            get() {
                Dep.target && dep.addDep(Dep.target);
                return val
            },
            set(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                // console.log(`${key}屬性更新了:${newVal}`)
                dep.notify();
            }
        })
    }
    //代理函數()
    proxyData(key) {
        Object.defineProperty(this, key, {
            get() {
                return this.$data[key];
            },
            set(newVal) {
                this.$data[key] = newVal;
            }
        })
    }

}
//vue 數據綁定的原理是什么?
//首先,把vue選項里的data中的每個屬性都利用了Object.defineProperty()定義了一個屬性,
//都定義了get和set這樣的話讓我們的機會監聽數據和變化,
//當這些屬性發生變化時,我們可以通知那些需要更新的地方去更新

//依賴搜集
//Dep: 用來管理 Watcher
class Dep {
    constructor() {
            //這里存在若干依賴(watcher,一個watcher對應一個屬性)
            this.deps = [];
        }
        //添加依賴的方法,搜集依賴時,往這里面放東西
    addDep(dep) {
            this.deps.push(dep)
        }
        //通知方法,用來通知所有的watcher 去更新
    notify() {
        this.deps.forEach(dep => dep.updata())
    }

}

//Watcher 用來做具體更新的對象
class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm;
        this.key = key;
        this.cb = cb;
        //將當前watcher實例指定到Dep靜態屬性target
        Dep.target = this;
        this.vm[this.key]; //觸發getter,添加依賴
        Dep.target = null;
    }
    updata() {
        // console.log('屬性更新了');
        this.cb.call(this.vm, this.vm[this.key])
    }
}

Compile.js

/*
 * @Author: liguowei01
 * @Date:   2020-01-02 10:34:50
 * @Last Modified by:   liguowei01
 * @Last Modified time: 2020-01-03 09:18:12
 */
//用法 new Compile(el,vm)

class Compile {
    constructor(el, vm) {
            //要遍歷的宿主節點
            this.$el = document.querySelector(el);
            this.$vm = vm; //在其他方法中方便使用
            //編譯
            if (this.$el) {
                //轉換內部內容為片段Fragment
                this.$fragment = this.node2Fragment(this.$el);
                //執行編譯
                this.compile(this.$fragment);
                //將編譯完的html追加到$el
                this.$el.appendChild(this.$fragment);
            }
        }
        //將宿主元素中的代碼片段拿出來遍歷,這樣做比較高效
    node2Fragment(el) {
        //創建一個代碼塊
        const frag = document.createDocumentFragment();
        //將el中所有子元素“搬家”(移動)到frag中
        let child;
        while (child = el.firstChild) {
            frag.appendChild(child);
        }
        return frag;
    }
    compile(el) {
        const childNodes = el.childNodes;
        Array.from(childNodes).forEach(node => {
            //判斷類型
            if (this.isElement(node)) {
                //元素
                console.log('編譯元素', node.nodeName);
                //查找k-, @, :
                const nodeAttrs = node.attributes;
                Array.from(nodeAttrs).forEach(attr => {
                    const attrName = attr.name;
                    const exp = attr.value;
                    if (this.isDirective(attrName)) {
                        //k-text
                        const dir = attrName.substring(2);
                        //執行指令
                        this[dir] && this[dir](node, this.$vm, exp);
                    }
                    if (this.isEvent(attrName)) {
                        //@click
                        let dir = attrName.substring(1); // text
                        this.eventHandler(node, this.$vm, exp, dir);
                    }
                })
            } else if (this.isInterpolation(node)) {
                //插值文本{{}}
                console.log('編譯文本', node.nodeName);
                this.compileText(node);
            }
            //遞歸子節點
            if (node.childNodes && node.childNodes.length > 0) {
                this.compile(node)
            }
        })
    }
    isDirective(attr) {
        return attr.indexOf('k-') == 0;
    }
    isEvent(attr) {
        return attr.indexOf('@') == 0;
    }
    isElement(node) {
            return node.nodeType === 1;
        }
        //插值文本
    isInterpolation(node) {
            return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
        }
        //編譯文本
    compileText(node) {
            //console.log(RegExp.$1);    //正則對象RegExp的靜態屬性$1就是第一個匹配的值 就是上面'name'
            //node.textContent = this.$vm.$data[RegExp.$1];
            this.updata(node, this.$vm, RegExp.$1, 'text');
        }
        /*
         * @作用: 更新函數 根據指令決定是哪個更新器 它將來需要知道(參數)
         * @params: node 更新的節點
         * @params: vm    kvue的實例
         * @params: exp 正則表達式    匹配的結果 如:name
         * @params: dir    指令(文本、事件、其他) 如:text,html,model
         * 這個方法是個通用方法,將來要被調用很多次
         */
    updata(node, vm, exp, dir) {
            const updaterFn = this[dir + 'Updater']; //在當前的類里面組合一個函數名
            /*這種寫法和 this.a 一樣,this是代表當前對象,也是一個對象,
            對象名.方法名 或 對象名.屬性名 調用對象中的屬性和方法
            還有一種調用方式:對象名['方法名'] 或 對象名['屬性名']
            也可以使用 對象名['方法名']() 執行此方法
            */
            //先判斷updaterFn是否存在,如果存在則執行
            updaterFn && updaterFn(node, vm[exp]); //初始化(第一次)
            //依賴收集
            new Watcher(vm, exp, function(value) {
                //觀察vm 里的exp(屬性),並在屬性變化時,如何更新
                updaterFn && updaterFn(node, value);
            })
        }
        //更新的具體操作
    textUpdater(node, value) {
        node.textContent = value;
    }
    text(node, vm, exp) {
            this.updata(node, vm, exp, 'text');
        } 
        // 事件處理     
    eventHandler(node, vm, exp, dir) {    
        let fn = vm.$options.methods && vm.$options.methods[exp];    
        if (dir && fn) {      
            node.addEventListener(dir, fn.bind(vm), false); 
        } 
    }
    model(node, vm, exp) {
        this.updata(node, vm, exp, 'model');    
        let val = vm.exp;    
        node.addEventListener('input', (e) => {      
            let newValue = e.target.value;      
            vm[exp] = newValue;      
            val = newValue;
        })
    }  
    modelUpdater(node, value) {
        node.value = value; 
    }
    html(node, vm, exp) {
        this.updata(node, vm, exp, 'html'); 
    }
    htmlUpdater(node, value) {
        node.innerHTML = value; 
    }


}


/*
問題1:vue編譯過程是怎樣的?
遵循3W1H原則,什么是編譯,為什么要編譯。
首先寫的這些模板的語句,html根本就不能識別,
我們通過編譯的過程,可以進行依賴的收集,
進行依賴收集以后,我們就把data中的數據模型和視圖之間產生了綁定關系
產生了依賴關系,那么以后模型發生變化的時候,
我們就會通知這些依賴的地方讓他們進行更新,
這就是我們執行編譯的目的,這樣就做到了模型驅動視圖的變化。

問題2:雙向綁定的原理是什么?
做雙向綁定時,通常在表單元素上綁定一個v-model,
我們在編譯的時候,可以解析到v-model
操作時做了兩件事:
1.在表單元素上做了事件監聽(監聽input、change事件)
2.如果值發生變化時,在事件回調函數把最新的值設置到vue的實例上
3.因為vue的實例已經實現了數據的響應化,
它的響應化的set函數會觸發,通知界面中所有模型的依賴的更新。
所以界面中的,跟這個數據相關的部分就更新了

*/


免責聲明!

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



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