手摸手带你理解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 后台用户 表的基本增 ... 
随机推荐
- tomcat漏洞利用总结
			一.后台war包getshell 漏洞利用: tomcat在conf/tomcat-users.xml配置用户权限 <?xml version="1.0" encoding= ... 
- 自动化测试: Selenium 自动登录授权,再 Requests 请求内容
			Selenium 自动登录网站.截图及 Requests 抓取登录后的网页内容.一起了解下吧. Selenium: 支持 Web 浏览器自动化的一系列工具和库的综合项目. Requests: 唯一的一 ... 
- python通用数据库操作工具 pydbclib
			pydbclib是一个通用的python关系型数据库操作工具包,使用统一的接口操作各种关系型数据库(如 oracle.mysql.postgres.hive.impala等)进行增删改查,它是对各个p ... 
- Rocket - jtag - JtagShifter
			https://mp.weixin.qq.com/s/pHtrlmSCPqzlDdfj3qkNPQ 简单介绍JtagShifter的实现. 1. 简单介绍 实现移位寄存器链,包含并行Capture和U ... 
- Python环境搭建—安利Python小白的Python和Pycharm安装详细教程
			人生苦短,我用Python.众所周知,Python目前越来越火,学习Python的小伙伴也越来越多.最近看到群里的小伙伴经常碰到不会安装Python或者不知道去哪下载Python安装包等系列问题,为了 ... 
- ASP.NET通过更改Url进行页面传值
			这里,通过假数据,手动创建的一个类,然后创建的一个集合,放入下拉框,选好值以后,点确定 会在另一个页面产生对应的id,有不懂的欢迎评论 创建一个类: using System; using Syste ... 
- Java实现 LeetCode 692 前K个高频单词(map的应用)
			692. 前K个高频单词 给一非空的单词列表,返回前 k 个出现次数最多的单词. 返回的答案应该按单词出现频率由高到低排序.如果不同的单词有相同出现频率,按字母顺序排序. 示例 1: 输入: [&qu ... 
- Java实现 蓝桥杯VIP 算法训练 奇偶判断
			问题描述 能被2整除的数称为偶数,不能被2整除的数称为奇数.给一个整数x,判断x是奇数还是偶数. 输入格式 输入包括一个整数x,0<=x<=100000000. 输出格式 如果x是奇数,则 ... 
- Java实现 蓝桥杯VIP 算法提高 我们的征途是星辰大海
			算法提高 我们的征途是星辰大海 时间限制:1.0s 内存限制:256.0MB 最新的火星探测机器人curiosity被困在了一个二维迷宫里,迷宫由一个个方格组成. 共有四种方格: '.' 代表空地,c ... 
- (五)SQLMap工具检测SQL注入漏洞、获取数据库中的数据
			目录结构 一.判断被测url的参数是否存在注入点 二.获取数据库系统的所有数据库名称(暴库) 三.获取Web应用当前所连接的数据库 四.获取Web应用当前所操作的DBMS用户 五.列出数据库中的所有用 ... 
