Virtual DOM的实现原理

Virtual DOM的实现原理

课程目标

  • 了解什么是虚拟DOM,以及虚拟DOM的作用
  • Snabbdom的基本使用(Vue内部的虚拟Dom是改造了开源库Snabbdom
  • Snabbdom的源码解析

在面试的时候经常会问到虚拟DOM是怎么工作的,通过查看Snabbdom源码,可以对这块内容有更加深入的了解。

1、什么是Virtual DOM

Virtual Dom(虚拟DOM),是由普通的JS对象来描述DOM对象,因为不是真实的DOM对象,所以叫做Virtual DOM.

我们为什么用虚拟DOM来模拟真实的DOM呢?

因为我们知道一个DOM对象中的成员是非常多。所以创建Dom对象的成本非常高。

如果使用虚拟Dom来描述真实Dom,就会发现创建的成员少,成本也就低了。

2、为什么使用Virtual DOM

  • 手动操作Dom比较麻烦,还需要考虑浏览器兼容性问题,虽然有Jquery等库简化DOM操作,但是随着项目的复杂度越来越高,DOM操作复杂提升,既要考虑Dom操作,还要考虑数据的操作。

  • 为了简化DOM的复杂操作于是出现了各种的MVVM框架,MVVM框架解决了视图和状态的同步问题,也就是当数据发生变化,更新视图,当视图发生变化更新数据。

  • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题(当数据发生了变化后,无法获取上一次的状态,只有将页面上的元素删除,然后在重新创建,这时页面有刷新的问题,同时频繁操作Dom,性能也会非常低),于是Virtual Dom出现了。

  • Virtual Dom的好处就是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述DOMVirtual Dom内部将弄清楚如何有效(diff)的更新DOM.(例如:向用户添加列表中添加一个用户,只添加新的内容,原有的结构会被重用)

    下面,我们看一段代码,该代码是使用jquery来实现的数据展示与排序,是纯DOM操作的方式

    <!DOCTYPE html>
    <html>
    <head><title></title><meta charset="utf-8"><script type="text/javascript" src="https://code.jquery.com/jquery-1.11.3.js"></script>
    </head>
    <body><div id="app"></div><div id="sort" style="margin-top: 20px;">按年纪排序</div><script type="text/javascript">var datas = [{ 'name': 'kongzhi11', 'age': 32 },{ 'name': 'kongzhi44', 'age': 29 },{ 'name': 'kongzhi22', 'age': 31 },{ 'name': 'kongzhi33', 'age': 30 }];var render = function() {var html = '';datas.forEach(function(item, index) {html += `<li><div class="u-cls"><span class="name">姓名:${item.name}</span><span class="age" style="margin-left:20px;">年龄:${item.age}</span><span class="closed">x</span></div></li>`;});return html;};$("#app").html(render());$('#sort').on('click', function() {datas = datas.sort(function(a, b) {return a.age - b.age;});$('#app').html(render());})</script>
    </body>
    </html>
    

    如上demo排序,虽然在使用jquery时代这种方式是可行的,我们点击按钮,它就可以从小到大的排序,但是它比较暴力,它会将之前的dom全部删除,然后重新渲染新的dom节点,我们知道,操作DOM会影响页面的性能,并且有时候数据根本就没有发生改变,我们希望未更改的数据不需要重新渲染操作。

    因此虚拟DOM的思想就出来了,虚拟DOM的思想是先控制数据再到视图,但是数据状态是通过diff比对,它会比对新旧虚拟DOM节点,然后找出两者之前的不同,然后再把不同的节点再发生渲染操作。

    如下图所示:
    在这里插入图片描述

  • 总结:

    虚拟DOM可以维护程序的状态,跟踪上一次的状态

    通过比较前后两次状态的差异来更新真实DOM

3、虚拟DOM的作用

维护视图和状态的关系(虚拟DOM会记录状态的变化,只需要更新状态变化的内容就可以了)

复杂视图情况下提升渲染性能。

下面我们看一个案例,该案例的功能比较简单,单击按钮后,更新div中的内容。

let div=document.querySelector('#app')
let btn=document.querySelectory('#btn')
btn.onclick=function(){div.textContent='Hello World'
}

以上代码非常简单,而且是使用DOM操作的方式来实现的。

如果上面的案例,我们使用虚拟DOM来实现,应该怎样处理呢?首先,我们要创建一个虚拟DOM的对象,

虚拟DOM对象就是一个普通的JS对象。当单击按钮的时候,需要对比两次状态的差异。所以说,仅仅是该案例,

我们使用虚拟DOM的方式来实现,要比使用纯DOM的方式来实现,性能要低。

所以说,并不是所有的情况下使用虚拟DOM都会提升性能的。只有在视图比较复杂的情况下使用虚拟DOM才会提升渲染的性能。

虚拟DOM除了渲染DOM以外,还可以实现渲染到其它的平台,例如可以实现服务端渲染(ssr),原生应用(React Native),小程序(uni-app等)。以上列举的案例中,内部都使用了虚拟DOM.

Vue中虚拟DOM生成真实DOM的过程

在这里插入图片描述

下面我们重点要讲解的就是一个开源的虚拟DOM库—Snabbdom.

Vue2.x开始内部使用的虚拟DOM,就是改造的Snabbdom.

Snabbdom源码大约200行作用,可以通过模块来进行扩展,所以功能比较强大。

源码使用TypeScript开发,官方宣称是最块的Virtual Dom之一。

4、Snabbdom基本使用

4.1 创建项目

在讲解Snabbdom的基本使用之前,我们先来创建一个项目。

打包工具为了方便使用,使用了parcel,你也可以使用webpack.

下面创建项目,并安装parcel

//创建项目目录
md snabbdom-demo
// 进入项目目录cd snabbdom-demo
// 创建package.json
npm init -y
//本地安装parcel
npm install parcel-bundler

配置package.json中的scripts

 "srcipts":{"dev":"parcel index.html --open" , //open打开浏览器"build""parcel build index.html"}

创建目录结构

index.html
package.json
---srcbasicusage.js

4.2 导入Snabbdom

方法文档:

https://github.com/snabbdom/snabbdom

下面先安装Snabbdom.(这里最新版本有问题,可以先安装0.7.4)

npm install snabbdom@0.7.4

在项目的js文件夹下的01- basicusage.js文件中,添加如下代码:

import  snabbdom  from "snabbdom";
console.log(snabbdom);

以上代码的意思就是导入snabbdom这个包,然后打印其内容。

项目的启动

npm run dev

这时,开启的端口号为1234

http://localhost:1234

在打开的浏览器中,查看控制台的输出,发现输出的内容为undefined

为什么输出的是undefined呢?

这里我们需要查看对应的源码,

node_modules/snabbdom/snabbdom.js文件中,

我们可以看到在整个文件中,并没有使用export default的方式进行导出,所以就不能使用import snabbdom这种方式进行导入。同时在,源码中,我们可以看到导出了三项内容分别为h,thunk,init.

所以,导入的代码修改成如下的形式

import { h, thunk, init } from "snabbdom";
console.log(h, thunk, init);

Snabbdom的核心仅提供最基本的功能,只导出了三个函数init(),h( ),thunk( )

init函数是高阶函数,返回patch( ),一会在看该方法

h函数返回虚拟节点VNode,这个函数我们在使用Vue.js的时候见过。

h()函数用于创建虚拟DOM,在Snabbdom中用VNode描述虚拟节点,也就是虚拟DOM

new Vue({
router,
render:h=>h(App)
}).$mount('#app')

thunk函数是一种优化策略,可以在处理不可变数据时使用(用于优化复杂的视图)。

4.3 Snabbdom的基本使用

下面我们来看一下Snabbdom的基本使用,在01- basicusage.js文件中编写如下代码

import { h, thunk, init } from "snabbdom";
// init方法返回值为patch函数,patch函数作用是对比两个vndoe的差异并更新到真实的DOM中。init函数的参数是一个数组,数组中的内容是模块,关于模块内容后面还会讲解
let patch = init([]);
//创建虚拟DOM
// 第一个参数:标签+选择器(id选择器或者是类选择器)
// 第二个参数:如果是字符串的话就是标签中的内容
let vnode = h("div#container.cls", "Hello World");
//我们这里需要将创建的虚拟dom,最终要渲染到`index.html`中`app`这个div中,所以这里需要获取一下该div
let app = document.querySelector("#app");
//要想将虚拟DOM渲染到`app`中,需要用到patch函数。
// 我们知道patch函数的作用是对比两个vnode的差异来更新到真实的`DOM`中。
//但是我们目前没有两个虚拟DOM.那么patch方法的第一个参数也可以是真实的DOM.patch方法会将真实的DOM转换成VNode.
// 第二个参数:为VNode
//返回值为VNode
let oldNode = patch(app, vnode);

运行上面的代码,可以在浏览器中看到Hello World.

可以查看对应生成的元素。
在这里插入图片描述

下面我们再来看一个问题,假如在某个时刻需要重新获取服务端的数据,并且将获取到的数据重新渲染到该div中(id='container'的div)。

我们这里,就需要重新创建一个VNode, 然后传递给patch,让patch比较一下新的VNode与原有的VNode之间的差异。

补充后的代码如下:

import { h, thunk, init } from "snabbdom";
// init方法返回值为patch函数,patch函数作用是对比两个vndoe的差异并更新到真实的DOM中。init函数的参数是一个数组,数组中的内容是模块,关于模块内容后面还会讲解
let patch = init([]);
//创建虚拟DOM
// 第一个参数:标签+选择器(id选择器或者是类选择器)
// 第二个参数:如果是字符串的话就是标签中的内容
let vnode = h("div#container.cls", "Hello World");
//我们这里需要将创建的虚拟dom,最终要渲染到`index.html`中`app`这个div中,所以这里需要获取一下该div
let app = document.querySelector("#app");
//要想将虚拟DOM渲染到`app`中,需要用到patch函数。
// 我们知道patch函数的作用是对比两个vnode的差异来更新到真实的`DOM`中。
//但是我们目前没有两个虚拟DOM.那么patch方法的第一个参数也可以是真实的DOM.patch方法会将真实的DOM转换成VNode.
// 第二个参数:为VNode
//返回值为VNode
let oldNode = patch(app, vnode);
vnode = h("div", "Hello Vue");
patch(oldNode, vnode);

在上面的代码中,我们又创建了一个虚拟DOM ,vnode。然后把这个vnodeoldNode进行对比,最后渲染到页面中。

下面我们在做一个案例:

js目录下面在创建一个文件:02-basicusage.js

实现的代码如下:

// 本案例实现的要求是:在div中设置子元素h1,p
import { h, init } from "snabbdom";
let patch = init([]);
// h函数的第二个参数可以是一个数组,在该数组中添加所要创建的子元素。
let vnode = h("div#container", [h("h1", "hello world"),h("p", "这是一个p标签"),
]);
let app = document.querySelector("#app");
patch(app, vnode);

同时还需要修改index.html文件中的引入。

<body><div id="app"></div><script src="./js/02-basicusage.js"></script></body>

这时,可以在浏览器中查看更新后的内容。

下面我们再来看另外一个问题,就是模拟从服务器获取数据,然后更新页面中的内容。

在上面的代码中,再增加如下的内容:

// 本案例实现的要求是:在div中设置子元素h1,p
import { h, init } from "snabbdom";
let patch = init([]);
// h函数的第二个参数可以是一个数组,在该数组中添加所要创建的子元素。
let vnode = h("div#container", [h("h1", "hello world"),h("p", "这是一个p标签"),
]);
let app = document.querySelector("#app");
// 记录更新后的VNode
let oldVnode = patch(app, vnode);
setTimeout(() => {vnode = h("div#container", [h("h1", "Hello Vue"), h("p", "Hello p")]);patch(oldVnode, vnode);}, 2000);

在上面的代码中,首先记录第一次patch方法更新后的vnode,同时在2秒钟以后,通过h函数重新创建了一个虚拟DOM,并且通过patch函数与原有的虚拟DOM进行比较,然后重新更新页面内容。

2秒钟以后清空节点内容

  let a = patch(oldVnode, vnode);patch(a, h("!"));//第二个参数,表示创建一个注释节点。

4.4 模块

Snabbdom的核心库并不能处理元素的属性/样式/事件等,如果需要处理,可以使用模块.

常用模块

官方提供了6个模块

**attributes**:设置DOM元素的属性,内部使用setAttribute()来设置属性,处理布尔类型的属性(可以对布尔类型的属性作相应的判断处理,布尔类型的属性,我们比较熟悉的有selected,checked`等)。

props:attributes模块类似,设置DOM元素的属性element[attr]=value,不处理布尔类型的属性。

class: 切换样式类,注意:给元素设置类样式是通过sel选择器。··

dataset:设置 HTML5 中的 data-* 的自定义属性

eventlisteners: 注册和移除事件

style:设置行内样式,支持动画(内部创建transitionend事件),会增加额外的属性:delayed / remove / destroy

下面看一下模块的使用

使用模块的步骤:

第一步:导入需要的模块

第二步:在init()中注册模块

第三步:使用h函数创建VNode的时候,可以把第二个参数设置为对象(对象中是模块需要的数据,可以设置行内样式、事件等),其它参数往后移。

下面我们要实现的案例,就是给div添加一个背景,同时为其添加一个单击事件,当然在div中还要创建两个元素分别是h1p.

具体实现的代码如下:

import { init, h } from "snabbdom";
//导入模块
import style from "snabbdom/modules/style";
import eventlisteners from "snabbdom/modules/eventlisteners";
//注册模块
let patch = init([style, eventlisteners]);
// 使用h函数的第二个参数传入模块所需要的数据(对象)
let vnode = h("div",{style: {backgroundColor: "red",},on: {click: eventHandler,},},[h("h1", "Hello Vue"), h("p", "这是p标签")]
);
function eventHandler() {console.log("点击了我");
}
let app = document.querySelector("#app");
patch(app, vnode);

注意:在index.html文件中要引入以上代码所在的js文件,如下所示:

 <body><div id="app"></div><script src="./js/03-modules.js"></script></body>

5、Snabbdom源码解读

通过前面的学习,我们可以总结出Snabbdom的核心:

  • 使用h( )函数创建JavaScript对象(VNode)描述真实DOM

  • init( )函数设置模块,创建patch( )函数。

  • patch( )函数比较新旧两个VNode

  • 把变化的内容更新到真实DOM树上

5.1 h函数

h函数介绍

在使用Vue的时候加过h( )函数

new Vue({router,render:h=>h(App)
}).$mount('#app')

render函数的参数就是Snabbdom中的h函数,当然在Vue中将h函数做了一定的修改,可以用来支持组件,原有的Snabbdom中的h函数不支持组件的内容。在Snabbdomh( )函数的作用就是用来创建VNode.

在看源码之前,我们先来了解一个概念:函数重载

因为在源码中用到了函数重载

所谓的函数重载: 参数个数类型不同的函数,称之为函数重载

但是在JavaScript中没有重载的概念,在TypeScript中是有重载的。

我们来看一段重载的示例代码

function add(a,b){return a+b
}
function add(a,b,c){console.log(a+b+c)
}
add(1,2)
add(1,2,3)

以上我们是通过参数个数的形式,展示了一段函数重载的代码。

下面我们来看一下h函数的源码。

源码位置:node_modules/snabbdom/src/h.ts

// h函数的重载
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
//h函数重载的具体实现
//h函数可以接收三个参数,?表示该参数可以不传递
export function h(sel: any, b?: any, c?: any): VNode {//定义变量var data: VNodeData = {}, children: any, text: any, i: number;// 处理参数,实现重载的机制//如果c这个参数的值不等于undefined,表示传递了三个参数if (c !== undefined) {//如果该条件成立,表示处理的就是有三个参数的情况//参数b中存储的就是模块处理的时候,需要的数据,例如:行内样式,事件等,关于这块在前面的案例中我们也写过,在这里将b参数的值赋给了data这个变量data = b;//下面是对参数c进行了判断。//关于参数c有三种情况,第一种情况为数组,第二种情况为字符串或者是数字,第三种情况为VNode.//首先判断参数c是否为数组,如果是数组,赋值给了children这个变量,表明c是子元素。//例如前面我们在使用模块的案例中,给h函数指定的第三个参数就为数组:[h("h1", "Hello Vue"), h("p", "这是p标签")]if (is.array(c)) { children = c; }//如果c参数是字符串或者是数字,将参数c赋值给了text变量,表明传递过来的内容其实就是标签中的文本内容else if (is.primitive(c)) { text = c; }//如果有sel属性,表明c是vnode,在这里需要转换成数组的形式,然后再赋值给children这个变量else if (c && c.sel) { children = [c]; }} else if (b !== undefined) {//如果该条件成立,表明处理的是两个参数的情况//如果b是一个数组,赋值给chilren这个变量:vnode = h("div#container", [h("h1", "Hello Vue"), h("p", "Hello p")]);if (is.array(b)) { children = b; }//如果b是字符串或者数字:h("div", "Hello Vue");else if (is.primitive(b)) { text = b; }//如果b是Vnode的情况else if (b && b.sel) { children = [b]; }else { data = b; }}//判断children中有值if (children !== undefined) {//对chilren进行遍历for (i = 0; i < children.length; ++i) {//判断从chilren中取出来的内容是否为:string/number,如果是创建文本的虚拟节点.if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);}}if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&(sel.length === 3 || sel[3] === '.' || sel[3] === '#')) {//如果是svg,添加命名空间addNS(data, children, sel);}//最后返回的是整个VNode.所h函数的核心就是调用vnode方法,来返回一个虚拟节点return vnode(sel, data, children, text, undefined);
};
// 导出h函数
export default h;

addNs方法的实现如下:

function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {data.ns = 'http://www.w3.org/2000/svg';if (sel !== 'foreignObject' && children !== undefined) {for (let i = 0; i < children.length; ++i) {let childData = children[i].data;if (childData !== undefined) {addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);}}}
}

addNs方法中就是给data添加了命名空间,然后通过递归的方式给chilren中的所有子元素都添加了命名空间。

在看VNode这个方法的源码之前,我们先来说一下看源码必备的快捷键。

  • 光标移动到某个变量处,按F12快速定位到该变量的定义位置。
  • ALT + 左方向键,回到上次的代码位置
  • Ctrl + 单击,跳转到某个变量的定义处
  • 选中某个变量或方法名,按F12显示出该变量或方法的具体代码

5.2 VNode函数

h函数的最后调用了VNode函数创建了一个虚拟节点,并返回。下面看一下VNode函数内部实现。

VNode函数的代码在vnode.ts文件中

import {Hooks} from './hooks';
import {AttachData} from './helpers/attachto'
import {VNodeStyle} from './modules/style'
import {On} from './modules/eventlisteners'
import {Attrs} from './modules/attributes'
import {Classes} from './modules/class'
import {Props} from './modules/props'
import {Dataset} from './modules/dataset'
import {Hero} from './modules/hero'export type Key = string | number;export interface VNode {sel: string | undefined;data: VNodeData | undefined;children: Array<VNode | string> | undefined;elm: Node | undefined;text: string | undefined;key: Key | undefined;
}export interface VNodeData {props?: Props;attrs?: Attrs;class?: Classes;style?: VNodeStyle;dataset?: Dataset;on?: On;hero?: Hero;attachData?: AttachData;hook?: Hooks;key?: Key;ns?: string; // for SVGsfn?: () => VNode; // for thunksargs?: Array<any>; // for thunks[key: string]: any; // for any other 3rd party module
}export function vnode(sel: string | undefined,data: any | undefined,children: Array<VNode | string> | undefined,text: string | undefined,elm: Element | Text | undefined): VNode {let key = data === undefined ? undefined : data.key;return {sel, data, children, text, elm, key};
}export default vnode;

在上面的代码中,我们首先关注的就是接口VNode,该接口中定义了很多的属性,而最终vnode这个函数返回的VNode对象必须都要实现该接口中的这些属性。

下面可以看一下这些属性的含义

export interface VNode {//选择器,也就是调用h函数的时候传递的第一个参数sel: string | undefined;// 节点数据:属性/样式/事件等。data: VNodeData | undefined;//子节点,和text互斥  VNode是描述真实DOM的,如果所描述的真实DOM中有子节点,通过children来表示这些子节点children: Array<VNode | string> | undefined;// 记录vnode对应的真是DOM,将Vnode转换成真实DOM以后,会存储到elm这个属性中。关于这一点可以在将VNode转换成真实DOM的时候看到。elm: Node | undefined;// 节点中的内容,和children只能互斥text: string | undefined;//优化,关于这个属性可以在将VNode转换成真实DOM的时候看到。key: Key | undefined;
}

最后看一下vnode这个函数,返回的就是一个js对象,该对象中包含了VNode这个接口中的属性,而这个js对象就行虚拟节点。而这个虚拟的节点是怎样转换成真实的DOM?后面会重点讲解这块内容。

5.3 复习h函数与Vnode函数应用

下面我们在通过一个案例,复习一下h函数与vnode函数。

假如,我们创建了如下的一个虚拟DOM

// 构造一个虚拟dom
var vnode = h('div#app',{style: {color: '#000'}},[h('span', {style: {fontWeight: 'bold'}}, "my name is zhangsan"),' and xxxx',h('a', {props: {href: '/foo'}}, '我是张三')]
);

下面看一下上面的代码的执行流程。

注意:这边先执行的是先内部的调用,然后再依次往外执行调用。

因此首先调用和执行的代码是:

第一步: h('span', {style: {fontWeight: 'bold'}}, "my name is kongzhi"), 因此把参数传递到h函数后:sel: 'span', b = {style: {fontWeight: 'bold'}}, c = "my name is kongzhi";

首先判断 if (c !== undefined) {} 代码,然后进入if语句内部代码,如下:

if (c !== undefined) {data = b;if (is.array(c)) { children = c; }else if (is.primitive(c)) { text = c; }
}

因此 data = {style: {fontWeight: 'bold'}}; 然后判断 c 是否是一个数组,可以看到,不是,因此进入 else if语句,因此 text = "my name is zhangsan"; 从代码中可以看到,就直接跳过所有的代码了,最后执行 return VNode(sel, data, children, text, undefined); 了,因此会调用 snabbdom/vnode.js 代码如下:

/** VNode函数如下:主要的功能是构造VNode, 把输入的参数转化为Vnode* @param {sel} 'span'* @param {data} {style: {fontWeight: 'bold'}}* @param {children} undefined* @param {text} "my name is zhangsan"* @param {elm} undefined
*/
module.exports = function(sel, data, children, text, elm) {var key = data === undefined ? undefined : data.key;return {sel: sel, data: data, children: children,text: text, elm: elm, key: key};
};

因此 var key = data.key = undefined; 最后返回值如下:

{ sel: 'span', data: {style: {fontWeight: 'bold'}},children: undefined,text: "my name is kongzhi",elm: undefined,key: undefined
}

第二步:调用h('a', {props: {href: '/foo'}}, '我是张三');代码

同理:sel = 'a'; b = {props: {href: '/foo'}}, c = '我是张三'; 然后执行如下代码:

if (c !== undefined) {data = b;if (is.array(c)) { children = c; }else if (is.primitive(c)) { text = c; }
}

因此 data = {props: {href: '/foo'}}; text = '我是张三'; children = undefined; 最后也一样执行返回:

return VNode(sel, data, children, text, undefined);

因此又调用 snabbdom/vnode.js 代码如下:

/** VNode函数如下:主要的功能是构造VNode, 把输入的参数转化为Vnode* @param {sel} 'a'* @param {data} {props: {href: '/foo'}}* @param {children} undefined* @param {text} "我是张三"* @param {elm} undefined
*/
module.exports = function(sel, data, children, text, elm) {var key = data === undefined ? undefined : data.key;return {sel: sel, data: data, children: children,text: text, elm: elm, key: key};
};

因此执行代码:var key = data.key = undefined; 最后返回值如下:

{sel: 'a',data: {props: {href: '/foo'}},children: undefined,text: "我是张三",elm: undefined,key: undefined
}

第三步调用外层的代码,把参数传递进去,因此代码初始化变成如下:

var vnode = h('div#app',{style: {color: '#000'}},[{ sel: 'span', data: {style: {fontWeight: 'bold'}},children: undefined,text: "my name is zhangsan",elm: undefined,key: undefined},' and xxxx',{sel: 'a',data: {props: {href: '/foo'}},children: undefined,text: "我是张三",elm: undefined,key: undefined}]
);

继续把参数传递进去,因此 sel = 'div#app'; b = {style: {color: '#000'}}; c 的值变为如下:

c = [{ sel: 'span', data: {style: {fontWeight: 'bold'}},children: undefined,text: "my name is 张三",elm: undefined,key: undefined},' and xxxx',{sel: 'a',data: {props: {href: '/foo'}},children: undefined,text: "我是张三",elm: undefined,key: undefined}
];

首先看if判断语句,if (c !== undefined) {}; 因此会进入if语句内部代码;

if (c !== undefined) {data = b;if (is.array(c)) { children = c; }else if (is.primitive(c)) { text = c; }
}

因此 data = {style: {color: '#000'}}; c 是数组的话,就把c赋值给children; 因此 children 值为如下

children = [{ sel: 'span', data: {style: {fontWeight: 'bold'}},children: undefined,text: "my name is zhangsan",elm: undefined,key: undefined},' and xxxx',{sel: 'a',data: {props: {href: '/foo'}},children: undefined,text: "我是张三",elm: undefined,key: undefined}
];

我们下面接着看 如下代码:

if (is.array(children)) {for (i = 0; i < children.length; ++i) {if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]);}
}

如上代码,判断如果 children 是一个数组的话,就循环该数组 children; 从上面我们知道 children 长度为3,因此会循环3次。进入for循环内部。判断其中一项是否是数字和字符串类型,因此只有 ' and xxxx' 符合要求,因此 children[1] = VNode(undefined, undefined, undefined, ' and xxxx'); 最后会调用 snabbdom/vnode.js 代码如下:

module.exports = function(sel, data, children, text, elm) {var key = data === undefined ? undefined : data.key;return {sel: sel, data: data, children: children,text: text, elm: elm, key: key};
};

通过上面的代码可知,我们最后返回的是如下:

children[1] = {sel: undefined,data: undefined,children: undefined,text: ' and xxxx',elm: undefined,key: undefined
};

执行完成后,我们最后返回代码:return VNode(sel, data, children, text, undefined); 因此会继续调用snabbdom/vnode.js代码如下:

/*@param {sel} 'div#app'@param {data} {style: {color: '#000'}}@param {children} 值变为如下:children = [{ sel: 'span', data: {style: {fontWeight: 'bold'}},children: undefined,text: "my name is zhangsan",elm: undefined,key: undefined},{sel: undefined,data: undefined,children: undefined,text: ' and xxxx',elm: undefined,key: undefined},{sel: 'a',data: {props: {href: '/foo'}},children: undefined,text: "我是张三",elm: undefined,key: undefined}];@param {text} undefined@param {elm} undefined
*/
module.exports = function(sel, data, children, text, elm) {var key = data === undefined ? undefined : data.key;return {sel: sel, data: data, children: children,text: text, elm: elm, key: key};
};

因此继续执行内部代码:var key = undefined; 最后返回代码:

return {sel: sel, data: data, children: children,text: text, elm: elm, key: key
};

因此最后构造一个虚拟dom返回的值为如下:

vnode = {sel: 'div#app',data: {style: {color: '#000'}},children: [{ sel: 'span', data: {style: {fontWeight: 'bold'}},children: undefined,text: "my name is zhangsan",elm: undefined,key: undefined},{sel: undefined,data: undefined,children: undefined,text: ' and xxxx',elm: undefined,key: undefined},{sel: 'a',data: {props: {href: '/foo'}},children: undefined,text: "我是张三",elm: undefined,key: undefined}],text: undefined,elm: undefined,key: undefined
}

接着往下执行如下代码:

// 初始化容器
var app = document.getElementById('app');// 将vnode patch 到 app 中
patch(app, vnode);

以上就是生成整个虚拟DOM的过程。

ned; `最后返回代码:

return {sel: sel, data: data, children: children,text: text, elm: elm, key: key
};

因此最后构造一个虚拟dom返回的值为如下:

vnode = {sel: 'div#app',data: {style: {color: '#000'}},children: [{ sel: 'span', data: {style: {fontWeight: 'bold'}},children: undefined,text: "my name is zhangsan",elm: undefined,key: undefined},{sel: undefined,data: undefined,children: undefined,text: ' and xxxx',elm: undefined,key: undefined},{sel: 'a',data: {props: {href: '/foo'}},children: undefined,text: "我是张三",elm: undefined,key: undefined}],text: undefined,elm: undefined,key: undefined
}

接着往下执行如下代码:

// 初始化容器
var app = document.getElementById('app');// 将vnode patch 到 app 中
patch(app, vnode);

以上就是生成整个虚拟DOM的过程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/662118.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

机器学习---半监督学习(基于分岐的方法)

1. 基于分歧的方法 与生成式方法、半监督SVM、图半监督学习等基于单学习器利用未标记数据不同&#xff0c;基于分歧的方 法(disagreement--based methods)使用多学习器&#xff0c;而学习器之间的“分歧”(disagreement)对未标记 数据的利用至关重要。 1.2 协同训练 “协同…

实验一 古典密码算法的设计与实现

✅作者简介&#xff1a;CSDN内容合伙人、信息安全专业在校大学生&#x1f3c6; &#x1f525;系列专栏 &#xff1a;简单外包单 &#x1f4c3;新人博主 &#xff1a;欢迎点赞收藏关注&#xff0c;会回访&#xff01; &#x1f4ac;舞台再大&#xff0c;你不上台&#xff0c;永远…

猫什么时候发腮?猫咪发腮指南!这些生骨肉冻干发腮效果好

猫什么时候发腮是许多猫主人非常关心的问题。在猫咪的成长过程中&#xff0c;发腮是一项重要的体征&#xff0c;也是猫咪成熟的标志。主人需要在适龄的年龄段加强营养补给&#xff0c;可以让让猫咪拥有可爱的肉嘟嘟脸型&#xff0c;不要错失最佳发腮期。那么&#xff0c;什么时…

花瓣网美女图片爬取

爬虫基础案例01 花瓣网美女图片 网站url&#xff1a;https://huaban.com 图片爬取 import requests import json import os res requests.get(url "https://api.huaban.com/search/file?text%E7%BE%8E%E5%A5%B3&sortall&limit40&page1&positionsear…

【论文阅读笔记】Advances in 3D Generation: A Survey

Advances in 3D Generation: A Survey 挖个坑&#xff0c;近期填完摘要 time&#xff1a;2024年1月31日 paper&#xff1a;arxiv 机构&#xff1a;腾讯 挖个坑&#xff0c;近期填完 摘要 生成 3D 模型位于计算机图形学的核心&#xff0c;一直是几十年研究的重点。随着高级神经…

第96讲:MySQL高可用集群MHA的核心概念以及集群搭建

文章目录 1.MHA高可用数据库集群的核心概念1.1.主从复制架构的演变1.2.MHA简介以及架构1.3.MHA的软件结构1.4.MHA Manager组件的启动过程1.5.MHA高可用集群的原理 2.搭建MHA高可用数据库集群2.1.环境架构简介2.2.搭建基于GTID的主从复制集群2.2.1.在三台服务器中分别搭建MySQL实…

Prometheus 企业级监控使用总结

一、监控概念&误区 监控是管理基础设施和业务的核心工具&#xff0c;监控应该和应用程序一起构建和部署&#xff0c;没有监控&#xff0c;将无法了解你的系统运行环境&#xff0c;进行故障诊断&#xff0c;也无法阻止提供系统性的性能、成本和状态等信息。 误区&#xff…

法兰缺损零件设计加工替换盾构机扫描建模厂家抄数修图出CAD图纸

在现代工业生产中&#xff0c;法兰缺损零件的问题时有发生&#xff0c;这不仅会影响设备的正常运行&#xff0c;还会给企业带来巨大的经济损失。为了解决这一问题&#xff0c;CASAIM中科广电三维扫描和3D打印设计加工技术的运用成为了关键。 首先&#xff0c;CASAIM中科广电需要…

“与客户,共昂首”——Anzo Capital昂首资本尽释行业进取之姿

“以匠心&#xff0c;铸不凡” 活动的现场&#xff0c;Anzo Capital 作为演讲嘉宾分享“以匠心&#xff0c;铸不凡”的产品理念。Anzo Capital积淀九载&#xff0c;匠心打造出“STP”和“ECN”两大核心账户&#xff0c;以光之速度将交易中的订单直达市场和流动性提供商&#…

Unity通过物理带动实现传输带运输物品

前言&#xff1a;遇到个听起来挺简单的需求&#xff0c;就是实现一个传输带&#xff0c;传输物品。但细想发现如果是直接设置物品的速度&#xff0c;或者通过设置父物体的方式带动物品&#xff0c;都挺不好&#xff0c;关联性太强。最后选择用到一个很实用的API, Rigidbody.M…

Vue+OpenLayers7入门到实战:OpenLayers7加载天地图

返回《Vue+OpenLayers7》专栏目录:Vue+OpenLayers7 前言 本章介绍如何使用OpenLayers7在地图上加载天地图. 天地图瓦片访问需要先到天地图申请key。天地图官网链接 本文使用xyz方式加载天地图,并且介绍如何加载xyz格式天地图url,包括天地图纯底图(无标记)、卫星影像图…

SpringMVC入门学习(十)----mvc:annotation-driven标签介绍

目录 1、关于mvc:annotation-driven作用2、mvc:annotation-driven在什么时候必须配置3、关于mvc:annotation-driven配合使用的几种情况 回到顶部 1、关于mvc:annotation-driven作用 [1]、<mvc:annotation-driven /> 会自动向容器中注册如下组件&#xff0c;并且会代替…

0101appscan安装与使用入门-扫描-信息收集

1 简介 HCL AppScan&#xff08;原IBM Security AppScan&#xff09;是原IBM的Rational软件部门的一组网络安全测试和监控工具&#xff0c;2019年被HCL技术公司收购。AppScan旨在在开发过程中对Web应用程序的安全漏洞进行测试[1]。该产品学习每个应用程序的行为&#xff0c;无…

【蓝桥杯51单片机入门记录】LED

目录 一、基础 &#xff08;1&#xff09;新建工程 &#xff08;2&#xff09;编写前准备 二、LED &#xff08;1&#xff09;点亮LED灯 &#xff08;2&#xff09;LED闪烁 延时函数的生成&#xff08;stc-isp中生成&#xff09; 实现 &#xff08;3&#xff09;流水灯…

MG7050HAN 基于声表的差分多输出 晶体振荡器 (HCSL)

基于MG7050 HAN的声表差分多输出晶体振荡器(HCSL)&#xff0c;采用两路或四路差分HCSL&#xff08;高速电流驱动逻辑&#xff09;输出&#xff0c;可以减少外部扇出缓冲区&#xff0c;特别适用于需要超低抖动、高频率范围内稳定工作的应用场合。其输出特性曲线超低抖动&#xf…

降维(Dimensionality Reduction)

一、动机一&#xff1a;数据压缩 这节我将开始谈论第二种类型的无监督学习问题&#xff0c;称为降维。有几个原因使我们可能想要做降维&#xff0c;其一是数据压缩&#xff0c;它不仅允许我们压缩数据使用较少的计算机内存或磁盘空间&#xff0c;而且它可以加快我们的学习算法。…

90年代的黄河路,大家都在用什么方式互相联络?

1992 年的上海&#xff0c;霓虹养眼&#xff0c;万花如海… 新年伊始&#xff0c;一部《繁花》爆火出圈&#xff0c;带观众穿越回了那个灯红酒绿的上海。90 年代的黄河路遍地是机会&#xff0c;商业战场上&#xff0c;信息成了最宝贵的财富&#xff0c;谁能获得最真实有用的资讯…

Python学习之路-DRF基础:视图

Python学习之路-DRF基础:视图 视图概览 简介 REST framework 提供了众多的通用视图基类与扩展类&#xff0c;以简化视图的编写。 视图的继承关系 视图的方法与属性 视图说明 两个基类 APIView 简介 rest_framework.views.APIView APIView是REST framework提供的所有视…

微服务-微服务Alibaba-Nacos 源码分析 (源码流程图)

客户端流程 客户端心跳与实例往服务端注册

vue3.0中从proxy中取值

使用vue3.0时&#xff0c;因为底层是使用proxy进行代理的所以当我们打印一些值的时候是proxy代理之后的&#xff0c;是Proxy 对象&#xff0c;Proxy对象里边的[[Target]]才是真实的对象。也是我们需要的 第一种获取target值的方式&#xff1a; import { toRaw } from vue; le…