上篇文章里大概的讲了Preact的渲染机制:node
这里的render函数,其实没有咱们写的那么简单.熟悉react渲染机制的人,都会知道,react
在state/props发生改变的时候,从新渲染全部的节点,构造出新的虚拟Dom tree跟原来的Dom tree用Diff算法进行比较,获得须要更新的地方在批量造做在真实的Dom上,因为这样作就减小了对Dom的频繁操做,从而提高的性能。
而实际的过程当中,第一次的渲染,会直接调用diff函数。git
// @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) }
解析:github
这里 vnode 是虚拟 DOM, parent 是容器的 DOM。web
merge 可选,是另一个已经存在的 dom 树,若是指定 merge 则会将虚拟 DOM 生成的 DOM 树替换到 merge 上。若是不指定的话,将会把生成的 DOM 树添加到 parent 里面。算法
而 React 的第三个参数是一个回调函数,在渲染时触发。app
咱们先从简单的渲染一个dom元素提及:render(<div id="foo"> hello !</div>, document.body, null);
dom
下面是乞丐版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节点以后,添加到父节点上。性能
/** 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
/** 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); } } }
/** 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节点。
可是还有遗留的问题没有解决:
一、渲染react组件的过程
二、目前还没有考虑diff节点间的对比逻辑
下篇文章会讲这两块
参考文档: