作者 | 爱编程的小和尚

责编 | 王晓曼

出品 | CSDN博客

学过 VUE 如果不了解响应式的原理,怎么能说自己熟练使用 VUE,要是没有写过一个简易版的 VUE 怎么能说自己精通 VUE,这篇文章通过300多行代码,带你写一个简易版的 VUE,主要实现 VUE 数据响应式 (数据劫持结合发布者-订阅者)、数组的变异方法、编译指令,数据的双向绑定的功能。

本文需要有一定 VUE 基础,并不适合新手学习。

文章较长,且有些难度,建议大家,找一个安静的环境,并在看之前沐浴更衣,保持编程的神圣感。下面是实现的简易版VUE 的源码地址,一定要先下载下来!因为文章中的并非全部的代码。

Github源码地址:https://github.com/young-monk/myVUE.git

前言

在开始学习之前,我们先来了解一下什么是 MVVM ,什么是数据响应式。

我们都知道 VUE 是一个典型的 MVVM 思想,由数据驱动视图。

那么什么是 MVVM 思想呢?

MVVM是Model-View-ViewModel,是把一个系统分为了模型( model )、视图( view )和 view-model 三个部分。

VUE在 MVVM 思想下,view 和model 之间没有直接的联系,但是 view 和 view-model 、model和 view-model之间时交互的,当 view 视图进行 dom 操作等使数据发生变化时,可以通过 view-model 同步到 model 中,同样的 model 数据变化也会同步到 view 中。

那么实现数据响应式都有什么方法呢?1、发布者-订阅者模式:当一个对象(发布者)状态发生改变时,所有依赖它的对象(订阅者)都会得到通知。通俗点来讲,发布者就相当于报纸,而订阅者相当于读报纸的人。2、脏值检查:通过存储旧的数据,和当前新的数据进行对比,观察是否有变更,来决定是否更新视图。angular.js 就是通过脏值检查的方式。最简单的实现方式就是通过 setInterval 定时轮询检测数据变动,但这样无疑会增加性能,所以, angular 只有在指定的事件触发时进入脏值检测。3、数据劫持:通过 Object.defineProperty 来劫持各个属性的 setter,getter,在数据变动时触发相应的方法。VUE是如何实现数据响应式的呢?

VUE.js 则是通过数据劫持结合发布者-订阅者模式的方式。

当执行 new VUE 时,VUE 就进入了初始化阶段,VUE会对指令进行解析(初始化视图,增加订阅者,绑定更新函数),同时通过 Obserber会遍历数据并通过 Object.defineProperty 的 getter 和 setter 实现对的监听, 当数据发生变化的时候,Observer 中的 setter 方法被触发,setter 会立即调用Dep.notify, Dep 开始遍历所有的订阅者,并调用订阅者的 update 方法,订阅者收到通知后对视图进行相应的更新。

我来依次介绍一下图中的重要的名词:1、Observer:数据监听器,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者,内部采用 Object.defineProperty 的 getter 和 setter 来实现2、Compile:指令解析器,它的作用对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数3、Dep:订阅者收集器或者叫消息订阅器都可以,它在内部维护了一个数组,用来收集订阅者,当数据改变触发 notify 函数,再调用订阅者的 update 方法4、Watcher:订阅者,它是连接 Observer 和 Compile 的桥梁,收到消息订阅器的通知,更新视图5、Updater:视图更新所以我们想要实现一个 VUE 响应式,需要完成数据劫持、依赖收集、 发布者订阅者模式。下面我来介绍我模仿源码实现的功能:

1、数据的响应式、双向绑定,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者

2、解析 VUE 常用的指令 v-html,v-text,v-bind,v-on,v-model,包括( @ 和 : )

3、数组变异方法的处理

4、在 VUE 中使用 this 访问或改变 data 中的数据

我们想要完成以上的功能,需要实现如下类和方法:

1、实现 Observe r类:对所有的数据进行监听

2、实现 array 工具方法:对变异方法的处理

3、实现 Dep 类:维护订阅者

4、实现 Watcher 类:接收 Dep 的更新通知,用于更新视图

5、实现 Compile 类:用于对指令进行解析

6、实现一个 CompileUtils 工具方法,实现通过指令更新视图、绑定更新函数Watcher

7、实现 this.data 代理:实现对 this. data 代理:实现对 this.data 代理:实现对 this.data 代理,可以直接在 VUE 中使用 this 获取当前数据

我是使用了webpack作为构建工具来协同开发的,所以在我实现的VUE响应式中会用到ES6模块化,webpack的相关知识。

实现 Observer 类

我们都知道要用 Obeject.defineProperty 来监听属性的数据变化,我们需要对 Observer 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter ,这样的话,当给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。当然我们在新增加数据的时候,也要对新的数据对象进行递归遍历,加上 setter 和 getter 。

但我们要注意数组,在处理数组时并不是把数组中的每一个元素都加上 setter 和 getter ,我们试想一下,一个从后端返回的数组数据是非常庞大的,如果为每个属性都加上 setter 和 getter ,性能消耗是十分巨大的。我们想要得到的效果和所消耗的性能不成正比,所以在数组方面,我们通过对数组的7 个变异方法来实现数据的响应式。只有通过数组变异方法来修改和删除数组时才会重新渲染页面。

那么监听到变化之后是如何通知订阅者来更新视图的呢?我们需要实现一个Dep(消息订阅器),其中有一个 notify 方法,是通知订阅者数据发生了变化,再让订阅者来更新视图。

我们怎么添加订阅者呢?我们可以通过 new Dep,通过 Dep 中的addSaubs 方法来添加订阅者。我们来看一下具体代码。

我们首先需要声明一个 Observer 类,在创建类的时候,我们需要创建一个消息订阅器,判断一下是否是数组,如果是数组,我们便改造数组,如果是对象,我们便需要为对象的每一个属性都加入 setter 和 getter 。

import { arrayMethods } from './array' //数组变异方法处理 class Observer {constructor(data) {//用于对数组进行处理,存放数组的观察者watcherthis.dep = new Depif (Array.isArray(data)) {//如果是数组,使用数组的变异方法data.__proto__ = arrayMethods//把数组数据添加 __ob__ 一个Observer,当使用数组变异方法时,可以更新视图data.__ob__ = this//给数组的每一项添加数据劫持(setter/getter处理)this.observerArray(data)} else {//非数组数据添加数据劫持(setter/getter处理)this.walk(data)}}}

在上面,我们给 data 的__proto__原型链重新赋值,我们来看一下 arrayMethods 是什么,arrayMethods 是 array.js 文件中,抛出的一个新的 Array 原型:

// 获取Array的原型链const arrayProto = Array.prototype;// 重新创建一个含有对应原型的对象,在下面称为新Arrayconst arrayMethods = Object.create(arrayProto);// 处理7个数组变异方法['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'].forEach(ele => {//修改新Array的对应的方法arrayMethods[ele] = function {// 执行数组的原生方法,完成其需要完成的内容arrayProto[ele].call(this, ...arguments)// 获取Observer对象const ob = this.__ob__// 更新视图ob.dep.notify}})export {arrayMethods}

此时呢,我们就拥有了数组的变异方法,我们还需要通过 observerArray 方法为数组的每一项添加 getter 和setter ,注意,此时的每一项只是最外面的一层,并非递归遍历。

//循环遍历数组,为数组每一项设置setter/getterobserverArray(items) {for (let i = 0; i < items.length; i++) {this.observer(items[i])}}

如果是一个对象的话,我们就要对对象 的每一个属性递归遍历,通过 walk 方法:

walk(data) {//数据劫持if (data && typeof data === "object") {for (const key in data) {//绑定setter和getterthis.defineReactive(data, key, data[key])}}}

在上面的调用了 defineReactive ,我们来看看这个方法是干什么的?这个方法就是设置数据劫持的,每一行都有注释。

//数据劫持,设置 setter/getteerdefineReactive(data, key, value) {//如果是数组的话,需要接受返回的Observer对象let arrayOb = this.observer(value)//创建订阅者/收集依赖const dep = new Dep//setter和getter处理Object.defineProperty(data, key, {//可枚举的enumerable: true,//可修改的configurable: false,get {//当 Dep 有 watcher 时, 添加 watcherDep.target && dep.addSubs(Dep.target)//如果是数组,则添加上数组的观察者Dep.target && arrayOb && arrayOb.dep.addSubs(Dep.target)return value},set: (newVal) => {//新旧数据不相等时更改if (value !== newVal) {//为新设置的数据添加setter/getterarrayOb = this.observer(newVal);value = newVal//通知 dep 数据发送了变化dep.notify}}})}}

我们需要注意的是,在上面的图解中,在 Observer 中,如果数据发生变化,会通知消息订阅器,那么在何时绑定消息订阅器呢?就是在设置 setter 和 getter 的时候,创建一个 Dep,并为 Dep添加订阅者,Dep.target&& dep.addSubs(Dep.target),通过调用 dep 的 addSubs 方法添加订阅者。

实现 Dep

Dep 是消息订阅器,它的作用就是维护一个订阅者数组,当数据发送变化是,通知对应的订阅者,Dep中有一个 notify 方法,作用就是通知订阅者,数据发送了变化:

// 订阅者收集器export default class Dep {constructor {//管理的watcher的数组this.subs =}addSubs(watcher) {//添加watcherthis.subs.push(watcher)}notify {//通知watcher更新domthis.subs.forEach(w => w.update)}}

实现 watcher

Watcher 就是订阅者, watcher 是 Observer 和 Compile 之间通信的桥梁,当数据改变时,接收到 Dep 的通知(Dep 的notify()方法),来调用自己的update方法,触发 Compile 中绑定的回调,达到更新视图的目的。

import Dep from './dep'import { complieUtils } from './utils'export default class Watcher {constructor(vm, expr, cb) {//当前的vue实例this.vm = vm;//表达式this.expr = expr;//回调函数,更新domthis.cb = cb//获取旧的数据,此时获取旧值的时候,Dep.target会绑定上当前的thisthis.oldVal = this.getOldVal}getOldVal {//将当前的watcher绑定起来Dep.target = this//获取旧数据const oldVal = complieUtils.getValue(this.expr, this.vm)//绑定完成后,将绑定的置空,防止多次绑定Dep.target =return oldVal}update {//更新函数const newVal = complieUtils.getValue(this.expr, this.vm)if (newVal !== this.oldVal || Array.isArray(newVal)) {//条用更新在compile中创建watcher时传入的回调函数this.cb(newVal)}}}

上面中用到了 ComplieUtils 中的 getValue 方法,会在下面讲,主要作用是获取到指定表达式的值。

我们把整个流程分成两条路线的话:

newVUE ==> Observer数据劫持 ==> 绑定Dep ==> 通知watcher ==> 更新视图newVUE ==> Compile解析模板指令 ==> 初始化视图 和 绑定watcher

此时,我们第一条线的内容已经实现了,我们再来实现一下第二条线。

实现 Compile

Compile 主要做的事情是解析模板指令,将模板中的变量替换成数据,初始化渲染页面视图。同时也要绑定更新函数,添加订阅者。

因为在解析的过程中,会多次的操作 dom,为提高性能和效率,会先将 VUE 实例根节点的 el 转换成文档碎片 fragment 进行解析编译操作,解析完成,再将 fragment 添加回原来的真实 dom 节点中。

class Complie {constructor(el, vm) {this.el = this.isNodeElement(el) ? el : document.querySelector(el);this.vm = vm;// 1、将所有的dom对象放到fragement文档碎片中,防止重复操作dom,消耗性能const fragments = this.nodeTofragments(this.el)// 2、编译模板this.complie(fragments)// 3、追加子元素到根元素this.el.appendChild(fragments)}}

我们可以看到,Complie 中主要进行了三步,第一步 nodeTofragments 是讲所有的 dom 节点放到文档碎片中操作,最后一步,是把解析好的 dom 元素,从文档碎片重新加入到页面中,这两步的具体方法,大家去下载我的源码,看一下就明白了,有注释。我就不再解释了。

我们来看一下第二步,编译模板:

 complie(fragments) {//获取所有节点const nodes = fragments.childNodes;[...nodes].forEach(ele => {if (this.isNodeElement(ele)) {//1. 编译元素节点this.complieElement(ele)} else {//编译文本节点this.complieText(ele)}//如果有子节点,循环遍历,编译指令if (ele.childNodes && ele.childNodes.length) {this.complie(ele)}})}

我们要知道,模板可能有两种情况,一种是文本节点(含有双大括号的插值表达式)和元素节点(含有指令)。我们获取所有节点后对每个节点进行判断,如果是元素节点,则用解析元素节点的方法,如果是文本节点,则调用解析文本的方法。

complieElement(node) {//1.获取所有的属性const attrs = node.attributes;//2.筛选出是属性的[...attrs].forEach(attr => {//attr是一个对象,name是属性名,value是属性值const {name,value} = attr//判断是否含有v-开头 如:v-htmlif (name.startsWith("v-")) {//将指令分离 text, html, on:clickconst [, directive] = name.split("-")//处理on:click或bind:name的情况 on,clickconst [dirName, paramName] = directive.split(":")//编译模板complieUtils[dirName](node, value, this.vm, paramName)//删除属性,在页面中的dom中不会再显示v-html这种指令的属性node.removeAttribute(name)} else if (name.startsWith("@")) {// 如果是事件处理 @click='handleClick'let [, paramName] = name.split('@');complieUtils['on'](node, value, this.vm, paramName);node.removeAttribute(name);} else if (name.startsWith(":")) {// 如果是事件处理 :href='...'let [, paramName] = name.split(':');complieUtils['bind'](node, value, this.vm, paramName);node.removeAttribute(name);}})}

我们在编译模板中调用了 complieUtils[dirName](node, value, this.vm, paramName)方法,这是工具类中的一个方法,用于处理指令。

我们再来看看文本节点,文本节点就相对比较简单,只需要匹配{{}}形式的插值表达式就可以了,同样的调用工具方法,来解析。

complieText(node) {//1.获取所有的文本内容const text = node.textContent//匹配{{}}if (/{{(.+?)}}/.test(text)) {//编译模板complieUtils['text'](node, text, this.vm)}}

上面用来这么多工具方法,我们来看看到底是什么。

实现 ComplieUtils 工具方法

这个方法主要是对指令进行处理,获取指令中的值,并在页面中更新相应的值,同时我们在这里要绑定 watcher 的回调函数。

我来以 v-text 指令来解释,其他指令都有注释,大家自己看。

import Watcher from './watcher'export const complieUtils = {//处理text指令text(node, expr, vm) {let value;if (/{{.+?}}/.test(expr)) {//处理 {{}}value = expr.replace(/{{(.+?)}}/g, (...args) => {//绑定观察者/更新函数new Watcher(vm, args[1], => {//第二个参数,传入回调函数this.updater.updaterText(node, this.getContentVal(expr, vm))})return this.getValue(args[1], vm)})} else {//v-textnew Watcher(vm, expr, (newVal) => {this.updater.updaterText(node, newVal)})//获取到value值value = this.getValue(expr, vm)}//调用更新函数this.updater.updaterText(node, value)},}

Text 处理函数是对 dom 元素的 TextContent 进行操作的,所以有两种情况,一种是使用 v-text 指令,会更新元素的 textContent,另一种情况是{{}} 的插值表达式,也是更新元素的 textContent。

在此方法中我们先判断是哪一种情况,如果是 v-text 指令,那么就绑定一个 watcher 的回调,获取到 textContent 的值,调用 updater.updaterText 在下面讲,是更新元素的方法。如果是双大括号的话,我们就要对其进行特殊处理,首先是将双大括号替换成指定的变量的值,同时为其绑定 watcher 的回调。

//通过表达式, vm获取data中的值, person.namegetValue(expr, vm) {return expr.split(".").reduce((data, currentVal) => {return data[currentVal]}, vm.$data)},

获取 textContent 的值是用一个 reduce 函数,用法在最后面的链接中,因为数据可能是 person.name 我们需要获取到最深的对象的值。

 //更新dom元素的方法updater: {//更新文本updaterText(node, value) {node.textContent = value}}

updater.updaterText更新dom的方法,其实就是对 textContent 重新赋值。

我们再来将一下v-model指令,实现双向的数据绑定,我们都知道,v-model其实实现的是 input 事件和 value 之间的语法糖。所以我们这里同样的监听一下当前 dom 元素的 input 事件,当数据改变时,调用设置新值的方法:

//处理model指令model(node, expr, vm) {const value = this.getValue(expr, vm)//绑定watchernew Watcher(vm, expr, (newVal) => {this.updater.updaterModel(node, newVal)})//双向数据绑定node.addEventListener("input

vue 数组 指定位置添加数据_VUE 响应式原理源码:带你一步精通 VUE | 原力计划...相关推荐

  1. VUE 响应式原理源码:带你一步精通 VUE | 原力计划

    作者 | 爱编程的小和尚 责编 | 王晓曼 出品 | CSDN博客 学过 VUE 如果不了解响应式的原理,怎么能说自己熟练使用 VUE,要是没有写过一个简易版的 VUE 怎么能说自己精通 VUE,这篇 ...

  2. php数组中插入数值,php中如何在数组指定位置插入数据单元

    方法: 使用array_splice()函数. 语法格式:array_splice(array,offset,length,array) 参数: array:必需.规定数组. offset:必需.数值 ...

  3. vue向list中添加数据_vue点击添加数据 - osc_sjg81se7的个人空间 - OSCHINA - 中文开源技术交流社区...

    通过v-model来实现数据的添加,以后会有更好的办法. 首先,写三个input 文本域,然后通过v-model 双向绑定data 里面的prop 对象里面对应的字段,里面的字段都给空字符串,因为一开 ...

  4. js数组指定位置添加删除

    示例参考:http://www.w3school.com.cn/jsref/jsref_splice.asp 转载于:https://www.cnblogs.com/CarryYou-lky/p/10 ...

  5. vueweb端响应式布局_vue响应式原理图文详解

    Vue最显著的特性之一便是不太引人注意的响应式系统(reactivity system).模型层(model)只是普通JS对象,修改它则更新视图(view).这会让状态管理变得非常简单且直观,不过理解 ...

  6. js 给json添加新的字段,或者添加一组数据,在JS数组指定位置删除、插入、替换元素...

    JS定义了一个json数据var test={name:"name",age:"12"};需要给test再添加一个字段,需要什么办法,可以让test的值为{na ...

  7. pandas在dataframe指定位置添加新的数据列、使用insert函数

    pandas在dataframe指定位置添加新的数据列.使用insert函数 目录 pandas在dataframe指定位置添加新的数据列.使用insert函数 #仿真数据

  8. vue项目统一响应_Vue响应式原理及总结

    Vue 的响应式原理是核心是通过 ES5 的保护对象的 Object.defindeProperty 中的访问器属性中的 get 和 set 方法,data 中声明的属性都被添加了访问器属性,当读取 ...

  9. 016_Vue数组数据的响应式处理

    1. 数组数据的响应式处理 1.1. 第一个参数表示要处理的数组名称; 第二个参数表示要处理的数组索引; 第三个参数表示要处理的数组的值. Vue.set(vm.list, index, newVal ...

最新文章

  1. eclipse svn 与资源库同步 符号说明
  2. Ubuntu环境下TensorFlow 的环境搭建(二)安装TensorFlow(CPU版)
  3. Python Split函数的用法总结
  4. OpenSceneGraph学习笔记
  5. 简单区分NMOS和PMOS的方法
  6. 在Zephyr RTOS上实现一个轮询系统
  7. OpenCV_cv::Mat的深拷贝 浅拷贝问题
  8. mysql数据库相关基础知识02
  9. java中的Map每次只能put一次,写段增强的put,可以一次put很多次
  10. canon lbp6200 macos下单面双面打印设置
  11. 清理windows10系统垃圾文件-bat批处理命令
  12. 目前微型计算机硬件主要采用,目前使用的微型计算机硬件主要采用的电子器件是()。 A. 真空管 B. 晶体管 C. 大规模和超大规模集成电路...
  13. 如何隐藏CNZZ统计图标
  14. 计算机组成与结构 英语,计算机组成与结构,Computer organization and architecture,音标,读音,翻译,英文例句,英语词典...
  15. 打开51cto.com网页出现病毒提示
  16. 【故障诊断】基于 KPCA 进行降维、故障检测和故障诊断研究(Matlab代码实现)
  17. 显卡测试软件毛毛虫,ATI Radeon Xpress200M与Intel GMA950谁强些?
  18. oral-b app Android,oral b app
  19. vscode setting 配置
  20. java indexeddb_HTML5之IndexedDB使用详解

热门文章

  1. 6. SQL 多表查询
  2. 小鑫の日常系列故事(六)——奇遇记 (sdut oj)
  3. mysql重启服务 自增列id的auto_increment重置问题
  4. Cadence教程(嘉立创封装导入到orcad)
  5. 36 | MySQL中神奇的用户临时表怎么用?
  6. python打开word格式的文件。查找中文错别字
  7. CXF中 wsdl2java工具的使用方法
  8. 安利安利-向大家推荐一个超级牛的etcd管理工具-EtcdKeeperFyne
  9. win7系统如何调待机时间
  10. idea配置git(附错误解决方法)