MrErHu / blog

Star 就是最大的鼓励 👏👏👏
MIT License
605 stars 40 forks source link

从preact了解一个类React的框架是怎么实现的(一): 元素创建 #22

Open MrErHu opened 6 years ago

MrErHu commented 6 years ago

  首先欢迎大家关注我的掘金账号和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来引用propsstatecontext。语法方面我们不再多做赘述,现在正式开始我们的内容。

  本人还是非常推崇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对外提供两个接口: hcreateElement,都是指向函数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中删除。然后循环遍历栈中的每一个子元素:

  函数结束循环遍历之后,创建了一个VNODE,并将nodeNamechildrenattributeskey都赋值到节点中。需要注意的是,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、组件设计等概念展开,欢迎大家关注我的账号获得最新的文章动态。

st1991s commented 6 years ago

可以把h函数对应的虚拟dom对象写一个例子写明吗? 我看了一下好像不是react用的 {type:‘button’,props:{id:‘test’,children:{}}}这样的呢