本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

2024-11(11)

Vue源码解析(一)深入浅出手撕简易VUE.JS和MVVM原理

发布于2021-05-30 11:39     阅读(1134)     评论(0)     点赞(11)     收藏(3)


Vue源码解析(一)深入浅出手撕简易VUE.JS和MVVM原理

声明
本文参考了小马哥的视频讲解和代码,结合自己的理解以及其它资料综合得出,水平有限,错误之处还望斧正。

一、最初的最初
先讲结论,Vue是采用数据劫持配合发布者-订阅者模式的方式,通过object.defineproperty()来劫持各个属性的getter和setter,当数据发生变动时,发布消息给依赖收集器,去通知观察者做出对应的回调函数来更新视图(也就是updater对象里面具体的更新方法)。
这其中最关键的在于几个类/对象:compile、observer、dep、watcher、updater,这几个各有处理的任务又相互关联共同实现MVVM。
其中最根本的是负责编译的compile(解析各种指令和{{}}),并且在其里面安排了updater对象负责数据在视图的渲染和更新(无非是利用innerHTML、content什么的的来呈现数据),在此也实例化了观察者watcher(其中已经绑定了数据更新时对应的函数)与之建立联系。
observer负责对数据的劫持和监听(主要靠defineProperty完成),并且在其中实例化化了工具人Dep(就是声明个数组存储观察者并提供对应方法的类),getter的时候添加观察者,setter的时候利用dep通知观察者及时更新,由此调用观察者的更新方法也就是回调到updater对象完成数据更新。

二、compiler类的实现
声明一个被外界实例化的大类(如Vue),并且判断节点,以后要往里面添加observer和compile两个主要的类。

class MVue { // 实现一个大类(外面new的类)
    constructor(options) { // 接收传来的el、data、method等等
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options; // 赋值
        if (this.$el) { // 判断是存在el节点就去实现编译类和observer类
            new compile(this.$el, this);
        }
    }
}

那么先来实现compile类,拿到真正的节点并且创建文档碎片。使用文档碎片是为了避免对节点的操作触发过多的重绘和回流。

class compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el); // 元素节点直接给,字符串就去找
        this.vm = vm;
        const fragment = this.node2Fragment(this.el); // 为根节点el创建文档碎片对象
        this.compile(fragment); // 编译子节点
        this.el.appendChild(fragment); // 编译完成给真正的根节点去显示编译修改过的子节点们
    }
}
    node2Fragment (el) { // 创建文档碎片
        const f = document.createDocumentFragment();
        let firstChild;
        while(firstChild = el.firstChild) { // 每次while都重新给一个firstChild的值
            f.appendChild(firstChild); // 当前的firstChild也会被删除
        }
        return f;
    }
    isElementNode (node) { // 判断是否是元素节点
        return node.nodeType === 1;
    }

重点是compile方法的实现,分别对元素节点和文本节点进行编译,这里调用的compileUtil是作为编译类compile的辅助对象。

    compile (fragment) {
        const childNodes = fragment.childNodes;
        [...childNodes].forEach((child) => {
            if (this.isElementNode(child)) { // 元素节点
                this.compileElment(child);
            } else { // 文本节点
                this.compileText(child);
            }
            if (child.childNodes && child.childNodes.length) { // 考虑到子节点可能有很多嵌套所以递归
                this.compile(child);
            }
        })
    }
    compileText (node) { // 文本节点处理
        const content = node.textContent; // 拿到对应的文本内容
        if ((/\{\{(.+?)\}\}/).test(content)) { // 文本内容是否匹配{{}}
            compileUtil['text'](node, content, this.vm); // 是就走text方法
        }
    }
    compileElment(node) { // 元素节点处理
        const attributes = node.attributes;
        [...attributes].forEach((attr) => { // 遍历元素们以键值对方式呈现
            const {name, value} = attr;
            if (this.isDirective(name)) { // v-text v-on:click 以V-开头不断分割得到操作名和紧跟着的vlaue
                const [, directive] = name.split('-'); // text on:click
                const [directiveName, eventName] = directive.split(':') // text on与click
                compileUtil[directiveName](node, value, this.vm, eventName); // 调用操作方法
                node.removeAttribute('v-' + directive)
            } else if (this.isEventName(name)) { // 以@开头
                const [, eventName] = name.split('@');
                compileUtil['on'](node, value, this.vm, eventName);
            }
        })
    }
    isEventName (attrName) { // 判断是否以@开头
        return attrName.startsWith('@')
    }
    isDirective (attrName) { // 判断是否以v-开头
        return attrName.startsWith('v-');
    }

辅助对象compileUtil对象里面有不同的方法,都是接受上面传来的参数们,这些方法都是为了从data中拿到对应的值然后调用一个公共的赋值的方法来赋值更新。

const compileUtil = { // 辅助编译
    getVal (expr, vm) { // 不断的.下去直到拿到真正的值
        return expr.split('.').reduce((data, currentVal) => {
            return data[currentVal];
        }, vm.$data)
    },
    text (node, expr, vm) {
        let value;
        if (expr.indexOf('{{') !== -1) { // 处理{{}}
            value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
                return this.getVal(args[1], vm);
            })
        } else {
            value = this.getVal(expr, vm); // 处理v-text
        }
        this.updater.textUpdater(node, value); // 在上面不同情况拿到值后直接更新视图即可
    },
    html (node, expr, vm) {
        const value = this.getVal(expr, vm);
        this.updater.htmlUpdater(node, value);
    },
    model (node, expr, vm) {
        const value = this.getVal(expr, vm);
        this.updater.modelUpdater(node, value);
    },
    on (node, expr, vm, eventName) {
        let fn = vm.$options.methods && vm.$options.methods[expr]; // 拿到对应的方法
        node.addEventListener(eventName, fn.bind(vm), false); // 添加方法记得绑定到当前vue实例
    },
    updater: {
        textUpdater(node, value) {
            node.textContent = value;
        },
        htmlUpdater(node, value) {
            node.innerHTML = value;
        },
        modelUpdater(node, value) {
            node.value = value;
        }
    }
}

截止到目前已经可以解析编译出指令和{{}}包裹的内容了。

三、实现observer劫持并监听所有属性

接下来是针对实例化传入的数据做一个劫持和监听。注意记得递归遍历完所有的数据,一个属性对应一个观察者。

class observer {
    constructor (data) {
        this.observe(data);
    }
    observe(data) { // 对数据做一个递归遍历去调用监听方法
        if (data && typeof data === 'object') {
            Object.keys(data).forEach((key) => {
                this.defineReactive(data, key, data[key]); // 住:data = data[key]
            })
        }
    }
    defineReactive(obj, key, value) { // 劫持和监听数据
        this.observe(value); // 首先需要遍历当前数据(因为可能多嵌套)
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: false,
            get() {
                return value;
            },
            set: (newVal) => {
                this.observe(newVal); // 更新对象时再监听一遍防止新对象没有被监听到
                if (newVal !== value) {
                    value = newVal;
                }
            }
        })
    }
}

四、实现观察者Watcher和依赖收集器Dep

实现Dep来存储watcher,一方面需要能够通知控制watcher更新,另一方面是添加watcher的方法。

class Dep {
    constructor () {
        this.subs = []; // 留着存储观察者们
    }
    addSub (watcher) { // 添加观察者
        this.subs.push(watcher);
    }
    notify () { // 通知观察者更新
        this.subs.forEach((w) => {
            w.update();
        })
    }
}

创建一个Watcher的class,作用就是看看新旧值是否有变化,有就需要做一个更新/回调去更新视图,接收vue实例、expr表达式、回调函数(拿到新值后回调到前面的compile更新,因为compile还是会与watcher有关联)。那么就需要拿到新值和旧值。

class watcher {
    constructor (vm, expr, cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        this.oldVal = this.getOldVal(); // 保存旧值
    }
    getOldVal () {
        Dep.target = this; // 将当前观察者挂载到Dep上再去触发get
        const oldVal = compileUtil.getVal(this.expr, this.vm);
        Dep.target = null; // 挂载完毕需要注销,防止重复挂载 (数据一更新就会挂载)
        return oldVal;
    }
    update () {
        const newVal = compileUtil.getVal(this.expr, this.vm); // 获取新值
        if (newVal !== this.oldVal) this.cb(newVal); // 更新,回去调更新函数
    }
}

那么Dep怎么与observer关联?劫持数据的时候就可以实例化Dep了,而get的时候可以添加观察者

    defineReactive(obj, key, value) { // 劫持和监听数据
        this.observe(value); // 首先需要递归遍历当前数据(因为可能多嵌套)
        const dep = new Dep(); // 实例化dep
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: false,
            get() {
                Dep.target && dep.addSub(Dep.target); // 确保存在观察者才添加
                return value;
            },
            set: (newVal) => {
                this.observe(newVal); // 更新对象时再监听一遍防止新对象没有被监听到
                if (newVal !== value) {
                    value = newVal;
                }
                dep.notify(); // 通知观察者更新
            }
        })
    }

那么观察者实例化的时机?可以看到应该是在compiler类中订阅数据变化,所以在test、html等等方法中可以实例化观察者。

getVal (expr, vm) { // 不断的.下去直到拿到真正的值
        return expr.split('.').reduce((data, currentVal) => {
            return data[currentVal];
        }, vm.$data)
    },
    getContentVal (expr, vm) {
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getVal(args[1], vm);
        })
    },
    text (node, expr, vm) {
        let value;
        if (expr.indexOf('{{') !== -1) { // 处理{{}}
            value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
                new watcher(vm, args[1], (newVal) => {
                    this.updater.textUpdater(node, this.getContentVal(expr, vm));
                })
                return this.getVal(args[1], vm);
            })
        } else {
            value = this.getVal(expr, vm); // 处理v-text
            new watcher(vm, expr, (newval) => {
                this.updater.textUpdater(node, newval);
            })
        }
        this.updater.textUpdater(node, value); // 在上面不同情况拿到值后直接更新视图即可
    },
    html (node, expr, vm) {
        const value = this.getVal(expr, vm);
        new watcher(vm, expr, (newval) => {
            this.updater.htmlUpdater(node, newval);
        })
        this.updater.htmlUpdater(node, value);
    },
    model (node, expr, vm) {
        const value = this.getVal(expr, vm);
        new watcher(vm, expr, (newval) => {
            this.updater.modelUpdater(node, newval);
        })
        this.updater.modelUpdater(node, value);
    },

六、实现双向的数据绑定和proxy代理

给input输入框添加一个input事件实现视图更改数据。

    setVal (expr, vm, inputVal) { // 设置输入框输入的值
        return expr.split('.').reduce((data, currentVal) => {
            return data[currentVal] = inputVal; // 给老值赋值输入框的值
        }, vm.$data)
    },
    model (node, expr, vm) {
        const value = this.getVal(expr, vm);
        new watcher(vm, expr, (newval) => { // 数据更改视图
            this.updater.modelUpdater(node, newval);
        })
        node.addEventListener('input', (e) => { //视图更改数据
            this.setVal(expr, vm, e.target.value);
        })
        this.updater.modelUpdater(node, value);
    },

至于代理的话是想要直接就this.属性不需要this.$data.属性,在大类里面声明一个代理的方法,只需要遍历所有的data去劫持返回在数据中的data!

    proxyData(data) {
        for(const key in data) {
            Object.defineProperty(this, key, {
                get() {
                    return data[key];
                },
                set(newVal) {
                    data[key] = newval;
                }
            })
        }
    }

具体源码:
https://github.com/Sheng14/Hit-the-frontend/tree/main/Vue/MVVM
我是O5,共勉,也请大家关注一下我的公众号:游戏前端。

原文链接:https://blog.csdn.net/ODST_TheSolverO5/article/details/117329517




所属网站分类: 技术文章 > 博客

作者:强哥你们辛苦了

链接:http://www.qianduanheidong.com/blog/article/115939/236468db2a1ba9afac44/

来源:前端黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

11 0
收藏该文
已收藏

评论内容:(最多支持255个字符)