VUE中的数据双向绑定(Object.defineProperty)

179 阅读4分钟

通过Object.defineProperty可以在一个对象上定义一个属性,或者修改这个属性,第一个参数。至于这个defineProperty的详细用法,可以参阅MDN手册,在此不赘叙。

var obj = {}

Object.defineProperty(obj, 'name', {
  get () {
    console.log('getter',value)
    return value
  },
  set (newValue) {
    console.log('setter',newValue)
    value = newValue
  },
  enumerable: true,
  configurable: true
})

// 如此以来,访问obj.name属性或修改obj.name的值,都会触发get()或set()。

在此基础上,遍历这个对象的所有属性,就可以实现一个丐版的数据劫持。

<input type="text" id ='input' onchange="input(event)">    
<span id="span"></span>
var obj = {}

Object.defineProperty(obj,'name',{
        get() {
            console.log('getter')
        },
        set(newValue) {
            console.log('setter',newValue)
            document.getElementById('input').value=newValue
            document.getElementById('span').innerHTML=newValue
        }
    })
function input (event){
    obj.name =event.target.value
}

但是上面的代码,数据、函数和DOM紧紧地耦合在一起,所以要进行解耦。 回顾学习VUE之初,推荐使用VUE的方法是引入vue.js文件。所以,下面,先写上。要做的第一件事,是将模版解析出来,能更好地理解数据的双向绑定。


<div id="app">
    <input type="text" v-model="user.name" />
    <div>
        <div>{{user.name}}-{{user.age}}</div>
    </div>
</div>
 
<script type="text/javascript">
    const app = new MineVue({
        el: '#app',
        data: {
            user: {
                name: 'Tom',
                age: 18
            }
        }
    })
    
    //基类,负责调度
    class MineVue {
        constructor(options) {
            this.el = options.el
            this.data = options.data
            this.init()
        }
        init() {
            if (this.el) {
                this.vm = document.querySelector(this.el)
                new Compiler(this.el, this)
            }
        }
    }
    
    //编译模版
    class Compiler {
        constructor(el, vm) {
            this.el = document.querySelector(el)
            this.vm = vm
            this.init(this.el)
        }
        init(el) {
            const fragment = this.nodeToFragment(el)
            this.compile(fragment)
            el.appendChild(fragment)
        }
        // 为了避免对DOM的多次操作引起重排回流,所以将DOM存到内存中
        nodeToFragment(node) {
            const fragment = document.createDocumentFragment()
            let firstChild = node.firstChild
            while (firstChild) {
                fragment.appendChild(firstChild)
                firstChild = node.firstChild
            }
            return fragment
        }
        // 将模版编译
        compile(node) {
            // 将子节点取出
            const childNodes = [...node.childNodes]
            // 编译
            childNodes.forEach(childNode => {
                if (childNode.nodeType === 1) {
                    this.compileElement(childNode)
                    this.compile(childNode)
                } else {
                    this.compileText(childNode)
                }
            })
        }
        // 编译标签中的v-model
        compileElement(node) {
            // 取出节点的所有属性
            const attributes = [...node.attributes]
            attributes.forEach(attr => {
                const {name,value: expression} = attr
                const reg = /^v-model/
                if (reg.test(name)) {
                // 将值放到节点中
                node.value = getValue(this.vm, expression)
                }
            })
        }
        // 编译{{}}
        compileText(node) {
            const reg = /\{\{(.+?)\}\}/g
            const expression = node.textContent
            if (reg.test(expression)) {
                const value = expression.replace(reg, (...args) => {
                    return getValue(this.vm, args[1])
                })
                node.textContent = value
                }
            }
        }
    
</script>

将模版编译好之后,引入一个发布订阅模式,首先实现一个观察者Observer,监听每个数据的变化。这样,就对MinewVue.html中app的数据进行了监控。

//修改一下MineVue,调用Observer,将数据都监控起来
class MineVue {
    constructor(options) {
        this.el = options.el
        this.data = options.data
        this.init()
    }
    init() {
        if (this.el) {
            this.vm = document.querySelector(this.el)
            new Observer(this.data)
            new Compiler(this.el, this)
        }
    }
}


//观察者,对data里面的所有属性进行拦截。
class Observer {
  constructor(data) {
    this.observer(data)
  }
  //遍历data对象的所有属性
  observer(data) {
    if (!data || typeof data !== 'object') return
    Object.keys(data).forEach(key => {
        this.defineReactive(data, key, data[key])
    })
  }
  //对所有属性设置setter和getter
  defineReactive(data, key, value) {
    //递归遍历
    this.observer(value)
    //拦截
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        return value
      },
      set(newValue) {
        if (value !== newValue) {
          console.log(`检测到${value}=>${newValue}`)
          value = newValue
        }
      }
    })
  }
}

既然这里把数据监控了起来,那么接下来要做的,就是做一个订阅者,来订阅这些数据,同时维护一份订阅者的列表,存放所有的订阅者,如果数据更新了,就从订阅者列表依次更新。

class Watcher {
    constructor(expression, vm) {
        this.expression = expression
        this.vm = vm
        //获取旧的值
        this.value = this.get()
    }
    get() {
        Deposit.target = this
        const value = getValue(this.expression, this.vm)
        Deposit.target = null
        return value
    }
    //数据更新时触发的函数
    updata() {
        const newValue = getValue(this.expression, this.vm)
        if (this.value !== newValue) {
            this.value = newValue
        }
    }
}
class Deposit {
    constructor() {
        this.watchers = []
    }
    //将订阅者(Watcher)添加到订阅者列表
    addWatcher(watcher) {
        this.watchers.push(watcher)
    }
     //通知所有的订阅者(Watcher)数据更新
    notify() {
        this.watchers.forEach(watcher => {
            watcher.update()
        })
    }
}

这样,就完成了一个订阅者和订阅发布中心。下面要做的事情就是关联起来。 首先,在Observer中修改一下,无论是访问数据还是更新数据,都通过订阅发布中心Deposit来进行。

class Observer {
    constructor(data) {
        this.observer(data)
    }
    observer(data) {
        if (!data || typeof data !== 'object') return
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
        })
    }
    defineReactive(data, key, value) {
        this.observer(value)
        //给当前属性添加监听
        const deposit = new Deposit()
        Object.defineProperty(data, key, {
            configurable: true,
            enumerable: true,
            get() {
                //将属性添加到订阅者中心Deposit当中
                if (Deposit.target) deposit.addWatcher(Deposit.target)
                return value
            },
            set(newValue) {
                if (value === newValue) return
                value = newValue
                //数据更新后,通知订阅者中心
                deposit.notify()
            }
        })
    }
}

这样,数据就和订阅者中心(Deposit)关联了起来,还差最后一个,就是在编译模版的过程当中,将数据通过Watcher来绑定,所以修改一下Compilder。

class Compiler {
    constructor(el, vm) {
        this.el = document.querySelector(el)
        this.vm = vm
        this.init(this.el)
    }
    init(el) {
        const fragment = this.nodeToFragment(el)
        this.compile(fragment)
        el.appendChild(fragment)

    }
    nodeToFragment(node) {
        const fragment = document.createDocumentFragment()
        let firstChild = node.firstChild
        while (firstChild) {
            fragment.appendChild(firstChild)
            firstChild = node.firstChild
        }
        return fragment
    }
    compile(node) {
        const childNodes = [...node.childNodes]
        childNodes.forEach(node => {
            if (node.nodeType === 1) {
                this.compileElement(node)
                this.compile(node)
            } else {
                this.compileText(node)
            }
        })
    }
    compileElement(node) {
        const attributes = [...node.attributes]
        attributes.forEach(attr => {
            const { name, value: expression } = attr
            if (name !== 'v-model') return
            const value = getValue(expression, this.vm)
            //加上观察者
            new Watcher(expression, this.vm, (newValue) => {
                node.value = newValue
            })
            node.addEventListener('input', e => {
                const value = e.target.value
                setValue(expression, this.vm, value)
            })
            node.value = value
        })

    }
    compileText(node) {
        const reg = /\{\{(.+?)\}\}/g
        const content = node.textContent
        if (reg.test(content)) {
            const value = content.replace(reg, (...args) => {
                //给每个{{}}表达式都加上观察者
                new Watcher(args[1], this.vm, () => {
                    //因为DOM中都模版可能是{{a}}{{b}}这样的形式,所以要返回整个字符串
                    node.textContent = this.getCompileTextValue(content, this.vm)
                })
                return getValue(args[1], this.vm)
            })
            node.textContent = value
        }
    }
    getCompileTextValue(expression, vm) {
        const reg = /\{\{(.+?)\}\}/g
        const value = expression.replace(reg, (...args) => {
            const value = getValue(args[1], vm)
            return value
        })
        return value
    }
}