Vue.js 源码学习笔记 -- 分析前准备1 -- vue三大利器
主体
实例方法归类: 先看个作者推荐, 清晰易懂的 23232
重点:
// Observer
class Observer {
constructor (data) {
this.walk(data)
}
walk (data) {
// 遍历
let keys = Object.keys(data)
for(let i = 0; i < keys.length; i++){
defineReactive(data, keys[i], data[keys[i]])
}
}
} function defineReactive (data, key, val) {
observer(val) // dep 为什么要在这里实例化, 就是为了实现, 对象每一层的 每一个key都有自己的一个订阅实例, 比如 a.b 对应 dep1, a.c 对应dep2, 这里虽然都是let dep = new Dep()
// 但每次来到这个方法, dep都是独立的, 会一直保留在内存. 这样在每次调用set方法都能找到这个a.b对应的dep
// dep 这里会一直保存, 是因为闭包的关系, Object这个全局的函数, 引用了上层的作用域, 这个作用域包含了 dep, 除非Object = null, 或者退出浏览器, dep才会消失 //实例化之后, dep就有了被订阅, 和发布消息的功能, dep不写在这里也是可以的, 多定义一个全局函数, 每次obser的时候增加一个dep
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
//每次new Watch('a.b'), 都会先执行get方法, 进而来到这里, 触发 dep.depend(), 这个dep就是 a.b 对应的 订阅,
dep.depend()
return val
},
set: function (newVal) {
if(val === newVal){
return
}
observer(newVal)
dep.notify()
}
})
} function observer (data) {
if(Object.prototype.toString.call(data) !== '[object Object]') {
return
}
new Observer(data)
} class Dep {
constructor () {
this.subs = []
}
depend () {
//这里是收集回调函数的过程, 也就是收集依赖项, 数据改变后, 需要触发的改变UI和其他函数
this.subs.push(Dep.target)
}
notify () {
for(let i = 0; i < this.subs.length; i++){
this.subs[i].fn()
}
}
} Dep.target = null function pushTarget(watch){
Dep.target = watch
} // watch
class Watch {
constructor (exp, fn) {
this.exp = exp
this.fn = fn
pushTarget(this)
data[exp]
}
} var data = {
a: 1,
b: {
c: 2
}
}
//递归对象的属性 , 层层监听
observer(data)
//new 产生 this, this挂载上exp 和 回调fn, 再利用data[exp] 触发get方法 从而订阅 dep.push( this)
new Watch('a', () => {
console.log(9)
}) new Watch('a', () => {
console.log(90)
}) new Watch('b.c', () => {
console.log(80)
}) setTimeout(() => {
data.a = 2
}, 1000)
observer: 检测每一个对象每一层的属性, 每个属性都具备get set的方法, 如果这些属性有变化, 调用相对的dep处理
Dep: 根据不同的数据生成不同的dep依赖, 这个依赖收集了相关回调方法, 和触发这些回调执行
wathcer: 包含需要触发的回调函数, 在get方法中, 订阅这个属性的dep, 如果 wacher('a.b', fn1) wacher('a.b', fn2),
就对同一个类型生成了 两个订阅者 subs1, subs2 ,当 a.b = 3; 赋值
的时候, 触发set方法, 从而依次执行了: subs1的回调方法fn1, 和 subs2的回调方法 fn2,
主要还是利用了 Object.defineProperty 方法, set get 会找到同一个dep00, 第一次访问数据的时候, 触发get , dep00收集依赖, 后面设置新值的时候, 触发set方法, dep00触发所有订阅者的回调函数。
整个vm 核心, 就是实现了 observer(观察数据变化) parser(解析依赖) watcher(观察到的数据更新,通知指令的执行更新相应页面方法) 这三个东西
具体实现
数据监听机制
如何监听某一个对象属性的变化呢?我们很容易想到 Object.defineProperty
这个 API,
为此属性设计一个特殊的 getter/setter,然后在 setter 里触发 ob.notify一个函数 来执行指令,就可以达到监听的效果。
看数组的方法的监听例子
[ 'push', 'pop', 'shift', 'unshift', splice', 'sort', 'reverse'].forEach(function( method){ var original = arrayProto[method] // Array.prototype.sort //数组的方法执行的时候, 会触发下面这个函数
_.define( arrayMethods, method, function mutator(){ //先在原生的数组原型方法中按传入的参数执行一遍, 得到结果
var result = original.apply(this , args); var ob = this.__ob__; var inserted switch (method){
case 'push': inserted = args ;break
case 'unshift': inserted = args ; break
case 'splice': inserted = args.slice(2); } if(inserted) ob.observeArray(inserted)
ob.notify() return result }) })
同时 Vue.js 提供了两个额外的“糖方法” arr.$set[0] = "c"
和 $remove(index)
来弥补这方面限制带来的不便。
path 解析器
var path = 'a.b[1].v'
var obj = {
a: { b:[ {v:1}, {v:2}, {v:3} ] }
}
parse( obj, path ) // => 2
如何解析 这个字符串 成为 js语句 是关键
vue.js 是通过状态机管理 来实现对路径的解析的
Vue 将表达式的访问路径字符串 解析成 更易于js使用的状态
比如 b.c.d 将会解析成 ['b', 'c', 'd' ]
经过js处理后 变成 a[ arr[0] ][ arr[1] ][ arr[2] ] 就可以访问的这个深层的属性值
对一个合法的路径来说, 是有规律的, 如果第一个字符为a
第二个字符可能是有四种情况
a.
a[
ab
a(没有了, undefinde 就是解析完毕了)
Vue 的状态机模式解析 path 实际上是将 path 的每个索引的字符视为一个状态,
将 接下来一个字符 视为 当前状态的 输入,
并 根据输入 进行 状态转移 以及 响应操作,
如果输入不是期望的,那么状态机将异常中止。
只有状态机正常运行直到转移到结束状态,才算解析成功。
Vue 的 pathStateMachine 有八种状态,例如 BEFORE_PATH
BEFORE_PATH 是 pathStateMachine 的初始状态,它的状态模型为
pathStateMachine[BEFORE_PATH] = {
'ws': [BEFORE_PATH],
'ident': [IN_IDENT, APPEND],
'[': [IN_SUB_PATH],
'eof': [AFTER_PATH]
}
从状态模型中知道 BEFORE_PATH 接受四种输入,
这些状态起的作用是分割字符, 再将这些字符依次放入数组中
ws ,状态转移到 BEFORE_PATH
indent ,状态转移到 IN_IDENT,并执行 APPEND 操作
[ ,状态转移到 IN_SUB_PATH
eof ,AFTER_PATH
indent 表示 a-z A-Z字符, 继续输入, 这个子路径名称还没有结束
ws 表示 空格 换行等
[ 表示要开记录下一个子路径
eof 表示路径结束
具体的意思和其他7种状态模型可以看这个函数 getPathCharType
( https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/parsers/path.js#L33-L81 )
状态机运行过程中,Vue 在通过 action 处理每一级 path 的路径值。
比如当处于状态 IN_IDENT 时,再次输入字符,会执行 APPEND 操作,将该字符串与之前的字符作 字符串拼接。
再次输入 .
或 [
会执行 PUSH 操作,将之间的字符串视为访问对象的一个属性。
Vue 的 pathStateMachine 有四种 action,他们主要是根据 path 特征和状态提取出对象的访问属性,并按照层级关系依次推入数组。
详细见代码。
下面是是一个详细例子分析状态机的状态转移过程,需要分析的 path 为 md[0].da["ky"]
先声明
keys = 存放对象访问属性的数组
key = 临时变量
index = 索引
mode = 当前状态
input = 输入
transfer = 状态转移
action = 操作
现在进入状态极 (简单使用字符串 for循环)
index = 0 (md[0].da["ky"])
mode = BEFORE_PATH
input = 'm'
transfer => IN_IDENT
action => APPEND
keys = []
key = 'm'
index = 1 (md[0].da["ky"])
mode = IN_IDENT
input = 'd'
transfer => IN_IDENT
action => APPEND
keys = []
key = 'md'
index = 2 (md[0].da["ky"])
mode = IN_IDENT
input = '['
transfer => IN_SUB_PATH
action => PUSH
keys = ['md']
key = undefined
index = 3 md[0].da["ky"]
mode = IN_SUB_PATH
input = '0'
transfer => IN_SUB_PATH
action => APPEND
keys = ['md']
key = '0'
index = 4 (md[0].da["ky"])
mode = IN_SUB_PATH
input = ']'
transfer => IN_PATH
action => INC_SUB_PATH_DEPTH
keys = ['md', '0']
key = undefined
index = 5 md[0].da["ky"]
mode = IN_PATH
input = '.'
transfer => BEFORE_IDENT
action => None
keys = ['md', '0']
key = undefined
index = 6
mode = BEFORE_IDENT
input = 'd'
transfer => IN_IDENT
action => APPEND
keys = ['md', '0']
key = 'd'
index = 7
mode = IN_IDENT
input = 'a'
transfer => IN_IDENT
action => APPEND
keys = ['md', '0']
key = 'da'
index = 8 md[0].da["ky"]
mode = IN_IDENT
input = '['
transfer => IN_SUB_PATH
action => PUSH
keys = ['md', '0', 'da']
key = undefined
index = 9
mode = IN_SUB_PATH
input = '"'
transfer => IN_DOUBLE_QUOTE
action => APPEND
keys = ['md', '0', 'da']
key = '"'
index = 10 md[0].da["ky"]
mode = IN_DOUBLE_QUOTE
input = 'k'
transfer => IN_DOUBLE_QUOTE
action => APPEND
keys = ['md', '0', 'da']
key = '"k'
index = 11
mode = IN_DOUBLE_QUOTE
input = 'y'
transfer => IN_DOUBLE_QUOTE
action => APPEND
keys = ['md', '0', 'da']
key = '"ky'
index = 12
mode = IN_DOUBLE_QUOTE
input = '"'
transfer => IN_SUB_PATH
action => APPEND
keys = ['md', '0', 'da']
key = '"ky"'
index = 13
mode = IN_SUB_PATH
input = ']'
transfer => IN_PATH
action => PUSH_SUB_PATH
keys = ['md', '0', 'da', 'ky']
key = undefined
index = 14
mode = IN_SUB_PATH
input = 'eof'
transfer => AFTER_PATH
action => None
keys = ['md', '0', 'da', 'ky']
key = undefined
最后的结果是
['md', '0', 'da', 'ky']
再拼接成 md[0]['da']['ky'] 就可以访问这个属性值了 arr[0][ arr[1]] [ arr[2] ]..
比如这个
var a = { b:[ {t1:1},{t2:2} ]};
a['b'][0]['t1'] => 1
表达式解析
vue 的表达式是通过自己的来解析的, 做了setter监听处理
而不是直接调用eval方法, 所以并不是所以的js表达式都支持
其解析 {{ mess.splite('').reverse().join('') }} 过程:
首先, 调用Vue.parsers.text.parseText(str), 解析成一个tokens对象
tokens = [
{
html: false,
hasOneTime: false,
tag: true,
value: "mess.split('').reverse().join('')" //通过正则获取到了大括号的内容
}
]
然后用 Vue.parsers.text.tokensToExp(tokens) 取出 value, 赋值为一个字符串表达式:
expression = "mess.split('').reverse().join('')";
这个 expression 正是创建 watcher 时所用的表达式,
wathcer 为表达式 和 数据 建立联系的时候, 会解析这个表达式 并获取值
var res = Vue.parsers.expresssion.parseExpresstion(expression)
解析这个表达式, 其实是为它定义了 getter 和 setter 方法
为了能定义并使用这两个方法, 表达式必须是合法的路径, 而且要有值
表达式的getter 方法结合组件的数据获取表达式的值,
通过 Function 构造器为表达式求值
var getter = function( s, expression ){
return new Function( 'return ' + s+'.'+ expression + ';' );
}
获取表达式的值时, 执行 getter方法 从作用域对象内取值
var model = { mess: 'abc' }
这里利用 new Function 可以传入字符串,解析成js语句, 来解析字符串表达式;
比如页面有 {{mess.substring(0,1)}}
var fn = getter( 'model', 'mess.substring(0,1)' );
fn(); // 输出 a
Vue中的双向绑定原理:
在视图中改变组件数据,驱动数据更新。
进而触发表达式的中熟悉的setter方法, 在根据订阅者更新表达式的内容, 和页面的内容
大概的实现参考 $watch方法, 所有的表达式 , 比如 mess.children.split('')
Vue.js 源码学习笔记 -- 分析前准备1 -- vue三大利器的更多相关文章
- Vue.js 源码学习笔记 -- 分析前准备2 -- Object.defineProperty
解析神奇的 Object.defineProperty 几行代码看他怎么用 var a= {} Object.defineProperty( a, "b", { value ...
- Vue.js 源码学习笔记
最近饶有兴致的又把最新版 Vue.js 的源码学习了一下,觉得真心不错,个人觉得 Vue.js 的代码非常之优雅而且精辟,作者本身可能无 (bu) 意 (xie) 提及这些.那么,就让我来吧:) 程序 ...
- Vue.js 源码学习笔记 - 细节
1. this._eventsCount = { } 这是为了避免不必要的深度遍历: 在有广播事件到来时,如果当前 vm 的 _eventsCount 为 0, 则不必向其子 vm 继续传播该 ...
- Underscore.js 源码学习笔记(下)
上接 Underscore.js 源码学习笔记(上) === 756 行开始 函数部分. var executeBound = function(sourceFunc, boundFunc, cont ...
- Underscore.js 源码学习笔记(上)
版本 Underscore.js 1.9.1 一共 1693 行.注释我就删了,太长了… 整体是一个 (function() {...}()); 这样的东西,我们应该知道这是一个 IIFE(立即执行 ...
- vue.js源码学习分享(一)
今天看了vue.js源码 发现非常不错,想一边看一遍写博客和大家分享 /** * Convert a value to a string that is actually rendered. *转换 ...
- AlloyTouch.js 源码 学习笔记及原理说明
alloyTouch这个库其实可以做很多事的, 比较抽象, 需要我们用户好好的思考作者提供的实例属性和一些回调方法(touchStart, change, touchMove, pressMove, ...
- AlloyFinger.js 源码 学习笔记及原理说明
此手势库利用了手机端touchstart, touchmove, touchend, touchcancel原生事件模拟出了 rotate touchStart multipointStart ...
- move.js 源码 学习笔记
源码笔记: /* move.js * @author:flfwzgl https://github.com/flfwzgl * @copyright: MIT license * Sorrow.X - ...
随机推荐
- 【Coursera】SecondWeek(1)
全球互联网的始祖 APRANET APRANET 是 DARPA(美国国防部高级研究计划局) 开发的世界上第一个运营PacketSwitching(分包交换)的网络. 鉴于二战后世界格局两极化的历史背 ...
- JS进阶系列之this
在javascript中,this的指向是在执行上下文的创建阶段确定的,其实只要知道不同执行方式下,this的指向分别是是什么,就能很好的掌握this这个让人摸不透的东西. 一.全局执行 全局执行又分 ...
- 获取CheckBox的值
前台获取 function chkCheckBox() { var code_arr = new Array(); //定义一数组 $('.C_B').each(function () { if ($ ...
- 关于Java中System.gc() 与System.runFinalization()
System.gc : 告诉垃圾收集器打算进行垃圾收集,而垃圾收集器进不进行收集是不确定的.只是建议进行回收 System.runFinalization(): 网上搜了一下很多人都说强制调用已经失 ...
- BeautifulSoup中的select方法
在写css时,标签名不加任何修饰,类名前加点,id名前加 #,我们可以用类似的方法来筛选元素,用到的方法是soup.select(),返回类型是list. (1).通过标签名查找 print(soup ...
- 《剑指offer》第十三题(机器人的运动范围)
// 面试题:机器人的运动范围 // 题目:地上有一个m行n列的方格.一个机器人从坐标(0, 0)的格子开始移动,它 // 每一次可以向左.右.上.下移动一格,但不能进入行坐标和列坐标的数位之和 // ...
- LeetCode--141--环形链表
问题描述: 给定一个链表,判断链表中是否有环. 思路:用快的指针追慢的指针,只要有圈,一定能追上. 错误: class Solution(object): def hasCycle(self, hea ...
- HDU-3729 二分匹配 匈牙利算法
题目大意:学生给出其成绩区间,但可能出现矛盾情况,找出合理组合使没有说谎的人尽可能多,并按maximum lexicographic规则输出组合. //用学生去和成绩匹配,成绩区间就是学生可以匹配的成 ...
- android------adb命令 pull或push手机和电脑文件交互
先说一下adb命令配置,如果遇到adb不是内部或外部命令,也不是可运行的程序或批量文件.配置下环境变量 1.adb不是内部或外部命令,也不是可运行的程序或批量文件. 解决办法:在我的电脑-属性-高级计 ...
- Confluence 6 导入 Active Directory 服务器证书 - UNIX
为了让你的应用服务器能够信任你的目录服务器.你目录服务器上导出的证书需要导入到你应用服务器的 Java 运行环境中.JDK 存储了信任的证书,这个存储信任证书的文件称为一个 keystore.默认的 ...