头图

In 2021, let us write a mini version of vue2.x and vue3.x framework

夕水
中文

The mini version of vue.js2.X version framework

Template code

First, let's take a look at the template code we want to implement:

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

Logic code

Then there is the javascript code we want to write.

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

running result

Let's take a look at the actual operation effect as follows:

Think about it, what should we do to achieve the above function? You can also open the above example separately:

click here .

Source code implementation-2.x

miniVue class

First of all, regardless of the number of mini-vue one, since it is to instantiate a 06107891ad58ed, then we first define a class, and its parameter must be a property configuration object. as follows:

 class miniVue {
     constructor(options = {}){
         //后续要做的事情
     }
 }

Now, let's initialize some attributes first, such as data, methods, options and so on.

//在miniVue构造函数的内部
//保存根元素,能简便就尽量简便,不考虑数组情况
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
this.$methods = options.methods;
this.$data = options.data;
this.$options = options;

After the initialization is complete, let's think about another question. Can we access the data object defined by Vue by using this inside Vue? So how should we implement this function? This function has a professional term, called proxy (proxy) .

Proxy data

So let's implement this function, it is obvious that a proxy method is defined inside the miniVue class. as follows:

//this.$data.xxx -> this.xxx;
//proxy代理实例上的data对象
proxy(data){
    //后续代码
}

Next, we need to know an api, namely Object.defineProperty , through this method to complete the proxy method. as follows:

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

Next, let's take a look at _isNaN tool method, as follows:

function _isNaN(a,b){
    return Number.isNaN(a) && Number.isNaN(b);
}

After the definition, we only need to call it once in the constructor of the miniVue class. as follows:

// 构造函数内部
this.proxy(this.$data);

This is how the agent is done, let's move on to the next step.

Data responsive observer observer class

We need to define a responsive object for each attribute of the data to monitor the changes of the data, so we need a class to manage it, and we will give it the name Observer . as follows:

class Observer {
    constructor(data){
        //后续实现
    }
}

We need to add responsive objects to each piece of data and convert them into getter and setter functions. Here we use the Object.defineProperty method. We need to collect dependencies in the getter function and send notifications in the setter function to notify the dependencies to update . We use a method to specifically execute the method of defining reactive objects, called walk, as follows:

//再次申明,不考虑数组,只考虑对象
walk(data){
    if(typeof data !== 'object' || !data)return;
    // 数据的每一个属性都调用定义响应式对象的方法
    Object.keys(data).forEach(key => this.defineReactive(data,key,data[key]));
}

Next, let's look defineReactive the implementation of the 06107891ad5b9e method, which also uses the Object.defineProperty method to define the responsive object, as shown below:

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 class is completed, we need to instantiate it in the constructor of the miniVue class, as follows:

//在miniVue构造函数内部
new Observer(this.$data);

Okay, let's move on to the next step.

Dependent class

defineReactive internal method uses Dep class Next, we define the class. as follows:

class Dep {
    constructor(){
        //后续代码
    }
}

Next, let's think about what we need to do in the dependency class. First, according to defineReactive , we obviously know that there will be a add method and a notify method, and we need a data structure to store the dependency, which is used by the vue source code. It is a queue, and here for simplicity, we use the set data structure of ES6. as follows:

//构造函数内部
this.deps = new Set();

Next, you need to implement the add method and the notify method. In fact, there will be a method to delete the dependency, but here for the easiest, we only need a add and notify method. as follows:

add(dep){
    //判断dep是否存在并且是否存在update方法,然后添加到存储的依赖数据结构中
    if(dep && dep.update)this.deps.add(dep);
}
notify(){
    // 发布通知无非是遍历一道dep,然后调用每一个dep的update方法,使得每一个依赖都会进行更新
    this.deps.forEach(dep => dep.update())
}

The Dep class is over, and then we need another class.

Watcher class

That is to manage the class of each component instance to ensure that each component instance can send view updates and state transfer operations by this class. This class, we call it Watcher .

class Watcher {
    //3个参数,当前组件实例vm,state也就是数据以及一个回调函数,或者叫处理器
    constructor(vm,key,cb){
        //后续代码
    }
}

Think again, what does our Watcher class need to do? Let’s first think about Watcher . Will we write it like this:

//3个参数,当前组件实例vm,state也就是数据以及一个回调函数,或者叫处理器
new Watcher(vm,key,cb);

Ok, after knowing how to use it, we can initialize some things inside the constructor. as follows:

//构造函数内部
this.vm = vm;
this.key = key;
this.cb = cb;
//依赖类
Dep.target = this;
// 我们用一个变量来存储旧值,也就是未变更之前的值
this.__old = vm[key];
Dep.target = null;

Then there is an update method in the Watcher class. Next, let's take a look at the implementation of this method. as follows:

update(){
    //获取新的值
    let newValue = this.vm[this.key];
    //与旧值做比较,如果没有改变就无需执行下一步
    if(newValue === this.__old || __isNaN(newValue,this.__old))return;
    //把新的值回调出去
    this.cb(newValue);
    //执行完之后,需要更新一下旧值的存储
    this.__old = newValue;
}

Compiler class

initialization

At this point, we are completely separated from the vue source code, because the compilation of the vue source code is very complicated, involving the diff algorithm and the virtual node vNode, and we are committed to simplifying it, so we write a separate Compiler class to compile. as follows:

class Compiler {
    constructor(vm){
        //后续代码
    }
}
Note: The compilation here is implemented by ourselves according to the process, and has nothing to do with the vue source code. Vue also has a compiler, but it is completely different from our implementation.

After the definition, we can instantiate this compiled class in the constructor of the miniVue class. as follows:

//在miniVue构造函数内部
new Compiler(this);

Okay, we have also seen how to use it, so let's complete some initialization operations inside the constructor of this compiled class. as follows:

//编译类构造函数内部
//根元素
this.el = vm.$el;
//事件方法
this.methods = vm.$methods;
//当前组件实例
this.vm = vm;
//调用编译函数开始编译
this.compile(vm.$el);

compile method

The initialization operation is complete, let's look at the internals of the compile method. Think about it, in this method, do we need to get all the nodes, and then compare whether it is text or element nodes to compile separately? as follows:

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

Here, we need two auxiliary methods to determine whether it is a text node or an element node. In fact, we can use the nodeType property of the node to make the judgment, because the nodeType value of the text node is 3, and the nodeType value of the element node is 1. So these two auxiliary methods can be implemented as follows:

isTextNode(node){
    return node.nodeType === 3;
}
isElementNode(node){
    return node.nodeType === 1;
}

Compile text node

Next, let's look at the method for compileText as follows:

//{{ count }}数据结构是类似如此的
compileText(node){
    //后续代码
}

Next, let us think about it. When we compile the text node, we {{ count }} the text node to 0, and the text node is not the node.textContent property? So at this time we can think of matching {{}} according to the regularity, and then replacing it with the count value in the data, and then we call the Watcher class again, and if it is updated, we will change the value of node.textContent again. as follows:

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

That's it for compiling the text node. Next, let's look at the method of compiling the element node.

Compile element node

instruction

First of all, let us think about it. When we compile element nodes, we just want to perform different operations according to the instructions on the element nodes. Therefore, when we compile the element nodes, we only need to determine whether there are related instructions. Here we only consider v-text,v-model,v-on:click These three instructions. Let's take a look at the compileElement method.

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

Here again involves a isDirective auxiliary method, we can use the startsWith method to determine whether it contains the v- to determine that this attribute is an instruction. as follows:

isDirective(dir){
    return dir.startsWith('v-');
}

Next, we look at the final update method. as follows:

update(node,attrName,key,value){
    //后续代码
}

Finally, let us think about what we need to do in update. Obviously, do we need to determine which instructions are used to perform different operations? as follows:

//update函数内部
if(attrName === 'text'){
    //执行v-text的操作
}else if(attrName === 'model'){
    //执行v-model的操作
}else if(attrName === 'click'){
    //执行v-on:click的操作
}

v-text command

Okay, we know that according to the previous method of compiling text element nodes, we know that the usage of this instruction is the same as the previous compiling text element nodes. So this judgment is easy to write, as follows:

//attrName === 'text'内部
node.textContent = value;
new Watcher(this.vm,key,newValue => {
    node.textContent = newValue;
})

v-model directive

The v-model instruction implements two-way binding. We all know that two-way binding is to change the value of the input box, and it is implemented by listening to input events. So this judgment, we are also very easy to write, as follows:

//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 instruction

The v-on:click instruction is to bind the event to the function defined in the methods. In order to ensure that this points to the current component instance, we need to change the this point through the bind method. as follows:

//attrName === 'click'内部
node.addEventListener(attrName,this.methods[key].bind(this.vm));

So far, our mini version of vue2.x is even realized. Continue to the next section to learn the mini implementation of vue3.x version.

The mini version of vue.js3.x framework

Template code

First, let's take a look at the template code we want to implement:

<div id="app"></div>

Logic code

Then there is the javascript code we want to write.

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

running result

Let's take a look at the actual operation effect as follows:

Think about it, what should we do to achieve the above function? You can also open the above example separately:

click here .

Source code implementation-3.x

Compare with vue2.x

In fact, the realization idea of vue3.x is similar to that of vue2.x, but the realization of vue3.x is somewhat different. In vue3.x, the method of collecting dependencies is called a side effect effect . Vue3.x is more like functional programming. Every function is a function. For example, to define a reactive object, it is a reactive method. Another example is computed. The same is also a computed method... Not much nonsense, let's Take a look!

reactive method

First, let's take a look at the reactive method of vue3.x. Here, we still only consider processing objects. as follows:

function reactive(data){
    if(!isObject(data))return;
    //后续代码
}

Next, we need to use the proxy API of es6. We need to be familiar with the usage of this API. If you are not familiar with it, please click here view.

We still collect dependencies in getters, trigger dependencies in setters, collect dependencies and trigger dependencies. We define two methods respectively, namely track and trigger methods. as follows:

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 method

The track method is used to collect dependencies. We use es6's weakMap data structure to store dependencies, and then use a global variable to represent dependencies for simplicity. as follows:

//全局变量表示依赖
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)
}

Collecting dependencies is as simple as that. It should be noted that the three data structures of es6 are involved here, namely WeakMap, Map, and Set. Next we will look at how to trigger dependencies.

trigger method

The trigger method is obviously to take out all the dependencies, and each dependency is a side effect function, so it can be called directly.

function trigger(target,key){
    const depsMap = targetMap.get(target);
    //存储依赖的数据结构都拿不到,则代表没有依赖,直接返回
    if(!depsMap)return;
    depsMap.get(key).forEach(effect => effect && effect());
}

Next, let's implement this side effect function, which is effect.

effect method

The role of the side effect function is also very simple, that is, to execute each callback function. So this method has 2 parameters, the first is a callback function, and the second is a configuration object. as follows:

function effect(handler,options = {}){
    const __effect = function(...args){
        activeEffect = __effect;
        return handler(...args);
    }
    //配置对象有一个lazy属性,用于computed计算属性的实现,因为计算属性是懒加载的,也就是延迟执行
    //也就是说如果不是一个计算属性的回调函数,则立即执行副作用函数
    if(!options.lazy){
        __effect();
    }
    return __effect;
}

The side effect function is so simple to implement, let's take a look at the implementation of computed.

Implementation of computed

Now that we talked about computing properties, we define a computed function. Let's take a look:

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

So far, the response of vue3.x is basically realized, and then the mount and compile of vue3.x must be realized. One more point, we have only dealt with the reactive type of reference type above, but in fact vue3.x also provides a ref method to deal with the basic type of reactive type. Therefore, we can still implement basic types of responsiveness.

ref method

So, how should we implement basic types of responsiveness? Just think about why basic types are defined in vue3.x. If you modify the value, you need to modify xxx.value to complete. as follows:

const count = ref(0);
//修改
count.value = 1;

From the above code, it is not difficult to derive the encapsulation principle of the basic type, which is actually to package the basic type into an object. Therefore, we can quickly write the following code:

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

This is the basic type of responsive implementation principle, let's take a look at the implementation of the mount method.

mount method

The mount method implements mounting, and our side-effect function is executed here. It has 2 parameters, the first parameter is a vue component, and the second parameter is the root element of the mounted DOM. Therefore, we can quickly write the following code:

function mount(instance,el){
    effect(function(){
        instance.$data && update(instance,el);
    });
    //setup返回的数据就是实例上的数据
    instance.$data = instance.setup();
    //这里的update实际上就是编译函数
    update(instance,el);
}

This is to achieve a simple mount, let's take a look at the implementation of the compiled function.

update compilation function

Here for simplification, the compiled function we implemented is relatively simple, directly assign the render function defined on the component to innerHTML root element. as follows:

//这是最简单的编译函数
function update(instance,el){
    el.innerHTML = instance.render();
}

In this way, a simple mini-vue3.x is implemented in this way, how about it, it can be done in less than 100 lines of code, it is relatively simple.

included on the website 16107891ad69ee.

阅读 1.9k

eveningwater
每天学习一点点,就可以进步一点点,工作能带来的不仅是技术知识点,还有与人的相处,沟通与交流。这是...

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性...

3.8k 声望
3.3k 粉丝
0 条评论
你知道吗?

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性...

3.8k 声望
3.3k 粉丝
宣传栏