|
小程序开放平台架构指南(下)
小程序开放平台架构指南(下)
姚泽源
贝壳产品技术
贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容
2021年12月03日 16:07
上文说到, 小程序架构中存在两个关键问题, 不解决则小程序项目就无从谈起. 在这篇文章中, 我们会尝试解决这两个问题.在逻辑进程和渲染进程中, js 如何与 Native 通信如果 js 只在逻辑进程中运行, 不能和外部通信, 那么它既不能使用原生能力, 也不能在渲染进程中构建出实际页面, 小程序架构也就无从谈起. 因此, 小程序基础库首先需要解决的就是 js 如何和 Native/渲染进程通信问题.逻辑引擎中的情况所谓逻辑引擎, 实际上是 V8/jsCore 的实例. Native 首先实例化 V8 对象, 然后执行小程序 js 文件. 我们需要的是, 如何在执行 js 文件的过程中, 实现和 V8 之间的双向通信.方法实际上比较简单.Native -> JS由于 V8 是 Native 构建出的一个对象实例, 所以 Native 可以直接在 V8 中执行方法. 此时, JS 方需要做的, 就是启动后注册全局函数V8CallJs供 Native 调用. Native 通过参数告知 JS 实际需要传递的信息.JS -> NativeJS 调用 Native 相对比较繁琐, 需要 Native 先在 V8 中注册全局函数JsCallNative供 js 调用, 在 java 中也要创建类并实现JavaCallback接口. 当 js 调用JsCallNative时, 会暂停 V8 引擎的运行并将控制权交给 Native. 待函数完成后才会恢复 V8 中 js 的执行. 在实际实现中, 为了避免暂停 V8 引擎导致界面失去响应, 基础库一般会把业务方的原生调用做成回调函数的形式. Native 获知 js 所要调用的函数名和相关参数后立刻返回, 后续通过V8CallJs通知具体执行结果, 然后基础库再执行回调函数, 将结果转发给业务方. 这就是为什么微信小程序库中那么多 callback 回调的原因.问: 15 年推出的微信小程序里都是 callback 可以理解, 但为什么后期出现的支付宝小程序/京东小程序里也是 callback, 而不是更加现代的 Promise答: 后续的开发者当然想优化 API 的设计, 用 Promise 替代难用难维护的 callback. 但问题是微信小程序的 API 是目前业内小程序方案的事实标准, 如果 API 参数&返回值和微信不一致, 接入新开发者/使用小程序转码工具接入新应用都会很困难. 然而应用数量是小程序平台的核心 KPI, 所以初期实现时只能以微信为准.不过好消息是微信也在逐步将 callback 回调改为 Promise. 相信未来我们终有告别 callback 的一天.Native 调用 js和js 调用 Native的具体实现可以参考 JS-V8 通信方案, 这里重点介绍一下 js 端的实现流程.JS 端与 Native 双向通信协议的实现js 与 Native 双向通信有两个核心要素跨语言通信中, 无法传递具体函数/原生复杂数据结构.这一条决定了, 跨语言通信期间, 需要传递的信息最好全部编码为字符串格式, 再具体点说, 是 JSON 字符串. 具体信息通过 json 字段进行传递为了性能当然也可以用二进制方案----只是要做好 debug 难度暴增的准备. 一般来说, 初始阶段快速验证为重, 不建议太追求性能.在双向通信过程中, 以异步回调为主. 因此通信协议中需要标明这个回调关联的命令 id, 以管理请求/回调之间的关联关系.所以我们最终的通信协议如下所示typeType_Protocol={/***命令id*/id:number;/***命令类型*/type:"JsCallV8"|"V8CallJs"|"V8CallWebview"|"WebviewCallV8";/***具体调用的API名**可能是Native向JS提供的API:pickerImg/httpRequest/getLocation*也可能是JS向Native提供的APInProgramHidden/onHomeButtonPress/onProgramHidden*/apiName:string;/***json化后的参数列表,视API具体约定*/argvListJson:string;//Json化后的参数列表};而通信时序图如下所示js-native双向通信流程渲染进程中的情况制定完逻辑进程的通信协议, 渲染进程的通信问题就很好处理了. 渲染进程的 webview 也是由 Native 实例化完成, 可以直接复用我们在逻辑进程中设定的通信协议----简单来说, 完全可以把渲染进程中的通信视为 js-bridge 进行处理.至此,逻辑进程 Native双向通信完成,渲染进程 Native双向通信完成, 逻辑进程和渲染进程利用 Native 中转也就可以进行通信. 最终通信模型如下逻辑进程 Native 渲染进程然后是第二个问题逻辑层中运行的 js 如何在渲染层生成对应 Dom 操作第一层思考: 转发 Dom API 操作逻辑层中运行的是正常 js, 渲染层中展示的是实际 dom 元素, js 不能直接在渲染层中进行操作也不能使用 DOM API 函数----那怎么生成最终的 dom 页面节点计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决首先想到的是封装 DOM API 操作指令. 虽然不能直接操作 Dom, 但是逻辑层和渲染层可以互相通信. 那我们可以通过向渲染层发送Dom API函数名 + 对应参数的形式, 由运行在渲染层上的 webview-render 实际执行这些函数, 从而间接实现调用 DOM API 的效果.这个思路很好, 比如我们可以在逻辑层中 mock 一个 document 对象, 对业务方暴露document.createElement方法, 从而在业务方调用该方法的时候把参数原样发送到渲染层完成问题.方案好是好, 但实际操作中有点麻烦, 能不能简单一点第二层思考: 利用类 React/vue 语法构建虚拟 dom, 屏蔽 Dom API 操作这样也可以, 我们可以自定义一套模板语法, 根据模板语法创建实际 dom, 让用户可以不去写document.createElement.在模板语法方面, 最简单的方案是html + innerHtml, 进阶而言是编写模板生成虚拟 dom + 利用 snabbdom(业内的虚拟 Dom 库) 生成 dom 更新指令, 也就是微信小程序目前使用的方案. 但, 订制模板语法 + 构建虚拟 dom 开发成本还是很高, 还能更简单点吗当然可以!第三层思考: 直接使用 React 作为小程序界面展示方案React16 相对 15 的一个重大变化, 就是将架构模型升级到了 Fiber. 在 Fiber 架构下, React 执行过程如下所示.React component API Reconciler 调和器 ----> Renderer 渲染器React component API对应于业务层代码, 是我们熟悉的 setState/useState 状态控制函数和 compontentDidUpdate/shouldComponentUpdate 生命周期方法.组件中的状态控制函数(setState/useState)由Reconciler 调和器实现. 这样当组件创建完成/状态发生改变时, 就可以被Reconciler 调和器发现, 进而比较虚拟 dom 变动生成更新指令. 然后用实现了HostConfig接口的对象作为中间层, 将虚拟 dom 指令转发给Renderer 渲染器, 并由Renderer 渲染器根据虚拟 Dom 指令在对应平台上转换为实际效果.在 React16 的渲染流程里, 有三个关键点前端界面使用 React 直接编写, 编写过程和生成虚拟Dom/最终页面展示无关Reconciler 调和器输出的虚拟 dom 操作指令通过实现了 HostConfig 接口的对象进行转发, 该对象只要求实现约定接口,对提供者和接口具体实现没有要求Renderer 渲染器只需要保证将传来的操作指令转译为平台上对应的操作, 对操作方式的具体实现没有要求, 对平台也没有要求那么, 我们是否可以在逻辑进程里实现一个 HostConfig 对象, 在渲染进程上实现一个 webview-render. 然后通过 Native 把 HostConfig 收到的操作指令转发给 webview-render, 从而完成页面的构建呢当然可以!实现方案如下业务方React代码 React component API Reconciler 调和器 --> HostConfig对象(逻辑进程) --> Native转发操作命令&操作 ----> webview-render((渲染进程))初次构建初次构建Reconciler 触发生命周期回调Reconciler 触发生命周期回调渲染层触发用户触发交互渲染层触发用户触发交互类小程序项目中项目的具体启动过程基本方案给出, 现在只有两个问题:[原理层面] React 项目是如何启动的 jsx 对象变化是怎么被Reconciler 调和器监听到的[实现层面] 小程序本身启动过程是什么样的. 我们的 HostConfig 和 webview-render 具体需要如何实现我们基于同样构建思路的remax@2.15.0为例, 分析类小程序项目中项目的具体启动过程.首先介绍一下 remax 方案下小程序项目的基本启动模型:解析 app.json, 获取其中注册的JSX对象和对应的 path初始化实现了HostConfig协议所约定接口的对象, 作为负责实际渲染的容器Container获取待渲染的JSX对象如果匹配到已注册 path, 则加载对应的JSX对象否则加载默认页面对应的JSX对象[可选]如果没找到匹配路径, 也可以直接报白屏错误, 看小程序引擎实现者的心情从 Native 中获取当前打开的 scheme, 解析出正在访问的路径&参数和已注册路由进行比较将Container对象, 和JSX对象 一起传入由Reconciler导出的render方法在传统浏览器环境中Reconciler会将JSX渲染为虚拟 Dom期间根据JSX变动, 不断产生更新指令, 将指令转换为HostConfig中约定的 Dom 操作, 并调用Container暴露的操作方法.Container根据被调用的操作, 创建实际 Dom. 从而生成实际页面在实际小程序运行环境中由于小程序环境中逻辑层和渲染层分开展示, 因此在逻辑层中运行的Container并不会创建实际 Dom.所以在小程序应用中, 我们引入一个中间层, 用 js 对象模拟 Dom 操作, 并记录Reconciler传入的 Dom 操作指令.在一个操作批次结束后, 将操作指令 json 化, 变成字符串格式的指令列表通过Native转发给位于渲染层的webview-render对象webview-render对象根据操作指令, 在 webview 中构建实际 Dom也就是这个模型ReactElement对象 -> Render(React-Reconciler) -> Container(HostConfig) -> 转发命令 -> Webview-Render我们以Remax@2.15.0和React@16.7.0为例, 结合实际代码对启动流程进行一次跟踪小程序启动示例代码如下所示//最简小程序模型.//https://github.dev/remaxjs/remax/blob/v2.15.0/packages/remax-runtime/src/__tests__/index.test.tsx#L53importContainerfrom"@remax/remax-runtime/Container";importrenderfrom"@remax/remax-runtime/render";constMiniProgramPage=()=>hello;constcontainer=newContainer();render(>,container);在这段代码中, 我们完成了以下工作:直接获取待渲染的 jsx 对象 MiniProgramPage在逻辑层内初始化 Dom 容器Container, 用于在 js-core 中模拟 Dom 功能, 接收并缓存后续ReactReconciler传过来的 Dom 指令将jsx对象和Container传给 render, 进入渲染逻辑.值得一提的是, 整个小程序启动进程只有这三行代码,render函数执行完毕启动进程即宣告结束. 后续 render 中的 react-reconciler 会接管jsx对象的 setState 方法, 从而可以接管组件中的所有变动, 进而和旧 jsx 对象进行比较, 计算虚拟 Dom 变更情况, 生成实际 Dom 操作指令, 然后再根据 HostConfig 协议调用 Container 对象上暴露的方法...HostConfig 协议和 Container 对象的实现我们放在下篇文章, 这篇文章我们只搞清楚两件事:render 函数的实现react-reconciler 接管 JSX 变更的实现render 函数的实现先看下 render 函数的实现//位于https://github.dev/remaxjs/remax/blob/v2.15.0/packages/remax-runtime/src/render.tsimport*asReactfrom"react";importReactReconcilerfrom"react-reconciler";importhostConfigfrom"./hostConfig";importContainerfrom"./Container";importAppContainerfrom"./AppContainer";exportconstReactReconcilerInst=ReactReconciler(hostConfigasany);if(process.env.NODE_ENV==="development"){ReactReconcilerInst.injectIntoDevTools({bundleType:1,version:"16.13.1",rendererPackageName:"remax",});}functiongetPublicRootInstance(container:ReactReconciler.FiberRoot){constcontainerFiber=container.current;if(!containerFiber.child){returnnull;}returncontainerFiber.child.stateNode;}exportdefaultfunctionrender(rootElement:React.ReactElement|null,container:Container|AppContainer){//CreatearootContainerifitdoesntexistif(!container._rootContainer){container._rootContainer=ReactReconcilerInst.createContainer(container,0,false,null);}ReactReconcilerInst.updateContainer(rootElement,container._rootContainer,null,()=>{//ignore});returngetPublicRootInstance(container._rootContainer);}可以看到, render 函数实际是对ReactReconciler的封装. 整个实现可以分为三步:基于 HostConfig 初始化ReactReconcilerInst对象, 后续ReactReconciler会根据 HostConfig 提供的 API 生成 Dom 操作指令, 然后按照指令调用container上的接口通过ReactReconcilerInst.createContainer方法将container对象包装为 Fiber 节点通过ReactReconcilerInst.updateContainer方法获取待渲染的JSX对象至此, 整个流程执行完毕. 为ReactReconciler输入HostConfig&container&JSX,ReactReconciler会启动对JSX的渲染, 并根据JSX对象的变动计算虚拟 Dom 的变更, 生成实际 Dom 更新指令并根据 HostConfig 配置调用 container 上的方法.但这里存在一个问题了,JSX只是一个普普通通的React.Component对象, 状态变更调用的也是内部的 setState 方法,ReactReconciler是怎么知到JSX的变动状态并计算虚拟 Dom 变更的呢实际情况是ReactReconciler在updateContainer方法中, 替换了JSX对象中 setState 方法的实现. 因此可以获知JSX的所有变动情况, 并根据需要调用JSX的生命周期钩子, 获取状态更新后的 render 结果.不过说归说, talk is cheap show me your code. 接下来还是要依次看下 createContainer 和 updateContainer 的实现, 这里要涉及 react 的源码, 我们以react@16.7.0为例ReactReconciler.createContainer的实现首先是 createContainer//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberReconciler.js#L274exportfunctioncreateContainer(containerInfo:Container,isConcurrent:boolean,hydrate:boolean):OpaqueRoot{//如果追下去的话会发现真的只初始化了一个FiberRoot,其他啥都没干.returncreateFiberRoot(containerInfo,isConcurrent,hydrate);}可以看到, 初始化容器只是简单创建了一个 Fiber 节点并返回, 本身没有多余操作ReactReconciler.updateContainer的实现然后看看 updateContainer 的实现//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberReconciler.js#L282exportfunctionupdateContainer(element:ReactNodeList,container:OpaqueRoot,parentComponent:React$Component,callback:Function):ExpirationTime{constcurrent=container.current;constcurrentTime=requestCurrentTime();constexpirationTime=computeExpirationForFiber(currentTime,current);returnupdateContainerAtExpirationTime(element,container,parentComponent,expirationTime,callback);}updateContainer 主要工作就是将jsx对象和container传给updateContainerAtExpirationTime, 并注册更新任务. 如果继续跟进的话, 可以看到以下调用链//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberReconciler.js#L161updateContainerAtExpirationTime{//...省略其余代码returnscheduleRootUpdate(current,element,expirationTime,callback);}=>//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberReconciler.js#L161exportfunctionupdateContainerAtExpirationTime(element:ReactNodeList,container:OpaqueRoot,parentComponent:React$Component,expirationTime:ExpirationTime,callback:Function){//...省略其余代码returnscheduleRootUpdate(current,element,expirationTime,callback);}=>//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberReconciler.js#114functionscheduleRootUpdate(current:Fiber,element:ReactNodeList,expirationTime:ExpirationTime,callback:Function){//...省略其他代码scheduleWork(current,expirationTime);}=>//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberScheduler.js#L1788functionscheduleWork(fiber:Fiber,expirationTime:ExpirationTime){requestWork(root,rootExpirationTime);//...省略其他代码}requestWork对应的是注册组件更新任务代码, 如果继续跟下去的话, 会依次看到下边的调用链, 一直到beginWorkrequestWork=>performWorkOnRoot=>renderRoot=>workLoop=>performUnitOfWork=>beginWork看下beginWork的代码//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberBeginWork.js#L1673functionbeginWork(current:Fiber|null,workInProgress:Fiber,renderExpirationTime:ExpirationTime):Fiber|null{//...省略其他代码switch(workInProgress.tag){caseFunctionComponent:{constComponent=workInProgress.type;constunresolvedProps=workInProgress.pendingProps;constresolvedProps=workInProgress.elementType===ComponentunresolvedProps:resolveDefaultProps(Component,unresolvedProps);returnupdateFunctionComponent(current,workInProgress,Component,resolvedProps,renderExpirationTime);}caseClassComponent:{constComponent=workInProgress.type;constunresolvedProps=workInProgress.pendingProps;constresolvedProps=workInProgress.elementType===ComponentunresolvedProps:resolveDefaultProps(Component,unresolvedProps);returnupdateClassComponent(current,workInProgress,Component,resolvedProps,renderExpirationTime);}}//...省略其他代码}对于函数组件, ReactReconciler 调用的是updateFunctionComponent函数, 对于类组件, ReactReconciler 调用的是updateClassComponent至此, render 函数的原理讲解完毕. 接下来是那个核心问题:ReactReconciler是怎么拿到JSX的状态变更的.ReactReconciler 获取 JSX 对象状态变更信息的实现类组件: ClassComponent先从类组件开始.//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberBeginWork.js#L531functionupdateClassComponent(current:Fiber|null,workInProgress:Fiber,Component:any,nextProps,renderExpirationTime:ExpirationTime){//...省略其他代码constructClassInstance(workInProgress,Component,nextProps,renderExpirationTime);}updateClassComponent中我们需要关注的是constructClassInstance, 这是类组件实例化的方法//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberClassComponent.js#513functionconstructClassInstance(workInProgress:Fiber,ctor:any,props:any,renderExpirationTime:ExpirationTime):any{//...省略其他代码adoptClassInstance(workInProgress,instance);}这里的关键是adoptClassInstance, 在这个函数中, 组件实例的updater被设置为了classComponentUpdater//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberClassComponent.js#L503functionadoptClassInstance(workInProgress:Fiber,instance:any):void{//关键代码instance.updater=classComponentUpdater;//...省略其他代码}而这个classComponentUpdater, 其代码如下//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberClassComponent.js#L188constclassComponentUpdater={isMounted,enqueueSetState(inst,payload,callback){constfiber=getInstance(inst);constcurrentTime=requestCurrentTime();constexpirationTime=computeExpirationForFiber(currentTime,fiber);constupdate=createUpdate(expirationTime);update.payload=payload;if(callback!==undefined&callback!==null){if(__DEV__){warnOnInvalidCallback(callback,"setState");}update.callback=callback;}flushPassiveEffects();enqueueUpdate(fiber,update);scheduleWork(fiber,expirationTime);},//...省略其他代码};由于classComponentUpdater由ReactReconciler提供, 所以对classComponentUpdater自然可以被ReactReconciler捕获到.但为什么将组件实例的updater设置成classComponentUpdater就会被捕获呢 搂一眼React.Component的源码//位于https://github.com/facebook/react/blob/v16.7.0/packages/react/src/ReactBaseClasses.js#L58Component.prototype.setState=function(partialState,callback){invariant(typeofpartialState==="object"||typeofpartialState==="function"||partialState==null,"setState(...):takesanobjectofstatevariablestoupdateora"+"functionwhichreturnsanobjectofstatevariables.");this.updater.enqueueSetState(this,partialState,callback,"setState");};显然,Component中的 setState 实际上调用的就是 updater 上的enqueueSetState方法. 而由于 updater 本身已经被替换为了ReactReconciler自身的实现, 所以自然可以捕获到类组件上的所有数据变更.问题得解函数组件: FunctionComponent接着看下一项,ReactReconciler对函数组件中 useState 的接管实现//位于https://github.com/facebook/react/blob/v16.7.0/packages/react/src/ReactHooks.js#L54exportfunctionuseState(initialState()=>S)|S){constdispatcher=resolveDispatcher();returndispatcher.useState(initialState);}useState 位于ReactHooks.js文件, 实际调用的是ReactCurrentOwner.currentDispatcher上提供的 useState 方法//位于https://github.com/facebook/react/blob/v16.7.0/packages/react/src/ReactHooks.js#L14importReactCurrentOwnerfrom"./ReactCurrentOwner";functionresolveDispatcher(){constdispatcher=ReactCurrentOwner.currentDispatcher;invariant(dispatcher!==null,"Hookscanonlybecalledinsidethebodyofafunctioncomponent.");returndispatcher;}而resolveDispatcher返回的又是ReactCurrentOwner.currentDispatcher对象. 这个ReactCurrentOwner看起来位于packages/react/src/ReactCurrentOwner.js, 但点进去会发现里边只有一个普通对象//位于https://github.com/facebook/react/blob/v16.7.0/packages/react/src/ReactCurrentOwner.js#L1importtype{Fiber}from'react-reconciler/src/ReactFiber';importtypeof{Dispatcher}from'react-reconciler/src/ReactFiberDispatcher';/***Keepstrackofthecurrentowner.**Thecurrentowneristhecomponentwhoshouldownanycomponentsthatare*currentlybeingconstructed.*/constReactCurrentOwner={/***@internal*@type{ReactComponent}*/currentnull:null|Fiber),currentDispatchernull:null|Dispatcher),}exportdefaultReactCurrentOwner;所以react/src/ReactCurrentOwner.js显然不是ReactCurrentOwner实际的提供者. 如果返回beginWork, 看ReactReconciler提供ReactCurrentOwner的方式时我们会看到//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberBeginWork.js#L47//...省略其他代码importReactSharedInternalsfrom"shared/ReactSharedInternals";//...省略其他代码constReactCurrentOwner=ReactSharedInternals.ReactCurrentOwner;//...省略其他代码functionupdateFunctionComponent(current,workInProgress,Component,nextProps:any,renderExpirationTime){//...省略其他代码}ReactReconciler也提供了一个ReactCurrentOwner, 如果继续往后跟, 可以看到他在workLoop中替换了ReactCurrentOwner.currentDispatcher//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberScheduler.js#29importReactSharedInternalsfrom"shared/ReactSharedInternals";//...省略其他代码const{ReactCurrentOwner}=ReactSharedInternals;//位于https://github.com/facebook/react/blob/v16.7.0/packages/react-reconciler/src/ReactFiberScheduler.js#1187functionworkLoop(isYieldy){//...省略其他代码if(enableHooks){ReactCurrentOwner.currentDispatcher=Dispatcher;}else{ReactCurrentOwner.currentDispatcher=DispatcherWithoutHooks;}}但问题是,ReactReconciler引入的是shared/ReactSharedInternals, react 中引用的却是react/src/ReactCurrentOwner.js, 这是怎么做到的来看这段代码//位于https://github.com/facebook/react/blob/v16.7.0/scripts/rollup/forks.js#L48//Withoutthisfork,importing`shared/ReactSharedInternals`inside//the`react`packageitselfwouldnotworkduetoacyclicaldependency.'shared/ReactSharedInternals'bundleType,entry,dependencies)=>{if(entry==='react'){return'react/src/ReactSharedInternals';}if(dependencies.indexOf('react')===-1){//Reactinternalsareunavailableifwecan'treferencethepackage.//Wereturnanerrorbecauseweonlywanttothrowifthismodulegetsused.returnnewError('CannotuseamodulethatdependsonReactSharedInternals'+'from"'+entry+'"becauseitdoesnotdeclare"react"inthepackage'+'dependenciesorpeerDependencies.Forexample,thiscanhappenifyouuse'+'warning()insteadofwarningWithoutStack()inapackagethatdoesnot'+'dependonReact.');}returnnull;},显然, 答案是 rollup.react 在使用 rollup 构建时, 通过定制编译脚本, 在输出将shared/ReactSharedInternals映射为了react/src/ReactSharedInternals, 从而实现对ReactCurrentOwner变量的替换, 进而将 useState 的实际提供者替换为ReactReconciler, 实现了对 useState 的控制而我们对ReactReconciler接管函数组件useState的过程, 也可以宣告结束.搞定了ReactReconciler的秘密, 在接下来的文章里, 我们就可以放心的研究 HostConfig 和 Container 的设计和实现了HostConfig 与 Container: Reconciler 与 Renderer 间的中间层通过之前的文章我们知道, Fiber 架构下的 React 分为三层, 分别是对外的React Component API, 也就是我们平常写的JSX, 和监控JSX变动, 根据对应虚拟 Dom 结构变更生成界面操作指令的React-Reconciler和将界面操作指令转化为对应平台实现的Renderer渲染器.React component API Reconciler 调和器 ----> Renderer 渲染器Reconciler通过接管useState/setState的实现获取JSX对象的变动情况, 并根据变动调用 JSX 对象的生命周期钩子和计算界面更新指令. 但具体实现时,Reconciler会面临这样一个问题:我怎么知道当前的 Renderer 渲染器支持哪些指令答案当然是在初始化Reconciler时, 就要告诉Reconciler当前渲染器支持的指令列表, 而这份列表, 就叫做HostConfig.对于 HostConfig,Reconciler规定了两类 API, 分别是必须接口和可选接口.按 React 项目组的说法, 这些接口目前还不稳定所以并没有公开介绍. 但实际上, 这个功能已经可以满足日常使用了(要不怎么会有 Remax 项目&一众小程序项目). react 项目组给出了HostConfig 的示例, 这里贴一下 remax 中 hostConfig 的部分内容//位于https://github.dev/remaxjs/remax/blob/v2.15.0/packages/remax-runtime/src/hostConfig/index.tsimport*asschedulerfrom"scheduler";import{REMAX_METHOD,TYPE_TEXT}from"../constants";import{generate}from"../instanceId";importVNodefrom"../VNode";importContainerfrom"../Container";import{createCallbackProxy}from"../SyntheticEvent/createCallbackProxy";importdiffPropertiesfrom"./diffProperties";//...省略其余代码exportdefault{now,//...省略其余代码//创建dom节点createInstance(type:string,newProps:any,container:Container){constid=generate();constnode=newVNode({id,typeOM_TAG_MAP[type]type,props:{},container,});node.props=processProps(newProps,node,id);returnnode;},//创建文本节点createTextInstance(text:string,container:Container){constid=generate();constnode=newVNode({id,type:TYPE_TEXT,props:null,container,});node.text=text;returnnode;},//...省略其余代码//Reconciler更新周期执行完毕后,会调用该接口,通知渲染器可以进行实际渲染//在小程序代码中用于作为向webview发送更新指令的标记resetAfterCommitcontainer:Container)=>{container.applyUpdate();},};Reconciler会根据虚拟 Dom 变动情况, 调用HostConfig中提供的接口, 这些调用方法和参数汇合到一起, 就是界面更新指令. 而对HostConfig接口的调用又会被转发给Container, 由Container对象维护updateQueue数组, 记录操作执行过程.//位于https://github.dev/remaxjs/remax/blob/v2.15.0/packages/remax-runtime/src/Container.tsexportdefaultclassContainer{//...省略其余代码updateQueue:Array=[];//...省略其余代码requestUpdate(update:SpliceUpdate|SetUpdate){this.updateQueue.push(update);}applyUpdate(){if(this.stopUpdate||this.updateQueue.length===0){return;}//...省略其余代码this.context.$spliceData({[this.normalizeUpdatePath([...update.path,"children"])]:[update.start,update.deleteCount,...update.items,],},callback);//...省略其余代码this.updateQueue=[];return;}}当Reconciler的一个更新周期结束时, 会调用HostConfig上的resetAfterCommit函数, 然后被转发给Container的applyUpdate方法.Container收到消息后, 将之前记录下来的界面更新指令 JSON 化为字符串, 通过 Native 转发给 运行在 webview 上的webview-render对象, webview-render 收到更新指令后, 根据指令操作实际 Dom, 界面构建完成.webview-render: 更新指令的设计与用户交互的实现界面的更新指令则由两种类型实现.SpliceUpdate对应于节点变动, 前端收到后直接删除旧 Dom, 创建新 Dom. 但这样会出现问题. 例如, 对于元素, 当 value 发生改变时, 如果直接删除重建 input 元素, 会导致输入光标丢失. 因此出现了SetUpdate指令, 对于该指令, 只更新 Dom 属性, 不重建 Dom.//界面更新指令类型定义//位于https://github.dev/remaxjs/remax/blob/v2.15.0/packages/remax-runtime/src/Container.ts#L8interfaceSpliceUpdate{path:string[];start:number;id:number;deleteCount:number;items:RawNode[];children:RawNode[];type:"splice";node:VNode;}interfaceSetUpdate{path:string[];name:string;value:any;type:"set";node:VNode;}//发送到webview-render端的VNode数据结构//位于https://github.dev/remaxjs/remax/blob/v2.15.0/packages/remax-runtime/src/VNode.ts#L6exportinterfaceRawNode{id:number;type:string;props:any;nodes:{[key:number]:RawNode};children:Array;text:string;}weview-render 收到指令后会根据 node 中的配置创建 Dom 元素, 并更新到 webview 中. 这个比较好实现, 直接document.createElement就行. 前端 render 的难点在于:如何将用户操作时产生的 click/touch/change 事件回传给 js-core 中的 Reconciler我们知道, jsx 中绑定的事件处理函数是不能在 json 化之后传递给 webview-render 的, 但是,不能传递函数, 我们可以传递函数名啊。在生成 Dom 构建命令时, 我们可以建立一个事件处理函数映射表, 函数名命名规范为${事件名}_${递增计数器}_handler. 在 webview 中则用 addEventListener 为对应 dom 节点绑定事件处理函数. 当事件发生时, 把 event 对象中的数据和需要调用的函数名通过 Native 传回 js-core 引擎, 然后在 js-core 中调用对应的实际函数, 触发组件状态变更, 组件重新渲染.至此, 小程序运行流程形成闭环.结尾的话通过上下两篇文章, 我们了解了小程序项目价值, 梳理了开发路线图, 解决了小程序开发过程中最为核心的数据传递和跨进程 Dom 交互问题. 但这并不意味着小程序任务的圆满结束. 事实上, 正如小程序业务流程与开发路线图中分析的那样, 后续的小程序基础库/IDE/后台/组件库更是小程序项目中所面临的难点。不过, 这一系列的文章已经写得太长, 有必要先收束一下. 至于小程序项目中面临的其他问题该怎么解决嘛。欲知后事如何, 请待下回分解。参考资料Remax 实现原理: https://remaxjs.org/guide/implementation-notes/
预览时标签不可点
大前端69小程序5大前端 · 目录#大前端上一篇关系图谱在太阿系统中的应用下一篇小程序开放平台架构指南(上)关闭更多小程序广告搜索「undefined」网络结果
|
|