VUE双向绑定原理实践

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

内容简介:近几年,前端框架层出不穷,在技术瞬息万变的时代里,关注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>
复制代码

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

查看所有标签

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

Powerful

Powerful

Patty McCord / Missionday / 2018-1-25

Named by The Washington Post as one of the 11 Leadership Books to Read in 2018 When it comes to recruiting, motivating, and creating great teams, Patty McCord says most companies have it all wrong. Mc......一起来看看 《Powerful》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

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

Base64 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具