学习《SolidJS》(二)响应式

273 阅读2分钟

本文章中所有的编译结果都只是假想的,只是逻辑上的,不代表真实的编译结果

组件定义

组件就是一个函数,只是返回的是JSX.Element(如下面的代码),但是在SolidJS里组件编译后就不一定了,不过目前不用管。

function SimpleComponent(){
    return (
        <div>
            这个一个简单组件
        </div>
    )
}

Signal

Signal 是最核心的响应式基本要素。使用createSignal可以创建一个响应式的数据。createSignal接收的是一个初始值,返回的就是一个元组(typescript里面的概念),实际上也确实是一个数组而已。都是函数一个是Getter一个是Setter。

import { createSignal } from "solid-js";

function IntervalCount(){
    const [count, setCount] = createSignal(0);
    setInterval(() => setCount(count() + 1), 1000);
    
    return (
        <div>
            当前数值:{count()}
        </div>
    )
}

为什么是Getter函数?为什么不是值?因为这样才可以随时取到最新的值,而不会过期。Setter就是修改响应式数据的值,被修改的时候,就会触发ui的更新。

对于return中的代码会被编译成js代码,那对于上面这个return中的代码我们可以理解为

var div = document.createElement('div');
div.innerText = `当前数值:${count()}`;

那只要现在只要想一个办法,当count被修改的时候重新执行一下div.innerText = `当前数值:${count()}`;这行代码不就行了吗?

Effect

其实我们可以监听count的变化,那就是使用createEffect这个函数。

下面代码是加上createEffect之后,对于createEffect的参数,我称为Effect函数

import { createSignal, createEffect } from "solid-js";

function IntervalCount(){
    const [count, setCount] = createSignal(0);
    setInterval(() => setCount(count() + 1), 1000);
    createEffect(() => { console.log("这是控制,count的值是", count()); });
    
    return (
        <div>
            当前数值:{count()}
        </div>
    )
}

这样就可以在控制台里面看到每秒都在打印count的当前值。

那现在有了这个createEffect函数,那其实又有了另外一个想法,可以把前面return中的代码改成

import { createSignal, createEffect } from "solid-js";

function IntervalCount(){
    const [count, setCount] = createSignal(0);
    setInterval(() => setCount(count() + 1), 1000);
    createEffect(() => { console.log("这是控制,count的值是", count()); });
    
    return () => {
        var div = document.createElement('div');
        createEffect(() => div.innerText = `当前数值:${count()}`);
        return div;
    }
}

这样的话是不是当count被改变的时候就只是修改div的内容,同时也不需要重新创建div,做到了真正的局部更新。

注意:这个代码只是为了演示它的逻辑,与实际的编译天差地别,请不要当真!!!!

小结:其实createEffect就是将传入的Effect函数执行一次,然后在执行过程中将count这个响应式数据与Effect函数进行绑定,当count改变的时候就可以找到对应的Effect函数然后执行,这样就是实现了count修改的时候重新执行的效果

createSignal可以写在任何地方

比如将createSignal写在组件外,如下代码,同样可以运行,没有约束

import { createSignal, createEffect } from "solid-js";

const [count, setCount] = createSignal(0);
setInterval(() => setCount(count() + 1), 1000);
createEffect(() => { console.log("这是控制,count的值是", count()); });

function IntervalCount(){
    return (
        <div>
            当前数值:{count()}
        </div>
    )
}

那写在组件里和组件外有没有区别?有什么意义?

区别其实就是于js语法一样,只是作用域不同。写在组件里面的一般认为是组件的状态,而写在外面的一般认为是全局的共享数据,比如下面这样,两个组件共用

import { createSignal, createEffect } from "solid-js";
const [username, setUsername] = createSignal("zhangsan");

function Header(){
    return (
        <div>username()</div>
    )
}

function UserInfo(){
    return (
        <div>
            <div>昵称:</div>
            <input type="text"  value={username()} />
        </div>
    )
}

衍生Signal

在开发过程中,有时候不是只使用源数据,而是要经过处理之后再使用。比如将原数值乘以2之后再显示,我们可以这样写

import { createSignal } from "solid-js";

function IntervalCount() {
    const [count, setCount] = createSignal(0);
    setInterval(() => setCount(count() + 1), 1000);
    const doubleCount = () => count() * 2;
    
    return (
        <div>
            当前数值的两倍:{doubleCount()}
        </div>
    )
}

会发现是定义为函数,为什么要定义为函数呢?

因为如果只是一个值的话,那count改变的时候界面是不会改变的。

为什么定义为函数界面就会改变?

因为

可以假设上面的代码编译之后

import { createSignal } from "solid-js";

function IntervalCount() {
    const [count, setCount] = createSignal(0);
    setInterval(() => setCount(count() + 1), 1000);
    const doubleCount = () => count() * 2;
    
    return (
        var div = document.createElement('div');
        createEffect(() => div.innerText = `当前数值的两倍:${doubleCount()}`);
        return div;
    )
}

那不就是createEffect函数执行的时候,

就执行了doubleCount()

然后执行了count()

这样不就相当于() => div.innerText = `当前数值的两倍:${doubleCount()}` 这个Effect函数订阅了count这个响应式数据吗?

所以理解为什么叫做衍生Signal了吧?

不过SolidJS的实现,并不是这么简单,只是可以这样理解而已

Memo

如果经常调用这个衍生Singal,那就是要重复的计算。比如下面这样(来自SolidJS官方教程)

import { createSignal } from 'solid-js';

function fibonacci(num) {
  if (num <= 1) return 1;

  return fibonacci(num - 1) + fibonacci(num - 2);
}

function Counter() {
  const [count, setCount] = createSignal(10);
  const fib = () => { 
      console.log("Calculating Fibonacci"); 
      return fibonacci(count()); 
  };

  return (
    <>
      <button onClick={() => setCount(count() + 1)}>Count: {count()}</button>
      <div>1. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>2. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>3. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>4. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>5. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>6. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>7. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>8. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>9. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>10. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
    </>
  );
}

点击按钮,会发现一次渲染,控制台打印了很多次,也就是这个fib其实触发了多个Effect函数,每个Effect函数都重新执行了一遍。

这个时候需要用到createMemo了,修改后如下

import { createSignal, createMemo } from 'solid-js';

function fibonacci(num) {
  if (num <= 1) return 1;

  return fibonacci(num - 1) + fibonacci(num - 2);
}

function Counter() {
  const [count, setCount] = createSignal(10);
  const fib = createMemo(() => { 
      console.log("Calculating Fibonacci"); 
      return fibonacci(count()); 
  });

  return (
    <>
      <button onClick={() => setCount(count() + 1)}>Count: {count()}</button>
      <div>1. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>2. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>3. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>4. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>5. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>6. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>7. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>8. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>9. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>10. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
    </>
  );
}

改成这样之后会发现,每次渲染,控制台都只会打印一次,也就是createMemo,缓存了fib的值,只有依赖项"count"改变的时候才会重新执行。

但是我用得非常少,因为这样的场景非常的少,如果切换界面或者其他操作,重新执行一次一般都不会有很大的性能问题。如果是网络请求,这一种其实我不会使用createMemo进行缓存的,而是另外启用一个Signal和createEffect,比如下面这样写,当然这代码只是说明,实际使用的时候还可以写的更好,比如封装到useXXX的函数中

import { createSignal, createEffect } from 'solid-js';

const [userId, setUserId] = createSignal(1);

const [userDetail, setUserDetail] = createSignal(null);

createEffect(() => {
    fetch(`http://xxx.com/userDetail/${userId()}`) 
    .then(response => response.json()) 
    .then(data => setUserDetail(data));
})

//后续导出或直接使用userDetail,又或者添加其它衍生的Signal

也就是我不会详细的说createMemo了,就到这里了。

今天就先写到这里,拜拜!