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从头学起第三讲
Jul 06 Javascript
使用AngularJS编写较为优美的JavaScript代码指南
Jun 19 Javascript
js省市联动效果完整实例代码
Dec 09 Javascript
让浏览器崩溃的12行JS代码(DoS攻击分析及防御)
Oct 10 Javascript
JavaScript日期选择功能示例
Jan 16 Javascript
原生javascript实现分页效果
Apr 21 Javascript
解决淘宝cnpm 安装后cnpm不是内部或外部命令的问题
May 17 Javascript
如何为vuex实现带参数的 getter和state.commit
Jan 04 Javascript
详解vue在项目中使用百度地图
Mar 26 Javascript
今天,小程序正式支持 SVG
Apr 20 Javascript
vuejs移动端实现div拖拽移动
Jul 25 Javascript
ant design的table组件实现全选功能以及自定义分页
Nov 17 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 mcrypt可逆加密算法分析
2011/07/19 PHP
PHP迭代器实现斐波纳契数列的函数
2013/11/12 PHP
thinkPHP简单遍历数组方法分析
2016/05/16 PHP
PHP读取目录树的实现方法分析
2019/03/22 PHP
jQuery ctrl+Enter shift+Enter实现代码
2010/02/07 Javascript
js location.replace与location.reload的区别
2010/09/08 Javascript
深入理解javaScript中的事件驱动
2013/05/21 Javascript
jQuery判断元素是否存在的可靠方法
2014/05/06 Javascript
AngularJS控制器之间的数据共享及通信详解
2016/08/01 Javascript
PHP捕捉异常中断的方法
2016/10/24 Javascript
Javascript中字符串replace方法的第二个参数探究
2016/12/05 Javascript
EasyUI修改DateBox和DateTimeBox的默认日期格式示例
2017/01/18 Javascript
完美的js图片轮换效果
2017/02/05 Javascript
React.js中常用的ES6写法总结(推荐)
2017/05/09 Javascript
详解js模板引擎art template数组渲染的方法
2018/10/09 Javascript
微信小程序dom操作的替代思路实例分析
2018/12/06 Javascript
JavaScript对象的特性与实践应用深入详解
2018/12/30 Javascript
微信小程序的开发范式BeautyWe.js入门详解
2019/07/10 Javascript
微信小程序自定义yPicker组件实现省市区三级联动功能
2020/10/29 Javascript
python通过正则查找微博@(at)用户的方法
2015/03/13 Python
python爬虫面试宝典(常见问题)
2018/03/02 Python
python中scikit-learn机器代码实例
2018/08/05 Python
10个Python面试常问的问题(小结)
2019/11/20 Python
python3爬虫中多线程进行解锁操作实例
2020/11/25 Python
护士进修自我鉴定
2014/02/07 职场文书
党员实事承诺书
2014/03/26 职场文书
小区推广策划方案
2014/06/06 职场文书
财务管理专业自荐书
2014/09/02 职场文书
2014年社区工会工作总结
2014/12/18 职场文书
欢送会主持词
2015/07/01 职场文书
银行文明优质服务培训心得体会
2016/01/09 职场文书
《少年闰土》教学反思
2016/02/18 职场文书
matlab xlabel位置的设置方式
2021/05/21 Python
Python多线程 Queue 模块常见用法
2021/07/04 Python
解决mysql的int型主键自增问题
2021/07/15 MySQL
Python 一键获取电脑浏览器的账号密码
2022/05/11 Python