vue数据双向绑定原理 记录

98 阅读4分钟

实现思路:

  1. 实现一个observer劫持属性变化,在vue2中就是用的object.defineProperty方法劫持的属性
  2. 实现一个watcher,使用的方法是实现一个订阅发布函数方法
  3. 实现一个compiler,解析指令,渲染dom等操作 首先observer劫持了数据,如果数据变化就通知watcher,watcher再通知compiler执行dom更新等操作。compiler就是watcher和observer中间的桥梁,但是为什么不直接从oberser通知到compiler呢?这中间watcher又具体干神马呢?查一查原来是如果不用订阅发布这个模式,那么每次通知变化到compiler会更新所有的dom,哪怕这个属性没有发生改变,也会执行compiler所有的代码,不能实现精准更新,那多余的更新操作肯定是不需要的。
  • 一步一步的来。
<input type="text" id="oInput" >

const data={
    name:''
}

document.getElementById('oInput').addEventListener('input', function (e) {
    data.name = e.target.value
})
console.log(data.name)

这里实现了数据的单向绑定,input的时候就会改变data.name,那么还需要初始化input的value为data.name,在data改变时需要改变input框的值。就初步实现了数据双向绑定。





<input type="text" id="oInput" >

const data={
    name:''
}

document.getElementById('oInput').addEventListener('input', function (e) {
    //改变data.name值
    data.name = e.target.value
})
Object.defineProperty(data, 'name', {
        get: function () {
            //获取属性值会调用此方法,如果不返回值,那么使用data.name = 2值会赋值不上 
            console.info('get', initVal) //2
            return initVal
        },
        set: function (val) {
            // 设置属性值会调用此方法,改变input的值
            document.getElementById('oInput').value = val
            console.info('set', val) // 2
            initVal = val
        }
    })
    data.name = 2//这里访问了set
    console.log(data.name); //2,这里访问了get
    // 劫持了obj对象的a属性,并且改变了a属性的值

到这里初步实现了数据的双向绑定,下一步定义一个ovserver

    var data = {
        name: 'waang',
        age: '10'
    }
    document.getElementById('oInput').addEventListener('input', function (e) {
    //改变data.name值
    data.name = e.target.value
    })
    Object.keys(data).map((key) => {
        defineReactive(data, key, data[key])
    })
    function defineReactive(data, key, value) {
        Object.defineProperty(data, key, {
            get: function () {
                //获取属性值会调用此方法
                console.info('get', value) //2
                // 如果这里不return 那么劫持的属性值不回发生变化
                return value
            },
            set: function (val) {
                if (value === val) {
                    return
                }
                value = val
                // 设置属性值会调用此方法
                console.info('set', val) // 2
                // 通知解析器,执行对应的页面dom更新操作
                document.getElementById('oInput').value = val
            }
        })
    }

实现了ovserver之后应该把更新dom等操作抽离出来,实现一个complier解析器,解析dom节点上的指令,在observer的get方法里面就调用compile方法执行更新操作

   function compile() {
        let app = document.getElementById('app')
        let childNodes = app.childNodes
        // console.log(childNodes)
        childNodes.forEach((node) => {
            // console.log(node.nodeType)
            if (node.nodeType === 1) {
                // console.log(node)
                // 拿到所有的标签属性
                const attrs = node.attributes
                // console.log(attrs)
                Array.from(attrs).forEach(attr => {
                    console.log(attr)
                    const nodeName = attr.nodeName
                    const nodeValue = attr.nodeValue
                    // nodeName  -> v-text  就是我们需要查找的标识
                    // nodeValue -> name    data中对应数据的key
                    console.log(11, nodeName, nodeValue)
                    // 把data中的数据 放到满足标识的dom上
                    if (nodeName === 'v-text') {
                        console.log('设置值', node)
                        node.innerText = data[nodeValue]
                    }
                    if (nodeName === 'v-model') {
                        console.log('设置值', node)
                        node.value = data[nodeValue]
                        // 监听input事件 在事件回调中 拿到最新的输入值 赋值给绑定的属性
                        node.addEventListener('input', (e) => {
                            let newValue = e.target.value
                            // 反向赋值
                            data[nodeValue] = newValue
                        })
                    }
                })
            }
        })
    }

但是这样做的问题是每次属性又变化都会更新所有的指令上的数据,需要优化,那么就需要定义一个watcher精准更新

   <input type="text" id="oInput" v-model='name'>
   <p id="p" v-text='age'></p>
   <p v-text='name'></p>

    const Dep = {
        map: {},
        // 收集事件的方法
        collect(eventName, fn) {
            // 如果当前map中已经初始化好了 
            // 就直接往里面push  如果没有初始化首次添加  就先进行初始化
            if (!this.map[eventName]) {
                this.map[eventName] = []
            }
            this.map[eventName].push(fn)
        },
        // 触发事件的方法
        trigger(eventName) {
            this.map[eventName].forEach(fn => fn())
        }
    }

修改之后的完整代码如下:

   var data = {
        name: 'waang',
        age: '10'
    }
    Object.keys(data).map((key) => {
        defineReactive(data, key, data[key])
    })
    // 引入发布订阅模式
    // watcher
    const Dep = {
        map: {},
        // 收集事件的方法
        collect(eventName, fn) {
            // 如果当前map中已经初始化好了 
            // 就直接往里面push  如果没有初始化首次添加  就先进行初始化
            if (!this.map[eventName]) {
                this.map[eventName] = []
            }
            this.map[eventName].push(fn)
        },
        // 触发事件的方法
        trigger(eventName) {
            this.map[eventName].forEach(fn => fn())
        }
    }
    // Observer
    function defineReactive(data, key, value) {
        Object.defineProperty(data, key, {
            get: function () {
                //获取属性值会调用此方法
                console.info('get', value) //2
                // 如果这里不return 那么劫持的属性值不回发生变化
                return value
            },
            set: function (val) {
                if (value === val) {
                    return
                }
                value = val
                // 设置属性值会调用此方法
                console.info('set', val) // 2
                // 通知解析器,执行对应的页面dom更新操作
                Dep.trigger(key)
            }
        })
    }

    function compile() {
        let app = document.getElementById('app')
        let childNodes = app.childNodes
        // console.log(childNodes)
        childNodes.forEach((node) => {
            // console.log(node.nodeType)
            if (node.nodeType === 1) {
                // console.log(node)
                // 拿到所有的标签属性
                const attrs = node.attributes
                // console.log(attrs)
                Array.from(attrs).forEach(attr => {
                    console.log(attr)
                    const nodeName = attr.nodeName
                    const nodeValue = attr.nodeValue
                    // nodeName  -> v-text  就是我们需要查找的标识
                    // nodeValue -> name    data中对应数据的key
                    console.log(11, nodeName, nodeValue)
                    // 把data中的数据 放到满足标识的dom上
                    if (nodeName === 'v-text') {
                        console.log('设置值', node)
                        node.innerText = data[nodeValue]
                        Dep.collect(nodeValue, () => {
                            console.log(`当前您修改了属性${nodeValue}`)
                            node.innerText = data[nodeValue]
                        })
                    }
                    if (nodeName === 'v-model') {
                        console.log('设置值', node)
                        node.value = data[nodeValue]
                        Dep.collect(nodeValue, () => {
                            node.value = data[nodeValue]
                        })
                        // 监听input事件 在事件回调中 拿到最新的输入值 赋值给绑定的属性
                        node.addEventListener('input', (e) => {
                            let newValue = e.target.value
                            // 反向赋值
                            data[nodeValue] = newValue
                        })
                    }
                })
            }
        })
    }

    compile()