[深入13] 观察者 发布订阅 双向数据绑定

2,102 阅读11分钟

导航

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数

[react] Hooks

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程

前置知识

一些单词

subject:目标对象
observer:观察者对象

pattern:模式
notify:通知

Publisher:发布者
Subscriber:订阅者

directive:指令
compile:编译

Recursive:递归 adj
recursion:递归 n
factorial:阶乘

splice

arr.splice(start, count, addElement1, addElement2, ...);
start:开始位置,从0开始( 如果是负数,表示从倒数位置开始删除 )
count:删除的个数
addElement1...:被添加的元素

作用:splice 删除( 原数组 )的一部分成员,并可以在删除的位置( 添加 )新的数组成员
返回值:被删除的元素组成的数组,注意是一个数组
注意:splice 改变原数组
特别:如果只有一个参数( 第一个参数 ),则变相相当于把数组拆分成两个数组,一个是返回值数组,一个是改变后的原数组

for...in 和 for...of

  • for...in:可以用于 ( 数组 ) 或 ( 对象 )
  • for...of:只能用于( 数组 ),因为对象没有部署 iterator 接口
for...in 和 for...of

(1)
for...in:可以用于数组 或 对象
for...of:只能用于数组,因为对象没有部署 iterator 接口


(2)
1. 数组
- for(let i of arr)  ----------- i 表示值
- for(let i in arr)  ----------- i 表示下标
2. 对象
- 对象只能用for in循环,不能用for of循环
- 因为对象没有部署iterator接口,不用使用for of循环
- for (let i in obj) ----------- i 表示key

如何优雅的遍历对象

  • ( Object对象 ) 和 ( Array.prototype对象 ) 都部署了三个方法 keysvaluesentries
  • Object.keys(obj) , Object.values(obj),Object.entries(obj) ---- 静态方法
  • Array.prototype.keys(),Array.prototype.values(),Array.prototype.entries() - 返回 Iterator 遍历器对象
如何优雅的遍历对象

const arr = [{name: '123'},2,3,4]
const obj = {name: '111', age: 222}


对象 
// (注意Object.keys() values() entries() 可以用于对象,也可以用于数组)
for(let [key, value] of Object.entries(obj)) { // object || array
  console.log(key, value)
  // name 111
  //  age 222
  // Object.entries(obj) 返回一个数组,每个成员也是一个数组,由( 键,值 )组成的数组
}


// 数组
// for(let [key, value] of arr.entries()) { // 返回 iterator 对象接口,可以用for...of遍历
//  console.log(key, value)
// }

Element.children 和 Node.ChildNodes

  • Element.children
    • 返回一个类似数组的对象,( HTMLCollection ) 实例
    • 包括 当前元素节点的( 所有子元素 ) ---- 只包括元素节点
    • 如果当前元素没有子元素,则返回的对象包含 0 个成员
    • 注意:返回的是当前元素的所有子( 子元素节点 ),不包括其他节点
  • Node.childNodes
    • 返回一个类似数组的对象,( NodeList ) 集合
    • 包括 当前元素的所有( 子节点 ) ---- 包括元素节点,文本节点,注释节点
  • Node.childNodes 和 Element.children的区别
    • Node.childNodes是NodeList集合, Element.children是HTMLCollection集合
    • Node.childNodes包括当前节点的所有子节点,包括元素节点,文本节点,注释节点
    • Element.children包括当前元素节点的所有子元素节点,只包括元素节点
    • 注意:类似数组的对象具有length属性,且 键 是0或正整数
<div id='content'>
  <div>111</div>
  <div>
    <div>222</div>
    <div>333</div>
  </div>
  <p1>444</p1>
</div>

<script>
  const contentHTMLCollection = document.getElementById('content') ---------- HTMLCollection
  console.log(content.children)
  // HTMLCollection(3) [div, div, p1]

  const contentNodeList = document.querySelector('div') --------------------- NodeList
  console.log(contentNodeList.childNodes)
  // NodeList(7) [text, div, text, div, text, p1, text]
</script>

document.querySelector(css选择器)

  • 返回第一个匹配的元素节点
  • document.querySelectorAll() 返回所有匹配的元素节点

递归 尾递归 尾调用

  • 递归的含义:函数调用自身,尾调用自身,尾递归
  • 递归的条件:边界条件,递归前进段,递归返回段
    • 不满足边界条件,递归前进
    • 满足边界条件,递归返回
  • 尾调用:函数内部最后一个动作是函数调用,该调用的返回值,直接返回给函数
  • 尾调用和非尾调用的区别:
    • 执行上下文栈变化不一样( 即函数调用栈不一样,内存占用不一样 )
尾调用 和 非尾调用

尾调用
function a() {
    return b()
}

非尾调用
function a() {
    return b() + 1
    // 非尾调用
    // 因为调用b()时,a函数并未执行完,未出栈 => b执行完后 + 1 ,这时a函数才执行完毕
}

----
尾调用优化
----

// (1) 正常版 - 阶乘函数
function factorial(number) {
  if(number === 1 ) return number;
  return number * factorial(number - 1)
}
const res = factorial(3)
console.log(res)
// 调用栈
// 1 ------------------------------ 3 * factorial(2)
// factorial(3) 
// 2 ------------------------------ 3 * factorial(2)
// factorial(2)
// factorial(3)
// 3 ------------------------------ 3 * 2 * factorial(1)
// factorial(1)
// factorial(2)
// factorial(3)
// 4 ------------------------------ 3 * 2 * 1
// factorial(2) 
// factorial(3)
// 5 ------------------------------ 6
// factorial(3)



// (2) 尾递归版 - 阶乘函数
function factorial(number, multiply) {
  if (number === 1) return multiply;
  return factorial(number-1, number * multiply)
}
const res = factorial(4, 1)
console.log(res)
// factorial(4, 1)
// factorial(3, 4*1)
// factorial(2, 3 * (4 * 1))
// factorial(1, 2 * (3 * 4 * 1))
// return 1 * 3 * 4 * 1 
// 12
// 每次都是尾递归,所以执行上线问栈中始终只有一个factorial()
// 即下一个 factorial 调用时, 外层的 factorial 已经执行完毕出栈了




// (3) 优化尾递归版 - 阶乘函数
function factorialTemp(multiply, number) {
  if (number === 1) return multiply;
  return factorialTemp(number * multiply, number-1)
}
function partial(fn) { // --------------------------------------- 偏函数
  let params = Array.prototype.slice.call(arguments, 1) // ------ 固定部分参数
  return function closure() {
    params = params.concat([...arguments])
    if (params.length < fn.length) { // 参数小于fn形参个数,就继续收集参数
      return closure
    }
    return fn.apply(this, params) // 否则,证明参数收集完毕,执行fn
  }
}
const factorial = partial(factorialTemp, 1) // 固定参数1
const res = factorial(4)
console.log(res)

观察者模式

概念

  • 对程序中的某个对象进行观察,并在其发生改变时得到通知

  • 存在( 观察者对象 ) 和 ( 目标对象 )两种角色

  • 目标对象:subject

  • 观察者对象:observer

在观察者模式中,subject 和 observer 相互独立又相互联系

  • 一个目标对象对应多个观察者对象 ( 一对多 )
  • 观察者对象在目标对象中( 订阅事件 ),目标对象( 广播事件 )
  • Subject 目标对象:维护一个观察者实例组成的数组,并且具有( 添加,删除,通知 )操作该数组的各种方法
  • Observer 观察者对象:仅仅只需要维护收到通知后( 更新 )操作的方法

代码实现 - ES5

说明:
(1) Subject构造函数
- Subject构造函数的实例( 目标对象 ),维护一个( 观察者实例对象 ) 组成的数组
- Subject构造函数的实例( 目标对象 ),拥有( 添加,删除,发布通知) 等操作观察者数组的方法

(2) Observer构造函数
- Observer构造函数的实例( 观察者对象 ),仅仅只需要维护收到通知后,更新的方法



---------
代码:

function Subject() { // 目标对象的构造函数
  this.observes = [] // 观察者对象实例组成的数组
}
Subject.prototype = { // 原型上挂载操作数组的方法
  add(...params) { // --------------------------------- 添加观察者
    // 对象方法的缩写,添加观察者对象
    // params是ES6中的( rest参数数组 ),用来代替 arguments 对象,可以接收多个参数
    // this在调用时确定指向,Subject构造函数的实例在调用,所以实例具有observes属性
    this.observes = this.observes.concat(params)
  },
  delete(obj) { // ------------------------------------ 删除观察者
    const cashObservers = this.observes // ------------ 缓存能提高性能,因为下面有多次用到
    cashObservers.forEach((item, index) => {
      if (item === obj) cashObservers.splice(index, 1)
      // 这里是三等判断,因为 obj 和 item 的指针都执行同一个堆内存地址
      // 举例
      // const a = {name: 'woow_wu7'}
      // const b = [a, 1, 2]
      // b[0] === a 
      // 上面的结果是true - 因为b[0]和a指向了同一个堆内存地址
      
      // 注意:cashObservers.splice(index, 1)
      // 删除cashObservers相当于删除this.observes
      // 因为:this.observes赋值给了cashObservers,并且this.observes是复合类型的数据,所以两个变量指针一致
    })
  },
  notify() { // --------------------------------------- 通知观察者,并执行观察者各自的更新函数
    if(this.observes.length) this.observes.forEach(item => item.update())
  }
}
Subject.prototype.constructor = Subject // ------------- 改变prototype的同时修改constructor,防止引用出错

function Observer(fn) { // ----------------------------- 观察者对象仅仅维护更新函数
  this.update = fn
}

const obsObj1 = new Observer(() => console.log(111111))
const obsObj2 = new Observer(() => console.log(222222))
const subObj = new Subject()
subObj.add(obsObj1, obsObj2)
subObj.notify()
subObj.delete(obsObj1)
subObj.notify()

代码实现 - ES6


-----------
观察者模式 - es6语法实现

class Subject {
  constructor() {
    this.observers = []
  }
  add = (...rest) => {
    this.observers = this.observers.concat(rest)
  }
  delete = (obj) => {
    const cachObservers = this.observers
    cachObservers.forEach((item, index) => {
      if (item === obj) cachObservers.splice(index, 1)
    })
  }
  notify = () => {
    this.observers.forEach(item => {
      item.update()
    });
  }
}
class Observer {
  constructor(fn) {
    this.update = fn
  }
}
const objserverIns = new Observer(() => console.log(1111))
const objserverIns2 = new Observer(() => console.log(2222))
const subjectIns = new Subject()
subjectIns.add(objserverIns, objserverIns2)
subjectIns.notify()

2021/4/7 观察者模式优化

  • 优化 add => subscribe订阅
  • 优化 delete => unSubscribe取消订阅
<!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>
  <script>
    function Subject() {
      this.observes = [] // 观察者实例对象 组成的数组
    }
    Subject.prototype = {
      subscribe(...params) {
        this.observes = this.observes.concat(params)
      },
      unSubscribe(obj) {
        this.observes.forEach((item, index) => {
          if (item === obj) {
            this.observes.splice(index, 1)
          }
        })
      },
      notify() {
        this.observes.length && this.observes.forEach(item => item.update())
      }
    }
    Subject.prototype.constructor = Subject

    function Observer(fn) {
      this.update = fn
    }

    const fn1 = () => console.log('fn1')
    const fn2 = () => console.log('fn2')
    const observer1 = new Observer(fn1)
    const observer2 = new Observer(fn2)

    const subject = new Subject()
    subject.subscribe(observer1, observer2) // ------- 订阅
    subject.notify() // ------------------------------ 发布
    console.log(subject.observes)
    subject.unSubscribe(observer1) // ---------------- 取消订阅
    console.log(subject.observes)
  </script>
</body>
</html>
  • 2020/12/28 复习

发布订阅模式

角色

  • 发布者:Publisher
  • 订阅者:Subscriber
  • 中介:Topic/Event Channel
  • ( 中介 ) 既要 '接收' ( 发布者 )所发布的消息事件,又要将消息 '派发' 给( 订阅者 )
  • 所以中介要根据不同的事件,存储响应的订阅者信息
  • 通过中介对象,完全解耦了发布者和订阅者

发布订阅模式 es5代码实现

发布订阅模式 - 代码实现

var pubsub = {}; // 中介对象
(function(pubsub) {
  var topics = {} 
  // topics对象,存放每个事件对应的( 订阅者对象 )组成的( 数组 )
  // key: 事件名 
  // eventName:[{functionName: fn.name, fn: fn}]
  
  pubsub.subscribe = function(eventName, fn) {
    // ------------------------------------------------------------ 订阅 
    if (!topics[eventName]) topics[eventName] = [] // 不存在,新建
    topics[eventName].push({
      functionName: fn.name, // 函数名作为标识,注意函数名不能相同
      fn, // 更新函数
    })
  }
  pubsub.publish = function(eventName, params) {
    // ---------------- --------------------------------------------- 发布 
    if (!topics[eventName].length) return; // 如果空数组,即没有该事件对象的订阅者对象组成的数组,跳出整个函数
    topics[eventName].forEach(item => {
      item.fn(params) // 数组不为空,就循环执行该数组所有订阅者对象绑定更新函数
    })
  }
  pubsub.unSubscribe = function(eventName, fn) { // 这里其实可以有化成传入回调函数名而不用传入整个回调函数
    // -------------------------------------------------------------- 取消订阅
    if (!topics[eventName]) {
      return
    }
    topics[eventName].forEach((item, index) => {
      if (item.functionName = fn.name) {
        // 如果:事件名 和 函数名 相同
        // 就:删除这个事件对应的数组中的订阅者对象,该对象包含函数名,fn两个属性
        topics[eventName].splice(index)
      }
    })
  }
})(pubsub)

function closoleLog(args) { // 订阅者收到通知后的更新函数
  console.log(args)
} 
function closoleLog2(args) {
  console.log(args)
}

pubsub.subscribe('go', closoleLog) // 订阅
pubsub.subscribe('go', closoleLog2)
pubsub.publish('go', 'home') // 发布
pubsub.unSubscribe('go', closoleLog) // 取消发布
console.log(pubsub)

发布订阅模式 es6代码实现

class PubSub {
  constructor() {
    this.topics = {}
  }
  subscribe(eventName, fn) {
    if (!this.topics[eventName]) {
      this.topics[eventName] = []
    }
    this.topics[eventName].push({
      fname: fn.name,
      fn
    })
  }
  publish(eventName, params) {
    if (!this.topics[eventName].length) {
      return
    }
    this.topics[eventName].forEach((item, index) => {
      item.fn(params)
    })
  }
  unSubscribe(eventName, fname) {
    if (!this.topics[eventName]) {
      return
    }
    this.topics[eventName].forEach((item, index) => {
      if (item.fname === fname) {
        this.topics[eventName].splice(index, 1)
      }
    })
  }
}

const pubsub = new PubSub()

function goFn1(params) {
  console.log('goFn1', params)
}
function goFn2(params) {
  console.log('goFn2', params)
}

pubsub.subscribe('go', goFn1)
pubsub.subscribe('go', goFn2)

pubsub.publish('go', '1') // 发布

pubsub.unSubscribe('go', goFn2.name) // 取消订阅
pubsub.publish('go', '2')

2020/12/29 复习 - 发布订阅模式ES5实现

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    // 中介对象
    const pubsub = {}

    // 注意:( 小括号 ) 和 ( 中括号 ) 开头的 ( 前一条语句 ) 必须加分号,或者在小括号或中括号的最前面加分号
    ;(function(pubsub) {
      const topic = {}

      // 订阅 
      // subscribe(订阅的事件名, 事件触发的回调函数)
      pubsub.subscribe = function(eventName, fn) {
        if (!topic[eventName]) topic[eventName] = [];
        topic[eventName].push({
          fnName: fn.name,
          fn,
        })
        console.log('topic[eventName]', topic[eventName])
      }

      // 发布
      // publish(事件名,事件触发对应的回调函数的参数)
      pubsub.publish = function(eventName, params) {
        console.log('topic[eventName]', topic[eventName])
        if (topic[eventName]) {
          topic[eventName].forEach(observer => {
            observer.fn(params)
          })
        }
      }

      // 取消订阅
      // unScribe(需要取消的事件名, 需要取消的回调函数名)
      pubsub.unScribe = function(eventName, fnName) {
        if (topic[eventName]) {
          topic[eventName].forEach((observer, index) => {
            if (observer.fnName === fnName) {
              topic[eventName].splice(index, 1)
            }
          })
        }
      }
    })(pubsub)
    pubsub.subscribe('go', function go1(address1){console.log(`${address1}one`)})
    pubsub.subscribe('go', function go2(address2){console.log(`${address2}two`)})
    pubsub.publish('go', 'home')
    pubsub.unScribe('go','go1') // 取消订阅go1函数
    pubsub.publish('go', 'work')
  </script>
</body>
</html>

2020/12/29 复习 - 发布订阅模式ES56实现

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    class PubSub {
      constructor() {
        this.topic = {}
      }

      subscribe = (eventName, fn) => {
        if (!this.topic[eventName]) this.topic[eventName] = [];
        this.topic[eventName].push({
          fnName: fn.name,
          fn
        })
      }

      publish = (eventName, params) => {
        if (this.topic[eventName]) {
          this.topic[eventName].forEach(observer => {
            observer.fn(params)
          })
        }
      }

      unSubscribe = (eventName, fnName) => {
        if (this.topic[eventName]) {
          this.topic[eventName].forEach((observer, index) => {
            if ( observer.fnName === fnName) {
              this.topic[eventName].splice(index, 1)
            }
          })
        }
      }
    }
    const pubSub = new PubSub()
    pubSub.subscribe('go', function go1(params) { console.log(`${params+'1'}`)})
    pubSub.subscribe('go', function go2(params) { console.log(`${params+'2'}`)})
    pubSub.publish('go', 'home')
    pubSub.unSubscribe('go', 'go1') // 取消订阅
    pubSub.publish('go', 'work')
  </script>
</body>
</html>

2021/12/05 优化 - 以上实现其实存在一些问题的

  • 订阅者身份的唯一标记,因为在unSubscribe时需要通过唯一性识别订阅者对象
  • 问题
    • 这里使用了 fn.name 来作为订阅者的唯一识别是有问题的
    • 因为如果订阅者是一个对象时,对象的方法是没有name属性的,只有函数才有
  • 解决
    • 我们使用为一个id来实现,每次自增
// 发布订阅模式
// 1 模型
// 发布者 -- 事件中心 --- 订阅者
// publisher -- topics/eventChannel --- subscriber
// 2 关系
// 订阅者 -> 可以订阅多个事件
// 3 实现过程的注意点
// 订阅者身份的唯一标记,因为在unSubscribe时需要通过唯一性识别订阅者对象
---

const publishSubscribeDesignPattern = () => {
  // topics对象,存放每个 ( 事件 ) 对应的 ( 订阅者对象 ) 组成的 ( 数组 )
  // key: 事件名
  // value: 订阅者对象组成的数组
  // eventName:[{functionName: fn.name, fn: fn}]
  const topics = {};

  // ID 订阅者为唯一性
  let id = 0;

  // 订阅
  topics.subscribe = (eventName, fn) => {
    // 1
    // 每一个事件可以有多个订阅者,所以 topics[eventName] 是一个数组,并且事件中心用一个对象去维护
    if (!topics[eventName]) topics[eventName] = [];

    // 2
    // topics[eventName].push({ fnName: fn.name, fn });
    // 问题:这里使用了 fn.name 来作为订阅者的唯一识别是有问题的,因为如果订阅者是一个对象时,对象的方法是没有name属性的,只有函数才有
    // 解决:我们使用为一个id来实现,每次自增
    if (fn) {
      fn.id = id++; // 给每个订阅者添加一个唯一的id,id用来做取消订阅时的身份识别
    }
    topics[eventName].push({
      id: fn.id,
      fn,
    });
  };

  // 发布
  topics.publish = (eventName, params) => {
    const subscribers = topics[eventName];
    subscribers?.length && subscribers.forEach(({ fn }) => fn(params));
  };

  // 取消订阅
  topics.unSubscribe = (eventName, fn) => {
    const subscribers = topics[eventName];
    if (subscribers?.length) {
      const index = subscribers.findIndex(({ id }) => id === fn.id); // 在 subscribe 时已经为每个订阅者对象添加了唯一的id
      subscribers.splice(index, 1);
    }
  };

  return topics;
};
const topics = publishSubscribeDesignPattern();
const a = (params) => console.log("a", params);
const b = (params) => console.log("b", params);

topics.subscribe("click", a);
topics.subscribe("click", b);
topics.unSubscribe("click", b);
topics.publish("click", "2021/12/05");

观察者模式,发布-订阅模式的区别和联系

(1)区别

  • 观察者模式:需要观察者自己定义事件发生时的响应函数
  • 发布-订阅模式:在(发布者对象),和(订阅者对象)之间,增加了(中介对象)

(2)联系

  • 二者都降低了代码的(耦合性)
  • 都具有消息传递的机制,以(数据为中心)的设计思想

vue双向数据绑定

前置知识:

1. Element.children
- 返回一个类似数组的对象(HTMLCollection实例)
- 包括当前元素节点的所有子元素
- 如果当前元素没有子元素,则返回的对象包含0个成员

2. Node.childNodes
- 返回一个类似数组的对象(NodeList集合),成员包括当前节点的所有子节点
- NodeList是一个动态集合

3. Node.childNodes 和 Element.children 的区别
- Element.children只包含元素类型的子节点,不包含其他类型的子节点
- Node.childNodes包含元素节点,文本节点,注释节点


代码
-------------

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <div v-text="name"></div>
    <input type="text" v-model="name">
  </div>

  <script>
    // 监听类
    // 主要作用是:将改变之后的data中的 ( 最新数据 ) 更新到 ( ui视图 ) 中
    class Watcher {
      constructor(directiveName, el, vm, exp, attr) {
        this.name = directiveName // 指令的名字,比如 'v-text','v-model'
        this.el = el // 每个具体的DOM节点
        this.vm = vm // MyVue实例对象
        this.exp = exp // el中的directiveName属性对应的属性值
        this.attr = attr // el的属性,需要需改的属性

        this._update() // 注意这里在实例化Watcher时,会执行_update()方法
      }
      _update() {
        this.el[this.attr] = this.vm.$data[this.exp]
        // 将MyVue实例的data属性的最新值更新到ui视图中
      }
    }

    class MyVue {
      constructor(options) {
        const {el, data} = this.options = options // 参数结构赋值
        this.$el = document.querySelector(el) // 获取el选择器的元素节点
        this.$data = data // data数据

        this._directive = {}
        // key:data对象中的 key,递归循环data从而获取每一层的属性作为key
        // value:数组,用来存放Watcher实例
        
        this._observes(this.$data) 
        // 1. 递归循环,获取data每一层的key
        // 2. 把_directive对象,用每个key做( 键 ),用一个Watcher实例组成的数组做( 值 )
        // 3. 监听data中每个key对应的value是否变化
            // 变化就执行 => _directive对象中key对应的数组中的Watcher实例对象的_update()方法
    
        this._compile(this.$el)
        // 1. 递归循环,获取所有$el对象的元素节点的 所有子元素节点 Element.children
        // 2. 如果元素节点有 v-text 指令,就把Watcher实例push进_directive对象该指令值对应的数组
            // 因为执行了new Watcher(),所以constructor中调用了_update,所以会执行一次
            // 即push实例的同时,也会执行_update(),已经是会把data的数据写入到对应的指令所在的元素节点中
        // 3. 如果元素节点有 v-model指令,同理
            // 不同的是:需要监听( input事件 ),键最新的input框的value值,赋值给data
            // data改变,会被Object.defineProperty监听,从而执行更新函数
            // 更新函数负责把最新的data中的数据更新到页面中
      }

      _observes(data) {
        for(let [key, value] of Object.entries(data)) {
          if (data.hasOwnProperty(key)) {
            this._directive[key] = []
          }
          if (typeof value === 'object') { // 数组 或 对象
            this._observes(value) // 递归调用
          }
          const _dir = this._directive[key]
          Object.defineProperty(this.$data, key, { // ------------- 监听属性变化
            enumerable: true,
            configurable: true,
            get() {
              return value
            },
            set(newValue) {
              if (newValue !== value) { // ------------------------ 属性变化后执行所以订阅者对象的更新函数
                value = newValue
                _dir.forEach(item => item._update())
              }
            }
          })
        }
      }

      _compile(el) {
        for(let [key, value] of Object.entries(el.children)) {
          if (value.length) {
            _compile(value)
          }
          if (value.hasAttribute('v-text')) {
            const attrValue = value.getAttribute('v-text')
            this._directive[attrValue].push(new Watcher('input', value, this, attrValue, 'innerHTML'))
          }
          if(value.hasAttribute('v-model') && (value.tagName === 'INPUT' || value.tagName === 'TEXTAREA')) {
            const attrValue = value.getAttribute('v-model')
            this._directive[attrValue].push(new Watcher('v-model', value, this, attrValue, 'value'))
            let that = this
            value.addEventListener('input', function() { // input事件监听
              that.$data[attrValue] = value.value 
              // 获取最新的input框值,赋值给data => Object.defineProperty监听到data变化 => 执行更新函数更新视图
            })
          }
        }
      }
    }

    // 实例化MyVue
    new MyVue({ 
      el: '#app', 
      data: {
        name: 'woow_wu7'
      }
    })
  </script>
</body>
</html>

2020/12/29 vue双向数据绑定 - 亲测可用

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <!-- 引入 Vue 通过CDN引入 -->
  <!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
</head>

<body>
  <div id="root">
    <div type="text" v-text="name">v-text的内容</div>
    <input type="text" v-model="name">
    <script>
      class MyVue {
        constructor(options) {
          const { el, data } = this.$options = options
          this.$el = document.getElementById(el)
          this.$data = data

          this._directive = {}
          // key:data对象中的key
          // value: 

          this._observer(this.$data)
          this._compile(this.$el)
        }

        _observer = (data) => {
          for (let [key, value] of Object.entries(data)) {
            // key就是data对象中的key;  value就是data对象中每个key对应的值
            // data: {name: 'woow_wu7'} => key=name,value='woow_wu7'
            if (data.hasOwnProperty(key)) {
              this._directive[key] = [] // data中每个key都对应一个数组
            }
            if (typeof value === 'object') this._observer(value);
            const that = this
            Reflect.defineProperty(this.$data, key, {
              enumerable: true,
              configurable: true,
              get() {
                return value
              },
              set(newValue) {
                if (value !== newValue) {
                  value = newValue
                  that._directive[key].forEach(item => item._update())
                }
              }
            })

          }
        }

        _compile = (el) => {
          for (let [key, value] of Object.entries(el.children)) {
            if (value.length) {
              this._compile(value)
            }
            if (value.hasAttribute('v-text')) {
              const attrubuteValue = value.getAttribute('v-text')
              this._directive[attrubuteValue].push(new Watcher('input', value, this, attrubuteValue, 'innerHTML'))
              // 注意:
              // attrubuteValue是v-text对应的值 => 其实就是data中的key值,和_observer中的声明保持一致了
            }
            if (value.hasAttribute('v-model') && value.tagName === 'INPUT' || value.tagName === 'TEXTAREA') {
              const attributeValue = value.getAttribute('v-model')
              this._directive[attributeValue].push(new Watcher('v-model', value, this, attributeValue, 'value'))

              const that = this
              value.addEventListener('input', (e) => {
                // 1. input事件修改data中的属性
                // 2. data中的属性被修改,触发 Reflect.defineProperty 的 setter() 函数
                this.$data[attributeValue] = e.target.value
              }, false)
            }
          }
        }
      }

      class Watcher {
        constructor(directiveName, el, vm, exp, attr) {
          this.name = directiveName // 指令的名字,比如 'v-text','v-model'
          this.el = el // 每个具体的DOM节点
          this.vm = vm // MyVue实例对象
          this.exp = exp // el中的directiveName属性对应的属性值
          this.attr = attr // el的属性,需要需改的属性

          this._update()
        }
        _update = () => {
          this.el[this.attr] = this.vm.$data[this.exp]
          // 将MyVue实例的data属性的最新值更新到ui视图中
        }
      }

      new MyVue({
        el: 'root',
        data: {
          name: 'woow_wu7'
        }
      })
    </script>
</body>

</html>

2021/12/06 vue双向数据绑定 - 更新

<!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">
      <input type="text" v-model="name" />
      <div v-text="name"></div>
    </div>
    <script>
      // const obj1 = { a: 1 }; // ------------ 1. 原型属性
      // const obj2 = Object.create(obj1);
      // obj2.b = 2;
      // Object.defineProperty(obj2, "c", {
      //   value: 3,
      //   configurable: true,
      //   enumerable: true, // -------------- 2. 可枚举属性
      //   writeable: true,
      // });
      // const keys = Object.keys(obj2);
      // console.log(`keys`, keys);
      // Object.keys() 返回 自身属性 + 可枚举属性 ,不包含继承的属性

      // Watcher
      // 监听类
      // 主要作用是:将改变之后的data中的 ( 最新数据 ) 更新到 ( ui视图 ) 中
      class Watcher {
        constructor(directiveName, el, attr, exp, vm) {
          // this.name = directiveName; // 指令的名字,比如 'v-text','v-model'
          this.el = el; // 每个具体的 DOM 节点
          this.attr = attr; // --- el中的属性,需要需改的属性 - key
          this.exp = exp; // ----- el中属性对应的 ( 属性值 ) - value
          this.vm = vm; // MyVue实例对象

          this.update(); // 注意这里在实例化Watcher时,会执行_update()方法
        }

        update = () => (this.el[this.attr] = this.vm.$data[this.exp]);
        // 将MyVue实例的data属性的最新值更新到ui视图中
      }

      // Vue
      class MyVue {
        constructor(props) {
          const { el, data } = props || {};
          // 在vue中 $ 开头的属性,是vue的保留属性
          this.$el = el;
          this.$data = typeof data === "function" ? data() : data;

          this._subs = {};
          // key:data对象中的 key,递归循环data从而获取每一层的属性作为key
          // value:数组,用来存放Watcher实例

          this._observer(this.$data);
          this._compiler(this.$el);
        }

        _observer = (data) => {
          // 1
          // object.keys() object.values() object.entries() --> 自身属性 + 可枚举属性
          // for...in ----------------------------------------> 自身属性 + 可枚举属性 + 继承的属性
          // 上面的三者包含的成员包括 ( 自身属性 + 可枚举属性 ),但 ( 不包含继承的属性 )
          // 所以 object.entries 不用像 for...in 那样判断是否具有继承的属性,因为 for...in 会遍历继承的属性
          // 详细:https://juejin.cn/post/7029703494877577246#heading-22

          Object.entries(data).forEach(([key, value]) => {
            if (typeof value === "object" && value !== null) {
              this.observer(value); // 还是对象,递归做依赖收集和派发更新
            }
            if (!this._subs[key]) {
              this._subs[key] = []; // 新建
            }
            const that = this;

            Reflect.defineProperty(this.$data, key, {
              enumerable: true, // 可枚举
              configurable: true, // 可修改配置对象,可删除对象的属性
              // writable: true, // value的值可以被修改,但这里使用了get和set,没有使用value
              get() {
                return value;
              },
              set(newValue) {
                if (value !== newValue) {
                  value = newValue;
                }
                // 修改data后去更新ui
                that._subs[key].forEach((watcher) => watcher.update());
              },
            });
          });
        };

        _compiler = (el) => {
          // const app = document.querySelector(el) // 这里不能用 querySelector,因为是静态的NodeList,不能动态变化
          const app = document.getElementById(el);
          const children = app.children; // 返回所有子元素的类似数组的对象
          Object.entries(children).forEach(([key, value]) => {
            if (value.length) {
              this._compiler(value); // 还有子元素,递归
            }
            const vText = value.getAttribute("v-text");
            const vModel = value.getAttribute("v-model");
            // if (value.hasAttribute('v-text')){} 这样判断也是可以的
            if (vText) {
              this._subs[vText].push(
                new Watcher("v-text", value, "innerHTML", vText, this)
              );
            }
            if (
              (vModel && value.tagName === "INPUT") ||
              value.tagName === "TEXTAREA"
            ) {
              this._subs[vModel].push(
                new Watcher("v-model", value, "value", vModel, this)
              ); // watcher 订阅 data中vModel对应的属性的变化
              const that = this;
              value.addEventListener(
                "input",
                (e) => (that.$data[vModel] = e.target.value) // 监听input输入值的变化,修改 data,触发派发更新
              );
            }
          });
        };
      }

      new MyVue({
        el: "app",
        data() {
          return {
            name: "woow_wu7",
          };
        },
      });
    </script>
  </body>
</html>

资料

详细 juejin.im/post/684490…
精简 juejin.im/post/684490…
实现vue数据双向绑定 juejin.im/post/684490…
segmentfault.com/a/119000001…
juejin.im/post/684490…
我的简书:www.jianshu.com/p/bdb03feab…