在项目中落地 SolidJS(二)

上一篇文章中,带大家入门了 SolidJS,今天继续讲解进阶知识,包括:

  • 生命周期
  • 事件绑定
  • 样式绑定
  • Ref 引用
  • Context 上下文
  • 嵌套响应式

生命周期

在 SolidJS 中,生命周期非常简单,只有 onMountonCleanup 两个,其中 onMount 相当于 React 中没有任何依赖项的 useEffect

jsx 复制代码
// React
useEffect(() => {
  fetch('xxx') // 发起数据请求
}, [])

// SolidJS 
onMount(() => {
  fetch('xxx') // 发起数据请求
})

onMount 会在组件初始渲染完成后执行,且只会运行一次,而 onCleanup 就相当于 React 中没有任何依赖项的 useEffect 回调中返回的清理函数:

jsx 复制代码
// React
useEffect(() => {
  const timer = setInterval(() => console.log('timer'), 1000)
  return () => {
    // 这里是清理代码
    clearInterval(timer)
  }
},[])

// SolidJS
const timer = setInterval(() => setCount(count() + 1), 1000);
onCleanup(() => clearInterval(timer))

可以看到,onCleanup 是一个一级方法,可以在任何范围内调用,它会在该范围被触发重新求值和组件销毁的时候执行。由于响应式系统接管了大部分工作,且是同步创建和更新的,所以开发者无需太过关心组件的生命周期。

事件绑定

语法和 React 是完全一样的:

jsx 复制代码
// React 事件绑定
<Button onClick={handleClick}>按钮</Button>

// SolidJS 事件绑定
<Button onClick={handleClick}>按钮</Button>

但是除此之外,还支持数组语法:

jsx 复制代码
<Button onClick={[handleClick, 'some params']}>按钮</Button>

因为这样可以避免创建额外的闭包,例如下面这种:

jsx 复制代码
<Button onClick={() => handleClick('some params')}>按钮</Button>

另外,如果是用户的自定义事件,可以通过 on: 命名空间语法来监听,例如:

jsx 复制代码
// 创建一个新的 CustomEvent 对象
const event = new CustomEvent('WierdEventName', {
  // 可选:可以在这里携带一些信息
  detail: { age: 12 }
});

// 使用 on: 前缀来监听这个自定义的事件。
const btn = <button on:WierdEventName={(evt) => console.log('WierdEvent', evt)} >Click Me</button>

// 在 btn 上触发这个事件
btn.dispatchEvent(event);

当然,对于内置的事件,on:XXXonXXX 效果是一样的,例如:

jsx 复制代码
<Button onClick={() => /* Do something */}>按钮</Button>
<Button on:click={() => /* Do something */}>按钮</Button>

样式绑定

与 React 不同的是,style 对象中写样式,CSS 属性不需要用驼峰:

jsx 复制代码
// React
<div style={{ fontWeight: 800, fontSize: 16 }}>
  Some Text
</div>

// SolidJS
<div style={{ "font-weight": 800, "font-size": `16px` }}>
  Some Text
</div>

CSS 变量也可以直接写:

jsx 复制代码
<div style={{ "--my-custom-color": themeColor() }} />

而且同时支持用 classclassName 来设置元素类名:

jsx 复制代码
<div class="red">红色</div>
<div className="black">黑色</div>

更贴心的是,还内置了 classList 属性,类似于 React 中的 classnames 包的效果,能够接受一个对象,key 是类名,value 是布尔表达式,当 true 时应用该样式,当 false 时移除该样式:

jsx 复制代码
<button
  classList={{ selected: current() === "foo" }}
  onClick={() => setCurrent("foo")}
>
  foo
</button>

Ref 引用

在 SolidJS 中,想拿到元素的引用非常简单,直接这么写就好了:

jsx 复制代码
const myDiv = <div>My Element</div>

就是变量赋值啊,没错!当然,它也支持 React 的写法:

jsx 复制代码
let myDiv

<div ref={myDiv}>My Element</div>

那么 myDiv 也会是 <div> 的引用,只不过初始值是 undefined,赋值行为发生在元素创建后,被添加到 DOM 前进行。SolidJS 同样也支持回调的写法:

jsx 复制代码
<div ref={el => /* 处理 el... */}>My Element</div>

回调写法的好处就是元素已创建就能拿到 el 了,不需要等到被添加到 DOM 之前。

Context 上下文

跟 React 一样,SolidJS 也提供了 Context API 来解决 props 透传的问题:

jsx 复制代码
import { createSignal, createContext, useContext } from "solid-js"
const CounterContext = createContext()

export function CounterProvider(props) {
  const [count, setCount] = createSignal(props.count || 0),
    store = [
      count,
      {
        increment() {
          setCount(c => c + 1)
        },
        decrement() {
          setCount(c => c - 1)
        }
      }
    ]

  return (
    <CounterContext.Provider value={store}>
      {props.children}
    </CounterContext.Provider>
  )
}

那么 CounterProvider 的所有后代组件,都可以通过 useContext(CounterContext) 拿到 count 变量和 incrementdecrement 方法,无论层级有多深。

jsx 复制代码
const [count, { increment, decrement }] = useCounter()

但实际上,由于 SolidJS 是响应式的,开发者在大部分场景下直接使用全局的 Store/Signal 即可,没必要用 Context,例如:

jsx 复制代码
import { createSignal, createMemo } from "solid-js"

function createCounter() {
  const [count, setCount] = createSignal(0)
  const increment = () => setCount(count() + 1)
  const doubleCount = createMemo(() => count() * 2)
  return { count, doubleCount, increment }
}

export default createCounter()

这样导出一个响应式对象,可以直接在另外一个组件中使用:

jsx 复制代码
function Counter() {
  const { count, doubleCount, increment } = counter
  return (
    <button type="button" onClick={increment}>
      {count()} {doubleCount()}
    </button>
  )
}

嵌套响应式

jsx 复制代码
import { For } from 'solid-js'

const Todos = () => {
  let input
  return (
    <>
      <div>
        <input ref={input} />
        <button onClick={add}>Add Todo</button>
      </div>
      <For each={todos()}>
        {(todo) => {
          const { id, text } = todo
          console.log(`Creating ${text}`)
          return (
            <div>
              <input type="checkbox" checked={todo.completed} onchange={[toggleTodo, id]} />
              <span
                style={{
                  'text-decoration': todo.completed ? 'line-through' : 'none',
                }}
              >
                {text}
              </span>
            </div>
          )
        }}
      </For>
    </>
  )
}

export default Todos

然后创建一个 Signal :

jsx 复制代码
import { createSignal } from 'solid-js'

function TodoSignal() {
  let todoId = 0
  const [todos, setTodos] = createSignal([])
  const addTodo = (text) => setTodos([...todos(), { id: ++todoId, text, completed: false }])
  const toggleTodo = (id) => setTodos(todos().map((todo) => (todo.id !== id ? todo : { ...todo, completed: !todo.completed })))
  return { todos, addTodo, toggleTodo }
}

export default TodoSignal()

接下来对 Todos 组件补充逻辑:

jsx 复制代码
import todoSignal from './signal'

const Todos = () => {
  let input
  const { todos, addTodo, toggleTodo } = todoSignal

  const add = () => {
    if (!input.value.trim()) return
    addTodo(input.value)
    input.value = ''
  }
  return (
    /* 省略代码 */
  )
}

这样就能实现响应式了:

但这是以 todo 为粒度的更新,意思是如果 todo 发生变化,下面的 div 元素整体发生更新:

jsx 复制代码
<div>
  <input type="checkbox" checked={todo.completed} onchange={[toggleTodo, id]} />
  <span
    style={{
      'text-decoration': todo.completed ? 'line-through' : 'none',
    }}
  >
    {text}
  </span>
</div>

SolidJS 通过 createStore 可以做到值更新 div 里面的 span,而 input 则不需要动:

jsx 复制代码
import { createStore } from 'solid-js/store'

function TodoStore() {
  let todoId = 0
  const [store, setStore] = createStore({ todos: [] })
  const addTodo = (text) => setStore('todos', (todos) => [...todos, { id: ++todoId, text, completed: false }])
  const toggleTodo = (id) =>
    setStore(
      'todos',
      (t) => t.id === id,
      'completed',
      (completed) => !completed
    )
  return { store, addTodo, toggleTodo }
}

export default TodoStore()

修改后的 Todos 组件:

jsx 复制代码
import todoStore from './store'

const Todos = () => {
  let input
  const { store, addTodo, toggleTodo } = todoStore
  const todos = () => store.todos

  const add = () => {
    if (!input.value.trim()) return
    addTodo(input.value)
    input.value = ''
  }
  return (
    /* 省略代码 */
  )
}

这简直不可思议!再次体现了 SolidJS 的强大之处------通过精确定位做出精细的响应。

总结

掌握了这一节的内容之后,结合上一讲,完全可以用 SolidJS 进行开发了。历史包袱非常少,对于一些简单的页面,追求极致性能的,SolidJS 绝对是不二之选。略有遗憾的是组件库比较少,不过可以选择那些支持多框架的组件库,例如 Ark UI,同时支持 React、Solid 和 Vue:

或者与框架无关的组件库,例如 daisyui,用 SolidJS 结合 tailwind css 也能写出漂亮的组件:

相关推荐
qbbmnnnnnn3 分钟前
【CSS Tricks】如何做一个粒子效果的logo
前端·css
唐家小妹5 分钟前
【flex-grow】计算 flex弹性盒子的子元素的宽度大小
前端·javascript·css·html
涔溪6 分钟前
uni-app环境搭建
前端·uni-app
安冬的码畜日常10 分钟前
【CSS in Depth 2 精译_032】5.4 Grid 网格布局的显示网格与隐式网格(上)
前端·css·css3·html5·网格布局·grid布局·css网格布局
洛千陨11 分钟前
element-plus弹窗内分页表格保留勾选项
前端·javascript·vue.js
小小199212 分钟前
elementui 单元格添加样式的两种方法
前端·javascript·elementui
前端没钱32 分钟前
若依Nodejs后台、实现90%以上接口,附体验地址、源码、拓展特色功能
前端·javascript·vue.js·node.js
爱喝水的小鼠38 分钟前
AJAX(一)HTTP协议(请求响应报文),AJAX发送请求,请求问题处理
前端·http·ajax
叫我:松哥1 小时前
基于机器学习的癌症数据分析与预测系统实现,有三种算法,bootstrap前端+flask
前端·python·随机森林·机器学习·数据分析·flask·bootstrap
让开,我要吃人了1 小时前
HarmonyOS鸿蒙开发实战(5.0)网格元素拖动交换案例实践
前端·华为·程序员·移动开发·harmonyos·鸿蒙·鸿蒙开发