手牵手带你实现mini-vue

手牵手,实现,mini,vue · 浏览次数 : 320

小编点评

**4.1 实现思路** 1)创建一个订阅者,其 update 事件就是接收到我们更新后的数据值然后去更新 dom,因为要更新 dom,所以此订阅者是在 compile 中定义的,并且大家会发现我们在编译过程中,是循环每一层节点去判断的,也就意味着我们页面有多少个符合条件的文本节点,就会新建多少个 watcher,那这时就需要把文本节点的对应 key 和 value 传入 watcher 中,用来判断更新的哪个节点值2) 2)当 watcher 定义好后,还需要修改下其 update 方法,因为我们的 watcher 第三个参数也就是回调函数中新增了参数,需要给其传参Watcher.prototype.update = function() { // this.exp 可取到 key 值 从 vm 中凭借 key 就可以取到属性值 let val = this.vm const arr = this.exp.split('.') arr.forEach(k => (val = val[k])) this.fn(val) // 传入newVal}4) **4.2 优缺点** **优点:** * 成功达到了数据和视图的双向驱动 * 像在操作表单时使用会更方便 *省略了给 dom 添加值的操作 *代码量会更少,更方便维护 **缺点:** *修改数据时会使得我们无法追踪数据的改变源头 *在数据劫持那步需要去循环,为一个对象的每一个属性增加劫持 **4.3 小结** *在工作中我们很多项目会用到框架 ,了解它的一些原理有助于我们更好的去使用,便于我们培养自己的‘造轮子’能力,遇到问题时能更好的解决,减少不必要的 bug,更好的去调试代码

正文

1 前言

随着 Vue、React、Angularjs 等框架的诞生,数据驱动视图的理念也深入人心,就 Vue 来说,它拥有着双向数据绑定、虚拟dom、组件化、视图与数据相分离等等造福程序员的优点,那 Vue 的双向数据绑定实现原理是什么样的,如果让我们自己去实现一个这样的双向数据绑定要怎么做呢,本文就与大家分享一下 Vue 的绑定原理及其简单实现

2 核心技术

大家都知道 Vue2 双向绑定是基于 ES5的 Object.defineProperty 方法+发布订阅者模式实现的 那我们首先简单了解一下这两个模块都是做什么的,在 Vue 中充当了什么角色

2.1 Object.defineProperty

用来在对象上定义或者修改一个属性值,实现数据劫持,为修改数据后去调用视图更新做准备

  const obj = {}
  let age = 18
  Object.defineProperty(obj, 'age',{
  get() {
      return age
   },
    set(newVal) {
     age = newVal + 1
    },
    enumerable: true
   })
 console.log(obj.age) // 18
 obj.age = 20
    console.log(obj.age) // 21

2.2 发布订阅者模式

此模式简单来讲就是分为发布和订阅两个概念,订阅意思就是我们会定义很多个订阅者,每个订阅者都会有自己的 update 方法,把需要更新的订阅者放到数组中,而发布就代表通知订阅者去依次执行其 update 方法,从而实现数据更新

// 定义放订阅者的数组
function Dep() {
  this.subs = []
}
// 定义存放订阅者的方法
Dep.prototype.addSub = function(sub) {
  this.subs.push(sub)
}
// 定义发布的方法
Dep.prototype.notify = function(sub) {
  this.subs.forEach(sub => {
    // 依次通知订阅者去执行update方法
    sub.update()
  })
}

写到这里我们就把发布和订阅准备好了,但是还缺少订阅者,且订阅者要保证提供一个 update 方法才行,那我们不禁想到能否去创建一个构造函数,通过这个构造函数创建的实例都会有 update 方法呢

function Watcher(fn) {
  this.fn = fn
}
// 通过该构造函数创建的实例都会有update方法
Watcher.prototype.update = function() {
  this.fn()
}
// new实例
const watcher1 = new Watcher(() => console.log('我是watcher1'))
const watcher2 = new Watcher(() => console.log('我是watcher2'))
const dep = new Dep()
// 把准备好的事件放入到数组中
dep.addSub(watcher1)
dep.addSub(watcher2)
// 进行发布
dep.notify()
// 最终输出 我是watcher1 我是watcher2

3 具体实现

3.1 初始化

一个框架都是从它的初始化开始的,Vue 也不例外

<body>
    <div id="app">
      <p>a value: {{a.a}}</p>
      <div>b value: {{b}}</div>
      <span>v-model: </span><input type="text" v-model="b">
    </div>
    <script>
      // 模仿 Vue 的初始化和传入的参数
      let vue = new Vue({
        el: '#app',
        data: {
          a: { a: 'is a' },
          b: 'is b',
          c: 'is c'
        }
      })
    </script>
  </body>

3.2 数据劫持 observe

Vue 中在 data 中定义的属性才可以实现双向绑定,为了实现这个功能,我们定义一个 Observe 用来劫持到对象的属性

// 给对象增加数据劫持
function Observe(data) {
  // 因 defineProperty 每次只能设置单个属性 所以需遍历
  for (let key in data) {
    let val = data[key]
    observe(val)
    Object.defineProperty(data, key, {
      enumerable: true,
      get() {
        return val
      },
      set(newVal) {
        if (newVal === val) return // 新值与旧值相等时 不做处理
        val = newVal // 之所以给 val 赋值 是因为取值时取得val
        observe(newVal) // 当给变量值赋予一个新对象时 依然需要劫持到其属性
      }
    })
  }
}
function observe(data) {
  if (typeof data !== 'object') return
  return new Observe(data)
}

3.3 构造函数编写

 function Vue(options = {}) {
  //  模仿 Vue 把属性挂载到 $options 且可以通过this._data访问属性
  this.$options = options
  const data = this._data = this.$options.data
  observe(data) // 给 data 增加数据劫持
}

上图中我们模仿 Vue 对 options 和 data 增加了一些可访问方式,给 data 增加了数据劫持,也在我们的实例中看到了效果,那这时又有了新的问题,Vue 中访问数据都是 this.xxx 可直接通过实例访问,这样比我们图中的访问方式还要更方便一些,那我们也能否把属性直接挂载到实例上呢,当然是可以的

3.4 数据代理

如果想要直接把属性挂载到实例上,那我们需要保证通过实例直接访问的属性值是实时无误的,且去修改该属性值还能够被劫持到,否则会影响后面的双向数据绑定,既然 data 中的数据我们已经通过 Observe(在3.2节)做了劫持,那我们在通过 this.xxx 直接修改属性时只需要去修改 data 对应中的属性就可以触发 Observe 劫持

 function Vue(options = {}) {
  //  模仿 Vue 把属性挂载到 $options
  this.$options = options
  const data = this._data = this.$options.data
  observe(data)
  // 将当前 this 传入方法 将属性挂载到 this 上
  proxyData.call(this, data)
}

// this 代理 this._data
function proxyData(data) {
  const vm = this
  for (let key in data) {
    Object.defineProperty(vm, key, {
      enumerable: true,
      get() {
        return vm._data[key]
      },
      set(newVal) {
// 直接修改 data 中对应属性 触发 data 中劫持 保持数据统一
        vm._data[key] = newVal 
      }
    })
  }
}

3.5 实现 compile

先在内存中创建一个文档碎片来递归所有 dom 节点,用正则匹配 {{}} 相符的节点,获取到括号里的 key,最后在 data 中拿到对应 key 的属性值,替换到节点上(因为主要是实现双向绑定,所以我们将 dom 的操作放到文档碎片中操作来代替虚拟 dom)

function Compile(el, vm) {
  vm.$el = document.querySelector(el)
  // 建立文档碎片 将 el 下的所有元素挪进文档碎片 避免死循环
  const fragment = document.createDocumentFragment()
  // 将 el 中的元素都移入碎片中
  while (child = vm.$el.firstChild) {
    fragment.appendChild(child)
  }
  // 匹配节点中的{{}} 将其替换为对应的值
  replace(fragment)
  function replace(fragment) {
    // 循环每一层节点
    Array.from(fragment.childNodes).forEach(node => {
      const text = node.textContent
// 定义正则表达式
      const reg = /{{(.*)}}/
      // 此判断为当节点是文本节点(因为变量都是文本)且被包含在{{}}中的文本节点时
// 文本节点 Node.TEXT_NODE: 3
      if (node.nodeType === Node.TEXT_NODE && reg.test(text)) {
// 以下三行为了获取到 key 对应的value值 页面初始化后正常将变量替换为值
        const arr = RegExp.$1.split('.') // [a, a]  [b]
        let val = vm
        arr.forEach(k => (val = val[k]))
        node.textContent = text.replace(reg, val)
      }
      if (node.childNodes) {
        replace(node)
      }
    })
  }
  // 将处理好的文档碎片塞回dom中
  vm.$el.appendChild(fragment)
}

初始化 Vue 时调用 compile

 function Vue(options = {}) {
  //  模仿 Vue 把属性挂载到 $options
  this.$options = options
  const data = this._data = this.$options.data
  observe(data)
  // 将当前 this 传入方法 将属性挂载到 this 上
  proxyData.call(this, data)
  new Compile(options.el, this)
}

3.6 Model -> ViewModel -> View

目前我们已经实现的功能:数据劫持、this代理、编译模板 ,最终我们要达到修改数据、视图自动更新的效果,还需要以下工作

1)第一步我们需要创建一个订阅者,其 update 事件就是接收到我们更新后的数据值然后去更新 dom, 因为要更新 dom,所以此订阅者是在 compile 中定义的,并且大家会发现我们在编译过程中,是循环每一层节点去判断的,也就意味着我们页面有多少个符合条件的文本节点,就会新建多少个 watcher,那这时就需要把文本节点的对应 key 和 value 传入 watcher 中,用来判断更新的哪个节点值

2)既然我们的 watcher 新增了参数(vue 实例、节点变量)所以我们需要对 watcher 方法做出更改

3)当 watcher 定义好后,还需要修改下其 update 方法,因为我们的 watcher 第三个参数也就是回调函数中新增了参数,需要给其传参

Watcher.prototype.update = function() {
  // this.exp 可取到 key 值 从 vm 中凭借 key 就可以取到属性值
  let val = this.vm
  const arr = this.exp.split('.')
  arr.forEach(k => (val = val[k]))
  this.fn(val) // 传入 newVal
}

4)订阅者都准备好了,还需要添加订阅者到 dep 数组并且在数据改动后调用发布,这个过程需要在 observe 中实现

5)最终效果如图所示

3.7 View -> ViewModel -> Model

上面我们实现了从数据到视图的更新,那视图从数据的更新呢,首先我们想到一个最常见的例子(v-model), 要想实现它,我们需要做以下两步

1)把 value 值展示到绑定 v-model 的 input 中

2)其次是每次我们更改值时都应该把值更新到界面上,所以我们还需要新建一个 watcher,在绑定 v-model 的 input 上绑定事件,当输入文案时获取到输入的值,改变 data 只对应的属性值

3)最终效果如图所示

4 总结

4.1 实现思路

4.2 优缺点

优点:成功达到了数据和视图的双向驱动,像在操作表单时使用会更方便,省略了很多重复的 onChange 事件去处理数据的变化,也省略了给 dom 添加值的操作,代码量会更少,更方便维护

缺点:修改数据时会使得我们无法追踪数据的改变源头,且在数据劫持那步需要去循环,为一个对象的每一个属性增加劫持,无法直接在一个对象上增加所有属性的劫持(该缺点 Vue3 已规避,大家可自行学习)

4.3 小结

在工作中我们很多项目会用到框架 ,了解它的一些原理有助于我们更好的去使用,便于我们培养自己的‘造轮子’能力,遇到问题时能更好的解决,减少不必要的 bug,更好的去调试代码,一些很复杂的组件如果找不到开源的话,自己也能去实现不至于一头雾水

4.4 参考资料

5 思考

至此,一个双向数据绑定功能就基本实现了,本文我们的实现是基于 Vue2 的双向数据绑定原理,目前 Vue3 已经趋于稳定,我们可以思考下,如果是基于 Vue3 的原理去做,那需要怎么去实现呢。

作者:京东物流 张婷婷

来源:京东云开发者社区

与手牵手带你实现mini-vue相似的内容:

手牵手带你实现mini-vue

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

6个实例带你解读TinyVue 组件库跨框架技术

本文分享自华为云社区《6个实例带你解读TinyVue 组件库跨框架技术》,作者: 华为云社区精选。 在DTSE Tech Talk 《 手把手教你实现mini版TinyVue组件库 》的主题直播中,华为云前端开发DTSE技术布道师阿健老师给开发者们展开了组件库跨框架的讨论,同时针对TinyVue组件

Go-Zero从0到1实现微服务项目开发(二)

继续更新GoZero微服务实战系列文章:上一篇被GoZero作者万总点赞了,本文将继续使用 Go-zero 提供的工具和组件,从零开始逐步构建一个基本的微服务项目。手把手带你完成:项目初始化+需求分析+表结构设计+api+rpc+goctl+apifox调试+细节处理。带你实现一个完整微服务的开发。

手把手带你使用JWT实现单点登录

JWT(英文全名:JSON Web Token)是目前最流行的跨域身份验证解决方案之一,今天我们一起来揭开它神秘的面纱! 一、故事起源 说起 JWT,我们先来谈一谈基于传统session认证的方案以及瓶颈。 传统session交互流程,如下图: 当浏览器向服务器发送登录请求时,验证通过之后,会将用户

手把手带你搞定用户权限控制

在实际的软件项目开发过程中,用户权限控制可以说是所有运营系统中必不可少的一个重点功能,根据业务的复杂度,设计的时候可深可浅,但无论怎么变化,设计的思路基本都是围绕着用户、角色、菜单这三个部分展开。 如何设计一套可以精确到按钮级别的用户权限功能呢? 今天通过这篇文章一起来了解一下相关的实现逻辑,不多说

AI实战 | 手把手带你打造校园生活助手

在文章中,我展示了手把手的教程和小雨校园生活助手的功能。我强调了插件开发的重要性,以及数据库和变量的使用。工作流的使用也得到了详细解释,包括节假日信息整合和课程查询。最后,我分享了我的开场白生成方法,强调了前期调试的重要性。通过这篇文章,希望大家能够更深入地了解扣子助手的功能和实现方式。我将继续努力...

手把手带你通过API创建一个loT边缘应用

摘要:使用API Arts&API Explorer调用IoT边缘服务接口创建应用,了解边缘计算在物联网行业的应用。 本文分享自华为云社区《使用API Arts&API Explorer调用IoT边缘服务接口创建应用》,作者:华为IoT云服务。 开始体验前需注册华为云账号并完成实名认证,实验过程中请

Go-Zero定义API实战:探索API语法规范与最佳实践(五)

前言 上一篇文章带你实现了Go-Zero模板定制化,本文将继续分享如何使用GO-ZERO进行业务开发。 通过编写API层,我们能够对外进行接口的暴露,因此学习规范的API层编写姿势是很重要的。 通过本文的分享,你将能够学习到Go-Zero的API语法规范,以及学会实际上手使用。 概述 下文所说的是

带你揭开神秘的javascript AST面纱之AST 基础与功能

在前端里面有一个很重要的概念,也是最原子化的内容,就是 AST ,几乎所有的框架,都是基于 AST 进行改造运行,比如:React / Vue /Taro 等等。 多端的运行使用,都离不开 AST 这个概念。在大家理解相关原理和背景后,我们可以通过手写简单的编译器,简单实现一个 Javascript 的代码编译器,编译后在浏览器端正常运行。

8000字详解Thread Pool Executor

摘要:Java是如何实现和管理线程池的? 本文分享自华为云社区《JUC线程池: ThreadPoolExecutor详解》,作者:龙哥手记 。 带着大厂的面试问题去理解 提示 请带着这些问题继续后文,会很大程度上帮助你更好的理解相关知识点。@pdai 为什么要有线程池? Java是实现和管理线程池有