又到了周末,开始梳理Vue源码的第二篇,在这篇中,咱们会讲到初始化Vue过程当中组件化的过程,包括如何建立Vnode的过程。以及搞清楚vm.$vnode
与vm._vnode
的区别和关系。上一篇文章咱们讲到vm.$mount
函数最后执行到了vm._update(vm._render(), hydrating)
那么这一篇咱们将接着上一篇的结尾开始讲。html
vm._update(vm._render(), hydrating)
这个行代码,咱们首先要看的是vm._render()
他到底干了什么。vue
vm._render()
干了什么首先,咱们来到vm._render()
的定义处src/core/instance/render.js
node
export function renderMixin (Vue: Class<Component>) { //Vue类初始化的时候挂载render函数
// install runtime convenience helpers
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode //首次挂载为undefined
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement) //Vnode的具体实现经过vm.$createElement
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it 只能有一个根节点
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out 只能有一个根节
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._render()
是在Vue的renderMixin
过程当中建立的,先不细究每一处代码,咱们能够看到vm._render()
最终返回的就是一个vnode。而后做为vm._update(vnode,hydrafting)
的参数传入。 咱们重点看其中的几个步骤,细节能够暂时忽略:react
一、const { render, _parentVnode } = vm.$options
咱们从vm.$options
拿到了render函数和他的父亲vnode(首次实例化确定是空的);git
二、vm.$vnode = _parentVnode
注释写的很是清楚这个节点可让render函数拿到占位符节点的数据。后续组件建立的时候会讲到是如何拿到占位符节点的数据的,能够这么理解vm.$vnode就是父亲节点,也就是占位符节点,例如element-ui的<el-button></el-button>
就是一个占位符节点。github
三、vnode = render.call(vm._renderProxy, vm.$createElement)
能够看到,实际上vnode是经过vm.$createElement
生成的,而vm.$createElement
也就是咱们常说的h函数:element-ui
new Vue({
render: h => h(App),
}).$mount('#app')
复制代码
至于vm._renderProxy
则是在vm._init()
的时候定义的,开发环境下就是vm自己。api
vm.$createElement
那么接下来咱们来看vm.$createElement
作了什么。代码也在src/core/instance/render.js
,是经过vm._init()
进行的initRender(vm)
来初始化vm.$createElement
。数组
export function initRender (vm: Component) {
//.....some codes
// 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
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.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
//... somecodes
}
复制代码
能够看到vm.$createElement
实际上就是返回了createElement(vm, a, b, c, d, true)
的结果,接下来看一下createElement(vm, a, b, c, d, true)
干了什么。浏览器
createElement
和_createElement
createElement
createElement
定义在src/core/vdom/create-elements.js
下:
export function createElement ( //对_createElement函数的封装
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) { //若是未传入data 则把变量值往下传递
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
复制代码
看到传入的参数你们就会以为很是的熟悉了,就是vue官方文档中的creatElement
参数中定义的那些字段。咱们在这里传入的固然是App.vue导出的这个App对象了,当作是tag传入。能够看到createElement()
方法实际就是若是未传入data参数,就把childer之类的参数所有向下移位,防止参数调用错误。真正的逻辑则在其返回的函数_createElement
中
_createElement
_createElement
相关定义也在src/core/vdom/create-elements.js
下:
export 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 = 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)) {
// 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))) {
// 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()
}
}
复制代码
代码很长,咱们关注几个重点便可:
一、children参数的处理:children参数有两种处理状况,根据传入的参数normalizationType
的值来肯定。若是是ALWAYS_NORMALIZE
调用normalizeChildren(children)
若是是SIMPLE_NORMALIZE
则调用simpleNormalizeChildren(children)
。这两个函数的定义在同级目录的./helper/normolize-children.js
下,这里不细展开来说了,本身看一看就大概了解了,一个是直接concat()
拍平数组,一个就是递归push进一个数组当中:
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
复制代码
二、判断tag类型:一、若是是string类型,则继续判断是不是原生的html标签,若是是,就new Vnode()
,不然该tag是经过vue.component()
或者局部component
注册的组件(后续会讲),则走createComponent()
生成组件vnode;二、若是不是string类型,那么则是相似咱们上面那种状况,传入的是App这个对象,则走createComponent()
方法建立vnode:
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
//生产环境相关
}
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
//未知命名空间
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
复制代码
三、return Vnode没啥好说的。
到这里那么vnode就生成了,那么咱们接下来去看一下createComponent(tag, data, context, children)
都干了些什么。
createComponent()
createComponent()
是一个比较复杂的环节,但愿你们可以反复的琢磨这里面的奥妙,本小白也是在这个地方停留了好久,有很是多的细节须要注意的。 createComponent()
定义在src/core/vdom/create-component.js
中:
export function createComponent (
Ctor: Class<Component> | Function | Object | void, //传入的对象
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
const baseCtor = context.$options._base //就是Vue自己,做为一个基类构造器
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor) // 转化为一个构造器,了解Vue.extend的实现
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
installComponentHooks(data) //
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
}
复制代码
咱们关注其中的几个重点:
Ctor = baseCtor.extend(Ctor)
即 Vue.extend()干了什么baseCtor = context.$options._base
首次渲染的时候,指的就是vue自己,定义在src/core/global-api/index.js
中: Vue.options._base = Vue
那么vue.extend()
到底对咱们的App对象作了什么呢,Vue.extend
接下去定义在src/core/global-api/extend.js
:
Vue.cid = 0
let cid = 1
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this //指向Vue静态方法
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) { //单例模式,若是是复用的组件,那么 直接返回缓存在cachedCtors[SuperId]上的Sub
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name) // 校验component的name属性
}
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype) //原型继承
Sub.prototype.constructor = Sub //再把constructor指回sub
Sub.cid = cid++
Sub.options = mergeOptions( //合并options
Super.options,
extendOptions
)
Sub['super'] = Super //super指向Vue
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}
复制代码
tips:可能不少同窗看到这里会有些晕了,毕竟文章是按顺序往下写的,可能没有那么直观,很容易形成架构的混淆之类的,这里贴一下我整理的层级幕布图,仅供参考,但愿能有所帮助:Vue源码解读
Vue.extend()大体作了如下五件事情。
一、cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
建立该组件实例的构造方法属性,单例模式,优化性能:
if (cachedCtors[SuperId]) { //单例模式,若是是复用的组件,那么 直接返回缓存在cachedCtors[SuperId]上的Sub
return cachedCtors[SuperId]
}
复制代码
二、原型继承模式,建立组件的构造函数:
const Sub = function VueComponent (options) {
this._init(options)//找到Vue的_init
}
Sub.prototype = Object.create(Super.prototype) //原型继承
Sub.prototype.constructor = Sub //再把constructor指回sub
Sub.cid = cid++
Sub.options = mergeOptions( //合并options
Super.options,
extendOptions
)
Sub['super'] = Super //super指向Vue
复制代码
三、提早数据双向绑定,防止patch实例化的时候在此调用object.defineProperty
:
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
复制代码
四、继承Vue父类的全局方法:
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
复制代码
五、缓存对象并返回:
cachedCtors[SuperId] = Sub
return Sub
复制代码
createcomponent()
介绍了如下extend方法,接下来还有一些比较重的地方的,日后会在patch环节重点讲到,这边先将其列出,但愿读者可以略做留意。
一、const propsData = extractPropsFromVNodeData(data, Ctor, tag)
这条代码将是日后咱们从占位符节点获取自定义props属性并传入子组件的关键所在,patch过程会详细说明。
二、函数式组件:
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
复制代码
三、installComponentHooks(data)
又是一个重点,注册组件上面的钩子函数,钩子函数定义在同一个文件下面:分为init、prepatch、insert、destroy四个钩子函数,patch过程讲到会专门介绍:
const hooksToMerge = Object.keys(componentVNodeHooks)
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
复制代码
四、new Vnode() & retrun vnode:
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
复制代码
能够看到组件的构造函数等一些被放在了一个对象当中传入了构造函数。
那么到这么,createComponent()
的过程也大体过了一遍了。
讲了半天的vnode建立,也没有讲到vnode类的实现,放到最后面讲是有缘由的,实在是建立vnode状况太多了,上面先铺开,再到最后用vnode一收,但愿能狗加深各位的印象。
真实的DOM节点node实现的属性不少,而vnode仅仅实现一些必要的属性,相比起来,建立一个vnode的成本比较低。因此建立vnode可让咱们更加关注需求自己。
是使用vnode,至关于加了一个缓冲,让一次数据变更所带来的全部node变化,先在vnode中进行修改,而后diff以后对全部产生差别的节点集中一次对DOM tree进行修改,以减小浏览器的重绘及回流。这一点咱们会在深刻响应原理的章节当中详细介绍,包括渲染watcher更新队列,组件的更新等等~
Class Vnode{}
Vnode类定义在src/core/vdom/vnode.js
中:
export default 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
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
//somecodes
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
复制代码
看起来这个Vnode类已经很是的长了,实际的dom节点要比这个更加的庞杂巨大,其中的属性,有些咱们暂时能够不用关心,咱们重点放在构造函数上:
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
复制代码
咱们来对照一下createComponent()
中建立的vnode传入的参数是哪几个:
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
复制代码
能够看到,实际上createComponent()
中就传入了一个tag、data、当前的vm:context还有一个componentOptions,固然精华都是在这个componentOptions里面的,后面的_update也就是围绕这个componentOptions展开patch的,那么就放到下一篇:《小白看源码之Vue篇-3:vnode的patch过程》!
那么vm._render()
建立虚拟dom的过程咱们也就大体过了一遍,文章一遍写下来,实际不少的地方须要读者本身反复的琢磨,不是看一遍就了解的,本小白也是在反复挣扎后才略知一二的。
「本节的重点如何建立一个vnode,以及不一样状况下的不一样建立方式,暂不要深究细节,日后慢慢会涉及到铺开,你在回过头来,恍然大明白!加深印象!」
有大佬们须要短信业务,或者号码认证的能力的话,能够看看这里!中国联通创新能力平台 运营商官方平台!没有中间商赚差价~