Vue是目前炙手可熱的JS框架习瑰,作為一個(gè)視圖庫(kù),最重要的功能當(dāng)然是數(shù)據(jù)綁定了抡笼,數(shù)據(jù)變化苏揣,模板變化。
接下來(lái)讓我看看Vue實(shí)現(xiàn)的大致原理是怎樣的推姻。
這是我們的模板平匈。
<div id="app">
<input type="text" v-model="name">
<p>
{{ name }}
</p>
<strong>{{ name }}</strong>
</div>
像下面這樣實(shí)例化
var vm = new Vue({
el: "#app",
data: {
name: 'ok'
}
})
我們先不討論v-model
指令。
我們初始化實(shí)例時(shí)傳入一個(gè)data。data的name屬性對(duì)應(yīng)模板中的name增炭。
我們要實(shí)現(xiàn)的功能點(diǎn)有
- vm實(shí)例代理data上的屬性忍燥。
- 在vm對(duì)象內(nèi)部,我們用this指代vm隙姿。當(dāng)在vm內(nèi)部this.name 被賦了一個(gè)新的值時(shí)梅垄,模板中的name也會(huì)同步變化。
這是單向數(shù)據(jù)綁定输玷。
然后再考慮v-model
队丝,v-model
相同于一個(gè)語(yǔ)法糖,監(jiān)聽(tīng)了表單控件欲鹏,用戶輸入炭玫。this.name也會(huì)變化。有了上面的條件貌虾,模板也會(huì)變化了。
這就是雙向數(shù)據(jù)綁定裙犹。
那應(yīng)該如何實(shí)現(xiàn)呢尽狠?
構(gòu)造函數(shù)
function Vue(option) {
let { el, data } = option;
let node = document.querySelector(el);
this.data = data;
observe(data,this) // 代理綁定屬性
let dom = kidnap(node,this) // 遍歷dom樹(shù),編譯叶圃,返回一個(gè)新的dom樹(shù)
node.appendChild(dom)
}
代理綁定屬性
Vue對(duì)屬性變化檢測(cè)的核心實(shí)現(xiàn)就是Object.defineProperty方法袄膏。這個(gè)方法可以為對(duì)象定義新的屬性〔艄冢可以設(shè)置getter沉馆,setter回調(diào)。
在這里的實(shí)踐就是遍歷data對(duì)象德崭,data對(duì)象上面的每個(gè)屬性被vm代理斥黑。當(dāng)屬性變化,setter回調(diào)眉厨,廣播通知訂閱者锌奴;getter被回調(diào)時(shí),檢測(cè)是否可以添加訂閱者憾股。
function defineReactive(obj,key,val) {
var dep = new Dep() // 為每一個(gè)屬性實(shí)例一個(gè)發(fā)布者
Object.defineProperty(obj,key,{
get: function() {
if(Dep.target) {
dep.addSub(Dep.target); // 添加訂閱者
}
return val
},
set: function (newVal) {
if(newVal === obj[key]) return;
val = newVal;
dep.notify() // 當(dāng)屬性變化鹿蜀,廣播通知訂閱者
}
})
}
function observe(obj,vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm,key,obj[key]);
})
}
發(fā)布
class Dep {
constructor () {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
notify () {
this.subs.forEach(item => {
item.update()
})
}
}
每一個(gè)發(fā)布者都維護(hù)了一個(gè)訂閱者數(shù)組,發(fā)布者的notify方法會(huì)遍歷所有訂閱者服球,調(diào)用訂閱者的update方法茴恰。所以每一個(gè)訂閱者必須實(shí)現(xiàn)一個(gè)update方法。
編譯
我們知道當(dāng)vm上的屬性變化時(shí)斩熊,所有的訂閱者都會(huì)收到通知往枣。那么這些訂閱者是誰(shuí)呢?
訂閱者就是模板中“Mustache” 語(yǔ)法(雙大括號(hào))的文本插值。
首先我們要將DOM中劫持過(guò)來(lái)婉商。
function kidnap(node,vm) {
if(!node) return;
let frag = document.createDocumentFragment();
while (child = node.firstChild) {
frag.appendChild(child)
}
DFS(frag,function(node) {
compile(node,vm)
})
return frag;
}
值得一提的是上面代碼中的appendChild方法似忧。
DOM規(guī)定,一個(gè)DOM節(jié)點(diǎn)不能同屬于兩個(gè)父節(jié)點(diǎn)丈秩,所以對(duì)一個(gè)擁有父節(jié)點(diǎn)的節(jié)點(diǎn)執(zhí)行appendChild其實(shí)是將它搬移到另一個(gè)節(jié)點(diǎn)盯捌。
同時(shí)進(jìn)行while循環(huán),可以巧妙的搬運(yùn)一個(gè)節(jié)點(diǎn)下的所有子節(jié)點(diǎn)蘑秽。
拿到模板之后對(duì)模板進(jìn)行一些處理饺著。
function compile(node,vm) {
if(!node) return;
var reg = /\{\{(.*)\}\}/;
if(node.nodeType == 1) {
let attr = node.attributes;
for (var i = 0; i < attr.length; i++) {
if(node.tagName.toLowerCase() == "input" && attr[i].name == "v-model") {
var name = attr[i].nodeValue;
node.addEventListener("keyup",function (e) {
vm[name] = e.target.value;
})
node.value = vm[name]
node.removeAttribute("v-model")
new Watcher(node,vm,name) // 訂閱者
}
}
}
if(node.nodeType == 3) {
if(reg.test(node.nodeValue)) {
var name = RegExp.$1;
name = name.trim();
node.nodeValue = vm[name]
new Watcher(node,vm,name) // 訂閱者
}
}
}
遍歷每一個(gè)DOM節(jié)點(diǎn)。這里用到了DFS,深度優(yōu)先搜索肠牲∮姿ィ可以參見(jiàn)我的上一篇文章。代碼如下缀雳。
function DFS(node, cb) {
let deep = 1;
DFSdom(node,deep,cb)
}
function DFSdom(node, deep, cb) {
if(!node)
return;
cb(node,deep)
if(!node.childNodes.length) {
return;
}
deep++;
Array.from(node.childNodes).forEach(item => DFSdom(item,deep,cb))
}
訂閱
我們可以看到在遍歷DOM樹(shù)的時(shí)候渡嚣,對(duì)符合我們語(yǔ)法條件的節(jié)點(diǎn)進(jìn)行了watch。watcher相當(dāng)于訂閱者肥印。
class Watcher {
constructor (node, vm, name) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.update()
Dep.target = null;
}
update () {
this.value = this.vm[this.name];
this.node.nodeValue = this.value;
if(this.node.nodeType == 1) {
this.node.value = this.value
}
}
}