• 【Vue2.x源码系列06】计算属性computed原理


    上一章 Vue2异步更新和nextTick原理,我们介绍了 JavaScript 执行机制是什么?nextTick源码是如何实现的?以及Vue是如何异步更新渲染的?

    本章目标

    • 计算属性是如何实现的?
    • 计算属性缓存原理 - 带有dirty属性的watcher
    • 洋葱模型的应用

    初始化

    在 Vue初始化实例的过程中,如果用户 options选项中存在计算属性时,则初始化计算属性

    // 初始化状态
    export function initState(vm) {
      const opts = vm.$options // 获取所有的选项
    
      // 初始化数据
      if (opts.data) { initData(vm) }
    
      // 初始化计算属性
      if (opts.computed) { initComputed(vm) }
    }

    我们给每个计算属性都创建了一个 Watcher实例,标识为lazy:true, 在初始化watcher时不会立即执行 get方法(计算属性方法)

    并将计算属性watcher 都保存到了 Vue实例上,让我们可以在后续的 getter方法中通过 vm获取到当前的计算属性watcher

    然后使用Object.defineProperty去劫持计算属性

    // 初始化计算属性
    function initComputed(vm) {
      const computed = vm.$options.computed
      const watchers = (vm._computedWatchers = {}) // 将每个计算属性对应的watcher 都保存到 vm上
      for (let key in computed) {
        let userDef = computed[key]
    
        // 兼容不同写法 函数方式 和 对象getter/setter方式
        let fn = typeof userDef === 'function' ? userDef : userDef.get
    
        // 给每个计算属性都创建一个 watcher,并标识为 lazy,不会立即执行 get-fn
        watchers[key] = new Watcher(vm, fn, { lazy: true })
    
        // 劫持计算属性getter/setter
        defineComputed(vm, key, userDef)
      }
    }
    
    // 劫持计算属性
    function defineComputed(target, key, userDef) {
      const setter = userDef.set || (() => {})
    
      Object.defineProperty(target, key, {
        get: createComputedGetter(key),
        set: setter,
      })
    }

    当我们劫持到计算属性被访问时,根据 dirty 值去决定是否更新 watcher缓存值

    然后让自己依赖的属性(准确来说是订阅的所有dep)都去收集上层watcher,即 Dep.target(可能是计算属性watcher,也可能是渲染watcher)

    // 劫持计算属性的访问
    function createComputedGetter(key) {
      return function () {
        const watcher = this._computedWatchers[key] // this就是 defineProperty 劫持的targer。获取到计算属性对应的watcher
    
        // 如果是脏的,就去执行用户传入的函数
        if (watcher.dirty) {
          watcher.evaluate() // 重新求值后 dirty变为false,下次就不求值了,走缓存值
        }
    
        // 当前计算属性watcher 出栈后,还有渲染watcher 或者其他计算属性watcher,我们应该让当前计算属性watcher 订阅的 dep,也去收集上一层的watcher 即Dep.target(可能是计算属性watcher,也可能是渲染watcher)
        if (Dep.target) {
          watcher.depend()
        }
    
        // 返回watcher上的值
        return watcher.value
      }

    Dep

    • Dep.target:当前渲染的 watcher,静态变量
    • stack:存放 watcher 的栈。 利用 pushTarget、popTarget 这两个方法做入栈出栈操作
    // 当前渲染的 watcher
    Dep.target = null
    
    // 存放 watcher 的栈
    let stack = []
    // 当前 watcher 入栈, Dep.target 指向 当前 watcher
    export function pushTarget(watcher) {
      stack.push(watcher)
      Dep.target = watcher
    }
    // 栈中最后一个 watcher 出栈,Dep.target指向栈中 最后一个 watcher,若栈为空,则为 undefined
    export function popTarget() {
      stack.pop()
      Dep.target = stack[stack.length - 1]
    }

    计算属性Watcher

    在初始化Vue实例时,我们会给每个计算属性都创建一个对应watcher(我们称之为计算属性watcher,除此之外还有 渲染watcher侦听器watcher ),他有一个 value 属性用于缓存计算属性方法的返回值。

    默认标识 lazy: true,懒的,代表计算属性watcher,创建时不会立即执行 get方法

    默认标识 dirty: true,脏的,当我们劫持到计算属性访问时,如果是脏的,我们会通过watcher.evaluate重新计算 watcher 的 value值 并将其标识为干净的;如果是干净的,则直接取 watcher 缓存值

    depend 方法,会让计算属性watcher 订阅的dep去收集上层watcher,可能是渲染watcher,也可能是计算属性watcher(计算属性嵌套的情况),实现洋葱模型的核心方法

    update 方法,当计算属性依赖的对象发生变化时,会触发dep.notify派发更新 并 调用 update 方法,只需更新 dirty 为 true即可。我们会在后续的渲染watcher 更新时,劫持到计算属性的访问操作,并通过 watcher.evaluate重新计算其 value值

    class Watcher {
      constructor(vm, fn, options) {
        // 计算属性watcher 用到的属性
        this.vm = vm
        this.lazy = options.lazy // 懒的,不会立即执行get方法
        this.dirty = this.lazy // 脏的,决定重新读取get返回值 还是 读取缓存值
    
        this.value = this.lazy ? undefined : this.get() // 存储 get返回值
      }
    
      // 重新渲染
      update() {
        console.log('watcher-update')
        if (this.lazy) {
          // 计算属性依赖的值发生改变,触发 setter 通知 watcher 更新,将计算属性watcher 标识为脏值即可
          // 后面还会触发渲染watcher,会走 evaluate 重新读取返回值
          this.dirty = true
        } else {
          queueWatcher(this) // 把当前的watcher 暂存起来,异步队列渲染
          // this.get(); // 重新渲染
        }
      }
    
      // 计算属性watcher为脏时,执行 evaluate,并将其标识为干净的
      evaluate() {
        this.value = this.get() // 重新获取到用户函数的返回值
        this.dirty = false
      }
      
      // 用于洋葱模型中计算属性watcher 订阅的dep去 depend收集上层watcher 即Dep.target(可能是计算属性watcher,也可能是渲染watcher)
      depend() {
        let i = this.deps.length
        while (i--) {
          this.deps[i].depend()
        }
      }
    }

    缓存原理

    计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。 缓存原理如下:

    在初始化计算属性时,我们使用Object.defineProperty劫持了计算属性,并做了一些 getter/setter操作

    计算属性watcher 有一个 dirty脏值属性,默认为 true

    当我们劫持到计算属性被访问时,如果 dirty 为 true,则执行 evaluate 更新 watcher 的 value值 并 将 dirty 标识为 false;如果为 false,则直接取 watcher 的缓存值

    当计算属性依赖的属性变化时,会通知 watcher 调用 update方法,此时我们将 dirty 标识为 true。这样再次取值时会重新进行计算

    洋葱模型

    在初始化Vue实例时,我们会给每个计算属性都创建一个对应的懒的watcher,不会立即调用计算属性方法

    当我们访问计算属性时,会通过watcher.evaluate()让其直接依赖的属性去收集当前的计算属性watcher,并且还会通过watcher.depend()让其订阅的所有 dep都去收集上层watcher,可能是渲染watcher,也可能是计算属性watcher(如果存在计算属性嵌套计算属性的话)。这样依赖的属性发生变化也可以让视图进行更新

    让我们一起来分析下计算属性嵌套的例子

    {{fullName}}

    computed: { fullAge() { return '今年' + this.age }, fullName() { console.log('run') return this.firstName + ' ' + this.lastName + ' ' + this.fullAge }, }
    1. 初始化组件时,渲染watcher 入栈
      • stack:[渲染watcher]
    2. 当执行 render方法并初次访问 fullName时,执行computed watcher1.evaluate()watcher1入栈
      • stack:[渲染watcher, watcher1]
    3. 当执行watcher1的 get方法时,其直接依赖的 firstName 和 lastName 会去收集当前的 watcher1;然后又访问 fullAge 并执行computed watcher2.evaluate()watcher2入栈
      • watcher1:[firstName, lastName]
      • stack:[渲染watcher, watcher1, watcher2]
    4. 执行watcher2的 get方法时,其直接依赖的 age 会去收集当前的 watcher2
      • watcher2:[age]
    5. watcher2出栈,并执行watcher2.depend(),让watcher2订阅的 dep再去收集当前watcher1
      • stack:[渲染watcher, watcher1]
      • watcher1:[firstName, lastName, age]
    6. watcher1出栈,执行watcher1.depend(),让watcher1订阅的 dep再去收集当前的渲染watcher
      • stack:[渲染watcher]
      • 渲染watcher:[firstName, lastName, age]

    __EOF__

  • 本文作者: 柏成
  • 本文链接: https://www.cnblogs.com/burc/p/17330998.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    redis集群的搭建
    阿里云ACE认证的含金量高吗?如何通过ACE认证考试?
    SpringBoot +Mybatis + Redis实现缓存(案例解析)
    【proteus】8086仿真/汇编:创建项目并添加汇编代码文件
    Jmeter常用线程组设置策略
    敏捷开发时代,彻底结束了
    牛客编程题--必刷101之双指针篇
    网心云容器魔方在iStoreOS下的多开
    【Android】带下划线的TextView
    Linux 中的内部命令和外部命令
  • 原文地址:https://www.cnblogs.com/burc/p/17330998.html