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 学习笔记一
Apr 07 Javascript
jquery获取焦点和失去焦点事件代码
Apr 21 Javascript
jQuery拖动div、移动div、弹出层实现原理及示例
Apr 08 Javascript
jquery实现的图片点击滚动效果
Apr 29 Javascript
JS实现的倒计时效果实例(2则实例)
Dec 23 Javascript
js创建数组的简单方法
Jul 27 Javascript
浅谈vue引入css,less遇到的坑和解决方法
Jan 20 Javascript
JS实现遍历不规则多维数组的方法
Mar 21 Javascript
Vue props 单向数据流的实现
Nov 06 Javascript
JS实现的雪花飘落特效示例
Dec 03 Javascript
小程序实现可拖动的悬浮按钮
Sep 07 Javascript
详解webpack的文件监听实现(热更新)
Sep 11 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新手上路(十二)
2006/10/09 PHP
Yii2框架可逆加密简单实现方法
2017/08/25 PHP
thinkphp5框架实现的自定义扩展类操作示例
2019/05/16 PHP
jQuery的事件委托实例分析
2015/07/15 Javascript
jquery实现简单的二级导航下拉菜单效果
2015/09/07 Javascript
详谈jQuery.load()和Jsp的include的区别
2017/04/12 jQuery
JS实现移动端实时监听输入框变化的实例代码
2017/04/12 Javascript
xmlplus组件设计系列之列表(4)
2017/04/26 Javascript
第一个Vue插件从封装到发布
2017/11/22 Javascript
浅谈在不使用ssr的情况下解决Vue单页面SEO问题(2)
2018/11/08 Javascript
JQuery Ajax执行跨域请求数据的解决方案
2018/12/10 jQuery
JavaScript 扩展运算符用法实例小结【基于ES6】
2019/06/17 Javascript
js遍历详解(forEach, map, for, for...in, for...of)
2019/08/28 Javascript
JavaScript对象访问器Getter及Setter原理解析
2020/12/08 Javascript
vue 在单页面应用里使用二级套嵌路由
2020/12/19 Vue.js
[00:47]DOTA2荣耀之路6:天火,天火!
2018/05/30 DOTA
python模拟登录并且保持cookie的方法详解
2017/04/04 Python
python 中字典嵌套列表的方法
2018/07/03 Python
简单了解python反射机制的一些知识
2019/07/13 Python
Django MEDIA的配置及用法详解
2019/07/25 Python
Python 写了个新型冠状病毒疫情传播模拟程序
2020/02/14 Python
python中threading开启关闭线程操作
2020/05/02 Python
浅析Python中字符串的intern机制
2020/10/03 Python
HQhair美国/加拿大:英国化妆品、美容及美发产品商城
2019/04/15 全球购物
自考毕业生自我鉴定
2013/11/04 职场文书
小学生暑假感言
2014/02/06 职场文书
小学校长汇报材料
2014/08/20 职场文书
个人工作总结范文2014
2014/11/07 职场文书
高考升学宴答谢词
2015/01/20 职场文书
2015年节能减排工作总结
2015/05/14 职场文书
四年级语文教学反思
2016/03/03 职场文书
高效笔记技巧分享:学会这些让你不再困扰
2019/09/04 职场文书
python3 实现mysql数据库连接池的示例代码
2021/04/17 Python
Ajax 的初步实现(使用vscode+node.js+express框架)
2021/06/18 Javascript
python中的mysql数据库LIKE操作符详解
2021/07/01 MySQL
oracle索引总结
2021/09/25 Oracle