首页 > 编程语言 >【Vue2.x源码系列06】计算属性computed原理

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

时间:2023-04-18 17:22:08浏览次数:56  
标签:06 computed 渲染 get vm watcher 源码 计算 属性

上一章 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(如果存在计算属性嵌套计算属性的话)。这样依赖的属性发生变化也可以让视图进行更新

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

<p>{{fullName}}</p>

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]

标签:06,computed,渲染,get,vm,watcher,源码,计算,属性
From: https://www.cnblogs.com/burc/p/17301398.html

相关文章

  • Spring源码分析——BeanFactory体系之接口详细分析
    Spring的BeanFactory的继承体系堪称经典。这是众所周知的!作为Java程序员,不能错过!前面的博文分析了Spring的Resource资源类Resouce。今天开始分析Spring的IOC部分。众所周知,IOC是Spring框架最迷人的地方。它最重要的接口,就是BeanFactory了。BeanFactory有着庞大的继承、实现......
  • 【内附源码和文档】基于C++14异步蒙特卡洛工具函数
    Simple-Monte-Carlo-Tool-Function这是一个使用C++实现的简单的异步蒙特卡洛算法工具函数C++标准:C++14使用autores=MonteCarlo(sample_nums,check_sample_funtion,generate_sample_funtion,…args);doublep=res.get();std::cout<<p<<std::endl;sample_nums:需要生成的样......
  • 如何运行编译.NetCore的源码?
    作为.net的开发人员,为了能更好的code,我们要知其然并知其所以然,了解.netcore的源码是我们的基本素养✊源码地址.NETPlatform(github.com)这个是.net在github上开源的源码地址aspnetcore这个是.netcore的源码地址构建方法构建有几点需要注意一下:构建比较费时间,可以......
  • vue全家桶进阶之路33:Vue3 计算属性computed
    在Vue3中,计算属性可以使用computed函数来定义。computed函数接受两个参数:第一个参数是一个函数,该函数返回计算属性的值;第二个参数是一个可选的配置对象,可以包含getter和setter函数,以及控制计算属性缓存的缓存配置。Vue3中的计算属性与Vue2中的计算属性相比有以下几个变化:使用......
  • 直播app源码,使用vue-awesome-swiper创建轮播图幻灯片
    直播app源码,使用vue-awesome-swiper创建轮播图幻灯片1.引入引入方式可以参考官方文档,两种方式选一种即可:vue-awesome-swiperatv3.1.3 (1)第一种方式:在main.js入口文件中全局引入 ///src/main.js //swiper全局引入importVueAwesomeSwiperfrom'vue-awesome-swiper'im......
  • app直播源码,Node.js实现密码散列加密
    app直播源码,Node.js实现密码散列加密1.安装所需的包: npmibcryptjs--save​2.修改MongoDB中的模型: ///models/AdminUser.js constmongoose=require('mongoose')//定义模型的字段constschema=newmongoose.Schema({  username:{//用户名    ty......
  • 从源码角度深入解析Callable接口
    摘要:从源码角度深入解析Callable接口,希望大家踏下心来,打开你的IDE,跟着文章看源码,相信你一定收获不小。本文分享自华为云社区《一个Callable接口能有多少知识点?》,作者:冰河。并发编程一直是程序员们比较头疼的,如何编写正确的并发程序相比其他程序来说,是一件比较困难的事情,并发编......
  • asp.net mvc 之旅 —— 第五站 从源码中分析asp.net mvc 中的TempData
    在mvc的controller中,我们知道有很多的临时变量存放数据,比如说viewData,viewBag,还有一个比较特殊的tempData,关于前两个或许大家都明白,基本上是一个东西,就是各自的编程写法不一样,最终都会放到viewContext中,然后送到WebPage中,如果你要证明的话,可以看下下面的......
  • 从源码角度深入解析Callable接口
    摘要:从源码角度深入解析Callable接口,希望大家踏下心来,打开你的IDE,跟着文章看源码,相信你一定收获不小。本文分享自华为云社区《一个Callable接口能有多少知识点?》,作者:冰河。并发编程一直是程序员们比较头疼的,如何编写正确的并发程序相比其他程序来说,是一件比较困难的事情,并发编程......
  • asp.net mvc 之旅 —— 第六站 ActionFilter的应用及源码分析
       这篇文章我们开始看一下ActionFilter,从名字上其实就大概知道ActionFilter就是Action上的Filter,对吧,那么Action上的Filter大概有几个呢???这个问题其实还是蛮简单的,因为我们听说Mvc本身就是一个扩展性极强的框架,自然就是层层有拦截,层层有过滤,对吧,比如我们看到的如下Control......