浅入理解跨端渲染:从零实现 React DSL 跨端渲染机制
前言
在移动应用开发领域,跨端技术已经成为主流选择。React Native、Flutter、Weex 等框架让我们能够用一套代码运行在多个平台上。不同的框架实现的原理不同,更多的总结对比可以看这篇博客大厂自研跨端框架技术揭秘。
笔者工作中使用的跨端框架叫做 Kun,是闲鱼基于 W3C 标准 & Flutter 打造的混合高性能终端容器,其原理与 React Native 相似:
- 使用 React 语法编写业务代码
- 编译打包成 JavaScript Bundle
- 在运行时通过桥接层渲染到各个平台
本文将通过一个极简的 Web 模拟案例,带你深入理解这种 React DSL 跨端渲染的核心机制。
跨端渲染的本质
跨端渲染的核心思想可以用一句话概括:用统一的 API 描述 UI,由框架负责在不同平台上完成渲染。
传统的移动端原生开发中,iOS 使用 UIKit,Android 使用 Android SDK,两者的 API 完全不同。而跨端框架通过引入一个中间层,让开发者用统一的方式描述 UI,然后由框架负责处理平台差异——可能是映射到原生组件(如 React Native、Kun),也可能是自己绘制 UI(如 Flutter),或是通过 WebView 渲染(如 H5、各家小程序方案等)。
架构分层
一个典型的基于 React DSL 的跨端框架包含三个核心层次:
┌─────────────────────────────────┐
│ 业务逻辑层 (JavaScript) │ ← 开发者编写的代码
├─────────────────────────────────┤
│ 桥接层 (Bridge) │ ← 通信中枢
├─────────────────────────────────┤
│ 渲染层 (Native) │ ← 平台原生渲染
└─────────────────────────────────┘
完整的原理链路如下:
![]()
在编译时,可以通过 React DSL 脚手架工具,将 JSX 转化成 createElement 形式。最终的产物可以理解成一个 JS 文件,可以称之为 JSBundle。
重点来了,在运行时,我们分别从 web 应用 和 Native 应用 两个角度来解析流程:
-
如果是 React DSL web 应用,那么可以通过浏览器加载 JSBundle ,然后通过运行时的 api 将页面结构,转化成虚拟 DOM , 虚拟 DOM 再转化成真实 DOM, 然后浏览器可以渲染真实 DOM 。
-
如果是 React DSL Native 应用,那么 Native 会通过一个 JS 引擎来运行 JSBundle ,然后同样通过运行时的 API 转化成虚拟 DOM, 接下来因为 Native 应用,所以不能直接转化的 DOM, 这个时候可以生成一些绘制指令,可以通过桥的方式,把指令传递给 Native 端,Native 端接收到指令之后,就可以绘制页面了。这样的好处就可以动态上传 bundle ,来实现动态化更新(个人认为没有动态化更新的跨端框架是没有灵魂的)。
核心概念解析
1. 虚拟 DOM (Virtual DOM)
虚拟 DOM 是对真实 UI 的轻量级描述,它是一个纯 JavaScript 对象树。
// 虚拟 DOM 节点结构
{
tag: 'View', // 组件类型
props: { id: 'root' }, // 属性
children: [ // 子节点
{
tag: 'Text',
props: { text: 'Hello World' },
children: []
}
]
}
为什么需要虚拟 DOM?
- 性能优化:直接操作原生 UI 成本高,虚拟 DOM 可以批量计算变更
- 跨平台抽象:提供统一的 UI 描述方式
- Diff 算法:通过对比新旧虚拟 DOM,最小化实际渲染操作
2. 桥接通信 (Bridge)
桥接层是 JavaScript 层和 Native 层之间的通信管道。
// Native → JS: 事件传递
bridge.sendToJS({
type: 'EVENT',
payload: {
eventName: 'handleClick',
params: { x: 100, y: 200 }
}
});
// JS → Native: 渲染指令
bridge.sendToNative({
type: 'RENDER',
payload: [
{ type: 'CREATE', payload: {...} },
{ type: 'UPDATE', payload: {...} }
]
});
桥接通信的特点:
- 异步通信:避免阻塞主线程
- 序列化传输:数据需要序列化为 JSON
- 双向通道:支持 JS ↔ Native 双向消息传递
3. 渲染指令 (Render Instructions)
渲染指令是 JavaScript 层告诉 Native 层"如何渲染"的命令集。
// 三种基本指令类型
const instructions = [
{
type: 'CREATE', // 创建新元素
payload: {
id: 'vdom_1',
tag: 'View',
props: {},
parentId: null
}
},
{
type: 'UPDATE', // 更新已有元素
payload: {
id: 'vdom_2',
props: { text: 'New Text' }
}
},
{
type: 'DELETE', // 删除元素
payload: {
id: 'vdom_3'
}
}
];
完整渲染流程
让我们通过一个完整的交互流程,理解整个渲染机制:
阶段一:初始化渲染
1. Native 层启动
↓
2. 初始化 JS 引擎(JSCore/V8/Hermes)
↓
3. 加载并执行 JavaScript 代码
↓
4. JS 层创建虚拟 DOM 树
↓
5. 生成渲染指令
↓
6. 通过 Bridge 发送到 Native
↓
7. Native 层执行渲染指令
↓
8. 显示 UI
代码示例:
// JS 层:初始化渲染
class ReactDSL {
mount() {
// 1. 执行 render 函数生成虚拟 DOM
const vdom = this.render();
// 2. 转换为渲染指令
const instructions = this.vdomToInstructions(vdom);
// 3. 发送到 Native
this.sendToNative({
type: 'RENDER',
payload: instructions
});
}
render() {
return this.createElement(
'View',
{},
this.createElement('Text', {
text: '欢迎使用 React Native'
})
);
}
}
阶段二:交互更新
1. 用户点击按钮
↓
2. Native 层捕获事件
↓
3. 通过 Bridge 发送事件到 JS 层
↓
4. JS 层执行事件处理函数
↓
5. 更新状态 (setState)
↓
6. 重新执行 render 生成新虚拟 DOM
↓
7. Diff 算法对比新旧虚拟 DOM
↓
8. 生成最小化的更新指令
↓
9. 通过 Bridge 发送到 Native
↓
10. Native 层执行更新指令
↓
11. UI 更新完成
代码示例:
// JS 层:状态更新流程
class ReactDSL {
handleIncrement() {
// 1. 更新状态
this.setState({ count: this.state.count + 1 });
}
setState(newState) {
this.state = { ...this.state, ...newState };
// 2. 触发更新
this.update();
}
update() {
// 3. 生成新虚拟 DOM
const newVDOM = this.render();
// 4. Diff 算法
const instructions = this.diff(this.currentVDOM, newVDOM);
// 5. 更新缓存
this.currentVDOM = newVDOM;
// 6. 发送更新指令
if (instructions.length > 0) {
this.sendToNative({
type: 'RENDER',
payload: instructions
});
}
}
}
Diff 算法简析
Diff 算法是跨端渲染的性能关键。它的目标是:找出新旧虚拟 DOM 的最小差异。
简化版 Diff 实现
diff(oldVDOM, newVDOM) {
const instructions = [];
// 策略1:如果节点类型不同,直接替换
if (oldVDOM.tag !== newVDOM.tag) {
instructions.push(
{ type: 'DELETE', payload: { id: oldVDOM.id } },
{ type: 'CREATE', payload: newVDOM }
);
return instructions;
}
// 策略2:对比属性变化
const propsChanged = this.diffProps(oldVDOM.props, newVDOM.props);
if (propsChanged) {
instructions.push({
type: 'UPDATE',
payload: {
id: oldVDOM.id,
props: newVDOM.props
}
});
}
// 策略3:递归对比子节点
const childInstructions = this.diffChildren(
oldVDOM.children,
newVDOM.children
);
instructions.push(...childInstructions);
return instructions;
}
React 的 Diff 优化策略
- 同层比较:只比较同一层级的节点,不跨层级
- Key 优化:通过 key 快速识别节点移动
- 类型判断:不同类型的组件直接替换,不深入比较
原理最简化实现及实战案例
下面用一个非常简单案例,来用前端的方式模拟 React DSL Native 渲染流程。
![]()
- index.html 为视图层, 这里用视图层模拟代替了 Native 应用。
- bridge 为 JS 层和 Native 层的代码。
- service.js 为我们写在 js 业务层的代码。
核心流程如下:
- 本质上 service.js 运行在 Native 的 JS 引擎中,形成虚拟 DOM ,和绘制指令。
- 绘制指令可以通过 bridge 传递给 Native 端 (案例中的 html 和 js ),然后渲染视图。
- 当触发更新时候,Native 端响应事件,然后把事件通过桥方式传递给 service.js, 接下来 service.js 处理逻辑,发生 diff 更新,产生新的绘制指令,通知给 Native 渲染视图。
因为这个案例是用 web 应用模拟的 Native ,所以实现细节和真实场景有所不同,尽请谅解,本案例主要让读者更清晰了解渲染流程。
完整代码在仓库,直接运行仓库下面的index.html即可看到相关效果:
![]()
让我们通过一个完整的计数器应用,串联所有知识点:
// service.js - 业务逻辑层
class CounterApp {
constructor() {
this.state = { count: 0 };
}
// 渲染函数
render() {
return this.createElement(
'View',
{},
this.createElement('Text', {
text: `计数: ${this.state.count}`
}),
this.createElement('Button', {
title: '增加',
onPress: 'handleIncrement'
})
);
}
// 事件处理
handleIncrement() {
this.setState({ count: this.state.count + 1 });
}
}
执行流程分析:
-
初始渲染:
- 生成虚拟 DOM:
View -> [Text, Button] - 转换为 3 条 CREATE 指令
- Native 创建对应的原生组件
- 生成虚拟 DOM:
-
点击按钮:
- Native 捕获点击事件
- 发送
EVENT消息到 JS - JS 执行
handleIncrement - 状态从
{count: 0}变为{count: 1} - 重新 render 生成新虚拟 DOM
- Diff 发现 Text 的 text 属性变化
- 生成 1 条 UPDATE 指令
- Native 更新 Text 组件显示
总结
通过本文的剖析,我们了解了 React DSL 跨端渲染的核心机制:
- 虚拟 DOM 提供了统一的 UI 描述方式
- Bridge 实现了 JS 与 Native 的通信桥梁
- Diff 算法 最小化了实际的渲染操作
- 渲染指令 将 UI 变更转换为平台操作
当然,用于生产环境的跨端框架在实现上会有更多细节和优化,使用的具体技术也可能不同,但核心原理是一致的。
跨端技术的本质是在性能和开发效率之间找到平衡。理解其底层原理,能帮助我们:
- 写出更高性能的跨端应用
- 更好地调试和优化问题
- 为技术选型提供依据