当前位置: 首页 > Web前端 > vue.js

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

时间:2023-03-31 16:00:26 vue.js

vue-toy约200行代码模拟Vue实现,视图渲染部分使用React代替Snabbdom,欢迎Star。项目地址:https://github.com/bplok20010/vue-toycodesandbox展示示例已经实现的参数:interfaceOptions{el:HTMLElement|细绳;propsData?:Record;道具?:字符串[];名称?:字符串;data?:()=>Record;methods?:Recordvoid>;computed?:Recordany>;watch?:Recordany>;render:(h:typeofReact.createElement)=>React.ReactNode;renderError?:(h:typeofReact.createElement,error:错误)=>React.ReactNode;安装?:()=>无效;更新了吗?:()=>无效;销毁?:()=>void;errorCaptured?:(e:Error,vm:React.ReactInstance)=>void;}示例:importVuefrom"vue-toy";constHello=Vue.component({render(h){returnh('span',null,'vue-toy');}})newVue({el:document.getElementById("root"),data(){return{msg:"hellovuetoy"};},render(h){返回h("h1",null,this.msg,h(Hello));}});基本原理官方示意图:实现基本步骤:使用Observable创建观察对象,定义视图和收集渲染函数查看依赖,监听依赖属性渲染视图重复3-4//创建观察对象//观察对象主要是使用Object.definePropertyorProxy,constdata=observable({name:'vue-toy',});//渲染模板constrender=function(){return

{data.name}

}//计算render的依赖属性,//当依赖属性发生变化时,会重新计算computedFn并执行监听函数watchFn,//属性依赖计算使用栈就可以了//watch(computedFn,watchFn);watch(render,function(newVNode,oldVNode){update(newVNode,mountNode);});//初始渲染mount(render(),mountNode);//改变观察对象的属性。如果render依赖这个属性,它会重新渲染data.name='hellovuetoy';view渲染部分(都是render)使用了vdom技术,vue使用了Snabbdom库,vue-toy使用react进行渲染,所以在render函数中可以直接使用React的JSX语法,但是不要忘记importReactfrom'react',当然也可以使用preactinferno等vdom库。由于最终解析了vue的模板并生成了一个render函数,所以可以使用htmleParser库生成AST进行模板解析,剩下的就是解析指令和生成代码了。由于工作量大,这里就不实现了,直接使用jsx。Reactive实现一个响应式示例代码:constdata=Observable({name:"none",});constwatcher=newWatch(data,functioncomputed(){return"hello"+this.name;},functionlistener(newValue,oldValue){console.log("changed:",newValue,oldValue);});//改变了vue-toynonedata.name="vue-toy";Observable实现源码观察对象的创建。这里使用Proxy来实现,例子:functionObservable(data){returnnewProxy(data,{get(target,key){returntarget[key];},set(target,key,value){target[key]=value;returntrue;},});}这样就完成了对一个对象的观察,但是上面的示例代码虽然可以观察到对象,但是无法在对象属性发生变化后通知观察者。这时候还缺少Watch对象来计算观察函数的属性依赖和Notify来实现属性变化时的通知。Watch实现源码定义如下:Watch(data,computedFn,watchFn);data是computedFn的上下文,这不是必需的。ComputedFn是一个watch函数,返回观察到的数据,Watch会计算里面的依赖属性。watchFn当computedFn返回的内容发生变化时,会调用watchFn,同时接收新旧值。实现大致如下://Watch.js//Watchconst当前收集依赖CurrentWatchDep={current:null,};classWatch{constructor(data,exp,fn){this.deps=[];这个.watchFn=fn;this.exp=()=>{returnexp.call(data);};//保存最后一个依赖集合对象constlastWatchDep=CurrentWatchDep.current;//设置当前依赖集合对象CurrentWatchDep.current=this;//开始收集依赖并获取监视函数返回的值this.last=this.exp();//恢复CurrentWatchDep.current=lastWatchDep;}clearDeps(){this.deps.forEach((cb)=>cb());这个.deps=[];}//监听依赖属性的变化并保存取消回调addDep(notify){//当依赖属性发生变化时,重新触发依赖计算this.deps.push(notify.sub(()=>{this。查看();}));}//重新执行依赖计算check(){//清除所有依赖,重新计算this.clearDeps();//与构造函数相同的功能constlastWatchDep=CurrentWatchDep.当前的;CurrentWatchDep.current=this;constnewValue=this.exp();CurrentWatchDep.current=lastWatchDep;constoldValue=this.last;//比较新旧值是否发生变化if(!shallowequal(oldValue,newValue)){this.最后=新值;//调用监听函数this.watchFn(newValue,oldValue);}}}Notify需要在观察到的对象发生变化后通知监听器,所以也需要实现Notify:classNotify{constructor(){this.听众=[];}sub(fn){this.listeners.push(fn);return()=>{constidx=this.listeners.indexOf(fn);如果(idx===-1)返回;这个.listeners.splice(idx,1);};}pub(){this.listeners.forEach((fn)=>fn());}}Observable前面调整Observable过于简单,无法完成属性计算需求,结合上面的WatchNotify调整ObservablefunctionObservable(data){constprotoListeners=Object.create(null);//为可观察数据的所有属性创建一个Notifyeach(data,(_,key)=>{protoListeners[key]=newNotify();});returnnewProxy(data,{get(target,key){//属性依赖计算if(CurrentWatchDep.current){constwatcher=CurrentWatchDep.current;watcher.addDep(protoListener[key]);}returntarget[key];},set(target,key,value){target[key]=value;if(protoListeners[key]){//通知所有监听器protoListeners[key].pub();}returntrue;},});}好了,观察者的创建和订阅就完成了,我们开始模拟Vue。模拟Vuevue-toy使用React渲染视图,所以如果在render函数中使用JSX,需要引入React来准备现在Observable和Watch已经实现了,下面来实现一个基本原理的例子:codesandboxexampleimportObservable来自“vue-toy/cjs/Observable”;从“vue-toy/cjs/Watch”导入手表;functionmount(vnode){console.log(vnode);}functionupdate(vnode){console.log(vnode);}constdata=Observable({msg:"hellovuetoy!",counter:1});functionrender(){return`render:${this.counter}|${this.msg}`;}newWatch(data,render,update);mount(render.call(data));setInterval(()=>data.counter++,1000);//可以看到输出信息persecond在控制台上,然后将mountupdate的实现替换为vdom即可完成基本的渲染。但这还不够,我们需要将其抽象封装成组件来使用。Component源码这里的Component就像是React的一个高阶函数HOC。示例:constHello=Component({props:["msg"],data(){return{counter:1,};},render(h){returnh("h1",null,this.msg,this.柜台);},});大致实现如下,options参考文章开头functionComponent(options){returnclassextendsReact.Component{//省略一些..//省略一些...//创建一个观察对象this.$data=Observable({...propsData,...methods,...data},computed);//省略一些...//计算渲染依赖并监听this.$watcher=newWatch(this.$data,()=>{returnoptions.render.call(this,React.createElement);},debounce((children)=>{this.$children=children;this.forceUpdate();}));这。$children=选项。使成为.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);返回真;}返回假;}//生命周期关联componentDidMount(){options.mounted?.call(this);}componentWillUnmount(){this.$watcher.clearDeps();options.destroyed?.call(this);}componentDidUpdate(){options.updated?.call(this);}render(){返回这个。$儿童;}};}创建主函数Vue,最后创建入口函数Vue,实现代码如下:exportdefaultfunctionVue(options){constRootComponent=Component(options);让埃尔;if(typeofel==="string"){el=document.querySelector(el);}constprops={...options.propsData,$el:el,};返回ReactDOM.render(React.createElement(RootComponent,props),el);}Vue.component=Component;好了,Vue的基本实现就完成了感谢阅读。最后,欢迎Star:https://github.com/bplok20010/vue-toy