美文网首页
Vue数据双向绑定的原理以及实现

Vue数据双向绑定的原理以及实现

作者: 迦叶凡 | 来源:发表于2019-02-12 17:35 被阅读0次

前言

使用Vue已经一段时间,所以对Vue的核心功能——双向的数据绑定原理进行了研究和实现。下面直接上代码,因为代码里基本上都写清楚了注释,所以就不多说废话了。(直接复制代码到本地就可以运行)
上菜~~

正文

<!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>Document</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="text">
        <h1>{{text}}</h1>
        <button v-on:click="changeText">change text</button>
    </div>

    <script>
        //vue双向数据绑定的原理几个关键点
        //1.observe 数据劫持
        //2.Dep 消息订阅器(收集订阅者,发布消息)
        //3.Watcher (订阅者)
        //4.Compile HTML模板解析器
        //5.Vue 入口函数

        //1.observe 对数据进行拦截
        function observe (data) {
            if(!data || typeof data !== 'object'){
                return
            }
            Object.keys(data).forEach(function(key) {
                defineReactive(data, key, data[key]);
            })
        }
        function defineReactive (data, key, value) {
            //如果子属性为object也进行遍历监听
            observe(value)
            //每一个key都会有一个dep实例来管理自己的订阅者
            let dep = new Dep()
            Object.defineProperty(data, key, {
                configurable: false,
                enumerable: true,
                get: function() {
                    //在Watcher初始化实例的时候回触发对应属性的get函数
                    //此时将对应的watcher添加到对应的subs中
                    if(Dep.target){
                        dep.addSub(Dep.target);
                    }
                    return value
                },
                set: function(newValue) {
                    if(value === newValue){
                        return
                    }
                    value = newValue
                    dep.notice()
                }
            })
        }
        //2.Dep消息订阅器(收集订阅者,发布消息)
        function Dep () {
            this.subs = []
        }
        Dep.prototype = {
            addSub: function(sub) {
                this.subs.push(sub)
            },
            notice: function() {
                this.subs.forEach(function(sub) {
                    sub.update()
                })
            }
        }
        //临时缓存watcher
        Dep.target = null
        //3. Watcher 观察者
        function Watcher (vm, key, callback) {
            this.callback = callback
            this.vm = vm
            this.key = key
            //触发属性的get函数,然后添加到对应的消息订阅器上
            this.value = this.get()
        }
        Watcher.prototype = {
            update: function() {
                this.run()
            },
            run: function() {
                //这里虽然也会触发get函数
                //但是不会再次添加观察者到消息订阅器中
                let value = this.vm[this.key]
                let oldValue = this.value
                if(oldValue !== value){
                    this.callback.call(this.vm, value, oldValue)
                }
            },
            get: function() {
                //缓存下watcher自己
                Dep.target = this
                //在第一次new Watcher执行到这里的时候
                //会触发get函数,此时会添加watcher到相应的sub中
                let value = this.vm[this.key]
                //添加成功
                Dep.target = null
                return value
            }
        }
        //4.compile 模板解析器
        function Compile (el, vm) {
            this.vm = vm
            this.$el = document.querySelector(el)
            if(this.$el){
                //初始化dom片段,防止频繁的操作dom
                this.$fragment = this.createFragment(this.$el)
                //解析节点
                this.init()
                this.$el.appendChild(this.$fragment)
            }
        }
        Compile.prototype = {
            createFragment: function (el) {
                let fragment = document.createDocumentFragment()
                let child
                while (child = el.firstChild) {
                    // firstChild和firstElementChild区别
                    // 若文档已存在了该节点,则会先删除,然后在插入到新的位置
                    fragment.appendChild(child)
                }
                return fragment
            },
            init: function () {
                this.compileElement(this.$fragment)
            },
            compileElement: function (el) {
                // childNodes获取元素节点和文本节点,children只获取元素节点
                let childNodes = el.childNodes
                let self = this
                Array.from(childNodes).forEach(function (node) {
                    let text = node.textContent
                    let reg = /\{\{(.*)\}\}/
                    if (node.nodeType === 1){
                        //按元素节点处理
                        self.compile(node)
                    }else if (node.nodeType === 3 && reg.test(text)) {
                        //按文本节点处理
                        self.compileText(node, RegExp.$1)
                    }
                })
            },
            compileText: function (node, exp) {
                let text = this.vm[exp]
                //更新文本节点的值
                node.textContent = text
                //生成订阅器并绑定更新函数, model => view
                new Watcher(this.vm, exp, function (value) {
                    node.textContent = value
                })
            },
            compile: function (node) {
                //解析元素节点的属性
                let nodeAttrs = node.attributes
                Array.from(nodeAttrs).forEach((attr) => {
                    let attrName = attr.name
                    //判断是否是规范的指令,v-开头
                    if(this.isDireactive(attrName)){
                        let exp = attr.value
                        let dir = attrName.slice(2)
                        //判断是什么指令,事件指令?还是普通指令
                        if(this.isEventDireactive(dir)){
                            //根据事件指令集进行处理
                            compileUtil.eventHander(node, this.vm, exp, dir)
                        } else {
                            //普通指令
                            //按普通指令处理
                            //这里假设是最简单的v-model指令
                            compileUtil[dir](node, this.vm, exp)
                        }
                    }
                })
                //继续解析该元素的子节点
                this.compileElement(node)
            },
            isDireactive: function (attrName) {
                if(attrName.includes('v-')){
                    return true
                }
                return false
            },
            isEventDireactive: function (dir) {
                if(dir.includes('on')){
                    return true
                }
                return false
            }

        }
        let compileUtil = {
            model: function (node, vm, exp) {
                //初始化值
                node.value = vm[exp]
                //view => model
                node.addEventListener('input', function (event) {
                    vm[exp] = event.target.value
                })
                //modal => view
                new Watcher(vm, exp, function (value) {
                    node.value = value
                })
                //强制触发对应的get函数,来通知其他的观察者更新数据
                node.nodeValue = vm[exp]
            },
            eventHander: function (node, vm, exp, dir) {
                //截取事件名称,click
                let eventName = dir.slice(3)
                //给指定的节点绑定事件监听
                node.addEventListener(eventName, function (event) {
                    vm[exp]()
                })
            }
        }
        //5.Vue函数入口
        function Vue (options) {
            //检测是否通过new关键调用Vue
            if (!this instanceof Vue) {
                //如果是当成普通函数调用,this=>window
                alert('please use Vue by "new" key word!')
            }
            let vm = this
            vm.$data = options.data
            vm.$methods = options.methods
            let data = options.data
            let methods = options.methods
            //将vm.key代理到vm.$data.key
            Object.keys(data).forEach(function (key) {
                vm.proxyKey(vm.$data, key)
            })
            //将vm.key代理到vm.$methods.key
            Object.keys(methods).forEach(function (key) {
                vm.proxyKey(vm.$methods, key)
            })
            observe(data)
            vm.$compile = new Compile(options.el, vm)
        }
        Vue.prototype = {
            proxyKey: function (targetObj, key) {
                let vm = this
                Object.defineProperty(vm, key, {
                    configurable: false,
                    enumerable: true,
                    get: function () {
                        return targetObj[key]
                    },
                    set: function (newValue) {
                        targetObj[key] = newValue
                    }
                })
            } 
        }
        let vm = new Vue({
            el: '#app',
            data: {
                text: 'hello world!'
            },
            methods: {
                changeText: function () {
                    this.$data.text = 'hello vue world!'
                }
            }
        })
    </script>
</body>
</html>

相关文章

  • Vue双向数据绑定原理

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • 关于双向绑定的问题

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • 深入Vue响应式原理

    1.Vue的双向数据绑定 参考 vue的双向绑定原理及实现Vue双向绑定的实现原理Object.definepro...

  • vue 双向数据绑定

    Vue实现数据双向绑定的原理:Object.defineProperty()vue实现数据双向绑定主要是:采用数据...

  • Vue实现数据双向绑定的原理

    Vue实现数据双向绑定的原理:Object.defineProperty() vue实现数据双向绑定主要是:采用数...

  • 【转】JavaScript的观察者模式(Vue双向绑定原理)

    关于Vue实现数据双向绑定的原理,请点击:Vue实现数据双向绑定的原理原文链接:JavaScript设计模式之观察...

  • 前端理论面试--VUE

    vue双向绑定的原理(详细链接) VUE实现双向数据绑定的原理就是利用了 Object.definePropert...

  • 剖析Vue原理&实现双向绑定MVVM

    1、了解vue的双向数据绑定原理以及核心代码模块 2、缓解好奇心的同时了解如何实现双向绑定 几种实现双向绑定的做法...

  • vue面试知识点

    vue 数据双向绑定原理 vue实现数据双向绑定原理主要是:采用数据劫持结合发布订阅设计模式的方式,通过对data...

  • 前端面试题:VUE

    1. vue的双向数据绑定实现原理? 2. vue如何在组件之间进行传值? 3. vuex和vue的双向数据绑定...

网友评论

      本文标题:Vue数据双向绑定的原理以及实现

      本文链接:https://www.haomeiwen.com/subject/ocikeqtx.html