React 虚拟DOM详解

什么是虚拟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的结构

参数:

  1. tag:DOM元素的标签( 'a' , 'p ', 'div' ...)
  2. props:DOM元素的属性( {className:'box' , id:'container' , style:{fontSize:'20px'} , key:1 , ...})
  3. 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并渲染到页面上

参数:

  1. virtualDOM:需要解析的虚拟DOM
  2. 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的底层原理就实现啦

✿✿ヽ(°▽°)ノ✿

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 40、React 什么是React?React 是一个用于构建用户界面的框架(采用的是MVC模式):集中处理VIE...
    萌妹撒阅读 1,052评论 0 1
  • 文章结构: React中的虚拟DOM是什么? 虚拟DOM的简单实现(diff算法) 虚拟DOM的内部工作原理 Re...
    李轻舟阅读 3,085评论 2 14
  • 1.(Didact)一个DIY教程:创建你自己的react1.1 引言 2.渲染dom元素2.1 什么是DOM2....
    johnzhu12阅读 795评论 0 51
  • 前言 在Jq,原生javascript时期,在写页面时,往往强调的是内容结构,层叠样式,行为动作要分离,三者之间分...
    itclanCoder阅读 735评论 0 2
  • 1 我们十年前从别处搬来这里。 当年来到这时,建筑物前的两棵透着灵气大树吸引了我们。春去秋来,它仍然这么苍劲有力,...
    桔子的子阅读 234评论 0 3