vue:虚拟dom的实现

news/2024/7/2 23:24:50

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写法是:
<ul id='list'><li class='item'>aa</li><li class='item'>aa</li><li class='item'>aa</li>
</ul>

Virtual DOM并没有完全实现DOMVirtual DOM最主要的还是保留了Element之间的层次关系和一些基本属性. 你给我一个数据,我根据这个数据生成一个全新的Virtual DOM,然后跟我上一次生成的Virtual DOMdiff,得到一个Patch,然后把这个Patch打到浏览器的DOM上去。

我们可以通过javascript对象表示的树结构来构建一棵真正的dom树,当数据状态发生变化时,可以直接修改这个javascript对象,接着对比修改后的javascript对象,记录下需要对页面做的dom操作,然后将其应用到真正的dom树,实现视图的更新,这个过程就是Virtual DOM的核心思想。

VNode的数据结构图:
clipboard.png

clipboard.png

VNode生成最关键的点是通过render2种生成方式,第一种是直接在vue对象的option中添加render字段。第二种是写一个模板或指定一个el根元素,它会首先转换成模板,经过html语法解析器生成一个ast抽象语法树,对语法树做优化,然后把语法树转换成代码片段,最后通过代码片段生成function添加到optionrender字段中。

ast语法优的过程,主要做了2件事:

  • 会检测出静态的class名和attributes,这样它们在初始化渲染后就永远不会再被比对了。
  • 会检测出最大的静态子树(不需要动态性的子树)并且从渲染函数中萃取出来。这样在每次重渲染时,它就会直接重用完全相同的vnode,同时跳过比对。
src/core/vdom/create-element.jsconst SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {// 兼容不传data的情况if (Array.isArray(data) || isPrimitive(data)) {normalizationType = childrenchildren = datadata = 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)) {// 如果是保留标签,就创建一个这样的vnodevnode = 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 {// 兜底方案,正常创建一个vnodevnode = new VNode(tag, data, children,undefined, undefined, context)}// 当tag不是字符串的时候,我们认为tag是组件的构造类// 所以直接创建} else {vnode = createComponent(tag, data, context, children)}// 如果有vnodeif (vnode) {// 如果有namespace,就应用下namespace,然后返回vnodeif (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.jsreturn 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 = falseconst insertedVnodeQueue = []if (isUndef(oldVnode)) {// 空挂载(可能是组件),创建新的根元素。isInitialPatch = truecreateElm(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 ' +'<p>, or missing <tbody>. Bailing hydration and performing ' +'full client-side render.')}}// 不是服务器呈现,就是水化失败。创建一个空节点并替换它。oldVnode = emptyNodeAt(oldVnode)}// 替换现有的元素const oldElm = oldVnode.elmconst parentElm = nodeOps.parentNode(oldElm)// create new nodecreateElm(vnode,insertedVnodeQueue,// 极为罕见的边缘情况:如果旧元素在a中,则不要插入。// 离开过渡。只有结合过渡+时才会发生。// keep-alive + HOCs. (#4590)oldElm._leaveCb ? null : parentElm,nodeOps.nextSibling(oldElm))// 递归地更新父占位符节点元素。if (isDef(vnode.parent)) {let ancestor = vnode.parentconst patchable = isPatchable(vnode)while (ancestor) {for (let i = 0; i < cbs.destroy.length; ++i) {cbs.destroy[i](ancestor)}ancestor.elm = vnode.elmif (patchable) {for (let i = 0; i < cbs.create.length; ++i) {cbs.create[i](emptyNode, ancestor)}// #6513// 调用插入钩子,这些钩子可能已经被创建钩子合并了。// 例如使用“插入”钩子的指令。const insert = ancestor.data.hook.insertif (insert.merged) {// 从索引1开始,以避免重新调用组件挂起的钩子。for (let i = 1; i < insert.fns.length; i++) {insert.fns[i]()}}} else {registerRef(ancestor)}ancestor = ancestor.parent}}// destroy old nodeif (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,vnodeVNode对象类型,它表示待替换的VNode,hydrationbool类型,它表示是否直接使用服务器端渲染的DOM元素,下面流程图表示patch的运行逻辑:

clipboard.png

patch运行逻辑看上去比较复杂,有2个方法createElmpatchVnode是生成dom的关键,源码如下:

/*** @param vnode根据vnode的数据结构创建真实的dom节点,如果vnode有children则会遍历这些子节点,递归调用createElm方法,* @param insertedVnodeQueue记录子节点创建顺序的队列,每创建一个dom元素就会往队列中插入当前的vnode,当整个vnode对象全部转换成为真实的dom 树时,会依次调用这个队列中vnode hook的insert方法*/let inPre = 0function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {vnode.isRootInsert = !nested // 过渡进入检查if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {return}const data = vnode.dataconst children = vnode.childrenconst tag = vnode.tagif (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: <' + tag + '> - 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节点,如果vnodechildren,则会遍历这些子节点,递归调用createElm方法,InsertedVnodeQueue是记录子节点创建顺序的队列,每创建一个DOM元素就会往这个队列中插入当前的VNode,当整个VNode对象全部转换成为真实的DOM树时,会依次调用这个队列中的VNode hookinsert方法。

/*** 比较新旧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.elmif (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.componentInstancereturn}let iconst data = vnode.dataif (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)}const oldCh = oldVnode.childrenconst ch = vnode.childrenif (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


http://lihuaxi.xjx100.cn/news/238812.html

相关文章

Mac MySQL配置环境变量的两种方法

第一种&#xff1a; 1.打开终端,输入&#xff1a; cd ~ 会进入~文件夹 2.然后输入&#xff1a;touch .bash_profile 回车执行后&#xff0c; 3.再输入&#xff1a;open -e .bash_profile 会在TextEdit中打开这个文件&#xff08;如果以前没有配置过环境变量&#xff0c;那么这…

再谈HOST文件

前几天弄了一个关于禁止打开某个网站的文章后&#xff0c;觉得这个HOST文件真的挺有意思的。并且也总是想把自己对它新的理解写下来&#xff08;也许大家都明白了&#xff09;以下是HOST文件的内容&#xff1a;# Copyright (c) 1993-1999 Microsoft Corp.## This is a sample H…

Dubbo配置文件详解

为新项目练手&#xff0c;把项目中用到的web service、RMI的服务改用DubboZookeeperSpring&#xff0c;网上找到几篇不错的配置详解 1.此篇博文主要从以下几种配置方式来讲 XML 配置文件方式、XML 配置文件方式、annotation 配置方式 https://www.cnblogs.com/chanshuyi/p/514…

phpinfo 信息利用

0x01 基础信息 1.system info:提供详细的操作系统信息&#xff0c;为提权做准备。 2.extension_dir:php扩展的路径 3.$_SERVER[‘HTTP_HOST’]:网站真实IP、CDN什么的都不存在的&#xff0c;找到真实ip&#xff0c;扫一扫旁站&#xff0c;没准就拿下几个站。 4.$_SERVER[‘…

brain.js 时间序列_免费的Brain JS课程学习JavaScript中的神经网络

brain.js 时间序列The last few years, machine learning has gone from a promising technology to something we’re surrounded with on a daily basis. And at the heart of many machine learning systems lies neural networks.在过去的几年中&#xff0c;机器学习已经从…

学习笔记TF065:TensorFlowOnSpark

2019独角兽企业重金招聘Python工程师标准>>> Hadoop生态大数据系统分为Yam、 HDFS、MapReduce计算框架。TensorFlow分布式相当于MapReduce计算框架&#xff0c;Kubernetes相当于Yam调度系统。TensorFlowOnSpark&#xff0c;利用远程直接内存访问(Remote Direct Memo…

CTO 基本功大盘点 —— 没有这些技能,谈何远大前程?

本文由 「TGO鲲鹏会」原创&#xff0c;原文链接&#xff1a;CTO 基本功大盘点 —— 没有这些技能&#xff0c;谈何远大前程&#xff1f; 作者&#xff5c;刘海星 2018 年马上就要过去六分之一了&#xff0c;你的 KPI 完成多少了&#xff1f; 别沮丧&#xff0c;其实我想说的是&…

vim编辑器异常退出产生备份文件

当非正常关闭vim编辑器时&#xff08;比如直接关闭终端或者电脑断电&#xff09;&#xff0c;会生成一个.swp文件&#xff0c;这个文件是一个临时交换文件&#xff0c;用来备份缓冲区中的内容。 需要注意的是如果你并没有对文件进行修改&#xff0c;而只是读取文件&#xff0c…