上一章双向数据绑定(一)主要讲解了Object.defineProperty() 的作用及用法。
github代码地址
现在咱们一起实现一个MyVue
MyVue封装
- 初始化
- html
- myVue.js
- 分析
- DocumentFragment
- 使用方式
- 将子节点劫持到文档
- compile
- nodeType = 1 :元素
- nodeType = 3 :文本
- 示例
- 效果图
- defineProperty
- 订阅者
- Dep 及 Watcher
- 最终代码
初始化
首先创建一个html、myVue.js ,然后按照vue的写法,看看会是什么样子。
html
<body>
<div id="app">
<input id="inputId" placeholder="请输入" v-model="text" />
<div>
这是内容text:
<p id="txtId">{{text}}</p>
</div>
</div>
<script src="./assets/myVue.js"></script>
<script>
var vm = new MyVue({
el:'app',
data:{
text:'hello word'
}
})
</script>
</body>
myVue.js
(function(win){
function MyVue (opts){
console.group('opts =======================')
console.log(opts)
}
win.MyVue = MyVue;
})(window)
分析
控制台打印的值,是咱们出入的对象,这里没有问题。
但是页面展示的文本,却不是咱们想要的,text 我们希望替换成 hello word 才对。那么咱们首先就应该先替换dom节点。
DocumentFragment
documentFragment最大的特点是可以像document一样,但不是真实 dom 树的一部分,它的变化不会触发 dom树的重新渲染,且不会导致性能等问题。
最常用的方法是使用文档片段作为参数,这种情况下被添加(append)或被插入(inserted)的是片段的所有子节点, 而非片段本身。因为所有的节点会被一次插入到文档中,而这个操作仅发生一个重渲染的操作,而不是每个节点分别被插入到文档中,因为后者会发生多次重渲染的操作。
使用方式
- 创建documentFragment对象flag
- 取出ul中的所有子节点并保存到flag
- 更新flag中的所有节点(app的内容)
- 将flag插入到app
关于DocumentFragment具体可以戳这里~
将子节点劫持到文档
(function(win){
function MyVue (opts){
console.group('opts =======================')
console.log(opts)
var id = opts.el;
var dom =nodeToFragment(document.getElementById(id),this);
document.getElementById(id).appendChild(dom)
}
//文档片段
function nodeToFragment (node,vm){
var flag = document.createDocumentFragment();
var child;
// firstChild 属性返回被选节点的第一个子节点
//如果 while (child = node.firstChild) 成立
// appendChild 方法向节点添加最后一个子节点
// appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
// 第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
// 直到 child = node.firstChild 不成立
while (child = node.firstChild){
//将子节点劫持到文档片段中
flag.appendChild(child);
}
return flag
}
win.MyVue = MyVue;
})(window)
这部分只是对子节点进行了劫持,具体变化还需要数据绑定。
compile_109">compile
compile的作用主要是数据初始化,确定数据绑定关系。使用node.nodeType 判断节点类型。
nodeType = 1 :元素
元素的话,需要判断两个方面:
- 是否还有子节点,有的话递归循环重复一下compile操作
- 当前节点是否有属性 v-model,有的话设置value值,并删除v-model属性
nodeType = 3 :文本
文本的话,需要判断其是否有 {{}} ,有的话从data中取值替换
示例
(function(win){
function MyVue (opts){
this.data = opts.data;
var id = opts.el;
var dom =nodeToFragment(document.getElementById(id),this);
document.getElementById(id).appendChild(dom)
}
//文档片段
function nodeToFragment (node,vm){
var flag = document.createDocumentFragment();
var child;
// firstChild 属性返回被选节点的第一个子节点
//如果 while (child = node.firstChild) 成立
// appendChild 方法向节点添加最后一个子节点
// appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
// 第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
// 直到 child = node.firstChild 不成立
while (child = node.firstChild){
compile(child,vm);
//将子节点劫持到文档片段中
flag.appendChild(child);
}
return flag
}
//数据初始化
function compile (node,vm){
var reg = /\{\{(.*)\}\}/;
//节点类型:元素
if(node.nodeType ===1){
var attr = node.attributes;
// 有属性
if(attr.length){
for(var i=0;i<attr.length;i++){
if(attr[i].nodeName == 'v-model'){
var name = attr[i].nodeValue;
node.value = vm.data[name];
node.removeAttribute('v-model');
}
}
}
var childs = node.childNodes;
//有子节点
if(childs.length){
for(var i=0;i<childs.length;i++){
compile(childs[i],vm)
}
}
}
//节点类型:text
else if(node.nodeType ===3){
if(reg.test(node.nodeValue)){
//指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串
var name = RegExp.$1;
name = name.trim();
node.nodeValue = vm.data[name];
}
}
}
win.MyVue = MyVue;
})(window)
重点 :
1、MyVue 里面需要指定
this.data = opts.data;
2、正则匹配符合 /{{(.*)}}/ ,取其值,node.nodeValue = vm.data[name]
效果图
到这里,已经可以实现 {{}} 替换成定义的值了。但是改变input的值,文案还无法改变,因此需要加defineProperty拦截。不清楚戳这里~
defineProperty_220">defineProperty
// MyVue 添加observer 方法初始化
observer(this.data,this)
// v-model 判断处,添加input方法
node.addEventListener('input', function (e) {
// 给相应的 data 属性赋值,进而触发该属性的 set 方法
vm[name] = e.target.value;
});
// observer
function observer (obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
})
}
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
get: function () {
return val
},
set: function (newVal) {
if (newVal === val) return
console.log(newVal,'newVal')
val = newVal;
}
});
}
效果图
随着输入框值的改变,set 里的newVal 也会发生变化
订阅者
text属性变化了,set方法触发了,但是文本还没有改变,如何让文本同步,这里又有一个知识点:订阅发布模式。
订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。
发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应操作
var pub = {
publish:function (){
dep.notify()
}
}
var sub1 = {
update:function(){console.log(1)}
}
var sub2 = {
update:function(){console.log(2)}
}
var sub3 = {
update:function(){console.log(3)}
}
function Dep (){
this.subs = [sub1,sub2,sub3];
}
Dep.prototype.notify = function(){
this.subs.forEach(function(sub){
sub.update()
})
};
var dep = new Dep();
pub.publish()
简单来说dep是一个数组集合,每次触发notify时,进行update更新
Dep 及 Watcher
//input监听
new Watcher(vm,node,name,'input')
//text 监听
new Watcher(vm,node,name,'text')
//Dep
function Dep () {
this.subs = []
}
Dep.prototype = {
addSub: function(sub) {
console.log(sub,'addSub')
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
}
// Watcher
function Watcher (vm, node, name, nodeType) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function () {
this.get();
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType == 'input') {
this.node.value = this.value;
}
},
// 获取 data 中的属性值
get: function () {
this.value = this.vm[this.name]; // 触发相应属性的 get
}
}
最终代码
(function(win){
function MyVue (opts){
this.data = opts.data;
observer(this.data,this)
var id = opts.el;
var dom =nodeToFragment(document.getElementById(id),this);
document.getElementById(id).appendChild(dom)
}
//文档片段
function nodeToFragment (node,vm){
var flag = document.createDocumentFragment();
var child;
// firstChild 属性返回被选节点的第一个子节点
//如果 while (child = node.firstChild) 成立
// appendChild 方法向节点添加最后一个子节点
// appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
// 第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
// 直到 child = node.firstChild 不成立
while (child = node.firstChild){
compile(child,vm);
//将子节点劫持到文档片段中
flag.appendChild(child);
}
return flag
}
//数据初始化
function compile (node,vm){
var reg = /\{\{(.*)\}\}/;
//节点类型:元素
if(node.nodeType ===1){
var attr = node.attributes;
// 有属性
if(attr.length){
for(var i=0;i<attr.length;i++){
if(attr[i].nodeName == 'v-model'){
var name = attr[i].nodeValue;
node.addEventListener('input', function (e) {
// 给相应的 data 属性赋值,进而触发该属性的 set 方法
vm[name] = e.target.value;
});
node.value = vm.data[name];
node.removeAttribute('v-model');
}
}
new Watcher(vm,node,name,'input')
}
var childs = node.childNodes;
//有子节点
if(childs.length){
for(var i=0;i<childs.length;i++){
compile(childs[i],vm)
}
}
}
//节点类型:text
else if(node.nodeType ===3){
if(reg.test(node.nodeValue)){
//指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串
var name = RegExp.$1;
name = name.trim();
node.nodeValue = vm.data[name];
new Watcher(vm,node,name,'text')
}
}
}
function observer (obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
})
}
function defineReactive (obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function () {
// 添加订阅者 watcher 到主题对象 Dep
if (Dep.target) {
dep.addSub(Dep.target)
};
return val
},
set: function (newVal) {
if (newVal === val) return
val = newVal;
// 作为发布者发出通知
dep.notify();
}
});
}
function Dep () {
this.subs = []
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
}
function Watcher (vm, node, name, nodeType) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function () {
this.get();
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType == 'input') {
this.node.value = this.value;
}
},
// 获取 data 中的属性值
get: function () {
this.value = this.vm[this.name]; // 触发相应属性的 get
}
}
win.MyVue = MyVue;
})(window)