原理分析
通过数据劫持实现数据双向绑定
- 监听器
Observer
, 用来劫持并监听所有属性, 所发生变动则通知订阅者 - 订阅者
Watcher
, 可接收到属性的变化通知并执行相应的函数, 从而更新视图- 由于订阅者有多个, 为方便统一管理, 通过一个消息订阅器
Dep
专门来收集订阅者
- 由于订阅者有多个, 为方便统一管理, 通过一个消息订阅器
- 指令解析器
Complie
,- 扫描和解析每个节点的相关指令, 并初始化模板数据以及初始化相应的订阅器
如图:
Version 1.0
实现 Observer
function Observer(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
});
}
function defineReactive(data, key, val) {
//递归子属性
Observer(val);
//植入消息订阅器
//为每个属性创建一个对应的 Dep 对象, 因为每个属性可能有多个订阅者
let dep = new Dep();
//在 getter 时收集订阅者 Watcher 到 Dep, 在 setter 时借由 Dep 通知所有的订阅者 Watcher
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log(`getter -> key :: [[${key}]], value :: [[${val}]]`);
if (Dep.target) { //是否需要添加订阅者
dep.addSub(Dep.target); // 订阅者
}
return val;
},
set: function (newVal) {
val = newVal;
console.log(`setter -> key :: [[${key}]], new value :: [[${newVal}]]`);
dep.notify(); // 通知订阅了该属性的所有订阅者更新
}
});
}
function Dep() {
this.subs = [];
}
Dep.prototype.addSub = function (watch) {
this.subs.push(watch);
}
Dep.prototype.notify = function () {
this.subs.forEach(watch => watch.update());
}
复制代码
实现 Watcher
function Watcher(data, key, cb) {
this.data = data;
this.cb = cb;
this.key = key;
this.value = this.get();
}
Watcher.prototype = {
get: function () {
Dep.target = this; // 缓存当前 watcher 实例到一个全局变量 , 以便在 getter 中将该 watcher 实例收集到 Dep 中
const value = this.data[this.key]; // 强制触发 data:key 属性的 getter
Dep.target = null; //释放
return value;
},
update: function () {
this.cb();
}
}
复制代码
demo
<body>
<h1 id="name">name</h1>
<script src="./myVue.js"></script>
<script>
let data = {
name: 'zheling',
};
observer(data);
new Watcher(data, 'name', function () {
document.querySelector('#name').innerHTML = 'hello vue??';
});
setTimeout(function () {
data.name = 'hello Vue'
}, 1000);
</script>
</body>
复制代码
Version 2.0
实现 Watcher 2.0
function Watcher(vm, key, cb) {
this.vm = vm;
this.data = this.vm.data;
this.cb = cb;
this.key = key;
this.value = this.get();
}
Watcher.prototype = {
get: function () {
Dep.target = this; // 缓存当前 watcher 实例到一个全局变量 , 以便在 getter 中将该 watcher 实例收集到 Dep 中
const value = this.data[this.key]; // 强制触发 data:key 属性的 getter
Dep.target = null; //释放
return value;
},
update: function () {
let oldVal = this.value;
let value = this.data[this.key];
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
}
}
复制代码
MyVue
将 Observer 和 Watcher 关联起来
function MyVue(data, el, key) {
this.data = data;
this.el = el;
observer(this.data); //监听属性
//代理属性: vm.data.key -> vm.key
Object.keys(this.data).forEach(key => {
this.proxyKeys(key);
});
new Watcher(this, key, function (value) {
el.innerHTML = value;
});
return this;
}
MyVue.prototype.proxyKeys = function (key) {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get: function () {
return this.data[key];
},
set: function (newVal) {
this.data[key] = newVal;
}
});
}
复制代码
demo
<body>
<h1 id="name">name</h1>
<script src="./myVue.js"></script>
<script>
let data = {
name: 'zheling',
};
let vm = new MyVue(
data,
document.querySelector('#name'),
'name'
);
setTimeout(function () {
vm.data.name = 'hello Vue'
}, 1000);
</script>
</body>
复制代码
Version 3.0
MyVue
function MyVue(options) {
let { el, data,methods } = options || {};
this.data = data;
this.el = el;
this.methods = methods;
observer(this.data);
Object.keys(this.data).forEach(key => {
this.proxyKeys(key);
});
// 解析 DOM , 创建订阅者并为其绑定对应的更新函数
new Compile(this);
return this;
}
MyVue.prototype.proxyKeys = function (key) {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get: function () {
return this.data[key];
},
set: function (newVal) {
this.data[key] = newVal;
}
});
}
复制代码
Compile
// 实现编译器, 将上例初始化订阅者以及解析 DOM 封装出来
function Compile(vm) {
this.vm = vm;
this.el = document.querySelector(vm.el);
// 拿到根容器下所有节点, 由于需要频繁操作 DOM 考虑到性能, 所以将其移动到一个 fragment 中来处理
this.fragment = null;
this.init();
}
Compile.prototype = {
init: function () {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
},
nodeToFragment: (el) => {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// 将DOM元素移入fragment中
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
},
// 遍历 el 下的所有节点
compileElement: function (el) {
let childNodes = el.childNodes; // 伪数组对象
let reg = /\{\{(.+)\}\}/;
Array.from(childNodes).forEach(node => {
let text = node.textContent;
if (this.isTextNode(node) && reg.test(text)) { // 匹配 {{}} 指令
console.log(node);
this.compileText(node, reg.exec(text)[1]); // 获取{{key}}中的 key
}
//遍历子节点
if (node.childNodes && node.childNodes.length) {
this.compileElement(node);
}
});
},
compileText: function (node, key) {
// 初始化数据
this.updateText(node, this.vm[key]);
// 创建订阅者并绑定更新函数
new Watcher(this.vm, key, (value) => {
this.updateText(node, value);
});
},
updateText: function (node, text) {
node.textContent = text ? text : '';
},
isTextNode: function (node) {
return node.nodeType === 3;
},
}
复制代码
demo
<body>
<div id="app">
<h1>{{name}}</h1>
<span>{{age}}</span>
</div>
<script src="./myVue.js"></script>
<script>
let vm = new MyVue({
el: '#app',
data: {
name: 'zheling',
age: 99
},
});
setTimeout(function () {
vm.name = 'hello Vue'
}, 1000);
</script>
</body>
复制代码
Version 4.0
添加v-on / v-model
指令的处理
Compile
Compile.prototype = {
init: function () {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
},
nodeToFragment: (el) => {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// 将DOM元素移入fragment中
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
},
// 遍历 el 下的所有节点
compileElement: function (el) {
let childNodes = el.childNodes; // 伪数组对象
let reg = /\{\{(.+)\}\}/;
Array.from(childNodes).forEach(node => {
let text = node.textContent;
// 是否为元素节点
if (this.isElementNode(node)) {
this.compile(node);
} else if (this.isTextNode(node) && reg.test(text)) { // 匹配 {{}} 指令
this.compileText(node, reg.exec(text)[1]); // 获取{{key}}中的 key
}
//遍历子节点
if (node.childNodes && node.childNodes.length) {
this.compileElement(node);
}
});
},
compileText: function (node, key) {
// 初始化数据
this.updateText(node, this.vm[key]);
// 创建订阅者并绑定更新函数
new Watcher(this.vm, key, (value) => {
this.updateText(node, value);
});
},
updateText: function (node, text) {
node.textContent = text ? text : '';
},
compile: function (node) {
var nodeAttrs = node.attributes;
var self = this;
// 遍历 node 的属性 attributes , 找到 v-* 开头的指令
Array.from(nodeAttrs).forEach(function (attr) {
var attrName = attr.name;
if (self.isDirective(attrName)) {
var exp = attr.value;
var dir = attrName.substring(2); // 移除 v-* 中的 'v-'
// 进一步解析指令, 是 model 还是 on 指令
if (self.isEventDirective(dir)) { // 事件指令
self.compileEvent(node, self.vm, exp, dir);
} else { // v-model 指令
self.compileModel(node, exp);
}
node.removeAttribute(attrName);
}
});
},
compileEvent: function (node, vm, key, dir) {
var eventType = dir.split(':')[1];
var cb = vm.methods && vm.methods[key];
if (eventType && cb) { // bind(vm) 使用时能快捷访问到 MyVue 实例
node.addEventListener(eventType, cb.bind(vm), false);
}
},
compileModel: function (node, key) {
var self = this;
var val = this.vm[key];
// 初始化 node.value
node.value = typeof val == 'undefined' ? '' : val;
// 双向绑定之一 : data.field -> input
new Watcher(this.vm, key, function (value) {
node.value = typeof value == 'undefined' ? '' : value;
});
// 双向绑定之二 : input -> data.field
node.addEventListener('input', function (e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
self.vm[key] = newValue;
});
},
isDirective: function (attr) {
return attr.indexOf('v-') == 0;
},
isEventDirective: function (dir) {
return dir.indexOf('on:') === 0;
},
isTextNode: function (node) {
return node.nodeType === 3;
},
isElementNode: node => node.nodeType === 1,
}
复制代码
demo
<body>
<div id="app">
<h2>{{title}}</h2>
<input v-model="name">
<h1>name : {{name}}</h1>
<button v-on:click="clickMe">click me!</button>
</div>
<script src="./myVue.js"></script>
<script>
let vm = new MyVue({
el: '#app',
data: {
title: 'zheling',
name: 'kangshi',
},
methods: {
clickMe: function () {
this.title = 'new title';
}
}
});
// 验证 v-model 指令 data.name -> input.value 方向是否正常同步
setTimeout(function () {
vm.name = 'hello Vue??'
}, 5000);
</script>
</body>
复制代码
参考文章
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END