vue 的双向绑定、虚拟dom、diff算法等等面试常见问题你可能在几年前就学过了,其中有些人可能看过Vue的源码,了解过Vue是如何实现数据监听和数据绑定这些技术的。不过让从零开始实现一个 vue,你可以吗?

模板语法其实早就存在,在Vue发布之前就有了。Vue除了具备基本的模板编译功能外,新增了很多功能属性,比如数据集data、方法集methods、组件集components等等,当然还具备了数据的响应式功能,具备生命周期函数……
我想如果能够从编译功能开始逐步增加这些功能属性,是否可以体会到尤大当初开发Vue的心路历程?或者至少能够更加清楚的理解Vue源码吧。

==简易版Vue基本实现思路==

简易版Vue基本实现思路

在这个背景下,我按照自己的理解决定从0开始,开发一个简版的Vue:Avue。

从类的创建开始

创建一个类,参数为对象options,里面是Vue的各种数据集。这里采用es6语法,出于兼容性考虑的话,可以使用babel做处理。

1
2
3
4
5
class Avue {
constructor(options) {

}
}

具备数据集data、方法集methods和挂载el

1
2
3
4
5
6
7
8
9
10
11
class Avue {
constructor(options) {
const { data,method, el } = options
// 数据集
this.data = data;
// 方法集
this.methods = methods;
// 挂载
this.el = el
}
}

具备一定编译功能

遵循单一职责原则,编译功能单独拿出来,创建编译类。这里的编译可以处理Mustache语法(双大括号)以及事件指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Avue {
constructor(options) {
const { data,methods, el } = options
// 数据集
this.data = data;
// 方法集
this.methods = methods;
// 挂载
this.el = el
// 编译功能
new Compile(this)
}
}

编译类实现。编译类实现了对元素节点和文本节点处理,能够处理其上的Mustache语法和事件指令。

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
class Compile {
constructor(vm) {
this.vm = vm
this.vm.el = document.querySelector(vm.el);
this.compile();
}

compile() {
this.replaceData(this.vm.el);
const documentFragment = this.nodeToFragment(this.vm.el)
this.vm.el.appendChild(documentFragment);
}

nodeToFragment(el) {
let fragment = document.createDocumentFragment();
let child;
while (child = el.firstChild) {
// 将Dom元素移入fragment中
fragment.appendChild(child);
}
return fragment;
}

replaceData(frag) {
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent;
let reg = /\{\{(.*?)\}\}/g;

if (this.isTextNode(node) && reg.test(txt)) {
let replaceTxt = () => {
node.textContent = txt.replace(reg, (matched, placeholder) => {
return placeholder.split('.').reduce((val, key) => {
return val[key];
}, this.vm);
});
};
replaceTxt();
}

if (this.isElementNode(node)) {
Array.from(node.attributes).forEach(attr => {
if (attr.name.startsWith('@')) {
const eventName = attr.name.slice(1);
const methodName = attr.value;
if (methodName in this.vm.methods) {
node.addEventListener(eventName, this.vm.methods[methodName].bind(this.vm));
}
}
});
if (node.childNodes && node.childNodes.length) {
this.replaceData(node);
}
}
});
}
// 元素节点
isElementNode(node) {
return node.nodeType == 1
}
// 文本节点
isTextNode(node) {
return node.nodeType == 3
}
}

注意:这个时候使用Avue,访问data中数据时,需要使用this.data[attr]方式。如果要解决这个问题需要加一层代理,访问代理。在Avue中添加访问代理方法proxyKeys。

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
class Avue {
constructor(options) {
const { data,methods, el } = options

// 数据集
this.data = data;
// 数据集代理
Object.keys(this.data).forEach(key => {
this.proxyKeys(key);
});
// 挂载
this.el = el
// 方法集
this.methods = methods;
// 编译功能
new Compile(this)
}

// 访问代理
proxyKeys(key) {
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get: function proxyGetter() {
return this.data[key];
},
set: function proxySetter(newVal) {
this.data[key] = newVal;
}
});
}
}

具备数据的响应式功能

增加Dep类:用于实现订阅发布模式,订阅Watcher对象,发布Watcher对象

增加Observe类:对数据集data数据进行拦截,在拦截过程中,get保存或订阅Watcher对象,set触发或者发布的Watcher对象

增加observe方法:用于对对象数据深层级进行拦截处理

增加Watcher类:用于触发Observe类数据拦截操作,然后以Dep.target为媒介,将当前Watcher对象保存到Dep对象中

总结:通过Observe类实现了对数据集的拦截,创建Watcher时触发get方法,此时Dep类订阅Watcher;设置数据集数据时,触发set方法,此时Dep类发布Watcher,触发update方法,触发回调函数,触发更新

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
class Avue {
constructor(options) {
...
this.data = options.data;

Object.keys(this.data).forEach(key => {
this.proxyKeys(key);
});
new Observe(this.data)
...
}
}
class Dep {
constructor() {
this.subs = [];
}

addSub(sub) {
this.subs.push(sub);
}

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

class Watcher {
constructor(vm, exp, fn) {
this.fn = fn;
this.vm = vm;
this.exp = exp;
Dep.target = this;
let arr = exp.split('.');
let val = vm;
// 手动获取一次data里面的数据 执行Observe添加方法
arr.forEach(key => {
val = val[key];
});
Dep.target = null;
}

update() {
let arr = this.exp.split('.');
let val = this.vm;
arr.forEach(key => {
val = val[key];
});
this.fn(val);
}
}

class Observe {
constructor(data) {
let dep = new Dep();
for (let key in data) {
let val = data[key];
observe(val);
Object.defineProperty(data, key, {
get() {
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
val = newVal;
observe(newVal);
dep.notify();
}
});
}
}
}

function observe(data) {
if (!data || typeof data !== 'object') return;
return new Observe(data);
}

编译类Compile增加Watcher,更新函数作为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
class Compile {
...
replaceData(frag) {
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent;
let reg = /\{\{(.*?)\}\}/g;

if (this.isTextNode(node) && reg.test(txt)) {
let replaceTxt = () => {
node.textContent = txt.replace(reg, (matched, placeholder) => {
// 增加Watcher
new Watcher(this.vm, placeholder, replaceTxt);
return placeholder.split('.').reduce((val, key) => {
return val[key];
}, this.vm);
});
};
replaceTxt();
}

if (this.isElementNode(node)) {
Array.from(node.attributes).forEach(attr => {
if (attr.name.startsWith('@')) {
const eventName = attr.name.slice(1);
const methodName = attr.value;
if (methodName in this.vm.methods) {
node.addEventListener(eventName, this.vm.methods[methodName].bind(this.vm));
}
}
});
if (node.childNodes && node.childNodes.length) {
this.replaceData(node);
}
}
});
}
}

具备计算属性功能

Avue增加用于处理计算属性功能。

实际就是将计算属性数据集computed打平,将所有计算属性添加到Avue实例对象上,同时进行拦截。使用计算属性数据时,执行get方法,执行计算属性函数。这里只是实现了基本的计算属性功能。
打平操作也说明了Vue的计算属性computed和数据集data不能有同名属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Avue {
constructor(options) {
....
const { computed } = options
this.computed = computed;
Object.keys(this.computed).forEach(key => {
Object.defineProperty(this, key, {
get: () => {
return this.computed[key].call(this);
}
});
});
...
}

具备watch功能

Avue增加用以处理watch数据集的功能。

遍历watch集合,创建Watcher对象,实际就是前面的发布订阅模式。不同的是此时的回调函数是watch里面的方法。这里也只是实现了基本功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Avue {
constructor(options) {
...
const { watch } = options
this.watch = watch;
// 处理watch
this.initWatch()
}
initWatch() {
for (let key in this.watch) {
new Watcher(this, key, this.watch[key]);
}
}
}

具备过滤器功能

增加过滤器功能。

过滤器功能就比较简单了,可以说是一种语法糖或者面向切面编程。需要拦截双大括号,判断是否有过滤器标识。然后在编译类更新内容函数replaceTxt里面添加部分代码。

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
class Compile {
...
replaceData(frag) {
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent;
let reg = /\{\{(.*?)\}\}/g;

if (this.isTextNode(node) && reg.test(txt)) {
let replaceTxt = () => {
node.textContent = txt.replace(reg, (matched, placeholder) => {
// 判断过滤器是否存在
let key = placeholder.split('|')[0].trim();
let filter = placeholder.split('|')[1];
if (filter) {
let filterFunc = this.vm.filters[filter.trim()];
if (filterFunc) {
new Watcher(this.vm, key, replaceTxt);
return filterFunc.call(this.vm, key.split('.').reduce((val, k) => {
return val[k];
}, this.vm));
}
} else {
// 增加Watcher
new Watcher(this.vm, placeholder, replaceTxt);
return placeholder.split('.').reduce((val, key) => {
return val[key];
}, this.vm);
}
});
};
replaceTxt();
}
});
}
}

具备组件注册功能

遵循单一职责原则,增加组件类。

组件本质上就是一个特殊的Avue实例。这里参照Vue,在Avue中创建静态方法extend,用以生成创建组件的构造函数。

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
class Avue {
constructor(options) {
this._init(options)
}

...
_init(options){
if(!options){
return
}
const { data, methods, el, computed, components, watch, filters, template } = options
this.data = data;
this.methods = methods;
this.computed = computed;
this.watch = watch;
this.filters = filters;
this.components =components;
this.template = template
this.el = el
Object.keys(this.data).forEach(key => {
this.proxyKeys(key);
});
// 注意如果没有计算属性会报错
this.computed && Object.keys(this.computed).forEach(key => {
Object.defineProperty(this, key, {
get: () => {
return this.computed[key].call(this);
}
});
});
new Observe(this.data)
this.initWatch()
// 创建编译模板以及编译赋值
new Compile(this);
}

// 增加extend方法
static extend(options) {
const Super = this;

// 闭包保存options
const Sub = (function (){
return function VueComponent() {
let instance = new Super(options);
Object.assign(this, instance);
}
})()
// 创建一个基于Avue的构造函数
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
// 合并参数
Sub.options = Object.assign({}, Super.options, options);
return Sub;
}
}

编译类Compile增加处理自定义组件功能。

着重说明一下,这里自定义组件使用的模板是template属性。

Avue本身没有使用template属性,而采用的是查询挂载el下面的dom结构。所以自定义组件的template需要单独处理。增加单独处理方法handleTemplate。然后replaceData方法里增加创建组件功能。

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
class Compile {
constructor(vm) {
this.vm = vm
// 兼容处理组件
this.vm.el = this.handleTemplate(vm)
this.compile();
}
handleTemplate(vm){
if(vm.el && typeof vm.el === 'string') {
return document.querySelector(vm.el)
}
// 将字符串转为dom
const div = document.createElement('div')
div.innerHTML = vm.template
return div.firstChild
}
...
replaceData(frag) {
Array.from(frag.childNodes).forEach(node => {

...
if (this.isElementNode(node)) {
...
let nodeName = node.nodeName.toLowerCase();

// 如果存在components 则创建组件
if (this.vm.components && this.vm.components[nodeName]) {
let ComponentConstructor = this.vm.components[nodeName];
let component = new ComponentConstructor();
node.parentNode.replaceChild(component.el, node);
}
if (node.childNodes && node.childNodes.length) {
this.replaceData(node);
}
}
});
}
// 元素节点
isElementNode(node) {
return node.nodeType == 1
}
// 文本节点
isTextNode(node) {
return node.nodeType == 3
}
}

组件注册功能给我的启发比较大。之前一直不理解为什么Vue可以做到局部更新。写了简版的Vue,明白组件实际是特殊的Vue实例,组件本身就有一套更新机制,组件本身就是局部更新。

具备Vuex功能

Vuex本质上Vue的实例属性,而且只能是Vue而不能是组件的,否则就不能全局使用了。将其独立出来,创建Store类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Store {
constructor(options) {
this.state = options.state;
this.mutations = options.mutations;
this.actions = options.actions;
}

commit(type, payload) {
this.mutations[type](this.state, payload);
}

dispatch(type, payload) {
this.actions[type]({ commit: this.commit.bind(this), state: this.state }, payload);
}
}

使用时创建实例,作为Avue实例的参数。同时需要将store中的state数据加入Observe类中,让其具备响应式特性。

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
let store = new Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
increment(context) {
context.commit('increment');
}
}
});

...
class Avue {
constructor(options) {
this._init(options)
}
_init(options){
if(!options){
return
}
const { data, methods, el, computed, components, watch, filters, template, store } = options
...
this.store = store

Object.keys(this.data).forEach(key => {
this.proxyKeys(key);
});
this.computed && Object.keys(this.computed).forEach(key => {
Object.defineProperty(this, key, {
get: () => {
return this.computed[key].call(this);
}
});
});
new Observe(this.data)
// 响应式功能
this.store && new Observe(this.store.state)

this.initWatch()
// 创建编译模板以及编译赋值
new Compile(this);
}
}

具备插件注册功能

Avue增加静态方法use,用于接收插件。实际就是运行插件里的install方法,将不同种类的插件加到Avue上,以原型的形式、静态数据形式或其他。

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 Avue {
constructor(options) {
// ...
this.plugins = [];
}

static use(plugin) {
plugin.install(this);
}
}
const MyPlugin = {
install(avue) {
// 在这里添加你的插件代码
// 你可以添加全局方法或属性
avue.myGlobalMethod = function () {
console.log('This is a global method');
}
// 添加实例方法
avue.prototype.$myMethod = function (methodOptions) {
// 一些逻辑……
}
}
}

new Avue({
el: '#app',
data: {
message: 'Hello Vue!'
}
});

Avue.use(MyPlugin);

具备生命周期函数

生命周期就简单了,生命周期是切面编程的体现。只需要在对应时机或者位置加上生命周期函数就可以了。

Avue类增加处理生命周期函数方法_callHook,以及Avue实例增加对应的生命周期方法beforeCreate和mounted。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Avue {
constructor(options) {
this._init(options)
}

_callHook(lifecycle) {
this.$options[lifecycle] && this.$options[lifecycle].call(this);
}

_init(options){
if(!options){
return
}
...
// 创建编译模板以及编译赋值
this._callHook('beforeCreate');
new Compile(this);
this._callHook('mounted')
}
}

编译类Compile增加created生命周期

1
2
3
4
5
6
7
8
class Compile {
constructor(vm) {
...
this.compile();
this.vm._callHook('created');
}
}

测试用例和所有功能代码

测试用例

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
let MyComponent = Avue.extend({
template: '<div>这是一个组件{{message}}</div>',
data:{
message: 'Hello, Component!'
}
// ...
})
let store = new Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
increment(context) {
context.commit('increment');
}
}
});
const avue = new Avue({
el: '#app',
store,
data: {
name: 'canf1oo'
},
components: {
'my-component': MyComponent
},
computed: {
computedName(){
return this.name + '我是计算属性'
}
},
filters: {
addAvue(val){
return val + 'Avue'
}
},
watch: {
name(){
console.log('测试室测试试试')
},
computedName(){
console.log('测试室测试试试12232323')
}
},
methods: {
clickMe() {
this.name = 'click me'
this.store.commit('increment');
this.$plugin()
}
},
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
mounted() {
console.log('mounted')
},
});
const MyPlugin = {
install(avue) {
// 在这里添加你的插件代码
// 你可以添加全局方法或属性
avue.myGlobalMethod = function () {
console.log('This is a global method');
}
// 添加实例方法
avue.prototype.$plugin = function (methodOptions) {
// 一些逻辑……
console.log('我是插件')
}
}
}

Avue.use(MyPlugin)

Avue测试模板

1
2
3
4
5
6
7
8
9
<div id="app">
<button @click="clickMe">{{name}}</button>
<button @click="clickMe">{{name | addAvue}}</button>

<button>{{computedName}}</button>
<span>{{store.state.count}}</span>
<my-component></my-component>
</div>

代码地址:github

整体上没有路由功能,所以是一个静态的非单页面的简版Vue。距离真实的Vue还差很远,比如props,比如render函数,比如插槽,比如作用域插槽,比如vdom等等。感兴趣的可以继续添加。