手写响应式实现
数据驱动
- 数据响应式、双向绑定、数据驱动
-
数据响应式
数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率
-
双向绑定
- 数据改变,视图改变;视图改变,数据也随之改变
- 使用
v-model
在表单元素上创建双向数据绑定
-
数据驱动是Vue独特的特性之一
- 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图
数据响应
Vue2.x:defineProperty
:数据劫持,当访问或者设置vm中的成员的时候,做一些干预操作;数据更改,更新DOM值
//数据劫持:当访问或者设置vm中的成员的时候,做一些干预操作
let vm = {}
Object.defineProperty(vm, 'msg', {
//可枚举(可遍历)
enumerable:true,
//可配置
configurable:true,
/**
* 1.当获取值的时候执行
*/
get(){
console.log("获取值")
return data.msg
}
/**
* 2.当设置值的时候执行
*/
set(newValue){
console.log("设置值")
if(newValue === data.msg){
return
}
data.msg = newValue
//数据更改,更新DOM值
document.querySelector('#app').textContent = data.msg
}
})
//test
vm.msg = '1'
console.log(vm.msg)
//控制台出现设置值 / 获取值
-
如果有多个属性需要转换
getter/setter
如何处理?在外层添加一个循环
forEach
proxyData(data)
function proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(vm, 'msg', {
...同上
})
})
}
Vue 3.x
Proxy
代理对象- 直接监听对象,而非属性
- ES6中新增,IE不支持,性能由浏览器优化
let data = {
msg: '1',
count: 0
}
let vm = new Proxy(data, {
/**
* 1.当访问vm的成员会执行
* target对象, key属性 不需要传递,由系统完成
*/
get(target, key不需要传递,由系统完成){
return target[key]
}
/**
* 2.当设置vm的成员会执行
*/
set(target,key,newValue){
if(target[key] === newValue){
return
}
target[key] = newValue
document.querySelector('#app').textContent = target[key]
}
})
//test
vm.msg = '1'
console.log(vm.msg)
发布/订阅模式
- 订阅者
- 发布者
- 信号中心
- vue的自定义事件
1.创建vue实例
2.$on注册事件,同一个事件可以注册多个事件处理函数
3.到了某时机使用$emit触发这个事件
- 兄弟组件的通信过程
1.创建eventBus.js
2.创建vue实例/事件中心
3.定义两个组件,组件互相不知道存在
4.A组件定义$emit触发B组件内容/发布消息;B组件注册$on事件/订阅消息
- 模拟自定义事件的实现
1.定义变量,去存储事件名称
//{ 'click' : [fn1, fn2], 'change': [fn]}
2.$emit:在事件对象中寻找对应的方法,再去执行
class EventEmitter {
constructor() {
//{ 'click' : [fn1, fn2], 'change': [fn]}
this.subs = Object.create(null)
}
//注册事件
//eventType:事件名称,handler:方法
$on(eventType, handler) {
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
}
//触发事件
//eventType:事件名称
$emit(eventType) {
if(this.subs[eventType]){
this.subs[eventType].forEach(handler => {
handler()
})
}
}
}
//test
let em = new EventEmitter()
em.$on('click', ()=>{
console.log('1')
})
em.$on('click', ()=>{
console.log('2')
})
em.$emit('click')
- 同时,模拟发布/订阅者模式可以通过兄弟传值体会
观察者模式
- 观察者(订阅者)
Watcher
update()
:当事件发生时,具体要做的事情
- 目标(发布者)
Dep
- subs数组:存储所有的观察者
addSub()
:添加观察者notify()
:当事件发生时,调用所有观察者的update()方法
- 和发布/订阅模式的区别:没有事件中心;并且发布者需要知道订阅者的存在
//发布者-目标
class Dep{
constructor(){
//记录所有的订阅者
this.subs = []
}
//添加观察者
addSub(sub) {
if(sub && sub.update){
this.subs.push(sub)
}
}
//发布通知
notify(){
this.subs.forEach(sub => {
sub.update()
})
}
}
//订阅者-观察者
class Watcher() {
update(){
console.log('1')
}
}
//test
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)//添加观察者
dep.notify()//通知观察者,并且调用方法
//不需要创建Vue实例
发布/订阅和观察者模式总结
- 观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者的方法,所以观察者的订阅者与发布者之间是存在依赖的
- 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方存在
代码模拟vue响应式原理
- vue基本结构
- 打印vue实例观察
- 整体结构
Vue
-
功能:
- 负责接收初始化的参数(选项)
- 负责把data中的属性注入到Vue实例,转换成
getter/setter
- 负责调用
observer
监听data中所有属性的变化 - 负责调用
compiler
解析指令/插值表达式
-
结构
$options $el $data //属性:记录构造函数传过来的参数 _proxyData() //私有方法:把data中属性转换注入实例
-
代码
//vue.js
class Vue {
constructor(options) {
//1. 通过属性保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$options = typeof options === 'string' ? document.query.querySelector(options.el) : options.el//如果是DOM对象直接返回
//2. 把data中的成员转换 为getter和setter,注入到实例中
this._proxyData(this.$data)
//3. 调用observer对象,监听数据的变化
new Observer(this.$data)
//4. 调用compiler对象,解析指令和插值表达式
new Compiler(this)
}
_proxyData(data){
//遍历data中所有的属性
Object.keys(data).forEach(key => {
//把data属性注入到vue实例中
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue){
if(newValue === data[key]){
return
}
data[key] = newValue
}
})
})
}
}
使用:
<!--index.html-->
<script src="./js/vue.js"></script>
<script>
let vm = new Vue({
el:'#app',//选择器
data: {
msg: 'hello',
count: 100
}
})
</script>
Observer
-
功能:
- 负责把data选项中的属性转换成响应式数据
- data中的某个属性也是对象,把该属性转换成响应式数据
- 数据变化发送通知:集合观察者去实现
-
结构
walk(data) //遍历所有属性 defineReactive(data,key,value) //把属性转换成get和set
-
代码
新建observer.js
class observer {
constructor(data) {
this.walk(data)//从vue接收data
}
walk(data) {
//1.判断data是否是对象
if(!data || typeof data !== 'object'){
return
}
//2.遍历data对象的所有属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
//使用到了this,箭头函数不会改变this的指向
})
}
defineReactive(boj, key, val){
let that = this
//为每一个属性创建对应的dep对象:负责收集依赖,并发送通知
let dep = new Dep()
//如果val是对象,把val内部的属性
this.walk(val)
Object.defineProperty(obj, key, {
enumrable: true,
configurable: true,
get() {
Dep.target && Dep.addSub(Dep.target)//收集依赖:Dep.target里存储的就是watcher对象;在dep类中并没有定义它,是在watcher类中定义的
return val
},
set(newValue) {
if(newValue === val){
return
}
val = newValue
this.walk(newValue)
//发送通知
dep.notify()//发送通知
}
})
}
}
使用:
<!--index.html-->
<script src="./js/observer.js"></script>
结果:把$data转换为get和set
defineReactive
- 需要修改$data内数据为响应式
Compiler类
-
功能
- 负责编译模板,解析指令/差值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
-
结构
el//DOM对象 vm//vue实例 compile(el)//遍历DOM对象的所有节点 //解析差值表达式 compileElement(node)//解析元素中指令 compileText(node)//解析差值表达式 isDirective(attrName)//判断当前属性是否是指令 //判断是文本节点还是元素节点 isTextNode(node) isElementNode(node)
-
代码
//compiler.js
class compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
//编译模板,处理文本节点和元素节点
compile(el) {
let childNodes = el.childNodes
//循环遍历节点:第一层子节点
Array.from(childNodes).forEach(node => {
//处理文本节点
if(this.isTextNode(node)) {
this.complieText(node)
}
//处理元素节点
else if(this.isElementNode){
this.compileElement(node)
}
//判断node节点,是否有子节点,如果有子节点,要递归调用compile
if(node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
//编译元素节点,处理指令
compileElement(node) {
//console.log(node.attributes)
/**
* 属性名称和属性值name/value
*/
//遍历所有的属性节点
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
}
})
}
update (node, key, attrName) {
let updateFn = this.[attrName + 'Updater']
updateFn && updateFn.call(this, node, this.vm[key], key)
//使用call改变内部方法的指向,此处的this就是compile对象???????????????????
}
/**
* 都需要创建watcher对象
*/
//处理v-text指令
textUpdater(node, value, key) {
node.textContent = value
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
//v-model
modelUpdater(node, value, key) {
node.value = value
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
//双向绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
//编译文本节点,处理差值表达式
compileText(node) {
//{{ msg }}
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if(reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
//创建watcher对象。当数据改变更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
/**
* 创建watcher对象end
*/
//判断元素是否是指令:判断是否是以'v-'开头
isDirective(attrName) {
return attrName.startsWith('v-')
}
//判断节点是否是文本节点:看nodeType的值
isTextNode(node) {
return node.nodeType === 3
}
//判断节点是否是元素节点
isElementNode(node) {
return node.nodeType === 1
}
}
使用:
<!--index.html-->
<script src="./js/compiler.js"></script>
Dep
-
功能
- 收集依赖,添加观察者
watcher
- 通知所有观察者
- 收集依赖,添加观察者
-
结构
subs//数组,存储dep中所有的watcher addSub(sub) notify()//发布通知,通知所有的观察者
-
代码
//dep.js
class Dep {
constructor() {
//存储所有的观察者
this.subs = []
}
//添加观察者
addSub(sub) {
if(sub && sub.update) {
this.subs.push(sub)
}
}
//发送通知
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
Watcher
-
功能
- 当数据变化触发依赖,dep通知所有的Wathcher实例更新视图
- 自身实例化的时候往dep对象中添加自己
-
结构
vm//vue实例 key//data中的属性名称 cb//回调函数:更新视图 oldValue//记录数据变化之前的值 update()/比较新旧值是否发生变化,不更新视图
-
代码
//watcher.js
class Watcher {
constructor(vm, key, cb){
this.vm = vm
this.key = key
this.cb = cb
//把watcher对象记录到Dep类的静态属性target中
Dep.target = this
//触发get方法,在get方法中会调用addSub
this.oldValue = vm[key]
Dep.target = null
}
//更新视图
update() {
let newValue = this.vm[this.key]
if(this.oldValue === newValue){
return
}
this.cb(newValue)//如果值不等要更新视图
}
}
使用:注意顺序
<!--index.html-->
<script src="./js/dep.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compiler.js"></script>
<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>
创建watcher类对象
- 指令和差值表达式都是依赖数据的,所有依赖数据的位置都需要创建一个watcher对象,当数据改变时,会通知所有watcher对象改变视图
- 在
compiler.js
中,textUpdater
、modelUpdate
和compileText
需要创建watcher对象
双向绑定
- 视图变化 <--> 数据变化
- 给
v-model
设置的:modelUpdate()
//compiler.js
modelUpdate(node, value, key){
...
//双向绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
调试
首次渲染/数据改变
总结
流程回顾:
-
给属性重新赋值成对象,是否是响应式的?
vm:{msg : 1} vm.msg = {w:'1'}
是
-
给Vue实例新增一个成员是否是响应式的?
不是。在Vue的构造函数中
new Observer(this.$data)
会把所有data转换为响应式数据,这件事在new Vue中执行。如果仅仅是vm.test='1'
只是给vm增加了一个js属性。如何把新增数据转换为响应式数据
- Vue
- 记录传入的选项,设置 data/el
- 把 data 的成员注入到 Vue 实例
- 负责调用 Observer 实现数据响应式处理(数据劫持)
- 负责调用 Compiler 编译指令/插值表达式等
- Observer
- 数据劫持
- 负责把 data 中的成员转换成 getter/setter
- 负责把多层属性转换成 getter/setter
- 如果给属性赋值为新对象,把新对象的成员设置为 getter/setter
- 添加 Dep 和 Watcher 的依赖关系 数据变化发送通知
- 数据劫持
- Compiler
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染过程
- 当数据变化后重新渲染
- Dep
- 收集依赖,添加订阅者(watcher) 通知所有订阅者
- Watcher
- 自身实例化的时候往dep对象中添加自己
- 当数据变化dep通知所有的 Watcher 实例更新视图
本文首发于我的GitHub博客,其它博客同步更新。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!