React的源码多达几万行,对于咱们想要快速阅读并看懂是至关有难度的,而Preact是一个轻量级的类react库,几千行代码就实现了react的大部分功能。所以阅读preact源码,对于咱们学习react的思想并增强认识是很是有用的。javascript
本文的仓库在github上,持续更新中。欢迎大佬们star或提意见。html
下面是正文部分java
源码结构node
Preact导出的函数结构 react
import { h, h as createElement } from './h';
import { cloneElement } from './clone-element';
import { Component } from './component';
import { render } from './render';
import { rerender } from './render-queue';
import options from './options';
/**
* h函数和createElement函数是同一个函数
*
* */
export default {
h,
createElement,
cloneElement,
Component,
render,
rerender,
options
};
export {
h,
createElement,
cloneElement,
Component,
render,
rerender,
options
};
复制代码
jsx要转化成virtualDOM,首先通过babel,再通过h函数的调用造成virtualDOM。具体以下git
源码连接 src/h.jsgithub
至关于react得createElement(),jsx通过babel转码后是h的循环调用,生成virtualDOM。算法
// jsx
<div>
<span className="sss" fpp="xxx">123</span>
<Hello/>
<span>xxx</span>
</div>
// h结果
h(
"div",
null,
h(
"span",
{ className: "sss", fpp: "xxx" },
"123"
),
h(Hello, null),
h(
"span",
null,
"xxx"
)
);
复制代码
经过源码中h的函数定义也能够看见。h的函数第一个参数是标签名(若是是组件类型的化就是组件名)、第二个参数是属性值的key-value对象,后面的参数是全部子组件。数组
vnode的结构bash
h函数会根据子组件的不一样类型进行封装,具体以下
最后赋值给child变量并存进childdren数组中,再封装成下面的vnode结构并返回
{
nodeName:"div",//标签名
children:[],//子组件组成的数组,每一项也是一个vnode
key:"",//key
attributes:{}//jsx的属性
}
复制代码
// 一个简单的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主要进行了如下操做
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;
}
复制代码
紧接上节,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的过程当中最重要的函数就是createComponent
和setComponentProps
。咱们能够发现,在前后执行了createComponent
和setComponentProps
后,真实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;// 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
主要逻辑简单来讲以下: 一、调用组件实例的render方法去产生vnode。
二、若是这个组件产生的vnode再也不是组件了。则经过diff
函数去产生真实dom并挂载(前面已经分析过)diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, true);
。
三、若是这个组件的子vnode仍是子组件的话。则再次调用setComponentProps
、renderComponent
去进一步生成真实dom,直到2中条件成立。(判断步骤和二、3相似),可是有点区别的是。这种调用代码是
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(。。。);// 挂载
}
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的分析就结束了。
流程图以下
setState(state, callback) {
let s = this.state;
if (!this.prevState) this.prevState = extend({}, s);
extend(s, typeof state==='function' ? state(s, this.props) : state);// 语句3
if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
enqueueRender(this);
},
复制代码
setState的定义如上,代码逻辑很容易看出
一、prevState若不存在,将要更新的state合并到prevState上
二、能够看出Preact中setState参数也是能够接收函数做为参数的。将要更新的state合并到当前的state
三、若是提供了回调函数,则将回调函数放进_renderCallbacks
队列
四、调用enqueueRender进行组件更新
why?我刚看到setState的第二、3行代码的时候也是一脸蒙蔽。为何它要这样又搞一个this.prevState
又搞一个this.state
,又有个state
呢?WTF。 经过理清Preact的setState的执行原理。
应该是用于处理一个组件在一次流程中调用了两次setState的状况。
// 例如这里的handleClick是绑定click事件
handleClick = () =>{
// 注意,preact中setState后state的值是会立刻更新的
this.setState({a:this.state.a+1});
console.log(this.state.a);
this.setState({a:this.state.a+1});
console.log(this.state.a);
}
复制代码
基本上每个学react的人,都知道上述代码函数在react中执行以后a的值只会加一,but!!!!在Preact中是加2的!!!!经过分析Preact的setState能够解释这个缘由。 在上面的语句3,extend函数调用后,当前的state值已经改变了。可是即便state的值改变了,可是屡次setState仍然是会只进行一次组件的更新(经过setTimeout把更新操做放在当前事件循环的最后),以最新的state为准。因此,这里的prevState应该是用于记录当前setState以前的上一次state的值,用于后面的diff计算。在enqueueRender执行diff时比较prevState和当前state的值
关于enqueueRender的相关定义
let items = [];
export function enqueueRender(component) {
// dirty 为true代表这个组件从新渲染
if (!component._dirty && (component._dirty = true) && items.push(component) == 1) {//语句1
// 只会执行一遍
(options.debounceRendering || defer)(rerender); // 至关于setTimeout render 语句2
}
}
export function rerender() {
let p, list = items;
items = [];
while ((p = list.pop())) {
if (p._dirty) renderComponent(p);
}
}
复制代码
enqueueRender的逻辑主要是
一、语句1: 将调用了setState
的组件的_dirty
属性设置为false。经过这段代码咱们还能够发现, 若是在一次流程中,调用了屡次setState,rerender函数实际上仍是只执行了一遍(经过判断component._dirty的值来保证一个组件内的屡次setState只执行一遍rerender和判断items.push(component) == 1
确保若是存在父组件调用setState,而后它的子组件也调用了setState,仍是只会执行一次rerender)。items队列是用来存放当前全部dirty组件。
二、语句2。能够看做是setTimeout
,将rerender
函数放在本次事件循环结束后执行。rerender
函数对全部的dirty组件执 行renderComponent
进行组件更新。
在renderComponent中将会执行的代码。只列出和初次渲染时有区别的主要部分
export function renderComponent(component, opts=undefined, mountAll=undefined, isChild=undefined) {
....
if (isUpdate) {
component.props = previousProps;
component.state = previousState;
component.context = previousContext;
if (opts !== FORCE_RENDER && // FORCE_RENDER是在调用组件的forceUpdate时设置的状态位
component.shouldComponentUpdate &&
component.shouldComponentUpdate(props, state, context) === false) {
skip = true;// 若是shouldComponentUpdate返回了false,设置skip标志为为true,后面的渲染部分将会被跳过
} else if (component.componentWillUpdate) {
component.componentWillUpdate(props, state, context);//执行componentWillUpdate生命周期函数
}
// 更新组件的props state context。由于componentWillUpdate里面有可能再次去修改它们的值
component.props = props;
component.state = state;
component.context = context;
}
....
component._dirty = false;
....
// 省略了diff渲染和dom更新部分代码
...
if (!skip) {
if (component.componentDidUpdate) {
//componentDidUpdate生命周期函数
component.componentDidUpdate(previousProps, previousState, previousContext);
}
}
if (component._renderCallbacks != null) {
// 执行setState的回调
while (component._renderCallbacks.length) component._renderCallbacks.pop().call(component);
}
}
复制代码
逻辑看代码注释就很清晰了。先shouldComponentUpdate
生命周期,根据返回值决定是都否更新(经过skip标志位)。而后将组件的_dirty设置为true代表已经更新了该组件。而后diff组件更新,执行componentDidUpdate
生命周期,最后执行setState传进的callback。
流程图以下:
下一步,就是研究setState组件进行更新时的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);
一、字符串或者布尔型 若是以前也是一个文本节点,则直接修改节点的nodeValue的值;不然,建立一个新节点,并取代旧节点。并调用recollectNodeTree
对旧的dom进行腊鸡回收。
二、html的标签类型
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
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操做后,把keyed
和children
中剩余的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完成了