精华内容
下载资源
问答
  • React setState 是同步更新还是异步更新
    千次阅读
    2020-12-31 11:19:55

    1. 为什么使用setState

    在开发中我们并不能直接通过修改state的值来让界面发生更新:

    • 因为修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化;
    • React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;
    • 必须通过setState来告知React数据已经发生了变化;

    如果我们直接修改state中的值,会报错:

    this.state.counter += 1   // Do not mutate state directly. Use setState()
    

    那在组件中并没有实现setState的方法,为什么可以调用呢?原因很简单,setState方法是当前组件从Component中继承过来的:
    在这里插入图片描述

    所以,在React中,我们需要使用setState来实现数据的更新:

    this.setState({
      counter: this.state.counter + 1 
    })  
    

    2. setState 异步更新

    组件除了可以接收外界传递的状态外,还可以拥有自己的状态,并且这个状态也可以通过 setState 来进行更新。setState 用于变更状态,触发组件重新渲染,更新视图 UI。其语法如下:

    setState(updater, callback)
    

    setState 可以接收两个参数:第一个参数可以是对象或函数,第二个参数是函数。

    • 第一个参数是对象的写法
    this.setState({
        key: newState
    });
    
    • 第一个参数是函数的写法
    // prevState 是上一次的 state,props 是此次更新被应用时的 props
    this.setState((prevState, props) => {
      return {
          key: prevState.key 
      }
    })
    

    那么,这两种写法的区别是什么呢?我们来看计数器的例子:

    class App extends React.Component {
      constructor (props) {
        super (props)
        this.state = {
          val: 0
        }
      }
      handleClick () {
        this.setState({
            val: this.state.val + 1
        })
      }
      render () {
        return (
          <div className="App">
            <input type="text" value={this.state.val} disabled/>
            <input type="button" onClick={this.handleClick.bind(this)} />
          </div>
        )
      }
    }
    

    如果在 handleClick 方法内调两次 setState,是不是每次点击就自增2了呢?

    handleClick () {
        this.setState({
            val: this.state.val + 1
        })
         this.setState({
            val: this.state.val + 1
        })
      }
    

    结果并非我们想的那样,每次点击按钮,依然是自增1。这是因为调用 setState 其实是异步的,也就是 setState 调用之后,this.state 不会立即映射为新的值。上面代码会解析为以下形式:

    // 后面的数据会覆盖前面的更改,所以最终只加了一次.
    Object.assign(
      previousState,
      {val: state.val + 1},
      {val: state.val + 1},
    )
    

    在上面我们调用了两次 setState,但 state 的更新会被合并,所以即使多次调用 setState,实际上可能也只是会重新渲染一次。

    如果想基于当前的 state 来计算出新的值,那么 setState 第一个参数不应该像上面一样传递一个对象,而应该传递一个函数。

    handleClick () {
        this.setState((prevState, props) => {
                val: prevState.val + 1
            }
        })
         this.setState((prevState, props) => {
                val: prevState.val + 1
            }
        })
     }
    

    此时,在 handleClick 方法内调两次 setState,就能实现每次点击都自增2了。

    传递一个函数可以让你在函数内访问到当前的 state 值。 setState 的调用是分批的,所以可以链式地进行更新,并确保它们是一个建立在另一个之上的,这样才不会发生冲突。

    setState 的第二个参数是一个可选的回调函数。这个回调函数将在组件重新渲染后执行。等价于在 componentDidUpdate 生命周期内执行。通常建议使用 componentDidUpdate 来代替此方式。在这个回调函数中你可以拿到更新后 state 的值。

    this.setState({
        key1: newState1,
        key2: newState2,
        ...
    }, callback) // 第二个参数是 state 更新完成后的回调函数
    

    通过上面内容,可以知道调用 setState 时,组件的 state 并不会立即改变, setState 只是把要修改的 state 放入一个队列, React 会优化真正的执行时机,并出于性能原因,会将 React 事件处理程序中的多次React 事件处理程序中的多次 setState 的状态修改合并成一次状态修改。 最终更新只产生一次组件及其子组件的重新渲染,这对于大型应用程序中的性能提升至关重要。

    批量更新的流程图如下:

    this.setState({
      count: this.state.count + 1    ===>    入队,[count+1的任务]
    });
    this.setState({
      count: this.state.count + 1    ===>    入队,[count+1的任务,count+1的任务]
    });
                                              ↓
                                             合并 state,[count+1的任务]
                                              ↓
                                             执行 count+1的任务
    

    注意: 在React中,不能直接使用 this.state.key = value 方式来更新状态,这种方式 React 内部无法知道我们修改了组件,因此也就没办法更新到界面上。所以一定要使用 React 提供的 setState 方法来更新组件的状态。

    为什么 setState 是异步的,React官方团队的解释如下:

    • 保持内部一致性。如果改为同步更新的方式,尽管 setState 变成了同步,但是 props 不是。
    • 为后续的架构升级启用并发更新。为了完成异步渲染,React 会在 setState 时,根据它们的数据来源分配不同的优先级,这些数据来源有:事件回调句柄、动画效果等,再根据优先级并发处理,提升渲染性能。

    简单总结如下:

    • setState设计为异步,可以显著的提升性能。如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;
    • 如果同步更新了state,但是还没有执行render函数,那么stateprops不能保持同步。stateprops不能保持一致性,会在开发中产生很多的问题;

    3. setState 同步场景

    上面的例子使我们建立了这样一个认知:setState 是异步的,但下面这个案例又会颠覆你的认知。如果我们将 setState 放在 setTimeout 事件中,那情况就完全不同了:

    class Test extends Component {
        state = {
            count: 0
        }
        componentDidMount(){
            this.setState({ count: this.state.count + 1 });
            console.log(this.state.count);
            setTimeout(() => {
              this.setState({ count: this.state.count + 1 });
              console.log("setTimeout: " + this.state.count);
            }, 0);
        }
        render(){
            ...
        }
    }
    

    这时就会输出 0,2。因为 setState 并不是真正的异步函数,它实际上是通过队列延迟执行操作实现的,通过 isBatchingUpdates 来判断 setState 是先存进 state 队列还是直接更新。值为 true 则执行异步操作,false 则直接同步更新。

    在 onClick、onFocus 等事件中,由于合成事件封装了一层,所以可以将 isBatchingUpdates 的状态更新为 true;在 React 的生命周期函数中,同样可以将 isBatchingUpdates 的状态更新为 true。那么在 React 自己的生命周期事件和合成事件中,可以拿到 isBatchingUpdates 的控制权,将状态放进队列,控制执行节奏。而在外部的原生事件中,并没有外层的封装与拦截,无法更新 isBatchingUpdates 的状态为 true。这就造成 isBatchingUpdates 的状态只会为 false,且立即执行。所以在 addEventListener 、setTimeout、setInterval 这些原生事件中都会同步更新。

    实际上,setState 并不是具备同步这种特性,只是在特定的情境下,它会从 React 的异步管控中“逃脱”掉。

    4. 调用 setState 发生了什么

    修改 state 方法有两种:

    • 构造函数里修改 state ,只需要直接操作 this.state 即可, 如果在构造函数里执行了异步操作,就需要调用 setState 来触发重新渲染。
    • 在其余的地方需要改变state的时候只能使用 setState,这样 React 才会触发 UI 更新。

    所以, setState 时会设置新的 state 并更新 UI。当然,state 的更新可能是异步的,出于性能考虑,React 可能会把多个 setState 调用合并成一个调用。那么 state 的更新何时是同步何时又是异步的呢?

    我们来看一下setState的执行流程图:
    在这里插入图片描述

    (1)setState

    下面来看下每一步的源码,首先是 setState 入口函数:

    ReactComponent.prototype.setState = function (partialState, callback) {
      this.updater.enqueueSetState(this, partialState);
      if (callback) {
        this.updater.enqueueCallback(this, callback, 'setState');
      }
    };
    

    入口函数在这里就是充当一个分发器的角色,根据入参的不同,将其分发到不同的功能函数中去。这里我们以对象形式的入参为例,可以看到它直接调用了 this.updater.enqueueSetState 这个方法。

    (2)enqueueSetState

    enqueueSetState: function (publicInstance, partialState) {
      // 根据 this 拿到对应的组件实例
      var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
      // 这个 queue 对应的就是一个组件实例的 state 数组
      var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
      queue.push(partialState);
      //  enqueueUpdate 用来处理当前的组件实例
      enqueueUpdate(internalInstance);
    }
    

    这里 enqueueSetState 做了两件事:

    • 将新的 state 放进组件的状态队列里;
    • 用 enqueueUpdate 来处理将要更新的实例对象。

    (3)enqueueUpdate

    function enqueueUpdate(component) {
      ensureInjected();
      // 注意这一句是问题的关键,isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段
      if (!batchingStrategy.isBatchingUpdates) {
        // 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
        batchingStrategy.batchedUpdates(enqueueUpdate, component);
        return;
      }
      // 否则,先把组件塞入 dirtyComponents 队列里,让它“再等等”
      dirtyComponents.push(component);
      if (component._updateBatchNumber == null) {
        component._updateBatchNumber = updateBatchNumber + 1;
      }
    }
    

    这个 enqueueUpdate 引出了一个关键的对象——batchingStrategy,该对象所具备的isBatchingUpdates属性直接决定了当下是要走更新流程,还是应该排队等待;其中的batchedUpdates 方法更是能够直接发起更新流程。由此可以推测,batchingStrategy 或许正是 React 内部专门用于管控批量更新的对象。

    (4)batchingStrategy

    var ReactDefaultBatchingStrategy = {
      // 全局唯一的锁标识
      isBatchingUpdates: false,
      // 发起更新动作的方法
      batchedUpdates: function(callback, a, b, c, d, e) {
        // 缓存锁变量
        var alreadyBatchingStrategy = ReactDefaultBatchingStrategy. isBatchingUpdates
        // 把锁“锁上”
        ReactDefaultBatchingStrategy. isBatchingUpdates = true
        if (alreadyBatchingStrategy) {
          callback(a, b, c, d, e)
        } else {
          // 启动事务,将 callback 放进事务里执行
          transaction.perform(callback, null, a, b, c, d, e)
        }
      }
    }
    

    batchingStrategy 对象可以理解为它是一个“锁管理器”。

    这里的“锁”,是指 React 全局唯一的 isBatchingUpdates 变量,isBatchingUpdates 的初始值是 false,意味着“当前并未进行任何批量更新操作”。每当 React 调用 batchedUpdate 去执行更新动作时,会先把这个锁给“锁上”(置为 true),表明“现在正处于批量更新过程中”。当锁被“锁上”的时候,任何需要更新的组件都只能暂时进入 dirtyComponents 里排队等候下一次的批量更新,而不能随意“插队”。此处体现的“任务锁”的思想,是 React 面对大量状态仍然能够实现有序分批处理的基石。**

    5. 总结

    对于那道常考的面试题:setState 是同步更新还是异步更新? 我们心中或许已经有了答案。

    setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同:在 React 钩子函数及合成事件中,它表现为异步;而在 setTimeout、setInterval 等函数中,包括在 DOM 原生事件中,它都表现为同步。这种差异,本质上是由 React 事务机制和批量更新机制的工作方式来决定的。

    在源码中,通过 isBatchingUpdates 来判断setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。

    那什么情况下 isBatchingUpdates 会为 true 呢?

    • 在 React 可以控制的地方,isBatchingUpdates就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。
    • 在 React 无法控制的地方,比如原生事件,具体就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新。

    一般认为,做异步设计是为了性能优化、减少渲染次数,React 团队还补充了两点:

    • 保持内部一致性。如果将 state 改为同步更新,那尽管 state 的更新是同步的,但是 props不是。
    • 启用并发更新,完成异步渲染。

    附一个常考的面试题:

    class Test extends React.Component {
      state  = {
          count: 0
      };
        componentDidMount() {
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
        setTimeout(() => {
          this.setState({count: this.state.count + 1});
          console.log(this.state.count);
          this.setState({count: this.state.count + 1});
          console.log(this.state.count);
        }, 0);
      }
      render() {
        return null;
      }
    };
    
    • 首先第一次和第二次的 console.log,都在 React 的生命周期事件中,所以是异步的处理方式,则输出都为 0;
    • 而在 setTimeout 中的 console.log 处于原生事件中,所以会同步的处理再输出结果,但需要注意,虽然 count 在前面经过了两次的 this.state.count + 1,但是每次获取的 this.state.count 都是初始化时的值,也就是 0;
    • 所以此时 count 是 1,那么后续在 setTimeout 中的输出则是 2 和 3。

    所以答案是 0,0,2,3。

    更多相关内容
  • 主要介绍了c#从数据库里取得数据并异步更新ui的方法,大家参考使用吧
  • 引言 在进行Winform程序开发需要进行大量的数据的读写操作的时候,往往会需要...下面就开始一步步的去实现异步线程更新ui的demo程序吧。 应用背景 写入一定量的数据到文本文件中,同时需要在主界面中反应出写入数据的
  • 使用AsyncTask异步任务; 使用runOnUiThread(action)方法; 使用Handler的post(Runnabel r)方法; 下面分别使用四种方式来更新一个TextView。 1.使用Handler消息传递机制 package ...
  • 前言 我们知道在Android开发中不能在非...android中有下列几种异步更新ui的解决办法: Activity.runOnUiThread(Runnable) View.post(Runnable) long) View.postDelayed(Runnable, long) 使用handler(线程间通讯)
  • 先说一下应用吧,一般我们进行网络训练时,都有一个batchsize设置,也就是一个batch一个batch的更新梯度,能有这个batch的前提是这个batch中所有的图片的大小一致,这样才能组成一个placeholder。那么若一个网络对...
  • Winform实现多线程异步更新UI(进度及状态信息) 实例代码
  • vue2.0里面的深入响应式原理的异步更新队列 官网说明如下: 只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会一次推入到队列中。这种...
  • 主要介绍了android使用handler异步更新ui的示例,大家参考使用吧
  • 本篇文章主要介绍了从Vue.js源码看异步更新DOM策略及nextTick,具有一定的参考价值,感兴趣的小伙伴们可以参考一
  • VUE异步更新DOM 首先,Vue 在更新 DOM 时是异步执行的! 所以只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在...
  • 主要给大家介绍了关于vue在使用ECharts时的异步更新和数据加载的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
  • 安卓Android源码——动态ListView,支持异步更新列表,异步更新图片.rar
  • C# WPF异步更新UI元素

    2022-03-28 22:06:37
    } 正确方式2: 使用异步函数, asyn, await 关键字 async Task UpdateMessage(string msg, Brush brush) { await this.txtMsg.Dispatcher.InvokeAsync(new Action(() => { Run run = new Run(); Paragraph paragraph ...

    常见的问题"调用线程无法访问此对象,因为另一个线程拥有该对象。"

    常见错误测试代码如下:

    private void UpdateWrongMethod()
      {
        try
         {
             Thread.Sleep(1000);
             txtMsg.AppendText("Wrong Method Demo!");
         }
        catch (Exception ex)
         {
           UpdateMessage(ex.Message, System.Windows.Media.Brushes.Red);
         }
    }
    
     private  void btnWrongMethod_Click(object sender, RoutedEventArgs e)
      {
        try
          {
              Thread th = new Thread(UpdateWrongMethod);
              th.Start();
          }
         catch (Exception ex)
         {
              UpdateMessage(ex.Message, System.Windows.Media.Brushes.Red);
         }
     }
    

    在这里插入图片描述

    正确方式1:

    使用 Dispatcher.BeginInvoke 方法

    private void UpdateRightMethod()
    {
    
      try
        {
         Thread.Sleep(1000);            		  
         this.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, new Action(() =>
              {
                 txtMsg.AppendText("Use dispatcher Begin Invoke is right!");
              }));
       }
       catch (Exception ex)
       {
           MessageBox.Show(ex.Message);
       }
    }
    
    private void btnRightMethod_Click(object sender, RoutedEventArgs e)
    {
       Thread th = new Thread(UpdateRightMethod);
       th.Start();
    }
    

    在这里插入图片描述

    正确方式2:

    使用异步函数, asyn, await 关键字

    async Task UpdateMessage(string msg, Brush brush)
    {
      await this.txtMsg.Dispatcher.InvokeAsync(new Action(() =>
      {
          Run run = new Run();
          Paragraph paragraph = new Paragraph();
          run.Foreground = brush;
          run.Text = msg;
          paragraph.Inlines.Add(run);
          paragraph.LineHeight = 1;
          this.txtMsg.Document.Blocks.Add(paragraph);
          this.txtMsg.ScrollToEnd();
     }));
    
    }
    
    private async void btnDirectRight\_Click(object sender, RoutedEventArgs e)
    {
      for (int i = 1; i < 10; i++)
       {
          await UpdateMessage($"Test Point {i}", Brushes.Green);
          await Task.Delay(500);
       }
    }
    

    在这里插入图片描述

    展开全文
  • 今天小编就为大家分享一篇vue 解决异步数据更新问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
  • AJAX 通过在后台与服务器交换少量数据的方式,允许网页进行异步更新。这意味着有可能在不重载整个页面的情况下,对网页的一部分进行更新。 通过 jQuery AJAX,你可以直接把远程数据载入网页被选HTML元素中。 Jquery...
  • Android应用源码动态ListView,支持异步更新列表,异步更新图片.zip项目安卓应用源码下载Android应用源码动态ListView,支持异步更新列表,异步更新图片.zip项目安卓应用源码下载 1.适合学生毕业设计研究参考 2.适合...
  • redis和mysql的异步更新

    2021-12-21 12:27:47
    #指定 gearman 的服务信息到server2上的4730端口 实现异步更新 server4: vim test.sql #编写 mysql 触发器 注释掉2、3行,取消注释5到9行 mysql < test.sql mysql --> SHOW TRIGGERS FROM test; #查看触发器 真机中...

    redis和mysql如何同步:

    一定要确定好master 

    --------  (上次实验server3是master)

    从真机中把lib_mysqludf_json-master.zip传给server4
    scp /home/westos/lib_mysqludf_json-master.zip server4: 

    在server2中开启nginx和php-fpm服务
    server2:

    nginx  #开启nginx
    systemctl start  php-fpm.service   #开启php-fpm服务

     server4:【lib_mysqludf_json】

    systemctl start mariadb      #开启mariadb服务
    ps ax  #查看mariadb服务是否开启
    mysql  
    --> use test
    --> select * from test;   #查看test表

    yum list mariadb-devel   #列出mariadb-devel
    yum install -y unzip    #下载unzip解压工具
    unzip lib_mysqludf_json-master.zip  #解压
    cd lib_mysqludf_json-master/
    ls
    ll
    yum install -y gcc   #下载依赖环境
    ls
    gcc $(mysql_config --cflags) -shared -fPIC -o lib_mysqludf_json.so lib_mysqludf_json.c
    ls
    ll
    cd /usr/lib64/mysql/
    ls
    ll
    cd plugin/
    ls
    pwd
    mysql
    > show global variables like 'plugin_dir';
    

    cd
    cd lib_mysqludf_json-master/
    ls
    cp lib_mysqludf_json.so /usr/lib64/mysql/plugin/
    cd /usr/lib64/mysql/plugin/
    mysql
    > CREATE FUNCTION json_object RETURNS STRING SONAME 'lib_mysqludf_json.so';
    > select * from mysql.func;    #查看函数


    安装 gearman-mysql-udf

    从真机中把实验需要的包传给server4:
    scp libgearman-* libevent-devel-2.0.21-4.el7.x86_64.rpm server4:
    scp gearman-mysql-udf-0.6.tar.gz server4:

     server4:  

     安装 gearman-mysql-udf【这个插件是用来管理调用 Gearman 的分布式的队列】

    cd
    yum install -y libgearman-* libevent-devel-2.0.21-4.el7.x86_64.rpm  
    tar zxf gearman-mysql-udf-0.6.tar.gz
    cd gearman-mysql-udf-0.6/
    ./configure --libdir=/usr/lib64/mysql/plugin/ --with-mysql
    make && make install
    cd /usr/lib64/mysql/plugin/
    ls
    cd
    mysql
    --> CREATE FUNCTION gman_do_background RETURNS STRING SONAME 'libgearman_mysql_udf.so';  
    #注册函数
    --> CREATE FUNCTION gman_servers_set RETURNS STRING SONAME 'libgearman_mysql_udf.so';   
    #注册函数
    --> select * from mysql.func;    #查看函数

     从真机中把实验需要的包传给server2:
    scp gearmand-1.1.12-18.el7.x86_64.rpm server2:
    scp libgearman-1.1.12-18.el7.x86_64.rpm server2:

    server2中:【slave端】 

    yum install gearmand-1.1.12-18.el7.x86_64.rpm libgearman-1.1.12-18.el7.x86_64.rpm 
    systemctl start gearmand.service   #开启gearmand服务
    netstat -anplt    #查看4730端口是否开启

     server4:

    mysql
    --> SELECT gman_servers_set('172.25.70.2:4730');

    #指定 gearman 的服务信息到server2上的4730端口


    实现异步更新

     

    server4:

    vim test.sql  #编写 mysql 触发器
      注释掉2、3行,取消注释5到9行
    mysql < test.sql
    mysql
    --> SHOW TRIGGERS FROM test;     #查看触发器


    真机中把需要的文件和包传给server2

     server2:

    vim worker.php    #编写 gearman 的 worker 端
       第7行改为:$redis->connect('172.25.70.3', 6379);  #指向ip是master
    php -m | grep redis
    php -m | grep gearman
    yum install -y php-pecl-gearman-1.1.2-1.el7.x86_64.rpm 
    php -m | grep gearman
    systemctl reload php-fpm   #平滑更新
    mv worker.php /usr/local/
    which php
    php /usr/local/worker.php   #先不要打入后台【ctrl+z打入后台】
    ps ax #查看进程  #确保worker.php在后台运行,只有一个进程开启


     

     server4:
    mysql
    > update test set name='hello' where id=1;   #更新 mysql 中的数据

    server3: #一定要在master主机上查看
    redis-cli
    > get 1 #看该的名字是否同步

    展开全文
  • 动态ListView,支持异步更新列表,异步更新图片毕业设计—(包含完整源码可运行).zip
  • 源码参考,欢迎下载
  • 动态ListView,支持异步更新列表,异步更新图片.zip
  • Vue的异步更新实现原理

    千次阅读 2020-12-21 15:52:13
    } 这就涉及到Vue底层的异步更新原理,也要说一说nextTick的实现。不过在说nextTick之前,有必要先介绍一下JS的事件运行机制。 JS运行机制 众所周知,JS是基于事件循环的单线程的语言。执行的步骤大致是: 当代码...

    关注公众号 前端开发博客,回复“加群”

    加入我们一起学习,天天进步

    作者:Liqiuyue

    链接:https://juejin.cn/post/6908264284032073736

    最近面试总是会被问到这么一个问题:在使用vue的时候,将for循环中声明的变量i从1增加到100,然后将i展示到页面上,页面上的i是从1跳到100,还是会怎样?答案当然是只会显示100,并不会有跳转的过程。

    怎么可以让页面上有从1到100显示的过程呢,就是用setTimeout或者Promise.then等方法去模拟。

    讲道理,如果不在vue里,单独运行这段程序的话,输出一定是从1到100,但是为什么在vue中就不一样了呢?

    for(let i=1; i<=100; i++){
     console.log(i);
    }
    

    这就涉及到Vue底层的异步更新原理,也要说一说nextTick的实现。不过在说nextTick之前,有必要先介绍一下JS的事件运行机制。

    JS运行机制

    众所周知,JS是基于事件循环的单线程的语言。执行的步骤大致是:

    1. 当代码执行时,所有同步的任务都在主线程上执行,形成一个执行栈

    2. 在主线程之外还有一个任务队列(task queue),只要异步任务有了运行结果就在任务队列中放置一个事件;

    3. 一旦执行栈中所有同步任务执行完毕(主线程代码执行完毕),此时主线程不会空闲而是去读取任务队列。此时,异步的任务就结束等待的状态被执行。

    4. 主线程不断重复以上的步骤。

    我们把主线程执行一次的过程叫一个tick,所以nextTick就是下一个tick的意思,也就是说用nextTick的场景就是我们想在下一个tick做一些事的时候。

    所有的异步任务结果都是通过任务队列来调度的。而任务分为两类:宏任务(macro task)和微任务(micro task)。它们之间的执行规则就是每个宏任务结束后都要将所有微任务清空。常见的宏任务有setTimeout/MessageChannel/postMessage/setImmediate,微任务有MutationObsever/Promise.then

    想要透彻学习事件循环,推荐Jake在JavaScript全球开发者大会的演讲,保证讲懂!

    nextTick原理

    派发更新

    大家都知道vue的响应式的靠依赖收集和派发更新来实现的。在修改数组之后的派发更新过程,会触发setter的逻辑,执行dep.notify()

    // src/core/observer/watcher.js
    class Dep {
     notify() {
         //subs是Watcher的实例数组
         const subs = this.subs.slice()
            for(let i=0, l=subs.length; i<l; i++){
             subs[i].update()
            }
        }
    }
    

    遍历subs里每一个Watcher实例,然后调用实例的update方法,下面我们来看看update是怎么去更新的:

    class Watcher {
     update() {
         ...
         //各种情况判断之后
            else{
             queueWatcher(this)
            }
        }
    }
    

    update执行后又走到了queueWatcher,那就继续去看看queueWatcher干啥了(希望不要继续套娃了:

    //queueWatcher 定义在 src/core/observer/scheduler.js
    const queue: Array<Watcher> = []
    let has: { [key: number]: ?true } = {}
    let waiting = false
    let flushing = false
    let index = 0
    
    export function queueWatcher(watcher: Watcher) {
     const id = watcher.id
        //根据id是否重复做优化
        if(has[id] == null){
         has[id] = true
            if(!flushing){
             queue.push(watcher)
            }else{
             let i=queue.length - 1
                while(i > index && queue[i].id > watcher.id){
                 i--
                }
                queue.splice(i + 1, 0, watcher)
            }
           
         if(!waiting){
          waiting = true
             //flushSchedulerQueue函数: Flush both queues and run the watchers
             nextTick(flushSchedulerQueue)
         }
        }
    }
    

    这里queue在pushwatcher时是根据idflushing做了一些优化的,并不会每次数据改变都触发watcher的回调,而是把这些watcher先添加到⼀个队列⾥,然后在nextTick后执⾏flushSchedulerQueue

    flushSchedulerQueue函数是保存更新事件的queue的一些加工,让更新可以满足Vue更新的生命周期。

    这里也解释了为什么for循环不能导致页面更新,因为for是主线程的代码,在一开始执行数据改变就会将它push到queue里,等到for里的代码执行完毕后i的值已经变化为100时,这时vue才走到nextTick(flushSchedulerQueue)这一步。

    nextTick源码

    接着打开vue2.x的源码,目录core/util/next-tick.js,代码量很小,加上注释才110行,是比较好理解的。

    const callbacks = []
    let pending = false
    
    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
      callbacks.push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      if (!pending) {
        pending = true
        timerFunc()
      }
    

    首先将传入的回调函数cb(上节的flushSchedulerQueue)压入callbacks数组,最后通过timerFunc函数一次性解决。

    let timerFunc
    
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      const p = Promise.resolve()
      timerFunc = () => {
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
        }
      isUsingMicroTask = true
    } else if (!isIE && typeof MutationObserver !== 'undefined' && (
      isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) {
      let counter = 1
      const observer = new MutationObserver(flushCallbacks)
      const textNode = document.createTextNode(String(counter))
      observer.observe(textNode, {
        characterData: true
      })
      timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter)
      }
      isUsingMicroTask = true
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else {
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    

    timerFunc下面一大片if else是在判断不同的设备和不同情况下选用哪种特性去实现异步任务:优先检测是否原生⽀持Promise,不⽀持的话再去检测是否⽀持MutationObserver,如果都不行就只能尝试宏任务实现,首先是setImmediate,这是⼀个⾼版本 IE 和 Edge 才⽀持的特性,如果都不⽀持的话最后就会降级为 setTimeout 0。

    这⾥使⽤callbacks⽽不是直接在nextTick中执⾏回调函数的原因是保证在同⼀个 tick 内多次执⾏nextTick,不会开启多个异步任务,⽽把这些异步任务都压成⼀个同步任务,在下⼀个 tick 执⾏完毕。

    nextTick使用

    nextTick不仅是vue的源码文件,更是vue的一个全局API。下面来看看怎么使用吧。

    当设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环tick中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用数据驱动的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。

    官网用例:

    <div id="example">{{message}}</div>
    
    var vm = new Vue({
      el: '#example',
      data: {
        message: '123'
      }
    })
    vm.message = 'new message' // 更改数据
    
    vm.$el.textContent === 'new message' // false
    Vue.nextTick(function () {
      vm.$el.textContent === 'new message' // true
    })
    

    并且因为$nextTick() 返回一个 Promise 对象,所以也可以使用async/await 语法去处理事件,非常方便。

    相关文章

    1. 分享8个非常实用的Vue自定义指令

    2. Vue这些修饰符帮我节省20%的开发时间

    3. Vue路由权限控制分析

    最后

    转发文章并关注公众号:前端开发博客,回复 1024,领取前端进阶资料

    1. 回复「电子书」领取27本精选电子书

    2. 回复「加群」加入前端大神交流群,一起学习进步

    3. 回复「Vue」获取 Vue 精选文章

    分享和在看就是最大的支持❤️

    展开全文
  • 异步更新缓存的逻辑

    2021-03-22 14:45:09
    消息队列 为完成数据异步更新到缓存,可以采用消息队列方式(主备AMQ)来管理异步任务。 异步更新缓存的核心逻辑是,如何判断缓存过期。上图中引入了一个Router。 举个例子:运营会设置细化一个航班段的缓存有效期...
  • Android项目动态ListView,支持异步更新列表,异步更新图片.rar

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 406,569
精华内容 162,627
关键字:

异步更新