Javascript

手写vue--1

本文主要是介绍手写vue--1,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

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

目录
  • 手写vue - 数据响应式、数据的双向绑定、时间监听
    • vue设计思想
      • 1. MVVM三要素:数据响应式、 模板引擎、渲染
        • 数据响应式:监听数据变化,并在视图中响应
        • 模板引擎: 提供描述视图的模板语法
        • 渲染: 如果将模板转换为html
    • 手写VUE
      • 数据响应式:数据劫持:依赖收集,通知更新
      • Vue对象
      • Oberver对象
      • Dep对象: 依赖收集器
      • Watcher对象
      • Compile对象:编译

vue设计思想

1. MVVM三要素:数据响应式、 模板引擎、渲染
数据响应式:监听数据变化,并在视图中响应
模板引擎: 提供描述视图的模板语法
渲染: 如果将模板转换为html

手写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--1的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!