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 相关文章推荐
firefox firebug中文入门教程 脚本之家新年特别版
Jan 02 Javascript
Js为表单动态添加节点内容的方法
Feb 10 Javascript
JavaScript将一个数组插入到另一个数组的方法
Mar 19 Javascript
jQuery实现购物车表单自动结算效果实例
Aug 10 Javascript
浅谈String.valueOf()方法的使用
Jun 06 Javascript
jQuery 生成svg矢量二维码
Aug 09 Javascript
关于Jquery中的bind(),on()绑定事件方式总结
Oct 26 Javascript
Node.js中Bootstrap-table的两种分页的实现方法
Sep 18 Javascript
微信小程序scroll-x失效的完美解决方法
Jul 18 Javascript
JS学习笔记之闭包小案例分析
May 29 Javascript
layui下拉框获取下拉值(select)的例子
Sep 10 Javascript
Typescript3.9 常用新特性一览(推荐)
May 14 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
使用无限生命期Session的方法
2006/10/09 PHP
PHP中防止SQL注入攻击和XSS攻击的两个简单方法
2010/04/15 PHP
zend api扩展的php对象的autoload工具
2011/04/18 PHP
PHP设计模式之代理模式的深入解析
2013/06/13 PHP
php获取Google机器人访问足迹的方法
2015/04/15 PHP
PHP SPL标准库之SplFixedArray使用实例
2015/05/12 PHP
thinkphp5框架扩展redis类方法示例
2019/05/06 PHP
PHP unset函数原理及使用方法解析
2020/08/14 PHP
7个Javascript地图脚本整理
2009/10/20 Javascript
用jquery实现的模拟QQ邮箱里的收件人选取及其他效果(一)
2011/01/06 Javascript
Jquery下判断Id是否存在的代码
2011/01/06 Javascript
jquery 操作日期、星期、元素的追加的实现代码
2012/02/07 Javascript
javascript标签在页面中的位置探讨
2013/04/11 Javascript
用unescape反编码得出汉字示例
2014/04/24 Javascript
学习javascript面向对象 javascript实现继承的方式
2016/01/04 Javascript
Node.js设置CORS跨域请求中多域名白名单的方法
2017/03/28 Javascript
微信小程序中的swiper组件详解
2017/04/14 Javascript
Vuex之理解state的用法实例
2017/04/19 Javascript
详细分析JS函数去抖和节流
2017/12/05 Javascript
JS中appendChild追加子节点无效的解决方法
2018/10/14 Javascript
javascriptvoid(0)含义以及与&quot;#&quot;的区别讲解
2019/01/19 Javascript
vue render函数动态加载img的src路径操作
2020/10/26 Javascript
Python文件和目录操作详解
2015/02/08 Python
低版本中Python除法运算小技巧
2015/04/05 Python
python实现批量按比例缩放图片效果
2018/03/30 Python
python队列queue模块详解
2018/04/27 Python
英文求职信结束语大全
2013/10/26 职场文书
个人培训自我鉴定
2014/03/28 职场文书
拒绝黄毒毒宣传标语
2014/06/26 职场文书
小学综治宣传月活动总结
2014/07/02 职场文书
实习介绍信范文
2015/05/05 职场文书
入党积极分子群众意见
2015/06/01 职场文书
西游记读书笔记
2015/06/25 职场文书
2015年教师个人业务工作总结
2015/10/23 职场文书
比较node.js和Deno
2021/04/27 Javascript
Python字符串格式化方式
2022/04/07 Python