liutao / vue2.0-source Goto Github PK
View Code? Open in Web Editor NEWvue源码分析 -- 基于 2.2.6版本
vue源码分析 -- 基于 2.2.6版本
我们学习阅读一个项目的源码时,首先当然要看它的package.json
文件。这里面有项目的依赖,有开发环境、生产环境等编译的启动脚本,有项目的许可信息等。别的暂且不说,直接来看script
。我们发现Vue.js
有许许多多的npm
命令。这里我们只看第一个,也就是npm run dev
所执行的命令。
"dev": "rollup -w -c build/config.js --environment TARGET:web-full-dev"
我们发现这里用到了rollup
,感兴趣的可以去了解一下,这里它并不是重点,我们只需要知道它是一个类似于webpack
的打包工具就行了。往后看我们发现它执行了build/config.js
,前面说过它是一个打包配置的文件,我们看看它里面又做了哪些事儿。
const builds = {
...
...
...
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: path.resolve(__dirname, '../src/entries/web-runtime-with-compiler.js'),
dest: path.resolve(__dirname, '../dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
...
...
...
}
function genConfig (opts) {
...
}
if (process.env.TARGET) {
module.exports = genConfig(builds[process.env.TARGET])
} else {
exports.getBuild = name => genConfig(builds[name])
exports.getAllBuilds = () => Object.keys(builds).map(name => genConfig(builds[name]))
}
直接看下面,我们看到它调用了getConfig(builds[process.env.TARGET])
,getConfig
用于生成rollup
的配置文件。builds
是一个对象,获取它的process.env.TARGET
值,在package.json
中,我们看到dev
中有TARGET:web-full-dev
参数,即上面我留下的那一段配置。这样入口文件我们就找到了,也就是/src/entries/web-runtime-with-compiler.js
。
既然我们找到了入口,我们就从这里开始我们的Vue.js
源码之旅。
打开web-runtime-with-compiler.js
文件,在该文件的第一行,我们看到如下代码:
import Vue from './web-runtime'
即引入了同一目录下的另一文件,本文件中我们只不过在'./web-runtime'导出的Vue
对象上进行了二次加工而已。
打开web-runtime
,看第一行代码我们就知道,该文件同理是在'core/index'导出的Vue
对象上进行了加工。
再次打开core/index
,发现它又是在'./instance/index'上进行加工的。这也是为什么打包后的文件内,最终返回的是Vue$3
。整个过程是这样的:
/src/entries/web-runtime-with-compiler.js --> /src/entries/web-runtime.js -->
/src/core/index.js --> /src/core/instance/index.js
历经千辛万苦,终于找到了定义Vue
对象的所在之处。它的构造函数及其简单:
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)
}
首先判断如果是生产环境,且不是通过new
关键字来创建对象的话,就在控制台打印一个warning
,之后调用了this._init(options)
函数。
下面的几个函数,分别在Vue.prototype
原型上绑定了一些实例方法。关于Vue
的静态方法和实例方法,我分别单列出来,这样看起来可以更加清晰。
// _init
initMixin(Vue)
// $set、$delete、$watch
stateMixin(Vue)
// $on、$once、$off、$emit
eventsMixin(Vue)
// _update、$forceUpdate、$destroy
lifecycleMixin(Vue)
// $nextTick、_render、以及多个内部调用的方法
renderMixin(Vue)
我们沿着刚才所提到的文件引入顺序一步步来看。 /src/core/instance/index.js
执行之后,是/src/core/index.js
文件。
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Vue.version = '__VERSION__'
该文件也很简单,首先调用了initGlobalAPI
,引自/src/core/global-api/index
。
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
Vue.options = Object.create(null)
// Vue.options.components、Vue.options.directives、Vue.options.filters
config._assetTypes.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
// Vue.options._base
Vue.options._base = Vue
// Vue.options.components.KeepAlive
extend(Vue.options.components, builtInComponents)
// Vue.use
initUse(Vue)
// Vue.mixin
initMixin(Vue)
// Vue.extend
initExtend(Vue)
// Vue.component、Vue.directive、Vue.filter
initAssetRegisters(Vue)
}
从上面的代码可以看出,它是给Vue
对象添加了一些静态方法和属性。我们之后在Vue
的静态方法中详细分析。
/src/core/index.js
文件中,还添加了一个Vue.prototype[$isServer]
属性,用于判断是不是服务端渲染,还有一个就是Vue.version
。
接着是/src/entries/web-runtime.js
。
// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// 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
代码整体上可以分为两个平台,一个是我们常用的web
,另一个是weex
。所以源码里把两个平台不同的内容单独提取出来了。这里我们只谈web
。
首先,在Vue.config
上添加了几个平台相关的方法,扩展了Vue.options.directives
(model
和show
)和Vue.options.components
(Transition
和TransitionGroup
)。在Vue.prototype
上添加了__patch__
(虚拟dom相关)和$mount
(挂载元素)。
最后是/src/entries/web-runtime-with-compiler.js
,该文件主要干了两件事,一个是定义了一个方法Vue.prototype.$mount
,另一个是将compileToFunctions
挂在到Vue.compile
上。
以上,简单且有点儿啰嗦的大体上讲了一下Vue
源码的结构,接下来,我们从一个小栗子入手。看看Vue
从创建对象,到挂载,到修改都分别经历了什么。
该文件主要是为了方便列出直接绑定在Vue
上的静态方法和静态属性都有哪些,以及绑定的位置,每个方法的作用是什么。
// src/core/index.js
Vue.version = '__VERSION__'
// src/entries/web-runtime-with-compiler.js
Vue.compile = compileToFunctions // 把模板template转换为render函数
// src/core/global-api 在目录结构中,我们指出,Vue的静态方法大多都是在该文件夹中定义的
// src/core/global-api/index.js
Vue.config //不过以直接替换整个config对象
Vue.util //几个工具方法,但是官方不建议使用
Vue.set
Vue.delete
Vue.nextTick
Vue.options = {
components: {KeepAlive: KeepAlive}
directives: {},
filters: {},
_base: Vue
}
// src/core/global-api/use.js
Vue.use
// src/core/global-api/mixin.js
Vue.mixin
// src/core/global-api/extend.js
Vue.extend
// src/core/global-api/assets.js
Vue.component
Vue.directive
Vue.filter
我的期望是有时间每个方法的具体实现都进行分析,慢慢来。
怎么把小栗子运行,怎么debug调式的啊?
既然是源码分析,所以大家最好对着源码,一步一步来看。本篇文章,旨在通过一个简单的小栗子,带着大家从vm
创建,到显示到页面上都经历了哪些过程。
例子如下:
<div id="app">
<p>{{message}}</p>
</div>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
message: '第一个vue实例'
}
})
</script>
创建对象,当然要从构造函数看起,构造函数在src/core/instance/index.js
中。
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)
}
我们看到,它首先判断了是不是通过new
关键词创建,然后调用了this._init(options)
。_init
函数是在src/core/instance/init.js
中添加的。我们先把整个函数都拿出来,然后看看每一步都做了什么。
this._init
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
// 性能统计相关
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-init:${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) {
// 内部使用Vnode部分使用
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')
// 性能相关
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
首先一进来,我们给当前vm
添加了一个唯一的_uid
,然后vm._isVue
设为true
(监听对象变化时用于过滤vm)。
因为我们自己传入的参数中,理论上不会有_isComponent
。所以我们的小栗子就直接走到了else
里面。mergeOptions
用于合并两个对象,不同于Object.assign
的简单合并,它还对数据还进行了一系列的操作,且源码中多处用到该方法,所以后面会详细讲解这个方法。在这之前,我们先看看resolveConstructorOptions
都做了什么。
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
// 有super属性,说明Ctor是通过Vue.extend()方法创建的子类
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions
// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
这里的Ctor
就是vm.constructor
也就是Vue
对象,在上一篇文章中,其实我们提到过,在/src/core/global-api/index
文件中,我们给Vue
添加了一些全局的属性或方法。
Vue.options = Object.create(null)
// Vue.options.components、Vue.options.directives、Vue.options.filters
config._assetTypes.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// Vue.options._base
Vue.options._base = Vue
// Vue.options.components.KeepAlive
extend(Vue.options.components, builtInComponents)
所以,这里打印一下Ctor.options
,如下所示:
Ctor.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
}
Ctor.super
是在调用Vue.extend
时,才会添加的属性,这里先直接跳过。所以mergeOptions
的第一个参数就是上面的Ctor.options
,第二个参数是我们传入的options
,第三个参数是当前对象vm
。
mergeOptions
本来打算在此处详细讲解一下mergeOptions
,但写着写着发现内容太多。有点偏离主题,所以这里还是基于前面的例子,简单说一下,另起一篇文章详细讲解Vue
的合并策略。
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
// 如果有options.components,则判断是否组件名是否合法
checkComponents(child)
}
// 格式化child的props
normalizeProps(child)
// 格式化child的directives
normalizeDirectives(child)
// options.extends
const extendsFrom = child.extends
if (extendsFrom) {
parent = typeof extendsFrom === 'function'
? mergeOptions(parent, extendsFrom.options, vm)
: mergeOptions(parent, extendsFrom, vm)
}
// options.mixins
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
let mixin = child.mixins[i]
if (mixin.prototype instanceof Vue) {
mixin = mixin.options
}
parent = mergeOptions(parent, mixin, vm)
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
前面和components
、props
、directives
、extends
、mixins
相关的内容我们暂且忽略,我们知道Vue
提供了配置optionMergeStrategies
对象,来让我们手动去控制属性的合并策略,这里的strats[key]
就是key
属性的合并方法。
function mergeAssets (parentVal: ?Object, childVal: ?Object): Object {
const res = Object.create(parentVal || null)
return childVal
? extend(res, childVal)
: res
}
config._assetTypes.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
_assetTypes
就是components
、directives
、filters
,这三个的合并策略都一样,这里我们都返回了parentVal
的一个子对象。
data
属性的合并策略,是也是Vue
内置的,如下:
function mergeData (to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
const keys = Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) {
set(to, key, fromVal)
} else if (isPlainObject(toVal) && isPlainObject(fromVal)) {
mergeData(toVal, fromVal)
}
}
return to
}
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (!childVal) {
return parentVal
}
if (typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
if (!parentVal) {
return childVal
}
return function mergedDataFn () {
return mergeData(
childVal.call(this),
parentVal.call(this)
)
}
} else if (parentVal || childVal) {
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm)
: undefined
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
这里vm
且data
都不为空,所以会走到else if
,返回的是mergedInstanceDataFn
方法。关于mergedInstanceDataFn
方法,我们都知道,子组件中定义data
时,必须是一个函数,这里简单的判断了是函数就执行,不是就返回自身的值。然后通过mergeData
去合并,其实就是递归把defaultData
合并到instanceData
,并观察。
最后合并之后的vm.$option
如下:
vm.$option = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue,
el: '#app',
data: function mergedInstanceDataFn(){}
}
回到我们的_init
接着放下看,之后如果是开发环境,则vm._renderProxy
值为一个Proxy
代理对象,生产环境就是vm
自身,这里不展开赘述。
接着就是一系列的操作,我们一个一个来看。
initLifecycle(vm)
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
该方法比较简单,主要就是给vm
对象添加了$parent
、$root
、$children
属性,以及一些其它的声明周期相关的标识。
options.abstract
用于判断是否是抽象组件,组件的父子关系建立会跳过抽象组件,抽象组件比如keep-alive
、transition
等。所有的子组件$root
都指向顶级组件。
initEvents(vm)
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
该方法给vm
添加了一些事件相关的属性,我们本例中没有添加事件,暂时跳过。
initRender(vm)
export function initRender (vm: Component) {
vm.$vnode = null
vm._vnode = null
vm._staticTrees = null
const parentVnode = vm.$options._parentVnode
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
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)
}
这里给vm
添加了一些虚拟dom、slot
等相关的属性和方法。
然后会调用beforeCreate
钩子函数。
initInjections(vm)
和initProvide(vm)
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
export function initInjections (vm: Component) {
const inject: any = vm.$options.inject
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
// isArray here
const isArray = Array.isArray(inject)
const keys = isArray
? inject
: hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const provideKey = isArray ? key : inject[key]
let source = vm
while (source) {
if (source._provided && provideKey in source._provided) {
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, source._provided[provideKey], () => {
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, source._provided[provideKey])
}
break
}
source = source.$parent
}
}
}
}
这两个配套使用,用于将父组件_provided
中定义的值,通过inject
注入到子组件,且这些属性不会被观察。简单的例子如下:
<div id="app">
<p>{{message}}</p>
<child></child>
</div>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
message: '第一个vue实例'
},
components: {
child: {
template: "<div>{{a}}</div>",
inject: ['a']
}
},
provide: {
a: 'a'
}
})
</script>
initState(vm)
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) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch) initWatch(vm, opts.watch)
}
这里主要就是操作数据了,props
、methods
、data
、computed
、watch
,从这里开始就涉及到了Observer
、Dep
和Watcher
,网上讲解双向绑定的文章很多,之后我也会单独去讲解这一块。而且,这里对数据操作也比较多,在讲完双向绑定的内容后,我们再结合没讲完的mergeOptions
详细看看Vue
对我们传入的数据都进行了什么操作。
到这一步,我们看看我们的vm
对象变成了什么样:
// _init
vm._uid = 0
vm._isVue = true
vm.$options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue,
el: '#app',
data: function mergedInstanceDataFn(){}
}
vm._renderProxy = vm
vm._self = vm
// initLifecycle
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
// initEvents
vm._events = Object.create(null)
vm._hasHookEvent = false
// initRender
vm.$vnode = null
vm._vnode = null
vm._staticTrees = null
vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
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)
// 在 initState 中添加的属性
vm._watchers = []
vm._data
vm.message
然后,就会调用我们的created
钩子函数。
我们看到create
阶段,基本就是对传入数据的格式化、数据的双向绑定、以及一些属性的初始化。
$mount
打开src/entries/web-runtime-with-compiler.js
。
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
const options = this.$options
// resolve template/el and convert to render function
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) {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
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
}
}
首先,通过mount = Vue.prototype.$mount
保存之前定义的$mount
方法,然后重写。
这里的query
可以理解为document.querySelector
,只不过内部判断了一下el
是不是字符串,不是的话就直接返回,所以我们的el
也可以直接传入dom元素。
之后判断是否有render
函数,如果有就不做处理直接执行mount.call(this, el, hydrating)
。如果没有render
函数,则获取template
,template
可以是#id
、模板字符串、dom元素,如果没有template
,则获取el
以及其子内容作为模板。
compileToFunctions
是对我们最后生成的模板进行解析,生成render
。这里的内容也比较多,简单说一下:
该方法创建的地方在src/compiler/index.js
的createCompiler
中。
function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
optimize(ast, options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
export function createCompiler (baseOptions: CompilerOptions) {
const functionCompileCache: {
[key: string]: CompiledFunctionResult;
} = Object.create(null)
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
...
const compiled = baseCompile(template, finalOptions)
...
return compiled
}
function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = options || {}
...
// compile
const compiled = compile(template, options)
...
return (functionCompileCache[key] = res)
}
return {
compile,
compileToFunctions
}
}
compileToFunctions
中调用了compile
,compile
中调用了baseCompile
。主要的操作就是baseCompile
中的三步。
第一步, const ast = parse(template.trim(), options)
。这里是解析template
,生成ast。我们的例子生成的ast
如下:
{
type: 1,
tag: 'div',
plain: false,
parent: undefined,
attrs: [{name:'id', value: '"app"'}],
attrsList: [{name:'id', value: 'app'}],
attrsMap: {id: 'app'},
children: [{
type: 1,
tag: 'p',
plain: true,
parent: ast,
attrs: [],
attrsList: [],
attrsMap: {},
children: [{
expression: "_s(message)",
text: "{{message}}",
type: 2
}]
}
第二步,optimize(ast, options)
主要是对ast进行优化,分析出静态不变的内容部分,增加了部分属性:
{
type: 1,
tag: 'div',
plain: false,
parent: undefined,
attrs: [{name:'id', value: '"app"'}],
attrsList: [{name:'id', value: 'app'}],
attrsMap: {id: 'app'},
static: false,
staticRoot: false,
children: [{
type: 1,
tag: 'p',
plain: true,
parent: ast,
attrs: [],
attrsList: [],
attrsMap: {},
static: false,
staticRoot: false,
children: [{
expression: "_s(message)",
text: "{{message}}",
type: 2,
static: false
}]
}
因为我们这里只有一个动态的{{message}}
,所以static
和staticRoot
都是false
。
最后一步,code = generate(ast, options)
,就是根据ast生成render
函数和staticRenderFns
数组。
最后生成的render
如下:
render = function () {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v(_s(message))])])}
}
在src/core/instance/render.js
中,我们曾经添加过如下多个函数,这里和render
内返回值调用一一对应。
Vue.prototype._o = markOnce
Vue.prototype._n = toNumber
Vue.prototype._s = _toString
Vue.prototype._l = renderList
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = renderStatic
Vue.prototype._f = resolveFilter
Vue.prototype._k = checkKeyCodes
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
这里的staticRenderFns
目前是一个空数组,其实它是用来保存template
中,静态内容的render
,比如我们把例子中的模板改为:
<div id="app">
<p>这是<span>静态内容</span></p>
<p>{{message}}</p>
</div>
staticRenderFns
就会变为:
staticRenderFns = function () {
with(this){return _c('p',[_v("这是"),_c('span',[_v("静态内容")])])}
}
从上面的内容,我们可以知道其实template
最终还是转换为render
函数,这也是官方文档中所说的render
函数更加底层。
前面保存了mount = Vue.prototype.$mount
,最后又调用了mount
方法,我们来看看它干了什么。
打开src/entries/web-runtime.js
。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
mountComponent
这里仅仅是返回了mountComponent
的执行结果,跟着代码的步伐,我们又回到了src/core/instance/lifecycle.js
。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
...
callHook(vm, 'beforeMount')
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
上面的代码我简单的做了一些精简。可以看到首先调用了beforeMount
钩子函数,新建了一个Watcher
对象,绑定在vm._watcher
上,之后就是判断如果vm.$vnode == null
,则设置vm._isMounted = true
并调用mounted
钩子函数,最后返回vm
对象。
感觉上似乎有头没尾似得。这里就又不得不提Watcher
了,先简单概述一下。
打开src/core/observer/watcher.js
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
...
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
if (this.user) {
try {
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
}
} else {
value = this.getter.call(vm, vm)
}
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
在构造函数中,我们会把expOrFn
也就是updateComponent
赋值给this.getter
,并且在获取this.value
的值时会调用this.get()
,这里的this.lazy
默认值是false
,在computed
属性中创建的Watcher
会传入true
。
在this.get()
中,我们会调用this.getter
,所以上面的例子中,updateComponent
方法会被调用,所以接下来沿着updateComponent
再一路找下去。
vm._render
updateComponent
中调用了vm._render()
函数,该方法在src/core/instance/render.js
中。
Vue.prototype._render = function (): VNode {
const vm: Component = this
const {
render,
staticRenderFns,
_parentVnode
} = vm.$options
...
if (staticRenderFns && !vm._staticTrees) {
vm._staticTrees = []
}
vm.$vnode = _parentVnode
// render self
let vnode
vnode = render.call(vm._renderProxy, vm.$createElement)
...
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
在该方法中,其实主要就是调用了vm.$options.render
方法,我们再拿出render
方法,看看它都干了什么。
render = function () {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v(_s(message))])])}
}
函数调用过程中的this
,是vm._renderProxy
,是一个Proxy
代理对象或vm
本身。我们暂且把它当做vm
本身。
_c
是(a, b, c, d) => createElement(vm, a, b, c, d, false)
。我们简单说一下createElement
干了什么。a
是要创建的标签名,这里是div
。接着b
是data
,也就是模板解析时,添加到div
上的属性等。c
是子元素数组,所以这里又调用了_c
来创建一个p
标签。
_v
是createTextVNode
,也就是创建一个文本结点。_s
是_toString
,也就是把message
转换为字符串,在这里,因为有with(this)
,所以message
传入的就是我们data
中定义的第一个vue实例
。
所以,从上面可以看出,render
函数返回的是一个VNode
对象,也就是我们的虚拟dom对象。它的返回值,将作为vm._update
的第一个参数。我们接着看该函数,返回src/core/instance/lifecycle.js
vm._update
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
} else {
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
}
}
从mountComponent
中我们知道创建Watcher
对象先于vm._isMounted = true
。所以这里的vm._isMounted
还是false
,不会调用beforeUpdate
钩子函数。
下面会调用vm.__patch__
,在这一步之前,页面的dom还没有真正渲染。该方法包括真实dom的创建、虚拟dom的diff修改、dom的销毁等,具体细节且等之后满满分析。
至此,一个Vue
对象的创建到显示到页面上的流程基本介绍完了。有问题欢迎吐槽~
vm.constructor = {
}
// 每个vue示例的id
vm._uid = 0
// 标示是否是vue的实例
vm._isVue = true
// 自身
vm._self = vm
// 调试时的名称
vm._name = 'name'
//如果有原生的Proxy对象,则返回代理对象,否则是vm本身
vm._renderProxy = vm || Proxy
/**
* 以下是和生命周期相关的属性
**/
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
// 父结点
vm.$parent = parent
// 根结点
vm.$root = parent ? parent.$root : vm
// 子节点
vm.$children = []
vm.$refs = {}
/**
* 以下是和事件相关的属性
**/
vm._events = Object.create(null)
vm._hasHookEvent = false
/**
* 以下是和render相关的属性
**/
vm.$vnode = null
vm._vnode = null
vm._staticTrees = null
vm.$slots = {}
vm.$scopedSlots = {}
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)
/**
* 以下是和数据相关相关的属性
**/
vm._watchers = []
vm._props = {}
vm._data = {}
vm._computedWatchers
vm.$options = {
}
vm.$mount = function(){
}
vue源码根目录下有很多文件夹,下面先列出我知道的几个,后续会补充。
Vue
|-- build 打包相关的配置文件,其中最重要的是config.js。主要是根据不同的入口,打包为不同的文件。
|-- dist 打包之后文件所在位置
|-- examples 部分示例
|-- flow 因为Vue使用了Flow来进行静态类型检查,这里定义了声明了一些静态类型
|-- packages vue还可以分别生成其它的npm包
|-- src 主要源码所在位置
|-- compiler 模板辨析解析的相关文件
|-- core 核心代码
|-- components 全局的组件,这里只有keep-alive
|-- global-api 全局方法,也就是添加在Vue对象上的方法,比如Vue.use,Vue.extend,,Vue.mixin等
|-- instance 实例相关内容,包括实例方法,生命周期,事件等
|-- observer 双向数据绑定相关文件
|-- util 工具方法
|-- vdom 虚拟dom相关
|-- entries 入口文件,也就是build文件夹下config.js中配置的入口文件。看源码可以从这里看起
|-- platforms 平台相关的内容,分为web和weex
|-- server 服务端渲染相关
|-- sfc 暂时未知
|-- shared 共享的工具方法
|-- test 测试用例
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.