手写Vue双向绑定

63 阅读1分钟

html部分

    <div id="app">
        <input type="text" v-model="myText">
        {{myText}}
        <input type="text" v-model.lazy="myText">
    </div>

js部分

    function defineReactive(vm, key, value) {
            let dep = new Dep()

            Object.defineProperty(vm, key, {
                get() {
                    if (Dep.target) {
                        dep.addSub(Dep.target)
                    }
                    return value
                },

                set(newVal) {
                    if (newVal === value) return
                    value = newVal
                    
                    // 通知更新
                    dep.notify()
                }
            })
        }

  

        
        function observe(vm, data) {
            Object.keys(data).forEach(key => {
                defineReactive(vm, key, data[key])
            })
        }

  
        function Dep() {
            this.subs = []
        }
 
        Dep.prototype.addSub = function (watcher) {
            this.subs.push(watcher)
        }
  
        Dep.prototype.notify = function () {
            this.subs.forEach(watcher => {
                watcher.update()
            })
        }

  
        function Watcher(vm, node, name) {
            // 实例化时标记此Watcher,并触发get()方法,将 Watcher 添加到订阅器 Dep 中
            Dep.target = this
            this.vm = vm
            this.node = node
            this.name = name
            this.get()
            this.update()
            // 添加完成,删除标记
            Dep.target = null
        }

        Watcher.prototype.get = function () {
            this.value = this.vm[this.name]  // 触发 defineReactive 中get方法,将Watcher添加到Dep中
        }

        Watcher.prototype.update = function () {
            let value = this.vm[this.name]

            // 更新文本
            this.node.nodeValue = value

            // 更新input框
            window.vModelObj[this.name].forEach(node => {
                node.value = value
            })
        }

  


        function compile(node, vm) {
            if (!window.vModelObj) window.vModelObj = {}

            // 元素节点 - 解析指令
            if (node.nodeType === 1) {
                let attrs = node.attributes
                for (let i = 0; i < attrs.length; i++) {
                    let attr = attrs[i]
                    if (attr.nodeName.includes('v-model')) {
                        if (window.vModelObj[attr.nodeValue]) {
                            window.vModelObj[attr.nodeValue].push(node)
                        } else {
                            window.vModelObj[attr.nodeValue] = [node]
                        }


                        // 解析修饰符 并 添加监听事件
                        node.addEventListener(attr.nodeName.includes('.lazy') ? 'change' : 'input', function (e) {
                            vm[attr.nodeValue] = e.target.value
                        })
                        node.value = vm[attr.nodeValue]
                    }
                }
            }

            // 文本节点 - 绑定订阅者 Watcher
            if (node.nodeType === 3) {
                let reg = /\{\{(.*)\}\}/;  
                // 正则匹配模版中 {{}} 中的内容
                if (reg.test(node.nodeValue)) {
                    let name = RegExp.$1

                    new Watcher(vm, node, name)
                }
            }
        }

  


        function nodeToFragment(vm, dom) {
            let fragment = document.createDocumentFragment()
            let child = dom.firstChild

            while (child) {
                compile(child, vm)
                fragment.appendChild(child)
                child = dom.firstChild
            }

            return fragment
        }

  


        function Vue(options) {
            let data = options.data
            
            // data 为函数类型判断
            if (Object.prototype.toString.call(options.data) === '[object Function]') {
                data = options.data()
            }

            observe(this, data)

            let dom = document.querySelector(options.el)
            let newDom = nodeToFragment(this, dom)

            dom.appendChild(newDom)
        }

  


        new Vue({
            el: '#app',
            data() {
                return {
                    myText: 'TOM'
                }
            }
        })