vue-toy: 200行代碼模擬Vue實現

vue-toy

200行左右代碼模擬vue實現,視圖渲染部分使用React來代替Snabbdom,歡迎Star。
項目地址:https://github.com/bplok20010/vue-toyhtml

codesandbox示例vue

已實現的參數: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

  1. 使用Observable建立觀察對象
  2. 定義好視圖既render函數
  3. 收集視圖依賴,並監聽依賴屬性
  4. 渲染視圖
  5. 重複3-4
// 建立觀察對象
// 觀察對象主要使用的是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庫。

因爲vue的template的最終也是解析並生成render函數,模版的解析可用htmleParser庫來生成AST,剩下就是解析指令並生產代碼,因爲工做量大,這裏就不具體實現,直接使用jsx。github

響應式實現

一個響應式示例代碼:dom

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實現

源碼
觀察對象建立這裏使用Proxy實現,示例:ide

function Observable(data) {
    return new Proxy(data, {
        get(target, key) {
            return target[key];
        },
        set(target, key, value) {
            target[key] = value;
            return true;
        },
    });
}

這就完成了一個對象的觀察,但以上示例代碼雖然能觀察對象,但沒法實現對象屬性改動後通知觀察者,這時還缺乏Watch對象來計算觀察函數的屬性依賴及Notify來實現屬性變動時的通知。函數

Watch實現

源碼this

定義以下:

Watch(data, computedFn, watchFn);
  • data 爲 computedFn 的 上下文 既 this 非必須
  • computedFn 爲觀察函數並返回觀察的數據,Watch會計算出裏面的依賴屬性。
  • watchFn 當computedFn 返回內容發生改變時,watchFn會被調用,同時接收到新、舊值

大概實現以下:

// 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實現

觀察對象發生改變後須要通知監聽者,因此還須要實現通知者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

前面的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

vue-toy 使用React來實現視圖的渲染,因此render函數裏若是使用JSX則須要引入React

準備

既然已經實現了Observable和Watch,那咱們就來實現基本原理的示例:

codesandbox示例

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

源碼

這裏的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

最後建立入口函數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

相關文章
相關標籤/搜索