Taro入门学习笔记

3,363 阅读12分钟

由于去年公司的小程序有跨端需求,于是陆陆续续做了部分基于Taro开发的小程序任务,后来觉得这段摸索的经历可以稍微整理成笔记,顺便分享给新人做参考。

笔记主要针对没有React/Taro经验的新人,如果内容有不准确的地方,还望指出,会及时更正。

环境搭建

这里直接丢官网文档。

Taro 2.x官方文档

由于公司小程序基于Taro2开发,这里只介绍Taro2相关功能。

前置知识

  1. 原生小程序开发经验;
  2. React基础;
  3. TypeScript基础(如果项目基于TS开发)。

前置知识是需要提前掌握的,否则可能会在开发中一脸懵逼,事倍功半。

Taro对比原生小程序

一些常见的使用场景

生命周期对比

原生小程序

Page({
  data: {
    text: "This is page data."
  },
  onLoad: function(options) {
    // 页面创建时执行
  },
  onShow: function() {
    // 页面出现在前台时执行
  },
  onReady: function() {
    // 页面首次渲染完毕时执行
  },
  onHide: function() {
    // 页面从前台变为后台时执行
  },
  onUnload: function() {
    // 页面销毁时执行
  },
  onPullDownRefresh: function() {
    // 触发下拉刷新时执行
  },
  onReachBottom: function() {
    // 页面触底时执行
  },
  onShareAppMessage: function () {
    // 页面被用户分享时执行
  },
  onPageScroll: function() {
    // 页面滚动时执行
  },
  onResize: function() {
    // 页面尺寸变化时执行
  }
})

Taro class组件

export default class Example extends Component {
  constructor(props) {
    super(props);

    this.state = {
      text: "This is page data.",
    };
  }

  // 对应onLoad方法,官方建议用constructor或componentDidMount(订阅、副作用时)替换,
  // 该钩子在React中已废弃
  componentWillMount() {}
  // 对应onShow方法
  componentDidShow() {}
  // 对应onReady方法
  componentDidMount() {}
  // 对应onHide方法
  componentDidHide() {}
  // 对应onUnload方法
  componentWillUnmount() {}
  // 组件更新前执行(接收到新的props或state时),首次渲染不触发,
  // 不能在这里setState,该钩子在React中已废弃
  componentWillUpdate(prevProps, prevState) {}
  // 接收到新的props时执行,推荐使用componentDidUpdate替换,该钩子在React中已废弃
  componentWillReceiveProps(nextProps) {}
  // 组件更新后执行
  componentDidUpdate(prevProps, prevState) {
    // 经常会在这里通过判断新旧数据的变化来做一些操作,比如
    // if (prevProps.sessionId !== this.props.sessionId) {
    // 已经登录了
    // }
  }
  // 子组件是否需要重现渲染
  shouldComponentUpdate(nextProps, nextState) {}
  // onPullDownRefresh、onReachBottom、onShareAppMessage...与原生小程序一致
}

在React中已废弃的生命周期componentWillMount、componentWillUpdate、componentWillReceiveProps不建议使用。

Taro function组件

function Example() {
  /**
   * useEffect => React Hooks 提供的钩子
   * 类似componentDidMount、componentDidUpdate、componentWillUnmount的组合
   */

  // 默认使用(不传参),mounted和unmounted以及update时执行
  useEffect(() => {
    // 不能这里setState,否则会造成死循环
  });
  
  // 传[],mounted和unmounted时执行
  useEffect(() => {
    // ...
  }, []);
  
  // 传state,只有当该state发生变化时,才会执行
  const [count, setCount] = useState(0);
  useEffect(() => {
    // 不能这里setState => count,否则会造成死循环
  }, [count]);
  

  // 返回函数时,unmounted阶段会自动执行,可用于销毁过时的对象
  useEffect(() => {
    const id = setInterval(() => {
      setCount(count => count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  /**
   * 其他 useDidShow、useDidHide、useReachBottom、useResize...
   * 参考:https://taro-docs.jd.com/taro/docs/2.x/hooks
   */

  return (<View></View>);
}

简单的示例

如果不熟悉React Hooks,建议从class组件开始学习、使用。

class组件

class组件的组成部分

  1. 定义defaultProps
  2. 定义state
  3. 组件生命周期
  4. render函数

父组件

import Taro, { Component } from "@tarojs/taro";
import { View } from "@tarojs/components";
// 公共组件
// import Empty from './empty';
// import Error from './error';
// 子组件
import Item from "./item";
export default class ExampleList extends Component {
  // constructor...super为固定写法,可忽略
  /*constructor(props) {
    super(props);
  }*/

  // 定义state
  state = {
    list: [],
    nothing: false,
    error: false,
  };

  // ref方式访问组件(如果需要)
  itemRefComp = Taro.createRef();

  // 相当于原生小程序的page.json
  config = {
    navigationBarTitleText: "示例列表",
    enablePullDownRefresh: true,
  };

  // 在生命周期中请求后台数据
  componentDidMount() {
    this.fetchData();
  }

  // 自定义方法
  fetchData() {
    const list = [
      {
        id: 1,
        value: "测试数据",
      },
      {
        id: 2,
        value: "测试数据",
      },
    ];

    this.setState({
      list,
    });
  }

  // 自定义方法,通过props传递给子组件
  handleJumpToDetail = () => {
    // 通过ref方式调用子组件方法
    this.itemRefComp.current.testRef();

    console.log("detail");
  };

  // render函数,返回JSX
  render() {
    const { list, nothing, error } = this.state;

    return (
      <View className="container">
        {!!list && !!list.length && (
          <View className="list">
            {list.map((item) => {
              return (
                <Item
                  ref={this.itemRefComp}
                  item={item}
                  key={item.id}
                  onItemClick={this.handleJumpToDetail}
                />
              );
            })}
          </View>
        )}
        {/* {nothing && <Empty message='暂无数据' />}
        {error && <Error />} */}
      </View>
    );
  }
}

子组件

import Taro, { Component } from "@tarojs/taro";
import { Button } from "@tarojs/components";
export default class Item extends Component {
  // 定义props
  static defaultProps = {
    item: {},
    onItemClick: null,
  };

  // 自定义方法
  handleClickItem = (item, e) => {
    const { onItemClick } = this.props;
    // 触发父组件通过props传递的方法
    onItemClick && onItemClick(item);
  };

  // 自定义方法(父组件中可通过ref方式调用)
  testRef() {
    console.log("testRef click", this.props.item);
  }

  // render函数,返回JSX
  render() {
    const { item } = this.props;

    return (
      <Button className="item" onClick={this.handleClickItem.bind(this, item)}>
        {item.value}
      </Button>
    );
  }
}

需要注意setState并非像原生小程序setData一样是同步的,如果后面有取值操作,需在回调函数中或利用async/await获取,例如:

logList() {
  const { list } = this.state;

  console.log(list);
}
// 错误用法
fetchData() {
  const list = [1, 2];

  this.setState({
    list,
  });
  // logList中无法正确获取list
  this.logList();
}
// 正确用法一,回调函数
fetchData() {
  const list = [1, 2];

  this.setState(
    {
      list,
    },
    () => {
      this.logList();
    }
  );
}
// 正确用法二,async/await
async fetchData() {
  const list = [1, 2];

  await this.setState({
    list,
  });

  this.logList();
}

function组件

function组件组成部分

  1. 定义defaultProps
  2. 定义state
  3. useEffect(类似生命周期)
  4. 返回JSX

父组件

import { useState, useEffect, useRef } from "@tarojs/taro";
import { View } from "@tarojs/components";
// 公共组件
// import Empty from './empty';
// import Error from './error';
// 子组件
import Item from "./item";
export default function ExampleList(props) {
  // 定义state
  const [list, setList] = useState([]);
  const [nothing, setNothing] = useState(false);
  const [error, setError] = useState(false);

  // ref方式访问组件(如果需要)
  const itemRefComp = useRef();

  // 在useEffect中请求后台数据
  useEffect(() => {
    fetchData();
  }, []);

  // 自定义方法
  const fetchData = () => {
    const list = [
      {
        id: 1,
        value: "测试数据",
      },
      {
        id: 2,
        value: "测试数据",
      },
    ];

    setList(list);
  };

  // 自定义方法,通过props传递给子组件
  const handleJumpToDetail = () => {
    // 通过ref方式调用子组件方法
    itemRefComp.current.testRef();

    console.log("detail");
  };

  // 返回JSX
  return (
    <View className="container">
      {!!list && !!list.length && (
        <View className="list">
          {list.map((item) => {
            return (
              <Item
                childRef={itemRefComp}
                key={item.id}
                item={item}
                onItemClick={handleJumpToDetail}
              />
            );
          })}
        </View>
      )}

      {/* {nothing && <Empty message="暂无数据" />}
      {error && <Error />} */}
    </View>
  );
}

// 相当于原生小程序的page.json
ExampleList.config = {
  navigationBarTitleText: "示例列表",
  enablePullDownRefresh: true,
};

子组件

import { useImperativeHandle } from "@tarojs/taro";
import { Button } from "@tarojs/components";

const Item = (props) => {
  const { item, onItemClick, childRef } = props;
  // 对外暴露ref方式调用的方法
  useImperativeHandle(childRef, () => {
    return {
      testRef,
    };
  });

  // 自定义方法
  const handleClickItem = (item) => {
    return () => {
      // 触发父组件通过props传递的方法
      onItemClick && onItemClick(item);
    };
  };

  // 自定义方法(父组件中可通过ref方式调用)
  function testRef() {
    console.log("testRef click", item);
  }

  // 返回JSX
  return (
    <Button className="item" onClick={handleClickItem(item)}>
      {item.value}
    </Button>
  );
};
// 定义defaultProps
Item.defaultProps = {
  item: {},
  onItemClick: null,
};
export default Item;

以上主要为了体现基本的用法,暂不会对各个细节做详细的介绍。

性能优化相关

为了方便调试,下面的部分示例代码基于React而非Taro,实际上它们的用法基本是一样的。

优化重新渲染问题

React不会像其他一些框架一样,在state变动时做一些依赖、是否相等的判断,以避免不必要的渲染。实际表现为每次setState后,无论JSX中是否引用了该state,甚至不管state的值是否改变,都会引起当前组件、子组件的重新渲染,这无疑是个不必要的开销。

虽然React通过virtual dom(H5端)做diff比较,能最小化改动,而非全部抛弃重新渲染,但减少render函数的执行、virtual dom生成和比较,仍然是有利于提升性能的,特别是在dom比较复杂、子组件较多的情况下。

一些基本的解决办法

  1. 将JSX中不依赖的、依赖但不会二次变动的数据移出state;
  2. 向子组件传递的函数不使用bind及不使用匿名函数包裹(会导致PureComponent优化失效)。

例如:

export default class Example extends Component {
  // 定义state
  state = {
    list: [],
    error: false,
    nothing: false,
  };

  // render函数中不依赖,或者不会二次变动的数据
  pageNo = 1;
  hasMore = true;
  noChange = 1;

  // 使用普通函数时,render中需要用bind才能保障this正确,不推荐使用
  // fun() {}

  // 使用箭头函数时,render中不需要bind(this)
  fun = () => {
    console.log(this);
  };

  render() {
    const { list, nothing, error } = this.state;
    const { noChange } = this;

    return (
      <View className="container">
        {list}
        {nothing}
        {error}
        {noChange}
        {/* 常见使用bind和匿名函数,会导致每次渲染都生成新的函数,触发子组件的重新渲染 */}
        {/* <Child onFun={this.fun.bind(this)}></Child>*/}
        {/* <Child onFun={() => this.fun()}></Child> */}
        <Child onFun={this.fun}></Child>
      </View>
    );
  }
}

再者使用shouldComponentUpdate、PureComponent、memo等官方API做一些新旧数据的判断,来决定是否重新渲染,以及用useMemo、useCallback来缓存函数。

shouldComponentUpdate

shouldComponentUpdate能让我们自主决定是否渲染子组件,但在数据量过大、涉及引用类型(对象、数组等)时,需要遍历、递归判断(深比较),可能会在性能上得不偿失。

shouldComponentUpdate(nextProps, nextState) {
   // state数据不相同时才允许渲染
   return nextState.someData !== this.state.someData;
}

PureComponent

PureComponent会自动帮我们将props和state做浅对比,来决定是否渲染视图,但当数据的内存指向没有发生变化时,会导致无法触发渲染,这时可以使用forceUpdate强制更新(不推荐),更推荐每次都返回一个新的对象。

import React, { PureComponent, Component } from "react";
class Child extends PureComponent {
  updateChild() {
    this.forceUpdate();
  }

  render() {
    console.log("Child Component render");
    const { name } = this.props.userInfo;
    return (
      <div>
        这里是child子组件:
        <p>{name}</p>
      </div>
    );
  }
}
class Parent extends PureComponent {
  state = {
    userInfo: { name: "张三", age: 18 },
  };

  childRef = React.createRef();

  changeName = () => {
    const { userInfo } = this.state;
    // 可以触发父子组件更新(对于引用类型数据,需要每次返回一个新的对象)
    /*this.setState({
      userInfo: {
        ...userInfo,
        name: "李四",
      },
    });*/

    // 数据内存地址没有变化,无法触发父子组件更新
    userInfo.name = "李四";
    this.setState({
      userInfo,
    });

    // 强制子组件更新,不推荐使用
    this.childRef.current.updateChild();
  };

  render() {
    console.log("Parent Component render");
    const { userInfo } = this.state;

    return (
      <div>
        <p>{userInfo.name}</p>
        <button onClick={this.changeName}>改变父组件state</button>
        <br />
        <Child ref={this.childRef} userInfo={userInfo}></Child>
      </div>
    );
  }
}
export default Parent;

memo

memo为高阶组件,比较像是shouldComponentUpdate及PureComponent的结合体,是提供给function组件使用的。

memo默认会帮我们做props浅比较,如果props相等则不做更新,但缺陷跟PureComponent一样,在判断引用类型数据时可能不靠谱,这种情况除了每次都返回一个新的对象,还可以通过第二个参数实现自定义新旧数据判断。

import React, { useState, memo } from "react";
// 未优化的function,父组件的更新都会引起子组件的更新
const Child = (props = {}) => {
  console.log(`--- re-render ---`);
  return (
    <div>
      <p>number is : {props.number}</p>
    </div>
  );
};
// 使用memo优化的function,类似PureComponent,父组件的step及count改变不会引起该组件的更新
/*const ChildMemo = memo((props = {}) => {
  console.log(`--- memo re-render ---`);

  return (
    <div>
      <p>number is : {props.number}</p>
    </div>
  );
});*/
/**
 * 可以传第二个参数,类似shouldComponentUpdate,不同的是返回false时,才会触发更新
 */
const isEqual = (prevProps, nextProps) => {
  if (prevProps.number !== nextProps.number) {
    return false;
  }
  return true;
};
const ChildMemo = memo((props = {}) => {
  console.log(`--- memo re-render ---`);

  return (
    <div>
      <p>number is : {props.number}</p>
    </div>
  );
}, isEqual);
export default (props = {}) => {
  const [step, setStep] = useState(0);
  const [count, setCount] = useState(0);
  const [number, setNumber] = useState(0);

  const handleSetStep = () => {
    setStep(step + 1);
  };

  const handleSetCount = () => {
    setCount(count + 1);
  };

  const handleCalNumber = () => {
    setNumber(count + step);
  };

  return (
    <div>
      <button onClick={handleSetStep}>step is : {step} </button>
      <button onClick={handleSetCount}>count is : {count} </button>
      <button onClick={handleCalNumber}>number is : {number} </button>
      <hr />
      <Child number={number} /> <hr />
      <ChildMemo number={number} />
    </div>
  );
};

useMemo

useMemo非常类似Vue的computed,具有缓存作用,并且根据第二个参数的传入,会有不同的执行结果,表现为:

  1. 不传参数,每次子组件更新都会执行(相当于没有优化);
  2. 传[],只会首次执行一次;
  3. 传[state/props],当依赖的参数改变时,才会重新执行。

由于function组件每次渲染都会重新创建,除了用useMemo实现computed,还可以用来缓存复杂的函数,以减少重新创建的消耗。

import React, { useState, useMemo } from "react";
export default (props = {}) => {
  console.log("---function---render---");
  const [step, setStep] = useState(5);
  const [count, setCount] = useState(0);

  // 类似vue的computed,具有缓存作用,只有当依赖(step)改变时才会重新执行
  const { sum } = useMemo(() => {
    console.log("---useMemo---render---");
    let sum = 10;

    sum += step;

    return {
      sum,
    };
  }, [step]);

  // 当成缓存一个普通函数来使用
  // const { sum } = useMemo(() => {
  //   console.log("---useMemo---render---");
  //   let sum = 0;
  //   // 假设是一个很复杂的计算过程
  //   for (let i = 0; i < 10000; i++) {
  //     sum += 5;
  //   }

  //   return {
  //     sum,
  //   };
  // }, []);

  const handleSetCount = () => {
    setCount(count + 1);
  };

  const handleSetStep = () => {
    setStep(step + 1);
  };

  return (
    <div>
      <button onClick={handleSetCount}>count is : {count} </button>
      <p onClick={handleSetStep}>
        step is: {step} sum is: {sum}
      </p>
    </div>
  );
};

useCallback

回到上面的memo的介绍,使用memo可以实现props数据不改变就不触发子组件的重新渲染。

然而这仍然不是银弹,我们知道function组件每次渲染都会重新创建,当通过props向子组件传递函数时,因为函数是重新创建的,所以该函数的内存地址每次都会变更,于是memo的浅比较就会无法准确识别,也就达不到优化的效果。

useCallback就是为了解决这一问题而诞生的,它能帮我们缓存函数,不至于每次重新渲染都会重新创建,具体用法如下:

import React, { useState, memo, useCallback } from "react";
const ChildMemo = memo((props = {}) => {
  console.log("--- memo re-render ---");
  return (
    <div>
      <p>number is : {props.number}</p>
    </div>
  );
});
const ChildMemo2 = memo((props = {}) => {
  console.log("--- memo re-render2 ---");

  return (
    <div>
      <p>number is : {props.number}</p>
    </div>
  );
});
export default (props = {}) => {
  const [step, setStep] = useState(0);
  const [count, setCount] = useState(0);
  const [number, setNumber] = useState(0);

  const handleSetStep = () => {
    setStep(step + 1);
  };

  const handleSetCount = () => {
    setCount(count + 1);
  };

  const handleCalNumber = () => {
    setNumber(count + step);
  };

  // 普通函数
  const onClickA = () => {};

  // useCallback
  // 不传参,没有缓存效果
  // const onClickB = useCallback(() => {});

  // 传空数组,不会再次更新
  const onClickB = useCallback(() => {}, []);

  // 传[state/props],依赖改变时才会更新
  // const onClickB = useCallback(() => {
  //   console.log(step);
  // }, [step]);

  return (
    <div>
      <button onClick={handleSetStep}>step is : {step} </button>
      <button onClick={handleSetCount}>count is : {count} </button>
      <button onClick={handleCalNumber}>number is : {number} </button>
      <hr />
      <ChildMemo number={number} onClick={onClickA} /> <hr />
      <ChildMemo2 number={number} clickFun={onClickB} />
    </div>
  );
};

上面列举了一些优化性能的办法,还有更多没能一一写出来,末尾分享一句话:

“我们应该忽略很小的性能优化,可以说97%的情况下,过早的优化是万恶之源,而我们应该关心对性能影响最关键的那另外3%的代码。”——高德纳

从这里应该可以得出一些结论,例如:

  1. 我们更应该关注有性能瓶颈的地方;
  2. 如果没有良好的规则、提升性能的确信,过早的优化可能会造成代码过于复杂且难以维护。

常见问题

为什么有时候在function组件中拿到的state或props是旧的(capture value)?

观察下面的例子,当我们点击button之后,button里的文本会显示“count is: 5”,似乎没什么问题,但handleClick里面的log却告诉我们count是0,这是为什么呢?

import React, { useState } from "react";

export default (props = {}) => {
  console.log('render')
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(5);
    setTimeout(() => {
      console.log(`count已经设置为5了,那么现在是=>${count}`);// 0
    }, 3e3);
  };

  return <button onClick={handleClick}>count is: {count}</button>;
};

这个问题仍然归属于上面提到的function组件每次渲染都会重新创建,可以用下面这个例子做一个简单的对比。

const funComponent = (isAlert) => {
  const { count } = state;
  console.log(`render----count is ${count}`);

  if (isAlert) {
    setTimeout(() => {
      console.log(`count is ${count}`);// 2 4
    }, 3e3);
  }
};
const state = new Proxy(
  {
    count: 0,
  },
  {
    set(target, property, value) {
      target[property] = value;
      funComponent(!(value % 2));
      return value;
    },
  }
);

state.count = 1;
state.count = 2;
state.count = 3;
state.count = 4;
state.count = 5;

由于function组件每次都会重新创建并执行,而state又是通过解构函数取值的,跟真正的state并没有引用关系。

click事件触发后,setTimeout处于上一次函数的运行环境中,而非当前的运行环境,也就无法从上一次的state中获取到最新值。

上面这个例子只需要把${count}改为${state.count}就可以了,因为是指向了真正的count,也就能保证获取到最新的值。

React中提供了类似的解决方法-useRef,用法如下:

import React, { useState, useRef } from "react";

export default (props = {}) => {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);

  const handleClick = () => {
    setCount(latestCount.current = 5);
    setTimeout(() => {
      console.log(`count已经设置为5了,那么现在是=>${latestCount.current}`); // 5
    }, 3e3);
  };

  return <button onClick={handleClick}>count is: {count}</button>;
};

使用循环的index变量作为key是一种反优化?

运行Taro小程序时,可能会遇到如题这种提示,大部分的原因应该是我们想要“轻松省事”,用现成的index当key就完事了,小部分的原因是不知道用index当key的弊端。

那么再次提上案头,为什么使用循环的index变量作为key是一种反优化呢?

简单来说,当存在唯一key的情况下,对数组进行增删改等操作时,diff算法能正确地识别节点,找到正确的位置更新数组。而index作为key使用时,由于唯一标识key改变了,会引发节点批量重新渲染,甚至节点更新不正确。

如果想要了解更多,可去搜索Vue/React相关dom diff算法。

一般而言,后端会给我们提供数据的唯一ID,例如:

<ul>
    {list.map((item) => {
      return <li key={item.id}>{item.name}</li>;
    })}
</ul>

如果没有,我们也可以按照一定的规则生成唯一key,或者使用uuid等一些插件生成。

虽然实际开发中,大部分的dom for循环仅为了数据展示,不需要绑定key。但尽可能地为for循环加上唯一的key属性,是代码严谨、消除隐藏bug以及避免编译器报错的良好习惯。