上個月螞蟻金服前端發佈了一個新的框架 Remax
, 口號是使用真正的、完整的 React 來開發小程序.html
對於本來的 React 開發者來講 'Learn once, write anywhere' , 和 ReactNative 開發體驗差很少,而對於小程序來講則是全新的開發體驗。前端
Taro
號稱是‘類React’的開發方案,可是它是使用靜態編譯的方式實現,邊柳 在它的 《Remax - 使用真正的 React 構建小程序》文章中也提到了這一點:node
所謂靜態編譯,就是使用工具把代碼語法分析一遍,把其中的 JSX 部分和邏輯部分抽取出來,分別生成小程序的模板和 Page 定義。
react
這種方案實現起來比較複雜,且運行時並無 React 存在。git
相比而言,Remax
的解決方案就簡單不少,它不過就是新的React渲染器.github
由於
Remax
剛發佈不久,核心代碼比較簡單,感興趣的能夠去 github 觀摩貢獻
能夠經過 CodeSandbox 遊樂場試玩自定義Renderer: Edit react-custom-renderer
文章看起來比較長,好戲在後頭,一步一步來 🦖算法
文章大綱小程序
建立一個 React 自定義渲染器,你須要對React渲染的基本原理有必定的瞭解。因此在深刻閱讀本文以前,先要確保你可以理解如下幾個基本概念:微信小程序
1. Element微信
咱們能夠經過 JSX
或者 React.createElement
來建立 Element,用來描述咱們要建立的視圖節點。好比:
<button class='button button-blue'>
<b>
OK!
</b>
</button>
複製代碼
JSX
會被轉義譯爲:
React.createElement(
"button",
{ class: 'button button-blue' },
React.createElement("b", null, "OK!")
)
複製代碼
React.createElement
最終構建出相似這樣的對象:
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
props: {
children: 'OK!'
}
}
}
}
複製代碼
也就是說 Element 就是一個普通的對象,描述用戶建立的節點類型、props 以及 children。這些 Elements 組合成樹,描述用戶視圖
2. Component
能夠認爲是 Element 的類型,它有兩種類型:
Host Component: 宿主組件,這是由渲染的平臺提供的‘內置’組件,例如ReactDOM
平臺下面的 DOM
節點,如 div
、span
... 這些組件類型爲字符串
Composite Component: 複合組件,這是一種用戶自定義的組件封裝單位。一般包含自定義的邏輯、狀態以及輸出 Element 樹。複合類型能夠爲類或函數
const DeleteAccount = () => (
<div> <p>Are you sure?</p> <DangerButton>Yep</DangerButton> <Button color='blue'>Cancel</Button> </div>
);
複製代碼
3. Instance
當 React 開始渲染一個 Element 時,會根據組件類型爲它建立一個‘實例’,例如類組件,會調用new
操做符實例化。這個實例會一直引用,直到 Element 從 Element Tree 中被移除。
首次渲染
: React 會實例化一個 MyButton
實例,調用掛載相關的生命週期方法,並執行 render
方法,遞歸渲染下級
render(<MyButton>foo</MyButton>, container)
複製代碼
更新
: 由於組件類型沒有變化,React 不會再實例化,這個屬於‘節點更新’,React 會執行更新相關的生命週期方法,如shouldComponentUpdate
。若是須要更新則再次執行render
方法
render(<MyButton>bar</MyButton>, container)
複製代碼
卸載
: 組件類型不同了, 原有的 MyButton 被替換. MyButton 的實例將要被銷燬,React 會執行卸載相關的生命週期方法,如componentWillUnmount
render(<button>bar</button>, container)
複製代碼
4. Reconciler & Renderer
Reconciler
和 Renderer
的關係能夠經過下圖縷清楚.
Reconciler 的職責是維護 VirtualDOM 樹,內部實現了 Diff/Fiber 算法,決定何時更新、以及要更新什麼
而 Renderer 負責具體平臺的渲染工做,它會提供宿主組件、處理事件等等。例如ReactDOM就是一個渲染器,負責DOM節點的渲染和DOM事件處理。
5. Fiber 的兩個階段 React 使用了 Fiber 架構以後,更新過程被分爲兩個階段(Phase)
若是按照render
爲界,能夠將生命週期函數按照兩個階段進行劃分:
constructor
componentWillMount
廢棄componentWillReceiveProps
廢棄static getDerivedStateFromProps
shouldComponentUpdate
componentWillUpdate
廢棄render
getSnapshotBeforeUpdate()
componentDidMount
componentDidUpdate
componentWillUnmount
沒理解?那麼下文讀起來對你可能比較吃力,建議閱讀一些關於React基本原理的相關文章。
就目前而言,React 大部分核心的工做已經在 Reconciler 中完成,好在 React 的架構和模塊劃分還比較清晰,React官方也暴露了一些庫,這極大簡化了咱們開發 Renderer 的難度。開始吧!
React官方暴露了一些庫供開發者來擴展自定義渲染器:
須要注意的是,這些包仍是實驗性的,API可能不太穩定。另外,沒有詳細的文檔,你須要查看源代碼或者其餘渲染器實現;本文以及擴展閱讀中的文章也是很好的學習資料。
建立一個自定義渲染器只需兩步:
第一步: 實現宿主配置,這是react-reconciler
要求宿主提供的一些適配器方法和配置項。這些配置項定義瞭如何建立節點實例、構建節點樹、提交和更新等操做。下文會詳細介紹這些配置項
const Reconciler = require('react-reconciler');
const HostConfig = {
// ... 實現適配器方法和配置項
};
複製代碼
第二步:實現渲染函數,相似於ReactDOM.render()
方法
// 建立Reconciler實例, 並將HostConfig傳遞給Reconciler
const MyRenderer = Reconciler(HostConfig);
/** * 假設和ReactDOM同樣,接收三個參數 * render(<MyComponent />, container, () => console.log('rendered')) */
export function render(element, container, callback) {
// 建立根容器
if (!container._rootContainer) {
container._rootContainer = ReactReconcilerInst.createContainer(container, false);
}
// 更新根容器
return ReactReconcilerInst.updateContainer(element, container._rootContainer, null, callback);
}
複製代碼
容器既是 React 組件樹掛載的目標
(例如 ReactDOM 咱們一般會掛載到 #root
元素,#root
就是一個容器)、也是組件樹的 根Fiber節點(FiberRoot)
。根節點是整個組件樹的入口,它將會被 Reconciler 用來保存一些信息,以及管理全部節點的更新和渲染。
關於 Fiber 架構的一些細節能夠看這些文章:
HostConfig
支持很是多的參數,完整列表能夠看這裏. 下面是一些自定義渲染器必須提供的參數:
interface HostConfig {
/** * 用於分享一些上下文信息 */
// 獲取根容器的上下文信息, 只在根節點調用一次
getRootHostContext(rootContainerInstance: Container): HostContext;
// 獲取子節點的上下文信息, 每遍歷一個節點都會調用一次
getChildHostContext(parentHostContext: HostContext, type: Type, rootContainerInstance: Container): HostContext;
/** * 節點實例的建立 */
// 普通節點實例建立,例如DOM的Element類型
createInstance(type: Type, props: Props, rootContainerInstance: Container, hostContext: HostContext, internalInstanceHandle: OpaqueHandle,): Instance;
// 文本節點的建立,例如DOM的Text類型
createTextInstance(text: string, rootContainerInstance: Container, hostContext: HostContext, internalInstanceHandle: OpaqueHandle): TextInstance;
// 決定是否要處理子節點/子文本節點. 若是不想建立則返回true. 例如ReactDOM中使用dangerouslySetInnerHTML, 這時候子節點會被忽略
shouldSetTextContent(type: Type, props: Props): boolean;
/** * 節點樹構建 */
// 若是節點在*未掛載*狀態下,會調用這個來添加子節點
appendInitialChild(parentInstance: Instance, child: Instance | TextInstance): void;
// **下面都是反作用(Effect),在’提交‘階段被執行**
// 添加子節點
appendChild?(parentInstance: Instance, child: Instance | TextInstance): void;
// 添加子節點到容器節點(根節點)
appendChildToContainer?(container: Container, child: Instance | TextInstance): void;
// 插入子節點
insertBefore?(parentInstance: Instance, child: Instance | TextInstance, beforeChild: Instance | TextInstance): void;
// 插入子節點到容器節點(根節點)
insertInContainerBefore?(container: Container, child: Instance | TextInstance, beforeChild: Instance | TextInstance,): void;
// 刪除子節點
removeChild?(parentInstance: Instance, child: Instance | TextInstance): void;
// 從容器節點(根節點)中移除子節點
removeChildFromContainer?(container: Container, child: Instance | TextInstance): void;
/** * 節點掛載 */
// 在完成全部子節點初始化時(全部子節點都appendInitialChild完畢)時被調用, 若是返回true,則commitMount將會被觸發
// ReactDOM經過這個屬性和commitMount配置實現表單元素的autofocus功能
finalizeInitialChildren(parentInstance: Instance, type: Type, props: Props, rootContainerInstance: Container, hostContext: HostContext): boolean;
// 和finalizeInitialChildren配合使用,commitRoot會在’提交‘完成後(resetAfterCommit)執行, 也就是說組件樹渲染完畢後執行
commitMount?(instance: Instance, type: Type, newProps: Props, internalInstanceHandle: OpaqueHandle): void;
/** * 節點更新 */
// 準備節點更新. 若是返回空則表示不更新,這時候commitUpdate則不會被調用
prepareUpdate(instance: Instance, type: Type, oldProps: Props, newProps: Props, rootContainerInstance: Container, hostContext: HostContext,): null | UpdatePayload;
// **下面都是反作用(Effect),在’提交‘階段被執行**
// 文本節點提交
commitTextUpdate?(textInstance: TextInstance, oldText: string, newText: string): void;
// 普通節點提交
commitUpdate?(instance: Instance, updatePayload: UpdatePayload, type: Type, oldProps: Props, newProps: Props, internalInstanceHandle: OpaqueHandle): void;
// 重置普通節點文本內容, 這個須要和shouldSetTextContent(返回true時)配合使用,
resetTextContent?(instance: Instance): void;
/** * 提交 */
// 開始’提交‘以前被調用,好比這裏能夠保存一些狀態,在’提交‘完成後恢復狀態。好比ReactDOM會保存當前元素的焦點狀態,在提交後恢復
// 執行完prepareForCommit,就會開始執行Effects(節點更新)
prepareForCommit(containerInfo: Container): void;
// 和prepareForCommit對應,在提交完成後被執行
resetAfterCommit(containerInfo: Container): void;
/** * 調度 */
// 這個函數將被Reconciler用來計算當前時間, 好比計算任務剩餘時間
// ReactDOM中會優先使用Performance.now, 普通場景用Date.now便可
now(): number;
// 自定義計時器
setTimeout(handler: (...args: any[]) => void, timeout: number): TimeoutHandle | NoTimeout;
// 取消計時器
clearTimeout(handle: TimeoutHandle | NoTimeout): void;
// 表示一個空的計時器,見👆clearTimeout的簽名
noTimeout: NoTimeout;
// ? 功能未知
shouldDeprioritizeSubtree(type: Type, props: Props): boolean;
// 廢棄
scheduleDeferredCallback(callback: () => any, options?: { timeout: number }): any;
// 廢棄
cancelDeferredCallback(callbackID: any): void;
/** * 功能開啓 */
// 開啓節點修改,通常渲染器都會開啓,否則沒法更新節點
supportsMutation: boolean;
// 開啓持久化 ?
supportsPersistence: boolean;
// 開啓hydrate,通常用於服務端渲染
supportsHydration: boolean;
/** * 雜項 */
// 獲取可公開的節點實例,即你願意暴露給用戶的節點信息,用戶經過ref能夠獲取到這個對象。通常自定義渲染器原樣返回便可, 除非你想有選擇地給用戶暴露信息
getPublicInstance(instance: Instance | TextInstance): PublicInstance;
// ... 還有不少參數,因爲通常渲染器不會用到,暫時不講了
}
複製代碼
若是按照Fiber的兩個階段
來劃分的話,接口分類是這樣的:
協調階段 | 開始提交 | 提交階段 | 提交完成 |
---|---|---|---|
createInstance | prepareCommit | appendChild | resetAfterCommit |
createTextInstance | appendChildToContainer | commitMount | |
shouldSetTextContent | insertBefore | ||
appendInitialChild | insertInContainerBefore | ||
finalizeInitialChildren | removeChild | ||
prepareUpdate | removeChildFromContainer | ||
commitTextUpdate | |||
commitUpdate | |||
resetTextContent |
經過上面接口定義能夠知道 HostConfig
配置比較豐富,涉及節點操做、掛載、更新、調度、以及各類生命週期鉤子, 能夠控制渲染器的各類行爲.
看得有點蒙圈?不要緊, 你暫時沒有必要了解全部的參數,下面會一點一點展開解釋這些功能。你能夠最後再回來看這裏。
React中有兩種組件類型,一種是宿主組件(Host Component)
, 另外一種是複合組件(CompositeComponent)
. 宿主組件
是平臺提供的,例如 ReactDOM
平臺提供了 div
、span
、h1
... 等組件. 這些組件一般是字符串類型,直接渲染爲平臺下面的視圖節點。
而複合組件
,也稱爲自定義組件
,用於組合其餘複合組件
和宿主組件
,一般是類或函數。
渲染器不須要關心複合組件
的處理, Reconciler 交給渲染器的是一顆宿主組件樹
。
固然在 Remax
中,也定義了不少小程序特定的宿主組件
,好比咱們能夠這樣子使用它們:
function MyComp() {
return <view><text>hello world</text></view>
}
複製代碼
Reconciler
會調用 HostConfig
的 createInstance
和createTextInstance
來建立宿主組件
的實例,因此自定義渲染器必須實現這兩個方法. 看看 Remax
是怎麼作的:
const HostConfig = {
// 建立宿主組件實例
createInstance(type: string, newProps: any, container: Container) {
const id = generate();
// 預處理props, remax會對事件類型Props進行一些特殊處理
const props = processProps(newProps, container, id);
return new VNode({
id,
type,
props,
container,
});
},
// 建立宿主組件文本節點實例
createTextInstance(text: string, container: Container) {
const id = generate();
const node = new VNode({
id,
type: TYPE_TEXT,
props: null,
container,
});
node.text = text;
return node;
},
// 判斷是否須要處理子節點。若是返回true則不建立,整個下級組件樹都會被忽略。
// 有一些場景是不須要建立文本節點的,而是由父節點內部消化。
// 舉個例子,在ReactDOM中,若是某個節點設置了dangerouslySetInnerHTML,那麼它的children應該被忽略,
// 這時候 shouldSetTextContent則應該返回true
shouldSetTextContent(type, nextProps) {
return false
}
}
複製代碼
在 ReactDOM 中上面兩個方法分別會經過 document.createElement
和 document.createTextNode
來建立宿主組件
(即DOM節點
)。
上面是微信小程序的架構圖(圖片來源: 一塊兒脫去小程序的外套 - 微信小程序架構解析)。
由於小程序隔離了渲染進程
和邏輯進程
。Remax
是跑在邏輯進程
上的,在邏輯進程
中沒法進行實際的渲染, 只能經過setData
方式將更新指令傳遞給渲染進程
後,再進行解析渲染。
因此Remax
選擇在邏輯進程
中先構成一顆鏡像樹
(Mirror Tree), 而後再同步到渲染進程
中,以下圖:
上面的 VNode
就是鏡像樹中的虛擬節點
,主要用於保存一些節點信息,不作任何特殊處理, 它的結構以下:
export default class VNode {
id: number; // 惟一的節點id
container: Container;
children: VNode[]; // 子節點
mounted = false; // 節點是否已經掛載
type: string | symbol; // 節點的類型
props?: any; // 節點的props
parent: VNode | null = null; // 父節點引用
text?: string; // 若是是文本節點,這裏保存文本內容
path(): Path // 節點的路徑. 同步到渲染進程後,經過path恢復到樹中
// 子節點操做
appendChild(node: VNode, immediately: boolean)
removeChild(node: VNode, immediately: boolean)
insertBefore(newNode: VNode, referenceNode: VNode, immediately: boolean)
update() // 觸發同步到渲染進程
toJSON(): string
}
複製代碼
VNode 的完整代碼能夠看這裏
要構建出完整的節點樹須要實現HostConfig
的 appendChild
、insertBefore
、removeChild
等方法, 以下, 這些方法都比較容易理解,因此不須要過多解釋。
const HostConfig = {
// ...
// 支持節點修改
// 有些靜態渲染的場景,例如渲染爲pdf文檔,這時候能夠關閉
// 當關閉時,只須要實現appendInitiaChild
supportsMutation: true,
// 用於初始化(首次)時添加子節點
appendInitialChild: (parent: VNode, child: VNode) => {
parent.appendChild(child, false);
},
// 添加子節點
appendChild(parent: VNode, child: VNode) {
parent.appendChild(child, false);
},
// 插入子節點
insertBefore(parent: VNode, child: VNode, beforeChild: VNode) {
parent.insertBefore(child, beforeChild, false);
},
// 刪除節點
removeChild(parent: VNode, child: VNode) {
parent.removeChild(child, false);
},
// 添加節點到容器節點,通常狀況咱們不須要和appendChild特殊區分
appendChildToContainer(container: any, child: VNode) {
container.appendChild(child);
child.mounted = true;
},
// 插入節點到容器節點
insertInContainerBefore(container: any, child: VNode, beforeChild: VNode) {
container.insertBefore(child, beforeChild);
},
// 從容器節點移除節點
removeChildFromContainer(container: any, child: VNode) {
container.removeChild(child);
},
}
複製代碼
上一節講的是樹結構層面的更新,當節點屬性變更或者文本內容變更時,也須要進行更新。咱們能夠經過下列 HostConfig
配置來處理這類更新:
const HostConfig = {
/** * 更新相關 */
// 能夠在這裏比對props,若是props沒有變化則不進行更新,這和React組件的shouldComponentUpdate差很少
// **返回’空‘則表示不更新該節點, 這時候commitUpdate則不會被調用**
prepareUpdate(node: VNode, type: string, oldProps: any, newProps: any) {
oldProps = processProps(oldProps, node.container, node.id);
newProps = processProps(newProps, node.container, node.id);
if (!shallowequal(newProps, oldProps)) {
return true;
}
return null;
},
// 進行節點更新
commitUpdate(
node: VNode,
updatePayload: any,
type: string,
oldProps: any,
newProps: any
) {
node.props = processProps(newProps, node.container, node.id);
node.update();
},
// 進行文本節點更新
commitTextUpdate(node: VNode, oldText: string, newText: string) {
if (oldText !== newText) {
node.text = newText;
// 更新節點
node.update();
}
},
}
複製代碼
Ok, 這個也比較好理解。 對於普通節點更新,Reconciler
會先調用 prepareUpdate
, 肯定是否要更新,若是返回非空數據,Reconciler
就會將節點放入 Effects
鏈中,在提交
階段調用 commitUpdate
來執行更新。 文本節點更新則直接調用 commitTextUpdate
,不在話下.
React 的更新的兩個階段
這個概念很是重要,這個也體如今HostConfig上:
const HostConfig = {
// Reconciler說,我要開始提交了,你提交前要作什麼,就在這作吧
// 好比ReactDOM會在這裏保存當前DOM文檔的選中狀態和焦點狀態, 以及禁用事件處理。由於DOM更新可能會破壞這些狀態
prepareForCommit: () => {},
// Reconciler說,我已經提交完了
// ReactDOM會在這裏恢復提交前的DOM文檔的選中狀態和焦點狀態
resetAfterCommit: () => {},
// 在協調階段,當一個節點完成'建立'後調用。若是有子節點,則在全部子節點appendInitialChild完成後調用
// 返回一個boolean值表示’完成提交‘後是否要調用commitMount. 通俗講就是告訴Reconciler,當前節點完成’掛載‘後要執行某些東西
// ReactDOM會使用這個鉤子來處理帶有autofoucs屬性的節點,在commitMount中實現自動獲取焦點
finalizeInitialChildren: () => false,
// 和finalizeInitialChildren配合使用,若是前者返回true,在Reconciler完成提交後,對應節點的commitMount會被執行
commitMount: () => {},
}
複製代碼
將上文講到的全部鉤子都聚合起來,按照更新的階段和應用的目標(target)進行劃分,它們的分佈是這樣的:
那麼對於 Remax
來講, 何時應該將'更新'提交到渲染進程
呢?答案是上圖全部在提交階段
的方法被調用時。
提交階段
原意就是用於執行各類反作用的,例如視圖更新、遠程方法請求、訂閱... 因此 Remax
也會在這個階段收集更新指令
,在下一個循環推送給渲染進程。
回顧一下自定義渲染器各類方法調用的流程, 首先看一下掛載的流程:
假設咱們的組件結構以下:
const container = new Container()
const MyComp = () => {
return (
<div> <span>hello world</span> </div>
)
}
render(
<div className="root"> <MyComp /> <span>--custom renderer</span> </div>,
container,
() => {
console.log("rendered")
},
)
複製代碼
React 組件樹的結構以下(左圖),但對於渲染器來講,樹結構是右圖。 自定義組件是React 層級的東西,渲染器只須要關心最終須要渲染的視圖結構, 換句話說渲染器只關心宿主組件
:
掛載會經歷如下流程:
經過上面的流程圖,能夠很清晰看到每一個鉤子的調用時機。
同理,咱們再來看一下節點更新時的流程. 咱們稍微改造一下上面的程序,讓它定時觸發更新:
const MyComp = () => {
const [count, setCount] = useState(1)
const isEven = count % 2 === 0
useEffect(() => {
const timer = setInterval(() => {
// 遞增計數器
setCount(c => c + 1)
}, 10000)
return () => clearInterval(timer)
}, [])
return (
<div className="mycomp" style={{ color: isEven ? "red" : "blue" }}> {isEven ? <div>even</div> : null} <span className="foo">hello world {count}</span> </div>
)
}
複製代碼
下面是更新的流程:
當MyComp
的 count
由1變爲2時,MyComp
會被從新渲染,這時候新增了一個div
節點(紅色虛框), 另外 hello world 1
也變成了 hello world 2
。
新增的 div
節點建立流程和掛載時同樣,只不過它不會當即插入到父節點中,而是先放到Effect
鏈表中,在提交階段
統一執行。
同理hello world {count}
文本節點的更新、以及其餘節點的 Props 更新都是放到Effect鏈表中,最後時刻才更新提交. 如上圖的 insertBefore
、commitTextUpdate
、commitUpdate
.
另一個比較重要的是 prepareUpdate
鉤子,你能夠在這裏告訴 Reconciler,節點是否須要更新,若是須要更新則返回非空值,這樣 commitUpdate
纔會被觸發。
React 自定義渲染器差很少就這樣了,接下來就是平臺相關的事情了。 Remax
目前的作法是在觸發更新後,經過小程序 Page
對象的 setData
方法將更新指令
傳遞給渲染進程; 渲染進程
側再經過 WXS
機制,將更新指令
恢復到樹中; 最後再經過模板
機制,將樹遞歸渲染出來。
總體的架構以下:
先來看看邏輯進程
側是如何推送更新指令的:
// 在根容器上管理更新
export default class Container {
// ...
// 觸發更新
requestUpdate(
path: Path,
start: number,
deleteCount: number,
immediately: boolean,
...items: RawNode[]
) {
const update: SpliceUpdate = {
path, // 更新節點的樹路徑
start, // 更新節點在children中的索引
deleteCount,
items, // 當前節點的信息
};
if (immediately) {
this.updateQueue.push(update);
this.applyUpdate();
} else {
// 放入更新隊列,延時收集更新指令
if (this.updateQueue.length === 0) {
setTimeout(() => this.applyUpdate());
}
this.updateQueue.push(update);
}
}
applyUpdate() {
const action = {
type: 'splice',
payload: this.updateQueue.map(update => ({
path: stringPath(update.path),
start: update.start,
deleteCount: update.deleteCount,
item: update.items[0],
})),
};
// 經過setData通知渲染進程
this.context.setData({ action });
this.updateQueue = [];
}
}
複製代碼
邏輯仍是比較清楚的,即將須要更新的節點(包含節點路徑、節點信息)推入更新隊列,而後觸發 setData
通知到渲染進程
。
渲染進程
側,則須要經過 WXS
機制,相對應地將更新指令
恢復到渲染樹
中:
// 渲染樹
var tree = {
root: {
children: [],
},
};
// 將指令應用到渲染樹
function reduce(action) {
switch (action.type) {
case 'splice':
for (var i = 0; i < action.payload.length; i += 1) {
var value = get(tree, action.payload[i].path);
if (action.payload[i].item) {
value.splice(
action.payload[i].start,
action.payload[i].deleteCount,
action.payload[i].item
);
} else {
value.splice(action.payload[i].start, action.payload[i].deleteCount);
}
set(tree, action.payload[i].path, value);
}
return tree;
default:
return tree;
}
}
複製代碼
OK, 接着開始渲染, Remax
採用了模板
的形式進行渲染:
<wxs src="../../helper.wxs" module="helper" />
<import src="../../base.wxml"/>
<template is="REMAX_TPL" data="{{tree: helper.reduce(action)}}" />
複製代碼
Remax
爲每一個組件類型都生成了一個template
,動態'遞歸'渲染整顆樹:
<template name="REMAX_TPL">
<block wx:for="{{tree.root.children}}" wx:key="{{id}}">
<template is="REMAX_TPL_1_CONTAINER" data="{{i: item}}" />
</block>
</template>
<wxs module="_h">
module.exports = {
v: function(value) {
return value !== undefined ? value : '';
}
};
</wxs>
<% for (var i = 1; i <= depth; i++) { %>
<%var id = i; %>
<% for (let component of components) { %>
<%- include('./component.ejs', {
props: component.props,
id: component.id,
templateId: id,
}) %>
<% } %>
<template name="REMAX_TPL_<%=id%>_plain-text" data="{{i: i}}">
<block>{{i.text}}</block>
</template>
<template name="REMAX_TPL_<%=id%>_CONTAINER" data="{{i: i}}">
<template is="{{'REMAX_TPL_<%=id%>_' + i.type}}" data="{{i: i}}" />
</template>
<% } %>
複製代碼
限於小程序的渲染機制,如下因素可能會影響渲染的性能:
WXS
這類方案,用來處理複雜的視圖交互問題,好比動畫。將來 Remax
也須要考慮這個問題Reconciler
這一層已經進行了 Diff,到渲染進程
可能須要重複再作一遍?本文以 Remax
爲例,科普一個 React 自定義渲染器是如何運做的。對於 Remax
,目前還處於開發階段,不少功能還不完善。至於性能如何,筆者還很差作評論,能夠看官方給出的初步基準測試。有能力的同窗,能夠參與代碼貢獻或者 Issue 討論。
最後謝謝邊柳對本文審校和建議。