学习 vue 有段时间了, mvvm 在 vue 中是个典型应用, 最近参考了参考网上一些资料, 整理了一下, 也加入了自己的理解, 实现一个简单版的 demo, 也方便有些面试的同学遇到设计一个 mvvm 的面试题.
2. 逻辑结构
mvvm 的设计模式是 "发布与订阅者" 模式(observe/watcher), 主要步骤有三步:
observe 来劫持并监听所有的属性(也就是 vue 中的 data)
给每一个需要监听的属性, 绑定一个订阅者(watcher)
当 observe 监听到属性变化时, 通知 watcher 去更新视图
ok, 步骤讲完了, 接下来就开始实现每一步
3. observe
observe 的主要功能:
劫持和监听数据
当数据更新时, 触发通知(后面的 dep 会讲, 这里跳过)
那么 observe 劫持和监听数据的呢? 用
Object.defineProperty
来实现
先看一个例子:
- function observe (obj) {
- var keys = Object.keys(obj)
- keys.forEach(function(key){
- var val = void 0;
- Object.defineProperty(obj, key, {
- enumerable : true,
- configurable : true,
- get : function () {
- console.log('这个属性是', key)
- return val;
- },
- set : function ( newValue ) {
- val = newValue
- console.log('属性' + key + '已经被监听了, 此时的值是:' + newValue)
- }
- })
- })
- }
- var book = {page : 300}
- observe(book)
- var b = book.page // 后台会打印 这个属性是 page
- book.page = 400 // 后台会打印 属性 page 已经被监听, 此时的值是 400
在这里我们就实现了属性的监听, 上述例子中, 我们用 Object.defineProperty 重写了 set 和 get 函数, 使得属性的值变化时可以被我们监听到(如果不了解 Object.defineProperty 的, 可以查阅 Object.defineProperty
ok, 原理我们清楚了, 接下来就开始写 observe 了
- //data 对象, key 和 val 分别是 data 的键值对
- function defineReactive(data, key, val){
- observe(val) // 递归调用 data 中的子对象
- Object.defineProperty(data, key, {
- enumerable : true, // 可枚举, 可在 for in 和 Object.keys 中得到
- configurable : true,
- get : function () {
- return val;
- },
- set : function ( newValue ) {
- if(val === newValue){
- return;
- }
- val = newValue
- console.log('属性' + key + '已经被监听了, 此时的值是:' + newValue)
- }
- })
- }
- // 观察者, 用来监听数据
- function observe (obj) {
- if(!obj || typeof obj !== 'object'){
- return;
- }
- Object.keys(obj).forEach(function(key){
- defineReactive(obj, key, obj[key])
- })
- }
defineReactive 函数的三个参数, 分别是要注册的对象, 对象的 key, 以及对象的值
defineReactive 中调用 observe, 目的是递归调用所有的属性
3. watcher
observe 写完了, 接下来我们就要看 watcher 了, 因为每个属性都绑定一个 watcher, 所以可能会有很多的 watcher, 因此我们需要一个调度中心(暂时定义为 Dep), 来统一指挥 watcher
Dep 的主要功能:
将每个 watcher 都 push 进去
当接收到 observe 的属性更新通知时, 通知对应的 watcher 来更新视图
接下来上代码
- // 订阅器, 用来收集订阅者, 并且通知订阅者更新函数
- function Dep(){
- this.subs = []
- }
- Dep.prototype = {
- addSub : function (sub){
- this.subs.push(sub)
- },
- notify : function (){
- this.subs.forEach(function(sub){
- sub.update()
- })
- }
- }
Dep 已经定义好了, 接下来我们需要改一下 observe, 将 dep 加进去, 这样我们就实现了在 get 函数中将属性注册一个 watcher 再 push 进 dep 中, 并且 set 函数中数据更新时通 dep,dep 会再通知 watcher 去更新视图
- function defineReactive(data, key, val){
- observe(val) // 递归调用 data 中的子对象
- var dep = new Dep();
- Object.defineProperty(data, key, {
- enumerable : true, // 可枚举, 可在 for in 和 Object.keys 中得到
- configurable : true,
- get : function () {
- // 这里目的是定义一个 flag, 用来判断什么时候需要 push 一个 sub
- // 因为不能每次调用属性都 push 一个 sub, 只有在第一次时才需要 push
- if(Dep.target){
- dep.addSub(Dep.target)
- }
- return val;
- },
- set : function ( newValue ) {
- if(val === newValue){
- return;
- }
- val = newValue
- console.log('属性' + key + '已经被监听了, 此时的值是:' + newValue)
- dep.notify()
- }
- })
- }
到这里 Dep 调度中心就完成了, 接下来我们实现 watcher
watcher 的主要功能:
observe 中 get 函数只是定义了一个 watcher, 但是触发这个 get 函数需要在这里, 这样就完成了注册
接到 dep 的更新通知后, 调用更新函数
ok, 先实现代码:
- function Watcher (vm, exp, cb) {
- this.vm = vm
- this.exp = exp
- this.cb = cb
- this.value = this.get()
- }
- Watcher.prototype = {
- update : function(){
- this.run()
- },
- run : function (){
- var value = this.vm.data[this.exp]
- var oldVal = this.value
- if (value !== oldVal) {
- this.value = value
- this.cb.call(this.vm, value, oldVal)
- }
- },
- get:function(){
- Dep.target = this // 这个暂时不知道干嘛
- var value = this.vm.data[this.exp]
- Dep.target = null
- return value
- }
- }
Watcher 的三个参数, 分别是 vue, 要订阅的属性, 以及回调函数(触发更新时调用的函数)
this.value = this.get()
这行代码就是初始化就去获取这个属性值, 这样就会调用 observe 中的 get 函数, 然后将 watcher 加入到 Dep 中去.
Dep.target = this 就是上文中提到的只有 target 有值时才会将 watcher 加入到 Dep 中
ok, 到这里最简易版本的 mvvm 已经完成了
然后我们定义一个 vue:
- // data 是所有的属性, el 是绑定的元素节点(#App),exp 是绑定的属性
- function dVue (data, el, exp) {
- this.data = data;
- observe(data);
- el.innerhtml = this.data[exp]; // 初始化模板数据的值
- new Watcher(this, exp, function (value) {
- el.innerHTML = value;
- });
- return this;
- }
在 HTML 中调用
- <body>
- <h1 id="app">{{math}}</h1>
- </body>
- <script src="./js/observer.js"></script>
- <script src="./js/watcher.js"></script>
- <script src="./js/index.js"></script>
- <script type="text/javascript">
- var ele = document.querySelector('#app');
- var dVue = new dVue({
- math : '1'
- }, ele, 'math');
- setInterval(function () {
- dVue.data.math = Math.random() * 100
- }, 1000);
- </script>
OK, 接下来我们完善一下, 实现 vue 中的 {{ }} 绑定
4. compile
compile 主要功能:
获取模板, 并且解析模板, 将数据替换模板, 完成初始化视图
给模板中绑定的属性, new 初始化一个 watcher(之前是在 dVue 函数中完成的, 现在移到这里)
- function Compile(el, vm){
- this.vm = vm
- this.el = document.querySelector(el)
- this.fragment = null
- this.init()
- }
- Compile.prototype = {
- init : function(){
- if(this.el){
- this.fragment = this.nodeToFragment(this.el)
- this.compileElement(this.fragment)
- this.el.appendChild(this.fragment)
- }else{
- console.log('节点不存在')
- }
- },
- nodeToFragment : function(el){
- // 创建一个虚拟的文档片段, 用来操作 dom 节点, 因为这个片段是存在于内存中
- // 所以相对于直接操作 dom, 性能会更好一点
- var fragment = document.createDocumentFragment()
- var child = el.firstChild
- while(child){
- fragment.appendChild(child)
- child = el.firstChild
- }
- return fragment
- },
- compileElement :function(el){
- var childNodes = el.childNodes
- var self = this;
- Array.prototype.slice.call(childNodes).forEach(function(node){
- var reg = /\{\{\s*(.*?)\s*\}\}/
- var text = node.textContent;
- // 判断该节点是否含有 {{ }} 这个指令
- if(self.isTextNode(node) && reg.test(text)){
- self.compileText(node, reg.exec(text)[1])
- }
- if(node.childNodes && node.childNodes.length){
- self.compileElement(node)
- }
- })
- },
- compileText :function(node, exp){
- var self = this
- var initText = this.vm[exp]
- this.updateText(node, initText) // 初始化视图
- new Watcher(this.vm, exp, function(value){
- self.updateText(node, value)
- })
- },
- updateText : function(node, value){
- node.textContent = typeof value == 'undefined' ? '' : value
- },
- isTextNode : function(node){
- return node.nodeType == 3
- }
- }
nodeToFragment 是在内存建立一个虚拟的节点, 然后将模板赋值给它, 再继续操作模板, 这样可以提升性能, 参考文档 nodeToFragment
compileElement 这个函数, 解析模板, 找到 {{ }} 指令的文本节点, 然后运行核心函数 compileText, 解析文本节点
compileText 这个函数中, 做了两件事, 第一件事是初始化视图, 也就是调用 updateText 函数, 第二件事就是给这个文本节点绑定一个 watcher, 用于订阅该属性, 当属性值改变时, 会调用里面的回调函数
到这里 compile 就完成了, 这样我们需要把 dVue 重新修改一下
- function dVue (options) {
- var self = this
- this.data = options.data
- this.vm = this
- Object.keys(this.data).forEach((key)=>{
- this.proxyKeys(key)
- })
- // 重写所有的 data 属性的 set 和 get 方法, 用于劫持监听数据
- observe(this.data)
- // 编译模板, 得到绑定的节点, 初始化视图, 并且给该节点所绑定的属性注册一个 watcher
- new Compile(options.el, this)
- return this
- }
- // 代理一下属性, 这样的话 dVue.name = dVue.data.name , 不用每次都带着 data 了
- // 相当于把 data 的所有属性都注册到了 dVue 上
- dVue.prototype = {
- proxyKeys : function(key){
- var self = this
- Object.defineProperty(this, key, {
- enumerable : false,
- configurable : true,
- get : function(){
- return self.data[key]
- },
- set :function(val){
- self.data[key] = val
- }
- })
- }
- }
到这里就基本结束了, 我们在 HTML 中调用一下
- <HTML>
- <head>
- <title > 实现一个简单的 mvvm</title>
- </head>
- <body>
- <div id = "app">
- <div>{{name}}</div>
- <div>{{age}}</div>
- <div>{{like}}</div>
- </div>
- <script type="text/javascript" src="./observe.js"></script>
- <script type="text/javascript" src="./watcher.js"></script>
- <script type="text/javascript" src="./compile.js"></script>
- <script type="text/javascript" src="./dvue.js"></script>
- <script>
- let dVueInit = new dVue({
- el : '#app',
- data:{
- name : 'ding',
- age : 14,
- like : '读书'
- }
- })
- </script>
- </body>
- </HTML>
到此结束!
参考文章:
- https://www.cnblogs.com/libin-1/p/6893712.html
- https://github.com/canfoo/self-vue/tree/master/v2
来源: http://www.jianshu.com/p/9b85f0ea22e6