手写Vue2.0实现-简易版

332 阅读3分钟

目标

初步掌握Vue2框架的底层原理

要点

不想啰嗦,直接盘它

盘它.webp

先来个HTML文件

<div id="app">
    <p>{{counter}}</p>
    <p v-text="counter"></p>
</div>

<script src="./MyVue.js"></script>
<script>
    const app = new XuSirVue({
        el: '#app',
        data: {
           counter: 1
        },
    })
    setInterval(() => {
        app.counter++
    }, 1000)
</script>

重点来了,我里乖乖,先看下MVVM经典原理图😎😎😎

vue.png

先来一个响应式,由浅入深,继续向下看⬇

function defineReactive(obj, key, val) {
    // 递归处理,给对象深层次拦截
    observe(val)
    // 创建Dep实例
    const dep = new Dep()
    Object.defineProperty(obj, key, {
        get() {
            // 判断一下Dep.target是否存在,若存在进行依赖收集
            // 这里的Dep.target其实就是watcher
            Dep.target && dep.addDep(Dep.target);
            return val
        },

        set(v) {
            if (v !== val) {
                val = v
            }
            // 检测到值变化通知对应的dep管理的watchers进行更新
            // this.deps.forEach(dep => dep.update());
            // 也就是watcher执行更新函数 this.updateFn.call(this.vm, this.vm[this.key]);对应下面⬇⬇⬇
            dep.notify()
        }
    })
}

// 对象响应式,执行defineReactive
function observe(obj) {
    // 首先判断obj是否是对象,忽略我的有问题的写法😂
    if (typeof obj !== 'object' || obj === null) {
        return
    }
    new Observer(obj) // 观察者⬇⬇⬇
}

搞一个观察者去监听数据的变化并使其变成响应式⬇

// 观察者:监听传过来的数据 ---> 变成响应式数据
class Observer {
    constructor(obj) {
        this.value = obj
        if (Array.isArray(obj)) {
            // todo 数组的重写7个方法暂不处理 push/shift/pop/unshift...
        } else {
            this.work(obj)
        }
    }
    // 给对象加上拦截实现响应式
    work(obj) {
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key])
        })
    }
}

插播一个代理--把data的key代理到vm上,我们可以直接写this.xxx拿到data中变量而不必写this.data.xxx⬇

// 异常处理忽略--比如vm已经有了某个$data[key],我们不能覆盖原属性key
function proxy(vm) {
    Object.keys(vm.$data).forEach(key => {
        Object.defineProperty(vm, key, {
            get() {
                return vm.$data[key];
            },
            set(newVal) {
                vm.$data[key] = newVal;
            }
        });
    })
}

Vue主入口来了⬇

class XuSirVue {
    constructor(options) {
        // 1.响应式
        this.$options = options
        // 把数据绑定到$data
        this.$data = options.data
        observe(this.$data) // 开始观察所有的data属性

        // 代理- 把vm传给proxy方法做处理
        proxy(this)
        
        // 2.compile编译
        // 就是把{{}}这些变量、 v-xx 指令编译成对应的值和绑定上对应的方法
        new Compile(options.el, this)
    }
}

搞一个编译器--有点长但是注释很清晰,哈哈😂⬇

class Compile {
    constructor(el, vm) {
        this.$vm = vm;
        this.$el = document.querySelector(el);
        // 遍历el
        if (this.$el) {
            this.compile(this.$el);
        }
    }

    compile(el) {
        const childNodes = el.childNodes;
        // childNodes是伪数组
        Array.from(childNodes).forEach(node => {
            if (this.isElement(node)) {
                console.log("编译元素" + node.nodeName);
                this.compileElement(node)
            } else if (this.isInterpolation(node)) {
                console.log("编译插值⽂本v-text" + node.textContent);
                this.compileText(node);
            }
            if (node.childNodes && node.childNodes.length > 0) {
                this.compile(node);
            }
        });
    }

    // 元素
    isElement(node) {
        return node.nodeType == 1;
    }

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

    // 使用RegExp.$1拿到{{xxx}}中的插值属性 xxx
    compileText(node) {
        console.log(RegExp.$1);
        // node.textContent = this.$vm[RegExp.$1];
        this.update(node, RegExp.$1, 'text')
    }

    // 获取指令表达式 v-xxx
    isDirective(attr) {
        return attr.indexOf("v-") == 0;
    }

    // 编译元素
    compileElement(node) {
        let nodeAttrs = node.attributes;
        // 编译元素上属性 v-if v-xxx
        Array.from(nodeAttrs).forEach(attr => {
            let attrName = attr.name;
            let exp = attr.value;
            if (this.isDirective(attrName)) {
                let dir = attrName.substring(2);
                this[dir] && this[dir](node, exp);
            }
        });
    }

    // v-text
    text(node, exp) {
        this.update(node, exp, 'text')
    }

    // v-html
    html(node, exp) {
        this.update(node, exp, 'html')
    }

    // 统一处理 指令v-
    // dir: text  exp: {{xxx}}中的xxx node: 使用指令的元素节点
    update(node, exp, dir) {
        const fn = this[dir + 'Updater']
        fn && fn(node, this.$vm[exp])
        // 触发视图更新
        new Watcher(this.$vm, exp, function (val) {
            fn && fn(node, val)
        })
    }

    // 处理v-text的节点赋值
    textUpdater(node, val) {
        node.textContent = val;
    }
    // 处理v-html的节点赋值
    htmlUpdater(node, val) {
        node.innerHTML = val
    }
}

看累了?来瓶红牛继续盘它⬇

红牛.png

dep 用于依赖收集 主要监听data中属性, 一个属性对应一个dep负责监听变化并通过watcher观察者进行更新⬇

class Dep {
    constructor() {
        // 依赖集合
        this.deps = []
    }
    // 收集所有的依赖,一个data对应一个dep管家,上面的拦截方法中的getter触发的时候进行收集
    addDep(dep) {
        this.deps.push(dep)
    }
    // data属性的值改变会触发setter,通知视图进行更新
    notify() {
        this.deps.forEach(dep => dep.update());
    }
}

创建watcher时触发getter,初始化时把对应的data[key]渲染到视图,后续管家dep拦截到值改变批量进行更新,为什么是批量,因为一个data[key]可能不止一个地方使用,比如页面中出现了插值表达式{{initStatus}} 也出现了指令 v-text="initStatus",那initStatus 就对应一个dep管家和两个watcher,initStatus改变触发dep的notify方法是watcher执行update最终使页面视图更新💘

class Watcher {
    constructor(vm, key, updateFn) {
        this.vm = vm;
        // 依赖key
        this.key = key;
        // 更新函数
        this.updateFn = updateFn;

        // 获取一下key的值触发get,并创建当前watcher实例和dep的映射关系
        Dep.target = this;
        // 这一步的目的是初始化取值触发getter 
        this.vm[this.key];
        Dep.target = null;
    }

    // 更新
    update() {
        this.updateFn.call(this.vm, this.vm[this.key]);
    }
}

来看下执行效果😇🤓🤡

55555.png

perfect.png

补充

如果觉得上述代码太乱,可以到我的github看完整版✔