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 相关文章推荐
jQuery 阴影插件代码分享
Jan 09 Javascript
浅析jQuery(function(){})与(function(){})(jQuery)之间的区别
Jan 09 Javascript
JS使用post提交的两种方式
Dec 03 Javascript
JS基于HTML5的canvas标签实现炫目的色相球动画效果实例
Aug 24 Javascript
浅谈javascript中遇到的字符串对象处理
Nov 18 Javascript
javascript和jQuery中的AJAX技术详解【包含AJAX各种跨域技术】
Dec 15 Javascript
JavaScript中Promise的使用详解
Feb 26 Javascript
node.js中fs.stat与fs.fstat的区别详解
Jun 01 Javascript
小程序视频列表中视频的播放与停止的示例代码
Jul 20 Javascript
vue项目中使用fetch的实现方法
Apr 25 Javascript
微信小程序的线程架构【推荐】
May 14 Javascript
Vue 中 a标签上href无法跳转的解决方式
Nov 12 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
linux下为php添加curl扩展的方法
2011/07/29 PHP
php模板函数 正则实现代码
2012/10/15 PHP
详解PHP序列化反序列化的方法
2015/10/27 PHP
学习php设计模式 php实现策略模式(strategy)
2015/12/07 PHP
php cookie工作原理与实例详解
2016/07/18 PHP
PHP基于自定义类随机生成姓名的方法示例
2017/08/05 PHP
Google Map API更新实现用户自定义标注坐标
2009/07/29 Javascript
JavaScript DOM学习第四章 getElementByTagNames
2010/02/19 Javascript
Jquery 1.42 checkbox 全选和反选代码
2010/03/27 Javascript
js实现字符串的16进制编码不加密
2014/04/25 Javascript
JavaScript面对国际化编程时的一些建议
2015/06/24 Javascript
jQuery检测返回值的数据类型
2015/07/13 Javascript
jQuery网页右侧广告跟随滚动代码分享
2020/04/20 Javascript
Js获取当前日期时间及格式化代码
2016/09/17 Javascript
Bootstrap笔记—折叠实例代码
2017/03/13 Javascript
vue项目常用组件和框架结构介绍
2017/12/24 Javascript
Java Varargs 可变参数用法详解
2020/01/28 Javascript
js 数组当前行添加数据方法详解
2020/07/28 Javascript
[52:15]2014 DOTA2国际邀请赛中国区预选赛5.21 HGT VS LGD-GAMING
2014/05/23 DOTA
[05:05]给小松五分钟系列 第二期介绍为什么打DOTA2
2014/07/02 DOTA
python如何为创建大量实例节省内存
2018/03/20 Python
django搭建项目配置环境和创建表过程详解
2019/07/22 Python
python django中8000端口被占用的解决
2019/12/17 Python
Pycharm如何自动生成头文件注释
2020/11/14 Python
用CSS3将你的设计带入下个高度
2009/08/08 HTML / CSS
ORACLE第二个十问
2013/12/14 面试题
财务经理的岗位职责
2013/12/17 职场文书
班主任评语大全
2014/04/26 职场文书
大学优秀班主任事迹材料
2014/05/02 职场文书
人力资源管理专业自荐信
2014/06/24 职场文书
学校关爱留守儿童活动方案
2014/08/27 职场文书
美容院合作经营协议书
2014/10/10 职场文书
优秀的商业计划书,让融资一步到位
2019/05/07 职场文书
如何解决.cuda()加载用时很长的问题
2021/05/24 Python
MySql存储过程之逻辑判断和条件控制
2021/05/26 MySQL
Python利用FlashText算法实现替换字符串
2022/03/31 Python