本文目标:实现一个自己的简单Vue双向绑定demo
在vue中,本地代码会通过compile编译成render函数,在编译的过程中,会收集到有哪些变量,并把这些变量和watcher关联,当浏览器在执行render函数时,会获取到vue实例中定义的变量。而在新建vue实例时,会通过Object.defineProperty来重新设置传入的data,当render函数执行获取变量时,watcher就会触发get方法,将当前water添加到当前vue的dep实例里,在修改数据时,则遍历当前vue实例的dep内部所有的watcher去触发更新。
清楚这些以后,就可以写代码了:
新建一个myvue文件夹并用 npm init -y初始化
进入目录 安装webpack(这里是webpack4):
1
| npm i webpack@4 wecpack-cli@4;
|
新建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') }, }
|
添加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", } }
|
- 新建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; }
|
- 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.
- 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]); }) }
|
- 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(); var child; while (child = node.firstChild) { self.compileElement(child, vm); frag.append(child); } return frag; }, compileElement: function (node, vm) { var reg = /\{\{(.*)\}\}/; if (node.nodeType === 1) { var attr = node.attributes; for (var i = 0; i < attr.length; i++) { if (attr[i].nodeName == 'v-model') { var name = attr[i].nodeValue; node.addEventListener('input', function (e) { vm[name] = e.target.value; }); new Watcher(vm, node,name, 'value'); } }; } if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { var name = RegExp.$1; name = name.trim(); new Watcher(vm, node, name, 'nodeValue'); } } }, }
|
compile中主要是通过正则去匹配文档片段中的特殊字符,并拿到这个节点和相应用到的变量,然后new watcher实例。
- 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的作用是批处理,避免不停修改时触发多次)
- 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;
|
- 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() }
}
|
- 执行npm run build,在浏览器中打开index.html即可看见一件简单的双向绑定demo