深入了解virtual dom

1,003 阅读4分钟

前言

vdom 是 vue 和 React 的核心,先讲哪个都绕不开它

vdom 比较独立,使用也比较简单

如果面试问到 vue 和 React 和实现,免不了问 vdom,带着三个问题去深入了解

问题

  • vdom 是什么?为何会存在 vdom?
  • vdom 的如何应用,核心 API 是什么
  • 介绍一下 diff 算法

1、vdom 是什么?为何会存在 vdom?

  • virtual dom,虚拟 DOM
  • 用JS模拟DOM的结构
  • DOM 变化的对比,放在 JS 层来做(图灵完备语言)
  • 提高重绘性能

DOM结构

<ul id='list'>
    <li class='item'>item1</li>
    <li class='item'>item2</li>
</ul>

JS模拟

{
    tag:'ul',
    attrs:{
        id:'list'
    },
    children:[
        {
            tag:'li',
            attrs:{
                className: 'item'
            },
            children:['item1']
        },{
            tag:'li',
            attrs:{
                className: 'item'
            },
            children:['item2']
        }
    ]
}

设计一个需求场景

用jQuery实现

遇到的问题

  • DOM的操作是“昂贵”的,js运行效率高
  • 尽量减少DOM的操作,而不是推倒重来
  • 项目越复杂,影响越严重
  • vdom即可解决这些问题

问题解答

  • virtual dom , 虚拟 DOM
  • 用 JS 模拟 DOM 结构
  • DOM 操作非常“昂贵”
  • 将 DOM 对比操作放在 JS 层,提高效率

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

  • 介绍 snabbdom (vdom的一个库)
  • 重做之前的 demo
  • 核心 API

snabbdom 一个注重简单性、模块化、强大功能和性能的虚拟DOM库。地址

介绍 snabbdom - h 函数

介绍 snabbdom - patch 函数

重做demo

// <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.js"></script>
// <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.js"></script>
// <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.js"></script>
// <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.js"></script>
// <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
// <script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.js"></script>
var snabbdom = window.snabbdom

// 定义 patch
var patch = snabbdom.init([
  snabbdom_class,
  snabbdom_props,
  snabbdom_style,
  snabbdom_eventlisteners
])

// 定义 h
var h = snabbdom.h

var container = document.getElementById('container')

// 生成 vnode
var vnode = h('ul#list', {}, [
  h('li.item', {}, 'Item 1'),
  h('li.item', {}, 'Item 2')
])
patch(container, vnode)

document.getElementById('btn-change').addEventListener('click', function () {
  // 生成 newVnode
  var newVnode = h('ul#list', {}, [
    h('li.item', {}, 'Item 1'),
    h('li.item', {}, 'Item B'),
    h('li.item', {}, 'Item 3')
  ])
  patch(vnode, newVnode) // 找出差异,渲染差异
})
// jquery例子改造
var snabbdom = window.snabbdom
// 定义关键函数 patch
var patch = snabbdom.init([
  snabbdom_class,
  snabbdom_props,
  snabbdom_style,
  snabbdom_eventlisteners
])

// 定义关键函数 h
var h = snabbdom.h

// 原始数据
var data = [{
    name: '张三',
    age: '20',
    address: '北京'
  },
  {
    name: '李四',
    age: '21',
    address: '上海'
  },
  {
    name: '王五',
    age: '22',
    address: '广州'
  }
]
// 把表头也放在 data 中
data.unshift({
  name: '姓名',
  age: '年龄',
  address: '地址'
})

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) {
    // re-render
    patch(vnode, newVnode)
  } else {
    // 初次渲染
    patch(container, newVnode)
  }

  // 存储当前的 vnode 结果
  vnode = newVnode
}

// 初次渲染
render(data)


var btnChange = document.getElementById('btn-change')
btnChange.addEventListener('click', function () {
  data[1].age = 30
  data[2].address = '深圳'
  // re-render
  render(data)
})

  • 使用 data 生成 vnode
  • 第一次渲染,将 vnode 渲染到 #container 中
  • 并将 vnode 缓存下来
  • 修改 data 之后,用新 data 生成 newVnode
  • 将 vnode 和 newVnode 对比

核心API:h 函数、patch 函数

  • h(‘<标签名>’, {…属性…}, […子元素…])
  • h(‘<标签名>’, {…属性…}, ‘….’)
  • patch(container, vnode)
  • patch(vnode, newVnode)

介绍一下 diff 算法

什么是diff算法

  • linux diff 命令
  • git diff (对比两个文件之间差异)

去繁就简

  • diff 算法非常复杂,实现难度很大,源码量很大
  • 去繁就简,讲明白核心流程,不关心细节
  • 面试官也大部分都不清楚细节,但是很关心核心流程
  • 去繁就简之后,依然具有很大挑战性,并不简单

vdom 为何用 diff 算法

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

diff 算法的实现流程

  • patch(container, vnode)
  • patch(vnode, newVnode)

核心逻辑:createElement 和 updateChildren

// diff 算法实现
// code demo
function createElement(vnode) {
  var tag = vnode.tag // 'ul'
  var attrs = vnode.attrs || {}
  var children = vnode.children || []
  if (!tag) {
    return null
  }
  // 创建真实的 DOM 元素
  var elem = document.createElement(tag)
  // 属性
  var attrName
  for (attrName in attrs) {
    if (attrs.hasOwnProperty(attrName)) {
      // 给 elem 添加属性
      elem.setAttribute(attrName, attrs[attrName])
    }
  }
  // 子元素
  children.forEach(function (childVnode) {
    // 给 elem 添加子元素
    elem.appendChild(createElement(childVnode)) // 递归
  })

  // 返回真实的 DOM 元素
  return elem
}

// vnode newVnode compare
function updateChildren(vnode, newVnode) {
  var children = vnode.children || []
  var newChildren = newVnode.children || []

  children.forEach(function (childVnode, index) {
    var newChildVnode = newChildren[index]
    if (childVnode.tag === newChildVnode.tag) {
      // 深层次对比,递归
      updateChildren(childVnode, newChildVnode)
    } else {
      // 替换
      replaceNode(childVnode, newChildVnode)
    }
  })
}

function replaceNode(vnode, newVnode) {
  var elem = vnode.elem // 真实的 DOM 节点
  var newElem = createElement(newVnode)
  // 替换
}

  • 节点新增和删除
  • 节点重新排序
  • 节点属性、样式、事件变化
  • 如何极致压榨性能
  • ......

answer:

  • 知道什么是 diff 算法,是 linux 的基础命令
  • vdom 中应用 diff 算法是为了找出需要更新的节点
  • vdom 实现过程,createElement 和 updateChildren
  • 与核心函数 patch 的关系