这里写目录标题
01-vue源码学习-柯里化函数
一、概念:
柯里化: 一个函数原本有多个参数, 之传入一个参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构.
偏函数: 一个函数原本有多个参数, 之传入一部分参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构.
高阶函数: 一个函数参数是一个函数, 该函数对参数这个函数进行加工, 得到一个函数, 这个加工用的函数就是高阶函数.
为什么要使用科里化? 为了提升性能. 使用科里化可以缓存一部分能力.
二、代码实现
vue源码中判断是否是html标签函数
let tags = 'div,p,a,img,ul,li'.split(',')
function makeMap(tags) {
let set = {}
tags.forEach(tag => set[tag.toLowerCase()] = true)
return function (tagname) {
return !!set[tagname.toLowerCase()]
}
}
let isHtmlTag = makeMap(tags)
console.log(isHtmlTag);
console.log(isHtmlTag('div'));
三、柯里化函数原理其实是闭包
闭包使用的利弊衡量:
使用闭包可能会存在内存泄露,或者性能会有些问题。但是,如果在具体的场景中,使用闭包能够提高性能,而且内存泄露风险并不高,其实是可以使用闭包的。开发过程中,很多时候的抉择,并不是绝对的,只有适合的方法或者架构,极少有完美的方法或者架构。
02-vue源码学习-vue的基本执行流程和简单渲染模型
一、vue的基本执行流程
获得模板: 模板中有 “坑”(比如{{}})
利用 Vue 构造函数中所提供的数据来 “填坑”, 得到可以在页面中显示的 “标签了”
将标签替换页面中原来有坑的标签
总的来说:Vue利用我们提供的数据和 页面中模板生成了一个新的 HTML 标签 ( node 元素 ),替换到了页面中放置模板的位置.
二、简单渲染模型
Vue 本质上是使用 HTML 的字符串作为模板的, 将字符串的 模板 转换为 AST(抽象语法树), 再转换为 VNode(虚拟dom).
- 模板 -> AST
- AST -> VNode
- VNode -> DOM
那一个阶段最消耗性能?
最消耗性能是字符串解析 ( 模板 -> AST )
三、模仿vue的渲染流程
将真正的 DOM 转换为 虚拟 DOM
将虚拟 DOM 转换为 真正的 DOM
<!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>Document</title>
</head>
<body>
<div id="root">
<div class="c1">
<div title="tt1" id="id">{{ name }}</div>
<div title="tt2">{{ age }}</div>
<div title="tt3">{{ gender }}</div>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</div>
</div>
<script>
/** 虚拟 DOM 构造函数 */
class VNode {
constructor( tag, data, value, type ) {
this.tag = tag && tag.toLowerCase();
this.data = data;
this.value = value;
this.type = type;
this.children = [];
}
appendChild ( vnode ) {
this.children.push( vnode );
}
}
/** 由 HTML DOM -> VNode: 将这个函数当做 compiler 函数 */
function getVNode( node ) {
let nodeType = node.nodeType;
let _vnode = null;
if ( nodeType === 1 ) {
// 元素
let nodeName = node.nodeName;
let attrs = node.attributes;
let _attrObj = {};
for ( let i = 0; i < attrs.length; i++ ) { // attrs[ i ] 属性节点 ( nodeType == 2 )
_attrObj[ attrs[ i ].nodeName ] = attrs[ i ].nodeValue;
}
_vnode = new VNode( nodeName, _attrObj, undefined, nodeType );
// 考虑 node 的子元素
let childNodes = node.childNodes;
for ( let i = 0; i < childNodes.length; i++ ) {
_vnode.appendChild( getVNode( childNodes[ i ] ) ); // 递归
}
} else if ( nodeType === 3 ) {
_vnode = new VNode( undefined, undefined, node.nodeValue, nodeType );
}
return _vnode;
}
/** 将虚拟 DOM 转换成真正的 DOM */
function parseVNode( vnode ) {
// 创建 真实的 DOM
let type = vnode.type;
let _node = null;
if ( type === 3 ) {
return document.createTextNode( vnode.value ); // 创建文本节点
} else if ( type === 1 ) {
_node = document.createElement( vnode.tag );
// 属性
let data = vnode.data; // 现在这个 data 是键值对
Object.keys( data ).forEach( ( key ) => {
let attrName = key;
let attrValue = data[ key ];
_node.setAttribute( attrName, attrValue );
} );
// 子元素
let children = vnode.children;
children.forEach( subvnode => {
_node.appendChild( parseVNode( subvnode ) ); // 递归转换子元素 ( 虚拟 DOM )
} );
return _node;
}
}
let rkuohao = /\{\{(.+?)\}\}/g;
/** 根据路径 访问对象成员 */
function getValueByPath( obj, path ) {
let paths = path.split( '.' ); // [ xxx, yyy, zzz ]
let res = obj;
let prop;
while( prop = paths.shift() ) {
res = res[ prop ];
}
return res;
}
/** 将 带有 坑的 Vnode 与数据 data 结合, 得到 填充数据的 VNode: 模拟 AST -> VNode */
function combine( vnode, data ) {
let _type = vnode.type;
let _data = vnode.data;
let _value = vnode.value;
let _tag = vnode.tag;
let _children = vnode.children;
let _vnode = null;
if ( _type === 3 ) { // 文本节点
// 对文本处理
_value = _value.replace( rkuohao, function ( _, g ) {
return getValueByPath( data, g.trim() );
} );
_vnode = new VNode( _tag, _data, _value, _type )
} else if ( _type === 1 ) { // 元素节点
_vnode = new VNode( _tag, _data, _value, _type );
_children.forEach( _subvnode => _vnode.appendChild( combine( _subvnode, data ) ) );
}
return _vnode;
}
function JGVue( options ) {
this._data = options.data;
let elm = document.querySelector( options.el ); // vue 是字符串, 这里是 DOM
this._template = elm;
this._parent = elm.parentNode;
this.mount(); // 挂载
}
JGVue.prototype.mount = function () {
// 需要提供一个 render 方法: 生成 虚拟 DOM
this.render = this.createRenderFn() // 带有缓存 ( Vue 本身是可以带有 render 成员 )
this.mountComponent();
}
JGVue.prototype.mountComponent = function () {
// 执行 mountComponent() 函数
let mount = () => { // 这里是一个函数, 函数的 this 默认是全局对象 "函数调用模式"
this.update( this.render() )
}
mount.call( this ); // 本质应该交给 watcher 来调用, 但是还没有讲到这里
// 为什么
// this.update( this.render() ); // 使用发布订阅模式. 渲染和计算的行为应该交给 watcher 来完成
}
/**
* 在真正的 Vue 中使用了 二次提交的 设计结构
* 1. 在 页面中 的 DOM 和 虚拟 DOM 是一一对应的关系
* 2. 先 有 AST 和 数据 生成 VNode ( 新, render )
* 3. 将 就的 VNode 和 新的 VNode 比较 ( diff ), 更新 ( update )
*/
// 这里是生成 render 函数, 目的是缓存 抽象语法树 ( 我们使用 虚拟 DOM 来模拟 )
JGVue.prototype.createRenderFn = function () {
let ast = getVNode( this._template );
// Vue: 将 AST + data => VNode
// 我们: 带有坑的 VNode + data => 含有数据的 VNode
return function render () {
// 将 带有 坑的 VNode 转换为 待数据的 VNode
let _tmp = combine( ast, this._data );
return _tmp;
}
}
// 将虚拟 DOM 渲染到页面中: diff 算法就在里
JGVue.prototype.update = function ( vnode ) {
// 简化, 直接生成 HTML DOM replaceChild 到页面中
// 父元素.replaceChild( 新元素, 旧元素 )
let realDOM = parseVNode( vnode );
// debugger;
// let _ = 0;
this._parent.replaceChild( realDOM, document.querySelector( '#root' ) );
// 这个算法是不负责任的:
// 每次会将页面中的 DOM 全部替换
}
let app = new JGVue( {
el: '#root',
data: {
name: '张三'
, age: 19
, gender: '难'
}
} );
</script>
</body>
</html>
03-vue源码学习-响应式原理(对象和数组)
一、响应式原理
什么是响应式原理:
意思就是在改变数据的时候,视图会跟着更新。
实现响应式原理的核心:Object.defineProperty
Object.defineProperty( 对象, '设置什么属性名', {
writeable
configurable
enumerable: 控制属性是否可枚举, 是不是可以被 for-in 取出来
set() {} 赋值触发
get() {} 取值触发
} )
二、对象响应式化
<script>
let o = {
name: 'jim',
age: 19,
gender: '男'
}
// 简化后的版本
function defineReactive(target, key, value, enumerable) {
// 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )
Object.defineProperty(target, key, {
configurable: true,
enumerable: !!enumerable,
get() {
console.log(`读取 o 的 ${key} 属性`); // 额外
return value
},
set(newVal) {
console.log(`设置 o 的 ${key} 属性为: ${newVal}`); // 额外
value = newVal
}
})
}
let keys = Object.keys(o)
keys.forEach(key => {
defineReactive(o, key, o[key], true)
})
</script>
三、对象数组响应式化
<script>
let data = {
name: '张三',
age: 19,
course: [
{ name: '语文' },
{ name: '数学' },
{ name: '英语' },
]
}
// 简化后的数据响应函数
function defineReactive(target, key, value, enumerable) {
// 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )
if (typeof value === 'object' && value != null && !Array.isArray(value)) {
// 是非数组的引用类型
reactify(value); // 递归
}
Object.defineProperty(target, key, {
configurable: true,
enumerable: !!enumerable,
get() {
console.log(`读取 o 的 ${key} 属性`); // 额外
return value
},
set(newVal) {
console.log(`设置 o 的 ${key} 属性为: ${newVal}`); // 额外
value = newVal
}
})
}
// 将对象 o 响应式化
function reactify(o) {
keys = Object.keys(o)
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
let value = o[key]
// 判断这个属性是不是引用类型, 判断是不是数组
// 如果引用类型就需要递归, 如果不是就不用递归
// 如果不是引用类型, 需要使用 defineReactive 将其变成响应式的
// 如果是引用类型, 还是需要调用 defineReactive 将其变成响应式的
// 如果是数组呢? 就需要循数组, 然后将数组里面的元素进行响应式化
if (Array.isArray(value)) {
for (let j = 0; j < value.length; j++) {
reactify(value[j])
}
} else {
// 对象或值类型
defineReactive(o, key, value, true);
}
}
}
reactify(data)
</script>
四、拦截数组方法
在改变数组时,加入的元素也要响应式化,所以要对数组方法进行重写,采用的方式是拦截数组的方法。
拦截数组方法:
<script>
let ARRAY_METHOD = [
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice',
];
// 思路, 原型式继承: 修改原型链的结构
let arr = [];
// 继承关系: arr -> Array.prototype -> Object.prototype -> ...
// 继承关系: arr -> 改写的方法 -> Array.prototype -> Object.prototype -> ...
let array_methods = Object.create(Array.prototype)
ARRAY_METHOD.forEach(method => {
array_methods[method] = function () {
// 调用原来的方法
console.log('调用的是拦截的 ' + method + ' 方法');
// 将数据进行响应式化
let res = Array.prototype[method].apply(this, arguments)
return res
}
})
arr.__proto__ = array_methods
</script>
五、完整的对象和数组响应式化代码
<script>
let data = {
name: '张三',
age: 19,
course: [
{ name: '语文' },
{ name: '数学' },
{ name: '英语' },
]
}; // 除了递归还可以使用队列 ( 深度优先转换为广度优先 )
let ARRAY_METHOD = [
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice',
];
let array_methods = Object.create(Array.prototype);
ARRAY_METHOD.forEach(method => {
array_methods[method] = function () {
// 调用原来的方法
console.log('调用的是拦截的 ' + method + ' 方法');
// 将数据进行响应式化
for (let i = 0; i < arguments.length; i++) {
reactify(arguments[i]);
}
let res = Array.prototype[method].apply(this, arguments);
// Array.prototype[ method ].call( this, ...arguments ); // 类比
return res;
}
});
// 简化后的版本
function defineReactive(target, key, value, enumerable) {
// 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )
if (typeof value === 'object' && value != null && !Array.isArray(value)) {
// 是非数组的引用类型
reactify(value); // 递归
}
Object.defineProperty(target, key, {
configurable: true,
enumerable: !!enumerable,
get() {
console.log(`读取 ${key} 属性`); // 额外
return value;
},
set(newVal) {
console.log(`设置 ${key} 属性为: ${newVal}`); // 额外
value = newVal;
}
});
}
// 将对象 o 响应式化
function reactify(o) {
let keys = Object.keys(o);
for (let i = 0; i < keys.length; i++) {
let key = keys[i]; // 属性名
let value = o[key];
if (Array.isArray(value)) {
// 数组
value.__proto__ = array_methods; // 数组就响应式了
for (let j = 0; j < value.length; j++) {
reactify(value[j]); // 递归
}
} else {
// 对象或值类型
defineReactive(o, key, value, true);
}
}
}
reactify(data);
</script>
04-vue源码学习-proxy
proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
app._data.name
// vue 设计, 不希望访问 _ 开头的数据
// vue 中有一个潜规则:
// - _ 开头的数据是私有数据
// - $ 开头的是只读数据
app.name
// 将 对 _data.xxx 的访问 交给了 实例
// 重点: 访问 app 的 xxx 就是在访问 app._data.xxx
假设:
var o1 = { name: '张三' };
// 要有一个对象 o2, 在访问 o2.name 的时候想要访问的是 o1.name
Object.defineProperty( o2, 'name', {
get() {
return o1.name
}
} );
访问 app 的 xxx 就是在访问 app._data.xxx
Object.defineProperty( app, 'name', {
get() {
return app._data.name
},
set( newVal ) {
app._data.name = newVal;
}
} )
将属性的操作转换为 参数
function proxy( app, key ) {
Object.defineProperty( app, key, {
get() {
return app._data[ key ]
},
set( newVal ) {
app._data[ key ] = newVal;
}
} )
}
问题:
在 vue 中不仅仅是只有 data 属性, properties 等等 都会挂载到 Vue 实例上
function proxy( app, prop, key ) {
Object.defineProperty( app, key, {
get() {
return app[ prop ][ key ]
},
set( newVal ) {
app[ prop ][ key ] = newVal;
}
} )
};
// 如果将 _data 的成员映射到 实例上
proxy( 实例, '_data', 属性名 )
// 如果要 _properties 的成员映射到 实例上
proxy( 实例, '_properties', 属性名 )
05-vue源码学习-发布订阅模式
发布订阅模式
目标: 解耦, 让各个模块之间没有紧密的联系
现在的处理办法是 属性在更新的 时候 调用 mountComponent 方法.
问题: mountComponent 更新的是什么??? (现在) 全部的页面 -> 当前虚拟 DOM 对应的页面 DOM
在 Vue 中, 整个的更新是按照组件为单位进行 判断, 已节点为单位进行更新.
- 如果代码中没有自定义组件, 那么在比较算法的时候, 我们会将全部的模板 对应的 虚拟 DOM 进行比较.
- 如果代码中含有自定义组件, 那么在比较算法的时候, 就会判断更新的是哪一些组件中的属性, 只会判断更新数据的组件, 其他组件不会更新.
复杂的页面是有很多组件构成. 每一个属性要更新的都要调用 更新的方法?
目标, 如果修改了什么属性, 就尽可能只更新这些属性对应的页面 DOM
这样就一定不能将更新的代码写死.
例子: 预售可能一个东西没有现货, 告诉老板, 如果东西到了 就告诉我.
老板就是发布者
订阅什么东西作为中间媒介
我就是订阅者
使用代码的结构来描述:
- 老板提供一个 账簿( 数组 )
- 我可以根据需求订阅我的商品( 老板要记录下 谁 定了什么东西, 在数组中存储 某些东西 )
- 等待, 可以做其他的事情
- 当货品来到的时候, 老板就查看 账簿, 挨个的打电话 ( 遍历数组, 取出数组的元素来使用 )
实际上就是事件模型
- 有一个 event 对象
- on, off, emit 方法
实现事件模型, 思考怎么用?
- event 是一个全局对象
- event.on( ‘事件名’, 处理函数 ), 订阅事件
- 事件可以连续订阅
- 可以移除: event.off()
- 移除所有
- 移除某一个类型的事件
- 移除某一个类型的某一个处理函数
- 写别的代码
- event.emit( ‘事件名’, 参数 ), 先前注册的事件处理函数就会依次调用
原因:
- 描述发布订阅模式
- 后面会使用到事件
发布订阅模式 ( 形式不局限于函数, 形式可以是对象等 ) :
- 中间的全局的容器, 用来存储可以被触发的东西( 函数, 对象 )
- 需要一个方法, 可以往容器中传入东西 ( 函数, 对象 )
- 需要一个方法, 可以将容器中的东西取出来使用( 函数调用, 对象的方法调用 ) 事件模型代码
<script>
// 全局的 event 对象, 提供 on, off, emit 方法
var event = (function () {
eventObjs = {}
return {
/** 注册事件, 可以连续注册, 可以注册多个事件 */
on: function (type, handler) {
(eventObjs[type] || (eventObjs[type] = [])).push(handler)
},
/** 移除事件,
* - 如果没有参数, 移除所有事件,
* - 如果只带有 事件名 参数, 就移除这个事件名下的所有事件,
* - 如果带有 两个 参数, 那么就是表示移除某一个事件的具体处理函数
* */
off: function (type, handler) {
if (arguments.length === 0) {
eventObjs = {}
} else if (arguments.length === 1) {
eventObjs[type] = 1
} else if (arguments.length === 2) {
let _events = eventObjs[type]
if (!_events) return
for (let i = _events.length; i >= 0; i--) {
if (_events[i] === handler) {
_events.splice(i, 1)
}
}
}
},
/**
* 发射事件, 触发事件, 包装参数 传递给事件处理函数
*/
emit: function (type) {
let args = Array.prototype.splice.call(arguments, 1)// 或 arguments 从 1 开始后的所有参数, 返回的是数组
let _events = eventObjs[type]
if (!_events) return
for (let i = 0; i < _events.length; i++) {
_events[i].apply(null, args)
}
}
}
}())
</script>
<script>
function f() { console.log(1) }
function foo() { console.log(2) }
// 注册事件
event.on('click', () => console.log('第一个 click 事件')); // 无法移除
event.on('click', () => console.log('第2个 click 事件'));
event.on('click', () => console.log('第3个 click 事件'));
event.on('click', () => console.log('第4个 click 事件'));
event.on('click', () => console.log('第5个 click 事件'));
console.log(1);
console.log(1);
console.log(1);
console.log(1);
console.log(1);
console.log(1);
function f() {
event.emit('click');
}
// js 中 基本类型是比较值
// 引用类型是 比较 地址
// 引用类型与基本类型, 是将其转换为 基本类型再比较 , 如果是 === 严格等于是不转换比较
</script>```
06-vue源码学习-理解依赖收集和派发更新(Observer、Watcher、Dep)
转载至:彻底理解Vue中的Watcher、Observer、Dep
在数据响应化时,在getter方法中做依赖收集,在setter方法中做派发更新。dep用于存储依赖和派发更新。
思考以下代码
new Vue({
el: '#example',
data(){
return{
obj:{
a:1
}
}
},
})
当我们写下这行代码时,vue将我们在data内定义的obj对象进行依赖追踪.
具体做法为执行new Observer(obj)
//经过上面的代码,我们的obj对象会变为以下的样子
{
obj:{
a:1,
__ob__:{ //Observer 实例
dep:{Dep 实例
subs:[ //存放 Watcher 实例
new Watcher(),
new Watcher(),
new Watcher(),
new Watcher(),
]
}
}
}
}
我们来一步步实现看下。
在obj对象上新增__ob__
属性,值为Observe
类的实例,我们编写一个 def
函数,用来增加属性
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
增加啥属性呢?之前提到了,我们需要增加一个 Observer 实例,实现如下
Observe 实现
const Dep = require('./Dep')
class Observer {
constructor(targetObject) {
def(targetObject, '__ob__', this);//在 targetObject 上 添加 Observer 实例, setter时 通知该实例
this.walk(targetObject)
this.dep = new Dep()
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
});
}
}
function observe(data) {
if (Object.prototype.toString.call(data) !== '[object Object]') {
return
}
new Observer(data)
}
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get');
const ob = this.__ob__
ob.dep.depend();
return val
},
set: function reactiveSetter(newVal) {
console.log('set');
if (newVal === val) return
val = newVal
observe(newVal)
const ob = this.__ob__
ob.dep.notify();
},
})
}
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
module.exports = Observer
这里面牵扯到了 Dep,我们也把Dep实现下
Dep
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
depend() {
this.subs.push(Dep.target);
}
notify() {
for (let i = 0; i < this.subs.length; i++) {
this.subs[i].fn();
}
}
}
Dep.target = null;
module.exports=Dep
Observer
类 主要做了以下事情
遍历 data 下的每一个属性,若是对象,则 执行 new Observer()
,在对象上新增__ob__
属性,该属性的值为 Observer
的实例
劫持对象属性的变化,在 getter
的时候,拿到 Observer
实例的dep
实例,执行dep.depend()
,代码如下
const ob = this.__ob__
ob.dep.depend();
看下 dep.depend()做了些啥
this.subs.push(Dep.target)
将Dep.target
添加到 订阅数组内(this.subs)
也就是说,只要我们 Dep.target 赋值了,再执行
dep.depend()
,那么该值就会被添加到 dep 的 subs 数组内,比如
实现自动添加依赖
这个时候该 Watcher出场了
Watcher 实现
const Dep = require("./Dep");
class Watcher {
constructor(vm, exp, fn) {
this.vm = vm;
this.exp = exp;
this.fn = fn;
Dep.target = this; //将自己挂载到 Dep.target,调用 Dep.depend时会读取该变量
this.vm[exp];
}
update() {
//加入队列
}
}
module.exports = Watcher;
根据一个小例子来理解 Watcher
const obj = {
a: 1,
b: {
c: 2
}
}
new Observer(obj)
new Watcher(obj, 'a', () => {
console.log('Watcher 回调执行')
})
obj.a='222'
流程如下:
- 先观测 obj 对象(
new Observer(obj)
) - 实例化
Watcher
时,会执行Dep.target = this
,然后执行this.vm[exp]
,也就是取一次值,那么会触发
getter
,将自身(Watcher实例)添加到dep
的订阅者数组内 - 最后,改变数据时候,触发
setter
set: function reactiveSetter(newVal) {
if (newVal === val) return
val = newVal
observe(newVal)
const ob = this.__ob__
ob.dep.notify();
},
执行ob.dep.notify()
notify() {
for (let i = 0; i < this.subs.length; i++) {
this.subs[i].fn()
}
遍历 订阅者(subs)执行回调函数,整个流程结束
07-vue源码学习-vue的响应式原理(MVVM原理)
什么是MVVM
MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。
要实现一个mvvm的库,我们首先要理解清楚其实现的整体思路。先看看下图的流程:
1.实现compile,进行模板的编译,包括编译元素(指令)、编译文本等,达到初始化视图的目的,并且还需要绑定好更新函数;
2.实现Observe,监听所有的数据,并对变化数据发布通知;
3.实现watcher,作为一个中枢,接收到observe发来的通知,并执行compile中相应的更新方法。
4.结合上述方法,向外暴露mvvm方法。
最简化 VUE的响应式原理(转载)
转载:最简化 VUE的响应式原理
下图是vue官方的原理图:
总共分为三步骤:
1、init 阶段
VUE 的 data的属性都会被reactive化,也就是加上 setter/getter函数
function defineReactive(obj: Object, key: string, ...) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
....
dep.depend()
return value
....
},
set: function reactiveSetter (newVal) {
...
val = newVal
dep.notify()
...
}
})
}
class Dep {
static target: ?Watcher;
subs: Array<Watcher>;
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
其中这里的Dep就是一个观察者类,每一个data的属性都会有一个dep对象。当getter调用的时候,去dep里注册函数,
至于注册了什么函数,我们等会再说。
setter的时候,就是去通知执行刚刚注册的函数。
2、mount 阶段:
mountComponent(vm: Component, el: ?Element, ...) {
vm.$el = el
...
updateComponent = () => {
vm._update(vm._render(), ...)
}
new Watcher(vm, updateComponent, ...)
...
}
class Watcher {
getter: Function;
// 代码经过简化
constructor(vm: Component, expOrFn: string | Function, ...) {
...
this.getter = expOrFn
Dep.target = this // 注意这里将当前的Watcher赋值给了Dep.target
this.value = this.getter.call(vm, vm) // 调用组件的更新函数
...
}
}
mount 阶段的时候,会创建一个Watcher类的对象。这个Watcher实际上是连接Vue组件与Dep的桥梁。
每一个Watcher对应一个vue component。
这里可以看出new Watcher的时候,constructor 里的this.getter.call(vm, vm)函数会被执行。getter就是updateComponent。这个函数会调用组件的render函数来更新重新渲染。
而render函数里,会访问data的属性,比如
render: function (createElement) {
return createElement('h1', this.blogTitle)
}
此时会去调用这个属性blogTitle的getter函数,即:
// getter函数
get: function reactiveGetter () {
....
dep.depend()
return value
....
},
// dep的depend函数
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
在depend的函数里,Dep.target就是watcher本身(我们在class Watch里讲过,不记得可以往上第三段代码),这里做的事情就是给blogTitle注册了Watcher这个对象。这样每次render一个vue 组件的时候,如果这个组件用到了blogTitle,那么这个组件相对应的Watcher对象都会被注册到blogTitle的Dep中。
这个过程就叫做依赖收集。
收集完所有依赖blogTitle属性的组件所对应的Watcher之后,当它发生改变的时候,就会去通知Watcher更新关联的组件。
3、更新阶段:
当blogTitle 发生改变的时候,就去调用Dep的notify函数,然后通知所有的Watcher调用update函数更新。
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
可以用一张图来表示:
由此图我们可以看出Watcher是连接VUE component 跟 data属性的桥梁。
08-自我总结的vue响应式原理
vue是采用数据劫持配合发布者-订阅者模式的方式,通过Object.definerProperty()
来劫持各个属性的setter和getter,在数据变动时,发布消息给依赖收集器,去通过观察者,做出对应的回调函数,去更新试图。
MVVM作为绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听model数据变化,通过Compile来解析编译指令,最终利用Watcher搭起Observer,Compile之间的通信桥梁,达到数据变化=>视图更新;视图交互变化=>数据model变更的双向绑定效果。