vue底层原理之实现简易版本vue框架

前言

vue框架实现了数据的双向绑定,即Data和Dom之间的双向通信,这两者之间的通信则需要一个Directive来连接,即:
在这里插入图片描述
表现得更复杂一点:
在这里插入图片描述

模块叙述

下面我结合一个具体的代码案例来介绍各个模块的功能以及最终的执行流程。

Observer/Dep

Observer的作用为对传入的data设置了 getter 和 setter即为一个可以获取和修改的观察者。
Dep 存在意义就是,他通过一个记录了 Watcher 和 Observer 之间的依赖关系,是二者的一个桥梁。

observer.js代码:

class Observer {
    constructor(data) {
        this.data = data;
        this.walk(data);
    }

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

    defineReactive = (data, key, val) => {
        const dep = new Dep();
        observe(val);
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: () => {
                if (Dep.target) {
                    dep.addSub(Dep.target);
                }
                return val;
            },
            set: (newVal) => {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                dep.notify();
            }
        });
    }
}

function observe(value) {
    if (!value || typeof value !== 'object') {
        return;
    }
    return new Observer(value);
};
class Dep {
    constructor() {
        this.subs = [];
    }

    addSub = (sub) => {
        this.subs.push(sub);
    }

    notify = () => {
        this.subs.forEach((sub) => {
            sub.update();
        });
    }
}

Dep.target = null;

详细来说,首先Observer中通过创建Dep对象并在其getter和setter中绑定与Dep的关系,而Watch中通过全局属性Dep.target来绑定与Dep的关系,当Observer触发setter时就能直接触发Watch中的update方法。

Watch

Watch的作用就是当观察到Observer中的数据变化时会执行Directive里给Watch传递的回调函数。
watch.js代码:

class Watcher {
    constructor(vm, exp, cb) {
        this.cb = cb;
        this.vm = vm;
        this.exp = exp;
        this.get();  // 将自己添加到订阅器的操作
    }

    update = () => {
        this.run();
    }

    run = () => {
        const value = this.vm.data[this.exp];
        const oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb(value);
        }
    }

    get = () => {
        Dep.target = this;  // 缓存自己
        this.value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        Dep.target = null;  // 释放自己
    }
}

Directive(Compile)

Compile是一个简化版本的Directive,Compile的作用就是解析传入的Data并且与之绑定相应的Watch。
compile.js代码:

class Compile {
    constructor(el, vm) {
        this.vm = vm;
        this.el = document.querySelector(el);
        this.fragment = null;
        this.init();
    }

    init = () => {
        if (this.el) {
            this.fragment = this.nodeToFragment(this.el);
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
        } else {
            console.log('Dom元素不存在');
        }
    }

    nodeToFragment = () => {
        const fragment = document.createDocumentFragment();
        let child = this.el.firstChild;
        while (child) {
            // 将Dom元素移入fragment中
            fragment.appendChild(child);
            child = this.el.firstChild;
        }
        return fragment;
    }

    compileElement = (el) => {
        const childNodes = el.childNodes;
        childNodes.forEach((node) => {
            const reg = /\{\{(.*)\}\}/;
            const text = node.textContent;
            if (this.isElementNode(node)) {
                this.compile(node);
            } else if (this.isTextNode(node) && reg.test(text)) {
                this.compileText(node, reg.exec(text)[1]);
            }

            if (node.childNodes && node.childNodes.length) {
                this.compileElement(node);
            }
        });
    }

    compile = (node) => {
        const nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach((attr) => {
            const attrName = attr.name;
            if (this.isDirective(attrName)) {
                const exp = attr.value;
                const dir = attrName.substring(2);
                if (this.isEventDirective(dir)) { // 事件指令
                    this.compileEvent(node, exp, dir);
                } else { // v-model 指令
                    this.compileModel(node, exp);
                }
                node.removeAttribute(attrName);
            }
        });
    }

    compileText = (node, exp) => {
        const initText = this.vm[exp];
        this.updateText(node, initText);
        new Watcher(this.vm, exp, (value) => {
            this.updateText(node, value);
        });
    }

    compileEvent = (node, exp, dir) => {
        const eventType = dir.split(':')[1];
        const cb = this.vm.methods && this.vm.methods[exp];
        if (eventType && cb) {
            node.addEventListener(eventType, cb, false);
        }
    }

    compileModel = (node, exp) => {
        const val = this.vm[exp];
        this.modelUpdater(node, val);
        new Watcher(this.vm, exp, (value) => {
            this.modelUpdater(node, value);
        });
        node.addEventListener('input', (e) => {
            const newValue = e.target.value;
            if (val === newValue) {
                return;
            }
            this.vm[exp] = newValue;
            val = newValue;
        });
    }

    updateText = (node, value) => {
        node.textContent = typeof value == 'undefined' ? '' : value;
    }

    modelUpdater = (node, value) => {
        node.value = typeof value == 'undefined' ? '' : value;
    }

    isDirective = (attr) => attr.indexOf('v-') == 0

    isEventDirective = (dir) => dir.indexOf('on:') === 0

    isElementNode = (node) => node.nodeType == 1

    isTextNode = (node) => node.nodeType == 3
}

入口文件

index.js:

class SelfVue {
    constructor(options) {
        this.data = options.data;
        this.methods = options.methods;
        Object.keys(this.data).forEach((key) => {
            this.proxyKeys(key);
        });
        observe(this.data);
        new Compile(options.el, this);
        options.mounted.call(this); // 所有事情处理好后执行mounted函数
    }

    proxyKeys = (key) => {
        Object.defineProperty(this, key, {
            enumerable: false,
            configurable: true,
            get: () => this.data[key],
            set: (newVal) => this.data[key] = newVal
        });
    }
}

index.html:

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

<head>
    <meta charset="UTF-8">
    <title>self-vue</title>
</head>
<style>
    #app {
        text-align: center;
    }
</style>

<body>
    <div id="app">
        <h2>{{title}}</h2>
        <input v-model="name">
        <h1>{{name}}</h1>
        <button v-on:click="clickMe">click me!</button>
    </div>
</body>
<script src="js/observer.js"></script>
<script src="js/watcher.js"></script>
<script src="js/compile.js"></script>
<script src="js/index.js"></script>
<script type="text/javascript">

    new SelfVue({
        el: '#app',
        data: {
            title: 'hello world',
            name: 'canfoo'
        },
        methods: {
            clickMe: () => {
                this.title = 'hello world';
            }
        },
        mounted: function () {
            setTimeout(() => {
                this.title = '你好';
            }, 1000);
        }
    });

</script>

</html>

流程概述

以上就是此案例的全部代码,其中h1里面的内容会实时与input框内的内容保持双向一致,这里我详细描述此过程执行流程。
初始化时在SelfVue里为这两个Data分别设置了setter和getter,然后在Compile里分别创建了Watch。当用户改变input里的内容时,首先会由input的data触发其Observer里的setter方法,然后由Dep触发Watch里的Compile传入的回调函数(注意,这里Dep里面有两个Watch一个是input一个是h1),从而改变input的Dom,紧接着又会触发第二个Watch里的Compile传入的回调函数,进而改变了h1的Dom。

结语

到此,代码和流程都已介绍完毕,有兴趣的朋友可以自己进行实现,或者也可以直接用此代码进行运行调试。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值