• 手写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>
  • 相关阅读:
    如何重新加载 Spring Boot 上的更改,而无需重新启动服务器?
    什么是 JavaConfig?
    序列号Sequences
    包Packages
    参数Parameters、变量Variables
    maven配置多个镜像
    各种http报错的报错的状态码的分析
    举例说明同步和异步。
    第二阶段的任务及燃尽图(第二天)
    第二阶段的任务及燃尽图(第一天)
  • 原文地址:https://www.cnblogs.com/shine-lovely/p/14782057.html
Copyright © 2020-2023  润新知