使用Vue也有一段时间了,最近去阅读了Vue的源码,想总结分享下学到的新东西。
如果觉得直接看源码很枯燥,可以结合前人总结的文章或者视频来看,相信会事半功倍。
源码这个东西,一定要多看多思考,要想精通,一遍两遍肯定是不够的。有的时候可能看着一个问题就会想通之前看过但是不明白的另个问题。
打算出一个Vue源码系列性的文章,算是我个人学习源码的一个历程。
首先找到Vue项目 github 地址:vue2.x源码链接,git clone xxx
下载源码。
新建一个 html 文件引入vue。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<div id="app">div>
<script src="./vue-2.7.14/dist/vue.js">script>
script>
<script>
new Vue({
el:'#app',
})
script>
body>
html>
vue 初始化就从这里开始了。
// src/core/instance/index.ts
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
import type { GlobalAPI } from 'types/global-api'
// Vue构造函数的声明
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
// 初始化方法
this._init(options)
}
// 从文件中可以看出 上面的 _init() 是从下面的混入中获得的,那么具体从哪个中得到的需要分析一下
// 初始化混入
initMixin(Vue)
// state的混入
stateMixin(Vue)
// events的混入
eventsMixin(Vue)
// 生命周期的混入
lifecycleMixin(Vue)
// 渲染函数的混入
renderMixin(Vue)
// 上面的这些混入其实就是初始化实例的方法和属性
// 其实通过名字不难发现, _init() 方法肯定是在初始化的混入中:initMixin()
export default Vue as unknown as GlobalAPI
其实通过名字不难发现, _init()
方法肯定是在 初始化的混入中: initMixin()
,那就继续看 initMixin()
所在的文件。
// src/core/instance/init.ts
export function initMixin(Vue: typeof Component) {
// 负责 Vue 的初始化过程;接收用户传进来的选项:options
Vue.prototype._init = function (options?: Record<string, any>) {
// vue的实例
const vm: Component = this
// 每个 vue 实例都有一个 _uid,并且是依次递增的
vm._uid = uid++
let startTag, endTag
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// vue标志, 避免被 Observe 观察
vm._isVue = true
vm.__v_skip = true
vm._scope = new EffectScope(true)
vm._scope._vm = true
// 选项合并:用户选项和系统默认的选项需要合并
// 处理组件的配置内容,将传入的options与构造函数本身的options进行合并(插件的策略都是默认配置和传入配置进行合并)
if (options && options._isComponent) {
// 子组件:优化内部组件(子组件)实例化,且动态的options合并相当慢,这里只有需要处理一些特殊的参数属性。减少原型链的动态查找,提高执行效率
initInternalComponent(vm, options as any)
} else {
// 根组件: 将全局配置选项合并到根组件的配置上,其实就是一个选项合并
vm.$options = mergeOptions(
// 获取当前构造函数的基本options
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
vm._self = vm
// 下面的方法才是整个初始化最重要的核心代码
initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
initEvents(vm) //初始化事件:$on, $off, $emit, $once
initRender(vm) // 初始化render渲染所需的slots、渲染函数等。其实就两件事1、插槽的处理、2、$createElm 也就是 render 函数中的 h 的声明
callHook(vm, 'beforeCreate', undefined, false /* setContext */) // 调用生命周期的钩子函数,在这里就能看出一个组件在创建之前和之后分别做了哪些初始化
// provide/inject 隔代传参
// provide:在祖辈中可以直接提供一个数据
// inject:在后代中可以通过inject注入后直接使用
initInjections(vm) // 在 data/props之前执行;隔代传参时 先inject。作为一个组件,在要给后辈组件提供数据之前,需要先把祖辈传下来的数据注入进来
initState(vm) // 数据响应式的重点,处理 props、methods、data、computed、watch初始化
initProvide(vm) // 在 data/props之后执行;在把祖辈传下来的数据注入进来以后 再provide
// 总而言之,上面的三个初始化其实就是:对组件的数据和状态的初始化
callHook(vm, 'created') // created 初始化完成,可以执行挂载了
if (__DEV__ && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount,反之,没有 el 则必须手动调用 $mount
if (vm.$options.el) {
// 调用 $mount 方法,进入挂载阶段
vm.$mount(vm.$options.el)
}
}
}
打开 $mount
,看看它做了什么。把一些多余的 代码简化一下。
// src/platforms/web/runtime-with-compiler.ts
import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'
import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref
} from './util/compat'
import type { Component } from 'types/component'
import type { GlobalAPI } from 'types/global-api'
// 获取宿主元素的方法
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
// 扩展 $mount 方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
// 获取选项 $options
const options = this.$options
/**
* 编译权重:
* 优先看有没有render函数,如果有直接用
* 如果没有render函数就看有没有template模板
* 如果都没有就直接获取el的outerHTML作为渲染模板
*/
// 如果 render 选项不存在
if (!options.render) {
// 则查找 template
let template = options.template
// 如果 template 存在
if (template) {
// 则判断一下 template 的写法
if (typeof template === 'string') { // 如果是字符串模板 例如:" template "
if (template.charAt(0) === '#') { // 如果是宿主元素的选择器,例如:"#app"
// 则调用上面的 idToTemplate() 方法查找
template = idToTemplate(template)
if (__DEV__ && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
// 如果是一个dom元素
} else if (template.nodeType) {
// 则使用它的 innerHTML
template = template.innerHTML
} else {
if (__DEV__) {
warn('invalid template option:' + template, this)
}
return this
}
// 如果设置了 el
} else if (el) {
// 则以 el 的 outerHTML 作为 template
template = getOuterHTML(el)
}
// 如果存在 template 选项,则编译它获取 render 函数
if (template) {
// 编译的过程:把 template 变为 render 函数
const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: __DEV__,
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
},
this
)
// 最终获得的 render 函数将赋值给 选项 options
options.render = render
options.staticRenderFns = staticRenderFns
// 执行默认的挂载
return mount.call(this, el, hydrating)
}
/**
* 总结一下:
* new Vue({
* el: "#app",
* template: " template ",
* template: "#app",
* render(h){ return h("div", "render")},
* data: {}
* })
* 在用户同时设置了 el、template、render的时候,优先级的判断为:render > template > el
*/
// 获取 outerHTML 的方法
function getOuterHTML(el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
Vue.compile = compileToFunctions
export default Vue as GlobalAPI
上面代码 主要 实现了 vue 渲染过程中很重要的一步,得到 render
函数。
如果我们使用的 template
进行编写 HTML 代码,Vue 内部会把模板编译成 Vue 可识别的 render
函数,如果有写 render 则可以省去编译过程。( 直接写 render 函数对 vue 编译效率会更好 )
上面 entry-runtime-with-compiler.js 文件中的Vue来自于 ‘./runtime/index’,那我们自己分析 ‘./runtime/index’ 文件。
// src/platforms/web/runtime/index.ts
// 能看到 Vue也不是在这里定义的,一样是导入的,那么这个文件主要做了什么呢?
import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'
import {
query,
mustUseProp,
isReservedTag,
isReservedAttr,
getTagNamespace,
isUnknownElement
} from 'web/util/index'
import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'
import type { Component } from 'types/component'
//...
// 安装了一个 patch 函数,也可以叫补丁函数或者更新函数。主要的作用就是把:虚拟dom 转化为真实的dom(vdom => dom)
Vue.prototype.__patch__ = inBrowser ? patch : noop
// 实现了 $mount 方法:其实就只调用了一个mountComponent()方法
// $mount的最终目的就是:把虚拟dom 转化为真实的dom,并且追加到宿主元素中去(vdom => dom => append)
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
export default Vue
下面打开源码 src/core/instance/lifecycle.js 找到 mountComponent 方法
// src/core/instance/lifecycle.ts
export function mountComponent(...): Component {
// 调用生命周期钩子函数
callHook(vm, 'beforeMount')
let updateComponent
// 创建一个更新渲染函数; 调用 _update 对 render 返回的虚拟 DOM 进行 patch(也就是 Diff )到真实DOM,这里是首次渲染
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 当触发更新的时候,会在更新之前调用
const watcherOptions: WatcherOptions = {
before() {
// 判断 DOM 是否是挂载状态,就是说首次渲染和卸载的时候不会执行
if (vm._isMounted && !vm._isDestroyed) {
// 调用生命周期钩子函数
callHook(vm, 'beforeUpdate')
}
}
}
//生成一个渲染 watcher 每次页面依赖的数据更新后会调用 updateComponent 进行渲染
new Watcher(
vm,
updateComponent,
noop,
watcherOptions,
true
)
// 没有老的 vnode,说明是首次渲染
if (vm.$vnode == null) {
vm._isMounted = true
// 渲染真实 dom 结束后调用 mounted 生命周期
callHook(vm, 'mounted')
}
return vm
}
到此整个Vue初始化就完成了,具体的细化代码这里不展示了,主要的就是做标注的代码,这里再做个总结吧。
从上面的函数看来,new Vue所做的事情,就像一个流程图一样展开了,分别是
选项合并
,处理组件的配置内容,将传入的options与构造函数本身的options进行合并(用户选项和系统默认的选项进行合并)vue实例生命周期
相关的属性,组件关系属性的初始化,定义了比如 $parent
、$children
、$root
、$refs
等。事件
,若存在父监听事件,则添加到该实例上。render渲染
所需的slots、渲染函数等。其实就两件事:插槽的处理 和 $createElm的声明,也就是 render 函数中的 h 函数的声明。beforeCreate
钩子函数,在这里就能看出一个组件在创建前和后分别做了哪些初始化。props
,methods
,data
,computed
,watch
进行初始化,包括响应式的处理。created
钩子函数,初始化完成,可以执行挂载了。DOM
元素上。如果组件构造函数设置了el选项,会自动挂载,所以就不用再手动调用 $mount
去挂载。可参考:
Vue源码系列(二):Vue初始化都做了什么?
vue源码阅读解析(超详细)