vue.js响应式原理解析与实现


Posted in Javascript onJune 22, 2020

从很久之前就已经接触过了angularjs了,当时就已经了解到,angularjs是通过脏检查来实现数据监测以及页面更新渲染。之后,再接触了vue.js,当时也一度很好奇vue.js是如何监测数据更新并且重新渲染页面。今天,就我们就来一步步解析vue.js响应式的原理,并且来实现一个简单的demo。

首先,先让我们来了解一些基础知识。

基础知识

Object.defineProperty

es5新增了Object.defineProperty这个api,它可以允许我们为对象的属性来设定getter和setter,从而我们可以劫持用户对对象属性的取值和赋值。比如以下代码:

const obj = {
};

let val = 'cjg';
Object.defineProperty(obj, 'name', {
 get() {
 console.log('劫持了你的取值操作啦');
 return val;
 },
 set(newVal) {
 console.log('劫持了你的赋值操作啦');
 val = newVal;
 }
});

console.log(obj.name);
obj.name = 'cwc';
console.log(obj.name);

我们通过Object.defineProperty劫持了obj[name]的取值和赋值操作,因此我们就可以在这里做一些手脚啦,比如说,我们可以在obj[name]被赋值的时候触发更新页面操作。

发布订阅模式

发布订阅模式是设计模式中比较常见的一种,其中有两个角色:发布者和订阅者。多个订阅者可以向同一发布者订阅一个事件,当事件发生的时候,发布者通知所有订阅该事件的订阅者。我们来看一个例子了解下。

class Dep {
 constructor() {
 this.subs = [];
 }
 // 增加订阅者
 addSub(sub) {
 if (this.subs.indexOf(sub) < 0) {
 this.subs.push(sub);
 }
 }
 // 通知订阅者
 notify() {
 this.subs.forEach((sub) => {
 sub.update();
 })
 }
}

const dep = new Dep();

const sub = {
 update() {
 console.log('sub1 update')
 }
}

const sub1 = {
 update() {
 console.log('sub2 update');
 }
}

dep.addSub(sub);
dep.addSub(sub1);

dep.notify(); // 通知订阅者事件发生,触发他们的更新函数

动手实践

我们了解了Object.defineProperty和发布订阅者模式后,我们不难可以想到,vue.js是基于以上两者来实现数据监听的。

1、vue.js首先通过Object.defineProperty来对要监听的数据进行getter和setter劫持,当数据的属性被赋值/取值的时候,vue.js就可以察觉到并做相应的处理。
2、通过订阅发布模式,我们可以为对象的每个属性都创建一个发布者,当有其他订阅者依赖于这个属性的时候,则将订阅者加入到发布者的队列中。利用Object.defineProperty的数据劫持,在属性的setter调用的时候,该属性的发布者通知所有订阅者更新内容。

接下来,我们来动手实现(详情可以看注释):

class Observer {
 constructor(data) {
 // 如果不是对象,则返回
 if (!data || typeof data !== 'object') {
 return;
 }
 this.data = data;
 this.walk();
 }

 // 对传入的数据进行数据劫持
 walk() {
 for (let key in this.data) {
 this.defineReactive(this.data, key, this.data[key]);
 }
 }
 // 创建当前属性的一个发布实例,使用Object.defineProperty来对当前属性进行数据劫持。
 defineReactive(obj, key, val) {
 // 创建当前属性的发布者
 const dep = new Dep();
 /*
 * 递归对子属性的值进行数据劫持,比如说对以下数据
 * let data = {
 * name: 'cjg',
 * obj: {
 * name: 'zht',
 * age: 22,
 * obj: {
 * name: 'cjg',
 * age: 22,
 * }
 * },
 * };
 * 我们先对data最外层的name和obj进行数据劫持,之后再对obj对象的子属性obj.name,obj.age, obj.obj进行数据劫持,层层递归下去,直到所有的数据都完成了数据劫持工作。
 */
 new Observer(val);
 Object.defineProperty(obj, key, {
 get() {
 // 若当前有对该属性的依赖项,则将其加入到发布者的订阅者队列里
 if (Dep.target) {
  dep.addSub(Dep.target);
 }
 return val;
 },
 set(newVal) {
 if (val === newVal) {
  return;
 }
 val = newVal;
 new Observer(newVal);
 dep.notify();
 }
 })
 }
}

// 发布者,将依赖该属性的watcher都加入subs数组,当该属性改变的时候,则调用所有依赖该属性的watcher的更新函数,触发更新。
class Dep {
 constructor() {
 this.subs = [];
 }

 addSub(sub) {
 if (this.subs.indexOf(sub) < 0) {
 this.subs.push(sub);
 }
 }

 notify() {
 this.subs.forEach((sub) => {
 sub.update();
 })
 }
}

Dep.target = null;

// 观察者
class Watcher {
 /**
 *Creates an instance of Watcher.
 * @param {*} vm
 * @param {*} keys
 * @param {*} updateCb
 * @memberof Watcher
 */
 constructor(vm, keys, updateCb) {
 this.vm = vm;
 this.keys = keys;
 this.updateCb = updateCb;
 this.value = null;
 this.get();
 }

 // 根据vm和keys获取到最新的观察值
 get() {
 Dep.target = this;
 const keys = this.keys.split('.');
 let value = this.vm;
 keys.forEach(_key => {
 value = value[_key];
 });
 this.value = value;
 Dep.target = null;
 return this.value;
 }

 update() {
 const oldValue = this.value;
 const newValue = this.get();
 if (oldValue !== newValue) {
 this.updateCb(oldValue, newValue);
 }
 }
}

let data = {
 name: 'cjg',
 obj: {
 name: 'zht',
 },
};

new Observer(data);
// 监听data对象的name属性,当data.name发现变化的时候,触发cb函数
new Watcher(data, 'name', (oldValue, newValue) => {
 console.log(oldValue, newValue);
})

data.name = 'zht';

// 监听data对象的obj.name属性,当data.obj.name发现变化的时候,触发cb函数
new Watcher(data, 'obj.name', (oldValue, newValue) => {
 console.log(oldValue, newValue);
})

data.obj.name = 'cwc';
data.obj.name = 'dmh';

结语

这样,一个简单的响应式数据监听就完成了。当然,这个也只是一个简单的demo,来说明vue.js响应式的原理,真实的vue.js源码会更加复杂,因为加了很多其他逻辑。

接下来我可能会将其与html联系起来,实现v-model、computed和{{}}语法。代码地址 有兴趣的欢迎来一起研究探讨下。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript实现页面滚动图片加载(仿lazyload效果)
Jul 22 Javascript
jWiard 基于JQuery的强大的向导控件介绍
Oct 28 Javascript
javascript中的循环语句for语句深入理解
Apr 04 Javascript
更快的异步执行(setTimeout多浏览器)
Aug 12 Javascript
页面向下滚动ajax获取数据的实现方法(兼容手机)
May 24 Javascript
让浏览器崩溃的12行JS代码(DoS攻击分析及防御)
Oct 10 Javascript
js实现定时进度条完成后切换图片
Jan 04 Javascript
canvas压缩图片转换成base64格式输出文件流
Mar 09 Javascript
开源一个微信小程序仪表盘组件过程解析
Jul 30 Javascript
element-ui树形控件后台返回的数据+生成组织树的工具类
Mar 05 Javascript
Vue实现点击导航栏当前标签后变色功能
Aug 19 Javascript
解决ant design vue中树形控件defaultExpandAll设置无效的问题
Oct 26 Javascript
JavaScript Canvas实现验证码
Aug 02 #Javascript
JS实现方形抽奖效果
Aug 27 #Javascript
对vuejs的v-for遍历、v-bind动态改变值、v-if进行判断的实例讲解
Aug 27 #Javascript
Vuex 在Vue 组件中获得Vuex 状态state的方法
Aug 27 #Javascript
JS实现匀速与减速缓慢运动的动画效果封装示例
Aug 27 #Javascript
vue展示dicom文件医疗系统的实现代码
Aug 27 #Javascript
vue中子组件的methods中获取到props中的值方法
Aug 27 #Javascript
You might like
发布一个迷你php+AJAX聊天程序[聊天室]提供下载
2007/07/21 PHP
php发送get、post请求的6种方法简明总结
2014/07/08 PHP
php实现微信公众号无限群发
2015/10/11 PHP
php实现学生管理系统
2020/03/21 PHP
JavaScript 事件冒泡简介及应用
2010/01/11 Javascript
海量经典的jQuery插件集合
2010/01/12 Javascript
JavaScript动态调整TextArea高度的代码
2010/12/28 Javascript
在ASP.NET中使用JavaScript脚本的方法
2013/11/12 Javascript
js身份证判断方法支持15位和18位
2014/03/18 Javascript
javascript中Function类型详解
2015/04/28 Javascript
让图片跳跃起来  javascript图片轮播特效
2016/02/16 Javascript
全面解析Bootstrap中tooltip、popover的使用方法
2016/06/13 Javascript
jQuery实现的手风琴侧边菜单效果
2017/03/29 jQuery
nodejs入门教程五:连接数据库的方法分析
2017/04/24 NodeJs
在百度搜索结果中去除掉一些网站的资料(通过js控制不让显示)
2017/05/02 Javascript
bootstrap table方法之expandRow-collapseRow展开或关闭当前行数据
2020/08/09 Javascript
JavaScript简单实现关键字文本搜索高亮显示功能示例
2018/07/25 Javascript
javascript中一些奇葩的日期换算方法总结
2018/11/14 Javascript
nodejs微信开发之自动回复的实现
2019/03/17 NodeJs
VUE 实现动态给对象增加属性,并触发视图更新操作示例
2019/11/29 Javascript
[01:34]传奇从这开始 2016国际邀请赛中国区预选赛震撼开启
2016/06/26 DOTA
Python反射用法实例简析
2017/12/22 Python
python单例模式获取IP代理的方法详解
2018/09/13 Python
pytorch构建多模型实例
2020/01/15 Python
python numpy库linspace相同间隔采样的实现
2020/02/25 Python
浅谈CSS3特性查询(Feature Query: @supports)功能简介
2017/07/31 HTML / CSS
JBL澳大利亚官方商店:扬声器、耳机和音响系统
2018/05/24 全球购物
香港士多网上超级市场:Ztore
2021/01/09 全球购物
日期和时间问题
2015/01/04 面试题
如何用Java判断一个文件或目录是否存在
2012/11/19 面试题
社区志愿者心得体会
2014/01/03 职场文书
教师演讲稿大全
2014/05/16 职场文书
党的群众路线教育实践活动整改落实情况自查报告
2014/10/28 职场文书
mybatis调用sqlserver存储过程返回结果集的方法
2021/05/08 SQL Server
海康机器人重磅发布全新算法开发平台VM4.2
2022/04/21 数码科技
win sever 2022如何占用操作主机角色
2022/06/25 Servers