Handwritten vue - implementation of a simplified version of Vue 1

Implementation of MVVM

The three elements of the MVVM framework: data responsiveness, template engine and its rendering
Data responsiveness: monitor data changes and update them in the view

  • Object.defineProperty(), implementation in Vue2
  • Proxy, the implementation in Vue3

Template engine: Provides a template syntax for describing views

  • Interpolation: { {}}
  • 指令:v-bind, v-on, v-model, v-for, v-if

Rendering: how to convert templates to html

  • template => vdom => dom

Code

Create vue_simple project

vue create vue_simple

project directory

insert image description here
Among them, hvue.html is the code for our test effect, and hvue.js is the code of the simplified version of Vue that I want to write.

writing process

Test code hvue.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>简版vue的实现</title>
</head>
<body>
    <div id="app">
        <p>{
    
    {
    
    counter}}
        	<span>{
    
    {
    
    sum}}</span>
        </p>
    </div>
</body>
<script src="../node_modules/vue/dist/vue.js"></script>
<script> 
    const app = new Vue({
    
    
        el:'#app',
        data:{
    
    
            counter:1,
            sum:1
        }
    })
</script>
</html>

Here you can see that vue is still used at this time, and we will refer to kvue.js we wrote next. The current page effect is as follows:
insert image description here

Clearly implement the process of hVue.js

  1. To achieve data responsiveness
  2. To implement a template engine, various interpolation and instructions of the template can be replaced with data and rendered
  3. After the data is updated, the template is automatically updated

data responsive

hvue.js implements the data responsive part

class Hvue{
    
    
    constructor(options){
    
    
        // 先保存相关参数
        this.$options = options;
        this.$data = options.data;
        // 代理data,直接用this.xxx访问数据
        proxy(this);
        //1、实现数据响应式 遍历data
        observe(options.data)
        //2、编译
    }
}

//遍历data 使其成为响应式数据
function observe(data){
    
    
    // 判断类型
    if(typeof data !== 'object' || data === null) return data;

    Observe(data)
}

class Observe{
    
    
    constructor(obj){
    
    
        // 数组类型要进行特殊处理
        if(Array.isArray(obj)){
    
    

        }else{
    
    
            this.walk(obj);
        }
    }

    walk(obj){
    
    
        Object.keys(obj).forEach((key)=>defineReactive(obj,key,obj[key]));
    }
}

// 给 object 定义一个响应式属性
function defineReactive(obj,key,val){
    
    
    //对象嵌套处理
    //如果val是对象
    observe(val)


    Object.defineProperty(obj,key,{
    
    
        get(){
    
    
            return val;
        },
        set(newValue){
    
    
            val = newValue;
            return val;
        }
    })
}

//将所有的data代理到vue上,实现在代码中 this.xxx和this.data.xxx同样的效果
function proxy(vm){
    
    
    Object.keys(vm.$data).forEach((key)=>{
    
    
        Object.defineProperty(vm.$data,key,{
    
    
            get(){
    
    
                return vm.$data[key];
            },
            set(newValue){
    
    
                vm.$data[key] = newValue;
                return vm.$data[key];
            }
        })
    })
}

Now although all the data has been converted into responsive, the page is still not bound to the data, because we still need to do template processing
insert image description here

template compilation

Now the template compilation is implemented. After this implementation is completed, we can initially check the page effect.

class Hvue{
    
    
    constructor(options){
    
    
        // 先保存相关参数
        this.$options = options;
        this.$data = options.data;
        // 代理data,直接用this.xxx访问数据
        proxy(this);
        //1、实现数据响应式 遍历data
        observe(options.data)
        //2、编译 遍历节点
        new Compile(options.el,this)
    }
}

//遍历data 使其成为响应式数据
function observe(data){
    
    
    // 判断类型
    if(typeof data !== 'object' || data === null) return data;

    new Observe(data)
}

class Observe{
    
    
    constructor(obj){
    
    
        // 数组类型要进行特殊处理
        if(Array.isArray(obj)){
    
    

        }else{
    
    
            this.walk(obj);
        }
    }

    walk(obj){
    
    
        Object.keys(obj).forEach((key)=>defineReactive(obj,key,obj[key]));
    }
}

// 给 object 定义一个响应式属性
function defineReactive(obj,key,val){
    
    
    //对象嵌套处理
    //如果val是对象
    observe(val)


    Object.defineProperty(obj,key,{
    
    
        get(){
    
    
            return val;
        },
        set(newValue){
    
    
            val = newValue;
        }
    })
}

//将所有的data代理到vue上,实现在代码中 this.xxx和this.data.xxx同样的效果
function proxy(vm){
    
    
    Object.keys(vm.$data).forEach((key)=>{
    
    
        Object.defineProperty(vm,key,{
    
    
            get(){
    
    
                return vm.$data[key];
            },
            set(newValue){
    
    
                vm.$data[key] = newValue;
            }
        })
    })
}

// 模版引擎编译
class Compile{
    
    
    constructor(el,vm){
    
    
        this.$vm = vm;
        this.$el = document.querySelector(el);

        if(this.$el){
    
    
            this.compile(this.$el);
        }
    }

    compile(node){
    
    
        const childNodes = node.childNodes;

        Array.from(childNodes).forEach(n=>{
    
    
            // 判断节点类型
            //是元素节点
            if(this.isElement(n)){
    
    
            	//继续遍历节点
                if (n.childNodes.length > 0) {
    
    
                    this.compile(n);
                  }
            //字符串 插值表达式
            }else if (this.isInter(n)) {
    
    
                n.textContent = this.$vm[RegExp.$1];
            }
        })
    }

    isElement(n) {
    
    
        return n.nodeType === 1;
    }

    isInter(n){
    
    
        console.log(n.textContent)
        return n.nodeType === 3 && /\{\{(.*)\}\}/.test(n.textContent)
    }
}

insert image description here

Now you can see the page, and now only interpolation processing is implemented. Next, we will improve the code and implement command processing. Now we add the relevant code of the instruction in the hvue.html file.


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>简版vue的实现</title>
</head>
<body>
    <div id="app">
        <p>{
    
    {
    
    counter}}</p>
        <p h-text="counter"></p> //指令
        <p h-html="htmlText"></p> //指令
    </div>
</body>
<!-- <script src="../node_modules/vue/dist/vue.js"></script> -->
<script src="./hvue.js"></script> 
<script> 
    const app = new Hvue({
    
    
        el:'#app',
        data:{
    
    
            counter:1,
            htmlText:'<span style="color:red">我成功啦</span>'
        }
    })
</script>
</html>

The page at this time does not display accurate data, and we will continue to deal with it below.
insert image description here

class Hvue{
    
    
    constructor(options){
    
    
        // 先保存相关参数
        this.$options = options;
        this.$data = options.data;
        // 代理data,直接用this.xxx访问数据
        proxy(this);
        //1、实现数据响应式 遍历data
        observe(options.data)
        //2、编译 遍历节点
        new Compile(options.el,this)
    }
}

//遍历data 使其成为响应式数据
function observe(data){
    
    
    // 判断类型
    if(typeof data !== 'object' || data === null) return data;

    new Observe(data)
}

class Observe{
    
    
    constructor(obj){
    
    
        // 数组类型要进行特殊处理
        if(Array.isArray(obj)){
    
    

        }else{
    
    
            this.walk(obj);
        }
    }

    walk(obj){
    
    
        Object.keys(obj).forEach((key)=>defineReactive(obj,key,obj[key]));
    }
}

// 给 object 定义一个响应式属性
function defineReactive(obj,key,val){
    
    
    //对象嵌套处理
    //如果val是对象
    observe(val)


    Object.defineProperty(obj,key,{
    
    
        get(){
    
    
            return val;
        },
        set(newValue){
    
    
            val = newValue;
        }
    })
}

//将所有的data代理到vue上,实现在代码中 this.xxx和this.data.xxx同样的效果
function proxy(vm){
    
    
    Object.keys(vm.$data).forEach((key)=>{
    
    
        Object.defineProperty(vm,key,{
    
    
            get(){
    
    
                return vm.$data[key];
            },
            set(newValue){
    
    
                vm.$data[key] = newValue;
            }
        })
    })
}

// 模版引擎编译
class Compile{
    
    
    constructor(el,vm){
    
    
        this.$vm = vm;
        this.$el = document.querySelector(el);

        if(this.$el){
    
    
            this.compile(this.$el);
        }
    }

    compile(node){
    
    
        const childNodes = node.childNodes;

        Array.from(childNodes).forEach(n=>{
    
    
            // 判断节点类型
            //是元素节点
            if(this.isElement(n)){
    
    
                // ---------------处理指令编译--------------------------
                this.compileElement(n);
                //继续遍历节点
                if (n.childNodes.length > 0) {
    
    
                    this.compile(n);
                  }
            //字符串 插值表达式
            }else if (this.isInter(n)) {
    
    
                n.textContent = this.$vm[RegExp.$1];
            }
        })
    }

    isElement(n) {
    
    
        return n.nodeType === 1;
    }

    isInter(n){
    
    
        return n.nodeType === 3 && /\{\{(.*)\}\}/.test(n.textContent)
    }

    // 处理指令 编译
    compileElement(n){
    
    
        const attrs = n.attributes;
        Array.from(attrs).forEach(attr=>{
    
    
            const attrName = attr.name;
            const exp = attr.value;
            // 判断是否为特定的属性指令 h-
            if(this.isDir(attrName)){
    
    
                this.commandHandler(n,attrName,exp);
            }
        })
    }

    isDir(attrName){
    
    
        return attrName&&attrName.startsWith('h-');
    }

    // 匹配对应的指令处理函数
    commandHandler(node,attrName,exp){
    
    
        const fn = this[attrName.replace('h-','')+'CommandHandler'];
        fn&&fn.call(this,node,exp);
    }

    //h-text指令编译
    textCommandHandler(node,exp){
    
    
        node.textContent = this.$vm[exp];
    }

    //h-html
    htmlCommandHandler(node,exp){
    
    
        node.innerHTML= this.$vm[exp];
    }
}

Let's take a look at the page effect.
insert image description here
Next, we need to change the data in js, and the page will be automatically updated. Simple idea to implement:

  1. When the template engine is compiling and processing, if an interpolation or instruction is encountered, a watcher is created to store the update operation of this node, and at the same time, the watcher and its corresponding response data are stored in the Dep, and a response data corresponds to a Dep .
  2. When the response data changes in the future, find the corresponding Dep, traverse and execute the watcher inside, and update the page.
    Attach a schematic diagram: insert image description here
    Below we use code to achieve.
class Hvue{
    
    
    constructor(options){
    
    
        // 先保存相关参数
        this.$options = options;
        this.$data = options.data;
        // 代理data,直接用this.xxx访问数据
        proxy(this);
        //1、实现数据响应式 遍历data
        observe(options.data)
        //2、编译 遍历节点
        new Compile(options.el,this)
    }
}

//遍历data 使其成为响应式数据
function observe(data){
    
    
    // 判断类型
    if(typeof data !== 'object' || data === null) return data;

    new Observe(data)
}

class Observe{
    
    
    constructor(obj){
    
    
        // 数组类型要进行特殊处理
        if(Array.isArray(obj)){
    
    

        }else{
    
    
            this.walk(obj);
        }
    }

    walk(obj){
    
    
        Object.keys(obj).forEach((key)=>defineReactive(obj,key,obj[key]));
    }
}

// 给 object 定义一个响应式属性
function defineReactive(obj,key,val){
    
    
    //对象嵌套处理
    //如果val是对象
    observe(val)

    // 创建Dep实例
    const dep = new Dep()
    console.log(dep.watchers)

    Object.defineProperty(obj,key,{
    
    
        get(){
    
    
            Dep.target && dep.add(Dep.target);
            console.log(dep.watchers)
            return val;
        },
        set(newValue){
    
    
            console.log('set触发了')
            val = newValue;
            dep.emit()
        }
    })
}

//将所有的data代理到vue上,实现在代码中 this.xxx和this.data.xxx同样的效果
function proxy(vm){
    
    
    Object.keys(vm.$data).forEach((key)=>{
    
    
        Object.defineProperty(vm,key,{
    
    
            get(){
    
    
                return vm.$data[key];
            },
            set(newValue){
    
    
                vm.$data[key] = newValue;
            }
        })
    })
}

// 模版引擎编译
class Compile{
    
    
    constructor(el,vm){
    
    
        this.$vm = vm;
        this.$el = document.querySelector(el);

        if(this.$el){
    
    
            this.compile(this.$el);
        }
    }

    compile(node){
    
    
        const childNodes = node.childNodes;

        Array.from(childNodes).forEach(n=>{
    
    
            // 判断节点类型
            //是元素节点
            if(this.isElement(n)){
    
    
                // 处理指令编译
                this.compileElement(n);
                //继续遍历节点
                if (n.childNodes.length > 0) {
    
    
                    this.compile(n);
                  }
            //字符串 插值表达式
            }else if (this.isInter(n)) {
    
    
                // n.textContent = this.$vm[RegExp.$1];
                this.commandHandler(n,'h-text',RegExp.$1)
            }
        })
    }

    isElement(n) {
    
    
        return n.nodeType === 1;
    }

    isInter(n){
    
    
        return n.nodeType === 3 && /\{\{(.*)\}\}/.test(n.textContent)
    }

    // 处理指令 编译
    compileElement(n){
    
    
        const attrs = n.attributes;
        Array.from(attrs).forEach(attr=>{
    
    
            const attrName = attr.name;
            const exp = attr.value;
            // 判断是否为特定的属性指令 h-
            if(this.isDir(attrName)){
    
    
                this.commandHandler(n,attrName,exp);
            }
        })
    }

    isDir(attrName){
    
    
        return attrName&&attrName.startsWith('h-');
    }

    // 匹配对应的指令处理函数
    commandHandler(node,attrName,exp){
    
    
        const fn = this[attrName.replace('h-','')+'CommandHandler'];
        fn&&fn.call(this,node,this.$vm[exp]);

        // 创建watcher 
        new Watcher(this.$vm,exp,()=>fn(node,this.$vm[exp]))
    }

    //h-text指令编译
    textCommandHandler(node,value){
    
    
        node.textContent = value;
    }

    //h-html
    htmlCommandHandler(node,value){
    
    
        node.innerHTML= value;
    }
}

// Watcher
class Watcher{
    
    
    constructor(vm,key,updater){
    
    
        this.vm = vm;
        this.key = key;
        this.updater = updater;

        Dep.target = this;
        console.log('Dep.target',Dep.target)
        this.vm[key]; //触发对应data的get函数
        Dep.target = null;
    }

    update(){
    
    
        this.updater()
    }
}
//保存Watcher 的 Dep 实例
class Dep{
    
    
    constructor(){
    
    
        this.watchers = []; 
    }

    add(watcher){
    
    
        this.watchers.push(watcher);
        console.log('add',watcher,this.watchers)
    }

    emit(){
    
    
        console.log('emit',this.watchers)
        this.watchers.forEach(watcher=>watcher.update());
    }
}

hvue.html, write a timer to change the value of counter.


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>简版vue的实现</title>
</head>
<body>
    <div id="app">
        <p>{
    
    {
    
    counter}}
            <span>{
    
    {
    
    sum}}</span>
        </p>
        <p h-text="counter"></p>
        <p h-html="htmlText"></p>
    </div>
</body>
<!-- <script src="../node_modules/vue/dist/vue.js"></script> -->
<script src="./hvue.js"></script> 
<script> 
    const app = new Hvue({
    
    
        el:'#app',
        data:{
    
    
            counter:1,
            sum:1,
            htmlText:'<span style="color:red">我成功啦</span>'
        }
    })
    setInterval(() => {
    
    
        // console.log(app.counter)
        app.counter++
    }, 1000);
</script>
</html>

We can see that the page can be updated
insert image description here

The source code is here.
The next part will implement onClick and h-input.

Guess you like

Origin blog.csdn.net/qq_42944436/article/details/125054942