Vue 响应式原理

1,756 阅读13分钟

VUE 响应式原理.png 在讲 Vue 响应式原理之前,我们需要熟悉数组当中的 reduce 方法,并且可以巧妙的运用 reduce() 方法来解决问题的需要,为下面理解 Vue 响应式原理先提前热热脑子。下面我们先来介绍一下 reduce() 方法。

reduce() 方法

reduce()方法:会循环当前的数组,下一次操作依赖上一次的返回值,就相当于进行“滚雪球”的操作。

reduce((上次计算的结果,当前循环的item项) => {
    return 上次结果 + 当前循环item项
    },初始值)

应用场景:下次操作的初始值,依赖于上次操作的返回值。

实现数值的累加操作

当我们实现一个数值的累加的时候,一般会想到的都会是循环遍历使用 forEach() 方法进行求值。

const array = [1, 2, 3, 4, 5]
let sum = 0
array.forEach(item => {
    sum += item
})
console.log(sum) // 15

上面的累加求和中,编写的代码行数太多了,还需要声明一个外部变量来存储数值。看起来比较啰嗦和麻烦。但是我们可以使用 reduce()方法快速的实现数值的累加操作。

const array = [1, 2, 3, 4, 5]
const sum =  array.reduce((pre, next) =>  pre + next,0)
console.log(sum) // 15

上面两种方法中均实现可以累加求和,但是 reduce()方法比 forEach()方法看起来更加的简洁。

实现对象链式取值操作

reduce()方法的强大之处不仅能实现数值的累加操作,还可以实现对象链式取值的操作等等。

const obj = {
    name: 'alex',
    info: {
        address: {
            location: 'gz'
        }
    }
}
const array = ['info', 'address', 'location']
const value = array.reduce((pre, next) => pre[next], obj)
console.log(value) // gz

链式获取对象属性值升级操作

const obj = {
    name: 'alex',
    info: {
        address: {
            location: 'gz'
        }
    }
}
const objInfo = 'info.address.loaction'
const value =objInfo.split('.').reduce((pre, next) => pre[next], obj)
console.log(value) // gz

好了,通过上面 reduce()方法的使用,相信你已经进入状态了,现在正式进入主题。

发布订阅模式

举个简单的例子:现有一家商店供应百事可乐汽水,此时来了A、B、C三个人,都是来买百事可乐的,但是这家商店的百事可乐卖完了,此时店长就拿出一本子分别来记录他们的联系方式,等到百事可乐到货了,再拿出本子一一对应的通知它们三个人来取货,然后A、B、C就分别拿着百事可乐去做另外的事情、。这个例子中,就可以将A、B、C三个人理解为 Watcher 订阅者,商店理解为一个 Dep,负责进行依赖的收集,并且通知 WatcherWatcher 收到通知之后就做相关的事情(比如重新渲染页面,数据驱动视图的更新)。

通过上面简单的例子,我们也知道了Dep类里面应该具备以下功能:

  • 负责进行依赖收集。
  • 首先,有个数组,专门用来存放所有的订阅信息。
  • 其次,还要提供一个向数组中追加订阅的addSub()方法。
  • 最后,还要提供一个循环,循环触发(通知)数组中每个订阅信息。 Watcher类:负责订阅一些事件,主要是一个回调函数

Vue 响应式原理

通过上面的例子,我们就很好的理解了什么是发布订阅模式了,也了解了Dep类和Watcher类都有什么作用。下面我们进行深入研究一下 Vue 响应式的原理。
Vue 响应式原理最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的,这个方法是本文中最重要、最基础的内容之一。

要实现Vue的双向数据绑定,就必须要实现以下几点:

  1. 实现一个 Dep,主要用来收集依赖(订阅者),通知对应的订阅者更新数据。
  2. 实现一个数据监听器 Observer,能够对数据对象的所有属性都进行监听,都加上setter、和 getter 方法,如有变动可拿到最新值通知依赖收集对象(Dep)并通知订阅者(Watcher)来更新视图变化。
  3. 实现一个解析器 Compile,对每一个元素节点的指令进行扫描和解析,根据指令替换数据,以及绑定相应的更新函数。
  4. 实现一个 Watcher,作为连接 ObserverCompile 的桥梁,能够订阅并接收到每个属性变动的通知,执行相应的回调函数,从而更新视图。

image.png

实现解析器 Compile

实现一个解析器 Compile,对每一个元素节点的指令进行扫描和解析,根据指令替换数据,以及绑定相应的更新函数。

初始化

class MVue {
    constructor (options) {
        this.$el = options.el,
        this.$data = options.data,
        this.$options = options
        // 如果存在template模板则开始编译
        if (this.$el) {
            // 创建解析器 Compile
            new Compile(this.$el, this)
        }
    }
}
class Compile {
    constructor (el, vm) {
        this.vm = vm
        // 判断是否是一个元素节点 如果是直接赋值 不是则获取值
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
    }
    isElementNode (node) {
        // node.nodeType 等于1 是元素节点 等于3是文本节点
        return node.nodeType === 1
    }
}

创建文档碎片

因为每次匹配到进行替换时,会导致页面的回流和重绘,影响页面的的性能,所以需要创建文档碎片来进行缓存,减少页面的回流和重绘。

class MVue {
    constructor (options) {
        this.$el = options.el,
        this.$data = options.data,
        this.$options = options
        // 如果存在template模板则开始编译
        if (this.$el) {
            // 创建解析器 Compile
            new Compile(this.$el, this)
        }
    }
}
class Compile {
    constructor (el, vm) {
        this.vm = vm
        // 判断是否是一个元素节点 如果是直接赋值 不是则获取值
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
        // 因为每次匹配到进行替换时,会导致页面的回流和重绘,影响页面的的性能
        // 所以需要创建文档碎片来进行缓存,减少页面的回流和重绘
        console.log(this.el);
        const framgent = this.createFramgent(this.el)
        // 再将文档碎片添加到根元素中然后渲染到页面
        this.el.appendChild(framgent)
    }
    // 创建文档碎片
    createFramgent (node) {
        const framgent = document.createDocumentFragment(node)
        // 循环依次将节点添加到文档碎片中 firstChild 包含空格换行符
        // console.log(node.firstChild);
        let children
        while (children = node.firstChild) {
            // 依次追加当文档碎片中
            framgent.appendChild(children)
        }
        return framgent
    }
    isElementNode (node) {
        // node.nodeType 等于1 是元素节点 等于3是文本节点
        return node.nodeType === 1
    }
}

递归编译模板

class MVue {
    constructor (options) {
        this.$el = options.el,
        this.$data = options.data,
        this.$options = options
        // 如果存在template模板则开始编译
        if (this.$el) {
            // 创建解析器 Compile
            new Compile(this.$el, this)
        }
    }
}
class Compile {
    constructor (el, vm) {
        this.vm = vm
        // 判断是否是一个元素节点 如果是直接赋值 不是则获取值
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
        // 因为每次匹配到进行替换时,会导致页面的回流和重绘,影响页面的的性能
        // 所以需要创建文档碎片来进行缓存,减少页面的回流和重绘
        console.log(this.el);
        const framgent = this.createFramgent(this.el)

        // 开始进行模板的编译
        this.compile(framgent)

        // 再将文档碎片添加到根元素中然后渲染到页面
        this.el.appendChild(framgent)
    }
    compile (framgent) {
        const childNodes = framgent.childNodes
        console.log(childNodes)
        // 遍历全部的节点并判断是元素节点还是文本节点
        // 将伪数组转为真数组
        const childNodesArray = Array.from(childNodes)
        childNodesArray.forEach(node => {
            if(this.isElementNode(node)){
                // 是元素节点
                console.log(node);
            } else {
                //是文本节点
                console.log(node);
            }
            // 多层嵌套需要递归 子元素
            if(node.childNodes && node.childNodes.length){
                this.compile(node)
            }
        })
    }
    // 创建文档碎片
    createFramgent (node) {
        const framgent = document.createDocumentFragment(node)
        // 循环依次将节点添加到文档碎片中 firstChild 包含空格换行符
        // console.log(node.firstChild);
        let children
        while (children = node.firstChild) {
            // 依次追加当文档碎片中
            framgent.appendChild(children)
        }
        return framgent
    }
    isElementNode (node) {
        // node.nodeType 等于1 是元素节点 等于3是文本节点
        return node.nodeType === 1
    }
}

解析编译元素

class MVue {
    constructor(options) {
        this.$el = options.el,
            this.$data = options.data,
            this.$options = options
        // 如果存在template模板则开始编译
        if (this.$el) {
            // 创建解析器 Compile
            new Compile(this.$el, this)
        }
    }
}
class Compile {
    constructor (el, vm) {
        this.vm = vm
        // 判断是否是一个元素节点 如果是直接赋值 不是则获取值
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
        // 因为每次匹配到进行替换是,会导致页面的回流和重绘,影响页面的的性能
        // 所以需要创建文档碎片来进行缓存,减少页面的回流和重绘
        console.log(this.el);
        const framgent = this.createFramgent(this.el)

        // 开始进行模板的编译
        this.compile(framgent)

        // 再将文档碎片添加到根元素中然后渲染到页面
        this.el.appendChild(framgent)
    }
    compile (framgent) {
        const childNodes = framgent.childNodes
        // console.log(childNodes)
        // 遍历全部的节点并判断是元素节点还是文本节点
        // 将伪数组转为真数组
        const childNodesArray = Array.from(childNodes)
        childNodesArray.forEach(node => {
            if(this.isElementNode(node)){
                // 是元素节点
                // console.log(node);
                this.compileElement(node)
            } else {
                //是文本节点
                // console.log(node);
                this.compileText(node)
            }
            // 多层嵌套需要递归 子元素
            if(node.childNodes && node.childNodes.length){
                this.compile(node)
            }
        })
    }
    // 解析编译元素节点
    compileElement (elementNode) {
        // 编译元素 通过attributes获取元素节点的属性 里面包含name 和 value name为属性名字 value为属性值
        const attributes = elementNode.attributes;
        [...attributes].forEach(attr => {
            // name 属性名 v-text v-html  value 属性值 obj.name obj.age
            const {name, value} = attr 
            if (this.isDirective(name)) {
                // 是指令
                // 解构 v-text v-html
                const [,directive] = name.split('-')
                const [dirName, eventName] = directive.split(':')
                // 在compileUtils对象黎曼是否存这个指令 根据不同的指令处理不同的数据 text html model
                compileUtils[dirName] && compileUtils[dirName](elementNode, value, this.vm, eventName)
                // 依次标签中的属性
                elementNode.removeAttribute('v-' + directive)
            } else if (this.isEventName(name)) {
                // 是事件
                const [,eventName] = name.split('@')
                // 根据不同的指令处理不同的数据 text html model
                compileUtils['on'](elementNode, value, this.vm, eventName)
            }
        });
    }
    // 是否是指令
    isDirective (name) {
        // 以v-开头
        return name.startsWith('v-')
    }
    // 是否是事件
    isEventName (name) {
        // 以@开头
        return name.startsWith('@')
    }
    // 解析编译文本节点
    compileText (textNode) {
        // 编译文本
    }
    // 创建文档碎片
    createFramgent (node) {
        const framgent = document.createDocumentFragment(node)
        // 循环依次将节点添加到文档碎片中 firstChild 包含空格换行符
        // console.log(node.firstChild);
        let children
        while (children = node.firstChild) {
            // 依次追加当文档碎片中
            framgent.appendChild(children)
        }
        return framgent
    }
    isElementNode (node) {
        // node.nodeType 等于1 是元素节点 等于3是文本节点
        return node.nodeType === 1
    }
}

解析编译文本

// 解析编译文本节点
    compileText (textNode) {
        // 编译文本
        // 获取文本内容
        const content = textNode.textContent
        // 正则匹配
        const reg = /\{\{(.+?)\}\}/
        if(reg.test(content)) {
        // 根据不同的指令处理不同的数据 text html model
            compileUtils['text'](textNode, content, this.vm)
        }
    }

看到这里你可能会有点疑惑,compileUtils 是一个什么东西,用来处理什么的,compileUtils 是一个对象,主要是对不同的指令来做不同的处理,比如 v-text 是处理文本,v-html 是处理 html元素,v-model 是处理表单数据的....
compileUtils 对象里面有一个 updater 对象里面有对应的方法主要来更新视图的。

compileUtils 对象

const compileUtils = {
    // 获取data中属性值值
    getValue (value, vm) {
        // 先以.分割为一个数组,然后使用reduce获取data中的属性值
        return value.split('.').reduce((pre, next) => {
            return pre[next]
        },vm.$data)
    },  
    text (node , value, vm) { // value可能是{{obj.name}} 可能是 obj.age
        let val
        if (value.indexOf('{{') !== -1) {
            // 有{{ 说名是 {{obj.name}}
            // 进行全局匹配
            val = value.replace(/\{\{(.+?)\}\}/g, (...args) => {
                return this.getValue(args[1], vm)
            })
        } else {
            // obj.age
            val =  this.getValue(value, vm)
        }
        // 更新/替换数据
        this.updater.textUpdata(node, val)
    },
    html (node, value, vm) {
        const val = this.getValue(value, vm)
        // 更新/替换数据
        this.updater.htmlUpdata(node,val)
    },
    model (node, value ,vm) {
        const val = this.getValue(value, vm)
        this.updater.modleUpdata(node, val)
    },
    on (node, value, vm, eventName) {
        // 获取回调函数
        let fn = vm.$options.methods && vm.$options.methods[value]
        node.addEventListener(eventName, fn.bind(vm), false)
    },
    // 里面存在对应指令的方法 用来更新视图
    updater:{
        textUpdata (node, value) {
            node.textContent = value
        },
        htmlUpdata (node, value) {
            node.innerHTML = value
        },
        modleUpdata (node, value) {
            node.value = value
        },
    }
}

image.png
根据上面的流程图,我们已经完成了 new MVVM() -> Compile -> Updater 的步骤,实现了初始化页面视图数据的展示。下面我们继续完成从 Observer -> Dep -> Watcher -> Updater。

实现一个监听器 Observer

Observer 的作用就是用来对对象中的每个属性进行数据的劫持监听设置 setter、getter 方法,如有变动,可以在setter 方法中拿到最新的值调用 依赖收集对象(Dep) 中的 notify() 通知订阅者。

我们可以利用 Object.defineProperty()来监听属性变动,通过 observe方法 将对象的数据进行递归遍历,包括子属性对象的属性,都加上 settergetter,当我们给某个对象赋值或者是获取,就会触发 setter、getter方法,就能够监听到数据的变化了。

class Observer{
    constructor (data) {
        this.observe(data)
    }
    observe (data) {
        // 不考虑数组 存在data 并且data为一个对象
        if (data && typeof data === 'object'){
            // 遍历对象的key
            Object.keys(data).forEach(key => {
                // 将对象 键传进入 值传进去
                this.defineReactive(data, key, data[key])
            })
        }
    }
    defineReactive (obj, key, value) {
        // 递归遍历
        this.observe(value)
        Object.defineProperty(obj, key, {
            configurable: false,
            enumerable: true,
            get () {
                return value
            },
            set: (newValue) => {
                if ( newValue != value) {
                    // 如果直接赋值对象 也需要对这个对象进行数据监听
                    this.observe(newValue)
                    value = newValue
                    // dep.notify 通知watcher变化 更新视图
                    dep.notify()
                }
            }
        })
    }
}

依赖收集对象 Dep

通过上面的 Observer 我们可以对每个对象属性进行了数据的监听,并且当数据变化的时候通过 dep 的 notify方法 通知订阅者(Watcher)执行回调函数变更视图。

Dep作用:

  • 创建一个数组存放订阅者,声明添加订阅者的方法(addSub)
  • 声明一个通知订阅之的方法(notify)
class Dep {
    constructor () {
        // 存放watcher
        this.sub = []
    }
    // 添加订阅者
    addSub (watcher) {
        // 将订阅者存放到数组中
        this.sub.push(watcher)
    }
    // 通知订阅者
    notify () {
        // 循环遍历 依次触发watcher绑定的回调函数
        this.sub.forEach(watcher => watcher.update())
    }

}

Watcher 订阅者

Watcher 作为连接 ObserverCompile 之间的桥梁,能够订阅并且收到每个属性变动的通知,执行绑定的相应的回调函数,从而更新视图。
Watcher 必须具备以下三点:

  1. 在自身实例化往Dep里面添加自己
  2. 自身必须要有一个 update()方法
  3. 待属性值变动 dep.notify()通知时,能够调用自身的 update()方法,并触发Compile中绑定的回调函数
class Watcher{
    constructor (vm, value, callback) {
        // vm 里面存放着最新值
        // value 数据变化的属性
        // 绑定的回调函数
        this.vm = vm
        this.value = value
        this.callback = callback
        // 存放旧值
        this.oldValue = this.getOldValue()
    }
    // 获取值 这里会间接触发 getter
    getOldValue () {
        Dep.target = this
        const olaValue = compileUtils.getValue(this.value, this.vm)
        Dep.target = null
        return olaValue
    }
    update () {
        // 更新操作 数据变化后 Dep会通过notify方法通知订阅者 然后订阅者更新视图
        const newValue = compileUtils.getValue(this.value, this.vm)
        if (newValue !== this.oldValue) {
            this.callback(newValue)
            this.oldValue = newValue
        }
    }
}

到此为止,我们已经编写好了所需要的 Observer、Watcher、Compiler、Dep。那么,剩下的就是应该如何把它们关联起来,形成一个回路,这才是我们实现响应式的目的。 当我们一打开页面时,首先执行的是 Observer 先将对象的每个属性进行数据的监听和劫持,然后再使用 Compile解析器 进行解析,第一大步:可以确定的是先 new Observernew Compile

class MVue {
    constructor(options) {
        this.$el = options.el,
            this.$data = options.data,
            this.$options = options
        // 如果存在template模板则开始编译
        if (this.$el) {
            // 1、对数据进行劫持
            new Observer(this.$data)
            // 2、创建解析器 Compile
            new Compile(this.$el, this)
        }
    }
}

一开始就进行数据的监听,这里面会进行递归监听子属性的对象属性,在数据修改的时候触发 dep.notify方法,所以我们可以确定的是在递归结束之后 Object.defineProperty 之前 new Dep 来收集依赖和触发notify,并且我们可以在 getter 方法中触发 dep.addSub 方法Watcher 实例 添加到数组中,这这样我们就收集了依赖对象。

class Observer{
    constructor (data) {
        this.observe(data)
    }
    observe (data) {
        // 不考虑数组 存在data 并且data为一个对象
        if (data && typeof data === 'object'){
            // 遍历对象的key
            Object.keys(data).forEach(key => {
                // 将对象 键传进入 值传进去
                this.defineReactive(data, key, data[key])
            })
        }
    }
    defineReactive (obj, key, value) {
        // 递归遍历
        this.observe(value)
        const dep = new Dep()
        Object.defineProperty(obj, key, {
            configurable: false,
            enumerable: true,
            get () {
                // 如果存在订阅者 往收集依赖对象数组中添加订阅者
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set: (newValue) => {
                if ( newValue != value) {
                    // 如果直接赋值对象 也需要对这个对象进行数据监听
                    this.observe(newValue)
                    value = newValue
                    // dep.notify 通知watcher变化 更新视图
                    dep.notify()
                }
            }
        })
    }
}

最后,就只剩下 ComplieWatcher 应该如何进行关联了,在 Compile 中,我们是通过声明一个 compileUtils对象 来对不同的指令进行不同的处理,在这个对象里面,我们声明了一个 updater对象,里面包含着对各种指令的数据更新,比如textUpdate、htmlUpdate、modelUpdate 等等操作,所以,我们找到了更新数据的函数,可以确定的是在更新函数之前 new Watcher,当数据变化的时候触发订阅者实例(Watcher)来触发绑定的回调函数,从而更新视图。

const compileUtils = {
    // 获取data中属性值值
    getValue (value, vm) {
        // 先以.分割为一个数组,然后使用reduce获取data中的属性值
        return value.split('.').reduce((pre, next) => {
            return pre[next]
        },vm.$data)
    },  
    // {{obj.name}}---{{obj.age}} 重新获取值,避免修改一个同时两个都变 重复渲染
    getContentValue (value, vm) {
        return value.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getValue(args[1], vm);
        })
    },
    text (node , value, vm) { // value可能是{{obj.name}} 可能是 obj.age
        let val
        if (value.indexOf('{{') !== -1) {
            // 有{{ 说名是 {{obj.name}}
            // 进行全局匹配
            val = value.replace(/\{\{(.+?)\}\}/g, (...args) => {
                // ...args 打印出的三个分别是 当前匹配的值,匹配项在字符串中最小的为止,原始字符串
                new Watcher(vm, args[1], () => {
                    this.updater.textUpdata(node, this.getContentValue(value, vm))
                })
                return this.getValue(args[1], vm)
            })
        } else {
            // obj.age
            val =  this.getValue(value, vm)
        }
        // 更新/替换数据
        this.updater.textUpdata(node, val)
    },
    html (node, value, vm) {
        const val = this.getValue(value, vm)
        new Watcher(vm, value, (newValue) => {
            this.updater.htmlUpdata(node, newValue)
        })
        // 更新/替换数据
        this.updater.htmlUpdata(node,val)
    },
    model (node, value ,vm) {
        const val = this.getValue(value, vm)
        new Watcher(vm, value, (newValue) => {
            this.updater.modleUpdata(node, newValue)
        })
        this.updater.modleUpdata(node, val)
    },
    on (node, value, vm, eventName) {
        // 获取回调函数
        let fn = vm.$options.methods && vm.$options.methods[value]
        node.addEventListener(eventName, fn.bind(vm), false)
    },
    updater:{
        textUpdata (node, value) {
            node.textContent = value
        },
        htmlUpdata (node, value) {
            node.innerHTML = value
        },
        modleUpdata (node, value) {
            node.value = value
        },
    }
}

image.png
经过上面分析,我们已经完成了数据变化到视图的更新,但是在表单中,还没实现视图变化到数据的更新,所以,我们还需要对表单数据进行分析。

/ 根据不同的指令处理不同的数据 text html model
const compileUtils = {
    // 获取data中属性值值
    getValue (value, vm) {
        // 先以.分割为一个数组,然后使用reduce获取data中的属性值
        return value.split('.').reduce((pre, next) => {
            return pre[next]
        },vm.$data)
    },  
    getContentValue (value, vm) {
        return value.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getValue(args[1], vm);
        })
    },
    // 视图 -> 数据
    setValue (vm,value,inputValue) {
        
        return value.split('.').reduce((pre, next) => {
            if (typeof pre[next] !== 'object') {
                // 如果不是是一个对象直接赋值   
                pre[next] = inputValue
            } 
            // 是对象 直接取值
            return pre[next]
        },vm.$data)
    },
    text (node , value, vm) { // value可能是{{obj.name}} 可能是 obj.age
        let val
        if (value.indexOf('{{') !== -1) {
            // 有{{ 说名是 {{obj.name}}
            // 进行全局匹配
            val = value.replace(/\{\{(.+?)\}\}/g, (...args) => {
                // ...args 打印出的三个分别是 当前匹配的值,匹配项在字符串中最小的为止,原始字符串
                new Watcher(vm, args[1], () => {
                    this.updater.textUpdata(node, this.getContentValue(value, vm))
                })
                return this.getValue(args[1], vm)
            })
        } else {
            // obj.age
            val =  this.getValue(value, vm)
        }
        // 更新/替换数据
        this.updater.textUpdata(node, val)
    },
    html (node, value, vm) {
        const val = this.getValue(value, vm)
        new Watcher(vm, value, (newValue) => {
            this.updater.htmlUpdata(node, newValue)
        })
        // 更新/替换数据
        this.updater.htmlUpdata(node,val)
    },
    model (node, value ,vm) {
        const val = this.getValue(value, vm)
        // 数据更新 -> 视图变化
        new Watcher(vm, value, (newValue) => {
            this.updater.modleUpdata(node, newValue)
        })
        // 视图变化 -> 数据更新
        node.addEventListener('input',(e) => {
            this.setValue(vm, value, e.target.value)
        } ,false)
        this.updater.modleUpdata(node, val)
    },
    on (node, value, vm, eventName) {
        // 获取回调函数
        let fn = vm.$options.methods && vm.$options.methods[value]
        node.addEventListener(eventName, fn.bind(vm), false)
    },
    updater:{
        textUpdata (node, value) {
            node.textContent = value
        },
        htmlUpdata (node, value) {
            node.innerHTML = value
        },
        modleUpdata (node, value) {
            node.value = value
        },
    }
}
class Compile {
    constructor (el, vm) {
        this.vm = vm
        // 判断是否是一个元素节点 如果是直接赋值 不是则获取值
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
        // 因为每次匹配到进行替换是,会导致页面的回流和重绘,影响页面的的性能
        // 所以需要创建文档碎片来进行缓存,减少页面的回流和重绘
        console.log(this.el);
        let framgent = this.createFramgent(this.el)

        // 开始进行模板的编译
        this.compile(framgent)

        // 再将文档碎片添加到根元素中然后渲染到页面
        this.el.appendChild(framgent)
    }
    compile (framgent) {
        const nodes = framgent.childNodes;
        // console.log(childNodes)
        // 遍历全部的节点并判断是元素节点还是文本节点
        // 将伪数组转为真数组
        // const childNodesArray = Array.from(childNodes)
        [...nodes].forEach(node => {
            if(this.isElementNode(node)){
                // 是元素节点
                // console.log(node);
                this.compileElement(node)
            } else {
                //是文本节点
                // console.log(node);
                this.compileText(node)
            }
            // 多层嵌套需要递归 子元素
            if(node.childNodes && node.childNodes.length){
                this.compile(node)
            }
        })
    }
    // 解析编译元素节点
    compileElement (elementNode) {
        // 编译元素 通过attributes获取元素节点的属性 里面包含name 和 value name为属性名字 value为属性值
        const attributes = elementNode.attributes;
        [...attributes].forEach(attr => {
            // name 属性名 v-text v-html  value 属性值 obj.name obj.age
            const {name, value} = attr 
            if (this.isDirective(name)) {
                // 是指令
                // 解构 v-text v-html
                const [,directive] = name.split('-')
                const [dirName, eventName] = directive.split(':')
                // 是否存在这个指令对应的函数
                compileUtils[dirName] && compileUtils[dirName](elementNode, value, this.vm, eventName)
                // 依次标签中的属性
                elementNode.removeAttribute('v-' + directive)
            } else if (this.isEventName(name)) {
                // 是事件
                const [,eventName] = name.split('@')
                compileUtils['on'](elementNode, value, this.vm, eventName)
            }
        });
    }
    // 是否是指令
    isDirective (name) {
        // 以v-开头
        return name.startsWith('v-')
    }
    // 是否是事件
    isEventName (name) {
        // 以@开头
        return name.startsWith('@')
    }
    // 解析编译文本节点
    compileText (textNode) {
        // 编译文本
        // 获取文本内容
        const content = textNode.textContent
        // 正则匹配
        const reg = /\{\{(.+?)\}\}/
        if(reg.test(content)) {
            compileUtils['text'](textNode, content, this.vm)
        }
    }
    // 创建文档碎片
    createFramgent (node) {
        const framgent = document.createDocumentFragment(node)
        // 循环依次将节点添加到文档碎片中 firstChild 包含空格换行符
        // console.log(node.firstChild);
        let children
        while (children = node.firstChild) {
            // 依次追加当文档碎片中
            framgent.appendChild(children)
        }
        return framgent
    }
    isElementNode (node) {
        // node.nodeType 等于1 是元素节点 等于3是文本节点
        return node.nodeType === 1
    }
}
class MVue {
    constructor(options) {
        this.$el = options.el,
            this.$data = options.data,
            this.$options = options
        // 如果存在template模板则开始编译
        if (this.$el) {
            // 1、对数据进行劫持
            new Observer(this.$data)
            // 2、创建解析器 Compile
            new Compile(this.$el, this)
        }
    }
}

数据代理 proxy

在vue中我们可以直接使用vm.msg获取到数据,其实是内部帮我们进行了数据的代理,相当于vm.$data.msg,所以我们也需要进行数据的代理。

class MVue {
    constructor(options) {
        this.$el = options.el,
            this.$data = options.data,
            this.$options = options
        // 如果存在template模板则开始编译
        if (this.$el) {
            // 1、对数据进行劫持
            new Observer(this.$data)
            // 数据代理
            this.proxy(this.$data)
            // 2、创建解析器 Compile
            new Compile(this.$el, this)
        }
    }
    // 数据代理
    proxy(data) {
        for (const key in data) {
            Object.defineProperty(this, key, {
                get () {
                    return data[key]
                },
                set (newValue) {
                    data[key] = newValue
                }
            })
        }
    }
}

image.png

梳理Vue响应式全过程

  1. 初始化 Vue实例 时,Observer 会遍历 data 中的所有属性,使用 Object.defineProperty()方法 将这些属性都转为 getter/setter。并且创建依赖收集对象 dep(一个属性一个Dep实例,用来管理该属性下的所有 Watcher,如果同一个属性在 DOM 节点中多次使用会创建多个 Watcher)。
  2. 在解析指令的时候,创建 Watcher实例,然后将更新的函数放到 Watcher实例 的回调上
  3. 在初始化视图的时候,会读取属性值,触发 getter,将创建 Watcher实例 添加到 dep 数组中
  4. 当修改数据的时候,触发 setter,调用 dep.notify方法,通知该 dep 内部的所有 Wacther 执行回调函数,重新 render 当前组件,生成新的虚拟 DOM树。
  5. Vue 框架会使用 diff算法 遍历并对比新虚拟DOM树和旧虚拟DOM树中每个节点的差别,并记录下来,最后,加载操作,将所记录的不同点,局部修改到真DOM树上。

面试回答术语

谈谈你对 vue 的 MVVM 响应式原理的理解。

Vue 是采用数据劫持结合发布订阅模式的方式,通过 Object.defineProperty()来劫持各个属性的 getter,setter,在数据变动时发布消息给订阅者,然后触发相应的监听回调函数来更新视图。

需要 Observer 对数据进行递归遍历,包括子属对象的属性,都添加上 getter、setter,当读取值或者修改数据的时候,就会 触发getter 或 setter,就能够监听到数据变化。

Compile 解析指令,初始化页面将模板中的变量替换成数据,并将每个指令对应的节点绑定更新的回调函数,添加订阅者,一旦数据变动,订阅者收到通知,触发回调更新视图

WatcherObserverCompile 之间的桥梁,首先,需要在自身实例时往 dep 中添加自己,其次,要有一个 update方法 更新,最后,数据变动时触发 dep.notify方法,调用自身的 update方法,触发 Compile 中绑定的回调函数。

MVVM 作为数据绑定的入口,整合ObserverCompileWatcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析指令,最终利用Watcher 搭起 ObserverCompile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model更新的双向绑定效果。

image.png
以上是Vue响应式的全部内容。需要获取源码可以点下面链接。

获取源码戳我!!!