虚拟DOM大家都不陌生,研究了一下略带收获,在这里简单记录下,做个备忘
1、什么是虚拟DOM以及虚拟DOM的表达
虚拟DOM简而言之就是,用JS去按照DOM结构来实现的树形结构对象,
也就是一个JS对象而已。
那么什么样的对象才能比较匹配的上dom结构呢?来看一下如下图所示的dom结构<div class="list" style="color: red;"> <div class="item"> <p class="leftP"> <span class="left" style="margin-right: 10px;">leftSpan</span> <span class="right">rightSpan</span></p> <p class="rightP"> <img class="imgs" src="https://pics2.baidu.com/feed/94cad1c8a786c9175fd11173392345ca3ac75749.jpeg?token=8d28956f2c93b2e7090e89980059a553&s=FAC2B144C3BCB469044C8C830000B081" style="width: 100px; height: 100px;"> <span class="imgs" style="width: 100px; height: 100px;">图片旁边的span</span> </p> </div> <p class="item">玉溪</p> <p class="item">黄鹤楼</p> </div>
上图是比较简单的一个结构,比较理想的是使用如下对象来模拟:
{ //节点类型 "type": "div", //节点属性 "props": { "class": "list", "style": "color:red;" }, //子节点 "children": [ { "type": "div", "props": {"class": "item"}, "children": [ { "type": "p", "props": {"class": "leftP"}, "children": [ { "type": "span", "props": { "class": "left", "style": "margin-right:10px" }, "children": ["leftSpan"] }, { "type": "span", "props": {"class": "right"}, //没有子节点了,直接显示文本 "children": ["rightSpan"] } ] }, { "type": "p", "props": {"class": "rightP"}, "children": [ { "type": "img", "props": { "class": "imgs", "style": "width:100px;height:100px", "src": "https://pics2.baidu.com/feed/94cad1c8a786c9175fd11173392345ca3ac75749.jpeg?token=8d28956f2c93b2e7090e89980059a553&s=FAC2B144C3BCB469044C8C830000B081" }, "children": [] }, { "type": "span", "props": { "class": "imgs", "style": "width:100px;height:100px" }, "children": ["图片旁边的span"] } ] } ] }, { "type": "p", "props": {"class": "item"}, "children": ["玉溪"] }, { "type": "p", "props": {"class": "item"}, "children": ["黄鹤楼"] } ] }
OK,对象结构已经解析完成,接下来我们就看怎么批量生成这样的结构,直接上代码
// 定义一个构造函数来 class Element { constructor(type, props, children) { this.type = type; this.props = props; this.children = children; } }; //生成虚拟dom对象 (批量调用构造函数,) class createElement=(type,props,children)=>{ return new Element(type,props,children); };
我们只需要调用createElement即可生成上述的dom对象结构,他接收三个参数,第一个表示dom元素的类型,第二个表示要设置的属性,第三个则表示子元素。生成上述结构调用代码如下:
let virtualDom = createElement( 'div', {class:'list',style:'color:red;'}, [ //div createElement( 'div', {class:'item'}, [ //p createElement( 'p', {class:'leftP'}, [ //span createElement('span',{class:'left',style:'margin-right:10px'},['leftSpan']), //span createElement('span',{class:'right'},['rightSpan']), ] ), //p createElement('p',{class:'rightP'},[ createElement( 'img', { class:'imgs', style:'width:100px;height:100px', src:'https://pics2.baidu.com/feed/94cad1c8a786c9175fd11173392345ca3ac75749.jpeg?token=8d28956f2c93b2e7090e89980059a553&s=FAC2B144C3BCB469044C8C830000B081' }, [], ), createElement( 'span', { class:'imgs', style:'width:100px;height:100px', }, ['图片旁边的span'], ), ]), ] ), createElement('p',{class:'item'},['玉溪']), createElement('p',{class:'item'},['黄鹤楼']), ] ); console.log(virtualDom);
我们可以在控制台很清楚的看到打印出来的结构正是我们想要的结构
2、虚拟DOM转换成真实的DOM
上面我们已经实现了一个虚拟DOM的表达,也就是我们可以轻而易举的通过一个对象来表达一个dom了,接下来我们看看怎么把虚拟dom转换成我们的真实dom,也就是吧我们的对象转换回我们的真实DOM,从而在页面显示。
直接上代码如下:
// 设置属性 const setAttr = (node, key, value)=> { // 需要判断key是什么 switch (key) { case 'value': // 属性是value就要看标签类型了,input和textarea的value就需要做区别 if (node.tagName.toLowerCase() === 'input' || node.tagName.toLowerCase() == 'textarea') { node.value = value; } else { node.setAttribute(key, value); } break; case 'style': node.style.cssText = value; break; default: node.setAttribute(key, value); break; } }; const render=(domObj)=> { // 根据元素类型来创建dom let el = document.createElement(domObj.type); // 遍历props对象,然后给创建的元素el设置属性 for (let key in domObj.props) { // 设置属性的方法 setAttr(el, key, domObj.props[key]); } // 遍历子节点数组 domObj.children.forEach(child =>{ // 需要注意的是:如果child是虚拟dom,就继续递归渲染 if (child instanceof Element) { child = this.render(child); } else { // 只是普通的文本内容 child = document.createTextNode(child); } // 把子节点添加到父节点中 el.appendChild(child); }); return el; };
直接调用我们的render函数,并把对应的虚拟DOM传入即可,接着上一步,我们已经生成了了虚拟dom对象virtualDom,因此只需要调用如下:
// 得到dom元素 let el = render(virtualDom); console.log(el); //插入页面 document.getElementById('app').appendChild(el)
到此,我们的转换也就告一段了。
3、整个过程的封装
virtualDom.js
// 定义一个构造函数来 class Element { constructor(type, props, children) { this.type = type; this.props = props; this.children = children; } }; export default function VirtualDom(extendsObj={}){ Object.assign(this,extendsObj); // 设置属性 const setAttr = (node, key, value)=> { // 需要判断key是什么 switch (key) { case 'value': // 属性是value就要看标签类型了,input和textarea的value就需要做区别 if (node.tagName.toLowerCase() === 'input' || node.tagName.toLowerCase() == 'textarea') { node.value = value; } else { node.setAttribute(key, value); } break; case 'style': node.style.cssText = value; break; default: node.setAttribute(key, value); break; } }; //生成虚拟dom (批量调用构造函数,) this.createElement=(type,props,children)=>{ return new Element(type,props,children); }; //生成真实dom this.render=(domObj)=> { // 根据元素类型来创建dom let el = document.createElement(domObj.type); // 遍历props对象,然后给创建的元素el设置属性 for (let key in domObj.props) { // 设置属性的方法 setAttr(el, key, domObj.props[key]); } // 遍历子节点数组 domObj.children.forEach(child =>{ // 需要注意的是:如果child是虚拟dom,就继续递归渲染 if (child instanceof Element) { child = this.render(child); } else { // 只是普通的文本内容 child = document.createTextNode(child); } // 把子节点添加到父节点中 el.appendChild(child); }); return el; }; // 将元素插入页面 this.renderDom = (el, target)=> { target.appendChild(el); }; }
test.js(测试文件)
import VirtualDom from './virtualDom.js'; const testDom = ()=>{ let virtualDom = new VirtualDom(); console.log(virtualDom); let domObj = virtualDom.createElement( 'div', {class:'list',style:'color:red;'}, [ //div virtualDom.createElement( 'div', {class:'item'}, [ //p virtualDom.createElement( 'p', {class:'leftP'}, [ //span virtualDom.createElement('span',{class:'left',style:'margin-right:10px'},['leftSpan']), //span virtualDom.createElement('span',{class:'right'},['rightSpan']), ] ), //p virtualDom.createElement('p',{class:'rightP'},[ virtualDom.createElement( 'img', { class:'imgs', style:'width:100px;height:100px', src:'https://pics2.baidu.com/feed/94cad1c8a786c9175fd11173392345ca3ac75749.jpeg?token=8d28956f2c93b2e7090e89980059a553&s=FAC2B144C3BCB469044C8C830000B081' }, [], ), virtualDom.createElement( 'span', { class:'imgs', style:'width:100px;height:100px', }, ['图片旁边的span'], ), ]), ] ), virtualDom.createElement('p',{class:'item'},['玉溪']), virtualDom.createElement('p',{class:'item'},['黄鹤楼']), ] ); console.log(domObj ); // 渲染dom let el = virtualDom.render(domObj); // 得到dom元素 console.log(el); virtualDom.renderDom(el,document.getElementById('app')); }; export { testDom }