课前预习

image.png
image.png
image.png

image.png

手写Vue

复习

https://www.processon.com/view/link/5e146d6be4b0da16bb15aa2a

image.png

理解Vue的设计思想

将视图View的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。

MVVM的框架是有共性的,不管是react还是angular。在模板方面,angular和vue是非常相似的,他们都是基于模板的。因为模板最终还是要进行编译的。用户看到的模板不是模板,而是渲染函数,所以一定要经过编译过程将模板变成渲染函数。后续再使用响应式的方式。每个框架都有自己的响应式的处理方式。比如说vue2中会使用数据拦截的方式(属性拦截),vue3中会使用代理。react中是比较主动,会直接使用setState的这种方式,主动去调用状态的变更。虽然他们使用的方式不同,但是他们最终的目标是一样的。他们的目标是: 1、希望数据层和视图层清晰的分开(view和model层分开) 2、视图如何工作,视图变化如何影响数据。所以需要有VM这一层。vue这个核心就是实现VM的这个逻辑(即数据发生变化,如何知道,如何实现响应机制,需要实现数据响应式,这样才能将数据绑定到视图,当数据发生变化的时候,重新执行渲染函数,重新执行更新函数,然后让视图去更新。反过来,如果视图发生变化,则应该有一个机制可以监听事件,这个事件可以反过来作用于模型。于是形成非常好的正向循环。MVVM的逻辑就运转起来了。) 在这个过程中,至少实现下面的基本点

image.png

模板引擎的语法,因为我们要使用数据绑定。需要写一个特殊的模板引擎来描述视图,因此要有语法。比如插值语句,包括一些指令。还需要将数据响应式、模板引擎及其渲染连起来,需要更新函数

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()实现变更检
测。(数据响应式是一种机制)
image.png

简单实现,01-reactive.js

  1. function defineReactive (obj, key, val) {
  2. Object.defineProperty(obj, key, {
  3. get () {
  4. console.log(`get ${key}:${val}`);
  5. return val
  6. },
  7. set (newVal) {
  8. if (newVal !== val) {
  9. console.log(`set ${key}:${newVal}`);
  10. val = newVal
  11. }
  12. }
  13. })
  14. }
  15. const obj = {}
  16. defineReactive(obj, 'foo', 'foo')
  17. obj.foo
  18. obj.foo = 'foooooooooooo'

结合视图,02-reactive.html

  1. <div id="app"></div>
  2. <script>
  3. const obj = {}
  4. function defineReactive (obj, key, val) {
  5. Object.defineProperty(obj, key, {
  6. get () {
  7. console.log(`get ${key}:${val}`);
  8. return val
  9. },
  10. set (newVal) {
  11. if (newVal !== val) {
  12. val = newVal
  13. update()
  14. }
  15. }
  16. })
  17. }
  18. defineReactive(obj, 'foo', '')
  19. obj.foo = new Date().toLocaleTimeString()
  20. function update () {
  21. app.innerText = obj.foo
  22. }
  23. setInterval(() => {
  24. obj.foo = new Date().toLocaleTimeString()
  25. }, 1000);
  26. </script>

遍历需要响应化的对象

  1. // 对象响应化:遍历每个key,定义getter、setter
  2. function observe (obj) {
  3. if (typeof obj !== 'object' || obj == null) {
  4. return
  5. }
  6. Object.keys(obj).forEach(key => {
  7. defineReactive(obj, key, obj[key])
  8. })
  9. }
  10. const obj = { foo: 'foo', bar: 'bar', baz: { a: 1 } }
  11. observe(obj)
  12. obj.foo
  13. obj.foo = 'foooooooooooo'
  14. obj.bar
  15. obj.bar = 'barrrrrrrrrrr'
  16. obj.baz.a = 10 // 嵌套对象no ok

解决嵌套对象问题

  1. function defineReactive(obj, key, val) {
  2. observe(val)
  3. Object.defineProperty(obj, key, {
  4. //...

解决赋的值是对象的情况

  1. obj.baz = {a:1}
  2. obj.baz.a = 10 // no ok
  1. set(newVal) {
  2. if (newVal !== val) {
  3. observe(newVal) // 新值是对象的情况
  4. notifyUpdate()

如果添加/删除了新属性无法检测

  1. obj.dong = 'dong'
  2. obj.dong // 并没有get信息
  1. function set(obj, key, val) {
  2. defineReactive(obj, key, val)
  3. }

测试

  1. set(obj, 'dong', 'dong')
  2. obj.dong

defineProperty()无法感知数组的push、pop等方法对数组的修改

Vue中的数据响应化

目标代码

kvue.html

  1. <div id="app">
  2. <p>{{counter}}</p>
  3. </div>
  4. <script src="../node_modules/vue/dist/vue.js"></script>
  5. <script>
  6. const app = new Vue({
  7. el: '#app',
  8. data: {
  9. counter: 1
  10. },
  11. })
  12. setInterval(() => {
  13. app.counter++
  14. }, 1000);
  15. </script>

原理分析

  1. 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,去做更新,页面就更新了,这是基本思路)

image.png

涉及类型介绍

  • 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;

  1. 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]) }) } }

  1. - $data做代理
  2. ```javascript
  3. class KVue {
  4. constructor(options) {
  5. // 。。。
  6. proxy(this);
  7. }
  8. }
  9. function proxy(vm) {
  10. Object.keys(vm.$data).forEach((key) => {
  11. Object.defineProperty(vm, key, {
  12. get() {
  13. return vm.$data[key];
  14. },
  15. set(newVal) {
  16. vm.$data[key] = newVal;
  17. },
  18. });
  19. });
  20. }

编译 - Compile

编译模板中vue模板特殊语法,初始化视图、更新视图

编译: 程序刚初始化的时候,得到dom中的模板,开始进行遍历子元素,找动态的东西(即vue的语法,插值绑定、属性绑定、指令、事件),如果是节点,则遍历节点中属性,看看属性是否是k-开头或者@开头。之后按照不同的方式来处理这些动态的语句(@开头使用事件处理)。如果是文本,看一下是否是{{}}包裹,是则是动态,否则直接跳过

image.png

初始化视图

根据节点类型编译,compile.js

  1. class Compile {
  2. constructor(el, vm) {
  3. this.$vm = vm;
  4. this.$el = document.querySelector(el);
  5. if (this.$el) {
  6. this.compile(this.$el);
  7. }
  8. }
  9. compile(el) {
  10. const childNodes = el.childNodes;
  11. Array.from(childNodes).forEach((node) => {
  12. if (this.isElement(node)) {
  13. console.log("编译元素" + node.nodeName);
  14. } else if (this.isInterpolation(node)) {
  15. console.log("编译插值文本" + node.textContent);
  16. }
  17. if (node.childNodes && node.childNodes.length > 0) {
  18. this.compile(node);
  19. }
  20. });
  21. }
  22. isElement(node) {
  23. return node.nodeType == 1;
  24. }
  25. isInterpolation(node) {
  26. return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
  27. }
  28. }

编译插值,compile.js

  1. compile(el) {
  2. // ...
  3. } else if (this.isInerpolation(node)) {
  4. // console.log("编译插值文本" + node.textContent);
  5. this.compileText(node);
  6. }
  7. });
  8. }
  9. compileText(node) {
  10. console.log(RegExp.$1);
  11. node.textContent = this.$vm[RegExp.$1];
  12. }

编译元素

  1. compile(el) {
  2. //...
  3. if (this.isElement(node)) {
  4. // console.log("编译元素" + node.nodeName);
  5. this.compileElement(node)
  6. }
  7. }
  8. compileElement(node) {
  9. let nodeAttrs = node.attributes;
  10. Array.from(nodeAttrs).forEach(attr => {
  11. let attrName = attr.name;
  12. let exp = attr.value;
  13. if (this.isDirective(attrName)) {
  14. let dir = attrName.substring(2);
  15. this[dir] && this[dir](node, exp);
  16. }
  17. });
  18. }
  19. isDirective(attr) {
  20. return attr.indexOf("k-") == 0;
  21. }
  22. text(node, exp) {
  23. node.textContent = this.$vm[exp];
  24. }

k-html

  1. html(node, exp) {
  2. node.innerHTML = this.$vm[exp]
  3. }

依赖收集

视图中会用到data中某key,这称为 依赖 。同一个key可能出现多次,每次都需要收集出来用一个Watcher来维护它们,此过程称为依赖收集。
多个Watcher需要一个Dep来管理,需要更新时由Dep统一通知。

Dep和Watcher1对多关系 Dep和响应式对象的key是一对一的关系

只要发现动态内容就创建Watcher 只要发现key就创建一个Dep。 在做初始化的时候,需要在数据响应式对象中获取最初的值,读取某个key值,一旦get那个key时。数据响应式中的get函数会被触发,在第一次读取的时候,可以将Watch和Dep之间建立映射关系,将Dep和Watcher关联起来。初始化做Dep和Watcher的关联。做完之后,当数据变化通知更新时,则可以更新视图。逻辑闭环就形成了

看下面案例,理出思路:

  1. new Vue({
  2. template:
  3. `<div>
  4. <p>{{name1}}</p>
  5. <p>{{name2}}</p>
  6. <p>{{name1}}</p>
  7. <div>`,
  8. data: {
  9. name1: 'name1',
  10. name2: 'name2'
  11. }
  12. });

image.png

实现思路

  1. defineReactive时为每一个key创建一个Dep实例
    2. 初始化视图时读取某个key,例如name1,创建一个watcher
    3. 由于触发name1的getter方法,便将watcher1添加到name1对应的Dep中
    4. 当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新

image.png

创建Watcher,kvue.js

  1. const watchers = [];//临时用于保存watcher测试用
  2. // 监听器:负责更新视图
  3. class Watcher {
  4. constructor (vm, key, updateFn) {
  5. // kvue实例
  6. this.vm = vm;
  7. // 依赖key
  8. this.key = key;
  9. // 更新函数
  10. this.updateFn = updateFn;
  11. // 临时放入watchers数组
  12. watchers.push(this)
  13. }
  14. // 更新
  15. update () {
  16. this.updateFn.call(this.vm, this.vm[this.key]);
  17. }
  18. }

编写更新函数、创建watcher

  1. // 调用update函数执插值文本赋值
  2. compileText(node) {
  3. // console.log(RegExp.$1);
  4. // node.textContent = this.$vm[RegExp.$1];
  5. this.update(node, RegExp.$1, 'text')
  6. }
  7. text(node, exp) {
  8. this.update(node, exp, 'text')
  9. }
  10. html(node, exp) {
  11. this.update(node, exp, 'html')
  12. }
  13. update(node, exp, dir) {
  14. const fn = this[dir + 'Updater']
  15. fn && fn(node, this.$vm[exp])
  16. new Watcher(this.$vm, exp, function (val) {
  17. fn && fn(node, val)
  18. })
  19. }
  20. textUpdater(node, val) {
  21. node.textContent = val;
  22. }
  23. htmlUpdater(node, val) {
  24. node.innerHTML = val
  25. }

声明Dep

  1. class Dep {
  2. constructor () {
  3. this.deps = []
  4. }
  5. addDep (dep) {
  6. this.deps.push(dep)
  7. }
  8. notify () {
  9. this.deps.forEach(dep => dep.update());
  10. }
  11. }

创建watcher时触发getter

  1. class Watcher {
  2. constructor (vm, key, updateFn) {
  3. Dep.target = this;
  4. this.vm[this.key];
  5. Dep.target = null;
  6. }
  7. }

依赖收集,创建Dep实例

  1. defineReactive(obj, key, val) {
  2. this.observe(val);
  3. const dep = new Dep()
  4. Object.defineProperty(obj, key, {
  5. get () {
  6. Dep.target && dep.addDep(Dep.target);
  7. return val
  8. },
  9. set (newVal) {
  10. if (newVal === val) return
  11. dep.notify()
  12. }
  13. })
  14. }

可以尝试调试代码

作业

  • 完成事件处理@xx,注意上下文
  • v-model: value, @input

思考拓展

  • 实现数组响应式

(vue1.0)这里的问题:watch太多了,watch颗粒度太细了。页面中只要有一个动态值,就有一个watch。页面庞大的时候,占用太多的内存资源,导致程序会崩溃

在vue2中,每个组件是一个watch。不知道每个组件中什么发生变化了。则使用虚拟dom和diff算法来比较页面中什么发生变化,再进行操作

作业

实现数组响应式

  • 找到数组原型

    在数组原型中存在哪些若干希望覆盖的方法(push、pop、shift、unshift),如果key将这些方法覆盖,让这些方法不仅可以做之前的事情之外。还能额外的做更新通知,这样就实现的数组的响应式操作(让数组的方法可以实现一个额外的更新通知)

  • 覆盖那些能够修改数组的更新方法,使其可以通知更新

    这样就是响应式了,数据发生变化之后,可以让他发送一个update的通知,视图中就可以做响应了,这就是基本思路

  • 将得到的新的原型设置到数组实例的原型上

    将来这些数组调用方法的时候,会以最新的覆盖方法为准

  1. 替换数组原型中的7个方法

    1. // 数组响应式
    2. // 1.替换数组原型中的7个方法
    3. const originalProto = Array.prototype;
    4. // 备份一份Array的原型,修改备份
    5. const arrayProto = Object.create(originalProto);
    6. // 尝试更改的方法有7个:push、pop、shift、unshift、splice、reserve、sort方法
    7. ['push', 'pop', 'shift', 'unshift'].forEach(method => {
    8. arrayProto[method] = function() {
    9. // 原始操作
    10. originalProto[method].apply(this, arguments)
    11. // 覆盖操作:通知更新
    12. console.log('数组执行' + method + '操作' + arguments);
    13. }
    14. })
  2. 对于响应式数组的实例进行对象原型的更改

    1. // 自动设置一个对象的所有属性为响应式的
    2. function observe(obj) {
    3. if (typeof obj !== "object" || obj === null) {
    4. return obj;
    5. }
    6. // 判断传入的obj的类型
    7. if (Array.isArray(obj)) {
    8. // 设置实例的原型 --- 不要更改原始的原型,否则会影响其他的数组使用
    9. // 覆盖原型,替换7个变更操作
    10. console.log('覆盖原型,替换7个变更操作')
    11. obj.__proto__ = arrayProto;
    12. // 对数组内部的元素执行响应化
    13. const keys = Object.keys(obj);
    14. for (let i=0; i< obj.length; i++) {
    15. observe(obj[i]);
    16. }
    17. } else {
    18. // 循环对象
    19. Object.keys(obj).forEach((key) => {
    20. defineReactive(obj, key, obj[key]);
    21. });
    22. }
    23. }

    对于手写的vue1.0中,数组的响应式代码写在Observer的类中

完成后续的k-model、@XX

@XXX

  1. 在元素编译的方法中添加事件的处理 ```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

    1. // 获取指令名称
    2. const dir = attrName.substring(2); // text
    3. // 执行dir对应的方法(node:表示节点,exp:表示表达式)
    4. this[dir] && this[dir](node, exp);

    }

    // 事件处理 if (this.isEvent(attrName)) {

    1. // @click="onClick"
    2. const dir = attrName.substring(1) // click
    3. // 事件监听
    4. // exp是onClick是函数的名称
    5. 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)) }

  1. <a name="ZAhRf"></a>
  2. ### k-model是语法糖
  3. k-model是语法糖,解决了两个事,一个是value值的设定,一个是事件的监听
  4. ```javascript
  5. // k-model="**"
  6. model(node, exp, dir) {
  7. // update方法只完成赋值和更新(是单向的)
  8. this.update(node, exp, 'model')
  9. // 事件监听(监听的事件有可能是其他事件)
  10. node.addEventListener('input', e => {
  11. // 新的值赋值给数据即可
  12. this.$vm[exp] = e.target.value;
  13. })
  14. }
  15. // 更新表单元素
  16. modelUpdater (node, value) {
  17. // 表单元素赋值 --- 大部分表单元素的元素赋值是下面的写法
  18. node.value = value;
  19. }