手写vue响应式原理

261 阅读2分钟

引言

首先vue是一个类,每次new Vue的时候会传入参数,参数是一个对象 data,el是他的固定属性,在Vue的构造函数中会把el和data作为el和data的属性值,每次new Vue就会得到一个Vue实例,得到一个实例的同时,也希望data中的数据能够实现数据劫持,利用Object.defineProperty(obj,key,{set(){},get(){}})为$data中的每一个属性做数据劫持,使属性在使用和赋值时都会触发get或set函数,实现随时更新页面的数据,尤其是set方法,当修改一个属性的值的时候,就会触发set方法,就会把这个属性的事件池中的每一个watcher实例拿到并执行update函数,实现用到这一属性的页面节点的更新

概述

最核心的点,就是他有两条主线,一条是对data数据的劫持,一条就是对页面的解析,在第一次渲染页面时会达到效果,但是如何实现,数据改变页面内容同样改变呢,页面解析的时候,每当有地方需要data中的值的时候,就会new 一个Watcher实例,并传入当前节点,和使用data中数据的属性名,Watcher中方法 update就是为当前节点更换值设定的函数,也就是说当data中相应的key的值变化的时候,就要调用这个watcher实例中的update方法,它可以直接为当前的节点更换内容,而如何在data中一个属性值变化时会让所有与这个key有关的node节点更新呢,就要用到Dep事件池,每次为属性设置劫持的时候,就创建一个Dep事件池对象,这个对象中的池子,只可以放与这个属性有关的watcher,要在get时候添加,每个watcher在创建对象之后,把watcher对象放到全局找的到的地方,一般Dep这个函数实例上,然后通过获取这个vm.$data[key]的值触发,这个get函数,然后在get函数中把这个watcher实例放到相应的事件池中

<body>
    <div id="app">
        <h1>{{name}}</h1>
        <h1>{{age}}</h1>
        {{gender}}
        <input v-model="name">
        <input v-model="age">
    </div>
    <script>
        //以实现-model和插值表达式为例实现双向数据绑定m数据v视图vm视图与数据之间的纽带
        //定义一个vue类


        class Vue {
            constructor(options) {
                this.$el = document.querySelector(options.el)
                this.$data = options.data
                    //只要一创建vue实例就要对数据进行劫持(data)
                observe(this.$data)
                nodeToFragment(this.$el, this)
            }

        }
        //数据劫持的核心就是使用Object.definedProperty(obj,key,{set(){},get(){}})(vue2.0)
        //vue(3.0)let newObj=new Proxy(obj,{set(){},get(){}})
        function observe(data) {
            if (Object.prototype.toString.call(data) !== "[object Object]") return
            let keys = Object.keys(data)
            keys.forEach(item => {
                setData(data, item, data[item])
            })
        }

        function setData(obj, key, value) {
            observe(value)
            let dep = new Dep
            Object.defineProperty(obj, key, {
                set(newValue) {
                    value = newValue
                    observe(newValue)
                    dep.notify()
                },
                get() {
                    Dep.target && dep.addTo(Dep.target)

                    return value
                }
            })

        }

        function nodeToFragment(el, vm) {
            let child
            let fragment = document.createDocumentFragment()
            while (child = el.firstChild) {
                complie(child, vm)
                fragment.appendChild(child)
            }
            el.appendChild(fragment)
        }

        function complie(doc, vm) {
            if (doc.nodeType === 1) {
                //nodeType为1就是元素对象,得到他的所有属性
                let attrs = doc.attributes
                    //遍历所有属性找到属性是v-开头的
                ;
                [...attrs].forEach(item => {
                    if (/^v\-/.test(item.nodeName)) {
                        new Watcher(doc, vm, item.nodeValue)
                        doc.value = vm.$data[item.nodeValue]
                        doc.addEventListener("input", function() {
                            vm.$data[item.nodeValue] = this.value
                            console.log(vm.$data[item.nodeValue]);


                        })
                    }
                })

            } else if (doc.nodeType === 3) {
                //如果得到的dom对象是文本节点,拿到他的值
                let str = doc.textContent
                str = str.replace(/\{\{(.+?)\}\}/g, (a, b) => {
                    new Watcher(doc, vm, b.trim())
                    return vm.$data[b.trim()]
                })
                doc.textContent = str
            }
            doc.childNodes.forEach(item => {
                complie(item, vm)
            })
        }
        class Dep {
            constructor() {
                this.pool = []
            }
            addTo(watch) {
                watch && this.pool.push(watch)
            }
            notify() {

                this.pool.forEach(item => {
                    item.update()
                })
            }
        }

        class Watcher {
            constructor(node, vm, key) {
                Dep.target = this
                this.node = node
                this.vm = vm
                this.key = key
                this.get()
                Dep.target = null
            }
            update() {
                this.get()
                if (this.node.nodeType === 1) {
                    //this.node.value = this.value
                } else if (this.node.nodeType === 3) {
                    this.node.textContent = this.value
                }
            }
            get() {
                this.value = this.vm.$data[this.key]

            }
        }

        let vm = new Vue({
            el: "#app",
            data: {
                name: "haha",
                age: 18,
                gender: "女"
            }
        })

       
    </script>

</body>