数据绑定的功能基本实现了,但也很明显存在很多问题,
首先,第一个问题,现在是将属性名作为了事件名来实现数据的监听,假设存在name
和person.name
,我们是将name
传入渲染函数,对节点遍历查找“指令的值”,将其与name
进行对比,符合就是找到了,但是很明显'person.name' !== 'name'
,所以无法实现双向绑定;第二个问题当然是数组类型的处理;
参考 Vue 源码(v0.10)来解决这些问题。
emitter.js
首先查看 Vue 使用的注册事件及响应事件的类。下面是与我们之前的dep.js
的 API 进行对比:
- addListeners => on
- notify => emit
- removeListener => off
- listeners => _cbs
多出一个this._ctx
,执行上下文,会在执行函数时作为第一个参数传入。
Vue 中的该文件,和通用的自定义事件类库很相似,可能接口名会不同。所以一次学习,终身受益~
使用该文件来替代之前的dep.js
文件,熟悉用法。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// ./scripts/emitter.js
var slice = [].slice
export default class Emitter{
constructor(ctx) {
this._ctx = ctx || this
}
on(event, fn) {
this._cbs = this._cbs || {}
;(this._cbs[event] = this._cbs[event] || []).push(fn)
return this
}
once(event, fn) {
this._cbs = this.cbs || {}
var self = this
function on() {
self.off(event, on)
fn.apply(this. arguments)
}
on.fn = fn
this.on(event, on)
return this
}
off(event, fn) {
this._cbs = this._cbs || {}
// all
if (!arguments.length) {
this._cbs = {}
return this
}
// specific event
var callbacks = this._cbs[event]
if (!callbacks) return this
// remove all handlers
if (arguments.length === 1) {
delete this._cbs[event]
return this
}
// remove specific handler
var cb
for (var i = 0; i < callbacks.length; i++) {
cb = callbacks[i]
if (cb === fn || cb.fn === fn) {
callbacks.splice(i, 1)
break
}
}
return this
}
emit(event, a, b, c) {
this._cbs = this._cbs || {}
var callbacks = this._cbs[event]
if (callbacks) {
callbacks = callbacks.slice(0)
for (var i = 0, len = callbacks.length; i < len; i++) {
callbacks[i].call(this._ctx, a, b, c)
}
}
return this
}
applyEmit(event, a, b, c) {
this._cbs = this._cbs || {}
var callbacks = this._cbs[event], args
if (callbacks) {
callbacks = callbacks.slice(0)
args = slice.call(arguments, 1)
for (var i = 0, len = callbacks.length; i < len; i++) {
callbacks[i].apply(this._ctx, args)
}
}
return this
}
}
大概描述一下,on
传入事件名与处理函数,注册事件;emit
传入事件名,触发事件(执行处理函数);off
参数为空时清空所有事件,传入事件名则只取消该事件;once
和on
用法相同,区别在于once
只响应一次,不同于on
可以响应多次;
由于接口和我们自己的dep.js
类似,只需要修改 watch.js 中相应的代码,逻辑完全可以不变。
observer.js
作用与之前的 watch.js
相同,都是对传进来的数据进行处理,添加 get 和 set。但 Vue 在该文件内有对数组类型的处理。大概逻辑如下
observe
是暴露的接口,可以使用该函数对对象类型(虽然数组也是对象,但这里指狭义的对象)进行处理,添加 set 和 get,注册事件与响应事件。
先暂时忽略convert
函数。watch
函数是对数据类型做判断,并且调用不同的添加 get 和 set 的函数。和我们之前的做对比,由于我们并没有对数组类型做处理,所以是这样的:
- observe => useForEachAddGetAndSet
- convertKey => addGetAndSet
重写我们的watch.js
1 | // ./scripts/watch.js |
逻辑是这样,如果传进来的 data 的值都是对象,则每个键都会添加上get 和 set。而且还需要加上触发事件,就是我们之前的notify(key)
而 Vue 的做法是,数据的改变,将会以类似冒泡的形式将“数据改变”层层向上传递。
假设有一个 data:1
2
3
4
5
6
7
8
9
10
11data: {
web: {
title: 'my web',
categories: {
books: {
name: 'nodejs'
}
}
},
url: 'ltaoo.com'
}
Vue 给每个对象添加了属性__emitter__
,该属性的值是 emitter 对象。所以上面的 data 最后会变成这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15data: {
web: {
title: 'my web',
categories: {
books: {
name: 'nodejs',
__emitter: emitter(booksObserver)// 括号实际并不存在,只是便于说明才加上的
},
__emitter__: emitter(categoriesObserver)
},
__emitter__: emitter(webObserver)
},
url: 'ltaoo.com',
__emitter__: emitter(dataObserver)
}
这些 emitter 会观察数据变化,观察“同级”的属性。
data 的 dataObserver 观察 web、url 变化
web 的 webObserver 观察 title、categories 变化
categories 的 categoriesObserver 观察 books 变化
books 的 booksObserver 观察 name 变化
反过来说,name 变化,会通知 booksObserver ,booksObserver 通知 categoriesObserver,categoriesObserver 通知webObserver,webObserver 通知 dataObserver,dataObserver 通知 DOM 节点数据改变了。
我们给我们的代码加上这一功能,就是convert
函数:1
2
3
4
5
6
7
8
9
10
11// ./scripts/watch.js
export function convert(obj) {
if(!obj.__emitter__) {
var emitter = new Emitter()
Object.defineProperty(obj, '__emitter__', {
value: emitter,
enumerable: true,
configurable: true
})
}
}
加上这里后,要在 convertKey 中对 key 做判断,如果是
__emitter__
,则直接跳过,不然会给该值也加上 get 和 set
然后在 get 和 set 中触发1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// ./scripts/watch.js convertKey()函数
// 这里的 emitter 是与之同级的,比如,如果 key 是 name,则 emitter 就是 booksObserver
var emitter = obj.__emitter__
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
//console.log('get value is', value)
emitter.emit('get', key)
return value
},
set: function (newVal) {
//console.log('set newVal is', newVal)
value = newVal
// name 的变化要通知 bookObserver
emitter.emit('set', key)
// 如果新值也是对象,也要对新值调用 observe
init(obj[key])
}
})
肯定要先有emit.on()
注册事件,才能够触发事件。有两个地方可以添加,一是 convert 函数,二是 observe 函数。
按照上面说的,要层层向上触发事件,那就要求emit.on()
可以获取到父级的 emitter (来触发该对象中的事件)。在 convert 函数中,只传入了 obj,并不能实现我们需要的效果。所以在 observe 函数内添加:1
2
3
4
5
6
7
8
9
10
11// ./scripts/watch.js
export function observe(obj, observer) {
if(isWatchable(obj)) {
convert(obj)
watch(obj)
var emitter = obj.__emitter__
emitter.on('set', function() {
observer.emit('set')
})
}
}
由于 observe 是递归函数,每一个属性都会添加__emitter__
并注册set
事件,而该事件的处理函数是触发父级的__emitter__
,所以会层层向上触发 set 事件。将代码完善:1
2
3
4
5
6// ./scripts/viewModel.js
this.emitter
.on('set', function(path) {
console.log(path + ' is setting')
})
observe(this.data, this.emitter)
1 | // ./scripts/watch.js |
1 | // index.js |
可以在浏览器中看到打印出 name is setting
,就表示成功。
事件是向上传递了,而且也可以知道是什么属性发生改变,渲染函数根据传过来的属性值了解到是什么属性发生了改变,但是并没有解决我们一开始提出的问题?1
2
3
4
5<div id="app">
<h2 v-bind="web"></h2>
<input type="text" v-model="web" placeholder="input something">
<input type="text" v-model="person.web"><a href="" v-bind="person.web"></a>
</div>
可以看到同时用到了web
和person.web
,如果只知道是 web 发生了变化,难道两个都要重新渲染吗?而且还要对所有的指令的值做分析,先判断是否有.
,再分割成数组后判断数组中是否有 web,毫无疑问这样做是很有问题的。所以 Vue 使用了“路径”来确定一个属性的位置。
在emit('set')
时,不仅传递 key,还传递 path,而 path 是由发生变化的属性与包含这一属性的属性名构成。所以我们可以知道是person.obj.name
发生变化。
在哪里添加 path 呢?联想到在调用 observe 时,我们将 emitter 作为参数传入,那同样可以将 key 也传入(__emitter__
和 key 是同级的),同样在observer.emit('set')
是调用到了上一级的 emitter,那也在这里把 path 传过去。所以最后代码是这样的:1
2
3
4
5
6
7
8
9
10
11
12
13export function observe(obj, observer, path) {
//
var rawPath = path === '' ? '' : path + '.'
if(isWatchable(obj)) {
convert(obj)
watch(obj)
var emitter = obj.__emitter__
emitter.on('set', function(path) {
console.log(path)
observer.emit('set', rawPath+path)
})
}
}
然后需要将所有调用observe
函数增加 path 参数。然后同样的数据,浏览器打印出:1
person.obj.name is setting
render()
事件可以传播了,最后会传播到我们在 viewModel.js
中实例化的 emitter 监听的 set 事件,所以我们需要在这里调用 render 函数并传入值,告诉 render 函数是什么属性发生了变化。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// ./scripts/viewModel.js
export default class Vue {
constructor (options) {
var vm = this
var data = this.data = options.data;
//console.log(data)
var render = new Render(vm)
// 订阅列表
var emitter = this.emitter = new Emitter()
//
//this.watcher = new Watcher(this.data, this.emitter, this.render)
this.emitter
.on('set', function(path) {
render.renderSingle(path, vm)
})
observe(this.data, this.emitter, '')
}
}
我们在set
事件的处理函数中调用render.renderSingle()
函数并传入path
和vm
,vm
是为了获取到 data
,所以传vm.data
也可。
其他都相同,需要将operation
内指令对应的函数进行修改,增加如果指令对应的值有.
的情况(person.web)。逻辑也简单,将值进行分割,使用循环来获取到值,并赋值给 DOM 节点。
v-model
和v-bind
的逻辑是相同的,只是一个用innerHTML
赋值,一个用value
,所以将其写成一个函数,减少代码的重复:1
2
3
4
5
6
7
8
9
10
11
12
13export function get(obj, key) {
if(key.indexOf('.') > -1) {
//
var ary = key.split('.')
var temp = obj
for(var i = 0; i < ary.length; i ++) {
temp = temp[ary[i]]
}
return temp
}else {
return obj[key]
}
}
所以operation
的代码是这样的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// ./scripts/render.js
this.operation = {
'v-bind': function (node, data, key) {
node.innerHTML = get(data, key)
},
'v-model': function (node, data, key) {
node.value = get(data, key)
},
'v-for': function (node, data, value) {
// console.log('v-for')
var content = '';
data[value].forEach(function (value) {
content += '<li>' + value + '</li>';
})
node.innerHTML = content
}
}
OK,可以正常获取值了,接下来处理赋值,即oninput
事件,将输入框的值赋给 data:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19'v-model': function (node, data, key) {
node.value = get(data, key)
// input event
node.oninput = function () {
if(key.indexOf('.') < 0) {
data[key] = node.value
return
}else {
//
var pathAry = key.split('.')
var temp = data
for(var i = 0; i < pathAry.length -1; i ++) {
temp = temp[pathAry[i]]
}
// console.log(v)
temp[pathAry[i]] = node.value
}
}
},
然后就可以正确实现双向绑定了。至此我们解决了第一个问题。