深入浅出 - vue1.0之State原理
berwin opened this issue · 8 comments
深入浅出 - vue之State
本文讲的内容是 vue 1.0 版本~
有些同学可能不知道state是什么,可能还会有疑问,这个跟vuex中的state是不是有啥联系?
在vue文档当中没有在任何地方提到过关于state
这个单词,所以同学们发蒙是正常的,不用担心
所以在一开始我先说说state是什么以及它都包含哪些内容。
State
state 是源码当中的一个概念,State中包含了大家非常熟悉的Props
、Methods
、Data
、Computed
,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 其实做了两件事
- 让
this.$options.data
中的数据可以在 this 中访问 - 观察数据的变化做出不同的响应
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的流程图
总结
State中的Props
、Methods
、Data
、Computed
这四个在实际应用中是非常常用的功能,如果大家能弄明白它内部的工作原理,对日后开发效率的提升会有很大的帮助
如果有不明白的地方,或者意见或建议都可以在下方评论。
九五,来个vue2的
@william-xue 哈哈哈,过段时间的,最近我们组的项目没怎么用vue,所以就一直没太深究vue2的实现原理。
学习了,谢谢博主
请教个问题:
computed 的缓存内部逻辑还是不太清楚。
如我下面的例子,msg_1 改变了会触发 getMsg1 重新计算,但getMsg2 不会重新计算。上面也提到了是因为 computed 有对依赖做了缓存。问题是怎么知道 getMsg1 的依赖对应的是 msg_1 呢?还望解答。
var vm = new Vue({
data: { msg_1: '',
msg_2: ''
},
computed: {
getMsg1: function () {
return this.msg_1;
},
getMsg2: function () {
return this.msg_2;
},
}
})
你的书什么时候出版呢。