精华内容
下载资源
问答
  • Vue响应式原理

    2021-01-07 13:48:56
    Vue响应式原理 // 源对象 let data = { name: hellow, pwd: world } // 委托对象 let $data = {}; Object.keys(data).forEach(key => { def($data, key); $data[key] = data[key]; }); // 监听属性 ...
  • vue响应式原理

    2020-04-04 16:17:12
    深入讲解Vue响应式原理 摘要:采用通俗易懂的方式,讲解Vue响应式原理 Vue响应式原理 ​ 什么是Vue的响应式?简单来说就是,当你修改数据时,页面会重新渲染,即时更新数据。要搞懂Vue响应式原理,需要弄懂三个...

    深入讲解Vue响应式原理

    摘要:采用通俗易懂的方式,讲解Vue响应式原理

    Vue响应式原理

    ​ 什么是Vue的响应式?简单来说就是,当你修改数据时,页面会重新渲染,即时更新数据。要搞懂Vue响应式原理,需要弄懂三个问题:

    • 如何知道哪些数据发生了变化?

      数据劫持

      在生成Vue实例实例时,会对属性data进行遍历,使用 Object.defineProperty 把这些属性转为 getter/setter ,来对数据追踪变化。

    • 当数据发生变化时,如何通知?(发布订阅模式)

      发布订阅模式

      当数据更改的时候,会调用data中数据的setter方法,在数据变更的时候, 通知notity订阅者,通过watcher更新视图。

    • 通知哪些使用该数据的地方进行更新?(收集依赖)

      收集依赖

      当页面渲染,获取数据的时候,会调用data中数据的getter方法,在getter方法中收集依赖(addSub())。所有的依赖信息,存储到Dep对象,即订阅者中。

    Vue响应式图解

    在这里插入图片描述

    Vue响应式——代码解读

    class Vue{
        constructor(options){
            // 保存数据
            this.$options = options;
            this.$data = options.data;
            this.$el = options.el;
            
            // 将data添加到响应式系统中
            new Observer(this.$data);
            
            // 代理this.$data的数据
            Object.keys(this.$data).foreach(key => {
                this._proxy(key)
            })
            
            // 处理el
            new Compiler(this.$el, this)
        }
        _proxy(key){
            Object.defineProperty(this, key,{
                configurable:true,
                enumerable:true,
                set(newValue){
                    this.$data[key] = newValue
                },
                get(){
                    return this.$data[key]
                }
            })
        }
       
        class Observer{
            constructor(data){
                this.data = data;
                // 遍历data中的数据
                Object.keys(data).forEach(key => {
                    this.defineReactive(this.data, key, data[key])
                })
            }
            // 定义响应式,添加get和set方法
            defineReactive(data,key,val){
                //一个属性对应一个Dep对象
                const dep = new Dep()
                Object.defineProperty(data, key, {
                    configurable:true,
                    enumerable:true,
                    set(newValue){
                        if(newValue === val)
                            return
                        val = newValue
                        //如何不一样,通知订阅者
                        dep.notify()
                    },
                    get(){
                        if(Dep.target){
                            //添加订阅者
                            dep.addSub(Dep.target)
                        }
                        return val
                    }
                })
            }
        }
    	
    	// 依赖者类,订阅者
    	class Dep{
            constructor(){
                this.subs = []
            }
            
            addSub(sub){
                this.subs.push(sub)
            }
            
            notity(){
                //遍历所有的订阅者,通知更新
                this.subs.forEach(sub => {
                    sub.update()
                })
            }
        }
    	
    	class Watcher{
            constructor(node, name, vm){
                constructor(node, name, vm){
                    this.node = node
                    this.name = name
                    this.vm = vm
                    Dep.target = this;
                    this.update()
                    Dep.target =  null  
                    //置空,防止每次调用get的时候重复添加订阅者
                }
            }
            
            update(){
                this.node.nodeValue = this.vm[this.name]
            }
        }
    	// 文本节点{{}}匹配正则表达式
    	// . 匹配任何内容(除了特殊字符) * 0个或多个  + 1个或多个
    	const reg = /\{\{(.+)\}\}/
    	class Compiler{
            // el:app    vm:Vue
            constructor(el, vm){
                this.el = document.querySelector(el)
                this.vm = vm
                
                this.frag = this._createFragment()
                this.el.appendChild(this.frag)
            }
            _createFragment(){
                const frag = document.createDocumentFragment()
                let child
                // 遍历 this.el中的每一个子节点
                while(child = this.el.firstChild){
                    this._compile(child)  //编译每一个node节点
                    frag.appendChild(child)
                }
                return frag
            }
            
            _compile(node){
                //nodeType = 1 标签节点 
                if(node.nodeType === 1){
                    const attrs = node.attributes
                    if(attrs.hasOwnProperty('v-model')){
                        const name = attrs['v-model'].nodeValue
                        node.addEventListener('input', e => {
                            this.vm[name] = e.target.value
                        })
                    }
                }
                //nodeType = 3 文本节点
                if(node.nodeType === 3){
                    if(reg.test(node.nodeValue)){
                        const name = RegExp.$1.trim()
                        new Watcher(node,name,this.vm)
                    }
                }
            }
        }
    
    	
    }
    
    const app = new Vue({
        el:'#app',
        data:{
            message:'你好啊'
        }
    })
    
    展开全文
  • VUE响应式原理

    2020-06-17 22:52:57
    VUE响应式原理 一、vue2的数据响应式原理 1. 什么是defineProperty 其实是定义对象的属性,并不是核心的微一个对象做数据双向绑定,而是去给对象做属性标签,只不过属性里的get和set实现了响应式。 属性名 ...

    VUE响应式原理

    一、vue2的数据响应式原理

    1. 什么是defineProperty

    其实是定义对象的属性,并不是核心的微一个对象做数据双向绑定,而是去给对象做属性标签,只不过属性里的get和set实现了响应式。

    属性名 默认值 说明
    value Undefined
    Get Undefined 取值
    Set Undefined 设置值
    Writable False 可写
    Enumerable False 可遍历
    Configurable False 可枚举

    defineProperty的使用

    let ob = {a: 123, b: 456}
    let _value = ob.a  // 需要额外变量去存储值
    Object.defineProperty(ob, "a", {
      writable: false,
      enumerable: false,
      configurable: false,
      get: () => {
        console.log("you get a number")
        return _value
      },
      set: (val) => {
        console.log("you set a number")
        _value = val
      }
    })
    console.log(Object.getOwnPropertyDescriptor(ob, "a"))
    
    ob.a = 111;
    console.log(ob.a)  // 123
    
    for (let item in ob) {
      console.log(item)  // 只能打印出 b 属性
    }
    

    2. 双向绑定实现

    2.1vue中从改变一个数据到发生改变的过程

    数据改变出发set --> set部分出发notify --> 更改对应的虚拟dom --> 更新render

    初始化 --> get部分手机依赖

    2.2 代码实现

    function vue() {
      this.$data = {a: 1};  // 写死了,不是传参,简化
      this.el = document.getElementById('app');
      this.virtualdom = '';  // 虚拟dom
      this.observe(this.$set)  // 将数据进行get/set处理
      this.render(); // 初始化页面
    }
    vue.prototype.observe = function(obj) {
      let value, self = this;
      for (let item in obj) {
        value = obj[item]
        if (typeof value === 'object') {
          this.observe(value)
        } else {
          Object.defineProperty(ob, item, {
            get: function() {
              // 依赖收集:收集这个属性,在哪几个组件使用
              return value
            },
            set: function(newvalue) {
              // 触发更新
              value = newvalue
              selt.render()
            }
          })
        }
      }
    }
    
    vue.prototype.render = function() {
      this.virtualdom = "i am" + this.$data.a  
      this.el.innerHTML = this.virtualdmo
    }
    

    数组更新

    let arrayPro = Array.protorype
    let arrayob = Object.create(arrayPro)
    let arr = ['push', 'pop', 'shift', 'unshift']  // 触发数组更新的方法
    arr.forEach((method, index) => {
      arrayob[method] = function() {
        let ret = arrayPro[method].apply(this, arguments)
        dep.notify()  // 触发更新
        return ret;
      }
    })
    // 最后将数组的prototype修改一下
    

    二、vue3的数据响应式原理

    1. 什么是proxy

    proxy对象用于定义基本操作的自定义行为,和defineProperty类型功能几乎一样,只不过用法上有些不同。

    2. 代码实现

    let ob = {a: 1}
    let obproxy = new Proxy(ob, {
      get: function(target, key, receiver) {
        return target[key]  // 可通过此方式直接取值,而不是还要需要中间的一个变量
      },
      set: function(target, key, value, receiver) {
        // target[key] = value
        return Reflect.set(target, key, value)
      },
    })
    

    defineProperty:是去改变原对象的,只能监听某个属性,不能对全对象监听(还要递归判断属性是否是对象)

    proxy:是去新建一个代理对象,并不会改变原对象;可以省去for in提升效率,可以监听数组,不用再去单独的对数组做特异性操作。

    function vue() {
      this.$data = {a: 1};  // 写死了,不是传参,简化
      this.el = document.getElementById('app');
      this.virtualdom = '';  // 虚拟dom
      this.observe()  // 将数据进行get/set处理
      this.render(); // 初始化页面
    }
    vue.prototype.observe = function() {
      let self = this
      this.$data = new Proxy(this.$data, {
        get: function(target, key, receiver) {
          return target[key]  // 可通过此方式直接取值,而不是还要需要中间的一个变量
        },
        set: function(target, key, value, receiver) {
          target[key] = value
          selt.render()
        },
      })
    }
    
    vue.prototype.render = function() {
      this.virtualdom = "i am" + this.$data.a  
      this.el.innerHTML = this.virtualdmo
    }
    

    3. 扩展:proxy还能做什么

    3.1 类型校验

    // 策略模式:新建一个策略对象,存放校验规则
    let validtor = {
      name: function(value) {
        let reg = /^[\u4e00-\u9fa5]+$/;
        if (typeof value === 'string' && reg.test(value)) {
         return false
        }
        return true
      }
    }
    
    function person(name, age) {
      this.age= age
      this.name = name
      return new Proxy(this, {
        get: function(target, key) {},
        set: function(target, key, value) {
          if (validtor[key](value)) {
            return Reflect.set(target, key, value)
          } else {
            throw new Error(key + 'is not right')
          }
        }
      })
    }
    

    3.2 真正的私有变量

    例如:this.$route不能进行更改,比如设置defineProperty只定义get不定义set,就无法进行set从而实现;

    三、diff算法和virtual dom

    1. 虚拟DOM

    用js去虚拟的描述dom元素,例如:

    <template>
      <div class="demo">
        <p>skdjfskdf</p>
      </div>
    </template>
    let ob = {
        el: "div",
        props: {class: "demo"},
        text: "",
        children: [{
          el: 'p',
          props: {},
          text: 'skdjfskdf',
          children: []
        }]
    }
    

    2. diff算法

    计算虚拟dom是否有更新

    patchVnode(oldVnode, vnode) {
      const el = vnode.el = oldVnode.el;
      let i, oldCh = oldVnode.children, ch = vnode.children;
      // 如果没有更新,则直接return
      if (oldVnode === vnode) return;
      // 如果是文字内容更新,则调用更新内容的方法
      if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
        api.setTextContent(el, vnode.text)
      } else {
        // 如果是子节点更新,调用方法更新dom节点
        updateEle(el, vnode, oldVnode)
        if (oldCh && ch && oldCh !== ch) {
          updateChildren()  // 节点都存在,则表示子节点有变动
        } else if (ch) {
          createEle(vnode)  // 如果新子节点存在,旧不存在,则创建
        } else {
          api.removeChildren(el)  // 如果旧存在,新不存在,则删除
        }
      }
    }
    

    3. vue性能优化

    比较尖端的操作:ssr,app混合reata-native

    给项目构建工具链流程(初始化-测试-规范-构建,如vue-cli),给项目构建有一套基础设施(项目的工具库,组件库,插件库)

    各种的优化实践

    源码:axios,vue-router,redux

    展开全文
  • vue 响应式原理

    2021-03-01 10:04:55
    // vue响应式原理: Vue 的响应式原理是核心是通过 ES5 的保护对象的 Object.defindeProperty 中的访问器属性中的 get 和 set 方法, // data 中声明的属性都被添加了访问器属性, // 当读取 data 中的数据时自动...
    // vue响应式原理: Vue 的响应式原理是核心是通过 ES5 的保护对象的 Object.defindeProperty 中的访问器属性中的 get 和 set 方法, 
    // data 中声明的属性都被添加了访问器属性, 
    // 当读取 data 中的数据时自动调用 get 方法, 
    // 当修改 data 中的数据时, 自动调用 set 方法, 
    // 检测到数据的变化, 会通知观察者 Wacher, 观察者Wacher自动触发重新render当前组件(子组件不会重新渲染),生成新的虚拟 DOM 树,
    // Vue 框架会遍历并对比新虚拟 DOM 树和旧虚拟 DOM 树中每个节点的差别, 并记录下来, 最后, 加载操作, 将所有记录的不同点, 局部修改到真实 DOM 树上。
    
     
    
    const Observer = function(data) {
    	// 循环修改为每个属性添加get set
    	for (let key in data) {
    		definereactive(data, key);
    	}
    }
    
    const definereactive = function(obj, key) {
    	// 局部变量dep,用于get set内部调用
    	const dep = new Dep();
    	// 获取当前值
    	let val = obj[key];
    	Object.defineProperty(obj, key, {
    		// 设置当前描述属性为可被循环
    		enumerable: true,
    		// 设置当前描述属性可被修改
    		configurable: true,
    		get() {
    			console.log('in get');
    			// 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
    			dep.depend();
    			return val;
    		},
    		set(newVal) {
    			if (newVal === val) {
    				return;
    			}
    			val = newVal;
    			// 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
    			// 这里每个需要更新通过什么断定?dep.subs
    			dep.notify();
    		}
    	});
    }
    
    const observe = function(data) {
    	return new Observer(data);
    }
    
    const Vue = function(options) {
    	const self = this;
    	// 将data赋值给this._data,源码这部分用的Proxy所以我们用最简单的方式临时实现
    	if (options && typeof options.data === 'function') {
    		this._data = options.data.apply(this);
    	}
    	// 挂载函数
    	this.mount = function() {
    		new Watcher(self, self.render);
    	}
    	// 渲染函数
    	this.render = function() {
    		with(self) {
    			_data.text;
    		}
    	}
    	// 监听this._data
    	observe(this._data);
    }
    
    const Watcher = function(vm, fn) {
    	const self = this;
    	this.vm = vm;
    	// 将当前Dep.target指向自己
    	Dep.target = this;
    	// 向Dep方法添加当前Wathcer
    	this.addDep = function(dep) {
    		dep.addSub(self);
    	}
    	// 更新方法,用于触发vm._render
    	this.update = function() {
    		console.log('in watcher update');
    		fn();
    	}
    	// 这里会首次调用vm._render,从而触发text的get
    	// 从而将当前的Wathcer与Dep关联起来
    	this.value = fn();
    	// 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
    	// 造成代码死循环
    	Dep.target = null;
    }
    
    const Dep = function() {
    	const self = this;
    	// 收集目标
    	this.target = null;
    	// 存储收集器中需要通知的Watcher
    	this.subs = [];
    	// 当有目标时,绑定Dep与Wathcer的关系
    	this.depend = function() {
    		if (Dep.target) {
    			// 这里其实可以直接写self.addSub(Dep.target),
    			// 没有这么写因为想还原源码的过程。
    			Dep.target.addDep(self);
    		}
    	}
    	// 为当前收集器添加Watcher
    	this.addSub = function(watcher) {
    		self.subs.push(watcher);
    	}
    	// 通知收集器中所的所有Wathcer,调用其update方法
    	this.notify = function() {
    		for (let i = 0; i < self.subs.length; i += 1) {
    			self.subs[i].update();
    		}
    	}
    }
    
    const vue = new Vue({
    	data() {
    		return {
    			text: 'hello world'
    		};
    	}
    })
    
    vue.mount(); // in get
    vue._data.text = '123'; // in watcher update /n in get
    
    展开全文
  • Vue 响应式原理

    2019-07-25 15:53:30
    经常有小伙伴向师傅反映: ...说起来也是,平时小伙伴们估计只是使用 vue 的情况更多一些,对于 vue响应式原理,编程又用不到,所以自然也没怎么去关心过,结果面试时一被问到,自然也就会懵逼了。 ...

    经常有小伙伴向师傅反映:

    师傅啊~面试的时候面试官说要问 vue 相关的问题,我当时还挺兴奋的,心想:来吧,有什么问题都冲着我来吧,我的 vue 用得杠杠的!

    [外链图片转存失败(img-myKGgt6k-1564040822810)(media/15631732887887/15633271281790.jpg)]

    结果面试官一问:说一下 vue 响应式的原理呢,我瞬间就懵逼了…

    [外链图片转存失败(img-rFMA4Uf4-1564040822811)(media/15631732887887/15633254217435.jpg)]

    说起来也是,平时小伙伴们估计只是使用 vue 的情况更多一些,对于 vue 的响应式原理,编程又用不到,所以自然也没怎么去关心过,结果面试时一被问到,自然也就会懵逼了。

    所以这里,就让沃师傅来给大家介绍一下这方面的知识。

    Let’s Go!

    [外链图片转存失败(img-wuj86c3j-1564040822811)(media/15631732887887/15633254846296.jpg)]

    设计模式与发布订阅模式

    在介绍 vue 的响应式原理之前,有几个知识点不得不提,首先就发布订阅模式的介绍。

    发布订阅模式是属于设计模式的一种。那什么又是设计模式呢?

    设计模式

    所谓设计模式,代表了软件开发中的一些最佳的实践,通常被有经验的面向对象的软件开发人员所采用。

    什么意思呢?来举一个生活中的例子。

    小明最近看上了一套房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼 MM 告诉小明,不久后还有一些尾盘推出,开发商正在办理相关手续,手续办好后便可以购买。但到底是什么时候,目前还没有人能够知道。

    于是小明记下了售楼处的电话,以后每天都会打电话过去询问是不是已经到了购买时间。除了小明,还有小红、小强、小龙也会每天向售楼处咨询这个问题。一个星期过后,售楼 MM 决定辞职,因为厌倦了每天回答 1000 个相同内容的电话。

    当然现实中没有这么笨的销售公司,实际上故事是这样的:小明离开之前,把电话号码留在了售楼处。售楼 MM 答应他,新楼盘一推出就马上发信息通知小明。小红、小强和小龙也是一样,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼 MM 会翻开花名册,遍历上面的电话号码,依次发送一条短信来通知他们。

    这其实就是一种设计模式,比起第一种方式,很明显第二种方式是最优解,所以以后再遇到相同的问题时,直接采用第二种方式来解决即可。

    所以说,所谓设计模式,就是软件开发人员在软件开发中所面临的一些共通问题时的最优解决方案,这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

    设计模式有很多,例如单例模式、观察者模式、工厂模式…等,在遇到不同类型的问题时,就可以采用不同的设计模式来解决。

    而我们要介绍的发布订阅模式,也是设计模式中的一种。

    发布订阅模式

    说完了设计模式,我们回头再来看发布订阅模式。什么是发布订阅模式呢?

    在刚刚的例子中,发送短信通知就是一个典型的发布订阅模式。

    小明、小红等购买者都是订阅者,他们订阅了房子开售的消息。售楼处作为发布者,会在合适的时候遍历花名册上的电话号码,依次给购房者发布消息。

    可以发现,在这个例子中使用发布—订阅模式有着显而易见的优点。

    • 购房者不用再天天给售楼处打电话咨询开售时间,在合适的时间点,售楼处作为发布者会通知这些消息订阅者。

    • 购房者和售楼处之间不再强耦合在一起,当有新的购房者出现时,他只需把手机号码留在售楼处,售楼处不关心购房者的任何情况,不管购房者是男是女还是一只猴子。而售楼处的任何变动也不会影响购买者,比如售楼 MM 离职,售楼处从一楼搬到二楼,这些改变都跟购房者无关,只要售楼处记得发短信这件事情。

    属性特性与描述符

    好了,至此,关于设计模式与发布订阅模式,你已经有一定的了解了,接下来还有一个知识点,那就是属性特性与描述符。

    这是通过一个Object.defineProperty()的方法来设置的。举个例子,平常我们习惯定义一个对象,直接书写:

    let obj = {
        name : 'xiejie',
        age : 18
    }
    

    这个对象定义好了后,外面可以随意的访问与修改该对象的属性。

    通过Object.defineProperty()方法,我们可以进行一定的限制。可以为属性所设置的选项如下:

    get:一旦目标属性被访问时,就会调用相应的方法
    set:一旦目标属性被设置时,就会调用相应的方法
    value:这是属性的值,默认是 undefined
    writable:这是一个布尔值,表示一个属性是否可以被修改,默认是 true
    enumerable:这是一个布尔值,表示在用 for-in 循环遍历对象的属性时,该属性是否可以显示出来,默认值为 true
    configurable:这是一个布尔值,表示我们是否能够删除一个属性或者修改属性的特性,默认值为 true
    

    因为 vue 中涉及到 get 和 set 的设置,所以我们重点来看一下这两个选项。

    举个具体的示例如下:

    const stu = {};
    const age = 18;
    Object.defineProperty(stu, "stuAge", {
        //get:当外界获取 stuAge 属性时会自动调用
        get: function () {
            return age;
        },
        //set:当外界要设置 stuAge 属性时会自动调用
        set: function (value) {
            if (value > 100 || value < 0) {
                age = 20;
            } else {
                age = value;
            }
        }
    })
    console.log(stu.stuAge); // 18;
    stu.stuAge = 30;
    console.log(stu.stuAge); // 30
    stu.stuAge = 1000;
    console.log(stu.stuAge); // 20
    console.log(stu.hasOwnProperty("stuAge")); // true
    

    这里我们为 stu 对象自定义了一个 stuAge 属性。然后为其设置了 get 和 set 属性描述符。

    当用户获取 stu 对象的 stuAge 属性值时,会自动调用 get 所对应的函数,然后返回 age 变量。当用户要设置 age 的值时,这时就会有一个限制,如果用户设置的值大于 100 或者小于 0,则将 age 的值设置为 20,否则就将用户设置的值赋值给 age。

    关于属性特性的更多介绍,可以参阅《属性特性与描述符》一文。

    实现响应式原理

    好了,至此,我们的准备知识就已经 OK 了,下面我们来手把手的实现一下 vue 中的响应式。

    [外链图片转存失败(img-bjmehzMl-1564040822812)(media/15631732887887/15633263674802.jpg)]

    首先,准备我们的 html 代码,代码如下:

    // index.html
    <body>
        <div id="app">
            {{msg}}<input type="text" v-model="msg">{{msg}}
        </div>
        <script src="./mvvm.js"></script>
        <script>
            const options = ({
                el: "#app",
                data: {
                    msg: 'hello vue'
                }
            })
            const vm = new Vue(options);
        </script>
    </body>
    

    在该 html 文件中,我们像 Vue 那样去实例化 Vue 对象,但是并没有引入 Vue,而是引入了一个 mvvm.js 文件,这是我们自己的 js 文件,目前里面没有写任何东西,所以现在打开 index.html,看到的应该是如下的效果:

    [外链图片转存失败(img-Jkag7mUL-1564040822812)(media/15631732887887/15633264112391.jpg)]

    接下来来到 mvvm.js 文件,首先,创建 Vue 构造函数,如下:

    // mvvm.js
    function Vue(options){
        // this 代表 Vue 实例对象,也就是 vm
        // options.data 等于 {msg: "hello vue"}
        observer(this,options.data); // 对数据进行劫持
        this.$el = options.el;
        compile(this); // 遍历模板,绑定事件
    }
    

    在该构造函数中,调用到了 2 个函数,分别是 observer 和 compile。我们一个一个来看,先来看 observer,该函数的作用是对数据进行劫持,并添加到订阅者数组中。具体的代码如下:

    // mvvm.js
    //数据侦听
    // 接收 2 个参数:vm 是 Vue 构造函数的实例对象,obj 为 {msg: "hello vue"}
    function observer(vm,obj){
        var dep = new Dep(); // 新增一个发布者
        // 遍历数据
        for(var key in obj){
            // 将数据的每一项添加到 vm 里面,至此,vm 也有了每一项数据
            // 但是不是单纯的添加,而是设置了 getter 和 setter
            // 在获取数据时触发 getter,在设置数据时触发 setter,至于 Dep.target 之类的可以先放一放
            Object.defineProperty(vm,key,{
                get(){
                    console.log("触发get了");
                    if(Dep.target){
                        dep.addSub(Dep.target);
                    }
                    console.log(dep.subs);
                    return obj[key];
                },
                set(newVal){
                    console.log("触发set了");
                    obj[key] = newVal;
                    dep.notify();
                }
            });
        }
    }
    

    通过 observer 函数,我们将用户所设置的 data 数据添加到了 vm(Vue 实例)上面,但是不是单纯的复制了一遍,而是设置了 getter 与 setter。

    observer 函数执行完毕后,接下来是:

    this.$el = options.el;
    

    这句代码很简单,就是给 vm 添加了一个$el属性,属性值为#app

    接下来,就是 compile 函数了。该函数接收一个参数,就是我们的 Vue 实例对象 vm,具体代码如下:

    // mvvm.js
    // 遍历模板
    function compile(vm){
        var el = document.querySelector(vm.$el); // 首先找到 <div id="app">{{msg}}<input type="text" v-model="msg">{{msg}}</div> 这个节点
        var documentFragment = document.createDocumentFragment(); // 创建一个文档碎片
        var reg = /\{\{(.*)\}\}/; // 创建正则,匹配到的是 {{ }}
        // 遍历子节点,遍历一个,就将其添加到文档碎片里面
        // 由于使用的是 appendChild 将节点添加到文档碎片里面,所以添加一个少一个
        // 最终 el 的子节点会变成空,然后就会退出 while,此时 el 变成了 <div id="app"></div>
        while(el.childNodes[0]){
            var child = el.childNodes[0]; // 将每一个子节点存储在 child 里面
            // 如果该节点是元素节点,能匹配上的就是 <input type="text" v-model="msg">
            if(child.nodeType == 1){
                // 遍历该元素节点的每一个属性,也就是 type="text",v-model="msg
                for(var key in child.attributes){
                    var attrName = child.attributes[key].nodeName; // 获取属性名
                    // 找到 v-model 这个属性
                    if( attrName == 'v-model'){
                        var vmKey = child.attributes[key].nodeValue; // 先获取属性值,也就是 msg
                        // 为该节点,也就是 <input type="text" v-model="msg"> 绑定一个 input 事件
                        child.addEventListener('input',function(event){
                            vm[vmKey] = event.target.value; // 获取用户输入的值,然后改变 vm 里面的 msg 属性对应的值,注意这里会触发 setter
                        })
                    }
                }
            }
            // 如果该节点是文本节点,进入此 if
            if(child.nodeType == 3){
                // 进行正则匹配,匹配上的就是两个{{msg}}
                if(reg.test(child.nodeValue) ){
                    var vmKey = RegExp.$1; // 获取正则里面的捕获值,也就是 msg
                    // 实例化一个 Watcher,接收 3 个参数:Vue 实例,该文本节点,捕获值 msg
                    new Watcher(vm,child,vmKey); 
               }
            }
            documentFragment.appendChild(el.childNodes[0]);
        }
        // 将文档碎片中节点重新添加到 el,也就是 <div id="app"></div> 下面
        el.appendChild(documentFragment);
    }
    

    可以看到,在该函数中,主要就是对节点进行分析,如果是元素节点,寻找是否有 v-model,有的话就绑定 input 事件,而如果是文本节点,就看能否匹配上{{ }},如果能,新实例化一个 watcher。

    那么这个 watcher 又是什么呢?watcher 就是依赖数据的观察者,他们会随时观察数据是否发生改变,如果改变,就会更新自身的数据。相当于发布订阅模式中的订阅者。

    Watcher 部分的代码如下:

    // mvvm.js
    // 新建观察者 Watcher 构造函数
    // 接收 3 个参数:Vue 实例,文本节点 {{ msg }} 以及捕获内容 msg
    function Watcher(vm,child,vmKey){
        this.vm = vm; // vm 
        this.child = child; // {{ msg }}
        this.vmKey = vmKey; // msg
        Dep.target = this; // 将该观察者实例对象添加给 Dep.target
        this.update(); // 执行节点更新方法
        Dep.target = null; // 最后清空 Dep.target
    }
    Watcher.prototype ={
        // 节点更新方法
        update : function(){
            // 相当于:{{ msg }}.nodeValue = this.vm['msg']
            // 这样就更新了文本节点的值,由于这里在获取 vm.msg,所以会触发 getter
            this.child.nodeValue = this.vm[this.vmKey];
        }
    }
    

    在 Watcher 构造函数中,我们要做的最重要的操作就是更新文本节点自身的 nodeValue,注意这里赋值时由于涉及到了获取 vm 的 msg 值,所以会触发前面我们所设置的 getter,所以会执行下面的代码:

    ...
    get(){
        console.log("触发get了");
        // 触发 getter 时,将该 watcher 添加到发布者维护的数组里面
        if(Dep.target){
            dep.addSub(Dep.target);
        }
        console.log(dep.subs);
        return obj[key];
    }
    ...
    

    这时就会将该 watcher 添加到 Dep 所维护的数组里面。那么 Dep 以及 Dep.target 究竟是什么呢?

    这实际上就是我们的发布者,类似于上面例子中说到的售楼部,它发布了消息后,观察者(类似于小明、小红、小强、小龙)就会去更新自身的节点内容。

    发布者的代码如下:

    // mvvm.js
    // 新建发布者构造函数
    function Dep(){
        // 将观察者添加到发布者内部的数组里面
        // 这样以便于通知所有的观察者去更新数据
        this.subs = []; 
    }
    
    Dep.prototype = {
        // 将 watcher 添加到发布者内置的数组里面
        addSub:function(sub){
            this.subs.push(sub);
    
        },
        // 遍历数组里面所有的 watcher,通知它们去更新数据
        notify:function(){
            this.subs.forEach(function(sub){
                sub.update();
            })
        }
    }
    

    在发布者里面维护了一个数组,该数组用于存放所有的观察者 watcher,还拥有 2 个方法 addSub 和 notify,addSub 方法负责将 watcher 添加到发布者内置的数组里面,而 notify 方法负责遍历数组里面所有的 watcher,通知它们去更新数据。

    至此,我们整个程序就书写完毕了,来看一下效果,如下:

    [外链图片转存失败(img-1w1sr36D-1564040823049)(media/15631732887887/2019-07-17%2009.24.03.gif)]

    最后附上整个程序的完整代码。

    // index.html
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>index</title>
    </head>
    
    <body>
        <div id="app">
            {{msg}}<input type="text" v-model="msg">{{msg}}
        </div>
        <script src="./mvvm.js"></script>
        <script>
            const options = ({
                el: "#app",
                data: {
                    msg: 'hello vue'
                }
            })
            const vm = new Vue(options);
        </script>
    </body>
    
    </html>
    
    // mvvm.js
    function Vue(options){
        // this 代表 Vue 实例对象,也就是 vm
        // options.data 等于 {msg: "hello vue"}
        observer(this,options.data); // 对数据进行劫持
        this.$el = options.el;
        compile(this); // 遍历模板,绑定事件
    }
    
    // 新建发布者构造函数
    function Dep(){
        // 将观察者添加到发布者内部的数组里面
        // 这样以便于通知所有的观察者去更新数据
        this.subs = []; 
    }
    
    Dep.prototype = {
        // 将 watcher 添加到发布者内置的数组里面
        addSub:function(sub){
            this.subs.push(sub);
    
        },
        // 遍历数组里面所有的 watcher,通知它们去更新数据
        notify:function(){
            this.subs.forEach(function(sub){
                sub.update();
            })
        }
    }
    
    // 新建观察者 Watcher 构造函数
    // 接收 3 个参数:Vue 实例,文本节点 {{ msg }} 以及捕获内容 msg
    function Watcher(vm,child,vmKey){
        this.vm = vm; // vm 
        this.child = child; // {{ msg }}
        this.vmKey = vmKey; // msg
        Dep.target = this; // 将该观察者实例对象添加给 Dep.target
        this.update(); // 执行节点更新方法
        Dep.target = null; // 最后清空 Dep.target
    }
    Watcher.prototype ={
        // 节点更新方法
        update : function(){
            // 相当于:{{ msg }}.nodeValue = this.vm['msg']
            // 这样就更新了文本节点的值,由于这里在获取 vm.msg,所以会触发 getter
            this.child.nodeValue = this.vm[this.vmKey];
        }
    }
    
    
    // 遍历模板
    function compile(vm){
        var el = document.querySelector(vm.$el); // 首先找到 <div id="app">{{msg}}<input type="text" v-model="msg">{{msg}}</div> 这个节点
        var documentFragment = document.createDocumentFragment(); // 创建一个文档碎片
        var reg = /\{\{(.*)\}\}/; // 创建正则,匹配到的是 {{ }}
        // 遍历子节点,遍历一个,就将其添加到文档碎片里面
        // 由于使用的是 appendChild 将节点添加到文档碎片里面,所以添加一个少一个
        // 最终 el 的子节点会变成空,然后就会退出 while,此时 el 变成了 <div id="app"></div>
        while(el.childNodes[0]){
            var child = el.childNodes[0]; // 将每一个子节点存储在 child 里面
            // 如果该节点是元素节点,能匹配上的就是 <input type="text" v-model="msg">
            if(child.nodeType == 1){
                // 遍历该元素节点的每一个属性,也就是 type="text",v-model="msg
                for(var key in child.attributes){
                    var attrName = child.attributes[key].nodeName; // 获取属性名
                    // 找到 v-model 这个属性
                    if( attrName == 'v-model'){
                        var vmKey = child.attributes[key].nodeValue; // 先获取属性值,也就是 msg
                        // 为该节点,也就是 <input type="text" v-model="msg"> 绑定一个 input 事件
                        child.addEventListener('input',function(event){
                            vm[vmKey] = event.target.value; // 获取用户输入的值,然后改变 vm 里面的 msg 属性对应的值,注意这里会触发 setter
                        })
                    }
                }
            }
            // 如果该节点是文本节点,进入此 if
            if(child.nodeType == 3){
                // 进行正则匹配,匹配上的就是两个{{msg}}
                if(reg.test(child.nodeValue) ){
                    var vmKey = RegExp.$1; // 获取正则里面的捕获值,也就是 msg
                    // 实例化一个 Watcher,接收 3 个参数:Vue 实例,该文本节点,捕获值 msg
                    new Watcher(vm,child,vmKey); 
               }
            }
            documentFragment.appendChild(el.childNodes[0]);
        }
        // 将文档碎片中节点重新添加到 el,也就是 <div id="app"></div> 下面
        el.appendChild(documentFragment);
    }
    
    // 数据侦听
    // 接收 2 个参数:vm 是 Vue 构造函数的实例对象,obj 为 {msg: "hello vue"}
    function observer(vm,obj){
        var dep = new Dep(); // 新增一个发布者
        // 遍历数据
        for(var key in obj){
            // 将数据的每一项添加到 vm 里面,至此,vm 也有了每一项数据
            // 但是不是单纯的添加,而是设置了 getter 和 setter
            // 在获取数据时触发 getter,在设置数据时触发 setter
            Object.defineProperty(vm,key,{
                get(){
                    console.log("触发get了");
                    // 触发 getter 时,将该 watcher 添加到发布者维护的数组里面
                    if(Dep.target){
                        dep.addSub(Dep.target);
                    }
                    console.log(dep.subs);
                    return obj[key];
                },
                set(newVal){
                    console.log("触发set了");
                    obj[key] = newVal;
                    dep.notify();
                }
            });
        }
    }
    

    所有具体的解释,都在代码的注释中了。阅读起来可能会有一定的难度,需要大家花一定的时间,逐行去剖析代码的意思。这是一个痛并快乐着的过程,各位小伙伴加油吧!

    如果通过阅读本文,大家下次面试被问到 vue 响应式原理时,能够道出一二,那么本文也是有价值的。

    这里送上沃师傅对大家面试时的祝福:

    [外链图片转存失败(img-KxDEUp5B-1564040822813)(media/15631732887887/15633269930418.jpg)]

    好了,今天的小小知识课堂就到这里,我们下次再见!
    了解更多请关注公众号“朗沃IT学习”

    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 4,817
精华内容 1,926
关键字:

vue响应式原理

vue 订阅