200行左右代码模拟vue实现,视图渲染部分使用React来代替Snabbdom,欢迎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)); } }); 基本原理官方原理图:
实现基本步骤:
使用Observable创建观察对象
定义好视图既render函数
收集视图依赖,并监听依赖属性
渲染视图
重复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实现,示例:
这就完成了一个对象的观察,但以上示例代码虽然能观察对象,但无法实现对象属性改动后通知观察者,这时还缺少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实现