前端提升5:手撸vue2响应式+渲染
2025-03-29 16:26:46阅读vue(2.6)的源码,data响应式大概有如下4个过程:
- 数据代理:core/instance/state.js--->vue函数--->initMixin--->initState--->数据遍历--->observer/index.js--->Observer---> defineReactive--->defineProperty
- 数据装载:$mount--->装载template--->compileToFunctions--->mountComponet--->创建【渲染Watcher】--->Watcher.get()--->Watcher.get()依赖收集--->Dep.target就是当前的Watcher(data中的属性—dep<----->watcher 形成双向引用)
- 数据相应式:需要修改data属性--->触发setter回调执行--->通知dep上的watcher更新dep.notify()--->遍历所有的watcher并调用watcher的update方法--->调用run方法--->调用get()方法实现页面渲染
- 数据渲染:
vm.__patch__
--->createPatchFunction--->patchVnode--->双方都有孩子节点updateChildrendiff代理
以下我们参考vue的源码,记录实现data响应式的全过程
一、数据劫持
收先我们混入一个方法,给Vue对象添加一个内部_init方法,我们在这个内部方法中去实现需要的功能
//index.js import { initMixin } from "./init"; function Vue(options){ // vue2 配置项 console.log("my vue 6666") this._init(options); // vue 内部约定 私有的方法或属性 一般是以 $ _ 开头 } initMixin(Vue); // 一执行,Vue就有_init方法 export default Vue //init.js 初始化 import { initState } from "./state"; // 混入 类似于 对象语言的继承 // 在不改变这个函数的前提下,给这个函数增加方法 增强功能 // 原理是在prototype上添加方法 export function initMixin(Vue) { Vue.prototype._init = function (options) { const vm = this; vm.$options = options; // 将配置项暂存到vm实例上 // 初始化 数据 initState(vm); } } //state.js 初始化数据 import { observe } from "./observer/index"; export function initState(vm) { const opts = vm.$options; if (opts.props) { // 处理父传子的属性 } if (opts.methods) { // 处理方法 } if (opts.data) { initData(vm); } } // 初始化 数据 function initData(vm) { // 拿到数据 let data = vm.$options.data; // 数据可能是对象 也可能是函数 data = vm._data = typeof data === 'function' ? data.call(vm) : data; // 监听处理 observe(data); }
进行监听处理
// 过程代码,之后还会改造 class Observer { constructor(data) { this.walk(data); } // 遍历属性 walk(data) { Object.keys(data).forEach(key => { // 需要对每个属性做响应式处理 console.log(key); defineReactive(data, key, data[key]); }) } } // 数据响应式处理 function defineReactive(data, key, value) { // 如果传入的是一个对象,则递归监听 observe(value); Object.defineProperty(data, key, { get() { console.log("触发get", key); return value; }, set(newValue) { console.log("触发set", key); // 如果值没有变化,则忽略 if (value === newValue) return; // 设置属性可能也是一个对象,需要监听处理 observe(newValue) value = newValue; } }) } // 监听函数可以监听 只能监听对象 export function observe(data) { return new Observer(data); }
监听数组
由于我们不能修改数组的原生方法,为了实现原生方法的监听,我们可以为数组的方法创建同名的新方法,在同名方法里面实现我们的监听逻辑
//observer/array.js // 数组监听 // 数组的旧原型对象 let oldArrayMethods = Array.prototype; // 基于array原型创建一个新的对象,可以找到数组原型上的方法,而且修改对象不影响原生数组方法 export let arrayMethods = Object.create(oldArrayMethods); // 在新的对象上实现自己的数组方法 let methods = [ "push", // 插入到最后 "pop", "shift", "unshift", // 插入到最前 "sort", "reverse", "splice" // 在某个位置上 先删除 后添加 ] methods.forEach(method => { // 方法的劫持 arrayMethods[method] = function(...args) { // 执行原生方法 let result = oldArrayMethods[method].apply(this, args); // 拿到Observer const ob = this.__ob__; // push unshift splice 都可以新增, 可能是对象, 如果是对象就要监听处理 let inserted; switch(method) { case 'push': case 'unshift': inserted = args; break; case 'splice': // 如何取到要添加数据? 从第2个参数开始截取 inserted = args.slice(2); break; default: break; } // 新添加的数据可能是对象, 需要做响应式处理 inserted && ob.observeArray(inserted); return result; } })
避免重复监听
目前我们已经完成了data的监听了,但是还有一个问题,已经监听过的对象,如何避免再次被监听呢?
解决办法: 在监听时,给当前对象添加一个标识属性 ob 引用的是Observe实例对象
我们修改Observer类,如下是最终的实现
//observer/index.js // 数据监听处理 import {isObject} from "../utils.js" import { arrayMethods } from "./array.js"; class Observer { constructor(data) { // 给每个对象添加__ob__ 引用当前的Observer实例 Object.defineProperty(data, "__ob__", { enumerable: false, configurable: false, value: this }) if (Array.isArray(data)) { // data设置新的代理对象 data.__proto__ = arrayMethods; this.observeArray(data); } else { this.walk(data); } } // 遍历属性 walk(data) { Object.keys(data).forEach(key => { // 需要对每个属性做响应式处理 console.log(key); defineReactive(data, key, data[key]); }) } // 监听数组中的每一项 observeArray(data) { for (let i=0; i<data.length; i++) { observe(data[i]); } } } // 数据响应式处理 function defineReactive(data, key, value) { // 如果传入的是一个对象,则递归监听 observe(value); Object.defineProperty(data, key, { get() { console.log("触发get", key); return value; }, set(newValue) { console.log("触发set", key); // 如果值没有变化,则忽略 if (value === newValue) return; // 设置属性可能也是一个对象,需要监听处理 observe(newValue) value = newValue; } }) } // 监听函数可以监听 只能监听对象 export function observe(data) { // 如果不对象 则返回 if (!isObject(data)) { return; } // 避免二次监听 if (data.__ob__ instanceof Observer) { return; } // 对对象属性进行监听处理(defineProperty) return new Observer(data); } //utils.js // 工具类 /** * 判断data是否是一个对象 * @param {} data * @returns */ export function isObject(data) { return typeof data === 'object' && data !== null; }
通过以上的过程,我们已经可以实现data的数据监听了。
但我们的任务还没有完,当数据发生变化时,如何在页面上进行渲染呢?
在vue中,需要实现响应式的数据格式如下,接下来我们实现把数据装载到vue对象中
<div><p>{{user.name}}</p></div>
二、模版渲染
1. 模版编译原理
首选创建一个方法来完成模版编译
//compiler/index.js
// 模板编译
import { generator } from "./generator";
import { parseHTML } from "./parser";
// 生成虚拟dom
export function compileToFunction(template) {
// 实现模板编译
// 1. 先把html转化为ast语法树
// 2. 标识静态树, 树的遍历
// 3. 通过ast产生的语法树, 生成渲染后的代码
let ast = parseHTML(template);
console.log("ast", ast)
}
//compiler/parser.js
// 标签名正则
// * 匹配前面的子表达式0次或多次
// 分2部分:前面可以为字母或下划线, 后面可以为字母,数字,下划线等
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
// ?: 匹配不捕获 div im:div
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// 开始标签正则 以<开头标签 用来匹配开始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`);
// 结束标签正则 [^>]不能有>标签, 如 </div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
// 用来匹配属性标签
// \s* 匹配任何空白字符 + 匹配前面的子表达式一次或多次
// 第一个分组是限制一些非法字符
// 第二个分组是捕获等号2边内容,等号右边内容分3种情况
// 1. 捕获用双引号包括内容, 里面不能再有双引号
// 2. 捕获用单引号包括内容
// 3. 捕获任何内容,但排除特定符号
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 标签关闭正则 <div> <div />
const startTagClose = /^\s*(\/?)>/;
// 模板内容 {{msg}}
// win \r\n linux \n mac \r
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
// 切香肠
export function parseHTML(html) {
// ast语法树
let root; // 根
let currentParent; // 父节点
let stack = []; // 用来判断标签是否正常闭合 <div> </div>
function createASTElement(tagName, attrs) {
return {
tag: tagName,
attrs,
children: [],
parent: null,
type: 1 // 1--普通元素 3-文本 <div> sss </div>
}
}
// 开始标签处理
function start(tagName, attrs) {
let element = createASTElement(tagName, attrs);
if (!root) {
root = element; // 根节点
}
currentParent = element; // 指定父节点
stack.push(element); // 维护父子关系
}
function end(tagName) {
let element = stack.pop(); // 取当前的元素
currentParent = stack[stack.length - 1]; // 取当前元素上一级元素
if (currentParent) {
element.parent = currentParent;
currentParent.children.push(element);
}
}
// 遍历html
while(html) {
let textEnd = html.indexOf("<");
if (textEnd === 0) {
// 正常处理 开始捕获开始标签
const startTagMatch = parseStartTag();
console.log("开始标签:", startTagMatch)
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs);
}
// 结束标签
const endTagMatch = html.match(endTag)
if (endTagMatch) {
// 结束标签 不用处理,直接删除
advance(endTagMatch[0].length);
end(endTagMatch[1]);
}
}
// 如果不是0, 说明是文本或换行符,需要清除
let text;
if (textEnd > 0) {
text = html.substring(0, textEnd); // 将标签前面的内容截取
// 过滤空格换行符,将文本保存下来
chars(text);
}
// 如果没有文本 只有换行符 空格
if (text) {
advance(text.length);
}
}
// 保存文本
function chars(text) {
// 去掉空格
text = text.replace(/\s/g, '');
if (text) {
currentParent.children.push({
text,
type: 3
})
}
}
// 将已经匹配的内容切除
function advance(len){
html = html.substring(len);
}
// 匹配开始标签
function parseStartTag() {
const start = html.match(startTagOpen);// 匹配开始标签
if (start) {
const match = {
tagName: start[1], // 匹配的标签名
attrs: []
}
// 切香肠
advance(start[0].length);
// 继续后面的匹配
let end, attr; // 匹配结束标签 属性
while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
// 存储标签的属性和属性值
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;
}
在init中调用模版编译函数
//init.js
// 初始化
import { compileToFunction } from "./compiler/index";
import { initState } from "./state";
// 混入 类似于 对象语言的继承
// 在不改变这个函数的前提下,给这个函数增加方法 增强功能
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = options; // 将配置项暂存到vm实例上
// 初始化 数据
initState(vm);
// 需要通过模板渲染页面
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
Vue.prototype.$mount = function(el) {
const vm = this;
// el 可能是字符串 也可能是dom对象
el = document.querySelector(el);
const opts = vm.$options;
// 如果没有render函数,要创建一个render函数, 使用id=app模板
if (!opts.render) {
let template = opts.template;
// 如果没有在vue中配置模板, 那就取el为模板
if (!template && el) {
template = el.outerHTML;
}
// console.log("template:", template)
// 模板编译
compileToFunction(template);
}
}
}
2. 模板渲染函数
模版编译完成时候,接下来我们添加一个模版渲染函数。
我们需要渲染的数据格式是这样的{{xxx.bb.cc}},因此我们的模版渲染函数需要能从对象中找的元素属性
//compiler/generator.js
// 构建模板渲染函数 {{xxx.bb.cc}} 应该要从对象中查找
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
// 处理子元素
function gen(node) {
// 如果是元素则递归处理
if (node.type === 1) {// 元素
return generator(node);
} else {
// 处理文本 hello {{msg}}---{{msg}}
let text = node.text;
if (!defaultTagRE.test(text)) {
// 单纯文本 就可以直接dom写入 _v()表示写文本函数
return `_v(${JSON.stringify(text)})` // _v('hello')
} else {
// {{}}
let tokens = []; // 保存一点点截取的字符串,最后拼接起来
// 最后索引
let lastIndex = defaultTagRE.lastIndex = 0;
let match, index;
while(match = defaultTagRE.exec(text)) {
// 匹配第一个{{}}的索引位置
index = match.index;
// 截取表达式{{}}左边内容
tokens.push(JSON.stringify(text.slice(lastIndex,index)));
// 取出{{}}表达式的内容 拼函数 _s()
tokens.push(`_s(_data.${match[1].trim()})`)
// 调整lastIndex的位置 下一次匹配的开始位置
lastIndex = index + match[0].length;
}
if (lastIndex < text.length) {
// 截取{{}}右边的内容
tokens.push(JSON.stringify(text.slice(lastIndex)));
}
// 拼接所有内容
return `_v(${tokens.join('+')})`;
}
}
}
// hello {{msg}}---{{msg}}
// 1. hello 切香肠
// 2. {{msg}} _s(msg) 切香肠
// 3. --- 切掉
// 4. {{msg}} _s(msg)
// 判断有没有子元素
function genChildren(el) {
const children = el.children;
if (children) {
return children.map(c => gen(c)).join(",")
} else {
return false;
}
}
// 模板的处理: _c("xxx", "yyy", {xx:yy})
// js 字符串表达式: function fun(){} fun(x,y,z) 执行: eval(js字符串表达式)
export function generator(el) {
// 遍历所有的子元素
let children = genChildren(el);
// console.log("children:", children)
// _c()表示:生成元素函数 参数:元素名,元素属性,元素子集
let code = `_c("${el.tag}", ${
el.attrs.length ? `${genProps(el.attrs)}` : undefined
} ${
children ? `, ${children}` : ""
})`
return code;
}
// 处理元素的属性 {}
function genProps(attrs) {
let str = "";
for (let i=0; i<attrs.length; i++) {
let attr = attrs[i]; // 取第一个属性
if (attr.name === 'style') {
// style color:red; font-size: 30;
let obj = {}
attr.value.split(';').forEach(item => {
let [key, value] = item.split(":");
obj[key] = value;
})
// 将原来的属性value变成obj
attr.value = obj;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`; // += "class: test, style:{color:red,xxx:yyy}"
}
// 最后的属性,需要去掉逗号
return `{${str.slice(0, -1)}}`
}
// 字符串代码 可以传参 jsonp eval with(this)
// _c("div", {id:"app"},
// _c("div", {class:"test",style:{"color":"red"," font-size":" 30"}},
// _c("span", undefined, _v("hello")),))
// "with(this){
// return _c("div", {id:"app"} , _c("div", {class:"test",style:{"color":"red"," fontSize":" 30"}} ,
// _c("span", undefined , _v("hello")),_v(""+_s(_data.msg))))
// }"
在init中调用模版渲染函数
//compiler/index.js
// 模板编译
import { generator } from "./generator";
import { parseHTML } from "./parser";
// 生成虚拟dom
export function compileToFunction(template) {
// 实现模板编译
// 1. 先把html转化为ast语法树
// 2. 标识静态树, 树的遍历
// 3. 通过ast产生的语法树, 生成渲染后的代码
let ast = parseHTML(template);
console.log("ast", ast)
// 构建渲染函数
let code = generator(ast);
// console.log("code:", code)
// 绑定对象作用域 this==当前的组件对象Component
code = `with(this){ \r\n return ${code} \r\n }`
// 将js字符串表达式转为函数
let render = new Function(code);
console.log("code:", render)
return render;
}
这里特别注意一下with方法,with方法在平时一般不建议使用,它在数据量大的时候存在性能问题。作者这里使用with方法应该是权衡了很多实现之后,最终还是选择使用with来实现的。
三、虚拟DOM
1. 挂载组件监听变化
首先我们在index中混入一个_update方法
//index.js
import { initMixin } from "./init";
import { lifeCycleMixin } from "./lifecycle";
function Vue(options){ // vue2 配置项
console.log("my vue 6666")
this._init(options); // vue 内部约定 私有的方法或属性 一般是以 $ _ 开头
}
initMixin(Vue); // 一执行,Vue就有_init方法
lifeCycleMixin(Vue); // 混入_udpate方法
export default Vue
接下来我们来创建一个生命周期过程,完成组件的挂载和监听
//init.js
import { mountComponent } from "./lifecycle";
export function initMixin(Vue) {
// 其他逻辑
Vue.prototype.$mount = function(el) {
const vm = this;
// el 可能是字符串 也可能是dom对象
el = document.querySelector(el);
// 其他逻辑
// 组件挂载
mountComponent(vm, el)
}
}
//lifecycle.js
import Watcher from "./observer/watcher"
// 混入mixin 继承
export function lifeCycleMixin(Vue) {
Vue.prototype._update = function(vnode) {
console.log("vnode:", vnode)
// 如果有子对象要对比更新 就要用diff算法
}
}
// 挂载组件
export function mountComponent(vm, el) {
// vue渲染过程中,会创建一个 “渲染Watcher”
// 数据响应式, Object.defineProperty 仅在属性中添加getter/setter
// 不能够在修改属性后渲染页面,需要Watcher监听
// 渲染函数
const updateComponent = ()=>{
let vnode = "";
// 这里需要拿到虚拟dom, 在下一步实现
vm._update(vnode)
}
let cb = ()=>{}
new Watcher(vm, updateComponent, cb, true /* 渲染Watcher */)
}
// 渲染watcher
class Watcher {
/**
* 参数
* @param {*} vm vue实例
* @param {*} exprOrFn 表达式 或 函数
* @param {*} cb 回调函数
* @param {*} options 配置项
*/
constructor(vm, exprOrFn, cb, options) {
exprOrFn();
}
}
export default Watcher
2. 虚拟dom生成与渲染
我们继续在index中混入一个_render方法
//index.js
import { initMixin } from "./init";
import { lifeCycleMixin } from "./lifecycle";
import { renderMixin } from "./render";
function Vue(options){ // vue2 配置项
console.log("my vue 6666")
this._init(options); // vue 内部约定 私有的方法或属性 一般是以 $ _ 开头
}
initMixin(Vue); // 一执行,Vue就有_init方法
lifeCycleMixin(Vue); // 混入_udpate方法
renderMixin(Vue); // 混入 _render方法
export default Vue
生成虚拟dom
//render.js
// 生成虚拟dom
// 需要实现_c:创建虚拟节点 _v:创建文本虚拟节点 _s:可能是对象
import { createElement, createTextVNode } from "./vdom/create-element"
// 将vue对象中混入 _render 方法
export function renderMixin(Vue) {
Vue.prototype._c = function(){
// 创建虚拟节点 arguments:函数的所有形参和实参集合
console.log("创建虚拟节点")
return createElement(...arguments);
}
Vue.prototype._v = function(text){
// 创建文本虚拟节点
console.log("创建文本虚拟节点")
return createTextVNode(text)
}
Vue.prototype._s = function(val){
// {{msg}} {{obj.name}} {{json}}
return val == undefined ? "" : (typeof val === 'object' ? JSON.stringify(val) : val);
}
// 渲染
Vue.prototype._render = function() {
// 调用compileToFunction生成的render函数
const vm = this;
const { render } = vm.$options;
let vnode = render.call(vm);
return vnode;
}
}
//vdom/create-element.js
// 构建虚拟dom对象 就是用js对象描述的一个对象
// 创建文本节点
export function createTextVNode(text) {
return vnode(undefined, undefined, undefined, undefined, text);
}
// 创建虚拟dom对象
export function createElement(tag, data = {}, ...children) {
return vnode(tag, data, data.key, children)
}
// 创建虚拟节点对象,用于描述dom结构
export function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text
}
}
/*
<ul id="my-id">
<li v-for="(i,item) in list" key="i">{{item}}</li>
</ul>
编译后的渲染函数
createElement(
"ul",
// 标签上的属性 用对象存储键值对
{
attr: {
id: 'my-id'
}
},
[
createElement("li", 1)
createElement("li", 2)
createElement("li", 3)
]
)
执行渲染函数结果 虚拟dom
vnode 用js对象形式表示
tag: ul li
data: array[]
children: []
*/
然后我们在lifecycle中将没有写完的逻辑补上
//lefecycle.js
import Watcher from "./observer/watcher"
import { patch } from "./vdom/patch";
// 混入mixin 继承
export function lifeCycleMixin(Vue) {
Vue.prototype._update = function(vnode) {
console.log("vnode:", vnode)
// 如果有子对象要对比更新 就要用diff算法
// 渲染真实的dom
const vm = this;
vm.$el = patch(vm.$el, vnode);
}
}
// 挂载组件
export function mountComponent(vm, el) {
// vue渲染过程中,会创建一个 “渲染Watcher”
// 数据响应式, Object.defineProperty 仅在属性中添加getter/setter
// 不能够在修改属性后渲染页面,需要Watcher监听
// 渲染函数
const updateComponent = ()=>{
let vnode = "";
// 先拿到虚拟dom, vue2引入虚拟dom
vm._update(vm._render())
}
let cb = ()=>{}
new Watcher(vm, updateComponent, cb, true /* 渲染Watcher */)
}
// 主流 chrome 内核 webkit 处理dom用 dom模块
// js操作dom成本高, 提高效率 dom模块批量处理
//vdom/patch.js
/**
* 给虚拟dom打补丁
* @param {*} oldVnode 旧的el对象
* @param {*} newVnode 新的虚拟dom对象
*/
export function patch(oldVnode, newVnode){
// 根据节点的类型 判断是否是真实元素 el-1 文本-3
const isRealElement = oldVnode.nodeType;
if (isRealElement) {
// 是真实的dom
const oldElm = oldVnode;
const parentElm = oldElm.parentNode;
// 创建元素 操作dom
let el = createElm(newVnode);
// 将新生成的元素插入到旧元素后面
parentElm.insertBefore(el, oldElm.nextSibling);
// 将旧的真实元素删除
parentElm.removeChild(oldElm);
} else {
// 虚拟dom diff
}
}
// 创建元素
export function createElm(vnode) {
let {tag, children, data, key, text} = vnode;
if (tag) {
// 元素
vnode.el = document.createElement(tag); // dom操作--浏览器内核--dom模块
// 处理元素属性
updateProperties(vnode);
children.forEach(child => { // 在浏览器内存中处理
// 递归创建dom元素,并添加到父元素中
vnode.el.appendChild(createElm(child));
})
} else {
// 文本
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') {
// 当作对象处理
for (let styleName in newProps.style) {
// style="color:red; font-size: 30;"
// 添加一个style对象
el.style[styleName] = newProps.style[styleName];
}
} else {
// 单一属性处理
el.setAttribute(key, newProps[key]);
}
}
}
我们再添加一个数据代理
// state.js
// 初始化数据
import { observe } from "./observer/index";
export function initState(vm) {
const opts = vm.$options;
if (opts.props) {
// 处理父传子的属性
}
if (opts.methods) {
// 处理方法
}
if (opts.data) {
initData(vm);
}
}
// 增加代理属性访问, vm._data.msg => vm.msg this.msg
// target---代理对象 property--代理对象属性 key---属性的属性
function proxy(target, property, key) {
Object.defineProperty(target, key, {
get(){// vm[_data][msg]
return target[property][key];
},
set(newValue) {
// vm[_data][msg] = 'test'
target[property][key] = newValue;
}
})
}
// 初始化 数据
function initData(vm) {
// 拿到数据
let data = vm.$options.data;
// 数据可能是对象 也可能是函数
data = vm._data = typeof data === 'function' ? data.call(vm) : data;
// 加一层数据访问代理
for (let key in data) {
proxy(vm, "_data", key);
}
// 监听处理
observe(data);
}
四、生命周期合并
我们先在工具类中完善常见的生命周期工具方法
//utils.js
// 工具类
/**
* 判断data是否是一个对象
* @param {} data
* @returns
*/
export function isObject(data) {
return typeof data === 'object' && data !== null;
}
/**
* 判断是否为函数
* @param {*} data
* @returns
*/
export function isFunction(data) {
return typeof data === 'function' && data !== undefined;
}
// 常见生命周期
const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated'
]
// 生命周期钩子函数,返回[]
function mergeHook(parentVal, childVal) {
if (childVal) {
// 如果有子生命周期
if (parentVal) { // 是一个数组
// 第二次mixin就会走这
return parentVal.concat(childVal);
} else {
return [childVal];
}
} else {
return parentVal;
}
}
// 初始化生命周期对象,{name: fn} fn负责合并相同的生命周期fn
let lifecycles = {}
// 生命周期钩子初始化
LIFECYCLE_HOOKS.forEach(hook => {
lifecycles[hook] = mergeHook;
})
/**
* 合并配置项
* @param {*} parentVal 父
* @param {*} childVal 子
*/
export function mergeOptions(parentVal, childVal) {
const options = {};
// 合并父中的options
for (let key in parentVal) {
// 第一次为空 第二次是有数据的
mergeField(key);
}
// 合并子中的options
for (let key in childVal) {
if (!parentVal.hasOwnProperty(key)) {
mergeField(key);
}
}
function mergeField(key) {
// 合并生全周期
if (lifecycles[key]) {
options[key] = lifecycles[key](parentVal[key], childVal[key]);
} else if (isObject(parentVal[key]) && isObject(childVal[key])) {
// 父子都是对象
// 用子对象覆盖父对象
options[key] = Object.assign(parentVal[key], childVal[key]);
} else if (parentVal[key] && isFunction(parentVal[key]) || childVal[key] && isFunction(childVal[key]) ){
// 判断data 必须是一个函数
// 拿到data的数据, 然后用子覆盖父的属性
let _parentVal = {}, _childVal = {};
if (parentVal[key]) {
// 如果父的data()存在
_parentVal = isFunction(parentVal[key]) ? parentVal[key].call() : parentVal[key];
}
if (childVal[key]) {
_childVal = isFunction(childVal[key]) ? childVal[key].call() : childVal[key];
}
// 用子的数据将父覆盖
options[key] = Object.assign(_parentVal, _childVal);
} else {
if (childVal[key] == null) {// 没有子属性
options[key] = parentVal[key];
} else {
options[key] = childVal[key]; // 子替换父
}
}
}
return options;
}
接着我们在index中添加初始化一个全局api,调用工具类中的mergeOptions函数,完成生命周期合并
//index.js
import { initGlobalAPI } from "./global-api/index";
import { initMixin } from "./init";
import { lifeCycleMixin } from "./lifecycle";
import { renderMixin } from "./render";
function Vue(options){ // vue2 配置项
console.log("my vue 6666")
this._init(options); // vue 内部约定 私有的方法或属性 一般是以 $ _ 开头
}
initMixin(Vue); // 一执行,Vue就有_init方法
lifeCycleMixin(Vue); // 混入_udpate方法
renderMixin(Vue); // 混入 _render方法
// 初始化全局api
initGlobalAPI(Vue);
export default Vue
//global-api/index.js
// 全局api初始化
import { mergeOptions } from "../utils";
export function initGlobalAPI(Vue) {
Vue.options = {};
Vue.mixin = function(mixin) {
// 将一个option合并
// 生命周期合并 数组
// data 当前的为准
this.options = mergeOptions(this.options, mixin);
console.log("options:", this.options)
}
}
生命周期的加载
//lifecycle.js
import Watcher from "./observer/watcher"
import { patch } from "./vdom/patch";
// 混入mixin 继承
export function lifeCycleMixin(Vue) {
Vue.prototype._update = function(vnode) {
console.log("vnode:", vnode)
// 如果有子对象要对比更新 就要用diff算法
// 渲染真实的dom
const vm = this;
vm.$el = patch(vm.$el, vnode);
}
}
// 挂载组件
export function mountComponent(vm, el) {
// vue渲染过程中,会创建一个 “渲染Watcher”
// 数据响应式, Object.defineProperty 仅在属性中添加getter/setter
// 不能够在修改属性后渲染页面,需要Watcher监听
// 渲染前钩子
callHook(vm, 'beforeMount')
// 渲染函数
const updateComponent = ()=>{
let vnode = "";
// 先拿到虚拟dom, vue2引入虚拟dom
vm._update(vm._render())// 调用渲染,要读取属性,触发getter
}
let cb = ()=>{}
new Watcher(vm, updateComponent, cb, true /* 渲染Watcher */)
// 渲染后钩子
callHook(vm, 'mounted');
}
// 主流 chrome 内核 webkit 处理dom用 dom模块
// js操作dom成本高, 提高效率 dom模块批量处理
// 生命周期的回调方法
// 在vue实例的创建---使用---销毁过程中 添加对应的事件钩子, 你只需要注入对应的钩子函数即可
// webpack--tapable
export function callHook(vm, hook) {
let handlers = vm.$options[hook]; // 生命周期函数 created---[fn, fn]
if(handlers) {
// 如果你配置了钩子函数
for (let i=0; i<handlers.length; i++) {
handlers[i].call(vm);
}
}
}
//init.js
// 初始化
import { compileToFunction } from "./compiler/index";
import { callHook, mountComponent } from "./lifecycle";
import { initState } from "./state";
import { mergeOptions } from "./utils";
// 混入 类似于 对象语言的继承
// 在不改变这个函数的前提下,给这个函数增加方法 增强功能
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this;
// vm.$options = options; // 将配置项暂存到vm实例上
vm.$options = mergeOptions(vm.constructor.options, options);
// 定义加载前事件
callHook(vm, 'beforeCreate');
// 初始化 数据
initState(vm);
// 定义已创建事件
callHook(vm, 'created');
// 需要通过模板渲染页面
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
Vue.prototype.$mount = function(el) {
const vm = this;
// el 可能是字符串 也可能是dom对象
el = vm.$el = document.querySelector(el);
const opts = vm.$options;
// 如果没有render函数,要创建一个render函数, 使用id=app模板
if (!opts.render) {
let template = opts.template;
// 如果没有在vue中配置模板, 那就取el为模板
if (!template && el) {
template = el.outerHTML;
}
// console.log("template:", template)
// 模板编译
const render = compileToFunction(template);
// 全局函数 将render添加到全局配置对象中
opts.render = render;
}
// 组件挂载
mountComponent(vm, el)
}
}
五、响应式处理
1. 依赖收集
数据已经装载了,生命周期也有了,接下来我们如何去收集呢?
这时我们需要一个全局变量Dep.target,
- 打开页面, vue实例化,模板解析,生成渲染函数_render, 挂载页面
- 先执行 new Watcher(), 把watcher实例保存到全局 Dep.target.
- 接着执行vm._render(), 触发getter回调,在回调中就可以拿到当前Watcher(dep.target)
//observer/dep.js
// 属性的依赖
let id = 0; // 每个属性的dep实例编号
// Dep <===> Watcher 双向依赖
class Dep {
constructor() {
this.id = id++;
this.subs = []; // 用于存储 渲染Watcher
}
depend(){
// 调用watcher的方法引用dep
Dep.target.addDep(this);
}
addSub(watcher){
// dep关联watcher
this.subs.push(watcher);
}
// 修改属性时, 触发setter回调, 需要通知deps上面所有的watcher更新渲染页面
notify(){
this.subs.forEach(watcher => watcher.update())
}
}
// 全局变量
Dep.target = null;
const stack = [];
// 压入watcher
export function pushTarget(watcher) {
stack.push(watcher);
// 设置全局变量,便于在getter中访问
Dep.target = watcher;
}
// 抛出watcher
export function popTarget() {
// 抛出栈最上面的watcher
stack.pop();
// 修改dep.target全局变量的引用
Dep.target = stack[stack.length - 1];
}
export default Dep;
响应式处理,形成双向依赖关系
//observer/index.js
// 数据监听处理
import {isObject} from "../utils.js"
import { arrayMethods } from "./array.js";
import Dep from './dep.js'
class Observer {
constructor(data) {
// 给每个对象添加__ob__ 引用当前的Observer实例
Object.defineProperty(data, "__ob__", {
enumerable: false,
configurable: false,
value: this
})
if (Array.isArray(data)) {
// data设置新的代理对象
data.__proto__ = arrayMethods;
this.observeArray(data);
} else {
this.walk(data);
}
}
// 遍历属性
walk(data) {
Object.keys(data).forEach(key => {
// 需要对每个属性做响应式处理
console.log(key);
defineReactive(data, key, data[key]);
})
}
// 监听数组中的每一项
observeArray(data) {
for (let i=0; i<data.length; i++) {
observe(data[i]);
}
}
}
// 数据响应式处理
function defineReactive(data, key, value) {
// 如果传入的是一个对象,则递归监听
observe(value);
// 属性--dep依赖
let dep = new Dep();
Object.defineProperty(data, key, {
get() {
if (Dep.target) {
// 属性添加dep依赖,依赖渲染watcher
dep.depend(); // 依赖收集
}
console.log("触发get", key, dep);
return value;
},
set(newValue) {
console.log("触发set", key, dep);
// 如果值没有变化,则忽略
if (value === newValue) return;
// 设置属性可能也是一个对象,需要监听处理
observe(newValue)
value = newValue;
// 通知deps--watcher更新
dep.notify();
}
})
}
// 监听函数可以监听 只能监听对象
export function observe(data) {
// 如果不对象 则返回
if (!isObject(data)) {
return;
}
// 避免二次监听
if (data.__ob__ instanceof Observer) {
return;
}
// 对对象属性进行监听处理(defineProperty)
return new Observer(data);
}
//observer/watcher.js
// 本节后面还要在该类添加计算属性
// 渲染watcher
import { popTarget, pushTarget } from "./dep";
// watcher的编号
let id = 0;
class Watcher {
/**
* 参数
* @param {*} vm vue实例
* @param {*} exprOrFn 表达式 或 函数
* @param {*} cb 回调函数
* @param {*} options 配置项
*/
constructor(vm, exprOrFn, cb, options) {
this.vm = vm;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.options = options;
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn;
}
this.id = id++;
// watcher需要关联dep
this.deps = [];
this.depsId = new Set(); // 数据结构 唯一,不重复
// 实例化时,就调用get();
this.get();
}
// watcher压栈---读属性---dep收集依赖---watcher出栈
get() {
// 初始化
// 保存watcher对象到Dep.target上为全局变量
pushTarget(this);
// 依赖收集
this.getter();
// 清除当前的watchar对象
popTarget();
}
// 在渲染watcher中关联 dep
addDep(dep) {
let id = dep.id;
if (!this.depsId.has(id)) {// 通过dep-id避免重复的收集
this.depsId.add(id);
this.deps.push(dep);
// 双向引用 dep <===> watcher
dep.addSub(this);
}
}
update(){
// 更新渲染页面
this.get();
}
}
export default Watcher
2. 异步队列
//observer/scheduler.js
// 异步任务队列
let has = {}; // 检查渲染watcher是否存在
let queue = []; // 用于保存渲染watcher
// 将队列中所有的watcher执行一遍
function flushSchedulerQueue(){
for (let i=0; i<queue.length; i++) {
queue[i].run();
}
// 清空
queue = [];
has = {};
}
export function queueWatcher(watcher) {
const id = watcher.id;
if (!has[id]) {
has[id] = true;
queue.push(watcher);
// 会创建多个 异步任务
// let n = setTimeout(flushSchedulerQueue)
// console.log("task:", n)
nextTick(flushSchedulerQueue);
}
}
// nexttick
let callbacks = []; // 存储多个异步函数 [flushSchedulerQueue, flushSchedulerQueue]
let pending = false; // 判断是否需要创建定时任务setTimeout
function flushCallbackQueue(){
// 执行所有的渲染watcher, 即所有的dom渲染完成后
callbacks.forEach(fn => fn());
pending = false;
}
export function nextTick(fn) {
callbacks.push(fn);
if (!pending) {// 判断是否创建定时任务
setTimeout(()=>{
// 执行所有异步任务
flushCallbackQueue();
}, 0)
pending = true;
}
}
3. 计算属性
//state.js
// 初始化数据
import Dep from "./observer/dep";
import { observe } from "./observer/index";
import Watcher from "./observer/watcher";
import Vue from './index'
export function initState(vm) {
const opts = vm.$options;
if (opts.props) {
// 处理父传子的属性
}
if (opts.methods) {
// 处理方法
}
if (opts.data) {
initData(vm);
}
if (opts.computed) {
initComputed(vm);
}
if (opts.watch) {
initWatch(vm);
}
}
// 前面数据劫持: data----dep-----渲染watcher
// 计算属性: key-----计算watcher
function initComputed(vm) {
// 用户定义计算属性
const computed = vm.$options.computed;
// 计算属性保存 key--value
const watchers = vm._computedWatcher = {}
// 遍历拿到所有的计算属性
for (let k in computed) {
// 计算属性值
const userDef = computed[k];
// 拿到计算属性的getter
const getter = typeof userDef === 'function' ? userDef : userDef.get;
// 将所有的计算属性保存, 计算属性-----计算watcher 渲染watcher
watchers[k] = new Watcher(vm, getter, ()=>{}, {lazy: true})
// 在vm实例上动态定义计算属性
defineComputed(vm, k, userDef);
}
}
// 定义一个普通的对象来劫持计算属性
const sharedPropertyDefinition = {
configurable: true,
enumerable: true,
get: ()=>{},
set: ()=>{}
}
// 动态定义计算属性
function defineComputed(target, key, userDef) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key);
} else {
sharedPropertyDefinition.get = createComputedGetter(key);
sharedPropertyDefinition.set = userDef.set;
}
// 给vm实例动态绑定计算属性
Object.defineProperty(target, key, sharedPropertyDefinition);
}
// 重写计算属性get方法,判断是否需要重新计算
function createComputedGetter(key) {
return function(){
// 拿到计算属性对应的 watcher
const watcher = this._computedWatcher[key];
if (watcher) {
// 判断是否需要计算
if (watcher.dirty) {
// 计算属性取值
watcher.evaluate();
// 判断当前的Watcher
if (Dep.target) {
watcher.depend();
}
}
return watcher.value;
}
}
}
// 实现watch侦听器
function initWatch(vm) {
let watch = vm.$options.watch;
for (let key in watch) {
// 回调函数
const handlerObj = watch[key];
if (Array.isArray(handlerObj)) {
// 遍历handler数组
handlerObj.forEach(handler => {
// 创建用户watcher
createWatcher(vm, key, handler)
})
} else {
createWatcher(vm, key, handlerObj);
}
}
}
// 创建 用户watcher 用于侦听watche
function createWatcher(vm, exprOrFn, handler, options={}) {
// 如果回调是对象, 那就要取hanlder函数
if (typeof handler === 'object') {
options = handler; // 保存用户传入的参数
handler = handler.handler; // 取出watch侦听回调
}
// 如果回调是一个字符串, 那一定是vm上
if (typeof handler === 'string') {
handler = vm[handler]; // 从vue实例上取侦听回调
}
// 回调是一个函数
return vm.$watch(exprOrFn, handler, options)
}
// 在vue原型上添加$watch, 需要在配置项上增加一个额外的标识 user:true
Vue.prototype.$watch = function(exprOrFn, cb, options) {
// console.log("watch:",exprOrFn, cb, options)
const vm = this;
// 用户watcher
let watcher = new Watcher(vm, exprOrFn, cb, {...options, user:true})
// 立即执行
if (options.immediate) {
cb();
}
}
// 增加代理属性访问, vm._data.msg => vm.msg this.msg
// target---代理对象 property--代理对象属性 key---属性的属性
function proxy(target, property, key) {
Object.defineProperty(target, key, {
get(){// vm[_data][msg]
return target[property][key];
},
set(newValue) {
// vm[_data][msg] = 'test'
target[property][key] = newValue;
}
})
}
// 初始化 数据
function initData(vm) {
// 拿到数据
let data = vm.$options.data;
// 数据可能是对象 也可能是函数
data = vm._data = typeof data === 'function' ? data.call(vm) : data;
// 加一层数据访问代理
for (let key in data) {
proxy(vm, "_data", key);
}
// 监听处理
observe(data);
}
//observer/watcher.js
// 渲染watcher 支持计算watcher 支持用户watcher
import { isObject } from "../utils";
import { popTarget, pushTarget } from "./dep";
import { queueWatcher } from "./scheduler";
// watcher的编号
let id = 0;
class Watcher {
/**
* 参数
* @param {*} vm vue实例
* @param {*} exprOrFn 表达式 或 函数
* @param {*} cb 回调函数
* @param {*} options 配置项
*/
constructor(vm, exprOrFn, cb, options) {
this.vm = vm;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.options = options;
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn;
} else {
// for 用户watcher 中 getter 即为读取属性的值
this.getter = function(){
// 用户watcher传递是一个字符串
let obj = vm;
let path = exprOrFn.split('.');
for (let i = 0; i<path.length; i++) {
// 一级级取对象的值 aa.bb.cc vm[aa][bb][cc]
obj = obj[path[i]];
}
return obj;
}
}
this.id = id++;
// watcher需要关联dep
this.deps = [];
this.depsId = new Set(); // 数据结构 唯一,不重复
// 用户watcher
this.user = options.user;
this.lazy = options.lazy;// 计算属性标识
this.dirty = this.lazy; // 脏值判断, 如果计算属性有修改需要重新计算则为脏
// 若为计算watcher则不处理, 为渲染watcher则执行get
this.value = this.lazy ? undefined : this.get();
}
// watcher压栈---读属性---dep收集依赖---watcher出栈
get() {
// 初始化
// 保存watcher对象到Dep.target上为全局变量
pushTarget(this);
// 依赖收集
const res = this.getter.call(this.vm);
// 清除当前的watchar对象
popTarget();
return res;
}
// 在渲染watcher中关联 dep
addDep(dep) {
let id = dep.id;
if (!this.depsId.has(id)) {// 通过dep-id避免重复的收集
this.depsId.add(id);
this.deps.push(dep);
// 双向引用 dep <===> watcher
dep.addSub(this);
}
}
update(){
// 更新渲染页面 同步
if (this.lazy) {
// 表示云计算属性有变化, 需要重新计算
this.dirty = true;
} else {
queueWatcher(this);
}
}
run(){
// 异步队列调用
// 先要拿侦听的属性的值
const oldValue = this.value;
const newValue = this.get();
// console.log("watcher:", oldValue, newValue)
this.value = newValue;
if (this.user) {
// 是用户侦听watcher
if (newValue !== oldValue || isObject(newValue)) {
// 执行侦听回调函数
this.cb.call(this.vm, newValue, oldValue);
}
} else {
// 渲染watcher
this.cb.call(this.vm);
}
}
evaluate() {
// 计算新的计算属性结果
this.value = this.get();
// 当计算属性执行完毕后,Dep.target指向的是渲染watcher
// 改变脏值状态
this.dirty = false;
}
// 让所有的计算属性都有渲染watcher
depend(){
let i = this.deps.length;
while(i--) {
this.deps[i].depend(); // 绑定当前的渲染watcher
}
}
}
export default Watcher
六、diff原理
虚拟DOM就像是一个轻量级的"内存中的UI模型",Vue用它来高效地更新真实DOM。Diff算法就是比较新旧虚拟DOM,找出最小变更集的算法。
diff算法
- 根节点对比, 如果不相同,直接替换, 如果相同, 检查属性是否相同。
- 老的有子节点,新的没有子节点, 直接删除老的子节点
- 老的无子节点,新的有子节点,直接插入新的子节点
- 老的和新的都有子节点,则使用diff比较
diff比较
- 从头开始比较, 从左到右, 老abcd, 新abcde, 直接插入多余的e节点
- 从右到左比较, 老abcd, 新eabcd.
- 头尾都不相同,有可能旧头与新尾相同,旧尾与新头相同 abcd dcba
- 乱序比较, 用新头节点与旧的所有的节点比较,若有相同的则移动元素, 若无相同则插入新的节点
abcd cdme
用新头与旧的所有匹配, 可以匹配到c, 移动c, abcd---> cab_d
用新d与旧的所有匹配, 可以匹配到d, 移动d, cab_d---> cdab__
新m与旧的所有匹配, 无匹配, 直接插入d后面, cdmab__
新e与旧的所有匹配, 无匹配, cdmeab__, 删除ab__
为什么要key?
// 老元素
<ul>
<li key=0>aaaa</li>
<li key=1>bbbb</li>
</ul>
// 新元素
<ul>
<li key=0>cccc</li>
<li key=1>aaaa</li>
</ul>
// diff比较会先比较key, 会使用元素内容替换,会替换2次
// 最好的情况
// 老元素
<ul>
<li key=aaaa>aaaa</li>
<li key=bbbb>bbbb</li>
</ul>
// 新元素
<ul>
<li key=ccccc>cccc</li>
<li key=aaaa>aaaa</li>
</ul>
// 只会插入一次,移动一次
代码实现
//vdom/patch.js
/**
* 给虚拟dom打补丁
* @param {*} oldVnode 旧的el对象
* @param {*} newVnode 新的虚拟dom对象
*/
export function patch(oldVnode, newVnode){
// 根据节点的类型 判断是否是真实元素 el-1 文本-3
const isRealElement = oldVnode.nodeType;
if (isRealElement) {
// 是真实的dom
const oldElm = oldVnode;
const parentElm = oldElm.parentNode;
// 创建元素 操作dom
let el = createElm(newVnode);
// 将新生成的元素插入到旧元素后面
parentElm.insertBefore(el, oldElm.nextSibling);
// 将旧的真实元素删除
parentElm.removeChild(oldElm);
return el;
} else {
// 虚拟dom diff
// 比较标签是否相同
if (oldVnode.tag !== newVnode.tag) {
// 直接替换旧元素
oldVnode.el.parentNode.replaceChild(createElm(newVnode), oldVnode.el)
}
// 标签相同,如果是文本变化, 直接用新的文本替换老的文本
if (!oldVnode.tag) {
// 比较内容
if (oldVnode.text !== newVnode.text) {
oldVnode.el.textContent = newVnode.text;
}
}
// 标签相同, 比较属性
let el = newVnode.el = oldVnode.el;
// 更新根节点属性
updateProperties(newVnode, oldVnode.data)
// 情况1,从左到右比较
let oldChildren = oldVnode.children || {}
let newChildren = newVnode.children || {}
if (oldChildren.length > 0 && newChildren.length > 0) {
// diff
updateChildren(el, oldChildren, newChildren);
} else if (oldChildren.length > 0) {
// 老的有子节点,新的没有,则直接删除
el.innerHTML = "";
} else if (newChildren.length > 0) {
// 老的没有子节点,新的有,则直接添加
for (let i=0; i<newChildren.length; i++) {
let child = newChildren[i];
el.appendChild(createElm(child));
}
}
}
}
// 判断节点是否相同, 通过key和标签比较
function isSameVnode(oldVnode, newVnode) {
return (oldVnode.key === newVnode.key) && (oldVnode.tag === newVnode.tag);
}
/**
* diff核心方法
* @param {*} parent
* @param {*} oldChildren
* @param {*} newChildren
*/
function updateChildren(parent, oldChildren, newChildren) {
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]; // 新的尾部节点
// 创建key--索引映射表 a-0 b-1
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index)=>{
map[item.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
// 情况1--从左到右比较
if (isSameVnode(oldStartVnode, newStartVnode)) {
// 比较节点的标签和属性
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)) {
// 旧尾节点与新的头节点
patch(oldEndVnode, newStartVnode);
// 移动元素 将旧尾节点移动到旧头节点前面
parent.insertBefore(oldEndVnode.el, oldStartVnode.el)
// 指针调整,旧的向左, 新的向右
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex];
} else {
// 乱序比较
// 根据新头的key查找是否存在对应索引
let oldMoveIndex = map[newStartVnode.key];
if (oldMoveIndex == undefined) {
// 未匹配到,说明是新元素,直接插入
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else {
// 匹配到cd 找到旧cd并清除原占位置
let oldMoveVnode = oldChildren[oldMoveIndex];
oldChildren[oldMoveIndex] = null;
// 将c移动到旧头前面
parent.insertBefore(oldMoveVnode.el, oldStartVnode.el);
// 比较节点内容
patch(oldMoveVnode, newStartVnode);
}
// 指针调整 新节点
newStartVnode = newChildren[++newStartIndex];
}
}
// 情况1--从左到右,后面多余的元素直接插入
if (newStartIndex <= newEndIndex) {
for(let i=newStartIndex; i <= newEndIndex; i++) {
// 新节点结束指针后面的元素
let el2 = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].el;
parent.insertBefore(createElm(newChildren[i]), el2);
}
}
// diff比较完毕, 删除多余的旧节点
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i<= oldEndIndex; i++) {
let child = oldChildren[i];
if (child) {
parent.removeChild(child.el);
}
}
}
}
// 创建元素
export function createElm(vnode) {
let {tag, children, data, key, text} = vnode;
if (tag) {
// 元素
vnode.el = document.createElement(tag); // dom操作--浏览器内核--dom模块
// 处理元素属性
updateProperties(vnode);
children.forEach(child => { // 在浏览器内存中处理
// 递归创建dom元素,并添加到父元素中
vnode.el.appendChild(createElm(child));
})
} else {
// 文本
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
// 元素属性更新
function updateProperties(vnode, oldProps = {}) {
let el = vnode.el;
// 属性
let newProps = vnode.data || {}
// diff 属性更新
// 获取旧的样式和新的样式的差异, 如果新的没有旧的属性,则删除
let newStyle = newProps.style || {}
let oldStyle = oldProps.style || {}
for (let key in oldStyle) {
// 新的样式中不包括旧的样式 则删除
if (!newStyle[key]) {
el.style[key] = "";
}
}
// 判断属性
for (let key in oldProps) {
// 新的属性中不存在旧的属性, 则删除
if (!newProps[key]) {
el.removeAttribute(key);
}
}
for (let key in newProps) {
if (key === 'style') {
// 当作对象处理
for (let styleName in newProps.style) {
// style="color:red; font-size: 30;"
// 添加一个style对象
el.style[styleName] = newProps.style[styleName];
}
} else {
// 单一属性处理
el.setAttribute(key, newProps[key]);
}
}
}
测试
//index.js
import { initGlobalAPI } from "./global-api/index";
import { initMixin } from "./init";
import { lifeCycleMixin } from "./lifecycle";
import { renderMixin } from "./render";
function Vue(options){ // vue2 配置项
console.log("my vue 6666")
this._init(options); // vue 内部约定 私有的方法或属性 一般是以 $ _ 开头
}
initMixin(Vue); // 一执行,Vue就有_init方法
lifeCycleMixin(Vue); // 混入_udpate方法
renderMixin(Vue); // 混入 _render方法
// 初始化全局api
initGlobalAPI(Vue);
// diff 真元素 虚拟元素 虚拟元素
import {compileToFunction} from './compiler/index'
import {createElm, patch} from './vdom/patch'
let vm1 = new Vue({data: {name: "test1"}})
let vm2 = new Vue({data: {name: "test2"}})
// 生成虚拟dom
let render1 = compileToFunction(`<div style="color:red;background-color:#333" id=2>{{name}}</div>`)
let oldVnode = render1.call(vm1);
console.log("oldVnode:", oldVnode)
// 把虚拟dom生成为真实dom
let realElement = createElm(oldVnode);
document.body.appendChild(realElement)
// 再创建一个虚拟dom
let render2 = compileToFunction(`<div style="color: blue;">{{name}}</div>`)
let newVnode = render2.call(vm2);
// diff比较
setTimeout(()=>{
// 真实元素与虚拟dom比较: 先插入新元素,再删除旧元素
// patch(realElement, newVnode);
// 虚拟dom比较
patch(oldVnode, newVnode);
}, 1000)
export default Vue
//public/index
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue</title>
</head>
<body>
<script src="../dist/vue.js"></script>
<div id=app>
<div class="test" style="color:red; fontSize: 30;">
<div>hello</div>
--{{msg}}----{{name.aa}}----{{myInfo}}----{{addr}}
</div>
</div>
<!-- <div id=app2>
<div class="test" style="color:red; fontSize: 30;">
<div>hello</div>
--{{msg2}}----{{name.aa}}
</div>
</div> -->
<script>
// 渲染watcher 计算watcher
// 依赖收集,在getter中收集依赖,收集的是渲染watcher, 在setter中触发更新
// 依赖收集结果:data---属性---dep---渲染watcher
// 更新:data---属性---触发更新---setter---找到对应属性上的dep---遍历执行所有关联的 渲染watcher
// 计算属性,用户自定义的属性, 在vm上动态添加一个属性,同时添加劫持,计算属性----计算watcher
// 计算watcher 与 渲染watcher 区别:
// 1. 计算watcher有一个 lazy=true 标识 2. 计算watcher只是改变dirty状态
// 如果修改计算属性,页面没刷新的原因是: 计算属性-----(计算watcher,渲染watcher)
// watch侦听器的原理:也需要添加一个watcher, 用户Watcher, 用户主观去监听某个属性变化
// 1. 取侦听属性名, 和 侦听回调函数
// 2. 创建一个用户wathcer, user:true
// 3. 取属性的值,判断,如果有变化,就执行 用户watcher的回调函数
const vm = new Vue({
el: "#app",
data() {
return {
msg: "abcd",
name: "asfdsafdsadf",
age: 25,
aa: {
bb: 'cc'
},
addr: "广东深圳",
arr: ['1','2']
}
},
watch: {
addr(newValue, oldValue){
console.log("侦听器:", newValue, oldValue)
},
"aa.bb": {
handler(newValue, oldValue) {
console.log("侦听器2:", newValue, oldValue)
}
},
age: [
function handler1(newValue, oldValue) {
console.log("侦听器3:", newValue, oldValue)
},
function handler2(newValue, oldValue) {
console.log("侦听器4:", newValue, oldValue)
}
]
},
computed: {
myInfo() {
return this.name + "===" + this.age;
},
myInfo2: {
get: function(){
return this.name + "=====" + this.age;
},
set: function(newValue){ // 只修改name
this.name = newValue;
}
}
}
})
// console.log(vm)
console.log(vm._data.msg, vm.msg) // 读取属性
// vm._data.msg = {value: "bbbb"} // 设置属性
// console.log(vm._data.msg.value)
// const vm2 = new Vue({
// el: "#app2",
// data() {
// return {
// msg2: "abcd22222",
// name: {
// aa: 'bb'
// },
// arr: ['1','2']
// }
// },
// data2: null,
// data3: {test: 4},
// created(){
// console.log('create 3')
// },
// mounted(){
// console.log("数据已装载完成")
// },
// beforeCreate(){
// console.log('beforeCreate 3')
// }
// })
setTimeout(()=>{
vm.msg="test1";
vm.age=26;
vm.aa.bb="test3";
vm.addr="北京"
}, 1000)
// setTimeout(()=>{
// vm.myInfo2 = "333333333333333"
// console.log("修改计算属性")
// }, 1000)
</script>
</body>
</html>