数据流和生命周期

如何处理 React 中的数据,组件之间如何通信,数据在 React 中如何流动?

常用的 React 生命周期方法以及开源项目 spug 中使用了哪些生命周期方法?

数据和数据流

虽然也有很多静态网站,但人们使用的大多数网站都充满了随时间变化的数据。

stateprops 是 React 组件处理数据和彼此通信的两种主要方法。

React 提供了两个数据相关的 api:state 和 props。

前面我们已经知道,不要直接修改 state,而是使用 setState(updater[, callback])

updater 函数签名:(preState, props) => stateChange

Tip: 建议传递函数而非对象。React 16 以前的版本允许传递对象而不是函数给 setState 的第一个参数。传递对象给 state 暗示是同步的,实际却并非如此,而使用函数,更符合 React 的异步范式,React 会更新,但不保证时间。

笔者测试,下面两种写法都生效:

this.setState({
    date: new Date()
})

this.setState(() => ({
    date: new Date()
}))

假设我们想根据 props.step 来增加 state,可以这样:

this.setState((state, props) => {
  return {counter: state.counter   props.step};
});

setState 是浅合并,而非替换。请看实验:合并还是替换

如果需要在应用更新后触发某些动作,可以使用 setState 的回调函数(setState(updater, callback)

不可变状态 props

在 React 中,属性是传递不可变数据的主要方式,所有组件都可以接收属性,并能在构造函数、render()和生命周期方法中使用。

属性通常来自父组件或自身的默认属性值(defaultProps)。

是否可以将父组件的 state 作为属性传给子组件呢?是的。一个组件的状态可以是另一个组件的属性。

可以将任何有效的 js 数据作为属性传递给子组件

Tip: 前面 我们已经测试过了,如果修改 props(this.props.name = 'aName'),控制台会报错。

属性可以随时间改变,但不是从组件内部改变。这是单向数据流的一部分,下面会提到。

只使用 props 的组件

倘若我们希望将状态保存在一个中心位置,而不是分散在各个组件中,比如 Flux、Redux等。这时我们可以创建无状态函数组件(或称之为函数组件)。

无状态不是没有任何种类的状态,而是它不会获得 React 进行管理的支撑实例,意味着没有生命周期方法,没有 state。

无状态函数组件与有支撑实例的父组件结合使用时非常强大。与其在多个组件间设置状态,不如创建单个有状态的父组件并让其余部分使用轻量级子组件。例如 Redux 会将这个模式提升到全新的水平。

Tip:请练习,使用一个组件的状态修改另一个组件的属性。(下文父子组件有使用)

组件通信

现在我们能够用子组件轻易的构建新组件,能够很容易地表示组件间的 has-a、is-a 的关系。如何让他们通信呢?

在 React 中,如果想让组件彼此通信,需要传递属性,开发人员会做两件事:

  • 访问父组件中的数据(状态或属性)
  • 传递数据给子组件

下面定义 1 个父组件,2 个子组件,其中 2 个子组件中的数据来自父组件。请看示例:

const MyComponent = (props) => (
    <div>
        <MyComponentSub1 content={props.cnt1}/>
        <MyComponentSub2 content={props.cnt2}/>
    </div>
)

const MyComponentSub1 = (props) => (
    <p>sub1: {props.content}</p>
)

const MyComponentSub2 = (props) => (
    <p>sub2: {props.content}</p>
)
ReactDOM.render(
    <MyComponent cnt1="apple" cnt2="orange" />,
    document.getElementById('root')
);

页面显示:

sub1: apple

sub2: orange

单向数据流

数据如何流经应用的不同部分?

在 React 中,UI 是数据投射到视图中的数据,当数据变化时,视图随之变化。

React 中,数据流是单向的。上层通过 props 传递数据给子组件,子组件通过回调函数将数据传递给上层。单向数据流让思考数据在应用中的流动变得更简单

也得益于组件的层次结构以及将属性和状态局限于组件,预测数据在应用中如何移动也更加简单。

数据在 React 中是按一个方向流动的。属性由父组件传递给子组件(从所有者到拥有者),子组件不能编辑父组件的状态或属性。每个拥有支撑实例的组件都能修改自身的 state 但无法修改超出自身的东西,除了设置子组件的属性。

如果允许从应用的部分随意修改想要修改的东西,听上去好像不错,实际上可能会导致难以捉摸的应用并且可能会造成调试困难。

React 中的数据流是单向的,从父组件流向子组件,子组件通过回调函数将数据回送给父组件,但它不能直接修改父组件的状态,而父组件也不能直接修改子组件的状态,组件间通信通过属性完成。

渲染和生命周期

渲染就是 React 创建和管理用户界面所做的工作,也就是让应用展现到屏幕上。

和 vue 中生命周期类似,react 也可以分 4 部分:

  • 初始 - 组件被实例化的时候
  • 挂载 - 组件插入到 dom
  • 更新 - 通过状态或属性用新数据更新组件
  • 卸载 - 组件从 dom 中移除

生命周期方法简介

笔者在前文已经分析过一次 react 生命周期相关方法,这里再次总结一下:

挂载时的顺序:constructor()、render()、componentDidMount()(组件挂载后立即调用)

Tip:

  • 挂载是 React 将组件插入到 dom 的过程。React 在实际 dom 中创建组件之前,组件只存在虚拟 dom 中。
  • 容易犯的一个错误是把不该放到 render() 中的东西放到了 render() 里面。render() 通常会在组件的生命周期内调用多次,也无法确定 React 会何时调用 render(),因为出于性能 React 会批量更新。
  • 挂载(ReactDOM.render)和卸载(ReactDOM.unmountComponentAtNode)由 React.DOM 从外部控制。没有 ReactDOM 的帮助,组件不能卸载自己。

更新时的顺序:shouldComponentUpdate、render()、componentDidUpdate(组件更新后被立即调用)

卸载时:componentWillUnmount

过时的生命周期有:componentWillMount、componentWillReceiveProps、componentWillUpdate。官网不建议在新代码中使用它们。

避免使用:forceUpdate、shouldComponentUpdate。

Tip:React 采用了复杂的、先进的方法确定应该更新什么以及何时更新。如果最终使用了 shouldComponentUpdate,则应该是那些方法由于某种原因不够用的情况下。

新增生命周期方法:getDerivedStateFromProps、getSnapshotBeforeUpdate。都属于不常见的情形。

spug 使用了哪些生命周期方法

React 有那么多生命周期相关方法,那么像 spug 这种运维相关的开源项目用到了哪些,是否都用到了?

答:只用到了挂载、更新和卸载 3 个方法。

请看笔者统计:

constructor

共 25 处

几乎都是初始化 state。没有发送数据请求。摘录如下:

constructor(props) {
    super(props);
    this.state = {
        groupMap: {}
    }
}
constructor(props) {
    super(props);
    this.textView = null;
    this.JSONView = null;
    this.state = {
        view: '1'
    }
}
constructor(props) {
    super(props);
    this.input = null;
    this.state = {
        fetching: false,
        showDot: false,
        uploading: false,
        uploadStatus: 'active',
        pwd: [],
        objects: [],
        percent: 0
    }
}
componentDidMount

共 29 处。都是用于发送 ajax 请求数据。摘录如下:

store.fetchRecords();
http.post('/api/config/history/'
this.updateValue()

Tip: 感觉请求数据时机有点迟。因为 vue 中请求数据我们通常会写在 created 生命周期中,能更快获取到服务端数据,减少页面loading 时间。

0 处使用

以下生命周期方法都在 spug 中使用。

  • shouldComponentUpdate
  • componentDidUpdate
  • componentWillUnmount
  • componentDidCatch(构造函数、渲染、生命周期中未捕获的错误)
  • 过时的生命周期:componentWillMount、componentWillReceiveProps、componentWillUpdate。
  • 不建议使用:forceUpdate、shouldComponentUpdate
  • 新增生命周期方法:getDerivedStateFromProps、getSnapshotBeforeUpdate
React.useEffect

我们知道 React.useEffect 可以在函数组件中模拟 componentDidMount、componentDidUpdate、componentWillUnmount。就像这样:

// 相当于 componentDidMount()
React.useEffect(() => {
    console.log('a')
}, [])
// 相当于 componentDidMount()、componentDidUpdate()
React.useEffect(() => {
    console.log('a')
})
// 相当于 componentDidMount、componentWillUnmount
React.useEffect(() => {
    console.log('a')
    return () => {
        console.log('b')
    }
}, [])

Tip: 有关 React.useEffect 的详细介绍请看 这里 -> 体验 useEffect

搜索 useEffect,发现 spug 有 126 处。作用就这 3 点:

  • 模拟 componentDidMount 发送网络请求,或初始化数据,或设置定时器
  • 模拟 componentWillUnmount 删除定时器
  • 模拟 componentDidUpdate

摘录如下:

// 模拟 componentDidMount,用于发送网络请求
useEffect(() => {
    setFetching(true);
    http.get('/api/app/deploy/')
        .then(res => setDeploys(res))
        .finally(() => setFetching(false))
    if (!envStore.records.length) {
        envStore.fetchRecords().then(_initEnv)
    } else {
        _initEnv()
    }
}, [])
// 模拟 componentDidMount 发送网络请求、添加定时器,模拟 componentWillUnmount 删除定时器
useEffect(() => {
    fetch();
    listen();
    timer = setInterval(() => {
      if (ws.readyState === 1) {
        ws.send('ping')
      } else if (ws.readyState === 3) {
        listen()
      }
    }, 10000)
    return () => {
      if (timer) clearInterval(timer);
      if (ws.close) ws.close()
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
React.useEffect(() => {
    store.fetchGitlabList()
    store.fetchProjectName()
}, [])
// 模拟 componentDidMount()、componentDidUpdate()
useEffect(() => {
    setLoading(true);
    http.get('/api/home/alarm/', {params})
      .then(res => setRes(res))
      .finally(() => setLoading(false))
}, [params])
useEffect(() => {
    let socket;
    initialTerm()
    http.get(`/api/repository/${store.record.id}/`)
      .then(res => {
        term.write(res.data)
        setStep(res.step)
        if (res.status === '1') {
          socket = _makeSocket(res.index)
        } else {
          setStatus('wait')
        }
      })
      .finally(() => setFetching(false))
    return () => socket && socket.close()
    // eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, [])
// 写多个 useEffect 也可以
useEffect(() => {
    if (!loading) {
      http.get('/api/exec/history/')
        .then(res => setHistories(res))
    }
}, [loading])

useEffect(() => {
    return () => {
        store.host_ids = []
        if (store.showConsole) {
        store.switchConsole()
        }
    }
}, [])
// 挂载时注册事件 resize,卸载时注销事件 resize
useEffect(() => {
    store.tag = ''
    gCurrent = current
    const fitPlugin = new FitAddon()
    term.setOption('disableStdin', false)
    term.setOption('fontFamily', 'Source Code Pro, Courier New, Courier, Monaco, monospace, PingFang SC, Microsoft YaHei')
    term.setOption('theme', {background: '#f0f0f0', foreground: '#000', selection: '#999', cursor: '#f0f0f0'})
    term.loadAddon(fitPlugin)
    term.open(el.current)
    fitPlugin.fit()
    term.write('\x1b[36m### WebSocket connecting ...\x1b[0m')
    const resize = () => fitPlugin.fit();
    window.addEventListener('resize', resize)
    setTerm(term)

    return () => window.removeEventListener('resize', resize);
    // eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

父子组件的生命周期

笔者创建父子两个组件,加入常见的生命周期方法(如:componentDidMount、shouldComponentUpdate、componentDidUpdate、componentWillUnmount、componentDidCatch),功能很简单,子组件的文案来自父组件的 state,父组件的 state.text 能被用户通过 input 修改。

代码如下:

<script type="text/babel">
    class ChildComponent extends React.Component {
        static propTypes = {
            name: PropTypes.string
        };
        static defaultProps = (function () {
            console.log("ChildComponent : defaultProps。孩子的 defaultProps 是");
            return {};
        })();
        constructor(props) {
            super(props);
            console.log("ChildComponent: state。孩子的 state 是");
            this.state = {
                text: "peng"
            };
        }

        // 组件挂载后(插入 DOM 树中)立即调用。常做网络请求
        componentDidMount() {
            console.log("ChildComponent : componentDidMount");
        }

        shouldComponentUpdate(nextProps, nextState) {
            console.log("<ChildComponent/> - shouldComponentUpdate()");
            return true;
        }
        // React 更新 dom 和 refs 后调用
        componentDidUpdate(previousProps, previousState) {
            console.log("ChildComponent: componentDidUpdate");
        }
        componentWillUnmount() {
            console.log("ChildComponent: componentWillUnmount");
        }

        render() {
            if (this.props.content === 'jiali12') {
                throw new Error("模拟组件报错");
            }

            console.log("ChildComponent: render");
            // 下面两种写法等效。一个是单根一个是多根。

            // return <div className="subClass">
            //     sub
            //     <p>{this.props.content}</p>
            // </div>

            // 下面这种数组的写法需要给每个元素添加 key,否则报错如下:
            // Warning: Each child in a list should have a unique "key" prop.
            // React 要求每个被迭代项传递一个 key,对于 render() 方法返回的任何数组组件亦如此。
            return [
                <div key="name" className="subClass">sub</div>,
                <p key="content">{this.props.content}</p>
            ]

        }
    }

    class ParentComponent extends React.Component {
        static defaultProps = (function () {
            console.log("ParentComponent: defaultProps。我的 defaultProps 是");
            return {
                true: false
            };
        })();
        constructor(props) {
            super(props);
            console.log("ParentComponent: state。我的 state 是");
            this.state = { text: "jiali" };
            this.onInputChange = this.onInputChange.bind(this);
        }

        onInputChange(e) {
            const text = e.target.value;
            this.setState(() => ({ text: text }));
        }
        componentDidMount() {
            console.log("ParentComponent: componentDidMount");
        }
        componentDidUpdate(previousProps, previousState) {
            console.log("ParentComponent: componentDidUpdate");
        }
        componentWillUnmount() {
            console.log("ParentComponent: componentWillUnmount");
        }
        // 此生命周期在后代组件抛出错误后被调用
        componentDidCatch(err, errorInfo) {
            console.log("componentDidCatch");
            this.setState(() => ({ err, errorInfo }));
        }

        render() {
            if (this.state.err) {
                return <div>降级处理</div>
            }
            console.log("ParentComponent: render");
            return <div className="parentClass">

                <p>parent</p>
                <input
                    key="input"
                    value={this.state.text}
                    onChange={this.onInputChange}
                />
                <ChildComponent content={this.state.text} />
            </div>

        }
    }

    ReactDOM.render(
        <ParentComponent />,
        document.getElementById('root')
    );
</script>

浏览器中生成的页面结构如下:

<div id="root">
    <div class="parentClass">
        <p>parent</p>
        <input value="jiali">
        <!-- 子组件 -->
        <div class="subClass">sub</div>
        <p>jiali</p>
        <!-- /子组件 -->
    </div>
</div>

控制台输出:

ChildComponent : defaultProps。孩子的 defaultProps 是
ParentComponent: defaultProps。我的 defaultProps 是
ParentComponent: state。我的 state 是
ParentComponent: render
ChildComponent: state。孩子的 state 是
ChildComponent: render
ChildComponent : componentDidMount
ParentComponent: componentDidMount

Tip:尽管初始 state 和属性并不使用特定的生命周期方法,但他们为组件提供数据方面发挥了重要作用,所以有必要把它们作为生命周期的一部分

此刻是初次渲染,也就是挂载时,根据输出日志我们知道:

  • 先 defaultProps 后 state
  • 先 render 后 componentDidMount
  • 先 render 父组件,后 render 子组件
  • 先挂载子组件,后挂载父组件

为何先 render 父组件,又先挂载子组件?

Tip: 其实 vue 也是这样,请看 这里

笔者推测如下:

入口是这里:

ReactDOM.render(
    <ParentComponent />,
    document.getElementById('root')
);

先渲染父组件,发现里面有子组件,接着渲染子组件。

我们将虚拟 Dom看做真实 Dom,虚拟 Dom 如何生成真实 Dom 的我们暂时不去管它,这是 React 做的事情。虚拟 DOM 树的生成过程前文我们已经说了:像一个小孩不停的问“里面是什么?”,”里面是什么?“,直到搞懂所有的子孙组件,就可以形成一颗完整的树。

接着往 input 中输入 1(此刻显示jiali1),控制台输出:

ParentComponent: render
<ChildComponent/> - shouldComponentUpdate()
ChildComponent: render
ChildComponent: componentDidUpdate
ParentComponent: componentDidUpdate

同样,先执行父组件的 render 再执行子组件的 render,先调用子组件的 componentDidUpdate 再调用父组件的 componentDidUpdate。可以翻译成:

  • 要渲染父组件
  • 里面有子组件需要渲染
  • 子组件更新完毕
  • 父组件更新完毕

接着往 input 中输入 2(此刻显示jiali12),触发子组件 render 里面报错,父组件中的 componentDidCatch 被执行,实现降级处理,页面显示 降级处理

到此这篇关于React 的数据流和生命周期的文章就介绍到这了,更多相关React 生命周期内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

详解React 的数据流和生命周期的更多相关文章

  1. ios – React native链接到另一个应用程序

    如果是错误的,有人知道如何调用正确的吗?

  2. ios – React Native – 在异步操作后导航

    我正在使用ReactNative和Redux开发移动应用程序,我正面临着软件设计问题.我想调用RESTAPI进行登录,如果该操作成功,则导航到主视图.我正在使用redux和thunk所以我已经实现了异步操作,所以我的主要疑问是:我应该把逻辑导航到主视图?我可以直接从动作访问导航器对象并在那里执行导航吗?.我对组件中的逻辑没有信心.似乎不是一个好习惯.有没有其他方法可以做到这一点?

  3. 在ios中使用带有React Native(0.43.4)的cocoapods的正确方法是什么?

    我已经挖掘了很多帖子试图使用cocoapods为本地ios库设置一个反应原生项目,但我不可避免地在#import中找到了丢失文件的错误.我的AppDelegate.m文件中的语句.什么是使用反应原生的可可豆荚的正确方法?在这篇文章发表时,我目前的RN版本是0.43.4,而我正在使用Xcode8.2.1.这是我的过程,好奇我可能会出错:1)

  4. ios – React Native WebView滚动行为无法按预期工作

    如何确保滚动事件的行为与ReactNative应用程序中的浏览器相同?

  5. ios – React Native – BVLinearGradient – 找不到’React/RCTViewManager.h’文件

    谢谢.解决方法几天前我遇到了完全相同的问题.问题是在构建应用程序时React尚未链接.试试这个:转到Product=>Scheme=>管理方案…=>点击你的应用程序Scheme,然后点击Edit=>转到Build选项卡=>取消选中ParallelizeBuild然后点击标志添加目标=>搜索React,选择第一个名为React的目标,然后单击Add然后在目标列表中选择React并将其向上拖动到该列表中的第一个.然后转到Product=>再次清理并构建项目.这应该有所帮助.

  6. ios – React Native – NSNumber无法转换为NSString

    解决方法在你的fontWeight()函数中也许变成:

  7. ios – React native error – react-native-xcode.sh:line 45:react-native:command not found命令/ bin/sh失败,退出代码127

    尝试构建任何(新的或旧的)项目时出现此错误.我的节点是版本4.2.1,react-native是版本0.1.7.我看过其他有相同问题的人,所以我已经更新了本机的最新版本,但是我仍然无法通过xcode构建任何项目.解决方法要解决此问题,请使用以下步骤:>使用节点版本v4.2.1>cd进入[你的应用]/node_modules/react-native/packager>$sh./packager.s

  8. 反应原生 – 如何通过Xcode构建React Native iOS应用程序到设备?

    我试图将AwesomeProject应用程序构建到设备上.构建成功并启动屏幕显示,但后来我看到一个红色的“无法连接到开发服务器”屏幕.它表示“确保节点服务器正在运行–从Reactroot运行”npmstart“.看起来节点服务器已经运行,因为当我做npm启动时,我收到一个EADDRINUSE消息,表示该端口已经在使用.解决方法从设备访问开发服务器您可以使用开发服务器快速迭代设备.要做到这一点,你的

  9. 静音iOS推送通知与React Native应用程序在后台

    我有一个ReactNative应用程序,我试图获得一个发送到JavaScript处理程序的静默iOS推送通知.我看到的行为是AppDelegate中的didReceiveRemoteNotification函数被调用,但是我的JavaScript中的处理程序不会被调用,除非应用程序在前台,或者最近才被关闭.我很困惑的事情显然是应用程序正在被唤醒,并且它的didReceiveRemoteNotifi

  10. 如何为iOS的React Native设置分析

    所以我已经完成了一个针对iOS的ReactNative项目,但是我想在其中分析.我尝试了react-native-google-analytics软件包,但是问题阻止了它的正常工作.此外,react-native-cordova-plugin软件包只适用于Android,因此插入Cordova插件进行分析的能力现在已成为问题.我也没有Swift/ObjectiveC的经验,所以将完全失去GA的插入.有没有人有任何建议如何连接GoogleAnalytics的ReactNativeforiOS?

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部