Vue简版源码 & 响应式原理 (学习途径:拉钩教育)

一、基本原理

vue响应式的基本原理:使用 Object.defineProperty 做数据劫持,并在 set 时通知观察者更新视图变化。

二、分析

通过观察和分析 Vue对象,可以得出以下几条初始化要做的事情 * vue 初始化时 options 内的 data 对象的属性被注册到了 vue 对象下,并转换成了 getter 和 setter(方便通过 this.msg 使用数据),并将 data 注册到 $data 下,转换成 getter 和 setter (数据劫持) * options 被注入到对象的 $options 属性下 * vue 初始化时 options内的 el 被转换成dom对象,注入到对象的 $el 下 * 将 methods 下的方法注册到 vue 对象下 * 解析 v-on、v-html、v-text、v-model 等 指令

在此示例中只作这四种指令的解析(声明周期钩子的方法处理,后续还会再完善)

三、简单 Vue 实现

#### Vue类实现
/*** Vue 对象入口* 核心逻辑:*  1. 将 this.$data 内的成员添加到vue实例下,添加getter和setter属性*  2. 将 this.$data 添加getter和setter属性,监听变化*  3. 解析 this.$el 内元素*  4. 将 methods内方法注册到vue对象下*/
class Vue {constructor(options) {// 初始化 & 保存配置及属性this.$options = options || {}this.$data = options.data || {}this.$methods = options.methods || {}this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el// 将methods内方法,注入到vue实例内this._addMethods(this.$methods)// 将data中的成员转换成getter和setter,注入到vue实例中this._proxyData(this.$data)// 将 this.$data 转换成 get & set 监听数据变化new Observer(this.$data)// 调用compiler对象,解析指令和差值表达式new Compiler(this)}_addMethods(methods) {Object.keys(methods).forEach(key => {this[key] = methods[key]})}_proxyData(data) {Object.keys(data).forEach(key => {Object.defineProperty(this, key, {get() {return data[key]},set(newVal) {if(newVal === data[key]) {return}data[key] = newVal}})})}
}

解析:

  1. 在 vue 类的 constructor 构造方法中,首先实现了 vue实例对 $options、$data、$methods、$el 的初始化
  2. 调用 _addMethods 方法,将 methods 内方法,注入到vue实例内
  3. 调用 _proxyData 方法,将 data 中的成员转换成 getter 和 setter ,注入到vue实例中
  4. 调用 Observer 对象,将 this.$data 转换成 get & set 监听数据变化
  5. 调用 Compiler 对象,解析指令和差值表达式
注意:this._proxyData(this.$data) 这里传递的是 this.$data 的原因是在 get 或 set vue实例下数据的时候可以触发 this.$data 下的数据的 getter 和 setter 方法 (this.$data 将在 Observer 类中完成数据劫持处理)

在此类中只做初始化及功能调用工作,其他功能将拆分模块进行调用,接下来我们看 Observer 类(数据劫持)

Observer 类实现

/*** Observer* 将data内所有对象的属性添加getter和setter监听* walk:当前值是对象添加遍历其属性,调用 defineReactive 方法处理* defineReactive:处理当前对象的key,添加getter和setter监听,对每一项key的value再次调用walk方法,如果是对象则继续递归此流程*/
class Observer {constructor(data) {this.walk(data)}walk(data) {// 不存在或者不是一个对象if(!data || typeof data !== 'object') {return}Object.keys(data).forEach(key => {this.defineReactive(data, key, data[key])})}defineReactive(obj, key, val) {const that = thislet dep = new Dep()// 当前val是对象-递归this.walk(val)// this.$data内的数据转化监听Object.defineProperty(obj, key, {get() {Dep.target && dep.addSub(Dep.target)return val},set(newVal) {if (newVal === val) {return}val = newVal// 改变之后的值时对象,递归监听that.walk(newVal)dep.notify()}})}
}

解析:

  1. 在vue的 constructor 构造方法内使用了 Observer 类,初始化传递了一个对象,即 vue内的响应式对象;
  2. 当前类功能是将data内所有对象的属性添加getter和setter监听
  3. walk方法 负责判断当前传递过来的数据是否是对象,如果是遍历处理每一个 key 调用 defineReactive 方法
  4. defineReactive 方法 负责将传递过来的key转换成getter和setter(这里有个坑稍后统一说明)
  5. 通过在 defineReactive 方法内调用 walk方法传递value进行递归调用,实现 $data 下全部层级数据的数据劫持
  6. 在 defineReactive 方法内所使用的 Dep 类就是我们实现数据响应式的发布对象,在 getter 方法内收集依赖即订阅者,在setter 方法内 调用 dep(发布者)的 notify 方法通知订阅者更新视图;(收集逻辑在后边会有讲到,后续会有说明)
注意:如果将 return val 换成 obj[key], 则还会触发get方法,会循环调用产生死递归,所以这里需要将 val 也传递给 defineReactive方法

在此类中我们完成了针对 this.$data 的数据劫持,接下来我们看 Compiler 类(指令解析)

Compiler 类实现

/*** Compiler* compile:递归遍历 el下所有节点,根据节点类型进行不同逻辑处理* compileText:处理文本节点*   文本节点匹配 {{ msg }} 初始化替换 && 添加观察者* compileElement:处理元素节点*   元素节点匹配指令 通过不同的指令完成 初始化替换 && 添加观察者* onMethods 通过字符串的拼接,完成不同v-on指令处理方法的调用*    on + click/change 等内部实现了 初始化替换 && 添加观察者* update:通过字符串的拼接,完成不同v-指令处理方法的调用*    text/module + Updater 内部实现了 初始化替换 && 添加观察者** isMethods 判断 methods是否存在 (工具方法)* isDirective:判断是否是指令(工具方法)* isTextNode:判断是否是文本节点(工具方法)* isElementNode:判断是否是元素节点(工具方法)*/
class Compiler {constructor(vm) {this.vm = vmthis.el = vm.$elthis.compile(this.el)}// 遍历子节点compile(el) {Array.from(el.childNodes).forEach(node => {if(this.isTextNode(node)) {// 文本节点this.compileText(node)} else if (this.isElementNode(node)) {// 元素节点this.compileElement(node)}if(node.childNodes && node.childNodes.length) {this.compile(node)}})}// 处理文本节点compileText(node) {let reg = /{{(.+?)}}/let value = node.textContentif(reg.test(value)) {// 获取差值表达式的keylet key = RegExp.$1.trim()node.textContent = value.replace(reg, this.vm[key])new Watcher(this.vm, key, (newValue) => {node.textContent = newValue})}}// 处理元素节点compileElement(node) {Array.from(node.attributes).forEach(attr => {// 指令名称 eg:v-modulelet attrName = attr.nameif(this.isDirective(attrName)) {if (attrName.startsWith("v-on:")) {attrName = attrName.substr(5)let funcName = attr.valuethis.onMethods(node, funcName, attrName)} else if(attrName.startsWith("v-")) {attrName = attrName.substr(2)// 绑定字段名 msglet key = attr.valuethis.update(node, key, attrName)}}})}/*** v-on 标签处理* @param node* @param funcName* @param attrName*/onMethods(node, funcName, attrName) {let updateFn = this['on' + attrName]updateFn && updateFn.call(this, node, funcName, attrName)}/*** v-on:click* @param node* @param funcName* @param attrName*/onclick(node, funcName, attrName) {// 是 methods内方法,给当前node节点绑定事件,传递一个 e 的参数(当前节点node)if (this.isMethods(funcName)) {node.addEventListener(attrName, this.vm[funcName].bind(this.vm, node))} else {// 不是 methods内方法,给当前node节点绑定事件,内部直接执行当前字符串表达式node.addEventListener(attrName, () => {try {eval(funcName)} catch (e) {throw Error(funcName + ' is not defined')}})}}// 合并请求方法update(node, key, attrName) {let updateFn = this[attrName + 'Updater']updateFn && updateFn.call(this, node, this.vm[key], key)}// v-text 属性处理textUpdater(node, value, key) {node.textContent = valuenew Watcher(this.vm, key, (newValue) => {node.textContent = newValue})}// v-model 属性处理modelUpdater (node, value, key) {node.value = valuenew Watcher(this.vm, key, (newValue) => {node.value = newValue})// 双向绑定node.addEventListener('input', () => {this.vm[key] = node.value})}// v-html 属性处理htmlUpdater (node, value, key) {node.innerHTML = valuenew Watcher(this.vm, key, (newValue) => {node.textContent = newValue})}// 判断methodsisMethods(key) {return this.vm[key] !== undefined}// 判断属性指令isDirective(attrName) {return attrName.startsWith('v-')}// 文本节点isTextNode(node) {return node.nodeType === 3}// 元素节点isElementNode(node) {return node.nodeType === 1}
}

解析:

  • compile:递归遍历 el下所有节点,根据节点类型进行不同逻辑处理
  • compileText:处理文本节点; 文本节点匹配 {{ msg }} 初始化替换 && 添加观察者
  • compileElement:处理元素节点 元素节点匹配指令 通过不同的指令完成 初始化替换 && 添加观察者
  • onMethods 通过字符串的拼接,完成不同v-on指令处理方法的调用 on + click/change 等内部实现了 初始化替换 && 添加观察者
  • update:通过字符串的拼接,完成不同v-指令处理方法的调用 text/module + Updater 内部实现了 初始化替换 && 添加观察者
  • isMethods 判断 methods是否存在 (工具方法)
  • isDirective:判断是否是指令(工具方法)
  • isTextNode:判断是否是文本节点(工具方法)
  • isElementNode:判断是否是元素节点(工具方法)
1. 在处理 元素节点 onclick 方法中,判断当前传入的 funcName 是否是挂载在 vue实例上的 methods 方法Y: node.addEventListener(attrName, this.vm[funcName].bind(this.vm, node)) 注意方法内 this 需要指向当前vue实例N: 在回调内使用 eval(funcName) 执行当前字符串
2. 在处理 指令初始化绑定数据的时候new Watcher(this.vm, key, (newValue) => {node.textContent = newValue})都会使用 Watcher 创建 订阅者对象,并且传入vue实例、当前改变数据名、回调函数(更新视图)

所以接下来我们进入 Watcher 类

Watcher 类实现

/*** 观察者:Watcher* 核心逻辑:*  1. Dep.target指向当前Watcher对象*  2. 在 this.oldValue = vm[key] 的时候,调用getter方法,在getter方法内将 当前 Watcher 当前key的Dep对象的addSub方法 (收集观察者)*  3. Dep.target 置空* update方法:当不更新数值和原始数值不同的时候,调用创建时候的回调更新视图*/
class Watcher {constructor(vm, key, cb) {this.vm = vmthis.key = keythis.cb = cbDep.target = thisthis.oldValue = vm[key]Dep.target = null}update() {let newValue = this.vm[this.key]if(this.oldValue === newValue) {return}this.cb(newValue)}
}

核心逻辑

Dep.target = this
this.oldValue = vm[key]
Dep.target = null

  1. 让 Dep.target 指向当前 watcher 观察者对象
  2. 在调用 this.oldValue = vm[key] 的时候,由于 vm[key] 会触发 getter 事件,并且在 getter 事件的 Dep.target && dep.addSub(Dep.target) 会将调用dep的 addSub 方法收集当前 watcher 对象
  3. 将 Dep.target 置空
  4. update 方法:判断数据是否发生改变,并且调用 cb 方法更新视图

在核心逻辑中调用了 Dep 对象,进行观察者收集,所以接下来看 Dep 类

Dep 类实现

/*** 发布对象 Dep* subs 观察者列表* addSub(sub, sub.update) 添加观察者到当前对象subs列表* notify() 发布消息,遍历subs列表,所有订阅者调用update方法执行,通知订阅者更新视图*/
class Dep {constructor() {this.subs = []}addSub(sub) {if(sub && sub.update) {this.subs.push(sub)}}notify() {this.subs.forEach(sub => {sub.update()})}
}

解析:

  1. subs 观察者列表
  2. addSub(sub, sub.update) 添加观察者到当前对象subs列表
  3. notify() 发布消息,遍历subs列表,所有订阅者调用update方法执行,通知订阅者更新视图

-----------------------------------------代码结束-----------------------------------------

数据驱动视图改变逻辑

发布对象 Dep 在 Compiler 对象在 this.$data 转换 getter 和setter 的时候创建;在Compiler 内创建观察者 Watcher 的时候,由 Watcher 的构造函数内的 vm[key] 触发数据get方法,并在get方法内调用 dep 的 addSub 方法将当前观察者对象添加到 发布者 subs 列表内;当 this.$data数据触发 set 的时候,调用当前数据发布者 dep.notify 方法,遍历 subs 内的 watcher 观察者,调观察者对象的 update 方法,执行在 Compiler  内创建的时候传递的更新视图的方法,更新视图;

视图改变驱动数据改变

// 双向绑定
node.addEventListener('input', () => {this.vm[key] = node.value
})
在 Compiler类 处理 v-module 的方法内,对 node 节点绑定input事件监听,更新数据

逻辑梳理

四、收获&知识点

1、发布者-观察者模式

1 观察者:Watcher
update() 此方法在观察者实例方法notify中被调用发布者:Dep
2 subs:观察者数组
addSub():为发布者收集观察者
notify():循环subs 观察者数组,调用观察者update()方法3 代码中为每一个属性的dep 收集watcher的方式

2、apply、call、bind 区别和使用

以上代码中使用到了call 和bind方法,其作用是:改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。

而两者的区别则是:bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用。

bind方法使用

/*** v-on:click* @param node* @param funcName* @param attrName*/onclick(node, funcName, attrName) {// 是 methods内方法,给当前node节点绑定事件,传递一个 e 的参数(当前节点node)if (this.isMethods(funcName)) {node.addEventListener(attrName, this.vm[funcName].bind(this.vm, node))} else {// 不是 methods内方法,给当前node节点绑定事件,内部直接执行当前字符串表达式node.addEventListener(attrName, () => {try {eval(funcName)} catch (e) {throw Error(funcName + ' is not defined')}})}}

call方法使用

 /*** v-on 标签处理* @param node* @param funcName* @param attrName*/onMethods(node, funcName, attrName) {let updateFn = this['on' + attrName]updateFn && updateFn.call(this, node, funcName, attrName)}

vuejs知乎_vueJS (简版)amp; 响应式原理相关推荐

  1. TS手写简陋版reactive响应式原理(依赖收集,依赖更新)

    最近博主看源码了解到了vue3的响应式原理 vue3的响应式实现是依赖收集与依赖更新,vue3已经从Object.property更换成Proxy,Proxy相比于前者可以直接监听对象数组,对于深层次 ...

  2. 极简风格的响应式简历模板

    Crisp Minimal Résumé github.com/crispgm/res- 简介 极简风格的响应式简历模板,基于 Jekyll,可以直接部署在 GitHub Pages 上. 通过配置 ...

  3. vuejs视图不能及时更新的问题 ,深入响应式原理

    最近三个多月,我和我的同事一起用vuejs 做公司的项目管理系统,因为是第一次用这种双向绑定的框架,难免遇到一些问题. 在做项目时,发现数据并没有实时更新,比如说你用element-ui的时间控件或者 ...

  4. Vue设计模式,发布订阅,响应式原理(简略版)

    Vue mvvm框架是什么? mvvm框架(model-view-viewMode),本质是mvc框架的改进版,mvc框架一旦项目复杂度越来越高,代码量大,维护起来很难,尤其管理层,controlle ...

  5. 【Vuejs】952- 一文带你了解vue2之响应式原理

    在面试的过程中也会问到:请阐述vue2的响应式原理?,凡是出现阐述或者理解,一般都是知无不言言无不尽,知道多少说多少.接下来,我来谈谈自己的理解,切记不要去背,一定要理解之后,用自己的语言来描述出来. ...

  6. 【Vue.js源码解析 一】-- 响应式原理

    前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 课程目标 Vue.js 的静态成员和实例成员初始化过程 首次渲染的过程 数据响应式原理 – 最核心的特性之一 准备工作 ...

  7. vue 获取响应头里set-cookie的值_最简化 VUE的响应式原理

    前言 前端目前两个当家花旦框架 VUE React,它们能够流行开来,响应式原理做出了巨大贡献.毕竟,它通过数据的变更就能够更新相应的视图,极大的将我们从繁琐的DOM操作中解放出来. 所以掌握它们的响 ...

  8. Vue响应式原理的简单模型

    1.前言 最近在梳理vue响应式的原理,有一些心得,值得写一篇博客出来看看. 其实之前也尝试过了解vue响应式的原理,毕竟现在面试看你用的是vue的话,基本上都会问你几句vue响应式的原理.以往学习这 ...

  9. vue 数组删除 dome没更新_详解Vue响应式原理

    摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归原作者所有. 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是 ...

  10. Vue.js 深入响应式原理

    深入响应式原理 现在是时候深入一下了!Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是普通的 JavaScript 对象.而当你修改它们时,视图会进行更新.这使得状态管理非常简单直接 ...

最新文章

  1. UNITY 内存问题资料收集
  2. 微型计算机实验代码,微型计算机原理实验1-数据传送
  3. 0414-复利计算再升级
  4. 自动生成考勤表_可自动变色的考勤表,逢周末自动更新,你会制作吗?
  5. word2010画布复制混乱
  6. php定时备份mysql,Windows服务器中PHP+MySQL设置定时备份
  7. java byte与char互转原理
  8. 既然有http 请求,为什么还要用rpc调用?
  9. 不同版本web.xml文件头声明
  10. 国美金融贷款Kube-apiserver源码分析(国美金融贷款)
  11. C++之常指针和指向常量的指针
  12. 【GitHub】GitHub上指定文件夹轻松下载
  13. 快手裁员30%,大部分年薪超100万!揭露职场真相:思考的深度,决定职场的高度...
  14. xshell如何将Windows文件上传到linux
  15. python小游戏 五子棋小游戏设计与实现
  16. 软考证书=获得职称?软考证书还能这样用
  17. CAD关于文字样式删除文字样式(com接口网页版)
  18. java监视器_java锁与监视器概念 为什么wait、notify、notifyAll定义在Object中 多线程中篇(九)...
  19. 智能传播中的人机融合智能
  20. Python3入门与进阶笔记(五):函数

热门文章

  1. C3模块-空洞可分离卷积存在的问题及轻量化语义分割模型架构技巧
  2. POJ-英语数字转化器
  3. oracle国家字符集
  4. word2007里插入分节符
  5. HTML5 目前无法实现的5件事
  6. 【学堂在线数据挖掘:理论方法笔记】第八天(4.2)
  7. 【生活相关】实验室专题研讨PPT模板说明备忘
  8. MFC(VS2010)编程实例之一(Edit Control控件)
  9. 排序算法专题-基数排序
  10. 从零基础入门Tensorflow2.0 ----二、5.3 实战sklearn超参数搜索