vue簡(jiǎn)介和初始化過程
vue的源碼結(jié)構(gòu)如下
src
├── compiler # 編譯相關(guān)
├── core # 核心代碼
├── platforms # 不同平臺(tái)的支持
├── server # 服務(wù)端渲染
├── sfc # .vue 文件解析
├── shared # 共享代碼
Vue對(duì)象
在使用vue時(shí)我們知道都是使用new Vue()
,來(lái)將vue的實(shí)例掛載到dom對(duì)象上從而運(yùn)用數(shù)據(jù)驅(qū)動(dòng)的方式來(lái)擴(kuò)展我們的代碼款违,我們首先來(lái)看一下Vue的定義
從源頭上看來(lái)自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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
在js中一切皆函數(shù)虐秋,其實(shí)Vue就是一個(gè)函數(shù)汉矿,在初始化的時(shí)候執(zhí)行原型鏈上的_init方法,vue沒有把所有的方法都寫在函數(shù)內(nèi)部般此,這樣從代碼上來(lái)說,每次實(shí)例化的時(shí)候不會(huì)生成重復(fù)的代碼
主要還是代碼結(jié)構(gòu)更清晰充边,利用mixin的概念唁毒,把每個(gè)模塊都抽離開,這樣代碼在結(jié)構(gòu)和擴(kuò)展性都有很大提高骡显,這里的每個(gè)mixin先不說疆栏,先看以一下整體結(jié)構(gòu),這里定義完還要被core里的index.js再次包裝調(diào)用initGlobalAPI(Vue)
來(lái)初始化全局的api方法惫谤,在web下runtime文件夾下引用再次封裝壁顶,vue是分為運(yùn)行時(shí)可編譯和只運(yùn)行的版本,所以如果需要編譯溜歪,在Vue原型上添加了$mount方法若专,先來(lái)看一下initGlobalAPI
,在instance中都是在原型鏈上擴(kuò)展方法,在這里是直接在Vue上擴(kuò)展靜態(tài)方法
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
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
Vue.options = Object.create(null)
ASSET_TYPES.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
extend(Vue.options.components, builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
數(shù)據(jù)雙向綁定
我們?cè)谑褂胿ue時(shí)看一個(gè)最簡(jiǎn)單的例子
<div class="root">
{{msg}}
</div>
var root = new Vue({
el: '#root',
data: {
msg: 'hello'
}
})
我們可以看到這樣簡(jiǎn)單的幾行就可以將數(shù)據(jù)綁定到dom對(duì)象上蝴猪,那內(nèi)部做了什么呢调衰,先來(lái)看一下前面說的膊爪,在實(shí)例化會(huì)調(diào)用原型鏈上的_init方法
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)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
這個(gè)方法包括合并配置,初始化生命周期嚎莉,時(shí)間米酬,渲染,data趋箩,props等赃额,最后判斷有沒有傳入的el執(zhí)行$mount方法掛載到dom對(duì)象上
看一下compile版本的mount方法,先將之前定義的mount的方法緩存后邊使用叫确,判斷有沒有render方法跳芳,vue也可以寫類似react的render函數(shù),
如果沒有的話启妹,判斷有沒有模版字符串筛严,獲取模版字符串,或者直接獲取html字符串使用compileToFunctions
編譯模版饶米,編譯好了之后主要是為了生成
render方法桨啃,無(wú)論使用單文件組件還是模版方法還是之前demo中的方法都必須經(jīng)過render方法,最后執(zhí)行之前緩存的mount方法檬输,這個(gè)方法是在runtime的index.js中定義的
這個(gè)實(shí)際上就是在執(zhí)行生命周期中的mount周期照瘾,mountComponent
方法,我刪除了一下在dev環(huán)境的處理丧慈,這不影響邏輯析命,可以看出mount方法主要就是定義了一個(gè)Watcher來(lái)相應(yīng)組件的變化
執(zhí)行了初始化的一些生命周期,render方法主要就是用來(lái)根據(jù)模版字符串來(lái)生成虛擬dom元素逃默,類似react的jsx編譯成ReactElmemnt的鹃愤,可以看出兩個(gè)庫(kù)從設(shè)計(jì)的基礎(chǔ)架構(gòu)還是很像的
var mount = Vue.prototype.$mount; // 緩存mount方法
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && query(el);
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
warn(
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
);
return this
}
var options = this.$options;
// resolve template/el and convert to render function
if (!options.render) { // 如果沒有render方法
var template = options.template;
if (template) { // 如果存在template模版文件
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template);
/* istanbul ignore if */
if (!template) {
warn(
("Template element not found or is empty: " + (options.template)),
this
);
}
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
{
warn('invalid template option:' + template, this);
}
return this
}
} else if (el) { //存在el直接獲取當(dāng)前dom下的html字符串模版
template = getOuterHTML(el);
}
if (template) {
/* istanbul ignore if */
if (config.performance && mark) {
mark('compile');
}
var ref = compileToFunctions(template, {// 編譯模版
outputSourceRange: "development" !== 'production',
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this);
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
options.render = render;
options.staticRenderFns = staticRenderFns;
/* istanbul ignore if */
if (config.performance && mark) {
mark('compile end');
measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
}
}
}
return mount.call(this, el, hydrating) // 最后執(zhí)行原先定義的mount方法
};
function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode // 創(chuàng)建一個(gè)空的vnode節(jié)點(diǎn)
if (process.env.NODE_ENV !== 'production') {
}
}
callHook(vm, 'beforeMount') // 執(zhí)行生命周期方法
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating) // 執(zhí)行原型鏈上的_update方法,_render()生成虛擬dom完域,進(jìn)行虛擬dom的比較
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
vue的虛擬dom
vue虛擬dom的實(shí)現(xiàn)借鑒了snabbdom软吐,增加了一些vue特有的屬性,我們一起來(lái)看一下vdom下的vnode.js文件
下邊是vnode的數(shù)據(jù)結(jié)構(gòu)吟税,可以看出相比snabbdom增加了很多凹耙,并且提供了一些創(chuàng)建vnode元素的方法
class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
}
來(lái)看一看是怎么通過render生成vdom的,實(shí)際上是通過_createElement生成的可以看到肠仪,先對(duì)傳入的一些參數(shù)進(jìn)行一些校驗(yàn)肖抱,對(duì)chilren進(jìn)行規(guī)范化使其是vnode的數(shù)組
然后根據(jù)tag進(jìn)行判斷,是原生標(biāo)簽直接生成vnode异旧,已注冊(cè)的組件通過component生成vnode意述,返回vnode
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__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) { // 把children格式化成vnode數(shù)組
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)) { // 判斷是不是一些內(nèi)置的div,span原生標(biāo)簽
// platform built-in elements
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // 判斷是不是已經(jīng)注冊(cè)的組件標(biāo)簽
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
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()
}
}
看到vnode的生成過程再來(lái)看看update這個(gè)方法在初次渲染和數(shù)據(jù)更新會(huì)執(zhí)行,并對(duì)dom進(jìn)行操作,這里比較重要的是patch方法荤崇,
我們通過源碼最終能找到這個(gè)代碼是在web下runtime目錄index.js下,由于不同平臺(tái)實(shí)現(xiàn)不一樣镐依,在瀏覽器中是在vdom中的patch.js中createPatchFunction,我們?cè)趕nabbdom中的patch也是根據(jù)依賴的hook生成的天试,在vue中也是基本的思路,在runtime中傳入依賴的一些模塊傳入的兩個(gè)參數(shù)然低,最終返回一個(gè)patch方法喜每,這個(gè)放會(huì)根據(jù)新舊vnode進(jìn)行diff算法,簡(jiǎn)單來(lái)說就是第一次生成直接創(chuàng)建新的元素雳攘,如果新舊的key带兜,tag,isComment相等就執(zhí)行patchVnode來(lái)更新dom和vnode吨灭,否則就刪除old創(chuàng)建新的到old的位置刚照,這和react的diff算法思想基本一致,只不過實(shí)現(xiàn)不一樣最后return真實(shí)的dom
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el // 真實(shí)dom
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm) // 生成還原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 { // 數(shù)據(jù)更新時(shí)
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) { // 更新真實(shí)dom上對(duì)虛擬dom的指向
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
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend // 依賴的模塊和dom操作api
for (i = 0; i < hooks.length; ++i) { // 根據(jù)定義好的hooks將依賴中的方法添加到cbs中去方便在不同時(shí)期執(zhí)行不同方法
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
// ...省略
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
最后盜一張圖看一下整個(gè)流程