1.小知识补充

1.1dom 的操作

dom.childeNodes 获取子节点
node.attributes = {name,value} 获取属性对象(获取键值和值)
dom.appendchild 追加
dom.nextSibling 获取下一个兄弟节点
dom.parentNode 获取父节点
dom.removeChild 删除子节点
dom.insertBefore 插入节点
dom.textContent(innerText) 改变文本内容

1.2正则

RegExp.$1 可以获取捕获的内容, /(123)/.test('123') //RegExp.$1 -- '123', ()--为捕获内容

1.3 数据的劫持 -- Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

语法: Object.defineproperty( object,‘ propName ’ ,descriptor);

  • object :要定义属性的对象,返回的也是

  • propName :要定义或修改的属性的名称。

  • descriptor:要定义或修改的属性描述符,属性描述符详情

简单的双向数据绑定
.html

<input type="text" value="1">
<p><strong>1</strong></p>

js

let obj = {
  name: '1'
}


let input = document.querySelector('input'),
  text = document.querySelector('p strong'),
  btn = document.querySelector('button')


// // 数据劫持
Object.defineProperty(obj, 'keys', {
  get() { 
    console.log('get')
    return input.value
  },
  set(newVal) { 
    console.log('set')
    text.textContent = newVal
    input.value = newVal
  }
})


input.oninput = function (e) {
  obj.name = e.target.value
}

2.vue 学习

2.1 本次文档将走完下面的流程图

  1. Vue类的实现
  2. 数据的劫持监听
  3. 解析器 Complie
  4. 视图初始化
  5. 获取更新函数
  6. 添加订阅者 Wacher -- 保存更新函数(触发get,通过dep去收集 wacher), Dep 收集器的实现 -- 发布订阅模式
  7. 通知变化 通知所有订阅者

3. Vue类的实现

index.html

<div id="app">
  <h2 id="test" class="123" v-text="name"></h2>
  {{age}}
  <p><span>{{age}}</span></p>
  <input type="text" v-bind:value="age" v-on:input="(e) => {this.age = e.target.value}">
</div>

index.js

const app = new Vue({
  el: '#app',
  data: {
    name: 'LONG·江',
    age: 18,
    isShow: true,
    arr: [1, 2, 3],
    detail: {
      qq: '123',
      wx: '321',
      test: { t1: '1', t2: 2 },
    },
  }
})

vue.js
总结: vue就做了这几件事情
1、数据劫持 ✔
2、complie ✔
3、发布订阅模式 ✔

class Vue {
  constructor(options) { 
    this.$el = document.querySelector(options.el)
    this.$options = options
    this.$data = options.data
  }
}

4. 数据的劫持监听

通过 Object.defineProperty 完成数据的劫持,每次访问修改数据都会触发 set get 方法。

4.1 vue.js

class Vue {
  constructor(options) { 
    this.$el = document.querySelector(options.el)
    this.$options = options
    this.$data = options.data

    observe(this.$options.data) // 数据劫持
  }
}

// 数据劫持 'object' 的判断
function observe(data) { 
  if (typeof data !== 'object') return 
  new Observe(data)
}

// Array Object 数组或对象作一个区别处理
class Observe { 
  constructor(data) { 
    this.data = data

    this.walk(data)
  }

  walk(data) { 
    // 遍历每一项数据做响应式劫持
    Object.keys(data).forEach(key => { 
      if (Array.isArray(data[key])) {

      } else { 
        defineReactive(data, key, data[key])
      }
    })
  }
}

// 数据响应式  核心 -- Object.defineProperty
function defineReactive(obj, key, val) { 
  if (typeof val === 'object') { observe(val) } // 作一个递归
  // 每项数据做一个劫持
  Object.defineProperty(obj, key, {
    get() { 
      return val
    },
    set(newVal) { 
      val = newVal
    }
  })
}

通过递归实现深层级的数据劫持

4.2 数据的代理

正常情况下, vue 可以通过 实例 app.name 去获取属性,下面完成这个功能

vue.js

class Vue {
  constructor(options) { 
    this.$el = document.querySelector(options.el)
    this.$options = options
    this.$data = options.data

    observe(this.$options.data) // 数据劫持
    this.proxy() // 代理
  }

  proxy() {
    // 相当于给 vue 实例一层级加上对应的数据劫持
    Object.keys(this.$data).forEach(key => { 
      Object.defineProperty(this, key, {
        get() { 
          // get 方法其实还是访问 this.$data 中的属性,就是做了个印射
          return this.$data[key]
        },
        set(newVal) { 
          // set 方法直接修改 this.$data 中的属性
          this.$data[key] = newVal
        }
      })
    })
  }
}

5. 解析器 Complie 和 视图初始化

解析器 Complie,就是用来对 el:#app 绑定的dom,作一些语法和词法的解析。

vue.js

// 解析器 - 编译的工作
class Complie { 
  constructor(el, vm) { 
    this.node = el
    this.vm = vm
    this.init(this.node)
  }

  init(nodes) { 
    nodes.childNodes.forEach(node => { 
      if (node.nodeType === 3) { // 文本节点
        let reg = /\{\{(.*?)\}\}/
        if (reg.test(node.textContent)) { 
          node.textContent = this.vm[RegExp.$1] // 给内容赋值
        }
      } else if (node.nodeType === 1) { // 标签
        let attrs = node.attributes

        Array.from(attrs).forEach(attr => {
          // attr.name 获取属性名,attr.value 获取属性值
          let key = attr.name, val = attr.value
          
          if (key.includes('v-text')) {
            node.textContent = this.vm[val]
          } 
        })

        if(node.childNodes.length > 0) this.init(node) // 这里做了一个递归子节点
      }
    })
  }
}

绑定好之后,视图便显示了对应的数据, 这便是视图的初始化

6. 获取更新函数

更新函数是 data 中属性更新时,去执行的更新视图的函数,对应更新视图的方法便是更新函数

vue.js

// 跟 5.解析器差不多,只是多了更新函数
class Complie { 
 ......

  init(nodes) { 
    nodes.childNodes.forEach(node => { 
      if (node.nodeType === 3) { // 文本节点
        let reg = /\{\{(.*?)\}\}/
        if (reg.test(node.textContent)) { 
          node.textContent = this.vm[RegExp.$1]
          // 获取更新函数
          let updateFn = function () { 
            node.textContent = this.vm[RegExp.$1]
          }
        }
      } else if (node.nodeType === 1) { // 标签
        let attrs = node.attributes

        Array.from(attrs).forEach(attr => {
          let key = attr.name, val = attr.value

          if (key.includes('v-text')) {
            node.textContent = this.vm[val]
            // 获取更新函数
            let updateFn = function () { 
                node.textContent = this.vm[val]
            }
          }
        })

        if(node.childNodes.length > 0) this.init(node)
      }
    })
  }
}

7. 添加订阅者

7.1 wacher 类的实现

Watcher 主要是初始化的时候触发, 作用是 用来保存更新函数的。

set 的触发,便是去 执行 watcher 中的更新函数。

vue.js

// 观察者 -- 保存更新视图的回调函数
class Watcher { 
  constructor(vm, cb, key) { 
    this.vm = vm
    this.cb = cb
  }
}

Complie 类

​ 初始化时,也就是第一次解析时, 会 new watcher 去保存更新函数

class Complie { 
  ...

init(nodes) { 
  ...

if (node.nodeType === 3) { // 文本节点
    let reg = /\{\{(.*?)\}\}/
    if (reg.test(node.textContent)) { 
        node.textContent = this.vm[RegExp.$1]
        new Watcher(this.vm, function () {  // 一个依赖 对应 new Watcher 实例
        	node.textContent = this.vm[RegExp.$1]
        }, RegExp.$1)
    }
}

7.2 订阅器 -- Dep

vue.js

// 订阅器 - 发布订阅模式
class Dep { 
  constructor() { 
    this.subs = []
  }

  add(watcher) { // 注册
    this.subs.push(watcher)
  }

  notify() { // 通知
    this.subs.forEach(w => { w.cb() })
  }
}

7.3 添加订阅者的实现

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


因为 new Watcher 是根据相关依赖,初始化时候才会执行,以后不会执行了。

1、所以 可以通过 Dep.target 判断是否是首次的加载,是则收集依赖

2、this.vm[key] 是为了触发 get 方法 收集依赖

3、 Dep.target 设置为空, 防止重复收集

Watcher 类的修改

// 观察者 -- 保存更新视图的回调函数
class Watcher { 
  constructor(vm, cb, key) { 
    this.vm = vm
    this.cb = cb
    
    // 核心代码
    Dep.target = this
    this.vm[key] // 触发get,为了依赖收集
    Dep.target = null
  }
}

defineReactive 方法的修改

1、为每个属性劫持时,创建一个收集器 dep 的实例,用来收集对应的依赖

2、get 判断是否是首次的加载,是则收集依赖

// 数据响应式
function defineReactive(obj, key, val) { 
  if (typeof val === 'object') { observe(val) }

  let dep = new Dep()

  Object.defineProperty(obj, key, {
    get() { 
      if (Dep.target) {
        dep.add(Dep.target) // 收集依赖
      }
      return val
    },
    set(newVal) { 
      val = newVal
    }
  })
}

8. 通知变化 -- 通知所有订阅者

数据变法触发 set , 通知所有对应的依赖

set 方法的修改

set(newVal) { 
      val = newVal
      dep.notify()
 }

相当于执行了 wacher 中的更新函数。

执行下面代码,发现对应视图更新

setTimeout(() => { app.age = 19}, 1000)

到这里 vue 的代码写完了。

9.v-modal 的实现

v-modal其实就是实现了 v-bind 和 v-on 的方法,知道原理后,我们知道,这些语法是在解析器里面解析出来的,所以我们要在解析器里面 实现这两个方法。

v-bind 的原理就是为 dom 加上对应的属性 node.setAttribute

v-on 的原理时为该 dom 加上的对应的事件监听 node.addEventListener

index.html

<input type="text" v-bind:value="age" v-on:input="(e) => {this.age = e.target.value}">

v-on 难点解析 -- 字符串转函数的实现

string to function

通过 正则获取 参数,和函数体

然后通过 new Function (参数,函数体)

实现了字符串转 函数
index.html

<input type="text" v-bind:value="age" v-on:input="(e) => {this.age = e.target.value}">

vue.js

class Complie { 
  constructor(el, vm) { 
    this.node = el
    this.vm = vm
    this.init(this.node)
  }

  init(nodes) { 
    nodes.childNodes.forEach(node => { 
      if (node.nodeType === 3) { // 文本节点
        let reg = /\{\{(.*?)\}\}/
        if (reg.test(node.textContent)) { 
          node.textContent = this.vm[RegExp.$1]
          new Watcher(this.vm, function () { 
            node.textContent = this.vm[RegExp.$1]
          }, RegExp.$1)
        }
      } else if (node.nodeType === 1) { // 标签
        let attrs = node.attributes

        Array.from(attrs).forEach(attr => {
          let key = attr.name, val = attr.value

          // v-text
          if (key.includes('v-text')) {
            node.textContent = this.vm[val]
          } else if (key.includes('v-bind')) { // v-bind
            let reg = /v-bind:(.*)/
            if (reg.test(key)) {
              node.setAttribute(RegExp.$1, this.vm[val])
              attrs.removeNamedItem(`v-bind:${RegExp.$1}`) // 移除dom上对应的vue语法
            }
          } else if (key.includes('v-on')) {  // v-on
            let reg = /v-on:(.*)/, reg1 = /\((.*)\)\W*\{(.*)\}/
            let vm = this.vm
            if (reg.test(key)) {
              let eventName = RegExp.$1
              if (reg1.test(val)) { 
                let fn = new Function(RegExp.$1, RegExp.$2.trim()) // 字符串转函数
                node.addEventListener(eventName, function (e) {  // 重写一层时为了获取 e
                  fn.bind(vm)(e) // 让 this 指向 vm
                })
              }
            }
          }
        })

        if(node.childNodes.length > 0) this.init(node)
      }
    })
  }
}

然后通过input输入,发现data也改变了

10. 完整代码

// 1、数据劫持 ✔
// 2、complie ✔
// 3、发布订阅模式 ✔

class Vue {
  constructor(options) { 
    this.$el = document.querySelector(options.el)
    this.$options = options
    this.$data = options.data

    observe(this.$options.data) // 数据劫持
    this.proxy() // 代理
    new Complie(this.$el, this)
  }

  proxy() {
    Object.keys(this.$data).forEach(key => { 
      Object.defineProperty(this, key, {
        get() { 
          return this.$data[key]
        },
        set(newVal) { 
          this.$data[key] = newVal
        }
      })
    })
  }
}

// 数据劫持 'object' 的判断
function observe(data) { 
  if (typeof data !== 'object') return 
  new Observe(data)
}

// Array Object
class Observe { 
  constructor(data) { 
    this.data = data

    this.walk(data)
  }

  walk(data) { 
    Object.keys(data).forEach(key => { 
      if (Array.isArray(data[key])) {
        1
      } else { 
        defineReactive(data, key, data[key])
      }
    })
  }
}

// 数据响应式
function defineReactive(obj, key, val) { 
  if (typeof val === 'object') { observe(val) }

  let dep = new Dep()

  Object.defineProperty(obj, key, {
    get() { 
      if (Dep.target) {
        dep.add(Dep.target)
      }
      return val
    },
    set(newVal) { 
      val = newVal
      dep.notify()
    }
  })
}

// 解析器 - 编译的工作
// 递归遍历dom树
// 判断节点类型,如果是文本,则判断时候是插值绑定
// 如果是元素,则遍历器属性判断是否是指令或事件,然后递归子元素
class Complie { 
  constructor(el, vm) { 
    this.node = el
    this.vm = vm
    this.init(this.node)
  }

  init(nodes) { 
    nodes.childNodes.forEach(node => { 
      if (node.nodeType === 3) { // 文本节点
        let reg = /\{\{(.*?)\}\}/
        if (reg.test(node.textContent)) { 
          node.textContent = this.vm[RegExp.$1]
          new Watcher(this.vm, function () { 
            node.textContent = this.vm[RegExp.$1]
          }, RegExp.$1)
        }
      } else if (node.nodeType === 1) { // 标签
        let attrs = node.attributes

        Array.from(attrs).forEach(attr => {
          let key = attr.name, val = attr.value

          if (key.includes('v-text')) {
            node.textContent = this.vm[val]
          } else if (key.includes('v-bind')) {
            let reg = /v-bind:(.*)/
            if (reg.test(key)) {
              node.setAttribute(RegExp.$1, this.vm[val])
              attrs.removeNamedItem(`v-bind:${RegExp.$1}`) // 移除dom上对应的vue语法
            }
          } else if (key.includes('v-on')) { 
            let reg = /v-on:(.*)/, reg1 = /\((.*)\)\W*\{(.*)\}/
            let vm = this.vm
            if (reg.test(key)) {
              let eventName = RegExp.$1
              if (reg1.test(val)) { 
                let fn = new Function(RegExp.$1, RegExp.$2.trim()) // 字符串转函数
                node.addEventListener(eventName, function (e) { // 重写一层时为了获取 e
                  fn.bind(vm)(e)
                })
              }
            }
          }
        })

        if(node.childNodes.length > 0) this.init(node)
      }
    })
  }
}

// 观察者 -- 保存更新视图的回调函数
class Watcher { 
  constructor(vm, cb, key) { 
    this.vm = vm
    this.cb = cb
    
    Dep.target = this
    this.vm[key] // 为了依赖收集
    Dep.target = null
  }
}

// 订阅器 - 发布订阅模式
class Dep { 
  constructor() { 
    this.subs = []
  }

  add(watcher) { 
    this.subs.push(watcher)
  }

  notify() { 
    this.subs.forEach(w => { w.cb() })
  }
}

Dep.target = null

export default Vue

posted on 2023-03-15 00:22  京鸿一瞥  阅读(156)  评论(0编辑  收藏  举报