什么是虚拟DOM
react 中的 virtual DOM (虚拟DOM),其实就是JS对象。
众所周知,浏览器的DOM元素的渲染效率极低,对DOM的优化是前端开发人员一直以来很头疼的问题,而虚拟DOM就是针对真实的DOM元素渲染效率低下而问世的。虚拟DOM在内存中以JS对象的形式存在,模拟了真实DOM的所有结构,在将虚拟DOM渲染到页面上之前,我们的所有操作都在虚拟DOM上进行,你要知道:对JS对象的操作要比对DOM的操作快得多,所以虚拟DOM的出现使前端性能得到了极大的优化。
虚拟DOM模 真实DOM的操作
假设我们有一个创建虚拟DOM的方法:createElement( tag, props, children )
createElement方法可以帮我们创建一个虚拟DOM,来模仿你想要的真实DOM的结构
参数:
- tag:DOM元素的标签( 'a' , 'p ', 'div' ...)
- props:DOM元素的属性( {className:'box' , id:'container' , style:{fontSize:'20px'} , key:1 , ...})
- children:DOM元素的内容( 字符串或其他虚拟DOM元素组成的数组 )
// 用 createElement 方法创建一个虚拟DOM的结构
let virtualDOM = createElement('div',{id:'container'},[
createElement('p',{className:'msg',key:1},'这是一条消息'),
createElement('p',{className:'msg',key:2},'这是另一条消息'),
createElement(null,null,'这是一条没有标签包裹的文本'),
createElement('button',{className:'btn',key:3},'按钮')
]);
现在,很简单的一个虚拟DOM结构已经创建完成了,显然,虽然它拥有自己的属性和结构,但是目前为止这个虚拟DOM只是一个JS对象而已,我们对它进行的任何操作都是在内存中完成的(虚拟DOM性能好的原因)。但是我们最终需要的是一个真实的DOM,所以我们还需要一个render方法:render( virtualDOM, DOM )
render 方法可以将你的虚拟DOM解析成真实的DOM并渲染到页面上
参数:
- virtualDOM:需要解析的虚拟DOM
- DOM:需要渲染在哪个DOM里
render(virtualDOM,document.getElementById('root'));
至此,一个简单的 virtualDOM 模拟 真实DOM 的流程就结束了。
虚拟DOM原理
初始化:定义类型。
// 定义一个tag类型集合
const tagTypes = {
HTML:"HTML",
TEXT:"TEXT"
}
// 定义children类型集合
const childrenTypes = {
// 子元素只有一个 说明是字符串
single:"single",
// 子元素是一个数组 数组里是多个元素
many:"many",
// 子元素是一个空 没有子元素
empty:"empty"
}
createElement()
实现原理:根据你传入参数的类型,返回一个对应出来你想要的结构的整合后的JS对象。
// 创建虚拟dom的方法
function createElment(tag,props,children){
// 定义tag类型
let type;
// 如果tag存在,那么该元素就是HTML元素,否则是字符串
if(typeof tag === 'string'){
type = tagTypes.HTML;
}else{
type = tagTypes.TEXT;
}
// 定义children类型
let childrenType;
// 如果children是文本的时候就创建一个文本虚拟dom
// 如果children是数组的时候就创建一个有子节点的虚拟dom
// 如果children是空的时候就创建一个空虚拟dom
if(typeof children === 'string'){
childrenType = childrenTypes.single;
// createTextNode:创建文本DOM方法
children = createTextNode(children)
}else if(Array.isArray(children)){
childrenType = childrenTypes.many;
}else{
childrenType = childrenTypes.empty;
}
// 返回虚拟dom对象
return {
el:null,
type,
tag,
props,
children,
childrenType
}
}
//创建文本虚拟dom,直接返回一个对应的文本虚拟DOM
function createTextNode(text){
return {
type:'text',
tag:null,
props:null,
children:text,
childrenType:childrenTypes.empty
}
}
render()
// 渲染方法
function render(vnode, container){
if(container.vnode){
// 如果虚拟DOM已经存在,那么执行更新
// 这一步是相当复杂的diff算法,单独开辟章节来讲,此处暂时只考虑首次渲染
}else{
// 如果虚拟DOM没有存在,那么执行挂载(首次渲染)
mounted(vnode, container);
}
// 判断是初次渲染还是更新渲染
container.vnode = vnode;
}
// 首次渲染函数
function mounted(vnode,container){
let {type} = vnode;
if(type === 'HTML'){
// 渲染HTML元素方法
mountedElement(vnode,container)
}else{
// 渲染文本元素方法
mountedText(vnode,container)
}
}
// 渲染HTML元素的方法
function mountedElement( vnode, container ){
let { type, tag, props, children, childrenType } = vnode;
// el是真是的DOM元素,此处创建tag对应的DOM元素并赋给el
var el = document.createElement(tag);
vnode.el = el;
// 遍历设置props属性
if(props){
for(var key in props){
/* 设置DOM的属性(方法在代码最后)
语法:patchProps( 设置属性的元素, 属性的key值, 旧的value值, 新的value ) */
patchProps(el,key,null,props[key])
}
}
// 判断该元素的子元素的类型
if(childrenType === childrenTypes.single){
// 如果 childrenType 属性为 single 那么肯定是文本,用渲染文本方法将子元素渲染
mountedText(children, el)
}else if(childrenType === childrenTypes.many){
// 如果 childrenType 属性为 many 则肯定是嵌套子元素,遍历后用首次渲染方法将子元素渲染(递归)
children.forEach((item)=>{
mounted( item, el )
})
}
// 渲染完成后,最终要插入到父级里面(最高父级就是root)
container.appendChild(el);
}
// 渲染文本虚拟dom的方法
function mountedText(vnode,container){
// 创建一个对应的文本节点,直接插入父元素
var textNode = document.createTextNode(vnode.children);
vnode.el = textNode;
container.appendChild(textNode);
}
// 挂载属性的方法(部分情况)
// patchProps( 设置属性的元素, 属性的key值, 旧的value值, 新的value )
function patchProps(el, key, oldVal, newVal) {
switch (key) {
case 'className':
el.className = newVal;
break;
case 'id':
el.id = newVal;
break;
case 'onClick':
el.addEventListener("click", newVal);
break;
case 'style': {
for (var sKey in newVal) {
el.style[sKey] = newVal[sKey];
}
break;
}
default:
if (key != 'key') {
el.setAttribute(key, newVal);
}
}
}
至此,一个简单版的react中虚拟DOM的底层原理就实现啦
✿✿ヽ(°▽°)ノ✿