首页 > 编程语言 >Vue源码解读:更新策略

Vue源码解读:更新策略

时间:2024-03-04 10:56:27浏览次数:29  
标签:nextTick Vue const vm watcher 解读 源码 && true

之前介绍过初始化时 Vue 对数据的响应式处理是利用了Object.defifineProperty(),通过定义对象属性 getter 方法拦截对象属性的访问,进行依赖的收集,依赖收集的作用就是在数据变更的时候能通知到相关依赖进行更新。

通知更新

setter

当响应式数据发生变更时,会触发拦截的 setter 函数,先来看看 setter :

// src/core/observer/index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // ...
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // ...
    // 劫持修改操作
    set: function reactiveSetter (newVal) {
      // 旧的 obj[key] 
      const value = getter ? getter.call(obj) : val
      // 如果新旧值一样,则直接 return,无需更新
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // setter 不存在说明该属性是一个只读属性,直接 return
      if (getter && !setter) return
      // 设置新值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 对新值进行观察,让新值也是响应式的
      childOb = !shallow && observe(newVal)
      // 依赖通知更新
      dep.notify()
    }
  })
}

dep.notify()

  // src/core/observer/dep.js
  // 通知更新
  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

遍历 dep 中存储的 watcher,执行 watcher.update()

watcher.update()

// src/core/observer/watcher.js
export default class Watcher {
  // ...
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      // 懒执行时走这里,比如 computed watcher
      // 将 dirty 置为 true,计算属性的求值就会重新计算
      this.dirty = true
    } else if (this.sync) {
      // 同步执行,在使用 vm.$watch 或者 watch 选项时可以传一个 sync 选项,
      // 当为 true 时在数据更新时该 watcher 就不走异步更新队列,直接执行 this.run方法进行更新
      // 这个属性在官方文档中没有出现
      this.run()
    } else {
      // 更新时一般都这里,将 watcher 放入 watcher 队列
      queueWatcher(this)
    }
  }
}

queueWatcher

// src/core/observer/scheduler.js
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/**
 * 将 watcher 放入 queue 队列
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 如果 watcher 已经存在,则跳过
  if (has[id] == null) {
    // 缓存 watcher.id,用于判断 watcher 是否已经入队
    has[id] = true
    if (!flushing) {
      // 当前没有处于刷新队列状态,watcher 直接入队
      queue.push(watcher)
    } else {
      // 正在刷新队列,这时用户可能添加新的 watcher,就会走到这里
      // 从后往前找,找到第一个 watcher.id 比当前队列中 watcher.id 大的位置,然后将自己插入到该位置。保持队列是有序的。
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // waiting 保证了 nextTick 的调用只有一次
    if (!waiting) {
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        // 直接刷新调度队列
        // 一般不会走这儿,Vue 默认是异步执行,如果改为同步执行,性能会大打折扣
        flushSchedulerQueue()
        return
      }
      // nextTick => vm.$nextTick、Vue.nextTick
      nextTick(flushSchedulerQueue)
    }
  }
}

nextTick 等会再看,它的作用主要就是把 flushSchedulerQueue 使用异步任务去执行,先尝试用微任务,不支持的情况再用宏任务去执行。

那么先看看 flushSchedulerQueue 的作用:

flushSchedulerQueue

// src/core/observer/scheduler.js
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  // 对队列做了从小到大的排序,目的:
  // 1. 组件的更新由父到子,因为父组件在子组件之前被创建,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。
  // 2. 一个组件的用户 watcher 先于渲染 watcher 执行,以为用户 watcher 创建先于渲染 watcher。
  // 3. 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。
  queue.sort((a, b) => a.id - b.id)
  // 在遍历的时候每次都会对 queue.length 求值,因为在 watcher.run() 的时候,很可能用户会再次添加新的 watcher
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    // 执行 beforeUpdate 生命周期钩子,在 mount 阶段创建 Watcher 时传入
    if (watcher.before) {
      watcher.before()
    }
    // 将缓存的 watcher 清除
    id = watcher.id
    has[id] = null
    // 执行 watcher.run,最终触发更新函数
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }
  // 在重置状态之前保留队列的副本
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  //重置刷新队列状态
  resetSchedulerState()
  // keep-alive 组件相关
  callActivatedHooks(activatedQueue)
  // 执行 updated 生命周期钩子
  callUpdatedHooks(updatedQueue)
  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

/**
  把这些控制流程状态的一些变量恢复到初始值,把 watcher 队列清空。
 
/
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

/**
  由子组件到父组件依次执行 updated 生命周期钩子
 
/
function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

上面代码可以看出 flushSchedulerQueue 的作用就是执行更新队列。通过 watcher.run() 触发最终的更新。

watcher.run()

// src/core/observer/watcher.js
export default class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.cb = cb
  } 
  run () {
    if (this.active) {
      // 调用 this.get 方法
      const value = this.get()
      if (
        value !== this.value ||  // 新旧值不相等
        isObject(value) ||   // 新值是对象
        this.deep   // deep模式
      ) {
        // 更新旧值为新值
        const oldValue = this.value
        this.value = value
        if (this.user) {
          // 如果是用户 watcher
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          // 渲染 watcher,this.cb = noop,一个空函数
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
}

这里有两种情况,当 this.usertrue 的时候代表用户 watcher,在之前介绍过也就是 user watcher , 否则执行渲染 watcher 的逻辑。

  • user watcher

invokeWithErrorHandling 接收的第一个参数就是我们自定义侦听属性的回调函数,在初始化侦听属性 initWatch 方法过程中,实例化 new Watcher(vm, expOrFn, cb, options) 的时候传入。
第三个参数就是 [value, oldValue] (新值和旧值),这也就是为什么在侦听属性的回调函数中能获得新值和旧值。

// src/core/util/error.js
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  // 利用 try catch 做一些错误处理
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}
  • 渲染 watcher

如果是渲染 watcher 则执行 this.cb.call(this.vm, value, oldValue)。渲染 Wather 的实例化是在挂载时 mountComponent 方法中执行的:

  //  src/core/instance/lifecycle.js
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

export function noop (a?: any, b?: any, c?: any) {} 是一个空函数,所以 this.cb.call(this.vm, value, oldValue),就是在执行一个空函数。

渲染 watcher 在执行 watcher.run 会调用 this.get() ,也就会执行 this.getter.call(vm, vm)this.getter 实际就是实例化时传入的第二个参数 updateComponent

//  src/core/instance/lifecycle.js
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

所以这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会进入 patch  的过程。

nextTick

前面介绍了 flushSchedulerQueue 的作用就是去执行更新队列,那么我们看看 queueWatcher 中的这段代码是怎么回事:

nextTick(flushSchedulerQueue)

nextTick

// src/core/util/next-tick.js
const callbacks = []
let pending = false

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 用 callbacks 数组存储经过包装的 cb 函数
  callbacks.push(() => {
    if (cb) {
      // 用 try catch 包装回调函数,便于错误捕获
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

nextTick 第一个参数是一个回调函数,这里的回调函数对应的就是 flushSchedulerQueue 了。通过 try catch 将回调函数包装,用于错误捕获,然后将其放入 callbacks 中。

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

接下来当 pendingfalse 的时候执行 timerFuncpendingtrue,表示正在将任务放入浏览器的任务队列中;pendingfalse ,表示任务已经放入浏览器任务队列中了。

最后,nextTick 在没有传入 cb 回调函数的时候,会返回 promise,提供了一个 .then 的调用。

nextTick().then(() => {})

timerFunc

// src/core/util/next-tick.js
// 可以看到 timerFunc 的作用很简单,就是将 flushCallbacks 函数放入浏览器的异步任务队列中
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    // 首选 Promise
    p.then(flushCallbacks)
    /**
     * 在有问题的UIWebViews中,Promise.then不会完全中断,但是它可能会陷入怪异的状态,
     * 在这种状态下,回调被推入微任务队列,但队列没有被刷新,直到浏览器需要执行其他工作,例如处理一个计时器。
     * 因此,我们可以通过添加空计时器来“强制”刷新微任务队列。
     */
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 然后使用 MutationObserver
  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)) {
  // 然后 setImmediate,宏任务
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 最后 setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

flushCallbacks

// src/core/util/next-tick.js
/**
 *   1、将 pending 置为 false
 *   2、清空 callbacks 数组
 *   3、执行 callbacks 数组中的每一个函数(比如 flushSchedulerQueue、用户调用 nextTick 传递的回调函数)
 */
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

不管是全局 API Vue.nextTick,还是实例方法 vm.$nextTick,最后都是调用 next-tick.js 中的 nextTick 方法。

如果觉得还凑合的话,给个赞吧!!!也可以来我的 个人博客 逛逛!

标签:nextTick,Vue,const,vm,watcher,解读,源码,&&,true
From: https://www.cnblogs.com/mounterLove/p/18051367

相关文章

  • springboot3+vue3大事件(一)项目准备工作
     1、执行sql脚本,准备数据库表--创建数据库createdatabasedev;--使用数据库usedev;--用户表createtableuser(idintunsignedprimarykeyauto_incrementcomment'ID',usernamevarchar(20)notnullunique......
  • 一款基于Vue3实现的漂亮且功能强大的在线海报设计器
    大家好,我是Java陈序员。我们在工作中经常需要设计各种各样的图片,海报、产品图、文章图片、视频/公众号等。我们可以选择使用PS来设计图片,但是有时候想快速完成任务,有没有一款工具支持快速生成海报呢?答案是有的,今天给大家介绍一款在线图片(海报)设计器。关注微信公众号:【Java......
  • 给大家推荐一款基于Vue3通用型后台管理模板
    ​ 给大家推荐一款基于Vue3通用型后台管理模板这款Vue3后台管理模板介绍如下:        使用Vue3、Vite、ElementPlus、Pinia最新开发技术栈,拥有完整的Token登录鉴权、路由配置、界面简洁美观,可根据需要灵活配置主题、系统采用响应式布局,自适应各类屏幕尺寸、源代码有......
  • 前端学习-vue视频学习004-响应式数据ref reacive
    尚硅谷视频教程响应式数据vue2中,写在data()里的数据自动成为响应式数据vue3定义响应式数据,使用ref()或reactive()什么是响应式数据:数据改变时。页面随之变化,即为响应式数据ref()创建基本类型的响应式数据首先引入refimport{ref}from'vue'所有需要动态变化的数......
  • vue3笔记3watch监视的几种变化
    <template> <divclass="about">  <p>情况一,监事ref的值</p>  <h1>求和{{sum}}</h1>  <button@click="changeSum">++++</button> </div></template><scriptlang="ts"......
  • vue2+Element-UI弹出层中带复选框的树结构反填
    vue2+Element-UI弹出层中带复选框的树结构反填前言​ 我们做RBAC时候做角色的权限设置时在弹出权限列表时可能需要反填该角色现有的权限,所以以下案例是个按简单的RBAC的角色权限的树结构的反填。案例​ 1、最关键的是树要有node-key属性,因为树的很多方法都要使用到这个属性<!......
  • Vue3学习(二十一)- 文档管理页面布局修改
    写在前面按照国际惯例,要先聊下生活,吐槽一番,今天是2月14日,也是下午听老妈说,我才知道!现在真的是对日期节日已经毫无概念可言,只知道星期几。现在已经觉得写博客也好,学习文章也罢,和写日记一样,已经融入到我的生活中,或者更确切的说,变成生活的一部分了。饭后和老妈闲聊了几句后,我发......
  • 网页浏览器Chrome开发者调试工具-Source(源码)-断点调试、条件断点、日志断点
    前言全局说明网页浏览器Chrome开发者调试工具-Source(源码)-断点调试、条件断点、日志断点断点,是某行代码要执行,还没有执行的一个暂停点一、截图对照1.1Chrome浏览器1.1.1蓝色,普通断点1.1.2设置断点类型图中分别是:backpoint:普通断点(蓝色)Conditionalbreakp......
  • Glide源码解析四(解码和转码)
    本文基于Glide4.11.0Glide加载过程有一个解码过程,比如将url加载为inputStream后,要将inputStream解码为Bitmap。 从Glide源码解析一我们大致知道了Glide加载的过程,所以我们可以直接从这里看起,在这个过程中我们以从文件中加载bitmap为例:DecodeJob的一个方法:privatevoiddec......
  • 【Mybatis】【三】源码分析- MapperFactoryBean 的创建过程以及 Mapper 接口代理的生
    1 前言本节我们续前两节(调试查看Mapper接口生成过程、源码分析Mapper生成注入入口分析)的内容,看下MapperFactoryBean是如何代理掉我们的@Mapper接口的。上节我们看到我们的Mapper接口的BeanDefinition,已经放进spring的上下文中了,也就是在BeanFactory的BeanDefin......