首先欢迎大家关注我的掘金账号和Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励。
之前分享过几篇关于React的文章:
其实我在阅读React源码的时候,真的非常痛苦。React的代码及其复杂、庞大,阅读起来挑战非常大,但是这却又挡不住我们的React的原理的好奇。前段时间有人就安利过Preact,千行代码就基本实现了React的绝大部分功能,相比于React动辄几万行的代码,Preact显得别样的简洁,这也就为了我们学习React开辟了另一条路。本系列文章将重点分析类似于React的这类框架是如何实现的,欢迎大家关注和讨论。如有不准确的地方,欢迎大家指正。
关于Preact,官网是这么介绍的:
Fast 3kb React alternative with the same ES6 API. Components & Virtual DOM.
我们用Preact编写代码就雷同于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
。语法方面我们不再多做赘述,现在正式开始我们的内容。
本人还是非常推崇React这一套机制的,React这套机制提我们完成了数据和视图的绑定,使得开发人员只需要关注数据和数据流的改变,从而极大的降低的开发的关注度,使得我们能够集中精力于数据本身。而且React引入了虚拟DOM(virtual-dom)的机制,从而提升渲染性能。在开始接触React时,觉得虚拟DOM机制十分的高大上,但经过一段时间的学习,开始对虚拟DOM有了进一步的认识。虚拟DOM从本质上将就是将复杂的DOM转化成轻量级的JavaScript对象,不同的渲染中会生成同的虚拟DOM对象,然后通过高效优化过的Diff算法,比较前后的虚拟DOM对象,以最小的变化去更新真实DOM。
正如上面的图,其实类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转化其实会得到下面结果:
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
中删除。然后循环遍历栈中的每一个子元素:
首先判别该元素是不是数组类型,这里采用的就是鸭子类型(duck type),即看起来来一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子,我们在这里通过是否含有函数
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","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","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,h( Child,"Hello World!" ) ); //转化为的元素节点 { nodeName: ƒ App(argument),children: [ { nodeName: ƒ Child(argument),children: ["Hello World!"],attributes: undefined,key: undefined } ],key: undefined }
上面JSX元素转化成的JavaScript对象就是DOM在内存中的表现。在Preact中不同的数据会生成不同的虚拟DOM节点,通过比较前后的虚拟DOM节点,Preact会找出一种最简单的方式去更新真实DOM,以使其匹配当前的虚拟DOM节点,当然这会在后面的系列文章讲到,我们会将源码和概念分割成一块块内容,方便大家理解,这篇文章着重讲述了Preact的元素创建与JSX,之后的文章会继续围绕Preact类似于diff、组件设计等概念展开,欢迎大家关注我的账号获得最新的文章动态。