vue2响应式原理

vue2响应式

vue响应式的根本原理就是发布订阅模式,分别需要观察者和订阅者。

1
2
3
4
5
6
7
8
9
var vm = new MVVM({
el:'#app',
data:{
message:{
greet: 'hello everyone!~',
tip: 'good job!~'
}
}
})

简易vue入口

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
class MVVM {
constructor(options) {
this.$el = options.el
this.$data = options.data
if (this.$el) {
// 数据劫持 把对象的所有属性都改为 get和set方法
new Observer(this.$data)
this.proxyData(this.$data)
// 用数据和元素进行编译
new Compiler(this.$el, this)
} else {
console.log('没能选中要挂载的dom元素(根节点)')
}
}

element(node) {
// 根据用户传入内容,获取dom元素
return node.nodeType == 1 ? node : document.querySelector(node)
}

proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newValue) {
data[key] = newValue
}
})
})
}
}

观察者

观察者的主要工作,就是监听虚拟dom节点数据变化,当数据变化执行对应的方法,其中还包含对新值和老值进行对比,发生变化,就通知订阅者。

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
class Observer {
constructor(data) {
this.observer(data)
}

observer(data) {
// 将data数据的属性改成set和get的形式
if (!data || typeof data !== 'object') {
return
}
// 要将数据一一劫持,获取key和value
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]) // 劫持
this.observer(data[key]) //深度递归劫持
})
}

defineReactive(obj, key, value) {
let that = this;
let dep = new Dep() //每个变化的数据 都会对应一个数组,这个数组是存放所有更新的操作
Object.defineProperty(obj, key, {
enumerable: true, //数据可枚举,for循环可循环出这个值
configurable: true,
get() {
// 当取值时调用的方法
Dep.target && dep.addSub(Dep.target)
return value;
},
set(newValue) {
//当给data属性中设置值时 更改获取的属性的值
if (newValue != value) {
// 这里 this不是实例
that.observer(newValue) //如果是对象继续劫持
value = newValue
dep.notify() //通知所有人 数据更新
}
}
})
}
}

class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
// 获取旧值
this.value = this.get()
}

getVal(vm, expr) {
// message.a.b.c.d
expr = expr.split('.')
return expr.reduce((prev, next) => {
return prev[next]
}, vm.$data)
}

get() {
Dep.target = this
let value = this.getVal(this.vm, this.expr)
Dep.target = null
return value
}

update() {
let newVal = this.getVal(this.vm, this.expr)
let oldVal = this.value
if (newVal != oldVal) {
this.cb(newVal) // 对应watch的回调函数
}
}
}

订阅者

订阅者是属于对象的,每个对象只有一个订阅者。
对象的订阅者会收集观察者(在解析dom中vue语法的时候,调用被监听的对象属性就会生成观察者,也就被当前对象的订阅者进行收集),在观察者发出通知的时候进行更新,但是这里有个问题就是更新对象的某一个属性,所有的观察者都会打补丁,当然这只是简易实现,没有性能优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Dep {
constructor() {
// 订阅的数组
this.subs = []
}

addSub(watcher) {
this.subs.push(watcher)
}

notify() {
this.subs.forEach(watcher => watcher.update())
}
}

补丁工具箱

主要提供针对vue语法糖的识别解析的一些工具函数

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
const CompileUtil = {
getVal(vm, expr) {
// message.a.b.c.d
let props = expr.split('.')
let value = props.reduce((prev, next) => {
return prev[next]
}, vm.$data)
return value
},

getTextVal(vm, expr) {
// {{message.greet}}{{message.tip}}
return expr.replace(/\{\{([^}]+)\}\}/g, (...args) => {
return this.getVal(vm, args[1])
})
// 返回的内容是拼接好的
// hello everyone!~good job!~
},

setVal(vm, expr, value) {
expr = expr.split('.')
// 收敛
return expr.reduce((prev, next, currentIndex) => {
if (currentIndex == expr.length - 1) {
return prev[next] = value;
}
return prev[next]
}, vm.$data)
},

text(node, vm, expr) { //处理文本
let updateFn = this.updater['textUpdater']
let value = this.getTextVal(vm, expr)
expr.replace(/\{\{([^}]+)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
// 如果数据变化了,文本节点需要重新获取依赖的属性更新文本中的内容
updateFn && updateFn(node, this.getTextVal(vm, expr))
})
})
updateFn && updateFn(node, value)
},

model(node, vm, expr) {
// 将值赋给对应的元素
// 添加监控 数据变化 调用cb
let updateFn = this.updater['modelUpdater']
// 这里应该加一个监控 数据变化 应该调用这个watch的callback
new Watcher(vm, expr, (newVal) => {
// 当值变化后会调用cb 将新的值传递过来
updateFn && updateFn(node, this.getVal(vm, expr))
})
node.addEventListener('input', (e) => {
let newValue = e.target.value
this.setVal(vm, expr, newValue)
})
updateFn && updateFn(node, this.getVal(vm, expr))
},

updater: {
// 文本更新
textUpdater(node, value) {
node.textContent = value
},
// 输入框更新
modelUpdater(node, value) {
node.value = value
}
}
}

编译器

编译器的任务就是渲染了,拿着虚拟dom的补丁列表,渲染到页面当中。

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
class Compiler {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm

if (this.el) {
// 如果可以获取到挂载元素,开始编译
// 1. 获取node节点,对节点属性进行遍历,转移到文档碎片
let fragment = this.node2Fragment(this.el)
// 2. 根据vm.$data对节点属性进行编译
this.compile(fragment)
// 3. 将编译完成的文档碎片重新插入根节点
this.el.appendChild(fragment)
}
}

isElementNode(node) {
return node.nodeType === 1;
}

node2Fragment(el) {
/* 创建文档碎片 */
let fragment = document.createDocumentFragment()
let firstChild
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild)
}
return fragment //内存中的节点
}

isDirective(attr) {
/* 判断是否是指令 */
return attr.includes('v-')
}


compile(fragment) {
// 执行编译,需要递归
let childNodes = fragment.childNodes
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) {
// 这里需要编译元素节点
this.compileElement(node)
// 是元素节点,还需要继续深入的检查
this.compile(node)
} else {
// 文本节点
var reg = /\{\{([^}]+)\}\}/g;
if (reg.test(node.textContent)) {
// 如果文本节点有内容就编译
this.compileText(node)
}
}
})
}

compileElement(node) {
let attrs = node.attributes
Array.from(attrs).forEach(attr => {
let attrName = attr.name
// 判断属性是否包含"v-"
if (this.isDirective(attrName)) {
// 取到对应的值放到节点中
let expr = attr.value
let [, type] = attrName.split('-')
// node this.vm.$data // v-model v-text v-html
CompileUtil[type](node, this.vm, expr)
}
})
}

compileText(node) {
// 带{{b.a.c}}
let expr = node.textContent //取文本中的内容
let reg = /\{\{([^}]+)\}\}/g //{{c}}{{b}}{{a}}
if (reg.test(expr)) {
// node this.vm.$data
CompileUtil['text'](node, this.vm, expr)
}
}
}