手写vue -1 --数据响应式、数据的双向绑定、事件监听

手写vue - 数据响应式、数据的双向绑定、事件监听

相关面试题:

1. MVVM的理解

MVVM:Model-View-ViewModel,也就是把MVC的Controller演变成ViewModel。

Model:数据模型,View:UI组件,ViewModel:View和Model的桥梁,数据绑定到ViewModel层并自动将数据渲染到页面中,视图变化会通知ViewModel层更新数据。

2. Vue实现数据双向绑定的原理: Object.defineProperty()
  • vue实现数据双向绑定主要是:采用 数据劫持 结合 发布者-订阅者模式的方式,通过Object.defineProperty() 来劫持data中各个属性的访问器setter,getter。当数据发生变动时,发布者dep消息(notice)给订阅者wathcer通知更新,触发相应的监听回调。

  • vue数据双向绑定,整合 Observer,Compile和Watcher三者,通过Observe来监听自己model的数据变化,通过Compile来解析编译模板({{}}/v-/@),最终Watcher调用update来进行视图更新。

3. 组件中的data为什么是一个函数

一个组件被复用多次的话,就会创建多个实例。

本质上,这些实例用的是同一个构造函数。

如果data是对象的话,对象是引用类型,会影响到所有的实例。

所以,为了保证不同实例之间的data不冲突,data是个函数,返回一个对象。

手写VUE

数据响应式:数据劫持:依赖收集,通知更新
Vue对象
class Vue {
  constructor(options) {
    this.$options = options
    this.$el = document.body
    this.$data = options.data()
    this.$methods = options.methods
    this.$mounted = options.mounted

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

    // 编译数据
    this.compile(this.$el)
  }

  // 遍历劫持数据
  observe(obj) {
    if (typeof obj !== 'object') {
      return;
    }
    Object.keys(obj).forEach(key => {
      // 递归遍历所有层次
      this.observe(obj[key])

      // 创建观察者
      new Oberver(obj, key)

      // 数据代理: this.$data.title ---> this.title
      this.proxyData(key)
    })
  }

  // 数据代理
  proxyData(key) {
    Object.defineProperty(this, key, {
      get() {
        return this.$data[key]
      },
      set(v) {
        this.$data[key] = v
      }
    })
  }

  // 编译
  compile(el) {
    new Compile(this, el)
  }

  $mount(sel) {
    this.$el = document.querySelector(sel)
    const update = () => {
      if (!this.mounted) {
        // 首次执行, 实现挂载
        this.mounted = true

        if (this.$mounted) {
          this.$mounted()
        }
      }
    }
    update()
  }
}
Oberver对象
// 观察者
class Oberver {
  constructor(obj, key) {
    this.defineReactive(obj, key, obj[key])
  }

  // 定义响应式: 遍历data中的每个数据,都要生成一个Dep容器,这个Dep容器用来收集该数据产生的依赖(即:每使用一次该数据,就会产生一个Watcher,用来update更新)
  defineReactive(obj, key, val) {
    const dep = new Dep() // 遍历data中的每一个数据,并生成相应的Dep容器
    Object.defineProperty(obj, key, {
      get() {
        // 每一次访问数据 this.title , 都会往Dep容器的实例里面deps推入一个watcher
        if (Dep.target) { // 每次使用该数据的时候,都要创建一个依赖(即Watcher,方便未来进行数据更新)
          dep.addDep(Dep.target)
        }
        return val
      },
      set(v) {
        if (val !== v) { // 数据值改变时,给该数据赋新值,并通知(notity)该数据的所有依赖进行更新
          val = v
          // 数据重新赋值,[通知]该数据所有的依赖更新数据
          dep.notice()
        }
      }
    })
  }
}
Dep对象: 依赖收集器
// 依赖收集容器Dep:即,管理wathcer的管理者。data中的每一个数据,都要生成一个dep容器,用来收集
// Dep容器,data中的每个数据会对应一个,用来收集并存储依赖(依赖: 就是 template中的 插值表达式,v-,@等等的数据)
// Dep对象有一个静态属性target,用来存放Watcher实例---即依赖deps数组中的元素---即Dep.target
class Dep {
  constructor() {
    // 每一项数据的依赖收集在这个数组中, 每一个依赖,就是一个Watcher
    this.deps = []
  }

  addDep(dep) {
    this.deps.push(dep)
  }

  notice() {
    this.deps.forEach(dep => { // 这里的dep是一个watcher
      dep.update()
    })
  }
}
Watcher对象
// Watcher: 编译时{{}},v-等,每访问一次数据,就要创建一个watcher实例
// 三个参数,vm:vue实例,方便是用
// 什么时候进行wathcer实例化呢?
//     在编译的时候,每次遇到{{}},v-,@ 等时,就要创建一个实例
class Watcher {
  constructor(vm, key, callback) {
    this.$cb = callback  // 回调函数用来更新数据
    Dep.target = this // 将Watcher实例存储一个全局变量中,存到Dep.target中,方便get方法,收集依赖
    vm[key] // 触发get方法,在get方法中收集依赖(defineReactive中定义了)
    Dep.target = null // Dep.target置空,方便下一次使用数据时,存储Watcher实例化时
  }

  update() {
    // 执行回调函数,来更新数据
    this.$cb()
  }
}
Compile对象:编译

知识储备
1. nodeType: 1:元素节点 3:文本节点
2. fragment节点: 存在内存中的文档片段,并不在DOM树中--> 将子元素插入fragment文档片段中,不会引起页面回流(对元素位置和几何上的计算)。所以,更好的性能
3. const reg = /\{\{.*\}\}/ 可以匹配{{name}}。
但要提取出name,还需要在 .* 外加一层()。这一对小括号就是一个捕获组,可以帮助我们在匹配字符串的同时并捕获字符串中更精细的信息。
RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串
以此类推,RegExp.$2RegExp.$3,……RegExp.$99总共可以有99个匹配

// 编译: 1. document-> fragment   2. fragment中将 {{}}、v-、@等 提取出并进行相应操作  3. 将fragment转为dom
class Compile {
  constructor(vm, el) {
    this.$vm = vm
    this.$el = el
    if (this.$el && this.isElementNode(this.$el)) {
      this.$fragment = this.node2Fragment(this.$el)
      this.compileFragment(this.$fragment)
      this.$el.appendChild(this.$fragment)
    }
  }

  // 将节点转化为fragment文档片段,在内存中操作不直接操作dom,不会引起页面回流
  node2Fragment(el) {
    const fragment = document.createDocumentFragment()
    let child
    while (child = el.firstChild) { // el.firstChil:返回文档的首个子节点,将el.firstChild赋值给child,并且当child===undefined就跳出循环
      fragment.appendChild(child) // appendChild会把原来的firstChild给移动到新的文档中, el中firstChild随之就会递进一个元素。
    }
    return fragment
  }

  // 编译fragment,提取出{{}}/v-/@,并创建相应的wathcer(依赖)
  compileFragment(fragment) {
    const nodes = fragment.childNodes // 伪数组
    Array.from(nodes).forEach(node => {
      if (this.isInterpolation(node)) { // 是插值表达式,提取出变量
        this.compileText(node)
      } else if (this.isElementNode(node)) {// 是v-model、v-html、v-text、@change
        this.compileElement(node)
      }
      node.childNodes.length > 0 && this.compileFragment(node)
    })
  }

  compileText(node) {
    const key = this.getInterKey(node)
    this.text(node, key)
  }

  compileElement(node) {
    const attrs = node.attributes // {0: {name:'class', value: 'active'}, length: 1}
    Array.from(attrs).forEach(attr => {
      if (attr.name.startsWith('v-')) {  // v-model = "inputValue"
        this[attr.name.substring(2)](node, attr.value) // model、html/text
      } else if (attr.name.startsWith('@')) {
        this.eventHandler(node, attr.name.substring(1), attr.value)
      }
    })
  }

  // 事件处理
  eventHandler(node, eventName, methodName) {
    node.addEventListener(eventName, (e) => {
      this.$vm.$methods[methodName].call(this.$vm, e.target.value, e)
    })
  }

  // 通过data数据的key值,更新数据,需要用到watcher
  text(node, key) {
    new Watcher(this.$vm, key, () => {
      node.textContent = this.$vm.$data[key]
    })
    node.textContent = this.$vm.$data[key]
  }

  // 通过data数据的key值,更新数据,需要用到watcher
  html(node, key) {
    new Watcher(this.$vm, key, () => {
      node.innerHtml = this.$vm[key]
    })
    node.innerHtml = this.$vm[key]
  }

  // 通过data数据的key值,更新数据,需要用到watcher
  model(node, key) {
    new Watcher(this.$vm, key, () => {
      node.value = this.$vm[key]
    })
    node.value = this.$vm[key]
    node.addEventListener('input', (e) => {
      this.$vm[key] = e.target.value
    })
  }

  // 提取出插值表达式中的变量:data数据对应的key {{title}}
  getInterKey(node) {
    const reg = /\{\{(.*)\}\}/ // 正则一对小括号就是一个捕获组,可以帮助我们在匹配字符串的同事捕获字符串中更精细的信息
    node.textContent.match(reg)
    return RegExp.$1.trim()
  }

  // 判断是否是差值表达式 {{ title }}: 1. 文本节点; 2. 有花括号
  isInterpolation(node) {
    return node.nodeType === 3 && /\{\{.*\}\}/.test(node.textContent)
  }

  // 判断节点是否为元素节点
  isElementNode(el) {
    return el && el.nodeType === 1
  }
}

引用Vue

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>手写mini-vue</title>
</head>
<body>
<div id="app">
  <h3>{{title}}</h3>
  <h3>{{input}}</h3>
  <div>
    <input v-model="input" type="text" @change="changeInput">

  </div>
</div>
<script src="./vue.js"></script>
<script>
  const mVue = new Vue({
    data() {
      return {
        title: '这里是mini-vue的标题初始值!',
        input: '111'
      }
    },
    created() {
      this.input = "input 在created阶段赋的值"
    },
    mounted() {
      setTimeout(() => {
        this.title = '标题已经改变了!'
      }, 1500)
    },
    methods: {
      changeInput(v, e) {
        console.log(v, e);
      }
    }
  })
  mVue.$mount('#app')
</script>
</body>
</html>
posted @ 2021-05-18 17:54  shine_lovely  阅读(118)  评论(0编辑  收藏  举报