组件

组件是构建页面元素的重要部分,它允许您创建可重用的元素和模块,与 web component 类似,可以做的事情有很多,这部分值得你的探索。

函数

函数是一个 javascript 的重要功能,函数可以是一组用来控制任务执行和中断的逻辑块。

将函数用作组件是一个非常贴近开发者思维的作法,它不仅简单,还便于代码复用。

下面介绍一个简单的例子。

import { FC } from 'gyron'

const HelloWorld = FC(() => {
  return <div>Hello World</div>
})

HelloWorld 就是我们的函数组件,用来渲染一个 div 元素,显示的内容就是 Hello world。

如果你想将这个组件用在不同的地方,那么你只需要导入这个组件然后像写元素一样使用即可。

import { FC } from 'gyron'

const HelloWorld = FC(() => {
  return <div>Hello World</div>
})
const App = FC(() => {
  return (
    <div class="container">
<HelloWorld />
</div> ) })

Setup 和 Render

如果你是按照脚手架的方式或者使用了我们的 babel plugin 就不需要再阅读这块文档,如果你是第三方开发者可能需要阅读这块内容。

函数组件分为两个区域,一个是 setup scope,另外一个是 render scope。 setup scope 区域在组件渲染的流程中只会被执行一次,只有在销毁组件后再次渲染才会被执行。 顾名思义,render scope 就是负责组件更新,只要组件依赖的数据发生变更或者使用forceUpdate强制更新时执行。

如果你够仔细,那么你会发现 App 组件和 HelloWorld 组件不太一样,App 组件返回了一个函数用来渲染页面,为什么可以参考前面一篇教程,里面解释的很详细。

为什么组件要返回一个函数

有状态组件setup函数中使用解构的时候需要特殊注意,props 是不会得到更新,如果要访问 props 需要使用“props.xxxxx”。

import { useValue, FC } from 'gyron'

const HelloWorld = FC(() => {
const count = useValue(0)
function update() {
count.value++
}
return <div onClick={update}>counter {count.value}</div> })

上述高亮行部分就是 setup 区域。其中的块只会被执行一次,然后其中的状态会用闭包缓存下来,再下一次更新时只会更新 render 区域。

import { useValue, FC } from 'gyron'

const HelloWorld = FC(() => {
  const count = useValue(0)
  function update() {
    count.value++
  }
return <div onClick={update}>counter {count.value}</div>
})

上述高亮行部分就是 render 区域。也就是我们所熟知的 html 标签,当组件发生更新时这个区域会一直更新。

自动转换

现在我们也提供一种选项,但是这个是基于@gyron/babel-plugin-jsx插件才能生效。 你只需要在插件选项中传递setuptrue,那么插件会自动识别这两种不同的模式,然后将所有无状态组件转换为有状态组件,完全就不需要关注这个组件的返回值是函数还是JSX.Element了。

import esbuild from 'esbuild'
import { babelESBuildJsx } from '@gyron/babel-plugin-jsx'

esbuild.build({
  // ...
  plugins: [
    babelESBuildJsx({
setup: true,
}), ], })
import { FC } from 'gyron'

const App = FC<{ msg: string }>((props) => {
  const count = useValue(0)
  function onClick() {
    count.value++
  }
  return (
    <div onClick={onClick}>
      {props.msg} - {count.value}
    </div>
  )
})

// ↓ ↓ ↓ ↓ ↓
const App = FC<{ msg: string }>((props) => {
  // ...
return (props) => (
<div onClick={onClick}> {props.msg} - {count.value} </div> ) })

在配置完插件后使用 FC 定义一个组件,然后插件就会自动识别转换成箭头下方的所示代码。

如果你需要在 render 区域定义一些方法或者变量之类的对象,那么组件可以返回一个函数。这个时候插件将什么都不会处理。

参数(Props)

有的时候我们想让组件变得更通用,可能需要传递数据到组件之后有不同的反馈,这就是 Props 的作用。

使用的方式也和函数一样,但是注意,有几个内部的参数名已经被占用:

  • children (用来向组件传递子组件/元素)
  • isSSR (当前环境是否处于 SSR 中,可以用这个参数编写更通用的代码)

一个常用例子。

const HelloWorld = ({ message }) => {
  return <div>I Am {message}</div>
}
const App1 = <HelloWorld message="Legend" />
const App2 = <HelloWorld message="Legend 2" />

我们向 HelloWorld 组件传递了一个 message 参数,在第一个场景下会展示I Am Legend,在第二个场景下会展示I Am Legend 2

数据在组件中往往都是单向流动的,但是有些场景需要在子组件中改变父组件的数据,我们推荐传递一个 update 函数给子组件,然后子组件调用 update 函数更新数据。

import { useValue, FC } from 'gyron'

const HelloWorld = ({ message, update }) => {
  return <div onClick={update}>I Am {message}</div>
}
const App = FC(() => {
  const name = useValue('Legend')
  return (
    <div class="container">
      <HelloWorld
        message={name.value}
        update={() => (name.value = 'Legend 2')}
      />
    </div>
  )
})

useWatchProps

通常情况下,在编写一个组件时,我们需要在 setup 区域监听一个 props 数据然后处理业务逻辑,那我们就需要在组件中使用onBeforeUpdate生命周期勾子,来判断 props 是否发生了变更,我们简单实现一个根据 token 变更然后换取用户信息。

import { FC, onAfterUpdate } from 'gyron'

interface InformationProps {
  token: string
}

const Information = FC<InformationProps>(({ token }) => {
  const avatar_url = useValue('')
  onAfterUpdate((perv: InformationProps, cur: InformationProps) => {
    if (perv.token !== cur.token) {
      if (cur.token) {
        fetch(`/info?token=${cur.token}`).then(({ url }) => {
          avatar_url.value = url
        })
      }
    }
  })
  return <img src={avatar_url} />
})

上述代码的逻辑就是当 token 发生变化的时候请求/info接口获取用户头像然后渲染。这种在业务开发中其实是一个高频操作,于是我们提供了useWatchProps方法,用此方法创建一个 watch 方法,然后传递需要监听的 props。接下来我们看看代码应该怎么改造

import { FC, useWatchProps } from 'gyron'

interface InformationProps {
  token: string
}

const Information = FC<InformationProps>(({ token }) => {
  const avatar_url = useValue('')
  const watch = useWatchProps<InformationProps>()
  watch('token', (token) => {
    fetch(`/info?token=${token}`).then(({ url }) => {
      avatar_url.value = url
    })
  })
  return <img src={avatar_url} />
})

useWatchProps 的返回值就是一个函数,并且会根据类型自动识别出第一个参数'token'和第二个参数的类型,你还可以这样

interface InformationProps {
  token: string
  age: number
}

const Information = FC<InformationProps>(({ token, age }) => {
  const watch = useWatchProps<InformationProps>()
  watch(['token', 'age'], ([token, age]) => {
    // ...
  })
})

TypesScript

Gyron.js 是完全支持 TypesScript 类型系统的,但是要接入 JSX 语法,还需要用包裹函数 FC、FCA、FCD 等,它完全不改变组件的行为,只是提供了一个类型提示,在编写组件和使用组件的时候可以提示用户。

import { FC } from 'gyron'

interface HelloWorldProps {
  message: string
  update: () => void
}

const HelloWorld = FC<HelloWorldProps>(({ message, update }) => {
  return <div onClick={update}>I Am {message}</div>
})

Ref

有的时候我们需要自定义渲染,或者想获取到真实的元素信息。我们可以使用createRef方法创建一个对象,它会暴露一个current属性,然后在对应的组件或元素上使用 ref 进行绑定。

import { createRef, onAfterMount, FC } from 'gyron'

const App = FC(() => {
const ref = createRef()
onAfterMount(() => { console.log(ref.current) // HTMLDivElement })
return <div ref={createRef}></div>
})

生命周期钩子

已经阅读过这篇文章,只是想确定生命周期执行的顺序,可以去查看实例这篇教程后面的生命周期图。

在制作一款复杂的应用时,往往需要更多的逻辑完善应用。比如,我们在请求后端数据时或者当组件销毁时需要一些自定义处理,就可以使用生命周期钩子帮助你完成这些功能。

import { onAfterMount } from 'gyron'

const HelloWorld = ({ message }) => {
onAfterMount(({ $el }) => {
// 在组件渲染完成之后将文字颜色改变成蓝色 $el.style.color = 'blue' }) return <div>I Am {message}</div> }

或者在组件销毁时删除组件内的定时器。

import { onDestroyed, FC } from 'gyron'

const HelloWorld = FC(({ message }) => {
  const timer = setInterval(() => {
    console.log(Date.now())
  }, 1000)
  onDestroyed(() => {
    // 移除定时器
clearInterval(timer)
}) return <div>I Am {message}</div> })

当组件销毁时,就会移除timer定时器。

再或者我们用 Gyron.js 完成一些高频 dom 操作,比如,改变元素的位置,但不需要及时更新 dom。通常在以前的编程模式中会直接改变 dom 元素的位置,但这会造成代码的臃肿和不可维护,如果使用 Gyron,那么这些问题可以迎刃而解。

import { useReactive, onBeforeUpdate, FC } from 'gyron'

const HelloWorld = FC(({ message }) => {
  const state = useReactive({
    allowUpdate: false,
    x: 0,
    y: 0,
  })
  onBeforeUpdate(() => {
return state.allowUpdate
}) return ( <div style={{ position: 'absolute', left: position.x + 'px', top: position.y + 'px', }} > I Am {message} </div> ) })

我们可以通过改变state.allowUpdate的值来告诉 Gyron 框架,此次数据变更是否需要自动更新。这样,即使state.x或者state.y发生变更也是可控的。

有状态组件 和 无状态组件

在阅读完 Setup 和 Render 一栏后就应该有一个大概的认知,在 Gyron.js 中为什么会有无状态组件和有状态组件。

有状态组件是指一个组件拥有内部数据,在下次更新时这些数据不被刷新。当一个组件的返回值是一个函数时,并且函数的返回值是一个节点时,那么我们就称之为有状态组件。

import { useValue, FC } from 'gyron'

const HelloWorld = FC(() => {
const count = useValue(0)
return <div>{count}</div> })

有些情况下只想封装一些 UI 组件,他们的所有状态全部来自父组件,也就是说自己内部不需要管理这些状态,只需要管理好 UI 表现和响应用户事件。无状态组件具有更好的迁移特性。

当组件直接返回一个节点时,那这个组件就是无状态组件。无状态组件无法拥有内部状态,在每一次更新都会重新执行一次函数,如果需要保留组件内部的状态,请使用有状态组件。

import { useValue, FC } from 'gyron'

const HelloWorld = FC(({ count }) => {
return <div>{count}</div>
})

缓存组件

缓存组件可以自动保持组件的状态,当组件不可见时,组件的状态会一直保留,当下次激活组件时,状态自动还原。

import { keepComponent, useValue, FC } from 'gyron'

const App = keepComponent(
  FC(() => {
    const count = useValue(0)

    function increment() {
      count.value++
    }

    return <div onClick={increment}>Cache Component {count.value}</div>
  })
)

当缓存组件 Props 发生变更时,缓存组件会自动更新,当组件依赖的数据发生变更时组件也会自动更新。 你可以使用 keepComponent 方法对组件进行优化,请勿过度使用 keepComponent 组件,因为这在内部使用了一个 Map 保存这些组件,如果缓存组件确定不再使用,你可以使用clearCacheComponent方法清空本地缓存并且断开组件的依赖关系。

import { clearCacheComponent, keepComponent, FC } from 'gyron'

const App = keepComponent(
  FC(() => {
    // ...
  })
)

clearCacheComponent(App)

异步组件

异步组件可以用在页面的懒加载上面,以减少首页需要加载的资源。

异步组件还可以做一些其它的事情,高级教程中会详细介绍。异步组件

FCD

FCD是用来定义延迟更新组件。顾名思义,用 FCD 定义的组件,更新不会阻塞 UI 的渲染和用户的行为。

使用方法和 FC 一样,用 FCD 定义一个组件。

const SlowItem = FCD<{ text: string }>(({ text }) => {
const startTime = performance.now() while (performance.now() - startTime < 1) { // ... } return <li>Text: {text}</li> })

当我们组件更新很耗时的情况下,可以使用 FCD 来定义组件。下面用两个实际例子来看看 FC 和 FCD 的区别。

index
list
item
index
TSX
LESS
资源加载中...

上面那个例子是使用 FCD 定义了一个组件,然后每修改一次,SlowItem的更新任务都会进入一个延迟更新队列,这个队列里面的任务更新超过 5ms 那么下次更新会进入一个空闲队列。

index
list
item
index
TSX
LESS
资源加载中...

对比可以发现,第一个例子中用户的输入时刻都会被浏览器响应,但是在第二个例子中用户每修改一次都有可能导致 UI 卡顿。