前两个星期花了一些时间学习preact的源码, 并写了几篇博客。可是如今回头看看写的并很差,并且源码的有些地方(diffChildren的部分)我还理解🙅错了。实在是很差意思。因此此次准备从新写一篇博客从新作下分析。css
preact虽然是react的最小实现, 不少react的特性preact里一点都没有少, 好比contextAPI, Fragment等。咱们分析时更注重实现过程,会对一些API的实现进行忽略。请见谅node
⚛️ Fast 3kB React alternative with the same modern API. Components & Virtual DOMreact
preact能够说是类react框架的最小实现算法
咱们首先看下preact官网上的demo。数组
import { h, render } from 'preact'; render(( <h1 id="title" >Hello, world!</h1> ), document.body);
其实上面👆的jsx代码,本质是下面👇代码的语法糖babel
h( 'h1', { id: 'title' }, 'Hello, world!' )
preact是如何作到的呢?preact自己并无实现这个语法转换的功能,preact是依赖transform-react-jsx的babel插件作到的。app
前面咱们看到了jsx的代码会被转换为用h函数包裹的代码, 咱们接下来看下h函数是如何实现的。createElement函数位于create-element.js这个文件中。框架
文件中主要为3个函数, createElement和createVNode, 以及coerceToVNode。dom
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一节中。回过头来介绍它。
ps: 👆咱们只须要比较同级的节点(相同颜色框内的), 若是两个节点type不一致, 咱们会销毁当前的节点。不进行比较子节点的操做。
在preact中diff算法以及真实dom的更新和渲染是杂糅在一块儿的。因此本节内容会比较多。
preact会存储上一次的渲染的VNode(存储在_prevVNode的私有属性上)。而本次渲染过程当中咱们会比较本次的VNode上前一次的_prevVNode。判断是否须要生成新的Dom, 卸载Dom的操做, 更新真实dom的操做(咱们将VNode对应的真实的dom存储在VNode的私有属性_dom, 能够实如今diff的过程当中更新dom的操做)。
咱们首先回忆一下文本节点的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节点🈯️的是那些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, 卸载这些节点或者组件。
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😢。