Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

computed的原理 #20

Open
Jouryjc opened this issue Sep 14, 2018 · 0 comments
Open

computed的原理 #20

Jouryjc opened this issue Sep 14, 2018 · 0 comments

Comments

@Jouryjc
Copy link
Owner

Jouryjc commented Sep 14, 2018

工具推荐

推荐vue/cli的一个工具,零配置直接运行vue文件。

安装方法:

npm install -g @vue/cli-service-global

使用方法:

vue serve
# or
vue serve MyComponent.vue

computed

这篇文章分享一下computed计算属性的实现原理。首先分享一个工作中遇到的code review问题!利用3分钟先看一个例子:

<template>
    <div>
        <p>valueText:{{ valueText }}</p>
        <p>xxx:{{ xxx }}</p>
        <button @click="changeValue">改变 abc 和 xxx 的值</button>
    </div>
</template>

<script>
    export default {
        data () {
            return {
                xxx: false
            }
        },

        computed: {
            valueText () {
                return this.abc && this.xxx;
            }
        },

        created () {
            this.abc = false;
        },

        methods:{
            changeValue () {
                this.abc = true;
                this.xxx = true;
            }
        }
    }
</script>

按钮点击前后分别输出什么?
点击前

点击后
上面的答案是不是如你所想呢?

又或者有为什么点击后valueText不是true的疑问?如果有,就继续往下通过源码分析computed的原理!

// new Vue之后就调用_init方法
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ......
    vm._self = vm

    // 初始化生命周期
    initLifecycle(vm)

    // 初始化事件中心
    initEvents(vm)

    // 初始化渲染
    initRender(vm)
    callHook(vm, 'beforeCreate')

    // 初始化注入
    initInjections(vm) // resolve injections before data/props

    // 初始化状态
    // 初始化 props、data、methods、watch、computed 等属性
    initState(vm)

    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    ......
  }
}

在Vue实例初始化时,注意到有一个 initState 的方法。这个方法就是初始化 props、data、methods、watch、computed 等属性。进入函数体里面继续看:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) // 初始化props
  if (opts.methods) initMethods(vm, opts.methods) // 初始化事件
  if (opts.data) {
    initData(vm)  // 初始化data
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)  // 初始化computed
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

非常简单,就是对一些属性做初始化调用,本文重点是 computed !好,继续看 initComputed 的内容:

const computedWatcherOptions = { computed: true }

// 定义computed属性
function initComputed (vm: Component, computed: Object) {
  // 先声明一个computedWatcher空对象
  const watchers = vm._computedWatchers = Object.create(null)

  // 判断是不是服务端渲染
  const isSSR = isServerRendering()

  // 遍历computed中的对象
  for (const key in computed) {
    const userDef = computed[key]

    // 获取属性的getter方法,这里有两种情况:是函数就直接获取,是对象就获取get值
    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
      )
    }

    // 非服务端渲染,new一个computed watcher实例。
    if (!isSSR) {
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // 不存在Vue实例中,就去定义
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {

      // 开发环境下判断computed的key不能跟data或props同名
      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 watcher 实例的声明,先看 defineComputed :

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {

  // 不是服务端渲染shouldCache为true
  const shouldCache = !isServerRendering() 

  // 如果是用户定义的函数
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop

  // 不是函数,也就是对象,那么获取用户定义的getter和setter方法
  } 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)
}

sharedPropertyDefinition结构如下:

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

sharedPropertyDefinition 的 get 函数也就是 createComputedGetter(key) 的结果:

// 创建计算属性的getter方法
function createComputedGetter (key) {
  return function computedGetter () {

    const watcher = this._computedWatchers && this._computedWatchers[key]

    if (watcher) {
      watcher.depend()  // 收集依赖
      return watcher.evaluate() // 计算属性的值
    }
  }
}

当计算属性被调用时便会执行 get 访问函数,从而关联上观察者对象 watcher 然后执行
wather.depend() 收集依赖和 watcher.evaluate() 计算求值。

OK,下面我们回到computed watcher的实例化:

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

这里的参数意义都比较清晰。 vm 指的 Vue 实例, getter 指的是计算属性的 getter 方法, computedWatcherOptions 即表明这是一个 computed watcher 。

export default class Watcher {
  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.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // 对于computed watcher这里是一个getter函数,赋值给getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }

    // 计算属性执行到这里为true
    if (this.computed) {

      // 和其他watcher的区别,计算属性这里并不会立刻返回求值
      this.value = undefined

      // 创建了该属性的消息订阅器
      this.dep = new Dep()
    } else {
      this.value = this.get()
    }
  }
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 () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

这里Watcher和Dep的关系就是:

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

当获取到计算属性的值时,就会执行getter函数,即

if (watcher) {
      watcher.depend() 
      return watcher.evaluate() 
}

再看到watcher对象中的depend和evaluate方法:

depend () {
    // Dep.target代表的是渲染watcher
    // 在获取计算属性值时,触发其他响应式数据的getter,此时Dep.target代表的是computed watcher
    if (this.dep && Dep.target) {

      // 渲染watcher订阅computed watcher的变化
      this.dep.depend()
    }
  }
evaluate () {
    if (this.dirty) {
      // 这里的get就是computed上的getter函数
      this.value = this.get()
      this.dirty = false
    }

    // 返回getter的值
    return this.value
  }

以上就是计算属性getter的整个过程,这里稍微总结一下:

  • new Vue 或 Vue.extend 实例化一个组件时,data 或 computed 会各自建立响应式系统,Observer 遍历 data 中每个属性设置 get/set 数据拦截。对于 computed 属性,会调用 initComputed 函数
  • 实例化一个computed watcher,其中会注册依赖对象dep
  • 调用计算属性的 watcher 执行 depend() 方法向自身的消息订阅器 dep 的 subs 中添加其他属性的 watcher
  • 调用 watcher 的 evaluate 方法(进而调用 watcher 的 get 方法)让自身成为其他 watcher 的消息订阅器的订阅者,首先将 watcher 赋给 Dep.target,然后执行 getter 求值函数,当访问求值函数里面的属性(比如来自 data、props 或其他 computed)时,会同样触发它们的 get 访问器函数从而将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭 Dep.target 赋为 null 并返回求值函数结果。

计算属性的setter的流程比较简单:

  • 调用 set 拦截函数,然后调用自身消息订阅器 dep 的 notify 方法,遍历当前 dep 中保存着所有订阅者 wathcer 的 subs 数组,并逐个调用 watcher 的 update 方法,完成响应更新。

现在再回头去看第一个问题的答案就一清二楚了。计算属性 valueText 因为 this.abc 为 undefined 并没有收集到 this.abc 的变化。所以点击之后是 valueText 并不会改变。如果将两个属性调换位置,那么就如我们所愿了:

computed: {
    valueText () {
         return this.xxx && this.abc;
    }
}

这里可以得出结论:

computed属性getter中,保证第一次调用时能执行到你所希望的监听的绑定数据。

@Jouryjc Jouryjc changed the title 1 vue中那些懵XX的问题 Sep 15, 2018
@Jouryjc Jouryjc changed the title vue中那些懵XX的问题 computed的原理 Sep 16, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant