Vue2.1.7源码学习
原本文章的名字叫做《源码解析》,不过后来想想,还是用“源码学习”来的合适一点,在没有彻底掌握源码中的每一个字母之前,“解析”就有点标题党了。建议在看这篇文章之前,最好打开2.1.7的源码对照着看,这样可能更容易理解。另外本人水平有限,文中有错误或不妥的地方望大家多多指正共同成长。
补充:Vue 2.2 刚刚发布,作为一个系列文章的第一篇,本篇文章主要从Vue代码的组织,Vue构造函数的还原,原型的设计,以及参数选项的处理和已经被写烂了的数据绑定与如何使用 Virtual DOM 更新视图入手。从整体的大方向观察框架,这么看来 V2.1.7
对于理解 V2.2
的代码不会有太大的影响。该系列文章的后续文章,都会从最新的源码入手,并对改动的地方做相应的提示。
很久之前写过一篇文章:JavaScript实现MVVM之我就是想监测一个普通对象的变化,文章开头提到了我写博客的风格,还是那句话,只写努力让小白,甚至是小学生都能看明白的文章。这不免会导致对于某些同学来说这篇文章有些墨迹,所以大家根据自己的喜好,可以详细的看,也可以跳跃着看。
一、从了解一个开源项目入手
要看一个项目的源码,不要一上来就看,先去了解一下项目本身的元数据和依赖,除此之外最好也了解一下 PR 规则,Issue Reporting 规则等等。特别是“前端”开源项目,我们在看源码之前第一个想到的应该是:package.json
文件。
在 package.json
文件中,我们最应该关注的就是 scripts
字段和 devDependencies
以及 dependencies
字段,通过 scripts
字段我们可以知道项目中定义的脚本命令,通过 devDependencies
和 dependencies
字段我们可以了解项目的依赖情况。
了解了这些之后,如果有依赖我们就 npm install
安装依赖就ok了。
除了 package.json
之外,我们还要阅读项目的贡献规则文档,了解如何开始,一个好的开源项目肯定会包含这部分内容的,Vue也不例外:https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md,在这个文档里说明了一些行为准则,PR指南,Issue Reporting 指南,Development Setup 以及 项目结构。通过阅读这些内容,我们可以了解项目如何开始,如何开发以及目录的说明,下面是对重要目录和文件的简单介绍,这些内容你都可以去自己阅读获取:
1 |
├── build --------------------------------- 构建相关的文件,一般情况下我们不需要动 |
大概了解了重要目录和文件之后,我们就可以查看 Development Setup 中的常用命令部分,来了解如何开始这个项目了,我们可以看到这样的介绍:
1 |
# watch and auto re-build dist/vue.js |
现在,我们只需要运行 npm run dev
即可监测文件变化并自动重新构建输出 dist/vue.js,然后运行 npm run dev:test
来测试。不过为了方便,我会在 examples
目录新建一个例子,然后引用 dist/vue.js 这样,我们可以直接拿这个例子一边改Vue源码一边看自己写的代码想怎么玩怎么玩。
二、看源码的小提示
在真正步入源码世界之前,我想简单说一说看源码的技巧:
注重大体框架,从宏观到微观
当你看一个项目代码的时候,最好是能找到一条主线,先把大体流程结构摸清楚,再深入到细节,逐项击破,拿Vue举个栗子:假如你已经知道Vue中数据状态改变后会采用virtual DOM的方式更新DOM,这个时候,如果你不了解virtual DOM,那么听我一句“暂且不要去研究内部具体实现,因为这会是你丧失主线”,而你仅仅需要知道virtual DOM分为三个步骤:
一、createElement(): 用 JavaScript对象(虚拟树) 描述 真实DOM对象(真实树)
二、diff(oldNode, newNode) : 对比新旧两个虚拟树的区别,收集差异
三、patch() : 将差异应用到真实DOM树
有的时候 第二步 可能与 第三步 合并成一步(Vue 中的patch就是这样),除此之外,还比如 src/compiler/codegen
内的代码,可能你不知道他写了什么,直接去看它会让你很痛苦,但是你只需要知道 codegen 是用来将抽象语法树(AST)生成render函数的就OK了,也就是生成类似下面这样的代码:
1 |
function anonymous() { |
当我们知道了一个东西存在,且知道它存在的目的,那么我们就很容易抓住这条主线,这个系列的第一篇文章就是围绕大体主线展开的。了解大体之后,我们就知道了每部分内容都是做什么的,比如 codegen 是生成类似上面贴出的代码所示的函数的,那么再去看codegen下的代码时,目的性就会更强,就更容易理解。
三、Vue 的构造函数是什么样的
balabala一大堆,开始来干货吧。我们要做的第一件事就是搞清楚 Vue 构造函数到底是什么样子的。
我们知道,我们要使用 new
操作符来调用 Vue
,那么也就是说 Vue
应该是一个构造函数,所以我们第一件要做的事儿就是把构造函数先扒的一清二楚,如何寻找 Vue
构造函数呢?当然是从 entry 开始啦,还记的我们运行 npm run dev
命令后,会输出 dist/vue.js
吗,那么我们就去看看 npm run dev
干了什么:
1 |
"dev": "TARGET=web-full-dev rollup -w -c build/config.js", |
首先将 TARGET 得值设置为 ‘web-full-dev’,然后,然后,然后如果你不了解 rollup 就应该简单去看一下啦……,简单的说就是一个JavaScript模块打包器,你可以把它简单的理解为和 webpack 一样,只不过它有他的优势,比如 Tree-shaking (webpack2也有),但同样,在某些场景它也有他的劣势。。。废话不多说,其中 -w
就是watch,-c
就是指定配置文件为 build/config.js
,我们打开这个配置文件看一看:
1 |
// 引入依赖,定义 banner |
上面的代码是简化过的,当我们运行 npm run dev
的时候 process.env.TARGET
的值等于 ‘web-full-dev’,所以
1 |
module.exports = genConfig(builds[process.env.TARGET]) |
这句代码相当于:
1 |
module.exports = genConfig({ |
最终,genConfig 函数返回一个 config 对象,这个config对象就是Rollup的配置对象。那么我们就不难看到,入口文件是:
1 |
src/entries/web-runtime-with-compiler.js |
我们打开这个文件,不要忘了我们的主题,我们在寻找Vue构造函数,所以当我们看到这个文件的第一行代码是:
1 |
import Vue from './web-runtime' |
这个时候,你就应该知道,这个文件暂时与你无缘,你应该打开 web-runtime.js
文件,不过当你打开这个文件时,你发现第一行是这样的:
1 |
import Vue from 'core/index' |
依照此思路,最终我们寻找到Vue构造函数的位置应该是在 src/core/instance/index.js
文件中,其实我们猜也猜得到,上面介绍目录的时候说过:instance 是存放Vue构造函数设计相关代码的目录。总结一下,我们寻找的过程是这样的:
我们回头看一看 src/core/instance/index.js
文件,很简单:
1 |
import { initMixin } from './init' |
引入依赖,定义 Vue 构造函数,然后以Vue构造函数为参数,调用了五个方法,最后导出 Vue。这五个方法分别来自五个文件:init.js
state.js
render.js
events.js
以及 lifecycle.js
。
打开这五个文件,找到相应的方法,你会发现,这些方法的作用,就是在 Vue 的原型 prototype 上挂载方法或属性,经历了这五个方法后的Vue会变成这样:
1 |
// initMixin(Vue) src/core/instance/init.js ************************************************** |
这样就结束了吗?并没有,根据我们之前寻找 Vue 的路线,这只是刚刚开始,我们追溯路线往回走,那么下一个处理 Vue 构造函数的应该是 src/core/index.js
文件,我们打开它:
1 |
import Vue from './instance/index' |
这个文件也很简单,从 instance/index 中导入已经在原型上挂载了方法和属性后的 Vue,然后导入 initGlobalAPI
和 isServerRendering
,之后将Vue作为参数传给 initGlobalAPI
,最后又在 Vue.prototype
上挂载了 $isServer
,在 Vue
上挂载了 version
属性。
initGlobalAPI
的作用是在 Vue
构造函数上挂载静态属性和方法,Vue
在经过 initGlobalAPI
之后,会变成这样:
1 |
// src/core/index.js / src/core/global-api/index.js |
其中,稍微复杂一点的就是 Vue.options
,大家稍微分析分析就会知道他的确长成那个样子。下一个就是 web-runtime.js
文件了,web-runtime.js
文件主要做了三件事儿:
1、覆盖
Vue.config
的属性,将其设置为平台特有的一些方法
2、Vue.options.directives
和Vue.options.components
安装平台特有的指令和组件
3、在Vue.prototype
上定义__patch__
和$mount
经过 web-runtime.js
文件之后,Vue
变成下面这个样子:
1 |
// 安装平台特定的utils |
这里大家要注意的是 Vue.options
的变化。另外这里的 $mount
方法很简单:
1 |
Vue.prototype.$mount = function ( |
首先根据是否是浏览器环境决定要不要 query(el)
获取元素,然后将 el
作为参数传递给 this._mount()
。
最后一个处理 Vue 的文件就是入口文件 web-runtime-with-compiler.js
了,该文件做了两件事:
1、缓存来自 web-runtime.js
文件的 $mount
函数
1 |
const mount = Vue.prototype.$mount |
然后覆盖覆盖了 Vue.prototype.$mount
2、在 Vue 上挂载 compile
1 |
Vue.compile = compileToFunctions |
compileToFunctions 函数的作用,就是将模板 template
编译为render函数。
至此,我们算是还原了 Vue 构造函数,总结一下:
1、
Vue.prototype
下的属性和方法的挂载主要是在src/core/instance
目录中的代码处理的2、
Vue
下的静态属性和方法的挂载主要是在src/core/global-api
目录下的代码处理的3、
web-runtime.js
主要是添加web平台特有的配置、组件和指令,web-runtime-with-compiler.js
给Vue的$mount
方法添加compiler
编译器,支持template
。
四、一个贯穿始终的例子
在了解了 Vue
构造函数的设计之后,接下来,我们一个贯穿始终的例子就要登场了,掌声有请:
1 |
let v = new Vue({ |
好吧,我承认这段代码你家没满月的孩子都会写了。这段代码就是我们贯穿始终的例子,它就是这篇文章的主线,在后续的讲解中,都会以这段代码为例,当讲到必要的地方,会为其添加选项,比如讲计算属性的时候当然要加上一个 computed
属性了。不过在最开始,我只传递了两个选项 el
以及 data
,“我们看看接下来会发生什么,让我们拭目以待“ —- NBA球星在接受采访时最喜欢说这句话。
当我们按照例子那样编码使用Vue的时候,Vue都做了什么?
想要知道Vue都干了什么,我们就要找到 Vue 初始化程序,查看 Vue 构造函数:
1 |
function Vue (options) { |
我们发现,_init()
方法就是Vue调用的第一个方法,然后将我们的参数 options
透传了过去。在调用 _init()
之前,还做了一个安全模式的处理,告诉开发者必须使用 new
操作符调用 Vue。根据之前我们的整理,_init()
方法应该是在 src/core/instance/init.js
文件中定义的,我们打开这个文件查看 _init()
方法:
1 |
Vue.prototype._init = function (options?: Object) { |
_init()
方法在一开始的时候,在 this
对象上定义了两个属性:_uid
和 _isVue
,然后判断有没有定义 options._isComponent
,在使用 Vue 开发项目的时候,我们是不会使用 _isComponent
选项的,这个选项是 Vue 内部使用的,按照本节开头的例子,这里会走 else
分支,也就是这段代码:
1 |
vm.$options = mergeOptions( |
这样 Vue
第一步所做的事情就来了:使用策略对象合并参数选项
可以发现,Vue使用 mergeOptions
来处理我们调用Vue时传入的参数选项(options),然后将返回值赋值给 this.$options
(vm === this),传给 mergeOptions
方法三个参数,我们分别来看一看,首先是:resolveConstructorOptions(vm.constructor)
,我们查看一下这个方法:
1 |
export function resolveConstructorOptions (Ctor: Class<Component>) { |
这个方法接收一个参数 Ctor
,通过传入的 vm.constructor
我们可以知道,其实就是 Vue
构造函数本身。所以下面这句代码:
1 |
let options = Ctor.options |
相当于:
1 |
let options = Vue.options |
大家还记得 Vue.options
吗?在寻找Vue构造函数一节里,我们整理了 Vue.options
应该长成下面这个样子:
1 |
Vue.options = { |
之后判断是否定义了 Vue.super
,这个是用来处理继承的,我们后续再讲,在本例中,resolveConstructorOptions
方法直接返回了 Vue.options
。也就是说,传递给 mergeOptions
方法的第一个参数就是 Vue.options
。
传给 mergeOptions
方法的第二个参数是我们调用Vue构造函数时的参数选项,第三个参数是 vm
也就是 this
对象,按照本节开头的例子那样使用 Vue,最终运行的代码应该如下:
1 |
vm.$options = mergeOptions( |
了解了这些,我们就可以看看 mergeOptions
到底做了些什么了,根据引用寻找到 mergeOptions
应该是在 src/core/util/options.js
文件中定义的。这个文件第一次看可能会头大,下面是我处理后的简略展示,大家看上去应该更容易理解了:
1 |
// 1、引用依赖 |
上面的代码中,我省略了一些工具函数,例如 mergeHook
和 mergeAssets
等等,唯一需要注意的是这段代码:
1 |
config._lifecycleHooks.forEach(hook => { |
config
对象引用自 src/core/config.js
文件,最终的结果就是在 strats
下添加了相应的生命周期选项的合并策略函数为 mergeHook
,添加指令(directives)、组件(components)、过滤器(filters)等选项的合并策略函数为 mergeAssets
。
这样看来就清晰多了,拿我们贯穿本文的例子来说:
1 |
let v = new Vue({ |
其中 el
选项会使用 defaultStrat
默认策略函数处理,data
选项则会使用 strats.data
策略函数处理,并且根据 strats.data
中的逻辑,strats.data
方法最终会返回一个函数:mergedInstanceDataFn
。
这里就不详细的讲解每一个策略函数的内容了,后续都会讲到,这里我们还是抓住主线理清思路为主,只需要知道Vue在处理选项的时候,使用了一个策略对象对父子选项进行合并。并将最终的值赋值给实例下的 $options
属性即:this.$options
,那么我们继续查看 _init()
方法在合并完选项之后,又做了什么:
合并完选项之后,Vue 第二部做的事情就来了:初始化工作与Vue实例对象的设计
前面讲了 Vue 构造函数的设计,并且整理了 Vue原型属性与方法 和 Vue静态属性与方法,而 Vue 实例对象就是通过构造函数创造出来的,让我们来看一看 Vue 实例对象是如何设计的,下面的代码是 _init()
方法合并完选项之后的代码:
1 |
/* istanbul ignore else */ |
根据上面的代码,在生产环境下会为实例添加两个属性,并且属性值都为实例本身:
1 |
vm._renderProxy = vm |
然后,调用了四个 init*
方法分别为:initLifecycle
、initEvents
、initState
、initRender
,且在 initState
前后分别回调了生命周期钩子 beforeCreate
和 created
,而 initRender
是在 created
钩子执行之后执行的,看到这里,也就明白了为什么 created 的时候不能操作DOM了。因为这个时候还没有渲染真正的DOM元素到文档中。created
仅仅代表数据状态的初始化完成。
根据四个 init*
方法的引用关系打开对应的文件查看对应的方法,我们发现,这些方法是在处理Vue实例对象,以及做一些初始化的工作,类似整理Vue构造函数一样,我同样针对Vue实例做了属性和方法的整理,如下:
1 |
// 在 Vue.prototype._init 中添加的属性 ********************************************************** |
以上就是一个Vue实例所包含的属性和方法,除此之外要注意的是,在 initEvents
中除了添加属性之外,如果有 vm.$options._parentListeners
还要调用 vm._updateListeners()
方法,在 initState
中又调用了一些其他init方法,如下:
1 |
export function initState (vm: Component) { |
最后在 initRender
中如果有 vm.$options.el
还要调用 vm.$mount(vm.$options.el)
,如下:
1 |
if (vm.$options.el) { |
这就是为什么如果不传递 el
选项就需要手动 mount 的原因了。
那么我们依照我们本节开头的的例子,以及初始化的先后顺序来逐一看一看都发生了什么。我们将 initState
中的 init*
方法展开来看,执行顺序应该是这样的(从上到下的顺序执行):
1 |
initLifecycle(vm) |
首先是 initLifecycle
,这个函数的作用就是在实例上添加一些属性,然后是 initEvents
,由于 vm.$options._parentListeners
的值为 undefined
所以也仅仅是在实例上添加属性, vm._updateListeners(listeners)
并不会执行,由于我们只传递了 el
和 data
,所以 initProps
、initMethods
、initComputed
、initWatch
这四个方法什么都不会做,只有 initData
会执行。最后是 initRender
,除了在实例上添加一些属性外,由于我们传递了 el
选项,所以会执行 vm.$mount(vm.$options.el)
。
综上所述:按照我们的例子那样写,初始化工作只包含两个主要内容即:initData
和 initRender
。
五、通过initData
看Vue的数据响应系统
Vue的数据响应系统包含三个部分:Observer
、Dep
、Watcher
。关于数据响应系统的内容真的已经被文章讲烂了,所以我就简单的说一下,力求大家能理解就ok,我们还是先看一下 initData
中的代码:
1 |
function initData (vm: Component) { |
首先,先拿到 data 数据:let data = vm.$options.data
,大家还记得此时 vm.$options.data
的值应该是通过 mergeOptions
合并处理后的 mergedInstanceDataFn
函数吗?所以在得到 data 后,它又判断了 data 的数据类型是不是 ‘function’,最终的结果是:data 还是我们传入的数据选项的 data,即:
1 |
data: { |
然后在实例对象上定义 _data
属性,该属性与 data
是相同的引用。
然后是一个 while
循环,循环的目的是在实例对象上对数据进行代理,这样我们就能通过 this.a
来访问 data.a
了,代码的处理是在 proxy
函数中,该函数非常简单,仅仅是在实例对象上设置与 data
属性同名的访问器属性,然后使用 _data
做数据劫持,如下:
1 |
function proxy (vm: Component, key: string) { |
做完数据的代理,就正式进入响应系统,
1 |
observe(data) |
我们说过,数据响应系统主要包含三部分:Observer
、Dep
、Watcher
,代码分别存放在:observer/index.js
、observer/dep.js
以及 observer/watcher.js
文件中,这回我们换一种方式,我们先不看其源码,大家先跟着我的思路来思考,最后回头再去看代码,你会有一种:”奥,不过如此“的感觉。
假如,我们有如下代码:
1 |
var data = { |
这段代码目的是,首先定义一个数据对象 data
,然后通过 observer 对其进行观测,之后定义了三个观察者,当数据有变化时,执行相应的方法,这个功能使用Vue的实现原来要如何去实现?其实就是在问 observer
怎么写?Watch
构造函数又怎么写?接下来我们逐一实现。
首先,observer 的作用是:将数据对象data的属性转换为访问器属性:
1 |
class Observer { |
上面的代码中,我们定义了 observer 方法,该方法检测了数据data是不是纯JavaScript对象,如果是就调用 Observer
类,并将 data
作为参数透传。在 Observer
类中,我们使用 walk
方法对数据data的属性循环调用 defineReactive
方法,defineReactive
方法很简单,仅仅是将数据data的属性转为访问器属性,并对数据进行递归观测,否则只能观测数据data的直属子属性。这样我们的第一步工作就完成了,当我们修改或者获取data属性值的时候,通过 get
和 set
即能获取到通知。
我们继续往下看,来看一下 Watch
:
1 |
new Watch('a', () => { |
现在的问题是,Watch
要怎么和 observer
关联???????我们看看 Watch
它知道些什么,通过上面调用 Watch
的方式,传递给 Watch
两个参数,一个是 ‘a’ 我们可以称其为表达式,另外一个是回调函数。所以我们目前只能写出这样的代码:
1 |
class Watch { |
那么要怎么关联呢,大家看下面的代码会发生什么:
1 |
class Watch { |
多了一句 data[exp]
,这句话是在干什么?是不是在获取 data
下某个属性的值,比如 exp 为 ‘a’ 的话,那么 data[exp]
就相当于在获取 data.a
的值,那这会放生什么?大家不要忘了,此时数据 data
下的属性已经是访问器属性了,所以这么做的结果会直接触发对应属性的 get
函数,这样我们就成功的和 observer
产生了关联,但这样还不够,我们还是没有达到目的,不过我们已经无限接近了,我们继续思考看一下可不可以这样:
既然在
Watch
中对表达式求值,能够触发observer
的get
,那么可不可以在get
中收集Watch
中函数呢?
答案是可以的,不过这个时候我们就需要 Dep
出场了,它是一个依赖收集器。我们的思路是:data
下的每一个属性都有一个唯一的 Dep
对象,在 get
中收集仅针对该属性的依赖,然后在 set
方法中触发所有收集的依赖,这样就搞定了,看如下代码:
1 |
class Dep { |
上面的代码中,我们在 Watch
中增加了 pushTarget(this)
,可以发现,这句代码的作用是将 Dep.target
的值设置为该Watch对象。在 pushTarget
之后我们才对表达式进行求值,接着,我们修改 defineReactive
代码如下
1 |
function defineReactive (data, key, val) { |
如标注,新增了三句代码,我们知道,Watch
中对表达式求值会触发 get
方法,我们在 get
方法中调用了 dep.addSub
,也就执行了这句代码:this.subs.push(Dep.target)
,由于在这句代码执行之前,Dep.target
的值已经被设置为一个 Watch
对象了,所以最终结果就是收集了一个 Watch
对象,然后在 set
方法中我们调用了 dep.notify
,所以当data属性值变化的时候,就会通过 dep.notify
循环调用所有收集的Watch对象中的回调函数:
1 |
notify () { |
这样 observer
、Dep
、Watch
三者就联系成为一个有机的整体,实现了我们最初的目标,完整的代码可以戳这里:observer-dep-watch。这里还给大家挖了个坑,因为我们没有处理对数组的观测,由于比较复杂并且这又不是我们讨论的重点,如果大家想了解可以戳我的这篇文章:JavaScript实现MVVM之我就是想监测一个普通对象的变化,另外,在 Watch 中对表达式求值的时候也只做了直接子属性的求值,所以如果 exp 的值为 ‘a.b’ 的时候,就不可以用了,Vue的做法是使用 .
分割表达式字符串为数组,然后遍历一下对其进行求值,大家可以查看其源码。如下:
1 |
/** |
Vue 的求值代码是在 src/core/util/lang.js
文件中 parsePath
函数中实现的。总结一下Vue的依赖收集过程应该是这样的:
实际上,Vue并没有直接在 get
中调用 addSub
,而是调用的 dep.depend
,目的是将当前的 dep 对象收集到 watch 对象中,如果要完整的流程,应该是这样的:(大家注意数据的每一个字段都拥有自己的 dep
对象和 get
方法。)
这样 Vue 就建立了一套数据响应系统,之前我们说过,按照我们的例子那样写,初始化工作只包含两个主要内容即:initData
和 initRender
。现在 initData
我们分析完了,接下来看一看 initRender
六、通过initRender
看Vue的 render(渲染) 与 re-render(重新渲染)
在 initRender
方法中,因为我们的例子中传递了 el
选项,所以下面的代码会执行:
1 |
if (vm.$options.el) { |
这里,调用了 $mount
方法,在还原Vue构造函数的时候,我们整理过所有的方法,其中 $mount
方法在两个地方出现过:
1、在 web-runtime.js
文件中:
1 |
Vue.prototype.$mount = function ( |
它的作用是通过 el
获取相应的DOM元素,然后调用 lifecycle.js
文件中的 _mount
方法。
2、在 web-runtime-with-compiler.js
文件中:
1 |
// 缓存了来自 web-runtime.js 的 $mount 方法 |
分析一下可知 web-runtime-with-compiler.js
的逻辑如下:
1、缓存来自 web-runtime.js
文件的 $mount
方法
2、判断有没有传递 render
选项,如果有直接调用来自 web-runtime.js
文件的 $mount 方法
3、如果没有传递 render
选项,那么查看有没有 template
选项,如果有就使用 compileToFunctions
函数根据其内容编译成 render
函数
4、如果没有 template
选项,那么查看有没有 el
选项,如果有就使用 compileToFunctions
函数将其内容(template = getOuterHTML(el))编译成 render
函数
5、将编译成的 render
函数挂载到 this.$options
属性下,并调用缓存下来的 web-runtime.js
文件中的 $mount 方法
简单的用一张图表示 mount
方法的调用关系,从上至下调用:
不过不管怎样,我们发现这些步骤的最终目的是生成 render
函数,然后再调用 lifecycle.js
文件中的 _mount
方法,我们看看这个方法做了什么事情,查看 _mount
方法的代码,这是简化过得:
1 |
Vue.prototype._mount = function ( |
上面的代码很简单,该注释的都注释了,唯一需要看的就是这段代码:
1 |
vm._watcher = new Watcher(vm, () => { |
看上去很眼熟有没有?我们平时使用Vue都是这样使用 watch的:
1 |
this.$watch('a', (newVal, oldVal) => { }) |
第一个参数是 表达式或者函数,第二个参数是回调函数,第三个参数是可选的选项。原理是 Watch
内部对表达式求值或者对函数求值从而触发数据的 get
方法收集依赖。可是 _mount
方法中使用 Watcher
的时候第一个参数 vm
是什么鬼。我们不妨去看看源码中 $watch
函数是如何实现的,根据之前还原Vue构造函数中所整理的内容可知:$warch
方法是在 src/core/instance/state.js
文件中的 stateMixin
方法中定义的,源码如下:
1 |
Vue.prototype.$watch = function ( |
我们可以发现,$warch
其实是对 Watcher
的一个封装,内部的 Watcher
的第一个参数实际上也是 vm
即:Vue实例对象,这一点我们可以在 Watcher
的源码中得到验证,代开 observer/watcher.js
文件查看:
1 |
export default class Watcher { constructor ( |
可以发现真正的 Watcher
第一个参数实际上就是 vm
。第二个参数是表达式或者函数,然后以此类推,所以现在再来看 _mount
中的这段代码:
1 |
vm._watcher = new Watcher(vm, () => { |
忽略第一个参数 vm
,也就说,Watcher
内部应该对第二个参数求值,也就是运行这个函数:
1 |
() => { |
所以 vm._render()
函数被第一个执行,该函数在 src/core/instance/render.js
中,该方法中的代码很多,下面是简化过的:
1 |
Vue.prototype._render = function (): VNode { |
_render
方法首先从 vm.$options
中解构出 render
函数,大家应该记得:vm.$options.render
方法是在 web-runtime-with-compiler.js
文件中通过 compileToFunctions
方法将 template
或 el
编译而来的。解构出 render
函数后,接下来便执行了该方法:
1 |
vnode = render.call(vm._renderProxy, vm.$createElement) |
其中使用 call
指定了 render
函数的作用域环境为 vm._renderProxy
,这个属性在我们整理实例对象的时候知道,他是在 Vue.prototype._init
方法中被添加的,即:vm._renderProxy = vm
,其实就是Vue实例对象本身,然后传递了一个参数:vm.$createElement
。那么 render
函数到底是干什么的呢?让我们根据上面那句代码猜一猜,我们已经知道 render
函数是从 template
或 el
编译而来的,如果没错的话应该是返回一个虚拟DOM对象。我们不妨使用 console.log
打印一下 render
函数,当我们的模板这样编写时:
1 |
<ul id="app"> |
打印的 render
函数如下:
我们修改模板为:
1 |
<ul id="app"> |
打印出来的 render
函数如下:
其实了解Vue2.x版本的同学都知道,Vue提供了 render
选项,作为 template
的代替方案,同时为JavaScript提供了完全编程的能力,下面两种编写模板的方式实际是等价的:
1 |
// 方案一: |
现在我们再来看我们打印的 render
函数:
1 |
function anonymous() { |
是不是与我们自己写 render
函数很像?因为 render 函数的作用域被绑定到了Vue实例,即:render.call(vm._renderProxy, vm.$createElement)
,所以上面代码中 _c
、_v
、_s
以及变量 a
相当于Vue实例下的方法和变量。大家还记得诸如 _c
、_v
、_s
这样的方法在哪里定义的吗?我们在整理Vue构造函数的时候知道,他们在 src/core/instance/render.js
文件中的 renderMixin
方法中定义,除了这些之外还有诸如:_l
、 _m
、 _o
等等。其中 _l
就在我们使用 v-for
指令的时候出现了。所以现在大家知道为什么这些方法都被定义在 render.js
文件中了吧,因为他们就是为了构造出 render
函数而存在的。
现在我们已经知道了 render
函数的长相,也知道了 render
函数的作用域是Vue实例本身即:this
(或vm
)。那么当我们执行 render
函数时,其中的变量如:a
,就相当于:this.a
,我们知道这是在求值,所以 _mount
中的这段代码:
1 |
vm._watcher = new Watcher(vm, () => { |
当 vm._render
执行的时候,所依赖的变量就会被求值,并被收集为依赖。按照Vue中 watcher.js
的逻辑,当依赖的变量有变化时不仅仅回调函数被执行,实际上还要重新求值,即还要执行一遍:
1 |
() => { |
这实际上就做到了 re-render
,因为 vm._update
就是文章开头所说的虚拟DOM中的最后一步:patch
vm_render
方法最终返回一个 vnode
对象,即虚拟DOM,然后作为 vm_update
的第一个参数传递了过去,我们看一下 vm_update
的逻辑,在 src/core/instance/lifecycle.js
文件中有这么一段代码:
1 |
if (!prevVnode) { |
如果还没有 prevVnode
说明是首次渲染,直接创建真实DOM。如果已经有了 prevVnode
说明不是首次渲染,那么就采用 patch
算法进行必要的DOM操作。这就是Vue更新DOM的逻辑。只不过我们没有将 virtual DOM 内部的实现。
现在我们来好好理理思路,当我们写如下代码时:
1 |
new Vue({ |
Vue 所做的事:
1、构建数据响应系统,使用
Observer
将数据data转换为访问器属性;将el
编译为render
函数,render
函数返回值为虚拟DOM2、在
_mount
中对_update
求值,而_update
又会对render
求值,render
内部又会对依赖的变量求值,收集为被求值的变量的依赖,当变量改变时,_update
又会重新执行一遍,从而做到re-render
。
用一张详细一点的图表示就是这样的:
到此,我们从大体流程,挑着重点的走了一遍Vue,但是还有很多细节我们没有提及,比如:
1、将模板转为 render
函数的时候,实际是先生成的抽象语法树(AST),再将抽象语法树转成的 render
函数,而且这一整套的代码我们也没有提及,因为他在复杂了,其实这部分内容就是在完正则。
2、我们也没有详细的讲 Virtual DOM 的实现原理,网上已经有文章讲了,大家可以搜一搜
3、我们的例子中仅仅传递了 el
,data
选项,大家知道 Vue 支持的选项很多,比如我们都没有讲到,但都是触类旁通的,比如你搞清楚了 data
选项再去看 computed
选项或者 props
选项就会很容易,比如你知道了 Watcher
的工作机制再去看 watch
选项就会很容易。
本篇文章作为Vue源码的启蒙文章,也许还有很多缺陷,全当抛砖引玉了。
Vue2.1.7源码学习的更多相关文章
- Vue2.0源码学习(4) - 合并配置
合并配置 通过之前的源码学习,我们已经了解到了new Vue主要有两种场景,第一种就是在外部主动调用new Vue创建一个实例,第二个就是代码内部创建子组件的时候自行创建一个new Vue实例.但是无 ...
- Vue源码学习(一):调试环境搭建
最近开始学习Vue源码,第一步就是要把调试环境搭好,这个过程遇到小坑着实费了点功夫,在这里记下来 一.调试环境搭建过程 1.安装node.js,具体不展开 2.下载vue项目源码,git或svn等均可 ...
- Vue3全局APi解析-源码学习
本文章共5314字,预计阅读时间5-15分钟. 前言 不知不觉Vue-next的版本已经来到了3.1.2,最近对照着源码学习Vue3的全局Api,边学习边整理了下来,希望可以和大家一起进步. 我们以官 ...
- 【Vue源码学习】响应式原理探秘
最近准备开启Vue的源码学习,并且每一个Vue的重要知识点都会记录下来.我们知道Vue的核心理念是数据驱动视图,所有操作都只需要在数据层做处理,不必关心视图层的操作.这里先来学习Vue的响应式原理,V ...
- 【Vue源码学习】依赖收集
前面我们学习了vue的响应式原理,我们知道了vue2底层是通过Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些 ...
- Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结
2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分 ...
- jQuery源码学习感想
还记得去年(2015)九月份的时候,作为一个大四的学生去参加美团霸面,结果被美团技术总监教育了一番,那次问了我很多jQuery源码的知识点,以前虽然喜欢研究框架,但水平还不足够来研究jQuery源码, ...
- MVC系列——MVC源码学习:打造自己的MVC框架(四:了解神奇的视图引擎)
前言:通过之前的三篇介绍,我们基本上完成了从请求发出到路由匹配.再到控制器的激活,再到Action的执行这些个过程.今天还是趁热打铁,将我们的View也来完善下,也让整个系列相对完整,博主不希望烂尾. ...
- MVC系列——MVC源码学习:打造自己的MVC框架(三:自定义路由规则)
前言:上篇介绍了下自己的MVC框架前两个版本,经过两天的整理,版本三基本已经完成,今天还是发出来供大家参考和学习.虽然微软的Routing功能已经非常强大,完全没有必要再“重复造轮子”了,但博主还是觉 ...
随机推荐
- 数据迁移之Sqoop
一 简介 Apache Sqoop(TM)是一种用于在Apache Hadoop和结构化数据存储(如关系数据库)之间高效传输批量数据的工具 . 官方下载地址:http://www.apache.org ...
- 001 Anaconda的介绍与安装
1.官网 www.continuum.io 2.ananconda的版本 同一个版本下对应一个python3与python2,在这里下载使用python 2.7的版本. 3.概述 Anaconda是一 ...
- Hibernate or JPA Annotation中BLOB、CLOB注解写法
BLOB和CLOB都是大字段类型,BLOB是按二进制字节码来存储的,而CLOB是可以直接存储字符串的. 在hibernate or JPA Annotation中,实体BLOB.CLOB类型的注解与普 ...
- js javascript 原型链详解
看了许多大神的博文,才少许明白了js 中原型链的概念,下面给大家浅谈一下,顺便也是为了巩固自己 首先看原型链之前先来了解一下new关键字的作用,在许多高级语言中,new是必不可少的关键字,其作用是为了 ...
- iOS 9应用开发教程之定制应用程序图标以及真机测试
iOS 9应用开发教程之定制应用程序图标以及真机测试 定制ios9应用程序图标 在图1.12中可以看到应用程序的图标是网状白色图像,它是iOS模拟器上的应用程序默认的图标.这个图标是可以进行改变的.以 ...
- XShell通过中转服务器直接连接目标服务器
最近由于公司生产环境的变化,使得我们不能使用自己的机器连接到生产环境去,而是要通过跳板机中转才可以连接.于是今天尝试使用 XShell 通过跳板机直接转接到生产环境. 一.使用代理方式 首先填写连接信 ...
- lamp 5.6.36 bug记录
后来发现另一个问题,php文字水印中文是乱码. 用yum安装lamp环境详见:https://blog.csdn.net/u010071211/article/details/80370201 在ce ...
- 2018-2019-2 20162318《网络攻防技术》Exp5 MSF基础应用
1.实验内容 1.一个主动攻击实践,如ms08_067 2. 一个针对浏览器的攻击,如ms11_050) 3. 一个针对客户端的攻击,如Adobe 4. 成功应用任何一个辅助模块 2.基础问题回答 2 ...
- hdu 2112 最短路
本来是拿来复习一下map的,没想搞了半天,一直wa,最后发现预处理没有处理到所有的点 就是个最短路 Sample Input 6 xiasha westlake xiasha station 60 x ...
- Problem F: 铺地砖
Description 元旦过去了,新年大酬宾活动也已经告一段落了.陈盖历望着堆在仓库的瓷砖,很无聊的他把这些瓷砖裁成很多1X1 1X2 1X3的小瓷砖,然后他把这些小瓷砖排在地上画的一个1*n的长方 ...