手写一个vue

整体结构

mvvm图

三个组成部分

  • Observer:数据处理,通过Object.defineProperty把数据变成响应式,收集观察者,在数据变化时通知观察者。
  • Compiler:模板编译,编译模板挂载数据,并生成观察者,在数据变化时接收通知,然后更新视图。
  • Dep/Watcher:观察者模式。

手写代码

class SimpleVue {
    constructor(options) {
        this.$el = typeof options.el === 'string'
            ? document.querySelector(options.el)
            : options.el
        this.$data = options.data
        this.methods = options.methods

        // 把data的属性都挂载到vm实例上,方便vm.msg这样调用,这一步是非必需的,可以不看
        this._proxyDataToVm(this.$data)

        // 把data变成响应式数据,数据变化时通知观察者
        new Observer(this.$data)
        // 编译模板挂载数据,并生成观察者,在数据变化时接收通知,然后更新视图
        new Compiler(this)
    }
    _proxyDataToVm(data) {
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                configurable: true,
                enumerable: true,
                get() {
                    return data[key]
                },
                set(newVal) {
                    if (newVal !== data[key]) {
                        data[key] = newVal
                    }
                }
            })
        })
    }
}

class Observer {
    constructor(data) {
        this.observe(data)
    }
    // 劫持数据,全部属性变为响应式
    observe(data) {
        Object.keys(data).forEach(key => {
            // 不能在get()里直接return data[key],否则会造成死循环
            let value = data[key]
            let dep = new Dep()
            Object.defineProperty(data, key, {
                configurable: true,
                enumerable: true,
                get: function () {
                    // if(!Dep.depMap[key]) {
                    //     Dep.depMap[key] = new Dep()
                    // }
                    // 通过targetWatcher中间变量添加warcher,注意看watcher的addSelfToSub方法
                    if (Dep.targetWatcher) {
                        console.log('add watcher');
                        dep.addSub(Dep.targetWatcher)
                    }
                    return value
                },
                set: function (newVal) {
                    let oldVal = data[key]
                    if (oldVal !== newVal) {
                        value = newVal
                        // 通知观察者更新视图
                        // Dep.depMap[key].notify(newVal)
                        dep.notify(newVal)
                    }
                }
            })
            if (typeof value === 'object') {
                this.observe(data[key])
            }
        })
    }
}

// 编译模板
// 1.编译初始化模板数据,第一次挂载页面
// 2.生成观察者,接收数据变化的通知,然后更新视图
class Compiler {
    constructor(vm) {
        this.el = vm.$el
        this.vm = vm
        this.compile(this.el)

        // 创建文档碎片,减少页面重排重绘,这一步不是必须的
        // const fragment = document.createDocumentFragment();
        // while (el.firstChild) {
        //     fragment.appendChild(el.firstChild)
        // }
        //this.el.appenChild(fragment)
    }
    // 编译模板
    compile(el) {
        let childNodes = el.childNodes
        childNodes.forEach(node => {
            if (this.isElementNode(node)) {
                this.compileElementNode(node)
            } else if (this.isTextNode(node)) {
                this.compileTextNode(node)
            }
            // 递归处理子节点
            if (node.childNodes && node.childNodes.length) {
                this.compile(node)
            }
        })
    }
    // 判断是否元素节点,如div标签,但div里面的文本属于文本节点
    isElementNode(node) {
        return node.nodeType === 1
    }
    // 判断是否文本节点
    isTextNode(node) {
        return node.nodeType === 3
    }
    // 编译文本节点
    compileTextNode(node) {
        // 1.匹配{{msg}}
        const text = node.textContent
        let reg = /\{\{(.+?)\}\}/;// 匹配{{xx}},{{xx.xx}}的正则
        // 如果是 {{}} 的文本节点,下面的代码只做原理实现
        // {{obj.name}}这种格式的取值的时候需要用reduce构造出this.vm.$data['obj']['name']才行
        // 赋值时要构造出`this.vm.$data['obj']['name']=newVal`字符串,然后用eval()强制执行
        if (reg.test(text)) {
            // 2.渲染msg数据
            let key = RegExp.$1.trim()
            console.log(key);
            node.textContent = this.vm.$data[key]
            // 3.添加观察者,在msg改变的时候通知观察者更新视图,这里实现比较巧妙!注意看wathcer的addSelfToDep方法
            let watcher = new Watcher(this.vm, key, (newVal) => {
                console.log('cb', newVal);
                node.textContent = newVal
            })
            // Dep.depMap[key].addSub(watcher)
        }
    }
    // 编译元素节点,处理指令v-text,v-html
    compileElementNode(node) {
        console.log(node.attributes);
        console.log(Array.from(node.attributes));
        Array.from(node.attributes).forEach(attr => {
            console.log(attr.nodeName, attr.nodeValue);
            let attrName = attr.name
            let attrValue = attr.value
            if (attrName === 'v-text') {
                node.textContent = this.vm.$data[attrValue]
                // 添加观察者
                new Watcher(this.vm, attrValue, (newVal) => {
                    console.log('cb', newVal);
                    node.textContent = newVal
                })
            }
            if (attrName === 'v-html') {
                node.innerHTML = this.vm.$data[attrValue]
                // 添加观察者
                new Watcher(this.vm, attrValue, (newVal) => {
                    console.log('cb', newVal);
                    node.innerHTML = newVal
                })
            }
            // v-model,本质上是input事件
            if (attrName === 'v-model') {
                // 初始值赋给输入框
                node.value = this.vm.$data[attrValue]
                new Watcher(this.vm, attrValue, (newVal) => {
                    console.log('cb', newVal);
                    node.value = newVal
                })
                node.addEventListener('input', (e) => {
                    console.log('input', e.target.value);
                    this.vm.$data[attrValue] = e.target.value
                })
            }
            // v-on:click
            if (attrName.startsWith('v-on')) {
                let [, event] = attrName.split(':')
                node.addEventListener(event, this.vm.methods[attrValue])
            }
        })
    }
}

class Dep {
    constructor() {
        this.subs = []
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    notify(newVal) {
        this.subs.forEach(sub => sub.update(newVal))
    }
}
Dep.depMap = {}
Dep.targetWatcher = null

class Watcher {
    constructor(vm, key, callback) {
        this.vm = vm
        this.key = key
        this.callback = callback

        this.addSelfToDep()
    }
    addSelfToDep() {
        console.log('add self to dep', this.vm, this.key);
        // 把当前watcher挂载到中间变量上
        Dep.targetWatcher = this
        // 通过访问来触发get方法,在get方法中调用addSub,通过targetWatcher中间变量把当前watcher添加到dep中
        let oldValue = this.vm.$data[this.key]
        Dep.targetWatcher = null
        // 这种方法比较巧妙,如果不理解也可以定义一个全局的depMap[key],然后在new Watcher的时候通过depMap[key].addSub添加。
    }
    update(newVal) {
        this.callback(newVal)
    }
}

var vm = new SimpleVue({
    el: '#app',
    data: {
        msg: 0,
        name: 'a',
        pager: {
            a: 1
        }
    },
    methods: {
        handleClick() {
            console.log('1');
        }
    }
})

// setInterval(() => {
//     vm.$data.msg++
//     // console.log(vm.$data.msg);
// }, 1000)