手写简易版vue

109 阅读3分钟

有了前一篇文章的defineReactive的知识前提,这一节,我们开始动手实现自己的简易版vue,我们的目标是实现下面html中的kvue.js,保证正常的功能即可

<!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>Document</title>
</head>

<body>
    <div id="app">
        <p>{{counter}}</p>
        <p>{{counter}}</p>
        <p k-text="counter"></p>
        <p k-html="desc"></p>
    </div>
    <script src="kvue.js"></script>

    <script>
        const app = new KVue({
            el: '#app',
            data: {
                counter: 1,
                desc: '<span style="color:red;">html</span>',
            },
        })
        setInterval(() => {
            app.counter++;
        }, 1000);
    </script>
</body>
</html>

首先分析一下需求,在我们引入kvue.js以后,我们会有一个kvue的类,它可以绑定我们的#appDOM ,会编译dom中的双花括号语法来替换成我们vue对象中的data数据,并且可以识别我们的k-text和k-html指令,这里的k-text可以理解成和{{}}一样的效果,k-html相当于在元素中插入html,并且下面的定时器可以触发页面的dom变化

image.png

上面这张图就是kvue的核心思想,在new vue的时候,主要做两件事

  • 1.创建一个observer对象,这个对象的负责接受我们data上的数据,来对这些数据做响应式,在创建observer的get的时候,去创建Dep(依赖)的实例,用来做通知更新

  • 2.compile模板引擎解析模板,初始化页面,收集依赖,并在初始化的同时做订阅

涉及类型介绍

  • KVue:框架构造函数
  • Observer:执⾏数据响应化
  • Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
  • Watcher:执⾏更新函数(更新dom)
  • Dep:管理多个Watcher,批量更新

开始实现我们kvue的类

//数据响应式实现
function defineReactive(obj,key,val){
    //递归
    observe(val);
    Object.defineProperty(obj,key,{
        get(){
            console.log('get',key);
            return val
        },
        set(newVal){
            console.log('set',key);
            //保证nweval是新对象  做响应式处理
            observe(newVal);
            if(newVal !== val){
                val = newVal
            }
        }
    })
}

// 遍历obj所有key  做响应式处理
function observe(obj){
    if(typeof(obj) !== 'object' || obj === null){
        return
    }
    Object.keys(obj).forEach(key=>{
        defineReactive(obj,key,obj[key]);
    })
}

class KVue{
    constructor(options){
        this.$outions = options;
        this.$data = options.data; 
        observe(this.$data);
    }
}

写完以后,我们已经把传入的data使用observe做响应式拦截了,但在页面发现并不能触发get和set的log输出,这是因为data现在不是在我们的实例上的,(我们把app.count++变成 app.$data.count++ 就可以看到log输出了),我们先改写一下observe,创建一个Observer类

function observe(obj){
    if(typeof(obj) !== 'object' || obj === null){
        return
    }
    new Observer(obj);
}

class Observer{
    constructor(value){
        this.value = value;
        if(Array.isArray(value)){

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

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

然后我们需要做完响应式以后做一步代理,外面实例就可以直接访问data了

function proxy(vm){
    Object.keys(vm.$data).forEach(key=>{
        Object.defineProperty(vm, key, {
            get(){
                return vm.$data[key];
            },
            set(v){
                vm.$data[key] = v;
            }
        })
    })
}

//1.对data选项做响应式处理
//2.编译模板
class KVue{
    constructor(options){
        this.$options = options;
        this.$data = options.data; 
        observe(this.$data);
        proxy(this);
    }
}

接下来实现编译器,这个就需要慢慢扣着一点点看了,取到元素遍历,解析指令,设置dom...

class Compile {
    constructor(el,vm){
        this.$el = document.querySelector(el);
        this.$vm = vm;

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

    compile(el){
        const childNodes = el.childNodes;
        childNodes.forEach(node => {
            // 1 元素
            if(node.nodeType === 1){
                const attrs = node.attributes;
                Array.from(attrs).forEach(attr=>{
                    const attrName = attr.name
                    const exp = attr.value
                    if(attrName.startsWith('k-')){
                        const dir = attrName.substring(2);
                        this[dir] && this[dir](node,exp);
                    }
                })
            }else if(this.isInter(node)){//3.文本
                this.compileText(node);
            }
            if(node.childNodes){
                this.compile(node);
            }
        })
    }

    update(node, exp, dir){
        //1 初始化
        const fn = this[dir + 'Updater'];
        fn && fn(node, this.$vm[exp]);
    }

    text(node, exp){
        this.update(node,exp,'text');
    }

    textUpdater(node,value){
        node.textContent = value;
    }

    html(node, exp){
        this.update(node,exp,'html');
    }

    htmlUpdater(node,value){
        node.innerHTML = value;
    }

    //是否插值表达式
    isInter(node){
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
    }

    compileText(node){
        this.update(node,RegExp.$1,'text');
    }
}

//1.对data选项做响应式处理
//2.编译模板
class KVue{
    constructor(options){
        this.$options = options;
        this.$data = options.data; 
        observe(this.$data);
        proxy(this);
        new Compile(options.el, this);
    }
}

接下来,重头戏来了,收集依赖,在我们Compile的时候,每发现一个动态的东西,都创建一个watcher,我们都称他为贴身小秘书,他负责管理当前元素的更新,1对1,每一个watcher都有一个dep大管家来管理,需要更新时由dep统一通知,

//监听器  负责依赖收集
class Watcher {
    constructor (vm, key, updateFn) {
        this.vm = vm;
        this.key = key;
        this.updateFn = updateFn;

        Dep.target = this;
        this.vm[this.key];
        Dep.target = null;
    }
    //未来被Dep调用
    update(){
        // 执行实际更新操作
        this.updateFn.call(this.vm, this.vm[this.key]);
    }
}

class Dep {
    constructor() {
        this.deps = [];
    }

    addDep(dep){
        this.deps.push(dep)
    }

    notify() {
        this.deps.forEach(dep => {
            dep.update();
        })
    }
}


//数据响应式实现
function defineReactive(obj,key,val){
    observe(val);
    const dep = new Dep();
    Object.defineProperty(obj,key,{
        get(){
            console.log('get',key);
            Dep.target && dep.addDep(Dep.target);
            return val
        },
        set(newVal){
            console.log('set',key);
            //保证nweval是新对象  做响应式处理
            if(newVal !== val){
                observe(newVal);
                val = newVal;
                dep.notify();
            }
        }
    })
}