前言
彻底摆脱秋招之后,我也来到了公司实习,我们主要使用的框架是Vue.js,如果自然而然地,我需要学习这个框架。平心而论,vue的api数量可以说是angular的1/10不到,虽然简洁,但内部实现并不简单。
随便下个断点冲进vue源码之处,都能感受到里面结构的复杂,对于我等菜鸟来说实在是无法多待一会儿的。
所以,本着面对复杂问题先将其简单化的思维方式,我和另一位朋友决定先从一个将Vue核心api实现的高仿库摸透,之后再来啃Vue源码。
为什么选择Moon
市面上有很多MVVM库,Vue也有早期版本在github上供人研究,为什么我们选择了Moon呢?
原因有三:
- Moon源码只有2000多行,却实现了实例属性(data、computed、methods、template)、render、指令、组件以及组件通信、生命钩子、slot等大部分Vue核心功能,在各大MVVM库中算是研究性价比最高的(目前为止我能找到的)。
- Moon的作者异常活跃,我们一旦有啥问题就可以和他邮件交流。
- Moon相比Vue的早期版本也有它独到的思想,作者本身实力也很厉害。比如他针对静态HTML元素渲染做的优化
研究源码一般而言有两种方法:自顶向下和自底向上。
自顶向下就是先从最抽象的层面上观察源码,从它的结构、设计着手,再一步步深入到各个代码模块->子程序->具体语句。
自底向上就是特意选择一个地方打一个断点,然后运行代码,通过一步步调试观察它所经过的调用栈和所有的中间变量。
我选择综合两种方法来研究源码:先自顶向下把它的代码结构摸清楚,再自底向上打断点一步步照亮黑暗区域。和我们平时打游戏需要一个大地图提供全局观,然后自己探索其中的黑暗处道理相似。
本系列文章于2017.12.1开始写,作者打包的最新版本是v0.11.0。
Moon源码整体结构
为了方便起见,我绘制了一张图(建议大家下载下来看):
这里我简单把各个文件夹(标了颜色的方块)看成是类,然后实现了看成接口的各个代码文件的功能。
每个代码文件里有若干个子程序,没有写成函数的我就假装它们是一个函数的内容,并且在这个我造的函数名前面打了星号,虚线所指向的方块是它们实际做的事情。
打开源码的package.json,可以看出作者是使用gulp打包构建他的代码的。而整块代码被他分割成了六个文件夹:
- compiler---编译模板到dom树的各个函数。
- util---通用工具、dom工具、vdom工具函数。
- observer---观察者实例和相关的函数。
- directives---处理指令的函数。
- instance---Moon实例上存在的函数。
- global---Moon对象上的静态属性和方法。
最后通过在index.js里逐个引入,交给wrapper.js(还有gulp)打包成最终版本。
//index.js
"use strict";
/* ======= Global Variables ======= */
let directives = {};
let specialDirectives = {};
let components = {};
let eventModifiersCode = {
stop: 'event.stopPropagation();',
prevent: 'event.preventDefault();',
ctrl: 'if(event.ctrlKey === false) {return null;};',
shift: 'if(event.shiftKey === false) {return null;};',
alt: 'if(event.altKey === false) {return null;};',
enter: 'if(event.keyCode !== 13) {return null;};'
};
let eventModifiers = {};
/* ======= Observer ======= */
//=require observer/methods.js
//=require observer/computed.js
//=require observer/observer.js
//=require util/util.js
//=require util/dom.js
//=require util/vdom.js
/* ======= Compiler ======= */
//=require compiler/template.js
//=require compiler/lexer.js
//=require compiler/parser.js
//=require compiler/generator.js
//=require compiler/compiler.js
function Moon(options) {
//省略,这里不是这篇文章的重点
}
//=require instance/methods.js
//=require global/api.js
//=require directives/default.js
//wrapper.js
(function(root, factory) {
/* ======= Global Moon ======= */
(typeof module === "object" && module.exports) ? module.exports = factory() : root.Moon = factory();
}(this, function() {
//=require ../dist/moon.js
return Moon;
}));
一个Moon实例的一生
有了全局观,我们就从探究一个new一个的Moon过程开始,看看它究竟经历了些什么?:
<div id="app">
{{yf}}
</div>
<script src="./moon.js"></script>
<script>
debugger
const app = new Moon({
el: "#app",
data: {
yf: "云峰"
}
})
</script>
-
很好,我们进来了:
preview-1.jpg -
嗯,不出所料,首先进的是Moon构造函数,毕竟new了一下。
preview-2.jpg -
我们没有定义methods,所以跳过了initMethods,不过没关系,以后会有机会进去的~
preview-3.jpg -
嘿!我们new了一个Observer对象,它肯定是观察当前这个实例变化的!
preview-4.jpg -
initComputed同样被我们跳过去了,不过没关系,我们要进init了!!
preview-5.jpg -
init里貌似做了一些事情,不过紧接着就进入了mount。
preview-6.jpg -
mount貌似也做了一些事情,我感觉快要迷路了,不过这时候一个compile让我好奇心大增,它竟然把模板字符串转成了一个匿名函数!做完这些它就进了build。
preview-7.jpg -
build一上来就把上一步生成的函数执行了!并且按作者的注释来看的话还得到了virtual node,我们发现光得到还不行,后续还要和node进行patch一下。
preview-8.jpg -
在patch里我们因为virtual node不是dom node类型跳转到了一个hydrate的子程序里
preview-9.jpg
- hydrate的子程序在作者注释中被解释为Hydrates Node and a VNode,也就是把人为生成的vnode混入到实际存在于dom树中的node里去。
在混入完成后我们便一步步出栈,回到了起点。
也就是说,我们的Moon实例经历了从init初始化->mount挂载->build建立的过程,期间实例化了一个Observer观察者,对模板字符串进行了编译,并执行得到vnode,最后作用于dom树中的node来改变最终渲染结果。(template->vnode->node)
那这些过程中具体细节是什么样的呢?我准备在之后的文章中从下面几个角度分析:
- render函数从一段code->html的过程
- compiler从html->code的过程
- 数据变更检测
- 指令
- 组件
...