像React,Vue这类的框架,响应式是其最外围的个性之一。通过响应式能够实现当扭转数据的时候,视图会主动变动,反之,视图变动,数据也随之更新。防止了繁琐的dom操作,让开发者在开发的时候只须要关注数据自身,而不须要关注数据如何渲染到视图。

实现原理

2.x

在vue2.0中通过Object.defineProperty办法实现数据拦挡,也就是为每个属性增加get和set办法,当获取属性值和批改属性值的时候会触发get和set办法。

let vue = {}let data = {    msg: 'foo'}Object.defineProperty(vue, 'msg', {    enumerable: true,    configurable: true,    get() {        console.log('正在获取msg属性对应的值')        return data.msg    },    set(newValue) {        if(newValue === data.msg) {            return         }        console.log('正在为msg属性赋值')        data.msg = newValue    }})console.log(vue.msg)vue.msg = 'bar'

Object.defineProperty增加的数据拦挡在针对数组的时候会呈现问题,也就是当属性值为一个数组的时候,如果进行push,shift等操作的时候,尽管批改了数组,但不会触发set拦挡。

为了解决这个问题,vue在外部重写了原生的数组操作方法,以反对响应式。

3.x

在vue3.0版本中应用ES6新增的Proxy对象替换了Object.defineProperty,不仅简化了增加拦挡的语法,同时也能够反对数组。

let data = {    msg: 'foo'}let vue = new Proxy(data, {    get(target, key) {        console.log('正在获取msg属性对应的值')        return target[key]    },    set(target, key, newValue) {        if(newValue === target[key]) {            return         }        console.log('正在为msg属性赋值')        target[key] = newValue    }})console.log(vue.msg)vue.msg = 'bar'

依赖的开发模式

在vue实现响应式的代码中,应用了观察者模式。

观察者模式

观察者模式中,蕴含两个局部:

  • 观察者watcher

观察者蕴含一个update办法,此办法示意当事件发生变化的时候须要做的事件

class Watcher {    update() {        console.log('执行操作')    }}
  • 指标dep

指标蕴含一个属性和两个办法:

  1. subs属性:用于存储所有注册的观察者。
  2. addSub办法: 用于增加观察者。
  3. notify办法: 当事件变动的时候,用于轮询subs中所有的观察者,并执行其update办法。
class Dep {    constructor() {        this.subs = []    }    addSub(watcher) {        if (watcher.update) {            this.subs.push(watcher)        }    }    notify() {        this.subs.forEach(watcher => {            watcher.update()        })    }}
  • 应用形式
// 创立观察者和指标对象const w = new Watcher()const d = new Dep()// 增加观察者d.addSub(w)// 触发变动d.notify()

公布订阅模式

与观察者模式很类似的是公布订阅模式,该模式蕴含三个方面:

  • 订阅者

订阅者相似观察者模式中的观察者,当事件发生变化的时候,订阅者会执行相应的操作。

  • 发布者

发布者相似观察者模式中的指标,其用于公布变动。

  • 事件核心

在事件核心中存储着事件对应的所有订阅者,当发布者公布事件变动后,事件核心会告诉所有的订阅者执行相应操作。

与观察者模式相比,公布订阅模式多了一个事件核心,其作用是隔离订阅者和发布者之间的依赖。

vue中的on和emit就是实现的公布订阅模式,因为其和响应式原理关系不大,所以此处不再具体阐明。

自实现简版vue

简化版的vue外围蕴含5大类,如下图:

通过实现这5大类,就能够一窥Vue外部如何实现响应式。

vue

vue是框架的入口,负责存储用户变量、增加数据拦挡,启动模版编译。

Vue类:

  • 属性

$options 存储初始化Vue实例时传递的参数
$data 存储响应式数据
$methods 存储传入的所有函数
$el 编译的模版节点

  • 办法

_proxyData 公有办法,负责将data中所有属性增加到Vue实例上。

_proxyMethods 公有办法,遍历传入的函数,将非申明周期函数增加到Vue实例上。

directive 静态方法,用于向Vue注入指令。

  • 实现
// 所有申明周期办法名称const hooks = ['beforeCreate', 'created', 'beforeMount', 'mounted',    'beforeUpdate', 'updated', 'activated', 'deactivated', 'beforeDestroy', 'destroyed']class Vue {    constructor(options) {        this.$options = Object.assign(Vue.options || {}, options || {})        this.$data = options.data || {}        this.$methods = options.methods || {}        if (options && options.el) {            this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el        }        this._proxyData(this.$data)        this._proxyMethods(this.$methods)        // 实现数据拦挡        // 启动模版编译    }    _proxyMethods(methods) {        let obj = {}        Object.keys(methods).forEach(key => {            if (hooks.indexOf(key) === -1 && typeof methods[key] === 'function') {                obj[key] = methods[key].bind(this)            }        })        this._proxyData(obj)    }    _proxyData(data) {        Object.keys(data).forEach(key => {            Object.defineProperty(this, key, {                enumerable: true,                configurable: true,                get() {                    return data[key]                },                set(newValue) {                    // 数据未产生任何变动,不须要解决                    if (newValue === data[key]) {                        return                    }                    data[key] = newValue                }            })        })    }    // 用于注册指令的办法    static directive(name, handle) {        if (!Vue.options) {            Vue.options = {                directives: {}            }        }        Vue.options.directives[name] = {            bind: handle,            update: handle        }    }}

observer

observer类负责为data对象增加数据拦挡。

  • 办法

walk 轮询对象属性,调用defineReactive办法为每个属性增加setter和getter。
defineReactive 增加setter和getter。

  • 实现
class Observer {    constructor(data) {        this.walk(data)    }    // 轮询对象    walk(data) {        // 只有data为object对象时,才轮询其属性        if (data && typeof data === 'object') {            Object.keys(data).forEach(key => {                this.defineReactive(data, key, data[key])            })        }    }    // 增加拦挡    defineReactive(data, key, val) {        const that = this        // 如果val是一个对象,为对象的每一个属性增加拦挡        this.walk(val)        Object.defineProperty(data, key, {            enumerable: true,            configurable: true,            get() {                return val            },            set(newValue) {                if (val === newValue) {                    return                }                // 如果赋值为一个对象,为对象的每一个属性增加拦挡                that.walk(newValue)                val = newValue            }        })    }}

在Vue的constructor构造函数中增加Observer:

constructor(options) {        this.$options = Object.assign(Vue.options || {}, options || {})        this.$data = options.data || {}        this.$methods = options.methods || {}        if (options && options.el) {            this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el        }        this._proxyData(this.$data)        this._proxyMethods(this.$methods)        // 实现数据拦挡        new Observer(this.$data)        // 启动模版编译        new Compiler(this)}

directive

因为在compiler编译模版的时候,须要用到指令解析,所以此处模仿一个指令初始化办法,用于向vue实例增加内置指令。

在此处模仿实现了四个指令:

// v-textVue.directive('text', function (el, binding) {    const { value } = binding    el.textContent = value})// v-modelVue.directive('model', function (el, binding) {    const { value, expression } = binding    el.value = value    // 实现双向绑定    el.addEventListener('input', () => {        el.vm[expression] = el.value    })})// v-htmlVue.directive('html', function (el, binding) {    const { value } = binding    el.innerHTML = value})// v-onVue.directive('on', function (el, binding) {    const { value, argument } = binding    el.addEventListener(argument, value)})

compiler

compiler负责html模版编译,解析模版中的插值表达式和指令等。

  • 属性

el 保留编译的指标元素
vm 保留编译时用到的vue上下文信息。

  • 办法

compile 负责具体的html编译。

  • 实现
class Compiler {    constructor(vm) {        this.vm = vm        this.el = vm.$el        // 构造函数中执行编译        this.compile(this.el)    }    compile(el) {        if (!el) {            return        }        const children = el.childNodes        Array.from(children).forEach(node => {            if (this.isElementNode(node)) {                this.compileElement(node)            } else if (this.isTextNode(node)) {                this.compileText(node)            }            // 递归解决node上面的子节点            if (node.childNodes && node.childNodes.length) {                this.compile(node)            }        })    }    compileElement(node) {        const directives = this.vm.$options.directives        Array.from(node.attributes).forEach(attr => {            // 判断是否是指令            let attrName = attr.name            if (this.isDirective(attrName)) {                // v-text --> text                // 获取指令的相干数据                let attrNames = attrName.substr(2).split(':')                let name = attrNames[0]                let arg = attrNames[1]                let key = attr.value                // 获取注册的指令并执行                if (directives[name]) {                    node.vm = this.vm                    // 执行指令绑定                    directives[name].bind(node, {                        name: name,                        value: this.vm[key],                        argument: arg,                        expression: key                    })                }            }        })    }    compileText(node) {        // 利用正则表达式匹配插值表达式        let reg = /\{\{(.+?)\}\}/        const value = node.textContent        if (reg.test(value)) {            let key = RegExp.$1.trim()            node.textContent = value.replace(reg, this.vm[key])        }    }    // 判断元素属性是否是指令,简化vue原来逻辑,当初默认只有v-结尾的属性是指令    isDirective(attrName) {        return attrName.startsWith('v-')    }    // 判断节点是否是文本节点    isTextNode(node) {        return node.nodeType === 3    }    // 判断节点是否是元素节点    isElementNode(node) {        return node.nodeType === 1    }}

批改vue的构造函数,启动模版编译。

constructor(options) {        this.$options = Object.assign(Vue.options || {}, options || {})        this.$data = options.data || {}        this.$methods = options.methods || {}        if (options && options.el) {            this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el        }        this._proxyData(this.$data)        this._proxyMethods(this.$methods)        // 实现数据拦挡        new Observer(this.$data)        // 启动模版编译        new Compiler(this)}

dep

dep负责收集某个属性的所有观察者,当属性值发生变化的时候,会顺次执行观察者的update办法。

  • 属性

subs 记录所有的观察者

  • 办法

addSub 增加观察者
notify 触发执行所有观察者的update办法

  • 实现
class Dep {    constructor() {        // 存储所有的观察者        this.subs = []    }    // 增加观察者    addSub(sub) {        if (sub && sub.update) {            this.subs.push(sub)        }    }    // 发送告诉    notify() {        this.subs.forEach(sub => {            sub.update()        })    }}

当初的问题是何时增加观察者,何时触发更新?

从上图能够看出,应该在Observer中触发拦挡的时候对Dep进行操作,也就是get的时候增加观察者,set时触发更新。

批改observer的defineReactive办法:

defineReactive(data, key, val) {        const that = this        // 创立dep对象        const dep = new Dep()        // 如果val是一个对象,为对象的每一个属性增加拦挡        this.walk(val)        Object.defineProperty(data, key, {            enumerable: true,            configurable: true,            get() {                // 增加依赖                // 在watcher中,获取属性值的时候,会把相应的观察者增加到Dep.target属性上                Dep.target && dep.addSub(Dep.target)                return val            },            set(newValue) {                if (val === newValue) {                    return                }                // 如果赋值为一个对象,为对象的每一个属性增加拦挡                that.walk(newValue)                val = newValue                // 触发更新                dep.notify()            }        })}

watcher

watcher是观察者对象,在vue对象的属性发生变化的时候执行相应的更新操作。

  • 办法

update 执行具体的更新操作

  • 实现
class Watcher {    // vm: vue实例    // key: 监控的属性键值    // cb: 回调函数,执行具体更新    constructor(vm, key, cb) {        this.vm = vm        this.key = key        this.cb = cb        // 指定在这个执行环境下的watcher实例        Dep.target = this        // 获取旧的数据,触发get办法中Dep.addSub        this.oldValue = vm[key]        // 删除target,期待下一次赋值        Dep.target = null    }    update() {        let newValue = this.vm[this.key]        if (this.oldValue === newValue) {            return        }        this.cb(newValue)        this.oldValue = newValue    }}

因为须要数据双向绑定,在compiler编译模版的时候,创立Watcher实例,并指定具体如何更新页面。

compileElement(node) {        const directives = this.vm.$options.directives        Array.from(node.attributes).forEach(attr => {            // 判断是否是指令            let attrName = attr.name            if (this.isDirective(attrName)) {                // v-text --> text                // 获取指令的相干数据                let attrNames = attrName.substr(2).split(':')                let name = attrNames[0]                let arg = attrNames[1]                let key = attr.value                // 获取注册的指令并执行                if (directives[name]) {                    node.vm = this.vm                    // 执行指令绑定                    directives[name].bind(node, {                        name: name,                        value: this.vm[key],                        argument: arg,                        expression: key                    })                    new Watcher(this.vm, key, () => {                        directives[name].update(node, {                            name: name,                            value: this.vm[key],                            argument: arg,                            expression: key                        })                    })                }            }        })    }