写一个超mini版的Vue

153 阅读4分钟

实例化

先上Demo

<body>
        <div id="app">
            <h1>差值表达式</h1>
            <div>
                {{ name }}<br />
                {{ age }}
            </div>
            <h1>v-text</h1>
            <div v-text="name"></div>
            <h1>v-model</h1>
            <input type="text" v-model="name" />
            <input type="text" v-model="age" />
        </div>
    </body>
    <script src="./js/vue.js"></script>
    <script>
        var vm = new Vue({
            el: "#app",
            data: {
                name: "dfklqw",
                age: 10,
                friend: {
                    name: "ccccc",
                },
            },
        });
    </script>

Demo页面就长这个样子: image.png

正常使用Vue时,引入Vue.js并实例化,接下来就是实现Vue.js。

Vue.js

class Vue {
    constructor(options) {
        // 存储实例化Vue时传入的选项
        // 保存data
        this.$data = options?.data || {};
        // 保存el
        this.$el = options?.el;
        // 将data中的属性添加到Vue实例上并添加getter和setter
        this.proxyData(this.$data);
        // 调用Observer把data中的数据转换为getter和setter
        new Observer(this.$data);
        // 将Vue实例传入Compiler
        new Compiler(this);
    }
    proxyData($data) {
        // 遍历data中的属性,并添加getter和setter
        Object.keys($data).forEach((key) => {
            Object.defineProperty(this, key, {
                configurable: true,
                enumerable: true,
                get() {
                    return $data[key];
                },
                set(newValue) {
                    if (newValue !== $data[key]) {
                        $data[key] = newValue;
                    }
                },
            });
        });
    }
}

Vue.js主要负责创建实例属性保存实例化Vue时传入的options选项,同时将data中的属性转换为getter和setter并注入到Vue实例上,方便后续使用。接着实例化Observer对象, 完成data中对象数据的响应式处理,以及实例化Compiler完成模板编译。

Observer.js

class Observer {
    constructor($data) {
        // 遍历$data对象
        this.walk($data);
    }
    walk($data) {
        if ($data && typeof $data === "object") {
            Object.keys($data).forEach((key) => {
                // 之所以传入$data[key]是为了防止发生死递归
                this.defineReactive($data, key, $data[key]);
            });
        }
    }
    defineReactive($data, key, value) {
        // 判断value是否是对象,如果是的话,将对象中的属性也转换为getter和setter
        this.walk(value);
        // 保存当前this ——> Observer
        let that = this;
        Object.defineProperty($data, key, {
            enumerable: true,
            configurable: true,
            get() {
                return value;
            },
            set(newValue) {
                if (newValue !== value) {
                    value = newValue;
                    // 判断新值是不是对象
                    that.walk(value);
                }
            },
        });
    }
}

Observer的主要作用是负责将data中的属性转换为getter和setter,如果data中的某个属性也是对象,则将对象中的属性也进行转换。

需要注意的是这个地方:

Object.keys($data).forEach((key) => {
       this.defineProperty($data, key, $data[key]);
});

如果不传入$data[key],则会在defineReactive中绑定get时发生死递归

...
walk($data) {
...
            Object.keys($data).forEach((key) => {
                this.defineReactive($data, key);
            });
...
}
defineReactive($data, key) {
        ...
        Object.defineProperty($data, key, {
            enumerable: true,
            configurable: true,
            get() {
                return $data[key];
            },
        ...
        });
}
...

按照这种不传入$data[key]的写法,当给data中的任意属性重新赋值时会报错

image.png

因为当给Vue实例中的属性重新赋值时,会触发对应的set(Vue.js中给Vue实例上的属性绑定了getter和setter)

// vue.js
set(newValue) {
   if (newValue !== $data[key]) {
          $data[key] = newValue;
   }
}

而set中$data[key]又会触发对应的get(Observer.js中给data中的的属性绑定了getter和setter),

// Observer.js
get() {
   return $data[key];
}

而get又返回$data[key],这就造成了无限递归导致超出最大调用堆栈大小的异常。

Compiler.js

到此为止,数据的响应式处理就已经处理完成了 image.png

Vue实例上的数据及data中的数据已经都转换为getter和setter。 然后实例化Compiler进行模板编译

// compiler.js
class Compiler {
    constructor(vm) {
        // 保存vm实例
        this.vm = vm;
        // 保存el
        this.el = typeof vm.$el === "string" ? document.querySelector(vm.$el) : vm.$el;
        this.compiler(this.el);
    }
    compiler(el) {
        // 获取#app的子节点
        let childNodes = el.childNodes;
        Array.from(childNodes).forEach((node) => {
            // 判断子节点是元素节点还是文本节点
            if (this.isTextNode(node)) {
                this.compilerText(node);
            } else if (this.isElementNode(node)) {
                this.compilerElement(node);
            }
            // 判断node是否有子节点
            if (node.childNodes && node.childNodes.length) {
                this.compiler(node);
            }
        });
    }
    // 判断是否是文本节点(差值表达式)
    isTextNode(node) {
        return node.nodeType === 3;
    }
    // 判断是否是元素节点(指令)
    isElementNode(node) {
        return node.nodeType === 1;
    }
    // 判断是否是vue指令
    isDirective(attrName) {
        // 属性如果以v-开始是vue指令,返回对应的指令v-之后的部分,否则返回false
        return attrName.startsWith("v-") ? attrName.slice(2) : false;
    }
    compilerText(node) {
        // 匹配差值表达式{{ xxx }}
        let reg = /\{\{(.+?)\}\}/;
        if (reg.test(node.textContent)) {
            // 获取匹配到的内容
            let key = RegExp.$1.trim();
            // 将数据替换差值表达式
            node.textContent = node.textContent.replace(reg, this.vm[key]);
        }
    }
    compilerElement(node) {
        // 获取节点所有属性
        let attrs = node.attributes;
        Array.from(attrs).forEach((attr) => {
            // 判断是否是vue指令
            // 如果是vue指令,则可以得到对应的指令名, v-text ——> text
            let name = this.isDirective(attr.name);
            if (name) {
                // 获取指令对应的数据 v-text="age" ——> age
                let key = attr.value;
                // 调用更新视图方法
                this.update(node, name, key);
            }
        });
    }
    update(node, name, key) {
        // 获取方法名  'text' + 'Update' = 'textUpdate'
        let fn = name + "Update";
        this[fn] && this[fn](node, key);
    }

    textUpdate(node, key) {
        // 替换数据
        node.textContent = this.vm[key];
    }
    modelUpdate(node, key) {
        // 替换数据
        node.value = this.vm[key];
    }
}

在HTML中引入对应的js文件

<script src="./js/compiler.js"></script>
<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>

此时页面已经可以正常渲染差值表达式和指令了

image.png

发布者 - 订阅者

接着来就是数据的双向绑定,先上代码

// dep.js 发布者
class Dep {
    constructor() {
        // 存储订阅者
        this.subs = [];
    }
    // 存储订阅者
    static sub = null;
    // 添加订阅者
    addSub(sub) {
        if (sub && sub.update) {
            this.subs.push(sub);
        }
    }
    // 发送通知
    notify() {
        this.subs.forEach((sub) => {
            sub.update();
        });
    }
}


// watcher.js 订阅者
class Watcher {
    constructor(vm, key, cb) {
        // 存储vue实例
        this.vm = vm;
        // 存储当前key
        this.key = key;
        // 存储更新视图回调
        this.cb = cb;
        // 将当前this存储到dep中
        Dep.sub = this;
        // 存储旧值,当前key对应的数据
        this.oldValue = this.vm[this.key]; // 此时触发vue.js中的get,继而触发observer.js中的get把当前订阅者添加到dep中
        // 清空Dep.sub,防止重复添加
        Dep.sub = null;
    }
    // 更新视图
    update() {
        // 当触发更新视图方法时,key对应的数据已经最新的数据
        let newValue = this.vm[this.key];
        if (newValue !== this.oldValue) {
            // 调用回调,更新视图
            this.cb && this.cb(newValue);
        }
    }
}
// compiler.js
    compilerText(node) {
        // 匹配差值表达式{{ xxx }}
        let reg = /\{\{(.+?)\}\}/;
        if (reg.test(node.textContent)) {
            // 获取匹配到的内容
            let key = RegExp.$1.trim();
            let value = this.vm[key];
            // 将数据替换差值表达式
            node.textContent = node.textContent.replace(reg,value);
            new Watcher(this.vm, key, (newValue) => {
                node.textContent = newValue;
            });
        }
    }
    textUpdate(node, key) {
        // 替换数据
        node.textContent = this.vm[key];
        new Watcher(this.vm, key, (newValue) => {
            node.textContent = newValue;
        });
    }
    modelUpdate(node, key) {
        node.value = this.vm[key];
        new Watcher(this.vm, key, (newValue) => {
            node.value = newValue;
        });
        // 当输入框输入内容时,更新数据
        node.addEventListener("input", () => {
            this.vm[key] = node.value;
        });
    }
// observer.js
...
    defineReactive($data, key, value) {
        ...
        // 实例化dep
        let dep = new Dep();
        Object.defineProperty($data, key, {
            enumerable: true,
            configurable: true,
            get() {
                Dep.sub && dep.addSub(Dep.sub);
                return value;
            },
            set(newValue) {
                if (newValue !== value) {
                    value = newValue;
                    // 更新视图
                    dep.notify();
                    // 判断新值是不是对象
                    that.walk(value);
                }
            },
        });
    }

核心逻辑编译模板时实例化Watcher对象,在Watcher内部存储当前Watcher实例Dep.sub = this;,同时存储旧值this.oldValue = this.vm[this.key],此时触发对应的get方法,在get方法中将watcher实例添加到订阅者数组中Dep.sub && dep.addSub(Dep.sub);。当数据再次更新时,触发对应的set方法,发布通知dep.notify();,触发实例化Watcher时传入的回调,更新视图。

总结

完整代码如下:

HTML

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <div id="app">
            <h1>差值表达式</h1>
            <div>
                {{ name }}<br />
                {{ age }}
            </div>
            <h1>v-text</h1>
            <div v-text="name"></div>
            <h1>v-model</h1>
            <input type="text" v-model="name" />
            <input type="text" v-model="age" />
        </div>
    </body>
    <script src="./js/dep.js"></script>
    <script src="./js/watcher.js"></script>
    <script src="./js/compiler.js"></script>
    <script src="./js/observer.js"></script>
    <script src="./js/vue.js"></script>
    <script>
        var vm = new Vue({
            el: "#app",
            data: {
                name: "name",
                age: 10,
                friend: {
                    name: "ccccc",
                },
            },
        });
    </script>
</html>

Vue.js

class Vue {
    constructor(options) {
        // 存储实例化Vue时传入的选项
        // 保存data
        this.$data = options?.data || {};
        // 保存el
        this.$el = options?.el;
        // 将data中的属性添加到Vue实例上并添加getter和setter
        this.proxyData(this.$data);
        // 调用Observer把data中的数据转换为getter和setter
        new Observer(this.$data);
        // 将Vue实例传入Compiler
        new Compiler(this);
    }
    proxyData($data) {
        // 遍历data中的属性,并添加getter和setter
        Object.keys($data).forEach((key) => {
            Object.defineProperty(this, key, {
                configurable: true,
                enumerable: true,
                get() {
                    return $data[key];
                },
                set(newValue) {
                    if (newValue !== $data[key]) {
                        $data[key] = newValue;
                    }
                },
            });
        });
    }
}

Observer.js

class Observer {
    constructor($data) {
        // 遍历$data对象
        this.walk($data);
    }
    walk($data) {
        if ($data && typeof $data === "object") {
            Object.keys($data).forEach((key) => {
                this.defineReactive($data, key, $data[key]);
            });
        }
    }
    defineReactive($data, key, value) {
        // 判断value是否是对象,如果是的话,将对象中的属性也转换为getter和setter
        this.walk(value);
        // 保存当前this ——> Observer
        let that = this;
        // 实例化dep
        let dep = new Dep();
        Object.defineProperty($data, key, {
            enumerable: true,
            configurable: true,
            get() {
                Dep.sub && dep.addSub(Dep.sub);
                return value;
            },
            set(newValue) {
                if (newValue !== value) {
                    value = newValue;
                    // 更新视图
                    dep.notify();
                    // 判断新值是不是对象
                    that.walk(value);
                }
            },
        });
    }
}

Compiler.js

class Compiler {
    constructor(vm) {
        // 保存vm实例
        this.vm = vm;
        // 保存el
        this.el = typeof vm.$el === "string" ? document.querySelector(vm.$el) : vm.$el;
        this.compiler(this.el);
    }
    compiler(el) {
        // 获取#app的子节点
        let childNodes = el.childNodes;
        Array.from(childNodes).forEach((node) => {
            // 判断子节点是元素节点还是文本节点
            if (this.isTextNode(node)) {
                this.compilerText(node);
            } else if (this.isElementNode(node)) {
                this.compilerElement(node);
            }
            // 判断node是否有子节点
            if (node.childNodes && node.childNodes.length) {
                this.compiler(node);
            }
        });
    }
    // 判断是否是文本节点(差值表达式)
    isTextNode(node) {
        return node.nodeType === 3;
    }
    // 判断是否是元素节点(指令)
    isElementNode(node) {
        return node.nodeType === 1;
    }
    // 判断是否是vue指令
    isDirective(attrName) {
        // 属性如果以v-开始是vue指令,返回对应的指令v-之后的部分,否则返回false
        return attrName.startsWith("v-") ? attrName.slice(2) : false;
    }
    compilerText(node) {
        // 匹配差值表达式{{ xxx }}
        let reg = /\{\{(.+?)\}\}/;
        if (reg.test(node.textContent)) {
            // 获取匹配到的内容
            let key = RegExp.$1.trim();
            let value = this.vm[key];
            // 将数据替换差值表达式
            node.textContent = node.textContent.replace(reg, value);
            new Watcher(this.vm, key, (newValue) => {
                node.textContent = newValue;
            });
        }
    }
    compilerElement(node) {
        // 获取节点所有属性
        let attrs = node.attributes;
        Array.from(attrs).forEach((attr) => {
            // 判断是否是vue指令
            // 如果是vue指令,则可以得到对应的指令名, v-text ——> text
            let name = this.isDirective(attr.name);
            if (name) {
                // 获取指令对应的数据 v-text="age" ——> age
                let key = attr.value;
                // 调用更新视图方法
                this.update(node, name, key);
            }
        });
    }
    update(node, name, key) {
        // 获取方法名  'text' + 'Update' = 'textUpdate'
        let fn = name + "Update";
        // this[fn]直接调用的this指向,将this指向改为this
        this[fn] && this[fn](node, key);
    }

    textUpdate(node, key) {
        // 替换数据
        node.textContent = this.vm[key];
        new Watcher(this.vm, key, (newValue) => {
            node.textContent = newValue;
        });
    }
    modelUpdate(node, key) {
        node.value = this.vm[key];
        new Watcher(this.vm, key, (newValue) => {
            node.value = newValue;
        });
        // 当输入框输入内容时,更新数据
        node.addEventListener("input", () => {
            this.vm[key] = node.value;
        });
    }
}

Dep.js

class Dep {
    constructor() {
        // 存储订阅者
        this.subs = [];
    }
    // 存储订阅者
    static sub = null;
    // 添加订阅者
    addSub(sub) {
        if (sub && sub.update) {
            this.subs.push(sub);
        }
    }
    // 发送通知
    notify() {
        this.subs.forEach((sub) => {
            sub.update();
        });
    }
}

Watcher.js

class Watcher {
    constructor(vm, key, cb) {
        // 存储vue实例
        this.vm = vm;
        // 存储当前key
        this.key = key;
        // 存储更新视图回调
        this.cb = cb;
        // 将当前this存储到dep中
        Dep.sub = this;
        // 存储旧值,当前key对应的数据
        this.oldValue = this.vm[this.key]; // 此时触发vue.js中的get,把当前订阅者添加到dep中
        // 清空Dep.sub,防止重复添加
        Dep.sub = null;
    }
    // 更新视图
    update() {
        // 当触发更新视图方法时,key对应的数据已经最新的数据
        let newValue = this.vm[this.key];
        if (newValue !== this.oldValue) {
            this.cb && this.cb(newValue);
        }
    }
}