背景
在上一篇中,我們保證了程式碼不報錯,並且引數也成功傳進去了,但未實現任何的邏輯,這一章我們的工作就是完成dom的解析,在vue中,透過構建虛擬dom結構實現dom的高效更新和渲染,模仿這個思路,我們構建一個超級簡單虛擬的dom結構,本著讓所有人都能看得懂的目的,這次構建的dom結構不考慮效能,不考慮設計,以能實現功能為目的。
domParser(dom解析)
基本的思路:
- 從引數el指定的節點開始遍歷
- 解析節點
- 解析子節點
- 遞迴的方式完成所有節點的解析
(function (global, factory) {
global.Vue = factory
})(this, function (options) {
//1. 解析dom結構
domParser();
function domParser() {
let el = options.el;
//配置的el以'#'開頭
if (el.startsWith("#")) {
el = el.substr(1);
}
let app = document.getElementById(el);
let virtualDom = document.createDocumentFragment();
for (let i = 0; i < app.childNodes.length; ++i) {
let node = compile(app.childNodes[i]);
virtualDom.appendChild(node);
}
app.innerHTML = '';
app.appendChild(virtualDom);
}
})
createDocumentFragment
建立虛擬dom的頂層節點- 先不考慮
compile
的實現,在compile中我們將實現變數的解析,事件掛載等,但這裡先不管 app.innerHTML = '';
暴力的將原始內容全部清除app.appendChild(virtualDom);
插入虛擬dom結構
compile(節點編譯)
在dom結構中,每一個節點都是一個Node,Node有不同的型別,雖然標準中型別較多,但常用的就兩種,元素型別(Node.ELEMENT_NODE)和文字型別(Node.TEXT_NODE),比如<p>hello</p>
包含兩種型別,<p>
為元素型別,hello
為文字型別。ELEMENT_NODE值為1,TEXT_NODE為3,在compile中,這兩種型別的解析方式不一樣,TEXT只需解析文字即可,ELEMENT型別需要解析屬性。
function compile(node) {
let element = node.cloneNode(false);
for (let i = 0; i < node.childNodes.length; ++i) {
element.appendChild(compile(node.childNodes[i]));
}
if (element.nodeType == 3) {
//文字型別解析
let vars = parseVariable(element.textContent);
for (let i = 0; i < vars.length; ++i) {
let directive = Directive(element);
addSubscriber(vars[i], directive);
}
} else if (element.nodeType == 1 && element.attributes) {
//元素型別解析
let attrs = element.attributes;
for (let i = 0; i < attrs.length; ++i) {
let name = attrs[i].name;
if (name.startsWith("v-bind") || name.startsWith(":")) {
let directive = Directive(element, name, attr[i].value);
addSubscriber(vars[i], directive);
}
}
}
return element;
}
cloneNode(false)
克隆當前節點,false表示不復制子節點,因為我們需要對子節點進行逐一的解析parseVariable
解析文字中變數,也就是類似{{param}}
字串- Directive內容見下文
- Subscriber內容見下文
- 對於元素型別,逐一遍歷屬性,如果屬性名稱以
v-bind
或者:
開頭就認為繫結了變數
parseVariable(變數解析)
function parseVariable(content) {
var variables = {};
let m;
let variableRegExp = new RegExp("\{\{([^\}]+\)}\}", "g");
while (m = variableRegExp.exec(content)) {
if (!variables[m[1]]) {
variables[m[1]] = true;
}
}
var items = [];
for (k in variables) {
items.push(k);
}
return items;
}
這裡直接用正則對變數進行解析。
Directive(指令)
這裡是借鑑了vue的概念,在vue中指令可以完成一系列特定的功能,比如指令v-model
可以將值繫結到變數,v-for
可以對變數進行迴圈。我們這次只完成頁面上使用到指令,我們在這個系列中將實現以下指令
- v-model:實現雙向繫結
- v-on: 實現事件繫結
- title:設定title屬性
- style:設定style屬性
function Directive(node, attr, expression) {
var directive = {
node: node
}
if (node.nodeType == 3) {
directive.change = function (value) {
this.node.textContent = value;
}
} else if (node.nodeType == 1) {
if (attr === 'title') {
directive.change = function (value) {
this.node.title = value;
}
} else if (attr === 'v-model') {
directive.change = function (value) {
this.node.value = value;
}
node.addEventListener(('input', function (e) {
valueTrigger(expression, e.target.value);
}))
} else if (attr === 'style') {
directive.change = function(name, value) {
this.node.style = this.origin.replace("\{\{" + name + "\}\}", value);
}
}
}
return directive;
}
定義了一個directive的物件,該物件包含以下屬性
- node:指令目標節點
- change:指定具體邏輯
對於文字型別節點(node.nodeType == 3),直接將變數的內容複製給節點,當然這個肯定是不對的,會將其他內容覆蓋,但請放心,我們下一節會解決這個問題,我只不過不想給這一篇引入太多內容,導致大家消化不良。對於元素型別(node.nodeType == 1),如果是雙向繫結,會給節點加上一個input的事件,監聽節點值的變化,並執行valueTrigger的邏輯,該函式邏輯下一篇再講
Subscriber(訂閱者)
如果一個節點繫結了變數,那麼這個節點就是一個Subscriber(訂閱者),變數值發生變化會呼叫節點相關指令(Directive),我們宣告一個subscriber的變數用於存放訂閱者資訊,以變數的名稱作為key,指令作為值,比如有A和B兩個節點繫結了變數name
,那麼subscriber的結構如下
{
name:[
{
node:A,
change:function(){...}
},{
node:B,
change:function(){...}
}]
}
addSubscriber
程式碼如下:
var subscriber = Object.create(null);
function addSubscriber(variableName, directive) {
let item = subscriber[variableName];
if (!item) {
item = [];
}
item.push(directive);
subscriber[variableName] = item;
}
總結
解析節點---->解析變數---->根據繫結的型別關聯指令--->新增訂閱者
|
|--->雙向繫結監聽值變化--->值變化觸發事件
點選這裡檢視程式碼和效果
參考
點選餘下連結,檢視該系列其他文章