手摸手带你理解Vue的Computed原理

时间: 2020-06-22阅读: 194标签: 原理

前言

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

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


computed 用法

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

第一种,函数声明:

Copy
var vm = new vue({ el: '#example', data: { message: 'Hello' }, computed: { // 计算属性的 getter reversedMessage: function () { // `this` 指向 vm 实例 return this.message.split('').reverse().join('') } } })

第二种,对象声明:

Copy
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 的大概流程,看看计算属性的核心点是什么。

入口文件:

Copy
// 源码位置:/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:

Copy
// 源码位置:/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:

Copy
// 源码位置:/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:

Copy
// 源码位置:/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) } } }
  1. 实例上定义 _computedWatchers 对象,用于存储“计算属性Watcher”
  2. 获取计算属性的 getter,需要判断是函数声明还是对象声明
  3. 创建“计算属性Watcher”,getter 作为参数传入,它会在依赖属性更新时进行调用,并对计算属性重新取值。需要注意 Watcher 的 lazy 配置,这是实现缓存的标识
  4. defineComputed 对计算属性进行数据劫持

defineComputed:

Copy
// 源码位置:/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) }
  1. sharedPropertyDefinition 是计算属性初始的属性描述对象
  2. 计算属性使用函数声明时,设置属性描述对象的 get 和 set
  3. 计算属性使用对象声明时,设置属性描述对象的 get 和 set
  4. 对计算属性进行数据劫持,sharedPropertyDefinition 作为第三个给参数传入

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

Copy
function createGetterInvoker(fn) { return function computedGetter () { return fn.call(this, this) } }

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

createComputedGetter:

Copy
// 源码位置:/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。

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

计算属性缓存及更新

缓存

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

Copy
if (watcher.dirty) { watcher.evaluate() }

接下来看看 Watcher 相关实现:

Copy
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。

Copy
evaluate() { this.value = this.get() this.dirty = false }

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

更新

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

Copy
notify() { this.subs.forEach(watcher => watcher.update()) }

然后执行 watcher.update:

Copy
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:

Copy
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 }
Copy
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”

Copy
if (Dep.target) { watcher.depend() }

watcher.depend 收集依赖:

Copy
depend() { let i = this.deps.length while (i--) { this.deps[i].depend() } }

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

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

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

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

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

Copy
<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 的作用。

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

Copy
<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。

站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

2.广告联盟: 整理了目前主流的广告联盟平台,如果你有流量,可以作为参考选择适合你的平台点击进入

链接: http://www.fly63.com/article/detial/9349

Vue的双向数据绑定原理

Object属性分为两个类型:数据属性、访问器属性,每类属性又有其不同的特显,双向绑定的原理是根据其访问器属性的特性来实现的。Configurable:是否可以通过delete删除,能否修改他的属性特性,能否修改为访问器属性。默认值true

天天都在使用CSS,那么CSS的原理是什么呢?

作为前端,我们每天都在与CSS打交道,那么CSS的原理是什么呢?开篇,我们还是不厌其烦的回顾一下浏览器的渲染过程,学会使用永远都是最基本的标准,但是懂得原理,你才能触类旁通,超越自我。

Js深浅拷贝原理

如果拷贝的值是基本数据类型,拷贝的是基本类型的值。如果是引用类型拷贝的是内存地址。浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址。

new,call,apply,bind方法的实现原理

javascript中new,call,apply,bind等方法是我们经常要使用到,在伪数组转数组、函数传参、继承等场景中,都离不开他们。这里就不具体讨论他们的使用方法,我们将研究他们的实现方式,重写属于我们自己的方法,让大家重新认识他们的实现过程

NGINX Ingress Controller 设计原理

nginx ingress 控制器目标是组织 nginx 配置文件, 当 nginx 的配置文件发生任何更改时都需要重新加载,当配置文件中upstream 的内容有变更时(例如 当部署的应用中的 endpoints 变更时), nginx 的配置文件不会被重新加载

小程序底层实现原理及一些思考

当时的我将我们的小程序定位成一个SPA单页应用 ,因为我们的小程序的宿主环境是浏览器。它只是看起来像小程序(因为这个窗口没有地址栏什么的),但其实包括UI渲染和事件交互在内的绝大部分功能都是基于Web技术

vue3的数据响应原理和实现

话说vue3已经发布,就引起了大量前端人员的关注,木得办法,学不动也得硬着头皮学呀,本篇文章就简单介绍一下「vue3的数据响应原理」,以及简单实现其reactive、effect、computed函数

Vue.js响应式原理

updateComponent在更新渲染组件时,会访问1或多个数据模版插值,当访问数据时,将通过getter拦截器把componentUpdateWatcher作为订阅者添加到多个依赖中,每当其中一个数据有更新,将执行setter函数

连v-show都不会你还敢说熟悉 Vue 原理?

Vue 作为最主流的前端框架,中文资料齐全、入门简单、生态活跃,可以说是工作中最常用的,如今对 Vue 原理的熟悉基本上是简历的标配了。之前参与了部分 2019 校园招聘的面试工作,发现很多简历上都写了:

也许,这样理解OAuth原理更容易!

以下业务场景只针对于 Web 系统,而且 Web 页面有后台服务程序的场景。那一天突然有一个合作商登门拜访,提出合作共赢的意向。业务的场景就是我们的系统用户能够在他们系统登录,并能够获取用户一定的信息以便进行一些业务操作。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!

文章投稿关于web前端网站点搜索站长推荐网站地图站长QQ:522607023

小程序专栏: 土味情话心理测试脑筋急转弯幽默笑话段子句子语录成语大全运营推广