vue源码阅读(三) - 尝试双向绑定

实现一个简单的双向绑定, 基本包括以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default class MyVue {
constructor(option) {
let vm:any = this
// 将 option 赋值给$option
vm.$options = option
// 初始化 $watch 方法
vm.$watch = function (key, cb) {
new Watcher(vm, key, cb)
}
//初始化option
initOptions(vm)
// 代理程序 vm[option] 获取值时 返回vm._data[option]
for (let key in vm._data) {
proxy(vm, '_data', key)
}
// 调用生命周期函数 created
callHook(vm, 'created')

new Compiler(vm.$options.el, vm)
callHook(vm, 'mounted')
}
}

基本类

在实现的过程中我们需要新建几个class:

  1. Watcher 观察者类 用来监听值的变化
  2. Dep 预定类
    1. 用sub储存所有watcher,以便统一更新
    2. 用Dep.target标记当前正在更新的值
  3. Compiler 编译类 用来绑定视图相关操作

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
import {popTarget, pushTarget} from './dep'
export default class Watcher {
vm: any
cb: Function
expression: any
value: any
// 初始化时传入 vm 表达式 和 回调函数
constructor (vm, expression, cb) {
this.vm = vm
this.cb = cb
this.expression = expression
// 直接调用getVal值获取参数
this.value = this.getVal()
}
getVal () {
pushTarget(this)
let val = this.vm
// 根据表达式获取值
this.expression.split('.').forEach((key) => {
val = val[key]
})
popTarget()
return val
}
addDep (dep) {
// 将当前 watcher 保存到dep.sub中
dep.addSub(this)
}
update () {
// 更新属性值
let val = this.vm
this.expression.split('.').forEach((key) => {
val = val[key]
})
// 调用watcher的回调(如:更新视图)
this.cb.call(this.vm, val, this.value)
}
}

Dep

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
import Watcher from './watcher'
export default class Dep {
static target:Watcher
// 保存页面渲染是新建的所有Watcher
sub:Array<Watcher> = []
addDepend () {
Dep.target.addDep(this)
}
addSub (sub:Watcher) {
this.sub.push(sub)
}
notify () {
// 遍历所有Watcher 更新
for (let sub of this.sub) {
sub.update()
}
}
}
// 维护当前更新watcher
const targetStack = []
export function pushTarget (_target:Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}

export function popTarget () {
Dep.target = targetStack.pop()
}

Compiler 编译类

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
export default class Compiler {
constructor(el, vm) {
// 获取被绑定的Element对象
vm.$el = document.querySelector(el)
this.replace(vm.$el, vm)
}

replace(frag, vm) {
// 遍历所有子节点
Array.from(frag.childNodes).forEach((node: any) => {
let txt = node.textContent;
// 双向绑定的正则
let reg = /\{\{(.*?)\}\}/g; // 正则匹配{{}}

if (node.nodeType === 3 && reg.test(txt)) { // 即是文本节点又有大括号的情况{{}}
let arr = RegExp.$1.split('.');
let val = vm;
arr.forEach(key => {
val = val[key];
});
// 用trim方法去除一下首尾空格
node.textContent = txt.replace(reg, val).trim();
vm.$watch(RegExp.$1, function (newVal) {
console.log('RegExp.$1', RegExp.$1)
node.textContent = txt.replace(reg, newVal).trim();
})
}
if (node.nodeType === 1) { // 元素节点
let nodeAttr = node.attributes; // 获取dom上的所有属性,是个类数组
Array.from(nodeAttr).forEach((attr: any) => {
let name = attr.name;
let exp = attr.value;
// 如果数组中包含v- 赋值value
if (name.includes('v-')) {
node.value = vm[exp];
// 监听变化
vm.$watch(exp, function (newVal) {
console.log('exp', exp)
node.value = newVal;
});

// 监听 input 事件, 更新值
node.addEventListener('input', e => {
let newVal = e.target.value;
let arr = exp.split('.')
// 更新 vm 里面的值, 触发双向绑定更新页面上其他使用该值的地方
let val = vm;
arr.forEach((key, i) => {
if (i === arr.length - 1) {
val[key] = newVal
return
}
val = val[key];
});
});
}
});
}

// 如果还有子节点,继续递归replace
if (node.childNodes && node.childNodes.length) {
this.replace(node, vm);
}
})
}
}

有了这几个类, 我们的双向绑定基本就实现了, 下面看下具体是怎么使用这几个累的

构造函数

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
export default class MyVue {
constructor(option) {
console.log("option",option)
let vm:any = this
// 将 option 赋值给$option
vm.$options = option
vm.$watch = function (key, cb) {
new Watcher(vm, key, cb)
}
//初始化option
vm._data = vm.$options.data
// observer数据劫持
observer(vm._data)

// 初始化生命周期函数
['created','mounted'].forEach(hook => {
vm.$options[hook] = vm.$options[hook] || function () {}
})

for (let key in vm._data) {
// 代理程序 vm[option] 获取值时 返回vm._data[option]
Object.defineProperty(target, key, {
configurable: true,
get: function proxyGetter () {
return target[sourceKey][key]
},
set: function proxySetter (newVal) {
target[sourceKey][key] = newVal
}
})
}
// 调用生命周期函数 created
callHook(vm, 'created')

new Compiler(vm.$options.el, vm)
callHook(vm, 'mounted')
}
}

function callHook (vm, hook) {
const handlers = vm.$options[hook]
if (handlers) {
handlers.call(vm)
}
}

这样一个简单的仿vue的双向绑定就实现了,完整代码可见https://github.com/snow-snow/myVue