有些同学可能不知道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 这四个在实际应用中是非常常用的功能,如果大家能弄明白它内部的工作原理,对日后开发效率的提升会有很大的帮助
如果有不明白的地方,或者意见或建议都可以在下方评论。