一比一手写迷你版vue,彻底搞懂vue运行机制

news2024/10/6 18:27:02

前言

现在前端面试Vue中都会问到响应式原理以及如何实现的,如果你还只是简单回答通过Object.defineProperty()来劫持属性可能已经不够了。

本篇文章通过学习文档及视频教程实现手写一个简易的Vue源码实现数据双向绑定,解析指令等。

几种实现双向绑定的做法

目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入的元素(input, textare等)添加了change(input)事件,来动态修改model和view,并没有多高深,所以无需太过介怀是实现的单向或双向绑定。

实现数据绑定的做法有大致如下几种:

发布者-订阅者模式(backbone.js)

脏值检查(angular.js)

数据劫持(Vue.js)

  • 发布者-订阅者模式

一般是通过sub, pub的方式来实现数据和试图的绑定坚听,更细数据方法通常做法是vm.set(‘property’, value) 这种方式现在毕竟太low来,我们更希望通过vm.property = value这种方式更新数据,同时自动更新视图,于是有来下面两种方式。

  • 脏值检查

angular.js是通过脏值检测的方式对比数据是否有变更,来决定是否更新视图,最简单的方式就是通过setInterval()定时轮询检测数据变动,当然Google不会这么low,angular只有在制定的事件触发时进入脏值检测,大致如下

* DOM事件,臂如用户输入文本,点击按钮等(ng-click)
* XHR响应事件($http)
* 浏览器location变更事件($location)
* Timer事件($timeout, $interval)
* 执行$diaest()或¥apply() 
  • 数据劫持

Vue.js则是通过数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

vue全家桶视频讲解:进入学习

Vue源码实现

index.html

<!DOCTYPE html>
<html><head><meta charset="utf-8" /><title></title><script type="text/javascript" src="./compile.js"></script><script type="text/javascript" src="./observe.js"></script><script type="text/javascript" src="./myvue.js"></script></head><body><div id="app"><h2>{{person.name}} -- {{person.age}}</h2><h3>{{person.sex}}</h3><ul><li>1</li><li>2</li><li>3</li></ul><div v-text="msg"></div><div>{{msg}}</div><div v-text="person.name"></div><div v-html="htmlStr"></div><input type="text" v-model="msg" /><button type="button" v-on:click="btnClick">v-on:事件</button><button type="button" @click="btnClick">@事件</button></div><script type="text/javascript"> let vm = new Myvue({el: '#app',data: {person: {name: '只会番茄炒蛋',age: 18,sex: '男'},msg: '学习MVVM实现原理',htmlStr: '<h1>我是html指令渲染的</h1>'},methods: {btnClick() {console.log(this.msg)}}}) </script></body>
</html> 

第一步 - 实现一个指令解析器(Compile)

compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

myvue.js

// 工具类根据指令执行对应方法
const compileUtils = {/* * node 当前元素节点 * expr 当前指令的value * vm 当前Myvue实例,* eventName 当前指令事件名称 */// 由于指令绑定的属性有可能是原始类型,也有可能是引用类型, 因此要取到最终渲染的值getValue(expr, vm) {// reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。return expr.split('.').reduce((data, currentVal) => {return data[currentVal]}, vm.$data)},// input双向数据绑定setValue(expr, vm, inputVal) {// reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。return expr.split('.').reduce((data, currentVal) => {// 将当前改变的值赋值data[currentVal] = inputValconsole.log(data);}, vm.$data)},// 处理{{person.name}}--{{person.age}}这种格式的数据,不更新值的时候会全部替换了getContentVal(expr, vm) {return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {// 获取{{}}中的属性return this.getValue(args[1], vm)})},// 这里简单就封装了几个指令方法text(node, expr, vm) {let value;// 处理{{}}的格式if (expr.indexOf('{{') !== -1) {value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {// 绑定观察者new Watcher(vm, args[1], (newValue) => {// 处理{{person.name}}--{{person.age}}这种格式的数据,不然更新值的时候会全部替换了this.upDater.textUpDater(node, this.getContentVal(expr, vm))})// 获取{{}}中的属性return this.getValue(args[1], vm)})} else {new Watcher(vm, expr, (newValue) => {this.upDater.textUpDater(node, newValue)})// 获取当前要节点要更新展示的值value = this.getValue(expr, vm)}// 更新的工具类this.upDater.textUpDater(node, value)},html(node, expr, vm) {const value = this.getValue(expr, vm)// 绑定观察者new Watcher(vm, expr, (newValue) => {this.upDater.htmlUpDater(node, newValue)})// 更新的工具类this.upDater.htmlUpDater(node, value)},model(node, expr, vm) {const value = this.getValue(expr, vm)// 绑定观察者new Watcher(vm, expr, (newValue) => {this.upDater.modelUpDater(node, newValue)})node.addEventListener('input', (e) => {// 设置值this.setValue(expr, vm, e.target.value)})// 更新的工具类this.upDater.modelUpDater(node, value)},on(node, expr, vm, eventName) {// 获取当前指令对应的方法const fn = vm.$options.methods && vm.$options.methods[expr]// console.log(fn);node.addEventListener(eventName, fn.bind(vm), false)},// 更新的工具类upDater: {// v-text指令的更新函数textUpDater(node, value) {node.textContent = value},// v-html指令的更新函数htmlUpDater(node, value) {node.innerHTML = value},// v-model指令的更新函数modelUpDater(node, value) {node.value = value}}
}

// Myvue
class Myvue {constructor(options) {this.$el = options.el;this.$data = options.data;this.$options = options;if (this.$el) {// 1.实现一个数据观察者new Observe(this.$data)// 2.实现一个指令解析器new Compile(this.$el, this)// 3.实现this代理, 访问数据可以直接通过this访问this.proxyData(this.$data)}}proxyData(data) {for (const key in data) {Object.defineProperty(this, key, {get() {return data[key]},set(newValue) {data[key] = newValue}})}}
} 

compile.js

// 指令解析器
class Compile {constructor(el, vm) {// 判断当前传入的el是不是一个元素节点// document.querySelector返回与指定的选择器组匹配的元素的后代的第一个元素。this.el = this.isElementNode(el) ? el : document.querySelector(el)this.vm = vm// 1.匹配节点内容及指令替换相应的内容, 因为每次匹配替换会导致页面回流和重绘, 所以使用文档碎片对象// 获取文档碎片对象, 放入内存中会减少页面的回流和重绘const fragment = this.node2Fragment(this.el)// 2.编译模版this.compile(fragment)// 3.追加子元素到根元素this.el.appendChild(fragment)}// 判断是否是元素节点isElementNode(node) {return node.nodeType === 1}// 将当前根元素中的所有子元素一层层取出来放到文档碎片中, 以减少页面回流和重绘node2Fragment(el) {// 创建文档碎片对象const fragment = document.createDocumentFragment()let firstChild;// 将当前el节点对象的所有子节点追加到文档碎片对象中while (firstChild = el.firstChild) {fragment.appendChild(firstChild)}return fragment}// 编译模版, 解析指令compile(fragment) {// 1.获取到所有的子节点, 当前获取的子节点数组是一个伪数组, 需要转为数组const childNodes = [...fragment.childNodes]childNodes.forEach(child => {// 判断当前节点是元素节点还是文本节点if (this.isElementNode(child)) {// 编译元素节点this.compileElement(child)} else {// 编译文本节点this.compileText(child)}// 递归遍历当前节点时候还有子节点对象if (child.childNodes && child.childNodes.length) {this.compile(child)}})}// 编译元素节点compileElement(node) {// 根据不同指令属性, 编译模版信息const attributes = [...node.attributes];attributes.forEach(attr => {// 通过解构将指令的name和value获取到const {name,value} = attr// 判断当前属性是指令还是原生属性if (this.isDirective(name)) {// 截取指令, 不需要v-const directive = name.split('-')[1]// 由于指令格式有 v-text v-html v-bind:属性 v-on:事件等等, 按照 : 再次分割const [dirName, eventName] = directive.split(':')// 更新数据, 数据驱动视图compileUtils[dirName](node, value, this.vm, eventName)// 删除有指令的标签上的属性node.removeAttribute('v-' + directive)} else if (this.isEventName(name)) { // 判断指令是以@开头绑定的事件// 截取指令, 不需要@, 这里就省略处理里 @click.stop.prevent等事件修饰符, 原理不难const eventName = name.split('@')[1]// 更新数据, 数据驱动视图compileUtils['on'](node, value, this.vm, eventName)}})}// 编译文本节点compileText(node) {// node.textContent获取文本并且匹配{{}} 模版字符串类型的const content = node.textContentif (/\{\{(.+?)\}\}/.test(content)) {compileUtils['text'](node, content, this.vm)}}// 判断当前属性是指令还是原生属性isDirective(attrName) {// startsWith() 方法用来判断当前字符串是否以另外一个给定的子字符串开头,并根据判断结果返回 true 或 false。return attrName.startsWith('v-')}// 判断指令是以@开头绑定的事件isEventName(attrName) {return attrName.startsWith('@')}
} 

第二步 - 实现一个数据监听器(Observer)

利用Obeject.defineProperty()来监听属性变动 那么将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter 这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。

observer.js

// 数据劫持
class Observe {constructor(data) {this.observe(data)}// 使用object.defineProperty监听对象, 数组暂时不考虑,太复杂observe(data) {if (data && typeof data === 'object') {// console.log(data);Object.keys(data).forEach(key => {this.defineReactive(data, key, data[key])})}}// 劫持属性defineReactive(obj, key, value) {// 递归遍历this.observe(value)// 创建依赖收集器const dep = new Dep()// console.log(dep);Object.defineProperty(obj, key, { // obj为已有对象, key为属性, 第三个参数为属性描述符enumerable: true, // enumerable:是否可以被枚举(for in),默认falseconfigurable: false, // 是否可以被删除,默认false// 获取get() {// console.log(dep.target);// 订阅数据变化时, 往Dep中添加观察者Dep.target && dep.addSub(Dep.target)return value},// 设置set: (newValue) => {// 这里要注意新设置的值也需要劫持他的属性this.observe(newValue)if (newValue !== value) {value = newValue}// 通知订阅器找到对应的观察者,通知观察者更新视图dep.notify()}})}
} 

第三部 - 实现一个Watcher去更新视图

在初始化myvue实例的时候,通过object。defineProperty()的get属性时去添加观察者,在set更改属性的时候去触发notify()来调用upDate方法更新视图

// 观察者
class Watcher {constructor(vm, expr, cb) {this.vm = vmthis.expr = exprthis.cb = cb// 存储旧值this.oldValue = this.getOldValue()}// 获取旧值getOldValue() {// 在获取旧值的时候将观察者挂在到Dep订阅器上Dep.target = thisconst oldValue = compileUtils.getValue(this.expr, this.vm)// 销毁Dep上的观察者Dep.target = null}// 更新视图upDate() {// 获取新值const newValue = compileUtils.getValue(this.expr, this.vm)if (newValue !== this.oldValue) {this.cb(newValue)}}
}

// 订阅器
class Dep {constructor() {this.subs = []}// 收集观察者addSub(watcher) {this.subs.push(watcher)}// 通知观察者去更新视图notify() {this.subs.forEach(watcher => {watcher.upDate()})}
} 

面试题-阐述你所理解的MVVM响应式原理

Vue是采用数据劫持配合发布者-订阅者模式,通过Object.defineProperty来()来劫持各个属性的getter和setter,在数据发生变化的时候,发布消息给依赖收集器,去通知观察者,做出对应的回调函数去更新视图。

具体就是:MVVM作为绑定的入口,整合Observe,Compil和Watcher三者,通过Observe来监听model的变化,通过Compil来解析编译模版指令,最终利用Watcher搭起Observe和Compil之前的通信桥梁,从而达到数据变化 => 更新视图,视图交互变化(input) => 数据model变更的双向绑定效果。

总结

本篇文章主要以几种实现双向绑定的做法实现Observer实现Compile实现Watcher实现MVVM这几个模块来阐述了双向绑定的原理和实现。并根据思路流程渐进梳理讲解了一些细节思路和比较关键的内容点,当然肯定有很多不完善的地方,但是对于如何实现双向数据绑定你肯定有了更加深刻的了解。

本篇文章也是通过查看Vue源码解析文章,以及B站相关视频总结出来的,俗话说好记性不如烂笔头, 自己即使照着抄一遍也能更加印象深刻。

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/86489.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

好物安利:老照片修复软件哪个好?

大家都知道&#xff0c;老照片承载了很多的回忆&#xff0c;还有许多见证了城市的变迁及发展。甚至可以说&#xff0c;那些老照片&#xff0c;每一张都蕴藏着一个故事&#xff0c;能还原一段历史。但这些老照片&#xff0c;或多或少因为之前的设备、或储存不当等问题&#xff0…

【Docker】Docker安装Redis,并配置数据备份同步到宿主机

专栏精选文章 《Docker是什么&#xff1f;Docker从介绍到Linux安装图文详细教程》《30条Docker常用命令图文举例总结》《Docker如何构建自己的镜像&#xff1f;从镜像构建到推送远程镜像仓库图文教程》《Docker多个容器和宿主机之间如何进行数据同步和数据共享&#xff1f;容器…

Spring:Aop

目录 一、Aop简介 AOP相关术语 二、使用AOP 一、Aop简介 AOP(Aspect Oriented Programming)面向切面编程&#xff0c;一种编程范式&#xff0c;指导开发者如何组织程序结 构 原理&#xff1a;将复杂的需求分解出不同方面&#xff0c;将散布在系统中的公共功能集中解决 …

MEE: A Novel Multilingual Event Extraction Dataset 论文解读

MEE: A Novel Multilingual Event Extraction Dataset paper&#xff1a;[2211.05955] MEE: A Novel Multilingual Event Extraction Dataset (arxiv.org) code&#xff1a;None 期刊/会议&#xff1a;EMNLP 2022 摘要 事件抽取&#xff08;EE&#xff09;是信息抽取&#…

OS_用户层的IO软件@缓冲区@磁盘高速缓存@异步IO

文章目录OS_用户层的IO软件缓冲区磁盘高速缓存异步IO用户层的IO软件&#x1f388;1.系统调用2.库函数高速缓存与缓冲区磁盘高速媛存(Disk Cache)缓冲区Buffer缓冲的用途设备速度的巨大差异&#x1f388;缓冲和缓存的比较联系区别缓冲区的结构缓冲的引入单缓冲区和双缓冲区单缓冲…

万应视野|蔡鑫莹:One in All,低代码平台从工具向商业的转变

2015年&#xff0c;AI、5G、大数据、云计算等前沿技术开始崭露头角&#xff0c;软件再出发、工业互联网等相关政策逐渐出台&#xff0c;云畅的悠悠创业路自此竟也走过了8年。 风华绝代总爱乱世生。2020年&#xff0c;百年变局与世纪疫情交织&#xff0c;经济全球化遭遇逆流&…

倾斜摄影模型数据在gis中的应用

倾斜摄影是近年来航测领域逐渐发展起来的新技术&#xff0c;可同时获得同一位置多个不同角度的、具有高分辨率的影像&#xff0c;采集丰富的地物侧面纹理及位置信息&#xff0c;批量建立高质量、高精度的三维GIS模型&#xff0c;对市政、轨道交通、水利水电、智慧城市等各工程领…

一个简单的HTML篮球网页【学生网页设计作业源码】

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

大比分领先!ACCV 2022 国际细粒度图像分析挑战赛冠军方案

写在前面 在刚刚结束的 ACCV 2022 国际细粒度图像分析挑战赛中&#xff0c;我们团队在 133 支参赛队伍中脱颖而出&#xff0c;在 Leadboard-B 上以 2.5 的绝对优势取得冠军。 比赛成绩截图 在比赛过程中&#xff0c;我们使用了一些对细粒度分类十分有效的解决方案。 例如&am…

服务器渲染技术-->Thymeleaf

目录 一.基本介绍 1.Thymeleaf 是什么 2.Thymeleaf 的优点 3. Thymeleaf 的缺点 二.Thymeleaf 机制说明 三.Thymeleaf 语法 1. 表达式 1. 表达式一览 2. 字面量 3. 文本操作 2.运算符 1. 数学运算 2. 布尔运算 3. 比较运算 4. 条件运算 3.th 属性 4.迭代 5.条…

PLC模拟量传输过程

网上对PLC模拟量的一些解释含糊不清&#xff0c;令参考人疑惑重重&#xff0c;搞不懂现场—>PLC—>上位机的过程走向。 . 应该了解什么是数字量&#xff1f;什么是模拟量&#xff1f;下面是我的理解。 数字量&#xff1a;没有单位&#xff0c;表示一个数字&#xff0c;0~…

Linux-iNode-软硬链接

文章目录inode磁盘了解站在OS角度磁盘就是线性结构如何确认inode和后面的那个block相关联呢&#xff1f;如何在大的inode Table中安排一个新文件的inode?目录下创建文件的过程都发生了什么软硬链接软链接&#xff1a;特别像快捷方式使用场景硬链接二者区别&#xff1a;硬链接有…

nodejs+vue+element+eachers构建开源项目大型连续剧(1)搭建nodejs服务器

在前端开发过程中&#xff0c;可能某些时候需要自己搭建一台服务器用于一些文件图片请求或者进行后端相关知识的学习。本文主要讲解如何通过nodejs进行一个基础服务器的搭建&#xff0c;包括如何将文件布置的服务器&#xff0c;以及基础接口的开发。后面可能会更新关于通过node…

从根源帮助改变数据延误, Qlik 辅助金融行业实现信贷渠道预测

信贷行业面临的业务挑战 传统方式下&#xff0c;营利性增长和正确管理信贷组合风险的能力是通过静态报告和电子表格完成的&#xff0c;不仅数据单独进行汇总&#xff0c;同时交付过程也存在延迟。这种情况导致组织不得不基于不完整的信息来制定关键的信贷和定价决策。信贷员需…

研发中台拆分过程的一些心得总结

背景在 21 年&#xff0c;中台拆分在 21 年&#xff0c;以下为中台拆分的过程心得&#xff0c;带有一定的主观&#xff0c;偏向于中小团队中台建设参考&#xff08;这里的中小团队指 3-100 人的团队&#xff09;&#xff0c;对于大型团队不太适用&#xff0c;毕竟大型团队人中 …

Elasticsearch8系列【2】Windows环境安装ES8

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 文章目录前言Windows安装Elasticsearch8.5.21. 下载2. 获取密码3. 注册服务4. 登录安装可视化工具前言 Elasticsearch使用Java语言开发&#xff0c;在安装之前需要安装JDK环境。基本支持所有主流操作系统…

Kioptrix Level2靶机

0x01信息收集 nmap -sV -O 10.121.65.83 0x02 万能密码&&命令执行 访问80端口(经测试 443端口并没有办法访问) 登录框尝试弱口令和sql注入 发现存在万能密码 admin# 存在一个ping命令&#xff0c;尝试通过管道符看是否会执行命令 127.0.0.0|id 存在命令执行漏洞&am…

network判断是否瓶颈

首先判断连接是否建立&#xff1a; tcp主机a->主机b的请求连接命令&#xff0c;半连接队列在主机b 判断半连接队列命令 netstat -s |grep -i listen 值过大&#xff0c;半连接队列无法建立 2.tcp主机a收到主机b的回复&#xff0c;主机a->主机b的连接请求&#xff0c;全…

成才之路杂志社成才之路编辑部成才之路杂志2022年第33期目录

高教与成才研究《成才之路》投稿&#xff1a;cn7kantougao163.com 融媒体时代大学生职业发展与就业指导课程体系建设研究 张效良;李玉波; 1-4 “三全育人”背景下高职辅导员思政教育方法研究 杨春蕊; 5-8 “五育”学生管理模式对高校思政教育的影响研究 张晓英; 9-…

基数排序python

一、基数排序介绍 基数排序&#xff08;radix sort&#xff09;属于“分配式排序”&#xff08;distribution sort&#xff09;&#xff0c;又称“桶子法”&#xff08;bucket sort&#xff09;或bin sort&#xff0c;顾名思义&#xff0c;它是透过键值的部份资讯&#xff0c;…