【个人总结】Vue源码解读
前言
作为一名前端开发攻城狮来讲,我觉得学习源码是必要的。当时所在的公司主要是使用Vue来做开发的,在那里面呆了两年多,主要是摸鱼过去的,Vue我自认为使用的算是熟练了,但是面试的时候被问到源码有关的问题,我还是回答不出来。以上面的背景未前提,我花了一个多星期的时间去浏览网上各种对Vue源码解析的文章再加上我自己的理解写下了以下文章,可能我的理解不一定是对的,大家可以适当参考下。
目录解说
源码方面我是在github下载的2.6版本
Vue2.6源码传送门
Vue源码根目录下有很多个文件夹,下面我对一些我知道的文件夹做出注释
Vue的实例
因为我们在使用Vue的时候通常都是new Vue(),并且通过npm run dev或其他命令启动Vue项目,所以我们根据这两个特点,从package.json文件中找到Vue项目的启动命令
由命令可见,这里用到了rollup,百度下可知这是个类似webpack的构建工具,但这个不是重点,我们发现它执行了scripts/config.js文件。现在看下scripts/config.js文件:
const builds = {
......
......
......
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'), // 入口文件
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
......
......
......
}
function getConfig(name) {
const opts = builds[name]
const config = {
input: opts.entry,
external: opts.external,
plugins: [
replace({
__WEEX__: !!opts.weex,
__WEEX_VERSION__: weexVersion,
__VERSION__: version,
}),
flow(),
alias(Object.assign({}, aliases, opts.alias)),
].concat(opts.plugins || []),
output: {
file: opts.dest,
format: opts.format,
banner: opts.banner,
name: opts.moduleName || "Vue",
},
onwarn: (msg, warn) => {
if (!/Circular/.test(msg)) {
warn(msg);
}
},
};
if (opts.env) {
config.plugins.push(replace({
'process.env.NODE_ENV': JSON.stringify(opts.env)
}))
}
if (opts.transpile !== false) {
config.plugins.push(buble())
}
Object.defineProperty(config, '_name', {
enumerable: false,
value: name
})
return config
}
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
在这段代码中,我们首先看最后一段代码,我们可以看到它调用了genConfig(process.env.TARGET),看向getConfig方法,不难得知这个方法是用于匹配build对象里面的参数并生成rollup配置,在package.json文件中我们看到 dev 中有 TARGET:web-full-dev ,所以我们在build对象中找到 web-full-dev ,根据resolve处的代码结合
const aliases = require('./alias')
const resolve = p => {
const base = p.split('/')[0]
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
return path.resolve(__dirname, '../', p)
}
}
在这里找到./alias文件
const path = require('path')
const resolve = p => path.resolve(__dirname, '../', p)
module.exports = {
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
compiler: resolve('src/compiler'),
core: resolve('src/core'),
shared: resolve('src/shared'),
web: resolve('src/platforms/web'),
weex: resolve('src/platforms/weex'),
server: resolve('src/server'),
entries: resolve('src/entries'),
sfc: resolve('src/sfc')
}
最终可以确认入口文件为 src/platforms/web/entry-runtime-with-compiler
打开 src/platforms/web/entry-runtime-with-compiler 文件
import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'
import Vue from './runtime/index' // 引入vue
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
const mount = Vue.prototype.$mount // 保存mount方法
// 把mount方法挂载到原型上
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
// 判断el,el不能挂载到body上
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 // 保存传递进来的options
// resolve template/el and convert to render function
if (!options.render) {
......
......
......
}
return mount.call(this, el, hydrating)
}
/**
* Vue为了解决获取DOM元素问题的方法,因为我们获取DOM节点时,很多时候不仅仅要获取该DOM节点里面包括的HTML代码,还** 需要获取他本身,但是当该DOM外层不存在元素包裹,例如文件节点,那么就会返回undefine,所以vue使用这一方法来解决 * 这个问题
*/
function getOuterHTML (el: Element): string {
// 存在最外层元素则直接返回
if (el.outerHTML) {
return el.outerHTML
} else {
// 不存在则先创建一个DIV,然后往DIV中添加el,最终返回的就是DIv包裹着的el代码
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
Vue.compile = compileToFunctions
export default Vue
由此可见该文件主要是在vueu原型上挂载了mount方法以及对el进行判断处理,所以我们根据 import Vue from ‘./runtime/index’ 继续往下寻找Vue实例,打开 ./runtime/index 文件
import Vue from 'core/index' // 引入vue
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'
......
......
......
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
在这个文件中并没有看到Vue实例方法,这里只是对 __patch__方法 和 $mount方法 进行处理,我们继续往下找,打开 src/core/index 文件
import Vue from './instance/index' // 引入vue
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue
由Object.defineProperty可以得知,这个文件主要是处理数据相应,因为Vue数据绑定的原理主要是通过Object.defineProperty进行数据劫持和使用观察者模式,完成发布订阅。既然这里没找到,那我们再看 ./instance/index 文件
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'
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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
Mixin的解说
在这个文件,我们找到了Vue实例对象以及在实例对象上挂载了不同的方法。需要了解上面方法的话打开相对应的文件就行。
- initMixin()
具体代码在 src/core/instance/init.js,主要是在Vue原型上面挂载_init方法,这个方法是Vue一个内部初始方法
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// ... _init 方法的函数体,此处省略
}
}
当我们调用 new Vue() 的时候会执行this._init(options)
- stateMixin()
在这里我们主要看 stateMixin 方法里面这段代码
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function (newData: Object) {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
这里的代码很熟悉,后面两句定义了 d a t a < / f o n t > ∗ ∗ 和 ∗ ∗ < f o n t c o l o r = r e d > data</font>** 和 **<font color=red> data</font>∗∗和∗∗<fontcolor=red>props 两个属性,而这两个属性分别写在了 dataDef 和 propsDef 两个对象上,从上面代码能看到 dataDef.get 和 propsDef.get 分别代理了 _data 和 _props 两个属性,然后就是一个生产环境的判断,当是生产环境时,就为他们设置set,实际上就是想说他们两个都是只读属性
- renderMixin()
export function renderMixin (Vue: Class<Component>) {
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
Vue.prototype._render = function (): VNode {
......
......
......
}
}
上面代码很清楚写着,renderMixin 调用得时候首先执行 installRenderHelpers 方法,这个方法是与该文件同级的 ./render-helpers/index当中,这个方法主要在Vue原型上添加了一系列方法(详情可以去看文件),然后分别往原型上面挂载 $nextTick 和 _render 方法
- eventsMixin()
eventsMixin,字面上意思就可以得知这是一个有关事件处理的方法,在这个方法中主要在原型上面挂载了4个方法
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
- lifecycleMixin()
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
这三个方法想必用Vue的都很熟悉,而lifecycleMixin这个方法主要是把这三个方法挂载到原型上面
从上面的解释可以得出,这里每个Mixin大概都是对Vue.Prototype(Vue原型)的一些包装,在其上面挂载一些属性和方法
Vue生命周期钩子
在讲解之前先放上一张图
我们Vue的生命周期钩子函数严格来讲有10个:
- beforeCreate
- created
- beforeMount
- mounted
- beforeUpdate
- updated
- beforeDestroy
- destroyed
- activated
- deactivate
大家都应该很了解这几个函数的作用,现在我们从源码角度上总结他们的调用时机:
- beforeCreate
在寻找Vue实例时我们就发现,在new Vue()创建实例开始,执行this._init()方法的时候,初始化生命周期,各种事件和渲染,
接着调用beforeCreate,这时有关组件、数据的属性还没初始化,在这个阶段获取这些是无法成功的
// src/core/instance/init.js 第52行 ~ 55行
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
- created
调用beforeCreate之后,继续往属性注入、状态等等,然后调用created,这是数据可以被访问但是页面还没开始渲染,在这一步只适合做一些数据初始化的操作,完成这一步就开始进入页面渲染流程
// src/core/instance/init.js 第56行 ~ 59行
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
- beforeMount
页面渲染流程稍微复杂,从代码上看,执行完created函数后紧接着是执行$mount()
// src/core/instance/init.js 第68行 ~ 70行
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
但是 m o u n t 他 是 根 据 平 台 的 不 同 需 求 定 义 的 , 在 w e b 中 , 执 行 mount他是根据平台的不同需求定义的,在web中,执行 mount他是根据平台的不同需求定义的,在web中,执行mount方法的时候开始装载组件,具体内容在 src/platforms/runtime/index.js
// 36 ~ 43 行
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
在这里执行 mountComponent 方法,该方法在最初渲染时就执行了beforeMount,然后调用updateComponent来渲染视图
// src/core/instance/lifecycle.js 第141 ~ 213行
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
......
......
......
callHook(vm, 'beforeMount')
let updateComponent
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)
}
}
......
......
......
}
- mounted
在视图渲染完成时,mounted会被调用,在这个时候观察器系统将监控所有的数据,执行数据更新并重新渲染视图 - beforeUpdate
在观察期系统的作用下,当数据更新时就会调用beforeUpdate函数 - updated
当数据更新并且视图重新渲染完成后就会调用updated - beforeDestroy 与 destroyed
这两个钩子函数执行的是生命周期的最后阶段也就是销毁,在销毁之前执行beforeDestroy清楚所有的数据、引用、观察期、监听器等等,然后执行destroyed宣告生命周期终结 - activated 与 deactivated
这两个钩子函数比较特殊,他是在只有使用keep-alive的组件才有效,分别在组件激活和切换组件时触发,在keep-alive模式中,切换组件时其他钩子函数不会触发,如果需要做操作,这时就需要用到这两个钩子函数
Vue响应式原理
在开始之前先放上官方的一张图
官方文档(深入响应式原理)中有说到:
当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因
响应式主要做了数据劫持、依赖收集、派发更新这三件事:
- 数据劫持:new Vue()的时候遍历data对象,通过Object.defineProperty()给所有属性添加 getter 和 setter
- 依赖收集:渲染的过程中,触发数据 getter ,在 getter 的时候把当前 watcher 对象收集起来
- 派发更新:setter 的时候,遍历这个数据的watcher对象,进行数据更新
数据劫持
当我们new Vue()时,会触发initState方法,在这个方法里
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) { // 存在data对象调用initData方法,不存在对空对象做处理
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
......
......
......
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
在上面这段代码大家可以看到,主要起作用的是 observe(data, true / asRootData /) 这句话,那么现在我们来看下这个方法,src/core/observe/index.js
export class Observer {
......
......
......
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
// 遍历所有的属性并调用defineReactive方法对他们进行处理
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
......
......
......
}
......
......
......
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
......
......
......
Object.defineProperty(obj, key, {
enumerable: true, // 属性可枚举
configurable: true, // 属性可修改删除
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend() // 依赖采集
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify() // 派发更新
}
})
}
defineReactive 这个方法主要是使用 Object.defineProperty 给属性添加 getter 和 setter
收集依赖与派发更新
Vue使用了一个Dep(订阅者),它用来存放我们的观察者对象,当数据发生改变时,就通知观察器,观察器调用自己的update方法完成更新
// Dep在 "src/core/observer/dep.js"
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = [] // 存放依赖对象,即存放观察者
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this) // 在收集依赖得时候会往队列中添加watcher对象
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
// 更新时遍历watche进行更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// watcher在 "src/core/observer/watcher.js"
export default class Watcher {
......
......
......
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
......
......
......
}
虚拟DOM
什么是虚拟DOM?
虚拟DOM简称VDOM,可以把它看作是由javaScript模拟出来DOM结构的属性结构,这个树结构包含整个DOM的结构信息
为什么使用虚拟DOM
虚拟DOM就是为了解决浏览器性能问题而被设计出来的,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制
有关虚拟DOM的文件在 “src/sore/vdom” 里面# 【个人总结】Vue源码解读