本篇内容以组件化为主,先来思考一下,组件解析从哪一步开始?是的,应该是从生成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框架(一):基本架构的搭建