前言
上篇文章里大概的讲了Preact的渲染机制:
- Babel解析JSX
- h函数将解析后JSX节点转成虚拟DOM
- render函数把它转成真实的节点
这里的render函数,其实没有我们写的那么简单.熟悉react渲染机制的人,都会知道,
在state/props发生改变的时候,重新渲染所有的节点,构造出新的虚拟Dom tree跟原来的Dom tree用Diff算法进行比较,得到需要更新的地方在批量造作在真实的Dom上,由于这样做就减少了对Dom的频繁操作,从而提升的性能。
diff函数
而实际的过程中,第一次的渲染,会直接调用diff函数。
render.js
// @example
// render a div into <body>:
// render(<div id="hello">hello!</div>, document.body);
export function render(vnode, parent, merge) {
return diff(merge, vnode, {}, false, parent, false)
}
解析:
这里 vnode 是虚拟 DOM, parent 是容器的 DOM。
merge 可选,是另外一个已经存在的 dom 树,如果指定 merge 则会将虚拟 DOM 生成的 DOM 树替换到 merge 上。如果不指定的话,将会把生成的 DOM 树添加到 parent 里面。
而 React 的第三个参数是一个回调函数,在渲染时触发。
我们先从简单的渲染一个dom元素说起:render(<div id="foo"> hello !</div>, document.body, null);
下面是乞丐版diff.js
diff.js
/** Apply differences in a given vnode (and it's deep children) to a real DOM Node.
* @param {Element} [dom=null]
* 指当前的vnode所对应的之前未更新的真实dom。
* 有两种情况,第一就是render的第三个参数,若为空,则是null,空的这种情况表面是首次渲染,
现在考虑首次渲染的情况
针对本例,dom = undefined
* 第二种就是vnode的对应的未更新的真实DOM,即表示渲染刷新界面。
* @param {VNode} vnode 主要是需要渲染的虚拟dom节点
* 对于本例来说
vnode = {
attributes: {id: "foo"}
children:[" hello !"]
key:undefined
nodeName:"div"
}
* @param {Element} context 用于全局的属性,跟React类似
* @returns {Element} dom The created/mutated element
* @private
*/
export function diff(dom, vnode, context, mountAll, parent, componentRoot) {
// idiff函数就是diff算法的内部实现,idiff会返回虚拟dom对应创建的真实dom节点
let ret = idiff(dom, vnode, context, mountAll, componentRoot);
// append the element if its a new parent
// 如果父节点之前没有创建这个子节点,则添加到父节点上,而不是替换
if (parent && ret.parentNode !== parent) parent.appendChild(ret)
// 最终返回真实dom节点
return ret;
}
上面的函数比较简单,就是通过idiff的到真实dom节点之后,添加到父节点上。
idiff.js
/** Internals of `diff()`, separated to allow bypassing diffLevel / mount flushing. */
/**
* 1. 首先判断vnode是否为空值,如果是将vnode设定为空字符串
* 2. 再次判断vnode是否为字符串或者数字,内部会判断是否为文本节点,最后进行更新或者替换工作
* 3. 如果vnode.nodeName是一个component则进行组件的渲染,由于这里我们的vnode是一个对象,所以走这一条逻辑
*/
function idiff(dom, vnode, context, mountAll, componentRoot) {
let out = dom,
let vnodeName = vnode.nodeName,
// empty values (null, undefined, booleans) render as empty Text nodes
// 将null, undefined, boolean转换为空字符
if (vnode == null || typeof vnode === 'boolean') vnode = ''
// Fast case: Strings & Numbers create/update Text nodes.
// 将字符串和数字转化为文本节点
if (typeof vnode === 'string' || typeof vnode === 'number') {
// 直接创建文本节点
out = document.createTextNode(vnode)
}
if (!dom || !isNamedNode(dom, vnodeName)) {
// createNode通过document.createElement(nodeName);返回一个真实的dom节点
out = createNode(vnodeName, isSvgMode)
}
// 由于out为新建的dom元素,fc = null
let fc = out.firstChild,
// 注意这里:此时out戴上了'__preactattr_'属性,说明它是由preact创建的
props = out[ATTR_KEY],
// vchildren = ['hello !']
vchildren = vnode.children;
// 对child进行比对,递归更新所有child节点
innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML!=null);
// Apply attributes/props from VNode to the DOM Element:
// 在VNode和DOM之间比较props和atrributes
diffAttributes(out, vnode.attributes, props)
return out;
}
idiff目前只处理三种类型的vnode:空值,字符串,数字,原生dom节点,也就是还不包含组件类型的。
可以看到:凡是由preact创建的标签,都带有'__preactattr_'属性,创建完毕之后,还需要
- 对子节点进行创建和更新
- 更新props
innerDiffNode.js
/** Apply child and attribute changes between a VNode and a DOM Node to the DOM.
* 内部diff,比较子节点和属性的变化
* @param {Element} dom Element whose children should be compared & mutated
* @param {Array} vchildren Array of VNodes to compare to `dom.childNodes`
* @param {Object} context Implicitly descendant context object (from most recent `getChildContext()`)
* @param {Boolean} mountAll
* @param {Boolean} isHydrating If `true`, consumes externally created elements similar to hydration
*/
function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) {
let originalChildren = dom.childNodes,
children = [],
keyed = {},
keyedLen = 0,
min = 0,
len = originalChildren.length,
childrenLen = 0,
vlen = vchildren ? vchildren.length : 0,
j,
c,
f,
vchild,
child
if (vlen !== 0) {
for (let i = 0; i < vlen; i++) {
vchild = vchildren[i]
child = null
// morph the matched/found/created DOM child to match vchild (deep)
// 通过idiff得到真实的dom节点,目前由于vchild是字符串'hello',所以
// 这里返回的是一个字符串节点
child = idiff(child, vchild, context, mountAll)
// 子节点为空,直接append
dom.appendChild(child);
}
}
}
diffAttributes.js
/** Apply differences in attributes from a VNode to the given DOM Element.
* @param {Element} dom 虚拟dom对应的真实dom
* @param {Object} attrs 期望的最终键值属性对
* @param {Object} old 当前或者之前的属性
*/
function diffAttributes(dom, attrs, old) {
let name
// remove attributes no longer present on the vnode by setting them to undefined
// 移除属性因为现在的name为空了
for (name in old) {
// 如果old[name]存在,但是attrs[name]不存在
if (!(attrs && attrs[name] != null) && old[name] != null) {
setAccessor(dom, name, old[name], (old[name] = undefined), isSvgMode)
}
}
// add new & update changed attributes
// 添加或更新改变的属性
for (name in attrs) {
if (
name !== 'children' &&
name !== 'innerHTML' &&
(!(name in old) ||
attrs[name] !==
(name === 'value' || name === 'checked' ? dom[name] : old[name]))
) {
setAccessor(dom, name, old[name], (old[name] = attrs[name]), isSvgMode)
}
}
}
上面就是preact初次渲染的一个简单流程,总结来说:
通过diff.js
可以得到一个完整的dom节点A,而在这个过程中,idiff
负责创建父节点A,innerDiffNode
用于递归调用idiff,从而得到A的所有child
节点并将它添加到A里,最后再将虚拟dom的新属性更新到A节点。
但是还有遗留的问题没有解决:
1、渲染react组件的过程
2、目前尚未考虑diff节点间的对比逻辑
下篇文章会讲这两块
参考文档:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。