Vue双向绑定实现原理与方法详解


Posted in Javascript onMay 07, 2020

本文实例讲述了Vue双向绑定实现原理与方法。分享给大家供大家参考,具体如下:

昨天接到一个电话面试,上来第一个问题就是Vue双向绑定的原理。当时我并不知道如何监听数据层到视图层的变化,于是没答上来,挂电话后,我赶忙查了下资料,主要思路有如下三种。

1.发布者-订阅者模式(backbone.js)

思路:使用自定义的data属性在HTML代码中指明绑定。所有绑定起来的JavaScript对象以及DOM元素都将“订阅”一个发布者对象。任何时候如果JavaScript对象或者一个HTML输入字段被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素。

2.脏值检查(angular.js)

思路:angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,angular只有在指定的事件触发时进入脏值检测,大致如下:

  • DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
  • XHR响应事件 ( $http )
  • 浏览器Location变更事件 ( $location )
  • Timer事件( $timeout , $interval )
  • 执行 $digest() 或 $apply()

3.数据劫持(Vue.js)

思路: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

Vue双向绑定实现原理与方法详解

Object.defineProperty():方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

var obj = {};
  Object.defineProperty(obj, 'hello', {
    get: function() {
      console.log('get val:'+ val);
      return val;
     },
  set: function(newVal) {
      val = newVal;
      console.log('set val:'+ val);
    }
  });
obj.hello='111';//控制台打印set val:111
obj.hello; //控制台打印get val:111

当获取hello属性时,触发get;设置hello值时,触发set;这就是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>v-model</title>
</head>

<body>
<div id='app'>
<h2>{{title}}</h2>
<input id='i' v-model='text' type="text">
<h1>{{text}}</h1>
<button v-on:click='clickMe'>click me</button>
</div>
<script>
//Dom类 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图
//并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
class Doms {
constructor(node, vm) {
if (node) {
this.$frag = this.nodeToFragment(node, vm)
return this.$frag
}
}
nodeToFragment(node, vm) {//将dom转换成fragment
var frag = document.createDocumentFragment()
var child;
while (child = node.firstChild) {
this.compileElement(child, vm)
frag.appendChild(child)
}
return frag
}
compileElement(node, vm) {//获取v-model属性,给dom赋值
var reg = /\{\{(.*)\}\}/ //匹配双括号里面的任何字符
if (node.nodeType === 1) {//element元素
var attr = node.attributes;
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue//获取绑定的key
node.addEventListener('input', function (e) {
vm[name] = e.target.value//触发set方法
})
new Watcher(vm, node, name, 'value')
} else if (attr[i].nodeName.includes(':')) {
var eventType = attr[i].nodeName.split(':')[1]//事件名
var cb = vm.methods && vm.methods[attr[i].nodeValue]
if (eventType && cb) {
node.addEventListener(eventType, cb.bind(vm), false)
}
}
}
if (node.childNodes && node.childNodes.length) {//如果还有子节点 递归
[...node.childNodes].forEach(n => this.compileElement(n, vm))
}
}
if (node.nodeType === 3) {//text
if (reg.test(node.nodeValue)) {
var name = RegExp.$1
name = name.trim()
new Watcher(vm, node, name, 'nodeValue')
}
}
}
}
class Vue {//Vue类
constructor(params) {
this.data = params.data //获取属性
this.methods = params.methods //获取方法
this.observe(params.data, this)//监听属性
var id = params.el;
var dom = new Doms(document.getElementById(id), this)
document.getElementById(id).appendChild(dom)
params.mounted.call(this)
}
observe(obj, vm) {//读取data内属性,并监听
if (!obj || typeof obj !== 'object') return
Object.keys(obj).forEach(key => this.defineReactive(vm, key, obj[key]))
}
defineReactive(obj, key, val) {//利用Object.defineProperty监听属性改变
var dep = new Dep()
Object.defineProperty(obj, key, {
get: function () {
if (Dep.target) {//添加订阅者watcher到主题对象Dep
dep.addSub(Dep.target)
}
return val
},
set: function (newVal) {
if (newVal === val) return
val = newVal
console.log(val)
//作为发布者发布通知
dep.notify()
}
})
}

}
class Dep {//收集订阅者的容器类
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => sub.update())
}
}

class Watcher {
constructor(vm, node, name, type) {
Dep.target = this
this.name = name
this.node = node
this.vm = vm
this.type = type
this.update()
Dep.target = null
}
update() {
this.get()
this.node[this.type] = this.value//订阅者执行响应操作
}
get() {
this.value = this.vm[this.name]//触发响应属性的get
}
}

var vm = new Vue({
el: 'app',
data: {
text: 'lyl',
title: 'hello world'
},
methods: {
clickMe() {
this.title = 'hello world'
}
},
mounted() {
setTimeout(() => {
this.title = '你好'
}, 1000);
}
})
</script>
</body>
</html>

GitHub地址:https://github.com/ChrisLuckComes/Vue2WayBind

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.3water.com/code/HtmlJsRun测试上述代码运行效果。

希望本文所述对大家JavaScript程序设计有所帮助。

Javascript 相关文章推荐
javascript实现的动态文字变换
Jul 28 Javascript
用js实现的检测浏览器和系统的函数
Apr 09 Javascript
jQuery实现公告文字左右滚动的实例代码
Oct 29 Javascript
juery框架写的弹窗效果适合新手
Nov 27 Javascript
浅谈JavaScript中的String对象常用方法
Feb 25 Javascript
JavaScript模拟重力状态下抛物运动的方法
Mar 03 Javascript
基于JavaScript代码实现微信扫一扫下载APP
Dec 30 Javascript
最全面的百度地图JavaScript离线版开发
Sep 10 Javascript
vue-router路由参数刷新消失的问题解决方法
Jun 17 Javascript
jQuery初级教程之网站品牌列表效果
Aug 02 jQuery
vue-router+vuex addRoutes实现路由动态加载及菜单动态加载
Sep 28 Javascript
详解vue组件开发脚手架
Jun 15 Javascript
JavaScript设计模式之观察者模式与发布订阅模式详解
May 07 #Javascript
微信小程序pinker组件使用实现自动相减日期
May 07 #Javascript
简单了解JavaScript弹窗实现代码
May 07 #Javascript
angular组件间传值测试的方法详解
May 07 #Javascript
Node.js API详解之 timer模块用法实例分析
May 07 #Javascript
JS面试题中深拷贝的实现讲解
May 07 #Javascript
javascript 代码是如何被压缩的示例代码
May 06 #Javascript
You might like
修改apache配置文件去除thinkphp url中的index.php
2014/01/17 PHP
php实例分享之通过递归实现删除目录下的所有文件详解
2014/05/15 PHP
深入理解PHP中的global
2014/08/19 PHP
Laravel实现短信注册的示例代码
2018/05/29 PHP
IE浏览器兼容Firefox的JS脚本的代码
2008/10/23 Javascript
javascript 网页跳转的方法
2008/12/24 Javascript
js控制href内容的连接内容的变化示例
2014/04/30 Javascript
JavaScript动态改变表格单元格内容的方法
2015/03/30 Javascript
AngularJS基础 ng-hide 指令用法及示例代码
2016/08/01 Javascript
jQuery中clone()函数实现表单中增加和减少输入项
2017/05/13 jQuery
bootstrap精简教程_动力节点Java学院整理
2017/07/14 Javascript
详解Web使用webpack构建前端项目
2017/09/23 Javascript
JS常见DOM节点操作示例【创建 ,插入,删除,复制,查找】
2018/05/14 Javascript
浅谈Vue render函数在ElementUi中的应用
2018/09/06 Javascript
vue中,在本地缓存中读写数据的方法
2018/09/21 Javascript
vue实现的微信机器人聊天功能案例【附源码下载】
2019/02/18 Javascript
js继承的这6种方式!(上)
2019/04/23 Javascript
在layui.use 中自定义 function 的正确方法
2019/09/16 Javascript
vue中使用router全局守卫实现页面拦截的示例
2020/10/23 Javascript
[02:51]2014DOTA2国际邀请赛 IG战队官方纪录片
2014/07/21 DOTA
Python数据拟合与广义线性回归算法学习
2017/12/22 Python
详解python while 函数及while和for的区别
2018/09/07 Python
Python 新建文件夹与复制文件夹内所有内容的方法
2018/10/27 Python
pytorch 常用函数 max ,eq说明
2020/06/28 Python
Python读取图像并显示灰度图的实现
2020/12/01 Python
Python解析m3u8拼接下载mp4视频文件的示例代码
2021/03/03 Python
软件生产职位结构化面试主要考察要素及面试题库
2015/06/12 面试题
数据员岗位职责
2013/11/19 职场文书
电子技术专业中专生的自我评价
2013/12/17 职场文书
厂长助理岗位职责
2013/12/27 职场文书
大学同学十年聚会感言
2014/02/21 职场文书
关于运动会广播稿200字
2014/10/08 职场文书
Nginx+Tomcat实现负载均衡、动静分离的原理解析
2021/03/31 Servers
golang goroutine顺序输出方式
2021/04/29 Golang
Python+Tkinter打造签名设计工具
2022/04/01 Python
Java Lambda表达式常用的函数式接口
2022/04/07 Java/Android