Bootstrap

从设计模式理解Vue响应式(多图警告)

序言

近日公司开发一个拖拽表单项目,用到了 Vue,部门老大便开始研读 Vue 源码,并且传授给我们,老大说,读源码不能仅仅只看懂源码,还得读懂他的设计思想,他为什么要这么设计,把自己当做设计者来读,这样才能真正理解,本文中,我会按照老大的指引方向,和自己的理解,来谈一谈 Vue 响应式原理,及其设计思想

什么是Vue响应式

官方解释: Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新 ,简单说就是数据发生改变视图会做出相应的更新,视图发生变化,例如 input 输入,数据也会做出对应的变化。

我们来看一个实例图:图片来源于阿宝哥的响应式原理。

这是一个Excel表格的加法表达式,我们会发现当加数发生改变,他的和就会会自动发生改变,不需要人为操作让他重新运算结果。在Vue中数据和视图的关系,就像这里的加数与和的关系。

那么Vue响应式是如何实现的呢,想要知道的话我们就得了解一下 他的设计模式了。

响应式的设计模式

Vue 响应式所使用的设计模式,是观察者模式,观察者模式通俗的说就是。

例如:郭老师每天上班带一个橙子,我想在他吃橙子的时候蹭一口,但是我又不想一直盯着郭老师看他什么时候吃,于是我和郭老师约定,你吃橙子的时候通知我。然后小郭老师看到了,也想要吃,就也和郭老师约定,郭老师吃橙子的时候也通知她,然后等郭老师吃橙子的时候,想起和他做好约定的我和小郭老师,于是发送通知,我和小郭老师收到消息,立马就做出我想要做的事情(凑过去和他一起吃)。

在编程中,这样做可以省去了反复检索的资源消耗,也得到更高的反馈速度。

联系方式:

响应式和观察者模式的融合

我们先来看一下实现一个基础的Vue需要哪些文件,我们以尤大的作为探讨的demo。

我们先来一一介绍一下这些文件分别做了什么功能;

我们看到,除了事件总线文件,和处理文件,就剩下3个文件了,这三个文件就是Vue响应式的根本了,我们看个关系图

小结

我们可以发现dep文件和watcher文件,就是一个观察者模式的实现,然后observe文件是他们之间的桥梁,通过劫持get和set操作,告诉dep什么时候应该添加观察者,和通知观察者,形成了自动化,当数据被读取,创造观察者和被观察者的联系,当数据改变,通知被观察者发送通知消息,如此一来就实现了响应式。

Vue实例初始化

响应式三步走源码

class Vue {
    constructor (options) {
    
        this.$options = options || {} // save options
        this.$el = typeof options.el === 'string' ? document.querySelector(options.el)
        :options.el // get dom
        this.$data = options.data // get data
        this.$methods = options.methods
        // 1.data 所有数据进行劫持代理
        this._proxyData(this.$data)
        // 2.调用observe对象,监听数据变化
        new Observer(this.$data)
        // 3.调用compiler对象,解析指令和差值表达式
        new Compiler(this)
    }
    _proxyData (data) {
        // 遍历所有data
        Object.keys(data).forEach(key => {
            // 将每一个data通过defineProperty进行劫持
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get () {
                    return data[key]
                },
                set (newValue) {
                    if (data[key] === newValue) {
                        return
                    }
                    data[key] = newValue
                }
            })
        })
    }
}


class Observer {
    constructor(data) {
        this.walk(data)
    }
    walk (data) { // 循环执行data
        if (!data || typeof data !== 'object') {
            return
        }
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
        })
    }
    defineReactive (obj, key, val) { 
        let that = this
        this.walk(val) // 如果val是对象,则给他绑定get和set时触发的方法
        let dep = new Dep() // 负责收集依赖,并发送通知
        Object.defineProperty(obj, key, {
            configurable: true,
            enumerable: true,
            get() {
                Dep.target && dep.addSub(Dep.target) // 收集依赖
                return val // 如果使用obj[key],会变成死循环
            },
            set(newValue) {
                if (newValue === val) {
                    return
                }
                val = newValue
                that.walk(newValue) // 修改后可能是对象,set函数内部调用,修改了this指向
                dep.notify() // 发送通知
            }
        })
    }
}

小结:这个文件是响应式自动化的实现,使用了一个 walk 方法,通过递归遍历 data 中的每一个属性,然后在放进 defineReactive 中给每个对象创建一个新的 Dep ,用于存储自身的依赖(观察者),然后将对象的可枚举和可写属性打开,并且定义一个 set 和 get 触发时的方法。 如果是触发的 get ,就把他的 Dep.target 添加到 Dep 列表,这一步也就是收集依赖,你取了我的说明你对我感兴趣,所以我把你添加进我的观察者列表。如果触发 set ,说明数据发生改变,触发 dep 中的 notify ,通知所有观察者,有数据更新了,快行动,以此达成响应式。

class Watcher {
    constructor (vm, key, cb) {
        this.vm = vm
        // data中的属性名称
        this.key = key
        // 回调函数负责更新视图
        this.cb = cb
        // 把watcher对象记录到Dep类的静态属性target
        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)
    }
}

我们最后来看一下初始化流程图

按照箭头得知:首先初始化阶段做了三个步骤:

小结:这三个步骤是 Vue 响应式的核心,也是观察者模式的实现,Watcher(观察者)通过触发 get ,将自身添加进观察目标的观察者列表。Dep 通过遍历自身观察者列表实现通知所有观察者,从而实现响应式。

视图渲染和视图更新

我们来看看 compiler.js 文件的主要内容

// 解析 v-model
    modelUpdater (node, value, key) {
        node.value = value
        new Watcher(this.vm, key, (newValue) => { // 创建watcher对象,当数据改变更新视图
            node.value = newValue
        })
        // 双向绑定
        node.addEventListener('input', () => {
            this.vm[key] = node.value
        })
    } 
// 编译模板
    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)
            }
            // 如果还有子节点,递归调用
            if (node.childNodes && node.childNodes.length > 0) {
                this.compile(node)
            }
        })
    }
    // 编译元素节点,处理指令
    compileElement (node) {
        // console.log(node.attributes)
        if (node.attributes.length) {
            Array.from(node.attributes).forEach(attr => { // 遍历所有元素节点
                let attrName = attr.name
                if (this.isDirective(attrName)) { // 判断是否是指令
                    attrName = attrName.indexOf(':') > -1 ? attrName.substr(5) : attrName.substr(2) 
                  // 获取 v- 后面的值
                    let key = attr.value // 获取data名称
                    this.update(node, key, attrName)
                }
            })
        }
    }
 // 编译文本节点,处理差值表达式
    compileText (node) {
        // 获取 {{  }} 中的值
        // console.dir(node) // console.dir => 转成对象形式
        let reg = /\{\{(.+?)\}\}/
        let value = node.textContent
        if (reg.test(value)) {
            let key = RegExp.$1.trim() // 返回匹配到的第一个字符串,去掉空格
            node.textContent = value.replace(reg, this.vm[key])
            new Watcher(this.vm, key, (newValue) => { // 创建watcher对象,当数据改变更新视图
                node.textContent = newValue
            })
        }
    }

我们再看一幅流程图

小结:compile 把元素转换为数据模型,他是普通的 JavaScript 对象,我们这叫做 vnode 对象,然后遍历 vnode 对象,根据标识分为元素节点,文本节点,数据三个分类,分别进入不同的处理函数,并且创建一个 Watcher 对象,然后在 Watcher 对象中触发 get 实现响应式,同步会进行 updata 更新数据,转换成真实 dom ,完成页面渲染,更新就是如此反复。

整体响应式运行流程图

最后,我们根据这张流程图进行一下知识回顾。首先是初始化三步走:

然后开始渲染阶段

至此, Vue响应式原理及其设计模式应该很清楚啦,如有疑问欢迎留言提出。

欢迎想要一起学习进步的朋友,加入我的学习群,大家可以在里面讨论一下进阶技巧,分享自己最新学习的内容!

如果觉得文章不错,可以点个赞,给作者一点小小的鼓励,谢谢,可以选择加我微信好友,我来拉你们进群。