vue自定义实现
# 简介
手写vue2核心原理:
Vue2
响应式原理,模板编译原理,虚拟DOM原理,vue初渲染流程Vue2
中生命周期原理,mixin原理,依赖收集Watcher
、Dep
原理。- 手写
computed
及watch
原理。异步更新原理 - 手写
Vue2
中组件渲染原理Vue.extend
原理、Vue2diff
算法
# 搭建开发环境
# 1.什么是Rollup?
Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码, rollup.js更专注于Javascript类库打包 (开发应用时使用Wwebpack,开发库时使用Rollup)
# 2.环境搭建
安装rollup环境
npm install @babel/preset-env @babel/core rollup rollup-plugin-babel rollup-plugin-serve cross-env -D
rollup.config.js文件编写
import babel from 'rollup-plugin-babel';
import serve from 'rollup-plugin-serve';
export default {
input: './src/index.js',
output: {
format: 'umd', // 模块化类型
file: 'dist/umd/vue.js',
name: 'Vue', // 打包后的全局变量的名字
sourcemap: true
},
plugins: [
babel({
exclude: 'node_modules/**'
}),
process.env.ENV === 'development'?serve({
open: true,
openPage: '/public/index.html',
port: 3000,
contentBase: ''
}):null
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
配置.babelrc文件
{
"presets": [
"@babel/preset-env"
]
}
2
3
4
5
执行脚本配置
"scripts": {
"dev": "rollup -c -w",
"build": "rollup -c",
"start": "cross-env ENV=development rollup -c -w"
},
2
3
4
5
# 响应式原理
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
导出vue
构造函数
import {initMixin} from './init';
function Vue(options) {
this._init(options);
}
initMixin(Vue); // 给原型上新增_init方法
export default Vue;
2
3
4
5
6
7
init
方法中初始化vue
状态
import {initState} from './state';
export function initMixin(Vue){
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = options
initState(vm)// 初始化状态
}
}
2
3
4
5
6
7
8
根据不同属性进行初始化操作
export function initState(vm){
const opts = vm.$options;
if(opts.props){
initProps(vm);
}
if(opts.data){
initData(vm);// 初始化data
}
if(opts.computed){
initComputed(vm);
}
if(opts.watch){
initWatch(vm);
}
if(opts.methods){
initMethod(vm);
}
}
function initProps(){}
function initData(){}
function initComputed(){}
function initWatch(){}
function initMethod(){}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1.初始化数据
import {observe} from './observer/index.js'
function initData(vm){
let data = vm.$options.data;
data = vm._data = typeof data === 'function' ? data.call(vm) : data;
observe(data);
}
2
3
4
5
6
# 2.递归属性劫持
class Observer { // 观测值
constructor(value){
this.walk(value);
}
walk(data){ // 让对象上的所有属性依次进行观测
let keys = Object.keys(data);
for(let i = 0; i < keys.length; i++){
let key = keys[i];
let value = data[key];
defineReactive(data,key,value);
}
}
}
function defineReactive(data,key,value){
observe(value);
Object.defineProperty(data,key,{
get(){
return value
},
set(newValue){
if(newValue == value) return;
observe(newValue);
value = newValue
}
})
}
export function observe(data) {
if(typeof data !== 'object' || data == null){
return;
}
return new Observer(data);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 3.数组方法的劫持
import {arrayMethods} from './array';
class Observer { // 观测值
constructor(value){
if(Array.isArray(value)){
value.__proto__ = arrayMethods; // 重写数组原型方法
this.observeArray(value);
}else{
this.walk(value);
}
}
observeArray(value){
for(let i = 0 ; i < value.length ;i ++){
observe(value[i]);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
重写数组原型方法
let oldArrayProtoMethods = Array.prototype;
export let arrayMethods = Object.create(oldArrayProtoMethods);
let methods = [
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice'
];
methods.forEach(method => {
arrayMethods[method] = function (...args) {
const result = oldArrayProtoMethods[method].apply(this, args);
const ob = this.__ob__;
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2)
default:
break;
}
if (inserted) ob.observeArray(inserted); // 对新增的每一项进行观测
return result
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
增加__ob__
属性
判断一个对象是否被观测过看他有没有 __ob__这个属性; 及再array重写中设置调用方法;
class Observer {
constructor(value){
Object.defineProperty(value,'__ob__',{
enumerable:false,
configurable:false,
value:this
});
// ...
}
}
2
3
4
5
6
7
8
9
10
给所有响应式数据增加标识,并且可以在响应式上获取
Observer
实例上的方法
# 4.数据代理
function proxy(vm,source,key){
Object.defineProperty(vm,key,{
get(){
return vm[source][key];
},
set(newValue){
vm[source][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){ // 将_data上的属性全部代理给vm实例
proxy(vm,'_data',key)
}
observe(data);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 模板编译
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = options;
// 初始化状态
initState(vm);
// 页面挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
}
Vue.prototype.$mount = function (el) {
const vm = this;
const options = vm.$options;
el = document.querySelector(el);
// 如果没有render方法
if (!options.render) {
let template = options.template;
// 如果没有模板但是有el
if (!template && el) {
template = el.outerHTML;
}
const render= compileToFunctions(template);
options.render = render;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
将template
编译成render函数
# 1.解析标签和内容
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 </div>
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 >
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
function start(tagName,attrs){
console.log(tagName,attrs)
}
function end(tagName){
console.log(tagName)
}
function chars(text){
console.log(text);
}
function parseHTML(html){
while(html){
let textEnd = html.indexOf('<');
if(textEnd == 0){
const startTagMatch = parseStartTag();
if(startTagMatch){
start(startTagMatch.tagName,startTagMatch.attrs);
continue;
}
const endTagMatch = html.match(endTag);
if(endTagMatch){
advance(endTagMatch[0].length);
end(endTagMatch[1]);
continue;
}
}
let text;
if(textEnd >= 0){
text = html.substring(0,textEnd);
}
if(text){
advance(text.length);
chars(text);
}
}
function advance(n){
html = html.substring(n);
}
function parseStartTag(){
const start = html.match(startTagOpen);
if(start){
const match = {
tagName:start[1],
attrs:[]
}
advance(start[0].length);
let attr,end;
while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))){
advance(attr[0].length);
match.attrs.push({name:attr[1],value:attr[3]});
}
if(end){
advance(end[0].length);
return match
}
}
}
}
export function compileToFunctions(template){
parseHTML(template);
return function(){}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 2.生成ast语法树
语法树就是用对象描述js
语法
{
tag:'div',
type:1,
children:[{tag:'span',type:1,attrs:[],parent:'div对象'}],
attrs:[{name:'zf',age:10}],
parent:null
}
2
3
4
5
6
7
let root;
let currentParent;
let stack = [];
const ELEMENT_TYPE = 1;
const TEXT_TYPE = 3;
function createASTElement(tagName,attrs){
return {
tag:tagName,
type:ELEMENT_TYPE,
children:[],
attrs,
parent:null
}
}
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);
}
}
function chars(text) {
text = text.replace(/\s/g,'');
if(text){
currentParent.children.push({
type:TEXT_TYPE,
text
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 3.生成代码
template
转化成render函数的结果
<div style="color:red">hello {{name}} <span></span></div>
render(){
return _c('div',{style:{color:'red'}},_v('hello'+_s(name)),_c('span',undefined,''))
}
2
3
4
实现代码生成
function gen(node) {
if (node.type == 1) {
return generate(node);
} else {
let text = node.text
if(!defaultTagRE.test(text)){
return `_v(${JSON.stringify(text)})`
}
let lastIndex = defaultTagRE.lastIndex = 0
let tokens = [];
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 getChildren(el) { // 生成儿子节点
const children = el.children;
if (children) {
return `${children.map(c=>gen(c)).join(',')}`
} else {
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)}}`;
}
function generate(el) {
let children = getChildren(el);
let code = `_c('${el.tag}',${
el.attrs.length?`${genProps(el.attrs)}`:'undefined'
}${
children? `,${children}`:''
})`;
return code;
}
let code = generate(root);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 4.生成render
函数
export function compileToFunctions(template) {
parseHTML(template);
let code = generate(root);
let render = `with(this){return ${code}}`;
let renderFn = new Function(render);
return renderFn
}
2
3
4
5
6
7
# 创建渲染watcher
# 1.初始化渲染Watcher
import {mountComponent} from './lifecycle'
Vue.prototype.$mount = function (el) {
const vm = this;
const options = vm.$options;
el = document.querySelector(el);
// 如果没有render方法
if (!options.render) {
let template = options.template;
// 如果没有模板但是有el
if (!template && el) {
template = el.outerHTML;
}
const render= compileToFunctions(template);
options.render = render;
}
mountComponent(vm,el);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lifecycle.js
先调用_render
方法生成虚拟dom
,通过_update
方法将虚拟dom
创建成真实的dom
export function lifecycleMixin() {
Vue.prototype._update = function (vnode) {}
}
export function mountComponent(vm, el) {
vm.$el = el;
let updateComponent = () => {
// 将虚拟节点 渲染到页面上
vm._update(vm._render());
}
new Watcher(vm, updateComponent, () => {}, true);
}
2
3
4
5
6
7
8
9
10
11
render.js
export function renderMixin(Vue){
Vue.prototype._render = function () {}
}
2
3
watcher.js
let id = 0;
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm;
this.exprOrFn = exprOrFn;
if (typeof exprOrFn == 'function') {
this.getter = exprOrFn;
}
this.cb = cb;
this.options = options;
this.id = id++;
this.get();
}
get() {
this.getter();
}
}
export default Watcher;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 2.生成虚拟dom
import {createTextNode,createElement} from './vdom/create-element'
export function renderMixin(Vue){
Vue.prototype._v = function (text) { // 创建文本
return createTextNode(text);
}
Vue.prototype._c = function () { // 创建元素
return createElement(...arguments);
}
Vue.prototype._s = function (val) {
return val == null? '' : (typeof val === 'object'?JSON.stringify(val):val);
}
Vue.prototype._render = function () {
const vm = this;
const {render} = vm.$options;
let vnode = render.call(vm);
return vnode;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
创建虚拟节点
export function createTextNode(text) {
return vnode(undefined,undefined,undefined,undefined,text)
}
export function createElement(tag,data={},...children){
let key = data.key;
if(key){
delete data.key;
}
return vnode(tag,data,key,children);
}
function vnode(tag,data,key,children,text){
return {
tag,
data,
key,
children,
text
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3.生成真实DOM
元素
将虚拟节点渲染成真实节点
import {patch} './observer/patch'
export function lifecycleMixin(Vue){
Vue.prototype._update = function (vnode) {
const vm = this;
vm.$el = patch(vm.$el,vnode);
}
}
2
3
4
5
6
7
export function patch(oldVnode,vnode){
const isRealElement = oldVnode.nodeType;
if(isRealElement){
const oldElm = oldVnode;
const parentElm = oldElm.parentNode;
let el = createElm(vnode);
parentElm.insertBefore(el,oldElm.nextSibling);
parentElm.removeChild(oldVnode)
return el;
}
}
function createElm(vnode){
let {tag,children,key,data,text} = vnode;
if(typeof tag === 'string'){
vnode.el = document.createElement(tag);
updateProperties(vnode);
children.forEach(child => {
return vnode.el.appendChild(createElm(child));
});
}else{
vnode.el = document.createTextNode(text);
}
return vnode.el
}
function updateProperties(vnode){
let newProps = vnode.data || {}; // 获取当前老节点中的属性
let el = vnode.el; // 当前的真实节点
for(let key in newProps){
if(key === 'style'){
for(let styleName in newProps.style){
el.style[styleName] = newProps.style[styleName]
}
}else if(key === 'class'){
el.className= newProps.class
}else{ // 给这个元素添加属性 值就是对应的值
el.setAttribute(key,newProps[key]);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 生命周期的合并
# 1.Mixin原理
import {mergeOptions} from '../util/index.js'
export function initGlobalAPI(Vue){
Vue.options = {};
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options,mixin);// 将属性合并到Vue.options上
return this;
}
}
2
3
4
5
6
7
8
# 2.合并生命周期
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
]
const strats = {};
function mergeHook(parentVal, childValue) {
if (childValue) {
if (parentVal) {
return parentVal.concat(childValue);
} else {
return [childValue]
}
} else {
return parentVal;
}
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
export function mergeOptions(parent, child) {
const options = {}
for (let key in parent) {
mergeField(key)
}
for (let key in child) {
if (!parent.hasOwnProperty(key)) {
mergeField(key);
}
}
function mergeField(key) {
if (strats[key]) {
options[key] = strats[key](parent[key], child[key]);
} else {
if (typeof parent[key] == 'object' && typeof child[key] == 'object') {
options[key] = {
...parent[key],
...child[key]
}
}else{
options[key] = child[key];
}
}
}
return options
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 4.生命周期发布订阅
export function callHook(vm, hook) {
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm);
}
}
}
2
3
4
5
6
7
8
# 5.调用生命周期
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options,options);
// 初始化状态
callHook(vm,'beforeCreate');
initState(vm);
callHook(vm,'created');
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
}
export function mountComponent(vm, el) {
callHook(vm, 'beforeMount');
// 在初始化时,下面两方法已经提取挂载到属性上;调用render方法去渲染 el属性;
vm._update(vm._render()); // 先调用render方法创建虚拟节点,在将虚拟节点渲染到页面上; 虚拟Dom ====> 真Dom
callHook(vm, 'mounted');
//优化后:
// 默认vue是通过watcher来进行渲染 = 渲染watcher (每一个组件都有一个渲染watcher)
// let updateComponent = () => {
// vm._update(vm._render()); // 虚拟节点
// }
// new Watcher(vm, updateComponent, () => { }, true);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 依赖收集
每个属性都要有一个dep
,每个dep
中存放着watcher
,同一个watcher
会被多个dep
所记录。
# 1.在渲染时存储watcher
class Watcher{
// ...
get(){
pushTarget(this);
this.getter();
popTarget();
}
}
2
3
4
5
6
7
8
let id = 0;
class Dep{
constructor(){
this.id = id++;
}
}
let stack = [];
export function pushTarget(watcher){
Dep.target = watcher;
stack.push(watcher);
}
export function popTarget(){
stack.pop();
Dep.target = stack[stack.length-1];
}
export default Dep;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.对象依赖收集
let dep = new Dep();
Object.defineProperty(data, key, {
get() {
if(Dep.target){ // 如果取值时有watcher
dep.depend(); // 让watcher保存dep,并且让dep 保存watcher
}
return value
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
dep.notify(); // 通知渲染watcher去更新
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Dep实现
class Dep{
constructor(){
this.id = id++;
this.subs = [];
}
depend(){
if(Dep.target){
Dep.target.addDep(this);// 让watcher,去存放dep
}
}
notify(){
this.subs.forEach(watcher=>watcher.update());
}
addSub(watcher){
this.subs.push(watcher);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
watcher
constructor(){
this.deps = [];
this.depsId = new Set();
}
addDep(dep){
let id = dep.id;
if(!this.depsId.has(id)){
this.depsId.add(id);
this.deps.push(dep);
dep.addSub(this);
}
}
update(){
this.get();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3.数组的依赖收集
this.dep = new Dep(); // 专门为数组设计的
if (Array.isArray(value)) {
value.__proto__ = arrayMethods;
this.observeArray(value);
} else {
this.walk(value);
}
function defineReactive(data, key, value) {
let childOb = observe(value);
let dep = new Dep();
Object.defineProperty(data, key, {
get() {
if(Dep.target){
dep.depend();
if(childOb){
childOb.dep.depend(); // 收集数组依赖
}
}
return value
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
dep.notify();
}
})
}
arrayMethods[method] = function (...args) {
// ...
ob.dep.notify()
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
递归收集数组依赖
if(Dep.target){
dep.depend();
if(childOb){
childOb.dep.depend(); // 收集数组依赖
if(Array.isArray(value)){ // 如果内部还是数组
dependArray(value);// 不停的进行依赖收集
}
}
}
function dependArray(value) {
for (let i = 0; i < value.length; i++) {
let current = value[i];
current.__ob__ && current.__ob__.dep.depend();
if (Array.isArray(current)) {
dependArray(current)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 实现Vue异步更新之nextTick
# 1.实现队列机制
update(){
queueWatcher(this);
}
2
3
scheduler
import {
nextTick
} from '../util/next-tick'
let has = {};
let queue = [];
function flushSchedulerQueue() {
for (let i = 0; i < queue.length; i++) {
let watcher = queue[i];
watcher.run()
}
queue = [];
has = {}
}
let pending = false
export function queueWatcher(watcher) {
const id = watcher.id;
if (has[id] == null) {
has[id] = true;
queue.push(watcher);
if(!pending){
nextTick(flushSchedulerQueue)
pending = true;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 2.nextTick实现原理
util/next-tick.js
let callbacks = [];
function flushCallbacks() {
callbacks.forEach(cb => cb());
}
let timerFunc;
if (Promise) { // then方法是异步的
timerFunc = () => {
Promise.resolve().then(flushCallbacks)
}
}else if (MutationObserver) { // MutationObserver 也是一个异步方法
let observe = new MutationObserver(flushCallbacks); // H5的api
let textNode = document.createTextNode(1);
observe.observe(textNode, {
characterData: true
});
timerFunc = () => {
textNode.textContent = 2;
}
}else if (setImmediate) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
}else{
timerFunc = () => {
setTimeout(flushCallbacks, 0);
}
}
export function nextTick(cb) {
callbacks.push(cb);
timerFunc();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# watch实现原理
watch用于监控用户的data变化,数据变化后会触发对应的watch的回调方法
# 几种监听方式
- 1.直接key value;
- 2.写成key 和数组的方式;
- 3.监控当前实例上的方法;
- 4.handler的写法;
let vm = new Vue({
el: '#app',
data() {
return {
name: 'samy',//普通的watch
bet: { name: 'samy' }, //对象的watch
a: { a: { a: 1 } }, //对象的watch2
winChips: new Array(11).fill(0)
}
},
computed: {
// 只要bet中的属性发生变化(可被监测到的),便会执行handler函数;如果想监测具体的属性变化,如samy变化时,才执行handler函数,则可以利用计算属性computed做中间层。
pokerSamy() {
return this.bet.samy
}
},
watch: {
name(newValue, oldValue) {
console.log(newValue, oldValue);
},
bet: {
handler(newValue, oldValue) {
console.log(newValue)
},
deep: true
},
//对象具体属性的watch[活用computed]
pokerSamy(newValue, oldValue) {
console.log(newValue)
},
'a.a.a': {
handler(newValue, oldValue) {
console.log(newValue, oldValue, '内部watch')
}
},
winChips: {
handler(newValue, oldValue) {
for (let i = 0; i < newValue.length; i++) {
if (oldValue[i] != newValue[i]) {
console.log(newValue)
}
}
},
deep: true
}
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 初始化
选项中如果有watch则对watch进行初始化
if (opts.watch) {
initWatch(vm,opts.watch);
}
function initWatch(vm, watch) {
for (const key in watch) {
const handler = watch[key];
// 如果结果值是数组循环创建watcher
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,exprOrFn,handler,options){
// 如果是对象则提取函数 和配置
if(isObject(handler)){
options = handler;
handler = handler.handler;
}
// 如果是字符串就是实例上的函数
if(typeof handler == 'string'){
handler = vm[handler];
}
return vm.$watch(exprOrFn,handler,options);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
这里涉及了watch的三种写法,1.值是对象、2.值是数组、3.值是字符串 (如果是对象可以传入一些watch参数),最终会调用vm.$watch来实现
# 扩展原型$watch
扩展Vue原型上的方法,都通过mixin的方式来进行添加的。
stateMixin(Vue);
export function stateMixin(Vue) {
Vue.prototype.$watch = function(exprOrFn, cb, options = {}) {
options.user = true; // 标记为用户watcher
// 核心就是创建个watcher
const watcher = new Watcher(this, exprOrFn, cb, options);
if(options.immediate){
cb.call(vm,watcher.value)
}
}
}
2
3
4
5
6
7
8
9
10
11
# 构造Watcher
class Watcher {
constructor(vm, exprOrFn, callback, options) {
// ...
this.user = !! options.user
if(typeof exprOrFn === 'function'){
this.getter = exprOrFn;
}else{
this.getter = function (){ // 将表达式转换成函数
let path = exprOrFn.split('.');
let obj = vm;
for(let i = 0; i < path.length;i++){
obj = obj[path[i]];
}
return obj;
}
}
this.value = this.get(); // 将初始值记录到value属性上
}
get() {
pushTarget(this); // 把用户定义的watcher存起来
const value = this.getter.call(this.vm); // 执行函数 (依赖收集)
popTarget(); // 移除watcher
return value;
}
run(){
let value = this.get(); // 获取新值
let oldValue = this.value; // 获取老值
this.value = value;
if(this.user){ // 如果是用户watcher 则调用用户传入的callback
this.callback.call(this.vm,value,oldValue)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
还是借助vue响应式原理,默认在取值时将watcher存放到对应属性的dep中,当数据发生变化时通知对应的watcher重新执行
# computed实现原理
# 初始化
if (opts.computed) {
initComputed(vm,opts.computed);
}
2
3
function initComputed(vm, computed) {
// 存放计算属性的watcher
const watchers = vm._computedWatchers = {};
for (const key in computed) {
const userDef = computed[key];
// 获取get方法
const getter = typeof userDef === 'function' ? userDef : userDef.get;
// 创建计算属性watcher
watchers[key] = new Watcher(vm, userDef, () => {}, { lazy: true });
defineComputed(vm, key, userDef)
}
}
2
3
4
5
6
7
8
9
10
11
12
# dirty/lazy
每个计算属性也都是一个
watcher
,计算属性需要表示lazy:true,这样在初始化watcher时不会立即调用计算属性方法
class Watcher {
constructor(vm, exprOrFn, callback, options) {
this.vm = vm;
this.dirty = this.lazy
// ...
this.value = this.lazy ? undefined : this.get(); // 调用get方法 会让渲染watcher执行
}
}
2
3
4
5
6
7
8
默认计算属性需要保存一个dirty属性,用来实现缓存功能
function defineComputed(target, key, userDef) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key)
} else {
sharedPropertyDefinition.get = createComputedGetter(userDef.get);
sharedPropertyDefinition.set = userDef.set;
}
// 使用defineProperty定义
Object.defineProperty(target, key, sharedPropertyDefinition)
}
2
3
4
5
6
7
8
9
10
# 创建缓存getter
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) { // 如果dirty为true
watcher.evaluate();// 计算出新值,并将dirty 更新为false
}
// 如果依赖的值不发生变化,则返回上次计算的结果
return watcher.value
}
}
}
2
3
4
5
6
7
8
9
10
11
12
watcher.evaluate
evaluate() {
this.value = this.get()
this.dirty = false
}
2
3
4
update() {
if (this.lazy) {
this.dirty = true;
} else {
queueWatcher(this);
}
}
2
3
4
5
6
7
当依赖的属性变化时,会通知watcher调用update方法,此时我们将dirty置换为true。这样再取值时会重新进行计算。
# 监听watcher
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) { // 计算属性在模板中使用 则存在Dep.target
watcher.depend()
}
return watcher.value
}
2
3
4
5
6
7
8
9
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
2
3
4
5
6
如果计算属性在模板中使用,就让计算属性中依赖的数据也记录渲染watcher,这样依赖的属性发生变化也可以让视图进行刷新
# diff算法实现原理
# 使用及步骤流程
想掌握vue中的diff算法就先构建出两个虚拟
dom
之后做patch
import {compileToFunction} from './compiler/index.js';
import { patch,createElm } from './vdom/patch';
// 1.创建第一个虚拟节点
let vm1 = new Vue({data:{name:'zf'}});
let render1 = compileToFunction('<div>{{name}}</div>')
let oldVnode = render1.call(vm1)
// 2.创建第二个虚拟节点
let vm2 = new Vue({data:{name:'jw'}});
let render2 = compileToFunction('<p>{{name}}</p>');
let newVnode = render2.call(vm2);
// 3.通过第一个虚拟节点做首次渲染
let el = createElm(oldVnode)
document.body.appendChild(el);
// 4.调用patch方法进行对比操作
patch(oldVnode,newVnode);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Diff 算法复杂度
- diff 算法是 vdom 中最核心、最关键的部分
- diff 算法能在日常使用 Vue React 中体现出来(循环的 key)
# 优化前 树 diff 的时间复杂度 (n^3)
- 遍历Tree1,遍历Tree2
- 排序
- 假设有1000个节点,就要计算1亿次,算法不可用
# 优化后时间复杂度 (n^1)
- 只比较同一层级,不跨级比较
- tag 不相同,则直接删掉重建,不再深度比较
- tag 和 key,两者都相同,则认为是相同节点,不再深度比较
# 基本Diff算法
# 1.比对标签
// 如果标签不一致说明是两个不同元素
if(oldVnode.tag !== vnode.tag){
oldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el)
}
2
3
4
在diff过程中会先比较标签是否一致,如果标签不一致用新的标签替换掉老的标签
// 如果标签一致但是不存在则是文本节点
if(!oldVnode.tag){
if(oldVnode.text !== vnode.text){
oldVnode.el.textContent = vnode.text;
}
}
2
3
4
5
6
如果标签一致,有可能都是文本节点,那就比较文本的内容即可;
标签一样 并且需要开始比对标签的属性和儿子;
# 2.比对属性和样式
当标签相同时,我们可以复用老的标签元素,并且进行属性的比对。
- 属性处理:老的有,新的没有 需要删除属性;
- 样式处理:老的样式中有 新的没有 删除老的样式;
- 老新的有:那就直接用新的去做更新即可;
// 复用标签,并且更新属性
let el = vnode.el = oldVnode.el;
updateProperties(vnode,oldVnode.data);
function updateProperties(vnode,oldProps={}) {
let newProps = vnode.data || {};
let el = vnode.el;
// 比对样式
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) {
el.style[styleName] = newProps.style[styleName];
}
} else if (key === 'class') {
el.className = newProps.class;
} else {
el.setAttribute(key, newProps[key]);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 3.比对子元素
儿子比较分为以下几种情况
- 老的有儿子 新的没儿子 ;
- 老的没儿子 新的有儿子;
- 老新都有儿子,用diff算法,头尾双指针处理;
- 先头头,尾尾对比;
- 再头尾对比
// 比较孩子节点
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
// 新老都有需要比对儿子
if(oldChildren.length > 0 && newChildren.length > 0){
// 老的有儿子新的没有清空即可
}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));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这里要判断新老节点儿子的状况
if (oldChildren.length > 0 && newChildren.length > 0) {
updateChildren(el, oldChildren, newChildren)
// 老的有儿子新的没有清空即可
}
2
3
4
# Diff中优化策略
# 1.在开头和结尾新增元素
function isSameVnode(oldVnode,newVnode){
// 如果两个人的标签和key 一样我认为是同一个节点 虚拟节点一样我就可以复用真实节点了
return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
}
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];
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// 优化向后追加逻辑
if(isSameVnode(oldStartVnode,newStartVnode)){
patch(oldStartVnode,newStartVnode);
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
// 优化向前追加逻辑
}else if(isSameVnode(oldEndVnode,newEndVnode)){
patch(oldEndVnode,newEndVnode); // 比较孩子
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
}
}
if(newStartIndex <= newEndIndex){
for(let i = newStartIndex ; i<=newEndIndex ;i++){
let ele = newChildren[newEndIndex+1] == null? null:newChildren[newEndIndex+1].el;
parent.insertBefore(createElm(newChildren[i]),ele);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 2.头移尾、尾移头
// 头移动到尾部
else if(isSameVnode(oldStartVnode,newEndVnode)){
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]
}
2
3
4
5
6
7
8
9
10
11
12
13
以上四个条件对常见的dom操作进行了优化。
# 3.暴力比对
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
map[item.key] = index
});
return map;
}
let map = makeIndexByKey(oldChildren);
2
3
4
5
6
7
8
对所有的孩子元素进行编号
let moveIndex = map[newStartVnode.key];
if (moveIndex == undefined) { // 老的中没有将新元素插入
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else { // 有的话做移动操作
let moveVnode = oldChildren[moveIndex];
oldChildren[moveIndex] = undefined;
parent.insertBefore(moveVnode.el, oldStartVnode.el);
patch(moveVnode, newStartVnode);
}
newStartVnode = newChildren[++newStartIndex]
2
3
4
5
6
7
8
9
10
用新的元素去老的中进行查找,如果找到则移动,找不到则直接插入
if(oldStartIndex <= oldEndIndex){
for(let i = oldStartIndex; i<=oldEndIndex;i++){
let child = oldChildren[i];
if(child != undefined){
parent.removeChild(child.el)
}
}
}
2
3
4
5
6
7
8
如果有剩余则直接删除
if(!oldStartVnode){
oldStartVnode = oldChildren[++oldStartIndex];
}else if(!oldEndVnode){
oldEndVnode = oldChildren[--oldEndIndex]
}
2
3
4
5
在比对过程中,可能出现空值情况则直接跳过
# 更新操作
prevVnode做缓存处理;没有创建过则创建虚拟节点,已经有了的话则通过diff算法对比;
Vue.prototype._update = function (vnode) {
const vm = this;
const prevVnode = vm._vnode; // 保留上一次的vnode
vm._vnode = vnode;
if(!prevVnode){
vm.$el = patch(vm.$el,vnode); // 需要用虚拟节点创建出真实节点 替换掉 真实的$el
// 我要通过虚拟节点 渲染出真实的dom
}else{
vm.$el = patch(prevVnode,vnode); // 更新时做diff操作
}
}
2
3
4
5
6
7
8
9
10
11
# 组件实现原理
# 全局组件的解析
<div id="app">
<my-component></my-component>
<my-component></my-component>
</div>
<script>
Vue.component('my-component',{
template:'<button>点我</button>',
});
let vm = new Vue({
el:'#app'
});
</script>
2
3
4
5
6
7
8
9
10
11
12
我们可以通过
Vue.component
注册全局组件,之后可以在模板中进行使用
/**
* 静态方法部分
* Vue.component Vue.directive Vue.extend Vue.mixin ...
*/
initGlobalAPI(Vue); // 混合全局的api
export default Vue;
export function initGlobalAPI(Vue){
// 整合了所有的全局相关的内容
Vue.options ={}
initMixin(Vue);
// _base 就是Vue的构造函数
Vue.options._base = Vue;
Vue.options.components = {}
// 注册API方法
initAssetRegisters(Vue);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1.Vue.component
方法
export default function initAssetRegisters(Vue) {
Vue.component = function (id, definition) {
definition.name = definition.name || id;
definition = this.options._base.extend(definition);
this.options['components'][id] = definition;
}
}
2
3
4
5
6
7
Vue.component
内部会调用Vue.extend
方法,将定义挂载到Vue.options.components
上。这也说明所有的全局组件最终都会挂载到这个变量上
export function initGlobalAPI(Vue){
// 整合了所有的全局相关的内容
Vue.options ={}
initMixin(Vue);
// _base 就是Vue的构造函数
Vue.options._base = Vue;
Vue.options.components = {}
// initExtend
+ initExtend(Vue);
// 注册API方法
initAssetRegisters(Vue);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 2.Vue.extend
方法
继承的实现:
Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub;
import {mergeOptions} from '../util/index'
export default function initExtend(Vue) {
let cid = 0;
Vue.extend = function (extendOptions) {
const Super = this;
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
);
return Sub
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extend
方法就是创建出一个子类,继承于Vue
,并返回这个类
# 3.属性合并
function mergeAssets(parentVal,childVal){
const res = Object.create(parentVal);
if(childVal){
for(let key in childVal){
res[key] = childVal[key];
}
}
return res;
}
strats.components = mergeAssets;
2
3
4
5
6
7
8
9
10
# 4.初始化合并
vm.$options = mergeOptions(vm.constructor.options,options);
# 组件的渲染
function makeMap(str) {
const map = {};
const list = str.split(',');
for (let i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return (key)=>map[key];
}
export const isReservedTag = makeMap(
'a,div,img,image,text,span,input,p,button'
)
2
3
4
5
6
7
8
9
10
11
12
在创建虚拟节点时我们要判断当前这个标签是否是组件,普通标签的虚拟节点和组件的虚拟节点有所不同
# 1.创建组件虚拟节点
export function createElement(vm,tag, data = {}, ...children) {
let key = data.key;
if (key) {
delete data.key;
}
if (typeof tag === 'string') {
if (isReservedTag(tag)) {
return vnode(tag, data, key, children, undefined);
} else {
// 如果是组件需要拿到组件的定义,通过组件的定义创造虚拟节点
let Ctor = vm.$options.components[tag];
return createComponent(vm,tag,data,key,children,Ctor)
}
}
}
function createComponent(vm,tag,data,key,children,Ctor){
// 获取父类构造函数t
const baseCtor = vm.$options._base;
if(isObject(Ctor)){
Ctor = baseCtor.extend(Ctor);
}
data.hook = { // 组件的生命周期钩子
init(){}
}
return vnode(`vue-component-${Ctor.cid}-${tag}`,data,key,undefined,{Ctor,children});
}
function vnode(tag, data, key, children, text, componentOptions) {
return {tag, data, key, children, text, componentOptions}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 2.创建组件的真实节点
export function patch(oldVnode,vnode){
// 1.判断是更新还是要渲染
if(!oldVnode){
return createElm(vnode);
}else{
// ...
}
}
function createElm(vnode){ // 根据虚拟节点创建真实的节点
let {tag,children,key,data,text} = vnode;
// 是标签就创建标签
if(typeof tag === 'string'){
// createElm需要返回真实节点
if(createComponent(vnode)){
return vnode.componentInstance.$el;
}
vnode.el = document.createElement(tag);
updateProperties(vnode);
children.forEach(child=>{ // 递归创建儿子节点,将儿子节点扔到父节点中
return vnode.el.appendChild(createElm(child))
})
}else{
// 虚拟dom上映射着真实dom 方便后续更新操作
vnode.el = document.createTextNode(text)
}
// 如果不是标签就是文本
return vnode.el;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function createComponent(vnode) {
let i = vnode.data;
if((i = i.hook) && (i = i.init)){
i(vnode);
}
if(vnode.componentInstance){
return true;
}
}
2
3
4
5
6
7
8
9
调用
init
方法,进行组件的初始化
data.hook = {
init(vnode){
let child = vnode.componentInstance = new Ctor({});
child.$mount(); // 组件的挂载
}
}
2
3
4
5
6
# 其他原理相关【要点】
#
vm.$on,vm.$off,vm.$once,vm.$emit
实现原理; 这四个实现方法是一个很标准的订阅发布模式
# vm.$on
监听当前实例上的自定义事件。事件可以由
vm.$emit
触发。回调函数会接收所有传入事件触发函数的额外参数。
Vue.prototype.$on = function (event, fn) {
const vm = this
if (Array.isArray(event)) {
for (let i = 0; i < event.length; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push[fn]
}
return vm
}
2
3
4
5
6
7
8
9
10
11
温馨提示:_events是实现初始化的时候定义的,this.events = Object.create(null),所以不要困惑_events哪里来的。
vm.$on('test', function (msg) {
console.log(msg)
})
vm.$emit('test', 'hi')
// => "hi"
2
3
4
5
# vm.$off
移除自定义事件监听器。
- 如果没有提供参数,则移除所有的事件监听器;
- 如果只提供了事件,则移除该事件所有的监听器;
- 如果同时提供了事件与回调,则只移除这个回调的监听器。
Vue.prototype.$off = function (event, fn) {
const vm = this
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
if (Array.isArray(event)) {
for (let i = 0; i < event.length; i++) {
vm.$off(event[i], fn)
}
return vm
}
const cbs = vm._events[event]
if (!cbs) return vm
if (arguments.length === 1) {
vm._events[event] = null
return vm
}
if (fn) {
let len = cbs.length
while (len--) {
let cb = cbs[len]
if (cb === fn || cb.fn === fn) {
cbs.splice(len, 1)
break
}
}
}
return vm
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# vm.$once
监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。
Vue.prototype.$once = function (event,fn){
const vm = this
function on(){
vm.$off(event,on)
fn.apply(vm,arguments)
}
on.fn = fn
vm.$on(event,on)
return vm
}
2
3
4
5
6
7
8
9
10
# vm.$emit
触发当前实例上的事件。附加参数都会传给监听器回调。
Vue.prototype.$emit = function (event, ...params) {
const vm = this
let cbs = vm._events[event]
if (cbs) {
for (let i = 0; i < cbs.length; i++) {
cbs[i].apply(vm, params)
}
}
return vm
}
2
3
4
5
6
7
8
9
10
# vm.$delete
1.target如果是数组的话并且key是合法的,那就通过splice去改变数组
2.target如果是vue实例.或者是this.$data,那就直接退出程序
3.target如果不是双向绑定数据,那就直接delete就行不需要,通知watcher
4.以上条件都不满足,那么target就是双向绑定数据,delete之后通知watcher
function del(target, key) {
if (Array.isArray(target) && key > 0) {
target.splice(key, 1)
return
}
const ob = target.__ob__
if ((target._isVue && (ob && ob.vmCount)) || !target.hasOwnProperty(key)) return
delete target[key]
if (!ob) return
ob.dep.notify()
}
2
3
4
5
6
7
8
9
10
11
# vm.$nextTick
nextTick主要是通过js eventLoop的执行机制原理,将回调通过(promise)添加到microTask上面,来实现,在下一次DOM周期后执行回调函数。
const callback = []
let pendding = false
function nextTick(cb){
if(cb){
callback.push(()=>{cb()})
}
if(!pendding){
pendding = true
promise.resolve().then(()=>{
padding =false
const copies = callback.slice(0)
callback.length = 0
for(let i = 0;i < copies.length;i++){
copies[i]()
}
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
new Vue({ // 示例
methods: {
example: function () {
this.message = 'changed'// 修改数据
this.$nextTick(function () {// DOM 还没有更新
this.doSomethingElse() // DOM 现在更新了// `this` 绑定到当前实例
})
}
}
})
2
3
4
5
6
7
8
9
10
# 指令的执行原理
- 在模版阶段,会将节点上的指令解析处理并添加到AST的directives属性中。
- 随后directives数据会传到Vnode中,接着就可以通过vnode.data.directives获取一个节点所绑定的指令。
- 最后,当VDom进行修补时,会根据节点的对比结果触发一些钩子函数。更新指令的程序会监听create,update,destory钩子函数,并在这三个钩子函数出发时对VNode和oldVNode进行对比,最终根据对比触发指令的钩子函数。
# vue中源码大纲
instance/index.js
真正的Vue的构造函数,并在Vue的原型上扩展方法core/index.js
增加全局API
方法runtime/index.js
扩展$mount
方法及平台对应的代码
# 总括
# 几个核心思想
- 数据驱动
- 组件化
- 虚拟dom、diff局部最优更新
# 核心知识点
- 数据驱动分析
- Vue 实例化
- Vue 实例挂载
- render
- Virtual DOM
- createElement
- update
- 组件化分析
- createComponent
- patch
- 合并配置
- 生命周期
- 组件注册
- 异步组件
- 响应式原理分析
- 响应式对象 / 依赖收集
- 派发更新 / nextTick
- 检测变化的注意事项
- 计算属性 VS 侦听属性
- 组件更新
# 总体把握
- template风格、对象配置风格
- 虚拟dom思想(js对象操作代替dom操作)
- diff算法思想(同层比较,添加、移动、删除)
- 组件化思想(组件编译、组件通信)
- 数据响应式(依赖收集、派发更新,发布订阅)
# 相关链接
http://www.zhufengpeixun.com/jg-vue/vue-analyse/source.html