vue2-组件化实现
我们在前面的文章对vue的创建渲染等都或多或少有所了解,现在我们再继续对串联其中的 组件化实现 进行学习,从单个组件的创建渲染到组件的递归创建渲染。
1.根组件实例化
根组件的实现和子组件的实现其实有所区别,根组件是我们手动调用Vue构造函数实例化,而子组件实例化的构造函数是继承自vue的 VueComponent,我们先来看看根组件实现。
1.1入口
我们知道组件实例化是在 new Vue 中进行的,所以对于根实例来说,其入口十分明确,就在开发者主动调用构造函数初始化中。
new Vue({
el: '#app',
render: h =>
h(App)
}
)
复制代码1.2实例化
结合之前的分析我们知道,实例化将调用 _init 函数进行组件的初始化
let uid = 0
export function initMixin (Vue: ClassComponent>
) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// 每个组件都有唯一UID
vm._uid = uid++
// 组件对象不可以监测
// 在observe部分可以看到具体判断
vm._isVue = true
// 配置合并
// merge options
if (options &
&
options._isComponent) {
initInternalComponent(vm, options)
// 首次实例化_isComponent为定义走else逻辑
}
else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {
}
,
vm
)
}
// 各种数据定义的初始化
// 生命周期的调用
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
// 挂载节点
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
复制代码其中配置合并将执行 resolveConstructorOptions 函数,对于根实例来说 resolveConstructorOptions 就是将 Vue.options 的配置合并到组件实例中
export function resolveConstructorOptions (Ctor: ClassComponent>
) {
let options = Ctor.options
if (Ctor.super) {
// ...
}
return options
}
复制代码而 Vue.options 对象则是包含了各种全局注册的conponents,filters,directives等。如此,通过 Vue.mixin,Vue.use,Vue.extend 等方法添加的资源即可在组件中调用了。
1.3创建虚拟节点
组件实例化的最后一步,是挂载节点,我们应该知道它其实是在组件实例化流程中进行的,我们这边将其单独拎出来作为下一步来分析。
在 vm.$mount 中实际先执行 render 函数,其调用在 watcher 实例的回调函数 updateComponent
updateComponent = () =>
{
vm._update(vm._render(), hydrating)
}
复制代码在 vm._render 执行的是我们的 rneder 函数,render 函数来源为开发者定义或 template 转化。在 render 中调用 _createElement 进行虚拟节点的创建。
export function _createElement (
context: Component,
tag?: string | ClassComponent>
| Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | ArrayVNode>
{
//...
// 子节点扁平化
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)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
}
else if ((!data || !data.pre) &
&
isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
vnode = createComponent(Ctor, data, context, children, tag)
}
else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
}
// ...
if (Array.isArray(vnode)) {
return vnode
}
else if (isDef(vnode)) {
return vnode
}
else {
return createEmptyVNode()
}
}
复制代码从简化后的 _createElement 来看,其通过 tag 来判断是否是创建普通虚拟节点,如果是的话就执行 new VNode 逻辑。
1.4渲染
执行了 render 函数之后,意味为虚拟节点已经准备完毕,接下来就是执行渲染逻辑了。其调用依然在 updateComponent 中
updateComponent = () =>
{
vm._update(vm._render(), hydrating)
}
复制代码我们来看看 vm._update
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
// 此时$el已经由配置#app转化为真实节点
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
// _vnode更新为最新的虚拟节点
vm._vnode = vnode
if (!prevVnode) {
// 首次渲染时将使用真实节点作为虚拟节点
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
}
else {
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
}
复制代码__patch__ 的逻辑我们就不细说了,等后面分析子组件的时候再去分析
1.5根组件小结
至此我们就完成了根组件从创建到渲染的分析,过程比较简单,我们来梳理下流程
- 开发者调用
new Vue实例化组件,resolveConstructorOptions将合并全局资源 - 在
render中执行_createElement创建虚拟节点vnode,注意是整个组件的虚拟节点 - 执行
__patch__逻辑完成渲染
2.子组件实例化
我们今天的主要任务是弄清楚子组件的实例化,这样才能弄清楚vue组件化实现的机制
2.1创建组件入口
在根组件生成虚拟节点的时候,遇到自定义组件会调用 createComponent 创建相应的组件虚拟节点,组件标签 =>
组件虚拟节点
export function _createElement (
context: Component,
tag?: string | ClassComponent>
| Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | ArrayVNode>
{
// ...
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode &
&
context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
}
else if ((!data || !data.pre) &
&
isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 创建组件节点
vnode = createComponent(Ctor, data, context, children, tag)
}
else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
}
else {
// 创建组件节点
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
}
else if (isDef(vnode)) {
return vnode
}
else {
return createEmptyVNode()
}
}
复制代码2.2创建组件节点
我们来看看创建组件节点 createComponent 的逻辑
export function createComponent (
Ctor: ClassComponent>
| Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?ArrayVNode>
,
tag?: string
): VNode | ArrayVNode>
| void {
// _base === Vue
const baseCtor = context.$options._base
// 调用Vue.extend生成组件构造函数
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// ...
data = data || {
}
// 合并配置
resolveConstructorOptions(Ctor)
// 提取props值并校验
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
const listeners = data.on
data.on = data.nativeOn
// 安装组件管理相关钩子
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
)
// 返回组件虚拟节点
return vnode
}
复制代码前面分析了 createComponent的主要流程,我们将其分为以下几个部分进行进一步分析
Vue.extend生成组件构造函数resolveConstructorOptions合并配置installComponentHooks安装组件管理相关钩子new VNode生成组件虚拟节点
2.2.1 Vue.extend生成组件构造函数
Vue.extend = function (extendOptions: Object): Function {
// extendOptions是我们的组件配置
// 包括created methods data components等
extendOptions = extendOptions || {
}
// this===Vue
const Super = this
const SuperId = Super.cid
// 往extendOptions._Ctor扩展属性保存此构造函数
// 当遇到相同的配置时就不需要再生成构造函数
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {
}
)
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
// 校验组件名
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' &
&
name) {
validateComponentName(name)
}
// VueComponent定义
const Sub = function VueComponent (options) {
// 调用_init执行组件初始化的流程
this._init(options)
}
// 原型继承
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 合并组件配置
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// prop/computed初始化暂不分析
// ...
// 扩展extension/mixin/plugin等函数
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// 扩展ASSET_TYPES中定义的函数包括component/directive/filter
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
}
)
// 注册自身以支持循环调用
if (name) {
Sub.options.components[name] = Sub
}
// 缓存各种配置
// 在后面将用于检查配置是否更新
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({
}
, Sub.options)
// 缓存子构造器
cachedCtors[SuperId] = Sub
return Sub
}
复制代码通过分析可以得到 Vue.extend 的主要功能就是继承了 Vue 的原型,定义了新的构造函数 VueComponent 与 Vue 进行区分,在构造函数中也很简单,就是执行 _init 函数。我们还是来总结下吧
- 校验组件名
validateComponentName - 生成新的构造函数
Sub = VueComponent - 合并配置,包括组件配置与全局配置,并定义到
Sub.options - 扩展子构造函数
- 缓存配置及构造函数
2.2.2 resolveConstructorOptions合并配置
resolveConstructorOptions 这里的功能其实和 Vue.extend 中的合并配置差不多,区别就是会在这边再次校验缓存的配置是否有更新
export function resolveConstructorOptions (Ctor: ClassComponent>
) {
let options = Ctor.options
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
}
复制代码2.2.3 安装组件管理相关钩子
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {
}
)
// 遍历hooksToMerge合并componentVNodeHooks
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
}
}
}
复制代码我们再来看看 componentVNodeHooks 的定义
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &
&
!vnode.componentInstance._isDestroyed &
&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
// ...
}
else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
,
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}
,
insert (vnode: MountedComponentVNode) {
const {
context, componentInstance }
= vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
}
,
destroy (vnode: MountedComponentVNode) {
const {
componentInstance }
= vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
}
else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
复制代码通过 installComponentHooks 函数为组件数据 data.hook 添加 componentVNodeHooks 中定义的钩子函数,钩子函数的具体实现我们具体调用的时候再进行分析,目前只需要知道在这边进行定义添加就行。
2.2.4 new 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
)
复制代码值得注意的是,此处的 children 并不是组件的根节点或者其它子节点,因为其定义在外层组件的 render 函数中,这边的 children 其实是关于 slot 的实现。
我们继续看看组件 vnode 的实例化
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?ArrayVNode>
;
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?: ?ArrayVNode>
,
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 // 组件实例(实例化之前为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
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
复制代码通过参数对比可以发现组件虚拟节点的参数主要是 tag data context componentOptions asyncFactory,其中 asyncFactory 和异步组件相关,与正常 vnode 不同之处在于其 tag 为组件名及 componentOptions 中保存了组件相关配置
2.3组件实例化
我们在前面的 1 ->
2 中梳理了组件的入口,组件构造函数的生成及组件节点 vnode 的生成,主要逻辑在于 _createElement 及 createcomponent 中。至此我们创建了组件的 vnode,但是组件的实例化及渲染并没有涉及,其实组件实例化的过程在 patch 函数中。
2.3.1组件实例化入口
我们回到最开始的 patch 中。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
// _vnode更新为最新的虚拟节点
vm._vnode = vnode
if (!prevVnode) {
// 首次渲染时将使用真实节点作为虚拟节点
// 对于子组件来说此时vm.$el为undefined
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
}
else {
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
}
复制代码关于 patch 的具体流程可以参考vue2-patch流程分析及vueDiff 算法解读,现在我们来重点分析其中的组件相关实现。因为源码比较多,我们会省略一些代码。我们来看看 patch 最终执行的函数 createPatchFunction
export function createPatchFunction (backend) {
let i, j
const cbs = {
}
const {
modules, nodeOps }
= backend
// ...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
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) {
// 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)
)
// ...
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
}
else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
复制代码我们没有看到与组件相关的代码,但是能发现其主要逻辑是执行节点创建函数 createElm,我们接着分析
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
vnode.isRootInsert = !nested // for transition enter check
// 组件实例化入口
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// 真实节点创建相关
// ...
复制代码2.3.2组件实例化实现
在 createElement 的执行中能看到创建组件的判断,我们分析其实现
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
if (isDef(i = i.hook) &
&
isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
return true
}
}
}
复制代码此时会先判断 vnode.data.hook.init 是否存在,如果存在则调用,这步是组件实例化的关键入口。init 的定义其实在上面我们分析过,存在 componentVNodeHooks 中,我们现在来分析下其实现。
2.3.2.1组件实例化调用
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &
&
!vnode.componentInstance._isDestroyed &
&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
}
else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
复制代码init 的逻辑主要是实现两步
- 执行
createComponentInstanceForVnode并赋值给 vnode.componentInstance- 调用
child.$mount完成渲染逻辑
我们先看看 createComponentInstanceForVnode
export function createComponentInstanceForVnode (
vnode: any,
parent: any
): Component {
// 定义了构造函数的入参options
// 仅包含三个参数_isComponent===true _parentVnode为组件vnode parent则会取到当前activeInstance也就是父组件实例
// 关于activeInstance的实现也很简单,大家可以自己去看看lifecycle中的_update实现即可。
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// ...
// 最后的结果是返回componentOptions.Ctor实例化的结果
// vnode.componentOptions.Ctor的生成我们在前面分析过
// 这边注意options不是开发者的组件配置
// 开发者对组件的配置数据其实在生成Ctor时就已经定义在静态属性Ctor.options中了
return new vnode.componentOptions.Ctor(options)
}
复制代码2.3.2.2组件初始化
我们继续复习下 Ctor 的定义
const Sub = function VueComponent (options) {
this._init(options)
}
复制代码其中 _init 函数就是我们熟悉的组件初始化的函数
export function initMixin (Vue: ClassComponent>
) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
vm._isVue = true
// merge options
if (options &
&
options._isComponent) {
// 此时会执行initInternalComponent
initInternalComponent(vm, options)
}
else {
// ...
}
// 组件初始化的其它逻辑
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')
// 我们在组件中一般是不会定义el值的
// 所以不会直接调用vm.$mount进行渲染
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
复制代码我们接着分析下 initInternalComponent(vm, options) 的逻辑
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
// 定义了$options继承自Ctor.options
const opts = vm.$options = Object.create(vm.constructor.options)
// 同时将_parentVnode与parent属性赋值到vm.$options
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
// 同时将组件vnode的数据赋值到vm.$options
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
复制代码2.3.2.3组件挂载渲染
至此,子组件的实例化也完成了,我们再看看前面所说的两步走的第二步
child.$mount(undefined) 复制代码
$mount 函数我们前面也有分析过,就是再执行组件的挂载渲染了
3.组件相关实现
本篇文章的重点组件实例化其实已经分析完,我们再来看看与组件相关的其它方面的实现
3.1 组件节点的处理
我们知道在创建虚拟节点的时候会有个组件节点,而我们在页面上看到的真实渲染是没有组件节点这个东西的,我们来看看组件节点是如何被跳过的,组件节点下面的节点又是如何找到正确的挂载父节点的。
我们知道在渲染组件的时候,会执行其中的 render 函数,而 render 函数外面又包了一层,在 core/instance/render.js 中
Vue.prototype._render = function (): VNode {
const vm: Component = this
// _parentVnode就是我们的组件节点
const {
render, _parentVnode }
= vm.$options
// ...
vm.$vnode = _parentVnode
// render self
let vnode
try {
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
}
catch (e) {
// ...
}
finally {
currentRenderingInstance = null
}
// ...
// set parent
vnode.parent = _parentVnode
return vnode
}
复制代码通过分析可以看到,在 _render 中,其实会将组件节点 _parentVnode 作为 parent 赋值给 vnode,而 vnode 来自于组件的 render 函数,所以实际在组件 _render 中,组件节点是不会被渲染在组件中的,而组件 render 返回的 vnode 会作为根节点返回。
以上代码解释了如何跳过组件节点的编译,我们再看看组件根节点是如何被挂载在父节点上的
在 patch 函数下的 createComponent
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) &
&
i.keepAlive
// 我们调用init函数完成了组件的实例化及真实节点创建
if (isDef(i = i.hook) &
&
isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
// 在这调用initComponent
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
复制代码其实节点的正确挂载逻辑在 initComponent 中
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
// 在这将vndoe.elm赋值为vnode.componentInstance.$el
// vnode.componentInstance指向组件实例
// 其$el实际在_update函数中生成,实际是__patch__函数的结果
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
}
}
复制代码所以其原理是将组件创建的真实节点先赋值给组件节点上,对于组件根节点的 vnode 来说,vnode.parent.elm === vnode.elm
3.2 其它组件管理钩子
我们在上面定义了组件的管理钩子,但是后面只分析了其中 init 的实现,我们现在来看看其它钩子的实现。
3.2.1 prepatch
prepatch 的调用其实在 diff 的 preVnode 中,当我们的组件节点有更新时就会先调用 prepatch
if (isDef(data) &
&
isDef(i = data.hook) &
&
isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
复制代码在 prepatch 中主要调用 updateChildComponent
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}
复制代码因为在更新节点的时候,我们的组件其实是已经完成创建的,所以组件实例已经存在。在更新中如果发现组件节点 vnode 的有更新,则需要更新组件节点对应的组件实例相关数据,这就是 updateChildComponent 的主要任务。
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?ArrayVNode>
) {
if (process.env.NODE_ENV !== 'production') {
isUpdatingChildComponent = true
}
// 更新组件节点
vm.$options._parentVnode = parentVnode
vm.$vnode = parentVnode // update vm's placeholder node without re-render
if (vm._vnode) {
// update child tree's parent
vm._vnode.parent = parentVnode
}
vm.$options._renderChildren = renderChildren
// 更新属性/事件
vm.$attrs = parentVnode.data.attrs || emptyObject
vm.$listeners = listeners || emptyObject
// update props
if (propsData &
&
vm.$options.props) {
toggleObserving(false)
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0;
i propKeys.length;
i++) {
const key = propKeys[i]
const propOptions: any = vm.$options.props // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
// update listeners
listeners = listeners || emptyObject
const oldListeners = vm.$options._parentListeners
vm.$options._parentListeners = listeners
updateComponentListeners(vm, listeners, oldListeners)
}
复制代码3.2.2 insert
在我们组件 patch 的最后一步,会执行 invokeInsertHook
// insertedVnodeQueue是在patch过程中收集的子组件 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) 复制代码
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) &
&
isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
}
else {
for (let i = 0;
i queue.length;
++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
复制代码invokeInsertHook 会遍历触发组件的 insert 钩子
insert (vnode: MountedComponentVNode) {
const {
context, componentInstance }
= vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
// 逻辑很简单 就是调用生命周期mounted
callHook(componentInstance, 'mounted')
}
}
复制代码这也解释了为什么子组件的 mounted 会比父组件早,因为在父组件的 patch 中,会调用子组件的 patch,而子组件的 patch 是先完成的。当然 insertedVnodeQueue 其实会有个存储的设计,因为 mounted 必须在根节点挂载后才能,所以并不是组件 patch 完成就会立即调用 insert 钩子。具体实现大家感兴趣可以自己去看看。
3.2.3 destroy
当移除组件节点的时候会触发 destroy
function removeVnodes (vnodes, startIdx, endIdx) {
for (;
startIdx = endIdx;
++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch)
invokeDestroyHook(ch)
}
else {
// Text node
removeNode(ch.elm)
}
}
}
}
复制代码function invokeDestroyHook (vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
// 在这触发destroy
if (isDef(i = data.hook) &
&
isDef(i = i.destroy)) i(vnode)
for (i = 0;
i cbs.destroy.length;
++i) cbs.destroy[i](vnode)
}
if (isDef(i = vnode.children)) {
for (j = 0;
j vnode.children.length;
++j) {
invokeDestroyHook(vnode.children[j])
}
}
}
复制代码我们来看看 destroy 的逻辑
destroy (vnode: MountedComponentVNode) {
const {
componentInstance }
= vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
}
else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
复制代码逻辑也很简,就是调用组件实例的 $destroy,我们就不继续分析了。
总结
本篇文章分析了vue组件化的实现,包括Vue实例及VueComponent实例的创建及渲染过程,篇幅比较长,很多方面讲的也不行。后面如果有时间的话会继续分析组件生命周期及节点更新函数,没有时间的话就先算了。anyway good good staduy day day up
声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!
若转载请注明出处: vue2-组件化实现
本文地址: https://pptw.com/jishu/294949.html
