看别人实现一个Observable

最近在看Rx相关的东西,刚好看到别人实现的一个Observable,一开始看这个代码的时候还是有点费劲,这里就记录一下这个代码的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var observable = new Observable(function(observer){
observer.next(1);
observer.next(2);
observer.complete();
})

var observer = {
next: function (value) {
console.log(value)
},
complete: function () {
console.log('complete!')
}
}

observable.subscribe(observer)
//1
//2
//complete!

现在我们来做最简单的Observable的实现,通过上面的代码我们知道Observable有一个订阅的方法subscribe

1
2
3
4
5
6
7
8
class Observable {
constructor(callFn){
this.callFn = callFn
}
subscribe(observer){
this.callFn(observer)
}
}

好了,运行一下没问题。真的没问题了吗,我们看一下下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var observable = new Observable(function(observer){
observer.next(1);
observer.next(2);
observer.complete();
observer.error();
observer.next(3);
})

var observer = {
next: function (value) {
console.log(value)
},
complete: function () {
console.log('complete!')
},
error: function () {
console.log('error!')
}
}

observable.subscribe(observer)
//1
//2
//complete!
//error!
//3

这个结果也是预料之中的了,那要怎么样才能在调用了observer的complete和error方法后不再继续执行下去呢。这里的observer完全是外部传入的,我们要实现上面的功能只能重新包装一下传入的observer,经过包装的observer有一个状态,调用complete跟error方法的时候就改变这个状态。然后每次调用next方法的时候就要先判断这个状态,看是否已经结束或者中间发生了错误。这样我们来重新定义一个新的observer,套用一下React的说法称这个新的observer高阶observer。233333

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Observer {
constructor(oldObserver){
this.isStopped = false
this.oldObserver = oldObserver
}
stop() {
this.isStopped = true
}
next(value){
if(!this.isStopped) {
try {
this.oldObserver.next(value)
} catch(e){
this.stop()
}
}
}
complete(){

}
error(){

}
}

记录一个关于babel的问题

今天帮同事调试一个问题,发现iOS9下面不支持const的关键字。当然这不是什么问题,问题就是为什么babel没有将const转成var呢。起初怀疑的是presets没有配置好。

1
2
3
4
5
6
7
8
9
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],

然而无论怎么配置都还是没有结果。这个时候就不得不怀疑上是不是引用的库有问题。找上那个库,看了一下package.json。

1
2
3
4
{
"main": "dist/xxx.js",
"module": "src/index.js",
}

因为引用是通过模块的引用方式,所以就去看了一下src/index.js的源码。果然就是这地方的问题,index.js里面使用了let,const了,也就是说这个库没有经过babel转译。一般我们在webpack里面的babel-loader配置了exclude: node_modules,这就是为什么src/index.js没有经过babel转译的原因了。这时候只要配置一下include包含这个库就好。
解决完上面的问题之后,还是得反思一下。写这些发布在npm上的东西,无论是main入口还是module入口的js,最好还是提供es5版本的,免得出现不兼容的问题。当然如果明确了不支持那些相对较旧的浏览器这样就没啥问题了。
写这个主要还是为了提醒一下自己,在写库时提供es6模块版本的时候究竟要不要处理成es5再提供给使用者。i

Vue计算属性

1
2
3
4
5
6
7
8
9
10
11
12
new Vue({
data:(){
return {
test: 'a',
}
},
computed: {
testComputed() {
return 'computed ' + this.test
}
}
}

上一次讲到了Vue的响应式原理,这次就可以依赖上次说到的东西展开说一下Vue的计算属性了。
上次说到我们希望监听一个属性变化的时候new Watcher然后把这个订阅者放到属性的订阅者队列里面。那我们计算属性不也算是可以用同样的原理来做吗?其实Vue确实也是这样做的。简单来说就是为每个一计算属性生成Watcher实例,然后这个Watcher执行求值操作,这样就触发了里面依赖属性的getter,这个getter就可以把这个计算属性的Watcher实例放到自己的订阅者队列里面,最后如果依赖的属性发生变化就会触发这个计算属性Watcher实例的update方法重新对计算属性进行求值。

Vue的响应式原理

今天也来写一下vue的响应式原理好让自己更加深刻理解整个过程。
其实大家都知道Vue采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter。在数据变动时发布消息给订阅者,然后触发相应回调。
这里先概括一下整个过程下来是怎样的,然后再一步步看Vue里面的代码是怎么实现这个过程。

在new Vue()里面,有一系列的初始化操作,其中有一个是initState,在initState这个方法里面包含了data,props,computed几个的初始化操作。我们现在先只管data,data的初始化工作是放在initData。initData将data里面的数据进行setter,getter劫持。
如果有多个订阅者想订阅这个属性变化怎么办呢,这个时候就需要一个存放订阅者的队列了。我们希望data里面的每个属性都维护好自己的订阅者队列。这个队列在Vue里面就是Dep啦,在劫持属性setter,getter的时候同时给这个属性添加了一个属于本身的订阅者队列。
看到上面发现观察者(劫持)有了,队列有了,只剩下关键的一点,怎么把订阅者放在队列里面。其实这里面往深层次想一下大概就知道答案肯定是订阅者自己把自己放到队列里面了,因为只有订阅者自己需要订阅哪一个属性。但是这里有一个问题就是队列维护在属性的观察者那里,作用域被隔离了。这里各自为了拿到对方的实例有点绕。概括一下就是,订阅者(Watcher类)实例化的时候会对通过Dep类的一个静态属性target来保存订阅者(Watcher实例)自身,然后对订阅的属性进行求值。我们从上面得知,属性的setter被劫持了,劫持做的东西就是调用队列(Dep实例)的depend方法。所以上面订阅者(Watcher实例)在对属性求值的时候会触发到这个depend方法,depend方法就会把队列(Dep实例)自身实例传回给订阅者(Watcher实例),这样订阅者(Watcher实例)就拿到了属性的订阅队列啦,这样就可以把订阅者(Watcher实例)自己放到队列(Dep实例)里面。到时候如果订阅属性发生变更的时候就可以直接从队列里面把所有订阅者拿出来触发回调了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**src/core/instance/init.js **/

export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
...
...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm) //数据初始化的开端
initProvide(vm)
callHook(vm, 'created')
...
...
}
}

我们现在进去看看这个initState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**src/core/instance/state.js **/

export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm) //如果参数里面有data我们就initData
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

好了,我们来到了关键的地方initData。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**src/core/instance/state.js **/

function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key) //代理data到实例上面
}
}

observe(data, true /* asRootData */) //劫持data上的属性
}
//代理的做法
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}

这里我们可以关注一下proxy这个方法。initData一开始就是将data挂到实例的_data字段上面去。按照上面的例子来就是我们可以通过test._data.a来访问,不过每次都这样访问有点麻烦,所以Vue就帮我们做了一层代理,让我们可以直接test.a这样来访问。这里的做法很简单就是重新在实例上面重新定义这里这些属性。
重头戏来了,observe方法。这里我们就略过一些代码直接看最重要的劫持属性getter,setter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**src/core/observer/index.js **/
export class Observer {
....
....
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
....
....
}
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep() //为当前属性建立自己的订阅队列

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
//如果对当前属性就行求值的时候发现有订阅者,就执行depend方法把订阅队列放回给订阅者
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**src/core/observer/dep.js**/
//Dep的实现
let uid = 0

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 () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

Dep.target = null
const targetStack = []
//指向当前的订阅者
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}

export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}

walk方法遍历data每个属性,执行defineReactive方法,defineReactive方法先为这个属性建立一个存放订阅者的队列(Dep实例),这里我们看一下Dep类的实现。劫持属性setter时做的就是执行Dep实例的depend方法。depend方法很简单,可以看上面的实现。这里的关键其实就是Dep.target跟this,看到Dep.target类型我们大概知道这是一个订阅者,但在哪里赋值的话我们先不管。然后就是这个this了,this是指向Dep实例的。这样我们就能顺利地把对订阅队列交给订阅者了。这样只需要调用订阅队列的addSub方法将自己放进去就行了。这里我们在Watcher里面看看实现就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**src/core/observer/watcher.js**/
...
export default class Watcher {
....省略代码
....省略代码

constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
....省略代码
....省略代码
this.value = this.lazy
? undefined
: this.get()
}
....省略代码
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
...省略代码
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
....省略代码
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}

depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}

}

看Watcher的addDep方法,这个方法就是将订阅者本身放到队列里面的实现。每一个订阅者都有自己的update方法,属性setter劫持就是调用队列里面每一个订阅者的update方法。
上面就是Vue的响应式原理了。这里比较绕的地方就是队列跟订阅者之间怎么互相传递自己的实例给对方使用而已。我们很简单的概括就是每个数据属性都有一个存放订阅自己的订阅者的队列,但是这个队列作用域与订阅者之间做了一个隔离,所以Vue就用了互相传递的方式将对方传给自己使用。就是通过一个类似全局的属性来存放当前的订阅者,然后属性的队列拿到这个订阅者之后将自己传入到这个订阅者里面,最后订阅者拿到了队列就又把自己放入队列中。
其实上面说了这么多,大家都发现这里面还缺乏了一个很关键的环境。就是怎么把订阅者跟Dep.target关联起来呢。其实就很简单就是new Watcher的时候对订阅的属性进行求值,求值的过程当作顺便把Dep.target关联到自身。求值的时候调用了Watcher类的get方法,get方法里面pushTarget(this)就是做这件事情的。

最后给出一个大家都看过的例子,然后用这个例子结合来最后说明一下整个情况。

1
2
3
4
5
6
7
const test = new Vue({
data:{
a:1
}
})
new Watcher(test, "a", (value)=>console.log(value))
test.a = 2

initData阶段: 为a生成一个订阅者队列,对a的getter,setter进行劫持。getter做的事情就是将自己的订阅者队列暴露给外部的订阅者

new Watcher阶段:将实例复制给Dep.target,然后对a进行求值操作,触发getter方法,然后把实例放进去上面暴露出来的订阅者队列,将匿名回调方法放在update方法里面以进行统一调用

test.a = 2阶段:触发setter方法,调用队列的notify方法,通知队列里面所有订阅者数据发生了变化,然后循环队列里面的订阅者,执行他们的update方法实现回调。

这一篇就说明一下Vue响应式的基本原理。

JavaScript里this的问题

其实这个问题已经有很多资料了。今天想记录一下的是一个修正this指向的奇技淫巧。
今天同事问了一个问题,就是发现webpack打包出来的东西里面有很多函数是这样子调用的。

1
(0, xx.defalut)()

一开始我也是懵了,想着这样调用跟xx.default()这样直接调用有什么区别呢。
后来看了一下发现区别还是很大的。我们都知道在JS里面,this的值是在函数调用的时候才确定的。看一下下面的例子

1
2
3
4
5
6
7
8
9
var test = {
testFn: function(){
console.log(this)
}
}
test.testFn() //this 指向 test

var tempFn = test.testFn
tempFn() //this 指向全局对象

其实(0, test.testFn)()就是跟上面调用tempFn的结果是一样的。如果再留意一下webpack打包出来的代码,像(0, test.testFn)()这样调用的基本都是在源码里面调用了全局方法。这样就需要把方法里面的this修正会指向全局对象。

练车

        今天终于结束了地狱般的练车生活。之所以称作为地狱般,是因为练车打乱了自己的生活节奏。作为一条经常一两点钟睡觉的老咸鱼来说,因为生物钟的原因经常躺在床上滚到一两点还睡不着,然后六点多就起来去练车,实在是太痛苦了。就在今天,属于自己的节奏终于回来了,啊哈哈哈。
        科目二考完之后一直拖了大半年才开始学科目三,懒是其中一个原因。更多的是被科目二的教练给恶心到了,以后驾校的教练都像那个人一样。不对那个人不应该称之为教练,因为他没有资格。感觉那个人就是一个社会蛀虫,只会让学员给他好处,又不用心教学员。就是这个人导致了心里有点排斥去练车,心想着真是花钱找人骂。还好的是科目三的教练很好,不会让学员给他好处。上车练习的时候认真教导,出错也比较耐心的指导,而不是骂你。
        反正这几天辛苦下来也算是有个好结果了。之前很多人都说后悔没有在学校去学车,偶尔我也有这样的感想。后面想了一下,觉得练车这个事情在哪个时间段去学都是会打乱原来的生活节奏的了,只是看成本问题了。很多人觉得在学校的时间成本没有出来工作后的高,但是呢在学校的时间也很宝贵的啦.(例如用来谈恋爱)
        最后就想写写身体的问题了。前段时间一直晚睡早起加上这段时间的练车,结果还是导致了身体出了一点问题。休息不足导致免疫力下降,经历了一场感冒,然后每天起来后发现心脏不是很舒服。所以还是得纠正一下自己的作息时间,千万不能把自己的身体搞垮了。今天也早点睡觉吧。

Hello

买了域名很长一段时间了,今天才开始用起来。很多东西还是得记下来才有积累。