Vue.js源码解析-Vue初始化流程之动态创建DOM
前言
各位道友大家好,我是LSF,在上一篇博文 中,分析了Vue初始化的整体流程,最后到了 update 动态创建 DOM 阶段。接下来这篇博文,会对这个流程进行分析,重点需要掌握 createElm 函数的执行逻辑。
一、_update 如何判断是初始化还是更新操作?
_update 是在Vue实例化之前,通过prototype混入的一个实例方法。主要目的是将vnode转化成真实DOM,它定义在 core/instance/lifecycle.js 文件中。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this // vm -> this
const prevEl = vm.$el
// 保存上一个vnode。
const prevVnode = vm._vnode
// 设置 activeInstance 当前活动的vm,返回方法。
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode // 赋值 _vnode 属性为新传入的 vnode。
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render 初始化渲染,如果有子组件,会递归初始化
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates 更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
// activeInstance 恢复到当前的vm
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
代码中可以看到,通过 prevVnode 是否为 null 来判断的是否是初始化 patch。由于是初始化操作,开始的时候 vm._vnode 没有被赋值成 vnode,从而 vm._vnode 为 null。所以代码的执行逻辑会走到初始化 patch。
二、patch
2.1 patch 定义
web端的 Vue.prototype.__patch__
方法,它定义的入口在 src/platforms/web/runtime/index.js 文件中。
import { patch } from './patch'
...
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// install platform patch function
// 安装web端的 patch 方法。
Vue.prototype.__patch__ = inBrowser ? patch : noop
...
如果是浏览器环境下,被赋值为 patch 方法,该方法定义在 src/platforms/web/runtime/patch.js中。如果是非浏览器环境,patch 被赋值成一个空函数。
/* @flow */
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
// 调用 createPatchFunction 函数,返回 patch。
export const patch: Function = createPatchFunction({ nodeOps, modules })
通过代码可以看到,最终 vue 是调用了 createPatchFunction 函数,它定义在 src/core/vdom/patch.js 中。createPatchFunction 函数内部定义了如 emptyNodeAt、removeNode、createElement、createChildren 等一系列的辅助函数,通过这些辅助函数,完成了对 patch 函数的代码逻辑的封装。
2.2 初始化的 patch
创建Vue实例,或者组件实例的,patch 都会被执行。
如果是创建vue实例执行 patch
isRealElement:判断是否是真实的DOM节点。
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly):负责DOM的更新。
oldVnode = emptyNodeAt(oldVnode):对容器DOM进行vnode的转化。
createElm():创建新节点,初始化创建需要重点关注的函数。
如果是创建组件实例执行的 patch
isInitialPatch:用户判断子组件否初次执行 patch,进行创建。
insertedVnodeQueue:新创建子组件节点,组件 vnode 会被push到这个队列中。
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)。
具体代码注释如下
export function createPatchFunction (backend) {
...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 如果新的 vnode 为空,调用 destory 钩子,销毁oldVnode
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
// 用户判断子组件否初次执行 patch,进行创建。
let isInitialPatch = false
// 新创建子组件节点,组件 vnode 会被push到这个队列中
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
// 空挂载(可能作为组件),创建新的根元素
isInitialPatch = true
// 创建组件节点的子元素
createElm(vnode, insertedVnodeQueue)
} else {
// 1.作为判断是否是真实的DOM节点条件
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
// 2. 传入的容器DOM(如 el: "#app"),会在这里被转化成 vnode。
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
// 3. 创建新节点
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)
)
// update parent placeholder node element, recursively
// 递归的更新父占位节点元素。
if (isDef(vnode.parent)) {...
}
// destroy old node
// 销毁旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
// 调用 insertedVnodeQueue 队列中所有子组件的 insert 钩子。
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
三、createElm 动态创建DOM
createElm
函数是动态创建 DOM 的核心,作用是通过 vnode 创建真实的 DOM,并插入到它的父 DOM 节点中。它定义在 src/core/vdom/patch.js
的 createPatchFunction 方法中。createElm 内部创建 DOM 的主要判断逻辑,可以概括为下面几种情况。
1、如果创建组件节点
如果碰到子组件标签,走创建组件节点逻辑。
创建完成,插入到父亲元素中。
2、如果创建标签元素节点
如果 vnode.tag 不为空,先创建标签元素, 赋值 vnode.elm 进行占位。
调用
createChildren
创建子节点,最终这些子节点会 append 到 vnode.elm 标签元素中。将 vnode.elm 标签元素插入到父亲元素中。
3、如果创建注释节点
如果 vnode.isComment 不为空,创建注释节点,赋值 vnode.elm。
将注释节点插入到父亲元素中。
4、如果创建文本节点
上面三种情况都不是,则创建文本节点,赋值 vnode.elm。
将文本节点插入到父亲元素中。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
// 1、如果碰到子组件标签,走创建组件节点逻辑,插入父亲节点。
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
// 2、如果是标签标记,先创建标签元素进行占位。
// 调用 createChildren 创建子节点(递归调用createElm)。
// 将标签元素,插入父亲元素。
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
)
}
}
// 通过上面的tag,创建标签元素,赋值给 vnode.elm 进行占位
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// in Weex, the default insertion order is parent-first.
// List items can be optimized to use children-first insertion
// with append="tree".
const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
if (!appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
createChildren(vnode, children, insertedVnodeQueue)
if (appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
} else {
// 创建子节点
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 将创建的标签元素节点,插入父亲元素
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
// 3、创建注释节点,插入到父亲元素
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
// 4、创建文本节点,插入到父亲元素
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
下面对动态创建的几种情况分别进行说明。
3.1 创建组件节点
创建组件节点和 vue 的组件系统息息相关,这里先不具体展开,之后的博文中单独分析 vue 组件系统。只需要记住 vue 模板里的子组件初始化创建,是在这一步进行即可。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
// 创建组件节点
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
...
}
createComponent 这个方法也定义在 src/core/vdom/patch.js 的 createPatchFunction 的方法中,这里先简单的介绍一下这个方法的内部逻辑。
通过 vnode.data 中是否包含组件相关的 hook,来判断当前 vnode 是否是子组件 vnode(组件的 vnode,会包含 init 等钩子方法)。
调用 init,执行子组件的初始化流程,创建子组件实例,进行子组件挂载。
将生成的子组件 DOM 赋值给 vnode.elm。
通过 vnode.elm 将创建的子组件节点,插入到父亲元素中。
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 */)
}
// 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.elm 占位符
// 将生成的组件节点,插入到父亲元素中
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
如果是创建组件节点,并且成功,createComponent 函数返回 true。createElm 函数执行到 return。
如果是其他类型的节点,createComponent 函数返回 undefined,createElm 函数,会向下执行创建其他类型节点(标签元素、注释、文本)的代码逻辑。
综上所述,createElm 函数执行,只要碰到组件标签,会递归的去初始化创建子组件,简图如下所示(绿色线路部分)。
再调用 insert(parentElm, vnode.elm, refElm),将生成的组件节点插入到父亲元素中(遵从先子后父)。
3.2 创建标签元素节点
createElm
判断如果 vnode 不是组件的 vnode,它会判断是否是标签元素,从而进行创建标签元素节点的代码逻辑, 主要逻辑分析如下。
vnode.tag 标签属性存在,通过 tag 创建对应的标签元素,赋值给 vnode.elm 进行占位。
调用 createChildren 创建子节点(遍历子vnode,递归调用 createElm 函数)。
将创建的标签元素节点,插入父亲元素。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
// 2、如果是标签标记,先创建标签元素进行占位。
// 调用 createChildren 创建子节点(遍历子vnode,递归调用 createElm 函数)。
// 将标签元素,插入父亲元素。
// 如果标签属性不为空
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
)
}
}
// 通过上面的tag,创建标签元素,赋值给 vnode.elm 进行占位
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
...
} else {
// 创建子节点
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
// vnode.data 不为空,调用所有create的钩子。
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 将创建的标签元素节点,插入父亲元素
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
...
}
createChildren
函数主要逻辑如下
如果 vnode.children 是子 vnode 数组,遍历 vnode.children 中的每个子 vnode,递归的调用了 createElm 函数,创建对应的子节点,并插入到父亲元素中(此时的父亲元素 parentElm 为 vnode.elm)。
如果 vnode.text 为空字符串。就创建一个空文本节点,插入到 vnode.elm 元素中。
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(children)
}
// 遍历子vnode数组,递归调用 createElm
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
// 创建空文本节点,appendChildren 到 vnode.elm 中
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
上面已经创建完成子标签节点,invokeCreateHooks 调用执行所有子组件相关的 create 钩子。这个方法createElm、
initComponent 中都会被调用。如果在 initComponent 中调用,说明创建的子节点中有组件节点,还会将组件 vnode 添加到 insertedVnodeQueue 队列中。
// createElm 中
if (isDef(data)) {
// vnode.data 不为空,调用所有create的钩子。
invokeCreateHooks(vnode, insertedVnodeQueue)
}
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
// 所有组件相关的create钩子都调用
// initComponent调用的话,还会将各个子组件的 vnode 添加到 insertedVnodeQueue 队列中。
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
综上所述,createElm 创建标签节点内部通过 createChildren 实现了对 createElm 的遍历递归调用,实现了深度优先遍历,简图如下所示(蓝色线路部分)。
再调用 insert(parentElm, vnode.elm, refElm),将生成的元素节点插入到父亲元素中(遵从先子后父)。
3.3 创建注释节点
如果不是创建组件节点和元素节点,vue 就通过 vnode.isComment 属性判断,是否创建注释节点。创建完成之后,插入到父亲元素中(遵从先子后父)
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
3.3 创建文本节点
如果不是创建组件节点、元素节点、注释节点,vue 就创建文本节点,创建完成之后,插入到父亲元素中(遵从先子后父)。
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
四、销毁旧节点
通过前面章节的分析,知道了 patch 函数,主要通过 createElm 动态的创建好了 DOM,并且已经成功添加到了旧DOM的后面,所以下一步操作,就只需要将旧 DOM 进行删除即可。
// destroy old node
// 销毁旧的节点(如 el: "app" 这个DOM)
// 创建完成的整个dom会append到 el: "app", 的父亲元素(如 parentElm 为 body)上
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
五、总结
vue 通过调用 patch 函数进行初始化 DOM 的创建。
patch 的关键是理解内部 createElm 这个函数,它会判断组件、元素、注释、文本这些类型的节点,来创建相应的DOM,完成之后添加到父元素。
vue 的组件系统实现,关键在于动态创建组件节点的逻辑当中。
新 DOM 创建添加过程是从子到父的,而组件的实例化是从父到子的。
Vue.js源码解析-Vue初始化流程之动态创建DOM的更多相关文章
- Vue.js源码解析-Vue初始化流程
目录 前言 1. 初始化流程概述图.代码流程图 1.1 初始化流程概述 1.2 初始化代码执行流程图 2. 初始化相关代码分析 2.1 initGlobalAPI(Vue) 初始化Vue的全局静态AP ...
- Vue.js源码解析-从scripts脚本看vue构建
目录 1. scripts 脚本构建 1.1 dev 开发环境构建过程 1.1.1 配置文件代码 1.1.2 如何进行代码调试? 1.2 build 生产环境构建过程 1.2.1 scripts/bu ...
- 从Vue.js源码角度再看数据绑定
写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ...
- Vue.js 源码分析(一) 代码结构
关于Vue vue是一个兴起的前端js库,是一个精简的MVVM.MVVM模式是由经典的软件架构MVC衍生来的,当View(视图层)变化时,会自动更新到ViewModel(视图模型),反之亦然,View ...
- 从template到DOM(Vue.js源码角度看内部运行机制)
写在前面 这篇文章算是对最近写的一系列Vue.js源码的文章(https://github.com/answershuto/learnVue)的总结吧,在阅读源码的过程中也确实受益匪浅,希望自己的这些 ...
- Vue.js 源码构建(三)
Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下. 构建脚本 通常一个基于 NPM 托管的项目都会有一个 package.json 文件,它是对项目的描述文 ...
- vue.js源码精析
MVVM大比拼之vue.js源码精析 VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多 ...
- Vue.js源码——事件机制
写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出.文章的原地址:https://github.com/an ...
- 2018-11-23 手工翻译Vue.js源码:尝试重命名标识符与文本
续前文: 手工翻译Vue.js源码第一步:14个文件重命名 对core/instance/索引中的变量, 方法进行重命名如下(题图): import { 混入初始化 } from './初始化' im ...
随机推荐
- CVPR2020论文解读:CNN合成的图片鉴别
CVPR2020论文解读:CNN合成的图片鉴别 <CNN-generated images are surprisingly easy to spot... for now> 论文链接:h ...
- NVIDIA GPU上的Tensor线性代数
NVIDIA GPU上的Tensor线性代数 cuTENSOR库是同类中第一个GPU加速的张量线性代数库,提供张量收缩,归约和逐元素运算.cuTENSOR用于加速在深度学习训练和推理,计算机视觉,量子 ...
- AlexeyAB DarkNet YOLOv3框架解析与应用实践(三)
AlexeyAB DarkNet YOLOv3框架解析与应用实践(三) ImageNet分类 您可以使用Darknet为1000级ImageNet挑战赛分类图像.如果你还没有安装Darknet,你应该 ...
- 自然语言推理:微调BERT
自然语言推理:微调BERT Natural Language Inference: Fine-Tuning BERT SNLI数据集上的自然语言推理任务设计了一个基于注意力的体系结构.现在通过微调BE ...
- Java IO学习笔记一:为什么带Buffer的比不带Buffer的快
作者:Grey 原文地址:Java IO学习笔记一:为什么带Buffer的比不带Buffer的快 Java中为什么BufferedReader,BufferedWriter要比FileReader 和 ...
- java面试必知必会——排序
二.排序 时间复杂度分析 排序算法 平均时间复杂度 最好 最坏 空间复杂度 稳定性 冒泡 O(n²) O(n) O(n²) O(1) 稳定 选择 O(n²) O(n²) O(n²) O(1) 不稳定 ...
- 尚硅谷Java——宋红康笔记【day11-day18】
day11 Eclipse中的快捷键: * 1.补全代码的声明:alt + / * 2.快速修复: ctrl + 1 * 3.批量导包:ctrl + shift + o * 4.使用单行注释:ctrl ...
- Spring:DI依赖注入的几种方式
据我所学,spring实现依赖注入(DI)的方式分为三大类:基于构造器(构造方法)的依赖注入.基于setter的依赖注入.其他方式(c命名空间.p命名空间等).其中推荐使用setter方法注入,这种注 ...
- APP的闪退和无响应
1.app闪退(crash,崩溃):程序异常退出不再运行 闪退的原因: a.程序内部逻辑错误(因算法或网络连接引起的异常,或是为捕捉到的错误) b.系统自身异常:一般自定ROM或刷机后比较常见 c.运 ...
- excel VBA一个fuction同时执行多个正则表达式,实现方法
代码: Function zhengze3(ze1 As String, ze2 As String, Rng1 As Range, Rng2 As Range) Set regx1 = Cre ...