精华内容
下载资源
问答
  • Vue中computed原理分析

    2020-09-10 14:40:24
    计算属性是有缓存的,比如某个计算属性C,它依赖data的A,如果没有缓存的话,每次读取C时,C都回去读取A,从而触发A的get。多次触发A的get有时候是一个非常消耗性能的操作。所以Computed必须要有缓存。 computed...

    1.Computed也是响应式的

    Computed是响应式的,读取Computed会触发get,设置Computed会触发set

    2.Computed如何控制缓存

    计算属性是有缓存的,比如某个计算属性C,它依赖data中的A,如果没有缓存的话,每次读取C时,C都回去读取A,从而触发A的get。多次触发A的get有时候是一个非常消耗性能的操作。所以Computed必须要有缓存。

    computed里面控制缓存最重要的一点就是脏数据标记为dirty, dirty是watcher的一个属性。

    • 当dirty为true时,读取computed会重新计算
    • 当dirty为false时,读取computed会使用缓存

    3. 依赖的data发生变化,computed是如何更新的

    页面P依赖计算属性C, 计算属性C又依赖data里面的A, computed更新步骤如下:

    1. 由于C依赖了A, A可以收集到C的watcher
    2. 当A发生变化时,会将watcher的脏数据标记位dirty设置为true
    3. 并且A会收集到页面P的watcher,A通知P进行更新,从而页面P重新读取计算属性C, 由于此时dirty为true,此时的计算属性会重新计算。
    4. computed更新完毕,重新将脏数据标记位dirty设置为false,如果其依赖的A不发生改变,那下次再进入就会读取缓存。

    4. 计算属性C是如何让data中的A收集到页面P的watcher的

    这其实是计算属性中一个非常巧妙的操作。来看一下核心的源码(已简化)

    function createComputedGetter(key) {
        return function() { 
            // 获取到相应 key 的 computed-watcher
            var watcher = this._computedWatchers[key];
            // 如果 computed 依赖的数据变化,dirty 会变成true,
            // 从而重新计算,然后更新缓存值 watcher.value
            if (watcher.dirty) {
                watcher.evaluate();
            }        
            // 这里是 月老computed 牵线的重点,让双方建立关系
            if (Dep.target) {
                watcher.depend();
            }        
            return watcher.value
        }
    }
    
    展开全文
  • computedVue 是很常用的属性配置,它能够随着依赖属性的变化而变化,为我们带来很大便利。那么本文就来带大家全面理解 computed 的内部原理以及工作流程。 在这之前,希望你能够对响应式原理有一些理解,因为...

    前言

    computedVue 中是很常用的属性配置,它能够随着依赖属性的变化而变化,为我们带来很大便利。那么本文就来带大家全面理解 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 选项和 new Vue 传入的 options 选项进行合并
          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)
        }
      }
    }
    复制代码
    1. 实例上定义 _computedWatchers 对象,用于存储“计算属性Watcher
    2. 获取计算属性的 getter,需要判断是函数声明还是对象声明
    3. 创建“计算属性Watcher”,getter 作为参数传入,它会在依赖属性更新时进行调用,并对计算属性重新取值。需要注意 Watcherlazy 配置,这是实现缓存的标识
    4. 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)
    }
    复制代码
    1. sharedPropertyDefinition 是计算属性初始的属性描述对象
    2. 计算属性使用函数声明时,设置属性描述对象的 getset
    3. 计算属性使用对象声明时,设置属性描述对象的 getset
    4. 对计算属性进行数据劫持,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

    1. 在上面的 initComputed 函数中,“计算属性Watcher”就存储在实例的_computedWatchers上,这里取出对应的“计算属性Watcher
    2. watcher.dirty 是实现计算属性缓存的触发点,watcher.evaluate 对计算属性重新求值
    3. 依赖属性收集“渲染Watcher
    4. 计算属性求值后会将值存储在 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
        // 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()
        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 () {
      // 1
      pushTarget(this)
      let value
      const vm = this.vm
      try {
        // 2
        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


    作者:WahFung
    链接:https://juejin.im/post/6844904199596015624
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    展开全文
  • 浅谈 Vue computed 实现原理

    千次阅读 2018-10-10 19:05:26
    虽然目前的技术栈已由 Vue 转到了 React,但从之前使用 Vue 开发的...之前也有断断续续研读过 Vue 的源码,但一直没有梳理总结,所以在此做一些技术归纳同时也加深自己对 Vue 的理解,那么今天要写的便是 Vue 最常...

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

    基本介绍

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

    {{fullName}}
    
    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)或高消耗性能的操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态,而这些都是计算属性无法做到的。

    下面还另外总结了几点关于 computed 和 watch 的差异:

    • computed 是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch 是监听已经存在且已挂载到 vm
      上的数据,所以用 watch 同样可以监听 computed 计算属性的变化(其它还有 data、props)
    • computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值,而
      watch 则是当数据发生变化便会调用执行函数
    • 从使用场景上说,computed 适用一个数据被多个数据影响,而 watch 适用一个数据影响多个数据;

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

    原理分析

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

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

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

    Vue 响应系统,其核心有三点:observe、watcher、dep:

    • observe:遍历 data 中的属性,使用 Object.defineProperty 的 get/set 方法对其进行数据劫持;
    • dep:每个属性拥有自己的消息订阅器 dep,用于存放所有订阅了该属性的观察者对象;
    • 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 函数(其前后也分别初始化了 initData 和 initWatch )并传入两个参数 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)
          }
        }
      }
    }
    

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

    获取计算属性的定义 userDef 和 getter 求值函数

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

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

    计算属性的观察者 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()
        }
      }
    }
    

    为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。 观察 Watcher 的 constructor ,结合刚才讲到的 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 同样精简了部分代码,我们观察 Watcher 和 Dep 的关系,用一句话总结

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

    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 前面的代码可以看到 sharedPropertyDefinition 的 get/set 方法在经过 userDef 和 shouldCache 等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinition 的 get 函数也就是 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 然后执行 wather.depend() 收集依赖和 watcher.evaluate() 计算求值。

    分析完所有步骤,我们再来总结下整个流程:

    • 当组件初始化的时候,computed 和 data 会分别建立各自的响应系统,Observer遍历 data 中每个属性设置
      get/set 数据拦截
    • 初始化 computed 会调用 initComputed 函数
      • 注册一个 watcher 实例,并在内实例化一个 Dep 消息订阅器用作后续收集依赖(比如渲染函数的 watcher
        或者其他观察该计算属性变化的 watcher )
      • 调用计算属性时会触发其Object.defineProperty的get访问器函数
      • 调用 watcher.depend() 方法向自身的消息订阅器 dep 的 subs 中添加其他属性的 watcher
      • 调用 watcher 的 evaluate 方法(进而调用 watcher 的 get 方法)让自身成为其他 watcher
        的消息订阅器的订阅者,首先将 watcher 赋给 Dep.target,然后执行 getter
        求值函数,当访问求值函数里面的属性(比如来自 data、props 或其他 computed)时,会同样触发它们的 get访问器函数从而将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭Dep.target 赋为 null 并返回求值函数结果。
    • 当某个属性发生变化,触发 set 拦截函数,然后调用自身消息订阅器 dep 的 notify 方法,遍历当前 dep 中保存着所有订阅者wathcer 的 subs 数组,并逐个调用 watcher 的 update 方法,完成响应更新。

    原文链接:https://mp.weixin.qq.com/s/igkif-J_BHd1q5mZ7TewCw

    展开全文
  • 对于Vue的响应式,想必大家都有所了解,在Vue响应式数据computed是比较特殊的响应式数据,它们可以监听使用到的数据,数据 改变computed的数据也会重新计算。 今天主要是讨论 computed 实现原理computed在...

    大家好,新人一个,初次写博客还请大家多多关照。

    对于Vue的响应式,想必大家都有所了解,在Vue响应式数据中,computed 是比较特殊的响应式数据,它们可以监听使用到的 数据,数据 改变 computed 的数据也会重新计算。

    今天主要是讨论 computed 实现原理 。 computed 在内部主要是运用 Watcher 和 Dep 构造函数进行收集依赖和派发更新。

    咱们先来看看 Watcher 和 Dep 源码。

      var uid = 0;
    
      /**
       * dep 就是用来给每个数据做个标记,可以用来收集数据和派发更新
       */
      var Dep = function Dep () {
        this.id = uid++; 
    //给每一个 Dep 打一个标记。这里需要说明一下,vm中data的每个数据进行初始化的时候都会调用this.dep = new Dep(),每一个数据都会有dep属性。
    //(通过Observer 构造函数进行数据初始化,这里就不多说了,大家感兴趣可以看一下源码)
        this.subs = []; // 这个subs收集的是watcher,派发更新的时候遍历每个watcher调用update方法
      };
    
      Dep.prototype.addSub = function addSub (sub) {
        this.subs.push(sub);//这个方式是用来收集watcher的(每个computed构建出来的Watcher实例)
      };
    
      Dep.prototype.depend = function depend () {
        if (Dep.target) {
          Dep.target.addDep(this);  // 这里的方法是Watcher原型上的addDep方法,请看下面
        }
      };
    
      Watcher.prototype.addDep = function addDep (dep) {
        var id = dep.id;
        if (!this.depIds.has(id)) {
          this.depIds.add(id); //将依赖的每个dep,添加到 watcher 的 deps集合中,完成数据的收集
          this.depIds.push(dep);
        }
      };
    
      Dep.prototype.notify = function notify () { 
    // 用来出发watcher的更新,每当数据改变,每个数据的dep就会出发motify方法,将收集的watcher逐个进行数据更新,
    //这里就是为什么computed知道依赖改变了,然后就会自动更新。其实不是computed知道依赖改变的,而是依赖改变以后出发computed更新。
        for (var i = 0, l = subs.length; i < l; i++) {
          subs[i].update();
        }
      };
    
      Dep.target = null;
      var targetStack = [];
    
      function pushTarget (target) {
        targetStack.push(target); // 将 Dep 原型 上的 Target 设置为 watcher
        Dep.target = target;
      }
    
      function popTarget () {
        targetStack.pop();
        Dep.target = targetStack[targetStack.length - 1];
      }
      // vm 是 Vue 实例
      // expOrFn 是传过来的数据 get 函数
      // cb 是回调函数
    
      var Watcher = function Watcher (vm,expOrFn,cb,) {
        this.vm = vm;
        this.cb = cb;
        this.id = ++uid$2; // uid for batching
        this.deps = []; // deps 是用来收集  依赖数据节点  的集合
        this.depIds = new _Set();
        this.value = this.get();  // 首次执行get方法(get也就是expOrFn),初次收集依赖是在这个环节
      }; 
    
    
      Watcher.prototype.get = function get () {
        pushTarget(this); // 将 Dep 的 target 设置为当前 watcher,这个函数在 Dep 那块最后面
        var value;
        var vm = this.vm;
        value = this.getter.call(vm, vm);
        popTarget();
        this.cleanupDeps(); 将 Dep 的 target 设置为当前 空,这个函数我会放在后面
        }
        return value
      };
    
      /**
       * 添加依赖的方法,重点!!! 着重看一下 (我进行了一些简化,便于理解)
       */
      Watcher.prototype.addDep = function addDep (dep) {
        var id = dep.id;
        if (!this.depIds.has(id)) {
          this.depIds.add(id);
          this.depIds.push(dep);
        }
        if (!this.depIds.has(id)) {
            dep.addSub(this);
          } //这步是将自身(也就是watcher)添加到dep中的watcher集合,派发数据更新时用到
      };
    
      /**
       * 更新数据的方法,在派发更新的时候会用到。 computed 更新数据的时候,用 dep 的 notify 方法进 
       * 行更新数据,更新数据主要调用的是 run 方法
       */
      Watcher.prototype.update = function update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true;
        } else if (this.sync) {
          this.run();
        } else {
          queueWatcher(this);
        }
      };
    
      /**
       * 在这个阶段主要是运行get方法,拿到数据 (简化以后的代码)
       */
      Watcher.prototype.run = function run () {
        if (this.active) {
          var value = this.get();
          this.value = value 
        }
      };
    
      /**
       * 深度收集依赖,computed 可以收集 computed 数据就是依靠这个方法
       */
      Watcher.prototype.depend = function depend () {
        console.log(this.deps)
        var i = this.deps.length;
        while (i--) {
          this.deps[i].depend(); //注意这里的 depend 方法是 Dep 原型上的方法,不是Watcher 的法
        }
      };
    

    大家可以仔细看一下我的注释,进行了每个步骤的描述,当然都只是我自己的理解,有不对的地方还请大家多多指教。

    现在来细说一下收集依赖的流程。当初始化Vue实例以后, 在初始化 computed 阶段,vue 会对每个 computed 进行运算和收集依赖。当 computed 初始化的时候会依靠当前的 computed 生成一个 Watcher,并且将 getter 方法传入。

    function initComputed (vm, computed) {
        var watchers = vm._computedWatchers = Object.create(null);
        for (var key in computed) {
          var userDef = computed[key];
          var getter = typeof userDef === 'function' ? userDef : userDef.get;
          if (getter == null) {
            warn(
              ("Getter is missing for computed property \"" + key + "\"."),
              vm
            );
          }
            watchers[key] = new Watcher(
              vm,
              getter || noop,
              noop,
              computedWatcherOptions
            );
          }
        )
    }
    

     这时候已经有了watcher实例。 当 watcher 初始化,会调用 get方法(收集依赖在这里),

    Watcher.prototype.get = function get () {
        pushTarget(this); // 将 Dep 的 target 设置为当前 watcher,这个函数在 Dep 那块最后面
        var value;
        var vm = this.vm;
        value = this.getter.call(vm, vm);
        popTarget();
        this.cleanupDeps(); 将 Dep 的 target 设置为当前 空,这个函数我会放在后面
        }
        return value
      };

    get执行得时候会调用pushTarget方法(这个方法就是将当前的全局的 Dep.target 设置成当前运行的 watcher ),然后会进行 getter(),getter() 执行的时候会触发 每个数据 的 get 方法,请看get源码

    Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get: function reactiveGetter () {
            var value = getter ? getter.call(obj) : val;
            if (Dep.target) { //判断当前执行是否为watcher,也可以说是否为computed运算的数据
              dep.depend(); // 收集到watcher里面的deps里面
              if (childOb) {
                childOb.dep.depend();
                if (Array.isArray(value)) {
                  dependArray(value);
                }
              }
            }
            return value
          })

    这里的 get 方法会首先判断是否有 Dep.target,这个判断最终目的是判断当前获取数据的是否是watcher,如果是,就会调用 Dep 的 depend 方法进行收集,这样 watcher 就会把运算的每个数据的dep收集到 自己的 deps 属性中,完成了数据依赖收集。

    咱们再来细说一下Dep 的 depend 方法,这个方法主要是调用的 Watcher 里面的 addDep方法, 这个方法进行的是双向数据收集,也就是说 watcher 会收集 deps, 同样 dep 也会收集 watcher 集合,存到dep 的 subs 属性中, 每当 dep 的数据更新时,就会将subs的每个 watcher 进行 update ,这样就完成了数据更新,这就是 computed 的实现原理。

    简单点说,就是 computed 在运行的时候,首先将全局的 Dep.target 设置当前 computed 的 watcher,然后在运行 computed 代码,里面用到的数据会调用它们自己的 get 方法,在 get 方法里,他们会将自己的 dep.id 存到当前的 Dep.target 里,然后还要将当前的 Dep.target(也就是当前的 watcher) 存到自己的 dep.subs 属性 中,每当自己数据更新触发 set 方法,就会把自己 dep.subs 中的每个watcher 拿出来进行数据更新,从而更新 computed 。

    展开全文
  • 1.初始化 data 和 computed,分别代理其 set 和 get 方法,对 data 的所有属性生成唯一的 dep 实例 2.对 computed 的 属性生成唯一的 watcher,并保存在 vm._computedWatchers 3.访问计算属性时,设置 Dep....
  • vue computed 原理

    2018-04-11 11:18:00
    vue computed 主要依靠数据依赖来更新,这里不展示computed源代码,只展示核心思想。...vue中如果b变化,a也会变化。这儿为了简单,不在展示computed.a的set跟get 1、data中的数据需要使用es5中的O...
  • 虽然目前的技术栈已由 Vue 转到了 React,但从之前使用 Vue 开发的多个项目实际经历来看还是非常愉悦的,Vue 文档清晰规范,api 设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多场景使用 Vue 比 ...
  • 继上一篇:深入理解Vue的watch实现原理及其实现方式 继续讲解Vue的...如果你还不是很理解推荐你先看此文章:彻底搞懂Vue针对数组和双向绑定(MVVM)的处理方式首先来看一波Vue中computed的使用方式:var vm = new Vue(...
  • 基于上一篇(Vue中实现数组) 根据计算属性几个特点设计思路: 1.他的值是一个函数的运行结果; 2.函数里用到的所有属性都会引起计算属性的变化; 计算属性仍然属于Vue响应式实现的一种,本质上还是一个watcher,但是又似乎...
  • 1.Vue中computed 和 watch的区别。 computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。 watch 侦听器 : 更多的是...
  • 浅谈Vue中computed

    2020-05-27 11:03:06
    1、computed特性: 计算属性在使用的时候,要当做普通属性使用就好,不需要加() 只要计算属性这个function内部所... 2.computed原理是什么: 学习最常见听到的一句话就是,computed就是...
  • computed原理Vue中的data定义的参数使用发布订阅模式初始化后,开始解析computed属性 1. 对每个computed属性建立专门的Watcher 2. computed属性方法中访问的每一个data参数,都会调用参数的get方法,这个...
  • vue中computed的实现原理---------需要建立数据依赖搜集,动态计算实现原理 1)问题:计算属性如何与属性建立依赖关系?属性发生变化又如何通知到计算属性重新计算? 如何建立依赖关系?----------利用 ...

空空如也

空空如也

1 2 3 4 5 ... 13
收藏数 252
精华内容 100
关键字:

vue中computed原理

vue 订阅