1. 首页

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

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

逆向思维

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

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


<!-- html --> <div id="app">{{name}}</div>

const app = new Vue({ el: '#app', data: { name: 'Fish Chan' } });

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

所以我新建了一个文件 core.js,内容如下:


// 目标:我需要一个 Vue 类,构造函数可以接收一个参 class Vue { constructor(options) { // TODO 编译模板并实现数据双向绑定 } }

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


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

还是和上面一样,我没有写任何实质性的内容,因为我始终坚持一个原则 不写无用的代码,用则写,所以我写代码的习惯是需要用到某个数据了才会把需要的数据传过来。

现在我的 Compile 需要知道从哪里开始编译,于是我们传入了第一个参数 el; 我还需要把模板内容替换成真实的数据,所以又传了第二个参数,携带数据的 vue 实例:


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

Js中文网 – 前端进阶资源教程 www.javascriptC.com,typescript 中文文档
一个帮助开发者成长的社区,你想要的,在这里都能找到

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

编译模板

我们顺着思路继续完成 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); // 编译好的文档碎片放进根节点 } } 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 = []; // 存放订阅者 }//码农进阶题库,每天一道面试题 or Js小知识 https://www.javascriptc.com/interview-tips/ 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); }); } //码农进阶题库,每天一道面试题 or Js小知识 https://www.javascriptc.com/interview-tips/ } 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(); } } }

看下最终的运行图吧:

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

总结

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

作者:树洞一下robot
链接:https://juejin.im/post/5df8ce956fb9a016510da74f

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

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

链接:https://www.javascriptc.com/3185.html

« 纯CSS实现简单骨骼动画
请问如何开发Babel插件?»
Flutter 中文教程资源

相关推荐

QR code