1.小知识补充
1.1dom 的操作
dom.childeNodes 获取子节点
node.attributes = {name,value} 获取属性对象(获取键值和值)
dom.appendchild 追加
dom.nextSibling 获取下一个兄弟节点
dom.parentNode 获取父节点
dom.removeChild 删除子节点
dom.insertBefore 插入节点
dom.textContent(innerText) 改变文本内容
1.2正则
RegExp.$1 可以获取捕获的内容, /(123)/.test('123') //RegExp.$1 -- '123', ()--为捕获内容
1.3 数据的劫持 -- Object.defineProperty
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
语法: Object.defineproperty( object,‘ propName ’ ,descriptor);
-
object :要定义属性的对象,返回的也是
-
propName :要定义或修改的属性的名称。
-
descriptor:要定义或修改的属性描述符,属性描述符详情
简单的双向数据绑定
.html
<input type="text" value="1">
<p><strong>1</strong></p>
js
let obj = {
name: '1'
}
let input = document.querySelector('input'),
text = document.querySelector('p strong'),
btn = document.querySelector('button')
// // 数据劫持
Object.defineProperty(obj, 'keys', {
get() {
console.log('get')
return input.value
},
set(newVal) {
console.log('set')
text.textContent = newVal
input.value = newVal
}
})
input.oninput = function (e) {
obj.name = e.target.value
}
2.vue 学习
2.1 本次文档将走完下面的流程图
- Vue类的实现
- 数据的劫持监听
- 解析器 Complie
- 视图初始化
- 获取更新函数
- 添加订阅者 Wacher -- 保存更新函数(触发get,通过dep去收集 wacher), Dep 收集器的实现 -- 发布订阅模式
- 通知变化 通知所有订阅者
3. Vue类的实现
index.html
<div id="app">
<h2 id="test" class="123" v-text="name"></h2>
{{age}}
<p><span>{{age}}</span></p>
<input type="text" v-bind:value="age" v-on:input="(e) => {this.age = e.target.value}">
</div>
index.js
const app = new Vue({
el: '#app',
data: {
name: 'LONG·江',
age: 18,
isShow: true,
arr: [1, 2, 3],
detail: {
qq: '123',
wx: '321',
test: { t1: '1', t2: 2 },
},
}
})
vue.js
总结: vue就做了这几件事情
1、数据劫持 ✔
2、complie ✔
3、发布订阅模式 ✔
class Vue {
constructor(options) {
this.$el = document.querySelector(options.el)
this.$options = options
this.$data = options.data
}
}
4. 数据的劫持监听
通过 Object.defineProperty 完成数据的劫持,每次访问修改数据都会触发 set get 方法。
4.1 vue.js
class Vue {
constructor(options) {
this.$el = document.querySelector(options.el)
this.$options = options
this.$data = options.data
observe(this.$options.data) // 数据劫持
}
}
// 数据劫持 'object' 的判断
function observe(data) {
if (typeof data !== 'object') return
new Observe(data)
}
// Array Object 数组或对象作一个区别处理
class Observe {
constructor(data) {
this.data = data
this.walk(data)
}
walk(data) {
// 遍历每一项数据做响应式劫持
Object.keys(data).forEach(key => {
if (Array.isArray(data[key])) {
} else {
defineReactive(data, key, data[key])
}
})
}
}
// 数据响应式 核心 -- Object.defineProperty
function defineReactive(obj, key, val) {
if (typeof val === 'object') { observe(val) } // 作一个递归
// 每项数据做一个劫持
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
val = newVal
}
})
}
通过递归实现深层级的数据劫持
4.2 数据的代理
正常情况下, vue 可以通过 实例 app.name 去获取属性,下面完成这个功能
vue.js
class Vue {
constructor(options) {
this.$el = document.querySelector(options.el)
this.$options = options
this.$data = options.data
observe(this.$options.data) // 数据劫持
this.proxy() // 代理
}
proxy() {
// 相当于给 vue 实例一层级加上对应的数据劫持
Object.keys(this.$data).forEach(key => {
Object.defineProperty(this, key, {
get() {
// get 方法其实还是访问 this.$data 中的属性,就是做了个印射
return this.$data[key]
},
set(newVal) {
// set 方法直接修改 this.$data 中的属性
this.$data[key] = newVal
}
})
})
}
}
5. 解析器 Complie 和 视图初始化
解析器 Complie,就是用来对 el:#app 绑定的dom,作一些语法和词法的解析。
vue.js
// 解析器 - 编译的工作
class Complie {
constructor(el, vm) {
this.node = el
this.vm = vm
this.init(this.node)
}
init(nodes) {
nodes.childNodes.forEach(node => {
if (node.nodeType === 3) { // 文本节点
let reg = /\{\{(.*?)\}\}/
if (reg.test(node.textContent)) {
node.textContent = this.vm[RegExp.$1] // 给内容赋值
}
} else if (node.nodeType === 1) { // 标签
let attrs = node.attributes
Array.from(attrs).forEach(attr => {
// attr.name 获取属性名,attr.value 获取属性值
let key = attr.name, val = attr.value
if (key.includes('v-text')) {
node.textContent = this.vm[val]
}
})
if(node.childNodes.length > 0) this.init(node) // 这里做了一个递归子节点
}
})
}
}
绑定好之后,视图便显示了对应的数据, 这便是视图的初始化
6. 获取更新函数
更新函数是 data 中属性更新时,去执行的更新视图的函数,对应更新视图的方法便是更新函数
vue.js
// 跟 5.解析器差不多,只是多了更新函数
class Complie {
......
init(nodes) {
nodes.childNodes.forEach(node => {
if (node.nodeType === 3) { // 文本节点
let reg = /\{\{(.*?)\}\}/
if (reg.test(node.textContent)) {
node.textContent = this.vm[RegExp.$1]
// 获取更新函数
let updateFn = function () {
node.textContent = this.vm[RegExp.$1]
}
}
} else if (node.nodeType === 1) { // 标签
let attrs = node.attributes
Array.from(attrs).forEach(attr => {
let key = attr.name, val = attr.value
if (key.includes('v-text')) {
node.textContent = this.vm[val]
// 获取更新函数
let updateFn = function () {
node.textContent = this.vm[val]
}
}
})
if(node.childNodes.length > 0) this.init(node)
}
})
}
}
7. 添加订阅者
7.1 wacher 类的实现
Watcher 主要是初始化的时候触发, 作用是 用来保存更新函数的。
set 的触发,便是去 执行 watcher 中的更新函数。
vue.js
// 观察者 -- 保存更新视图的回调函数
class Watcher {
constructor(vm, cb, key) {
this.vm = vm
this.cb = cb
}
}
Complie 类
初始化时,也就是第一次解析时, 会 new watcher 去保存更新函数
class Complie {
...
init(nodes) {
...
if (node.nodeType === 3) { // 文本节点
let reg = /\{\{(.*?)\}\}/
if (reg.test(node.textContent)) {
node.textContent = this.vm[RegExp.$1]
new Watcher(this.vm, function () { // 一个依赖 对应 new Watcher 实例
node.textContent = this.vm[RegExp.$1]
}, RegExp.$1)
}
}
7.2 订阅器 -- Dep
vue.js
// 订阅器 - 发布订阅模式
class Dep {
constructor() {
this.subs = []
}
add(watcher) { // 注册
this.subs.push(watcher)
}
notify() { // 通知
this.subs.forEach(w => { w.cb() })
}
}
7.3 添加订阅者的实现
视图中会用到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来用⼀个Watcher来维护它们,此过程称为依赖收集。
多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知
因为 new Watcher 是根据相关依赖,初始化时候才会执行,以后不会执行了。
1、所以 可以通过 Dep.target 判断是否是首次的加载,是则收集依赖
2、this.vm[key] 是为了触发 get 方法 收集依赖
3、 Dep.target 设置为空, 防止重复收集
Watcher 类的修改
// 观察者 -- 保存更新视图的回调函数
class Watcher {
constructor(vm, cb, key) {
this.vm = vm
this.cb = cb
// 核心代码
Dep.target = this
this.vm[key] // 触发get,为了依赖收集
Dep.target = null
}
}
defineReactive 方法的修改
1、为每个属性劫持时,创建一个收集器 dep 的实例,用来收集对应的依赖
2、get 判断是否是首次的加载,是则收集依赖
// 数据响应式
function defineReactive(obj, key, val) {
if (typeof val === 'object') { observe(val) }
let dep = new Dep()
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.add(Dep.target) // 收集依赖
}
return val
},
set(newVal) {
val = newVal
}
})
}
8. 通知变化 -- 通知所有订阅者
数据变法触发 set , 通知所有对应的依赖
set 方法的修改
set(newVal) {
val = newVal
dep.notify()
}
相当于执行了 wacher 中的更新函数。
执行下面代码,发现对应视图更新
setTimeout(() => { app.age = 19}, 1000)
到这里 vue 的代码写完了。
9.v-modal 的实现
v-modal其实就是实现了 v-bind 和 v-on 的方法,知道原理后,我们知道,这些语法是在解析器里面解析出来的,所以我们要在解析器里面 实现这两个方法。
v-bind 的原理就是为 dom 加上对应的属性 node.setAttribute
v-on 的原理时为该 dom 加上的对应的事件监听 node.addEventListener
index.html
<input type="text" v-bind:value="age" v-on:input="(e) => {this.age = e.target.value}">
v-on 难点解析 -- 字符串转函数的实现
string to function
通过 正则获取 参数,和函数体
然后通过 new Function (参数,函数体)
实现了字符串转 函数
index.html
<input type="text" v-bind:value="age" v-on:input="(e) => {this.age = e.target.value}">
vue.js
class Complie {
constructor(el, vm) {
this.node = el
this.vm = vm
this.init(this.node)
}
init(nodes) {
nodes.childNodes.forEach(node => {
if (node.nodeType === 3) { // 文本节点
let reg = /\{\{(.*?)\}\}/
if (reg.test(node.textContent)) {
node.textContent = this.vm[RegExp.$1]
new Watcher(this.vm, function () {
node.textContent = this.vm[RegExp.$1]
}, RegExp.$1)
}
} else if (node.nodeType === 1) { // 标签
let attrs = node.attributes
Array.from(attrs).forEach(attr => {
let key = attr.name, val = attr.value
// v-text
if (key.includes('v-text')) {
node.textContent = this.vm[val]
} else if (key.includes('v-bind')) { // v-bind
let reg = /v-bind:(.*)/
if (reg.test(key)) {
node.setAttribute(RegExp.$1, this.vm[val])
attrs.removeNamedItem(`v-bind:${RegExp.$1}`) // 移除dom上对应的vue语法
}
} else if (key.includes('v-on')) { // v-on
let reg = /v-on:(.*)/, reg1 = /\((.*)\)\W*\{(.*)\}/
let vm = this.vm
if (reg.test(key)) {
let eventName = RegExp.$1
if (reg1.test(val)) {
let fn = new Function(RegExp.$1, RegExp.$2.trim()) // 字符串转函数
node.addEventListener(eventName, function (e) { // 重写一层时为了获取 e
fn.bind(vm)(e) // 让 this 指向 vm
})
}
}
}
})
if(node.childNodes.length > 0) this.init(node)
}
})
}
}
然后通过input输入,发现data也改变了
10. 完整代码
// 1、数据劫持 ✔
// 2、complie ✔
// 3、发布订阅模式 ✔
class Vue {
constructor(options) {
this.$el = document.querySelector(options.el)
this.$options = options
this.$data = options.data
observe(this.$options.data) // 数据劫持
this.proxy() // 代理
new Complie(this.$el, this)
}
proxy() {
Object.keys(this.$data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
})
}
}
// 数据劫持 'object' 的判断
function observe(data) {
if (typeof data !== 'object') return
new Observe(data)
}
// Array Object
class Observe {
constructor(data) {
this.data = data
this.walk(data)
}
walk(data) {
Object.keys(data).forEach(key => {
if (Array.isArray(data[key])) {
1
} else {
defineReactive(data, key, data[key])
}
})
}
}
// 数据响应式
function defineReactive(obj, key, val) {
if (typeof val === 'object') { observe(val) }
let dep = new Dep()
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.add(Dep.target)
}
return val
},
set(newVal) {
val = newVal
dep.notify()
}
})
}
// 解析器 - 编译的工作
// 递归遍历dom树
// 判断节点类型,如果是文本,则判断时候是插值绑定
// 如果是元素,则遍历器属性判断是否是指令或事件,然后递归子元素
class Complie {
constructor(el, vm) {
this.node = el
this.vm = vm
this.init(this.node)
}
init(nodes) {
nodes.childNodes.forEach(node => {
if (node.nodeType === 3) { // 文本节点
let reg = /\{\{(.*?)\}\}/
if (reg.test(node.textContent)) {
node.textContent = this.vm[RegExp.$1]
new Watcher(this.vm, function () {
node.textContent = this.vm[RegExp.$1]
}, RegExp.$1)
}
} else if (node.nodeType === 1) { // 标签
let attrs = node.attributes
Array.from(attrs).forEach(attr => {
let key = attr.name, val = attr.value
if (key.includes('v-text')) {
node.textContent = this.vm[val]
} else if (key.includes('v-bind')) {
let reg = /v-bind:(.*)/
if (reg.test(key)) {
node.setAttribute(RegExp.$1, this.vm[val])
attrs.removeNamedItem(`v-bind:${RegExp.$1}`) // 移除dom上对应的vue语法
}
} else if (key.includes('v-on')) {
let reg = /v-on:(.*)/, reg1 = /\((.*)\)\W*\{(.*)\}/
let vm = this.vm
if (reg.test(key)) {
let eventName = RegExp.$1
if (reg1.test(val)) {
let fn = new Function(RegExp.$1, RegExp.$2.trim()) // 字符串转函数
node.addEventListener(eventName, function (e) { // 重写一层时为了获取 e
fn.bind(vm)(e)
})
}
}
}
})
if(node.childNodes.length > 0) this.init(node)
}
})
}
}
// 观察者 -- 保存更新视图的回调函数
class Watcher {
constructor(vm, cb, key) {
this.vm = vm
this.cb = cb
Dep.target = this
this.vm[key] // 为了依赖收集
Dep.target = null
}
}
// 订阅器 - 发布订阅模式
class Dep {
constructor() {
this.subs = []
}
add(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(w => { w.cb() })
}
}
Dep.target = null
export default Vue