我是如何写 Vue 源码的:思路篇

看了那么多篇文章,我发现不少文章只会告诉你他是怎么写的而不会告诉你他是怎么想的。而我认为,可否写出代码最主要的是如何构思的?为何有的人能把代码写的很优雅而有的人写的却很臃肿?为何有的人能一直写下去而有的人却容易“中道崩殂”?我但愿你在本篇文章有所收获,谢谢你的阅读!javascript

逆向思惟

我不知道你有没有试图寻找过 Vue 源码的入口,固然,这对熟悉代码审计的老手来讲很容易。可是若是你并无代码审计的任何经验,我想也你会头疼。固然,我这里并不讲如何进行代码审计。我要告诉你的是如何在不阅读源码的状况下去实现相似的功能,我称之为逆向思惟html

固然在你要模仿一个东西的时候你首先要熟悉它,并且还要有十分清晰的思路。下面我就谈谈我是如何用最简单的思路去实现 Vue 数据双向绑定的:vue

<!-- html -->
<div id="app">{{name}}</div>
复制代码
const app = new Vue({
    el: '#app',
    
    data: {
        name: 'Fish Chan'
    }
});
复制代码

这是最简单的 Vue 代码,我相信只要是学过 Vue 都能看懂。上面的代码 new 了一个 Vue 实例,且传了一个参数(对象类型)。java

因此我新建了一个文件 core.js,内容以下:node

// 目标:我须要一个 Vue 类,构造函数能够接收一个参
class Vue {
    constructor(options) {
        // TODO 编译模板并实现数据双向绑定
    }
}
复制代码

就这样,咱们就有了一个基础的 Vue 类,它没有作任何事情。接下来,咱们继续。替换模板里面的内容属于_编译_,因此我又建立了一个文件叫 compile.js(这里模拟了 Java 的思惟,一个类一个文件,这样每一个文件都很小巧,也很清楚每一个文件是干吗的):设计模式

// 目标:编译模板,替换掉模板内容: {{name}}
class Compile {
    constructor() {
        // TODO 编译模板
    }
}
复制代码

仍是和上面同样,我没有写任何实质性的内容,由于我始终坚持一个原则 不写无用的代码,用则写,因此我写代码的习惯是须要用到某个数据了才会把须要的数据传过来。app

如今个人 Compile 须要知道从哪里开始编译,因而咱们传入了第一个参数 el; 我还须要把模板内容替换成真实的数据,因此又传了第二个参数,携带数据的 vue 实例:函数

class Compile {
    constructor(el, vue) {
        this.$el = document.querySelector(el);
        this.$vue = vue;
    }
    
    compileText() {
        // TODO 编译模板,找到 {{name}} 并替换成真实数据
    }
}
复制代码

为了一步步的牵引思路,你会发现我在代码中习惯用 TODO 去写好下一步,固然这在你思路十分清晰的时候是不必这样作的,除非你临时有事须要离开你的电脑桌。ui

编译模板

咱们顺着思路继续完成 compile.jsthis

class Compile {
    constructor(el, vue) {
        this.$el = document.querySelector(el);
        this.$vue = vue;
    }
    
    compileText() {
        const reg = /\{\{(.*)\}\}/; // 用于匹配 {{name}} 的正则
        
        const fragment = this.node2Fragment(this.$el); // 把操做 DOM 改为操做文档碎片
        const node = fragment.childNodes[0]; // 取节点_对象_
        
        if (reg.test(node.textContent)) {
            let matchedName = RegExp.$1;
            node.textContent = this.$vue._data[matchedName]; // 替换数据
            this.$el.appendChild(node); // 编译好的文档碎片放进根节点
        }
    }
    
    node2Fragment(node) {
        const fragment = document.createDocumentFragment();
        fragment.appendChild(node.firstChild);
        return fragment;
    }
}
复制代码

其实,写到这里咱们就已经完成了模板编译的部分。下面咱们只须要在 core.js 里面调用它就行了:

class Vue {
    constructor(options) {
    	let data = this._data = options.data;
    	
    	const _complie = new Compile(options.el, this);
    	_complie.compileText();
    }
}
复制代码

先运行一下看看:

成功编译模板

数据双向绑定

嗯,编译模板已经实现了,如今开始实现数据双向绑定,在这以前我但愿你先去了解下设计模式之观察者模式Object.defineProperty

新建一个 Observer 类,用于数据双向绑定:

class Observer {
    constructor(data) {
        this.defineReactive(data);
    }
    
    defineReactive(data) {
        Object.keys(data).forEach(key => {
            let val = data[key];
            Object.defineProperty(data, key, {
                get() {
                    // TODO 监听数据
                    return val;
                },
                set(newVal) {
                    val = newVal;
                    // TODO 更新视图
                }
            })
        });
    }
}
复制代码

接下来就是观察者模式的实现了,基本上是一个固定的模板(我认为设计模式是很好学的东西,就比如数学公式同样):

class Dep {
    constructor(vue) {
        this.subs = []; // 存放订阅者
    }
    
    addSubscribe(subscribe) {
        this.subs.push(subscribe);
    }
    
    notify() {
        let length = this.subs.length;
        while(length--)
        {
            this.subs[length].update();
        }
    }
}
复制代码

接下来是订阅者Watcher,订阅者要作的事情就是执行某个事件:

class Watcher {
    constructor(vue, exp, callback) {
        this.vue = vue;
        this.exp = exp;
        this.callback = callback;
        this.value = this.get();
    }
    
    get() {
        Dep.target = this;
        let value = this.vue._data[this.exp];
        Dep.target = null;
        return value;
    }
    
    update() {
        this.value = this.get();
        this.callback.call(this.vue, this.value); // 将新的数据传回,用于更新视图;这里保证了 this 指向 vue
    }
}
复制代码

就这样,照搬了观察者模式和利用Object.defineProperty就简单实现了一个数据双向绑定。

完整代码

下面把全部的 TODO 部分进行代码替换,咱们就实现了全部的功能:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="author" content="Fish Chan">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vue-demo</title>
    <script src="./Dep.js"></script>
    <script src="./Watch.js"></script>
    <script src="./Compile.js"></script>
    <script src="./Observer.js"></script>
    <script src="./core.js"></script>
</head>
<body>
    <div id="app">{{name}}</div>

    <script> const app = new Vue({ el: '#app', data: { name: 'Fish Chan' } }); </script>
</body>
</html>
复制代码

core.js

class Vue {
    constructor(options) {
        let data = this._data = options.data;

        new Observer(data);

        const _complie = new Compile(options.el, this);

        _complie.compileText();
    }
}
复制代码

Observer.js

class Observer {
    constructor(data) {
        this.defineReactive(data);
    }
    
    defineReactive(data) {
        let dep = new Dep();
        Object.keys(data).forEach(key => {
            let val = data[key];
            Object.defineProperty(data, key, {
                get() {
                    Dep.target && dep.addSubscribe(Dep.target);
                    return val;
                },
                set(newVal) {
                    val = newVal;
                    dep.notify();
                }
            })
        });
    }
}
复制代码

Compile.js

class Compile {
    constructor(el, vue) {
        this.$el = document.querySelector(el);
        this.$vue = vue;
    }
    
    compileText() {
        const reg = /\{\{(.*)\}\}/; // 用于匹配 {{name}} 的正则
        
        const fragment = this.node2Fragment(this.$el); // 把操做 DOM 改为操做文档碎片
        const node = fragment.childNodes[0];
        
        if (reg.test(node.textContent)) {
            let matchedName = RegExp.$1;
            node.textContent = this.$vue._data[matchedName]; // 替换数据
            this.$el.appendChild(node); // 编译好的文档碎片放进根节点

            new Watcher(this.$vue, matchedName, function(value) {
                node.textContent = value;
                console.log(node.textContent);
            });
        }
    }
    
    node2Fragment(node) {
        const fragment = document.createDocumentFragment();
        fragment.appendChild(node.firstChild);
        return fragment;
    }
}
复制代码

Watch.js

class Watcher {
    constructor(vue, exp, callback) {
        this.vue = vue;
        this.exp = exp;
        this.callback = callback;
        this.value = this.get();
    }
    
    get() {
        Dep.target = this;
        let value = this.vue._data[this.exp];
        Dep.target = null;
        return value;
    }
    
    update() {
        this.value = this.get();
        this.callback.call(this.vue, this.value); // 将新的数据传回,用于更新视图
    }
}
复制代码

Dep.js

class Dep {
    constructor(vue) {
        this.subs = []; // 存放订阅者
    }
    
    addSubscribe(subscribe) {
        this.subs.push(subscribe);
    }
    
    notify() {
        let length = this.subs.length;
        while(length--)
        {
            this.subs[length].update();
        }
    }
}
复制代码

看下最终的运行图吧:

总结

除了基本功扎实外,写代码必定要理清思路。思路是否清晰可能决定了你可否写出一份优雅的代码,也可能决定你是否能从始至终的完成一个项目。

相关文章
相关标签/搜索