前端面试之虚拟 DOM

vdom 是什么,为何会存在 vdom

什么是 vdom

  1. virtual dom, 虚拟 DOM
  2. 用 JS 模拟 DOM 结构
  3. DOM 操作非常“昂贵”
  4. DOM 变化的对比,放在 JS 层来做(图灵完备语言)提高效率
  5. 提高重绘性能
// 真实的 DOM
<ul id="list">
    <li class="item">Item1</li>
    <li class="item">Item2</li>
</ul>

// 虚拟 DOM
{
  tag: 'ul',
  attrs: {
    id: 'list'
  },
  children: [{
    tag: 'li',
    attrs: { calssName: 'item' },
    children: ['Item1']
  }, {
    tag: 'li',
    attrs: { calssName: 'item' },
    children: ['Item2']
  }]
}

设计一个需求场景

// 1、将该数据展示成一个表格。 2、随便修改一个信息,表格也跟着更改
[{
    name: '张三',
    age: '20',
    address: '北京'
}, {
    name: '李四',
    age: '21',
    address: '上海'
}, {
    name: '王五',
    age: '22',
    address: '广州'
}]

用 jQuery 实现

<div id="container"></div>
<button id="btn-change">change</button>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
    var data = [{
        name: '张三',
        age: '20',
        address: '北京'
    }, {
        name: '李四',
        age: '21',
        address: '上海'
    }, {
        name: '王五',
        age: '22',
        address: '广州'
    }];
    // 渲染函数
    function render(data) {
      var $container = $('#container')
      // 清空容器
      $container.html('')
    
      // 拼接table
      var $table = $('<table>')
      $table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'));
      data.forEach(function(item) {
        $table.append($('<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address + '</td></tr>'))
      });
      $container.append($table)
    }
    $('#btn-change').click(function() {
      data[1].age = 30;
      data[2].address = '深圳';
      // re-render
      render(data);
    })
    // 页面加载完立刻执行(初次渲染)
    render(data)
</script>

遇到的问题

  1. DOM 操作是低效的,js 运行效率高
  2. 尽量减少 DOM 操作
  3. 项目越复杂,影响越严重
  4. vdom 可解决这个问题
var div = document.createElement('div')
var item, result = '';
for (item in div) {
  result += '|' + item;
}
console.log(result)

打印的内容为:(证明操作DOM 是低效的 )
|align|title|lang|translate|dir|dataset|hidden|tabIndex|accessKey|draggable|spellcheck|autocapitalize|contentEditable|isContentEditable|inputMode|offsetParent|off...

vdom 如何应用,核心 API 是什么

解答思路

  1. 可用snabbdom 的用法来举例
  2. 核心 API: h 函数, patch 函数

介绍 snabbdom

var container = document.getElementById('container')

var vnode = h('div#container.two.classes', { on: { click: someFn } }, [
  h('span', { style: { fontWeight: 'bold' } }, 'This is bold'),
  ' and this is just normal text',
  h('a', { props: { href: '/foo' } }, 'I\'ll take you places!')
])
// Patch into empty DOM element – this modifies the DOM as a side effect
// 第一次渲染!!!!!!!!!!
patch(container, vnode)

var newVnode = h('div#container.two.classes', { on: { click: anotherEventHandler } }, [
  h('span', { style: { fontWeight: 'normal', fontStyle: 'italic' } }, 'This is now italic type'),
  ' and this is still just normal text',
  h('a', { props: { href: '/bar' } }, 'I\'ll take you places!')
])
// Second `patch` invocation
// 非第一次渲染!!!!!!
patch(vnode, newVnode) // Snabbdom efficiently updates the old view to the new state

重做之前的 demo

  <div id="container"></div>
  <button id="btn-change">change</button>
  <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.1/snabbdom.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.1/snabbdom-class.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.1/snabbdom-props.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.1/snabbdom-style.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.1/h.js"></script>
  <script>
var data = [{
    name: '张三',
    age: '20',
    address: '北京'
}, {
    name: '李四',
    age: '21',
    address: '上海'
}, {
    name: '王五',
    age: '22',
    address: '广州'
}];
data.unshift({
  name: '姓名',
  age: '年龄',
  address: '地址'
})
var snabbdom = window.snabbdom;
var patch = snabbdom.init([
  snabbdom_class,
  snabbdom_props,
  snabbdom_style,
  snabbdom_eventlisteners
]);
var h = snabbdom.h;
var container = document.getElementById('container');

var vnode;
function render(data) {
  var newVnode = h('table', {}, data.map(function(item) {
    var tds = [];
    var i;
    for (i in item) {
      if (item.hasOwnProperty(i)) {
        tds.push(h('td', {}, item[i] + ''))
      }
    }
    return h('tr', {}, tds);
  }))
  if (vnode) {
    patch(vnode, newVnode);
  } else {
    patch(container, newVnode);
  }
  vnode = newVnode;
}
render(data);
var btnChange = document.getElementById('btn-change')
btnChange.addEventListener('click', function() {
  data[1].age = 30;
  data[2].address = '深圳';
  render(data);
})
</script>

核心 API

  1. h('<标签名>', {...属性...}, [...子元素...])
  2. h('<标签名>', {...属性...}, '...')
  3. patch(container, vnode)
  4. patch(vnode, newVnode)

介绍一下 diff 算法

解答思路:

  1. 知道什么是 diff 算法,是 linux 的基础命令
  2. vdom 中应用 diff 算法是为了找出需要更新的节点
  3. diff 实现 patch(container, vnode) patch(vnode, newVnode)
  4. 核心逻辑 createElement 和 updateChildren

diff 算法非常复杂,实现难度大,源码量很大

去繁就简,讲明白核心流程,不关心细节

面试官也大部分不清楚细节,但是很关心核心流程

去繁就简后,依然有很大的挑战性,并不简单

vdom 为何使用 diff 算法

  1. DOM 操作是昂贵的,因此尽量减少 DOM 操作
  2. 找出本次 DOM 必须更新的节点来更新,其他的不更新
  3. 这个“找出”的过程,就需要 diff 算法

diff 实现过程

  1. patch(container, vnode)
  2. patch(vnode, newVnode)
// patch(container, vnode)
// 虚拟 DOM
{
  tag: 'ul',
  attrs: {
    id: 'list'
  },
  children: [{
    tag: 'li',
    attrs: { calssName: 'item' },
    children: ['Item1']
  }, {
    tag: 'li',
    attrs: { calssName: 'item' },
    children: ['Item2']
  }]
}

// 模拟代码(将虚拟 dom 转化为真实 DOM)
function createElement(vnode) {
    var tag = vnode.tag;
    var attrs = vnode.attrs || {};
    var children = vnode.children || [];
    if (!tag) {
        return null
    }
    // 创建元素
    var elem = document.createElement(tag);
    // 属性
    var attrName;
    for (attrName in attrs) {
        if(attrs.hasOwnProperty(attrName)) {
            elem.setAttribute(attrName. attrs[attrName])
        }
    }
    // 子元素
    children.forEach(function(childVnode) {
        // 递归调用createElement 创建子元素
        elem.appendChild(createElement(childVnode))
    })
    return elem;
}
// patch(vnode, newVnode) 对比两个 vnode
function updateChildren(vnode, newVnode) {
    var children = vnode.children || []
    var newChildren = newVnode.children || []
    // 遍历现有的 children
    children.forEach(function(child, index){
        var newChild = newChildren[index]
        if (newChild == null) {
            return
        }
        if (child.tag === newChild.tag) {
            // 两者 tag 一样
            updateChildren(child, newChild)
        } else {
            // 两者 tag 不一样
            replaceNode(child, newChild)
        }
    }) 
}

另外还有 节点新增和删除、节点重新排序、节点属性、样式、事件绑定、如何极致压榨性能

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • vdom是什么?为何使用vdom? virtual dom,虚拟DOM 用JS模拟DOM结构 DOM操作非常昂贵 ...
    简单tao的简单阅读 6,743评论 0 3
  • 前言   Vue2.0引入了虚拟DOM,比Vue1.0的初始渲染速度提升了2~4倍,并大大降低了内存消耗。目前主流...
    A郑家庆阅读 14,652评论 0 10
  • Vue2.0采用了虚拟DOM来代替对真实DOM的操作,最后通过某种机制来完成对真实DOM的更新,渲染视图。所谓的虚...
    lwz4070阅读 7,996评论 0 2
  • 摘要: 什么是虚拟DOM? 作者:浪里行舟 Fundebug经授权转载,版权归原作者所有。 前言 Vue.js 2...
    Fundebug阅读 12,087评论 0 6
  • 一、虚拟Dom简介 虚拟Dom的最初出现是在Rect中,性能卓越 二、什么是虚拟Dom? vdom可以看作是一个使...
    笑红尘123阅读 3,511评论 0 4

友情链接更多精彩内容