上一篇,我们分析了compiler过程,其核心是将template转化为render函数。
那么,问题来了:
- render函数执行后:得到的是什么?
- 虚拟DOM又是什么?
- vue多层组件嵌套,其组件化又是如何实现的?
我们带着这些问题,来一探究竟。
一. Render函数
我们知道,compiler结果是个render函数。(不熟悉的小伙伴,可以看我的上一篇文章:vue源码解析-compiler)。
先来看一个 ?:
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<div id='root'>
</div>
<script src="../vue/dist/vue.js"></script>
<script>
Vue.component("test", {
template: "<div>{{ testName }}</div>",
data() {
return {
testName: '这是测试名称啊'
}
}
})
let vm = new Vue({
el: '#root',
data() {
return {
a: "这是根节点"
}
},
template: "<div data-test='这是测试属性'> <test/> </div>",
})
</script>
</body>
</html>
执行结果如下:
vue在mount的时候,会调用如下代码:
mountComponent 主干:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
updateComponent是在Watcher实例化时调用,这里vm会执行update方法更新视图,而参数是render函数的返回结果。下面,我们重点分析vm._render背后发生了什么
_render函数,在vue初始化renderMixin的时候有定义。其核心代码如下:
renderMixin
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// ...
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
}catch(e) {
// ...
}
// ...
return vnode
}
我们可以看到,执行_render方法,实际上就是执行 render.call(vm._renderProxy, vm.$createElement)
而render的定义在vm.$options上, 这个其实就是compiler出来的render函数。render函数结果如下:(这一步不清楚的小伙伴可以参考我的上一篇分享:vue源码解析-compiler)
(
function anonymous() {
with(this){
return _c(
'div',
{
attrs:{
"data-test":"这是测试属性"
}
},
[
_c('test')
],
1
)
}
}
)
render.call实际上执行就是这个函数。this的指向,就是当前组件实例化的对象。首次执行是Vue。
那么_c又是什么呢?这个定义,实际上在vue初始化initRender定义的。
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
下面,我们重点分析createElement
二. createElement
render函数执行后,最终是调用的createElement函数,其参数就是function anonymous对应的入参。
我们先来看下createElement函数的定义:
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// ...
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 做了一些判断,避免使用可观察的data对象做虚拟节点data,否则返回 空节点
// ...
// tag不存在,返回空节点
// ...
if(typeof tag == 'string') {
let Ctor
if (config.isReservedTag(tag)) {
// 如果是平台保留标签,将返回一个虚拟DOM
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
}else if(
(!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))
) {
// 这里就是我们写个 组件 components
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 未知标签,返回一个虚拟dom
vnode = new VNode(tag, data, children, undefined, undefined, context)
}
} else {
// 直接组件 options / constructor
vnode = createComponent(tag, data, context, children)
}
// ...
return vnode
}
根据render()执行,首先会执行 _c("test"),实际上调用的是 createElement(vm, "test", undefined, undefined, undefined, false) 这个时候,会进入 createComponent 逻辑。
需要说明的是,从这里开始,需要聊到vue组件化了。这里,我们重点关注 4个函数:
-
- Vue.component
-
- extend
-
- resolveAsset
-
- createComponent
下面,我们来逐个分析
三. 组件化-Vue.component
在我们的demo中,先注册了test组件,然后在div中使用。
我们先待了解Vue.component背后发生了什么。
在vue初始化时,会调用一个 initGlobalAPI 方法,这个函数里面,就是声明的Vue下面的全局Api,比如我们经常使用的:Vue.component, Vue.extend, Vue.use, Vue.minxin等等。
其中initAssetRegisters这一步很重要。我们先来看主干代码:
ASSET_TYPES.forEach(type => {
Vue[type] = function(id: string, definition: Function | Object): Function | Object | void {
// ...
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
// ...
this.options[type + 's'][id] = definition
return definition
}
})
ASSET_TYPES如下:
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
这里我们可以看到Vue.component第一个参数就是组件的name,第二个是组件的options。
this.options._base指向的就是Vue的构造函数。下面我们看Vue.extend方法
四. 组件化-extend
主干代码如下:
export function initExtend (Vue: GlobalAPI) {
const Super = this
const SuperId = Super.cid
// 省略cid cache
// ...
// 校验组件名称...
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// 初始化props...
// 初始化computed...
// 初始化VueComponent构造函数的方法,实际上使用的是Vue构造函数方法
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// 添加asset types, 其实就是3个值 component, directive, filter
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// ...其他options
// cache superId, 下次执行extend时,在cache中存在,直接返回,不必再走继承流程
return Sub;
}
可以看到,在我们使用Vue.component注册组件的时候:被注册的组件,他也是个构造函数,只是他不是Vue,他是VueComponent。
而VueComponent继承了Vue,他拥有了Vue的一切。
好了,到这里,我们终于知道,注册组件,实际上就是以下结构:
Vue.$options.components.__proto__ = {
test: VueComponent // 子组件的构造函数
}
而这个VueComponent通过原型继承至Vue,所以如果子组件里面,还有孙组件。那么子组件中的结构就变成如下:
VueComponent.$options.components.__proto__ = {
"孙组件name": VueComponent // 孙组件的构造函数
}
如此递归下去,在每个组件的$options.components属性上,都存放着对应的子组件的构造函数。这一点很重要,这才是实现组件化的开始。
五. 组件化-resolveAsset
在createElement中,我们看到y有这么两行代码:
let Ctor
// ...
Ctor = resolveAsset(context.$options, 'components', tag)
// ...
这一步也很重要,我们继续看resolveAsset 主干:
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
// ...
const assets = options[type]
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
// ...
return res;
}
这里已经很清晰了,assets变量指的就是Vue.$options.components对象。id就是 我们demo中的子组件name = "test"。
这里,我们看到camelize和capitalize这2个函数。
- 其中camelize就是将子组件名使用中划线连接起来,比如,我们的组件name = "helloWorld", 最终转化为 hello-world使用。 这也是为什么,我们使用中划线的方式可以引用组件的原因。
- 其中capitalize函数,是将首字母进行大写,比如:name = "test",组件名称将转化为Test,这也是为什么,我们在vue中,首字母大写组件名也能正常引用的原因。
到这里,我们可以看到,最后返回的是Vue.$options.components.test,其实就是子组件的构造函数。即Ctor就是子组件test的构造函数。
六. 组件化-createComponent
主干代码如下:
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// ...
// 异步组件暂不在讨论范围之内,暂时忽略,后面关注我,分析异步组件实现
// 调用子组件options merge
// ...
// 函数式组件,暂时忽略,不在本次主流程中讨论,后面关注我,分析函数式组件实现
// ...
installComponentHooks(data)
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 平台兼容
return vnode
}
installComponentHooks
该函数也是非常重要的一个环节,在render函数执行,最终返回虚拟DOM时,组件内部的转化处理,也有个生命周期。
其核心hooks如下:
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// keep-alive逻辑,暂省略
// ...
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) {
// 调用子 组件 mounted hooks
// ...
// keep-alive 逻辑处理
},
destroy(vnode: MountedComponentVNode) {
// 调用子组件 $destroy 方法
// ...
// 删除当前组件active 实例
}
}
到这里,我们已经看到,data对象上绑定了对应的 组件 实例化的hooks。
注意,这里子组件还没有实例化,相关的Dep依赖收集还未开始。
这个时候,data: VNodeData 数据结构大致如下:
{
on: 'xx',
hooks: {
init: () => {
// ...
},
prepatch: () => {
// ...
},
insert: () => {
// ...
},
destroy: () => {
// ...
}
}
}
下面,我们进入组件化实现的另一个环节-虚拟DOM
七. 组件化-VNode
真实DOM
在html中,我们任意打印一个真实的div dom,会看到如下效果:
可以看到,一个真实dom的基础属性就有这么多,总计296个属性。
虚拟DOM
vnode 中文意为 虚拟DOM。 什么是虚拟DOM,其实他就是对 真实DOM 的一种描述。
世间万物的本质,就是 数据结构 + 算法。同样,真实的dom,我们也可以使用js对象进行描述他。
通过上图,我们可以发现,真实的dom有许多属性,而虚拟dom根本不需要那么多,只需要知道,tag名称,数据对象,是否有childrens,parent是谁等基本属性。
频繁操作真实的dom代价是昂贵的,而操作虚拟dom,代价是很小的,我们可以通过虚拟的dom,即js对象,在内存中变更对比,再一把crud。
实际上,虚拟dom存在的意义,不外乎两种:
- 提升性能
- 跨平台
虚拟dom主干代码:
class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void;
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void;
parent: VNode | void;
// 其他属性 ...
isStatic: boolean;
isComment: boolean;
ssrContext: Object | void;
// ...
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.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
// ...
}
get child (): Component | void {
return this.componentInstance
}
}
// 其他方法,创建空节点
// clone vnode...
// 创建文本节点...
我们可以看到,VNode是一个class类,其中常用的属性: tag, data, children, text, elm, context, componentOptions等,这些参数很重要。
接着第六步,createComponent:
需要指出的是:
- data: VNodeData 不一定有值。如果是组件,该值为空。 componentOptions和componentInstance是组件的特有属性。
- 组件的tag和普通的不一样,组件的tag统一为: "vue-component-${cid}-${name}"
createComponent方法,最后返回虚拟dom,子组件Test: 其核心结构大致如下:
{
tag: "vue-component-1-test",
text: '',
isStatic: false, // 是否是静态节点,后面做diff算法时,是很关键的一步
isComment: false,
ele: div, // div为真实的dom div 对象, 每个子组件,都会先生成一个空div占位节点
data: {
hooks: {
init: () => {
// ...
},
prepatch: () => {
// ...
},
insert: () => {
// ...
},
destroy: () => {
// ...
}
}
}, // data数据很重要,组件实例化时需要使用到
context: Vue,
componentOptions: {
// 此项很重要,是组件特有属性
Ctor: VueComponent, // 子组件构造函数
children: undefined, // 子组件中是否还有嵌套,这里我们demo中没有
tag: "test",
// ...
},
componentInstance: undefined, // 子组件的实例对象, 为什么这里是undefined,因为还没实例化
children: undefined,
// ...
}
好了,到这里,虚拟dom神秘的面纱被揭开了,他就是个js对象。
demo中,整个div的虚拟dom结构如下:
{
tag: "div",
isStatic: false,
isRootInsert: true,
isComment: false,
// ...
data: {
attrs: {
"data-test": "这是测试属性"
}
},
context: Vue,
componentOptions: undefined,
componentInstance: undefined,
children: [
// 上述子组件VNode
],
// ...
}
我们再回归到 菜单一 中的 mountComponent函数。
我们可以看到vm._render() 函数执行结果,就是返回的一个虚拟DOM。 这个虚拟dom,实际上就是个js对象,如上述代码所述。
而组件化工作并未结束,高潮即将来临:
下面,我们来分析:update patch工作。
八. 组件化-Patch
patch过程,是vue将虚拟dom转化为真实dom,展示在页面上的最后一个环节了。 但在此处,我们只看组件化实现相关部分。 其他部分:更新队列,diff算法,映射真实dom等,我会在下个章节里面,重点来分析。
废话少说,开干。
patch环节,核心的入口是 createPatchFunction。我先开看 与 组件化相关的 核心代码
function createPatchFunction(backend) {
const { modules, nodeOps } = backend
function createElm(
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if( createComponent(vnode, insertedVnodeQueue, parentElm, refElm) ) return
// ...
if( isDef(tag) ) {
// ...
if(__WEEX__) {
// weex 平台相关处理
}else {
createChildren(vnode, children, insertedVnodeQueue)
}
}
}
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
// ...
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
// 初始化component, insert队列
// 暂时忽略...
}
}
}
function createChildren (vnode, children, insertedVnodeQueue) {
// ...
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
// ...
}
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// ...
}
}
抽丝剥茧,我们直接看组件化相关逻辑,vnode就是 render函数执行之后的虚拟dom js对象。
不难看出,
- 第一层的vdom对象,没有hook属性,最后整个函数返回undefined,不会进行相关hooks
- 往下执行到createChildren 方法,递归执行所有childrens
- 我们demo只,只有一个子组件test,这里将再次调用 createElm 方法。而此时传入的vnode就是子组件vnode数据
重点来了,子组件vnode进入createComponent方法,此时vnode.componentInstance 是存在的。(前面说过,这是组件的特有属性)
核心:
let i = vnode.data
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false)
}
子组件vnode.data 实际上就是 之前提到的 组件实例化中的生命周期hooks,包括: init, prepatch, insert, destroy
显示子组件的Init方法是存在的, 那么执行子组件的init方法。
init核心如下:
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// keep-alive处理省略...
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
createComponentInstanceForVnode核心如下:
function createComponentInstanceForVnode (vnode: any, parent: any) {
// ...
return new vnode.componentOptions.Ctor(options)
}
终于,我们看到了子组件处理的本质。
前面我们提到,vnode.componentOptions.Ctor 就是子组件的构造函数。即VueComponent构造函数。
而此构造函数通过原型继承至Vue构造函数。 那么当子组件的VueComponent构造函数被实例化时。我们再回顾下,VueComponent相关代码:
// ...
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
// ...
所以,当实例化子组件的时候,就会执行_init方法,而_init方法继承至Vue构造函数。那么子组件实例化时,实际上就是走了一遍 new Vue的过程,只是当前的this指向是VueComponent,而不是Vue。
具体实例化发了什么,不清楚的小伙伴,可以看我的《vue源码解析-开始》。
实例化子组件后,其实_watcher 还是 undefined。
紧接着执行:
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
子组件被挂载了。 不清楚$mount背后发生了什么的小伙伴,可以看我的《vue源码解析-$mount》
而子组件挂载时,又将执行子组件的compiler,返回对应的子组件render函数。(demo中,第一次执行时,子组件是个_c(test))。这里,子组件render函数如下:
function anonymous(
) {
with(this){return _c('div',[_v(_s(testName)+" "+_s(a))])}
}
那么当子组件执行render函数,返回虚拟dom时, 那么将触发子组件的依赖收集。(而不是Watcher, Dep, Observe实例化时,真正的执行dep收集在render函数返回虚拟dom阶段)。
嗯,综上,如果是n层嵌套,那么将递归执行上面的流程,每个组件其实都是单独的一个 VueComponent构造函数。每个子组件被实例化时,都是新的vm实例。
组件化的设计很巧妙,这里只分析了核心流程,而细枝末节还有许多,比如:异步组件,函数式组件,keep-alive等。这些我会在后面的章节,详细探讨。
下一章,我们将分析,patch详细过程
码字不易,多多关注~?
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!