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

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

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

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

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
        }
    })
}

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

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

  • 其核心思想就是经过感知变化从而作出反应java

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

在这里插入图片描述

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
    }
}

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

在这里插入图片描述

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
                }
            }
        })
    }
}

在这里插入图片描述

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

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>