【vue源码系列】深入理解Vue源码,持续更新中~ 简单实现Vue双向绑定、dom编译、依赖收集【三】


index.html

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue源码</title>
</head>

<body>
    <div id="app">
        <p>{{name}}</p>
        <p e-text="name"></p>
        <p>{{age}}</p>
        <input type="text" k-model="name" />
        <button @click="changeName">click button</button>
        <div e-html="buttonStr"></div>
    </div>

    <script src="./js/e-complie.js"></script>
    <script src="./js/e-vue.js"></script>
</body>
<script>
    let vm = new EVue({
        el: '#app',
        data: {
            name: 'ESullivan',
            age: 18,
            buttonStr: '<button>click html btn</button>'
        },
        created() {
            setTimeout(() => {
                this.name = '我是create修改的'
            }, 2000)
        },
        methods: {
            changeName() {
                this.name = 'vuejs,函数改变name'
                this.age = 19
            }
        }
    })
</script>

</html>

e-compile.js

class Compile {
    constructor(el, vm) {
        this.$vm = vm
        this.$el = document.querySelector(el)
        //获取所有子元素
        this.$fragment = this.nodeFragment(this.$el)
        this.compileElement(this.$fragment)
        this.$el.appendChild(this.$fragment)
    }
    nodeFragment(el) {
        let fragment = document.createDocumentFragment()
        console.log('nodeFragment() fragment:', fragment)
        let child
        while (child = el.firstChild) {
            console.log('nodeFragment() child:', child)
            fragment.appendChild(child)
        }
        console.log('nodeFragment() fragment:', fragment)
        return fragment
    }
    compileElement(el) {
        let childNodes = el.childNodes
        //使伪数组变为数组在forEach循环处理
        Array.from(childNodes).forEach(node => {
            //获取节点文本内容
            let text = node.textContent
            //使用简单正则匹配{{}}
            let reg = /\{\{(.*)\}\}/;
            if (this.isElementNode(node)) {
                // 如果是一个标签
                this.compile(node)
            }
            if (this.isTextNode(node) && reg.test(text)) {
                // 文本节点 并且包含{{}}
                this.compileText(node, RegExp.$1)
                // this.compileText(node,)
            }
            //获取子节点递归调用函数
            if (node.childNodes && node.childNodes.length) {
                this.compileElement(node)
            }
        })
    }
    compileText(node, key) {
        console.log('文本替换 并且可以收集依赖', node, key)
        this.text(node, this.$vm, key)
    }
    compile(node) {
        let attrs = node.attributes
        Array.from(attrs).forEach(attr => {
            const attrName = attr.name
            const key = attr.value
            if (this.isVueDirective(attrName)) {
                // 是vue的属性
                console.log('vue的变量', attrName)
                // 过滤到k-
                // k-text
                // k-model
                // k-html
                const dir = attrName.slice(2)
                if (this[dir]) {
                    this[dir](node, this.$vm, key)
                }
            }
            if (this.isVueEvent(attrName)) {
                console.log('vue的事件绑定', attrName)
                // @click ==> click
                let action = attrName.substring(1)
                this.eventHander(node, this.$vm, key, action)
                // 如果是绑定的事件
            }
        })
        // 标签比较复杂 在这里统一做
    }
    eventHander(node, vm, key, action) {
        const fn = vm.$options.methods && vm.$options.methods[key]
        node.addEventListener(action, fn.bind(vm), false)
    }
    text(node, vm, key) {
        this.update(node, vm, key, 'text')
    }

    html(node, vm, key) {
        this.update(node, vm, key, 'html')
    }
    model(node, vm, key) {
        this.update(node, vm, key, 'model')
        // 双向绑定  做数据修改
        node.addEventListener('input', e => {
            const newValue = e.target.value
            vm[key] = newValue
        })

    }

    update(node, vm, key, dir) {
        const fn = this[dir + "Updater"]
        fn && fn(node, vm[key])
        // 实际的更新操作
        // 自动添加依赖
        new Watcher(vm, key, value => {
            fn && fn(node, value)
        })
    }

    textUpdater(node, value) {
        node.textContent = value
    }
    htmlUpdater(node, value) {
        node.innerHTML = value
    }
    modelUpdater(node, value) {
        node.value = value
    }
    isVueDirective(name) {
        return name.indexOf('k-') == 0
    }
    isVueEvent(name) {
        return name.indexOf('@') == 0
    }
    isElementNode(node) {
        return node.nodeType == 1
    }
    isTextNode(node) {
        return node.nodeType == 3
    }
}

e-vue.js

// 依赖收集类
class Dep {
    constructor() {
        // 数组里的是依赖(监听器)
        this.deps = []
    }
    // 依赖添加到数组中
    addDeps(dep) {
        this.deps.push(dep)
    }
    // 循环缓存数组里的依赖循环触发通知更新
    notify() {
        this.deps.forEach(dep => {
            dep.update()
        })
    }
}
Dep.target = null
class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm
        this.cb = cb
        this.key = key
        //Dep的target设置为监听器
        this.value = this.get()
    }
    get() {
        //收集依赖
        Dep.target = this
        //读取数据自动收集依赖
        let value = this.vm[this.key]
        Dep.target = undefined
        return value
    }
    update() {
        this.value = this.get()
        this.cb.call(this.vm, this.value)
        // 实际更新的函数
        // console.log('视图想更新~~ 监听器收到')
    }
}

class EVue {
    constructor(options) {
        this.$options = options
        this.$data = options.data
        //递归为data里的数据添加getter/setter使其变为响应式的。
        this.observer(this.$data)
        // new Watcher()
        // console.log('模拟render,出发name的getter',this,name)
        if (options.el) {
            this.$compile = new Compile(options.el, this)
        }
        if (options.created) {
            options.created.call(this)
        }
        //设置$data在this上的依赖
    }
    observer(data) {
        Object.keys(data).forEach(key => {
            this.proxyData(key)
            this.defineReactive(data, key, data[key])
        })
    }
    defineReactive(obj, key, val) {
        // 新建一个依赖
        // 每一个key一个依赖收集的小朋友
        let dep = new Dep()
        Object.defineProperty(obj, key, {
            get() {
                // 收集依赖 addDep
                // console.log('收集依赖')
                Dep.target && dep.addDeps(Dep.target)
                return val
            },
            set(newVal) {
                val = newVal
                // console.log('通知依赖去更新')
                dep.notify()
                // 通知依赖去更新
                // 依赖.notify
            }
        })
    }
    proxyData(key) {
        Object.defineProperty(this, key, {
            get() {
                return this.$data[key]
            },
            set(newVal) {
                this.$data[key] = newVal
            }
        })
    }
}

sp20210323_101001_203.png
sp20210323_100950_701.png
sp20210323_100956_142.png

声明:麋鹿与鲸鱼|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 【vue源码系列】深入理解Vue源码,持续更新中~ 简单实现Vue双向绑定、dom编译、依赖收集【三】


Carpe Diem and Do what I like