vue中的computed 和 vm.$data 原理解析

目录
  • 官方一个最简单的例子如下
  • 先看看 initData 这条线
  • 我们具体看看defineReactive的源代码
  • 在初始化computed时,有2个地方需要去关注
  • 我们看看Watcher的构造函数

使用vuex中store中的数据,基本上离不开vue中一个常用的属性computed。

官方一个最简单的例子如下

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

不知大家有没有思考过,vue的computed是如何更新的,为什么当vm.message发生变化时,vm.reversedMessage也会自动发生变化?

我们来看看vue中data属性和computed相关的源代码。

// src/core/instance/state.js
// 初始化组件的state
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)
  // 当组件存在data属性
  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)
  }
}

initState方法当组件实例化时会自动触发,该方法主要完成了初始化data,methods,props,computed,watch这些我们常用的属性,我们来看看我们需要关注的initData和initComputed(为了节省时间,去除了不太相关的代码)

先看看 initData 这条线

// src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // .....省略无关代码
  
  // 将vue的data传入observe方法
  observe(data, true /* asRootData */)
}
// src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value)) {
    return
  }
  let ob: Observer | void
  // ...省略无关代码
  ob = new Observer(value)
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

在初始化的时候observe方法本质上是实例化了一个Observer对象,这个对象的类是这样的

// src/core/observer/index.js
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data
  constructor (value: any) {
    this.value = value
    // 关键代码 new Dep对象
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // ...省略无关代码
    this.walk(value)
  }
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 给data的所有属性调用defineReactive
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}

在对象的构造函数中,最后调用了walk方法,该方法即遍历data中的所有属性,并调用defineReactive方法,defineReactive方法是vue实现 MDV(Model-Driven-View)的基础,本质上就是代理了数据的set,get方法,当数据修改或获取的时候,能够感知(当然vue还要考虑数组,Object中嵌套Object等各种情况,本文不在分析)。

我们具体看看defineReactive的源代码

// src/core/observer/index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 重点,在给具体属性调用该方法时,都会为该属性生成唯一的dep对象
  const dep = new Dep()
  // 获取该属性的描述对象
  // 该方法会返回对象中某个属性的具体描述
  // api地址https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果该描述不能被更改,直接返回,因为不能更改,那么就无法代理set和get方法,无法做到响应式
  if (property && property.configurable === false) {
    return
  }
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  let childOb = !shallow && observe(val)
  // 重新定义data当中的属性,对get和set进行代理。
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 收集依赖, reversedMessage为什么会跟着message变化的原因
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 通知依赖进行更新
      dep.notify()
    }
  })
}

我们可以看到,在所代理的属性的get方法中,当dep.Target存在的时候会调用dep.depend()方法,这个方法非常的简单,不过在说这个方法之前,我们要认识一个新的类Dep

Dep 是 vue 实现的一个处理依赖关系的对象,

主要起到一个纽带的作用,就是连接 reactive data 与 watcher,代码非常的简单

// src/core/observer/dep.js
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, 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++) {
      // 更新 watcher 的值,与 watcher.evaluate() 类似,
      // 但 update 是给依赖变化时使用的,包含对 watch 的处理
      subs[i].update()
    }
  }
}
// 当首次计算 computed 属性的值时,Dep 将会在计算期间对依赖进行收集
Dep.target = null
const targetStack = []
export function pushTarget (_target: Watcher) {
  // 在一次依赖收集期间,如果有其他依赖收集任务开始(比如:当前 computed 计算属性嵌套其他 computed 计算属性),
  // 那么将会把当前 target 暂存到 targetStack,先进行其他 target 的依赖收集,
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}
export function popTarget () {
  // 当嵌套的依赖收集任务完成后,将 target 恢复为上一层的 Watcher,并继续做依赖收集
  Dep.target = targetStack.pop()
}

代码非常的简单,回到调用dep.depend()方法的时候,当Dep.Target存在,就会调用,而depend方法则是将该dep加入watcher的newDeps中,同时,将所访问当前属性的dep对象中的subs插入当前Dep.target的watcher.看起来有点绕,不过没关系,我们一会跟着例子讲解一下就清楚了。

讲完了代理的get,方法,我们讲一下代理的set方法,set方法的最后调用了dep.notify(),当设置data中具体属性值的时候,就会调用该属性下面的dep.notify()方法,通过class Dep了解到,notify方法即将加入该dep的watcher全部更新,也就是说,当你修改data中某个属性值时,会同时调用dep.notify()来更新依赖该值的所有watcher。

介绍完了initData这条线,我们继续来介绍initComputed这条线,这条线主要解决了什么时候去设置Dep.target的问题(如果没有设置该值,就不会调用dep.depend(), 即无法获取依赖)。

// src/core/instance/state.js
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  // 初始化watchers列表
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (!isSSR) {
      // 关注点1,给所有属性生成自己的watcher, 可以在this._computedWatchers下看到
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    if (!(key in vm)) {
      // 关注点2
      defineComputed(vm, key, userDef)
    }
  }
}

在初始化computed时,有2个地方需要去关注

  • 对每一个属性都生成了一个属于自己的Watcher实例,并将 { lazy: true }作为options传入
  • 对每一个属性调用了defineComputed方法(本质和data一样,代理了自己的set和get方法,我们重点关注代理的get方法)

我们看看Watcher的构造函数

// src/core/observer/watcher.js
constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // 如果初始化lazy=true时(暗示是computed属性),那么dirty也是true,需要等待更新
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.getter = expOrFn // 在computed实例化时,将具体的属性值放入this.getter中
    // 省略不相关的代码
    this.value = this.lazy
      ? undefined
      : this.get()
  }

除了日常的初始化外,还有2行重要的代码

this.dirty = this.lazy
this.getter = expOrFn

在computed生成的watcher,会将watcher的lazy设置为true,以减少计算量。

因此,实例化时,this.dirty也是true,标明数据需要更新操作。

我们先记住现在computed中初始化对各个属性生成的watcher的dirty和lazy都设置为了true。

同时,将computed传入的属性值(一般为funtion),放入watcher的getter中保存起来。

我们在来看看第二个关注点defineComputed所代理属性的get方法是什么

// src/core/instance/state.js
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    // 如果找到了该属性的watcher
    if (watcher) {
      // 和上文对应,初始化时,该dirty为true,也就是说,当第一次访问computed中的属性的时候,会调用 watcher.evaluate()方法;
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

当第一次访问computed中的值时,会因为初始化watcher.dirty = watcher.lazy的原因,从而调用evalute()方法,evalute()方法很简单,就是调用了watcher实例中的get方法以及设置dirty = false,我们将这两个方法放在一起

// src/core/instance/state.js
evaluate () {
  this.value = this.get()
  this.dirty = false
}
  
get () {  
// 重点1,将当前watcher放入Dep.target对象
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 重点2,当调用用户传入的方法时,会触发什么?
    value = this.getter.call(vm, vm)
  } catch (e) {
  } finally {
    popTarget()
    // 去除不相关代码
  }
  return value
}

在get方法中中,第一行就调用了pushTarget方法,其作用就是将Dep.target设置为所传入的watcher,即所访问的computed中属性的watcher,

然后调用了value = this.getter.call(vm, vm)方法,想一想,调用这个方法会发生什么?

this.getter 在Watcher构建函数中提到,本质就是用户传入的方法,也就是说,this.getter.call(vm, vm)就会调用用户自己声明的方法,那么如果方法里面用到了 this.data中的值或者其他被用defineReactive包装过的对象,那么,访问this.data.或者其他被defineReactive包装过的属性,是不是就会访问被代理的该属性的get方法。我们在回头看看

get方法是什么样子的。

注意:我讲了其他被用defineReactive,这个和后面的vuex有关系,我们后面在提

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 这个时候,有值了
      if (Dep.target) {
        // computed的watcher依赖了this.data的dep
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    }

代码注释已经写明了,就不在解释了,这个时候我们走完了一个依赖收集流程,知道了computed是如何知道依赖了谁。最后根据this.data所代理的set方法中调用的notify,就可以改变this.data的值,去更新所有依赖this.data值的computed属性value了。

那么,我们根据下面的代码,来简易拆解获取依赖并更新的过程

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

初始化 data和computed,分别代理其set以及get方法, 对data中的所有属性生成唯一的dep实例。

对computed中的reversedMessage生成唯一watcher,并保存在vm._computedWatchers中 访问 reversedMessage,设置Dep.target指向reversedMessage的watcher,调用该属性具体方法reversedMessage。

方法中访问this.message,即会调用this.message代理的get方法,将this.message的dep加入reversedMessage的watcher,同时该dep中的subs添加这个watcher

设置vm.message = 'World',调用message代理的set方法触发dep的notify方法’

因为是computed属性,只是将watcher中的dirty设置为true

最后一步vm.reversedMessage,访问其get方法时,得知reversedMessage的watcher.dirty为true,调用watcher.evaluate()方法获取新的值。

这样,也可以解释了为什么有些时候当computed没有被访问(或者没有被模板依赖),当修改了this.data值后,通过vue-tools发现其computed中的值没有变化的原因,因为没有触发到其get方法。

现在在回头看 vuex的使用会有一种豁然开朗的感觉 回看vuex 的设计思路

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Vue的data、computed、watch源码浅谈

    导读 记得初学Vue源码的时候,在defineReactive.Observer.Dep.Watcher等等内部设计源码之间跳来跳去,发现再也绕不出来了.Vue发展了很久,很多fix和feature的增加让内部源码越来越庞大,太多的边界情况和优化设计掩盖了原本精简的代码设计,让新手阅读源码变得越来越困难,但是面试的时候,Vue的响应式原理几乎成了Vue技术栈的公司面试中高级前端必问的点之一. 这篇文章通过自己实现一个响应式系统,尽量还原和Vue内部源码同样结构,但是剔除掉和渲染.优化等等相关的代

  • Vue中computed计算属性和data数据获取方式

    目录 computed计算属性和data数据获取 解决方法一 解决方法二 computed计算属性取对象的值,第一次报错undefined 报错和打印值 解决方案 computed计算属性和data数据获取 获取到数据(对象.数组),截取一部分显示到页面中,用computed计算属性来实现截取数据然后直接输出到页面. <div class="detailBox"> <h1 class="detailHead">{{ActiveData.tit

  • 深入理解Vue Computed计算属性原理

    Computed 计算属性是 Vue 中常用的一个功能,但你理解它是怎么工作的吗? 拿官网简单的例子来看一下: <div id="example"> <p>Original message: "{{ message }}"</p> <p>Computed reversed message: "{{ reversedMessage }}"</p> </div> var vm =

  • Vue中计算属性computed的示例解读

    计算属性 表达式是非常便利的,但是它们实际上只用于简单的运算.在模板中放入太多的逻辑会让模板过重且难以维护,所以引入了计算属性computed,将复杂的逻辑放入计算中进行处理,同时computed有缓存功能,防止复杂计算逻辑多次调用引起的性能问题. computed原理 computed的属性reversedMessage在data中会有一个对我们不可见的cacheReversedMessage属性对应 cacheReversedMessage的值是根据其绑定的data中的message来决定的

  • vue中的computed 和 vm.$data 原理解析

    目录 官方一个最简单的例子如下 先看看 initData 这条线 我们具体看看defineReactive的源代码 在初始化computed时,有2个地方需要去关注 我们看看Watcher的构造函数 使用vuex中store中的数据,基本上离不开vue中一个常用的属性computed. 官方一个最简单的例子如下 var vm = new Vue({   el: '#example',   data: {     message: 'Hello'   },   computed: {     //

  • Vue 2.0学习笔记之Vue中的computed属性

    Vue中的 computed 属性称为 计算属性 .在这一节中,我们学习Vue中的计算属性如何使用?记得在学习Vue的模板相关的知识的时候,知道在模板内可以使用表达式,而且模板内的表达式是非常的便利,但这种遍历是有一定的限制的,它们实际上是用于一些简单的运算.也就是说,如果在模板中放入太多的逻辑会让模板过重而且难以维护.咱们先来看一个示例: <div id="app"> <h1>{{ message.split('').reverse().join('') }}

  • 深入浅析Vue中的 computed 和 watch

    computed 计算属性:通过属性计算得来的属性 计算属性,是在相关联的属性发生变化才计算,计算过一次,如果相关属性没有变化,下一次就不需要计算了,直接去缓存的值 a:<input type="number" v-model.number="a" /> b:<input type="number" v-model.number="b" /> <!--c:<input type="

  • Vue 中v-model的完整用法及原理

    目录 一. v-model的基本使用 二. v-model的原理 三. v-model结合radio类型使用 四. v-model结合复选框类型使用 五. v-model结合select 六. v-model的修饰符的使用 一. v-model的基本使用 v-model双向绑定,既输入框的value改变,对应的message对象值也会改变,修改message的值,input的value也会随之改变.无论改变那个值,另外一个值都会变化. <!DOCTYPE html> <html lang

  • Vue 中的 computed 和 watch 的区别详解

    目录 computed 注意 应用场景 watch 总结 computed computed 看上去是方法,但是实际上是计算属性,它会根据你所依赖的数据动态显示新的计算结果.计算结果会被缓存,computed 的值在 getter 执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取 computed 的值时才会重新调用对应的 getter 来计算. 1)下面是一个比较经典简单的案例 <template> <div class="hello"> {{ful

  • 详解vue中的computed的this指向问题

    今天在写vue项目时,用到了computed计算属性,遇到了使用箭头函数出现this指向问题,这里记录下 1.箭头函数中的this 箭头函数内部的this是词法作用域,由上下文确定 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象 2.vue中的computed 使用箭头函数 list: () => { console.log(this) } 不使用箭头函数 allFigure: function() { console.log(this) }, 使用get() allFig

  • vue中传参params和data的区别

    目录 1.使用data传参 2.使用params传参 3.总而言之 1.使用data传参 前端请求方式为post import request from '@/utils/request' // 新增banner export function saveBanner(data){ return request({ url:'/system/banner/saveBanner', method:'post', data:data }) } 后端接口接收 /** * 保存导航图 * * @param

  • Python 中 -m 的典型用法、原理解析与发展演变

    在命令行中使用 Python 时,它可以接收大约 20 个选项(option),语法格式如下: python [-bBdEhiIOqsSuvVWx?] [-c command | -m module-name | script | - ] [args] 本文想要聊聊比较特殊的"-m"选项: 关于它的典型用法.原理解析与发展演变的过程. 首先,让我们用"--help"来看看它的解释: -m mod run library module as a script (ter

  • python中的函数递归和迭代原理解析

    这篇文章主要介绍了python中的函数递归和迭代原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.递归 1.递归的介绍 什么是递归? 程序调用自身的编程技巧称为递归( recursion).递归做为一种算法在程序设计语言中广泛应用. 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大

  • Vue中的computed属性详解

    目录 插值表达式 methods computed 总结 今天来说说vue中的计算属性computed,为了更好的理解计算属性的好处,我们先通过一个案例来慢慢 了解计算属性,有如下案例:定义两个输入框以及一个span标签,span标签中的内容为两个输入框中的值,span标签中的内容随着输入框中的内容变化而变化 插值表达式 我们先用插值表达式的方法来实现这一效果 <body> <div id="app"> 姓: <input type="text&

随机推荐