vue 如何生成一个dom元素_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的数据结构图:

50af9fa6eeb0b441b1a340aab2b69804.png

078cde5bfde93d7f7d942ff7c5c22a5f.png

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的运行逻辑:

58039c2fa70fcb507ee37cf67a7ddedd.png

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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_34245171/article/details/113002635

智能推荐

unity中fixedUpdate和Update的区别_差点忘记打铁了的博客-程序员秘密

下面这段代码演示游戏暂停using UnityEngine;using System.Collections;public class GamePauseTest : MonoBehaviour{ public float moveSpeed = 2.0f; void Update () { //move transform.Transla

2017东北长春第十届国际装备制造业博览会、2017第十三届汽车零部件展览会会刊(参展商名录)_华展云的博客-程序员秘密

2017东北长春第十届国际装备制造业博览会、2017第十三届汽车零部件展览会会刊(参展商名录)

股市公式编辑器_JackieFrederickHYZ的博客-程序员秘密

通达信公式教程公式入门我们大多数的用户并不是完全了解“公式编辑器”的意义,简单地,我们可以从以下几个角度进行理解:一、指标分析:“公式编辑器”好比是一个工作母床,通过这个工作母床可以制造出所需要的各式各样的零件,同样,在指标分析的工作中,利用编辑器可以编写出相应的分析条件,这种方法是在技术分析当中最为常用的方法之一。例如,指标KD、指标MA等等,通过对这些指标的观察、分析,找出一些

Kaggle入门:Digit Recognizer_你的指尖有改变世界的力量的博客-程序员秘密

Kaggle入门:Digit Recognizer数据是经典的lecun大神的mnist手写数字数据集。采用tensorflow进行训练。

解决ScrollView 嵌套RecyclerView 给RecyclerView设置空布局时布局不能撑满_recycleview 布局无法沾满空间_淡淡的季节-的博客-程序员秘密

解决ScrollView 嵌套RecyclerView 给RecyclerView设置空布局时布局不能撑满写这篇文章是也是为了记录一下工作中遇到的问题最近在维护一个老项目 代码是以前同事写的 发现一些问题其中用RecyclerView的时候如果数据为空会给Adapter设置EmptyView(无内容布局) 如果这个RecyclerView被ScrollView或者NestedScrollView嵌套,并且ScrollView里还嵌套了一些别的布局和控件,这时候就会出现设置EmptyView不能

idea如何调试服务器linux,Idea如何远程连接linux服务器进行debug操作-Fun言_龚子仪的博客-程序员秘密

前言:在日常开发中,经常会遇到在本地运行程序一切正常,但是只要上传到服务器就错误百出,但是程序是已经打包好的,那么该如何发现错误呢,所以今天教大家如何在本地idea编辑器上远程debug放在linux服务器上的程序。第一步:上传jar包在本地通过mvn clean package install -Dmaven.test.skip=true命令将本地程序打成jar包。第二步:Idea设置1、Run...

随便推点

Refused to display because an ancestor violates the following Content Security问题简单解决_AnNong。的博客-程序员秘密

Refused to display because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'self'

C++学习(四) 深度剖析堆与栈_pjw100的博客-程序员秘密

C++的指针是C++的基础也是它的核心内容,在学习指针时,我们需要对内存空间进行剖析和理解,了解内存,学习起指针我认为是有事半功倍的效果的。一、预备知识—程序的内存分配一个由c/C++编译的程序占用的内存分为以下几个部分 1、栈区(stack)—   由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 2、堆区(heap) —   一

Vue学习笔记_JTOOP的博客-程序员秘密

文章目录一、Vue模板语法视图更新注意事项二、事件的绑定1、双向绑定v-model2、计算属性3、set4、监听属性5、表单input输入绑定6、修饰符三、自定义组件1、自定义组件定义2、给组件添加属性3、单一根元素4、事件的传递5、自定义组件v-model6、插槽7、插槽的命名8、生命周期函数四、过滤器五、图书管理系统的案例六、Vue-Router路由1、动态路由2、错误页面3、嵌套路由4、编程式导航5、命名路由6、命名视图一、Vue模板语法视图更新注意事项1、直接修改数组中的值 不会触发视图更新

用php制作抖音视频去水印小程序?_php 抖音去水印_weixin_53666293的博客-程序员秘密

一、原理抖音视频是在下载的时候才加水印的,播放的时候无水印,所以原理就是利用PHP或其它语言去抓取这个用于播放的无水印的视频地址(大概走了 3 次重定向);二、PHP 实现如下```php&lt;?php// 测试地址$url = "https://v.douyin.com/7J48Rf/"; // 模拟手机端浏览器function http_get($url){ $ch = curl_init(); $opt = []; $opt[CURLOPT_URL]

常用查找算法_Abner_Niu的博客-程序员秘密

1,顺序查找条件:无原理:按顺序比较每个元素,直到找到或遍历完元素为止时间复杂度:O(n) private static int findSequent(int[] arr,int k){ int p = -1; for(int i=0;i<arr.length;i++){ if(k == arr[i]){

那群“沙雕”年轻人,正在闲鱼上蹦迪_背地里闲鱼_螳螂观察的博客-程序员秘密

(图片来源于网络)文 | 易不二来源 | 螳螂财经(ID:TanglangFin)闲鱼从来就不是一只没有故事的“鱼”同学。女生想要买彩妆包包,搜一搜“舔狗送的”,就能看到不少“十动然鱼”的宝贝;男生想入科技数码产品,查找一下“老婆/女朋友不让玩”,就能打开新世界的大门;在此之外,搬家、公司年会这些关键词下,或许也能促成“卖家想变现,买家想省钱”的双边心愿。除了“十动然鱼”、...