[TOC]
>[success] # 手写 -- 简单的vue数据响应模型(一)
~~~
1.首先前需要先分析生成的'Vue' 实例上挂载了那些关于响应式数据相关的属性
// 声明一个简单vue 实例
const vm = new Vue({
el: '#app',
data: {
msg: '测试',
info: {
name: 'w'
}
}
})
console.log(vm)
~~~
* 上面实例控制台打印参数
![](https://img.kancloud.cn/c7/10/c7101a986a6e74a96e4c8a9f59b6ada4_755x650.png)
>[danger] ##### 对这部分参数说明
~~~
1.为什么Vue 会把上面案例中的data属性中的变量(msg和info)挂载到Vue 实例上?
Vue 构造函数内部需要把 data 中的成员转换成 getter 和 setter 注入到 Vue实例,
这样可以直接通过 this.msg, this.info使用
2.'$options':简单认为把构造函数的参数记录到 $options 当中
3.'$el':$el 对应选项中的 el, 我们设置 el 选项时,可以是选择器,也可以是一个DOM对象。
如果是选择器,vue 构造函数内部需要把这个选择器转换成对应的DOM对象
4.'$data':data 选项中的成员被记录到 $data 中并且转换成 getter 和 setter,
$data 中的 setter 是真正监视数据变化的地方
5.'_data' :和 $data 指向的是同一个对象,_ 开头的是私有成员,$ 开头的是公共成员
~~~
>[info] ## 分解实现一个简单数据响应
~~~
1.
~~~
>[danger] ##### 实现vue 部分
~~~
1.根据上面分析,需要将实例化时候传入的构造函数进行处理
1.1.将初始化实例传入的配置绑定在Vue 实例的'$opition'
1.2.将初始化实例传入对象参数中'el' 字段绑定在Vue 实例的'$el' 上
1.3.将初始化实例传入对象参数中的data中值绑定在Vue 实例上(具备getter 和setter)
1.4.将初始化实例传入对象参数中的data绑定在Vue 实例上'$data'上(具备getter 和setter)
2.设计自己 Vue 类根据上面分析我们类需要'$options','$data','$el' 这些属性,以及一个'_proxyData'
方法将data上的属性绑定在Vue实例上
~~~
~~~
class Vue {
constructor(options) {
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 将data 数据挂载到Vue 实例上
this._proxyData(this.$data)
// 数据劫持
new Observe(this.$data)
}
_proxyData(data) {
// 遍历data中的属性
Object.keys(data).forEach((key) => {
// 将data 中的属性注入到vue实例中
Object.defineProperty(this, key, {
enumerable: true,
constructor: true,
get() {
return data[key]
},
set(newValue) {
if (newValue === data[key]) return
console.log(1)
data[key] = newValue
}
})
})
}
}
~~~
>[danger] ##### Observe --监听data中的所有属性的变化
~~~
1.Observe 对数据对象的所有属性进行监听,负责把整个$data 中的属性都转换成响应式数据
2.因为可以能会出现 {info:{name:'w'}} 这里不仅仅要把info变成响应式的也需要把,name变成响应式
的因此数据需要递归简单的说'data中的某个属性也是对象,把该属性转化成响应式数据'
~~~
~~~
//数据劫持
class Observe {
constructor(data) {
this.walk(data)
}
// 循环遍历data对象的所有属性,作为递归的一个判断条件
walk(data) {
if (typeof data !== 'object' || !data) return
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(data, key, val) {
let that = this
// 负责收集依赖,并发送通知
// let dep = new Dep()
this.walk(val)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
// 将需要的观察者 收集到发布者中
// Dep.target && dep.addSub(Dep.target)
return val
},
set(newValue) {
if (newValue === val) return
val = newValue
console.log(2)
// 如果赋值的 是对象需要从新进行getter 和setter 绑定
that.walk(newValue)
// 发送通知
// dep.notify()
}
})
}
}
~~~
* 上面代码中get 里面为什么没有直接使用 data[key]
~~~
1.上面代码get 方法中使用的'return val' 为什么 不能是'return data[key]'?
原因:obj[key]访问自身的相应属性,会触发自身的get 函数 造成死循环等同下面效果
class Test {
get name() {
return this.name
}
}
const test = new Test()
test.name
~~~
>[danger] ##### 关于 Observe 和_proxyData 疑问
~~~
1.讲解一个我自己误区很久的问题'_proxyData' 和'Observe ',二者都是做响应数据区别在哪里?
首先'_proxyData' 是给实类挂载data上的属性方便后续直接this的形式调用,Observe 是将整个data
中所有数据进行数据响应的绑定
2.新的问题'Observe ' 使用了递归形式保证'data中的某个属性也是对象,把该属性转化成响应式数据',
那'_proxyData' 不递归?
先看'_proxyData'关于响应数据代码
Object.keys(data).forEach((key) => {
// 将data 中的属性注入到vue实例中
Object.defineProperty(this, key, {
enumerable: true,
constructor: true,
get() {
return data[key]
},
set(newValue) {
if (newValue === data[key]) return
console.log(1)
data[key] = newValue
}
})
})
这里明确第一点,这里的this 是vue实例,他仅仅是将data这些key绑定在了vue实例,此时的$data属性
并没有转换成响应的数据,'Observe' 才是本质上做到了将整个'$data'数据转换成了响应数据
('data中的某个属性也是对象,把该属性转化成响应式数据')
这里还有一个点注意'_proxyData'中的get 和set 本质上操作的实际是'$data',这样当在实例上data值进行操作
或者改变的时候由于进行了数据绑定会渐渐的触发$data上的数据绑定
因为'_proxyData' 会间接触发'$data',因此本质上只要'$data'只要所有数据都转换成响应数据即可
3.现在有个数据
const vm = {name:'www',info:{age:12}}
当进行vm.name = 'sss' 赋值操作的时候,先触发'_proxyData' 上的set方法,set方法内部可以看成是
'$data[key] = newValue',$data 又被'Observe' 做了数据响应出发了'Observe' 中的set
现在如果进行vm.info.age = 16 这时候注意了虽然'_proxyData' 只将第一层key和vue实例做了数据响应
此时vm.info 会触发'_proxyData'的get 因此实际得到是$data['info']此时会触发'Observe' 关于key为info
的get触发$data['info']中的age由于在'Observe'做了递归处理因此相当于$data['info'].age = 16直接触发
了age的set
总结:'_proxyData' 是给vue实例挂载data上的属性,虽然只对了最外一层做了数据响应,但是他们的get和set
实际触发的是'$data',我们使用了'Observe'将'$data'做了深度的数据响应,因此'_proxyData'会通过最外层
间接触发'$data'那些进行深层绑定的数据响应。'_proxyData' 只是为了让data绑定在vue实例上让开发者
方便使用,但是整个是数据调用和数据变化发送通知都是要在'Observe' 中完成,'_proxyData' 这样可以间接
调用'Observe','Observe' 也可根据数据变化发送通知(观察者)
~~~
>[danger] ##### Compiler -- 模板指令解析
~~~
1.上面的方法已经对数据方面处理好了,接下需要对html 页面上 'v-'指令 以及'{{ ... }}' 模板语法和处理的数据
可以绑定在一起进行渲染整个'Compiler '主要工作'编译模板,解析指令、差值表达式'这三个方面进行处理
2.'Compiler ' 期待解决的问题'负责页面的首次渲染','当数据变化后重新渲染视图'
3.设计结构
*data
el - vue中传入的$el
vm - Vue实例
*methods - 都是DOM操作
compile(el) - 遍历DOM对象的所有节点,判断节点并解析相应东西
compileElement(node) - 解析元素节点中的指令
compileText(node) - 解析文本的插值表达式
isDirective(attrName) - 判断属性是否为指令
isTextNode(node) - 判断是否为文本节点
isElementNode(node) - 判断是否为元素节点
4.<p v-if='test'>{{ 我是文本节点 }} </p>, 首先明确p 标签是元素节点,里面的表达式是文本节点
想要区分它们 node 对象有一个属性是'nodeType ',3是文本节点 ,1是元素节点
~~~
~~~
// compiler.js
// 解析Vue并进行DOM操作
class Compiler {
// 构造函数传入vue实例
constructor(vm) {
// 创建相应属性
this.el = vm.$el
this.vm = vm
// Vue实例调用Compiler则开始处理DOM
this.compile(this.el)
}
// 判断元素属性是否是指令
isDirective(attrName) {
// 判断属性是否以 ‘v-’开头
return attrName.startsWith('v-')
}
// 判断节点是否是文本节点
isTextNode(node) {
// 节点有nodeType 节点类型以及nodeValue 节点值
// nodeType值为 3是文本节点 1是元素节点
return node.nodeType === 3
}
// 判断节点是否是元素节点
isElementNode(node) {
return node.nodeType === 1
}
// 编译模板,处理文本节点和元素节点
compile(el) {
// 获取所有的子节点 childNodes为子节点 children为子元素
let childNodes = el.childNodes
// 将伪数组转换为数组 并遍历数组中的每一个节点
// 此处仅为一层子节点
Array.from(childNodes).forEach(node => {
// 此处箭头函数中的this为compiler实例
// 处理文本节点
if (this.isTextNode(node)) {
this.compileText(node)
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node)
}
// 判断node节点,是否有子节点,如果有子节点,要递归调用compile
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 调用对应指令的方法
update(node, key, attrName) {
let updateFn = this[attrName + 'Updater']
// 指令方法存在则调用该方法
updateFn && updateFn.call(this, node, this.vm[key], key)
}
// 编译元素节点,处理指令
compileElement(node) {
// console.log(node.attributes)
// 遍历所有的属性节点 node.attributes获取属性节点
Array.from(node.attributes).forEach(attr => {
// 获取属性名字
let attrName = attr.name
// 判断是否是指令
if (this.isDirective(attrName)) {
// v-text --> text 截取字符串
attrName = attrName.substr(2)
// 获取属性值
let key = attr.value
this.update(node, key, attrName)
}
})
}
// 编译文本节点,处理差值表达式
compileText(node) {
// 以对象类型打印
// console.dir(node)
// {{ msg }}
// . 匹配任意字符不包括换行 | + 前面的内容出现一或多次 | ?非贪婪模式 尽快结束匹配 | () 分组含义 提取括号中的匹配内容
let reg = /\{\{(.+?)\}\}/
// 文本节点中的内容 使用 node.textContent / node.nodeValue 获取
let value = node.textContent
if (reg.test(value)) {
// $num 为第num个小括号()分组的内容
let key = RegExp.$1.trim()
// 将正则匹配内容替换为Vue实例属性中的值
node.textContent = value.replace(reg, this.vm[key])
// 创建watcher对象,当数据改变更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
// 处理 v-text 指令
textUpdater(node, value, key) {
// 将节点的文本内容更改为Vue属性值
node.textContent = value
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// v-model
modelUpdater(node, value, key) {
// 表单元素的值 value 等于Vue属性值
node.value = value
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
//双向绑定 监听节点中的输入框
node.addEventListener('input', () => {
// 触发属性set机制 发布者通知所有的观察者执行函数 更新DOM视图
this.vm[key] = node.value
})
}
}
~~~
>[danger] ##### 观察者
~~~
1.现在要做的就是数据改变了如何和视图形成关联,利用观察者模式,数据改变了
就通知它在视图时候对应的订阅者,触发渲染更新视图
2.Dep(Dependency) -- 发布者'收集依赖,添加观察者(watcher),通知所有观察者'
data
subs - 存储所有的watcher
methods
addSub(sub) - 添加观察者
notify() - 发布通知
3.Watcher -- 观察者'当数据变化触发依赖,dep通知所有的watcher实例更新视图'
'自身实例化的时候往dep对象中添加自己'
data
vm - Vue实例
key - data中的属性名称
cb - 回调函数,如何更新视图
oldValue - 数据变化之前的值
methods
update() - 更新视图
~~~
* Dep
~~~
// dep.js
// 发布者
class Dep {
constructor () {
// 存储所有的观察者
this.subs = []
}
// 添加观察者
addSub (sub) {
// 不为空且拥有update方法
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知
notify () {
// 遍历所有的观察者并更新视图
this.subs.forEach(sub => {
sub.update()
})
}
}
~~~
* Watcher
~~~
// watcher.js
// 观察者
class Watcher {
constructor (vm, key, cb) {
// vue实例
this.vm = vm
// data中的属性名称
this.key = key
// 回调函数负责更新视图
this.cb = cb
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this
// 触发get方法,在get方法中会调用addSub
this.oldValue = vm[key]
Dep.target = null
}
// 当数据发生变化的时候更新视图
update () {
// 获取Vue中的属性
let newValue = this.vm[this.key]
// 属性值未变化
if (this.oldValue === newValue) {
return
}
this.cb(newValue)
}
}
~~~
>[danger] ##### 总结
~~~
1.当创建vue 实例的时候,会将data中的数据挂载到实例上,并且会调用'Observer'将data中的所有
属性成员转成gettter、setter,并且每个属性都有自己的'Dep'观察者,在getter时候添加订阅者,
在setter 时候发送通知(只有赋值的时候视图是需要发生变化因此此时发送通知告诉订阅者),
所有的订阅者也就是Watcher在视图第一次渲染的时就行了触发订阅,'Compiler' 是处理模板渲染,
这个类第一次初始化是在vue实例创建时候执行,'Compiler'这时候会把最初的模板渲染出来例如
这种模板语法'{{ ... }}'解析成对应的data值在以他的'compileText' 方法为例里面会执行
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
这里要注意'Watcher'里面几个小操作,他会给Dep观察者类方法target属性加一个当前'Watcher'对象
Dep.target = this,紧接着执行this.oldValue = vm[key],这样会触发'vm[key]'的getter属性,间接
触发了$data中的getter ,$data中的getter在'Observer'进行的这里的getter 有一段代码
'Dep.target && dep.addSub(Dep.target)',因为在上面此时的Dep.target 记录就是当前的订阅者'watcher'
对象,成功将'watcher'这个订阅者和'dep'发布者绑定在一起
2.根据上面的分析可以知道如果这个属性没有在页面上使用,那在这个属性getter中不会有和视图相关的
观察者模型
4.MVVM 框架解决了视图和状态的同步问题,因此很好理解数据变化的时候 视图也需要变化
~~~
![](https://img.kancloud.cn/8c/3a/8c3ace9ef69e23b2696be0836d9942c8_1127x535.png)
- Vue--基础篇章
- Vue -- 介绍
- Vue -- MVVM
- Vue -- 创建Vue实例
- Vue -- 模板语法
- Vue -- 指令用法
- v-cloak -- 遮盖
- v-bind -- 标签属性动态绑定
- v-on -- 绑定事件
- v-model -- 双向数据绑定
- v-for -- 只是循环没那么简单
- 小知识点 -- 计划内属性
- key -- 属性为什么要加
- 案例说明
- v-if/v-show -- 显示隐藏
- v-for 和 v-if 同时使用
- v-pre -- 不渲染大大胡语法
- v-once -- 只渲染一次
- Vue -- class和style绑定
- Vue -- filter 过滤器
- Vue--watch/computed/fun
- watch -- 巧妙利用watch思想
- Vue -- 自定义指令
- Vue -- $方法
- Vue--生命周期
- Vue -- 专属ajax
- Vue -- transition过渡动画
- 前面章节的案例
- 案例 -- 跑马灯效果
- 案例 -- 选项卡内容切换
- 案例-- 筛选商品
- 案例 -- 搜索/删除/更改
- 案例 -- 用computed做多选
- 案例 -- checked 多选
- Vue--组件篇章
- component -- 介绍
- component -- 使用全局组件
- component -- 使用局部组件
- component -- 组件深入
- component -- 组件传值父传子
- component -- 组件传值子传父
- component -- 子传父语法糖拆解
- component -- 父组件操作子组件
- component -- is 动态切换组件
- component -- 用v-if/v-show控制子组件
- component -- 组件切换的动画效果
- component -- slot 插槽
- component -- 插槽2.6
- component -- 组件的生命周期
- component -- 基础组件全局注册
- VueRouter--获取路由参数
- VueRouter -- 介绍路由
- VueRouter -- 安装
- VueRouter -- 使用
- VueRouter--router-link简单参数
- VueRouter--router-link样式问题
- VueRouter--router-view动画效果
- VueRouter -- 匹配优先级
- vueRouter -- 动态路由
- VueRouter -- 命名路由
- VueRouter -- 命名视图
- VueRouter--$router 获取函数
- VueRouter--$route获取参数
- VueRouter--路由嵌套
- VueRouter -- 导航守卫
- VueRouter -- 写在最后
- Vue--模块化方式结构
- webpack--自定义配置
- webpack -- 自定义Vue操作
- VueCli -- 3.0可视化配置
- VueCli -- 3.0 项目目录
- Vue -- 组件升级篇
- Vue -- 组件种类与组件组成
- Vue -- 组件prop、event、slot 技巧
- Vue -- 组件通信(一)
- Vue -- 组件通信(二)
- Vue -- 组件通信(三)
- Vue -- 组件通信(四)
- Vue -- 组件通信(五)
- Vue -- 组件通信(六)
- Vue -- bus非父子组件通信
- Vue -- 封装js插件成vue组件
- vue组件分装 -- 进阶篇
- Vue -- 组件封装splitpane(分割面板)
- UI -- 正式封装
- Vue -- iview 可编辑表格案例
- Ui -- iview 可以同时编辑多行
- Vue -- 了解递归组件
- UI -- 正式使用递归菜单
- Vue -- iview Tree组件
- Vue -- 利用通信仿写一个form验证
- Vue -- 使用自己的Form
- Vue -- Checkbox 组件
- Vue -- CheckboxGroup.vue
- Vue -- Alert 组件
- Vue -- 手动挂载组件
- Vue -- Alert开始封装
- Vue -- 动态表单组件
- Vue -- Vuex组件的状态管理
- Vuex -- 参数使用理解
- Vuex -- state扩展
- Vuex -- getters扩展
- Vuex--mutations扩展
- Vuex -- Action 异步
- Vuex -- plugins插件
- Vuex -- v-model写法
- Vuex -- 更多
- VueCli -- 技巧总结篇
- CLI -- 路由基础
- CLI -- 路由升级篇
- CLI --异步axios
- axios -- 封装axios
- CLI -- 登录写法
- CLI -- 权限
- CLI -- 简单权限
- CLI -- 动态路由加载
- CLI -- 数据性能优化
- ES6 -- 类的概念
- ES6类 -- 基础
- ES6 -- 继承
- ES6 -- 工作实战用类数据管理
- JS -- 适配器模式
- ES7 -- 装饰器(Decorator)
- 装饰器 -- 装饰器修饰类
- 装饰器--修饰类方法(知识扩展)
- 装饰器 -- 装饰器修饰类中的方法
- 装饰器 -- 执行顺序
- Reflect -- es6 自带版本
- Reflect -- reflect-metadata 版本
- 实战 -- 验证篇章(基础)
- 验证篇章 -- 搭建和目录
- 验证篇章 -- 创建基本模板
- 验证篇章 -- 使用
- 实战 -- 更新模型(为了迎合ui升级)
- 实战 -- 模型与接口对接
- TypeSprict -- 基础篇章
- TS-- 搭建(一)webpack版本
- TS -- 搭建(二)直接使用
- TS -- 基础类型
- TS -- 枚举类型
- TS -- Symbol
- TS -- interface 接口
- TS -- 函数
- TS -- 泛型
- TS -- 类
- TS -- 类型推论和兼容
- TS -- 高级类型(一)
- TS -- 高级类型(二)
- TS -- 关于模块解析
- TS -- 声明合并
- TS -- 混入
- Vue -- TS项目模拟
- TS -- vue和以前代码对比
- TS -- vue简单案例上手
- Vue -- 简单弄懂VueRouter过程
- VueRouter -- 实现简单Router
- Vue-- 原理2.x源码简单理解
- 了解 -- 简单的响应式工作原理
- 准备工作 -- 了解发布订阅和观察者模式
- 了解 -- 响应式工作原理(一)
- 了解 -- 响应式工作原理(二)
- 手写 -- 简单的vue数据响应(一)
- 手写 -- 简单的vue数据响应(二)
- 模板引擎可以做的
- 了解 -- 虚拟DOM
- 虚拟dom -- 使用Snabbdom
- 阅读 -- Snabbdom
- 分析snabbdom源码 -- h函数
- 分析snabbdom -- init 方法
- init 方法 -- patch方法分析(一)
- init 方法 -- patch方法分析(二)
- init方法 -- patch方法分析(三)
- 手写 -- 简单的虚拟dom渲染
- 函数表达解析 - h 和 create-element
- dom操作 -- patch.js
- Vue -- 完成一个minVue
- minVue -- 打包入口
- Vue -- new实例做了什么
- Vue -- $mount 模板编译阶段
- 模板编译 -- 分析入口
- 模板编译 -- 分析模板转译
- Vue -- mountComponent 挂载阶段
- 挂载阶段 -- vm._render()
- 挂载阶段 -- vnode
- 备份章节
- Vue -- Nuxt.js
- Vue3 -- 学习
- Vue3.x --基本功能快速预览
- Vue3.x -- createApp
- Vue3.x -- 生命周期
- Vue3.x -- 组件
- vue3.x -- 异步组件???
- vue3.x -- Teleport???
- vue3.x -- 动画章节 ??
- vue3.x -- 自定义指令 ???
- 深入响应性原理 ???
- vue3.x -- Option API VS Composition API
- Vue3.x -- 使用set up
- Vue3.x -- 响应性API
- 其他 Api 使用
- 计算属性和监听属性
- 生命周期
- 小的案例(一)
- 小的案例(二)-- 泛型
- Vue2.x => Vue3.x 导读
- v-for 中的 Ref 数组 -- 非兼容
- 异步组件
- attribute 强制行为 -- 非兼容
- $attrs 包括 class & style -- 非兼容
- $children -- 移除
- 自定义指令 -- 非兼容
- 自定义元素交互 -- 非兼容
- Data选项 -- 非兼容
- emits Option -- 新增
- 事件 API -- 非兼容
- 过滤器 -- 移除
- 片段 -- 新增
- 函数式组件 -- 非兼容
- 全局 API -- 非兼容
- 全局 API Treeshaking -- 非兼容
- 内联模板 Attribute -- 非兼容
- key attribute -- 非兼容
- 按键修饰符 -- 非兼容
- 移除 $listeners 和 v-on.native -- 非兼容
- 在 prop 的默认函数中访问 this -- ??
- 组件使用 v-model -- 非兼容
- 渲染函数 API -- ??
- Slot 统一 ??
- 过渡的 class 名更改 ???
- Transition Group 根元素 -- ??
- v-if 与 v-for 的优先级对比 -- 非兼容
- v-bind 合并行为 非兼容
- 监听数组 -- 非兼容