第872期学习Vue.js源码

Posted 前端早读课

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第872期学习Vue.js源码相关的知识,希望对你有一定的参考价值。

前言

3月12号又周末了,今日早读文章由臭豆腐前端@Fedora带来分享。


正文从这开始~


Vue.js是目前MVVM框架中比较流行的一种,在之前的项目中也用过Vue+Vuex。在使用的过程中,也是一边看文档一边进行开发。对Vue的了解也仅限官方文档和一些社区上回答,很大程度上是知其然而不知其所以然。所以在项目告一段落之后,决定从Vue的源码入手,学习一下其内部架构。


Vue源码目录


目标Vue.js版本是2.0.5 /src目录


对应的目录分别为 

/compiler、/core、/entries、/platforms、/server、/sfc、/shared


  • /compiler目录是编译模版;

  • /core目录是Vue.js的核心(也是后面的重点);

  • /entries目录是生产打包的入口;

  • /platforms目录是针对核心模块的’平台’模块,platforms目录下暂时只有web目录(在最新的开发目录里面已经有weex目录了)。web目录下有对应的/compiler、/runtime、/server、/util目录;

  • /server目录是处理服务端渲染;

  • /sfc目录处理单文件.vue;

  • /shared目录提供全局用到的工具函数。 


在刚学习源码时,会对类似

【第872期】学习Vue.js源码


这样函数申明或者变量申明感动疑惑的,可以先了解一下flow。


独立构建&&运行时构建


Vue.js从2.0以后开始出现两个不同的构建版本,详情可以查看官网文档。一开始说到这个,是因为从Vue的build命令里面可以看到有7个build版本,这对学习其源码非常有帮助。

【第872期】学习Vue.js源码


这里,关注的重点是runtime的版本。


Vue.js结构


通过上面的分析,可以看到Vue.js的组成是由core+对应的‘平台’补充代码构成(独立构建和运行时构建只是platforms下web平台的两种选择)。

【第872期】学习Vue.js源码


core目录下面对应的components、global-api、instance、observer、util、vdom模块。


Vue.js的目标是通过尽可能简单的API实现响应的数据绑定和组合的视图组件


Vue2.0在保持实现‘响应的数据绑定’的同时又引入了’virtual-dom’,那么它是怎么实现的呢?


响应的数据绑定


Vue.js实现数据绑定的关键是Object.defineProperty(obj, prop, descriptor),这也是为什么2.0不支持IE8原因之一,IE8下无法实现defineProperty的腻子脚本。当然实现类似功能的现代语法还有Object.observe(已经废弃)和Proxy。 

Vue源码对此实现的逻辑在core/observer目录下。 关注三个类class Observer,class Dep,class Watcher 


class Observer在core/observer/index.js中

【第872期】学习Vue.js源码


Observer类实例是用来附加到每个被观察的对象(后面称之为响应式对象)上的。普通对象通常是不会变成‘响应式对象’的。经过defineReactive函数的调用,才会将传入的普通对象变成‘响应式对象’。而defineReactive就是利用了Object.defineProperty这个方法。defineReactive源码如下:

【第872期】学习Vue.js源码


Object.defineProperty(obj, prop, descriptor)中的第三个参数就是对象描述符。对象描述符的传入是有要求的,分为赋值描述符合存取描述符。而每次传入的参数只能是其中之一,很显然在defineReactive中传入的就是存取描述符,在传入的存取描述符对象中有get,set方法。set方法会实例化一个Observer,get方法会关联到一个class Dep的实例。但是仔细看get方法,发现只有在Dep.target值为true的时候才会发生关联。所以,接下来分析一下class Dep的源码。

【第872期】学习Vue.js源码


class Dep就是连接class Observer和class Watch类的介质。因为在set方法里面最终会调用dep.notify()方法。class Dep类中的notify方法会使这个dep实例下所有的watch数组更新一次。class Watch类的update方法会调用对应的回调方法,进行对应的更新。同时在前面提到的get方法关联class Dep实例时,是在Dep.target为true的时候才会执行。通过源码可以看到Dep.target的初始值是null,也就是默认是不会执行关联的。源码上对此做了注释,可以看到Dep.target是被赋予全局性质,用来保证同一时刻只有一个Watcher实例在被‘关联’(源码注释的地方是’evaluated’)。而激活Dep.target这个属性的是函数pushTarget。pushTarget函数就在class Watcher中调用了。class Watcher的源码如下:

【第872期】学习Vue.js源码


到这里,class Observer、class Dep和class Watch三个类的关系就清楚了。这也是Vue中实现数据绑定用到的观察者模式的体现。


Virtual-dom


React的大热,是因为其带来了’Virtual-dom’和数据驱动视图的理念(尽管很多人觉得后者更重要)。这里并不想比较‘Virtual-dom’和原生的DOM操作谁快谁慢的问题(事实上在dom结构改动很多的情况下,原生DOM操作比较快。。。),仅仅是理解一下Virtual-dom。 


‘Virtual-dom’是一系列的模块集合,用来提供声明式的DOM渲染。来看一个简单的DOM片段

【第872期】学习Vue.js源码


对DOM片段结构抽象一下:一个根节点div,三个元素子节点span(对应的内部文本节点)。然后用javascript对象表示:


进而扩展到整个html页面的结构。整个HTML页面结构其实可以用一个JavaScript对象表示,通过这个抽象,对dom对象的修改就会影响到HTML页面的结构。所以在改变HTML结构的时候,我们仅仅是修改JavaScript对象。相对以前修改HTML页面结构式通过直接修改DOM元素,现在变成修改对应的JavaScript对象。Vue.js在对DOM的抽象做的更细致,具体代码可以看core/vdom/create-element.js。


在实现了对HTML结构的映射后,接下来就是‘Virtual-dom’的重点,如何比较两个不同HTML结构树的对象–diff算法。diff算法比较的就是两颗’树’的差异。而传统的’树’比较是一个时间复杂度为O(n^3),这个效率明显是不够的。而HTML的结构树的改变不同于传统的’树’。HTML结构树的改变,很少会出现跨越不同层级的改变。基于这个实际上的差异化改变,‘Virtual-dom’的diff算法的时间复杂度是O(n)。有兴趣研究diff算法的,可以看下这里。


在解决了diff算法核心问题后,就要把’新HTML结构树’相对’老HTML结构树’的差异应用到’老HTML结构树’上–‘patch’。Vue.js在’patch’的解决方案上参考了开源项目Snabbdom。源码篇幅有点长,也有些复杂,想深入了解的可以配合开源的项目一起分析。


至此,‘Virtual-dom’大致的实现逻辑也清楚了。Vue.js的源码在架构组织上还有很多可以学习的,比如区分’核心模块’和’平台模块’。另外内部实现的’keep-alive’组件也是值得关注的地方,更多了解请到这里。以上只是很’粗浅’的学习,不对的地方希望大家指出来(认真脸.jpg)。


参考资料

  • Vue官网文档

  • What is Virtual Dom

  • The difference between Virtual DOM and DOM


最后,你可能还需要:




关于本文

以上是关于第872期学习Vue.js源码的主要内容,如果未能解决你的问题,请参考以下文章

[第18期] Vue.js最佳实践(五招让你成为Vue.js大师)

一起走进Vue.js——Vue.js的全局API

第784期Vue.js 和 MVVM 小细节

[第7期] 基于 Nuxt 的 Vue.js 服务端渲染实践

前端每周清单第 35 期:Vue.js 2.5 发布微前端概念详解

第53期Vue.js异步更新DOM策略及nextTick