阅读 251

virtual dom 简单了解

管理应用程序状态和用户界面的同步一直是前端UI开发复杂性的主要来源。目前出现了不同的方式来处理这个问题。本文简单讨论其中一种方式virtual dom。

文章概要:

  1. virtual dom 基本概念,存在原因。
  2. virtual dom 简单应用。
  3. virtual dom 简单实现思路。
  4. 小结

1.virtual dom 基本概念

1.1什么是virtual dom?

virtual dom:虚拟节点。通过JS模拟DOM中的节点。

下图是虚拟节点的使用,通过特定的render方法将html模板转换成js,再用特殊的方法h函数得到虚拟节点的结构,通过特定的patch方法渲染成真实的DOM更新。

数据更新时,渲染得到新的 virtual dom,然后与上一次得到的 virtual dom 进行 diff比对,dom diff 在JS层面计算得到所有需要变更 DOM,然后在 patch 过程中将需要变更 DOM更新到UI界面。


 图片来源:JavaScript框架中的变化及其检测

1.2为啥要用virtual dom?

 DOM 操作是“昂贵”的,怎么个昂贵法?先看一张图浏览器渲染的流程(WebKit 主流程 内容参考浏览器的工作原理



⑴ HTML解析出DOM Tree

⑵ CSS解析出Style Rules

⑶ WebKit内核的浏览器上,处理一个节点的样式的过程称为attachment,DOM Tree关联Style Rules生成Render Tree

⑷ Layout 根据Render Tree计算每个节点的信息

⑸ Painting 根据计算好的信息绘制整个页面

DOM最终呈现在界面的过程复杂

再看一张图,DOM 的大概数据结构


图片来源:Vue.js 技术揭秘

DOM本身数据结构复杂

综上:DOM最终呈现在界面的过程复杂,DOM本身数据结构复杂

在开发过程中尽量减少直接操作 DOM ,用 JS 模拟 DOM 结构, DOM 变化的对比计算,放在 JS 层来做,计算完成后少量重绘或回流,提高性能。

同时DOM的操作跟数据挂钩,只用关心数据的变化,不需要关心具体DOM的操作。virtual dom这种方式就诞生了。

2.virtual dom 简单应用

virtual dom 的简单应用 此处snabbdom(A virtual dom library with focus on simplicity, modularity, powerful features and performance.) 库来举例(因为vue Virtual DOM 借鉴 Snabbdom库,想更多了解一下),当然也有其他的库 javaScript virtual dom 库。

2.1 snabbdom的几个函数

网站上的inline example 

var snabbdom = require('snabbdom');
var patch = snabbdom.init([ // Init patch function with chosen modules
  require('snabbdom/modules/class').default, // makes it easy to toggle classes
  require('snabbdom/modules/props').default, // for setting properties on DOM elements
  require('snabbdom/modules/style').default, // handles styling on elements with support for animations
  require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes

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复制代码

可以看出 snabbdom.init 方法 返回 patch 方法,patch 方法 有两种用法

(1)
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);


(2)
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
复制代码

然后就是h 函数(生成vnodes)DOM 结构通常被视为一棵树,而元素则被比成树上的节点(node),同理 virtual dom 的节点就是 Virtual Node 了及vnode。

var h = require('snabbdom/h').default; // helper function for creating vnodes复制代码

h函数的使用:h accepts a tag/selector as a string, an optional data object and an optional string or array of children.

h(选择器或者标签字符串,可选object对象属性,子节点数组或者string) 下图从h函vode 对应的dom 结构


2.2 snabbdom的简单应用

js 部分

var snabbdom = require('snabbdom');
// 定义patch 函数
var patch = snabbdom.init([ // Init patch function with chosen modules
  require('snabbdom/modules/class').default, // makes it easy to toggle classes
  require('snabbdom/modules/props').default, // for setting properties on DOM elements
  require('snabbdom/modules/style').default, // handles styling on elements with support for animations
  require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);

// 定义h 函数
var h = require('snabbdom/h').default; // helper function for creating vnodes

var container = document.getElementById('container');

// 原始数据
var data = ['周杰伦', '林俊杰 ', '陈奕迅']
// 渲染函数
var vnode
render(data)

function render(data) {
  var newVnode = h('div', {
    id: 'container'
  }, [
    h('button', {
      id: 'btn1',
      on: {
        click: changeSinger
      }
    }, '替换一个歌手'),
    h('button', {
      id: 'btn2',
      on: {
        click: addSinger
      }
    }, '增加一个歌手'),
    h('ul', {}, data.map(function(item) {
      return h('li', {
        style: {
          color: '#f3c8d3'
        }
      }, item)
    }))
  ])

  if (vnode) {
    // 重新渲染
    patch(vnode, newVnode)
  } else {
    // 初次渲染
    patch(container, newVnode)
  }

  // 存储当前的 vnode,方便下次做对比
  vnode = newVnode
}

function changeSinger() {
  data.splice(0, 1, '五月天')
  render(data)
}

count = 0

function addSinger() {
  count++
  data.push('五月天' + count)
  render(data)
}

// html 部分-----------------------------------------------------------------------

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>demo</title>
  </head>
  <body>
    <div id="container"></div>
    <!-- built files will be auto injected -->
  </body>
</html>复制代码

以上代码 写完安装依赖打包出来,点击‘替换一个歌手’ 按钮,观察Element变化 如下图 变化的地方只有一个li 的文本

// 原始数据

var data = ['周杰伦', '林俊杰 ', '陈奕迅']

// 新数据

var data = ['五月天', '林俊杰 ', '陈奕迅']


virtual dom 这种实现,用 JS 模拟 DOM 结构, DOM diff比对,放在 JS 层来做,找出变化了DOM渲染,提高性能。同时DOM的操作跟数据挂钩,我们只用关心数据的变化,不需要关心具体DOM的操作。

 3.virtual dom 简单实现思路

从上面例子,可以大概推测 实现一个简单的 virtual dom 比较重要的就是

  • h函数生成 vnode (JS 模拟 DOM 结构virtual dom)
  • path(container, vnode) 初次渲染界面(用virtual dom构建了真的DOM树,挂载到container上面)
  • 缓存 vnode
  • 数据更新(生成新的vnode),生成新的newVnode
  • path(vnode, newVnode) 将旧视图更新为新状态(diff算法 比对vnode, newVnode,找出需要修改的dom 修改)
  • 更新缓存 vnode

根据不同的结构生成vnode(可能对模版处理渲染)。假设此时已经存在一个JS模拟DOM的数据结构。实现简单的如下函数比对vnode, newVnode的不同,并修改需要变化的真实节点。此时的模拟object 结构.

{
  tagName: 'ul',
  attrs: {
    id: 'list'
  },
  children: [{
    tag: 'li',
    attrs: {
      className: 'item'
    }
    children: '周杰伦'
  }, {
    tag: 'li',
    attrs: {
      className: 'item'
    }
    children: '林俊杰'
  }]
}复制代码

递归构建dom树

function createElement(vnode) {
  var tagName = vnode.tagName;
  var attrs = vnode.attrs || {};
  var children = vnode.children;
  if (!tagName) {
    return
  }

  // 创建真实的 DOM 元素
  let element = document.createElement(this.tagName);
  // 属性
  var attrName;
  for (attrName in attrs) {
    if (attrs.hasOwnProperty(attrName)) {
      // 给 element 添加属性
      element.setAttribute(attrName, attrs[attrName]);
    }
  }
  // 子元素是字符串
  if (isString(children)) {
    let childElement = document.createTextNode(children);
    element.appendChild(childElement);
  } else {
    // 子元素是数组
    children.forEach(child => {
      // 给 element 添加子元素
      element.appendChild(createElement(child));
    })
  }
  vnode.elem = element;
  return element;
}复制代码

比对不同更行element

function updateChildren(vnode, newVnode) {
  var children = vnode.children || []
  var newChildren = newVnode.children || []
  // 比对 tagName 和 Attrs 不同直接替换节点
  if(diffTag(children,newChildren) || diffAttrs(children,newChildren)) {
    replaceNode(children, newChildren)
    return 
  }
  // 子节点都是字符串 文本不同直接替换节点
  if (isString(children)&&isString(newChildren)) {
    if (diffText(children, newChildren)) {
      replaceNode(children, newChildren)
    }
    return
  }
  // 一个是数组 一个字符串 文本  子节点不同替换节点
  if (!isString(children) && isString(newChildren)) {
    replaceNode(children, newChildren)
    return
  }
  // 一个字符串 文本 一个是数组 子节点不同替换节点
  if (isString(children) && !isString(newChildren)) {
    replaceNode(children, newChildren)
    return
  }
  // 子节点都是 数组
  children.forEach(function(childVnode, index) {
    var newChildVnode = newChildren[index]
    // 深层次对比,递归
    updateChildren(childVnode, newChildVnode)
  })
}

function isString(value) {
  return typeof value === 'string'
}

function diffText (text, newtext) {
  return text !== newtext
}
function diffTag (children, newChildren) {
  return children.tagName !== newChildren.tagName
}
function diffAttrs(oldNode, newNode) {
  for (let attr in oldNode.attrs) {
    if (oldNode.attrs[attr] != newNode.attrs[attr]) {
      return true
    }
  }
  for (let attr in newNode.attrs) {
    if (!(oldNode.attrs.hasOwnProperty(attr))) {
      return true
    }
  }
  return false;
}


function replaceNode (vnode, newVnode) {
  var elem = vnode.elem  // 真实的 DOM 节点
  var parent = elem.parentNode
  var newElem = createElement(newVnode)
  parent.replaceChild(elem,newElem)
}复制代码

简单梳理了下思路,dom diff 比较复杂,上面函数是理想情况下简单实现。

 4.小结

文中例子比较粗糙,理解不准确之处,还请教正。文章简单的描述了virtual dom 是什么,解决了什么问题,以及简单实现的小思路。撒花~~

参考资料:

JavaScript框架中的变化及其检测

浏览器的工作原理

Vue.js 技术揭秘

snabbdom