写点什么

爱奇艺 RND 框架之 JS Framework 解析

2019 年 10 月 09 日

爱奇艺RND框架之JS Framework解析

背景介绍

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 函数中返回节点数组



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

  2. 支持自定义 DOM 属性


但最关键的一点还是 React16 是一次重写,在保持 API 不变的情况下,将核心架构改为了 Fiber。在介绍 React 的 Fiber 架构之前,首先介绍几个概念:


  1. React 为每种类型的节点都分配了一个数字编号,具体为:



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



  1. 一个完整的 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 写法)函数创建的控件。控件的创建阶段主要执行三个函数:


a. constructClassInstance


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


b. mountClassInstance


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


c. finishClassComponent


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


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


a. updateClassInstance


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


根据 updateQueue,计算新的 state;


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


b. 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 阶段被处理。


commitAll

Workworkloop 阶段执行完毕就进入到 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 很快会成为最受开发者欢迎的语言之一


本文转载自公众号爱奇艺技术产品团队(ID:iQIYI-TP)


原文链接


https://mp.weixin.qq.com/s?__biz=MzI0MjczMjM2NA==&mid=2247485562&idx=1&sn=dc46b97ec66111f44ee6be52f10a028b&chksm=e9769459de011d4fbf5366f9838e9e61dfeea968567c7570bd50846d25b47997382cf71b1e30&scene=27#wechat_redirect


2019 年 10 月 09 日 08:00840

评论

发布
暂无评论
发现更多内容

食堂就餐卡系统设计

跳蚤

如何设计一个高性能网关?

程序员小毕

Java GitHub 架构 高性能 网关

物联网基础知识整理及实战

garlic

物联网

编程常用的加密方式

皮蛋

加密 加解密 加密技术

第六周 技术选型(二) 课后作业

简简单单

架构师训练营第一周作业-学习总结

阿德儿

元旦首献!腾讯高工甩出的“MyBatis源码解析”传授你年薪百万级干货!

比伯

Java 编程 架构 面试 技术宅

LeetCode题解:264. 丑数 II,二叉堆,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

GitHub标星127K!字节内部必刷“微服务架构设计模式”

Java成神之路

Java 程序员 架构 面试 编程语言

架构师训练营第一周作业-命题作业

阿德儿

架构师培训第一周学习总结

跳蚤

[架构师训练营] 第一周学习总结

Trent

架构师训练营 4 期

架构师训练营第十一周作业2

韩儿

國際網路 商業因果-緣起

因田木

我把这份“Redis实战文档”扔给我们村口“大黄狗”看,它看完之后竟一去不复返?

Java成神之路

Java 程序员 架构 面试 编程语言

Week11总结

lggl

[架构师训练营] 食堂就餐卡系统设计

Trent

架构师训练营 4 期

第十周-学习总结

Mr_No爱学习

智慧平安小区平台开发,大数据分析决策平台建设方案

WX13823153201

智慧平安小区平台开发

最全总结 | 聊聊 Python 数据处理全家桶(Mysql 篇)

星安果

Python MySQL 数据库 最全总结

辞幕2020,前行2021

iHTC

中年危机 程序员赚钱 提升自我 年终总结 技术学习

架构师训练营第十一周作业1

韩儿

AOP的姿势之 简化 MemoryCache 使用方式

八苦-瞿昙

C# aop cache

架构师系列 12 单向散列加密算法对用户密码加密

桃花原记

十二周作业

solike

指令重排序、内存屏障很难?看完这篇你就懂了!

Java鱼仔

Java 程序员 面试 JMM 指令重排序

知识改变命运,你相信这句话吗?

熊斌

成长 演讲 教育

2020年金九银十面试总结,至今最全的Java程序员高频面试知识点解析笔记

Java成神之路

Java 程序员 架构 面试 编程语言

第十周-作业1

Mr_No爱学习

性能优化-1-压测

raox

架构训练营第十一周作业

一期一会

高可用架构

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

爱奇艺RND框架之JS Framework解析-InfoQ