转载

深入浅出 - vue之State

深入浅出 - vue之State

有些同学可能不知道state是什么,可能还会有疑问,这个跟vuex中的state是不是有啥联系?

在vue文档当中没有在任何地方提到过关于 state 这个单词,所以同学们发蒙是正常的,不用担心

所以在一开始我先说说state是什么以及它都包含哪些内容。

State

state 是源码当中的一个概念,State中包含了大家非常熟悉的 Props 、 Methods 、 Data 、 Computed ,vue内部把他们划分为state中方便管理

深入浅出 - vue之State

所以本篇文章会详细介绍State中这四个大家常用的api的内部是怎样工作的

Methods

Methods 在我们日常使用vue的时候,使用频率可能是最高的一个功能了,那么它的内部实现其实也特别简单,我先贴一段代码

Vue.prototype._initMethods = function () {
  var methods = this.$options.methods
  if (methods) {
    for (var key in methods) {
      this[key] = bind(methods[key], this)
    }
  }
}

在看逻辑之前有几个地方我先翻译一下:

_initMethods 这个内部方法是在初始化Methods时执行,就是上面的流程图中的初始化Methods

this 是当前vue的实例

this.$options 是初始化当前vue实例时传入的参数,举个栗子

const vm = new Vue({
  data: data,
  methods: {},
  computed: {},
  ...
})

上面实例化Vue的时候,传递了一个Object字面量,这个字面量就是 this.$options

清楚了这些之后,我们看这个逻辑其实就是把 this.$options.methods 中的方法绑定到 this 上,这也就不难理解为什么我们可以使用 this.xxx 来访问方法了

Data

Data 跟 methods 类似,但是比 methods 高级点,主要高级在两个地方, proxy 和 observe

proxy

Data 没有直接写到 this 中,而是写到 this._data 中(注意: this.$options.data 是一个函数, data 是执行函数得到的),然后在 this 上写一个同名的属性,通过绑定setter和getter来操作 this._data 中的数据

proxy的实现:

Vue.prototype._proxy = function (key) {
  // isReserved 判断 key 的首字母是否为 $ 或 _
  if (!isReserved(key)) {
    var self = this
    Object.defineProperty(self, key, {
      configurable: true,
      enumerable: true,
      get: function proxyGetter () {
        return self._data[key]
      },
      set: function proxySetter (val) {
        self._data[key] = val
      }
    })
  }
}

observe

observe 是用来观察数据变化的,先看一段源码:

Vue.prototype._initData = function () {
  var dataFn = this.$options.data
  var data = this._data = dataFn ? dataFn() : {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object.',
      this
    )
  }
  var props = this._props
  // proxy data on instance
  var keys = Object.keys(data)
  var i, key
  i = keys.length
  while (i--) {
    key = keys[i]
    // there are two scenarios where we can proxy a data key:
    // 1. it's not already defined as a prop
    // 2. it's provided via a instantiation option AND there are no
    //    template prop present
    if (!props || !hasOwn(props, key)) {
      this._proxy(key)
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        'Data field "' + key + '" is already defined ' +
        'as a prop. To provide default value for a prop, use the "default" ' +
        'prop option; if you want to pass prop values to an instantiation ' +
        'call, use the "propsData" option.',
        this
      )
    }
  }
  // observe data
  observe(data, this)
}

上面源码中可以看到先处理 _proxy ,之后把 data 传入了 observe 中, observe 会把 data 中的key转换成getter与setter,当触发getter时会收集依赖,当触发setter时会触发消息,更新视图,具体可以看之前写的一篇文章 《深入浅出 - vue之深入响应式原理》

这地方可能有一个地方不容易理解,observe 在转换 getter 和 setter 的时候是这样转换的

// 伪代码
function observe(value) {
  this.value = value
  Object.defineProperty(this.value, key, {...
}

但是我们操作数据是代理到 _data 上的,实际上操作的是 _data ,那这个 observe 监听的是 this.value ,好像有点不对劲?后来我才发现有一个地方忽略了。

var data = this._data = dataFn ? dataFn() : {}

其实这个地方是同一个引用, observe 中的 this.value 其实就是 _initData 中的 this._data ,所以给 this.value 添加getter 和 setter 就等于给 this._data 设置 getter 和 setter

总结

总结起来 data 其实做了两件事

  1. 让 this.$options.data 中的数据可以在 this 中访问
  2. 观察数据的变化做出不同的响应

Computed

计算属性在vue中也是一个非常常用的功能,而且好多同学搞不清楚它跟watch有什么区别,这里就详细说说计算属性到底是什么,以及它是如何工作的

简单点说, Computed 其实就是一个 getter 和 setter,经常使用 Computed 的同学可能知道, Computed 有几种用法

var vm = new Vue({
  data: { a: 1 },
  computed: {
    // 用法一: 仅读取,值只须为函数
    aDouble: function () {
      return this.a * 2
    },
    // 用法二:读取和设置
    aPlus: {
      get: function () {
        return this.a + 1
      },
      set: function (v) {
        this.a = v - 1
      }
    }
  }
})

如果不希望Computed有缓存还可以去掉缓存

computed: {
  example: {
    // 关闭缓存
    cache: false,
    get: function () {
      return Date.now() + this.msg
    }
  }
}

先说上面那两种用法,一种 value 的类型是function,一种 value 的类型是对象字面量,对象里面有get和set两个方法,talk is a cheap, show you a code...

function noop () {}
Vue.prototype._initComputed = function () {
  var computed = this.$options.computed
  if (computed) {
    for (var key in computed) {
      var userDef = computed[key]
      var def = {
        enumerable: true,
        configurable: true
      }
      if (typeof userDef === 'function') {
        def.get = makeComputedGetter(userDef, this)
        def.set = noop
      } else {
        def.get = userDef.get
          ? userDef.cache !== false
            ? makeComputedGetter(userDef.get, this)
            : bind(userDef.get, this)
          : noop
        def.set = userDef.set
          ? bind(userDef.set, this)
          : noop
      }
      Object.defineProperty(this, key, def)
    }
  }
}

可以看到对两种不同的类型做了两种不同的操作, function 类型的会把函数当做 getter 赋值给 def.get

而 object 类型的直接取 def.get 当做 getter 取 def.set 当做 setter 。

就是这么easy

但是细心的同学可能发现了一个问题, makeComputedGetter 是什么鬼啊?????直接把 def.get 当做 getter 就好了啊,为毛要用 makeComputedGetter 生成一个 getter ???

嘿嘿嘿

其实这是vue做的一个优化策略,就是上面最后说的缓存,如果直接把 def.get 当做 getter 其实也可以,但是如果当 getter 中做了大量的计算那么每次用到就会做大量计算比较消耗性能,如果有很多地方都使用到了这个属性,那么程序会变得非常卡。

但如果只有在依赖的数据发生了变化后才重新计算,这样就可以降低一些消耗。

实现这个功能我们需要具备一个条件,就是当 getter 中使用的数据发生变化时能通知到我们这里,也就是说依赖的数据发生变化时,我们能接收到消息,接收到消息后我们在进行清除缓存等操作

而vue中具备这项能力的很明显是 Watcher ,当依赖的数据发生变化时 watcher 可以帮助我们接收到消息

function makeComputedGetter (getter, owner) {
  var watcher = new Watcher(owner, getter, null, {
    lazy: true
  })
  return function computedGetter () {
    if (watcher.dirty) {
      watcher.evaluate()
    }
    if (Dep.target) {
      watcher.depend()
    }
    return watcher.value
  }
}

上面就是 makeComputedGetter 的实现原理

代码中 watcher.evaluate() 可以先暂时理解为,执行了 getter 求值的过程,计算后的值会保存在 watcher.value 中。

我们看到求值操作的外面有一个判断条件,当 watcher.dirty 为 true 时会执行求值操作

其实,这就相当于缓存了,求值后的值存储在 watcher.value 中,当下一次执行到 computedGetter 时,如果 watcher.dirty 为 false 则直接返回上一次计算的结果

那么这里就有一个问题, watcher.dirty 何时为 true 何时为 false 呢??

默认一开始是 true ,当执行了 watcher.evaluate() 后为 false ,当依赖发生变化接收到通知后为 true

Watcher.prototype.evaluate = function () {
  // avoid overwriting another watcher that is being
  // collected.
  var current = Dep.target
  this.value = this.get()
  this.dirty = false
  Dep.target = current
}

上面是 evaluate 的实现,就是这么easy~

Watcher.prototype.update = function (shallow) {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync || !config.async) {
    this.run()
  } else {
    ...
  }
}

当 watcher 接收到消息时,会执行 update 这个方法,这个方法因为我们的 watcher 是 lazy 为 true 的所以走第一个判断条件里的逻辑,里面很直接,就是把 this.dirty 设置了 true

这里就又引发了一个问题,我们怎么知道 getter 中到底有哪些依赖,毕竟使用Computed开发的人并不会告诉我们他用到了哪些依赖,那我们怎么知道他使用了哪些依赖?

这个问题非常好

vue在全局弄了一个 Dep.target 用来存当前的watcher,全局只能同时存在一个

当watcher执行get求值的时候,会先把 Dep.target 设为自己,然后在执行 用户写的 getter 方法计算返回值,这时候其实有一个挺有意思的逻辑,data上面我们说过,当数据触发 getter 的时候,会收集依赖,那依赖怎么收集,就是通过全局的 Dep.target 来收集,把 Dep.target 添加到观察者列表中,等日后数据发生变化触发 setter 时 执行 Dep.target 的 notify ,到这不知道大家明白过来没???

就是我先把全局的唯一的一个 Dep.target 设置成我自己,然后用户逻辑里爱依赖谁依赖谁,不管你依赖谁都会把我添加到你依赖的那个数据的观察者中,日后只要这个数据发生了变化,我就把 this.dirty 设置为 true

所以上面看 Watcher.prototype.evaluate 这个代码的逻辑, this.get() 里会设置 Dep.target ,等逻辑执行完了他在把 Dep.target 设置回最初的

到这里关于 Computed 就说完了,在使用上其实它跟 watch 没有任何关系,一个是事件,一个是getter和setter,根本不是同一个性质的东西,但在内部实现上 Computed 又是基于watcher实现的。

Props

props 提供了父子组件之间传递数据的能力,在本文讲的vue 1.x.x 版本中,props分三种类型 静态 、 一次(oneTime) 、 单向 、 双向

静态

我们先说静态,什么是静态props?

静态props就是父组件把数据传递给子组件之后,就不在有任何联系,父组件把数据改了子组件中的数据不会变,子组件把数据改了父组件也不会变,数据传过去后他们俩互相之间就没什么事了~

静态的内部工作原理也比较简单:

组件内会通过 props: ['message'] 这样的语法来明确指定子组件组要用到的props,而内部需要做的事就是拿着这些 key 直接通过 node.getAttribute 在当前 el 上读一个 value ,然后将读到的 value 通过 observer 绑到子组件的上下文中,绑定后的 value 与当前组件内的 data 数据一样

一次(oneTime)

其实与静态差不多,只有一点不同,oneTime 的值是从父组件中读来的,什么意思呢?

静态的值是通过 node.getAttribute 读来的,读完后直接放到子组件里。

而 oneTime 的值是通过 node.getAttribute 先读一个 key ,然后用这个 key 去父组件的上下文读一个值放到子组件里。

所以 oneTime 更强大,因为他可以传递一个用Computed计算后的值,也可以传递一个方法,或什么其他的等等...

单向

单向的意思是说父组件将数据通过props传递给子组件后,父组件把数据改了子组件的数据也会发生变化。

单向props内部的工作原理其实也挺简单的,实现单向props其实我们需要具备一项能力:当数据发生变化时会发出通知,而这项能力就是能够接收到通知。

具备这项能力后,当数据发生变化我们可以得到通知,然后将变化后的数据同步给子组件

而具备这项能力的只有 Watcher ,当数据发生变化时,会通知给 Watcher ,而 Watcher 在更新子组件内的数据。这样就实现了单向props,废话不多说,上代码:

const parentWatcher = this.parentWatcher = new Watcher(
  parent,
  parentKey,
  function (val) {
    updateProp(child, prop, val)
  }, {
    twoWay: twoWay,
    filters: prop.filters,
    // important: props need to be observed on the
    // v-for scope if present
    scope: this._scope
  }
)

解释一下上面代码:

  • parent 是父组件实例
  • parentKey 是父组件中的一个key,也就是传递给子组件的那个key,是通过这个key在父组件实例中取值然后传递给子组件用的
  • Watcher中的第三个参数是一个更新函数,当 parent 组件的 parentKey 发生变化时,执行这个函数,并把新数据传进来
  • 更新函数中的 updateProp 是用来更新prop的,逻辑很简单,写个伪代码
export function updateProp (vm, prop, value) {
  vm[prop.path] = value
}

所以工作原理就是当 parent 中的 parentKey 这个值发生了变化,会执行更新函数,执行函数中拿到新数据把子组件中的数据更新一下

就是这么easy

双向

双向不只是父组件改数据子组件会发生变化,子组件修改数据父组件也会发生变化,实现了父子组件间的数据同步。

双向prop的工作原理与单向的基本一样,只不过多了一个子组件数据变化时,更新父组件内的数据,其实就是多了一个Watcher

self.childWatcher = new Watcher(
  child,
  childKey,
  function (val) {
    parentWatcher.set(val)
  }, {
    // ensure sync upward before parent sync down.
    // this is necessary in cases e.g. the child
    // mutates a prop array, then replaces it. (#1683)
    sync: true
  }
)

其实就是单向prop一个Watcher,双向Prop两个Watcher

const parentWatcher = this.parentWatcher = new Watcher(
  parent,
  parentKey,
  function (val) {
    updateProp(child, prop, val)
  }, {
    twoWay: twoWay,
    filters: prop.filters,
    // important: props need to be observed on the
    // v-for scope if present
    scope: this._scope
  }
)

// set the child initial value.
initProp(child, prop, parentWatcher.value)

// setup two-way binding
if (twoWay) {
  // important: defer the child watcher creation until
  // the created hook (after data observation)
  var self = this
  child.$once('pre-hook:created', function () {
    self.childWatcher = new Watcher(
      child,
      childKey,
      function (val) {
        parentWatcher.set(val)
      }, {
        // ensure sync upward before parent sync down.
        // this is necessary in cases e.g. the child
        // mutates a prop array, then replaces it. (#1683)
        sync: true
      }
    )
  })
}

twoWay 是用来判断当前Prop的类型是单向还是双向用的

下面提供一个关于Props的流程图

深入浅出 - vue之State

总结

State中的 Props 、 Methods 、 Data 、 Computed 这四个在实际应用中是非常常用的功能,如果大家能弄明白它内部的工作原理,对日后开发效率的提升会有很大的帮助

如果有不明白的地方,或者意见或建议都可以在下方评论。

原文  https://github.com/berwin/Blog/issues/13
正文到此结束
Loading...