React 源码解读之 Custom Renderer

引言

从React的渲染流程[1]我们知道,JSX 会先转为一颗 Fiber Tree,然后通过 Renderer 渲染成页面。

对于 Web 平台,这个 Renderer 就是 react-dom,对于 Native 平台,这个 Renderer 就是 react-native

当然,我们也可以创建我们自己的 Renderer,将 React 应用渲染到其他目标平台,比如本文中的 Canvas:

React 源码解读之 Custom Renderer

React 源码解读之 Custom Renderer

下面就来剖析下 Canvas Renderer 的实现方式。

Canvas Renderer

组件

如图,我们的 Canvas Renderer 包括 StageRectCircleText 这些组件,其中将他们一些公共的特征抽离成了一个父类 Layer

React 源码解读之 Custom Renderer

不需要 React,现在的 Canvas Renderer 已经可以渲染出内容了,比如:

const renderDom = document.getElementById('demo')

const stage = new Stage({
  renderDom,
  width500,
  height300,
  style: {border'1px solid gray'},
})
const rect = new Rect({x50y50width100height100color'red'})
const circle = new Circle({x50y50radius20color'green'})
const text = new Text({
  content'我是一个 Demo',
  fillStyle'blue',
  x100,
  y30,
  font'20px serif',
})
rect.appendChild(circle)
stage.appendChild(text)
stage.appendChild(rect)
stage.render()
React 源码解读之 Custom Renderer

Canvas Renderer 实现方式

我们通过引言中第一个 Demo 来分析 Canvas Renderer 的实现方式:

// 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'), {
  width400,
  height200,
  style: {
    backgroundColor'white',
    border'1px solid gray',
  },
})

Demo1 是一个函数组件,返回了 textrectcircle 这些标签,并通过一个 setInterval 定时器实现了一个简单的动画。接下来看看 CanvasRenderer.render 函数做了啥:

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
  },
}

该函数主要是创建了一个 Stage 对象作为 Reconciler 对象 reconcilerInstancecontainer,最后调用 reconcilerInstance.updateContainer() 将 Demo1 组件通过 Canvas Renderer 进行渲染。

我们知道 Reconciler 在 React 渲染流程中充当着非常重要的作用,它会计算出哪些组件需要更新,并会将需要更新的信息提交给 Renderer 来处理,而将 ReconcilerRenderer 连接起来的秘诀就在 HostConfig 之中:

const HostConfig = {
  supportsMutationtrue,
  // 通过 FiberNode 创建 instance,会保存在 FiberNode 的 stateNode 属性上
  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
  },

  /* 操作子组件相关 */
  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)
  },

  /* 组件属性发生变化时会调用该方法 */
  commitUpdate(
    instance,
    updatePayload,
    type,
    oldProps,
    newProps,
    finishedWork
  ) {
    instance.update(newProps)
  },

  // react 流程结束后,调用此方法,我们可以在这里触发我们的渲染器重新渲染
  // 此处参考 remax:https://github.com/remaxjs/remax/blob/80606f640b08c79b9fc61d52a03355f0282c5e14/packages/remax-runtime/src/hostConfig/index.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 {}
  },
  // 暂时不需要实现的接口
  finalizeInitialChildren() {},
  appendAllChildren(...args) {},
  commitTextUpdate(textInstance, oldText, newText) {},
  removeChildFromContainer(container, child) {},
  commitMount(domElement, type, newProps, fiberNode){},
  clearContainer(...args) {},
  createTextInstance(
    newText,
    rootContainerInstance,
    currentHostContext,
    workInProgress
  ) {},
  shouldSetTextContent(...args) {},
}

HostConfig 中是我们的 Canvas Renderer 需要实现的一些接口,这里来说明一下:

supportsMutation

当前渲染器是否支持修改节点,毫无疑问这里必须是 true

createInstance

该函数会在通过 FiberNode 创建宿主相关的元素时进行调用,返回的元素会保存在 FiberNodestateNode 属性上,参考React的渲染流程。

对于 Canvas Renderer 来说,这里会根据 type 值创建出不同的组件。

appendInitialChildappendChildappendChildToContainerinsertBefore

这几个接口都涉及到元素的插入操作,前三个是把元素插到最后面,其中 appendInitialChild 在首次渲染时调用,appendChild 在更新的时候调用,而 appendChildToContainer 则在把元素插入到 container 时使用。

对于 Canvas Renderer 来说,这些接口中均调用 parent.appendChild(child) 即可:

  appendChild(child) {
    this.__children.push(child)
    child.parent = this
  }

insertBefore 则是把元素插入到某个元素前面,同样,Canvas Renderer 也有对应的实现:

  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
      }
    }
  }

commitUpdate

当组件属性发生变化的时候会调用该函数,Canvas Renderer 对应的实现方法也比较简单,即更新 instance 的属性即可:

  update(props) {
    Object.keys(props).forEach((k) => {
      this[k] = props[k]
    })
  }

resetAfterCommit

在React 源码解读之首次渲染流程[1]这篇文章中已阐明 React 的每次更新过程包括 RenderCommit 两大阶段。

其中 Render 阶段会计算出 Effect 链表供 Commit 阶段处理,而 resetAfterCommit 这个函数就是在 Commit 阶段执行完 commitMutationEffects 函数后进行调用,此时所有对元素的更新操作已处理完毕,所以这里是一个适合 Canvas Renderer 调用 container.render() 进行重新渲染的地方。

该函数中首先清空了整个画布,然后依次调用子组件的 render 方法:

// Stage.js
  render() {
    this.context.clearRect(00this.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, 02 * 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)
  }

值得一提的是,Remax[2] 也是在这里触发了小程序的更新。

至此,我们的 Canvas Renderer 的核心实现原理就分析完了,更多内容及 Demo 详见源码[3]

参考资料

[1]

React 源码解读之首次渲染流程: https://mp.weixin.qq.com/s?__biz=MzIwOTM2ODM1OQ==&mid=2247483729&idx=1&sn=b8001469891e097f20db16bd481fe253&chksm=9775a519a0022c0f6563a856a92c1522c9a0269687569d1b0340605df9db6433abc6bdb29119&token=2040200027&lang=zh_CN#rd

[2]

Remax: https://remaxjs.org/

[3]

react-canvas-renderer: https://github.com/ParadeTo/react-canvas-renderer


原文始发于微信公众号(前端游):React 源码解读之 Custom Renderer

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/282343.html

(0)
前端游的头像前端游bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!