npm init -y
进行一个初始化
npm install rollup rollup-plugin-babel @babel/core @babel/preset-env --save-dev
安装rollup,babel,然后让其在保存在本地,并且只在调试中运行
创建rollup.config.js,以便通过rollup进行打包。
将package.json里面的script配置项中的test配置项,名字换为dev,内容变为rollup -cw -c指定配置文件,-w监视文件变化
创建src入口文件夹,里面创建index.js作为入口文件
配置rollup.config.js文件
import babel from 'rollup-plugin-babel'
//rollup默认可以导出一个对象 作为打包的配置文件
export default{
input:'./src/index.js',//入口
output:{
file:'./dist/vue.js',//出口
format:'umd',//打包模式 常见有esm es6模块 commonjs模块 iife自执行函数 umd统一模块规范(commonjs && amd)
name:'Vue',// global.Vue
sourcemap:true,//希望可以调试代码
},
plugins:[
babel({
exclude: 'node_modules/**' //排除node_modules所有的文件
})
],//插件
}配置babel文件 .babelrc 预设引入插件
{
"presets":[
"@babel/preset-env"
]
}
Vue没有使用class进行创建,而是使用的构造函数,通过构造函数来扩建方法,这样扩建的方法可以放在不同文件中,而类必须放在一个类声明中,非常臃肿
index.js
//将所有的方法耦合在一起
import {initMixin} from './init'
function Vue(options){//options就是用户的选项
this._init(options);//初始化操作
}
initMixin(Vue);//扩展了init方法
export default Vueinit.js
import { initState } from "./state";
export function initMixin(Vue){//给Vue 增加init方法
Vue.prototype._init = function(options){//用于初始化操作
//vue vm.$options 就是获取用户的配置
const vm = this; //
vm.$options = options;//把用户配置赋值给vm.,挂载到vm身上 使用$标识表示这是vue里面的
//初始化状态
initState(vm);
}
}像这样,然后不同添加的方法可以放在某某文件中,然后进行导入添加上就OK。
比较多的方法可以放在其他文件里面
比如需要initState函数
state.js
import { observe } from "./observe/index";
export function initState(vm){
const opts = vm.$options;//获取所有的选项
if(opts.data){//如果有data选项那么初始化data
initData(vm);
}
}
function initData(vm){
let data = vm.$options.data;//data可能是函数,可能是对象
// debugger;
data = typeof data === 'function' ? data.call(vm) : data;//call进行执行函数\
vm._data = data;//将data挂载到vm._data
//对数据进行劫持,Vue2使用defineProperty
observe(data);
}对对象进行深度劫持
Vue2使用的api defineProperty
/observe/index.js
class Observer{
constructor(data){
//Object.defineProperty只能劫持已经存在的
this.walk(data);
}
walk(data){//循环对象 对属性依次劫持
// "重新定义"属性
Object.keys(data).forEach(key => defineReactive(data, key, data[key]));
}
}
export function defineReactive(data, key, val){//形成闭包,值不会消失
observe(val);//这里是递归处理对象,如果data的属性中存在对象,那么就会继续递归下去处理,因此Vue2的数据劫持性能较低,但是数组还没有处理
Object.defineProperty(data,key,{
get(){//取值会执行get
return val;
},
set(newVal){
if(newVal !== val){
val = newVal;
console.log('数据被修改了');
}
}
})
}
export function observe(data){
if(typeof data !== 'object' || data === null) return;//只对对象劫持
//如果一个对象被劫持了,那么就不需要再被劫持了(判断一个对象是否被劫持过,可以添加一个实例,用实例进行判定)
return new Observer(data);
}像上面完成了代理,但是比较阴间,因为每次需要data中的xxx属性都需要vm._data.xxx
因此可以将vm._data进行代理,放在vm上,这是state.js补充的内容
function proxy(vm,target,key){
Object.defineProperty(vm,key,{
get(){
return vm[target][key];
},
set(newVal){
vm[target][key] = newVal;
}
})
}
//.....省略了一些东西
function initData(vm){
//...
observe(data);
for(let key in data){//将vm._data用vm进行代理
proxy(vm,'_data',key);
}
}或者写成另一种情况
function initData(vm){
//...
observe(data);
proxy(vm,'_data');
}
function proxy(vm,target){
Object.keys(target).forEach(key =>{
Object.defineProperty(vm,key,{
get(){
return vm[target][key];
},
set(newVal){
vm[target][key] = newVal
}
})
})
}数组如果仍然是用对象的方式去添加,虽然能添加上,但是改动一个,又要全部重新更新,非常不方便。
修改数组很少使用索引进行直接操作,因为类似于 a[9999] = 0,内部进行劫持非常消耗性能。
而且大多数时候是通过shift,unshift,push,pop这些操作进行操作的
不可枚举enumerabel:false,是指不可循环、不可以取值。
observe/index.js
class Observe{
constructor(data){
//数组单独处理
data.__ob__ = this;//这里可以给data添加一个__ob__属性,这个属性指向Observer实例,这样就可以通过__ob__属性访问Observer实例了
Object.defineProperty(data, '__ob__', {
value:this,
enumerable:false//将ob变为不可枚举否则会陷入死循环,因为进入data后枚举到其__ob__属性(实际回到了本身),那么就会出现问题
})
// 同时给数据加了标识,数据上有ob那么对象被代理过,但是也需要其变成不可枚举属性
if(Array.isArray(data)){
//这里我们可以重写数组中的七个编译方法,可以修改数组本身的。除此之外还有数组内的引用方法也应该劫持,比如一个对象作为数组的内容
//同时保留其他的方法,因此需要在array.js里面重写
data.__proto__ = newArrayProto;
this.observeArray(data);//观测数组
}else{
this.walk(data);
}
}
walk(data){
Object.keys(data).forEach(key => defineReactive(data,key,data[key]))
}
observeArray(){
data.forEach(item => observe(item))
}
//剩下的和之前一样
}
//.....
export function observe(data){
if(typeof data !== 'object' || data === null) return;//只对对象劫持
//如果一个对象被劫持了,那么就不需要再被劫持了(判断一个对象是否被劫持过,可以添加一个实例,用实例进行判定)
if(data.__ob__ instanceof Observer){//说明被代理过 就不需要再次代理
return data.__ob__;
}
return new Observer(data);
}observe/array.js
//重写数组的部分方法
let oldArrayProto = Array.prototype;//获取数组的原型
export let newArrayProto = Object.create(oldArrayProto);//将原有的原型加在新数组原型上
//找到所有的变异方法
let methods =[
'push','pop','shift','unshift','splice','sort','reverse'
]
methods.forEach((method)=>{
//arr.push(1,2,3)
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'://arr.splice(0,1,{a:1})//第一个是删除的位置,第二个是个数
inserted = args.slice(2);
break;
default:
break;
}
if(inserted){
//对新增内容再次进行观测,这是新的数组,可以使用observeArray进行观测,那么可以将Observer的实例对象,放在data的某一个值上
ob.observeArray(inserted);
}
return result;
}
})比如插值语法等等
模版引擎 性能比较差需要正则匹配 1.0的时候,没有引入虚拟DOM的改变
2.采用虚拟DOM,数据变化后比较虚拟DOM的差异 最后渲染到页面
3.核心:我们需要将模版变成我们的js语法,通过js语法生成虚拟DOM
从一个东西变成一个东西,语法之间的转化,es6=>es5
css压缩 我们需要先变成语法树再重新组装代码成为新的语法 将template语法换成render函数
渲染优先顺序,render函数>template>el
需要安装新插件
npm install @rollup/plugin-node-resolve
这样可以导入的时候自动找寻index文件,就只用写import xxx from './compiler'实际上是import xxx from './compiler/index.js'
比如使用正则去匹配,正则的规则如下(Vue3不是使用正则)
const startTagOpen = new RegExp(`^<${qnameCapture}`);//它匹配到的分组是一个标签名<xxx> 匹配开始标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);//匹配的是</xxx> 最终匹配到结束标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;//匹配属性 a="xxxx" b = 'xxx' c = xxx
//第一个分组是属性的key,value是分组3或分组4或者分组5
const startTagClose = /^\s*(\/?)>/; //可以匹配<div>或者<br/>
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g;//匹配双括号,内容是表达式的变量首先在init里面进行修改,因为初始化完毕,那么就需要解析模版进行渲染了
init.js
import { initState } from "./state";
import { compileToFunction } from "./compiler/index";
export function initMixin(Vue){//给Vue 增加init方法
Vue.prototype._init = function(options){//用于初始化操作
//vue vm.$options 就是获取用户的配置
const vm = this; //
vm.$options = options;//把用户配置赋值给vm.,挂载到vm身上 使用$标识表示这是vue里面的
//初始化状态
initState(vm);
//判断是否由el属性
if(options.el){
vm.$mount(options.el);//挂载
}
}
Vue.prototype.$mount = function(el){
const vm = this;
el = document.querySelector(el);
let op = vm.$options;
if(!op.render){//如果没有render函数
let template;//没有render看是否写了template。没有写template采用外部的template
if(!op.template && el){//没有模版但是有el
template = el.outerHTML;
}else{
template = op.template;//如果有el则采用模版
}
// console.log(template);
if(template){//存在模版就对模版进行编译
const render = compileToFunction(template);
op.render = render;//把编译后的render函数赋值给render
}
}
op.render;
//script标签引用的vue.global.js这个编译过程是在浏览器进行的,runtime是不包含模版编译的,整个编译是打包的时候通过loader来转义.vue文件的
//用runtime的时候不能使用template模版的
}
}转化为ast(抽象语法树),可以使用栈结构。 遇到开始标签扔进去标签名,遇到结束标签弹出,这样就可以逐步得到,该标签的父结构和子结构分别是什么
主结构:compiler/index.js
import { parseHTML } from "./parse";
export function compileToFunction(template){
//1.将template转化为ast语法树
let ast = parseHTML(template);
// 2.生成render函数(render方法执行后返回的结果是虚拟DOM)
}compiler/parse.js将得到的字符串转化为html结构,DOM树
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`);//它匹配到的分组是一个标签名<xxx> 匹配开始标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);//匹配的是</xxx> 最终匹配到结束标签名 分组1是标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;//匹配属性 a="xxxx" b = 'xxx' c = xxx
//第一个分组是属性的key,value是分组3或分组4或者分组5
const startTagClose = /^\s*(\/?)>/; //可以匹配<div>或者<br/>
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g;//匹配双括号,内容是表达式的变量
export function parseHTML(html){// html最开始肯定是一个<
//最终需要转化为一颗抽象的语法树
const ELEMENT_TYPE = 1;//元素类型1
const TEXT_TYPE = 3;//文本类型3
const stack = [];//存放标签的栈
let currentParent = null;//指向栈中的最后一个
let root = null;//根节点
function createASTElement(tag,attrs){
return {
tag: tag,
type: ELEMENT_TYPE,
children: [],
attrs,
parent: null
}
}
function start(tag,attrs){//给标签名和属性
const node = createASTElement(tag,attrs);//创造ast结点
if(!root){
root = node;//如果树为空,则为根节点
}
if(currentParent){
node.parent = currentParent;//当前结点的父节点是当前的父节点
currentParent.children.push(node);//把它父亲结点的儿子指向它;
}
stack.push(node);//把当前的标签名压入栈中
currentParent = node;
}
function chars(text){//文本放在当前结点中
text = text.replace(/\s/g,'');//如果空格超过两个以上就删除两个以上
text &¤tParent.children.push({
type: TEXT_TYPE,
text,
parent: currentParent
})
}
function end(tag){
stack.pop();//弹出最后一个
currentParent = stack[stack.length - 1];
}
function advance(len){
html = html.substring(len);
}
function parseStartTag(){
const start = html.match(startTagOpen);//
if(start){
const match = {
tagName: start[1],
attrs: [],
// start: index
}
advance(start[0].length);//匹配上了就进行截取
// console.log(match,html);
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
}) //true是为了弄disabled
}
if(end)advance(end[0].length);//结束的尖角号
return match;
}
//如果不是开始标签的结束就一直匹配
// console.log(html);
return false;
}
while(html){
//如果textEnd为0,说明是一个开始标签或者结束标签 如果textEnd>0说明就是文本的结束位置
let textEnd = html.indexOf('<');//如果indexOf中的索引是0,则说明是个标签
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[1]);
// console.log(endTagMatch);
continue;
}
}
if(textEnd > 0){
let text = html.substring(0,textEnd);//文本内容
if(text){
chars(text);//将文本内容传递给chars
advance(text.length);
}
}
}
// console.log(root);
return root;
}compiler/index.js
这里面就是生成字符串以便后面的生成,_c是标签,_v是文本,_s是插值语法
import { parseHTML } from "./parse";
function genProps(attrs){
let str = '';
for(let i=0;i<attrs.length;i++){
let attr = attrs[i];
if(attr.name === 'style'){
// color:red ==> {color:'red'}
let obj = {};
attr.value.split(';').forEach(item=>{//qs库
let [key,value] = item.split(':');
obj[key] = value;
});
attr.value = obj;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`
}
return `{${str.slice(0,-1)}}`; //去掉最后一个逗号
}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;//匹配双括号,内容是表达式的变量
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)+'hello'+_s(age))
let tokens = [];
let match;
defaultTagRE.lastIndex = 0;//从文本开头执行匹配,每次exec后,lastIndex都会更新为下一次匹配开始的位置
let lastIndex = 0;//最后匹配的位置
while(match = defaultTagRE.exec(text)){
//使用正则来捕获文本
let index = match.index;//匹配的位置
if(index > lastIndex){//比如 {{name}} hello {{age}},取得就是 hello 这段
tokens.push(JSON.stringify(text.slice(lastIndex,index)));
}
tokens.push(`_s(${match[1].trim()})`)
lastIndex = index + match[0].length;//更新最后匹配的位置
}
// 防止插入语法后面还存在一些文本
if(lastIndex < text.length){
tokens.push(JSON.stringify(text.slice(lastIndex)));
}
return `_v(${tokens.join('+')})`
}
}
}
function genChildren(children){
return children.map(item=> gen(item));
}
function codegen(ast){
let children = genChildren(ast.children);
let code = (`_c('${ast.tag}',${ast.attrs.length > 0 ? genProps(ast.attrs) : 'null'}${ast.children.length > 0 ? `,${children}` : ',null'})`)
return code;
}
export function compileToFunction(template){
//1.将template转化为ast语法树
let ast = parseHTML(template);
// console.log(ast);
// 2.生成render函数(render方法执行后返回的结果是虚拟DOM)
//将ast树生成为类似于下面的字符串
// _c('div',{id:'app'},_c('div',{style:{color:'red'}},_v(_s(name)+'hello'),_c('span',undefine,_v(_s(age)))))
//
let code = codegen(ast);
code = `with(this){return ${code}}`
let render = new Function(code);//根据代码生成render函数
return render;
}模版引擎的实现原理就是with + new Function
即在compiler/index.js里面的这个
export function compileToFunction(template){
//1.将template转化为ast语法树
let ast = parseHTML(template);
// console.log(ast);
// 2.生成render函数(render方法执行后返回的结果是虚拟DOM)
//将ast树生成为类似于下面的字符串
// _c('div',{id:'app'},_c('div',{style:{color:'red'}},_v(_s(name)+'hello'),_c('span',undefine,_v(_s(age)))))
//
let code = codegen(ast);
code = `with(this){return ${code}}`
let render = new Function(code);//根据代码生成render函数
return render;
}完成之后需要在init.js里面去执行挂载
import {mountComponent} from './lifeCycle'
//...
Vue.prototype.$mount = function(el){
//....
mountComponent(vm,el);
}lifeCycle.js
export function initLifeCycle(Vue){
Vue.prototype._updata = function(vnode){
}
Vue.prototype._render = function(){
}
}
export function mountComponent(vm,el){
//调用render方法产生虚拟DOM
vm._updata(vm._render());//vm.$options.render()渲染虚拟结点,vm._update()生成真实DOM
// 根据虚拟DOM产生真实DOM
// 3.插入到el元素中
}Vue核心流程 1)创造了响应式 2)模版转化为ast语法树 3)将ast语法树转换为render函数 4)后续每次数据更新可以只执行render函数(无需再次执行ast转化过程)
render函数会产生虚拟节点(使用响应式数据)
再根据虚拟节点创造真实的DOM
当渲染函数render执行时,会执行_c,_v,_s等函数
当渲染时,会在事例中取值,我们就可以将属性和视图绑定在一起
_update函数就是将虚拟的DOM结点转化为真实的DOM节点
lifyCycle.js
import { createElementVNode } from "./vdom/index"
import { createTextNodeVNode } from "./vdom/index"
export function initLifeCycle(Vue){
Vue.prototype._updata = function(vnode){//将虚拟DOM转化为真实DOM
const vm = this;
const el = vm.$el;
//这里vnode是虚拟节点,是真实节点
patch(el,vnode);//使用vnode,更新出真正的dom
//patch既有初始化的功能,又有更新的功能
}
Vue.prototype._c = function(){
return createElementVNode(this,...arguments)
}
// _c('div',{},...children)
Vue.prototype._v = function(){
return createTextNodeVNode(this,...arguments)
}
// _v(text)
Vue.prototype._s = function(value){
if(typeof value !== 'object')return value;
return JSON.stringify(value);
}
Vue.prototype._render = function(){
const vm = this;
// 让with中的this指向vm
// console.log(vm.name,vm.age);
return vm.$options.render.call(vm);//通过ast语法转义后生成的render方法
}
}
export function mountComponent(vm,el){ //这里的el是通过querySelector处理过的
vm.$el = el;
//调用render方法产生虚拟DOM
vm._updata(vm._render());//vm.$options.render()渲染虚拟结点,vm._update()生成真实DOM
// 根据虚拟DOM产生真实DOM
// 3.插入到el元素中
}然后在
vdom/index.js中进行创建虚拟结点
// h函数,_c函数都是调用这些
export function createElementVNode(vm,tag,data,...children){
if(data==null){
data = {};
}
let key = data.key || null;
if(key){
delete data.key;
}
return VNode(vm,tag,key,data,children)
}
// _v
export function createTextNodeVNode(vm,text){
return VNode(vm,undefined,undefined,undefined,undefined,text)
}
// 创建虚拟DOM,和ast一样吗?
// ast是做的语法上的转化,他描述的是语法本身(可以描述js css html)
// 虚拟DOM是描述的dom元素,可以增加一些自定义属性(描述dom)
function VNode(vm,tag,key,data,children,text){
return {
vm,tag,key,data,children,text
}
}转化是发生在更新时,初始化也会有更新操作。
只不过初始化的更新,是从旧的DOM转化为由虚拟DOM产生的真实DOM。
后续的更新是diff算法进行比较进而更新
通过xxx.nodeType,如果这个xxx是元素节点上截取下来的那么,其值为1否则就是undefined
新的DOM产生时需要先将新的DOM放在旧DOM之后,再将其删除
lifeCycle.js
function createElm(vnode){
let {tag,data,children,text} = vnode;
if(typeof tag ==='string'){//标签
vnode.el = document.createElement(tag);// 这里将真实节点和虚拟节点对应起来,后续如果修改属性了
patchProps(vnode.el,data);// 处理data
children.forEach(child=>{
vnode.el.appendChild(createElm(child));
});
}else{//就是创建文本
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
function patchProps(el,props){
for(let key in props){ //styly{color:'red'}
if(key === 'style'){
for(let styleName in props.style){
el.style[styleName] = props.style[styleName];
}
}else{
el.setAttribute(key,props[key]);//这里将属性都设置到真实dom上
}
}
}
function patch(oldVNode,vnode){
//初渲染和后面的diff渲染一样的
const isRealElement = oldVNode.nodeType;//nodeType是js原生属性,如果是元素节点的那么值就是1
if(isRealElement){
const elm = oldVNode;//这里oldVNode是真实dom
const parentElm = elm.parentNode;//拿到真实元素
let newElm = createElm(vnode);
parentElm.insertBefore(newElm,elm);//将新节点插入到老节点后面
parentElm.removeChild(elm);//删除老节点
return newElm;
}else{
// diff算法
}
}生成节点操作
dependency collection
我们更新后节点需要自动去完成修改节点的操作,而非人为。即是观察者模式,在该模式下,会监听到数据变化从而更新视图
1.将数据处理为响应式,initState(针对对象来说主要是增加defineProperty,针对数组就是重写方法)
2.模板编译:将模板转为ast语法树,将ast语法树生成render方法
3.调用render函数会进行取值操作产生对应的虚拟DOM render(){_c(‘div’,null,-v(name))}
4.将虚拟DOM转化为真实DOM
一个属性对应一个dep,在一个视图中,一个watcher对应多个dep。(dep是监视属性的东西)
Vue里面是否是在使用数据的地方对应着一个watcher,一个属性对应着处使用,因此一个dep对应多个watcher
每个属性、对象、数组上都有一个 Dep 类型,Dep 类主要就是收集用于渲染的 watcher,Dep相当于发布者,如果有数据变动就调用发布者的更新方法
一个属性可能被多个组件用到,那么一个dep就对应着多个watcher。dep和watcher是多对多的关系
封装一个watcher类,监视数据变化
在挂载组件的的时候就需要给组件一个监视器,那么需要在lifeCycle那里改改
export function mountComponent(vm,el){ //这里的el是通过querySelector处理过的
vm.$el = el;
//调用render方法产生虚拟DOM
const updateComponent = vm._updata(vm._render());//vm.$options.render()渲染虚拟结点,vm._update()生成真实DOM
// 根据虚拟DOM产生真实DOM
// 3.插入到el元素中
new Watcher(vm,updateComponent,true);//这里的true标识着一个渲染过程
}这里传入true是为了判定是挂载还是更新
通过dep依赖对象和watcher监视属性和组件,那么在修改时就会非常方便
observe/dep.js
let id = 0;
class Dep{
constructor(){
this.id = id++;//属性的dep需要收集watcher
this.subs = [];//存放着当前属性对应的watcher有哪些
}
depend(){
//为了避免一个模板中使用两个数据导致重复收集,除了dep->watcher还希望watcher->dep
// this.subs.push(Dep.target);//收集watcher这样写会重复
Dep.target.addDep(this);//收集dep,先让watcher收集到dep,再让dep存储watcher
// 一个组件中由多个属性组成(那么对应一个watcher监视多个dep)
}
addSub(watcher){
this.subs.push(watcher);
}
notify(){
this.subs.forEach(watcher => watcher.update());//让视图去更新
}
}
Dep.target = null;
export default Dep;observe/watcher.js
import Dep from "./dep";
let id = 0;
// 1)当我们创建渲染watcher的时候我们会把当前的渲染watcher放在Dep.target上
// 2)调用_render()会取值走到get上
class Watcher{//不同组件有不同的watcher
constructor(vm,fn,options){
this.id = id++;
this.renderWatcher = options;
this.getter = fn;//getter意味调用这个函数可以发生取值操作
this.deps = [];//收集依赖
this.depsId = new Set();//收集依赖的id
this.get();
}
get(){
Dep.target = this;//静态属性只有一份
this.getter();//会去vm上取值,当渲染时就会有取值操作触发getter,然后在getter里面操作
Dep.target = null;//渲染完成就清空,只是在模板中收集的时候才会做依赖收集
}
addDep(dep){//一个组件对应多个属性,重复的属性无需记录
let id = dep.id;
if(!this.depsId.has(id)){
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this);
}
}
update(){
this.get();//重新更新
}
}
//需要给每个属性添加一个dep,目的是收集watcher
// n个dep对应一个视图(一个watcher)
// 一个属性对应多个组件,一个dep对应多个watcher 所以两者关系是多对多
export default Watcher;dep监视属性,watcher监视一个组件。
在修改属性时,调用dep.notify()方法,这个方法的添加可以在循环给每个值添加监视器的set里面(这样就能在更新后去对应的更新视图)
比如在observe/index.js中的defineProperty函数中
export function defineReactive(data, key, val){//形成闭包,值不会消失
//如果数据是对象那么再次递归处理进行劫持
observe(val);
let dep = new Dep();//每一个属性都有dep
Object.defineProperty(data,key,{
get(){//取值会执行get
if(Dep.target){
dep.depend();//让这个属性的收集器记住当前的watcher
}
return val;
},
set(newVal){
if(newVal !== val){
val = newVal;
dep.notify();//值更新了,通知更新视图
}
}
})
}当值更新,调用set方法,里面调用dep的notify方法,在此方法中遍历循环subs数组,每个里面的watcher(也就是使用到该属性的组件 )都调用update方法,在update里面调用this.get(),进行更新渲染
每个属性都有一个dep监视(属性就是被观察者),watcher是观察者(属性变化了会通知观察者来更新) ====》观察者模式
但是这个有缺陷,每次更新数据就立即更新页面,应该等待数据更新完毕再更新页面,这样性能更高。
目的:为了减少渲染次数,期望数据更新完毕,再进行渲染操作
那么可以考虑浏览器对于事件循环的操作。
浏览器都是先处理同步任务,然后执行异步任务,异步任务分为宏任务和微任务,浏览器优先清空微任务队列,再执行定时器等宏任务
同步任务>微任务>宏任务,那么可以利用这个进行更新
渲染放在一个异步任务中,那么只能等所有同步任务执行完毕才会去更新页面。使用watcher里面的更新
在observe/watcher.js里面添加一些东西
//.....以前的都一样,下面是改变内容
class Watcher{
//.....
update(){
queueWatcher(this);//将该监视器放入调度队列中
}
run(){
this.get();//真正执行渲染操作
}
}
let queue = [];//因为可能更新同一属性多次,那么需要去重,只保留最后一个
let has ={};//使用对象去重,或者set去重
let pending = false;//进行防抖操作,无论调用多少次,只执行一次
function flushSchedulerQueue(){
let flushQueue = queue.slice(0);//拷贝一下queue
flushQueue.forEach(q => q.run());//在刷新过程中可能存在新的watcher,重新被放回在队列中
queue = [];
has = {};
pending = false;
}
function queueWatcher(watcher){
let id = watcher.id;//取出每个监视器的唯一标识id
if(has[id] == null){
queue.push(watcher);
has[id] = true;
//可能有多个组件不管update多少次,最终只执行一次刷新操作
if(!pending){
nextTick(flushSchedulerQueue,0)//刷新调度队列
pending = true;
}
}
}
let callbacks = [];
let waiting = false;
function flushCallbacks(){
waiting = false;
let cbs = callbacks.slice(0);
callbacks = [];
cbs.forEach(cb=>cb());
}
let timerFunc;
if(Promise){
timerFunc = (flushCallbacks)=>{
Promise.resolve().then(flushCallbacks)
}
}else if(MutationObserver){
let observer = new MutationObserver(flushCallbacks);//这里传入的回调是异步执行的
let textNode = document.createTextNode(1);
observer.observe(textNode,{
characterData:true
});//让observer监控文本,如果数据变化,那么就执行cb任务
timerFunc = () =>{
textNode.textContent = 2;
}
}else if(setImmediate){
timerFunc = () =>{
setImmediate(flushCallbacks);
}
}else{
timerFunc = () =>{
setTimeout(flushCallbacks,0);
}
}
//nextTick中没有直接使用某个api,而是采用优雅降级的方式
// 内部采用promise(ie不兼容)降级为MutationObserver(h5的api) 可以再降级为ie专享setImmediate 降级为 setTimeout
export function nextTick(cb){//先内部还是先用户
callbacks.push(cb);//维护nextTick中的callback方法,同步操作
if(!waiting){
// debugger;
timerFunc();
waiting = true;
}
}这里面就相当于是对应一个watcher 执行一次run
但是这个有个问题,当一个属性被多个组件使用时,更改这个属性,那么还是要执行多次run,而且当使用promise.resolve.then或者同步来获取更新后的数据得到的却得到是更新前的(因为在获取时,页面并没有进行更新)
此时可以定义一个nextTick方法。同时在vue原型上添加。
Vue2在异步更新时是采取的降级方法,Promise => Mutation => setImmediate =>setTimeout
上面我们的代码只是重写了数组的方法,没有探究通过数组下标进行改变值是否能监视到
答案是不能
但是如果直接像这样 arr = []是可以更新的,因为劫持到了数组,但是数组内的元素变化没有劫持
给数组和对象本身都增加dep,当他们修改时就可以触发更新了
深层次嵌套会递归处理,递归的话性能就比较差
不存在的属性监控不到,存在的属性要重写方法
observe/index.js
//...在其中的改变一些东西
class Observer{
constructor(data){
//添加一个依赖收集器,其他和上面一样
this.dep = new Dep();
}
}
//然后再defineProperty里面进行一些改变
export default defineProperty(date,key,val){
let childOb = observe(val);//observe函数返回的Observer对象
let dep = new Dep();//每一个属性都有dep
Object.defineProperty(data,key,{
get(){//取值会执行get
if(Dep.target){
dep.depend();//让这个属性的收集器记住当前的watcher
if(childOb){//比如对象,数组类
childOb.dep.depend();//让数组或者对象本身进行依赖收集
if(Array.isArray(val)){//如果值还是数组
dependArray(val);
}
}
}
return val;
}//set这些和之前一样
})
}
//多一个函数
function dependArray(val){
for(let i = 0; i < val.length; i ++){
let current = val[i];
current.__ob__.dep.depend();//__ob__是Observer的实例对象,上面有dep收集器,调用其依赖收集的方法
if(Array.isArray(current)){
dependArray(current);//如果对象里面套对象再套对象
}
}
}observe/array.js
在这里面添加通知,数组改变了需要重新渲染模版
methods.forEach((method)=>{
newArrayProto[method] = function (...args){
//上面和之前一样
ob.dep.notify();//ob是数组的实例对象,然后在上面添加了dep属性
return result;
}
})如果是
new Vue({
el:'#app',
data:{
firstname:'赤',
lastname:'橙'
},
computed:{
fullname(){
return this.firstname + this.lastname;
}
}
})那么就是defineProperty中的get方法
如果是对象写法,那就需要再写
计算属性 依赖的值发生变化才会重新执行用户的方法 那么需要维持一个dirty属性, 默认计算属性不会立刻执行
计算属性就是一个defineProperty
计算属性也是一个watcher,默认渲染会创造 一个渲染watcher,放入队列中,先有渲染watcher,然后是计算属性watcher(使用到数据的地方就是会有watcher,管理每一个watcher的就是dep)
observe/dep.js
//添加一些方法,在暴露之前
let stack = [];
export function pushTarget(watcher){
let stack = [];
export function pushTarget(watcher){
stack.push(watcher);
Dep.target = watcher;
// 渲染时会将watcher入栈,渲染完就出栈
}
export function popTarget(){
stack.pop();
Dep.target = stack[stack.length - 1];
}
export default Dep;然后在watcher里面也需要修改
class Watcher{
constructor(vm,fn,options){
//和之前一样
this.lazy = options.lazy;
this.dirty = this.lazy;//缓存值
this.lazy?undefined:this.get();
this.vm = vm;//防止在计算属性取得getter时,调用的this不是对应的this
}
//方法只是修改了get和update,其他没有改变,添加了evaluate
get(){
pushTarget();
let val = this.getter().call(this.vm);//因为调用this.firstname时不使用回调,this会丢失
popTarget();
return value;
}
evaluate(){
this.value = this.get();//缓存存储值,多次用到而没有修改的计算属性时,就是拿到的缓存值
this.dirty = false;
}
update(){
if(this.lazy){//如果是计算属性,依赖属性变化了,就标识是脏值
this.dirty = true;
}else{
queueWatcher(this);//暂存watcher
// this.get();//重新更新
}
depend(){
let i = this.deps.length;
while(i--){
this.deps[i].depend();//让计算属性watcher也收集渲染watcher
}
}
}
}然后在初始化文件中去修改内容,因为初始化还包括初始化计算属性
计算属性根本不会收集依赖,只会让自己的依赖属性去收集依赖
state.js
export function initState(vm){
const opts = vm.$options;//获取所有的选项
if(opts.data){//如果有data选项那么初始化data
initData(vm);
}
if(opts.computed){
initComputed(vm);
}
}
//剩下的和之前一样除了我在下面声明的函数
}function initComputed(vm){
// debugger;
const computed = vm.$options.computed;
const watchers = vm._computedWatchers ={};//将计算属性watcher保存到vm上
//循环对象
for(let key in computed){
let userDef = computed[key];
//userDef 可能是对象可能是函数
//需要监控计算属性中get的变化,传入值,监视的实例,方法,配置项
let fn = typeof userDef === 'function' ? userDef :userDef.get;
watchers[key] = new Watcher(vm,fn,{lazy:true});
defineComputed(vm,key,userDef);
}
}
function defineComputed(target,key,userDef){
// const getter = typeof userDef === 'function' ? userDef :userDef.get;
const setter = userDef.set || (()=>{});
Object.defineProperty(target,key,{
get:createComputedGetter(key),//希望当重复取值时不会调用getter
set:setter
})
}
function createComputedGetter(key){
//我们需要检测是否执行这个getter
return function(){
const watcher = this._computedWatchers[key];//获取到对应属性的watcher
if(watcher.dirty){
//如果是脏的就去执行用户传入的函数
watcher.evalaute();//求值后 dirty变为false,下次取值,就不求值了
}
if(Dep.target){//计算属性出栈后还有渲染 watcher,应该也让计算属性中的watcher去收集上一层watcher
watcher.depend();
}
return watcher.value;//这样就不用每次取值都是get来取,可以从缓存中来取
}
}watch写法:1函数,2数组写法
底层最终调用的是$watch写法
在Vue原型链上添加$watch方法
index.js
Vue.prototype.$watch = function (exprOrFn, cb) {//还有deep:true,immediate等
// firstname
// ()=>{}
new Watcher(this, exprOrFn, { user: true },cb);
}因为这里调用了watcher,而且与之前不同,所以要进行修改
observe/watcher.js
class Watcher{
constructor(vm,exprOrFn,options,cb){
this.cb = cb;//对于watch
if (typeof exprOrFn === 'string') {
this.getter = function(){return vm[exprOrFn]}//将字符串变为函数
} else {
this.getter = exprOrFn;//getter意味调用这个函数可以发生取值操作
}
this.user = options.user//标识是否是用户自己的watcher
}
run(){
this.get();
if(this.user){
this.cb( );
}
}
}然后因为watch在一开始就要判断有无,那么在state.js里面要进行修改
export function initState(vm){
const opts = vm.$options
if(opts.watch){
initWatch(vm);
}
}
function initWatch(vm) {
let watch = vm.$options.watch;
for (let key in watch) {//字符串,数组,函数
const handler = watch[key];
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
}else{
createWatcher(vm, key, handler);
}
}
}
function createWatcher(vm,key,handler){//字符串,数组,函数(还有可能是对象)
if(typeof handler === 'string'){
handler = vm[handler];
}
return vm.$watch(key,handler);
}
//这些上面部分是改变内容我们之前的更新非常的暴力,直接生成新的虚拟结点,通过新的虚拟姐弟那生成真实节点,生成后替换掉老的节点
但是这样子性能消耗非常大
第一次渲染的时候,我们会产生虚拟节点,第二次更新会调用render方法产生新的虚拟节点。比对出需要更新的内容更新部分内容
先将一些方法从lifeCycle.js放在state.js里面
export function initStateMixin(Vue) {
Vue.prototype.$nextTick = nextTick;
Vue.prototype.$watch = function (exprOrFn, cb) {//还有deep:true,immediate等
// firstname
// ()=>{}
new Watcher(this, exprOrFn, { user: true }, cb);
}
}将之前渲染节点的函数放在vdom/patch.js里面,顺便更新内容
vdom/patch.js
export function createElm(vnode){
//内容省略,除了以下函数改变
patchProps(vnode.el,{},data)
}
export function patchProps(el,oldProps = {},props = {}){
//可能老的属性有而新的属性没有的情况需要去除老的
let oldStyle = oldProps.style || {};
let newStyle = props.style || {};
for (let key in oldStyle) {//老的样式中有而新的样式中没有,则删除
if (!newStyle[key]) {
el.style[key] = '';
}
}
for (let key in oldProps) {
if (!props[key]) {
el.removeAttribute(key);//老的属性中有而新的没有,则删除属性
}
}
//剩下内容省略(和之前相同)
}
export function patch(oldVNode,vnode){
//初渲染和后面的diff渲染一样的
const isRealElement = oldVNode.nodeType;//nodeType是js原生属性,如果是元素节点的那么值就是1
if(isRealElement){
const elm = oldVNode;//这里oldVNode是真实dom
const parentElm = elm.parentNode;//拿到真实元素
let newElm = createElm(vnode);
parentElm.insertBefore(newElm,elm);//将新节点插入到老节点后面
parentElm.removeChild(elm);//删除老节点
return newElm;
}else{
// diff算法
patchVnode(oldVNode,vnode);
}
}
function patchVnode(oldVNode, vnode) {
if (!isSameVnode(oldVNode, vnode)) {
//使用老节点的父亲进行替换
let el = createElm(vnode)
oldVNode.el.parentNode.replaceChild(el, oldVNode.el)
return el;
}
//文本情况,期望对文本内容进行比较
let el = vnode.el = oldVNode.el //复用老节点的元素
if (!oldVNode.tag) {//是文本
if (oldVNode.text !== vnode.text) {
el.textContent = vnode.text;//用新的文本覆盖掉老的
}
}
//是标签 需要比对标签的属性
patchProps(el, oldVNode.data, vnode.data);
//比较儿子节点 比较一方有儿子,一方没有儿子 两方都有儿子
let oldChildren = oldVNode.children || [];//防止取到的是一个值
let newChildren = vnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
//完整的diff算法(需要比较两个人的儿子)
updateChildren(el, oldChildren, newChildren);
} else if (newChildren.length > 0) {//没有老的儿子直接插入
mountChildren(el, newChildren);
} else if (oldChildren.length > 0) {//新的没有,老的有,需要删除
el.innerHTML = '';//也可以循环删除
}
return el;
}
function mountChildren(el, newChildren) {
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i];
el.appendChild(createElm(child));
}
}
function updateChildren(el, oldChildren, newChildren) {
//比较两个儿子的时候,为了增高性能会有优化手段
let oldStartIndex = 0;//老儿子开始的位置
let newStartIndex = 0;//新儿子开始的位置
let oldEndIndex = oldChildren.length - 1;//老儿子结束的位置
let newEndIndex = newChildren.length - 1;//新儿子结束的位置
let oldStartVnode = oldChildren[oldStartIndex];
let oldEndVnode = oldChildren[oldEndIndex];
let newStartVnode = newChildren[newStartIndex];
let newEndVnode = newChildren[newEndIndex];
function makeIndexByKey() {
let map = {};
children.forEach((child, index) => {
map[child.key] = index;
})
return map;
}
let map = makeIndexByKey(oldChildren);
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIndex];
} else if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex];
}
//有一方大于尾指针就停止
else if (isSameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)// 如果是相同节点,则递归比较子节点
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
}
else if (isSameVnode(oldEndVnode, newEndVnode)) {//比较尾节点
patchVnode(oldStartVnode, newStartVnode)// 如果是相同节点,则递归比较子节点
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
}
// 交叉比较 abcd -> dabc
// 头尾比对和尾头比对,同时处理的倒序的情况
else if (isSameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode);
el.insertBefore(oldEndVnode.el, oldStartVnode.el)//将老的后面的节点插入到开头节点的前面
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex];
}
else if (isSameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode);
el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling)//将老头节点放在尾节点之后
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex];
}
// 在给动态列表添加key时,尽可能避免使用索引,无论你怎么改变索引都是从0开始非常容易错乱
else {
// 乱序比对
let moveIndex = map[newStartVnode.key];//如果拿到则说明是要移动的索引
if (moveIndex !== undefined) {
let moveVnode = oldChildren[moveIndex];//找到对应的虚拟节点 ,复用
el.insertBefore(moveVnode.el, oldStartVnode.el);
oldChildren[moveIndex] = undefined//标识这个节点清空了
patch(moveVnode, newStartVnode);
} else {//找不到的情况
el.insertBefore(createElm(newStartVnode), oldStartVnode.el);
}
}
}
if (newStartIndex <= newEndIndex) {//多余的塞进去
for (let i = newStartIndex; i <= newEndIndex; i++) {
let childEl = createElm(newChildren[i]);
// 可能像后追加,可能向前追加
// el.appendChild(childEl)
let anchor = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].el : null; //获取下一个元素
el.insertBefore(childEl, anchor);//当anchor为null的时候,就会认为是appendChild
}
}
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
if(oldChildren[i]){
el.removeChild(oldChildren[i].el);
}
}
}
}每次获取dom需要计算位置,因此比较消耗性能。希望比较两个节点的差异之后再进行替换
diff算法是一个评级比较的过程,父亲和父亲比,儿子和儿子比
比较节点:不是同一节点直接替换(删除老的,换上新的)。
两个节点是同一个节点(判断节点的tag和节点的key),比较两个节点的属性是否由差异(复用老节点,将差异的属性更新)
文本进行特判,先判断是否有tag属性,文本是没有的,然后再进行比对,如果不同,那么使用新节点的内容替换掉老节点的内容
Vue2中通过双指针的方式比较两个节点(子节点 )
在lifeCycle.js里面将第一次产生的虚拟结点保存到_vnode上。更新 _update函数
export function initLifeCycle(Vue){
Vue.prototype._update = function(vnode){
const vm = this;
const el = vm.$el;
//这里vnode是虚拟节点,是真实节点
const preVnode = vm._vnode;
vm._vnode = vnode;//把组件第一次产生的虚拟节点保存到_vnode上
if(preVnode){//之前渲染
vm.$el = patch(preVnode,vnode)
}else{
vm.$el = patch(el,vnode);//使用vnode,更新出真正的dom
}
//patch既有初始化的功能,又有更新的功能
}
}组件的定义方式:全局注册和局部注册
Vue.component('my-button',{
template:`<button></button>`
})
new Vue({
el:'#app',
component:{
'mu-button':{
template:`<button></button>`
}
}
})其查找方式类似于JS的原型链,先在自身找,找不到再往外找。
实际上是调用的Vue.extend这个API
Vue.component('my-button',Vue.extend({template:'<button></button>'}))
创建组件就相当于创建一个子类
在globalAPI里面(改自Vue源码)
import { observe } from './observe/index'
import {mergeOptions} from './utils'
export function initGlobalAPI(Vue) {
// config
const configDef = {}
configDef.get = () => config
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
// 2.6 explicit observable API
Vue.observable = function(obj){
observe(obj)
return obj
}
Vue.options = Object.create(null)
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options = {
_base:Vue,
}
Vue.mixin = function(mixin) {
this.options = mergeOptions(this.options, mixin)
return this
}
Vue.extend = function(options) {
//实现根据用户的参数返回一个构造函数
function Sub(options = {}){
//调用Vue的构造函数
this._init(options)
}
Sub.prototype = Object.create(this.prototype)//Sub.protoytype.__proto__ = Vue.protoType
Sub.prototype.constructor = Sub
//将用户传递的参数和全局的Vue.options来合并
Sub.options = mergeOptions(Vue.options, options);
return Sub
}
Vue.options.components = {}//全局的指令 Vue.options.directives
Vue.components = function(id, definition){
//如果是函数直接返回,不是函数就进行包装
definition = typeof definition === 'function' ? definition : Vue.extend(definition)
Vue.options.components[id] = definition
}
}utils.js
export function mergeOptions(ops = {}, mixin) {
// 创建一个空对象作为合并后的结果
const options = {};
// 定义一个合并策略对象,存放不同属性名对应的合并策略函数
const strats = {};
// 定义一个默认的合并策略函数,如果没有找到对应的策略函数,就使用它
const defaultStrat = function (parentVal, childVal) {
// 如果子选项有值,就使用子选项的值,否则就使用父选项的值
return childVal === undefined ? parentVal : childVal;
};
// 定义一个合并字段的函数,用于遍历属性并调用合并策略函数
function mergeField(key) {
// 根据属性名查找合并策略对象,如果没有找到,就使用默认的合并策略函数
const strat = strats.hasOwnProperty(key) ? strats[key] : defaultStrat;
// 调用合并策略函数,传入父选项和子选项的属性值,以及当前的属性名
options[key] = strat(ops[key], mixin[key], key);
}
// 遍历父选项的所有属性,并调用合并字段的函数
for (let key in ops) {
mergeField(key);
}
// 遍历子选项的所有属性,并调用合并字段的函数
for (let key in mixin) {
// 如果父选项没有该属性,才需要调用合并字段的函数
if (!Object.hasOwn(ops, key)) {
mergeField(key);
}
}
// 返回合并后的结果对象
return options;
}在init.js里面也需要做出小小的改变,因为有多个组件(多个vue)那么就需要合并选项
export function initMixin(Vue){
Vue.prototype._init = function(options){
const vm = this;
vm.$options = mergeOptions(this.constructor.options,options)
//其他的不变
}
}创建完之后要对组件和标签进行一个区分,那么在以前生成虚拟结点的地方就存在一些问题。
那么需要判定是否为真实的标签
那么在vdom/index.js里面就要进行改变
const isReservedTag = (tag) => {
return [
"a",
"ul",
"ol",
"li",
"div",
"span",
"p",
"img",
"input",
"button",
"textarea",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"table",
"tr",
"td",
"th",
"tbody",
"thead",
"tfoot",
"tr",
"th",
"td",
"select",
"option",
"form",
].includes(tag);
};
export function createElementVNode(vm, tag, data, ...children) {
if (data == null) {
data = {};
}
let key = data.key || null;
if (key) {
delete data.key;
}
//判断是否为原生标签
if(isReservedTag(tag)){
return VNode(vm, tag, key, data, children);
}else{
//创建组件的虚拟结点
// 需要包含组件的构造函数
let Ctor = vm.$options.components[tag];
// Ctor可能是组件的定义,可能是一个Sub类,还有可能是组件的component选项
return createComponentVnode(vm,tag,key,data,children,Ctor);
}
}
function createComponentVnode(vm,tag,key,data,children,Ctor){
if(typeof Ctor === 'Object'){
Ctor = vm.$options._base.extend(Ctor);//将对象转化一下得到构造函数,_base声明于globalAPI上面的
}
data.hook = {
init(){//稍后创造真实节点的时候,如果是组件则调用此init方法
}
}
return VNode(vm,tag,key,data,children,null,{Ctor})
}
//然后VNode代码需要重构一下,因为之前没有处理component,多了一个componentOptions的选项
function VNode(vm, tag, key, data, children, text, componentOptions) {
return {
vm,
tag,
key,
data,
children,
text,
componentOptions//组件的构造函数
};
}在vdom/index.js里面继续完善
function createComponentVnode(vm, tag, key, data, children, Ctor) {
if (typeof Ctor === 'object') {
Ctor = vm.$options._base.extend(Ctor);//将对象转化一下得到构造函数,_base声明于globalAPI上面的
}
data.hook = {
init(vnode) {//稍后创造真实节点的时候,如果是组件则调用此init方法
//保存组件的实例到虚拟节点
let instance = vnode.componentInstance = new vnode.componentOptions.Ctor
instance.$mount()
}
}
return VNode(vm, tag, key, data, children, null, { Ctor })
}在vdom/patch.js里面,需要修改createElm函数,因为还需要判定组件和元素
function createComponent(vnode) {
let i = vnode.data
if((i = i.hook) && (i = i.init)){
i(vnode)
}
if(vnode.componentInstance){
return true // 说明是组件
}
}
export function createElm(vnode) {
let { tag, data, children, text } = vnode;
if (typeof tag === 'string') {//标签
//创建真实元素也要区分组件还是元素
if(createComponent(vnode)){
//组件
return vnode.componentInstance.$el//返回组件对应的真实元素
}
vnode.el = document.createElement(tag);// 这里将真实节点和虚拟节点对应起来,后续如果修改属性了
patchProps(vnode.el, {}, data);// 处理data
children.forEach(child => {
vnode.el.appendChild(createElm(child));
});
} else {//就是创建文本
vnode.el = document.createTextNode(text);
}
return vnode.el;
}然后再判定createComponent时,就会进入data.hook对象里面的init()组件渲染函数
因为Ctor是构造函数,那么就调用new方法,将实例保存下来,然后进行挂载。挂载时,因为传入的为空,那么就会进入到mountComponent(vm,el)方法,然后el仍然没有值,调用vm._update,进入其中后,调用patch方法,因为patch(el,vnode),el为空,那么就需要在vdom/patch.js上面修改以下patch方法
export function patch(oldVNode, vnode) {
if(!oldVNode){ //这就是组件的挂载
return createElm(vnode)//vm.$el 对应的就是组件渲染的结果了
}
//后面都一样
}因为patch的返回值就是挂载的vm.$el,那么在实例instance上就多了一个 $el
组件的真实元素是template生成render之后 , Vnode生成的真实节点
- 使用npm install安装依赖
- 代码的目录结构:
- bechmarks性能测试的
- dist最终打包的结果
- examples官方的例子
- flow类型检测(现在被ts代替了)
- packages一些写好的包
- scripts所有打包的脚本
- src源代码目录 compiler专门做模板编译的
- core Vue2的核心代码
- platform
- server服务端渲染相关的
- sfc解析单文件组件的
通过package.json 找到打包入口
scripts/config.js (full-dev runtime-cjs-dev runtime-esm.......)
web-runtime(运行时 无法解析new Vue传入的template) web-full(runtime + 模板解析) compiler(只有compiler)
cjs esm(支持import 和export 导入模块) browser umd
cjs使用的 require导入模块,导入的模块时拷贝形式,如果修改其中的值,导入的东西并不会改变
esm导入的是地址,那么进行操作修改包内的数据时,再次使用得到的是修改后的数据
cjs
// cjs_module1.js
var count = 1;
function incCount() {
count += 1;
}
module.exports = {
count: count,
incCount: incCount,
}
// cjs_demo.js
var { count, incCount } = require('./cjs_module1.js');
console.log(count); // 1
incCount();
console.log(count); // 1esm
// esm_module1.js
let count = 1;
function incCount() {
count += 1;
}
export {
count,
incCount,
}
// esm_demo.js
import { count, incCount } from './esm_module1.js';
console.log(count); // 1
incCount();
console.log(count); // 2在html中使用esm,type=“module”是关键
<script src="./esm_main.js" type="module"></script>- 打包的入口
-
src/platforms/web/entry-runtime.js
src/platforms/web/entry-runtime-with-compiler.js(两个入口的区别是带有compiler的会重写$mount,将template变成render函数)
runtime/index.js(所谓的运行时,会提供一些操作DOM的API 、属性操作、元素操作,提供一些组件和指令 )
-
对于里面的方法如何执行
(1)了解核心流程,单独打开源码去看
(2)不知道流程,可以通过测试样例,或者自己写一些样例进行实现
指定sourcemap参数 可以开启代码调试
比如在package.json的script里面,scripts/config.js后面加入 -s 或者 -sourcemap
在globalAPI里面 Object.defineProperty(Vue.'config',configDef)进行配置信息
Vue.util装载的工具方法,比如warn,extend,mergeOptions,defineReactive(extend合并操作,mergeOptions合并策略,defineReactive定义响应式)
set,delete,nextTick 2.6新增了observable让一个对象变成响应式
ASSET_TYPES里面存入了,components,directives,filter这些
还有mixin,use,extend
可以监控一个数据的修改和获取操作,针对对象格式给每个对象的属性进行劫持 使用 Object.defineProperty
源码层面:initData -> observe ->defineReactive方法(内部对所有属性进行重写,因此存在性能问题),递归的给每个属性添加getter和setter
我们使用Vue的时候如果层级过深(考虑性能),如果数据不是响应式就不要放在data中了。属性取值,尽可能避免多次取值。如果有些对象放在data中但是不是响应式的,可以使用Object.freeze()进行冻结
Vue2中没有使用defineProperty进行检测,因为直接修改索引的情况很少,通常是使用push,pop等方法进行修改,那么就需要对这些方法进行重写
并且对每个数据进行检测。
注意这里重写是针对的每一个在data里面的数组,并没有在Array的原型链上直接覆盖。
还有通过索引进行修改数据无法进行实时渲染,比如arr[1]=100;arr.length = 300
但是arr[0].x=100是可以的,因为可以监视对象的每一个属性的修改和取值操作。因此会触发更新
-
被观察者指代的是数据(dep),观察者(Watcher,3种,渲染watcher,计算属性 ,用户watcher)
-
一个watcher中可能有多个数据,因此watcher中还需要保存dep。
-
多对多的关系,一个dep对应多个watcher,一个watcher会有多个dep。默认是在渲染时,进行依赖收集
计算属性和清理会用到watcher上的dep。
在进行取值的时候,watcher收集dep(相当于粉丝订阅自己喜欢的人一样)
然后让dep收集watcher(相当于up主得到粉丝名单),以便up主更新时去通知粉丝去看最新视频(也就是属性改变,通知相应的watcher进行一个更新)
为什么要清理watcher,比如使用v-if的指令第一次渲染了name第二次渲染的age那么进行清理,当name更新时,会通知视图进行更新,但是此时没用到这个值,也没必要更新
编译原理:用户传递的template属性,需要将这个template编译为render函数。
- template -> ast语法树
- 对语法树进行标记(标记的是静态节点)
- 将ast语法树生成render函数
最终每次渲染可以调用render函数返回对应的虚拟结点(递归是先子后父)
内部利用发布订阅模式,将用户写的钩子维护称了一个数组,后续一次调用 callHook
策略模式
渲染顺序:父->子->子完->父完那么子组件挂载了之后父组件才会挂载完毕
生命周期钩子:beforeCreate created beforeMount mounted beforeUpdate updated actived deactived beforeDestory destoryed errorCaptured
在beforeCreate中实现的initEvent(初始化$ on ,off,emit等事件 )和initLifecycle(因为什么事情都没有干,因此Vue3中直接取代了)
在create中实现了 initInjections(inject)和initState(响应式数据处理),InitProvide(provide),可以拿到响应式数据而且不涉及到dom渲染,这个api可以在服务端渲染中使用
beforreUpdate每次更新之前调用,在清空队列的时候,会去调用各个watcher的beforUpdate方法
beforDestory手动移除能触发:销毁的时候,是销毁watcher而不是销毁DOM(react是销毁DOM)
destroyed:触发在:路由切换,v-if切换组件,:is动态绑定组件




