手写极简VUE(MVVM) 实现 v-click 和 v-model

40 篇文章 0 订阅
4 篇文章 0 订阅

一、介绍

通过渐进式的方式实现一个最基础的Vue响应式功能,Vue其实基础功能简单来说就以下几点:

  • 负责结构初始化的参数(选项)
  • 负责把data中的属性注入到Vue实例,转化成getter、setter
  • 负责调用observer监听data中的所有属性的变化
  • 负责调用compiler解析指令、插值表达式、

二、前置知识

1、观察者模式

观察者模式就是当一个对象(目标)被修改时,则会自动通知依赖它的对象(观察者)。

所以我们可以分别给两者定义一个类,实现一些模式方法:

  • 观察者(订阅者) – Watcher

    • update(): 当事件发生时,具体要做的事情
  • 目标(发布者) – Dep

    • subs数组:存储所有的观察者
    • addSub(): 添加观察者
    • notify(): 当事件发生,调用所有观察者的 update() 方法
  • 没有事件中心

2、发布/订阅者模式

发布/订阅模式

  • 订阅者
  • 发布者
  • 信号中心

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscrible)这个信号,从而知道什么时候自己可以开始执行。“这就叫做发布/订阅模式 (publish-subscrible pattern)”

3、 总结:

观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者的方法,所有观察者模式的订阅者于发布者之间是存在依赖的。
发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

三、MVVM原理分析

在这里插入图片描述
如上图所示,我们可以看到,整体实现分为四步

  1. 实现一个Observer,对数据进行劫持,通知数据的变化
  2. 实现一个Compile,对指令进行解析,初始化视图,并且订阅数据的变更,绑定好更新函数
  3. 实现一个Watcher,将其作为以上两者的一个中介点,在接收数据变更的同时,让Dep添加当前
  4. 实现Watcher/Dep,并及时通知视图进行update

实现MVVM,整合以上三者,作为一个入口函数。

四、动手时间

1、Vue

Vue结构:

  • $options:选项
  • $el:挂载DOM
  • $data:响应式数据
  • _proxyData():转化data,注入到Vue实例中
class Vue {
    constructor(options) {

        // 保存 options 和 data
        this.$optiosn = options || {}
        this.$data = options.data || {}
        this.$methods = options.methods || {}
        // 绑定DOM对象(字符串则通过DOM查询获取DOM)
        this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
        // 2、把data中的成员转化为getter和setter,注入到vue实例中
        this._proxyData(this.$data)
        new Observer(this.$data)
        new Compiler(this)
    }

    _proxyData(data) {
        //遍历 data 属性
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return data[key]
                },
                set(newVal) {
                    if (newVal === data[key]) {
                        return
                    }
                    data[key] = newVal
                }
            })

        })
    }
}

2、Observer

  • 功能
    • 负责把data选项中的属性转换成响应式数据
    • data中的某个属性也是对象,把该属性转化成响应式数据
    • 数据变化发送通知
  • 结构
    • walk(data) :遍历data中所有属性,实现响应式
    • defineReactive(data, key, value) :定义响应式数据
/*
* 负责把data选项中的属性转换成响应式数据
* data中的某个属性也是对象,把该属性转化成响应式数据
* */
class Observer {
    constructor(data) {
        this.walk(data)
    }

    walk(data) {
        // 1. 判断data是否是对象
        if (!data || typeof data !== 'object') {
            return
        }
        // 2.遍历data对象的所有属性
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
        })

    }

    defineReactive(obj, key, val) {
        let that = this
        // 负责收集依赖,并发送通知
        let dep = new Dep()
        // 如果val是对象,把val内部的属性转换成响应式数据
        this.walk(val)
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                Dep.target && dep.addSub(Dep.target)
                Dep.target = null
                return val
            },
            set(newValue) {
                if (newValue === val) {
                    return
                }
                val = newValue
                //    当data的属性重新赋值新对象时,该对象的属性也会被转换为响应式的
                that.walk(newValue)
                // 发送通知
                dep.notify()

            }
        })

    }
}

3、Compiler

  • 功能
    • 负责编译模板,解析指令/差值表达式
    • 负责页面的首次渲染
    • 当数据变化后重新渲染视图
  • 结构
    • el:Vue挂载的DOM
    • vm:Vue实例
    • compile(el):遍历el所有节点,根据节点类型处理
    • 文本节点 - 解析插值表达式
    • 标签节点 - 解析Vue的指令
    • compileElement(node):解析元素节点
    • complieText(node):解析文本节点
    • update(node, key, attrName):辅助方法,根据指令类型更新内容
    • isDirective(attrName):判断标签属性值是否是指令
    • isTextNode(node):判断是否是文本节点
    • isElementNode(node):判断是否是元素节点
    • clickUpdater(node, value, key):处理 v-click事件
    • modelUpdater(node, value, key):处理v-model指令
/*
* 负责编译模板,解析指令/差值表达式
* 负责页面的首次渲染
* 当数据变化后重新渲染视图
* */
class Compiler {
    constructor(vm) {
        this.el = vm.$el
        this.vm = vm
        this.compile(this.el)  //构造函数立即编译模板
    }

    compile(el) {
        let childNodes = el.childNodes
        Array.from(childNodes).forEach(node => {
            if (this.isTextNode(node)) {
                this.compileText(node)
            } else if (this.isElementNode(node)) {
                this.compileElement(node)
            }
            // 处理深层子节点,递归调用
            // 判断node节点是否有子节点,如果有子节点,递归调用complie
            if (node.childNodes && node.childNodes.length) {
                this.compile(node)
            }

        })
    }

    compileElement(node) {
        // 遍历所有的属性节点,判断是否是v-指令
        Array.from(node.attributes).forEach(attr => {
            let attrName = attr.name
            if (this.isDirective(attrName)) {
                attrName = attrName.substr(2)
                let key = attr.value
                this.update(node, key, attrName)
            }


        })
    }

    // 辅助方法,根据指令类型更新内容
    update(node, key, attrName) {
        // 根据属性名动态调用对应的更新方法
        let updateFn = this[attrName + 'Updater']
        updateFn && updateFn.call(this, node, this.vm[key], key)
    }

    compileText(node) {
        // 提取 {{}} 中的 变量 或 表达式
        let reg = /\{\{(.+?)\}\}/
        let value = node.textContent
        // 是否可以匹配插值表达式
        if (reg.test(value)) {
            let key = RegExp.$1.trim()
            // 替换成插值表达式对应的值(Vue实例的值)
            node.textContent = value.replace(reg, this.vm[key])
            // 创建watch对象,当数据改变时改变视图
            new Watcher(this.vm, key, newValue => {
                console.log(newValue)
                node.textContent = newValue
            })
        }
    }

    //处理 v-click 指令
    clickUpdater(node, value, key) {
        let _this = this.vm
        // node.addEventListener(key, _this.$methods[value].bind(_this), false)
        node.onclick = function () {
            return _this.$methods[key] && _this.$methods[key].call(_this);
        };
    }

    // 处理v-model指令
    modelUpdater(node, value, key) {
        let _this = this.vm
        node.value = value
        new Watcher(_this, value, (newValue) => {
            node.value = typeof newValue === 'undefined' ? '' : newValue
        })
        // 双向绑定,视图更新数据也更新
        node.addEventListener('input', (e) => {
            let newValue = e.target.value
            if (value == newValue) {
                return
            }
            _this[key] = newValue
        })
    }

    isTextNode(node) {
        return node.nodeType === 3
    }

    isElementNode(node) {
        return node.nodeType === 1
    }

    isDirective(artName) {
        return artName.startsWith('v-')
    }

}

4、Watcher Dep

Dep

  • 功能
    • 收集依赖,添加观察者(watcher)
    • 通知所有观察者
  • 结构
    • subs:存储所有观察者(Watcher)
    • addSub(sub):添加观察者
    • notify():通知观察者

Watcher

  • 功能
    • 当数据变化触发依赖,dep通知所有的watcher实例更新视图
    • 自身实例化的时候往dep对象中添加自己
  • 结构
    • vm(Vue实例)
    • key(data中的属性名称)
    • cb(callback,回调函数):定义如何更新视图
    • oldValue:记录之前的值(判断新旧值是否变化,是则更新视图)
    • update():更新视图
/*
* 收集依赖,添加观察者(watcher)
* 通知所有观察者
* */

class Dep {
    constructor() {
        this.subs = []
    }

    // 添加观察者
    addSub(sub) {
        // sub存在且有update()方法
        if (sub && sub.update) {
            this.subs.push(sub)
        }
    }

    // 发送通知,遍历并调用每个观察者的update()
    notify() {
        this.subs.forEach(sub => {
            sub.update()
        })
    }
}
class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm
        this.key = key

        // 回调函数负责更新视图
        this.cb = cb

        // 把watcher对象记录到dep的静态属性target,此时Dep会收集依赖
        Dep.target = this
        // 触发get方法,在get方法中调用addSub
        this.oldValue = vm[key]
        Dep.target = null
    }

    // 当数据变化的时候,更新视图
    update() {
        let newValue = this.vm[this.key]
        if (this.oldValue === newValue) {
            return
        }
        this.cb(newValue)
    }
}


五、测试

<div id="app">
    <div class="box">
        <h3>{{msg}}</h3>
        <h3>{{count}}</h3>
        <h3>{{name}}</h3>
        <input type="text" v-model="name">
        <button v-click="changeNameUpdater">点击</button>
    </div>
</div>

<script src="./dep.js"></script>
<script src="./watcher.js"></script>
<script src="./compiler.js"></script>
<script src="./observer.js"></script>
<script src="./vue.js"></script>
<script>
    let vm = new Vue({
        el: '#app',
        data: {
            msg: '你好 vue',
            count: 1,
            name: '张三'
        },
        methods: {
            changeNameUpdater() {
                console.log(this.name)
            }
        }
    })
    console.log(vm)
</script>

测试例子

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值