方便记忆:
- React原理:一套可以用简洁的语法高效绘制 DOM 的框架
- React特点:
- 简洁:不单单指它的 HTML 和 CSS 语法,更因为可以单用 JavaScript 构造页面
- 高效:因为 React 独创了 Virtual DOM 机制,两大特征
- 它存在于内存中的 JavaScript 对象,并且与 DOM 是对应关系
- 使用高效的 DOM Diff 算法不需要对 DOM 进行重新绘制
- React Native原理:通过 JS 对 OC 的 JavaScript Core 框架的交互来实现对原生的调用
- rn 在 OC 和 JS 两端都保存了一份配置表,里面标记了所有 OC 暴露给 JS 的模块和方法 ,js对oc的调用通过block方式实现回调
- AppDelegate初始化过程中创建bridge,内部通过setUp创建BatchedBridge来批量读取 JS 对 OC 的方法调用并通过JavaScriptExecutor执行 JS 代码
- 创建 BatchedBridge 步骤
- 读取 JS 源码:把 JSX 代码转成 JS 加载进内存中
- 初始化模块信息:找到所有需要暴露给 JS 的类
- 初始化 JS 代码的执行器:即 RCTJSCExecutor 对象
- 生成模块列表并写入 JS 端:接受 ModuleName 并且生成模块信息
- 执行 JavaScript 源码:通过RCTJSCExecutor执行代码,写入信息
- 相互调用方法:
- OC调用JS:OC会通过executeBlockOnJavaScriptQueue方法在单独的线程上运行 JS 代码
- 处理参数:_executeJSCall:(NSString *)method方法
- 实际调用:sendAppEventWithName和body方法
- JS调用OC:JS 会解析出方法的类、方法和方法参数并放入到 MessageQueue 中,等待 OC 调用或超时发送
- 使用RCT_EXPORT_METHOD 宏,用来注册模块表
- JS中使用NativeModules.CryptoExport.注册方法调用
- rn 的更新机制:React 状态机,不停地检查确认更新
- 文本元素:ReactDOMTextComponent 比较替换文本元素
- 基本元素:updateComponent方法分属性、节点替换基本元素
- 自定义元素:_performComponentUpdate判断-先卸载再安装子节点
20180522更新:React Native 原理解析
准备工作,首先要有个解剖对象
从 HelloWord 看起,我们来分析RN的实现原理
import React, { Component } from 'react'; import { AppRegistry, Text } from 'react-native'; class HelloWorldApp extends Component { render() { return ( <Text>Hello world!</Text> ); } } // 注意,这里用引号括起来的'HelloWorldApp'必须和你init创建的项目名一致 AppRegistry.registerComponent('HelloWorldApp', () => HelloWorldApp);
可以创建一个新的项目
react-native init ProjectName
创建完成你可以手动打开项目,也可以在项目根目录执行
// 启动 iOS react-native run-ios // 启动 Android react-native run-android
准备工作完成了
React 原理探究
首先我们聊聊 React,我们注意到这条数据源代码
return ( <Text>Hello world!</Text> );
“为什么 JavaScript 代码里面出现了 HTML 的语法?”
React Native 把一组相关的 HTML 标签,也就是 app 内的 UI 控件,封装进一个组件(Component)中,这种语法被称为 JSX,它是一种 JavaScript 语法拓展。
JSX 允许我们写 HTML 标签或 React 标签,它们终将被转换成原生的 JavaScript 并创建 DOM。
在 React 框架中,除了可以用 JavaScript 写 HTML 以外,我们甚至可以写 CSS。
总之 React 是一套可以用简洁的语法高效绘制 DOM 的框架
- 简洁:不单单指它的 HTML 和 CSS 语法,更因为可以单用 JavaScript 构造页面;
- 高效:因为 React 独创了 Virtual DOM 机制,Virtual DOM 有两大特征,一它存在于内存中的 JavaScript 对象,并且与 DOM 是一一对应的关系;二使用高效的 DOM Diff 算法不需要对 DOM 进行重新绘制。
当然,React 并不是前端开发的全部。从之前的描述也能看出,它专注于 UI 部分,对应到 MVC 结构中就是 View 层。
要想实现完整的 MVC 架构,还需要 Model 和 Controller 的结构。在前端开发时,我们可以采用 Flux 和 Redux(基于Flux) 架构,它们并非框架(Library),而是和 MVC 一样都是一种架构设计(Architecture)。
React Native 原理探究
谈谈 RN 的故事背景
而 React 在前端取得突破性成功以后,JavaScript 开始试图一统三端。
他们利用了移动平台能够运行 JavaScript (脚本语言)代码的能力,并且发挥了 JavaScript 不仅仅可以传递配置信息,还可以表达逻辑信息的优点。
最终,一个基于 JavaScript,具备动态配置能力,面向前端开发者的移动端开发框架 —— React Native
谈谈 RN 的原理
即使使用了 React Native,我们依然需要 UIKit 等框架,调用的是 Objective-C 代码,JavaScript 只是提供了配置信息和逻辑的处理结果。
而 JavaScript 是一种脚本语言,它不会经过编译、链接等操作,而是在运行时才动态的进行词法、语法分析,生成抽象语法树(AST)和字节码,然后由解释器负责执行或者使用 JIT 将字节码转化为机器码再执行。
苹果提供了一个叫做 JavaScript Core 的框架,这是一个 JavaScript 引擎。整个流程由 JavaScript 引擎负责完成。
JSContext *context = [[JSContext alloc] init]; JSValue *jsVal = [context evaluateScript:@"21+7"]; int iVal = [jsVal toInt32];
JavaScript 是一种单线程的语言,它不具备自运行的能力,因此总是被动调用。很多介绍 React Native 的文章都会提到 “JavaScript 线程” 的概念,实际上,它表示的是 Objective-C 创建了一个单独的线程,这个线程只用于执行 JavaScript 代码,而且 JavaScript 代码只会在这个线程中执行。
下面将 JavaScript 👉 OC
由于 JavaScript Core 是一个面向 Objective-C 的框架,在 Objective-C 这一端,我们对 JavaScript 上下文知根知底,可以很容易的获取到对象,方法等各种信息,当然也包括调用 JavaScript 函数。
真正复杂的问题在于,JavaScript 不知道 Objective-C 有哪些方法可以调用。
React Native 解决这个问题的方案是在 Objective-C 和 JavaScript 两端都保存了一份配置表,里面标记了所有 Objective-C 暴露给 JavaScript 的模块和方法。
这样,无论是哪一方调用另一方的方法,实际上传递的数据只有
- ModuleId 类
- MethodId 方法
- Arguments 方法参数
当 Objective-C 接收到这三个值后,就可以通过 runtime 唯一确定要调用的是哪个函数,然后调用这个函数。
对于 Objective-C 来说,执行完 JavaScript 代码再执行 Objective-C 回调毫无难度,难点依然在于 JavaScript 代码调用 Objective-C 之后,如何在 Objective-C 的代码中,回调执行 JavaScript 代码。
目前 React Native 的做法是:在 JavaScript 调用 Objective-C 代码时,注册要回调的 Block,并且把 Block Id 作为参数发送给 Objective-C,Objective-C 收到参数时会创建 Block,调用完 Objective-C 函数后就会执行这个刚刚创建的 Block。
Objective-C 会向 Block 中传入参数和 Block Id,然后在 Block 内部调用 JavaScript 的方法,随后 JavaScript 查找到当时注册的 Block 并执行。
简单的表示就是:JS 👉 OC (Block 👉 JS)
继续看项目-初始化
除了 index.js 中的 JavaScript 代码,留给我们的还有 AppDelegate 中的入口方法:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSURL *jsCodeLocation; jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"demo" initialProperties:nil launchOptions:launchOptions]; rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIViewController *rootViewController = [UIViewController new]; rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; return YES; }
实际我们操作的视图就是这个 RootView ,但是 RootView 是依托于 Bridge 对象,它是 Objective-C 与 JavaScript 交互的桥梁,后续的方法交互完全依赖于它,而整个初始化过程的最终目的其实也就是创建这个桥梁对象。
- (instancetype)initWithBundleURL:(NSURL *)bundleURL moduleName:(NSString *)moduleName initialProperties:(NSDictionary *)initialProperties launchOptions:(NSDictionary *)launchOptions { RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL moduleProvider:nil launchOptions:launchOptions]; return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties]; }
初始化方法的核心是 setUp 方法,而 setUp 方法的主要任务则是创建 BatchedBridge。
- (void)setUp { RCT_PROFILE_BEGIN_EVENT(0, @"-[RCTBridge setUp]", nil); _performanceLogger = [RCTPerformanceLogger new]; [_performanceLogger markStartForTag:RCTPLBridgeStartup]; [_performanceLogger markStartForTag:RCTPLTTI]; Class bridgeClass = self.bridgeClass; #if RCT_DEV RCTExecuteOnMainQueue(^{ RCTRegisterReloadCommandListener(self); }); #endif // Only update bundleURL from delegate if delegate bundleURL has changed NSURL *previousDelegateURL = _delegateBundleURL; _delegateBundleURL = [self.delegate sourceURLForBridge:self]; if (_delegateBundleURL && ![_delegateBundleURL isEqual:previousDelegateURL]) { _bundleURL = _delegateBundleURL; } // Sanitize the bundle URL _bundleURL = [RCTConvert NSURL:_bundleURL.absoluteString]; self.batchedBridge = [[bridgeClass alloc] initWithParentBridge:self]; [self.batchedBridge start]; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); }
BatchedBridge 的作用是批量读取 JavaScript 对 Objective-C 的方法调用,同时它内部持有一个 JavaScriptExecutor,顾名思义,这个对象用来执行 JavaScript 代码。
创建 BatchedBridge 的关键是 start 方法,它可以分为五个步骤:
- 读取 JavaScript 源码
- 初始化模块信息
- 初始化 JavaScript 代码的执行器,即 RCTJSCExecutor 对象
- 生成模块列表并写入 JavaScript 端
- 执行 JavaScript 源码
逐个分析上面每一步完成的操作:
1.读取JavaScript源码这一部分的具体代码实现没有太大的讨论意义。我们只要明白,JavaScript 的代码是在 Objective-C 提供的环境下运行的,所以第一步就是把 JavaScript 加载进内存中,对于一个空的项目来说,所有的 JavaScript 代码大约占用 1.5 Mb 的内存空间。
需要说明的是,在这一步中,JSX 代码已经被转化成原生的 JavaScript 代码。
2.初始化模块信息这一步在方法 initModulesWithDispatchGroup: 中实现,主要任务是找到所有需要暴露给 JavaScript 的类。
每一个需要暴露给 JavaScript 的类(也成为 Module,以下不作区分)都会标记一个宏:RCT_EXPORT_MODULE,这个宏的具体实现并不复杂
#define RCT_EXPORT_MODULE(js_name) \ RCT_EXTERN void RCTRegisterModule(Class); \ + (NSString *)moduleName { return @#js_name; } \ + (void)load { RCTRegisterModule(self); }
这样,这个类在 load 方法中就会调用 RCTRegisterModule 方法注册自己:
void RCTRegisterModule(Class moduleClass) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ RCTModuleClasses = [NSMutableArray new]; }); [RCTModuleClasses addObject:moduleClass]; }
因此,React Native 可以通过 RCTModuleClasses 拿到所有暴露给 JavaScript 的类。下一步操作是遍历这个数组,然后生成 RCTModuleData 对象:
for (Class moduleClass in RCTGetModuleClasses()) { RCTModuleData *moduleData = [[RCTModuleData alloc]initWithModuleClass:moduleClass bridge:self]; [moduleClassesByID addObject:moduleClass]; [moduleDataByID addObject:moduleData]; }
可以想见,RCTModuleData 对象是模块配置表的主要组成部分。如果把模块配置表想象成一个数组,那么每一个元素就是一个 RCTModuleData 对象。
这个对象保存了 Module 的名字,常量等基本信息,最重要的属性是一个数组,保存了所有需要暴露给 JavaScript 的方法。
暴露给 JavaScript 的方法需要用 RCT_EXPORT_METHOD 这个宏来标记,它的实现原理比较复杂,有兴趣的读者可以自行阅读。简单来说,它为函数名加上了 rct_export 前缀,再通过 runtime 获取类的函数列表,找出其中带有指定前缀的方法并放入数组中:
- (NSArray<id<RCTBridgeMethod>> *)methods{ unsigned int methodCount; Method *methods = class_copyMethodList(object_getClass(_moduleClass), &methodCount); // 获取方法列表 for (unsigned int i = 0; i < methodCount; i++) { RCTModuleMethod *moduleMethod = /* 创建 method */ [_methods addObject:moduleMethod]; } } return _methods; }
因此 Objective-C 管理模块配置表的逻辑是:Bridge 持有一个数组,数组中保存了所有的模块的 RCTModuleData 对象,RCTModuleData又保存了类的方法、常亮、类名等信息。只要给定 ModuleId 和 MethodId 就可以唯一确定要调用的方法。
3.初始化JavaScript执行器(RCTJSCExecutor)通过查看源码可以看到,初始化 JavaScript 执行器的时候,会调用
+ (instancetype)initializedExecutorWithContextProvider:(RCTJSContextProvider *)JSContextProvider applicationScript:(NSData *)applicationScript sourceURL:(NSURL *)sourceURL JSContext:(JSContext **)JSContext error:(NSError **)error;
返回的 excuter 对象是已经被同步执行的
// 执行对应的方法 - (void)callFunctionOnModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args jsValueCallback:(RCTJavaScriptValueCallback)onComplete { [self _callFunctionOnModule:module method:method arguments:args returnValue:NO unwrapResult:NO callback:onComplete]; }
这里需要关注 nativeRequireModuleConfig 和 nativeFlushQueueImmediate 这两个block。
在这两个 block 中会通过 bridge 调用 oc 的方法。
[self executeBlockOnJavaScriptQueue:^{ if (!self.valid) { return; } JSContext *context = nil; if (self->_jscWrapper) { RCTAssert(self->_context != nil, @"If wrapper was pre-initialized, context should be too"); context = self->_context.context; } else { [self->_performanceLogger markStartForTag:RCTPLJSCWrapperOpenLibrary]; self->_jscWrapper = RCTJSCWrapperCreate(self->_useCustomJSCLibrary); [self->_performanceLogger markStopForTag:RCTPLJSCWrapperOpenLibrary]; RCTAssert(self->_context == nil, @"Didn't expect to set up twice"); context = [self->_jscWrapper->JSContext new]; self->_context = [[RCTJavaScriptContext alloc] initWithJSContext:context onThread:self->_javaScriptThread]; [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptContextCreatedNotification object:context]; configureCacheOnContext(context, self->_jscWrapper); installBasicSynchronousHooksOnContext(context); } __weak RCTJSCExecutor *weakSelf = self; context[@"nativeRequireModuleConfig"] = ^NSString *(NSString *moduleName) { RCTJSCExecutor *strongSelf = weakSelf; if (!strongSelf.valid) { return nil; } RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"nativeRequireModuleConfig", nil); NSArray *config = [strongSelf->_bridge configForModuleName:moduleName]; NSString *result = config ? RCTJSONStringify(config, NULL) : nil; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"js_call,config", @{ @"moduleName": moduleName }); return result; }; context[@"nativeFlushQueueImmediate"] = ^(NSArray<NSArray *> *calls){ RCTJSCExecutor *strongSelf = weakSelf; if (!strongSelf.valid || !calls) { return; } RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"nativeFlushQueueImmediate", nil); [strongSelf->_bridge handleBuffer:calls batchEnded:NO]; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"js_call", nil); }; #if RCT_PROFILE __weak RCTBridge *weakBridge = self->_bridge; context[@"nativeTraceBeginAsyncFlow"] = ^(__unused uint64_t tag, __unused NSString *name, int64_t cookie) { if (RCTProfileIsProfiling()) { [weakBridge.flowIDMapLock lock]; int64_t newCookie = [_RCTProfileBeginFlowEvent() longLongValue]; CFDictionarySetValue(weakBridge.flowIDMap, (const void *)cookie, (const void *)newCookie); [weakBridge.flowIDMapLock unlock]; } }; context[@"nativeTraceEndAsyncFlow"] = ^(__unused uint64_t tag, __unused NSString *name, int64_t cookie) { if (RCTProfileIsProfiling()) { [weakBridge.flowIDMapLock lock]; int64_t newCookie = (int64_t)CFDictionaryGetValue(weakBridge.flowIDMap, (const void *)cookie); _RCTProfileEndFlowEvent(@(newCookie)); CFDictionaryRemoveValue(weakBridge.flowIDMap, (const void *)cookie); [weakBridge.flowIDMapLock unlock]; } }; #endif #if RCT_DEV RCTInstallJSCProfiler(self->_bridge, context.JSGlobalContextRef); // Inject handler used by HMR context[@"nativeInjectHMRUpdate"] = ^(NSString *sourceCode, NSString *sourceCodeURL) { RCTJSCExecutor *strongSelf = weakSelf; if (!strongSelf.valid) { return; } RCTJSCWrapper *jscWrapper = strongSelf->_jscWrapper; JSStringRef execJSString = jscWrapper->JSStringCreateWithUTF8CString(sourceCode.UTF8String); JSStringRef jsURL = jscWrapper->JSStringCreateWithUTF8CString(sourceCodeURL.UTF8String); jscWrapper->JSEvaluateScript(strongSelf->_context.context.JSGlobalContextRef, execJSString, NULL, jsURL, 0, NULL); jscWrapper->JSStringRelease(jsURL); jscWrapper->JSStringRelease(execJSString); }; #endif }]; }