您的位置:首页 > Web前端 > JavaScript

爱奇艺RND框架之JS Framework解析

2019-09-27 00:00 447 查看

精选30+云产品,助力企业轻松上云!>>>

背景介绍

RND,全称React Node Desktop,起源于RN在爱奇艺PC端的实现,采用React JS framework + node.JS runtime + native UI engine架构,目标是成为最轻量的JS开发桌面应用的跨平台方案。目前爱奇艺PC客户端的大多数页面都是基于RND开发的。

传统的JS开发native应用的方案都是将native组件注入到JS,JS会按照native的开发模式开发应用,更多的是开发语言从C++换到了JS,开发思想还是native的。React JS带来了全新的开发思路,非常好地隔离了JS层和native层,业务开发基于React JS开发范式而不用受native约束。为了适配自研的lyra引擎以及为业务层提供更方便的开发设施,团队对React JS Framework做了深度的适配,接下来将带着大家深入了解React JS Framework,帮助大家理解这个优雅的view层框架。

React Fiber

撰写本篇文章时RN的最新版本是0.57.8,团队适配的RN版本是0.51.0,它依赖的React版本是16.0.0。 本文主要针对0.51.0进行说明,0.51.0与0.57.8的差别不大,基本原理是一样的。 React 16除了将备受争议的BSD+Patents协议改为MIT协议之外,还带来了许多新特性,比如:
  1. 允许在render函数中返回节点数组

  2. 提供更好的错误处理机制:componentDidCatch

  3. 支持自定义DOM属性

但最关键的一点还是React16是一次重写,在保持API不变的情况下,将核心架构改为了Fiber。 在介绍React的Fiber架构之前,首先介绍几个概念:
  1. React为每种类型的节点都分配了一个数字编号,具体为:

  2. 每一个控件都会对应一个fiber对象节点,所有的fiber节点就构成了virtualdom树,fiber对象的结构和表示的意义如下:

  3. 一个完整的virtualdom树的结构如下

节点树主要分为创建和更新两个过程,每个过程都可以分为以下四个阶段:

 

节点的创建

初始化阶段

RN程序的入口如下:

其中Index是需要渲染的根控件,runApplication函数会调用ReactNative.render函数,将业务的根节点包裹在AppContainer控件中传递给该函数:

其中RootComponent就是注册的根控件,AppContainer是RN提供的自定义控件。 在该函数中会创建一个HostRoot节点,该节点挂载在root对象的current属性上,root对象就是整个节点树的根。 接着会调用addTopLevelUpdate主动生成一个update,待更新数据就是传给ReactNative.render的参数,这个过程可以看做是RN内部主动调用了setState函数。 然后调用scheduleUpdate函数将待更新的根节点保存在nextScheduledRoot变量中。 在RN中更新都是从根节点开始的,无论setState函数在哪个控件上调用。 最后调用performWork函数进入workloop阶段。

workloop阶段

该阶段包含的主要函数及其调用关系如下:

首先根据HostRoot节点创建待操作节点(注意: RN不是直接处理当前节点,而是处理当前节点的拷贝,也就是节点的alternate属性),然后从该节点开始根据节点类型处理当前节点,也就是beginWork中的各种update方法,并且生成下一个待处理的节点,赋值给nextUnitOfWork。 如果nextUnitOfWork不为空,就对其进行处理,否则执行completeUnitOfWork,然后依次遍历处理该节点的兄弟节点和父节点的兄弟节点,直到父节点为空,循环结束。 不难发现RN是按照深度优先来创建和更新节点的。 具体的创建顺序如下图所示:

节点的update操作包含了对新节点的创建和已有节点更新两种情况,alternate为空是创建。 节点的处理主要包括三种: 自定义节点、根节点以及基础控件节点。 节点的创建和更新都是在响应节点的update函数中。 下面主要针对这三种节点进行说明:
1. updateHostRoot HostRoot节点不暴露给业务层,是RN内部使用的。前面说过在创建阶段addTopLevelUpdate函数中会生成一个update,保存在节点的updateQueue属性中,就是通过判断这个属性是否为空来区分是创建节点还是更新节点。如果updateQueue不为空,则取出AppContainer,开始创建AppContainer的fiber节点;如果updateQueue为空(更新阶段)就直接调用bailoutOnAlreadyFinishedWork获取已经创建好的AppContainer的fiber节点返回。 2. updateClassComponent ClassComponent是复合控件,也就是通过React.createClass(es5写法)函数创建的控件。控件的创建阶段主要执行三个函数:
  1. constructClassInstance

    从fiber节点的type属性中取得控件的构造函数,然后创建一个实例,保存在控件fiber节点的stateNode属性中;

  2. mountClassInstance

    执行实例的componentWillMount函数,如果实例的componentDidMount存在,更新effectTag,待所有子节点处理完毕后再执行;

  3. finishClassComponent

    先执行实例的render函数,然后根据render函数中的返回值执行reconcileChildren函数创建对应的fiber节点。

在更新阶段则会调用下面两个函数:
  1. updateClassInstance

    判断控件是否需要更新shouldUpdate;

    根据updateQueue,计算新的state;

    存在生命周期函数时标记effectTag

  2. finishClassComponent

    不需要更新时cloneChildFibers;

    需要更新时执行instance.render,然后执行reconcileChildren

3. updateHostComponent 这个函数中做的工作很少,在创建阶段调用reconcileChildren函数创建子节点的fiber节点并返回,更新阶段调用cloneChildFibers函数复制子节点并返回。 在处理节点过程中,如果遇到节点的子节点为空,那么就会调用completeUnitOfWork函数。 该函数根据节点类型进行相应处理:

如果是HostComponent,在创建阶段就会创建实例(createInstance),生成_nativeTag,生成createView命令,并且将子节点的HostComponent添加到实例的children属性中,发送setChildren命令添加节点,在更新阶段则标记是否需要更新。 如果是ClassComponent节点则无需处理,因为在update阶段已经处理完毕。 根据节点的effectTag值,向节点的firstEffect、nextEffect、lastEffect赋值,节点的firstEffect、nextEffect、lastEffect组成一个单链表结构,父节点会继承子节点的相应属性值,这些值会在接下来的commitAllWork阶段被处理。

commitAllWork

workloop阶段执行完毕就进入到commitAllWork阶段。 该阶段会调用以下两个函数:
  1. commitAllHostEffects 。 由节点的firstEffect开始遍历,根据effectTag值进行相应的操作,节点更新、插入、删除等。
  2. commitAllLifeCycles 。 改变root.current的值; 执行生命周期函数: componentDidMount、componentDidUpdate等; 执行ref函数。

节点的更新

节点更新阶段的处理流程如下图: 节点的update函数中已经包含了节点的创建和更新两种情况。 这里主要说一下更新的流程和初始化阶段。 节点的更新一般是通过实例的setState函数触发的,setState函数会调用Updater.enqueueSetState函数将需要更新的数据保存在fiber节点的updateQueue属性中,然后从当前节点开始向上更新父节点的优先级,更新到根节点结束。 然后从根节点开始进入workloop和commitAllWork阶段。

通信机制

RND中JS层与native层的通信与RN是类似的,具体的通信机制如下所示:

JS端的messageQueue模块负责消息的接收和发送,JS端产生的命令会存储在messageQueue中,最后通过调用native向JS注入的接口函数将命令发送到native端,native端的BatchedBridge类负责接收处理JS命令。 JS端将messageQueue的实例挂载到global对象上,native就能通过global对象访问到messageQueue中的所有实例属性和方法。 native通过EventEmitter类将消息发送到JS端,实现方式为在JS运行环境中获取到messageQueue实例动态执行代码段。 类似于浏览器中的window.eval函数的功能。 这样就完成了JS和native端的双向通信。

优化与扩展

RND在JS层面还进行了一些优化和扩展,主要集中在bundle拆分、css3动画支持、脚手架工具、typescript声明文件扩展等方面。 RN的bundle体积很大,在有多个页面实例时尤为突出。 因此考虑将RN框架代码单独分离出来,形成公共的base bundle,而将各页面的业务代码打包成各个jobbundle,从而减少了安装包体积和线上更新时的流量消耗。 我们在RND中增加了css3动画支持,应用程序可通过在style中指定符合css3动画规范的animation属性,即可实现高性能的动画效果。 类似于RN的react-native run-android命令,RND还扩展支持了run-desktop等脚手架命令。 最后,我们也为RND提供了ts声明文件,支持开发者使用ts进行开发。 未来团队还将陆续为RND增加一些新的组件和API,特别是与桌面开发相关的特性,例如WindowComponent、Shell API、File API等。

结语

至此React JS Framework的整个处理流程大致都说完了,本文目的是希望起到一个抛砖引玉的作用。 对于框架源码的分析有助于对框架本身有更深的理解,这样才能发现其本身的优点以及缺点,才能让我们在特定的使用场景中去取舍设计方案,去迭代、去优化、去创新。 RND在爱奇艺客户端的成功实践表明,RN同样适用于以运营内容为主的、迭代周期密集的互联网桌面应用,JS非常适合UI和业务逻辑的快速开发。随着各大JS引擎性能不断的优化,很多大厂都推出了基于JS语言的轻量级高性能App应用框架,可以预测在不久的将来, 以内容运营为主的桌面产品上,JS很快会成为最受开发者欢迎的语言之一

end

也许你还想看

搭建连接JS与C++的桥梁 - 爱奇艺RND框架之JS绑定

爱奇艺RND框架技术探索——架构与实现


扫一扫下方二维码,更多精彩内容陪伴你!

爱奇艺技术产品团队

简单想,简单做



本文分享自微信公众号 - 爱奇艺技术产品团队(iQIYI-TP)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  solid amp react reactjs