目前前端的三个主流的框架都使用virtual-dom
来处理dom
的渲染,每个框架都会在virtual-dom
的核心原理上进行了一些特色的扩展,这篇文章主要是通过github.com/Matt-Esch/v…源码来分析最基础核心的原理。
virtual-dom
简介
virtual-dom
也就是使用js
的数据结构来表示dom
元素的结构,因为不是真是的dom
节点,也就称为虚拟DOM。它最大的特点是将页面进行抽象成JS
对象形式,配合不同的工具使跨平台成为可能,可以根据不同的平台渲染出相应的真实“DOM
”。除此之外,在页面进行更新的时候,可以将DOM
元素的变动放在内存比较,再结合一些框架的机制,将多次的渲染合并成一次渲染更新。
开始
先看看virtual-dom
的库是怎么使用的,平时接触的都是框架或者webpack
转换之前的代码,比如jsx
和vue
的模板等。以下将virtual-dom
简称为VD
。
var h = require('virtual-dom/h');
var diff = require('virtual-dom/diff');
var patch = require('virtual-dom/patch');
var createElement = require('virtual-dom/create-element');
// 1: Create a function that declares what the DOM should look like
function render(count) {
return h('div', {
style: {
textAlign: 'center',
lineHeight: (100 + count) + 'px',
border: '1px solid red',
width: (100 + count) + 'px',
height: (100 + count) + 'px'
}
}, [String(count)]);
}
// 2: Initialise the document
var count = 0; // We need some app data. Here we just store a count.
var tree = render(count); // We need an initial tree
var rootNode = createElement(tree); // Create an initial root DOM node ...
document.body.appendChild(rootNode); // ... and it should be in the document
// 3: Wire up the update logic
setInterval(function () {
count++;
var newTree = render(count);
var patches = diff(tree, newTree);
rootNode = patch(rootNode, patches);
tree = newTree;
}, 1000);
以上代码为我上面说的那个github上库的代码的例子。其实从这上面我们就可以看到VD
主要分为四大部分或者说是功能:
h
(可以叫其他名字,比如 React.creatElement) 函数,用来创建VD
对象,比如jsx
书写的代码就会被转换成React.creatElement(tag, props, ...children)
这样的。diff
函数,用来比较两个VD
对象的具体不同,形成一个描述(描述两个VD的不同点)对象。patch
函数,用来通过对比后产生的描述对象对前一个DOM
树进行更新或者成为打补丁(个人理解)。createElement
函数,根据VD
对象产生真实的DOM
树。
之后内容就会根据这四个部分进行梳理,可能顺序不同,会先梳理h
函数和createElement
函数这两个比较简单 :joy: 。该篇就是讲这两点。
创建 VD 对象
看一下h
函数的实现
function h(tagName, properties, children) {
var childNodes = [];
var tag, props, key, namespace;
// 如果第三参数为空,就先看看第二个参数是否为子节点,
// 也就是可以这样写 h('div', ['children']) ,如果没有props就可以不用穿,
// 可以不需要这样 h('div', null, ['children'])
if (!children && isChildren(properties)) {
children = properties;
props = {};
}
props = props || properties || {}; // props 赋值
// 做一下检查和解析,比如,如果tagName参数为空,则返回 div 作为兜底
tag = parseTag(tagName, props);
...
if (children !== undefined && children !== null) {
// 为子元素做一些判断,如果是字符串就转成 VText 对象,数值就先 String 化再转,这些
// 最终目的是将传入的 children 进行标准话,做一下边界处理,
// 使得符合 VNode 构造需要的几个参数
addChild(children, childNodes, tag, props);
}
...
return new VNode(tag, props, childNodes, key, namespace);
}
简单的先看一下函数的结构。h
接受三个参数,如最开始的代码中:
h('div', {
style: {
textAlign: 'center',
lineHeight: (100 + count) + 'px',
border: '1px solid red',
width: (100 + count) + 'px',
height: (100 + count) + 'px'
}
}, [String(count)]);
这里就是对于的个参数,其实转换成DOM
就是这样的(jsx的伪代码):
<div style={{...}}>count</div>
所以h
函数的三个参数是比较好理解的。分别是 标签名称、属性和子节点。
该函数最终返回了一个 VNode
对象,除了传入重要的标签名、属性和子节点之外,还传入了两个其他属性,其实在React
和Vue
中都会自己对VNode
这个对象进行扩展,所以这里也是该库的优化可扩展,暂时先不看,主要是梳理过程。
VNode
通过h
函数可以new
一个VNode
对象,那么这个VNode
对象,本质上也就是一个描述Dom
的字面量对象。
function VirtualNode(tagName, properties, children, key, namespace) {
this.tagName = tagName
this.properties = properties || noProperties
this.children = children || noChildren
this.key = key != null ? String(key) : undefined
this.namespace = (typeof namespace === "string") ? namespace : null
...
this.count = count + descendants
this.hasWidgets = hasWidgets
this.hasThunks = hasThunks
this.hooks = hooks
this.descendantHooks = descendantHooks
}
省略的代码主要是来计算和根据传入参数产生一些附加信息,这些我觉得属性扩展吧,所以先不说吧,还是关注流程。通过h
函数,我们就可以得到这样的一个字面对象。h
函数嵌套使用就可以得到一颗这样的字面对象的“树”。
h('div', {}, [
h('p', {}, ['demo']),
....
])
根据 VNode 子面量对象,创建真实 DOM
当我们使用 h
函数得到整颗VD
树之后,我们就需要通过createElement
函数创建真实的DOM
,最后插入到某个节点里面,页面就这样生成了(页面更新的过程,旧 VD Tree 和 新 VD Tree 还需要进行比较)。
function createElement(vnode, opts) {
var doc = opts ? opts.document || document : document
var warn = opts ? opts.warn : null
vnode = handleThunk(vnode).a
if (isWidget(vnode)) {
// 不是很清楚 Widget
// 我好像没有试过用,大家要是知道,可以指点指点,谢谢了。
// 应该是用来初始化一些可以复用的小部件
return vnode.init()
} else if (isVText(vnode)) {
// 如果是文本,就创建一个简单的文本节点
return doc.createTextNode(vnode.text)
} else if (!isVNode(vnode)) {
// 如果都不是VD就警告
if (warn) {
warn("Item is not a valid virtual dom node", vnode)
}
return null
}
...
var props = vnode.properties
// 这个方法就是给节点赋值属性,比如: node.setAttribute('key', props[key])
applyProperties(node, props)
var children = vnode.children
// 递归创建子节点。
for (var i = 0; i < children.length; i++) {
var childNode = createElement(children[i], opts)
if (childNode) {
node.appendChild(childNode)
}
}
return node
}
代码上加了一些注释,其实createElement
是相对简单的,核心就是判断 VD 是个什么类型的节点,创建相应的 DOM ,然后再低柜创建子节点。
创建好 DOM 后就可以如官方给出的示例,将这份 DOM 树插入到指点的页面元素下面了。VD 到真实的 DOM就是这样一个过程。内容比较基础,梳理为主。
小结
通过上面的梳理,可以知道我们平常写的jsx
就是先通过Babel
这样的工具,先将jsx
转出h('div', {}, [...])
这样的函数,然后执行得到 VD Tree
,再通过createElement
创建对象的DOM
节点,然后插入到页面某个节点下面。
下一章就来梳理一下。diff 算法的流程和基础原理。