上一章雙向資料綁定(一)主要講解了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
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
// 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)