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 相关文章推荐
jquery 插件 web2.0分格的分页脚本,可用于ajax无刷新分页
Dec 25 Javascript
用js正确判断用户名cookie是否存在的方法
Jan 28 Javascript
javascript简单实现跟随滚动条漂浮的返回顶部按钮效果
Aug 19 Javascript
AngularJS 在同一个界面启动多个ng-app应用模块详解
Dec 20 Javascript
node.js实现登录注册页面
Apr 08 Javascript
bootstrap confirmation按钮提示组件使用详解
Aug 22 Javascript
详解React 的几种条件渲染以及选择
Oct 23 Javascript
Vue指令v-for遍历输出JavaScript数组及json对象的常见方式小结
Feb 11 Javascript
vue+eslint+vscode配置教程
Aug 09 Javascript
浅谈vue项目用到的mock数据接口的两种方式
Oct 09 Javascript
jQuery 图片查看器插件 Viewer.js用法简单示例
Apr 04 jQuery
JS严格模式原理与用法实例分析
Apr 27 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扩展开发经验分享
2012/09/06 PHP
php简单计算年龄的方法(周岁与虚岁)
2016/12/06 PHP
利用php生成验证码
2017/02/23 PHP
JS+CSS实现带关闭按钮DIV弹出窗口的方法
2015/02/27 Javascript
js实现同一页面多个运动效果的方法
2015/04/10 Javascript
JavaScript实现瀑布流布局
2020/06/28 Javascript
学习JavaScript事件流和事件处理程序
2016/01/25 Javascript
通过隐藏iframe实现无刷新上传文件操作
2016/03/16 Javascript
JS实现兼容各种浏览器的获取选择文本的方法【测试可用】
2016/06/21 Javascript
Bootstrap实现弹性搜索框
2016/07/11 Javascript
在微信、支付宝、百度钱包实现点击返回按钮关闭当前页面和窗口的方法
2016/08/05 Javascript
JavaScript学习笔记整理_setTimeout的应用
2016/09/19 Javascript
使用bootstrap validator的remote验证代码经验分享(推荐)
2016/09/21 Javascript
JS改变页面颜色源码分享
2018/02/24 Javascript
详解vuex中action何时完成以及如何正确调用dispatch的思考
2019/01/21 Javascript
Python中使用bidict模块双向字典结构的奇技淫巧
2016/07/12 Python
Python实现批量读取图片并存入mongodb数据库的方法示例
2018/04/02 Python
python smtplib模块实现发送邮件带附件sendmail
2018/05/22 Python
使用python判断jpeg图片的完整性实例
2019/06/10 Python
python如何实现异步调用函数执行
2019/07/08 Python
Python 仅获取响应头, 不获取实体的实例
2019/08/21 Python
PyQt5中多线程模块QThread使用方法的实现
2020/01/31 Python
Python迭代器Iterable判断方法解析
2020/03/16 Python
在 Windows 下搭建高效的 django 开发环境的详细教程
2020/07/27 Python
Django项目创建及管理实现流程详解
2020/10/13 Python
python 爬虫请求模块requests详解
2020/12/04 Python
css图标制作教程制作云图标
2014/01/19 HTML / CSS
中国汽车租赁行业头部企业:一嗨租车
2019/05/16 全球购物
建筑工程专业毕业生自荐信
2013/10/19 职场文书
新学期家长寄语
2014/01/19 职场文书
行政人事经理职位说明书
2014/03/05 职场文书
铅球加油稿100字
2014/09/26 职场文书
2015年纪念“卢沟桥事变”78周年活动方案
2015/05/06 职场文书
学历证明样本
2015/06/16 职场文书
pytorch显存一直变大的解决方案
2021/04/08 Python
Python爬虫 简单介绍一下Xpath及使用
2022/04/26 Python