共计 15084 个字符,预计需要花费 38 分钟才能阅读完成。
前言
前两个星期花了一些时间学习 preact 的源码, 并写了几篇博客。但是现在回头看看写的并不好,而且源码的有些地方 (diffChildren 的部分) 我还理解???? 错了。实在是不好意思。所以这次准备重新写一篇博客重新做下分析。
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????。