renderer

June 12, 2021

React 和 Vue 都使用vdom 来描述ui,这使得基于他们来实现跨平台就显得十分简单。比如React 使用react-dom将ui渲染到 dom上,使用react-native 将 ui渲染到 android / iOS 这样的原生平台上。Vue2的实现中没有将平台相关的逻辑抽离出来,而Vue3则将平台无关的代码与平台相关的代码彻底分割开了,可以通过自定义渲染器将Vue3 渲染到任意平台。

Vue的渲染器 设计,可以看以前的文章

React 的渲染器是怎么设计的呢?我们知道 React通过 reconciler 来 决定如何更新host tree,reconciler 决定了是重新生成dom还是在现有内容上做一些修改。

react-reconciler 有两个工作模式

  • mutation mode: 所有的 host instance 都是可以被修改的。react-dom就是这样的
const view = createView()
updateView(view, { color: 'red' })
  • persistent mode: 整个host tree都是 immutable的,更新就是重建一棵树
view = createView()
view = cloneView(view, { color: 'red' })

一个渲染器包含了两部分内容。

  • built-in component: dom 平台的 div, h1, react-native平台的 View, Text
  • host config: 与host 相关的一些api,比如创建host instance等

此处我们仅仅讨论 渲染器的 host config部分。

在接口上,一个React渲染器只需要实现一个 供 Reconciler 调用的对象接口。

const ReactDOMRenderer = Reconciler(ReactDOMHostConfig)

这个 ReactDOMHostConfig 就是一系列 与平台相关的 基础接口。在这里react中可以看到完整的。

比如下面这个例子就实现了一个简单的dom 渲染器

import ReactReconciler from 'react-reconciler'

ReactReconciler({
	// hostConfig
	supportsMutation: true, // mutation mode
	
	createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
		// 这里的type就是原生dom了,组件的处理已经在react 被处理完了
		const el = document.createElement(type)
		// 简单处理
		['alt', 'className', 'href', 'rel', 'src', 'target'].forEatch(k => {
			if (props[k]) el[k] = props[k]
		})

		if (props.onClick) {
			el.addEventListener('click', props.onClick)
		}
		// react 是平台无关的,这个返回值不是给react使用的,而是给hostConfig其他函数用的
		return el
	},
	createTextInstance(text, rootContianerInstance, hostContext, internalInstanceHandle) {
		return document.createTextNode(text)
	},

	apendChildToContainer(container, child) {
		container.appendChild(child)
	},
	appendChild(container, child) {
		container.appendChild(child)
	},
	appendInitialChild(container, child) {
		container.appendChild(child)
	},
	removeChildFromContainer(container, child) {
		container.removeChild(child)
	}
	removeChild(container, child) {
		container.removeChild(child)
	}
	insertInContainerBefore(container, child, before) {
		container.inserBefore(child, before)
	},
	insertBefore(container, child, before) {
		container.inserBefore(child, before)
	},
	// called in render phase
	prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, currentHostContext) {
		let payload
		if (oldProps.bgColor !== newProps.bgColor) {
			payload = { newBgColor: newProps.bgColor }
		}
		// 返回一个diff 结果,这个结果react 并不关心,是给下一个函数使用的
		return payload
	},
	// called in commit phase
	commitUpdate(instance, updatePayload, type, oldProps, newProps, rootContainerInstance, currentHostContext) {
		if (updatePayload.newBgColor) {
			instance.style.backgroundColor = updatePayload.newBgColor
		}
	}
})

let ReactDomMini = {
	render(comp, target) {
		// disabled concurrent mode and hydration
		let container = reconciler.createContainer(target, false, false)
		reconciler.updateContainer(comp, contaienr, null, null)
	}
}

以上代码参考自 Building a Custom React Renderer | Sophie Alpert

其实可以看到React 和 Vue3的渲染器api设计有一些不同,react是将参数丢给 reconciler,由 reconciler来实现调度。而Vue3则是没有 reconciler这一层,直接由hostConfig 生成 一套通用的 api,vue runtime 调用这一套通用的api实现渲染。

参考


Profile picture

Written by Colgin who lives and works in China, focus on web development. You can comment on github