200行左右代碼模擬vue實現,視圖渲染部分使用React
來代替Snabbdom
,歡迎Star。
項目地址:https://github.com/bplok20010/vue-toyhtml
已實現的參數:node
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; }
示例:react
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)); } });
官方原理圖:
實現基本步驟:git
// 建立觀察對象 // 觀察對象主要使用的是Object.defineProperty或Proxy來實現, const data = observable({ name: 'vue-toy', }); // 渲染模版 const render = function(){ return <h1>{data.name}</h1> } // 計算render的依賴屬性, // 依賴屬性改變時,會從新計算computedFn,並執行監控函數watchFn, // 屬性依賴計算使用棧及能夠了。 // watch(computedFn, watchFn); watch(render, function(newVNode, oldVNode){ update(newVNode, mountNode); }); //初始渲染 mount(render(), mountNode); // 改變觀察對象屬性,若是render依賴了該屬性,則會從新渲染 data.name = 'hello vue toy';
視圖渲染部分(既render)使用的是vdom技術,vue使用
Snabbdom
庫,vue-toy
使用的是react
來進行渲染,因此在render函數裏你能夠直接使用React的JSX語法,不過別忘記import React from 'react'
,固然也能夠使用preact
inferno
等 vdom庫。github
因爲vue的template的最終也是解析並生成render函數,模版的解析可用
htmleParser
庫來生成AST
,剩下就是解析指令並生產代碼,因爲工做量大,這裏就不具體實現,直接使用jsx。dom
一個響應式示例代碼:ide
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";
源碼
觀察對象建立這裏使用Proxy實現,示例:函數
function Observable(data) { return new Proxy(data, { get(target, key) { return target[key]; }, set(target, key, value) { target[key] = value; return true; }, }); }
這就完成了一個對象的觀察,但以上示例代碼雖然能觀察對象,但沒法實現對象屬性改動後通知觀察者,這時還缺乏Watch對象來計算觀察函數的屬性依賴及Notify來實現屬性變動時的通知。this
定義以下:
Watch(data, computedFn, watchFn);
this
非必須大概實現以下:
// Watch.js // 當前正在收集依賴的Watch const CurrentWatchDep = { current: null, }; class Watch { constructor(data, exp, fn) { this.deps = []; this.watchFn = fn; this.exp = () => { return exp.call(data); }; // 保存上一個依賴收集對象 const lastWatchDep = CurrentWatchDep.current; // 設置當前依賴收集對象 CurrentWatchDep.current = this; // 開始收集依賴,並獲取觀察函數返回的值 this.last = this.exp(); // 還原 CurrentWatchDep.current = lastWatchDep; } clearDeps() { this.deps.forEach((cb) => cb()); this.deps = []; } // 監聽依賴屬性的改動,並保存取消回調 addDep(notify) { // 當依賴屬性改變時,從新觸發依賴計算 this.deps.push(notify.sub(() => { this.check(); })); } // 從新執行依賴計算 check() { // 清空全部依賴,從新計算 this.clearDeps(); // 做用同構造函數 const lastWatchDep = CurrentWatchDep.current; CurrentWatchDep.current = this; const newValue = this.exp(); CurrentWatchDep.current = lastWatchDep; const oldValue = this.last; // 對比新舊值是否改變 if (!shallowequal(oldValue, newValue)) { this.last = newValue; // 調用監聽函數 this.watchFn(newValue, oldValue); } } }
觀察對象發生改變後須要通知監聽者,因此還須要實現通知者Notify:
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()); } }
前面的Observable
太簡單了,沒法完成屬性計算的需求,結合上面Watch
Notify
的來調整下Observable。
function Observable(data) { const protoListeners = Object.create(null); // 給觀察數據的全部屬性建立一個Notify each(data, (_, key) => { protoListeners[key] = new Notify(); }); return new Proxy(data, { get(target, key) { // 屬性依賴計算 if (CurrentWatchDep.current) { const watcher = CurrentWatchDep.current; watcher.addDep(protoListener[key]); } return target[key]; }, set(target, key, value) { target[key] = value; if (protoListeners[key]) { // 通知全部監聽者 protoListeners[key].pub(); } return true; }, }); }
好了,觀察者的建立和訂閱都完成了,開始模擬Vue。
vue-toy
使用React
來實現視圖的渲染,因此render函數裏若是使用JSX則須要引入React
既然已經實現了Observable和Watch,那咱們就來實現基本原理的示例:
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); // 在控制檯可看到每秒的輸出信息
這時將mount update的實現換成vdom就能夠完成一個基本的渲染。
但這還不夠,咱們須要抽象並封裝成組件來用。
這裏的Component像是React的高階函數HOC,使用示例:
const Hello = Component({ props: ["msg"], data() { return { counter: 1, }; }, render(h) { return h("h1", null, this.msg, this.counter); }, });
大概實現以下,options
參考文章開頭
function Component(options) { return class extends React.Component { // 省略若干... constructor(props) { super(props); // 省略若干... // 建立觀察對象 this.$data = Observable({ ...propsData, ...methods, ...data }, computed); // 省略若干... // 計算render依賴並監聽 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; } // 生命週期關聯 componentDidMount() { options.mounted?.call(this); } componentWillUnmount() { this.$watcher.clearDeps(); options.destroyed?.call(this); } componentDidUpdate() { options.updated?.call(this); } render() { return this.$children; } }; }
最後建立入口函數Vue,實現代碼以下:
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;
好了,Vue的基本實現完成了。
感謝閱讀。
最後,歡迎Star:https://github.com/bplok20010/vue-toy