Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue实现原理 #17

Open
huandie2012 opened this issue Dec 1, 2017 · 0 comments
Open

vue实现原理 #17

huandie2012 opened this issue Dec 1, 2017 · 0 comments

Comments

@huandie2012
Copy link
Owner

huandie2012 commented Dec 1, 2017

vue是一种mvvm框架,在单向数据绑定的基础上给可输入元素(如:input、textarea等)添加了change事件,来动态修改model和view,从而实现了数据的双向绑定效果。
实现数据绑定的做法有大致如下几种:
发布者--订阅者模式(backbone.js);(订阅是请求在某些事件(event)到达时可以通知它并执行对应的动作(action),而发布则相对的是向订阅告知事件(event)已经到达,你可以执行对应的动作(action)了。)
脏值检查(angular.js);
数据劫持(vue.js);
这里我们主要说的是vue的数据劫持: vue.js 采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()(核心方法)来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
要实现mvvm的双向绑定,就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者;
实现Observer---我们知道可以利用Obeject.defineProperty()来监听属性变动,在vue中,把一个普通对象传给 Vue 实例作为它的 data 选项,Vue.js 将递归遍历它的属性,包括子属性对象的属性,都加上 setter和getter,这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。(这是 ES5 特性,不能打补丁实现,这便是为什么 Vue.js 不支持 IE8 及更低版本。)
结合代码理解:

var data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; //监听到值变化了 kindeng --> dmq

function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    // 取出所有属性遍历
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};

function defineReactive(data, key, val) {
    var dep = new Dep();
    observe(val); // 监听子属性
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚举
        configurable: false, // 不能再define
        get: function() {
            // 订阅者应该是Watcher, 想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以将内容放在 getter,由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
            Dep.target && dep.addDep(Dep.target);
            return val;
        }
        set: function(newVal) {
            if (val === newVal) return;
            val = newVal;
            dep.notify(); // 通知所有订阅者
        }
    });
}

function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

// Watcher.js
Watcher.prototype = {
    get: function(key) {
        Dep.target = this;
        this.value = data[key];    // 这里会触发属性的getter,从而添加订阅者
        Dep.target = null;
    }
}

说明:
通过 defineProperty() 方法单独定义的属性被称为是访问器属性,它的属性的值比较特殊,当我们像正常情况读取或设置时,例如:obj.hello; obj.hello = "abc" ;实际上是调用它的内部特性:get和set函数。get 和 set 方法内部的 this 都指向 obj,这意味着 get 和 set 函数可以操作对象内部的值。另外,访问器属性的会"覆盖"同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略。
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数;
Compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图;这里说明一下:因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中。
通过递归遍历保证了每个节点及子节点都会解析编译到,包括了{{}}表达式声明的文本节点。
这里运用了bind()方法监听数据、绑定更新函数的处理,通过new Watcher()添加回调来接收数据变化的通知。接下来就要实现Watcher了。
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图;
Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
在自身实例化时往属性订阅器(dep)里面添加自己;
自身必须有一个update()方法;
待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调;
每一个实例对象都有一个watcher,他会在编译过程中,用getter去访问data属性,watcher此时就会把用到的data属性记为依赖,这样就建立了视图与数据之间的联系,当之后我们渲染视图的数据依赖发生改变(即数据的setter被调用)的时候,watcher会对比前后两个的数值是否发生变化,然后确定是否通知视图进行重新渲染。
4、mvvm入口函数,整合以上三者;
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
这里简单说一下MVVM框架:
MVVM框架主要包含3个部分:model、view和 viewmodel:
Model:指的是数据部分,对应到前端就是Javascript对象;
View:指的是视图部分,对应前端就是DOM;
Viewmodel:就是连接视图与数据的中间件;
数据(Model)和视图(View)是不能直接通讯的,而是需要通过ViewModel来实现双方的通讯。当数据变化的时候,viewModel能够监听到这种变化,并及时的通知view做出修改。同样的,当页面有事件触发时,viewMOdel也能够监听到事件,并通知model进行响应。Viewmodel就相当于上文所说的Watcher,监控着双方的动作,并及时通知对方进行相应的操作。
下面举一个简单的例子帮助大家理解:

<div id="app">
       <input type="text" v-model="text">
       {{text}}
</div>
var vm = new Vue({
      el: "#app",
      data: {
             text: 'hello world'
      }
});

将数据双向绑定任务分解成多个子任务:
输入框以及文本节点与 data 中的数据绑定;
输入框内容变化时,data 中的数据同步变化。即 view => model 的变化;
data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化;
要实现任务一,需要对 DOM 进行编译,这里有一个知识点:DocumentFragment---DocumentFragment(文档片段)可以看作节点容器,它可以包含多个子节点,当我们将它插入到 DOM 中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。使用 DocumentFragment 处理节点,速度和性能远远优于直接操作 DOM。Vue 进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持,通过 append 方法,DOM 中的节点会被自动删除)到 DocumentFragment 中,经过一番处理后,再将 DocumentFragment 整体返回插入挂载目标。

var dom = nodeToFragment(document.getElementById("app"));
function nodeToFragment(node){
	var flag = document.createDocumentFragment();
	var child;
	while(child = node.firstChild){
		flag.appendChild(child);//劫持node所有子节点
	}
	return flag;
}
document.getElementById("app").appendChild(dom);//返回到app中

**再来看任务二:**当我们在输入框输入数据的时候,首先触发 input 事件(或者 keyup、change 事件),在相应的事件处理程序中,我们获取输入框的 value 并赋值给 vm 实例的 text 属性。我们会利用 defineProperty 将 data 中的 text 设置为 vm 的访问器属性,因此给 vm.text 赋值,就会触发 set 方法。
在 set 方法中主要做两件事,第一是更新属性的值,第二就是作为发布者发出通知:“我是属性 text,我变了”。文本节点则是作为订阅者,在收到消息后执行相应的更新操作。
更详尽的源码分析,可以参考滴滴这篇文章:https:/DDFE/DDFE-blog/issues/7https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant