奇步互动技术分享会

Vue双向绑定原理与实现

2019-07-22  本文已影响84人  charoner

前言

实现Vue的数据的双向绑定 是采用数据劫持结合发布者-订阅者模式的方式 通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调 需要实现以下几点:
1 实现一个 Observer 观察者对data中的数据进行监听,若有变化,通知相应的订阅者
2 实现一个 Compile 编译器将页面中子节点拷贝到DocumentFragment对象中,然后对每个元素节点的指令进行扫描与解析
3 实现一个 Watcher 用来连接ObserverCompile,并为每个属性绑定相应的订阅者,当数据发生变化时,执行相应的回调函数,从而更新视图。
4 new MVVM的类实例对象 作为入口整合使用

双向绑定.png

准备工作

下面是文件目录,由于浏览器不支持 es6 的export default / import 语法所以用webpack来打包解析es6语法


711563808099_.pic.jpg

实现vue的类

vue.js

import Observer from './Observer' //观察者
import Compiler from './Compiler' //编译模版

class Vue {
    constructor(options) {
        this.$options = options
        //保存挂载元素的ID
        this.$el = this.$options.el
        //保存实例对象中的data对象数据
        this._data = this.$options.data
        //遍历属性添加数据代理
        Object.keys(this._data).forEach(key => {
            this._proxy(key)
        })
        //通过数据劫持实现双向绑定 通知发布订阅
        new Observer(this._data)
        //模版指令解析
        new Compiler(this.$el, this)
    }
    //将实例对象上的属性 映射到内部_data对象属性上面 (实现 MVVM.xxx => MVVM._data.xxx)
    _proxy(key) {
        //属性描述符
        Object.defineProperty(this, key, {
            //读取属性值时调用
            get() {
                //返回_data 中属性名的对应的属性值
                return this._data[key]
            },
            //监听设置属性调用
            set(value) {
                //赋值到_data属性名对应的属性值
                this._data[key] = value
            }
        })
    }
}

export default Vue

实现Observer 观察者

Observer.js

import Dep from "./Dep";

class Observer {
    constructor(data) {
        //保存数据
        this.data = data

        //对数据中所有属性设置setter getter 通过数据劫持实现双向绑定
        Object.keys(data).forEach(key => {
            this._bind(data, key, data[key])
        })
    }
    _bind(data, key, value) {
        //new Dep对象 保存订阅者 遍历调用订阅者的update对象 (myDep实例对象与data中的属性一一对应)
        let myDep = new Dep()

        //属性描述符
        Object.defineProperty(data, key, {
            //获取属性值
            get() {
                //获取属性值 判断是否存在wathcer实例对象 存在将其保存到Dep当中
                if (Dep.target) myDep.listen(Dep.target)
                return value
            },
            //监视属性值的变化
            set(newValue) {
                if (newValue === value) return
                //赋值
                value = newValue

                //观察者改变完数据 通知发布订阅
                myDep.notify()
            }
        })
    }
}

export default Observer

发布订阅

Dep .js

class Dep {
    constructor() {
        this.target = null
        this.list = []
    }
    //添加订阅者 watcher
    listen(subs) {
        this.list.push(subs)
    }
    //调用watcher中的update方法更新视图
    notify() {
        this.list.forEach(item => {
            item.update()
        })
    }
}
export default Dep

实现Compile

Compiler.js

import Watcher from "./Watcher";
const reg = /\{\{(.*)\}\}/
class Compiler {
    constructor(el, vm) {
        //保存根据获取到ID对应的页面节点对象
        this.$el = document.querySelector(el)
        //保存当前的MVVM对象
        this.$vm = vm
        //将页面中的子节点转移到fragment中
        this.fragment = this.createFragment()
        //将fragment 转移回到页面当中
        this.$el.appendChild(this.fragment)
    }
    createFragment() {
        //创建Dom碎片对象
        let fragment = document.createDocumentFragment(), child
        //将原生节点拷贝到fragment
        while (child = this.$el.firstChild) {
            //编译元素节点
            this.compileElement(child)
            //将节点加入fragment对象
            fragment.appendChild(child)
        }
        return fragment
    }
    //编译元素节点
    compileElement(node) {
        //元素节点
        if (node.nodeType == 1) {
            //获取当前元素节点的属性值数组
            let attr = node.attributes
            //判断属性值数组中是否存在v-model
            if (attr.hasOwnProperty('v-model')) {
                let self = this
                //获取属性为 v-model 的元素节点 并获取节点值
                let name = attr['v-model'].nodeValue
                //初始化显示输入框内容
                node.value = this.$vm[name]

                //给输入框添加input事件监听
                node.addEventListener('input', function(e){
                    self.$vm[name] = e.target.value
                })
            }
        }
        //文本节点
        if (node.nodeType == 3) {
            //判断是否符合 双大括号表达式 {{ }}
            if (reg.test(node.nodeValue)) {
                //获取表达式内容
                let name = RegExp.$1
                //去空格
                name = name.trim()

                //new一个Watcher对象更新双大括号表达式内容
                new Watcher(node, name, this.$vm)
            }
        }
    }
}
export default Compiler

实现Watcher

Watcher.js

import Dep from './Dep'

//页面上所有订阅数据的地方 (watcher 与页面的表达式一一对应)
class Watcher {
    constructor(node, name, vm) {
        this.node = node
        this.name = name
        this.vm = vm

        //将Dep.target 赋值为 this (Watcher的示例对象)
        Dep.target = this
        //调用update() 方法更新节点数据
        this.update()
        Dep.target = null
    }
    update() {
        //将当前的标签节点下的值改成 vm对象中对应属性的值
        //触发Observer中对MVVM对象监听的name属性添加的get属性 从而将当前的{{}}的表达式注入到 Dep中
        this.node.nodeValue = this.vm[this.name]
    }
}

export default Watcher

vue实例对象

main.js入口文件

import Vue from './vue'

const MVVM = new Vue({
   el: "#app",
   data: {
       name: 'hello world',
   }
})  
window.MVVM = MVVM

HTML代码

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-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="name" />
        {{ name }}
    </div>
    <script src="./dist/main.js"></script>
</body>
</html>

总结

至此就实现了一个简单的数据双向绑定的例子,主要的目的是为了更好的理解双向绑定的实现原理与设计思想

上一篇下一篇

猜你喜欢

热点阅读