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 相关文章推荐
一段实时更新的时间代码
Jul 07 Javascript
js程序中美元符号$是什么
Jun 05 Javascript
Javascript(AJAX)解析XML的代码(兼容FIREFOX/IE)
Jul 11 Javascript
js取滚动条的尺寸的函数代码
Nov 30 Javascript
JS获取节点的兄弟,父级,子级元素的方法
Jan 09 Javascript
动态的创建一个元素createElement及删除一个元素
Jan 24 Javascript
在AngularJS中如何使用谷歌地图把当前位置显示出来
Jan 25 Javascript
基于JS实现移动端访问PC端页面时跳转到对应的移动端网页
Dec 24 Javascript
深入理解react-router@4.0 使用和源码解析
May 23 Javascript
vuejs 制作背景淡入淡出切换动画的实例
Sep 01 Javascript
three.js 制作动态二维码的示例代码
Jul 31 Javascript
vue使用过滤器格式化日期
Jan 20 Vue.js
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详细彻底学习Smarty
2008/03/27 PHP
PHP中几种常见的超时处理全面总结
2012/09/11 PHP
php防止sql注入之过滤分页参数实例
2014/11/03 PHP
php实现scws中文分词搜索的方法
2015/12/25 PHP
thinkPHP自动验证、自动添加及表单错误问题分析
2016/10/17 PHP
redirect_uri参数错误的解决方法(必看)
2017/02/16 PHP
PHP多进程编程之僵尸进程问题的理解
2017/10/15 PHP
PHP基于curl模拟post提交json数据示例
2018/06/22 PHP
Javascript 跨域访问解决方案
2009/02/14 Javascript
javascript 延迟加载技术(lazyload)简单实现
2011/01/17 Javascript
js 遍历对象的属性的代码
2011/12/29 Javascript
jQuery编辑器KindEditor4.1.4代码高亮显示设置教程
2013/03/01 Javascript
从数据结构的角度分析 for each in 比 for in 快的多
2013/07/07 Javascript
详解JavaScript的内置对象
2016/12/07 Javascript
Angular.js中$resource高大上的数据交互详解
2017/07/30 Javascript
浅谈js中的bind
2019/03/18 Javascript
更强大的vue ssr实现预取数据的方式
2019/07/19 Javascript
微信小程序实现音频文件播放进度的实例代码
2020/03/02 Javascript
Python文件夹与文件的操作实现代码
2014/07/13 Python
python实现从字符串中找出字符1的位置以及个数的方法
2014/08/25 Python
寻找网站后台地址的python脚本
2014/09/01 Python
python冒泡排序简单实现方法
2015/07/09 Python
Python使用QRCode模块生成二维码实例详解
2017/06/14 Python
python利用标准库如何获取本地IP示例详解
2017/11/01 Python
Python使用MD5加密算法对字符串进行加密操作示例
2018/03/30 Python
Python类装饰器实现方法详解
2018/12/21 Python
Python函数和模块的使用总结
2019/05/20 Python
Python八皇后问题解答过程详解
2019/07/29 Python
python读取raw binary图片并提取统计信息的实例
2020/01/09 Python
Python基于time模块表示时间常用方法
2020/06/18 Python
捷克多品牌在线时尚商店:ANSWEAR.cz
2020/10/03 全球购物
自荐信要包含哪些内容
2013/11/06 职场文书
婚礼司仪主持词
2014/03/14 职场文书
小学运动会口号
2014/06/07 职场文书
2016年寒假家长评语
2015/10/10 职场文书
css3手动实现pc端横向滚动
2022/06/21 HTML / CSS