记录手写简易vue2对象

135 阅读4分钟
原理

每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把“接触”过的数据property记录为依赖。之后当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染

解析

Vue类负责初始化,将data中的属性注入到Vue实例中,并调用Observer类和Complier类,对数据进行劫持和解析。 Observer类负责数据劫持,通过Object,definePrototype,实现每一个data转换为getter和setter。 Compiler类负责解析指令和编译模板、初始化视图、收集依赖、更新视图。 Dep类负责收集依赖、添加观察者模式、通知data对应的所有观察者watcher来更新视图。 Watcher类负责数据更新后,使关联视图重新渲染(更新DOM)。

涉及知识点:
  • Object.defineProperty(obj, property, descriptor) 参数
  1. obj:绑定属性的目标对象
  2. property: 绑定的属性名
  3. descriptor:属性描述(配置),且此参数本身为一个对象 descriptor 属性值
  4. value: 设置属性默认值
  5. writable:设置属性是否能够修改
  6. enumerable:设置属性是否可以枚举,即是否允许遍历
  7. configurable: 设置属性是否可以删除或编辑
  8. get:获取属性的值
  9. set:设置属性的值
  • Array.reduce((total, currentValue, currentIndex, arr) => {}, initialValue) 参数
  1. 回调函数
  2. initialValue:初始值的默认值 回调函数参数
  3. total:初始值
  4. currentValue:当前值
  5. currentIndex:当前下标
  6. arr:当前数组
myVue类
class myVue {
    constructor(options) {
        // 限制options类型必须为对象,否则抛出错误
        if (!(options instanceof Object)) throw new TypeError('The options must be an Object !')

        // 保存options中的数据
        this.$options = options || {}
        this.$data = options.data || {}
        this.$el = options.el

        // 将vue实例中的data属性转换为getter和setter,并注入到vue实例中
        this._proxyData(this.$data)
        //调用Observer类, 进行数据监听
        new Observer(this.$data)
        // 如果el元素有值, 调用Compiler类, 解析指令和插值表达式
        if (this.$el) new Compiler(this.$el, this)

    }


    _proxyData(data) {
        // 限制data类型必须为对象,否则抛出错误
        if (!(data instanceof Object)) throw new TypeError('The data must be an Object !')

        // 遍历data属性的key, 利用Object,definePrototype 进行数据劫持
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                enumerable: true,      // 设置属性是否可以枚举,即是否允许遍历
                configurable: true,    // 设置属性是否可以删除或编辑
                set(newVal) {
                    if (newVal !== data[key]) data[key] = newVal
                },
                get() {
                    return data[key]
                }
            })
        })
    }
}
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])
            })
        }
    }

    // 设置属性为响应式数据
    defineReactive(obj, key, value) {
        this.observe(value)     // 递归保证深层对象也设置为响应式数据     
        const that = this       // 保存内部this, 方便内部调用
        let dep = new Dep()     // 负责收集依赖并发送通知
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                Dep.target && dep.addSub(Dep.target)     // 订阅数据变化时, 往Dep中添加观察者, 收集依赖
                return value
            },
            set(newVal) {
                that.observe(newVal)
                if (newVal !== value) value = newVal
                dep.notify()     // 发送通知
            }
        })
    }
}
Compiler类
class Compiler {
    // 解析指令的对象
    static compileUtil = {
        // 获取指令上的数据值
        getVal(key, vm) {
            //利用reduce 获取实例对象深层的属性值 
            return key.split('.').reduce((data, current) => {
                return data[current]
            }, vm.$data)
        },
        // 改变实例上的数据
        // key 属性值, vm: vue实例对象, inputVal: 设置的值
        setVal(key, vm, inputVal) {
            // 用于拼接出 vm['person']['name'] = inputVal
            let total = 'vm'
            if (key.split('.').length === 1) {
                vm[key] = inputVal
            } else {
                key.split('.').forEach(k => total += `['${k}']`)
                total += ('=' + `${'inputVal'}`)
                eval(total)     // 利用eval强行将字符串解析为函数执行
            }
        },
        // 编译v-text指令的方法
        text(node, key, vm) {
            let value// 保存获取的数据值
            if (/\{\{(.+?)\}\}/.test(key)) {
                // 利用正则全局匹配{{}}里面的变量, 利用...运算符展开匹配的内容,并取出相应的变量值
                value = key.replace(/\{\{(.+?)\}\}/, (...args) => {
                    // 创建watcher对象, 当数据改变时, 更新视图
                    new Watcher(vm, args[1], newVal => {
                        this.updater.textUpdater(node, newVal)
                    })
                    return this.getVal(args[1], vm)
                })
            } else {
                value = this.getVal(key, vm)          // 获取key对应的数据
            }
            this.updater.textUpdater(node, value)     // 更新视图
        },
        // 解析v-model 指令
        model(node, key, vm) {
            const value = this.getVal(key, vm)
            /// 数据 => 视图 
            new Watcher(vm, key, newVal => {
                this.updater.modelUpdater(node, newVal)
            })
            // 通过添加input事件监听视图更新,再修改数据,实现双向绑定
            node.addEventListener('input', e => {
                this.setVal(key, vm, e.target.value)
            })
            this.updater.modelUpdater(node, value)
        },
        // 解析HTML指令
        html(node, key, vm) {
            const value = this.getVal(key, vm)
            new Watcher(vm, key, newVal => {
                this.updater.htmlUpdater(node, newVal)
            })
            this.updater.htmlUpdater(node, value)
        },
        // 解析v-on:click指令
        on(node, key, vm, eventName) {
            // 获取实例对象中的methods中的方法
            const fn = vm.$options.methods && vm.$options.methods[key]
            // 绑定事件
            node.addEventListener(eventName, function (ev) {
                fn.call(vm, ev)   // 改变fn函数内部的this,并传递事件对象event
            }, false)
        },
        // 解析 v-bind 指令
        bind(node, key, vm, AttrName) {
            node[AttrName] = vm.$data[key]
        },

        // 保存所有更新页面视图的方法的对象
        updater: {
            textUpdater(node, value) {
                node.textContent = value
            },
            htmlUpdater(node, value) {
                node.innerHTML = value
            },
            modelUpdater(node, value) {
                node.value = value
            }
        }
    }
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
        this.vm = vm
        // 获取文档碎片, 减少页面的回流和重绘
        const fragment = this.nodeFragment(this.el)
        // 编译文档碎片
        this.compile(fragment)
        // 追加到根元素
        this.el.appendChild(fragment)
    }
    // 创建文档碎片
    nodeFragment(el) {
        const fragment = document.createDocumentFragment()
        // 如果当前第一个子节点有值, 追加到文档碎片
        while (el.firstChild) {
            fragment.appendChild(el.firstChild)
        }
        return fragment
    }
    // 获取子节点
    compile(fragment) {
        const childNodes = fragment.childNodes;
        // 遍历所的子节点
        [...childNodes].forEach(child => {
            // 如果为元素节点
            if (this.isElementNode(child)) {
                this.compileElement(child)
            } else {
                // 解析文本节点
                this.compileText(child)
            }
            // 如果子节点还有子节点元素就递归遍历该子节点
            if (child.childNodes && child.childNodes.length) {
                this.compile(child)
            }
        })
    }

    // 编译元素节点
    compileElement(node) {
        const { compileUtil } = Compiler
        // 获取元素节点的所有自定义属性
        const attributes = node.attributes;

        // 利用展开运算符将attributes类数组对象转换为数组并遍历
        [...attributes].forEach(attr => {
            // 将v-mode=msg 中的 v-model 和 msg 解构出来
            const { name, value } = attr
            // 判断属性是否为 v-开头
            if (this.isDirective(name)) {
                // 解构出v-text 中的 text
                const [, directive] = name.split('-')
                // 解构出 v-on:click 中的 on 和 click
                const [dirname, eventName] = directive.split(':')
                // 利用策略组合模式,调用相应的解析方法并更新数据及数据驱动视图
                compileUtil[dirname](node, value, this.vm, eventName)
                // 删除有指令的标签上的属性
                node.removeAttribute('v-' + directive)
            }
        })
    }
    // 编译文本节点
    compileText(node) {
        const text = node.textContent
        const { compileUtil } = Compiler
        // 匹配 {{}} 内的节点
        let reg = /\{\{(.+?)\}\}/ 
        if (reg.test(text)) { 
            compileUtil['text'](node, text, this.vm)
        }
    }
    // 检查是否为指令,既是否为v-开头的指令
    isDirective(attrName) {
        return attrName.startsWith('v-')
    }
    // 检查是否为元素节点
    isElementNode(node) { 
        return node.nodeType === 1
    }
}
Dep类
class Dep {
    constructor() {
        this.subs = []  // 保存所有的观察者列表
    }
    addSub(sub) { // 收集观察者
        this.subs.push(sub)
    }
    notify() { // 通知观察者就更新视图
        this.subs.forEach(w => w.update())
    }
}
Watcher类
class Watcher {
    constructor(vm, key, callback = value => { }) {
        this.vm = vm  
        this.key = key                      // data中的属性名
        this.callback = callback            //回调函数负责更新视图
        this.oldValue = this.saveOldValue()  //先把旧值保存起来
    }
    saveOldValue() {
        // 把Watcher对象挂载到Dep类的静态属性target中
        Dep.target = this
        const oldVal = Compiler.compileUtil.getVal(this.key, this.vm)
        // 清空watcher对象,避免重复设置
        Dep.target = null
        return oldVal
    }
    // 当数据发生改变时调用callback并传递新值用于更新视图
    update() {
        const newVal = Compiler.compileUtil.getVal(this.key, this.vm)
        if (newVal !== this.oldValue) {
            this.callback(newVal)
        }
    }
}
测试环节
<html lang="en">
<head>
	<meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>test new Vue</title>
</head>
<body>
	<div id="app">
        <span v-on:click="onClick">{{msg}}</span>
        <input v-model="inputValue" v-on:input="onInput" />
    </div>
</body>
<script type="text/javascript" src="./vue.js"></script>
<script type="text/javascript">
    let vm = new myVue({
        el: '#app',
        data: {
            msg : '111',
            inputValue: '',
        },
        methods: {
            onClick(e) {
                vm.msg = 'data msg = 1111'
                console.log(vm.msg)
            },
            onInput(e) {
                console.log(vm.inputValue)
            }
        }
    })
</script>
</html>

1651583353(1).jpg

1651583377(1).jpg