Vue 源码学习

Flow

Vue.js 的源码利用了 Flow 做了静态类型检查

类型检查

  • 类型检查,就是在编译期尽早发现(由类型错误引起的)bug,又不影响代码运行(不需要运行时动态检查类型),使编写 JavaScript 具有和编写 Java 等强类型语言相近的体验。
  • 选择 Flow,主要是因为 Babel 和 ESLint 都有对应的 Flow 插件以支持语法,可以完全沿用现有的构建配置,非常小成本的改动就可以拥有静态类型检查的能力。

通常类型检查分成 2 种方式:

  • 类型推断:通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型。

    • 它不需要任何代码修改即可进行类型检查,最小化开发者的工作量。它不会强制你改变开发习惯,因为它会自动推断出变量的类型

    例如:

    1
    2
    3
    4
    function split(str) {
    return str.split(' ')
    }
    split(11) // flow 检查该代码会报错 因为期待的是字符串 而传入的是数字
  • 类型注释:事先注释好我们期待的类型,Flow 会基于这些注释来判断。

    • 借助类型注释来指明期望的类型。类型注释是以冒号 : 开头,可以在函数参数,返回值,变量声明中使用。
    1
    2
    3
    4
    function add(x: number, y: number): number {
    return x + y
    }
    add('Hello', 11)

    flow 官方文档:https://flow.org/en/docs/config/

源码目录

1
2
3
4
5
6
7
src
├── compiler # 编译相关
├── core # 核心代码
├── platforms # 不同平台的支持
├── server # 服务端渲染
├── sfc # .vue 文件解析
├── shared # 共享代码

vue源码基于Rollup(JavaScript模块打包器)构建

  • Runtime Only
    • 我们在使用 Runtime Only 版本的 Vue.js 的时候,通常需要借助如 webpack 的 vue-loader 工具把 .vue 文件编译成 JavaScript,因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,因此代码体积也会更轻量。
  • Runtime + Compile
    • 我们如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,则需要在客户端编译模板
1
2
3
4
5
6
7
8
9
10
// 需要编译器的版本
new Vue({
template: '<div>{{ hi }}</div>'
})
// 这种情况不需要
new Vue({
render (h) {
return h('div', this.hi)
}
})

Vue的入口

1
2
3
4
5
6
7
8
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}

src/core/instance/index.js中 我们可以看到Vue实际上就是一个Function实现的类 我们只能通过new Vue去对它进行实例化

initGlobalAPI

Vue.js 在整个初始化过程中,除了给它的原型 prototype 上扩展方法,还会给 Vue 这个对象本身扩展全局的静态方法

数据驱动

Vue的一个核心思想就是数据驱动

  • 数据驱动是指视图是由数据驱动而生成的 对视图的修改不会直接操作DOM 而是修改数据

New Vue() 到底发生了什么

通过入口的代码可以看到 当new Vue() 时 会调用this._init方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++

let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}

// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
//检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}

Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。

Vue实例挂载的实现

vue通过$mount方式挂载实例 那么我们先看看源码中$mount的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//首先 对原型上的$mount进行了缓存
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)

/* istanbul ignore if */
//对el进行限制 不能挂载在body HTML这样的根节点上
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}

const options = this.$options
// resolve template/el and convert to render function
//如果没有定义render方法 把 el 或者 template 字符串转换成 render 方法。

if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
//在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个“在线编译”的过程,它是调用 compileToFunctions 方法实现的,
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
//调用原先原型上的$mount方法
return mount.call(this, el, hydrating)
}

原先原型上的 $mount 方法在 src/platform/web/runtime/index.js 中定义 如此设计为了复用 能够在runtime only版本直接使用

1
2
3
4
5
6
7
8
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}

实际调用的是mountComponent方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')

let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`

mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)

mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined

//实例化一个渲染watcher 在它的回调函数中会调用 updateComponent 方法
//在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。
//Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false

// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
//函数最后判断为根节点的时候设置 vm._isMounted 为 true, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}

Render

Vue 的 _render 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node

在 _render方法中调用了render函数

1
vnode = render.call(vm._renderProxy, vm.$createElement)

render 函数中的 createElement 方法就是 vm.$createElement 方法

1
2
3
4
5
6
7
8
9
10
11
12
export function initRender (vm: Component) {
// ...
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

实际上,vm.$createElement 方法定义是在执行 initRender 方法的时候,可以看到除了 vm.$createElement 方法,还有一个 vm._c 方法,它是被模板编译成的 render 函数使用,而 vm.$createElement 是用户手写 render 方法使用的, 这两个方法支持的参数相同,并且内部都调用了 createElement 方法。

vm.render 最终通过执行createElement 方法并返回的是vnode 它是一个虚拟Node 那么Virtual DOM又是什么呢?

Virtual DOM

真正的DOM元素是很庞大的 如果频繁的对DOM进行操作 那么会产生一定的性能问题

irtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。在 Vue.js 中,Virtual DOM 是用 VNode 这么一个 Class 去描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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; // functional scope id support

constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}

// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}

Vue.js 中 Virtual DOM 是借鉴了一个开源库 snabbdom 的实现

实际上我们会发现 Virtual DOM就是对真实DOM的一种抽象描述 它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。

Virtual DOM 除了它的数据结构的定义,映射到真实的 DOM 实际上要经历 VNode 的 create、diff、patch 等过程 在 Vue.js 中,VNode 的 create 是通过之前提到的 createElement 方法创建的

createElement

Vue.js 利用 createElement 方法创建 VNode createElement是对_createElement的一层封装 能够让传入的参数更加灵活 真正创建Vnode 的函数是 _createElement

_createElement方法有五个参数

  • context 表示 VNode 的上下文环境,它是 Component 类型;
  • tag 表示标签,它可以是一个字符串,也可以是一个 Component
  • data 表示 VNode 的数据,它是一个 VNodeData 类型,可以在 flow/vnode.js 中找到它的定义
  • children 表示当前 VNode 的子节点,它是任意类型的,它接下来需要被规范为标准的 VNode 数组;
  • normalizationType 表示子节点规范的类型,类型不同规范的方法也就不一样,它主要是参考 render 函数是编译生成的还是用户手写的。

children的规范化

Virtual DOM 实际上是一个树状结构,每一个 VNode 可能会有若干个子节点,这些子节点应该也是 VNode 的类型。_createElement 接收的第 4 个参数 children 是任意类型的,因此我们需要把它们规范成 VNode 类型

根据 normalizationType 的不同,调用了 normalizeChildren(children)simpleNormalizeChildren(children) 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
export function simpleNormalizeChildren (children: any) {
for (let i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}

// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}

指令

  • v-once

    • 只在首次渲染页面时,替换元素的绑定语法内容。但是不会将当前元素加入虚拟DOM树。结果: 只在首次加载一次,之后都不会再改变。
  • v-html

    • 双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,需要使用 v-html
  • v-bind

    • 动态绑定属性
      • 比如a 的hred img的src
  • v-on

    • 事件监听

    • 在事件定义时,写函数时省略了小括号,但是方法本身是需要一个参数的,这个时候,

      vue会默认将浏览器生产的event事件对象作为参数传入到方法

    • 在方法定义时,我们需要event对象,同时又需要其他参数

    • 在调用方法时,如何手动的获取到浏览器参数的event对象:$event

  • v-if

    • 当我们条件为false时,包含v-if指令的元素,根本不会存在dom中
  • v-show

    • 当我们的条件为false时,v-show只是给我们的元素增加了一个行内样式:display:none
    • 开发中的选择: 当需要在显示隐藏之间切片很频繁时,使用v-show
    • 当只有一次切换,通常使用v-if
  • v-model

    • 等价于
    • <input type=”text”**:value**=”message”**@input**=”message=$event.target.value “>
  • 组件中的data为函数

    • 组件中如果data不是函数 那么组件实例都会使用同一个data 指向的是同一个地址,会导致数据混乱的问题 影响组件的复用性
    • 调用函数时每次都会返回新的对象 指向的就不是同一个地址 组件之间不会相互影响
  • 路由的懒加载

    • 路由懒加载是结合了Vue 的异步组件和webpack代码分割

      • import()返回一个promise函数

      • components: {
            'my-component': () => import('./my-async-component')
          }
        <!--hexoPostRenderEscape:<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">* &#96;&#96;&#96;js</span><br><span class="line">  const Foo &#x3D; () &#x3D;&gt; import(&#39;.&#x2F;Foo.vue&#39;)</span><br><span class="line">  然后再路由配置中正常使用Foo</span><br></pre></td></tr></table></figure>:hexoPostRenderEscape-->
  • namespaced这是属性用于解决不同模块的命名冲突问题