VUE双向绑定原理实践

栏目: JavaScript · 发布时间: 5年前

内容简介:近几年,前端框架层出不穷,在技术瞬息万变的时代里,关注JS语言本身,探究一些框架底层实现原理也许会让我们走得更深更远。下面是自己看vue源码的一些理解和实践,主要是对vue双向绑定原理和观察者模式做了一些实践,以v-model为例。整个过程分为以下几步:主要涉及到几个对象:

近几年,前端框架层出不穷,在技术瞬息万变的时代里,关注JS语言本身,探究一些框架底层实现原理也许会让我们走得更深更远。下面是自己看vue源码的一些理解和实践,主要是对vue双向绑定原理和观察者模式做了一些实践,以v-model为例。

双向绑定原理解析

整个过程分为以下几步:

1. compile:vue对template模板中进行编译,编译成真正的html,在编译的过程中对vue的指令解析
2. observe:在对template编译的过程中,对一些v-model,{{}}之类的指令使用在data中定义的变量来初始化,同时开启一个对该变量的watcher,相当于开启观察者模式,发布者是data中我们定义的变量,订阅者是我们需要更新的dom视图。
3. 发布者data中的变量改变,通知订阅者做相关操作,更新dom视图。
复制代码

主要涉及到几个对象:

1. 模拟vue的原型
2. 观察者原型watcher
3. Dep对象,封装了对订阅者的操作,一个data中的变量对应不同的watcher,这些watcher都存在在一个dep对象的数组中。
复制代码

对象原型解析

  • Dep 对订阅者的操作可以抽象成一个Dep的原型。这里的对象属性subscriber是订阅者的数组,target是每次要加入subscriber中的目标订阅者,每次都只能加一个target到subscriber中。
function Dep(cb) {
            this.subscriber = []
            this.target = null
        }
        Dep.prototype = {
            addSub(sub) {
                // 加入到订阅者数组中
                this.subscriber.push(sub)
            },
            notify() {
                // 
                this.subscriber.forEach((item) => {
                    item.update()
                })
            }
        }
复制代码
  • 然后,我们再来抽象一个watcher对象。exp是我们在data中定义的变量,cb是订阅者要执行的回调函数。get获取的是变量的值,update对应的当变量值发生更新时,执行订阅者的cb函数。初始化一个watcher的时候,调用get可把该watcher加入到订阅者的数组里。
function Watcher(vm, exp, cb) {
            this.vm = vm
            this.exp = exp
            this.cb = cb
            // 添加到订阅者列表中
            this.value = this.get()
        }
        Watcher.prototype = {
            get() {
                Dep.target = this
                let value = this.vm.$data[this.exp]
                Dep.target = null
                return value
            },
            update() {
                let oldVal = this.value
                let newVal = this.vm.$data[this.exp]
                if (newVal !== oldVal ) {
                    console.log('更新', this.exp)
                    this.value = newVal
                    this.cb.call(this.vm, newVal)
                }
            }
        }
复制代码
  • 最后,我们先模拟一个vue的原型。每一个data中的变量对应一个Dep对象,用于收集这个变量所对应的所有订阅者。这里,使用了原生JS中的Object.defineProperty方法,当get的时候把相关订阅者加入到dep对象中,当set的时候通知订阅者执行相关回调。
function Vue(options) {
            let vm = this
            vm._init(options)

            //开启观察者模式观察数据变化
            vm._observe(options.data)

            // 编译dom
            vm._compile()
        }
        Vue.prototype._init = function(options) {
            var vm = this
            
            vm.$el = options.el
            vm.$data = options.data 
        }
        Vue.prototype._observe = function(data) {
            Object.entries(data).forEach(([key, value]) => {
                var dep = new Dep()
                let property = Object.getOwnPropertyDescriptor(data, key)

                if (property && !property.configurable) {
                    return
                }
                let getter = property && property.get
                let setter = property && property.set

                Object.defineProperty(data, key, {
                    enumerable: true,
                    configurable: true,
                    get() {
                        let val = getter ? getter.call(data) : value
                        // get时候添加订阅者
                        if (Dep.target) {
                            dep.addSub(Dep.target)
                        }
                        
                        return val
                    },
                    set(newValue) {
                        let val = getter ? getter.call(data) : value
                        // 脏检查,排除NaN !== NaN
                        if (newValue === val || (newValue !== newValue)) {
                            return
                        }
                        if (setter) {
                            setter.call(data, newValue)
                        } else {
                            value = newValue
                        }
                        // 通知订阅者
                        dep.notify()
                    }
                })
            })
        }
        Vue.prototype._compile = function() {
            let rootNode = document.querySelector(this.$el)
            let compile = (rootNode) => {
                if (rootNode.childNodes) {
                    Array.prototype.forEach.call(rootNode.childNodes, (node) => {
                        if (node.attributes && node.attributes.hasOwnProperty('v-model')) {
                            compileVmodel(this, node)
                        }
                        if (/{{.*}}/.test(node.innerHTML)) {
                            compileBrace(this, node)
                        }
                        if (node.childNodes) {
                            compile(node)
                        }
                    }) 
                }
            }
            compile(rootNode)
        }
        function compileVmodel(vm, node) {
            // 检测到有v-model属性,则添加对应watcher
            let exp = node.getAttribute('v-model')
            new Watcher(vm, exp, (val) => {
                node.setAttribute('v-model', val)
            })
            // 监听input事件
            node.addEventListener('input', () => {
                if (node.value !== vm.$data[exp]) {
                    vm.$data[exp] = node.value
                }
            }, false)
        }
复制代码

这里用到了两个函数compileVmodel和compileBrace,只是作为模拟,实际vue解析的过程中用到了AST。

function compileVmodel(vm, node) {
            // 检测到有v-model属性,则添加对应watcher
            let exp = node.getAttribute('v-model')
            new Watcher(vm, exp, (val) => {
                node.setAttribute('v-model', val)
            })
            // 监听input事件
            node.addEventListener('input', () => {
                if (node.value !== vm.$data[exp]) {
                    vm.$data[exp] = node.value
                }
            }, false)
        }
        function compileBrace(vm, node) {
            // 解析{{}}中值
            let exp = node.innerHTML.match(/{{(.*)}}/)[1]
            console.log('compileBrace', exp)
            new Watcher(vm, exp, (val) => {
                console.log('新的值:', node)
                // let innerHTML = node.innerHTML.replace(/{{.*}}/g, val)
                node.textContent = val
                // console.log(innerHTML)
            })
        }
复制代码

验证和实践

以上就完成了对vue双向绑定原理的简单建模,现在写段代码来实践验证下

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>vue双向绑定原理实践</title>
</head>
<body>
    <div id="app">
        <input type="testVmodel" name="testVmodel" v-model="inputValue">
        <p>{{inputValue}}</p>
    </div>

    <script type="text/javascript"> 
        // 这里是引入上面的原型

        window.onload = (function(window) {
            let app = new Vue({
                el: '#app',
                data: {
                    inputValue: '初始化inputValue值'
                }
            })
            // 开启观察者模式监测 inputValue变化
            new Watcher(app, 'inputValue', function(value) {
                let inputEl = document.querySelector('input')
                inputEl.value = value
            })
            app.$data.inputValue = '测试更新'
            setTimeout(() => {
                app.$data.inputValue = '测试更新2'
            }, 1000)
        })(window)
    </script>
</body>
</html>
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Software Paradigms

Software Paradigms

Stephen H. Kaisler / Wiley-Interscience / 2005-03-17 / USD 93.95

Software Paradigms provides the first complete compilation of software paradigms commonly used to develop large software applications, with coverage ranging from discrete problems to full-scale applic......一起来看看 《Software Paradigms》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具