Vue简易版 - 学习笔记
大家好,我是小瑜, 最近跟着峰哥大佬学习并手撕了Vue的简易版实现, 没有学习之前感觉框架的底层原理很复杂很难, 但是跟着大佬渐渐深入后发现并不是特别的难以理解, 反而越发喜欢, 也越发成谜. 学习完后就马不停蹄的在近凌晨2点, 按照实现思路,完成并产出了这篇学习笔记博客.
此篇笔记包含 响应式 简易编译器 Watcher和Dep,知道他们如何建立关系
1. 初识object.defineProperty
1.1基本语法
const object1 = {};
const object2 = Object.defineProperty(object1, 'property1', {
value: 42,
writable: true
});
object1.property1 = 77;
// Throws an error in strict mode
console.log(object1.property1);
// Expected output: 42
console.log(object1,object2); true
参数1: 要定义属性的对象
参数2:一个字符串或 Symbol,指定了要定义或修改的属性键
参数3:要定义或修改的属性的描述符
1) value 属性值 2) writable是否允许被修改
返回值: 传入函数的对象,其指定的属性已被添加或修改 所以 object1 === object2 true
1.2 描述-configurable - 是否可删除
当设置为 false 时,
- 该属性的类型不能在数据属性和访问器属性之间更改
- 该属性不可被删除
- 其描述符的其他属性也不能被更改(但是,如果它是一个可写的数据描述符,则
value可以被更改,writable可以更改为false)。
默认值为 false。
const obj = { a: 1, b: 2 }
delete obj.b
console.log(obj) // {a: 1}
Object.defineProperty(obj, 'c', {
value: 3, // 属性值
// configurable: false // 默认不允许被删除,如果不写,默认为false
})
delete obj.c
console.log(obj) // {a: 1, c: 3}
1.3 描述-enumerable - 是否可枚举
当且仅当该属性在对应对象的属性枚举中出现时,值为 true。默认值为 false。
const obj = { a: 1, b: 2 }
Object.defineProperty(obj, 'c', {
value: 3,
}
)
console.log(obj) // {a: 1, b: 2, c: 3}
// 1. 通过Object.defineProperty()方法定义的属性,默认是不可枚举的
// 例如这里的c属性,是不可枚举的
// for (let key in obj) {
// console.log(key) // a b
// }
// 2. 如果想要让属性可枚举,需要设置enumerable为true
Object.defineProperty(obj, 'd', {
value: 4,
enumerable: true
}
)
for (let key in obj) {
console.log(key) // a b d
}
console.log(Object.keys(obj)) // ["a", "b", "d"]
1.4 描述-value - 关联值
与属性相关联的值。可以是任何有效的 JavaScript 值(数字、对象、函数等)。默认值为 undefined。
const obj = { a: 1, b: 2 }
Object.defineProperty(obj, 'c', {})
console.log(obj) //{a: 1, b: 2, c: undefined}
// 注意 如果这里写
Object.defineProperty(obj, c, {}) // Uncaught ReferenceError: c is not defined 因为找不到c变量
1.5 描述-writable - 是否可修改
如果与属性相关联的值可以使用赋值运算符更改,则为 true。默认值为 false。
// 如果与属性相关联的值可以使用赋值运算符更改,writable则为 true。默认值为 false
const obj = { a: 1, b: 2 }
Object.defineProperty(obj, 'c', {
// writable:true
})
obj.c = 999
console.log(obj)
// 如果writable为false或者不写结果是{a:1,b:2,c:undefined}
// 如果writable为true或者不写结果是{a:1,b:2,c:999}
1.6 小练习-给对象添加属性
面试: 不利用对象点语法, 给对象添加a属性,值为1
const obj = {}
Object.defineProperty(obj.'a',{value:1})
console.log(a) // {a:1}
注意 利用Object.defineProperty的对象 默认不可删除 不可修改 不可遍历 等价于
Object.defineProperty(o, "a", {
value: 1,
writable: false,
configurable: false,
enumerable: false,
});
而我们经常使用的obj.a = 1 等价于
Object.defineProperty(o, "a", {
value: 1,
writable: true,
configurable: true,
enumerable: true,
});
2.1 访问器描述符 get | set
类似于vue中 计算属性的写法
money 访问的时候(boy.money)打印出来一句话 我有100块钱 set 设置的时候 打印一句话 如果money 小于100, 今晚吃土 如果大于100, 今晚买车
const boy = { name: '帅瑜', money: 100 }
Object.defineProperty(boy, 'money', {
get() {
console.log('我有100块钱')
return 100
},
set(val) {
console.log(val < 100 ? '今晚吃土' : '今晚买车')
}
})
console.log(boy.money) // '我有100块钱' 100
console.log(boy.money = 200) // 今晚买车 200
console.log(boy.money = 50) // 今晚吃土 50
2.2 小练习
访问器属性符添加对象属性
const obj = {}
let aValue = 10
Object.defineProperty(obj, 'b', {
get() {
return aValue
},
set(newValue) {
aValue = newValue
}
})
console.log(obj.b = 99) //99
2. 封装definReactive
2.1 简单搭设一个definReactive函数的架子
// definReactive(obj,key,value)
function definReactive(obj, key, value) {
Object.defineProperty(obj, key, {
get() {
console.log('get', { key, value })
return value
},
set(newValue) {
// 如果新值和旧值不相等,就更新
if (newValue !== value) {
console.log('set', { key, value: newValue })
value = newValue
}
},
})
}
// 声明一个空对象
const obj = {}
// 通过definReactive给他响应式设置键a 值 我是a的值
// 通过访问, 和设置值看有没有触发打印
definReactive(obj, 'a', '我是a的值')
obj.a // get { key: 'a', value: '我是a的值' }
obj.a = 'new a' // set { key: 'a', value: 'new a' }
console.log(obj.a) // get { key: 'a', value: '我是a的值' } 我是a的值
2.2 提供update方法测试 - 实现数据变化视图也变化
/**
* 声明个空对象
* 劫持
* a属性 - 初始值a
* b属性 - 初始值b
* c属性 - 初始值c
*
* update方法
* 具体就是给h1里面添加内容
* <h1 id="a">a属性的内容</h1>
* update方法写在set里面 修改值的时候会触发update函数
*/
const obj = {}
defineReactive(obj, 'a', 'a')
defineReactive(obj, 'b', 'b')
defineReactive(obj, 'c', 'c')
function update() {
a.innerHTML = obj.a
b.innerHTML = obj.b
c.innerHTML = obj.c
}
update() // 初次渲染页面
控制台打印结果
因为每次数据的变化都会触发update函数的执行
所以会让视图发生
因为每次修改都会执行一次set方法
因为执行了三次 defineReactive() 所以访问了3次get
现在控制台修改了数据,视图更新了, 需要考虑三个问题
- 在使用vue的时候,更新凹函数我们不需要写
- 其实都是template自动编译成更新函数的
- 劫持字段要用户一个一个自己去劫持,不合理
- 需要遍历,后面还需要递归
- 全量更新, 只要数据一发生变化(改变其中一个属性), 视图就直接全部更新了,不合理
- 精确定位具体的dom元素 - 本次简易版实现使用这种
2.3 提供observe方法进行遍历对象
// 帮助我们遍历obj里的所有属性
function observe(obj) {
Object.keys(obj).forEach(key=>definReactive(obj,key,obj[key]))
}
//obj里有多个属性
const obj = {
a: '我是a的值',
b: '我是b的值',
}
// 对象有多个属性,我们要进行遍历 调用observe方法
observe(obj)
obj.a
obj.a = 'new a'
// get { key: 'a', value: '我是a的值' }
// set { key: 'a', value: 'new a' }
obj.b
obj.b = 'new b'
// get { key: 'b', value: '我是b的值' }
// set { key: 'b', value: 'new b' }
出现的问题
const obj = {
a: '我是a的值',
b: '我是b的值',
c:{
d:'我是c里的d'
}
}
此时去访问和修改的结果
get { key: 'c', value: { d: '我是c里的d' } } get { key: 'c', value: { d: '我是c里的d' } }
d属性没有被修改, 因为c属性是一个对象, 需要再次进行递归至d才可以修改
function definReactive(obj, key, value) {
// 递归调用
// observe(value)
Object.defineProperty(obj, key, {
get() {
console.log('get', { key, value })
return value
},
set(newValue) {
// 如果新值和旧值不相等,就更新
if (newValue !== value) {
console.log('set', { key, value: newValue })
value = newValue
}
},
})
}
// 帮助我们遍历obj里的所有属性
function observe(obj) {
// 递归就是自己调用自己
// 递归要注意 终止条件, 判断如果不是一个对象或者是null,直接返回
if(typeof obj !== 'object' || obj === null) return
Object.keys(obj).forEach(key=>definReactive(obj,key,obj[key]))
}
此时执行obj.c.d = '我是c里的d 我被修改了'
obj.c.d
obj.c.d = '我是c里的d,我被修改了'
get { key: 'c', value: { d: [Getter/Setter] } }
get { key: 'd', value: '我是c里的d' }
get { key: 'c', value: { d: [Getter/Setter] } }
set { key: 'd', value: '我是c里的d,我被修改了' }
此时还是有问题,例如给劫持的属性赋值新的对象时没有发生变化
此时 需要给set方法也要定义成响应式
2.4. set方法 劫持新的属性
首先看一下问题
const obj = {
a: '我是a的值',
b: '我是b的值',
}
// 对象有多个属性,我们要进行遍历 调用observe方法
observe(obj)
obj.c
obj.c = '我是c的值'
此时运行后发现没有反应 这是因为动态添加的对象没有拦截到, 所以无法触发响应式, 那么在vue2中提供了$set方法, 文档的写法Vue.set( target, propertyName/index, valu…
因为observe触发的访问器描述符中的set,只会将obj原本的值进行拦截,
所以利用set方法 再次调用definReactive方法后再进行添加即可变成响应式
2.5 数组可以劫持和不可以劫持的情况
const arr = [1, 2, 3]
observe(arr)
arr[0]
arr[1] = 222
arr[999]
arr[999] = 100
// get { key: '0', value: 1 }
// set { key: '1', value: 222 }
此时如果数组中包含的可以劫持 如果数组中没有包含就无法劫持
3. 写静态页面使用官方的vuejs
按照文档使用vue,并且将引入的资源 替换成本地的my-vue.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
{{count}}
<div>{{count}}</div>
<p v-text="count"></p>
</div>
//<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="./my-vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
count: 10
}
})
setInterval(() => {
console.log(vm)
vm.count++
}, 1000)
</script>
</body>
</html>
创建类Vue
class Vue {
// constructor及静态资源
// options及实例化时传入的对象
constructor(options) {
//$opptions=>配置项就是new Vue时传入的对象
this.$options = options
//$data=>保存响应式数据 判断data是对象还是函数 函数执行去返回值
this.$data = options.data
// 响应式处理
observe(options.data)
}
}
count的值并没发生变化的问题
此时页面出来的打印结果, 发现虽然每个一秒会输出打印 但是count的值并没发生变化
其实原因也很简单, 这行代码时给vm.count, vm就是new Vue实例化传入的对象
vm上并没有count, count是在data对象里,所以将vm.count改为 vm.$data.count即可
setInterval(() => {
console.log(vm)
// vm.count++
vm.$data.count
}, 1000)
但是在vue中我们并不需要点data来获取响应式数据, 也就是vue帮助我们通过外层属性直接访问到了内层数据,这里还是需要使用Object.defineProperty进行对象的代理
通过外层属性直接访问内层属性
首先写一个需求 => 需要直接通过 obj.a或b 访问和修改值
//需要直接通过 obj.a或b 访问和修改值
const obj = {
data: {
a: 1,
b: 2,
},
}
Object.keys(obj.data).forEach((key) => {
Object.defineProperty(obj, key, {
get() {
return obj.data[key]
},
set(val) {
obj.data[key] = val
},
})
})
obj.a
obj.a = 99 { data: { a: 99, b: 2 } }
obj.b
obj.b = 100 { data: { a: 99, b: 100 } }
console.log(obj)
同理运用到my-vue中
直接通过属性访问及修改data里的属性
function proxy(vm) {
console.log(vm)
Object.keys(vm.$data).forEach((key) => {
Object.defineProperty(vm, key, {
get() {
return vm.data[key]
},
set(val) {
vm.data[key] = val
},
})
})
}
class Vue {
constructor(options) {
console.log('options', options)
//$opptions=>配置项就是new Vue时传入的对象
this.$options = options
//$data=?保存响应式数据 判断data是对象还是函数 函数执行去返回值
this.$data = options.data
// 响应式处理
observe(options.data)
// 代理 希望
// 访问 vm.count => vm.$data.count
// 修改 vm.count++ => vm.$data.count++
proxy(this) // this这里是实例也就是vm
}
}
4. 编译器的实现
分析编译器要做什么事情
-
要知道vm实例, data中有哪些数据和方法, 解析双大括号 指令等内容
-
el => 告诉Vue 需要编译哪一个区域的内容
搭Compile架子
简单实现一个解析模板的架子
// 解析模板
class Compile {
// el (要知道解析哪个模板) 和vm实例 (要获取里面数据data还有方法methods等)
constructor(elSelector, vm) {
// console.log(elSelector, vm)
// 把vm实例挂载this上, 方便后面获取
this.vm = vm
// 选择器 获取 对应的元素
const element = document.querySelector(elSelector)
// 新写方法compile, 参数传入元素, 该方法专门处理编译逻辑
this.compile(element)
}
compile(element) {
console.log(element,this.vm)
}
}
在 Vue中实例化方法
class Vue {
... // 选择器 实例
new Compile(options.el, this)
}
4.1 获取模板内容
首先解析模板, 需要获取到所有的#app下的子元素内容
compile(element) {
// console.log(element, this.vm)
console.log(element.children)
console.log(element.childNodes)
}
有element.children 以及element.childNodes 那么应该使用哪一个呢? 看一下打印.
其中childNodes的打印内容, 用不同颜色的箭头做了标记, 很显然 对所有的文本以及标签都获取到了, 所以 这里得使用childNodes方法
4.2 判断区分是元素节点还是文本节点
// 获取元素里的内容
compile(element) {
// console.log(element, this.vm)
// console.log(element.children)
console.log(element.childNodes)
element.childNodes.forEach((node) => {
// node.nodeType 1 元素节点 3 文本节点
// console.log(node,node.nodeType)
if (this.isElement(node)) {
console.log('元素',node)
}
if (this.isInter(node)) {
// 能近这个if 说明是文本并且里面有双大括号语法
//node.textContent 文本内容
console.log('文本',node,node.textContent)
}
})
}
// 判断是否为元素节点
isElement(node) {
return node.nodeType === 1
}
// 判断是否为文本节点 且里面有双大括号语法 (正则)匹配
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
node.nodeType 用数字表示当前元素的类型
node.textContent 获取文本内容 例如可以获取到 "{{ count }}"
判断是否为 双大括号 需要文本节点&&正则进行区分, 具体可以看一下控制台的测试
4.3 元素节点需要递归处理
此时如果标签嵌套 则无法获取里面的{{coumt}} 例如
<div id="app">
{{count}}
<div>{{count}}</div>
<div>
<p>
<span>{{count}}</span>
</p>
</div>
<p v-text="count"></p>
</div>
所以需要在获取元素的时候递归
element.childNodes.forEach((node) => {
// node.nodeType 1 元素节点 3 文本节点
if (this.isElement(node)) {
// 获取元素和所有的节点node.childNodes,并且也是递归的结束条件
if (node.childNodes.length) {
// 如果能进来 说明还有 那么就进行递归
this.compile(node)
}
}
此时 再嵌套, 通过递归就可以获取到所有的双大括号文本
接下来就是将{{ count }}替换成data中定义的值即可
4.4 解析文本节点
定义compileText方法 用来编译文本
if (this.isInter(node)) {
// 能近这个if 说明是文本并且里面有双大括号语法
// 调用编译方法
this.compileText(node,this)
}
/**
* 编译文本
* 简易版本 - 本次仅支持直接替换双大括号语法内容
* node.textContent 既可以获取文本 也可以设置文本
* RegExp.$1 获取到的是正则匹配到的第一个分组内容 也就是(.*)里要被替换的内容
* this.vm.$data[RegExp.$1.trim()] 通过vm实例获取到data里的数据
* @param {*} node
*/
compileText(node) {
console.log('编译文本', node, RegExp.$1, this.vm)
node.textContent = this.vm.$data[RegExp.$1.trim()]
}
此时页面上就可以渲染出来内容
4.5 解析标签上的指令
如果是元素节点, 需要解析元素上的属性, 看看是否有v-开头的
if (this.isElement(node)) {
// 如果是元素节点, 需要解析元素上的属性, 看看是否有v-开头的
const attrs = node.attributes
console.log(attrs) // attrs是一个类数组
Array.from(attrs).forEach(({name,value}) => {
console.log(name,value)
// 结果是 v-text count
})
if (node.childNodes.length) {
// 递归处理 拿到所有的元素
this.compile(node)
}
}
并且还需要判断是否为v-开头
Array.from(attrs).forEach(({ name:attrName, value:exp }) => {
// attrName - 属性名 - 键
// exp - 属性值 - 值
if (this.isDir(attrName)) {
console.log('需要解析指令 指令名', attrName)
console.log('需要解析指令 表达式', exp)
}
})
// direction isDir - 判断字符串是否是指令
// 也就是判断一个字符串是v-开头
isDir(attrName) {
return attrName.startsWith('v-')
}
4.6 解析指令
现在以及知道了v-text 以及表达式的名字, 接下来就可以解析指令, 每一个指令都是一个方法, 所以都需要创建方法
为了动态解析每一个自定义指令的名字, 需要对当前获取的字符串进行截取,这样就可以自动匹配并调用对应的指令方法
const dirName = attrName.slice(2) // v-text => text console.log('需要解析指令 指令名', dirName)
// 获取元素里的内容
compile(element) {
element.childNodes.forEach((node) => {
if (this.isElement(node)) {
// 如果是元素节点, 需要解析元素上的属性, 看看是否有v-开头的
const attrs = node.attributes
// console.log(attrs) // attrs是一个类数组
Array.from(attrs).forEach(({ name:attrName, value:exp }) => {
// attrName - 属性名 - 键
// exp - 属性值 - 值
if (this.isDir(attrName)) {
// console.log('需要解析指令 指令名', attrName)
// console.log('需要解析指令 表达式', exp)
const dirName = attrName.slice(2) // v-text => text
console.log('需要解析指令 指令名', dirName)
this[dirName]?.(node,exp)
}
})
if (node.childNodes.length) {
// 递归处理 拿到所有的元素
this.compile(node)
}
}
if (this.isInter(node)) {
// 能近这个if 说明是文本并且里面有双大括号语法
//node.textContent 文本内容
// console.log('文本', node.textContent)
this.compileText(node,this)
}
})
}
// 自定义指令 - v-text
text(node, exp) {
node.innerText = this.vm[exp]
}
// 自定义指令 - v-html
html(node, exp) {
node.innerHTML = this.vm[exp]
}
5. 观察者模式
5.1 基本逻辑以及demo案例
观察者模式 观察者和被观察者是紧密联系的
需求: 通过面向对象的思路
-
工人的类Worker
-
构造函数里提供一个name(前端 | 后端 | ui)
-
work 方法 - 工作 打印工作的内容 work接收一个参数 ,pid(需求)
class Worker {
constructor(name) {
this.name = name
}
work(prd) {
console.log(`${this.name}开始做${prd}`)
}
}
const frontendWorker = new Worker('前端')
frontendWorker.work('前端的需求')
const backendWorker = new Worker('后端')
backendWorker.work('后端的需求')
const uiWorker = new Worker('ui')
uiWorker.work('ui的需求')
// 前端开始做前端的需求
// 后端开始做后端的需求
// ui开始做ui的需求
现在要求产品经理只要一提需求, 所有的工人就立即干活
class ProductManager {
constructor() {
this.workers = []
}
// 添加工人
addWorker(...worker) {
this.workers.push(...worker)
}
// 通知工人开始干活
notify(prd) {
this.workers.forEach((worker) => worker.work(prd))
}
}
const pm = new ProductManager()
pm.addWorker(frontendWorker, backendWorker, uiWorker)
pm.notify('开始做产品的需求')
通过上面的案例就能知道 观察者模式就是观察一个人有没有通知他们干活,
而是实例化工人后, 通过产品经理通知工人进行干活
所以Worker就是观察者 ProductManager就是被观察者
5.2 搭建watcher的架子
观察者 water更新的逻辑就是要拿到data中的数据
data中最新的数据就是在vm中data里面的内容
拿到最新的值后就需要进行更新,并渲染视图
class Watcher {
实例 每一项key 更新视图的方法
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updater = updater
}
update() {
// 拿到vm中的每一个key
this.updater(this.vm[this.key])
}
}
5.3 实例化watcher之前准备的工作
接下来就需要在编译文本的时候去实例化Water方法
也就是在Compile类中的compileText text html 分别去new Watcher
但是每一个都这么这么写 就需要实例化三次Watcher 所以每次当发生变化的时候, 应该需要调用更新函数
这个更新函数可能会更新 compileText 或text 或 html
// 在Compile类中提供更新函数
// 更新text以及compileText的方法
textUpdater(node, value) {
node.textContent = value
}
// 更新html的方法
htmlUpdater(node, value) {
node.innerHTML = value
}
但是如何调用他们呢 就需要再额外提供一个update方法
// node - 节点
// exp - 表达式
// dir - 指令名或参数
update(node, exp, dir) {
// 初始化的工作
this[dir + 'Updater']?.(node, this.vm[exp])
// 实例化watcher 做监听, 做数据变化后的一些功能
}
重新调用update方法
compileText(node) {
// console.log('编译文本', node, RegExp.$1, this.vm)
// node.textContent = this.vm[RegExp.$1.trim()]
// new Watcher()
this.update(node, RegExp.$1, 'text')
}
// 自定义指令 - v-text
text(node, exp) {
// node.innerText = this.vm[exp]
// new Watcher()
this.update(node,exp, 'text')
}
// 自定义指令 - v-html
html(node, exp) {
// node.innerHTML = this.vm[exp]
// new Watcher()
this.update(node,exp, 'html')
}
5.4 实例化watcher进行全量更新
- 在update中 更新函数 去实例化 Watcher 并且在第三个回调函数中接收最新的参数进行视图更新
- 进行全量更新, 创建一个wachers数组, 当数据发生变化也就是在defineProperty中set方法执行的时候去调用wachers中的update方法
- wachers数组在哪里添加实例呢? 就在Watcher构造函数中的constructor 只要new了一个 就会自动执行
1 update(node, exp, dir) {
// 初始化的工作
this[dir + 'Updater']?.(node, this.vm[exp])
// 实例化watcher 做监听, 做数据变化后的一些功能
new Watcher(this.vm, exp, (val) => {
// 每次只要更新了数据, 就会触发这里的回调函数所以会更新视图
// val指的是最新的值
this[dir + 'Updater']?.(node, val)
})
}
2. Object.defineProperty(obj, key, {
get(){},....
set(newValue) {
// 如果新值和旧值不相等,就更新
if (newValue !== value) {
console.log('set', { key, value: newValue })
value = newValue
// 调用watcher的update方法
watchers.forEach((w) => w.update())
}
},
3. class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updater = updater
// 添加到观察者数组中
watchers.push(this)
}
.....
5.5 Dep和watcher进行关联
观察者模式就是要让观察者和被观察者紧密联系,也就是将方法添加至数组的方式, 去遍历执行每一个数组中的方法
class Dep {
constructor() {
this.deps = []
}
// 添加观察者
addDep(watcher) {
this.deps.push(watcher)
}
// 通知观察者并执行更新
notify() {
this.deps.forEach(w=>w.update())
}
}
接下来就是建立关联
在建立关联之前需要将全量更新的代码进行注释
- 这里有一个很巧妙的方法, 在new Watcher的时候会执行constructor
- 里面将实例挂载至Dep的静态属性target
- 挂载上的用处就是去触发get方法去访问this.vm[this.key]也就是访问data中的每一个响应式数据的key
- 因为是响应式数据只要一访问就会触发get
- 触发完成get后就将Dep的target属性置为null
class Watcher {
1. constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updater = updater
2 // 添加到观察者数组中
Dep.target = this
3 // 触发get方法
this.vm[this.key]
Dep.target = null
}
update() {
this.updater(this.vm[this.key])
}
}
function definReactive(obj, key, value) {
// 实例化关键 管家里有两个方法 addDep notify
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
4. // 如果Dep中的静态方法存在target就添加
if (Dep.target) {
console.log(Dep)
dep.addDep(Dep.target)
}
return value
},
set(newValue) {
5. // 如果新值和旧值不相等,就更新
if (newValue !== value) {
console.log('set', { key, value: newValue })
value = newValue
// 调用watcher的update方法
dep.notify()
}
},
})
}
6. 编译器@事件实现
例如点击按钮 实现count++
{{count}}
<div>{{count}}</div>
<div>
<p>
<span>{{count}}</span>
</p>
{{name}}
</div>
<button @click="onClick">click</button>
<script>
const vm = new Vue({
el: '#app',
data: {
count: 10,
name: '王小帅',
dirText: 'v-text 自定义指令',
dirHTML: `<h1 style="color:pink;">v-html 自定义指令</h1>`
},
// 事件处理
methods: {
onClick() {
this.count++
}
}
})
</script>
-
需要在解析模板中判断当前传入的是否为事件 也就是是否为@开头
-
需要在获取元素内容中去触发事件,其中需要告诉处理函数是哪一个元素 事件名称 需要改变的属性值
-
进行事件处理=> 获取事件的回调,也就是传入的methods中的事件回调 并且去监听触发回调的执行
class Compile {
...
compile(element) {
// 遍历node所有的孩子元素
element.childNodes.forEach((node) => {
...转为真数组进行处理 (参数1 事件名称, exp 表达式 )
Array.from(attrs).forEach(({ name: attrName, value: exp }) => {
// 触发事件
if (this.isEvent(attrName)) {
// console.log('事件', node, attrName, exp)
// 获取事件
const eventName = attrName.slice(1)
console.log(eventName)
this.eventHandler(node, exp, eventName)
}
}
}
// 判断是否是事件
isEvent(str) {
// 判断字符串是否是以@开头
// return str.startsWith('@')
return str.indexOf('@') === 0
}
// 处理事件
eventHandler(node, exp, eventName) {
// 获取事件回调函数
const fn = this.vm.$options.methods[exp]
console.log(fn)
// 绑定this指向vm实例从而获取methods, 如果不修改 this指向Compile
node.addEventListener(eventName, fn.bind(this.vm))
}
}
}
7. v-model的实现
-
v-model在vue2中是一个语法糖 :value + @input
-
定义model指令并,update更新并触发input事件将
-
提供更新model的方法
Compile中定义方法
//v-model
model(node, exp) {
console.log(node, exp)
this.update(node, exp, 'model')
// 视图对模型响应
node.addEventListener('input', e => {
this.vm[exp] = e.target.value
})
}
// 更新model的方法 - 处理更新视图
modelUpdater(node, value) {
node.value = value
}
补充 - 发布订阅模式demo
观察者模式 观察者和被观察者是紧密联系的
发布订阅模式, 他们就不是强关联, 他们有另外沟通的渠道
产品和工人再也不紧密关联了 产品把需求发到一个平台上 按工人们on去监听平台
在平台上emit下, 就可以通知工人们
const eb = new EvenBus()
eb.on('abc', 方法A)
eb.on('abc', 方法B)
eb.on('abc', 方法C)
eb.emit('def', 方法D)
eb.emit('def', 方法E)
通过以上分析, eventBus里维护一个对象
event {
abc: [方法A, 方法B, 方法C],
def: [方法D, 方法E]
}
接下来开始写代码
class EventBus {
constructor() {
this.events = {}
}
// on方法
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = []
}
// 走到这里的代码 event对应的键肯定是一个数组
this.events[eventName].push(callback)
console.log(this.events)
}
// emit方法
emit(eventName) {
this.events[eventName].forEach((cb) => cb())
}
}
const eb = new EventBus()
eb.on('小帅', () => {
console.log('我爱学习')
})
eb.on('小帅', () => {
console.log('学习也爱我')
})
eb.on('小帅', () => {
console.log('我爱学习, 学习也爱我')
})
eb.emit('小帅')
// 我爱学习
// 学习也爱我
// 我爱学习, 学习也爱我
完结~
之前背八股文 : 虚拟dom可以更好的提高编译叭叭叭...
其实最原本的就是Vue1里在模板中每一个变量就会有一个观察者wacher, 会造成很大的性能消耗, 这是不合理的, 所以Vue2后面一个组件一个wacher,为了要知道哪一个更新, 所以引入了虚拟dom, 这也是虚拟dom存在的重要性.