本篇內容以組件化爲主,先來思考一下,組件解析從哪一步開始?是的,應該是從生成vnode階段開始。當咱們組件化進行編程時,咱們export導出的實際上是一個Xue的options,因此咱們獲取到的標籤,其實就是這個options,看一下下面的例子:node
const HelloWorld = {
// 省略了具體內容
// ...
}
function Fn(){}
render() {
return (
<div> {/* 下面這個標籤由咱們的解析函數解析後,其tag其實就是上面的HelloWorld對象 */} <HelloWorld></HelloWorld> {/* 函數式組件也是同理,tag爲函數Fn */} <Fn></Fn> </div>
);
}
複製代碼
瞭解瞭解析過程以後,就開始完善咱們的代碼,首先在解析完JSX代碼後,咱們會生成VNode,讓咱們來改一下這一塊的邏輯:git
class VNode {
constructor(tagMsg, xm) {
this.xm = xm;
this.children = [];
this.attrs = {};
this.events = {};
this.tagType = '';
// 若是是JSXObj對象,則進行解析
if(tagMsg instanceof JSXObj) {
this.tag = tagMsg.tag;
// 對attrs進行處理,分離出屬性和事件
tagMsg.attrs && Object.entries(tagMsg.attrs).forEach(([key, value]) => {
if(key.match(/on[A-Z][a-zA-Z]*/)) {
const eventName = key.substring(2, 3).toLowerCase() + key.substring(3);
this.events[eventName] = value;
}
else this.attrs[key] = value;
});
// 判斷是不是原生標籤
if(NativeTags.includes(this.tag)) this.tagType = 'native';
// 上面的內容以前都介紹過,因此跳過,直接看這一塊
// 若是傳入的是一個對象,則認爲是Xue組件
else if(typeof this.tag === 'object') {
// 組件化邏輯
this.tagType = 'component';
}
// 若是是一個函數,則認爲是一個函數式組件
// 函數式組件處理較爲簡單,只須要從新解析一下函數的返回值便可,並把attrs做爲props傳入
// 這裏直接return瞭解析結果,因此當前的this對象其實是parseJsxObj的返回值
else if(typeof this.tag === 'function') {
this.tagType = 'function';
return parseJsxObj(xm, tagMsg.tag(this.attrs));
}
}
else if(tagMsg === null) {
this.tag = null;
}
// 若是不是,則默認當作文本節點處理,文本節點的tag屬性爲空字符串
else {
this.tag = '';
this.text = tagMsg;
}
}
// 省略下面的內容...
}
複製代碼
完善了VNode類以後,接下來就是完善Element類:github
class Element {
constructor(vnode, xm) {
this.xm = xm;
this.tagType = 'native';
// 若是爲null的話,則不作任何處理
if(vnode.tag === null) return;
// 文本節點
if(vnode.tag === '') {
// 這句話不能接在return後
this.el = document.createTextNode(vnode.text);
return;
}
// 處理非文本節點
if(vnode.tagType === 'native') {
this.el = document.createElement(vnode.tag);
// 綁定屬性
Object.entries(vnode.attrs).forEach(([key, value]) => {
this.setAttribute(key, value);
});
// 綁定事件
Object.keys(vnode.events).forEach(key => {
// 緩存bind後的函數,用於以後的函數移除
vnode.events[key] = vnode.events[key].bind(xm);
this.addEventListener(key, vnode.events[key]);
});
}
// 直接看這裏對組件的處理
// 當tagType類型爲組件時
else if(vnode.tagType === 'component') {
this.tagType = 'component';
// 將它的父級vnode做爲組件實例的根節點
vnode.tag.root = vnode.parent && vnode.parent.element.el;
// 緩存其父組件
vnode.tag.$parent = xm;
// 將attrs做爲props傳入
vnode.tag.$props = vnode.attrs;
// vnode.tag就是Xue的options
const childXM = new Xue(vnode.tag);
// 重置當前的xm和el爲新建子Xue的實例
this.xm = childXM;
this.el = childXM.$el;
// 更新vnode對應的xm
vnode.updateXM(childXM);
// 組件init完成後,把組件的Watcher出棧
Dep.popTarget();
}
}
// 省略下面的內容
// ...
}
複製代碼
首先,在生成Element實例的時候,當咱們遇到component類型的vnode後,確定要作的事就是new Xue(options)
,將vnode.tag
做爲options
傳入,可是不能直接將options傳入,必須得先作一些擴展:編程
經過擴展後,咱們就拿到了新的子Xue實例,拿到了新的實例後,咱們就得更新當前element的xm和el,同時也須要更新vnode對應的xm,這時候Dep.target
指向的是子的Xue的render watcher
,因此必須經過Dep.popTarget()
彈出子watcher,回到父watcher。下面是watcher類中這兩個方法的實現:緩存
// 在init過程當中,會有一個把當前watcher入棧的過程
// 把當前Wacther入棧
Dep.pushTarget(xm.$watcher);
xm._callHook.call(xm, 'beforeMount');
// Dep中,入棧出棧相關的代碼
let targetList = [];
class Dep {
static target = null;
static pushTarget(watcher) {
targetList.push(watcher);
Dep.target = watcher;
}
static popTarget() {
targetList.pop();
const length = targetList.length;
if(length > 0)
Dep.target = targetList[length - 1];
}
// 如下內容省略
// ...
}
複製代碼
到如今爲止,咱們的子組件已經能夠渲染出來了,可是目前爲止它的props還不是響應式的,因此咱們須要爲props設置響應式:架構
export const initState = function() {
this.$data = this.$options.data() || {};
this.$methods = this.$options.methods;
// 保存props值,這樣能夠直接經過this.props.xxx訪問props
this.props = this.$options.$props || {};
const dataNames = Object.keys(this.$data);
const methodNames = Object.keys(this.$methods);
// 檢測是否有重名的data,methods或者props
const checkedSet = new Set([...dataNames, ...methodNames]);
if(checkedSet.size < dataNames.length + methodNames.length) return warn('you have same name in data, method');
// 分別爲data,props,methods中的屬性代理到this上
dataNames.forEach(name => proxy(this, '$data', name));
// propNames.forEach(name => proxy(this, '$props', name));
methodNames.forEach(name => proxy(this, '$methods', name));
// 將data設置爲響應式
observe(this.$data);
// 將props設置爲響應式
observe(this.props);
}
複製代碼
observe的邏輯以前在第一章已經提過了,這裏就再也不復述了。其實,到了這裏,組件化的內容就已經完成了。讓咱們寫個demo看一下app
let Child = {
data() {
return {
msg: 'i am test1 in Child:'
}
},
beforeCreate() {
setTimeout(() => {
this.msg = 'hello world:'
}, 4000)
},
render() {
return (<div> { this.msg } { this.props.test } </div>)
}
};
function Child2(props) {
return (<div>i am test1 in Child2:{ props.test }</div>)
}
let father = new Xue({
root: '#app',
data() {
return {
test1: 'i am text1',
}
},
render() {
return (<div> <div> i am test1 in father:{ this.test1 } </div> <Child test={ this.test1 }></Child> <Child2 test={ this.test1 }></Child2> </div>);
},
mounted() {
setTimeout(() => {
this.test1 = 'i am text1 change';
}, 3000)
}
});
複製代碼
開始的渲染結果是這樣的:框架
3s後:函數
再過1s後:組件化
組件完成後,讓咱們嘗試用咱們寫好的組件化功能來寫一個路由組件,那麼咱們就須要一個router組件,接下來就是一個router類用來配置options:
export const XueRouterCom = {
render() {
// 獲取當前路由下的組件
const Current = this.props.options.getCurrentCom();
return (
<div> <Current></Current> </div>
);
}
};
// 這裏以hash模式爲例
export class XueRouterCls {
current = null;
// 刷新當前路由下的組件
// 採用箭頭函數來綁定this,否則在addEventListener後this會指向window
refresh = () => {
const currentPath = this.getRoute();
const currentRoute = this.routes.find(item => item.path === currentPath);
// 匹配不到時拋出錯誤
if(!currentRoute) return warn(`no such route ${ currentPath }, this page's route is ${ this.current.path }`);
this.current = currentRoute;
}
constructor({ routes, type = 'hash' }) {
this.routes = routes;
this.type = type;
// 默認初始化,默認先取第0個路由下,由於下面的refresh方法可能由於不正確的輸入致使匹配不到
this.current = routes[0];
// 刷新當前路由下的組件
this.refresh();
// 監聽hashchange
window.addEventListener('hashchange', this.refresh, false);
}
// 獲取當前route對象下的組件
getCurrentCom() {
return this.current && this.current.component;
}
// 獲取當前路由
getRoute() {
if(this.type === 'hash')
return location.hash.slice(1);
}
};
複製代碼
這裏其實就是簡單的實現了hash模式下的路由,嗯......的確挺簡單的,哈哈哈。
完成路由組件後,讓咱們再寫個demo測試一下:
function Child1(props) {
return (<div>hello world1</div>)
}
function Child2(props) {
return (<div>hello world2</div>)
}
const router = new XueRouterCls({
routes: [
{
path: '/hello1',
component: Child1
},
{
path: '/hello2',
component: Child2
}
]
});
let c = new Xue({
root: '#app',
render() {
return (<div> <XueRouterCom options={ router }></XueRouterCom> </div>);
},
});
複製代碼
不一樣路由下顯示不一樣的組件:
目前這一系列打算就先到這裏了,由於最近有更高優先級的事情要作,因此這部份內容就先到此爲止啦,謝謝你們觀看。
github項目地址:點此跳轉
第一章:從零開始,採用Vue的思想,開發一個本身的JS框架(一):基本架構的搭建