2018-04-17 21:04:30 flytam 阅读数 573

一个简单的Preact代码如下

// 一个简单的Preact demo
import { h, render, Component } from 'preact';

class Clock extends Component {
	render() {
		let time = new Date().toLocaleTimeString();
		return <span>{ time }</span>;
	}
}

render(<Clock />, document.body);

调用了preact的render方法将virtualDOM渲染到真实dom。

// render.js
import { diff } from './vdom/diff';
export function render(vnode, parent, merge) {
	return diff(merge, vnode, {}, false, parent, false);
}

可见,render方法的第一个参数一个vnode,第二个参数是要挂载到的dom的节点,这里暂时不考虑第三个参数。而render方法实际上又是
去调用/vdom/diff.js下的diff方法

//diff函数的定义
export function diff(dom, vnode, context, mountAll, parent, componentRoot) {}

render函数使vnode转换成真实dom主要进行了以下操作

  • render函数实际上调用了diff方法,diff方法进而调用了idiff。
  • idiff方法会返回真实的html。idiff内将vnode分为4大类型进行处理封装在html
  • 然后调用diffAttributes,将vnode上的属性值更新到html domnode的属性上。(通过setAccessor)
  • 初次render时,下面if条件恒为真,所以真实html就这样被装进了。
 if (parent && ret.parentNode !== parent) parent.appendChild(ret);

这样初次的vnode转化成真实html就完成了

这里写图片描述

tips:在diff中会见到很多的out[ATTR_KEY],这个是用来将dom的attributrs数组每一项的name value转化为键值对存进 out[ATTR_KEY]。

组件的buildComponentFromNode是怎样的?

buildComponentFromNode的定义

 /** Apply the Component referenced by a VNode to the DOM.
 *	@param {Element} dom	The DOM node to mutate
 *	@param {VNode} vnode	A Component-referencing VNode
 *	@returns {Element} dom	The created/mutated element
 *	@private
 */
export function buildComponentFromVNode(dom, vnode, context, mountAll) {}

 初次调用时 buildComponentFromNode(undefined,vnode,{},false)。因此,初次render时的buildComponentFromVNode内部只是调用了如下的逻辑(不执行的代码去掉了)



export function buildComponentFromVNode(dom, vnode, context, mountAll) {
let c = dom && dom._component, // undefined
originalComponent = c,//undefined
oldDom = dom,// undefined
isDirectOwner = c && dom._componentConstructor===vnode.nodeName,//undefined
props = getNodeProps(vnode);// 这个函数除了一般的props获取外,还会加上defaultProps。
c = createComponent(vnode.nodeName, props, context);// 创建组件
setComponentProps(c, props, SYNC_RENDER, context, mountAll);
dom = c.base;
return dom;
}

组件的创建和后续处理,后面讲解

2019-04-07 04:33:40 weixin_33826609 阅读数 13

前言

前两个星期花了一些时间学习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的操作)。

对比文本节点

我们首先回忆一下文本节点的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。只会在两种情况下出现,第一种可能是第一次渲染oldChildren尚不存在,第二种可能是我们已经比较到了列表最后的位置childDom已经为null。这两种情况都是则将diff返回的dom, append的到父DOM中。

如果childDom不为null, 我们先查找childDom后面的dom是否存在与newDom相等的dom。如果存在我们不去做任何处理(dom属性相关的变更已经在diffElementNode完成了)。如果我们在childDom后面没有找到与newDom相等的dom,则说明列表不存在newDom,我们将插入到childDom的前面

下面遍历剩余没有使用到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();
};
复制代码

render

export function render(vnode, parentDom) {
	if (options.root) options.root(vnode, parentDom);
	let oldVNode = parentDom._prevVNode;
	vnode = createElement(Fragment, null, [vnode]);

	let mounts = [];
	diffChildren(parentDom, parentDom._prevVNode = vnode, oldVNode, EMPTY_OBJ, parentDom.ownerSVGElement!==undefined, oldVNode ? null : EMPTY_ARR.slice.call(parentDom.childNodes), mounts, vnode);
	commitRoot(mounts, vnode);
}
复制代码

在preact的render函数中第一个参数是根组件,第二个参数为根组件挂载的根节点。preact会使用Fragment包裹根组件, 成为如下的VNode。


{
  type: Fragment,
  props: {
    children: [
      vnode // 根节点
    ]
  }
}
复制代码

接着使用diffChildren遍历经过Fragment包裹的组件。在very-simple-react中,由于我没有实现Fragment的功能,render函数中直接使用diff函数。在render函数的最后使用commitRoot执行组件的componentDidMount生命周期。

unmount


export function unmount(vnode, ancestorComponent, skipRemove) {
	let r;
	if (options.unmount) options.unmount(vnode);

	if (r = vnode.ref) {
		applyRef(r, null, ancestorComponent);
	}

	let dom;
	if (!skipRemove && vnode._lastDomChild==null) {
		skipRemove = (dom = vnode._dom)!=null;
	}

	vnode._dom = vnode._lastDomChild = null;

	if ((r = vnode._component)!=null) {
		if (r.componentWillUnmount) {
			try {
				r.componentWillUnmount();
			}
			catch (e) {
				catchErrorInComponent(e, ancestorComponent);
			}
		}

		r.base = r._parentDom = null;
		if (r = r._prevVNode) unmount(r, ancestorComponent, skipRemove);
	}
	else if (r = vnode._children) {
		for (let i = 0; i < r.length; i++) {
			unmount(r[i], ancestorComponent, skipRemove);
		}
	}

	if (dom!=null) removeNode(dom);
}
复制代码

unmount函数的作用, 见名知义, 作用就是卸载组件和删除对应的DOM节点。

skipRemove参数的作用是标记当前节点的父节点是否被从Document中删除。如果当前节点的父节点已经被删除,那我们无须删除当前节点。只需要判断当前的节点是否为组件节点,如果是组件节点执行组件的componentWillUnmount生命周期即可。

commitRoot


export function commitRoot(mounts, root) {
	let c;
	while ((c = mounts.pop())) {
		try {
			c.componentDidMount();
		}
		catch (e) {
			catchErrorInComponent(e, c._ancestorComponent);
		}
	}

	if (options.commit) options.commit(root);
}
复制代码

commitRoot方法很简单, 就是在组件渲染完成后执行组件的componentDidMount生命周期。其中mounts中装载了所有第一次渲染的组件。


if (isNew) {
	if (newType.getDerivedStateFromProps==null && c.componentWillMount!=null) {
    c.componentWillMount()
  }
	if (c.componentDidMount!=null) {
    mounts.push(c)
  }
}
复制代码

在diff方法中会判断是否为第一次渲染, 和组件实例是否包含componentWillMount生命周期, 如果满足条件就会将实例push到mounts的队列中。

结语

到这里我们已经吧preact的源码大致浏览了一遍。我们接下来可以参考preact的源码,实现自己的react。话说我还给preact的项目提交了pr?,不过还没有merge?。

其他

very-simple-react。源码主要是我在学习preact的源码后, 参考(抄?‍)preact源码实现的。如果说preact是react的精简的实现,那我这个就是preact精简实现。虽然不能用于生产环境,但是对于学习了解react原理还是有一定帮助的。代码实现了JSX,组件,生命周期,diff,setState等核心功能。

转载于:https://juejin.im/post/5ca97d60f265da24d5070613

2018-03-14 04:20:00 weixin_33831673 阅读数 9
最近读了读preact源码,记录点笔记,这里采用例子的形式,把代码的执行过程带到源码里走一遍,顺便说明一些重要的点,建议对着preact源码看

vnode和h()

虚拟结点是对真实DOM元素的一个js对象表示,由h()创建

h()方法在根据指定结点名称、属性、子节点来创建vnode之前,会对子节点进行处理,包括

  1. 当前要创建的vnode不是组件,而是普通标签的话,文本子节点是null,undefined,转成'',文本子节点是number类型,转成字符串
  2. 连续相邻的两个子节点都是文本结点,合并成一个

例如:

h('div',{ id: 'foo', name : 'bar' },[
            h('p',null,'test1'),
            'hello',
            null
            'world', 
            h('p',null,'test2')
        ]
)

对应的vnode={

    nodeName:'div',
    attributes:{
        id:'foo',
        name:'bar'
    },
    [
        {
            nodeName:'p',
            children:['test1']
        },
        'hello world',
        {
            nodeName:'p',
            children:['test2']
        }
    ]

}

render()

render()就是react中的ReactDOM.render(vnode,parent,merge),将一个vnode转换成真实DOM,插入到parent中,只有一句话,重点在diff中

return diff(merge, vnode, {}, false, parent, false);

diff

diff主要做三件事

  1. 调用idff()生成真实DOM
  2. 挂载dom
  3. 在组件及所有子节点diff完成后,统一执行收集到的组件的componentDidMount()

重点看idiff

idiff(dom,vnode)处理vnode的三种情况

  1. vnode是一个js基本类型值,直接替换dom的文本或dom不存在,根据vnode创建新的文本返回
  2. vnode.nodeName是function 即当前vnode表示一个组件
  3. vnode.nodeName是string 即当前vnode表示一个对普通html元素的js表示

一般我们写react应用,最外层有一个类似<App>的组件,渲染时ReactDOM.render(<App/>>,root),这时候diff走的就是第二步,根据vnode.nodeName==='function'来构建组件,执行buildComponentFromVNode(),实例化组件,子组件等等

第三种情况一般出现在组件的定义是以普通标签包裹的,组件内部状态发生改变了或者初次实例化时,要render组件了,此时,要将当前组件现有的dom与执行compoent.render()方法得到的新的vnode进行Diff,来决定当前组件要怎么更新DOM

class Comp1 extends Component{

    render(){
        return <div>
                {
                    list.map(x=>{
                        return <p key={x.id}>{x.txt}</p>
                    })
                }
            <Comp2></Comp2>
        </div>
    }
    //而不是
    //render(){
    //    return <Comp2></Comp2>
    //}

}

普通标签元素及子节点的diff

我们以一个真实的组件的渲染过程来对照着走一下表示普通dom及子节点的vnode和真实dom之间的diff过程

假设现在有这样一个组件


class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      change: false,
      data: [1, 2, 3, 4]
    };
  }

 change(){
    this.setState(preState => {
        return {
            change: !preState.change,
            data: [11, 22, 33, 44]
        };
    });
 }

  render(props) {
    const { data, change } = this.state;
    return (
      <div>
        <button onClick={this.change.bind(this)}>change</button>
        {data.map((x, index) => {
          if (index == 2 && this.state.change) {
            return <h2 key={index}>{x}</h2>;
          }
          return <p key={index}>{x}</p>;
        })}
        {!change ? <h1>hello world</h1> : null}
      </div>
    );
  }
}

初次渲染

App组件初次挂载后的DOM结构大致表示为

dom = {
       tageName:"DIV",
       childNodes:[
           <button>change</button>
           <p key="0">1</p>,
           <p key="1">2</p>,
           <p key="2">3</p>,
           <p key="3">4</p>,
           <h1>hello world</h1>
       ]
}

更新

点击一下按钮,触发setState,状态发生变化,App组件实例入渲染队列,一段时间后(异步的),渲染队列中的组件被渲染,实例.render执行,此时生成的vnode结构大致是

vnode= {
    nodeName:"div"
    children:[
        { nodeName:"button", children:["change"] },
        { nodeName:"p", attributes:{key:"0"}, children:[11]},
        { nodeName:"p", attributes:{key:"1"}, children:[22]},
         { nodeName:"h2", attributes:{key:"2"}, children:[33]},
        { nodeName:"p", attributes:{key:"3"}, children:[44]},
    ]
 }

//少了最后的h1元素,第三个p元素变成了h2

然后在renderComponent方法内diff上面的dom和vnode diff(dom,vnode),此时在diff内部调用的idff方法内,执行的就是上面说的第三种情况vnode.nodeType是普通标签,关于renderComponent后面介绍

首先dom和vnode标签名是一样的,都是div(如果不一样,要通过vnode.nodeName来创建一个新元素,并把dom子节点复制到这个新元素下),并且vnode有多个children,所以直接进入innerDiffNode(dom,vnode.children)函数

innerDiffNode(dom,vchildren)工作流程

  1. 对dom结点下的子节点遍历,根据是否有key,放入两个数组keyed和children(那些没有key放到这个里)
  2. 遍历vchildren,为当前的vchild找一个相对应的dom下的子节点child,例如,key一样的,如果vchild没有key,就从children数组中找标签名一样的
  3. child=idiff(child, vchild); 递归diff,根据vchild来得到处理后的child,将child应用到当前父元素dom下

接着看上面的例子

  1. dom子节点遍历 得到两个数组
keyed=[
    <p key="0">1</p>,
       <p key="1">2</p>,
       <p key="2">3</p>,
       <p key="3">4</p>
]
children=[
    <button>change</button>,
    <h1>hello world</h1>
]
  1. 迭代vnode的children数组

存在key相等的

vchild={ nodeName:"p", attributes:{key:"0"}, children:[11]},
child=keyed[0]=<p key="0">1</p>

存在标签名改变的

vchild={ nodeName:"h2", attributes:{key:"2"}, children:[33]},
child=keyed[2]=<p key="2">3</p>,

存在标签名相等的

vchild={ nodeName:"button", children:["change"] },
child=<button>change</button>,

然后对vchild和child进行diff

child=idff(child,vchild)

看一组子元素的更新

看上面那组存在keys相等的子元素的diff,vchild.nodeName=='p'是个普通标签,所以还是走的idff内的第三种情况。

但这里vchild只有一个后代元素,并且child只有一个文本结点,可以明确是文本替换的情况,源码中这样处理,而不是进入innerDiffNode,算是一点优化

let fc = out.firstChild,
        props = out[ATTR_KEY],
        vchildren = vnode.children;

    if (props == null) {
        props = out[ATTR_KEY] = {};
        for (let a = out.attributes, i = a.length; i--;) props[a[i].name] = a[i].value;
    }

    // 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];
        }
    }

所有执行child=idiff(child,vchild)

child=<p key="0">11</p>
//文本值更新了

然后将这个child放入当前dom下的合适位置,一个子元素的更新就完成了

如果vchild.children数组有多个元素,又会进行vchild的子元素的迭代diff

至此,diff算是说了一半了,另一半是vnode表示一个组件的情况,进行组件渲染或更新diff

组件的渲染、diff与更新

和组件的渲染,diff相关的方法主要有三个,依次调用关系

buildComponentFromVNode

  1. 组件之前没有实例化过,实例化组件,为组件应用props,setComponentProps()
  2. 组件已经实例化过,属于更新阶段,setComponentProps()

setComponentProps

在setComponentProps(compInst)内部进行两件事

  1. 根据当前组件实例是首次实例化还是更新属性来调用组件的componentWillMount或者componentWillReceiveProps
  2. 判断是否时强制渲染,renderComponent()或者把组件入渲染队列,异步渲染

renderComponent

renderComponent内会做这些事:

  1. 判断组件是否更新,更新的话执行componentWillUpdate(),
  2. 判断shouldComponentUpdate()的结果,决定是否跳过执行组件的render方法
  3. 需要render,执行组件render(),返回一个vnode,diff当前组件表示的页面结构上的真实DOM和返回的这个vnode,应用更新.(像上面说明的那个例子一样)

依然从例子入手,假设现在有这样一个组件

class Welcom extends Component{

    render(props){
        return <p>{props.text}</p>
    }

}

class App extends Component {

    constructor(props){
        super(props) 
        this.state={
            text:"hello world"
        }
    }

    change(){
        this.setState({
            text:"now changed"
        })
    }

    render(props){

        return <div>
                <button onClick={this.change.bind(this)}>change</button>
                <h1>preact</h1>
                <Welcom text={this.state.text} />
            </div>

    }

}

render(<App></App>,root)

vnode={
    nodeName:App,
}

首次render

render(<App/>,root)执行,进入diff(),vnode.nodeName==App,进入buildComponentFromVNode(null,vnode)

程序首次执行,页面还没有dom结构,所以此时buildComponentFromVNode第一个参数是null,进入实例化App组件阶段

c = createComponent(vnode.nodeName, props, context);
if (dom && !c.nextBase) {
    c.nextBase = dom;
    // passing dom/oldDom as nextBase will recycle it if unused, so bypass recycling on L229:
    oldDom = null;
}
setComponentProps(c, props, SYNC_RENDER, context, mountAll);
dom = c.base;

在setComponentProps中,执行component.componentWillMount(),组件入异步渲染队列,在一段时间后,组件渲染,执行
renderComponent()

rendered = component.render(props, state, context);

根据上面的定义,这里有

rendered={
    nodeName:"div",
    children:[
        {
            nodeName:"button",
            children:['change']
        },
        {
            nodeName:"h1",
            children:['preact']
        },{
            nodeName:Welcom,
            attributes:{
                text:'hello world'
            }
        }
    ]
}

nodeName是普通标签,所以执行

base = diff(null, rendered) 
//这里需要注意的是,renderd有一个组件child,所以在diff()-->idiff()[**走第三种情况**]---->innerDiffNode()中,对这个组件child进行idiff()时,因为是组件,所以走第二种情况,进入buildComponentFromVNode,相同的流程

component.base=base //这里的baes是vnode diff完成后生成的真实dom结构,组件实例上有个base属性,指向这个dom

base大体表示为

base={
    tageName:"DIV",
       childNodes:[
        <button>change</button>
           <h1>preact</h1>
        <p>hello world</p>
       ]
}

然后为当前dom元素添加一些组件的信息

base._component = component;
base._componentConstructor = component.constructor;

至此,初始化的这次组件渲染就差不多了,buildComponentFromVNode返回dom,即实例化的App的c.base,在diff()中将dom插入页面

更新

然后现在点击按钮,setState()更新状态,setState源码中

let s = this.state;
if (!this.prevState) this.prevState = extend({}, s);
extend(s, typeof state==='function' ? state(s, this.props) : state);
/**
* _renderCallbacks保存回调列表
*/
if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
enqueueRender(this);

组件入队列了,延迟后执行renderComponent()

这次,在renderComponent中,因为当前App的实例已经有一个base属性,所以此时实例属于更新阶段isUpdate = component.base =true,执行实例的componentWillUpdate()方法,如果实例的shouldComponentUpdate()返回true,实例进入render阶段。

这时候根据新的props,state

rendered = component.render(props, state, context);

rendered={
    nodeName:"div",
    children:[
        {
            nodeName:"button",
            children:['change']
        },
        {
            nodeName:"h1",
            children:['preact']
        },{
            nodeName:Welcom,
            attributes:{
                text:'now changed' //这里变化
            }
        }
    ]
}

然后,像第一次render一样,base = diff(cbase, rendered),但这时候,cbase是上一次render后产生的dom,即实例.base,然后页面引用更新后的新的dom.rendered的那个组件子元素(Welcom)同样执行一次更新过程,进入buildComponentFromVNode(),走一遍buildComponentFromVNode()-->setComponentProps()--->renderComponent()--->render()--->diff(),直到数据更新完毕

总结

preact src下只有15个js文件,但一篇文章不能覆盖所有点,这里只是记录了一些主要的流程,最后放一张有毒的图

github

2018-04-17 21:06:01 flytam 阅读数 402

紧接上节,Preact组件从vnode到真实html的过程发生了什么?

...
// buildComponentFromVNode方法内部
// buildComponentFromVNode(undefined, vnode, {}, false);
c = createComponent(vnode.nodeName, props, context);// 创建组件
setComponentProps(c, props, SYNC_RENDER, context, mountAll);
dom = c.base;
    return dom;
....

从上节组件变成真实dom的过程中最重要的函数就是createComponentsetComponentProps。我们可以发现,在先后执行了createComponentsetComponentProps后,真实dom就是c.base了。那么
这个createComponent干了什么?去掉一些初始渲染时不会去执行的代码,简化后的代码如下:

// 如果是用class定义的那种有生命周期的组件,上文代码中的```vnode.nodeName```其实就是我们定义的那个class。
export function createComponent(Ctor, props, context) {
    let inst;
    if (Ctor.prototype && Ctor.prototype.render) {
        // 正常的组件 class xxx extends Component{} 定义的
        //首先是对自己的组件实例化
        inst = new Ctor(props, context);
        //然后再在我们实例化的组件,去获得一些Preact的内置属性(props、state,这两个是挂在实例上的)和一些内置方法(setState、render之类的,这些方法是挂在原型上的)
        Component.call(inst, props, context);
    } else {
        // 无状态组件
        //无状态组件是没有定义render的,它的render方法就是这个无状态组件本身
        inst = new Component(props, context);
        inst.constructor = Ctor;
        inst.render = doRender;
    }
    return inst;
}

function doRender(props, state, context) {
    // 无状态组件的render方法就是自己本身
    return this.constructor(props, context);
}

Component的定义如下。通过上面和下面的代码可以知道,createComponent的主要作用就是让我们编写的class型和无状态型组件实例化,
这个实例是具有相似的结构。并供后面的setComponentProps去使用产生真实dom。

// Component的定义
export function Component(props, context) {
    this._dirty = true;// 这个东西先不管,应该是和diff有关
    this.context = context;// 类似react的context
    this.props = props;
    this.state = this.state || {};
}
// 这里的extend就是一个工具函数,把setState、forceUpdate、render方法挂载到原型上
extend(Component.prototype,{
    setState(state,callback){},
    forceUpdate(callback){},
    render() {}
})

setComponentProps产生真实dom的过程。

setComponentProps(c, props, SYNC_RENDER, {}, false);

export function setComponentProps(component, props, opts, context, mountAll) {
    // 同理去除条件不成立的代码,只保留首次渲染时运行的关键步骤
    if (!component.base || mountAll) {
        // 可见。componentWillMount生命周期方法只会在未加载之前执行,
        if (component.componentWillMount) component.componentWillMount();
    }
    renderComponent(component, SYNC_RENDER, mountAll);
}

由上面代码可见,setComponentProps内部,实际上关键是调用了renderComponent方法。renderComponent逻辑有点绕,
精简版代码如下。

renderComponent主要逻辑简单来说如下:
1、调用组件实例的render方法去产生vnode。

2、如果这个组件产生的vnode不再是组件了。则通过diff函数去产生真实dom并挂载(前面已经分析过)diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, true);

3、如果这个组件的子vnode还是子组件的话。则再次调用setComponentPropsrenderComponent去进一步生成真实dom,直到2中条件成立。(判断步骤和2、3类似),但是有点区别的是。这种调用代码是

component._component = inst = createComponent(childComponent, childProps, context);
setComponentProps(inst, childProps, NO_RENDER, context, false);// 不渲染。只是去执行下生命周期方法,在这个setComponentProps内部是不调用 renderComponent的。 至于为啥。。暂时我也不知道。NO_RENDER标志位
renderComponent(inst, SYNC_RENDER, mountAll, true);

精简版代码

export function renderComponent(component, opts, mountAll, isChild) {
    // 这个函数其实很长有点复杂的,只保留了初次渲染时执行的部分和关键的部分。
        // 调用组件的render方法,返回vnode
        rendered = component.render(props, state, context);//*****
        let childComponent = rendered && rendered.nodeName,base;
        if (typeof childComponent === 'function') {
            // 子节点也是自定义组件的情况
            let childProps = getNodeProps(rendered);
                component._component = inst = createComponent(childComponent, childProps, context);
                setComponentProps(inst, childProps, NO_RENDER, context, false);// 不渲染啊。只是去执行下生命周期方法
                renderComponent(inst, SYNC_RENDER, mountAll, true);// 对比  renderComponent(component, SYNC_RENDER, mountAll);
        } else {
            base = diff(。。。);// 挂载
        }
        if (!isUpdate || mountAll) {// 成立
        mounts.unshift(component);// 把已经挂载的组件实例存进mounts数组
        }
        component.base = base; //把真实dom挂载到base属性上
        if (!diffLevel && !isChild) flushMounts();
}

前面看到了componentWillMount生命周期了,那么componentDidMount这个生命周期呢?它就是在flushMounts。这个if语句成立的条件是在祖先组件并且初次渲染时才执行(初次渲染的diffLevel值为0)。

export function flushMounts() {
    let c;
    while ((c = mounts.pop())) {
        if (options.afterMount) options.afterMount(c);
        if (c.componentDidMount) c.componentDidMount();
    }
}

flushMounts中的mounts就是当前挂载的组件的实例。它是一个栈的结构并依次出栈执行componentDidMount。所以,
这就能说明了Preact(React也一样)父子组件的生命周期执行顺序了 parentWillMount -> parentRender -> childWillMount -> childRender -> childDidMount -> parentDidParent。

至此组件类型的vnode产生真实dom的分析就结束了。

流程图如下
这里写图片描述

2018-04-20 16:45:23 flytam 阅读数 221

非组件节点的diff分析

diff的流程,我们从简单到复杂进行分析

通过前面几篇文章的源码阅读,我们也大概清楚了diff函数参数的定义和component各参数的作用

/**
 * @param dom 初次渲染是undefinde,第二次起是指当前vnode前一次渲染出的真实dom
 * @param vnode vnode,需要和dom进行比较
 * @param context 类似与react的react
 * @param mountAll
 * @param parent
 * @param componentRoot
 * **/
function diff(dom, vnode, context, mountAll, parent, componentRoot){}
// component
{

    base,// dom
    nextBase,//dom

    _component,//vnode对应的组件
    _parentComponent,// 父vnode对应的component
    _ref,// props.ref 
    _key,// props.key
    _disable,

    prevContext,
    context,

    props,
    prevProps,

    state,
    previousState

    _dirty,// true表示该组件需要被更新
    __preactattr_// 属性值

    /***生命周期方法**/
    .....
}

diff不同类型的vnode也是不同的。Preact的diff算法,是将setState后的vnode与前一次的dom进行比较的,边比较边更新。diff主要进行了两步操作(对于非文本节点来说),
先diff内容innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML != null);,再diff属性diffAttributes(out, vnode.attributes, props);

1、字符串或者布尔型
如果之前也是一个文本节点,则直接修改节点的nodeValue的值;否则,创建一个新节点,并取代旧节点。并调用recollectNodeTree对旧的dom进行腊鸡回收。

2、html的标签类型
- 如果vnode的标签对比dom发生了改变(例如原来是span,后来是div),则新建一个div节点,然后把span的子元素都添加到新的div节点上,把新的div节点替换掉旧的span节点,然后回收旧的(回收节点的操作主要是把这个节点从dom中去掉,从vdom中也去掉)

    if (!dom || !isNamedNode(dom, vnodeName)) {
         // isNamedNode方法就是比较dom和vnode的标签类型是不是一样
        out = createNode(vnodeName, isSvgMode);
        if (dom) {
            while (dom.firstChild) out.appendChild(dom.firstChild);
            if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
            recollectNodeTree(dom, true);//recollectNodeTree
        }
    }
  • 对于子节点的diff

    • Preact对于只含有一个的子字符串节点直接进行特殊处理

          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];
          }
      }
    • 对于一般情况

      /****/
      innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML != null);

    那么,innerDiffNode函数做了什么?
    首先,先解释下函数内定义的一些关键变量到底干了啥

        let originalChildren = dom.childNodes,// 旧dom的子node集合
        children = [],// 用来存储旧dom中,没有提供key属性的dom node
        keyed = {},// 用来存旧dom中有key的dom node,

    首先,第一步的操作就是对旧的dom node进行分类。将含有key的node存进keyed变量有,这是一个键值对结构;
    将无key的存进children中,这是一个数组结构。

    然后,去循环遍历vchildren的每一项,用vchild表示每一项。若有key属性,则取寻找keyed中是否有该key对应的真实dom;若无,则去遍历children
    数据,寻找一个与其类型相同(例如都是div标签这样)的节点进行diff(用child这个变量去存储)。然后执行idiff函数
    child = idiff(child, vchild, context, mountAll);。通过前面分析idiff函数,我们知道如果传进idiff的child为空,则会新建一个节点。所以对于普通节点的内容的diff就完成了。然后把这个返回新的dom node去取代旧的就可以了,代码如下

            f = originalChildren[i];
            if (child && child !== dom && child !== f) {
                if (f == null) {
                    dom.appendChild(child);
                } else if (child === f.nextSibling) {
                    removeNode(f);
                } else {
                    dom.insertBefore(child, f);
                }
            }

    当对vchildren遍历完成diff操作后,把keyedchildren中剩余的dom节点清除。因为他们在新的vnode结构中已经不存在了

    然后对于属性进行diff就可以了。diffAttributes的逻辑就比较简单了,取出新vnode 的 props和旧dom的props进行比较。新无旧有的去除,新有旧有的替代,新有旧无的添加。setAccessor是对于属性值设置时一些保留字和特殊情况进行一层封装处理

    function diffAttributes(dom, attrs, old) {
    let name;
    for (name in old) {
        if (!(attrs && attrs[name] != null) && old[name] != null) {
            setAccessor(dom, name, old[name], old[name] = undefined, isSvgMode);
        }
    }
    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);
        }
    }
    }

    至此,对于非组件节点的内容的diff完成了

没有更多推荐了,返回首页