Vue.js(Vue2)响应式原理浅析

206 阅读2分钟

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

  具体步骤:

  第一步:需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化

// 观察者
class Observer {
    constructor(data) {
        this.observer(data)
    }
    observer(data) {
        // 判断:如果是对象才观察
        if (data && typeof data == 'object') {
            // 如果是对象
            for (let key in data) { // 循环遍历 $data , 让每一个属性都具有 set 和 get 方法
                this.definedReactive(data, key, data[key])
            }
        }
    }
    definedReactive(obj, key, value) {// Object.definedProperty 必须的参数:对象,属性名,操作属性
        this.observer(value)// 递归操作,遍历对象中的对象
        let dep = new Dep() // 给每一个属性,都加上具有发布订阅的功能
        Object.defineProperty(obj, key, {
            get() {
                // 创建 watcher 时 , 会取到对应的内容 , 并且把watcher 放到了全局上
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set: (newVal) => {
                if (newVal != value) {
                    this.observer(newVal)// 监控新值,保证对象更改属性也为对象时,新属性具有 get 和 set 方法
                    value = newVal
                    dep.notify()
                }
            }
        })
    }
}

  第二步:compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

// 基类 调度
class Compile {
    constructor(el, vm) {
        // 判断el 属性,是不是一个元素,如果不是元素,那就获取它
        this.vm = vm
        this.el = this.isELementNode(el) ? el : document.querySelector(el)
        // 把当前节点中的元素 获取到 然后放到内存中
        let fragment = this.nodeToFragment(this.el)
        // 把节点中的内容进行替换

        // 编译模板 用数据编译
        this.compileNode(fragment)
        // 把内容再塞回到页面中
        this.el.appendChild(fragment)

    }
    在这里添加判断和处理不同类型节点的函数... ...
  }

  第三步:Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:

class Dep {
    constructor() {
        this.subs = []; // 存放所有的 watcher
    }
    // 订阅
    addSub(watcher) { // 添加 watcher
        this.subs.push(watcher)
    }
    // 发布
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}
// 这里的每一个watcher都是单独的 类Watcher 的实例对象,发布者即修改对象属性时都会触发watcher的update方法 

  1、在自身实例化时往属性订阅器(dep)里面添加自己 即上面代码块中的 this.subs.push(watcher)

  2、自身必须有一个update()方法

update() { // 更新操作 , 数据变化后,会调用观察者的update方法
        let newVal = CompileUtil.getVal(this.vm, this.expr)
        if (newVal !== this.oldValue) {
            this.callback(newVal)
        }
    }

  3、待属性变动dep.notice()通知时,能调用自身的 update() 方法,并触发Compile中绑定的回调,则功成身退。 上文代码块中的 // 发布 notify() { this.subs.forEach(watcher => watcher.update()) }

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

class Vue {
    constructor(options) {
        this.$el = options.el
        this.$data = options.data
        let computed = options.computed
        let methods = options.methods
        // 如果这个根元素 存在 那么就编译模板
        if (this.$el) {

            // 把数据,全部转换成用 Object.definedProperty 来定义的
            new Observer(this.$data)

            for (let key in computed) { // 处理computed依赖关系
                Object.defineProperty(this.$data, key, {
                    get: () => {
                        return computed[key].call(this)
                    }
                })
            }

            for (let key in methods) {
                Object.defineProperty(this, key, {
                    get() {
                        return methods[key]
                    }
                })
            }

            // 把数据获取操作 vm 上的取值操作,都代理到 vm.$data
            this.proxyVm(this.$data)


            new Compile(this.$el, this)
        }
    }
    // 将 this.data 代理到 this
    proxyVm(data) {
        for (let key in data) {
            Object.defineProperty(this, key, {
                get() {
                    return data[key] // 进行了转化操作
                },
                set(newVal){ // 设置代理方法
                    data[key] = newVal
                }
            })
        }
    }
}

页面中构造 Vue 实例函数:

const vm = new Vue({
        el: '#App',
        // methods computed 等... ... 
    })

=======================分割线=================================== 这里是完整的代码,你可以直接copy下来体验一下,当然这个代码并不完整,没有对数组进行处理(Vue的源码一篇文章是放不下的...):

// 观察者 (发布-订阅) 观察者 被观察者

class Dep {
    constructor() {
        this.subs = []; // 存放所有的 watcher
    }
    // 订阅
    addSub(watcher) { // 添加 watcher
        this.subs.push(watcher)
    }
    // 发布
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}

class Watcher {
    constructor(vm, expr, callback) {// 实例, 表达式,回调函数
        this.vm = vm,
            this.expr = expr,
            this.callback = callback
        // 默认先存放一个老值
        this.oldValue = this.get()
    }
    get() {
        Dep.target = this // 取值 ,把这个观察者和数据关联起来
        let value = CompileUtil.getVal(this.vm, this.expr)// 调用函数取到实例中的 $data 定义的值
        Dep.target = null // 如果不取消,则任何数据取值,都会添加 watcher
        return value
    }
    update() { // 更新操作 , 数据变化后,会调用观察者的update方法
        let newVal = CompileUtil.getVal(this.vm, this.expr)
        if (newVal !== this.oldValue) {
            this.callback(newVal)
        }
    }
}

// 观察者
class Observer {
    constructor(data) {
        this.observer(data)
    }
    observer(data) {
        // 判断:如果是对象才观察
        if (data && typeof data == 'object') {
            // 如果是对象
            for (let key in data) { // 循环遍历 $data , 让每一个属性都具有 set 和 get 方法
                this.definedReactive(data, key, data[key])
            }
        }
    }
    definedReactive(obj, key, value) {// Object.definedProperty 必须的参数:对象,属性名,操作属性
        this.observer(value)// 递归操作,遍历对象中的对象
        let dep = new Dep() // 给每一个属性,都加上具有发布订阅的功能
        Object.defineProperty(obj, key, {
            get() {
                // 创建 watcher 时 , 会取到对应的内容 , 并且把watcher 放到了全局上
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set: (newVal) => {
                if (newVal != value) {
                    this.observer(newVal)// 监控新值,保证对象更改属性也为对象时,新属性具有 get 和 set 方法
                    value = newVal
                    dep.notify()
                }
            }
        })
    }
}
// 基类 调度
class Compile {
    constructor(el, vm) {
        // 判断el 属性,是不是一个元素,如果不是元素,那就获取它
        this.vm = vm
        this.el = this.isELementNode(el) ? el : document.querySelector(el)
        // 把当前节点中的元素 获取到 然后放到内存中
        let fragment = this.nodeToFragment(this.el)
        // 把节点中的内容进行替换

        // 编译模板 用数据编译
        this.compileNode(fragment)
        // 把内容再塞回到页面中
        this.el.appendChild(fragment)

    }
    // 处理元素节点
    compileElement(ele) {
        let attributez = ele.attributes;// 获取元素节点的属性赋值给类数组
        [...attributez].forEach(attr => {
            // 对属性进行判断
            let { name, value: expr } = attr

            // 判断是不是 Vue 指令,即是不是使用 “v-” 开头的,使用 startsWith方法
            if (name.startsWith('v-')) {// v-model v-html v-bind// 解构赋值后value 名为 expr

                let [, directive] = name.split('-')// 使用 - 对name进行切割,然后使用不同的指令进行处理,自定义函数 CompileUtil[directive]
                let [directiveName, eventName] = directive.split(':')
                // 将元素节点:ele,attr的表达式:expr,以及实例:vm 传入函数进行处理
                CompileUtil[directiveName](ele, expr, this.vm, eventName)
            }
        })
    }

    // 处理文本节点
    compileText(text) {
        let content = text.textContent
        // 使用正则表达式匹配文本节点中含有 {{}} 插值表达式符号的文本
        if (/\{\{(.+?)\}\}/.test(content)) {// 找到所有带有插值表达式的文本
            CompileUtil['text'](text, content, this.vm)
        }
    }
    compileNode(node) {// 编译内存中的dom节点
        let childNodes = node.childNodes;// 将获取到的子节点赋值
        [...childNodes].forEach(child => {// 展开类数组
            if (this.isELementNode(child)) {
                this.compileElement(child)// 对元素节点进行处理
                // 递归处理,深层遍历
                this.compileNode(child)
            } else {
                this.compileText(child)// 对文本节点进行处理
            }
        })

    }
    nodeToFragment(node) {
        // 创建一个文档碎片 使用 createDocumentFragment()方法
        let fragment = document.createDocumentFragment()
        let firstChild
        // while 当满足条件为 true 时则循环一直执行
        while (firstChild = node.firstChild) {
            // appendChild具有移动性,把真实DOM 树上的元素节点放入 自定义的 fragment中
            fragment.appendChild(firstChild)
        }
        return fragment
    }
    isELementNode(node) {
        // nodeType 判断节点为元素节点还是文本节点
        return node.nodeType === 1
    }
}
// 不同类型数据处理
CompileUtil = {
    getVal(vm, expr) {
        return expr.split('.').reduce((data, current) => {
            return data[current]
        }, vm.$data)// 使用 reduce 方法对$data进行遍历最终返回为expr的切割值的返回==> expr.split('.')==>$data.thame==>'泰晤士'
    },
    setValue(vm, expr, value) {
        expr.split('.').reduce((data, current, index, arr) => {
            if (index == arr.length - 1) {
                return data[current] = value
            }
            return data[current]
        }, vm.$data)
    },
    model(node, expr, vm) {// node 是节点 expr 是表达式 vm 是当前实例
        // 给输入框赋予value 属性, node.value = 实例中的$data的值
        let fn = this.updater['modelUpdater']
        // 给元素节点添加一个观察者,如果内容更新了就会触发此方法,会拿新值,给输入框赋予值
        new Watcher(vm, expr, (newVal) => {
            fn(node, newVal)
        })
        node.addEventListener('input', (e) => {
            let value = e.target.value; // 获取用户输入的内容
            this.setValue(vm, expr, value)
        })
        let value = this.getVal(vm, expr)
        fn(node, value)

    },
    html(node,expr,vm) { // v-html="message"
        let fn = this.updater['htmlUpdater']
        new Watcher(vm, expr, (newVal) => {
            fn(node, newVal)
        })
        
        let value = this.getVal(vm, expr)
        fn(node, value)
    },
    getContentValue(vm, expr) {
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getVal(vm, args[1])
        })
    },
    on(node, expr, vm, eventName) { // 对 v-on 进行处理 v-on:click="change" 被切割 expr === change
        node.addEventListener(eventName, (e) => {
            vm[expr].call(vm, e)// 改变this指向
        })
    },
    text(node, expr, vm) {
        let fn = this.updater['textUpdater']// 将获取到的值,重新插入到节点中
        let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {

            // 给表达式,每个插值表达式都加上观察者
            new Watcher(vm, args[1], () => {
                fn(node, this.getContentValue(vm, expr)) // 返回一个全的字符串
            })
            return this.getVal(vm, args[1])// 通过正则条件,拿到匹配到的插值表达式里的字符串,将字符串传入getVal函数取到vm 中的 $data的值
        })
        fn(node, content)
    },
    updater: {
        htmlUpdater(node,value){ // 简易赋值, innerHTML不安全,存在 xss攻击风险
            node.innerHTML = value
        },
        // 把数据插入到节点中
        modelUpdater(node, value) {
            return node.value = value
        },
        textUpdater(node, value) {
            return node.textContent = value
        }
    }
}
class Vue {
    constructor(options) {
        this.$el = options.el
        this.$data = options.data
        let computed = options.computed
        let methods = options.methods
        // 如果这个根元素 存在 那么就编译模板
        if (this.$el) {

            // 把数据,全部转换成用 Object.definedProperty 来定义的
            new Observer(this.$data)

            for (let key in computed) { // 处理computed依赖关系
                Object.defineProperty(this.$data, key, {
                    get: () => {
                        return computed[key].call(this)
                    }
                })
            }

            for (let key in methods) {
                Object.defineProperty(this, key, {
                    get() {
                        return methods[key]
                    }
                })
            }

            // 把数据获取操作 vm 上的取值操作,都代理到 vm.$data
            this.proxyVm(this.$data)

            new Compile(this.$el, this)
        }
    }
    proxyVm(data) {
        for (let key in data) {
            Object.defineProperty(this, key, {
                get() {
                    return data[key] // 进行了转化操作
                },
                set(newVal){ // 设置代理方法
                    data[key] = newVal
                }
            })
        }
    }
}