preface
This series of articles will document my attempts to develop a JS framework. After all, I have been using Vue and React for some time. Try to learn from their ideas (mainly Vue) and see if I can build a complete framework from scratch, which is also a deep understanding of Vue. PS: This article won’t go into too much detail.
Attached project address: click me jump
So let’s go back to the original question, what am I building? My goal is to build a vUe-like framework, so the responsive core should be done with Object.defineProperty, and at the template level I’m going to go JSX and use the Render function to parse the template. So I named it Xue, which is also the pronunciation of “learn”. Now that you know what you want to do, start rolling up your sleeves.
The preparatory work
Since it is starting from scratch, the first step is of course to create a folder named Xue, think about whether there is a little excited. Second, create a project with NPM init to create package.json. Step 3: Install webpack-dev-server, Babel, etc. Fourth, configure webpack.config.js and.babelrc.
My final directory is as follows:
-node_modules-src -- modules -- element.js -- holds node-related operations -- hooks. Js -- holds life-cycle operations -- init.js -- holds initialization operations -- State.js -- holds data binding related operations -- utils -- -- main.js -.babelrc-index.html - package-lock.json - package.json - webpack.config.jsCopy the code
To explain how to parse JSX, check out the Babel website link. So far we have only used parsing functions, so the configuration is as follows:
{
"presets": [
[
"@babel/preset-react",
{
"pragma": "parseJSX", // default pragma is React.createElement
}
]
]
}
Copy the code
We use the parseJSX function for parsing. For React, this is the familiar React. CreateElement method. Then let’s write a parseJSX method:
function parseJSX(tag, attrs, ... children) {
return {
tag,
attrs,
children
}
}
Copy the code
When encountering a JSX such as const a =
, our parsing function parseJSX takes three arguments and returns them directly as an object, equivalent to the following code:
const a = {
tag: 'div'.attrs: {
className: 'class_a'
},
children: ['hahaha']}Copy the code
At this point, the preparatory work has almost done, next into the body.
Start with the Xue constructor
First write a constructor for Xue:
function Xue(options) {
this.init(this, options);
}
// Extend the prototype with mixin methods
// Here are some possible mixins to fill in step by step
stateMixin(Xue);
elementMixin(Xue);
initMixin(Xue);
hooksMixin(Xue);
Copy the code
Why not use class here, because the logic is going to be a little bit complicated, so we need to write in modules, and it feels weird to mix class with extended prototype, even though class is just syntactic sugar.
Then verify some parameters of options:
function Xue(options) {
// OPTIONS_NORM is an object constant that contains the types of possible values for options
// Use OPTIONS_NORM as the standard to check the options type, and throw a message if the requirements do not meet and perform default assignment operations to prevent internal crashes due to options type
checkAndAssignDefault(options, OPTIONS_NORM);
if(typeofoptions ! = ='object') return warn(`options should be an object rather than a/an The ${typeof options }`);
this.init(this, options);
}
Copy the code
Next we extend a few simpler prototyping approaches:
// Mount the parse method to the Xue prototype instead of the parseJSX method above
export default const elementMixin = function(Xue) {
Xue.prototype._parseJSX = (tag, attrs, ... children) = > {
return {
tag,
attrs,
children
}
}
}
// Extend lifecycle-related methods
// HOOK_NAMES is a constant that holds life cycle names, which is an array
import { HOOK_NAMES } from '.. /utils/constant';
export const hooksMixin = function(Xue) {
// The lifecycle calls the method, where call is needed to modify the lifecycle this reference
Xue.prototype._callHook = function(hookName) {
this.$hooks[hookName].call(this);
};
};
// Initialize the lifecycle method by mounting it into $hooks
export const initHooks = function() {
HOOK_NAMES.forEach(item= > {
this.$hooks[item] = this.$options[item]; })};Copy the code
Then start writing our init method:
export const initMixin = function(Xue) {
Xue.prototype.init = (xm, options) = > {
// Cache options and render
xm.$options = options;
xm.$render = xm.$options.render.bind(xm);
this.$hooks = {};
// Initialize the life cycle
initHooks.call(xm);
xm._callHook.call(xm, 'beforeCreate');
// Initialize the data mount
initState.call(xm);
xm._callHook.call(xm, 'created');
// Call render to generate a VNode for reactive binding
// ...
xm._callHook.call(xm, 'beforeMount');
/ / mount DOM
// ...
xm._callHook.call(xm, 'mounted'); }};Copy the code
At this point, our init method framework has been built.
Data mount
Next, complete the initState method to mount the data. Here, for convenience, I use the data structure of Set to determine whether there are data, methods, and props with the same name, and then throw the error. Of course, this approach does not specify which variable is the same name, but it is probably better to walk through it.
// Initialize the data broker
export const initState = function() {
this.$data = this.$options.data() || {};
this.$props = this.$options.props;
this.$methods = this.$options.methods;
const dataNames = Object.keys(this.$data);
const propNames = Object.keys(this.$props);
const methodNames = Object.keys(this.$methods);
// Check if there are any identical data, methods, or props
const checkedSet = new Set([...dataNames, ...propNames, ...methodNames]);
if(checkedSet.size < dataNames.length + propNames.length + methodNames.length) return warn('you have same name in data, method, props');
// 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, as described below
observe(this.$data);
}
Copy the code
The proxy method is data hijacking via Object.defineProperty, as you all know.
export default function proxy(xm, sourceKey, key) {
Object.defineProperty(xm, key, {
get() {
returnxm[sourceKey][key]; }, set(newV) { xm[sourceKey][key] = newV; }})}Copy the code
So let’s talk about concrete, reactive implementations, and let’s talk about diagrams, and it’s time to show my soul painting.
For each piece of data, there is a dependency collector, the DEP, which holds all observers related to the current piece of data, the Watcher. The dependency collection process is completed when the render function is called, because in the process of calling render to generate a VNode, the get attribute is used. When the data changes, it notifies its observers by calling set.
Next is the time of the code, the details of things first do not discuss, the most basic function to achieve:
// Define the response by iterating over $data recursively
function observe(obj) {
Object.entries(obj).forEach(([key, value]) = > {
defineReactive(obj, key);
if(typeof value === 'object') observe(value); })}// Perform data hijacking for data
function defineReactive(target, key) {
let value = target[key];
let dep = new Dep();
Object.defineProperty(target, key, {
get() {
dep.depend();
Dep.target.addDep(dep);
returnvalue; }, set(newV) { value = newV; dep.notify(); }})}Copy the code
Dep
import { addUpdateQueue } from './queue';
let id = 0;
class Dep {
// Static property to ensure that only one watcher is currently executing
static target = null;
constructor() {
this.id = id++;
this.watchers = [];
}
depend() {
const watcherIds = this.watchers.map(item= > item.id);
// Prevent repeated additions
if(Dep.target && ! watcherIds.includes(Dep.target.id))this.watchers.push(Dep.target);
}
changeWatcher(watcher) {
Dep.target = watcher;
}
notify() {
addUpdateQueue(this.watchers); }}export default Dep;
Copy the code
watcher
let id = 0;
class Watcher {
constructor() {
this.id = id++;
this.deps = [];
}
addDep(dep) {
const depIds = this.deps.map(item= > item.id);
if(dep && ! depIds.includes(dep.id))this.deps.push(dep);
}
run() {
console.log('i have update')}}export default Watcher;
Copy the code
queue
// Add watcer to queue
let queue = [];
export const addUpdateQueue = function(watchers) {
const queueSet = new Set([...queue, ...watchers]);
queue = [...queueSet];
// Perform the sort operation, the sort logic is not determined
// queue.sort();
// Execute in nextTick instead
queue.forEach(watcher= > watcher.run());
}
Copy the code
Then we simply call the Render method during init
// Run before beforeMount
// Dep. Target is assigned this way and will be improved later
Dep.target = xm.$watcher = new Watcher('render', xm.$render);
xm._callHook.call(xm, 'beforeMount');
Copy the code
So far, our framework has a prototype, in the next section, we will generate VNode and insert its corresponding part into the DOM for implementation, please wait patiently for the update……
PS: If you find this article helpful, please give a thumbs up or a star. Thank you.
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 (three) : Update and diff
Chapter 4: starting from scratch, using the idea of Vue, develop a own JS framework (4) : componentization and routing components