componentization
This article focuses on componentization. Where does component parsing start? Yes, it should start from the vNode generation phase. When we componentize the programming, the export is actually a Xue options, so the label we get is actually the options, take a look at the following example:
const HelloWorld = {
// It is not the same
// ...
}
function Fn(){}
render() {
return (
<div>{/* The following tag is parsed by our parsing function, and its tag is actually the HelloWorld object above */}<HelloWorld></HelloWorld>{/* The same goes for functional components. The tag is the function Fn */}.<Fn></Fn>
</div>
);
}
Copy the code
After the JSX code is parsed, we will generate VNode. Let’s change the logic of this section:
class VNode {
constructor(tagMsg, xm) {
this.xm = xm;
this.children = [];
this.attrs = {};
this.events = {};
this.tagType = ' ';
// If it is a JSXObj object, parse it
if(tagMsg instanceof JSXObj) {
this.tag = tagMsg.tag;
Attrs is processed to separate out attributes and events
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;
});
// Check whether it is a native tag
if(NativeTags.includes(this.tag)) this.tagType = 'native';
// This has all been covered before, so skip ahead and go straight to this one
// If an object is passed in, it is considered a Xue component
else if(typeof this.tag === 'object') {
// Componentize logic
this.tagType = 'component';
}
// If it is a function, it is considered a functional component
// Functional components are simpler to handle, just reparse the function return value and pass attrs as props
// Return the result directly, so the current this object is actually the return value of parseJsxObj
else if(typeof this.tag === 'function') {
this.tagType = 'function';
return parseJsxObj(xm, tagMsg.tag(this.attrs)); }}else if(tagMsg === null) {
this.tag = null;
}
// If it is not, it is treated as a text node by default. The tag attribute of the text node is an empty string
else {
this.tag = ' ';
this.text = tagMsg; }}// Omit the following content...
}
Copy the code
After completing the VNode class, the next step is to complete the Element class:
class Element {
constructor(vnode, xm) {
this.xm = xm;
this.tagType = 'native';
// If it is null, no processing is done
if(vnode.tag === null) return;
// Text node
if(vnode.tag === ' ') {
// Return this sentence is not followed by return
this.el = document.createTextNode(vnode.text);
return;
}
// Process non-text nodes
if(vnode.tagType === 'native') {
this.el = document.createElement(vnode.tag);
// Bind attributes
Object.entries(vnode.attrs).forEach(([key, value]) = > {
this.setAttribute(key, value);
});
// Bind events
Object.keys(vnode.events).forEach(key= > {
// Cache the function after bind for subsequent function removal
vnode.events[key] = vnode.events[key].bind(xm);
this.addEventListener(key, vnode.events[key]);
});
}
// Look directly at the handling of components here
// When tagType is component
else if(vnode.tagType === 'component') {
this.tagType = 'component';
// Make its parent vNode the root node of the component instance
vnode.tag.root = vnode.parent && vnode.parent.element.el;
// Cache the parent component
vnode.tag.$parent = xm;
// Pass attrs as props
vnode.tag.$props = vnode.attrs;
// vnode.tag is Xue options
const childXM = new Xue(vnode.tag);
// Reset the current XM and EL to the instance of new child Xue
this.xm = childXM;
this.el = childXM.$el;
// Update the xM corresponding to vNode
vnode.updateXM(childXM);
// After component init is complete, remove component Watcher from stackDep.popTarget(); }}// omit the following
// ...
}
Copy the code
New Xue(options), pass vNode. tag as options, we cannot pass options, we must do some extension:
- Set root to the parent node of the vnode
- Pass attrs as props
Now we have a new instance of the child Xue. When we have the new instance, we need to update the xM and EL of the current element. We also need to update the xM of the vNode. So you must pop the child watcher back to the parent watcher via dep.popTarget (). Here is an implementation of the two methods in the Watcher class:
// During init, there is a process to push the current watcher onto the stack
// Push the current Wacther to the stack
Dep.pushTarget(xm.$watcher);
xm._callHook.call(xm, 'beforeMount');
// in Dep, the code related to loading and unloading the stack
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];
}
// The following content is omitted
// ...
}
Copy the code
So far, our subcomponent has rendered, but so far its props is not reactive, so we need to set reactive for props:
export const initState = function() {
this.$data = this.$options.data() || {};
this.$methods = this.$options.methods;
// Save the props value so that the props can be accessed directly through this.props
this.props = this.$options.$props || {};
const dataNames = Object.keys(this.$data);
const methodNames = Object.keys(this.$methods);
// Check if there are any identical data, methods, or props
const checkedSet = new Set([...dataNames, ...methodNames]);
if(checkedSet.size < dataNames.length + methodNames.length) return warn('you have same name in data, method');
// proxies the properties of data, props, and methods to this
dataNames.forEach(name= > proxy(this.'$data', name));
// propNames.forEach(name => proxy(this, '$props', name));
methodNames.forEach(name= > proxy(this.'$methods', name));
// Set data to reactive
observe(this.$data);
// Set props to responsive
observe(this.props);
}
Copy the code
Observe’s logic has been mentioned previously in Chapter 1 and will not be repeated here. In fact, at this point, the componentized content is done. So let’s write a demo and see
demo
let Child = {
data() {
return {
msg: 'i am test1 in Child:'
}
},
beforeCreate() {
setTimeout((a)= > {
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((a)= > {
this.test1 = 'i am text1 change';
}, 3000)}});Copy the code
The initial render looks like this:
After the 3 s:
After another 1s:
Write a simple routing component
After the component is complete, let’s try to write a routing component using the componentization we wrote. We need a Router component, followed by a Router class to configure options:
export const XueRouterCom = {
render() {
// Get the component under the current route
const Current = this.props.options.getCurrentCom();
return (
<div>
<Current></Current>
</div>); }};// The hash mode is used as an example
export class XueRouterCls {
current = null;
// Refresh the components under the current route
// Use the arrow function to bind this, otherwise this will point to window after addEventListener
refresh = (a)= > {
const currentPath = this.getRoute();
const currentRoute = this.routes.find(item= > item.path === currentPath);
// Throw an error if no match is found
if(! currentRoute)return warn(`no such route ${ currentPath }, this page's route is The ${this.current.path }`);
this.current = currentRoute;
}
constructor({ routes, type = 'hash'{})this.routes = routes;
this.type = type;
// It is initialized by default. By default, the 0th route is taken first, because the following refresh methods may not match due to incorrect input
this.current = routes[0];
// Refresh the components under the current route
this.refresh();
/ / monitoring hashchange
window.addEventListener('hashchange'.this.refresh, false);
}
// Get the components under the current route object
getCurrentCom() {
return this.current && this.current.component;
}
// Get the current route
getRoute() {
if(this.type === 'hash')
return location.hash.slice(1); }};Copy the code
This is a simple implementation of hash routing, um…… It is easy, hahaha.
demo
After completing the routing component, let’s write another demo to test it:
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>); }});Copy the code
Different components are displayed under different routes:
That’s the end of this series of plans for now, because I have a higher priority to do recently, so this part of the content for now, thank you for watching.
Github project address: Click here to jump to
Chapter 1: start from scratch, using the idea of Vue, develop a JS framework of their own (I) : the basic structure of the construction
Chapter two: starting from scratch, using the idea of Vue, the development of a own JS framework (two) : the first rendering
Chapter 3: start from scratch, using the idea of Vue, develop a own JS framework (2) : Update and diff