Preact源码分析

栏目: 编程工具 · 发布时间: 5年前

内容简介:本文同步在个人博客shymean.com上,欢迎关注最近打算学习React源码,发现了一个简易版的框架本文使用源码版本

本文同步在个人博客shymean.com上,欢迎关注

最近打算学习React源码,发现了一个简易版的框架 Preact ,且与React的API比较相似,因此决定先看看它的代码。

本文使用源码版本 preact 10.0.0-beta.3 ,复制了部分核心源码,删除了一些逻辑分支并增加了注释。

开发环境

克隆整个项目,安装依赖,然后进行断点调试

git clone git@github.com:preactjs/preact.git
# 安装项目依赖
cd preact
npm i

# 进入demo项目,安装webpack、babel等相关依赖
cd demo 
npm i

# 启动demo项目,开始进行断点调试
npm run start
复制代码

修改 demo/index.js 中的代码,构建一个最基本的应用

import { createElement, render, Component } from 'preact';
class Home extends Component {
	constructor(props) {
		super(props);
		this.state = {
			count: 1
		};
	}
	render() {
		let { count } = this.state;
		let { msg } = this.props;
		return (
			<div>
				<h1>{msg}</h1>
				<p>count:{count}</p>
			</div>
		);
	}
}
let vnode = (
	<Home msg="hello msg"/>
);
console.log(vnode);

let app = document.createElement('div');
document.body.appendChild(app);

render(vnode, app);

复制代码

我们构建了一个叫 Home 的组件,然后将它挂载到一个DOM节点上上,从上面代码可以看出,我们首先需要了解 Component 类和 render 函数

渲染流程

Component

下面是Component类的源码,

// src/component.js
export function Component(props, context) {
	this.props = props;
	this.context = context;
}
Component.prototype.setState = function(update, callback) {}
Component.prototype.forceUpdate = function(callback) {}
Component.prototype.render = Fragment

// src/create-element.js
export function Fragment(props) {
	return props.children;
}
复制代码

可以把Component看做是一个类,我们暂时不需要关心其方法的作用和实现。

vnode

再回过头来看看 <Home /> 到底是啥东西

// demo/index.js
let vnode = (
	<Home msg="hello msg"/>
);
// 被babel转化成下面代码,chrome调试模式->network面板->main.js中可查看babel编译后的代码
var vnode = Object(preact__WEBPACK_IMPORTED_MODULE_0__["createElement"])(Home, {
    msg: "hello msg"
});
// 打印vnode, 控制台输出下面内容,可见标签上的属性转换成了props
{"props":{"msg":"hello msg"},"_children":null,"_parent":null,"_depth":0,"_dom":null,"_lastDomChild":null,"_component":null}
复制代码

babel编译JSX,将其转换成 createElement 方法调用,这也是为什么 demo/index.js 文件头部需要手动引入一个 createElement 方法的原因,查看 createElement 相关源码

// src/create-element.js
export function createElement(type, props, children) {
	props = assign({}, props);

	if (arguments.length>3) {
        // children后传入多个参数时转换为数组
        // ...
	}
	if (type!=null && type.defaultProps!=null) {
        // 处理type.defaultProps,并将其合并到props上
        // ...
	}
    // 处理key和ref
	let ref = props.ref;
	let key = props.key;
	if (ref!=null) delete props.ref;
	if (key!=null) delete props.key;
    // 调用createVNode,因此createElement 返回的是一个vnode
	return createVNode(type, props, key, ref);
}
复制代码

注意 propschildren 参数都是有babel编译JSX时,通过解析模板替我们传入的参数。顺藤摸瓜,我们来看看 createNode

// src/create-element.js
export function createVNode(type, props, key, ref) {
    // 已经把vnode简化成一个对象字面量了,可以看到这跟上面打印的<Home />基本一致
	const vnode = {
		type,
		props,
		key,
		ref,
		_children: null,
		_parent: null,
		_depth: 0,
		_dom: null,
		_lastDomChild: null,
		_component: null,
		constructor: undefined
	};

	return vnode;
}
复制代码

render

现在我们知道了 <Home /> 实际上就是一个vnode,接下来再看看 render(<Home />, document.body) 中的逻辑

// src/render.js
export function render(vnode, parentDom, replaceNode) {
	if (options._root) options._root(vnode, parentDom);
	let oldVNode = parentDom._children;
	vnode = createElement(Fragment, null, [vnode]); // 使用Fragment包裹了实际的vnode

	let mounts = [];
	diff(
		parentDom, // document.body
		replaceNode ? vnode : (parentDom._children = vnode), // parentDom._children = vnode
		oldVNode || EMPTY_OBJ, // {}
		EMPTY_OBJ, // {}
		parentDom.ownerSVGElement !== undefined, // false
		replaceNode
			? [replaceNode]
			: oldVNode
				? null
				: EMPTY_ARR.slice.call(parentDom.childNodes), // document.body所有DOM子节点
		mounts, // []
		false,
		replaceNode || EMPTY_OBJ, // {}
	);
	commitRoot(mounts, vnode);
}
复制代码

通过断点发现,在 diff 方法结束之后页面进行了渲染,那么在该方法内,肯定实现了从 vnode 到实际DOM节点的转变。至此,整个渲染流程分析基本完毕。

小结

大致流程如下

  • 创建了一个组件类 Home ,然后构造了一个 vnode ,最后调用 render 方法将该vnode挂载到了页面DOM节点上
  • render 函数内部,调用了diff方法,将递归遍历以该vnode构造的AST,并将所有vnode转换成DOM节点,完成页面渲染

preact把渲染相关的操作一并放在了 diff 代码中,因此看起来涉及到的流程还是比较多的。初始化时可以当做新vnode与旧的空节点做比较,因此第一次渲染也使用与页面更新时相同的diff逻辑来完成渲染。

那么,diff方法内部的流程到底是如何实现的呢?

diff三部曲

该函数有点长,我们现在暂时只需要关注初始化时页面的渲染流程,因此下面源码删除了与初始化无关的条件分支。记住,我们现在把初识化的过程当做一个全新的vnode与空节点之间的对比。

// src/diff/index.js
/*
parentDom: 父DOM节点
newVNode: 新的AST根节点
oldVNode: 旧的AST根节点
context: 当前context
isSvg: 是否是svg节点
excessDomChildren: 父节点下其余的DOM节点
mounts: 一个表示需要触发挂载成功的组件列表,从根节点一直透传到所有叶子节点,并收集所有需要出发的节点
force: 是否强制更新
oldDom: 当前DOM节点
*/
export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, force, oldDom) {
	let tmp, newType = newVNode.type;

	try {
		outer: if (typeof newType==='function') {
      // 根据oldVNode是否存在判断是更新还是新增节点,初始化相关数组和组件实例
			let c, isNew, oldProps, oldState, snapshot, clearProcessingException;
			let newProps = newVNode.props;
			let cctx =  context;
			// 如果是一个注册的Component组件,则调用构造函数获取组件实例,因此Home组件就是在此处实例化的
      if (newType.prototype && newType.prototype.render) {
        // vnode通过_component维持了对于组件实例的引用,因此可以newVNode._component.setState()等方式调用组件方法
        newVNode._component = c = new newType(newProps, cctx); // eslint-disable-line new-cap
      }else {
        // 根节点由Fragment组件包裹,无render方法,因此直接调用Component
        newVNode._component = c = new Component(newProps, cctx);
        c.constructor = newType;
        c.render = doRender; //  (props, state, context) => this.constructor(props, context)
      }

      c.props = newProps;
      if (!c.state) c.state = {}; // 设置组件默认的state
      c.context = cctx;
      c._context = context;
      isNew = c._dirty = true;
      c._renderCallbacks = [];

			if (c._nextState==null) {
				c._nextState = c.state;
			}
      // 调用组件的getDerivedStateFromProps生命周期,该钩子函数是组件的一个静态方法
      if (newType.getDerivedStateFromProps!=null) {
				assign(c._nextState==c.state ? (c._nextState = assign({}, c._nextState)) : c._nextState, newType.getDerivedStateFromProps(newProps, c._nextState));
			}
      // 调用componentWillMount声明周期函数,可见父组件的componentWillMount先于子组件调用
      // 将注册了componentDidMount声明周期函数的组件放在mounts数组中,等待所有子节点都挂载完毕后在render的commitRoot方法中统一调用
			if (isNew) {
				if (newType.getDerivedStateFromProps==null && c.componentWillMount!=null) c.componentWillMount();
				if (c.componentDidMount!=null) mounts.push(c);
			}

			oldProps = c.props;
			oldState = c.state;

			c.context = cctx;
			c.props = newProps;
      // 设置_nextState的初始值为state
      if (c._nextState==null) {
				c._nextState = c.state;
			}

			c._dirty = false;
			c._vnode = newVNode;
			c._parentDom = parentDom;

      tmp = c.render(c.props, c.state, c.context); // 调用组件render方法

      // 将tmp子节点转换为一个一维数组, 并存放在newVNode._children中
      // 其中coerceToVNode接收一个vnode作为参数,如果vnode已经有了_dom属性,则返回一个克隆后的vnode;否则返回当前vnode
      toChildArray(tmp, newVNode._children=[], coerceToVNode, true); 

      // 开始对比子节点,其内部递归调用了diff方法,通过diffElementNodes获取子节点的真实dom
      // 然后调用parentDom.appendChild(newDom)或parentDom.insertBefore(newDom, oldDom),将dom插入页面
			diffChildren(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, oldDom);

			c.base = newVNode._dom;
			while (tmp=c._renderCallbacks.pop()) tmp.call(c);
		}else {
      // 一个封装组件的最底层都是用html标签构造的,当newType不是Component时,表示渲染的是元素DOM,其内部调用了document.createElement方法渲染真正的dom
			newVNode._dom = diffElementNodes(oldVNode._dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts);
		}
	}catch (e) {
		catchErrorInComponent(e, newVNode._parent);
	}

	return newVNode._dom;
}
复制代码

可见在 diff 中调用了 diffChildren 方法来比较两个vNode的所有子节点的差异,让我们紧随其后,一探究竟

// src/diff/children.js
export function diffChildren(parentDom, newParentVNode, oldParentVNode, context, isSvg, excessDomChildren, mounts, oldDom) {
    let childVNode, i, j, oldVNode, newDom, sibDom, firstChildDom, refs;
    // 在上一层的diff方法中已经调用了toChildArray将其组件的render函数返回值转换成了_children属性,
    // 如果render函数未返回数据,则再次调用toChildArray将其props.children属性转换成_children属性
    // 这就是为什么在无render函数的时候还可以染组件内标签的原因
    let newChildren =newParentVNode._children || toChildArray(newParentVNode.props.children, newParentVNode._children=[], coerceToVNode, true); 
    // 获取旧的子节点列表
    let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
    for (i=0; i<newChildren.length; i++) {
        // 如果vnode已被使用且关联了一个_dom元素,则克隆出一个新的vnode
        childVNode = newChildren[i] = coerceToVNode(newChildren[i]);
        // 跳过为null的子节点
        if (childVNode!=null) {
            childVNode._parent = newParentVNode;
            childVNode._depth = newParentVNode._depth + 1;

            oldVNode = oldChildren[i];
            // 处理oldChildren[i],如果存在某些未与childVNode做比较的子节点,则再后面会调用unmount进行移除
            if (oldVNode===null || (oldVNode && childVNode.key == oldVNode.key && childVNode.type === oldVNode.type)) {
				oldChildren[i] = undefined;
			}else {
				for (j=0; j<oldChildrenLength; j++) {
					oldVNode = oldChildren[j];
					if (oldVNode && childVNode.key == oldVNode.key && childVNode.type === oldVNode.type) {
						oldChildren[j] = undefined;
						break;
					}
					oldVNode = null;
				}
			}
      oldVNode = oldVNode || EMPTY_OBJ;

      // 开始比较每个子节点的区别,递归调用diff方法内的diffChildren方法,
      // 此时我们将跳转会diff方法,并最终跳转到diffElementNodes方法,获取到一个真实的dom节点,因此这里先阅读下面的 diffElementNodes 源码部分
      newDom = diff(parentDom, childVNode, oldVNode, context, isSvg, excessDomChildren, mounts, null, oldDom);

      // 阅读完diffElementNodes源码,我们知道了diff方法返回的是一个oldVNode._dom经过初始化、diffChildren和diffProps后的DOM节点
      // 此时 newVNode._dom = newDom
          if (newDom!=null) {
                if (firstChildDom == null) {
                    firstChildDom = newDom;
                }
                if (childVNode._lastDomChild != null) {
                    // 我们知道一个组件只能包含一个最外层的子节点,
                    // 如果childVNode.type是一个组件,那么将childVNode保存的_lastDomChild属性赋值给newDom,无需进行下面分支的判断比较
                    newDom = childVNode._lastDomChild;
                    childVNode._lastDomChild = null;
                }else if (excessDomChildren==oldVNode || newDom!=oldDom || newDom.parentNode==null) {
                    outer: if (oldDom==null || oldDom.parentNode!==parentDom) {
                        // 如果父节点都已经修改,则直接向新的parentDom中追加newDom即可
                        parentDom.appendChild(newDom);
                    }
                    else {
                        // 如果父节点相同,则判断newDom是否已经存在parentDom中,不存在则调用insertBefore插入newDom
                        // todo 这里为什么调用的是insertBefore
                        // `j<oldChildrenLength; j+=2` is an alternative to `j++<oldChildrenLength/2`
                        for (sibDom=oldDom, j=0; (sibDom=sibDom.nextSibling) && j<oldChildrenLength; j+=2) {
                            if (sibDom==newDom) {
                                break outer;
                            }
                        }
                        parentDom.insertBefore(newDom, oldDom);
                    }
                }

                oldDom = newDom.nextSibling;
                // 如果childVNode.type是一个组件,保存newDom到其_lastDomChild属性
                if (typeof newParentVNode.type == 'function') {
                    newParentVNode._lastDomChild = newDom;
                }
            }
        }
    }
    newParentVNode._dom = firstChildDom;
    // 如果还存在未被设置为undefined的旧节点,如oldChildrenLength > newChildren.length 的情况,则需要移除旧节点
	for (i=oldChildrenLength; i--; ) if (oldChildren[i]!=null) unmount(oldChildren[i], newParentVNode);
}
复制代码

接下来看看 diffElementNodes 是何方神圣

// src/diff/index.js
function diffElementNodes(dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts) {
	let i;
	let oldProps = oldVNode.props;
	let newProps = newVNode.props;

	isSvg = newVNode.type==='svg' || isSvg;
    // 在diff方法中传入的是oldVNode._dom,第一次调用时会初始化,生成真实的dom节点
	if (dom==null) {
        // 无type类型,返回纯文本节点
		if (newVNode.type===null) {
			return document.createTextNode(newProps);
		}
        // 有类型,如div、h1、p标签等,则返回实际dom节点
		dom = isSvg ? document.createElementNS('http://www.w3.org/2000/svg', newVNode.type) : document.createElement(newVNode.type);
	}
    // 如果节点未发生变化,则可以以该节点为根的子AST未发生变化,即所有子节点均无变化,diff到此为止
    // 只有当新节点发生变化时,才进行该条件判断的逻辑,其内部会继续调用diffChildren判断相关子节点
	if (newVNode!==oldVNode) {
		oldProps = oldVNode.props || EMPTY_OBJ;

        // 替换dom节点html内容为dangerouslySetInnerHTML属性传递的内容
		let oldHtml = oldProps.dangerouslySetInnerHTML;
		let newHtml = newProps.dangerouslySetInnerHTML;
		if ((newHtml || oldHtml) && excessDomChildren==null) {
			// Avoid re-applying the same '__html' if it did not changed between re-render
			if (!newHtml || !oldHtml || newHtml.__html!=oldHtml.__html) {
				dom.innerHTML = newHtml && newHtml.__html || '';
			}
		}
        // 处理multiple属性
		if (newProps.multiple) {
			dom.multiple = newProps.multiple;
		}
        // 将dom作为parentDom,并开始对比newVNode和oldVNode的子节点列表
		diffChildren(dom, newVNode, oldVNode, context, newVNode.type==='foreignObject' ? false : isSvg, excessDomChildren, mounts, EMPTY_OBJ);
        
        // 将新旧属性的变化复制到新的dom节点上,如style、value、checked等属性
		diffProps(dom, newProps, oldProps, isSvg);
	}
    // 返回新的dom节点,此时跳回diffChildren调用diff方法的地方,调用diff方法得到的就是这个dom节点
	return dom;
}
复制代码

diff 方法需要结合 diffChildrendiffElementNodes 这两个方法一起阅读,他们内部互相嵌套调用,直至遍历完整个vnode组成的AST。

  • 首先调用diff,根据newType的类型判断调用 diffChildren 还是 diffElementNodes
  • diffChildren 中,获取新旧节点的子节点列表,依次递归调用diff方法;
  • diffElementNodes ,通过判断newVNode和 oldVNode 是否相同,如果不相同,则递归调用 diffChildren ,如果相同,则表示无变化,递归出栈。

在render函数中调用 diff 方法进行初始化时, oldVnode 为空, oldVnode._dom 也为null,因此就会进入上面相关代码的初始化化流程。

我们知道 diff 主要是用来比较新旧两个VNode树,用于减少真实DOM操作的性能消耗,在状态更新引起的页面重新渲染时,我们需要继续关注diff函数的其他工作,在此之前,我们只需要关注vnode是如何转换成DOM即可。

setState

在diff代码中可以看见,初始化时,vnode通过 vnode._component 属性维持了组件实例的引用。而在调用 setState 更新状态之后,页面会重新渲染组件,接下来让我们看看状态更新时发生了什么。

渲染流程

修改demo内的代码

// demo/index.js
setTimeout(() => {
	vnode._component.setState({
		count: 2
	});
}, 1000);
复制代码

经过1s的延迟之后,会重新渲染文本内容为 count:2 ,现在我们从 _component.setState 入手,看看调用setState之后的执行流程

// src/component.js
Component.prototype.setState = function(update, callback) {
    // 在diff中初始化时,将_nextState初始化为state,需要注意assign返回的是它的第一个参数
	let s = (this._nextState!==this.state && this._nextState) || (this._nextState = assign({}, this.state));
	if (typeof update!=='function' || (update = update(s, this.props))) {
        // 合并this._nextState和需要更新的数据update,update上的属性会覆盖this._nextState的值
        // 注意此处并不会修改当前this.state的值,setState()方法是异步的!!
		assign(s, update);
	}
	if (update==null) return;
	if (this._vnode) {
        // 收集更新后的回调,在渲染完成之后将执行该回调
		if (callback) this._renderCallbacks.push(callback);
		enqueueRender(this); // 将此次更新入队列
	}
};
复制代码

然后我们来看看这个渲染队列 enqueueRender 的实现

let q = [];
const defer = typeof Promise=='function' ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout;

export function enqueueRender(c) {
	if (!c._dirty && (c._dirty = true) && q.push(c) === 1) {
		(options.debounceRendering || defer)(process); // 异步执行process,这里可以说明setState方法是异步的
	}
}
function process() {
	let p;
    // 将节点按深度进行排序,深度越大,排位越靠前,可见子组件先触发forceUpdate
	q.sort((a, b) => b._vnode._depth - a._vnode._depth);
    // 逐步调用forceUpdate,最后清空q
	while ((p=q.pop())) {
		// forceUpdate's callback argument is reused here to indicate a non-forced update.
		if (p._dirty) p.forceUpdate(false);
	}
}
复制代码

从上面的代码我们知道了 setState方法是异步 的原因,可知调用setState之后,preact会把组件更新后的数据放在 _nextState 上,然后将该组件放入渲染队列中,等待所有的setState调用完毕,浏览器进入异步事件队列时,根据组件对应vnode的深度进行排序,依次调用组件的 forceUpdate 方法,接下来看看 forceUpdate 这个方法

// src/component.js
Component.prototype.forceUpdate = function(callback) {
	let vnode = this._vnode, oldDom = this._vnode._dom, parentDom = this._parentDom;
	if (parentDom) {
		const force = callback!==false;

		let mounts = [];
        // 调用diff方法,重新渲染页面
		let newDom = diff(parentDom, vnode, assign({}, vnode), this._context, parentDom.ownerSVGElement!==undefined, null, mounts, force, oldDom == null ? getDomSibling(vnode) : oldDom);

		commitRoot(mounts, vnode);

		if (newDom != oldDom) {
			updateParentDomPointers(vnode);
		}
	}
	if (callback) callback();
};

复制代码

diff

可以发现,在 forceUpdate 中,调用的仍旧是 diff 方法,通过对比新的节点 vnode 和旧的节点 assign({}, vnode) 重新渲染页面,因此我们现在需要回到diff方法,查看当更新节点state时是如何重新渲染的。同样地,为了简化流程,移除了大部分与setState相关流程无关的代码。

export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, force, oldDom) {
	let tmp, newType = newVNode.type;

	try {
		outer: if (typeof newType==='function') {
			let c, isNew, oldProps, oldState, snapshot, clearProcessingException;
			let newProps = newVNode.props;
			let cctx =  context;

            // 之前已经调用过组件构造函数,因此此处直接赋值
			if (oldVNode._component) {
				c = newVNode._component = oldVNode._component;
				clearProcessingException = c._processingException = c._pendingError;
			}
            // 调用相关声明周期函数
            if (newType.getDerivedStateFromProps!=null) {
				assign(c._nextState==c.state ? (c._nextState = assign({}, c._nextState)) : c._nextState, newType.getDerivedStateFromProps(newProps, c._nextState));
			}
            if (newType.getDerivedStateFromProps==null && force==null && c.componentWillReceiveProps!=null) {
                c.componentWillReceiveProps(newProps, cctx);
            }
            // 如果组件的shouldComponentUpdate方法返回false,则不更新组件,跳出最外层outer处的if循环
            if (!force && c.shouldComponentUpdate!=null && c.shouldComponentUpdate(newProps, c._nextState, cctx)===false) {
                c.props = newProps;
                c.state = c._nextState;
                c._dirty = false;
                c._vnode = newVNode;
                newVNode._dom = oldVNode._dom;
                newVNode._children = oldVNode._children;
                break outer;
            }
            if (c.componentWillUpdate!=null) {
                c.componentWillUpdate(newProps, c._nextState, cctx);
            }
            
            // 获取新旧节点的props和state
			oldProps = c.props;
			oldState = c.state;

			c.context = cctx;
			c.props = newProps; // 设置新的props
			c.state = c._nextState; // 此时才开始将组件的state设置为调用setState方法传入的新值,牢记setState方法是异步的!!

			c._dirty = false;
			c._vnode = newVNode;
			c._parentDom = parentDom;

			try {
				tmp = c.render(c.props, c.state, c.context); // 重新调用render函数,生成新的vnode,新的vnode会渲染新的dom节点
				toChildArray(tmp, newVNode._children=[], coerceToVNode, true);
			}catch (e) {
				if ((tmp = options._catchRender) && tmp(e, newVNode, oldVNode)) break outer;
				throw e;
			}
            // 调用getSnapshotBeforeUpdate生命周期函数
			if (!isNew && c.getSnapshotBeforeUpdate!=null) {
				snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
			}

            // 开始对比子节点,在内部修改newVNode._dom实际DOM节点并挂载到parentDom上,这里与上面在初识化渲染时候分析基本一致
            // 递归diffChildren、diff和diffElementNodes,获取新的newVNode._dom
			diffChildren(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, oldDom);

			c.base = newVNode._dom;
            // 此时渲染完毕,开始执行setState时传入的回调函数
			while (tmp=c._renderCallbacks.pop()) tmp.call(c);

            // 调用componentDidUpdate生命周期函数
			if (!isNew && oldProps!=null && c.componentDidUpdate!=null) {
				c.componentDidUpdate(oldProps, oldState, snapshot);
			}
		}
		else {
			newVNode._dom = diffElementNodes(oldVNode._dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts);
		}
	}
	catch (e) {
		catchErrorInComponent(e, newVNode._parent);
	}

	return newVNode._dom;
}
复制代码

剩下的 diffChildren 方法与 diffElementNodes 在上面render流程中已基本理清,这里不再赘述。

小结

总结一下上面的执行流程

  • 首先调用 setState ,其内部把需要更新的属性挂载到 c._nextState 属性上,然后将组件放入 enqueueRender 队列中
  • 当浏览器进行异步事件循环阶段时,会调用根据 enqueueRender 中每个组件的深度,从大到小依次调用组件的 forceUpdate 方法
  • 在组件的 forceUpdate 中,会调用 diff 方法重新渲染 c._vnode._dom DOM节点
    • 在diff方法中,会将 c._nextState 赋值给 c.state ,然后重新调用 c.render 方法,获取新的vnode节点,并通过 toChildArray 将新的 vnode.children 赋值为 newVNode._children
    • 在diffChildren中,会依次比较 newVNode._childrenoldVNode._children 所有子节点,如果节点不相同,则返回新的DOM节点;最后删除多余的旧节点
    • 将更新后的DOM节点挂载到parentDom上,并移除多余的旧DOM节点,完成页面渲染的更新

props

我们知道,babel会把 JSX 中标签上的属性转换成 props 属性,然后传入 createElement 函数,在上面的 diffElementNodes 中我们知道,当vnode节点发生改变时,会递归调用 diffChildren 比较子节点,此外,还会调用 diffProps 更新当前DOM节点的属性

// src/diff/index.js
function diffElementNodes(dom, newVNode, oldVNode, ...) {
	if (newVNode!==oldVNode) {
		diffChildren(dom, newVNode, oldVNode, ...);
		diffProps(dom, newProps, oldProps, isSvg);
	}
}
复制代码

前面我们只关注了 diffChildren ,接下来我们看看props是如何传递给子组件的,下面是 diffProps 的源码

export function diffProps(dom, newProps, oldProps, isSvg) {
	let i;

	const keys = Object.keys(newProps).sort();
	for (i = 0; i < keys.length; i++) {
		const k = keys[i];
		// 跳过一些特殊的属性名
		if (k!=='children' && k!=='key' && (!oldProps || ((k==='value' || k==='checked') ? dom : oldProps)[k]!==newProps[k])) {
			setProperty(dom, k, newProps[k], oldProps[k], isSvg);
		}
	}

	for (i in oldProps) {
		if (i!=='children' && i!=='key' && !(i in newProps)) {
			setProperty(dom, i, null, oldProps[i], isSvg);
		}
	}
}
复制代码

可见其内部是通过 setProperty 更新DOM属性的,针对于属性,我们需要着重关注一下DOM事件是如何绑定的

// src/diff/props.js
function setProperty(dom, name, value, oldValue, isSvg) {
	name = isSvg ? (name==='className' ? 'class' : name) : (name==='class' ? 'className' : name);
	if (name==='style') {
		// 修改样式...
	}
	// Benchmark for comparison: https://esbench.com/bench/574c954bdb965b9a00965ac6
	else if (name[0]==='o' && name[1]==='n') {
		// 处理onClick等事件
		let useCapture = name !== (name=name.replace(/Capture$/, ''));
		let nameLower = name.toLowerCase();
		name = (nameLower in dom ? nameLower : name).slice(2);

		// 注册事件函数
		if (value) {
			if (!oldValue) dom.addEventListener(name, eventProxy, useCapture);
			(dom._listeners || (dom._listeners = {}))[name] = value;
		}
		else {
			dom.removeEventListener(name, eventProxy, useCapture);
		}
	}
	else if (name!=='list' && name!=='tagName' && !isSvg && (name in dom)) {
		// ...特殊处理select和options
	}
	else if (typeof value!=='function' && name!=='dangerouslySetInnerHTML') {
		// 调用setAttribute和removeAttribute...
	}
}
复制代码

在组件更新时,如果相关的vnode上props属性发生了变化,则会进入 diffProps 的操作,DOM节点的属性就会随之更新。

小结

本文从一段简易的demo代码触发,分析了preact几段比较核心的代码,包括

  • Component 组件系统,包括组件的初始化,生命周期以及render函数的调用时机
  • createElement 方法,以及 vnode 的作用,可见在diff操作中,基本的思路是比较新旧两个vnode
  • setState 方法调用时,将组件放在队列中并调用 forceUpdate 来触发页面的更新
  • diff 操作的流程,包括 diffdiffChildrendiffElementNodesdiffProps 几个方法,了解页面是如何从vnode组成的AST转换成一颗真实的DOM树,可以看见diff过程中对于 vnode._dom 的复用
  • 分析了 props 系统以及事件注册,preact的事件是注册在对应的单个DOM节点上的,貌似存在事件委托的逻辑

整体来说, Preact 还是比较简单的,通过阅读源码,我们可以大致了解 React 的实现原理,接下来可以去了解一下 preact-routerpreact-redux ,这样对于学习React来说应该是有一定帮助的。最后,就应该去试试阅读 React 的源码了,毕竟只是当一个API使用者是远远不够的。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Effective JavaScript

Effective JavaScript

赫尔曼 (David Herman) / 黄博文、喻杨 / 机械工业出版社 / 2014-1-1 / CNY 49.00

Effective 系列丛书经典著作,亚马逊五星级畅销书,Ecma 的JavaScript 标准化委员会著名专家撰写,JavaScript 语言之父、Mozilla CTO —— Brendan Eich 作序鼎力推荐!作者凭借多年标准化委员会工作和实践经验,深刻辨析JavaScript 的内部运作机制、特性、陷阱和编程最佳实践,将它们高度浓缩为极具实践指导意义的 68 条精华建议。 本书共......一起来看看 《Effective JavaScript》 这本书的介绍吧!

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具