Vue核心原理
一、使用Rollup搭建开发环境
1、官网
2、什么是Rollup?
Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码, rollup.js更专注于Javascript类库打包 (开发应用时使用Webpack,开发库时使用Rollup)
3、环境搭建
(1) 安装命令
npm install rollup rollup-plugin-babel @babel/core @babel/preset-env rollup-plugin-serve -D
(2) rollup.config.js文件编写
import babel from 'rollup-plugin-babel'
import serve from 'rollup-plugin-serve'
export default {
input: './src/index.js', // 打包入口
output: {
format: 'umd', //模块化类型
name: 'Vue', // 全局变量的名字
file: 'dist/umd/vue.js', // 打包输出文件名称
sourcemap: true // 打包前后源码映射,方便调试
},
plugins: [
babel({
exclude: 'node_modules/**' // 排除目录
}),
serve({
// open: true, // 打开默认浏览器
port: 3000, // 端口
contentBase: '', // 以当前目录为根目录标准
openPage: '/index.html' // 默认打开入口文件
})
]
}
(3) 配置.babelrc文件
{
"presets": [
// es6转es5
"@babel/preset-env"
]
}
(4) 新建一个html入口文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- 引入vue文件 -->
<script src="dist/umd/vue.js"></script>
<script>
console.log(Vue);
</script>
</body>
</html>
(5) 配置package.json
"scripts": {
"dev": "rollup -c -w"
},
(6) 执行命令
npm run dev
二、响应式原理
1、定义并导出构造函数
// src\index.js
import { initMixin } from "./init";
// Vue构造函数
function Vue(options) {
// 入口方法,做初始化操作
this._init(options)
}
// 插件思想:对原型进行扩展
initMixin(Vue)
export default Vue
2、定义初始化混合方法
// src\init.js
import { initState } from "./state"
// 定义init方法,进行扩展Vue的原型
export function initMixin(Vue) {
// 初始化方法
Vue.prototype._init = function(options) {
// 拿到当前实例
const vm = this
// 拿到配置参数挂载到$options
vm.$options = options
// 初始化状态:将数据做一个初始化的劫持,数据改变就去更新视图
initState(vm)
// 其他初始化方法
// initEvents
}
}
3、初始化状态
// src\state.js
/**
* 初始化状态
* 顺序:props > methods > data > computed > watch
*/
export function initState(vm) {
const opts = vm.$options
// 按照顺序依次拆分初始化
if (opts.props) {
initProps(vm)
}
if (opts.methods) {
initMethods(vm)
}
if (opts.data) {
initData(vm)
}
if (opts.computed) {
initComputed(vm)
}
if (opts.watch) {
initWatch(vm)
}
}
function initProps(vm) {}
function initMethods(vm) {}
function initData(vm) {}
function initComputed(vm) {}
function initWatch(vm) {}
4、初始化数据
// src\state.js
import { observe } from "./observer/index"
// 初始化数据方法
function initData(vm) {
let data = vm.$options.data;
// 拿到data属性,如果是函数直接执行,其余放行
vm._data = data = typeof data === 'function' ? data.call(vm) : data;
// 数据劫持方案
// 对象Object.defineProperty
// 数组 单独处理:拦截可以改变数组的方法进行操作
// 观测数据
observe(data);
}
5、对象劫持-递归属性
- data本身为对象,需要观测
- data对象里面还有对象,需要递归观测
- 给data设值的新值也是对象,需要进行递归观测
// src\observer\index.js
/**
* 数据观测类
* 使用defineProperty 重新定义属性
*/
class Observer {
constructor(value) {
// 判断一个对象是否被观测过,看他有没有__ob__这个属性
Object.defineProperty(value, '__ob__', {
enumerable: false, // 不能被枚举,不能被循环出来
configurable: false,
value: this // 注入当前的实例对象
})
// 对象处理
this.walk(value);
}
walk(data) {
// 遍历对象,进行循环观测
let keys = Object.keys(data);
keys.forEach(key => {
defineReactive(data, key, data[key]); // 源码对应 > Vue.util.defineReactive
})
}
}
// ES5的双向数据绑定类
// vue2慢的核心原因就是这个方法
// vue2 应用了defineProperty需要一加载的时候 就进行递归操作,所以好性能,如果层次过深也会浪费性能
// 1.性能优化的原则:
// 1) 不要把所有的数据都放在data中,因为所有的数据都会增加get和set
// 2) 不要写数据的时候 层次过深, 尽量扁平化数据
// 3) 不要频繁获取数据
// 4) 如果数据不需要响应式 可以使用Object.freeze 冻结属性
function defineReactive(data, key, value) {
// 如果值是对象进行递归观测
observe(value);
// 定义双向绑定,进行get和set的观测
Object.defineProperty(data, key, {
get() {
console.log('取值');
return value
},
set(newValue) {
console.log('设值');
// 值没变化就跳过
if (newValue == value) return;
// 如果用户设值的值是对象,需要再次进行递归观测
observe(newValue);
// 更新值
value = newValue;
}
})
}
/**
* 数据观测
*/
export function observe(data) {
// 对象数据校验:不是对象 或 null 就返回
if (typeof data !== 'object' || data === null) {
return data
}
// 如果数据被观测过,直接返回,防止重复观测
if (data.__ob__) {
return data
}
// 数据观测
new Observer(data);
}
6、数组劫持-重写原型
- 如果data是数组,需要重写能改变数组的七个方法
- 如果data数组里面每个item都是对象,那么需要循环遍历,观测每一项item数据
- 如果给data数组新增的数据也是对象,那么新增的对象也需要进行观测
// src\observer\index.js
import { arrayMethods } from "./array";
// 1、改写观测类
class Observer {
constructor(value) {
if (Array.isArray(value)) {
// 数组处理:函数劫持、切片编程思想
// 重写push shift pop unshift splice sort reverse
value.__proto__ = arrayMethods;
// 观测数组中的对象类型
this.observeArray(value);
} else {
// 对象处理
this.walk(value);
}
}
// 数组观测方法
observeArray(value) {
// 遍历数组的每一项进行观测
value.forEach(item => {
observe(item);
})
}
}
// src\observer\array.js
// 拿到数组原型上的方法
let oldArrayProtoMethods = Array.prototype
// 原型继承 arrayMethods.__proto__ = oldArrayProtoMethods
export let arrayMethods = Object.create(oldArrayProtoMethods)
let methods = [
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice'
]
// 数组方法重写
methods.forEach(method => {
arrayMethods[method] = function(...args) {
console.log('数组调用');
// this为observer里面的value
const result = oldArrayProtoMethods[method].apply(this, args);
let inserted;
let ob = this.__ob__;
switch (method) {
case 'push':
case 'unshift':
// 如果追加的内容也是对象,需要再次进行对象劫持
inserted = args;
break;
case 'splice':
// 截取参数下标为2到末尾:arr.splice(0, 1, {a: 1})
inserted = args.slice(2);
default:
break;
}
// 如果给数组新增的值是对象要继续进行观测
if (inserted) ob.observeArray(inserted)
return result
}
})
7、数据代理
- 将取值全部代理到vm上面 vm.message = vm._data.message
// src\state.js
// 代理方法
function proxy(vm, data, key) {
Object.defineProperty(vm, key, {
get() {
return vm[data][key]; // vm_data.a
},
set(newValue) { // vm.a = 100
vm[data][key] = newValue; // vm._data.a = 100
}
})
}
// 初始化数据
function initData(vm) {
let data = vm.$options.data;
vm._data = data = typeof data === 'function' ? data.call(vm) : data;
// 用代理,从vm取属性,代理到vm_data上
for (let key in data) {
proxy(vm, '_data', key)
}
// 观测数据
observe(data);
}
三、模板编译
两种页面挂载方式
// 1. 参数中挂载
new Vue({ el: '#app'})
// 2. 手动挂载
vm.$mount("#app")
0、定义挂载函数-获取模板
- 默认先找render方法
- 没有render方法会查找template
- 没有template会找当前el指定的元素中的内容来进行渲染
// src\init.js
import { compileToFunction } from "./compiler/index"
// 初始化方法
Vue.prototype._init = function(options) {
// 拿到当前实例
const vm = this
vm.$options = options
// 初始化状态
initState(vm)
// 如果当前有el属性,需要进行模板渲染
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
}
// 挂载函数
Vue.prototype.$mount = function(el) {
// 拿到当前实例
const vm = this;
const options = vm.$options;
// 获取dom对象
el = document.querySelector(el);
// 1. 如果没有render方法,需要将template转化为render方法
if (!options.render) {
// 判断是否配置了模板
let template = options.template;
// 如果没有模板但是有el,就获取整个外部HTML
if (!template && el) {
template = el.outerHTML;
}
// 编译原理:将模板编译成render函数
const render = compileToFunction(template);
options.render = render
}
// 2. 有render方法
// 渲染最终用的都是这个render方法
// 需要挂载这个组件
// mountComponent(vm, el);
}
1、解析模板-标签和内容
- ast 抽象语法树,用对象来描述语言本身
- 虚拟dom,用对象来描述节点
// src\compiler\index.js
import { parseHTML } from "./parse";
export function compileToFunction(template) {
// 1、将模板转为ast
let ast = parseHTML(template);
console.log(ast);
}
// src\compiler\parse.js
// 思路:利用正则匹配字符串,匹配到了就截取字符串放到相应位置,一直截取完成就转化为了ast了
// 模板解析正则
// 匹配标签名,aaa-123aaa
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
// 匹配命名空间标签 <my:xxx></my:xxx>,捕获的内容是标签名
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// 匹配标签开头
const startTagOpen = new RegExp(`^<${qnameCapture}`);
// 匹配标签结尾的 </div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
// 匹配属性,三种写法:aaa="aaaa" | aaa = 'aaaa' | aaa = aaa
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 匹配标签结束的 >
const startTagClose = /^\s*(\/?)>/;
// 匹配双大括号,{{ xxx }}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
// 解析函数
export function parseHTML(html) {
function start(tagName, attrs) {
console.log(tagName, attrs, '------开始---');
}
function end(tagName) {
console.log(tagName, '------结束---');
}
function chars(text) {
console.log(text, '-------文本---');
}
// 循环解析:只要html不为空字符串就一直解析
while (html) {
// 匹配开始|结束标签,尖括号开头
let textEnd = html.indexOf('<');
if (textEnd == 0) {
// 1、处理开始标签:开始标签匹配结果,获得标签名称和属性
const startTagMatch = parseStartTag();
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
// 2、处理结束标签:匹配结束标签
const endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1]);
continue;
}
}
// 3、处理文本
let text;
if (textEnd > 0) {
// 截取文本
text = html.substring(0, textEnd);
}
if (text) {
// 处理文本
advance(text.length);
chars(text);
}
}
// 字符串进行截取操作,再更新html内容
function advance(n) {
html = html.substring(n);
}
// 处理开始标签函数
function parseStartTag() {
const start = html.match(startTagOpen);
if (start) {
// ["<div", "div", index: 0, input: "<div id="app">...</div>", groups: undefined]
const match = {
tagName: start[1], // 标签名
attrs: [] // 属性
};
// 删除开始标签
advance(start[0].length);
let end;
let attr;
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
// 不是结束标签,并且有属性,就进行属性取值
// [" id="app"", "id", "=", "app", undefined, undefined, index: 0, ...]
match.attrs.push({
name: attr[1], // 属性名称
value: attr[3] || attr[4] || attr[5] // 属性值
});
// 删除属性
advance(attr[0].length);
}
// > 没有属性就表示为 结束的闭合标签
if (end) {
// 删除结束标签
advance(end[0].length);
return match;
}
}
}
return root;
}
2、生成ast语法树
// ast语法树模板
// <div>hello {{name}} <span>world</span></div>
{
tag: 'div',
parent: null,
type: 1,
attrs: [],
children: [
{
tag: null,
parent: '父div对象',
attrs: [],
text: hello {{name}}
}
]
}
// src\compiler\parse.js
// 开始标签依次存入stack中,在结束标签的时候取出建立父子关系
export function parseHTML(html) {
let root; // 根节点,也是树根
let currentParent; // 当前父元素
let stack = []; // 栈
const ELEMENT_TYPE = 1; // 元素类型
const TEXT_TYPE = 3; // 文本类型
// 创建ast对象
function createASTElement(tagName, attrs) {
return {
tag: tagName, // 标签名
type: ELEMENT_TYPE, // 元素类型
children: [], // 孩子列表
attrs, // 属性集合
parent: null // 父元素
}
}
// 标签是否符合预期
// <div><span></span></div>
// 处理开始标签
function start(tagName, attrs) {
// 创建一个元素,作为根元素
let element = createASTElement(tagName, attrs);
if (!root) {
root = element;
}
// 当前解析标签保存起来
currentParent = element;
// 将生产的ast元素放到栈中
stack.push(element);
}
// 在结尾标签处,创建父子关系
// <div><p><span></span></p></div> [div, p, span]
function end(tagName) {
let element = stack.pop(); // 取出栈中的最后一个
currentParent = stack[stack.length - 1]; // 倒数第二个是父亲
if (currentParent) {
// 闭合时可以知道这个标签的父亲是谁,儿子是谁
element.parent = currentParent;
currentParent.children.push(element);
}
}
// 处理文本
function chars(text) {
// 去除空格
text = text.replace(/\s/g, '');
if (text) {
currentParent.children.push({
type: TEXT_TYPE,
text
});
}
}
return root;
}
3、生成代码
template模板转化为render函数示例
// src\compiler\generate.js
// 编写:
<div id="app" style="color:red">hello {{name}} <span>hello</span></div>
// 结果:
render() {
return _c('div', {id: 'app', style: {color: 'red'}}, _v('hello'+_s(name)),_c('span',null,_v('hello')))
}
代码生成
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{ xxx }}
// 生成单个儿子节点
function gen(node) {
// 判断元素还是标签
if (node.type === 1) {
// 递归进行元素节点字符串的生成
return generate(node);
} else {
let text = node.text; // 获取文本
// 如果是普通文本,不带{{}}
if (!defaultTagRE.test(text)) {
//_v('hello {{name}} world {{msg}}') => _v('hello' + _s(name))
return `_v(${JSON.stringify(text)})`;
}
// 存放每一段代码
let tokens = [];
// 如果正则是全局模式,需要每次使用前置为0
let lastIndex = defaultTagRE.lastIndex = 0;
// 每次匹配到的结果
let match, index;
while (match = defaultTagRE.exec(text)) {
index = match.index; // 保存匹配到的索引
if (index > lastIndex) {
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(el) {
const children = el.children;
if (children) {
// 将所有转化后的儿子用都好拼接起来
return children.map(child => gen(child)).join(',');
}
return false;
}
// 生成属性
function genProps(attrs) {
let str = '';
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
if (attr.name === 'style') {
// 如果是样式需要特殊处理下
let obj = {};
attr.value.split(';').forEach(item => {
let [key, value] = item.split(':');
obj[key] = value;
})
attr.value = obj;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`;
}
return `{${str.slice(0, -1)}}`;
}
// 语法层面的转义:ast树转化为code字符串代码
export function generate(el) {
// 儿子的生成
let children = genChildren(el);
// 拼接代码:元素和儿子
let code = `_c('${el.tag}',${
el.attrs.length ? `${genProps(el.attrs)}` : undefined
}${
children ? `,${children}` : ''
})`;
return code;
}
let code = generate(ast);
4、生成render函数
// src\compiler\index.js
import { parseHTML } from "./parse";
import { generate } from "./generate";
export function compileToFunctions(template) {
// 1、模板转为ast
let ast = parseHTML(template);
// 2、ast转为code代码字符串
let code = generate(ast);
// 3、通过new Function + with的方式:将字符串变成函数
// 原理:通过with来限制取值范围,后续调用render函数改变this就可以取到结果了
let render = `with(this){return ${code}}`;
let renderFn = new Function(render);
return renderFn
}
四、初渲染原理
- 先初始化数据,进行观测
- 将模板进行编译为render函数
- 利用render.call(vm)进行改变this指向,最终产生虚拟DOM
- 调用_update()方法,执行patch函数将虚拟DOM转化为真实DOM
- 将真实DOM放到页面替换#app
1、在Vue的原型上混入_render和_update方法
// src\index.js
import { lifecycleMixin } from "./lifecycle";
import { renderMixin } from "./vdom/index";
// Vue构造函数
function Vue(options) {
// console.log(options);
// 入口方法,做初始化操作
this._init(options)
}
// 混合生命周期和渲染函数
lifecycleMixin(Vue);
// 混入_render方法
renderMixin(Vue);
2、生成虚拟DOM
- 在Vue的原型上定义_render方法
- 在Vue的原型上定义很多编译方法_c、_s、_v等
- 执行render.call(vm)的时候,传入vue实例,改变this指向
- 利用with的原理,最后将包括字符串的函数转化为虚拟dom
// src\vdom\index.js
export function renderMixin(Vue) {
// 创建虚拟dom-标签元素
Vue.prototype._c = function() {
return createElement(...arguments);
}
// 处理虚拟dom中双大括号{{}}。如果结果一个对象时,stringify会对这个对象取值
Vue.prototype._s = function(val) {
return val == null ? '' : (typeof val == 'object') ? JSON.stringify(val) : val;
}
// 创建虚拟dom-文本元素
Vue.prototype._v = function(text) {
return createTextVnode(text);
}
// 扩展_render方法
Vue.prototype._render = function() { //_render = render
const vm = this;
const render = vm.$options.render;
// 执行render方法,改变里面this的指向为vm,最后生成虚拟dom
let vnode = render.call(vm);
// 返回虚拟dom
return vnode;
}
}
// 生成元素节点的虚拟dom对象
function createElement(tag, data = {}, ...children) {
return vnode(tag, data, data.key, children);
}
// 生成文本节点的虚拟dom对象
function createTextVnode(text) {
return vnode(undefined, undefined, undefined, undefined, text);
}
// 用来产生虚拟dom的,可以自定义一些属性
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
}
}
3、调用渲染函数
// src\init.js
// 挂载函数
Vue.prototype.$mount = function(el) {
const vm = this;
const options = vm.$options;
el = document.querySelector(el);
......
// 调用渲染函数
mountComponent(vm, el);
}
// src\lifecycle.js
import { patch } from "./vdom/patch";
// 定义_update方法
export function lifecycleMixin(Vue) {
Vue.prototype._update = function(vnode) {
const vm = this;
// 将虚拟节点渲染成真实节点:用新创建的元素,替换老的vm.$el
vm.$el = patch(vm.$el, vnode);
}
}
// 定义渲染函数
export function mountComponent(vm, el) {
vm.$el = el;
// 先调用render方法创建虚拟节点,再将虚拟节点渲染到页面上
vm._update(vm._render());
}
4、生成真实DOM
// src\vdom\patch.js
// 将虚拟节点转化为真实节点
export function patch(oldVnode, vnode) {
// 产生真实的dom
let el = createElm(vnode);
// 获取老的app的父亲-body
let parentElm = oldVnode.parentNode;
// 当前真实元素插入到app的后面
parentElm.insertBefore(el, oldVnode.nextSibling);
// 删除老的节点
parentElm.removeChild(oldVnode);
// 返回新节点进行实时替换
return el;
}
function createElm(vnode) {
let { tag, children, key, data, text } = vnode;
if (typeof tag == 'string') {
// 创建元素,放到vnode.el上
vnode.el = document.createElement(tag);
// 只有元素才有属性
updateProperties(vnode);
// 遍历儿子,将儿子渲染后的结果放到父亲中
children.forEach(child => {
vnode.el.appendChild(createElm(child));
})
} else {
// 创建文本,放到vnode.el上
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
// 处理属性
function updateProperties(vnode) {
let el = vnode.el; // 当前的真实节点
let newProps = vnode.data || {}; // 获取当前节点的属性
for (let key in newProps) {
if (key == 'style') { // {color: red}
// 样式需要遍历添加
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName];
}
} else if (key == 'class') {
// class直接添加
el.className = el.class;
} else {
// 属性就需要利用方法添加,值就是对应的值
el.setAttribute(key, newProps[key]);
}
}
}
五、生命周期的合并
0、生命周期方法的使用
// 1、全局mixin混入使用
Vue.mixin({
created: function a() {
console.log('created 1');
}
})
// 2、在options中以属性方式使用
let vm = new Vue({
el: '#app',
created() {
// 生命周期就是回调函数,先订阅号,后续触发
console.log('created 3');
}
})
1、Mixin原理
// src\global-api\index.js
import { mergeOptions } from "../../util";
// 定义全局API
export function initGlobalApi(Vue) {
Vue.options = {};
// 定义混入的静态方法
Vue.mixin = function(mixin) {
// 合并对象-生命周期
this.options = mergeOptions(this.options, mixin);
}
}
2、合并生命周期
// 01vue\util.js
// 定义完整的生命周期数组
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed'
];
// 使用策略模式进行不同方法的调用
const strats = {};
strats.data = function(parentVal, childVal) {
return childVal;
}
strats.computed = function() {}
strats.watch = function() {}
// 生命周期的合并
function mergeHook(parentVal, childVal) {
if (childVal) {
// 如果儿子有值,然后根据父亲是否有值来处理
if (parentVal) {
// 爸爸和儿子进行拼接
return parentVal.concat(childVal);
} else {
// 儿子需要转化为数组
return [childVal];
}
} else {
// 如果只有父亲有值,不合并,直接采用父亲的
return parentVal;
}
}
// 定义生命周期的策略方法
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook;
})
// 对象合并方法
export function mergeOptions(parent, child) {
const options = {};
// 1、处理父亲有,儿子有或没有的情况
for (let key in parent) {
mergeField(key);
}
// 2、处理儿子有,父亲没有:把儿子多余的属性赋予到父亲上
for (let key in child) {
if (!parent.hasOwnProperty(key)) {
mergeField(key);
}
}
function mergeField(key) {
// 根据key,采取不同的策略合并
if (strats[key]) {
options[key] = strats[key](parent[key], child[key]);
} else {
// todo 默认合并
options[key] = child[key];
}
}
return options;
}
3、定义生命周期的调用函数
// src\lifecycle.js
// 定义生命周期调用方法
export function callHook(vm, hook) {
// 是数组
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm); // 更改生命周期中的this
}
}
}
4、在各个节点调用生命周期
// 调用beforeCreate和created
Vue.prototype._init = function(options) {
// 拿到当前实例
const vm = this
// 需要将用户自定义的options和全局的options做合并
vm.$options = mergeOptions(vm.constructor.options, options);
callHook(vm, 'beforeCreate');
initState(vm)
callHook(vm, 'created');
// 如果当前有el属性,需要进行模板渲染
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
// 调用beforeMount和mounted
export function mountComponent(vm, el) {
vm.$el = el;
callHook(vm, 'beforeMount');
// 先调用render方法创建虚拟节点,再将虚拟节点渲染到页面上
vm._update(vm._render());
callHook(vm, 'mounted');
}
六、依赖收集
- 每个属性都要有一个dep,用来收集watcher
- 每个dep中存放着多个watcher
- 同一个watcher会被多个dep所记录
- dep与watcher是多对多的关系
1、定义Dep类
// src\observer\dep.js
// 唯一标识
let id = 0;
class Dep {
constructor() {
this.subs = []; // 用于存储watcher
this.id = id++; // 标识dep的唯一性,用于防止重复取值添加dep
}
depend() {
// watcher也可以存放dep,实现双向记忆,让watcher记住dep的同时,让dep也记住watcher
Dep.target.addDep(this);
}
addSub(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
// 静态属性
Dep.target = null;
export function pushTarget(watcher) {
// 保留watcher
Dep.target = watcher;
}
export function popTarget() {
// 将变量删除掉
Dep.target = null;
}
export default Dep;
2、定义Watcher类
// src\observer\watcher.js
import { pushTarget, popTarget } from "./dep";
let id = 0;
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm;
this.exprOrFn = exprOrFn; // 渲染函数
this.cb = cb; // 回调函数
this.options = options;
this.id = id++; //watcher的唯一标识
this.deps = []; // watcher记录有多少dep依赖
this.depsId = new Set(); // 用于去重dep
if (typeof exprOrFn == 'function') {
this.getter = exprOrFn;
}
// 默认调用get方法
this.get();
}
addDep(dep) {
let id = dep.id;
// 去重
if (!this.depsId.has(id)) {
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this);
}
}
// 利用JS的单线程机制,先存储watcher,然后更新,最后删除watcher
get() {
// 存储当前watcher实例
pushTarget(this);
// 调用exprOrFn,渲染页面 > 取值(执行了get方法)
this.getter();
// 渲染完成后,将watcher删掉
popTarget();
}
run() {
// 渲染逻辑
this.get();
}
update() {
// 重新渲染
this.get();
}
}
export default Watcher;
3、对象依赖收集
// src\observer\index.js
// 每个属性都有一个dep
let dep = new Dep();
// 当页面取值时,说明这个值用来渲染了,将这个watcher和这个属性对应起来
Object.defineProperty(data, key, {
get() {
// 如果取值时有watcher
if (Dep.target) {
// 让watcher保存dep,并且让dep 保存watcher
dep.depend();
}
return value
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
// 通知渲染watcher去依赖更新
dep.notify();
}
})
4、数组依赖收集
- 获取arr的值,会调用get方法,就让当前数组记住渲染watcher
- 给所有的对象类型都增加一个dep属性
- 当页面对arr取值时,让这个数组的dep记住这个watcher
- 当操作push, shift等更新数组的方法时,就找到数组对应的watcher来更新
// src\observer\index.js
// 1、构造函数中添加一个dep
class Observer {
constructor(value) {
this.dep = new Dep(); // value = {} / []
......
}
}
// 2、定义响应式中进行添加依赖
function defineReactive(data, key, value) {
// 获取到数组对应的dep
let childDep = observe(value);
// 每个属性都有一个dep
let dep = new Dep();
// 当页面取值时,说明这个值用来渲染了,将这个watcher和这个属性对应起来
Object.defineProperty(data, key, {
get() {
// 如果取值时有watcher
if (Dep.target) {
// 让watcher保存dep,并且让dep 保存watcher
dep.depend();
// 可能是数组,可能是对象
if (childDep) {
// 默认给数组增加了一个dep属性,当对数组这个对象取值的时候
childDep.dep.depend(); // 数组存起来了渲染watcher
}
}
return value
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
// 通知渲染watcher去依赖更新
dep.notify();
}
})
}
// src\observer\array.js
// 3、重写方法中进行调用更新
arrayMethods[method] = function (...args) {
// ...
ob.dep.notify()
return result;
}
5、调用渲染函数进行依赖更新
渲染流程
- 先把这个渲染watcher放到Dep.target属性上
- 开始渲染,取值会调用get方法,需要让这个属性的dep 存储当前的watcher
- 页面上所属需要的属性都会将这个watcher存在自己的dep中
- 等会属性更新了,就重新调用渲染逻辑,通知自己存储的watcher来更新
// src\lifecycle.js
export function mountComponent(vm, el) {
vm.$el = el;
callHook(vm, 'beforeMount');
// 定义更新函数
let updateComponent = () => {
vm._update(vm._render());
};
// 初始化就会创建watcher
new Watcher(vm, updateComponent, () => {
callHook(vm, 'updated');
}, true);
callHook(vm, 'mounted');
}
七、异步更新与nextTick
0、index.html中调用
let vm = new Vue();
setTimeout(() => {
vm.arr.push(123);
vm.arr.push(123);
vm.arr.push(123);
console.log(vm.$el.innerHTML);
vm.$nextTick(() => {
console.log(vm.$el.innerHTML);
});
}, 2000);
1、Vue原型找那个扩展nextTick方法
// src\state.js
import { nextTick } from "./util"
export function stateMixin(Vue) {
Vue.prototype.$nextTick = function(cb) {
nextTick(cb);
}
}
2、实现队列机制
// src\observer\watcher.js
class Watcher {
constructor(vm, exprOrFn, cb, options) {
......
}
.....
update() {
// 这里不能每次都调用get方法,get方法会重新渲染页面,需要用队列实现多次调用只刷新一次
queueWatcher(this);
}
}
// 实现队列
// src\observer\watcher.js
import { nextTick } from "../util";
// 将需要批量更新的watcher存到一个队列中,稍后让watcher执行
let queue = [];
let has = {}; // 用于去重
let pending = false; // 防抖
function flushSchedulerQueue() {
queue.forEach(watcher => {
watcher.run();
watcher.cb();
});
queue = []; // 清空watcher队列为了下次使用
has = {}; // 清空标识的id
pending = false;
}
function queueWatcher(watcher) {
// 对watcher去重
const id = watcher.id;
if (has[id] == null) {
// 将watcher存到队列中
queue.push(watcher);
has[id] = true;
// 异步更新,等到所有同步代码执行完毕后再执行
if (!pending) {
// 内部调用
nextTick(flushSchedulerQueue);
pending = true;
}
}
// console.log(watcher.id);
}
export default Watcher;
3、nextTick原理
Promise > MutationObserver > setImmediate > setTimeout
// src\util.js
const callbacks = [];
let pending = false;
let timerFunc;
function flushCallbacks() {
// 让nextTick中传入的方法依次执行
while (callbacks.length) {
let cb = callbacks.pop();
cb();
}
// 标识已经执行完毕
pending = false;
}
if (Promise) {
timerFunc = () => {
// 异步处理更新
Promise.resolve().then(flushCallbacks);
}
} else if (MutationObserver) {
// 可以监控dom变化,监控完毕后是异步更新
let observe = new MutationObserver(flushCallbacks);
/**
* 思路:
* 1、创建一个文本节点
* 2、监控这个文本节点
* 3、当文本节点里面的字符变化,就异步调用flushCallbacks进行更新操作
*/
// 先创建一个文本节点
let textNode = document.createTextNode(1);
// 观测文本节点中的内容
observe.observe(textNode, { characterData: true });
timerFunc = () => {
// 文本内容更新为2,触发异步调用flushCallbacks
textNode.textContent = 2;
}
} else if (setImmediate) {
// ie浏览器里面的api,性能比setTimeout要好些
timerFunc = () => {
setImmediate(flushCallbacks);
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks);
}
}
// 内部会调用,用户也会调用,但是异步只需要一次
export function nextTick(cb) {
callbacks.push(cb);
// Vue3里面的nextTick原理就是Promise.resolve().then() 没有兼容性处理
if (!pending) {
timerFunc();
pending = true;
}
}
八、$watch的原理
0、使用方式
// 第一种
let vm = new Vue();
vm.$watch(function (newVal, oldVal) {
console.log(newVal, oldVal);
});
vm.$watch('a.b.c', function (newVal, oldVal) {
console.log(newVal, oldVal);
})
// 第二种
let vm = new Vue({
el: '#app',
data: {
a: {a: {a: 1}},
b: 2
},
methods: {
cc() {
console.log('method cc');
}
},
watch: {
// 1. key value
'a.a.a': {
handler(newValue, oldValue) {
// 对象没有老值,都是新
console.log(newValue, oldValue);
},
immediate: true // 可选
},
// 2. 写成key和数组的方式
'b': [
(newValue, oldValue) => {
console.log(newValue);
},
(newValue, oldValue) => {
console.log(newValue);
}
],
// 3、监控当前实例上的方法
'c': 'cc',
// 4、handler的写法
'd': {
handler() {
console.log('ddd');
}
}
}
});
1、扩展$watch方法
// src\state.js
export function initState(vm) {
const opts = vm.$options
// ......
if (opts.watch) {
initWatch(vm)
}
}
function initProps(vm) {}
function initMethods(vm) {}
// 初始化数据
function initData(vm) {}
function initComputed(vm) {}
// 初始化watch
function initWatch(vm) {
let watch = vm.$options.watch;
for (let key in watch) {
// handler 可能是数组、字符串、对象、函数
const handler = watch[key];
if (Array.isArray(handler)) {
// 数组
handler.forEach(handle => {
createWatcher(vm, key, handle);
})
} else {
// 字符串、对象、函数
createWatcher(vm, key, handler);
}
}
}
// 创建watcher:options 用来标识是用户watcher
function createWatcher(vm, exprOrFn, handler, options) {
if (typeof handler == 'object') {
options = handler;
handler = handler.handler; // 是一个函数
}
if (typeof handler == 'string') {
handler = vm[handler]; // 将实例的方法作为handler
}
// key handler 用户传入的选项
return vm.$watch(exprOrFn, handler, options);
}
export function stateMixin(Vue) {
Vue.prototype.$nextTick = function(cb) {
nextTick(cb);
}
// 在Vue的原型上面挂载$watch方法
Vue.prototype.$watch = function(exprOrFn, cb, options = {}) {
// 数据应该依赖这个watcher,数据变化后应该让watcher从新执行,user表示为自定义的watcher
let watcher = new Watcher(this, exprOrFn, cb, {...options, user: true });
if (options.immediate) {
// 如果是immediate,立刻执行更新
cb();
}
}
}
2、watcher类的getter方法改写
// src\observer\watcher.js
class Watcher {
constructor(vm, exprOrFn, cb, options) {
// .....
this.user = options.user; // 用户watcher
this.isWatcher = typeof options === 'boolean'; // 标识是渲染watcher
if (typeof exprOrFn == 'function') {
this.getter = exprOrFn;
} else {
// exprOrFn 可能传递过来的是一个字符串a
this.getter = function() {
// 当去当前实例上取值时,才会触发依赖收集
let path = exprOrFn.split('.'); // ['a', 'a', 'a']
let obj = vm;
for (let i = 0; i < path.length; i++) {
obj = obj[path[i]]; // vm.a.a.a
}
return obj;
}
}
// 默认会先调用一次get方法,进行取值,将结果保留下来
this.value = this.get();
}
addDep(dep) {}
get() {
// 当前watcher实例
pushTarget(this);
// 调用exprOrFn,渲染页面 > 取值(执行了get方法)
let result = this.getter();
// 渲染完成后,将watcher删掉
popTarget();
return result;
}
run() {
// 渲染逻辑
let newValue = this.get();
let oldValue = this.value;
this.value = newValue; // 更新老值
if (this.user) {
// 调用cb方法
this.cb.call(this.vm, newValue, oldValue);
}
}
update() {}
}
export default Watcher;
九、computed原理
0、使用方式
let vm = new Vue({
el: '#app',
data: {
firstName: '张',
lastName: '三'
},
computed: {
// 内部使用了defineProperty,内部有一个变量dirty
// 第一种: 函数方式-常用的方式
fullName() {
// this.firstName和this.lastName在求值时,会记住当前计算属性的watcher
return this.firstName + this.lastName
},
// 第二种:对象方式-几乎不用
fullName2: {
get() {},
set() {}
}
}
});
1、初始化方法
// src\state.js
// 实现三大步骤
// 1. 需要watcher
// 2. 需要defineProperty
// 3. 需要dirty
// 初始化计算属性
function initComputed(vm) {
// 获取计算属性对象
let computed = vm.$options.computed;
// 用来存放计算属性的watcher
const watchers = vm._computedWatchers = {};
for (let key in computed) {
// 取出对应的值
const userDef = computed[key];
// 判断是函数还是对象,如果是对象就取get方法,最终得到一个getter函数
const getter = typeof userDef == 'function' ? userDef : userDef.get;
// 给每一个计算属性的对象key加一个watcher进行监听
watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true });
// 给每一个计算属性的对象key加一个数据观测
defineComputed(vm, key, userDef);
}
}
function defineComputed(target, key, userDef) {
const sharePropertyDefinition = {};
if (typeof userDef == 'function') {
sharePropertyDefinition.get = createComputedGetter(key, userDef);
} else {
sharePropertyDefinition.get = createComputedGetter(key, userDef.get);
sharePropertyDefinition.set = userDef.set;
}
Object.defineProperty(target, key, sharePropertyDefinition);
}
// 高阶函数做缓存
function createComputedGetter(key) {
// 包装的计算属性的方法,每次取值会调用此方法
return function() {
// 拿到属性对应的watcher
const watcher = this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
// 默认是脏的
watcher.evaluate();
}
// 还有渲染watcher,也需要收集起来
if (Dep.target) {
watcher.depend();
}
// 默认返回watcher上的值
return watcher.value;
}
}
}
2、watcher中添加方法
// src\observer\watcher.js
class Watcher {
constructor(vm, exprOrFn, cb, options) {
// ...
this.lazy = options.lazy; // 如果watcher上有lazy属性,说明是一个计算属性
this.dirty = this.lazy; // dirty表示取值时是否执行用户提供的方法
// ...
// 默认会先调用一次get方法,进行取值,将结果保留下来
this.value = this.lazy ? void 0 : this.get();
}
addDep(dep) { }
get() { }
run() { }
update() {
if (this.lazy) {
// 计算属性,页面重新渲染获取最新的值
this.dirty = true;
} else {
// 这里不能每次都调用get方法,get方法会重新渲染页面
queueWatcher(this);
}
}
evaluate() {
this.value = this.get();
// 取过一次值之后,就标识为已经取过值了
this.dirty = false;
}
depend() {
// 计算属性watcher会存储dep
// 通过watcher找到对应的所有dep,也让所有的dep记住这个watcher
let i = this.deps.length;
while (i--) {
// 让dep去存储渲染watcher
this.deps[i].depend();
}
}
}
3、dep中添加收集
// src\observer\dep.js
// 静态属性
Dep.target = null;
let stack = [];
export function pushTarget(watcher) {
// 保留watcher
Dep.target = watcher;
stack.push(watcher); // 渲染watcher、其他watcher
}
export function popTarget() {
// 将变量删除掉
// Dep.target = null;
stack.pop();
Dep.target = stack[stack.length - 1];
}
十、Diff算法
0、构造数据
import { compileToFunction } from './compiler/index';
import { createElm, patch } from './vdom/patch';
// 1. 创建第一个虚拟节点
let vm1 = new Vue({ data: { name: 'test1' } });
let render1 = compileToFunction(
`<div>
<li style="background:red;" key="A">A</li>
<li style="background:yellow;" key="B">B</li>
<li style="background:pink;" key="C">C</li>
<li style="background:green;" key="D">D</li>
<li style="background:green;" key="F">F</li>
</div>`
);
let oldVnode = render1.call(vm1);
// 2. 创建第二个虚拟节点
let vm2 = new Vue({ data: { name: 'test2' } });
let render2 = compileToFunction(
`<div>
<li style="background:green;" key="M">M</li>
<li style="background:pink;" key="B">B</li>
<li style="background:yellow;" key="A">A</li>
<li style="background:purple;" key="Q">Q</li>
</div>`
);
let newVnode = render2.call(vm2);
// 3. 通过第一个虚拟节点做首次渲染
let el = createElm(oldVnode)
document.body.appendChild(el);
// 4. 调用patch方法进行对比操作
// 传入新的、老的虚拟节点, 然后用新的虚拟节点对比老的虚拟节点,找到差异,去更新老的dom元素
setTimeout(() => {
patch(oldVnode, newVnode);
}, 3000);
1、具体思路
- 通过同层的树节点进⾏比较,⽽非对树进行逐层搜索遍历的⽅式,所以时间复杂度只有O(n),是⼀种相当高效的算法
- 同层三件事:增删改,具体规则是:
- 新节点(new Vnode)不存在就删
- 老节点(old Vnode)不存在就增
- 都存在就比较类型,类型不同直接替换、类型相同执行更新
2、基本比对
(1) 比对标签
// 1. 比较两个元素的标签,标签不一样直接替换掉
if (oldVnode.tag !== vnode.tag) {
// 新的标签替换标签
return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
}
// 2. 如果标签一样,而里面的文本内容不一致,例如:<div>1</div> <div>2</div>
if (!oldVnode.tag) {
// 因为文本节点的虚拟节点tag 都是undefined,所以需要!oldVnode.tag来判断
// 两个文本不一致,直接用新的替换老的
if (oldVnode.text !== vnode.text) {
return oldVnode.el.textContent = vnode.text;
}
}
(2) 比对属性
// 3. 标签一样,并且需要开始比对标签的属性和儿子
// 第一步:直接复用节点
let el = vnode.el = oldVnode.el;
// 第二步:新老属性做对比,然后更新属性,用新的虚拟节点的属性和老的比较,去更新节点
updateProperties(vnode, oldVnode.data);
(3) 比对子元素
// 4. 比较儿子的几种情况
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
// (3) 老的有儿子,新的也有儿子,diff算法
updateChildren(oldChildren, newChildren, el);
} else if (oldChildren.length > 0) {
// (1) 老的有儿子,新的没有儿子
el.innerHTML = '';
} else if (newChildren.length > 0) {
// (2) 老的没有儿子。新的有儿子
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i];
el.appendChild(createElm(child));
}
}
3、优化策略
(1) diff算法
- 采用双指针操作:一个循环,同时循环老的和新的,哪个先结束,循环就停止,将多余的删除或者添加进去
- 比对策略:
- 老的头部和新的头部比对
- 老的尾部和新的尾部比对
- 老的头部和新的尾部比对
- 老的尾部和新的头部比对
- 儿子直接没关系进行暴力比对
(2) 代码实现
// src\vdom\patch.js
// 判断是否为相同的虚拟节点
function isSameVnode(oldVnode, newVnode) {
return oldVnode.tag == newVnode.tag && oldVnode.key == newVnode.key;
}
// 比对儿子
function updateChildren(oldChildren, newChildren, parent) {
// vue2 采用双指针操作
// 一个循环,同时循环老的和新的,那个新结束,循环就停止,将多余的删除或者添加进去
// 老节点
let oldStartIndex = 0; // 开始索引
let oldStartVnode = oldChildren[0]; // 开始节点
let oldEndIndex = oldChildren.length - 1; // 结束索引
let oldEndVnode = oldChildren[oldEndIndex]; // 结束节点
// 新节点
let newStartIndex = 0; // 开始索引
let newStartVnode = newChildren[0]; // 开始节点
let newEndIndex = newChildren.length - 1; // 结束索引
let newEndVnode = newChildren[newEndIndex]; // 结束节点
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
if (item.key) {
map[item.key] = index; // {A:0, B:1, C:2, D:3}
}
});
return map;
}
let map = makeIndexByKey(oldChildren);
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (!oldStartVnode) {
// 如果指针指向null,需要跳过这些节点
// 从左往右遍历的情况
oldStartVnode = oldChildren[++oldStartIndex];
} else if (!oldEndVnode) {
// 如果指针指向null,需要跳过这些节点
// 从右往左遍历的情况
oldEndVnode = oldChildren[--oldEndIndex];
} else if (isSameVnode(oldStartVnode, newStartVnode)) {
// 1. 老的头部和新的头部比较
// 从左往右开始比对,如果两个是同一个元素,比对儿子,更新属性和再去更新子节点
patch(oldStartVnode, newStartVnode);
// 向后移动指针
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
// 2. 老的尾部和新的尾部比较
// 从右往左开始比对,如果两个是同一个元素,比对儿子,更新属性和再去更新子节点
patch(oldEndVnode, newEndVnode);
// 向前移动指针
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
} else if (isSameVnode(oldStartVnode, newEndVnode)) {
// 3. 老的头部和新的尾部比较
patch(oldStartVnode, newEndVnode);
// 将老的当前元素插入到尾部的下一个元素的前面
parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
// 移动指针:老的开始指针从左往右移动(向后移动),新的尾部指针从右往左移动(往前移动)
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex];
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
// 4. 老的尾部和新的头部比较
patch(oldEndVnode, newStartVnode);
// 将当前元素插入到尾部的下一个元素的前面
parent.insertBefore(oldEndVnode.el, oldStartVnode.el);
// 移动指针:老的尾部指针从右往左移动(往前移动),新的开始指针从左往右移动(向后移动)
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex];
} else {
// 5. 儿子之前没关系:暴力比对
// 拿到新的开头的虚拟节点的key,去老的中找
let moveIndex = map[newStartVnode.key];
if (moveIndex == undefined) {
// 没有复用的key,不需要移动老节点,只需要将新的节点插入到老节点的开头就行了
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else {
// 有复用的key,需要先移动老节点,然后再将老节点原来的索引位置置为null,方便最后删除
let moveVnode = oldChildren[moveIndex];
oldChildren[moveIndex] = null;
parent.insertBefore(moveVnode.el, oldStartVnode.el);
// 可能老节点和新节点的属性和儿子不一样,需要比较属性和儿子
patch(moveVnode, newStartVnode);
}
// 往后移动指针,用新的不停地去老的里面找
newStartVnode = newChildren[++newStartIndex];
}
// 反转节点,头部移动到尾部,尾部移动到头部
// 为什么要有key,不能用index作为key?
// key没变,元素复用,但是内容发生了变化
}
// 新节点中多余的元素添加到父亲中,从新节点的结束指针开始到末尾
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
// 将多余的元素插入进去,可能是向前添加,也可能向后添加
// parent.appendChild(createElm(newChildren[i]));
// 向后插入:ele = null
// 向前插入:ele就是当前向谁前面插入
let ele = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].el;
parent.insertBefore(createElm(newChildren[i]), ele);
}
}
// 老的节点还没有处理的,说明这些老节点是不需要的节点
// 如果这里面有null,说明这个节点已经被处理过了,就跳过
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
let child = oldChildren[i];
if (child != undefined) {
parent.removeChild(child.el);
}
}
}
}
// src\vdom\patch.js
// 创建节点
export function createElm(vnode) {
let { tag, children, key, data, text } = vnode;
if (typeof tag == 'string') {
// 创建元素,放到vnode.el上
vnode.el = document.createElement(tag);
// 只有元素才有属性
updateProperties(vnode);
// 遍历儿子,将儿子渲染后的结果放到父亲中
children.forEach(child => {
vnode.el.appendChild(createElm(child));
})
} else {
// 创建文本,放到vnode.el上
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
// 更新属性的方法
function updateProperties(vnode, oldProps = {}) {
// 当前的真实节点
let el = vnode.el;
// 获取当前节点的属性(新的属性)
let newProps = vnode.data || {};
// 老的有,新的没有,需要删除属性
for (let key in oldProps) {
if (!newProps[key]) {
// 移除真实dom的属性
el.removeAttribute(key);
}
}
// 样式处理,老的 style={color:red} 新的 style={background:Red}
let newStyle = newProps.style || {};
let oldStyle = oldProps.style || {};
// 老的样式中有,新的没有,删除老的样式
for (let key in oldStyle) {
if (!newStyle[key]) {
el.style[key] = '';
}
}
// 新的有,直接用新的去更新
for (let key in newProps) {
if (key == 'style') { // {color: red}
// 样式需要遍历添加
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName];
}
} else if (key == 'class') {
// class直接添加
el.className = newProps[key];
} else {
// 属性就需要利用方法添加,值就是对应的值
el.setAttribute(key, newProps[key]);
}
}
}
4、更新操作
// src\lifecycle.js
export function lifecycleMixin(Vue) {
Vue.prototype._update = function(vnode) {
const vm = this;
// 需要区分首次渲染还是更新
const prevVnode = vm._vnode;
if (!prevVnode) {
// 用新创建的元素,替换老的vm.$el
vm.$el = patch(vm.$el, vnode);
} else {
// 上一次的vnode和本次做对比
vm.$el = patch(prevVnode, vnode);
}
// 保存上一次的vnode
vm._vnode = vnode;
}
}
十 组件原理
0、使用
- 全局组件
Vue.component('my-button', {
template:'<button>+</button>'
});
- 局部组件
let vm = new Vue({
el: '#app',
components:{
aa:{
template:'<div>hello </div>'
}
},
data: { },
});
1、组件的作用
- 实现复用
- 方便维护
- 合理拆分组件可以提高性能:因为每个组件都有一个Watcher,当组件更新的时候,越小的组件,vdom越小,就能减少比对,提高性能
2、组件初始化
- 通过Vue.component注册全局组件,之后可以在模板中进行使用
- Vue.component内部会调用Vue.extend方法,将定义挂载到Vue.options.components上
- Vue.extend方法就是创建出一个子类,继承于Vue,并返回这个类
// src\global-api\index.js
export function initGlobalApi(Vue) {
Vue.options = {};
Vue.mixin = function(mixin) {
// 合并对象-生命周期
this.options = mergeOptions(this.options, mixin);
}
// 核心就是创造一个子类继承我们的父类
let cid = 0;
Vue.extend = function(extendOptions) {
const Super = this; // this > vue的构造函数Vue
// 定义子类的构造函数
const Sub = function VueComponent(options) {
// 子类实例的初始化方法
this._init(options);
}
// 唯一标识
Sub.cid = cid++;
// 子类要继承父类原型上的方法, 原型继承
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
// 合并其他属性
Sub.options = mergeOptions(Super.options, extendOptions);
Sub.components = Super.components;
// 返回子类
return Sub;
}
// 全局组件方法
Vue.options._base = Vue; // 保留Vue的构造函数
Vue.options.components = {};
Vue.component = function(id, definition) {
// 默认以name属性为准
definition.name = definition.name || id;
// 根据当前组件对象 生成了一个子类的构造函数,用于指向父类,用的时候得new definition().$mount()
definition = this.options._base.extend(definition);
// 缓存到options中
Vue.options.components[id] = definition;
}
}
- 因为在初始化_init方法中会合并属性,需要加一个合并策略
// src\util.js
// 组件的合并策略-就近策略:当同时存在全局组件和局部组件的时候,以局部组件为主,没有再用全局组件
strats.components = function(parentVal, childVal) {
// 将全局组件放到原型链上,沿着原型链进行查找
const res = Object.create(parentVal);
if (childVal) {
for(let key in childVal){
res[key] = childVal[key];
}
}
return res;
}
2、组件转虚拟dom
- vdom中的_c方法中调用createElement创建元素方法中进去区分组件还是原生标签
- 给组件的vdom标记属性,存放构造函数和插槽
// src\vdom\index.js
// 生成元素节点的虚拟dom对象
function createElement(vm, tag, data = {}, ...children) {
// 判断是否为原生标签
if (isReservedTag(tag)) {
// 原生标签直接创建虚拟节点
return vnode(tag, data, data.key, children);
} else {
// 如果是组件,在产生虚拟节点时需要把组件的构造函数传入 new Ctor().$mount()
let Ctor = vm.$options.components[tag];
return createComponent(vm, tag, data, data.key, children, Ctor);
}
}
// 生成组件
function createComponent(vm, tag, data, key, children, Ctor) {
const baseCtor = vm.$options._base;
// 如果组件是一个对象,需要通过Vue.extend来创建一个子组件构造函数
if (typeof Ctor === 'object') {
Ctor = baseCtor.extend(Ctor);
}
// 给子组件增加生命周期
data.hook = {
// 组件初始化会调用init方法,然后挂载
init(vnode) {
let child = vnode.componentInstance = new Ctor({});
child.$mount();
}
}
return vnode(`vue-component-${Ctor.cid}-${tag}`, data, key, undefined, undefined,
{ Ctor, children });
}
// 用来产生虚拟dom的,可以自定义一些属性
function vnode(tag, data, key, children, text, componentOptions) {
return {
tag,
data,
key,
children,
text,
// 组件的虚拟节点多一个属性,用来保存当前组件的构造函数和他的插槽
componentOptions
}
}
3、组件转真实dom
export function patch(oldVnode, vnode) {
// 组件初始化的时候 oldvnode为undefined
if(!oldVnode){ // 如果是组件这个oldVnode是个undefined
return createElm(vnode); // vnode是组件中的内容
}
// ...
}
function createComponent(vnode) {
// 调用hook中init方法
let i = vnode.data;
// 拿到hook中的init方法,然后调用,内部会new 子组件,然后挂载到vnode上
if ((i = i.hook) && (i = i.init)) {
i(vnode);
}
if (vnode.componentInstance) {
return true;
}
}
export function createElm(vnode) {
let { tag, children, key, data, text } = vnode;
if (typeof tag == 'string') {
// 如果是组件,组件渲染后的结果 放到当前组件的实例上 vm.$el
if (createComponent(vnode)) {
// 返回组件对应的dom元素
return vnode.componentInstance.$el;
}
// 创建元素,放到vnode.el上
vnode.el = document.createElement(tag);
// 只有元素才有属性
updateProperties(vnode);
// 遍历儿子,将儿子渲染后的结果放到父亲中
children.forEach(child => {
vnode.el.appendChild(createElm(child));
})
} else {
// 创建文本,放到vnode.el上
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
4、组件的渲染流程
- 调用Vue.component,注册全局组件
- 内部用的是Vue.extend 就是产生一个子类来继承父类
- 等会创建子类实例时会调用父类的_init方法,再$mount
- 组件的初始化就是 new 这个组件的构造函数并且调用$mount方法
- 创建虚拟节点 根据标签筛选出对应的组件,然后生成组件的虚拟节点 componentOptions里面包含Ctor,children
- 组件创建真实dom时 (先渲染的是父组件) 遇到是组件的虚拟节点时,去调用init方法,让组件初始化并挂载, 组件的$mount无参数会把渲染后的dom放到 vm.$el上 =》 vnode.componentInstance中,这样渲染时就 获取这个对象的$el属性来渲染