前言
本系列将用七天时间,带你分析一波 Vue2 源码,基于 Vue 2.6.11 版本。文章内容基于个人学习总结,同时也大量借鉴了大佬们的资料,引用处均会注释说明并给出传送门。
水平一般,能力有限。如对源码理解有误,欢迎指正,万分感谢。
准备
环境准备 💻
git clone https://github.com/vuejs/vue.gitcd vue & npm installnpm run dev- 创建
index.html并引入dist/vue.js文件
建议在 dev 命令中添加 --sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
目录结构 📁
src
├─shared # 浏览器 Browser 和服务端 SSR 共享的代码
├─sfc # 解析单文件组件
├─server # 服务端 SSR 代码
├─platforms # 平台支持 Web / Weex
├─core # Vue 核心
├─compiler # 编译器
入口文件 🚪
在根目录中找到 script/config.js 文件,该文件包括各个平台的编译参数,目前我们基于浏览器去调试源码,所以找到 Runtime+compiler development build (Browser) 对应的配置。resolve('web/entry-runtime-with-compiler.js') 其实对应就是 src/platforms/web/entry-runtime-with-compiler.js 文件,这就是携带编译器 Browser 版本的入口。
const builds = {
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
'web-runtime-cjs-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.dev.js'),
format: 'cjs',
env: 'development',
banner
},
'web-runtime-cjs-prod': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.prod.js'),
format: 'cjs',
env: 'production',
banner
},
// Runtime+compiler CommonJS build (CommonJS)
'web-full-cjs-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.dev.js'),
format: 'cjs',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
'web-full-cjs-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.prod.js'),
format: 'cjs',
env: 'production',
alias: { he: './entity-decoder' },
banner
},
// Runtime only ES modules build (for bundlers)
'web-runtime-esm': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.esm.js'),
format: 'es',
banner
},
// Runtime+compiler ES modules build (for bundlers)
'web-full-esm': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.js'),
format: 'es',
alias: { he: './entity-decoder' },
banner
},
// Runtime+compiler ES modules build (for direct import in browser)
'web-full-esm-browser-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.browser.js'),
format: 'es',
transpile: false,
env: 'development',
alias: { he: './entity-decoder' },
banner
},
// Runtime+compiler ES modules build (for direct import in browser)
'web-full-esm-browser-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.browser.min.js'),
format: 'es',
transpile: false,
env: 'production',
alias: { he: './entity-decoder' },
banner
},
// runtime-only build (Browser)
'web-runtime-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.js'),
format: 'umd',
env: 'development',
banner
},
// runtime-only production build (Browser)
'web-runtime-prod': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.min.js'),
format: 'umd',
env: 'production',
banner
},
// 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
},
// Runtime+compiler production build (Browser)
'web-full-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.min.js'),
format: 'umd',
env: 'production',
alias: { he: './entity-decoder' },
banner
},
// Web compiler (CommonJS).
'web-compiler': {
entry: resolve('web/entry-compiler.js'),
dest: resolve('packages/vue-template-compiler/build.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies)
},
// Web compiler (UMD for in-browser use).
'web-compiler-browser': {
entry: resolve('web/entry-compiler.js'),
dest: resolve('packages/vue-template-compiler/browser.js'),
format: 'umd',
env: 'development',
moduleName: 'VueTemplateCompiler',
plugins: [node(), cjs()]
},
// Web server renderer (CommonJS).
'web-server-renderer-dev': {
entry: resolve('web/entry-server-renderer.js'),
dest: resolve('packages/vue-server-renderer/build.dev.js'),
format: 'cjs',
env: 'development',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
'web-server-renderer-prod': {
entry: resolve('web/entry-server-renderer.js'),
dest: resolve('packages/vue-server-renderer/build.prod.js'),
format: 'cjs',
env: 'production',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
'web-server-renderer-basic': {
entry: resolve('web/entry-server-basic-renderer.js'),
dest: resolve('packages/vue-server-renderer/basic.js'),
format: 'umd',
env: 'development',
moduleName: 'renderVueComponentToString',
plugins: [node(), cjs()]
},
'web-server-renderer-webpack-server-plugin': {
entry: resolve('server/webpack-plugin/server.js'),
dest: resolve('packages/vue-server-renderer/server-plugin.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
'web-server-renderer-webpack-client-plugin': {
entry: resolve('server/webpack-plugin/client.js'),
dest: resolve('packages/vue-server-renderer/client-plugin.js'),
format: 'cjs',
external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
},
// Weex runtime factory
'weex-factory': {
weex: true,
entry: resolve('weex/entry-runtime-factory.js'),
dest: resolve('packages/weex-vue-framework/factory.js'),
format: 'cjs',
plugins: [weexFactoryPlugin]
},
// Weex runtime framework (CommonJS).
'weex-framework': {
weex: true,
entry: resolve('weex/entry-framework.js'),
dest: resolve('packages/weex-vue-framework/index.js'),
format: 'cjs'
},
// Weex compiler (CommonJS). Used by Weex's Webpack loader.
'weex-compiler': {
weex: true,
entry: resolve('weex/entry-compiler.js'),
dest: resolve('packages/weex-template-compiler/build.js'),
format: 'cjs',
external: Object.keys(require('../packages/weex-template-compiler/package.json').dependencies)
}
}
开始
Demo
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
<div>
<h1>Vue</h1>
</div>
{{ msg }}
</div>
<script>
new Vue({
data: {
msg: 'Hello'
}
}).$mount('#app')
</script>
</body>
</html>
src/platforms/web/entry-runtime-with-compiler.js
入口文件 entry-runtime-with-compiler.js 主要的作用是扩展了原型 $mount 方法,使用 compileToFunctions 将 template 编译成渲染函数。如果使用 entry-runtime 版本则需要预先编译,比如 webpack。
import Vue from './runtime/index'
/**
* 扩展了 src/platforms/web/runtime/index.js - $mount
* 最终执行的是 src/core/instance/lifecycle.js - mountComponent
* 用于判断是否需要添加 render 函数,在浏览器中默认需要通过 compiler 生成一个渲染函数
* runtime-only 版本没有 compiler 编译器,需要预先编译,如 webpack
* 浏览器环境 <script> 应该使用 entry-runtime-with-compiler 版本
* @param el 需要挂载的根结点 #app
* @param hydrating Vue SSR 强制使用激活模式
*/
// 缓存 Vue.prototype.$mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
// 不能挂载 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
// 把 template 或者 el 转换成 render function
if (!options.render) {
// 如果没有写 render () {} 则找 template / el
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')
}
// 调用 src/platforms/web/compiler/index.js - compileToFunctions
// template 编译成渲染函数 => render, staticRenderFns
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
/*
生成的 render
function anonymous() {
with(this){return _c('div',{attrs:{"id":"app"}},[_m(0),_v("\n "+_s(msg)+"\n ")])}
}
*/
console.log(render.toString())
options.staticRenderFns = staticRenderFns
/*
生成的 staticRenderFns 是一个数组,索性代表对应元素在 render 中出现的位置
vnode 节点的 staticRoot 为 true 时(包括其在内的所有子节点全部是静态节点)
如
<div>
<h1>Vue</h1>
</div>
转化成:
[0: fn ...] 0 与 render 中 _m(0) 对应
function anonymous() {
with(this){return _c('div',[_c('h1',[_v("Vue")])])}
}
*/
console.log(staticRenderFns)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 生成好 render 后,最终调用 src/core/instance/lifecycle.js - mountComponent
return mount.call(this, el, hydrating)
}
src/platforms/web/runtime/index.js
对 Vue 添加与 web 平台运行时相关的类属性
import Vue from 'core/index'
// install platform specific utils
// 安装平台相关的工具方法
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// install platform runtime directives & components
// 安装平台相关指令和组件
extend(Vue.options.directives, platformDirectives) // v-show v-model
extend(Vue.options.components, platformComponents) // Transition TransitionGroup
// install platform patch function
// 安装平台的 patch 方法,这里判断 inBrowser 应该是只考虑在 window 拥有 dom 的环境
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
// 被 entry-runtime-with-compiler.js 扩展的 $mount 方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
src/core/index.js
为 Vue 添加全局 API 方法
import Vue from './instance/index'
/*
添加全局类属性 API 方法,包括:
1. Vue.config
2. Vue.set
3. Vue.delete
4. Vue.nextTick
5. Vue.util
6. Vue.observable
7. Vue.options
8. Vue.mixin
9. Vue.use
10. Vue.extend
11. 内置组件 KeepAlive
*/
initGlobalAPI(Vue)
src/core/instance/index.js
这个文件就是 Vue 类的本尊
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) // 添加原型 _init 方法 this._init(options)
stateMixin(Vue) // 添加原型 $data $props $set $delete $watch
eventsMixin(Vue) // 添加原型 $on $once $off $emit
lifecycleMixin(Vue) // 添加原型 _update $forceUpdate $destroy
renderMixin(Vue) // 添加原型 $nextTick _render
export default Vue
src/core/instance/init.js
Vue 类中调用 this._init(options) 方法初始化,主要做的是合并选项、挂载原型属性和执行 $mount 挂载节点。
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
// 每个组件实例的递增 id
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
// 不观测 this 目前我发现的 observe 只观测 props 和 data
vm._isVue = true
// merge options
// 合并传进来的选项
if (options && options._isComponent) {
// 如果是组件场景 Vue.component => Vue.extend()
// 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 {
// 如果是外部调用场景,new Vue()
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) // $parent $root $children $refs
initEvents(vm) // _events
initRender(vm) // _vnode $slots $scopedSlots $createElement
callHook(vm, 'beforeCreate') // 执行生命周期 beforeCreate
initInjections(vm) // resolve injections before data/props
initState(vm) // props methods data computed watch
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 执行生命周期 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)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el) // 挂载节点
}
}
initInternalComponent
src/core/instance/init.js
把 Vue.component({...options}) 中的 options 合并到组件实例上
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
// vm.constructor 是一个通过 extend 产生的 VueComponent 类
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode // 当前组件 vnode created by createComponentInstanceForVnode
opts.parent = options.parent // 父组件实例
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData // 当前组件 props
opts._parentListeners = vnodeComponentOptions.listeners // 当前组件 listener
opts._renderChildren = vnodeComponentOptions.children // 当前组件 children
opts._componentTag = vnodeComponentOptions.tag // 当前组件 tag
// 是否定义了组件的 render
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
mergeOptions
src/core/util/options.js
这个方法里有 strats 对象,它使用策略模式定义了每种属性应该如何合并
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
// 递归把 options 中的 extends 和 mixins 合并到 vm.$options 中
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
// parent 上所有的属性直接合
for (key in parent) {
mergeField(key)
}
// child 里的属性,如果 parent 没有就合并
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
// strats 存储各种属性的合并策略(策略模式)
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
initLifecycle
src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
// 有父组件且不是 keep-alive 或 transition
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm) // 把自己 push 到父组件的 $children
}
vm.$parent = parent // 把父组件赋值给当前组件 $parent
vm.$root = parent ? parent.$root : vm
...
}
initEvents
src/core/instance/events.js
initEvents 最终是执行名为 updateListeners 的方法,该方法位于 src/core/vdom/helpers/update-listeners.js,主要是用于事件的注册更新。
事件分为浏览器原生事件和自定义事件,分别对应不同的处理流程:
- 对于浏览器原生事件,如
@click.native='xxx',是在父组件中处理。 - 对于自定义事件,如
@select='xxx',父组件给子组件的注册事件中,把自定义事件传给子组件,在子组件实例化的时候进行初始化。
总结来说,对于自定义事件,子组件派发,最终还是子组件接收。
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {
let name, def, cur, old, event
// 遍历 Listener 的属性 @select="selectHandler" ===> { select: fn () ... }
for (name in on) {
def = cur = on[name]
old = oldOn[name]
/**
* capture: false
* name: "select"
* once: false
* passive: false
*/
event = normalizeEvent(name) // 解析事件所带的何种修饰符
/* istanbul ignore if */
if (__WEEX__ && isPlainObject(def)) {
cur = def.handler
event.params = def.params
}
if (isUndef(cur)) {
process.env.NODE_ENV !== 'production' && warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else if (isUndef(old)) {
// 如果事件名在 Old Listener 中不存在 则注册
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm) // 返回了一个 invoker function
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
add(event.name, cur, event.capture, event.passive, event.params) // 注册 其实就是调用实例的 $on 方法
} else if (cur !== old) {
// 更新事件名 name 的引用 old.fns 为最新的 cur
old.fns = cur
on[name] = old
}
}
// 遍历 oldOn 移除在 on 中不存在的事件名
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
initRender
src/core/instance/render.js
这个方法在 vm 实例上挂载了 $slots、_c、$createElement,并对 $attrs 和 $listeners 做了响应式处理。首先很有意思的是 _c 和 $createElement 本质都是 createElement,唯一的区别在于第 6 个参数 alwaysNormalize: boolean 不同。
// 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
/**
* 默认编译器已经生成 render 时,如 with(this){return _c('div',[_c('h1',[_v("Vue")])])}
*/
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.
/**
* 一般用于用户手写 render Function 的场景
*/
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
下面我们看一下 alwaysNormalize 这个参数的作用。
跳转到 src/core/vdom/create-element.js 的 createElement 方法,我们发现 alwaysNormalize 的值对应两个常量 const SIMPLE_NORMALIZE = 1 和 const ALWAYS_NORMALIZE = 2,这个常量在下面的 _createElement 中使用,会调用 normalizeChildren 或 simpleNormalizeChildren 去生成 children
simpleNormalizeChildren 的作用其实就是将 vnode 打平成一个数组,让其深度只有一层。
normalizeChildren 的作用是判断 children 是否为 string / number / symbol / boolean 类型其中之一,是则生成一个文本节点数组 [createTextVNode(children)],否则继续调用 simpleNormalizeChildren 打平。
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (normalizationType === ALWAYS_NORMALIZE) {
// 如果 children 是 string / number / symbol / boolean 其中一种则创建 [createTextVNode(children)]
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 打平成数组,深度一层
children = simpleNormalizeChildren(children)
}
}
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/**
* 为了更早的创建 HOC 高阶组件,所以要对 $attrs & $listeners 做响应式处理,以便当使用它们是保持最新
*/
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
...
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
initInjections
src/core/instance/inject.js
首先要解释一个问题,为什么 initInjections initState initProvide 的顺序:
因为 provide 选项注入的值作为 data、props、watch、computed 及 method 入口,inject 选项接收到注入的值有可能被以上这些数据所使用到,所以初始化好 inject 后要调用 initState 初始化好数据,然后才能初始化 provide
export function initInjections (vm: Component) {
/**
* 把子组件中的 inject 转换成对象 inject: ['foo'] => { 'foo': 'bar' }
*/
const result = resolveInject(vm.$options.inject, vm)
if (result) {
/**
* provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
*/
toggleObserving(false) // 通知 defineReactive 不要把 provide / inject 转换成响应式
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key]) // 挂载在实例上 this.foo = 'bar'
}
})
toggleObserving(true)
}
}
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
const provideKey = inject[key].from
let source = vm
/**
* 把 inject 的 key 取出来,然后递归遍历当前组件的 $parent 直至找到提供当前 key 的_provided
*/
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
// 没有找到 就使用默认值
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${key}" not found`, vm)
}
}
}
return result
}
}
initState
src/core/instance/state.js
-
initProps
function initProps (vm: Component, propsOptions: Object) { const propsData = vm.$options.propsData || {} // 传入的 props const props = vm._props = {} // 指向 vm._props 的指针 // cache prop keys so that future props updates can iterate using Array // instead of dynamic object key enumeration. /** * 如果 props 发生变化,使用数组的方式取代动态枚举对象属性迭代 key 可能是处于性能考虑 */ const keys = vm.$options._propKeys = [] // 缓存 props 的 key const isRoot = !vm.$parent // root instance props should be converted // 根组件的 props 已经响应式转化完 why? if (!isRoot) { toggleObserving(false) } for (const key in propsOptions) { keys.push(key) // prop key 加入缓存中 const value = validateProp(key, propsOptions, propsData, vm) // 校验传入的 prop value 类型是否匹配 /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { const hyphenatedKey = hyphenate(key) if (isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey)) { warn( `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`, vm ) } defineReactive(props, key, value, () => { if (!isRoot && !isUpdatingChildComponent) { warn( `Avoid mutating a prop directly since the value will be ` + `overwritten whenever the parent component re-renders. ` + `Instead, use a data or computed property based on the prop's ` + `value. Prop being mutated: "${key}"`, vm ) } }) } else { defineReactive(props, key, value) // 转化响应式 添加到 vm._props } // static props are already proxied on the component's prototype // during Vue.extend(). We only need to proxy props defined at // instantiation here. /** * 判断当前 key vm.key 在实例中是否存在,如果不存在则添加一个代理,vm[key] ===> vm._props[key] */ if (!(key in vm)) { proxy(vm, `_props`, key) } } toggleObserving(true) } -
initProps
function initMethods (vm: Component, methods: Object) { const props = vm.$options.props // 遍历 methods 的 key 如果 value 是函数则 bind 当前 vm 上下文,然后添加到 vm[key] for (const key in methods) { if (process.env.NODE_ENV !== 'production') { if (typeof methods[key] !== 'function') { warn( `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` + `Did you reference the function correctly?`, vm ) } if (props && hasOwn(props, key)) { warn( `Method "${key}" has already been defined as a prop.`, vm ) } if ((key in vm) && isReserved(key)) { warn( `Method "${key}" conflicts with an existing Vue instance method. ` + `Avoid defining component methods that start with _ or $.` ) } } vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm) } } -
initData
function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} // 校验 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 // 遍历 data key while (i--) { const key = keys[i] // data key 不可以和 props 和 methods 中的 key 重名 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) // 添加 key 的代理到 vm _data 以后可以 vm.key 访问 } } // observe data observe(data, true /* asRootData */) // 响应式处理 data key } -
initComputed
function initComputed (vm: Component, computed: Object) { // $flow-disable-line // 定义 _computedWatchers 计算属性相关的 watchers const watchers = vm._computedWatchers = Object.create(null) // computed properties are just getters during SSR const isSSR = isServerRendering() for (const key in computed) { const userDef = computed[key] // 计算属可以是 function 或者包含 get 的对象 const getter = typeof userDef === 'function' ? userDef : userDef.get if (process.env.NODE_ENV !== 'production' && getter == null) { warn( `Getter is missing for computed property "${key}".`, vm ) } if (!isSSR) { // create internal watcher for the computed property. // 为计算属性创建 watcher watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } // component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. // 如果 key 在 vm 上不存在,则为 vm 设置计算属性 if (!(key in vm)) { defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== 'production') { if (key in vm.$data) { warn(`The computed property "${key}" is already defined in data.`, vm) } else if (vm.$options.props && key in vm.$options.props) { warn(`The computed property "${key}" is already defined as a prop.`, vm) } } } } export function defineComputed ( target: any, key: string, userDef: Object | Function ) { const shouldCache = !isServerRendering() // 是否缓存,非 SSR 下是 true // sharedPropertyDefinition 属性描述符 descriptor => Object.defineProperty(obj, prop, descriptor) if (typeof userDef === 'function') { // 如果用户写的 computed 是 function 则调用 createComputedGetter 去定义属性描述符的 get sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef) sharedPropertyDefinition.set = noop } else { // 如果是对象 则取 get sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop sharedPropertyDefinition.set = userDef.set || noop } if (process.env.NODE_ENV !== 'production' && sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function () { warn( `Computed property "${key}" was assigned to but it has no setter.`, this ) } } // 为当前实例 vm 定为 key 的计算属性 Object.defineProperty(target, key, sharedPropertyDefinition) } /** * 使用时 获取计算属性的值 * @param {*} key */ function createComputedGetter (key) { return function computedGetter () { // 取出当前计算属性 key 的 watcher const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // watcher dirty 为 true 证明依赖更新了,要重新计算。 // 依赖更新会触发 watcher update 会把 watcher dirty 设置为 true if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } } -
initWatch
/** * 在 vm 定义 key 的 watcher * @param {*} vm * @param {*} watch */ function initWatch (vm: Component, watch: Object) { for (const key in watch) { const handler = watch[key] if (Array.isArray(handler)) { for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { createWatcher(vm, key, handler) } } }
initProvide
src/core/instance/inject.js
/**
* provide 的初始化就是调用 _provided 如果是函数则执行,如果是其他则直接返回
* @param {*} vm
*/
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
结束
至此 Vue 在初始化阶段的流程就全部结束,下一章,我们来分析一下数据变化相关的源码。