vue2.0 双向绑定原理

375 阅读6分钟

初始化项目

// index.html
<!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"></div>
  <script type="module" src="./index.js"></script>
</body>
</html>
// index.js
import MyVue from './src/index.js'
new MyVue({
  el: '#app',
  data() {
    return {
      name: 'MyVue name'
    }
  },
  // 验证数据效果
  resultCb() {
    console.log(this._data.name)
  }
})
// .src/index.js
import { initState } from './state.js'
function MyVue(options) {
  this._init(options)
}

MyVue.prototype._init = function(op) {
  const vm = this
  vm.$options = op
  initState(vm)
}

export default MyVue

获取data

data在组件内必须是function,并且function内可以通过this访问methods等,但是data在new Vue的时候也可以是个普通对象,所以在取值的时候我们需要区分这2种情况

// ./src/state.js
export function initState(vm) {
  const opts = vm.$options
  if(opts.data) initData(vm, opts.data)
}

function initData(vm, data) {
  data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
  vm.$options.resultCb.call(vm)
}
// 'MyVue name'

代理

此时我们想获取data中的数据,需要通过this._data.xxx获取,显然这么写非常麻烦,最理想状态肯定是this.xxx 代替this._data.xxx获取数据,所以我们可以通过代理(Object.defineProperty)的方式解决,修改initData

// ./src/state.js
function initData(vm, data) {
  data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
  // 遍历data的所有属性,给每个属性添加代理
  for(let key in data) {
    proxy(vm, '_data', key)
  }
  vm.$options.resultCb.call(vm)
}

function proxy(target, source, key) {
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get() {
      // this = target
      return this[source][key]
    },
    set(val) {
      this[source][key] = val
    }
  })
}

然后我们修改一下resultCb的获取data方式

// index.js
import MyVue from './src/index.js'
new MyVue({
  el: '#app',
  data() {
    return {
      name: 11
    }
  },
  // 验证数据效果
  resultCb() {
    console.log(this.name) 
  }
})
// 'MyVue name'

观察者模式

在说原理之前我们先说一下观察者模式和发布订阅模式。 能够直接接触到被观察的对象。减少了模块间的耦合问题,两个分离的、毫不相关的模块也能进行通信,但并未完全解耦,被观察者必须去维护一套观察者的集合,观察者必须实现统一的方法供被观察者调用,两者相互联系,用一个简化版本demo实现一下观察者模式

// 被观察者列表
class Sub{
    arr = []
    add(target){
       this.arr.push(target)
    }
    depend(){
        const w = new Watcher()
        w.add(this)
    }
    notify(){
        this.arr.forEach(w => w.update())
    }
}
let wid = 1
// 观察者
class Watcher{
    constructor(){
        this.id = wid++
    }
  	// 关联
    add(target){
        target.add(this)
    }
  	// 统一更新方法
    update(){
        // do something
        console.log(this.id)
    }
}
var sub = new Sub()
sub.depend()
sub.depend()
sub.notify()

无标题-2021-11-29-1953.png

对象劫持

首先我们需要监听data中的取值和赋值,理所当然的我们会想到Object.defineProperty的get和set,Object.defineProperty只能监听单个属性,所以我们需要递归遍历每个属性

// ./src/state.js
import { observe } from './observe.js'
function initData(vm, data) {
  data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
  // 遍历data的所有属性,给每个属性添加代理
  for(let key in data) {
    proxy(vm, '_data', key)
  }
  // 劫持
  observe(data, vm)
  vm.$options.resultCb.call(vm)
}

为了更好的log劫持后的数据,我把resultCb的执行放到了劫持之后,然后我们看看observe的实现

// ./src/observe.js
export function observe(data, vm) {
  // 是对象或者数组就处理,为了递归拦截不是对象或者数组的值
  if (isObjectAndArray(data)) {
    new Observer(data)
  }
}

Observer内是为了处理对象和数组的差异,这里只实现对象的处理。其实就是遍历对象属性,然后给每一个属性添加get set

// ./src/observe.js
class Observer {
  constructor(data) {
    if (Array.isArray(data)) {
      this.observeArray(data)
    } else {
      this.walk(data)
    }
  }
  // 处理对象
  walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i])
    }
  }
  // 处理数组
  observeArray() {

  }
}

function defineReactive(data, key) {
  let value = data[key]
  // 递归处理:{a: {b: 3}}
  observe(value)
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get() {
      console.log(value, 'get')
      return value
    },
    set(val) {
      if(val === value) return 
      console.log(val, 'set')
      value = val
    }
  })
}

然后我们修改一下resultCb的回调

// index.js
import MyVue from './src/index.js'
new MyVue({
  el: '#app',
  data() {
    return {
      name: 'MyVue name'
    }
  },
  // 验证数据效果
  resultCb() {
    console.log(this.name)
    setTimeout(() => {
      this.name = 'change'
      console.log(this.name)
    }, 1000);
  }
})
/*
MyVue name get
MyVue name
change set
change get
change
*/

get执行,set也执行,值也没毛病,所以接下我们实现Dep和Watcher,我们先来看一张图 无标题-2021-11-29-1953.png 解释一下这张图,我们在初始化的时候劫持了所有的数据,所以在编译器阶段,我们会读取到某些数据,然后走到Observer,这时Dep去收集当前的Watcher(编译器Watcher),Watcher也会保存当前的Dep(去重,收集依赖),然后当值改变的时候,会触发收集到的Watcher(编译器Watcher)执行update,重新更新 然后我们先来熟悉一下几个关键词

  • Dep:被观察者,收集当前key对应的所有Watcher
  • Watcher:观察者,收集关联的Dep,执行更新,于Dep是多对多的关系
  • Dep.target:保存进行中的Watcher

首先我们先看看如何维护Dep.target,其实就是维护一个栈,push和pop target。

// ./src/dep.js
// 进行中的Watcher
Dep.target = null
// 栈
const targetStack = []

export function pushTarget(target) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

然后修改一下defineReactive,给每个key创建一个Dep,用来收集Watcher

// ./src/observe.js
import { Dep } from './dep.js'
function defineReactive(data, key) {
  let value = data[key]
  // 创建dep
  const dep = new Dep()
  // 递归处理:{a: {b: 3}}
  observe(value)
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get() {
      // 存在进行中的Watcher
      if(Dep.target) {
        // 触发收集
        dep.depend()
      }
      return value
    },
    set(val) {
      if(val === value) return 
      value = val
      // 值修改触发Watcher更新
      dep.notify()
    }
  })
}

然后我们来看看Dep如何实现,其实和上文中观察者模式中的Sub实现基本一摸一样,只不过增加了id和修改了Watcher的创建方式

// ./src/dep.js
let uid = 1
export class Dep{
  constructor() {
    // 唯一id,用来去重
    this.id = uid++
    // watcher依赖
    this.subs = []
  }
  // Watcher添加dep
  depend() {
    if (Dep.target) {
      Dep.target.addSub(this)
    }
  }
  // 执行收集到的Watcher的update触发更新
  notify() {
    this.subs.forEach(w => w.update())
  }
  // 添加Watcher
  addSub(w) {
    this.subs.push(w)
  }
}

接下来实现Watcher,其实就是一个立即执行回调的构造函数,然后执行前pushTarget,执行完成后popTarget

// ./src/watcher.js
import { popTarget, pushTarget } from "./dep.js"

let uid = 1
export class Watcher{
  constructor(vm, expOrFn) {
    // 唯一id
    this.id = uid++
    // dep id集合
    this.depIds = new Set()
    // dep
    this.deps = []
    // 回调
    this.getter = expOrFn
    this.get()
  }
  get() {
    // Dep.target等于当前Watcher
    pushTarget(this)
    // 执行回调
    this.getter()
    // 交还Dep.target
    popTarget()
  }
  // 更新就是重新执行回调
  update() {
    this.get()
  }
  // watcher保存dep,dep添加watcher
  addSub(dep) {
    const id = dep.id
    // 去重
    if(!this.depIds.has(id)) {
      this.depIds.add(id)
      this.deps.push(dep)
      // dep添加当前watcher
      dep.addSub(this)
    }
  }
}

然后我们在Observer之后用Watcher模拟一下compiler,接着修改initData

// .src/state.js
function initData(vm, data) {
  data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
  // 遍历data的所有属性,给每个属性添加代理
  for(let key in data) {
    // if(methods && methods.hasOwnProperty(key)) {
    //   console.log(`data ${key}和methods函数重名了`)
    // }
    proxy(vm, '_data', key)
  }
  // 劫持
  observe(data, vm)
  // 模拟compiler,执行回调
  new Watcher(vm, () => {vm.$options.resultCb.call(vm)})
}

接下来修改一下resultCb

// index.js
import MyVue from './src/index.js'
new MyVue({
  el: '#app',
  data() {
    return {
      name: 'MyVue name'
    }
  },
  resultCb() {
    // 获取name,1s后修改name的值,重新触发resultCb
    console.log(this.name)
    setTimeout(() => {
      this.name = 'change'
      // console.log(this.name)
    }, 1000);
  }
})
/*
MyVue name
change 1s后输出
*/

最后我们看看数组的劫持

数组劫持

数组劫持就没法使用Object.defineProperty了,我们可以劫持数组提供的几个会改变自身的方法(push pop unshift shift splice sort reserve),实现劫持的功能,来实现,打个比方

var obj = {
	names: ['zm']
}
obj.names.push('yg')

那么我们是不是只要知道这个数组是不是发生了push就可以了,那么我们就可以使用AOP(在执行原有代码的基础之上再拓展所需要的功能),着手开始实现切片的代码。 首先我们先完善数组的处理

// ./src/observe.js
class Observer {
  constructor(data) {
    if (Array.isArray(data)) {
      this.observeArray(data)
    } else {
      this.walk(data)
    }
  }
  // 处理对象
  walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i])
    }
  }
  // 处理数组
  observeArray(data) {
    // 循环数组每一项,递归处理
    data.forEach(i => observe(i))
  }
}

然后我们按上面说的,我们需要处理['zm']的原型,也就是constructor里的data,达到监听push的目的

// ./src/observe.js
import { arrayMethods } from './array.js'
class Observer {
  constructor(data) {
    if (Array.isArray(data)) {
      // 修改数据原型
      data.__proto__ = arrayMethods
      this.observeArray(data)
    } else {
      this.walk(data)
    }
  }
  // 处理对象
  walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i])
    }
  }
  // 处理数组
  observeArray(data) {
    // 循环数组每一项,递归处理
    data.forEach(i => observe(i))
  }
}

接着看一下arrayMethods的实现

// 数组的原型
const arrayProto = Array.prototype
// 继承数组原型
// 原型式继承
// function create(target) {
//   function f() {}
//   f.prototype = target
//   return new f()
// }
export const arrayMethods = Object.create(arrayProto)

// 会修改原数据的数组方法
const list = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

list.forEach(method => {
  // 原数组方法
  const original = arrayProto[method]
  Object.defineProperty(arrayMethods, method, {
    enumerable: true,
    configurable: true,
    value(...args) {
      // 原型数组方法取值
      const value = original.apply(this, args)
      // 返回值
      return value
    }
  })
}) 

到这一步我们就可以监听到push等这种方法,而且正常返回了值。 ​

想一个问题,如果新加入的数据是对象,对象内某个属性修改了怎么办? 答案是执行Watcher update更新啊。 那么执行Watcher update的前提是经过Observer劫持,所以我们需要先劫持新增的数据,然后新增的数据也是数组,我们就需要经过Observer.observeArray处理,但是如何获取Observer.observeArray方法呢? ​

给data添加不可枚举的Observer啊,而且这么做还有一点好处是可以证明这个数据经过了响应式处理!所以再次修改Observer方法

// ./src/observe.js
import { arrayMethods } from './array.js'
class Observer {
  constructor(data) {
    // 数组dep
    this.dep = new Dep()
    // 给data添加不可枚举的__ob__属性,标记为响应式
    insetOb(data, this)
    if (Array.isArray(data)) {
      // 修改数据原型
      data.__proto__ = arrayMethods
      this.observeArray(data)
    } else {
      this.walk(data)
    }
  }
  // 处理对象
  walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i])
    }
  }
  // 处理数组
  observeArray(data) {
    // 循环数组每一项,递归处理
    data.forEach(i => observe(i))
  }
  insetOb(data, value) {
    Object.defineProperty(data, '__ob__', {
      configurable: true,
      enumerable: false,
      value
    })
  }
}

还有一点当前的数组数据更新了,我们也需要触发当前数组的Dep更新,所以我们完善一下方法



// 数组的原型
const arrayProto = Array.prototype
// 继承数组原型
// 原型式继承
// function create(target) {
//   function f() {}
//   f.prototype = target
//   return new f()
// }
export const arrayMethods = Object.create(arrayProto)

// 会修改原数据的数组方法
const list = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

list.forEach(method => {
  // 原数组方法
  const original = arrayProto[method]
  Object.defineProperty(arrayMethods, method, {
    enumerable: true,
    configurable: true,
    value(...args) {
      // 原型数组方法取值
      const value = original.apply(this, args)
      // Observer,this代表数据本身,比如{a:[1,2]},this代表a,__ob__在Observer中添加过,代表响应式
      const ob = this.__ob__
      let inset
      // 截取新的数据
      switch(method) {
        case 'push':
        case 'unshift':
          inset = args
          break;
        case 'splice':
          inset = args.slice(2)
          break;
      }
      // 新数据响应式处理
      if(inset) ob.observeArray(inset)
      // 触发更新
      ob.dep.notify()
      // 返回值
      return value
    }
  })
}) 

接下来实现一个数组的Watcher是如何收集的

{
	ages: [1, [2], [3, [4, 5]]]
}

如果我们执行了ages.push(8),那么是不是可以理解为ages变了。所以说,数组里的所有Watcher其实和ages这个key的Watcher完全是一致的,我们只需要不断的重写每一层数组,和收集每一层数据对应的的Watcher,得到类似这种映射关系

ages -> 渲染Watcher
[1, [2], [3, [4, 5]]] -> 重写,dep保存渲染Watcher
[2] -> 重写,dep保存渲染Watcher
[3, [4, 5]] -> 重写,dep保存渲染Watcher
[4, 5] -> 重写,dep保存渲染Watcher

所以这时我们只需要不断重写,不断收集key的Watcher即可

import { Dep } from './dep.js'
import { arrayMethods } from './array.js'
export function observe(data, vm) {
  // 是对象或者数组就处理
  if (!isObjectAndArray(data)) return 
  // 如果存在__ob__属性,就证明已经是响应式,直接返回实例就可以
  let ob
  if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observer) {
    ob = data.__ob__
  } else {
    ob = new Observer(data)
  }
  return ob
}

class Observer {
  constructor(data) {
    // 数组dep
    this.dep = new Dep()
    // 给data添加不可枚举的__ob__属性,标记为响应式
    this.insetOb(data, this)
    if (Array.isArray(data)) {
      // 修改数据原型
      data.__proto__ = arrayMethods
      this.observeArray(data)
    } else {
      this.walk(data)
    }
  }
  // 处理对象
  walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i])
    }
  }
  // 处理数组
  observeArray(data) {
    // 循环数组每一项,递归处理
    data.forEach(i => observe(i))
  }
  insetOb(data, value) {
    Object.defineProperty(data, '__ob__', {
      configurable: true,
      enumerable: false,
      value
    })
  }
}

function defineReactive(data, key) {
  let value = data[key]
  // 创建dep
  const dep = new Dep()
  // 值的Observer实例
  let childOb = observe(value)
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get() {
      // 存在进行中的Watcher
      if(Dep.target) {
        // 触发收集
        dep.depend()
        // 如果存在值的Observer实例,也收集key对应的Watcher
        if(childOb) {
          childOb.dep.depend()
          // [1, [2, [3]]],可能存在无限个嵌套数组,就无限收集Watcher
          if(Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set(val) {
      if(val === value) return 
      value = val
      // 值修改触发Watcher更新
      dep.notify()
    }
  })
}

function dependArray(val) {
  for(let i=0;i<val.length;i++) {
    let e = val[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if(Array.isArray(e)) {
      dependArray(e)
    }
  }
}

function isObjectAndArray(data) {
  return Array.isArray(data) || Object.prototype.toString.call(data) === '[object Object]'
}