编辑推荐: |
本文重点介绍了preact是什么?虚拟DOM、组件、diff算法、异步setState等相关内容。
本文来自于思否,由火龙果软件Anna编辑推荐。 |
|
前言
preact虽然是react的最小实现, 很多react的特性preact里一点都没有少, 比如contextAPI,
Fragment等。我们分析时更注重实现过程,会对一些API的实现进行忽略。请见谅
preact是什么?
Fast 3kB React alternative with the
same modern API. Components & Virtual DOM
preact可以说是类react框架的最小实现
虚拟DOM
关于jsx
我们首先看下preact官网上的demo。
import { h, render
} from 'preact';
render((
<h1 id="title" >Hello, world!</h1>
), document.body); |
其实上面的jsx代码,本质是下面代码的语法糖
h(
'h1',
{ id: 'title' },
'Hello, world!'
) |
preact是如何做到的呢?preact本身并没有实现这个语法转换的功能,preact是依赖transform-react-jsx的babel插件做到的。
createElement
前面我们看到了jsx的代码会被转换为用h函数包裹的代码, 我们接下来看下h函数是如何实现的。createElement函数位于create-element.js这个文件中。
文件中主要为3个函数, createElement和createVNode, 以及coerceToVNode。
createElement和createVNode是一对的, createElement会将children挂载到VNode的props中。既props.children的数组中。createVNode则会将根据这些参数返回一个对象,
这个对象就是虚拟DOM。
在createElement中我们还可以看到对defaultProps的处理, 而defaultProps可以为我们设置props的默认的初始值。
export function createElement (type, props, children)
{
if (props==null) props = {};
if (arguments.length>3) {
children = [children];
for (let i=3; i<arguments.length; i++) {
children.push(arguments[i]);
}
}
if (children!=null) {
props.children = children;
}
if (type!=null && type.defaultProps!=null)
{
for (let i in type.defaultProps) {
if (props[i]===undefined) props[i] = type.defaultProps[i];
}
}
let ref = props.ref;
if (ref) delete props.ref;
let key = props.key;
if (key) delete props.key; return createVNode (type, props, null, key,
ref);
} export function createVNode (type, props, text,
key, ref) { const vnode = {
type,
props,
text,
key,
ref,
_children: null,
_dom: null,
_lastDomChild: null,
_component: null
}; return vnode;
}
|
而coerceToVNode函数的作用则是将一些没有type类型的节点。比如一段字符串,
一个数字强制转换为VNode节点, 这些节点的type值为null, text属性中保留了字符串和数字的值。
export function
coerceToVNode (possibleVNode) {
if (possibleVNode == null || typeof possibleVNode
=== 'boolean') return null;
if (typeof possibleVNode === 'string' || typeof
possibleVNode === 'number') {
return createVNode (null, null, possibleVNode,
null, null);
} if (Array.isArray(possibleVNode)) {
return createElement(Fragment, null, possibleVNode);
} if (possibleVNode._dom!=null) {
return createVNode (possibleVNode.type, possibleVNode.props,
possibleVNode.text, possibleVNode.key, null);
} return possibleVNode;
}
|
到这里create-element的这个模块我们就介绍完了。这是一个非常简单的模块, 做的功能就是根据对应的jsx->虚拟DOM。我们这里还没有涉及如何渲染出真正的DOM节点,
这是因为preact中渲染的过程是直接在diff算法中实现,一边比对一边跟更新真实的dom。
组件
preact中有一个通用Component类, 组件的实现需要继承这个通用的Component类。我们来看下preact中Component类是如何实现的。它位于component.js文件??中。
我们首先看下Component类的构造函数,非常的简单。只有两个属性props, context。因为通用的Component类实现了props属性,所以我们的组件类在继承Component类后,需要显式的使用super作为函数调用,并将props传入。
export function Component(props,
context) {
this.props = props
this.context = context
}
|
Component类中实现了setState方法, forceUpdate方法,render方法,以及其他的一些辅助函数。forceUpdate涉及到了setState的异步更新,
我们将在setState一节中专门介绍。这里暂不做介绍。我们接下来看看setState的实现。
Component.prototype.setState
= function(update, callback) {
let s = (this._nextState!==this.state &&
this._nextState) || (this._nextState = assign({},
this.state));
if (typeof update!=='function' || (update
= update(s, this.props))) {
assign(s, update);
} if (update==null) return; if (this._vnode) {
if (callback) this._renderCallbacks.push(callback);
enqueueRender (this);
}
}; // src/util.js
export function assign(obj, props) {
for (let i in props) obj[i] = props[i];
return obj;
} |
在preact的setState方法, 同react一样支持函数或者Object两种方式更新state,
并且支持setState的回调。我们这里看到了两个个私有属性_nextState, _renderCallbacks。_renderCallbacks则是存储了setState回调的队列。
_nextState里存储了最新的state, 为什么我们不去直接更新state呢?因为我们要实现生命周期,
比如getDerivedStateFromProps生命周期中组件的state并没有更新呢。我们需要使用_nextState存储最新的state??。enqueueRender函数涉及到了state的异步更新,
我们在本节先不介绍。
// src/component.js
export function Fragment() { }
Component.prototype.render = Fragment; |
基类的render方法本身是一个空函数, 需要继承的子类自己具体实现。
component.js的模块的部分内容,我们已经介绍完成了, 同样不是很复杂。component.js的模块的其他的内容因为涉及了setState异步更新队列,所以我们将在setState一节中。回过头来介绍它。
diff算法
ps:我们只需要比较同级的节点(相同颜色框内的), 如果两个节点type不一致,
我们会销毁当前的节点。不进行比较子节点的操作。
在preact中diff算法以及真实dom的更新和渲染是杂糅在一起的。所以本节内容会比较多。
preact会存储上一次的渲染的VNode(存储在_prevVNode的私有属性上)。而本次渲染过程中我们会比较本次的VNode上前一次的_prevVNode。判断是否需要生成新的Dom,
卸载Dom的操作, 更新真实dom的操作(我们将VNode对应的真实的dom存储在VNode的私有属性_dom,
可以实现在diff的过程中更新dom的操作)。
render
对比文本节点
我们首先回忆一下文本节点的VNode的结构是怎么样的
// 文本节点VNode
{
type: null,
props: null,
text: '你的文本'
_dom: TextNode
} |
我们首先进入diff方法。diff方法中会对VNode类型进行判断,
如果不是function类型(组件类型), 和Fragment类型。我们的会调用diffElementNodes函数。
// src/diff/index.js
// func diff
// 参数很多, 我们来说下几个参数的具体含义
// dom为VNode对应的真实的Dom节点
// newVNode新的VNode
// oldVNode旧的VNode
// mounts存储挂载组件的列表
dom = diffElementNodes(dom, newVNode, oldVNode,
context, isSvg, excessDomChildren, mounts, ancestorComponent) |
如果此时dom还没有创建。初次渲染, 那么我们根据VNode类型创建对应的真实dom节点。文本类型会使用createTextNode创建文本节点。
接下来我们会标签之前VNode的text的内容, 如果新旧不相等。我们将新VNode的text属性,赋值给dom节点。完成对dom的更新操作。
// src/diff/index.js
// func diffElementNodes
if (dom==null) {
dom = newVNode.type===null ? document.createTextNode(newVNode.text)
: isSvg ? document.createElementNS('http://www.w3.org/2000/svg',
newVNode.type) : document.createElement(newVNode.type); excessDomChildren = null;
} newVNode._dom = dom; if (newVNode.type===null) {
if ((d===null || dom===d) && newVNode.text!==oldVNode.text)
{
dom.data = newVNode.text;
}
} |
对比非文本DOM节点
非文本DOM节点指的是那些type为div, span, h1的VNode节点。这些类型的节点在diff方法中,
我们依旧会调用diffElementNodes函数去处理。
// src/diff/index.js
// func diff
dom = diffElementNodes(dom, newVNode, oldVNode,
context, isSvg, excessDomChildren, mounts, ancestorComponent) |
进入diffElementNodes方法后, 如果是初次渲染我们会使用createElement创建真实的dom节点挂载到VNode的_dom属性上。
接下来我们会比较新旧VNode的属性props。但是之前会调用diffChildren方法,
对当前的VNode子节点进行比较。我们这里先不进入diffChildren函数中。我们只需要知道我们在更新当前节点属性的时候,
我们已经通过递归形式, 完成了对当前节点的子节点的更新操作。接下来我们进入diffProps函数中。
// src/diff/index.js
// func diffElementNodes
if (dom==null) {
dom = newVNode.type===null ? document.createTextNode
(newVNode.text) : isSvg ? document.createElementNS
('http://www.w3.org/2000/svg', newVNode.type)
: document.createElement (newVNode.type);
} newVNode._dom = dom; if (newVNode !== oldVNode) {
let oldProps = oldVNode.props;
let newProps = newVNode.props; if (oldProps == null) {
oldProps = {};
}
diffChildren(dom, newVNode, oldVNode, context,
newVNode.type === 'foreignObject' ? false :
isSvg, excessDomChildren, mounts, ancestorComponent);
diffProps (dom, newProps, oldProps, isSvg);
}
|
在diffProps函数中我们会做两件事。设置, 更新属性。删除新的props中不存在的属性。setProperty在preact中的具体实现,
我们往下看。
// src/diff/props.js
export function diffProps (dom, newProps, oldProps,
isSvg) {
// 设置或更新属性值
for (let i in newProps) {
if (i!=='children' && i!=='key' &&
(!oldProps || ((i==='value' || i==='checked')
? dom : oldProps) [i]!==newProps[i])) {
setProperty(dom, i, newProps[i], oldProps[i],
isSvg);
}
}
// 删除属性
for (let i in oldProps) {
if (i!=='children' && i!=='key' &&
(!newProps || !(i in newProps))) {
setProperty (dom, i, null, oldProps[i], isSvg);
}
}
} |
在setProperty方法中, 如果value(新的属性值)为null, 我们会删除对应的属性。如果不为null,
我们将会更新或者设置新的属性。同时还会对事件进行处理, 例如onClick属性, 我们会使用addEventListener添加原生的click事件。
// src/diff/props.js
function setProperty (dom, name, value, oldValue,
isSvg) {
let v;
// 对class处理
if (name==='class' || name==='className') name
= isSvg ? 'class' : 'className'; // 对style处理, style传入Object或者字符串都会得到兼容的处理
if (name==='style') { let s = dom.style; // 如果style是string类型
if (typeof value==='string') {
s.cssText = value;
}
else {
// 如果style是object类型
if (typeof oldValue==='string') s.cssText =
'';
else {
for (let i in oldValue) {
if (value==null || !(i in value)) s.setProperty (i.replace(CAMEL_REG,
'-'), '');
}
}
for (let i in value) {
v = value[i];
if (oldValue==null || v!==oldValue[i]) {
s.setProperty(i.replace(CAMEL_REG, '-'), typeof
v==='number' && IS_NON_DIMENSIONAL.test(i)===false
? (v + 'px') : v);
}
}
}
}
else if (name==='dangerouslySetInnerHTML') {
return;
}
else if (name[0]==='o' & & name[1]==='n')
{
// 对事件处理
let useCapture = name !== (name=name.replace(/Capture$/,
''));
let nameLower = name.toLowerCase();
name = (nameLower in dom ? nameLower : name).substring(2); if (value) {
if (!oldValue) dom.addEventListener (name, eventProxy,
useCapture);
}
else {
dom.removeEventListener (name, eventProxy, useCapture);
}
(dom._listeners || (dom._listeners = {}))[name]
= value;
}
else if (name!=='list' && name!=='tagName'
&& !isSvg && (name in dom))
{
dom[name] = value==null ? '' : value;
}
else if (value==null || value===false) {
// 删除以及为null的属性
if (name!== (name = name.replace(/^xlink:?/,
''))) dom.removeAttributeNS ('http://www.w3.org/1999/xlink',
name.toLowerCase());
else dom.removeAttribute(name);
}
else if (typeof value!=='function') {
// 更新或设置新的属性
if (name!==(name = name.replace(/^xlink:?/,
''))) dom.setAttributeNS ('http://www.w3.org/1999/xlink',
name.toLowerCase(), value);
else dom.setAttribute (name, value);
}
} |
对比组件
如果VNode是组件类型。在diff函数中, 会在不同的时刻执行组件的生命周期。在diff中, 执行组件实例的render函数。我们将会拿到组件返回的VNode,
然后再将VNode再一次带入diff方法中进行diff比较。大致的流程可以如上图所示。
// src/diff/index.js
// func diff
let c, p, isNew = false, oldProps, oldState,
snapshot,
newType = newVNode.type;
let cxType = newType.contextType;
let provider = cxType && context[cxType._id];
let cctx = cxType != null ? (provider ? provider.props.value
: cxType._defaultValue) : context; if (oldVNode._component) {
c = newVNode._component = oldVNode._component;
clearProcessingException = c._processingException;
}
else {
isNew = true; // 创建组件的实例
if (newType.prototype && newType.prototype.render)
{
newVNode._component = c = new newType(newVNode.props,
cctx);
}
else {
newVNode._component = c = new Component(newVNode.props,
cctx);
c.constructor = newType;
c.render = doRender;
}
c._ancestorComponent = ancestorComponent;
if (provider) provider.sub(c); // 初始化,组件的state, props的属性
c.props = newVNode.props;
if (!c.state) c.state = {};
c.context = cctx;
c._context = context;
c._dirty = true;
c._renderCallbacks = [];
} // 组件的实例上挂载组件所对应的VNode节点
c._vnode = newVNode; let s = c._nextState || c.state; // 执行getDerivedStateFromProps生命周期函数, 返回只会更新组件的state
if (newType.getDerivedStateFromProps != null)
{
oldState = assign({}, c.state);
if (s === c.state) s = c._nextState = assign({},
s);
assign(s, newType.getDerivedStateFromProps(newVNode.props,
s));
} if (isNew) {
// 执行componentWillMount生命周期
if (newType.getDerivedStateFromProps == null
&& c.componentWillMount != null) c.componentWillMount();
// 将需要执行componentDidMount生命周期的组件, push到mounts队列中
if (c.componentDidMount != null) mounts.push(c);
}
else {
// 执行componentWillReceiveProps生命周期
if (newType.getDerivedStateFromProps == null
&& force == null && c.componentWillReceiveProps
!= null) {
c.componentWillReceiveProps(newVNode.props,
cctx);
s = c._nextState || c.state;
} // 执行shouldComponentUpdate生命周期, 并将_dirty设置为false,
当_dirty被设置为false时, 执行的更新操作将会被暂停
if (!force && c.shouldComponentUpdate
!= null && c.shouldComponentUpdate (newVNode.props,
s, cctx) === false) {
c.props = newVNode.props;
c.state = s;
c._dirty = false;
// break后,不在执行以下的代码
break outer;
} // 执行componentWillUpdate生命周期
if (c.componentWillUpdate != null) {
c.componentWillUpdate (newVNode.props, s, cctx);
}
} oldProps = c.props;
if (!oldState) oldState = c.state; c.context = cctx;
c.props = newVNode.props;
// 将更新后的state的s,赋予组件的state
c.state = s; // prev为上一次渲染时对应的VNode节点
let prev = c._prevVNode;
// 调用组件的render方法获取组件的VNode
let vnode = c._prevVNode = coerceToVNode (c.render(c.props,
c.state, c.context));
c._dirty = false; if (c.getChildContext != null) {
context = assign (assign({}, context), c.getChildContext());
} // 执行getSnapshotBeforeUpdate生命周期
if (!isNew && c.getSnapshotBeforeUpdate
!= null) {
snapshot = c.getSnapshotBeforeUpdate(oldProps,
oldState);
} // 更新组件所对应的VNode,返回对应的dom
c.base = dom = diff(dom, parentDom, vnode, prev,
context, isSvg, excessDomChildren, mounts, c,
null); if (vnode != null) {
newVNode._lastDomChild = vnode._lastDomChild;
} c._parentDom = parentDom; |
在diff函数的顶部有这样一段代码上面有一句英文注释(If the
previous type doesn't match the new type we drop the
whole subtree), 如果oldVNode和newVNode类型不同,我们将会卸载整个子树。
if (oldVNode==null || newVNode==null || oldVNode.type!==newVNode.type)
{
// 如果newVNode为null, 我们将会卸载整个组件, 并删除对应的dom节点
if (oldVNode!=null) unmount(oldVNode, ancestorComponent);
if (newVNode==null) return null;
dom = null;
oldVNode = EMPTY_OBJ;
} |
对比子节点
export function diffChildren (parentDom, newParentVNode,
oldParentVNode, context, isSvg, excessDomChildren,
mounts, ancestorComponent) {
let childVNode, i, j, p, index, oldVNode, newDom,
nextDom, sibDom, focus,
childDom; let newChildren = newParentVNode._children
|| toChildArray (newParentVNode.props.children,
newParentVNode._children=[], coerceToVNode);
let oldChildren = oldParentVNode !=null &&
oldParentVNode!= EMPTY_OBJ && oldParentVNode._children
|| EMPTY_ARR; let oldChildrenLength = oldChildren.length; childDom = oldChildrenLength ? oldChildren[0]
&& oldChildren[0]._dom : null;
for (i=0; i<newChildren.length; i++) {
childVNode = newChildren[i] = coerceToVNode(newChildren[i]);
oldVNode = index = null; p = oldChildren[i];
//
if (p != null && (childVNode.key==null
&& p.key==null ? (childVNode.type ===
p.type) : (childVNode.key === p.key))) {
index = i;
}
else {
for (j=0; j<oldChildrenLength; j++) {
p = oldChildren[j];
if (p!=null) {
if (childVNode.key==null && p.key==null
? (childVNode.type === p.type) : (childVNode.key
=== p.key)) {
index = j;
break;
}
}
}
} if (index!=null) {
oldVNode = oldChildren[index];
oldChildren[index] = null;
} nextDom = childDom!=null && childDom.nextSibling;
newDom = diff (oldVNode==null ? null : oldVNode._dom,
parentDom, childVNode, oldVNode, context, isSvg,
excessDomChildren, mounts, ancestorComponent,
null); if (childVNode!=null && newDom !=null)
{
focus = document.activeElement; if (childVNode._lastDomChild != null) {
newDom = childVNode._lastDomChild;
}
else if (excessDomChildren==oldVNode || newDom!=childDom
|| newDom.parentNode==null) { outer: if (childDom==null || childDom.parentNode !==parentDom)
{
parentDom.appendChild(newDom);
}
else {
sibDom = childDom;
j = 0;
while ((sibDom=sibDom.nextSibling) &&
j++<oldChildrenLength/2) {
if (sibDom===newDom) {
break outer;
}
}
parentDom.insertBefore (newDom, childDom);
}
} if (focus!== document.activeElement) {
focus.focus();
} childDom = newDom! =null ? newDom.nextSibling
: nextDom;
}
}
for (i=oldChildrenLength; i--; ) {
if (oldChildren[i]!=null) {
unmount(oldChildren[i], ancestorComponent);
}
}
} |
diffChildren是最为复杂的一部分内容。子VNode作为一个数组, 数组中的内容可能改变了顺序或者数目,
很难确定新的VNode要和那一个旧的VNode比较。所以preact中当面对列表时,我们将要求用户提供key,
帮助我们比较VNode。达到复用Dom的目的。
在diffChildren中,我们会首先通过toChildArray函数将子节点以数组的形式存储在_children属性上。
childDom为第一个子节点真实的dom(这很有用, 我们在后面将通过它来判断是使用appendChild插入newDom还是使用insertBefore插入newDom,或者什么都不做)
接下来遍历_children属性。如果VNode有key属性, 则找到key与key相等的旧的VNode。如果没有key,
则找到最近的type相等的旧的VNode。然后将oldChildren对应的位置设置null, 避免重复的查找。使用diff算法对比,
新旧VNode。返回新的dom。
如果childDom为null, 则将新dom, append的到父DOM中。如果找到了与新的dom相等的dom(引用类型),
我们则不做任何处理(props已经在diffElementNode中更新了)。如果在childDom的nextSibling没有找到和新的dom相等的dom,
我们将dom插入childDom的前面。接着更新childom。
遍历剩余没有使用到oldChildren, 卸载这些节点或者组件。
异步setState
preact除了使用diff算法减少dom操作优化性能外, preact会将一段时间内的多次setState合并减少组件渲染的次数。
我们首先在setState中, 并没有直接更新state, 或者直接重新渲染函数函数。而是将组件的实例带入到了enqueueRender函数中。
Component.prototype.setState = function(update,
callback) {
let s = (this._nextState!== this.state &&
this._nextState) || (this._nextState = assign({},
this.state)); if (typeof update!== 'function' || (update
= update(s, this.props))) {
assign(s, update);
} if (update==null) return; if (this._vnode) {
if (callback) this._renderCallbacks.push (callback);
enqueueRender(this);
}
};
|
在enqueueRender函数中, 我们将组件push到队列q中。
同时使用_dirty控制, 避免q队列中被push了相同的组件。我们应该在多长时间内清空q队列呢?
我们该如何定义这么一段时间呢?比较好的做法是使用Promise.resolve()。在这一段时间的setState操作都会被push到q队列中。_nextState将会被合并在清空队列的时候,一并更新到state上,避免了重复的渲染。
let q = []; export function enqueueRender(c) {
if (!c._dirty && (c._dirty = true) &&
q.push(c) === 1) {
(options.debounceRendering || defer)(process);
}
} function process() {
let p;
while ((p=q.pop())) {
if (p._dirty) p.forceUpdate(false);
}
} const defer = typeof Promise=='function' ?
Promise.prototype.then.bind(Promise.resolve())
: setTimeout;
|
在宏任务完成后,我们执行微任务Promise.resolve(),
清空q队列,使用diff方法更新队列中的组件。
Component.prototype.forceUpdate
= function(callback) {
let vnode = this._vnode, dom = this._vnode._dom,
parentDom = this._parentDom;
if (parentDom) {
const force = callback!==false; let mounts = [];
dom = diff (dom, parentDom, vnode, vnode, this._context,
parentDom.ownerSVGElement!==undefined, null,
mounts, this._ancestorComponent, force);
if (dom!=null && dom.parentNode!==parentDom)
{
parentDom.appendChild(dom);
}
commitRoot(mounts, vnode);
}
if (callback) callback();
};
|
结语
到这里我们已经吧preact的源码大致浏览了一遍。我们接下来可以参考preact的源码,实现自己的react。话说我还给preact的项目提交了pr,不过还没有merge。
|