课前预习
手写Vue
复习
https://www.processon.com/view/link/5e146d6be4b0da16bb15aa2a
理解Vue的设计思想
将视图View的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。
MVVM的框架是有共性的,不管是react还是angular。在模板方面,angular和vue是非常相似的,他们都是基于模板的。因为模板最终还是要进行编译的。用户看到的模板不是模板,而是渲染函数,所以一定要经过编译过程将模板变成渲染函数。后续再使用响应式的方式。每个框架都有自己的响应式的处理方式。比如说vue2中会使用数据拦截的方式(属性拦截),vue3中会使用代理。react中是比较主动,会直接使用setState的这种方式,主动去调用状态的变更。虽然他们使用的方式不同,但是他们最终的目标是一样的。他们的目标是: 1、希望数据层和视图层清晰的分开(view和model层分开) 2、视图如何工作,视图变化如何影响数据。所以需要有VM这一层。vue这个核心就是实现VM的这个逻辑(即数据发生变化,如何知道,如何实现响应机制,需要实现数据响应式,这样才能将数据绑定到视图,当数据发生变化的时候,重新执行渲染函数,重新执行更新函数,然后让视图去更新。反过来,如果视图发生变化,则应该有一个机制可以监听事件,这个事件可以反过来作用于模型。于是形成非常好的正向循环。MVVM的逻辑就运转起来了。) 在这个过程中,至少实现下面的基本点
模板引擎的语法,因为我们要使用数据绑定。需要写一个特殊的模板引擎来描述视图,因此要有语法。比如插值语句,包括一些指令。还需要将数据响应式、模板引擎及其渲染连起来,需要更新函数
MVVM框架的三要素: 数据响应式、模板引擎及其渲染
数据响应式:监听数据变化并在视图中更新
- Object.defineProperty()
- Proxy
模版引擎:提供描述视图的模版语法
- 插值:{{}}
- 指令:v-bind,v-on,v-model,v-for,v-if
渲染:如何将模板转换为html
- 模板 => vdom => dom
vue2中中间有一个vdom,有虚拟函数的原因是:渲染函数是作为中间的这一层,当数据发生变化的时候,先执行渲染函数得到最新的vdom,再将当前的vdom和上次的vdom之间进行比对,从而得到真实的dom操作,借助上面的方式减少dom的操作次数,使应用程序的性能稍微提高一点。这个是vue2中核心的工作方式
今天实现:将模板直接编译,直接变成一个更新函数,更新函数是直接根据数据去做更新的。(直接跳过vdom层,直接编译模板),如果数据发生变化,则直接走编译的更新函数,之后直接进行dom操作
数据响应式原理
数据响应式是一种机制。数据发生变化了,需要知道,知道数据变化之后,需要进行更新或者后续的一些事情。这个就是目的。(在vuex的原理中)vue2中利用了Object.defineProperty()这样可以定义里面的每一个key,当有人去get(访问)的时候,这时候能拦截到,这时候将定义的getters的函数执行一下。这时候将这个key对应的getter函数执行一下,将得到的结果返回。这时候借助get的拦截实现了自定义的逻辑
数据变更能够响应在视图中,就是数据响应式。vue2中利用Object.defineProperty()
实现变更检
测。(数据响应式是一种机制)
简单实现,01-reactive.js
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 = 'foooooooooooo'
结合视图,02-reactive.html
<div id="app"></div>
<script>
const obj = {}
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
get () {
console.log(`get ${key}:${val}`);
return val
},
set (newVal) {
if (newVal !== val) {
val = newVal
update()
}
}
})
}
defineReactive(obj, 'foo', '')
obj.foo = new Date().toLocaleTimeString()
function update () {
app.innerText = obj.foo
}
setInterval(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000);
</script>
遍历需要响应化的对象
// 对象响应化:遍历每个key,定义getter、setter
function observe (obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
const obj = { foo: 'foo', bar: 'bar', baz: { a: 1 } }
observe(obj)
obj.foo
obj.foo = 'foooooooooooo'
obj.bar
obj.bar = 'barrrrrrrrrrr'
obj.baz.a = 10 // 嵌套对象no ok
解决嵌套对象问题
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
//...
解决赋的值是对象的情况
obj.baz = {a:1}
obj.baz.a = 10 // no ok
set(newVal) {
if (newVal !== val) {
observe(newVal) // 新值是对象的情况
notifyUpdate()
如果添加/删除了新属性无法检测
obj.dong = 'dong'
obj.dong // 并没有get信息
function set(obj, key, val) {
defineReactive(obj, key, val)
}
测试
set(obj, 'dong', 'dong')
obj.dong
defineProperty()
无法感知数组的push、pop等方法对数组的修改
Vue中的数据响应化
目标代码
kvue.html
<div id="app">
<p>{{counter}}</p>
</div>
<script src="../node_modules/vue/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
counter: 1
},
})
setInterval(() => {
app.counter++
}, 1000);
</script>
原理分析
new Vue()
首先执行初始化 ,对 data执行响应化处理 ,这个过程发生在Observer中
2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在
Compile中
3. 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
4. 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个
Watcher
5. 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
MVVM的类,在实例化的时候,需要做两件事: 1.创建Observer实例(劫持MVVM类中data中的所有属性,对他们进行拦截)即数据响应式 2.执行一次编译(Compile),编译模板,找出下面动态的部分,也就是符合vue语法的部分。之后可以尝试设置初始值,以及未来的更新逻辑。找到动态部分的时候可以初始化视图,将最新的值放入,替换vue语法为最新值。这个就是依赖,为依赖创建一个小秘书(Watcher),Watcher做的是一对一的贴身服务具体的依赖(也就是页面中的vue的语法),如果在模板中找到了多个vue的语法,则会出现多个依赖(vue的语法和watcher是一对一的)。Watcher的目标是什么,他将自己去Dep的对象中做一次订阅。Dep的产生,在做数据响应式的时候会产生一个Dep对象,也就是数据响应式数据中的key都有一个Dep对象,称Dep对象为大管家。Dep的手下管理无数的Watcher,为了数据响应式中的值发生变化,则大管家通过手下的所有的Watcher去执行真正的更新函数。这就是这张图的逻辑。 (编译完成之后创建了很多Watcher,并将Watcher放入到响应式数据初始化创建的Dep中,当数据响应式值发生变化的时候,再让Dep通知他手下的Watcher,去做更新,页面就更新了,这是基本思路)
涉及类型介绍
KVue:框架构造函数
(对应上面的MVVM)
Observer:执行数据响应化(分辨数据是对象还是数组)
(根据传入对象的类型做不同的响应式操作)
Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
编译作用是将模板中的vue语法变成更新函数,创建对应的Watcher
Watcher:执行更新函数(更新dom)
做具体的更新
Dep:管理多个Watcher,批量更新
Dep负责做协调,他知道哪个key发生变化,要通知哪些Watcher,所以他是中间人是协调者
KVue
框架构造函数:执行初始化
执行初始化,对data执行响应化处理,kvue.js ```javascript function observe (obj) { if (typeof obj !== ‘object’ || obj == null) { return }
new Observer(obj) }
function defineReactive (obj, key, val) { }
class KVue { constructor (options) { this.$options = options; this.$data = options.data;
observe(this.$data)
} }
class Observer { constructor (value) { this.value = value this.walk(value); }
walk (obj) { Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) } }
- 为$data做代理
```javascript
class KVue {
constructor(options) {
// 。。。
proxy(this);
}
}
function proxy(vm) {
Object.keys(vm.$data).forEach((key) => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(newVal) {
vm.$data[key] = newVal;
},
});
});
}
编译 - Compile
编译模板中vue模板特殊语法,初始化视图、更新视图
编译: 程序刚初始化的时候,得到dom中的模板,开始进行遍历子元素,找动态的东西(即vue的语法,插值绑定、属性绑定、指令、事件),如果是节点,则遍历节点中属性,看看属性是否是k-开头或者@开头。之后按照不同的方式来处理这些动态的语句(@开头使用事件处理)。如果是文本,看一下是否是{{}}包裹,是则是动态,否则直接跳过
初始化视图
根据节点类型编译,compile.js
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => {
if (this.isElement(node)) {
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("编译插值文本" + node.textContent);
}
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
编译插值,compile.js
compile(el) {
// ...
} else if (this.isInerpolation(node)) {
// console.log("编译插值文本" + node.textContent);
this.compileText(node);
}
});
}
compileText(node) {
console.log(RegExp.$1);
node.textContent = this.$vm[RegExp.$1];
}
编译元素
compile(el) {
//...
if (this.isElement(node)) {
// console.log("编译元素" + node.nodeName);
this.compileElement(node)
}
}
compileElement(node) {
let nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(attr => {
let attrName = attr.name;
let exp = attr.value;
if (this.isDirective(attrName)) {
let dir = attrName.substring(2);
this[dir] && this[dir](node, exp);
}
});
}
isDirective(attr) {
return attr.indexOf("k-") == 0;
}
text(node, exp) {
node.textContent = this.$vm[exp];
}
k-html
html(node, exp) {
node.innerHTML = this.$vm[exp]
}
依赖收集
视图中会用到data中某key,这称为 依赖 。同一个key可能出现多次,每次都需要收集出来用一个Watcher来维护它们,此过程称为依赖收集。
多个Watcher需要一个Dep来管理,需要更新时由Dep统一通知。
Dep和Watcher1对多关系 Dep和响应式对象的key是一对一的关系
只要发现动态内容就创建Watcher 只要发现key就创建一个Dep。 在做初始化的时候,需要在数据响应式对象中获取最初的值,读取某个key值,一旦get那个key时。数据响应式中的get函数会被触发,在第一次读取的时候,可以将Watch和Dep之间建立映射关系,将Dep和Watcher关联起来。初始化做Dep和Watcher的关联。做完之后,当数据变化通知更新时,则可以更新视图。逻辑闭环就形成了
看下面案例,理出思路:
new Vue({
template:
`<div>
<p>{{name1}}</p>
<p>{{name2}}</p>
<p>{{name1}}</p>
<div>`,
data: {
name1: 'name1',
name2: 'name2'
}
});
实现思路
- defineReactive时为每一个key创建一个Dep实例
2. 初始化视图时读取某个key,例如name1,创建一个watcher
3. 由于触发name1的getter方法,便将watcher1添加到name1对应的Dep中
4. 当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
创建Watcher,kvue.js
const watchers = [];//临时用于保存watcher测试用
// 监听器:负责更新视图
class Watcher {
constructor (vm, key, updateFn) {
// kvue实例
this.vm = vm;
// 依赖key
this.key = key;
// 更新函数
this.updateFn = updateFn;
// 临时放入watchers数组
watchers.push(this)
}
// 更新
update () {
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
编写更新函数、创建watcher
// 调用update函数执插值文本赋值
compileText(node) {
// console.log(RegExp.$1);
// node.textContent = this.$vm[RegExp.$1];
this.update(node, RegExp.$1, 'text')
}
text(node, exp) {
this.update(node, exp, 'text')
}
html(node, exp) {
this.update(node, exp, 'html')
}
update(node, exp, dir) {
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val)
})
}
textUpdater(node, val) {
node.textContent = val;
}
htmlUpdater(node, val) {
node.innerHTML = val
}
声明Dep
class Dep {
constructor () {
this.deps = []
}
addDep (dep) {
this.deps.push(dep)
}
notify () {
this.deps.forEach(dep => dep.update());
}
}
创建watcher时触发getter
class Watcher {
constructor (vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
依赖收集,创建Dep实例
defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep()
Object.defineProperty(obj, key, {
get () {
Dep.target && dep.addDep(Dep.target);
return val
},
set (newVal) {
if (newVal === val) return
dep.notify()
}
})
}
可以尝试调试代码
作业
- 完成事件处理@xx,注意上下文
- v-model: value, @input
思考拓展
- 实现数组响应式
(vue1.0)这里的问题:watch太多了,watch颗粒度太细了。页面中只要有一个动态值,就有一个watch。页面庞大的时候,占用太多的内存资源,导致程序会崩溃
在vue2中,每个组件是一个watch。不知道每个组件中什么发生变化了。则使用虚拟dom和diff算法来比较页面中什么发生变化,再进行操作
作业
实现数组响应式
找到数组原型
在数组原型中存在哪些若干希望覆盖的方法(push、pop、shift、unshift),如果key将这些方法覆盖,让这些方法不仅可以做之前的事情之外。还能额外的做更新通知,这样就实现的数组的响应式操作(让数组的方法可以实现一个额外的更新通知)
覆盖那些能够修改数组的更新方法,使其可以通知更新
这样就是响应式了,数据发生变化之后,可以让他发送一个update的通知,视图中就可以做响应了,这就是基本思路
将得到的新的原型设置到数组实例的原型上
将来这些数组调用方法的时候,会以最新的覆盖方法为准
替换数组原型中的7个方法
// 数组响应式
// 1.替换数组原型中的7个方法
const originalProto = Array.prototype;
// 备份一份Array的原型,修改备份
const arrayProto = Object.create(originalProto);
// 尝试更改的方法有7个:push、pop、shift、unshift、splice、reserve、sort方法
['push', 'pop', 'shift', 'unshift'].forEach(method => {
arrayProto[method] = function() {
// 原始操作
originalProto[method].apply(this, arguments)
// 覆盖操作:通知更新
console.log('数组执行' + method + '操作' + arguments);
}
})
对于响应式数组的实例进行对象原型的更改
// 自动设置一个对象的所有属性为响应式的
function observe(obj) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
// 判断传入的obj的类型
if (Array.isArray(obj)) {
// 设置实例的原型 --- 不要更改原始的原型,否则会影响其他的数组使用
// 覆盖原型,替换7个变更操作
console.log('覆盖原型,替换7个变更操作')
obj.__proto__ = arrayProto;
// 对数组内部的元素执行响应化
const keys = Object.keys(obj);
for (let i=0; i< obj.length; i++) {
observe(obj[i]);
}
} else {
// 循环对象
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
对于手写的vue1.0中,数组的响应式代码写在Observer的类中
完成后续的k-model、@XX
@XXX
在元素编译的方法中添加事件的处理 ```javascript // 元素的编译 compileElement(node) { // 遍历元素所有特性,判断是否动态 let nodeAttrs = node.attributes; // attributes:访问节点的所有特性
// 伪数组转换成数组 Array.from(nodeAttrs).forEach((attr) => { // k-text=”counter” const attrName = attr.name; // k-text const exp = attr.value; // counter // 判断是否是一个指令 if (attrName.startsWith(“k-“)) { // 以什么什么开头startsWith
// 获取指令名称
const dir = attrName.substring(2); // text
// 执行dir对应的方法(node:表示节点,exp:表示表达式)
this[dir] && this[dir](node, exp);
}
// 事件处理 if (this.isEvent(attrName)) {
// @click="onClick"
const dir = attrName.substring(1) // click
// 事件监听
// exp是onClick是函数的名称
this.eventHandler(node, exp, dir);
} }); }
// 判断是否是事件指令 isEvent(dir) { return dir.indexOf(‘@’) == 0 }
// 事件处理 eventHandler(node, exp, dir) { // methods: {onClick:function(){}} const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp] // 事件的回调函数,this绑定到vue实例 node.addEventListener(dir, fn.bind(this.$vm)) }
<a name="ZAhRf"></a>
### k-model是语法糖
k-model是语法糖,解决了两个事,一个是value值的设定,一个是事件的监听
```javascript
// k-model="**"
model(node, exp, dir) {
// update方法只完成赋值和更新(是单向的)
this.update(node, exp, 'model')
// 事件监听(监听的事件有可能是其他事件)
node.addEventListener('input', e => {
// 新的值赋值给数据即可
this.$vm[exp] = e.target.value;
})
}
// 更新表单元素
modelUpdater (node, value) {
// 表单元素赋值 --- 大部分表单元素的元素赋值是下面的写法
node.value = value;
}