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的实现相关推荐

  1. 【Vue.js源码解析 二】-- 虚拟 DOM

    前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 虚拟 DOM 基本介绍 什么是虚拟 DOM 虚拟 DOM(Virtual DOM) 是使用 JavaScript 对象 ...

  2. Vue 获取DOM元素 ,给DOM增加事件的特殊情况

    Vue 获取DOM元素 ,给DOM增加事件的特殊情况 给标签绑定ref属性 使用this.$refs.xxx 获取原生的jsDOM对象 ref属性值不能重名 Vue.component('subCom ...

  3. React:DOM树与虚拟DOM树(概念与区别)

    React:DOM树与虚拟DOM树(概念与区别) DOM的本质: 浏览器中的概念,用JS对象来表示页面上的元素,并提供了操作DOM对象的API: React中的虚拟DOM: 是框架中的概念,是程序员 ...

  4. vue 如何生成一个dom元素_vue 学习心得——DOM树如何被构建

    代码编辑不够友好,为更好一点的阅读体验,还是推荐原文链接:https://www.yuque.com/longtengsong/blog/bu09tk DOM 树是 vue 在构建实例的时候,通过 $ ...

  5. vue 如何生成一个dom元素_通过一个简单的示例学习如何编写Vue组件

    大家好,本篇文章我将带着大家一起学习如何编写自定义组件(Components),通过「vue基础」新手快速入门篇(一)这篇文章的学习,我们知道了 Vue 设计的目的就是为了方便我们创建基于组件UI的项 ...

  6. vue 获取dom子元素_vue获取dom元素注意事项

    mounted(){ setTimeout(()=>{ this.contentToggle(); },1000) }, methods:{ contentToggle(){ console.l ...

  7. vue 获取当前元素的父元素_vue获取dom元素内容

    通过ref来获取dom元素 在vue官网上对ref的解释 ref 被用来给元素或子组件注册引用信息.引用信息将会注册在父组件的 $refs 对象上.如果在普通的 DOM 元素上使用,引用指向的就是 D ...

  8. vue如何获取div的宽度_vue获取dom元素高度的方法

    获取DOM高度: 要在钩子mounted里面dom结构生成后去获取dom的高度,宽度,修改样式等操作(参照生命周期钩子mounted语法:https://cn.vuejs.org/v2/api/#mo ...

  9. Vue 原理解析(五)之 虚拟Dom 到真实Dom的转换过程

    上一篇 vue 原理解析(四): 虚拟Dom 是怎么生成的 再有一颗树形结构的Javascript对象后, 我们需要做的就是讲这棵树跟真实Dom树形成映射关系.我们先回顾之前的mountComponn ...

  10. vue 插入dom_vue内部复用问题以及虚拟dom的更新

    由于在浏览器中操作DOM的代价是非常"昂贵"的,所以才在Vue引入了Virtual DOM,Virtual DOM是对真实DOM的一种抽象描述.使用virtual DOM渲染页面时 ...

最新文章

  1. 通过ddmlib杀死某个android进程的方法
  2. iOS-pushMeBaby经典错误解决
  3. Redis源码剖析(三)字典结构的设计与实现
  4. 雷军反击董明珠:感觉董总好像认输了似的
  5. Linux系统编程---14(回收子线程,回收多个子线程,线程分离,杀死线程)
  6. python抓有趣的东西_Python 五个有趣的彩蛋,你都知道吗?
  7. linux 系统一键安装 lnmp
  8. bzoj3190 [JLOI2013]赛车 半平面交
  9. 如何提取sql语句中绑定变量的值?
  10. oracle 基本dos命令,Oracle 常用 Dos命令
  11. 基于线程池技术的web服务器
  12. c语言程序设计贪吃蛇报告,C语言“贪吃蛇”程序设计报告.doc
  13. Unexpandable Clocks不可扩展时钟 UG903
  14. Tegra TX1 build tensorflow r1.1
  15. SCRATCH编程与科学——简单电路
  16. 嵌入式开发需要学习哪些东西
  17. js 字符串转化成数字
  18. 记录一下大三暑假来广州的实习生活
  19. python提取图片频谱_Python提取音乐谱并将其可视化,频谱
  20. Mysql 1022

热门文章

  1. Word页码从正文开始-请务必文档格式要规范,这在你未来的工作中的细节是很重要的
  2. 程序员应该如何对待面试?
  3. 单元格内容分列多行_excel拆分单元格内容 excel单元格拆分多行
  4. 企业邮件服务器哪个好?常用邮箱客户端是哪个?
  5. 手动打开与关闭软键盘
  6. Android 强制关闭软键盘/修改软键盘状态——弹出或关闭
  7. asp空间和php空间_两个最新空间及回顾100Mphp及数个asp免费空间放
  8. DBeaver 离线安装
  9. model.load_state_dict(state_dict, strict=False)
  10. iOS企业ipa(299)证书制作、打包发布全流程