前言
大家好,我是console没有log
,一名大二的专科学生,这篇文章讲给大家分享一下vue的数据驱动视图的原理,也夹杂着模板编译的知识点。我会把这个文章写的非常简单,相信我,你一定看的懂。
这个文章我主要从需求、流程图、代码里讲解原理的逻辑,话不多说了我现在开始写文章。
走进项目的小知识点
闭包
function run() {
let a = 11
return a
}
let num = run() // 当此次代码执行完之后在run方法局部作用域的变量a就会呗垃圾回收机制处理掉
let num2 = run() // 执行这次代码的时候,则是会重新创建一个局部作用域,产生一个变量a,等待函数执行完毕则会再次被垃圾回收机制处理掉
function run () {
let a = 11
let b = 12
function asd() {
a = a+1
return a
}
return asd
}
let me = run() // 此时按照原来的方案,run的局部作用域里面的变量会在run方法执行完毕之后,会被垃圾回收机制回收掉,但是发现a变量被另一个局部作用域使用了,则就会产生一个效果,就是run这个局部作用域下的其他变量会被垃圾回收机制处理掉,a变量会保存到内存中。
let num = me() // 而这里就会读取到内存中变量a的值并且加一返回出来,这里会吧a变量销毁吗? 答案是并不会,a还在内存中 这里num的值为 12
console.log(num); // -- 12
let num2 = me() // 这里的打印结果是13,就很显而易见,调用了两次返回的me方法用的是同一个内存地址的a变量
console.log(num2); // -- 13
这里提一句函数的参数也是享有这样的权利的,函数声明变量,也就是在局部作用域声明了一个变量只是没有用 var let const 这几个声明标志而已,看一下代码
function run(a) {
function asd() {
a += 1
return a
}
return asd
}
let me = run(1)
let num = me()
console.log(num); // -- 2
let num2 = me()
console.log(num2); // -- 3
// 原理就不说了,就是想表达函数的参数也是可以实现的。
Object.defineProperty
可以直接看这个链接Object.defineProperty() - JavaScript | MDN (mozilla.org),如果看不懂的话再来看我的解释。
let obj = {}
Object.defineProperty(obj, 'a', {
value: '111',
})
console.log(obj); // -- { a : '111' }
Object.keys(obj).forEach(key => console.log(key)); // -- 'a'
// 此方法会直接在一个对象上定义一个新属性,并可以设置值
let obj = {}
Object.defineProperty(obj, 'a', {
value: '111',
enumerable: false // 这里可以设置这个键的是否可以枚举,如果是false的话我们不管是用for...in...,还是Object.keys等都不会把它读取出来
})
console.log(obj); // -- { a : '111' }
Object.keys(obj).forEach(key => console.log(key)); // -- 没有打印
for (let key in obj) {
console.log(key); // 没有打印
}
let obj = {}
Object.defineProperty(obj, 'a', {
value: '111',
writable: false // 设置这个值是否可以修改,如果是false则是不可以修改
})
console.log(obj); // -- { a : '111' }
obj.a = '222'
console.log(obj) // -- { a : '111' }
// 下面的这个图片就是此代码块运行之后的打印,发现超出最大的栈内存了,这是为什么呢
let obj = {
a: 11
}
Object.defineProperty(obj, 'a', {
get() {
console.log('obj.a被使用了');
return '我是a'
},
set(val) {
// console.log(val) // -- 12
obj.a = val; // 这是因为,如果你直接给obj.a 赋值的话就相当于由一次出发了set方法,无限触发set就会爆仓
}
})
let a = obj.a // -- ‘我是a’
obj.a = 12
怎么样解决这个问题呢,看以下代码
解决方法一
// 我们可以通过一个变量来接受我们设置的新的值,get的时候也返回我们设置了新的值的变量
let obj = {
a: 11
}
let temp;
Object.defineProperty(obj, 'a', {
get() {
console.log('obj.a被使用了');
return temp
},
set(val) {
temp = val;
}
})
obj.a = 12
let as = obj.a
console.log('as的值是:', as);
// 下面是运行结果
解决方法二
let obj = {
a: 11
}
function defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
get() {
return value;
},
set(newValue) {
value = newValue;
}
})
}
defineReactive(obj)
let as = obj.a
console.log(as);
obj.a = 22
console.log(obj);
设计模式--订阅发布模式
我觉得这片文章写的挺好,大家可以去看一看观察者模式 vs 发布订阅模式 - 知乎 (zhihu.com)看完各篇文章以后,再回来看我的解释
构建基础目录
整体功能
因为我们只要研究数据驱动视图,所以我们只实现data的功能就好了
理论知识
理论实现
数据驱动序章
-
在
index.js
写入vue类的基本配置- 保存要挂载的dom元素,默认我们绑定id为app的dome元素
- 执行传进来的data函数,获取他返回的数据对象
class Vue { constructor(option) { this.$el = option.el ? document.querySelector(option.el) : document.querySelector('#app') this.$data = option.data() } }
-
我们想要直接可以用this访问data中的数据
- 在Vue这个类中建立
_proxyData
方法,接收一个参数(data) - 通过
Object.keys
方法吧对象中的所有的键提取成一个数组 - forEach遍历由data键值形成的数组
- 通过
Object.defineProperty
方法实现可以实现this访问data数据的需求 - 在构造函数中执行_proxyData
class Vue { constructor(option) { this.$el = option.el ? document.querySelector(option.el) : document.querySelector('#app') this.$data = option.data() this._proxyData(this.$data) } _proxyData(data) { Object.keys(data).forEach(key => { Object.defineProperty(this, key, { get() { return data[key] }, set(newValue) { if(newValue === data[key]) return data[key] = newValue } }) }) } }
写到这先不着急往下面看,咱们敲一下代码试试到底可不可以
let vue = new Vue({ data() { return { name: '我是console没有log' } } }) console.log(vue.$data.name); // 我是console没有log ---本来是要这么写的 console.log(vue.name); // 我是console没有log
完全没问题哦,这样就可以了,嘿嘿。
- 在Vue这个类中建立
-
实现观察者类
Observer
- 在根目录创建Observer.js,并且创建Observer类
- 上边我们用到
Object.defineProperty
就只是为了实现this可以访问data的数据,这次我们重新给data中的数据使用Object.defineProperty
,这次就是为了监听data中的数据变化了
export 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, value) { const self = this self.walk(value) Object.defineProperty(obj, key, { get() { return value }, set(newValue) { if(newValue === value) return value = newValue self.walk(value) } }) } }
稍等,我画一个流程图,再给大家解释一下......
Ok!画好了
这里里面还没有任何更改视图的操作,别着急因为还有三个类要写,我们现在Vue这个类中用上Observer类好伐~
import {Observer} from './Observer' class Vue { constructor(option) { this.$el = option.el ? document.querySelector(option.el) : document.querySelector('#app') this.$data = option.data() this._proxyData(this.$data) new Observer(this.$data) } _proxyData(data) { Object.keys(data).forEach(key => { Object.defineProperty(this, key, { get() { return data[key] }, set(newValue) { if(newValue === data[key]) return data[key] = newValue } }) }) } }
这样好了,已经写了好一会的逻辑但是并不能看到实际的效果,下面我们现实现一个compiler(编译器好了,让我们能直接看到页面)
-
compiler编译器类
- 实现对dom文本节点的编译
- 在根目录创建
Compiler.js
文件
export class Compiler { constructor(vm) { this.vm = vm this.el = vm.$el this.compile(this.el) } compile(el) { // 取到元素的所有子节点 let childNodes = [...el.childNodes] childNodes.forEach(node => { // 如果是文本节点 if(this.isTextNode(node)) { this.compileText(node) // 如果是元素节点 } else if(this.isElementNode) { } // 判断元素下面还有没有子节点,如果有则进行递归 if(node.childNodes && node.childNodes.length) { this.compile(node) } }) } compileText(node) { let val = node.textContent let reg = /\{\{(.+?)\}\}/ if(reg.test(val)) { // 取到双大括号里面的值 let key = RegExp.$1.trim() node.textContent = val.replace(reg, this.vm[key]) } } compileElement(node) { // 这里就先啥也不用写 } isTextNode(node) { return node.nodeType === 3 } isElementNode(node) { return node.nodeType === 1 } }
还是得等我写一个流程图,稍等!
ok,又好了
嘿嘿这个时候我们就立马可以看到效果了!!!!!!!!!
我们在index.html中这么写!
<div id="app"> {{name}} </div> <script type="module" src="./index.js"></script>
在Vue类中用上Compiler类并且,并且那么写
import {Observer} from './Observer' import {Compiler} from './Compiler' class Vue { constructor(option) { this.$el = option.el ? document.querySelector(option.el) : document.querySelector('#app') this.$data = option.data() this._proxyData(this.$data) new Observer(this.$data) new Compiler(this) // 要用上哦 } _proxyData(data) { Object.keys(data).forEach(key => { Object.defineProperty(this, key, { get() { return data[key] }, set(newValue) { if(newValue === data[key]) return data[key] = newValue } }) }) } } // 那么写在这哦 new Vue({ data() { return { name: '我是console没有log' } } })
接下来看我们的页面!!!!
这等好事,真的赞
下面的这两个类解释起来有点麻烦,我的解决办法就是,打算一口气写完这两个类,直接画一个整个的流程图!!直接秒懂。
-
Dep类
- 在根目录创建
Dep.js
- 用
addSub
方法可以在目前的Dep
对象中增加一个Watcher
的订阅操作; - 用
notify
方法通知目前Dep
对象的subs
中的所有Watcher
对象触发更新操作
export class Dep { constructor() { this.subs = [] } addSub(sub) { if(sub && sub.update) { this.subs.push(sub) } } notify() { this.subs.forEach(sub => { sub.update() }) } }
- 在根目录创建
-
watcher类
- 在根目录创建
Watcher.js
- 它的作用就是,给每一个在页面上使用的变量添加一个监听者,每当data中数据发生改变,它就会接收到通知,从而更新页面。如果页面上并没有使用该变量,那么让他绑定视图更新的方法这样岂不是浪费。 ----
请注意看这句话
import {Dep} from './Dep' export class Watcher { constructor(vm, key, cd) { this.vm = vm this.key = key this.cd = cd Dep.nb = this this.oldValue = vm[key] Dep.nb = null } update() { let newValue = this.vm[this.key] if(newValue === this.oldValue) return this.cd(newValue) } }
现在我们把这两个类用上
watcher用到Compiler里
import { Watcher } from "./Watcher" export class Compiler { constructor(vm) { this.vm = vm this.el = vm.$el this.compile(this.el) } compile(el) { let childNodes = [...el.childNodes] childNodes.forEach(node => { if(this.isTextNode(node)) { this.compileText(node) } else if(this.isElementNode) { } if(node.childNodes && node.childNodes.length) { this.compile(node) } }) } compileText(node) { let val = node.textContent let reg = /\{\{(.+?)\}\}/ if(reg.test(val)) { let key = RegExp.$1.trim() node.textContent = val.replace(reg, this.vm[key]) new Watcher(this.vm, key, (newValue) => { // 在这里哦 -- 页面使用到的变量我们才会给他设置更新页面的方法 node.textContent = newValue }) } } compileElement(node) { // 这里就先啥也不用写 } isTextNode(node) { return node.nodeType === 3 } isElementNode(node) { return node.nodeType === 1 } }
Dep用到Observer里面
import { Dep } from "./Dep" export 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, value) { const self = this self.walk(value) let dep = new Dep() // 这里 Object.defineProperty(obj, key, { get() { Dep.nb && dep.addSub(Dep.nb) // 这里 return value }, set(newValue) { if(newValue === value) return value = newValue self.walk(value) dep.notify() // 这里 } }) } }
再看一下这个理论,然后看全部的流程图
提醒一下就是,如果data中数据发生改变的时候会运行Observer中绑定的set方法,可以结合着我这个流程图看
玩一下
到这数据影响视图的原理就结束了,敲了这么长时间的代码,竟然还需要我们自己绑定事件,来改变data中的数据,这我不能忍,下面我们来玩一下
记得我们当时判断如果是元素节点的时候啥也么有干吗,接下来我们就干点什么
给Vue类添加$methods
import {Observer} from './Observer' import {Compiler} from './Compiler' class Vue { constructor(option) { this.$el = option.el ? document.querySelector(option.el) : document.querySelector('#app') this.$data = option.data() this.$methods = option.methods ? option.methods : {} this._proxyData(this.$data) this._initMethods(this.$methods) this._proxyMethods(this.$methods) new Observer(this.$data) new Compiler(this) } _proxyData(data) { Object.keys(data).forEach(key => { Object.defineProperty(this, key, { get() { return data[key] }, set(newValue) { if(newValue === data[key]) return data[key] = newValue } }) }) } _proxyMethods(methods) { Object.keys(methods).forEach(key => { Object.defineProperty(this, key, { get() { return methods[key] }, set(newValue) { if(newValue === methods[key]) return; methods[key] = newValue } }) }) } _initMethods(methods) { Object.keys(methods).forEach(key => { methods[key] = methods[key].bind(this) }) } }
添加Compiler中的compileElement方法
compileElement(node) { let self = this, props = node.getAttributeNames(), reg = /^c-(\w+)/, reg2 = /\((.+)\)/, reg3 = /\'(.+)\'/, reg4 = /^\d+$/, reg5 = /(\w+)\(/, value, methodName props.forEach((key) => { if (reg.test(key)) { methodName = RegExp.$1.trim() } let qian = node.getAttribute(key) if (reg2.test(qian)) { value = RegExp.$1.trim() if (reg4.test(value)) { value = parseInt(value) } if (reg3.test(value)) { value = RegExp.$1.trim } } if (reg5.test(qian)) { qian = RegExp.$1.trim } console.log(qian) console.log(methodName) console.log(value) node.addEventListener(methodName, function (e) { self.vm[qian](value ? (self.vm[value] ? self.vm[value] : value) : e) }) }) }
现在我们就可以在实例化的Vue对象中绑定方法啦 在index.js文件中加上
let vue = new Vue({ data() { return { name: '我是console没有log' } }, methods: { run() { this.name = '真的好棒哦' } } })
在html文件中加上
<div id="app"> <div> {{name}} </div> <button c-click="run">改变</button> </div> <script type="module" src="./index.js"></script>
- 在根目录创建
结束语
这是我在掘金的第一篇文章,其实第一篇文章我纠结了好久,根本不知道发什么,想发这个一看那个博主已经写了而且写的很好,诶有点自卑了。不过我又想了想每个人的学习方式不一样吗,指不定看我的能看懂呢嘿嘿
在这里我想特别感谢彭哥的指导,真的帮了我好多,人也特别好。这个是彭哥的掘金地址,希望大家也多多关注彭哥,真的非常厉害
gitee的仓库地址
如果大家喜欢我这种风格的文章请在点赞和关注,还有评论告诉我!!
拜拜啦~下次见
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!