重新手寫一個Vue


該版把上一次的數據修改就更新全部頁面改為了局部更新,相比於上一版的在數據綁定上不是簡單的一個監聽set再全部更新,具體見下文。

總體流程

仍然是根據自己理解來實現的綁定,相較於上一版的數據更新就全部刷新,這次改成了部分頁面更改,總體流程大致如圖:(字本來就丑,那個筆芯寫更丑了,希望能看懂吧)

QQ圖片20210728143535.jpg

這里就從頭介紹下怎樣實現整個流程的

createApp

這里是整個Vue的入口,通過傳入options參數會將里面的data,methods等掛載到Vue實例上,再通過代理,讓對vm的屬性訪問轉換為對vm.$data中屬性的訪問:

static createApp(options) {
        //? 將data代理到vm上
        const vm = new Proxy(new Vue(), {
            get(target, p) {
                if (Reflect.get(target, p)) {
                    return Reflect.get(target, p);
                } else {
                    return target.$data[p]._isref ? target.$data[p].value : target.$data[p];
                }
            },
            set(target, p, value) {
                if (target[p]) {
                    Reflect.set(target, p, value);
                } else if (target.$data[p]?._isref) {
                    Reflect.set(target.$data[p], "value", value);
                } else {
                    Reflect.set(target.$data, p, value);
                }
                return true;
            },
        });

        options.onBeforCreate?.call(vm);

        vm.$data = options.data.call(vm);
        new Observer(vm).observeData(); //! 將data的數據轉為響應式

        for (const key in options.methouds) {
            vm.$methouds[key] = options.methouds[key].bind(vm);
        }

        options.onCreated?.call(vm);
        return vm;
    }

將data中的數據轉換為響應式

這個步驟通過Observer實例中的observeData來進行,我這里通過Proxy來實現(Vue2.x中使用Object.defineProperty)。

import Dep from "./dep.js";

const dep = new Dep();

export default class Observer {
    constructor(vm) {
        this.vm = vm;
    }
    observeData() {
        const data = this.vm.$data;
        for (const key in data) {
            data[key] = this.ref(data[key]);
        }
    }
    // *===============↓ 將數據轉換為響應式數據的方法 ↓===============* //
    reactive(data) {
        //? 如果對象里還有對象,遞歸實現響應式
        for (const key in data) {
            if (typeof data[key] === "object") {
                data[key] = this.reactive(data[key]);
            }
        }
        return new Proxy(data, {
            get(target, p) {
                window.target && dep.add(window.target);
                window.target = null; //? 將watch實例保存后刪除
                return Reflect.get(target, p);
            },
            //todo 修改對象屬性后修改Vnode
            set(target, p, value) {
                target._isref
                    ? Reflect.set(target, "value", value)
                    : Reflect.set(target, p, value);

                dep.notify();

                return true;
            },
        });
    }
    ref(data) {
        //? 基本數據類型會被包裝為對象再進行代理
        if (typeof data != "object") {
            data = {
                value: data,
                _isref: true,
                toSting() {
                    return this.value;
                },
            };
        }
        return this.reactive(data);
    }
}

這里在get上設置了dep.add,在第一次渲染頁面的時候會讀取到對應的$data中的屬性,在這個時候將這個屬性的位置和一個用來更新視圖的回調函數打包進Watcher的實例再放入dep中儲存起來,在以后數據更新時會觸發set,通知dep調用儲存的所有watcher實例上的update方法,update方法會比較儲存的舊值來決定是否觸發回調函數來更新視圖。

Dep:

import { nextTick } from "./util.js";

export default class Dep {
    constructor() {
        this.watchers = [];
        this.lock = true;
    }
    add(watcher) {
        this.watchers.push(watcher);
    }
    notify() {
        //? 放入微任務隊列,只要觸發一次notify就不再觸發,在微任務里更新視圖,這樣所有數據都更新后再觸發更新
        if (this.lock) {
            this.lock = false;
            nextTick(() => {
                this.watchers.forEach((watcher) => {
                    watcher.update(); //? 用watcher實例的update更新視圖
                });
                this.lock = true;
            });
        }
    }
}

Watcher:

import { getByPath } from "./util.js";

export default class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm;
        this.key = key; //? 代表該數據在$data哪里的字符串
        this.cb = cb; //? 更新頁面的回調函數
        window.target = this;
        //! 獲得舊數據,同時觸發vm[key]的get把上面一行設置watcher實例push進dep 見observer.js
        this.oldValue = getByPath(vm, key);
    }

    //? dep調用notify來調用所有的update更新視圖
    update() {
        let newValue = getByPath(this.vm, this.key);
        if (newValue === this.oldValue) return;
        this.oldValue = newValue;
        this.cb(newValue);
    }
}

為了使用方便,這里把Watcher的實例化過程掛載到vm上,實例化Watcher並推入dep的過程全由vm.$watche完成:

class Vue {
    constructor() {
       this.$watch = function (key, cb) {
            new Watcher(this, key, cb);
        }; 
    }
}

頁面渲染

通過修改原來的第一版渲染函數,這里改為了挨個讀取節點來轉換,通過讀取每個節點的字符串形式來把數據替換或把方法掛載:

export default function render($el, vm) {
    const nodes = $el.children;
    Array.prototype.forEach.call(nodes, (el) => {
        if (el.children.length > 0) {
            render(el, vm); //? 遞歸渲染子節點
        } else {
            renderTemplate(vm, el);
        }
    });
}

function renderTemplate(vm, el) {
    renderData(vm, el);
    renderEvent(vm, el);
    renderVModel(vm, el);
}

//? 將{{}}里的數據渲染
function renderData(vm, el) {
    const nodeText = el.textContent;
    const regexp = /\{\{(\s*)(?<data>.+?)(\s*)\}\}/g;
    if (regexp.test(nodeText)) {
        return nodeText.replace(regexp, (...arg) => {
            const groups = JSON.parse(JSON.stringify(arg.pop()));
            //! 將這個數據相對於vm的位置儲存進dep,每次dep收到更新時觸發回調
            vm.$watch(groups.data, (newValue) => {
                el.textContent = newValue;
            });
            el.textContent = getByPath(vm, groups.data);
        });
    }
}

... ...

再說明一下,現在的渲染操作只在進行mount的時候會執行,當以后$data屬性改變時會觸發在這里設置的回調函數,通過它來修改頁面。

一些其它細節的地方

在頁面渲染時讀取$data屬性只能通過寫在模板上的字符串,這里用了reduce方法來獲取字符串對應的值:

export function getByPath(obj, path) {
    const pathArr = path.split(".");
    return pathArr.reduce((result, curr) => {
        return result[curr];
    }, obj);
}

nextTick函數在這里只是用了開啟微任務隊列的方式實現:

export function nextTick(cb, ...arg) {
    Promise.resolve().then(() => {
        cb(...arg);
    });
}

測試

最后簡單寫個計數器來看看實現的所有功能,可以看到和預期的一樣

GIF 2021-7-28 18-06-38.gif

代碼倉庫


免責聲明!

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



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