不帶響應式的Vue縮減實現
模板
現有模板如下:
<div id ="app">
<div class="c1">
<div title='tt1' id="id">{{ name }}</div>
<div title='tt2' >{{age}}</div>
<div>hello3</div>
</div>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</div>
<script>
let app = new Vue({
el: '#app',
data:{
name: '張三',
age: 19
}
})
</script>
Vue初始化流程
Vue的初始化,是從new Vue
開始的,以下的圖中可以知道在new Vue
後,會執行init
,再$mount
實現掛載,再到編譯compile
,生成render
函數,接下來是響應式依賴收集,通過pach
實現異步更新。render function
會被轉化爲Vnode
節點,Virtual DOM
是一棵以JavaScript
對象(Vnode
節點)爲基礎的樹。是對真實DOM的描述。通過patch()
轉化爲真實DOM。在數據有變化時,會通過setter -> Watcher -> update
來更新視圖。整個Vue的運行機制大致就是這樣
實現
-
在這裏實現
new Vue -> $mount -> compile -> render function -> Virtual DOM Tree -> patch() -> DOM
,即除了響應式的部分. -
簡略版
【流程梳理】:
-
首先要明確目的,我們需要將現有的HTML模板與數據結合,生成一個新的HTML結構,並渲染到頁面上.考慮到性能問題,我們首先將模板讀取到內存中(源代碼是進行HTML解析,生成一棵抽象AST).在這裏使用
帶mustcache語法的HTML模板
代替. -
首先是執行
new Vue
,在Vue函數中會將傳入的數據和模板保存起來,爲了後續的方便,會將模板及其父元素也保存起來,然後執行mount
function Vue(options){
let elm = document.querySelector()
this._data = options.data
this._template = elm
this._parent = elm.parentNode
this.mount()
}
- 然後是mount函數,在裏面做了2件事:
- 第一件事是將HTML讀取爲AST保存在內存中,並返回一個
根據AST 和 data 生成 虛擬DOM
的render函數 - 第二件事是調用
mountComponent
: 將render
函數生成的VNode(虛擬DOM)轉換成真實的HTML節點渲染到頁面上
- 第一件事是將HTML讀取爲AST保存在內存中,並返回一個
【先看第一件事】
Vue.prototype.mount = function(){
this.render = this.createRenderFn()
}
Vue.prototype.createRenderFn = function(){
let AST = getVNode(this._template)
return function render(){
let _tmp = combine(AST, this._data)
return _tmp
}
}
上面在mount中調用了createRenderFn
,生成了一個render函數(AST + DATA -> VNode). 之所以寫出那種形式,
是因爲AST僅在一開始讀取DOM結構時候就固定不變了,採用上面的寫法可以提高性能.
getVNode
函數根據模板,返回帶mustache語法的
虛擬DOM.更多參考
class VNode {
constructor(tag ,data, value, type){
this.tag = tag && tag.toLowerCase()
this.data = data
this.value = value
this.type = type
this.children = []
}
appendChild(vnode){
this.children.push(vnode)
}
}
function getVNode(node){
let nodeType = node.nodeType
let _vnode = null
if(nodeType == 1){
// 元素節點
let tag = node.nodeName
,attrs = node.attributes
,_data = {}
for(let i = 0, len = attrs.length; i < len; i++){
_data[attrs[i].nodeName] = attrs[i].nodeValue
}
_vnode = new VNode(tag, _data, undefined, nodeType)
// 考慮子元素
let childNodes = node.childNodes;
for(let i = 0, len = childNodes.length; i< len; i++){
_vnode.appendChild(getVNode(childNodes[i]))
}
} else if(nodeType == 3){
// 文本節點
_vnode = new VNode(undefined, undefined, node.nodeValue, nodeType)
}
return _vnode
}
此時得到的是一個對象,這個對象中的值類似{{name}}
(模擬了AST),下面使用combine將該對象模板與數據結合生成一個新的對象(在Vue中是虛擬的DOM)。即將mustache語法用真實的數據替換
function combine(vnode ,data){
let _type = vnode.type
, _data = vnode.data
, _tag = vnode.tag
, _value = vnode.value
, _children = vnode.children
, _vnode = null
if(_type == 3){
// 文本節點
_value = _value.replace(/\{\{(.+?)\}\}/g, function(_, g){
return getValueByPath(data, g.trim())
})
_vnode = new VNode(_tag, _data, _value, _type)
} else if(_type == 1){
// 元素節點
_vnode = new VNode(_tag, _data, _value, _type)
_children.forEach(_subVNode => _vnode.appendChild(combine(_subVNode, data)))
}
return _vnode
}
// getValueByPath,深層次獲取對象的數據. 栗子: 獲取 a.name.age.salary
function getValueByPath(obj, path){
let res=obj
, currProp
, props = path.join('.')
while(currProp = props.shift()){
res = res[props]
}
return res
}
【再看第二件事】
在mountComponent
中會使用第一件事中的render函數將AST和Data結合起來生成虛擬DOM,然後調用this.update方法將虛擬DOM渲染到頁面上
Vue.prototype.mountComponent = function(){
let mount = () => {
this.update(this.render())
}
mount.call(this)
}
// 之所以採用this.update,是因爲update後面會交付給watcher來調用的
Vue.prototype.update = function (vnode){
let realDOM = parseVNode(vnode)
this._parent.replaceChild(realDOM, this._template)
}
function parseVNode(vnode){
let type = vnode.type
, _node = null
if(type ==3){
return document.createTextNode(vnode.value)
} else if (type == 1){
_node = document.createElement(vnode.tag)
let data = vnode.data
Object.keys(data).forEach(key => {
_node.setAttribute(key, data[key])
})
let children = vnode.children
children.forEach(subvnode =>{
_node.appendChild(parseNode(subvnode))
})
}
return _node
}
整體代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<div class="c1">
<div title="tt1" id="id">{{ name }}</div>
<div title="tt2">{{age}}</div>
<div>hello3</div>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</div>
</div>
<script>
/* 虛擬DOM 構造函數 */
class VNode {
constructor(tag, data, value, type) {
this.tag = tag && tag.toLowerCase()
this.data = data
this.value = value
this.type = type
this.children = []
}
appendChild(vnode) {
this.children.push(vnode)
}
}
/* HTML DOM -> VNode(帶坑的Vnode): 將這個函數當做 compiler 函數 */
/*
Vue中會將真實的DOM結構當作字符串去解析得到一棵 AST
此處使用帶有mustache語法的虛擬DOM來代替 AST
*/
function getVNode(node) {
let nodeType = node.nodeType
let _vnode = null
if (nodeType == 1) {
// 元素
let nodeName = node.nodeName
let attrs = node.attributes
let _attrObj = {}
for (let i = 0; i < attrs.length; i++) {
_attrObj[attrs[i].nodeName] = attrs[i].nodeValue
}
_vnode = new VNode(nodeName, _attrObj, undefined, nodeType)
// 考慮node的子元素
let childNodes = node.childNodes
for (let i = 0; i < childNodes.length; i++) {
_vnode.appendChild(getVNode(childNodes[i]))
}
} else if (nodeType == 3) {
_vnode = new VNode(undefined, undefined, node.nodeValue, nodeType)
}
return _vnode
}
/* 將虛擬DOM轉換成真正的DOM */
function parseVNode(vnode){
// 創建真實的DOM
let type = vnode.type;
let _node = null;
if( type == 3){
return document.createTextNode(vnode.value)
} else if(type == 1){
_node = document.createElement(vnode.tag)
// 屬性
let data = vnode.data // 現在這個data是鍵值對
Object.keys(data).forEach((key)=>{
let attrName = key
let attrValue = data[key]
_node.setAttribute(attrName, attrValue)
})
// 子元素
let children = vnode.children;
children.forEach(subvnode =>{
_node.appendChild(parseVNode(subvnode))
})
return _node
}
}
const mustache = /\{\{(.+?)\}\}/g // 匹配{{}}的正則表達式
// 根據路徑訪問對象成員
function getValueByPath(obj, path) {
let res = obj,
currProp,
props = path.split('.')
while ((currProp = props.shift())) {
res = res[currProp]
}
return res
}
/*
模擬 AST -> VNode 的過程
將帶有坑(mustache語法)的VNode與數據data結合,得到填充數據的VNode:
*/
function combine(vnode, data) {
let _type = vnode.type
let _data = vnode.data
let _tag = vnode.tag
let _value = vnode.value
let _children = vnode.children
let _vnode = null
if (_type == 3) {
// 文本節點
// 對文本處理
_value = _value.replace(mustache, function(_, g) {
return getValueByPath(data, g.trim())
})
_vnode = new VNode(_tag, _data, _value, _type)
} else if (_type == 1) {
// 元素節點
_vnode = new VNode(_tag, _data, _value, _type)
_children.forEach(_subVNode => _vnode.appendChild(combine(_subVNode, data)))
}
return _vnode
}
function JGVue(options) {
// this._options = options;
this._data = options.data
let elm = document.querySelector(options.el)
this._template = elm
this._parent = elm.parentNode
this.mount() // 掛載
}
JGVue.prototype.mount = function() {
// 需要提供一個render方法: 生成虛擬DOM
// if(typeof this._options.render !== 'function'){
// }
this.render = this.createRenderFn() // 帶有緩存
this.mountComponent()
}
JGVue.prototype.mountComponent = function() {
// 執行mountComponent()
let mount = () => {
// update將虛擬DOM渲染到頁面上
this.update(this.render())
}
mount.call(this) // 本質上應該交給 watcher 來調用
// 爲什麼
// this.update(this.render()) // 使用發佈訂閱模式,渲染和計算的行爲應該交給watcher來完成
}
/*
在真正的Vue中,使用了二次提交的設計結構
第一次提交是在內存中,在內存中確定沒有問題了在修改硬盤中的數據
1. 在頁面中的DOM和虛擬DOM是一一對應的關係
*/
// 這裏是生成render函數,目的是緩存抽象語法樹(我們使用虛擬DOM來模擬)
JGVue.prototype.createRenderFn = function() {
let AST = getVNode(this._template)
// 將 AST + data => VNode
// 我們: 帶坑的VNode + data => 含有數據的 VNode
return function render() {
// 將帶坑的VNode轉換爲真正帶數據的VNode
let _tmp = combine(AST, this._data)
return _tmp
}
}
// 將虛擬DOM薰染到頁面中: diff算法就在這裏
JGVue.prototype.update = function(vnode) {
// 簡化,直接生成HTML DOM replaceChild 到頁面中
// 父元素.replaceChild(新元素,舊元素)
let realDOM = parseVNode(vnode)
// debugger
this._parent.replaceChild(realDOM, document.querySelector('#app'))
// 這個算法是不負責任的
// 每次都會將頁面中的DOM全部替換
}
let app = new ({
el: '#app',
data: {
name: '張三',
age: 19
}
})
</script>
</body>
</html>