从Preact中了解React组件和hooks基本原理

本文深入剖析Preact的Virtual-DOM、组件和Hooks的实现原理,通过源码分析帮助读者理解React的基本概念。从createElement、Component的实现到diff算法的细节,包括useState和useEffect的实现,旨在提供React开发的底层洞察,适合有一定基础的学习者巩固提升。
摘要由CSDN通过智能技术生成

640?wx_fmt=gif

作者:荒山

https://juejin.im/post/5cfa29e151882539c33e4f5e

React 的代码库现在已经比较庞大了,加上 v16 的 Fiber 重构,初学者很容易陷入细节的汪洋大海,搞懂了会让人觉得自己很牛逼,搞不懂很容易让人失去信心, 怀疑自己是否应该继续搞前端。那么尝试在本文这里找回一点自信吧(高手绕路).

Preact 是 React 的缩略版, 体积非常小, 但五脏俱全. 如果你想了解 React 的基本原理, 可以去学习学习 Preact 的源码, 这也正是本文的目的。

关于 React 原理的优秀的文章已经非常多, 本文就是老酒装新瓶, 算是自己的一点总结,也为后面的文章作一下铺垫吧.

文章篇幅较长,阅读时间约 20min,主要被代码占据,另外也画了流程图配合理解代码。

注意:代码有所简化,忽略掉 svg、replaceNode、context 等特性 本文代码基于 Preact v10 版本

  • Virtual-DOM

  • 从 createElement 开始

  • Component 的实现

  • diff 算法

    • diffChildren
    • diff
    • diffElementNodes
    • diffProps
  • Hooks 的实现

    • useState
    • useEffect
  • 技术地图

  • 扩展

Virtual-DOM

640?wx_fmt=png

Virtual-DOM 其实就是一颗对象树,没有什么特别的,这个对象树最终要映射到图形对象. Virtual-DOM 比较核心的是它的diff算法.

你可以想象这里有一个DOM映射器,见名知义,这个’DOM 映射器‘的工作就是将 Virtual-DOM 对象树映射浏览器页面的 DOM,只不过为了提高 DOM 的'操作性能'. 它不是每一次都全量渲染整个 Virtual-DOM 树,而是支持接收两颗 Virtual-DOM 对象树(一个更新前,一个更新后), 通过 diff 算法计算出两颗 Virtual-DOM 树差异的地方,然后只应用这些差异的地方到实际的 DOM 树, 从而减少 DOM 变更的成本.

Virtual-DOM 是比较有争议性,推荐阅读《网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?》 。切记永远都不要离开场景去评判一个技术的好坏。当初网上把 React 吹得多么牛逼, 一些小白就会觉得 Virtual-DOM 很吊,JQuery 弱爆了。

我觉得两个可比性不大,从性能上看, 框架再怎么牛逼它也是需要操作原生 DOM 的,而且它未必有你使用 JQuery 手动操作 DOM 来得'精细'. 框架不合理使用也可能出现修改一个小状态,导致渲染雪崩(大范围重新渲染)的情况; 同理 JQuery 虽然可以精细化操作 DOM, 但是不合理的 DOM 更新策略可能也会成为应用的性能瓶颈. 所以关键还得看你怎么用.

那为什么需要 Virtual-DOM?

我个人的理解就是为了解放生产力。现如今硬件的性能越来越好,web 应用也越来越复杂,生产力也是要跟上的. 尽管手动操作 DOM 可能可以达到更高的性能和灵活性,但是这样对大部分开发者来说太低效了,我们是可以接受牺牲一点性能换取更高的开发效率的.

所以说 Virtual-DOM 更大的意义在于开发方式的改变: 声明式、 数据驱动, 让开发者不需要关心 DOM 的操作细节(属性操作、事件绑定、DOM 节点变更),也就是说应用的开发方式变成了view=f(state), 这对生产力的解放是有很大推动作用的.

当然 Virtual-DOM 不是唯一,也不是第一个的这样解决方案. 比如 AngularJS, Vue1.x 这些基于模板的实现方式, 也可以说实现这种开发方式转变的. 那相对于他们 Virtual-DOM 的买点可能就是更高的性能了, 另外 Virtual-DOM 在渲染层上面的抽象更加彻底, 不再耦合于 DOM 本身,比如可以渲染为 ReactNative,PDF,终端 UI 等等。

从 createElement 开始

很多小白将 JSX 等价为 Virtual-DOM,其实这两者并没有直接的关系, 我们知道 JSX 不过是一个语法糖.

例如<a href="/"><span>Home</span></a>最终会转换为h('a', { href:'/' }, h('span', null, 'Home'))这种形式, h是 JSX Element 工厂方法.

h 在 React 下约定是React.createElement, 而大部分 Virtual-DOM 框架则使用h. hcreateElement 的别名, Vue 生态系统也是使用这个惯例, 具体为什么没作考究(比较简短?)。

可以使用@jsx注解或 babel 配置项来配置 JSX 工厂:

    /**
     * @jsx h
     */
    render(<div>hello jsx</div>, el);

本文不是 React 或 Preact 的入门文章,所以点到为止,更多内容可以查看官方教程.

现在来看看createElement, createElement 不过就是构造一个对象(VNode):

    // ⚛️type 节点的类型,有DOM元素(string)和自定义组件,以及Fragment, 为null时表示文本节点exportfunctioncreateElement(type, props, children) {
      props.children = children;
      // ⚛️应用defaultPropsif (type != null && type.defaultProps != null)
        for (let i in type.defaultProps)
          if (props[i] === undefined) props[i] = type.defaultProps[i];
      let ref = props.ref;
      let key = props.key;
      // ...// ⚛️构建VNode对象return createVNode(type, props, key, ref);
    }
    
    exportfunctioncreateVNode(type, props, key, ref) {
      return { type, props, key, ref, /* ... 忽略部分内置字段 */constructor: undefined };
    }

通过 JSX 和组件, 可以构造复杂的对象树:

    render(
      <divclassName="container"><SideBar /><Body /></div>,
      root,
    );

Component 的实现

对于一个视图框架来说,组件就是它的灵魂, 就像函数之于函数式语言,类之于面向对象语言, 没有组件则无法组成复杂的应用.

组件化的思维推荐将一个应用分而治之, 拆分和组合不同级别的组件,这样可以简化应用的开发和维护,让程序更好理解. 从技术上看组件是一个自定义的元素类型,可以声明组件的输入(props)、有自己的生命周期和状态以及方法、最终输出 Virtual-DOM 对象树, 作为应用 Virtual-DOM 树的一个分支存在.

Preact 的自定义组件是基于 Component 类实现的. 对组件来说最基本的就是状态的维护, 这个通过 setState 来实现:

    functionComponent(props, context) {}
    
    // ⚛️setState实现
    Component.prototype.setState = function(update, callback) {
      // 克隆下一次渲染的State, _nextState会在一些生命周期方式中用到(例如shouldComponentUpdate)let s = (this._nextState !== this.state && this._nextState) ||
        (this._nextState = assign({}, this.state));
    
      // state更新if (typeof update !== 'function' || (update = update(s, this.props)))
        assign(s, update);
    
      if (this._vnode) { // 已挂载// 推入渲染回调队列, 在渲染完成后批量调用if (callback) this._renderCallbacks.push(callback);
        // 放入异步调度队列
        enqueueRender(this);
      }
    };

enqueueRender 将组件放进一个异步的批执行队列中,这样可以归并频繁的 setState 调用,实现也非常简单:

    let q = [];
    // 异步调度器,用于异步执行一个回调const defer = typeofPromise == 'function'
        ? Promise.prototype.then.bind(Promise.resolve()) // micro task
        : setTimeout; // 回调到setTimeoutfunctionenqueueRender(c) {
      // 不需要重复推入已经在队列的Componentif (!c._dirty && (c._dirty = true) && q.push(c) === 1)
        defer(process); // 当队列从空变为非空时,开始调度
    }
 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值