Vue源码学习9-组件化-createElement
前言
前面已经讲完了 Vue 的一个核心思想 响应式原理,Vue 的另一个核心思想就是组件化。什么是组件化?为啥要用组件化的方式开发?怎样做到组件化?这些问题网上随便一搜都能搜出一堆关于 组件化 的文章,文字上的描述可能略有不同,但其核心思想都是差不多的,不过我的文章是要讲源码,就不说太多这种概念性的东西了。
说说使用感受,平时使用Vue开发网页的时候,最直观的感受就像是在搭积木,各种功能不一的组件相互嵌套、堆叠,最后拼接出一个漂亮的网页出来,但是有时候过度抽取组件,把自己搞得脑壳疼,这就非常考验自己的组件抽象思维了。
在讲Vue组件化之前,要搞清楚一个问题,组件化和组件不是同一个东西。
组件化是一种思想,一种概念,是多年以来由开发人员总结出的一种最佳编程思想。
而组件,是代码编写规范,是 组件化思想 在代码上的体现。既然是代码体现,那么每个人的编写规范可能就会不一样,这个时候就需要框架了,以框架的规范统一开发人员的编码规范,省去很多不必要的麻烦。现阶段前端最流行、好用的框架就只有Vue、React、Angular(幸好只有三个),它们都是组件化思想的集大成者,虽然编码规范不一样
--以下所有代码来自Vue 2.6.11 版本
--以下的vm都代指vue实例
Vue组件化流程
Vue中组件的注册、命名规范这种问题就看看官方文档就行了。
首先说一点,组件在定义的时候就是一个Object对象,它的那些 options 和 new Vue 传入的 options 其实是一样的,不过还是有不同的地方,比如说 el data name 这些配置组件和 new Vue 的就不一样。我们在第一篇文章中就知道,Vue 本质上是个function,它可以通过new得到一个vm,但是我们注册传入的这个组件对象,最后也会变成一个个的vm,这是怎么做到的?看看源码就知道了。
前面在讲 mountComponent 的时候,提到了一个 vm._update 的方法,但是在执行 update 之前,还有一个叫_render(src/core/instance/render.js) 私有方法,事实上 vnode 正是在其中生成的,依赖收集 也是发生在这个过程中,我看看到下面的 _render 的源码,直接看到 render.call 那个地方,忽略前面的一些代码,这个 render 是从哪里来的?
createElement
1:看看Vue的文档,再看看示例代码,没错了,就是我们自己传入的这个render函数(在实际开发中我们可能和很少用这种写法,更多的是使用模板的方式,因为模板方式的可读性是最好的,但我们要知道,模板最终也是会被处理成这样的render函数,这是编译的内容)
2:看到 vm.$createElement 方法,一个入参,正是示例代码里面的 h 方法,它在render方法中被执行
/*示例代码*/ new Vue({ el:'#app', render(h){ return h('div'......); } })
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 ) } vm.$vnode = _parentVnode // render self let vnode try { currentRenderingInstance = vm vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) 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 }
3:$createElement 在哪里定义的?在第一篇文章中我们查看 _init 的时候,里面就有一个叫做 initRender(源码就不贴了) 的方法,就是在这里面赋的值,接着进入 createElement 方法,最终到达 _createElement(src/core/vdom/create-element.js) ,下面贴了源码
4:看到这段逻辑 typeof tag === 'string' ,tag 正是我们传入的标签字符串(上面示例代码传入的是div)
进入后遇到第一个 if 判断 config.isReservedTag(tag),这里是在检测标签是否是浏览器保留标签,如果是 div 这样的浏览器标签,就会创建一个普通的vnode;假设我们的 tag 名叫 Hello ,并且它成功注册为Vue的全局组件,那么就能顺利的进入第二个 if 逻辑--- createComponent
/*initRender*/ ....... vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) .......
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() } }
createComponent
在文章开始的时候我提过一个问题,组件是对象是怎么变成一个个的 vm?答案就在这个 createComponent 方法中(下面把源码贴出来了)
1:我们看到源码,_base是什么?它就是 Vue,它的赋值就发生在 最初的 _init 方法中,就是下面这段代码
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm )
extend 又是什么?它是 Vue 上的一个全局方法(src/core/global-api/extend.js,代码就不贴了,私下看看就行了),用来继承Vue的。我们注册的组件对象就是通过这个继承方法获取得了 Vue 的所有功能,并且被它会返回一个 function ,源码中的 Ctor 被处理之后就会变成一个 function,然后就它就可以执行 new 操作了(文章开始提的那个疑问到这里就搞定了)
2:接着看代码,跳过异步组件的那块内容,直接走到 installComponentHooks 安装组件钩子,这里是干什么的?先在这里留个悬念,后面的 patch 内容同会涉及这个东西的
3:紧接着,创建一个组件 vnode ,我们可以看到它的tag和普通的 vnode 是不一样的,也算是个区别于普通 vnode 身份标记吧。
4:至此,vnode生成完毕,我们的 createElement 或者说 vm._render 就告一段落了,后面还一些代码杂七杂八的代码在这也不用管了,影响不大
以下面示例代码为例:
此时 root-vm 中的 vnode 应该有一个 div-vnode,一个 span-vnode,以及一个 tag 为Hello 的组件vnode,还有一个依赖 name 的表达式,它们在代码中的表现就是和真实 dom 一样的树形结构
/*示例代码*/ new Vue({ components:{ Hello:{ template:'<div>{{ name }}</div>' data(){ return { name:'Tifa' } } } }, el:'#app', data:{ name:'Cloud' }, template:'<div> <span> {{ name }} </span> <Hello/> </div>' })
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 // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) } // 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 }
总结
以上我们简单梳理了整个 vnode树 的生成过程,下篇文章开始我们梳理 patch 的过程,从图中可以看出,我们已经走到了最后一步了,组件实例化也发生在这一步中。
发表回复