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 相关文章推荐
用javascript实现的仿Flash广告图片轮换效果
Apr 24 Javascript
动态加载图片路径 保持JavaScript控件的相对独立性
Sep 06 Javascript
js日期时间补零的小例子
Mar 05 Javascript
jquery.validate.js插件使用经验记录
Jul 02 Javascript
Node.js 服务器端应用开发框架 -- Hapi.js
Jul 29 Javascript
JS使用ajax从xml文件动态获取数据显示的方法
Mar 24 Javascript
使用jquery/js获取iframe父子级、同级获取元素的方法
Aug 05 Javascript
JavaScript通过改变文字透明度实现的文字闪烁效果实例
Apr 27 Javascript
React Native第三方平台分享的实例(Android,IOS双平台)
Aug 04 Javascript
React 组件转 Vue 组件的命令写法
Feb 28 Javascript
简单说说angular.json文件的使用
Oct 29 Javascript
VUE UPLOAD 通过ACTION返回上传结果操作
Sep 07 Javascript
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 EOT定界符的使用详解
2008/09/30 PHP
PHP 批量删除数据的方法分析
2009/10/30 PHP
php利用scws实现mysql全文搜索功能的方法
2014/12/25 PHP
Js中sort()方法的用法
2006/11/04 Javascript
学习ExtJS accordion布局
2009/10/08 Javascript
从零开始学习jQuery (六) jquery中的AJAX使用
2011/02/23 Javascript
DB.ASP 用Javascript写ASP很灵活很好用很easy
2011/07/31 Javascript
jquery validate在ie8下的bug解决方法
2013/11/13 Javascript
JQEasy-ui在IE9以下版本中二次加载的问题分析及处理方法
2014/06/23 Javascript
javascript中字符串拼接详解
2014/09/26 Javascript
jQuery中html()方法用法实例
2014/12/25 Javascript
Nodejs学习笔记之入门篇
2015/04/16 NodeJs
jQuery文字横向滚动效果的实现代码
2016/05/31 Javascript
JavaScript 拖拽实例代码
2016/09/21 Javascript
简单实现jQuery多选框功能
2017/01/09 Javascript
微信小程序实现多个按钮toggle功能的实例
2017/06/13 Javascript
react.js组件实现拖拽复制和可排序的示例代码
2018/08/20 Javascript
详解mpvue小程序中怎么引入iconfont字体图标
2018/10/01 Javascript
Python程序中使用SQLAlchemy时出现乱码的解决方案
2015/04/24 Python
python中assert用法实例分析
2015/04/30 Python
RC4文件加密的python实现方法
2015/06/30 Python
Python3中的真除和Floor除法用法分析
2016/03/16 Python
判断Threading.start新线程是否执行完毕的实例
2020/05/02 Python
python中加背景音乐如何操作
2020/07/19 Python
详解使用postMessage解决iframe跨域通信问题
2019/11/01 HTML / CSS
使用html2canvas实现将html内容写入到canvas中生成图片
2020/01/03 HTML / CSS
巴西最大的在线约会网站:ParPerfeito
2018/07/11 全球购物
戴森英国官网:Dyson英国
2019/05/07 全球购物
教师考核材料
2014/05/21 职场文书
安全标兵事迹材料
2014/08/17 职场文书
2015年学校减负工作总结
2015/05/19 职场文书
python基于OpenCV模板匹配识别图片中的数字
2021/03/31 Python
PyTorch 如何设置随机数种子使结果可复现
2021/05/12 Python
JavaScript 与 TypeScript之间的联系
2021/11/27 Javascript
css3中2D转换之有趣的transform形变效果
2022/02/24 HTML / CSS
浅谈Redis缓冲区机制
2022/06/05 Redis