课前预习




手写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.fooobj.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 = newValupdate()}}})}defineReactive(obj, 'foo', '')obj.foo = new Date().toLocaleTimeString()function update () {app.innerText = obj.foo}setInterval(() => {obj.foo = new Date().toLocaleTimeString()}, 1000);</script>
遍历需要响应化的对象
// 对象响应化:遍历每个key,定义getter、setterfunction 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.fooobj.foo = 'foooooooooooo'obj.barobj.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做代理```javascriptclass 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;// 依赖keythis.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) returndep.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;}
