VUE的实现原理(数据劫持、观察者模式)

1,712 阅读2分钟

记一次vue原理学习记录,涉及了如何进行数据劫持、数据代理、观察者模式(发布订阅)、数据双向绑定功能等知识点,也简单实现了vue中一些常用功能,代码中做了比较详细的注释,可以直接复制执行查看结果

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MVVM原理</title>
</head>
<body>
    <div id="app">
        {{school.name}}{{age}}
        <div>{{age}}</div>
        <input type="text" v-model="school.name">
        <div>{{school.name}}</div>
        <div>compute: {{getNewName}}</div>
        <ul>
            <li>li_1</li>
            <li>li_2</li>
        </ul>
        <button v-on:click="changeName">methods</button>
        <div v-html="htmlName"></div>
    </div>
    <!-- <script src="./node_modules/vue/dist/vue.js"></script> -->
    <script src="mvvm.js"></script>
    <script>
        let vm = new Vue({
            // el: document.querySelector('#app'),
            el: '#app',
            data: {
                school: {
                    name: '学校名'
                },
                age: 20,
                htmlName: '<h1>inner html</h1>'
            },
            methods: {
                changeName () {
                    this.school.name = '我是methods方法修改的'
                }
            },
            computed: {
                getNewName () {
                    return this.school.name + 'new'
                }
            }
        })

    </script>
</body>
</html>

mvvm.js

// 基础类 负责调度
class Vue {
    constructor (options) {
        this.$el = options.el
        this.$data = options.data
        let computed = options.computed
        let methods = options.methods
        // 编译模版
        if (this.$el) {
            // 数据劫持 把数据全部转化为Object.defineProperty来定义
            new Observer(this.$data)
            // computed存在依赖关系,执行computed方法会取值触发get
            for (let key in computed) {
                Object.defineProperty(this.$data, key, {
                    get: () => {
                        return computed[key].call(this)
                    }
                })
            }
            for (let key in methods) {
                Object.defineProperty(this, key, {
                    get () {
                        return methods[key]
                    }
                })
            }
            // 数据代理 vm上的数据操作代理到vm.$data上
            this.proxyVm(this.$data)
            // 编译模版
            new Compiler(this.$el, this)
        }
    }
    // 将vm操作代理到vm.$data上取值
    proxyVm (data) {
        for (let key in data) {
            Object.defineProperty(this, key, {
                get () {
                    return data[key] // 进行转化操作
                }, 
                set (newVal) {
                    data[key] = newVal
                }
            })
        }
    }
}

// 对数据进行劫持
class Observer {
    constructor (data) {
        this.observer(data)
    }
    observer (data) {
        // 如果是对象就进行观察
        if (data && typeof data === 'object') {
            for (let key in data) {
                this.defineReactive(data, key, data[key])
            }
        }
    }
    defineReactive (obj, key, value) {
        // 给每个数据都添加一个具有发布订阅的功能
        let dep = new Dep()
        Object.defineProperty(obj, key, {
            get () {
                // 当创建watcher时候会取到值,会触发get,并且将watcher放到全局
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set: (newVal) => {
                if (value !== newVal) {
                    this.observer(newVal) // 对新赋值的进行劫持
                    value = newVal
                    dep.notify() // 触发发布方法,通知watcher修改了数据
                }
            }
        })
        this.observer(value) // 进行递归劫持数据
    }
}

// 观察者模式 (发布订阅模式)观察者 被观察者
class Watcher { // 观察者
    constructor (vm, expr, cb) {
        this.vm = vm
        this.expr = expr
        this.cb = cb
        this.oldVal = this.getValue() // 先存放一个老值
    }
    getValue () {
        Dep.target = this // 先将自己放到Dep.target
        let value = CompilerUtils.getValue(this.vm, this.expr)
        Dep.target = null // 触发get后清除掉
        return value
    }
    update () { // 更新操作据发生变化了会执行观察者的update方法
        let newVal = CompilerUtils.getValue(this.vm, this.expr)
        if (newVal !== this.oldVal) {
            this.cb(newVal)
        }
    }
}
// 发布订阅
class Dep {
    constructor () {
        this.subs = [] // 存放所有的watcher
    }
    // 订阅
    addSub (watcher) {
        this.subs.push(watcher)
    }
    // 发布
    notify () { // 执行所有watcher的update方法
        this.subs.forEach(watcher => watcher.update())
    }
}

// 编译类
class Compiler {
    constructor (el, vm) { // 传入 '#app' || document.querySelector('#app')
        this.vm = vm
        // 判断el是否为元素节点
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
        // console.log(this.el)
        // 将元素使用文档碎片存到内存中
        this.fragment = this.el2fragment(this.el)
        // console.log('fragment:', this.fragment)
        // 编译模版 将fragment中的变量进行替换
        this.compile(this.fragment)
        // 重新将fragment塞回el
        this.el.appendChild(this.fragment)
    }
    isElementNode (node) { // 判断是否元素节点
        return node.nodeType === 1
    }
    // 把节点存到内存中
    el2fragment (node) {
        // 创建一个文档碎片
        let fragment = document.createDocumentFragment()
        let firstChild
        while (firstChild = node.firstChild) {
            // console.log(firstChild)
            // fragment.appendChild 具有移动性,会删除相应的节点
            fragment.appendChild(firstChild)
        }
        return fragment
    }
    compile (fragment) { // 用来编译内存中的dom节点
        let childNodes = [...fragment.childNodes]// 类数组
        childNodes.forEach(child => {
            if (this.isElementNode(child)) {
                this.compileElement(child)
                // 递归编译子节点
                this.compile(child)
            } else {
                // console.log('text: ', child)
                this.complieText(child)
            }
        })
    }
    // 编译节点
    compileElement (node) {
        let attrs = [...node.attributes]
        // console.log('attrs:', [...attrs])
        attrs.forEach(attr => {
            let { name, value:expr } = attr
            // 判断是否节点
            if (this.isDirecttive(name)) {
                let [, directive] = name.split('-')
                let arr
                if (directive.split(':').length > 0) {
                    arr = directive.split(':')
                } else {
                    arr = [directive]
                }
                let [directiveName, eventName] = arr
                CompilerUtils[directiveName](node, expr, this.vm, eventName)
            }
        })
    }
    // 编译文本节点
    complieText (node) {
        let content = node.textContent
        if (/\{\{(.+?)\}\}/.test(content)) {
            // console.log(content)
            CompilerUtils['text'](node, content, this.vm)
        }
    }
    // 判断是否指令
    isDirecttive (attrName) {
        return attrName.startsWith('v-')
    }
}

// 编译工具方法
CompilerUtils = {
    // 根据表达式取值
    getValue (vm, expr) {
        return expr.split('.').reduce((data, curKey) => {
            return data[curKey]
        }, vm.$data)
    },
    setValue (vm, expr, value) {
        expr.split('.').reduce((data, curKey, index, arr) => {
            if (index === arr.length -1) { // 给表达式最后一个赋值
                return data[curKey] = value
            }
            return data[curKey]
        }, vm.$data)
    },
    updater: { // 更新值的方法
        modelUpdater (node, value) {
            node.value = value
        },
        htmlUpdater (node, value) {
            node.innerHTML = value
        },
        textUpdater (node, value) { // 更新文本节点
            node.textContent = value
        }
    },
    model (node, expr, vm) { // node 节点 expr 表达式 vm 当前实例
        // 给输入框赋值value
        let value = this.getValue(vm, expr)
        let fn = this.updater['modelUpdater']
        // 放入观察者
        new Watcher(vm, expr, (newVal) => {
            fn(node, newVal) // 将新值给输入框赋值
        })
        fn(node, value)
        // 视图驱动数据  给v-model绑定input事件
        node.addEventListener('input', evt => {
            let val = evt.target.value
            this.setValue(vm, expr, val)
        })
    },
    on (node, expr, vm, eventName) {
        node.addEventListener(eventName, (evt) => {
            // console.log(vm, expr)
            vm[expr].call(vm, evt) // 执行相应方法methods
        })
    },
    getContentVal (vm, expr) {
        let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getValue(vm, args[1])
        })
        return content
    },
    text (node, expr, vm) { // 处理文本节点
        let fn = this.updater.textUpdater
        let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            // 为文本插入观察者 给{{}}的都加上观察者
            new Watcher(vm, args[1], () => {
                fn(node, this.getContentVal(vm, expr)) // 返回一个全字符串
            })
            return this.getValue(vm, args[1])
        })
        fn(node, content) // 更新值
    },
    html (node, expr, vm) {
        let fn = this.updater['htmlUpdater']
        new Watcher(vm, expr, (newVal) => {
            fn(node, newVal) // 将新值给v-html赋值
        })
        fn(node, this.getValue(vm, expr))
    }
}