阅读 2228

React 实践揭秘之旅,中高级前端必备(下)

引言

上一篇文章我们主要实现了 JSXWebGL 上的渲染与更新,对 虚拟DOMDiff 有了更深的了解,但相比于我们使用的 React,还缺乏了之中很重要的一环 --- 组件模式

想必大家能认同,React组件(Component)具有 强大的功能,高拓展性和高解耦性,在其基础上构建的各种 UI 组件框架完全改变了传统的 Web 开发模式,成为了Web 中的大型复杂应用提供了一种很好的构建模式和保障,也让我们的开发效率也有了质的变化。

在这篇文章中,我们会在上一篇的实现基础上,加入 React 组件模式,并在实现的过程中适时的去讲解一些原理和思维,也有利于大家 由浅入深的理解编程思维上的提升

由于下篇是完全以上篇作为基础的。所以如果你还没看过上篇,请优先猛戳:

React 实践揭秘之旅,中高级前端必备(上) ->

第七站: 破碎之墟 - Component

代码复用性,向来是编程领域一个核心的理念。我们最常使用 函数、类 方式进行代码的封装,但这里有个痛点:

Web 中 UI 与 逻辑 的分离的特性,导致较难优雅地整合封装。

通常我们需要引 JSCSS,再在 HTML 中按规定书写结构,最后再初始化库。整个过程十分割裂,且不优雅。

React Component 帮我们解决了这个痛点,即保持了 动态UI逻辑 的分离,又有了全新的整合方式。从而让 UI 组件变得非常的 高效易用,只需要在结构中以标签的形式引入即可。

我们就继续在上篇文章的基础上,加入 Component 的特性。基于 JSX 的变量传递,我们只需要实现一个 Component 类,并针对性地去完成组件的 渲染和更新 即可。

TIPs:

由于组件的加入,这时我们的 虚拟DOM(VNode) 就包含了两种类型: 组件节点(compVNode)元素节点(elVNode)。后续都会以此区分,便于讲解。

// 组件基类
class Component {
    // 通过保持三份不同的时期的快照
    // 有利于对组件的状态及属性的管理和追踪

    // 属性
    public __prevProps
    public props = {}
    public __nextProps
    
    // 状态
    public __prevState
    public state = {}
    public __nextState
    
    // 储存当前组件渲染出的 VNode
    public __vnode
    
    // 储存 组件节点
    public __component
    
    constructor(props) {
        // 初始化参数
        this.props = props
    }
    public render(): any { return null }
    public __createVNode() {
        // 该方法用于封装重新执行 render 的逻辑
    
        // state 与 props 状态变更的逻辑
        this.__prevProps = this.props
        this.__prevState = this.state

        this.props = this.__nextProps
        this.state = this.__nextState

        this.__nextState = this.__nextProps = undefined
        
        // 重新执行 render 生成 VNode
        this.__vnode = this.render()
        
        return this.__vnode
    }
}
复制代码

有了这个类后,我们就可以使用 React 的方式继承出 自定义组件 进行渲染:

class App extends Component {
    constructor(props) {
        super(props)
        this.state = {
            content: 'ReactWebGL Component, Hello World',
        }
    }
    public render() {
        return (
            <Container name="parent">
                {this.state.content}
            </Container>
        )
    }
}

render(<App />, game.stage)
复制代码

由于我们之前提过,JSX 是以 变量传递 的形式编译的。因此将 <App /> 转换成 VNode 后,其 vnode.type 的值便是 App 这个类,并不是类似 div 那样的 字符串标签名。所以接下来我们需要实现一个 组件初始化 的函数(Component)。这个函数的主要功能是:

实例化组件并获取组件 render 方法中返回的 元素节点。

// 渲染组件
function renderComponent(compVNode) {
    const { type: Comp, props } = compVNode
    let instance = compVNode.instance
    
    // 当 instance 已经存在时,则不需要重新创建实例
    if (!instance) {
        // 传入 props,初始化组件实例
        // 支持 类组件 或者 函数组件
        if (Comp.prototype && Comp.prototype.render) {
            instance = new Comp(props)
        } else {
            instance = new Component(props)
            instance.constructor = Comp
            instance.render = () => instance.constructor(props)
        }
        
	    // 初次渲染时
	    // 将来的属性与状态其实便是与当前值一致
	    instance.__nextProps = props
	    instance.__nextState = instance.state
    }    
    
    // 调用 render 获取 VNode
    const vnode = instance.__createVNode()

    // 组件、元素、实例之间保持相互引用,有利于双向连接整棵虚拟树
    instance.__component = compVNode
    compVNode.instance = instance
    compVNode.vnode = vnode
    vnode.component = compVNode

    return vnode
}
复制代码

接下来我们只需要在 createElm 的函数加入: 当传入的为 组件节点 时调用函数初始化生成 元素节点,后续只需要继续原有的逻辑继续创建,便能正确渲染组件。

function createElm(vnode) {
	// 当为组件时,初始化组件
	// 重新赋值成 元素节点
    if (typeof vnode.type === 'function') {
        vnode = renderComponent(vnode) 
    }
    
    // 维持原有逻辑
    ...
    
    return vnode.elm
}
复制代码

保存执行,页面中已经能正确的渲染出 <App> 组件了。延续之前的逻辑,在完成初次渲染后,接下来就是组件的更新。这就是我们最常使用的 this.setState了。

第八站: 净化之匙 - setState

在上一篇文章中,我们实现了 虚拟DOM 更新函数 diff。参数为 新旧虚拟节点(oldVNodenewVNode)。所以组件更新的原理也一样:

获取组件实例先后渲染的新旧 VNode 再触发 diff 函数。

在刚才 Component的渲染中,我们已经把 render 生成的 VNode 保存在 this.__vnode 上,这便是初始化时生成的 旧虚拟节点(oldVNode)。所以我们要做的就是:

setState 中 更新状态,调用 render 生成 新虚拟节点(newVNode),触发 diff 更新。

因此我们在类上新增两个方法: setState__update:

class Component {
	// 其余方法
	...
	
	// 更新函数
	public __update = () => {
        // 临时存储 旧虚拟节点 (oldVNode)
        const oldVNode = this.__vnode
        
        this.__nextProps = this.props
        if (!this.__nextState) this.__nextState = this.state

        // 重新生成 新虚拟节点(newVNode)
        this.__vnode = this.__createVNode()

        // 调用 diff,更新 VNode
        diff(oldVNode, this.__vnode)
    }
    
    // 更新状态
    public setState(partialState, callback?) {
        // 合并状态, 暂存于 即将更新态 中
        if (typeof partialState === 'function') {
            partialState = partialState(this.state)
        }
        this.__nextState = {
            ...this.state,
            ...partialState,
        }

        // 调用更新,并执行回调
        this.__update()
        callback && callback()
    }
}
复制代码

更新优化

到这里我们可以看出: setState 封装了 diff 方法。但由于 diff 的复杂度,性能的优化会是一个我们需要着重考虑的点。每执行一次 setState,就需要重新生成 newVNode 进行 diff。因此,当组件非常复杂时或者连续更新时,可能会导致 主进程的阻塞,造成页面的卡死。

这里我们需要有两个优化:

  • setState 异步化,避免阻塞主进程;

  • setState 合并,多次连续调用会被最终合并成一次;

    • 同一组件 连续多次更新

    • 父子级连续触发更新,由于 父级更新其实已经包含子级的更新,此时如果子级再自我更新一次,则就变成了一种无谓消耗;

为了这个优化,我们首先需要一个更新队列功能:

  • 可以异步化调用更新

  • 为组件标注,保证一次循环中单个组件只会更新一次

我们先来实现个 异步执行队列:

// 更新异步化,采用属于微任务的 Promise,兼容性使用 setTimeout
// 这里使用微任务,可以保证宏任务的优先执行
// 保证例如 UI渲染 等更为重要的任务,避免页面卡顿;
const defer = typeof Promise === 'function' 
    ? Promise.prototype.then.bind(Promise.resolve())
    : setTimeout

// 更新队列
const updateQueue: any[] = []

// 队列更新 API
export function enqueueRender(updater) {

    // 将所有 updater 同步推入更新队列中
    // 为实例添加一个属性 __dirty,标识是否处于待更新状态
    // 初始 和 更新完毕,该值会被置为 false
    // 推入队列时,标记为 true
    if (
        !updater.__dirty && 
        (updater.__dirty = true) && 
        updateQueue.push(updater) === 1
    ) {
        // 异步化冲洗队列
        // 最终只执行一次冲洗
        defer(flushRenderQueue)
    }
}

// 合并一次循环中多次 updater
function flushRenderQueue() {
    if (updateQueue.length) {
        // 排序更新队列
        updateQueue.sort()

        // 循环队列出栈
        let curUpdater = updateQueue.pop()
        while (curUpdater) {
        
            // 当组件处于 待更新态 时,触发组件更新
            // 如果该组件已经被更新完毕,则该状态为 false
            // 则后续的更新均不再执行
            if (curUpdater.__dirty) {
                // 调用组件自身的更新函数
                curUpdater.__update()

                // 执行 callback
                flushCallback(curUpdater)
            }
            
            curUpdater = updateQueue.pop()
        }
    }
}

// 执行缓存在 updater.__setStateCallbacks 上的回调
function flushCallback(updater) {
    const callbacks = updater.__setStateCallbacks
    let cbk
    if (callbacks && callbacks.length) {
        while (cbk = callbacks.shift()) cbk.call(updater)
    }   
}
复制代码

完成这个方法后,我们来修改下上面的 setState 函数:

class Component {
	// 其余方法
	...
	
	public setState(partialState = {}, callback?) {
        // 合并状态, 暂存于 即将更新态 中
        // 处理参数为函数的形式
        if (typeof partialState === 'function') {
            partialState = partialState(this.state, this.props)
        }
        
        this.__nextState = {
            ...this.state,
            ...partialState,
        }

        // 缓存回调
        callback && this.__setStateCallbacks.push(callback)
        // 把组件自身先推入更新队列
        enqueueUpdate(this)
	}
}
复制代码

此时,由于 更新队列 为异步的,因此当多次连续调用 setState 时,组件的状态会被 同步合并,待全部完成后,才会进入更新队列的冲洗并最终只执行一次组件更新。

React 中组件还有个更新的方法: forceUpdate(callback),该方法的功能其实是与 this.setState({}, callback) 相同的,唯一有个需要注意的点就是: 触发的更新不需要经过 shouldComponentUpdate 的判断,实现只需要加个标识位即可。

优化策略

回到性能优化这个点,从这里的简单实现我们可以看出: 虽然异步化了更新流程,但本质上仍然没有解决 复杂的组件 diff 带来长时间执行阻塞主进程。我记得以前文章有说过: 最有效的性能优化方式就是 异步、任务分割 和 缓存策略。

1. 异步化:

通过把同步的代码执行变成异步,把串行变成并行,可以有效提高 执行的时间利用率保证代码优先级。从这里可以延伸出两种优化方向:

    1. 异步: 如我们上面所做的优化,这样能保证主进程的执行优先级,保证页面渲染或者更主要任务的优先执行,避免卡顿;
    1. 并行: 通过把某些高消耗的操作放到 非主进程 上执行,例如 worker 线程。不过由于 diff 本身就较为复杂,还要需要处理好主进程与线程之间的交互,会导致复杂度极高,但也并非不可行,后续也许是个优化方向。
    • 例如我就在思考在这里引入 wasm 的可能性,代价与收益比如何,有兴趣的童鞋可以一起探讨。

2. 任务分割

将原本会阻塞主进程的 大块逻辑执行进行拆解,分割成一个个小任务。从而可以在逻辑中找到合适的时机点 分段执行,即 不会阻塞主进程,又可以让代码快速高效的执行,最大化利用物理资源。

Facebook 的大神们选择了这条优化方向,这就是 React 16 新引入的 Fiber 理念的最主要目的。上面我们实现的 diff 中,有着一个很大的障碍:

一棵完整 虚拟DOM树 更新,必须一次性更新完成,中间无法被暂停,也无法被分割。

Fiber 最主要的功能就是 指针映射,保存上一个更新的组件与下一步需要更新的组件,从而完成 可暂停可重启。计算进程的运行时间,利用浏览器的 requestIdleCallbackrequestAnimationFrame 接口,当有优先级更高的任务时,优先执行,暂停下一个组件的更新。待空闲时再重启更新。

Fiber 算是一种编程思想,在其它语言中也有许多应用(Ruby Fiber)。核心思想是:

任务拆分和协同,主动把执行权交给主线程,使主线程有时间空挡处理其他高优先级任务。

但实现复杂度较高,为了本文便于理解,暂时并没有引入。等以后有机会我们再来一起深挖 Fiber 的实现,也许能成为更多使用场景的性能优化手段。

子组件更新

在上篇文章中,我们优先实现了 diffVNode 方法用于更新 元素节点,但组件节点的更新与元素节点的更新是不同的。当出现组件嵌套的情况时,我们就需要一个新的方法(diffComponent)用于组件节点的更新。

元素节点 不同,组件节点之间的更新重要的是重渲染,类似于我们上面的 setState

复用已创建好的组件实例,根据新的 状态(state)与 属性(props) 重新执行 render 生成 元素节点,再递归比对

也就是说,我们需要在 diffVNode 外围再做一层判断处理:

function diff(oldVNode, newVNode) {
	if (isSameVNode(oldVNode, newVNode)) {
        if (typeof oldVNode.type === 'function') {
            // 组件节点
            diffComponent(oldVNode, newVNode)
        } else {
            // 元素节点,
            // 直接执行比对
            diffVNode(oldVNode, newVNode)
        }
	} else {
		// 新节点替换旧节点
		...
	}
}


// 组件比对
function diffComponent(oldCompVNode, newCompVNode) {
    const { instance, vnode: oldVNode, elm } = oldCompVNode
    const { props: nextProps } = newCompVNode

    if (instance && oldVNode) {
        instance.__dirty = false
        
        // 更新状态和属性
        instance.__nextProps = nextProps
        if (!instance.__nextState) instance.__nextState = instance.state
        
        // 复用旧组件实例和元素
        newCompVNode.instance = instance
        newCompVNode.elm = elm

        // 使用新属性、新状态,旧组件实例
        // 重新生成 新虚拟DOM
        const newVNode = initComponent(newCompVNode)
        
        // 递归触发 diff
        diff(oldVNode, newVNode)
    }
}
复制代码

第九站: 生命之泉 - Lifecycle

组件有一个相当重要的特征,便是具有 生命周期。不同的函数钩子对应了组件 从初始化到销毁 的各个关键时间点。主要是为了让业务方有能力 插入组件的渲染工作流 中,编写业务逻辑。我们先来简单梳理下最新 React 组件的生命周期:

首次渲染:

  • constructor

    • 即组件的 实例化时机,通常可以用来设置初始化 state
  • static getDerivedStateFromProps(nextProps, prevState)

    • 在组件的模板渲染中,我们通常使用的数据为 stateprops,而 props 由父级传入,组件本身并无法直接修改,因此唯一的常见需求就是: 根据父级传入的 props 动态修改 state。该生命周期就是为此而生;

    • 大家可能会有疑问: 该方法为什么为静态方法? 而不是常规的实例方法呢?

      • 先肯定一点: 使用实例方法肯定也是能满足需求的。但这个钩子比较特殊,它的执行时机是位于 新状态合并之后,重渲染之前,而且该方法会 侵入更新机制 中。如果在之中做例如修改状态之类的操作是十分不可控的。当设计为静态方法后,函数内部便无法访问组件实例,成为一个 纯函数,便能保证更新流程的安全与稳定。
  • render()

    • 根据 stateprops,生成 虚拟DOM
  • componentDidMount()

    • 组件被创建成真实元素并渲染后 被调用,此时可获取真实的元素状态,主要用于业务逻辑的执行,例如数据请求,事件绑定等;

更新阶段:

  • static getDerivedStateFromProps(nextProps, prevState)

  • shouldComponentUpdate(nextProps, nextState)

    • 上篇文章中的 diff 优化策略中有提到: 为了减少 无谓的更新消耗,赋予组件一个可以 主动中断更新流API。根据参数中的 更新属性更新状态,业务方自行判断是否需要继续往下执行 diff,从而能有效地提升 更新性能

    • 大家记得 React 中有种组件叫 纯组件(PureComponent) 吧,其实这个类继承于普通的 Component 上封装的,可以减少多余的 render,提升性能。

      • 默认使用 shouldComponentUpdate 函数设定更新条件: 仅当 propsstate 发生改变时,才会触发更新。这里使用了 Object 浅层比对,也就是仅做第一层比对,即 1. key 是否完全匹配;2. value 是否全等; 所以如果需要超过一层的数据变动,纯组件即无法正确更新了;

      • 这也是为什么 React 提倡使用 不变数据 的原理,能有效地使用浅层比对;

      • 不变数据: 提倡数据不可变,任何修改都需要返回一个新的对象,不能直接修改原对象,这样能有效提高比对效率,减少无谓性能损耗。

  • render()

  • getSnapshotBeforeUpdate(prevProps, prevState)

    • 替换旧版的 componentWillUpdate,触发时机点: 在数据状态已更新,最新 VNode 已生成,但 真实元素还未被更新

    • 可以用来在 更新之前 从真实元素或状态中获取计算一些信息,便于在更新后使用;

  • componentDidUpdate(prevProps, prevState, snapshot)

    • 组件更新完成后调用;

    • 可以用于 监听数据变化,使用 setState 时必须加条件,避免无限循环;

卸载阶段:

  • componentWillUnmount()
    • 组件即将被销毁。通常可以用于 解绑事件清除数据释放内存 等功能;

我们也按这样的目标来在我们的 Component 上实现这些生命周期,那如何更好的组织生命周期呢?这里我考虑到的是:

组件作为元素的容器,生命周期的本质其实是 其渲染出的元素节点的生命周期

也就是说,关键点还是在于 元素在视图中的工作流,何时被挂载 - 更新 - 卸载。所以为了更好的维护性和拓展性,更理想的方式应该是 为元素节点统一添加生命周期,而不是单独为组件,这样可大大降低复杂度,增加可拓展性。

节点生命周期

那我们第一步先根据上面需要的生命周期来理一下需要哪些时机:

  • 创建后(create)
  • 挂载后(insert)
  • 更新前(willupdate)
  • 更新后(update)
  • 删除前(willremove)

原理就很简单了,只需要在 VNode 工作流中的对应时期调用相应的生命周期函数即可。那我们现在 VNode 上新增一个属性 hooks,用于 储存 对应的生命周期函数:

interface VNode {
	...
	
	hooks: {
        create?: (vnode) => void
        insert?: (vnode) => void
        willupdate?: (oldVNode, newVNode) => void
        update?: (oldVNode, newVNode) => void
        willremove?: (vnode) => void
    }
}
复制代码

新增一个触发的方法(fireVNodeHook):

function fireVNodeHook(vnode, name, ...data) {
    // 根据 生命周期名称
    // 执行储存在 VNode 上的对应函数即可
    const { hooks: _hooks } = vnode
    if (_hooks) {
        hook = _hooks[name]
        hook && hook(...data)
    }
}
复制代码

有了这层基础方法后,我们只需要分别在之前所写的 渲染与更新 流程中的各个函数适时地触发就行了。

1. create

该时机是在 元素被创建后,但还未被挂载之前。由于我们之前将逻辑统一收归为 createElm,因此只需要在该函数末尾统一加入触发即可。

function createElm(vnode) {
	// 创建元素逻辑
	...
	
	// 触发 虚拟DOM 上储存的 钩子函数
	fireVNodeHook(vnode, 'create', vnode)
	
	return vnode.elm
}
复制代码

2. insert

元素被挂载到视图 上的时机。从元素的角度来看,就是被 append 到父级中的时机点。这个时机点比较分散,但也比较好加入,找到我们使用 Api 中的 append 方法加入,总共有三个地方:

  • render 函数中加入对 根节点 的触发;
  • createElm 函数中加入对 所有子级 的触发;
  • diffChildren 列表比对中 新增列表项 的触发;

3. willupdateupdate

更新之前更新之后,对应的便是我们的 diff 函数。由于最终均需走到 diffVNode 中,因此只需要在 diffVNode 开头和末尾触发即可。

4. willremove

元素被卸载时,其实与 insert 类似,只需要关注 ApiremoveChild 的调用时机即可。在 diff 列表比对期间,当新列表中不存在时,我们需要删除旧列表中的元素,也就是之前写的业务函数 removeVNodes

组件生命周期

由于 元素节点 才是贯穿整棵 虚拟DOM 渲染与更新的关键,因此我们上面先实现的是对 元素节点 的生命周期触发。但是我们最终需要是 组件节点 的生命周期。由于 组件节点元素节点 为一一对应的 上下层级关系,因此这里我们还需要做一层转接:

把组件节点的生命周期赋值给其生成的元素节点

首先我们先来为组件定义上生命周期,并定义一个中转对象 __hooks,实现 组件节点周期与元素节点周期的转换:

class Component {
    public __hooks = {
        // 元素节点 插入时机,触发 didMount
        insert: () => this.componentDidMount(),

        // 元素节点 更新之前,触发 getSnapshotBeforeUpdate
        willupdate: (vnode) => {
            this.__snapshot = this.getSnapshotBeforeUpdate(this.__prevProps, this.__prevState)
        },

        // 元素节点 更新之后, 触发 didUpdate
        update: (oldVNode, vnode) => {
            this.componentDidUpdate(this.__prevProps, this.__prevState, this.__snapshot)
            this.__snapshot = undefined
        },

        // 元素节点 卸载之前, 触发 willUnmount
        willremove: (vnode) => this.componentWillUnmount(),
    }

    // 默认生命周期函数	
    // getDerivedStateFromProps(nextProps, state)
    public getSnapshotBeforeUpdate(prevProps, prevState) { return undefined }
    public shouldComponentUpdate(nextProps, nextState) { return true }
    public componentDidMount() { }
    public componentDidUpdate(prevProps, prevState, snapshot) { }
    public componentWillUnmount() { }
}
复制代码

然后我们只需要在 __createVNode 方法中将 this.__hooks 赋值给生成出的 VNode 即可:

class Component {
	...
	
	public __createVNode() {
	    // ...
	    this.__vnode = this.render()
	    
	    // 赋值给对应的 元素节点,
	    // 实现该 元素节点 与 组件 之间生命周期的绑定
	    this.__vnode.hooks = this.__hooks
	    
	    return this.__vnode
	}
}
复制代码

最后,大家可能发现我们还有两个钩子没有实现: getDerivedStateFromPropsshouldComponentUpdate,这是因为这两个生命周期会影响到更新结果,因此需要 深入到更新流程中,无法单纯的通过 元素节点 的生命周期来实现。

但其实也很简单,就是在 更新之前,需要根据这两个函数的返回结果,适当调整下更新逻辑即可:

// Componet 中的 __update 方法
class Component {
	// ...

	public __update = () => {
        // 临时存储 旧虚拟节点 (oldVNode)
        const oldVNode = this.__vnode
        
        this.__nextProps = this.props
        if (!this.__nextState) this.__nextState = this.state
        
        // 执行 getDerivedStateFromProps
        // 更新 state 状态
        const cls = this.constructor
        if (cls.getDerivedStateFromProps) {
            const state = cls.getDerivedStateFromProps(this.__nextProps, this.state)
            if (state) {
                this.__nextState = Object.assign(this.__nextState, state)
            }
        }
        
        // 在 diff 之前调用 shouldComponentUpdate 进行判断
        // true: 生成新 VNode,继续 diff
        // false: 清空状态
        if (this.shouldComponentUpdate(this.props, this.__nextState)) {
            // 重新生成 新虚拟节点(newVNode)
            this.__vnode = this.__createVNode()

            // 调用 diff,更新 VNode
            diff(oldVNode, this.__vnode)
        } else {
            // 清空状态更新
            this.__nextProps = this.__nextState = undefined
        }
        
        // 刚才 异步更新队列 中标识的组件 待更新状态
        // 在更新完后置为 false
        this.__dirty = false
	}
}
复制代码

组件更新还有另外一个地方,即 diffComponent,也需要加入上述类似的执行和判断。完成这部分代码后,我们来简单测试个 DEMO:

    1. <App><BBB txt={this.state.txt} /> 正确渲染
    1. 双组件 渲染生命周期 与预期一致;
    1. 触发更新,调用 <App> setState<BBB> 文字元素正确更新;
    1. 双组件 更新生命周期 与预期一致;

图1. 生命周期演示DEMO

最后一站: 旅途之末

在这系列文章中,我们实现了 React 最核心的部分: JSX、组件、渲染、更新。我们基于 动手实践 的方式,循序渐进地探讨了一些原理与策略,得出一些最佳实践。相信走完这遍旅程后,大家能对 React 有了更深层次的了解,能够给到各位小伙伴启发与帮助。其实我也一样,也是在这个旅程中跟大家一起共同学习,共同成长。

React 实践揭秘之旅,中高级前端必备(上) ->

还有许多模块,如 ContextRefsFragment 和 一些全局API,如 cloneElement 等,还有代码中一些更严谨的判断及边界情况的处理,并没有在文章中体现,主要是由于这些部分更多的是纯逻辑的扩展,同时也是为了便于理解。如果童鞋们有兴趣,可以到 github 中查看完整版代码:

react-webgl.js ->

另外我也想稍微唠嗑下关于 React-WebGL 这个想法。

近阶段我接触了一些 Web 游戏的开发,有了一些从前端开发者出发的思考与理解。在游戏开发领域,传统的游戏开发者有着一套与前端领域完全不同的思维编程模式。随着 Web 的发展,使他们需要拓展到 Js 的环境中。所以出现了一系列的游戏引擎库,本质上是从其它平台的库移植过来的。当我从一个前端开发者的角度在进行开发时,其实并不是说入门难,学习成本高,而是给我的感觉是: 类似用纯原生 js 在写页面,觉得效率低下。所以这也是 React-WebGL 的出发点,期望能将现在 Web 中更优秀的理念运用到游戏开发,甚至找到一种更高效的开发模式,提升效率,完善生态。

当然,这仅仅只是一个起点, 游戏开发 与 界面开发 确实有着许多异同点,如何找到一种更现代化更高效的 Web 游戏开发模式,这还需要很长的一段旅程。我也一直在思考,一直在摸索,相信能有一些好玩的东西。没有尝试,没有努力,就千万别在起点就放弃了。有什么问题,有什么想法,直接找我一起探讨哈。🙃~~

Tips:

看博主写得这么辛苦下,跪求点赞、关注、Star!更多文章猛戳 ->

邮箱: 159042708@qq.com 微信/QQ: 159042708

祝福#感恩#武汉加油##RIP KOBE#