有些同学可能不知道state是什么,可能还会有疑问,这个跟vuex中的state是不是有啥联系?
在vue文档当中没有在任何地方提到过关于 state
这个单词,所以同学们发蒙是正常的,不用担心
所以在一开始我先说说state是什么以及它都包含哪些内容。
state 是源码当中的一个概念,State中包含了大家非常熟悉的 Props
、 Methods
、 Data
、 Computed
,vue内部把他们划分为state中方便管理
所以本篇文章会详细介绍State中这四个大家常用的api的内部是怎样工作的
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 跟 methods 类似,但是比 methods 高级点,主要高级在两个地方, proxy
和 observe
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 是用来观察数据变化的,先看一段源码:
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 其实做了两件事
this.$options.data
中的数据可以在 this 中访问 计算属性在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 提供了父子组件之间传递数据的能力,在本文讲的vue 1.x.x 版本中,props分三种类型 静态
、 一次(oneTime)
、 单向
、 双向
我们先说静态,什么是静态props?
静态props就是父组件把数据传递给子组件之后,就不在有任何联系,父组件把数据改了子组件中的数据不会变,子组件把数据改了父组件也不会变,数据传过去后他们俩互相之间就没什么事了~
静态的内部工作原理也比较简单:
组件内会通过 props: ['message']
这样的语法来明确指定子组件组要用到的props,而内部需要做的事就是拿着这些 key
直接通过 node.getAttribute
在当前 el
上读一个 value
,然后将读到的 value
通过 observer
绑到子组件的上下文中,绑定后的 value
与当前组件内的 data
数据一样
其实与静态差不多,只有一点不同,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在父组件实例中取值然后传递给子组件用的 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的流程图
State中的 Props
、 Methods
、 Data
、 Computed
这四个在实际应用中是非常常用的功能,如果大家能弄明白它内部的工作原理,对日后开发效率的提升会有很大的帮助
如果有不明白的地方,或者意见或建议都可以在下方评论。