前言

computedVue 中是很常用的属性配置,它能够随着依赖属性的变化而变化,为我们带来很大便利。那么本文就来带大家全面理解 computed 的内部原理以及工作流程。

在这之前,希望你能够对响应式原理有一些理解,因为 computed 是基于响应式原理进行工作。如果你对响应式原理还不是很了解,可以阅读我的上一篇文章:手摸手带你理解Vue响应式原理

computed 用法

想要理解原理,最基本就是要知道如何使用,这对于后面的理解有一定的帮助。

第一种,函数声明:

  1. var vm = new Vue({
  2. el: '#example',
  3. data: {
  4. message: 'Hello'
  5. },
  6. computed: {
  7. // 计算属性的 getter
  8. reversedMessage: function () {
  9. // `this` 指向 vm 实例
  10. return this.message.split('').reverse().join('')
  11. }
  12. }
  13. })

第二种,对象声明:

  1. computed: {
  2. fullName: {
  3. // getter
  4. get: function () {
  5. return this.firstName + ' ' + this.lastName
  6. },
  7. // setter
  8. set: function (newValue) {
  9. var names = newValue.split(' ')
  10. this.firstName = names[0]
  11. this.lastName = names[names.length - 1]
  12. }
  13. }
  14. }

温馨提示:computed 内使用的 data 属性,下文统称为“依赖属性”

工作流程

先来了解下 computed 的大概流程,看看计算属性的核心点是什么。

入口文件:

  1. // 源码位置:/src/core/instance/index.js
  2. import { initMixin } from './init'
  3. import { stateMixin } from './state'
  4. import { renderMixin } from './render'
  5. import { eventsMixin } from './events'
  6. import { lifecycleMixin } from './lifecycle'
  7. import { warn } from '../util/index'
  8. function Vue (options) {
  9. this._init(options)
  10. }
  11. initMixin(Vue)
  12. stateMixin(Vue)
  13. eventsMixin(Vue)
  14. lifecycleMixin(Vue)
  15. renderMixin(Vue)
  16. export default Vue

_init:

  1. // 源码位置:/src/core/instance/init.js
  2. export function initMixin (Vue: Class<Component>) {
  3. Vue.prototype._init = function (options?: Object) {
  4. const vm: Component = this
  5. // a uid
  6. vm._uid = uid++
  7. // merge options
  8. if (options && options._isComponent) {
  9. // optimize internal component instantiation
  10. // since dynamic options merging is pretty slow, and none of the
  11. // internal component options needs special treatment.
  12. initInternalComponent(vm, options)
  13. } else {
  14. // mergeOptions 对 mixin 选项和传入的 options 选项进行合并
  15. // 这里的 $options 可以理解为 new Vue 时传入的对象
  16. vm.$options = mergeOptions(
  17. resolveConstructorOptions(vm.constructor),
  18. options || {},
  19. vm
  20. )
  21. }
  22. // expose real self
  23. vm._self = vm
  24. initLifecycle(vm)
  25. initEvents(vm)
  26. initRender(vm)
  27. callHook(vm, 'beforeCreate')
  28. initInjections(vm) // resolve injections before data/props
  29. // 初始化数据
  30. initState(vm)
  31. initProvide(vm) // resolve provide after data/props
  32. callHook(vm, 'created')
  33. if (vm.$options.el) {
  34. vm.$mount(vm.$options.el)
  35. }
  36. }
  37. }

initState:

  1. // 源码位置:/src/core/instance/state.js
  2. export function initState (vm: Component) {
  3. vm._watchers = []
  4. const opts = vm.$options
  5. if (opts.props) initProps(vm, opts.props)
  6. if (opts.methods) initMethods(vm, opts.methods)
  7. if (opts.data) {
  8. initData(vm)
  9. } else {
  10. observe(vm._data = {}, true /* asRootData */)
  11. }
  12. // 这里会初始化 Computed
  13. if (opts.computed) initComputed(vm, opts.computed)
  14. if (opts.watch && opts.watch !== nativeWatch) {
  15. initWatch(vm, opts.watch)
  16. }
  17. }

initComputed:

  1. // 源码位置:/src/core/instance/state.js
  2. function initComputed (vm: Component, computed: Object) {
  3. // $flow-disable-line
  4. // 1
  5. const watchers = vm._computedWatchers = Object.create(null)
  6. // computed properties are just getters during SSR
  7. const isSSR = isServerRendering()
  8. for (const key in computed) {
  9. const userDef = computed[key]
  10. // 2
  11. const getter = typeof userDef === 'function' ? userDef : userDef.get
  12. if (!isSSR) {
  13. // create internal watcher for the computed property.
  14. // 3
  15. watchers[key] = new Watcher(
  16. vm,
  17. getter || noop,
  18. noop,
  19. { lazy: true }
  20. )
  21. }
  22. // component-defined computed properties are already defined on the
  23. // component prototype. We only need to define computed properties defined
  24. // at instantiation here.
  25. if (!(key in vm)) {
  26. // 4
  27. defineComputed(vm, key, userDef)
  28. }
  29. }
  30. }
  1. 实例上定义 _computedWatchers 对象,用于存储“计算属性Watcher
  2. 获取计算属性的 getter,需要判断是函数声明还是对象声明
  3. 创建“计算属性Watcher”,getter 作为参数传入,它会在依赖属性更新时进行调用,并对计算属性重新取值。需要注意 Watcherlazy 配置,这是实现缓存的标识
  4. defineComputed 对计算属性进行数据劫持

defineComputed:

  1. // 源码位置:/src/core/instance/state.js
  2. const noop = function() {}
  3. // 1
  4. const sharedPropertyDefinition = {
  5. enumerable: true,
  6. configurable: true,
  7. get: noop,
  8. set: noop
  9. }
  10. export function defineComputed (
  11. target: any,
  12. key: string,
  13. userDef: Object | Function
  14. ) {
  15. // 判断是否为服务端渲染
  16. const shouldCache = !isServerRendering()
  17. if (typeof userDef === 'function') {
  18. // 2
  19. sharedPropertyDefinition.get = shouldCache
  20. ? createComputedGetter(key)
  21. : createGetterInvoker(userDef)
  22. sharedPropertyDefinition.set = noop
  23. } else {
  24. // 3
  25. sharedPropertyDefinition.get = userDef.get
  26. ? shouldCache && userDef.cache !== false
  27. ? createComputedGetter(key)
  28. : createGetterInvoker(userDef.get)
  29. : noop
  30. sharedPropertyDefinition.set = userDef.set || noop
  31. }
  32. // 4
  33. Object.defineProperty(target, key, sharedPropertyDefinition)
  34. }
  1. sharedPropertyDefinition 是计算属性初始的属性描述对象
  2. 计算属性使用函数声明时,设置属性描述对象的 getset
  3. 计算属性使用对象声明时,设置属性描述对象的 getset
  4. 对计算属性进行数据劫持,sharedPropertyDefinition 作为第三个给参数传入

客户端渲染使用 createComputedGetter 创建 get,服务端渲染使用 createGetterInvoker 创建 get。它们两者有很大的不同,服务端渲染不会对计算属性缓存,而是直接求值:

  1. function createGetterInvoker(fn) {
  2. return function computedGetter () {
  3. return fn.call(this, this)
  4. }
  5. }

但我们平常更多的是讨论客户端渲染,下面看看 createComputedGetter 的实现。

createComputedGetter:

  1. // 源码位置:/src/core/instance/state.js
  2. function createComputedGetter (key) {
  3. return function computedGetter () {
  4. // 1
  5. const watcher = this._computedWatchers && this._computedWatchers[key]
  6. if (watcher) {
  7. // 2
  8. if (watcher.dirty) {
  9. watcher.evaluate()
  10. }
  11. // 3
  12. if (Dep.target) {
  13. watcher.depend()
  14. }
  15. // 4
  16. return watcher.value
  17. }
  18. }
  19. }

这里就是计算属性的实现核心,computedGetter 也就是计算属性进行数据劫持时触发的 get

  1. 在上面的 initComputed 函数中,“计算属性Watcher”就存储在实例的_computedWatchers上,这里取出对应的“计算属性Watcher
  2. watcher.dirty 是实现计算属性缓存的触发点,watcher.evaluate 对计算属性重新求值
  3. 依赖属性收集“渲染Watcher
  4. 计算属性求值后会将值存储在 value 中,get 返回计算属性的值

计算属性缓存及更新

缓存

下面我们来将 createComputedGetter 拆分,分析它们单独的工作流程。这是缓存的触发点:

  1. if (watcher.dirty) {
  2. watcher.evaluate()
  3. }

接下来看看 Watcher 相关实现:

  1. export default class Watcher {
  2. vm: Component;
  3. expression: string;
  4. cb: Function;
  5. id: number;
  6. deep: boolean;
  7. user: boolean;
  8. lazy: boolean;
  9. sync: boolean;
  10. dirty: boolean;
  11. active: boolean;
  12. deps: Array<Dep>;
  13. newDeps: Array<Dep>;
  14. depIds: SimpleSet;
  15. newDepIds: SimpleSet;
  16. before: ?Function;
  17. getter: Function;
  18. value: any;
  19. constructor (
  20. vm: Component,
  21. expOrFn: string | Function,
  22. cb: Function,
  23. options?: ?Object,
  24. isRenderWatcher?: boolean
  25. ) {
  26. this.vm = vm
  27. if (isRenderWatcher) {
  28. vm._watcher = this
  29. }
  30. vm._watchers.push(this)
  31. // options
  32. if (options) {
  33. this.deep = !!options.deep
  34. this.user = !!options.user
  35. this.lazy = !!options.lazy
  36. this.sync = !!options.sync
  37. this.before = options.before
  38. } else {
  39. this.deep = this.user = this.lazy = this.sync = false
  40. }
  41. this.cb = cb
  42. this.id = ++uid // uid for batching
  43. this.active = true
  44. // dirty 初始值等同于 lazy
  45. this.dirty = this.lazy // for lazy watchers
  46. this.deps = []
  47. this.newDeps = []
  48. this.depIds = new Set()
  49. this.newDepIds = new Set()
  50. // parse expression for getter
  51. if (typeof expOrFn === 'function') {
  52. this.getter = expOrFn
  53. }
  54. this.value = this.lazy
  55. ? undefined
  56. : this.get()
  57. }
  58. }

还记得创建“计算属性Watcher”,配置的 lazy 为 true。dirty 的初始值等同于 lazy。所以在初始化页面渲染,对计算属性取值时,会执行一次 watcher.evaluate

  1. evaluate() {
  2. this.value = this.get()
  3. this.dirty = false
  4. }

求值后将值赋给 this.value,上面 createComputedGetter 内的 watcher.value 就是在这里更新。接着 dirty 置为 false,如果依赖属性没有变化,下一次取值时,是不会执行 watcher.evaluate 的, 而是直接就返回 watcher.value,这样就实现了缓存机制。

更新

依赖属性在更新时,会调用 dep.notify:

  1. notify() {
  2. this.subs.forEach(watcher => watcher.update())
  3. }

然后执行 watcher.update:

  1. update() {
  2. if (this.lazy) {
  3. this.dirty = true
  4. } else if (this.sync) {
  5. this.run()
  6. } else {
  7. queueWatcher(this)
  8. }
  9. }

由于“计算属性Watcher”的 lazy 为 true,这里 dirty 会置为 true。等到页面渲染对计算属性取值时,执行 watcher.evaluate 重新求值,计算属性随之更新。

依赖属性收集依赖

收集计算属性Watcher

初始化时,页面渲染会将“渲染Watcher”入栈,并挂载到Dep.target

在页面渲染过程中遇到计算属性,因此执行 watcher.evaluate 的逻辑,内部调用 this.get:

  1. get () {
  2. pushTarget(this)
  3. let value
  4. const vm = this.vm
  5. try {
  6. value = this.getter.call(vm, vm) // 计算属性求值
  7. } catch (e) {
  8. if (this.user) {
  9. handleError(e, vm, `getter for watcher "${this.expression}"`)
  10. } else {
  11. throw e
  12. }
  13. } finally {
  14. popTarget()
  15. this.cleanupDeps()
  16. }
  17. return value
  18. }
  1. Dep.target = null
  2. let stack = [] // 存储 watcher 的栈
  3. export function pushTarget(watcher) {
  4. stack.push(watcher)
  5. Dep.target = watcher
  6. }
  7. export function popTarget(){
  8. stack.pop()
  9. Dep.target = stack[stack.length - 1]
  10. }

pushTarget 轮到“计算属性Watcher”入栈,并挂载到Dep.target,此时栈中为 [渲染Watcher, 计算属性Watcher]

this.getter 对计算属性求值,在获取依赖属性时,触发依赖属性的 数据劫持get,执行 dep.depend 收集依赖(“计算属性Watcher”)

收集渲染Watcher

this.getter 求值完成后popTragte,“计算属性Watcher”出栈,Dep.target 设置为“渲染Watcher”,此时的 Dep.target 是“渲染Watcher

  1. if (Dep.target) {
  2. watcher.depend()
  3. }

watcher.depend 收集依赖:

  1. depend() {
  2. let i = this.deps.length
  3. while (i--) {
  4. this.deps[i].depend()
  5. }
  6. }

deps 内存储的是依赖属性的 dep,这一步是依赖属性收集依赖(“渲染Watcher”)

经过上面两次收集依赖后,依赖属性的 subs 存储两个 Watcher,[计算属性Watcher,渲染Watcher]

为什么依赖属性要收集渲染Watcher

我在初次阅读源码时,很奇怪的是依赖属性收集到“计算属性Watcher”不就好了吗?为什么依赖属性还要收集“渲染Watcher”?

第一种场景:模板里同时用到依赖属性和计算属性

  1. <template>
  2. <div>{{msg}} {{msg1}}</div>
  3. </template>
  4. export default {
  5. data(){
  6. return {
  7. msg: 'hello'
  8. }
  9. },
  10. computed:{
  11. msg1(){
  12. return this.msg + ' world'
  13. }
  14. }
  15. }

模板有用到依赖属性,在页面渲染对依赖属性取值时,依赖属性就存储了“渲染Watcher”,所以 watcher.depend 这步是属于重复收集的,但 watcher 内部会去重。

这也是我为什么会产生疑问的点,Vue 作为一个优秀的框架,这么做肯定有它的道理。于是我想到了另一个场景能合理解释 watcher.depend 的作用。

第二种场景:模板内只用到计算属性

  1. <template>
  2. <div>{{msg1}}</div>
  3. </template>
  4. export default {
  5. data(){
  6. return {
  7. msg: 'hello'
  8. }
  9. },
  10. computed:{
  11. msg1(){
  12. return this.msg + ' world'
  13. }
  14. }
  15. }

模板上没有使用到依赖属性,页面渲染时,那么依赖属性是不会收集 “渲染Watcher”的。此时依赖属性里只会有“计算属性Watcher”,当依赖属性被修改,只会触发“计算属性Watcher”的 update。而计算属性的 update 里仅仅是将 dirty 设置为 true,并没有立刻求值,那么计算属性也不会被更新。

所以需要收集“渲染Watcher”,在执行完“计算属性Watcher”后,再执行“渲染Watcher”。页面渲染对计算属性取值,执行 watcher.evaluate 才会重新计算求值,页面计算属性更新。

总结

计算属性原理和响应式原理都是大同小异的,同样的是使用数据劫持以及依赖收集,不同的是计算属性有做缓存优化,只有在依赖属性变化时才会重新求值,其它情况都是直接返回缓存值。服务端不对计算属性缓存。

计算属性更新的前提需要“渲染Watcher”的配合,因此依赖属性的 subs 中至少会存储两个 Watcher

手摸手带你理解Vue的Computed原理的更多相关文章

  1. 手摸手带你理解Vue的Watch原理

    前言 watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用.在面试时,也是必问知识点,一般会用作和 computed 进行比较. 那么本文就来带大家从源码理解 ...

  2. 手摸手带你理解Vue响应式原理

    前言 响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图.在面试中是经常考查的知识点,也是面试加分项. 本文将会循序渐进的解析响应式原理的工作流程,主要以下面结构进行: 分析主要成员,了解它 ...

  3. 【转】手摸手,带你用vue撸后台 系列三(实战篇)

    前言 在前面两篇文章中已经把基础工作环境构建完成,也已经把后台核心的登录和权限完成了,现在手摸手,一起进入实操. Element 去年十月份开始用vue做管理后台的时候毫不犹豫的就选择了Elemen, ...

  4. 【转】手摸手,带你用vue撸后台 系列二(登录权限篇)

    前言 拖更有点严重,过了半个月才写了第二篇教程.无奈自己是一个业务猿,每天被我司的产品虐的死去活来,之前又病了一下休息了几天,大家见谅. 进入正题,做后台项目区别于做其它的项目,权限验证与安全性是非常 ...

  5. 【转】手摸手,带你用vue撸后台 系列四(vueAdmin 一个极简的后台基础模板)

    前言 做这个 vueAdmin-template 的主要原因是: vue-element-admin 这个项目的初衷是一个vue的管理后台集成方案,把平时用到的一些组件或者经验分享给大家,同时它也在不 ...

  6. 【转】手摸手,带你用vue撸后台 系列一

    前言 说好的教程终于来了,第一篇文章主要来说一说在开始写业务代码前的一些准备工作吧,但这里不会教你webpack的基础配置,热更新怎么做,webpack速度优化等等,有需求的请自行google. 目录 ...

  7. 【手摸手,带你搭建前后端分离商城系统】01 搭建基本代码框架、生成一个基本API

    [手摸手,带你搭建前后端分离商城系统]01 搭建基本代码框架.生成一个基本API 通过本教程的学习,将带你从零搭建一个商城系统. 当然,这个商城涵盖了很多流行的知识点和技术核心 我可以学习到什么? S ...

  8. 【手摸手,带你搭建前后端分离商城系统】03 整合Spring Security token 实现方案,完成主业务登录

    [手摸手,带你搭建前后端分离商城系统]03 整合Spring Security token 实现方案,完成主业务登录 上节里面,我们已经将基本的前端 VUE + Element UI 整合到了一起.并 ...

  9. 【手摸手,带你搭建前后端分离商城系统】02 VUE-CLI 脚手架生成基本项目,axios配置请求、解决跨域问题

    [手摸手,带你搭建前后端分离商城系统]02 VUE-CLI 脚手架生成基本项目,axios配置请求.解决跨域问题. 回顾一下上一节我们学习到的内容.已经将一个 usm_admin 后台用户 表的基本增 ...

随机推荐

  1. git的相关基础操作

    一.git安装 从https://git-scm.com/下载相应版本安装即可,一路默认安装到底即可,安装目录可以自行选择 二.git配置 安装完git后在任意文件夹内单击鼠标右键,会出现Git GU ...

  2. js函数prototype属性学习(一)

    W3school上针对prototype属性是这么给出定义和用法的:使您有能力向对象添加属性和方法.再看w3school上给的那个实例,如下图: 仔细一看,原来最基本的作用就是对某些对象的属性.方法来 ...

  3. Java 解析 XML文件

    ​个人博客网:https://wushaopei.github.io/    (你想要这里多有) package com.example.poiutis.xml; import com.example ...

  4. Java实现 LeetCode 566 重塑矩阵(遍历矩阵)

    566. 重塑矩阵 在MATLAB中,有一个非常有用的函数 reshape,它可以将一个矩阵重塑为另一个大小不同的新矩阵,但保留其原始数据. 给出一个由二维数组表示的矩阵,以及两个正整数r和c,分别表 ...

  5. Java实现 LeetCode 126 单词接龙 II

    126. 单词接龙 II 给定两个单词(beginWord 和 endWord)和一个字典 wordList,找出所有从 beginWord 到 endWord 的最短转换序列.转换需遵循如下规则: ...

  6. Java实现 LeetCode 31下一个排列

    31. 下一个排列 实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列. 如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列). 必须原地修改,只允许 ...

  7. java实现排列序数

    X星系的某次考古活动发现了史前智能痕迹. 这是一些用来计数的符号,经过分析它的计数规律如下: (为了表示方便,我们把这些奇怪的符号用a~q代替) abcdefghijklmnopq 表示0 abcde ...

  8. Java实现第十届蓝桥杯等差数列

    试题 I: 等差数列 时间限制: 1.0s 内存限制: 512.0MB 本题总分:25 分 [问题描述] 数学老师给小明出了一道等差数列求和的题目.但是粗心的小明忘记了一 部分的数列,只记得其中 N ...

  9. IOS App如何调用python后端服务

    本篇文章旨在通过一个小的Demo形式来了解ios app是如何调用python后端服务的,以便我们在今后的工作中可以清晰的明白ios app与后端服务之间是如何实现交互的,今天的示例是拿登录功能做一个 ...

  10. js数据劫持 Object.defineProperty() 作用

    原生js Object.defineProperty() 作用 假设我们有一个obj对象,我们要给他设置一个name属性会这么做 Object.defineProperty()也可以设置对象属性 这个 ...