Vue2源码学习(一)

本文目标:实现一个自己的简单Vue双向绑定demo

在vue中,本地代码会通过compile编译成render函数,在编译的过程中,会收集到有哪些变量,并把这些变量和watcher关联,当浏览器在执行render函数时,会获取到vue实例中定义的变量。而在新建vue实例时,会通过Object.defineProperty来重新设置传入的data,当render函数执行获取变量时,watcher就会触发get方法,将当前water添加到当前vue的dep实例里,在修改数据时,则遍历当前vue实例的dep内部所有的watcher去触发更新。

清楚这些以后,就可以写代码了:

  1. 新建一个myvue文件夹并用 npm init -y初始化

  2. 进入目录 安装webpack(这里是webpack4):

    1
    npm i webpack@4 wecpack-cli@4;
  3. 新建webpack.config.js配置文件,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const path = require('path');
    module.exports = {
    entry:'./main.js',
    output: {
    filename: 'bundle.js',
    path:path.resolve(__dirname, 'dist')
    },
    // devtool:'source-map'
    }

添加index.html引入打包后的js,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<div id="app">
<input type="text" id="a" v-model="text">{{text}}
</div>
<script src="./dist/bundle.js"></script>
</body>

</html>

package.json中配置打包命令,build:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "vue",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"webpack": "^4.44.2",
"webpack-cli": "^4.1.0",
}
}
  1. 新建mian.js文件,如下:
1
2
3
4
5
6
7
8
9
10
11
12
import Vue from './src/Vue';

var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});

for (var i = 0; i < 100; i++) {
vm["text"] = i;//99 批处理
}
  1. main.js中从src目录下引入了Vue,所以我们新建src目录,并在里面新建我们打造vue-demo需要的文件,首先新建Vue.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

import observe from "./Observe";
import Compile from './Complie';
class Vue {
constructor(opitions) {
this.data = opitions.data;
var data = this.data;
observe(data, this);
var id = opitions.el;

var dom = new Compile(document.getElementById(id), this);
document.getElementById(id).appendChild(dom);
}

}

export default Vue;

在Vue.js 文件中我们看到,在初始化vue实例时,首先获取传入的data,并将data和当前实例传给了observe函数,observe函数的作用就是将data对象中的每一个数据绑定到当前实例并用Object.defineProperty改写数据方法。
然后获取到dom中的元素,通过compile编译成可以直接挂载的dom,最后挂载在页面中。接下来新建Observe和Complie.

  1. Observe.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
import Dep from './Dep';

function defineReactive(vm, key, val) {
var dep = new Dep();

Object.defineProperty(vm, key, {

get(){
if(Dep.target){
dep.addSub(Dep.target);
}
return val;
},

set(newval) {
if(newval === val) return;
val = newval;
dep.notify()
}

})
}

export default function observe(obj, vm) {
Object.keys(obj).forEach(key => {
defineReactive(vm, key, obj[key]);
})
}
  1. complie.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
import Watcher from './Watcher'

export default function Compile(node, vm) {

if (node) {

this.$frag = this.nodeToFragment(node, vm);

return this.$frag;
}
}
Compile.prototype = {

nodeToFragment: function (node, vm) {
var self = this;
var frag = document.createDocumentFragment();//创建文档片段,不会真实操作dom,存在于内存中
var child;

while (child = node.firstChild) {

self.compileElement(child, vm);// <input type="text" id="a" v-model="text">

frag.append(child);
}

return frag;
},
compileElement: function (node, vm) {
var reg = /\{\{(.*)\}\}/;

//判断元素节点
if (node.nodeType === 1) {
//<input type="text" id="a" v-model="text">
var attr = node.attributes;
// 解析属性
for (var i = 0; i < attr.length; i++) {

if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue;//text

node.addEventListener('input', function (e) {
vm[name] = e.target.value;
});

new Watcher(vm, node,name, 'value');
}
};
}


//{{text}}
//判断文本节点
if (node.nodeType === 3) {

if (reg.test(node.nodeValue)) {
var name = RegExp.$1;//text
//render
name = name.trim();
new Watcher(vm, node, name, 'nodeValue');
}
}
},
}

compile中主要是通过正则去匹配文档片段中的特殊字符,并拿到这个节点和相应用到的变量,然后new watcher实例。

  1. Watcher.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
import Dep from './Dep';
import Batcher from './Batcher';
let uid = 0;
export default class Watcher {

constructor(vm, node, name, type){
Dep.target = this;
this.name = name;
this.id = uid++;
this.node = node;
this.vm = vm;
this.type = type;

this.update()
Dep.target = null;
}

update(){
this.get()

if(!this.batcher){
this.batcher = new Batcher()
}
this.batcher.push(this);
}

cb(){
this.node[this.type] = this.value;
console.log(this.value);
}

get(){
this.value = this.vm[this.name]
}
}

watcher实例生成时会将Dep.target指向当前watcher实例,当执行update函数时,就会触发cb,cb中获取了当前vue实例中的数据,这样就会触发上面observe中的get方法,在get方法中,将Dep.target也就是当前watcher添加到了vue实例的dep中,然后update执行完毕之后,将Dep.target置为null,这样下次获取这个数据的时候就不会重复添加相同的watcher。而在新的watcer实例生成时,则又会将Dep.target指向watcher。(batcher的作用是批处理,避免不停修改时触发多次)

  1. Dep.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Dep {
constructor(){
this.subs = []
}

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

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

export default Dep;
  1. Batcher.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

export default class Batcher{

constructor(){
this.reset()
}

reset(){
this.has = {};
this.queue = [];
this.waiting = false;
}

push(job){
let id = job.id;
if(!this.has[id]){
this.queue.push(job);
this.has[id] = true;
if(!this.waiting){
this.waiting = true;
Promise.resolve().then(()=>{
this.flush()
})
}
}
}

flush(){

this.queue.forEach(job =>{
job.cb()
})

this.reset()
}


}

  1. 执行npm run build,在浏览器中打开index.html即可看见一件简单的双向绑定demo
查看评论