vue-toy: 200行代码模拟Vue实现

九路 等级 546 0 0

vue-toy

200行左右代码模拟vue实现,视图渲染部分使用React来代替Sanbbdom,欢迎Star。 项目地址:https://github.com/bplok20010/vue-toy

codesandbox示例

已实现的参数:

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;
} 

示例:

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));
  }
}); 

基本原理

官方原理图: vue-toy: 200行代码模拟Vue实现 实现基本步骤:

  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。

响应式实现

一个响应式示例代码:

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实现,示例:

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实现

源码

定义如下:

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

收藏
评论区

相关推荐

【官宣】Vue 3.0 发布!
Vue 团队于 2020 年 9 月 18 日晚 11 点半发布了 Vue 3.0 版本,我们连夜对 Release 进行了翻译。由于时间仓促,文中如有翻译不当的地方还望提出。如有侵权,请联系删帖。以下为译文正文。 原文:https://github.com/vuejs/vuenext/releases 作者:Vue 团队 译文:https://zh
教你用200行代码写一个爱豆拼拼乐H5小游戏(附源码)
前言 本文将带大家一步步实现一个H5拼图小游戏,考虑到H5游戏的轻量级和代码体积,我没有使用react或vue这些框架,而采用我自己写的dom库和原生javascript来实现业务功能,具体库代码可见我的文章如何用不到200行代码写一款属于自己的js类库(https://juejin.im/post/6844903880707293198),构建工具我采
vue-toy: 200行代码模拟Vue实现
vuetoy 200行左右代码模拟vue实现,视图渲染部分使用React来代替Sanbbdom,欢迎Star。 项目地址:https://github.com/bplok20010/vuetoy(https://github.com/bplok20010/vuetoy) codesandbox示例(https://codes
前端组件/库打包利器rollup使用与配置实战
目前主流的前端框架vue和react都采用rollup来打包,为了探索rollup的奥妙,接下来就让我们一步步来探索,并基于rollup搭建一个库打包脚手架,来发布自己的库和组件。 (https://imghelloworld.osscnbeijing.aliyuncs.com/imgs/16cb1c297071015523fb08d9e0f
基于jsoneditor二次封装一个可实时预览的json编辑器组件(react版)
前言 做为一名前端开发人员,掌握vue/react/angular等框架已经是必不可少的技能了,我们都知道,vue或react等MVVM框架提倡组件化开发,这样一方面可以提高组件复用性和可扩展性,另一方面也带来了项目开发的灵活性和可维护,方便多人开发协作.接下来文章将介绍如何使用react,开发一个自定义json编辑器组件.我们这里使用了jsoneditor
《精通react/vue组件设计》之用纯css打造类materialUI的按钮点击动画并封装成react组件
前言 作为一个前端框架的重度使用者,在技术选型上也会非常注意其生态和完整性.笔者先后开发过基于vue,react,angular等框架的项目,碧如vue生态的elementUI, antdesignvue, iView等成熟的UI框架, react生态的antdesign, materialUI等,这些第三方UI框架极大的降低了我们开发一个项目的成本和
Taro 入门教程
简介 Taro 是一个遵循 React 语法规范的开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发微信/京东/百度/
基于Vue实现一个有点意思的拼拼乐小游戏
笔者去年曾写过一个类似的拼拼乐小游戏,技术栈采用自己的Xuery框架和原生javascript实现的,脚手架采用gulp来实现,为了满足对vue的需求,笔者再次使用vue生态将其重构,脚手架采用比较火的vuecli。 前言 为了加深大家对vue的了解和vue项目实战,笔者采用vue生态来重构此项目,方便大家学习和探索。技术栈如下: vuecli4
有关CSS预编译、管理工具和网络安全的相关面试题
我经常会听到一些想入行前端的人问,前端开发需要学习哪些技术?其实,除了核心的HTML、CSS、JavaScript技术外,像CSS预编译、前端框架,如Vue,React、Node、Angular;代码管理工具,如git,代码编辑器,sublime text,还有测试,网络安全等相关技术也是需要会一些。当然要求可能不会那么高,但至少要懂一些。所有今天准备了一些
【Flutter实战】状态管理
3.2 状态管理响应式的编程框架中都会有一个永恒的主题——“状态(State)管理”,无论是在React/Vue(两者都是支持响应式编程的Web开发框架)还是Flutter中,他们讨论的问题和解决的思想都是一致的。所以,如果你对React/Vue的状态管理有了解,可以跳过本节。言归正传,我们想一个问题,StatefulWidget的状态应该被谁管理?
2021前端技术面试必备Vue:(四)Vuex状态管理
前三章陆续已经更新了Vue基本使用 和Vue Router的基本使用,如果你读了前三篇文章的话,并且掌握差不多,那么你现在可以开发简单的应用了,例如Blog,电商网站........唯一不足的是,随着你的业务需求不断增加,页面中的状态数据也不断庞大,维护起来就特别困难了,Vue 提供了一种状态管理 解决办法 Vuex,它的思想和React 的R
30 道 Vue 面试题,内含详细讲解(涵盖入门到精通,自测 Vue 掌握程度)
30 道 Vue 面试题,内含详细讲解(涵盖入门到精通,自测 Vue 掌握程度)(https://www.zhihu.com/people/huobodexiaobaiyang).css1cd9gw4{marginleft:.3em;}545 人赞同了该文章前言
前端培训-Vue专题之Vue基础
简介特点:MVVM框架,双向绑定,数据驱动,单页面,组件化。 区别Vue 和 jQuery 的区别:不直接操作DOM,而是操作数据。案例:Hello World 你好,世界HTML代码:xml<h1msg</h1jQuery实现javascript$("h1").text("你好,世界");Vue 实现javascriptthis.msg '你好,世界'
vue项目无配置文件解决方案
问题在开发vue项目时,需要重新设置项目的启动端口,发现项目目录中并没有相关的配置文件【config目录】vue脚手架版本【通过vue version查询】:@vue/cli 4.5.13解决方案在vue项目根目录下创建vue.config.js配置文件vue.config.jsmodule.exports devServer: d
项目中的富文本编辑器该如何选择?
项目中经常需要用到富文本编辑器的时候,而常见的富文本编辑器都有哪些?该如何选择?先看看市面上都有哪些可用的富文本编辑器: (插件式的,支持 Vue,React,Angular 框架) (Typescript 开发的 Web 富文本编辑器, 轻量、简洁、易用、开源免费,支持 JS 直接引入使用,或者 Vue2/3,React) (开源,插件多,功能齐全,支持