先丢个效果图,本人前端菜鸟一枚,只是实现了一个简单的demo,有很多瑕疵,和考虑不周到的地方,欢迎大家提出建议。
先看个大致原理流程图 ,图片是从前端攻城狮大佬那里复制的。下面会一步步根据流程图一块块解析。
1. 实现数据劫持 相信大家都知道vue2的双向绑定的原理是通过 Object.defineProperty() 来实现数据劫持的。 那什么是数据劫持呢?具体是怎么劫持的呢?数据劫持 :指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。 那我们来继续看看 Object.defineProperty() 怎么实现数据劫持的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const obj = {}let name = '张三' Object .defineProperty(obj, 'name' ,{ get () { return name }, set (newVal) { name = newVal console .log('obj的name属性值被更改了' ) } }) console .log(obj.name)obj.name = '李四' console .log(obj.name)
可以发现在获取obj.name的时候我们可以设置返回的值,以及在更改obj的name属性值的时候我们也可以知道属性被赋值的操作。这样就实现了对数据的劫持。 但是我们在开发的时候,data里可能有多个变量,以及一个变量可能有多层的结构,那么我们怎么去监听所有变量呢? 这个时候我们就得对所有的变量执行 defineProperty() 方法,这样每一个变量值变更的时候我们都能知道。 话不多说,直接上代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function defineReactive (data, key, val ) { observe(val); Object .defineProperty(data, key, { enumerable: true , configurable: true , get: function ( ) { return val; }, set: function (newVal ) { val = newVal; console .log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”' ); } }); } function observe (data ) { if (!data || typeof data !== 'object' ) { return ; } Object .keys(data).forEach(function (key ) { defineReactive(data, key, data[key]); }); }; const obj = { name: { firstName: '张' , lastName: '三' }, age: 18 } observe(obj) obj.name.firstName = '李' obj.age = 20 console .log(obj)
可以发现通过递归 实现了变量的所有属性和值的监听
2. 实现订阅器 我们在上面实现了数据劫持,那这个时候我们只需要在变量进行变更的时候去通知视图更新。 订阅器的作用就是收集订阅者,在属性变化的时候告诉订阅者。比如我们很喜欢看某个公众号的文章,但是我们不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function Dep ( ) { this .subs = [] } Dep.prototype = { addSub (watcher) { this .subs.push(watcher) }, notify () { this .subs.forEach(watcher => { watcher.update() }) } } const dep = new Dep()Object .defineProperty(data, key, { enumerable: true , configurable: true , get: function ( ) { if (是否需要添加订阅者) { dep.addSub(watcher) } return val; }, set: function (newVal ) { val = newVal; dep.notify() console .log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”' ); } });
在改变变量值的时候,让订阅器的告诉订阅者该变量变更了,让订阅者执行相应的视图更新操作。
3. 实现订阅者 看上面的代码可以发现if条件里是文字描述,所以在什么时机去添加订阅者呢? 如果时机不对可能会频繁的添加重复的订阅,所以在变量初始化的时候就是最好的时机。 在变量初始化的时候,先缓存订阅者,然后触发get方法,将订阅者添加进订阅器,再把缓存清空。 所以只要缓存不为空就添加订阅者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 function Watcher (vm, exp, cb ) { this .vm = vm this .exp = exp this .cb = cb this .value = this .get() } Watcher.prototype = { update () { const oldVal = this .value let value = this .vm.data[this .exp] if (value !== oldVal) { this .value = value this .cb(value) } }, get () { Dep.target = this const value = this .vm.data[this .exp] Dep.target = null return value } } get: function ( ) { if (Dep.target) { dep.push(Dep.target) } return val; },
4. 实现模板解析 这个时候已经实现了observer和watcher的关联,那么哪一些是需要视图更新? 一般在我们开发dom数据里的变量可能是我们需要更新的视图,那么我们就需要去解析dom节点。
1 2 3 4 5 6 <template > <div id ="app" > <p > {{name}}</p > <p > {{age}}</p > </div > </template >
上面的代码是不是很熟悉,我们这里就简单的实现一个解析器,解析模板数据 符号,只考虑大括号里存在一种变量的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function compileElement (el ) { const childNodes = el.childNodes childNodes.forEach(node => { let text = node.textContent const reg = /\{\{(.*)\}\}/ ; if (node.nodeType === 1 ) { compileElement(node) } else if (node.nodeType === 3 && reg.test(text)) { console .log(reg.exec(text)[1 ]) } }) } compileElement(document .querySelector('#app' ))
我们用上面的代码解析上面的模板数据,可以发现我们已经实现可以解析出模板数据里的变量。
1 2 3 4 5 6 var vm = new Vue({ el: '#app' , data:{ count:1 , } })
接下来我们还需要优化一下compile,可以看到el的传入形式,所以我们需要改一下代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function Compile (el,vm) { this.el = document.querySelector(el) this.vm = vm this.compileElement(this.el) } Compile.prototype = { compileElement (el) { let childNodes = el.childNodes const _self = this childNodes.forEach(node => { let text = node.textContent const reg = /\{\{(.*)\}\}/; if (node.nodeType === 1) { // 如果是element _self.compileElement(node) } else if (node.nodeType === 3 && reg.test(text)) { // 文本内容 const exp = reg.exec(text)[1] const value = this.vm.data[exp] node.textContent = value // 将初始化的数据初始化到视图中 new Watcher(this.vm,exp,(val) => { node.textContent = val }) } }) } }
效果 模板的解析已经简单的实现了,接下来我们来定义一下vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <div id="app" > <p>{{name}}</p> <p>{{age}}</p> </div> function myVue (option ) { this .data = option.data observe(this .data) new Compile(option.el,this ) } const selfVue = myVue({ el: '#app' , data: { name: '张三' , age: 18 } })
可以看到我们通过控制台直接改变变量值,可以实现同步更新视图。 大部分都是参考前端攻城狮的文章 ,写的非常好。之前总有一些点没看懂,多看几遍,终于看懂了,并自己手敲实现了一下。