从零开始,采用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

相关文章
相关标签/搜索