Vue简单实现原理详解


Posted in Javascript onMay 07, 2020

本文实例讲述了Vue实现原理。分享给大家供大家参考,具体如下:

用了Vue也有两年时间了,一直以来都是只知其然,不知其所以然,为了能更好的使用Vue不被Vue所奴役,学习一下Vue底层的基本原理。

Vue官网有一段这样的介绍:当你把一个普通的JavaScript对象传给Vue实例的data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setterObject.definePropertyES5中一个无法shim的特性,这也就是为什么Vue不支持 IE8 以及更低版本浏览器。

通过这一段的介绍不难可以得出,Vue是通过Object.defineProperty对实例中的data数据做了挟持并且使用Object.definePropertygetter/setter并对其进行处理之后完成了数据的与视图的同步。

Vue简单实现原理详解

这张图应该不会很陌生,熟悉Vue的同学如果仔细阅读过Vue文档的话应该都看到过。猜想一下Vue使用Object.defineProperty做为ViewModel,对数据进行挟持之后如果ViewModel发生变化的话,就会通知其相对应引用的地方进行更新处理,完成视图的与数据的双向绑定。

下面举个例子:

html:

<div id="name"></div>

javaScript:

var obj = {};
Object.defineProperty(obj,"name",{
  get() {
    return document.querySelector("#name").innerHTML;
  },
  set(val) {
    document.querySelector("#name").innerHTML = val;
  }
})
obj.name = "Aaron";

通过上面的代码使用Object.definePropertyObj对象中的name属性进行了挟持,一旦该属性发生了变化则会触发set函数执行,做出响应的操作。

扯了这么多,具体说一下Vue实现的原理。

  1. 需要数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者。
  2. 需要指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
  3. 一个Watcher,作为连接ObserverCompile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
  4. MVVM入口函数,整合以上三者,实现数据响应。

Vue简单实现原理详解

接下来的文章将沿着这个思路一步一步向下进行,以便完成一个简单的Vue类,完成数据与视图的实时更新。

<div id="app">
  <p>{{name}}</p>
  <p q-text="name"></p>
  <p>{{age}}</p>
  <p>{{doubleAge}}</p>
  <input type="text" q-model="name"/>
  <button @click="changeName">点击</button>
  <div q-html="html"></div>
</div>
<script>
new QVue({
  el:"#app",
  data:{
    name:"I am test",
    age:12,
    html:"<button>这是一个后插入的按钮</button>"
  },
  created(){
    console.log("开始吧,QVue");
    setTimeout(() => {
      this.name = "测试数据,更改了么";
    },2000)
  },
  methods:{
    changeName(){
      this.name = "点击啦,改变吧";
      this.age = 1000000;
    }
  }
})
</script>

以上代码则是需要完成的功能,保证所有功能全部都能实现。

首先我们要考虑的是,要创建一个Vue的类,该类接收的是一个options的对象,也就是我们在实例化Vue的时候需要传递的参数。

class QVue {
  constructor(options){
    // 缓存options对象数据
    this.$options = options;
    // 取出data数据,做数据响应
    this.$data = options.data || {};
  }
}

通过上面的代码可以看出了,为什么我们可以在Vue实例上通过this.$data拿到我们所写的data数据。

对数据已经进行了缓存之后,接下来要做的事情就是对数据进行观察,达到数据变化之后能够做出对虚拟Dom的操作。

class QVue {
  constructor(options){
    this.$options = options;
    // 数据响应
    this.$data = options.data || {};
    // 监听数据变化
    this.observe(this.$data);
    // 主要用来解析各种指令,比如v-modal,v-on:click等指令
    new Compile(options.el,this);
    // 执行生命周期
    if(options.created){
      options.created.call(this);
    }
  }
  // 观察数据变化
  observe(value){
    if(!value || typeof value !== "object"){
      return;
    }
    let keys = Object.keys(value);
    keys.forEach((key)=> {
      this.defineReactive(value,key,value[key]);
      // 代理data中的属性到vue实例上
      this.proxyData(key);
    })
  }
  // 代理Data
  proxyData(key){
    Object.defineProperty(this,key,{
      get(){
        return this.$data[key];
      },
      set(newVal){
        this.$data[key] = newVal;
      }
    })
  }
  // 数据响应
  defineReactive(obj,key,val){
    // 解决数据层次嵌套
    this.observe(val);
    const dep = new Dep();
    Object.defineProperty(obj, key,{
      get(){
        // 向管理watcher的对象追加watcher实例
        // 方便管理
        Dep.target && dep.appDep(Dep.target);
        return val;
      },
      set(newVal){
        if(newVal === val){
          return;
        }
        val = newVal;
        // console.log(`${key}更新了:${newVal}`)
        dep.notify();
      }
    })
  }
}

我们对data数据中的每一项都进行了数据挟持,可是然而并没有什么卵用啊,我们并没有对相对应的虚拟dom进行数据改变,当然我们肯定是不能把我们的需要更改的虚拟dom操作写在这里,然而在Vue中对其Dom进行了特殊的处理,慢慢的向下看。

想要做数据响应要做一个做具体更新的类何以用来管理这些观察者的类

// 管理watcher
class Dep {
  constructor() {
    // 存储
    this.deps = [];
  }
  // 添加watcher
  appDep(dep){
    this.deps.push(dep);
  }
  // 通知所有的watcher进行更新
  notify(){
    this.deps.forEach((dep) => {
      dep.update();
    })
  }
}
// 观察者 做具体更新
class Watcher {
  constructor(vm,key,cb){
    // Vue实例
    this.vm = vm;
    // 需要更新的key
    this.key = key;
    // 更新后执行的函数
    this.cb = cb;
    // 将当前watcher实例指定到Dep静态属性target
    // 用来在类间进行通信
    Dep.target = this;
    // 触发getter,添加依赖
    this.vm[this.key];
    Dep.target = null;
  }
  update(){
    this.cb.call(this.vm,this.vm[this.key]);
  }
}

Dep.target = this上面这段代码一定要注意,是向Dep类中添加了一个静态属性。

主要用来解析各种指令,比如v-modalv-on:click等指令。然后将模版中的变量替换成数据,渲染view,将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据发生变动,收到通知,更新视图。

简单说下双向绑定,双向绑定原理,在编译的时候可以解析出v-model在做操作的时候,在使用v-model元素上添加了一个事件监听(input),把事件监听的回调函数作为事件监听的回调函数,如果input发生变化的时候把最新的值设置到vue的实例上,因为vue已经实现了数据的响应化,响应化的set函数会触发界面中所有依赖模块的更新,然后通知哪些model做依赖更新,所以界面中所有跟这个数据有管的东西就更新了。

class Compile {
  constructor(el,vm) {
    // 要遍历的宿主节点
    this.$el = document.querySelector(el);
    this.$vm = vm;

    // 编译
    if(this.$el){
      // 转换宿主节点内容为片段Fragment元素
      this.$fragment = this.node2Fragment(this.$el);
      // 执行编译过程
      this.compile(this.$fragment);
      // 将编译完的HTML结果追加至宿主节点中
      this.$el.appendChild(this.$fragment);
    }
  }

  // 将宿主元素中代码片段取出来,遍历,这样做比较高效
  node2Fragment(el){
    const frag = document.createDocumentFragment();
    // 将宿主元素中所有子元素**(搬家,搬家,搬家)**至frag中
    let child;
    // 如果 el.firstChild 为undefined或null则会停止循环
    while(child = el.firstChild){
      frag.appendChild(child);
    }
    return frag;
  }

  compile(el){
    // 宿主节点下的所有子元素
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach((node) => {
      if(this.isElement(node)){
        // 如果是元素
        console.log("编译元素"+node.nodeName)
        // 拿到元素上所有的执行,伪数组
        const nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach((attr) => {
          // 属性名
          const attrName = attr.name; 
          // 属性值
          const exp = attr.value;   
          // 如果是指令
          if(this.isDirective(attrName)){
            // q-text
            // 获取指令后面的内容
            const dir = attrName.substring(2);
            // 执行更新
            this[dir] && this[dir](node,this.$vm,exp);
          }
          // 如果是事件
          if(this.isEvent(attrName)){
            // 事件处理
            let dir = attrName.substring(1);  // @
            this.eventHandler(node,this.$vm,exp,dir);
          }
        })
      }else if(this.isInterpolation(node)){
        // 如果是插值文本
        this.compileText(node);
        console.log("编译文本"+node.textContent)
      }
      // 递归子元素,解决元素嵌套问题
      if(node.childNodes && node.childNodes.length){
        this.compile(node);
      }
    })
  }
  // 是否为节点
  isElement(node){
    return node.nodeType === 1;
  }
  // 是否为插值文本
  isInterpolation(node){
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }
  // 是否为指令
  isDirective(attr){
    return attr.indexOf("q-") == 0;
  }
  // 是否为事件
  isEvent(attr){
    return attr.indexOf("@") == 0;
  }

  // v-text
  text(node,vm,exp){
    this.update( node, vm, exp, "text");
  }
  textUpdater(node,value){
    node.textContent = value;
  }

  // 双向绑定
  // v-model
  model(node,vm,exp){
    // 指定input的value属性,模型到视图的绑定
    this.update(node,vm,exp,"model");
    // 试图对模型的响应
    node.addEventListener('input',(e) => {
      vm[exp] = e.target.value;
    })
  }
  modelUpdater(node,value){
    node.value = value;
  }

  // v-html
  html(node,vm,exp){
    this.update(node,vm,exp,"html")
  }
  htmlUpdater(node,value){
    node.innerHTML = value;
  }
  
  // 更新插值文本
  compileText(node){
    let key = RegExp.$1;
    this.update( node, this.$vm, key, "text");
  }
  // 事件处理器
  eventHandler(node,vm,exp,dir){
    let fn = vm.$options.methods && vm.$options.methods[exp];
    if(dir && fn){
      node.addEventListener(dir,fn.bind(vm));
    }
  }

  // 更新函数 - 桥接
  update(node,vm,exp,dir){
    const updateFn = this[`${dir}Updater`];
    // 初始化
    updateFn && updateFn(node,vm[exp]);
    // 依赖收集
    new Watcher(vm,exp,function(value){
      updateFn && updateFn(node,value);
    })
  }
}

其实Compile整个编译过程,就是在做一个依赖收集的工作,然Vue知道每一个指令是做什么的。并做出对应的更新处理。

Vue整体的编译过程,因为vue所编写的指令html无法进行识别,通过编译的过程可以进行依赖收集,依赖收集以后把data中的数据和视图进行了关联,产生了依赖关系,如果以后数据模型发生变化我们可以通过这些依赖通知这些视图进行更新,这是执行编译的目的,就可以做到数据模型驱动视图变化。

参考文章:

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.3water.com/code/HtmlJsRun测试上述代码运行效果。

希望本文所述对大家JavaScript程序设计有所帮助。

Javascript 相关文章推荐
举例讲解Node.js中的Writable对象
Jul 29 Javascript
Bootstrap按钮下拉菜单组件详解
May 10 Javascript
浅谈js图片前端预览之filereader和window.URL.createObjectURL
Jun 30 Javascript
有关suggest快速删除后仍然出现下拉列表的bug问题
Dec 02 Javascript
vue2组件之select2调用的示例代码
Oct 12 Javascript
vue 中filter的多种用法
Apr 26 Javascript
Node.js 使用axios读写influxDB的方法示例
Oct 26 Javascript
vue实现记事本功能
Jun 26 Javascript
Vue设置长时间未操作登录自动到期返回登录页
Jan 22 Javascript
node.js中module模块的功能理解与用法实例分析
Feb 14 Javascript
vue 实现在同一界面实现组件的动态添加和删除功能
Jun 16 Javascript
vue中echarts的用法及与elementui-select的协同绑定操作
Nov 17 Vue.js
angular组件间通讯的实现方法示例
May 07 #Javascript
基于better-scroll 实现歌词联动功能的代码
May 07 #Javascript
Vue双向绑定实现原理与方法详解
May 07 #Javascript
JavaScript设计模式之观察者模式与发布订阅模式详解
May 07 #Javascript
微信小程序pinker组件使用实现自动相减日期
May 07 #Javascript
简单了解JavaScript弹窗实现代码
May 07 #Javascript
angular组件间传值测试的方法详解
May 07 #Javascript
You might like
PHP实例分享判断客户端是否使用代理服务器及其匿名级别
2014/06/04 PHP
php中实现xml与mysql数据相互转换的方法
2014/12/25 PHP
Zend Framework教程之Loader以及PluginLoader用法详解
2016/03/09 PHP
php 流程控制switch的简单实例
2016/06/07 PHP
php中static和const关键字用法分析
2016/12/07 PHP
PHP设计模式(五)适配器模式Adapter实例详解【结构型】
2020/05/02 PHP
点击下载链接 弹出页面实现代码
2009/10/01 Javascript
基于jquery的获取浏览器窗口大小的代码
2011/03/28 Javascript
jquery交替变换颜色的三种方法 实例代码
2013/11/19 Javascript
将字符串中由空格隔开的每个单词首字母大写
2014/04/06 Javascript
使用javascript获取页面名称
2014/12/23 Javascript
js中函数声明与函数表达式
2015/06/03 Javascript
jquery实现Li滚动时滚动条自动添加样式的方法
2015/08/10 Javascript
jQuery侧边栏实现代码
2016/05/06 Javascript
JavaScript中解决多浏览器兼容性23个问题的快速解决方法
2016/05/19 Javascript
【经验总结】编写JavaScript代码时应遵循的14条规律
2016/06/20 Javascript
AngularJS 指令详细介绍
2016/07/27 Javascript
AngularJS 在同一个界面启动多个ng-app应用模块详解
2016/12/20 Javascript
浅谈箭头函数写法在ReactJs中的使用
2017/08/22 Javascript
JavaScript代码执行的先后顺序问题
2017/10/29 Javascript
Vue-cli 使用json server在本地模拟请求数据的示例代码
2017/11/02 Javascript
ReactNative之FlatList的具体使用方法
2017/11/29 Javascript
Vue.set()实现数据动态响应的方法
2018/02/07 Javascript
浅谈Vue响应式(数组变异方法)
2018/05/07 Javascript
微信小程序实现元素渐入渐出动画效果封装方法
2019/05/18 Javascript
微信小程序文章详情页跳转案例详解
2019/07/09 Javascript
解决微信小程序中的滚动穿透问题
2019/09/16 Javascript
JQuery实现ul中添加LI和删除指定的Li元素功能完整示例
2019/10/16 jQuery
Vue中keep-alive 实现后退不刷新并保持滚动位置
2020/03/17 Javascript
[30:37]【全国守擂赛】第三周擂主赛 Dark Knight vs. Leopard Gaming
2020/05/04 DOTA
使用anaconda安装pytorch的实现步骤
2020/09/03 Python
如何用用Python将地址标记在地图上
2021/02/07 Python
CSS3之2D与3D变换的实现方法
2019/01/28 HTML / CSS
什么是JNDI的上下文?如何初始化JNDI上下文
2012/03/10 面试题
商务英语专业应届毕业生求职信
2013/10/28 职场文书
部门经理助理岗位职责
2015/04/13 职场文书