React 虚拟DOM

virtul DOM 也就是虚拟节点。通过JS的Object对象模拟DOM中的真实节点对象,再通过特定的render方法将其渲染成真实的DOM节点。

一、渲染步骤

生成vNode---->渲染成真实节点 --------->挂载到页面--------->diff比较
1、模拟方法和渲染方法
需求,生成一个如下图所示的DOM结构


image.png

调用

let virtualDom1 = createElement('ul', {class: 'list'}, [
    createElement('li', {class: 'item'}, ['a']),
    createElement('li', {class: 'item'}, ['b']),
    createElement('li', {class: 'item'}, ['c']),
])
let virtualDom2 = createElement('ul', {class: 'list'}, [
    createElement('li', {class: 'item'}, ['1']),
    createElement('li', {class: 'item'}, ['2']),
    createElement('li', {class: 'item'}, ['3']),
])
let el = render(virtualDom);
renderDom(el, window.root);
let patchs = diff(virtualDom1, virtualDom2);

生成虚拟对象的方法createElement

function createElement(type, props, children) {
    return new Element(type, props, children)
}
class Element{
    constructor(type, props, children){
        this.type = type;
        this.props = props;
        this.children = children
    }
}

将虚拟对象渲染成真实DOM的render方法

//render方法将vNode转化成真实DOM
function render(eleObj){
    //创建元素
    let el = document.createElement(eleObj.type);
    //设置属性
    for(let key in eleObj.props) {
        setAttr(el, key, eleObj.props[key]);
    }
    //递归渲染子元素
    eleObj.children.foEach(child => {
        child = child instanceof Element ? render(child) : document.createTextNode(child);
        el.appendChild(child);
    })
}
setAttr(node, key, value) {
    switch(key) {
        case 'value':
            if (node.tagName.toUpperCase() === 'INPUT' || node.tagName.toUpperCase() === 'TEXTAREA') {
                node.value = value;          
            }else {
                node.setAttribute(key, value);
            }
            break;
        case 'style':
            node.style.cssText = value;
            break;
        default:
            node.setAttribute(key, value);
            break;
    }
}

渲染节点到页面的方法renderDom

//将真实DOM渲染到页面
function renderDom(el, target) {
    target.appendChild(el);
}

二、DOM DIFF 算法

1、核心思想

DOM DIFF 就是比较两个虚拟DOM的区别,实际上就是比较两个对象的区别。根据两个虚拟对象创建出补丁,描述改变的内容。将这个补丁用来更新DOM。


image.png

【注意】不会更改所有节点,只更改有改变的部分

2、DOM DIFF 两种优化策略

1)分层比较,一层一层比,不会跨级对比
2)如果一层的对象只是换了下位置,可以通过key值直接换位置。

2、算法实现

差异计算:先序深度优先遍历


image.png

规则:
1、若节点类型不相同,直接采用替换模式,{type:'REPLACE',newNode:newNode}
2、当节点类型相同时,去看一下属性是否相同,产生一个属性的补丁包,比如{type:'ATTRS',attrs:{class: 'list-group'}
3、新的DOM节点不存在,也返回一个不存在的补丁包{type:'REMOVE',index:XXX}
4、文本的变化{type:'TEXT', text:1}

DIff 算法
//diff 算法
let Index = 0;
function diff(oldTree, newTree) {
    let patches = {};
    let index = 0;
    //递归数比较后的结果放到补丁包中
    walk(oldTree, newTree, index, patches);
    return patches;
}
function walk(oldTree, newTree, index, patches){
    let currentPatch = [];//每个元素都有一个补丁对象
    if (!newTree) {
        currentPatch.push({type:'REMOVE', index})
    } 
    if (isString(oldTree) && isString(newTree)) {
        // 判断文本是否一致
        if (oldTree !== newTree) {
            currentPatch.push({type:'TEXT',text:newTree}); 
        }
    }else if(oldTree.type === newTree.type) {
        //比较属性是否有更改
        let attrs = diffAttr(oldTree.props, newTree.props);
        if(Object.keys(attrs).length) {
            currentPatch.push({type:'ATTRS', attrs});
        }
        // 如果有儿子节点,遍历子节点
          diffChildren(oldTree.children, newTree.children, index, patches);
    } else {
        // 节点类型不同的时候,直接替换
        currentPatch.push({type:'REPLACE', newTree});
    }
    // 当前元素有补丁的情况下,将元素和补丁对应起来,放到大补丁包中
    if(currentPatch.length) {
        patches[index] = currentPatch; 
    }
}
function diffAttr(oldAttrs, newAttrs) {
    let patch = {};
    for(let key in oldAttrs) {
        if(oldAttrs[key] !== newAttrs[key]) {
            patch[key] = newAttrs[key];//有可能是undefined,新节点没有旧节点的属性      
        }
    }
    for(let key in newAttrs) {
        //老节点没有新节点的属性
        if(! oldAttrs.hasOwnProperty(key)) {
            patch[key] = newAttrs[key]
        }
    }
    return patch;
}

function diffChildren(oldChildren, newChildren, index, patches){
    // 比较老的第一个和新的第一个
    oldChildren.forEach((child, idx) => {
        // 记得索引得改
        // Index 每次传递给walk时,index是递增的,所有节点都基于一个序号实现,因此需要维护一个全局Index
        walk(child, newChildren[idx], ++Index, patches);
    }) 
}


function isString(node) {
    return Object.prototype.toString.call(node) === '[object string]';
}


function patch(node, patches) {
 // 给某个元素打补丁
 
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,874评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,102评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,676评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,911评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,937评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,935评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,860评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,660评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,113评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,363评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,506评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,238评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,861评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,486评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,674评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,513评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,426评论 2 352

推荐阅读更多精彩内容