vue-toy
About 200 lines of code simulate the implementation of vue. The view rendering part uses React instead of Snabbdom. Welcome to Star.
Project address: https://github.com/bplok20010/vue-toy
Implemented parameters:
interface Options { el: HTMLElement | string; propsData?: Record<string, any>; props?: string[]; name?: string; data?: () => Record<string, any>; methods?: Record<string, (e: Event) => void>; computed?: Record<string, () => any>; watch?: Record<string, (newValue: any, oldValue: any) => any>; render: (h: typeof React.createElement) => React.ReactNode; renderError?: (h: typeof React.createElement, error: Error) => React.ReactNode; mounted?: () => void; updated?: () => void; destroyed?: () => void; errorCaptured?: (e: Error, vm: React.ReactInstance) => void; }
Example:
import Vue from "vue-toy"; const Hello = Vue.component({ render(h){ return h('span', null, 'vue-toy') ; } }) new Vue({ el: document.getElementById("root"), data() { return { msg: "hello vue toy" }; }, render(h) { return h("h1", null, this.msg, h(Hello)); } });
Fundamentals
Official schematic:
Basic steps:
- Creating observation objects with Observable
- Define the render function of the view
- Collect view dependencies and listen for dependency properties
- Render view
- Repeat 3-4
// Create observation objects // The main objects of observation are Object.defineProperty Or Proxy, const data = observable({ name: 'vue-toy', }); // Rendering template const render = function(){ return <h1>{data.name}</h1> } // Calculate the dependent properties of the render, // When the dependency property changes, computedFn will be recalculated and the monitoring function watchFn will be executed, // Property dependency calculation using stack and OK. // watch(computedFn, watchFn); watch(render, function(newVNode, oldVNode){ update(newVNode, mountNode); }); //Initial rendering mount(render(), mountNode); // Change the properties of the observation object. If the render depends on this property, it will render again data.name = 'hello vue toy';
The view rendering part (both render) uses vdom technology, vue uses the Snabbdom library, vue toy uses react for rendering, so you can directly use the JSX syntax of react in the render function, but don't forget to import React from 'react', of course, you can also use the vdom library such as react inferno.
Because the template of vue is also used to parse and generate the render function, the template can be parsed using htmleParser library to generate AST, and the rest is to parse instructions and produce code. Due to the heavy workload, jsx is not used here.
Responsive implementation
A responsive example code:
const data = Observable({ name: "none", }); const watcher =new Watch( data, function computed() { return "hello " + this.name; }, function listener(newValue, oldValue) { console.log("changed:", newValue, oldValue); } ); // changed vue-toy none data.name = "vue-toy";
Observable implementation
Source code
The observation object is created by Proxy. For example:
function Observable(data) { return new Proxy(data, { get(target, key) { return target[key]; }, set(target, key, value) { target[key] = value; return true; }, }); }
This completes the observation of an object. However, although the above example code can observe the object, it cannot Notify the observer when the object property changes. At this time, there is still a lack of Watch object to calculate the property dependency of the observation function and Notify to implement the notification when the property changes.
Watch implementation
It is defined as follows:
Watch(data, computedFn, watchFn);
- The context with data as computedFn is not necessary for this
- computedFn is the observation function and returns the observed data. Watch accountant calculates the dependency attribute.
- watchFn when the returned content of computedFn changes, watchFn will be called and new and old values will be received at the same time
The implementation is as follows:
// Watch.js // Currently collecting dependent watches const CurrentWatchDep = { current: null, }; class Watch { constructor(data, exp, fn) { this.deps = []; this.watchFn = fn; this.exp = () => { return exp.call(data); }; // Save Last Dependency collection object const lastWatchDep = CurrentWatchDep.current; // Set current dependent collection object CurrentWatchDep.current = this; // Start collecting dependencies and get the values returned by the watch function this.last = this.exp(); // reduction CurrentWatchDep.current = lastWatchDep; } clearDeps() { this.deps.forEach((cb) => cb()); this.deps = []; } // Listen for changes in dependent properties and save the cancel callback addDep(notify) { // When the dependency property changes, the dependency calculation will be triggered again this.deps.push(notify.sub(() => { this.check(); })); } // Re perform dependency calculation check() { // Clear all dependencies, recalculate this.clearDeps(); // Same as constructor const lastWatchDep = CurrentWatchDep.current; CurrentWatchDep.current = this; const newValue = this.exp(); CurrentWatchDep.current = lastWatchDep; const oldValue = this.last; // Compare old and new values for changes if (!shallowequal(oldValue, newValue)) { this.last = newValue; // Call listening function this.watchFn(newValue, oldValue); } } }
Notify implementation
After the observation object changes, the listener needs to be notified, so Notify is also needed:
class Notify { constructor() { this.listeners = []; } sub(fn) { this.listeners.push(fn); return () => { const idx = this.listeners.indexOf(fn); if (idx === -1) return; this.listeners.splice(idx, 1); }; } pub() { this.listeners.forEach((fn) => fn()); } }
Adjust Observable
The previous Observable is too simple to fulfill the requirement of attribute calculation. Adjust the Observable based on the Watch Notify above.
function Observable(data) { const protoListeners = Object.create(null); // Create a Notify for all attributes of the observation data each(data, (_, key) => { protoListeners[key] = new Notify(); }); return new Proxy(data, { get(target, key) { // Attribute dependency calculation if (CurrentWatchDep.current) { const watcher = CurrentWatchDep.current; watcher.addDep(protoListener[key]); } return target[key]; }, set(target, key, value) { target[key] = value; if (protoListeners[key]) { // Notify all listeners protoListeners[key].pub(); } return true; }, }); }
OK, the observer's creation and subscription are finished, and start to simulate Vue.
Simulate Vue
Vue toy uses React to render the view, so if JSX is used in render function, React needs to be introduced
get ready
Now that we have implemented Observable and Watch, let's implement the example of the basic principle:
import Observable from "vue-toy/cjs/Observable"; import Watch from "vue-toy/cjs/Watch"; function mount(vnode) { console.log(vnode); } function update(vnode) { console.log(vnode); } const data = Observable({ msg: "hello vue toy!", counter: 1 }); function render() { return `render: ${this.counter} | ${this.msg}`; } new Watch(data, render, update); mount(render.call(data)); setInterval(() => data.counter++, 1000); // Output information per second can be seen in the console
At this time, you can complete a basic rendering by replacing the implementation of mount update with vdom.
But this is not enough. We need to abstract and encapsulate it into components.
Component
The Component here is like the higher-order function HOC of React. Use example:
const Hello = Component({ props: ["msg"], data() { return { counter: 1, }; }, render(h) { return h("h1", null, this.msg, this.counter); }, });
The implementation is as follows. The options reference article begins
function Component(options) { return class extends React.Component { // Omit several constructor(props) { super(props); // Omit several // Create observation objects this.$data = Observable({ ...propsData, ...methods, ...data }, computed); // Omit several // Calculate render dependency and listen this.$watcher = new Watch( this.$data, () => { return options.render.call(this, React.createElement); }, debounce((children) => { this.$children = children; this.forceUpdate(); }) ); this.$children = options.render.call(this, React.createElement); } shouldComponentUpdate(nextProps) { if ( !shallowequal( pick(this.props, options.props || []), pick(nextProps, options.props || []) ) ) { this.updateProps(nextProps); this.$children = options.render.call(this, React.createElement); return true; } return false; } // Lifecycle correlation componentDidMount() { options.mounted?.call(this); } componentWillUnmount() { this.$watcher.clearDeps(); options.destroyed?.call(this); } componentDidUpdate() { options.updated?.call(this); } render() { return this.$children; } }; }
Create the main function Vue
Finally, the entry function Vue is created, and the implementation code is as follows:
export default function Vue(options) { const RootComponent = Component(options); let el; if (typeof el === "string") { el = document.querySelector(el); } const props = { ...options.propsData, $el: el, }; return ReactDOM.render(React.createElement(RootComponent, props), el); } Vue.component = Component;
OK, the basic implementation of Vue is finished.
Thank you for reading.
Finally, welcome to Star: https://github.com/bplok20010/vue-toy