虽然目前的技术栈已由Vue转到了React,但从之前使用Vue开发的多个项目实际经历来看还是非常愉悦的,Vue文档清晰规范,api设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多场景使用Vue比React开发效率更高,之前也有断断续续研读过Vue的源码,但一直没有梳理总结,所以在此做一些技术归纳同时也加深自己对Vue的理解,那么今天要写的便是Vue中最常用到的API之一computed的实现原理。

基本介绍

话不多说,一个最基本的例子如下:


<div id="app">
<p>{{fullName}}</p>
</div>

new Vue({
data: {
firstName: 'Xiao',
lastName: 'Ming'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})

Vue中我们不需要在template里面直接计算{{this.firstName + ' ' + this.lastName}},因为在模版中放入太多声明式的逻辑会让模板本身过重,尤其当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性造成很大的影响,而computed的设计初衷也正是用于解决此类问题。

对比侦听器watch

当然很多时候我们使用computed时往往会与Vue中另一个API也就是侦听器watch相比较,因为在某些方面它们是一致的,都是以Vue的依赖追踪机制为基础,当某个依赖数据发生变化时,所有依赖这个数据的相关数据或函数都会自动发生变化或调用。

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

从vue官方文档对watch的解释我们可以了解到,使用 watch 选项允许我们执行异步操作 (访问一个API)或高消耗性能的操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态,而这些都是计算属性无法做到的。

下面还另外总结了几点关于computedwatch的差异:
  1. computed是计算一个新的属性,并将该属性挂载到vm(Vue实例)上,而watch是监听已经存在且已挂载到vm上的数据,所以用watch同样可以监听computed计算属性的变化(其它还有dataprops
  2. computed本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值,而watch则是当数据发生变化便会调用执行函数
  3. 从使用场景上说,computed适用一个数据被多个数据影响,而watch适用一个数据影响多个数据;

以上我们了解了computedwatch之间的一些差异和使用场景的区别,当然某些时候两者并没有那么明确严格的限制,最后还是要具体到不同的业务进行分析。

原理分析

言归正传,回到文章的主题computed身上,为了更深层次地了解计算属性的内在机制,接下来就让我们一步步探索Vue源码中关于它的实现原理吧。

在分析computed源码之前我们先得对Vue的响应式系统有一个基本的了解,Vue称其为非侵入性的响应式系统,数据模型仅仅是普通的JavaScript对象,而当你修改它们时,视图便会进行自动更新。

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项时,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter,这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

Vue响应系统,其核心有三点:observewatcherdep

  1. observe:遍历data中的属性,使用 Object.definePropertyget/set方法对其进行数据劫持
  2. dep:每个属性拥有自己的消息订阅器dep,用于存放所有订阅了该属性的观察者对象
  3. watcher:观察者(对象),通过dep实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应

对响应式系统有一个初步了解后,我们再来分析计算属性。
首先我们找到计算属性的初始化是在src/core/instance/state.js文件中的initState函数中完成的


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函数(其前后也分别初始化了initDatainitWatch)并传入两个参数vm实例和opt.computed开发者定义的computed选项,转到initComputed函数:


const computedWatcherOptions = { computed: true } function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
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]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
} if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
} // 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)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}

从这段代码开始我们观察这几部分:

  1. 获取计算属性的定义userDefgetter求值函数


    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加setget方法的对象形式,所以这里首先获取计算属性的定义userDef,再根据userDef的类型获取相应的getter求值函数。

  2. 计算属性的观察者watcher和消息订阅器dep


    watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    computedWatcherOptions
    )

    这里的watchers也就是vm._computedWatchers对象的引用,存放了每个计算属性的观察者watcher实例(注:后文中提到的“计算属性的观察者”、“订阅者”和watcher均指代同一个意思但注意和Watcher构造函数区分),Watcher构造函数在实例化时传入了4个参数:vm实例、getter求值函数、noop空函数、computedWatcherOptions常量对象(在这里提供给Watcher一个标识{computed:true}项,表明这是一个计算属性而不是非计算属性的观察者,我们来到Watcher构造函数的定义:


    class Watcher {
    constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
    ) {
    if (options) {
    this.computed = !!options.computed
    } if (this.computed) {
    this.value = undefined
    this.dep = new Dep()
    } else {
    this.value = this.get()
    }
    } get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
    value = this.getter.call(vm, vm)
    } catch (e) { } finally {
    popTarget()
    }
    return value
    } update () {
    if (this.computed) {
    if (this.dep.subs.length === 0) {
    this.dirty = true
    } else {
    this.getAndInvoke(() => {
    this.dep.notify()
    })
    }
    } else if (this.sync) {
    this.run()
    } else {
    queueWatcher(this)
    }
    } evaluate () {
    if (this.dirty) {
    this.value = this.get()
    this.dirty = false
    }
    return this.value
    } depend () {
    if (this.dep && Dep.target) {
    this.dep.depend()
    }
    }
    }

    为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。
    观察Watcherconstructor,结合刚才讲到的new Watcher传入的第四个参数{computed:true}知道,对于计算属性而言watcher会执行if条件成立的代码this.dep = new Dep(),dep也就是创建了该属性的消息订阅器。


    export default class Dep {
    static target: ?Watcher;
    subs: Array<Watcher>; constructor () {
    this.id = uid++
    this.subs = []
    } addSub (sub: Watcher) {
    this.subs.push(sub)
    } depend () {
    if (Dep.target) {
    Dep.target.addDep(this)
    }
    } notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
    }
    }
    } Dep.target = null

    dep同样精简了部分代码,我们观察Watcherdep的关系,用一句话总结

    watcher中实例化了dep并向dep.subs中添加了订阅者,dep通过notify遍历了dep.subs通知每个watcher更新。

  3. defineComputed定义计算属性


    if (!(key in vm)) {
    defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
    if (key in vm.$data) {
    warn(`The computed property "${key}" is already defined in data.`, vm)
    } else if (vm.$options.props && key in vm.$options.props) {
    warn(`The computed property "${key}" is already defined as a prop.`, vm)
    }
    }

    因为computed属性是直接挂载到实例对象中的,所以在定义之前需要判断对象中是否已经存在重名的属性,defineComputed传入了三个参数:vm实例、计算属性的key以及userDef计算属性的定义(对象或函数)。
    然后继续找到defineComputed定义处:


    export function defineComputed (
    target: any,
    key: string,
    userDef: Object | Function
    ) {
    const shouldCache = !isServerRendering()
    if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
    ? createComputedGetter(key)
    : userDef
    sharedPropertyDefinition.set = noop
    } else {
    sharedPropertyDefinition.get = userDef.get
    ? shouldCache && userDef.cache !== false
    ? createComputedGetter(key)
    : userDef.get
    : noop
    sharedPropertyDefinition.set = userDef.set
    ? userDef.set
    : noop
    }
    if (process.env.NODE_ENV !== 'production' &&
    sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
    warn(
    `Computed property "${key}" was assigned to but it has no setter.`,
    this
    )
    }
    }
    Object.defineProperty(target, key, sharedPropertyDefinition)
    }

    在这段代码的最后调用了原生Object.defineProperty方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition,初始化为:


    const sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
    }

    随后根据Object.defineProperty前面的代码可以看到sharedPropertyDefinitionget/set方法在经过userDefshouldCache等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinitionget函数也就是createComputedGetter(key)的结果,我们找到createComputedGetter函数调用结果并最终改写sharedPropertyDefinition大致呈现如下:


    sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
    watcher.depend()
    return watcher.evaluate()
    }
    },
    set: userDef.set || noop
    }

    当计算属性被调用时便会执行get访问函数,从而关联上观察者对象watcher


分析完以上步骤,我们再来梳理下整个流程:

  1. 当组件初始化的时候,computeddata会分别建立各自的响应系统,Observer遍历data中每个属性设置get/set数据拦截
  2. 初始化computed会调用initComputed函数

    1. 注册一个watcher实例,并在内实例化一个Dep消息订阅器用作后续收集依赖(比如渲染函数的watcher或者其他观察该计算属性变化的watcher
    2. 调用计算属性时会触发其Object.definePropertyget访问器函数
    3. 调用watcher.depend()方法向自身的消息订阅器depsubs中添加其他属性的watcher
    4. 调用watcherevaluate方法(进而调用watcherget方法)让自身成为其他watcher的消息订阅器的订阅者,首先将watcher赋给Dep.target,然后执行getter求值函数,当访问求值函数里面的属性(比如来自dataprops或其他computed)时,会同样触发它们的get访问器函数从而将该计算属性的watcher添加到求值函数中属性的watcher的消息订阅器dep中,当这些操作完成,最后关闭Dep.target赋为null并返回求值函数结果。
  3. 当某个属性发生变化,触发set拦截函数,然后调用自身消息订阅器depnotify方法,遍历当前dep中保存着所有订阅者wathcersubs数组,并逐个调用watcherupdate方法,完成响应更新。

浅谈Vue中计算属性computed的实现原理的更多相关文章

  1. 浅谈Vue中计算属性(computed)和方法(methods)的差别

    浅谈Vue中计算属性(computed)和方法(methods)的差别 源码地址 methods方法和computed计算属性,两种方式的最终结果确实是完全相同 计算属性是基于它们的响应式依赖进行缓存 ...

  2. vue中计算属性computed方法内传参

    vue中computed计算属性无法直接进行传参 如果有传参数的需求比如说做数据筛选功能可以使用闭包函数(也叫匿名函数)实现 例如: 在上篇博客vue安装使用最后的成绩表练习中的过滤功能的实现: &l ...

  3. 浅谈CSS3中display属性的Flex布局

    浅谈CSS3中display属性的Flex布局   最近在学习微信小程序,在设计首页布局的时候,新认识了一种布局方式display:flex 1 .container { 2 display: fle ...

  4. Vue.js 计算属性(computed)

    Vue.js 计算属性(computed) 如果在模板中使用一些复杂的表达式,会让模板显得过于繁重,且后期难以维护.对此,vue.js 提供了计算属性(computed),你可以把这些复杂的表达式写到 ...

  5. vue的计算属性computed和监听器watch

    <template> <div> this is A.vue <br> <!--计算属性--> <label for="msg" ...

  6. vue中计算属性的get与set方法

    计算属性get set方法 在vue的计算属性中,所定义的都是属性,可以直接调用 正常情况下,计算属性中的每一个属性对应的都是一个对象,对象中包括了set方法与get方法 computed:{ ful ...

  7. Vue之计算属性Computed和属性监听Watch,Computed和Watch的区别

    一. 计算属性(computed) 1.计算属性是为了模板中的表达式简洁,易维护,符合用于简单运算的设计初衷. 例如: <div id="app"> {{ myname ...

  8. vue中计算属性中的set和get

    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <body& ...

  9. 浅谈vue中的计算属性和侦听属性

    计算属性 计算属性用于处理复杂的业务逻辑 计算属性具有依赖性,计算属性依赖 data中的初始值,只有当初始值改变的时候,计算属性才会再次计算 计算属性一般书写为一个函数,返回了一个值,这个值具有依赖性 ...

随机推荐

  1. (数据科学学习手札94)QGIS+Conda+jupyter玩转Python GIS

    本文完整代码及数据已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 QGIS随着近些年的发展,得益于其开源免费 ...

  2. Java面试题(Java基础篇)

    Java 基础 1.JDK 和 JRE 有什么区别? JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境. JRE:Java Run ...

  3. Pycharm2019.3永久激活

    1. 下载破解补丁,https://pan.baidu.com/s/1mcQM8CLUnweY02ahKEr4PQ ,下载最新上传的压缩包 2. 将压缩包解压,里面有激活文件ACTIVATION_CO ...

  4. 放眼SEM现状及发展历程

    http://www.wocaoseo.com/thread-187-1-1.html 由于近年来移动应用的基本普及,搜索引擎营销随之进入高速发展时代,应用层次的提升已经成为企业营销策略的一个重要组成 ...

  5. Jeecg-Cloud学习之路(一)

    首先,Spring-Cloud目前是行业的潮流,貌似不会就落后了,笔者为了不脱离大部队只能深入学习一下了. 其次.跳槽到一家公司,给公司推荐了Jeecg-Boot的开发平台,那么为了后面扩展为clou ...

  6. 23种设计模式 - 对象创建(FactoryMethod - AbstractFactory - Prototype - Builder)

    其他设计模式 23种设计模式(C++) 每一种都有对应理解的相关代码示例 → Git原码 ⌨ 对象创建 通过"对象创建" 模式绕开new,来避免对象创建(new)过程中所导致的紧耦 ...

  7. 从后端到前端之Vue(六)表单组件

    表单组件 做项目的时候会遇到一个比较头疼的问题,一个大表单里面有好多控件,一个一个做设置太麻烦,更头疼的是,需求还总在变化,一会多选.一会单选.一会下拉的,变来变去的烦死宝宝了. 那么怎么解决这个问题 ...

  8. nginx如何限制并发连接请求数?

    简介 限制并发连接数的模块为:http_limit_conn_module,地址:http://nginx.org/en/docs/http/ngx_http_limit_conn_module.ht ...

  9. 2019HNCPC C Distinct Substrings 后缀自动机

    题意 给定一个长度为n字符串,字符集大小为m(1<=n,m<=1e6),求\(\bigoplus_{c = 1}^{m}\left(h(c) \cdot 3^c \bmod (10^9+7 ...

  10. 1.异常页面: /File/ApplyFileForm.aspx 2.异常信息: 基类包括字段“PageOfficeCtrl1”,但其类型(PageOffice.PageOfficeCtrl)与控件(PageOffice.PageOfficeCtrl)的类型不兼容。

    出现页面报错的解决如下: 1.  在  .aspx  页面报错的字段  ID 名称更改: 2.  在没有重复 ID控件,先实行步骤1, 生成项目是ok的,然后,还原,原来的ID名称,再去浏览页面运行, ...