手写vue2双向绑定

107 阅读2分钟

项目地址:gitee.com/luobf22/vue…

vue2双向绑定核心:

  • Vue2 利用Object.defineProperty来进行数据劫持。这个方法可以对对象的属性进行重新定义,使其具有自定义的访问器属性(getset
  • 当读取属性(get)时,可以收集依赖;当修改属性(set)时,可以通知相关的依赖进行更新。

完整代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <!-- 
  通过Object.defineProperty劫持数据发生的改变
  如果数据改变,在set进行赋值,触发update方法
  进行更新节点内容,从而实现数据双向绑定 
  -->
</head>

<body>
  <div id="app">
    <button @click="btn">修改msg</button>
    <p>{{msg}}</p>
    <button @click="add">增加count</button>
    <p>{{count}}</p>
    <button @click="xiugaiArray">修改数组</button>
    <p>{{arr}}</p>
    <button @click="xiugaiObj">obj修改某个属性</button>
    <button @click="addObjAttr">obj添加属性</button>
    <p>{{obj.person.name}}</p>
    <p>{{obj.animal}}</p>
    <input type="text" v-model="inputModel">
    {{inputModel}}
  </div>
</body>

<script src="./手写Vue2.js"></script>

<script>

  const vm = new Vue({
    el: '#app',
    data: {
      msg: 'hello vue2',
      count: 100,
      arr: [1, 2, 3],
      obj: {
        person: {
          name: 'hhh'
        },
        animal: {
          dog: {
            name: '小狗'
          }
        }
      },
      inputModel: '请输入'
    },
    beforeCreate() {
      // console.log('beforeCreate',this.$data,this.$el)
    },
    created() {
      // console.log('created',this.$data,this.$el)
    },
    beforeMount() {
      // console.log('beforeMount',this.$data,this.$el)
    },
    mounted() {
      // console.log('mounted',this.$data,this.$el)
    },
    methods: {
      add() {
        this.count += 1
      },
      btn(e) {
        this.msg = 'welcome vue2 双向绑定'
      },
      xiugaiArray() {
        this.arr[0] = 9
        // this.arr = [3, 6, 9]
        console.log(this.arr);
      },
      xiugaiObj() {
        // this.obj.person.name = 'luo'
        this.obj = {
          person: {
            name: 'luo'
          },
          animal: {
            dog: {
              name: '小哈'
            }
          }
        }
      },
      addObjAttr() {
        this.obj.animal.dog.color = 'black'
        // console.log(this);
      }
    },
  })


</script>

</html>
class Vue {
    constructor(options) {
        this.$options = options
        console.log(this.$options);

        this.$watchEvent = {}
        // if (typeof options.beforeCreate == 'function') {
        //     options.beforeCreate.call(this)
        // }
        this.$data = options.data
        this.proxyData()
        this.observe()
        // if (typeof options.created == 'function') {
        //     options.created.call(this)
        // }
        // if (typeof options.beforeMount == 'function') {
        //     options.beforeMount.call(this)
        // }
        this.$el = document.querySelector(options.el)

        // if (typeof options.mounted == 'function') {
        //     options.mounted.call(this)
        // }
        this.compile(this.$el)
    }
    // 双向绑定 数据劫持
    proxyData() {
        for (const key in this.$data) {
            Object.defineProperty(this, key, {
                get() {
                    return this.$data[key];
                },
                set(val) {
                    // console.log('22', key, val);

                    this.$data[key] = val
                }
            })
        }
    }

    // 观察更新
    observe(obj) {
        for (const key in this.$data) {
            let value = this.$data[key]
            let that = this
            Object.defineProperty(this.$data, key, {
                get() {
                    return value
                },
                set(val) {
                    value = val
                    if (that.$watchEvent[key]) {
                        that.$watchEvent[key].forEach((item, index) => {
                            item.update()
                        })
                    }
                }
            })
        }
    };

    // 模板解析 将属性名转为数据 将事件绑定到元素
    compile(node) {
        node.childNodes.forEach((item, index) => {
            if (item.nodeType == 1) {
                // 元素节点
                if (item.hasAttribute('@click')) {
                    let vmKey = item.getAttribute('@click').trim()
                    item.addEventListener('click', (event) => {
                        this.eventFn = this.$options.methods[vmKey].bind(this)
                        this.eventFn(event)
                    })
                }
                if (item.hasAttribute('v-model')) {
                    let vmKey = item.getAttribute('v-model').trim()
                    if (this.hasOwnProperty(vmKey)) {
                        item.value = this[vmKey]
                    }
                    item.addEventListener('input', (event) => {
                        this[vmKey] = item.value
                    })
                }
                if (item.childNodes.length > 0) {
                    this.compile(item)
                }

            } else if (item.nodeType == 3) {
                // 文本节点
                let reg = /\{\{(.*?)\}\}/g
                let text = item.textContent
                let that = this
                const keys = [];
                text.replace(reg, (match, $1) => {
                    keys.push($1.trim());
                    return match;
                });

                function getObjData(arr, data, item, vmKey) {
                    let firstKey = arr.shift()
                    firstKey = firstKey.trim()
                    let node = {}
                    node.value = data[firstKey]

                    if (data.hasOwnProperty(firstKey)) {
                        let watch = new watcher(that, firstKey, item, 'textContent', vmKey)
                        if (that.$watchEvent[firstKey]) {
                            that.$watchEvent[firstKey].push(watch)
                        } else {
                            that.$watchEvent[firstKey] = []
                            that.$watchEvent[firstKey].push(watch)
                        }
                    }
                    let res = data[firstKey]
                    if (arr.length !== 0) {
                        return getObjData(arr, res, item)
                    } else {
                        return JSON.stringify(res)
                    }
                }
                // 给节点赋值
                item.textContent = text.replace(reg, (match, vmKey) => {
                    vmKey = vmKey.trim()
                    if (this.hasOwnProperty(vmKey)) {
                        let watch = new watcher(this, vmKey, item, 'textContent', vmKey)

                        if (this.$watchEvent[vmKey]) {
                            this.$watchEvent[vmKey].push(watch)
                        } else {
                            this.$watchEvent[vmKey] = []
                            this.$watchEvent[vmKey].push(watch)
                        }
                    }

                    let index = vmKey.indexOf('.')
                    if (index !== -1) {
                        let objArr = vmKey.split('.')
                        return getObjData(objArr, this.$data, item, vmKey)
                    } else {
                        // JSON.stringify将[object object] 展开显示 {"dog":{"name":"小狗"}}
                        let res = JSON.stringify(this.$data[vmKey])
                        return res
                    }

                })
            }
        });
    }
}

// 观察者类,用于关联DOM元素和对应的数据,实现更新逻辑
class watcher {
    // vm对象  key属性名称 node节点 attr改变文本节点内容的字符串
    constructor(vm, key, node, attr, vmKey) {
        this.vm = vm;
        this.key = key;
        this.node = node;
        this.attr = attr;
        this.vmKey = vmKey;
    }
    // 执行改变操作
    update() {
        console.log(this.vm);
        console.log(this.key);
        console.log(this.vmKey);

        function getObjData(arr, data) {
            // console.log('data', data);
            // console.log('arr', arr);
            let firstKey = arr.shift()
            firstKey = firstKey.trim()
            // console.log('firstKey', firstKey);
            let res = data[firstKey]
            // console.log('res', res);
            if (arr.length !== 0) {
                return getObjData(arr, res)
            } else {
                return res
            }
        }

        let index = this.vmKey.indexOf('.')
        let res 
        if (index !== -1) {
            let objArr = this.vmKey.split('.')
            res = JSON.stringify(getObjData(objArr, this.vm))
            this.node[this.attr] = res
        } else {
            res = JSON.stringify(this.vm[this.key])
            this.node[this.attr] = res
        }
    }
}

逐个分析函数

constructor(options)
将数据绑定到vue实例里的constructor
this.$options = options
this.$data = options.data

创建一个观察事件hash表,来监听每个node节点的变化,收集依赖相当于dep功能
this.$watchEvent = {}

获取el挂在的节点
this.$el = document.querySelector(options.el)

数据劫持,初始化
        this.proxyData()
数据劫持,观察数据变化
        this.observe()
模板解析,将html上的文本转为数据
        this.compile(this.$el)
双向绑定,数据劫持,第一次初始化数据
    proxyData() {
        let that = this
        for (const key in this.$data) {
            Object.defineProperty(this, key, {
                get() {
                    return this.$data[key];
                },
                set(val) {
                    this.$data[key] = val
                }
            })

        }
    }
双向绑定,数据劫持,观察更新,数据变化,触发watcher里的update函数
    observe(obj) {
        for (const key in this.$data) {
            let value = this.$data[key]
            let that = this
            Object.defineProperty(this.$data, key, {
                get() {
                    return value
                },
                set(val) {
                    value = val
                    if (that.$watchEvent[key]) {
                        that.$watchEvent[key].forEach((item, index) => {
                            item.update()
                        })
                    }
                }
            })
        }
    };
模板解析
    compile(node) {
        node.childNodes.forEach((item, index) => {
            if (item.nodeType == 1) {
                // 元素节点
                if (item.hasAttribute('@click')) {
                    let vmKey = item.getAttribute('@click').trim()
                    item.addEventListener('click', (event) => {
                        this.eventFn = this.$options.methods[vmKey].bind(this)
                        this.eventFn(event)
                    })
                }
                if (item.hasAttribute('v-model')) {
                    let vmKey = item.getAttribute('v-model').trim()
                    if (this.hasOwnProperty(vmKey)) {
                        item.value = this[vmKey]
                    }
                    item.addEventListener('input', (event) => {
                        this[vmKey] = item.value
                    })
                }
                if (item.childNodes.length > 0) {
                    this.compile(item)
                }

            } else if (item.nodeType == 3) {
                // 文本节点
                let reg = /\{\{(.*?)\}\}/g
                let text = item.textContent
                let that = this
                const keys = [];
                text.replace(reg, (match, $1) => {
                    keys.push($1.trim());
                    return match;
                });

                function getObjData(arr, data, item, vmKey) {
                    let firstKey = arr.shift()
                    firstKey = firstKey.trim()
                    let node = {}
                    node.value = data[firstKey]

                    if (data.hasOwnProperty(firstKey)) {
                        let watch = new watcher(that, firstKey, item, 'textContent', vmKey)
                        if (that.$watchEvent[firstKey]) {
                            that.$watchEvent[firstKey].push(watch)
                        } else {
                            that.$watchEvent[firstKey] = []
                            that.$watchEvent[firstKey].push(watch)
                        }
                    }
                    let res = data[firstKey]
                    if (arr.length !== 0) {
                        return getObjData(arr, res, item)
                    } else {
                        return JSON.stringify(res)
                    }
                }
                // 给节点赋值
                item.textContent = text.replace(reg, (match, vmKey) => {
                    vmKey = vmKey.trim()
                    if (this.hasOwnProperty(vmKey)) {
                        let watch = new watcher(this, vmKey, item, 'textContent', vmKey)

                        if (this.$watchEvent[vmKey]) {
                            this.$watchEvent[vmKey].push(watch)
                        } else {
                            this.$watchEvent[vmKey] = []
                            this.$watchEvent[vmKey].push(watch)
                        }
                    }

如果是对象,则需要层层遍历对象的属性,找到obj下的key值,然后通过update赋值
                    let index = vmKey.indexOf('.')
                    if (index !== -1) {
                        let objArr = vmKey.split('.')
                        return getObjData(objArr, this.$data, item, vmKey)
                    } else {
                        // JSON.stringify将[object object] 展开显示 {"dog":{"name":"小狗"}}
                        let res = JSON.stringify(this.$data[vmKey])
                        return res
                    }

                })
            }
        });
    }
}

观察者:创建观察者,赋予update函数
class watcher {
    // vm对象  key属性名称 node节点 attr改变文本节点内容的字符串
    constructor(vm, key, node, attr, vmKey) {
        this.vm = vm;
        this.key = key;
        this.node = node;
        this.attr = attr;
        this.vmKey = vmKey;
    }
    // 执行改变操作
    update() {
        function getObjData(arr, data) {
            let firstKey = arr.shift()
            firstKey = firstKey.trim()
            let res = data[firstKey]
            if (arr.length !== 0) {
                return getObjData(arr, res)
            } else {
                return res
            }
        }

        let index = this.vmKey.indexOf('.')
        let res
        if (index !== -1) {
        //obj属性层层遍历,找到最终的key值赋值
            let objArr = this.vmKey.split('.')
            res = JSON.stringify(getObjData(objArr, this.vm))
            this.node[this.attr] = res
        } else {
        //不是obj,赋值
        //为什么要用JSON.stringify 因为有时候需要将obj整个放到html上,
        //如果不加JSON.stringify就会显示[object object]
            res = JSON.stringify(this.vm[this.key])
            this.node[this.attr] = res
        }
    }
}

vue2 双向绑定的缺陷

  • 在 Vue 2 中,通过Object.defineProperty进行数据劫持来实现双向绑定。但是这种方式对于已经创建的对象,只能劫持对象已有的属性,无法自动劫持新添加的属性。这是因为 JavaScript 的Object.defineProperty是在对象属性定义时设置访问器属性来进行数据劫持的
  • 当给obj对象添加新的属性以及通过下边index给数组修改数据时,set方法无法劫持到数据,虽然get可以获取劫持的数据,但是无法通知update方法去对textContent进行修改,本来设想时可以通过对比之前的data数据,然后调用update方法,但是js会报错,显示超过栈的大小,所以也失败告终
  • 虽然视图没有及时更新,但是数据更新了,而且也可以通过vue.$set方式去进行修改视图