首先欢迎你们关注个人掘金帐号和Github博客,也算是对个人一点鼓励,毕竟写东西无法得到变现,能坚持下去也是靠的是本身的热情和你们的鼓励。
以前分享过几篇关于React的文章:javascript
其实我在阅读React源码的时候,真的很是痛苦。React的代码及其复杂、庞大,阅读起来挑战很是大,可是这却又挡不住咱们的React的原理的好奇。前段时间有人就安利过Preact,千行代码就基本实现了React的绝大部分功能,相比于React动辄几万行的代码,Preact显得别样的简洁,这也就为了咱们学习React开辟了另外一条路。本系列文章将重点分析相似于React的这类框架是如何实现的,欢迎你们关注和讨论。若有不许确的地方,欢迎你们指正。
关于Preact,官网是这么介绍的: java
Fast 3kb React alternative with the same ES6 API. Components & Virtual DOM.node
咱们用Preact编写代码就雷同于React,好比举个例子: react
import { Component , h } from 'preact'
export default class TodoList extends Component {
state = { todos: [], text: '' };
setText = e => {
this.setState({ text: e.target.value });
};
addTodo = () => {
let { todos, text } = this.state;
todos = todos.concat({ text });
this.setState({ todos, text: '' });
};
render({ }, { todos, text }) {
return (
<form onSubmit={this.addTodo} action="javascript:"> <input value={text} onInput={this.setText} /> <button type="submit">Add</button> <ul> { todos.map( todo => ( <li>{todo.text}</li> )) } </ul> </form> ); } }复制代码
上面就是用Preact编写TodoList的例子,掌握React的你是否是感受再熟悉不过了,上面的例子和React不太相同的地方是render
函数有参数传入,分别是render(props,state,context)
,其目的是为了你解构赋值方便,固然你仍然能够render
函数中经过this
来引用props
、state
和context
。语法方面咱们再也不多作赘述,如今正式开始咱们的内容。git
本人仍是很是推崇React这一套机制的,React这套机制提咱们完成了数据和视图的绑定,使得开发人员只须要关注数据和数据流的改变,从而极大的下降的开发的关注度,使得咱们可以集中精力于数据自己。并且React引入了虚拟DOM(virtual-dom)的机制,从而提高渲染性能。在开始接触React时,以为虚拟DOM机制十分的高大上,但通过一段时间的学习,开始对虚拟DOM有了进一步的认识。虚拟DOM从本质上将就是将复杂的DOM转化成轻量级的JavaScript对象,不一样的渲染中会生成同的虚拟DOM对象,而后经过高效优化过的Diff算法,比较先后的虚拟DOM对象,以最小的变化去更新真实DOM。github
正如上面的图,其实类React的框架的代码都基本能够分为两部分,组件到虚拟DOM的转化、以及虚拟DOM到真实DOM的映射。固然细节性的东西还有很是多,好比生命周期、事件机制(代理)、批量刷新等等。其实Preact精简了React中的不少部分,好比React中采用的是事件代理机制,Preact就没这么作。这篇文章将着重于叙述Preact的JSX与组件相关的部分代码。
最开始学习React的时候,觉得JSX是React的所独有的,如今其实明白了JSX语法并非某个库所独有的,而是一种JavaScript函数调用的语法糖。咱们举个例子,假若有下面的代码: 算法
import ReactDOM from 'react-dom'
const App = (props)=>(<div>Hello World</div>)
ReactDOM.render(<APP />, document.body);复制代码
请问能够执行吗?事实上是不能只能的,浏览器会告诉你:数组
Uncaught ReferenceError: React is not defined浏览器
若是你不了解JSX你就会感受奇怪,由于没有地方显式地调用React,可是事实上上面的代码确实用到了React模块,奥秘就在于JSX。JSX其实至关于JavaScript + HTML(也被称为hyperscript,即hyper + script,hyper是HyperText超文本的简写,而script是JavaScript的简写)。JSX并不属于新的语法,其目的也只是为了在JavaScript脚本中更方便的构建UI视图,相比于其余的模板语言更加的易于上手,提高开发效率。上面的实例若是通过Babel转化其实会获得下面结果: babel
var App = function App(props) {
return React.createElement(
'div',
null,
'Hello World'
);
};复制代码
咱们能够看到,以前的JSX语法都被转换成函数React.createElement
的调用方式。这就是为何在React中有JSX的地方都须要显式地引入React的缘由,也是为何说JSX只是JavaScript的语法糖。可是按照上面的说法,全部的JSX语法都会被转化成React.createElement
,那岂不是JSX只是React所独有的?固然不是,好比下面代码:
/** @jsx h */
let foo = <div id="foo">Hello!</div>;复制代码
咱们经过为JSX添加注释@jsx
(这也被成为Pragma,即编译注释),可使得Babel在转化JSX代码时,将其装换成函数h
的调用,转化结果成为:
/** @jsx h */
var foo = h(
"div",
{ id: "foo" },
"Hello!"
);复制代码
固然在每一个JSX上都设置Pragma是没有必要的,咱们能够在工程全局进行配置,好比咱们能够在Babel6中的.babelrc
文件中设置:
{
"plugins": [
["transform-react-jsx", { "pragma":"h" }]
]
}复制代码
这样工程中全部用到JSX的地方都是被Babel转化成使用h
函数的调用。
说了这么多,咱们开始了解一下Preact是怎么构造h
函数的(关于为何Preact将其称为h
函数,是由于做为hyperscript
的缩写去命名的),Preact对外提供两个接口: h
与createElement
,都是指向函数h
:
import {VNode} from './vnode';
const stack = [];
const EMPTY_CHILDREN = [];
export function h(nodeName, attributes) {
let children = EMPTY_CHILDREN, lastSimple, child, simple, i;
for (i = arguments.length; i-- > 2;) {
stack.push(arguments[i]);
}
if (attributes && attributes.children != null) {
if (!stack.length) stack.push(attributes.children);
delete attributes.children;
}
while (stack.length) {
if ((child = stack.pop()) && child.pop !== undefined) {
for (i = child.length; i--;) stack.push(child[i]);
}
else {
if (typeof child === 'boolean') child = null;
if ((simple = typeof nodeName !== 'function')) {
if (child == null) child = '';
else if (typeof child === 'number') child = String(child);
else if (typeof child !== 'string') simple = false;
}
if (simple && lastSimple) {
children[children.length - 1] += child;
}
else if (children === EMPTY_CHILDREN) {
children = [child];
}
else {
children.push(child);
}
lastSimple = simple;
}
}
let p = new VNode();
p.nodeName = nodeName;
p.children = children;
p.attributes = attributes == null ? undefined : attributes;
p.key = attributes == null ? undefined : attributes.key;
return p;
}复制代码
函数h
接受两个参数节点名nodeName
,与属性attributes
。而后将除了前两个以外的参数都压如栈stack。这种写法挺使人吐槽的,写成h(nodeName, attributes, ...children)
不是一目了然吗?由于h
的参数是不限的,从第三个参数起的全部参数都是节点的子元素,因此栈存储的是当前元素的子元素。而后会再排除一下第二个参数(其实就是props
)中是否含有children
属性,有的话也将其压如栈中,而且从attributes
中删除。而后循环遍历栈中的每个子元素:
pop
去判别是不是一个数组,若是子元素是一个数组,就将其所有压入栈中。为何这么作呢?由于子元素有多是数组,好比:render(){
return(
<ul> { [1,2,3].map((val)=><li>{val}</li>) } </ul>
)
}复制代码
由于子元素是不支持布尔类型的,所以将其置为: null
。 若是传入的节点不是函数的话,分别判断若是是null
,则置为空字符,若是是数字的话,将其转化成字符串类型。变量simple
用来记录节点是不是简单类型,好比dom
名称或者函数就不属于,若是是字符串或者是数字,就会被认为是简单类型
而后代码
if (simple && lastSimple) {
children[children.length - 1] += child;
}复制代码
其实作的就是一个字符串拼接,lastSimple是用来记录上次的节点是不是简单类型。之因此这么作,是由于某些编译器会将下面代码
let foo = <div id="foo">Hello World!</div>;复制代码
转化为:
var foo = h(
"div",
{ id: "foo" },
"Hello",
"World!"
);复制代码
这是时候h
函数就会将后两个参数拼接成一个字符串。
最后将处理子节点的传入数组children
中,如今传入children
中的节点有三种类型: 纯字符串、表明dom
节点的字符串以及表明组件的函数(或者是类)
函数结束循环遍历以后,建立了一个VNODE
,并将nodeName
、children
、attributes
、key
都赋值到节点中。须要注意的是,VNODE
只是一个普通的构造函数:
function VNode() {}复制代码
说了这么多,咱们看几个转化以后的例子:
//jsx
let foo = <div id="foo">Hello World!</div>;
//js
var Element = h(
"div",
{ id: "foo" },
"Hello World!"
);
//转化为的元素节点
{
nodeName: "div",
children: [
"Hello World!"
],
attributes: {
id: "foo"
},
key: undefined
}复制代码
/* jsx class App extends Component{ //.... } class Child extends Component{ //.... } */
let Element = <App><Child>Hello World!</Child></App>
//js
var Element = h(
App,
null,
h(
Child,
null,
"Hello World!"
)
);
//转化为的元素节点
{
nodeName: ƒ App(argument),
children: [
{
nodeName: ƒ Child(argument),
children: ["Hello World!"],
attributes: undefined,
key: undefined
}
],
attributes: undefined,
key: undefined
}复制代码
上面JSX元素转化成的JavaScript对象就是DOM在内存中的表现。在Preact中不一样的数据会生成不一样的虚拟DOM节点,经过比较先后的虚拟DOM节点,Preact会找出一种最简单的方式去更新真实DOM,以使其匹配当前的虚拟DOM节点,固然这会在后面的系列文章讲到,咱们会将源码和概念分割成一块块内容,方便你们理解,这篇文章着重讲述了Preact的元素建立与JSX,以后的文章会继续围绕Preact相似于diff、组件设计等概念展开,欢迎你们关注个人帐号得到最新的文章动态。