欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

200行代码实现简易的 mvvm - vue

程序员文章站 2022-03-30 10:41:40
...

第一个知识点 - Object.defineProperty()

  • 说明: 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

  • 备注: 应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。

1. 在这里我们需要了解 getset 方法

const data = {}
let value = ''

Object.defineProperty(this.data, "msg", {
    get(){
        // 当对象的 key 被访问的时候会执行这个方法
        // 这里添加我们自己的方法就会优先执行
        return value
    },
    set(newVal){
        // 与 get 方法相似,当给当前属性赋值的时候会自调用 set 方法
        // 自己的方法
        if (value === newVal) return
        value = newVal
    }
})

2. 当同时使用 set get 方法时需要一个真实的中间变量,而我们又不想将这个变量暴露在外面,因此我们将其封装

// 我们封装这样一个函数,这样 value 可以充当中间变量
// 这里会触发闭包,value 这个值一直保存在内存中
defineReactive(obj, key, value) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(value);
            return value
        },
        set: (newVal) => {
            if (value === newVal) return
            console.log(value, newVal);
            value = newVal
        }
    })
}

第二个知识点 - 发布订阅模式和观察者模式

  • 这两种设计模式一直傻傻分不清楚,知道有一天我逛 知乎 我发现其中的奥妙,没啥区别 - -!

  • 其核心思想就是通过感知变化从而做出反应

1. 举个例子来说明下观察者模式

// 定义一个被观察者 Subject 或者叫 Observable
class Subject {
    constructor() {
        this.observers = [] // 维护一个观察者(Observer)的集合 - 观察列表
        this.data = {}
        this.defineReactive(this.data, "msg", '')
    }
	// 将 this.data 进行数据劫持,当给这个属性赋值时向订阅者推送消息
    defineReactive(obj, key, value) {
        Object.defineProperty(obj, key, {
            set: (newVal) => {
                if (value === newVal) return
                this.publicMsg(newVal)
                value = newVal
            }
        })
    }

    publicMsg(msg) {
        this.observers.forEach(observer => [
            observer.receive(msg)
        ])
    }

    addObserver(observer) {
        this.observers.push(observer)
    }
}

// 定义观察者,需要接收一个参数一个被观察者,将自己添加到其观察列表
class Observer {
    constructor(name, subject) {
        this.name = name
        subject.addObserver(this)
    }
    receive(msg) {
        console.log(`${this.name} 收到了消息 ${msg}`);
    }
}


const sub = new Subject

const obs1 = new Observer('limy1', sub)
const obs2 = new Observer('limy2', sub)

sub.data.msg = '观察者模式'

现在我们来实现一个精简版的 vue

  • 说明:因为是精简版的 vue 我们只看实现原理,一些特殊情况不做考虑,以最理想的最精简的方式展现 MVVM

200行代码实现简易的 mvvm - vue

1. 准备一个测试的数据,我们先建一个 class Vue 拿到外部传入的数据

class Vue {
    constructor(options) {
        this.$el = options.el
        this.$data = options.data
    }
}

2. 怎么将页面上的 {{obj.name}}v-text 这种类似于槽的地方填上我们传入的数据呢

// 我们创建一个编译的类
class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
        this.vm = vm
        const fragment = this.node2Fragment(this.el) // 将 dom 转化为文档碎片
        this.compile(fragment) // 在这里完成 dom 上的槽与 data 上的数据结合
        this.el.appendChild(fragment) // 合并好的数据添加到页面
    }

    node2Fragment(node) {
        const fragment = document.createDocumentFragment()
        let firstChild
        while (firstChild = node.firstChild) {
            fragment.appendChild(firstChild)
        }
        return fragment
    }

    // 文本节点和元素节点不同,所以我们分别处理,考虑到 dom 节点会出现嵌套,因此使用递归完成深度遍历
    compile(node) {
        const childNodes = node.childNodes;
        [...childNodes].forEach(childNode => {
            this.isElementNode(childNode) ? this.compileElement(childNode) : this.compileText(childNode);
            (childNode.childNodes && childNode.childNodes.length) && this.compile(childNode)
        })
    }

    compileText(node) {
        const text = node.textContent
        if (/\{\{(.+?)\}\}/g.test(text)) {
            compileUtil.text(node, text, this.vm)
        }
    }

    compileElement(node) {
        const [...attrs] = node.attributes
        attrs.forEach(attr => {
            const {
                name,
                value
            } = attr
            if (name.startsWith('v-')) { // 找到以 v-开头的属性
                const [_, directive] = name.split('-') // ["v", "text"]
                compileUtil[directive](node, value, this.vm)
            }
        })
    }
    isElementNode(node) {
        return node.nodeType === 1
    }
}
// 解耦 将处理不同格式的数据封装
const compileUtil = {
    getVal(expr, vm) { // 将传入的表达式在 data 中取值
        return expr.split('.').reduce((data, currentVal) => data[currentVal], vm.$data)
    },
    text(node, expr, vm) { // 这里是精简版不考虑 {{obj.name}} -- {{obj.name}} 这种情况
        let val
        if (expr.indexOf('{{') !== -1) { // expr {{obj.name}}
            val = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
                expr = args[1]
                return this.getVal(args[1], vm)
            })
        } else { // expr v-text
            val = this.getVal(expr, vm)
        }
        // new Watcher(vm, expr, newVal => this.updater(node, newVal))
        this.updater(node, val)
    },
    updater(node, val) {
        node.textContent = val
    }
}

完成这些 我们就能在网页上看到合并后的结果,控制台也没有出现错误

200行代码实现简易的 mvvm - vue

3. 劫持监听 $data 上的所有属性

class Observer {
    constructor(data) {
        this.observe(data)
    }
    observe(obj) {
        if (obj && typeof obj === 'object') {
            Object.keys(obj).forEach(key => {
                this.defineReactive(obj, key, obj[key])
            })
        }
    }
    defineReactive(obj, key, value) {
        this.observe(value) // 考虑到数据嵌套,我们对其递归处理
        Object.defineProperty(obj, key, {
            get() {
                return value
            },
            set(newVal) {
                console.log('newVal', newVal);
                if (value !== newVal) {
                    value = newVal
                }
            }
        })
    }
}

200行代码实现简易的 mvvm - vue

可以看到当我们对 vue 实例上 $datamsg 属性进行赋值时,会打印出 newVal newMsg ,说明我们已经完成了对 $data 数据的劫持监听

4. Dep 是一个简单的观察者模式实现,它的 subs 用来存储所有订阅它的 Watcher

class Dep {
    constructor() {
        this.subs = []
    }
    addSub(watcher) {
        this.subs.push(watcher);
    }
    // 通知变化
    notify() {
        this.subs.forEach(w => w.update());
    }
}

5. Watcher 可以看作一个更新函数,每一个数据都有自己的更新函数

class Watcher {
    constructor(vm, expr, cb) {
        // 观察新值和旧值的变化,如果有变化 更新视图
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 先把旧值存起来  
        this.oldVal = this.getOldVal();
    }
    getOldVal() {
        Dep.target = this; // 在这里我们将 watcher 实例挂在到 Dep.target 上
        // 在执行时会访问 $data 上的属性,这样就会触发劫持的 get() 方法
        // 在 get 方法中 我们通过 Dep.target 就能够获取到当前实例 将其添加到 subs 中,这样就完成了对应
        let oldVal = compileUtil.getVal(this.expr, this.vm);
        Dep.target = null;  // 防止同时添加多个 watcher 我们将 Dep.target 置空
        return oldVal;
    }
    update() {
        // 更新操作 数据变化后 Dep会发生通知 告诉观察者更新视图
        let newVal = compileUtil.getVal(this.expr, this.vm);
        if (newVal !== this.oldVal) {
            this.cb(newVal);
        }
    }
}

6. vue 中访问或者修改属性可以通过实例直接修改,怎么弄的呢

// 对数据代理,使之可以通过实例访问属性 vm.$data.msg => vm.msg
proxyData() {
    Object.keys(this.$data).forEach(key => {
        Object.defineProperty(this, key, {
            get() {
                return this.$data[key]
            },
            set(newVal) {
                this.$data[key] = newVal
            }
        })
    })
}

完整代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue</title>
</head>

<body>
    <div id="app">
        <h2>{{obj.name}}</h2>
        <h2>{{obj.age}}</h2>
        <h3 v-text='obj.name' id="h3"></h3>
        <h4 v-text='msg'></h4>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
        <h3>{{msg}}</h3>
    </div>
    <script>
        class Vue {
            constructor(options) {
                this.$el = options.el
                this.$data = options.data
                new Observer(this.$data)
                this.proxyData()
                new Compile(this.$el, this)
            }
        }

        class Compile {
            constructor(el, vm) {
                this.el = this.isElementNode(el) ? el : document.querySelector(el)
                this.vm = vm
                const fragment = this.node2Fragment(this.el) // 将 dom 转化为文档碎片
                this.compile(fragment) // 在这里完成 dom 上的槽与 data 上的数据结合
                this.el.appendChild(fragment) // 合并好的数据添加到页面
            }

            node2Fragment(node) {
                const fragment = document.createDocumentFragment()
                let firstChild
                while (firstChild = node.firstChild) {
                    fragment.appendChild(firstChild)
                }
                return fragment
            }

            // 文本节点和元素节点不同,所以我们分别处理,考虑到 dom 节点会出现嵌套,因此使用递归完成深度遍历
            compile(node) {
                const childNodes = node.childNodes;
                [...childNodes].forEach(childNode => {
                    this.isElementNode(childNode) ? this.compileElement(childNode) : this.compileText(childNode);
                    (childNode.childNodes && childNode.childNodes.length) && this.compile(childNode)
                })
            }

            compileText(node) {
                const text = node.textContent
                if (/\{\{(.+?)\}\}/g.test(text)) {
                    compileUtil.text(node, text, this.vm)
                }
            }

            compileElement(node) {
                const [...attrs] = node.attributes
                attrs.forEach(attr => {
                    const {
                        name,
                        value
                    } = attr
                    if (name.startsWith('v-')) { // 找到以 v-开头的属性
                        const [_, directive] = name.split('-') // ["v", "text"]
                        compileUtil[directive](node, value, this.vm)
                    }
                })
            }
            isElementNode(node) {
                return node.nodeType === 1
            }
        }

        const compileUtil = {
            getVal(expr, vm) { // 将传入的表达式在 data 中取值
                return expr.split('.').reduce((data, currentVal) => data[currentVal], vm.$data)
            },
            text(node, expr, vm) { // 这里是精简版不考虑 {{obj.name}} -- {{obj.name}} 这种情况
                let val
                if (expr.indexOf('{{') !== -1) { // expr {{obj.name}}
                    val = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
                        expr = args[1]
                        return this.getVal(args[1], vm)
                    })
                } else { // expr v-text
                    val = this.getVal(expr, vm)
                }
                new Watcher(vm, expr, newVal => this.updater(node, newVal))
                this.updater(node, val)
            },
            updater(node, val) {
                node.textContent = val
            }
        }

        class Observer {
            constructor(data) {
                this.observe(data)
            }
            observe(obj) {
                if (obj && typeof obj === 'object') {
                    Object.keys(obj).forEach(key => {
                        this.defineReactive(obj, key, obj[key])
                    })
                }
            }
            defineReactive(obj, key, value) {
                this.observe(value) // 考虑到数据嵌套,我们对其递归处理
                const dep = new Dep
                Object.defineProperty(obj, key, {
                    get() {
                        Dep.target && dep.addSub(Dep.target)
                        return value
                    },
                    set(newVal) {
                        console.log('newVal', newVal);
                        if (value !== newVal) {
                            value = newVal
                        }
                        dep.notify()
                    }
                })
            }
        }

        class Watcher {
            constructor(vm, expr, cb) {
                // 观察新值和旧值的变化,如果有变化 更新视图
                this.vm = vm;
                this.expr = expr;
                this.cb = cb;
                // 先把旧值存起来  
                this.oldVal = this.getOldVal();
            }
            getOldVal() {
                Dep.target = this;
                let oldVal = compileUtil.getVal(this.expr, this.vm);
                Dep.target = null;
                return oldVal;
            }
            update() {
                // 更新操作 数据变化后 Dep会发生通知 告诉观察者更新视图
                let newVal = compileUtil.getVal(this.expr, this.vm);
                if (newVal !== this.oldVal) {
                    this.cb(newVal);
                }
            }
        }


        class Dep {
            constructor() {
                this.subs = []
            }
            // 添加订阅者
            addSub(watcher) {
                this.subs.push(watcher);
            }
            // 通知变化
            notify() {
                // 观察者中有个update方法 来更新视图
                this.subs.forEach(w => w.update());
            }
        }




        const vm = new Vue({
            el: '#app',
            data: {
                obj: {
                    name: 'limy',
                    age: 24,
                },
                msg: 'vue 简易版',
            }
        })
    </script>
</body>

</html>
相关标签: WEB前端 vue