mini版本的vue.js2.X版本框架
模板代码
首先咱们看一下咱们要实现的模板代码:
<div id="app"> <h3>{{ msg }}</h3> <p>{{ count }}</p> <h1>v-text</h1> <p v-text="msg"></p> <input type="text" v-model="count"> <button type="button" v-on:click="increase">add+</button> <button type="button" v-on:click="changeMessage">change message!</button> <button type="button" v-on:click="recoverMessage">recoverMessage!</button></div>
逻辑代码
而后就是咱们要编写的javascript代码。
const app = new miniVue({ el:"#app", data:{ msg:"hello,mini vue.js", count:666 }, methods:{ increase(){ this.count++; }, changeMessage(){ this.msg = "hello,eveningwater!"; }, recoverMessage(){ console.log(this) this.msg = "hello,mini vue.js"; } }});
运行成果
咱们来看一下理论运行成果如下所示:
思考一下,咱们要实现如上的性能应该怎么做呢?你也能够独自关上以上示例:
点击此处。
源码实现-2.x
miniVue类
首先,不管三七二十一,既然是实例化一个mini-vue
,那么咱们先定义一个类,并且它的参数肯定是一个属性配置对象。如下:
class miniVue { constructor(options = {}){ //后续要做的事件 } }
当初,让咱们先初始化一些属性,比方data,methods,options等等。
//在miniVue构造函数的外部//保留根元素,能简便就尽量简便,不思考数组状况this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;this.$methods = options.methods;this.$data = options.data;this.$options = options;
初始化完了之后,咱们再来思考一个问题,咱们是不是能够通过在vue外部应用this拜访到vue定义的数据对象呢?那么咱们应该如何实现这一个性能呢?这个性能有一个业余的名词,叫做代理(proxy)。
代理数据
因而咱们来实现一下这个性能,很显著在这个miniVue类的外部定义一个proxy办法。如下:
//this.$data.xxx -> this.xxx;//proxy代理实例上的data对象proxy(data){ //后续代码}
接下来,咱们须要晓得一个api,即Object.defineProperty
,通过这个办法来实现这个代理办法。如下:
//proxy办法外部// 因为咱们是代理每一个属性,所以咱们须要将所有属性拿到Object.keys(data).forEach(key => { Object.defineProperty(this,key,{ enumerable:true, configurable:true, get:() => { return data[key]; }, set:(newValue){ //这里咱们须要判断一下如果值没有做扭转就不必赋值,须要排除NaN的状况 if(newValue === data[key] || _isNaN(newValue,data[key]))return; data[key] = newValue; } })})
接下来,咱们来看一下这个_isNaN
工具办法的实现,如下:
function _isNaN(a,b){ return Number.isNaN(a) && Number.isNaN(b);}
定义好了之后,咱们只须要在miniVue类的构造函数中调用一次即可。如下:
// 构造函数外部this.proxy(this.$data);
代理就这样实现了,让咱们持续下一步。
数据响应式观察者observer类
咱们须要对数据的每一个属性都定义一个响应式对象,用来监听数据的扭转,所以咱们须要一个类来治理它,咱们就给它取个名字叫Observer
。如下:
class Observer { constructor(data){ //后续实现 }}
咱们须要给每一个数据都增加响应式对象,并且转换成getter和setter函数,这里咱们又用到了Object.defineProperty
办法,咱们须要在getter函数中收集依赖,在setter函数中发送告诉,用来告诉依赖进行更新。咱们用一个办法来专门去执行定义响应式对象的办法,叫walk,如下:
//再次申明,不思考数组,只思考对象walk(data){ if(typeof data !== 'object' || !data)return; // 数据的每一个属性都调用定义响应式对象的办法 Object.keys(data).forEach(key => this.defineReactive(data,key,data[key]));}
接下来咱们来看defineReactive
办法的实现,同样也是应用Object.defineProperty
办法来定义响应式对象,如下所示:
defineReactive(data,key,value){ // 获取以后this,以防止后续用vm的时候,this指向不对 const vm = this; // 递归调用walk办法,因为对象外面还有可能是对象 this.walk(value); //实例化收集依赖的类 let dep = new Dep(); Object.defineProperty(data,key,{ enumerable:true, configurable:true, get(){ // 收集依赖,依赖存在Dep类上 Dep.target && Dep.add(Dep.target); return value; }, set(newValue){ // 这里也判断一下 if(newValue === value || __isNaN(value,newValue))return; // 否则扭转值 value = newValue; // newValue也有可能是对象,所以递归 vm.walk(newValue); // 告诉Dep类 dep.notify(); } })}
Observer
类实现了之后,咱们须要在miniVue类的构造函数中实例化一下它,如下:
//在miniVue构造函数外部new Observer(this.$data);
好的,让咱们持续下一步。
依赖类
defineReactive
办法外部用到了Dep
类,接下来,咱们来定义这个类。如下:
class Dep { constructor(){ //后续代码 }}
接下来,咱们来思考一下,依赖类外面,咱们须要做什么,首先依据defineReactive
中,咱们很显著就晓得会有add
办法和notify
办法,并且咱们须要一种数据结构来存储依赖,vue源码用的是队列,而在这里为了简单化,咱们应用ES6的set数据结构。如下:
//构造函数外部this.deps = new Set();
接下来,就须要实现add
办法和notify
办法,事实上这里还会有删除依赖的办法,然而这里为了最简便,咱们只须要一个add
和notify
办法即可。如下:
add(dep){ //判断dep是否存在并且是否存在update办法,而后增加到存储的依赖数据结构中 if(dep && dep.update)this.deps.add(dep);}notify(){ // 公布告诉无非是遍历一道dep,而后调用每一个dep的update办法,使得每一个依赖都会进行更新 this.deps.forEach(dep => dep.update())}
Dep类算是完了,接下来咱们就须要另一个类。
Watcher类
那就是为了治理每一个组件实例的类,确保每个组件实例能够由这个类来发送视图更新以及状态流转的操作。这个类,咱们把它叫做Watcher
。
class Watcher { //3个参数,以后组件实例vm,state也就是数据以及一个回调函数,或者叫处理器 constructor(vm,key,cb){ //后续代码 }}
再次思考一下,咱们的Watcher类须要做哪些事件呢?咱们先来思考一下Watcher
的用法,咱们是不是会像如下这样来写:
//3个参数,以后组件实例vm,state也就是数据以及一个回调函数,或者叫处理器new Watcher(vm,key,cb);
ok,晓得了应用形式之后,咱们就能够在构造函数外部初始化一些货色了。如下:
//构造函数外部this.vm = vm;this.key = key;this.cb = cb;//依赖类Dep.target = this;// 咱们用一个变量来存储旧值,也就是未变更之前的值this.__old = vm[key];Dep.target = null;
而后Watcher类就多了一个update办法,接下来让咱们来看一下这个办法的实现吧。如下:
update(){ //获取新的值 let newValue = this.vm[this.key]; //与旧值做比拟,如果没有扭转就无需执行下一步 if(newValue === this.__old || __isNaN(newValue,this.__old))return; //把新的值回调进来 this.cb(newValue); //执行完之后,须要更新一下旧值的存储 this.__old = newValue;}
编译类compiler类
初始化
到了这一步,咱们就算是齐全脱离vue源码了,因为vue源码的编译十分复杂,波及到diff算法以及虚构节点vNode,而咱们这里致力于将其最简化,所以独自写一个Compiler类来编译。如下:
class Compiler { constructor(vm){ //后续代码 }}
留神:这里的编译是咱们本人依据流程来实现的,与vue源码并没有任何关联,vue也有compiler,然而与咱们实现的齐全不同。
定义好了之后,咱们在miniVue类的构造函数中实例化一下这个编译类即可。如下:
//在miniVue构造函数外部new Compiler(this);
好的,咱们也看到了应用形式,所以接下来咱们来欠缺这个编译类的构造函数外部的一些初始化操作。如下:
//编译类构造函数外部//根元素this.el = vm.$el;//事件办法this.methods = vm.$methods;//以后组件实例this.vm = vm;//调用编译函数开始编译this.compile(vm.$el);
compile办法
初始化操作算是实现了,接下来咱们来看compile办法的外部。思考一下,在这个办法的外部,咱们是不是须要拿到所有的节点,而后比照是文本还是元素节点去别离进行编译呢?如下:
compile(el){ //拿到所有子节点(蕴含文本节点) let childNodes = el.childNodes; //转成数组 Array.from(childNodes).forEach(node => { //判断是文本节点还是元素节点别离执行不同的编译办法 if(this.isTextNode(node)){ this.compileText(node); }else if(this.isElementNode(node)){ this.compileElement(node); } //递归判断node下是否还含有子节点,如果有的话持续编译 if(node.childNodes && node.childNodes.length)this.compile(node); })}
这里,咱们须要2个辅助办法,判断是文本节点还是元素节点,其实咱们能够应用节点的nodeType属性来进行判断,因为文本节点的nodeType值为3,而元素节点的nodeType值为1。所以这2个辅助办法咱们就能够实现如下:
isTextNode(node){ return node.nodeType === 3;}isElementNode(node){ return node.nodeType === 3;}
编译文本节点
接下来,咱们下来看compileText
编译文本节点的办法。如下:
//{{ count }}数据结构是相似如此的compileText(node){ //后续代码}
接下来,让咱们思考一下,咱们编译文本节点,无非就是把文本节点中的{{ count }}
映射成为0,而文本节点不就是node.textContent属性吗?所以此时咱们能够想到依据正则来匹配{{}}
中的count值,而后对应替换成数据中的count值,而后咱们再调用一次Watcher类,如果更新了,就再次更改这个node.textContent的值。如下:
compileText(node){ //定义正则,匹配{{}}中的count let reg = /\{\{(.+?)\}\}/g; let value = node.textContent; //判断是否含有{{}} if(reg.test(value)){ //拿到{{}}中的count,因为咱们是匹配一个捕捉组,所以咱们能够依据RegExp类的$1属性来获取这个count let key = RegExp.$1.trim(); node.textContent = value.replace(reg,this.vm[key]); //如果更新了值,还要做更改 new Watcher(this.vm,key,newValue => { node.textContent = newValue; }) }}
编译文本节点到此为止了,接下来咱们来看编译元素节点的办法。
编译元素节点
指令
首先,让咱们想一下,咱们编译元素节点无非是想要依据元素节点上的指令来别离执行不同的操作,所以咱们编译元素节点就只须要判断是否含有相干指令即可,这里咱们只思考了v-text,v-model,v-on:click
这三个指令。让咱们来看看compileElement办法吧。
compileElement(node){ //指令不就是一堆属性吗,所以咱们只须要获取属性即可 const attrs = node.attributes; if(attrs.length){ Array.from(attrs).forEach(attr => { //这里因为咱们拿到的attributes可能蕴含不是指令的属性,所以咱们须要先做一次判断 if(this.isDirective(attr)){ //依据v-来截取一下后缀属性名,例如v-on:click,subStr(5)即可截取到click,v-text与v-model则subStr(2)截取到text和model即可 let attrName = attr.indexOf(':') > -1 ? attr.subStr(5) : attr.subStr(2); let key = attr.value; //独自定义一个update办法来辨别这些 this.update(node,attrName,key,this.vm[key]); } }) }}
这里又波及到了一个isDirective
辅助办法,咱们能够应用startsWith
办法,判断是否含有v-
值即可认定这个属性就是一个指令。如下:
isDirective(dir){ return dir.startsWith('v-');}
接下来,咱们来看最初的update
办法。如下:
update(node,attrName,key,value){ //后续代码}
最初,让咱们来思考一下,咱们update外面须要做什么。很显然,咱们是不是须要判断是哪种指令来执行不同的操作?如下:
//update函数外部if(attrName === 'text'){ //执行v-text的操作}else if(attrName === 'model'){ //执行v-model的操作}else if(attrName === 'click'){ //执行v-on:click的操作}
v-text指令
好的,咱们晓得,依据后面的编译文本元素节点的办法,咱们就晓得这个指令的用法同后面编译文本元素节点。所以这个判断外面就好写了,如下:
//attrName === 'text'外部node.textContent = value;new Watcher(this.vm,key,newValue => { node.textContent = newValue;})
v-model指令
v-model指令实现的是双向绑定,咱们都晓得双向绑定是更改输入框的value值,并且通过监听input事件来实现。所以这个判断,咱们也很好写了,如下:
//attrName === 'model'外部node.value = value;new Watcher(this.vm,key,newValue => { node.value = newValue;});node.addEventListener('input',(e) => { this.vm[key] = node.value;})
v-on:click指令
v-on:click指令就是将事件绑定到methods内定义的函数,为了确保this指向以后组件实例,咱们须要通过bind办法扭转一下this指向。如下:
//attrName === 'click'外部node.addEventListener(attrName,this.methods[key].bind(this.vm));
到此为止,咱们一个mini版本的vue2.x就算是实现了。持续下一节,学习vue3.x版本的mini实现吧。
mini版本的vue.js3.x框架
模板代码
首先咱们看一下咱们要实现的模板代码:
<div id="app"></div>
逻辑代码
而后就是咱们要编写的javascript代码。
const App = { $data:null, setup(){ let count = ref(0); let time = reactive({ second:0 }); let com = computed(() => `${ count.value + time.second }`); setInterval(() => { time.second++; },1000); setInterval(() => { count.value++; },2000); return { count, time, com } }, render(){ return ` <h1>How reactive?</h1> <p>this is reactive work:${ this.$data.time.second }</p> <p>this is ref work:${ this.$data.count.value }</p> <p>this is computed work:${ this.$data.com.value }</p> ` }}mount(App,document.querySelector("#app"));
运行成果
咱们来看一下理论运行成果如下所示:
思考一下,咱们要实现如上的性能应该怎么做呢?你也能够独自关上以上示例:
点击此处。
源码实现-3.x
与vue2.x做比拟
事实上,vue3.x的实现思维与vue2.x差不多,只不过vue3.x的实现形式有些不同,在vue3.x,把收集依赖的办法称作是副作用effect
。vue3.x更像是函数式编程了,每一个性能都是一个函数,比方定义响应式对象,那就是reactive办法,再比方computed,同样的也是computed办法...废话不多说,让咱们来看一下吧!
reactive办法
首先,咱们来看一下vue3.x的响应式办法,在这里,咱们依然只思考解决对象。如下:
function reactive(data){ if(!isObject(data))return; //后续代码}
接下来咱们须要应用到es6的proxyAPI,咱们须要相熟这个API的用法,如果不相熟,请点击此处查看。
咱们还是在getter中收集依赖,setter中触发依赖,收集依赖与触发依赖,咱们都别离定义为2个办法,即track和trigger办法。如下:
function reactive(data){ if(!isObject(data))return; return new Proxy(data,{ get(target,key,receiver){ //反射api const ret = Reflect.get(target,key,receiver); //收集依赖 track(target,key); return isObject(ret) ? reactive(ret) : ret; }, set(target,key,val,receiver){ Reflect.set(target,key,val,receiver); //触发依赖办法 trigger(target,key); return true; }, deleteProperty(target,key,receiver){ const ret = Reflect.deleteProperty(target,key,receiver); trigger(target,key); return ret; } })}
track办法
track办法就是用来收集依赖的。咱们用es6的weakMap数据结构来存储依赖,而后为了简便化用一个全局变量来示意依赖。如下:
//全局变量示意依赖let activeEffect;//存储依赖的数据结构let targetMap = new WeakMap();//每一个依赖又是一个map构造,每一个map存储一个副作用函数即effect函数function track(target,key){ //拿到依赖 let depsMap = targetMap.get(target); // 如果依赖不存在则初始化 if(!depsMap)targetMap.set(target,(depsMap = new Map())); //拿到具体的依赖,是一个set构造 let dep = depsMap.get(key); if(!dep)depsMap.set(key,(dep = new Set())); //如果没有依赖,则存储再set数据结构中 if(!dep.has(activeEffect))dep.add(activeEffect)}
收集依赖就这么简略,须要留神的是,这里波及到了es6的三种数据结构即WeakMap,Map,Set。下一步咱们就来看如何触发依赖。
trigger办法
trigger办法很显著就是拿出所有依赖,每一个依赖就是一个副作用函数,所以间接调用即可。
function trigger(target,key){ const depsMap = targetMap.get(target); //存储依赖的数据结构都拿不到,则代表没有依赖,间接返回 if(!depsMap)return; depsMap.get(key).forEach(effect => effect && effect());}
接下来,咱们来实现一下这个副作用函数,也即effect。
effect办法
副作用函数的作用也很简略,就是执行每一个回调函数。所以该办法有2个参数,第一个是回调函数,第二个则是一个配置对象。如下:
function effect(handler,options = {}){ const __effect = function(...args){ activeEffect = __effect; return fn(...args); } //配置对象有一个lazy属性,用于computed计算属性的实现,因为计算属性是懒加载的,也就是提早执行 //也就是说如果不是一个计算属性的回调函数,则立刻执行副作用函数 if(!options.lazy){ __effect(); } return __effect;}
副作用函数就是如此简略的实现了,接下来咱们来看一下computed的实现。
computed的实现
既然谈到了计算属性,所以咱们就定义了一个computed函数。咱们来看一下:
function computed(handler){ // 只思考函数的状况 // 提早计算 const c = computed(() => `${ count.value}!`) let _computed; //能够看到computed就是一个增加了lazy为true的配置对象的副作用函数 const run = effect(handler,{ lazy:true }); _computed = { //get 拜访器 get value(){ return run(); } } return _computed;}
到此为止,vue3.x的响应式算是根本实现了,接下来要实现vue3.x的mount以及compile。还有一点,咱们以上只是解决了援用类型的响应式,但实际上vue3.x还提供了一个ref办法用来解决根本类型的响应式。因而,咱们依然能够实现根本类型的响应式。
ref办法
那么,咱们应该如何来实现根本类型的响应式呢?试想一下,为什么vue3.x中定义根本类型,如果批改值,须要批改xxx.value来实现。如下:
const count = ref(0);//批改count.value = 1;
从以上代码,咱们不难得出根本类型的封装原理,实际上就是将根本类型包装成一个对象。因而,咱们很快能够写出如下代码:
function ref(target){ let value = target; const obj = { get value(){ //收集依赖 track(obj,'value'); return value; }, set value(newValue){ if(value === newValue)return; value = newValue; //触发依赖 trigger(obj,'value'); } } return obj;}
这就是根本类型的响应式实现原理,接下来咱们来看一下mount办法的实现。
mount办法
mount办法实现挂载,而咱们的副作用函数就是在这里执行。它有2个参数,第一个参数即一个vue组件,第二个参数则是挂载的DOM根元素。所以,咱们能够很快写出以下代码:
function mount(instance,el){ effect(function(){ instance.$data && update(instance,el); }); //setup返回的数据就是实例上的数据 instance.$data = instance.setup(); //这里的update实际上就是编译函数 update(instance,el);}
这样就是实现了一个简略的挂载,接下来咱们来看一下编译函数的实现。
update编译函数
这里为了简便化,咱们实现的编译函数就比较简单,间接就将定义在组件上的render函数给赋值给根元素的innerHTML
。如下:
//这是最简略的编译函数function update(instance,el){ el.innerHTML = instance.render();}
如此一来,一个简略的mini-vue3.x就这样实现了,怎么样,不到100行代码就搞定了,还是比较简单的。
具体文档收录网站。