Vue简易版实现 - 笔记

224 阅读14分钟

Vue简易版 - 学习笔记

大家好,我是小瑜, 最近跟着峰哥大佬学习并手撕了Vue的简易版实现, 没有学习之前感觉框架的底层原理很复杂很难, 但是跟着大佬渐渐深入后发现并不是特别的难以理解, 反而越发喜欢, 也越发成谜. 学习完后就马不停蹄的在近凌晨2点, 按照实现思路,完成并产出了这篇学习笔记博客. 此篇笔记包含 响应式 简易编译器 WatcherDep,知道他们如何建立关系

f9afdbde142cf78f617edfb1010cf91.png

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函数的执行

所以会让视图发生

05ade3aac120fec26f3a6b3845b937b.png

因为每次修改都会执行一次set方法

因为执行了三次 defineReactive() 所以访问了3次get

b55b7329023a3ef361196bc5af18595.png

现在控制台修改了数据,视图更新了, 需要考虑三个问题

  1. 在使用vue的时候,更新凹函数我们不需要写
  • 其实都是template自动编译成更新函数的
  1. 劫持字段要用户一个一个自己去劫持,不合理
  • 需要遍历,后面还需要递归
  1. 全量更新, 只要数据一发生变化(改变其中一个属性), 视图就直接全部更新了,不合理
  • 精确定位具体的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,我被修改了' }

此时还是有问题,例如给劫持的属性赋值新的对象时没有发生变化

f3676ac6310a22823606d6d66582e8d.png

此时 需要给set方法也要定义成响应式

f4df8425aad88a0e2b9e64ea9bd1967.png

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…

025de1ad211bdd72d513e662209b3e4.png

因为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的值并没发生变化

7420ff7ef152ea9766e9dc89d0a7af7.png

其实原因也很简单, 这行代码时给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
  }
}

d5defdb7aa6594a57629ea7aa1fc92b.png

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方法

image.png

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 }}"

判断是否为 双大括号 需要文本节点&&正则进行区分, 具体可以看一下控制台的测试

57bbbc8203969b316cbef40acfaab23.png

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)
   }
 }

dd05658562127cbe1515639b9953d2a.png

此时 再嵌套, 通过递归就可以获取到所有的双大括号文本

接下来就是将{{ 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()]
  }

此时页面上就可以渲染出来内容

291ce2b1840573fc9bcbc1d5757c2cf.png

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-')
  }

b042ab98a9d5cce96752753f5fd4f73.png

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]
  }

b9aaeaff1bb539a2b366a0ef1530714.png

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进行全量更新
  1. 在update中 更新函数 去实例化 Watcher 并且在第三个回调函数中接收最新的参数进行视图更新
  2. 进行全量更新, 创建一个wachers数组, 当数据发生变化也就是在defineProperty中set方法执行的时候去调用wachers中的update方法
  3. 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())
  }
}

接下来就是建立关联

在建立关联之前需要将全量更新的代码进行注释

  1. 这里有一个很巧妙的方法, 在new Watcher的时候会执行constructor
  2. 里面将实例挂载至Dep的静态属性target
  3. 挂载上的用处就是去触发get方法去访问this.vm[this.key]也就是访问data中的每一个响应式数据的key
  4. 因为是响应式数据只要一访问就会触发get
  5. 触发完成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>
  1. 需要在解析模板中判断当前传入的是否为事件 也就是是否为@开头

  2. 需要在获取元素内容中去触发事件,其中需要告诉处理函数是哪一个元素 事件名称 需要改变的属性值

  3. 进行事件处理=> 获取事件的回调,也就是传入的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的实现

  1. v-model在vue2中是一个语法糖 :value + @input

  2. 定义model指令并,update更新并触发input事件将

  3. 提供更新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存在的重要性.

git地址