【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(如果存在计算属性嵌套计算属性的话)。这样依赖的属性发生变化也可以让视图进行更新
让我们一起来分析下计算属性嵌套的例子
<p>{{fullName}}</p>
computed: {
fullAge() {
return '今年' + this.age
},
fullName() {
console.log('run')
return this.firstName + ' ' + this.lastName + ' ' + this.fullAge
},
}
- 初始化组件时,渲染watcher 入栈
stack:[渲染watcher]
- 当执行 render方法并初次访问 fullName时,执行
computed watcher1.evaluate(),watcher1入栈stack:[渲染watcher, watcher1]
- 当执行
watcher1的 get方法时,其直接依赖的 firstName 和 lastName 会去收集当前的 watcher1;然后又访问 fullAge 并执行computed watcher2.evaluate(),watcher2入栈watcher1:[firstName, lastName]stack:[渲染watcher, watcher1, watcher2]
- 执行
watcher2的 get方法时,其直接依赖的 age 会去收集当前的 watcher2watcher2:[age]
watcher2出栈,并执行watcher2.depend(),让watcher2订阅的 dep再去收集当前watcher1stack:[渲染watcher, watcher1]watcher1:[firstName, lastName, age]
watcher1出栈,执行watcher1.depend(),让watcher1订阅的 dep再去收集当前的渲染watcherstack:[渲染watcher]渲染watcher:[firstName, lastName, age]
【Vue2.x源码系列06】计算属性computed原理的更多相关文章
- vue2.0中的watch和计算属性computed
watch和computed均可以监控程序员想要监控的对象,当这些对象发生改变之后,可以触发回调函数做一些逻辑处理 watch监控自身属性变化 <!DOCTYPE html> <ht ...
- Vue2.0源码阅读笔记--双向绑定实现原理
上一篇 文章 了解了Vue.js的生命周期.这篇分析Observe Data过程,了解Vue.js的双向数据绑定实现原理. 一.实现双向绑定的做法 前端MVVM最令人激动的就是双向绑定机制了,实现双向 ...
- Mybaits 源码解析 (五)----- 面试源码系列:Mapper接口底层原理(为什么Mapper不用写实现类就能访问到数据库?)
刚开始使用Mybaits的同学有没有这样的疑惑,为什么我们没有编写Mapper的实现类,却能调用Mapper的方法呢?本篇文章我带大家一起来解决这个疑问 上一篇文章我们获取到了DefaultSqlSe ...
- 大白话Vue源码系列(05):运行时鸟瞰图
阅读目录 Vue 实例的生命周期 实例创建 响应的数据绑定 挂载到 DOM 节点 结论 研究 runtime 一边 Vue 一边源码 初看 Vue 是 Vue 源码是源码 再看 Vue 不是 Vue ...
- Spring源码系列(二)--bean组件的源码分析
简介 spring-bean 组件是 Spring IoC 的核心,我们可以使用它的 beanFactory 来获取所需的对象,对象的实例化.属性装配和初始化等都可以交给 spring 来管理. 本文 ...
- 深入学习JDK源码系列之、ArrayList
前言 JDK源码解析系列文章,都是基于JDK8分析的,虽然JDK15马上要出来了,但是JDK8我还不会,我... 类图 实现了RandomAccess接口,可以随机访问 实现了Cloneable接口, ...
- 事件机制-Spring 源码系列(4)
事件机制-Spring 源码系列(4) 目录: Ioc容器beanDefinition-Spring 源码(1) Ioc容器依赖注入-Spring 源码(2) Ioc容器BeanPostProcess ...
- Ioc容器BeanPostProcessor-Spring 源码系列(3)
Ioc容器BeanPostProcessor-Spring 源码系列(3) 目录: Ioc容器beanDefinition-Spring 源码(1) Ioc容器依赖注入-Spring 源码(2) Io ...
- 大白话Vue源码系列(03):生成AST
阅读目录 AST 节点定义 标签的正则匹配 解析用到的工具方法 解析开始标签 解析结束标签 解析文本 解析整块 HTML 模板 未提及的细节 本篇探讨 Vue 根据 html 模板片段构建出 AST ...
- 大白话Vue源码系列(03):生成render函数
阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...
随机推荐
- P2212 Watering the Fields S
题目描述 给定n个点,第i个点的坐标为(xi,yi)(xi,yi),如果想连通第i个点与第j个点,需要耗费的代价为两点的距离.第i个点与第j个点之间的距离使用欧几里得距离进行计算,即:(xi-xj ...
- maya灯光导入houdini插件开发
加入工作室时师兄给了两道测试题,由于第一道是完善师兄的一个houdini项目管理插件,我只是开发了一些小功能,所以不好意思拿出来. 第二道题就完全是由自己开发的一个小插件,功能是把maya里的灯光导入 ...
- pip下载时使用国内镜像 设置pip.ini文件
https://blog.csdn.net/u011107575/article/details/109901086 https://www.python.org/ftp/python/https:/ ...
- Windows10常用快捷键总结
--Windows10常用快捷键总结 1. Window键: 打开或关闭|开始菜单 2. Win + A 打开操作中心 3. Win + D 显示桌面 4. Win + E 打开计算机文件管理器 5. ...
- vite + vue安装 注意事项
一.要求node版本必须>12.0.0 1.node 如何升级 · 执行npm cache clean -f 清除缓冲 · npm install -g n 安装 n 模块 n模块用于管理 n ...
- 鲁迅文集 第3卷 而已集 华盖集续编 华盖集 热风\四十一.md
目录 导读 正文 导读 本篇首次发表于1919年1月15日<新青年>第六卷第一号.署名唐俟. 文章以生物进化的事实,驳斥旧势力对改革者的嘲讽,号召青年蔑视反改革者的冷笑和暗箭,&quo ...
- git练习网站(图形化版)
https://learngitbranching.js.org/?locale=zh_CN
- CH573 CH582 CH579蓝牙从机(peripheral)例程讲解六(蓝牙设置白名单)
蓝牙从机设置白名单,可以只扫描应答(白名单中列出的)设备,只允许(白名单中列出的)设备连接. 蓝牙主机设置白名单,可以只扫描.连接特定的蓝牙设备(白名单中列出的). 一.蓝牙从机白名单设置有关的函数介 ...
- loadrunner之录制脚本
LoadRunner是一款性能测试软件,通过模拟真实的用户行为,通过负载.并发和性能实时监控以及完成后的测试报告,分析系统可能存在的瓶颈,LoadRunner最为有效的手段之一应该就是并发控制,通过在 ...
- 大数据组件对应Ranger插件的选择
在都是开源组件的前提下,一般需要我们多关注到组件和插件的版本和类型选择. 参考 https://zhuanlan.zhihu.com/p/370263573 https://www.bookstack ...