Vue双向绑定原理梳理

vue,双向,绑定,原理,梳理 · 浏览次数 : 185

小编点评

**代码解析** **获取模板** ```javascript const fragment = document.createDocumentFragment() let child = el.firstChild while (child = el.firstChild) { fragment.append(child) } ``` **循环渲染** ```javascript fragment_compile(fragment) function fragment_compile(node) { const parttern = /\\{\\{\\s*(\\S+)\\s*\\}\\}/ // 文本节点 if (node.nodeType === 3) { // 匹配 {{}}, 第一项为匹配的内容, 第二项为匹配的变量名称 const match = parttern.exec(node.nodeValue) if (match) { const needChangeValue = node.nodeValue // 获取到匹配的内容, 可能是 msg, 也可能是 mmm.msg let arr = match[1].split('.') let value = arr.reduce( (total, current) => total[current], vm._data ) // 将真实的值替换掉模板字符串, 这个就是更新模板的方法, 将这个方法封装到 watcher 里面 node.nodeValue = needChangeValue.replace(parttern, value) const updateFn = value => { node.nodeValue = needChangeValue.replace(parttern, value) } // 有个问题, node.nodeValue 在执行过一次之后, 值就变了,不是 {{name}},而是真实值, 要将 {{name}} 里面的 name(即match[1]) 暂存起来 new Watcher(vm, match[1], updateFn) // 添加 input 事件 node.addEventListener('input', e => { const name = item.nodeValue // 给 vm 上的属性赋值 // 不能直接 vm._data[name] = e.target.value , 因为 name 可能是 a.b 的形式 // 也不能直接获取 b 的值, 然后赋新值, 因为这个值是一个值类型, 需要先获取前面的引用类型 const arr1 = name.split('.') const arr2 = arr1.slice(0, arr1.length - 1) const head = arr2.reduce((total, current) => total[current], vm._data) head[arr1[arr1.length - 1]] = e.target.value }) } return } // 元素节点 if (node.nodeType === 1 && node.nodeName === 'INPUT') { // 伪数组 const attrs = node.attributes let attr = Array.prototype.slice.call(attrs) // 里面有个 nodeName === v-model, 有个 nodeValue 对应 name attr.forEach(item => { if (item.nodeName === 'v-model') { let value = getVmValue(item.nodeValue, vm) // input 标签是修改 node.value node.value = value // 也需要添加 watcher new Watcher(vm, item.nodeValue, newValue => node.value = newValue) // 添加 input 事件 node.addEventListener('input', e => { const name = item.nodeValue // 给 vm 上的属性赋值 // 不能直接 vm._data[name] = e.target.value , 因为 name 可能是 a.b 的形式 // 也不能直接获取 b 的值, 然后赋新值, 因为这个值是一个值类型, 需要先获取前面的引用类型 const arr1 = name.split('.') const arr2 = arr1.slice(0, arr1.length - 1) const head = arr2.reduce((total, current) => total[current], vm._data) head[arr1[arr1.length - 1]] = e.target.value }) } }) } node.childNodes.forEach(child => fragment_compile(child)) } } ``` **设置值** ```javascript function getVmValue(key, vm) { return key.split('.').reduce((total, current) => total[current], vm._data) } function setVmValue(key, vm) { let tem = key.split('.') let fin = tem.reduce((total, current) => total[current], vm._data) return fin } ``` **注意事项** * 代码中使用了一些排版技巧,例如在数组中使用 `reduce` 来获取元素的值。 * 代码中使用了 `Watcher` 来管理模板中的事件。 * 代码中使用了 `vm` 对象来设置和获取模板的值。

正文

简介

vue数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。
实现方式:数据劫持 结合 发布者-订阅者 模式。
数据劫持通过 Object.defineProperty()方法。

对对象的劫持

  1. 构造一个监听器 Observer ,用来劫持并监听所有属性,添加到收集器 Dep 中,当数据发生变化的时候发出一个 notice(通知)。

  2. 添加一个发布者 Dep , 用来收集订阅者 Watcher 和发布更新通知 。

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

  3. 构造一个订阅者 Watcher ,一方面接收监听器 Observer 通过 Dep 传递过来的数据变化,一方面执行自身绑定的回调函数(update)进行界面更新。

  4. 实现一个解析器 Compile ,实现指令解析,初始化视图,并订阅数据变化,绑定好更新函数。

    值得注意的是,解析器在解析DOM的时候,是用的 DocumentFragment,它是一个文档片段接口,表示一个没有父对象的最小文档对象,它的变化不会触发DOM树的重新渲染,性能优于直接操作DOM树。

image

Dep 如何收集 Watcher

Watcher 在取值的时候让 Dep.target 指向 this 即当前的 watcher,取值结束之后,让 Dep.targetnull ,这样就可以通过属性的 get 方法里面将当前的 watcher 添加到属性里面的实例 dep 中。

// 通过获取操作, 触发属性里面的get方法
key.split('.').reduce((total, current) => total[current], vm._data)
// get 方法
get: function getter () {
  if (Dep.target) { // 在这里添加一个订阅者
    dep.addSub(Dep.target);
  }
  return val;
}

对数组的劫持

由于数组不能直接使用 Object.defineProperty() 方法,所以它是这样操作的:
创建一个空对象继承Array的原型,然后创建了一个数组方法拦截器,在拦截器内重写了操作数组的一些方法(主要有7个:pushpopshiftunshiftsplicesortreverse),当数组实例使用操作数组方法时,先触发拦截器中的方法使数组数据变成响应式数据。
数组数据变成响应式数据的方法,和前面对象的类似,主要是修改了监听器 Observer,增加对数组的依赖收集和递归监听子元素,最后遍历数组,在循环内部如果有对象则进入对象的监听,触发依赖更新和监测新增元素通过拦截器。

vue 双向绑定原理简单模拟

以上说明可结合vue源码或下面简单模拟进行理解
html部分

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>vue双向绑定原理</title>
  </head>
  <body>
    <div id="app">
      <input type="text" v-model="value">
      <div>{{value}}</div>
      <br>
      <input type="text" v-model="data.input">
      <div>{{data.input}}</div>
    </div>
  </body>
  <script src="js/vue.index.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data() {
        return {
          value: '输入框1',
          data: {
            input: '输入框2'
          }
        }
      }
    })
  </script>
</html>

js部分

class Vue {
  constructor(options) {
    this.$options = options
    const vm = this
    if (this.$options.data) {
      this.initData(vm)
    }
    if (this.$options.el) {
      compile(this.$options.el, vm)
    }
  }
  initData(vm) {
    let data = vm.$options.data
    data = typeof data === 'function' ? data.call(vm) : data
    vm._data = data
    observe(data)
    for (let key in data) {
      proxy(vm, key, data[key])
    }
  }
}

// 代理:实现 vm.name 可以直接访问 vm._data.name
function proxy(target, key, value) {
  Object.defineProperty(target, key, {
    get() {
      return target['_data'][key]
    },
    set(newValue) {
      target['_data'][key] = newValue
    }
  })
}

// 不是对象将被拦截
function observe(data) {
  if (data === null || typeof data !== 'object') {
    return
  }
  return new Observer(data)
}

// 监听器 这里只考虑对象了和对象嵌套, 没有考虑数组
class Observer {
  constructor(data) {
    this.walk(data)
  }
  walk(data) {
    // 遍历对象的每一项, 进行监听和劫持
    Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
  }
}

// 通过defineProperty监听对象的属性并且给属性收集依赖
function defineReactive(target, key, value) {
  // 递归监听属性值是对象的属性
  observe(value)
  // 添加Dep实例, 收集依赖
  let dep = new Dep()
  Object.defineProperty(target, key, {
    get() {
      Dep.target && dep.addSub(Dep.target)
      return value
    },
    set(newValue) {
      value = newValue
      // 给新值添加监听
      observe(newValue)
      // 修改值的时候通知订阅者Watcher去更新
      dep.notify()
    }
  })
}

// 收集器 - 依赖收集
class Dep {
  constructor() {
    // 里面装的是收集的watcher
    this.subs = []
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // 让收集到的所有watcher去更新
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

// 订阅者
class Watcher {
  constructor(vm, key, callback) {
    this.vm = vm
    this.key = key
    this.callback = callback
    // 让 Dep.target 属性指向当前 watcher 实例
    Dep.target = this
    // reduce 逐个遍历数组元素, 每一步都将当前元素的值与上一步的计算结果相加
    // 通过 reduce, 触发 defineReactive 的 get 方法, 从而让 Dep 收集到
    key.split('.').reduce((total, current) => total[current], vm._data)
    Dep.target = null
  }
  update() {
    const value = this.key.split('.').reduce((total, current) => total[current], this.vm._data)
    this.callback(value)
  }
}

/** 解析模板
​ 使用 documentFragment 创建模板, 注意 fragment.append 会让被插入的 child 节点从父节点中移除, while 循环结束后, 页面就没了
​ 然后对模板里面的每一项进行解析:
 先实例 node.nodeType === 3 的元素, 表示文本节点, 看文本节点里面有没有匹配到 {{name}} 模板表达式的, 
   如果有, 从 vm._data 里面去取对应的值, 替换文本的值, 最后 vm.$el.appendChild(fragment) 就可以将替换后的结果显示在页面上
​ 对 nodeType === 1 的元素, 即标签解析, 这里我们假设处理的是 input, 
   获取节点的所有属性, 一个伪数组, 变成真数组, 里面有个 nodeName === v-model 和 nodeValue 对应 name 的, 
   同样获取 vm._data 里面 name 的值, 然后让节点的 node.value = 这个值, 就能显示在输入框里面了, 这就是数据改变视图。
   接下来是视图改变数据, 添加 input 方法, 为 node 添加 addEventListener方法, input, 然后让 vm._data 里面对应属性的值等于 e.target.value, 这样就实现了视图改变数据。
​ 重点: 上面的两种情况, nodeType == 3 的时候更新方法是 node.nodeValue = newValue, nodeType == 1 的时候更新方法是 node.value = newValue, 
    需要将这两个方法封装到 watcher 中, 在更新之后 new 一个 Watcher, 并将对应的参数传入, 后面在获取值的时候就会自动收集依赖, set 值的时候就会触发更新。
 * @param {Object} el
 * @param {Object} vm
 */
function compile(el, vm) {
  vm.$el = el = document.querySelector(el)

  const fragment = document.createDocumentFragment()
  let child
  while (child = el.firstChild) {
    fragment.append(child)
  }

  fragment_compile(fragment)

  function fragment_compile(node) {
    const parttern = /\{\{\s*(\S+)\s*\}\}/
    // 文本节点
    if (node.nodeType === 3) {
      // 匹配 {{}}, 第一项为匹配的内容, 第二项为匹配的变量名称
      const match = parttern.exec(node.nodeValue)
      if (match) {
        const needChangeValue = node.nodeValue
        // 获取到匹配的内容, 可能是 msg,   也可能是 mmm.msg,
        // 注意通过 vm[mmm.msg] 是拿不到数据的, 要 vm[mmm][msg]
        // 获取真实的值, 替换掉模板里面的 {{name}}, 真实的值从 vm.$options.data 里面取
        let arr = match[1].split('.')
        let value = arr.reduce(
          (total, current) => total[current], vm._data
        )
        // 将真实的值替换掉模板字符串, 这个就是更新模板的方法, 将这个方法封装到 watcher 里面
        node.nodeValue = needChangeValue.replace(parttern, value)
        const updateFn = value => {
          node.nodeValue = needChangeValue.replace(parttern, value)
        }
        // 有个问题, node.nodeValue 在执行过一次之后, 值就变了, 不是 {{name}}, 而是真实值, 要将 {{name}} 里面的 name(即match[1]) 暂存起来
        new Watcher(vm, match[1], updateFn)
      }
      return
    }
    // 元素节点
    if (node.nodeType === 1 && node.nodeName === 'INPUT') {
      // 伪数组
      const attrs = node.attributes
      let attr = Array.prototype.slice.call(attrs)
      // 里面有个 nodeName === v-model,  有个 nodeValue 对应 name
      attr.forEach(item => {
        if (item.nodeName === 'v-model') {
          let value = getVmValue(item.nodeValue, vm)
          // input 标签是修改 node.value 
          node.value = value
          // 也需要添加 watcher
          new Watcher(vm, item.nodeValue, newValue => node.value = newValue)
          // 添加 input 事件
          node.addEventListener('input', e => {
            const name = item.nodeValue
            // 给 vm 上的属性赋值
            // 不能直接 vm._data[name] = e.target.value , 因为 name 可能是 a.b 的形式
            // 也不能直接获取 b 的值, 然后赋新值, 因为这个值是一个值类型, 需要先获取前面的引用类型
            // 如: let tem = vm._data.a, 然后 tem[b] = 新值,  这样就可以达到 vm._data.a.b = 新值的效果
            const arr1 = name.split('.')
            const arr2 = arr1.slice(0, arr1.length - 1)
            const head = arr2.reduce((total, current) => total[current], vm._data)
            head[arr1[arr1.length - 1]] = e.target.value
          })
        }
      })
    }
    node.childNodes.forEach(child => fragment_compile(child))
  }

  vm.$el.appendChild(fragment)
}

function getVmValue(key, vm) {
  return key.split('.').reduce((total, current) => total[current], vm._data)
}

function setVmValue(key, vm) {
  let tem = key.split('.')
  let fin = tem.reduce((total, current) => total[current], vm._data)
  return fin
}

window.Vue = Vue;

与Vue双向绑定原理梳理相似的内容:

Vue双向绑定原理梳理

简介 vue数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。 实现方式:数据劫持 结合 发布者-订阅者 模式。 数据劫持通过 Object.defineProperty()方法。 对对象的劫持 构造一个监听器 Observer ,用来劫持并监听所有属性,添加到收集器 Dep 中,当数据发生

手牵手带你实现mini-vue

Vue 的双向数据绑定实现原理是什么样的,如果让我们自己去实现一个这样的双向数据绑定要怎么做呢,本文就与大家分享一下 Vue 的绑定原理及其简单实现

Vue核心概念与其指令

Vue简述 Vue是一套构建用户UI界面的前端框架。 构建用户界面的意思是:往html中填充数据,框架的意思是:一套开发规范。 Vue的特点 1.数据驱动视图 当页面是一个普通的数据展示时,数据改变了,Vue会充当中间驱动者的角色,把视图更新为最新数据的展示。 2.双向数据绑定 当使用form表单时

在 Vue 中控制表单输入

Vue中v-model的思路很简单。定义一个可响应式的text(通常是一个ref),然后用v-model="text"将这个值绑定到一个input上。这就创造了一个双向的数据流: 用户在输入框中输入,text会发生变化。 text发生变化,输入框的值也随之变化。 让我们看看如何在Vue 3中使用v-

Vue框架快速上手

Vue基础 vue指令 内容绑定 v-text 设置标签的内容一般通过双大括号的表达式{{ }}去替换内容 {{ hello }} v-html 与v-text类似区别在于html中的结构会被解析为标签设置元素的innerHTML,v-text只会解析文本 事件绑定 v-on 可以简写为@,绑定的方

Vue3.0+typescript+Vite+Pinia+Element-plus搭建vue3框架!

使用 Vite 快速搭建脚手架 命令行选项直接指定项目名称和想要使用的模板,Vite + Vue 项目,运行(推荐使用yarn) # npm 6.x npm init vite@latest my-vue-app --template vue # npm 7+, 需要额外的双横线: npm init

VUE系列之性能优化--懒加载

一、懒加载的基本概念 懒加载是一种按需加载技术,即在用户需要时才加载相应的资源,而不是在页面初始加载时一次性加载所有资源。这样可以减少页面初始加载的资源量,提高页面加载速度和用户体验。 二、Vue 中的懒加载 在 Vue.js 中,懒加载主要用于路由组件的按需加载。Vue Router 提供了非常便

深入理解 Vue 3 组件通信

在 Vue 3 中,组件通信是一个关键的概念,它允许我们在组件之间传递数据和事件。本文将介绍几种常见的 Vue 3 组件通信方法,包括 props、emits、provide 和 inject、事件总线以及 Vuex 状态管理。 1. 使用 props 和 emits 进行父子组件通信 props

Vue 3 后端错误消息处理范例

前端如何存储处理后端返回的错误信息,并按不同来源绑定到页面,例如显示在不同输入框的周围。这样即可实现清晰的错误显示。

Vue - 入门

零:前端目前形势 前端的发展史 HTML(5)、CSS(3)、JavaScript(ES5、ES6):编写一个个的页面 -> 给后端(PHP、Python、Go、Java) -> 后端嵌入模板语法 -> 后端渲染完数据 -> 返回数据给前端 -> 在浏览器中查看 Ajax的出现 -> 后台发送异步请