(精華2020年5月17日更新) vue實戰篇 手寫vue底層原始碼

2b勿擾發表於2020-05-17

MYvue.js 主要作用監聽屬性變化

class MYvue {
    constructor(options) {
        this.$options = options;
        this.$data = options.data;
        //資料劫持
        this.observe(this.$data);
        this.$el = options.el;
        //包含Watcher建立
        new Complie(options.el, this)

    }
    //資料劫持
    observe(data) {
        if (!data || typeof data != 'object') {
            return;
        }
        Object.keys(data).forEach((key) => {
            //讓資料可觀測
            //this.$data.test 改變
            this.defineReactive(data, key, data[key]);
            //this.test 代理改變
            this.proxyData(key);
        })
    }
    //讓資料可觀測
    defineReactive(obj, key, value) {
        var dept = new Dep();
        //value值為物件遞迴遍歷
        this.observe(value);

        Object.defineProperty(obj, key, {
            get() {
                //屬性被讀取了,將來需要新增訂閱
                console.log('我被讀取了');
                //將Dep.target(當前的watcher物件存入Dep的deps)
                Dep.target && dept.addDep(Dep.target)
                return value;
            },
            set(newVal) {
                if (newVal == value) {
                    return;
                }
                value = newVal;
                console.log(key + '屬性更行了,他更新的值是' + newVal);
                //如果我被改變, 我將來會在這裡通知的
                dept.notify(); //變化的資料,讓watcher的update方法執行
            },
            enumerable: true,
            configurable: true
        })
    }

    //代理data中的屬性到vue例項上
    proxyData(key) {
        Object.defineProperty(this, key, {
            get() {
                return this.$data[key]
            },
            set(newVal) {
                this.$data[key] = newVal;
            }
        })
    }
}

//Dep 用來管理wather,管理者的角色
class Dep {
    constructor() {
        //存放所有的依賴(watcher),一個watcher對應一個屬性
        this.deps = [];
    }
    //收集訂閱者
    addDep(sub) {
        this.deps.push(sub);
    }
    //通知訂閱更新
    notify() {
        this.deps.forEach((sub) => {
            //你要更新了
            sub.update() //update是watchr裡面的一個函式
        })
    }
}

//監聽器物件
class Watcher {
    constructor(vm, key, cb) {
        // vm :Vue例項化物件
        // key:  需要監聽的屬性
        // cb: 是Watccher繫結的更新函式
        this.vm = vm;
        this.key = key;
        this.cb = cb;
        Dep.target = this;//this指Watcher本身

        this.vm[key] // 觸發getter ,新增依賴
        Dep.target = null;

    }
    update() {
        console.log('你的屬性要更新了');
        this.cb.call(this.vm, this.vm[this.key]);
    }
}

Complie.js 把屬性變化重新渲染html

class Complie {
    constructor(el, vm) {
        //遍歷節點
        this.$el = document.querySelector(el);
        this.$vm = vm;
        //編譯
        if (this.$el) {
            //轉換內容為片段fragment
            this.$fragment = this.node2Fragment(this.$el);

            //執行編譯
            this.replaceTemplate(this.$fragment)
            //將編譯完的html結果追加到$el
            this.$el.appendChild(this.$fragment);
        }
    }
    node2Fragment(el) {
        // createDocumentFragment 用來建立虛擬dom節點
        var frag = document.createDocumentFragment();
        let child;
        //講el中所有的元素搬家到frag
        while (el.firstChild && (child = el.firstChild)) {
            frag.appendChild(child);
        }
        return frag;
    }
    //對el立面的內容進行替換
    // 對el裡面的內容進行替換
    replaceTemplate(frag) {
        // console.log('frag.childNodes',frag.childNodes);
        var childNodes = Array.from(frag.childNodes);
        if (childNodes.length == 0) return;
        childNodes.forEach(node => {
            let txt = node.textContent;
            let reg = /\{\{(.*?)\}\}/g; // 正則匹配{{}}

            // 只讀屬性 Node.nodeType 表示的是該節點的型別   
            // nodeType 屬性可用來區分不同型別的節點,比如 元素, 文字 和 註釋。 
            // 即是文字節點又有大括號的情況{{}}
            if (node.nodeType === 3 && reg.test(txt)) {
                // console.log(RegExp.$1); // 匹配到的第一個分組 如: a.b, c
                let arr = RegExp.$1.split('.');
                let valTest = this.$vm;
                arr.forEach(key => {
                    valTest = valTest[key]; // 如this.a.b
                });
                // 用trim方法去除一下首尾空格
                node.textContent = txt.replace(reg, valTest).trim();

                // 監聽變化,第二步加的
                // 給Watcher再新增兩個引數,用來取新的值(newVal)給回撥函式傳參
                new Watcher(this.$vm, RegExp.$1, newVal => {
                    //這裡是有屬性改變的時候會更新資料
                    node.textContent = txt.replace(reg, newVal).trim();
                });
            }

            // v-modle資料的雙向繫結
            if (node.nodeType === 1) { // 元素節點
                let nodeAttr = Array.from(node.attributes); // 獲取dom上的所有屬性,是個類陣列
                nodeAttr.length > 0 && nodeAttr.forEach(attr => {
                    let name = attr.name; // v-model  type
                    let exp = attr.value; // c        text
                    if (name.includes('v-')) {
                        node.value = this.$vm[exp]; // this.c 為 2
                    }
                    // oninput .onclick
                    node.addEventListener('input', e => {
                        let newVal = e.target.value;
                        // 相當於給this.c賦了一個新值
                        // 而值的改變會呼叫set,set中又會呼叫notify,notify中呼叫watcher的update方法實現了更新
                        this.$vm[exp] = newVal;
                    });
                });
            }

            // 如果還有子節點,繼續遞迴replaceTemplate
            if (node.childNodes && node.childNodes.length) {
                this.replaceTemplate(node);
            }
        });
    }
}

使用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<script src="./MYvue.js"></script>
<script src="./compile.js"></script>
<body>
    <div id="app">
        <h1>{{song}}</h1>
        <p>主打歌為{{album.theme}} </p>
        <p> <span>作詞人為{{singer}}等人。</span><em>qq</em></p>
        <input type="text" v-model="song">
    </div>
    <script>
        // 寫法和Vue一樣
        let MYvue = new MYvue({
            el: '#app',
            data: { 
                song: '飛啊飛啊',
                album: {
                    name: '一眼萬年',
                    theme: '天外飛仙'
                },
                singer: '胡歌'
            }
        });
    </script>
</body>
</html>

相關文章