双向数据绑定 - pod4g/tool GitHub Wiki

class Dep {
    constructor() {
        this.subs = []
    }
    addSub(watcher) {
        this.subs.push(watcher)
    }
    notify() {
        console.log('this.subs:', this.subs)
        this.subs.forEach(watcher => watcher.update())
    }
}
class Watcher {
    constructor(node, vm, name) {
        Dep.target = this
        this.node = node
        this.vm = vm
        this.name = name
        this.update()
        Dep.target = null
    }
    get() {
        this.value = this.vm[this.name]
    }
    update() {
        this.get()
        if(this.node.nodeType === Node.ELEMENT_NODE) {
            console.log('this.node.tagName:', this.node.tagName, this.value)
            if (this.node.tagName === 'INPUT') {
                this.node.value = this.value
            } else {
                this.node.textContent = this.value
            }
        } else if(this.node.nodeType === Node.TEXT_NODE) {
            this.node.nodeValue = this.value
        }
    }
}
class Vue {
    constructor(el, vm) {
        this.el = document.getElementById(el)
        this.vm = vm
        this.observe(vm)
        if (this.el) {
            const fragment = this.nodeToFragment()
            this.compile(fragment)
            this.el.appendChild(fragment)
        }
    }
    observe(vm) {
        if(!vm || typeof vm !== 'object') return
        Object.keys(vm).forEach(key => this.defineReactive(vm, key, vm[key]))
    }
    defineReactive(vm, key, value) {
        const dep = new Dep()
        Object.defineProperty(vm, key, {
            enumerable: true,
            configurable: true,
            set(newValue) {
                value = newValue
                dep.notify()
                this.observe(newValue)
            },
            get() {
                if(Dep.target) dep.addSub(Dep.target)
                return value
            }
        })
    }
    nodeToFragment() {
        const fragment = document.createDocumentFragment()
        let firstchild
        while (firstchild = this.el.firstChild) {
            fragment.appendChild(firstchild)
        }
        return fragment
    }
    compile(fragment) {
        const reg = /\{{2}(.*)\}{2}/
        Array.from(fragment.childNodes).forEach(node => {
            const { TEXT_NODE, ELEMENT_NODE } = Node
            const { nodeType, nodeValue, attributes } = node
            let dataName
            if (nodeType === TEXT_NODE) {
                const match = nodeValue.trim().match(reg)
                if(match) {
                    dataName = match[1]
                    const data = this.vm[dataName]
                    node.nodeValue = node.nodeValue.replace(reg, data)
                    new Watcher(node, this.vm, dataName)
                }
            } else if(nodeType === ELEMENT_NODE) {
                Array.from(attributes).forEach(({name, nodeValue}) => {
                    dataName = nodeValue
                    const data = this.vm[nodeValue]
                    switch(name) {
                        case 'v-model':
                            node.addEventListener('input', event => this.vm[nodeValue] = event.target.value)
                            node.value = data
                        case 'v-text':
                            node.textContent = data
                        case 'v-html':
                            node.innerHTML = data
                    }
                })
                new Watcher(node, this.vm, dataName)
                this.compile(node)
            }
        })
    }
}

使用

<!DOCTYPE html>
<html lang="ZH-cn">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge, chrome=1"/>
<meta name="renderer" content="webkit"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="format-detection" content="telephone=no"/>
<meta name="format-detection" content="email=no">
<meta name="description" content=""/>
<meta name="keywords" content=""/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
<title>测试vue</title>
</head>
<body>
<div id="app">
    <input type="text" v-model="name">
    <p v-text="name"></p>
    {{name}}
</div>
<script src="./lib/vue2.js"></script>
<script>
const vm = {
    name: '李彦峰'
}
new Vue('app', vm)
</script>
</body>
</html>

双向绑定中的对象关键点:

  1. compiler 用来解析模板,取出指令或text节点,把相应的数据赋值给dom
  2. observer 用来劫持数据
  3. dep 依赖收集,一个依赖就是一个watcher对象,dep对象用一个数据来存放依赖(即watcher),这个对象其实就是一个简易的发布订阅对象的实现
  4. watcher 绑定依赖,当数据发生变化时,更新视图,new一个watcher,就是声明了一个依赖
  5. 使用Dep.target暂时保存每一个新new的watcher对象,在数据劫持的getter中,把watcher添加到dep对象的数组中,数据劫持的getter其实是由watcher触发(这个设计很巧妙)

关键代码:

class Watcher {
    get() {
        this.value = this.vm[this.name] // 触发数据劫持的getter
    }
}

defineReactive(vm, key, value) {
        const dep = new Dep()
        Object.defineProperty(vm, key, {
            get() {
                if(Dep.target) dep.addSub(Dep.target) // 由watcher的get方法触发,一触发就进入dep数组,所以本质上,进入dep数组还是由watcher自己控制
                return value
            }
        })
    }

相关:https://github.com/pod4g/tool/wiki/%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E6%95%B0%E6%8D%AE%E5%8A%AB%E6%8C%81%E5%99%A8

⚠️ **GitHub.com Fallback** ⚠️