從零開始,採用Vue的思想,開發一個本身的JS框架(四):組件化和路由組件

組件化

本篇內容以組件化爲主,先來思考一下,組件解析從哪一步開始?是的,應該是從生成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傳入,必須得先作一些擴展:編程

  1. 將root設爲vnode的父節點
  2. 將attrs做爲props傳入

經過擴展後,咱們就拿到了新的子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

demo

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)
  }
});
複製代碼

開始的渲染結果是這樣的:框架

avatar

3s後:函數

avatar

再過1s後:組件化

avatar

寫一個簡單的路由組件

組件完成後,讓咱們嘗試用咱們寫好的組件化功能來寫一個路由組件,那麼咱們就須要一個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

完成路由組件後,讓咱們再寫個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>);
  },
});

複製代碼

不一樣路由下顯示不一樣的組件:

avatar

avatar

目前這一系列打算就先到這裏了,由於最近有更高優先級的事情要作,因此這部份內容就先到此爲止啦,謝謝你們觀看。

github項目地址:點此跳轉

第一章:從零開始,採用Vue的思想,開發一個本身的JS框架(一):基本架構的搭建

第二章:從零開始,採用Vue的思想,開發一個本身的JS框架(二):首次渲染

第三章:從零開始,採用Vue的思想,開發一個本身的JS框架(二):update和diff

相關文章
相關標籤/搜索