Vue2手写源码---响应式数据的变化
响应式数据变化
数据发生变化后,我们可以监听到这个数据的变化 (每一步后面的括号是表示在那个模块进行的操作)
手写简单的响应式数据的实现(对象属性劫持、深度属性劫持、数组函数劫持)、模板转成 ast 语法树、将 ast 语法树转换成 render 函数、render 函数生成虚拟节点、根据生成的虚拟节点创造真实 DOM
响应式数据的实现
创建一个Vue实例 vm (index.html)
const vm = new Vue({
data() {
return {
name : 'zs',
age : 18
}
}
})在 index.js 中使用 this._init 做数据的初始化,并使用 initMaxin 方法,将 vue 实例传过去 (index.js)
function Vue(options) { // options 就是用户的选项
this._init(options); //默认调用了init
}
initMixin(Vue); // 扩展了 init 方法在 init.js 中,将用户的数据信息挂载到 vue 实例上,并初始化状态 (init.js)
export function initMixin(Vue) { //就是给 Vue 增加 init 方法
Vue.prototype._init = function (options) { //用于初始化操作
//vue vm.$options 就是获取用户的配置
// 将 用户的选项 挂载到 Vue 实例上
const vm = this;
vm.$options = options;
// 初始化状态
initState(vm)
}
}
在 state.js 中,要先获取到所有的配置,判断是否存在某一配置,然进行获取这个配置。将data放到实例上的_data 上,然后对数据进行劫持。 (state.js)
export function initState(vm) {
const opts = vm.$options; //获取所有的选项
if(opts.data) { //判断是否存在data
initData(vm);
}
}
function initData(vm) {
let data = vm.$options.data; //获取到data中的数据 data 可能是函数 也可能是对象
// 判断 data 中数据是函数还是对象
data = typeof data === 'function'? data.call(vm) : data; //data 是用户返回的对象
vm._data = data; //将返回的对象放到 _data 上
// 对数据进行劫持 vue2使用一个 api defineProperty
observe(data);
}
创建一个 处理 数据劫持的文件夹,在里面创建一个 index.js 文件进行数据劫持 (/observe/index.js)
先判断数据是否为对象(只对对象进行劫持),再判断是否被劫持过(被劫持过就不需要再被劫持),再通过类实例进行劫持。 (/observe/index.js)
// 判断是否为对象
if(typeof data !== 'object' || data == null) {
return; //不是对象,直接 return 出来
}
// 判断是否被劫持过,使用实例进行判断,被劫持过就不需要再被劫持
return new Observe(data);
声明一个类实例 Observe,然后再将 defineReactive 作为公共 API 导出 (/observe/index.js)
class Observe {
constructor(data) { //用户传入的数据
this.walk(data)
}
walk(data) { //循环这个 数据对象,对属性依次进行劫持
// 重新定义属性 所以 vue2 性能会比 vue3 差
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
}
}
export function defineReactive(target,key,value) { //作为公共 API 导出,target:需要重新定义的 值,key:需要重新定义的值的key值,value:需要重新定义的值的value值
observe(value); //值如果是对象的话,就再次进行劫持(避免深层次的值没有被劫持)
Object.defineProperty(target, key, {
get() { //取值时候,执行 get
console.log('get');
return value;
},
set(newValue) { //修改的时候,执行set
console.log('set');
if(value === newValue) return
value = newValue;
}
})
}
访问 vm.name 就相当于访问 vm._data.name(还是在 state.js 中进行代理) (state.js)
// 将vm._data 用 vm来进行代理 访问vm.name 就相当于 访问 vm._data.name
for(let key in data) {
proxy(vm, '_data', key)
}
function proxy(vm, target, key) {
Object.defineProperty(vm, key, {
get() {
return vm[target][key];
},
set(newValue) {
vm[target][key] = newValue;
}
})
}
对于数组的话,会遍历数组内部的所有元素,造成性能的浪费。所以要对数组的方法进行重写。数组有七个方法可以修改本身。(/observe/index.js)
Object.defineProperty(data, '__ob__', { //将 Observe 实例赋给data 对象的自定义属性上,让array.js中能拿到 Observe 中的方法。
value : this,
enumerable : false //将 __ob__ 变成不可枚举的(循环的时候就获取不到了)
})
// 判断数据是不是数组
if(Array.isArray(data)) {
// 重写数组中的方法 7个方法 可以修改数组本身
data.__proto__ = newArrayProto;
// 在对数组里面的每一项进行劫持
this.observeArray(data);
} else {
this.walk(data) //不是数组就直接进入循环,进行数据劫持
}
observeArray(data) { //循环这个数组,然后进行数据劫持
data.forEach(item => observe(item));
}
在 array.js 中,重写了能修改数组本身的七个方法,还要对数组新增的数据再次进行劫持.(/observe/array.js)
// 获取数组的原型
let oldArrayProto = Array.prototype;
// newArrayProto._proto = oldArrayProto
export let newArrayProto = Object.create(oldArrayProto);
let methods = [ //找到所有的变异方法
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice'
]
methods.forEach(method => {
newArrayProto[method] = function(...args) { //重写了数组的方法
const result = oldArrayProto[method].call(this, ...args); //函数的劫持 切片编程
console.log('method:' ,method);
// 对新增的数据 也要再次进行劫持
let inserted;
let ob = this.__ob__;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
default:
break;
}
if(inserted) { //对新增的内容再次进行观测
ob.observeArray(inserted); //使用观测实例中的 observeArray 方法,来遍历新增的数据,然后进行数据劫持。
}
return result;
}
})
监听到数据变化后,要进行模板参数的解析
要先判断有没有传进 el 参数 (init.js)
// 判断是否有传入 el 参数
if(options.el) {
vm.$mount(options.el); //实现数据的挂载
}
编写 $mount 方法 (init.js)
Vue.prototype.$mount = function (el) {
const vm = this;
el = document.querySelector(el)
let ops = vm.$options
if(!ops.render) { //判断是否写了 render
let template; //没有render 就看看有没有模板
if(!ops.template && el) { //没有模板,但是有el
template = el.outerHTML
} else {
if (el) {
template = ops.template; //如果有el , 则采用模板的内容
}
}
// 如果写了模板
if(template) {
const render = compileToFunction(template); //将模板编译成 render 函数
ops.render = render;
}
}
ops.render;
}
模板转化为 ast 语法树
compileToFunction 对模板进行编译处理(/compile/index.js)
先将模板转化为 ast 语法树 (/compile/parse.js)
利用正则先匹配 标签名、属性、文本内容
//利用正则匹配出模板
const ncname = `[a-zA-Z][\\-\\.0-9_a-zA-Z]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); //这里匹配的是开始标签名 <div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); //这里匹配的是结束标签名 </div>
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的
const startTagClose = /^\s*(\/?)>/; //匹配开始标签的两种格式 <div> <div />
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // 匹配双花括号 {{}}然后依据这个正则开始匹配 模板中的内容,匹配到一个,就删除一个 (开始标签解析比较复杂,所以放在 parseStartTag 方法中)
// 存在 html 的时候
while(html) {
// 如果 textEnd 为 0,则说明是开始标签和结束标签。大于 0 就是文本的结束位置
let textEnd = html.indexOf('<');
debugger;
// 匹配开始标签
if(textEnd == 0) {
const startTagMatch = parseStartTag(); //开始标签的匹配结果
if(startTagMatch) { //解析到的开始标签
start(startTagMatch.tagName, startTagMatch.attrs)
continue;
}
// 如果不是开始标签,那就是结束标签
let endTagMatch = html.match(endTag);
if(endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch.tagName)
continue;
}
}
// 匹配文本
if(textEnd > 0) {
let text = html.substring(0, textEnd); //截取的文本内容
if(text) { //存在文本的话
chars(text);
advance(text.length) //删除文本的部分
}
}
}开始的标签比较复杂,需要匹配开始的标签和标签中的属性
// 匹配开始标签
function parseStartTag() {
const start = html.match(startTagOpen);
if(start) {
const match = {
tagName:start[1], //标签名
attrs:[] //存放属性
}
advance(start[0].length); //匹配完开始标签后就去掉开始标签
// 如果不是 开始标签的结束,就一直匹配下去
let attr, end;
while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { //如果没有匹配到结束标签的时候,就一直匹配下去
advance(attr[0].length); //匹配完属性后就去掉属性
match.attrs.push({name : attr[1], value : attr[3] || attr[4] || attr[5] || true}) //往 match 的 attrs 中存放属性的键值对
}
if(end) { //如果匹配到结束标签
advance(end[0].length) //直接去掉结束标签
}
return match; //返回解析完成的结果
}
return false; //不是标签
}拿获取到的开始标签、文本、结束标签来做处理---最终转换成一颗抽象树
抽象树的格式
const ELEMENT_TYPE = 1; //元素类型
const TEXT_TYPE = 3;//文本类型
const stack = []; //创建用于存放元素的栈---利用栈先进后出的特性来构建抽象语法树
let currentParent; //用于指向栈中的最后一个元素
let root;//根节点
// 转换为一颗抽象语法树
function createASTElement(tag,attrs) {
return {
tag,
type:ELEMENT_TYPE,
children:[],
attrs,
parent:null
}
}// 对开始 文本 结束标签做处理---最终转换成一颗抽象语法树
function start(tag, attrs) { //标签名 + 属性
let node = createASTElement(tag,attrs); //调用 createASTElement 函数,生成一个 ast 节点
if(!root) { //判断是否有根节点,没有的话就将当前节点作为根节点
root = node;
}
if(currentParent) { //如果栈中已经有最后一个元素,则当前节点的父亲就是栈中最后一个元素
node.parent = currentParent;
currentParent.children.push(node);//还需要在父节点添加children属性的值
}
stack.push(node);//将节点放入栈中
currentParent = node; //指向栈中的最后一个元素
}
function chars(text) { //文本内容直接放到当前指向的节点
text = text.replace(/\s/g,'')
text && currentParent.children.push({
type:TEXT_TYPE,
text,
parent: currentParent
})
}
function end() { //标签
stack.pop(); //弹出 栈中最后一个元素
currentParent = stack[stack.length - 1]; //更新 currentParent , 指向最后一个元素
}
ast 语法树 转换成 render 方法
(/compiler/index.js)
// 2.生成 render方法 (render方法执行返回的结果就是 虚拟DOM)
let code = codegen(ast);
code = `with(this){return ${code}}`
let render = new Function(code); //根据代码生成 render 函数
return render;
```javascript
// 转化为 render
function codegen(ast) {
let children = genChildren(ast.children)
let code = `_c('${ast.tag}', ${ast.attrs.length > 0? genProps(ast.attrs) : 'null'}${ast.children.length ? `,${children}` : ''})`
return code
}
```
转换的规则(属性的转换、文本内容的转换) (/compiler/index.js)
// 属性的转化
function genProps(attrs) {
let str = ''
for(let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
if(attr.name === 'style') { //判断属性是不是style
let obj = {};
attr.value.split(';').forEach(item => { //先利用 ; 进行分隔出成对的 key value。 再利用 : 分隔出单个的 key 和 value
let [key, value] = item.split(':');
obj[key] = value; //将成对的 key value 写入空对象中
});
attr.value = obj; //再将 obj 对象作为 value 放进 attr 中
}
str += `${attr.name}:${JSON.stringify(attr.value)},` //拼接属性
}
return `{${str.slice(0, -1)}}`
}
// 生成元素和文本内容(普通文本内容,双花括号文本内容)
function gen(node) {
if(node.type === 1) { //元素
return codegen(node);
} else { //文本
let text = node.text
if(!defaultTagRE.test(text)) { //普通文本的话
return `_v(${JSON.stringify(text)})`
} else {
// _v(_s(name) + 'one' + _s(name))
let tokens = [];
let match;
defaultTagRE.lastIndex = 0;
let lastIndex = 0; //最后匹配的位置
while(match = defaultTagRE.exec(text)) {
let index = match.index; //双花括号内容匹配的位置
if(index > lastIndex) { //判断双花括号内容匹配的位置 看看是否中间有普通文本内容
tokens.push(JSON.stringify(text.slice(lastIndex, index))); //有就将普通文本放进 tokens 中
}
tokens.push(`_s(${match[1].trim()})`) //将花括号内容放进 tokens
lastIndex = index + match[0].length //更改最后匹配的位置
}
if(lastIndex < text.length) { //要是普通文本在 双花括号后面的话
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return `_v(${tokens.join('+')})` //将内容 return 出去
}
}
}
// 孩子的转化
function genChildren(children) {
return children.map(child => gen(child)).join(',')
}
根据render方法产生虚拟节点
在 mountComponent 函数中 调用两个原型方法 (lifecycle.js)
export function mountComponent(vm, el) {
vm.$el = el;
// 1.调用 render 方法产生虚拟节点 虚拟DOM
// 2.根据虚拟 DOM 生成真实 DOM
// 3.插入到 el 元素中
vm._update(vm._render());
}
将 render 方法生成的 _c 、__v 、 _s 进行解析(lifecycle.js)
// _c 里面放标签、属性、孩子
Vue.prototype._c = function() {
return createElementVNode(this, ...arguments)
}
Vue.prototype._s = function (value) {
if(typeof value !== 'object') return value;
return JSON.stringify(value)
}
Vue.prototype._v = function () {
return createTextVNode(this, ...arguments)
}
_c 中的 createElementVNode 方法 和 _v 中的 createTextVNode 方法 (/vdom/index.js)
// h() _c()
export function createElementVNode(vm, tag, data, ...children) {
if(data == null) {
data = {}
}
let key = data.key;
if(key) {
delete data.key;
}
return Vnode(vm,tag,key,data,children,undefined)
}
// _v()
export function createTextVNode(vm, text) {
return Vnode(vm,undefined,undefined,undefined,undefined,text)
}
// ast 做的是语法层面上的转换 描述的是语法本身(html js css)
// 虚拟DOM 是描述 Dom 元素,可以增加一些自定义属性 (描述 DOM 的)
function Vnode(vm, tag, key, data, children, text) {
return {
vm,
tag,
key,
data,
children,
text
}
}
_render 方法 (lifecycle.js)
Vue.prototype._render = function () {
const vm = this;
// 当渲染的时候,会去实例中取值,我们就可以将属性和视图绑定在一起了
// call => 让 with 中的 this 指向 vm
return vm.$options.render.call(vm); //调用 ast 语法树转义后生成的 render 方法
}
将虚拟节点转化为真实DOM
_update 方法 (lifecycle.js)
Vue.prototype._update = function (Vnode) {//将 Vnode 转化为真实 Dom
const vm = this;
const el = vm.$el;
// patch既有初始化功能、又有更新功能
vm.$el = patch(el, Vnode);
}
_update 的 patch 方法 --- 根据虚拟节点创建真实 DOM,将新节点放在老节点下面,然后移除老节点。
// 根据虚拟节点创建真实 DOM
function patch(oldVnode,Vnode) {
// 初渲染流程
const isRealElement = oldVnode.nodeType; //判断是 真实元素 还是 虚拟节点
if(isRealElement) {
const el = oldVnode; //获取真实元素
const parentElm = el.parentNode; //获取父元素
let newElm = createElm(Vnode); //根据虚拟节点创建真实 DOM
parentElm.insertBefore(newElm, el.nextSibling); //将虚拟节点生成的真实节点放进老节点的下面
parentElm.removeChild(el); //移除老节点
return newElm
} else {
// diff 算法
}
}
patch 中的 createElm 方法 --- 根据 render 方法的数据,创建出虚拟节点
// 根据 render 方法的数据 创建出虚拟节点
function createElm(Vnode) {
let {tag, data, children, text} = Vnode;
if(typeof tag === 'string') { //说明是标签,处理标签
Vnode.el = document.createElement(tag); //将真实节点与虚拟节点对应起来,后续如果修改属性,(diff)就可以直接找到虚拟节点对应的真实节点
patchProps(Vnode.el, data); //标签的属性
children.forEach(child => { //处理儿子元素
Vnode.el.appendChild( createElm(child));
})
} else{ // 说明是文本,处理文本
Vnode.el = document.createTextNode(text);
}
return Vnode.el
}
createElm 中的 patchProps 方法 ---- 处理标签的属性
// 处理标签的属性
function patchProps(el, props) {
for(let key in props) {
if(key === 'style') { //属性是 style 的话
for(let styleName in props.style) {
el.style[styleName] = props.style[styleName];
}
} else { //普通属性的话,直接加进 el 就行
el.setAttribute(key, props[key]);
}
}
}
Vue2手写源码---响应式数据的变化的更多相关文章
- 学习 vue 源码 -- 响应式原理
概述 由于刚开始学习 vue 源码,而且水平有限,有理解或表述的不对的地方,还请不吝指教. vue 主要通过 Watcher.Dep 和 Observer 三个类来实现响应式视图.另外还有一个 sch ...
- vue源码之响应式数据
分析vue是如何实现数据响应的. 前记 现在回顾一下看数据响应的原因. 之前看了vuex和vue-i18n的源码, 他们都有自己内部的vm, 也就是vue实例. 使用的都是vue的响应式数据特性及$w ...
- 简单对比vue2.x与vue3.x响应式及新功能
简单对比vue2.x与vue3.x响应式 对响应方式来讲:Vue3.x 将使用Proxy ,取代Vue2.x 版本的 Object.defineProperty. 为何要将Object.defineP ...
- HDFS源码分析之数据块及副本状态BlockUCState、ReplicaState
关于数据块.副本的介绍,请参考文章<HDFS源码分析之数据块Block.副本Replica>. 一.数据块状态BlockUCState 数据块状态用枚举类BlockUCState来表示,代 ...
- jQuery 源码分析(十) 数据缓存模块 data详解
jQuery的数据缓存模块以一种安全的方式为DOM元素附加任意类型的数据,避免了在JavaScript对象和DOM元素之间出现循环引用,以及由此而导致的内存泄漏. 数据缓存模块为DOM元素和JavaS ...
- vue2.0与3.0响应式原理机制
vue2.0响应式原理 - defineProperty 这个原理老生常谈了,就是拦截对象,给对象的属性增加set 和 get方法,因为核心是defineProperty所以还需要对数组的方法进行拦截 ...
- angular,vue,react的基本语法—插值表达式,渲染数据,响应式数据
基本语法: 1.插值表达式: vue:{{}} react:{} angular:{{}} 2.渲染数据 vue js: export default{ data(){ return{ msg:&qu ...
- Vue实现双向绑定的原理以及响应式数据
一.vue中的响应式属性 Vue中的数据实现响应式绑定 1.对象实现响应式: 是在初始化的时候利用definePrototype的定义set和get过滤器,在进行组件模板编译时实现water的监听搜集 ...
- vue响应式数据变化
vue响应式数据变化 话不多说,先上代码: //拷贝一份数组原型,防止修改所有数组类型变量的原型方法 let arrayProto = Array.prototype;// 数组原型上的方法 let ...
随机推荐
- Servlet的url-pattern配置
url匹配规则 1)精确配置 精确匹配是指<servlet-mapping>中配置的值必须与请求中的url完全精确匹配. <servlet-mapping> <servl ...
- Pascal的旅行
[问题描述] 一块的nxn游戏板上填充着整数,每个方格上为一个非负整数.目标是沿着从左上角到右下角的任何合法路径行进,方格中的整数决定离开该位置的距离有多大,所有步骤必须向右或向下.请注意,0是一个死 ...
- python---双链表的常用操作
class Node(object): """结点""" def __init__(self, data): self.data = dat ...
- Jx.Cms开发笔记(二)-系统登录
界面 此界面完全抄了BootstrapAdmin css隔离 由于登录页面的css与其他页面没有什么关系,所以为了防止其他界面的css被污染,我们需要使用css隔离. css隔离需要在_Host.cs ...
- optimoptions requires Optimization Toolbox(optimoptions 需要 Optimization Toolbox)解决方法
问题:在下载版的matlab中做coursera的machine learning里的ex2,做到 1.2.3 Learning parameters using fminunc 时出现optimop ...
- 判断是否微信,qq等登陆。进去不同的页面下载
<!DOCTYPE html><html> <head> <meta charset="utf-8"> <title>安 ...
- JavaScript学习基础2
##JavaScript基本对象 1 .function:函数(方法)对象 * 创建: 1.var fun =new Function(形式参数,方法体): 2.function 方法名(参数){ 方 ...
- docker进阶_dockerswarm
DockerSwarm Docker Swarm简介 Docker Swarm的功能 Docker Swarm包含两个方面:docker安全集群,以及一个微服务应用引擎 集群方面,swarm将 ...
- python基础练习题(题目 使用lambda来创建匿名函数。)
day34 --------------------------------------------------------------- 实例049:lambda 题目 使用lambda来创建匿名函 ...
- python基础练习题(题目 计算两个矩阵相加)
day30 --------------------------------------------------------------- 实例044:矩阵相加 题目 计算两个矩阵相加. 分析:矩阵可 ...