vue 如何生成一个dom元素_vue:虚拟dom的实现
Vitual DOM是一种虚拟dom技术,本质上是基于javascript实现的,相对于dom对象,javascript对象更简单,处理速度更快,dom树的结构,属性信息都可以很容易的用javascript对象来表示:
let element={
tagName:'ul',//节点标签名
props:{//dom的属性,用一个对象存储键值对
id:'list'
},
children:[//该节点的子节点
{tagName:'li',props:{class:'item'},children:['aa']},
{tagName:'li',props:{class:'item'},children:['bb']},
{tagName:'li',props:{class:'item'},children:['cc']}
]
}
对应的html写法是:
- aa
- aa
- aa
Virtual DOM并没有完全实现DOM,Virtual DOM最主要的还是保留了Element之间的层次关系和一些基本属性. 你给我一个数据,我根据这个数据生成一个全新的Virtual DOM,然后跟我上一次生成的Virtual DOM去 diff,得到一个Patch,然后把这个Patch打到浏览器的DOM上去。
我们可以通过javascript对象表示的树结构来构建一棵真正的dom树,当数据状态发生变化时,可以直接修改这个javascript对象,接着对比修改后的javascript对象,记录下需要对页面做的dom操作,然后将其应用到真正的dom树,实现视图的更新,这个过程就是Virtual DOM的核心思想。
VNode的数据结构图:
VNode生成最关键的点是通过render有2种生成方式,第一种是直接在vue对象的option中添加render字段。第二种是写一个模板或指定一个el根元素,它会首先转换成模板,经过html语法解析器生成一个ast抽象语法树,对语法树做优化,然后把语法树转换成代码片段,最后通过代码片段生成function添加到option的render字段中。
ast语法优的过程,主要做了2件事:
会检测出静态的class名和attributes,这样它们在初始化渲染后就永远不会再被比对了。
会检测出最大的静态子树(不需要动态性的子树)并且从渲染函数中萃取出来。这样在每次重渲染时,它就会直接重用完全相同的vnode,同时跳过比对。
src/core/vdom/create-element.js
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {
// 兼容不传data的情况
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
// 如果alwaysNormalize是true
// 那么normalizationType应该设置为常量ALWAYS_NORMALIZE的值
if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
// 调用_createElement创建虚拟节点
return _createElement(context, tag, data, children, normalizationType)
}
function _createElement (context, tag, data, children, normalizationType) {
/**
* 如果存在data.__ob__,说明data是被Observer观察的数据
* 不能用作虚拟节点的data
* 需要抛出警告,并返回一个空节点
* 被监控的data不能被用作vnode渲染的数据的原因是:
* data在vnode渲染过程中可能会被改变,这样会触发监控,导致不符合预期的操作
*/
if (data && data.__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()
}
// 当组件的is属性被设置为一个falsy的值
// Vue将不会知道要把这个组件渲染成什么
// 所以渲染一个空节点
if (!tag) {
return createEmptyVNode()
}
// 作用域插槽
if (Array.isArray(children) &&
typeof children[0] === 'function') {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 根据normalizationType的值,选择不同的处理方法
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 = config.getTagNamespace(tag)
// 判断是否为保留标签
if (config.isReservedTag(tag)) {
// 如果是保留标签,就创建一个这样的vnode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
// 如果不是保留标签,那么我们将尝试从vm的components上查找是否有这个标签的定义
} else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
// 如果找到了这个标签的定义,就以此创建虚拟组件节点
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 兜底方案,正常创建一个vnode
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
// 当tag不是字符串的时候,我们认为tag是组件的构造类
// 所以直接创建
} else {
vnode = createComponent(tag, data, context, children)
}
// 如果有vnode
if (vnode) {
// 如果有namespace,就应用下namespace,然后返回vnode
if (ns) applyNS(vnode, ns)
return vnode
// 否则,返回一个空节点
} else {
return createEmptyVNode()
}
}
方法的功能是给一个Vnode对象对象添加若干个子Vnode,因为整个Virtual DOM是一种树状结构,每个节点都可能会有若干子节点。然后创建一个VNode对象,如果是一个reserved tag(比如html,head等一些合法的html标签)则会创建普通的DOM VNode,如果是一个component tag(通过vue注册的自定义component),则会创建Component VNode对象,它的VnodeComponentOptions不为Null.
创建好Vnode,下一步就是要把Virtual DOM渲染成真正的DOM,是通过patch来实现的,源码如下:
src/core/vdom/patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { // oldVnoe:dom||当前vnode,vnode:vnoder=对象类型,hydration是否直接用服务端渲染的dom元素
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// 空挂载(可能是组件),创建新的根元素。
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch 现有的根节点
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
// 安装到一个真实的元素。
// 检查这是否是服务器渲染的内容,如果我们可以执行。
// 成功的水合作用。
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'
, or missing
. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// 不是服务器呈现,就是水化失败。创建一个空节点并替换它。
oldVnode = emptyNodeAt(oldVnode)
}
// 替换现有的元素
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// 极为罕见的边缘情况:如果旧元素在a中,则不要插入。
// 离开过渡。只有结合过渡+时才会发生。
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 递归地更新父占位符节点元素。
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// 调用插入钩子,这些钩子可能已经被创建钩子合并了。
// 例如使用“插入”钩子的指令。
const insert = ancestor.data.hook.insert
if (insert.merged) {
// 从索引1开始,以避免重新调用组件挂起的钩子。
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
patch支持的3个参数,其中oldVnode是一个真实的DOM或者一个VNode对象,它表示当前的VNode,vnode是VNode对象类型,它表示待替换的VNode,hydration是bool类型,它表示是否直接使用服务器端渲染的DOM元素,下面流程图表示patch的运行逻辑:
patch运行逻辑看上去比较复杂,有2个方法createElm和patchVnode是生成dom的关键,源码如下:
/**
* @param vnode根据vnode的数据结构创建真实的dom节点,如果vnode有children则会遍历这些子节点,递归调用createElm方法,
* @param insertedVnodeQueue记录子节点创建顺序的队列,每创建一个dom元素就会往队列中插入当前的vnode,当整个vnode对象全部转换成为真实的dom 树时,会依次调用这个队列中vnode hook的insert方法
*/
let inPre = 0
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
vnode.isRootInsert = !nested // 过渡进入检查
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
inPre++
}
if (
!inPre &&
!vnode.ns &&
!(
config.ignoredElements.length &&
config.ignoredElements.some(ignore => {
return isRegExp(ignore)
? ignore.test(tag)
: ignore === tag
})
) &&
config.isUnknownElement(tag)
) {
warn(
'Unknown custom element: - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
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) {
inPre--
}
} 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)
}
}
方法会根据vnode的数据结构创建真实的DOM节点,如果vnode有children,则会遍历这些子节点,递归调用createElm方法,InsertedVnodeQueue是记录子节点创建顺序的队列,每创建一个DOM元素就会往这个队列中插入当前的VNode,当整个VNode对象全部转换成为真实的DOM树时,会依次调用这个队列中的VNode hook的insert方法。
/**
* 比较新旧vnode节点,根据不同的状态对dom做合理的更新操作(添加,移动,删除)整个过程还会依次调用prepatch,update,postpatch等钩子函数,在编译阶段生成的一些静态子树,在这个过程
* @param oldVnode 中由于不会改变而直接跳过比对,动态子树在比较过程中比较核心的部分就是当新旧vnode同时存在children,通过updateChildren方法对子节点做更新,
* @param vnode
* @param insertedVnodeQueue
* @param removeOnly
*/
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
if (oldVnode === vnode) {
return
}
const elm = vnode.elm = oldVnode.elm
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 用于静态树的重用元素。
// 注意,如果vnode是克隆的,我们只做这个。
// 如果新节点不是克隆的,则表示呈现函数。
// 由热重加载api重新设置,我们需要进行适当的重新渲染。
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
updateChildren方法解析在此:vue:虚拟DOM的patch
vue 如何生成一个dom元素_vue:虚拟dom的实现相关推荐
- 【Vue.js源码解析 二】-- 虚拟 DOM
前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 虚拟 DOM 基本介绍 什么是虚拟 DOM 虚拟 DOM(Virtual DOM) 是使用 JavaScript 对象 ...
- Vue 获取DOM元素 ,给DOM增加事件的特殊情况
Vue 获取DOM元素 ,给DOM增加事件的特殊情况 给标签绑定ref属性 使用this.$refs.xxx 获取原生的jsDOM对象 ref属性值不能重名 Vue.component('subCom ...
- React:DOM树与虚拟DOM树(概念与区别)
React:DOM树与虚拟DOM树(概念与区别) DOM的本质: 浏览器中的概念,用JS对象来表示页面上的元素,并提供了操作DOM对象的API: React中的虚拟DOM: 是框架中的概念,是程序员 ...
- vue 如何生成一个dom元素_vue 学习心得——DOM树如何被构建
代码编辑不够友好,为更好一点的阅读体验,还是推荐原文链接:https://www.yuque.com/longtengsong/blog/bu09tk DOM 树是 vue 在构建实例的时候,通过 $ ...
- vue 如何生成一个dom元素_通过一个简单的示例学习如何编写Vue组件
大家好,本篇文章我将带着大家一起学习如何编写自定义组件(Components),通过「vue基础」新手快速入门篇(一)这篇文章的学习,我们知道了 Vue 设计的目的就是为了方便我们创建基于组件UI的项 ...
- vue 获取dom子元素_vue获取dom元素注意事项
mounted(){ setTimeout(()=>{ this.contentToggle(); },1000) }, methods:{ contentToggle(){ console.l ...
- vue 获取当前元素的父元素_vue获取dom元素内容
通过ref来获取dom元素 在vue官网上对ref的解释 ref 被用来给元素或子组件注册引用信息.引用信息将会注册在父组件的 $refs 对象上.如果在普通的 DOM 元素上使用,引用指向的就是 D ...
- vue如何获取div的宽度_vue获取dom元素高度的方法
获取DOM高度: 要在钩子mounted里面dom结构生成后去获取dom的高度,宽度,修改样式等操作(参照生命周期钩子mounted语法:https://cn.vuejs.org/v2/api/#mo ...
- Vue 原理解析(五)之 虚拟Dom 到真实Dom的转换过程
上一篇 vue 原理解析(四): 虚拟Dom 是怎么生成的 再有一颗树形结构的Javascript对象后, 我们需要做的就是讲这棵树跟真实Dom树形成映射关系.我们先回顾之前的mountComponn ...
- vue 插入dom_vue内部复用问题以及虚拟dom的更新
由于在浏览器中操作DOM的代价是非常"昂贵"的,所以才在Vue引入了Virtual DOM,Virtual DOM是对真实DOM的一种抽象描述.使用virtual DOM渲染页面时 ...
最新文章
- 通过ddmlib杀死某个android进程的方法
- iOS-pushMeBaby经典错误解决
- Redis源码剖析(三)字典结构的设计与实现
- 雷军反击董明珠:感觉董总好像认输了似的
- Linux系统编程---14(回收子线程,回收多个子线程,线程分离,杀死线程)
- python抓有趣的东西_Python 五个有趣的彩蛋,你都知道吗?
- linux 系统一键安装 lnmp
- bzoj3190 [JLOI2013]赛车 半平面交
- 如何提取sql语句中绑定变量的值?
- oracle 基本dos命令,Oracle 常用 Dos命令
- 基于线程池技术的web服务器
- c语言程序设计贪吃蛇报告,C语言“贪吃蛇”程序设计报告.doc
- Unexpandable Clocks不可扩展时钟 UG903
- Tegra TX1 build tensorflow r1.1
- SCRATCH编程与科学——简单电路
- 嵌入式开发需要学习哪些东西
- js 字符串转化成数字
- 记录一下大三暑假来广州的实习生活
- python提取图片频谱_Python提取音乐谱并将其可视化,频谱
- Mysql 1022
热门文章
- Word页码从正文开始-请务必文档格式要规范,这在你未来的工作中的细节是很重要的
- 程序员应该如何对待面试?
- 单元格内容分列多行_excel拆分单元格内容 excel单元格拆分多行
- 企业邮件服务器哪个好?常用邮箱客户端是哪个?
- 手动打开与关闭软键盘
- Android 强制关闭软键盘/修改软键盘状态——弹出或关闭
- asp空间和php空间_两个最新空间及回顾100Mphp及数个asp免费空间放
- DBeaver 离线安装
- model.load_state_dict(state_dict, strict=False)
- iOS企业ipa(299)证书制作、打包发布全流程