详解 Vue 2.X 核心源码,手撸一个简易版Vue框架(上篇)

658 阅读10分钟

属性描述符

Object.defineProperty(属性描述符)可以说是Vue中非常核心的API了,通过它完成了属性代理和数据劫持继而实现了数据的响应式。

Object.defineProperty语法

Object.defineProperty( object, prop, descriptor )

  • object:必需,目标对象
  • prop:必需,需要定义或修改的属性名
  • descriptor:必需,目标属性的特性
    • value:目标属性的值,默认为undefined
    • writable:目标属性是否可以被重写,true:可被重写 false:不能重写 默认为false
    • enumberable:目标属性是否可被枚举(通过for...in,Object.keys()),true:可被枚举 false:不可枚举 默认为false
    • configurable:目标属性属性是否可被删除或可再次修改属性特性(writable、enumberable、configurable),true:目标属性可被删除或属性特性可重新设置 false:目标属性不可被删除或属性特性不可重新设置 默认为false
    • get:function(){...}:当访问该属性时,会触发get方法。get方法返回值作为该属性的值返回
    • set:function(value){...}:当修改该属性值时,会触发set方法。属性的新值作为函数的参数传入 注意:当时使用 getset 方法时,不允许使用 writablevalue
const person = {}
let initName = '哥哥'
Object.defineProperty(obj,"name",{
  get() {
    return initName  
  },
  set(newVal) {
    initName = newVal
  }
})
console.log(person.name)   // '哥哥'
person.name = '弟弟'
console.log(person.name,initName)   // '弟弟' '弟弟'

实现数据代理

在Vue中,我们可以通过实例访问data的所有属性,这是因为Vue实例代理了data属性。

class Vue {
  constructor(options) {
    // 将配置项挂在到Vue实例上
    this.$options = options
    this._data = options.data
    // 通过initData完成data初始化(将data属性挂载在到Vue实例上)
    this.initData()
  }
  initData() {
    let data = this._data
    let keys = Object.keys(data)
    // 遍历data 将挡data所有字段代理到Vue实例上
    for (let i= 0; i<keys.length; i++) {
      Object.defineProperty(this,key[i],{
        enumberable: true,
        configurable: true,
        set: function proxySetter(newVal) {
          data[keys[i]] = newVal
        },
        get: function prosyGetter() {
          return data[keys[i]]
        }
      })
    }
  }
}
const VM = new Vue({data:{name:"张三"}})
console.log(VM)
VM.name = '李四'       // 修改data属性 name的值 
console.log(VM.name)   // '李四'

实现数据劫持

在Vue实例属性特性的 setget 中,可以劫持到每个属性的读写操作,继而完成一些特殊逻辑来实现响应式。

class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
  }
  initData() {
    let data = this._data
    let keys = Object.keys(data)
    // 数据代理
    ...
    // 数据劫持
    // 注意:数据劫持操作后面会被复用,所以单独实现,没有跟数据代理for循环放到一起
    for (let i= 0; i<keys.length; i++) {
      let value = data[keys[i]]
      Object.defineProperty(this,key[i],{
        enumberable: true,
        configurable: true,
        set: function reactiveSetter(newVal) {
          if(newVal===data[keys[i]]) return // 与原来值相同 终止逻辑
          console.log(`${keys[i]}被赋予了新值--${newVal}`)
          value = newVal
        },
        get: function reactiveGetter() {
          console.log(`获取${keys[i]}的值`)
          return data[keys[i]]
        }
      })
    }
  }
}
const VM = new Vue({data:{name:"张三"}})
console.log(VM)
VM.name = '李四'       // name被赋予了新值--李四

递归深层次数据劫持

上面的操作只实现一维data数据的劫持,如果某个data数据为复杂数据类型,这个data数据内部属性的读写我们就无法劫持了。所以我们需要递归实现劫持数据。即:

const VM = new Vue({
  data:{
    person:{
      name:"张三"
    }
  }
})
console.log(VM)
VM.person = '李四'       // person被赋予了新值--李四
VM.person.name = '李四'  // name的setter无法劫持到数据
class Vue {
  ...
  initData() {
    ...
    // 数据劫持
    observe(data)
  }
}
// 1、 observe函数:判断数据类型,新建Observer实例
function observe(data) {
  // 1.1 如果被观测的data为基本数据类型 就返回
  const type = Object.prototype.toString.call(data)
  if (type !== '[object Object]' || (type !== '[object Array]')) return
  // 1.2 观测数据涉及一些复杂的逻辑 将这个过程封装为一个Observer类
  new Observer(data)
}
// 2、Observer类:用来观测数据、生成负责处理依赖的Dep实例等复杂逻辑
class Observer {
  constructor(data) {
    this.walk(data)
  }
  walk(data) {
    let keys = Object.keys(data)
    // 2.1 将initData中,劫持数据的操作放到此处
    //     遍历data,劫持传入data的所有属性 
    for (let i= 0; i<keys.length; i++) {
      defineReactive(data,keys[i],data[keys[i]])
    }
  }
}
// 3、defineReactive工具函数:用来递归劫持data,将data数据变为响应式数据
defineReactive(object,key,value){
  // 3.1、递归调用defineReactive来递归劫持深层次data数据   defineReactive--observe--Observer--defineReactive
  observe(obj[key])
  // 3.2、数据劫持
  Object.defineProperty(object,key,{
    enumberable: true,
    configurable: true,
    // 注意:不要在object属性的setter/getter中 通过object访问这个属性否则会陷入死循环
    set: function reactiveSetter(newVal) {
      if(newVal===value) return // 与原来值相同 终止逻辑
      console.log(`${keys[i]}被赋予了新值--${newVal}`)
      // 数据更新 在此处可做视图更新操作
      value = newVal
    },
    get: function reactiveGetter() {
      console.log(`获取${keys[i]}的值`)
      return value
    }
  })
}

逻辑梳理

let option = {
  person:{
    name:"张三"
  }
}
let vm = new Vue(option)

1、new Vue实例

在这个阶段,会将 option实例option.data 等挂载到Vue示例上,然后调用 initData 等方法完成相关数据的初始化

2、data数据的初始化

这个阶段是在 initData 方法中实现,会完成 data数据初始化 操作
例如
1、遍历 option.data,将person等data数据挂载到Vue实例上
2、劫持data数据,将其变为响应式数据

3、判断数据类型、new Observer实例

这个阶段通过 observe 方法完成对 参数data 的观测,先判断类型 如果为复杂数据类型则继续 new Observer 实例 实现对data的观测,初始调用传入的实参为 option.data

4、劫持数据

这个阶段 在 Observe 类的实例化中,首先调用实例walk方法 遍历传入的data,调用 defineReactive 为每个data数据增加响应式逻辑

5、数据的响应式

这个阶段defineReactive函数中,首先调用observe传入data,实现递归劫持深层次data数据,对data数据进行劫持在set/get中添加 数据的响应式逻辑

Watcher的实现

watcher可以侦听data数据的变化并触发各种回调,例如 模板更新回调、计算属性更新回调等.
由于模板的更新涉及 模板编译VDOM ,这里先通过 watch 选项 和 $watch来测试 Watcher的实现

1、通过option选项添加 Watcher

var vm = new Vue({
  data:{
    msg: 1
  },
  watch:{
    // 当data.msg值发生变化时,该回调就会执行同时传入data.msg变化后的值,变化前的值。
    msg(val, oldVal) { ...... }
  }
})

2、调用实例方法添加 Watcher

var callback = function(val, oldVal) { ...... }
var unwatch = vm.$watch('msg',callback)
// 解除侦听
unwatch('msg',callback)

实现思路

为每个响应式数据添加一个事件中心,通过这个事件中心去收集watcher。响应式数据发生变化时,它会通过事件中心通知watcher更新。在Vue中,使用抽象类Dep实现了事件中心的功能。

Dep类

为每个响应式data数据添加Dep实例,通过Dep来保存、收集和派发watcher。

// 3、defineReactive工具函数:用来递归劫持data,将data数据变为响应式数据
function defineReactive(obj, key, value) {
  // 递归调用defineReactive来递归劫持深层次data数据   defineReactive--observe--Observer--defineReactive
  observe(obj[key])
  // 4.0、为每个data数据新建一个Dep实例,并通过闭包维护
  let dep = new Dep()
  // 数据劫持
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    set: function reactiveSetter(newVal) {
      if (newVal === value) return
      // 4.4、Dep派发依赖更新  
      dep.notify(newVal,value)
      value = newVal
    },
    get: function reactiveGetter() {
      // 4.5、Dep收集依赖  
      dep.depend()
      return value
    }
  })
}
// 4、Dep抽象类:负责收集依赖、通知依赖更新等  
class Dep {
// 4.1、subs用来保存所有订阅者
  constructor(option) { this.subs = []}
  // 4.2、depend方法用来收集订阅者依赖
  depend() { this.subs.push(/**新增回调 */)}
  // 4.3、notify方法用来派发订阅者更新
  notify(newVal,value) {
    this.subs.forEach(watcher => watcher.update(newVal,value))
  }
}

Watcher类

每个订阅者回调会涉及到复杂的逻辑,所以将其抽离成了Watcher类来实现。

// 4、Dep类:负责收集依赖、通知依赖更新等  
class Dep {
  constructor(option) {
    // 4.1、subs用来保存所有订阅者
    this.subs = []
  }
  // 4.2、depend方法用来收集订阅者依赖
  depend() {
    // 5.5、如果为Watcher实例初始化
    if (Dep.target) {
      // 5.6、 每个data数据Watcher实例化,都会先设置Dep.target并触发data数据得getter,完成依赖得收集
      this.subs.push(Dep.target)
    }
  }
  // 4.3、notify方法用来派发订阅者更新
  notify(newVal,value) {
    // 5.7、 执行每个订阅者Watcher的run方法完成 更新
    this.subs.forEach(watcher => watcher.run(newVal,value))
  }
}
// 5、Watcher类: 触发依赖收集、处理更新回调
class Watcher {
  constructor(vm, exp, cb) {
    // 5.1、将Vue实例、data属性名和更新回调 挂载到watcher实例上
    this.vm = vm
    this.exp = exp
    this.cb = cb
    // 5.2、触发data数据的getter 完成依赖收集
    this.get()
  }
  get() {
    // 5.3、将Watcher实例设为 Dep依赖收集的目标对象
    Dep.target = this
    // 5.4、触发data数据getter拦截器
    this.vm[this.exp]
    // 清空依赖目标对象
    Dep.target = null
  }
  run(newVal,value) {
    this.cb.call(this.vm,newVal,value)
  }
}

实现vm.$watch

在Vue类中通过$watch实例方法动态添加订阅回调

class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
  }
  initData() {....}  
  // 6、动态添加订阅回调
  $watch(key, cb) {
    new Watcher(this, key, cb)
  }
}
// 测试
let vm = new Vue({ data: {message: 111} })
vm.$watch("message", function (val, oldVal) {
   console.log("message值变化了", val, oldVal);
})
vm.message = 22  // 打印出  message值变化了 22 111

实现option.watch

在Vue类中通过initWatch实例方法动态添加订阅回调

class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
    this.initWatch()
  }
  initData() {....}  
  initWatch() {
    const watches = this.$options.watch
    // 存在watch选项
    if (watches) {
      const keys = Object.keys(watches)
      for (let index = 0; index < keys.length; index++) {
        new Watcher(this, keys[index], watches[keys[index]])
      }
    }
  }
  // 6、动态添加订阅回调
  $watch(key, cb) {
    new Watcher(this, key, cb)
  }
}
// 测试
let vm = new Vue({
  data: {
    name: "哈哈哈",
    age: 13
  },
  watch: {
    age(newVal, val) {console.log("age", newVal, val);},
    name(newVal, val) {console.log("name", newVal, val);}
  }
})
 vm.age = 22
 vm.name = "测试"

异步Watcher

Vue源码中watcher回调就是异步执行的。

为什么要将watcher回调设置为异步执行?

  • 1、异步执行的watcher可以避免watch回调先执行
// 当watcher回调为同步时
// mounted中的修改this.age值会触发age的watch回调 继而修改this.name的值
let vm = new Vue({
  data: {
    name: "哈哈哈",
    age: 13
  },
  watch: {
    age(newVal, val) {
       this.name = '李四'   
    }
  },
  mounted() {
     this.age = 20 
     console.log(this.name)  // '李四'
  }
})

  • 2、避免多次触发watch回调,有利于性能优化
// 当watcher回调为同步时,频繁修改watch侦听的data数据,watch回调会被多次触发。会造成性能浪费。
let vm = new Vue({
  data: {
    name: "哈哈哈",
    age: 13
  },
  watch: {
    age(newVal, val) {
       this.name = '李四'   
    }
  },
  mounted() {
     this.age = 20 
     this.age = 21 
     this.age = 22
     this.age = 23 
  }
})

实现思路

let watcherId = 0
// watcher任务队列
let watcherQueue = []
// 5、Watcher类: 触发依赖收集、处理更新回调
class Watcher {
  constructor(vm, exp, cb) {
    // 5.1、将Vue实例、data属性名和更新回调 挂载到watcher实例上
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    // 5.2、触发data数据的getter 完成依赖收集
    this.get()
  }
  get() {
    // 5.3、将Watcher实例设为 Dep依赖收集的目标对象
    Dep.target = this
    // 5.4、触发data数据getter拦截器
    this.vm[this.exp]
    // 清空依赖目标对象
    Dep.target = this
  }
  run(newVal, value) {
    // 5.8 如果该任务已存在与任务队列中 则终止
    if (watcherQueue.indexOf(this.id)!==-1) return
    // 5.9 将当前watcher添加到 队列中
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() => {
      this.cb.call(this.vm, newVal, value)
      // 5.10 任务执行结束 将其从任务队列中删除  
      watcherQueue.splice(index, 1)
    })
  }
}

$set的实现

$set的作用是什么,解决了什么问题?

1623201526(1).jpg

<body>
  <script crossorigin="anonymous"
    integrity="sha512-pSyYzOKCLD2xoGM1GwkeHbdXgMRVsSqQaaUoHskx/HF09POwvow2VfVEdARIYwdeFLbu+2FCOTRYuiyeGxXkEg=="
    src="https://lib.baomitu.com/vue/2.6.14/vue.js"></script>
  <script>
    let VM = new Vue({
      data: {
        person: {
          age: 10
        }
      },
      watch: {
        person() {
          console.log("person发生了变化");
        }
      }
    })
    // 通过对象.属性的方法 并不会将该属性变为响应式数据,也不会触发watch中监听person的回调
    // VM.person.name = '哈哈'
    // 通过Vue实例的$set,可以为对象新增响应式属性,并触发watch中监听person的回调
    // VM.$set(VM.person, 'name', '哈哈')
  </script>
</body>

实现思路

打印Vue实例我们可看到,每个data数据都会有一个__ob__属性。这个__ob__就是Observer实例,前面我们以闭包的形式为每个data数据配置Observer实例,并通过Dep实例完成依赖的收集和派发。在这里可以为每个data数据挂载Observer实例(__ob__),使用$set修改data数据并派发data数据依赖的回调。

所以$set的实现思路基本如下:

  • 在生成Observer实例,也新建一个新的Dep实例(事件中心),挂在Observer实例上。然后将Observer实例挂载到data数据上。
  • 触发getter,不仅要将Watcher在闭包的Dep实例收集一份,也要在__ob__Dep实例中也要收集一份。
  • 使用$set时,手动触发__ob__.Dep.notify()派发依赖更新。
  • 同时在调用notify之前,需要先调用definedReactive,将新属性也变为响应式。
    截止该阶段的全部源码
// 6.X 是该阶段实现$set相关源码!!!
class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
    this.initWatch()
  }
  initData() {
    let data = this._data
    let keys = Object.keys(data)
    // 数据代理
    for (let i = 0; i < keys.length; i++) {
      Object.defineProperty(this, keys[i], {
        enumerable: true,
        configurable: true,
        set: function proxySetter(newVal) {
          data[keys[i]] = newVal
        },
        get: function proxyGetter() {
          return data[keys[i]]
        },
      })
    }
    // 数据劫持
    observe(data)
  }
  initWatch() {
    const watches = this.$options.watch
    // 存在watch选项
    if (watches) {
      const keys = Object.keys(watches)
      for (let index = 0; index < keys.length; index++) {
        new Watcher(this, keys[index], watches[keys[index]])
      }
    }
  }
  $watch(key, cb) {
    new Watcher(this, key, cb)
  }
  // 6.6 __ob__的挂载,依赖的收集工作已做完  
  $set(targt,key,value) {
   const oldValue = {...targt}
    // 6.7 将传入的新属性也变为响应式  
    defineReactive(targt,key,value)
    // 6.8 手动派发依赖更新  
    targt.__ob__.dep.notify(oldValue,targt)
  }
}
// 1、 observe函数:判断数据类型,新建Observer实例
function observe(data) {
  const type = Object.prototype.toString.call(data)
  // 1.1 如果被观测的data为基本数据类型 就返回
  if (type !== '[object Object]' && (type !== '[object Array]')) return
  // 1.2 观测数据涉及一些复杂的逻辑 将这个过程封装为一个Observer类
  // 1.2 new Observer(data)
  // 6.3 将Observer实例 return出去,并在defineReactive中接收。
  if(data.__ob__) return  data.__ob__
  return new Observer(data)
}
// 2、Observer类:观察者/侦听器,用来观测数据、生成负责处理依赖的Dep实例等复杂逻辑
class Observer {
  constructor(data) {
    // 6.1 为observer实例挂一个Dep实例(事件中心)
    this.dep = new Dep()
    // 2.1 将data所有属性 变为响应式 
    
    this.walk(data)
    // 6.2 将observer实例挂在到不可枚举的属性__ob__上,供外部$set使用 
    Object.defineProperty(data, "__ob__", {
      value: this,
      enumerable: false,
      configurable: true,
      writable: true
    })
  }
  walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      // console.log("definedBeforer",keys[i]);
      defineReactive(data, keys[i], data[keys[i]])
    }
  }
}
// 3、defineReactive工具函数:用来递归劫持data,将data数据变为响应式数据
function defineReactive(obj, key, value) {
  // 3.1 递归调用defineReactive来递归劫持深层次data数据   defineReactive--observe--Observer--defineReactive
  // 3.1 observe(obj[key])
  // 6.4 接收Observer实例,为属性Dep收集依赖 Watcher
  let childOb = observe(obj[key])
  // 4.0、为每个data数据新建一个Dep实例,并通过闭包维护
  let dep = new Dep()
  // 3.2 对当前data对象的 key 进行数据劫持
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    set: function reactiveSetter(newVal) {
      if (newVal === value) return
      // 4.4、Dep派发依赖更新  
      dep.notify(newVal, value)
      value = newVal
    },
    get: function reactiveGetter() {
      // 4.5、闭包Dep收集依赖 Watcher
      dep.depend()
      // 6.5 observe函数 如果传入数据为简单数据类型 就不会返回Observer实例 所以需要判断一下是否有Observer实例,如果有就为Observer实例的Dep也收集一份 依赖
      if(childOb) childOb.dep.depend()
      return value
    }
  })
}
// 4、Dep类:事件中心,负责收集依赖、通知依赖更新等  
class Dep {
  constructor(option) {
    // 4.1、subs用来保存所有订阅者
    this.subs = []
  }
  // 4.2、depend方法用来收集订阅者依赖
  depend() {
    // 5.5、如果为Watcher实例初始化
    if (Dep.target) {   
      // 5.6、 每个data数据Watcher实例化,都会先设置Dep.target并触发data数据得getter,完成依赖得收集
      this.subs.push(Dep.target)
    }
  }
  // 4.3、notify方法用来派发订阅者更新
  notify(newVal, value) {
    // 5.7、 执行每个订阅者Watcher的run方法完成 更新
    this.subs.forEach(watcher => watcher.run(newVal, value))
  }
}
let watcherId = 0
// watcher任务队列
let watcherQueue = []
// 5、Watcher类:订阅者,触发依赖收集、处理更新回调
class Watcher {
  constructor(vm, exp, cb) {
    // 5.1、将Vue实例、data属性名和更新回调 挂载到watcher实例上
    this.vm = vm
    this.exp = exp
    this.cb = cb
    // console.log("watchID",watcherId);
    this.id = ++watcherId
    // 5.2、触发data数据的getter 完成依赖收集
    this.get()
  }
  get() {
    // 5.3、将Watcher实例设为 Dep依赖收集的目标对象
    Dep.target = this
    // 5.4、触发data数据getter拦截器
    this.vm[this.exp]
    // 清空依赖目标对象
    Dep.target = null
  }
  run(newVal, value) {
    // 5.8 如果该任务已存在与任务队列中 则终止
    if (watcherQueue.indexOf(this.id)!==-1) return
    // 5.9 将当前watcher添加到 队列中
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() => {
      
      this.cb.call(this.vm, newVal, value)
      // 5.10 任务执行结束 将其从任务队列中删除  
      watcherQueue.splice(index, 1)
    })

  }
}

实现对数组的处理

Vue2.X在对数组的处理上有一定的缺陷

通过数组下标劫持数组可能会产生的问题:

监听下标回调错乱
使用数组下标去劫持数组,当数组元素顺序发生变化很有可能会造成 监听下标回调错乱的问题。

let arr = [
  {name:"0"},
  {name:"1"},
  {name:"2"}
] 
// 在初始化可以通过数组下标 劫持到每个元素,但是如果在每个下标1前面插入新的元素 就会造成 监听下标回调错乱的问题。
// 原来 arr[1] 用来处理元素 {name:2}的回调就会变成处理插入新元素的回调。

造成性能浪费
一个数组列表数据可能会有几十条、几百条甚至上千条,而用户真正操作的数据可能只有十几条。这个显然与我们的付出不成正比。(尤大大也曾提过,劫持数组所有数据所做的成本 对我们的收益而言,远远不成正比。所以Vue 2.x放弃了这种方式。)

源码中对数组的处理

源码中,并没有使用数组下标去劫持数组,对数组做了以下处理
1、数组依赖回调的收集也是通过__ob__.dep实现的。在数组调用pushpop等方法时手动触发__ob__.dep.notify
2、在数组原型对象上,我们插入一个新对象作为中间层。当用户调用数组原型对象上的方法 会先走中间层。我们可以进行拦截,先执行原本的方法,在触发__ob__.dep.notify()进行依赖更新派发。
3、将数组的每个元素也要变成响应式
改造数组的方法

// 7.X 为实现改造数组的新增代码
class Observer {
  constructor(data) {
    // 6.1 为observer实例挂一个Dep实例(事件中心)
    this.dep = new Dep()
    // 7.5 数组不能调用walk,因为walk会通过defineProperty劫持下标会出现依赖回调错乱等问题
    if(Array.isArray(data)) {
      // 7.6 用我们改造好的数组原型覆盖 自身的原型对象
      data.__proto__ = ArrayMethods
    }else {
      // 2.1 将data所有属性 变为响应式  
      this.walk(data)
    }
    // 6.2 将observer实例挂在到不可枚举的属性__ob__上,供外部$set使用 
    Object.defineProperty(data, "__ob__", {
      value: this,
      enumerable: false,
      configurable: true,
      writable: true
    })
  }
  walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      // console.log("definedBeforer",keys[i]);
      defineReactive(data, keys[i], data[keys[i]])
    }
  }
}
// 7.0 获取数组原型对象
const ArrayMethods = {}
ArrayMethods.__proto__ = Array.prototype
// 7.1 声明需要被改造的数组方法 这里举两个例子
const methods = ['push','pop']
// 7.2 对数组方法进行改造
methods.forEach(method=>{
  ArrayMethods[method] = function (...args) {
    const oldValue = [...this]  
    // 7.3 传入参数执行原本方法
    const result = Array.prototype[method].apply(this,args)
    // 7.4 派发依赖更新 
    this.__ob__.dep.notify(oldValue,this)
    return result
  }
})

将数组元素和插入的新数据变为响应式

class Observer {
  constructor(data) {
    // 6.1 为observer实例挂一个Dep实例(事件中心)
    this.dep = new Dep()
    // 7.5 数组不能调用walk,因为walk会通过defineProperty劫持下标会出现依赖回调错乱等问题
    if(Array.isArray(data)) {
      // 7.6 用我们改造好的数组原型覆盖 自身的原型对象
      data.__proto__ = ArrayMethods
      // 7.7 将数组所有子元素变为响应式 
      this.observeArray(data)
    }else {
      // 2.1 将data所有属性 变为响应式  
      this.walk(data)
    }
    // 6.2 将observer实例挂在到不可枚举的属性__ob__上,供外部$set使用 
    Object.defineProperty(data, "__ob__", {
      value: this,
      enumerable: false,
      configurable: true,
      writable: true
    })
  }
  walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      // console.log("definedBeforer",keys[i]);
      defineReactive(data, keys[i], data[keys[i]])
    }
  }
  // 7.8 将传入的数组的所有子元素 变为响应式
  observeArray(arr) {
   for (let i = 0; i < arr.length; i++) {
    observe(arr[i])
   }
  }
}
// 7.0 获取数组原型对象
const ArrayMethods = {}
ArrayMethods.__proto__ = Array.prototype
// 7.1 声明需要被改造的数组方法 这里举两个例子
const methods = ['push','pop']
// 7.2 对数组方法进行改造
methods.forEach(method=>{
  ArrayMethods[method] = function (...args) {
    const oldValue = [...this] 
    // 7.9 将新插入的数据也变为响应式  
     if(method==='push'){
       this.__ob__.observeArray(args)
     }
    // 7.3 传入参数执行原本方法
    const result = Array.prototype[method].apply(this,args)
    // 7.4 派发依赖更新 
    this.__ob__.dep.notify(oldValue,this)
    return result
  }
})

实现计算属性

特点

1、它是一个函数,它的值时这个函数运行的结果
2、计算属性使用的任何数据发生变化都会引起计算属性的变化
3、计算属性不存在与data中,需要单独初始化。
4、计算属性是只读的,不能修改它的值。它没有setter
5、计算属性是惰性的,它依赖的数据发生变化时不会立即重新计算结果,只有当你重新获取计算属性时,才会重新计算。 6、计算属性是有缓存的,当它依赖的数据没有发生变化时,不会重新获取计算属性的结果而是使用 之前的计算结果。

实现思路

计算属性本身也是一个watcher回调,只不过它可能依赖多个属性。在watcher初始化时,第二个参数传入key名代表watcher依赖的属性。我们可以传入并执行计算属性的函数,这样可以触发计算属性中多个依赖属性的getter,收集计算属性的watcher回调。

初步实现(8.0 - 8.8):

// 此阶段代码:8.0 -- 8.8
class Vue {
  constructor(options) {
    ...
    // 8.4 无论是计算属性的初始化还是data的初始化都必须放到watch初始化之前,因为计算属性和data的初始化完成 watch才能侦测到它们。
    this.initComputed()
    this.initWatch()
  }
  ...
  // 8.3 对计算属性单独初始化 
  initComputed() {
    const computeds = this.$options.computed
    if (computeds) {
      const keys = Object.keys(computeds)
      for (let index = 0; index < keys.length; index++) {
        // 8.5 第二个参数传入计算属性函数
        const watcher = new Watcher(this,  computeds[keys[index]],function() { })
        // 8.6 将该watcher挂载到Vue实例上  
        Object.defineProperty(this,keys[index],{
          enumerable: true,
          configurable: true,
          // 8.7 不允许用户修改计算属性
          set:function computedSetter() {
            console.warn("请不要修改计算属性")
          },
          // 8.8 通过watcher的get方法求值,并将求值结果返回出去
          get:function computedGetter() {
            watcher.get()
            return watcher.value
          }
        })
      }
    }
  }
  ...
}
...
let watcherId = 0
// watcher 任务队列
let watcherQueue = []
// 5、Watcher类:订阅者,触发依赖收集、处理更新回调
class Watcher {
  constructor(vm, exp, cb) {
    // 5.1、将Vue实例、data属性名和更新回调 挂载到watcher实例上
    this.vm = vm
    this.exp = exp
    this.cb = cb
    // console.log("watchID",watcherId);
    this.id = ++watcherId
    // 5.2、触发data数据的getter 完成依赖收集
    this.get()
  }
  get() {
    // 5.3、将Watcher实例设为 Dep依赖收集的目标对象
    Dep.target = this
    // 8.1  收集依赖之前先判断是否为函数 计算属性求值时会传入函数  
    if(typeof this.exp === 'function'){
      // 8.2 执行函数 并求出值
      this.value = this.exp.call(this.vm)
    }else {
      // 5.4、触发data数据getter拦截器
      this.value = this.vm[this.exp]
    }
    // 清空依赖目标对象
    Dep.target = null
  }
  run(newVal, value) {
    // 5.8 如果该任务已存在与任务队列中 则终止
    if (watcherQueue.indexOf(this.id)!==-1) return
    // 5.9 将当前watcher添加到 队列中
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() => {
      this.cb.call(this.vm, newVal, value)
      // 5.10 任务执行结束 将其从任务队列中删除  
      watcherQueue.splice(index, 1)
    })
  }
}
...

对计算属性进行缓存和惰性处理(8.9 - 8.15):

我们对计算属性的watcher做一个标识this.lazy = true,代表这是惰性的watcher。还要再加一个标识this.dirty,代表这个计算属性的依赖已经发生变化了,计算属性的计算结果是脏值 必须重新求值不能使用上次计算结果了。

class Vue {
  constructor(options) {
    ...
    // 8.4 无论是计算属性的初始化还是data的初始化都必须放到watch初始化之前,因为计算属性和data的初始化完成 watch才能侦测到它们。
    this.initComputed()
   ...
  }
  // 8.3 对计算属性单独初始化 
  initComputed() {
    const computeds = this.$options.computed
    if (computeds) {
      const keys = Object.keys(computeds)
      for (let index = 0; index < keys.length; index++) {
        // 8.5 第二个参数传入计算属性函数
        // 8.15 计算属性初始化的watcher  需要将其标记为惰性的
        const watcher = new Watcher(this,  computeds[keys[index]],function() { },{lazy:true})
        // 8.6 将该watcher挂载到Vue实例上  
        Object.defineProperty(this,keys[index],{
          enumerable: true,
          configurable: true,
          // 8.7 不允许用户修改计算属性
          set:function computedSetter() {
            console.warn("请不要修改计算属性")
          },
          // 8.8 通过watcher的get方法求值,并将求值结果返回出去
          get:function computedGetter() {
            // 8.9 只有watcher为脏数据时,再重新求值
            if(watcher.dirty) {
              watcher.get()
              // 8.10 求出新值 更新dirty状态  
              watcher.dirty = false
            }
            return watcher.value
          }
        })
      }
    }
  }
}
// 4、Dep类:事件中心,负责收集依赖、通知依赖更新等  
class Dep {
  constructor(option) {
    // 4.1、subs用来保存所有订阅者
    this.subs = []
  }
  // 4.2、depend方法用来收集订阅者依赖
  depend() {
    // 5.5、如果为Watcher实例初始化
    if (Dep.target) {
      // 5.6、 每个data数据Watcher实例化,都会先设置Dep.target并触发data数据得getter,完成依赖得收集
      this.subs.push(Dep.target)
    }
  }
  // 4.3、notify方法用来派发订阅者更新
  notify(newVal, value) {
    // 5.7、 执行每个订阅者Watcher的run方法完成 更新
    // 8.12 依赖更新派发更新时 先走update判断是否要更新
    this.subs.forEach(watcher => watcher.update(newVal, value))
  }
}
let watcherId = 0
// watcher任务队列
let watcherQueue = []
// 5、Watcher类:订阅者,触发依赖收集、处理更新回调
class Watcher {
  constructor(vm, exp, cb,option = {}) {
    // 8.13 watcher增加新参数 option ,对watcher进行默认配置
    this.lazy = this.dirty = !!option.lazy
    // 5.1、将Vue实例、data属性名和更新回调 挂载到watcher实例上
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    // 8.14 惰性watcher 初始化时不需要收集依赖
    if(!option.lazy) {
       // 5.2、触发data数据的getter 完成依赖收集
      this.get()
    }
  }
  get() {
    // 5.3、将Watcher实例设为 Dep依赖收集的目标对象
    Dep.target = this
    // 8.1  收集依赖之前先判断是否为函数 计算属性求值时会传入函数  
    if(typeof this.exp === 'function'){
      // 8.2 执行函数 并求出值
      this.value = this.exp.call(this.vm)
    }else {
      // 5.4、触发data数据getter拦截器
      this.value = this.vm[this.exp]
    }
    // 清空依赖目标对象
    Dep.target = null
  }
  // 8.11 在调用run之前先调用update,判断是否要直接run
  update(newVal, value) {
    // 8.12 依赖更新当前watcher为惰性时,不要直接run。而是将watcher标记为脏数据,等到用户主动获取结果再去run
    if(this.lazy) {
      this.dirty = true
    }else {
      thiss.run(newVal, value)
    }
  }
  run(newVal, value) {
    // 5.8 如果该任务已存在与任务队列中 则终止
    if (watcherQueue.indexOf(this.id)!==-1) return
    // 5.9 将当前watcher添加到 队列中
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() => {
      this.cb.call(this.vm, newVal, value)
      // 5.10 任务执行结束 将其从任务队列中删除  
      watcherQueue.splice(index, 1)
    })
  }
}

测试没问题,在不更改计算属性依赖的数据时,不会重新去计算。

1623768194(1).jpg

1623768220(1).jpg

当前计算属性出现的bug:

问题描述

在我们更改计算属性(x)的依赖(person)时,计算属性和监听计算属性的watch回调都没有被触发.

  • 我们的代码
  <script src="./index.js"></script>
  <script>
    let vm = new Vue({
      data: {
        person: {
          name: "张三"
        }
      },
      watch: {
        x(oldValue, newValue) {
          console.log("x监听触发");
        }
      },
      computed: {
        x() {
          console.log("x计算触发");
          return JSON.stringify(this.person)
        }
      }
    })
  </script>

1623823233(1).jpg ``

  • 在Vue中,没有任何问题
  <script crossorigin="anonymous"
    integrity="sha512-pSyYzOKCLD2xoGM1GwkeHbdXgMRVsSqQaaUoHskx/HF09POwvow2VfVEdARIYwdeFLbu+2FCOTRYuiyeGxXkEg=="
    src="https://lib.baomitu.com/vue/2.6.14/vue.js"></script>
  <script>
    let vm = new Vue({
      data: {
        person: {
          name: "张三"
        }
      },
      watch: {
        x(oldValue, newValue) {
          console.log("x监听触发");
        }
      },
      computed: {
        x() {
          console.log("x计算触发");
          return JSON.stringify(this.person)
        }
      }
    })
  </script>

1623823459(1).png

问题一

这里方便说明,我们将计算属性和watch中的watcher分别命名为1号watcher、2号watcher.

image.png

1号watcher的回调执行时有两个先决条件,它依赖的数据发生变化变为脏数据并且对这个watcher进行求值。我们虽然更改了依赖,但是并没有求值。
2号watcher是监听1号watcher的,当2号watcher初始化会先对1号watcher进行求值。

image.png

Vue初始化时,person会在person.dep收集一份1号wtacher。当person的值更改进行notify时,应该对watcher进行一次get求值。

// 9.0 调用watcher的get方法,对它进行求值
class Watcher {
  constructor(vm, exp, cb, option = {}) {
    // 8.13 watcher增加新参数 option ,对watcher进行默认配置
    this.lazy = this.dirty = !!option.lazy
    // 5.1、将Vue实例、data属性名和更新回调 挂载到watcher实例上
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    // 8.14 惰性watcher 初始化时不需要收集依赖
    if (!option.lazy) {
      // 5.2、触发data数据的getter 完成依赖收集
      this.get()
    }
  }
  get() {
    // 5.3、将Watcher实例设为 Dep依赖收集的目标对象
    Dep.target = this
    // 8.1  收集依赖之前先判断是否为函数 计算属性求值时会传入函数  
    if (typeof this.exp === 'function') {
      // 8.2 执行函数 并求出值
      this.value = this.exp.call(this.vm)
    } else {
      // 5.4、触发data数据getter拦截器 对其进行求值
      this.value = this.vm[this.exp]
    }
    // 清空依赖目标对象
    Dep.target = null
  }
  // 8.11 在调用run之前先调用update,判断是否要直接run
  update(newVal, value) {
    // 8.12 依赖更新当前watcher为惰性时,不要直接run。而是将watcher标记为脏数据,等到用户主动获取结果再去run
    if (this.lazy) {
      this.dirty = true
    } else {
      thiss.run(newVal, value)
    }
  }
  run(newVal, value) {
    // 5.8 如果该任务已存在与任务队列中 则终止
    if (watcherQueue.indexOf(this.id) !== -1) return
    // 5.9 将当前watcher添加到 队列中
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() => {
      // 9.0 依赖更新,对watcher进行求值 解决计算属性watcher 不被触发的问题
      this.get()
      this.cb.call(this.vm, newVal, value)
      // 5.10 任务执行结束 将其从任务队列中删除  
      watcherQueue.splice(index, 1)
    })
  }
}

问题二

我们更改计算属性x的依赖person值时,发现计算属性和监听计算属性的watch回调还是没有被触发.
我们分别打印一下Vue的person和我们代码中person,查看他们依赖的手机有没有问题

image.png

正常流程

  • vm初始化,vm.person的dep收集依赖,依次收集一号watcher和二号watcher。
  • vm.person = {name:'李四'},vm.person值发生变化。vm.person的dep通过notify去更新所有watcher
  • 由于计算属性1号watcher为惰性的,调用一号watcher的get方法并不会求值。只是把一号watcher的dirty标记为true.
  • 在更新二号watcher时,也是先调用get方法(对一号watcher求值),此时一号watcher的dirty为true,先执行wathcer(计算属性x)算出结果,然后在执行二号watcher的回调(watch中x回调).

image.png

我们的person的dep只收集到一号watcher(计算属性),并没有收集到二号watcher。这也是导致计算属性和监听计算属性的watch回调没有被触发的原因

image.png

问题三

我们为什么没有收集到二号watcher?

  • 查看我们的代码,在initComputed中,为计算属性x生成一号watcher。watcher初始化调用watcher.get(),在get方法中将一号watcher挂载到Dep.target,然后执行vm.x。
class Watcher {
  constructor(vm, exp, cb, option = {}) {
    this.lazy = this.dirty = !!option.lazy
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    if (!option.lazy) {
      this.get()
    }
  }
  get() {
    Dep.target = this
    // 8.1  收集依赖之前先判断是否为函数 计算属性求值时会传入函数  
    if (typeof this.exp === 'function') {
      // 8.2 执行函数 并求出值
      this.value = this.exp.call(this.vm)
    } else {
      // 5.4、触发data数据getter拦截器 对其进行求值
      this.value = this.vm[this.exp]
    }
    // 清空依赖目标对象
    Dep.target = null
  }
}
  • 在vm.x中,this.person触发person的getter,person.dep收集一号watcher
function defineReactive(obj, key, value) {
  let childOb = observe(obj[key])
  let dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    set: function reactiveSetter(newVal) {
      if (newVal === value) return
      // 4.4、Dep派发依赖更新  
      dep.notify(newVal, value)
      value = newVal
    },
    get: function reactiveGetter() {
      // 4.5、闭包Dep收集依赖 Watcher
      dep.depend()
      // 6.5 observe函数 如果传入数据为简单数据类型 就不会返回Observer实例 所以需要判断一下是否有Observer实例,如果有就为Observer实例的Dep也收集一份 依赖
      if (childOb) childOb.dep.depend()
      return value
    }
  })
}
class Dep {
  constructor(option) {
    this.subs = []
  }
  // 4.2、depend方法用来收集订阅者依赖
  depend() {
    // 5.5、如果为Watcher实例初始化
    if (Dep.target) {
      // 5.6、 每个data数据Watcher实例化,都会先设置Dep.target并触发data数据得getter,完成依赖得收集
      this.subs.push(Dep.target)
    }
  }
   ...
}
  • initComputed结束后执行initWatch,在initWatch中new二号watcher.二号watcher在初始化时,先二号watcher挂载到Dep.target上,调用get触发vm.x的getter。
class Vue {
  ...
  // 1.0
  initWatch() {
    const watches = this.$options.watch
    if (watches) {
      const keys = Object.keys(watches)
      for (let index = 0; index < keys.length; index++) {
        // 1.1
        new Watcher(this, keys[index], watches[keys[index]])
      }
    }
  }
  initComputed() {
    const computeds = this.$options.computed
    if (computeds) {
      const keys = Object.keys(computeds)
      for (let index = 0; index < keys.length; index++) {
        const watcher = new Watcher(this, computeds[keys[index]], function () { }, { lazy: true })
        Object.defineProperty(this, keys[index], {
          ...
          // 1.5 二号watcher的get方法 触发了一号watcher的computedGetter
          get: function computedGetter() {
            if (watcher.dirty) {
              watcher.get()
              watcher.dirty = false
            }
            return watcher.value
          }
        })
      }
    }
  }
...
class Watcher {
  constructor(vm, exp, cb, option = {}) {
    this.lazy = this.dirty = !!option.lazy
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    if (!option.lazy) {
      // 1.2 二号watcher初始化 执行get方法
      this.get()
    }
  }
  get() {
    // 1.3 二号watcher 挂载到Dep.target
    Dep.target = this
    if (typeof this.exp === 'function') {
      this.value = this.exp.call(this.vm)
    } else {
      // 1.4 二号watcher 触发一号watcher的getter
      this.value = this.vm[this.exp]
    }
    Dep.target = null
  }
 ...
}
  • 一号watcher调用get方法后,Dep.targety由二号watcher被一号watcehr覆盖。所以最终导致person的dep收集二号watcher失败

解决思路

  • watcher初始化挂载到Dep.target时,我们应该用栈去保存watcher。当有新watcher生成并被收集完成后,将新watcher从栈中弹出,取栈中上一个watcher挂载到Dep.target上。
  • dep收集watcher时,watcher也要收集一下dep.当计算属性getter结束后,查看Dep.target是否有未被收集的watcher.如果有未被收集的watcher,就通知计算属性watcher收集的dep,让它继续收集Dep.target

完整代码(9.0-9.12 为此阶段代码)

class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
    // 8.4 无论是计算属性的初始化还是data的初始化都必须放到watch初始化之前,因为计算属性和data的初始化完成 watch才能侦测到它们。
    this.initComputed()
    this.initWatch()
  }
  initData() {
    let data = this._data
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      Object.defineProperty(this, keys[i], {
        enumerable: true,
        configurable: true,
        set: function proxySetter(newVal) {
          data[keys[i]] = newVal
        },
        get: function proxyGetter() {
          return data[keys[i]]
        },
      })
    }
    observe(data)
  }
  initWatch() {
    const watches = this.$options.watch
    if (watches) {
      const keys = Object.keys(watches)
      for (let index = 0; index < keys.length; index++) {
        new Watcher(this, keys[index], watches[keys[index]])
      }
    }
  }
  // 8.3 对计算属性单独初始化 
  initComputed() {
    const computeds = this.$options.computed
    if (computeds) {
      const keys = Object.keys(computeds)
      for (let index = 0; index < keys.length; index++) {
        // 8.5 第二个参数传入计算属性函数
        // 8.15 计算属性初始化的watcher  需要将其标记为惰性的
        const watcher = new Watcher(this, computeds[keys[index]], function () { }, { lazy: true })
        // 8.6 将该watcher挂载到Vue实例上  
        Object.defineProperty(this, keys[index], {
          enumerable: true,
          configurable: true,
          // 8.7 不允许用户修改计算属性
          set: function computedSetter() {
            console.warn("请不要修改计算属性")
          },
          // 8.8 通过watcher的get方法求值,并将求值结果返回出去
          get: function computedGetter() {
            // 8.9 只有watcher为脏数据时,再重新求值
            if (watcher.dirty) {
              watcher.get()
              // 8.10 求出新值 更新dirty状态  
              watcher.dirty = false
            }
            // 9.12 在计算属性的getter中判断 是否还有watcher需要收集
            if(Dep.target) {
              for (let i = 0; i < watcher.deps.length; i++) {
                // 9.13 将watcher的dep 拿出来继续收集剩余的watcher
                watcher.deps[i].depend()
              }
            }
            return watcher.value
          }
        })
      }
    }
  }
  $watch(key, cb) {
    new Watcher(this, key, cb)
  }
  // 6.6 __ob__的挂载,依赖的收集工作已做完  
  $set(targt, key, value) {
    const oldValue = { ...targt }
    // 6.7 将传入的新属性也变为响应式  
    defineReactive(targt, key, value)
    // 6.8 手动派发依赖更新  
    targt.__ob__.dep.notify(oldValue, targt)
  }
}
function observe(data) {
  const type = Object.prototype.toString.call(data)
  if (type !== '[object Object]' && (type !== '[object Array]')) return
  // 6.3 将Observer实例 return出去,并在defineReactive中接收。
  if (data.__ob__) return data.__ob__
  return new Observer(data)
}
// 2、Observer类:观察者/侦听器,用来观测数据、生成负责处理依赖的Dep实例等复杂逻辑
class Observer {
  constructor(data) {
    // 6.1 为observer实例挂一个Dep实例(事件中心)
    this.dep = new Dep()
    // 7.5 数组不能调用walk,因为walk会通过defineProperty劫持下标会出现依赖回调错乱等问题
    if (Array.isArray(data)) {
      // 7.6 用我们改造好的数组原型覆盖 自身的原型对象
      data.__proto__ = ArrayMethods
      // 7.7 将数组所有子元素变为响应式 
      this.observeArray(data)
    } else {
      // 2.1 将data所有属性 变为响应式  
      this.walk(data)
    }
    // 6.2 将observer实例挂在到不可枚举的属性__ob__上,供外部$set使用 
    Object.defineProperty(data, "__ob__", {
      value: this,
      enumerable: false,
      configurable: true,
      writable: true
    })
  }
  walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      // console.log("definedBeforer",keys[i]);
      defineReactive(data, keys[i], data[keys[i]])
    }
  }
  // 7.8 将传入的数组的所有子元素 变为响应式
  observeArray(arr) {
    for (let i = 0; i < arr.length; i++) {
      observe(arr[i])
    }
  }
}
// 3、defineReactive工具函数:用来递归劫持data,将data数据变为响应式数据
function defineReactive(obj, key, value) {
  // 6.4 接收Observer实例,为属性Dep收集依赖 Watcher
  let childOb = observe(obj[key])
  // 4.0、为每个data数据新建一个Dep实例,并通过闭包维护
  let dep = new Dep()
  // 3.2 对当前data对象的 key 进行数据劫持
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    set: function reactiveSetter(newVal) {
      if (newVal === value) return
      // 4.4、Dep派发依赖更新  
      dep.notify(newVal, value)
      value = newVal
    },
    get: function reactiveGetter() {
      // 4.5、闭包Dep收集依赖 Watcher
      dep.depend()
      // 6.5 observe函数 如果传入数据为简单数据类型 就不会返回Observer实例 所以需要判断一下是否有Observer实例,如果有就为Observer实例的Dep也收集一份 依赖
      if (childOb) childOb.dep.depend()
      return value
    }
  })
}
// 9.1 新增保存depTarget的栈  
let targetStack = []
// 4、Dep类:事件中心,负责收集依赖、通知依赖更新等  
class Dep {
  constructor(option) {
    // 4.1、subs用来保存所有订阅者
    this.subs = []
  }
  // 9.7 watcher收集完dep后,调用dep.addSub来收集watcher
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // 4.2、depend方法用来收集订阅者依赖
  depend() {
    // 5.5、如果为Watcher实例初始化
    if (Dep.target) {
      // 5.6 每个data数据Watcher实例化,都会先设置Dep.target并触发data数据得getter,完成依赖得收集
      // this.subs.push(Dep.target)
      // 9.6 watcher收集dep
      Dep.target.addDep(this)
    }
  }
  // 4.3、notify方法用来派发订阅者更新
  notify(newVal, value) {
    // 5.7、 执行每个订阅者Watcher的run方法完成 更新
    // 8.12 依赖更新派发更新时 先走update判断是否要更新
    this.subs.forEach(watcher => watcher.update(newVal, value))
  }
}
let watcherId = 0
// watcher任务队列
let watcherQueue = []
// 5、Watcher类:订阅者,触发依赖收集、处理回调
class Watcher {
  constructor(vm, exp, cb, option = {}) {
    // 8.13 watcher增加新参数 option ,对watcher进行默认配置
    this.lazy = this.dirty = !!option.lazy
    // 5.1、将Vue实例、data属性名和处理回调 挂载到watcher实例上
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    // 9.8 watcher用来保存收集到的dep
    this.deps = []
    // 8.14 惰性watcher 初始化时不需要收集依赖
    if (!option.lazy) {
      // 5.2、触发data数据的getter 完成依赖收集
      this.get()
    }
  }
  addDep(dep) {
    // 9.9 由于每次9.0求值 watcher可能会收集多次dep 如果已经收集过就终止  
    if (this.deps.indexOf(dep) !== -1) return
    // 9.10 收集dep
    this.deps.push(dep)
    // 9.11 让dep收集watcher
    dep.addSub(this)
  }
  get() {
    // 9.2 在dep收集依赖watcehr时,先添加进栈中
    targetStack.push(this)
    // 5.3、将Watcher实例设为 Dep依赖收集的目标对象
    Dep.target = this
    // 8.1  收集依赖之前先判断是否为函数 计算属性求值时会传入函数  
    if (typeof this.exp === 'function') {
      // 8.2 执行函数 并求出值
      this.value = this.exp.call(this.vm)
    } else {
      // 5.4、触发data数据getter拦截器 对其进行求值
      this.value = this.vm[this.exp]
    }
    // 9.3 求值 收集依赖结束后 让watcher出栈
    targetStack.pop()
    // 9.4 判断栈中 是否有未被收集的watcher
    if (targetStack.length) {
      // 9.5 获取到栈顶的watcher
      Dep.target = targetStack[targetStack.length - 1]
    } else {
      // 清空依赖目标对象
      Dep.target = null
    }
  }
  // 8.11 在调用run之前先调用update,判断是否要直接run
  update(newVal, value) {
    // 8.12 依赖更新当前watcher为惰性时,不要直接run。而是将watcher标记为脏数据,等到用户主动获取结果再去run
    if (this.lazy) {
      this.dirty = true
    } else {
      this.run(newVal, value)
    }
  }
  run(newVal, value) {
    // 5.8 如果该任务已存在与任务队列中 则终止
    if (watcherQueue.indexOf(this.id) !== -1) return
    // 5.9 将当前watcher添加到 队列中
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() => {
      // 9.0 依赖更新,对watcher进行求值 解决计算属性watcher 不被触发的问题
      this.get()
      this.cb.call(this.vm, newVal, value)
      // 5.10 任务执行结束 将其从任务队列中删除  
      watcherQueue.splice(index, 1)
    })
  }
}
// 7.0 获取数组原型对象
const ArrayMethods = {}
ArrayMethods.__proto__ = Array.prototype
// 7.1 声明需要被改造的数组方法 这里举两个例子
const methods = ['push', 'pop']
// 7.2 对数组方法进行改造
methods.forEach(method => {
  ArrayMethods[method] = function (...args) {
    const oldValue = [...this]
    // 7.9 将新插入的数据也变为响应式  
    if (method === 'push') {
      this.__ob__.observeArray(args)
    }
    // 7.3 传入参数执行原本方法
    const result = Array.prototype[method].apply(this, args)
    // 7.4 派发依赖更新 
    this.__ob__.dep.notify(oldValue, this)
    return result
  }
})

测试没有问题

image.png

实现模板的编译

前面我们实现了Vue的响应试系统,现在我们需要在数据发生变化时完成对UI DOM的响应。

先举一个小栗子:

  • 我们在Vue构造器中,new一个watcher,在watcher中,操作DOM绑定vm.name
  • 由于watcehr第二个参数为求值函数,watcher会先执行求值函数,在求值函数中 触发vm.name的getter,vm.name的dep会去收集这个watcher。修改vm.name时,vm.name.dep会通知watcher更新再次执行求值函数
class Vue {
  constructor(options) {
    ...
    this.initWatch()
    // 10.0 简单示例:Vue初始化new一个watcher 通过watcher来更新Html  
    new Watcher(this, () => {
      document.querySelector("#app").innerHTML = `<p>${this.name}</p>`
    }, () => { })
  }
  ...}

测试

修改name属性,DOM更新成功。

image.png

image.png

image.png

Vue中对模板的编译

在Vue中,这个负责更新DOM的watcher被称为render watcher,而它的求值函数远比我们的复杂

我们存在的问题

  • 用户可以在模板中使用模板语法、Vue指令等,我需要先对模板进行处理,最终转换成更新DOM的函数
  • 直接更新DOM的开销很大,我们要实现DOM的按需更新

虚拟DOM

虚拟DOM是真实DOM的抽象层,通过它可以减少不必要的DOM操作实现跨平台性等特性。Vue 引入了Virtual DOM(VDOM).
什么是虚拟DOM?简单来说,他就是js对象他描述了当前DOM长什么样。
每个Vue实例都一个渲染函数vm.$opstions.render,实例可以通过它来生成VDOM。 Vue实例如果传入DOM或者template,首先会把模板字符串转换成渲染函数,这个过程就是编译。

解析器(parser)

Vue的编译流程大概分为三步

  • 第一步将模板字符串转换成Element ASTs,这个过程是通过解析器实现。
  • 第二步Element ASTs对静态节点进行标记,主要用来对虚拟DOM渲染优化,这个过程通过优化器实现
  • 第三步将Element ASTs转换为render函数函数体,这个过程是通过代码生成器实现

5.png AST

AST:抽象语法树,它是一种代码转换为另外一种代码的时,对源代码的描述。

Vue中的AST

type代表节点的类型,当type为2时,代表节点使用了变量,expression会记录使用了什么变量,text记录文本节点编译前的字符串

{
  children: [{…}],
  parent: {},
  tag: "div",
  type: 1, //1-元素节点 2-带变量的文本节点 3-纯文本节点,
  expression:'_s(name)', //type如果是2,则返回_s(变量)
  text:'{{name}}' //文本节点编译前的字符串
}

生成ast一般会经过两个阶段:词法分析、语法分析

const a = 1

词法分析

解析代码中关键字例如 consta=1,将它们转换为token.

语法分析

将token进行处理和组合 最终生成ast
在Vue中,解析出token后立即对它进行处理。

对元素节点的parse

这里只以简单的HTML模板为例,不对v-ifv-showv-for单标签注释等复杂情况进行处理。
解析元素节点,我们可以以<为标识,它可以代表开始标签或结束标签。如果为开始标签,我们就在ast树的层级上加上一层。如果为结束标签,就回退到ast树层级的上次一层。同时每一层要记录它的父级元素。
同时还需要一个栈 记录当前元素处于哪个层级,有开始标签就推这个元素入栈,结束标签就出栈。当为文本节点是,不对栈进行处理.

示例

// 对HTML模板字符串进行解析 最终得到元素树抽象语法树(ElementASTs)
/**
* {
*    children: [{…}],
*    parent: {},
*    tag: "div",
*    type: 1, //1-元素节点 2-带变量的文本节点 3-纯文本节点,
*    expression:'_s(name)', //type如果是2,则返回_s(变量)
*    text:'{{name}}' //文本节点编译前的字符串
*  }
*/
function parser(html) {
  // 层级栈:记录当前元素的层级
  let stack = []
  // 根元素节点
  let root = null
  // 当前元素的父元素节点  
  let currentParent = null
  // 1.0 不断对模板字符串解析
  while (html) {
    let index = html.indexOf("<")
    // 2.1 如果元素之前有文本节点   例: html = "{{name}}<div>1</div></root>"
    if (index > 0) {
      // 2.2 截取标签前文字部分
      let text = html.slice(0, index)
      // 2.3 将文字节点推进父元素的children中
      currentParent.children.push(element)
      // 2.4 截掉已经处理完的部分
      html = html.slice(index)
      // 1.0 如果为开始标签  例: html = "<root>{{name}}<div>1</div></root>"
    } else if (html[index + 1] !== '/') {
      // 1.1 获取元素类型
      let gtIndex = html.indexOf(">")
      let eleType = html.slice(index + 1, gtIndex).trim()
      // 1.2 如果标签内存在属性  截掉标签属性部分  例: eleType = 'div id="app"' 处理后:eleType = 'div'
      let emptyIndex = eleType.indexOf(" ")
      let attrs = {}
      if (emptyIndex !== -1) {
        // 1.3 获取元素标签属性
        attrs = parseAttr(eleType.slice(emptyIndex + 1))
        eleType = eleType.slice(0, emptyIndex)
      }
      // 1.4 新建AST节点  
      const element = {
        children: [],
        attrs,
        parent: currentParent,
        tag: eleType,
        type: 1
      }
      // 1.5 没有根元素节点
      if (!root) {
        root = element
      } else {
        // 1.6 将当前元素节点推进父元素的children中 
        currentParent.children.push(element)
      }
      // 1.7 解析到元素开始标签 推元素进层级栈
      stack.push(element)
      // 1.8 更新当前父级元素
      currentParent = element
      // 1.9 截掉已经处理完的部分
      html = html.slice(gtIndex + 1)
      // 3.0 为结束标签  例: html = "</div></root>"
    } else {
      let gtIndex = html.indexOf(">")
      // 3.1 解析到元素的结束标签 层级栈退一个
      stack.pop()
      // 3.2 更新当前父级元素
      currentParent = stack[stack.length - 1]
      // 3.3 截掉已经处理完的部分
      html = html.slice(gtIndex + 1)
    }
  }
  return root
}
// 解析标签属性  
function parseAttr(eleAttrs) {
  let attrs = {}
  attrString = eleAttrs.split(" ")
  attrString.forEach(e => {
    if (e && e.indexOf("=") !== -1) {
      const attrsArr = e.split("=")
      attrs[attrsArr[0]] = attrsArr[1]
    } else {
      attrs[e] = true
    }
  });
  return attrs
}

测试:没有问题

<body>
  <div id="app">{{name}}<p>第一个P标签</p>
    <p>第二个P标签 <i>这i标签</i></p>
  </div>
  <script src="./parse.js"></script>
  <script>
    const ast = parser(document.getElementById("app").outerHTML)
    console.log(ast);
  </script>
</body>

1624006082(1).jpg

对文本节点的parse

前面在对元素节点的parse中,文字节点整个抽离出来并没有parse.这里单独对它处理一下。
这里我们以{{}}为标识符,把文本中的差值表达式转换为_s(name)的形式

function parser(html) {
  ...
  while (html) {
    ...
    if (index > 0) {
      // 2.2 截取标签前文字部分
      let text = html.slice(0, index)
      // 5.4 调用parseText工具函数解析文本
      let element = parseText(text)
      // 5.5 文本节点增加 父节点属性
      element.parent = currentParent
      // 2.3 将文字节点推进父元素的children中
      currentParent.children.push(element)
      ...
    } else if (html[index + 1] !== '/') {
      ...
    }
  }
  return root
}
//解析文本节点
function parseText(text) {
  // 未解析的文本
  let originText = text
  // 有可能是纯文本或者带变量的文本  默认:纯文本
  let type = 3
  // 节点碎片 元素节点的文本节点可能是多段组成的 
  // 例:<p>我的 {{name}},我的 {{age}}</p>   token=['我的',{{name}},',我的',{{age}}]
  let token = []
  while (text) {
    let start = text.indexOf("{{")
    let end = text.indexOf("}}")
    //4.0 如果存在插值表达式
    if (start !== -1 && end !== -1) {
      // 4.1 将文本节点类型标记为 带变量的文本
      type = 2
      // 4.2 插值表达式前存在纯文本
      if (start > 0) {
        // 4.3 将插值表达式 前纯文本 推进token
        token.push(JSON.stringify(text.slice(0, start)))
      }
      // 4.4 获取插值表达式内的 表达式  
      let exp = text.slice(start + 2, end)
      // 4.5 解析表达式 并推进token  
      token.push(`_s(${exp})`)
      // 4.6 截掉已经处理完的部分
      text = text.slice(end + 2)
      // 5.0 不存在插值表达式
    } else {
      // 5.1 终止解析text 直接推进token
      token.push(JSON.stringify(text))
      text = ''
    }
  }
  let element = {
    text: originText,
    type
  }
  // 5.3 如果type为2带有变量  文本节点需要expression
  if (type === 2) {
    element.expression = token.join("+")
  }
  return element
}

测试

<body>
  <div id="app">我的{{name}},我今年{{age}}<p>第一个P标签</p>
    <p>第二个P标签 <i>这i标签</i></p>
  </div>
  <script src="./parse.js"></script>
  <script>
    const ast = parser(document.getElementById("app").outerHTML)
    console.log(ast);
  </script>
</body>

image.png

代码生成器

前面我们通过解析器parser将模板字符串(<div>...</div>)转换成抽象语法树ASTs.
现在我们需要通过代码生成器codegen将抽象语法树转换成渲染函数render

查看Vue源码中的渲染函数

<body>
  <div>123
    <p>{{name}}</p>
  </div>
  <script crossorigin="anonymous"
    integrity="sha512-pSyYzOKCLD2xoGM1GwkeHbdXgMRVsSqQaaUoHskx/HF09POwvow2VfVEdARIYwdeFLbu+2FCOTRYuiyeGxXkEg=="
    src="https://lib.baomitu.com/vue/2.6.14/vue.js"></script>
  <script>
    let vm = new Vue({
      el: "div",
      data: { name: "张三" }
    })
    console.log("Vue", vm.$options.render)
  </script>
</body>

image.png

function anonymous() {
   `with(this){
      return _c('div',[_v("123\n    "),_c('p',[_v(_s(name))])])
    }`
}
// 插入this后,改变函数体中this的执行,函数中任何变量或函数,都会被当做this的属性或方法 最终函数等同于  
// `with(this){
//  return this._c('div',[this._v("123\n    "),this._c('p',[this._v(this._s(this.name))])])
//}`

渲染函数 函数体

_c:用来转换虚拟DOM的元素节点

  • 参数1(字符串):元素节点的标签名
  • 参数2(数组):可选,元素节点的子节点
    _v:用来转换纯文本节点
    _s:用来获取文本中变量的值

将AST转换为渲染函数思路

  • 递归AST节点,遇到元素节点就生成如下格式字符串 _c( 标签名 , 标签属性对象 , 后代数组)
  • 遇到文本节点 如果是纯文本节点就生成字符串_v(文本字符串)
  • 遇到带有变量的节点 就_v(_s(变量名))
  • 在函数体外面包一个with(this)方法,传入上下文。 新建codegen.js
// 将AST转换为渲染函数函数体
/**{
    children: [{ … }],
    parent: { },
    tag: "div",
    type: 1, //1-元素节点 2-带变量的文本节点 3-纯文本节点,
    expression: '_s(name)', //type如果是2,则返回_s(变量)
    text: '{{name}}' //文本节点编译前的字符串
} */
function codegen(ast) {
  // 1.0 ast第一层一定是个元素节点
  let code = genElement(ast)
  return {
     // 1.1 渲染函数执行时,传入this改变函数体内this指向。
    render: `with(this){return ${code}}`
  }
}
// 转换元素节点
function genElement(el) {
  // 2.1 获取子节点 
  let children = genChildren(el)
  // 2.0 返回_c(标签名,标签属性对象,标签子节点数组)
   return `_c(${JSON.stringify(el.tag)}, ${JSON.stringify(el.attrs)}, ${children})`
}
// 转换文本节点
function genText(node) {
  // 5.0 带有变量的文本节点
  if (node.type === 2) {
    // node.expression 任何变量都会通过this.[node.expression] 进行求值 !!!!
    return `_v(${node.expression})`
  }
  // 5.1 纯文本节点
  return `_v(${JSON.stringify(node.text)})`
}
// 判断类型 转移对应节点 
function genNode(node) {
  // 4.0 判断节点类型
  if (node.type === 1) {
    return genElement(node)
  } else {
    return genText(node)
  }
}
// 转换子节点
function genChildren(node) {
  // 3.0 判断是否存在子节点
  if (node.children && node.children.length > 0) {
    // 3.1 转换所有子节点 [ 子节点1,子节点2,...],递归转换所有子节点  genNode--genElement--genChildren--genNode
    return `[${node.children.map(node => genNode(node))}]`
  }
}

测试:引入前面完成的解析器,解析完成的ast通过代码生成器生成 渲染函数体

<body>
  <div>标签前<p>
      我的{{name}}
    </p> 标签后</div>
  <!-- <div id="app">1{{name}}这是其他内容{{age}}</div> -->
  <script src="./parse.js"></script>
  <script src="./codegen.js"></script>
  <script>
    const ast = parser(document.querySelector("div").outerHTML)
    const render = codegen(ast)
    console.log(render);
  </script>
</body>
</html>

image.png

在源码中配置解析器和代码生成器

class Vue {
  constructor(options) {
    this.$options = options
    ......
    // 10.0 简单示例:Vue初始化new一个watcher 通过watcher来更新Html  
    // new Watcher(this, () => {
    //   document.querySelector("#app").innerHTML = `<p>${this.name}</p>`
    // }, () => { })
    //  10.0 使用解析器和代码生成器 生成渲染函数  
       if (this.$options.el) {
      // 10.1 获取模板字符串
      let html = document.querySelector("div").outerHTML
      // 10.2 生成抽象语法树
      let ast = parser(html)
      // 10.3 生成渲染函数函数体
      let funCode = codegen(ast).render
      // 10.4 生成渲染函数并挂载到Vue实例上
      this.$options.render = new Function(funCode)
    }
    ......
  }

测试
新建index.html,分别引入parse.js、codegen.js、index.js

<body>
  <div>标签前<p>
      我的{{name}}</p>
  </div>
  <script src="./parse.js"></script>
  <script src="./codegen.js"></script>
  <script src="./index.js"></script>
  <script>
    let vm = new Vue({
      el: "div",
      data: {
        name: '哈哈',
      },
    })
    console.log("vm", vm.$options.render);
  </script>
</body>

image.png

实现VDOM(虚拟DOM)

VDOM就是一个JS对象,它用来描述当前的DOM。

例如

<ul>
 <li>1</li>
 <li>2</li>
</ul>
// 对应的VDOM
{
  tag:"ul",
  attrs:{},
  children:[
    {
      tag:"li",
      attrs:{},
      chilren:[
        {
          tag:null,
          attrs:{},
          children:[],
          text:"1"
        }
      ]
    },
     {
      tag:"li",
      attrs:{},
      chilren:[
        {
          tag:null,
          attrs:{},
          children:[],
          text:"2"
        }
      ]
    }
  ]
}

VDOM的作用

1、他在大多数情况下,提供了比暴力刷新整个DOM树更好的性能。

操作js对象很快,但是操作DOM元素却很慢。如果数据发生变化,我们不可能直接根据模板字符串重新生成DOM塞入页面中,这显然非常浪费时间。我们可以用VDOM去描述视图,当数据发生变化时重新VDOM树。通过对比两颗VDOM树,找到发生变化的元素指定的更新DOM节点。

2、VDOM具有天然的跨平台性,它只需要调用对应平台的DOM API。就可以生成、更新对应平台的视图。

由渲染函数生成VDOM

这个阶段 我们通过VNode抽象类来实现

// Vue抽象类中 增加_c、_v、_s等方法  10.X - 13.X
class Vue {
 constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
    // 8.4 无论是计算属性的初始化还是data的初始化都必须放到watch初始化之前,因为计算属性和data的初始化完成 watch才能侦测到它们。
    this.initComputed()
    this.initWatch()
    // 10.0 使用解析器和代码生成器 生成渲染函数  
    if (this.$options.el) {
      // 10.1 获取模板字符串
      let html = document.querySelector("div").outerHTML
      // 10.2 生成抽象语法树
      let ast = parser(html)
      // 10.3 生成渲染函数函数体
      let funCode = codegen(ast).render
      // 10.4 生成渲染函数并挂载到Vue实例上
      this.$options.render = new Function(funCode)
    }
  }
  // 11.0 生成元素节点
  _c(tag, attrs, children, text) {
    return new VNode(tag, attrs, children, text)
  }
  // 12.0 生成纯文本节点
  _v(text) {
    return new VNode(null, null, null, text)
  }
  // 13.0 获取变量内容
  _s(val) {
    console.log("_s", val);
    // 13.1 如果值为空就返回空字符串
    if (val === null || val === undefined) {
      return ''
      // 13.2 如果为对象
    } else if (typeof val === 'object') {
      return JSON.stringify(val)
      // 13.3 如果为数字或字符串
    } else {
      return val
    }
  }
}
// 新建VNode抽象类
class VNode {
  constructor(tag, attrs, children, text) {
    this.tag = tag
    this.attrs = attrs
    this.children = children
    this.text = text
  }
}

测试:成功获取虚拟DOM

image.png

image.png

VDOM的diff和patch

VDOM之所以高效,是因为它可以通过diff算法比较出两颗VDOM的节点不同,然后通过patch更新指定的DOM节点。在实现diff和patch之前,我们需要实现一个将VDOM转换成真实DOM的createEle方法,后面DOM更新会用到它。

createEle

createEle函数 将VNode及子节点 转换为真实DOM

// 15.0  生成真实DOM 
function createEle(vnode) {
  // 15.1 为文字节点时
  if (!vnode.tag) {
    const el = document.createTextNode(vnode.text)
    // 15.2 将节点保存起来
    vnode.ele = el
    return el
  }
  // 15.3 为元素节点时 
  const el = document.createElement(vnode.tag)
  vnode.ele = el
  // 15.4 将子节点也转换成真实DOM 并插入到父节点中 
  vnode.children.map(createEle).forEach(e => {
    el.appendChild(e)
  })
  return el
}

测试

image.png

响应式更新视图

前面在Vue初始化时,new Watcher 通过这个watcher的求值函数去收集依赖(this.name),当依赖发生变化时,这个求值函数重新进行求值并完成视图的更新。

// 10.0 之前的示例:Vue初始化new一个watcher 通过watcher来更新Html  
  new Watcher(this, () => {
    document.querySelector("#app").innerHTML = `<p>${this.name}</p>`
  }, () => { })

依赖数据变化时,触发求值函数立刻更新视图。这种暴力更新视图的方式显然是有性能问题的,我们需要引入虚拟DOM来完成性能的优化。

思路

  • 首先实现一个$mount函数,初次挂载到真实DOM时调用,在原来render watcehr的逻辑放到$mount中。
  • 实现_update函数,该函数接收新的vdom,然后对比新旧vdom并更新真实DOM。render watcehr的逻辑不再是暴力更新视图而是 调用_update函数
// 16.x - 17.X: Vue初始化--$mount--new Watcher--_update--patch  
class Vue {
  constructor(options) {
    ...
    this.initWatch()
    // 10.0 使用解析器和代码生成器 生成渲染函数  
    if (this.$options.el) {
      // 10.1 获取模板字符串
      let html = document.querySelector("div").outerHTML
      // 10.2 生成抽象语法树
      let ast = parser(html)
      // 10.3 生成渲染函数函数体
      let funCode = codegen(ast).render
      // 10.4 生成渲染函数并挂载到Vue实例上
      this.$options.render = new Function(funCode)
      // 16.0 调用$mount 更新视图
      this.$mount(this.$options.el)
    }
  }
  $mount(el) {
    // 16.1 将容器根节点挂载到Vue实例上
    this.$el = document.querySelector(el)
    // 16.2 新建render watcher
    this._watcher = new Watcher(this, () => {
      // 16.3 生成虚拟DOM
      const vnode = this.$options.render.call(this)
      // 16.4 调用_update,更新视图
      this._update(vnode)
    }, () => { })
  }
  _update(vnode) {
    //17.0 有上次vnode时
    if (this._vnode) {
      // 17.1 调用patch 并传入上次vnode和此次vnode
      patch(this._vnode, vnode)
    } else {
      // 17.2 第一次挂载Vue实例时 传入真实DOM节点
      patch(this.$el, vnode)
    }
    // 17.3 保存此次vnode
    this._vnode = vnode
  }
  ......
 }

实现patch

patch是vdom机制中最核心的环节,Vue中vdom进行patch的逻辑是通过snabbdomjs库来实现的。这里主要实现思想 并不不考虑节点属性和key等复杂的情况。

思路

  • patch函数接收两个参数 分别为旧的vdom和新的vdom
  • 当第一次挂载时,patch旧vdom传入的为真实DOM,这个需要单独处理
  • 后续的更新,会分为这几种情况
    • 新节点不存在,则删除对应的DOM
    • 新旧节点标签类型不同或者文本不一样,就调用createEle生成新DOM并替换旧DOM
    • 旧节点不存在,新节点存在。则调用createEle生成新DOM,并在原DOM节点后添加新DOM
    • 遍历子节点递归执行以上逻辑
// 此阶段代码:18.X  
// 18.6 判断新旧节点是否发生变化
function changed(oldNode, newNode) {
  return oldNode.tag !== newNode.tag || oldNode.text !== newNode.text
}
function patch(oldNode, newNode) {
  const isRealyElement = oldNode.nodeType
  // 18.0 当oldNode=this.$el  为元素节点  页面第一次挂载时
  if (isRealyElement) {
    let parent = oldNode.parentNode
    // 18.1 将vue容器节点替换为 vdom生成的新节点
    parent.replaceChild(createEle(newNode), oldNode)
    return
  }
  // 18.2 获取当前vdom的真实dom  上次patch 会在newNode上挂载ele 
  let el = oldNode.ele
  // 18.3 新vdom节点存在 将DOM挂载到vdom.ele上,下次patch 会使用ele
  if (newNode) {
    newNode.ele = el
  }
  let parent = el.parentNode
  // 18.4 新vdom节点不存在,就删除掉DOM中对应的节点
  if (!newNode) {
    parent.removeChild(el)
    // 18.5 新旧节点标签类型或文本不一致时
  } else if (changed(oldNode, newNode)) {
    // 18.7 调用createEle生成新DOM节点替换旧DOM节点  
    parent.replaceChild(createEle(newNode), el)
    // 18.8 对比子节点
  } else if (newNode.children) {
    let newLength = newNode.children.length
    let oldLength = oldNode.children.length
    // 18.9 遍历新旧vdom节点的所有子节点
    for (let index = 0; index < newLength || index < oldLength; index++) {
      // 18.10 子节点旧vdom不存在,调用createEle生成DOM插入到父节点el中
      if (index > oldLength) {
        el.appendChild(createEle(newNode.children[index]))
      } else {
        // 18.11 其余情况的子节点对比 通过调用 patch实现  
        patch(oldNode.children[index], newNode.children[index])
      }
    }
  }
}

测试

我们可以看到修改 修改视图绑定name值后,视图会发生更新。

image.png

image.png

到此Vue的数据响应式系统、虚拟DOM、DOM定向更新基本已经完成。由于本篇文章字数的限制,我会在另外一篇文章《详解 Vue 2.X核心源码,手撸一个简易版Vue框架(下篇》进行流程总结以及全部代码(带注释)展示