vue2.0的雙向資料系結原理(手動實作)
- 一句話概括:資料劫持(Object.defineProperty)+發布訂閱模式
- 一.首先了解什么是發布訂閱模式
- 二.new Vue()的時候做了什么?
一句話概括:資料劫持(Object.defineProperty)+發布訂閱模式
雙向資料系結有三大核心模塊(dep 、observer、watcher),它們之間是怎么連接的,下面來一一介紹,
為了大家更好的理解雙向資料系結原理以及它們之間是如何實作關聯的,先帶領大家復習一下發布訂閱模式,
一.首先了解什么是發布訂閱模式
直接上代碼:
一個簡單的發布訂閱模式,幫助大家更好的理解雙向資料系結原理
//發布訂閱模式
function Dep() {
this.subs = []//收集依賴(也就是手機watcher實體),
}
Dep.prototype.addSub = function (sub) { //添加訂閱者
this.subs.push(sub); //實際上添加的是watcher這個實體
}
Dep.prototype.notify = function (sub) { //發布,這個方法的作用是遍歷陣列,讓每個訂閱者的update方法去執行
this.subs.forEach((sub) => sub.update())
}
function Watcher(fn) {
this.fn = fn;
}
Watcher.prototype.update = function () { //添加一個update屬性讓每一個實體都可以繼承這個方法
this.fn();
}
let watcher = new Watcher(function () {
alert(1)
});//訂閱
let dep = new Dep();
dep.addSub(watcher);//添加依賴,添加訂閱者
dep.notify();//發布,讓每個訂閱者的update方法執行
二.new Vue()的時候做了什么?
只是針對雙向資料系結做說明
<template>
<div id="app">
<div>obj.text的值:{{obj.text}}</div>
<p>word的值:{{word}}</p>
<input type="text" v-model="word">
</div>
</template>
<script>
new Vue({
el: "#app",
data: {
obj: {
text: "向上",
},
word: "學習"
},
methods:{
// ...
}
})
</script>
Vue建構式都干什么了?
function Vue(options = {}) {
this.$options = options;//接收引數
var data = this._data = this.$options.data;
observer(data);//對data中的資料進型回圈遞回系結
for (let key in data) {
let val = data[key];
observer(val);
Object.defineProperty(this, key, {
enumerable: true,
get() {
return this._data[key];
},
set(newVal) {
this._data[key] = newVal;
}
})
}
new Compile(options.el, this)
};
在new Vue({…})建構式時,首先獲取引數options,然后把引數中的data資料賦值給當前實體的_data屬性上(this._data = this.$options.data),重點來了,那下面的遍歷是為什么呢?首先我們在操作資料的時候是this.word獲取,而不是this._data.word,所以是做了一個映射,在獲取資料的時候this.word,其實是獲取的this._data.word的值,大家可以在自己專案中輸出this查看一下

1.接下來看看observer方法干了什么
function observer(data) {
if (typeof data !== "object") return;
return new Observer(data);//回傳一個實體
}
function Observer(data) {
let dep = new Dep();//創建一個dep實體
for (let key in data) {//對資料進行回圈遞回系結
let val = data[key];
observer(val);
Object.defineProperty(data, key, {
enumerable: true,
get() {
Dep.target && dep.depend(Dep.target);//Dep.target就是Watcher的一個實體
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
observer(newVal);
dep.notify() //讓所有方法執行
}
})
}
}
Observer建構式,首先let dep=new Dep(),作為之后的觸發資料劫持的get方法和set方法時,去收集依賴和發布時呼叫,主要的操作就是通過Object.defineProperty對data資料進行回圈遞回系結,使用getter/setter修改其默認讀寫,用于收集依賴和發布更新,
2.再來看看Compile具體干了那些事情
function Compile(el, vm) {
vm.$el = document.querySelector(el);
let fragment = document.createDocumentFragment(); //創建檔案碎片,是object型別
while (child = vm.$el.firstChild) {
fragment.appendChild(child);
};//用while回圈把所有節點都添加到檔案碎片中,之后都是對檔案碎片的操作,最后再把檔案碎片添加到頁面中,這里有一個很重要的特性是,如果使用appendChid方法將原dom樹中的節點添加到fragment中時,會洗掉原來的節點,
replace(fragment);
function replace(fragment) {
Array.from(fragment.childNodes).forEach((node) => {//回圈所有的節點
let text = node.textContent;
let reg = /\{\{(.*)\}\}/;
if (node.nodeType === 3 && reg.test(text)) {//判斷當前節點是不是文本節點且符不符合{{obj.text}}的輸出方式,如果滿足條件說明它是雙向的資料系結,要添加訂閱者(watcher)
console.log(RegExp.$1); //obj.text
let arr = RegExp.$1.split("."); //轉換成陣列的方式[obj,text],方便取值
let val = vm;
arr.forEach((key) => { //實作取值this.obj.text
val = val[key];
});
new Watcher(vm, RegExp.$1, function (newVal) {
node.textContent = text.replace(/\{\{(.*)\}\}/, newVal)
});
node.textContent = text.replace(/\{\{(.*)\}\}/, val); //對節點內容進行初始化的賦值
}
if (node.nodeType === 1) { //說明是元素節點
let nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach((item) => {
if (item.name.indexOf("v-") >= 0) {//判斷是不是v-model這種指令
node.value = vm[item.value]//對節點賦值操作
}
//添加訂閱者
new Watcher(vm, item.value, function (newVal) {
node.value = vm[item.value]
});
node.addEventListener("input", function (e) {
let newVal = e.target.value;
vm[item.value] = newVal;
})
})
}
if (node.childNodes) { //這個節點里還有子元素,再遞回
replace(node);
}
})
}
//這是頁面中的檔案已經沒有了,所以還要把檔案碎片放到頁面中
vm.$el.appendChild(fragment);
}
Compile(編譯方法)
首先解釋一下DocuemntFragment(檔案碎片)它是一個dom節點收容器,當你創造了多個節點,當每個節點都插入到檔案當中都會引發一次回流,也就是說瀏覽器要回流多次,十分耗性能,而使用檔案碎片就是把多個節點都先放入到一個容器中,最后再把整個容器直接插入就可以了,瀏覽器只回流了1次,
Compile方法首先遍歷檔案碎片的所有節點,1.判斷是否是文本節點且符不符合{{obj.text}}的雙大括號的輸出方式,如果滿足條件說明它是雙向的資料系結,要添加訂閱者(watcher),new Watcher(vm,動態系結的變數,回呼函式fn) 2.判斷是否是元素節點且屬性中是否含有v-model這種指令,如果滿足條件說明它是雙向的資料系結,要添加訂閱者(watcher),new Watcher(vm,動態系結的變數,回呼函式fn) ,直至遍歷完成,
最后別忘了把檔案碎片放到頁面中
3.Dep建構式(怎么收集依賴的)
var uid=0;
//發布訂閱
function Dep() {
this.id=uid++;
this.subs = [];
}
Dep.prototype.addSub = function (sub) { //訂閱
this.subs.push(sub); //實際上添加的是watcher這個實體
}
Dep.prototype.depend = function () { // 訂閱管理器
if(Dep.target){//只有Dep.target存在時采取添加
Dep.target.addDep(this);
}
}
Dep.prototype.notify = function (sub) { //發布,遍歷陣列讓每個訂閱者的update方法去執行
this.subs.forEach((sub) => sub.update())
}
Dep建構式內部有一個id和一個subs,id=uid++ ,id用于作為dep物件的唯一標識,subs就是保存watcher的陣列,depend方法就是一個訂閱的管理器,會呼叫當前watcher的addDep方法添加訂閱者,當觸發資料劫持(Object.defineProperty)的get方法時會呼叫Dep.target && dep.depend(Dep.target)添加訂閱者,當資料改變時觸發資料劫持(Object.defineProperty)的set方法時會呼叫dep.notify方法更新操作,
4.Watcher建構式干了什么
function Watcher(vm, exp, fn) {
this.fn = fn;
this.vm = vm;
this.exp = exp //
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
Dep.target = this; //this是指向當前(Watcher)的一個實體
let val = vm;
let arr = exp.split(".");
arr.forEach((k) => { //取值this.obj.text
val = val[k] //取值this.obj.text,就會觸發資料劫持的get方法,把當前的訂閱者(watcher實體)添加到依賴中
});
Dep.target = null;
}
Watcher.prototype.addDep = function (dep) {
var id=dep.id;
if(!this.newDepIds.has(id)){
this.newDepIds.add(id);
this.newDeps.push(dep);
if(!this.depIds.has(id)){
dep.addSub(this);
}
}
}
Watcher.prototype.update = function () { //這就是每個系結的方法都添加一個update屬性
let val = this.vm;
let arr = this.exp.split(".");
arr.forEach((k) => {
val = val[k] //取值this.obj.text,傳給fn更新操作
});
this.fn(val); //傳一個新值
}
Watcher建構式干了什么
1 接收引數,定義了幾個私有屬性( this.newDep ,this.depIds
,this.newDepIds)
2. Dep.target = this,通過引數進行data取值操作,這就會觸發Object.defineProperty的get方法,它會通過訂閱者管理器(dep.depend())添加訂閱者,添加完之后再將Dep.target=null置為空;
3.原型上的addDep是通過id這個唯一標識,和幾個私有屬性的判斷防止訂閱者被多次重復添加
4.update方法就是當資料更新時,dep.notify()執行,觸發訂閱者的update這個方法, 執行發布更新操作,
總結一下
vue2.0中雙向資料系結,簡單來說就是Observer、Watcher、Dep三大部分;
1.首先用Object.defineProperty()回圈遞回實作資料劫持,為每個屬性分配一個訂閱者集合的管理陣列dep;
2.在編譯的時候,創建檔案碎片,把所有節點添加到檔案碎片中,遍歷檔案碎片的所有結點,如果是{{}},v-model這種,new Watcher()實體并向dep的subs陣列中添加該實體
3.最后修改值就會觸發Object.defineProperty()的set方法,在set方法中會執行dep.notify(),然后回圈呼叫所有訂閱者的update方法更新視圖,
初次寫技術博客,有不嚴謹和不對的地方,希望大家多多批評指正,
同時也希望能給大家帶來一些幫助(無論是面試還是對原始碼的理解),謝謝!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/257123.html
標籤:其他
下一篇:點擊出現子選單
