Vue高级个性(一)

Vue 的优缺点

长处

  • 创立单页面利用的轻量级Web利用框架
  • 简略易用
  • 双向数据绑定
  • 组件化的思维
  • 虚构DOM
  • 数据驱动视图

毛病

  • 不反对IE8
  • SPA 的了解

    SPA是Single-Page-Application的缩写,翻译过去就是单页利用。在WEB页面初始化时一起加载Html、Javascript、Css。一旦页面加载实现,SPA不会因为用户操作而进行页面从新加载或跳转,取而代之的是利用路由机制实现Html内容的变换。

长处

  • 良好的用户体验,内容更改无需重载页面。
  • 基于下面一点,SPA绝对服务端压力更小。
  • 前后端职责拆散,架构清晰。

毛病

  • 因为单页WEB利用,需在加载渲染页面时申请JavaScript、Css文件,所以耗时更多。
  • 因为前端渲染,搜索引擎不会解析JS,只能抓取首页未渲染的模板,不利于SEO。
  • 因为单页利用需在一个页面显示所有的内容,默认不反对浏览器的后退后退。

毛病3,想必有人和我有同样的疑难。

通过材料查阅,其实是前端路由机制解决了单页利用无奈后退后退的问题。Hash模式中Hash变动会被浏览器记录(onhashchange事件),History模式利用 H5 新增的pushStatereplaceState办法可扭转浏览器历史记录栈。

new Vue(options) 都做了些什么

如下 Vue 构造函数所示,次要执行了 this._init(options)办法,该办法在initMixin函数中注册。

import { initMixin } from './init'import { stateMixin } from './state'import { renderMixin } from './render'import { eventsMixin } from './events'import { lifecycleMixin } from './lifecycle'import { warn } from '../util/index'function Vue (options) {  if (process.env.NODE_ENV !== 'production' &&    !(this instanceof Vue)  ) {    warn('Vue is a constructor and should be called with the `new` keyword')  }  // Vue.prototype._init 办法  this._init(options)}// _init 办法在 initMixin 注册initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue)export default Vue

查看initMixin办法的实现,其余函数具体实现可自行查看,这里就不贴出了。

let uid = 0export function initMixin() {  Vue.prototype._init = function(options) {    const vm = this    vm._uid = uid++    vm._isVue = true       // 解决组件配置项    if (options && options._isComponent) {       /**       * 如果是子组件,走以后 if 分支       * 函数作用是性能优化:将原型链上的办法都放到vm.$options中,缩小原型链上的拜访       */         initInternalComponent(vm, options)    } else {      /**       * 如果是根组件,走以后 else 分支       * 合并 Vue 的全局配置到根组件中,如 Vue.component 注册的全局组件合并到根组件的 components 的选项中       * 子组件的选项合并产生在两个中央       * 1. Vue.component 办法注册的全局组件在注册时做了选项合并       * 2. { component: {xx} } 办法注册的部分组件在执行编译器生成的 render 函数时做了选项合并       */        vm.$options = mergeOptions(        resolveConstructorOptions(vm.constructor),        options || {},        vm      )    }      if (process.env.NODE_ENV !== 'production') {      initProxy(vm)    } else {      vm._renderProxy = vm    }    vm._self = vm    /**    * 初始化组件实例关系属性,如:$parent $root $children $refs    */    initLifecycle(vm)    /**    * 初始化自定义事件    * <component @click="handleClick"></component>    * 组件上注册的事件,监听者不是父组件,而是子组件自身    */    initEvents(vm)    /**    * 解析组件插槽信息,失去vm.$slot,解决渲染函数,失去 vm.$createElement 办法,即 h 函数。    */    initRender(vm)    /**    * 执行 beforeCreate 生命周期函数    */    callHook(vm, 'beforeCreate')    /**    * 解析 inject 配置项,失去 result[key] = val 的配置对象,做响应式解决且代理到 vm 实力上    */    initInjections(vm)     /**    * 响应式解决外围,解决 props、methods、data、computed、watch    */    initState(vm)    /**    * 解析 provide 对象,并挂载到 vm 实例上    */    initProvide(vm)     /**    * 执行 created 生命周期函数    */    callHook(vm, 'created')    // 如果 el 选项,主动执行$mount    if (vm.$options.el) {      vm.$mount(vm.$options.el)    }  }}

MVVM 的了解

MVVM是Model-View-ViewModel的缩写。Model 代表数据层,可定义批改数据、编写业务逻辑。View 代表视图层,负责将数据渲染成页面。ViewModel 负责监听数据层数据变动,管制视图层行为交互,简略讲,就是同步数据层和视图层的对象。ViewModel 通过双向绑定把 View 和 Model 层连接起来,且同步工作无需人为干预,使开发人员只关注业务逻辑,无需频繁操作DOM,不需关注数据状态的同步问题。

如何实现 v-model

v-model指令用于实现input、select等表单元素的双向绑定,是个语法糖。

原生 input 元素若是text/textarea类型,应用 value 属性和 input 事件。
原生 input 元素若是radio/checkbox类型,应用 checked属性和 change 事件。
原生 select 元素,应用 value 属性和 change 事件。

input 元素上应用 v-model 等价于

<input :value="message" @input="message = $event.target.value" />

实现自定义组件的 v-model

自定义组件的v-model应用prop值为value和input事件。若是radio/checkbox类型,须要应用model来解决原生 DOM 应用的是 checked 属性 和 change 事件,如下所示。

// 父组件<template>  <base-checkbox v-model="baseCheck" /></template>复制代码// 子组件<template>  <input type="checkbox" :checked="checked" @change="$emit('change', $event.target.checked)" /></template><script>export default {  model: {    prop: 'checked',    event: 'change'  },  prop: {    checked: Boolean  }}</script>

如何了解 Vue 单向数据流

咱们常常说 Vue 的双向绑定,其实是在单向绑定的根底上给元素增加 input/change 事件,来动静批改视图。Vue 组件间传递数据依然是单项的,即父组件传递到子组件。子组件外部能够定义依赖 props 中的值,但无权批改父组件传递的数据,这样做避免子组件意外变更父组件的状态,导致利用数据流向难以了解。

如果在子组件外部间接更改prop,会遇到正告解决。

2 种定义依赖 props 中的值

//通过 data 定义属性并将 prop 作为初始值。<script>export default {  props: ['initialNumber'],  data() {    return {      number: this.initailNumber    }  }}</script>

用 computed 计算属性去定义依赖 prop 的值。若页面会更改以后值,得分 get 和 set 办法。

<script>export default {  props: ['size'],  computed: {    normalizedSize() {      return this.size.trim().toLowerCase()    }  }}</sciprt>

Vue 响应式原理

外围源码地位:vue/src/core/observer/index.js

响应式原理3个步骤:数据劫持、依赖收集、派发更新。

数据分为两类:对象、数组。

对象

遍历对象,通过Object.defineProperty为每个属性增加 getter 和 setter,进行数据劫持。getter 函数用于在数据读取时进行依赖收集,在对应的 dep 中存储所有的 watcher;setter 则是数据更新后告诉所有的 watcher 进行更新。

外围源码

function defineReactive(obj, key, val, shallow) {  // 实例化一个 dep, 一个 key 对应一个 dep  const dep = new Dep()   // 获取属性描述符  const getter = property && property.get  const setter = property && property.set  if ((!getter || setter) && arguments.length === 2) {    val = obj[key]  }  // 通过递归的形式解决 val 为对象的状况,即解决嵌套对象  let childOb = !shallow && observe(val)    Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    // 拦挡obj.key,进行依赖收集    get: function reactiveGetter () {      const value = getter ? getter.call(obj) : val      // Dep.target 是以后组件渲染的 watcher      if (Dep.target) {        // 将 dep 增加到 watcher 中        dep.depend()        if (childOb) {          // 嵌套对象依赖收集          childOb.dep.depend()          // 响应式解决 value 值为数组的状况          if (Array.isArray(value)) {            dependArray(value)          }        }      }      return value    },    set: function reactiveSetter (newVal) {      // 获取旧值      const value = getter ? getter.call(obj) : val      // 判断新旧值是否统一      if (newVal === value || (newVal !== newVal && value !== value)) {        return      }      if (process.env.NODE_ENV !== 'production' && customSetter) {        customSetter()      }      if (getter && !setter) return      // 如果是新值,用新值替换旧值      if (setter) {        setter.call(obj, newVal)      } else {        val = newVal      }      // 新值做响应式解决      childOb = !shallow && observe(newVal)      // 当响应式数据更新,依赖告诉更新      dep.notify()    }  })}

数组

用数组加强的形式,笼罩原属性上默认的数组办法,保障在新增或删除数据时,通过 dep 告诉所有的 watcher 进行更新。

外围源码

const arrayProto = Array.prototype// 基于数组原型对象创立一个新的对象export const arrayMethods = Object.create(arrayProto)const methodsToPatch = [  'push',  'pop',  'shift',  'unshift',  'splice',  'sort',  'reverse']methodsToPatch.forEach(function (method) {  const original = arrayProto[method]  // 别离在 arrayMethods 对象上定义7个办法  def(arrayMethods, method, function mutator (...args) {    // 先执行原生的办法    const result = original.apply(this, args)    const ob = this.__ob__    let inserted    switch (method) {      case 'push':      case 'unshift':        inserted = args        break      case 'splice':        inserted = args.slice(2)        break    }    // 针对新增元素进行响应式解决    if (inserted) ob.observeArray(inserted)    // 数据无论是新增还是删除都进行派发更新    ob.dep.notify()    return result  })})

手写观察者模式

当对象间存在一对多的关系,应用观察者模式。比方:当一个对象被批改,会主动告诉依赖它的对象。

let uid = 0class Dep {  constructor() {    this.id = uid++    // 存储所有的 watcher    this.subs = []  }  addSub(sub) {    this.subs.push(sub)  }  removeSub(sub) {    if(this.subs.length) {      const index = this.subs.indexOf(sub)      if(index > -1) return this.subs.splice(index, 1)    }  }  notify() {    this.subs.forEach(sub => {      sub.update()    })  }}class Watcher {  constructor(name) {    this.name = name  }  update() {    console.log('更新')  }}

手写公布订阅模式

与观察者模式类似,区别在于发布者和订阅者是解耦的,由两头的调度核心去与发布者和订阅者通信。

Vue响应式原理集体更偏向于公布订阅模式。其中 Observer 是发布者,Watcher 是订阅者,Dep 是调度核心。

vue中数据绑定原理的设计模式到底观察者还是公布订阅?[4],知乎有相干争执,感兴趣的能够看下。

class EventEmitter {  constructor() {    this.events = {}  }  on(type, cb) {    if(!this.events[type]) this.events[type] = []    this.events[type].push(cb)  }  emit(type, ...args) {    if(this.events[type]) {      this.events[type].forEach(cb => {        cb(...args)      })    }  }  off(type, cb) {    if(this.events[type]) {      const index = this.events[type].indexOf(cb)      if(index > -1) this.events[type].splice(index, 1)    }  }}

对于 Vue.observable 的理解

Vue.observable 可使对象可响应。返回的对象可间接用于渲染函数和计算属性内,并且在产生变更时触发相应的更新。也能够作为最小化的跨组件状态存储器。

Vue 2.x 中传入的对象和返回的对象是同一个对象。
Vue 3.x 则不是一个对象,源对象不具备响应式性能。

实用的场景:在我的项目中没有大量的非父子组件通信时,能够应用 Vue.observable 去代替 eventBus和vuex计划。

用法如下

// store.jsimport Vue from 'vue'export const state = Vue.observable({  count: 1})export const mutations = {  setCount(count) {    state.count = count  }} // vue 文件<template>  <div>{{ count }}</div></template><script>import { state, mutation } from './store.js'export default {  computed: {    count() {      return state.count    }  }}</script>

原理局部和响应式原理解决组件 data 是同一个函数,实例化一个 Observe,对数据劫持。

组件中的 data 为什么是个函数

对象在栈中存储的都是地址,函数的作用就是属性私有化,保障组件批改本身属性时不会影响其余复用组件。

Vue 生命周期

  • beforeCreate vue实例初始化后,数据观测(data observer)和事件配置之前。data、computed、watch、methods都无法访问。
  • created vue实例创立实现后立刻调用 ,可拜访 data、computed、watch、methods。未挂载 DOM,不能拜访 、ref。
  • beforeMount 在 DOM 挂载开始之前调用。
  • mounted vue实例被挂载到 DOM。
  • beforeUpdate 数据更新之前调用,产生在虚构 DOM 打补丁之前。
  • updated 数据更新之后调用。
  • beforeDestroy 实例销毁前调用。
  • destroyed 实例销毁后调用 。

调用异步申请可在created、beforeMount、mounted生命周期中调用,因为相干数据都已创立。最好的抉择是在created中调用。

获取DOM在mounted中获取,获取可用$ref办法,这点毋庸置疑。

Vue 父组件和子组件生命周期执行程序

加载渲染过程

父先创立,能力有子;子创立实现,父才残缺。

程序:父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

子组件更新过程

1.子组件更新 影响到 父组件的状况。
程序:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

2.子组件更新 不影响到 父组件的状况。
程序:子 beforeUpdate -> 子 updated

父组件更新过程

1.父组件更新 影响到 子组件的状况。
程序:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

2.父组件更新 不影响到 子组件的状况。
程序:父 beforeUpdate -> 父 updated

销毁过程

程序:父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

父组件如何监听子组件生命周期的钩子函数

两种形式都以 mounted 为例子。

$emit实现

// 父组件<template>  <div class="parent">    <Child @mounted="doSomething"/>  </div></template><script>export default {    methods: {    doSomething() {      console.log('父组件监听到子组件 mounted 钩子函数')    }  }}</script>//子组件<template>  <div class="child">  </div></template><script>export default {  mounted() {    console.log('触发mounted事件...')    this.$emit("mounted")  }}</script>

@hook实现

// 父组件<template>  <div class="parent">    <Child @hook:mounted="doSomething"/>  </div></template><script>export default {    methods: {    doSomething() {      console.log('父组件监听到子组件 mounted 钩子函数')    }  }}</script>//子组件<template>  <div class="child">  </div></template><script>export default {  mounted() {    console.log('触发mounted事件...')  }}</script>

Vue 组件间通信形式

父子组件通信

  • props 与 $emit
  • 与children

隔代组件通信

  • 与listeners
  • provide 和 inject

父子、兄弟、隔代组件通信

  • EventBus
  • Vuex

v-on 监听多个办法

<button v-on="{mouseenter: onEnter, mouseleave: onLeave}">鼠标进来1</button>

罕用的修饰符

表单修饰符

  • lazy: 失去焦点后同步信息
  • trim: 主动过滤首尾空格
  • number: 输出值转为数值类型

事件修饰符

  • stop:阻止冒泡
  • prevent:阻止默认行为
  • self:仅绑定元素本身触发
  • once:只触发一次

鼠标按钮修饰符

  • left:鼠标左键
  • right:鼠标右键
  • middle:鼠标两头键

class 与 style 如何动静绑定

class 和 style 能够通过对象语法和数组语法进行动静绑定

对象写法

<template>  <div :class="{ active: isActive }"></div>  <div :style="{ fontSize: fontSize }"></template><script>export default {  data() {    return {      isActive: true,      fontSize: 30    }  }}</script>

数组写法

<template>  <div :class="[activeClass]"></div>  <div :style="[styleFontSize]"></template><script>export default {  data() {    return {      activeClass: 'active',      styleFontSize: {        fontSize: '12px'      }    }  }}</script>

v-show 和 v-if 区别

共同点:管制元素显示和暗藏。

不同点:

  • v-show 管制的是元素的CSS(display);v-if 是管制元素自身的增加或删除。
  • v-show 由 false 变为 true 的时候不会触发组件的生命周期。v-if 由 false 变为 true 则会触发组件的beforeCreate、create、beforeMount、mounted钩子,由 true 变为 false 会触发组件的beforeDestory、destoryed办法。
  • v-if 比 v-show有更高的性能耗费。

为什么 v-if 不能和 v-for 一起应用

性能节约,每次渲染都要先循环再进行条件判断,思考用计算属性代替。

Vue2.x中v-for比v-if更高的优先级。

Vue3.x中v-if 比 v-for 更高的优先级。

computed 和 watch 的区别和使用的场景

computed 和 watch 实质都是通过实例化 Watcher 实现,最大区别就是实用场景不同。

computed

计算属性,依赖其余属性值,且值具备缓存的个性。只有它依赖的属性值产生扭转,下一次获取的值才会从新计算。

实用于数值计算,并且依赖于其余属性时。因为能够利用缓存个性,防止每次获取值,都须要从新计算。

watch

察看属性,监听属性值变动。每当属性值发生变化,都会执行相应的回调。

实用于数据变动时执行异步或开销比拟大的操作。

slot 插槽

slot 插槽,能够了解为slot在组件模板中提前占据了地位。当复用组件时,应用相干的slot标签时,标签里的内容就会主动替换组件模板中对应slot标签的地位,作为承载散发内容的进口。

次要作用是复用和扩大组件,做一些定制化组件的解决。

插槽次要有3种

默认插槽

// 子组件<template>  <slot>    <div>默认插槽备选内容</div>  </slot></template>// 父组件<template>  <Child>    <div>替换默认插槽内容</div>  </Child></template>

具名插槽
slot 标签没有name属性,则为默认插槽。具备name属性,则为具名插槽

// 子组件<template>  <slot>默认插槽的地位</slot>  <slot name="content">插槽content内容</slot></template>// 父组件<template>   <Child>     <template v-slot:default>       默认...     </template>     <template v-slot:content>       内容...     </template>   </Child></template>

作用域插槽
子组件在作用域上绑定的属性来将组件的信息传给父组件应用,这些属性会被挂在父组件承受的对象上。

// 子组件<template>  <slot name="footer" childProps="子组件">    作用域插槽内容  </slot></template>// 父组件<template>  <Child v-slot="slotProps">    {{ slotProps.childProps }}  </Child></template>

Vue.$delete 和 delete 的区别

Vue.$delete 是间接删除了元素,扭转了数组的长度;delete 是将被删除的元素变成内 undefined ,其余元素键值不变。

Vue.$set 如何解决对象新增属性不能响应的问题

Vue.$set的呈现是因为Object.defineProperty的局限性:无奈检测对象属性的新增或删除。

源码地位:vue/src/core/observer/index.js

export function set(target, key, val) {  // 数组  if(Array.isArray(target) && isValidArrayIndex(key)) {    // 批改数组长度,防止索引大于数组长度导致splice谬误    target.length = Math.max(target.length, key)    // 利用数组splice触发响应    target.splice(key, 1, val)    return val  }  // key 曾经存在,间接批改属性值  if(key in target && !(key in Object.prototype)) {    target[key] = val    return val  }  const ob = target.__ob__  // target 不是响应式数据,间接赋值  if(!ob) {    target[key] = val    return val  }  // 响应式解决属性  defineReactive(ob.value, key, val)  // 派发更新  ob.dep.notify()  return val}

实现原理:

  • 若是数组,间接应用数组的 splice 办法触发响应式。
  • 若是对象,判断属性是否存在,对象是否是响应式。
  • 以上都不满足,最初通过 defineReactive 对属性进行响应式解决。
  • Vue 异步更新机制

    Vue 异步更新机制外围是利用浏览器的异步工作队列实现的。

当响应式数据更新后,会触发 dep.notify 告诉所有的 watcher 执行 update 办法。

dep 类的 notify 办法

notify() {  // 获取所有的 watcher  const subs = this.subs.slice()  // 遍历 dep 中存储的 watcher,执行 watcher.update  for(let i = 0; i < subs.length; i++) {    subs[i].update()  }}

watcher.update 将本身放入全局的 watcher 队列,期待执行。

watcher 类的 update 办法

update() {  if(this.lazy) {    // 懒执行走以后 if 分支,如 computed    // 这里的 标识 次要用于 computed 缓存复用逻辑    this.dirty = true  } else if(this.sync) {    // 同步执行,在 watch 选项参数传 sync 时,走以后分支    // 若为 true ,间接执行 watcher.run(),不塞入异步更新队列    this.run()  } else {    // 失常更新走以后 else 分支    queueWatcher(this)  }}

queueWatcher 办法,发现相熟的 nextTick 办法。看到这能够先跳到nextTick的原理,看明确了再折返。

function queueWatcher(watcher) {  const id = watcher.id  // 依据 watcher id 判断是否在队列中,若在队列中,不反复入队   if (has[id] == null) {    has[id] = true    // 全局 queue 队列未处于刷新状态,watcher 可入队    if (!flushing) {      queue.push(watcher)    // 全局 queue 队列处于刷新状态    // 在枯燥递增序列寻找以后 id 的地位并进行插入操作    } 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      // 同步执行逻辑      if (process.env.NODE_ENV !== 'production' && !config.async) {        flushSchedulerQueue()        return      }      // 将回调函数 flushSchedulerQueue 放入 callbacks 数组      nextTick(flushSchedulerQueue)    }  }}

nextTick 函数最终其实是执行 flushCallbacks 函数,flushCallbacks 函数则是运行 flushSchedulerQueue 回调和我的项目中调用 nextTick 函数传入的回调。

搬运 flushSchedulerQueue 源码看做了些什么

/***  更新 flushing 为 true,示意正在刷新队列,在此期间退出的 watcher 必须有序插入队列,保障枯燥递增*  依照队列的 watcher.id 从小到大排序,保障先创立的先执行*  遍历 watcher 队列,按序执行 watcher.before 和 watcher.run,最初革除缓存的 watcher*/function flushSchedulerQueue () {  currentFlushTimestamp = getNow()  // 标识正在刷新队列  flushing = true  let watcher, id  queue.sort((a, b) => a.id - b.id)  // 未缓存长度是因为可能在执行 watcher 时退出 watcher  for (index = 0; index < queue.length; index++) {    watcher = queue[index]    if (watcher.before) {      watcher.before()    }    id = watcher.id    // 革除缓存的 watcher    has[id] = null    // 触发更新函数,如 updateComponent 或 执行用户的 watch 回调    watcher.run()  }    const activatedQueue = activatedChildren.slice()  const updatedQueue = queue.slice()    // 执行 waiting = flushing = false,标识刷新队列完结,能够向浏览器的工作队列退出下一个 flushCallbacks  resetSchedulerState()   callActivatedHooks(activatedQueue)  callUpdatedHooks(updatedQueue)  if (devtools && config.devtools) {    devtools.emit('flush')  }}

查看下 watcher.run 做了些什么,首先调用了 get 函数,咱们一起看下。

/***  执行实例化 watcher 传递的第二个参数,如 updateComponent*  更新旧值为新值*  执行实例化 watcher 时传递的第三个参数,用户传递的 watcher 回调*/run () {  if (this.active) {    // 调用 get    const value = this.get()    if (      value !== this.value ||      isObject(value) ||      this.deep    ) {      // 更新旧值为新值      const oldValue = this.value      this.value = value      // 若是我的项目传入的 watcher,则执行实例化传递的回调函数。      if (this.user) {        const info = `callback for watcher "${this.expression}"`        invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)      } else {        this.cb.call(this.vm, value, oldValue)      }    }  }}/*** 执行 this.getter,并从新收集依赖。* 从新收集依赖是因为触发更新 setter 中只做了响应式观测,但没有收集依赖的操作。* 所以,在更新页面时,会从新执行一次 render 函数,执行期间会触发读取操作,这时进行依赖收集。*/get () {  // Dep.target = this  pushTarget(this)  let value  const vm = this.vm  try {    // 执行回调函数,如 updateComponent,进入 patch 阶段    value = this.getter.call(vm, vm)  } catch (e) {    if (this.user) {      handleError(e, vm, `getter for watcher "${this.expression}"`)    } else {      throw e    }  } finally {    // watch 参数为 deep 的状况    if (this.deep) {      traverse(value)    }    // 敞开 Dep.target 置空    popTarget()    this.cleanupDeps()  }  return value}

Vue.$nextTick 的原理

nextTick:在下次 DOM 更新循环完结之后执行提早回调。罕用于批改数据后获取更新后的DOM。

源码地位:vue/src/core/util/next-tick.js

import { noop } from 'shared/util'import { handleError } from './error'import { isIE, isIOS, isNative } from './env'// 是否应用微工作标识export let isUsingMicroTask = false// 回调函数队列const callbacks = []// 异步锁let pending = falsefunction flushCallbacks () {  // 示意下一个 flushCallbacks 能够进入浏览器的工作队列了  pending = false  // 避免 nextTick 中蕴含 nextTick时呈现问题,在执行回调函数队列前,提前复制备份,清空回调函数队列  const copies = callbacks.slice(0)  // 清空 callbacks 数组  callbacks.length = 0  for (let i = 0; i < copies.length; i++) {    copies[i]()  }}let timerFunc// 浏览器能力检测// 应用宏工作或微工作的目标是宏工作和微工作必在同步代码完结之后执行,这时能保障是最终渲染好的DOM。// 宏工作消耗工夫是大于微工作,在浏览器反对的状况下,优先应用微工作。// 宏工作中效率也有差距,最低的就是 setTimeoutif (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) ||  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)  }}export function nextTick (cb?: Function, ctx?: Object) {  let _resolve  // 将 nextTick 的回调函数用 try catch 包裹一层,用于异样捕捉  // 将包裹后的函数放到 callback 中  callbacks.push(() => {    if (cb) {      try {        cb.call(ctx)      } catch (e) {        handleError(e, ctx, 'nextTick')      }    } else if (_resolve) {      _resolve(ctx)    }  })  // pengding 为 false, 执行 timerFunc  if (!pending) {    // 关上锁    pending = true    timerFunc()  }  if (!cb && typeof Promise !== 'undefined') {    return new Promise(resolve => {      _resolve = resolve    })  }}

总结:

  • 使用异步锁的概念,保障同一时刻工作队列中只有一个 flushCallbacks。当 pengding 为 false 的时候,示意浏览器工作队列中没有 flushCallbacks 函数;当 pengding 为 true 的时候,示意浏览器工作队列中曾经放入 flushCallbacks;待执行 flushCallback 函数时,pengding 会被再次置为 false,示意下一个 flushCallbacks 可进入工作队列。
  • 环境能力检测,抉择可选中效率最高的(宏工作/微工作)进行包装执行,保障是在同步代码都执行实现后再去执行批改 DOM 等操作。
  • flushCallbacks 先拷贝再清空,为了避免nextTick嵌套nextTick导致循环不完结。