通过几个问题深入浅出Vue

栏目: JavaScript · 发布时间: 5年前

内容简介:通常,Vue给我们的印象是“小巧易用”,凭借其简洁明了的模板开发方式,以及强大的指令系统,我们可以轻轻松松几行代码搞定一个数据双向绑定的页面。但是,这背后Vue帮我们做了多少工作,我们是知之甚少的。Vue就像一个黑盒子,我们输入一些数据,它给我们输出一个渲染好的页面。对于开发,这很方便,我们不需要关心何时去触发更新,因为Vue已经帮我们做了。但是,在面对一些棘手问题时,我们需要去分析数据是何时变化的、被谁更改的、为什么会触发更新、为什么又不能触发更新,等等问题,这个时候就很难,因为这些逻辑都隐藏在黑盒子内

通常,Vue给我们的印象是“小巧易用”,凭借其简洁明了的模板开发方式,以及强大的指令系统,我们可以轻轻松松几行代码搞定一个数据双向绑定的页面。但是,这背后Vue帮我们做了多少工作,我们是知之甚少的。

Vue就像一个黑盒子,我们输入一些数据,它给我们输出一个渲染好的页面。对于开发,这很方便,我们不需要关心何时去触发更新,因为Vue已经帮我们做了。但是,在面对一些棘手问题时,我们需要去分析数据是何时变化的、被谁更改的、为什么会触发更新、为什么又不能触发更新,等等问题,这个时候就很难,因为这些逻辑都隐藏在黑盒子内部,我们无法观察,更无法控制。

所以说,想要用好Vue其实还挺难的。

面对这些问题,我们往往会基于个人的经验,个人的理解去分析问题,会花费大量时间去debug,这很低效。不如我们先抽点时间,了解一下Vue的实现细节吧。

:key:问题分类

其实按照Vue的开发方式,一般都会有如下流程:

  1. 先初始化一个Vue实例,然后传入各种配置信息
  2. 尝试修改一些数据
  3. 期待视图更新

所以,我们遇到的问题可以归为以下三类:

  1. Vue的初始化流程是怎样的
  2. 数据的更改会不会触发视图的更新
  3. 数据的更改何时会触发视图的更新

:bulb:问题

Q1. 我对Vue的配置信息不理解(第一类问题)

通常我们的一个Vue实例是这样的:

const options = {
  props: ...,
  data: ...,
  computed: ...,
  watch: ...,
  methods: ...,
  created: ...,
  mounted: ...
}
new Vue(options).$mount('#app')
复制代码

也许,我们很清楚每个属性的含义,但是我们却不知道这些属性是如何被调用的,是以怎样的顺序来初始化的。通过这种”无顺序“配置对象的方式来构建Vue实例,我们先天性的丢失了一些重要的“信息”,那就是代码的执行顺序,这会带来一系列理解上的问题。

其实,通过文档里的生命周期图,或者看源码,我们就会知道,在 new Vue(opions) 的这个过程中,即Vue这个类的构造函数中,Vue会依次”同步“地把 props,methods,data,computed,watch 取出来,然后分别初始化,之后再执行我们的created方法。最后, 在我们执行 $mount('#app) 之后,再执行我们的mounted方法。

知道了这个顺序,就能解决很多疑惑比如:

  1. 在watch, created, mounted里面第一次用计算属性的时候,计算属性已经初始化了吗?
  2. data属性能否使用计算属性来初始化
  3. 在created里修改watch监听的属性,会不会触发watch的执行?如果加了immediate又会执行几次呢?
  4. ...等等自己不确定的问题

Q2. 我修改数据为什么没有生效(第二类问题)

我们或多或少都会遇到,明明自己改了数据,可是视图就是不更新的问题,举个例子:

<template>
  <div>
    <p>{{lib}}</p>
    <p>{{detail.version}}</p>
    <p>{{detail.type}}</p>
  </div>
</template>
复制代码
export default {
  data() {
    return {
      lib: 'vue',
      detail: {
        version: '2.5.1',
        // type: 'fe'
      }
    }
  },
  mounted() {
    this.detail.type = 'fe'
  }
}
复制代码
// 输出
vue
2.5.1
复制代码

问题很容易看出,是因为我们没有事先在data里声明好type这个属性,所以在data的初始化过程中,并没有observe(即把属性转变为getter和setter)这个type属性,所以我们的修改是不会触发setter的,也就不会引起视图更新。

接下来,我们更新一下代码:

// 把mounted改成created
created() {
  this.detail.type = 'fe'
}
复制代码

我们神奇的发现,竟然显示出来了,这跟我们刚才的结论相悖呀。其实,我们的结论没错,之所以这次能显示出来,纯粹是巧合。我们知道created是在data初始化之后调用的,同时又是在mounted之前调用的,所以data在mounted之前就被赋予了一个未被observe的属性type,然后在$mount的时候,顺带显示在页面上了。也就是说,这次的显示,并非setter的触发,而是本来data就已经有了type属性罢了。

我们再来改一下:

created() {
  this.detail.type = 'fe'
},
mounted() {
  this.detail.type = 'be'
}
复制代码

我们会发现,依旧显示的是fe,而不是be,这验证了上一个结论。

一句话:只有被observe的数据改变后才会触发视图的更新

Q3:Vue是如何使用事件循环的(第三类问题)

这个问题比较抽象,具体一点举几个例子:

  • 怎么准确的判断watch的handler什么时候执行、执行几次
  • 我更改了数据,何时才会触发视图更新
  • $nextTick和setTimeou区别

可以说, Vue的本质其实就是一套精心设计的事件循环系统 ,要弄懂Vue必须弄懂两件事:

  1. 事件循环本身
  2. Vue的事件循环系统

事件循环需要理解到task和microTask的层次,而Vue的事件循环系统需要读透文档,理解作者的思想,另外多看源码。下面我们举一些例子

Q3-1:watch的handler什么时候执行

export default {
  data() {
    return {
      lib: 'vue',
      lock: true
    }
  },
  watch: {
    lib(val, old) {
      if(this.lock) {
        console.log(`lib changed from ${old} to ${val}`)        
      }
    }
  },
  created() {
    this.lock = false;
    this.lib = 'react'
    this.lock = true;
  }
}
复制代码
// 输出
lib changed from vue to react
复制代码

按照我们常规的理解,当我们执行 this.lib = 'react 的时候,理应当触发watch,而此时lock其实是false,不应该输出。这里有一个『陷阱』,就是我们错误的以为Vue的内部的执行机制是同步的,而事实上,Vue会充分利用事件循环做一些异步的事情,比如这里的handler执行机制。

根据Q1,并结合一定的源码阅读,我们知道本次示例的初始化顺序为:

  1. 先初始化data,取出lib、lock两个属性,分别observe
  2. 初始化watch,取出lib属性的key和value(即handler),并用他们初始化一个watcher,从而形成一个watcher对lib属性的监听,并等待一个『合适的时机』去执行我们给定的handler
  3. 执行created声明周期函数,此时数据的初始化工作已经完成,接下来先执行 this.lock = false ,这不会影响什么。再执行 this.lib = 'react' , 此时触发了lib的setter方法,接着Vue会找出所以正在监听lib属性的watcher,并执行其update方法
update () {
  queueWatcher(this)
}
复制代码

我们发现,watcher并没有立即执行handler,而是发起了一个queueWatcher:

flushing = false
waiting = true
export function queueWatcher (watcher) {
  if (!flushing) {
    queue.push(watcher)
  }
  // queue the flush
  if (!waiting) {
    waiting = true
    // flushSchedulerQueueh会依次把queue中的watcher拿出来执行
    nextTick(flushSchedulerQueue) 
  }
}
复制代码

已知flushing和wating默认值都是false,所以『第一次触发watch』代码会像这样执行

queue.push(watcher)
nextTick(flushSchedulerQueue)
复制代码

可以看到,我们的handler会在nextTick时执行(关于nextTick我们后面会讲,在这里可以暂时理解成setTimeout),而等到下一次事件循环, this.lock = true 已经执行,所以我们console了出来。

总结:我们现在我们对Vue的事件循环机制有了一个认知,即Vue中数据的变化所引起的响应,是依托事件循环就机制来完成的。

Q3-2: 深入Vue的事件循环细节

仅知道数据的响应是依托于事件循环还不够,因为我们的代码会越写越复杂,常常会有多个异步任务,此时我们需要准确的知道,我们的watch何时触发,我们的视图何时更新。因此,我们需要更加细致的去研究。

Q3-2-1:直接修改值

export default {
  data() {
    return {
      lib: 'vue'
    }
  },
  watch: {
    lib(val, old) {
      console.log(`lib changed from ${old} to ${val}`)        
    }
  },
  created() {
    this.lib = 'react'
    this.lib = 'angular'
  }
}
复制代码
// 输出
lib changed from vue to angular
复制代码

问题有两个:

  1. 为什么watch只执行一次
  2. 为什么是angular

虽然这个问题比较蠢,但是为了帮助我们理解后面的例子,我们还是得深入的去研究一下。

根据Q3-1,watch中的lib会创建一个watcher,并监听lib属性的变化。当我们第一次 this.lib = 'react' , 此时会触发lib的setter,并找到正在监听的watcher,依次执行watcher的update方法,update方法会调用queueWatcher方法,现在我们看一下queueWatcher更完整一点的代码:

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // has维持着所有watcher的id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      ...
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}
复制代码

可以看到,这里有一个 if (has[id] == null) 的判断,要知道,我们watch只有一个lib属性,所以只会初始化一个watcher,所以当第二次执行 this.lib = 'angular' 的时候,queueWatcher其实什么都没干。

所以结果是,虽然有两次lib的变化,但是watcher会在下一次事件循环,只执行一次handler。

Q3-2-2 在setTimeout里修改值

export default {
  data() {
    return {
      lib: 'vue'
    }
  },
  watch: {
    lib(val, old) {
      console.log(`lib changed from ${old} to ${val}`)        
    }
  },
  created() {
    setTimeout(() => {
      this.lib = 'react'
    }, 0)
    setTimeout(() => {
      this.lib = 'angular'
    }, 0)
  }
}
复制代码
// 输出
lib changed from vue to react
lib changed from react to angular
复制代码

为什么现在又输出两次了?再看一遍queueWatcher方法:

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // has维持着所有watcher的id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      ...
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}
复制代码

还需要看flushSchedulerQueue方法

/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  flushing = true
  let watcher, id
  ...
  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    ...
  }
  ...
}
复制代码

点我查看预备知识(事件循环中的task和microTask)

当Vue执行created方法,会执行两个setTimeout,而setTimeout又是task,所以我们写的两个setTimeout函数,会分别在两次事件循环中执行,而不是一次,如下:

  1. 第一次事件循环(script任务, 也是task):vue初始化,created方法发起两个异步任务
  2. 第二次事件循环(task):开始执行第一个setTimeout的callback
  3. 第三次事件循环(task):开始执行第二个setTimeout的callback

在第二次事件循环中,我们修改了lib,同时出发了相应的watcher,最终执行queueWatcher方法,于是当前的watcher被push到queue队列,待到nextTick执行,而nextTick是microTask,于是我们上面的过程变成了:

nextTick(flushSchedulerQueue)

注意:在js的一次事件循环中,先执行所有同步代码,之后,会从macroTask队列里取出1个macroTask执行。然后,再取出所有microTask队列里的microTask,并依次执行。这整个过程结束后,便会开启下一次事件循环。

接下来到了第三次事件循环,我们再次修改lib,同样的过程,因为上一步已经清空了has[id],所以本次lib 的更新其实跟上一次一模一样,所以过程变成了:

nextTick(flushSchedulerQueue)
nextTick(flushSchedulerQueue)

所以,会打印两次。

Q3-2-3 在nextTick里修改值

export default {
  data() {
    return {
      lib: 'vue'
    }
  },
  watch: {
    lib(val, old) {
      console.log(`lib changed from ${old} to ${val}`)        
    }
  },
  created() {
    this.$nextTick(() => {
      this.lib = 'react'
    })
    this.$nextTick(() => {
      this.lib = 'angular'
    })
  }
}
复制代码

问题是,为什么watch只执行了一次。

  1. 第一次事件循环(script任务):vue初始化,created方法发起两个microTask异步任务
  2. 第二次事件循环(task):没有macroTask
  3. 第二次事件循环(microTask):现在microTask任务队列是这样的 [callback1, callback2] , 从microTask队列取出第一个callback1,执行,触发lib更新,从而执行queueWatcher,在里面又触发了一个nextTick(microTask)异步任务,并push到microTask任务队列,队列变成这样 [callback2, callback3]
  4. 第二次事件循环(microTask):从micoTask队列取出callback2,执行,触发lib更新,从而执行queueWatcher,而此时has[id]已经有值(因为callback3还没执行,因此has[id]还没被清空),所以直接略过
  5. 第二次事件循环(microTask):从microTask队列取出callback3,执行

可以看到,这就是只打印一次的原因了。

总结

其实Vue的设计思想就是:事件循环+双向绑定,只要我们搞明白这两点,我们就可以真正的掌握Vue,写出稳定、可预测的代码,轻松的解决使用中遇到的各种问题。

有了设计思想还不够,还需要 工具 去实现这种思想,那就是compiler和vdom干的事了,还有很多东西要学呢:muscle|type_1_2:。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

快速转行做产品经理

快速转行做产品经理

李三科 / 华中科技大学出版社 / 2018-6-1 / 39.90

互联网已经进入以产品为中心的时代,不懂技术一样做高薪产品经理。本书将满足你转行、就业、加薪的愿望。 . 作者李三科,互联网资深产品经理。2011年离开传统销售行业进入互联网行业工作,从对产品经理的工作一无所知,到成长为一名年薪几十万的资深产品经理,他对产品经理职业有着深刻的理解,也积累了丰富的学习、工作经验。本书以作者亲身经历为线索,讲解学习产品经理相关知识和工作方法的经验,同时介绍求......一起来看看 《快速转行做产品经理》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具