The introduction
As we know from the React rendering process, JSX is converted to a Fiber Tree and rendered to a page using Renderer. For Web platforms, the Renderer is react-dom, and for Native platforms, the Renderer is React-Native. Of course, we can also create our own Renderer and render the React application to other target platforms, such as the Canvas in this article:
Let’s look at how the Canvas Renderer is implemented.
Canvas Renderer
component
As shown, our Canvas Renderer includes Stage, Rect, Circle, and Text components, which separate some of their common features into a Layer parent class.
You don’t need React. The Canvas Renderer can already render content, such as:
const renderDom = document.getElementById('demo')
const stage = new Stage({
renderDom,
width: 500.height: 300.style: {border: '1px solid gray'}})const rect = new Rect({x: 50.y: 50.width: 100.height: 100.color: 'red'})
const circle = new Circle({x: 50.y: 50.radius: 20.color: 'green'})
const text = new Text({
content: 'I'm a Demo.'.fillStyle: 'blue'.x: 100.y: 30.font: '20px serif',
})
rect.appendChild(circle)
stage.appendChild(text)
stage.appendChild(rect)
stage.render()
Copy the code
Canvas Renderer implementation
We use the first Demo in the introduction to analyze the Canvas Renderer implementation:
// Demo1.jsx
import {useEffect, useState} from 'react'
const R = 20
const W = 100
const H = 100
function Demo1() {
const [x, setX] = useState(R)
const [y, setY] = useState(R)
useEffect(() = > {
setTimeout(() = > {
if (y === R && x < W - R) {
setX(x + 1)}else if (x === W - R && y < H - R) {
setY(y + 1)}else if (y === H - R && x > R) {
setX(x - 1)}else {
setY(y - 1)}},10)
}, [x, y])
return (
<>
<text x={10} y={20} content='DEMO1' font='18px serif' fillStyle='black' />
<rect x={50} y={50} width={W} height={H} color='blue'>
<circle x={x} y={y} radius={R} color='red'>
<rect x={10} y={10} width={20} height={20} color='green' />
</circle>
</rect>
</>)}export default Demo1
// index.js
import CanvasRenderer from './CanvasRenderer'
import Demo1 from './Demo1'
CanvasRenderer.render(<Demo1 />.document.getElementById('demo1'), {
width: 400.height: 200.style: {
backgroundColor: 'white'.border: '1px solid gray',}})Copy the code
Demo1 is a function component that returns the text, rect, and circle tags that need to be rendered by the Canvas Renderer. Let’s see what the render function does:
const reconcilerInstance = Reconciler(HostConfig)
const CanvasRenderer = {
render(element, renderDom, {width, height, style}, callback) {
const stage = new Stage({renderDom, width, height, style})
const isAsync = false // Disables async rendering
const container = reconcilerInstance.createContainer(stage, isAsync) // Creates root fiber node.
const parentComponent = null // Since there is no parent (since this is the root fiber). We set parentComponent to null.
reconcilerInstance.updateContainer(
element,
container,
parentComponent,
callback
) // Start reconcilation and render the result}},Copy the code
This function essentially creates a Stage object for the Reconciler object as a Container for the reconcilerInstance, Last call reconcilerInstance. UpdateContainer () will not components using Canvas rendered at the Renderer. We know that Reconciler plays an important role in the React rendering process, figuring out which components need to be updated and submitting the information that needs to be updated to the Renderer. And the secret to Reconciler and Renderer is in HostConfig:
const HostConfig = {
supportsMutation: true.// Instance created through FiberNode is saved on the FiberNode stateNode property
createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress) {
let element
switch (type) {
case 'rect':
element = new Rect(newProps)
break
case 'circle':
element = new Circle(newProps)
break
case 'text':
element = new Text(newProps)
break
default:
break
}
return element
},
/* Operation subcomponent related */
appendInitialChild(parent, child) {
parent.appendChild(child)
},
appendChildToContainer(parent, child) {
parent.appendChild(child)
},
appendChild(parent, child) {
parent.appendChild(child)
},
insertBefore(parent, child, beforeChild) {
parent.insertBefore(child, beforeChild)
},
removeChild(parent, child) {
parent.removeChild(child)
},
/* This method is called when component properties change
commitUpdate(instance, updatePayload, type, oldProps, newProps, finishedWork) {
instance.update(newProps)
},
// After the react process ends, call this method, where we can trigger our renderer to re-render
// See remax here: https://github.com/remaxjs/remax/blob/80606f640b08c79b9fc61d52a03355f0282c5e14/packages/remax-runtime/src/hostConfig/ind ex.ts#L63
resetAfterCommit(container) {
container.render()
},
getRootHostContext(nextRootInstance) {
const rootContext = {}
return rootContext
},
getChildHostContext(parentContext, fiberType, rootInstance) {
const context = {}
return context
},
prepareForCommit(rootContainerInstance) {
return null
},
prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, currentHostContext) {
return{}},// Interface that is not needed to be implemented for the time being
finalizeInitialChildren() {},
appendAllChildren(. args) {},
commitTextUpdate(textInstance, oldText, newText) {},
removeChildFromContainer(container, child) {},
commitMount(domElement, type, newProps, fiberNode){},
clearContainer(. args) {},
createTextInstance(newText, rootContainerInstance, currentHostContext, workInProgress) {},
shouldSetTextContent(. args){},}Copy the code
HostConfig contains some interfaces that our Canvas Renderer needs to implement. Here are some examples:
supportsMutation
If the current renderer supports modifying nodes, it must be true.
createInstance
This function is called when a host-specific element is created through FiberNode, and the returned element is stored on the FiberNode stateNode property as shown in the React rendering process. For the Canvas Renderer, different components are created based on the type value.
AppendInitialChild, appendChild, appendChildToContainer, insertBefore
AppendInitialChild is called on the first render and appendChild is called on the update. AppendChildToContainer is used when inserting elements into the container. In the case of the Canvas Renderer, parent.appendChild(child) is called:
appendChild(child) {
this.__children.push(child)
child.parent = this
}
Copy the code
InsertBefore inserts an element in front of an element, and the Canvas Renderer has its own implementation:
insertBefore(child, beforeChild) {
for (let i = 0; i < this.__children.length; i++) {
if (this.__children[i] === beforeChild) {
this.__children.splice(i, 0, child)
child.parent = this
break}}}Copy the code
commitUpdate
This function is called when a component property changes, and the Canvas Renderer can be implemented simply by updating the instance property:
update(props) {
Object.keys(props).forEach((k) = > {
this[k] = props[k]
})
}
Copy the code
resetAfterCommit
React update process includes Render and Commit phases. Render phase computs the Effect list for Commit phase. ResetAfterCommit is called after the commitMutationEffects function is executed in the Commit phase. At this point, all updates to elements have been processed. So this is a good place for the Canvas Renderer to call container.render() for rerendering. This function first clears the entire canvas and then calls the render method of the child components in turn:
// Stage.js
render() {
this.context.clearRect(0.0.this.width, this.height)
this.renderChildren()
}
// Layer.js
renderChildren() {
for (let child of this.__children) {
child.render()
}
}
// Rect.js
render() {
const {x, y, stage} = this.resolvePosAndStage()
if(! stage)return
stage.context.beginPath()
stage.context.rect(x, y, this.width, this.height)
stage.context.strokeStyle = this.color
stage.context.stroke()
this.renderChildren()
}
// Circle.js
render() {
const {x, y, stage} = this.resolvePosAndStage()
if(! stage)return
stage.context.beginPath()
stage.context.arc(x, y, this.radius, 0.2 * Math.PI, true)
if (this.fill) {
stage.context.fillStyle = this.color
stage.context.fill()
} else {
stage.context.strokeStyle = this.color
stage.context.stroke()
}
this.renderChildren()
}
// Text.js
render() {
const {x, y, stage} = this.resolvePosAndStage()
if(! stage)return
stage.context.font = this.font
stage.context.fillStyle = this.fillStyle
stage.context.fillText(this.content, x, y)
}
Copy the code
It is also here that Remax triggers the applets update.
At this point, the core implementation principle of our Canvas Renderer is analyzed, more content and Demo see source code.
Welcome to pay attention to the public account “front-end tour”, let us travel in the front-end ocean together.