[Vue] source code analysis--the realization of two-way data binding

Summarize

Vue's two-way data binding is mainly implemented through Object.defineProperty. First, add get/set monitoring to all properties, so that when the property value changes, the corresponding set method will be triggered, and then pass the observer in the set method. to update the view while doing dependency collection in the get method.


Implementation of the minimalist version

  • index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>极简双向数据绑定</title>
</head>
<body>
    <input type="text" id="message" />
    <div id="msg"></div>

    <script src="app.js"></script>
</body>
</html>
  • app.js
var obj = {}
Object.defineProperty(obj, "data", {
    get: function () {
        console.log("get")
    },
    set: function (newValue) {
        document.getElementById("message").value = newValue
        document.getElementById("msg").innerText = newValue
    }
})
document.getElementById("message").addEventListener('keyup', function () {
    obj.data = event.target.value
}) 
  • analyze
1)通过Object.defineProperty的方法为属性加上get/set的监控2)通过EventListener监听属性的改变,不断触发属性的set方法,从而实现数据的双向绑定

A slightly more complicated implementation

  • index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>双向数据绑定</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="message"> 
        {{ message }}
    </div>

    <script src="dist/bundle.js"></script>
</body>
</html>
  • main.js
import myVue from './myVue'

new myVue({
    el: '#app',
    data: {
        message: "hello myVue"
    }
})
  • myVue.js
import Observer from './Observer'
import Compiler from './Compiler'

class myVue {
    constructor(options) {
        // 获取对象传入的数据
        this.$options = options
        this.$el = this.$options.el
        this._data = this.$options.data

        // 将传入的所有属性添加get/set的属性监听
        // 属性值发生改变会触发set方法
        Object.keys(this._data).forEach(key=>{
            this.add_watch(key)
        })

        // 将所有属性加入订阅发布者模式的管理中
        new Observer(this._data)

        // 编译渲染页面
        new Compiler(this.$el, this)
    }
    add_watch(key) {
        var self = this
        Object.defineProperty(this, key, {
            get() {
                return self._data[key]
            },
            set(value) {
                self._data[key] = value
            }
        })
    }
}

export default myVue
  • Observer.js
import Dep from './Dep'

class Observer {
    constructor(data) {
        // 获取所有属性数据
        this.data = data

        // 为所有属性数据添加get/sete的属性监听
        Object.keys(this.data).forEach(key=>{
            this._bind(data, key, data[key])
        })
    }
    _bind(data, key, val) {
        var myDep = new Dep()
        Object.defineProperty(data, key, {
            get() {
                // 如果是为订阅的对象,则添订阅
                if(Dep.target) myDep.listen(Dep.target)
                return val
            },
            set(newValue) {
                if (newValue === val) return 
                val = newValue
                // 如果数值改变,则发布更新
                myDep.notify()    
            }
        })
    }
}

export default Observer
  • Watcher.js
import Dep from './Dep'

class Watcher {
    constructor(node, name, vm) {
        this.node = node
        this.name = name
        this.vm = vm

        Dep.target = this
        this.update()
        Dep.target = null
    }
    update() {
        this.node.nodeValue = this.vm[this.name]
    }
}

export default Watcher
  • Dep.js

class Dep {
    constructor() {
        this.list = []
    }
    listen(subs) {
        this.list.push(subs)
    }
    notify() {
        for(var i=0; i<this.list.length; i++){
            this.list[i].update()
        }
    }
}
Dep.prototype.target = null

export default Dep
  • Compiler.js
import Watcher from './Watcher'

const REG = /\{\{(.*)\}\}/

class Compiler {
    constructor(el, vm) {
        this.el = document.querySelector(el)
        this.vm = vm

        // 创建文档片段,编译完成后,挂载到el元素上
        this.frag = this._createFragment()
        this.el.appendChild(this.frag)
    }
    _createFragment() {
        var frag = document.createDocumentFragment()
        var child
        while (child = this.el.firstChild) {
            this._compile(child)
            frag.appendChild(child)
        }
        return frag
    }
    _compile(node) {
        // 如果传入的是节点node
        if(node.nodeType === 1) {
            var attr = node.attributes
            var self = this
            if(attr.hasOwnProperty('v-model')){
                var name = attr['v-model'].nodeValue
                node.addEventListener('input', function(e) {
                    self.vm[name] = e.target.value
                })
                node.value = this.vm[name]
            }
        }

        // 如果传入的是元素elemet
        if (node.nodeType === 3) {
            if(REG.test(node.nodeValue)) {
                var name = RegExp.$1
                name = name.trim()
                new Watcher(node, name, this.vm)
            }
        }
    }
}

export default Compiler

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326076027&siteId=291194637