300行代码手写简单vue.js,彻底弄懂MVVM底层原理

当我们对vue的用法较为熟练的时候,但有时候在排查bug的时候还是会有点迷惑。主要是因为对vue各种用法和各种api使用都是只知其然而不知其所以然。这时候我们想到可以去看看源码,但是源码太长,其实我们只要把大概实现流程实现一遍,很多开发中想不明白的地方就会豁然开朗。下面我们就来实现一个简单的vue.js

vue采取数据劫持,配合观察者模式,通过Object.defineProperty() 来劫持各个属性的setter和getter,在数据变动时,发布消息给依赖收集器dep,去通知观察者,做出对应的回调函数,去更新视图。(也就是在getter中收集依赖,在setter中通知依赖更新。)

其实vue主要就是整合Observer,compile和watcher三者,通过Observer来监听 model数据变化表,通过compile来解析编译模板指令,最终利用Watcher搭起observer 和compile的通信桥梁,达到数据变化=>视图变化,视图变化=>数据变化的双向绑定效果。

下面来一张图↓

300行代码手写简单vue.js,彻底弄懂MVVM底层原理

 这个流程图已经非常形象深刻的表达了vue的运行模式,当你理解了这个流程,再去看vue源码时就会容易很多了

声明一下,下面的代码只简单实现了vue里的

  1. v-model(数据的双向绑定)
  2. v-bind/v-on
  3. v-text/v-html
  4. 没有实现虚拟dom,采用文档碎片(createDocumentFragment)代替
  5. 数据只劫持了Object,数组Array没有做处理

代码大致结构如下,初步定义了6个类

300行代码手写简单vue.js,彻底弄懂MVVM底层原理

 代码如下,具体操作案例可以看==>GitHub

// 定义Vue类
class Vue {
    constructor(options) {
        // 把数据对象挂载到实例上
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        // 如果有需要编译的模板
        if (this.$el) {
            // 数据劫持 就是把对象的所有属性 改成get和set方法
            new Observer(this.$data);
            // 用数据和元素进行编译
            new Compiler(this.$el, this);
            // 3. 通过数据代理实现 主要给methods里的方法this直接访问data
            this.proxyData(this.$data);
        }
    }
    //用vm代理vm.$data 
    proxyData(data){
        for(let key in data){
            Object.defineProperty(this,key,{
                get(){
                    return data[key];
                },
                set(newVal){
                    data[key] = newVal;
                }
            })
        }
    }
}

// 编译html模板
class Compiler {
    // vm就是vue对象
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        if(this.el){ // 如果该元素能获取到,我们开始编译
            // 1.把真实的dom放到内存中fragment文档碎片
            let fragment = this.node2fragment(this.el);
            // console.log(fragment);
            // 2.编译 => 提取想要的元素节点 v-model和文本节点{{}}
            this.compile(fragment);
            // 3.把编译好的fragment再放到页面里
            this.el.appendChild(fragment);
        }
    }
    
    /* 一些辅助方法 */
    isElementNode(node) {
        return node.nodeType === 1;
    }
    isDirective(name) { // 判断是不是指令
        return name.includes(‘v-‘);
    }
    isEventName(attrName){ // 判断是否@开头
        return attrName.startsWith(‘@‘);
    }
    isBindName(attrName){ // 判断是否:开头
        return attrName.startsWith(‘:‘);
    }
    
    /* 核心方法区 */
    node2fragment(el){ // 需要将el中的内容全部放到内存中
        // 文档碎片
        let fragment = document.createDocumentFragment();
        let firstChild;
        while(firstChild = el.firstChild){
            fragment.appendChild(firstChild);
        }
        return fragment; // 内存中的节点
    }
    compile(fragment){
        // 1.获取子节点
        let childNodes = fragment.childNodes;
        // 2.递归循环编译
        [...childNodes].forEach(node=>{
            if(this.isElementNode(node)){
                this.compileElement(node); // 这里需要编译元素
                this.compile(node); // 是元素节点,还需要继续深入的检查
            }else{
                // 文本节点
                // 这里需要编译文本
                this.compileText(node);
            }
        });
    }
    compileElement(node){ // 编译元素
        // 带v-model v-html ...
        let attrs = node.attributes; // 取出当前节点的属性
        // attrs是类数组,因此需要先转数组
        [...attrs].forEach(attr=>{
            // console.log(attr); // type="text" v-model="content" v-on:click="handleclick" @click=""...
            let attrName = attr.name; // type v-model v-on:click @click
            if(this.isDirective(attrName)){ // 判断属性名字是不是包含v-
                // 取到对应的值放到节点中
                let expr = attr.value; // content/变量 handleclick/方法名
                // console.log(expr)
                let [, type] = attrName.split(‘-‘); // model html on:click
                let [compileKey, detailStr] = type.split(‘:‘); // 处理 on: bind:
                // node this.vm.$data expr
                CompileUtil[compileKey](node, this.vm, expr, detailStr);
                // 删除有指令的标签属性 v-text v-html等,普通的value等原生html标签不必删除
                node.removeAttribute(‘v-‘ + type);
            }else if(this.isEventName(attrName)){ // 如果是事件处理 @click=‘handleClick‘
                let [, detailStr] = attrName.split(‘@‘);
                CompileUtil[‘on‘](node, this.vm, attr.value, detailStr);
                // 删除有指令的标签属性
                node.removeAttribute(‘@‘ + detailStr);
            }else if(this.isBindName(attrName)){ // 如果是:开头,动态绑定值
                let [, detailStr] = attrName.split(‘:‘);
                CompileUtil[‘bind‘](node, this.vm, attr.value, detailStr);
                // 删除有指令的标签属性
                node.removeAttribute(‘:‘ + detailStr);
            }
        })
    }
    compileText(node){ // 编译文本
        // 带{{}}
        let expr = node.textContent; // 取文本中的内容
        let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}}
        if(reg.test(expr)){
            // node this.$data
            // console.log(expr); // {{content}}
            CompileUtil[‘text‘](node, this.vm, expr);
        }
    }
}

// 编译模版具体执行
const CompileUtil = {
    getVal(vm, expr){ // 获取实例上对应的数据
        expr = expr.split(‘.‘); // [animal,dog]/[animal,cat]
        return expr.reduce((prev, next)=>{ // vm.$data.
            return prev[next];
        }, vm.$data)
    },
    // 这里实现input输入值变化时 修改绑定的v-model对应的值
    setVal(vm, expr, inputValue){ // [animal,dog]
        let exprs = expr.split(‘.‘), len = exprs.length;
        exprs.reduce((data,currentVal, idx)=>{
            if(idx===len-1){
                data[currentVal] = inputValue;
            }else{
                return data[currentVal]
            }
        }, vm.$data)
    },
    getTextVal(vm, expr){ // 获取编译文本后的结果
        return expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
            // console.log(args); // ["{{title}}", "title", 0, "{{title}}"]
            // ["{{ animal.dog }}", " animal.dog ", 0, "{{ animal.dog }}-vs-{{ animal.cat }}"]
            return this.getVal(vm, args[1].trim());
        });
    },
    text(node, vm, expr){ // 文本处理
        let updateFn = this.updater[‘textUpdater‘];
        // {{content}} => "welcome to animal world"
        let value;
        if(expr.indexOf(‘{{‘)!==-1){ // dom里直接写{{}}的时候
            value = this.getTextVal(vm, expr);
            // {{a}} {{b}} 对多个值进行监控
            expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
                new Watcher(vm, args[1].trim(), ()=>{
                    // 如果数据变化了,文本节点需要重新获取依赖的属性更新文本中的内容
                    updateFn && updateFn(node, this.getTextVal(vm, expr));
                })
            });
        }else{ // v-text 的时候
            value = this.getVal(vm, expr);
            new Watcher(vm, expr, (newVal)=>{
                // 当值变化后会调用cb 将新值传递过来
                updateFn && updateFn(node, newVal);
            });
        }
        updateFn && updateFn(node, value);
    },
    html(node, vm, expr) { // 
        let updateFn = this.updater[‘htmlUpdater‘];
        updateFn && updateFn(node, this.getVal(vm, expr));
    },
    model(node, vm, expr){ // 输入框处理
        let updateFn = this.updater[‘modelUpdater‘];
        // console.log(this.getVal(vm, expr)); // "welcome to animal world"
        // 这里应该加一个监控 数据变化了  应该调用这个watch的callback
        new Watcher(vm, expr, (newVal)=>{
            // 当值变化后会调用cb 将新值传递过来
            updateFn && updateFn(node, newVal);
        });
        // 视图 => 数据 => 视图
        node.addEventListener(‘input‘, (e)=>{
            this.setVal(vm, expr, e.target.value);
        })
        updateFn && updateFn(node, this.getVal(vm, expr));
    },
    on(node, vm, expr, detailStr) {
        let fn = vm.$options.methods && vm.$options.methods[expr];
        node.addEventListener(detailStr, fn.bind(vm), false);
    },
    bind(node, vm, expr, detailStr){
        // v-bind:src=‘...‘ => href=‘...‘
        node.setAttribute(detailStr, expr);
    },
    updater:{
        // 文本更新
        textUpdater(node, value){
            node.textContent = value;
        },
        // html更新
        htmlUpdater(node, value){
            node.innerHTML = value;
        },
        // 输入框更新
        modelUpdater(node, value){
            node.value = value;
        }
    }
}

// 观察者
class Observer{
    constructor(data){
        this.observe(data);
    }
    observe(data){
        // 要对data数据原有属性改成set和get的形式
        if(!data || typeof data !== ‘object‘){ // 不是对象就不劫持了
            return
        }
        // 要劫持 先获取到data的key和value
        Object.keys(data).forEach(key=>{
            this.defineReactive(data, key, data[key]); // 劫持
            this.observe(data[key]); // 深度递归劫持
        })
    }
    // 定义响应式
    defineReactive(obj, key, value){
        let dep = new Dep();
        // 在获取某个值的时候
        Object.defineProperty(obj, key, {
            enumerable: true, // 可枚举
            configurable: false, // 可修改
            get(){ // 当取值的时候
                // 订阅数据变化时,往Dev中添加观察者
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            // 采用箭头函数在定义时绑定this的定义域
            set: (newVal)=>{ // 更改data里的属性值的时候
                if(value === newVal) return;
                this.observe(newVal); // 如果设置新值是对象,劫持
                value = newVal;
                // 通知watcher数据发生改变
                dep.notify();
            }
        })
    }
}

// 观察者的目的就是给需要变化的那个元素增加一个观察者,当数据变化后执行对应的方法
class Watcher{
    constructor(vm, expr, cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 先获取一下老的值
        this.oldVal = this.getOldVal();
    }
    // 获取实例上对应的老值
    getOldVal(){
        // 在利用getValue获取数据调用getter()方法时先把当前观察者挂载
        Dep.target = this;
        const oldVal = CompileUtil.getVal(this.vm, this.expr);
        // 挂载完毕需要注销,防止重复挂载 (数据一更新就会挂载)
        Dep.target = null;
        return oldVal;
    }
    // 对外暴露的方法 通过回调函数更新数据
    update(){
        const newVal = CompileUtil.getVal(this.vm, this.expr);
        if(newVal !== this.oldVal){
            this.cb(newVal); // 对应watch的callback
        }
    }
}

// Dep类存储watcher对象,并在数据变化时通知watcher
class Dep{
    constructor(arg) {
        // 订阅的数组
        this.subs = []
    }
    addSub(watcher){
        this.subs.push(watcher);
    }
    notify(){ // 数据变化时通知watcher更新
        this.subs.forEach(w=>w.update());
    }
}

300行代码手写简单vue.js,彻底弄懂MVVM底层原理

上一篇:Flume基础(七):企业开发案例(四)


下一篇:[HAOI2008]糖果传递