手摸手带你理解Vue的Computed原理
前言
computed
在 Vue
中是很常用的属性配置,它能够随着依赖属性的变化而变化,为我们带来很大便利。那么本文就来带大家全面理解 computed
的内部原理以及工作流程。
在这之前,希望你能够对响应式原理有一些理解,因为 computed
是基于响应式原理进行工作。如果你对响应式原理还不是很了解,可以阅读我的上一篇文章:手摸手带你理解Vue响应式原理
computed 用法
想要理解原理,最基本就是要知道如何使用,这对于后面的理解有一定的帮助。
第一种,函数声明:
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
})
第二种,对象声明:
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
温馨提示:computed 内使用的 data 属性,下文统称为“依赖属性”
工作流程
先来了解下 computed
的大概流程,看看计算属性的核心点是什么。
入口文件:
// 源码位置:/src/core/instance/index.js
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) {
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
_init
:
// 源码位置:/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
// mergeOptions 对 mixin 选项和传入的 options 选项进行合并
// 这里的 $options 可以理解为 new Vue 时传入的对象
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// 初始化数据
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
initState
:
// 源码位置:/src/core/instance/state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 这里会初始化 Computed
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initComputed
:
// 源码位置:/src/core/instance/state.js
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
// 1
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
// 2
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
// create internal watcher for the computed property.
// 3
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{ lazy: true }
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
// 4
defineComputed(vm, key, userDef)
}
}
}
- 实例上定义
_computedWatchers
对象,用于存储“计算属性Watcher
” - 获取计算属性的
getter
,需要判断是函数声明还是对象声明 - 创建“计算属性
Watcher
”,getter
作为参数传入,它会在依赖属性更新时进行调用,并对计算属性重新取值。需要注意Watcher
的lazy
配置,这是实现缓存的标识 defineComputed
对计算属性进行数据劫持
defineComputed
:
// 源码位置:/src/core/instance/state.js
const noop = function() {}
// 1
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
// 判断是否为服务端渲染
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
// 2
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
// 3
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
// 4
Object.defineProperty(target, key, sharedPropertyDefinition)
}
sharedPropertyDefinition
是计算属性初始的属性描述对象- 计算属性使用函数声明时,设置属性描述对象的
get
和set
- 计算属性使用对象声明时,设置属性描述对象的
get
和set
- 对计算属性进行数据劫持,
sharedPropertyDefinition
作为第三个给参数传入
客户端渲染使用 createComputedGetter
创建 get
,服务端渲染使用 createGetterInvoker
创建 get
。它们两者有很大的不同,服务端渲染不会对计算属性缓存,而是直接求值:
function createGetterInvoker(fn) {
return function computedGetter () {
return fn.call(this, this)
}
}
但我们平常更多的是讨论客户端渲染,下面看看 createComputedGetter
的实现。
createComputedGetter
:
// 源码位置:/src/core/instance/state.js
function createComputedGetter (key) {
return function computedGetter () {
// 1
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 2
if (watcher.dirty) {
watcher.evaluate()
}
// 3
if (Dep.target) {
watcher.depend()
}
// 4
return watcher.value
}
}
}
这里就是计算属性的实现核心,computedGetter
也就是计算属性进行数据劫持时触发的 get
。
- 在上面的
initComputed
函数中,“计算属性Watcher
”就存储在实例的_computedWatchers
上,这里取出对应的“计算属性Watcher
” watcher.dirty
是实现计算属性缓存的触发点,watcher.evaluate
对计算属性重新求值- 依赖属性收集“渲染
Watcher
” - 计算属性求值后会将值存储在
value
中,get
返回计算属性的值
计算属性缓存及更新
缓存
下面我们来将 createComputedGetter
拆分,分析它们单独的工作流程。这是缓存的触发点:
if (watcher.dirty) {
watcher.evaluate()
}
接下来看看 Watcher
相关实现:
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
// dirty 初始值等同于 lazy
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
this.value = this.lazy
? undefined
: this.get()
}
}
还记得创建“计算属性Watcher
”,配置的 lazy
为 true。dirty
的初始值等同于 lazy
。所以在初始化页面渲染,对计算属性取值时,会执行一次 watcher.evaluate
。
evaluate() {
this.value = this.get()
this.dirty = false
}
求值后将值赋给 this.value
,上面 createComputedGetter
内的 watcher.value
就是在这里更新。接着 dirty
置为 false,如果依赖属性没有变化,下一次取值时,是不会执行 watcher.evaluate
的, 而是直接就返回 watcher.value
,这样就实现了缓存机制。
更新
依赖属性在更新时,会调用 dep.notify
:
notify() {
this.subs.forEach(watcher => watcher.update())
}
然后执行 watcher.update
:
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
由于“计算属性Watcher
”的 lazy
为 true,这里 dirty
会置为 true。等到页面渲染对计算属性取值时,执行 watcher.evaluate
重新求值,计算属性随之更新。
依赖属性收集依赖
收集计算属性Watcher
初始化时,页面渲染会将“渲染Watcher
”入栈,并挂载到Dep.target
在页面渲染过程中遇到计算属性,因此执行 watcher.evaluate
的逻辑,内部调用 this.get
:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 计算属性求值
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
popTarget()
this.cleanupDeps()
}
return value
}
Dep.target = null
let stack = [] // 存储 watcher 的栈
export function pushTarget(watcher) {
stack.push(watcher)
Dep.target = watcher
}
export function popTarget(){
stack.pop()
Dep.target = stack[stack.length - 1]
}
pushTarget
轮到“计算属性Watcher
”入栈,并挂载到Dep.target
,此时栈中为 [渲染Watcher, 计算属性Watcher]
this.getter
对计算属性求值,在获取依赖属性时,触发依赖属性的 数据劫持get
,执行 dep.depend
收集依赖(“计算属性Watcher
”)
收集渲染Watcher
this.getter
求值完成后popTragte
,“计算属性Watcher
”出栈,Dep.target
设置为“渲染Watcher
”,此时的 Dep.target
是“渲染Watcher
”
if (Dep.target) {
watcher.depend()
}
watcher.depend
收集依赖:
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
deps
内存储的是依赖属性的 dep
,这一步是依赖属性收集依赖(“渲染Watcher
”)
经过上面两次收集依赖后,依赖属性的 subs
存储两个 Watcher
,[计算属性Watcher,渲染Watcher]
为什么依赖属性要收集渲染Watcher
我在初次阅读源码时,很奇怪的是依赖属性收集到“计算属性Watcher
”不就好了吗?为什么依赖属性还要收集“渲染Watcher
”?
第一种场景:模板里同时用到依赖属性和计算属性
<template>
<div>{{msg}} {{msg1}}</div>
</template>
export default {
data(){
return {
msg: 'hello'
}
},
computed:{
msg1(){
return this.msg + ' world'
}
}
}
模板有用到依赖属性,在页面渲染对依赖属性取值时,依赖属性就存储了“渲染Watcher
”,所以 watcher.depend
这步是属于重复收集的,但 watcher
内部会去重。
这也是我为什么会产生疑问的点,Vue
作为一个优秀的框架,这么做肯定有它的道理。于是我想到了另一个场景能合理解释 watcher.depend
的作用。
第二种场景:模板内只用到计算属性
<template>
<div>{{msg1}}</div>
</template>
export default {
data(){
return {
msg: 'hello'
}
},
computed:{
msg1(){
return this.msg + ' world'
}
}
}
模板上没有使用到依赖属性,页面渲染时,那么依赖属性是不会收集 “渲染Watcher
”的。此时依赖属性里只会有“计算属性Watcher
”,当依赖属性被修改,只会触发“计算属性Watcher
”的 update
。而计算属性的 update
里仅仅是将 dirty
设置为 true,并没有立刻求值,那么计算属性也不会被更新。
所以需要收集“渲染Watcher
”,在执行完“计算属性Watcher
”后,再执行“渲染Watcher
”。页面渲染对计算属性取值,执行 watcher.evaluate
才会重新计算求值,页面计算属性更新。
总结
计算属性原理和响应式原理都是大同小异的,同样的是使用数据劫持以及依赖收集,不同的是计算属性有做缓存优化,只有在依赖属性变化时才会重新求值,其它情况都是直接返回缓存值。服务端不对计算属性缓存。
计算属性更新的前提需要“渲染Watcher
”的配合,因此依赖属性的 subs
中至少会存储两个 Watcher
。
手摸手带你理解Vue的Computed原理的更多相关文章
- 手摸手带你理解Vue的Watch原理
前言 watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用.在面试时,也是必问知识点,一般会用作和 computed 进行比较. 那么本文就来带大家从源码理解 ...
- 手摸手带你理解Vue响应式原理
前言 响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图.在面试中是经常考查的知识点,也是面试加分项. 本文将会循序渐进的解析响应式原理的工作流程,主要以下面结构进行: 分析主要成员,了解它 ...
- 【转】手摸手,带你用vue撸后台 系列三(实战篇)
前言 在前面两篇文章中已经把基础工作环境构建完成,也已经把后台核心的登录和权限完成了,现在手摸手,一起进入实操. Element 去年十月份开始用vue做管理后台的时候毫不犹豫的就选择了Elemen, ...
- 【转】手摸手,带你用vue撸后台 系列二(登录权限篇)
前言 拖更有点严重,过了半个月才写了第二篇教程.无奈自己是一个业务猿,每天被我司的产品虐的死去活来,之前又病了一下休息了几天,大家见谅. 进入正题,做后台项目区别于做其它的项目,权限验证与安全性是非常 ...
- 【转】手摸手,带你用vue撸后台 系列四(vueAdmin 一个极简的后台基础模板)
前言 做这个 vueAdmin-template 的主要原因是: vue-element-admin 这个项目的初衷是一个vue的管理后台集成方案,把平时用到的一些组件或者经验分享给大家,同时它也在不 ...
- 【转】手摸手,带你用vue撸后台 系列一
前言 说好的教程终于来了,第一篇文章主要来说一说在开始写业务代码前的一些准备工作吧,但这里不会教你webpack的基础配置,热更新怎么做,webpack速度优化等等,有需求的请自行google. 目录 ...
- 【手摸手,带你搭建前后端分离商城系统】01 搭建基本代码框架、生成一个基本API
[手摸手,带你搭建前后端分离商城系统]01 搭建基本代码框架.生成一个基本API 通过本教程的学习,将带你从零搭建一个商城系统. 当然,这个商城涵盖了很多流行的知识点和技术核心 我可以学习到什么? S ...
- 【手摸手,带你搭建前后端分离商城系统】03 整合Spring Security token 实现方案,完成主业务登录
[手摸手,带你搭建前后端分离商城系统]03 整合Spring Security token 实现方案,完成主业务登录 上节里面,我们已经将基本的前端 VUE + Element UI 整合到了一起.并 ...
- 【手摸手,带你搭建前后端分离商城系统】02 VUE-CLI 脚手架生成基本项目,axios配置请求、解决跨域问题
[手摸手,带你搭建前后端分离商城系统]02 VUE-CLI 脚手架生成基本项目,axios配置请求.解决跨域问题. 回顾一下上一节我们学习到的内容.已经将一个 usm_admin 后台用户 表的基本增 ...
随机推荐
- Java实现 LeetCode 798 得分最高的最小轮调 (暴力分析)
798. 得分最高的最小轮调 给定一个数组 A,我们可以将它按一个非负整数 K 进行轮调,这样可以使数组变为 A[K], A[K+1], A{K+2], - A[A.length - 1], A[0] ...
- C# Winform学习(六)
目标: 1.树型控件TreeView 2.列表视图控件ListView 一.树型控件 1.命名:tv开始 2.作用:用来显示有层次结构的数据 3.特点 1)允许有多个根节点 2)每个节点可以有多个子节 ...
- Java实现 蓝桥杯 算法训练 关联矩阵
算法训练 关联矩阵 时间限制:1.0s 内存限制:512.0MB 提交此题 问题描述 有一个n个结点m条边的有向图,请输出他的关联矩阵. 输入格式 第一行两个整数n.m,表示图中结点和边的数目.n&l ...
- Spring MVC详细讲解
一:三层架构和MVC 1:三层架构 我们的开发架构一般都是基于两种形式:一种是 C/S 架构,也就是客户端/服务器,另一种是 B/S 架构,也就是浏览器服务器.在 JavaEE 开发中,几乎全都是基于 ...
- electron内使用vue-slider-component组件报“$attrs is readonly”错误
解决办法 安装vue-no-ssr插件 https://www.npmjs.com/package/vue-no-ssr npm install vue-no-ssr --save-dev 代码 &l ...
- 如何安装vue脚手架?
前提(已经安装好node,可以正常使用npm) 一.cmd输入 npm install vue-cli -g ---- 全局安装vue-cli工具 安装好过后,再输入指令 vue --version ...
- Spring事务的传播属性
前言 Spring在TransactionDefinition接口中规定了7种类型的事务传播行为.事务传播行为是Spring框架独有的事务增强特性,他不属于的事务实际提供方数据库行为.这是Spring ...
- iOS -实现imageView中的button响应点击事件的方法
<pre name="code" class="cpp" style="font-size: 13px;">/** imagev ...
- ES 复合查询
ES在查询过程中比较多遇到符合查询,既需要多个字段过滤也需要特殊情况处理,本文简单介绍几种查询组合方便快捷查询ES. bool布尔查询有一个或者多个布尔子句组成 filter 只过滤符合条件的 ...
- 03 . Django之腾讯云短信
简介 由于项目在注册.登录.找回密码 时需要发送短信验证的功能,我们使用腾讯云短信做. 为什么要用腾讯云短信呢? 因为注册就送 100条免费短信 的额度. 注册腾讯云 注册一个腾讯云账户,腾讯云中提供 ...