Vue快速转React指南(二)

404 阅读7分钟

前置说明

已发布:

本篇要点:给UI增加交互性

Vue快速转React指南(二):

  1. state和props

开始

state、props、event等

1.Event事件

让UI动起来的第一步就是绑定事件,事件的使用上几乎和原生JS没什么区别。这里不多说。

export default function Button() {
  function handleClick(e) {
    console.log(e);
    alert('You clicked me!');
  }

  return (
    <button onClick={handleClick}>
      Click me
    </button>
  );
}

2.props

react里的props和Vue里的props都是一个意思,表示传入的参数。

比如上面的例子,需要在点击的时候弹出我们传入的message

export default function Button({ message }) {
  function handleClick(e) {
    console.log(e);
    alert(message);
  }

  return (
    <button onClick={handleClick}>
      Click me
    </button>
  );
}

Button函数入参就是props参数,我们使用ES6的解构语法,将它解构为{message}

这和Vue3的setup函数一致,setup的第一个入参也是props

<script> 
import { defineComponent } from "vue"; 
export default defineComponent({ 
  props: ['msg'], 
  setup(props) { 
    // setup 
  } 
}) 
</script>

3.state⚠️

react里state的意义和Vue2里的data、Vue3里的ref或者reactive包裹产生的变量等价。

要想实现数据<->视图相互影响,在Vue里的规则就是数据必须是响应式数据,我们需要将它包裹在data里。

那么,在react里同样,要想数据可以影响视图的变化,就需要useState这个函数来申明变量,可以把这个简单对比理解为Vue3里的reactive

下面举例说明一下:

export default function Gallery() {
  let index = 0; // 不使用setState申明

  function handleClick() {
    index = index + 1;
  }

  return (
    <>
      <button onClick={handleClick}>
        +1
      </button>
       {index}
    </>
  );
}

此时界面会渲染出来:

image.png

但是点击+1的按钮界面并不会再次渲染,因为此时的index属于一个函数内的临时变量。然后,使用useState来修改一下:

import {useState} from 'react'

export default function Gallery() {
  let [index, setIndex] = useState(0);

  function handleClick() {
    setIndex(index +1);
  }

  return (
    <>
      <button onClick={handleClick}>
        +1
      </button>
       {index}
    </>
  );
}

useState函数的定义:function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>]

传入的参数是一个变量的初始值,返回的是使用数据构出两个变量,一个是index,另一个是setIndex函数用来更新index值。这里setIndex是在react里一个建议的规范,对变量进行更新的函数取名为setXxx这样的名字。

这里变量index的改变也是有规则的,直接使用index = index + 1是没办法触视图更新的,需要使用框架内部导出的函数setInedx来间接改变。 可以把导出的setXxx函数理解成这样:

// 传入对Xxx进行修改的表达式
function setXxx(expression) {
  Xxx = expression;
}

和Vue3里用ref包裹的变量一样,不能使用index=index+1更新而是要使用index.value做改变一样,底层有一些自己的实现的规则,用户在使用的时候要遵守这个规范。

state和响应式变量的区别

这一部分主要讲state和响应式变量的区别。Vue里就用ref来申明响应式变量。

对于上面的例子,在Vue里可以这么写: (这里采用jsx来写,因为这样和react更接近)

import { defineComponent, ref } from "vue";

export default defineComponent({
  setup(props) {
    let index = ref(0);
    function handleClick() {
      index.value = index.value + 1;
    }

    return () => (
      <div>
        <button onClick={handleClick}>
          click
        </button>
        {index.value}
      </div>
    );
  }
});

区别一:state是一次快照;响应式数据劫持数据的变化,不是一次的快照。

将上面的例子修改:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  function handleClick() {
    setNumber(number + 1)
  }

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        handleClick();
        handleClick();
        handleClick();
      }}>+3</button>
    </>
  )
}

此时点击+3,大家觉得number会显示3吗?错❎,number是1。一次快照的意思是在组件进行一次更新时就算一次快照,此时的number变量会在当前这一次快照保持不变,哪怕使用了异步:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        handleClick();
        handleClick();
        handleClick();
        
        setTimeout(() => {
          alert(number); // 0
        }, 3000);
      }}>+5</button>
    </>
  )

哪怕此时是异步,number返回的是执行那次的快照,所以number在当前这一次快照里始终是0。就像你在餐馆点餐一样,react是等你点完了菜之后才会把菜单传递给厨房,不会你点一个菜就传递给厨房一次。

在Vue里:

import { defineComponent, ref } from "vue";

export default defineComponent({
  setup(props) {
    let index = ref(0);
    function handleClick() {
      index.value = index.value + 1;
    }

    return () => (
      <div>
        <button onClick={() => {
          handleClick();
          handleClick();
          handleClick();
          
          setTimeout(() => {
            alert(index.value); // 3
          }, 3000);
        }}>
          click
        </button>
        {index.value}
      </div>
    );
  }
});

此时点击+3按钮,index显示的是3! 并且在setTimeout里返回的也是3。这就是Vue和react底层实现细节不同产生的不同表现。Vue的响应式系统可以劫持到数据的任何改变,然后将这些改变整合在一起里进行修改。

从react的角度,为什么state会表现成这样,正是因为没有响应式系统无法劫持到数据的改变,只能通过存储当前变量在上一次快照的值来完成下一次快照的取值。这就导致在每一次快照取到的都是上一次储存的state,自然执行三次setNumber(number + 1)都是在执行setNumber(0 + 1),那么最终的结果就是1并不是3。

区别二:state为Object类型的数据,改变这类数据的方式state和Vue完全相反

  • 在react里,对比state有没有改变使用的是Object.is方法。

对于对象和数组这类数据,比较的是内存地址,所以只是单纯的修改对象里的属性是不会触发更新的,因为内存地址并没有变,Object.is返回是true改变对象的内存地址才可以使得Object.is判断为false进而进行重新更新渲染,比如对对象进行重新赋值操作。

  • 在Vue里是通过Proxy跟踪某个对象的方式来实现更新,所以不能改变这个对象的内存地址

因为你改变了之后Vue就无法继续跟踪这个对象了,那么你后续的修改就无法触发Proxy的代理。因此,Vue3里对reactive对象的修改只能通过修改属性的方式,不能进行赋值操作。

举例:

实现一个跟随鼠标移动的红点

import { useState } from 'react';
export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        position.x = e.clientX; 
        position.y = e.clientY;
        setPosition(position);
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  );
}

上面代码里:

onPointerMove={e => {
  position.x = e.clientX; 
  position.y = e.clientY;
  console.log(postion);
  setPosition(position);
}}

虽然打印出position已经改变,但是上面这种方式打印是无法触发更新的。因为上面的方式只是改变了position属性的值,它的内存地址并没有改变,所以在react里使用Object.is对比的话返回的是true表示数据并没有改变,自然也不会更新。

所以需要创建一个新的对象数据去更新,那在JS里就需要实现对对象的深拷贝了。深拷贝之后再去修改需要修改的数据。

onPointerMove={e => {
  const p = { ...postion };
  p.x = e.clientX;
  p.y = e.clientY;
  setPosition(p);
}}

于是乎,在未来的每一个对象要修改属性都需要通过赋值的方式

比如有一个嵌套的对象

const [person, setPerson] = useState({  
  name: 'Niki de Saint Phalle',  
  artwork: {  
    title: 'Blue Nana',  
    city: 'Hamburg',  
    image: 'https://i.imgur.com/Sd1AgUOm.jpg',  
  }  
});

如果你只是想要更新一下artwork里的city,你需要把整个对象深拷贝然后修改city之后再赋值:

setPerson({
  ...person, // 先拷贝第一层
  artwork: {
    ...person.artwork, // 再拷贝第二层
    city: 'New Delhi' // 拷贝的同时修改我们需要修改的数据
})

而在Vue里只需要修改city的属性就好了。可以看出对象类型的state在react里修改是一个非常极其麻烦的操作。其实在这里也可以看出,react的编写需要一定的JS基础,假如你对深拷贝并不熟悉的话很容易写错代码导致最终更新异常。

为此,react还专门写了一个包叫immutable.js去帮助用户简化这个过程。mutable是可共享的意思,就像对象的地址一样,那immutable就是不可共享的。使用这个去将对象类数据变为不可共享的,来代替用户自己做深拷贝的行为。

使用这个包之后,这个包内部的fromJS方法会将对象变为Map类型,将数组变为List类型,然后toJS方法会将数据恢复为JS类型的数据。具体用法大家可以查教程单独学习。

这个包很需要了,但是要手动引入,react并没有把它装入官方脚手架。

最后

回顾本篇文章要点: props、state与Vue的区别。

预告: 下一篇主要讲React Hooks vs. Vue Composition API