本文为原创文章,未获授权禁止转载,侵权必究!
本篇是 Vue 源码解析系列第 3 篇,关注专栏
前言
数据驱动是 Vue.js 的核心思想。所谓数据驱动
,是指视图是由数据生成的,我们对视图的修改不会直接操作DOM,而是通过修改数据进行视图的更新。通过如下案例,我们从源码角度来分析 Vue 是如何实现的。
<div id="app">
{{ message }}
</div>
const app = new Vue({
el: '#app',
data: {
message: 'Hello World'
}
})
准备工作
源码目录
src
├── compiler // 编译相关
├── core // 核心代码
├── platforms // 不同平台的支持
├── server // 服务端渲染
├── sfc // .vue 文件解析
├── shared // 共享代码
- compiler
- 该目录包含 Vue.js 所有
编译相关
的代码,包括将模板解析成 AST 语法树,AST 语法优化,代码生成等功能
- 该目录包含 Vue.js 所有
- core
- 该目录为 Vue.js 的
核心代码
,包含 Vue 实例化、全局 API 分装、内置组件、虚拟 DOM等
- 该目录为 Vue.js 的
- platforms
- 该目录是 Vue.js 入口,主要对 Vue 代码如何在浏览器上运行起来
- server
- 该目录是运行服务端相关代码,所有服务端渲染相关逻辑都在该目录下,如运行在服务端的Node.js,与运行在客户端的 Vue.js 有所不同
- sfc
- 该目录下的代码逻辑会把 .vue 文件内容解析成一个 JavaScript 的对象
- shared
- 定义一些工具方法,供全局调用
源码构建
通常我们利用 vue-cli 初始化项目,会询问我们选择 Runtime Only
还是 Runtime + Compiler
版本,它们区别如下:
- Runtime Only
在使用该版本时,需借助 webpack 的 vue-loader 工具把 .vue 文件编译成 js,代码体积会更轻量
- Runtime + Compiler
我们如果没有对代码做预编译,但⼜使⽤了 Vue 的 template 属性并传⼊⼀个字符串,则需要在客户端编译模板,如下所⽰:
// 需要编译器的版本
new Vue({
template: '<div>{{ hi }}</div>'
})
// 这种情况不需要
new Vue({
render (h) {
return h('div', this.hi)
}
})
因为在 Vue.js 2.0 中,最终渲染都是通过 render
函数,如果写 template
属性,则需要编译成 render
函数,那么这个编译过程会发⽣运⾏时,所以需要带有编译器的版本。
很显然,这个编译过程对性能会有⼀定损耗,所以通常我们更推荐使⽤ Runtime-Only
的 Vue.js。
从入口出发
- 入口文件被定义在
src/platforms/web/entry-runtime-with-compiler.js
- 找到入口文件下的引入文件
import Vue from './runtime/index
- 再找到引入文件
import Vue from 'core/index'
- 最后找到 Vue 的引入文件
import Vue from './instance/index'
至此,我们要找的 Vue 被定义在 src/core/instance/index.js
中,它实际是一个构造函数
。
function Vue (options) {
// 初始化配置
this._init(options)
}
// 以下方法均是给 vue 原型即 vue.prototype 挂载相应的方法
// 挂载_init方法
initMixin(Vue)
// 挂载 $set、$delete、$watch 等方法
stateMixin(Vue)
// 挂载 $on、$once 等方法
eventsMixin(Vue)
// 挂载 $forceUpdate 等方法
lifecycleMixin(Vue)
// 挂载 _render 渲染方法
renderMixin(Vue)
export default Vue
_init
方法被定义在 src/core/instance/init.js
,该函数会初始化生命周期、初始化事件中心、初始化渲染、初始化data、watcher、props、computed等,最后检测到如果有 el 属性,则调用 vm.$mount 挂载,目标就是把模板渲染成最终 DOM。
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 其目的是将模板渲染成最终 DOM
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
Vue 实例的挂载过程
上文讲述 Vue 实例初始完后,最终会检测是否含有 el
属性,如果有则调用 vm.$mount
挂载,目标就是把模板渲染成最终 DOM。$mount
被定义在 src/platforms/web/runtime/index
中,其核心就是调用mountComponent
方法
import { mountComponent } from 'core/instance/lifecycle'
// 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
函数被定义在 src/core/instance/lifecycle
中
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 省略
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
// 省略
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// 省略
return vm
}
我们可以很清晰看到 mountComponent
方法实际就是生成一个 watcher
实例,其中第二个参数updateComponent
是关键,它实际是调用 _update
方法,之后又再调用 _render
方法。_render
方法定义在 src/core/instance/render
,实际是调用 createElement
方法
// createElement引入
import { createElement } from '../vdom/create-element'
export function initRender (vm: Component) {
// 省略
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// 省略
}
createElement
方法定义在 src/core/vdom/create-element
,该方法实际是创建 VDOM
虚拟 DOM 过程
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 省略
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
// 省略
return createEmptyVNode()
}
// 省略
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
最后回到 _update
方法,它被定义在 src/core/instance/lifecycle.js
,该方法核心是调用 __patch__
方法,里面会执行 diff 算法,将真实 DOM 解析成虚拟 DOM,并执行插入和替换操作,最终渲染完成
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
}
总结
new Vue实例化过程主要发生如下几件事:
- init初始化一些配置、初始化生命周期、初始化事件中心、初始化data、watcher、props、computed等
- 通过
$mount
实例方法挂载 vm - compile 编译生成 render 方法,如果模板中直接编写 render 方法,那直接跳过 compile 过程
- render 方法的核心 是调用
createElement
函数 - 之后会执行 update 方法,该方法核心是执行
patch
方法,patch 函数会将真实 DOM 解析成 虚拟 DOM,并执行替换和插入操作,最终渲染完成