虚拟dom到真实dom

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了虚拟dom到真实dom相关的知识,希望对你有一定的参考价值。

参考技术A 因为浏览器渲染真实dom的时候,根据html和css进行解析-渲染成页面,对于浏览器来说,它会一个一个dom安先后顺序来进行解析渲染,不会进行汇总比较,或者是因为考虑的真实的dom太复杂了,没法进行快速的比较得到最小差异化。

而虚拟dom简单来说就是一开始用js画了一个假的dom结构,映射真实的dom,去除一些我们不用去关注的事情。然后在之后更新dom的时候,先根据这些变更来得出最小的差异化。再把这些差异一次写入真实dom,性能会有很大的提升。

从虚拟dom到真是dom,找到渲染前后真正发生变化的地方,这就是diff算法,考虑到很少进行跨层移动,所以用平层对比,时间复杂度从O(n^3)缩短为O(n),在对比过程中直接对真实dom更新。变更一般有三种: 文本 ,节点属性,节点变更,增删节点(绑定key值的作用)

ps html和css解析成的render树,每一个节点renderObject,浏览器会根据原则

1、它是页面的根对象

2、它具有显式的CSS位置属性(相对、绝对或转换)

3、它是透明的

4、是否有溢出、alpha掩码或反射

5、有一个CSS过滤器

6、对应于具有3D (WebGL)上下文或加速2D上下文的画布元素

7、对应于一个视频元素

来生成renderLayer,每一个renderObject会指向一个renderLayer,直接或者间接指向父节点的renderLayer。当浏览器渲染的时候renderLayer决定了渲染的层级,renderObject决定了渲染的内容

什么时候虚拟 DOM 比真实 DOM 快?

【中文标题】什么时候虚拟 DOM 比真实 DOM 快?【英文标题】:When is virtual DOM faster than real DOM? 【发布时间】:2021-03-10 12:37:22 【问题描述】:

我知道如果在vanilla js中更改DOM,整个浏览器每次都会重新布局和重新绘制。

所以在元素多、变化频繁的单页应用中,vanilla js 会变慢。

但我最近在 Benchmark 表中看到 vanilla js 比使用虚拟 DOM 时的反应快得多,即使更改了大量数据也是如此。

那么,使用虚拟 DOM 的原因是为了自动化和方便开发人员而不是为了提高速度吗?

这是我看到的 Benchmark 表。benchmark table

这是 vanillajs 测试代码

'use strict';

function _random(max) 
    return Math.round(Math.random()*1000)%max;


const rowTemplate = document.createElement("tr");
rowTemplate.innerHTML = "<td class='col-md-1'></td><td class='col-md-4'><a class='lbl'></a></td><td class='col-md-1'><a class='remove'><span class='remove glyphicon glyphicon-remove' aria-hidden='true'></span></a></td><td class='col-md-6'></td>";

class Store 
    constructor() 
        this.data = [];
        this.backup = null;
        this.selected = null;
        this.id = 1;
    
    buildData(count = 1000) 
        var adjectives = ["pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain", "quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd", "unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"];
        var colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
        var nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"];
        var data = [];
        for (var i = 0; i < count; i++)
            data.push(id: this.id++, label: adjectives[_random(adjectives.length)] + " " + colours[_random(colours.length)] + " " + nouns[_random(nouns.length)] );
        return data;
    
    updateData(mod = 10) 
        for (let i=0;i<this.data.length;i+=10) 
            this.data[i].label += ' !!!';
            // this.data[i] = Object.assign(, this.data[i], label: this.data[i].label +' !!!');
        
    
    delete(id) 
        const idx = this.data.findIndex(d => d.id==id);
        this.data = this.data.filter((e,i) => i!=idx);
        return this;
    
    run() 
        this.data = this.buildData();
        this.selected = null;
    
    add() 
        this.data = this.data.concat(this.buildData(1000));
        this.selected = null;
    
    update() 
        this.updateData();
        this.selected = null;
    
    select(id) 
        this.selected = id;
    
    hideAll() 
        this.backup = this.data;
        this.data = [];
        this.selected = null;
    
    showAll() 
        this.data = this.backup;
        this.backup = null;
        this.selected = null;
    
    runLots() 
        this.data = this.buildData(10000);
        this.selected = null;
    
    clear() 
        this.data = [];
        this.selected = null;
    
    swapRows() 
        if(this.data.length > 998) 
            var a = this.data[1];
            this.data[1] = this.data[998];
            this.data[998] = a;
        
    


var getParentId = function(elem) 
    while (elem) 
        if (elem.tagName==="TR") 
            return elem.data_id;
        
        elem = elem.parentNode;
    
    return undefined;

class Main 
    constructor(props) 
        this.store = new Store();
        this.select = this.select.bind(this);
        this.delete = this.delete.bind(this);
        this.add = this.add.bind(this);
        this.run = this.run.bind(this);
        this.update = this.update.bind(this);
        this.start = 0;
        this.rows = [];
        this.data = [];
        this.selectedRow = undefined;

        document.getElementById("main").addEventListener('click', e => 
            //console.log("listener",e);
            if (e.target.matches('#add')) 
                e.preventDefault();
                //console.log("add");
                this.add();
            
            else if (e.target.matches('#run')) 
                e.preventDefault();
                //console.log("run");
                this.run();
            
            else if (e.target.matches('#update')) 
                e.preventDefault();
                //console.log("update");
                this.update();
            
            else if (e.target.matches('#hideall')) 
                e.preventDefault();
                //console.log("hideAll");
                this.hideAll();
            
            else if (e.target.matches('#showall')) 
                e.preventDefault();
                //console.log("showAll");
                this.showAll();
            
            else if (e.target.matches('#runlots')) 
                e.preventDefault();
                //console.log("runLots");
                this.runLots();
            
            else if (e.target.matches('#clear')) 
                e.preventDefault();
                //console.log("clear");
                this.clear();
            
            else if (e.target.matches('#swaprows')) 
                e.preventDefault();
                //console.log("swapRows");
                this.swapRows();
            
            else if (e.target.matches('.remove')) 
                e.preventDefault();
                let id = getParentId(e.target);
                let idx = this.findIdx(id);
                //console.log("delete",idx);
                this.delete(idx);
            
            else if (e.target.matches('.lbl')) 
                e.preventDefault();
                let id = getParentId(e.target);
                let idx = this.findIdx(id);
                //console.log("select",idx);
                this.select(idx);
            
        );
        this.tbody = document.getElementById("tbody");
    
    findIdx(id) 
        for (let i=0;i<this.data.length;i++)
            if (this.data[i].id === id) return i;
        
        return undefined;
    
    run() 
        this.removeAllRows();
        this.store.clear();
        this.rows = [];
        this.data = [];
        this.store.run();
        this.appendRows();
        this.unselect();
    
    add() 
        this.store.add();
        this.appendRows();
    
    update() 
        this.store.update();
        for (let i=0;i<this.data.length;i+=10) 
            this.rows[i].childNodes[1].childNodes[0].innerText = this.store.data[i].label;
        
    
    unselect() 
        if (this.selectedRow !== undefined) 
            this.selectedRow.className = "";
            this.selectedRow = undefined;
        
    
    select(idx) 
        this.unselect();
        this.store.select(this.data[idx].id);
        this.selectedRow = this.rows[idx];
        this.selectedRow.className = "danger";
    
    recreateSelection() 
        let old_selection = this.store.selected;
        let sel_idx = this.store.data.findIndex(d => d.id === old_selection);
        if (sel_idx >= 0) 
            this.store.select(this.data[sel_idx].id);
            this.selectedRow = this.rows[sel_idx];
            this.selectedRow.className = "danger";
        
    
    delete(idx) 
        // Remove that row from the DOM
        this.store.delete(this.data[idx].id);
        this.rows[idx].remove();
        this.rows.splice(idx, 1);
        this.data.splice(idx, 1);
        this.unselect();
        this.recreateSelection();
    
    removeAllRows() 
        // ~258 msecs
        // for(let i=this.rows.length-1;i>=0;i--) 
        //     tbody.removeChild(this.rows[i]);
        // 
        // ~251 msecs
        // for(let i=0;i<this.rows.length;i++) 
        //     tbody.removeChild(this.rows[i]);
        // 
        // ~216 msecs
        // var cNode = tbody.cloneNode(false);
        // tbody.parentNode.replaceChild(cNode ,tbody);
        // ~212 msecs
        this.tbody.textContent = "";

        // ~236 msecs
        // var rangeObj = new Range();
        // rangeObj.selectNodeContents(tbody);
        // rangeObj.deleteContents();
        // ~260 msecs
        // var last;
        // while (last = tbody.lastChild) tbody.removeChild(last);
    
    runLots() 
        this.removeAllRows();
        this.store.clear();
        this.rows = [];
        this.data = [];
        this.store.runLots();
        this.appendRows();
        this.unselect();
    
    clear() 
        this.store.clear();
        this.rows = [];
        this.data = [];
        // This is actually a bit faster, but close to cheating
        // requestAnimationFrame(() => 
            this.removeAllRows();
            this.unselect();
        // );
    
    swapRows() 
        if (this.data.length>10) 
            this.store.swapRows();
            this.data[1] = this.store.data[1];
            this.data[998] = this.store.data[998];

            this.tbody.insertBefore(this.rows[998], this.rows[2])
            this.tbody.insertBefore(this.rows[1], this.rows[999])

            let tmp = this.rows[998];
            this.rows[998] = this.rows[1];
            this.rows[1] = tmp;
        


        // let old_selection = this.store.selected;
        // this.store.swapRows();
        // this.updateRows();
        // this.unselect();
        // if (old_selection>=0) 
        //     let idx = this.store.data.findIndex(d => d.id === old_selection);
        //     if (idx > 0) 
        //         this.store.select(this.data[idx].id);
        //         this.selectedRow = this.rows[idx];
        //         this.selectedRow.className = "danger";
        //     
        // 
    
    appendRows() 
        // Using a document fragment is slower...
        // var docfrag = document.createDocumentFragment();
        // for(let i=this.rows.length;i<this.store.data.length; i++) 
        //     let tr = this.createRow(this.store.data[i]);
        //     this.rows[i] = tr;
        //     this.data[i] = this.store.data[i];
        //     docfrag.appendChild(tr);
        // 
        // this.tbody.appendChild(docfrag);

        // ... than adding directly
        var rows = this.rows, s_data = this.store.data, data = this.data, tbody = this.tbody;
        for(let i=rows.length;i<s_data.length; i++) 
            let tr = this.createRow(s_data[i]);
            rows[i] = tr;
            data[i] = s_data[i];
            tbody.appendChild(tr);
        
    
    createRow(data) 
        const tr = rowTemplate.cloneNode(true),
            td1 = tr.firstChild,
            a2 = td1.nextSibling.firstChild;
        tr.data_id = data.id;
        td1.textContent = data.id;
        a2.textContent = data.label;
        return tr;
    


new Main();
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <title>VanillaJS-"keyed"</title>
    <link href="/css/currentStyle.css" rel="stylesheet"/>
</head>
<body>
<div id='main'>
    <div class="container">
        <div class="jumbotron">
            <div class="row">
                <div class="col-md-6">
                    <h1>VanillaJS-"keyed"</h1>
                </div>
                <div class="col-md-6">
                    <div class="row">
                        <div class="col-sm-6 smallpad">
                            <button type='button' class='btn btn-primary btn-block' id='run'>Create 1,000 rows</button>
                        </div>
                        <div class="col-sm-6 smallpad">
                            <button type='button' class='btn btn-primary btn-block' id='runlots'>Create 10,000 rows</button>
                        </div>
                        <div class="col-sm-6 smallpad">
                            <button type='button' class='btn btn-primary btn-block' id='add'>Append 1,000 rows</button>
                        </div>
                        <div class="col-sm-6 smallpad">
                            <button type='button' class='btn btn-primary btn-block' id='update'>Update every 10th row</button>
                        </div>
                        <div class="col-sm-6 smallpad">
                            <button type='button' class='btn btn-primary btn-block' id='clear'>Clear</button>
                        </div>
                        <div class="col-sm-6 smallpad">
                            <button type='button' class='btn btn-primary btn-block' id='swaprows'>Swap Rows</button>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <table class="table table-hover table-striped test-data">
            <tbody id="tbody">
            </tbody>
        </table>
        <span class="preloadicon glyphicon glyphicon-remove" aria-hidden="true"></span>
    </div>
</div>
<script src='src/Main.js'></script>
</body>
</html>

这是反应测试代码

var React = require('react');
var ReactDOM = require('react-dom');

function random(max) 
  return Math.round(Math.random() * 1000) % max;


const A = ["pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain", "quaint", "clean",
  "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd", "unsightly", "adorable", "important", "inexpensive",
  "cheap", "expensive", "fancy"];
const C = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
const N = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse",
  "keyboard"];

let nextId = 1;

function buildData(count) 
  const data = new Array(count);
  for (let i = 0; i < count; i++) 
    data[i] = 
      id: nextId++,
      label: `$A[random(A.length)] $C[random(C.length)] $N[random(N.length)]`,
    ;
  
  return data;


const GlyphIcon = <span className="glyphicon glyphicon-remove" aria-hidden="true"></span>;

class Row extends React.Component 
  onSelect = () => 
    this.props.select(this.props.item);
  

  onRemove = () => 
    this.props.remove(this.props.item);
  

  shouldComponentUpdate(nextProps) 
    return nextProps.item !== this.props.item || nextProps.selected !== this.props.selected;
  

  render() 
    let  selected, item  = this.props;
    return (<tr className=selected ? "danger" : "">
      <td className="col-md-1">item.id</td>
      <td className="col-md-4"><a onClick=this.onSelect>item.label</a></td>
      <td className="col-md-1"><a onClick=this.onRemove>GlyphIcon</a></td>
      <td className="col-md-6"></td>
    </tr>);
  


function Button( id, cb, title ) 
  return (
    <div className="col-sm-6 smallpad">
      <button type="button" className="btn btn-primary btn-block" id=id onClick=cb>title</button>
    </div>
  );


class Jumbotron extends React.Component 
  shouldComponentUpdate() 
    return false;
  

  render() 
    const  run, runLots, add, update, clear, swapRows  = this.props;
    return (
      <div className="jumbotron">
        <div className="row">
          <div className="col-md-6">
            <h1>React keyed</h1>
          </div>
          <div className="col-md-6">
            <div className="row">
              <Button id="run" title="Create 1,000 rows" cb=run />
              <Button id="runlots" title="Create 10,000 rows" cb=runLots />
              <Button id="add" title="Append 1,000 rows" cb=add />
              <Button id="update" title="Update every 10th row" cb=update />
              <Button id="clear" title="Clear" cb=clear />
              <Button id="swaprows" title="Swap Rows" cb=swapRows />
            </div>
          </div>
        </div>
      </div>
    );
  


class Main extends React.Component 
  state = 
    data: [],
    selected: 0,
  ;

  run = () => 
    this.setState( data: buildData(1000), selected: 0 );
  

  runLots = () => 
    this.setState( data: buildData(10000), selected: 0 );
  

  add = () => 
    this.setState( data: this.state.data.concat(buildData(1000)), selected: this.state.selected );
  

  update = () => 
    const data = this.state.data;
    for (let i = 0; i < data.length; i += 10) 
      const item = data[i];
      data[i] =  id: item.id, label: item.label + ' !!!' ;
    
    this.forceUpdate();
  

  select = (item) => 
    this.setState( selected: item.id );
  

  remove = (item) => 
    const data = this.state.data;
    data.splice(data.indexOf(item), 1);
    this.forceUpdate();
  

  clear = () => 
    this.setState( data: [], selected: 0 );
  

  swapRows = () => 
    const data = this.state.data;
    if (data.length > 998) 
      let temp = data[1];
      data[1] = data[998];
      data[998] = temp;
    
    this.forceUpdate();
  

  render() 
    return (<div className="container">
      <Jumbotron run=this.run runLots=this.runLots add=this.add update=this.update clear=this.clear swapRows=this.swapRows />
      <table className="table table-hover table-striped test-data"><tbody>
        this.state.data.map((item) => (
          <Row key=item.id item=item selected=this.state.selected === item.id select=this.select remove=this.remove></Row>
        ))
      </tbody></table>
      <span className="preloadicon glyphicon glyphicon-remove" aria-hidden="true"></span>
    </div>);
  


ReactDOM.render(
  <Main />,
  document.getElementById('main'),
);
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>React</title>
  <link href="/css/currentStyle.css" rel="stylesheet"/>
</head>
<body>
  <div id='main'></div>
  <script src='dist/main.js'></script>
</body>
</html>

在此处进行基准测试结果站点 js-framework-benchmark result 和github站点 js-framework-benchmark github

【问题讨论】:

当你做基准测试时,你总是能得到更好的 vanilla js 结果。因为使用虚拟 dom,您完成了 VDOM + DOM 更改,然后进行测量。当您进行一些小的更改并需要检测其他元素的更改(例如三个)时,好处就来了。 【参考方案1】:

React 使用虚拟 DOM 来增强 它的性能。

React 旨在帮助开发人员从称为“组件”的孤立代码片段制作复杂的 UI。所以它的主要目的是帮助创建大型项目,而不是比 Vanilla JS 更快。但是为了提高性能,它使用了虚拟 DOM。

【讨论】:

以上是关于虚拟dom到真实dom的主要内容,如果未能解决你的问题,请参考以下文章

虚拟DOM与dom diff

Vue原理解析(五):彻底搞懂虚拟Dom到真实Dom的生成过程

是否可以将元素添加到 React 的虚拟 DOM 但使用 jQuery 附加到真实 DOM?

React虚拟dom中的key值

虚拟DOM(Virtual DOM)

面试官:什么是虚拟DOM?如何实现一个虚拟DOM?