vue3实现v-model原理详解


Posted in Javascript onOctober 09, 2019

vue3 源码正式放出来了,想必大家也都开始争先恐后的学习 vue3 的知识了。由于 vue3 已经不再支持 v-model 了,而使用 .sync 来代替,但是为了这篇文章可以帮助大家快速了解 vue 的双向绑定实现原理,部分使用了 vue2.x v-model 的实现原理

proxy 的基础知识,相信大家已经都很了解了,让我们一起来回顾一下吧

proxy 是对一个对象的代理,并返回一个已代理的对象,已代理的对象如果发生任何 set 跟 get 的方法都可以被捕获到,我们写一个简单的 :chestnut:

const target = {
 a: 1
}
const handers = {
 get() {
  // 当对 observed.a 进行取值时会触发
 },
 set() {
  // 当对 observed.a 进行赋值时会触发
 },
 // 还有一些额外的参数如 has 等,这里用不到,就不多说了
 ....
}
const observed = new Proxy(target, handers)

这样我们就可以对 target 对象设置了一层代理,当我们对 target 进行取赋值操作的时候就可以接可以截获到它的行为了,但是如果你以为就只有这么简单你就错了。

我们把 target 改写成多层嵌套

const target = {
 a: {
  b: 1
 }
}

...

const observed = new Proxy(target, handers)

我们再获取 observed.a.b = 2 的时候,get 方法取到的是 a 的值 { b: 1 }, 而 set 并不会触发。这也说明了 proxy 只能代理一层对象,不能深层代理!

那么我们需要监听到嵌套的对象怎么办?

其实这个也不难,就是在 get 的时候判断一下得到的值是不是对象,如果是对象的话就 在对它代理一层,直到最后一层,全部代理完为止,这里就需要一个递归函数

const target = {
 a: {
  b: 1
 }
}

function reactive(data: any) {
 const handers = {
  get(target, key, receiver) {
   const res = Reflect.get(target, key, receiver);
   if (isObject(res)) {
    data[key] = reactive(res);
   }
   return target[key];
  }
 }
 const observed = new Proxy(target, handers)
}

这样我们就可以对目标函数内部的所有属性进行深层监听了,但是这样还是不够,因为我们每次取值的时候都会设置代理这样会导致代码无限循环->死循环,所以我们需要做一层判断,如果已经设置了代理的或这已经是代理的对象就不需要在此设置代理了。又因为我们要储存对象的映射,所以需要使用map函数。下面是reactive完整的代码。

const rawToReactive: WeakMap<any, any> = new WeakMap();
const reactiveToRaw: WeakMap<any, any> = new WeakMap();

function reactive(data: any) {
 // 已经有代理
 let observed = rawToReactive.get(data);
 if (observed !== void 0) {
  return observed;
 }
 // 这个数据已经是代理
 if (reactiveToRaw.has(data)) {
  return data;
 }
 const handler = {
  get: function(target: any, key: string, receiver: any) {
   const res = Reflect.get(target, key, receiver);
   if (isObject(res)) {
    data[key] = data[key] = reactive(res);
   }
   return target[key];
  },
  set: function(target: any, key: string, value: any) {
   // 将新值赋值
   target[key] = value;
   // 通知所有订阅者触发更新
   trigger(target);
   // 严格模式下需要设置返回值,否则会报错
   return value;
  }
 };
 // 返回代理监听对象
 observed = new Proxy(data, handler as any);
 rawToReactive.set(data, observed);
 reactiveToRaw.set(observed, data);

 return observed;
}

定义watcher 用来作为 compile 跟 reactive 的桥梁, 跟 vue3 的实现可能不一样

// 收集watcher依赖
const Dep: Dep = {
 deps: [],
 add(watcher: Watcher) {
  this.deps.push(watcher);
 }
};

// observer跟compile的桥梁,在编译时添加watcher,在数据更新时触发update更新视图
function _watcher(node: any, attr: string, data: any, key: string): Watcher {
 return {
  node,
  attr,
  data,
  key,
  update() {
   // 逐层取值
   const mutationKeys = this.key.split('.');
   if (mutationKeys.length > 1) {
    let d: any = null;
    mutationKeys.forEach(key => (d = this.data[key] || (d && d[key])));
    this.node[this.attr] = d;
    return;
   }
   this.node[this.attr] = this.data[this.key];
  }
 };
}

接下来是编译模板

这里只是模拟编译,真正的编译不是这样的

获取到模板上的 v-model 、 v-bind 属性,获取到绑定的属性。当数据发生变化时,更新视图(这里会在trigger进行触发),当视图改变数据时修改数据(为了简单,通过eval函数实现),具体代码如下

// 编译模板
function _compile(nodes: any, $data: any) {
 [...nodes].forEach((e, index) => {
  const theNode = nodes[index];
  // 获取到 input标签下的 v-model 属性,并添加watcher
  if (theNode.tagName === 'INPUT' && theNode.hasAttribute('v-model')) {
   const key = theNode.getAttribute('v-model');
   Dep.add(_watcher(theNode, 'value', $data, key));
   // 监听input事件
   theNode.addEventListener('input', () => {
    const mutationKeys = key.split('.');
    if (mutationKeys.length > 1) {
     eval(`$data.${key}='${theNode.value}'`);
     return;
    }
    $data[key] = theNode.value;
   });
  }
  // 获取 v-bind 属性,并添加watcher
  if (theNode.hasAttribute('v-bind')) {
   const key = theNode.getAttribute('v-bind');
   Dep.add(_watcher(theNode, 'innerHTML', $data, key));
  }
 });
 trigger($data);
}

trigger 对依赖进行触发

function trigger(target: any, key?: string | symbol) {
 Dep.deps.forEach((e: Watcher) => {
  e.update();
 });
}

使用效果

废话不多说。直接上代码!

假设我们有一个模板是这样的,接下来我们在这个模板的 id="my-app" 元素内实现双向绑定

<div id="my-app">
 <h1 v-bind="a"></h1>
 <input v-model="a" type="text">
</div>

vue3 中 new Vue 已经被 createApp 所代替,reactive 是反应原理,可以抽出来单独使用,vue3 外漏了所有内部的 api,都可以在外部使用

const { createApp, reactive } = require('./vue.ts').default;
const App = {
 setup() {
  const react = reactive({
   a: {
    b: {
     c: {
      d: {
       e: 111
      }
     }
    }
   }
  });
  // 测试异步反应
  setTimeout(() => {
   react.a.b.c.d.e = 222;
  }, 100);
  return react;
 }
};
createApp().mount(App, '#my-app');

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

Javascript 相关文章推荐
PHP+mysql+Highcharts生成饼状图
May 04 Javascript
编写高质量JavaScript代码的基本要点
Mar 02 Javascript
jQuery1.9+中删除了live以后的替代方法
Jun 17 Javascript
第一次动手实现bootstrap table分页效果
Sep 22 Javascript
Vue中计算属性computed的示例解读
Jul 26 Javascript
vue里面v-bind和Props 利用props绑定动态数据的方法
Aug 27 Javascript
nvm、nrm、npm 安装和使用详解(小结)
Jan 17 Javascript
JavaScript迭代器的含义及用法
Jun 21 Javascript
关于Vue中axios的封装实例详解
Oct 20 Javascript
js get和post请求实现代码解析
Feb 06 Javascript
如何在微信小程序中使用骨架屏的步骤
Jun 12 Javascript
javascript中闭包closure的深入讲解
Mar 03 Javascript
bootstrap+spring boot实现面包屑导航功能(前端代码)
Oct 09 #Javascript
使用Webpack提升Vue.js应用程序的4种方法(翻译)
Oct 09 #Javascript
微信小程序本地存储实现每日签到、连续签到功能
Oct 09 #Javascript
Vue.js实现大转盘抽奖总结及实现思路
Oct 09 #Javascript
js基础之事件捕获与冒泡原理
Oct 09 #Javascript
微信内置浏览器图片查看器的代码实例
Oct 08 #Javascript
vue封装swiper代码实例解析
Oct 08 #Javascript
You might like
PHP邮件专题
2006/10/09 PHP
PHP安装攻略:常见问题解答(二)
2006/10/09 PHP
php读取远程gzip压缩网页的方法
2014/12/29 PHP
yii,CI,yaf框架+smarty模板使用方法
2015/12/29 PHP
Thinkphp事务操作实例(推荐)
2017/04/01 PHP
JQuery浮动DIV提示信息并自动隐藏的代码
2010/08/29 Javascript
editable.js 基于jquery的表格的编辑插件
2011/10/24 Javascript
JS幻灯片可循环播放可平滑旋转带滚动导航(自写)
2013/08/05 Javascript
JS操作数据库的实例代码
2013/10/17 Javascript
script标签属性用type还是language
2015/01/21 Javascript
Jquery实现顶部弹出框特效
2015/08/08 Javascript
jquery Easyui快速开发总结
2015/08/20 Javascript
简单学习JavaScript中的for语句循环结构
2015/11/10 Javascript
JavaScript的removeChild()函数用法详解
2015/12/27 Javascript
JavaScript缓冲运动实现方法(2则示例)
2016/01/08 Javascript
jQuery Ajax 上传文件处理方式介绍(推荐)
2016/06/30 Javascript
bootstrap suggest下拉框使用详解
2017/04/10 Javascript
JS匿名函数和匿名自执行函数概念与用法分析
2018/03/16 Javascript
微信小程序 JS动态修改样式的实现方法
2018/12/16 Javascript
Node.js折腾记一:读指定文件夹,输出该文件夹的文件树详解
2019/04/20 Javascript
详解微信小程序之一键复制到剪切板
2019/04/24 Javascript
解决layui-table单元格设置为百分比在ie8下不能自适应的问题
2019/09/28 Javascript
JS实现简单tab选项卡切换
2019/10/25 Javascript
vue 指令和过滤器的基本使用(品牌管理案例)
2019/11/04 Javascript
搭建Python的Django框架环境并建立和运行第一个App的教程
2016/07/02 Python
Mac中Python 3环境下安装scrapy的方法教程
2017/10/26 Python
Python实现的线性回归算法示例【附csv文件下载】
2018/12/29 Python
深入了解Python枚举类型的相关知识
2019/07/09 Python
浅谈Django中view对数据库的调用方法
2019/07/18 Python
mac使用python识别图形验证码功能
2020/01/10 Python
Win10环境中如何实现python2和python3并存
2020/07/20 Python
专业见习报告范文
2014/11/03 职场文书
社区义诊通知
2015/04/24 职场文书
初中班级口号霸气押韵
2015/12/24 职场文书
三严三实·严以用权心得体会
2016/01/12 职场文书
Jpa Specification如何实现and和or同时使用查询
2021/11/23 Java/Android