作为一名前端,我们需要深入学习react的运行机制,但是react源码量已经相当庞大,从学习的角度,性价比不高,所以学习一个react mini库是一个深入学习react的一个不错的方法。
preact是一个最小的react mini库,但由于其对尺寸的追求,它的很多代码可读性比较差,市面上也很少有全面且详细介绍的文章,本篇文章希望能帮助你学习preact的源码。
在最开始我会先介绍preact整体流程,帮助您有一个整体概念,以便不会陷入源码的细枝末节里,然后会分别讲解preact各个值得学习的机制。建议与preact源码一起阅读本文。
希望能帮你理清如下问题:
- JSX是怎么被处理的?
- diff算法是如何工作的?
- vue和react中我们为什么需要一个稳定的key?
- preact是怎么处理事件的?
- preact的回收机制是如何提高性能的?
- setState之后会发生什么?
- fiber是用来解决什么问题的?
以下图是preact源码大致流程图,现在看不懂没关系,也不需要刻意记,在学习的过程中,不妨根据此图试着猜想preact每一步都做了什么,下一步要做什么。
JSX
在react的官方文档中,我们可以得知,jsx内容在会被babel编译为以下格式:
In
const element = (
Hello, world!
);
Out
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
这样通过createElement就可以生成虚拟dom树,在preact里面对应的函数是h。
h函数根据nodeName,attributes,children,返回一个虚拟dom树,这个虚拟dom树往往有三个属性:
function h(nodeName, props, ...children){
.... // 其他代码
return {
nodeName,
props, // props中包含children
key, // 为diff算法做准备
}
}
这里不贴出preact的源代码,因为h函数的实现方式有很多,不希望最开始的学习就陷入到细枝末节,只需要明白h函数的作用即可。
diff
从上图中可以看到,preact主流程调用的第一个函数就是render,render函数很简单就是调用了一下diff函数。
function render(vnode, parent, merge) {
return diff(merge, vnode, {}, false, parent, false);
}
diff函数的主要作用是调用idiff函数,然后将idff函数返回的真实dom append到dom中
function diff(dom, vnode, context, mountAll, parent, componentRoot) {
// 返回的是一个真实的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);
}
idiff
接下来我们要介绍idff函数,开启react高性能diff算法的大门,但在这之前,我们应该了解react diff算法的前提:
- 两个不同类型的element会产生不同类型的树。
- 开发者通过一个key标识同一层级的子节点。
基于第一个前提,不同类型的节点就可以不再向下比较,直接销毁,然后重新创建即可。
idiff函数主要分为三块,分别处理vnode三种情况:
- vnode是string或者Number,类似于上面例子的'Hello World',一般是虚拟dom树的叶子节点。
- vnode中的nodeName是一个function,即vnode对应一个组件,例如上例中的
。 - vnode中nodeName是一个字符串,即vnode对应一个html元素,例如上例中的h1。
对于string或Number:
// 如果要比较的dom是一个textNode,直接更改dom的nodeValue
// 如果要比较的dom不是一个textNode,就创建textNode,然后回收老的节点树,回收的节点树会保留结构,然后保存在内存中,在// 需要的时候复用。(回收相关的处理会在之后详细说明)
if (typeof vnode === 'string' || typeof vnode === 'number') {
// update if it's already a Text node:
if (dom && dom.splitText !== undefined && dom.parentNode && (!dom._component || componentRoot)) {
/* istanbul ignore if */
/* Browser quirk that can't be covered: https://github.com/developit/preact/commit/fd4f21f5c45dfd75151bd27b4c217d8003aa5eb9 */
if (dom.nodeValue != vnode) {
dom.nodeValue = vnode;
}
} else {
// it wasn't a Text node: replace it with one and recycle the old Element
out = document.createTextNode(vnode);
if (dom) {
if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
recollectNodeTree(dom, true);
}
}
out.__preactattr_ = true;
return out;
}
如果nodeName是一个function,会直接调用buildComponentFromVNode方法
let vnodeName = vnode.nodeName;
if (typeof vnodeName === 'function') {
return buildComponentFromVNode(dom, vnode, context, mountAll);
}
如果nodeName是一个字符串,以下很长的代码,就是做三步:
- 对于类型不同的节点,直接做替换操作,不做diff比较。
- diffAttrites
- diffChildren
// Tracks entering and exiting SVG namespace when descending through the tree.
isSvgMode = vnodeName === 'svg' ? true : vnodeName === 'foreignObject' ? false : isSvgMode;
// If there's no existing element or it's the wrong type, create a new one:
vnodeName = String(vnodeName);
// 如果不存在dom对象,或者dom的nodeName和vnodeName不一样的情况下
if (!dom || !isNamedNode(dom, vnodeName)) {
out = createNode(vnodeName, isSvgMode);
if (dom) {
// 在后面你会发现preact的diffChildren的方式,是通过把真实dom的子节点与虚拟dom的子节点相比较,所以需要老的// 孩子暂时先移动到新的节点上
// move children into the replacement node
while (dom.firstChild) {
out.appendChild(dom.firstChild);
} // if the previous Element was mounted into the DOM, replace it inline
if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
// recycle the old element (skips non-Element node types)
recollectNodeTree(dom, true);
}
}
let fc = out.firstChild,
props = out.__preactattr_,
vchildren = vnode.children;
// 把dom节点的attributes都放在了dom['__preactattr_']上
if (props == null) {
props = out.__preactattr_ = {};
for (let a = out.attributes, i = a.length; i--;) {
props[a[i].name] = a[i].value;
}
}
// 如果vchildren只有一个节点,且是textnode节点时,直接更改nodeValue,优化性能
// Optimization: fast-path for elements containing a single TextNode:
if (!hydrating && vchildren && vchildren.length === 1 && typeof vchildren[0] === 'string' && fc != null && fc.splitText !== undefined && fc.nextSibling == null) {
if (fc.nodeValue != vchildren[0]) {
fc.nodeValue = vchildren[0];
}
}
// 比较子节点,将真实dom的children与vhildren比较
// otherwise, if there are existing or new children, diff them:
else if (vchildren && vchildren.length || fc != null) {
innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML != null);
}
diffAttributes(out, vnode.attributes, props);
return out;