前言
通过上一节分析得知,首次渲染调用vm.patch(vm.el上,那么接下来我们分析一下createElm方法
createElm()
createElm方法就是用来递归创建真实的dom,我们看一下createElm具体做了如下几件事情
- 创建dom节点(html标签/文本节点/注释节点)
- 递归创建子元素dom节点
- 如果是组件,递归调用createElm方法,创建子组件的dom节点
- 调用create渲染钩子,生成标签属性,样式,事件等等
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
)
{
/*非重要代码省略*/
// 如果创建组件成功,就不再往后执行
// 只有vnode是子组件的时候createComponent才会返回true
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// 拿到vnode的数据
const data = vnode.data
// 拿到子组件vnode
const children = vnode.children
// 拿到标签
const tag = vnode.tag
// 如果tag定义了
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
// 创建真实dom节点
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// weex平台代码省略
} else {
// 递归创建子节点元素,如果是组件,会渲染组件的dom
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 将生成的dom传入到父节点中
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
}
// 否则如果tag是个注释节点
else if (isTrue(vnode.isComment)) {
// 创建一个注释节点
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
// 否则创建一个文本节点
else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
以上代码调用了nodeOps的方法创建dom,nodeOps其实就是定义的一系列原生dom操作的方法
export function createElement (tagName: string, vnode: VNode): Element {
const elm = document.createElement(tagName)
if (tagName !== 'select') {
return elm
}
// false or null will remove the attribute but undefined will not
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple')
}
return elm
}
export function createElementNS (namespace: string, tagName: string): Element {
return document.createElementNS(namespaceMap[namespace], tagName)
}
export function createTextNode (text: string): Text {
return document.createTextNode(text)
}
export function createComment (text: string): Comment {
return document.createComment(text)
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
export function parentNode (node: Node): ?Node {
return node.parentNode
}
export function nextSibling (node: Node): ?Node {
return node.nextSibling
}
export function tagName (node: Element): string {
return node.tagName
}
export function setTextContent (node: Node, text: string) {
node.textContent = text
}
export function setStyleScope (node: Element, scopeId: string) {
node.setAttribute(scopeId, '')
}
createElm方法先是进入判断是否是子组件的创建,如果不是那么继续判断,进而创建html节点/文本节点/注释节点 此方法中会在执行到
createChildren(vnode, children, insertedVnodeQueue)
时创建子节点,这个方法中会递归的调用createElm方法创建dom元素,如果是组件的话会进入createComponent方法中,如何创建子组件会在专门的部分详细讲解。
创建完dom节点并没有结束,因为dom还需要有标签属性,样式,事件绑定等等,这些都是在invokeCreateHooks(vnode, insertedVnodeQueue)方法中执行cbs.create方法实现的,那么cbs是什么呢?cbs其实就是渲染时的钩子
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
这些钩子的实现部分都是定义在传入的modules中
export const patch: Function = createPatchFunction({ nodeOps, modules })
这些钩子方法被定义在vue/src/platforms/web/runtime/modules 目录下,每个文件都提供一个create方法
export default {
create: updateAttrs,
update: updateAttrs
}
我们以处理class为例,生成和更新class走的都是同一个方法,主要逻辑就是重新生成样式赋值给el
function updateClass (oldVnode: any, vnode: any) {
// 拿到真实的dom
const el = vnode.elm
const data: VNodeData = vnode.data
const oldData: VNodeData = oldVnode.data
if (
isUndef(data.staticClass) &&
isUndef(data.class) && (
isUndef(oldData) || (
isUndef(oldData.staticClass) &&
isUndef(oldData.class)
)
)
) {
return
}
// 生成样式
let cls = genClassForVnode(vnode)
// handle transition classes
const transitionClass = el._transitionClasses
if (isDef(transitionClass)) {
cls = concat(cls, stringifyClass(transitionClass))
}
// 重新设置样式
// set the class
if (cls !== el._prevClass) {
el.setAttribute('class', cls)
el._prevClass = cls
}
}
export default {
create: updateClass,
update: updateClass
}
其他的创建,如下,感兴趣可自行分析
export default [
attrs,
klass,
events,
domProps,
style,
transition
]
至此如果没有定义子组件的情况下,真实的dom已经生成了
接下来我们看一下子组件是如何被渲染的。
createComponent
首先我们看一下子组件的Vnode是如何生成的,在_createElement创建vnode的方法中,定义了如下代码,如果当前render函数被判定为是一个组件的render函数,那么会调用createComponent方法生成组件的vnode
// 如果判定是组件
else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
createComponent方法主要就是将构造函数和一系列创建子组件的钩子函数保存起来,在创建子组件的时候,从vnode中拿到构造函数并实例化,从而生成子组件的Vue实例
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
生成的vnode结构类似如下图所示
言归正传我们看一下createElm方法中的createComponent方法
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 如果vnode.data.hook存在,vnode.data.hook.init存在
// i = vnode.data.hook.init
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 调用组件的init方法,init回调用mount方法,i执行完以后,vnode.elm就是真实创建的dom
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.
// 如果vnode是一个子组件的话,即非根组件,返回true
if (isDef(vnode.componentInstance)) {
// 初始化组件
initComponent(vnode, insertedVnodeQueue)
// 将子组件的dom插入至父节点中
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
其主要逻辑就是调用了vnode.data.hook.init方法,那么vnode.data.hook.init方法又是在哪里定义的呢
其实是在_createElement方法中的这个方法中定义的
vnode = createComponent(Ctor, data, context, children, tag)
createComponent方法中调用了installComponentHooks方法
// 安装vnode组件的钩子
installComponentHooks(data)
installComponentHooks方法其实就是生成一系列渲染方法,并赋值给vnode.data.hook
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
}
}
}
生成后类似下图
init方法定义如下
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)
}
以上的child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) 是由如下方法返回
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
重点就在最后一行,其返回了一个Vue的实例,因为 Ctor就是保存在vnode中的继承自Vue的构造函数。Vue的实例被返回以后,又调用了vm.$mount方法又进行了一次 模版编译->ast->render函数->dom渲染的逻辑,子组件的dom被渲染以后,插入到父节点中,就这样递归的深度优先似的进行整个文档的首次渲染。
总结
至此,vue的初始化以及首次渲染就已经完毕,接下来在使用系统的时候,我们会与dom交互并修改数据,这时界面会因数据的变化而自动发生对应的变化,这就是所谓的数据响应式。在数据变动影响到界面的变动,我们重点关注如下几方面
- 数据响应式原理(为什么修改数据,对应的界面就能变化呢)
- dom更新渲染时,虚拟dom的dom diff算法