2021年,让我们手写一个mini版本的vue2.x和vue3.x框架
SegmentFault
共 24577字,需浏览 50分钟
·
2021-07-10 13:02
作者:夕水
来源:SegmentFault 思否社区
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 = {}){
//后续要做的事情
}
}
//在miniVue构造函数的内部
//保存根元素,能简便就尽量简便,不考虑数组情况
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
this.$methods = options.methods;
this.$data = options.data;
this.$options = options;
代理数据
//this.$data.xxx -> this.xxx;
//proxy代理实例上的data对象
proxy(data){
//后续代码
}
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);
}
// 构造函数内部
this.proxy(this.$data);
数据响应式观察者observer类
Observer
。如下:class Observer {
constructor(data){
//后续实现
}
}
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())
}
Watcher类
Watcher
。class Watcher {
//3个参数,当前组件实例vm,state也就是数据以及一个回调函数,或者叫处理器
constructor(vm,key,cb){
//后续代码
}
}
Watcher
的用法,我们是不是会像如下这样来写://3个参数,当前组件实例vm,state也就是数据以及一个回调函数,或者叫处理器
new Watcher(vm,key,cb);
//构造函数内部
this.vm = vm;
this.key = key;
this.cb = cb;
//依赖类
Dep.target = this;
// 我们用一个变量来存储旧值,也就是未变更之前的值
this.__old = vm[key];
Dep.target = null;
update(){
//获取新的值
let newValue = this.vm[this.key];
//与旧值做比较,如果没有改变就无需执行下一步
if(newValue === this.__old || __isNaN(newValue,this.__old))return;
//把新的值回调出去
this.cb(newValue);
//执行完之后,需要更新一下旧值的存储
this.__old = newValue;
}
编译类compiler类
初始化
class Compiler {
constructor(vm){
//后续代码
}
}
//在miniVue构造函数内部
new Compiler(this);
//编译类构造函数内部
//根元素
this.el = vm.$el;
//事件方法
this.methods = vm.$methods;
//当前组件实例
this.vm = vm;
//调用编译函数开始编译
this.compile(vm.$el);
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);
})
}
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函数内部
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指令
//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指令
//attrName === 'click'内部
node.addEventListener(attrName,this.methods[key].bind(this.vm));
mini版本的vue.js3.x框架
模板代码
<div id="app"></div>
逻辑代码
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做比较
effect
。vue3.x更像是函数式编程了,每一个功能都是一个函数,比如定义响应式对象,那就是reactive方法,再比如computed,同样的也是computed方法...废话不多说,让我们来看一下吧!reactive方法
function reactive(data){
if(!isObject(data))return;
//后续代码
}
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方法
//全局变量表示依赖
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)
}
trigger方法
function trigger(target,key){
const depsMap = targetMap.get(target);
//存储依赖的数据结构都拿不到,则代表没有依赖,直接返回
if(!depsMap)return;
depsMap.get(key).forEach(effect => effect && effect());
}
effect方法
function effect(handler,options = {}){
const __effect = function(...args){
activeEffect = __effect;
return handler(...args);
}
//配置对象有一个lazy属性,用于computed计算属性的实现,因为计算属性是懒加载的,也就是延迟执行
//也就是说如果不是一个计算属性的回调函数,则立即执行副作用函数
if(!options.lazy){
__effect();
}
return __effect;
}
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;
}
ref方法
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方法
function mount(instance,el){
effect(function(){
instance.$data && update(instance,el);
});
//setup返回的数据就是实例上的数据
instance.$data = instance.setup();
//这里的update实际上就是编译函数
update(instance,el);
}
update编译函数
innerHTML
。如下://这是最简单的编译函数
function update(instance,el){
el.innerHTML = instance.render();
}
评论