最近利用空闲时间又翻看了一遍Vue的源码,只不过此次不一样的是看了Flow版本的源码。说来惭愧,最先看的第一遍时对Flow不了解,所以阅读的是打包以后的vue文件,你们能够想象这过程的痛苦,没有类型的支持,看代码时摸索了很长时间,因此咱们此次对Vue源码的剖析是Flow版本的源码,也就是从Github上下载下来的源码中src目录下的代码。不过,在分析以前,我想先说说阅读Vue源码所须要的一些知识点,掌握这些知识点以后,相信再阅读源码会较为轻松。javascript
我我的认为要想深刻理解Vue的源码,至少须要如下知识点: html
下面我们一一介绍前端
相信你们都知道,javascript是弱类型的语言,在写代码灰常爽的同时也十分容易犯错误,因此Facebook搞了这么一个类型检查工具,能够加入类型的限制,提升代码质量,举个例子:vue
function sum(a, b) {
return a + b;
}
复制代码
但是这样,咱们若是这么调用这个函数sum('a', 1) 甚至sum(1, [1,2,3])这么调用,执行时会获得一些你想不到的结果,这样编程未免太不稳定了。那咱们看看用了Flow以后的结果:java
function sum(a: number, b:number) {
return a + b;
}
复制代码
咱们能够看到多了一个number的限制,标明对a和b只能传递数字类型的,不然的话用Flow工具检测会报错。其实这里你们可能有疑问,这么写仍是js吗? 浏览器还能认识执行吗?固然不认识了,因此须要翻译或者说编译。其实如今前端技术发展太快了,各类插件层出不穷--Babel、Typescript等等,其实都是将一种更好的写法编译成浏览器认识的javascript代码(咱们之前都是写浏览器认识的javascript代码的)。咱们继续说Flow的事情,在Vue源码中其实出现的Flow语法都比较好懂,好比下面这个函数的定义:node
export function renderList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => VNode
): ?Array<VNode>{
...
}
复制代码
val是any表明能够传入的类型是任何类型, keyOrIndex是string|number类型,表明要不是string类型,要不是number,不能是别的;index?:number这个咱们想一想正则表达式中?的含义---0个或者1个,这里其实意义也是一致的,可是要注意?的位置是在冒号以前仍是冒号以后--由于这两种可能性都有,上面代码中问号是跟在冒号前面,表明index能够不传,可是传的话必定要传入数字类型;若是问号是在冒号后面的话,则表明这个参数必需要传递,可是能够是数字类型也能够是空。这样是否是顿时感受严谨多了?同时,代码意义更明确了。为啥这么说呢? 以前看打包后的vue源码,其中看到观察者模式实现时因为没有类型十分难看懂,可是看了这个Flow版本的源码,感受容易懂。 固然,若是想学习Flow更多的细节, 能够看看下面这个学习文档: Flow学习资料python
Vue中的组件相信你们都使用过,而且组件之中能够有子组件,那么这里就涉及到父子组件了。组件其实初始化过程都是同样的,显然有些方法是能够继承的。Vue代码中是使用原型继承的方式实现父子组件共享初始化代码的。因此,要看懂这里,须要了解js中原型的概念;这里很少谈,只是提供几个学习资料供你们参考: 廖雪峰js教程 js原型理解 1.3 Object.defineProperty 这个方法在js中十分强大,Vue正是使用了它实现了响应式数据功能。咱们先瞄一眼Vue中定义响应式数据的代码:react
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
.....
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
复制代码
其中咱们看到Object.defineProperty这个函数的运用,其中第一个参数表明要设置的对象,第二个参数表明要设置的对象的键值,第三个参数是一个配置对象,对象里面能够设置参数以下: value: 对应key的值,无需多言 configurable:是否能够删除该key或者从新配置该key enumerable:是否能够遍历该key writable:是否能够修改该key get: 获取该key值时调用的函数 set: 设置该key值时调用的函数 咱们经过一个例子来了解一下这些属性:程序员
let x = {}
x['name'] = 'vue'
console.log(Object.getOwnPropertyDescriptor(x,'name'))
Object.getOwnPropertyDescriptor能够获取对象某个key的描述对象,打印结果以下:
{
value: "vue",
writable: true,
enumerable: true,
configurable: true
}
复制代码
从上可知,该key对应的属性咱们能够改写(writable:true),能够从新设置或者删除(configurable: true),同时能够遍历(enumerable:true)。那么让咱们修改一下这些属性,好比configurable,代码以下:正则表达式
Object.defineProperty(x, 'name', {
configurable: false
})
复制代码
执行成功以后,若是你再想删除该属性,好比delete x['name'],你会发现返回为false,即没法删除了。 那enumerable是什么意思呢?来个例子就明白了,代码以下:
let x = {}
x[1] = 2
x[2] = 4
Object.defineProperty(x, 2, {
enumerable: false
})
for(let key in x){
console.log("key:" + key + "|value:" + x[key])
}
复制代码
结果以下: key:1|value:2 为何呢? 由于咱们把2设置为不可遍历了,那么咱们的for循环就取不到了,固然咱们仍是能够用x[2]去取到2对应的值得,只是for循环中取不到而已。这个有什么用呢?Vue源码中Observer类中有下面一行代码: def(value, 'ob', this);
这里def是个工具函数,目的是想给value添加一个key为__ob__,值为this,可是为何不直接 value.ob = this 反而要大费周章呢? 由于程序下面要遍历value对其子内容进行递归设置,若是直接用value.__ob__这种方式,在遍历时又会取到形成,这显然不是本意,因此def函数是利用Object.defineProperty给value添加的属性,同时enumerable设置为false。 至于get和set嘛?这个就更强大了,相似于在获取对象值和设置对象值时加了一个代理,在这个代理函数中能够作的东西你就能够想象了,好比设置值时再通知一下View视图作更新。也来个例子体会一下吧: let x = {} Object.defineProperty(x, 1, { get: function(){ console.log("getter called!") }, set: function(newVal){ console.log("setter called! newVal is:" + newVal) } })
当咱们访问x[1]时便会打印getter called,当咱们设置x[1] = 2时,打印setter called。Vue源码正是经过这种方式实现了访问属性时收集依赖,设置属性时源码有一句dep.notify,里面即是通知视图更新的相关操做。
Vnode,顾名思义,Virtual node,虚拟节点,首先声明,这不是Vue本身独创的概念,其实Github上早就有一个相似的项目:Snabbdom。我我的认为,Vue应该也参考过这个库的实现,由于这个库包含了完整的Vnode以及dom diff算法,甚至实现的具体代码上感受Vue和这个库也是有点相像的。为啥要用Vnode呢?其实缘由主要是原生的dom节点对象太大了,咱们运行一下代码:
let dom = document.createElement('div');
for(let key in dom){
console.log(key)
}
复制代码
打印的结果灰常长!!!说明这个dom对象节点有点重量级,而咱们的html网页常常数以百计个这种dom节点,若是采用以前的Jquery这种方式直接操做dom,性能上确实稍微low一点。因此snabbdom或者Vue中应用了Vnode,Vnode对象啥样呢? 看看Vue源码对Vnode的定义:
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching fnScopeId: ?string; .... } 复制代码
相比之下, Vnode对象的属性确实少了不少;其实光属性少也不见得性能就能高到哪儿去,另外一个方面即是针对新旧Vnode的diff算法了。这里其实有一个现象:其实大多数场景下即使有不少修改,可是若是从宏观角度观看,其实修改的点很少。举个例子: 好比有如下三个dom节点A B C 咱们的操做中依次会改为 B C D 若是采用Jquery的改法,当碰到第一次A改成B时,修改了一次,再碰到B改成C,又修改了一次,再次碰到C改成D,又又修改了一次,显然其实从宏观上看,只须要删除A,而后末尾加上D便可,修改次数获得减小;可是这种优化是有前提的,也就是说可以从宏观角度看才行。之前Jquery的修改方法在碰到第一次修改的时候,须要把A改成B,这时代码尚未执行到后面,它是不可能知道后面的修改的,也就是没法以全局视角看问题。因此从全局看问题的方式就是异步,先把修改放到队列中,而后整成一批去修改,作diff,这个时候从统计学意义上来说确实能够优化性能。这也是为啥Vue源码中出现下述代码的缘由: queueWatcher(this);
函数柯里化是什么鬼呢?其实就是将多参数的函数化做多个部分函数去调用。举个例子:
function getSum(a,b){
return a+b;
}
复制代码
这是个两个参数的函数,能够直接getSum(1,2)调用拿到结果;然而,有时候并不会两个参数都能肯定,只想先传一个值,另一个在其余时间点再传入,那咱们把函数改成:
function getSum(a){
return function(b){
return a+b;
}
}
复制代码
那咱们如何调用这个柯里化以后的函数呢?
let f = getSum(2)
console.log(f(3))
console.log(getSum(2)(3)) //结果同上
复制代码
可见,柯里化的效果即是以前必须同时传入两个参数才能调用成功而如今两个参数能够在不一样时间点传入。那为毛要这么作嘛?Vue源码是这么应用这个特性的,Vue源码中有一个platform目录,专门存放和平台相关的源码(Vue能够在多平台上运行 好比Weex)。那这些源码中确定有些操做是和平台相关的,好比会有些如下伪代码所表示的逻辑: if(平台A){ .... }else if(平台B){ .... }
但是若是这么写会有个小不舒服的地方,那就是其实代码运行时第一次走到这里根据当前平台就已经知道走哪个分支了,而如今这么写必当致使代码再次运行到这里的时候还会进行平台判断,这样总感受会多一些无聊的多余判断,所以Vue解决此问题的方式就是应用了函数柯里化技巧,相似声明了如下一个函数: function ...(平台相关参数){ return function(平台不相关参数){ 处理逻辑 } }
在Vue的patch以及编译环节都应用了这种方式,讲到那部分代码时咱们再细致的看,读者提早先了解一下能够帮助理解Vue的设计。
可能有的读者第一次听到这两个词,实际上这个和js的事件循环机制息息相关。在上面咱们也提到,Vue更新不是数据一改立刻同步更新视图的,这样确定会有性能问题,好比在一个事件处理函数里先this.data = A 而后再this.data=B,若是要渲染两次,想一想都感受很low。Vue源码其实是将更改都放入到队列中,同一个watcher不会重复(不理解这些概念没关系,后面源码会重点介绍),而后异步处理更新逻辑。在实现异步的方式时,js实际提供了两种task--Macrotask与Microtask。两种task有什么区别呢?先从一个例子讲起:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
Promise.resolve().then(function() {
console.log('promise3');
}).then(function() {
console.log('promise4');
});
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
复制代码
以上代码运行结果是什么呢?读者能够思考一下,答案应该是:
script start
script end
promise1
promise2
setTimeout
promise3
promise4
复制代码
简单能够这么理解,js事件循环中有两个队列,一个叫MacroTask,一个MircroTask,看名字就知道Macro是大的,Micro是小的(想一想宏观经济学和微观经济学的翻译)。那么大任务队列跑大任务--好比主流程程序了、事件处理函数了、setTimeout了等等,小任务队列跑小任务,目前读者记住一个就能够--Promise。js老是先从大任务队列拿一个执行,而后再把全部小任务队列所有执行再循环往复。以上面示例程序,首先总体上个这个程序是一个大任务先执行,执行完毕后要执行全部小任务,Promise就是小任务,因此又打印出promise1和promise2,而setTimeout是大任务,因此执行完全部小任务以后,再取一个大任务执行,就是setTimeout,这里面又往小任务队列扔了一个Promise,因此等setTimeout执行完毕以后,又去执行全部小任务队列,因此最后是promise3和promise4。说的有点绕,把上面示例程序拷贝到浏览器执行一下多思考一下就明白了,关键是要知道上面程序自己也是一个大任务。必定要理解了以后再去看Vue源码,不然不会理解Vue中的nextTick函数。 推荐几篇文章吧(我都认真读完了,受益不浅) Macrotask Vs Microtask 理解js中Macrotask和Microtask 阮一峰 Eventloop理解
不少程序员比较惧怕递归,可是递归真的是一种灰常灰常强大的算法。Vue源码中大量使用了递归算法--好比dom diff算法、ast的优化、目标代码的生成等等....不少不少。并且这些递归不只仅是A->A这么简单,大多数源码中的递归是A->B->C...->A等等这种复杂递归调用。好比Vue中经典的dom diff算法:
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined;
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
复制代码
上面代码是比较新旧Vnode节点更新孩子节点的部分源码,调用者是patchVnode函数,咱们发现这部分函数中又会调用会patchVnode,调用链条为:patchVnode->updateChildren->patchVnode。同时,即使没有直接应用递归,在将模板编译成AST(抽象语法树)的过程当中,其使用了栈去模拟了递归的思想,因而可知递归算法的重要性。这也难怪,毕竟无论是真实dom仍是vnode,其实本质都是树状结构,原本就是递归定义的东西。咱们也会单独拿出一篇文章讲讲递归,好比用递归实现一下JSON串的解析。但愿读者注意查看。
这恐怕比递归更让某些程序员蛋疼,可是我相信只要读者认真把Vue这部分代码看懂,绝对比看N遍编译原理的课本更能管用。咱们看看Vue源码这里的实现:
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
复制代码
上述代码首先经过parse函数将template编译为抽象语法树ast,而后对ast进行代码优化,最后生成render函数。其实这个过程就是翻译,好比gcc把c语言翻译为汇编、又好比Babel把ES6翻译为ES5等等,这里面的流程十分都是十分地类似。Vue也玩了这么一把,把模板html编译为render函数,什么意思呢?
<li v-for="record in commits">
<span class="date">{{record.commit.author.date}}</span>
</li>
复制代码
好比上面的html,你以为浏览器会认识嘛?显然v-for不是html原生的属性,上述代码若是直接在浏览器运行,你会发现{{record.commit.author.date}}就直接展现出来了,v-for也没有起做用,固然仍是会出现html里面(毕竟html容错性很高的);可是通过Vue的编译系统一编译生成一些函数,这些函数一执行就是浏览器认识的html元素了,神奇吧? 其实仅仅是应用了编译原理课本的部分知识罢了,这部分咱们后面会灰常灰常详细的介绍源码,只要跟着看下来,一定会对编译过程有所理解。如今能够这么简单理解一下AST(抽象语法树),好比java能够写一个if判断,C语言也能够写,js、python等等也能够(以下所示): java: if(x > 5){ .... }
python: if x>5: ....
虽然从语法形式上写法不太一致,可是抽象出共同点其实都是一个if语句跟着一个x>5 的条件, 综上,Vue源码其实代码行数并非不少,可是其简约凝练的风格深深吸引了我。我会重点分析Vue源码中观察者模式的实现、Vnode以及dom diff算法的实现以及模板编译为render函数的实现。这三者我感受就是Vue源码中最精彩的地方,但愿你我均可以从中汲取营养,不断提升!