手撕简易Vue-非Vue3
- 手撕响应式
- 手撕简易编译器
- 手撕
Watcher
和Dep
,知道他们如何建立关系
前置知识补充
众所周知,vue
是个mvvm
框架,这里就不做具体展开了,具体请看下图
其中,图中的ViewModel对于我们来说,就像黑盒般的存在,他到底做了什么,让我们写vue,纵享丝滑,写的如此快乐轻松呢!这也就是此篇文章的目的,带大家来写个简易Vue,具体要实现的东西,也请看下图
可能现在的你还看不懂,看上去好复杂啊,随着文章的深入,这幅图的所有实现都会带着大家写完!那我们开始吧
开始开发
Observer - 数据劫持
defineReactive架子 - 测试拦截
大家都知道,Vue2的响应式原理是Object.defineProperty
, 如果对Object.defineProperty
的使用还不清楚想更深入的小伙伴,可以去看下MDN,这里直接跟着我写也行!
新建my-vue.js
/* eslint-disable */
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
get () {
console.log('get', { key, val })
return val
},
set (newVal) {
if (newVal !== val) {
console.log('set', { key, newVal })
val = newVal
}
}
})
}
const obj = {}
defineReactive(obj, 'foo', 'foo')
obj.foo
obj.foo = 'new foo'
写完这个,我们可以测试下,用node环境跑下代码,会发现,的确get和set的时候都触发了打印!
放在一个测试页面并提供update方法测试
有了这个初步的劫持,那我们还可以玩什么呢,既然修改数据能触发set,那如果我在set里提供方法,更改视图会发生什么事呢!我们赶紧来试试!
cv前面的js代码到测试页面,修改下,添加update函数,之后在控制台修改对象的属性玩耍下!
<!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>测试数据劫持</title>
</head>
<body>
<div id="app">
<h1 id="a"></h1>
<h1 id="b"></h1>
<h1 id="c"></h1>
</div>
<script>
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log('get', { key, val });
return val
},
set(newVal) {
if (newVal != val) {
console.log('set', { key, newVal });
val = newVal
// 注意以下代码新增, 要写在赋值后面哈!
update()
}
}
})
}
const obj = {}
// 添加3个属性
defineReactive(obj, 'a', 'a')
defineReactive(obj, 'b', 'b')
defineReactive(obj, 'c', 'c')
// 定义update函数,就做简单的页面视图更新
function update () {
a.innerHTML = obj.a
b.innerHTML = obj.b
c.innerHTML = obj.c
}
// 一进页面先调用下
update()
</script>
</body>
</html>
我们可以发现,只要控制台修改了数据,视图就更新了,接着我们分析下问题,有这么几个疑问
- 在使用vue的时候,更新函数我们有写过吗
- 其实都是template自动编译成更新函数的
- 劫持字段要用户一个一个自己去劫持,不合理
- 需要遍历,后面还需要递归(等等就讲)
- 全量更新,只要数据一发生改变(改变其中一个属性),视图就直接全部更新了,显然不合理
- 精确定位具体的dom元素 - 本次简易版实现使用这种
- 利用虚拟dom差异化更新 - 以后带大家看源码,我会产出博客的!
observe的初步实现
用户不应该手动设置属性,通过遍历处理劫持每个key
function observe (obj) {
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
const obj = {
foo: 'foo',
bar: 'bar'
}
// defineReactive(obj, 'foo', 'foo')
observe(obj)
obj.foo
obj.foo = 'new foo'
obj.bar
obj.bar = 'new bar'
数据劫持需要递归处理
以上代码还没有递归,为什么要递归,因为用户设置的属性,还可以是对象,不递归会带来问题
const obj = {
a: 1,
b: 2,
c: {
haha: 3
}
}
obj.c.haha
obj.c.haha = 4
上面的截图,能发现,并没有劫持到haha这个属性,所以observe
方法中defineReactive
的obj[key]
如果是对象就需要递归
function defineReactive (obj, key, val) {
// 直接数据劫持,不用担心observe方法写了递归结束条件
observe(val)
...
}
function observe (obj) {
// 递归结束条件
if (typeof obj !== 'object' || obj === null) {
return
}
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
此时在跑前面的代码,就能劫持到我们想要的属性了
劫持set函数里还要在observe,因为用户可能直接赋值新的对象
obj.c = {
heihei: 444
}
obj.c.heihei
obj.c.heihei = 555
如果给属性赋值了新对象,数据劫持还是会出现问题
set(newVal) {
if (newVal !== val) {
// 需要劫持
observe(newVal)
val = newVal
}
}
在set添加observe后,此时效果就出来了
set方法
给obj设置个新属性,会劫持吗
const obj = {
a: 1,
b: 2,
}
observe(obj)
obj.c = 3
obj.c
obj.c = 4
所以Vue提供了set方法,新的属性就要新的劫持!
// 手写,新增set方法
function set(obj, key, val) {
defineReactive(obj, key, val)
}
const obj = {
a: 1,
b: 2,
}
observe(obj)
// 调用set方法
set(obj, 'c', 3)
// 这行就不需要了
// obj.c = 3
obj.c
obj.c = 4
数组问题
可以拦截的情况
const arr = [1, 2, 3]
observe(arr)
arr[0]
arr[1] = 222
用户可能访问更大的索引,或者给数组添加删除等方法(会改变数组自身的方法),此时就拦截不了, 数组7个变更方法处理会变更数组自身的方法,在做数组操作的同时,进行变更通知 此次不实现
写静态页面使用官方的vuejs
<!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>
const vm = new Vue({
el: '#app',
data: {
count: 0
}
})
setInterval(() => {
vm.count++
}, 1000)
</script>
</body>
</html>
能发现页面中的count在不断变化,接下去我们要替换我们自己的my-vue.js
my-vue.js
首先报错了,我们可以这么处理
class Vue {
constructor (options) {
// 0 - 保存选项
this.$options = options
this.$data = options.data
// 1 - 响应式 - 写好点可以做判断因为可能传的是函数
observe(this.$data)
// 2 - 编译模板
}
}
然后发现没有打印get和set,为什么没有劫持,因为vm上没有count,count在$data上
setInterval(() => {
vm.$data.count++
}, 1000)
将代码从vm.count++
改成vm.$data.count++
就有对应的打印了
代理属性
前面的问题是因为app上是没有counter属性的,在$data上才有counter属性,所以要做一层代理
在实现前,先闲扯一句,相当于现在有这么一个面试题
const obj = {
data: {
a: 1,
b: 2
}
}
// 希望你可以通过以下方式访问到data中的a和b属性
obj.a
obj.b
// 希望你可以通过以下方式,可以修改data里a和b的值
obj.a = 11
obj.b = 22
实现的代码如下
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
}
}))
通过以上练习,接下去对Vue实例代理一层是不是就简单多了!实现后就能看到vm.count++
也能触发劫持效果
// 简单代理一层
function proxy (vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get () {
return vm.$data[key]
},
set (v) {
vm.$data[key] = v
}
})
})
}
class Vue {
constructor (options) {
// 0 - 保存选项
this.$options = options
this.$data = options.data
// 1 - 响应式
observe(this.$data)
// 2- 做代理
proxy(this)
// 3 - 编译模板
}
}
至此,我们数据劫持相关的知识就先到这里,接下去就要开始写个简简单单的编译器
Compile - 编译器
接下去做编译的部分
原理简单说明
- 获取dom
- 遍历childNodes
- 编译节点
- 遍历属性
- v-开头
- @开头
- 遍历属性
- 编译文本
- 编译节点
Compile类
我们先简单搭个架子,Compile类,构造函数需要传入一开始设置的el选项以及vm实例(后面会用到),通过el选择器,获取元素,在调用编译方法!(compile方法用于处理编译的逻辑)具体代码如下
class Vue {
constructor(options) {
// console.log(options);
this.$options = options
this.$data = options.data
observe(this.$data)
proxy(this)
// 将选择器和实例都传入
new Compile(options.el, this)
}
}
class Compile {
constructor(elSelector, vm) {
this.vm = vm
const element = document.querySelector(elSelector)
// console.log(element, this.$vm);
this.compile(element)
}
compile(element) {
// 这里不能用children要用childNodes
// console.log(element.children);
// console.log(element.childNodes);
const childNodes = element.childNodes
console.log(childNodes);
}
}
可以给各位小伙伴一个思考,为什么编译方法里获取的是childNodes而不是children,基础好的小伙伴应该猜到了!因为children只能获取元素,但childNodes可以获取节点
判断元素还是文本
这里的知识相对容易,首先要知道的nodeType的值,可以查看MDN,其中文本节点,我们还要判断是否有双大扩号语法,所以要用到正则,具体代码如下
// 是否元素
isElement (node) {
return node.nodeType === 1
}
// 文本
isInter (node) {
// nodeType在mdn查询 正则可以在浏览器里调试 RegExp.$1
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
打印节点相关内容
接着遍历childNodes,使用前面的判断方法,做些打印
compile(element) {
const childNodes = element.childNodes;
// console.log(childNodes);
childNodes.forEach(node => {
// console.log(node.nodeName, node.nodeType);
if (this.isElement(node)) {
console.log('element',node.nodeName);
// 如果是元素 还需要递归
} else if (this.isInter(node)) {
// 文本
console.log('inter', node.textContent);
}
})
}
元素节点需要递归处理
元素节点里还有文本节点,所以要做递归处理
if (this.isElement(node)) {
console.log('element', node.nodeName)
if (node.childNodes.length > 0) {
// 元素节点里还有节点,那递归遍历
this.compile(node)
}
}
编译text节点
实现compileText(不要忘记加trim,还有这里简易实现没有考虑文本节点里复杂的逻辑),并在判断文本节点的地方调用这个方法
compileText (node) {
// 不要忘记trim 可能双大扩前后加了空格
node.textContent = this.vm[RegExp.$1.trim()]
}
else if(this.isInter(node)) {
console.log('inter', node.textContent);
this.compileText(node)
}
元素节点解析指令
元素先要解析他的属性
if (this.isElement(node)) {
// console.log('element', node.nodeName)
const attrs = node.attributes; // 新增代码
console.log(attrs); // 新增代码
if (node.childNodes.length > 0) {
// 递归处理
this.compile(node)
}
}
接着遍历attrs,注意了他是个伪数组,所以先要转成真数组,在遍历!遍历后console.dir
打印下attr
[...attrs].forEach(attr => {
console.dir(attr)
})
我们知道了attr
上有name
和value
属性,那接下去,我们就解构加起别名处理下!
const attrs = node.attributes;
// console.log(attrs);
[...attrs].forEach(({name: attrName, value: exp}) => {
console.log(attrName, exp)
})
判断是否是指令
判断是否是指令,只要判断字符串开始是否有v-
就可以
isDir (str) {
return str.startsWith('v-')
}
每个指令提供一个方法
text方法实现
text (node, exp) {
console.log('text方法', node, exp);
node.textContent = this.vm[exp]
}
v-text就调用text方法(v-html就调用html方法等)
if (this.isDir(attrName)) {
// console.log('是指令', attrName, '表达式为', exp);
const dirName = attrName.slice(2)
// console.log(dirName);
this[dirName]?.(node, exp)
}
思考:小伙伴们可以自行完成v-html
指令试试哈,后面还会带大家实现v-model
,还有事件@click
等等
至此,解析指令,初始化视图,一个简易的编译器就写完了
Watcher - 观察者
-
vue的实现:模板 => vdom => dom
-
本次实现跳过虚拟dom,简易的实现(其实是Vue1的实现)
-
观察者模式不理解的,可以自行补充该知识哈,或者看下以下这个扎心的例子
class ProductManager {
constructor () {
this.workers = []
}
addWorker (...worker) {
this.workers.push(...worker)
}
notify (prd) {
this.workers.forEach(item => {
item.update(prd)
})
}
}
class Worker {
constructor(name) {
this.name = name
}
update(prd) {
console.log(prd + `需求来了,${this.name}准备996`);
}
}
const pm = new ProductManager()
const frontWorker = new Worker('前端')
const endWorker = new Worker('后端')
const testWorker = new Worker('测试')
pm.addWorker(frontWorker, endWorker, testWorker)
pm.notify('一个复杂的功能')
此例子模拟了产品发布一个复杂的功能,只要setPrd
就通知了所有人员一起干活!
依赖收集
视图中会用到data中的某key,这称为依赖,同一个key可能出现多次,每次都需要收集出来用一个Watcher来维护他们,此过程称为依赖收集
多个Watcher需要一个Dep来管理(vue的最终实现是多对多的关系),需要更新时由Dep统一通知,接下去说明下关系
<div>
<p>{{name1}}</p>
<p>{{name2}}</p>
<p>{{name1}}</p>
</div>
收集依赖 有几个大括号就有几个watcher 所以这里有3个watcher
有几个key就有几个管家dep 所以dep只有2个,
Dep1 deps = [watcher1, watcher3]
Dep2 deps = [watcher2]
实现思路
- defineReactive时为每一个key创建一个Dep实例
- 初始化视图时读取个key 创建一个watcher 比如name1 就创建个watcher1
- 触发name1的getter方法,将watcher1添加到name1对应的Dep中
- 当name1更新,setter触发时,通过对应Dep通知管理所有Watcher更新
源码是N对N 这里我们简化下Dep和watcher 1对N
那接下来搭架子, 我们要了解watcher具体要干什么,请看以下代码及注释
/**
* 负责具体节点更新
* Watcher的用法是是用来更新数据的 vm[exp]可以拿到对应的数据,在通过val修改,这里第三个参数传递方法
* new Watcher(vm, exp, (val) => {})
*/
class Watcher {
constructor(vm, key, updater) {
this.vm = vm;
this.key = key
this.updater = updater
}
// 给管家调用
update () {
this.updater(this.vm[this.key])
}
}
watcher实例化前准备工作
只要涉及到编译的地方都要new Watcher
watcher在什么时候实例化,编译的时候实例化,只要有动态绑定的就实例化!比如text方法,compileText方法等,因为他们都要解析动态绑定的值,所以我们需要重构下Compile类,提供更高级的方法
为了复用,可以提供textUpdater,他做的事情就是给节点赋值内容
除了textUpdater,以后可能有其他的xxxUpdater,所以可以提供一个统一的update
方法,
update
函数的形参为node
, exp
, dir
,分别指的是节点,表达式,哪一种指令(之后调用哪种Updater)
// 处理所有动态绑定
update(node, exp, dir) {
// 1. 初始化
this[dir + 'Updater']?.(node, this.vm[exp])
// 2. 创建Watcher实例,负责后续管理
}
textUpdater(node, val) {
// console.log(node, val);
node.textContent = val
}
text (node, exp) {
// console.log('text方法', node, value);
// node.textContent = this.vm[value]
this.update(node, exp, 'text')
}
compileText (node) {
// node.textContent = this.vm[RegExp.$1]
this.update(node, RegExp.$1, 'text')
}
小伙伴们还可以自己实现下htmlUpdater和html,看下初始化功能是否ok
实例化watcher
实例化Watcher
update(node, exp, dir) {
// 1. 初始化
this[dir + 'Updater']?.(node, this.vm[exp])
// 2. 创建Watcher实例,负责后续管理, 一定要用箭头函数否则this指向有问题
new Watcher(this.vm, exp, (val) => {
this[dir + 'Updater']?.(node, val)
})
}
简单粗暴全量更新
简单粗暴全量更新,声明一个全局的watchers,在实例化Watcher的时候,就添加watcher,最后在劫持的set里调用update方法
const watchers = []
function defineReactive (obj, key, val) {
// 直接数据劫持,不用担心observe方法写了递归结束条件
observe(val)
Object.defineProperty(obj, key, {
get () {
// 形成闭包
// console.log('get', { key })
return val
},
set (newVal) {
if (newVal !== val) {
// 形成闭包
// console.log('set', { key })
// console.log(watchers)
observe(newVal)
val = newVal
watchers.forEach(w => w.update())
}
}
})
}
class Watcher {
constructor(vm, key, updater) {
this.vm = vm;
this.key = key
this.updater = updater
watchers.push(this)
}
// 给管家调用
update () {
this.updater(this.vm[this.key])
}
}
此时效果已经出来了,已经能看到页面能不断更新了,(前面定时器的原因不断累加),但我们实现还不够完美,还要处理下管家Dep
处理Dep
把前面粗暴的代码删了
class Dep {
constructor() {
this.subs = []
}
addSub (sub) {
// sub就是watcher
this.subs.push(sub)
}
notify () {
this.subs.forEach(sub => sub.update())
}
}
难点-如何建立Watcher和Dep的关系
- 之前说了,一个key对应一个管家,所以在defineReactive中,实例化Dep
- 实例化后的Watcher什么时候添加进管家的subs,这里有个巧妙的方式
- 实例化后Watcher挂在Dep.target上
- 手动取值触发下劫持的get函数
- 最后Dep.target清空
- 何时通知所有的watcher
- 在set的时候,当前dep直接notify
核心代码如下
function defineReactive (obj, key, val) {
// 新增实例化Dep
const dep = new Dep()
observe(val)
Object.defineProperty(obj, key, {
get () {
// 新增 - 添加watcher
Dep.target && dep.addSub(Dep.target)
return val
},
set(newVal) {
if (newVal != val) {
// console.log('set', {key, newVal});
observe(newVal)
val = newVal
// 新增 - 通知所有watcher更新
dep.notify()
}
}
})
}
class Watcher {
constructor(vm, key, updater) {
this.vm = vm;
this.key = key;
this.updater = updater
// 以下三行代码新增
Dep.target = this // 实例化watcher挂在Dep.target上
this.vm[this.key] // 手动触发劫持的get
Dep.target = null // 触发完将Dep.target清空
}
update () {
this.updater(this.vm[this.key])
}
}
至此整个简易的vue就实现好了,在来回看之前的那个图,应该就能看懂了
完结撒花!
补充
数组变异方法 - 了解
- 目前数组是无法感知有变化的
const arr = [1, 2, 3]
observe(arr)
arr.push(4, 5)
arr[3] = 444
arr[4] = 555
-
实现数组响应式
- 找到数组原型
- 覆盖修改数组的更新方法
// 原先的 const arrayProto = Array.prototype // 备份一份 const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(method => { const original = arrayProto[method] arrayMethods[method] = function () { // 执行原先的 const result = original.apply(this, args) // 处理覆盖的逻辑 console.log('变异方法开始', method); // ... return result } })
- 在observe方法中做判断,是数组的话将得到的新的原型设置到数组实例原型上
function observe(obj) { if (typeof obj !== 'object' || obj === null) { return; } if (Array.isArray(obj)) { obj.__proto__ = arrayMethods for (let i = 0; i < obj.length; i++) { const item = obj[i]; observe(item) } } else { Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) } }
编译器@事件实现
if (this.isEvent(attrName)) {
// console.log(attrName, exp);
const eventName = attrName.slice(1)
// console.log(eventName, exp);
this.eventHandler(node, eventName, exp)
}
eventHandler(node, eventName, exp) {
const fn = this.vm.$options?.methods?.[exp]
node.addEventListener(eventName, fn.bind(this.vm))
}
isEvent(str) {
return str.indexOf('@') === 0
}
编译器 v-model实现
modelUpdater(node, val) {
node.value = val
}
model(node, exp) {
// update方法只完成赋值和更新
this.update(node, exp, 'model')
// 事件监听
node.addEventListener('input', e => {
this.vm[exp] = e.target.value
})
}
总结
完整代码
// 先不用dep做测试,只要实例化Watcher 就添加进数组,并且在劫持set的时候,直接更新
// const watchers = []
function defineReactive(obj, key, value) {
// 实例化管家 管家里有2个方法,一个是addDep 还有一个notify
const dep = new Dep()
observe(value)
Object.defineProperty(obj, key, {
get() {
// console.log('get', { key, value });
if (Dep.target) {
// dep.addSub(watcher实例)
dep.addSub(Dep.target)
}
return value
},
set(newValue) {
if (newValue !== value) {
observe(newValue)
// console.log('set', {key, newValue});
value = newValue
// watchers.forEach(item => item.update())
dep.notify()
}
}
})
}
function observe (obj) {
// 递归要注意 终止条件 判断不是对象 或者是null 就return
if(typeof obj !== 'object' || obj === null) return
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
// set方法 劫持新的属性
function set(target, key, value) {
defineReactive(target, key, value)
}
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
this.$options = options
// $data 判断data是对象还是函数 函数执行取返回值
this.$data = options.data
// data数据是响应式的
observe(options.data)
// 代理 希望
// 访问 vm.count => vm.$data.count
// 修改 vm.count++ => vm.$data.count++
proxy(this) // this在这里就是实例 就是vm
new Compile(options.el, this)
}
}
// 解析模板
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);
// children只能拿到子元素,不能用这个方案,因为除了元素还有文本,文本有双大括号需要解析
// console.log(element.children);
// console.log(element.childNodes);
element.childNodes.forEach(node => {
// nodeType === 1 元素节点
// nodeType === 3 文本节点
// console.log(node, node.nodeType);
if (this.isElement(node)) {
// console.log('元素', node);
const attrs = node.attributes
// console.log([...attrs]);
Array.from(attrs).forEach(({name: attrName, value: exp}) => {
// console.dir(attr)
// name - 属性名 - 键
// value - 属性值 - 值
// console.log(attrName ,exp);
if (this.isDir(attrName)) {
// console.log('需要解析指令 指令名',attrName);
const dirName = attrName.slice(2)
// console.log(dirName);
// console.log('需要解析指令 表达式',exp);
/**
* 每个指令提供一个方法
* v-text -> 调用text方法 text(node, exp)
* v-html -> 调用html方法 html(node, exp)
*/
// 以下代码不能这么写,因为text写死了
// this.text(node, exp)
// dirName就动态的
this[dirName]?.(node, exp)
}
})
if (node.childNodes.length > 0) {
// 递归处理
this.compile(node)
}
}
if (this.isInter(node)) {
// 能进这个if 说明是文本并且里面有双大括号
// console.log('文本', node);
this.compileText(node)
}
})
}
isElement (node) {
return node.nodeType === 1
}
// 是文本节点,且里面有双大扩号语法 (正则) (node节点里的内容 正则匹配)
// 判断有双大括号语法的文本
isInter (node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
// direction isDir - 判断个字符串是否是指令 (指令都是v-开头)
// 如何判断一个字符串是v-开头呢 startsWith
isDir (str) {
return str.startsWith('v-')
}
update (node, exp, dir) {
// this.textUpdater(node, this.vm[exp])
// 初始化的工作
this[dir + 'Updater']?.(node, this.vm[exp])
// 实例化Watcher 做监听,做数据变化之后的一些功能
new Watcher(this.vm, exp, val => {
// console.log(this);
// val指的是最新的值
this[dir + 'Updater']?.(node, val)
})
}
// xxxUpdater
// yyyUpdater
textUpdater (node, val) {
node.textContent = val
}
htmlUpdater (node, val) {
node.innerHTML = val
}
// 编译文本
/**
* 简易版 直接替换双大扩号语法内容
* 复杂版的逻辑 本次不考虑 有兴趣的可以自己扩展
* {{ 1 + count}} 双大括号里复杂的逻辑 本次不考虑
* {{count}} {{haha}} {{heihei}} 一个文本节点中有多个双大括号 本次也不考虑
* xxxx {{xxx}} 文案加双大括号 本次也不考虑
* @param {*} node
*/
compileText(node) {
// (haha) => ( haha )
// console.log('compileText', node, RegExp.$1, this.vm);
// node.textContent = this.vm[RegExp.$1.trim()]
// new Watcher()
this.update(node, RegExp.$1.trim(), 'text')
}
text (node, exp) {
// node.textContent = this.vm[exp]
// new Watcher()
this.update(node, exp, 'text')
}
html (node, exp) {
// node.innerHTML = this.vm[exp]
// new Watcher
this.update(node, exp, 'html')
}
}
class Watcher {
// new Watcher(vm, key, updater函数)
// new Watcher(vm, key, val => {渲染的逻辑})
// 有值就能做渲染 node.textContent = vm[key]
constructor(vm, key, updater) {
this.vm = vm;
this.key = key;
this.updater = updater
Dep.target = this
this.vm[this.key]
Dep.target = null
// watchers.push(this)
}
// dep通知所有对应的watcher 遍历执行update
update () {
// 执行updater函数就可以了
this.updater(this.vm[this.key])
}
}
class Dep {
constructor() {
this.subs = [] // watcher数组
}
// 添加watcher
addSub (sub) {
this.subs.push(sub)
}
// 通知对应的watchers里的每一个观察者做更新
notify () {
this.subs.forEach(item => item.update())
}
}
简易Vue分析
vue1的实现基本就是这样,vue2用虚拟dom是因为vue1带来的问题,这次实现,模板中每有一个变量就有一个watcher,明显不合理! 所以vue2后面一个组件一个watcher,但他怎么知道要更新具体哪个呢,所以有了虚拟dom的概念,这也是引进来虚拟dom的必要性
转载自:https://juejin.cn/post/7241554520760746041