从 React 到 React 今天就谈 React (1)

879 阅读8分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前言

当前周边的大多数前端都在使用 Vue ,关于 Vue 自己了解的不多。回想一下在这之前自己也将近做的了3 5 年的前端。从 Jqeury 到 Angularjs 再到 React,放下前端后随后从事过一段 Android 开发,现在是一名 AI 工程师。可能是因为 javascript 这门语言引起我对编程兴趣,并且伴我多个那段困难时期,所以对于 javascript 还总是一些感情。

007.jpeg

想写一篇比较完整全面关于 React 的分享,然后将之前自己模仿去写个 React 捡起来,继续写一个 React 可以用于写个无人驾驶的界面。

可能无法一次将所有内容都罗列出来,说清楚,所以分享会持续更新,既往内容也会随着自己对 React 不断深入,不断更新

005.jpeg

什么是 React

React 是一个用于构建用户界面的 JavaScript 库。官方用 3 个短语给出 React 的特征,声明式、组件化和一次开发处处开花。这里我们就不引用官方对 3 个特点介绍了,用自己话解释解释一下他们都是什么以及为什么官方会拿出这 3 个特点来标注 React 呢。

声明式

什么是声明式呢,他又有什么好处? 这里举一个你熟悉可能还没有意识到他是声明式语言,在我们开始学前端时,第一个接触通常,或者一定是 html 这个超文本标记语言,他就是一个声明式的语言。学习 html 想必大家都很快吧,简单看看就可以 html 来定义页面结构了。这说明声明式语言对于我们人类来说更好理解,更友好。进一步说明,如果你对 html 有一定了解,当用 html 语言来定义了一个页面,无需浏览器根据 html 代码生成页面,你可能也会在脑子里大概有一个页面的样子,这就是声明式语言的好处。那么声明式语言好处更加可靠,且方便调试,而且具有很好可读性,好的可读性就意味着易于维护。

组件化

各种各样设计模式,设计模式的一个目标就是减少代码冗余。其实减少代码冗余不仅仅为了减少工作量,其实还不一定会减少工作量,而是为了好维护,这个我就不多说了,开发过大型项目的一定会深有感触,从项目里 copy 代码随后会带来什么。现在 web 项目越来越负责,将页面进行组件化化,将功能组件化,提供代码和逻辑的复用。

008.png

每个组件都是高内聚,组件间低耦合,组件间经过统一方式相互通讯交换信息。

一次学习,跨平台编写

这是不仅是 Facebook 的梦想,而且许多大公司梦想。Google 的 Flutter 也在做这件事,通常都会拿 Flutter 和 React 来对比,自己前年也写了几行 Flutter 代码,写起来也有一种在写 React 感觉。大多数新框架和语言都是拥抱未来,不忘过去,例如 这是这样成功的。那么为什么 React 能够做到这一点呢? 这是因为 vDom,这是个人一点理解,Java 之所以可以做到跨平台仅是因为有字节码和虚拟机。那么 React 跨平台也就是将页面表示抽象为 VDom,vDom 作为对视图描述,然后不同平台上只要实现渲染器就可以。

009.png

JSX

JSX 是一个 JavaScript 的语法扩展,好处就是更加直观。

const element = <h1 title="foo">hello</h1>

随后 Babel 会将 JSX 转换为 js 代码,将 html 标签转换 createElement 来创建元素。

const element = React.createElement(
    "h1",
    {title: "foo"},
    "Hello"
)

虚拟 DOM 和 diff 算法

React 的虚拟DOM 和 Diff 算法是 React 的非常重要的核心特性,没有记错的虚拟 DOM 的概念应该是在 React 首次提出的。所以这部分源码也非常复杂,理解这部分知识的原理对更深入的掌握 React 是非常必要的。在 16 版本 fiber 就对应一个虚拟 DOM。本次分享先保留,随后估计需要拿出一次分享来专门说一说这块内容。

002.jpeg

fiber

fiber 是小任务,也是数据结构,fiber,为什么叫做 fiber,进程是系统分配给应用内存资源的单位,线程是 CPU 调度的最小单位,都是到一个进程可以包含多个线程,那么 fiber 是纤维意思,其比 thread 还细小,所以就叫 fiber。

010.jpeg

运行 javascript 和渲染页面线程

这个问题源于 javascript 的设计,javascript 是单线程非阻塞的。所以 javascript 代码运行和页面的渲染都在一个线程上。异步也就是非阻塞是借助事件循环(event loop)这个机制实现,之前已经详细介绍,这里就不再过多介绍了。

在这个线程上,需要做许多事,例如事件处理,计时器,启动帧事件,还有 rAF 通常作为这些就开始渲染页面,渲染页面分为布局和绘制两个步骤。其实通常渲染动作优先级低于之前这些处理,随意渲染动作是紧随这些任务之后,但是浏览器很聪明,因为通常我们显示器都是 60 HZ 也就是一秒中刷新 60 次屏幕,那么也就是大概 16.6 毫秒会刷新一下屏幕,随意当其他任何很快被执行后,浏览器也不会离开执行渲染页面动作,而是保持大概 16.6 毫秒频率来更新页面。那么如果 javascript 代码执行时间过长一直占据主线程不放就会出现卡顿的现象。

011.jpeg

原先的 stack reconciler 像是一个递归执行的函数,从父组件调用子组件的 reconciler 过程就是一个递归执行的过程,这也是为什么被称为 stack reconciler 的原因。当我们调用 setState 的时候,react 从根节点开始遍历,找出所有的不同,而对于特别庞大的 dom 树来说,这个递归遍历的过程会消耗特别长的时间。在这个期间,任何交互和渲染都会被阻塞,这样就给用户一种“死机”的感觉。

001.png

这个分享源于网上一篇文章 Build your own React,这篇文章很好教你一步一步自己写一个 React,可以自己简单地写出一个 mini 版本的 React。其中介绍了 hooks 、filter 和 concurrent 模式实现, 麻雀虽小,五脏俱全。推荐大家自己写一遍,自己动手去写一方面了解 React 设计的美,另一方面也会给你在自己开发项目中找到点灵感。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="root"></div>
    <script src="index.js"></script>
</body>
</html>

例子中代码量不算大,所以一个 index.js 就够用,创建一个 index.html 文件,创建一个用于加载引用的根节点 <div id="root"></div> 最后再引用一下 index.js 这个开发环境或者说项目就创建完了。

const element = {
    type:"h1",
    props:{
        title:"foo",
        children:"Hello"
    }
}

const container = document.getElementById("root");
// ReactDOM.render(element, container)


const node = document.createElement(element.type);
node["title"] = element.props.title;

const text = document.createTextNode("");
text["nodeValue"] = element.props.children;

node.append(text);
container.append(node);

012.png

实现 createElement 函数

const element = (
    <div id="foo">
        <a>bar</a>
        <b/>
    </div>
)

const container = document.getElementById("root");
ReactDOM.render(element,container)

从上面 React 代码中我们可以出声明式语言的优点,易读性很好。

const element = React.createElement(
    "div",
    {id:"foo"},
    React.createElement("a",null,"bar"),
    React.createElement("b")
)

const container = document.getElementById("root");
ReactDOM.render(element,container);

React.createElement 接受 3 参数,第一个参数是结点类型,字符串类型,第二个参数是是一个对象类型,以键值对形式传入结点的属性名以及对应属性值,然后是该结点的子结点,可能是多个子结点。JSX 语法就是由 Babel 将其转换为 createElement 方法

function createElement(type, props, ...children){
    return{
        type,
        props:{
            ...props,
            children
        },
    }
}

miniReact = {
    createElement
}

const element = miniReact.createElement(
    "div",
    {id:"foo"},
    miniReact.createElement("a",null,"bar"),
    miniReact.createElement("b")
)

这里用 miniReact 作为我们要写框架的名称,然后将之前的 React 名称都对应替换为 miniReact ,然后 createElement

function createElement(type, props, ...children){
    return{
        type,
        props:{
            ...props,
            children: children.map(child => 
              typeof child === "object"
                ? child
                : createTextElement(child)  
                
            ),
        },
    }
}

function createTextElement(text){
    return {
        type: "TEXT_ELEMENT",
        props :{
            nodeValue: text,
            children: []
        }
    }
}

对 props 使用 spread 运算符,对 children 也使用了 spread 运算符这个 ES6 新特性,值得注意 children 的 prop 就总是一个数组。

function createTextElement(text){
    return {
        type: "TEXT_ELEMENT",
        props :{
            nodeValue: text,
            children: []
        }
    }
}

018.jpeg

render 函数

这里 render 方法是 React 的 commit 阶段,将更新完成虚拟 DOM 渲染到界面上。

function render(element, container){

}

miniReact = {
    createElement,
    render
}

Render 工作就是读取 element 将其转换为 html 的 Dom 元素,然后添加到容器结点。React 通过不同渲染引擎实现将 VMOD 渲染不同设备从而实现跨平台。


function render(element, container){
    console.log("render phase")
    const dom = document.createElement(element.type);
    container.appendChild(dom);
}

递归元素的子结点,看到递归大家就应该联想到对内存消耗和线程的占用,之前通过缓存或者替换使用动态规划方式来解决递归中的问题。

function render(element, container){
    // console.log("render phase")
    const dom = document.createElement(element.type);
    element.props.children.forEach(child => 
        render(child,dom)
    )
    container.appendChild(dom);
}
function render(element, container){
    // console.log("render phase")
    const dom = element.type == "TEXT_ELEMENT"
        ? document.createTextNode("")
        : document.createElement(element.type);
    element.props.children.forEach(child => 
        render(child,dom)
    )
    container.appendChild(dom);
}

在创建 Dom 元素时,对文本结点进行单独处理,所以这里稍作了特殊处理。在判断虚拟节点为 TEXT_ELEMENT 调用createTextNode方法来生成节点。

function render(element, container){
    // console.log("render phase")
    const dom = element.type == "TEXT_ELEMENT"
        ? document.createTextNode("")
        : document.createElement(element.type);

    const isProperty = key => key !== "children"
    Object.keys(element.props)
        .filter(isProperty)
        .forEach(name => {
            dom[name] = element.props[name]
        })
    element.props.children.forEach(child => 
        render(child,dom)
    )
    container.appendChild(dom);
}

今天暂时分享到这里,随后分享 Concurrent 模式实现,会谈到 fiber 这个算法是如何解决上面问题。

016.jpeg